Compare commits
536 Commits
v0.2.0
...
663/master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57f81b7c23 | ||
|
|
ad288f5168 | ||
|
|
c5d387cb53 | ||
|
|
2f4413be6e | ||
|
|
83d3ec73e7 | ||
|
|
cf8eda04a1 | ||
|
|
6bdb97c55c | ||
|
|
7a6c3d9db1 | ||
|
|
bfcc5504bb | ||
|
|
669a4d5652 | ||
|
|
b808305507 | ||
|
|
a04b190251 | ||
|
|
e869869462 | ||
|
|
20c0051026 | ||
|
|
284b0b8de0 | ||
|
|
130bdf8337 | ||
|
|
8b7b7881f5 | ||
|
|
28a8435a52 | ||
|
|
dca9871744 | ||
|
|
6c2fbbf275 | ||
|
|
43bac80e4d | ||
|
|
0bf6af17e7 | ||
|
|
f7af8a3863 | ||
|
|
492af7a92d | ||
|
|
de4f2b9990 | ||
|
|
4a2ff204ec | ||
|
|
26dbb29b3d | ||
|
|
231aae2920 | ||
|
|
4cc7380a88 | ||
|
|
593fd29d00 | ||
|
|
f84d97b17b | ||
|
|
ef4d743af3 | ||
|
|
9ecc4a15df | ||
|
|
e165025c94 | ||
|
|
d711f2aef3 | ||
|
|
e724bec987 | ||
|
|
40b3543c3f | ||
|
|
e95b5127ca | ||
|
|
5243aa0628 | ||
|
|
509d18501c | ||
|
|
77067bdc58 | ||
|
|
358b50ba21 | ||
|
|
b40ca44e1a | ||
|
|
a68d621f2d | ||
|
|
21ca38d9e9 | ||
|
|
769efc20d1 | ||
|
|
32e416ea95 | ||
|
|
49e0f4e983 | ||
|
|
e08b466166 | ||
|
|
3f9935bbcc | ||
|
|
ef8bc7c5a8 | ||
|
|
fa02cf0f2b | ||
|
|
bc66a27baf | ||
|
|
3e54ac3aca | ||
|
|
a425102cf3 | ||
|
|
60cd28005f | ||
|
|
728f82c084 | ||
|
|
4437835057 | ||
|
|
1cc405d2dc | ||
|
|
2f0d549a50 | ||
|
|
c7aca64ba1 | ||
|
|
548961f610 | ||
|
|
b3072ce354 | ||
|
|
98b6b1911c | ||
|
|
ef96109ea5 | ||
|
|
cf1a759fa5 | ||
|
|
86c3fc9fac | ||
|
|
5f12f06297 | ||
|
|
33b3f4e312 | ||
|
|
603ec3f10a | ||
|
|
37bb82e87d | ||
|
|
047e0b7e8d | ||
|
|
782820c34a | ||
|
|
26cf1f7a89 | ||
|
|
f7ab4f04ac | ||
|
|
2751f08bdc | ||
|
|
0904d448e0 | ||
|
|
fab7952af6 | ||
|
|
6af75d6d40 | ||
|
|
5f1a37f0db | ||
|
|
b7bd3051b1 | ||
|
|
7adc3fe19b | ||
|
|
4842885aa6 | ||
|
|
00e8c0d1b0 | ||
|
|
239efa5fbd | ||
|
|
a9ba23bae8 | ||
|
|
62930f2821 | ||
|
|
4334c71bc7 | ||
|
|
21a029f17e | ||
|
|
9d37c3bd45 | ||
|
|
343ec220b4 | ||
|
|
2da4c10384 | ||
|
|
829dfee9e5 | ||
|
|
26c7bcfdd5 | ||
|
|
cf1b05d16e | ||
|
|
cfa8b8042a | ||
|
|
f0c0985708 | ||
|
|
73c937bc30 | ||
|
|
33e67abbe9 | ||
|
|
c20fb723ef | ||
|
|
ccd142df97 | ||
|
|
f6dbd1c0b5 | ||
|
|
d38d185d1c | ||
|
|
ef79b72471 | ||
|
|
ed12ab16e0 | ||
|
|
24820cfcff | ||
|
|
e10f62663e | ||
|
|
02573b0ad2 | ||
|
|
0dc39434c2 | ||
|
|
33acfce083 | ||
|
|
79d0eadbd6 | ||
|
|
73f7f16298 | ||
|
|
66eb0e42fe | ||
|
|
052ae53b6e | ||
|
|
1c0ed3268b | ||
|
|
0456abb327 | ||
|
|
ec50458491 | ||
|
|
b834ccaa2f | ||
|
|
ffb3de6c36 | ||
|
|
454c8459f2 | ||
|
|
94a0d09591 | ||
|
|
9b7a6ed85d | ||
|
|
7e31035114 | ||
|
|
e15a6146f8 | ||
|
|
e49cb1126b | ||
|
|
142bc5720e | ||
|
|
feaeb7870f | ||
|
|
9df0eefe49 | ||
|
|
8e89a9377a | ||
|
|
d3df8fe7ef | ||
|
|
9feda988a5 | ||
|
|
9534d533e3 | ||
|
|
85eefe1d8b | ||
|
|
3343270680 | ||
|
|
bf9d502742 | ||
|
|
85c0779ac0 | ||
|
|
070de44069 | ||
|
|
33087e3a99 | ||
|
|
fe4eb5e771 | ||
|
|
2fead23556 | ||
|
|
bc5a9e4c06 | ||
|
|
fafad6c961 | ||
|
|
a4de409235 | ||
|
|
a05fd45959 | ||
|
|
24de2f8a96 | ||
|
|
eee37011a5 | ||
|
|
a67706bea0 | ||
|
|
faa69b6cfe | ||
|
|
ba5ea2deff | ||
|
|
a6b25a4877 | ||
|
|
90d8cb6526 | ||
|
|
e71faa988e | ||
|
|
ed0ae81aae | ||
|
|
a61b078dea | ||
|
|
85939306e3 | ||
|
|
cf2d9c2c1d | ||
|
|
853d9047b0 | ||
|
|
6069d89dee | ||
|
|
d25e263b8e | ||
|
|
d05e696d45 | ||
|
|
ef583cead9 | ||
|
|
90c4da4e68 | ||
|
|
8032191366 | ||
|
|
c8c03294e1 | ||
|
|
e00df22588 | ||
|
|
9806217a6a | ||
|
|
1be5cf2d90 | ||
|
|
ca68bae4ed | ||
|
|
8c1f58079f | ||
|
|
4845c03eec | ||
|
|
532a595c41 | ||
|
|
25ce5bc90b | ||
|
|
80a929ccc6 | ||
|
|
3797863e14 | ||
|
|
7870793b4b | ||
|
|
a7c21a9729 | ||
|
|
914d54e672 | ||
|
|
a68e38e59e | ||
|
|
e870e5d8a5 | ||
|
|
29387e785c | ||
|
|
8eb6336f5e | ||
|
|
34a2be6458 | ||
|
|
fbd834469f | ||
|
|
8da5f740af | ||
|
|
38dcddb126 | ||
|
|
92948d2394 | ||
|
|
a3a0a80a02 | ||
|
|
a5f7019b2a | ||
|
|
e05b80cec1 | ||
|
|
23d5fbde56 | ||
|
|
a346704cdc | ||
|
|
24396d97ed | ||
|
|
703e41cd49 | ||
|
|
975c4165d0 | ||
|
|
dbf38d847a | ||
|
|
91a2519cc3 | ||
|
|
a1c3ba2088 | ||
|
|
d47565be5c | ||
|
|
1028d39db0 | ||
|
|
b250825c38 | ||
|
|
90a6a8f2d6 | ||
|
|
414386e797 | ||
|
|
3a843d5074 | ||
|
|
4e76bfa2ca | ||
|
|
8832281dcf | ||
|
|
853f3d9200 | ||
|
|
67e996c5f4 | ||
|
|
f09863faa0 | ||
|
|
eb1e3be722 | ||
|
|
4ec902b96f | ||
|
|
74243394d9 | ||
|
|
e7f263efa7 | ||
|
|
0991145c58 | ||
|
|
01d2a8588a | ||
|
|
45431a2649 | ||
|
|
0b78fb9201 | ||
|
|
9cdff275cb | ||
|
|
77c6e106e4 | ||
|
|
efdd6bfb19 | ||
|
|
117098d2d2 | ||
|
|
f933d892aa | ||
|
|
5ea54792c0 | ||
|
|
23a9280db7 | ||
|
|
79e27b1778 | ||
|
|
0a05579a1c | ||
|
|
0030eb4a13 | ||
|
|
5bf40343eb | ||
|
|
1e35f983c4 | ||
|
|
a15ac8870b | ||
|
|
8a27036a54 | ||
|
|
8543523f18 | ||
|
|
5a9b59866b | ||
|
|
dc76956215 | ||
|
|
98fb5e4bbd | ||
|
|
25ff2e5e61 | ||
|
|
5050f1ce1c | ||
|
|
51b691e7ac | ||
|
|
c4cd0a5f31 | ||
|
|
41142732ec | ||
|
|
62495c3bd1 | ||
|
|
d00184a7c3 | ||
|
|
ce32d5537d | ||
|
|
25921fa91a | ||
|
|
932a496c3c | ||
|
|
57862eeda6 | ||
|
|
11df94d601 | ||
|
|
0abaa20de9 | ||
|
|
c35a1dd79f | ||
|
|
e0b2572eba | ||
|
|
aada695b3f | ||
|
|
90f3858eff | ||
|
|
ecb482f297 | ||
|
|
641f391137 | ||
|
|
dc26f7ba9f | ||
|
|
6504930888 | ||
|
|
6b52c91257 | ||
|
|
0ffea495b1 | ||
|
|
72ba4ff2d4 | ||
|
|
88c4b191fb | ||
|
|
112d2a65f6 | ||
|
|
d999c1b434 | ||
|
|
3aa8b9a259 | ||
|
|
fdbea9e2ee | ||
|
|
6204eddade | ||
|
|
e789c671b0 | ||
|
|
8c2ee0ed85 | ||
|
|
2b48409cfd | ||
|
|
7251186762 | ||
|
|
82fda4ac0e | ||
|
|
1d12ddbdfc | ||
|
|
f474c76e19 | ||
|
|
ac99104114 | ||
|
|
0bb9b388f7 | ||
|
|
b59e4bb808 | ||
|
|
4fe647df0a | ||
|
|
a00350ab54 | ||
|
|
96c6b4efcb | ||
|
|
18714caa60 | ||
|
|
7110fe0159 | ||
|
|
5a590bca74 | ||
|
|
963f11a6b1 | ||
|
|
a7761fe55d | ||
|
|
10cf9305f1 | ||
|
|
b72ced4511 | ||
|
|
eb47c778db | ||
|
|
359b7feb8c | ||
|
|
6ffdede95a | ||
|
|
4db0250b95 | ||
|
|
69780bbbec | ||
|
|
fda89d6859 | ||
|
|
5d99b4af00 | ||
|
|
da4d4e1672 | ||
|
|
8f9aa276e8 | ||
|
|
8debb0d338 | ||
|
|
bc2a512101 | ||
|
|
4f728d363f | ||
|
|
e81af75427 | ||
|
|
8387b32bb8 | ||
|
|
2fccee740b | ||
|
|
c98002eb76 | ||
|
|
584e1b0500 | ||
|
|
cee65ed283 | ||
|
|
8104b17ee6 | ||
|
|
7676d3c7df | ||
|
|
3e6211e0a3 | ||
|
|
05c472b741 | ||
|
|
867ba1fd8c | ||
|
|
fd48719040 | ||
|
|
d987225ac8 | ||
|
|
503bdeeadb | ||
|
|
d3f1669234 | ||
|
|
3f62ce9c19 | ||
|
|
278c153d31 | ||
|
|
ae677099d6 | ||
|
|
140db9b2e2 | ||
|
|
a6b35031ae | ||
|
|
004cf2687a | ||
|
|
cf8db5ea23 | ||
|
|
1683e8d609 | ||
|
|
f372e034e8 | ||
|
|
8c3db49fba | ||
|
|
02b1aac0b0 | ||
|
|
6cb57f5d2a | ||
|
|
67dd1ac608 | ||
|
|
808a5c9ffd | ||
|
|
d16db5ed90 | ||
|
|
6e24f9d47b | ||
|
|
92ab09496a | ||
|
|
28017f97ea | ||
|
|
ea43413507 | ||
|
|
829b7b6b70 | ||
|
|
262bf441ce | ||
|
|
7aae9b380e | ||
|
|
d50327548b | ||
|
|
e6ce0ab9a7 | ||
|
|
9085c81e76 | ||
|
|
682349c03e | ||
|
|
a72389b28c | ||
|
|
f1bc00b67f | ||
|
|
06d159fb7b | ||
|
|
578560766d | ||
|
|
9e5c924ef1 | ||
|
|
cf39de882a | ||
|
|
8293cef703 | ||
|
|
60b99cfc66 | ||
|
|
7cc4189eb0 | ||
|
|
86d4a32314 | ||
|
|
67c9c64eab | ||
|
|
b8d0f947e8 | ||
|
|
bbd4363fa9 | ||
|
|
e0083fb8de | ||
|
|
3abafc307c | ||
|
|
055af0f78a | ||
|
|
e4873e4da9 | ||
|
|
2233cdc9cc | ||
|
|
816bc9b5c8 | ||
|
|
a82c82fcd7 | ||
|
|
bb28d02277 | ||
|
|
94877f4e7e | ||
|
|
3747ddbefb | ||
|
|
42731da546 | ||
|
|
e183d63a5e | ||
|
|
e5fdd442c3 | ||
|
|
97357c0e08 | ||
|
|
8649ce4c78 | ||
|
|
a0f6605f59 | ||
|
|
db9b1dd689 | ||
|
|
9c8d62151b | ||
|
|
c44d521279 | ||
|
|
ba9da05cef | ||
|
|
abd552fde6 | ||
|
|
3726761549 | ||
|
|
06c7145ac5 | ||
|
|
85f74dd802 | ||
|
|
86f681a007 | ||
|
|
bd5862437d | ||
|
|
a3827aaeae | ||
|
|
47c68e40a2 | ||
|
|
2a7eec816a | ||
|
|
fe0ddf6c83 | ||
|
|
9a73ead88d | ||
|
|
8fbb764c9e | ||
|
|
4756801fd9 | ||
|
|
f0e0b515ad | ||
|
|
25a0825ae4 | ||
|
|
b1ac297d71 | ||
|
|
2dfe9c1663 | ||
|
|
8a9c76b003 | ||
|
|
41cdd3e261 | ||
|
|
fe17165c39 | ||
|
|
e18671c1e4 | ||
|
|
b5f6219d39 | ||
|
|
5ed82aac5f | ||
|
|
f6a0a91a23 | ||
|
|
5645d0de03 | ||
|
|
ffaaf5e39c | ||
|
|
567cf7b8e5 | ||
|
|
5f8dd38135 | ||
|
|
a74d335cb4 | ||
|
|
6d594143ed | ||
|
|
7a5ad3fbdb | ||
|
|
584f7688f4 | ||
|
|
4436110c44 | ||
|
|
8a7c9d49b2 | ||
|
|
b5d41caace | ||
|
|
206813d560 | ||
|
|
e0ab1e906e | ||
|
|
f8b3526426 | ||
|
|
d83baab433 | ||
|
|
43e38ac483 | ||
|
|
b079d4da4c | ||
|
|
21e79ca078 | ||
|
|
a25bbea555 | ||
|
|
b7664a4108 | ||
|
|
d360cd3434 | ||
|
|
e037db076c | ||
|
|
3ef19f41e6 | ||
|
|
da90ec15fa | ||
|
|
7f5af46300 | ||
|
|
624e6ee047 | ||
|
|
4a1f3cd61f | ||
|
|
7c4a3d2b02 | ||
|
|
8db1bb56f2 | ||
|
|
d75198a8ee | ||
|
|
cadb41c9e3 | ||
|
|
b30cae0473 | ||
|
|
7290086fe9 | ||
|
|
bca920bea0 | ||
|
|
32de7a3fdc | ||
|
|
f20512b599 | ||
|
|
cd41ca571f | ||
|
|
dc654e9f6c | ||
|
|
f5d7f70472 | ||
|
|
0168442c22 | ||
|
|
22579b77cc | ||
|
|
09c09d2fd1 | ||
|
|
b669cf9ce7 | ||
|
|
5bc617a9a6 | ||
|
|
a75b811061 | ||
|
|
ec6b46324e | ||
|
|
97f764b45d | ||
|
|
7f31a55506 | ||
|
|
2286d097dc | ||
|
|
52a40ec99a | ||
|
|
a78fa73b34 | ||
|
|
d7e4a252fb | ||
|
|
1c0b0abf61 | ||
|
|
f7c6620e25 | ||
|
|
16372f7847 | ||
|
|
72c2eb7182 | ||
|
|
144bfb71cf | ||
|
|
3fd9e23851 | ||
|
|
10642d0e04 | ||
|
|
063ab2f87d | ||
|
|
1802cf8dbc | ||
|
|
090975481b | ||
|
|
228816f5f8 | ||
|
|
8522e028f1 | ||
|
|
a2776dfc86 | ||
|
|
cc95c8cfb0 | ||
|
|
89dac9d2a6 | ||
|
|
8cdfc883b9 | ||
|
|
b3689eceb7 | ||
|
|
5cee2afc6d | ||
|
|
50fef0fb26 | ||
|
|
4c46ef69e9 | ||
|
|
22e8fade7e | ||
|
|
37aa06f508 | ||
|
|
f6d2f8f929 | ||
|
|
32947669d5 | ||
|
|
fdf3015ad0 | ||
|
|
03bfcde147 | ||
|
|
56fc43400a | ||
|
|
7b4d35d224 | ||
|
|
a99fc115f8 | ||
|
|
d8e5f57d53 | ||
|
|
aa85e597d9 | ||
|
|
08ab92da80 | ||
|
|
5d52fd2486 | ||
|
|
4ae9850e13 | ||
|
|
e14190ae4b | ||
|
|
ce445a8096 | ||
|
|
dd71d6471c | ||
|
|
f795173886 | ||
|
|
e42ab1fed8 | ||
|
|
0544c023f5 | ||
|
|
ff47f9480b | ||
|
|
70561b7c54 | ||
|
|
559c9c75f3 | ||
|
|
6c69160d6b | ||
|
|
d0cee47e22 | ||
|
|
ccebb56a83 | ||
|
|
cf169d1582 | ||
|
|
bcd1e30376 | ||
|
|
40bad7a718 | ||
|
|
3d63f9607f | ||
|
|
13e194cd26 | ||
|
|
d6016788ef | ||
|
|
ad602a54bf | ||
|
|
7181970a32 | ||
|
|
cfc90ab7f6 | ||
|
|
05c96eaa28 | ||
|
|
9a9f49f467 | ||
|
|
c552ae98b4 | ||
|
|
df7493fd33 | ||
|
|
5de571fb03 | ||
|
|
62df7badf3 | ||
|
|
597e219257 | ||
|
|
3f8a9079ee | ||
|
|
5981767543 | ||
|
|
36146d970a | ||
|
|
464ba4f334 | ||
|
|
36a5eb2110 | ||
|
|
55840210c7 | ||
|
|
3c38abb203 | ||
|
|
4816563452 | ||
|
|
24dc73912b | ||
|
|
f96db9c74f | ||
|
|
ef2054a45b | ||
|
|
f4052e0e71 | ||
|
|
524845cc74 | ||
|
|
4c356c5077 | ||
|
|
36dea8373f | ||
|
|
2cb823a15b | ||
|
|
169dc43565 | ||
|
|
4b53acab14 | ||
|
|
c3acac797a | ||
|
|
dd2bf0ad13 | ||
|
|
f620af1455 | ||
|
|
fcd1b7b187 | ||
|
|
d0d2f88346 | ||
|
|
c56173462a | ||
|
|
58074f23c5 | ||
|
|
f816e6bbc4 | ||
|
|
d53ecaeade | ||
|
|
299279dc2d |
16
.cargo-husky/hooks/pre-push
Executable file
16
.cargo-husky/hooks/pre-push
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if !(command cargo-make >/dev/null 2>&1); then # Check if cargo-make is installed
|
||||
echo Attempting to run cargo-make as part of the pre-push hook but it\'s not installed.
|
||||
echo Please install it by running the following command:
|
||||
echo
|
||||
echo " cargo install --force cargo-make"
|
||||
echo
|
||||
echo If you don\'t want to run cargo-make as part of the pre-push hook, you can run
|
||||
echo the following command instead of git push:
|
||||
echo
|
||||
echo " git push --no-verify"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cargo make ci
|
||||
78
.cz.toml
Normal file
78
.cz.toml
Normal file
@@ -0,0 +1,78 @@
|
||||
# configuration for https://github.com/commitizen/cz-cli
|
||||
|
||||
[tool.commitizen]
|
||||
name = "cz_customize"
|
||||
tag_format = "$version"
|
||||
version_type = "semver"
|
||||
version_provider = "cargo"
|
||||
update_changelog_on_bump = true
|
||||
major_version_zero = true
|
||||
use_shortcuts = true
|
||||
|
||||
[tool.commitizen.customize]
|
||||
message_template = """{{change_type}}({{scope}}): {{subject}}
|
||||
|
||||
{% if body %}\
|
||||
{{body}}\
|
||||
{% endif %}
|
||||
|
||||
{%if is_breaking_change %}\
|
||||
BREAKING_CHANGE: \
|
||||
{% endif %}\
|
||||
{{footer}}\
|
||||
"""
|
||||
example = "feature: this feature enable customize through config file"
|
||||
schema = "<type>(<scope>): <subject>\n\n<body>\n\n<footer>"
|
||||
schema_pattern = "(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)\\(\\w+\\):\\s(?P<subject>.*)(\\n\\n(?P<body>.*))?(\\n\\n(?P<footer>.*))?"
|
||||
|
||||
# The order needs to be preserved, as it influences the order when executing cz commit/cz c
|
||||
|
||||
# Change types
|
||||
[[tool.commitizen.customize.questions]]
|
||||
type = "list"
|
||||
name = "change_type"
|
||||
choices = [
|
||||
{ value = "build", name = "build: Changes that affect the build system or external dependencies (example scopes: pip, docker, npm)", key = "b" },
|
||||
{ value = "chore", name = "chore: A modification that generally does not fall into any other category", key = "c" },
|
||||
{ value = "ci", name = "ci: Changes to our CI configuration files and scripts (example scopes: GitLabCI)", key = "i" },
|
||||
{ value = "docs", name = "docs: Documentation only changes", key = "d" },
|
||||
{ value = "feat", name = "feat: A new feature.", key = "f" },
|
||||
{ value = "fix", name = "fix: A bug fix.", key = "x" },
|
||||
{ value = "perf", name = "perf: A code change that improves performance", key = "p" },
|
||||
{ value = "refactor", name = "refactor: A code change that neither fixes a bug nor adds a feature", key = "r" },
|
||||
{ value = "revert", name = "revert: Revert previous commits", key = "v" },
|
||||
{ value = "style", name = "style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)", key = "s" },
|
||||
{ value = "test", name = "test: Adding missing or correcting existing tests", key = "t" },
|
||||
]
|
||||
message = "Select the type of change you are committing"
|
||||
|
||||
# The scope of the change, can be a file, class name or other context
|
||||
[[tool.commitizen.customize.questions]]
|
||||
type = "input"
|
||||
name = "scope"
|
||||
message = "What is the scope of this change? (class or file name): (press [enter] to skip)\n"
|
||||
|
||||
# Summary of the changes
|
||||
[[tool.commitizen.customize.questions]]
|
||||
"type" = "input"
|
||||
"name" = "subject"
|
||||
"message" = "Write a short and imperative summary of the code changes: (lower case and no period)\n"
|
||||
|
||||
# The commit body, elaborate the changes if need be.
|
||||
[[tool.commitizen.customize.questions]]
|
||||
type = "input"
|
||||
name = "body"
|
||||
message = "Provide additional contextual information about the code changes: (press [enter] to skip)\n"
|
||||
|
||||
# Specify if the changes are breaking
|
||||
[[tool.commitizen.customize.questions]]
|
||||
type = "confirm"
|
||||
name = "is_breaking_change"
|
||||
message = "Is this a BREAKING CHANGE?"
|
||||
default = false
|
||||
|
||||
# Reference closing issues and share other
|
||||
[[tool.commitizen.customize.questions]]
|
||||
type = "input"
|
||||
name = "footer"
|
||||
message = "Footer. Information about Breaking Changes and reference issues that this commit closes: (press [enter] to skip)"
|
||||
8
.github/CODEOWNERS
vendored
Normal file
8
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# See https://help.github.com/articles/about-codeowners/
|
||||
# for more info about CODEOWNERS file
|
||||
|
||||
# It uses the same pattern rule for gitignore file
|
||||
# https://git-scm.com/docs/gitignore#_pattern_format
|
||||
|
||||
# Maintainers
|
||||
* @orhun @mindoodoo @sayanarijit @sophacles @joshka
|
||||
60
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
60
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create an issue about a bug you encountered
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
<!--
|
||||
Hi there, sorry `ratatui` is not working as expected.
|
||||
Please fill this bug report conscientiously.
|
||||
A detailed and complete issue is more likely to be processed quickly.
|
||||
-->
|
||||
|
||||
## Description
|
||||
<!--
|
||||
A clear and concise description of what the bug is.
|
||||
-->
|
||||
|
||||
|
||||
## To Reproduce
|
||||
<!--
|
||||
Try to reduce the issue to a simple code sample exhibiting the problem.
|
||||
Ideally, fork the project and add a test or an example.
|
||||
-->
|
||||
|
||||
|
||||
## Expected behavior
|
||||
<!--
|
||||
A clear and concise description of what you expected to happen.
|
||||
-->
|
||||
|
||||
|
||||
## Screenshots
|
||||
<!--
|
||||
If applicable, add screenshots, gifs or videos to help explain your problem.
|
||||
-->
|
||||
|
||||
|
||||
## Environment
|
||||
<!--
|
||||
Add a description of the systems where you are observing the issue. For example:
|
||||
- OS: Linux
|
||||
- Terminal Emulator: xterm
|
||||
- Font: Inconsolata (Patched)
|
||||
- Crate version: 0.7
|
||||
- Backend: termion
|
||||
-->
|
||||
|
||||
- OS:
|
||||
- Terminal Emulator:
|
||||
- Font:
|
||||
- Crate version:
|
||||
- Backend:
|
||||
|
||||
## Additional context
|
||||
<!--
|
||||
Add any other context about the problem here.
|
||||
If you already looked into the issue, include all the leads you have explored.
|
||||
-->
|
||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
32
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
32
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Problem
|
||||
<!--
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
-->
|
||||
|
||||
## Solution
|
||||
<!--
|
||||
A clear and concise description of what you want to happen.
|
||||
Things to consider:
|
||||
- backward compatibility
|
||||
- ease of use of the API (https://rust-lang.github.io/api-guidelines/)
|
||||
- consistency with the rest of the crate
|
||||
-->
|
||||
|
||||
## Alternatives
|
||||
<!--
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
-->
|
||||
|
||||
## Additional context
|
||||
<!--
|
||||
Add any other context or screenshots about the feature request here.
|
||||
-->
|
||||
1
.github/pull_request_template.md
vendored
Normal file
1
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<!-- Please read CONTRIBUTING.md before submitting any pull request. -->
|
||||
19
.github/workflows/cd.yml
vendored
Normal file
19
.github/workflows/cd.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: Continuous Deployment
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish on crates.io
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Publish
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: publish
|
||||
args: --token ${{ secrets.CARGO_TOKEN }}
|
||||
121
.github/workflows/ci.yml
vendored
Normal file
121
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,121 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- feat-wrapping
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- feat-wrapping
|
||||
merge_group:
|
||||
|
||||
|
||||
name: CI
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||
toolchain: [ "1.65.0", "stable" ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: lint
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- name: "Check"
|
||||
run: cargo make check
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
CARGO_HUSKY_DONT_INSTALL_HOOKS: true
|
||||
|
||||
clippy:
|
||||
name: Clippy
|
||||
runs-on: ubuntu-latest
|
||||
needs: lint
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- name: "Clippy"
|
||||
run: cargo make clippy
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
CARGO_HUSKY_DONT_INSTALL_HOOKS: true
|
||||
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||
toolchain: [ "1.65.0", "stable" ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: lint
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- name: "Test"
|
||||
run: cargo make test
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
CARGO_HUSKY_DONT_INSTALL_HOOKS: true
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: actions/checkout@v3
|
||||
- name: Checkout
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: "Check conventional commits"
|
||||
uses: crate-ci/committed@master
|
||||
with:
|
||||
args: "-vv"
|
||||
commits: "HEAD"
|
||||
- name: "Check typos"
|
||||
uses: crate-ci/typos@master
|
||||
- name: "Lint dependencies"
|
||||
uses: EmbarkStudios/cargo-deny-action@v1
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
components: rustfmt
|
||||
- name: "Formatting"
|
||||
run: cargo fmt --all --check
|
||||
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
needs: lint
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: llvm-tools
|
||||
- name: cargo install cargo-llvm-cov
|
||||
uses: taiki-e/install-action@cargo-llvm-cov
|
||||
- name: cargo llvm-cov
|
||||
run: cargo llvm-cov --all-features --lcov --output-path lcov.info
|
||||
env:
|
||||
CARGO_HUSKY_DONT_INSTALL_HOOKS: true
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@ Cargo.lock
|
||||
*.log
|
||||
*.rs.rustfmt
|
||||
.gdb_history
|
||||
.idea/
|
||||
|
||||
9
.markdownlint.yaml
Normal file
9
.markdownlint.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
# configuration for https://github.com/DavidAnson/markdownlint
|
||||
|
||||
no-inline-html:
|
||||
allowed_elements:
|
||||
- img
|
||||
- details
|
||||
- summary
|
||||
line-length:
|
||||
line_length: 100
|
||||
22
.travis.yml
22
.travis.yml
@@ -1,22 +0,0 @@
|
||||
language: rust
|
||||
|
||||
rust:
|
||||
- stable
|
||||
- beta
|
||||
- nightly
|
||||
|
||||
env:
|
||||
- NO_RUSTUP=1
|
||||
|
||||
cache: cargo
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- rust: nightly
|
||||
- rust: beta
|
||||
|
||||
before_script:
|
||||
- ./scripts/travis/before_script.sh
|
||||
|
||||
script:
|
||||
- ./scripts/travis/script.sh
|
||||
949
CHANGELOG.md
949
CHANGELOG.md
@@ -1,22 +1,955 @@
|
||||
# Changelog
|
||||
|
||||
## v0.21.0 - 2023-05-28
|
||||
|
||||
### Features
|
||||
|
||||
- *(backend)* Add termwiz backend and example ([#5](https://github.com/tui-rs-revival/ratatui/issues/5))
|
||||
- *(block)* Support placing the title on bottom ([#36](https://github.com/tui-rs-revival/ratatui/issues/36))
|
||||
- *(border)* Add border! macro for easy bitflag manipulation ([#11](https://github.com/tui-rs-revival/ratatui/issues/11))
|
||||
- *(calendar)* Add calendar widget ([#138](https://github.com/tui-rs-revival/ratatui/issues/138))
|
||||
- *(color)* Add `FromStr` implementation for `Color` ([#180](https://github.com/tui-rs-revival/ratatui/issues/180))
|
||||
- *(list)* Add len() to List ([#24](https://github.com/tui-rs-revival/ratatui/pull/24))
|
||||
- *(paragraph)* Allow Lines to be individually aligned ([#149](https://github.com/tui-rs-revival/ratatui/issues/149))
|
||||
- *(sparkline)* Finish #1 Sparkline directions PR ([#134](https://github.com/tui-rs-revival/ratatui/issues/134))
|
||||
- *(terminal)* Add inline viewport ([#114](https://github.com/tui-rs-revival/ratatui/issues/114)) [**breaking**]
|
||||
- *(test)* Expose test buffer ([#160](https://github.com/tui-rs-revival/ratatui/issues/160))
|
||||
- *(text)* Add `Masked` to display secure data ([#168](https://github.com/tui-rs-revival/ratatui/issues/168)) [**breaking**]
|
||||
- *(widget)* Add circle widget ([#159](https://github.com/tui-rs-revival/ratatui/issues/159))
|
||||
- *(widget)* Add style methods to Span, Spans, Text ([#148](https://github.com/tui-rs-revival/ratatui/issues/148))
|
||||
- *(widget)* Support adding padding to Block ([#20](https://github.com/tui-rs-revival/ratatui/issues/20))
|
||||
- *(widget)* Add offset() and offset_mut() for table and list state ([#12](https://github.com/tui-rs-revival/ratatui/issues/12))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- *(canvas)* Use full block for Marker::Block ([#133](https://github.com/tui-rs-revival/ratatui/issues/133)) [**breaking**]
|
||||
- *(example)* Update input in examples to only use press events ([#129](https://github.com/tui-rs-revival/ratatui/issues/129))
|
||||
- *(uncategorized)* Cleanup doc example ([#145](https://github.com/tui-rs-revival/ratatui/issues/145))
|
||||
- *(reflow)* Remove debug macro call ([#198](https://github.com/tui-rs-revival/ratatui/issues/198))
|
||||
|
||||
### Refactor
|
||||
|
||||
- *(example)* Remove redundant `vec![]` in `user_input` example ([#26](https://github.com/tui-rs-revival/ratatui/issues/26))
|
||||
- *(example)* Refactor paragraph example ([#152](https://github.com/tui-rs-revival/ratatui/issues/152))
|
||||
- *(style)* Mark some Style fns const so they can be defined globally ([#115](https://github.com/tui-rs-revival/ratatui/issues/115))
|
||||
- *(text)* Replace `Spans` with `Line` ([#178](https://github.com/tui-rs-revival/ratatui/issues/178))
|
||||
|
||||
### Documentation
|
||||
|
||||
- *(apps)* Fix rsadsb/adsb_deku radar link ([#140](https://github.com/tui-rs-revival/ratatui/issues/140))
|
||||
- *(apps)* Add tenere ([#141](https://github.com/tui-rs-revival/ratatui/issues/141))
|
||||
- *(apps)* Add twitch-tui ([#124](https://github.com/tui-rs-revival/ratatui/issues/124))
|
||||
- *(apps)* Add oxycards ([#113](https://github.com/tui-rs-revival/ratatui/issues/113))
|
||||
- *(apps)* Re-add trippy to APPS.md ([#117](https://github.com/tui-rs-revival/ratatui/issues/117))
|
||||
- *(block)* Add example for block.inner ([#158](https://github.com/tui-rs-revival/ratatui/issues/158))
|
||||
- *(changelog)* Update the empty profile link in contributors ([#112](https://github.com/tui-rs-revival/ratatui/issues/112))
|
||||
- *(readme)* Fix small typo in readme ([#186](https://github.com/tui-rs-revival/ratatui/issues/186))
|
||||
- *(readme)* Add termwiz demo to examples ([#183](https://github.com/tui-rs-revival/ratatui/issues/183))
|
||||
- *(readme)* Add acknowledgement section ([#154](https://github.com/tui-rs-revival/ratatui/issues/154))
|
||||
- *(readme)* Update project description ([#127](https://github.com/tui-rs-revival/ratatui/issues/127))
|
||||
- *(uncategorized)* Scrape example code from examples/* ([#195](https://github.com/tui-rs-revival/ratatui/issues/195))
|
||||
|
||||
### Styling
|
||||
|
||||
- *(apps)* Update the style of application list ([#184](https://github.com/tui-rs-revival/ratatui/issues/184))
|
||||
- *(readme)* Update project introduction in README.md ([#153](https://github.com/tui-rs-revival/ratatui/issues/153))
|
||||
- *(uncategorized)* Clippy's variable inlining in format macros
|
||||
|
||||
### Testing
|
||||
|
||||
- *(buffer)* Add `assert_buffer_eq!` and Debug implementation ([#161](https://github.com/tui-rs-revival/ratatui/issues/161))
|
||||
- *(list)* Add characterization tests for list ([#167](https://github.com/tui-rs-revival/ratatui/issues/167))
|
||||
- *(widget)* Add unit tests for Paragraph ([#156](https://github.com/tui-rs-revival/ratatui/issues/156))
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- *(uncategorized)* Inline format args ([#190](https://github.com/tui-rs-revival/ratatui/issues/190))
|
||||
- *(uncategorized)* Minor lints, making Clippy happier ([#189](https://github.com/tui-rs-revival/ratatui/issues/189))
|
||||
|
||||
### Build
|
||||
|
||||
- *(uncategorized)* Bump MSRV to 1.65.0 ([#171](https://github.com/tui-rs-revival/ratatui/issues/171))
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
- *(uncategorized)* Add ci, build, and revert to allowed commit types
|
||||
|
||||
### Contributors
|
||||
|
||||
Thank you so much to everyone that contributed to this release!
|
||||
|
||||
Here is the list of contributors who has contributed to `ratatui` for the first time!
|
||||
|
||||
- [@kpcyrd](https://github.com/kpcyrd)
|
||||
- [@fujiapple852](https://github.com/fujiapple852)
|
||||
- [@BrookJeynes](https://github.com/BrookJeynes)
|
||||
- [@Ziqi-Yang](https://github.com/Ziqi-Yang)
|
||||
- [@Xithrius](https://github.com/Xithrius)
|
||||
- [@lesleyrs](https://github.com/lesleyrs)
|
||||
- [@pythops](https://github.com/pythops)
|
||||
- [@wcampbell0x2a](https://github.com/wcampbell0x2a)
|
||||
- [@sophacles](https://github.com/sophacles)
|
||||
- [@Eyesonjune18](https://github.com/Eyesonjune18)
|
||||
- [@a-kenji](https://github.com/a-kenji)
|
||||
- [@TimerErTim](https://github.com/TimerErTim)
|
||||
- [@Mehrbod2002](https://github.com/Mehrbod2002)
|
||||
- [@thomas-mauran](https://github.com/thomas-mauran)
|
||||
- [@nyurik](https://github.com/nyurik)
|
||||
|
||||
## v0.20.1 - 2023-03-19
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- *(style)* Bold needs a bit ([#104](https://github.com/tui-rs-revival/ratatui/issues/104))
|
||||
|
||||
### Documentation
|
||||
|
||||
- *(apps)* Add "logss" to apps ([#105](https://github.com/tui-rs-revival/ratatui/issues/105))
|
||||
- *(uncategorized)* Fixup remaining tui references ([#106](https://github.com/tui-rs-revival/ratatui/issues/106))
|
||||
|
||||
### Contributors
|
||||
|
||||
Thank you so much to everyone that contributed to this release!
|
||||
|
||||
- [@joshka](https://github.com/joshka)
|
||||
- [@todoesverso](https://github.com/todoesverso)
|
||||
- [@UncleScientist](https://github.com/UncleScientist)
|
||||
|
||||
## v0.20.0 - 2023-03-19
|
||||
|
||||
This marks the first release of `ratatui`, a community-maintained fork of [tui](https://github.com/fdehau/tui-rs).
|
||||
|
||||
The purpose of this release is to include **bug fixes** and **small changes** into the repository thus **no new features** are added. We have transferred all the pull requests from the original repository and worked on the low hanging ones to incorporate them in this "maintenance" release.
|
||||
|
||||
Here is a list of changes:
|
||||
|
||||
### Features
|
||||
|
||||
- *(cd)* Add continuous deployment workflow ([#93](https://github.com/tui-rs-revival/ratatui/issues/93))
|
||||
- *(ci)* Add MacOS to CI ([#60](https://github.com/tui-rs-revival/ratatui/issues/60))
|
||||
- *(widget)* Add `offset()` to `TableState` ([#10](https://github.com/tui-rs-revival/ratatui/issues/10))
|
||||
- *(widget)* Add `width()` to ListItem ([#17](https://github.com/tui-rs-revival/ratatui/issues/17))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- *(ci)* Test MSRV compatibility on CI ([#85](https://github.com/tui-rs-revival/ratatui/issues/85))
|
||||
- *(ci)* Bump Rust version to 1.63.0 ([#80](https://github.com/tui-rs-revival/ratatui/issues/80))
|
||||
- *(ci)* Use env for the cargo-make version ([#76](https://github.com/tui-rs-revival/ratatui/issues/76))
|
||||
- *(ci)* Fix deprecation warnings on CI ([#58](https://github.com/tui-rs-revival/ratatui/issues/58))
|
||||
- *(doc)* Add 3rd party libraries accidentally removed at #21 ([#61](https://github.com/tui-rs-revival/ratatui/issues/61))
|
||||
- *(widget)* List should not ignore empty string items ([#42](https://github.com/tui-rs-revival/ratatui/issues/42)) [**breaking**]
|
||||
- *(uncategorized)* Cassowary/layouts: add extra constraints for fixing Min(v)/Max(v) combination. ([#31](https://github.com/tui-rs-revival/ratatui/issues/31))
|
||||
- *(uncategorized)* Fix user_input example double key press registered on windows
|
||||
- *(uncategorized)* Ignore zero-width symbol on rendering `Paragraph`
|
||||
- *(uncategorized)* Fix typos ([#45](https://github.com/tui-rs-revival/ratatui/issues/45))
|
||||
- *(uncategorized)* Fix typos ([#47](https://github.com/tui-rs-revival/ratatui/issues/47))
|
||||
|
||||
### Refactor
|
||||
|
||||
- *(style)* Make bitflags smaller ([#13](https://github.com/tui-rs-revival/ratatui/issues/13))
|
||||
|
||||
### Documentation
|
||||
|
||||
- *(apps)* Move 'apps using ratatui' to dedicated file ([#98](https://github.com/tui-rs-revival/ratatui/issues/98)) ([#99](https://github.com/tui-rs-revival/ratatui/issues/99))
|
||||
- *(canvas)* Add documentation for x_bounds, y_bounds ([#35](https://github.com/tui-rs-revival/ratatui/issues/35))
|
||||
- *(contributing)* Specify the use of unsafe for optimization ([#67](https://github.com/tui-rs-revival/ratatui/issues/67))
|
||||
- *(github)* Remove pull request template ([#68](https://github.com/tui-rs-revival/ratatui/issues/68))
|
||||
- *(readme)* Update crate status badge ([#102](https://github.com/tui-rs-revival/ratatui/issues/102))
|
||||
- *(readme)* Small edits before first release ([#101](https://github.com/tui-rs-revival/ratatui/issues/101))
|
||||
- *(readme)* Add install instruction and update title ([#100](https://github.com/tui-rs-revival/ratatui/issues/100))
|
||||
- *(readme)* Add systeroid to application list ([#92](https://github.com/tui-rs-revival/ratatui/issues/92))
|
||||
- *(readme)* Add glicol-cli to showcase list ([#95](https://github.com/tui-rs-revival/ratatui/issues/95))
|
||||
- *(readme)* Add oxker to application list ([#74](https://github.com/tui-rs-revival/ratatui/issues/74))
|
||||
- *(readme)* Add app kubectl-watch which uses tui ([#73](https://github.com/tui-rs-revival/ratatui/issues/73))
|
||||
- *(readme)* Add poketex to 'apps using tui' in README ([#64](https://github.com/tui-rs-revival/ratatui/issues/64))
|
||||
- *(readme)* Update README.md ([#39](https://github.com/tui-rs-revival/ratatui/issues/39))
|
||||
- *(readme)* Update README.md ([#40](https://github.com/tui-rs-revival/ratatui/issues/40))
|
||||
- *(readme)* Clarify README.md fork status update
|
||||
- *(uncategorized)* Fix: fix typos ([#90](https://github.com/tui-rs-revival/ratatui/issues/90))
|
||||
- *(uncategorized)* Update to build more backends ([#81](https://github.com/tui-rs-revival/ratatui/issues/81))
|
||||
- *(uncategorized)* Expand "Apps" and "Third-party" sections ([#21](https://github.com/tui-rs-revival/ratatui/issues/21))
|
||||
- *(uncategorized)* Add tui-input and update xplr in README.md
|
||||
- *(uncategorized)* Add hncli to list of applications made with tui-rs ([#41](https://github.com/tui-rs-revival/ratatui/issues/41))
|
||||
- *(uncategorized)* Updated readme and contributing guide with updates about the fork ([#46](https://github.com/tui-rs-revival/ratatui/issues/46))
|
||||
|
||||
### Performance
|
||||
|
||||
- *(layout)* Better safe shared layout cache ([#62](https://github.com/tui-rs-revival/ratatui/issues/62))
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- *(cargo)* Update project metadata ([#94](https://github.com/tui-rs-revival/ratatui/issues/94))
|
||||
- *(ci)* Integrate `typos` for checking typos ([#91](https://github.com/tui-rs-revival/ratatui/issues/91))
|
||||
- *(ci)* Change the target branch to main ([#79](https://github.com/tui-rs-revival/ratatui/issues/79))
|
||||
- *(ci)* Re-enable clippy on CI ([#59](https://github.com/tui-rs-revival/ratatui/issues/59))
|
||||
- *(uncategorized)* Integrate `committed` for checking conventional commits ([#77](https://github.com/tui-rs-revival/ratatui/issues/77))
|
||||
- *(uncategorized)* Update `rust-version` to 1.59 in Cargo.toml ([#57](https://github.com/tui-rs-revival/ratatui/issues/57))
|
||||
- *(uncategorized)* Update deps ([#51](https://github.com/tui-rs-revival/ratatui/issues/51))
|
||||
- *(uncategorized)* Fix typo in layout.rs ([#619](https://github.com/tui-rs-revival/ratatui/issues/619))
|
||||
- *(uncategorized)* Add apps using `tui`
|
||||
|
||||
### Contributors
|
||||
|
||||
Thank you so much to everyone that contributed to this release!
|
||||
|
||||
- [@orhun](https://github.com/orhun)
|
||||
- [@mindoodoo](https://github.com/mindoodoo)
|
||||
- [@sayanarijit](https://github.com/sayanarijit)
|
||||
- [@Owletti](https://github.com/Owletti)
|
||||
- [@UncleScientist](https://github.com/UncleScientist)
|
||||
- [@rhysd](https://github.com/rhysd)
|
||||
- [@ckaznable](https://github.com/ckaznable)
|
||||
- [@imuxin](https://github.com/imuxin)
|
||||
- [@mrjackwills](https://github.com/mrjackwills)
|
||||
- [@conradludgate](https://github.com/conradludgate)
|
||||
- [@kianmeng](https://github.com/kianmeng)
|
||||
- [@chaosprint](https://github.com/chaosprint)
|
||||
|
||||
And most importantly, special thanks to [Florian Dehau](https://github.com/fdehau) for creating this awesome library 💖 We look forward to building on the strong foundations that the original crate laid out.
|
||||
|
||||
## v0.19.0 - 2022-08-14
|
||||
|
||||
### Features
|
||||
|
||||
* Bump `crossterm` to `0.25`
|
||||
|
||||
## v0.18.0 - 2022-04-24
|
||||
|
||||
### Features
|
||||
|
||||
* Update `crossterm` to `0.23`
|
||||
|
||||
## v0.17.0 - 2022-01-22
|
||||
|
||||
### Features
|
||||
|
||||
* Add option to `widgets::List` to repeat the highlight symbol for each line of multi-line items (#533).
|
||||
* Add option to control the alignment of `Axis` labels in the `Chart` widget (#568).
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* The minimum supported rust version is now `1.56.1`.
|
||||
|
||||
#### New default backend and consolidated backend options (#553)
|
||||
|
||||
* `crossterm` is now the default backend.
|
||||
If you are already using the `crossterm` backend, you can simplify your dependency specification in `Cargo.toml`:
|
||||
```diff
|
||||
- tui = { version = "0.16", default-features = false, features = ["crossterm"] }
|
||||
+ tui = "0.17"
|
||||
```
|
||||
If you are using the `termion` backend, your `Cargo` is now a bit more verbose:
|
||||
```diff
|
||||
- tui = "0.16"
|
||||
+ tui = { version = "0.17", default-features = false, features = ["termion"] }
|
||||
```
|
||||
|
||||
`crossterm` has also been bumped to version `0.22`.
|
||||
|
||||
Because of their apparent low usage, `curses` and `rustbox` backends have been removed.
|
||||
If you are using one of them, you can import their last implementation in your own project:
|
||||
* [curses](https://github.com/fdehau/tui-rs/blob/v0.16.0/src/backend/curses.rs)
|
||||
* [rustbox](https://github.com/fdehau/tui-rs/blob/v0.16.0/src/backend/rustbox.rs)
|
||||
|
||||
#### Canvas labels (#543)
|
||||
|
||||
* Labels of the `Canvas` widget are now `text::Spans`.
|
||||
The signature of `widgets::canvas::Context::print` has thus been updated:
|
||||
```diff
|
||||
- ctx.print(x, y, "Some text", Color::Yellow);
|
||||
+ ctx.print(x, y, Span::styled("Some text", Style::default().fg(Color::Yellow)))
|
||||
```
|
||||
|
||||
## v0.16.0 - 2021-08-01
|
||||
|
||||
### Features
|
||||
|
||||
* Update `crossterm` to `0.20`.
|
||||
* Add `From<Cow<str>>` implementation for `text::Text` (#471).
|
||||
* Add option to right or center align the title of a `widgets::Block` (#462).
|
||||
|
||||
### Fixes
|
||||
|
||||
* Apply label style in `widgets::Gauge` and avoid panics because of overflows with long labels (#494).
|
||||
* Avoid panics because of overflows with long axis labels in `widgets::Chart` (#512).
|
||||
* Fix computation of column widths in `widgets::Table` (#514).
|
||||
* Fix panics because of invalid offset when input changes between two frames in `widgets::List` and
|
||||
`widgets::Chart` (#516).
|
||||
|
||||
## v0.15.0 - 2021-05-02
|
||||
|
||||
### Features
|
||||
|
||||
* Update `crossterm` to `0.19`.
|
||||
* Update `rand` to `0.8`.
|
||||
* Add a read-only view of the terminal state after the draw call (#440).
|
||||
|
||||
### Fixes
|
||||
|
||||
* Remove compile warning in `TestBackend::assert_buffer` (#466).
|
||||
|
||||
## v0.14.0 - 2021-01-01
|
||||
|
||||
### Breaking changes
|
||||
|
||||
#### New API for the Table widget
|
||||
|
||||
The `Table` widget got a lot of improvements that should make it easier to work with:
|
||||
* It should not longer panic when rendered on small areas.
|
||||
* `Row`s are now a collection of `Cell`s, themselves wrapping a `Text`. This means you can style
|
||||
the entire `Table`, an entire `Row`, an entire `Cell` and rely on the styling capabilities of
|
||||
`Text` to get full control over the look of your `Table`.
|
||||
* `Row`s can have multiple lines.
|
||||
* The header is now optional and is just another `Row` always visible at the top.
|
||||
* `Row`s can have a bottom margin.
|
||||
* The header alignment is no longer off when an item is selected.
|
||||
|
||||
Taking the example of the code in `examples/demo/ui.rs`, this is what you may have to change:
|
||||
```diff
|
||||
let failure_style = Style::default()
|
||||
.fg(Color::Red)
|
||||
.add_modifier(Modifier::RAPID_BLINK | Modifier::CROSSED_OUT);
|
||||
- let header = ["Server", "Location", "Status"];
|
||||
let rows = app.servers.iter().map(|s| {
|
||||
let style = if s.status == "Up" {
|
||||
up_style
|
||||
} else {
|
||||
failure_style
|
||||
};
|
||||
- Row::StyledData(vec![s.name, s.location, s.status].into_iter(), style)
|
||||
+ Row::new(vec![s.name, s.location, s.status]).style(style)
|
||||
});
|
||||
- let table = Table::new(header.iter(), rows)
|
||||
+ let table = Table::new(rows)
|
||||
+ .header(
|
||||
+ Row::new(vec!["Server", "Location", "Status"])
|
||||
+ .style(Style::default().fg(Color::Yellow))
|
||||
+ .bottom_margin(1),
|
||||
+ )
|
||||
.block(Block::default().title("Servers").borders(Borders::ALL))
|
||||
- .header_style(Style::default().fg(Color::Yellow))
|
||||
.widths(&[
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(15),
|
||||
```
|
||||
Here, we had to:
|
||||
- Change the way we construct [`Row`](https://docs.rs/tui/*/tui/widgets/struct.Row.html) which is no
|
||||
longer an `enum` but a `struct`. It accepts anything that can be converted to an iterator of things
|
||||
that can be converted to a [`Cell`](https://docs.rs/tui/*/tui/widgets/struct.Cell.html)
|
||||
- The header is no longer a required parameter so we use
|
||||
[`Table::header`](https://docs.rs/tui/*/tui/widgets/struct.Table.html#method.header) to set it.
|
||||
`Table::header_style` has been removed since the style can be directly set using
|
||||
[`Row::style`](https://docs.rs/tui/*/tui/widgets/struct.Row.html#method.style). In addition, we want
|
||||
to preserve the old margin between the header and the rest of the rows so we add a bottom margin to
|
||||
the header using
|
||||
[`Row::bottom_margin`](https://docs.rs/tui/*/tui/widgets/struct.Row.html#method.bottom_margin).
|
||||
|
||||
You may want to look at the documentation of the different types to get a better understanding:
|
||||
- [`Table`](https://docs.rs/tui/*/tui/widgets/struct.Table.html)
|
||||
- [`Row`](https://docs.rs/tui/*/tui/widgets/struct.Row.html)
|
||||
- [`Cell`](https://docs.rs/tui/*/tui/widgets/struct.Cell.html)
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix handling of Non Breaking Space (NBSP) in wrapped text in `Paragraph` widget.
|
||||
|
||||
### Features
|
||||
|
||||
- Add `Style::reset` to create a `Style` resetting all styling properties when applied.
|
||||
- Add an option to render the `Gauge` widget with unicode blocks.
|
||||
- Manage common project tasks with `cargo-make` rather than `make` for easier on-boarding.
|
||||
|
||||
## v0.13.0 - 2020-11-14
|
||||
|
||||
### Features
|
||||
|
||||
* Add `LineGauge` widget which is a more compact variant of the existing `Gauge`.
|
||||
* Bump `crossterm` to 0.18
|
||||
|
||||
### Fixes
|
||||
|
||||
* Take into account the borders of the `Table` widget when the widths of columns is controlled by
|
||||
`Percentage` and `Ratio` constraints.
|
||||
|
||||
## v0.12.0 - 2020-09-27
|
||||
|
||||
### Features
|
||||
|
||||
* Make it easier to work with string with multiple lines in `Text` (#361).
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fix a style leak in `Graph` so components drawn on top of the plotted data (i.e legend and axis
|
||||
titles) are not affected by the style of the `Dataset`s (#388).
|
||||
* Make sure `BarChart` shows bars with the max height only when the plotted data is actually equal
|
||||
to the max (#383).
|
||||
|
||||
## v0.11.0 - 2020-09-20
|
||||
|
||||
### Features
|
||||
|
||||
* Add the dot character as a new type of canvas marker (#350).
|
||||
* Support more style modifiers on Windows (#368).
|
||||
|
||||
### Fixes
|
||||
|
||||
* Clearing the terminal through `Terminal::clear` will cause the whole UI to be redrawn (#380).
|
||||
* Fix incorrect output when the first diff to draw is on the second cell of the terminal (#347).
|
||||
|
||||
## v0.10.0 - 2020-07-17
|
||||
|
||||
### Breaking changes
|
||||
|
||||
#### Easier cursor management
|
||||
|
||||
A new method has been added to `Frame` called `set_cursor`. It lets you specify where the cursor
|
||||
should be placed after the draw call. Furthermore like any other widgets, if you do not set a cursor
|
||||
position during a draw call, the cursor is automatically hidden.
|
||||
|
||||
For example:
|
||||
|
||||
```rust
|
||||
fn draw_input(f: &mut Frame, app: &App) {
|
||||
if app.editing {
|
||||
let input_width = app.input.width() as u16;
|
||||
// The cursor will be placed just after the last character of the input
|
||||
f.set_cursor((input_width + 1, 0));
|
||||
} else {
|
||||
// We are no longer editing, the cursor does not have to be shown, set_cursor is not called and
|
||||
// thus automatically hidden.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In order to make this possible, the draw closure takes in input `&mut Frame` instead of `mut Frame`.
|
||||
|
||||
#### Advanced text styling
|
||||
|
||||
It has been reported several times that the text styling capabilities were somewhat limited in many
|
||||
places of the crate. To solve the issue, this release includes a new set of text primitives that are
|
||||
now used by a majority of widgets to provide flexible text styling.
|
||||
|
||||
`Text` is replaced by the following types:
|
||||
- `Span`: a string with a unique style.
|
||||
- `Spans`: a string with multiple styles.
|
||||
- `Text`: a multi-lines string with multiple styles.
|
||||
|
||||
However, you do not always need this complexity so the crate provides `From` implementations to
|
||||
let you use simple strings as a default and switch to the previous primitives when you need
|
||||
additional styling capabilities.
|
||||
|
||||
For example, the title of a `Block` can be set in the following ways:
|
||||
|
||||
```rust
|
||||
// A title with no styling
|
||||
Block::default().title("My title");
|
||||
// A yellow title
|
||||
Block::default().title(Span::styled("My title", Style::default().fg(Color::Yellow)));
|
||||
// A title where "My" is bold and "title" is a simple string
|
||||
Block::default().title(vec![
|
||||
Span::styled("My", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::from("title")
|
||||
]);
|
||||
```
|
||||
|
||||
- `Buffer::set_spans` and `Buffer::set_span` were added.
|
||||
- `Paragraph::new` expects an input that can be converted to a `Text`.
|
||||
- `Block::title_style` is deprecated.
|
||||
- `Block::title` expects a `Spans`.
|
||||
- `Tabs` expects a list of `Spans`.
|
||||
- `Gauge` custom label is now a `Span`.
|
||||
- `Axis` title and labels are `Spans` (as a consequence `Chart` no longer has generic bounds).
|
||||
|
||||
#### Incremental styling
|
||||
|
||||
Previously `Style` was used to represent an exhaustive set of style rules to be applied to an UI
|
||||
element. It implied that whenever you wanted to change even only one property you had to provide the
|
||||
complete style. For example, if you had a `Block` where you wanted to have a green background and
|
||||
a title in bold, you had to do the following:
|
||||
|
||||
```rust
|
||||
let style = Style::default().bg(Color::Green);
|
||||
Block::default()
|
||||
.style(style)
|
||||
.title("My title")
|
||||
// Here we reused the style otherwise the background color would have been reset
|
||||
.title_style(style.modifier(Modifier::BOLD));
|
||||
```
|
||||
|
||||
In this new release, you may now write this as:
|
||||
|
||||
```rust
|
||||
Block::default()
|
||||
.style(Style::default().bg(Color::Green))
|
||||
// The style is not overridden anymore, we simply add new style rule for the title.
|
||||
.title(Span::styled("My title", Style::default().add_modifier(Modifier::BOLD)))
|
||||
```
|
||||
|
||||
In addition, the crate now provides a method `patch` to combine two styles into a new set of style
|
||||
rules:
|
||||
|
||||
```rust
|
||||
let style = Style::default().modifier(Modifier::BOLD);
|
||||
let style = style.patch(Style::default().add_modifier(Modifier::ITALIC));
|
||||
// style.modifier == Modifier::BOLD | Modifier::ITALIC, the modifier has been enriched not overridden
|
||||
```
|
||||
|
||||
- `Style::modifier` has been removed in favor of `Style::add_modifier` and `Style::remove_modifier`.
|
||||
- `Buffer::set_style` has been added. `Buffer::set_background` is deprecated.
|
||||
- `BarChart::style` no longer set the style of the bars. Use `BarChart::bar_style` in replacement.
|
||||
- `Gauge::style` no longer set the style of the gauge. Use `Gauge::gauge_style` in replacement.
|
||||
|
||||
#### List with item on multiple lines
|
||||
|
||||
The `List` widget has been refactored once again to support items with variable heights and complex
|
||||
styling.
|
||||
|
||||
- `List::new` expects an input that can be converted to a `Vec<ListItem>` where `ListItem` is a
|
||||
wrapper around the item content to provide additional styling capabilities. `ListItem` contains a
|
||||
`Text`.
|
||||
- `List::items` has been removed.
|
||||
|
||||
```rust
|
||||
// Before
|
||||
let items = vec![
|
||||
"Item1",
|
||||
"Item2",
|
||||
"Item3"
|
||||
];
|
||||
List::default().items(items.iters());
|
||||
|
||||
// After
|
||||
let items = vec![
|
||||
ListItem::new("Item1"),
|
||||
ListItem::new("Item2"),
|
||||
ListItem::new("Item3"),
|
||||
];
|
||||
List::new(items);
|
||||
```
|
||||
|
||||
See the examples for more advanced usages.
|
||||
|
||||
#### More wrapping options
|
||||
|
||||
`Paragraph::wrap` expects `Wrap` instead of `bool` to let users decided whether they want to trim
|
||||
whitespaces when the text is wrapped.
|
||||
|
||||
```rust
|
||||
// before
|
||||
Paragraph::new(text).wrap(true)
|
||||
// after
|
||||
Paragraph::new(text).wrap(Wrap { trim: true }) // to have the same behavior
|
||||
Paragraph::new(text).wrap(Wrap { trim: false }) // to use the new behavior
|
||||
```
|
||||
|
||||
#### Horizontal scrolling in paragraph
|
||||
|
||||
You can now scroll horizontally in `Paragraph`. The argument of `Paragraph::scroll` has thus be
|
||||
changed from `u16` to `(u16, u16)`.
|
||||
|
||||
### Features
|
||||
|
||||
#### Serialization of style
|
||||
|
||||
You can now serialize and de-serialize `Style` using the optional `serde` feature.
|
||||
|
||||
## v0.9.5 - 2020-05-21
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fix out of bounds panic in `widgets::Tabs` when the widget is rendered on
|
||||
small areas.
|
||||
|
||||
## v0.9.4 - 2020-05-12
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Ignore zero-width graphemes in `Buffer::set_stringn`.
|
||||
|
||||
## v0.9.3 - 2020-05-11
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fix usize overflows in `widgets::Chart` when a dataset is empty.
|
||||
|
||||
## v0.9.2 - 2020-05-10
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fix usize overflows in `widgets::canvas::Line` drawing algorithm.
|
||||
|
||||
## v0.9.1 - 2020-04-16
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* The `List` widget now takes into account the width of the `highlight_symbol`
|
||||
when calculating the total width of its items. It prevents items to overflow
|
||||
outside of the widget area.
|
||||
|
||||
## v0.9.0 - 2020-04-14
|
||||
|
||||
### Features
|
||||
|
||||
* Introduce stateful widgets, i.e widgets that can take advantage of keeping
|
||||
some state around between two draw calls (#210 goes a bit more into the
|
||||
details).
|
||||
* Allow a `Table` row to be selected.
|
||||
```rust
|
||||
// State initialization
|
||||
let mut state = TableState::default();
|
||||
|
||||
// In the terminal.draw closure
|
||||
let header = ["Col1", "Col2", "Col"];
|
||||
let rows = [
|
||||
Row::Data(["Row11", "Row12", "Row13"].into_iter())
|
||||
];
|
||||
let table = Table::new(header.into_iter(), rows.into_iter());
|
||||
f.render_stateful_widget(table, area, &mut state);
|
||||
|
||||
// In response to some event:
|
||||
state.select(Some(1));
|
||||
```
|
||||
* Add a way to choose the type of border used to draw a block. You can now
|
||||
choose from plain, rounded, double and thick lines.
|
||||
* Add a `graph_type` property on the `Dataset` of a `Chart` widget. By
|
||||
default it will be `Scatter` where the points are drawn as is. An other
|
||||
option is `Line` where a line will be draw between each consecutive points
|
||||
of the dataset.
|
||||
* Style methods are now const, allowing you to initialize const `Style`
|
||||
objects.
|
||||
* Improve control over whether the legend in the `Chart` widget is shown or
|
||||
not. You can now set custom constraints using
|
||||
`Chart::hidden_legend_constraints`.
|
||||
* Add `Table::header_gap` to add some space between the header and the first
|
||||
row.
|
||||
* Remove `log` from the dependencies
|
||||
* Add a way to use a restricted set of unicode symbols in several widgets to
|
||||
improve portability in exchange of a degraded output. (see `BarChart::bar_set`,
|
||||
`Sparkline::bar_set` and `Canvas::marker`). You can check how the
|
||||
`--enhanced-graphics` flag is used in the demos.
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* `Widget::render` has been deleted. You should now use `Frame::render_widget`
|
||||
to render a widget on the corresponding `Frame`. This makes the `Widget`
|
||||
implementation totally decoupled from the `Frame`.
|
||||
```rust
|
||||
// Before
|
||||
Block::default().render(&mut f, size);
|
||||
|
||||
// After
|
||||
let block = Block::default();
|
||||
f.render_widget(block, size);
|
||||
```
|
||||
* `Widget::draw` has been renamed to `Widget::render` and the signature has
|
||||
been updated to reflect that widgets are consumable objects. Thus the method
|
||||
takes `self` instead of `&mut self`.
|
||||
```rust
|
||||
// Before
|
||||
impl Widget for MyWidget {
|
||||
fn draw(&mut self, area: Rect, buf: &mut Buffer) {
|
||||
}
|
||||
}
|
||||
|
||||
/// After
|
||||
impl Widget for MyWidget {
|
||||
fn render(self, arera: Rect, buf: &mut Buffer) {
|
||||
}
|
||||
}
|
||||
```
|
||||
* `Widget::background` has been replaced by `Buffer::set_background`
|
||||
```rust
|
||||
// Before
|
||||
impl Widget for MyWidget {
|
||||
fn render(self, arera: Rect, buf: &mut Buffer) {
|
||||
self.background(area, buf, self.style.bg);
|
||||
}
|
||||
}
|
||||
|
||||
// After
|
||||
impl Widget for MyWidget {
|
||||
fn render(self, arera: Rect, buf: &mut Buffer) {
|
||||
buf.set_background(area, self.style.bg);
|
||||
}
|
||||
}
|
||||
```
|
||||
* Update the `Shape` trait for objects that can be draw on a `Canvas` widgets.
|
||||
Instead of returning an iterator over its points, a `Shape` is given a
|
||||
`Painter` object that provides a `paint` as well as a `get_point` method. This
|
||||
gives the `Shape` more information about the surface it will be drawn to. In
|
||||
particular, this change allows the `Line` shape to use a more precise and
|
||||
efficient drawing algorithm (Bresenham's line algorithm).
|
||||
* `SelectableList` has been deleted. You can now take advantage of the
|
||||
associated `ListState` of the `List` widget to select an item.
|
||||
```rust
|
||||
// Before
|
||||
List::new(&["Item1", "Item2", "Item3"])
|
||||
.select(Some(1))
|
||||
.render(&mut f, area);
|
||||
|
||||
// After
|
||||
|
||||
// State initialization
|
||||
let mut state = ListState::default();
|
||||
|
||||
// In the terminal.draw closure
|
||||
let list = List::new(&["Item1", "Item2", "Item3"]);
|
||||
f.render_stateful_widget(list, area, &mut state);
|
||||
|
||||
// In response to some events
|
||||
state.select(Some(1));
|
||||
```
|
||||
* `widgets::Marker` has been moved to `symbols::Marker`
|
||||
|
||||
## v0.8.0 - 2019-12-15
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* Bump crossterm to 0.14.
|
||||
* Add cross symbol to the symbols list.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Use the value of `title_style` to style the title of `Axis`.
|
||||
|
||||
## v0.7.0 - 2019-11-29
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* Use `Constraint` instead of integers to specify the widths of the `Table`
|
||||
widget's columns. This will allow more responsive tables.
|
||||
|
||||
```rust
|
||||
Table::new(header, row)
|
||||
.widths(&[15, 15, 10])
|
||||
.render(f, chunk);
|
||||
```
|
||||
|
||||
becomes:
|
||||
|
||||
```rust
|
||||
Table::new(header, row)
|
||||
.widths(&[
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(10),
|
||||
])
|
||||
.render(f, chunk);
|
||||
```
|
||||
|
||||
* Bump crossterm to 0.13.
|
||||
* Use Github Actions for CI (Travis and Azure Pipelines integrations have been deleted).
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for horizontal and vertical margins in `Layout`.
|
||||
|
||||
## v0.6.2 - 2019-07-16
|
||||
|
||||
### Features
|
||||
|
||||
* `Text` implements PartialEq
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Avoid overflow errors in canvas
|
||||
|
||||
## v0.6.1 - 2019-06-16
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Avoid a division by zero when all values in a barchart are equal to 0.
|
||||
* Fix the inverted cursor position in the curses backend.
|
||||
* Ensure that the correct terminal size is returned when using the crossterm
|
||||
backend.
|
||||
* Avoid highlighting the separator after the selected item in the Tabs widget.
|
||||
|
||||
## v0.6.0 - 2019-05-18
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* Update crossterm backend
|
||||
|
||||
## v0.5.1 - 2019-04-14
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fix a panic in the Sparkline widget
|
||||
|
||||
## v0.5.0 - 2019-03-10
|
||||
|
||||
### Features
|
||||
|
||||
* Add a new curses backend (with Windows support thanks to `pancurses`).
|
||||
* Add `Backend::get_cursor` and `Backend::set_cursor` methods to query and
|
||||
set the position of the cursor.
|
||||
* Add more constructors to the `Crossterm` backend.
|
||||
* Add a demo for all backends using a shared UI and application state.
|
||||
* Add `Ratio` as a new variant of layout `Constraint`. It can be used to define
|
||||
exact ratios constraints.
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* Add support for multiple modifiers on the same `Style` by changing `Modifier`
|
||||
from an enum to a bitflags struct.
|
||||
|
||||
So instead of writing:
|
||||
|
||||
```rust
|
||||
let style = Style::default().add_modifier(Modifier::Italic);
|
||||
```
|
||||
|
||||
one should use:
|
||||
|
||||
```rust
|
||||
let style = Style::default().add_modifier(Modifier::ITALIC);
|
||||
// or
|
||||
let style = Style::default().add_modifier(Modifier::ITALIC | Modifier::BOLD);
|
||||
```
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Ensure correct behavior of the alternate screens with the `Crossterm` backend.
|
||||
* Fix out of bounds panic when two `Buffer` are merged.
|
||||
|
||||
## v0.4.0 - 2019-02-03
|
||||
|
||||
### Features
|
||||
|
||||
* Add a new canvas shape: `Rectangle`.
|
||||
* Official support of `Crossterm` backend.
|
||||
* Make it possible to choose the divider between `Tabs`.
|
||||
* Add word wrapping on Paragraph.
|
||||
* The gauge widget accepts a ratio (f64 between 0 and 1) in addition of a
|
||||
percentage.
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* Upgrade to Rust 2018 edition.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fix rendering of double-width characters.
|
||||
* Fix race condition on the size of the terminal and expose a size that is
|
||||
safe to use when drawing through `Frame::size`.
|
||||
* Prevent unsigned int overflow on large screens.
|
||||
|
||||
## v0.3.0 - 2018-11-04
|
||||
|
||||
### Features
|
||||
|
||||
* Add experimental test backend
|
||||
|
||||
## v0.3.0-beta.3 - 2018-09-24
|
||||
|
||||
### Features
|
||||
|
||||
* `show_cursor` is called when `Terminal` is dropped if the cursor is hidden.
|
||||
|
||||
## v0.3.0-beta.2 - 2018-09-23
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* Remove custom `termion` backends. This is motivated by the fact that
|
||||
`termion` structs are meant to be combined/wrapped to provide additional
|
||||
functionalities to the terminal (e.g AlternateScreen, Mouse support, ...).
|
||||
Thus providing exclusive types do not make a lot of sense and give a false
|
||||
hint that additional features cannot be used together. The recommended
|
||||
approach is now to create your own version of `stdout`:
|
||||
|
||||
```rust
|
||||
let stdout = io::stdout().into_raw_mode()?;
|
||||
let stdout = MouseTerminal::from(stdout);
|
||||
let stdout = AlternateScreen::from(stdout);
|
||||
```
|
||||
|
||||
and then to create the corresponding `termion` backend:
|
||||
|
||||
```rust
|
||||
let backend = TermionBackend::new(stdout);
|
||||
```
|
||||
|
||||
The resulting code is more verbose but it works with all combinations of
|
||||
additional `termion` features.
|
||||
|
||||
## v0.3.0-beta.1 - 2018-09-08
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* Replace `Item` by a generic and flexible `Text` that can be used in both
|
||||
`Paragraph` and `List` widgets.
|
||||
* Remove unnecessary borrows on `Style`.
|
||||
|
||||
## v0.3.0-beta.0 - 2018-09-04
|
||||
|
||||
### Features
|
||||
|
||||
* Add a basic `Crossterm` backend
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* Remove `Group` and introduce `Layout` in its place
|
||||
- `Terminal` is no longer required to compute a layout
|
||||
- `Size` has been renamed `Constraint`
|
||||
* Widgets are rendered on a `Frame` instead of a `Terminal` in order to
|
||||
avoid mixing `draw` and `render` calls
|
||||
* `draw` on `Terminal` expects a closure where the UI is built by rendering
|
||||
widgets on the given `Frame`
|
||||
* Update `Widget` trait
|
||||
- `draw` takes area by value
|
||||
- `render` takes a `Frame` instead of a `Terminal`
|
||||
* All widgets use the consumable builder pattern
|
||||
* `SelectableList` can have no selected item and the highlight symbol is hidden
|
||||
in this case
|
||||
* Remove markup language inside `Paragraph`. `Paragraph` now expects an iterator
|
||||
of `Text` items
|
||||
|
||||
## v0.2.3 - 2018-06-09
|
||||
|
||||
### Features
|
||||
|
||||
* Add `start_corner` option for `List`
|
||||
* Add more text alignment options for `Paragraph`
|
||||
|
||||
## v0.2.2 - 2018-05-06
|
||||
|
||||
### Features
|
||||
|
||||
* `Terminal` implements `Debug`
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* Use `FnOnce` instead of `FnMut` in Group::render
|
||||
|
||||
## v0.2.1 - 2018-04-01
|
||||
|
||||
### Features
|
||||
|
||||
* Add `AlternateScreenBackend` in `termion` backend
|
||||
* Add `TermionBackend::with_stdout` in order to let an user of the library
|
||||
provides its own termion struct
|
||||
* Add tests and documentation for `Buffer::pos_of`
|
||||
* Remove leading whitespaces when wrapping text
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fix `debug_assert` in `Buffer::pos_of`
|
||||
* Pass the style of `SelectableList` to the underlying `List`
|
||||
* Fix missing character when wrapping text
|
||||
* Fix panic when specifying layout constraints
|
||||
|
||||
## v0.2.0 - 2017-12-26
|
||||
|
||||
### Added
|
||||
### Features
|
||||
|
||||
* Add `MouseBackend` in `termion` backend to handle scroll and mouse events
|
||||
* Add generic `Item` for items in a `List`
|
||||
* Drop `log4rs` as a dev-dependencies in favor of `stderrlog`
|
||||
|
||||
### Changed
|
||||
### Breaking Changes
|
||||
|
||||
* Rename `TermionBackend` to `RawBackend` (to distinguish it from the `MouseBackend`)
|
||||
* Generic parameters for `List` to allow passing iterators as items
|
||||
* Generic parameters for `Table` to allow using iterators as rows and header
|
||||
* Generic parameters for `Tabs`
|
||||
* Rename `border` bitflags to `Borders`
|
||||
|
||||
* Run latest `rustfmt` on all sources
|
||||
|
||||
### Removed
|
||||
|
||||
* Drop `log4rs` as a dev-dependencies in favor of `stderrlog`
|
||||
|
||||
69
CONTRIBUTING.md
Normal file
69
CONTRIBUTING.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Fork Status
|
||||
|
||||
## Pull Requests
|
||||
|
||||
**All** pull requests opened on the original repository have been imported. We'll be going through any open PRs in a timely manner, starting with the **smallest bug fixes and README updates**. If you have an open PR make sure to let us know about it on our [discord](https://discord.gg/pMCEU9hNEj) as it helps to know you are still active.
|
||||
|
||||
## Issues
|
||||
|
||||
We have been unsuccessful in importing all issues opened on the previous repository.
|
||||
For that reason, anyone wanting to **work on or discuss** an issue will have to follow the following workflow :
|
||||
|
||||
- Recreate the issue
|
||||
- Start by referencing the **original issue**: ```Referencing issue #[<issue number>](<original issue link>)```
|
||||
- Then, paste the original issues **opening** text
|
||||
|
||||
You can then resume the conversation by replying to this new issue you have created.
|
||||
|
||||
### Closing Issues
|
||||
|
||||
If you close an issue that you have "imported" to this fork, please make sure that you add the issue to the **CLOSED_ISSUES.md**. This will enable us to keep track of which issues have been closed from the original repo, in case we are able to have the original repository transferred.
|
||||
|
||||
# Contributing
|
||||
|
||||
## Implementation Guidelines
|
||||
|
||||
### Use of unsafe for optimization purposes
|
||||
|
||||
**Do not** use unsafe to achieve better performances. This is subject to change, [see.](https://github.com/tui-rs-revival/tui-rs-revival/discussions/66)
|
||||
The only exception to this rule is if it's to fix **reproducible slowness.**
|
||||
|
||||
## Building
|
||||
|
||||
[cargo-make]: https://github.com/sagiegurari/cargo-make "cargo-make"
|
||||
|
||||
`ratatui` is an ordinary Rust project where common tasks are managed with [cargo-make].
|
||||
It wraps common `cargo` commands with sane defaults depending on your platform of choice.
|
||||
Building the project should be as easy as running `cargo make build`.
|
||||
|
||||
## :hammer_and_wrench: Pull requests
|
||||
|
||||
All contributions are obviously welcome.
|
||||
Please include as many details as possible in your PR description to help the reviewer (follow the provided template).
|
||||
Make sure to highlight changes which may need additional attention or you are uncertain about.
|
||||
Any idea with a large scale impact on the crate or its users should ideally be discussed in a "Feature Request" issue beforehand.
|
||||
|
||||
## Committing
|
||||
|
||||
To avoid any issues that may arrise with the CI/CD by not following the [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) syntax, we recommend to install [Commitizen](https://commitizen-tools.github.io/commitizen/).\
|
||||
By using this tool you automatically follow the configuration defined in [.cz.toml](.cz.toml).
|
||||
|
||||
Additionally, we're using [cargo-husky](https://github.com/rhysd/cargo-husky) to automatically load pre-push hook, which will run `cargo make ci` before each push. It will load the hook automatically when you run `cargo test`. If `cargo-make` is not installed, it will install it for you.\
|
||||
This will ensure that your code is formatted, compiles and passes all tests before you push. If you want to skip this check, you can use `git push --no-verify`.
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
We use Github Actions for the CI where we perform the following checks:
|
||||
- The code should compile on `stable` and the Minimum Supported Rust Version (MSRV).
|
||||
- The tests (docs, lib, tests and examples) should pass.
|
||||
- The code should conform to the default format enforced by `rustfmt`.
|
||||
- The code should not contain common style issues `clippy`.
|
||||
|
||||
You can also check most of those things yourself locally using `cargo make ci` which will offer you a shorter feedback loop.
|
||||
|
||||
## Tests
|
||||
|
||||
The test coverage of the crate is far from being ideal but we already have a fair amount of tests in place.
|
||||
Beside the usual doc and unit tests, one of the most valuable test you can write for `ratatui` is a test against the `TestBackend`.
|
||||
It allows you to assert the content of the output buffer that would have been flushed to the terminal after a given draw call.
|
||||
See `widgets_block_renders` in [tests/widgets_block.rs](./tests/widget_block.rs) for an example.
|
||||
143
Cargo.toml
143
Cargo.toml
@@ -1,83 +1,160 @@
|
||||
[package]
|
||||
name = "tui"
|
||||
version = "0.2.0"
|
||||
name = "ratatui"
|
||||
version = "0.21.0"
|
||||
authors = ["Florian Dehau <work@fdehau.com>"]
|
||||
description = """
|
||||
A library to build rich terminal user interfaces or dashboards
|
||||
"""
|
||||
description = "A library to build rich terminal user interfaces or dashboards"
|
||||
documentation = "https://docs.rs/ratatui/latest/ratatui/"
|
||||
keywords = ["tui", "terminal", "dashboard"]
|
||||
repository = "https://github.com/fdehau/tui-rs"
|
||||
repository = "https://github.com/tui-rs-revival/ratatui"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
exclude = ["docs/*", ".travis.yml"]
|
||||
exclude = [
|
||||
"assets/*",
|
||||
".github",
|
||||
"Makefile.toml",
|
||||
"CONTRIBUTING.md",
|
||||
"*.log",
|
||||
"tags",
|
||||
]
|
||||
autoexamples = true
|
||||
edition = "2021"
|
||||
rust-version = "1.65.0"
|
||||
|
||||
[badges]
|
||||
travis-ci = { repository = "fdehau/tui-rs" }
|
||||
|
||||
[features]
|
||||
default = ["termion"]
|
||||
default = ["crossterm"]
|
||||
all-widgets = ["widget-calendar"]
|
||||
widget-calendar = ["time"]
|
||||
macros = []
|
||||
serde = ["dep:serde", "bitflags/serde"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
# see https://doc.rust-lang.org/nightly/rustdoc/scraped-examples.html
|
||||
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[dependencies]
|
||||
bitflags = "1.0.1"
|
||||
cassowary = "0.3.0"
|
||||
log = "0.4.0"
|
||||
unicode-segmentation = "1.2.0"
|
||||
unicode-width = "0.1.4"
|
||||
termion = { version = "1.5.1", optional = true }
|
||||
rustbox = { version = "0.9.0", optional = true }
|
||||
bitflags = "2.3"
|
||||
cassowary = "0.3"
|
||||
crossterm = { version = "0.26", optional = true }
|
||||
indoc = "2.0"
|
||||
serde = { version = "1", optional = true, features = ["derive"] }
|
||||
termion = { version = "2.0", optional = true }
|
||||
termwiz = { version = "0.20.0", optional = true }
|
||||
time = { version = "0.3.11", optional = true, features = ["local-offset"] }
|
||||
unicode-segmentation = "1.10"
|
||||
unicode-width = "0.1"
|
||||
|
||||
[dev-dependencies]
|
||||
stderrlog = "0.2.3"
|
||||
rand = "0.4.1"
|
||||
anyhow = "1.0.71"
|
||||
argh = "0.1"
|
||||
cargo-husky = { version = "1.5.0", default-features = false, features = ["user-hooks"] }
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
fakeit = "1.1"
|
||||
rand = "0.8"
|
||||
|
||||
[[bench]]
|
||||
name = "paragraph"
|
||||
harness = false
|
||||
|
||||
[[example]]
|
||||
name = "barchart"
|
||||
path = "examples/barchart.rs"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "block"
|
||||
path = "examples/block.rs"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "canvas"
|
||||
path = "examples/canvas.rs"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "calendar"
|
||||
required-features = ["crossterm", "widget-calendar"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "chart"
|
||||
path = "examples/chart.rs"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "custom_widget"
|
||||
path = "examples/custom_widget.rs"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "demo"
|
||||
path = "examples/demo.rs"
|
||||
# this runs for all of the terminal backends, so it can't be built using --all-features or scraped
|
||||
doc-scrape-examples = false
|
||||
|
||||
[[example]]
|
||||
name = "gauge"
|
||||
path = "examples/gauge.rs"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "hello_world"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "layout"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "list"
|
||||
path = "examples/list.rs"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "panic"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "paragraph"
|
||||
path = "examples/paragraph.rs"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "rustbox"
|
||||
path = "examples/rustbox.rs"
|
||||
required-features = ["rustbox"]
|
||||
name = "popup"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "scrollbar"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "sparkline"
|
||||
path = "examples/sparkline.rs"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "table"
|
||||
path = "examples/table.rs"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "tabs"
|
||||
path = "examples/tabs.rs"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "user_input"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "inline"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
111
Makefile
111
Makefile
@@ -1,111 +0,0 @@
|
||||
# Makefile for the tui-rs project (https://github.com/fdehau/tui-rs)
|
||||
|
||||
|
||||
# ================================ Cargo ======================================
|
||||
|
||||
|
||||
RUST_CHANNEL ?= stable
|
||||
CARGO_FLAGS =
|
||||
RUSTUP_INSTALLED = $(shell command -v rustup 2> /dev/null)
|
||||
|
||||
ifndef RUSTUP_INSTALLED
|
||||
CARGO = cargo
|
||||
else
|
||||
ifdef CI
|
||||
CARGO = cargo
|
||||
else
|
||||
CARGO = rustup run $(RUST_CHANNEL) cargo
|
||||
endif
|
||||
endif
|
||||
|
||||
|
||||
# ================================ Help =======================================
|
||||
|
||||
|
||||
help: ## Print all the available commands
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
|
||||
awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
|
||||
# ================================ Tools ======================================
|
||||
|
||||
|
||||
install-tools: install-rustfmt install-clippy ## Install tools dependencies
|
||||
|
||||
INSTALL_RUSTFMT = ./scripts/tools/install.sh --name=rustfmt-nightly
|
||||
ifndef CI
|
||||
INSTALL_RUSTFMT += --channel=nightly
|
||||
endif
|
||||
install-rustfmt: ## Intall rustfmt
|
||||
$(INSTALL_RUSTFMT)
|
||||
|
||||
INSTALL_CLIPPY = ./scripts/tools/install.sh --name=clippy
|
||||
ifndef CI
|
||||
INSTALL_CLIPPY += --channel=nightly
|
||||
endif
|
||||
install-clippy: ## Intall rustfmt
|
||||
$(INSTALL_CLIPPY)
|
||||
|
||||
|
||||
# =============================== Build =======================================
|
||||
|
||||
check: ## Validate the project code
|
||||
$(CARGO) check
|
||||
|
||||
build: ## Build the project in debug mode
|
||||
$(CARGO) build $(CARGO_FLAGS)
|
||||
|
||||
release: CARGO_FLAGS += --release
|
||||
release: build ## Build the project in release mode
|
||||
|
||||
|
||||
# ================================ Lint =======================================
|
||||
|
||||
RUSTFMT_WRITEMODE ?= 'diff'
|
||||
|
||||
lint: fmt clippy ## Lint project files
|
||||
|
||||
fmt: ## Check the format of the source code
|
||||
cargo fmt -- --write-mode=$(RUSTFMT_WRITEMODE)
|
||||
|
||||
clippy: RUST_CHANNEL = nightly
|
||||
clippy: ## Check the style of the source code and catch common errors
|
||||
$(CARGO) clippy --features="termion rustbox"
|
||||
|
||||
|
||||
# ================================ Test =======================================
|
||||
|
||||
|
||||
test: ## Run the tests
|
||||
$(CARGO) test
|
||||
|
||||
# ================================ Doc ========================================
|
||||
|
||||
|
||||
doc: ## Build the documentation (available at ./target/doc)
|
||||
$(CARGO) doc
|
||||
|
||||
|
||||
# ================================= Watch =====================================
|
||||
|
||||
# Requires watchman and watchman-make (https://facebook.github.io/watchman/docs/install.html)
|
||||
|
||||
watch: ## Watch file changes and build the project if any
|
||||
watchman-make -p 'src/**/*.rs' -t check build
|
||||
|
||||
watch-test: ## Watch files changes and run the tests if any
|
||||
watchman-make -p 'src/**/*.rs' 'tests/**/*.rs' 'examples/**/*.rs' -t test
|
||||
|
||||
watch-doc: ## Watch file changes and rebuild the documentation if any
|
||||
watchman-make -p 'src/**/*.rs' -t doc
|
||||
|
||||
# ================================= Pipelines =================================
|
||||
|
||||
stable: RUST_CHANNEL = stable
|
||||
stable: build test ## Run build and tests for stable
|
||||
|
||||
beta: RUST_CHANNEL = beta
|
||||
beta: build test ## Run build and tests for beta
|
||||
|
||||
nightly: RUST_CHANNEL = nightly
|
||||
nightly: install-tools build lint test ## Run build, lint and tests for nightly
|
||||
275
Makefile.toml
Normal file
275
Makefile.toml
Normal file
@@ -0,0 +1,275 @@
|
||||
# configuration for https://github.com/sagiegurari/cargo-make
|
||||
|
||||
[config]
|
||||
skip_core_tasks = true
|
||||
|
||||
[tasks.ci]
|
||||
run_task = [
|
||||
{ name = "ci-unix", condition = { platforms = [
|
||||
"linux",
|
||||
"mac",
|
||||
] } },
|
||||
{ name = "ci-windows", condition = { platforms = [
|
||||
"windows",
|
||||
] } },
|
||||
]
|
||||
|
||||
[tasks.ci-unix]
|
||||
private = true
|
||||
dependencies = [
|
||||
"style-check",
|
||||
"check-unix",
|
||||
"test-unix",
|
||||
"clippy-unix",
|
||||
]
|
||||
|
||||
[tasks.ci-windows]
|
||||
private = true
|
||||
dependencies = [
|
||||
"style-check",
|
||||
"check-windows",
|
||||
"test-windows",
|
||||
"clippy-windows",
|
||||
]
|
||||
|
||||
[tasks.style-check]
|
||||
dependencies = ["fmt", "typos"]
|
||||
|
||||
[tasks.fmt]
|
||||
toolchain = "nightly"
|
||||
command = "cargo"
|
||||
args = ["fmt", "--all", "--check"]
|
||||
|
||||
[tasks.typos]
|
||||
install_crate = { crate_name = "typos-cli", binary = "typos", test_arg = "--version" }
|
||||
command = "typos"
|
||||
|
||||
[tasks.check]
|
||||
run_task = [
|
||||
{ name = "check-unix", condition = { platforms = [
|
||||
"linux",
|
||||
"mac",
|
||||
] } },
|
||||
{ name = "check-windows", condition = { platforms = [
|
||||
"windows",
|
||||
] } },
|
||||
]
|
||||
|
||||
[tasks.check-unix]
|
||||
private = true
|
||||
dependencies = [
|
||||
"check-crossterm",
|
||||
"check-termion",
|
||||
"check-termwiz",
|
||||
]
|
||||
|
||||
[tasks.check-windows]
|
||||
private = true
|
||||
dependencies = [
|
||||
"check-crossterm",
|
||||
"check-termwiz",
|
||||
]
|
||||
|
||||
[tasks.check-crossterm]
|
||||
env = { TUI_FEATURES = "serde,crossterm" }
|
||||
run_task = "check-backend"
|
||||
|
||||
[tasks.check-termion]
|
||||
env = { TUI_FEATURES = "serde,termion" }
|
||||
run_task = "check-backend"
|
||||
|
||||
[tasks.check-termwiz]
|
||||
env = { TUI_FEATURES = "serde,termwiz" }
|
||||
run_task = "check-backend"
|
||||
|
||||
[tasks.check-backend]
|
||||
command = "cargo"
|
||||
condition = { env_set = ["TUI_FEATURES"] }
|
||||
args = [
|
||||
"check",
|
||||
"--no-default-features",
|
||||
"--features",
|
||||
"${TUI_FEATURES}",
|
||||
"--all-targets",
|
||||
]
|
||||
|
||||
[tasks.build]
|
||||
run_task = [
|
||||
{ name = "build-unix", condition = { platforms = [
|
||||
"linux",
|
||||
"mac",
|
||||
] } },
|
||||
{ name = "build-windows", condition = { platforms = [
|
||||
"windows",
|
||||
] } },
|
||||
]
|
||||
|
||||
[tasks.build-unix]
|
||||
private = true
|
||||
dependencies = [
|
||||
"build-crossterm",
|
||||
"build-termion",
|
||||
"build-termwiz",
|
||||
]
|
||||
|
||||
[tasks.build-windows]
|
||||
private = true
|
||||
dependencies = [
|
||||
"build-crossterm",
|
||||
"build-termwiz",
|
||||
]
|
||||
|
||||
[tasks.build-crossterm]
|
||||
env = { TUI_FEATURES = "serde,crossterm" }
|
||||
run_task = "build-backend"
|
||||
|
||||
[tasks.build-termion]
|
||||
env = { TUI_FEATURES = "serde,termion" }
|
||||
run_task = "build-backend"
|
||||
|
||||
[tasks.build-termwiz]
|
||||
env = { TUI_FEATURES = "serde,termwiz" }
|
||||
run_task = "build-backend"
|
||||
|
||||
[tasks.build-backend]
|
||||
command = "cargo"
|
||||
condition = { env_set = ["TUI_FEATURES"] }
|
||||
args = [
|
||||
"build",
|
||||
"--no-default-features",
|
||||
"--features",
|
||||
"${TUI_FEATURES}",
|
||||
"--all-targets",
|
||||
]
|
||||
|
||||
[tasks.clippy]
|
||||
run_task = [
|
||||
{ name = "clippy-unix", condition = { platforms = [
|
||||
"linux",
|
||||
"mac",
|
||||
] } },
|
||||
{ name = "clippy-windows", condition = { platforms = [
|
||||
"windows",
|
||||
] } },
|
||||
]
|
||||
|
||||
[tasks.clippy-unix]
|
||||
private = true
|
||||
dependencies = [
|
||||
"clippy-crossterm",
|
||||
"clippy-termion",
|
||||
"clippy-termwiz",
|
||||
]
|
||||
|
||||
[tasks.clippy-windows]
|
||||
private = true
|
||||
dependencies = [
|
||||
"clippy-crossterm",
|
||||
"clippy-termwiz",
|
||||
]
|
||||
|
||||
[tasks.clippy-crossterm]
|
||||
env = { TUI_FEATURES = "serde,crossterm" }
|
||||
run_task = "clippy-backend"
|
||||
|
||||
[tasks.clippy-termion]
|
||||
env = { TUI_FEATURES = "serde,termion" }
|
||||
run_task = "clippy-backend"
|
||||
|
||||
[tasks.clippy-termwiz]
|
||||
env = { TUI_FEATURES = "serde,termwiz" }
|
||||
run_task = "clippy-backend"
|
||||
|
||||
[tasks.clippy-backend]
|
||||
command = "cargo"
|
||||
condition = { env_set = ["TUI_FEATURES"] }
|
||||
args = [
|
||||
"clippy",
|
||||
"--all-targets",
|
||||
"--no-default-features",
|
||||
"--tests",
|
||||
"--benches",
|
||||
"--features",
|
||||
"${TUI_FEATURES}",
|
||||
"--",
|
||||
"-D",
|
||||
"warnings",
|
||||
]
|
||||
|
||||
[tasks.test]
|
||||
run_task = [
|
||||
{ name = "test-unix", condition = { platforms = [
|
||||
"linux",
|
||||
"mac",
|
||||
] } },
|
||||
{ name = "test-windows", condition = { platforms = [
|
||||
"windows",
|
||||
] } },
|
||||
]
|
||||
|
||||
[tasks.test-unix]
|
||||
private = true
|
||||
dependencies = [
|
||||
"test-crossterm",
|
||||
"test-termion",
|
||||
"test-termwiz",
|
||||
"test-doc",
|
||||
]
|
||||
|
||||
[tasks.test-windows]
|
||||
private = true
|
||||
dependencies = [
|
||||
"test-crossterm",
|
||||
"test-termwiz",
|
||||
"test-doc",
|
||||
]
|
||||
|
||||
[tasks.test-crossterm]
|
||||
env = { TUI_FEATURES = "serde,crossterm,all-widgets,macros" }
|
||||
run_task = "test-backend"
|
||||
|
||||
[tasks.test-termion]
|
||||
env = { TUI_FEATURES = "serde,termion,all-widgets,macros" }
|
||||
run_task = "test-backend"
|
||||
|
||||
[tasks.test-termwiz]
|
||||
env = { TUI_FEATURES = "serde,termwiz,all-widgets,macros" }
|
||||
run_task = "test-backend"
|
||||
|
||||
[tasks.test-backend]
|
||||
command = "cargo"
|
||||
condition = { env_set = ["TUI_FEATURES"] }
|
||||
args = [
|
||||
"test",
|
||||
"--no-default-features",
|
||||
"--features",
|
||||
"${TUI_FEATURES}",
|
||||
"--all-targets",
|
||||
]
|
||||
|
||||
[tasks.test-doc]
|
||||
command = "cargo"
|
||||
args = ["test", "--doc"]
|
||||
|
||||
[tasks.run-example]
|
||||
private = true
|
||||
condition = { env_set = ["TUI_EXAMPLE_NAME"] }
|
||||
command = "cargo"
|
||||
args = ["run", "--release", "--example", "${TUI_EXAMPLE_NAME}"]
|
||||
|
||||
[tasks.build-examples]
|
||||
command = "cargo"
|
||||
args = ["build", "--examples", "--release"]
|
||||
|
||||
[tasks.run-examples]
|
||||
dependencies = ["build-examples"]
|
||||
script = '''
|
||||
#!@duckscript
|
||||
files = glob_array ./examples/*.rs
|
||||
for file in ${files}
|
||||
name = basename ${file}
|
||||
name = substring ${name} -3
|
||||
set_env TUI_EXAMPLE_NAME ${name}
|
||||
cm_run_task run-example
|
||||
end
|
||||
'''
|
||||
290
README.md
290
README.md
@@ -1,63 +1,263 @@
|
||||
# tui-rs
|
||||
# Ratatui
|
||||
|
||||
[](https://travis-ci.org/fdehau/tui-rs)
|
||||
[](https://crates.io/crates/tui)
|
||||
[](https://docs.rs/crate/tui/)
|
||||
<img align="left" src="https://avatars.githubusercontent.com/u/125200832?s=128&v=4">
|
||||
|
||||
<img src="./docs/demo.gif" alt="Demo cast under Linux Termite with Inconsolata font 12pt">
|
||||
`ratatui` is a [Rust](https://www.rust-lang.org) library to build rich terminal user interfaces and
|
||||
dashboards. It is a community fork of the original [tui-rs](https://github.com/fdehau/tui-rs)
|
||||
project.
|
||||
|
||||
`tui-rs` is a [Rust](https://www.rust-lang.org) library to build rich terminal
|
||||
user interfaces and dashboards. It is heavily inspired by the `Javascript`
|
||||
library [blessed-contrib](https://github.com/yaronn/blessed-contrib) and the
|
||||
`Go` library [termui](https://github.com/gizak/termui).
|
||||
[](https://crates.io/crates/ratatui)
|
||||
[](./LICENSE) [](https://github.com/tui-rs-revival/ratatui/actions?query=workflow%3ACI+)
|
||||
[](https://docs.rs/crate/ratatui/)
|
||||
[](https://deps.rs/repo/github/tui-rs-revival/ratatui)
|
||||
[](https://app.codecov.io/gh/tui-rs-revival/ratatui)
|
||||
[](https://discord.gg/pMCEU9hNEj)
|
||||
|
||||
The library itself supports two different backends to draw to the terminal. You
|
||||
can either choose from:
|
||||
<!-- See RELEASE.md for instructions on creating the demo gif --->
|
||||

|
||||
|
||||
- [termion](https://github.com/ticki/termion)
|
||||
- [rustbox](https://github.com/gchp/rustbox)
|
||||
<details>
|
||||
<summary>Table of Contents</summary>
|
||||
|
||||
However, some features may only be available in one of the two.
|
||||
* [Ratatui](#ratatui)
|
||||
* [Installation](#installation)
|
||||
* [Introduction](#introduction)
|
||||
* [Quickstart](#quickstart)
|
||||
* [Status of this fork](#status-of-this-fork)
|
||||
* [Rust version requirements](#rust-version-requirements)
|
||||
* [Documentation](#documentation)
|
||||
* [Examples](#examples)
|
||||
* [Widgets](#widgets)
|
||||
* [Built in](#built-in)
|
||||
* [Third\-party libraries, bootstrapping templates and
|
||||
widgets](#third-party-libraries-bootstrapping-templates-and-widgets)
|
||||
* [Apps](#apps)
|
||||
* [Alternatives](#alternatives)
|
||||
* [Contributors](#contributors)
|
||||
* [Acknowledgments](#acknowledgments)
|
||||
* [License](#license)
|
||||
|
||||
The library is based on the principle of immediate rendering with intermediate
|
||||
buffers. This means that at each new frame you should build all widgets that are
|
||||
supposed to be part of the UI. While providing a great flexibility for rich and
|
||||
interactive UI, this may introduce overhead for highly dynamic content. So, the
|
||||
implementation try to minimize the number of ansi escapes sequences generated to
|
||||
draw the updated UI. In practice, given the speed of `Rust` the overhead rather
|
||||
comes from the terminal emulator than the library itself.
|
||||
</details>
|
||||
|
||||
Moreover, the library does not provide any input handling nor any event system and
|
||||
you may rely on the previously cited libraries to achieve such features.
|
||||
## Installation
|
||||
|
||||
### [Documentation](https://docs.rs/tui)
|
||||
```shell
|
||||
cargo add ratatui --features all-widgets
|
||||
```
|
||||
|
||||
### Widgets
|
||||
Or modify your `Cargo.toml`
|
||||
|
||||
The library comes with the following list of widgets:
|
||||
```toml
|
||||
[dependencies]
|
||||
ratatui = { version = "0.21.0", features = ["all-widgets"]}
|
||||
```
|
||||
|
||||
* [Block](examples/block.rs)
|
||||
* [Gauge](examples/gauge.rs)
|
||||
* [Sparkline](examples/sparkline.rs)
|
||||
* [Chart](examples/chart.rs)
|
||||
* [BarChart](examples/bar_chart.rs)
|
||||
* [List](examples/list.rs)
|
||||
* [Table](examples/table.rs)
|
||||
* [Paragraph](examples/paragraph.rs)
|
||||
* [Canvas (with line, point cloud, map)](examples/canvas.rs)
|
||||
* [Tabs](examples/tabs.rs)
|
||||
Ratatui is mostly backwards compatible with `tui-rs`. To migrate an existing project, it may be
|
||||
easier to rename the ratatui dependency to `tui` rather than updating every usage of the crate.
|
||||
E.g.:
|
||||
|
||||
Click on each item to see the source of the example. Run the examples with with
|
||||
cargo (e.g. to run the demo `cargo run --example demo`), and quit by pressing `q`.
|
||||
```toml
|
||||
[dependencies]
|
||||
tui = { package = "ratatui", version = "0.21.0", features = ["all-widgets"]}
|
||||
```
|
||||
|
||||
### Demo
|
||||
## Introduction
|
||||
|
||||
The [source code](examples/demo.rs) of the demo gif.
|
||||
`ratatui` is a terminal UI library that supports multiple backends:
|
||||
|
||||
* [crossterm](https://github.com/crossterm-rs/crossterm) [default]
|
||||
* [termion](https://github.com/ticki/termion)
|
||||
* [termwiz](https://github.com/wez/wezterm/tree/master/termwiz)
|
||||
|
||||
The library is based on the principle of immediate rendering with intermediate buffers. This means
|
||||
that at each new frame you should build all widgets that are supposed to be part of the UI. While
|
||||
providing a great flexibility for rich and interactive UI, this may introduce overhead for highly
|
||||
dynamic content. So, the implementation try to minimize the number of ansi escapes sequences
|
||||
generated to draw the updated UI. In practice, given the speed of `Rust` the overhead rather comes
|
||||
from the terminal emulator than the library itself.
|
||||
|
||||
Moreover, the library does not provide any input handling nor any event system and you may rely on
|
||||
the previously cited libraries to achieve such features.
|
||||
|
||||
We keep a [CHANGELOG](./CHANGELOG.md) generated by [git-cliff](https://github.com/orhun/git-cliff)
|
||||
utilizing [Conventional Commits](https://www.conventionalcommits.org/).
|
||||
|
||||
## Quickstart
|
||||
|
||||
The following example demonstrates the minimal amount of code necessary to setup a terminal and
|
||||
render "Hello World!". The full code for this example which contains a little more detail is in
|
||||
[hello_world.rs](./examples/hello_world.rs). For more guidance on how to create Ratatui apps, see
|
||||
the [Docs](https://docs.rs/ratatui) and [Examples](#examples). There is also a starter template
|
||||
available at [rust-tui-template](https://github.com/tui-rs-revival/rust-tui-template).
|
||||
|
||||
```rust
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let mut terminal = setup_terminal()?;
|
||||
run(&mut terminal)?;
|
||||
restore_terminal(&mut terminal)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>, Box<dyn Error>> {
|
||||
let mut stdout = io::stdout();
|
||||
enable_raw_mode()?;
|
||||
execute!(stdout, EnterAlternateScreen)?;
|
||||
Ok(Terminal::new(CrosstermBackend::new(stdout))?)
|
||||
}
|
||||
|
||||
fn restore_terminal(
|
||||
terminal: &mut Terminal<CrosstermBackend<Stdout>>,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
disable_raw_mode()?;
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen,)?;
|
||||
Ok(terminal.show_cursor()?)
|
||||
}
|
||||
|
||||
fn run(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<(), Box<dyn Error>> {
|
||||
Ok(loop {
|
||||
terminal.draw(|frame| {
|
||||
let greeting = Paragraph::new("Hello World!");
|
||||
frame.render_widget(greeting, frame.size());
|
||||
})?;
|
||||
if event::poll(Duration::from_millis(250))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if KeyCode::Char('q') == key.code {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Status of this fork
|
||||
|
||||
In response to the original maintainer [**Florian Dehau**](https://github.com/fdehau)'s issue
|
||||
regarding the [future of `tui-rs`](https://github.com/fdehau/tui-rs/issues/654), several members of
|
||||
the community forked the project and created this crate. We look forward to continuing the work
|
||||
started by Florian 🚀
|
||||
|
||||
In order to organize ourselves, we currently use a [Discord server](https://discord.gg/pMCEU9hNEj),
|
||||
feel free to join and come chat! There are also plans to implement a [Matrix](https://matrix.org/)
|
||||
bridge in the near future. **Discord is not a MUST to contribute**. We follow a pretty standard
|
||||
github centered open source workflow keeping the most important conversations on GitHub, open an
|
||||
issue or PR and it will be addressed. 😄
|
||||
|
||||
Please make sure you read the updated [contributing](./CONTRIBUTING.md) guidelines, especially if
|
||||
you are interested in working on a PR or issue opened in the previous repository.
|
||||
|
||||
## Rust version requirements
|
||||
|
||||
Since version 0.21.0, The Minimum Supported Rust Version (MSRV) of `ratatui` is 1.65.0.
|
||||
|
||||
## Documentation
|
||||
|
||||
The documentation can be found on [docs.rs.](https://docs.rs/ratatui)
|
||||
|
||||
## Examples
|
||||
|
||||
The demo shown in the gif above is available on all available backends.
|
||||
|
||||
```shell
|
||||
# crossterm
|
||||
cargo run --example demo
|
||||
# termion
|
||||
cargo run --example demo --no-default-features --features=termion
|
||||
# termwiz
|
||||
cargo run --example demo --no-default-features --features=termwiz
|
||||
```
|
||||
|
||||
The UI code for the is in [examples/demo/ui.rs](./examples/demo/ui.rs) while the application state
|
||||
is in [examples/demo/app.rs](./examples/demo/app.rs).
|
||||
|
||||
If the user interface contains glyphs that are not displayed correctly by your terminal, you may
|
||||
want to run the demo without those symbols:
|
||||
|
||||
```shell
|
||||
cargo run --example demo --release -- --tick-rate 200 --enhanced-graphics false
|
||||
```
|
||||
|
||||
More examples are available in the [examples](./examples/) folder.
|
||||
|
||||
## Widgets
|
||||
|
||||
### Built in
|
||||
|
||||
The library comes with the following
|
||||
[widgets](https://docs.rs/ratatui/latest/ratatui/widgets/index.html):
|
||||
|
||||
* [Canvas](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/struct.Canvas.html) which allows
|
||||
rendering [points, lines, shapes and a world
|
||||
map](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/index.html)
|
||||
* [BarChart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.BarChart.html)
|
||||
* [Block](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Block.html)
|
||||
* [Calendar](https://docs.rs/ratatui/latest/ratatui/widgets/calendar/index.html)
|
||||
* [Chart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Chart.html)
|
||||
* [Gauge](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Gauge.html)
|
||||
* [List](https://docs.rs/ratatui/latest/ratatui/widgets/struct.List.html)
|
||||
* [Paragraph](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Paragraph.html)
|
||||
* [Sparkline](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Sparkline.html)
|
||||
* [Table](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Table.html)
|
||||
* [Tabs](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Tabs.html)
|
||||
|
||||
Each wiget has an associated example which can be found in the [examples](./examples/) folder. Run
|
||||
each examples with cargo (e.g. to run the gauge example `cargo run --example gauge`), and quit by
|
||||
pressing `q`.
|
||||
|
||||
You can also run all examples by running `cargo make run-examples` (requires `cargo-make` that can
|
||||
be installed with `cargo install cargo-make`).
|
||||
|
||||
### Third-party libraries, bootstrapping templates and widgets
|
||||
|
||||
* [ansi-to-tui](https://github.com/uttarayan21/ansi-to-tui) — Convert ansi colored text to
|
||||
`tui::text::Text`
|
||||
* [color-to-tui](https://github.com/uttarayan21/color-to-tui) — Parse hex colors to
|
||||
`tui::style::Color`
|
||||
* [rust-tui-template](https://github.com/orhun/rust-tui-template) — A template for bootstrapping a
|
||||
Rust TUI application with Tui-rs & crossterm
|
||||
* [simple-tui-rs](https://github.com/pmsanford/simple-tui-rs) — A simple example tui-rs app
|
||||
* [tui-builder](https://github.com/jkelleyrtp/tui-builder) — Batteries-included MVC framework for
|
||||
Tui-rs + Crossterm apps
|
||||
* [tui-clap](https://github.com/kegesch/tui-clap-rs) — Use clap-rs together with Tui-rs
|
||||
* [tui-log](https://github.com/kegesch/tui-log-rs) — Example of how to use logging with Tui-rs
|
||||
* [tui-logger](https://github.com/gin66/tui-logger) — Logger and Widget for Tui-rs
|
||||
* [tui-realm](https://github.com/veeso/tui-realm) — Tui-rs framework to build stateful applications
|
||||
with a React/Elm inspired approach
|
||||
* [tui-realm-treeview](https://github.com/veeso/tui-realm-treeview) — Treeview component for
|
||||
Tui-realm
|
||||
* [tui-rs-tree-widgets](https://github.com/EdJoPaTo/tui-rs-tree-widget): Widget for tree data
|
||||
structures.
|
||||
* [tui-windows](https://github.com/markatk/tui-windows-rs) — Tui-rs abstraction to handle multiple
|
||||
windows and their rendering
|
||||
* [tui-textarea](https://github.com/rhysd/tui-textarea): Simple yet powerful multi-line text editor
|
||||
widget supporting several key shortcuts, undo/redo, text search, etc.
|
||||
* [tui-input](https://github.com/sayanarijit/tui-input): TUI input library supporting multiple
|
||||
backends and tui-rs.
|
||||
* [tui-term](https://github.com/a-kenji/tui-term): A pseudoterminal widget library
|
||||
that enables the rendering of terminal applications as ratatui widgets.
|
||||
|
||||
## Apps
|
||||
|
||||
Check out the list of more than 50 [Apps using
|
||||
`Ratatui`](https://github.com/tui-rs-revival/ratatui/wiki/Apps-using-Ratatui)!
|
||||
|
||||
## Alternatives
|
||||
|
||||
You might want to checkout [Cursive](https://github.com/gyscos/Cursive) for an alternative solution
|
||||
to build text user interfaces in Rust.
|
||||
|
||||
## Contributors
|
||||
|
||||
[](https://github.com/tui-rs-revival/ratatui/graphs/contributors)
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
Special thanks to [**Pavel Fomchenkov**](https://github.com/nawok) for his work in designing **an
|
||||
awesome logo** for the ratatui project and tui-rs-revival organization.
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
||||
## Author
|
||||
|
||||
Florian Dehau
|
||||
[MIT](./LICENSE)
|
||||
|
||||
30
RELEASE.md
Normal file
30
RELEASE.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Creating a Release
|
||||
|
||||
[crates.io](https://crates.io/crates/ratatui) releases are automated via [GitHub
|
||||
actions](.github/workflows/cd.yml) and triggered by pushing a tag.
|
||||
|
||||
1. Record a new demo gif. The preferred tool for this is [ttyrec](http://0xcc.net/ttyrec/) and
|
||||
[ttygif](https://github.com/icholy/ttygif). [Asciinema](https://asciinema.org/) handles block
|
||||
character height poorly, [termanilizer](https://www.terminalizer.com/) takes forever to render,
|
||||
[vhs](https://github.com/charmbracelet/vhs) handles braille
|
||||
characters poorly (though if <https://github.com/charmbracelet/vhs/issues/322> is fixed, then
|
||||
it's probably the best option).
|
||||
|
||||
```shell
|
||||
cargo build --example demo
|
||||
ttyrec -e 'cargo --quiet run --release --example demo -- --tick-rate 100' demo.rec
|
||||
ttygif demo.rec
|
||||
```
|
||||
|
||||
Then upload it somewhere (e.g. use `vhs publish tty.gif` to publish it or upload it to a GitHub
|
||||
wiki page as an attachment). Avoid adding the gif to the git repo as binary files tend to bloat
|
||||
repositories.
|
||||
|
||||
1. Bump the version in [Cargo.toml](Cargo.toml).
|
||||
1. Ensure [CHANGELOG.md](CHANGELOG.md) is updated. [git-cliff](https://github.com/orhun/git-cliff)
|
||||
can be used for generating the entries.
|
||||
1. Commit and push the changes.
|
||||
1. Create a new tag: `git tag -a v[X.Y.Z]`
|
||||
1. Push the tag: `git push --tags`
|
||||
1. Wait for [Continuous Deployment](https://github.com/tui-rs-revival/ratatui/actions) workflow to
|
||||
finish.
|
||||
91
bacon.toml
Normal file
91
bacon.toml
Normal file
@@ -0,0 +1,91 @@
|
||||
# This is a configuration file for the bacon tool
|
||||
#
|
||||
# Bacon repository: https://github.com/Canop/bacon
|
||||
# Complete help on configuration: https://dystroy.org/bacon/config/
|
||||
# You can also check bacon's own bacon.toml file
|
||||
# as an example: https://github.com/Canop/bacon/blob/main/bacon.toml
|
||||
|
||||
default_job = "check"
|
||||
|
||||
[jobs.check]
|
||||
command = ["cargo", "check", "--all-features", "--color", "always"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.check-all]
|
||||
command = ["cargo", "check", "--all-targets", "--all-features", "--color", "always"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.clippy]
|
||||
command = [
|
||||
"cargo", "clippy",
|
||||
"--all-targets",
|
||||
"--color", "always",
|
||||
]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.test]
|
||||
command = [
|
||||
"cargo", "test",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
"--", "--color", "always", # see https://github.com/Canop/bacon/issues/124
|
||||
]
|
||||
need_stdout = true
|
||||
|
||||
[jobs.doc]
|
||||
command = [
|
||||
"cargo", "+nightly", "doc",
|
||||
"-Zunstable-options", "-Zrustdoc-scrape-examples",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
"--no-deps",
|
||||
]
|
||||
env.RUSTDOCFLAGS = "--cfg docsrs"
|
||||
need_stdout = false
|
||||
|
||||
# If the doc compiles, then it opens in your browser and bacon switches
|
||||
# to the previous job
|
||||
[jobs.doc-open]
|
||||
command = [
|
||||
"cargo", "+nightly", "doc",
|
||||
"-Zunstable-options", "-Zrustdoc-scrape-examples",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
"--no-deps",
|
||||
"--open",
|
||||
]
|
||||
env.RUSTDOCFLAGS = "--cfg docsrs"
|
||||
need_stdout = false
|
||||
on_success = "job:doc" # so that we don't open the browser at each change
|
||||
|
||||
# You can run your application and have the result displayed in bacon,
|
||||
# *if* it makes sense for this crate. You can run an example the same
|
||||
# way. Don't forget the `--color always` part or the errors won't be
|
||||
# properly parsed.
|
||||
[jobs.run]
|
||||
command = [
|
||||
"cargo", "run",
|
||||
"--color", "always",
|
||||
# put launch parameters for your program behind a `--` separator
|
||||
]
|
||||
need_stdout = true
|
||||
allow_warnings = true
|
||||
|
||||
[jobs.check-crossterm]
|
||||
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "crossterm"]
|
||||
|
||||
[jobs.check-termion]
|
||||
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "termion"]
|
||||
|
||||
[jobs.check-termwiz]
|
||||
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "termwiz"]
|
||||
|
||||
# You may define here keybindings that would be specific to
|
||||
# a project, for example a shortcut to launch a specific job.
|
||||
# Shortcuts to internal functions (scrolling, toggling, etc.)
|
||||
# should go in your personal global prefs.toml file instead.
|
||||
[keybindings]
|
||||
# alt-m = "job:my-job"
|
||||
ctrl-c = "job:check-crossterm"
|
||||
ctrl-t = "job:check-termion"
|
||||
ctrl-w = "job:check-termwiz"
|
||||
89
benches/paragraph.rs
Normal file
89
benches/paragraph.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
use criterion::{black_box, criterion_group, criterion_main, Bencher, BenchmarkId, Criterion};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
widgets::{Paragraph, Widget, Wrap},
|
||||
};
|
||||
|
||||
/// because the scroll offset is a u16, the maximum number of lines that can be scrolled is 65535.
|
||||
/// This is a limitation of the current implementation and may be fixed by changing the type of the
|
||||
/// scroll offset to a u32.
|
||||
const MAX_SCROLL_OFFSET: u16 = u16::MAX;
|
||||
const NO_WRAP_WIDTH: u16 = 200;
|
||||
const WRAP_WIDTH: u16 = 100;
|
||||
|
||||
/// Benchmark for rendering a paragraph with a given number of lines. The design of this benchmark
|
||||
/// allows comparison of the performance of rendering a paragraph with different numbers of lines.
|
||||
/// as well as comparing with the various settings on the scroll and wrap features.
|
||||
pub fn paragraph(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("paragraph");
|
||||
for &line_count in [64, 2048, MAX_SCROLL_OFFSET].iter() {
|
||||
let lines = random_lines(line_count);
|
||||
let lines = lines.as_str();
|
||||
|
||||
// benchmark that measures the overhead of creating a paragraph separately from rendering
|
||||
group.bench_with_input(BenchmarkId::new("new", line_count), lines, |b, lines| {
|
||||
b.iter(|| Paragraph::new(black_box(lines)))
|
||||
});
|
||||
|
||||
// render the paragraph with no scroll
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("render", line_count),
|
||||
&Paragraph::new(lines),
|
||||
|bencher, paragraph| render(bencher, paragraph, NO_WRAP_WIDTH),
|
||||
);
|
||||
|
||||
// scroll the paragraph by half the number of lines and render
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("render_scroll_half", line_count),
|
||||
&Paragraph::new(lines).scroll((0u16, line_count / 2)),
|
||||
|bencher, paragraph| render(bencher, paragraph, NO_WRAP_WIDTH),
|
||||
);
|
||||
|
||||
// scroll the paragraph by the full number of lines and render
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("render_scroll_full", line_count),
|
||||
&Paragraph::new(lines).scroll((0u16, line_count)),
|
||||
|bencher, paragraph| render(bencher, paragraph, NO_WRAP_WIDTH),
|
||||
);
|
||||
|
||||
// render the paragraph wrapped to 100 characters
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("render_wrap", line_count),
|
||||
&Paragraph::new(lines).wrap(Wrap { trim: false }),
|
||||
|bencher, paragraph| render(bencher, paragraph, WRAP_WIDTH),
|
||||
);
|
||||
|
||||
// scroll the paragraph by the full number of lines and render wrapped to 100 characters
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("render_wrap_scroll_full", line_count),
|
||||
&Paragraph::new(lines)
|
||||
.wrap(Wrap { trim: false })
|
||||
.scroll((0u16, line_count)),
|
||||
|bencher, paragraph| render(bencher, paragraph, WRAP_WIDTH),
|
||||
);
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// render the paragraph into a buffer with the given width
|
||||
fn render(bencher: &mut Bencher, paragraph: &Paragraph, width: u16) {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, width, 50));
|
||||
bencher.iter(|| {
|
||||
paragraph.clone().render(buffer.area, &mut buffer);
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a string with the given number of lines filled with nonsense words
|
||||
///
|
||||
/// English language has about 5.1 average characters per word so including the space between words
|
||||
/// this should emit around 200 characters per paragraph on average.
|
||||
fn random_lines(count: u16) -> String {
|
||||
let count = count as i64;
|
||||
let sentence_count = 3;
|
||||
let word_count = 11;
|
||||
fakeit::words::paragraph(count, sentence_count, word_count, "\n".into())
|
||||
}
|
||||
|
||||
criterion_group!(benches, paragraph);
|
||||
criterion_main!(benches);
|
||||
86
cliff.toml
Normal file
86
cliff.toml
Normal file
@@ -0,0 +1,86 @@
|
||||
# configuration for https://github.com/orhun/git-cliff
|
||||
|
||||
[changelog]
|
||||
# changelog header
|
||||
header = """
|
||||
# Changelog\n
|
||||
All notable changes to this project will be documented in this file.\n
|
||||
"""
|
||||
# template for the changelog body
|
||||
# https://tera.netlify.app/docs/#introduction
|
||||
body = """
|
||||
{% if version %}\
|
||||
## {{ version }} - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% else %}\
|
||||
## [unreleased]
|
||||
{% endif %}\
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
### {{ group | striptags | trim | upper_first }}
|
||||
{% for commit in commits
|
||||
| filter(attribute="scope")
|
||||
| sort(attribute="scope") %}
|
||||
- *({{commit.scope}})* {{ commit.message | upper_first }}{% if commit.breaking %} [**breaking**]{% endif %}
|
||||
{%- endfor -%}
|
||||
{% raw %}\n{% endraw %}\
|
||||
{%- for commit in commits %}
|
||||
{%- if commit.scope -%}
|
||||
{% else -%}
|
||||
- *(uncategorized)* {{ commit.message | upper_first }}{% if commit.breaking %} [**breaking**]{% endif %}
|
||||
{% endif -%}
|
||||
{% endfor -%}
|
||||
{% endfor %}\n
|
||||
"""
|
||||
# remove the leading and trailing whitespace from the template
|
||||
trim = true
|
||||
# changelog footer
|
||||
footer = """
|
||||
<!-- generated by git-cliff -->
|
||||
"""
|
||||
|
||||
[git]
|
||||
# parse the commits based on https://www.conventionalcommits.org
|
||||
conventional_commits = true
|
||||
# filter out the commits that are not conventional
|
||||
filter_unconventional = true
|
||||
# process each line of a commit as an individual commit
|
||||
split_commits = false
|
||||
# regex for preprocessing the commit messages
|
||||
commit_preprocessors = [
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/tui-rs-revival/ratatui/issues/${2}))" },
|
||||
{ pattern = '(better safe shared layout cache)', replace = "perf(layout): ${1}" },
|
||||
{ pattern = '(Clarify README.md)', replace = "docs(readme): ${1}" },
|
||||
{ pattern = '(Update README.md)', replace = "docs(readme): ${1}" },
|
||||
{ pattern = '(fix typos|Fix typos)', replace = "fix: ${1}" },
|
||||
]
|
||||
# regex for parsing and grouping commits
|
||||
commit_parsers = [
|
||||
{ message = "^feat", group = "<!-- 00 -->Features" },
|
||||
{ message = "^[fF]ix", group = "<!-- 01 -->Bug Fixes" },
|
||||
{ message = "^refactor", group = "<!-- 02 -->Refactor" },
|
||||
{ message = "^doc", group = "<!-- 03 -->Documentation" },
|
||||
{ message = "^perf", group = "<!-- 04 -->Performance" },
|
||||
{ message = "^style", group = "<!-- 05 -->Styling" },
|
||||
{ message = "^test", group = "<!-- 06 -->Testing" },
|
||||
{ message = "^chore\\(release\\): prepare for", skip = true },
|
||||
{ message = "^chore\\(pr\\)", skip = true },
|
||||
{ message = "^chore\\(pull\\)", skip = true },
|
||||
{ message = "^chore", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
{ body = ".*security", group = "<!-- 08 -->Security" },
|
||||
{ message = "^build", group = "<!-- 09 -->Build" },
|
||||
{ message = "^ci", group = "<!-- 10 -->Continuous Integration" },
|
||||
{ message = "^revert", group = "<!-- 11 -->Reverted Commits" },
|
||||
]
|
||||
# protect breaking changes from being skipped due to matching a skipping commit_parser
|
||||
protect_breaking_commits = false
|
||||
# filter out the commits that are not matched by commit parsers
|
||||
filter_commits = false
|
||||
# glob pattern for matching git tags
|
||||
tag_pattern = "v[0-9]*"
|
||||
# regex for skipping tags
|
||||
skip_tags = "v0.1.0-rc.1"
|
||||
# regex for ignoring tags
|
||||
ignore_tags = ""
|
||||
# sort the tags topologically
|
||||
topo_order = false
|
||||
# sort the commits inside sections by oldest/newest order
|
||||
sort_commits = "newest"
|
||||
30
committed.toml
Normal file
30
committed.toml
Normal file
@@ -0,0 +1,30 @@
|
||||
# configuration for https://github.com/crate-ci/committed
|
||||
|
||||
# https://www.conventionalcommits.org
|
||||
style = "conventional"
|
||||
# disallow merge commits
|
||||
merge_commit = false
|
||||
# subject is not required to be capitalized
|
||||
subject_capitalized = false
|
||||
# subject should start with an imperative verb
|
||||
imperative_subject = true
|
||||
# subject should not end with a punctuation
|
||||
subject_not_punctuated = true
|
||||
# disable line length
|
||||
line_length = 0
|
||||
# disable subject length
|
||||
subject_length = 0
|
||||
# default allowed_types [ "chore", "docs", "feat", "fix", "perf", "refactor", "style", "test" ]
|
||||
allowed_types = [
|
||||
"build",
|
||||
"chore",
|
||||
"ci",
|
||||
"docs",
|
||||
"feat",
|
||||
"fix",
|
||||
"perf",
|
||||
"refactor",
|
||||
"revert",
|
||||
"style",
|
||||
"test",
|
||||
]
|
||||
28
deny.toml
Normal file
28
deny.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
# configuration for https://github.com/EmbarkStudios/cargo-deny
|
||||
|
||||
[licenses]
|
||||
default = "deny"
|
||||
unlicensed = "deny"
|
||||
copyleft = "deny"
|
||||
confidence-threshold = 0.8
|
||||
allow = [
|
||||
"Apache-2.0",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"Unicode-DFS-2016",
|
||||
"WTFPL",
|
||||
]
|
||||
|
||||
[advisories]
|
||||
unmaintained = "deny"
|
||||
yanked = "deny"
|
||||
|
||||
[bans]
|
||||
multiple-versions = "allow"
|
||||
|
||||
[sources]
|
||||
unknown-registry = "deny"
|
||||
unknown-git = "warn"
|
||||
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||
BIN
docs/demo.gif
BIN
docs/demo.gif
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 MiB |
@@ -1,29 +1,29 @@
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use std::io;
|
||||
use std::thread;
|
||||
use std::time;
|
||||
use std::sync::mpsc;
|
||||
|
||||
use termion::event;
|
||||
use termion::input::TermRead;
|
||||
|
||||
use tui::Terminal;
|
||||
use tui::backend::MouseBackend;
|
||||
use tui::widgets::{BarChart, Block, Borders, Widget};
|
||||
use tui::layout::{Direction, Group, Rect, Size};
|
||||
use tui::style::{Color, Modifier, Style};
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
widgets::{BarChart, Block, Borders},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
struct App<'a> {
|
||||
size: Rect,
|
||||
data: Vec<(&'a str, u64)>,
|
||||
}
|
||||
|
||||
impl<'a> App<'a> {
|
||||
fn new() -> App<'a> {
|
||||
App {
|
||||
size: Rect::default(),
|
||||
data: vec![
|
||||
("B1", 9),
|
||||
("B2", 12),
|
||||
@@ -53,112 +53,110 @@ impl<'a> App<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn advance(&mut self) {
|
||||
fn on_tick(&mut self) {
|
||||
let value = self.data.pop().unwrap();
|
||||
self.data.insert(0, value);
|
||||
}
|
||||
}
|
||||
|
||||
enum Event {
|
||||
Input(event::Key),
|
||||
Tick,
|
||||
}
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
fn main() {
|
||||
// Terminal initialization
|
||||
let backend = MouseBackend::new().unwrap();
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// Channels
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let input_tx = tx.clone();
|
||||
let clock_tx = tx.clone();
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
// Input
|
||||
thread::spawn(move || {
|
||||
let stdin = io::stdin();
|
||||
for c in stdin.keys() {
|
||||
let evt = c.unwrap();
|
||||
input_tx.send(Event::Input(evt)).unwrap();
|
||||
if evt == event::Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Tick
|
||||
thread::spawn(move || loop {
|
||||
clock_tx.send(Event::Tick).unwrap();
|
||||
thread::sleep(time::Duration::from_millis(500));
|
||||
});
|
||||
|
||||
// App
|
||||
let mut app = App::new();
|
||||
|
||||
// First draw call
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
app.size = terminal.size().unwrap();
|
||||
draw(&mut terminal, &app);
|
||||
|
||||
loop {
|
||||
let size = terminal.size().unwrap();
|
||||
if app.size != size {
|
||||
terminal.resize(size).unwrap();
|
||||
app.size = size;
|
||||
}
|
||||
|
||||
let evt = rx.recv().unwrap();
|
||||
match evt {
|
||||
Event::Input(input) => if input == event::Key::Char('q') {
|
||||
break;
|
||||
},
|
||||
Event::Tick => {
|
||||
app.advance();
|
||||
}
|
||||
}
|
||||
draw(&mut terminal, &app);
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
terminal.show_cursor().unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<MouseBackend>, app: &App) {
|
||||
Group::default()
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.sizes(&[Size::Percent(50), Size::Percent(50)])
|
||||
.render(t, &app.size, |t, chunks| {
|
||||
BarChart::default()
|
||||
.block(Block::default().title("Data1").borders(Borders::ALL))
|
||||
.data(&app.data)
|
||||
.bar_width(9)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.value_style(Style::default().fg(Color::Black).bg(Color::Yellow))
|
||||
.render(t, &chunks[0]);
|
||||
Group::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.sizes(&[Size::Percent(50), Size::Percent(50)])
|
||||
.render(t, &chunks[1], |t, chunks| {
|
||||
BarChart::default()
|
||||
.block(Block::default().title("Data2").borders(Borders::ALL))
|
||||
.data(&app.data)
|
||||
.bar_width(5)
|
||||
.bar_gap(3)
|
||||
.style(Style::default().fg(Color::Green))
|
||||
.value_style(Style::default().bg(Color::Green).modifier(Modifier::Bold))
|
||||
.render(t, &chunks[0]);
|
||||
BarChart::default()
|
||||
.block(Block::default().title("Data3").borders(Borders::ALL))
|
||||
.data(&app.data)
|
||||
.style(Style::default().fg(Color::Red))
|
||||
.bar_width(7)
|
||||
.bar_gap(0)
|
||||
.value_style(Style::default().bg(Color::Red))
|
||||
.label_style(Style::default().fg(Color::Cyan).modifier(Modifier::Italic))
|
||||
.render(t, &chunks[1]);
|
||||
})
|
||||
});
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(f.size());
|
||||
let barchart = BarChart::default()
|
||||
.block(Block::default().title("Data1").borders(Borders::ALL))
|
||||
.data(&app.data)
|
||||
.bar_width(9)
|
||||
.bar_style(Style::default().fg(Color::Yellow))
|
||||
.value_style(Style::default().fg(Color::Black).bg(Color::Yellow));
|
||||
f.render_widget(barchart, chunks[0]);
|
||||
|
||||
t.draw().unwrap();
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(chunks[1]);
|
||||
|
||||
let barchart = BarChart::default()
|
||||
.block(Block::default().title("Data2").borders(Borders::ALL))
|
||||
.data(&app.data)
|
||||
.bar_width(5)
|
||||
.bar_gap(3)
|
||||
.bar_style(Style::default().fg(Color::Green))
|
||||
.value_style(
|
||||
Style::default()
|
||||
.bg(Color::Green)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
f.render_widget(barchart, chunks[0]);
|
||||
|
||||
let barchart = BarChart::default()
|
||||
.block(Block::default().title("Data3").borders(Borders::ALL))
|
||||
.data(&app.data)
|
||||
.bar_style(Style::default().fg(Color::Red))
|
||||
.bar_width(7)
|
||||
.bar_gap(0)
|
||||
.value_style(Style::default().bg(Color::Red))
|
||||
.label_style(
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::ITALIC),
|
||||
);
|
||||
f.render_widget(barchart, chunks[1]);
|
||||
}
|
||||
|
||||
@@ -1,83 +1,126 @@
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
use std::{error::Error, io};
|
||||
|
||||
use std::io;
|
||||
use termion::event;
|
||||
use termion::input::TermRead;
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Alignment, Constraint, Direction, Layout},
|
||||
style::{Color, Style, Stylize},
|
||||
widgets::{block::title::Title, Block, BorderType, Borders, Padding, Paragraph},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
use tui::Terminal;
|
||||
use tui::backend::MouseBackend;
|
||||
use tui::widgets::{Block, Borders, Widget};
|
||||
use tui::layout::{Direction, Group, Rect, Size};
|
||||
use tui::style::{Color, Modifier, Style};
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
fn main() {
|
||||
let mut terminal = Terminal::new(MouseBackend::new().unwrap()).unwrap();
|
||||
let stdin = io::stdin();
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
// create app and run it
|
||||
let res = run_app(&mut terminal);
|
||||
|
||||
let mut term_size = terminal.size().unwrap();
|
||||
draw(&mut terminal, &term_size);
|
||||
for c in stdin.keys() {
|
||||
let size = terminal.size().unwrap();
|
||||
if term_size != size {
|
||||
terminal.resize(size).unwrap();
|
||||
term_size = size;
|
||||
}
|
||||
draw(&mut terminal, &term_size);
|
||||
let evt = c.unwrap();
|
||||
if evt == event::Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
terminal.show_cursor().unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<MouseBackend>, size: &Rect) {
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(ui)?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>) {
|
||||
// Wrapping block for a group
|
||||
// Just draw the block and the group on the same area and build the group
|
||||
// with at least a margin of 1
|
||||
Block::default().borders(Borders::ALL).render(t, size);
|
||||
Group::default()
|
||||
let size = f.size();
|
||||
|
||||
// Surrounding block
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(Title::from("Main block with round corners").alignment(Alignment::Center))
|
||||
.border_type(BorderType::Rounded);
|
||||
f.render_widget(block, size);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(4)
|
||||
.sizes(&[Size::Percent(50), Size::Percent(50)])
|
||||
.render(t, size, |t, chunks| {
|
||||
Group::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.sizes(&[Size::Percent(50), Size::Percent(50)])
|
||||
.render(t, &chunks[0], |t, chunks| {
|
||||
Block::default()
|
||||
.title("With background")
|
||||
.title_style(Style::default().fg(Color::Yellow))
|
||||
.style(Style::default().bg(Color::Green))
|
||||
.render(t, &chunks[0]);
|
||||
Block::default()
|
||||
.title("Styled title")
|
||||
.title_style(
|
||||
Style::default()
|
||||
.fg(Color::White)
|
||||
.bg(Color::Red)
|
||||
.modifier(Modifier::Bold),
|
||||
)
|
||||
.render(t, &chunks[1]);
|
||||
});
|
||||
Group::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.sizes(&[Size::Percent(50), Size::Percent(50)])
|
||||
.render(t, &chunks[1], |t, chunks| {
|
||||
Block::default()
|
||||
.title("With borders")
|
||||
.borders(Borders::ALL)
|
||||
.render(t, &chunks[0]);
|
||||
Block::default()
|
||||
.title("With styled borders")
|
||||
.border_style(Style::default().fg(Color::Cyan))
|
||||
.borders(Borders::LEFT | Borders::RIGHT)
|
||||
.render(t, &chunks[1]);
|
||||
});
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(f.size());
|
||||
|
||||
// Top two inner blocks
|
||||
let top_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(chunks[0]);
|
||||
|
||||
// Top left inner block with green background
|
||||
let block = Block::default()
|
||||
.title(vec!["With".yellow(), " background".into()])
|
||||
.on_green();
|
||||
f.render_widget(block, top_chunks[0]);
|
||||
|
||||
// Top right inner block with styled title aligned to the right
|
||||
let block = Block::default()
|
||||
.title(Title::from("Styled title".white().on_red().bold()).alignment(Alignment::Right));
|
||||
f.render_widget(block, top_chunks[1]);
|
||||
|
||||
// Bottom two inner blocks
|
||||
let bottom_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(chunks[1]);
|
||||
|
||||
// Bottom left block with all default borders
|
||||
let block = Block::default()
|
||||
.title("With borders")
|
||||
.borders(Borders::ALL)
|
||||
.padding(Padding {
|
||||
left: 4,
|
||||
right: 4,
|
||||
top: 2,
|
||||
bottom: 2,
|
||||
});
|
||||
|
||||
t.draw().unwrap();
|
||||
let text = Paragraph::new("text inside padded block").block(block);
|
||||
f.render_widget(text, bottom_chunks[0]);
|
||||
|
||||
// Bottom right block with styled left and right border
|
||||
let block = Block::default()
|
||||
.title("With styled borders and doubled borders")
|
||||
.border_style(Style::default().fg(Color::Cyan))
|
||||
.borders(Borders::LEFT | Borders::RIGHT)
|
||||
.border_type(BorderType::Double)
|
||||
.padding(Padding::uniform(1));
|
||||
|
||||
let inner_block = Block::default()
|
||||
.title("Block inside padded block")
|
||||
.borders(Borders::ALL);
|
||||
|
||||
let inner_area = block.inner(bottom_chunks[1]);
|
||||
f.render_widget(block, bottom_chunks[1]);
|
||||
f.render_widget(inner_block, inner_area);
|
||||
}
|
||||
|
||||
283
examples/calendar.rs
Normal file
283
examples/calendar.rs
Normal file
@@ -0,0 +1,283 @@
|
||||
use std::{error::Error, io, rc::Rc};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
widgets::calendar::{CalendarEventStore, DateStyler, Monthly},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use time::{Date, Month, OffsetDateTime};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
loop {
|
||||
let _ = terminal.draw(|f| draw(f));
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
#[allow(clippy::single_match)]
|
||||
match key.code {
|
||||
KeyCode::Char(_) => {
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
disable_raw_mode()?;
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||
terminal.show_cursor()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw<B: Backend>(f: &mut Frame<B>) {
|
||||
let app_area = f.size();
|
||||
|
||||
let calarea = Rect {
|
||||
x: app_area.x + 1,
|
||||
y: app_area.y + 1,
|
||||
height: app_area.height - 1,
|
||||
width: app_area.width - 1,
|
||||
};
|
||||
|
||||
let mut start = OffsetDateTime::now_local()
|
||||
.unwrap()
|
||||
.date()
|
||||
.replace_month(Month::January)
|
||||
.unwrap()
|
||||
.replace_day(1)
|
||||
.unwrap();
|
||||
|
||||
let list = make_dates(start.year());
|
||||
|
||||
for chunk in split_rows(&calarea)
|
||||
.iter()
|
||||
.flat_map(|row| split_cols(row).to_vec())
|
||||
{
|
||||
let cal = cals::get_cal(start.month(), start.year(), &list);
|
||||
f.render_widget(cal, chunk);
|
||||
start = start.replace_month(start.month().next()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn split_rows(area: &Rect) -> Rc<[Rect]> {
|
||||
let list_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(0)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(33),
|
||||
Constraint::Percentage(33),
|
||||
Constraint::Percentage(33),
|
||||
]
|
||||
.as_ref(),
|
||||
);
|
||||
|
||||
list_layout.split(*area)
|
||||
}
|
||||
|
||||
fn split_cols(area: &Rect) -> Rc<[Rect]> {
|
||||
let list_layout = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.margin(0)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
]
|
||||
.as_ref(),
|
||||
);
|
||||
|
||||
list_layout.split(*area)
|
||||
}
|
||||
|
||||
fn make_dates(current_year: i32) -> CalendarEventStore {
|
||||
let mut list = CalendarEventStore::today(
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Blue),
|
||||
);
|
||||
|
||||
// Holidays
|
||||
let holiday_style = Style::default()
|
||||
.fg(Color::Red)
|
||||
.add_modifier(Modifier::UNDERLINED);
|
||||
|
||||
// new year's
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::January, 1).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
// next new_year's for December "show surrounding"
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year + 1, Month::January, 1).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
// groundhog day
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::February, 2).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
// april fool's
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::April, 1).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
// earth day
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::April, 22).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
// star wars day
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::May, 4).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
// festivus
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::December, 23).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
// new year's eve
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::December, 31).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
|
||||
// seasons
|
||||
let season_style = Style::default()
|
||||
.fg(Color::White)
|
||||
.bg(Color::Yellow)
|
||||
.add_modifier(Modifier::UNDERLINED);
|
||||
// spring equinox
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::March, 22).unwrap(),
|
||||
season_style,
|
||||
);
|
||||
// summer solstice
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::June, 21).unwrap(),
|
||||
season_style,
|
||||
);
|
||||
// fall equinox
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::September, 22).unwrap(),
|
||||
season_style,
|
||||
);
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::December, 21).unwrap(),
|
||||
season_style,
|
||||
);
|
||||
list
|
||||
}
|
||||
|
||||
mod cals {
|
||||
use super::*;
|
||||
|
||||
pub(super) fn get_cal<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
use Month::*;
|
||||
match m {
|
||||
May => example1(m, y, es),
|
||||
June => example2(m, y, es),
|
||||
July => example3(m, y, es),
|
||||
December => example3(m, y, es),
|
||||
February => example4(m, y, es),
|
||||
November => example5(m, y, es),
|
||||
_ => default(m, y, es),
|
||||
}
|
||||
}
|
||||
|
||||
fn default<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
let default_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Rgb(50, 50, 50));
|
||||
|
||||
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
|
||||
.show_month_header(Style::default())
|
||||
.default_style(default_style)
|
||||
}
|
||||
|
||||
fn example1<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
let default_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Rgb(50, 50, 50));
|
||||
|
||||
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
|
||||
.show_surrounding(default_style)
|
||||
.default_style(default_style)
|
||||
.show_month_header(Style::default())
|
||||
}
|
||||
|
||||
fn example2<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
let header_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::DIM)
|
||||
.fg(Color::LightYellow);
|
||||
|
||||
let default_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Rgb(50, 50, 50));
|
||||
|
||||
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
|
||||
.show_weekdays_header(header_style)
|
||||
.default_style(default_style)
|
||||
.show_month_header(Style::default())
|
||||
}
|
||||
|
||||
fn example3<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
let header_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::Green);
|
||||
|
||||
let default_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Rgb(50, 50, 50));
|
||||
|
||||
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
|
||||
.show_surrounding(Style::default().add_modifier(Modifier::DIM))
|
||||
.show_weekdays_header(header_style)
|
||||
.default_style(default_style)
|
||||
.show_month_header(Style::default())
|
||||
}
|
||||
|
||||
fn example4<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
let header_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::Green);
|
||||
|
||||
let default_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Rgb(50, 50, 50));
|
||||
|
||||
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
|
||||
.show_weekdays_header(header_style)
|
||||
.default_style(default_style)
|
||||
}
|
||||
|
||||
fn example5<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
let header_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::Green);
|
||||
|
||||
let default_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Rgb(50, 50, 50));
|
||||
|
||||
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
|
||||
.show_month_header(header_style)
|
||||
.default_style(default_style)
|
||||
}
|
||||
}
|
||||
@@ -1,54 +1,79 @@
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use std::io;
|
||||
use std::thread;
|
||||
use std::time;
|
||||
use std::sync::mpsc;
|
||||
|
||||
use termion::event;
|
||||
use termion::input::TermRead;
|
||||
|
||||
use tui::Terminal;
|
||||
use tui::backend::MouseBackend;
|
||||
use tui::widgets::{Block, Borders, Widget};
|
||||
use tui::widgets::canvas::{Canvas, Line, Map, MapResolution};
|
||||
use tui::layout::{Direction, Group, Rect, Size};
|
||||
use tui::style::Color;
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Stylize},
|
||||
symbols::Marker,
|
||||
widgets::{
|
||||
canvas::{Canvas, Map, MapResolution, Rectangle},
|
||||
Block, Borders,
|
||||
},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
struct App {
|
||||
size: Rect,
|
||||
x: f64,
|
||||
y: f64,
|
||||
ball: Rect,
|
||||
ball: Rectangle,
|
||||
playground: Rect,
|
||||
vx: u16,
|
||||
vy: u16,
|
||||
vx: f64,
|
||||
vy: f64,
|
||||
dir_x: bool,
|
||||
dir_y: bool,
|
||||
tick_count: u64,
|
||||
marker: Marker,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> App {
|
||||
App {
|
||||
size: Default::default(),
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
ball: Rect::new(10, 30, 10, 10),
|
||||
ball: Rectangle {
|
||||
x: 10.0,
|
||||
y: 30.0,
|
||||
width: 10.0,
|
||||
height: 10.0,
|
||||
color: Color::Yellow,
|
||||
},
|
||||
playground: Rect::new(10, 10, 100, 100),
|
||||
vx: 1,
|
||||
vy: 1,
|
||||
vx: 1.0,
|
||||
vy: 1.0,
|
||||
dir_x: true,
|
||||
dir_y: true,
|
||||
tick_count: 0,
|
||||
marker: Marker::Dot,
|
||||
}
|
||||
}
|
||||
|
||||
fn advance(&mut self) {
|
||||
if self.ball.left() < self.playground.left() || self.ball.right() > self.playground.right()
|
||||
fn on_tick(&mut self) {
|
||||
self.tick_count += 1;
|
||||
// only change marker every 4 ticks (1s) to avoid stroboscopic effect
|
||||
if (self.tick_count % 4) == 0 {
|
||||
self.marker = match self.marker {
|
||||
Marker::Dot => Marker::Block,
|
||||
Marker::Block => Marker::Bar,
|
||||
Marker::Bar => Marker::Braille,
|
||||
Marker::Braille => Marker::Dot,
|
||||
};
|
||||
}
|
||||
if self.ball.x < self.playground.left() as f64
|
||||
|| self.ball.x + self.ball.width > self.playground.right() as f64
|
||||
{
|
||||
self.dir_x = !self.dir_x;
|
||||
}
|
||||
if self.ball.top() < self.playground.top() || self.ball.bottom() > self.playground.bottom()
|
||||
if self.ball.y < self.playground.top() as f64
|
||||
|| self.ball.y + self.ball.height > self.playground.bottom() as f64
|
||||
{
|
||||
self.dir_y = !self.dir_y;
|
||||
}
|
||||
@@ -67,139 +92,102 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
enum Event {
|
||||
Input(event::Key),
|
||||
Tick,
|
||||
}
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
fn main() {
|
||||
// Terminal initialization
|
||||
let backend = MouseBackend::new().unwrap();
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// Channels
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let input_tx = tx.clone();
|
||||
let clock_tx = tx.clone();
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
// Input
|
||||
thread::spawn(move || {
|
||||
let stdin = io::stdin();
|
||||
for c in stdin.keys() {
|
||||
let evt = c.unwrap();
|
||||
input_tx.send(Event::Input(evt)).unwrap();
|
||||
if evt == event::Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Tick
|
||||
thread::spawn(move || loop {
|
||||
clock_tx.send(Event::Tick).unwrap();
|
||||
thread::sleep(time::Duration::from_millis(500));
|
||||
});
|
||||
|
||||
// App
|
||||
let mut app = App::new();
|
||||
|
||||
// First draw call
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
app.size = terminal.size().unwrap();
|
||||
draw(&mut terminal, &app);
|
||||
|
||||
loop {
|
||||
let size = terminal.size().unwrap();
|
||||
if size != app.size {
|
||||
terminal.resize(size).unwrap();
|
||||
app.size = size;
|
||||
}
|
||||
|
||||
let evt = rx.recv().unwrap();
|
||||
match evt {
|
||||
Event::Input(input) => match input {
|
||||
event::Key::Char('q') => {
|
||||
break;
|
||||
}
|
||||
event::Key::Down => {
|
||||
app.y += 1.0;
|
||||
}
|
||||
event::Key::Up => {
|
||||
app.y -= 1.0;
|
||||
}
|
||||
event::Key::Right => {
|
||||
app.x += 1.0;
|
||||
}
|
||||
event::Key::Left => {
|
||||
app.x -= 1.0;
|
||||
}
|
||||
|
||||
_ => {}
|
||||
},
|
||||
Event::Tick => {
|
||||
app.advance();
|
||||
}
|
||||
}
|
||||
draw(&mut terminal, &app);
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
terminal.show_cursor().unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<MouseBackend>, app: &App) {
|
||||
Group::default()
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => {
|
||||
return Ok(());
|
||||
}
|
||||
KeyCode::Down => {
|
||||
app.y += 1.0;
|
||||
}
|
||||
KeyCode::Up => {
|
||||
app.y -= 1.0;
|
||||
}
|
||||
KeyCode::Right => {
|
||||
app.x += 1.0;
|
||||
}
|
||||
KeyCode::Left => {
|
||||
app.x -= 1.0;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.sizes(&[Size::Percent(50), Size::Percent(50)])
|
||||
.render(t, &app.size, |t, chunks| {
|
||||
Canvas::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("World"))
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&Map {
|
||||
color: Color::White,
|
||||
resolution: MapResolution::High,
|
||||
});
|
||||
ctx.print(app.x, -app.y, "You are here", Color::Yellow);
|
||||
})
|
||||
.x_bounds([-180.0, 180.0])
|
||||
.y_bounds([-90.0, 90.0])
|
||||
.render(t, &chunks[0]);
|
||||
Canvas::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&Line {
|
||||
x1: f64::from(app.ball.left()),
|
||||
y1: f64::from(app.ball.top()),
|
||||
x2: f64::from(app.ball.right()),
|
||||
y2: f64::from(app.ball.top()),
|
||||
color: Color::Yellow,
|
||||
});
|
||||
ctx.draw(&Line {
|
||||
x1: f64::from(app.ball.right()),
|
||||
y1: f64::from(app.ball.top()),
|
||||
x2: f64::from(app.ball.right()),
|
||||
y2: f64::from(app.ball.bottom()),
|
||||
color: Color::Yellow,
|
||||
});
|
||||
ctx.draw(&Line {
|
||||
x1: f64::from(app.ball.right()),
|
||||
y1: f64::from(app.ball.bottom()),
|
||||
x2: f64::from(app.ball.left()),
|
||||
y2: f64::from(app.ball.bottom()),
|
||||
color: Color::Yellow,
|
||||
});
|
||||
ctx.draw(&Line {
|
||||
x1: f64::from(app.ball.left()),
|
||||
y1: f64::from(app.ball.bottom()),
|
||||
x2: f64::from(app.ball.left()),
|
||||
y2: f64::from(app.ball.top()),
|
||||
color: Color::Yellow,
|
||||
});
|
||||
})
|
||||
.x_bounds([10.0, 110.0])
|
||||
.y_bounds([10.0, 110.0])
|
||||
.render(t, &chunks[1]);
|
||||
});
|
||||
|
||||
t.draw().unwrap();
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(f.size());
|
||||
let canvas = Canvas::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("World"))
|
||||
.marker(app.marker)
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&Map {
|
||||
color: Color::White,
|
||||
resolution: MapResolution::High,
|
||||
});
|
||||
ctx.print(app.x, -app.y, "You are here".yellow());
|
||||
})
|
||||
.x_bounds([-180.0, 180.0])
|
||||
.y_bounds([-90.0, 90.0]);
|
||||
f.render_widget(canvas, chunks[0]);
|
||||
let canvas = Canvas::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("Pong"))
|
||||
.marker(app.marker)
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&app.ball);
|
||||
})
|
||||
.x_bounds([10.0, 110.0])
|
||||
.y_bounds([10.0, 110.0]);
|
||||
f.render_widget(canvas, chunks[1]);
|
||||
}
|
||||
|
||||
@@ -1,25 +1,64 @@
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
mod util;
|
||||
use util::*;
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
symbols,
|
||||
text::Span,
|
||||
widgets::{Axis, Block, Borders, Chart, Dataset, GraphType},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
use std::io;
|
||||
use std::thread;
|
||||
use std::time;
|
||||
use std::sync::mpsc;
|
||||
const DATA: [(f64, f64); 5] = [(0.0, 0.0), (1.0, 1.0), (2.0, 2.0), (3.0, 3.0), (4.0, 4.0)];
|
||||
const DATA2: [(f64, f64); 7] = [
|
||||
(0.0, 0.0),
|
||||
(10.0, 1.0),
|
||||
(20.0, 0.5),
|
||||
(30.0, 1.5),
|
||||
(40.0, 1.0),
|
||||
(50.0, 2.5),
|
||||
(60.0, 3.0),
|
||||
];
|
||||
|
||||
use termion::event;
|
||||
use termion::input::TermRead;
|
||||
#[derive(Clone)]
|
||||
pub struct SinSignal {
|
||||
x: f64,
|
||||
interval: f64,
|
||||
period: f64,
|
||||
scale: f64,
|
||||
}
|
||||
|
||||
use tui::Terminal;
|
||||
use tui::backend::MouseBackend;
|
||||
use tui::widgets::{Axis, Block, Borders, Chart, Dataset, Marker, Widget};
|
||||
use tui::layout::Rect;
|
||||
use tui::style::{Color, Modifier, Style};
|
||||
impl SinSignal {
|
||||
pub fn new(interval: f64, period: f64, scale: f64) -> SinSignal {
|
||||
SinSignal {
|
||||
x: 0.0,
|
||||
interval,
|
||||
period,
|
||||
scale,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for SinSignal {
|
||||
type Item = (f64, f64);
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let point = (self.x, (self.x * 1.0 / self.period).sin() * self.scale);
|
||||
self.x += self.interval;
|
||||
Some(point)
|
||||
}
|
||||
}
|
||||
|
||||
struct App {
|
||||
size: Rect,
|
||||
signal1: SinSignal,
|
||||
data1: Vec<(f64, f64)>,
|
||||
signal2: SinSignal,
|
||||
@@ -34,16 +73,15 @@ impl App {
|
||||
let data1 = signal1.by_ref().take(200).collect::<Vec<(f64, f64)>>();
|
||||
let data2 = signal2.by_ref().take(200).collect::<Vec<(f64, f64)>>();
|
||||
App {
|
||||
size: Rect::default(),
|
||||
signal1: signal1,
|
||||
data1: data1,
|
||||
signal2: signal2,
|
||||
data2: data2,
|
||||
signal1,
|
||||
data1,
|
||||
signal2,
|
||||
data2,
|
||||
window: [0.0, 20.0],
|
||||
}
|
||||
}
|
||||
|
||||
fn advance(&mut self) {
|
||||
fn on_tick(&mut self) {
|
||||
for _ in 0..5 {
|
||||
self.data1.remove(0);
|
||||
}
|
||||
@@ -57,111 +95,173 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
enum Event {
|
||||
Input(event::Key),
|
||||
Tick,
|
||||
}
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
fn main() {
|
||||
// Terminal initialization
|
||||
let backend = MouseBackend::new().unwrap();
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// Channels
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let input_tx = tx.clone();
|
||||
let clock_tx = tx.clone();
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
// Input
|
||||
thread::spawn(move || {
|
||||
let stdin = io::stdin();
|
||||
for c in stdin.keys() {
|
||||
let evt = c.unwrap();
|
||||
input_tx.send(Event::Input(evt)).unwrap();
|
||||
if evt == event::Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Tick
|
||||
thread::spawn(move || loop {
|
||||
clock_tx.send(Event::Tick).unwrap();
|
||||
thread::sleep(time::Duration::from_millis(500));
|
||||
});
|
||||
|
||||
// App
|
||||
let mut app = App::new();
|
||||
|
||||
// First draw call
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
app.size = terminal.size().unwrap();
|
||||
draw(&mut terminal, &app);
|
||||
|
||||
loop {
|
||||
let size = terminal.size().unwrap();
|
||||
if app.size != size {
|
||||
terminal.resize(size).unwrap();
|
||||
app.size = size;
|
||||
}
|
||||
|
||||
let evt = rx.recv().unwrap();
|
||||
match evt {
|
||||
Event::Input(input) => if input == event::Key::Char('q') {
|
||||
break;
|
||||
},
|
||||
Event::Tick => {
|
||||
app.advance();
|
||||
}
|
||||
}
|
||||
draw(&mut terminal, &app);
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
terminal.show_cursor().unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<MouseBackend>, app: &App) {
|
||||
Chart::default()
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let size = f.size();
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(size);
|
||||
let x_labels = vec![
|
||||
Span::styled(
|
||||
format!("{}", app.window[0]),
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(format!("{}", (app.window[0] + app.window[1]) / 2.0)),
|
||||
Span::styled(
|
||||
format!("{}", app.window[1]),
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
),
|
||||
];
|
||||
let datasets = vec![
|
||||
Dataset::default()
|
||||
.name("data2")
|
||||
.marker(symbols::Marker::Dot)
|
||||
.style(Style::default().fg(Color::Cyan))
|
||||
.data(&app.data1),
|
||||
Dataset::default()
|
||||
.name("data3")
|
||||
.marker(symbols::Marker::Braille)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.data(&app.data2),
|
||||
];
|
||||
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Chart")
|
||||
.title_style(Style::default().fg(Color::Cyan).modifier(Modifier::Bold))
|
||||
.title("Chart 1".cyan().bold())
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("X Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels_style(Style::default().modifier(Modifier::Italic))
|
||||
.bounds(app.window)
|
||||
.labels(&[
|
||||
&format!("{}", app.window[0]),
|
||||
&format!("{}", (app.window[0] + app.window[1]) / 2.0),
|
||||
&format!("{}", app.window[1]),
|
||||
]),
|
||||
.labels(x_labels)
|
||||
.bounds(app.window),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels_style(Style::default().modifier(Modifier::Italic))
|
||||
.bounds([-20.0, 20.0])
|
||||
.labels(&["-20", "0", "20"]),
|
||||
)
|
||||
.datasets(&[
|
||||
Dataset::default()
|
||||
.name("data2")
|
||||
.marker(Marker::Dot)
|
||||
.style(Style::default().fg(Color::Cyan))
|
||||
.data(&app.data1),
|
||||
Dataset::default()
|
||||
.name("data3")
|
||||
.marker(Marker::Braille)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.data(&app.data2),
|
||||
])
|
||||
.render(t, &app.size);
|
||||
.labels(vec!["-20".bold(), "0".into(), "20".bold()])
|
||||
.bounds([-20.0, 20.0]),
|
||||
);
|
||||
f.render_widget(chart, chunks[0]);
|
||||
|
||||
t.draw().unwrap();
|
||||
let datasets = vec![Dataset::default()
|
||||
.name("data")
|
||||
.marker(symbols::Marker::Braille)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.graph_type(GraphType::Line)
|
||||
.data(&DATA)];
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Chart 2".cyan().bold())
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("X Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.bounds([0.0, 5.0])
|
||||
.labels(vec!["0".bold(), "2.5".into(), "5.0".bold()]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.bounds([0.0, 5.0])
|
||||
.labels(vec!["0".bold(), "2.5".into(), "5.0".bold()]),
|
||||
);
|
||||
f.render_widget(chart, chunks[1]);
|
||||
|
||||
let datasets = vec![Dataset::default()
|
||||
.name("data")
|
||||
.marker(symbols::Marker::Braille)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.graph_type(GraphType::Line)
|
||||
.data(&DATA2)];
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Chart 3".cyan().bold())
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("X Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.bounds([0.0, 50.0])
|
||||
.labels(vec!["0".bold(), "25".into(), "50".bold()]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.bounds([0.0, 5.0])
|
||||
.labels(vec!["0".bold(), "2.5".into(), "5".bold()]),
|
||||
);
|
||||
f.render_widget(chart, chunks[2]);
|
||||
}
|
||||
|
||||
@@ -1,39 +1,78 @@
|
||||
extern crate tui;
|
||||
use std::{error::Error, io};
|
||||
|
||||
use tui::Terminal;
|
||||
use tui::backend::MouseBackend;
|
||||
use tui::widgets::Widget;
|
||||
use tui::buffer::Buffer;
|
||||
use tui::layout::Rect;
|
||||
use tui::style::Style;
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::Style,
|
||||
widgets::Widget,
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
struct Label<'a> {
|
||||
text: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> Default for Label<'a> {
|
||||
fn default() -> Label<'a> {
|
||||
Label { text: "" }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Label<'a> {
|
||||
fn draw(&mut self, area: &Rect, buf: &mut Buffer) {
|
||||
buf.set_string(area.left(), area.top(), self.text, &Style::default());
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_string(area.left(), area.top(), self.text, Style::default());
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Label<'a> {
|
||||
fn text(&mut self, text: &'a str) -> &mut Label<'a> {
|
||||
fn text(mut self, text: &'a str) -> Label<'a> {
|
||||
self.text = text;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut terminal = Terminal::new(MouseBackend::new().unwrap()).unwrap();
|
||||
let size = terminal.size().unwrap();
|
||||
terminal.clear().unwrap();
|
||||
Label::default().text("Test").render(&mut terminal, &size);
|
||||
terminal.draw().unwrap();
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let res = run_app(&mut terminal);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(ui)?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>) {
|
||||
let size = f.size();
|
||||
let label = Label::default().text("Test");
|
||||
f.render_widget(label, size);
|
||||
}
|
||||
|
||||
517
examples/demo.rs
517
examples/demo.rs
@@ -1,517 +0,0 @@
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate stderrlog;
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
|
||||
mod util;
|
||||
|
||||
use std::io;
|
||||
use std::thread;
|
||||
use std::time;
|
||||
use std::sync::mpsc;
|
||||
|
||||
use termion::event;
|
||||
use termion::input::TermRead;
|
||||
|
||||
use tui::Terminal;
|
||||
use tui::backend::MouseBackend;
|
||||
use tui::widgets::{Axis, BarChart, Block, Borders, Chart, Dataset, Gauge, Item, List, Marker,
|
||||
Paragraph, Row, SelectableList, Sparkline, Table, Tabs, Widget};
|
||||
use tui::widgets::canvas::{Canvas, Line, Map, MapResolution};
|
||||
use tui::layout::{Direction, Group, Rect, Size};
|
||||
use tui::style::{Color, Modifier, Style};
|
||||
|
||||
use util::*;
|
||||
|
||||
struct Server<'a> {
|
||||
name: &'a str,
|
||||
location: &'a str,
|
||||
coords: (f64, f64),
|
||||
status: &'a str,
|
||||
}
|
||||
|
||||
struct App<'a> {
|
||||
size: Rect,
|
||||
items: Vec<&'a str>,
|
||||
events: Vec<(&'a str, &'a str)>,
|
||||
selected: usize,
|
||||
tabs: MyTabs<'a>,
|
||||
show_chart: bool,
|
||||
progress: u16,
|
||||
data: Vec<u64>,
|
||||
data2: Vec<(f64, f64)>,
|
||||
data3: Vec<(f64, f64)>,
|
||||
data4: Vec<(&'a str, u64)>,
|
||||
window: [f64; 2],
|
||||
colors: [Color; 2],
|
||||
color_index: usize,
|
||||
servers: Vec<Server<'a>>,
|
||||
}
|
||||
|
||||
enum Event {
|
||||
Input(event::Key),
|
||||
Tick,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
stderrlog::new()
|
||||
.module(module_path!())
|
||||
.verbosity(4)
|
||||
.init()
|
||||
.unwrap();
|
||||
info!("Start");
|
||||
|
||||
let mut rand_signal = RandomSignal::new(0, 100);
|
||||
let mut sin_signal = SinSignal::new(0.2, 3.0, 18.0);
|
||||
let mut sin_signal2 = SinSignal::new(0.1, 2.0, 10.0);
|
||||
|
||||
let mut app = App {
|
||||
size: Rect::default(),
|
||||
items: vec![
|
||||
"Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", "Item9",
|
||||
"Item10", "Item11", "Item12", "Item13", "Item14", "Item15", "Item16", "Item17",
|
||||
"Item18", "Item19", "Item20", "Item21", "Item22", "Item23", "Item24",
|
||||
],
|
||||
events: vec![
|
||||
("Event1", "INFO"),
|
||||
("Event2", "INFO"),
|
||||
("Event3", "CRITICAL"),
|
||||
("Event4", "ERROR"),
|
||||
("Event5", "INFO"),
|
||||
("Event6", "INFO"),
|
||||
("Event7", "WARNING"),
|
||||
("Event8", "INFO"),
|
||||
("Event9", "INFO"),
|
||||
("Event10", "INFO"),
|
||||
("Event11", "CRITICAL"),
|
||||
("Event12", "INFO"),
|
||||
("Event13", "INFO"),
|
||||
("Event14", "INFO"),
|
||||
("Event15", "INFO"),
|
||||
("Event16", "INFO"),
|
||||
("Event17", "ERROR"),
|
||||
("Event18", "ERROR"),
|
||||
("Event19", "INFO"),
|
||||
("Event20", "INFO"),
|
||||
("Event21", "WARNING"),
|
||||
("Event22", "INFO"),
|
||||
("Event23", "INFO"),
|
||||
("Event24", "WARNING"),
|
||||
("Event25", "INFO"),
|
||||
("Event26", "INFO"),
|
||||
],
|
||||
selected: 0,
|
||||
tabs: MyTabs {
|
||||
titles: vec!["Tab0", "Tab1"],
|
||||
selection: 0,
|
||||
},
|
||||
show_chart: true,
|
||||
progress: 0,
|
||||
data: rand_signal.clone().take(300).collect(),
|
||||
data2: sin_signal.clone().take(100).collect(),
|
||||
data3: sin_signal2.clone().take(200).collect(),
|
||||
data4: vec![
|
||||
("B1", 9),
|
||||
("B2", 12),
|
||||
("B3", 5),
|
||||
("B4", 8),
|
||||
("B5", 2),
|
||||
("B6", 4),
|
||||
("B7", 5),
|
||||
("B8", 9),
|
||||
("B9", 14),
|
||||
("B10", 15),
|
||||
("B11", 1),
|
||||
("B12", 0),
|
||||
("B13", 4),
|
||||
("B14", 6),
|
||||
("B15", 4),
|
||||
("B16", 6),
|
||||
("B17", 4),
|
||||
("B18", 7),
|
||||
("B19", 13),
|
||||
("B20", 8),
|
||||
("B21", 11),
|
||||
("B22", 9),
|
||||
("B23", 3),
|
||||
("B24", 5),
|
||||
],
|
||||
window: [0.0, 20.0],
|
||||
colors: [Color::Magenta, Color::Red],
|
||||
color_index: 0,
|
||||
servers: vec![
|
||||
Server {
|
||||
name: "NorthAmerica-1",
|
||||
location: "New York City",
|
||||
coords: (40.71, -74.00),
|
||||
status: "Up",
|
||||
},
|
||||
Server {
|
||||
name: "Europe-1",
|
||||
location: "Paris",
|
||||
coords: (48.85, 2.35),
|
||||
status: "Failure",
|
||||
},
|
||||
Server {
|
||||
name: "SouthAmerica-1",
|
||||
location: "São Paulo",
|
||||
coords: (-23.54, -46.62),
|
||||
status: "Up",
|
||||
},
|
||||
Server {
|
||||
name: "Asia-1",
|
||||
location: "Singapore",
|
||||
coords: (1.35, 103.86),
|
||||
status: "Up",
|
||||
},
|
||||
],
|
||||
};
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let input_tx = tx.clone();
|
||||
|
||||
for _ in 0..100 {
|
||||
sin_signal.next();
|
||||
}
|
||||
for _ in 0..200 {
|
||||
sin_signal2.next();
|
||||
}
|
||||
|
||||
thread::spawn(move || {
|
||||
let stdin = io::stdin();
|
||||
for c in stdin.keys() {
|
||||
let evt = c.unwrap();
|
||||
input_tx.send(Event::Input(evt)).unwrap();
|
||||
if evt == event::Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
thread::spawn(move || {
|
||||
let tx = tx.clone();
|
||||
loop {
|
||||
tx.send(Event::Tick).unwrap();
|
||||
thread::sleep(time::Duration::from_millis(200));
|
||||
}
|
||||
});
|
||||
|
||||
let backend = MouseBackend::new().unwrap();
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
|
||||
loop {
|
||||
let size = terminal.size().unwrap();
|
||||
if size != app.size {
|
||||
terminal.resize(size).unwrap();
|
||||
app.size = size;
|
||||
}
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
let evt = rx.recv().unwrap();
|
||||
match evt {
|
||||
Event::Input(input) => match input {
|
||||
event::Key::Char('q') => {
|
||||
break;
|
||||
}
|
||||
event::Key::Up => {
|
||||
if app.selected > 0 {
|
||||
app.selected -= 1
|
||||
};
|
||||
}
|
||||
event::Key::Down => if app.selected < app.items.len() - 1 {
|
||||
app.selected += 1;
|
||||
},
|
||||
event::Key::Left => {
|
||||
app.tabs.previous();
|
||||
}
|
||||
event::Key::Right => {
|
||||
app.tabs.next();
|
||||
}
|
||||
event::Key::Char('t') => {
|
||||
app.show_chart = !app.show_chart;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Event::Tick => {
|
||||
app.progress += 5;
|
||||
if app.progress > 100 {
|
||||
app.progress = 0;
|
||||
}
|
||||
app.data.insert(0, rand_signal.next().unwrap());
|
||||
app.data.pop();
|
||||
for _ in 0..5 {
|
||||
app.data2.remove(0);
|
||||
app.data2.push(sin_signal.next().unwrap());
|
||||
}
|
||||
for _ in 0..10 {
|
||||
app.data3.remove(0);
|
||||
app.data3.push(sin_signal2.next().unwrap());
|
||||
}
|
||||
let i = app.data4.pop().unwrap();
|
||||
app.data4.insert(0, i);
|
||||
app.window[0] += 1.0;
|
||||
app.window[1] += 1.0;
|
||||
let i = app.events.pop().unwrap();
|
||||
app.events.insert(0, i);
|
||||
app.color_index += 1;
|
||||
if app.color_index >= app.colors.len() {
|
||||
app.color_index = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
terminal.show_cursor().unwrap();
|
||||
terminal.clear().unwrap();
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<MouseBackend>, app: &App) -> Result<(), io::Error> {
|
||||
Group::default()
|
||||
.direction(Direction::Vertical)
|
||||
.sizes(&[Size::Fixed(3), Size::Min(0)])
|
||||
.render(t, &app.size, |t, chunks| {
|
||||
Tabs::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("Tabs"))
|
||||
.titles(&app.tabs.titles)
|
||||
.style(Style::default().fg(Color::Green))
|
||||
.highlight_style(Style::default().fg(Color::Yellow))
|
||||
.select(app.tabs.selection)
|
||||
.render(t, &chunks[0]);
|
||||
match app.tabs.selection {
|
||||
0 => {
|
||||
draw_first_tab(t, app, &chunks[1]);
|
||||
}
|
||||
1 => {
|
||||
draw_second_tab(t, app, &chunks[1]);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
});
|
||||
try!(t.draw());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw_first_tab(t: &mut Terminal<MouseBackend>, app: &App, area: &Rect) {
|
||||
Group::default()
|
||||
.direction(Direction::Vertical)
|
||||
.sizes(&[Size::Fixed(7), Size::Min(7), Size::Fixed(7)])
|
||||
.render(t, area, |t, chunks| {
|
||||
draw_gauges(t, app, &chunks[0]);
|
||||
draw_charts(t, app, &chunks[1]);
|
||||
draw_text(t, &chunks[2]);
|
||||
});
|
||||
}
|
||||
|
||||
fn draw_gauges(t: &mut Terminal<MouseBackend>, app: &App, area: &Rect) {
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title("Graphs")
|
||||
.render(t, area);
|
||||
Group::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(1)
|
||||
.sizes(&[Size::Fixed(2), Size::Fixed(3)])
|
||||
.render(t, area, |t, chunks| {
|
||||
Gauge::default()
|
||||
.block(Block::default().title("Gauge:"))
|
||||
.style(
|
||||
Style::default()
|
||||
.fg(Color::Magenta)
|
||||
.bg(Color::Black)
|
||||
.modifier(Modifier::Italic),
|
||||
)
|
||||
.label(&format!("{} / 100", app.progress))
|
||||
.percent(app.progress)
|
||||
.render(t, &chunks[0]);
|
||||
Sparkline::default()
|
||||
.block(Block::default().title("Sparkline:"))
|
||||
.style(Style::default().fg(Color::Green))
|
||||
.data(&app.data)
|
||||
.render(t, &chunks[1]);
|
||||
});
|
||||
}
|
||||
|
||||
fn draw_charts(t: &mut Terminal<MouseBackend>, app: &App, area: &Rect) {
|
||||
let sizes = if app.show_chart {
|
||||
vec![Size::Percent(50), Size::Percent(50)]
|
||||
} else {
|
||||
vec![Size::Percent(100)]
|
||||
};
|
||||
Group::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.sizes(&sizes)
|
||||
.render(t, area, |t, chunks| {
|
||||
Group::default()
|
||||
.direction(Direction::Vertical)
|
||||
.sizes(&[Size::Percent(50), Size::Percent(50)])
|
||||
.render(t, &chunks[0], |t, chunks| {
|
||||
Group::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.sizes(&[Size::Percent(50), Size::Percent(50)])
|
||||
.render(t, &chunks[0], |t, chunks| {
|
||||
SelectableList::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.items(&app.items)
|
||||
.select(app.selected)
|
||||
.highlight_style(
|
||||
Style::default().fg(Color::Yellow).modifier(Modifier::Bold),
|
||||
)
|
||||
.highlight_symbol(">")
|
||||
.render(t, &chunks[0]);
|
||||
let info_style = Style::default().fg(Color::White);
|
||||
let warning_style = Style::default().fg(Color::Yellow);
|
||||
let error_style = Style::default().fg(Color::Magenta);
|
||||
let critical_style = Style::default().fg(Color::Red);
|
||||
let events = app.events.iter().map(|&(evt, level)| {
|
||||
Item::StyledData(
|
||||
format!("{}: {}", level, evt),
|
||||
match level {
|
||||
"ERROR" => &error_style,
|
||||
"CRITICAL" => &critical_style,
|
||||
"WARNING" => &warning_style,
|
||||
_ => &info_style,
|
||||
},
|
||||
)
|
||||
});
|
||||
List::new(events)
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.render(t, &chunks[1]);
|
||||
});
|
||||
BarChart::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("Bar chart"))
|
||||
.data(&app.data4)
|
||||
.bar_width(3)
|
||||
.bar_gap(2)
|
||||
.value_style(
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::Green)
|
||||
.modifier(Modifier::Italic),
|
||||
)
|
||||
.label_style(Style::default().fg(Color::Yellow))
|
||||
.style(Style::default().fg(Color::Green))
|
||||
.render(t, &chunks[1]);
|
||||
});
|
||||
if app.show_chart {
|
||||
Chart::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Chart")
|
||||
.title_style(Style::default().fg(Color::Cyan).modifier(Modifier::Bold))
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("X Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels_style(Style::default().modifier(Modifier::Italic))
|
||||
.bounds(app.window)
|
||||
.labels(&[
|
||||
&format!("{}", app.window[0]),
|
||||
&format!("{}", (app.window[0] + app.window[1]) / 2.0),
|
||||
&format!("{}", app.window[1]),
|
||||
]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels_style(Style::default().modifier(Modifier::Italic))
|
||||
.bounds([-20.0, 20.0])
|
||||
.labels(&["-20", "0", "20"]),
|
||||
)
|
||||
.datasets(&[
|
||||
Dataset::default()
|
||||
.name("data2")
|
||||
.marker(Marker::Dot)
|
||||
.style(Style::default().fg(Color::Cyan))
|
||||
.data(&app.data2),
|
||||
Dataset::default()
|
||||
.name("data3")
|
||||
.marker(Marker::Braille)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.data(&app.data3),
|
||||
])
|
||||
.render(t, &chunks[1]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn draw_text(t: &mut Terminal<MouseBackend>, area: &Rect) {
|
||||
Paragraph::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title("Footer")
|
||||
.title_style(Style::default().fg(Color::Magenta).modifier(Modifier::Bold)),
|
||||
)
|
||||
.wrap(true)
|
||||
.text(
|
||||
"This is a paragraph with several lines.\nYou can change the color.\nUse \
|
||||
\\{fg=[color];bg=[color];mod=[modifier] [text]} to highlight the text with a color. \
|
||||
For example, {fg=red u}{fg=green n}{fg=yellow d}{fg=magenta e}{fg=cyan r} \
|
||||
{fg=gray t}{fg=light_gray h}{fg=light_red e} {fg=light_green r}{fg=light_yellow a} \
|
||||
{fg=light_magenta i}{fg=light_cyan n}{fg=white b}{fg=red o}{fg=green w}.\n\
|
||||
Oh, and if you didn't {mod=italic notice} you can {mod=bold automatically} \
|
||||
{mod=invert wrap} your {mod=underline text} =).\nOne more thing is that \
|
||||
it should display unicode characters properly: 日本国, ٩(-̮̮̃-̃)۶ ٩(●̮̮̃•̃)۶ ٩(͡๏̯͡๏)۶ \
|
||||
٩(-̮̮̃•̃).",
|
||||
)
|
||||
.render(t, area);
|
||||
}
|
||||
|
||||
fn draw_second_tab(t: &mut Terminal<MouseBackend>, app: &App, area: &Rect) {
|
||||
Group::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.sizes(&[Size::Percent(30), Size::Percent(70)])
|
||||
.render(t, area, |t, chunks| {
|
||||
let up_style = Style::default().fg(Color::Green);
|
||||
let failure_style = Style::default().fg(Color::Red);
|
||||
Table::new(
|
||||
["Server", "Location", "Status"].into_iter(),
|
||||
app.servers.iter().map(|s| {
|
||||
let style = if s.status == "Up" {
|
||||
&up_style
|
||||
} else {
|
||||
&failure_style
|
||||
};
|
||||
Row::StyledData(vec![s.name, s.location, s.status].into_iter(), style)
|
||||
}),
|
||||
).block(Block::default().title("Servers").borders(Borders::ALL))
|
||||
.header_style(Style::default().fg(Color::Yellow))
|
||||
.widths(&[15, 15, 10])
|
||||
.render(t, &chunks[0]);
|
||||
|
||||
Canvas::default()
|
||||
.block(Block::default().title("World").borders(Borders::ALL))
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&Map {
|
||||
color: Color::White,
|
||||
resolution: MapResolution::High,
|
||||
});
|
||||
ctx.layer();
|
||||
for (i, s1) in app.servers.iter().enumerate() {
|
||||
for s2 in &app.servers[i + 1..] {
|
||||
ctx.draw(&Line {
|
||||
x1: s1.coords.1,
|
||||
y1: s1.coords.0,
|
||||
y2: s2.coords.0,
|
||||
x2: s2.coords.1,
|
||||
color: Color::Yellow,
|
||||
});
|
||||
}
|
||||
}
|
||||
for server in &app.servers {
|
||||
let color = if server.status == "Up" {
|
||||
Color::Green
|
||||
} else {
|
||||
Color::Red
|
||||
};
|
||||
ctx.print(server.coords.1, server.coords.0, "X", color);
|
||||
}
|
||||
})
|
||||
.x_bounds([-180.0, 180.0])
|
||||
.y_bounds([-90.0, 90.0])
|
||||
.render(t, &chunks[1]);
|
||||
})
|
||||
}
|
||||
348
examples/demo/app.rs
Normal file
348
examples/demo/app.rs
Normal file
@@ -0,0 +1,348 @@
|
||||
use rand::{
|
||||
distributions::{Distribution, Uniform},
|
||||
rngs::ThreadRng,
|
||||
};
|
||||
use ratatui::widgets::ListState;
|
||||
|
||||
const TASKS: [&str; 24] = [
|
||||
"Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", "Item9", "Item10",
|
||||
"Item11", "Item12", "Item13", "Item14", "Item15", "Item16", "Item17", "Item18", "Item19",
|
||||
"Item20", "Item21", "Item22", "Item23", "Item24",
|
||||
];
|
||||
|
||||
const LOGS: [(&str, &str); 26] = [
|
||||
("Event1", "INFO"),
|
||||
("Event2", "INFO"),
|
||||
("Event3", "CRITICAL"),
|
||||
("Event4", "ERROR"),
|
||||
("Event5", "INFO"),
|
||||
("Event6", "INFO"),
|
||||
("Event7", "WARNING"),
|
||||
("Event8", "INFO"),
|
||||
("Event9", "INFO"),
|
||||
("Event10", "INFO"),
|
||||
("Event11", "CRITICAL"),
|
||||
("Event12", "INFO"),
|
||||
("Event13", "INFO"),
|
||||
("Event14", "INFO"),
|
||||
("Event15", "INFO"),
|
||||
("Event16", "INFO"),
|
||||
("Event17", "ERROR"),
|
||||
("Event18", "ERROR"),
|
||||
("Event19", "INFO"),
|
||||
("Event20", "INFO"),
|
||||
("Event21", "WARNING"),
|
||||
("Event22", "INFO"),
|
||||
("Event23", "INFO"),
|
||||
("Event24", "WARNING"),
|
||||
("Event25", "INFO"),
|
||||
("Event26", "INFO"),
|
||||
];
|
||||
|
||||
const EVENTS: [(&str, u64); 24] = [
|
||||
("B1", 9),
|
||||
("B2", 12),
|
||||
("B3", 5),
|
||||
("B4", 8),
|
||||
("B5", 2),
|
||||
("B6", 4),
|
||||
("B7", 5),
|
||||
("B8", 9),
|
||||
("B9", 14),
|
||||
("B10", 15),
|
||||
("B11", 1),
|
||||
("B12", 0),
|
||||
("B13", 4),
|
||||
("B14", 6),
|
||||
("B15", 4),
|
||||
("B16", 6),
|
||||
("B17", 4),
|
||||
("B18", 7),
|
||||
("B19", 13),
|
||||
("B20", 8),
|
||||
("B21", 11),
|
||||
("B22", 9),
|
||||
("B23", 3),
|
||||
("B24", 5),
|
||||
];
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RandomSignal {
|
||||
distribution: Uniform<u64>,
|
||||
rng: ThreadRng,
|
||||
}
|
||||
|
||||
impl RandomSignal {
|
||||
pub fn new(lower: u64, upper: u64) -> RandomSignal {
|
||||
RandomSignal {
|
||||
distribution: Uniform::new(lower, upper),
|
||||
rng: rand::thread_rng(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for RandomSignal {
|
||||
type Item = u64;
|
||||
fn next(&mut self) -> Option<u64> {
|
||||
Some(self.distribution.sample(&mut self.rng))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SinSignal {
|
||||
x: f64,
|
||||
interval: f64,
|
||||
period: f64,
|
||||
scale: f64,
|
||||
}
|
||||
|
||||
impl SinSignal {
|
||||
pub fn new(interval: f64, period: f64, scale: f64) -> SinSignal {
|
||||
SinSignal {
|
||||
x: 0.0,
|
||||
interval,
|
||||
period,
|
||||
scale,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for SinSignal {
|
||||
type Item = (f64, f64);
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let point = (self.x, (self.x * 1.0 / self.period).sin() * self.scale);
|
||||
self.x += self.interval;
|
||||
Some(point)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TabsState<'a> {
|
||||
pub titles: Vec<&'a str>,
|
||||
pub index: usize,
|
||||
}
|
||||
|
||||
impl<'a> TabsState<'a> {
|
||||
pub fn new(titles: Vec<&'a str>) -> TabsState {
|
||||
TabsState { titles, index: 0 }
|
||||
}
|
||||
pub fn next(&mut self) {
|
||||
self.index = (self.index + 1) % self.titles.len();
|
||||
}
|
||||
|
||||
pub fn previous(&mut self) {
|
||||
if self.index > 0 {
|
||||
self.index -= 1;
|
||||
} else {
|
||||
self.index = self.titles.len() - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StatefulList<T> {
|
||||
pub state: ListState,
|
||||
pub items: Vec<T>,
|
||||
}
|
||||
|
||||
impl<T> StatefulList<T> {
|
||||
pub fn with_items(items: Vec<T>) -> StatefulList<T> {
|
||||
StatefulList {
|
||||
state: ListState::default(),
|
||||
items,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.items.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
pub fn previous(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
self.items.len() - 1
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Signal<S: Iterator> {
|
||||
source: S,
|
||||
pub points: Vec<S::Item>,
|
||||
tick_rate: usize,
|
||||
}
|
||||
|
||||
impl<S> Signal<S>
|
||||
where
|
||||
S: Iterator,
|
||||
{
|
||||
fn on_tick(&mut self) {
|
||||
for _ in 0..self.tick_rate {
|
||||
self.points.remove(0);
|
||||
}
|
||||
self.points
|
||||
.extend(self.source.by_ref().take(self.tick_rate));
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Signals {
|
||||
pub sin1: Signal<SinSignal>,
|
||||
pub sin2: Signal<SinSignal>,
|
||||
pub window: [f64; 2],
|
||||
}
|
||||
|
||||
impl Signals {
|
||||
fn on_tick(&mut self) {
|
||||
self.sin1.on_tick();
|
||||
self.sin2.on_tick();
|
||||
self.window[0] += 1.0;
|
||||
self.window[1] += 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Server<'a> {
|
||||
pub name: &'a str,
|
||||
pub location: &'a str,
|
||||
pub coords: (f64, f64),
|
||||
pub status: &'a str,
|
||||
}
|
||||
|
||||
pub struct App<'a> {
|
||||
pub title: &'a str,
|
||||
pub should_quit: bool,
|
||||
pub tabs: TabsState<'a>,
|
||||
pub show_chart: bool,
|
||||
pub progress: f64,
|
||||
pub sparkline: Signal<RandomSignal>,
|
||||
pub tasks: StatefulList<&'a str>,
|
||||
pub logs: StatefulList<(&'a str, &'a str)>,
|
||||
pub signals: Signals,
|
||||
pub barchart: Vec<(&'a str, u64)>,
|
||||
pub servers: Vec<Server<'a>>,
|
||||
pub enhanced_graphics: bool,
|
||||
}
|
||||
|
||||
impl<'a> App<'a> {
|
||||
pub fn new(title: &'a str, enhanced_graphics: bool) -> App<'a> {
|
||||
let mut rand_signal = RandomSignal::new(0, 100);
|
||||
let sparkline_points = rand_signal.by_ref().take(300).collect();
|
||||
let mut sin_signal = SinSignal::new(0.2, 3.0, 18.0);
|
||||
let sin1_points = sin_signal.by_ref().take(100).collect();
|
||||
let mut sin_signal2 = SinSignal::new(0.1, 2.0, 10.0);
|
||||
let sin2_points = sin_signal2.by_ref().take(200).collect();
|
||||
App {
|
||||
title,
|
||||
should_quit: false,
|
||||
tabs: TabsState::new(vec!["Tab0", "Tab1", "Tab2"]),
|
||||
show_chart: true,
|
||||
progress: 0.0,
|
||||
sparkline: Signal {
|
||||
source: rand_signal,
|
||||
points: sparkline_points,
|
||||
tick_rate: 1,
|
||||
},
|
||||
tasks: StatefulList::with_items(TASKS.to_vec()),
|
||||
logs: StatefulList::with_items(LOGS.to_vec()),
|
||||
signals: Signals {
|
||||
sin1: Signal {
|
||||
source: sin_signal,
|
||||
points: sin1_points,
|
||||
tick_rate: 5,
|
||||
},
|
||||
sin2: Signal {
|
||||
source: sin_signal2,
|
||||
points: sin2_points,
|
||||
tick_rate: 10,
|
||||
},
|
||||
window: [0.0, 20.0],
|
||||
},
|
||||
barchart: EVENTS.to_vec(),
|
||||
servers: vec![
|
||||
Server {
|
||||
name: "NorthAmerica-1",
|
||||
location: "New York City",
|
||||
coords: (40.71, -74.00),
|
||||
status: "Up",
|
||||
},
|
||||
Server {
|
||||
name: "Europe-1",
|
||||
location: "Paris",
|
||||
coords: (48.85, 2.35),
|
||||
status: "Failure",
|
||||
},
|
||||
Server {
|
||||
name: "SouthAmerica-1",
|
||||
location: "São Paulo",
|
||||
coords: (-23.54, -46.62),
|
||||
status: "Up",
|
||||
},
|
||||
Server {
|
||||
name: "Asia-1",
|
||||
location: "Singapore",
|
||||
coords: (1.35, 103.86),
|
||||
status: "Up",
|
||||
},
|
||||
],
|
||||
enhanced_graphics,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_up(&mut self) {
|
||||
self.tasks.previous();
|
||||
}
|
||||
|
||||
pub fn on_down(&mut self) {
|
||||
self.tasks.next();
|
||||
}
|
||||
|
||||
pub fn on_right(&mut self) {
|
||||
self.tabs.next();
|
||||
}
|
||||
|
||||
pub fn on_left(&mut self) {
|
||||
self.tabs.previous();
|
||||
}
|
||||
|
||||
pub fn on_key(&mut self, c: char) {
|
||||
match c {
|
||||
'q' => {
|
||||
self.should_quit = true;
|
||||
}
|
||||
't' => {
|
||||
self.show_chart = !self.show_chart;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_tick(&mut self) {
|
||||
// Update progress
|
||||
self.progress += 0.001;
|
||||
if self.progress > 1.0 {
|
||||
self.progress = 0.0;
|
||||
}
|
||||
|
||||
self.sparkline.on_tick();
|
||||
self.signals.on_tick();
|
||||
|
||||
let log = self.logs.items.pop().unwrap();
|
||||
self.logs.items.insert(0, log);
|
||||
|
||||
let event = self.barchart.pop().unwrap();
|
||||
self.barchart.insert(0, event);
|
||||
}
|
||||
}
|
||||
81
examples/demo/crossterm.rs
Normal file
81
examples/demo/crossterm.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
Terminal,
|
||||
};
|
||||
|
||||
use crate::{app::App, ui};
|
||||
|
||||
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let app = App::new("Crossterm Demo", enhanced_graphics);
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui::draw(f, &mut app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Char(c) => app.on_key(c),
|
||||
KeyCode::Left => app.on_left(),
|
||||
KeyCode::Up => app.on_up(),
|
||||
KeyCode::Right => app.on_right(),
|
||||
KeyCode::Down => app.on_down(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
if app.should_quit {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
36
examples/demo/main.rs
Normal file
36
examples/demo/main.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use std::{error::Error, time::Duration};
|
||||
|
||||
use argh::FromArgs;
|
||||
|
||||
mod app;
|
||||
#[cfg(feature = "crossterm")]
|
||||
mod crossterm;
|
||||
#[cfg(feature = "termion")]
|
||||
mod termion;
|
||||
#[cfg(feature = "termwiz")]
|
||||
mod termwiz;
|
||||
|
||||
mod ui;
|
||||
|
||||
/// Demo
|
||||
#[derive(Debug, FromArgs)]
|
||||
struct Cli {
|
||||
/// time in ms between two ticks.
|
||||
#[argh(option, default = "250")]
|
||||
tick_rate: u64,
|
||||
/// whether unicode symbols are used to improve the overall look of the app
|
||||
#[argh(option, default = "true")]
|
||||
enhanced_graphics: bool,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let cli: Cli = argh::from_env();
|
||||
let tick_rate = Duration::from_millis(cli.tick_rate);
|
||||
#[cfg(feature = "crossterm")]
|
||||
crate::crossterm::run(tick_rate, cli.enhanced_graphics)?;
|
||||
#[cfg(feature = "termion")]
|
||||
crate::termion::run(tick_rate, cli.enhanced_graphics)?;
|
||||
#[cfg(feature = "termwiz")]
|
||||
crate::termwiz::run(tick_rate, cli.enhanced_graphics)?;
|
||||
Ok(())
|
||||
}
|
||||
85
examples/demo/termion.rs
Normal file
85
examples/demo/termion.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use std::{error::Error, io, sync::mpsc, thread, time::Duration};
|
||||
|
||||
use ratatui::{
|
||||
backend::{Backend, TermionBackend},
|
||||
Terminal,
|
||||
};
|
||||
use termion::{
|
||||
event::Key,
|
||||
input::{MouseTerminal, TermRead},
|
||||
raw::IntoRawMode,
|
||||
screen::IntoAlternateScreen,
|
||||
};
|
||||
|
||||
use crate::{app::App, ui};
|
||||
|
||||
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
let stdout = io::stdout()
|
||||
.into_raw_mode()
|
||||
.unwrap()
|
||||
.into_alternate_screen()
|
||||
.unwrap();
|
||||
let stdout = MouseTerminal::from(stdout);
|
||||
let backend = TermionBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let app = App::new("Termion demo", enhanced_graphics);
|
||||
run_app(&mut terminal, app, tick_rate)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let events = events(tick_rate);
|
||||
loop {
|
||||
terminal.draw(|f| ui::draw(f, &mut app))?;
|
||||
|
||||
match events.recv()? {
|
||||
Event::Input(key) => match key {
|
||||
Key::Char(c) => app.on_key(c),
|
||||
Key::Up => app.on_up(),
|
||||
Key::Down => app.on_down(),
|
||||
Key::Left => app.on_left(),
|
||||
Key::Right => app.on_right(),
|
||||
_ => {}
|
||||
},
|
||||
Event::Tick => app.on_tick(),
|
||||
}
|
||||
if app.should_quit {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Event {
|
||||
Input(Key),
|
||||
Tick,
|
||||
}
|
||||
|
||||
fn events(tick_rate: Duration) -> mpsc::Receiver<Event> {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let keys_tx = tx.clone();
|
||||
thread::spawn(move || {
|
||||
let stdin = io::stdin();
|
||||
for key in stdin.keys().flatten() {
|
||||
if let Err(err) = keys_tx.send(Event::Input(key)) {
|
||||
eprintln!("{err}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
thread::spawn(move || loop {
|
||||
if let Err(err) = tx.send(Event::Tick) {
|
||||
eprintln!("{err}");
|
||||
break;
|
||||
}
|
||||
thread::sleep(tick_rate);
|
||||
});
|
||||
rx
|
||||
}
|
||||
76
examples/demo/termwiz.rs
Normal file
76
examples/demo/termwiz.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use ratatui::{backend::TermwizBackend, Terminal};
|
||||
use termwiz::{input::*, terminal::Terminal as TermwizTerminal};
|
||||
|
||||
use crate::{app::App, ui};
|
||||
|
||||
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn Error>> {
|
||||
let backend = TermwizBackend::new()?;
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
terminal.hide_cursor()?;
|
||||
|
||||
// create app and run it
|
||||
let app = App::new("Termwiz Demo", enhanced_graphics);
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
terminal.show_cursor()?;
|
||||
terminal.flush()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app(
|
||||
terminal: &mut Terminal<TermwizBackend>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui::draw(f, &mut app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if let Ok(Some(input)) = terminal
|
||||
.backend_mut()
|
||||
.buffered_terminal_mut()
|
||||
.terminal()
|
||||
.poll_input(Some(timeout))
|
||||
{
|
||||
match input {
|
||||
InputEvent::Key(key_code) => match key_code.key {
|
||||
KeyCode::UpArrow => app.on_up(),
|
||||
KeyCode::DownArrow => app.on_down(),
|
||||
KeyCode::LeftArrow => app.on_left(),
|
||||
KeyCode::RightArrow => app.on_right(),
|
||||
KeyCode::Char(c) => app.on_key(c),
|
||||
_ => {}
|
||||
},
|
||||
InputEvent::Resized { cols, rows } => {
|
||||
terminal
|
||||
.backend_mut()
|
||||
.buffered_terminal_mut()
|
||||
.resize(cols, rows);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
if app.should_quit {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
437
examples/demo/ui.rs
Normal file
437
examples/demo/ui.rs
Normal file
@@ -0,0 +1,437 @@
|
||||
use ratatui::{
|
||||
backend::Backend,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
symbols,
|
||||
text::{Line, Span},
|
||||
widgets::{
|
||||
canvas::{Canvas, Circle, Line as CanvasLine, Map, MapResolution, Rectangle},
|
||||
Axis, BarChart, Block, Borders, Cell, Chart, Dataset, Gauge, LineGauge, List, ListItem,
|
||||
Paragraph, Row, Sparkline, Table, Tabs, Wrap,
|
||||
},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use crate::app::App;
|
||||
|
||||
pub fn draw<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||
let chunks = Layout::default()
|
||||
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
|
||||
.split(f.size());
|
||||
let titles = app
|
||||
.tabs
|
||||
.titles
|
||||
.iter()
|
||||
.map(|t| Line::from(Span::styled(*t, Style::default().fg(Color::Green))))
|
||||
.collect();
|
||||
let tabs = Tabs::new(titles)
|
||||
.block(Block::default().borders(Borders::ALL).title(app.title))
|
||||
.highlight_style(Style::default().fg(Color::Yellow))
|
||||
.select(app.tabs.index);
|
||||
f.render_widget(tabs, chunks[0]);
|
||||
match app.tabs.index {
|
||||
0 => draw_first_tab(f, app, chunks[1]),
|
||||
1 => draw_second_tab(f, app, chunks[1]),
|
||||
2 => draw_third_tab(f, app, chunks[1]),
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
fn draw_first_tab<B>(f: &mut Frame<B>, app: &mut App, area: Rect)
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(9),
|
||||
Constraint::Min(8),
|
||||
Constraint::Length(7),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(area);
|
||||
draw_gauges(f, app, chunks[0]);
|
||||
draw_charts(f, app, chunks[1]);
|
||||
draw_text(f, chunks[2]);
|
||||
}
|
||||
|
||||
fn draw_gauges<B>(f: &mut Frame<B>, app: &mut App, area: Rect)
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(2),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(1),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.margin(1)
|
||||
.split(area);
|
||||
let block = Block::default().borders(Borders::ALL).title("Graphs");
|
||||
f.render_widget(block, area);
|
||||
|
||||
let label = format!("{:.2}%", app.progress * 100.0);
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("Gauge:"))
|
||||
.gauge_style(
|
||||
Style::default()
|
||||
.fg(Color::Magenta)
|
||||
.bg(Color::Black)
|
||||
.add_modifier(Modifier::ITALIC | Modifier::BOLD),
|
||||
)
|
||||
.use_unicode(app.enhanced_graphics)
|
||||
.label(label)
|
||||
.ratio(app.progress);
|
||||
f.render_widget(gauge, chunks[0]);
|
||||
|
||||
let sparkline = Sparkline::default()
|
||||
.block(Block::default().title("Sparkline:"))
|
||||
.style(Style::default().fg(Color::Green))
|
||||
.data(&app.sparkline.points)
|
||||
.bar_set(if app.enhanced_graphics {
|
||||
symbols::bar::NINE_LEVELS
|
||||
} else {
|
||||
symbols::bar::THREE_LEVELS
|
||||
});
|
||||
f.render_widget(sparkline, chunks[1]);
|
||||
|
||||
let line_gauge = LineGauge::default()
|
||||
.block(Block::default().title("LineGauge:"))
|
||||
.gauge_style(Style::default().fg(Color::Magenta))
|
||||
.line_set(if app.enhanced_graphics {
|
||||
symbols::line::THICK
|
||||
} else {
|
||||
symbols::line::NORMAL
|
||||
})
|
||||
.ratio(app.progress);
|
||||
f.render_widget(line_gauge, chunks[2]);
|
||||
}
|
||||
|
||||
fn draw_charts<B>(f: &mut Frame<B>, app: &mut App, area: Rect)
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
let constraints = if app.show_chart {
|
||||
vec![Constraint::Percentage(50), Constraint::Percentage(50)]
|
||||
} else {
|
||||
vec![Constraint::Percentage(100)]
|
||||
};
|
||||
let chunks = Layout::default()
|
||||
.constraints(constraints)
|
||||
.direction(Direction::Horizontal)
|
||||
.split(area);
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(chunks[0]);
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.direction(Direction::Horizontal)
|
||||
.split(chunks[0]);
|
||||
|
||||
// Draw tasks
|
||||
let tasks: Vec<ListItem> = app
|
||||
.tasks
|
||||
.items
|
||||
.iter()
|
||||
.map(|i| ListItem::new(vec![Line::from(Span::raw(*i))]))
|
||||
.collect();
|
||||
let tasks = List::new(tasks)
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
|
||||
.highlight_symbol("> ");
|
||||
f.render_stateful_widget(tasks, chunks[0], &mut app.tasks.state);
|
||||
|
||||
// Draw logs
|
||||
let info_style = Style::default().fg(Color::Blue);
|
||||
let warning_style = Style::default().fg(Color::Yellow);
|
||||
let error_style = Style::default().fg(Color::Magenta);
|
||||
let critical_style = Style::default().fg(Color::Red);
|
||||
let logs: Vec<ListItem> = app
|
||||
.logs
|
||||
.items
|
||||
.iter()
|
||||
.map(|&(evt, level)| {
|
||||
let s = match level {
|
||||
"ERROR" => error_style,
|
||||
"CRITICAL" => critical_style,
|
||||
"WARNING" => warning_style,
|
||||
_ => info_style,
|
||||
};
|
||||
let content = vec![Line::from(vec![
|
||||
Span::styled(format!("{level:<9}"), s),
|
||||
Span::raw(evt),
|
||||
])];
|
||||
ListItem::new(content)
|
||||
})
|
||||
.collect();
|
||||
let logs = List::new(logs).block(Block::default().borders(Borders::ALL).title("List"));
|
||||
f.render_stateful_widget(logs, chunks[1], &mut app.logs.state);
|
||||
}
|
||||
|
||||
let barchart = BarChart::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("Bar chart"))
|
||||
.data(&app.barchart)
|
||||
.bar_width(3)
|
||||
.bar_gap(2)
|
||||
.bar_set(if app.enhanced_graphics {
|
||||
symbols::bar::NINE_LEVELS
|
||||
} else {
|
||||
symbols::bar::THREE_LEVELS
|
||||
})
|
||||
.value_style(
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::Green)
|
||||
.add_modifier(Modifier::ITALIC),
|
||||
)
|
||||
.label_style(Style::default().fg(Color::Yellow))
|
||||
.bar_style(Style::default().fg(Color::Green));
|
||||
f.render_widget(barchart, chunks[1]);
|
||||
}
|
||||
if app.show_chart {
|
||||
let x_labels = vec![
|
||||
Span::styled(
|
||||
format!("{}", app.signals.window[0]),
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(format!(
|
||||
"{}",
|
||||
(app.signals.window[0] + app.signals.window[1]) / 2.0
|
||||
)),
|
||||
Span::styled(
|
||||
format!("{}", app.signals.window[1]),
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
),
|
||||
];
|
||||
let datasets = vec![
|
||||
Dataset::default()
|
||||
.name("data2")
|
||||
.marker(symbols::Marker::Dot)
|
||||
.style(Style::default().fg(Color::Cyan))
|
||||
.data(&app.signals.sin1.points),
|
||||
Dataset::default()
|
||||
.name("data3")
|
||||
.marker(if app.enhanced_graphics {
|
||||
symbols::Marker::Braille
|
||||
} else {
|
||||
symbols::Marker::Dot
|
||||
})
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.data(&app.signals.sin2.points),
|
||||
];
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::default()
|
||||
.title(Span::styled(
|
||||
"Chart",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
))
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("X Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.bounds(app.signals.window)
|
||||
.labels(x_labels),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.bounds([-20.0, 20.0])
|
||||
.labels(vec![
|
||||
Span::styled("-20", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw("0"),
|
||||
Span::styled("20", Style::default().add_modifier(Modifier::BOLD)),
|
||||
]),
|
||||
);
|
||||
f.render_widget(chart, chunks[1]);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_text<B>(f: &mut Frame<B>, area: Rect)
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
let text = vec![
|
||||
Line::from("This is a paragraph with several lines. You can change style your text the way you want"),
|
||||
Line::from(""),
|
||||
Line::from(vec![
|
||||
Span::from("For example: "),
|
||||
Span::styled("under", Style::default().fg(Color::Red)),
|
||||
Span::raw(" "),
|
||||
Span::styled("the", Style::default().fg(Color::Green)),
|
||||
Span::raw(" "),
|
||||
Span::styled("rainbow", Style::default().fg(Color::Blue)),
|
||||
Span::raw("."),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::raw("Oh and if you didn't "),
|
||||
Span::styled("notice", Style::default().add_modifier(Modifier::ITALIC)),
|
||||
Span::raw(" you can "),
|
||||
Span::styled("automatically", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(" "),
|
||||
Span::styled("wrap", Style::default().add_modifier(Modifier::REVERSED)),
|
||||
Span::raw(" your "),
|
||||
Span::styled("text", Style::default().add_modifier(Modifier::UNDERLINED)),
|
||||
Span::raw(".")
|
||||
]),
|
||||
Line::from(
|
||||
"One more thing is that it should display unicode characters: 10€"
|
||||
),
|
||||
];
|
||||
let block = Block::default().borders(Borders::ALL).title(Span::styled(
|
||||
"Footer",
|
||||
Style::default()
|
||||
.fg(Color::Magenta)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, area);
|
||||
}
|
||||
|
||||
fn draw_second_tab<B>(f: &mut Frame<B>, app: &mut App, area: Rect)
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)].as_ref())
|
||||
.direction(Direction::Horizontal)
|
||||
.split(area);
|
||||
let up_style = Style::default().fg(Color::Green);
|
||||
let failure_style = Style::default()
|
||||
.fg(Color::Red)
|
||||
.add_modifier(Modifier::RAPID_BLINK | Modifier::CROSSED_OUT);
|
||||
let rows = app.servers.iter().map(|s| {
|
||||
let style = if s.status == "Up" {
|
||||
up_style
|
||||
} else {
|
||||
failure_style
|
||||
};
|
||||
Row::new(vec![s.name, s.location, s.status]).style(style)
|
||||
});
|
||||
let table = Table::new(rows)
|
||||
.header(
|
||||
Row::new(vec!["Server", "Location", "Status"])
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.bottom_margin(1),
|
||||
)
|
||||
.block(Block::default().title("Servers").borders(Borders::ALL))
|
||||
.widths(&[
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(10),
|
||||
]);
|
||||
f.render_widget(table, chunks[0]);
|
||||
|
||||
let map = Canvas::default()
|
||||
.block(Block::default().title("World").borders(Borders::ALL))
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&Map {
|
||||
color: Color::White,
|
||||
resolution: MapResolution::High,
|
||||
});
|
||||
ctx.layer();
|
||||
ctx.draw(&Rectangle {
|
||||
x: 0.0,
|
||||
y: 30.0,
|
||||
width: 10.0,
|
||||
height: 10.0,
|
||||
color: Color::Yellow,
|
||||
});
|
||||
ctx.draw(&Circle {
|
||||
x: app.servers[2].coords.1,
|
||||
y: app.servers[2].coords.0,
|
||||
radius: 10.0,
|
||||
color: Color::Green,
|
||||
});
|
||||
for (i, s1) in app.servers.iter().enumerate() {
|
||||
for s2 in &app.servers[i + 1..] {
|
||||
ctx.draw(&CanvasLine {
|
||||
x1: s1.coords.1,
|
||||
y1: s1.coords.0,
|
||||
y2: s2.coords.0,
|
||||
x2: s2.coords.1,
|
||||
color: Color::Yellow,
|
||||
});
|
||||
}
|
||||
}
|
||||
for server in &app.servers {
|
||||
let color = if server.status == "Up" {
|
||||
Color::Green
|
||||
} else {
|
||||
Color::Red
|
||||
};
|
||||
ctx.print(
|
||||
server.coords.1,
|
||||
server.coords.0,
|
||||
Span::styled("X", Style::default().fg(color)),
|
||||
);
|
||||
}
|
||||
})
|
||||
.marker(if app.enhanced_graphics {
|
||||
symbols::Marker::Braille
|
||||
} else {
|
||||
symbols::Marker::Dot
|
||||
})
|
||||
.x_bounds([-180.0, 180.0])
|
||||
.y_bounds([-90.0, 90.0]);
|
||||
f.render_widget(map, chunks[1]);
|
||||
}
|
||||
|
||||
fn draw_third_tab<B>(f: &mut Frame<B>, _app: &mut App, area: Rect)
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)])
|
||||
.split(area);
|
||||
let colors = [
|
||||
Color::Reset,
|
||||
Color::Black,
|
||||
Color::Red,
|
||||
Color::Green,
|
||||
Color::Yellow,
|
||||
Color::Blue,
|
||||
Color::Magenta,
|
||||
Color::Cyan,
|
||||
Color::Gray,
|
||||
Color::DarkGray,
|
||||
Color::LightRed,
|
||||
Color::LightGreen,
|
||||
Color::LightYellow,
|
||||
Color::LightBlue,
|
||||
Color::LightMagenta,
|
||||
Color::LightCyan,
|
||||
Color::White,
|
||||
];
|
||||
let items: Vec<Row> = colors
|
||||
.iter()
|
||||
.map(|c| {
|
||||
let cells = vec![
|
||||
Cell::from(Span::raw(format!("{c:?}: "))),
|
||||
Cell::from(Span::styled("Foreground", Style::default().fg(*c))),
|
||||
Cell::from(Span::styled("Background", Style::default().bg(*c))),
|
||||
];
|
||||
Row::new(cells)
|
||||
})
|
||||
.collect();
|
||||
let table = Table::new(items)
|
||||
.block(Block::default().title("Colors").borders(Borders::ALL))
|
||||
.widths(&[
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
]);
|
||||
f.render_widget(table, chunks[0]);
|
||||
}
|
||||
@@ -1,157 +1,167 @@
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use std::io;
|
||||
use std::thread;
|
||||
use std::time;
|
||||
use std::sync::mpsc;
|
||||
|
||||
use termion::event;
|
||||
use termion::input::TermRead;
|
||||
|
||||
use tui::Terminal;
|
||||
use tui::backend::MouseBackend;
|
||||
use tui::widgets::{Block, Borders, Gauge, Widget};
|
||||
use tui::layout::{Direction, Group, Rect, Size};
|
||||
use tui::style::{Color, Modifier, Style};
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::Span,
|
||||
widgets::{Block, Borders, Gauge},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
struct App {
|
||||
size: Rect,
|
||||
progress1: u16,
|
||||
progress2: u16,
|
||||
progress3: u16,
|
||||
progress3: f64,
|
||||
progress4: u16,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> App {
|
||||
App {
|
||||
size: Rect::default(),
|
||||
progress1: 0,
|
||||
progress2: 0,
|
||||
progress3: 0,
|
||||
progress3: 0.45,
|
||||
progress4: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn advance(&mut self) {
|
||||
self.progress1 += 5;
|
||||
fn on_tick(&mut self) {
|
||||
self.progress1 += 1;
|
||||
if self.progress1 > 100 {
|
||||
self.progress1 = 0;
|
||||
}
|
||||
self.progress2 += 10;
|
||||
self.progress2 += 2;
|
||||
if self.progress2 > 100 {
|
||||
self.progress2 = 0;
|
||||
}
|
||||
self.progress3 += 1;
|
||||
if self.progress3 > 100 {
|
||||
self.progress3 = 0;
|
||||
self.progress3 += 0.001;
|
||||
if self.progress3 > 1.0 {
|
||||
self.progress3 = 0.0;
|
||||
}
|
||||
self.progress4 += 3;
|
||||
self.progress4 += 1;
|
||||
if self.progress4 > 100 {
|
||||
self.progress4 = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Event {
|
||||
Input(event::Key),
|
||||
Tick,
|
||||
}
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
fn main() {
|
||||
// Terminal initialization
|
||||
let backend = MouseBackend::new().unwrap();
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// Channels
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let input_tx = tx.clone();
|
||||
let clock_tx = tx.clone();
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
// Input
|
||||
thread::spawn(move || {
|
||||
let stdin = io::stdin();
|
||||
for c in stdin.keys() {
|
||||
let evt = c.unwrap();
|
||||
input_tx.send(Event::Input(evt)).unwrap();
|
||||
if evt == event::Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Tick
|
||||
thread::spawn(move || loop {
|
||||
clock_tx.send(Event::Tick).unwrap();
|
||||
thread::sleep(time::Duration::from_millis(500));
|
||||
});
|
||||
|
||||
// App
|
||||
let mut app = App::new();
|
||||
|
||||
// First draw call
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
app.size = terminal.size().unwrap();
|
||||
draw(&mut terminal, &app);
|
||||
|
||||
loop {
|
||||
let size = terminal.size().unwrap();
|
||||
if size != app.size {
|
||||
terminal.resize(size).unwrap();
|
||||
app.size = size;
|
||||
}
|
||||
|
||||
let evt = rx.recv().unwrap();
|
||||
match evt {
|
||||
Event::Input(input) => if input == event::Key::Char('q') {
|
||||
break;
|
||||
},
|
||||
Event::Tick => {
|
||||
app.advance();
|
||||
}
|
||||
}
|
||||
draw(&mut terminal, &app);
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
terminal.show_cursor().unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<MouseBackend>, app: &App) {
|
||||
Group::default()
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.sizes(&[
|
||||
Size::Percent(25),
|
||||
Size::Percent(25),
|
||||
Size::Percent(25),
|
||||
Size::Percent(25),
|
||||
])
|
||||
.render(t, &app.size, |t, chunks| {
|
||||
Gauge::default()
|
||||
.block(Block::default().title("Gauge1").borders(Borders::ALL))
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.percent(app.progress1)
|
||||
.render(t, &chunks[0]);
|
||||
Gauge::default()
|
||||
.block(Block::default().title("Gauge2").borders(Borders::ALL))
|
||||
.style(Style::default().fg(Color::Magenta).bg(Color::Green))
|
||||
.percent(app.progress2)
|
||||
.label(&format!("{}/100", app.progress2))
|
||||
.render(t, &chunks[1]);
|
||||
Gauge::default()
|
||||
.block(Block::default().title("Gauge2").borders(Borders::ALL))
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.percent(app.progress3)
|
||||
.render(t, &chunks[2]);
|
||||
Gauge::default()
|
||||
.block(Block::default().title("Gauge3").borders(Borders::ALL))
|
||||
.style(Style::default().fg(Color::Cyan).modifier(Modifier::Italic))
|
||||
.percent(app.progress4)
|
||||
.label(&format!("{}/100", app.progress2))
|
||||
.render(t, &chunks[3]);
|
||||
});
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
|
||||
t.draw().unwrap();
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("Gauge1").borders(Borders::ALL))
|
||||
.gauge_style(Style::default().fg(Color::Yellow))
|
||||
.percent(app.progress1);
|
||||
f.render_widget(gauge, chunks[0]);
|
||||
|
||||
let label = format!("{}/100", app.progress2);
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("Gauge2").borders(Borders::ALL))
|
||||
.gauge_style(Style::default().fg(Color::Magenta).bg(Color::Green))
|
||||
.percent(app.progress2)
|
||||
.label(label);
|
||||
f.render_widget(gauge, chunks[1]);
|
||||
|
||||
let label = Span::styled(
|
||||
format!("{:.2}%", app.progress3 * 100.0),
|
||||
Style::default()
|
||||
.fg(Color::Red)
|
||||
.add_modifier(Modifier::ITALIC | Modifier::BOLD),
|
||||
);
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("Gauge3").borders(Borders::ALL))
|
||||
.gauge_style(Style::default().fg(Color::Yellow))
|
||||
.ratio(app.progress3)
|
||||
.label(label)
|
||||
.use_unicode(true);
|
||||
f.render_widget(gauge, chunks[2]);
|
||||
|
||||
let label = format!("{}/100", app.progress4);
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("Gauge4"))
|
||||
.gauge_style(
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::ITALIC),
|
||||
)
|
||||
.percent(app.progress4)
|
||||
.label(label);
|
||||
f.render_widget(gauge, chunks[3]);
|
||||
}
|
||||
|
||||
80
examples/hello_world.rs
Normal file
80
examples/hello_world.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use std::{
|
||||
io::{self, Stdout},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{backend::CrosstermBackend, widgets::Paragraph, Terminal};
|
||||
|
||||
/// This is a bare minimum example. There are many approaches to running an application loop, so
|
||||
/// this is not meant to be prescriptive. It is only meant to demonstrate the basic setup and
|
||||
/// teardown of a terminal application.
|
||||
///
|
||||
/// A more robust application would probably want to handle errors and ensure that the terminal is
|
||||
/// restored to a sane state before exiting. This example does not do that. It also does not handle
|
||||
/// events or update the application state. It just draws a greeting and exits when the user
|
||||
/// presses 'q'.
|
||||
fn main() -> Result<()> {
|
||||
let mut terminal = setup_terminal().context("setup failed")?;
|
||||
run(&mut terminal).context("app loop failed")?;
|
||||
restore_terminal(&mut terminal).context("restore terminal failed")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Setup the terminal. This is where you would enable raw mode, enter the alternate screen, and
|
||||
/// hide the cursor. This example does not handle errors. A more robust application would probably
|
||||
/// want to handle errors and ensure that the terminal is restored to a sane state before exiting.
|
||||
fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
|
||||
let mut stdout = io::stdout();
|
||||
enable_raw_mode().context("failed to enable raw mode")?;
|
||||
execute!(stdout, EnterAlternateScreen).context("unable to enter alternate screen")?;
|
||||
Terminal::new(CrosstermBackend::new(stdout)).context("creating terminal failed")
|
||||
}
|
||||
|
||||
/// Restore the terminal. This is where you disable raw mode, leave the alternate screen, and show
|
||||
/// the cursor.
|
||||
fn restore_terminal(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
|
||||
disable_raw_mode().context("failed to disable raw mode")?;
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)
|
||||
.context("unable to switch to main screen")?;
|
||||
terminal.show_cursor().context("unable to show cursor")
|
||||
}
|
||||
|
||||
/// Run the application loop. This is where you would handle events and update the application
|
||||
/// state. This example exits when the user presses 'q'. Other styles of application loops are
|
||||
/// possible, for example, you could have multiple application states and switch between them based
|
||||
/// on events, or you could have a single application state and update it based on events.
|
||||
fn run(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
|
||||
loop {
|
||||
terminal.draw(crate::render_app)?;
|
||||
if should_quit()? {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Render the application. This is where you would draw the application UI. This example just
|
||||
/// draws a greeting.
|
||||
fn render_app(frame: &mut ratatui::Frame<CrosstermBackend<Stdout>>) {
|
||||
let greeting = Paragraph::new("Hello World! (press 'q' to quit)");
|
||||
frame.render_widget(greeting, frame.size());
|
||||
}
|
||||
|
||||
/// Check if the user has pressed 'q'. This is where you would handle events. This example just
|
||||
/// checks if the user has pressed 'q' and returns true if they have. It does not handle any other
|
||||
/// events. There is a 250ms timeout on the event poll so that the application can exit in a timely
|
||||
/// manner, and to ensure that the terminal is rendered at least once every 250ms.
|
||||
fn should_quit() -> Result<bool> {
|
||||
if event::poll(Duration::from_millis(250)).context("event poll failed")? {
|
||||
if let Event::Key(key) = event::read().context("event read failed")? {
|
||||
return Ok(KeyCode::Char('q') == key.code);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
111
examples/histogram.rs
Normal file
111
examples/histogram.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Style},
|
||||
widgets::{Block, Borders, Histogram},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use rand::{rngs::ThreadRng, thread_rng, Rng};
|
||||
|
||||
struct App {
|
||||
data: Vec<u64>,
|
||||
size: usize,
|
||||
rng: ThreadRng,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new(size: usize) -> App {
|
||||
let data = vec![0; size];
|
||||
App {
|
||||
data,
|
||||
rng: thread_rng(),
|
||||
size,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_tick(&mut self) {
|
||||
for i in 0..self.size {
|
||||
self.data[i] = self.rng.gen_range(0..100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::new(100);
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints([Constraint::Percentage(100)].as_ref())
|
||||
.split(f.size());
|
||||
let histogram = Histogram::default()
|
||||
.block(Block::default().title("Data1").borders(Borders::ALL))
|
||||
.data(&app.data, 10)
|
||||
.bar_style(Style::default().fg(Color::Yellow))
|
||||
.value_style(Style::default().fg(Color::Black).bg(Color::Yellow));
|
||||
f.render_widget(histogram, chunks[0]);
|
||||
}
|
||||
291
examples/inline.rs
Normal file
291
examples/inline.rs
Normal file
@@ -0,0 +1,291 @@
|
||||
use std::{
|
||||
collections::{BTreeMap, VecDeque},
|
||||
error::Error,
|
||||
io,
|
||||
sync::mpsc,
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use rand::distributions::{Distribution, Uniform};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
symbols,
|
||||
text::{Line, Span},
|
||||
widgets::{block::title::Title, Block, Gauge, LineGauge, List, ListItem, Paragraph, Widget},
|
||||
Frame, Terminal, TerminalOptions, Viewport,
|
||||
};
|
||||
|
||||
const NUM_DOWNLOADS: usize = 10;
|
||||
|
||||
type DownloadId = usize;
|
||||
type WorkerId = usize;
|
||||
|
||||
enum Event {
|
||||
Input(crossterm::event::KeyEvent),
|
||||
Tick,
|
||||
Resize,
|
||||
DownloadUpdate(WorkerId, DownloadId, f64),
|
||||
DownloadDone(WorkerId, DownloadId),
|
||||
}
|
||||
|
||||
struct Downloads {
|
||||
pending: VecDeque<Download>,
|
||||
in_progress: BTreeMap<WorkerId, DownloadInProgress>,
|
||||
}
|
||||
|
||||
impl Downloads {
|
||||
fn next(&mut self, worker_id: WorkerId) -> Option<Download> {
|
||||
match self.pending.pop_front() {
|
||||
Some(d) => {
|
||||
self.in_progress.insert(
|
||||
worker_id,
|
||||
DownloadInProgress {
|
||||
id: d.id,
|
||||
started_at: Instant::now(),
|
||||
progress: 0.0,
|
||||
},
|
||||
);
|
||||
Some(d)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DownloadInProgress {
|
||||
id: DownloadId,
|
||||
started_at: Instant,
|
||||
progress: f64,
|
||||
}
|
||||
|
||||
struct Download {
|
||||
id: DownloadId,
|
||||
size: usize,
|
||||
}
|
||||
|
||||
struct Worker {
|
||||
id: WorkerId,
|
||||
tx: mpsc::Sender<Download>,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
crossterm::terminal::enable_raw_mode()?;
|
||||
let stdout = io::stdout();
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: Viewport::Inline(8),
|
||||
},
|
||||
)?;
|
||||
|
||||
let (tx, rx) = mpsc::channel();
|
||||
input_handling(tx.clone());
|
||||
let workers = workers(tx);
|
||||
let mut downloads = downloads();
|
||||
|
||||
for w in &workers {
|
||||
let d = downloads.next(w.id).unwrap();
|
||||
w.tx.send(d).unwrap();
|
||||
}
|
||||
|
||||
run_app(&mut terminal, workers, downloads, rx)?;
|
||||
|
||||
crossterm::terminal::disable_raw_mode()?;
|
||||
terminal.clear()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn input_handling(tx: mpsc::Sender<Event>) {
|
||||
let tick_rate = Duration::from_millis(200);
|
||||
thread::spawn(move || {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
// poll for tick rate duration, if no events, sent tick event.
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout).unwrap() {
|
||||
match crossterm::event::read().unwrap() {
|
||||
crossterm::event::Event::Key(key) => tx.send(Event::Input(key)).unwrap(),
|
||||
crossterm::event::Event::Resize(_, _) => tx.send(Event::Resize).unwrap(),
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
tx.send(Event::Tick).unwrap();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn workers(tx: mpsc::Sender<Event>) -> Vec<Worker> {
|
||||
(0..4)
|
||||
.map(|id| {
|
||||
let (worker_tx, worker_rx) = mpsc::channel::<Download>();
|
||||
let tx = tx.clone();
|
||||
thread::spawn(move || {
|
||||
while let Ok(download) = worker_rx.recv() {
|
||||
let mut remaining = download.size;
|
||||
while remaining > 0 {
|
||||
let wait = (remaining as u64).min(10);
|
||||
thread::sleep(Duration::from_millis(wait * 10));
|
||||
remaining = remaining.saturating_sub(10);
|
||||
let progress = (download.size - remaining) * 100 / download.size;
|
||||
tx.send(Event::DownloadUpdate(id, download.id, progress as f64))
|
||||
.unwrap();
|
||||
}
|
||||
tx.send(Event::DownloadDone(id, download.id)).unwrap();
|
||||
}
|
||||
});
|
||||
Worker { id, tx: worker_tx }
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn downloads() -> Downloads {
|
||||
let distribution = Uniform::new(0, 1000);
|
||||
let mut rng = rand::thread_rng();
|
||||
let pending = (0..NUM_DOWNLOADS)
|
||||
.map(|id| {
|
||||
let size = distribution.sample(&mut rng);
|
||||
Download { id, size }
|
||||
})
|
||||
.collect();
|
||||
Downloads {
|
||||
pending,
|
||||
in_progress: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
workers: Vec<Worker>,
|
||||
mut downloads: Downloads,
|
||||
rx: mpsc::Receiver<Event>,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let mut redraw = true;
|
||||
loop {
|
||||
if redraw {
|
||||
terminal.draw(|f| ui(f, &downloads))?;
|
||||
}
|
||||
redraw = true;
|
||||
|
||||
match rx.recv()? {
|
||||
Event::Input(event) => {
|
||||
if event.code == crossterm::event::KeyCode::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Event::Resize => {
|
||||
terminal.autoresize()?;
|
||||
}
|
||||
Event::Tick => {}
|
||||
Event::DownloadUpdate(worker_id, _download_id, progress) => {
|
||||
let download = downloads.in_progress.get_mut(&worker_id).unwrap();
|
||||
download.progress = progress;
|
||||
redraw = false
|
||||
}
|
||||
Event::DownloadDone(worker_id, download_id) => {
|
||||
let download = downloads.in_progress.remove(&worker_id).unwrap();
|
||||
terminal.insert_before(1, |buf| {
|
||||
Paragraph::new(Line::from(vec![
|
||||
Span::from("Finished "),
|
||||
Span::styled(
|
||||
format!("download {download_id}"),
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::from(format!(
|
||||
" in {}ms",
|
||||
download.started_at.elapsed().as_millis()
|
||||
)),
|
||||
]))
|
||||
.render(buf.area, buf);
|
||||
})?;
|
||||
match downloads.next(worker_id) {
|
||||
Some(d) => workers[worker_id].tx.send(d).unwrap(),
|
||||
None => {
|
||||
if downloads.in_progress.is_empty() {
|
||||
terminal.insert_before(1, |buf| {
|
||||
Paragraph::new("Done !").render(buf.area, buf);
|
||||
})?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, downloads: &Downloads) {
|
||||
let size = f.size();
|
||||
|
||||
let block = Block::default().title(Title::from("Progress").alignment(Alignment::Center));
|
||||
f.render_widget(block, size);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.constraints(vec![Constraint::Length(2), Constraint::Length(4)])
|
||||
.margin(1)
|
||||
.split(size);
|
||||
|
||||
// total progress
|
||||
let done = NUM_DOWNLOADS - downloads.pending.len() - downloads.in_progress.len();
|
||||
let progress = LineGauge::default()
|
||||
.gauge_style(Style::default().fg(Color::Blue))
|
||||
.label(format!("{done}/{NUM_DOWNLOADS}"))
|
||||
.ratio(done as f64 / NUM_DOWNLOADS as f64);
|
||||
f.render_widget(progress, chunks[0]);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(vec![Constraint::Percentage(20), Constraint::Percentage(80)])
|
||||
.split(chunks[1]);
|
||||
|
||||
// in progress downloads
|
||||
let items: Vec<ListItem> = downloads
|
||||
.in_progress
|
||||
.values()
|
||||
.map(|download| {
|
||||
ListItem::new(Line::from(vec![
|
||||
Span::raw(symbols::DOT),
|
||||
Span::styled(
|
||||
format!(" download {:>2}", download.id),
|
||||
Style::default()
|
||||
.fg(Color::LightGreen)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(format!(
|
||||
" ({}ms)",
|
||||
download.started_at.elapsed().as_millis()
|
||||
)),
|
||||
]))
|
||||
})
|
||||
.collect();
|
||||
let list = List::new(items);
|
||||
f.render_widget(list, chunks[0]);
|
||||
|
||||
for (i, (_, download)) in downloads.in_progress.iter().enumerate() {
|
||||
let gauge = Gauge::default()
|
||||
.gauge_style(Style::default().fg(Color::Yellow))
|
||||
.ratio(download.progress / 100.0);
|
||||
if chunks[1].top().saturating_add(i as u16) > size.bottom() {
|
||||
continue;
|
||||
}
|
||||
f.render_widget(
|
||||
gauge,
|
||||
Rect {
|
||||
x: chunks[1].left(),
|
||||
y: chunks[1].top().saturating_add(i as u16),
|
||||
width: chunks[1].width,
|
||||
height: 1,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
71
examples/layout.rs
Normal file
71
examples/layout.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use std::{error::Error, io};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
widgets::{Block, Borders},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let res = run_app(&mut terminal);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(|f| ui(f))?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Percentage(80),
|
||||
Constraint::Percentage(10),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
|
||||
let block = Block::default().title("Block").borders(Borders::ALL);
|
||||
f.render_widget(block, chunks[0]);
|
||||
let block = Block::default().title("Block 2").borders(Borders::ALL);
|
||||
f.render_widget(block, chunks[2]);
|
||||
}
|
||||
358
examples/list.rs
358
examples/list.rs
@@ -1,41 +1,110 @@
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use std::io;
|
||||
use std::thread;
|
||||
use std::time;
|
||||
use std::sync::mpsc;
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Corner, Direction, Layout},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Borders, List, ListItem, ListState},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
use termion::event;
|
||||
use termion::input::TermRead;
|
||||
struct StatefulList<T> {
|
||||
state: ListState,
|
||||
items: Vec<T>,
|
||||
}
|
||||
|
||||
use tui::Terminal;
|
||||
use tui::backend::MouseBackend;
|
||||
use tui::widgets::{Block, Borders, Item, List, SelectableList, Widget};
|
||||
use tui::layout::{Direction, Group, Rect, Size};
|
||||
use tui::style::{Color, Modifier, Style};
|
||||
impl<T> StatefulList<T> {
|
||||
fn with_items(items: Vec<T>) -> StatefulList<T> {
|
||||
StatefulList {
|
||||
state: ListState::default(),
|
||||
items,
|
||||
}
|
||||
}
|
||||
|
||||
fn next(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.items.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
fn previous(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
self.items.len() - 1
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
fn unselect(&mut self) {
|
||||
self.state.select(None);
|
||||
}
|
||||
}
|
||||
|
||||
/// This struct holds the current state of the app. In particular, it has the `items` field which is
|
||||
/// a wrapper around `ListState`. Keeping track of the items state let us render the associated
|
||||
/// widget with its state and have access to features such as natural scrolling.
|
||||
///
|
||||
/// Check the event handling at the bottom to see how to change the state on incoming events.
|
||||
/// Check the drawing logic for items on how to specify the highlighting style for selected items.
|
||||
struct App<'a> {
|
||||
size: Rect,
|
||||
items: Vec<&'a str>,
|
||||
selected: usize,
|
||||
items: StatefulList<(&'a str, usize)>,
|
||||
events: Vec<(&'a str, &'a str)>,
|
||||
info_style: Style,
|
||||
warning_style: Style,
|
||||
error_style: Style,
|
||||
critical_style: Style,
|
||||
}
|
||||
|
||||
impl<'a> App<'a> {
|
||||
fn new() -> App<'a> {
|
||||
App {
|
||||
size: Rect::default(),
|
||||
items: vec![
|
||||
"Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", "Item9",
|
||||
"Item10", "Item11", "Item12", "Item13", "Item14", "Item15", "Item16", "Item17",
|
||||
"Item18", "Item19", "Item20", "Item21", "Item22", "Item23", "Item24",
|
||||
],
|
||||
selected: 0,
|
||||
items: StatefulList::with_items(vec![
|
||||
("Item0", 1),
|
||||
("Item1", 2),
|
||||
("Item2", 1),
|
||||
("Item3", 3),
|
||||
("Item4", 1),
|
||||
("Item5", 4),
|
||||
("Item6", 1),
|
||||
("Item7", 3),
|
||||
("Item8", 1),
|
||||
("Item9", 6),
|
||||
("Item10", 1),
|
||||
("Item11", 3),
|
||||
("Item12", 1),
|
||||
("Item13", 2),
|
||||
("Item14", 1),
|
||||
("Item15", 1),
|
||||
("Item16", 4),
|
||||
("Item17", 1),
|
||||
("Item18", 5),
|
||||
("Item19", 4),
|
||||
("Item20", 1),
|
||||
("Item21", 2),
|
||||
("Item22", 1),
|
||||
("Item23", 3),
|
||||
("Item24", 1),
|
||||
]),
|
||||
events: vec![
|
||||
("Event1", "INFO"),
|
||||
("Event2", "INFO"),
|
||||
@@ -64,126 +133,155 @@ impl<'a> App<'a> {
|
||||
("Event25", "INFO"),
|
||||
("Event26", "INFO"),
|
||||
],
|
||||
info_style: Style::default().fg(Color::White),
|
||||
warning_style: Style::default().fg(Color::Yellow),
|
||||
error_style: Style::default().fg(Color::Magenta),
|
||||
critical_style: Style::default().fg(Color::Red),
|
||||
}
|
||||
}
|
||||
|
||||
fn advance(&mut self) {
|
||||
let event = self.events.pop().unwrap();
|
||||
self.events.insert(0, event);
|
||||
/// Rotate through the event list.
|
||||
/// This only exists to simulate some kind of "progress"
|
||||
fn on_tick(&mut self) {
|
||||
let event = self.events.remove(0);
|
||||
self.events.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
enum Event {
|
||||
Input(event::Key),
|
||||
Tick,
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Terminal initialization
|
||||
let backend = MouseBackend::new().unwrap();
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
// Channels
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let input_tx = tx.clone();
|
||||
let clock_tx = tx.clone();
|
||||
|
||||
// Input
|
||||
thread::spawn(move || {
|
||||
let stdin = io::stdin();
|
||||
for c in stdin.keys() {
|
||||
let evt = c.unwrap();
|
||||
input_tx.send(Event::Input(evt)).unwrap();
|
||||
if evt == event::Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Tick
|
||||
thread::spawn(move || loop {
|
||||
clock_tx.send(Event::Tick).unwrap();
|
||||
thread::sleep(time::Duration::from_millis(500));
|
||||
});
|
||||
|
||||
// App
|
||||
let mut app = App::new();
|
||||
|
||||
// First draw call
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
app.size = terminal.size().unwrap();
|
||||
draw(&mut terminal, &app);
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
let size = terminal.size().unwrap();
|
||||
if size != app.size {
|
||||
terminal.resize(size).unwrap();
|
||||
app.size = size;
|
||||
}
|
||||
terminal.draw(|f| ui(f, &mut app))?;
|
||||
|
||||
let evt = rx.recv().unwrap();
|
||||
match evt {
|
||||
Event::Input(input) => match input {
|
||||
event::Key::Char('q') => {
|
||||
break;
|
||||
}
|
||||
event::Key::Down => {
|
||||
app.selected += 1;
|
||||
if app.selected > app.items.len() - 1 {
|
||||
app.selected = 0;
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Left => app.items.unselect(),
|
||||
KeyCode::Down => app.items.next(),
|
||||
KeyCode::Up => app.items.previous(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
event::Key::Up => if app.selected > 0 {
|
||||
app.selected -= 1;
|
||||
} else {
|
||||
app.selected = app.items.len() - 1;
|
||||
},
|
||||
_ => {}
|
||||
},
|
||||
Event::Tick => {
|
||||
app.advance();
|
||||
}
|
||||
}
|
||||
draw(&mut terminal, &app);
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
|
||||
terminal.show_cursor().unwrap();
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<MouseBackend>, app: &App) {
|
||||
Group::default()
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||
// Create two chunks with equal horizontal screen space
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.sizes(&[Size::Percent(50), Size::Percent(50)])
|
||||
.render(t, &app.size, |t, chunks| {
|
||||
SelectableList::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.items(&app.items)
|
||||
.select(app.selected)
|
||||
.highlight_style(Style::default().fg(Color::Yellow).modifier(Modifier::Bold))
|
||||
.highlight_symbol(">")
|
||||
.render(t, &chunks[0]);
|
||||
{
|
||||
let events = app.events.iter().map(|&(evt, level)| {
|
||||
Item::StyledData(
|
||||
format!("{}: {}", level, evt),
|
||||
match level {
|
||||
"ERROR" => &app.error_style,
|
||||
"CRITICAL" => &app.critical_style,
|
||||
"WARNING" => &app.warning_style,
|
||||
_ => &app.info_style,
|
||||
},
|
||||
)
|
||||
});
|
||||
List::new(events)
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.render(t, &chunks[1]);
|
||||
}
|
||||
});
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(f.size());
|
||||
|
||||
t.draw().unwrap();
|
||||
// Iterate through all elements in the `items` app and append some debug text to it.
|
||||
let items: Vec<ListItem> = app
|
||||
.items
|
||||
.items
|
||||
.iter()
|
||||
.map(|i| {
|
||||
let mut lines = vec![Line::from(i.0)];
|
||||
for _ in 0..i.1 {
|
||||
lines.push(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
.italic()
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
ListItem::new(lines).style(Style::default().fg(Color::Black).bg(Color::White))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Create a List from all list items and highlight the currently selected one
|
||||
let items = List::new(items)
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.bg(Color::LightGreen)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)
|
||||
.highlight_symbol(">> ");
|
||||
|
||||
// We can now render the item list
|
||||
f.render_stateful_widget(items, chunks[0], &mut app.items.state);
|
||||
|
||||
// Let's do the same for the events.
|
||||
// The event list doesn't have any state and only displays the current state of the list.
|
||||
let events: Vec<ListItem> = app
|
||||
.events
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|&(event, level)| {
|
||||
// Colorcode the level depending on its type
|
||||
let s = match level {
|
||||
"CRITICAL" => Style::default().fg(Color::Red),
|
||||
"ERROR" => Style::default().fg(Color::Magenta),
|
||||
"WARNING" => Style::default().fg(Color::Yellow),
|
||||
"INFO" => Style::default().fg(Color::Blue),
|
||||
_ => Style::default(),
|
||||
};
|
||||
// Add a example datetime and apply proper spacing between them
|
||||
let header = Line::from(vec![
|
||||
Span::styled(format!("{level:<9}"), s),
|
||||
" ".into(),
|
||||
"2020-01-01 10:00:00".italic(),
|
||||
]);
|
||||
// The event gets its own line
|
||||
let log = Line::from(vec![event.into()]);
|
||||
|
||||
// Here several things happen:
|
||||
// 1. Add a `---` spacing line above the final list entry
|
||||
// 2. Add the Level + datetime
|
||||
// 3. Add a spacer line
|
||||
// 4. Add the actual event
|
||||
ListItem::new(vec![
|
||||
Line::from("-".repeat(chunks[1].width as usize)),
|
||||
header,
|
||||
Line::from(""),
|
||||
log,
|
||||
])
|
||||
})
|
||||
.collect();
|
||||
let events_list = List::new(events)
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.start_corner(Corner::BottomLeft);
|
||||
f.render_widget(events_list, chunks[1]);
|
||||
}
|
||||
|
||||
143
examples/panic.rs
Normal file
143
examples/panic.rs
Normal file
@@ -0,0 +1,143 @@
|
||||
//! How to use a panic hook to reset the terminal before printing the panic to
|
||||
//! the terminal.
|
||||
//!
|
||||
//! When exiting normally or when handling `Result::Err`, we can reset the
|
||||
//! terminal manually at the end of `main` just before we print the error.
|
||||
//!
|
||||
//! Because a panic interrupts the normal control flow, manually resetting the
|
||||
//! terminal at the end of `main` won't do us any good. Instead, we need to
|
||||
//! make sure to set up a panic hook that first resets the terminal before
|
||||
//! handling the panic. This both reuses the standard panic hook to ensure a
|
||||
//! consistent panic handling UX and properly resets the terminal to not
|
||||
//! distort the output.
|
||||
//!
|
||||
//! That's why this example is set up to show both situations, with and without
|
||||
//! the chained panic hook, to see the difference.
|
||||
|
||||
#![deny(clippy::all)]
|
||||
#![warn(clippy::pedantic, clippy::nursery)]
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::Alignment,
|
||||
text::Line,
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
||||
|
||||
#[derive(Default)]
|
||||
struct App {
|
||||
hook_enabled: bool,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn chain_hook(&mut self) {
|
||||
let original_hook = std::panic::take_hook();
|
||||
|
||||
std::panic::set_hook(Box::new(move |panic| {
|
||||
reset_terminal().unwrap();
|
||||
original_hook(panic);
|
||||
}));
|
||||
|
||||
self.hook_enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut terminal = init_terminal()?;
|
||||
|
||||
let mut app = App::default();
|
||||
let res = run_tui(&mut terminal, &mut app);
|
||||
|
||||
reset_terminal()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Initializes the terminal.
|
||||
fn init_terminal() -> Result<Terminal<CrosstermBackend<io::Stdout>>> {
|
||||
crossterm::execute!(io::stdout(), EnterAlternateScreen)?;
|
||||
enable_raw_mode()?;
|
||||
|
||||
let backend = CrosstermBackend::new(io::stdout());
|
||||
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
terminal.hide_cursor()?;
|
||||
|
||||
Ok(terminal)
|
||||
}
|
||||
|
||||
/// Resets the terminal.
|
||||
fn reset_terminal() -> Result<()> {
|
||||
disable_raw_mode()?;
|
||||
crossterm::execute!(io::stdout(), LeaveAlternateScreen)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Runs the TUI loop.
|
||||
fn run_tui<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, app))?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('p') => {
|
||||
panic!("intentional demo panic");
|
||||
}
|
||||
|
||||
KeyCode::Char('e') => {
|
||||
app.chain_hook();
|
||||
}
|
||||
|
||||
_ => {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the TUI.
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let text = vec![
|
||||
if app.hook_enabled {
|
||||
Line::from("HOOK IS CURRENTLY **ENABLED**")
|
||||
} else {
|
||||
Line::from("HOOK IS CURRENTLY **DISABLED**")
|
||||
},
|
||||
Line::from(""),
|
||||
Line::from("press `p` to panic"),
|
||||
Line::from("press `e` to enable the terminal-resetting panic hook"),
|
||||
Line::from("press any other key to quit without panic"),
|
||||
Line::from(""),
|
||||
Line::from("when you panic without the chained hook,"),
|
||||
Line::from("you will likely have to reset your terminal afterwards"),
|
||||
Line::from("with the `reset` command"),
|
||||
Line::from(""),
|
||||
Line::from("with the chained panic hook enabled,"),
|
||||
Line::from("you should see the panic report as you would without ratatui"),
|
||||
Line::from(""),
|
||||
Line::from("try first without the panic handler to see the difference"),
|
||||
];
|
||||
|
||||
let b = Block::default()
|
||||
.title("Panic Handler Demo")
|
||||
.borders(Borders::ALL);
|
||||
|
||||
let p = Paragraph::new(text).block(b).alignment(Alignment::Center);
|
||||
|
||||
f.render_widget(p, f.size());
|
||||
}
|
||||
@@ -1,66 +1,167 @@
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use std::io;
|
||||
use termion::event;
|
||||
use termion::input::TermRead;
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Alignment, Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
text::{Line, Masked, Span},
|
||||
widgets::{Block, Borders, Paragraph, Wrap},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
use tui::Terminal;
|
||||
use tui::backend::MouseBackend;
|
||||
use tui::widgets::{Block, Paragraph, Widget};
|
||||
use tui::layout::{Direction, Group, Rect, Size};
|
||||
use tui::style::{Color, Style};
|
||||
struct App {
|
||||
scroll: u16,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut terminal = Terminal::new(MouseBackend::new().unwrap()).unwrap();
|
||||
let stdin = io::stdin();
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
impl App {
|
||||
fn new() -> App {
|
||||
App { scroll: 0 }
|
||||
}
|
||||
|
||||
let mut term_size = terminal.size().unwrap();
|
||||
draw(&mut terminal, &term_size);
|
||||
fn on_tick(&mut self) {
|
||||
self.scroll += 1;
|
||||
self.scroll %= 10;
|
||||
}
|
||||
}
|
||||
|
||||
for c in stdin.keys() {
|
||||
let size = terminal.size().unwrap();
|
||||
if size != term_size {
|
||||
terminal.resize(size).unwrap();
|
||||
term_size = size;
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
draw(&mut terminal, &term_size);
|
||||
let evt = c.unwrap();
|
||||
if evt == event::Key::Char('q') {
|
||||
break;
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
terminal.show_cursor().unwrap();
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<MouseBackend>, size: &Rect) {
|
||||
Block::default()
|
||||
.style(Style::default().bg(Color::White))
|
||||
.render(t, size);
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let size = f.size();
|
||||
|
||||
Group::default()
|
||||
// Words made "loooong" to demonstrate line breaking.
|
||||
let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. ";
|
||||
let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4);
|
||||
long_line.push('\n');
|
||||
|
||||
let block = Block::default().black();
|
||||
f.render_widget(block, size);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(5)
|
||||
.sizes(&[Size::Percent(100)])
|
||||
.render(t, size, |t, chunks| {
|
||||
Group::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.sizes(&[Size::Percent(100)])
|
||||
.render(t, &chunks[0], |t, chunks| {
|
||||
Paragraph::default()
|
||||
.text(
|
||||
"This is a line\n{fg=red This is a line}\n{bg=red This is a \
|
||||
line}\n{mod=italic This is a line}\n{mod=bold This is a \
|
||||
line}\n{mod=crossed_out This is a line}\n{mod=invert This is a \
|
||||
line}\n{mod=underline This is a \
|
||||
line}\n{bg=green;fg=yellow;mod=italic This is a line}\n",
|
||||
)
|
||||
.render(t, &chunks[0]);
|
||||
});
|
||||
});
|
||||
.margin(2)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(size);
|
||||
|
||||
t.draw().unwrap();
|
||||
let text = vec![
|
||||
Line::from("This is a line "),
|
||||
Line::from("This is a line ".red()),
|
||||
Line::from("This is a line".on_blue()),
|
||||
Line::from("This is a longer line".crossed_out()),
|
||||
Line::from(long_line.on_green()),
|
||||
Line::from("This is a line".green().italic()),
|
||||
Line::from(vec![
|
||||
"Masked text: ".into(),
|
||||
Span::styled(
|
||||
Masked::new("password", '*'),
|
||||
Style::default().fg(Color::Red),
|
||||
),
|
||||
]),
|
||||
];
|
||||
|
||||
let create_block = |title| {
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.title(Span::styled(
|
||||
title,
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
))
|
||||
};
|
||||
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.block(create_block("Default alignment (Left), no wrap"));
|
||||
f.render_widget(paragraph, chunks[0]);
|
||||
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.block(create_block("Default alignment (Left), with wrap"))
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, chunks[1]);
|
||||
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.block(create_block("Right alignment, with wrap"))
|
||||
.alignment(Alignment::Right)
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, chunks[2]);
|
||||
|
||||
let paragraph = Paragraph::new(text)
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.block(create_block("Center alignment, with wrap, with scroll"))
|
||||
.alignment(Alignment::Center)
|
||||
.wrap(Wrap { trim: true })
|
||||
.scroll((app.scroll, 0));
|
||||
f.render_widget(paragraph, chunks[3]);
|
||||
}
|
||||
|
||||
126
examples/popup.rs
Normal file
126
examples/popup.rs
Normal file
@@ -0,0 +1,126 @@
|
||||
use std::{error::Error, io};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
style::Stylize,
|
||||
widgets::{Block, Borders, Clear, Paragraph, Wrap},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
struct App {
|
||||
show_popup: bool,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> App {
|
||||
App { show_popup: false }
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Char('p') => app.show_popup = !app.show_popup,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let size = f.size();
|
||||
|
||||
let chunks = Layout::default()
|
||||
.constraints([Constraint::Percentage(20), Constraint::Percentage(80)].as_ref())
|
||||
.split(size);
|
||||
|
||||
let text = if app.show_popup {
|
||||
"Press p to close the popup"
|
||||
} else {
|
||||
"Press p to show the popup"
|
||||
};
|
||||
let paragraph = Paragraph::new(text.slow_blink())
|
||||
.alignment(Alignment::Center)
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, chunks[0]);
|
||||
|
||||
let block = Block::default()
|
||||
.title("Content")
|
||||
.borders(Borders::ALL)
|
||||
.on_blue();
|
||||
f.render_widget(block, chunks[1]);
|
||||
|
||||
if app.show_popup {
|
||||
let block = Block::default().title("Popup").borders(Borders::ALL);
|
||||
let area = centered_rect(60, 20, size);
|
||||
f.render_widget(Clear, area); //this clears out the background
|
||||
f.render_widget(block, area);
|
||||
}
|
||||
}
|
||||
|
||||
/// helper function to create a centered rect using up certain percentage of the available rect `r`
|
||||
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
|
||||
let popup_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage((100 - percent_y) / 2),
|
||||
Constraint::Percentage(percent_y),
|
||||
Constraint::Percentage((100 - percent_y) / 2),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(r);
|
||||
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage((100 - percent_x) / 2),
|
||||
Constraint::Percentage(percent_x),
|
||||
Constraint::Percentage((100 - percent_x) / 2),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(popup_layout[1])[1]
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
extern crate rustbox;
|
||||
extern crate tui;
|
||||
|
||||
use std::error::Error;
|
||||
use rustbox::Key;
|
||||
|
||||
use tui::Terminal;
|
||||
use tui::backend::RustboxBackend;
|
||||
use tui::widgets::{Block, Borders, Paragraph, Widget};
|
||||
use tui::layout::{Direction, Group, Size};
|
||||
use tui::style::{Color, Modifier, Style};
|
||||
|
||||
fn main() {
|
||||
let mut terminal = Terminal::new(RustboxBackend::new().unwrap()).unwrap();
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
draw(&mut terminal);
|
||||
loop {
|
||||
match terminal.backend().rustbox().poll_event(false) {
|
||||
Ok(rustbox::Event::KeyEvent(key)) => if key == Key::Char('q') {
|
||||
break;
|
||||
},
|
||||
Err(e) => panic!("{}", e.description()),
|
||||
_ => {}
|
||||
};
|
||||
draw(&mut terminal);
|
||||
}
|
||||
terminal.show_cursor().unwrap();
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<RustboxBackend>) {
|
||||
let size = t.size().unwrap();
|
||||
|
||||
Group::default()
|
||||
.direction(Direction::Vertical)
|
||||
.sizes(&[Size::Percent(100)])
|
||||
.render(t, &size, |t, chunks| {
|
||||
Paragraph::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Rustbox backend")
|
||||
.title_style(Style::default().fg(Color::Yellow).modifier(Modifier::Bold))
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Magenta)),
|
||||
)
|
||||
.text("It {yellow works}!")
|
||||
.render(t, &chunks[0]);
|
||||
});
|
||||
|
||||
t.draw().unwrap();
|
||||
}
|
||||
255
examples/scrollbar.rs
Normal file
255
examples/scrollbar.rs
Normal file
@@ -0,0 +1,255 @@
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Alignment, Constraint, Direction, Layout, Margin},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
text::{Line, Masked, Span},
|
||||
widgets::{
|
||||
scrollbar, Block, Borders, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState,
|
||||
},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
struct App {
|
||||
pub vertical_scroll_state: ScrollbarState,
|
||||
pub horizontal_scroll_state: ScrollbarState,
|
||||
pub vertical_scroll: usize,
|
||||
pub horizontal_scroll: usize,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::default();
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &mut app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Char('j') => {
|
||||
app.vertical_scroll = app.vertical_scroll.saturating_add(1);
|
||||
app.vertical_scroll_state = app
|
||||
.vertical_scroll_state
|
||||
.position(app.vertical_scroll as u16);
|
||||
}
|
||||
KeyCode::Char('k') => {
|
||||
app.vertical_scroll = app.vertical_scroll.saturating_sub(1);
|
||||
app.vertical_scroll_state = app
|
||||
.vertical_scroll_state
|
||||
.position(app.vertical_scroll as u16);
|
||||
}
|
||||
KeyCode::Char('h') => {
|
||||
app.horizontal_scroll = app.horizontal_scroll.saturating_sub(1);
|
||||
app.horizontal_scroll_state = app
|
||||
.horizontal_scroll_state
|
||||
.position(app.horizontal_scroll as u16);
|
||||
}
|
||||
KeyCode::Char('l') => {
|
||||
app.horizontal_scroll = app.horizontal_scroll.saturating_add(1);
|
||||
app.horizontal_scroll_state = app
|
||||
.horizontal_scroll_state
|
||||
.position(app.horizontal_scroll as u16);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||
let size = f.size();
|
||||
|
||||
// Words made "loooong" to demonstrate line breaking.
|
||||
let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. ";
|
||||
let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4);
|
||||
long_line.push('\n');
|
||||
|
||||
let block = Block::default().black();
|
||||
f.render_widget(block, size);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Min(1),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(size);
|
||||
|
||||
let text = vec![
|
||||
Line::from("This is a line "),
|
||||
Line::from("This is a line ".red()),
|
||||
Line::from("This is a line".on_dark_gray()),
|
||||
Line::from("This is a longer line".crossed_out()),
|
||||
Line::from(long_line.reset()),
|
||||
Line::from("This is a line".reset()),
|
||||
Line::from(vec![
|
||||
Span::raw("Masked text: "),
|
||||
Span::styled(
|
||||
Masked::new("password", '*'),
|
||||
Style::default().fg(Color::Red),
|
||||
),
|
||||
]),
|
||||
Line::from("This is a line "),
|
||||
Line::from("This is a line ".red()),
|
||||
Line::from("This is a line".on_dark_gray()),
|
||||
Line::from("This is a longer line".crossed_out()),
|
||||
Line::from(long_line.reset()),
|
||||
Line::from("This is a line".reset()),
|
||||
Line::from(vec![
|
||||
Span::raw("Masked text: "),
|
||||
Span::styled(
|
||||
Masked::new("password", '*'),
|
||||
Style::default().fg(Color::Red),
|
||||
),
|
||||
]),
|
||||
];
|
||||
app.vertical_scroll_state = app.vertical_scroll_state.content_length(text.len() as u16);
|
||||
app.horizontal_scroll_state = app
|
||||
.horizontal_scroll_state
|
||||
.content_length(long_line.len() as u16);
|
||||
|
||||
let create_block = |title| {
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.gray()
|
||||
.title(Span::styled(
|
||||
title,
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
))
|
||||
};
|
||||
|
||||
let title = Block::default()
|
||||
.title("Use h j k l to scroll ◄ ▲ ▼ ►")
|
||||
.title_alignment(Alignment::Center);
|
||||
f.render_widget(title, chunks[0]);
|
||||
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.gray()
|
||||
.block(create_block("Vertical scrollbar with arrows"))
|
||||
.scroll((app.vertical_scroll as u16, 0));
|
||||
f.render_widget(paragraph, chunks[1]);
|
||||
f.render_stateful_widget(
|
||||
Scrollbar::default()
|
||||
.orientation(ScrollbarOrientation::VerticalRight)
|
||||
.begin_symbol(Some("↑"))
|
||||
.end_symbol(Some("↓")),
|
||||
chunks[1],
|
||||
&mut app.vertical_scroll_state,
|
||||
);
|
||||
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.gray()
|
||||
.block(create_block(
|
||||
"Vertical scrollbar without arrows and mirrored",
|
||||
))
|
||||
.scroll((app.vertical_scroll as u16, 0));
|
||||
f.render_widget(paragraph, chunks[2]);
|
||||
f.render_stateful_widget(
|
||||
Scrollbar::default()
|
||||
.orientation(ScrollbarOrientation::VerticalLeft)
|
||||
.symbols(scrollbar::VERTICAL)
|
||||
.begin_symbol(None)
|
||||
.end_symbol(None),
|
||||
chunks[2].inner(&Margin {
|
||||
vertical: 1,
|
||||
horizontal: 0,
|
||||
}),
|
||||
&mut app.vertical_scroll_state,
|
||||
);
|
||||
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.gray()
|
||||
.block(create_block(
|
||||
"Horizontal scrollbar with only begin arrow & custom thumb symbol",
|
||||
))
|
||||
.scroll((0, app.horizontal_scroll as u16));
|
||||
f.render_widget(paragraph, chunks[3]);
|
||||
f.render_stateful_widget(
|
||||
Scrollbar::default()
|
||||
.orientation(ScrollbarOrientation::HorizontalBottom)
|
||||
.thumb_symbol("🬋")
|
||||
.end_symbol(None),
|
||||
chunks[3].inner(&Margin {
|
||||
vertical: 0,
|
||||
horizontal: 1,
|
||||
}),
|
||||
&mut app.horizontal_scroll_state,
|
||||
);
|
||||
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.gray()
|
||||
.block(create_block(
|
||||
"Horizontal scrollbar without arrows & custom thumb and track symbol",
|
||||
))
|
||||
.scroll((0, app.horizontal_scroll as u16));
|
||||
f.render_widget(paragraph, chunks[4]);
|
||||
f.render_stateful_widget(
|
||||
Scrollbar::default()
|
||||
.orientation(ScrollbarOrientation::HorizontalBottom)
|
||||
.thumb_symbol("░")
|
||||
.track_symbol("─"),
|
||||
chunks[4].inner(&Margin {
|
||||
vertical: 0,
|
||||
horizontal: 1,
|
||||
}),
|
||||
&mut app.horizontal_scroll_state,
|
||||
);
|
||||
}
|
||||
@@ -1,25 +1,49 @@
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
mod util;
|
||||
use util::*;
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use rand::{
|
||||
distributions::{Distribution, Uniform},
|
||||
rngs::ThreadRng,
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Style},
|
||||
widgets::{Block, Borders, Sparkline},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
use std::io;
|
||||
use std::thread;
|
||||
use std::time;
|
||||
use std::sync::mpsc;
|
||||
#[derive(Clone)]
|
||||
pub struct RandomSignal {
|
||||
distribution: Uniform<u64>,
|
||||
rng: ThreadRng,
|
||||
}
|
||||
|
||||
use termion::event;
|
||||
use termion::input::TermRead;
|
||||
impl RandomSignal {
|
||||
pub fn new(lower: u64, upper: u64) -> RandomSignal {
|
||||
RandomSignal {
|
||||
distribution: Uniform::new(lower, upper),
|
||||
rng: rand::thread_rng(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use tui::Terminal;
|
||||
use tui::backend::MouseBackend;
|
||||
use tui::widgets::{Block, Borders, Sparkline, Widget};
|
||||
use tui::layout::{Direction, Group, Rect, Size};
|
||||
use tui::style::{Color, Style};
|
||||
impl Iterator for RandomSignal {
|
||||
type Item = u64;
|
||||
fn next(&mut self) -> Option<u64> {
|
||||
Some(self.distribution.sample(&mut self.rng))
|
||||
}
|
||||
}
|
||||
|
||||
struct App {
|
||||
size: Rect,
|
||||
signal: RandomSignal,
|
||||
data1: Vec<u64>,
|
||||
data2: Vec<u64>,
|
||||
@@ -33,15 +57,14 @@ impl App {
|
||||
let data2 = signal.by_ref().take(200).collect::<Vec<u64>>();
|
||||
let data3 = signal.by_ref().take(200).collect::<Vec<u64>>();
|
||||
App {
|
||||
size: Rect::default(),
|
||||
signal: signal,
|
||||
data1: data1,
|
||||
data2: data2,
|
||||
data3: data3,
|
||||
signal,
|
||||
data1,
|
||||
data2,
|
||||
data3,
|
||||
}
|
||||
}
|
||||
|
||||
fn advance(&mut self) {
|
||||
fn on_tick(&mut self) {
|
||||
let value = self.signal.next().unwrap();
|
||||
self.data1.pop();
|
||||
self.data1.insert(0, value);
|
||||
@@ -54,105 +77,101 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
enum Event {
|
||||
Input(event::Key),
|
||||
Tick,
|
||||
}
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
fn main() {
|
||||
// Terminal initialization
|
||||
let backend = MouseBackend::new().unwrap();
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// Channels
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let input_tx = tx.clone();
|
||||
let clock_tx = tx.clone();
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
// Input
|
||||
thread::spawn(move || {
|
||||
let stdin = io::stdin();
|
||||
for c in stdin.keys() {
|
||||
let evt = c.unwrap();
|
||||
input_tx.send(Event::Input(evt)).unwrap();
|
||||
if evt == event::Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Tick
|
||||
thread::spawn(move || loop {
|
||||
clock_tx.send(Event::Tick).unwrap();
|
||||
thread::sleep(time::Duration::from_millis(500));
|
||||
});
|
||||
|
||||
// App
|
||||
let mut app = App::new();
|
||||
|
||||
// First draw call
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
app.size = terminal.size().unwrap();
|
||||
draw(&mut terminal, &app);
|
||||
|
||||
loop {
|
||||
let size = terminal.size().unwrap();
|
||||
if size != app.size {
|
||||
terminal.resize(size).unwrap();
|
||||
app.size = size;
|
||||
}
|
||||
|
||||
let evt = rx.recv().unwrap();
|
||||
match evt {
|
||||
Event::Input(input) => if input == event::Key::Char('q') {
|
||||
break;
|
||||
},
|
||||
Event::Tick => {
|
||||
app.advance();
|
||||
}
|
||||
}
|
||||
draw(&mut terminal, &app);
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
terminal.show_cursor().unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<MouseBackend>, app: &App) {
|
||||
Group::default()
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.sizes(&[Size::Fixed(3), Size::Fixed(3), Size::Fixed(7), Size::Min(0)])
|
||||
.render(t, &app.size, |t, chunks| {
|
||||
Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data1")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
)
|
||||
.data(&app.data1)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.render(t, &chunks[0]);
|
||||
Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data2")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
)
|
||||
.data(&app.data2)
|
||||
.style(Style::default().bg(Color::Green))
|
||||
.render(t, &chunks[1]);
|
||||
// Multiline
|
||||
Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data3")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
)
|
||||
.data(&app.data3)
|
||||
.style(Style::default().fg(Color::Red))
|
||||
.render(t, &chunks[2]);
|
||||
});
|
||||
|
||||
t.draw().unwrap();
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(7),
|
||||
Constraint::Min(0),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
let sparkline = Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data1")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
)
|
||||
.data(&app.data1)
|
||||
.style(Style::default().fg(Color::Yellow));
|
||||
f.render_widget(sparkline, chunks[0]);
|
||||
let sparkline = Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data2")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
)
|
||||
.data(&app.data2)
|
||||
.style(Style::default().bg(Color::Green));
|
||||
f.render_widget(sparkline, chunks[1]);
|
||||
// Multiline
|
||||
let sparkline = Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data3")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
)
|
||||
.data(&app.data3)
|
||||
.style(Style::default().fg(Color::Red));
|
||||
f.render_widget(sparkline, chunks[2]);
|
||||
}
|
||||
|
||||
@@ -1,109 +1,158 @@
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
use std::{error::Error, io};
|
||||
|
||||
use std::io;
|
||||
|
||||
use termion::event;
|
||||
use termion::input::TermRead;
|
||||
|
||||
use tui::Terminal;
|
||||
use tui::backend::MouseBackend;
|
||||
use tui::widgets::{Block, Borders, Row, Table, Widget};
|
||||
use tui::layout::{Direction, Group, Rect, Size};
|
||||
use tui::style::{Color, Modifier, Style};
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
widgets::{Block, Borders, Cell, Row, Table, TableState},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
struct App<'a> {
|
||||
size: Rect,
|
||||
state: TableState,
|
||||
items: Vec<Vec<&'a str>>,
|
||||
selected: usize,
|
||||
}
|
||||
|
||||
impl<'a> App<'a> {
|
||||
fn new() -> App<'a> {
|
||||
App {
|
||||
size: Rect::default(),
|
||||
state: TableState::default(),
|
||||
items: vec![
|
||||
vec!["Row12", "Row12", "Row13"],
|
||||
vec!["Row11", "Row12", "Row13"],
|
||||
vec!["Row21", "Row22", "Row23"],
|
||||
vec!["Row31", "Row32", "Row33"],
|
||||
vec!["Row41", "Row42", "Row43"],
|
||||
vec!["Row51", "Row52", "Row53"],
|
||||
vec!["Row61", "Row62", "Row63"],
|
||||
vec!["Row61", "Row62\nTest", "Row63"],
|
||||
vec!["Row71", "Row72", "Row73"],
|
||||
vec!["Row81", "Row82", "Row83"],
|
||||
vec!["Row91", "Row92", "Row93"],
|
||||
vec!["Row101", "Row102", "Row103"],
|
||||
vec!["Row111", "Row112", "Row113"],
|
||||
vec!["Row121", "Row122", "Row123"],
|
||||
vec!["Row131", "Row132", "Row133"],
|
||||
vec!["Row141", "Row142", "Row143"],
|
||||
vec!["Row151", "Row152", "Row153"],
|
||||
vec!["Row161", "Row162", "Row163"],
|
||||
vec!["Row171", "Row172", "Row173"],
|
||||
vec!["Row181", "Row182", "Row183"],
|
||||
vec!["Row191", "Row192", "Row193"],
|
||||
],
|
||||
selected: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Terminal initialization
|
||||
let backend = MouseBackend::new().unwrap();
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
// App
|
||||
let mut app = App::new();
|
||||
|
||||
// First draw call
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
app.size = terminal.size().unwrap();
|
||||
draw(&mut terminal, &app);
|
||||
|
||||
// Input
|
||||
let stdin = io::stdin();
|
||||
for c in stdin.keys() {
|
||||
let size = terminal.size().unwrap();
|
||||
if size != app.size {
|
||||
terminal.resize(size).unwrap();
|
||||
app.size = size;
|
||||
}
|
||||
|
||||
let evt = c.unwrap();
|
||||
match evt {
|
||||
event::Key::Char('q') => {
|
||||
break;
|
||||
}
|
||||
event::Key::Down => {
|
||||
app.selected += 1;
|
||||
if app.selected > app.items.len() - 1 {
|
||||
app.selected = 0;
|
||||
pub fn next(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.items.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
event::Key::Up => if app.selected > 0 {
|
||||
app.selected -= 1;
|
||||
} else {
|
||||
app.selected = app.items.len() - 1;
|
||||
},
|
||||
_ => {}
|
||||
None => 0,
|
||||
};
|
||||
draw(&mut terminal, &app);
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
terminal.show_cursor().unwrap();
|
||||
terminal.clear().unwrap();
|
||||
pub fn previous(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
self.items.len() - 1
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<MouseBackend>, app: &App) {
|
||||
Group::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.sizes(&[Size::Percent(100)])
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &mut app))?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Down => app.next(),
|
||||
KeyCode::Up => app.previous(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||
let rects = Layout::default()
|
||||
.constraints([Constraint::Percentage(100)].as_ref())
|
||||
.margin(5)
|
||||
.render(t, &app.size, |t, chunks| {
|
||||
let selected_style = Style::default().fg(Color::Yellow).modifier(Modifier::Bold);
|
||||
let normal_style = Style::default().fg(Color::White);
|
||||
Table::new(
|
||||
["Header1", "Header2", "Header3"].into_iter(),
|
||||
app.items.iter().enumerate().map(|(i, item)| {
|
||||
if i == app.selected {
|
||||
Row::StyledData(item.into_iter(), &selected_style)
|
||||
} else {
|
||||
Row::StyledData(item.into_iter(), &normal_style)
|
||||
}
|
||||
}),
|
||||
).block(Block::default().borders(Borders::ALL).title("Table"))
|
||||
.widths(&[10, 10, 10])
|
||||
.render(t, &chunks[0]);
|
||||
});
|
||||
.split(f.size());
|
||||
|
||||
t.draw().unwrap();
|
||||
let selected_style = Style::default().add_modifier(Modifier::REVERSED);
|
||||
let normal_style = Style::default().bg(Color::Blue);
|
||||
let header_cells = ["Header1", "Header2", "Header3"]
|
||||
.iter()
|
||||
.map(|h| Cell::from(*h).style(Style::default().fg(Color::Red)));
|
||||
let header = Row::new(header_cells)
|
||||
.style(normal_style)
|
||||
.height(1)
|
||||
.bottom_margin(1);
|
||||
let rows = app.items.iter().map(|item| {
|
||||
let height = item
|
||||
.iter()
|
||||
.map(|content| content.chars().filter(|c| *c == '\n').count())
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
+ 1;
|
||||
let cells = item.iter().map(|c| Cell::from(*c));
|
||||
Row::new(cells).height(height as u16).bottom_margin(1)
|
||||
});
|
||||
let t = Table::new(rows)
|
||||
.header(header)
|
||||
.block(Block::default().borders(Borders::ALL).title("Table"))
|
||||
.highlight_style(selected_style)
|
||||
.highlight_symbol(">> ")
|
||||
.widths(&[
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Length(30),
|
||||
Constraint::Min(10),
|
||||
]);
|
||||
f.render_stateful_widget(t, rects[0], &mut app.state);
|
||||
}
|
||||
|
||||
205
examples/tabs.rs
205
examples/tabs.rs
@@ -1,113 +1,124 @@
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
use std::{error::Error, io};
|
||||
|
||||
mod util;
|
||||
use util::*;
|
||||
|
||||
use std::io;
|
||||
use termion::event;
|
||||
use termion::input::TermRead;
|
||||
|
||||
use tui::Terminal;
|
||||
use tui::backend::MouseBackend;
|
||||
use tui::widgets::{Block, Borders, Tabs, Widget};
|
||||
use tui::layout::{Direction, Group, Rect, Size};
|
||||
use tui::style::{Color, Style};
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
text::Line,
|
||||
widgets::{Block, Borders, Tabs},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
struct App<'a> {
|
||||
size: Rect,
|
||||
tabs: MyTabs<'a>,
|
||||
pub titles: Vec<&'a str>,
|
||||
pub index: usize,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Terminal initialization
|
||||
let backend = MouseBackend::new().unwrap();
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
// App
|
||||
let mut app = App {
|
||||
size: Rect::default(),
|
||||
tabs: MyTabs {
|
||||
impl<'a> App<'a> {
|
||||
fn new() -> App<'a> {
|
||||
App {
|
||||
titles: vec!["Tab0", "Tab1", "Tab2", "Tab3"],
|
||||
selection: 0,
|
||||
},
|
||||
};
|
||||
|
||||
// First draw call
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
app.size = terminal.size().unwrap();
|
||||
draw(&mut terminal, &mut app);
|
||||
|
||||
// Main loop
|
||||
let stdin = io::stdin();
|
||||
for c in stdin.keys() {
|
||||
let size = terminal.size().unwrap();
|
||||
if size != app.size {
|
||||
terminal.resize(size).unwrap();
|
||||
app.size = size;
|
||||
index: 0,
|
||||
}
|
||||
|
||||
let evt = c.unwrap();
|
||||
match evt {
|
||||
event::Key::Char('q') => {
|
||||
break;
|
||||
}
|
||||
event::Key::Right => app.tabs.next(),
|
||||
event::Key::Left => app.tabs.previous(),
|
||||
_ => {}
|
||||
}
|
||||
draw(&mut terminal, &mut app);
|
||||
}
|
||||
|
||||
terminal.show_cursor().unwrap();
|
||||
pub fn next(&mut self) {
|
||||
self.index = (self.index + 1) % self.titles.len();
|
||||
}
|
||||
|
||||
pub fn previous(&mut self) {
|
||||
if self.index > 0 {
|
||||
self.index -= 1;
|
||||
} else {
|
||||
self.index = self.titles.len() - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<MouseBackend>, app: &mut App) {
|
||||
Block::default()
|
||||
.style(Style::default().bg(Color::White))
|
||||
.render(t, &app.size);
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
Group::default()
|
||||
// create app and run it
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Right => app.next(),
|
||||
KeyCode::Left => app.previous(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let size = f.size();
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(5)
|
||||
.sizes(&[Size::Fixed(3), Size::Min(0)])
|
||||
.render(t, &app.size, |t, chunks| {
|
||||
Tabs::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("Tabs"))
|
||||
.titles(&app.tabs.titles)
|
||||
.select(app.tabs.selection)
|
||||
.style(Style::default().fg(Color::Cyan))
|
||||
.highlight_style(Style::default().fg(Color::Yellow))
|
||||
.render(t, &chunks[0]);
|
||||
match app.tabs.selection {
|
||||
0 => {
|
||||
Block::default()
|
||||
.title("Inner 0")
|
||||
.borders(Borders::ALL)
|
||||
.render(t, &chunks[1]);
|
||||
}
|
||||
1 => {
|
||||
Block::default()
|
||||
.title("Inner 1")
|
||||
.borders(Borders::ALL)
|
||||
.render(t, &chunks[1]);
|
||||
}
|
||||
2 => {
|
||||
Block::default()
|
||||
.title("Inner 2")
|
||||
.borders(Borders::ALL)
|
||||
.render(t, &chunks[1]);
|
||||
}
|
||||
3 => {
|
||||
Block::default()
|
||||
.title("Inner 3")
|
||||
.borders(Borders::ALL)
|
||||
.render(t, &chunks[1]);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
|
||||
.split(size);
|
||||
|
||||
t.draw().unwrap();
|
||||
let block = Block::default().on_white().black();
|
||||
f.render_widget(block, size);
|
||||
let titles = app
|
||||
.titles
|
||||
.iter()
|
||||
.map(|t| {
|
||||
let (first, rest) = t.split_at(1);
|
||||
Line::from(vec![first.yellow(), rest.green()])
|
||||
})
|
||||
.collect();
|
||||
let tabs = Tabs::new(titles)
|
||||
.block(Block::default().borders(Borders::ALL).title("Tabs"))
|
||||
.select(app.index)
|
||||
.style(Style::default().fg(Color::Cyan))
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Black),
|
||||
);
|
||||
f.render_widget(tabs, chunks[0]);
|
||||
let inner = match app.index {
|
||||
0 => Block::default().title("Inner 0").borders(Borders::ALL),
|
||||
1 => Block::default().title("Inner 1").borders(Borders::ALL),
|
||||
2 => Block::default().title("Inner 2").borders(Borders::ALL),
|
||||
3 => Block::default().title("Inner 3").borders(Borders::ALL),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
f.render_widget(inner, chunks[1]);
|
||||
}
|
||||
|
||||
195
examples/user_input.rs
Normal file
195
examples/user_input.rs
Normal file
@@ -0,0 +1,195 @@
|
||||
use std::{error::Error, io};
|
||||
|
||||
/// A simple example demonstrating how to handle user input. This is
|
||||
/// a bit out of the scope of the library as it does not provide any
|
||||
/// input handling out of the box. However, it may helps some to get
|
||||
/// started.
|
||||
///
|
||||
/// This is a very simple example:
|
||||
/// * A input box always focused. Every character you type is registered
|
||||
/// here
|
||||
/// * Pressing Backspace erases a character
|
||||
/// * Pressing Enter pushes the current input in the history of previous
|
||||
/// messages
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
text::{Line, Span, Text},
|
||||
widgets::{Block, Borders, List, ListItem, Paragraph},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
enum InputMode {
|
||||
Normal,
|
||||
Editing,
|
||||
}
|
||||
|
||||
/// App holds the state of the application
|
||||
struct App {
|
||||
/// Current value of the input box
|
||||
input: String,
|
||||
/// Current input mode
|
||||
input_mode: InputMode,
|
||||
/// History of recorded messages
|
||||
messages: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for App {
|
||||
fn default() -> App {
|
||||
App {
|
||||
input: String::new(),
|
||||
input_mode: InputMode::Normal,
|
||||
messages: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let app = App::default();
|
||||
let res = run_app(&mut terminal, app);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match app.input_mode {
|
||||
InputMode::Normal => match key.code {
|
||||
KeyCode::Char('e') => {
|
||||
app.input_mode = InputMode::Editing;
|
||||
}
|
||||
KeyCode::Char('q') => {
|
||||
return Ok(());
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
InputMode::Editing if key.kind == KeyEventKind::Press => match key.code {
|
||||
KeyCode::Enter => {
|
||||
app.messages.push(app.input.drain(..).collect());
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
app.input.push(c);
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
app.input.pop();
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
app.input_mode = InputMode::Normal;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(1),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
|
||||
let (msg, style) = match app.input_mode {
|
||||
InputMode::Normal => (
|
||||
vec![
|
||||
"Press ".into(),
|
||||
"q".bold(),
|
||||
" to exist, ".into(),
|
||||
"e".bold(),
|
||||
" to start editing.".bold(),
|
||||
],
|
||||
Style::default().add_modifier(Modifier::RAPID_BLINK),
|
||||
),
|
||||
InputMode::Editing => (
|
||||
vec![
|
||||
"Press ".into(),
|
||||
"Esc".bold(),
|
||||
" to stop editing, ".into(),
|
||||
"Enter".bold(),
|
||||
" to record the message".into(),
|
||||
],
|
||||
Style::default(),
|
||||
),
|
||||
};
|
||||
let mut text = Text::from(Line::from(msg));
|
||||
text.patch_style(style);
|
||||
let help_message = Paragraph::new(text);
|
||||
f.render_widget(help_message, chunks[0]);
|
||||
|
||||
let input = Paragraph::new(app.input.as_str())
|
||||
.style(match app.input_mode {
|
||||
InputMode::Normal => Style::default(),
|
||||
InputMode::Editing => Style::default().fg(Color::Yellow),
|
||||
})
|
||||
.block(Block::default().borders(Borders::ALL).title("Input"));
|
||||
f.render_widget(input, chunks[1]);
|
||||
match app.input_mode {
|
||||
InputMode::Normal =>
|
||||
// Hide the cursor. `Frame` does this by default, so we don't need to do anything here
|
||||
{}
|
||||
|
||||
InputMode::Editing => {
|
||||
// Make the cursor visible and ask ratatui to put it at the specified coordinates after
|
||||
// rendering
|
||||
f.set_cursor(
|
||||
// Put cursor past the end of the input text
|
||||
chunks[1].x + app.input.width() as u16 + 1,
|
||||
// Move one line down, from the border to the input line
|
||||
chunks[1].y + 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let messages: Vec<ListItem> = app
|
||||
.messages
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, m)| {
|
||||
let content = Line::from(Span::raw(format!("{i}: {m}")));
|
||||
ListItem::new(content)
|
||||
})
|
||||
.collect();
|
||||
let messages =
|
||||
List::new(messages).block(Block::default().borders(Borders::ALL).title("Messages"));
|
||||
f.render_widget(messages, chunks[2]);
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
extern crate rand;
|
||||
|
||||
use self::rand::distributions::{IndependentSample, Range};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RandomSignal {
|
||||
range: Range<u64>,
|
||||
rng: rand::ThreadRng,
|
||||
}
|
||||
|
||||
impl RandomSignal {
|
||||
pub fn new(lower: u64, upper: u64) -> RandomSignal {
|
||||
RandomSignal {
|
||||
range: Range::new(lower, upper),
|
||||
rng: rand::thread_rng(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for RandomSignal {
|
||||
type Item = u64;
|
||||
fn next(&mut self) -> Option<u64> {
|
||||
Some(self.range.ind_sample(&mut self.rng))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SinSignal {
|
||||
x: f64,
|
||||
interval: f64,
|
||||
period: f64,
|
||||
scale: f64,
|
||||
}
|
||||
|
||||
impl SinSignal {
|
||||
pub fn new(interval: f64, period: f64, scale: f64) -> SinSignal {
|
||||
SinSignal {
|
||||
x: 0.0,
|
||||
interval: interval,
|
||||
period: period,
|
||||
scale: scale,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for SinSignal {
|
||||
type Item = (f64, f64);
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let point = (self.x, (self.x * 1.0 / self.period).sin() * self.scale);
|
||||
self.x += self.interval;
|
||||
Some(point)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MyTabs<'a> {
|
||||
pub titles: Vec<&'a str>,
|
||||
pub selection: usize,
|
||||
}
|
||||
|
||||
impl<'a> MyTabs<'a> {
|
||||
pub fn next(&mut self) {
|
||||
self.selection = (self.selection + 1) % self.titles.len();
|
||||
}
|
||||
|
||||
pub fn previous(&mut self) {
|
||||
if self.selection > 0 {
|
||||
self.selection -= 1;
|
||||
} else {
|
||||
self.selection = self.titles.len() - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
5
rustfmt.toml
Normal file
5
rustfmt.toml
Normal file
@@ -0,0 +1,5 @@
|
||||
# configuration for https://rust-lang.github.io/rustfmt/
|
||||
group_imports = "StdExternalCrate"
|
||||
imports_granularity = "Crate"
|
||||
wrap_comments = true
|
||||
comment_width = 100
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Build all examples in examples directory
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
for file in examples/*.rs; do
|
||||
name=$(basename ${file//.rs/})
|
||||
echo "[EXAMPLE] $name"
|
||||
if [[ "$name" == "rustbox" ]]; then
|
||||
cargo build --features rustbox --example "$name"
|
||||
else
|
||||
cargo build --example "$name"
|
||||
fi
|
||||
done
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Run all examples in examples directory
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
for file in examples/*.rs; do
|
||||
name=$(basename ${file//.rs/})
|
||||
if [[ "$name" == "rustbox" ]]; then
|
||||
cargo run --features rustbox --example "$name"
|
||||
else
|
||||
cargo run --example "$name"
|
||||
fi
|
||||
done
|
||||
@@ -1,65 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
USAGE="$0 --name=STRING [--channel=STRING]"
|
||||
|
||||
# Default values
|
||||
CARGO="cargo"
|
||||
NAME=""
|
||||
CHANNEL=""
|
||||
|
||||
# Parse args
|
||||
for i in "$@"; do
|
||||
case $i in
|
||||
--name=*)
|
||||
NAME="${i#*=}"
|
||||
shift
|
||||
;;
|
||||
--channel=*)
|
||||
CHANNEL="${i#*=}"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "$USAGE"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
current_version() {
|
||||
local crate="$1"
|
||||
$CARGO install --list | \
|
||||
grep "$crate" | \
|
||||
head -n 1 | \
|
||||
cut -d ' ' -f 2 | \
|
||||
sed 's/v\(.*\):/\1/g'
|
||||
}
|
||||
|
||||
upstream_version() {
|
||||
local crate="$1"
|
||||
$CARGO search "$crate" | \
|
||||
grep "$crate" | \
|
||||
head -n 1 | \
|
||||
cut -d' ' -f 3 | \
|
||||
sed 's/"//g'
|
||||
}
|
||||
|
||||
if [ "$NAME" == "" ]; then
|
||||
echo "$USAGE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$CHANNEL" != "" ]; then
|
||||
CARGO+=" +$CHANNEL"
|
||||
fi
|
||||
|
||||
CURRENT_VERSION=$(current_version "$NAME")
|
||||
UPSTREAM_VERSION=$(upstream_version "$NAME")
|
||||
|
||||
if [[ "$CURRENT_VERSION" != "$UPSTREAM_VERSION" ]]; then
|
||||
echo "WARN: Latest version of $NAME not installed: $CURRENT_VERSION -> $UPSTREAM_VERSION"
|
||||
$CARGO install --force "$NAME"
|
||||
else
|
||||
echo "INFO: Latest version of $NAME already installed ($CURRENT_VERSION)"
|
||||
fi
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu
|
||||
|
||||
export PATH="$PATH:$HOME/.cargo/bin"
|
||||
if [[ "$TRAVIS_RUST_VERSION" == "nightly" ]]; then
|
||||
export LD_LIBRARY_PATH="$(rustc +nightly --print sysroot)/lib:${LD_LIBRARY_PATH:-""}"
|
||||
make install-tools
|
||||
fi
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu
|
||||
|
||||
make build
|
||||
make test
|
||||
if [[ "$TRAVIS_RUST_VERSION" == "nightly" ]]; then
|
||||
make lint
|
||||
fi
|
||||
274
src/backend/crossterm.rs
Normal file
274
src/backend/crossterm.rs
Normal file
@@ -0,0 +1,274 @@
|
||||
//! This module provides the `CrosstermBackend` implementation for the `Backend` trait.
|
||||
//! It uses the `crossterm` crate to interact with the terminal.
|
||||
//!
|
||||
//!
|
||||
//! [`Backend`]: trait.Backend.html
|
||||
//! [`CrosstermBackend`]: struct.CrosstermBackend.html
|
||||
|
||||
use std::io::{self, Write};
|
||||
|
||||
use crossterm::{
|
||||
cursor::{Hide, MoveTo, Show},
|
||||
execute, queue,
|
||||
style::{
|
||||
Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor,
|
||||
SetForegroundColor,
|
||||
},
|
||||
terminal::{self, Clear},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
backend::{Backend, ClearType},
|
||||
buffer::Cell,
|
||||
layout::Rect,
|
||||
style::{Color, Modifier},
|
||||
};
|
||||
|
||||
/// A backend implementation using the `crossterm` crate.
|
||||
///
|
||||
/// The `CrosstermBackend` struct is a wrapper around a type implementing `Write`, which
|
||||
/// is used to send commands to the terminal. It provides methods for drawing content,
|
||||
/// manipulating the cursor, and clearing the terminal screen.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::backend::{Backend, CrosstermBackend};
|
||||
///
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let buffer = std::io::stdout();
|
||||
/// let mut backend = CrosstermBackend::new(buffer);
|
||||
/// backend.clear()?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub struct CrosstermBackend<W: Write> {
|
||||
buffer: W,
|
||||
}
|
||||
|
||||
impl<W> CrosstermBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
/// Creates a new `CrosstermBackend` with the given buffer.
|
||||
pub fn new(buffer: W) -> CrosstermBackend<W> {
|
||||
CrosstermBackend { buffer }
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> Write for CrosstermBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
/// Writes a buffer of bytes to the underlying buffer.
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.buffer.write(buf)
|
||||
}
|
||||
|
||||
/// Flushes the underlying buffer.
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.buffer.flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> Backend for CrosstermBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
let mut fg = Color::Reset;
|
||||
let mut bg = Color::Reset;
|
||||
let mut modifier = Modifier::empty();
|
||||
let mut last_pos: Option<(u16, u16)> = None;
|
||||
for (x, y, cell) in content {
|
||||
// Move the cursor if the previous location was not (x - 1, y)
|
||||
if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) {
|
||||
map_error(queue!(self.buffer, MoveTo(x, y)))?;
|
||||
}
|
||||
last_pos = Some((x, y));
|
||||
if cell.modifier != modifier {
|
||||
let diff = ModifierDiff {
|
||||
from: modifier,
|
||||
to: cell.modifier,
|
||||
};
|
||||
diff.queue(&mut self.buffer)?;
|
||||
modifier = cell.modifier;
|
||||
}
|
||||
if cell.fg != fg {
|
||||
let color = CColor::from(cell.fg);
|
||||
map_error(queue!(self.buffer, SetForegroundColor(color)))?;
|
||||
fg = cell.fg;
|
||||
}
|
||||
if cell.bg != bg {
|
||||
let color = CColor::from(cell.bg);
|
||||
map_error(queue!(self.buffer, SetBackgroundColor(color)))?;
|
||||
bg = cell.bg;
|
||||
}
|
||||
|
||||
map_error(queue!(self.buffer, Print(&cell.symbol)))?;
|
||||
}
|
||||
|
||||
map_error(queue!(
|
||||
self.buffer,
|
||||
SetForegroundColor(CColor::Reset),
|
||||
SetBackgroundColor(CColor::Reset),
|
||||
SetAttribute(CAttribute::Reset)
|
||||
))
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
map_error(execute!(self.buffer, Hide))
|
||||
}
|
||||
|
||||
fn show_cursor(&mut self) -> io::Result<()> {
|
||||
map_error(execute!(self.buffer, Show))
|
||||
}
|
||||
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
crossterm::cursor::position()
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
|
||||
}
|
||||
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
map_error(execute!(self.buffer, MoveTo(x, y)))
|
||||
}
|
||||
|
||||
fn clear(&mut self) -> io::Result<()> {
|
||||
self.clear_region(ClearType::All)
|
||||
}
|
||||
|
||||
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
|
||||
map_error(execute!(
|
||||
self.buffer,
|
||||
Clear(match clear_type {
|
||||
ClearType::All => crossterm::terminal::ClearType::All,
|
||||
ClearType::AfterCursor => crossterm::terminal::ClearType::FromCursorDown,
|
||||
ClearType::BeforeCursor => crossterm::terminal::ClearType::FromCursorUp,
|
||||
ClearType::CurrentLine => crossterm::terminal::ClearType::CurrentLine,
|
||||
ClearType::UntilNewLine => crossterm::terminal::ClearType::UntilNewLine,
|
||||
})
|
||||
))
|
||||
}
|
||||
|
||||
fn append_lines(&mut self, n: u16) -> io::Result<()> {
|
||||
for _ in 0..n {
|
||||
map_error(queue!(self.buffer, Print("\n")))?;
|
||||
}
|
||||
self.buffer.flush()
|
||||
}
|
||||
|
||||
fn size(&self) -> io::Result<Rect> {
|
||||
let (width, height) =
|
||||
terminal::size().map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
|
||||
|
||||
Ok(Rect::new(0, 0, width, height))
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.buffer.flush()
|
||||
}
|
||||
}
|
||||
|
||||
fn map_error(error: crossterm::Result<()>) -> io::Result<()> {
|
||||
error.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
|
||||
}
|
||||
|
||||
impl From<Color> for CColor {
|
||||
fn from(color: Color) -> Self {
|
||||
match color {
|
||||
Color::Reset => CColor::Reset,
|
||||
Color::Black => CColor::Black,
|
||||
Color::Red => CColor::DarkRed,
|
||||
Color::Green => CColor::DarkGreen,
|
||||
Color::Yellow => CColor::DarkYellow,
|
||||
Color::Blue => CColor::DarkBlue,
|
||||
Color::Magenta => CColor::DarkMagenta,
|
||||
Color::Cyan => CColor::DarkCyan,
|
||||
Color::Gray => CColor::Grey,
|
||||
Color::DarkGray => CColor::DarkGrey,
|
||||
Color::LightRed => CColor::Red,
|
||||
Color::LightGreen => CColor::Green,
|
||||
Color::LightBlue => CColor::Blue,
|
||||
Color::LightYellow => CColor::Yellow,
|
||||
Color::LightMagenta => CColor::Magenta,
|
||||
Color::LightCyan => CColor::Cyan,
|
||||
Color::White => CColor::White,
|
||||
Color::Indexed(i) => CColor::AnsiValue(i),
|
||||
Color::Rgb(r, g, b) => CColor::Rgb { r, g, b },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The `ModifierDiff` struct is used to calculate the difference between two `Modifier`
|
||||
/// values. This is useful when updating the terminal display, as it allows for more
|
||||
/// efficient updates by only sending the necessary changes.
|
||||
#[derive(Debug)]
|
||||
struct ModifierDiff {
|
||||
pub from: Modifier,
|
||||
pub to: Modifier,
|
||||
}
|
||||
|
||||
impl ModifierDiff {
|
||||
fn queue<W>(&self, mut w: W) -> io::Result<()>
|
||||
where
|
||||
W: io::Write,
|
||||
{
|
||||
//use crossterm::Attribute;
|
||||
let removed = self.from - self.to;
|
||||
if removed.contains(Modifier::REVERSED) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NoReverse)))?;
|
||||
}
|
||||
if removed.contains(Modifier::BOLD) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
|
||||
if self.to.contains(Modifier::DIM) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
|
||||
}
|
||||
}
|
||||
if removed.contains(Modifier::ITALIC) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NoItalic)))?;
|
||||
}
|
||||
if removed.contains(Modifier::UNDERLINED) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NoUnderline)))?;
|
||||
}
|
||||
if removed.contains(Modifier::DIM) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
|
||||
}
|
||||
if removed.contains(Modifier::CROSSED_OUT) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NotCrossedOut)))?;
|
||||
}
|
||||
if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NoBlink)))?;
|
||||
}
|
||||
|
||||
let added = self.to - self.from;
|
||||
if added.contains(Modifier::REVERSED) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::Reverse)))?;
|
||||
}
|
||||
if added.contains(Modifier::BOLD) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::Bold)))?;
|
||||
}
|
||||
if added.contains(Modifier::ITALIC) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::Italic)))?;
|
||||
}
|
||||
if added.contains(Modifier::UNDERLINED) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::Underlined)))?;
|
||||
}
|
||||
if added.contains(Modifier::DIM) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
|
||||
}
|
||||
if added.contains(Modifier::CROSSED_OUT) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::CrossedOut)))?;
|
||||
}
|
||||
if added.contains(Modifier::SLOW_BLINK) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::SlowBlink)))?;
|
||||
}
|
||||
if added.contains(Modifier::RAPID_BLINK) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::RapidBlink)))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,117 @@
|
||||
//! This module provides the backend implementations for different terminal libraries.
|
||||
//! It defines the [`Backend`] trait which is used to abstract over the specific
|
||||
//! terminal library being used.
|
||||
//!
|
||||
//! The following terminal libraries are supported:
|
||||
//! - Crossterm (with the `crossterm` feature)
|
||||
//! - Termion (with the `termion` feature)
|
||||
//! - Termwiz (with the `termwiz` feature)
|
||||
//!
|
||||
//! Additionally, a [`TestBackend`] is provided for testing purposes.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust
|
||||
//! use ratatui::backend::{Backend, CrosstermBackend};
|
||||
//!
|
||||
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! let buffer = std::io::stdout();
|
||||
//! let mut backend = CrosstermBackend::new(buffer);
|
||||
//! backend.clear()?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! [`Backend`]: trait.Backend.html
|
||||
//! [`TestBackend`]: struct.TestBackend.html
|
||||
|
||||
use std::io;
|
||||
|
||||
use buffer::Cell;
|
||||
use layout::Rect;
|
||||
|
||||
#[cfg(feature = "rustbox")]
|
||||
mod rustbox;
|
||||
#[cfg(feature = "rustbox")]
|
||||
pub use self::rustbox::RustboxBackend;
|
||||
use crate::{buffer::Cell, layout::Rect};
|
||||
|
||||
#[cfg(feature = "termion")]
|
||||
mod termion;
|
||||
#[cfg(feature = "termion")]
|
||||
pub use self::termion::{MouseBackend, RawBackend, TermionBackend};
|
||||
pub use self::termion::TermionBackend;
|
||||
|
||||
#[cfg(feature = "crossterm")]
|
||||
mod crossterm;
|
||||
#[cfg(feature = "crossterm")]
|
||||
pub use self::crossterm::CrosstermBackend;
|
||||
|
||||
#[cfg(feature = "termwiz")]
|
||||
mod termwiz;
|
||||
#[cfg(feature = "termwiz")]
|
||||
pub use self::termwiz::TermwizBackend;
|
||||
|
||||
mod test;
|
||||
pub use self::test::TestBackend;
|
||||
|
||||
/// Enum representing the different types of clearing operations that can be performed
|
||||
/// on the terminal screen.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ClearType {
|
||||
All,
|
||||
AfterCursor,
|
||||
BeforeCursor,
|
||||
CurrentLine,
|
||||
UntilNewLine,
|
||||
}
|
||||
|
||||
/// The `Backend` trait provides an abstraction over different terminal libraries.
|
||||
/// It defines the methods required to draw content, manipulate the cursor, and
|
||||
/// clear the terminal screen.
|
||||
pub trait Backend {
|
||||
/// Draw the given content to the terminal screen.
|
||||
///
|
||||
/// The content is provided as an iterator over `(u16, u16, &Cell)` tuples,
|
||||
/// where the first two elements represent the x and y coordinates, and the
|
||||
/// third element is a reference to the [`Cell`] to be drawn.
|
||||
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>;
|
||||
|
||||
/// Insert `n` line breaks to the terminal screen.
|
||||
///
|
||||
/// This method is optional and may not be implemented by all backends.
|
||||
fn append_lines(&mut self, _n: u16) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Hide the cursor on the terminal screen.
|
||||
fn hide_cursor(&mut self) -> Result<(), io::Error>;
|
||||
|
||||
/// Show the cursor on the terminal screen.
|
||||
fn show_cursor(&mut self) -> Result<(), io::Error>;
|
||||
|
||||
/// Get the current cursor position on the terminal screen.
|
||||
fn get_cursor(&mut self) -> Result<(u16, u16), io::Error>;
|
||||
|
||||
/// Set the cursor position on the terminal screen to the given x and y coordinates.
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), io::Error>;
|
||||
|
||||
/// Clears the whole terminal screen
|
||||
fn clear(&mut self) -> Result<(), io::Error>;
|
||||
|
||||
/// Clears a specific region of the terminal specified by the [`ClearType`] parameter
|
||||
///
|
||||
/// This method is optional and may not be implemented by all backends.
|
||||
fn clear_region(&mut self, clear_type: ClearType) -> Result<(), io::Error> {
|
||||
match clear_type {
|
||||
ClearType::All => self.clear(),
|
||||
ClearType::AfterCursor
|
||||
| ClearType::BeforeCursor
|
||||
| ClearType::CurrentLine
|
||||
| ClearType::UntilNewLine => Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("clear_type [{clear_type:?}] not supported with this backend"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the size of the terminal screen as a [`Rect`].
|
||||
fn size(&self) -> Result<Rect, io::Error>;
|
||||
|
||||
/// Flush any buffered content to the terminal screen.
|
||||
fn flush(&mut self) -> Result<(), io::Error>;
|
||||
}
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
extern crate rustbox;
|
||||
|
||||
use std::io;
|
||||
|
||||
use super::Backend;
|
||||
use buffer::Cell;
|
||||
use layout::Rect;
|
||||
use style::{Color, Modifier};
|
||||
|
||||
pub struct RustboxBackend {
|
||||
rustbox: rustbox::RustBox,
|
||||
}
|
||||
|
||||
impl RustboxBackend {
|
||||
pub fn new() -> Result<RustboxBackend, rustbox::InitError> {
|
||||
let rustbox = try!(rustbox::RustBox::init(Default::default()));
|
||||
Ok(RustboxBackend { rustbox: rustbox })
|
||||
}
|
||||
|
||||
pub fn with_rustbox(instance: rustbox::RustBox) -> RustboxBackend {
|
||||
RustboxBackend { rustbox: instance }
|
||||
}
|
||||
|
||||
pub fn rustbox(&self) -> &rustbox::RustBox {
|
||||
&self.rustbox
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend for RustboxBackend {
|
||||
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
let mut inst = 0;
|
||||
for (x, y, cell) in content {
|
||||
inst += 1;
|
||||
self.rustbox.print(
|
||||
x as usize,
|
||||
y as usize,
|
||||
cell.style.modifier.into(),
|
||||
cell.style.fg.into(),
|
||||
cell.style.bg.into(),
|
||||
&cell.symbol,
|
||||
);
|
||||
}
|
||||
debug!("{} instructions outputed", inst);
|
||||
Ok(())
|
||||
}
|
||||
fn hide_cursor(&mut self) -> Result<(), io::Error> {
|
||||
Ok(())
|
||||
}
|
||||
fn show_cursor(&mut self) -> Result<(), io::Error> {
|
||||
Ok(())
|
||||
}
|
||||
fn clear(&mut self) -> Result<(), io::Error> {
|
||||
self.rustbox.clear();
|
||||
Ok(())
|
||||
}
|
||||
fn size(&self) -> Result<Rect, io::Error> {
|
||||
Ok(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: self.rustbox.width() as u16,
|
||||
height: self.rustbox.height() as u16,
|
||||
})
|
||||
}
|
||||
fn flush(&mut self) -> Result<(), io::Error> {
|
||||
self.rustbox.present();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn rgb_to_byte(r: u8, g: u8, b: u8) -> u16 {
|
||||
u16::from((r & 0xC0) + ((g & 0xE0) >> 2) + ((b & 0xE0) >> 5))
|
||||
}
|
||||
|
||||
impl Into<rustbox::Color> for Color {
|
||||
fn into(self) -> rustbox::Color {
|
||||
match self {
|
||||
Color::Reset => rustbox::Color::Default,
|
||||
Color::Black | Color::Gray | Color::DarkGray => rustbox::Color::Black,
|
||||
Color::Red | Color::LightRed => rustbox::Color::Red,
|
||||
Color::Green | Color::LightGreen => rustbox::Color::Green,
|
||||
Color::Yellow | Color::LightYellow => rustbox::Color::Yellow,
|
||||
Color::Magenta | Color::LightMagenta => rustbox::Color::Magenta,
|
||||
Color::Cyan | Color::LightCyan => rustbox::Color::Cyan,
|
||||
Color::White => rustbox::Color::White,
|
||||
Color::Rgb(r, g, b) => rustbox::Color::Byte(rgb_to_byte(r, g, b)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<rustbox::Style> for Modifier {
|
||||
fn into(self) -> rustbox::Style {
|
||||
match self {
|
||||
Modifier::Bold => rustbox::RB_BOLD,
|
||||
Modifier::Underline => rustbox::RB_UNDERLINE,
|
||||
Modifier::Invert => rustbox::RB_REVERSE,
|
||||
_ => rustbox::RB_NORMAL,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,36 @@
|
||||
extern crate termion;
|
||||
//! This module provides the `TermionBackend` implementation for the [`Backend`] trait.
|
||||
//! It uses the Termion crate to interact with the terminal.
|
||||
//!
|
||||
//! [`Backend`]: crate::backend::Backend
|
||||
//! [`TermionBackend`]: crate::backend::TermionBackend
|
||||
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
use std::{
|
||||
fmt,
|
||||
io::{self, Write},
|
||||
};
|
||||
|
||||
use self::termion::raw::IntoRawMode;
|
||||
|
||||
use super::Backend;
|
||||
use buffer::Cell;
|
||||
use layout::Rect;
|
||||
use style::{Color, Modifier, Style};
|
||||
use crate::{
|
||||
backend::{Backend, ClearType},
|
||||
buffer::Cell,
|
||||
layout::Rect,
|
||||
style::{Color, Modifier},
|
||||
};
|
||||
|
||||
/// A backend that uses the Termion library to draw content, manipulate the cursor,
|
||||
/// and clear the terminal screen.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::backend::{Backend, TermionBackend};
|
||||
///
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let stdout = std::io::stdout();
|
||||
/// let mut backend = TermionBackend::new(stdout);
|
||||
/// backend.clear()?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub struct TermionBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
@@ -17,22 +38,26 @@ where
|
||||
stdout: W,
|
||||
}
|
||||
|
||||
pub type RawBackend = TermionBackend<termion::raw::RawTerminal<io::Stdout>>;
|
||||
pub type MouseBackend =
|
||||
TermionBackend<termion::input::MouseTerminal<termion::raw::RawTerminal<io::Stdout>>>;
|
||||
|
||||
impl RawBackend {
|
||||
pub fn new() -> Result<RawBackend, io::Error> {
|
||||
let raw = io::stdout().into_raw_mode()?;
|
||||
Ok(TermionBackend { stdout: raw })
|
||||
impl<W> TermionBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
/// Creates a new Termion backend with the given output.
|
||||
pub fn new(stdout: W) -> TermionBackend<W> {
|
||||
TermionBackend { stdout }
|
||||
}
|
||||
}
|
||||
|
||||
impl MouseBackend {
|
||||
pub fn new() -> Result<MouseBackend, io::Error> {
|
||||
let raw = io::stdout().into_raw_mode()?;
|
||||
let mouse = termion::input::MouseTerminal::from(raw);
|
||||
Ok(TermionBackend { stdout: mouse })
|
||||
impl<W> Write for TermionBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.stdout.write(buf)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.stdout.flush()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,187 +65,231 @@ impl<W> Backend for TermionBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
/// Clears the entire screen and move the cursor to the top left of the screen
|
||||
fn clear(&mut self) -> Result<(), io::Error> {
|
||||
write!(self.stdout, "{}", termion::clear::All)?;
|
||||
write!(self.stdout, "{}", termion::cursor::Goto(1, 1))?;
|
||||
self.stdout.flush()?;
|
||||
Ok(())
|
||||
fn clear(&mut self) -> io::Result<()> {
|
||||
self.clear_region(ClearType::All)
|
||||
}
|
||||
|
||||
/// Hides cursor
|
||||
fn hide_cursor(&mut self) -> Result<(), io::Error> {
|
||||
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
|
||||
match clear_type {
|
||||
ClearType::All => write!(self.stdout, "{}", termion::clear::All)?,
|
||||
ClearType::AfterCursor => write!(self.stdout, "{}", termion::clear::AfterCursor)?,
|
||||
ClearType::BeforeCursor => write!(self.stdout, "{}", termion::clear::BeforeCursor)?,
|
||||
ClearType::CurrentLine => write!(self.stdout, "{}", termion::clear::CurrentLine)?,
|
||||
ClearType::UntilNewLine => write!(self.stdout, "{}", termion::clear::UntilNewline)?,
|
||||
};
|
||||
self.stdout.flush()
|
||||
}
|
||||
|
||||
fn append_lines(&mut self, n: u16) -> io::Result<()> {
|
||||
for _ in 0..n {
|
||||
writeln!(self.stdout)?;
|
||||
}
|
||||
self.stdout.flush()
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
write!(self.stdout, "{}", termion::cursor::Hide)?;
|
||||
self.stdout.flush()?;
|
||||
Ok(())
|
||||
self.stdout.flush()
|
||||
}
|
||||
|
||||
/// Shows cursor
|
||||
fn show_cursor(&mut self) -> Result<(), io::Error> {
|
||||
fn show_cursor(&mut self) -> io::Result<()> {
|
||||
write!(self.stdout, "{}", termion::cursor::Show)?;
|
||||
self.stdout.flush()?;
|
||||
Ok(())
|
||||
self.stdout.flush()
|
||||
}
|
||||
|
||||
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
termion::cursor::DetectCursorPos::cursor_pos(&mut self.stdout).map(|(x, y)| (x - 1, y - 1))
|
||||
}
|
||||
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
write!(self.stdout, "{}", termion::cursor::Goto(x + 1, y + 1))?;
|
||||
self.stdout.flush()
|
||||
}
|
||||
|
||||
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
use std::fmt::Write;
|
||||
|
||||
let mut string = String::with_capacity(content.size_hint().0 * 3);
|
||||
let mut style = Style::default();
|
||||
let mut last_y = 0;
|
||||
let mut last_x = 0;
|
||||
let mut inst = 0;
|
||||
let mut fg = Color::Reset;
|
||||
let mut bg = Color::Reset;
|
||||
let mut modifier = Modifier::empty();
|
||||
let mut last_pos: Option<(u16, u16)> = None;
|
||||
for (x, y, cell) in content {
|
||||
if y != last_y || x != last_x + 1 {
|
||||
string.push_str(&format!("{}", termion::cursor::Goto(x + 1, y + 1)));
|
||||
inst += 1;
|
||||
// Move the cursor if the previous location was not (x - 1, y)
|
||||
if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) {
|
||||
write!(string, "{}", termion::cursor::Goto(x + 1, y + 1)).unwrap();
|
||||
}
|
||||
last_x = x;
|
||||
last_y = y;
|
||||
if cell.style.modifier != style.modifier {
|
||||
string.push_str(&cell.style.modifier.termion_modifier());
|
||||
style.modifier = cell.style.modifier;
|
||||
if style.modifier == Modifier::Reset {
|
||||
style.bg = Color::Reset;
|
||||
style.fg = Color::Reset;
|
||||
}
|
||||
inst += 1;
|
||||
last_pos = Some((x, y));
|
||||
if cell.modifier != modifier {
|
||||
write!(
|
||||
string,
|
||||
"{}",
|
||||
ModifierDiff {
|
||||
from: modifier,
|
||||
to: cell.modifier
|
||||
}
|
||||
)
|
||||
.unwrap();
|
||||
modifier = cell.modifier;
|
||||
}
|
||||
if cell.style.fg != style.fg {
|
||||
string.push_str(&cell.style.fg.termion_fg());
|
||||
style.fg = cell.style.fg;
|
||||
inst += 1;
|
||||
if cell.fg != fg {
|
||||
write!(string, "{}", Fg(cell.fg)).unwrap();
|
||||
fg = cell.fg;
|
||||
}
|
||||
if cell.style.bg != style.bg {
|
||||
string.push_str(&cell.style.bg.termion_bg());
|
||||
style.bg = cell.style.bg;
|
||||
inst += 1;
|
||||
if cell.bg != bg {
|
||||
write!(string, "{}", Bg(cell.bg)).unwrap();
|
||||
bg = cell.bg;
|
||||
}
|
||||
string.push_str(&cell.symbol);
|
||||
inst += 1;
|
||||
}
|
||||
debug!("{} instructions outputed.", inst);
|
||||
write!(
|
||||
self.stdout,
|
||||
"{}{}{}{}",
|
||||
string,
|
||||
Color::Reset.termion_fg(),
|
||||
Color::Reset.termion_bg(),
|
||||
Modifier::Reset.termion_modifier()
|
||||
)?;
|
||||
Ok(())
|
||||
"{string}{}{}{}",
|
||||
Fg(Color::Reset),
|
||||
Bg(Color::Reset),
|
||||
termion::style::Reset,
|
||||
)
|
||||
}
|
||||
|
||||
/// Return the size of the terminal
|
||||
fn size(&self) -> Result<Rect, io::Error> {
|
||||
let terminal = try!(termion::terminal_size());
|
||||
Ok(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: terminal.0,
|
||||
height: terminal.1,
|
||||
})
|
||||
fn size(&self) -> io::Result<Rect> {
|
||||
let terminal = termion::terminal_size()?;
|
||||
Ok(Rect::new(0, 0, terminal.0, terminal.1))
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<(), io::Error> {
|
||||
try!(self.stdout.flush());
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.stdout.flush()
|
||||
}
|
||||
}
|
||||
|
||||
struct Fg(Color);
|
||||
|
||||
struct Bg(Color);
|
||||
|
||||
/// The `ModifierDiff` struct is used to calculate the difference between two `Modifier`
|
||||
/// values. This is useful when updating the terminal display, as it allows for more
|
||||
/// efficient updates by only sending the necessary changes.
|
||||
struct ModifierDiff {
|
||||
from: Modifier,
|
||||
to: Modifier,
|
||||
}
|
||||
|
||||
impl fmt::Display for Fg {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
use termion::color::Color as TermionColor;
|
||||
match self.0 {
|
||||
Color::Reset => termion::color::Reset.write_fg(f),
|
||||
Color::Black => termion::color::Black.write_fg(f),
|
||||
Color::Red => termion::color::Red.write_fg(f),
|
||||
Color::Green => termion::color::Green.write_fg(f),
|
||||
Color::Yellow => termion::color::Yellow.write_fg(f),
|
||||
Color::Blue => termion::color::Blue.write_fg(f),
|
||||
Color::Magenta => termion::color::Magenta.write_fg(f),
|
||||
Color::Cyan => termion::color::Cyan.write_fg(f),
|
||||
Color::Gray => termion::color::White.write_fg(f),
|
||||
Color::DarkGray => termion::color::LightBlack.write_fg(f),
|
||||
Color::LightRed => termion::color::LightRed.write_fg(f),
|
||||
Color::LightGreen => termion::color::LightGreen.write_fg(f),
|
||||
Color::LightBlue => termion::color::LightBlue.write_fg(f),
|
||||
Color::LightYellow => termion::color::LightYellow.write_fg(f),
|
||||
Color::LightMagenta => termion::color::LightMagenta.write_fg(f),
|
||||
Color::LightCyan => termion::color::LightCyan.write_fg(f),
|
||||
Color::White => termion::color::LightWhite.write_fg(f),
|
||||
Color::Indexed(i) => termion::color::AnsiValue(i).write_fg(f),
|
||||
Color::Rgb(r, g, b) => termion::color::Rgb(r, g, b).write_fg(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl fmt::Display for Bg {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
use termion::color::Color as TermionColor;
|
||||
match self.0 {
|
||||
Color::Reset => termion::color::Reset.write_bg(f),
|
||||
Color::Black => termion::color::Black.write_bg(f),
|
||||
Color::Red => termion::color::Red.write_bg(f),
|
||||
Color::Green => termion::color::Green.write_bg(f),
|
||||
Color::Yellow => termion::color::Yellow.write_bg(f),
|
||||
Color::Blue => termion::color::Blue.write_bg(f),
|
||||
Color::Magenta => termion::color::Magenta.write_bg(f),
|
||||
Color::Cyan => termion::color::Cyan.write_bg(f),
|
||||
Color::Gray => termion::color::White.write_bg(f),
|
||||
Color::DarkGray => termion::color::LightBlack.write_bg(f),
|
||||
Color::LightRed => termion::color::LightRed.write_bg(f),
|
||||
Color::LightGreen => termion::color::LightGreen.write_bg(f),
|
||||
Color::LightBlue => termion::color::LightBlue.write_bg(f),
|
||||
Color::LightYellow => termion::color::LightYellow.write_bg(f),
|
||||
Color::LightMagenta => termion::color::LightMagenta.write_bg(f),
|
||||
Color::LightCyan => termion::color::LightCyan.write_bg(f),
|
||||
Color::White => termion::color::LightWhite.write_bg(f),
|
||||
Color::Indexed(i) => termion::color::AnsiValue(i).write_bg(f),
|
||||
Color::Rgb(r, g, b) => termion::color::Rgb(r, g, b).write_bg(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ModifierDiff {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let remove = self.from - self.to;
|
||||
if remove.contains(Modifier::REVERSED) {
|
||||
write!(f, "{}", termion::style::NoInvert)?;
|
||||
}
|
||||
if remove.contains(Modifier::BOLD) {
|
||||
// XXX: the termion NoBold flag actually enables double-underline on ECMA-48 compliant
|
||||
// terminals, and NoFaint additionally disables bold... so we use this trick to get
|
||||
// the right semantics.
|
||||
write!(f, "{}", termion::style::NoFaint)?;
|
||||
|
||||
if self.to.contains(Modifier::DIM) {
|
||||
write!(f, "{}", termion::style::Faint)?;
|
||||
}
|
||||
}
|
||||
if remove.contains(Modifier::ITALIC) {
|
||||
write!(f, "{}", termion::style::NoItalic)?;
|
||||
}
|
||||
if remove.contains(Modifier::UNDERLINED) {
|
||||
write!(f, "{}", termion::style::NoUnderline)?;
|
||||
}
|
||||
if remove.contains(Modifier::DIM) {
|
||||
write!(f, "{}", termion::style::NoFaint)?;
|
||||
|
||||
// XXX: the NoFaint flag additionally disables bold as well, so we need to re-enable it
|
||||
// here if we want it.
|
||||
if self.to.contains(Modifier::BOLD) {
|
||||
write!(f, "{}", termion::style::Bold)?;
|
||||
}
|
||||
}
|
||||
if remove.contains(Modifier::CROSSED_OUT) {
|
||||
write!(f, "{}", termion::style::NoCrossedOut)?;
|
||||
}
|
||||
if remove.contains(Modifier::SLOW_BLINK) || remove.contains(Modifier::RAPID_BLINK) {
|
||||
write!(f, "{}", termion::style::NoBlink)?;
|
||||
}
|
||||
|
||||
let add = self.to - self.from;
|
||||
if add.contains(Modifier::REVERSED) {
|
||||
write!(f, "{}", termion::style::Invert)?;
|
||||
}
|
||||
if add.contains(Modifier::BOLD) {
|
||||
write!(f, "{}", termion::style::Bold)?;
|
||||
}
|
||||
if add.contains(Modifier::ITALIC) {
|
||||
write!(f, "{}", termion::style::Italic)?;
|
||||
}
|
||||
if add.contains(Modifier::UNDERLINED) {
|
||||
write!(f, "{}", termion::style::Underline)?;
|
||||
}
|
||||
if add.contains(Modifier::DIM) {
|
||||
write!(f, "{}", termion::style::Faint)?;
|
||||
}
|
||||
if add.contains(Modifier::CROSSED_OUT) {
|
||||
write!(f, "{}", termion::style::CrossedOut)?;
|
||||
}
|
||||
if add.contains(Modifier::SLOW_BLINK) || add.contains(Modifier::RAPID_BLINK) {
|
||||
write!(f, "{}", termion::style::Blink)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! termion_fg {
|
||||
($color:ident) => (
|
||||
format!("{}", termion::color::Fg(termion::color::$color))
|
||||
);
|
||||
}
|
||||
|
||||
macro_rules! termion_fg_rgb {
|
||||
($r:expr, $g:expr, $b:expr) => (
|
||||
format!("{}", termion::color::Fg(termion::color::Rgb($r, $g, $b)))
|
||||
);
|
||||
}
|
||||
|
||||
macro_rules! termion_bg {
|
||||
($color:ident) => (
|
||||
format!("{}", termion::color::Bg(termion::color::$color))
|
||||
);
|
||||
}
|
||||
|
||||
macro_rules! termion_bg_rgb {
|
||||
($r:expr, $g:expr, $b:expr) => (
|
||||
format!("{}", termion::color::Bg(termion::color::Rgb($r, $g, $b)))
|
||||
);
|
||||
}
|
||||
|
||||
macro_rules! termion_modifier {
|
||||
($style:ident) => (
|
||||
format!("{}", termion::style::$style)
|
||||
);
|
||||
}
|
||||
|
||||
impl Color {
|
||||
pub fn termion_fg(&self) -> String {
|
||||
match *self {
|
||||
Color::Reset => termion_fg!(Reset),
|
||||
Color::Black => termion_fg!(Black),
|
||||
Color::Red => termion_fg!(Red),
|
||||
Color::Green => termion_fg!(Green),
|
||||
Color::Yellow => termion_fg!(Yellow),
|
||||
Color::Magenta => termion_fg!(Magenta),
|
||||
Color::Cyan => termion_fg!(Cyan),
|
||||
Color::Gray => termion_fg_rgb!(146, 131, 116),
|
||||
Color::DarkGray => termion_fg_rgb!(80, 73, 69),
|
||||
Color::LightRed => termion_fg!(LightRed),
|
||||
Color::LightGreen => termion_fg!(LightGreen),
|
||||
Color::LightYellow => termion_fg!(LightYellow),
|
||||
Color::LightMagenta => termion_fg!(LightMagenta),
|
||||
Color::LightCyan => termion_fg!(LightCyan),
|
||||
Color::White => termion_fg!(White),
|
||||
Color::Rgb(r, g, b) => termion_fg_rgb!(r, g, b),
|
||||
}
|
||||
}
|
||||
pub fn termion_bg(&self) -> String {
|
||||
match *self {
|
||||
Color::Reset => termion_bg!(Reset),
|
||||
Color::Black => termion_bg!(Black),
|
||||
Color::Red => termion_bg!(Red),
|
||||
Color::Green => termion_bg!(Green),
|
||||
Color::Yellow => termion_bg!(Yellow),
|
||||
Color::Magenta => termion_bg!(Magenta),
|
||||
Color::Cyan => termion_bg!(Cyan),
|
||||
Color::Gray => termion_bg_rgb!(146, 131, 116),
|
||||
Color::DarkGray => termion_bg_rgb!(80, 73, 69),
|
||||
Color::LightRed => termion_bg!(LightRed),
|
||||
Color::LightGreen => termion_bg!(LightGreen),
|
||||
Color::LightYellow => termion_bg!(LightYellow),
|
||||
Color::LightMagenta => termion_bg!(LightMagenta),
|
||||
Color::LightCyan => termion_bg!(LightCyan),
|
||||
Color::White => termion_bg!(White),
|
||||
Color::Rgb(r, g, b) => termion_bg_rgb!(r, g, b),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Modifier {
|
||||
pub fn termion_modifier(&self) -> String {
|
||||
match *self {
|
||||
Modifier::Blink => termion_modifier!(Blink),
|
||||
Modifier::Bold => termion_modifier!(Bold),
|
||||
Modifier::CrossedOut => termion_modifier!(CrossedOut),
|
||||
Modifier::Faint => termion_modifier!(Faint),
|
||||
Modifier::Framed => termion_modifier!(Framed),
|
||||
Modifier::Invert => termion_modifier!(Invert),
|
||||
Modifier::Italic => termion_modifier!(Italic),
|
||||
Modifier::NoBlink => termion_modifier!(NoBlink),
|
||||
Modifier::NoBold => termion_modifier!(NoBold),
|
||||
Modifier::NoCrossedOut => termion_modifier!(NoCrossedOut),
|
||||
Modifier::NoFaint => termion_modifier!(NoFaint),
|
||||
Modifier::NoInvert => termion_modifier!(NoInvert),
|
||||
Modifier::NoItalic => termion_modifier!(NoItalic),
|
||||
Modifier::NoUnderline => termion_modifier!(NoUnderline),
|
||||
Modifier::Reset => termion_modifier!(Reset),
|
||||
Modifier::Underline => termion_modifier!(Underline),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
223
src/backend/termwiz.rs
Normal file
223
src/backend/termwiz.rs
Normal file
@@ -0,0 +1,223 @@
|
||||
//! This module provides the `TermwizBackend` implementation for the [`Backend`] trait.
|
||||
//! It uses the `termwiz` crate to interact with the terminal.
|
||||
//!
|
||||
//! [`Backend`]: trait.Backend.html
|
||||
//! [`TermwizBackend`]: crate::backend::TermionBackend
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
use termwiz::{
|
||||
caps::Capabilities,
|
||||
cell::{AttributeChange, Blink, Intensity, Underline},
|
||||
color::{AnsiColor, ColorAttribute, SrgbaTuple},
|
||||
surface::{Change, CursorVisibility, Position},
|
||||
terminal::{buffered::BufferedTerminal, SystemTerminal, Terminal},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
backend::Backend,
|
||||
buffer::Cell,
|
||||
layout::Rect,
|
||||
style::{Color, Modifier},
|
||||
};
|
||||
|
||||
/// Termwiz backend implementation for the [`Backend`] trait.
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::backend::{Backend, TermwizBackend};
|
||||
///
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let mut backend = TermwizBackend::new()?;
|
||||
/// backend.clear()?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub struct TermwizBackend {
|
||||
buffered_terminal: BufferedTerminal<SystemTerminal>,
|
||||
}
|
||||
|
||||
impl TermwizBackend {
|
||||
/// Creates a new Termwiz backend instance.
|
||||
pub fn new() -> Result<TermwizBackend, Box<dyn Error>> {
|
||||
let mut buffered_terminal =
|
||||
BufferedTerminal::new(SystemTerminal::new(Capabilities::new_from_env()?)?)?;
|
||||
buffered_terminal.terminal().set_raw_mode()?;
|
||||
buffered_terminal.terminal().enter_alternate_screen()?;
|
||||
Ok(TermwizBackend { buffered_terminal })
|
||||
}
|
||||
|
||||
/// Creates a new Termwiz backend instance with the given buffered terminal.
|
||||
pub fn with_buffered_terminal(instance: BufferedTerminal<SystemTerminal>) -> TermwizBackend {
|
||||
TermwizBackend {
|
||||
buffered_terminal: instance,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a reference to the buffered terminal used by the backend.
|
||||
pub fn buffered_terminal(&self) -> &BufferedTerminal<SystemTerminal> {
|
||||
&self.buffered_terminal
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the buffered terminal used by the backend.
|
||||
pub fn buffered_terminal_mut(&mut self) -> &mut BufferedTerminal<SystemTerminal> {
|
||||
&mut self.buffered_terminal
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend for TermwizBackend {
|
||||
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
for (x, y, cell) in content {
|
||||
self.buffered_terminal.add_changes(vec![
|
||||
Change::CursorPosition {
|
||||
x: Position::Absolute(x as usize),
|
||||
y: Position::Absolute(y as usize),
|
||||
},
|
||||
Change::Attribute(AttributeChange::Foreground(cell.fg.into())),
|
||||
Change::Attribute(AttributeChange::Background(cell.bg.into())),
|
||||
]);
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::Intensity(
|
||||
if cell.modifier.contains(Modifier::BOLD) {
|
||||
Intensity::Bold
|
||||
} else if cell.modifier.contains(Modifier::DIM) {
|
||||
Intensity::Half
|
||||
} else {
|
||||
Intensity::Normal
|
||||
},
|
||||
)));
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::Italic(
|
||||
cell.modifier.contains(Modifier::ITALIC),
|
||||
)));
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::Underline(
|
||||
if cell.modifier.contains(Modifier::UNDERLINED) {
|
||||
Underline::Single
|
||||
} else {
|
||||
Underline::None
|
||||
},
|
||||
)));
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::Reverse(
|
||||
cell.modifier.contains(Modifier::REVERSED),
|
||||
)));
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::Invisible(
|
||||
cell.modifier.contains(Modifier::HIDDEN),
|
||||
)));
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::StrikeThrough(
|
||||
cell.modifier.contains(Modifier::CROSSED_OUT),
|
||||
)));
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::Blink(
|
||||
if cell.modifier.contains(Modifier::SLOW_BLINK) {
|
||||
Blink::Slow
|
||||
} else if cell.modifier.contains(Modifier::RAPID_BLINK) {
|
||||
Blink::Rapid
|
||||
} else {
|
||||
Blink::None
|
||||
},
|
||||
)));
|
||||
|
||||
self.buffered_terminal.add_change(&cell.symbol);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> Result<(), io::Error> {
|
||||
self.buffered_terminal
|
||||
.add_change(Change::CursorVisibility(CursorVisibility::Hidden));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_cursor(&mut self) -> Result<(), io::Error> {
|
||||
self.buffered_terminal
|
||||
.add_change(Change::CursorVisibility(CursorVisibility::Visible));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
let (x, y) = self.buffered_terminal.cursor_position();
|
||||
Ok((x as u16, y as u16))
|
||||
}
|
||||
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
self.buffered_terminal.add_change(Change::CursorPosition {
|
||||
x: Position::Absolute(x as usize),
|
||||
y: Position::Absolute(y as usize),
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear(&mut self) -> Result<(), io::Error> {
|
||||
self.buffered_terminal
|
||||
.add_change(Change::ClearScreen(termwiz::color::ColorAttribute::Default));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn size(&self) -> Result<Rect, io::Error> {
|
||||
let (term_width, term_height) = self.buffered_terminal.dimensions();
|
||||
let max = u16::max_value();
|
||||
Ok(Rect::new(
|
||||
0,
|
||||
0,
|
||||
if term_width > usize::from(max) {
|
||||
max
|
||||
} else {
|
||||
term_width as u16
|
||||
},
|
||||
if term_height > usize::from(max) {
|
||||
max
|
||||
} else {
|
||||
term_height as u16
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<(), io::Error> {
|
||||
self.buffered_terminal
|
||||
.flush()
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for ColorAttribute {
|
||||
fn from(color: Color) -> ColorAttribute {
|
||||
match color {
|
||||
Color::Reset => ColorAttribute::Default,
|
||||
Color::Black => AnsiColor::Black.into(),
|
||||
Color::Gray | Color::DarkGray => AnsiColor::Grey.into(),
|
||||
Color::Red => AnsiColor::Maroon.into(),
|
||||
Color::LightRed => AnsiColor::Red.into(),
|
||||
Color::Green => AnsiColor::Green.into(),
|
||||
Color::LightGreen => AnsiColor::Lime.into(),
|
||||
Color::Yellow => AnsiColor::Olive.into(),
|
||||
Color::LightYellow => AnsiColor::Yellow.into(),
|
||||
Color::Magenta => AnsiColor::Purple.into(),
|
||||
Color::LightMagenta => AnsiColor::Fuchsia.into(),
|
||||
Color::Cyan => AnsiColor::Teal.into(),
|
||||
Color::LightCyan => AnsiColor::Aqua.into(),
|
||||
Color::White => AnsiColor::White.into(),
|
||||
Color::Blue => AnsiColor::Navy.into(),
|
||||
Color::LightBlue => AnsiColor::Blue.into(),
|
||||
Color::Indexed(i) => ColorAttribute::PaletteIndex(i),
|
||||
Color::Rgb(r, g, b) => {
|
||||
ColorAttribute::TrueColorWithDefaultFallback(SrgbaTuple::from((r, g, b)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
183
src/backend/test.rs
Normal file
183
src/backend/test.rs
Normal file
@@ -0,0 +1,183 @@
|
||||
//! This module provides the `TestBackend` implementation for the [`Backend`] trait.
|
||||
//! It is used in the integration tests to verify the correctness of the library.
|
||||
|
||||
use std::{
|
||||
fmt::{Display, Write},
|
||||
io,
|
||||
};
|
||||
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{
|
||||
backend::Backend,
|
||||
buffer::{Buffer, Cell},
|
||||
layout::Rect,
|
||||
};
|
||||
|
||||
/// A backend used for the integration tests.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{backend::{Backend, TestBackend}, buffer::Buffer};
|
||||
///
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let mut backend = TestBackend::new(10, 2);
|
||||
/// backend.clear()?;
|
||||
/// backend.assert_buffer(&Buffer::with_lines(vec![" "; 2]));
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
pub struct TestBackend {
|
||||
width: u16,
|
||||
buffer: Buffer,
|
||||
height: u16,
|
||||
cursor: bool,
|
||||
pos: (u16, u16),
|
||||
}
|
||||
|
||||
/// Returns a string representation of the given buffer for debugging purpose.
|
||||
///
|
||||
/// This function is used to visualize the buffer content in a human-readable format.
|
||||
/// It iterates through the buffer content and appends each cell's symbol to the view string.
|
||||
/// If a cell is hidden by a multi-width symbol, it is added to the overwritten vector and
|
||||
/// displayed at the end of the line.
|
||||
fn buffer_view(buffer: &Buffer) -> String {
|
||||
let mut view = String::with_capacity(buffer.content.len() + buffer.area.height as usize * 3);
|
||||
for cells in buffer.content.chunks(buffer.area.width as usize) {
|
||||
let mut overwritten = vec![];
|
||||
let mut skip: usize = 0;
|
||||
view.push('"');
|
||||
for (x, c) in cells.iter().enumerate() {
|
||||
if skip == 0 {
|
||||
view.push_str(&c.symbol);
|
||||
} else {
|
||||
overwritten.push((x, &c.symbol));
|
||||
}
|
||||
skip = std::cmp::max(skip, c.symbol.width()).saturating_sub(1);
|
||||
}
|
||||
view.push('"');
|
||||
if !overwritten.is_empty() {
|
||||
write!(&mut view, " Hidden by multi-width symbols: {overwritten:?}").unwrap();
|
||||
}
|
||||
view.push('\n');
|
||||
}
|
||||
view
|
||||
}
|
||||
|
||||
impl TestBackend {
|
||||
/// Creates a new TestBackend with the specified width and height.
|
||||
pub fn new(width: u16, height: u16) -> TestBackend {
|
||||
TestBackend {
|
||||
width,
|
||||
height,
|
||||
buffer: Buffer::empty(Rect::new(0, 0, width, height)),
|
||||
cursor: false,
|
||||
pos: (0, 0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a reference to the internal buffer of the TestBackend.
|
||||
pub fn buffer(&self) -> &Buffer {
|
||||
&self.buffer
|
||||
}
|
||||
|
||||
/// Resizes the TestBackend to the specified width and height.
|
||||
pub fn resize(&mut self, width: u16, height: u16) {
|
||||
self.buffer.resize(Rect::new(0, 0, width, height));
|
||||
self.width = width;
|
||||
self.height = height;
|
||||
}
|
||||
|
||||
/// Asserts that the TestBackend's buffer is equal to the expected buffer.
|
||||
/// If the buffers are not equal, a panic occurs with a detailed error message
|
||||
/// showing the differences between the expected and actual buffers.
|
||||
pub fn assert_buffer(&self, expected: &Buffer) {
|
||||
assert_eq!(expected.area, self.buffer.area);
|
||||
let diff = expected.diff(&self.buffer);
|
||||
if diff.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut debug_info = String::from("Buffers are not equal");
|
||||
debug_info.push('\n');
|
||||
debug_info.push_str("Expected:");
|
||||
debug_info.push('\n');
|
||||
let expected_view = buffer_view(expected);
|
||||
debug_info.push_str(&expected_view);
|
||||
debug_info.push('\n');
|
||||
debug_info.push_str("Got:");
|
||||
debug_info.push('\n');
|
||||
let view = buffer_view(&self.buffer);
|
||||
debug_info.push_str(&view);
|
||||
debug_info.push('\n');
|
||||
|
||||
debug_info.push_str("Diff:");
|
||||
debug_info.push('\n');
|
||||
let nice_diff = diff
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (x, y, cell))| {
|
||||
let expected_cell = expected.get(*x, *y);
|
||||
format!("{i}: at ({x}, {y}) expected {expected_cell:?} got {cell:?}")
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
debug_info.push_str(&nice_diff);
|
||||
panic!("{debug_info}");
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for TestBackend {
|
||||
/// Formats the TestBackend for display by calling the buffer_view function
|
||||
/// on its internal buffer.
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", buffer_view(&self.buffer))
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend for TestBackend {
|
||||
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
for (x, y, c) in content {
|
||||
let cell = self.buffer.get_mut(x, y);
|
||||
*cell = c.clone();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> Result<(), io::Error> {
|
||||
self.cursor = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_cursor(&mut self) -> Result<(), io::Error> {
|
||||
self.cursor = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_cursor(&mut self) -> Result<(u16, u16), io::Error> {
|
||||
Ok(self.pos)
|
||||
}
|
||||
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), io::Error> {
|
||||
self.pos = (x, y);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear(&mut self) -> Result<(), io::Error> {
|
||||
self.buffer.reset();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn size(&self) -> Result<Rect, io::Error> {
|
||||
Ok(Rect::new(0, 0, self.width, self.height))
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<(), io::Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
828
src/buffer.rs
828
src/buffer.rs
@@ -1,16 +1,25 @@
|
||||
use std::cmp::min;
|
||||
use std::usize;
|
||||
use std::{
|
||||
cmp::min,
|
||||
fmt::{Debug, Formatter, Result},
|
||||
};
|
||||
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use layout::Rect;
|
||||
use style::{Color, Modifier, Style};
|
||||
#[allow(deprecated)]
|
||||
use crate::{
|
||||
layout::Rect,
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span, Spans},
|
||||
};
|
||||
|
||||
/// A buffer cell
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Cell {
|
||||
pub symbol: String,
|
||||
pub style: Style,
|
||||
pub fg: Color,
|
||||
pub bg: Color,
|
||||
pub modifier: Modifier,
|
||||
}
|
||||
|
||||
impl Cell {
|
||||
@@ -27,29 +36,40 @@ impl Cell {
|
||||
}
|
||||
|
||||
pub fn set_fg(&mut self, color: Color) -> &mut Cell {
|
||||
self.style.fg = color;
|
||||
self.fg = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_bg(&mut self, color: Color) -> &mut Cell {
|
||||
self.style.bg = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_modifier(&mut self, modifier: Modifier) -> &mut Cell {
|
||||
self.style.modifier = modifier;
|
||||
self.bg = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_style(&mut self, style: Style) -> &mut Cell {
|
||||
self.style = style;
|
||||
if let Some(c) = style.fg {
|
||||
self.fg = c;
|
||||
}
|
||||
if let Some(c) = style.bg {
|
||||
self.bg = c;
|
||||
}
|
||||
self.modifier.insert(style.add_modifier);
|
||||
self.modifier.remove(style.sub_modifier);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(&self) -> Style {
|
||||
Style::default()
|
||||
.fg(self.fg)
|
||||
.bg(self.bg)
|
||||
.add_modifier(self.modifier)
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.symbol.clear();
|
||||
self.symbol.push(' ');
|
||||
self.style.reset();
|
||||
self.fg = Color::Reset;
|
||||
self.bg = Color::Reset;
|
||||
self.modifier = Modifier::empty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +77,9 @@ impl Default for Cell {
|
||||
fn default() -> Cell {
|
||||
Cell {
|
||||
symbol: " ".into(),
|
||||
style: Default::default(),
|
||||
fg: Color::Reset,
|
||||
bg: Color::Reset,
|
||||
modifier: Modifier::empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,28 +94,24 @@ impl Default for Cell {
|
||||
/// # Examples:
|
||||
///
|
||||
/// ```
|
||||
/// # extern crate tui;
|
||||
/// use tui::buffer::{Buffer, Cell};
|
||||
/// use tui::layout::Rect;
|
||||
/// use tui::style::{Color, Style, Modifier};
|
||||
/// use ratatui::buffer::{Buffer, Cell};
|
||||
/// use ratatui::layout::Rect;
|
||||
/// use ratatui::style::{Color, Style, Modifier};
|
||||
///
|
||||
/// # fn main() {
|
||||
/// let mut buf = Buffer::empty(Rect{x: 0, y: 0, width: 10, height: 5});
|
||||
/// buf.get_mut(0, 2).set_symbol("x");
|
||||
/// assert_eq!(buf.get(0, 2).symbol, "x");
|
||||
/// buf.set_string(3, 0, "string", &Style::default().fg(Color::Red).bg(Color::White));
|
||||
/// buf.set_string(3, 0, "string", Style::default().fg(Color::Red).bg(Color::White));
|
||||
/// assert_eq!(buf.get(5, 0), &Cell{
|
||||
/// symbol: String::from("r"),
|
||||
/// style: Style {
|
||||
/// fg: Color::Red,
|
||||
/// bg: Color::White,
|
||||
/// modifier: Modifier::Reset
|
||||
/// }});
|
||||
/// fg: Color::Red,
|
||||
/// bg: Color::White,
|
||||
/// modifier: Modifier::empty()
|
||||
/// });
|
||||
/// buf.get_mut(5, 0).set_char('x');
|
||||
/// assert_eq!(buf.get(5, 0).symbol, "x");
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Clone, PartialEq, Eq, Default)]
|
||||
pub struct Buffer {
|
||||
/// The area represented by this buffer
|
||||
pub area: Rect,
|
||||
@@ -102,19 +120,10 @@ pub struct Buffer {
|
||||
pub content: Vec<Cell>,
|
||||
}
|
||||
|
||||
impl Default for Buffer {
|
||||
fn default() -> Buffer {
|
||||
Buffer {
|
||||
area: Default::default(),
|
||||
content: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
/// Returns a Buffer with all cells set to the default one
|
||||
pub fn empty(area: Rect) -> Buffer {
|
||||
let cell: Cell = Default::default();
|
||||
let cell = Cell::default();
|
||||
Buffer::filled(area, &cell)
|
||||
}
|
||||
|
||||
@@ -125,10 +134,30 @@ impl Buffer {
|
||||
for _ in 0..size {
|
||||
content.push(cell.clone());
|
||||
}
|
||||
Buffer {
|
||||
area: area,
|
||||
content: content,
|
||||
Buffer { area, content }
|
||||
}
|
||||
|
||||
/// Returns a Buffer containing the given lines
|
||||
pub fn with_lines<S>(lines: Vec<S>) -> Buffer
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
let height = lines.len() as u16;
|
||||
let width = lines
|
||||
.iter()
|
||||
.map(|i| i.as_ref().width() as u16)
|
||||
.max()
|
||||
.unwrap_or_default();
|
||||
let mut buffer = Buffer::empty(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width,
|
||||
height,
|
||||
});
|
||||
for (y, line) in lines.iter().enumerate() {
|
||||
buffer.set_string(0, y as u16, line, Style::default());
|
||||
}
|
||||
buffer
|
||||
}
|
||||
|
||||
/// Returns the content of the buffer as a slice
|
||||
@@ -153,49 +182,198 @@ impl Buffer {
|
||||
&mut self.content[i]
|
||||
}
|
||||
|
||||
/// Returns the index in the Vec<Cell> for the given (x, y)
|
||||
/// Returns the index in the `Vec<Cell>` for the given global (x, y) coordinates.
|
||||
///
|
||||
/// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::buffer::Buffer;
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// let rect = Rect::new(200, 100, 10, 10);
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// // Global coordinates to the top corner of this buffer's area
|
||||
/// assert_eq!(buffer.index_of(200, 100), 0);
|
||||
/// ```
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics when given an coordinate that is outside of this Buffer's area.
|
||||
///
|
||||
/// ```should_panic
|
||||
/// # use ratatui::buffer::Buffer;
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// let rect = Rect::new(200, 100, 10, 10);
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// // Top coordinate is outside of the buffer in global coordinate space, as the Buffer's area
|
||||
/// // starts at (200, 100).
|
||||
/// buffer.index_of(0, 0); // Panics
|
||||
/// ```
|
||||
pub fn index_of(&self, x: u16, y: u16) -> usize {
|
||||
debug_assert!(
|
||||
x >= self.area.left() && x < self.area.right() && y >= self.area.top()
|
||||
x >= self.area.left()
|
||||
&& x < self.area.right()
|
||||
&& y >= self.area.top()
|
||||
&& y < self.area.bottom(),
|
||||
"Trying to access position outside the buffer: x={}, y={}, area={:?}",
|
||||
x,
|
||||
y,
|
||||
"Trying to access position outside the buffer: x={x}, y={y}, area={:?}",
|
||||
self.area
|
||||
);
|
||||
((y - self.area.y) * self.area.width + (x - self.area.x)) as usize
|
||||
}
|
||||
|
||||
/// Returns the coordinates of a cell given its index
|
||||
/// Returns the (global) coordinates of a cell given its index
|
||||
///
|
||||
/// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::buffer::Buffer;
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// let rect = Rect::new(200, 100, 10, 10);
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// assert_eq!(buffer.pos_of(0), (200, 100));
|
||||
/// assert_eq!(buffer.pos_of(14), (204, 101));
|
||||
/// ```
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics when given an index that is outside the Buffer's content.
|
||||
///
|
||||
/// ```should_panic
|
||||
/// # use ratatui::buffer::Buffer;
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// let rect = Rect::new(0, 0, 10, 10); // 100 cells in total
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// // Index 100 is the 101th cell, which lies outside of the area of this Buffer.
|
||||
/// buffer.pos_of(100); // Panics
|
||||
/// ```
|
||||
pub fn pos_of(&self, i: usize) -> (u16, u16) {
|
||||
debug_assert!(
|
||||
i >= self.content.len(),
|
||||
"Trying to get the coords of a cell outside the buffer: i={} len={}",
|
||||
i,
|
||||
i < self.content.len(),
|
||||
"Trying to get the coords of a cell outside the buffer: i={i} len={}",
|
||||
self.content.len()
|
||||
);
|
||||
(
|
||||
self.area.x + i as u16 % self.area.width,
|
||||
self.area.y + i as u16 / self.area.width,
|
||||
self.area.x + (i as u16) % self.area.width,
|
||||
self.area.y + (i as u16) / self.area.width,
|
||||
)
|
||||
}
|
||||
|
||||
/// Print a string, starting at the position (x, y)
|
||||
pub fn set_string(&mut self, x: u16, y: u16, string: &str, style: &Style) {
|
||||
pub fn set_string<S>(&mut self, x: u16, y: u16, string: S, style: Style)
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
self.set_stringn(x, y, string, usize::MAX, style);
|
||||
}
|
||||
|
||||
/// Print at most the first n characters of a string if enough space is available
|
||||
/// until the end of the line
|
||||
pub fn set_stringn(&mut self, x: u16, y: u16, string: &str, limit: usize, style: &Style) {
|
||||
pub fn set_stringn<S>(
|
||||
&mut self,
|
||||
x: u16,
|
||||
y: u16,
|
||||
string: S,
|
||||
width: usize,
|
||||
style: Style,
|
||||
) -> (u16, u16)
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
let mut index = self.index_of(x, y);
|
||||
let graphemes = UnicodeSegmentation::graphemes(string, true).collect::<Vec<&str>>();
|
||||
let max_index = min((self.area.right() - x) as usize, limit);
|
||||
for s in graphemes.into_iter().take(max_index) {
|
||||
self.content[index].symbol.clear();
|
||||
self.content[index].symbol.push_str(s);
|
||||
self.content[index].style = *style;
|
||||
index += 1;
|
||||
let mut x_offset = x as usize;
|
||||
let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true);
|
||||
let max_offset = min(self.area.right() as usize, width.saturating_add(x as usize));
|
||||
for s in graphemes {
|
||||
let width = s.width();
|
||||
if width == 0 {
|
||||
continue;
|
||||
}
|
||||
// `x_offset + width > max_offset` could be integer overflow on 32-bit machines if we
|
||||
// change dimensions to usize or u32 and someone resizes the terminal to 1x2^32.
|
||||
if width > max_offset.saturating_sub(x_offset) {
|
||||
break;
|
||||
}
|
||||
|
||||
self.content[index].set_symbol(s);
|
||||
self.content[index].set_style(style);
|
||||
// Reset following cells if multi-width (they would be hidden by the grapheme),
|
||||
for i in index + 1..index + width {
|
||||
self.content[i].reset();
|
||||
}
|
||||
index += width;
|
||||
x_offset += width;
|
||||
}
|
||||
(x_offset as u16, y)
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
#[deprecated(note = "Use `Buffer::set_line` instead")]
|
||||
pub fn set_spans(&mut self, x: u16, y: u16, spans: &Spans<'_>, width: u16) -> (u16, u16) {
|
||||
let mut remaining_width = width;
|
||||
let mut x = x;
|
||||
for span in &spans.0 {
|
||||
if remaining_width == 0 {
|
||||
break;
|
||||
}
|
||||
let pos = self.set_stringn(
|
||||
x,
|
||||
y,
|
||||
span.content.as_ref(),
|
||||
remaining_width as usize,
|
||||
span.style,
|
||||
);
|
||||
let w = pos.0.saturating_sub(x);
|
||||
x = pos.0;
|
||||
remaining_width = remaining_width.saturating_sub(w);
|
||||
}
|
||||
(x, y)
|
||||
}
|
||||
|
||||
pub fn set_line(&mut self, x: u16, y: u16, line: &Line<'_>, width: u16) -> (u16, u16) {
|
||||
let mut remaining_width = width;
|
||||
let mut x = x;
|
||||
for span in &line.spans {
|
||||
if remaining_width == 0 {
|
||||
break;
|
||||
}
|
||||
let pos = self.set_stringn(
|
||||
x,
|
||||
y,
|
||||
span.content.as_ref(),
|
||||
remaining_width as usize,
|
||||
span.style,
|
||||
);
|
||||
let w = pos.0.saturating_sub(x);
|
||||
x = pos.0;
|
||||
remaining_width = remaining_width.saturating_sub(w);
|
||||
}
|
||||
(x, y)
|
||||
}
|
||||
|
||||
pub fn set_span(&mut self, x: u16, y: u16, span: &Span<'_>, width: u16) -> (u16, u16) {
|
||||
self.set_stringn(x, y, span.content.as_ref(), width as usize, span.style)
|
||||
}
|
||||
|
||||
#[deprecated(
|
||||
since = "0.10.0",
|
||||
note = "You should use styling capabilities of `Buffer::set_style`"
|
||||
)]
|
||||
pub fn set_background(&mut self, area: Rect, color: Color) {
|
||||
for y in area.top()..area.bottom() {
|
||||
for x in area.left()..area.right() {
|
||||
self.get_mut(x, y).set_bg(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_style(&mut self, area: Rect, style: Style) {
|
||||
for y in area.top()..area.bottom() {
|
||||
for x in area.left()..area.right() {
|
||||
self.get_mut(x, y).set_style(style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,7 +384,7 @@ impl Buffer {
|
||||
if self.content.len() > length {
|
||||
self.content.truncate(length);
|
||||
} else {
|
||||
self.content.resize(length, Default::default());
|
||||
self.content.resize(length, Cell::default());
|
||||
}
|
||||
self.area = area;
|
||||
}
|
||||
@@ -220,35 +398,541 @@ impl Buffer {
|
||||
|
||||
/// Merge an other buffer into this one
|
||||
pub fn merge(&mut self, other: &Buffer) {
|
||||
let area = self.area.union(&other.area);
|
||||
let cell: Cell = Default::default();
|
||||
let area = self.area.union(other.area);
|
||||
let cell = Cell::default();
|
||||
self.content.resize(area.area() as usize, cell.clone());
|
||||
|
||||
// Move original content to the appropriate space
|
||||
let offset_x = self.area.x - area.x;
|
||||
let offset_y = self.area.y - area.y;
|
||||
let size = self.area.area() as usize;
|
||||
for i in (0..size).rev() {
|
||||
let (x, y) = self.pos_of(i);
|
||||
// New index in content
|
||||
let k = ((y + offset_y) * area.width + (x + offset_x)) as usize;
|
||||
self.content[k] = self.content[i].clone();
|
||||
let k = ((y - area.y) * area.width + x - area.x) as usize;
|
||||
if i != k {
|
||||
self.content[k] = self.content[i].clone();
|
||||
self.content[i] = cell.clone();
|
||||
}
|
||||
}
|
||||
|
||||
// Push content of the other buffer into this one (may erase previous
|
||||
// data)
|
||||
let offset_x = other.area.x - area.x;
|
||||
let offset_y = other.area.y - area.y;
|
||||
let size = other.area.area() as usize;
|
||||
for i in 0..size {
|
||||
let (x, y) = other.pos_of(i);
|
||||
// New index in content
|
||||
let k = ((y + offset_y) * area.width + (x + offset_x)) as usize;
|
||||
let k = ((y - area.y) * area.width + x - area.x) as usize;
|
||||
self.content[k] = other.content[i].clone();
|
||||
}
|
||||
self.area = area;
|
||||
}
|
||||
|
||||
/// Builds a minimal sequence of coordinates and Cells necessary to update the UI from
|
||||
/// self to other.
|
||||
///
|
||||
/// We're assuming that buffers are well-formed, that is no double-width cell is followed by
|
||||
/// a non-blank cell.
|
||||
///
|
||||
/// # Multi-width characters handling:
|
||||
///
|
||||
/// ```text
|
||||
/// (Index:) `01`
|
||||
/// Prev: `コ`
|
||||
/// Next: `aa`
|
||||
/// Updates: `0: a, 1: a'
|
||||
/// ```
|
||||
///
|
||||
/// ```text
|
||||
/// (Index:) `01`
|
||||
/// Prev: `a `
|
||||
/// Next: `コ`
|
||||
/// Updates: `0: コ` (double width symbol at index 0 - skip index 1)
|
||||
/// ```
|
||||
///
|
||||
/// ```text
|
||||
/// (Index:) `012`
|
||||
/// Prev: `aaa`
|
||||
/// Next: `aコ`
|
||||
/// Updates: `0: a, 1: コ` (double width symbol at index 1 - skip index 2)
|
||||
/// ```
|
||||
pub fn diff<'a>(&self, other: &'a Buffer) -> Vec<(u16, u16, &'a Cell)> {
|
||||
let previous_buffer = &self.content;
|
||||
let next_buffer = &other.content;
|
||||
|
||||
let mut updates: Vec<(u16, u16, &Cell)> = vec![];
|
||||
// Cells invalidated by drawing/replacing preceding multi-width characters:
|
||||
let mut invalidated: usize = 0;
|
||||
// Cells from the current buffer to skip due to preceding multi-width characters taking
|
||||
// their place (the skipped cells should be blank anyway):
|
||||
let mut to_skip: usize = 0;
|
||||
for (i, (current, previous)) in next_buffer.iter().zip(previous_buffer.iter()).enumerate() {
|
||||
if (current != previous || invalidated > 0) && to_skip == 0 {
|
||||
let (x, y) = self.pos_of(i);
|
||||
updates.push((x, y, &next_buffer[i]));
|
||||
}
|
||||
|
||||
to_skip = current.symbol.width().saturating_sub(1);
|
||||
|
||||
let affected_width = std::cmp::max(current.symbol.width(), previous.symbol.width());
|
||||
invalidated = std::cmp::max(affected_width, invalidated).saturating_sub(1);
|
||||
}
|
||||
updates
|
||||
}
|
||||
}
|
||||
|
||||
/// Assert that two buffers are equal by comparing their areas and content.
|
||||
///
|
||||
/// On panic, displays the areas or the content and a diff of the contents.
|
||||
#[macro_export]
|
||||
macro_rules! assert_buffer_eq {
|
||||
($actual_expr:expr, $expected_expr:expr) => {
|
||||
match (&$actual_expr, &$expected_expr) {
|
||||
(actual, expected) => {
|
||||
if actual.area != expected.area {
|
||||
panic!(
|
||||
indoc::indoc!(
|
||||
"
|
||||
buffer areas not equal
|
||||
expected: {:?}
|
||||
actual: {:?}"
|
||||
),
|
||||
expected, actual
|
||||
);
|
||||
}
|
||||
let diff = expected.diff(&actual);
|
||||
if !diff.is_empty() {
|
||||
let nice_diff = diff
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (x, y, cell))| {
|
||||
let expected_cell = expected.get(*x, *y);
|
||||
indoc::formatdoc! {"
|
||||
{i}: at ({x}, {y})
|
||||
expected: {expected_cell:?}
|
||||
actual: {cell:?}
|
||||
"}
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
panic!(
|
||||
indoc::indoc!(
|
||||
"
|
||||
buffer contents not equal
|
||||
expected: {:?}
|
||||
actual: {:?}
|
||||
diff:
|
||||
{}"
|
||||
),
|
||||
expected, actual, nice_diff
|
||||
);
|
||||
}
|
||||
// shouldn't get here, but this guards against future behavior
|
||||
// that changes equality but not area or content
|
||||
assert_eq!(actual, expected, "buffers not equal");
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl Debug for Buffer {
|
||||
/// Writes a debug representation of the buffer to the given formatter.
|
||||
///
|
||||
/// The format is like a pretty printed struct, with the following fields:
|
||||
/// * `area`: displayed as `Rect { x: 1, y: 2, width: 3, height: 4 }`
|
||||
/// * `content`: displayed as a list of strings representing the content of the buffer
|
||||
/// * `styles`: displayed as a list of: `{ x: 1, y: 2, fg: Color::Red, bg: Color::Blue,
|
||||
/// modifier: Modifier::BOLD }` only showing a value when there is a change in style.
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||
f.write_fmt(format_args!(
|
||||
"Buffer {{\n area: {:?},\n content: [\n",
|
||||
&self.area
|
||||
))?;
|
||||
let mut last_style = None;
|
||||
let mut styles = vec![];
|
||||
for (y, line) in self.content.chunks(self.area.width as usize).enumerate() {
|
||||
let mut overwritten = vec![];
|
||||
let mut skip: usize = 0;
|
||||
f.write_str(" \"")?;
|
||||
for (x, c) in line.iter().enumerate() {
|
||||
if skip == 0 {
|
||||
f.write_str(&c.symbol)?;
|
||||
} else {
|
||||
overwritten.push((x, &c.symbol));
|
||||
}
|
||||
skip = std::cmp::max(skip, c.symbol.width()).saturating_sub(1);
|
||||
let style = (c.fg, c.bg, c.modifier);
|
||||
if last_style != Some(style) {
|
||||
last_style = Some(style);
|
||||
styles.push((x, y, c.fg, c.bg, c.modifier));
|
||||
}
|
||||
}
|
||||
if !overwritten.is_empty() {
|
||||
f.write_fmt(format_args!(
|
||||
"// hidden by multi-width symbols: {overwritten:?}"
|
||||
))?;
|
||||
}
|
||||
f.write_str("\",\n")?;
|
||||
}
|
||||
f.write_str(" ],\n styles: [\n")?;
|
||||
for s in styles {
|
||||
f.write_fmt(format_args!(
|
||||
" x: {}, y: {}, fg: {:?}, bg: {:?}, modifier: {:?},\n",
|
||||
s.0, s.1, s.2, s.3, s.4
|
||||
))?;
|
||||
}
|
||||
f.write_str(" ]\n}")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn cell(s: &str) -> Cell {
|
||||
let mut cell = Cell::default();
|
||||
cell.set_symbol(s);
|
||||
cell
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_implements_debug() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 12, 2));
|
||||
buf.set_string(0, 0, "Hello World!", Style::default());
|
||||
buf.set_string(
|
||||
0,
|
||||
1,
|
||||
"G'day World!",
|
||||
Style::default()
|
||||
.fg(Color::Green)
|
||||
.bg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{buf:?}"),
|
||||
indoc::indoc!(
|
||||
"
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 12, height: 2 },
|
||||
content: [
|
||||
\"Hello World!\",
|
||||
\"G'day World!\",
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, modifier: NONE,
|
||||
x: 0, y: 1, fg: Green, bg: Yellow, modifier: BOLD,
|
||||
]
|
||||
}"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assert_buffer_eq_does_not_panic_on_equal_buffers() {
|
||||
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
let other_buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
assert_buffer_eq!(buffer, other_buffer);
|
||||
}
|
||||
|
||||
#[should_panic]
|
||||
#[test]
|
||||
fn assert_buffer_eq_panics_on_unequal_area() {
|
||||
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
let other_buffer = Buffer::empty(Rect::new(0, 0, 6, 1));
|
||||
assert_buffer_eq!(buffer, other_buffer);
|
||||
}
|
||||
|
||||
#[should_panic]
|
||||
#[test]
|
||||
fn assert_buffer_eq_panics_on_unequal_style() {
|
||||
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
let mut other_buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
other_buffer.set_string(0, 0, " ", Style::default().fg(Color::Red));
|
||||
assert_buffer_eq!(buffer, other_buffer);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_translates_to_and_from_coordinates() {
|
||||
let rect = Rect::new(200, 100, 50, 80);
|
||||
let buf = Buffer::empty(rect);
|
||||
|
||||
// First cell is at the upper left corner.
|
||||
assert_eq!(buf.pos_of(0), (200, 100));
|
||||
assert_eq!(buf.index_of(200, 100), 0);
|
||||
|
||||
// Last cell is in the lower right.
|
||||
assert_eq!(buf.pos_of(buf.content.len() - 1), (249, 179));
|
||||
assert_eq!(buf.index_of(249, 179), buf.content.len() - 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "outside the buffer")]
|
||||
fn pos_of_panics_on_out_of_bounds() {
|
||||
let rect = Rect::new(0, 0, 10, 10);
|
||||
let buf = Buffer::empty(rect);
|
||||
|
||||
// There are a total of 100 cells; zero-indexed means that 100 would be the 101st cell.
|
||||
buf.pos_of(100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "outside the buffer")]
|
||||
fn index_of_panics_on_out_of_bounds() {
|
||||
let rect = Rect::new(0, 0, 10, 10);
|
||||
let buf = Buffer::empty(rect);
|
||||
|
||||
// width is 10; zero-indexed means that 10 would be the 11th cell.
|
||||
buf.index_of(10, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_set_string() {
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
|
||||
// Zero-width
|
||||
buffer.set_stringn(0, 0, "aaa", 0, Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" "]));
|
||||
|
||||
buffer.set_string(0, 0, "aaa", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["aaa "]));
|
||||
|
||||
// Width limit:
|
||||
buffer.set_stringn(0, 0, "bbbbbbbbbbbbbb", 4, Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["bbbb "]));
|
||||
|
||||
buffer.set_string(0, 0, "12345", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345"]));
|
||||
|
||||
// Width truncation:
|
||||
buffer.set_string(0, 0, "123456", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345"]));
|
||||
|
||||
// multi-line
|
||||
buffer = Buffer::empty(Rect::new(0, 0, 5, 2));
|
||||
buffer.set_string(0, 0, "12345", Style::default());
|
||||
buffer.set_string(0, 1, "67890", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345", "67890"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_set_string_multi_width_overwrite() {
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
|
||||
// multi-width overwrite
|
||||
buffer.set_string(0, 0, "aaaaa", Style::default());
|
||||
buffer.set_string(0, 0, "称号", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["称号a"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_set_string_zero_width() {
|
||||
let area = Rect::new(0, 0, 1, 1);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
|
||||
// Leading grapheme with zero width
|
||||
let s = "\u{1}a";
|
||||
buffer.set_stringn(0, 0, s, 1, Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["a"]));
|
||||
|
||||
// Trailing grapheme with zero with
|
||||
let s = "a\u{1}";
|
||||
buffer.set_stringn(0, 0, s, 1, Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["a"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_set_string_double_width() {
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
buffer.set_string(0, 0, "コン", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["コン "]));
|
||||
|
||||
// Only 1 space left.
|
||||
buffer.set_string(0, 0, "コンピ", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["コン "]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_with_lines() {
|
||||
let buffer =
|
||||
Buffer::with_lines(vec!["┌────────┐", "│コンピュ│", "│ーa 上で│", "└────────┘"]);
|
||||
assert_eq!(buffer.area.x, 0);
|
||||
assert_eq!(buffer.area.y, 0);
|
||||
assert_eq!(buffer.area.width, 10);
|
||||
assert_eq!(buffer.area.height, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_diffing_empty_empty() {
|
||||
let area = Rect::new(0, 0, 40, 40);
|
||||
let prev = Buffer::empty(area);
|
||||
let next = Buffer::empty(area);
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(diff, vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_diffing_empty_filled() {
|
||||
let area = Rect::new(0, 0, 40, 40);
|
||||
let prev = Buffer::empty(area);
|
||||
let next = Buffer::filled(area, Cell::default().set_symbol("a"));
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(diff.len(), 40 * 40);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_diffing_filled_filled() {
|
||||
let area = Rect::new(0, 0, 40, 40);
|
||||
let prev = Buffer::filled(area, Cell::default().set_symbol("a"));
|
||||
let next = Buffer::filled(area, Cell::default().set_symbol("a"));
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(diff, vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_diffing_single_width() {
|
||||
let prev = Buffer::with_lines(vec![
|
||||
" ",
|
||||
"┌Title─┐ ",
|
||||
"│ │ ",
|
||||
"│ │ ",
|
||||
"└──────┘ ",
|
||||
]);
|
||||
let next = Buffer::with_lines(vec![
|
||||
" ",
|
||||
"┌TITLE─┐ ",
|
||||
"│ │ ",
|
||||
"│ │ ",
|
||||
"└──────┘ ",
|
||||
]);
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(
|
||||
diff,
|
||||
vec![
|
||||
(2, 1, &cell("I")),
|
||||
(3, 1, &cell("T")),
|
||||
(4, 1, &cell("L")),
|
||||
(5, 1, &cell("E")),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[rustfmt::skip]
|
||||
fn buffer_diffing_multi_width() {
|
||||
let prev = Buffer::with_lines(vec![
|
||||
"┌Title─┐ ",
|
||||
"└──────┘ ",
|
||||
]);
|
||||
let next = Buffer::with_lines(vec![
|
||||
"┌称号──┐ ",
|
||||
"└──────┘ ",
|
||||
]);
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(
|
||||
diff,
|
||||
vec![
|
||||
(1, 0, &cell("称")),
|
||||
// Skipped "i"
|
||||
(3, 0, &cell("号")),
|
||||
// Skipped "l"
|
||||
(5, 0, &cell("─")),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_diffing_multi_width_offset() {
|
||||
let prev = Buffer::with_lines(vec!["┌称号──┐"]);
|
||||
let next = Buffer::with_lines(vec!["┌─称号─┐"]);
|
||||
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(
|
||||
diff,
|
||||
vec![(1, 0, &cell("─")), (2, 0, &cell("称")), (4, 0, &cell("号")),]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_merge() {
|
||||
let mut one = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("1"),
|
||||
);
|
||||
let two = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 2,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("2"),
|
||||
);
|
||||
one.merge(&two);
|
||||
assert_buffer_eq!(one, Buffer::with_lines(vec!["11", "11", "22", "22"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_merge2() {
|
||||
let mut one = Buffer::filled(
|
||||
Rect {
|
||||
x: 2,
|
||||
y: 2,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("1"),
|
||||
);
|
||||
let two = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("2"),
|
||||
);
|
||||
one.merge(&two);
|
||||
assert_buffer_eq!(
|
||||
one,
|
||||
Buffer::with_lines(vec!["22 ", "22 ", " 11", " 11"])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_merge3() {
|
||||
let mut one = Buffer::filled(
|
||||
Rect {
|
||||
x: 3,
|
||||
y: 3,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("1"),
|
||||
);
|
||||
let two = Buffer::filled(
|
||||
Rect {
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 3,
|
||||
height: 4,
|
||||
},
|
||||
Cell::default().set_symbol("2"),
|
||||
);
|
||||
one.merge(&two);
|
||||
let mut merged = Buffer::with_lines(vec!["222 ", "222 ", "2221", "2221"]);
|
||||
merged.area = Rect {
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 4,
|
||||
height: 4,
|
||||
};
|
||||
assert_buffer_eq!(one, merged);
|
||||
}
|
||||
}
|
||||
|
||||
728
src/layout.rs
728
src/layout.rs
@@ -1,215 +1,330 @@
|
||||
use std::cmp::{max, min};
|
||||
use std::collections::HashMap;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
cmp::{max, min},
|
||||
collections::HashMap,
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use cassowary::{Constraint, Expression, Solver, Variable};
|
||||
use cassowary::WeightedRelation::*;
|
||||
use cassowary::strength::{REQUIRED, WEAK};
|
||||
|
||||
use terminal::Terminal;
|
||||
use backend::Backend;
|
||||
use cassowary::{
|
||||
strength::{MEDIUM, REQUIRED, WEAK},
|
||||
Constraint as CassowaryConstraint, Expression, Solver, Variable,
|
||||
WeightedRelation::{EQ, GE, LE},
|
||||
};
|
||||
|
||||
#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Corner {
|
||||
TopLeft,
|
||||
TopRight,
|
||||
BottomRight,
|
||||
BottomLeft,
|
||||
}
|
||||
|
||||
#[derive(Debug, Hash, Clone, PartialEq, Eq)]
|
||||
pub enum Direction {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
/// A simple rectangle used in the computation of the layout and to give widgets an hint about the
|
||||
/// area they are supposed to render to.
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||
pub struct Rect {
|
||||
pub x: u16,
|
||||
pub y: u16,
|
||||
pub width: u16,
|
||||
pub height: u16,
|
||||
}
|
||||
|
||||
impl Default for Rect {
|
||||
fn default() -> Rect {
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Rect {
|
||||
Rect {
|
||||
x: x,
|
||||
y: y,
|
||||
width: width,
|
||||
height: height,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn area(&self) -> u16 {
|
||||
self.width * self.height
|
||||
}
|
||||
|
||||
pub fn left(&self) -> u16 {
|
||||
self.x
|
||||
}
|
||||
|
||||
pub fn right(&self) -> u16 {
|
||||
self.x + self.width
|
||||
}
|
||||
|
||||
pub fn top(&self) -> u16 {
|
||||
self.y
|
||||
}
|
||||
|
||||
pub fn bottom(&self) -> u16 {
|
||||
self.y + self.height
|
||||
}
|
||||
|
||||
pub fn inner(&self, margin: u16) -> Rect {
|
||||
if self.width < 2 * margin || self.height < 2 * margin {
|
||||
Rect::default()
|
||||
} else {
|
||||
Rect {
|
||||
x: self.x + margin,
|
||||
y: self.y + margin,
|
||||
width: self.width - 2 * margin,
|
||||
height: self.height - 2 * margin,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn union(&self, other: &Rect) -> Rect {
|
||||
let x1 = min(self.x, other.x);
|
||||
let y1 = min(self.y, other.y);
|
||||
let x2 = max(self.x + self.width, other.x + other.width);
|
||||
let y2 = max(self.y + self.height, other.y + other.height);
|
||||
Rect {
|
||||
x: x1,
|
||||
y: y1,
|
||||
width: x2 - x1,
|
||||
height: y2 - y1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn intersection(&self, other: &Rect) -> Rect {
|
||||
let x1 = max(self.x, other.x);
|
||||
let y1 = max(self.y, other.y);
|
||||
let x2 = min(self.x + self.width, other.x + other.width);
|
||||
let y2 = min(self.y + self.height, other.y + other.height);
|
||||
Rect {
|
||||
x: x1,
|
||||
y: y1,
|
||||
width: x2 - x1,
|
||||
height: y2 - y1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn intersects(&self, other: &Rect) -> bool {
|
||||
self.x < other.x + other.width && self.x + self.width > other.x
|
||||
&& self.y < other.y + other.height && self.y + self.height > other.y
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Size {
|
||||
Fixed(u16),
|
||||
Percent(u16),
|
||||
pub enum Constraint {
|
||||
Percentage(u16),
|
||||
Ratio(u32, u32),
|
||||
Length(u16),
|
||||
Max(u16),
|
||||
Min(u16),
|
||||
}
|
||||
|
||||
/// Wrapper function around the cassowary-rs solver to be able to split a given
|
||||
/// area into smaller ones based on the preferred widths or heights and the direction.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # extern crate tui;
|
||||
/// # use tui::layout::{Rect, Size, Direction, split};
|
||||
///
|
||||
/// # fn main() {
|
||||
/// let chunks = split(&Rect{x: 2, y: 2, width: 10, height: 10},
|
||||
/// &Direction::Vertical,
|
||||
/// 0,
|
||||
/// &[Size::Fixed(5), Size::Min(0)]);
|
||||
/// assert_eq!(chunks, vec![Rect{x:2, y: 2, width: 10, height: 5},
|
||||
/// Rect{x: 2, y: 7, width: 10, height: 5}])
|
||||
/// # }
|
||||
///
|
||||
/// ```
|
||||
pub fn split(area: &Rect, dir: &Direction, margin: u16, sizes: &[Size]) -> Vec<Rect> {
|
||||
impl Constraint {
|
||||
pub fn apply(&self, length: u16) -> u16 {
|
||||
match *self {
|
||||
Constraint::Percentage(p) => {
|
||||
let p = p as f32 / 100.0;
|
||||
let length = length as f32;
|
||||
(p * length).min(length) as u16
|
||||
}
|
||||
Constraint::Ratio(numerator, denominator) => {
|
||||
// avoid division by zero by using 1 when denominator is 0
|
||||
// this results in 0/0 -> 0 and x/0 -> x for x != 0
|
||||
let percentage = numerator as f32 / denominator.max(1) as f32;
|
||||
let length = length as f32;
|
||||
(percentage * length).min(length) as u16
|
||||
}
|
||||
Constraint::Length(l) => length.min(l),
|
||||
Constraint::Max(m) => length.min(m),
|
||||
Constraint::Min(m) => length.max(m),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct Margin {
|
||||
pub vertical: u16,
|
||||
pub horizontal: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Alignment {
|
||||
Left,
|
||||
Center,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct Layout {
|
||||
direction: Direction,
|
||||
margin: Margin,
|
||||
constraints: Vec<Constraint>,
|
||||
/// Whether the last chunk of the computed layout should be expanded to fill the available
|
||||
/// space.
|
||||
expand_to_fill: bool,
|
||||
}
|
||||
|
||||
type Cache = HashMap<(Rect, Layout), Rc<[Rect]>>;
|
||||
thread_local! {
|
||||
static LAYOUT_CACHE: RefCell<Cache> = RefCell::new(HashMap::new());
|
||||
}
|
||||
|
||||
impl Default for Layout {
|
||||
fn default() -> Layout {
|
||||
Layout::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Layout {
|
||||
pub const fn new() -> Layout {
|
||||
Layout {
|
||||
direction: Direction::Vertical,
|
||||
margin: Margin {
|
||||
horizontal: 0,
|
||||
vertical: 0,
|
||||
},
|
||||
constraints: Vec::new(),
|
||||
expand_to_fill: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn constraints<C>(mut self, constraints: C) -> Layout
|
||||
where
|
||||
C: Into<Vec<Constraint>>,
|
||||
{
|
||||
self.constraints = constraints.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub const fn margin(mut self, margin: u16) -> Layout {
|
||||
self.margin = Margin {
|
||||
horizontal: margin,
|
||||
vertical: margin,
|
||||
};
|
||||
self
|
||||
}
|
||||
|
||||
pub const fn horizontal_margin(mut self, horizontal: u16) -> Layout {
|
||||
self.margin.horizontal = horizontal;
|
||||
self
|
||||
}
|
||||
|
||||
pub const fn vertical_margin(mut self, vertical: u16) -> Layout {
|
||||
self.margin.vertical = vertical;
|
||||
self
|
||||
}
|
||||
|
||||
pub const fn direction(mut self, direction: Direction) -> Layout {
|
||||
self.direction = direction;
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) const fn expand_to_fill(mut self, expand_to_fill: bool) -> Layout {
|
||||
self.expand_to_fill = expand_to_fill;
|
||||
self
|
||||
}
|
||||
|
||||
/// Wrapper function around the cassowary-rs solver to be able to split a given
|
||||
/// area into smaller ones based on the preferred widths or heights and the direction.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use ratatui::layout::{Rect, Constraint, Direction, Layout};
|
||||
/// let chunks = Layout::default()
|
||||
/// .direction(Direction::Vertical)
|
||||
/// .constraints([Constraint::Length(5), Constraint::Min(0)].as_ref())
|
||||
/// .split(Rect {
|
||||
/// x: 2,
|
||||
/// y: 2,
|
||||
/// width: 10,
|
||||
/// height: 10,
|
||||
/// });
|
||||
/// assert_eq!(
|
||||
/// chunks[..],
|
||||
/// [
|
||||
/// Rect {
|
||||
/// x: 2,
|
||||
/// y: 2,
|
||||
/// width: 10,
|
||||
/// height: 5
|
||||
/// },
|
||||
/// Rect {
|
||||
/// x: 2,
|
||||
/// y: 7,
|
||||
/// width: 10,
|
||||
/// height: 5
|
||||
/// }
|
||||
/// ]
|
||||
/// );
|
||||
///
|
||||
/// let chunks = Layout::default()
|
||||
/// .direction(Direction::Horizontal)
|
||||
/// .constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)].as_ref())
|
||||
/// .split(Rect {
|
||||
/// x: 0,
|
||||
/// y: 0,
|
||||
/// width: 9,
|
||||
/// height: 2,
|
||||
/// });
|
||||
/// assert_eq!(
|
||||
/// chunks[..],
|
||||
/// [
|
||||
/// Rect {
|
||||
/// x: 0,
|
||||
/// y: 0,
|
||||
/// width: 3,
|
||||
/// height: 2
|
||||
/// },
|
||||
/// Rect {
|
||||
/// x: 3,
|
||||
/// y: 0,
|
||||
/// width: 6,
|
||||
/// height: 2
|
||||
/// }
|
||||
/// ]
|
||||
/// );
|
||||
/// ```
|
||||
pub fn split(&self, area: Rect) -> Rc<[Rect]> {
|
||||
// TODO: Maybe use a fixed size cache ?
|
||||
LAYOUT_CACHE.with(|c| {
|
||||
c.borrow_mut()
|
||||
.entry((area, self.clone()))
|
||||
.or_insert_with(|| split(area, self))
|
||||
.clone()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn split(area: Rect, layout: &Layout) -> Rc<[Rect]> {
|
||||
let mut solver = Solver::new();
|
||||
let mut vars: HashMap<Variable, (usize, usize)> = HashMap::new();
|
||||
let elements = sizes
|
||||
let elements = layout
|
||||
.constraints
|
||||
.iter()
|
||||
.map(|_| Element::new())
|
||||
.collect::<Vec<Element>>();
|
||||
let mut results = sizes.iter().map(|_| Rect::default()).collect::<Vec<Rect>>();
|
||||
let dest_area = area.inner(margin);
|
||||
let mut res = layout
|
||||
.constraints
|
||||
.iter()
|
||||
.map(|_| Rect::default())
|
||||
.collect::<Rc<[Rect]>>();
|
||||
|
||||
let results = Rc::get_mut(&mut res).expect("newly created Rc should have no shared refs");
|
||||
|
||||
let dest_area = area.inner(&layout.margin);
|
||||
for (i, e) in elements.iter().enumerate() {
|
||||
vars.insert(e.x, (i, 0));
|
||||
vars.insert(e.y, (i, 1));
|
||||
vars.insert(e.width, (i, 2));
|
||||
vars.insert(e.height, (i, 3));
|
||||
}
|
||||
let mut constraints: Vec<Constraint> = Vec::with_capacity(elements.len() * 4 + sizes.len() * 6);
|
||||
let mut ccs: Vec<CassowaryConstraint> =
|
||||
Vec::with_capacity(elements.len() * 4 + layout.constraints.len() * 6);
|
||||
for elt in &elements {
|
||||
constraints.push(elt.left() | GE(REQUIRED) | f64::from(dest_area.left()));
|
||||
constraints.push(elt.top() | GE(REQUIRED) | f64::from(dest_area.top()));
|
||||
constraints.push(elt.right() | LE(REQUIRED) | f64::from(dest_area.right()));
|
||||
constraints.push(elt.bottom() | LE(REQUIRED) | f64::from(dest_area.bottom()));
|
||||
ccs.push(elt.width | GE(REQUIRED) | 0f64);
|
||||
ccs.push(elt.height | GE(REQUIRED) | 0f64);
|
||||
ccs.push(elt.left() | GE(REQUIRED) | f64::from(dest_area.left()));
|
||||
ccs.push(elt.top() | GE(REQUIRED) | f64::from(dest_area.top()));
|
||||
ccs.push(elt.right() | LE(REQUIRED) | f64::from(dest_area.right()));
|
||||
ccs.push(elt.bottom() | LE(REQUIRED) | f64::from(dest_area.bottom()));
|
||||
}
|
||||
if let Some(first) = elements.first() {
|
||||
constraints.push(match *dir {
|
||||
ccs.push(match layout.direction {
|
||||
Direction::Horizontal => first.left() | EQ(REQUIRED) | f64::from(dest_area.left()),
|
||||
Direction::Vertical => first.top() | EQ(REQUIRED) | f64::from(dest_area.top()),
|
||||
});
|
||||
}
|
||||
if let Some(last) = elements.last() {
|
||||
constraints.push(match *dir {
|
||||
Direction::Horizontal => last.right() | EQ(REQUIRED) | f64::from(dest_area.right()),
|
||||
Direction::Vertical => last.bottom() | EQ(REQUIRED) | f64::from(dest_area.bottom()),
|
||||
});
|
||||
if layout.expand_to_fill {
|
||||
if let Some(last) = elements.last() {
|
||||
ccs.push(match layout.direction {
|
||||
Direction::Horizontal => last.right() | EQ(REQUIRED) | f64::from(dest_area.right()),
|
||||
Direction::Vertical => last.bottom() | EQ(REQUIRED) | f64::from(dest_area.bottom()),
|
||||
});
|
||||
}
|
||||
}
|
||||
match *dir {
|
||||
match layout.direction {
|
||||
Direction::Horizontal => {
|
||||
for pair in elements.windows(2) {
|
||||
constraints.push((pair[0].x + pair[0].width) | EQ(REQUIRED) | pair[1].x);
|
||||
ccs.push((pair[0].x + pair[0].width) | EQ(REQUIRED) | pair[1].x);
|
||||
}
|
||||
for (i, size) in sizes.iter().enumerate() {
|
||||
constraints.push(elements[i].y | EQ(REQUIRED) | f64::from(dest_area.y));
|
||||
constraints.push(elements[i].height | EQ(REQUIRED) | f64::from(dest_area.height));
|
||||
constraints.push(match *size {
|
||||
Size::Fixed(v) => elements[i].width | EQ(WEAK) | f64::from(v),
|
||||
Size::Percent(v) => {
|
||||
elements[i].width | EQ(WEAK) | (f64::from(v * dest_area.width) / 100.0)
|
||||
for (i, size) in layout.constraints.iter().enumerate() {
|
||||
ccs.push(elements[i].y | EQ(REQUIRED) | f64::from(dest_area.y));
|
||||
ccs.push(elements[i].height | EQ(REQUIRED) | f64::from(dest_area.height));
|
||||
ccs.push(match *size {
|
||||
Constraint::Length(v) => elements[i].width | EQ(MEDIUM) | f64::from(v),
|
||||
Constraint::Percentage(v) => {
|
||||
elements[i].width | EQ(MEDIUM) | (f64::from(v * dest_area.width) / 100.0)
|
||||
}
|
||||
Size::Min(v) => elements[i].width | GE(WEAK) | f64::from(v),
|
||||
Size::Max(v) => elements[i].width | LE(WEAK) | f64::from(v),
|
||||
Constraint::Ratio(n, d) => {
|
||||
elements[i].width
|
||||
| EQ(MEDIUM)
|
||||
| (f64::from(dest_area.width) * f64::from(n) / f64::from(d))
|
||||
}
|
||||
Constraint::Min(v) => elements[i].width | GE(MEDIUM) | f64::from(v),
|
||||
Constraint::Max(v) => elements[i].width | LE(MEDIUM) | f64::from(v),
|
||||
});
|
||||
|
||||
match *size {
|
||||
Constraint::Min(v) | Constraint::Max(v) => {
|
||||
ccs.push(elements[i].width | EQ(WEAK) | f64::from(v));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Direction::Vertical => {
|
||||
for pair in elements.windows(2) {
|
||||
constraints.push((pair[0].y + pair[0].height) | EQ(REQUIRED) | pair[1].y);
|
||||
ccs.push((pair[0].y + pair[0].height) | EQ(REQUIRED) | pair[1].y);
|
||||
}
|
||||
for (i, size) in sizes.iter().enumerate() {
|
||||
constraints.push(elements[i].x | EQ(REQUIRED) | f64::from(dest_area.x));
|
||||
constraints.push(elements[i].width | EQ(REQUIRED) | f64::from(dest_area.width));
|
||||
constraints.push(match *size {
|
||||
Size::Fixed(v) => elements[i].height | EQ(WEAK) | f64::from(v),
|
||||
Size::Percent(v) => {
|
||||
elements[i].height | EQ(WEAK) | (f64::from(v * dest_area.height) / 100.0)
|
||||
for (i, size) in layout.constraints.iter().enumerate() {
|
||||
ccs.push(elements[i].x | EQ(REQUIRED) | f64::from(dest_area.x));
|
||||
ccs.push(elements[i].width | EQ(REQUIRED) | f64::from(dest_area.width));
|
||||
ccs.push(match *size {
|
||||
Constraint::Length(v) => elements[i].height | EQ(MEDIUM) | f64::from(v),
|
||||
Constraint::Percentage(v) => {
|
||||
elements[i].height | EQ(MEDIUM) | (f64::from(v * dest_area.height) / 100.0)
|
||||
}
|
||||
Size::Min(v) => elements[i].height | GE(WEAK) | f64::from(v),
|
||||
Size::Max(v) => elements[i].height | LE(WEAK) | f64::from(v),
|
||||
Constraint::Ratio(n, d) => {
|
||||
elements[i].height
|
||||
| EQ(MEDIUM)
|
||||
| (f64::from(dest_area.height) * f64::from(n) / f64::from(d))
|
||||
}
|
||||
Constraint::Min(v) => elements[i].height | GE(MEDIUM) | f64::from(v),
|
||||
Constraint::Max(v) => elements[i].height | LE(MEDIUM) | f64::from(v),
|
||||
});
|
||||
|
||||
match *size {
|
||||
Constraint::Min(v) | Constraint::Max(v) => {
|
||||
ccs.push(elements[i].height | EQ(WEAK) | f64::from(v));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
solver.add_constraints(&constraints).unwrap();
|
||||
solver.add_constraints(&ccs).unwrap();
|
||||
for &(var, value) in solver.fetch_changes() {
|
||||
let (index, attr) = vars[&var];
|
||||
let value = value as u16;
|
||||
let value = if value.is_sign_negative() {
|
||||
0
|
||||
} else {
|
||||
value as u16
|
||||
};
|
||||
match attr {
|
||||
0 => {
|
||||
results[index].x = value;
|
||||
@@ -226,18 +341,21 @@ pub fn split(area: &Rect, dir: &Direction, margin: u16, sizes: &[Size]) -> Vec<R
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// Fix imprecision by extending the last item a bit if necessary
|
||||
if let Some(last) = results.last_mut() {
|
||||
match *dir {
|
||||
Direction::Vertical => {
|
||||
last.height = dest_area.bottom() - last.y;
|
||||
}
|
||||
Direction::Horizontal => {
|
||||
last.width = dest_area.right() - last.x;
|
||||
|
||||
if layout.expand_to_fill {
|
||||
// Fix imprecision by extending the last item a bit if necessary
|
||||
if let Some(last) = results.last_mut() {
|
||||
match layout.direction {
|
||||
Direction::Vertical => {
|
||||
last.height = dest_area.bottom() - last.y;
|
||||
}
|
||||
Direction::Horizontal => {
|
||||
last.width = dest_area.right() - last.x;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
results
|
||||
res
|
||||
}
|
||||
|
||||
/// A container used by the solver inside split
|
||||
@@ -275,58 +393,240 @@ impl Element {
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes a layout and may be used to group widgets in a specific area of the terminal
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # extern crate tui;
|
||||
/// use tui::layout::{Group, Direction, Size};
|
||||
/// # fn main() {
|
||||
/// Group::default()
|
||||
/// .direction(Direction::Vertical)
|
||||
/// .margin(0)
|
||||
/// .sizes(&[Size::Percent(50), Size::Percent(50)]);
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Debug, PartialEq, Clone, Eq, Hash)]
|
||||
pub struct Group {
|
||||
pub direction: Direction,
|
||||
pub margin: u16,
|
||||
pub sizes: Vec<Size>,
|
||||
/// A simple rectangle used in the computation of the layout and to give widgets a hint about the
|
||||
/// area they are supposed to render to.
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default)]
|
||||
pub struct Rect {
|
||||
pub x: u16,
|
||||
pub y: u16,
|
||||
pub width: u16,
|
||||
pub height: u16,
|
||||
}
|
||||
|
||||
impl Default for Group {
|
||||
fn default() -> Group {
|
||||
Group {
|
||||
direction: Direction::Horizontal,
|
||||
margin: 0,
|
||||
sizes: Vec::new(),
|
||||
impl Rect {
|
||||
/// Creates a new rect, with width and height limited to keep the area under max u16.
|
||||
/// If clipped, aspect ratio will be preserved.
|
||||
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Rect {
|
||||
let max_area = u16::max_value();
|
||||
let (clipped_width, clipped_height) =
|
||||
if u32::from(width) * u32::from(height) > u32::from(max_area) {
|
||||
let aspect_ratio = f64::from(width) / f64::from(height);
|
||||
let max_area_f = f64::from(max_area);
|
||||
let height_f = (max_area_f / aspect_ratio).sqrt();
|
||||
let width_f = height_f * aspect_ratio;
|
||||
(width_f as u16, height_f as u16)
|
||||
} else {
|
||||
(width, height)
|
||||
};
|
||||
Rect {
|
||||
x,
|
||||
y,
|
||||
width: clipped_width,
|
||||
height: clipped_height,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn area(self) -> u16 {
|
||||
self.width * self.height
|
||||
}
|
||||
|
||||
pub const fn left(self) -> u16 {
|
||||
self.x
|
||||
}
|
||||
|
||||
pub const fn right(self) -> u16 {
|
||||
self.x.saturating_add(self.width)
|
||||
}
|
||||
|
||||
pub const fn top(self) -> u16 {
|
||||
self.y
|
||||
}
|
||||
|
||||
pub const fn bottom(self) -> u16 {
|
||||
self.y.saturating_add(self.height)
|
||||
}
|
||||
|
||||
pub fn inner(self, margin: &Margin) -> Rect {
|
||||
if self.width < 2 * margin.horizontal || self.height < 2 * margin.vertical {
|
||||
Rect::default()
|
||||
} else {
|
||||
Rect {
|
||||
x: self.x + margin.horizontal,
|
||||
y: self.y + margin.vertical,
|
||||
width: self.width - 2 * margin.horizontal,
|
||||
height: self.height - 2 * margin.vertical,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn union(self, other: Rect) -> Rect {
|
||||
let x1 = min(self.x, other.x);
|
||||
let y1 = min(self.y, other.y);
|
||||
let x2 = max(self.x + self.width, other.x + other.width);
|
||||
let y2 = max(self.y + self.height, other.y + other.height);
|
||||
Rect {
|
||||
x: x1,
|
||||
y: y1,
|
||||
width: x2 - x1,
|
||||
height: y2 - y1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn intersection(self, other: Rect) -> Rect {
|
||||
let x1 = max(self.x, other.x);
|
||||
let y1 = max(self.y, other.y);
|
||||
let x2 = min(self.x + self.width, other.x + other.width);
|
||||
let y2 = min(self.y + self.height, other.y + other.height);
|
||||
Rect {
|
||||
x: x1,
|
||||
y: y1,
|
||||
width: x2 - x1,
|
||||
height: y2 - y1,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn intersects(self, other: Rect) -> bool {
|
||||
self.x < other.x + other.width
|
||||
&& self.x + self.width > other.x
|
||||
&& self.y < other.y + other.height
|
||||
&& self.y + self.height > other.y
|
||||
}
|
||||
}
|
||||
|
||||
impl Group {
|
||||
pub fn direction(&mut self, direction: Direction) -> &mut Group {
|
||||
self.direction = direction;
|
||||
self
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_vertical_split_by_height() {
|
||||
let target = Rect {
|
||||
x: 2,
|
||||
y: 2,
|
||||
width: 10,
|
||||
height: 10,
|
||||
};
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Max(5),
|
||||
Constraint::Min(1),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(target);
|
||||
|
||||
assert_eq!(target.height, chunks.iter().map(|r| r.height).sum::<u16>());
|
||||
chunks.windows(2).for_each(|w| assert!(w[0].y <= w[1].y));
|
||||
}
|
||||
|
||||
pub fn margin(&mut self, margin: u16) -> &mut Group {
|
||||
self.margin = margin;
|
||||
self
|
||||
#[test]
|
||||
fn test_rect_size_truncation() {
|
||||
for width in 256u16..300u16 {
|
||||
for height in 256u16..300u16 {
|
||||
let rect = Rect::new(0, 0, width, height);
|
||||
rect.area(); // Should not panic.
|
||||
assert!(rect.width < width || rect.height < height);
|
||||
// The target dimensions are rounded down so the math will not be too precise
|
||||
// but let's make sure the ratios don't diverge crazily.
|
||||
assert!(
|
||||
(f64::from(rect.width) / f64::from(rect.height)
|
||||
- f64::from(width) / f64::from(height))
|
||||
.abs()
|
||||
< 1.0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// One dimension below 255, one above. Area above max u16.
|
||||
let width = 900;
|
||||
let height = 100;
|
||||
let rect = Rect::new(0, 0, width, height);
|
||||
assert_ne!(rect.width, 900);
|
||||
assert_ne!(rect.height, 100);
|
||||
assert!(rect.width < width || rect.height < height);
|
||||
}
|
||||
|
||||
pub fn sizes(&mut self, sizes: &[Size]) -> &mut Group {
|
||||
self.sizes = Vec::from(sizes);
|
||||
self
|
||||
#[test]
|
||||
fn test_rect_size_preservation() {
|
||||
for width in 0..256u16 {
|
||||
for height in 0..256u16 {
|
||||
let rect = Rect::new(0, 0, width, height);
|
||||
rect.area(); // Should not panic.
|
||||
assert_eq!(rect.width, width);
|
||||
assert_eq!(rect.height, height);
|
||||
}
|
||||
}
|
||||
|
||||
// One dimension below 255, one above. Area below max u16.
|
||||
let rect = Rect::new(0, 0, 300, 100);
|
||||
assert_eq!(rect.width, 300);
|
||||
assert_eq!(rect.height, 100);
|
||||
}
|
||||
pub fn render<F, B>(&self, t: &mut Terminal<B>, area: &Rect, mut f: F)
|
||||
where
|
||||
B: Backend,
|
||||
F: FnMut(&mut Terminal<B>, &[Rect]),
|
||||
{
|
||||
let chunks = t.compute_layout(self, area);
|
||||
f(t, &chunks);
|
||||
|
||||
#[test]
|
||||
fn test_constraint_apply() {
|
||||
assert_eq!(Constraint::Percentage(0).apply(100), 0);
|
||||
assert_eq!(Constraint::Percentage(50).apply(100), 50);
|
||||
assert_eq!(Constraint::Percentage(100).apply(100), 100);
|
||||
assert_eq!(Constraint::Percentage(200).apply(100), 100);
|
||||
assert_eq!(Constraint::Percentage(u16::MAX).apply(100), 100);
|
||||
|
||||
// 0/0 intentionally avoids a panic by returning 0.
|
||||
assert_eq!(Constraint::Ratio(0, 0).apply(100), 0);
|
||||
// 1/0 intentionally avoids a panic by returning 100% of the length.
|
||||
assert_eq!(Constraint::Ratio(1, 0).apply(100), 100);
|
||||
assert_eq!(Constraint::Ratio(0, 1).apply(100), 0);
|
||||
assert_eq!(Constraint::Ratio(1, 2).apply(100), 50);
|
||||
assert_eq!(Constraint::Ratio(2, 2).apply(100), 100);
|
||||
assert_eq!(Constraint::Ratio(3, 2).apply(100), 100);
|
||||
assert_eq!(Constraint::Ratio(u32::MAX, 2).apply(100), 100);
|
||||
|
||||
assert_eq!(Constraint::Length(0).apply(100), 0);
|
||||
assert_eq!(Constraint::Length(50).apply(100), 50);
|
||||
assert_eq!(Constraint::Length(100).apply(100), 100);
|
||||
assert_eq!(Constraint::Length(200).apply(100), 100);
|
||||
assert_eq!(Constraint::Length(u16::MAX).apply(100), 100);
|
||||
|
||||
assert_eq!(Constraint::Max(0).apply(100), 0);
|
||||
assert_eq!(Constraint::Max(50).apply(100), 50);
|
||||
assert_eq!(Constraint::Max(100).apply(100), 100);
|
||||
assert_eq!(Constraint::Max(200).apply(100), 100);
|
||||
assert_eq!(Constraint::Max(u16::MAX).apply(100), 100);
|
||||
|
||||
assert_eq!(Constraint::Min(0).apply(100), 100);
|
||||
assert_eq!(Constraint::Min(50).apply(100), 100);
|
||||
assert_eq!(Constraint::Min(100).apply(100), 100);
|
||||
assert_eq!(Constraint::Min(200).apply(100), 200);
|
||||
assert_eq!(Constraint::Min(u16::MAX).apply(100), u16::MAX);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rect_can_be_const() {
|
||||
const RECT: Rect = Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 10,
|
||||
height: 10,
|
||||
};
|
||||
const _AREA: u16 = RECT.area();
|
||||
const _LEFT: u16 = RECT.left();
|
||||
const _RIGHT: u16 = RECT.right();
|
||||
const _TOP: u16 = RECT.top();
|
||||
const _BOTTOM: u16 = RECT.bottom();
|
||||
assert!(RECT.intersects(RECT));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_can_be_const() {
|
||||
const _LAYOUT: Layout = Layout::new();
|
||||
const _DEFAULT_LAYOUT: Layout = Layout::new()
|
||||
.direction(Direction::Horizontal)
|
||||
.margin(1)
|
||||
.expand_to_fill(false);
|
||||
const _HORIZONTAL_LAYOUT: Layout = Layout::new().horizontal_margin(1);
|
||||
const _VERTICAL_LAYOUT: Layout = Layout::new().vertical_margin(1);
|
||||
}
|
||||
}
|
||||
|
||||
265
src/lib.rs
265
src/lib.rs
@@ -1,172 +1,189 @@
|
||||
//! [tui](https://github.com/fdehau/tui-rs) is a library used to build rich
|
||||
//! [ratatui](https://github.com/tui-rs-revival/ratatui) is a library used to build rich
|
||||
//! terminal users interfaces and dashboards.
|
||||
//!
|
||||
//! 
|
||||
//! 
|
||||
//!
|
||||
//! # Get started
|
||||
//!
|
||||
//! ## Creating a `Terminal`
|
||||
//! ## Adding `ratatui` as a dependency
|
||||
//!
|
||||
//! Every application using `tui` should start by instantiating a `Terminal`. It is
|
||||
//! a light abstraction over available backends that provides basic functionalities
|
||||
//! such as clearing the screen, hiding the cursor, etc. By default only the `termion`
|
||||
//! backend is available.
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! extern crate tui;
|
||||
//!
|
||||
//! use tui::Terminal;
|
||||
//! use tui::backend::RawBackend;
|
||||
//!
|
||||
//! fn main() {
|
||||
//! let backend = RawBackend::new().unwrap();
|
||||
//! let mut terminal = Terminal::new(backend).unwrap();
|
||||
//! }
|
||||
//! Add the following to your `Cargo.toml`:
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! crossterm = "0.26"
|
||||
//! ratatui = "0.20"
|
||||
//! ```
|
||||
//!
|
||||
//! If for some reason, you might want to use the `rustbox` backend instead, you
|
||||
//! need the to replace your `tui` dependency specification by:
|
||||
//! The crate is using the `crossterm` backend by default that works on most platforms. But if for
|
||||
//! example you want to use the `termion` backend instead. This can be done by changing your
|
||||
//! dependencies specification to the following:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies.tui]
|
||||
//! version = "0.2.0"
|
||||
//! default-features = false
|
||||
//! features = ['rustbox']
|
||||
//! [dependencies]
|
||||
//! termion = "1.5"
|
||||
//! ratatui = { version = "0.20", default-features = false, features = ['termion'] }
|
||||
//! ```
|
||||
//!
|
||||
//! and then create the terminal in a similar way:
|
||||
//! The same logic applies for all other available backends.
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! extern crate tui;
|
||||
//! ### Features
|
||||
//!
|
||||
//! use tui::Terminal;
|
||||
//! use tui::backend::RustboxBackend;
|
||||
//! Widgets which add dependencies are gated behind feature flags to prevent unused transitive
|
||||
//! dependencies. The available features are:
|
||||
//!
|
||||
//! fn main() {
|
||||
//! let backend = RustboxBackend::new().unwrap();
|
||||
//! let mut terminal = Terminal::new(backend).unwrap();
|
||||
//! * `widget-calendar` - enables [`widgets::calendar`] and adds a dependency on the [time
|
||||
//! crate](https://crates.io/crates/time).
|
||||
//!
|
||||
//! ## Creating a `Terminal`
|
||||
//!
|
||||
//! Every application using `ratatui` should start by instantiating a `Terminal`. It is a light
|
||||
//! abstraction over available backends that provides basic functionalities such as clearing the
|
||||
//! screen, hiding the cursor, etc.
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use std::io;
|
||||
//! use ratatui::{backend::CrosstermBackend, Terminal};
|
||||
//!
|
||||
//! fn main() -> Result<(), io::Error> {
|
||||
//! let stdout = io::stdout();
|
||||
//! let backend = CrosstermBackend::new(stdout);
|
||||
//! let mut terminal = Terminal::new(backend)?;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! If you had previously chosen `termion` as a backend, the terminal can be created in a similar
|
||||
//! way:
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! use std::io;
|
||||
//! use ratatui::{backend::TermionBackend, Terminal};
|
||||
//! use termion::raw::IntoRawMode;
|
||||
//!
|
||||
//! fn main() -> Result<(), io::Error> {
|
||||
//! let stdout = io::stdout().into_raw_mode()?;
|
||||
//! let backend = TermionBackend::new(stdout);
|
||||
//! let mut terminal = Terminal::new(backend)?;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! You may also refer to the examples to find out how to create a `Terminal` for each available
|
||||
//! backend.
|
||||
//!
|
||||
//! ## Building a User Interface (UI)
|
||||
//!
|
||||
//! Every component of your interface will be implementing the `Widget` trait.
|
||||
//! The library comes with a predefined set of widgets that should met most of
|
||||
//! your use cases. You are also free to implement your owns.
|
||||
//! Every component of your interface will be implementing the `Widget` trait. The library comes
|
||||
//! with a predefined set of widgets that should meet most of your use cases. You are also free to
|
||||
//! implement your own.
|
||||
//!
|
||||
//! Each widget follows a builder pattern API providing a default configuration
|
||||
//! along with methods to customize them. The widget is then registered using
|
||||
//! its `render` method that take a `Terminal` instance and an area to draw
|
||||
//! to.
|
||||
//! Each widget follows a builder pattern API providing a default configuration along with methods
|
||||
//! to customize them. The widget is then rendered using [`Frame::render_widget`] which takes
|
||||
//! your widget instance and an area to draw to.
|
||||
//!
|
||||
//! The following example renders a block of the size of the terminal:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! extern crate tui;
|
||||
//! use std::{io, thread, time::Duration};
|
||||
//! use ratatui::{
|
||||
//! backend::CrosstermBackend,
|
||||
//! widgets::{Block, Borders},
|
||||
//! Terminal
|
||||
//! };
|
||||
//! use crossterm::{
|
||||
//! event::{self, DisableMouseCapture, EnableMouseCapture},
|
||||
//! execute,
|
||||
//! terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
//! };
|
||||
//!
|
||||
//! use std::io;
|
||||
//! fn main() -> Result<(), io::Error> {
|
||||
//! // setup terminal
|
||||
//! enable_raw_mode()?;
|
||||
//! let mut stdout = io::stdout();
|
||||
//! execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
//! let backend = CrosstermBackend::new(stdout);
|
||||
//! let mut terminal = Terminal::new(backend)?;
|
||||
//!
|
||||
//! use tui::Terminal;
|
||||
//! use tui::backend::RawBackend;
|
||||
//! use tui::widgets::{Widget, Block, Borders};
|
||||
//! use tui::layout::{Group, Size, Direction};
|
||||
//! terminal.draw(|f| {
|
||||
//! let size = f.size();
|
||||
//! let block = Block::default()
|
||||
//! .title("Block")
|
||||
//! .borders(Borders::ALL);
|
||||
//! f.render_widget(block, size);
|
||||
//! })?;
|
||||
//!
|
||||
//! fn main() {
|
||||
//! let mut terminal = init().expect("Failed initialization");
|
||||
//! draw(&mut terminal).expect("Failed to draw");
|
||||
//! }
|
||||
//! // Start a thread to discard any input events. Without handling events, the
|
||||
//! // stdin buffer will fill up, and be read into the shell when the program exits.
|
||||
//! thread::spawn(|| loop {
|
||||
//! event::read();
|
||||
//! });
|
||||
//!
|
||||
//! fn init() -> Result<Terminal<RawBackend>, io::Error> {
|
||||
//! let backend = RawBackend::new()?;
|
||||
//! Terminal::new(backend)
|
||||
//! }
|
||||
//! thread::sleep(Duration::from_millis(5000));
|
||||
//!
|
||||
//! fn draw(t: &mut Terminal<RawBackend>) -> Result<(), io::Error> {
|
||||
//! // restore terminal
|
||||
//! disable_raw_mode()?;
|
||||
//! execute!(
|
||||
//! terminal.backend_mut(),
|
||||
//! LeaveAlternateScreen,
|
||||
//! DisableMouseCapture
|
||||
//! )?;
|
||||
//! terminal.show_cursor()?;
|
||||
//!
|
||||
//! let size = t.size()?;
|
||||
//!
|
||||
//! Block::default()
|
||||
//! .title("Block")
|
||||
//! .borders(Borders::ALL)
|
||||
//! .render(t, &size);
|
||||
//!
|
||||
//! t.draw()
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Layout
|
||||
//!
|
||||
//! The library comes with a basic yet useful layout management object called
|
||||
//! `Group`. As you may see below and in the examples, the library makes heavy
|
||||
//! use of the builder pattern to provide full customization. And the `Group`
|
||||
//! object is no exception:
|
||||
//! The library comes with a basic yet useful layout management object called `Layout`. As you may
|
||||
//! see below and in the examples, the library makes heavy use of the builder pattern to provide
|
||||
//! full customization. And `Layout` is no exception:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! extern crate tui;
|
||||
//!
|
||||
//! use std::io;
|
||||
//!
|
||||
//! use tui::Terminal;
|
||||
//! use tui::backend::RawBackend;
|
||||
//! use tui::widgets::{Widget, Block, Borders};
|
||||
//! use tui::layout::{Group, Size, Direction};
|
||||
//!
|
||||
//! fn main() {
|
||||
//! let mut terminal = init().expect("Failed initialization");
|
||||
//! draw(&mut terminal).expect("Failed to draw");
|
||||
//! }
|
||||
//!
|
||||
//! fn init() -> Result<Terminal<RawBackend>, io::Error> {
|
||||
//! let backend = RawBackend::new()?;
|
||||
//! Terminal::new(backend)
|
||||
//! }
|
||||
//!
|
||||
//! fn draw(t: &mut Terminal<RawBackend>) -> Result<(), io::Error> {
|
||||
//!
|
||||
//! let size = t.size()?;
|
||||
//!
|
||||
//! Group::default()
|
||||
//! use ratatui::{
|
||||
//! backend::Backend,
|
||||
//! layout::{Constraint, Direction, Layout},
|
||||
//! widgets::{Block, Borders},
|
||||
//! Frame,
|
||||
//! };
|
||||
//! fn ui<B: Backend>(f: &mut Frame<B>) {
|
||||
//! let chunks = Layout::default()
|
||||
//! .direction(Direction::Vertical)
|
||||
//! .margin(1)
|
||||
//! .sizes(&[Size::Fixed(10), Size::Max(20), Size::Min(10)])
|
||||
//! .render(t, &size, |t, chunks| {
|
||||
//! Block::default()
|
||||
//! .title("Block")
|
||||
//! .borders(Borders::ALL)
|
||||
//! .render(t, &chunks[0]);
|
||||
//! Block::default()
|
||||
//! .title("Block 2")
|
||||
//! .borders(Borders::ALL)
|
||||
//! .render(t, &chunks[2]);
|
||||
//! });
|
||||
//!
|
||||
//! t.draw()
|
||||
//! .constraints(
|
||||
//! [
|
||||
//! Constraint::Percentage(10),
|
||||
//! Constraint::Percentage(80),
|
||||
//! Constraint::Percentage(10)
|
||||
//! ].as_ref()
|
||||
//! )
|
||||
//! .split(f.size());
|
||||
//! let block = Block::default()
|
||||
//! .title("Block")
|
||||
//! .borders(Borders::ALL);
|
||||
//! f.render_widget(block, chunks[0]);
|
||||
//! let block = Block::default()
|
||||
//! .title("Block 2")
|
||||
//! .borders(Borders::ALL);
|
||||
//! f.render_widget(block, chunks[1]);
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! This let you describe responsive terminal UI by nesting groups. You should note
|
||||
//! that by default the computed layout tries to fill the available space
|
||||
//! completely. So if for any reason you might need a blank space somewhere, try to
|
||||
//! pass an additional size to the group and don't use the corresponding area inside
|
||||
//! the render method.
|
||||
//!
|
||||
//! Once you have finished to describe the UI, you just need to call `draw`
|
||||
//! on `Terminal` to actually flush to the terminal.
|
||||
//! This let you describe responsive terminal UI by nesting layouts. You should note that by
|
||||
//! default the computed layout tries to fill the available space completely. So if for any reason
|
||||
//! you might need a blank space somewhere, try to pass an additional constraint and don't use the
|
||||
//! corresponding area.
|
||||
|
||||
#[macro_use]
|
||||
extern crate bitflags;
|
||||
extern crate cassowary;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate unicode_segmentation;
|
||||
extern crate unicode_width;
|
||||
// show the feature flags in the generated documentation
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
|
||||
pub mod buffer;
|
||||
pub mod symbols;
|
||||
pub mod backend;
|
||||
pub mod terminal;
|
||||
pub mod widgets;
|
||||
pub mod style;
|
||||
pub mod buffer;
|
||||
pub mod layout;
|
||||
pub mod style;
|
||||
pub mod symbols;
|
||||
pub mod terminal;
|
||||
pub mod text;
|
||||
pub mod widgets;
|
||||
|
||||
pub use self::terminal::Terminal;
|
||||
pub use self::terminal::{Frame, Terminal, TerminalOptions, Viewport};
|
||||
|
||||
507
src/style.rs
507
src/style.rs
@@ -1,10 +1,52 @@
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
//! `style` contains the primitives used to control how your user interface will look.
|
||||
//!
|
||||
//! # Using the `Style` struct
|
||||
//!
|
||||
//! This is useful when creating style variables.
|
||||
//! ## Example
|
||||
//! ```
|
||||
//! use ratatui::style::{Color, Modifier, Style};
|
||||
//!
|
||||
//! Style::default()
|
||||
//! .fg(Color::Black)
|
||||
//! .bg(Color::Green)
|
||||
//! .add_modifier(Modifier::ITALIC | Modifier::BOLD);
|
||||
//! ```
|
||||
//!
|
||||
//! # Using style shorthands
|
||||
//!
|
||||
//! This is best for consise styling.
|
||||
//! ## Example
|
||||
//! ```
|
||||
//! use ratatui::{
|
||||
//! style::{Color, Modifier, Style, Styled, Stylize},
|
||||
//! text::Span,
|
||||
//! };
|
||||
//!
|
||||
//! assert_eq!(
|
||||
//! "hello".red().on_blue().bold(),
|
||||
//! Span::styled("hello", Style::default().fg(Color::Red).bg(Color::Blue).add_modifier(Modifier::BOLD))
|
||||
//! )
|
||||
//! ```
|
||||
mod stylized;
|
||||
|
||||
use std::{
|
||||
fmt::{self, Debug},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use bitflags::bitflags;
|
||||
pub use stylized::{Styled, Stylize};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Color {
|
||||
Reset,
|
||||
Black,
|
||||
Red,
|
||||
Green,
|
||||
Yellow,
|
||||
Blue,
|
||||
Magenta,
|
||||
Cyan,
|
||||
Gray,
|
||||
@@ -12,66 +54,455 @@ pub enum Color {
|
||||
LightRed,
|
||||
LightGreen,
|
||||
LightYellow,
|
||||
LightBlue,
|
||||
LightMagenta,
|
||||
LightCyan,
|
||||
White,
|
||||
Rgb(u8, u8, u8),
|
||||
Indexed(u8),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum Modifier {
|
||||
Blink,
|
||||
Bold,
|
||||
CrossedOut,
|
||||
Faint,
|
||||
Framed,
|
||||
Invert,
|
||||
Italic,
|
||||
NoBlink,
|
||||
NoBold,
|
||||
NoCrossedOut,
|
||||
NoFaint,
|
||||
NoInvert,
|
||||
NoItalic,
|
||||
NoUnderline,
|
||||
Reset,
|
||||
Underline,
|
||||
bitflags! {
|
||||
/// Modifier changes the way a piece of text is displayed.
|
||||
///
|
||||
/// They are bitflags so they can easily be composed.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::style::Modifier;
|
||||
///
|
||||
/// let m = Modifier::BOLD | Modifier::ITALIC;
|
||||
/// ```
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Modifier: u16 {
|
||||
const BOLD = 0b0000_0000_0001;
|
||||
const DIM = 0b0000_0000_0010;
|
||||
const ITALIC = 0b0000_0000_0100;
|
||||
const UNDERLINED = 0b0000_0000_1000;
|
||||
const SLOW_BLINK = 0b0000_0001_0000;
|
||||
const RAPID_BLINK = 0b0000_0010_0000;
|
||||
const REVERSED = 0b0000_0100_0000;
|
||||
const HIDDEN = 0b0000_1000_0000;
|
||||
const CROSSED_OUT = 0b0001_0000_0000;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
/// Implement the `Debug` trait for `Modifier` manually.
|
||||
///
|
||||
/// This will avoid printing the empty modifier as 'Borders(0x0)' and instead print it as 'NONE'.
|
||||
impl fmt::Debug for Modifier {
|
||||
/// Format the modifier as `NONE` if the modifier is empty or as a list of flags separated by
|
||||
/// `|` otherwise.
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
if self.is_empty() {
|
||||
return write!(f, "NONE");
|
||||
}
|
||||
fmt::Debug::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
/// Style let you control the main characteristics of the displayed elements.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// Style::default()
|
||||
/// .fg(Color::Black)
|
||||
/// .bg(Color::Green)
|
||||
/// .add_modifier(Modifier::ITALIC | Modifier::BOLD);
|
||||
/// ```
|
||||
///
|
||||
/// It represents an incremental change. If you apply the styles S1, S2, S3 to a cell of the
|
||||
/// terminal buffer, the style of this cell will be the result of the merge of S1, S2 and S3, not
|
||||
/// just S3.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// # use ratatui::buffer::Buffer;
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// let styles = [
|
||||
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
|
||||
/// Style::default().bg(Color::Red),
|
||||
/// Style::default().fg(Color::Yellow).remove_modifier(Modifier::ITALIC),
|
||||
/// ];
|
||||
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
/// for style in &styles {
|
||||
/// buffer.get_mut(0, 0).set_style(*style);
|
||||
/// }
|
||||
/// assert_eq!(
|
||||
/// Style {
|
||||
/// fg: Some(Color::Yellow),
|
||||
/// bg: Some(Color::Red),
|
||||
/// add_modifier: Modifier::BOLD,
|
||||
/// sub_modifier: Modifier::empty(),
|
||||
/// },
|
||||
/// buffer.get(0, 0).style(),
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// The default implementation returns a `Style` that does not modify anything. If you wish to
|
||||
/// reset all properties until that point use [`Style::reset`].
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// # use ratatui::buffer::Buffer;
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// let styles = [
|
||||
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
|
||||
/// Style::reset().fg(Color::Yellow),
|
||||
/// ];
|
||||
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
/// for style in &styles {
|
||||
/// buffer.get_mut(0, 0).set_style(*style);
|
||||
/// }
|
||||
/// assert_eq!(
|
||||
/// Style {
|
||||
/// fg: Some(Color::Yellow),
|
||||
/// bg: Some(Color::Reset),
|
||||
/// add_modifier: Modifier::empty(),
|
||||
/// sub_modifier: Modifier::empty(),
|
||||
/// },
|
||||
/// buffer.get(0, 0).style(),
|
||||
/// );
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Style {
|
||||
pub fg: Color,
|
||||
pub bg: Color,
|
||||
pub modifier: Modifier,
|
||||
pub fg: Option<Color>,
|
||||
pub bg: Option<Color>,
|
||||
pub add_modifier: Modifier,
|
||||
pub sub_modifier: Modifier,
|
||||
}
|
||||
|
||||
impl Default for Style {
|
||||
fn default() -> Style {
|
||||
Style {
|
||||
fg: Color::Reset,
|
||||
bg: Color::Reset,
|
||||
modifier: Modifier::Reset,
|
||||
}
|
||||
Style::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Style {
|
||||
pub fn reset(&mut self) {
|
||||
self.fg = Color::Reset;
|
||||
self.bg = Color::Reset;
|
||||
self.modifier = Modifier::Reset;
|
||||
pub const fn new() -> Style {
|
||||
Style {
|
||||
fg: None,
|
||||
bg: None,
|
||||
add_modifier: Modifier::empty(),
|
||||
sub_modifier: Modifier::empty(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fg(mut self, color: Color) -> Style {
|
||||
self.fg = color;
|
||||
/// Returns a `Style` resetting all properties.
|
||||
pub const fn reset() -> Style {
|
||||
Style {
|
||||
fg: Some(Color::Reset),
|
||||
bg: Some(Color::Reset),
|
||||
add_modifier: Modifier::empty(),
|
||||
sub_modifier: Modifier::all(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Changes the foreground color.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::style::{Color, Style};
|
||||
/// let style = Style::default().fg(Color::Blue);
|
||||
/// let diff = Style::default().fg(Color::Red);
|
||||
/// assert_eq!(style.patch(diff), Style::default().fg(Color::Red));
|
||||
/// ```
|
||||
pub const fn fg(mut self, color: Color) -> Style {
|
||||
self.fg = Some(color);
|
||||
self
|
||||
}
|
||||
pub fn bg(mut self, color: Color) -> Style {
|
||||
self.bg = color;
|
||||
|
||||
/// Changes the background color.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::style::{Color, Style};
|
||||
/// let style = Style::default().bg(Color::Blue);
|
||||
/// let diff = Style::default().bg(Color::Red);
|
||||
/// assert_eq!(style.patch(diff), Style::default().bg(Color::Red));
|
||||
/// ```
|
||||
pub const fn bg(mut self, color: Color) -> Style {
|
||||
self.bg = Some(color);
|
||||
self
|
||||
}
|
||||
pub fn modifier(mut self, modifier: Modifier) -> Style {
|
||||
self.modifier = modifier;
|
||||
|
||||
/// Changes the text emphasis.
|
||||
///
|
||||
/// When applied, it adds the given modifier to the `Style` modifiers.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// let style = Style::default().add_modifier(Modifier::BOLD);
|
||||
/// let diff = Style::default().add_modifier(Modifier::ITALIC);
|
||||
/// let patched = style.patch(diff);
|
||||
/// assert_eq!(patched.add_modifier, Modifier::BOLD | Modifier::ITALIC);
|
||||
/// assert_eq!(patched.sub_modifier, Modifier::empty());
|
||||
/// ```
|
||||
pub fn add_modifier(mut self, modifier: Modifier) -> Style {
|
||||
self.sub_modifier.remove(modifier);
|
||||
self.add_modifier.insert(modifier);
|
||||
self
|
||||
}
|
||||
|
||||
/// Changes the text emphasis.
|
||||
///
|
||||
/// When applied, it removes the given modifier from the `Style` modifiers.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// let style = Style::default().add_modifier(Modifier::BOLD | Modifier::ITALIC);
|
||||
/// let diff = Style::default().remove_modifier(Modifier::ITALIC);
|
||||
/// let patched = style.patch(diff);
|
||||
/// assert_eq!(patched.add_modifier, Modifier::BOLD);
|
||||
/// assert_eq!(patched.sub_modifier, Modifier::ITALIC);
|
||||
/// ```
|
||||
pub fn remove_modifier(mut self, modifier: Modifier) -> Style {
|
||||
self.add_modifier.remove(modifier);
|
||||
self.sub_modifier.insert(modifier);
|
||||
self
|
||||
}
|
||||
|
||||
/// Results in a combined style that is equivalent to applying the two individual styles to
|
||||
/// a style one after the other.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// let style_1 = Style::default().fg(Color::Yellow);
|
||||
/// let style_2 = Style::default().bg(Color::Red);
|
||||
/// let combined = style_1.patch(style_2);
|
||||
/// assert_eq!(
|
||||
/// Style::default().patch(style_1).patch(style_2),
|
||||
/// Style::default().patch(combined));
|
||||
/// ```
|
||||
pub fn patch(mut self, other: Style) -> Style {
|
||||
self.fg = other.fg.or(self.fg);
|
||||
self.bg = other.bg.or(self.bg);
|
||||
|
||||
self.add_modifier.remove(other.sub_modifier);
|
||||
self.add_modifier.insert(other.add_modifier);
|
||||
self.sub_modifier.remove(other.add_modifier);
|
||||
self.sub_modifier.insert(other.sub_modifier);
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Error type indicating a failure to parse a color string.
|
||||
#[derive(Debug)]
|
||||
pub struct ParseColorError;
|
||||
|
||||
impl std::fmt::Display for ParseColorError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Failed to parse Colors")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ParseColorError {}
|
||||
|
||||
/// Converts a string representation to a `Color` instance.
|
||||
///
|
||||
/// The `from_str` function attempts to parse the given string and convert it
|
||||
/// to the corresponding `Color` variant. It supports named colors, RGB values,
|
||||
/// and indexed colors. If the string cannot be parsed, a `ParseColorError` is returned.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use std::str::FromStr;
|
||||
/// # use ratatui::style::Color;
|
||||
/// let color: Color = Color::from_str("blue").unwrap();
|
||||
/// assert_eq!(color, Color::Blue);
|
||||
///
|
||||
/// let color: Color = Color::from_str("#FF0000").unwrap();
|
||||
/// assert_eq!(color, Color::Rgb(255, 0, 0));
|
||||
///
|
||||
/// let color: Color = Color::from_str("10").unwrap();
|
||||
/// assert_eq!(color, Color::Indexed(10));
|
||||
///
|
||||
/// let color: Result<Color, _> = Color::from_str("invalid_color");
|
||||
/// assert!(color.is_err());
|
||||
/// ```
|
||||
impl FromStr for Color {
|
||||
type Err = ParseColorError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(match s.to_lowercase().as_ref() {
|
||||
"reset" => Self::Reset,
|
||||
"black" => Self::Black,
|
||||
"red" => Self::Red,
|
||||
"green" => Self::Green,
|
||||
"yellow" => Self::Yellow,
|
||||
"blue" => Self::Blue,
|
||||
"magenta" => Self::Magenta,
|
||||
"cyan" => Self::Cyan,
|
||||
"gray" => Self::Gray,
|
||||
"darkgray" | "dark gray" => Self::DarkGray,
|
||||
"lightred" | "light red" => Self::LightRed,
|
||||
"lightgreen" | "light green" => Self::LightGreen,
|
||||
"lightyellow" | "light yellow" => Self::LightYellow,
|
||||
"lightblue" | "light blue" => Self::LightBlue,
|
||||
"lightmagenta" | "light magenta" => Self::LightMagenta,
|
||||
"lightcyan" | "light cyan" => Self::LightCyan,
|
||||
"white" => Self::White,
|
||||
_ => {
|
||||
if let Ok(index) = s.parse::<u8>() {
|
||||
Self::Indexed(index)
|
||||
} else if let (Ok(r), Ok(g), Ok(b)) = {
|
||||
if !s.starts_with('#') || s.len() != 7 {
|
||||
return Err(ParseColorError);
|
||||
}
|
||||
(
|
||||
u8::from_str_radix(&s[1..3], 16),
|
||||
u8::from_str_radix(&s[3..5], 16),
|
||||
u8::from_str_radix(&s[5..7], 16),
|
||||
)
|
||||
} {
|
||||
Self::Rgb(r, g, b)
|
||||
} else {
|
||||
return Err(ParseColorError);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn styles() -> Vec<Style> {
|
||||
vec![
|
||||
Style::default(),
|
||||
Style::default().fg(Color::Yellow),
|
||||
Style::default().bg(Color::Yellow),
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
Style::default().remove_modifier(Modifier::BOLD),
|
||||
Style::default().add_modifier(Modifier::ITALIC),
|
||||
Style::default().remove_modifier(Modifier::ITALIC),
|
||||
Style::default().add_modifier(Modifier::ITALIC | Modifier::BOLD),
|
||||
Style::default().remove_modifier(Modifier::ITALIC | Modifier::BOLD),
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combined_patch_gives_same_result_as_individual_patch() {
|
||||
let styles = styles();
|
||||
for &a in &styles {
|
||||
for &b in &styles {
|
||||
for &c in &styles {
|
||||
for &d in &styles {
|
||||
let combined = a.patch(b.patch(c.patch(d)));
|
||||
|
||||
assert_eq!(
|
||||
Style::default().patch(a).patch(b).patch(c).patch(d),
|
||||
Style::default().patch(combined)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combine_individual_modifiers() {
|
||||
use crate::{buffer::Buffer, layout::Rect};
|
||||
|
||||
let mods = vec![
|
||||
Modifier::BOLD,
|
||||
Modifier::DIM,
|
||||
Modifier::ITALIC,
|
||||
Modifier::UNDERLINED,
|
||||
Modifier::SLOW_BLINK,
|
||||
Modifier::RAPID_BLINK,
|
||||
Modifier::REVERSED,
|
||||
Modifier::HIDDEN,
|
||||
Modifier::CROSSED_OUT,
|
||||
];
|
||||
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
|
||||
for m in &mods {
|
||||
buffer.get_mut(0, 0).set_style(Style::reset());
|
||||
buffer
|
||||
.get_mut(0, 0)
|
||||
.set_style(Style::default().add_modifier(*m));
|
||||
let style = buffer.get(0, 0).style();
|
||||
assert!(style.add_modifier.contains(*m));
|
||||
assert!(!style.sub_modifier.contains(*m));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_modifier_debug() {
|
||||
assert_eq!(format!("{:?}", Modifier::empty()), "NONE");
|
||||
assert_eq!(format!("{:?}", Modifier::BOLD), "BOLD");
|
||||
assert_eq!(format!("{:?}", Modifier::DIM), "DIM");
|
||||
assert_eq!(format!("{:?}", Modifier::ITALIC), "ITALIC");
|
||||
assert_eq!(format!("{:?}", Modifier::UNDERLINED), "UNDERLINED");
|
||||
assert_eq!(format!("{:?}", Modifier::SLOW_BLINK), "SLOW_BLINK");
|
||||
assert_eq!(format!("{:?}", Modifier::RAPID_BLINK), "RAPID_BLINK");
|
||||
assert_eq!(format!("{:?}", Modifier::REVERSED), "REVERSED");
|
||||
assert_eq!(format!("{:?}", Modifier::HIDDEN), "HIDDEN");
|
||||
assert_eq!(format!("{:?}", Modifier::CROSSED_OUT), "CROSSED_OUT");
|
||||
assert_eq!(
|
||||
format!("{:?}", Modifier::BOLD | Modifier::DIM),
|
||||
"BOLD | DIM"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{:?}", Modifier::all()),
|
||||
"BOLD | DIM | ITALIC | UNDERLINED | SLOW_BLINK | RAPID_BLINK | REVERSED | HIDDEN | CROSSED_OUT"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rgb_color() {
|
||||
let color: Color = Color::from_str("#FF0000").unwrap();
|
||||
assert_eq!(color, Color::Rgb(255, 0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_indexed_color() {
|
||||
let color: Color = Color::from_str("10").unwrap();
|
||||
assert_eq!(color, Color::Indexed(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_color() {
|
||||
let color: Color = Color::from_str("lightblue").unwrap();
|
||||
assert_eq!(color, Color::LightBlue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_colors() {
|
||||
let bad_colors = [
|
||||
"invalid_color", // not a color string
|
||||
"abcdef0", // 7 chars is not a color
|
||||
" bcdefa", // doesn't start with a '#'
|
||||
"blue ", // has space at end
|
||||
" blue", // has space at start
|
||||
"#abcdef00", // too many chars
|
||||
];
|
||||
|
||||
for bad_color in bad_colors {
|
||||
assert!(
|
||||
Color::from_str(bad_color).is_err(),
|
||||
"bad color: '{bad_color}'"
|
||||
);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn style_can_be_const() {
|
||||
const _DEFAULT_STYLE: Style = Style::new().fg(Color::Red).bg(Color::Black);
|
||||
const _RESET: Style = Style::reset();
|
||||
}
|
||||
}
|
||||
|
||||
228
src/style/stylized.rs
Normal file
228
src/style/stylized.rs
Normal file
@@ -0,0 +1,228 @@
|
||||
use crate::{
|
||||
style::{Color, Modifier, Style},
|
||||
text::Span,
|
||||
};
|
||||
|
||||
pub trait Styled {
|
||||
type Item;
|
||||
|
||||
fn style(&self) -> Style;
|
||||
fn set_style(self, style: Style) -> Self::Item;
|
||||
}
|
||||
|
||||
// Otherwise rustfmt behaves weirdly for some reason
|
||||
macro_rules! calculated_docs {
|
||||
($(#[doc = $doc:expr] $item:item)*) => { $(#[doc = $doc] $item)* };
|
||||
}
|
||||
|
||||
macro_rules! modifier_method {
|
||||
($method_name:ident Modifier::$modifier:ident) => {
|
||||
calculated_docs! {
|
||||
#[doc = concat!(
|
||||
"Applies the [`",
|
||||
stringify!($modifier),
|
||||
"`](crate::style::Modifier::",
|
||||
stringify!($modifier),
|
||||
") modifier.",
|
||||
)]
|
||||
fn $method_name(self) -> T {
|
||||
self.modifier(Modifier::$modifier)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! color_method {
|
||||
($method_name_fg:ident, $method_name_bg:ident Color::$color:ident) => {
|
||||
calculated_docs! {
|
||||
#[doc = concat!(
|
||||
"Sets the foreground color to [`",
|
||||
stringify!($color),
|
||||
"`](Color::",
|
||||
stringify!($color),
|
||||
")."
|
||||
)]
|
||||
fn $method_name_fg(self) -> T {
|
||||
self.fg(Color::$color)
|
||||
}
|
||||
|
||||
#[doc = concat!(
|
||||
"Sets the background color to [`",
|
||||
stringify!($color),
|
||||
"`](Color::",
|
||||
stringify!($color),
|
||||
")."
|
||||
)]
|
||||
fn $method_name_bg(self) -> T {
|
||||
self.bg(Color::$color)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// The trait that enables something to be have a style.
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use ratatui::{
|
||||
/// style::{Color, Modifier, Style, Styled, Stylize},
|
||||
/// text::Span,
|
||||
/// };
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// "hello".red().on_blue().bold(),
|
||||
/// Span::styled("hello", Style::default().fg(Color::Red).bg(Color::Blue).add_modifier(Modifier::BOLD))
|
||||
/// )
|
||||
pub trait Stylize<'a, T>: Sized {
|
||||
// Colors
|
||||
fn fg<S: Into<Color>>(self, color: S) -> T;
|
||||
fn bg(self, color: Color) -> T;
|
||||
|
||||
color_method!(black, on_black Color::Black);
|
||||
color_method!(red, on_red Color::Red);
|
||||
color_method!(green, on_green Color::Green);
|
||||
color_method!(yellow, on_yellow Color::Yellow);
|
||||
color_method!(blue, on_blue Color::Blue);
|
||||
color_method!(magenta, on_magenta Color::Magenta);
|
||||
color_method!(cyan, on_cyan Color::Cyan);
|
||||
color_method!(gray, on_gray Color::Gray);
|
||||
color_method!(dark_gray, on_dark_gray Color::DarkGray);
|
||||
color_method!(light_red, on_light_red Color::LightRed);
|
||||
color_method!(light_green, on_light_green Color::LightGreen);
|
||||
color_method!(light_yellow, on_light_yellow Color::LightYellow);
|
||||
color_method!(light_blue, on_light_blue Color::LightBlue);
|
||||
color_method!(light_magenta, on_light_magenta Color::LightMagenta);
|
||||
color_method!(light_cyan, on_light_cyan Color::LightCyan);
|
||||
color_method!(white, on_white Color::White);
|
||||
|
||||
// Styles
|
||||
fn reset(self) -> T;
|
||||
|
||||
// Modifiers
|
||||
fn modifier(self, modifier: Modifier) -> T;
|
||||
|
||||
modifier_method!(bold Modifier::BOLD);
|
||||
modifier_method!(dimmed Modifier::DIM);
|
||||
modifier_method!(italic Modifier::ITALIC);
|
||||
modifier_method!(underline Modifier::UNDERLINED);
|
||||
modifier_method!(slow_blink Modifier::SLOW_BLINK);
|
||||
modifier_method!(rapid_blink Modifier::RAPID_BLINK);
|
||||
modifier_method!(reversed Modifier::REVERSED);
|
||||
modifier_method!(hidden Modifier::HIDDEN);
|
||||
modifier_method!(crossed_out Modifier::CROSSED_OUT);
|
||||
}
|
||||
|
||||
impl<'a, T, U> Stylize<'a, T> for U
|
||||
where
|
||||
U: Styled<Item = T>,
|
||||
{
|
||||
fn fg<S: Into<Color>>(self, color: S) -> T {
|
||||
let style = self.style().fg(color.into());
|
||||
self.set_style(style)
|
||||
}
|
||||
|
||||
fn modifier(self, modifier: Modifier) -> T {
|
||||
let style = self.style().add_modifier(modifier);
|
||||
self.set_style(style)
|
||||
}
|
||||
|
||||
fn bg(self, color: Color) -> T {
|
||||
let style = self.style().bg(color);
|
||||
self.set_style(style)
|
||||
}
|
||||
|
||||
fn reset(self) -> T {
|
||||
self.set_style(Style::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Styled for &'a str {
|
||||
type Item = Span<'a>;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
Style::default()
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self::Item {
|
||||
Span::styled(self, style)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn reset() {
|
||||
assert_eq!(
|
||||
"hello".on_cyan().light_red().bold().underline().reset(),
|
||||
Span::from("hello")
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fg() {
|
||||
let cyan_fg = Style::default().fg(Color::Cyan);
|
||||
|
||||
assert_eq!("hello".cyan(), Span::styled("hello", cyan_fg));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bg() {
|
||||
let cyan_bg = Style::default().bg(Color::Cyan);
|
||||
|
||||
assert_eq!("hello".on_cyan(), Span::styled("hello", cyan_bg));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_modifier() {
|
||||
let cyan_bold = Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD);
|
||||
|
||||
assert_eq!("hello".cyan().bold(), Span::styled("hello", cyan_bold))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fg_bg() {
|
||||
let cyan_fg_bg = Style::default().bg(Color::Cyan).fg(Color::Cyan);
|
||||
|
||||
assert_eq!("hello".cyan().on_cyan(), Span::styled("hello", cyan_fg_bg))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_attributes() {
|
||||
let cyan_bg = Style::default().bg(Color::Cyan);
|
||||
let cyan_fg = Style::default().fg(Color::Cyan);
|
||||
|
||||
// Behavior: the last one set is the definitive one
|
||||
assert_eq!("hello".on_red().on_cyan(), Span::styled("hello", cyan_bg));
|
||||
assert_eq!("hello".red().cyan(), Span::styled("hello", cyan_fg));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_chained() {
|
||||
let all_modifier_black = Style::default()
|
||||
.bg(Color::Black)
|
||||
.fg(Color::Black)
|
||||
.add_modifier(
|
||||
Modifier::UNDERLINED
|
||||
| Modifier::BOLD
|
||||
| Modifier::DIM
|
||||
| Modifier::SLOW_BLINK
|
||||
| Modifier::REVERSED
|
||||
| Modifier::CROSSED_OUT,
|
||||
);
|
||||
assert_eq!(
|
||||
"hello"
|
||||
.on_black()
|
||||
.black()
|
||||
.bold()
|
||||
.underline()
|
||||
.dimmed()
|
||||
.slow_blink()
|
||||
.crossed_out()
|
||||
.reversed(),
|
||||
Span::styled("hello", all_modifier_black)
|
||||
);
|
||||
}
|
||||
}
|
||||
209
src/symbols.rs
209
src/symbols.rs
@@ -1,36 +1,235 @@
|
||||
pub mod block {
|
||||
pub const FULL: &str = "█";
|
||||
pub const SEVEN_EIGHTHS: &str = "▉";
|
||||
pub const THREE_QUATERS: &str = "▊";
|
||||
pub const THREE_QUARTERS: &str = "▊";
|
||||
pub const FIVE_EIGHTHS: &str = "▋";
|
||||
pub const HALF: &str = "▌";
|
||||
pub const THREE_EIGHTHS: &str = "▍";
|
||||
pub const ONE_QUATER: &str = "▎";
|
||||
pub const ONE_QUARTER: &str = "▎";
|
||||
pub const ONE_EIGHTH: &str = "▏";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Set {
|
||||
pub full: &'static str,
|
||||
pub seven_eighths: &'static str,
|
||||
pub three_quarters: &'static str,
|
||||
pub five_eighths: &'static str,
|
||||
pub half: &'static str,
|
||||
pub three_eighths: &'static str,
|
||||
pub one_quarter: &'static str,
|
||||
pub one_eighth: &'static str,
|
||||
pub empty: &'static str,
|
||||
}
|
||||
|
||||
pub const THREE_LEVELS: Set = Set {
|
||||
full: FULL,
|
||||
seven_eighths: FULL,
|
||||
three_quarters: HALF,
|
||||
five_eighths: HALF,
|
||||
half: HALF,
|
||||
three_eighths: HALF,
|
||||
one_quarter: HALF,
|
||||
one_eighth: " ",
|
||||
empty: " ",
|
||||
};
|
||||
|
||||
pub const NINE_LEVELS: Set = Set {
|
||||
full: FULL,
|
||||
seven_eighths: SEVEN_EIGHTHS,
|
||||
three_quarters: THREE_QUARTERS,
|
||||
five_eighths: FIVE_EIGHTHS,
|
||||
half: HALF,
|
||||
three_eighths: THREE_EIGHTHS,
|
||||
one_quarter: ONE_QUARTER,
|
||||
one_eighth: ONE_EIGHTH,
|
||||
empty: " ",
|
||||
};
|
||||
}
|
||||
|
||||
pub mod bar {
|
||||
pub const FULL: &str = "█";
|
||||
pub const SEVEN_EIGHTHS: &str = "▇";
|
||||
pub const THREE_QUATERS: &str = "▆";
|
||||
pub const THREE_QUARTERS: &str = "▆";
|
||||
pub const FIVE_EIGHTHS: &str = "▅";
|
||||
pub const HALF: &str = "▄";
|
||||
pub const THREE_EIGHTHS: &str = "▃";
|
||||
pub const ONE_QUATER: &str = "▂";
|
||||
pub const ONE_QUARTER: &str = "▂";
|
||||
pub const ONE_EIGHTH: &str = "▁";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Set {
|
||||
pub full: &'static str,
|
||||
pub seven_eighths: &'static str,
|
||||
pub three_quarters: &'static str,
|
||||
pub five_eighths: &'static str,
|
||||
pub half: &'static str,
|
||||
pub three_eighths: &'static str,
|
||||
pub one_quarter: &'static str,
|
||||
pub one_eighth: &'static str,
|
||||
pub empty: &'static str,
|
||||
}
|
||||
|
||||
pub const THREE_LEVELS: Set = Set {
|
||||
full: FULL,
|
||||
seven_eighths: FULL,
|
||||
three_quarters: HALF,
|
||||
five_eighths: HALF,
|
||||
half: HALF,
|
||||
three_eighths: HALF,
|
||||
one_quarter: HALF,
|
||||
one_eighth: " ",
|
||||
empty: " ",
|
||||
};
|
||||
|
||||
pub const NINE_LEVELS: Set = Set {
|
||||
full: FULL,
|
||||
seven_eighths: SEVEN_EIGHTHS,
|
||||
three_quarters: THREE_QUARTERS,
|
||||
five_eighths: FIVE_EIGHTHS,
|
||||
half: HALF,
|
||||
three_eighths: THREE_EIGHTHS,
|
||||
one_quarter: ONE_QUARTER,
|
||||
one_eighth: ONE_EIGHTH,
|
||||
empty: " ",
|
||||
};
|
||||
}
|
||||
|
||||
pub mod line {
|
||||
pub const TOP_RIGHT: &str = "┐";
|
||||
pub const VERTICAL: &str = "│";
|
||||
pub const DOUBLE_VERTICAL: &str = "║";
|
||||
pub const THICK_VERTICAL: &str = "┃";
|
||||
|
||||
pub const HORIZONTAL: &str = "─";
|
||||
pub const DOUBLE_HORIZONTAL: &str = "═";
|
||||
pub const THICK_HORIZONTAL: &str = "━";
|
||||
|
||||
pub const TOP_RIGHT: &str = "┐";
|
||||
pub const ROUNDED_TOP_RIGHT: &str = "╮";
|
||||
pub const DOUBLE_TOP_RIGHT: &str = "╗";
|
||||
pub const THICK_TOP_RIGHT: &str = "┓";
|
||||
|
||||
pub const TOP_LEFT: &str = "┌";
|
||||
pub const ROUNDED_TOP_LEFT: &str = "╭";
|
||||
pub const DOUBLE_TOP_LEFT: &str = "╔";
|
||||
pub const THICK_TOP_LEFT: &str = "┏";
|
||||
|
||||
pub const BOTTOM_RIGHT: &str = "┘";
|
||||
pub const ROUNDED_BOTTOM_RIGHT: &str = "╯";
|
||||
pub const DOUBLE_BOTTOM_RIGHT: &str = "╝";
|
||||
pub const THICK_BOTTOM_RIGHT: &str = "┛";
|
||||
|
||||
pub const BOTTOM_LEFT: &str = "└";
|
||||
pub const ROUNDED_BOTTOM_LEFT: &str = "╰";
|
||||
pub const DOUBLE_BOTTOM_LEFT: &str = "╚";
|
||||
pub const THICK_BOTTOM_LEFT: &str = "┗";
|
||||
|
||||
pub const VERTICAL_LEFT: &str = "┤";
|
||||
pub const DOUBLE_VERTICAL_LEFT: &str = "╣";
|
||||
pub const THICK_VERTICAL_LEFT: &str = "┫";
|
||||
|
||||
pub const VERTICAL_RIGHT: &str = "├";
|
||||
pub const DOUBLE_VERTICAL_RIGHT: &str = "╠";
|
||||
pub const THICK_VERTICAL_RIGHT: &str = "┣";
|
||||
|
||||
pub const HORIZONTAL_DOWN: &str = "┬";
|
||||
pub const DOUBLE_HORIZONTAL_DOWN: &str = "╦";
|
||||
pub const THICK_HORIZONTAL_DOWN: &str = "┳";
|
||||
|
||||
pub const HORIZONTAL_UP: &str = "┴";
|
||||
pub const DOUBLE_HORIZONTAL_UP: &str = "╩";
|
||||
pub const THICK_HORIZONTAL_UP: &str = "┻";
|
||||
|
||||
pub const CROSS: &str = "┼";
|
||||
pub const DOUBLE_CROSS: &str = "╬";
|
||||
pub const THICK_CROSS: &str = "╋";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Set {
|
||||
pub vertical: &'static str,
|
||||
pub horizontal: &'static str,
|
||||
pub top_right: &'static str,
|
||||
pub top_left: &'static str,
|
||||
pub bottom_right: &'static str,
|
||||
pub bottom_left: &'static str,
|
||||
pub vertical_left: &'static str,
|
||||
pub vertical_right: &'static str,
|
||||
pub horizontal_down: &'static str,
|
||||
pub horizontal_up: &'static str,
|
||||
pub cross: &'static str,
|
||||
}
|
||||
|
||||
pub const NORMAL: Set = Set {
|
||||
vertical: VERTICAL,
|
||||
horizontal: HORIZONTAL,
|
||||
top_right: TOP_RIGHT,
|
||||
top_left: TOP_LEFT,
|
||||
bottom_right: BOTTOM_RIGHT,
|
||||
bottom_left: BOTTOM_LEFT,
|
||||
vertical_left: VERTICAL_LEFT,
|
||||
vertical_right: VERTICAL_RIGHT,
|
||||
horizontal_down: HORIZONTAL_DOWN,
|
||||
horizontal_up: HORIZONTAL_UP,
|
||||
cross: CROSS,
|
||||
};
|
||||
|
||||
pub const ROUNDED: Set = Set {
|
||||
top_right: ROUNDED_TOP_RIGHT,
|
||||
top_left: ROUNDED_TOP_LEFT,
|
||||
bottom_right: ROUNDED_BOTTOM_RIGHT,
|
||||
bottom_left: ROUNDED_BOTTOM_LEFT,
|
||||
..NORMAL
|
||||
};
|
||||
|
||||
pub const DOUBLE: Set = Set {
|
||||
vertical: DOUBLE_VERTICAL,
|
||||
horizontal: DOUBLE_HORIZONTAL,
|
||||
top_right: DOUBLE_TOP_RIGHT,
|
||||
top_left: DOUBLE_TOP_LEFT,
|
||||
bottom_right: DOUBLE_BOTTOM_RIGHT,
|
||||
bottom_left: DOUBLE_BOTTOM_LEFT,
|
||||
vertical_left: DOUBLE_VERTICAL_LEFT,
|
||||
vertical_right: DOUBLE_VERTICAL_RIGHT,
|
||||
horizontal_down: DOUBLE_HORIZONTAL_DOWN,
|
||||
horizontal_up: DOUBLE_HORIZONTAL_UP,
|
||||
cross: DOUBLE_CROSS,
|
||||
};
|
||||
|
||||
pub const THICK: Set = Set {
|
||||
vertical: THICK_VERTICAL,
|
||||
horizontal: THICK_HORIZONTAL,
|
||||
top_right: THICK_TOP_RIGHT,
|
||||
top_left: THICK_TOP_LEFT,
|
||||
bottom_right: THICK_BOTTOM_RIGHT,
|
||||
bottom_left: THICK_BOTTOM_LEFT,
|
||||
vertical_left: THICK_VERTICAL_LEFT,
|
||||
vertical_right: THICK_VERTICAL_RIGHT,
|
||||
horizontal_down: THICK_HORIZONTAL_DOWN,
|
||||
horizontal_up: THICK_HORIZONTAL_UP,
|
||||
cross: THICK_CROSS,
|
||||
};
|
||||
}
|
||||
|
||||
pub const DOT: &str = "•";
|
||||
|
||||
pub mod braille {
|
||||
pub const BLANK: u16 = 0x2800;
|
||||
pub const DOTS: [[u16; 2]; 4] = [
|
||||
[0x0001, 0x0008],
|
||||
[0x0002, 0x0010],
|
||||
[0x0004, 0x0020],
|
||||
[0x0040, 0x0080],
|
||||
];
|
||||
}
|
||||
|
||||
/// Marker to use when plotting data points
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Marker {
|
||||
/// One point per cell in shape of dot
|
||||
Dot,
|
||||
/// One point per cell in shape of a block
|
||||
Block,
|
||||
/// One point per cell in the shape of a bar
|
||||
Bar,
|
||||
/// Up to 8 points per cell
|
||||
Braille,
|
||||
}
|
||||
|
||||
546
src/terminal.rs
546
src/terminal.rs
@@ -1,49 +1,210 @@
|
||||
use std::io;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use backend::Backend;
|
||||
use buffer::Buffer;
|
||||
use layout::{split, Group, Rect};
|
||||
use widgets::Widget;
|
||||
use crate::{
|
||||
backend::{Backend, ClearType},
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
widgets::{StatefulWidget, Widget},
|
||||
};
|
||||
|
||||
/// Holds a computed layout and keeps track of its use between successive draw calls
|
||||
#[derive(Debug)]
|
||||
pub struct LayoutEntry {
|
||||
chunks: Vec<Rect>,
|
||||
hot: bool,
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Viewport {
|
||||
Fullscreen,
|
||||
Inline(u16),
|
||||
Fixed(Rect),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// Options to pass to [`Terminal::with_options`]
|
||||
pub struct TerminalOptions {
|
||||
/// Viewport used to draw to the terminal
|
||||
pub viewport: Viewport,
|
||||
}
|
||||
|
||||
/// Interface to the terminal backed by Termion
|
||||
#[derive(Debug)]
|
||||
pub struct Terminal<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
backend: B,
|
||||
/// Cache to prevent the layout to be computed at each draw call
|
||||
layout_cache: HashMap<(Group, Rect), LayoutEntry>,
|
||||
/// Holds the results of the current and previous draw calls. The two are compared at the end
|
||||
/// of each draw pass to output the necessary updates to the terminal
|
||||
buffers: [Buffer; 2],
|
||||
/// Index of the current buffer in the previous array
|
||||
current: usize,
|
||||
/// Whether the cursor is currently hidden
|
||||
hidden_cursor: bool,
|
||||
/// Viewport
|
||||
viewport: Viewport,
|
||||
viewport_area: Rect,
|
||||
/// Last known size of the terminal. Used to detect if the internal buffers have to be resized.
|
||||
last_known_size: Rect,
|
||||
/// Last known position of the cursor. Used to find the new area when the viewport is inlined
|
||||
/// and the terminal resized.
|
||||
last_known_cursor_pos: (u16, u16),
|
||||
}
|
||||
|
||||
/// Represents a consistent terminal interface for rendering.
|
||||
pub struct Frame<'a, B: 'a>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
terminal: &'a mut Terminal<B>,
|
||||
|
||||
/// Where should the cursor be after drawing this frame?
|
||||
///
|
||||
/// If `None`, the cursor is hidden and its position is controlled by the backend. If `Some((x,
|
||||
/// y))`, the cursor is shown and placed at `(x, y)` after the call to `Terminal::draw()`.
|
||||
cursor_position: Option<(u16, u16)>,
|
||||
}
|
||||
|
||||
impl<'a, B> Frame<'a, B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
/// Frame size, guaranteed not to change when rendering.
|
||||
pub fn size(&self) -> Rect {
|
||||
self.terminal.viewport_area
|
||||
}
|
||||
|
||||
/// Render a [`Widget`] to the current buffer using [`Widget::render`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::Terminal;
|
||||
/// # use ratatui::backend::TestBackend;
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// # use ratatui::widgets::Block;
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// let block = Block::default();
|
||||
/// let area = Rect::new(0, 0, 5, 5);
|
||||
/// let mut frame = terminal.get_frame();
|
||||
/// frame.render_widget(block, area);
|
||||
/// ```
|
||||
pub fn render_widget<W>(&mut self, widget: W, area: Rect)
|
||||
where
|
||||
W: Widget,
|
||||
{
|
||||
widget.render(area, self.terminal.current_buffer_mut());
|
||||
}
|
||||
|
||||
/// Render a [`StatefulWidget`] to the current buffer using [`StatefulWidget::render`].
|
||||
///
|
||||
/// The last argument should be an instance of the [`StatefulWidget::State`] associated to the
|
||||
/// given [`StatefulWidget`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::Terminal;
|
||||
/// # use ratatui::backend::TestBackend;
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// # use ratatui::widgets::{List, ListItem, ListState};
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// let mut state = ListState::default();
|
||||
/// state.select(Some(1));
|
||||
/// let items = vec![
|
||||
/// ListItem::new("Item 1"),
|
||||
/// ListItem::new("Item 2"),
|
||||
/// ];
|
||||
/// let list = List::new(items);
|
||||
/// let area = Rect::new(0, 0, 5, 5);
|
||||
/// let mut frame = terminal.get_frame();
|
||||
/// frame.render_stateful_widget(list, area, &mut state);
|
||||
/// ```
|
||||
pub fn render_stateful_widget<W>(&mut self, widget: W, area: Rect, state: &mut W::State)
|
||||
where
|
||||
W: StatefulWidget,
|
||||
{
|
||||
widget.render(area, self.terminal.current_buffer_mut(), state);
|
||||
}
|
||||
|
||||
/// After drawing this frame, make the cursor visible and put it at the specified (x, y)
|
||||
/// coordinates. If this method is not called, the cursor will be hidden.
|
||||
///
|
||||
/// Note that this will interfere with calls to `Terminal::hide_cursor()`,
|
||||
/// `Terminal::show_cursor()`, and `Terminal::set_cursor()`. Pick one of the APIs and stick
|
||||
/// with it.
|
||||
pub fn set_cursor(&mut self, x: u16, y: u16) {
|
||||
self.cursor_position = Some((x, y));
|
||||
}
|
||||
}
|
||||
|
||||
/// `CompletedFrame` represents the state of the terminal after all changes performed in the last
|
||||
/// [`Terminal::draw`] call have been applied. Therefore, it is only valid until the next call to
|
||||
/// [`Terminal::draw`].
|
||||
pub struct CompletedFrame<'a> {
|
||||
pub buffer: &'a Buffer,
|
||||
pub area: Rect,
|
||||
}
|
||||
|
||||
impl<B> Drop for Terminal<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
fn drop(&mut self) {
|
||||
// Attempt to restore the cursor state
|
||||
if self.hidden_cursor {
|
||||
if let Err(err) = self.show_cursor() {
|
||||
eprintln!("Failed to show the cursor: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> Terminal<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
/// Wrapper around Termion initialization. Each buffer is initialized with a blank string and
|
||||
/// Wrapper around Terminal initialization. Each buffer is initialized with a blank string and
|
||||
/// default colors for the foreground and the background
|
||||
pub fn new(backend: B) -> Result<Terminal<B>, io::Error> {
|
||||
let size = try!(backend.size());
|
||||
pub fn new(backend: B) -> io::Result<Terminal<B>> {
|
||||
Terminal::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: Viewport::Fullscreen,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn with_options(mut backend: B, options: TerminalOptions) -> io::Result<Terminal<B>> {
|
||||
let size = match options.viewport {
|
||||
Viewport::Fullscreen | Viewport::Inline(_) => backend.size()?,
|
||||
Viewport::Fixed(area) => area,
|
||||
};
|
||||
let (viewport_area, cursor_pos) = match options.viewport {
|
||||
Viewport::Fullscreen => (size, (0, 0)),
|
||||
Viewport::Inline(height) => compute_inline_size(&mut backend, height, size, 0)?,
|
||||
Viewport::Fixed(area) => (area, (area.left(), area.top())),
|
||||
};
|
||||
Ok(Terminal {
|
||||
backend: backend,
|
||||
layout_cache: HashMap::new(),
|
||||
buffers: [Buffer::empty(size), Buffer::empty(size)],
|
||||
backend,
|
||||
buffers: [Buffer::empty(viewport_area), Buffer::empty(viewport_area)],
|
||||
current: 0,
|
||||
hidden_cursor: false,
|
||||
viewport: options.viewport,
|
||||
viewport_area,
|
||||
last_known_size: size,
|
||||
last_known_cursor_pos: cursor_pos,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a Frame object which provides a consistent view into the terminal state for rendering.
|
||||
pub fn get_frame(&mut self) -> Frame<B> {
|
||||
Frame {
|
||||
terminal: self,
|
||||
cursor_position: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current_buffer_mut(&mut self) -> &mut Buffer {
|
||||
&mut self.buffers[self.current]
|
||||
}
|
||||
|
||||
pub fn backend(&self) -> &B {
|
||||
&self.backend
|
||||
}
|
||||
@@ -52,105 +213,274 @@ where
|
||||
&mut self.backend
|
||||
}
|
||||
|
||||
/// Check if we have already computed a layout for a given group, otherwise it creates one and
|
||||
/// add it to the layout cache. Moreover the function marks the queried entries so that we can
|
||||
/// clean outdated ones at the end of the draw call.
|
||||
pub fn compute_layout(&mut self, group: &Group, area: &Rect) -> Vec<Rect> {
|
||||
let entry = self.layout_cache
|
||||
.entry((group.clone(), *area))
|
||||
.or_insert_with(|| {
|
||||
let chunks = split(area, &group.direction, group.margin, &group.sizes);
|
||||
debug!(
|
||||
"New layout computed:\n* Group = {:?}\n* Chunks = {:?}",
|
||||
group, chunks
|
||||
);
|
||||
LayoutEntry {
|
||||
chunks: chunks,
|
||||
hot: true,
|
||||
}
|
||||
});
|
||||
entry.hot = true;
|
||||
entry.chunks.clone()
|
||||
}
|
||||
|
||||
/// Builds a string representing the minimal escape sequences and characters set necessary to
|
||||
/// update the UI and writes it to stdout.
|
||||
pub fn flush(&mut self) -> Result<(), io::Error> {
|
||||
let width = self.buffers[self.current].area.width;
|
||||
let content = self.buffers[self.current]
|
||||
.content
|
||||
.iter()
|
||||
.zip(self.buffers[1 - self.current].content.iter())
|
||||
.enumerate()
|
||||
.filter_map(|(i, (c, p))| {
|
||||
if c != p {
|
||||
let i = i as u16;
|
||||
let x = i % width;
|
||||
let y = i / width;
|
||||
Some((x, y, c))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
self.backend.draw(content)
|
||||
}
|
||||
|
||||
/// Calls the draw method of a given widget on the current buffer
|
||||
pub fn render<W>(&mut self, widget: &mut W, area: &Rect)
|
||||
where
|
||||
W: Widget,
|
||||
{
|
||||
widget.draw(area, &mut self.buffers[self.current]);
|
||||
}
|
||||
|
||||
/// Updates the interface so that internal buffers matches the current size of the terminal.
|
||||
/// This leads to a full redraw of the screen.
|
||||
pub fn resize(&mut self, area: Rect) -> Result<(), io::Error> {
|
||||
self.buffers[self.current].resize(area);
|
||||
self.buffers[1 - self.current].resize(area);
|
||||
self.buffers[1 - self.current].reset();
|
||||
self.layout_cache.clear();
|
||||
self.backend.clear()
|
||||
}
|
||||
|
||||
/// Flushes the current internal state and prepares the interface for the next draw call
|
||||
pub fn draw(&mut self) -> Result<(), io::Error> {
|
||||
// Draw to stdout
|
||||
self.flush()?;
|
||||
|
||||
// Clean layout cache
|
||||
let hot = self.layout_cache
|
||||
.drain()
|
||||
.filter(|&(_, ref v)| v.hot)
|
||||
.collect::<Vec<((Group, Rect), LayoutEntry)>>();
|
||||
|
||||
for (key, value) in hot {
|
||||
self.layout_cache.insert(key, value);
|
||||
/// Obtains a difference between the previous and the current buffer and passes it to the
|
||||
/// current backend for drawing.
|
||||
pub fn flush(&mut self) -> io::Result<()> {
|
||||
let previous_buffer = &self.buffers[1 - self.current];
|
||||
let current_buffer = &self.buffers[self.current];
|
||||
let updates = previous_buffer.diff(current_buffer);
|
||||
if let Some((col, row, _)) = updates.last() {
|
||||
self.last_known_cursor_pos = (*col, *row);
|
||||
}
|
||||
self.backend.draw(updates.into_iter())
|
||||
}
|
||||
|
||||
for e in self.layout_cache.values_mut() {
|
||||
e.hot = false;
|
||||
}
|
||||
/// Updates the Terminal so that internal buffers match the requested size. Requested size will
|
||||
/// be saved so the size can remain consistent when rendering.
|
||||
/// This leads to a full clear of the screen.
|
||||
pub fn resize(&mut self, size: Rect) -> io::Result<()> {
|
||||
let next_area = match self.viewport {
|
||||
Viewport::Fullscreen => size,
|
||||
Viewport::Inline(height) => {
|
||||
let offset_in_previous_viewport = self
|
||||
.last_known_cursor_pos
|
||||
.1
|
||||
.saturating_sub(self.viewport_area.top());
|
||||
compute_inline_size(&mut self.backend, height, size, offset_in_previous_viewport)?.0
|
||||
}
|
||||
Viewport::Fixed(area) => area,
|
||||
};
|
||||
self.set_viewport_area(next_area);
|
||||
self.clear()?;
|
||||
|
||||
// Swap buffers
|
||||
self.buffers[1 - self.current].reset();
|
||||
self.current = 1 - self.current;
|
||||
|
||||
// Flush
|
||||
self.backend.flush()?;
|
||||
self.last_known_size = size;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn hide_cursor(&mut self) -> Result<(), io::Error> {
|
||||
self.backend.hide_cursor()
|
||||
fn set_viewport_area(&mut self, area: Rect) {
|
||||
self.buffers[self.current].resize(area);
|
||||
self.buffers[1 - self.current].resize(area);
|
||||
self.viewport_area = area;
|
||||
}
|
||||
pub fn show_cursor(&mut self) -> Result<(), io::Error> {
|
||||
self.backend.show_cursor()
|
||||
|
||||
/// Queries the backend for size and resizes if it doesn't match the previous size.
|
||||
pub fn autoresize(&mut self) -> io::Result<()> {
|
||||
// fixed viewports do not get autoresized
|
||||
if matches!(self.viewport, Viewport::Fullscreen | Viewport::Inline(_)) {
|
||||
let size = self.size()?;
|
||||
if size != self.last_known_size {
|
||||
self.resize(size)?;
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
pub fn clear(&mut self) -> Result<(), io::Error> {
|
||||
self.backend.clear()
|
||||
|
||||
/// Synchronizes terminal size, calls the rendering closure, flushes the current internal state
|
||||
/// and prepares for the next draw call.
|
||||
pub fn draw<F>(&mut self, f: F) -> io::Result<CompletedFrame>
|
||||
where
|
||||
F: FnOnce(&mut Frame<B>),
|
||||
{
|
||||
// Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
|
||||
// and the terminal (if growing), which may OOB.
|
||||
self.autoresize()?;
|
||||
|
||||
let mut frame = self.get_frame();
|
||||
f(&mut frame);
|
||||
// We can't change the cursor position right away because we have to flush the frame to
|
||||
// stdout first. But we also can't keep the frame around, since it holds a &mut to
|
||||
// Terminal. Thus, we're taking the important data out of the Frame and dropping it.
|
||||
let cursor_position = frame.cursor_position;
|
||||
|
||||
// Draw to stdout
|
||||
self.flush()?;
|
||||
|
||||
match cursor_position {
|
||||
None => self.hide_cursor()?,
|
||||
Some((x, y)) => {
|
||||
self.show_cursor()?;
|
||||
self.set_cursor(x, y)?;
|
||||
}
|
||||
}
|
||||
|
||||
self.swap_buffers();
|
||||
|
||||
// Flush
|
||||
self.backend.flush()?;
|
||||
|
||||
Ok(CompletedFrame {
|
||||
buffer: &self.buffers[1 - self.current],
|
||||
area: self.last_known_size,
|
||||
})
|
||||
}
|
||||
pub fn size(&self) -> Result<Rect, io::Error> {
|
||||
|
||||
pub fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
self.backend.hide_cursor()?;
|
||||
self.hidden_cursor = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn show_cursor(&mut self) -> io::Result<()> {
|
||||
self.backend.show_cursor()?;
|
||||
self.hidden_cursor = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
self.backend.get_cursor()
|
||||
}
|
||||
|
||||
pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
self.backend.set_cursor(x, y)?;
|
||||
self.last_known_cursor_pos = (x, y);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clear the terminal and force a full redraw on the next draw call.
|
||||
pub fn clear(&mut self) -> io::Result<()> {
|
||||
match self.viewport {
|
||||
Viewport::Fullscreen => self.backend.clear_region(ClearType::All)?,
|
||||
Viewport::Inline(_) => {
|
||||
self.backend
|
||||
.set_cursor(self.viewport_area.left(), self.viewport_area.top())?;
|
||||
self.backend.clear_region(ClearType::AfterCursor)?;
|
||||
}
|
||||
Viewport::Fixed(area) => {
|
||||
for row in area.top()..area.bottom() {
|
||||
self.backend.set_cursor(0, row)?;
|
||||
self.backend.clear_region(ClearType::AfterCursor)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Reset the back buffer to make sure the next update will redraw everything.
|
||||
self.buffers[1 - self.current].reset();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clears the inactive buffer and swaps it with the current buffer
|
||||
pub fn swap_buffers(&mut self) {
|
||||
self.buffers[1 - self.current].reset();
|
||||
self.current = 1 - self.current;
|
||||
}
|
||||
|
||||
/// Queries the real size of the backend.
|
||||
pub fn size(&self) -> io::Result<Rect> {
|
||||
self.backend.size()
|
||||
}
|
||||
|
||||
/// Insert some content before the current inline viewport. This has no effect when the
|
||||
/// viewport is fullscreen.
|
||||
///
|
||||
/// This function scrolls down the current viewport by the given height. The newly freed space
|
||||
/// is then made available to the `draw_fn` closure through a writable `Buffer`.
|
||||
///
|
||||
/// Before:
|
||||
/// ```ignore
|
||||
/// +-------------------+
|
||||
/// | |
|
||||
/// | viewport |
|
||||
/// | |
|
||||
/// +-------------------+
|
||||
/// ```
|
||||
///
|
||||
/// After:
|
||||
/// ```ignore
|
||||
/// +-------------------+
|
||||
/// | buffer |
|
||||
/// +-------------------+
|
||||
/// +-------------------+
|
||||
/// | |
|
||||
/// | viewport |
|
||||
/// | |
|
||||
/// +-------------------+
|
||||
/// ```
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ## Insert a single line before the current viewport
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::widgets::{Paragraph, Widget};
|
||||
/// # use ratatui::text::{Line, Span};
|
||||
/// # use ratatui::style::{Color, Style};
|
||||
/// # use ratatui::{Terminal};
|
||||
/// # use ratatui::backend::TestBackend;
|
||||
/// # let backend = TestBackend::new(10, 10);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// terminal.insert_before(1, |buf| {
|
||||
/// Paragraph::new(Line::from(vec![
|
||||
/// Span::raw("This line will be added "),
|
||||
/// Span::styled("before", Style::default().fg(Color::Blue)),
|
||||
/// Span::raw(" the current viewport")
|
||||
/// ])).render(buf.area, buf);
|
||||
/// });
|
||||
/// ```
|
||||
pub fn insert_before<F>(&mut self, height: u16, draw_fn: F) -> io::Result<()>
|
||||
where
|
||||
F: FnOnce(&mut Buffer),
|
||||
{
|
||||
if !matches!(self.viewport, Viewport::Inline(_)) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.clear()?;
|
||||
let height = height.min(self.last_known_size.height);
|
||||
self.backend.append_lines(height)?;
|
||||
let missing_lines =
|
||||
height.saturating_sub(self.last_known_size.bottom() - self.viewport_area.top());
|
||||
let area = Rect {
|
||||
x: self.viewport_area.left(),
|
||||
y: self.viewport_area.top().saturating_sub(missing_lines),
|
||||
width: self.viewport_area.width,
|
||||
height,
|
||||
};
|
||||
let mut buffer = Buffer::empty(area);
|
||||
|
||||
draw_fn(&mut buffer);
|
||||
|
||||
let iter = buffer.content.iter().enumerate().map(|(i, c)| {
|
||||
let (x, y) = buffer.pos_of(i);
|
||||
(x, y, c)
|
||||
});
|
||||
self.backend.draw(iter)?;
|
||||
self.backend.flush()?;
|
||||
|
||||
let remaining_lines = self.last_known_size.height - area.bottom();
|
||||
let missing_lines = self.viewport_area.height.saturating_sub(remaining_lines);
|
||||
self.backend.append_lines(self.viewport_area.height)?;
|
||||
|
||||
self.set_viewport_area(Rect {
|
||||
x: area.left(),
|
||||
y: area.bottom().saturating_sub(missing_lines),
|
||||
width: area.width,
|
||||
height: self.viewport_area.height,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_inline_size<B: Backend>(
|
||||
backend: &mut B,
|
||||
height: u16,
|
||||
size: Rect,
|
||||
offset_in_previous_viewport: u16,
|
||||
) -> io::Result<(Rect, (u16, u16))> {
|
||||
let pos = backend.get_cursor()?;
|
||||
let mut row = pos.1;
|
||||
|
||||
let max_height = size.height.min(height);
|
||||
|
||||
let lines_after_cursor = height
|
||||
.saturating_sub(offset_in_previous_viewport)
|
||||
.saturating_sub(1);
|
||||
|
||||
backend.append_lines(lines_after_cursor)?;
|
||||
|
||||
let available_lines = size.height.saturating_sub(row).saturating_sub(1);
|
||||
let missing_lines = lines_after_cursor.saturating_sub(available_lines);
|
||||
if missing_lines > 0 {
|
||||
row = row.saturating_sub(missing_lines);
|
||||
}
|
||||
row = row.saturating_sub(offset_in_previous_viewport);
|
||||
|
||||
Ok((
|
||||
Rect {
|
||||
x: 0,
|
||||
y: row,
|
||||
width: size.width,
|
||||
height: max_height,
|
||||
},
|
||||
pos,
|
||||
))
|
||||
}
|
||||
|
||||
470
src/text.rs
Normal file
470
src/text.rs
Normal file
@@ -0,0 +1,470 @@
|
||||
//! Primitives for styled text.
|
||||
//!
|
||||
//! A terminal UI is at its root a lot of strings. In order to make it accessible and stylish,
|
||||
//! those strings may be associated to a set of styles. `ratatui` has three ways to represent them:
|
||||
//! - A single line string where all graphemes have the same style is represented by a [`Span`].
|
||||
//! - A single line string where each grapheme may have its own style is represented by [`Line`].
|
||||
//! - A multiple line string where each grapheme may have its own style is represented by a
|
||||
//! [`Text`].
|
||||
//!
|
||||
//! These types form a hierarchy: [`Line`] is a collection of [`Span`] and each line of [`Text`]
|
||||
//! is a [`Line`].
|
||||
//!
|
||||
//! Keep it mind that a lot of widgets will use those types to advertise what kind of string is
|
||||
//! supported for their properties. Moreover, `ratatui` provides convenient `From` implementations
|
||||
//! so that you can start by using simple `String` or `&str` and then promote them to the previous
|
||||
//! primitives when you need additional styling capabilities.
|
||||
//!
|
||||
//! For example, for the [`crate::widgets::Block`] widget, all the following calls are valid to set
|
||||
//! its `title` property (which is a [`Line`] under the hood):
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use ratatui::widgets::Block;
|
||||
//! # use ratatui::text::{Span, Line};
|
||||
//! # use ratatui::style::{Color, Style};
|
||||
//! // A simple string with no styling.
|
||||
//! // Converted to Line(vec![
|
||||
//! // Span { content: Cow::Borrowed("My title"), style: Style { .. } }
|
||||
//! // ])
|
||||
//! let block = Block::default().title("My title");
|
||||
//!
|
||||
//! // A simple string with a unique style.
|
||||
//! // Converted to Line(vec![
|
||||
//! // Span { content: Cow::Borrowed("My title"), style: Style { fg: Some(Color::Yellow), .. }
|
||||
//! // ])
|
||||
//! let block = Block::default().title(
|
||||
//! Span::styled("My title", Style::default().fg(Color::Yellow))
|
||||
//! );
|
||||
//!
|
||||
//! // A string with multiple styles.
|
||||
//! // Converted to Line(vec![
|
||||
//! // Span { content: Cow::Borrowed("My"), style: Style { fg: Some(Color::Yellow), .. } },
|
||||
//! // Span { content: Cow::Borrowed(" title"), .. }
|
||||
//! // ])
|
||||
//! let block = Block::default().title(vec![
|
||||
//! Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
//! Span::raw(" title"),
|
||||
//! ]);
|
||||
//! ```
|
||||
use std::{borrow::Cow, fmt::Debug};
|
||||
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::style::{Style, Styled};
|
||||
|
||||
mod line;
|
||||
mod masked;
|
||||
mod spans;
|
||||
#[allow(deprecated)]
|
||||
pub use {line::Line, masked::Masked, spans::Spans};
|
||||
|
||||
/// A grapheme associated to a style.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct StyledGrapheme<'a> {
|
||||
pub symbol: &'a str,
|
||||
pub style: Style,
|
||||
}
|
||||
|
||||
/// A string where all graphemes have the same style.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Span<'a> {
|
||||
pub content: Cow<'a, str>,
|
||||
pub style: Style,
|
||||
}
|
||||
|
||||
impl<'a> Span<'a> {
|
||||
/// Create a span with no style.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Span;
|
||||
/// Span::raw("My text");
|
||||
/// Span::raw(String::from("My text"));
|
||||
/// ```
|
||||
pub fn raw<T>(content: T) -> Span<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
{
|
||||
Span {
|
||||
content: content.into(),
|
||||
style: Style::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a span with a style.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Span;
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// Span::styled("My text", style);
|
||||
/// Span::styled(String::from("My text"), style);
|
||||
/// ```
|
||||
pub fn styled<T>(content: T, style: Style) -> Span<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
{
|
||||
Span {
|
||||
content: content.into(),
|
||||
style,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the width of the content held by this span.
|
||||
pub fn width(&self) -> usize {
|
||||
self.content.width()
|
||||
}
|
||||
|
||||
/// Returns an iterator over the graphemes held by this span.
|
||||
///
|
||||
/// `base_style` is the [`Style`] that will be patched with each grapheme [`Style`] to get
|
||||
/// the resulting [`Style`].
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::{Span, StyledGrapheme};
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// # use std::iter::Iterator;
|
||||
/// let style = Style::default().fg(Color::Yellow);
|
||||
/// let span = Span::styled("Text", style);
|
||||
/// let style = Style::default().fg(Color::Green).bg(Color::Black);
|
||||
/// let styled_graphemes = span.styled_graphemes(style);
|
||||
/// assert_eq!(
|
||||
/// vec![
|
||||
/// StyledGrapheme {
|
||||
/// symbol: "T",
|
||||
/// style: Style {
|
||||
/// fg: Some(Color::Yellow),
|
||||
/// bg: Some(Color::Black),
|
||||
/// add_modifier: Modifier::empty(),
|
||||
/// sub_modifier: Modifier::empty(),
|
||||
/// },
|
||||
/// },
|
||||
/// StyledGrapheme {
|
||||
/// symbol: "e",
|
||||
/// style: Style {
|
||||
/// fg: Some(Color::Yellow),
|
||||
/// bg: Some(Color::Black),
|
||||
/// add_modifier: Modifier::empty(),
|
||||
/// sub_modifier: Modifier::empty(),
|
||||
/// },
|
||||
/// },
|
||||
/// StyledGrapheme {
|
||||
/// symbol: "x",
|
||||
/// style: Style {
|
||||
/// fg: Some(Color::Yellow),
|
||||
/// bg: Some(Color::Black),
|
||||
/// add_modifier: Modifier::empty(),
|
||||
/// sub_modifier: Modifier::empty(),
|
||||
/// },
|
||||
/// },
|
||||
/// StyledGrapheme {
|
||||
/// symbol: "t",
|
||||
/// style: Style {
|
||||
/// fg: Some(Color::Yellow),
|
||||
/// bg: Some(Color::Black),
|
||||
/// add_modifier: Modifier::empty(),
|
||||
/// sub_modifier: Modifier::empty(),
|
||||
/// },
|
||||
/// },
|
||||
/// ],
|
||||
/// styled_graphemes.collect::<Vec<StyledGrapheme>>()
|
||||
/// );
|
||||
/// ```
|
||||
pub fn styled_graphemes(
|
||||
&'a self,
|
||||
base_style: Style,
|
||||
) -> impl Iterator<Item = StyledGrapheme<'a>> {
|
||||
UnicodeSegmentation::graphemes(self.content.as_ref(), true)
|
||||
.map(move |g| StyledGrapheme {
|
||||
symbol: g,
|
||||
style: base_style.patch(self.style),
|
||||
})
|
||||
.filter(|s| s.symbol != "\n")
|
||||
}
|
||||
|
||||
/// Patches the style an existing Span, adding modifiers from the given style.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Span;
|
||||
/// # use ratatui::style::{Color, Style, Modifier};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// let mut raw_span = Span::raw("My text");
|
||||
/// let mut styled_span = Span::styled("My text", style);
|
||||
///
|
||||
/// assert_ne!(raw_span, styled_span);
|
||||
///
|
||||
/// raw_span.patch_style(style);
|
||||
/// assert_eq!(raw_span, styled_span);
|
||||
/// ```
|
||||
pub fn patch_style(&mut self, style: Style) {
|
||||
self.style = self.style.patch(style);
|
||||
}
|
||||
|
||||
/// Resets the style of the Span.
|
||||
/// Equivalent to calling `patch_style(Style::reset())`.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Span;
|
||||
/// # use ratatui::style::{Color, Style, Modifier};
|
||||
/// let mut span = Span::styled("My text", Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC));
|
||||
///
|
||||
/// span.reset_style();
|
||||
/// assert_eq!(Style::reset(), span.style);
|
||||
/// ```
|
||||
pub fn reset_style(&mut self) {
|
||||
self.patch_style(Style::reset());
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<String> for Span<'a> {
|
||||
fn from(s: String) -> Span<'a> {
|
||||
Span::raw(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for Span<'a> {
|
||||
fn from(s: &'a str) -> Span<'a> {
|
||||
Span::raw(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Styled for Span<'a> {
|
||||
type Item = Span<'a>;
|
||||
fn style(&self) -> Style {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self {
|
||||
Self::styled(self.content, style)
|
||||
}
|
||||
}
|
||||
|
||||
/// A string split over multiple lines where each line is composed of several clusters, each with
|
||||
/// their own style.
|
||||
///
|
||||
/// A [`Text`], like a [`Span`], can be constructed using one of the many `From` implementations
|
||||
/// or via the [`Text::raw`] and [`Text::styled`] methods. Helpfully, [`Text`] also implements
|
||||
/// [`core::iter::Extend`] which enables the concatenation of several [`Text`] blocks.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Text;
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
///
|
||||
/// // An initial two lines of `Text` built from a `&str`
|
||||
/// let mut text = Text::from("The first line\nThe second line");
|
||||
/// assert_eq!(2, text.height());
|
||||
///
|
||||
/// // Adding two more unstyled lines
|
||||
/// text.extend(Text::raw("These are two\nmore lines!"));
|
||||
/// assert_eq!(4, text.height());
|
||||
///
|
||||
/// // Adding a final two styled lines
|
||||
/// text.extend(Text::styled("Some more lines\nnow with more style!", style));
|
||||
/// assert_eq!(6, text.height());
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq, Default, Eq)]
|
||||
pub struct Text<'a> {
|
||||
pub lines: Vec<Line<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Text<'a> {
|
||||
/// Create some text (potentially multiple lines) with no style.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Text;
|
||||
/// Text::raw("The first line\nThe second line");
|
||||
/// Text::raw(String::from("The first line\nThe second line"));
|
||||
/// ```
|
||||
pub fn raw<T>(content: T) -> Text<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
{
|
||||
let lines: Vec<_> = match content.into() {
|
||||
Cow::Borrowed("") => vec![Line::from("")],
|
||||
Cow::Borrowed(s) => s.lines().map(Line::from).collect(),
|
||||
Cow::Owned(s) if s.is_empty() => vec![Line::from("")],
|
||||
Cow::Owned(s) => s.lines().map(|l| Line::from(l.to_owned())).collect(),
|
||||
};
|
||||
|
||||
Text::from(lines)
|
||||
}
|
||||
|
||||
/// Create some text (potentially multiple lines) with a style.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Text;
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// Text::styled("The first line\nThe second line", style);
|
||||
/// Text::styled(String::from("The first line\nThe second line"), style);
|
||||
/// ```
|
||||
pub fn styled<T>(content: T, style: Style) -> Text<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
{
|
||||
let mut text = Text::raw(content);
|
||||
text.patch_style(style);
|
||||
text
|
||||
}
|
||||
|
||||
/// Returns the max width of all the lines.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::text::Text;
|
||||
/// let text = Text::from("The first line\nThe second line");
|
||||
/// assert_eq!(15, text.width());
|
||||
/// ```
|
||||
pub fn width(&self) -> usize {
|
||||
self.lines.iter().map(Line::width).max().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns the height.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::text::Text;
|
||||
/// let text = Text::from("The first line\nThe second line");
|
||||
/// assert_eq!(2, text.height());
|
||||
/// ```
|
||||
pub fn height(&self) -> usize {
|
||||
self.lines.len()
|
||||
}
|
||||
|
||||
/// Patches the style of each line in an existing Text, adding modifiers from the given style.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Text;
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// let mut raw_text = Text::raw("The first line\nThe second line");
|
||||
/// let styled_text = Text::styled(String::from("The first line\nThe second line"), style);
|
||||
/// assert_ne!(raw_text, styled_text);
|
||||
///
|
||||
/// raw_text.patch_style(style);
|
||||
/// assert_eq!(raw_text, styled_text);
|
||||
/// ```
|
||||
pub fn patch_style(&mut self, style: Style) {
|
||||
for line in &mut self.lines {
|
||||
line.patch_style(style);
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets the style of the Text.
|
||||
/// Equivalent to calling `patch_style(Style::reset())`.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::{Span, Line, Text};
|
||||
/// # use ratatui::style::{Color, Style, Modifier};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// let mut text = Text::styled("The first line\nThe second line", style);
|
||||
///
|
||||
/// text.reset_style();
|
||||
/// for line in &text.lines {
|
||||
/// for span in &line.spans {
|
||||
/// assert_eq!(Style::reset(), span.style);
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn reset_style(&mut self) {
|
||||
for line in &mut self.lines {
|
||||
line.reset_style();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<String> for Text<'a> {
|
||||
fn from(s: String) -> Text<'a> {
|
||||
Text::raw(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for Text<'a> {
|
||||
fn from(s: &'a str) -> Text<'a> {
|
||||
Text::raw(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Cow<'a, str>> for Text<'a> {
|
||||
fn from(s: Cow<'a, str>) -> Text<'a> {
|
||||
Text::raw(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Span<'a>> for Text<'a> {
|
||||
fn from(span: Span<'a>) -> Text<'a> {
|
||||
Text {
|
||||
lines: vec![Line::from(span)],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<'a> From<Spans<'a>> for Text<'a> {
|
||||
fn from(spans: Spans<'a>) -> Text<'a> {
|
||||
Text {
|
||||
lines: vec![spans.into()],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Line<'a>> for Text<'a> {
|
||||
fn from(line: Line<'a>) -> Text<'a> {
|
||||
Text { lines: vec![line] }
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<'a> From<Vec<Spans<'a>>> for Text<'a> {
|
||||
fn from(lines: Vec<Spans<'a>>) -> Text<'a> {
|
||||
Text {
|
||||
lines: lines.into_iter().map(|l| l.0.into()).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Vec<Line<'a>>> for Text<'a> {
|
||||
fn from(lines: Vec<Line<'a>>) -> Text<'a> {
|
||||
Text { lines }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for Text<'a> {
|
||||
type Item = Line<'a>;
|
||||
type IntoIter = std::vec::IntoIter<Self::Item>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.lines.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> Extend<T> for Text<'a>
|
||||
where
|
||||
T: Into<Line<'a>>,
|
||||
{
|
||||
fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
|
||||
let lines = iter.into_iter().map(Into::into);
|
||||
self.lines.extend(lines);
|
||||
}
|
||||
}
|
||||
251
src/text/line.rs
Normal file
251
src/text/line.rs
Normal file
@@ -0,0 +1,251 @@
|
||||
#![allow(deprecated)]
|
||||
use super::{Span, Spans, Style};
|
||||
use crate::layout::Alignment;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default, Eq)]
|
||||
pub struct Line<'a> {
|
||||
pub spans: Vec<Span<'a>>,
|
||||
pub alignment: Option<Alignment>,
|
||||
}
|
||||
|
||||
impl<'a> Line<'a> {
|
||||
/// Returns the width of the underlying string.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::{Span, Line};
|
||||
/// # use ratatui::style::{Color, Style};
|
||||
/// let line = Line::from(vec![
|
||||
/// Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
/// Span::raw(" text"),
|
||||
/// ]);
|
||||
/// assert_eq!(7, line.width());
|
||||
/// ```
|
||||
pub fn width(&self) -> usize {
|
||||
self.spans.iter().map(Span::width).sum()
|
||||
}
|
||||
|
||||
/// Patches the style of each Span in an existing Line, adding modifiers from the given style.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::{Span, Line};
|
||||
/// # use ratatui::style::{Color, Style, Modifier};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// let mut raw_line = Line::from(vec![
|
||||
/// Span::raw("My"),
|
||||
/// Span::raw(" text"),
|
||||
/// ]);
|
||||
/// let mut styled_line = Line::from(vec![
|
||||
/// Span::styled("My", style),
|
||||
/// Span::styled(" text", style),
|
||||
/// ]);
|
||||
///
|
||||
/// assert_ne!(raw_line, styled_line);
|
||||
///
|
||||
/// raw_line.patch_style(style);
|
||||
/// assert_eq!(raw_line, styled_line);
|
||||
/// ```
|
||||
pub fn patch_style(&mut self, style: Style) {
|
||||
for span in &mut self.spans {
|
||||
span.patch_style(style);
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets the style of each Span in the Line.
|
||||
/// Equivalent to calling `patch_style(Style::reset())`.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::{Span, Line};
|
||||
/// # use ratatui::style::{Color, Style, Modifier};
|
||||
/// let mut line = Line::from(vec![
|
||||
/// Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
/// Span::styled(" text", Style::default().add_modifier(Modifier::BOLD)),
|
||||
/// ]);
|
||||
///
|
||||
/// line.reset_style();
|
||||
/// assert_eq!(Style::reset(), line.spans[0].style);
|
||||
/// assert_eq!(Style::reset(), line.spans[1].style);
|
||||
/// ```
|
||||
pub fn reset_style(&mut self) {
|
||||
for span in &mut self.spans {
|
||||
span.reset_style();
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the target alignment for this line of text.
|
||||
/// Defaults to: [`None`], meaning the alignment is determined by the rendering widget.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use std::borrow::Cow;
|
||||
/// # use ratatui::layout::Alignment;
|
||||
/// # use ratatui::text::{Span, Line};
|
||||
/// # use ratatui::style::{Color, Style, Modifier};
|
||||
/// let mut line = Line::from("Hi, what's up?");
|
||||
/// assert_eq!(None, line.alignment);
|
||||
/// assert_eq!(Some(Alignment::Right), line.alignment(Alignment::Right).alignment)
|
||||
/// ```
|
||||
pub fn alignment(self, alignment: Alignment) -> Self {
|
||||
Self {
|
||||
alignment: Some(alignment),
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<String> for Line<'a> {
|
||||
fn from(s: String) -> Self {
|
||||
Self::from(vec![Span::from(s)])
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for Line<'a> {
|
||||
fn from(s: &'a str) -> Self {
|
||||
Self::from(vec![Span::from(s)])
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Vec<Span<'a>>> for Line<'a> {
|
||||
fn from(spans: Vec<Span<'a>>) -> Self {
|
||||
Self {
|
||||
spans,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Span<'a>> for Line<'a> {
|
||||
fn from(span: Span<'a>) -> Self {
|
||||
Self::from(vec![span])
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Line<'a>> for String {
|
||||
fn from(line: Line<'a>) -> String {
|
||||
line.spans.iter().fold(String::new(), |mut acc, s| {
|
||||
acc.push_str(s.content.as_ref());
|
||||
acc
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Spans<'a>> for Line<'a> {
|
||||
fn from(value: Spans<'a>) -> Self {
|
||||
Self::from(value.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
layout::Alignment,
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span, Spans},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_width() {
|
||||
let line = Line::from(vec![
|
||||
Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
Span::raw(" text"),
|
||||
]);
|
||||
assert_eq!(7, line.width());
|
||||
|
||||
let empty_line = Line::default();
|
||||
assert_eq!(0, empty_line.width());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_patch_style() {
|
||||
let style = Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::ITALIC);
|
||||
let mut raw_line = Line::from(vec![Span::raw("My"), Span::raw(" text")]);
|
||||
let styled_line = Line::from(vec![
|
||||
Span::styled("My", style),
|
||||
Span::styled(" text", style),
|
||||
]);
|
||||
|
||||
assert_ne!(raw_line, styled_line);
|
||||
|
||||
raw_line.patch_style(style);
|
||||
assert_eq!(raw_line, styled_line);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reset_style() {
|
||||
let mut line = Line::from(vec![
|
||||
Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
Span::styled(" text", Style::default().add_modifier(Modifier::BOLD)),
|
||||
]);
|
||||
|
||||
line.reset_style();
|
||||
assert_eq!(Style::reset(), line.spans[0].style);
|
||||
assert_eq!(Style::reset(), line.spans[1].style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_string() {
|
||||
let s = String::from("Hello, world!");
|
||||
let line = Line::from(s);
|
||||
assert_eq!(vec![Span::from("Hello, world!")], line.spans);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_str() {
|
||||
let s = "Hello, world!";
|
||||
let line = Line::from(s);
|
||||
assert_eq!(vec![Span::from("Hello, world!")], line.spans);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_vec() {
|
||||
let spans = vec![
|
||||
Span::styled("Hello,", Style::default().fg(Color::Red)),
|
||||
Span::styled(" world!", Style::default().fg(Color::Green)),
|
||||
];
|
||||
let line = Line::from(spans.clone());
|
||||
assert_eq!(spans, line.spans);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_span() {
|
||||
let span = Span::styled("Hello, world!", Style::default().fg(Color::Yellow));
|
||||
let line = Line::from(span.clone());
|
||||
assert_eq!(vec![span], line.spans);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_spans() {
|
||||
let spans = vec![
|
||||
Span::styled("Hello,", Style::default().fg(Color::Red)),
|
||||
Span::styled(" world!", Style::default().fg(Color::Green)),
|
||||
];
|
||||
assert_eq!(Line::from(Spans::from(spans.clone())), Line::from(spans));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_into_string() {
|
||||
let line = Line::from(vec![
|
||||
Span::styled("Hello,", Style::default().fg(Color::Red)),
|
||||
Span::styled(" world!", Style::default().fg(Color::Green)),
|
||||
]);
|
||||
let s: String = line.into();
|
||||
assert_eq!("Hello, world!", s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_alignment() {
|
||||
let line = Line::from("This is left").alignment(Alignment::Left);
|
||||
assert_eq!(Some(Alignment::Left), line.alignment);
|
||||
|
||||
let line = Line::from("This is default");
|
||||
assert_eq!(None, line.alignment);
|
||||
}
|
||||
}
|
||||
128
src/text/masked.rs
Normal file
128
src/text/masked.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fmt::{self, Debug, Display},
|
||||
};
|
||||
|
||||
use super::Text;
|
||||
|
||||
/// A wrapper around a string that is masked when displayed.
|
||||
///
|
||||
/// The masked string is displayed as a series of the same character.
|
||||
/// This might be used to display a password field or similar secure data.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{buffer::Buffer, layout::Rect, text::Masked, widgets::{Paragraph, Widget}};
|
||||
///
|
||||
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
/// let password = Masked::new("12345", 'x');
|
||||
///
|
||||
/// Paragraph::new(password).render(buffer.area, &mut buffer);
|
||||
/// assert_eq!(buffer, Buffer::with_lines(vec!["xxxxx"]));
|
||||
/// ```
|
||||
#[derive(Clone)]
|
||||
pub struct Masked<'a> {
|
||||
inner: Cow<'a, str>,
|
||||
mask_char: char,
|
||||
}
|
||||
|
||||
impl<'a> Masked<'a> {
|
||||
pub fn new(s: impl Into<Cow<'a, str>>, mask_char: char) -> Self {
|
||||
Self {
|
||||
inner: s.into(),
|
||||
mask_char,
|
||||
}
|
||||
}
|
||||
|
||||
/// The character to use for masking.
|
||||
pub fn mask_char(&self) -> char {
|
||||
self.mask_char
|
||||
}
|
||||
|
||||
/// The underlying string, with all characters masked.
|
||||
pub fn value(&self) -> Cow<'a, str> {
|
||||
self.inner.chars().map(|_| self.mask_char).collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Masked<'_> {
|
||||
/// Debug representation of a masked string is the underlying string
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.inner).map_err(|_| fmt::Error)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Masked<'_> {
|
||||
/// Display representation of a masked string is the masked string
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.value()).map_err(|_| fmt::Error)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Masked<'a>> for Cow<'a, str> {
|
||||
fn from(masked: &'a Masked) -> Cow<'a, str> {
|
||||
masked.value()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Masked<'a>> for Cow<'a, str> {
|
||||
fn from(masked: Masked<'a>) -> Cow<'a, str> {
|
||||
masked.value()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Masked<'_>> for Text<'a> {
|
||||
fn from(masked: &'a Masked) -> Text<'a> {
|
||||
Text::raw(masked.value())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Masked<'a>> for Text<'a> {
|
||||
fn from(masked: Masked<'a>) -> Text<'a> {
|
||||
Text::raw(masked.value())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::borrow::Borrow;
|
||||
|
||||
use super::*;
|
||||
use crate::text::Line;
|
||||
|
||||
#[test]
|
||||
fn test_masked_value() {
|
||||
let masked = Masked::new("12345", 'x');
|
||||
assert_eq!(masked.value(), "xxxxx");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_masked_debug() {
|
||||
let masked = Masked::new("12345", 'x');
|
||||
assert_eq!(format!("{masked:?}"), "12345");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_masked_display() {
|
||||
let masked = Masked::new("12345", 'x');
|
||||
assert_eq!(format!("{masked}"), "xxxxx");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_masked_conversions() {
|
||||
let masked = Masked::new("12345", 'x');
|
||||
|
||||
let text: Text = masked.borrow().into();
|
||||
assert_eq!(text.lines, vec![Line::from("xxxxx")]);
|
||||
|
||||
let text: Text = masked.to_owned().into();
|
||||
assert_eq!(text.lines, vec![Line::from("xxxxx")]);
|
||||
|
||||
let cow: Cow<str> = masked.borrow().into();
|
||||
assert_eq!(cow, "xxxxx");
|
||||
|
||||
let cow: Cow<str> = masked.to_owned().into();
|
||||
assert_eq!(cow, "xxxxx");
|
||||
}
|
||||
}
|
||||
225
src/text/spans.rs
Normal file
225
src/text/spans.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
#![allow(deprecated)]
|
||||
|
||||
use super::{Span, Style};
|
||||
use crate::{layout::Alignment, text::Line};
|
||||
|
||||
/// A string composed of clusters of graphemes, each with their own style.
|
||||
///
|
||||
/// `Spans` has been deprecated in favor of `Line`, and will be removed in the
|
||||
/// future. All methods that accept Spans have been replaced with methods that
|
||||
/// accept Into<Line<'a>> (which is implemented on `Spans`) to allow users of
|
||||
/// this crate to gradually transition to Line.
|
||||
#[derive(Debug, Clone, PartialEq, Default, Eq)]
|
||||
#[deprecated(note = "Use `ratatui::text::Line` instead")]
|
||||
pub struct Spans<'a>(pub Vec<Span<'a>>);
|
||||
|
||||
impl<'a> Spans<'a> {
|
||||
/// Returns the width of the underlying string.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::{Span, Spans};
|
||||
/// # use ratatui::style::{Color, Style};
|
||||
/// let spans = Spans::from(vec![
|
||||
/// Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
/// Span::raw(" text"),
|
||||
/// ]);
|
||||
/// assert_eq!(7, spans.width());
|
||||
/// ```
|
||||
pub fn width(&self) -> usize {
|
||||
self.0.iter().map(Span::width).sum()
|
||||
}
|
||||
|
||||
/// Patches the style of each Span in an existing Spans, adding modifiers from the given style.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::{Span, Spans};
|
||||
/// # use ratatui::style::{Color, Style, Modifier};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// let mut raw_spans = Spans::from(vec![
|
||||
/// Span::raw("My"),
|
||||
/// Span::raw(" text"),
|
||||
/// ]);
|
||||
/// let mut styled_spans = Spans::from(vec![
|
||||
/// Span::styled("My", style),
|
||||
/// Span::styled(" text", style),
|
||||
/// ]);
|
||||
///
|
||||
/// assert_ne!(raw_spans, styled_spans);
|
||||
///
|
||||
/// raw_spans.patch_style(style);
|
||||
/// assert_eq!(raw_spans, styled_spans);
|
||||
/// ```
|
||||
pub fn patch_style(&mut self, style: Style) {
|
||||
for span in &mut self.0 {
|
||||
span.patch_style(style);
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets the style of each Span in the Spans.
|
||||
/// Equivalent to calling `patch_style(Style::reset())`.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::{Span, Spans};
|
||||
/// # use ratatui::style::{Color, Style, Modifier};
|
||||
/// let mut spans = Spans::from(vec![
|
||||
/// Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
/// Span::styled(" text", Style::default().add_modifier(Modifier::BOLD)),
|
||||
/// ]);
|
||||
///
|
||||
/// spans.reset_style();
|
||||
/// assert_eq!(Style::reset(), spans.0[0].style);
|
||||
/// assert_eq!(Style::reset(), spans.0[1].style);
|
||||
/// ```
|
||||
pub fn reset_style(&mut self) {
|
||||
for span in &mut self.0 {
|
||||
span.reset_style();
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the target alignment for this line of text.
|
||||
/// Defaults to: [`None`], meaning the alignment is determined by the rendering widget.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use std::borrow::Cow;
|
||||
/// # use ratatui::layout::Alignment;
|
||||
/// # use ratatui::text::{Span, Spans};
|
||||
/// # use ratatui::style::{Color, Style, Modifier};
|
||||
/// let mut line = Spans::from("Hi, what's up?").alignment(Alignment::Right);
|
||||
/// assert_eq!(Some(Alignment::Right), line.alignment)
|
||||
/// ```
|
||||
pub fn alignment(self, alignment: Alignment) -> Line<'a> {
|
||||
let line = Line::from(self);
|
||||
line.alignment(alignment)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<String> for Spans<'a> {
|
||||
fn from(s: String) -> Spans<'a> {
|
||||
Spans(vec![Span::from(s)])
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for Spans<'a> {
|
||||
fn from(s: &'a str) -> Spans<'a> {
|
||||
Spans(vec![Span::from(s)])
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Vec<Span<'a>>> for Spans<'a> {
|
||||
fn from(spans: Vec<Span<'a>>) -> Spans<'a> {
|
||||
Spans(spans)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Span<'a>> for Spans<'a> {
|
||||
fn from(span: Span<'a>) -> Spans<'a> {
|
||||
Spans(vec![span])
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Spans<'a>> for String {
|
||||
fn from(line: Spans<'a>) -> String {
|
||||
line.0.iter().fold(String::new(), |mut acc, s| {
|
||||
acc.push_str(s.content.as_ref());
|
||||
acc
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_width() {
|
||||
let spans = Spans::from(vec![
|
||||
Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
Span::raw(" text"),
|
||||
]);
|
||||
assert_eq!(7, spans.width());
|
||||
|
||||
let empty_spans = Spans::default();
|
||||
assert_eq!(0, empty_spans.width());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_patch_style() {
|
||||
let style = Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::ITALIC);
|
||||
let mut raw_spans = Spans::from(vec![Span::raw("My"), Span::raw(" text")]);
|
||||
let styled_spans = Spans::from(vec![
|
||||
Span::styled("My", style),
|
||||
Span::styled(" text", style),
|
||||
]);
|
||||
|
||||
assert_ne!(raw_spans, styled_spans);
|
||||
|
||||
raw_spans.patch_style(style);
|
||||
assert_eq!(raw_spans, styled_spans);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reset_style() {
|
||||
let mut spans = Spans::from(vec![
|
||||
Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
Span::styled(" text", Style::default().add_modifier(Modifier::BOLD)),
|
||||
]);
|
||||
|
||||
spans.reset_style();
|
||||
assert_eq!(Style::reset(), spans.0[0].style);
|
||||
assert_eq!(Style::reset(), spans.0[1].style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_string() {
|
||||
let s = String::from("Hello, world!");
|
||||
let spans = Spans::from(s);
|
||||
assert_eq!(vec![Span::from("Hello, world!")], spans.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_str() {
|
||||
let s = "Hello, world!";
|
||||
let spans = Spans::from(s);
|
||||
assert_eq!(vec![Span::from("Hello, world!")], spans.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_vec() {
|
||||
let spans_vec = vec![
|
||||
Span::styled("Hello,", Style::default().fg(Color::Red)),
|
||||
Span::styled(" world!", Style::default().fg(Color::Green)),
|
||||
];
|
||||
let spans = Spans::from(spans_vec.clone());
|
||||
assert_eq!(spans_vec, spans.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_span() {
|
||||
let span = Span::styled("Hello, world!", Style::default().fg(Color::Yellow));
|
||||
let spans = Spans::from(span.clone());
|
||||
assert_eq!(vec![span], spans.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_into_string() {
|
||||
let spans = Spans::from(vec![
|
||||
Span::styled("Hello,", Style::default().fg(Color::Red)),
|
||||
Span::styled(" world!", Style::default().fg(Color::Green)),
|
||||
]);
|
||||
let s: String = spans.into();
|
||||
assert_eq!("Hello, world!", s);
|
||||
}
|
||||
}
|
||||
57
src/title.rs
Normal file
57
src/title.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
use crate::{layout::Alignment, text::Line};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Title<'a> {
|
||||
pub content: Line<'a>,
|
||||
/// Defaults to Left if unset
|
||||
pub alignment: Option<Alignment>,
|
||||
|
||||
/// Defaults to Top if unset
|
||||
pub position: Option<Position>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Position {
|
||||
#[default]
|
||||
Top,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
impl<'a> Title<'a> {
|
||||
pub fn content<T>(mut self, content: T) -> Title<'a>
|
||||
where
|
||||
T: Into<Line<'a>>,
|
||||
{
|
||||
self.content = content.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn alignment(mut self, alignment: Alignment) -> Title<'a> {
|
||||
self.alignment = Some(alignment);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn position(mut self, position: Position) -> Title<'a> {
|
||||
self.position = Some(position);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> From<T> for Title<'a>
|
||||
where
|
||||
T: Into<Line<'a>>,
|
||||
{
|
||||
fn from(value: T) -> Self {
|
||||
Self::default().content(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Default for Title<'a> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
content: Line::from(""),
|
||||
alignment: Some(Alignment::Left),
|
||||
position: Some(Position::Top),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,33 @@
|
||||
use std::cmp::{max, min};
|
||||
use std::cmp::min;
|
||||
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use widgets::{Block, Widget};
|
||||
use buffer::Buffer;
|
||||
use layout::Rect;
|
||||
use style::Style;
|
||||
use symbols::bar;
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::Style,
|
||||
symbols,
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
|
||||
/// Display multiple bars in a single widgets
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # extern crate tui;
|
||||
/// # use tui::widgets::{Block, Borders, BarChart};
|
||||
/// # use tui::style::{Style, Color, Modifier};
|
||||
/// # fn main() {
|
||||
/// # use ratatui::widgets::{Block, Borders, BarChart};
|
||||
/// # use ratatui::style::{Style, Color, Modifier};
|
||||
/// BarChart::default()
|
||||
/// .block(Block::default().title("BarChart").borders(Borders::ALL))
|
||||
/// .bar_width(3)
|
||||
/// .bar_gap(1)
|
||||
/// .style(Style::default().fg(Color::Yellow).bg(Color::Red))
|
||||
/// .value_style(Style::default().fg(Color::Red).modifier(Modifier::Bold))
|
||||
/// .bar_style(Style::default().fg(Color::Yellow).bg(Color::Red))
|
||||
/// .value_style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD))
|
||||
/// .label_style(Style::default().fg(Color::White))
|
||||
/// .data(&[("B0", 0), ("B1", 2), ("B2", 4), ("B3", 3)])
|
||||
/// .max(4);
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BarChart<'a> {
|
||||
/// Block to wrap the widget in
|
||||
block: Option<Block<'a>>,
|
||||
@@ -35,6 +35,10 @@ pub struct BarChart<'a> {
|
||||
bar_width: u16,
|
||||
/// The gap between each bar
|
||||
bar_gap: u16,
|
||||
/// Set of symbols used to display the data
|
||||
bar_set: symbols::bar::Set,
|
||||
/// Style of the bars
|
||||
bar_style: Style,
|
||||
/// Style of the values printed at the bottom of each bar
|
||||
value_style: Style,
|
||||
/// Style of the labels printed under each bar
|
||||
@@ -57,103 +61,129 @@ impl<'a> Default for BarChart<'a> {
|
||||
max: None,
|
||||
data: &[],
|
||||
values: Vec::new(),
|
||||
bar_style: Style::default(),
|
||||
bar_width: 1,
|
||||
bar_gap: 1,
|
||||
value_style: Default::default(),
|
||||
label_style: Default::default(),
|
||||
style: Default::default(),
|
||||
bar_set: symbols::bar::NINE_LEVELS,
|
||||
value_style: Style::default(),
|
||||
label_style: Style::default(),
|
||||
style: Style::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> BarChart<'a> {
|
||||
pub fn data(&'a mut self, data: &'a [(&'a str, u64)]) -> &mut BarChart<'a> {
|
||||
pub fn data(mut self, data: &'a [(&'a str, u64)]) -> BarChart<'a> {
|
||||
self.data = data;
|
||||
self.values = Vec::with_capacity(self.data.len());
|
||||
for &(_, v) in self.data {
|
||||
self.values.push(format!("{}", v));
|
||||
self.values.push(format!("{v}"));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn block(&'a mut self, block: Block<'a>) -> &mut BarChart<'a> {
|
||||
pub fn block(mut self, block: Block<'a>) -> BarChart<'a> {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
pub fn max(&'a mut self, max: u64) -> &mut BarChart<'a> {
|
||||
|
||||
pub fn max(mut self, max: u64) -> BarChart<'a> {
|
||||
self.max = Some(max);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn bar_width(&'a mut self, width: u16) -> &mut BarChart<'a> {
|
||||
pub fn bar_style(mut self, style: Style) -> BarChart<'a> {
|
||||
self.bar_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn bar_width(mut self, width: u16) -> BarChart<'a> {
|
||||
self.bar_width = width;
|
||||
self
|
||||
}
|
||||
pub fn bar_gap(&'a mut self, gap: u16) -> &mut BarChart<'a> {
|
||||
|
||||
pub fn bar_gap(mut self, gap: u16) -> BarChart<'a> {
|
||||
self.bar_gap = gap;
|
||||
self
|
||||
}
|
||||
pub fn value_style(&'a mut self, style: Style) -> &mut BarChart<'a> {
|
||||
|
||||
pub fn bar_set(mut self, bar_set: symbols::bar::Set) -> BarChart<'a> {
|
||||
self.bar_set = bar_set;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn value_style(mut self, style: Style) -> BarChart<'a> {
|
||||
self.value_style = style;
|
||||
self
|
||||
}
|
||||
pub fn label_style(&'a mut self, style: Style) -> &mut BarChart<'a> {
|
||||
|
||||
pub fn label_style(mut self, style: Style) -> BarChart<'a> {
|
||||
self.label_style = style;
|
||||
self
|
||||
}
|
||||
pub fn style(&'a mut self, style: Style) -> &mut BarChart<'a> {
|
||||
|
||||
pub fn style(mut self, style: Style) -> BarChart<'a> {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for BarChart<'a> {
|
||||
fn draw(&mut self, area: &Rect, buf: &mut Buffer) {
|
||||
let chart_area = match self.block {
|
||||
Some(ref mut b) => {
|
||||
b.draw(area, buf);
|
||||
b.inner(area)
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_style(area, self.style);
|
||||
|
||||
let chart_area = match self.block.take() {
|
||||
Some(b) => {
|
||||
let inner_area = b.inner(area);
|
||||
b.render(area, buf);
|
||||
inner_area
|
||||
}
|
||||
None => *area,
|
||||
None => area,
|
||||
};
|
||||
|
||||
if chart_area.height < 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
self.background(&chart_area, buf, self.style.bg);
|
||||
|
||||
let max = self.max
|
||||
.unwrap_or_else(|| self.data.iter().fold(0, |acc, &(_, v)| max(v, acc)));
|
||||
let max = self
|
||||
.max
|
||||
.unwrap_or_else(|| self.data.iter().map(|t| t.1).max().unwrap_or_default());
|
||||
let max_index = min(
|
||||
(chart_area.width / (self.bar_width + self.bar_gap)) as usize,
|
||||
self.data.len(),
|
||||
);
|
||||
let mut data = self.data
|
||||
let mut data = self
|
||||
.data
|
||||
.iter()
|
||||
.take(max_index)
|
||||
.map(|&(l, v)| (l, v * u64::from(chart_area.height) * 8 / max))
|
||||
.map(|&(l, v)| {
|
||||
(
|
||||
l,
|
||||
v * u64::from(chart_area.height - 1) * 8 / std::cmp::max(max, 1),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<(&str, u64)>>();
|
||||
for j in (0..chart_area.height - 1).rev() {
|
||||
for (i, d) in data.iter_mut().enumerate() {
|
||||
let symbol = match d.1 {
|
||||
0 => " ",
|
||||
1 => bar::ONE_EIGHTH,
|
||||
2 => bar::ONE_QUATER,
|
||||
3 => bar::THREE_EIGHTHS,
|
||||
4 => bar::HALF,
|
||||
5 => bar::FIVE_EIGHTHS,
|
||||
6 => bar::THREE_QUATERS,
|
||||
7 => bar::SEVEN_EIGHTHS,
|
||||
_ => bar::FULL,
|
||||
0 => self.bar_set.empty,
|
||||
1 => self.bar_set.one_eighth,
|
||||
2 => self.bar_set.one_quarter,
|
||||
3 => self.bar_set.three_eighths,
|
||||
4 => self.bar_set.half,
|
||||
5 => self.bar_set.five_eighths,
|
||||
6 => self.bar_set.three_quarters,
|
||||
7 => self.bar_set.seven_eighths,
|
||||
_ => self.bar_set.full,
|
||||
};
|
||||
|
||||
for x in 0..self.bar_width {
|
||||
buf.get_mut(
|
||||
chart_area.left() + i as u16 * (self.bar_width + self.bar_gap) + x,
|
||||
chart_area.top() + j,
|
||||
).set_symbol(symbol)
|
||||
.set_style(self.style);
|
||||
)
|
||||
.set_symbol(symbol)
|
||||
.set_style(self.bar_style);
|
||||
}
|
||||
|
||||
if d.1 > 8 {
|
||||
@@ -170,11 +200,12 @@ impl<'a> Widget for BarChart<'a> {
|
||||
let width = value_label.width() as u16;
|
||||
if width < self.bar_width {
|
||||
buf.set_string(
|
||||
chart_area.left() + i as u16 * (self.bar_width + self.bar_gap)
|
||||
chart_area.left()
|
||||
+ i as u16 * (self.bar_width + self.bar_gap)
|
||||
+ (self.bar_width - width) / 2,
|
||||
chart_area.bottom() - 2,
|
||||
value_label,
|
||||
&self.value_style,
|
||||
self.value_style,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -183,7 +214,7 @@ impl<'a> Widget for BarChart<'a> {
|
||||
chart_area.bottom() - 1,
|
||||
label,
|
||||
self.bar_width as usize,
|
||||
&self.label_style,
|
||||
self.label_style,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,88 @@
|
||||
use buffer::Buffer;
|
||||
use layout::Rect;
|
||||
use style::Style;
|
||||
use widgets::{Borders, Widget};
|
||||
use symbols::line;
|
||||
#[path = "../title.rs"]
|
||||
pub mod title;
|
||||
|
||||
use self::title::{Position, Title};
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
style::{Style, Styled},
|
||||
symbols::line,
|
||||
widgets::{Borders, Widget},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BorderType {
|
||||
Plain,
|
||||
Rounded,
|
||||
Double,
|
||||
Thick,
|
||||
}
|
||||
|
||||
impl BorderType {
|
||||
pub const fn line_symbols(border_type: BorderType) -> line::Set {
|
||||
match border_type {
|
||||
BorderType::Plain => line::NORMAL,
|
||||
BorderType::Rounded => line::ROUNDED,
|
||||
BorderType::Double => line::DOUBLE,
|
||||
BorderType::Thick => line::THICK,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct Padding {
|
||||
pub left: u16,
|
||||
pub right: u16,
|
||||
pub top: u16,
|
||||
pub bottom: u16,
|
||||
}
|
||||
|
||||
impl Padding {
|
||||
pub const fn new(left: u16, right: u16, top: u16, bottom: u16) -> Self {
|
||||
Padding {
|
||||
left,
|
||||
right,
|
||||
top,
|
||||
bottom,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn zero() -> Self {
|
||||
Padding {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn horizontal(value: u16) -> Self {
|
||||
Padding {
|
||||
left: value,
|
||||
right: value,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn vertical(value: u16) -> Self {
|
||||
Padding {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: value,
|
||||
bottom: value,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn uniform(value: u16) -> Self {
|
||||
Padding {
|
||||
left: value,
|
||||
right: value,
|
||||
top: value,
|
||||
bottom: value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Base widget to be used with all upper level ones. It may be used to display a box border around
|
||||
/// the widget and/or add a title.
|
||||
@@ -10,114 +90,257 @@ use symbols::line;
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # extern crate tui;
|
||||
/// # use tui::widgets::{Block, Borders};
|
||||
/// # use tui::style::{Style, Color};
|
||||
/// # fn main() {
|
||||
/// # use ratatui::widgets::{Block, BorderType, Borders};
|
||||
/// # use ratatui::style::{Style, Color};
|
||||
/// Block::default()
|
||||
/// .title("Block")
|
||||
/// .title_style(Style::default().fg(Color::Red))
|
||||
/// .borders(Borders::LEFT | Borders::RIGHT)
|
||||
/// .border_style(Style::default().fg(Color::White))
|
||||
/// .border_type(BorderType::Rounded)
|
||||
/// .style(Style::default().bg(Color::Black));
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Clone, Copy)]
|
||||
///
|
||||
/// You may also use multiple titles like in the following:
|
||||
/// ```
|
||||
/// # use ratatui::widgets::{Block, BorderType, Borders, block::title::{Position, Title}};
|
||||
/// # use ratatui::style::{Style, Color};
|
||||
/// Block::default()
|
||||
/// .title("Title 1")
|
||||
/// .title(Title::from("Title 2").position(Position::Bottom))
|
||||
/// .borders(Borders::LEFT | Borders::RIGHT)
|
||||
/// .border_style(Style::default().fg(Color::White))
|
||||
/// .border_type(BorderType::Rounded)
|
||||
/// .style(Style::default().bg(Color::Black));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Block<'a> {
|
||||
/// Optional title place on the upper left of the block
|
||||
title: Option<&'a str>,
|
||||
/// Title style
|
||||
title_style: Style,
|
||||
/// List of titles
|
||||
titles: Vec<Title<'a>>,
|
||||
/// The style to be patched to all titles of the block
|
||||
titles_style: Style,
|
||||
/// The default alignment of the titles that don't have one
|
||||
titles_alignment: Alignment,
|
||||
/// The default position of the titles that don't have one
|
||||
titles_position: Position,
|
||||
|
||||
/// Visible borders
|
||||
borders: Borders,
|
||||
/// Border style
|
||||
border_style: Style,
|
||||
/// Type of the border. The default is plain lines but one can choose to have rounded or
|
||||
/// doubled lines instead.
|
||||
border_type: BorderType,
|
||||
|
||||
/// Widget style
|
||||
style: Style,
|
||||
/// Block padding
|
||||
padding: Padding,
|
||||
}
|
||||
|
||||
impl<'a> Default for Block<'a> {
|
||||
fn default() -> Block<'a> {
|
||||
Block {
|
||||
title: None,
|
||||
title_style: Default::default(),
|
||||
borders: Borders::NONE,
|
||||
border_style: Default::default(),
|
||||
style: Default::default(),
|
||||
}
|
||||
Block::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Block<'a> {
|
||||
pub fn title(mut self, title: &'a str) -> Block<'a> {
|
||||
self.title = Some(title);
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
titles: Vec::new(),
|
||||
titles_style: Style::new(),
|
||||
titles_alignment: Alignment::Left,
|
||||
titles_position: Position::Top,
|
||||
borders: Borders::NONE,
|
||||
border_style: Style::new(),
|
||||
border_type: BorderType::Plain,
|
||||
style: Style::new(),
|
||||
padding: Padding::zero(),
|
||||
}
|
||||
}
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use ratatui::widgets::{Block, block::title::Title};
|
||||
/// # use ratatui::layout::Alignment;
|
||||
/// Block::default()
|
||||
/// .title("Title") // By default in the top right corner
|
||||
/// .title(Title::from("Left").alignment(Alignment::Left))
|
||||
/// .title(
|
||||
/// Title::from("Center")
|
||||
/// .alignment(Alignment::Center),
|
||||
/// );
|
||||
/// ```
|
||||
/// Adds a title to the block.
|
||||
///
|
||||
/// The `title` function allows you to add a title to the block. You can call this function
|
||||
/// multiple times to add multiple titles.
|
||||
///
|
||||
/// Each title will be rendered with a single space separating titles that are in the same
|
||||
/// position or alignment. When both centered and non-centered titles are rendered, the centered
|
||||
/// space is calculated based on the full width of the block, rather than the leftover width.
|
||||
///
|
||||
/// You can provide various types as the title, including strings, string slices, borrowed
|
||||
/// strings (`Cow<str>`), spans, or vectors of spans (`Vec<Span>`).
|
||||
///
|
||||
/// By default, the titles will avoid being rendered in the corners of the block but will align
|
||||
/// against the left or right edge of the block if there is no border on that edge.
|
||||
///
|
||||
/// Note: If the block is too small and multiple titles overlap, the border might get cut off at
|
||||
/// a corner.
|
||||
pub fn title<T>(mut self, title: T) -> Block<'a>
|
||||
where
|
||||
T: Into<Title<'a>>,
|
||||
{
|
||||
self.titles.push(title.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn title_style(mut self, style: Style) -> Block<'a> {
|
||||
self.title_style = style;
|
||||
/// Applies the style to all titles. If a title already has a style, it will add on top of it.
|
||||
pub const fn title_style(mut self, style: Style) -> Block<'a> {
|
||||
self.titles_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn border_style(mut self, style: Style) -> Block<'a> {
|
||||
/// Aligns all elements that don't have an alignment
|
||||
/// # Example
|
||||
/// This example aligns all titles in the center except "right" title
|
||||
/// ```
|
||||
/// # use ratatui::widgets::{Block, block::title::Title};
|
||||
/// # use ratatui::layout::Alignment;
|
||||
/// Block::default()
|
||||
/// // This title won't be aligned in the center
|
||||
/// .title(Title::from("right").alignment(Alignment::Right))
|
||||
/// .title("foo")
|
||||
/// .title("bar")
|
||||
/// .title_alignment(Alignment::Center);
|
||||
/// ```
|
||||
pub const fn title_alignment(mut self, alignment: Alignment) -> Block<'a> {
|
||||
self.titles_alignment = alignment;
|
||||
self
|
||||
}
|
||||
|
||||
#[deprecated(since = "0.22.0", note = "You should use a `title_position` instead.")]
|
||||
/// This method just calls `title_position` with Position::Bottom
|
||||
pub fn title_on_bottom(self) -> Block<'a> {
|
||||
self.title_position(Position::Bottom)
|
||||
}
|
||||
|
||||
/// Positions all titles that don't have a position
|
||||
/// # Example
|
||||
/// This example position all titles on the bottom except "top" title
|
||||
/// ```
|
||||
/// # use ratatui::widgets::{Block, BorderType, Borders, block::title::{Position, Title}};
|
||||
/// Block::default()
|
||||
/// // This title won't be aligned in the center
|
||||
/// .title(Title::from("top").position(Position::Top))
|
||||
/// .title("foo")
|
||||
/// .title("bar")
|
||||
/// .title_position(Position::Bottom);
|
||||
/// ```
|
||||
pub const fn title_position(mut self, position: Position) -> Block<'a> {
|
||||
self.titles_position = position;
|
||||
self
|
||||
}
|
||||
|
||||
pub const fn border_style(mut self, style: Style) -> Block<'a> {
|
||||
self.border_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: Style) -> Block<'a> {
|
||||
pub const fn style(mut self, style: Style) -> Block<'a> {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn borders(mut self, flag: Borders) -> Block<'a> {
|
||||
pub const fn borders(mut self, flag: Borders) -> Block<'a> {
|
||||
self.borders = flag;
|
||||
self
|
||||
}
|
||||
|
||||
pub const fn border_type(mut self, border_type: BorderType) -> Block<'a> {
|
||||
self.border_type = border_type;
|
||||
self
|
||||
}
|
||||
|
||||
/// Compute the inner area of a block based on its border visibility rules.
|
||||
pub fn inner(&self, area: &Rect) -> Rect {
|
||||
if area.width < 2 || area.height < 2 {
|
||||
return Rect::default();
|
||||
}
|
||||
let mut inner = *area;
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// // Draw a block nested within another block
|
||||
/// use ratatui::{backend::TestBackend, buffer::Buffer, terminal::Terminal, widgets::{Block, Borders}};
|
||||
/// let backend = TestBackend::new(15, 5);
|
||||
/// let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// let outer_block = Block::default()
|
||||
/// .title("Outer Block")
|
||||
/// .borders(Borders::ALL);
|
||||
/// let inner_block = Block::default()
|
||||
/// .title("Inner Block")
|
||||
/// .borders(Borders::ALL);
|
||||
/// terminal.draw(|f| {
|
||||
/// let inner_area = outer_block.inner(f.size());
|
||||
/// f.render_widget(outer_block, f.size());
|
||||
/// f.render_widget(inner_block, inner_area);
|
||||
/// });
|
||||
/// let expected = Buffer::with_lines(vec![
|
||||
/// "┌Outer Block──┐",
|
||||
/// "│┌Inner Block┐│",
|
||||
/// "││ ││",
|
||||
/// "│└───────────┘│",
|
||||
/// "└─────────────┘",
|
||||
/// ]);
|
||||
/// terminal.backend().assert_buffer(&expected);
|
||||
/// ```
|
||||
pub fn inner(&self, area: Rect) -> Rect {
|
||||
let mut inner = area;
|
||||
if self.borders.intersects(Borders::LEFT) {
|
||||
inner.x += 1;
|
||||
inner.width -= 1;
|
||||
inner.x = inner.x.saturating_add(1).min(inner.right());
|
||||
inner.width = inner.width.saturating_sub(1);
|
||||
}
|
||||
if self.borders.intersects(Borders::TOP) || self.title.is_some() {
|
||||
inner.y += 1;
|
||||
inner.height -= 1;
|
||||
if self.borders.intersects(Borders::TOP) || !self.titles.is_empty() {
|
||||
inner.y = inner.y.saturating_add(1).min(inner.bottom());
|
||||
inner.height = inner.height.saturating_sub(1);
|
||||
}
|
||||
if self.borders.intersects(Borders::RIGHT) {
|
||||
inner.width -= 1;
|
||||
inner.width = inner.width.saturating_sub(1);
|
||||
}
|
||||
if self.borders.intersects(Borders::BOTTOM) {
|
||||
inner.height -= 1;
|
||||
inner.height = inner.height.saturating_sub(1);
|
||||
}
|
||||
|
||||
inner.x = inner.x.saturating_add(self.padding.left);
|
||||
inner.y = inner.y.saturating_add(self.padding.top);
|
||||
|
||||
inner.width = inner
|
||||
.width
|
||||
.saturating_sub(self.padding.left + self.padding.right);
|
||||
inner.height = inner
|
||||
.height
|
||||
.saturating_sub(self.padding.top + self.padding.bottom);
|
||||
|
||||
inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Block<'a> {
|
||||
fn draw(&mut self, area: &Rect, buf: &mut Buffer) {
|
||||
if area.width < 2 || area.height < 2 {
|
||||
return;
|
||||
}
|
||||
pub const fn padding(mut self, padding: Padding) -> Block<'a> {
|
||||
self.padding = padding;
|
||||
self
|
||||
}
|
||||
|
||||
self.background(area, buf, self.style.bg);
|
||||
fn render_borders(&self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_style(area, self.style);
|
||||
let symbols = BorderType::line_symbols(self.border_type);
|
||||
|
||||
// Sides
|
||||
if self.borders.intersects(Borders::LEFT) {
|
||||
for y in area.top()..area.bottom() {
|
||||
buf.get_mut(area.left(), y)
|
||||
.set_symbol(line::VERTICAL)
|
||||
.set_symbol(symbols.vertical)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
}
|
||||
if self.borders.intersects(Borders::TOP) {
|
||||
for x in area.left()..area.right() {
|
||||
buf.get_mut(x, area.top())
|
||||
.set_symbol(line::HORIZONTAL)
|
||||
.set_symbol(symbols.horizontal)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
}
|
||||
@@ -125,7 +348,7 @@ impl<'a> Widget for Block<'a> {
|
||||
let x = area.right() - 1;
|
||||
for y in area.top()..area.bottom() {
|
||||
buf.get_mut(x, y)
|
||||
.set_symbol(line::VERTICAL)
|
||||
.set_symbol(symbols.vertical)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
}
|
||||
@@ -133,54 +356,520 @@ impl<'a> Widget for Block<'a> {
|
||||
let y = area.bottom() - 1;
|
||||
for x in area.left()..area.right() {
|
||||
buf.get_mut(x, y)
|
||||
.set_symbol(line::HORIZONTAL)
|
||||
.set_symbol(symbols.horizontal)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
}
|
||||
|
||||
// Corners
|
||||
if self.borders.contains(Borders::LEFT | Borders::TOP) {
|
||||
buf.get_mut(area.left(), area.top())
|
||||
.set_symbol(line::TOP_LEFT)
|
||||
if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) {
|
||||
buf.get_mut(area.right() - 1, area.bottom() - 1)
|
||||
.set_symbol(symbols.bottom_right)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
if self.borders.contains(Borders::RIGHT | Borders::TOP) {
|
||||
buf.get_mut(area.right() - 1, area.top())
|
||||
.set_symbol(line::TOP_RIGHT)
|
||||
.set_symbol(symbols.top_right)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
if self.borders.contains(Borders::LEFT | Borders::BOTTOM) {
|
||||
buf.get_mut(area.left(), area.bottom() - 1)
|
||||
.set_symbol(line::BOTTOM_LEFT)
|
||||
.set_symbol(symbols.bottom_left)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) {
|
||||
buf.get_mut(area.right() - 1, area.bottom() - 1)
|
||||
.set_symbol(line::BOTTOM_RIGHT)
|
||||
if self.borders.contains(Borders::LEFT | Borders::TOP) {
|
||||
buf.get_mut(area.left(), area.top())
|
||||
.set_symbol(symbols.top_left)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
|
||||
if area.width > 2 {
|
||||
if let Some(title) = self.title {
|
||||
let lx = if self.borders.intersects(Borders::LEFT) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let rx = if self.borders.intersects(Borders::RIGHT) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let width = area.width - lx - rx;
|
||||
buf.set_stringn(
|
||||
area.left() + lx,
|
||||
area.top(),
|
||||
title,
|
||||
width as usize,
|
||||
&self.title_style,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Titles Rendering */
|
||||
fn get_title_y(&self, position: Position, area: Rect) -> u16 {
|
||||
match position {
|
||||
Position::Bottom => area.bottom() - 1,
|
||||
Position::Top => area.top(),
|
||||
}
|
||||
}
|
||||
|
||||
fn title_filter(&self, title: &Title, alignment: Alignment, position: Position) -> bool {
|
||||
title.alignment.unwrap_or(self.titles_alignment) == alignment
|
||||
&& title.position.unwrap_or(self.titles_position) == position
|
||||
}
|
||||
|
||||
fn calculate_title_area_offsets(&self, area: Rect) -> (u16, u16, u16) {
|
||||
let left_border_dx = u16::from(self.borders.intersects(Borders::LEFT));
|
||||
let right_border_dx = u16::from(self.borders.intersects(Borders::RIGHT));
|
||||
|
||||
let title_area_width = area
|
||||
.width
|
||||
.saturating_sub(left_border_dx)
|
||||
.saturating_sub(right_border_dx);
|
||||
|
||||
(left_border_dx, right_border_dx, title_area_width)
|
||||
}
|
||||
|
||||
fn render_left_titles(&self, position: Position, area: Rect, buf: &mut Buffer) {
|
||||
let (left_border_dx, _, title_area_width) = self.calculate_title_area_offsets(area);
|
||||
|
||||
let mut current_offset = left_border_dx;
|
||||
self.titles
|
||||
.iter()
|
||||
.filter(|title| self.title_filter(title, Alignment::Left, position))
|
||||
.for_each(|title| {
|
||||
let title_x = current_offset;
|
||||
current_offset += title.content.width() as u16 + 1;
|
||||
|
||||
buf.set_line(
|
||||
title_x + area.left(),
|
||||
self.get_title_y(position, area),
|
||||
&title.content,
|
||||
title_area_width,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn render_center_titles(&self, position: Position, area: Rect, buf: &mut Buffer) {
|
||||
let (_, _, title_area_width) = self.calculate_title_area_offsets(area);
|
||||
|
||||
let titles = self
|
||||
.titles
|
||||
.iter()
|
||||
.filter(|title| self.title_filter(title, Alignment::Center, position));
|
||||
|
||||
let titles_sum = titles
|
||||
.clone()
|
||||
.fold(-1, |acc, f| acc + f.content.width() as i16 + 1); // First element isn't spaced
|
||||
|
||||
let mut current_offset = area.width.saturating_sub(titles_sum as u16) / 2;
|
||||
titles.for_each(|title| {
|
||||
let title_x = current_offset;
|
||||
current_offset += title.content.width() as u16 + 1;
|
||||
|
||||
buf.set_line(
|
||||
title_x + area.left(),
|
||||
self.get_title_y(position, area),
|
||||
&title.content,
|
||||
title_area_width,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn render_right_titles(&self, position: Position, area: Rect, buf: &mut Buffer) {
|
||||
let (_, right_border_dx, title_area_width) = self.calculate_title_area_offsets(area);
|
||||
|
||||
let mut current_offset = right_border_dx;
|
||||
self.titles
|
||||
.iter()
|
||||
.filter(|title| self.title_filter(title, Alignment::Right, position))
|
||||
.rev() // so that the titles appear in the order they have been set
|
||||
.for_each(|title| {
|
||||
current_offset += title.content.width() as u16 + 1;
|
||||
let title_x = current_offset - 1; // First element isn't spaced
|
||||
|
||||
buf.set_line(
|
||||
area.width.saturating_sub(title_x) + area.left(),
|
||||
self.get_title_y(position, area),
|
||||
&title.content,
|
||||
title_area_width,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn render_title_position(&self, position: Position, area: Rect, buf: &mut Buffer) {
|
||||
// Note: the order in which these functions are called define the overlapping behavior
|
||||
self.render_right_titles(position, area, buf);
|
||||
self.render_center_titles(position, area, buf);
|
||||
self.render_left_titles(position, area, buf);
|
||||
}
|
||||
|
||||
fn render_titles(&self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_title_position(Position::Top, area, buf);
|
||||
self.render_title_position(Position::Bottom, area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Block<'a> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
if area.area() == 0 {
|
||||
return;
|
||||
}
|
||||
self.render_borders(area, buf);
|
||||
self.render_titles(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Styled for Block<'a> {
|
||||
type Item = Block<'a>;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self::Item {
|
||||
self.style(style)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::layout::Rect;
|
||||
|
||||
#[test]
|
||||
fn inner_takes_into_account_the_borders() {
|
||||
// No borders
|
||||
assert_eq!(
|
||||
Block::default().inner(Rect::default()),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0
|
||||
},
|
||||
"no borders, width=0, height=0"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
},
|
||||
"no borders, width=1, height=1"
|
||||
);
|
||||
|
||||
// Left border
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::LEFT).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1
|
||||
},
|
||||
"left, width=0"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::LEFT).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 1,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1
|
||||
},
|
||||
"left, width=1"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::LEFT).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 1,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
},
|
||||
"left, width=2"
|
||||
);
|
||||
|
||||
// Top border
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::TOP).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 0
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 0
|
||||
},
|
||||
"top, height=0"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::TOP).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 1,
|
||||
width: 1,
|
||||
height: 0
|
||||
},
|
||||
"top, height=1"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::TOP).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 2
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 1,
|
||||
width: 1,
|
||||
height: 1
|
||||
},
|
||||
"top, height=2"
|
||||
);
|
||||
|
||||
// Right border
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::RIGHT).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1
|
||||
},
|
||||
"right, width=0"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::RIGHT).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1
|
||||
},
|
||||
"right, width=1"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::RIGHT).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
},
|
||||
"right, width=2"
|
||||
);
|
||||
|
||||
// Bottom border
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::BOTTOM).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 0
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 0
|
||||
},
|
||||
"bottom, height=0"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::BOTTOM).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 0
|
||||
},
|
||||
"bottom, height=1"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::BOTTOM).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 2
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
},
|
||||
"bottom, height=2"
|
||||
);
|
||||
|
||||
// All borders
|
||||
assert_eq!(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.inner(Rect::default()),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0
|
||||
},
|
||||
"all borders, width=0, height=0"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::ALL).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
"all borders, width=1, height=1"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::ALL).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 2,
|
||||
}),
|
||||
Rect {
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
"all borders, width=2, height=2"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::ALL).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 3,
|
||||
height: 3,
|
||||
}),
|
||||
Rect {
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
"all borders, width=3, height=3"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inner_takes_into_account_the_title() {
|
||||
assert_eq!(
|
||||
Block::default().title("Test").inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1,
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 1,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default()
|
||||
.title(Title::from("Test").alignment(Alignment::Center))
|
||||
.inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1,
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 1,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default()
|
||||
.title(Title::from("Test").alignment(Alignment::Right))
|
||||
.inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1,
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 1,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn border_type_can_be_const() {
|
||||
const _PLAIN: line::Set = BorderType::line_symbols(BorderType::Plain);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn padding_can_be_const() {
|
||||
const PADDING: Padding = Padding::new(1, 1, 1, 1);
|
||||
const UNI_PADDING: Padding = Padding::uniform(1);
|
||||
assert_eq!(PADDING, UNI_PADDING);
|
||||
|
||||
const _NO_PADDING: Padding = Padding::zero();
|
||||
const _HORIZONTAL: Padding = Padding::horizontal(1);
|
||||
const _VERTICAL: Padding = Padding::vertical(1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_can_be_const() {
|
||||
const _DEFAULT_STYLE: Style = Style::new();
|
||||
const _DEFAULT_PADDING: Padding = Padding::uniform(1);
|
||||
const _DEFAULT_BLOCK: Block = Block::new()
|
||||
.title_style(_DEFAULT_STYLE)
|
||||
.title_alignment(Alignment::Left)
|
||||
.title_position(Position::Top)
|
||||
.borders(Borders::ALL)
|
||||
.border_style(_DEFAULT_STYLE)
|
||||
.style(_DEFAULT_STYLE)
|
||||
.padding(_DEFAULT_PADDING);
|
||||
}
|
||||
}
|
||||
|
||||
246
src/widgets/calendar.rs
Normal file
246
src/widgets/calendar.rs
Normal file
@@ -0,0 +1,246 @@
|
||||
//! A simple calendar widget. `(feature: widget-calendar)`
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//! The [`Monthly`] widget will display a calendar for the monh provided in `display_date`. Days are
|
||||
//! styled using the default style unless:
|
||||
//! * `show_surrounding` is set, then days not in the `display_date` month will use that style.
|
||||
//! * a style is returned by the [`DateStyler`] for the day
|
||||
//!
|
||||
//! [`Monthly`] has several controls for what should be displayed
|
||||
use std::collections::HashMap;
|
||||
|
||||
use time::{Date, Duration, OffsetDateTime};
|
||||
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::Style,
|
||||
text::Span,
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
|
||||
/// Display a month calendar for the month containing `display_date`
|
||||
pub struct Monthly<'a, S: DateStyler> {
|
||||
display_date: Date,
|
||||
events: S,
|
||||
show_surrounding: Option<Style>,
|
||||
show_weekday: Option<Style>,
|
||||
show_month: Option<Style>,
|
||||
default_style: Style,
|
||||
block: Option<Block<'a>>,
|
||||
}
|
||||
|
||||
impl<'a, S: DateStyler> Monthly<'a, S> {
|
||||
/// Construct a calendar for the `display_date` and highlight the `events`
|
||||
pub fn new(display_date: Date, events: S) -> Self {
|
||||
Self {
|
||||
display_date,
|
||||
events,
|
||||
show_surrounding: None,
|
||||
show_weekday: None,
|
||||
show_month: None,
|
||||
default_style: Style::default(),
|
||||
block: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Fill the calendar slots for days not in the current month also, this causes each line to be
|
||||
/// completely filled. If there is an event style for a date, this style will be patched with
|
||||
/// the event's style
|
||||
pub fn show_surrounding(mut self, style: Style) -> Self {
|
||||
self.show_surrounding = Some(style);
|
||||
self
|
||||
}
|
||||
|
||||
/// Display a header containing weekday abbreviations
|
||||
pub fn show_weekdays_header(mut self, style: Style) -> Self {
|
||||
self.show_weekday = Some(style);
|
||||
self
|
||||
}
|
||||
|
||||
/// Display a header containing the month and year
|
||||
pub fn show_month_header(mut self, style: Style) -> Self {
|
||||
self.show_month = Some(style);
|
||||
self
|
||||
}
|
||||
|
||||
/// How to render otherwise unstyled dates
|
||||
pub fn default_style(mut self, s: Style) -> Self {
|
||||
self.default_style = s;
|
||||
self
|
||||
}
|
||||
|
||||
/// Render the calendar within a [Block](crate::widgets::Block)
|
||||
pub fn block(mut self, b: Block<'a>) -> Self {
|
||||
self.block = Some(b);
|
||||
self
|
||||
}
|
||||
|
||||
/// Return a style with only the background from the default style
|
||||
fn default_bg(&self) -> Style {
|
||||
match self.default_style.bg {
|
||||
None => Style::default(),
|
||||
Some(c) => Style::default().bg(c),
|
||||
}
|
||||
}
|
||||
|
||||
/// All logic to style a date goes here.
|
||||
fn format_date(&self, date: Date) -> Span {
|
||||
if date.month() != self.display_date.month() {
|
||||
match self.show_surrounding {
|
||||
None => Span::styled(" ", self.default_bg()),
|
||||
Some(s) => {
|
||||
let style = self
|
||||
.default_style
|
||||
.patch(s)
|
||||
.patch(self.events.get_style(date));
|
||||
Span::styled(format!("{:2?}", date.day()), style)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Span::styled(
|
||||
format!("{:2?}", date.day()),
|
||||
self.default_style.patch(self.events.get_style(date)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, S: DateStyler> Widget for Monthly<'a, S> {
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer) {
|
||||
// Block is used for borders and such
|
||||
// Draw that first, and use the blank area inside the block for our own purposes
|
||||
let mut area = match self.block.take() {
|
||||
None => area,
|
||||
Some(b) => {
|
||||
let inner = b.inner(area);
|
||||
b.render(area, buf);
|
||||
inner
|
||||
}
|
||||
};
|
||||
|
||||
// Draw the month name and year
|
||||
if let Some(style) = self.show_month {
|
||||
let line = Span::styled(
|
||||
format!("{} {}", self.display_date.month(), self.display_date.year()),
|
||||
style,
|
||||
);
|
||||
// cal is 21 cells wide, so hard code the 11
|
||||
let x_off = 11_u16.saturating_sub(line.width() as u16 / 2);
|
||||
buf.set_line(area.x + x_off, area.y, &line.into(), area.width);
|
||||
area.y += 1
|
||||
}
|
||||
|
||||
// Draw days of week
|
||||
if let Some(style) = self.show_weekday {
|
||||
let days = String::from(" Su Mo Tu We Th Fr Sa");
|
||||
buf.set_string(area.x, area.y, days, style);
|
||||
area.y += 1;
|
||||
}
|
||||
|
||||
// Set the start of the calendar to the Sunday before the 1st (or the sunday of the first)
|
||||
let first_of_month = self.display_date.replace_day(1).unwrap();
|
||||
let offset = Duration::days(first_of_month.weekday().number_days_from_sunday().into());
|
||||
let mut curr_day = first_of_month - offset;
|
||||
|
||||
// go through all the weeks containing a day in the target month.
|
||||
while curr_day.month() as u8 != self.display_date.month().next() as u8 {
|
||||
let mut spans = Vec::with_capacity(14);
|
||||
for i in 0..7 {
|
||||
// Draw the gutter. Do it here so we can avoid worrying about
|
||||
// styling the ' ' in the format_date method
|
||||
if i == 0 {
|
||||
spans.push(Span::styled(" ", Style::default()));
|
||||
} else {
|
||||
spans.push(Span::styled(" ", self.default_bg()));
|
||||
}
|
||||
spans.push(self.format_date(curr_day));
|
||||
curr_day += Duration::DAY;
|
||||
}
|
||||
buf.set_line(area.x, area.y, &spans.into(), area.width);
|
||||
area.y += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides a method for styling a given date. [Monthly] is generic on this trait, so any type
|
||||
/// that implements this trait can be used.
|
||||
pub trait DateStyler {
|
||||
/// Given a date, return a style for that date
|
||||
fn get_style(&self, date: Date) -> Style;
|
||||
}
|
||||
|
||||
/// A simple `DateStyler` based on a [`HashMap`]
|
||||
pub struct CalendarEventStore(pub HashMap<Date, Style>);
|
||||
|
||||
impl CalendarEventStore {
|
||||
/// Construct a store that has the current date styled.
|
||||
pub fn today(style: Style) -> Self {
|
||||
let mut res = Self::default();
|
||||
res.add(OffsetDateTime::now_local().unwrap().date(), style);
|
||||
res
|
||||
}
|
||||
|
||||
/// Add a date and style to the store
|
||||
pub fn add(&mut self, date: Date, style: Style) {
|
||||
// to simplify style nonsense, last write wins
|
||||
let _ = self.0.insert(date, style);
|
||||
}
|
||||
|
||||
/// Helper for trait impls
|
||||
fn lookup_style(&self, date: Date) -> Style {
|
||||
self.0.get(&date).copied().unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl DateStyler for CalendarEventStore {
|
||||
fn get_style(&self, date: Date) -> Style {
|
||||
self.lookup_style(date)
|
||||
}
|
||||
}
|
||||
|
||||
impl DateStyler for &CalendarEventStore {
|
||||
fn get_style(&self, date: Date) -> Style {
|
||||
self.lookup_style(date)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CalendarEventStore {
|
||||
fn default() -> Self {
|
||||
Self(HashMap::with_capacity(4))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use time::Month;
|
||||
|
||||
use super::*;
|
||||
use crate::style::Color;
|
||||
|
||||
#[test]
|
||||
fn event_store() {
|
||||
let a = (
|
||||
Date::from_calendar_date(2023, Month::January, 1).unwrap(),
|
||||
Style::default(),
|
||||
);
|
||||
let b = (
|
||||
Date::from_calendar_date(2023, Month::January, 2).unwrap(),
|
||||
Style::default().bg(Color::Red).fg(Color::Blue),
|
||||
);
|
||||
let mut s = CalendarEventStore::default();
|
||||
s.add(b.0, b.1);
|
||||
|
||||
assert_eq!(
|
||||
s.get_style(a.0),
|
||||
a.1,
|
||||
"Date not added to the styler should look up as Style::default()"
|
||||
);
|
||||
assert_eq!(
|
||||
s.get_style(b.0),
|
||||
b.1,
|
||||
"Date added to styler should return the provided style"
|
||||
);
|
||||
}
|
||||
}
|
||||
66
src/widgets/canvas/circle.rs
Normal file
66
src/widgets/canvas/circle.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use crate::{
|
||||
style::Color,
|
||||
widgets::canvas::{Painter, Shape},
|
||||
};
|
||||
|
||||
/// Shape to draw a circle with a given center and radius and with the given color
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Circle {
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
pub radius: f64,
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
impl Shape for Circle {
|
||||
fn draw(&self, painter: &mut Painter<'_, '_>) {
|
||||
for angle in 0..360 {
|
||||
let radians = f64::from(angle).to_radians();
|
||||
let circle_x = self.radius.mul_add(radians.cos(), self.x);
|
||||
let circle_y = self.radius.mul_add(radians.sin(), self.y);
|
||||
if let Some((x, y)) = painter.get_point(circle_x, circle_y) {
|
||||
painter.paint(x, y, self.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::Color,
|
||||
symbols::Marker,
|
||||
widgets::{
|
||||
canvas::{Canvas, Circle},
|
||||
Widget,
|
||||
},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_it_draws_a_circle() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 5));
|
||||
let canvas = Canvas::default()
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&Circle {
|
||||
x: 5.0,
|
||||
y: 2.0,
|
||||
radius: 5.0,
|
||||
color: Color::Reset,
|
||||
});
|
||||
})
|
||||
.marker(Marker::Braille)
|
||||
.x_bounds([-10.0, 10.0])
|
||||
.y_bounds([-10.0, 10.0]);
|
||||
canvas.render(buffer.area, &mut buffer);
|
||||
let expected = Buffer::with_lines(vec![
|
||||
" ⢀⣠⢤⣀ ",
|
||||
" ⢰⠋ ⠈⣇",
|
||||
" ⠘⣆⡀ ⣠⠇",
|
||||
" ⠉⠉⠁ ",
|
||||
" ",
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
use super::Shape;
|
||||
use style::Color;
|
||||
use crate::{
|
||||
style::Color,
|
||||
widgets::canvas::{Painter, Shape},
|
||||
};
|
||||
|
||||
/// Shape to draw a line from (x1, y1) to (x2, y2) with the given color
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Line {
|
||||
pub x1: f64,
|
||||
pub y1: f64,
|
||||
@@ -10,60 +13,81 @@ pub struct Line {
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
pub struct LineIterator {
|
||||
x: f64,
|
||||
y: f64,
|
||||
dx: f64,
|
||||
dy: f64,
|
||||
dir_x: f64,
|
||||
dir_y: f64,
|
||||
current: f64,
|
||||
end: f64,
|
||||
}
|
||||
|
||||
impl Iterator for LineIterator {
|
||||
type Item = (f64, f64);
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.current < self.end {
|
||||
let pos = (
|
||||
self.x + (self.current * self.dx) / self.end * self.dir_x,
|
||||
self.y + (self.current * self.dy) / self.end * self.dir_y,
|
||||
);
|
||||
self.current += 1.0;
|
||||
Some(pos)
|
||||
impl Shape for Line {
|
||||
fn draw(&self, painter: &mut Painter) {
|
||||
let Some((x1, y1)) = painter.get_point(self.x1, self.y1) else {
|
||||
return;
|
||||
};
|
||||
let Some((x2, y2)) = painter.get_point(self.x2, self.y2) else {
|
||||
return;
|
||||
};
|
||||
let (dx, x_range) = if x2 >= x1 {
|
||||
(x2 - x1, x1..=x2)
|
||||
} else {
|
||||
None
|
||||
(x1 - x2, x2..=x1)
|
||||
};
|
||||
let (dy, y_range) = if y2 >= y1 {
|
||||
(y2 - y1, y1..=y2)
|
||||
} else {
|
||||
(y1 - y2, y2..=y1)
|
||||
};
|
||||
|
||||
if dx == 0 {
|
||||
for y in y_range {
|
||||
painter.paint(x1, y, self.color);
|
||||
}
|
||||
} else if dy == 0 {
|
||||
for x in x_range {
|
||||
painter.paint(x, y1, self.color);
|
||||
}
|
||||
} else if dy < dx {
|
||||
if x1 > x2 {
|
||||
draw_line_low(painter, x2, y2, x1, y1, self.color);
|
||||
} else {
|
||||
draw_line_low(painter, x1, y1, x2, y2, self.color);
|
||||
}
|
||||
} else if y1 > y2 {
|
||||
draw_line_high(painter, x2, y2, x1, y1, self.color);
|
||||
} else {
|
||||
draw_line_high(painter, x1, y1, x2, y2, self.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a Line {
|
||||
type Item = (f64, f64);
|
||||
type IntoIter = LineIterator;
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
let dx = self.x1.max(self.x2) - self.x1.min(self.x2);
|
||||
let dy = self.y1.max(self.y2) - self.y1.min(self.y2);
|
||||
let dir_x = if self.x1 <= self.x2 { 1.0 } else { -1.0 };
|
||||
let dir_y = if self.y1 <= self.y2 { 1.0 } else { -1.0 };
|
||||
let end = dx.max(dy);
|
||||
LineIterator {
|
||||
x: self.x1,
|
||||
y: self.y1,
|
||||
dx: dx,
|
||||
dy: dy,
|
||||
dir_x: dir_x,
|
||||
dir_y: dir_y,
|
||||
current: 0.0,
|
||||
end: end,
|
||||
fn draw_line_low(painter: &mut Painter, x1: usize, y1: usize, x2: usize, y2: usize, color: Color) {
|
||||
let dx = (x2 - x1) as isize;
|
||||
let dy = (y2 as isize - y1 as isize).abs();
|
||||
let mut d = 2 * dy - dx;
|
||||
let mut y = y1;
|
||||
for x in x1..=x2 {
|
||||
painter.paint(x, y, color);
|
||||
if d > 0 {
|
||||
y = if y1 > y2 {
|
||||
y.saturating_sub(1)
|
||||
} else {
|
||||
y.saturating_add(1)
|
||||
};
|
||||
d -= 2 * dx;
|
||||
}
|
||||
d += 2 * dy;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Shape<'a> for Line {
|
||||
fn color(&self) -> Color {
|
||||
self.color
|
||||
}
|
||||
fn points(&'a self) -> Box<Iterator<Item = (f64, f64)> + 'a> {
|
||||
Box::new(self.into_iter())
|
||||
fn draw_line_high(painter: &mut Painter, x1: usize, y1: usize, x2: usize, y2: usize, color: Color) {
|
||||
let dx = (x2 as isize - x1 as isize).abs();
|
||||
let dy = (y2 - y1) as isize;
|
||||
let mut d = 2 * dx - dy;
|
||||
let mut x = x1;
|
||||
for y in y1..=y2 {
|
||||
painter.paint(x, y, color);
|
||||
if d > 0 {
|
||||
x = if x1 > x2 {
|
||||
x.saturating_sub(1)
|
||||
} else {
|
||||
x.saturating_add(1)
|
||||
};
|
||||
d -= 2 * dy;
|
||||
}
|
||||
d += 2 * dx;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
use widgets::canvas::Shape;
|
||||
use widgets::canvas::points::PointsIterator;
|
||||
use widgets::canvas::world::{WORLD_HIGH_RESOLUTION, WORLD_LOW_RESOLUTION};
|
||||
use style::Color;
|
||||
use crate::{
|
||||
style::Color,
|
||||
widgets::canvas::{
|
||||
world::{WORLD_HIGH_RESOLUTION, WORLD_LOW_RESOLUTION},
|
||||
Painter, Shape,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum MapResolution {
|
||||
Low,
|
||||
High,
|
||||
}
|
||||
|
||||
impl MapResolution {
|
||||
fn data(&self) -> &'static [(f64, f64)] {
|
||||
match *self {
|
||||
fn data(self) -> &'static [(f64, f64)] {
|
||||
match self {
|
||||
MapResolution::Low => &WORLD_LOW_RESOLUTION,
|
||||
MapResolution::High => &WORLD_HIGH_RESOLUTION,
|
||||
}
|
||||
@@ -19,6 +22,7 @@ impl MapResolution {
|
||||
}
|
||||
|
||||
/// Shape to draw a world map with the given resolution and color
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Map {
|
||||
pub resolution: MapResolution,
|
||||
pub color: Color,
|
||||
@@ -33,19 +37,12 @@ impl Default for Map {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Shape<'a> for Map {
|
||||
fn color(&self) -> Color {
|
||||
self.color
|
||||
}
|
||||
fn points(&'a self) -> Box<Iterator<Item = (f64, f64)> + 'a> {
|
||||
Box::new(self.into_iter())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a Map {
|
||||
type Item = (f64, f64);
|
||||
type IntoIter = PointsIterator<'a>;
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
PointsIterator::from(self.resolution.data())
|
||||
impl Shape for Map {
|
||||
fn draw(&self, painter: &mut Painter) {
|
||||
for (x, y) in self.resolution.data() {
|
||||
if let Some((x, y)) = painter.get_point(*x, *y) {
|
||||
painter.paint(x, y, self.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,59 +1,91 @@
|
||||
mod points;
|
||||
mod circle;
|
||||
mod line;
|
||||
mod map;
|
||||
mod points;
|
||||
mod rectangle;
|
||||
mod world;
|
||||
|
||||
pub use self::points::Points;
|
||||
pub use self::line::Line;
|
||||
pub use self::map::{Map, MapResolution};
|
||||
use std::fmt::Debug;
|
||||
|
||||
use style::{Color, Style};
|
||||
use buffer::Buffer;
|
||||
use widgets::{Block, Widget};
|
||||
use layout::Rect;
|
||||
|
||||
pub const DOTS: [[u16; 2]; 4] = [
|
||||
[0x0001, 0x0008],
|
||||
[0x0002, 0x0010],
|
||||
[0x0004, 0x0020],
|
||||
[0x0040, 0x0080],
|
||||
];
|
||||
pub const BRAILLE_OFFSET: u16 = 0x2800;
|
||||
pub const BRAILLE_BLANK: char = '⠀';
|
||||
pub use self::{
|
||||
circle::Circle,
|
||||
line::Line,
|
||||
map::{Map, MapResolution},
|
||||
points::Points,
|
||||
rectangle::Rectangle,
|
||||
};
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
symbols,
|
||||
text::Line as TextLine,
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
|
||||
/// Interface for all shapes that may be drawn on a Canvas widget.
|
||||
pub trait Shape<'a> {
|
||||
/// Returns the color of the shape
|
||||
fn color(&self) -> Color;
|
||||
/// Returns an iterator over all points of the shape
|
||||
fn points(&'a self) -> Box<Iterator<Item = (f64, f64)> + 'a>;
|
||||
pub trait Shape {
|
||||
fn draw(&self, painter: &mut Painter);
|
||||
}
|
||||
|
||||
/// Label to draw some text on the canvas
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Label<'a> {
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
pub text: &'a str,
|
||||
pub color: Color,
|
||||
x: f64,
|
||||
y: f64,
|
||||
line: TextLine<'a>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Layer {
|
||||
string: String,
|
||||
colors: Vec<Color>,
|
||||
}
|
||||
|
||||
struct Grid {
|
||||
trait Grid: Debug {
|
||||
fn width(&self) -> u16;
|
||||
fn height(&self) -> u16;
|
||||
fn resolution(&self) -> (f64, f64);
|
||||
fn paint(&mut self, x: usize, y: usize, color: Color);
|
||||
fn save(&self) -> Layer;
|
||||
fn reset(&mut self);
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct BrailleGrid {
|
||||
width: u16,
|
||||
height: u16,
|
||||
cells: Vec<u16>,
|
||||
colors: Vec<Color>,
|
||||
}
|
||||
|
||||
impl Grid {
|
||||
fn new(width: usize, height: usize) -> Grid {
|
||||
Grid {
|
||||
cells: vec![BRAILLE_OFFSET; width * height],
|
||||
colors: vec![Color::Reset; width * height],
|
||||
impl BrailleGrid {
|
||||
fn new(width: u16, height: u16) -> BrailleGrid {
|
||||
let length = usize::from(width * height);
|
||||
BrailleGrid {
|
||||
width,
|
||||
height,
|
||||
cells: vec![symbols::braille::BLANK; length],
|
||||
colors: vec![Color::Reset; length],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Grid for BrailleGrid {
|
||||
fn width(&self) -> u16 {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> u16 {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn resolution(&self) -> (f64, f64) {
|
||||
(
|
||||
f64::from(self.width) * 2.0 - 1.0,
|
||||
f64::from(self.height) * 4.0 - 1.0,
|
||||
)
|
||||
}
|
||||
|
||||
fn save(&self) -> Layer {
|
||||
Layer {
|
||||
@@ -64,47 +96,201 @@ impl Grid {
|
||||
|
||||
fn reset(&mut self) {
|
||||
for c in &mut self.cells {
|
||||
*c = BRAILLE_OFFSET;
|
||||
*c = symbols::braille::BLANK;
|
||||
}
|
||||
for c in &mut self.colors {
|
||||
*c = Color::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(&mut self, x: usize, y: usize, color: Color) {
|
||||
let index = y / 4 * self.width as usize + x / 2;
|
||||
if let Some(c) = self.cells.get_mut(index) {
|
||||
*c |= symbols::braille::DOTS[y % 4][x % 2];
|
||||
}
|
||||
if let Some(c) = self.colors.get_mut(index) {
|
||||
*c = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct CharGrid {
|
||||
width: u16,
|
||||
height: u16,
|
||||
cells: Vec<char>,
|
||||
colors: Vec<Color>,
|
||||
cell_char: char,
|
||||
}
|
||||
|
||||
impl CharGrid {
|
||||
fn new(width: u16, height: u16, cell_char: char) -> CharGrid {
|
||||
let length = usize::from(width * height);
|
||||
CharGrid {
|
||||
width,
|
||||
height,
|
||||
cells: vec![' '; length],
|
||||
colors: vec![Color::Reset; length],
|
||||
cell_char,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Grid for CharGrid {
|
||||
fn width(&self) -> u16 {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> u16 {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn resolution(&self) -> (f64, f64) {
|
||||
(f64::from(self.width) - 1.0, f64::from(self.height) - 1.0)
|
||||
}
|
||||
|
||||
fn save(&self) -> Layer {
|
||||
Layer {
|
||||
string: self.cells.iter().collect(),
|
||||
colors: self.colors.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
for c in &mut self.cells {
|
||||
*c = ' ';
|
||||
}
|
||||
for c in &mut self.colors {
|
||||
*c = Color::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(&mut self, x: usize, y: usize, color: Color) {
|
||||
let index = y * self.width as usize + x;
|
||||
if let Some(c) = self.cells.get_mut(index) {
|
||||
*c = self.cell_char;
|
||||
}
|
||||
if let Some(c) = self.colors.get_mut(index) {
|
||||
*c = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Painter<'a, 'b> {
|
||||
context: &'a mut Context<'b>,
|
||||
resolution: (f64, f64),
|
||||
}
|
||||
|
||||
impl<'a, 'b> Painter<'a, 'b> {
|
||||
/// Convert the (x, y) coordinates to location of a point on the grid
|
||||
///
|
||||
/// # Examples:
|
||||
/// ```
|
||||
/// use ratatui::{symbols, widgets::canvas::{Painter, Context}};
|
||||
///
|
||||
/// let mut ctx = Context::new(2, 2, [1.0, 2.0], [0.0, 2.0], symbols::Marker::Braille);
|
||||
/// let mut painter = Painter::from(&mut ctx);
|
||||
/// let point = painter.get_point(1.0, 0.0);
|
||||
/// assert_eq!(point, Some((0, 7)));
|
||||
/// let point = painter.get_point(1.5, 1.0);
|
||||
/// assert_eq!(point, Some((1, 3)));
|
||||
/// let point = painter.get_point(0.0, 0.0);
|
||||
/// assert_eq!(point, None);
|
||||
/// let point = painter.get_point(2.0, 2.0);
|
||||
/// assert_eq!(point, Some((3, 0)));
|
||||
/// let point = painter.get_point(1.0, 2.0);
|
||||
/// assert_eq!(point, Some((0, 0)));
|
||||
/// ```
|
||||
pub fn get_point(&self, x: f64, y: f64) -> Option<(usize, usize)> {
|
||||
let left = self.context.x_bounds[0];
|
||||
let right = self.context.x_bounds[1];
|
||||
let top = self.context.y_bounds[1];
|
||||
let bottom = self.context.y_bounds[0];
|
||||
if x < left || x > right || y < bottom || y > top {
|
||||
return None;
|
||||
}
|
||||
let width = (self.context.x_bounds[1] - self.context.x_bounds[0]).abs();
|
||||
let height = (self.context.y_bounds[1] - self.context.y_bounds[0]).abs();
|
||||
if width == 0.0 || height == 0.0 {
|
||||
return None;
|
||||
}
|
||||
let x = ((x - left) * self.resolution.0 / width) as usize;
|
||||
let y = ((top - y) * self.resolution.1 / height) as usize;
|
||||
Some((x, y))
|
||||
}
|
||||
|
||||
/// Paint a point of the grid
|
||||
///
|
||||
/// # Examples:
|
||||
/// ```
|
||||
/// use ratatui::{style::Color, symbols, widgets::canvas::{Painter, Context}};
|
||||
///
|
||||
/// let mut ctx = Context::new(1, 1, [0.0, 2.0], [0.0, 2.0], symbols::Marker::Braille);
|
||||
/// let mut painter = Painter::from(&mut ctx);
|
||||
/// let cell = painter.paint(1, 3, Color::Red);
|
||||
/// ```
|
||||
pub fn paint(&mut self, x: usize, y: usize, color: Color) {
|
||||
self.context.grid.paint(x, y, color);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> From<&'a mut Context<'b>> for Painter<'a, 'b> {
|
||||
fn from(context: &'a mut Context<'b>) -> Painter<'a, 'b> {
|
||||
let resolution = context.grid.resolution();
|
||||
Painter {
|
||||
context,
|
||||
resolution,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds the state of the Canvas when painting to it.
|
||||
#[derive(Debug)]
|
||||
pub struct Context<'a> {
|
||||
width: u16,
|
||||
height: u16,
|
||||
x_bounds: [f64; 2],
|
||||
y_bounds: [f64; 2],
|
||||
grid: Grid,
|
||||
grid: Box<dyn Grid>,
|
||||
dirty: bool,
|
||||
layers: Vec<Layer>,
|
||||
labels: Vec<Label<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Context<'a> {
|
||||
pub fn new(
|
||||
width: u16,
|
||||
height: u16,
|
||||
x_bounds: [f64; 2],
|
||||
y_bounds: [f64; 2],
|
||||
marker: symbols::Marker,
|
||||
) -> Context<'a> {
|
||||
let dot = symbols::DOT.chars().next().unwrap();
|
||||
let block = symbols::block::FULL.chars().next().unwrap();
|
||||
let bar = symbols::bar::HALF.chars().next().unwrap();
|
||||
let grid: Box<dyn Grid> = match marker {
|
||||
symbols::Marker::Dot => Box::new(CharGrid::new(width, height, dot)),
|
||||
symbols::Marker::Block => Box::new(CharGrid::new(width, height, block)),
|
||||
symbols::Marker::Bar => Box::new(CharGrid::new(width, height, bar)),
|
||||
symbols::Marker::Braille => Box::new(BrailleGrid::new(width, height)),
|
||||
};
|
||||
Context {
|
||||
x_bounds,
|
||||
y_bounds,
|
||||
grid,
|
||||
dirty: false,
|
||||
layers: Vec::new(),
|
||||
labels: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw any object that may implement the Shape trait
|
||||
pub fn draw<'b, S>(&mut self, shape: &'b S)
|
||||
pub fn draw<S>(&mut self, shape: &S)
|
||||
where
|
||||
S: Shape<'b>,
|
||||
S: Shape,
|
||||
{
|
||||
self.dirty = true;
|
||||
let left = self.x_bounds[0];
|
||||
let right = self.x_bounds[1];
|
||||
let bottom = self.y_bounds[0];
|
||||
let top = self.y_bounds[1];
|
||||
for (x, y) in shape
|
||||
.points()
|
||||
.filter(|&(x, y)| !(x < left || x > right || y < bottom || y > top))
|
||||
{
|
||||
let dy = ((top - y) * f64::from(self.height - 1) * 4.0 / (top - bottom)) as usize;
|
||||
let dx = ((x - left) * f64::from(self.width - 1) * 2.0 / (right - left)) as usize;
|
||||
let index = dy / 4 * self.width as usize + dx / 2;
|
||||
self.grid.cells[index] |= DOTS[dy % 4][dx % 2];
|
||||
self.grid.colors[index] = shape.color();
|
||||
}
|
||||
let mut painter = Painter::from(self);
|
||||
shape.draw(&mut painter);
|
||||
}
|
||||
|
||||
/// Go one layer above in the canvas.
|
||||
@@ -115,19 +301,21 @@ impl<'a> Context<'a> {
|
||||
}
|
||||
|
||||
/// Print a string on the canvas at the given position
|
||||
pub fn print(&mut self, x: f64, y: f64, text: &'a str, color: Color) {
|
||||
pub fn print<T>(&mut self, x: f64, y: f64, line: T)
|
||||
where
|
||||
T: Into<TextLine<'a>>,
|
||||
{
|
||||
self.labels.push(Label {
|
||||
x: x,
|
||||
y: y,
|
||||
text: text,
|
||||
color: color,
|
||||
x,
|
||||
y,
|
||||
line: line.into(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Push the last layer if necessary
|
||||
fn finish(&mut self) {
|
||||
if self.dirty {
|
||||
self.layer()
|
||||
self.layer();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,37 +325,35 @@ impl<'a> Context<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # extern crate tui;
|
||||
/// # use tui::widgets::{Block, Borders};
|
||||
/// # use tui::widgets::canvas::{Canvas, Shape, Line, Map, MapResolution};
|
||||
/// # use tui::style::Color;
|
||||
/// # fn main() {
|
||||
/// # use ratatui::widgets::{Block, Borders};
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// # use ratatui::widgets::canvas::{Canvas, Shape, Line, Rectangle, Map, MapResolution};
|
||||
/// # use ratatui::style::Color;
|
||||
/// Canvas::default()
|
||||
/// .block(Block::default().title("Canvas").borders(Borders::ALL))
|
||||
/// .x_bounds([-180.0, 180.0])
|
||||
/// .y_bounds([-90.0, 90.0])
|
||||
/// .paint(|ctx| {
|
||||
/// ctx.draw(&Map{
|
||||
/// ctx.draw(&Map {
|
||||
/// resolution: MapResolution::High,
|
||||
/// color: Color::White
|
||||
/// });
|
||||
/// ctx.layer();
|
||||
/// ctx.draw(&Line{
|
||||
/// ctx.draw(&Line {
|
||||
/// x1: 0.0,
|
||||
/// y1: 10.0,
|
||||
/// x2: 10.0,
|
||||
/// y2: 10.0,
|
||||
/// color: Color::White,
|
||||
/// });
|
||||
/// ctx.draw(&Line{
|
||||
/// x1: 10.0,
|
||||
/// y1: 10.0,
|
||||
/// x2: 20.0,
|
||||
/// y2: 20.0,
|
||||
/// ctx.draw(&Rectangle {
|
||||
/// x: 10.0,
|
||||
/// y: 20.0,
|
||||
/// width: 10.0,
|
||||
/// height: 10.0,
|
||||
/// color: Color::Red
|
||||
/// });
|
||||
/// });
|
||||
/// # }
|
||||
/// ```
|
||||
pub struct Canvas<'a, F>
|
||||
where
|
||||
@@ -178,6 +364,7 @@ where
|
||||
y_bounds: [f64; 2],
|
||||
painter: Option<F>,
|
||||
background_color: Color,
|
||||
marker: symbols::Marker,
|
||||
}
|
||||
|
||||
impl<'a, F> Default for Canvas<'a, F>
|
||||
@@ -191,6 +378,7 @@ where
|
||||
y_bounds: [0.0, 0.0],
|
||||
painter: None,
|
||||
background_color: Color::Reset,
|
||||
marker: symbols::Marker::Braille,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -199,98 +387,232 @@ impl<'a, F> Canvas<'a, F>
|
||||
where
|
||||
F: Fn(&mut Context),
|
||||
{
|
||||
pub fn block(&mut self, block: Block<'a>) -> &mut Canvas<'a, F> {
|
||||
pub fn block(mut self, block: Block<'a>) -> Canvas<'a, F> {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
pub fn x_bounds(&mut self, bounds: [f64; 2]) -> &mut Canvas<'a, F> {
|
||||
|
||||
/// Define the viewport of the canvas.
|
||||
/// If you were to "zoom" to a certain part of the world you may want to choose different
|
||||
/// bounds.
|
||||
pub fn x_bounds(mut self, bounds: [f64; 2]) -> Canvas<'a, F> {
|
||||
self.x_bounds = bounds;
|
||||
self
|
||||
}
|
||||
pub fn y_bounds(&mut self, bounds: [f64; 2]) -> &mut Canvas<'a, F> {
|
||||
|
||||
/// Define the viewport of the canvas.
|
||||
///
|
||||
/// If you were to "zoom" to a certain part of the world you may want to choose different
|
||||
/// bounds.
|
||||
pub fn y_bounds(mut self, bounds: [f64; 2]) -> Canvas<'a, F> {
|
||||
self.y_bounds = bounds;
|
||||
self
|
||||
}
|
||||
|
||||
/// Store the closure that will be used to draw to the Canvas
|
||||
pub fn paint(&mut self, f: F) -> &mut Canvas<'a, F> {
|
||||
pub fn paint(mut self, f: F) -> Canvas<'a, F> {
|
||||
self.painter = Some(f);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn background_color(&'a mut self, color: Color) -> &mut Canvas<'a, F> {
|
||||
pub fn background_color(mut self, color: Color) -> Canvas<'a, F> {
|
||||
self.background_color = color;
|
||||
self
|
||||
}
|
||||
|
||||
/// Change the type of points used to draw the shapes. By default the braille patterns are used
|
||||
/// as they provide a more fine grained result but you might want to use the simple dot or
|
||||
/// block instead if the targeted terminal does not support those symbols.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::widgets::canvas::Canvas;
|
||||
/// # use ratatui::symbols;
|
||||
/// Canvas::default().marker(symbols::Marker::Braille).paint(|ctx| {});
|
||||
///
|
||||
/// Canvas::default().marker(symbols::Marker::Dot).paint(|ctx| {});
|
||||
///
|
||||
/// Canvas::default().marker(symbols::Marker::Block).paint(|ctx| {});
|
||||
/// ```
|
||||
pub fn marker(mut self, marker: symbols::Marker) -> Canvas<'a, F> {
|
||||
self.marker = marker;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, F> Widget for Canvas<'a, F>
|
||||
where
|
||||
F: Fn(&mut Context),
|
||||
{
|
||||
fn draw(&mut self, area: &Rect, buf: &mut Buffer) {
|
||||
let canvas_area = match self.block {
|
||||
Some(ref mut b) => {
|
||||
b.draw(area, buf);
|
||||
b.inner(area)
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer) {
|
||||
let canvas_area = match self.block.take() {
|
||||
Some(b) => {
|
||||
let inner_area = b.inner(area);
|
||||
b.render(area, buf);
|
||||
inner_area
|
||||
}
|
||||
None => *area,
|
||||
None => area,
|
||||
};
|
||||
|
||||
buf.set_style(canvas_area, Style::default().bg(self.background_color));
|
||||
|
||||
let width = canvas_area.width as usize;
|
||||
let height = canvas_area.height as usize;
|
||||
|
||||
if let Some(ref painter) = self.painter {
|
||||
// Create a blank context that match the size of the terminal
|
||||
let mut ctx = Context {
|
||||
x_bounds: self.x_bounds,
|
||||
y_bounds: self.y_bounds,
|
||||
width: canvas_area.width,
|
||||
height: canvas_area.height,
|
||||
grid: Grid::new(width, height),
|
||||
dirty: false,
|
||||
layers: Vec::new(),
|
||||
labels: Vec::new(),
|
||||
};
|
||||
// Paint to this context
|
||||
painter(&mut ctx);
|
||||
ctx.finish();
|
||||
let Some(ref painter) = self.painter else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Retreive painted points for each layer
|
||||
for layer in ctx.layers {
|
||||
for (i, (ch, color)) in layer
|
||||
.string
|
||||
.chars()
|
||||
.zip(layer.colors.into_iter())
|
||||
.enumerate()
|
||||
{
|
||||
if ch != BRAILLE_BLANK {
|
||||
let (x, y) = (i % width, i / width);
|
||||
buf.get_mut(x as u16 + canvas_area.left(), y as u16 + canvas_area.top())
|
||||
.set_char(ch)
|
||||
.set_fg(color)
|
||||
.set_bg(self.background_color);
|
||||
}
|
||||
// Create a blank context that match the size of the canvas
|
||||
let mut ctx = Context::new(
|
||||
canvas_area.width,
|
||||
canvas_area.height,
|
||||
self.x_bounds,
|
||||
self.y_bounds,
|
||||
self.marker,
|
||||
);
|
||||
// Paint to this context
|
||||
painter(&mut ctx);
|
||||
ctx.finish();
|
||||
|
||||
// Retrieve painted points for each layer
|
||||
for layer in ctx.layers {
|
||||
for (i, (ch, color)) in layer
|
||||
.string
|
||||
.chars()
|
||||
.zip(layer.colors.into_iter())
|
||||
.enumerate()
|
||||
{
|
||||
if ch != ' ' && ch != '\u{2800}' {
|
||||
let (x, y) = (i % width, i / width);
|
||||
buf.get_mut(x as u16 + canvas_area.left(), y as u16 + canvas_area.top())
|
||||
.set_char(ch)
|
||||
.set_fg(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finally draw the labels
|
||||
let style = Style::default().bg(self.background_color);
|
||||
for label in ctx.labels.iter().filter(|l| {
|
||||
!(l.x < self.x_bounds[0] || l.x > self.x_bounds[1] || l.y < self.y_bounds[0]
|
||||
|| l.y > self.y_bounds[1])
|
||||
}) {
|
||||
let dy = ((self.y_bounds[1] - label.y) * f64::from(canvas_area.height - 1)
|
||||
/ (self.y_bounds[1] - self.y_bounds[0])) as u16;
|
||||
let dx = ((label.x - self.x_bounds[0]) * f64::from(canvas_area.width - 1)
|
||||
/ (self.x_bounds[1] - self.x_bounds[0])) as u16;
|
||||
buf.set_string(
|
||||
dx + canvas_area.left(),
|
||||
dy + canvas_area.top(),
|
||||
label.text,
|
||||
&style.fg(label.color),
|
||||
);
|
||||
}
|
||||
// Finally draw the labels
|
||||
let left = self.x_bounds[0];
|
||||
let right = self.x_bounds[1];
|
||||
let top = self.y_bounds[1];
|
||||
let bottom = self.y_bounds[0];
|
||||
let width = (self.x_bounds[1] - self.x_bounds[0]).abs();
|
||||
let height = (self.y_bounds[1] - self.y_bounds[0]).abs();
|
||||
let resolution = {
|
||||
let width = f64::from(canvas_area.width - 1);
|
||||
let height = f64::from(canvas_area.height - 1);
|
||||
(width, height)
|
||||
};
|
||||
for label in ctx
|
||||
.labels
|
||||
.iter()
|
||||
.filter(|l| l.x >= left && l.x <= right && l.y <= top && l.y >= bottom)
|
||||
{
|
||||
let x = ((label.x - left) * resolution.0 / width) as u16 + canvas_area.left();
|
||||
let y = ((top - label.y) * resolution.1 / height) as u16 + canvas_area.top();
|
||||
buf.set_line(x, y, &label.line, canvas_area.right() - x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use indoc::indoc;
|
||||
|
||||
use super::*;
|
||||
use crate::{buffer::Cell, symbols::Marker};
|
||||
|
||||
// helper to test the canvas checks that drawing a vertical and horizontal line
|
||||
// results in the expected output
|
||||
fn test_marker(marker: Marker, expected: &str) {
|
||||
let area = Rect::new(0, 0, 5, 5);
|
||||
let mut cell = Cell::default();
|
||||
cell.set_char('x');
|
||||
let mut buf = Buffer::filled(area, &cell);
|
||||
let horizontal_line = Line {
|
||||
x1: 0.0,
|
||||
y1: 0.0,
|
||||
x2: 10.0,
|
||||
y2: 0.0,
|
||||
color: Color::Reset,
|
||||
};
|
||||
let vertical_line = Line {
|
||||
x1: 0.0,
|
||||
y1: 0.0,
|
||||
x2: 0.0,
|
||||
y2: 10.0,
|
||||
color: Color::Reset,
|
||||
};
|
||||
Canvas::default()
|
||||
.marker(marker)
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&vertical_line);
|
||||
ctx.draw(&horizontal_line);
|
||||
})
|
||||
.x_bounds([0.0, 10.0])
|
||||
.y_bounds([0.0, 10.0])
|
||||
.render(area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(expected.lines().collect()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bar_marker() {
|
||||
test_marker(
|
||||
Marker::Bar,
|
||||
indoc!(
|
||||
"
|
||||
▄xxxx
|
||||
▄xxxx
|
||||
▄xxxx
|
||||
▄xxxx
|
||||
▄▄▄▄▄"
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_block_marker() {
|
||||
test_marker(
|
||||
Marker::Block,
|
||||
indoc!(
|
||||
"
|
||||
█xxxx
|
||||
█xxxx
|
||||
█xxxx
|
||||
█xxxx
|
||||
█████"
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_braille_marker() {
|
||||
test_marker(
|
||||
Marker::Braille,
|
||||
indoc!(
|
||||
"
|
||||
⡇xxxx
|
||||
⡇xxxx
|
||||
⡇xxxx
|
||||
⡇xxxx
|
||||
⣇⣀⣀⣀⣀"
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dot_marker() {
|
||||
test_marker(
|
||||
Marker::Dot,
|
||||
indoc!(
|
||||
"
|
||||
•xxxx
|
||||
•xxxx
|
||||
•xxxx
|
||||
•xxxx
|
||||
•••••"
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
use std::slice;
|
||||
|
||||
use super::Shape;
|
||||
use style::Color;
|
||||
use crate::{
|
||||
style::Color,
|
||||
widgets::canvas::{Painter, Shape},
|
||||
};
|
||||
|
||||
/// A shape to draw a group of points with the given color
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Points<'a> {
|
||||
pub coords: &'a [(f64, f64)],
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
impl<'a> Shape<'a> for Points<'a> {
|
||||
fn color(&self) -> Color {
|
||||
self.color
|
||||
}
|
||||
fn points(&'a self) -> Box<Iterator<Item = (f64, f64)> + 'a> {
|
||||
Box::new(self.into_iter())
|
||||
impl<'a> Shape for Points<'a> {
|
||||
fn draw(&self, painter: &mut Painter) {
|
||||
for (x, y) in self.coords {
|
||||
if let Some((x, y)) = painter.get_point(*x, *y) {
|
||||
painter.paint(x, y, self.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,33 +28,3 @@ impl<'a> Default for Points<'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a Points<'a> {
|
||||
type Item = (f64, f64);
|
||||
type IntoIter = PointsIterator<'a>;
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
PointsIterator {
|
||||
iter: self.coords.iter(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PointsIterator<'a> {
|
||||
iter: slice::Iter<'a, (f64, f64)>,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a [(f64, f64)]> for PointsIterator<'a> {
|
||||
fn from(data: &'a [(f64, f64)]) -> PointsIterator<'a> {
|
||||
PointsIterator { iter: data.iter() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for PointsIterator<'a> {
|
||||
type Item = (f64, f64);
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self.iter.next() {
|
||||
Some(p) => Some(*p),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
52
src/widgets/canvas/rectangle.rs
Normal file
52
src/widgets/canvas/rectangle.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
use crate::{
|
||||
style::Color,
|
||||
widgets::canvas::{Line, Painter, Shape},
|
||||
};
|
||||
|
||||
/// Shape to draw a rectangle from a `Rect` with the given color
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Rectangle {
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
pub width: f64,
|
||||
pub height: f64,
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
impl Shape for Rectangle {
|
||||
fn draw(&self, painter: &mut Painter) {
|
||||
let lines: [Line; 4] = [
|
||||
Line {
|
||||
x1: self.x,
|
||||
y1: self.y,
|
||||
x2: self.x,
|
||||
y2: self.y + self.height,
|
||||
color: self.color,
|
||||
},
|
||||
Line {
|
||||
x1: self.x,
|
||||
y1: self.y + self.height,
|
||||
x2: self.x + self.width,
|
||||
y2: self.y + self.height,
|
||||
color: self.color,
|
||||
},
|
||||
Line {
|
||||
x1: self.x + self.width,
|
||||
y1: self.y,
|
||||
x2: self.x + self.width,
|
||||
y2: self.y + self.height,
|
||||
color: self.color,
|
||||
},
|
||||
Line {
|
||||
x1: self.x,
|
||||
y1: self.y,
|
||||
x2: self.x + self.width,
|
||||
y2: self.y,
|
||||
color: self.color,
|
||||
},
|
||||
];
|
||||
for line in &lines {
|
||||
line.draw(painter);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,100 +1,112 @@
|
||||
use std::cmp::max;
|
||||
use std::{borrow::Cow, cmp::max};
|
||||
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use widgets::{Block, Borders, Widget};
|
||||
use widgets::canvas::{Canvas, Points};
|
||||
use buffer::Buffer;
|
||||
use layout::Rect;
|
||||
use style::Style;
|
||||
use symbols;
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Constraint, Rect},
|
||||
style::{Color, Style},
|
||||
symbols,
|
||||
text::{Line as TextLine, Span},
|
||||
widgets::{
|
||||
canvas::{Canvas, Line, Points},
|
||||
Block, Borders, Widget,
|
||||
},
|
||||
};
|
||||
|
||||
/// An X or Y axis for the chart widget
|
||||
pub struct Axis<'a, L>
|
||||
where
|
||||
L: AsRef<str> + 'a,
|
||||
{
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Axis<'a> {
|
||||
/// Title displayed next to axis end
|
||||
title: Option<&'a str>,
|
||||
/// Style of the title
|
||||
title_style: Style,
|
||||
title: Option<TextLine<'a>>,
|
||||
/// Bounds for the axis (all data points outside these limits will not be represented)
|
||||
bounds: [f64; 2],
|
||||
/// A list of labels to put to the left or below the axis
|
||||
labels: Option<&'a [L]>,
|
||||
/// The labels' style
|
||||
labels_style: Style,
|
||||
labels: Option<Vec<Span<'a>>>,
|
||||
/// The style used to draw the axis itself
|
||||
style: Style,
|
||||
/// The alignment of the labels of the Axis
|
||||
labels_alignment: Alignment,
|
||||
}
|
||||
|
||||
impl<'a, L> Default for Axis<'a, L>
|
||||
where
|
||||
L: AsRef<str>,
|
||||
{
|
||||
fn default() -> Axis<'a, L> {
|
||||
impl<'a> Default for Axis<'a> {
|
||||
fn default() -> Axis<'a> {
|
||||
Axis {
|
||||
title: None,
|
||||
title_style: Default::default(),
|
||||
bounds: [0.0, 0.0],
|
||||
labels: None,
|
||||
labels_style: Default::default(),
|
||||
style: Default::default(),
|
||||
style: Style::default(),
|
||||
labels_alignment: Alignment::Left,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, L> Axis<'a, L>
|
||||
where
|
||||
L: AsRef<str>,
|
||||
{
|
||||
pub fn title(mut self, title: &'a str) -> Axis<'a, L> {
|
||||
self.title = Some(title);
|
||||
impl<'a> Axis<'a> {
|
||||
pub fn title<T>(mut self, title: T) -> Axis<'a>
|
||||
where
|
||||
T: Into<TextLine<'a>>,
|
||||
{
|
||||
self.title = Some(title.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn title_style(mut self, style: Style) -> Axis<'a, L> {
|
||||
self.title_style = style;
|
||||
#[deprecated(
|
||||
since = "0.10.0",
|
||||
note = "You should use styling capabilities of `text::Line` given as argument of the `title` method to apply styling to the title."
|
||||
)]
|
||||
pub fn title_style(mut self, style: Style) -> Axis<'a> {
|
||||
if let Some(t) = self.title {
|
||||
let title = String::from(t);
|
||||
self.title = Some(TextLine::from(Span::styled(title, style)));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn bounds(mut self, bounds: [f64; 2]) -> Axis<'a, L> {
|
||||
pub fn bounds(mut self, bounds: [f64; 2]) -> Axis<'a> {
|
||||
self.bounds = bounds;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn labels(mut self, labels: &'a [L]) -> Axis<'a, L> {
|
||||
pub fn labels(mut self, labels: Vec<Span<'a>>) -> Axis<'a> {
|
||||
self.labels = Some(labels);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn labels_style(mut self, style: Style) -> Axis<'a, L> {
|
||||
self.labels_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: Style) -> Axis<'a, L> {
|
||||
pub fn style(mut self, style: Style) -> Axis<'a> {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Defines the alignment of the labels of the axis.
|
||||
/// The alignment behaves differently based on the axis:
|
||||
/// - Y-Axis: The labels are aligned within the area on the left of the axis
|
||||
/// - X-Axis: The first X-axis label is aligned relative to the Y-axis
|
||||
pub fn labels_alignment(mut self, alignment: Alignment) -> Axis<'a> {
|
||||
self.labels_alignment = alignment;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Marker to use when plotting data points
|
||||
pub enum Marker {
|
||||
/// One point per cell
|
||||
Dot,
|
||||
/// Up to 8 points per cell
|
||||
Braille,
|
||||
/// Used to determine which style of graphing to use
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum GraphType {
|
||||
/// Draw each point
|
||||
Scatter,
|
||||
/// Draw each point and lines between each point using the same marker
|
||||
Line,
|
||||
}
|
||||
|
||||
/// A group of data points
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Dataset<'a> {
|
||||
/// Name of the dataset (used in the legend if shown)
|
||||
name: &'a str,
|
||||
name: Cow<'a, str>,
|
||||
/// A reference to the actual data
|
||||
data: &'a [(f64, f64)],
|
||||
/// Symbol used for each points of this dataset
|
||||
marker: Marker,
|
||||
marker: symbols::Marker,
|
||||
/// Determines graph type used for drawing points
|
||||
graph_type: GraphType,
|
||||
/// Style used to plot this dataset
|
||||
style: Style,
|
||||
}
|
||||
@@ -102,17 +114,21 @@ pub struct Dataset<'a> {
|
||||
impl<'a> Default for Dataset<'a> {
|
||||
fn default() -> Dataset<'a> {
|
||||
Dataset {
|
||||
name: "",
|
||||
name: Cow::from(""),
|
||||
data: &[],
|
||||
marker: Marker::Dot,
|
||||
marker: symbols::Marker::Dot,
|
||||
graph_type: GraphType::Scatter,
|
||||
style: Style::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Dataset<'a> {
|
||||
pub fn name(mut self, name: &'a str) -> Dataset<'a> {
|
||||
self.name = name;
|
||||
pub fn name<S>(mut self, name: S) -> Dataset<'a>
|
||||
where
|
||||
S: Into<Cow<'a, str>>,
|
||||
{
|
||||
self.name = name.into();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -121,11 +137,16 @@ impl<'a> Dataset<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn marker(mut self, marker: Marker) -> Dataset<'a> {
|
||||
pub fn marker(mut self, marker: symbols::Marker) -> Dataset<'a> {
|
||||
self.marker = marker;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn graph_type(mut self, graph_type: GraphType) -> Dataset<'a> {
|
||||
self.graph_type = graph_type;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: Style) -> Dataset<'a> {
|
||||
self.style = style;
|
||||
self
|
||||
@@ -134,133 +155,134 @@ impl<'a> Dataset<'a> {
|
||||
|
||||
/// A container that holds all the infos about where to display each elements of the chart (axis,
|
||||
/// labels, legend, ...).
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
struct ChartLayout {
|
||||
/// Location of the title of the x axis
|
||||
title_x: Option<(u16, u16)>,
|
||||
/// Location of the title of the y axis
|
||||
title_y: Option<(u16, u16)>,
|
||||
/// Location of the first label of the x axis
|
||||
label_x: Option<u16>,
|
||||
/// Location of the first label of the y axis
|
||||
label_y: Option<u16>,
|
||||
/// Y coordinate of the horizontal axis
|
||||
axis_x: Option<u16>,
|
||||
/// X coordinate of the vertical axis
|
||||
axis_y: Option<u16>,
|
||||
/// Area of the legend
|
||||
legend_area: Option<Rect>,
|
||||
/// Area of the graph
|
||||
graph_area: Rect,
|
||||
}
|
||||
|
||||
impl Default for ChartLayout {
|
||||
fn default() -> ChartLayout {
|
||||
ChartLayout {
|
||||
title_x: None,
|
||||
title_y: None,
|
||||
label_x: None,
|
||||
label_y: None,
|
||||
axis_x: None,
|
||||
axis_y: None,
|
||||
legend_area: None,
|
||||
graph_area: Rect::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget to plot one or more dataset in a cartesian coordinate system
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # extern crate tui;
|
||||
/// # use tui::widgets::{Block, Borders, Chart, Axis, Dataset, Marker};
|
||||
/// # use tui::style::{Style, Color};
|
||||
/// # fn main() {
|
||||
/// Chart::default()
|
||||
/// # use ratatui::symbols;
|
||||
/// # use ratatui::widgets::{Block, Borders, Chart, Axis, Dataset, GraphType};
|
||||
/// # use ratatui::style::{Style, Color};
|
||||
/// # use ratatui::text::Span;
|
||||
/// let datasets = vec![
|
||||
/// Dataset::default()
|
||||
/// .name("data1")
|
||||
/// .marker(symbols::Marker::Dot)
|
||||
/// .graph_type(GraphType::Scatter)
|
||||
/// .style(Style::default().fg(Color::Cyan))
|
||||
/// .data(&[(0.0, 5.0), (1.0, 6.0), (1.5, 6.434)]),
|
||||
/// Dataset::default()
|
||||
/// .name("data2")
|
||||
/// .marker(symbols::Marker::Braille)
|
||||
/// .graph_type(GraphType::Line)
|
||||
/// .style(Style::default().fg(Color::Magenta))
|
||||
/// .data(&[(4.0, 5.0), (5.0, 8.0), (7.66, 13.5)]),
|
||||
/// ];
|
||||
/// Chart::new(datasets)
|
||||
/// .block(Block::default().title("Chart"))
|
||||
/// .x_axis(Axis::default()
|
||||
/// .title("X Axis")
|
||||
/// .title_style(Style::default().fg(Color::Red))
|
||||
/// .style(Style::default().fg(Color::Gray))
|
||||
/// .title(Span::styled("X Axis", Style::default().fg(Color::Red)))
|
||||
/// .style(Style::default().fg(Color::White))
|
||||
/// .bounds([0.0, 10.0])
|
||||
/// .labels(&["0.0", "5.0", "10.0"]))
|
||||
/// .labels(["0.0", "5.0", "10.0"].iter().cloned().map(Span::from).collect()))
|
||||
/// .y_axis(Axis::default()
|
||||
/// .title("Y Axis")
|
||||
/// .title_style(Style::default().fg(Color::Red))
|
||||
/// .style(Style::default().fg(Color::Gray))
|
||||
/// .title(Span::styled("Y Axis", Style::default().fg(Color::Red)))
|
||||
/// .style(Style::default().fg(Color::White))
|
||||
/// .bounds([0.0, 10.0])
|
||||
/// .labels(&["0.0", "5.0", "10.0"]))
|
||||
/// .datasets(&[Dataset::default()
|
||||
/// .name("data1")
|
||||
/// .marker(Marker::Dot)
|
||||
/// .style(Style::default().fg(Color::Cyan))
|
||||
/// .data(&[(0.0, 5.0), (1.0, 6.0), (1.5, 6.434)]),
|
||||
/// Dataset::default()
|
||||
/// .name("data2")
|
||||
/// .marker(Marker::Braille)
|
||||
/// .style(Style::default().fg(Color::Magenta))
|
||||
/// .data(&[(4.0, 5.0), (5.0, 8.0), (7.66, 13.5)])]);
|
||||
/// # }
|
||||
pub struct Chart<'a, LX, LY>
|
||||
where
|
||||
LX: AsRef<str> + 'a,
|
||||
LY: AsRef<str> + 'a,
|
||||
{
|
||||
/// .labels(["0.0", "5.0", "10.0"].iter().cloned().map(Span::from).collect()));
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Chart<'a> {
|
||||
/// A block to display around the widget eventually
|
||||
block: Option<Block<'a>>,
|
||||
/// The horizontal axis
|
||||
x_axis: Axis<'a, LX>,
|
||||
x_axis: Axis<'a>,
|
||||
/// The vertical axis
|
||||
y_axis: Axis<'a, LY>,
|
||||
y_axis: Axis<'a>,
|
||||
/// A reference to the datasets
|
||||
datasets: &'a [Dataset<'a>],
|
||||
datasets: Vec<Dataset<'a>>,
|
||||
/// The widget base style
|
||||
style: Style,
|
||||
/// Constraints used to determine whether the legend should be shown or not
|
||||
hidden_legend_constraints: (Constraint, Constraint),
|
||||
}
|
||||
|
||||
impl<'a, LX, LY> Default for Chart<'a, LX, LY>
|
||||
where
|
||||
LX: AsRef<str>,
|
||||
LY: AsRef<str>,
|
||||
{
|
||||
fn default() -> Chart<'a, LX, LY> {
|
||||
impl<'a> Chart<'a> {
|
||||
pub fn new(datasets: Vec<Dataset<'a>>) -> Chart<'a> {
|
||||
Chart {
|
||||
block: None,
|
||||
x_axis: Axis::default(),
|
||||
y_axis: Axis::default(),
|
||||
style: Default::default(),
|
||||
datasets: &[],
|
||||
style: Style::default(),
|
||||
datasets,
|
||||
hidden_legend_constraints: (Constraint::Ratio(1, 4), Constraint::Ratio(1, 4)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, LX, LY> Chart<'a, LX, LY>
|
||||
where
|
||||
LX: AsRef<str>,
|
||||
LY: AsRef<str>,
|
||||
{
|
||||
pub fn block(&'a mut self, block: Block<'a>) -> &mut Chart<'a, LX, LY> {
|
||||
pub fn block(mut self, block: Block<'a>) -> Chart<'a> {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(&mut self, style: Style) -> &mut Chart<'a, LX, LY> {
|
||||
pub fn style(mut self, style: Style) -> Chart<'a> {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn x_axis(&mut self, axis: Axis<'a, LX>) -> &mut Chart<'a, LX, LY> {
|
||||
pub fn x_axis(mut self, axis: Axis<'a>) -> Chart<'a> {
|
||||
self.x_axis = axis;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn y_axis(&mut self, axis: Axis<'a, LY>) -> &mut Chart<'a, LX, LY> {
|
||||
pub fn y_axis(mut self, axis: Axis<'a>) -> Chart<'a> {
|
||||
self.y_axis = axis;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn datasets(&mut self, datasets: &'a [Dataset<'a>]) -> &mut Chart<'a, LX, LY> {
|
||||
self.datasets = datasets;
|
||||
/// Set the constraints used to determine whether the legend should be shown or not.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::widgets::Chart;
|
||||
/// # use ratatui::layout::Constraint;
|
||||
/// let constraints = (
|
||||
/// Constraint::Ratio(1, 3),
|
||||
/// Constraint::Ratio(1, 4)
|
||||
/// );
|
||||
/// // Hide the legend when either its width is greater than 33% of the total widget width
|
||||
/// // or if its height is greater than 25% of the total widget height.
|
||||
/// let _chart: Chart = Chart::new(vec![])
|
||||
/// .hidden_legend_constraints(constraints);
|
||||
/// ```
|
||||
pub fn hidden_legend_constraints(mut self, constraints: (Constraint, Constraint)) -> Chart<'a> {
|
||||
self.hidden_legend_constraints = constraints;
|
||||
self
|
||||
}
|
||||
|
||||
/// Compute the internal layout of the chart given the area. If the area is too small some
|
||||
/// elements may be automatically hidden
|
||||
fn layout(&self, area: &Rect) -> ChartLayout {
|
||||
fn layout(&self, area: Rect) -> ChartLayout {
|
||||
let mut layout = ChartLayout::default();
|
||||
if area.height == 0 || area.width == 0 {
|
||||
return layout;
|
||||
@@ -273,21 +295,8 @@ where
|
||||
y -= 1;
|
||||
}
|
||||
|
||||
if let Some(y_labels) = self.y_axis.labels {
|
||||
let mut max_width = y_labels
|
||||
.iter()
|
||||
.fold(0, |acc, l| max(l.as_ref().width(), acc))
|
||||
as u16;
|
||||
if let Some(x_labels) = self.x_axis.labels {
|
||||
if x_labels.len() > 0 {
|
||||
max_width = max(max_width, x_labels[0].as_ref().width() as u16);
|
||||
}
|
||||
}
|
||||
if x + max_width < area.right() {
|
||||
layout.label_y = Some(x);
|
||||
x += max_width;
|
||||
}
|
||||
}
|
||||
layout.label_y = self.y_axis.labels.as_ref().and(Some(x));
|
||||
x += self.max_width_of_labels_left_of_y_axis(area, self.y_axis.labels.is_some());
|
||||
|
||||
if self.x_axis.labels.is_some() && y > area.top() {
|
||||
layout.axis_x = Some(y);
|
||||
@@ -303,25 +312,34 @@ where
|
||||
layout.graph_area = Rect::new(x, area.top(), area.right() - x, y - area.top() + 1);
|
||||
}
|
||||
|
||||
if let Some(title) = self.x_axis.title {
|
||||
if let Some(ref title) = self.x_axis.title {
|
||||
let w = title.width() as u16;
|
||||
if w < layout.graph_area.width && layout.graph_area.height > 2 {
|
||||
layout.title_x = Some((x + layout.graph_area.width - w, y));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(title) = self.y_axis.title {
|
||||
if let Some(ref title) = self.y_axis.title {
|
||||
let w = title.width() as u16;
|
||||
if w + 1 < layout.graph_area.width && layout.graph_area.height > 2 {
|
||||
layout.title_y = Some((x + 1, area.top()));
|
||||
layout.title_y = Some((x, area.top()));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(inner_width) = self.datasets.iter().map(|d| d.name.width() as u16).max() {
|
||||
let legend_width = inner_width + 2;
|
||||
let legend_height = self.datasets.len() as u16 + 2;
|
||||
if legend_width < layout.graph_area.width / 3
|
||||
&& legend_height < layout.graph_area.height / 3
|
||||
let max_legend_width = self
|
||||
.hidden_legend_constraints
|
||||
.0
|
||||
.apply(layout.graph_area.width);
|
||||
let max_legend_height = self
|
||||
.hidden_legend_constraints
|
||||
.1
|
||||
.apply(layout.graph_area.height);
|
||||
if inner_width > 0
|
||||
&& legend_width < max_legend_width
|
||||
&& legend_height < max_legend_height
|
||||
{
|
||||
layout.legend_area = Some(Rect::new(
|
||||
layout.graph_area.right() - legend_width,
|
||||
@@ -333,72 +351,169 @@ where
|
||||
}
|
||||
layout
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, LX, LY> Widget for Chart<'a, LX, LY>
|
||||
where
|
||||
LX: AsRef<str>,
|
||||
LY: AsRef<str>,
|
||||
{
|
||||
fn draw(&mut self, area: &Rect, buf: &mut Buffer) {
|
||||
let chart_area = match self.block {
|
||||
Some(ref mut b) => {
|
||||
b.draw(area, buf);
|
||||
b.inner(area)
|
||||
}
|
||||
None => *area,
|
||||
fn max_width_of_labels_left_of_y_axis(&self, area: Rect, has_y_axis: bool) -> u16 {
|
||||
let mut max_width = self
|
||||
.y_axis
|
||||
.labels
|
||||
.as_ref()
|
||||
.map(|l| l.iter().map(Span::width).max().unwrap_or_default() as u16)
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Some(first_x_label) = self.x_axis.labels.as_ref().and_then(|labels| labels.get(0)) {
|
||||
let first_label_width = first_x_label.content.width() as u16;
|
||||
let width_left_of_y_axis = match self.x_axis.labels_alignment {
|
||||
Alignment::Left => {
|
||||
// The last character of the label should be below the Y-Axis when it exists,
|
||||
// not on its left
|
||||
let y_axis_offset = u16::from(has_y_axis);
|
||||
first_label_width.saturating_sub(y_axis_offset)
|
||||
}
|
||||
Alignment::Center => first_label_width / 2,
|
||||
Alignment::Right => 0,
|
||||
};
|
||||
max_width = max(max_width, width_left_of_y_axis);
|
||||
}
|
||||
// labels of y axis and first label of x axis can take at most 1/3rd of the total width
|
||||
max_width.min(area.width / 3)
|
||||
}
|
||||
|
||||
fn render_x_labels(
|
||||
&mut self,
|
||||
buf: &mut Buffer,
|
||||
layout: &ChartLayout,
|
||||
chart_area: Rect,
|
||||
graph_area: Rect,
|
||||
) {
|
||||
let Some(y) = layout.label_x else { return };
|
||||
let labels = self.x_axis.labels.as_ref().unwrap();
|
||||
let labels_len = labels.len() as u16;
|
||||
if labels_len < 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
let width_between_ticks = graph_area.width / labels_len;
|
||||
|
||||
let label_area = self.first_x_label_area(
|
||||
y,
|
||||
labels.first().unwrap().width() as u16,
|
||||
width_between_ticks,
|
||||
chart_area,
|
||||
graph_area,
|
||||
);
|
||||
|
||||
let label_alignment = match self.x_axis.labels_alignment {
|
||||
Alignment::Left => Alignment::Right,
|
||||
Alignment::Center => Alignment::Center,
|
||||
Alignment::Right => Alignment::Left,
|
||||
};
|
||||
|
||||
let layout = self.layout(&chart_area);
|
||||
Self::render_label(buf, labels.first().unwrap(), label_area, label_alignment);
|
||||
|
||||
for (i, label) in labels[1..labels.len() - 1].iter().enumerate() {
|
||||
// We add 1 to x (and width-1 below) to leave at least one space before each
|
||||
// intermediate labels
|
||||
let x = graph_area.left() + (i + 1) as u16 * width_between_ticks + 1;
|
||||
let label_area = Rect::new(x, y, width_between_ticks.saturating_sub(1), 1);
|
||||
|
||||
Self::render_label(buf, label, label_area, Alignment::Center);
|
||||
}
|
||||
|
||||
let x = graph_area.right() - width_between_ticks;
|
||||
let label_area = Rect::new(x, y, width_between_ticks, 1);
|
||||
// The last label should be aligned Right to be at the edge of the graph area
|
||||
Self::render_label(buf, labels.last().unwrap(), label_area, Alignment::Right);
|
||||
}
|
||||
|
||||
fn first_x_label_area(
|
||||
&self,
|
||||
y: u16,
|
||||
label_width: u16,
|
||||
max_width_after_y_axis: u16,
|
||||
chart_area: Rect,
|
||||
graph_area: Rect,
|
||||
) -> Rect {
|
||||
let (min_x, max_x) = match self.x_axis.labels_alignment {
|
||||
Alignment::Left => (chart_area.left(), graph_area.left()),
|
||||
Alignment::Center => (
|
||||
chart_area.left(),
|
||||
graph_area.left() + max_width_after_y_axis.min(label_width),
|
||||
),
|
||||
Alignment::Right => (
|
||||
graph_area.left().saturating_sub(1),
|
||||
graph_area.left() + max_width_after_y_axis,
|
||||
),
|
||||
};
|
||||
|
||||
Rect::new(min_x, y, max_x - min_x, 1)
|
||||
}
|
||||
|
||||
fn render_label(buf: &mut Buffer, label: &Span, label_area: Rect, alignment: Alignment) {
|
||||
let label_width = label.width() as u16;
|
||||
let bounded_label_width = label_area.width.min(label_width);
|
||||
|
||||
let x = match alignment {
|
||||
Alignment::Left => label_area.left(),
|
||||
Alignment::Center => label_area.left() + label_area.width / 2 - bounded_label_width / 2,
|
||||
Alignment::Right => label_area.right() - bounded_label_width,
|
||||
};
|
||||
|
||||
buf.set_span(x, label_area.top(), label, bounded_label_width);
|
||||
}
|
||||
|
||||
fn render_y_labels(
|
||||
&mut self,
|
||||
buf: &mut Buffer,
|
||||
layout: &ChartLayout,
|
||||
chart_area: Rect,
|
||||
graph_area: Rect,
|
||||
) {
|
||||
let Some(x) = layout.label_y else { return };
|
||||
let labels = self.y_axis.labels.as_ref().unwrap();
|
||||
let labels_len = labels.len() as u16;
|
||||
for (i, label) in labels.iter().enumerate() {
|
||||
let dy = i as u16 * (graph_area.height - 1) / (labels_len - 1);
|
||||
if dy < graph_area.bottom() {
|
||||
let label_area = Rect::new(
|
||||
x,
|
||||
graph_area.bottom().saturating_sub(1) - dy,
|
||||
(graph_area.left() - chart_area.left()).saturating_sub(1),
|
||||
1,
|
||||
);
|
||||
Self::render_label(buf, label, label_area, self.y_axis.labels_alignment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Chart<'a> {
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer) {
|
||||
if area.area() == 0 {
|
||||
return;
|
||||
}
|
||||
buf.set_style(area, self.style);
|
||||
// Sample the style of the entire widget. This sample will be used to reset the style of
|
||||
// the cells that are part of the components put on top of the grah area (i.e legend and
|
||||
// axis names).
|
||||
let original_style = buf.get(area.left(), area.top()).style();
|
||||
|
||||
let chart_area = match self.block.take() {
|
||||
Some(b) => {
|
||||
let inner_area = b.inner(area);
|
||||
b.render(area, buf);
|
||||
inner_area
|
||||
}
|
||||
None => area,
|
||||
};
|
||||
|
||||
let layout = self.layout(chart_area);
|
||||
let graph_area = layout.graph_area;
|
||||
if graph_area.width < 1 || graph_area.height < 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
self.background(&chart_area, buf, self.style.bg);
|
||||
|
||||
if let Some((x, y)) = layout.title_x {
|
||||
let title = self.x_axis.title.unwrap();
|
||||
buf.set_string(x, y, title, &self.x_axis.style);
|
||||
}
|
||||
|
||||
if let Some((x, y)) = layout.title_y {
|
||||
let title = self.y_axis.title.unwrap();
|
||||
buf.set_string(x, y, title, &self.y_axis.style);
|
||||
}
|
||||
|
||||
if let Some(y) = layout.label_x {
|
||||
let labels = self.x_axis.labels.unwrap();
|
||||
let total_width = labels.iter().fold(0, |acc, l| l.as_ref().width() + acc) as u16;
|
||||
let labels_len = labels.len() as u16;
|
||||
if total_width < graph_area.width && labels_len > 1 {
|
||||
for (i, label) in labels.iter().enumerate() {
|
||||
buf.set_string(
|
||||
graph_area.left() + i as u16 * (graph_area.width - 1) / (labels_len - 1)
|
||||
- label.as_ref().width() as u16,
|
||||
y,
|
||||
label.as_ref(),
|
||||
&self.x_axis.labels_style,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(x) = layout.label_y {
|
||||
let labels = self.y_axis.labels.unwrap();
|
||||
let labels_len = labels.len() as u16;
|
||||
for (i, label) in labels.iter().enumerate() {
|
||||
let dy = i as u16 * (graph_area.height - 1) / (labels_len - 1);
|
||||
if dy < graph_area.bottom() {
|
||||
buf.set_string(
|
||||
x,
|
||||
graph_area.bottom() - 1 - dy,
|
||||
label.as_ref(),
|
||||
&self.y_axis.labels_style,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.render_x_labels(buf, &layout, chart_area, graph_area);
|
||||
self.render_y_labels(buf, &layout, chart_area, graph_area);
|
||||
|
||||
if let Some(y) = layout.axis_x {
|
||||
for x in graph_area.left()..graph_area.right() {
|
||||
@@ -424,53 +539,117 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
for dataset in self.datasets {
|
||||
match dataset.marker {
|
||||
Marker::Dot => for &(x, y) in dataset.data.iter().filter(|&&(x, y)| {
|
||||
!(x < self.x_axis.bounds[0] || x > self.x_axis.bounds[1]
|
||||
|| y < self.y_axis.bounds[0]
|
||||
|| y > self.y_axis.bounds[1])
|
||||
}) {
|
||||
let dy = ((self.y_axis.bounds[1] - y) * f64::from(graph_area.height - 1)
|
||||
/ (self.y_axis.bounds[1] - self.y_axis.bounds[0]))
|
||||
as u16;
|
||||
let dx = ((x - self.x_axis.bounds[0]) * f64::from(graph_area.width - 1)
|
||||
/ (self.x_axis.bounds[1] - self.x_axis.bounds[0]))
|
||||
as u16;
|
||||
|
||||
buf.get_mut(graph_area.left() + dx, graph_area.top() + dy)
|
||||
.set_symbol(symbols::DOT)
|
||||
.set_fg(dataset.style.fg)
|
||||
.set_bg(dataset.style.bg);
|
||||
},
|
||||
Marker::Braille => {
|
||||
Canvas::default()
|
||||
.background_color(self.style.bg)
|
||||
.x_bounds(self.x_axis.bounds)
|
||||
.y_bounds(self.y_axis.bounds)
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&Points {
|
||||
coords: dataset.data,
|
||||
color: dataset.style.fg,
|
||||
for dataset in &self.datasets {
|
||||
Canvas::default()
|
||||
.background_color(self.style.bg.unwrap_or(Color::Reset))
|
||||
.x_bounds(self.x_axis.bounds)
|
||||
.y_bounds(self.y_axis.bounds)
|
||||
.marker(dataset.marker)
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&Points {
|
||||
coords: dataset.data,
|
||||
color: dataset.style.fg.unwrap_or(Color::Reset),
|
||||
});
|
||||
if let GraphType::Line = dataset.graph_type {
|
||||
for data in dataset.data.windows(2) {
|
||||
ctx.draw(&Line {
|
||||
x1: data[0].0,
|
||||
y1: data[0].1,
|
||||
x2: data[1].0,
|
||||
y2: data[1].1,
|
||||
color: dataset.style.fg.unwrap_or(Color::Reset),
|
||||
});
|
||||
})
|
||||
.draw(&graph_area, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.render(graph_area, buf);
|
||||
}
|
||||
|
||||
if let Some(legend_area) = layout.legend_area {
|
||||
buf.set_style(legend_area, original_style);
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.draw(&legend_area, buf);
|
||||
.render(legend_area, buf);
|
||||
for (i, dataset) in self.datasets.iter().enumerate() {
|
||||
buf.set_string(
|
||||
legend_area.x + 1,
|
||||
legend_area.y + 1 + i as u16,
|
||||
dataset.name,
|
||||
&dataset.style,
|
||||
&dataset.name,
|
||||
dataset.style,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((x, y)) = layout.title_x {
|
||||
let title = self.x_axis.title.unwrap();
|
||||
let width = graph_area.right().saturating_sub(x);
|
||||
buf.set_style(
|
||||
Rect {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height: 1,
|
||||
},
|
||||
original_style,
|
||||
);
|
||||
buf.set_line(x, y, &title, width);
|
||||
}
|
||||
|
||||
if let Some((x, y)) = layout.title_y {
|
||||
let title = self.y_axis.title.unwrap();
|
||||
let width = graph_area.right().saturating_sub(x);
|
||||
buf.set_style(
|
||||
Rect {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height: 1,
|
||||
},
|
||||
original_style,
|
||||
);
|
||||
buf.set_line(x, y, &title, width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
struct LegendTestCase {
|
||||
chart_area: Rect,
|
||||
hidden_legend_constraints: (Constraint, Constraint),
|
||||
legend_area: Option<Rect>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_hide_the_legend() {
|
||||
let data = [(0.0, 5.0), (1.0, 6.0), (3.0, 7.0)];
|
||||
let cases = [
|
||||
LegendTestCase {
|
||||
chart_area: Rect::new(0, 0, 100, 100),
|
||||
hidden_legend_constraints: (Constraint::Ratio(1, 4), Constraint::Ratio(1, 4)),
|
||||
legend_area: Some(Rect::new(88, 0, 12, 12)),
|
||||
},
|
||||
LegendTestCase {
|
||||
chart_area: Rect::new(0, 0, 100, 100),
|
||||
hidden_legend_constraints: (Constraint::Ratio(1, 10), Constraint::Ratio(1, 4)),
|
||||
legend_area: None,
|
||||
},
|
||||
];
|
||||
for case in &cases {
|
||||
let datasets = (0..10)
|
||||
.map(|i| {
|
||||
let name = format!("Dataset #{i}");
|
||||
Dataset::default().name(name).data(&data)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let chart = Chart::new(datasets)
|
||||
.x_axis(Axis::default().title("X axis"))
|
||||
.y_axis(Axis::default().title("Y axis"))
|
||||
.hidden_legend_constraints(case.hidden_legend_constraints);
|
||||
let layout = chart.layout(case.chart_area);
|
||||
assert_eq!(layout.legend_area, case.legend_area);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
37
src/widgets/clear.rs
Normal file
37
src/widgets/clear.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use crate::{buffer::Buffer, layout::Rect, widgets::Widget};
|
||||
|
||||
/// A widget to clear/reset a certain area to allow overdrawing (e.g. for popups).
|
||||
///
|
||||
/// This widget **cannot be used to clear the terminal on the first render** as `ratatui` assumes
|
||||
/// the render area is empty. Use [`crate::Terminal::clear`] instead.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::widgets::{Clear, Block, Borders};
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// # use ratatui::Frame;
|
||||
/// # use ratatui::backend::Backend;
|
||||
/// fn draw_on_clear<B: Backend>(f: &mut Frame<B>, area: Rect) {
|
||||
/// let block = Block::default().title("Block").borders(Borders::ALL);
|
||||
/// f.render_widget(Clear, area); // <- this will clear/reset the area first
|
||||
/// f.render_widget(block, area); // now render the block widget
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Popup Example
|
||||
///
|
||||
/// For a more complete example how to utilize `Clear` to realize popups see
|
||||
/// the example `examples/popup.rs`
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Clear;
|
||||
|
||||
impl Widget for Clear {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
for x in area.left()..area.right() {
|
||||
for y in area.top()..area.bottom() {
|
||||
buf.get_mut(x, y).reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,106 +1,320 @@
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use widgets::{Block, Widget};
|
||||
use buffer::Buffer;
|
||||
use style::{Color, Style};
|
||||
use layout::Rect;
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
symbols,
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
|
||||
/// A widget to display a task progress.
|
||||
///
|
||||
/// # Examples:
|
||||
///
|
||||
/// ```
|
||||
/// # extern crate tui;
|
||||
/// # use tui::widgets::{Widget, Gauge, Block, Borders};
|
||||
/// # use tui::style::{Style, Color, Modifier};
|
||||
/// # fn main() {
|
||||
/// # use ratatui::widgets::{Widget, Gauge, Block, Borders};
|
||||
/// # use ratatui::style::{Style, Color, Modifier};
|
||||
/// Gauge::default()
|
||||
/// .block(Block::default().borders(Borders::ALL).title("Progress"))
|
||||
/// .style(Style::default().fg(Color::White).bg(Color::Black).modifier(Modifier::Italic))
|
||||
/// .gauge_style(Style::default().fg(Color::White).bg(Color::Black).add_modifier(Modifier::ITALIC))
|
||||
/// .percent(20);
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Gauge<'a> {
|
||||
block: Option<Block<'a>>,
|
||||
percent: u16,
|
||||
label: Option<&'a str>,
|
||||
ratio: f64,
|
||||
label: Option<Span<'a>>,
|
||||
use_unicode: bool,
|
||||
style: Style,
|
||||
gauge_style: Style,
|
||||
}
|
||||
|
||||
impl<'a> Default for Gauge<'a> {
|
||||
fn default() -> Gauge<'a> {
|
||||
Gauge {
|
||||
block: None,
|
||||
percent: 0,
|
||||
ratio: 0.0,
|
||||
label: None,
|
||||
style: Default::default(),
|
||||
use_unicode: false,
|
||||
style: Style::default(),
|
||||
gauge_style: Style::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Gauge<'a> {
|
||||
pub fn block(&mut self, block: Block<'a>) -> &mut Gauge<'a> {
|
||||
pub fn block(mut self, block: Block<'a>) -> Gauge<'a> {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn percent(&mut self, percent: u16) -> &mut Gauge<'a> {
|
||||
self.percent = percent;
|
||||
pub fn percent(mut self, percent: u16) -> Gauge<'a> {
|
||||
assert!(
|
||||
percent <= 100,
|
||||
"Percentage should be between 0 and 100 inclusively."
|
||||
);
|
||||
self.ratio = f64::from(percent) / 100.0;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn label(&mut self, string: &'a str) -> &mut Gauge<'a> {
|
||||
self.label = Some(string);
|
||||
/// Sets ratio ([0.0, 1.0]) directly.
|
||||
pub fn ratio(mut self, ratio: f64) -> Gauge<'a> {
|
||||
assert!(
|
||||
(0.0..=1.0).contains(&ratio),
|
||||
"Ratio should be between 0 and 1 inclusively."
|
||||
);
|
||||
self.ratio = ratio;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(&mut self, style: Style) -> &mut Gauge<'a> {
|
||||
pub fn label<T>(mut self, label: T) -> Gauge<'a>
|
||||
where
|
||||
T: Into<Span<'a>>,
|
||||
{
|
||||
self.label = Some(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: Style) -> Gauge<'a> {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn gauge_style(mut self, style: Style) -> Gauge<'a> {
|
||||
self.gauge_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn use_unicode(mut self, unicode: bool) -> Gauge<'a> {
|
||||
self.use_unicode = unicode;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Gauge<'a> {
|
||||
fn draw(&mut self, area: &Rect, buf: &mut Buffer) {
|
||||
let gauge_area = match self.block {
|
||||
Some(ref mut b) => {
|
||||
b.draw(area, buf);
|
||||
b.inner(area)
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_style(area, self.style);
|
||||
let gauge_area = match self.block.take() {
|
||||
Some(b) => {
|
||||
let inner_area = b.inner(area);
|
||||
b.render(area, buf);
|
||||
inner_area
|
||||
}
|
||||
None => *area,
|
||||
None => area,
|
||||
};
|
||||
buf.set_style(gauge_area, self.gauge_style);
|
||||
if gauge_area.height < 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.style.bg != Color::Reset {
|
||||
self.background(&gauge_area, buf, self.style.bg);
|
||||
}
|
||||
// compute label value and its position
|
||||
// label is put at the center of the gauge_area
|
||||
let label = {
|
||||
let pct = f64::round(self.ratio * 100.0);
|
||||
self.label.unwrap_or_else(|| Span::from(format!("{pct}%")))
|
||||
};
|
||||
let clamped_label_width = gauge_area.width.min(label.width() as u16);
|
||||
let label_col = gauge_area.left() + (gauge_area.width - clamped_label_width) / 2;
|
||||
let label_row = gauge_area.top() + gauge_area.height / 2;
|
||||
|
||||
let center = gauge_area.height / 2 + gauge_area.top();
|
||||
let width = (gauge_area.width * self.percent) / 100;
|
||||
let end = gauge_area.left() + width;
|
||||
// the gauge will be filled proportionally to the ratio
|
||||
let filled_width = f64::from(gauge_area.width) * self.ratio;
|
||||
let end = if self.use_unicode {
|
||||
gauge_area.left() + filled_width.floor() as u16
|
||||
} else {
|
||||
gauge_area.left() + filled_width.round() as u16
|
||||
};
|
||||
for y in gauge_area.top()..gauge_area.bottom() {
|
||||
// Gauge
|
||||
// render the filled area (left to end)
|
||||
for x in gauge_area.left()..end {
|
||||
buf.get_mut(x, y).set_symbol(" ");
|
||||
let cell = buf.get_mut(x, y);
|
||||
if self.use_unicode {
|
||||
cell.set_symbol(symbols::block::FULL)
|
||||
.set_fg(self.gauge_style.fg.unwrap_or(Color::Reset))
|
||||
.set_bg(self.gauge_style.bg.unwrap_or(Color::Reset));
|
||||
} else {
|
||||
// spaces are needed to apply the background styling.
|
||||
// note that the background and foreground colors are swapped
|
||||
// otherwise the gauge will be inverted
|
||||
cell.set_symbol(" ")
|
||||
.set_fg(self.gauge_style.bg.unwrap_or(Color::Reset))
|
||||
.set_bg(self.gauge_style.fg.unwrap_or(Color::Reset));
|
||||
}
|
||||
}
|
||||
if self.use_unicode && self.ratio < 1.0 {
|
||||
buf.get_mut(end, y)
|
||||
.set_symbol(get_unicode_block(filled_width % 1.0));
|
||||
}
|
||||
}
|
||||
// render the label
|
||||
buf.set_span(label_col, label_row, &label, clamped_label_width);
|
||||
}
|
||||
}
|
||||
|
||||
if y == center {
|
||||
// Label
|
||||
let precent_label = format!("{}%", self.percent);
|
||||
let label = self.label.unwrap_or(&precent_label);
|
||||
let label_width = label.width() as u16;
|
||||
let middle = (gauge_area.width - label_width) / 2 + gauge_area.left();
|
||||
buf.set_string(middle, y, label, &self.style);
|
||||
}
|
||||
fn get_unicode_block<'a>(frac: f64) -> &'a str {
|
||||
match (frac * 8.0).round() as u16 {
|
||||
1 => symbols::block::ONE_EIGHTH,
|
||||
2 => symbols::block::ONE_QUARTER,
|
||||
3 => symbols::block::THREE_EIGHTHS,
|
||||
4 => symbols::block::HALF,
|
||||
5 => symbols::block::FIVE_EIGHTHS,
|
||||
6 => symbols::block::THREE_QUARTERS,
|
||||
7 => symbols::block::SEVEN_EIGHTHS,
|
||||
8 => symbols::block::FULL,
|
||||
_ => " ",
|
||||
}
|
||||
}
|
||||
|
||||
// Fix colors
|
||||
for x in gauge_area.left()..end {
|
||||
buf.get_mut(x, y)
|
||||
.set_fg(self.style.bg)
|
||||
.set_bg(self.style.fg);
|
||||
}
|
||||
/// A compact widget to display a task progress over a single line.
|
||||
///
|
||||
/// # Examples:
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::widgets::{Widget, LineGauge, Block, Borders};
|
||||
/// # use ratatui::style::{Style, Color, Modifier};
|
||||
/// # use ratatui::symbols;
|
||||
/// LineGauge::default()
|
||||
/// .block(Block::default().borders(Borders::ALL).title("Progress"))
|
||||
/// .gauge_style(Style::default().fg(Color::White).bg(Color::Black).add_modifier(Modifier::BOLD))
|
||||
/// .line_set(symbols::line::THICK)
|
||||
/// .ratio(0.4);
|
||||
/// ```
|
||||
pub struct LineGauge<'a> {
|
||||
block: Option<Block<'a>>,
|
||||
ratio: f64,
|
||||
label: Option<Line<'a>>,
|
||||
line_set: symbols::line::Set,
|
||||
style: Style,
|
||||
gauge_style: Style,
|
||||
}
|
||||
|
||||
impl<'a> Default for LineGauge<'a> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
block: None,
|
||||
ratio: 0.0,
|
||||
label: None,
|
||||
style: Style::default(),
|
||||
line_set: symbols::line::NORMAL,
|
||||
gauge_style: Style::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> LineGauge<'a> {
|
||||
pub fn block(mut self, block: Block<'a>) -> Self {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn ratio(mut self, ratio: f64) -> Self {
|
||||
assert!(
|
||||
(0.0..=1.0).contains(&ratio),
|
||||
"Ratio should be between 0 and 1 inclusively."
|
||||
);
|
||||
self.ratio = ratio;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn line_set(mut self, set: symbols::line::Set) -> Self {
|
||||
self.line_set = set;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn label<T>(mut self, label: T) -> Self
|
||||
where
|
||||
T: Into<Line<'a>>,
|
||||
{
|
||||
self.label = Some(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: Style) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn gauge_style(mut self, style: Style) -> Self {
|
||||
self.gauge_style = style;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for LineGauge<'a> {
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_style(area, self.style);
|
||||
let gauge_area = match self.block.take() {
|
||||
Some(b) => {
|
||||
let inner_area = b.inner(area);
|
||||
b.render(area, buf);
|
||||
inner_area
|
||||
}
|
||||
None => area,
|
||||
};
|
||||
|
||||
if gauge_area.height < 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
let ratio = self.ratio;
|
||||
let label = self
|
||||
.label
|
||||
.unwrap_or_else(move || Line::from(format!("{:.0}%", ratio * 100.0)));
|
||||
let (col, row) = buf.set_line(
|
||||
gauge_area.left(),
|
||||
gauge_area.top(),
|
||||
&label,
|
||||
gauge_area.width,
|
||||
);
|
||||
let start = col + 1;
|
||||
if start >= gauge_area.right() {
|
||||
return;
|
||||
}
|
||||
|
||||
let end = start
|
||||
+ (f64::from(gauge_area.right().saturating_sub(start)) * self.ratio).floor() as u16;
|
||||
for col in start..end {
|
||||
buf.get_mut(col, row)
|
||||
.set_symbol(self.line_set.horizontal)
|
||||
.set_style(Style {
|
||||
fg: self.gauge_style.fg,
|
||||
bg: None,
|
||||
add_modifier: self.gauge_style.add_modifier,
|
||||
sub_modifier: self.gauge_style.sub_modifier,
|
||||
});
|
||||
}
|
||||
for col in end..gauge_area.right() {
|
||||
buf.get_mut(col, row)
|
||||
.set_symbol(self.line_set.horizontal)
|
||||
.set_style(Style {
|
||||
fg: self.gauge_style.bg,
|
||||
bg: None,
|
||||
add_modifier: self.gauge_style.add_modifier,
|
||||
sub_modifier: self.gauge_style.sub_modifier,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn gauge_invalid_percentage() {
|
||||
Gauge::default().percent(110);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn gauge_invalid_ratio_upper_bound() {
|
||||
Gauge::default().ratio(1.1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn gauge_invalid_ratio_lower_bound() {
|
||||
Gauge::default().ratio(-0.5);
|
||||
}
|
||||
}
|
||||
|
||||
228
src/widgets/histogram.rs
Normal file
228
src/widgets/histogram.rs
Normal file
@@ -0,0 +1,228 @@
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::Style,
|
||||
symbols,
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
/// A bar chart specialized for showing histograms
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use tui::widgets::{Block, Borders, Histogram};
|
||||
/// # use tui::style::{Style, Color, Modifier};
|
||||
/// Histogram::default()
|
||||
/// .block(Block::default().title("Histogram").borders(Borders::ALL))
|
||||
/// .bar_width(3)
|
||||
/// .bar_gap(1)
|
||||
/// .bar_style(Style::default().fg(Color::Yellow).bg(Color::Red))
|
||||
/// .value_style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD))
|
||||
/// .label_style(Style::default().fg(Color::White))
|
||||
/// .data(&[("B0", 0), ("B1", 2), ("B2", 4), ("B3", 3)])
|
||||
/// .max(4);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Histogram<'a> {
|
||||
/// Block to wrap the widget in
|
||||
block: Option<Block<'a>>,
|
||||
/// The gap between each bar
|
||||
bar_gap: u16,
|
||||
/// Set of symbols used to display the data
|
||||
bar_set: symbols::bar::Set,
|
||||
/// Style of the bars
|
||||
bar_style: Style,
|
||||
/// Style of the values printed at the bottom of each bar
|
||||
value_style: Style,
|
||||
/// Style of the labels printed under each bar
|
||||
label_style: Style,
|
||||
/// Style for the widget
|
||||
style: Style,
|
||||
/// Slice of values to plot on the chart
|
||||
data: &'a [u64],
|
||||
/// each bucket keeps a count of the data points that fall into it
|
||||
/// buckets[0] counts items where 0 <= x < bucket_size
|
||||
/// buckets[1] counts items where bucket_size <= x < 2*bucket_size
|
||||
/// etc.
|
||||
buckets: Vec<u64>,
|
||||
/// Value necessary for a bar to reach the maximum height (if no value is specified,
|
||||
/// the maximum value in the data is taken as reference)
|
||||
max: Option<u64>,
|
||||
/// Values to display on the bar (computed when the data is passed to the widget)
|
||||
values: Vec<String>,
|
||||
}
|
||||
|
||||
impl<'a> Default for Histogram<'a> {
|
||||
fn default() -> Histogram<'a> {
|
||||
Histogram {
|
||||
block: None,
|
||||
max: None,
|
||||
data: &[],
|
||||
values: Vec::new(),
|
||||
bar_style: Style::default(),
|
||||
bar_gap: 1,
|
||||
bar_set: symbols::bar::NINE_LEVELS,
|
||||
buckets: Vec::new(),
|
||||
value_style: Default::default(),
|
||||
label_style: Default::default(),
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Histogram<'a> {
|
||||
pub fn data(mut self, data: &'a [u64], n_buckets: u64) -> Histogram<'a> {
|
||||
self.data = data;
|
||||
|
||||
let min = *self.data.iter().min().unwrap();
|
||||
let max = *self.data.iter().max().unwrap() + 1;
|
||||
let bucket_size: u64 = ((max - min) as f64 / n_buckets as f64).ceil() as u64;
|
||||
self.buckets = vec![0; n_buckets as usize];
|
||||
|
||||
// initialize buckets
|
||||
self.values = Vec::with_capacity(n_buckets as usize);
|
||||
for v in 0..n_buckets {
|
||||
self.values.push(format!("{}", v * bucket_size));
|
||||
}
|
||||
|
||||
// bucketize data
|
||||
for &x in self.data.iter() {
|
||||
let idx: usize = ((x - min) / bucket_size) as usize;
|
||||
self.buckets[idx] += 1;
|
||||
}
|
||||
|
||||
self.max = Some(*self.buckets.iter().max().unwrap());
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn block(mut self, block: Block<'a>) -> Histogram<'a> {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn max(mut self, max: u64) -> Histogram<'a> {
|
||||
self.max = Some(max);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn bar_style(mut self, style: Style) -> Histogram<'a> {
|
||||
self.bar_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn bar_gap(mut self, gap: u16) -> Histogram<'a> {
|
||||
self.bar_gap = gap;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn bar_set(mut self, bar_set: symbols::bar::Set) -> Histogram<'a> {
|
||||
self.bar_set = bar_set;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn value_style(mut self, style: Style) -> Histogram<'a> {
|
||||
self.value_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn label_style(mut self, style: Style) -> Histogram<'a> {
|
||||
self.label_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: Style) -> Histogram<'a> {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Histogram<'a> {
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_style(area, self.style);
|
||||
|
||||
let chart_area = match self.block.take() {
|
||||
Some(b) => {
|
||||
let inner_area = b.inner(area);
|
||||
b.render(area, buf);
|
||||
inner_area
|
||||
}
|
||||
None => area,
|
||||
};
|
||||
|
||||
if chart_area.height < 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
let n_bars = self.buckets.len() as u16;
|
||||
let bar_width: u16 = (chart_area.width - (n_bars + 1) * self.bar_gap) / n_bars;
|
||||
|
||||
let max = self
|
||||
.max
|
||||
.unwrap_or_else(|| self.buckets.iter().copied().max().unwrap_or_default());
|
||||
|
||||
let mut data = self
|
||||
.buckets
|
||||
.iter()
|
||||
.take(n_bars as usize)
|
||||
.map(|&v| v * u64::from(chart_area.height - 1) * 8 / std::cmp::max(max, 1))
|
||||
.collect::<Vec<u64>>();
|
||||
for j in (0..chart_area.height - 1).rev() {
|
||||
for (i, d) in data.iter_mut().enumerate() {
|
||||
let symbol = match d {
|
||||
0 => self.bar_set.empty,
|
||||
1 => self.bar_set.one_eighth,
|
||||
2 => self.bar_set.one_quarter,
|
||||
3 => self.bar_set.three_eighths,
|
||||
4 => self.bar_set.half,
|
||||
5 => self.bar_set.five_eighths,
|
||||
6 => self.bar_set.three_quarters,
|
||||
7 => self.bar_set.seven_eighths,
|
||||
_ => self.bar_set.full,
|
||||
};
|
||||
|
||||
for x in 0..bar_width {
|
||||
buf.get_mut(
|
||||
chart_area.left() + i as u16 * (bar_width + self.bar_gap) + x,
|
||||
chart_area.top() + j,
|
||||
)
|
||||
.set_symbol(symbol)
|
||||
.set_style(self.bar_style);
|
||||
}
|
||||
|
||||
if *d > 8 {
|
||||
*d -= 8;
|
||||
} else {
|
||||
*d = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (i, &value) in self.buckets.iter().enumerate() {
|
||||
let label = &self.values[i];
|
||||
if value != 0 {
|
||||
let value_label = format!("{}", &self.buckets[i]);
|
||||
let width = value_label.width() as u16;
|
||||
if width < bar_width {
|
||||
buf.set_string(
|
||||
chart_area.left()
|
||||
+ i as u16 * (bar_width + self.bar_gap)
|
||||
+ (bar_width - width) / 2,
|
||||
chart_area.bottom() - 2,
|
||||
value_label,
|
||||
self.value_style,
|
||||
);
|
||||
}
|
||||
}
|
||||
buf.set_stringn(
|
||||
chart_area.left() + i as u16 * (bar_width + self.bar_gap),
|
||||
chart_area.bottom() - 1,
|
||||
label,
|
||||
bar_width as usize,
|
||||
self.label_style,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,67 +1,285 @@
|
||||
mod block;
|
||||
mod paragraph;
|
||||
mod list;
|
||||
mod gauge;
|
||||
mod sparkline;
|
||||
mod chart;
|
||||
//! `widgets` is a collection of types that implement [`Widget`] or [`StatefulWidget`] or both.
|
||||
//!
|
||||
//! All widgets are implemented using the builder pattern and are consumable objects. They are not
|
||||
//! meant to be stored but used as *commands* to draw common figures in the UI.
|
||||
//!
|
||||
//! The available widgets are:
|
||||
//! - [`Block`]
|
||||
//! - [`Tabs`]
|
||||
//! - [`List`]
|
||||
//! - [`Table`]
|
||||
//! - [`Paragraph`]
|
||||
//! - [`Chart`]
|
||||
//! - [`BarChart`]
|
||||
//! - [`Gauge`]
|
||||
//! - [`Sparkline`]
|
||||
//! - [`calendar::Monthly`]
|
||||
//! - [`Clear`]
|
||||
|
||||
mod barchart;
|
||||
mod tabs;
|
||||
mod table;
|
||||
pub mod block;
|
||||
#[cfg(feature = "widget-calendar")]
|
||||
pub mod calendar;
|
||||
pub mod canvas;
|
||||
mod chart;
|
||||
mod clear;
|
||||
mod gauge;
|
||||
mod histogram;
|
||||
mod list;
|
||||
mod paragraph;
|
||||
mod reflow;
|
||||
pub mod scrollbar;
|
||||
mod sparkline;
|
||||
mod table;
|
||||
mod tabs;
|
||||
|
||||
pub use self::block::Block;
|
||||
pub use self::paragraph::Paragraph;
|
||||
pub use self::list::{Item, List, SelectableList};
|
||||
pub use self::gauge::Gauge;
|
||||
pub use self::sparkline::Sparkline;
|
||||
pub use self::chart::{Axis, Chart, Dataset, Marker};
|
||||
pub use self::barchart::BarChart;
|
||||
pub use self::tabs::Tabs;
|
||||
pub use self::table::{Row, Table};
|
||||
use std::fmt::{self, Debug};
|
||||
|
||||
use buffer::Buffer;
|
||||
use layout::Rect;
|
||||
use terminal::Terminal;
|
||||
use backend::Backend;
|
||||
use style::Color;
|
||||
use bitflags::bitflags;
|
||||
|
||||
pub use self::{
|
||||
barchart::BarChart,
|
||||
block::{Block, BorderType, Padding},
|
||||
chart::{Axis, Chart, Dataset, GraphType},
|
||||
clear::Clear,
|
||||
gauge::{Gauge, LineGauge},
|
||||
histogram::Histogram,
|
||||
list::{List, ListItem, ListState},
|
||||
paragraph::{Paragraph, Wrap},
|
||||
scrollbar::{ScrollDirection, Scrollbar, ScrollbarOrientation, ScrollbarState},
|
||||
sparkline::{RenderDirection, Sparkline},
|
||||
table::{Cell, Row, Table, TableState},
|
||||
tabs::Tabs,
|
||||
};
|
||||
use crate::{buffer::Buffer, layout::Rect};
|
||||
|
||||
/// Bitflags that can be composed to set the visible borders essentially on the block widget.
|
||||
bitflags! {
|
||||
pub struct Borders: u32 {
|
||||
/// Bitflags that can be composed to set the visible borders essentially on the block widget.
|
||||
#[derive(Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub struct Borders: u8 {
|
||||
/// Show no border (default)
|
||||
const NONE = 0b0000_0001;
|
||||
const NONE = 0b0000;
|
||||
/// Show the top border
|
||||
const TOP = 0b0000_0010;
|
||||
const TOP = 0b0001;
|
||||
/// Show the right border
|
||||
const RIGHT = 0b0000_0100;
|
||||
const RIGHT = 0b0010;
|
||||
/// Show the bottom border
|
||||
const BOTTOM = 0b000_1000;
|
||||
const BOTTOM = 0b0100;
|
||||
/// Show the left border
|
||||
const LEFT = 0b0001_0000;
|
||||
const LEFT = 0b1000;
|
||||
/// Show all borders
|
||||
const ALL = Self::TOP.bits | Self::RIGHT.bits | Self::BOTTOM.bits | Self::LEFT.bits;
|
||||
const ALL = Self::TOP.bits() | Self::RIGHT.bits() | Self::BOTTOM.bits() | Self::LEFT.bits();
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement the `Debug` trait for the `Borders` bitflags. This is a manual implementation to
|
||||
/// display the flags in a more readable way. The default implementation would display the
|
||||
/// flags as 'Border(0x0)' for `Borders::NONE` for example.
|
||||
impl Debug for Borders {
|
||||
/// Display the Borders bitflags as a list of names. For example, `Borders::NONE` will be
|
||||
/// displayed as `NONE` and `Borders::ALL` will be displayed as `ALL`. If multiple flags are
|
||||
/// set, they will be displayed separated by a pipe character.
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if self.is_empty() {
|
||||
return write!(f, "NONE");
|
||||
}
|
||||
if self.is_all() {
|
||||
return write!(f, "ALL");
|
||||
}
|
||||
let mut first = true;
|
||||
for (name, border) in self.iter_names() {
|
||||
if border == Borders::NONE {
|
||||
continue;
|
||||
}
|
||||
if first {
|
||||
write!(f, "{name}")?;
|
||||
first = false;
|
||||
} else {
|
||||
write!(f, " | {name}")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Base requirements for a Widget
|
||||
pub trait Widget {
|
||||
/// Draws the current state of the widget in the given buffer. That the only method required to
|
||||
/// implement a custom widget.
|
||||
fn draw(&mut self, area: &Rect, buf: &mut Buffer);
|
||||
/// Helper method to quickly set the background of all cells inside the specified area.
|
||||
fn background(&self, area: &Rect, buf: &mut Buffer, color: Color) {
|
||||
for y in area.top()..area.bottom() {
|
||||
for x in area.left()..area.right() {
|
||||
buf.get_mut(x, y).set_bg(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Helper method that can be chained with a widget's builder methods to render it.
|
||||
fn render<B>(&mut self, t: &mut Terminal<B>, area: &Rect)
|
||||
where
|
||||
Self: Sized,
|
||||
B: Backend,
|
||||
{
|
||||
t.render(self, area);
|
||||
/// Draws the current state of the widget in the given buffer. That is the only method required
|
||||
/// to implement a custom widget.
|
||||
fn render(self, area: Rect, buf: &mut Buffer);
|
||||
}
|
||||
|
||||
/// A `StatefulWidget` is a widget that can take advantage of some local state to remember things
|
||||
/// between two draw calls.
|
||||
///
|
||||
/// Most widgets can be drawn directly based on the input parameters. However, some features may
|
||||
/// require some kind of associated state to be implemented.
|
||||
///
|
||||
/// For example, the [`List`] widget can highlight the item currently selected. This can be
|
||||
/// translated in an offset, which is the number of elements to skip in order to have the selected
|
||||
/// item within the viewport currently allocated to this widget. The widget can therefore only
|
||||
/// provide the following behavior: whenever the selected item is out of the viewport scroll to a
|
||||
/// predefined position (making the selected item the last viewable item or the one in the middle
|
||||
/// for example). Nonetheless, if the widget has access to the last computed offset then it can
|
||||
/// implement a natural scrolling experience where the last offset is reused until the selected
|
||||
/// item is out of the viewport.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use std::io;
|
||||
/// # use ratatui::Terminal;
|
||||
/// # use ratatui::backend::{Backend, TestBackend};
|
||||
/// # use ratatui::widgets::{Widget, List, ListItem, ListState};
|
||||
///
|
||||
/// // Let's say we have some events to display.
|
||||
/// struct Events {
|
||||
/// // `items` is the state managed by your application.
|
||||
/// items: Vec<String>,
|
||||
/// // `state` is the state that can be modified by the UI. It stores the index of the selected
|
||||
/// // item as well as the offset computed during the previous draw call (used to implement
|
||||
/// // natural scrolling).
|
||||
/// state: ListState
|
||||
/// }
|
||||
///
|
||||
/// impl Events {
|
||||
/// fn new(items: Vec<String>) -> Events {
|
||||
/// Events {
|
||||
/// items,
|
||||
/// state: ListState::default(),
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// pub fn set_items(&mut self, items: Vec<String>) {
|
||||
/// self.items = items;
|
||||
/// // We reset the state as the associated items have changed. This effectively reset
|
||||
/// // the selection as well as the stored offset.
|
||||
/// self.state = ListState::default();
|
||||
/// }
|
||||
///
|
||||
/// // Select the next item. This will not be reflected until the widget is drawn in the
|
||||
/// // `Terminal::draw` callback using `Frame::render_stateful_widget`.
|
||||
/// pub fn next(&mut self) {
|
||||
/// let i = match self.state.selected() {
|
||||
/// Some(i) => {
|
||||
/// if i >= self.items.len() - 1 {
|
||||
/// 0
|
||||
/// } else {
|
||||
/// i + 1
|
||||
/// }
|
||||
/// }
|
||||
/// None => 0,
|
||||
/// };
|
||||
/// self.state.select(Some(i));
|
||||
/// }
|
||||
///
|
||||
/// // Select the previous item. This will not be reflected until the widget is drawn in the
|
||||
/// // `Terminal::draw` callback using `Frame::render_stateful_widget`.
|
||||
/// pub fn previous(&mut self) {
|
||||
/// let i = match self.state.selected() {
|
||||
/// Some(i) => {
|
||||
/// if i == 0 {
|
||||
/// self.items.len() - 1
|
||||
/// } else {
|
||||
/// i - 1
|
||||
/// }
|
||||
/// }
|
||||
/// None => 0,
|
||||
/// };
|
||||
/// self.state.select(Some(i));
|
||||
/// }
|
||||
///
|
||||
/// // Unselect the currently selected item if any. The implementation of `ListState` makes
|
||||
/// // sure that the stored offset is also reset.
|
||||
/// pub fn unselect(&mut self) {
|
||||
/// self.state.select(None);
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
///
|
||||
/// let mut events = Events::new(vec![
|
||||
/// String::from("Item 1"),
|
||||
/// String::from("Item 2")
|
||||
/// ]);
|
||||
///
|
||||
/// loop {
|
||||
/// terminal.draw(|f| {
|
||||
/// // The items managed by the application are transformed to something
|
||||
/// // that is understood by ratatui.
|
||||
/// let items: Vec<ListItem>= events.items.iter().map(|i| ListItem::new(i.as_str())).collect();
|
||||
/// // The `List` widget is then built with those items.
|
||||
/// let list = List::new(items);
|
||||
/// // Finally the widget is rendered using the associated state. `events.state` is
|
||||
/// // effectively the only thing that we will "remember" from this draw call.
|
||||
/// f.render_stateful_widget(list, f.size(), &mut events.state);
|
||||
/// });
|
||||
///
|
||||
/// // In response to some input events or an external http request or whatever:
|
||||
/// events.next();
|
||||
/// }
|
||||
/// ```
|
||||
pub trait StatefulWidget {
|
||||
type State;
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State);
|
||||
}
|
||||
|
||||
/// Macro that constructs and returns a [`Borders`] object from TOP, BOTTOM, LEFT, RIGHT, NONE, and
|
||||
/// ALL. Internally it creates an empty `Borders` object and then inserts each bit flag specified
|
||||
/// into it using `Borders::insert()`.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
///```
|
||||
/// # use ratatui::widgets::{Block, Borders};
|
||||
/// # use ratatui::style::{Style, Color};
|
||||
/// # use ratatui::border;
|
||||
///
|
||||
/// Block::default()
|
||||
/// //Construct a `Borders` object and use it in place
|
||||
/// .borders(border!(TOP, BOTTOM));
|
||||
///
|
||||
/// //`border!` can be called with any order of individual sides
|
||||
/// let bottom_first = border!(BOTTOM, LEFT, TOP);
|
||||
/// //with the ALL keyword which works as expected
|
||||
/// let all = border!(ALL);
|
||||
/// //or with nothing to return a `Borders::NONE' bitflag.
|
||||
/// let none = border!(NONE);
|
||||
/// ```
|
||||
#[cfg(feature = "macros")]
|
||||
#[macro_export]
|
||||
macro_rules! border {
|
||||
( $($b:tt), +) => {{
|
||||
let mut border = Borders::empty();
|
||||
$(
|
||||
border.insert(Borders::$b);
|
||||
)*
|
||||
border
|
||||
}};
|
||||
() =>{
|
||||
Borders::NONE
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_borders_debug() {
|
||||
assert_eq!(format!("{:?}", Borders::empty()), "NONE");
|
||||
assert_eq!(format!("{:?}", Borders::NONE), "NONE");
|
||||
assert_eq!(format!("{:?}", Borders::TOP), "TOP");
|
||||
assert_eq!(format!("{:?}", Borders::BOTTOM), "BOTTOM");
|
||||
assert_eq!(format!("{:?}", Borders::LEFT), "LEFT");
|
||||
assert_eq!(format!("{:?}", Borders::RIGHT), "RIGHT");
|
||||
assert_eq!(format!("{:?}", Borders::ALL), "ALL");
|
||||
assert_eq!(format!("{:?}", Borders::all()), "ALL");
|
||||
|
||||
assert_eq!(
|
||||
format!("{:?}", Borders::TOP | Borders::BOTTOM),
|
||||
"TOP | BOTTOM"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
230
src/widgets/overlappingbarchart.rs
Normal file
230
src/widgets/overlappingbarchart.rs
Normal file
@@ -0,0 +1,230 @@
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::Style,
|
||||
symbols,
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
use std::cmp::min;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
/// A series for a stacked bar chart
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BarSeries<'a> {
|
||||
/// Name of the series
|
||||
name: Cow<'a, str>,
|
||||
/// The color to display for this series
|
||||
bar_style: Style,
|
||||
/// A reference to the data for this series
|
||||
data: &'a [u64]
|
||||
}
|
||||
|
||||
/// Display multiple bars in a single widgets
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use tui::widgets::{Block, Borders, BarChart};
|
||||
/// # use tui::style::{Style, Color, Modifier};
|
||||
/// BarChart::default()
|
||||
/// .block(Block::default().title("BarChart").borders(Borders::ALL))
|
||||
/// .bar_width(3)
|
||||
/// .bar_gap(1)
|
||||
/// .bar_style(Style::default().fg(Color::Yellow).bg(Color::Red))
|
||||
/// .value_style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD))
|
||||
/// .label_style(Style::default().fg(Color::White))
|
||||
/// .data(&[("B0", 0), ("B1", 2), ("B2", 4), ("B3", 3)])
|
||||
/// .max(4);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OverlappingBarChart<'a> {
|
||||
/// Block to wrap the widget in
|
||||
block: Option<Block<'a>>,
|
||||
/// The width of each bar
|
||||
bar_width: u16,
|
||||
/// The gap between each bar
|
||||
bar_gap: u16,
|
||||
/// Set of symbols used to display the data
|
||||
bar_set: symbols::bar::Set,
|
||||
/// Style of the bars
|
||||
bar_style: Style,
|
||||
/// Style of the values printed at the bottom of each bar
|
||||
value_style: Style,
|
||||
/// Style of the labels printed under each bar
|
||||
label_style: Style,
|
||||
/// Style for the widget
|
||||
style: Style,
|
||||
/// Vec of slices of (label, value) pair to plot on the chart
|
||||
data: Vec<&'a [(&'a str, u64)]>,
|
||||
/// Value necessary for a bar to reach the maximum height (if no value is specified,
|
||||
/// the maximum value in the data is taken as reference)
|
||||
max: Option<u64>,
|
||||
/// Values to display on the bar (computed when the data is passed to the widget)
|
||||
values: Vec<String>,
|
||||
}
|
||||
|
||||
impl<'a> Default for BarChart<'a> {
|
||||
fn default() -> BarChart<'a> {
|
||||
BarChart {
|
||||
block: None,
|
||||
max: None,
|
||||
data: Vec::new(),
|
||||
values: Vec::new(),
|
||||
bar_style: Style::default(),
|
||||
bar_width: 1,
|
||||
bar_gap: 1,
|
||||
bar_set: symbols::bar::NINE_LEVELS,
|
||||
value_style: Default::default(),
|
||||
label_style: Default::default(),
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> BarChart<'a> {
|
||||
pub fn data(mut self, data: &'a [(&'a str, u64)]) -> BarChart<'a> {
|
||||
self.data = data;
|
||||
self.values = Vec::with_capacity(self.data.len());
|
||||
for &(_, v) in self.data {
|
||||
self.values.push(format!("{}", v));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn block(mut self, block: Block<'a>) -> BarChart<'a> {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn max(mut self, max: u64) -> BarChart<'a> {
|
||||
self.max = Some(max);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn bar_style(mut self, style: Style) -> BarChart<'a> {
|
||||
self.bar_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn bar_width(mut self, width: u16) -> BarChart<'a> {
|
||||
self.bar_width = width;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn bar_gap(mut self, gap: u16) -> BarChart<'a> {
|
||||
self.bar_gap = gap;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn bar_set(mut self, bar_set: symbols::bar::Set) -> BarChart<'a> {
|
||||
self.bar_set = bar_set;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn value_style(mut self, style: Style) -> BarChart<'a> {
|
||||
self.value_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn label_style(mut self, style: Style) -> BarChart<'a> {
|
||||
self.label_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: Style) -> BarChart<'a> {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for BarChart<'a> {
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_style(area, self.style);
|
||||
|
||||
let chart_area = match self.block.take() {
|
||||
Some(b) => {
|
||||
let inner_area = b.inner(area);
|
||||
b.render(area, buf);
|
||||
inner_area
|
||||
}
|
||||
None => area,
|
||||
};
|
||||
|
||||
if chart_area.height < 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
let max = self
|
||||
.max
|
||||
.unwrap_or_else(|| self.data.iter().map(|t| t.1).max().unwrap_or_default());
|
||||
let max_index = min(
|
||||
(chart_area.width / (self.bar_width + self.bar_gap)) as usize,
|
||||
self.data.len(),
|
||||
);
|
||||
let mut data = self
|
||||
.data
|
||||
.iter()
|
||||
.take(max_index)
|
||||
.map(|&(l, v)| {
|
||||
(
|
||||
l,
|
||||
v * u64::from(chart_area.height - 1) * 8 / std::cmp::max(max, 1),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<(&str, u64)>>();
|
||||
for j in (0..chart_area.height - 1).rev() {
|
||||
for (i, d) in data.iter_mut().enumerate() {
|
||||
let symbol = match d.1 {
|
||||
0 => self.bar_set.empty,
|
||||
1 => self.bar_set.one_eighth,
|
||||
2 => self.bar_set.one_quarter,
|
||||
3 => self.bar_set.three_eighths,
|
||||
4 => self.bar_set.half,
|
||||
5 => self.bar_set.five_eighths,
|
||||
6 => self.bar_set.three_quarters,
|
||||
7 => self.bar_set.seven_eighths,
|
||||
_ => self.bar_set.full,
|
||||
};
|
||||
|
||||
for x in 0..self.bar_width {
|
||||
buf.get_mut(
|
||||
chart_area.left() + i as u16 * (self.bar_width + self.bar_gap) + x,
|
||||
chart_area.top() + j,
|
||||
)
|
||||
.set_symbol(symbol)
|
||||
.set_style(self.bar_style);
|
||||
}
|
||||
|
||||
if d.1 > 8 {
|
||||
d.1 -= 8;
|
||||
} else {
|
||||
d.1 = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (i, &(label, value)) in self.data.iter().take(max_index).enumerate() {
|
||||
if value != 0 {
|
||||
let value_label = &self.values[i];
|
||||
let width = value_label.width() as u16;
|
||||
if width < self.bar_width {
|
||||
buf.set_string(
|
||||
chart_area.left()
|
||||
+ i as u16 * (self.bar_width + self.bar_gap)
|
||||
+ (self.bar_width - width) / 2,
|
||||
chart_area.bottom() - 2,
|
||||
value_label,
|
||||
self.value_style,
|
||||
);
|
||||
}
|
||||
}
|
||||
buf.set_stringn(
|
||||
chart_area.left() + i as u16 * (self.bar_width + self.bar_gap),
|
||||
chart_area.bottom() - 1,
|
||||
label,
|
||||
self.bar_width as usize,
|
||||
self.label_style,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,274 +1,705 @@
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use widgets::{Block, Widget};
|
||||
use buffer::Buffer;
|
||||
use layout::Rect;
|
||||
use style::{Color, Modifier, Style};
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
style::{Style, Styled},
|
||||
text::{StyledGrapheme, Text},
|
||||
widgets::{
|
||||
reflow::{LineComposer, LineTruncator, WordWrapper},
|
||||
Block, Widget,
|
||||
},
|
||||
};
|
||||
|
||||
/// A widget to display some text. You can specify colors using commands embedded in
|
||||
/// the text such as "{[color] [text]}".
|
||||
fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment) -> u16 {
|
||||
match alignment {
|
||||
Alignment::Center => (text_area_width / 2).saturating_sub(line_width / 2),
|
||||
Alignment::Right => text_area_width.saturating_sub(line_width),
|
||||
Alignment::Left => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget to display some text.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # extern crate tui;
|
||||
/// # use tui::widgets::{Block, Borders, Paragraph};
|
||||
/// # use tui::style::{Style, Color};
|
||||
/// # fn main() {
|
||||
/// Paragraph::default()
|
||||
/// # use ratatui::text::{Text, Line, Span};
|
||||
/// # use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
|
||||
/// # use ratatui::style::{Style, Color, Modifier};
|
||||
/// # use ratatui::layout::{Alignment};
|
||||
/// let text = vec![
|
||||
/// Line::from(vec![
|
||||
/// Span::raw("First"),
|
||||
/// Span::styled("line",Style::default().add_modifier(Modifier::ITALIC)),
|
||||
/// Span::raw("."),
|
||||
/// ]),
|
||||
/// Line::from(Span::styled("Second line", Style::default().fg(Color::Red))),
|
||||
/// ];
|
||||
/// Paragraph::new(text)
|
||||
/// .block(Block::default().title("Paragraph").borders(Borders::ALL))
|
||||
/// .style(Style::default().fg(Color::White).bg(Color::Black))
|
||||
/// .wrap(true)
|
||||
/// .text("First line\nSecond line\n{red Colored text}.");
|
||||
/// # }
|
||||
/// .alignment(Alignment::Center)
|
||||
/// .wrap(Wrap { trim: true });
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Paragraph<'a> {
|
||||
/// A block to wrap the widget in
|
||||
block: Option<Block<'a>>,
|
||||
/// Widget style
|
||||
style: Style,
|
||||
/// Wrap the text or not
|
||||
wrapping: bool,
|
||||
/// How to wrap the text
|
||||
wrap: Option<Wrap>,
|
||||
/// The text to display
|
||||
text: &'a str,
|
||||
/// Should we parse the text for embedded commands
|
||||
raw: bool,
|
||||
text: Text<'a>,
|
||||
/// Scroll
|
||||
scroll: u16,
|
||||
scroll: (u16, u16),
|
||||
/// Alignment of the text
|
||||
alignment: Alignment,
|
||||
}
|
||||
|
||||
impl<'a> Default for Paragraph<'a> {
|
||||
fn default() -> Paragraph<'a> {
|
||||
Paragraph {
|
||||
block: None,
|
||||
style: Default::default(),
|
||||
wrapping: false,
|
||||
raw: false,
|
||||
text: "",
|
||||
scroll: 0,
|
||||
}
|
||||
}
|
||||
/// Describes how to wrap text across lines.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::widgets::{Paragraph, Wrap};
|
||||
/// # use ratatui::text::Text;
|
||||
/// let bullet_points = Text::from(r#"Some indented points:
|
||||
/// - First thing goes here and is long so that it wraps
|
||||
/// - Here is another point that is long enough to wrap"#);
|
||||
///
|
||||
/// // With leading spaces trimmed (window width of 30 chars):
|
||||
/// Paragraph::new(bullet_points.clone()).wrap(Wrap { trim: true });
|
||||
/// // Some indented points:
|
||||
/// // - First thing goes here and is
|
||||
/// // long so that it wraps
|
||||
/// // - Here is another point that
|
||||
/// // is long enough to wrap
|
||||
///
|
||||
/// // But without trimming, indentation is preserved:
|
||||
/// Paragraph::new(bullet_points).wrap(Wrap { trim: false });
|
||||
/// // Some indented points:
|
||||
/// // - First thing goes here
|
||||
/// // and is long so that it wraps
|
||||
/// // - Here is another point
|
||||
/// // that is long enough to wrap
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Wrap {
|
||||
/// Should leading whitespace be trimmed
|
||||
pub trim: bool,
|
||||
}
|
||||
|
||||
impl<'a> Paragraph<'a> {
|
||||
pub fn block(&'a mut self, block: Block<'a>) -> &mut Paragraph<'a> {
|
||||
pub fn new<T>(text: T) -> Paragraph<'a>
|
||||
where
|
||||
T: Into<Text<'a>>,
|
||||
{
|
||||
Paragraph {
|
||||
block: None,
|
||||
style: Style::default(),
|
||||
wrap: None,
|
||||
text: text.into(),
|
||||
scroll: (0, 0),
|
||||
alignment: Alignment::Left,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn block(mut self, block: Block<'a>) -> Paragraph<'a> {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn text(&mut self, text: &'a str) -> &mut Paragraph<'a> {
|
||||
self.text = text;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(&mut self, style: Style) -> &mut Paragraph<'a> {
|
||||
pub fn style(mut self, style: Style) -> Paragraph<'a> {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn wrap(&mut self, flag: bool) -> &mut Paragraph<'a> {
|
||||
self.wrapping = flag;
|
||||
pub fn wrap(mut self, wrap: Wrap) -> Paragraph<'a> {
|
||||
self.wrap = Some(wrap);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn raw(&mut self, flag: bool) -> &mut Paragraph<'a> {
|
||||
self.raw = flag;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn scroll(&mut self, offset: u16) -> &mut Paragraph<'a> {
|
||||
pub fn scroll(mut self, offset: (u16, u16)) -> Paragraph<'a> {
|
||||
self.scroll = offset;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
struct Parser<'a, T>
|
||||
where
|
||||
T: Iterator<Item = &'a str>,
|
||||
{
|
||||
text: T,
|
||||
mark: bool,
|
||||
cmd_string: String,
|
||||
style: Style,
|
||||
base_style: Style,
|
||||
escaping: bool,
|
||||
styling: bool,
|
||||
}
|
||||
|
||||
impl<'a, T> Parser<'a, T>
|
||||
where
|
||||
T: Iterator<Item = &'a str>,
|
||||
{
|
||||
fn new(text: T, base_style: Style) -> Parser<'a, T> {
|
||||
Parser {
|
||||
text: text,
|
||||
mark: false,
|
||||
cmd_string: String::from(""),
|
||||
style: base_style,
|
||||
base_style: base_style,
|
||||
escaping: false,
|
||||
styling: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_style(&mut self) {
|
||||
for cmd in self.cmd_string.split(';') {
|
||||
let args = cmd.split('=').collect::<Vec<&str>>();
|
||||
if let Some(first) = args.get(0) {
|
||||
match *first {
|
||||
"fg" => if let Some(snd) = args.get(1) {
|
||||
self.style.fg = Parser::<T>::str_to_color(snd);
|
||||
},
|
||||
"bg" => if let Some(snd) = args.get(1) {
|
||||
self.style.bg = Parser::<T>::str_to_color(snd);
|
||||
},
|
||||
"mod" => if let Some(snd) = args.get(1) {
|
||||
self.style.modifier = Parser::<T>::str_to_modifier(snd);
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn str_to_color(string: &str) -> Color {
|
||||
match string {
|
||||
"black" => Color::Black,
|
||||
"red" => Color::Red,
|
||||
"green" => Color::Green,
|
||||
"yellow" => Color::Yellow,
|
||||
"magenta" => Color::Magenta,
|
||||
"cyan" => Color::Cyan,
|
||||
"gray" => Color::Gray,
|
||||
"dark_gray" => Color::DarkGray,
|
||||
"light_red" => Color::LightRed,
|
||||
"light_green" => Color::LightGreen,
|
||||
"light_yellow" => Color::LightYellow,
|
||||
"light_magenta" => Color::LightMagenta,
|
||||
"light_cyan" => Color::LightCyan,
|
||||
"white" => Color::White,
|
||||
_ => Color::Reset,
|
||||
}
|
||||
}
|
||||
|
||||
fn str_to_modifier(string: &str) -> Modifier {
|
||||
match string {
|
||||
"bold" => Modifier::Bold,
|
||||
"italic" => Modifier::Italic,
|
||||
"underline" => Modifier::Underline,
|
||||
"invert" => Modifier::Invert,
|
||||
"crossed_out" => Modifier::CrossedOut,
|
||||
_ => Modifier::Reset,
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.styling = false;
|
||||
self.mark = false;
|
||||
self.style = self.base_style;
|
||||
self.cmd_string.clear();
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> Iterator for Parser<'a, T>
|
||||
where
|
||||
T: Iterator<Item = &'a str>,
|
||||
{
|
||||
type Item = (&'a str, Style);
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self.text.next() {
|
||||
Some(s) => if s == "\\" {
|
||||
if self.escaping {
|
||||
Some((s, self.style))
|
||||
} else {
|
||||
self.escaping = true;
|
||||
self.next()
|
||||
}
|
||||
} else if s == "{" {
|
||||
if self.escaping {
|
||||
self.escaping = false;
|
||||
Some((s, self.style))
|
||||
} else if self.mark {
|
||||
Some((s, self.style))
|
||||
} else {
|
||||
self.style = self.base_style;
|
||||
self.mark = true;
|
||||
self.next()
|
||||
}
|
||||
} else if s == "}" && self.mark {
|
||||
self.reset();
|
||||
self.next()
|
||||
} else if s == " " && self.mark {
|
||||
if self.styling {
|
||||
Some((s, self.style))
|
||||
} else {
|
||||
self.styling = true;
|
||||
self.update_style();
|
||||
self.next()
|
||||
}
|
||||
} else if self.mark && !self.styling {
|
||||
self.cmd_string.push_str(s);
|
||||
self.next()
|
||||
} else {
|
||||
Some((s, self.style))
|
||||
},
|
||||
None => None,
|
||||
}
|
||||
pub fn alignment(mut self, alignment: Alignment) -> Paragraph<'a> {
|
||||
self.alignment = alignment;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Paragraph<'a> {
|
||||
fn draw(&mut self, area: &Rect, buf: &mut Buffer) {
|
||||
let text_area = match self.block {
|
||||
Some(ref mut b) => {
|
||||
b.draw(area, buf);
|
||||
b.inner(area)
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_style(area, self.style);
|
||||
let text_area = match self.block.take() {
|
||||
Some(b) => {
|
||||
let inner_area = b.inner(area);
|
||||
b.render(area, buf);
|
||||
inner_area
|
||||
}
|
||||
None => *area,
|
||||
None => area,
|
||||
};
|
||||
|
||||
if text_area.height < 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
self.background(&text_area, buf, self.style.bg);
|
||||
let style = self.style;
|
||||
let styled = self.text.lines.iter().map(|line| {
|
||||
(
|
||||
line.spans
|
||||
.iter()
|
||||
.flat_map(|span| span.styled_graphemes(style)),
|
||||
line.alignment.unwrap_or(self.alignment),
|
||||
)
|
||||
});
|
||||
|
||||
let mut x = 0;
|
||||
let mut y = 0;
|
||||
let graphemes = UnicodeSegmentation::graphemes(self.text, true);
|
||||
let styled: Box<Iterator<Item = (&str, Style)>> = if self.raw {
|
||||
Box::new(graphemes.map(|g| (g, self.style)))
|
||||
let mut line_composer: Box<dyn LineComposer> = if let Some(Wrap { trim }) = self.wrap {
|
||||
Box::new(WordWrapper::new(styled, text_area.width, trim))
|
||||
} else {
|
||||
Box::new(Parser::new(graphemes, self.style))
|
||||
let mut line_composer = Box::new(LineTruncator::new(styled, text_area.width));
|
||||
line_composer.set_horizontal_offset(self.scroll.1);
|
||||
line_composer
|
||||
};
|
||||
|
||||
for (string, style) in styled {
|
||||
if string == "\n" {
|
||||
x = 0;
|
||||
y += 1;
|
||||
continue;
|
||||
}
|
||||
if x >= text_area.width {
|
||||
if self.wrapping {
|
||||
x = 0;
|
||||
y += 1;
|
||||
let mut y = 0;
|
||||
while let Some((current_line, current_line_width, current_line_alignment)) =
|
||||
line_composer.next_line()
|
||||
{
|
||||
if y >= self.scroll.0 {
|
||||
let mut x =
|
||||
get_line_offset(current_line_width, text_area.width, current_line_alignment);
|
||||
for StyledGrapheme { symbol, style } in current_line {
|
||||
let width = symbol.width();
|
||||
if width == 0 {
|
||||
continue;
|
||||
}
|
||||
buf.get_mut(text_area.left() + x, text_area.top() + y - self.scroll.0)
|
||||
.set_symbol(if symbol.is_empty() {
|
||||
// If the symbol is empty, the last char which rendered last time will
|
||||
// leave on the line. It's a quick fix.
|
||||
" "
|
||||
} else {
|
||||
symbol
|
||||
})
|
||||
.set_style(*style);
|
||||
x += width as u16;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if y > text_area.height + self.scroll - 1 {
|
||||
y += 1;
|
||||
if y >= text_area.height + self.scroll.0 {
|
||||
break;
|
||||
}
|
||||
|
||||
if y < self.scroll {
|
||||
continue;
|
||||
}
|
||||
|
||||
buf.get_mut(text_area.left() + x, text_area.top() + y - self.scroll)
|
||||
.set_symbol(string)
|
||||
.set_style(style);
|
||||
x += string.width() as u16;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Styled for Paragraph<'a> {
|
||||
type Item = Paragraph<'a>;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self::Item {
|
||||
self.style(style)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::{
|
||||
backend::TestBackend,
|
||||
style::Color,
|
||||
text::{Line, Span},
|
||||
widgets::Borders,
|
||||
Terminal,
|
||||
};
|
||||
|
||||
/// Tests the [`Paragraph`] widget against the expected [`Buffer`] by rendering it onto an equal
|
||||
/// area and comparing the rendered and expected content.
|
||||
/// This can be used for easy testing of varying configured paragraphs with the same expected
|
||||
/// buffer or any other test case really.
|
||||
fn test_case(paragraph: &Paragraph, expected: Buffer) {
|
||||
let backend = TestBackend::new(expected.area.width, expected.area.height);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
f.render_widget(paragraph.clone(), size);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_width_char_at_end_of_line() {
|
||||
let line = "foo\0";
|
||||
let paragraphs = vec![
|
||||
Paragraph::new(line),
|
||||
Paragraph::new(line).wrap(Wrap { trim: false }),
|
||||
Paragraph::new(line).wrap(Wrap { trim: true }),
|
||||
];
|
||||
|
||||
for paragraph in paragraphs {
|
||||
test_case(¶graph, Buffer::with_lines(vec!["foo"]));
|
||||
test_case(¶graph, Buffer::with_lines(vec!["foo "]));
|
||||
test_case(¶graph, Buffer::with_lines(vec!["foo ", " "]));
|
||||
test_case(¶graph, Buffer::with_lines(vec!["foo", " "]));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_empty_paragraph() {
|
||||
let paragraphs = vec![
|
||||
Paragraph::new(""),
|
||||
Paragraph::new("").wrap(Wrap { trim: false }),
|
||||
Paragraph::new("").wrap(Wrap { trim: true }),
|
||||
];
|
||||
|
||||
for paragraph in paragraphs {
|
||||
test_case(¶graph, Buffer::with_lines(vec![" "]));
|
||||
test_case(¶graph, Buffer::with_lines(vec![" "]));
|
||||
test_case(¶graph, Buffer::with_lines(vec![" "; 10]));
|
||||
test_case(¶graph, Buffer::with_lines(vec![" ", " "]));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_single_line_paragraph() {
|
||||
let text = "Hello, world!";
|
||||
let truncated_paragraph = Paragraph::new(text);
|
||||
let wrapped_paragraph = Paragraph::new(text).wrap(Wrap { trim: false });
|
||||
let trimmed_paragraph = Paragraph::new(text).wrap(Wrap { trim: true });
|
||||
|
||||
let paragraphs = vec![&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph];
|
||||
|
||||
for paragraph in paragraphs {
|
||||
test_case(paragraph, Buffer::with_lines(vec!["Hello, world! "]));
|
||||
test_case(paragraph, Buffer::with_lines(vec!["Hello, world!"]));
|
||||
test_case(
|
||||
paragraph,
|
||||
Buffer::with_lines(vec!["Hello, world! ", " "]),
|
||||
);
|
||||
test_case(
|
||||
paragraph,
|
||||
Buffer::with_lines(vec!["Hello, world!", " "]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_multi_line_paragraph() {
|
||||
let text = "This is a\nmultiline\nparagraph.";
|
||||
|
||||
let paragraphs = vec![
|
||||
Paragraph::new(text),
|
||||
Paragraph::new(text).wrap(Wrap { trim: false }),
|
||||
Paragraph::new(text).wrap(Wrap { trim: true }),
|
||||
];
|
||||
|
||||
for paragraph in paragraphs {
|
||||
test_case(
|
||||
¶graph,
|
||||
Buffer::with_lines(vec!["This is a ", "multiline ", "paragraph."]),
|
||||
);
|
||||
test_case(
|
||||
¶graph,
|
||||
Buffer::with_lines(vec![
|
||||
"This is a ",
|
||||
"multiline ",
|
||||
"paragraph. ",
|
||||
]),
|
||||
);
|
||||
test_case(
|
||||
¶graph,
|
||||
Buffer::with_lines(vec![
|
||||
"This is a ",
|
||||
"multiline ",
|
||||
"paragraph. ",
|
||||
" ",
|
||||
" ",
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_paragraph_with_block() {
|
||||
// We use the slightly unconventional "worlds" instead of "world" here to make sure when we
|
||||
// can truncate this without triggering the typos linter.
|
||||
let text = "Hello, worlds!";
|
||||
let truncated_paragraph =
|
||||
Paragraph::new(text).block(Block::default().title("Title").borders(Borders::ALL));
|
||||
let wrapped_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: false });
|
||||
let trimmed_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: true });
|
||||
|
||||
let paragraphs = vec![&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph];
|
||||
|
||||
for paragraph in paragraphs {
|
||||
test_case(
|
||||
paragraph,
|
||||
Buffer::with_lines(vec![
|
||||
"┌Title─────────┐",
|
||||
"│Hello, worlds!│",
|
||||
"└──────────────┘",
|
||||
]),
|
||||
);
|
||||
test_case(
|
||||
paragraph,
|
||||
Buffer::with_lines(vec![
|
||||
"┌Title───────────┐",
|
||||
"│Hello, worlds! │",
|
||||
"└────────────────┘",
|
||||
]),
|
||||
);
|
||||
test_case(
|
||||
paragraph,
|
||||
Buffer::with_lines(vec![
|
||||
"┌Title────────────┐",
|
||||
"│Hello, worlds! │",
|
||||
"│ │",
|
||||
"└─────────────────┘",
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
test_case(
|
||||
&truncated_paragraph,
|
||||
Buffer::with_lines(vec![
|
||||
"┌Title───────┐",
|
||||
"│Hello, world│",
|
||||
"│ │",
|
||||
"└────────────┘",
|
||||
]),
|
||||
);
|
||||
test_case(
|
||||
&wrapped_paragraph,
|
||||
Buffer::with_lines(vec![
|
||||
"┌Title──────┐",
|
||||
"│Hello, │",
|
||||
"│worlds! │",
|
||||
"└───────────┘",
|
||||
]),
|
||||
);
|
||||
test_case(
|
||||
&trimmed_paragraph,
|
||||
Buffer::with_lines(vec![
|
||||
"┌Title──────┐",
|
||||
"│Hello, │",
|
||||
"│worlds! │",
|
||||
"└───────────┘",
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_paragraph_with_word_wrap() {
|
||||
let text = "This is a long line of text that should wrap and contains a superultramegagigalong word.";
|
||||
let wrapped_paragraph = Paragraph::new(text).wrap(Wrap { trim: false });
|
||||
let trimmed_paragraph = Paragraph::new(text).wrap(Wrap { trim: true });
|
||||
|
||||
test_case(
|
||||
&wrapped_paragraph,
|
||||
Buffer::with_lines(vec![
|
||||
"This is a long line",
|
||||
"of text that should",
|
||||
"wrap and ",
|
||||
"contains a ",
|
||||
"superultramegagigal",
|
||||
"ong word. ",
|
||||
]),
|
||||
);
|
||||
test_case(
|
||||
&wrapped_paragraph,
|
||||
Buffer::with_lines(vec![
|
||||
"This is a ",
|
||||
"long line of",
|
||||
"text that ",
|
||||
"should wrap ",
|
||||
" and ",
|
||||
"contains a ",
|
||||
"superultrame",
|
||||
"gagigalong ",
|
||||
"word. ",
|
||||
]),
|
||||
);
|
||||
|
||||
test_case(
|
||||
&trimmed_paragraph,
|
||||
Buffer::with_lines(vec![
|
||||
"This is a long line",
|
||||
"of text that should",
|
||||
"wrap and ",
|
||||
"contains a ",
|
||||
"superultramegagigal",
|
||||
"ong word. ",
|
||||
]),
|
||||
);
|
||||
test_case(
|
||||
&trimmed_paragraph,
|
||||
Buffer::with_lines(vec![
|
||||
"This is a ",
|
||||
"long line of",
|
||||
"text that ",
|
||||
"should wrap ",
|
||||
"and contains",
|
||||
"a ",
|
||||
"superultrame",
|
||||
"gagigalong ",
|
||||
"word. ",
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_paragraph_with_line_truncation() {
|
||||
let text = "This is a long line of text that should be truncated.";
|
||||
let truncated_paragraph = Paragraph::new(text);
|
||||
|
||||
test_case(
|
||||
&truncated_paragraph,
|
||||
Buffer::with_lines(vec!["This is a long line of"]),
|
||||
);
|
||||
test_case(
|
||||
&truncated_paragraph,
|
||||
Buffer::with_lines(vec!["This is a long line of te"]),
|
||||
);
|
||||
test_case(
|
||||
&truncated_paragraph,
|
||||
Buffer::with_lines(vec!["This is a long line of "]),
|
||||
);
|
||||
test_case(
|
||||
&truncated_paragraph.clone().scroll((0, 2)),
|
||||
Buffer::with_lines(vec!["is is a long line of te"]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_paragraph_with_left_alignment() {
|
||||
let text = "Hello, world!";
|
||||
let truncated_paragraph = Paragraph::new(text).alignment(Alignment::Left);
|
||||
let wrapped_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: false });
|
||||
let trimmed_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: true });
|
||||
|
||||
let paragraphs = vec![&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph];
|
||||
|
||||
for paragraph in paragraphs {
|
||||
test_case(paragraph, Buffer::with_lines(vec!["Hello, world! "]));
|
||||
test_case(paragraph, Buffer::with_lines(vec!["Hello, world!"]));
|
||||
}
|
||||
|
||||
test_case(&truncated_paragraph, Buffer::with_lines(vec!["Hello, wor"]));
|
||||
test_case(
|
||||
&wrapped_paragraph,
|
||||
Buffer::with_lines(vec!["Hello, ", "world! "]),
|
||||
);
|
||||
test_case(
|
||||
&trimmed_paragraph,
|
||||
Buffer::with_lines(vec!["Hello, ", "world! "]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_paragraph_with_center_alignment() {
|
||||
let text = "Hello, world!";
|
||||
let truncated_paragraph = Paragraph::new(text).alignment(Alignment::Center);
|
||||
let wrapped_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: false });
|
||||
let trimmed_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: true });
|
||||
|
||||
let paragraphs = vec![&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph];
|
||||
|
||||
for paragraph in paragraphs {
|
||||
test_case(paragraph, Buffer::with_lines(vec![" Hello, world! "]));
|
||||
test_case(paragraph, Buffer::with_lines(vec![" Hello, world! "]));
|
||||
test_case(paragraph, Buffer::with_lines(vec![" Hello, world! "]));
|
||||
test_case(paragraph, Buffer::with_lines(vec!["Hello, world!"]));
|
||||
}
|
||||
|
||||
test_case(&truncated_paragraph, Buffer::with_lines(vec!["Hello, wor"]));
|
||||
test_case(
|
||||
&wrapped_paragraph,
|
||||
Buffer::with_lines(vec![" Hello, ", " world! "]),
|
||||
);
|
||||
test_case(
|
||||
&trimmed_paragraph,
|
||||
Buffer::with_lines(vec![" Hello, ", " world! "]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_paragraph_with_right_alignment() {
|
||||
let text = "Hello, world!";
|
||||
let truncated_paragraph = Paragraph::new(text).alignment(Alignment::Right);
|
||||
let wrapped_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: false });
|
||||
let trimmed_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: true });
|
||||
|
||||
let paragraphs = vec![&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph];
|
||||
|
||||
for paragraph in paragraphs {
|
||||
test_case(paragraph, Buffer::with_lines(vec![" Hello, world!"]));
|
||||
test_case(paragraph, Buffer::with_lines(vec!["Hello, world!"]));
|
||||
}
|
||||
|
||||
test_case(&truncated_paragraph, Buffer::with_lines(vec!["Hello, wor"]));
|
||||
test_case(
|
||||
&wrapped_paragraph,
|
||||
Buffer::with_lines(vec![" Hello,", " world!"]),
|
||||
);
|
||||
test_case(
|
||||
&trimmed_paragraph,
|
||||
Buffer::with_lines(vec![" Hello,", " world!"]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_paragraph_with_scroll_offset() {
|
||||
let text = "This is a\ncool\nmultiline\nparagraph.";
|
||||
let truncated_paragraph = Paragraph::new(text).scroll((2, 0));
|
||||
let wrapped_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: false });
|
||||
let trimmed_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: true });
|
||||
|
||||
let paragraphs = vec![&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph];
|
||||
|
||||
for paragraph in paragraphs {
|
||||
test_case(
|
||||
paragraph,
|
||||
Buffer::with_lines(vec!["multiline ", "paragraph. ", " "]),
|
||||
);
|
||||
test_case(paragraph, Buffer::with_lines(vec!["multiline "]));
|
||||
}
|
||||
|
||||
test_case(
|
||||
&truncated_paragraph.clone().scroll((2, 4)),
|
||||
Buffer::with_lines(vec!["iline ", "graph. "]),
|
||||
);
|
||||
test_case(
|
||||
&wrapped_paragraph,
|
||||
Buffer::with_lines(vec!["cool ", "multili", "ne "]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_paragraph_with_zero_width_area() {
|
||||
let text = "Hello, world!";
|
||||
|
||||
let paragraphs = vec![
|
||||
Paragraph::new(text),
|
||||
Paragraph::new(text).wrap(Wrap { trim: false }),
|
||||
Paragraph::new(text).wrap(Wrap { trim: true }),
|
||||
];
|
||||
|
||||
let area = Rect::new(0, 0, 0, 3);
|
||||
for paragraph in paragraphs {
|
||||
test_case(¶graph, Buffer::empty(area));
|
||||
test_case(¶graph.clone().scroll((2, 4)), Buffer::empty(area));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_paragraph_with_zero_height_area() {
|
||||
let text = "Hello, world!";
|
||||
|
||||
let paragraphs = vec![
|
||||
Paragraph::new(text),
|
||||
Paragraph::new(text).wrap(Wrap { trim: false }),
|
||||
Paragraph::new(text).wrap(Wrap { trim: true }),
|
||||
];
|
||||
|
||||
let area = Rect::new(0, 0, 10, 0);
|
||||
for paragraph in paragraphs {
|
||||
test_case(¶graph, Buffer::empty(area));
|
||||
test_case(¶graph.clone().scroll((2, 4)), Buffer::empty(area));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_paragraph_with_styled_text() {
|
||||
let text = Line::from(vec![
|
||||
Span::styled("Hello, ", Style::default().fg(Color::Red)),
|
||||
Span::styled("world!", Style::default().fg(Color::Blue)),
|
||||
]);
|
||||
|
||||
let paragraphs = vec![
|
||||
Paragraph::new(text.clone()),
|
||||
Paragraph::new(text.clone()).wrap(Wrap { trim: false }),
|
||||
Paragraph::new(text.clone()).wrap(Wrap { trim: true }),
|
||||
];
|
||||
|
||||
let mut expected_buffer = Buffer::with_lines(vec!["Hello, world!"]);
|
||||
expected_buffer.set_style(
|
||||
Rect::new(0, 0, 7, 1),
|
||||
Style::default().fg(Color::Red).bg(Color::Green),
|
||||
);
|
||||
expected_buffer.set_style(
|
||||
Rect::new(7, 0, 6, 1),
|
||||
Style::default().fg(Color::Blue).bg(Color::Green),
|
||||
);
|
||||
for paragraph in paragraphs {
|
||||
test_case(
|
||||
¶graph.style(Style::default().bg(Color::Green)),
|
||||
expected_buffer.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_paragraph_with_special_characters() {
|
||||
let text = "Hello, <world>!";
|
||||
let paragraphs = vec![
|
||||
Paragraph::new(text),
|
||||
Paragraph::new(text).wrap(Wrap { trim: false }),
|
||||
Paragraph::new(text).wrap(Wrap { trim: true }),
|
||||
];
|
||||
|
||||
for paragraph in paragraphs {
|
||||
test_case(¶graph, Buffer::with_lines(vec!["Hello, <world>!"]));
|
||||
test_case(¶graph, Buffer::with_lines(vec!["Hello, <world>! "]));
|
||||
test_case(
|
||||
¶graph,
|
||||
Buffer::with_lines(vec!["Hello, <world>! ", " "]),
|
||||
);
|
||||
test_case(
|
||||
¶graph,
|
||||
Buffer::with_lines(vec!["Hello, <world>!", " "]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_paragraph_with_unicode_characters() {
|
||||
let text = "こんにちは, 世界! 😃";
|
||||
let truncated_paragraph = Paragraph::new(text);
|
||||
let wrapped_paragraph = Paragraph::new(text).wrap(Wrap { trim: false });
|
||||
let trimmed_paragraph = Paragraph::new(text).wrap(Wrap { trim: true });
|
||||
|
||||
let paragraphs = vec![&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph];
|
||||
|
||||
for paragraph in paragraphs {
|
||||
test_case(paragraph, Buffer::with_lines(vec!["こんにちは, 世界! 😃"]));
|
||||
test_case(
|
||||
paragraph,
|
||||
Buffer::with_lines(vec!["こんにちは, 世界! 😃 "]),
|
||||
);
|
||||
}
|
||||
|
||||
test_case(
|
||||
&truncated_paragraph,
|
||||
Buffer::with_lines(vec!["こんにちは, 世 "]),
|
||||
);
|
||||
test_case(
|
||||
&wrapped_paragraph,
|
||||
Buffer::with_lines(vec!["こんにちは, ", "世界! 😃 "]),
|
||||
);
|
||||
test_case(
|
||||
&trimmed_paragraph,
|
||||
Buffer::with_lines(vec!["こんにちは, ", "世界! 😃 "]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
690
src/widgets/reflow.rs
Normal file
690
src/widgets/reflow.rs
Normal file
@@ -0,0 +1,690 @@
|
||||
use std::{collections::VecDeque, vec::IntoIter};
|
||||
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{layout::Alignment, text::StyledGrapheme};
|
||||
|
||||
const NBSP: &str = "\u{00a0}";
|
||||
|
||||
/// A state machine to pack styled symbols into lines.
|
||||
/// Cannot implement it as Iterator since it yields slices of the internal buffer (need streaming
|
||||
/// iterators for that).
|
||||
pub trait LineComposer<'a> {
|
||||
fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16, Alignment)>;
|
||||
}
|
||||
|
||||
/// A state machine that wraps lines on word boundaries.
|
||||
pub struct WordWrapper<'a, O, I>
|
||||
where
|
||||
// Outer iterator providing the individual lines
|
||||
O: Iterator<Item = (I, Alignment)>,
|
||||
// Inner iterator providing the styled symbols of a line Each line consists of an alignment and
|
||||
// a series of symbols
|
||||
I: Iterator<Item = StyledGrapheme<'a>>,
|
||||
{
|
||||
/// The given, unprocessed lines
|
||||
input_lines: O,
|
||||
max_line_width: u16,
|
||||
wrapped_lines: Option<IntoIter<Vec<StyledGrapheme<'a>>>>,
|
||||
current_alignment: Alignment,
|
||||
current_line: Vec<StyledGrapheme<'a>>,
|
||||
/// Removes the leading whitespace from lines
|
||||
trim: bool,
|
||||
}
|
||||
|
||||
impl<'a, O, I> WordWrapper<'a, O, I>
|
||||
where
|
||||
O: Iterator<Item = (I, Alignment)>,
|
||||
I: Iterator<Item = StyledGrapheme<'a>>,
|
||||
{
|
||||
pub fn new(lines: O, max_line_width: u16, trim: bool) -> WordWrapper<'a, O, I> {
|
||||
WordWrapper {
|
||||
input_lines: lines,
|
||||
max_line_width,
|
||||
wrapped_lines: None,
|
||||
current_alignment: Alignment::Left,
|
||||
current_line: vec![],
|
||||
trim,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, O, I> LineComposer<'a> for WordWrapper<'a, O, I>
|
||||
where
|
||||
O: Iterator<Item = (I, Alignment)>,
|
||||
I: Iterator<Item = StyledGrapheme<'a>>,
|
||||
{
|
||||
fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16, Alignment)> {
|
||||
if self.max_line_width == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut current_line: Option<Vec<StyledGrapheme<'a>>> = None;
|
||||
let mut line_width: u16 = 0;
|
||||
|
||||
// Try to repeatedly retrieve next line
|
||||
while current_line.is_none() {
|
||||
// Retrieve next preprocessed wrapped line
|
||||
if let Some(line_iterator) = &mut self.wrapped_lines {
|
||||
if let Some(line) = line_iterator.next() {
|
||||
line_width = line
|
||||
.iter()
|
||||
.map(|grapheme| grapheme.symbol.width())
|
||||
.sum::<usize>() as u16;
|
||||
current_line = Some(line);
|
||||
}
|
||||
}
|
||||
|
||||
// When no more preprocessed wrapped lines
|
||||
if current_line.is_none() {
|
||||
// Try to calculate next wrapped lines based on current whole line
|
||||
if let Some((line_symbols, line_alignment)) = &mut self.input_lines.next() {
|
||||
// Save the whole line's alignment
|
||||
self.current_alignment = *line_alignment;
|
||||
let mut wrapped_lines = vec![]; // Saves the wrapped lines
|
||||
// Saves the unfinished wrapped line
|
||||
let (mut current_line, mut current_line_width) = (vec![], 0);
|
||||
// Saves the partially processed word
|
||||
let (mut unfinished_word, mut word_width) = (vec![], 0);
|
||||
// Saves the whitespaces of the partially unfinished word
|
||||
let (mut unfinished_whitespaces, mut whitespace_width) =
|
||||
(VecDeque::<StyledGrapheme>::new(), 0);
|
||||
|
||||
let mut has_seen_non_whitespace = false;
|
||||
for StyledGrapheme { symbol, style } in line_symbols {
|
||||
let symbol_whitespace =
|
||||
symbol.chars().all(&char::is_whitespace) && symbol != NBSP;
|
||||
let symbol_width = symbol.width() as u16;
|
||||
// Ignore characters wider than the total max width
|
||||
if symbol_width > self.max_line_width {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Append finished word to current line
|
||||
if has_seen_non_whitespace && symbol_whitespace
|
||||
// Append if trimmed (whitespaces removed) word would overflow
|
||||
|| word_width + symbol_width > self.max_line_width && current_line.is_empty() && self.trim
|
||||
// Append if removed whitespace would overflow -> reset whitespace counting to prevent overflow
|
||||
|| whitespace_width + symbol_width > self.max_line_width && current_line.is_empty() && self.trim
|
||||
// Append if complete word would overflow
|
||||
|| word_width + whitespace_width + symbol_width > self.max_line_width && current_line.is_empty() && !self.trim
|
||||
{
|
||||
if !current_line.is_empty() || !self.trim {
|
||||
// Also append whitespaces if not trimming or current line is not
|
||||
// empty
|
||||
current_line.extend(
|
||||
std::mem::take(&mut unfinished_whitespaces).into_iter(),
|
||||
);
|
||||
current_line_width += whitespace_width;
|
||||
}
|
||||
// Append trimmed word
|
||||
current_line.append(&mut unfinished_word);
|
||||
current_line_width += word_width;
|
||||
|
||||
// Clear whitespace buffer
|
||||
unfinished_whitespaces.clear();
|
||||
whitespace_width = 0;
|
||||
word_width = 0;
|
||||
}
|
||||
|
||||
// Append the unfinished wrapped line to wrapped lines if it is as wide as
|
||||
// max line width
|
||||
if current_line_width >= self.max_line_width
|
||||
// or if it would be too long with the current partially processed word added
|
||||
|| current_line_width + whitespace_width + word_width >= self.max_line_width && symbol_width > 0
|
||||
{
|
||||
let mut remaining_width =
|
||||
(self.max_line_width as i32 - current_line_width as i32).max(0)
|
||||
as u16;
|
||||
wrapped_lines.push(std::mem::take(&mut current_line));
|
||||
current_line_width = 0;
|
||||
|
||||
// Remove all whitespaces till end of just appended wrapped line + next
|
||||
// whitespace
|
||||
let mut first_whitespace = unfinished_whitespaces.pop_front();
|
||||
while let Some(grapheme) = first_whitespace.as_ref() {
|
||||
let symbol_width = grapheme.symbol.width() as u16;
|
||||
whitespace_width -= symbol_width;
|
||||
|
||||
if symbol_width > remaining_width {
|
||||
break;
|
||||
}
|
||||
remaining_width -= symbol_width;
|
||||
first_whitespace = unfinished_whitespaces.pop_front();
|
||||
}
|
||||
// In case all whitespaces have been exhausted
|
||||
if symbol_whitespace && first_whitespace.is_none() {
|
||||
// Prevent first whitespace to count towards next word
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Append symbol to unfinished, partially processed word
|
||||
if symbol_whitespace {
|
||||
whitespace_width += symbol_width;
|
||||
unfinished_whitespaces.push_back(StyledGrapheme { symbol, style });
|
||||
} else {
|
||||
word_width += symbol_width;
|
||||
unfinished_word.push(StyledGrapheme { symbol, style });
|
||||
}
|
||||
|
||||
has_seen_non_whitespace = !symbol_whitespace;
|
||||
}
|
||||
|
||||
// Append remaining text parts
|
||||
if !unfinished_word.is_empty() || !unfinished_whitespaces.is_empty() {
|
||||
if current_line.is_empty() && unfinished_word.is_empty() {
|
||||
wrapped_lines.push(vec![]);
|
||||
} else if !self.trim || !current_line.is_empty() {
|
||||
current_line.extend(unfinished_whitespaces.into_iter());
|
||||
}
|
||||
current_line.append(&mut unfinished_word);
|
||||
}
|
||||
if !current_line.is_empty() {
|
||||
wrapped_lines.push(current_line);
|
||||
}
|
||||
if wrapped_lines.is_empty() {
|
||||
// Append empty line if there was nothing to wrap in the first place
|
||||
wrapped_lines.push(vec![]);
|
||||
}
|
||||
|
||||
self.wrapped_lines = Some(wrapped_lines.into_iter());
|
||||
} else {
|
||||
// No more whole lines available -> stop repeatedly retrieving next wrapped line
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(line) = current_line {
|
||||
self.current_line = line;
|
||||
Some((&self.current_line[..], line_width, self.current_alignment))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A state machine that truncates overhanging lines.
|
||||
pub struct LineTruncator<'a, O, I>
|
||||
where
|
||||
// Outer iterator providing the individual lines
|
||||
O: Iterator<Item = (I, Alignment)>,
|
||||
// Inner iterator providing the styled symbols of a line Each line consists of an alignment and
|
||||
// a series of symbols
|
||||
I: Iterator<Item = StyledGrapheme<'a>>,
|
||||
{
|
||||
/// The given, unprocessed lines
|
||||
input_lines: O,
|
||||
max_line_width: u16,
|
||||
current_line: Vec<StyledGrapheme<'a>>,
|
||||
/// Record the offset to skip render
|
||||
horizontal_offset: u16,
|
||||
}
|
||||
|
||||
impl<'a, O, I> LineTruncator<'a, O, I>
|
||||
where
|
||||
O: Iterator<Item = (I, Alignment)>,
|
||||
I: Iterator<Item = StyledGrapheme<'a>>,
|
||||
{
|
||||
pub fn new(lines: O, max_line_width: u16) -> LineTruncator<'a, O, I> {
|
||||
LineTruncator {
|
||||
input_lines: lines,
|
||||
max_line_width,
|
||||
horizontal_offset: 0,
|
||||
current_line: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_horizontal_offset(&mut self, horizontal_offset: u16) {
|
||||
self.horizontal_offset = horizontal_offset;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, O, I> LineComposer<'a> for LineTruncator<'a, O, I>
|
||||
where
|
||||
O: Iterator<Item = (I, Alignment)>,
|
||||
I: Iterator<Item = StyledGrapheme<'a>>,
|
||||
{
|
||||
fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16, Alignment)> {
|
||||
if self.max_line_width == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.current_line.truncate(0);
|
||||
let mut current_line_width = 0;
|
||||
|
||||
let mut lines_exhausted = true;
|
||||
let mut horizontal_offset = self.horizontal_offset as usize;
|
||||
let mut current_alignment = Alignment::Left;
|
||||
if let Some((current_line, alignment)) = &mut self.input_lines.next() {
|
||||
lines_exhausted = false;
|
||||
current_alignment = *alignment;
|
||||
|
||||
for StyledGrapheme { symbol, style } in current_line {
|
||||
// Ignore characters wider that the total max width.
|
||||
if symbol.width() as u16 > self.max_line_width {
|
||||
continue;
|
||||
}
|
||||
|
||||
if current_line_width + symbol.width() as u16 > self.max_line_width {
|
||||
// Truncate line
|
||||
break;
|
||||
}
|
||||
|
||||
let symbol = if horizontal_offset == 0 || Alignment::Left != *alignment {
|
||||
symbol
|
||||
} else {
|
||||
let w = symbol.width();
|
||||
if w > horizontal_offset {
|
||||
let t = trim_offset(symbol, horizontal_offset);
|
||||
horizontal_offset = 0;
|
||||
t
|
||||
} else {
|
||||
horizontal_offset -= w;
|
||||
""
|
||||
}
|
||||
};
|
||||
current_line_width += symbol.width() as u16;
|
||||
self.current_line.push(StyledGrapheme { symbol, style });
|
||||
}
|
||||
}
|
||||
|
||||
if lines_exhausted {
|
||||
None
|
||||
} else {
|
||||
Some((
|
||||
&self.current_line[..],
|
||||
current_line_width,
|
||||
current_alignment,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This function will return a str slice which start at specified offset.
|
||||
/// As src is a unicode str, start offset has to be calculated with each character.
|
||||
fn trim_offset(src: &str, mut offset: usize) -> &str {
|
||||
let mut start = 0;
|
||||
for c in UnicodeSegmentation::graphemes(src, true) {
|
||||
let w = c.width();
|
||||
if w <= offset {
|
||||
offset -= w;
|
||||
start += c.len();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
&src[start..]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
style::Style,
|
||||
text::{Line, Text},
|
||||
};
|
||||
|
||||
enum Composer {
|
||||
WordWrapper { trim: bool },
|
||||
LineTruncator,
|
||||
}
|
||||
|
||||
fn run_composer<'a>(
|
||||
which: Composer,
|
||||
text: impl Into<Text<'a>>,
|
||||
text_area_width: u16,
|
||||
) -> (Vec<String>, Vec<u16>, Vec<Alignment>) {
|
||||
let text = text.into();
|
||||
let styled_lines = text.lines.iter().map(|line| {
|
||||
(
|
||||
line.spans
|
||||
.iter()
|
||||
.flat_map(|span| span.styled_graphemes(Style::default())),
|
||||
line.alignment.unwrap_or(Alignment::Left),
|
||||
)
|
||||
});
|
||||
|
||||
let mut composer: Box<dyn LineComposer> = match which {
|
||||
Composer::WordWrapper { trim } => {
|
||||
Box::new(WordWrapper::new(styled_lines, text_area_width, trim))
|
||||
}
|
||||
Composer::LineTruncator => Box::new(LineTruncator::new(styled_lines, text_area_width)),
|
||||
};
|
||||
let mut lines = vec![];
|
||||
let mut widths = vec![];
|
||||
let mut alignments = vec![];
|
||||
while let Some((styled, width, alignment)) = composer.next_line() {
|
||||
let line = styled
|
||||
.iter()
|
||||
.map(|StyledGrapheme { symbol, .. }| *symbol)
|
||||
.collect::<String>();
|
||||
assert!(width <= text_area_width);
|
||||
lines.push(line);
|
||||
widths.push(width);
|
||||
alignments.push(alignment);
|
||||
}
|
||||
(lines, widths, alignments)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_one_line() {
|
||||
let width = 40;
|
||||
for i in 1..width {
|
||||
let text = "a".repeat(i);
|
||||
let (word_wrapper, _, _) = run_composer(
|
||||
Composer::WordWrapper { trim: true },
|
||||
&text[..],
|
||||
width as u16,
|
||||
);
|
||||
let (line_truncator, _, _) =
|
||||
run_composer(Composer::LineTruncator, &text[..], width as u16);
|
||||
let expected = vec![text];
|
||||
assert_eq!(word_wrapper, expected);
|
||||
assert_eq!(line_truncator, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_short_lines() {
|
||||
let width = 20;
|
||||
let text =
|
||||
"abcdefg\nhijklmno\npabcdefg\nhijklmn\nopabcdefghijk\nlmnopabcd\n\n\nefghijklmno";
|
||||
let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
|
||||
let wrapped: Vec<&str> = text.split('\n').collect();
|
||||
assert_eq!(word_wrapper, wrapped);
|
||||
assert_eq!(line_truncator, wrapped);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_long_word() {
|
||||
let width = 20;
|
||||
let text = "abcdefghijklmnopabcdefghijklmnopabcdefghijklmnopabcdefghijklmno";
|
||||
let (word_wrapper, _, _) =
|
||||
run_composer(Composer::WordWrapper { trim: true }, text, width as u16);
|
||||
let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width as u16);
|
||||
|
||||
let wrapped = vec![
|
||||
&text[..width],
|
||||
&text[width..width * 2],
|
||||
&text[width * 2..width * 3],
|
||||
&text[width * 3..],
|
||||
];
|
||||
assert_eq!(
|
||||
word_wrapper, wrapped,
|
||||
"WordWrapper should detect the line cannot be broken on word boundary and \
|
||||
break it at line width limit."
|
||||
);
|
||||
assert_eq!(line_truncator, vec![&text[..width]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_long_sentence() {
|
||||
let width = 20;
|
||||
let text =
|
||||
"abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab c d e f g h i j k l m n o";
|
||||
let text_multi_space =
|
||||
"abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab c d e f g h i j k l \
|
||||
m n o";
|
||||
let (word_wrapper_single_space, _, _) =
|
||||
run_composer(Composer::WordWrapper { trim: true }, text, width as u16);
|
||||
let (word_wrapper_multi_space, _, _) = run_composer(
|
||||
Composer::WordWrapper { trim: true },
|
||||
text_multi_space,
|
||||
width as u16,
|
||||
);
|
||||
let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width as u16);
|
||||
|
||||
let word_wrapped = vec![
|
||||
"abcd efghij",
|
||||
"klmnopabcd efgh",
|
||||
"ijklmnopabcdefg",
|
||||
"hijkl mnopab c d e f",
|
||||
"g h i j k l m n o",
|
||||
];
|
||||
assert_eq!(word_wrapper_single_space, word_wrapped);
|
||||
assert_eq!(word_wrapper_multi_space, word_wrapped);
|
||||
|
||||
assert_eq!(line_truncator, vec![&text[..width]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_zero_width() {
|
||||
let width = 0;
|
||||
let text = "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab ";
|
||||
let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
|
||||
let expected: Vec<&str> = Vec::new();
|
||||
assert_eq!(word_wrapper, expected);
|
||||
assert_eq!(line_truncator, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_max_line_width_of_1() {
|
||||
let width = 1;
|
||||
let text = "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab ";
|
||||
let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
|
||||
let expected: Vec<&str> = UnicodeSegmentation::graphemes(text, true)
|
||||
.filter(|g| g.chars().any(|c| !c.is_whitespace()))
|
||||
.collect();
|
||||
assert_eq!(word_wrapper, expected);
|
||||
assert_eq!(line_truncator, vec!["a"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_max_line_width_of_1_double_width_characters() {
|
||||
let width = 1;
|
||||
let text =
|
||||
"コンピュータ上で文字を扱う場合、典型的には文字\naaa\naによる通信を行う場合にその\
|
||||
両端点では、";
|
||||
let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
assert_eq!(word_wrapper, vec!["", "a", "a", "a", "a"]);
|
||||
assert_eq!(line_truncator, vec!["", "a", "a"]);
|
||||
}
|
||||
|
||||
/// Tests `WordWrapper` with words some of which exceed line length and some not.
|
||||
#[test]
|
||||
fn line_composer_word_wrapper_mixed_length() {
|
||||
let width = 20;
|
||||
let text = "abcd efghij klmnopabcdefghijklmnopabcdefghijkl mnopab cdefghi j klmno";
|
||||
let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
assert_eq!(
|
||||
word_wrapper,
|
||||
vec![
|
||||
"abcd efghij",
|
||||
"klmnopabcdefghijklmn",
|
||||
"opabcdefghijkl",
|
||||
"mnopab cdefghi j",
|
||||
"klmno",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_double_width_chars() {
|
||||
let width = 20;
|
||||
let text = "コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点\
|
||||
では、";
|
||||
let (word_wrapper, word_wrapper_width, _) =
|
||||
run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
assert_eq!(line_truncator, vec!["コンピュータ上で文字"]);
|
||||
let wrapped = vec![
|
||||
"コンピュータ上で文字",
|
||||
"を扱う場合、典型的に",
|
||||
"は文字による通信を行",
|
||||
"う場合にその両端点で",
|
||||
"は、",
|
||||
];
|
||||
assert_eq!(word_wrapper, wrapped);
|
||||
assert_eq!(word_wrapper_width, vec![width, width, width, width, 4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_leading_whitespace_removal() {
|
||||
let width = 20;
|
||||
let text = "AAAAAAAAAAAAAAAAAAAA AAA";
|
||||
let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAAAAAAA", "AAA",]);
|
||||
assert_eq!(line_truncator, vec!["AAAAAAAAAAAAAAAAAAAA"]);
|
||||
}
|
||||
|
||||
/// Tests truncation of leading whitespace.
|
||||
#[test]
|
||||
fn line_composer_lots_of_spaces() {
|
||||
let width = 20;
|
||||
let text = " ";
|
||||
let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
assert_eq!(word_wrapper, vec![""]);
|
||||
assert_eq!(line_truncator, vec![" "]);
|
||||
}
|
||||
|
||||
/// Tests an input starting with a letter, followed by spaces - some of the behaviour is
|
||||
/// incidental.
|
||||
#[test]
|
||||
fn line_composer_char_plus_lots_of_spaces() {
|
||||
let width = 20;
|
||||
let text = "a ";
|
||||
let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
// What's happening below is: the first line gets consumed, trailing spaces discarded,
|
||||
// after 20 of which a word break occurs (probably shouldn't). The second line break
|
||||
// discards all whitespace. The result should probably be vec!["a"] but it doesn't matter
|
||||
// that much.
|
||||
assert_eq!(word_wrapper, vec!["a", ""]);
|
||||
assert_eq!(line_truncator, vec!["a "]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_word_wrapper_double_width_chars_mixed_with_spaces() {
|
||||
let width = 20;
|
||||
// Japanese seems not to use spaces but we should break on spaces anyway... We're using it
|
||||
// to test double-width chars.
|
||||
// You are more than welcome to add word boundary detection based of alterations of
|
||||
// hiragana and katakana...
|
||||
// This happens to also be a test case for mixed width because regular spaces are single
|
||||
// width.
|
||||
let text = "コンピュ ータ上で文字を扱う場合、 典型的には文 字による 通信を行 う場合にその両端点では、";
|
||||
let (word_wrapper, word_wrapper_width, _) =
|
||||
run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
assert_eq!(
|
||||
word_wrapper,
|
||||
vec![
|
||||
"コンピュ",
|
||||
"ータ上で文字を扱う場",
|
||||
"合、 典型的には文",
|
||||
"字による 通信を行",
|
||||
"う場合にその両端点で",
|
||||
"は、",
|
||||
]
|
||||
);
|
||||
// Odd-sized lines have a space in them.
|
||||
assert_eq!(word_wrapper_width, vec![8, 20, 17, 17, 20, 4]);
|
||||
}
|
||||
|
||||
/// Ensure words separated by nbsp are wrapped as if they were a single one.
|
||||
#[test]
|
||||
fn line_composer_word_wrapper_nbsp() {
|
||||
let width = 20;
|
||||
let text = "AAAAAAAAAAAAAAA AAAA\u{00a0}AAA";
|
||||
let (word_wrapper, word_wrapper_widths, _) =
|
||||
run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAA", "AAAA\u{00a0}AAA",]);
|
||||
assert_eq!(word_wrapper_widths, vec![15, 8]);
|
||||
|
||||
// Ensure that if the character was a regular space, it would be wrapped differently.
|
||||
let text_space = text.replace('\u{00a0}', " ");
|
||||
let (word_wrapper_space, word_wrapper_widths, _) =
|
||||
run_composer(Composer::WordWrapper { trim: true }, text_space, width);
|
||||
assert_eq!(word_wrapper_space, vec!["AAAAAAAAAAAAAAA AAAA", "AAA",]);
|
||||
assert_eq!(word_wrapper_widths, vec![20, 3])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_word_wrapper_preserve_indentation() {
|
||||
let width = 20;
|
||||
let text = "AAAAAAAAAAAAAAAAAAAA AAA";
|
||||
let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
|
||||
assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAAAAAAA", " AAA",]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_word_wrapper_preserve_indentation_with_wrap() {
|
||||
let width = 10;
|
||||
let text = "AAA AAA AAAAA AA AAAAAA\n B\n C\n D";
|
||||
let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
|
||||
assert_eq!(
|
||||
word_wrapper,
|
||||
vec!["AAA AAA", "AAAAA AA", "AAAAAA", " B", " C", " D"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_word_wrapper_preserve_indentation_lots_of_whitespace() {
|
||||
let width = 10;
|
||||
let text = " 4 Indent\n must wrap!";
|
||||
let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
|
||||
assert_eq!(
|
||||
word_wrapper,
|
||||
vec![
|
||||
" ",
|
||||
" 4",
|
||||
"Indent",
|
||||
" ",
|
||||
" must",
|
||||
"wrap!"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_zero_width_at_end() {
|
||||
let width = 3;
|
||||
let line = "foo\0";
|
||||
let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, line, width);
|
||||
let (line_truncator, _, _) = run_composer(Composer::LineTruncator, line, width);
|
||||
assert_eq!(word_wrapper, vec!["foo\0"]);
|
||||
assert_eq!(line_truncator, vec!["foo\0"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_preserves_line_alignment() {
|
||||
let width = 20;
|
||||
let lines = vec![
|
||||
Line::from("Something that is left aligned.").alignment(Alignment::Left),
|
||||
Line::from("This is right aligned and half short.").alignment(Alignment::Right),
|
||||
Line::from("This should sit in the center.").alignment(Alignment::Center),
|
||||
];
|
||||
let (_, _, wrapped_alignments) =
|
||||
run_composer(Composer::WordWrapper { trim: true }, lines.clone(), width);
|
||||
let (_, _, truncated_alignments) = run_composer(Composer::LineTruncator, lines, width);
|
||||
assert_eq!(
|
||||
wrapped_alignments,
|
||||
vec![
|
||||
Alignment::Left,
|
||||
Alignment::Left,
|
||||
Alignment::Right,
|
||||
Alignment::Right,
|
||||
Alignment::Right,
|
||||
Alignment::Center,
|
||||
Alignment::Center
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
truncated_alignments,
|
||||
vec![Alignment::Left, Alignment::Right, Alignment::Center]
|
||||
);
|
||||
}
|
||||
}
|
||||
881
src/widgets/scrollbar.rs
Normal file
881
src/widgets/scrollbar.rs
Normal file
@@ -0,0 +1,881 @@
|
||||
use super::StatefulWidget;
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::Style,
|
||||
symbols::{block::FULL, line},
|
||||
};
|
||||
|
||||
/// Scrollbar Set
|
||||
/// ```text
|
||||
/// <--▮------->
|
||||
/// ^ ^ ^ ^
|
||||
/// │ │ │ └ end
|
||||
/// │ │ └──── track
|
||||
/// │ └──────── thumb
|
||||
/// └─────────── begin
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Set {
|
||||
pub track: &'static str,
|
||||
pub thumb: &'static str,
|
||||
pub begin: &'static str,
|
||||
pub end: &'static str,
|
||||
}
|
||||
|
||||
pub const DOUBLE_VERTICAL: Set = Set {
|
||||
track: line::DOUBLE_VERTICAL,
|
||||
thumb: FULL,
|
||||
begin: "▲",
|
||||
end: "▼",
|
||||
};
|
||||
|
||||
pub const DOUBLE_HORIZONTAL: Set = Set {
|
||||
track: line::DOUBLE_HORIZONTAL,
|
||||
thumb: FULL,
|
||||
begin: "◄",
|
||||
end: "►",
|
||||
};
|
||||
|
||||
pub const VERTICAL: Set = Set {
|
||||
track: line::VERTICAL,
|
||||
thumb: FULL,
|
||||
begin: "↑",
|
||||
end: "↓",
|
||||
};
|
||||
|
||||
pub const HORIZONTAL: Set = Set {
|
||||
track: line::HORIZONTAL,
|
||||
thumb: FULL,
|
||||
begin: "←",
|
||||
end: "→",
|
||||
};
|
||||
|
||||
/// An enum representing the direction of scrolling in a Scrollbar widget.
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub enum ScrollDirection {
|
||||
/// Forward scroll direction, usually corresponds to scrolling downwards or rightwards.
|
||||
#[default]
|
||||
Forward,
|
||||
/// Backward scroll direction, usually corresponds to scrolling upwards or leftwards.
|
||||
Backward,
|
||||
}
|
||||
|
||||
/// A struct representing the state of a Scrollbar widget.
|
||||
///
|
||||
/// For example, in the following list, assume there are 4 bullet points:
|
||||
///
|
||||
/// - the `position` is 0
|
||||
/// - the `content_length` is 4
|
||||
/// - the `viewport_content_length` is 2
|
||||
///
|
||||
/// ```text
|
||||
/// ┌───────────────┐
|
||||
/// │1. this is a █
|
||||
/// │ single item █
|
||||
/// │2. this is a ║
|
||||
/// │ second item ║
|
||||
/// └───────────────┘
|
||||
/// ```
|
||||
///
|
||||
/// If you don't have multi-line content, you can leave the `viewport_content_length` set to the
|
||||
/// default of 0 and it'll use the track size as a `viewport_content_length`.
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct ScrollbarState {
|
||||
// The current position within the scrollable content.
|
||||
position: u16,
|
||||
// The total length of the scrollable content.
|
||||
content_length: u16,
|
||||
// The length of content in current viewport.
|
||||
viewport_content_length: u16,
|
||||
}
|
||||
|
||||
impl ScrollbarState {
|
||||
/// Sets the scroll position of the scrollbar and returns the modified ScrollbarState.
|
||||
pub fn position(mut self, position: u16) -> Self {
|
||||
self.position = position;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the length of the scrollable content and returns the modified ScrollbarState.
|
||||
pub fn content_length(mut self, content_length: u16) -> Self {
|
||||
self.content_length = content_length;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the length of the viewport content and returns the modified ScrollbarState.
|
||||
pub fn viewport_content_length(mut self, viewport_content_length: u16) -> Self {
|
||||
self.viewport_content_length = viewport_content_length;
|
||||
self
|
||||
}
|
||||
|
||||
/// Decrements the scroll position by one, ensuring it doesn't go below zero.
|
||||
pub fn prev(&mut self) {
|
||||
self.position = self.position.saturating_sub(1);
|
||||
}
|
||||
|
||||
/// Increments the scroll position by one, ensuring it doesn't exceed the length of the content.
|
||||
pub fn next(&mut self) {
|
||||
self.position = self
|
||||
.position
|
||||
.saturating_add(1)
|
||||
.clamp(0, self.content_length.saturating_sub(1))
|
||||
}
|
||||
|
||||
/// Sets the scroll position to the start of the scrollable content.
|
||||
pub fn first(&mut self) {
|
||||
self.position = 0;
|
||||
}
|
||||
|
||||
/// Sets the scroll position to the end of the scrollable content.
|
||||
pub fn last(&mut self) {
|
||||
self.position = self.content_length.saturating_sub(1)
|
||||
}
|
||||
|
||||
/// Changes the scroll position based on the provided ScrollDirection.
|
||||
pub fn scroll(&mut self, direction: ScrollDirection) {
|
||||
match direction {
|
||||
ScrollDirection::Forward => {
|
||||
self.next();
|
||||
}
|
||||
ScrollDirection::Backward => {
|
||||
self.prev();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Scrollbar Orientation
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub enum ScrollbarOrientation {
|
||||
#[default]
|
||||
VerticalRight,
|
||||
VerticalLeft,
|
||||
HorizontalBottom,
|
||||
HorizontalTop,
|
||||
}
|
||||
|
||||
/// Scrollbar widget for tui-rs library.
|
||||
///
|
||||
/// This widget can be used to display a scrollbar in a terminal user interface.
|
||||
/// The following components of the scrollbar are customizable in symbol and style.
|
||||
///
|
||||
/// ```text
|
||||
/// <--▮------->
|
||||
/// ^ ^ ^ ^
|
||||
/// │ │ │ └ end
|
||||
/// │ │ └──── track
|
||||
/// │ └──────── thumb
|
||||
/// └─────────── begin
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Scrollbar<'a> {
|
||||
orientation: ScrollbarOrientation,
|
||||
thumb_style: Style,
|
||||
thumb_symbol: &'a str,
|
||||
track_style: Style,
|
||||
track_symbol: &'a str,
|
||||
begin_symbol: Option<&'a str>,
|
||||
begin_style: Style,
|
||||
end_symbol: Option<&'a str>,
|
||||
end_style: Style,
|
||||
}
|
||||
|
||||
impl<'a> Default for Scrollbar<'a> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
orientation: ScrollbarOrientation::default(),
|
||||
thumb_symbol: DOUBLE_VERTICAL.thumb,
|
||||
thumb_style: Style::default(),
|
||||
track_symbol: DOUBLE_VERTICAL.track,
|
||||
track_style: Style::default(),
|
||||
begin_symbol: Some(DOUBLE_VERTICAL.begin),
|
||||
begin_style: Style::default(),
|
||||
end_symbol: Some(DOUBLE_VERTICAL.end),
|
||||
end_style: Style::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Scrollbar<'a> {
|
||||
pub fn new(orientation: ScrollbarOrientation) -> Self {
|
||||
Self::default().orientation(orientation)
|
||||
}
|
||||
|
||||
/// Sets the orientation of the scrollbar.
|
||||
/// Resets the symbols to [`DOUBLE_VERTICAL`] or [`DOUBLE_HORIZONTAL`] based on orientation
|
||||
pub fn orientation(mut self, orientation: ScrollbarOrientation) -> Self {
|
||||
self.orientation = orientation;
|
||||
let set = if self.is_vertical() {
|
||||
DOUBLE_VERTICAL
|
||||
} else {
|
||||
DOUBLE_HORIZONTAL
|
||||
};
|
||||
self.symbols(set)
|
||||
}
|
||||
|
||||
/// Sets the orientation and symbols for the scrollbar from a [`Set`].
|
||||
pub fn orientation_and_symbol(mut self, orientation: ScrollbarOrientation, set: Set) -> Self {
|
||||
self.orientation = orientation;
|
||||
self.symbols(set)
|
||||
}
|
||||
|
||||
/// Sets the symbol that represents the thumb of the scrollbar.
|
||||
pub fn thumb_symbol(mut self, thumb_symbol: &'a str) -> Self {
|
||||
self.thumb_symbol = thumb_symbol;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style that represents the thumb of the scrollbar.
|
||||
pub fn thumb_style(mut self, thumb_style: Style) -> Self {
|
||||
self.thumb_style = thumb_style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the symbol that represents the track of the scrollbar.
|
||||
pub fn track_symbol(mut self, track_symbol: &'a str) -> Self {
|
||||
self.track_symbol = track_symbol;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style that is used for the track of the scrollbar.
|
||||
pub fn track_style(mut self, track_style: Style) -> Self {
|
||||
self.track_style = track_style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the symbol that represents the beginning of the scrollbar.
|
||||
pub fn begin_symbol(mut self, begin_symbol: Option<&'a str>) -> Self {
|
||||
self.begin_symbol = begin_symbol;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style that is used for the beginning of the scrollbar.
|
||||
pub fn begin_style(mut self, begin_style: Style) -> Self {
|
||||
self.begin_style = begin_style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the symbol that represents the end of the scrollbar.
|
||||
pub fn end_symbol(mut self, end_symbol: Option<&'a str>) -> Self {
|
||||
self.end_symbol = end_symbol;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style that is used for the end of the scrollbar.
|
||||
pub fn end_style(mut self, end_style: Style) -> Self {
|
||||
self.end_style = end_style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the symbols used for the various parts of the scrollbar from a [`Set`].
|
||||
///
|
||||
/// ```text
|
||||
/// <--▮------->
|
||||
/// ^ ^ ^ ^
|
||||
/// │ │ │ └ end
|
||||
/// │ │ └──── track
|
||||
/// │ └──────── thumb
|
||||
/// └─────────── begin
|
||||
/// ```
|
||||
///
|
||||
/// Only sets begin_symbol and end_symbol if they already contain a value.
|
||||
/// If begin_symbol and/or end_symbol were set to `None` explicitly, this function will respect
|
||||
/// that choice.
|
||||
pub fn symbols(mut self, symbol: Set) -> Self {
|
||||
self.track_symbol = symbol.track;
|
||||
self.thumb_symbol = symbol.thumb;
|
||||
if self.begin_symbol.is_some() {
|
||||
self.begin_symbol = Some(symbol.begin);
|
||||
}
|
||||
if self.end_symbol.is_some() {
|
||||
self.end_symbol = Some(symbol.end);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style used for the various parts of the scrollbar from a [`Style`].
|
||||
/// ```text
|
||||
/// <--▮------->
|
||||
/// ^ ^ ^ ^
|
||||
/// │ │ │ └ end
|
||||
/// │ │ └──── track
|
||||
/// │ └──────── thumb
|
||||
/// └─────────── begin
|
||||
/// ```
|
||||
pub fn style(mut self, style: Style) -> Self {
|
||||
self.track_style = style;
|
||||
self.thumb_style = style;
|
||||
self.begin_style = style;
|
||||
self.end_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
fn is_vertical(&self) -> bool {
|
||||
match self.orientation {
|
||||
ScrollbarOrientation::VerticalRight | ScrollbarOrientation::VerticalLeft => true,
|
||||
ScrollbarOrientation::HorizontalBottom | ScrollbarOrientation::HorizontalTop => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_track_area(&self, area: Rect) -> Rect {
|
||||
// Decrease track area if a begin arrow is present
|
||||
let area = if self.begin_symbol.is_some() {
|
||||
if self.is_vertical() {
|
||||
// For vertical scrollbar, reduce the height by one
|
||||
Rect::new(
|
||||
area.x,
|
||||
area.y + 1,
|
||||
area.width,
|
||||
area.height.saturating_sub(1),
|
||||
)
|
||||
} else {
|
||||
// For horizontal scrollbar, reduce the width by one
|
||||
Rect::new(
|
||||
area.x + 1,
|
||||
area.y,
|
||||
area.width.saturating_sub(1),
|
||||
area.height,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
area
|
||||
};
|
||||
// Further decrease scrollbar area if an end arrow is present
|
||||
if self.end_symbol.is_some() {
|
||||
if self.is_vertical() {
|
||||
// For vertical scrollbar, reduce the height by one
|
||||
Rect::new(area.x, area.y, area.width, area.height.saturating_sub(1))
|
||||
} else {
|
||||
// For horizontal scrollbar, reduce the width by one
|
||||
Rect::new(area.x, area.y, area.width.saturating_sub(1), area.height)
|
||||
}
|
||||
} else {
|
||||
area
|
||||
}
|
||||
}
|
||||
|
||||
fn should_not_render(&self, track_start: u16, track_end: u16, content_length: u16) -> bool {
|
||||
if track_end - track_start == 0 || content_length == 0 {
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn get_track_start_end(&self, area: Rect) -> (u16, u16, u16) {
|
||||
match self.orientation {
|
||||
ScrollbarOrientation::VerticalRight => {
|
||||
(area.top(), area.bottom(), area.right().saturating_sub(1))
|
||||
}
|
||||
ScrollbarOrientation::VerticalLeft => (area.top(), area.bottom(), area.left()),
|
||||
ScrollbarOrientation::HorizontalBottom => {
|
||||
(area.left(), area.right(), area.bottom().saturating_sub(1))
|
||||
}
|
||||
ScrollbarOrientation::HorizontalTop => (area.left(), area.right(), area.top()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate the starting and ending position of a scrollbar thumb.
|
||||
///
|
||||
/// The scrollbar thumb's position and size are determined based on the current state of the
|
||||
/// scrollbar, and the dimensions of the scrollbar track.
|
||||
///
|
||||
/// This function returns a tuple `(thumb_start, thumb_end)` where `thumb_start` is the position
|
||||
/// at which the scrollbar thumb begins, and `thumb_end` is the position at which the
|
||||
/// scrollbar thumb ends.
|
||||
///
|
||||
/// The size of the thumb (i.e., `thumb_end - thumb_start`) is proportional to the ratio of the
|
||||
/// viewport content length to the total content length.
|
||||
///
|
||||
/// The position of the thumb (i.e., `thumb_start`) is proportional to the ratio of the current
|
||||
/// scroll position to the total content length.
|
||||
fn get_thumb_start_end(
|
||||
&self,
|
||||
state: &ScrollbarState,
|
||||
track_start_end: (u16, u16),
|
||||
) -> (u16, u16) {
|
||||
// let (track_start, track_end) = track_start_end;
|
||||
// let track_size = track_end - track_start;
|
||||
// let thumb_size =
|
||||
// ((state.viewport_content_length / state.content_length) * track_size).max(1);
|
||||
// let thumb_start = (state.position / state.content_length) *
|
||||
// state.viewport_content_length;
|
||||
// let thumb_end = thumb_size + thumb_start;
|
||||
// (thumb_start, thumb_end)
|
||||
|
||||
let (track_start, track_end) = track_start_end;
|
||||
|
||||
let viewport_content_length = if state.viewport_content_length == 0 {
|
||||
track_end - track_start
|
||||
} else {
|
||||
state.viewport_content_length
|
||||
};
|
||||
|
||||
let scroll_position_ratio = (state.position as f64 / state.content_length as f64).min(1.0);
|
||||
|
||||
let thumb_size = (((viewport_content_length as f64 / state.content_length as f64)
|
||||
* (track_end - track_start) as f64)
|
||||
.round() as u16)
|
||||
.max(1);
|
||||
|
||||
let track_size = (track_end - track_start).saturating_sub(thumb_size);
|
||||
|
||||
let thumb_start = track_start + (scroll_position_ratio * track_size as f64).round() as u16;
|
||||
|
||||
let thumb_end = thumb_start + thumb_size;
|
||||
|
||||
(thumb_start, thumb_end)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> StatefulWidget for Scrollbar<'a> {
|
||||
type State = ScrollbarState;
|
||||
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
//
|
||||
// For ScrollbarOrientation::VerticalRight
|
||||
//
|
||||
// ┌───────── track_axis (x)
|
||||
// v
|
||||
// ┌───────────────┐
|
||||
// │ ║<──────── track_start (y1)
|
||||
// │ █
|
||||
// │ █
|
||||
// │ ║
|
||||
// │ ║<──────── track_end (y2)
|
||||
// └───────────────┘
|
||||
//
|
||||
// For ScrollbarOrientation::HorizontalBottom
|
||||
//
|
||||
// ┌───────────────┐
|
||||
// │ │
|
||||
// │ │
|
||||
// │ │
|
||||
// └═══███═════════┘<──────── track_axis (y)
|
||||
// ^ ^
|
||||
// │ └────────── track_end (x2)
|
||||
// │
|
||||
// └──────────────────────── track_start (x1)
|
||||
//
|
||||
|
||||
// Find track_start, track_end, and track_axis
|
||||
let area = self.get_track_area(area);
|
||||
let (track_start, track_end, track_axis) = self.get_track_start_end(area);
|
||||
|
||||
if self.should_not_render(track_start, track_end, state.content_length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let (thumb_start, thumb_end) = self.get_thumb_start_end(state, (track_start, track_end));
|
||||
|
||||
for i in track_start..track_end {
|
||||
let (style, symbol) = if i >= thumb_start && i < thumb_end {
|
||||
(self.thumb_style, self.thumb_symbol)
|
||||
} else {
|
||||
(self.track_style, self.track_symbol)
|
||||
};
|
||||
|
||||
if self.is_vertical() {
|
||||
buf.set_string(track_axis, i, symbol, style);
|
||||
} else {
|
||||
buf.set_string(i, track_axis, symbol, style);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(s) = self.begin_symbol {
|
||||
if self.is_vertical() {
|
||||
buf.set_string(track_axis, track_start - 1, s, self.begin_style);
|
||||
} else {
|
||||
buf.set_string(track_start - 1, track_axis, s, self.begin_style);
|
||||
}
|
||||
};
|
||||
if let Some(s) = self.end_symbol {
|
||||
if self.is_vertical() {
|
||||
buf.set_string(track_axis, track_end, s, self.end_style);
|
||||
} else {
|
||||
buf.set_string(track_end, track_axis, s, self.end_style);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::assert_buffer_eq;
|
||||
|
||||
#[test]
|
||||
fn test_no_render_when_area_zero() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 0, 0));
|
||||
let mut state = ScrollbarState::default().position(0).content_length(1);
|
||||
Scrollbar::default().render(buffer.area, &mut buffer, &mut state);
|
||||
assert_buffer_eq!(buffer, Buffer::empty(buffer.area));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_render_when_height_zero_with_without_arrows() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 3, 0));
|
||||
let mut state = ScrollbarState::default().position(0).content_length(1);
|
||||
Scrollbar::default().render(buffer.area, &mut buffer, &mut state);
|
||||
assert_buffer_eq!(buffer, Buffer::empty(buffer.area));
|
||||
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 3, 0));
|
||||
let mut state = ScrollbarState::default().position(0).content_length(1);
|
||||
Scrollbar::default()
|
||||
.begin_symbol(None)
|
||||
.end_symbol(None)
|
||||
.render(buffer.area, &mut buffer, &mut state);
|
||||
assert_buffer_eq!(buffer, Buffer::empty(buffer.area));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_render_when_height_too_small_for_arrows() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 2));
|
||||
let mut state = ScrollbarState::default().position(0).content_length(1);
|
||||
Scrollbar::default().render(buffer.area, &mut buffer, &mut state);
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" ", " "]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_renders_all_thumbs_at_minimum_height_without_arrows() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 2));
|
||||
let mut state = ScrollbarState::default().position(0).content_length(1);
|
||||
Scrollbar::default()
|
||||
.begin_symbol(None)
|
||||
.end_symbol(None)
|
||||
.render(buffer.area, &mut buffer, &mut state);
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" █", " █"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_renders_all_thumbs_at_minimum_height_and_minimum_width_without_arrows() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 2));
|
||||
let mut state = ScrollbarState::default().position(0).content_length(1);
|
||||
Scrollbar::default()
|
||||
.begin_symbol(None)
|
||||
.end_symbol(None)
|
||||
.render(buffer.area, &mut buffer, &mut state);
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["█", "█"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_renders_two_arrows_one_thumb_at_minimum_height_with_arrows() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 3));
|
||||
let mut state = ScrollbarState::default().position(0).content_length(1);
|
||||
Scrollbar::default().render(buffer.area, &mut buffer, &mut state);
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" ▲", " █", " ▼"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_render_when_content_length_zero() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 2, 2));
|
||||
let mut state = ScrollbarState::default().position(0).content_length(0);
|
||||
Scrollbar::default().render(buffer.area, &mut buffer, &mut state);
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" ", " "]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_renders_all_thumbs_when_height_equals_content_length() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 2, 2));
|
||||
let mut state = ScrollbarState::default().position(0).content_length(2);
|
||||
Scrollbar::default()
|
||||
.begin_symbol(None)
|
||||
.end_symbol(None)
|
||||
.render(buffer.area, &mut buffer, &mut state);
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" █", " █"]));
|
||||
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 2, 8));
|
||||
let mut state = ScrollbarState::default().position(0).content_length(8);
|
||||
Scrollbar::default()
|
||||
.begin_symbol(None)
|
||||
.end_symbol(None)
|
||||
.render(buffer.area, &mut buffer, &mut state);
|
||||
assert_buffer_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec![" █", " █", " █", " █", " █", " █", " █", " █"])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_renders_single_vertical_thumb_when_content_length_square_of_height() {
|
||||
for i in 0..=17 {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 2, 4));
|
||||
let mut state = ScrollbarState::default().position(i).content_length(16);
|
||||
Scrollbar::default()
|
||||
.begin_symbol(None)
|
||||
.end_symbol(None)
|
||||
.render(buffer.area, &mut buffer, &mut state);
|
||||
let expected = if i <= 2 {
|
||||
vec![" █", " ║", " ║", " ║"]
|
||||
} else if i <= 7 {
|
||||
vec![" ║", " █", " ║", " ║"]
|
||||
} else if i <= 13 {
|
||||
vec![" ║", " ║", " █", " ║"]
|
||||
} else {
|
||||
vec![" ║", " ║", " ║", " █"]
|
||||
};
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_renders_single_horizontal_thumb_when_content_length_square_of_width() {
|
||||
for i in 0..=17 {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 2));
|
||||
let mut state = ScrollbarState::default().position(i).content_length(16);
|
||||
Scrollbar::default()
|
||||
.begin_symbol(None)
|
||||
.end_symbol(None)
|
||||
.orientation(ScrollbarOrientation::HorizontalBottom)
|
||||
.render(buffer.area, &mut buffer, &mut state);
|
||||
let expected = if i <= 2 {
|
||||
vec![" ", "█═══"]
|
||||
} else if i <= 7 {
|
||||
vec![" ", "═█══"]
|
||||
} else if i <= 13 {
|
||||
vec![" ", "══█═"]
|
||||
} else {
|
||||
vec![" ", "═══█"]
|
||||
};
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_renders_one_thumb_for_large_content_relative_to_height() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 2));
|
||||
let mut state = ScrollbarState::default().position(0).content_length(1600);
|
||||
Scrollbar::default()
|
||||
.begin_symbol(None)
|
||||
.end_symbol(None)
|
||||
.orientation(ScrollbarOrientation::HorizontalBottom)
|
||||
.render(buffer.area, &mut buffer, &mut state);
|
||||
let expected = vec![" ", "█═══"];
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
|
||||
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 2));
|
||||
let mut state = ScrollbarState::default().position(800).content_length(1600);
|
||||
Scrollbar::default()
|
||||
.begin_symbol(None)
|
||||
.end_symbol(None)
|
||||
.orientation(ScrollbarOrientation::HorizontalBottom)
|
||||
.render(buffer.area, &mut buffer, &mut state);
|
||||
let expected = vec![" ", "══█═"];
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_renders_two_thumb_default_symbols_for_content_double_height() {
|
||||
for i in 0..=7 {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 2, 4));
|
||||
let mut state = ScrollbarState::default().position(i).content_length(8);
|
||||
Scrollbar::default()
|
||||
.begin_symbol(None)
|
||||
.end_symbol(None)
|
||||
.render(buffer.area, &mut buffer, &mut state);
|
||||
let expected = if i <= 1 {
|
||||
vec![" █", " █", " ║", " ║"]
|
||||
} else if i <= 5 {
|
||||
vec![" ║", " █", " █", " ║"]
|
||||
} else {
|
||||
vec![" ║", " ║", " █", " █"]
|
||||
};
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_renders_two_thumb_custom_symbols_for_content_double_height() {
|
||||
for i in 0..=7 {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 2, 4));
|
||||
let mut state = ScrollbarState::default().position(i).content_length(8);
|
||||
Scrollbar::default()
|
||||
.symbols(VERTICAL)
|
||||
.begin_symbol(None)
|
||||
.end_symbol(None)
|
||||
.render(buffer.area, &mut buffer, &mut state);
|
||||
let expected = if i <= 1 {
|
||||
vec![" █", " █", " │", " │"]
|
||||
} else if i <= 5 {
|
||||
vec![" │", " █", " █", " │"]
|
||||
} else {
|
||||
vec![" │", " │", " █", " █"]
|
||||
};
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_renders_two_thumb_default_symbols_for_content_double_width() {
|
||||
for i in 0..=7 {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 2));
|
||||
let mut state = ScrollbarState::default().position(i).content_length(8);
|
||||
Scrollbar::default()
|
||||
.orientation(ScrollbarOrientation::HorizontalBottom)
|
||||
.begin_symbol(None)
|
||||
.end_symbol(None)
|
||||
.render(buffer.area, &mut buffer, &mut state);
|
||||
let expected = if i <= 1 {
|
||||
vec![" ", "██══"]
|
||||
} else if i <= 5 {
|
||||
vec![" ", "═██═"]
|
||||
} else {
|
||||
vec![" ", "══██"]
|
||||
};
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_renders_two_thumb_custom_symbols_for_content_double_width() {
|
||||
for i in 0..=7 {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 2));
|
||||
let mut state = ScrollbarState::default().position(i).content_length(8);
|
||||
Scrollbar::default()
|
||||
.orientation(ScrollbarOrientation::HorizontalBottom)
|
||||
.symbols(HORIZONTAL)
|
||||
.begin_symbol(None)
|
||||
.end_symbol(None)
|
||||
.render(buffer.area, &mut buffer, &mut state);
|
||||
let expected = if i <= 1 {
|
||||
vec![" ", "██──"]
|
||||
} else if i <= 5 {
|
||||
vec![" ", "─██─"]
|
||||
} else {
|
||||
vec![" ", "──██"]
|
||||
};
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rendering_viewport_content_length() {
|
||||
for i in 0..=16 {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 2));
|
||||
let mut state = ScrollbarState::default()
|
||||
.position(i)
|
||||
.content_length(16)
|
||||
.viewport_content_length(4);
|
||||
Scrollbar::default()
|
||||
.orientation(ScrollbarOrientation::HorizontalBottom)
|
||||
.begin_symbol(Some(DOUBLE_HORIZONTAL.begin))
|
||||
.end_symbol(Some(DOUBLE_HORIZONTAL.end))
|
||||
.render(buffer.area, &mut buffer, &mut state);
|
||||
let expected = if i <= 1 {
|
||||
vec![" ", "◄██════►"]
|
||||
} else if i <= 5 {
|
||||
vec![" ", "◄═██═══►"]
|
||||
} else if i <= 9 {
|
||||
vec![" ", "◄══██══►"]
|
||||
} else if i <= 13 {
|
||||
vec![" ", "◄═══██═►"]
|
||||
} else {
|
||||
vec![" ", "◄════██►"]
|
||||
};
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
|
||||
}
|
||||
|
||||
for i in 0..=16 {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 2));
|
||||
let mut state = ScrollbarState::default()
|
||||
.position(i)
|
||||
.content_length(16)
|
||||
.viewport_content_length(1);
|
||||
Scrollbar::default()
|
||||
.orientation(ScrollbarOrientation::HorizontalBottom)
|
||||
.begin_symbol(Some(DOUBLE_HORIZONTAL.begin))
|
||||
.end_symbol(Some(DOUBLE_HORIZONTAL.end))
|
||||
.render(buffer.area, &mut buffer, &mut state);
|
||||
dbg!(i);
|
||||
let expected = if i <= 1 {
|
||||
vec![" ", "◄█═════►"]
|
||||
} else if i <= 4 {
|
||||
vec![" ", "◄═█════►"]
|
||||
} else if i <= 7 {
|
||||
vec![" ", "◄══█═══►"]
|
||||
} else if i <= 11 {
|
||||
vec![" ", "◄═══█══►"]
|
||||
} else if i <= 14 {
|
||||
vec![" ", "◄════█═►"]
|
||||
} else {
|
||||
vec![" ", "◄═════█►"]
|
||||
};
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rendering_begin_end_arrows_horizontal_bottom() {
|
||||
for i in 0..=16 {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 2));
|
||||
let mut state = ScrollbarState::default().position(i).content_length(16);
|
||||
Scrollbar::default()
|
||||
.orientation(ScrollbarOrientation::HorizontalBottom)
|
||||
.begin_symbol(Some(DOUBLE_HORIZONTAL.begin))
|
||||
.end_symbol(Some(DOUBLE_HORIZONTAL.end))
|
||||
.render(buffer.area, &mut buffer, &mut state);
|
||||
let expected = if i <= 1 {
|
||||
vec![" ", "◄██════►"]
|
||||
} else if i <= 5 {
|
||||
vec![" ", "◄═██═══►"]
|
||||
} else if i <= 9 {
|
||||
vec![" ", "◄══██══►"]
|
||||
} else if i <= 13 {
|
||||
vec![" ", "◄═══██═►"]
|
||||
} else {
|
||||
vec![" ", "◄════██►"]
|
||||
};
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rendering_begin_end_arrows_horizontal_top() {
|
||||
for i in 0..=16 {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 2));
|
||||
let mut state = ScrollbarState::default().position(i).content_length(16);
|
||||
Scrollbar::default()
|
||||
.orientation(ScrollbarOrientation::HorizontalTop)
|
||||
.begin_symbol(Some(DOUBLE_HORIZONTAL.begin))
|
||||
.end_symbol(Some(DOUBLE_HORIZONTAL.end))
|
||||
.render(buffer.area, &mut buffer, &mut state);
|
||||
let expected = if i <= 1 {
|
||||
vec!["◄██════►", " "]
|
||||
} else if i <= 5 {
|
||||
vec!["◄═██═══►", " "]
|
||||
} else if i <= 9 {
|
||||
vec!["◄══██══►", " "]
|
||||
} else if i <= 13 {
|
||||
vec!["◄═══██═►", " "]
|
||||
} else {
|
||||
vec!["◄════██►", " "]
|
||||
};
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rendering_only_begin_arrow_horizontal_bottom() {
|
||||
for i in 0..=16 {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 2));
|
||||
let mut state = ScrollbarState::default().position(i).content_length(16);
|
||||
Scrollbar::default()
|
||||
.orientation(ScrollbarOrientation::HorizontalBottom)
|
||||
.begin_symbol(Some(DOUBLE_HORIZONTAL.begin))
|
||||
.end_symbol(None)
|
||||
.render(buffer.area, &mut buffer, &mut state);
|
||||
let expected = if i <= 1 {
|
||||
vec![" ", "◄███════"]
|
||||
} else if i <= 5 {
|
||||
vec![" ", "◄═███═══"]
|
||||
} else if i <= 9 {
|
||||
vec![" ", "◄══███══"]
|
||||
} else if i <= 13 {
|
||||
vec![" ", "◄═══███═"]
|
||||
} else {
|
||||
vec![" ", "◄════███"]
|
||||
};
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,27 @@
|
||||
use std::cmp::min;
|
||||
|
||||
use layout::Rect;
|
||||
use buffer::Buffer;
|
||||
use widgets::{Block, Widget};
|
||||
use style::Style;
|
||||
use symbols::bar;
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::Style,
|
||||
symbols,
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
|
||||
/// Widget to render a sparkline over one or more lines.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # extern crate tui;
|
||||
/// # use tui::widgets::{Block, Borders, Sparkline};
|
||||
/// # use tui::style::{Style, Color};
|
||||
/// # fn main() {
|
||||
/// # use ratatui::widgets::{Block, Borders, Sparkline};
|
||||
/// # use ratatui::style::{Style, Color};
|
||||
/// Sparkline::default()
|
||||
/// .block(Block::default().title("Sparkline").borders(Borders::ALL))
|
||||
/// .data(&[0, 2, 3, 4, 1, 4, 10])
|
||||
/// .max(5)
|
||||
/// .style(Style::default().fg(Color::Red).bg(Color::White));
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Sparkline<'a> {
|
||||
/// A block to wrap the widget in
|
||||
block: Option<Block<'a>>,
|
||||
@@ -32,49 +32,72 @@ pub struct Sparkline<'a> {
|
||||
/// The maximum value to take to compute the maximum bar height (if nothing is specified, the
|
||||
/// widget uses the max of the dataset)
|
||||
max: Option<u64>,
|
||||
/// A set of bar symbols used to represent the give data
|
||||
bar_set: symbols::bar::Set,
|
||||
// The direction to render the sparkine, either from left to right, or from right to left
|
||||
direction: RenderDirection,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum RenderDirection {
|
||||
LeftToRight,
|
||||
RightToLeft,
|
||||
}
|
||||
|
||||
impl<'a> Default for Sparkline<'a> {
|
||||
fn default() -> Sparkline<'a> {
|
||||
Sparkline {
|
||||
block: None,
|
||||
style: Default::default(),
|
||||
style: Style::default(),
|
||||
data: &[],
|
||||
max: None,
|
||||
bar_set: symbols::bar::NINE_LEVELS,
|
||||
direction: RenderDirection::LeftToRight,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Sparkline<'a> {
|
||||
pub fn block(&mut self, block: Block<'a>) -> &mut Sparkline<'a> {
|
||||
pub fn block(mut self, block: Block<'a>) -> Sparkline<'a> {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(&mut self, style: Style) -> &mut Sparkline<'a> {
|
||||
pub fn style(mut self, style: Style) -> Sparkline<'a> {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn data(&mut self, data: &'a [u64]) -> &mut Sparkline<'a> {
|
||||
pub fn data(mut self, data: &'a [u64]) -> Sparkline<'a> {
|
||||
self.data = data;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn max(&mut self, max: u64) -> &mut Sparkline<'a> {
|
||||
pub fn max(mut self, max: u64) -> Sparkline<'a> {
|
||||
self.max = Some(max);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn bar_set(mut self, bar_set: symbols::bar::Set) -> Sparkline<'a> {
|
||||
self.bar_set = bar_set;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn direction(mut self, direction: RenderDirection) -> Sparkline<'a> {
|
||||
self.direction = direction;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Sparkline<'a> {
|
||||
fn draw(&mut self, area: &Rect, buf: &mut Buffer) {
|
||||
let spark_area = match self.block {
|
||||
Some(ref mut b) => {
|
||||
b.draw(area, buf);
|
||||
b.inner(area)
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer) {
|
||||
let spark_area = match self.block.take() {
|
||||
Some(b) => {
|
||||
let inner_area = b.inner(area);
|
||||
b.render(area, buf);
|
||||
inner_area
|
||||
}
|
||||
None => *area,
|
||||
None => area,
|
||||
};
|
||||
|
||||
if spark_area.height < 1 {
|
||||
@@ -86,28 +109,38 @@ impl<'a> Widget for Sparkline<'a> {
|
||||
None => *self.data.iter().max().unwrap_or(&1u64),
|
||||
};
|
||||
let max_index = min(spark_area.width as usize, self.data.len());
|
||||
let mut data = self.data
|
||||
let mut data = self
|
||||
.data
|
||||
.iter()
|
||||
.take(max_index)
|
||||
.map(|e| e * u64::from(spark_area.height) * 8 / max)
|
||||
.map(|e| {
|
||||
if max == 0 {
|
||||
0
|
||||
} else {
|
||||
e * u64::from(spark_area.height) * 8 / max
|
||||
}
|
||||
})
|
||||
.collect::<Vec<u64>>();
|
||||
for j in (0..spark_area.height).rev() {
|
||||
for (i, d) in data.iter_mut().enumerate() {
|
||||
let symbol = match *d {
|
||||
0 => " ",
|
||||
1 => bar::ONE_EIGHTH,
|
||||
2 => bar::ONE_QUATER,
|
||||
3 => bar::THREE_EIGHTHS,
|
||||
4 => bar::HALF,
|
||||
5 => bar::FIVE_EIGHTHS,
|
||||
6 => bar::THREE_QUATERS,
|
||||
7 => bar::SEVEN_EIGHTHS,
|
||||
_ => bar::FULL,
|
||||
0 => self.bar_set.empty,
|
||||
1 => self.bar_set.one_eighth,
|
||||
2 => self.bar_set.one_quarter,
|
||||
3 => self.bar_set.three_eighths,
|
||||
4 => self.bar_set.half,
|
||||
5 => self.bar_set.five_eighths,
|
||||
6 => self.bar_set.three_quarters,
|
||||
7 => self.bar_set.seven_eighths,
|
||||
_ => self.bar_set.full,
|
||||
};
|
||||
buf.get_mut(spark_area.left() + i as u16, spark_area.top() + j)
|
||||
let x = match self.direction {
|
||||
RenderDirection::LeftToRight => spark_area.left() + i as u16,
|
||||
RenderDirection::RightToLeft => spark_area.right() - i as u16 - 1,
|
||||
};
|
||||
buf.get_mut(x, spark_area.top() + j)
|
||||
.set_symbol(symbol)
|
||||
.set_fg(self.style.fg)
|
||||
.set_bg(self.style.bg);
|
||||
.set_style(self.style);
|
||||
|
||||
if *d > 8 {
|
||||
*d -= 8;
|
||||
@@ -118,3 +151,59 @@ impl<'a> Widget for Sparkline<'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{assert_buffer_eq, buffer::Cell};
|
||||
|
||||
// Helper function to render a sparkline to a buffer with a given width
|
||||
// filled with x symbols to make it easier to assert on the result
|
||||
fn render(widget: Sparkline, width: u16) -> Buffer {
|
||||
let area = Rect::new(0, 0, width, 1);
|
||||
let mut cell = Cell::default();
|
||||
cell.set_symbol("x");
|
||||
let mut buffer = Buffer::filled(area, &cell);
|
||||
widget.render(area, &mut buffer);
|
||||
buffer
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_does_not_panic_if_max_is_zero() {
|
||||
let widget = Sparkline::default().data(&[0, 0, 0]);
|
||||
let buffer = render(widget, 6);
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" xxx"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_does_not_panic_if_max_is_set_to_zero() {
|
||||
let widget = Sparkline::default().data(&[0, 1, 2]).max(0);
|
||||
let buffer = render(widget, 6);
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" xxx"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_draws() {
|
||||
let widget = Sparkline::default().data(&[0, 1, 2, 3, 4, 5, 6, 7, 8]);
|
||||
let buffer = render(widget, 12);
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" ▁▂▃▄▅▆▇█xxx"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_renders_left_to_right() {
|
||||
let widget = Sparkline::default()
|
||||
.data(&[0, 1, 2, 3, 4, 5, 6, 7, 8])
|
||||
.direction(RenderDirection::LeftToRight);
|
||||
let buffer = render(widget, 12);
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" ▁▂▃▄▅▆▇█xxx"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_renders_right_to_left() {
|
||||
let widget = Sparkline::default()
|
||||
.data(&[0, 1, 2, 3, 4, 5, 6, 7, 8])
|
||||
.direction(RenderDirection::RightToLeft);
|
||||
let buffer = render(widget, 12);
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["xxx█▇▆▅▄▃▂▁ "]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,211 +1,515 @@
|
||||
use std::fmt::Display;
|
||||
use std::iter::Iterator;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use buffer::Buffer;
|
||||
use widgets::{Block, Widget};
|
||||
use layout::Rect;
|
||||
use style::Style;
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::Style,
|
||||
text::Text,
|
||||
widgets::{Block, StatefulWidget, Widget},
|
||||
};
|
||||
|
||||
/// Holds data to be displayed in a Table widget
|
||||
pub enum Row<'i, D, I>
|
||||
where
|
||||
D: Iterator<Item = I>,
|
||||
I: Display,
|
||||
{
|
||||
Data(D),
|
||||
StyledData(D, &'i Style),
|
||||
}
|
||||
|
||||
/// A widget to display data in formatted columns
|
||||
/// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`].
|
||||
///
|
||||
/// # Examples
|
||||
/// It can be created from anything that can be converted to a [`Text`].
|
||||
/// ```rust
|
||||
/// # use ratatui::widgets::Cell;
|
||||
/// # use ratatui::style::{Style, Modifier};
|
||||
/// # use ratatui::text::{Span, Line, Text};
|
||||
/// # use std::borrow::Cow;
|
||||
/// Cell::from("simple string");
|
||||
///
|
||||
/// Cell::from(Span::from("span"));
|
||||
///
|
||||
/// Cell::from(Line::from(vec![
|
||||
/// Span::raw("a vec of "),
|
||||
/// Span::styled("spans", Style::default().add_modifier(Modifier::BOLD))
|
||||
/// ]));
|
||||
///
|
||||
/// Cell::from(Text::from("a text"));
|
||||
///
|
||||
/// Cell::from(Text::from(Cow::Borrowed("hello")));
|
||||
/// ```
|
||||
/// # use tui::widgets::{Block, Borders, Table, Row};
|
||||
/// # use tui::style::{Style, Color};
|
||||
/// # fn main() {
|
||||
/// let row_style = Style::default().fg(Color::White);
|
||||
/// Table::new(
|
||||
/// ["Col1", "Col2", "Col3"].into_iter(),
|
||||
/// vec![
|
||||
/// Row::StyledData(["Row11", "Row12", "Row13"].into_iter(), &row_style),
|
||||
/// Row::StyledData(["Row21", "Row22", "Row23"].into_iter(), &row_style),
|
||||
/// Row::StyledData(["Row31", "Row32", "Row33"].into_iter(), &row_style),
|
||||
/// Row::Data(["Row41", "Row42", "Row43"].into_iter())
|
||||
/// ].into_iter()
|
||||
/// )
|
||||
/// .block(Block::default().title("Table"))
|
||||
/// .header_style(Style::default().fg(Color::Yellow))
|
||||
/// .widths(&[5, 5, 10])
|
||||
/// .style(Style::default().fg(Color::White))
|
||||
/// .column_spacing(1);
|
||||
/// # }
|
||||
/// ```
|
||||
pub struct Table<'a, 'i, T, H, I, D, R>
|
||||
where
|
||||
T: Display,
|
||||
H: Iterator<Item = T>,
|
||||
I: Display,
|
||||
D: Iterator<Item = I>,
|
||||
R: Iterator<Item = Row<'i, D, I>>,
|
||||
{
|
||||
/// A block to wrap the widget in
|
||||
block: Option<Block<'a>>,
|
||||
/// Base style for the widget
|
||||
///
|
||||
/// You can apply a [`Style`] on the entire [`Cell`] using [`Cell::style`] or rely on the styling
|
||||
/// capabilities of [`Text`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct Cell<'a> {
|
||||
content: Text<'a>,
|
||||
style: Style,
|
||||
/// Header row for all columns
|
||||
header: H,
|
||||
/// Style for the header
|
||||
header_style: Style,
|
||||
/// Width of each column (if the total width is greater than the widget width some columns may
|
||||
/// not be displayed)
|
||||
widths: &'a [u16],
|
||||
/// Space between each column
|
||||
column_spacing: u16,
|
||||
/// Data to display in each row
|
||||
rows: R,
|
||||
}
|
||||
|
||||
impl<'a, 'i, T, H, I, D, R> Default for Table<'a, 'i, T, H, I, D, R>
|
||||
impl<'a> Cell<'a> {
|
||||
/// Set the `Style` of this cell.
|
||||
pub fn style(mut self, style: Style) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> From<T> for Cell<'a>
|
||||
where
|
||||
T: Display,
|
||||
H: Iterator<Item = T> + Default,
|
||||
I: Display,
|
||||
D: Iterator<Item = I>,
|
||||
R: Iterator<Item = Row<'i, D, I>> + Default,
|
||||
T: Into<Text<'a>>,
|
||||
{
|
||||
fn default() -> Table<'a, 'i, T, H, I, D, R> {
|
||||
Table {
|
||||
block: None,
|
||||
fn from(content: T) -> Cell<'a> {
|
||||
Cell {
|
||||
content: content.into(),
|
||||
style: Style::default(),
|
||||
header: H::default(),
|
||||
header_style: Style::default(),
|
||||
widths: &[],
|
||||
rows: R::default(),
|
||||
column_spacing: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'i, T, H, I, D, R> Table<'a, 'i, T, H, I, D, R>
|
||||
where
|
||||
T: Display,
|
||||
H: Iterator<Item = T>,
|
||||
I: Display,
|
||||
D: Iterator<Item = I>,
|
||||
R: Iterator<Item = Row<'i, D, I>>,
|
||||
{
|
||||
pub fn new(header: H, rows: R) -> Table<'a, 'i, T, H, I, D, R> {
|
||||
Table {
|
||||
block: None,
|
||||
/// Holds data to be displayed in a [`Table`] widget.
|
||||
///
|
||||
/// A [`Row`] is a collection of cells. It can be created from simple strings:
|
||||
/// ```rust
|
||||
/// # use ratatui::widgets::Row;
|
||||
/// Row::new(vec!["Cell1", "Cell2", "Cell3"]);
|
||||
/// ```
|
||||
///
|
||||
/// But if you need a bit more control over individual cells, you can explicitly create [`Cell`]s:
|
||||
/// ```rust
|
||||
/// # use ratatui::widgets::{Row, Cell};
|
||||
/// # use ratatui::style::{Style, Color};
|
||||
/// Row::new(vec![
|
||||
/// Cell::from("Cell1"),
|
||||
/// Cell::from("Cell2").style(Style::default().fg(Color::Yellow)),
|
||||
/// ]);
|
||||
/// ```
|
||||
///
|
||||
/// You can also construct a row from any type that can be converted into [`Text`]:
|
||||
/// ```rust
|
||||
/// # use std::borrow::Cow;
|
||||
/// # use ratatui::widgets::Row;
|
||||
/// Row::new(vec![
|
||||
/// Cow::Borrowed("hello"),
|
||||
/// Cow::Owned("world".to_uppercase()),
|
||||
/// ]);
|
||||
/// ```
|
||||
///
|
||||
/// By default, a row has a height of 1 but you can change this using [`Row::height`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct Row<'a> {
|
||||
cells: Vec<Cell<'a>>,
|
||||
height: u16,
|
||||
style: Style,
|
||||
bottom_margin: u16,
|
||||
}
|
||||
|
||||
impl<'a> Row<'a> {
|
||||
/// Creates a new [`Row`] from an iterator where items can be converted to a [`Cell`].
|
||||
pub fn new<T>(cells: T) -> Self
|
||||
where
|
||||
T: IntoIterator,
|
||||
T::Item: Into<Cell<'a>>,
|
||||
{
|
||||
Self {
|
||||
height: 1,
|
||||
cells: cells.into_iter().map(Into::into).collect(),
|
||||
style: Style::default(),
|
||||
header: header,
|
||||
header_style: Style::default(),
|
||||
widths: &[],
|
||||
rows: rows,
|
||||
column_spacing: 1,
|
||||
bottom_margin: 0,
|
||||
}
|
||||
}
|
||||
pub fn block(&'a mut self, block: Block<'a>) -> &mut Table<'a, 'i, T, H, I, D, R> {
|
||||
self.block = Some(block);
|
||||
|
||||
/// Set the fixed height of the [`Row`]. Any [`Cell`] whose content has more lines than this
|
||||
/// height will see its content truncated.
|
||||
pub fn height(mut self, height: u16) -> Self {
|
||||
self.height = height;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn header<II>(&mut self, header: II) -> &mut Table<'a, 'i, T, H, I, D, R>
|
||||
where
|
||||
II: IntoIterator<Item = T, IntoIter = H>,
|
||||
{
|
||||
self.header = header.into_iter();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn header_style(&mut self, style: Style) -> &mut Table<'a, 'i, T, H, I, D, R> {
|
||||
self.header_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn widths(&mut self, widths: &'a [u16]) -> &mut Table<'a, 'i, T, H, I, D, R> {
|
||||
self.widths = widths;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn rows<II>(&mut self, rows: II) -> &mut Table<'a, 'i, T, H, I, D, R>
|
||||
where
|
||||
II: IntoIterator<Item = Row<'i, D, I>, IntoIter = R>,
|
||||
{
|
||||
self.rows = rows.into_iter();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(&mut self, style: Style) -> &mut Table<'a, 'i, T, H, I, D, R> {
|
||||
/// Set the [`Style`] of the entire row. This [`Style`] can be overridden by the [`Style`] of a
|
||||
/// any individual [`Cell`] or event by their [`Text`] content.
|
||||
pub fn style(mut self, style: Style) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn column_spacing(&mut self, spacing: u16) -> &mut Table<'a, 'i, T, H, I, D, R> {
|
||||
/// Set the bottom margin. By default, the bottom margin is `0`.
|
||||
pub fn bottom_margin(mut self, margin: u16) -> Self {
|
||||
self.bottom_margin = margin;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the total height of the row.
|
||||
fn total_height(&self) -> u16 {
|
||||
self.height.saturating_add(self.bottom_margin)
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget to display data in formatted columns.
|
||||
///
|
||||
/// It is a collection of [`Row`]s, themselves composed of [`Cell`]s:
|
||||
/// ```rust
|
||||
/// # use ratatui::widgets::{Block, Borders, Table, Row, Cell};
|
||||
/// # use ratatui::layout::Constraint;
|
||||
/// # use ratatui::style::{Style, Color, Modifier};
|
||||
/// # use ratatui::text::{Text, Line, Span};
|
||||
/// Table::new(vec![
|
||||
/// // Row can be created from simple strings.
|
||||
/// Row::new(vec!["Row11", "Row12", "Row13"]),
|
||||
/// // You can style the entire row.
|
||||
/// Row::new(vec!["Row21", "Row22", "Row23"]).style(Style::default().fg(Color::Blue)),
|
||||
/// // If you need more control over the styling you may need to create Cells directly
|
||||
/// Row::new(vec![
|
||||
/// Cell::from("Row31"),
|
||||
/// Cell::from("Row32").style(Style::default().fg(Color::Yellow)),
|
||||
/// Cell::from(Line::from(vec![
|
||||
/// Span::raw("Row"),
|
||||
/// Span::styled("33", Style::default().fg(Color::Green))
|
||||
/// ])),
|
||||
/// ]),
|
||||
/// // If a Row need to display some content over multiple lines, you just have to change
|
||||
/// // its height.
|
||||
/// Row::new(vec![
|
||||
/// Cell::from("Row\n41"),
|
||||
/// Cell::from("Row\n42"),
|
||||
/// Cell::from("Row\n43"),
|
||||
/// ]).height(2),
|
||||
/// ])
|
||||
/// // You can set the style of the entire Table.
|
||||
/// .style(Style::default().fg(Color::White))
|
||||
/// // It has an optional header, which is simply a Row always visible at the top.
|
||||
/// .header(
|
||||
/// Row::new(vec!["Col1", "Col2", "Col3"])
|
||||
/// .style(Style::default().fg(Color::Yellow))
|
||||
/// // If you want some space between the header and the rest of the rows, you can always
|
||||
/// // specify some margin at the bottom.
|
||||
/// .bottom_margin(1)
|
||||
/// )
|
||||
/// // As any other widget, a Table can be wrapped in a Block.
|
||||
/// .block(Block::default().title("Table"))
|
||||
/// // Columns widths are constrained in the same way as Layout...
|
||||
/// .widths(&[Constraint::Length(5), Constraint::Length(5), Constraint::Length(10)])
|
||||
/// // ...and they can be separated by a fixed spacing.
|
||||
/// .column_spacing(1)
|
||||
/// // If you wish to highlight a row in any specific way when it is selected...
|
||||
/// .highlight_style(Style::default().add_modifier(Modifier::BOLD))
|
||||
/// // ...and potentially show a symbol in front of the selection.
|
||||
/// .highlight_symbol(">>");
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Table<'a> {
|
||||
/// A block to wrap the widget in
|
||||
block: Option<Block<'a>>,
|
||||
/// Base style for the widget
|
||||
style: Style,
|
||||
/// Width constraints for each column
|
||||
widths: &'a [Constraint],
|
||||
/// Space between each column
|
||||
column_spacing: u16,
|
||||
/// Style used to render the selected row
|
||||
highlight_style: Style,
|
||||
/// Symbol in front of the selected rom
|
||||
highlight_symbol: Option<&'a str>,
|
||||
/// Optional header
|
||||
header: Option<Row<'a>>,
|
||||
/// Data to display in each row
|
||||
rows: Vec<Row<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Table<'a> {
|
||||
pub fn new<T>(rows: T) -> Self
|
||||
where
|
||||
T: IntoIterator<Item = Row<'a>>,
|
||||
{
|
||||
Self {
|
||||
block: None,
|
||||
style: Style::default(),
|
||||
widths: &[],
|
||||
column_spacing: 1,
|
||||
highlight_style: Style::default(),
|
||||
highlight_symbol: None,
|
||||
header: None,
|
||||
rows: rows.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn block(mut self, block: Block<'a>) -> Self {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn header(mut self, header: Row<'a>) -> Self {
|
||||
self.header = Some(header);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn widths(mut self, widths: &'a [Constraint]) -> Self {
|
||||
let between_0_and_100 = |&w| match w {
|
||||
Constraint::Percentage(p) => p <= 100,
|
||||
_ => true,
|
||||
};
|
||||
assert!(
|
||||
widths.iter().all(between_0_and_100),
|
||||
"Percentages should be between 0 and 100 inclusively."
|
||||
);
|
||||
self.widths = widths;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: Style) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> Self {
|
||||
self.highlight_symbol = Some(highlight_symbol);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn highlight_style(mut self, highlight_style: Style) -> Self {
|
||||
self.highlight_style = highlight_style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn column_spacing(mut self, spacing: u16) -> Self {
|
||||
self.column_spacing = spacing;
|
||||
self
|
||||
}
|
||||
|
||||
fn get_columns_widths(&self, max_width: u16, has_selection: bool) -> Vec<u16> {
|
||||
let mut constraints = Vec::with_capacity(self.widths.len() * 2 + 1);
|
||||
if has_selection {
|
||||
let highlight_symbol_width = self.highlight_symbol.map_or(0, |s| s.width() as u16);
|
||||
constraints.push(Constraint::Length(highlight_symbol_width));
|
||||
}
|
||||
for constraint in self.widths {
|
||||
constraints.push(*constraint);
|
||||
constraints.push(Constraint::Length(self.column_spacing));
|
||||
}
|
||||
if !self.widths.is_empty() {
|
||||
constraints.pop();
|
||||
}
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(constraints)
|
||||
.expand_to_fill(false)
|
||||
.split(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: max_width,
|
||||
height: 1,
|
||||
});
|
||||
let mut chunks = &chunks[..];
|
||||
if has_selection {
|
||||
chunks = &chunks[1..];
|
||||
}
|
||||
chunks.iter().step_by(2).map(|c| c.width).collect()
|
||||
}
|
||||
|
||||
fn get_row_bounds(
|
||||
&self,
|
||||
selected: Option<usize>,
|
||||
offset: usize,
|
||||
max_height: u16,
|
||||
) -> (usize, usize) {
|
||||
let offset = offset.min(self.rows.len().saturating_sub(1));
|
||||
let mut start = offset;
|
||||
let mut end = offset;
|
||||
let mut height = 0;
|
||||
for item in self.rows.iter().skip(offset) {
|
||||
if height + item.height > max_height {
|
||||
break;
|
||||
}
|
||||
height += item.total_height();
|
||||
end += 1;
|
||||
}
|
||||
|
||||
let selected = selected.unwrap_or(0).min(self.rows.len() - 1);
|
||||
while selected >= end {
|
||||
height = height.saturating_add(self.rows[end].total_height());
|
||||
end += 1;
|
||||
while height > max_height {
|
||||
height = height.saturating_sub(self.rows[start].total_height());
|
||||
start += 1;
|
||||
}
|
||||
}
|
||||
while selected < start {
|
||||
start -= 1;
|
||||
height = height.saturating_add(self.rows[start].total_height());
|
||||
while height > max_height {
|
||||
end -= 1;
|
||||
height = height.saturating_sub(self.rows[end].total_height());
|
||||
}
|
||||
}
|
||||
(start, end)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'i, T, H, I, D, R> Widget for Table<'a, 'i, T, H, I, D, R>
|
||||
where
|
||||
T: Display,
|
||||
H: Iterator<Item = T>,
|
||||
I: Display,
|
||||
D: Iterator<Item = I>,
|
||||
R: Iterator<Item = Row<'i, D, I>>,
|
||||
{
|
||||
fn draw(&mut self, area: &Rect, buf: &mut Buffer) {
|
||||
// Render block if necessary and get the drawing area
|
||||
let table_area = match self.block {
|
||||
Some(ref mut b) => {
|
||||
b.draw(area, buf);
|
||||
b.inner(area)
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct TableState {
|
||||
offset: usize,
|
||||
selected: Option<usize>,
|
||||
}
|
||||
|
||||
impl TableState {
|
||||
pub fn offset(&self) -> usize {
|
||||
self.offset
|
||||
}
|
||||
|
||||
pub fn offset_mut(&mut self) -> &mut usize {
|
||||
&mut self.offset
|
||||
}
|
||||
|
||||
pub fn with_selected(mut self, selected: Option<usize>) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_offset(mut self, offset: usize) -> Self {
|
||||
self.offset = offset;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn selected(&self) -> Option<usize> {
|
||||
self.selected
|
||||
}
|
||||
|
||||
pub fn select(&mut self, index: Option<usize>) {
|
||||
self.selected = index;
|
||||
if index.is_none() {
|
||||
self.offset = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> StatefulWidget for Table<'a> {
|
||||
type State = TableState;
|
||||
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
if area.area() == 0 {
|
||||
return;
|
||||
}
|
||||
buf.set_style(area, self.style);
|
||||
let table_area = match self.block.take() {
|
||||
Some(b) => {
|
||||
let inner_area = b.inner(area);
|
||||
b.render(area, buf);
|
||||
inner_area
|
||||
}
|
||||
None => *area,
|
||||
None => area,
|
||||
};
|
||||
|
||||
// Set the background
|
||||
self.background(&table_area, buf, self.style.bg);
|
||||
|
||||
// Save widths of the columns that will fit in the given area
|
||||
let mut x = 0;
|
||||
let mut widths = Vec::with_capacity(self.widths.len());
|
||||
for width in self.widths.iter() {
|
||||
if x + width < table_area.width {
|
||||
widths.push(*width);
|
||||
}
|
||||
x += *width;
|
||||
}
|
||||
|
||||
let mut y = table_area.top();
|
||||
let has_selection = state.selected.is_some();
|
||||
let columns_widths = self.get_columns_widths(table_area.width, has_selection);
|
||||
let highlight_symbol = self.highlight_symbol.unwrap_or("");
|
||||
let blank_symbol = " ".repeat(highlight_symbol.width());
|
||||
let mut current_height = 0;
|
||||
let mut rows_height = table_area.height;
|
||||
|
||||
// Draw header
|
||||
if y < table_area.bottom() {
|
||||
x = table_area.left();
|
||||
for (w, t) in widths.iter().zip(self.header.by_ref()) {
|
||||
buf.set_string(x, y, &format!("{}", t), &self.header_style);
|
||||
x += *w + self.column_spacing;
|
||||
if let Some(ref header) = self.header {
|
||||
let max_header_height = table_area.height.min(header.total_height());
|
||||
buf.set_style(
|
||||
Rect {
|
||||
x: table_area.left(),
|
||||
y: table_area.top(),
|
||||
width: table_area.width,
|
||||
height: table_area.height.min(header.height),
|
||||
},
|
||||
header.style,
|
||||
);
|
||||
let mut col = table_area.left();
|
||||
if has_selection {
|
||||
col += (highlight_symbol.width() as u16).min(table_area.width);
|
||||
}
|
||||
for (width, cell) in columns_widths.iter().zip(header.cells.iter()) {
|
||||
render_cell(
|
||||
buf,
|
||||
cell,
|
||||
Rect {
|
||||
x: col,
|
||||
y: table_area.top(),
|
||||
width: *width,
|
||||
height: max_header_height,
|
||||
},
|
||||
);
|
||||
col += *width + self.column_spacing;
|
||||
}
|
||||
current_height += max_header_height;
|
||||
rows_height = rows_height.saturating_sub(max_header_height);
|
||||
}
|
||||
y += 2;
|
||||
|
||||
// Draw rows
|
||||
let default_style = Style::default();
|
||||
if y < table_area.bottom() {
|
||||
let remaining = (table_area.bottom() - y) as usize;
|
||||
for (i, row) in self.rows.by_ref().take(remaining).enumerate() {
|
||||
let (data, style) = match row {
|
||||
Row::Data(d) => (d, &default_style),
|
||||
Row::StyledData(d, s) => (d, s),
|
||||
if self.rows.is_empty() {
|
||||
return;
|
||||
}
|
||||
let (start, end) = self.get_row_bounds(state.selected, state.offset, rows_height);
|
||||
state.offset = start;
|
||||
for (i, table_row) in self
|
||||
.rows
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.skip(state.offset)
|
||||
.take(end - start)
|
||||
{
|
||||
let (row, col) = (table_area.top() + current_height, table_area.left());
|
||||
current_height += table_row.total_height();
|
||||
let table_row_area = Rect {
|
||||
x: col,
|
||||
y: row,
|
||||
width: table_area.width,
|
||||
height: table_row.height,
|
||||
};
|
||||
buf.set_style(table_row_area, table_row.style);
|
||||
let is_selected = state.selected.map_or(false, |s| s == i);
|
||||
let table_row_start_col = if has_selection {
|
||||
let symbol = if is_selected {
|
||||
highlight_symbol
|
||||
} else {
|
||||
&blank_symbol
|
||||
};
|
||||
x = table_area.left();
|
||||
for (w, elt) in widths.iter().zip(data) {
|
||||
buf.set_stringn(x, y + i as u16, &format!("{}", elt), *w as usize, style);
|
||||
x += *w + self.column_spacing;
|
||||
}
|
||||
let (col, _) =
|
||||
buf.set_stringn(col, row, symbol, table_area.width as usize, table_row.style);
|
||||
col
|
||||
} else {
|
||||
col
|
||||
};
|
||||
let mut col = table_row_start_col;
|
||||
for (width, cell) in columns_widths.iter().zip(table_row.cells.iter()) {
|
||||
render_cell(
|
||||
buf,
|
||||
cell,
|
||||
Rect {
|
||||
x: col,
|
||||
y: row,
|
||||
width: *width,
|
||||
height: table_row.height,
|
||||
},
|
||||
);
|
||||
col += *width + self.column_spacing;
|
||||
}
|
||||
if is_selected {
|
||||
buf.set_style(table_row_area, self.highlight_style);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_cell(buf: &mut Buffer, cell: &Cell, area: Rect) {
|
||||
buf.set_style(area, cell.style);
|
||||
for (i, line) in cell.content.lines.iter().enumerate() {
|
||||
if i as u16 >= area.height {
|
||||
break;
|
||||
}
|
||||
buf.set_line(area.x, area.y + i as u16, line, area.width);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Table<'a> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let mut state = TableState::default();
|
||||
StatefulWidget::render(self, area, buf, &mut state);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn table_invalid_percentages() {
|
||||
Table::new(vec![]).widths(&[Constraint::Percentage(110)]);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user