Compare commits
851 Commits
feat-inlin
...
jm/modular
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26163effdf | ||
|
|
bc10af5931 | ||
|
|
784f67a912 | ||
|
|
f4880b40cc | ||
|
|
67c0ea243b | ||
|
|
b9653ba05a | ||
|
|
9875d9facc | ||
|
|
b88717b65f | ||
|
|
5635b930c7 | ||
|
|
870bc6a64a | ||
|
|
da821b431e | ||
|
|
68886d1787 | ||
|
|
0f48239778 | ||
|
|
b13e2f9473 | ||
|
|
c777beb658 | ||
|
|
20c88aaa5b | ||
|
|
e02947be61 | ||
|
|
3a90e2a761 | ||
|
|
65da535745 | ||
|
|
9ed85fd1dd | ||
|
|
aed60b9839 | ||
|
|
3631b34f53 | ||
|
|
0d5f3c091f | ||
|
|
ed51c4b342 | ||
|
|
23516bce76 | ||
|
|
6d1bd99544 | ||
|
|
2fb0b8a741 | ||
|
|
0256269a7f | ||
|
|
fdd5d8c092 | ||
|
|
8b624f5952 | ||
|
|
57d8b742e5 | ||
|
|
d5477b50d5 | ||
|
|
730dfd4940 | ||
|
|
097ee86e39 | ||
|
|
3fdb5e8987 | ||
|
|
ec88bb81e5 | ||
|
|
f04bf855cb | ||
|
|
4753b7241b | ||
|
|
36fa3c11c1 | ||
|
|
69e8ed7db8 | ||
|
|
5f7a7fbe19 | ||
|
|
e6d2e04bcf | ||
|
|
45fcab7497 | ||
|
|
1b9bdd425c | ||
|
|
c68ee6c64a | ||
|
|
afe15349c8 | ||
|
|
fe4eeab676 | ||
|
|
a23ecd9b45 | ||
|
|
bb71e5ffd4 | ||
|
|
f2fa1ae9aa | ||
|
|
f687af7c0d | ||
|
|
f97e07c08a | ||
|
|
bb68bc6968 | ||
|
|
ffeb8e46b9 | ||
|
|
2fd5ae64bf | ||
|
|
82b70fd329 | ||
|
|
94328a2977 | ||
|
|
d468463fc6 | ||
|
|
6e7b4e4d55 | ||
|
|
864cd9ffef | ||
|
|
c08b522d34 | ||
|
|
716c93136e | ||
|
|
5eeb1ccbc4 | ||
|
|
f77503050f | ||
|
|
cd0d31c2dc | ||
|
|
41a910004d | ||
|
|
8433d0958b | ||
|
|
8d4a1026ab | ||
|
|
edc2af9822 | ||
|
|
a80a8a6a47 | ||
|
|
29c8c84fd0 | ||
|
|
c2d38509b4 | ||
|
|
b70cd03c02 | ||
|
|
476ac87c99 | ||
|
|
8857037bff | ||
|
|
e707ff11d1 | ||
|
|
a9fe4284ac | ||
|
|
ffc4300558 | ||
|
|
84cb16483a | ||
|
|
5b89bd04a8 | ||
|
|
32d0695cc2 | ||
|
|
cd93547db8 | ||
|
|
c245c13cc1 | ||
|
|
b2aa843b31 | ||
|
|
3ca920e881 | ||
|
|
b344f95b7c | ||
|
|
b304bb99bd | ||
|
|
be3eb75ea5 | ||
|
|
efef0d0dc0 | ||
|
|
663486f1e8 | ||
|
|
7ddfbc0010 | ||
|
|
3725262ca3 | ||
|
|
84f334163b | ||
|
|
03f3124c1d | ||
|
|
c34fb77818 | ||
|
|
6ce447c4f3 | ||
|
|
272d0591a7 | ||
|
|
e81663bec0 | ||
|
|
7e1bab049b | ||
|
|
379dab9cdb | ||
|
|
5b51018501 | ||
|
|
7bab9f0d80 | ||
|
|
6d210b3b6b | ||
|
|
935a7187c2 | ||
|
|
3bb374df88 | ||
|
|
50e5674a20 | ||
|
|
810da72f26 | ||
|
|
7c0665cb0e | ||
|
|
ccf83e6d76 | ||
|
|
60bd7f4814 | ||
|
|
55e0880d2f | ||
|
|
3e7458fdb8 | ||
|
|
32a0b26525 | ||
|
|
36d49e549b | ||
|
|
0a18dcb329 | ||
|
|
7ef2daee06 | ||
|
|
46977d8851 | ||
|
|
38bb196404 | ||
|
|
1908b06b4a | ||
|
|
3f2f2cd6ab | ||
|
|
efa965e1e8 | ||
|
|
127d706ee4 | ||
|
|
1365620606 | ||
|
|
cd64367e24 | ||
|
|
07efde5233 | ||
|
|
308c1df649 | ||
|
|
4bfdc15b80 | ||
|
|
7d175f85c1 | ||
|
|
d370aa75af | ||
|
|
7fdccafd52 | ||
|
|
10d778866e | ||
|
|
e6871b9e21 | ||
|
|
4f307e69db | ||
|
|
7f3efb02e6 | ||
|
|
7b45f74b71 | ||
|
|
1520ed9d10 | ||
|
|
2a74f9d8c1 | ||
|
|
d7ed6c8bad | ||
|
|
313135c68e | ||
|
|
8061813f32 | ||
|
|
74a32afbae | ||
|
|
4ce67fc84e | ||
|
|
df4b706674 | ||
|
|
8b447ec4d6 | ||
|
|
7a48c5b11b | ||
|
|
8cfc316bcc | ||
|
|
2f8a9363fc | ||
|
|
d92997105b | ||
|
|
42cda6d287 | ||
|
|
4f7791079e | ||
|
|
cf67ed9b88 | ||
|
|
8a60a561c9 | ||
|
|
35941809e1 | ||
|
|
73fd367a74 | ||
|
|
1de9a82b7a | ||
|
|
d6587bc6b0 | ||
|
|
f429f688da | ||
|
|
4770e71581 | ||
|
|
eef1afe915 | ||
|
|
257db6257f | ||
|
|
70df102de0 | ||
|
|
bf2036987f | ||
|
|
0b5fd6bf8e | ||
|
|
fadc73d62e | ||
|
|
fcb5d589bb | ||
|
|
4955380932 | ||
|
|
828d17a3f5 | ||
|
|
9bd89c218a | ||
|
|
2cfe82a47e | ||
|
|
1a4bb1cbb8 | ||
|
|
839cca20bf | ||
|
|
f945a0bcff | ||
|
|
eb281df974 | ||
|
|
28e81c0714 | ||
|
|
76e5fe5a9a | ||
|
|
366cbae09f | ||
|
|
ec763af851 | ||
|
|
699c2d7c8d | ||
|
|
3cc29bdada | ||
|
|
8de3d52469 | ||
|
|
b30411d1c7 | ||
|
|
aa4260f92c | ||
|
|
5f1e119563 | ||
|
|
4d1784f2de | ||
|
|
baedc39494 | ||
|
|
366c2a0e6d | ||
|
|
bef2bc1e7c | ||
|
|
64eb3913a4 | ||
|
|
e95230beda | ||
|
|
f4637d40c3 | ||
|
|
da1ade7b2e | ||
|
|
1706b0a3e4 | ||
|
|
4392759501 | ||
|
|
20fc0ddfca | ||
|
|
3687f78f6a | ||
|
|
5fbb77ad20 | ||
|
|
97ee102f17 | ||
|
|
c442dfd1ad | ||
|
|
0a164965ea | ||
|
|
bf0923473c | ||
|
|
81b96338ea | ||
|
|
2e71c1874e | ||
|
|
c75aa1990f | ||
|
|
326a461f9a | ||
|
|
f3172c59d4 | ||
|
|
bef5bcf750 | ||
|
|
11264787d0 | ||
|
|
9b3c260b76 | ||
|
|
363c4c54e8 | ||
|
|
b7778e5cd1 | ||
|
|
b5061c5250 | ||
|
|
359204c929 | ||
|
|
14461c3a35 | ||
|
|
3b002fdcab | ||
|
|
0207160784 | ||
|
|
26af65043e | ||
|
|
07da90a718 | ||
|
|
125ee929ee | ||
|
|
742a5ead06 | ||
|
|
8719608bda | ||
|
|
f6c4e447e6 | ||
|
|
c56f49b9fb | ||
|
|
078e97e4ff | ||
|
|
8e68db9e2f | ||
|
|
541f0f9953 | ||
|
|
88bfb5a430 | ||
|
|
c4ce7e8ff6 | ||
|
|
3be189e3c6 | ||
|
|
5c4efacd1d | ||
|
|
bbb6d65e06 | ||
|
|
fdb14dc7cd | ||
|
|
9b3b23ac14 | ||
|
|
58b6e0be0f | ||
|
|
6e6ba27a12 | ||
|
|
c870a41057 | ||
|
|
a6036ad789 | ||
|
|
060d26b6dc | ||
|
|
a4e84a6a7f | ||
|
|
fcbea9ee68 | ||
|
|
14b24e7585 | ||
|
|
5ed1f43c62 | ||
|
|
c8c7924e0c | ||
|
|
e3afe7c8a1 | ||
|
|
a1f54de7d6 | ||
|
|
b8ea190bf2 | ||
|
|
0de5238ed3 | ||
|
|
df5dddfbc9 | ||
|
|
f1398ae6cb | ||
|
|
525848ff4e | ||
|
|
660c7183c7 | ||
|
|
ab951fae81 | ||
|
|
3cd4369176 | ||
|
|
9bc014d7f1 | ||
|
|
36a0cd56e5 | ||
|
|
b831c5688c | ||
|
|
8195f526cb | ||
|
|
f7f66928a8 | ||
|
|
01418eb7c2 | ||
|
|
8536760e78 | ||
|
|
a558b19c9a | ||
|
|
183c07ef43 | ||
|
|
a13867ffce | ||
|
|
5b00e3aae9 | ||
|
|
38c17e091c | ||
|
|
3834374652 | ||
|
|
27680c05ce | ||
|
|
6fd5f631bb | ||
|
|
37b957c7e1 | ||
|
|
e02f4768ce | ||
|
|
c12bcfefa2 | ||
|
|
94f4547dcf | ||
|
|
3a6b8808ed | ||
|
|
1cff511934 | ||
|
|
b5bdde079e | ||
|
|
654949bb00 | ||
|
|
943c0431d9 | ||
|
|
65e7923753 | ||
|
|
d0067c8815 | ||
|
|
35e971f7eb | ||
|
|
b0314c5731 | ||
|
|
12f67e810f | ||
|
|
11b452d56f | ||
|
|
efd1e47642 | ||
|
|
410d08b2b5 | ||
|
|
a4892ad444 | ||
|
|
18870ce990 | ||
|
|
1f208ffd03 | ||
|
|
e51ca6e0d2 | ||
|
|
9182f47026 | ||
|
|
91040c0865 | ||
|
|
2202059259 | ||
|
|
8fb46301a0 | ||
|
|
0dcdbea083 | ||
|
|
74a051147a | ||
|
|
c3fb25898f | ||
|
|
fae5862c6e | ||
|
|
788e6d9fb8 | ||
|
|
14c67fbb52 | ||
|
|
096346350e | ||
|
|
61a827821d | ||
|
|
fbb5dfaaa9 | ||
|
|
d2d91f754c | ||
|
|
bcf43688ec | ||
|
|
b7942ee252 | ||
|
|
c8dd87918d | ||
|
|
652dc469ea | ||
|
|
87bf1dd9df | ||
|
|
f8367fdfdd | ||
|
|
9ba7354335 | ||
|
|
dab08b99b6 | ||
|
|
2a12f7bddf | ||
|
|
4278b4088d | ||
|
|
86168aa711 | ||
|
|
78f1c1446b | ||
|
|
9ec43eff1c | ||
|
|
4ee4e6d78a | ||
|
|
525479546a | ||
|
|
cf861232c7 | ||
|
|
dd5ca3a0c8 | ||
|
|
aeec16369b | ||
|
|
eb31617539 | ||
|
|
1ad629acd7 | ||
|
|
04e6ccd448 | ||
|
|
540fd2df03 | ||
|
|
984afd580b | ||
|
|
bbcfa55a88 | ||
|
|
1cbe1f52ab | ||
|
|
663bbde9c3 | ||
|
|
27e9216cea | ||
|
|
be4fdaa0c7 | ||
|
|
c1ed5c3637 | ||
|
|
4b8e54e811 | ||
|
|
5b7ad2ad82 | ||
|
|
ba20372c23 | ||
|
|
f383625f0e | ||
|
|
847bacf32e | ||
|
|
815757fcbb | ||
|
|
736605ec88 | ||
|
|
6e76729ce8 | ||
|
|
7f42ec9713 | ||
|
|
d7132011f9 | ||
|
|
d726e928d2 | ||
|
|
b80264de87 | ||
|
|
804c841fdc | ||
|
|
79ceb9f7b6 | ||
|
|
f780be31f3 | ||
|
|
eb1484b6db | ||
|
|
6ecaeed549 | ||
|
|
a489d85f2d | ||
|
|
065b6b05b7 | ||
|
|
1e755967c5 | ||
|
|
1d3fbc1b15 | ||
|
|
330a899eac | ||
|
|
405a125c82 | ||
|
|
b3a57f3dff | ||
|
|
2819eea82b | ||
|
|
41de8846fd | ||
|
|
68d5783a69 | ||
|
|
d49bbb2590 | ||
|
|
fd4703c086 | ||
|
|
0df935473f | ||
|
|
9df6cebb58 | ||
|
|
f299463847 | ||
|
|
4d262d21cb | ||
|
|
ae6a2b0007 | ||
|
|
f71bf18297 | ||
|
|
cddf4b2930 | ||
|
|
813f707892 | ||
|
|
48b0380cb3 | ||
|
|
c959bd2881 | ||
|
|
5131c813ce | ||
|
|
11e4f6a0ba | ||
|
|
cc6737b8bc | ||
|
|
3e7810a2ab | ||
|
|
fc0879f98d | ||
|
|
bb5444f618 | ||
|
|
e0aa6c5e1f | ||
|
|
1746a61659 | ||
|
|
7a8af8da6b | ||
|
|
dfd6db988f | ||
|
|
151db6ac7d | ||
|
|
de97a1f1da | ||
|
|
9a3815b66d | ||
|
|
425a65140b | ||
|
|
fe06f0c7b0 | ||
|
|
43b2b57191 | ||
|
|
50b81c9d4e | ||
|
|
2b4aa46a6a | ||
|
|
e1e85aa7af | ||
|
|
bf67850739 | ||
|
|
1561d64c80 | ||
|
|
ffd5fc79fc | ||
|
|
34648941d4 | ||
|
|
c69ca47922 | ||
|
|
f29c73fb1c | ||
|
|
f2eab71ccf | ||
|
|
6645d2e058 | ||
|
|
2faa879658 | ||
|
|
eb79256cee | ||
|
|
388aa467f1 | ||
|
|
8dd177a051 | ||
|
|
bbf2f906fb | ||
|
|
c24216cf30 | ||
|
|
5db895dcbf | ||
|
|
c50ff08a63 | ||
|
|
06141900b4 | ||
|
|
fab943b61a | ||
|
|
a67815e138 | ||
|
|
bd6b91c958 | ||
|
|
5aba988fac | ||
|
|
e64e194b6b | ||
|
|
bc274e2bd9 | ||
|
|
f13fd73d9e | ||
|
|
fe84141119 | ||
|
|
fb93db0730 | ||
|
|
23f6938498 | ||
|
|
98bcf1c0a5 | ||
|
|
803a72df27 | ||
|
|
0494ee52f1 | ||
|
|
6d15b2570f | ||
|
|
659460e19c | ||
|
|
ba036cd579 | ||
|
|
8724aeb9e7 | ||
|
|
da6c299804 | ||
|
|
50374b2456 | ||
|
|
49df5d4626 | ||
|
|
7ab12ed8ce | ||
|
|
b459228e26 | ||
|
|
8f56fabcdd | ||
|
|
a62632a947 | ||
|
|
f025d2bfa2 | ||
|
|
63645333d6 | ||
|
|
5d410c6895 | ||
|
|
8d77b734bb | ||
|
|
9574198958 | ||
|
|
ee54493163 | ||
|
|
c977293f14 | ||
|
|
b0ed658970 | ||
|
|
37c183636b | ||
|
|
e67d3c64e0 | ||
|
|
4f2db82a77 | ||
|
|
d6b851301e | ||
|
|
b7a479392e | ||
|
|
e1cc849554 | ||
|
|
7f5884829c | ||
|
|
a15c3b2660 | ||
|
|
41c44a4af6 | ||
|
|
1b8b6261e2 | ||
|
|
5bf4f52119 | ||
|
|
f4c8de041d | ||
|
|
910ad00059 | ||
|
|
b282a06932 | ||
|
|
b8f71c0d6e | ||
|
|
113b4b7a4e | ||
|
|
b82451fb33 | ||
|
|
4be18aba8b | ||
|
|
ebf1f42942 | ||
|
|
2169a0da01 | ||
|
|
d118565ef6 | ||
|
|
aaeba2709c | ||
|
|
d19b266e0e | ||
|
|
f767ea7d37 | ||
|
|
0576a8aa32 | ||
|
|
03401cd46e | ||
|
|
f69d57c3b5 | ||
|
|
2a87251152 | ||
|
|
aef495604c | ||
|
|
8bfd6661e2 | ||
|
|
3ec4e24d00 | ||
|
|
7ced7c0aa3 | ||
|
|
dd22e721e3 | ||
|
|
4424637af2 | ||
|
|
37c70dbb8e | ||
|
|
91c67eb100 | ||
|
|
e49385b78c | ||
|
|
6b2efd0f6c | ||
|
|
34d099c99a | ||
|
|
987f7eed4c | ||
|
|
e4579f0db2 | ||
|
|
6a6e9dde9d | ||
|
|
28ac55bc62 | ||
|
|
458fa90362 | ||
|
|
56fc410105 | ||
|
|
753e246531 | ||
|
|
211160ca16 | ||
|
|
1229b96e42 | ||
|
|
fe632d70cb | ||
|
|
c862aa5e9e | ||
|
|
18e19f6ce6 | ||
|
|
7ef0afcb62 | ||
|
|
1e2f0be75a | ||
|
|
a58cce2dba | ||
|
|
ffa78aa67c | ||
|
|
7cbb1060ac | ||
|
|
a05541358e | ||
|
|
1f88da7538 | ||
|
|
36d8c53645 | ||
|
|
ec7b3872b4 | ||
|
|
edacaf7ff4 | ||
|
|
df0eb1f8e9 | ||
|
|
59b9c32fbc | ||
|
|
9f37100096 | ||
|
|
a2f2bd5df5 | ||
|
|
c597b87f72 | ||
|
|
82a0d01a42 | ||
|
|
0e573cd6c7 | ||
|
|
b07000835f | ||
|
|
c6c3f88a79 | ||
|
|
a20bd6adb5 | ||
|
|
5213f78d25 | ||
|
|
12f92911c7 | ||
|
|
ad2dc5646d | ||
|
|
6cbdb06fd8 | ||
|
|
27c5637675 | ||
|
|
0c52ff431a | ||
|
|
8d507c43fa | ||
|
|
b61f65bc20 | ||
|
|
3a57e76ed1 | ||
|
|
6c7bef8d11 | ||
|
|
88ae3485c2 | ||
|
|
e5caf170c8 | ||
|
|
089f8ba66a | ||
|
|
fbf1a451c8 | ||
|
|
4541336514 | ||
|
|
346e7b4f4d | ||
|
|
15641c8475 | ||
|
|
2fd85af33c | ||
|
|
401a7a7f71 | ||
|
|
e35e4135c9 | ||
|
|
8ae4403b63 | ||
|
|
11076d0af3 | ||
|
|
9cfb133a98 | ||
|
|
4548a9b7e2 | ||
|
|
61af0d9906 | ||
|
|
c0991cc576 | ||
|
|
301366c4fa | ||
|
|
3bda372847 | ||
|
|
082cbcbc50 | ||
|
|
cbf86da0e7 | ||
|
|
32e461953c | ||
|
|
d67fa2c00d | ||
|
|
c3155a2489 | ||
|
|
c5ea656385 | ||
|
|
be55a5fbcd | ||
|
|
21303f2167 | ||
|
|
c9b8e7cf41 | ||
|
|
5498a889ae | ||
|
|
0fe738500c | ||
|
|
dd9a8df03a | ||
|
|
0c7d547db1 | ||
|
|
638d596a3b | ||
|
|
d4976d4b63 | ||
|
|
a7bf4b3f36 | ||
|
|
94af2a29e1 | ||
|
|
42f816999e | ||
|
|
1414fbcc05 | ||
|
|
1947c58c60 | ||
|
|
1e20475061 | ||
|
|
af36282df5 | ||
|
|
322e46f15d | ||
|
|
983ea7f7a5 | ||
|
|
384e616231 | ||
|
|
6b8725f091 | ||
|
|
17797d83da | ||
|
|
ebd3680a47 | ||
|
|
74c5244be1 | ||
|
|
3cf0b83bda | ||
|
|
127813120e | ||
|
|
0c68ebed4f | ||
|
|
232be80325 | ||
|
|
c95a75c5d5 | ||
|
|
c8ab2d5908 | ||
|
|
572df758ba | ||
|
|
b996102837 | ||
|
|
080a05bbd3 | ||
|
|
343c6cdc47 | ||
|
|
5c785b2270 | ||
|
|
ca9bcd3156 | ||
|
|
82b40be4ab | ||
|
|
e098731d6c | ||
|
|
5f6aa30be5 | ||
|
|
ea70bffe5d | ||
|
|
47ae602df4 | ||
|
|
878b6fc258 | ||
|
|
0696f484e8 | ||
|
|
927a5d8251 | ||
|
|
28e7fd4bc5 | ||
|
|
28c61571e8 | ||
|
|
d0779034e7 | ||
|
|
51fdcbe7e9 | ||
|
|
eda2fb7077 | ||
|
|
3f781cad0a | ||
|
|
fc727df7d2 | ||
|
|
47fe4ad69f | ||
|
|
7a70602ec6 | ||
|
|
14eb6b6979 | ||
|
|
6009844e25 | ||
|
|
8b36683571 | ||
|
|
e9bd736b1a | ||
|
|
a890f2ac00 | ||
|
|
b35f19ec44 | ||
|
|
ad3413eeec | ||
|
|
f0716edbcf | ||
|
|
fc9f637fb0 | ||
|
|
292a11d81e | ||
|
|
ad4d6e7dec | ||
|
|
e4bcf78afa | ||
|
|
d0ee04a69f | ||
|
|
6d6eceeb88 | ||
|
|
0dca6a689a | ||
|
|
a937500ae4 | ||
|
|
80fd77e476 | ||
|
|
98155dce25 | ||
|
|
1ba2246d95 | ||
|
|
57ea871753 | ||
|
|
61533712be | ||
|
|
dc552116cf | ||
|
|
ab5e616635 | ||
|
|
b6b2da5eb7 | ||
|
|
89ef0e29f5 | ||
|
|
4cd843eda9 | ||
|
|
d2429bc3e4 | ||
|
|
b090101b23 | ||
|
|
56455e0fee | ||
|
|
f4ed3b7584 | ||
|
|
c86924b925 | ||
|
|
de25de0a95 | ||
|
|
ea48af1c9a | ||
|
|
418ed20479 | ||
|
|
519509945b | ||
|
|
8c55158822 | ||
|
|
7748720963 | ||
|
|
4d70169bef | ||
|
|
10dbd6f207 | ||
|
|
778c320008 | ||
|
|
268bbed17e | ||
|
|
f63ac72305 | ||
|
|
3293c6b80b | ||
|
|
149d48919d | ||
|
|
8c4a2e0fbf | ||
|
|
664fb4cffd | ||
|
|
6ad4bd4cf2 | ||
|
|
37fa6abe9d | ||
|
|
8b28672131 | ||
|
|
de9f52ff2c | ||
|
|
c8ddc164c7 | ||
|
|
e18393dbc6 | ||
|
|
aad164a531 | ||
|
|
3a37d2f6ed | ||
|
|
8cd3205d70 | ||
|
|
e82521ea79 | ||
|
|
9191ad60fd | ||
|
|
49a82e062f | ||
|
|
181706c564 | ||
|
|
554805d6cb | ||
|
|
1727fa5120 | ||
|
|
440f62ff54 | ||
|
|
6f659cfb07 | ||
|
|
bf4944683d | ||
|
|
7539f775fe | ||
|
|
8db9fb4aeb | ||
|
|
d05ab6fb70 | ||
|
|
2920e045ba | ||
|
|
add578a7d6 | ||
|
|
60a4131384 | ||
|
|
964190a859 | ||
|
|
b9290b35d1 | ||
|
|
daf5890152 | ||
|
|
7e37a96678 | ||
|
|
bcb7417785 | ||
|
|
9c956733f7 | ||
|
|
13fb11a62c | ||
|
|
0fb1ed85c6 | ||
|
|
e2cb11cc30 | ||
|
|
c3f87f245a | ||
|
|
df90982632 | ||
|
|
bb061fdab6 | ||
|
|
1ff85535c8 | ||
|
|
33f3212cbf | ||
|
|
fb6d4b2f51 | ||
|
|
446efae185 | ||
|
|
b347201b9f | ||
|
|
9f1f59a51c | ||
|
|
6f6c355c5c | ||
|
|
60150f6236 | ||
|
|
2889c7d084 | ||
|
|
57678a5fe8 | ||
|
|
ae8ed8867d | ||
|
|
e66d5cdee0 | ||
|
|
804115ac6f | ||
|
|
a1813af297 | ||
|
|
085fde7d4a | ||
|
|
860a40c13a | ||
|
|
0833c9018b | ||
|
|
f7c4b44962 | ||
|
|
56e44a0efa | ||
|
|
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 |
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)"
|
||||
17
.editorconfig
Normal file
17
.editorconfig
Normal file
@@ -0,0 +1,17 @@
|
||||
# configuration for https://editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.rs]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.yml]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
11
.github/CODEOWNERS
vendored
Normal file
11
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
# 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
|
||||
|
||||
* @ratatui/maintainers
|
||||
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -2,12 +2,12 @@
|
||||
name: Bug report
|
||||
about: Create an issue about a bug you encountered
|
||||
title: ''
|
||||
labels: bug
|
||||
labels: 'Type: Bug'
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
<!--
|
||||
Hi there, sorry `tui` is not working as expected.
|
||||
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.
|
||||
-->
|
||||
@@ -17,26 +17,22 @@ A detailed and complete issue is more likely to be processed quickly.
|
||||
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:
|
||||
|
||||
13
.github/ISSUE_TEMPLATE/config.yml
vendored
13
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1 +1,14 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Frequently Asked Questions
|
||||
url: https://ratatui.rs/faq/
|
||||
about: Check the website FAQ section to see if your question has already been answered
|
||||
- name: Ratatui Forum
|
||||
url: https://forum.ratatui.rs
|
||||
about: Ask questions about ratatui on our Forum
|
||||
- name: Discord Chat
|
||||
url: https://discord.gg/pMCEU9hNEj
|
||||
about: Ask questions about ratatui on Discord
|
||||
- name: Matrix Chat
|
||||
url: https://matrix.to/#/#ratatui:matrix.org
|
||||
about: Ask questions about ratatui on Matrix
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -2,7 +2,7 @@
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
labels: 'Type: Enhancement'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
18
.github/dependabot.yml
vendored
Normal file
18
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
# Maintain dependencies for Cargo
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
# Maintain dependencies for GitHub Actions
|
||||
- package-ecosystem: github-actions
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 10
|
||||
18
.github/pull_request_template.md
vendored
18
.github/pull_request_template.md
vendored
@@ -1,17 +1 @@
|
||||
## Description
|
||||
<!--
|
||||
A clear and concise description of what this PR changes.
|
||||
-->
|
||||
|
||||
## Testing guidelines
|
||||
<!--
|
||||
A clear and concise description of how the changes can be tested.
|
||||
For example, you can include a command to run the relevant tests or examples.
|
||||
You can also include screenshots of the expected behavior.
|
||||
-->
|
||||
|
||||
## Checklist
|
||||
|
||||
* [ ] I have read the [contributing guidelines](../CONTRIBUTING.md).
|
||||
* [ ] I have added relevant tests.
|
||||
* [ ] I have documented all new additions.
|
||||
<!-- Please read CONTRIBUTING.md before submitting any pull request. -->
|
||||
|
||||
25
.github/workflows/bench_base.yml
vendored
Normal file
25
.github/workflows/bench_base.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Run Benchmarks
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
benchmark_base_branch:
|
||||
name: Continuous Benchmarking with Bencher
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- uses: bencherdev/bencher@main
|
||||
- name: Track base branch benchmarks with Bencher
|
||||
run: |
|
||||
bencher run \
|
||||
--project ratatui-org \
|
||||
--token '${{ secrets.BENCHER_API_TOKEN }}' \
|
||||
--branch main \
|
||||
--testbed ubuntu-latest \
|
||||
--adapter rust_criterion \
|
||||
--err \
|
||||
cargo bench
|
||||
25
.github/workflows/bench_run_fork_pr.yml
vendored
Normal file
25
.github/workflows/bench_run_fork_pr.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Run and Cache Benchmarks
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, edited, synchronize]
|
||||
|
||||
jobs:
|
||||
benchmark_fork_pr_branch:
|
||||
name: Run Fork PR Benchmarks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Run Benchmarks
|
||||
run: cargo bench > benchmark_results.txt
|
||||
- name: Upload Benchmark Results
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: benchmark_results.txt
|
||||
path: ./benchmark_results.txt
|
||||
- name: Upload GitHub Pull Request Event
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: event.json
|
||||
path: ${{ github.event_path }}
|
||||
75
.github/workflows/bench_track_fork_pr.yml
vendored
Normal file
75
.github/workflows/bench_track_fork_pr.yml
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
name: Track Benchmarks with Bencher
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [Run and Cache Benchmarks]
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
track_fork_pr_branch:
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
BENCHMARK_RESULTS: benchmark_results.txt
|
||||
PR_EVENT: event.json
|
||||
steps:
|
||||
- name: Download Benchmark Results
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
async function downloadArtifact(artifactName) {
|
||||
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: context.payload.workflow_run.id,
|
||||
});
|
||||
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
|
||||
return artifact.name == artifactName
|
||||
})[0];
|
||||
if (!matchArtifact) {
|
||||
core.setFailed(`Failed to find artifact: ${artifactName}`);
|
||||
}
|
||||
let download = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: matchArtifact.id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
let fs = require('fs');
|
||||
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/${artifactName}.zip`, Buffer.from(download.data));
|
||||
}
|
||||
await downloadArtifact(process.env.BENCHMARK_RESULTS);
|
||||
await downloadArtifact(process.env.PR_EVENT);
|
||||
- name: Unzip Benchmark Results
|
||||
run: |
|
||||
unzip $BENCHMARK_RESULTS.zip
|
||||
unzip $PR_EVENT.zip
|
||||
- name: Export PR Event Data
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
let fs = require('fs');
|
||||
let prEvent = JSON.parse(fs.readFileSync(process.env.PR_EVENT, {encoding: 'utf8'}));
|
||||
core.exportVariable("PR_HEAD", `${prEvent.number}/merge`);
|
||||
core.exportVariable("PR_BASE", prEvent.pull_request.base.ref);
|
||||
core.exportVariable("PR_BASE_SHA", prEvent.pull_request.base.sha);
|
||||
core.exportVariable("PR_NUMBER", prEvent.number);
|
||||
- uses: bencherdev/bencher@main
|
||||
- name: Track Benchmarks with Bencher
|
||||
run: |
|
||||
bencher run \
|
||||
--project ratatui-org \
|
||||
--token '${{ secrets.BENCHER_API_TOKEN }}' \
|
||||
--branch '${{ env.PR_HEAD }}' \
|
||||
--branch-start-point '${{ env.PR_BASE }}' \
|
||||
--branch-start-point-hash '${{ env.PR_BASE_SHA }}' \
|
||||
--testbed ubuntu-latest \
|
||||
--adapter rust_criterion \
|
||||
--err \
|
||||
--github-actions '${{ secrets.GITHUB_TOKEN }}' \
|
||||
--ci-number '${{ env.PR_NUMBER }}' \
|
||||
--file "$BENCHMARK_RESULTS"
|
||||
52
.github/workflows/calculate-alpha-release.bash
vendored
Executable file
52
.github/workflows/calculate-alpha-release.bash
vendored
Executable file
@@ -0,0 +1,52 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Exit on error. Append "|| true" if you expect an error.
|
||||
set -o errexit
|
||||
# Exit on error inside any functions or subshells.
|
||||
set -o errtrace
|
||||
# Do not allow use of undefined vars. Use ${VAR:-} to use an undefined VAR
|
||||
set -o nounset
|
||||
# Catch the error in case mysqldump fails (but gzip succeeds) in `mysqldump |gzip`
|
||||
set -o pipefail
|
||||
# Turn on traces, useful while debugging but commented out by default
|
||||
# set -o xtrace
|
||||
|
||||
last_release="$(git tag --sort=committerdate | grep -P "v0+\.\d+\.\d+$" | tail -1)"
|
||||
echo "🐭 Last release: ${last_release}"
|
||||
|
||||
# detect breaking changes
|
||||
if [ -n "$(git log --oneline ${last_release}..HEAD | grep '!:')" ]; then
|
||||
echo "🐭 Breaking changes detected since ${last_release}"
|
||||
git log --oneline ${last_release}..HEAD | grep '!:'
|
||||
# increment the minor version
|
||||
minor="${last_release##v0.}"
|
||||
minor="${minor%.*}"
|
||||
next_minor="$((minor + 1))"
|
||||
next_release="v0.${next_minor}.0"
|
||||
else
|
||||
# increment the patch version
|
||||
patch="${last_release##*.}"
|
||||
next_patch="$((patch + 1))"
|
||||
next_release="${last_release/%${patch}/${next_patch}}"
|
||||
fi
|
||||
echo "🐭 Next release: ${next_release}"
|
||||
|
||||
suffix="alpha"
|
||||
last_tag="$(git tag --sort=committerdate | tail -1)"
|
||||
if [[ "${last_tag}" = "${next_release}-${suffix}"* ]]; then
|
||||
echo "🐭 Last alpha release: ${last_tag}"
|
||||
# increment the alpha version
|
||||
# e.g. v0.22.1-alpha.12 -> v0.22.1-alpha.13
|
||||
alpha="${last_tag##*-${suffix}.}"
|
||||
next_alpha="$((alpha + 1))"
|
||||
next_tag="${last_tag/%${alpha}/${next_alpha}}"
|
||||
else
|
||||
# increment the patch and start the alpha version from 0
|
||||
# e.g. v0.22.0 -> v0.22.1-alpha.0
|
||||
next_tag="${next_release}-${suffix}.0"
|
||||
fi
|
||||
# update the crate version
|
||||
msg="# crate version"
|
||||
sed -E -i "s/^version = .* ${msg}$/version = \"${next_tag#v}\" ${msg}/" Cargo.toml
|
||||
echo "NEXT_TAG=${next_tag}" >> $GITHUB_ENV
|
||||
echo "🐭 Next alpha release: ${next_tag}"
|
||||
86
.github/workflows/check-pr.yml
vendored
Normal file
86
.github/workflows/check-pr.yml
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
name: Check Pull Requests
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- synchronize
|
||||
- labeled
|
||||
- unlabeled
|
||||
merge_group:
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
check-title:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check PR title
|
||||
if: github.event_name == 'pull_request_target'
|
||||
uses: amannn/action-semantic-pull-request@v5
|
||||
id: check_pr_title
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Add comment indicating we require pull request titles to follow conventional commits specification
|
||||
- uses: marocchino/sticky-pull-request-comment@v2
|
||||
if: always() && (steps.check_pr_title.outputs.error_message != null)
|
||||
with:
|
||||
header: pr-title-lint-error
|
||||
message: |
|
||||
Thank you for opening this pull request!
|
||||
|
||||
We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted.
|
||||
|
||||
Details:
|
||||
|
||||
> ${{ steps.check_pr_title.outputs.error_message }}
|
||||
|
||||
# Delete a previous comment when the issue has been resolved
|
||||
- if: ${{ steps.check_pr_title.outputs.error_message == null }}
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
with:
|
||||
header: pr-title-lint-error
|
||||
delete: true
|
||||
|
||||
check-breaking-change-label:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
# use an environment variable to pass untrusted input to the script
|
||||
# see https://securitylab.github.com/research/github-actions-untrusted-input/
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
steps:
|
||||
- name: Check breaking change label
|
||||
id: check_breaking_change
|
||||
run: |
|
||||
pattern='^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\(\w+\))?!:'
|
||||
# Check if pattern matches
|
||||
if echo "${PR_TITLE}" | grep -qE "$pattern"; then
|
||||
echo "breaking_change=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "breaking_change=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
- name: Add label
|
||||
if: steps.check_breaking_change.outputs.breaking_change == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
github.rest.issues.addLabels({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: ['Type: Breaking Change']
|
||||
})
|
||||
|
||||
do-not-merge:
|
||||
if: ${{ contains(github.event.*.labels.*.name, 'do not merge') }}
|
||||
name: Prevent Merging
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check for label
|
||||
run: |
|
||||
echo "Pull request is labeled as 'do not merge'"
|
||||
echo "This workflow fails so that the pull request cannot be merged"
|
||||
exit 1
|
||||
16
.github/workflows/check-semver.yml
vendored
Normal file
16
.github/workflows/check-semver.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
name: Check Semver
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
check-semver:
|
||||
name: Check semver
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Check semver
|
||||
uses: obi1kenobi/cargo-semver-checks-action@v2
|
||||
199
.github/workflows/ci.yml
vendored
199
.github/workflows/ci.yml
vendored
@@ -1,73 +1,160 @@
|
||||
name: Continuous Integration
|
||||
|
||||
on:
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
merge_group:
|
||||
|
||||
name: CI
|
||||
|
||||
env:
|
||||
CI_CARGO_MAKE_VERSION: 0.35.6
|
||||
# ensure that the workflow is only triggered once per PR, subsequent pushes to the PR will cancel
|
||||
# and restart the workflow. See https://docs.github.com/en/actions/using-jobs/using-concurrency
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# lint, clippy and coverage jobs are intentionally early in the workflow to catch simple formatting,
|
||||
# typos, and missing tests as early as possible. This allows us to fix these and resubmit the PR
|
||||
# without having to wait for the comprehensive matrix of tests to complete.
|
||||
jobs:
|
||||
linux:
|
||||
name: Linux
|
||||
rustfmt:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
rust: ["1.52.1", "stable"]
|
||||
steps:
|
||||
- name: "Install dependencies"
|
||||
run: sudo apt-get install libncurses5-dev
|
||||
- uses: hecrj/setup-rust-action@967aec96c6a27a0ce15c1dac3aaba332d60565e2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
rust-version: ${{ matrix.rust }}
|
||||
components: rustfmt,clippy
|
||||
- uses: actions/checkout@v1
|
||||
- name: "Get cargo bin directory"
|
||||
id: cargo-bin-dir
|
||||
run: echo "::set-output name=dir::$HOME/.cargo/bin"
|
||||
- name: "Cache cargo make"
|
||||
id: cache-cargo-make
|
||||
uses: actions/cache@v2
|
||||
components: rustfmt
|
||||
- run: cargo +nightly fmt --all --check
|
||||
|
||||
typos:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: crate-ci/typos@master
|
||||
|
||||
dependencies:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||
|
||||
cargo-machete:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: bnjbvr/cargo-machete@v0.6.2
|
||||
|
||||
clippy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
path: ${{ steps.cargo-bin-dir.outputs.dir }}/cargo-make
|
||||
key: ${{ runner.os }}-${{ matrix.rust }}-cargo-make-${{ env.CI_CARGO_MAKE_VERSION }}
|
||||
- name: "Install cargo-make"
|
||||
if: steps.cache-cargo-make.outputs.cache-hit != 'true'
|
||||
run: cargo install cargo-make --version ${{ env.CI_CARGO_MAKE_VERSION }}
|
||||
- name: "Format / Build / Test"
|
||||
run: cargo make ci
|
||||
components: clippy
|
||||
- uses: taiki-e/install-action@cargo-make
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo make clippy
|
||||
|
||||
markdownlint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: DavidAnson/markdownlint-cli2-action@v17
|
||||
with:
|
||||
globs: |
|
||||
'**/*.md'
|
||||
'!target'
|
||||
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: llvm-tools
|
||||
- uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-llvm-cov,cargo-make
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo make coverage
|
||||
- uses: codecov/codecov-action@v4
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
|
||||
check:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
toolchain: ["1.74.0", "stable"]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
- uses: taiki-e/install-action@cargo-make
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo make check
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
windows:
|
||||
name: Windows
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
matrix:
|
||||
rust: ["1.52.1", "stable"]
|
||||
|
||||
lint-docs:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTDOCFLAGS: -Dwarnings
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: hecrj/setup-rust-action@967aec96c6a27a0ce15c1dac3aaba332d60565e2
|
||||
with:
|
||||
rust-version: ${{ matrix.rust }}
|
||||
components: rustfmt,clippy
|
||||
- uses: actions/checkout@v1
|
||||
- name: "Get cargo bin directory"
|
||||
id: cargo-bin-dir
|
||||
run: echo "::set-output name=dir::$HOME\.cargo\bin"
|
||||
- name: "Cache cargo make"
|
||||
id: cache-cargo-make
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.cargo-bin-dir.outputs.dir }}\cargo-make.exe
|
||||
key: ${{ runner.os }}-${{ matrix.rust }}-cargo-make-${{ env.CI_CARGO_MAKE_VERSION }}
|
||||
- name: "Install cargo-make"
|
||||
if: steps.cache-cargo-make.outputs.cache-hit != 'true'
|
||||
run: cargo install cargo-make --version ${{ env.CI_CARGO_MAKE_VERSION }}
|
||||
- name: "Format / Build / Test"
|
||||
run: cargo make ci
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
- uses: dtolnay/install@cargo-docs-rs
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
# Run cargo rustdoc with the same options that would be used by docs.rs, taking into account
|
||||
# the package.metadata.docs.rs configured in Cargo.toml.
|
||||
# https://github.com/dtolnay/cargo-docs-rs
|
||||
- run: cargo +nightly docs-rs
|
||||
|
||||
test-doc:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: taiki-e/install-action@cargo-make
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo make test-doc
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
|
||||
test:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
toolchain: ["1.74.0", "stable"]
|
||||
backend: [crossterm, termion, termwiz]
|
||||
exclude:
|
||||
# termion is not supported on windows
|
||||
- os: windows-latest
|
||||
backend: termion
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
- uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-make,nextest
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo make test-backend ${{ matrix.backend }}
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
|
||||
48
.github/workflows/release-alpha.yml
vendored
Normal file
48
.github/workflows/release-alpha.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: Release alpha version
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# At 00:00 on Saturday
|
||||
# https://crontab.guru/#0_0_*_*_6
|
||||
- cron: "0 0 * * 6"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
publish-alpha:
|
||||
name: Create an alpha release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Calculate the next release
|
||||
run: .github/workflows/calculate-alpha-release.bash
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Publish
|
||||
run: cargo publish --allow-dirty --token ${{ secrets.CARGO_TOKEN }}
|
||||
|
||||
- name: Generate a changelog
|
||||
uses: orhun/git-cliff-action@v4
|
||||
with:
|
||||
config: cliff.toml
|
||||
args: --unreleased --tag ${{ env.NEXT_TAG }} --strip header
|
||||
env:
|
||||
OUTPUT: BODY.md
|
||||
|
||||
- name: Publish on GitHub
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
tag: ${{ env.NEXT_TAG }}
|
||||
prerelease: true
|
||||
bodyFile: BODY.md
|
||||
45
.github/workflows/release-stable.yml
vendored
Normal file
45
.github/workflows/release-stable.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
name: Release stable version
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
jobs:
|
||||
publish-stable:
|
||||
name: Create an stable release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Generate a changelog
|
||||
uses: orhun/git-cliff-action@v4
|
||||
with:
|
||||
config: cliff.toml
|
||||
args: --latest --strip header
|
||||
env:
|
||||
OUTPUT: BODY.md
|
||||
|
||||
- name: Publish on GitHub
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
prerelease: false
|
||||
bodyFile: BODY.md
|
||||
|
||||
publish-crate:
|
||||
name: Publish crate
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Publish
|
||||
run: cargo publish --token ${{ secrets.CARGO_TOKEN }}
|
||||
15
.markdownlint.yaml
Normal file
15
.markdownlint.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
# configuration for https://github.com/DavidAnson/markdownlint
|
||||
|
||||
first-line-heading: false
|
||||
no-inline-html:
|
||||
allowed_elements:
|
||||
- img
|
||||
- details
|
||||
- summary
|
||||
- div
|
||||
- br
|
||||
line-length:
|
||||
line_length: 100
|
||||
|
||||
# to support repeated headers in the changelog
|
||||
no-duplicate-heading: false
|
||||
699
BREAKING-CHANGES.md
Normal file
699
BREAKING-CHANGES.md
Normal file
@@ -0,0 +1,699 @@
|
||||
# Breaking Changes
|
||||
|
||||
This document contains a list of breaking changes in each version and some notes to help migrate
|
||||
between versions. It is compiled manually from the commit history and changelog. We also tag PRs on
|
||||
GitHub with a [breaking change] label.
|
||||
|
||||
[breaking change]: (https://github.com/ratatui/ratatui/issues?q=label%3A%22breaking+change%22)
|
||||
|
||||
## Summary
|
||||
|
||||
This is a quick summary of the sections below:
|
||||
|
||||
- [v0.28.0](#v0280)
|
||||
⁻ `Backend::size` returns `Size` instead of `Rect`
|
||||
- `Backend` trait migrates to `get/set_cursor_position`
|
||||
- Ratatui now requires Crossterm 0.28.0
|
||||
- `Axis::labels` now accepts `IntoIterator<Into<Line>>`
|
||||
- `Layout::init_cache` no longer returns bool and takes a `NonZeroUsize` instead of `usize`
|
||||
- `ratatui::terminal` module is now private
|
||||
- `ToText` no longer has a lifetime
|
||||
- `Frame::size` is deprecated and renamed to `Frame::area`
|
||||
- [v0.27.0](#v0270)
|
||||
- List no clamps the selected index to list
|
||||
- Prelude items added / removed
|
||||
- 'termion' updated to 4.0
|
||||
- `Rect::inner` takes `Margin` directly instead of reference
|
||||
- `Buffer::filled` takes `Cell` directly instead of reference
|
||||
- `Stylize::bg()` now accepts `Into<Color>`
|
||||
- Removed deprecated `List::start_corner`
|
||||
- `LineGauge::gauge_style` is deprecated
|
||||
- [v0.26.0](#v0260)
|
||||
- `Flex::Start` is the new default flex mode for `Layout`
|
||||
- `patch_style` & `reset_style` now consume and return `Self`
|
||||
- Removed deprecated `Block::title_on_bottom`
|
||||
- `Line` now has an extra `style` field which applies the style to the entire line
|
||||
- `Block` style methods cannot be created in a const context
|
||||
- `Tabs::new()` now accepts `IntoIterator<Item: Into<Line<'a>>>`
|
||||
- `Table::new` now accepts `IntoIterator<Item: Into<Row<'a>>>`.
|
||||
- [v0.25.0](#v0250)
|
||||
- Removed `Axis::title_style` and `Buffer::set_background`
|
||||
- `List::new()` now accepts `IntoIterator<Item = Into<ListItem<'a>>>`
|
||||
- `Table::new()` now requires specifying the widths
|
||||
- `Table::widths()` now accepts `IntoIterator<Item = AsRef<Constraint>>`
|
||||
- Layout::new() now accepts direction and constraint parameters
|
||||
- The default `Tabs::highlight_style` is now `Style::new().reversed()`
|
||||
- [v0.24.0](#v0240)
|
||||
- MSRV is now 1.70.0
|
||||
- `ScrollbarState`: `position`, `content_length`, and `viewport_content_length` are now `usize`
|
||||
- `BorderType`: `line_symbols` is now `border_symbols` and returns `symbols::border::set`
|
||||
- `Frame<'a, B: Backend>` is now `Frame<'a>`
|
||||
- `Stylize` shorthands for `String` now consume the value and return `Span<'static>`
|
||||
- `Spans` is removed
|
||||
- [v0.23.0](#v0230)
|
||||
- `Scrollbar`: `track_symbol` now takes `Option<&str>`
|
||||
- `Scrollbar`: symbols moved to `symbols` module
|
||||
- MSRV is now 1.67.0
|
||||
- [v0.22.0](#v0220)
|
||||
- `serde` representation of `Borders` and `Modifiers` has changed
|
||||
- [v0.21.0](#v0210)
|
||||
- MSRV is now 1.65.0
|
||||
- `terminal::ViewPort` is now an enum
|
||||
- `"".as_ref()` must be annotated to implement `Into<Text<'a>>`
|
||||
- `Marker::Block` renders as a block char instead of a bar char
|
||||
- [v0.20.0](#v0200)
|
||||
- MSRV is now 1.63.0
|
||||
- `List` no longer ignores empty strings
|
||||
|
||||
## v0.28.0
|
||||
|
||||
### `Backend::size` returns `Size` instead of `Rect` ([#1254])
|
||||
|
||||
[#1254]: https://github.com/ratatui/ratatui/pull/1254
|
||||
|
||||
The `Backend::size` method returns a `Size` instead of a `Rect`.
|
||||
There is no need for the position here as it was always 0,0.
|
||||
|
||||
### `Backend` trait migrates to `get/set_cursor_position` ([#1284])
|
||||
|
||||
[#1284]: https://github.com/ratatui/ratatui/pull/1284
|
||||
|
||||
If you just use the types implementing the `Backend` trait, you will see deprecation hints but
|
||||
nothing is a breaking change for you.
|
||||
|
||||
If you implement the Backend trait yourself, you need to update the implementation and add the
|
||||
`get/set_cursor_position` method. You can remove the `get/set_cursor` methods as they are deprecated
|
||||
and a default implementation for them exists.
|
||||
|
||||
### Ratatui now requires Crossterm 0.28.0 ([#1278])
|
||||
|
||||
[#1278]: https://github.com/ratatui/ratatui/pull/1278
|
||||
|
||||
Crossterm is updated to version 0.28.0, which is a semver incompatible version with the previous
|
||||
version (0.27.0). Ratatui re-exports the version of crossterm that it is compatible with under
|
||||
`ratatui::crossterm`, which can be used to avoid incompatible versions in your dependency list.
|
||||
|
||||
### `Axis::labels()` now accepts `IntoIterator<Into<Line>>` ([#1273] and [#1283])
|
||||
|
||||
[#1273]: https://github.com/ratatui/ratatui/pull/1173
|
||||
[#1283]: https://github.com/ratatui/ratatui/pull/1283
|
||||
|
||||
Previously Axis::labels accepted `Vec<Span>`. Any code that uses conversion methods that infer the
|
||||
type will need to be rewritten as the compiler cannot infer the correct type.
|
||||
|
||||
```diff
|
||||
- Axis::default().labels(vec!["a".into(), "b".into()])
|
||||
+ Axis::default().labels(["a", "b"])
|
||||
```
|
||||
|
||||
### `Layout::init_cache` no longer returns bool and takes a `NonZeroUsize` instead of `usize` ([#1245])
|
||||
|
||||
[#1245]: https://github.com/ratatui/ratatui/pull/1245
|
||||
|
||||
```diff
|
||||
- let is_initialized = Layout::init_cache(100);
|
||||
+ Layout::init_cache(NonZeroUsize::new(100).unwrap());
|
||||
```
|
||||
|
||||
### `ratatui::terminal` module is now private ([#1160])
|
||||
|
||||
[#1160]: https://github.com/ratatui/ratatui/pull/1160
|
||||
|
||||
The `terminal` module is now private and can not be used directly. The types under this module are
|
||||
exported from the root of the crate. This reduces clashes with other modules in the backends that
|
||||
are also named terminal, and confusion about module exports for newer Rust users.
|
||||
|
||||
```diff
|
||||
- use ratatui::terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, ViewPort};
|
||||
+ use ratatui::{CompletedFrame, Frame, Terminal, TerminalOptions, ViewPort};
|
||||
```
|
||||
|
||||
### `ToText` no longer has a lifetime ([#1234])
|
||||
|
||||
[#1234]: https://github.com/ratatui/ratatui/pull/1234
|
||||
|
||||
This change simplifies the trait and makes it easier to implement.
|
||||
|
||||
### `Frame::size` is deprecated and renamed to `Frame::area`
|
||||
|
||||
[#1293]: https://github.com/ratatui/ratatui/pull/1293
|
||||
|
||||
`Frame::size` is renamed to `Frame::area` as it's the more correct name.
|
||||
|
||||
## [v0.27.0](https://github.com/ratatui/ratatui/releases/tag/v0.27.0)
|
||||
|
||||
### List no clamps the selected index to list ([#1159])
|
||||
|
||||
[#1149]: https://github.com/ratatui/ratatui/pull/1149
|
||||
|
||||
The `List` widget now clamps the selected index to the bounds of the list when navigating with
|
||||
`first`, `last`, `previous`, and `next`, as well as when setting the index directly with `select`.
|
||||
|
||||
Previously selecting an index past the end of the list would show treat the list as having a
|
||||
selection which was not visible. Now the last item in the list will be selected instead.
|
||||
|
||||
### Prelude items added / removed ([#1149])
|
||||
|
||||
The following items have been removed from the prelude:
|
||||
|
||||
- `style::Styled` - this trait is useful for widgets that want to
|
||||
support the Stylize trait, but it adds complexity as widgets have two
|
||||
`style` methods and a `set_style` method.
|
||||
- `symbols::Marker` - this item is used by code that needs to draw to
|
||||
the `Canvas` widget, but it's not a common item that would be used by
|
||||
most users of the library.
|
||||
- `terminal::{CompletedFrame, TerminalOptions, Viewport}` - these items
|
||||
are rarely used by code that needs to interact with the terminal, and
|
||||
they're generally only ever used once in any app.
|
||||
|
||||
The following items have been added to the prelude:
|
||||
|
||||
- `layout::{Position, Size}` - these items are used by code that needs
|
||||
to interact with the layout system. These are newer items that were
|
||||
added in the last few releases, which should be used more liberally.
|
||||
This may cause conflicts for types defined elsewhere with a similar
|
||||
name.
|
||||
|
||||
To update your app:
|
||||
|
||||
```diff
|
||||
// if your app uses Styled::style() or Styled::set_style():
|
||||
-use ratatui::prelude::*;
|
||||
+use ratatui::{prelude::*, style::Styled};
|
||||
|
||||
// if your app uses symbols::Marker:
|
||||
-use ratatui::prelude::*;
|
||||
+use ratatui::{prelude::*, symbols::Marker}
|
||||
|
||||
// if your app uses terminal::{CompletedFrame, TerminalOptions, Viewport}
|
||||
-use ratatui::prelude::*;
|
||||
+use ratatui::{prelude::*, terminal::{CompletedFrame, TerminalOptions, Viewport}};
|
||||
|
||||
// to disambiguate existing types named Position or Size:
|
||||
- use some_crate::{Position, Size};
|
||||
- let size: Size = ...;
|
||||
- let position: Position = ...;
|
||||
+ let size: some_crate::Size = ...;
|
||||
+ let position: some_crate::Position = ...;
|
||||
```
|
||||
|
||||
### Termion is updated to 4.0 [#1106]
|
||||
|
||||
Changelog: <https://gitlab.redox-os.org/redox-os/termion/-/blob/master/CHANGELOG.md>
|
||||
|
||||
A change is only necessary if you were matching on all variants of the `MouseEvent` enum without a
|
||||
wildcard. In this case, you need to either handle the two new variants, `MouseLeft` and
|
||||
`MouseRight`, or add a wildcard.
|
||||
|
||||
[#1106]: https://github.com/ratatui/ratatui/pull/1106
|
||||
|
||||
### `Rect::inner` takes `Margin` directly instead of reference ([#1008])
|
||||
|
||||
[#1008]: https://github.com/ratatui/ratatui/pull/1008
|
||||
|
||||
`Margin` needs to be passed without reference now.
|
||||
|
||||
```diff
|
||||
-let area = area.inner(&Margin {
|
||||
+let area = area.inner(Margin {
|
||||
vertical: 0,
|
||||
horizontal: 2,
|
||||
});
|
||||
```
|
||||
|
||||
### `Buffer::filled` takes `Cell` directly instead of reference ([#1148])
|
||||
|
||||
[#1148]: https://github.com/ratatui/ratatui/pull/1148
|
||||
|
||||
`Buffer::filled` moves the `Cell` instead of taking a reference.
|
||||
|
||||
```diff
|
||||
-Buffer::filled(area, &Cell::new("X"));
|
||||
+Buffer::filled(area, Cell::new("X"));
|
||||
```
|
||||
|
||||
### `Stylize::bg()` now accepts `Into<Color>` ([#1103])
|
||||
|
||||
[#1103]: https://github.com/ratatui/ratatui/pull/1103
|
||||
|
||||
Previously, `Stylize::bg()` accepted `Color` but now accepts `Into<Color>`. This allows more
|
||||
flexible types from calling scopes, though it can break some type inference in the calling scope.
|
||||
|
||||
### Remove deprecated `List::start_corner` and `layout::Corner` ([#759])
|
||||
|
||||
[#759]: https://github.com/ratatui/ratatui/pull/759
|
||||
|
||||
`List::start_corner` was deprecated in v0.25. Use `List::direction` and `ListDirection` instead.
|
||||
|
||||
```diff
|
||||
- list.start_corner(Corner::TopLeft);
|
||||
- list.start_corner(Corner::TopRight);
|
||||
// This is not an error, BottomRight rendered top to bottom previously
|
||||
- list.start_corner(Corner::BottomRight);
|
||||
// all becomes
|
||||
+ list.direction(ListDirection::TopToBottom);
|
||||
```
|
||||
|
||||
```diff
|
||||
- list.start_corner(Corner::BottomLeft);
|
||||
// becomes
|
||||
+ list.direction(ListDirection::BottomToTop);
|
||||
```
|
||||
|
||||
`layout::Corner` was removed entirely.
|
||||
|
||||
### `LineGauge::gauge_style` is deprecated ([#565])
|
||||
|
||||
[#565]: https://github.com/ratatui/ratatui/pull/1148
|
||||
|
||||
`LineGauge::gauge_style` is deprecated and replaced with `LineGauge::filled_style` and `LineGauge::unfilled_style`:
|
||||
|
||||
```diff
|
||||
let gauge = LineGauge::default()
|
||||
- .gauge_style(Style::default().fg(Color::Red).bg(Color::Blue)
|
||||
+ .filled_style(Style::default().fg(Color::Green))
|
||||
+ .unfilled_style(Style::default().fg(Color::White));
|
||||
```
|
||||
|
||||
## [v0.26.0](https://github.com/ratatui/ratatui/releases/tag/v0.26.0)
|
||||
|
||||
### `Flex::Start` is the new default flex mode for `Layout` ([#881])
|
||||
|
||||
[#881]: https://github.com/ratatui/ratatui/pull/881
|
||||
|
||||
Previously, constraints would stretch to fill all available space, violating constraints if
|
||||
necessary.
|
||||
|
||||
With v0.26.0, `Flex` modes are introduced, and the default is `Flex::Start`, which will align
|
||||
areas associated with constraints to be beginning of the area. With v0.26.0, additionally,
|
||||
`Min` constraints grow to fill excess space. These changes will allow users to build layouts
|
||||
more easily.
|
||||
|
||||
With v0.26.0, users will most likely not need to change what constraints they use to create
|
||||
existing layouts with `Flex::Start`. However, to get old behavior, use `Flex::Legacy`.
|
||||
|
||||
```diff
|
||||
- let rects = Layout::horizontal([Length(1), Length(2)]).split(area);
|
||||
// becomes
|
||||
+ let rects = Layout::horizontal([Length(1), Length(2)]).flex(Flex::Legacy).split(area);
|
||||
```
|
||||
|
||||
### `Table::new()` now accepts `IntoIterator<Item: Into<Row<'a>>>` ([#774])
|
||||
|
||||
[#774]: https://github.com/ratatui/ratatui/pull/774
|
||||
|
||||
Previously, `Table::new()` accepted `IntoIterator<Item=Row<'a>>`. The argument change to
|
||||
`IntoIterator<Item: Into<Row<'a>>>`, This allows more flexible types from calling scopes, though it
|
||||
can some break type inference in the calling scope for empty containers.
|
||||
|
||||
This can be resolved either by providing an explicit type (e.g. `Vec::<Row>::new()`), or by using
|
||||
`Table::default()`.
|
||||
|
||||
```diff
|
||||
- let table = Table::new(vec![], widths);
|
||||
// becomes
|
||||
+ let table = Table::default().widths(widths);
|
||||
```
|
||||
|
||||
### `Tabs::new()` now accepts `IntoIterator<Item: Into<Line<'a>>>` ([#776])
|
||||
|
||||
[#776]: https://github.com/ratatui/ratatui/pull/776
|
||||
|
||||
Previously, `Tabs::new()` accepted `Vec<T>` where `T: Into<Line<'a>>`. This allows more flexible
|
||||
types from calling scopes, though it can break some type inference in the calling scope.
|
||||
|
||||
This typically occurs when collecting an iterator prior to calling `Tabs::new`, and can be resolved
|
||||
by removing the call to `.collect()`.
|
||||
|
||||
```diff
|
||||
- let tabs = Tabs::new((0.3).map(|i| format!("{i}")).collect());
|
||||
// becomes
|
||||
+ let tabs = Tabs::new((0.3).map(|i| format!("{i}")));
|
||||
```
|
||||
|
||||
### Table::default() now sets segment_size to None and column_spacing to ([#751])
|
||||
|
||||
[#751]: https://github.com/ratatui/ratatui/pull/751
|
||||
|
||||
The default() implementation of Table now sets the column_spacing field to 1 and the segment_size
|
||||
field to `SegmentSize::None`. This will affect the rendering of a small amount of apps.
|
||||
|
||||
To use the previous default values, call `table.segment_size(Default::default())` and
|
||||
`table.column_spacing(0)`.
|
||||
|
||||
### `patch_style` & `reset_style` now consumes and returns `Self` ([#754])
|
||||
|
||||
[#754]: https://github.com/ratatui/ratatui/pull/754
|
||||
|
||||
Previously, `patch_style` and `reset_style` in `Text`, `Line` and `Span` were using a mutable
|
||||
reference to `Self`. To be more consistent with the rest of `ratatui`, which is using fluent
|
||||
setters, these now take ownership of `Self` and return it.
|
||||
|
||||
The following example shows how to migrate for `Line`, but the same applies for `Text` and `Span`.
|
||||
|
||||
```diff
|
||||
- let mut line = Line::from("foobar");
|
||||
- line.patch_style(style);
|
||||
// becomes
|
||||
+ let line = Line::new("foobar").patch_style(style);
|
||||
```
|
||||
|
||||
### Remove deprecated `Block::title_on_bottom` ([#757])
|
||||
|
||||
`Block::title_on_bottom` was deprecated in v0.22. Use `Block::title` and `Title::position` instead.
|
||||
|
||||
```diff
|
||||
- block.title("foobar").title_on_bottom();
|
||||
+ block.title(Title::from("foobar").position(Position::Bottom));
|
||||
```
|
||||
|
||||
### `Block` style methods cannot be used in a const context ([#720])
|
||||
|
||||
[#720]: https://github.com/ratatui/ratatui/pull/720
|
||||
|
||||
Previously the `style()`, `border_style()` and `title_style()` methods could be used to create a
|
||||
`Block` in a constant context. These now accept `Into<Style>` instead of `Style`. These methods no
|
||||
longer can be called from a constant context.
|
||||
|
||||
### `Line` now has a `style` field that applies to the entire line ([#708])
|
||||
|
||||
[#708]: https://github.com/ratatui/ratatui/pull/708
|
||||
|
||||
Previously the style of a `Line` was stored in the `Span`s that make up the line. Now the `Line`
|
||||
itself has a `style` field, which can be set with the `Line::styled` method. Any code that creates
|
||||
`Line`s using the struct initializer instead of constructors will fail to compile due to the added
|
||||
field. This can be easily fixed by adding `..Default::default()` to the field list or by using a
|
||||
constructor method (`Line::styled()`, `Line::raw()`) or conversion method (`Line::from()`).
|
||||
|
||||
Each `Span` contained within the line will no longer have the style that is applied to the line in
|
||||
the `Span::style` field.
|
||||
|
||||
```diff
|
||||
let line = Line {
|
||||
spans: vec!["".into()],
|
||||
alignment: Alignment::Left,
|
||||
+ ..Default::default()
|
||||
};
|
||||
|
||||
// or
|
||||
|
||||
let line = Line::raw(vec!["".into()])
|
||||
.alignment(Alignment::Left);
|
||||
```
|
||||
|
||||
## [v0.25.0](https://github.com/ratatui/ratatui/releases/tag/v0.25.0)
|
||||
|
||||
### Removed `Axis::title_style` and `Buffer::set_background` ([#691])
|
||||
|
||||
[#691]: https://github.com/ratatui/ratatui/pull/691
|
||||
|
||||
These items were deprecated since 0.10.
|
||||
|
||||
- You should use styling capabilities of [`text::Line`] given as argument of [`Axis::title`]
|
||||
instead of `Axis::title_style`
|
||||
- You should use styling capabilities of [`Buffer::set_style`] instead of `Buffer::set_background`
|
||||
|
||||
[`text::Line`]: https://docs.rs/ratatui/latest/ratatui/text/struct.Line.html
|
||||
[`Axis::title`]: https://docs.rs/ratatui/latest/ratatui/widgets/struct.Axis.html#method.title
|
||||
[`Buffer::set_style`]: https://docs.rs/ratatui/latest/ratatui/buffer/struct.Buffer.html#method.set_style
|
||||
|
||||
### `List::new()` now accepts `IntoIterator<Item = Into<ListItem<'a>>>` ([#672])
|
||||
|
||||
[#672]: https://github.com/ratatui/ratatui/pull/672
|
||||
|
||||
Previously `List::new()` took `Into<Vec<ListItem<'a>>>`. This change will throw a compilation
|
||||
error for `IntoIterator`s with an indeterminate item (e.g. empty vecs).
|
||||
|
||||
E.g.
|
||||
|
||||
```diff
|
||||
- let list = List::new(vec![]);
|
||||
// becomes
|
||||
+ let list = List::default();
|
||||
```
|
||||
|
||||
### The default `Tabs::highlight_style` is now `Style::new().reversed()` ([#635])
|
||||
|
||||
[#635]: https://github.com/ratatui/ratatui/pull/635
|
||||
|
||||
Previously the default highlight style for tabs was `Style::default()`, which meant that a `Tabs`
|
||||
widget in the default configuration would not show any indication of the selected tab.
|
||||
|
||||
### The default `Tabs::highlight_style` is now `Style::new().reversed()` ([#635])
|
||||
|
||||
Previously the default highlight style for tabs was `Style::default()`, which meant that a `Tabs`
|
||||
widget in the default configuration would not show any indication of the selected tab.
|
||||
|
||||
### `Table::new()` now requires specifying the widths of the columns ([#664])
|
||||
|
||||
[#664]: https://github.com/ratatui/ratatui/pull/664
|
||||
|
||||
Previously `Table`s could be constructed without `widths`. In almost all cases this is an error.
|
||||
A new `widths` parameter is now mandatory on `Table::new()`. Existing code of the form:
|
||||
|
||||
```diff
|
||||
- Table::new(rows).widths(widths)
|
||||
```
|
||||
|
||||
Should be updated to:
|
||||
|
||||
```diff
|
||||
+ Table::new(rows, widths)
|
||||
```
|
||||
|
||||
For ease of automated replacement in cases where the amount of code broken by this change is large
|
||||
or complex, it may be convenient to replace `Table::new` with `Table::default().rows`.
|
||||
|
||||
```diff
|
||||
- Table::new(rows).block(block).widths(widths);
|
||||
// becomes
|
||||
+ Table::default().rows(rows).widths(widths)
|
||||
```
|
||||
|
||||
### `Table::widths()` now accepts `IntoIterator<Item = AsRef<Constraint>>` ([#663])
|
||||
|
||||
[#663]: https://github.com/ratatui/ratatui/pull/663
|
||||
|
||||
Previously `Table::widths()` took a slice (`&'a [Constraint]`). This change will introduce clippy
|
||||
`needless_borrow` warnings for places where slices are passed to this method. To fix these, remove
|
||||
the `&`.
|
||||
|
||||
E.g.
|
||||
|
||||
```diff
|
||||
- let table = Table::new(rows).widths(&[Constraint::Length(1)]);
|
||||
// becomes
|
||||
+ let table = Table::new(rows, [Constraint::Length(1)]);
|
||||
```
|
||||
|
||||
### Layout::new() now accepts direction and constraint parameters ([#557])
|
||||
|
||||
[#557]: https://github.com/ratatui/ratatui/pull/557
|
||||
|
||||
Previously layout new took no parameters. Existing code should either use `Layout::default()` or
|
||||
the new constructor.
|
||||
|
||||
```rust
|
||||
let layout = layout::new()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Min(1), Constraint::Max(2)]);
|
||||
// becomes either
|
||||
let layout = layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Min(1), Constraint::Max(2)]);
|
||||
// or
|
||||
let layout = layout::new(Direction::Vertical, [Constraint::Min(1), Constraint::Max(2)]);
|
||||
```
|
||||
|
||||
## [v0.24.0](https://github.com/ratatui/ratatui/releases/tag/v0.24.0)
|
||||
|
||||
### `ScrollbarState` field type changed from `u16` to `usize` ([#456])
|
||||
|
||||
[#456]: https://github.com/ratatui/ratatui/pull/456
|
||||
|
||||
In order to support larger content lengths, the `position`, `content_length` and
|
||||
`viewport_content_length` methods on `ScrollbarState` now take `usize` instead of `u16`
|
||||
|
||||
### `BorderType::line_symbols` renamed to `border_symbols` ([#529])
|
||||
|
||||
[#529]: https://github.com/ratatui/ratatui/issues/529
|
||||
|
||||
Applications can now set custom borders on a `Block` by calling `border_set()`. The
|
||||
`BorderType::line_symbols()` is renamed to `border_symbols()` and now returns a new struct
|
||||
`symbols::border::Set`. E.g.:
|
||||
|
||||
```diff
|
||||
- let line_set: symbols::line::Set = BorderType::line_symbols(BorderType::Plain);
|
||||
// becomes
|
||||
+ let border_set: symbols::border::Set = BorderType::border_symbols(BorderType::Plain);
|
||||
```
|
||||
|
||||
### Generic `Backend` parameter removed from `Frame` ([#530])
|
||||
|
||||
[#530]: https://github.com/ratatui/ratatui/issues/530
|
||||
|
||||
`Frame` is no longer generic over Backend. Code that accepted `Frame<Backend>` will now need to
|
||||
accept `Frame`. To migrate existing code, remove any generic parameters from code that uses an
|
||||
instance of a Frame. E.g.:
|
||||
|
||||
```diff
|
||||
- fn ui<B: Backend>(frame: &mut Frame<B>) { ... }
|
||||
// becomes
|
||||
+ fn ui(frame: Frame) { ... }
|
||||
```
|
||||
|
||||
### `Stylize` shorthands now consume rather than borrow `String` ([#466])
|
||||
|
||||
[#466]: https://github.com/ratatui/ratatui/issues/466
|
||||
|
||||
In order to support using `Stylize` shorthands (e.g. `"foo".red()`) on temporary `String` values, a
|
||||
new implementation of `Stylize` was added that returns a `Span<'static>`. This causes the value to
|
||||
be consumed rather than borrowed. Existing code that expects to use the string after a call will no
|
||||
longer compile. E.g.
|
||||
|
||||
```diff
|
||||
- let s = String::new("foo");
|
||||
- let span1 = s.red();
|
||||
- let span2 = s.blue(); // will no longer compile as s is consumed by the previous line
|
||||
// becomes
|
||||
+ let span1 = s.clone().red();
|
||||
+ let span2 = s.blue();
|
||||
```
|
||||
|
||||
### Deprecated `Spans` type removed (replaced with `Line`) ([#426])
|
||||
|
||||
[#426]: https://github.com/ratatui/ratatui/issues/426
|
||||
|
||||
`Spans` was replaced with `Line` in 0.21.0. `Buffer::set_spans` was replaced with
|
||||
`Buffer::set_line`.
|
||||
|
||||
```diff
|
||||
- let spans = Spans::from(some_string_str_span_or_vec_span);
|
||||
- buffer.set_spans(0, 0, spans, 10);
|
||||
// becomes
|
||||
+ let line - Line::from(some_string_str_span_or_vec_span);
|
||||
+ buffer.set_line(0, 0, line, 10);
|
||||
```
|
||||
|
||||
## [v0.23.0](https://github.com/ratatui/ratatui/releases/tag/v0.23.0)
|
||||
|
||||
### `Scrollbar::track_symbol()` now takes an `Option<&str>` instead of `&str` ([#360])
|
||||
|
||||
[#360]: https://github.com/ratatui/ratatui/issues/360
|
||||
|
||||
The track symbol of `Scrollbar` is now optional, this method now takes an optional value.
|
||||
|
||||
```diff
|
||||
- let scrollbar = Scrollbar::default().track_symbol("|");
|
||||
// becomes
|
||||
+ let scrollbar = Scrollbar::default().track_symbol(Some("|"));
|
||||
```
|
||||
|
||||
### `Scrollbar` symbols moved to `symbols::scrollbar` and `widgets::scrollbar` module is private ([#330])
|
||||
|
||||
[#330]: https://github.com/ratatui/ratatui/issues/330
|
||||
|
||||
The symbols for defining scrollbars have been moved to the `symbols` module from the
|
||||
`widgets::scrollbar` module which is no longer public. To update your code update any imports to the
|
||||
new module locations. E.g.:
|
||||
|
||||
```diff
|
||||
- use ratatui::{widgets::scrollbar::{Scrollbar, Set}};
|
||||
// becomes
|
||||
+ use ratatui::{widgets::Scrollbar, symbols::scrollbar::Set}
|
||||
```
|
||||
|
||||
### MSRV updated to 1.67 ([#361])
|
||||
|
||||
[#361]: https://github.com/ratatui/ratatui/issues/361
|
||||
|
||||
The MSRV of ratatui is now 1.67 due to an MSRV update in a dependency (`time`).
|
||||
|
||||
## [v0.22.0](https://github.com/ratatui/ratatui/releases/tag/v0.22.0)
|
||||
|
||||
### `bitflags` updated to 2.3 ([#205])
|
||||
|
||||
[#205]: https://github.com/ratatui/ratatui/issues/205
|
||||
|
||||
The `serde` representation of `bitflags` has changed. Any existing serialized types that have
|
||||
Borders or Modifiers will need to be re-serialized. This is documented in the [`bitflags`
|
||||
changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md#200-rc2)..
|
||||
|
||||
## [v0.21.0](https://github.com/ratatui/ratatui/releases/tag/v0.21.0)
|
||||
|
||||
### MSRV is 1.65.0 ([#171])
|
||||
|
||||
[#171]: https://github.com/ratatui/ratatui/issues/171
|
||||
|
||||
The minimum supported rust version is now 1.65.0.
|
||||
|
||||
### `Terminal::with_options()` stabilized to allow configuring the viewport ([#114])
|
||||
|
||||
[#114]: https://github.com/ratatui/ratatui/issues/114
|
||||
|
||||
In order to support inline viewports, the unstable method `Terminal::with_options()` was stabilized
|
||||
and `ViewPort` was changed from a struct to an enum.
|
||||
|
||||
```diff
|
||||
let terminal = Terminal::with_options(backend, TerminalOptions {
|
||||
- viewport: Viewport::fixed(area),
|
||||
});
|
||||
// becomes
|
||||
let terminal = Terminal::with_options(backend, TerminalOptions {
|
||||
+ viewport: Viewport::Fixed(area),
|
||||
});
|
||||
```
|
||||
|
||||
### Code that binds `Into<Text<'a>>` now requires type annotations ([#168])
|
||||
|
||||
[#168]: https://github.com/ratatui/ratatui/issues/168
|
||||
|
||||
A new type `Masked` was introduced that implements `From<Text<'a>>`. This causes any code that
|
||||
previously did not need to use type annotations to fail to compile. To fix this, annotate or call
|
||||
`to_string()` / `to_owned()` / `as_str()` on the value. E.g.:
|
||||
|
||||
```diff
|
||||
- let paragraph = Paragraph::new("".as_ref());
|
||||
// becomes
|
||||
+ let paragraph = Paragraph::new("".as_str());
|
||||
```
|
||||
|
||||
### `Marker::Block` now renders as a block rather than a bar character ([#133])
|
||||
|
||||
[#133]: https://github.com/ratatui/ratatui/issues/133
|
||||
|
||||
Code using the `Block` marker that previously rendered using a half block character (`'▀'``) now
|
||||
renders using the full block character (`'█'`). A new marker variant`Bar` is introduced to replace
|
||||
the existing code.
|
||||
|
||||
```diff
|
||||
- let canvas = Canvas::default().marker(Marker::Block);
|
||||
// becomes
|
||||
+ let canvas = Canvas::default().marker(Marker::Bar);
|
||||
```
|
||||
|
||||
## [v0.20.0](https://github.com/ratatui/ratatui/releases/tag/v0.20.0)
|
||||
|
||||
v0.20.0 was the first release of Ratatui - versions prior to this were release as tui-rs. See the
|
||||
[Changelog](./CHANGELOG.md) for more details.
|
||||
|
||||
### MSRV is update to 1.63.0 ([#80])
|
||||
|
||||
[#80]: https://github.com/ratatui/ratatui/issues/80
|
||||
|
||||
The minimum supported rust version is 1.63.0
|
||||
|
||||
### List no longer ignores empty string in items ([#42])
|
||||
|
||||
[#42]: https://github.com/ratatui/ratatui/issues/42
|
||||
|
||||
The following code now renders 3 items instead of 2. Code which relies on the previous behavior will
|
||||
need to manually filter empty items prior to display.
|
||||
|
||||
```rust
|
||||
let items = vec![
|
||||
ListItem::new("line one"),
|
||||
ListItem::new(""),
|
||||
ListItem::new("line four"),
|
||||
];
|
||||
```
|
||||
5952
CHANGELOG.md
5952
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
128
CODE_OF_CONDUCT.md
Normal file
128
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
https://forum.ratatui.rs/ or https://discord.gg/pMCEU9hNEj.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
214
CONTRIBUTING.md
214
CONTRIBUTING.md
@@ -1,33 +1,211 @@
|
||||
# Contributing
|
||||
# Contribution guidelines
|
||||
|
||||
## Building
|
||||
First off, thank you for considering contributing to Ratatui.
|
||||
|
||||
[cargo-make]: https://github.com/sagiegurari/cargo-make "cargo-make"
|
||||
If your contribution is not straightforward, please first discuss the change you wish to make by
|
||||
creating a new issue before making the change, or starting a discussion on
|
||||
[discord](https://discord.gg/pMCEU9hNEj).
|
||||
|
||||
`tui` 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`.
|
||||
## Reporting issues
|
||||
|
||||
## :hammer_and_wrench: Pull requests
|
||||
Before reporting an issue on the [issue tracker](https://github.com/ratatui/ratatui/issues),
|
||||
please check that it has not already been reported by searching for some related keywords. Please
|
||||
also check [`tui-rs` issues](https://github.com/fdehau/tui-rs/issues/) and link any related issues
|
||||
found.
|
||||
|
||||
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.
|
||||
## 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.
|
||||
|
||||
### Keep PRs small, intentional, and focused
|
||||
|
||||
Try to do one pull request per change. The time taken to review a PR grows exponential with the size
|
||||
of the change. Small focused PRs will generally be much more faster to review. PRs that include both
|
||||
refactoring (or reformatting) with actual changes are more difficult to review as every line of the
|
||||
change becomes a place where a bug may have been introduced. Consider splitting refactoring /
|
||||
reformatting changes into a separate PR from those that make a behavioral change, as the tests help
|
||||
guarantee that the behavior is unchanged.
|
||||
|
||||
### Code formatting
|
||||
|
||||
Run `cargo make format` before committing to ensure that code is consistently formatted with
|
||||
rustfmt. Configuration is in [`rustfmt.toml`](./rustfmt.toml).
|
||||
|
||||
### Search `tui-rs` for similar work
|
||||
|
||||
The original fork of Ratatui, [`tui-rs`](https://github.com/fdehau/tui-rs/), has a large amount of
|
||||
history of the project. Please search, read, link, and summarize any relevant
|
||||
[issues](https://github.com/fdehau/tui-rs/issues/),
|
||||
[discussions](https://github.com/fdehau/tui-rs/discussions/) and [pull
|
||||
requests](https://github.com/fdehau/tui-rs/pulls).
|
||||
|
||||
### Use conventional commits
|
||||
|
||||
We use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) and check for them as
|
||||
a lint build step. To help adhere to the format, 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). Your commit messages should have enough
|
||||
information to help someone reading the [CHANGELOG](./CHANGELOG.md) understand what is new just from
|
||||
the title. The summary helps expand on that to provide information that helps provide more context,
|
||||
describes the nature of the problem that the commit is solving and any unintuitive effects of the
|
||||
change. It's rare that code changes can easily communicate intent, so make sure this is clearly
|
||||
documented.
|
||||
|
||||
### Run CI tests before pushing a PR
|
||||
|
||||
Running `cargo make ci` before pushing will perform the same checks that we do in the CI process.
|
||||
It's not mandatory to do this before pushing, however it may save you time to do so instead of
|
||||
waiting for GitHub to run the checks.
|
||||
|
||||
### Sign your commits
|
||||
|
||||
We use commit signature verification, which will block commits from being merged via the UI unless
|
||||
they are signed. To set up your machine to sign commits, see [managing commit signature
|
||||
verification](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification)
|
||||
in GitHub docs.
|
||||
|
||||
## Implementation Guidelines
|
||||
|
||||
### Setup
|
||||
|
||||
Clone the repo and build it using [cargo-make](https://sagiegurari.github.io/cargo-make/)
|
||||
|
||||
Ratatui is an ordinary Rust project where common tasks are managed with
|
||||
[cargo-make](https://github.com/sagiegurari/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`.
|
||||
|
||||
```shell
|
||||
git clone https://github.com/ratatui/ratatui.git
|
||||
cd ratatui
|
||||
cargo make build
|
||||
```
|
||||
|
||||
### Tests
|
||||
|
||||
The [test coverage](https://app.codecov.io/gh/ratatui/ratatui) of the crate is reasonably
|
||||
good, but this can always be improved. Focus on keeping the tests simple and obvious and write unit
|
||||
tests for all new or modified code. 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.
|
||||
|
||||
When writing tests, generally prefer to write unit tests and doc tests directly in the code file
|
||||
being tested rather than integration tests in the `tests/` folder.
|
||||
|
||||
If an area that you're making a change in is not tested, write tests to characterize the existing
|
||||
behavior before changing it. This helps ensure that we don't introduce bugs to existing software
|
||||
using Ratatui (and helps make it easy to migrate apps still using `tui-rs`).
|
||||
|
||||
For coverage, we have two [bacon](https://dystroy.org/bacon/) jobs (one for all tests, and one for
|
||||
unit tests, keyboard shortcuts `v` and `u` respectively) that run
|
||||
[cargo-llvm-cov](https://github.com/taiki-e/cargo-llvm-cov) to report the coverage. Several plugins
|
||||
exist to show coverage directly in your editor. E.g.:
|
||||
|
||||
- <https://marketplace.visualstudio.com/items?itemName=ryanluker.vscode-coverage-gutters>
|
||||
- <https://github.com/alepez/vim-llvmcov>
|
||||
|
||||
### Documentation
|
||||
|
||||
Here are some guidelines for writing documentation in Ratatui.
|
||||
|
||||
Every public API **must** be documented.
|
||||
|
||||
Keep in mind that Ratatui tends to attract beginner Rust users that may not be familiar with Rust
|
||||
concepts.
|
||||
|
||||
#### Content
|
||||
|
||||
The main doc comment should talk about the general features that the widget supports and introduce
|
||||
the concepts pointing to the various methods. Focus on interaction with various features and giving
|
||||
enough information that helps understand why you might want something.
|
||||
|
||||
Examples should help users understand a particular usage, not test a feature. They should be as
|
||||
simple as possible. Prefer hiding imports and using wildcards to keep things concise. Some imports
|
||||
may still be shown to demonstrate a particular non-obvious import (e.g. `Stylize` trait to use style
|
||||
methods). Speaking of `Stylize`, you should use it over the more verbose style setters:
|
||||
|
||||
```rust
|
||||
let style = Style::new().red().bold();
|
||||
// not
|
||||
let style = Style::default().fg(Color::Red).add_modifier(Modifiers::BOLD);
|
||||
```
|
||||
|
||||
#### Format
|
||||
|
||||
- First line is summary, second is blank, third onward is more detail
|
||||
|
||||
```rust
|
||||
/// Summary
|
||||
///
|
||||
/// A detailed description
|
||||
/// with examples.
|
||||
fn foo() {}
|
||||
```
|
||||
|
||||
- Max line length is 100 characters
|
||||
See [VS Code rewrap extension](https://marketplace.visualstudio.com/items?itemName=stkb.rewrap)
|
||||
|
||||
- Doc comments are above macros
|
||||
i.e.
|
||||
|
||||
```rust
|
||||
/// doc comment
|
||||
#[derive(Debug)]
|
||||
struct Foo {}
|
||||
```
|
||||
|
||||
- Code items should be between backticks
|
||||
i.e. ``[`Block`]``, **NOT** ``[Block]``
|
||||
|
||||
### Deprecation notice
|
||||
|
||||
We generally want to wait at least two versions before removing deprecated items, so users have
|
||||
time to update. However, if a deprecation is blocking for us to implement a new feature we may
|
||||
*consider* removing it in a one version notice.
|
||||
|
||||
### Use of unsafe for optimization purposes
|
||||
|
||||
We don't currently use any unsafe code in Ratatui, and would like to keep it that way. However, there
|
||||
may be specific cases that this becomes necessary in order to avoid slowness. Please see [this
|
||||
discussion](https://github.com/ratatui/ratatui/discussions/66) for more about the decision.
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
We use Github Actions for the CI where we perform the following checks:
|
||||
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.
|
||||
You can also check most of those things yourself locally using `cargo make ci` which will offer you
|
||||
a shorter feedback loop than pushing to github.
|
||||
|
||||
## Tests
|
||||
## Relationship with `tui-rs`
|
||||
|
||||
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 `tui` is a test again 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.
|
||||
This project was forked from [`tui-rs`](https://github.com/fdehau/tui-rs/) in February 2023, with the
|
||||
[blessing of the original author](https://github.com/fdehau/tui-rs/issues/654), Florian Dehau
|
||||
([@fdehau](https://github.com/fdehau)).
|
||||
|
||||
The original repository contains all the issues, PRs, and discussion that were raised originally, and
|
||||
it is useful to refer to when contributing code, documentation, or issues with Ratatui.
|
||||
|
||||
We imported all the PRs from the original repository, implemented many of the smaller ones, and
|
||||
made notes on the leftovers. These are marked as draft PRs and labelled as [imported from
|
||||
tui](https://github.com/ratatui/ratatui/pulls?q=is%3Apr+is%3Aopen+label%3A%22imported+from+tui%22).
|
||||
We have documented the current state of those PRs, and anyone is welcome to pick them up and
|
||||
continue the work on them.
|
||||
|
||||
We have not imported 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.
|
||||
|
||||
138
Cargo.toml
138
Cargo.toml
@@ -1,89 +1,57 @@
|
||||
[package]
|
||||
name = "tui"
|
||||
version = "0.16.0"
|
||||
authors = ["Florian Dehau <work@fdehau.com>"]
|
||||
description = """
|
||||
A library to build rich terminal user interfaces or dashboards
|
||||
"""
|
||||
documentation = "https://docs.rs/tui/0.16.0/tui/"
|
||||
keywords = ["tui", "terminal", "dashboard"]
|
||||
repository = "https://github.com/fdehau/tui-rs"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
exclude = ["assets/*", ".github", "Makefile.toml", "CONTRIBUTING.md", "*.log", "tags"]
|
||||
autoexamples = true
|
||||
edition = "2018"
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["ratatui*"]
|
||||
# disabled for now because of the orphan rule on conversions
|
||||
# <https://github.com/ratatui/ratatui/issues/1388#issuecomment-2379895747>
|
||||
exclude = ["ratatui-termion", "ratatui-termwiz"]
|
||||
|
||||
[badges]
|
||||
[workspace.dependencies]
|
||||
ratatui = { path = "ratatui" }
|
||||
ratatui-core = { path = "ratatui-core" }
|
||||
ratatui-crossterm = { path = "ratatui-crossterm" }
|
||||
ratatui-termion = { path = "ratatui-termion" }
|
||||
ratatui-termwiz = { path = "ratatui-termwiz" }
|
||||
ratatui-widgets = { path = "ratatui-widgets" }
|
||||
|
||||
[features]
|
||||
default = ["crossterm"]
|
||||
|
||||
[dependencies]
|
||||
bitflags = "1.3"
|
||||
bitflags = "2.3"
|
||||
cassowary = "0.3"
|
||||
unicode-segmentation = "1.2"
|
||||
unicode-width = "0.1"
|
||||
termion = { version = "1.5", optional = true }
|
||||
crossterm = { version = "0.22", optional = true }
|
||||
serde = { version = "1", optional = true, features = ["derive"]}
|
||||
compact_str = "0.8.0"
|
||||
crossterm = { version = "0.28.1" }
|
||||
document-features = { version = "0.2.7" }
|
||||
instability = "0.3.1"
|
||||
itertools = "0.13"
|
||||
lru = "0.12.0"
|
||||
paste = "1.0.2"
|
||||
palette = { version = "0.7.6" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
strum = { version = "0.26.3", features = ["derive"] }
|
||||
termion = { version = "4.0.0" }
|
||||
termwiz = { version = "0.22.0" }
|
||||
time = { version = "0.3.11", features = ["local-offset"] }
|
||||
unicode-segmentation = "1.10"
|
||||
unicode-truncate = "1"
|
||||
unicode-width = "=0.1.13"
|
||||
|
||||
[dev-dependencies]
|
||||
rand = "0.8"
|
||||
argh = "0.1"
|
||||
|
||||
[[example]]
|
||||
name = "barchart"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "block"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "canvas"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "chart"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "custom_widget"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "gauge"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "layout"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "list"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "paragraph"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "popup"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "sparkline"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "table"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "tabs"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "user_input"
|
||||
required-features = ["crossterm"]
|
||||
# dev-dependencies
|
||||
argh = "0.1.12"
|
||||
color-eyre = "0.6.2"
|
||||
criterion = { version = "0.5.1", features = ["html_reports"] }
|
||||
fakeit = "1.1"
|
||||
font8x8 = "0.3.1"
|
||||
futures = "0.3.30"
|
||||
indoc = "2"
|
||||
octocrab = "0.40.0"
|
||||
pretty_assertions = "1.4.0"
|
||||
rand = "0.8.5"
|
||||
rand_chacha = "0.3.1"
|
||||
rstest = "0.22.0"
|
||||
serde_json = "1.0.109"
|
||||
tokio = { version = "1.39.2", features = [
|
||||
"rt",
|
||||
"macros",
|
||||
"time",
|
||||
"rt-multi-thread",
|
||||
] }
|
||||
tracing = "0.1.40"
|
||||
tracing-appender = "0.2.3"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
|
||||
7
FUNDING.json
Normal file
7
FUNDING.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"drips": {
|
||||
"ethereum": {
|
||||
"ownedBy": "0x6053C8984f4F214Ad12c4653F28514E1E09213B5"
|
||||
}
|
||||
}
|
||||
}
|
||||
3
LICENSE
3
LICENSE
@@ -1,6 +1,7 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Florian Dehau
|
||||
Copyright (c) 2016-2022 Florian Dehau
|
||||
Copyright (c) 2023-2024 The Ratatui Developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
15
MAINTAINERS.md
Normal file
15
MAINTAINERS.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Maintainers
|
||||
|
||||
This file documents current and past maintainers.
|
||||
|
||||
- [orhun](https://github.com/orhun)
|
||||
- [joshka](https://github.com/joshka)
|
||||
- [kdheepak](https://github.com/kdheepak)
|
||||
- [Valentin271](https://github.com/Valentin271)
|
||||
|
||||
## Past Maintainers
|
||||
|
||||
- [fdehau](https://github.com/fdehau)
|
||||
- [mindoodoo](https://github.com/mindoodoo)
|
||||
- [sayanarijit](https://github.com/sayanarijit)
|
||||
- [EdJoPaTo](https://github.com/EdJoPaTo)
|
||||
192
Makefile.toml
192
Makefile.toml
@@ -1,130 +1,124 @@
|
||||
# configuration for https://github.com/sagiegurari/cargo-make
|
||||
|
||||
[config]
|
||||
skip_core_tasks = true
|
||||
|
||||
[env]
|
||||
# all features except the backend ones
|
||||
NON_BACKEND_FEATURES = "all-widgets,macros,serde"
|
||||
|
||||
[tasks.default]
|
||||
alias = "ci"
|
||||
|
||||
[tasks.ci]
|
||||
run_task = [
|
||||
{ name = "ci-unix", condition = { platforms = ["linux", "mac"] } },
|
||||
{ name = "ci-windows", condition = { platforms = ["windows"] } },
|
||||
]
|
||||
description = "Run continuous integration tasks"
|
||||
dependencies = ["lint", "clippy", "check", "test"]
|
||||
|
||||
[tasks.ci-unix]
|
||||
private = true
|
||||
dependencies = [
|
||||
"fmt",
|
||||
"check-crossterm",
|
||||
"check-termion",
|
||||
"test-crossterm",
|
||||
"test-termion",
|
||||
"clippy-crossterm",
|
||||
"clippy-termion",
|
||||
"test-doc",
|
||||
]
|
||||
[tasks.lint]
|
||||
description = "Lint code style (formatting, typos, docs, markdown)"
|
||||
dependencies = ["lint-format", "lint-typos", "lint-docs"]
|
||||
|
||||
[tasks.ci-windows]
|
||||
private = true
|
||||
dependencies = [
|
||||
"fmt",
|
||||
"check-crossterm",
|
||||
"test-crossterm",
|
||||
"clippy-crossterm",
|
||||
"test-doc",
|
||||
]
|
||||
[tasks.lint-format]
|
||||
description = "Lint code formatting"
|
||||
toolchain = "nightly"
|
||||
command = "cargo"
|
||||
args = ["fmt", "--all", "--check"]
|
||||
|
||||
[tasks.fmt]
|
||||
[tasks.format]
|
||||
description = "Fix code formatting"
|
||||
toolchain = "nightly"
|
||||
command = "cargo"
|
||||
args = ["fmt", "--all"]
|
||||
|
||||
[tasks.lint-typos]
|
||||
description = "Run typo checks"
|
||||
install_crate = { crate_name = "typos-cli", binary = "typos", test_arg = "--version" }
|
||||
command = "typos"
|
||||
|
||||
[tasks.lint-docs]
|
||||
description = "Check documentation for errors and warnings"
|
||||
toolchain = "nightly"
|
||||
command = "cargo"
|
||||
args = [
|
||||
"fmt",
|
||||
"--all",
|
||||
"rustdoc",
|
||||
"--all-features",
|
||||
"--",
|
||||
"-Zunstable-options",
|
||||
"--check",
|
||||
"-Dwarnings",
|
||||
]
|
||||
|
||||
[tasks.check-crossterm]
|
||||
env = { TUI_FEATURES = "serde,crossterm" }
|
||||
run_task = "check"
|
||||
|
||||
[tasks.check-termion]
|
||||
env = { TUI_FEATURES = "serde,termion" }
|
||||
run_task = "check"
|
||||
[tasks.lint-markdown]
|
||||
description = "Check markdown files for errors and warnings"
|
||||
command = "markdownlint-cli2"
|
||||
args = ["**/*.md", "!target"]
|
||||
|
||||
[tasks.check]
|
||||
description = "Check code for errors and warnings"
|
||||
command = "cargo"
|
||||
condition = { env_set = ["TUI_FEATURES"] }
|
||||
args = [
|
||||
"check",
|
||||
"--no-default-features",
|
||||
"--features",
|
||||
"${TUI_FEATURES}",
|
||||
"--all-targets",
|
||||
]
|
||||
|
||||
[tasks.build-crossterm]
|
||||
env = { TUI_FEATURES = "serde,crossterm" }
|
||||
run_task = "build"
|
||||
|
||||
[tasks.build-termion]
|
||||
env = { TUI_FEATURES = "serde,termion" }
|
||||
run_task = "build"
|
||||
args = ["check", "--all-targets", "--all-features"]
|
||||
|
||||
[tasks.build]
|
||||
description = "Compile the project"
|
||||
command = "cargo"
|
||||
condition = { env_set = ["TUI_FEATURES"] }
|
||||
args = [
|
||||
"build",
|
||||
"--no-default-features",
|
||||
"--features",
|
||||
"${TUI_FEATURES}",
|
||||
"--all-targets",
|
||||
]
|
||||
|
||||
[tasks.clippy-crossterm]
|
||||
env = { TUI_FEATURES = "serde,crossterm" }
|
||||
run_task = "clippy"
|
||||
|
||||
[tasks.clippy-termion]
|
||||
env = { TUI_FEATURES = "serde,termion" }
|
||||
run_task = "clippy"
|
||||
args = ["build", "--all-targets", "--all-features"]
|
||||
|
||||
[tasks.clippy]
|
||||
description = "Run Clippy for linting"
|
||||
command = "cargo"
|
||||
condition = { env_set = ["TUI_FEATURES"] }
|
||||
args = [
|
||||
"clippy",
|
||||
"--all-targets",
|
||||
"--no-default-features",
|
||||
"--features",
|
||||
"${TUI_FEATURES}",
|
||||
"--all-features",
|
||||
"--tests",
|
||||
"--benches",
|
||||
"--",
|
||||
"-D",
|
||||
"warnings",
|
||||
]
|
||||
|
||||
[tasks.test-crossterm]
|
||||
env = { TUI_FEATURES = "serde,crossterm" }
|
||||
run_task = "test"
|
||||
|
||||
[tasks.test-termion]
|
||||
env = { TUI_FEATURES = "serde,termion" }
|
||||
run_task = "test"
|
||||
[tasks.install-nextest]
|
||||
description = "Install cargo-nextest"
|
||||
install_crate = { crate_name = "cargo-nextest", binary = "cargo-nextest", test_arg = "--help" }
|
||||
|
||||
[tasks.test]
|
||||
description = "Run tests"
|
||||
run_task = { name = ["test-lib", "test-doc"] }
|
||||
|
||||
[tasks.test-lib]
|
||||
description = "Run default tests"
|
||||
dependencies = ["install-nextest"]
|
||||
command = "cargo"
|
||||
condition = { env_set = ["TUI_FEATURES"] }
|
||||
args = [
|
||||
"test",
|
||||
"--no-default-features",
|
||||
"--features",
|
||||
"${TUI_FEATURES}",
|
||||
"--lib",
|
||||
"--tests",
|
||||
"--examples",
|
||||
]
|
||||
args = ["nextest", "run", "--all-targets", "--all-features"]
|
||||
|
||||
[tasks.test-doc]
|
||||
description = "Run documentation tests"
|
||||
command = "cargo"
|
||||
args = ["test", "--doc", "--all-features"]
|
||||
|
||||
[tasks.test-backend]
|
||||
# takes a command line parameter to specify the backend to test (e.g. "crossterm")
|
||||
description = "Run backend-specific tests"
|
||||
dependencies = ["install-nextest"]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"test",
|
||||
"--doc",
|
||||
"nextest",
|
||||
"run",
|
||||
"--all-targets",
|
||||
"--no-default-features",
|
||||
"--features",
|
||||
"${NON_BACKEND_FEATURES},${@}",
|
||||
]
|
||||
|
||||
[tasks.coverage]
|
||||
description = "Generate code coverage report"
|
||||
command = "cargo"
|
||||
args = [
|
||||
"llvm-cov",
|
||||
"--lcov",
|
||||
"--output-path",
|
||||
"target/lcov.info",
|
||||
"--all-features",
|
||||
]
|
||||
|
||||
[tasks.run-example]
|
||||
@@ -132,21 +126,21 @@ private = true
|
||||
condition = { env_set = ["TUI_EXAMPLE_NAME"] }
|
||||
command = "cargo"
|
||||
args = [
|
||||
"run",
|
||||
"--release",
|
||||
"--example",
|
||||
"${TUI_EXAMPLE_NAME}"
|
||||
"run",
|
||||
"--release",
|
||||
"--example",
|
||||
"${TUI_EXAMPLE_NAME}",
|
||||
"--features",
|
||||
"all-widgets",
|
||||
]
|
||||
|
||||
[tasks.build-examples]
|
||||
description = "Compile project examples"
|
||||
command = "cargo"
|
||||
args = [
|
||||
"build",
|
||||
"--examples",
|
||||
"--release"
|
||||
]
|
||||
args = ["build", "--examples", "--release", "--features", "all-widgets"]
|
||||
|
||||
[tasks.run-examples]
|
||||
description = "Run project examples"
|
||||
dependencies = ["build-examples"]
|
||||
script = '''
|
||||
#!@duckscript
|
||||
|
||||
488
README.md
488
README.md
@@ -1,119 +1,433 @@
|
||||
# tui-rs
|
||||
<details>
|
||||
<summary>Table of Contents</summary>
|
||||
|
||||
[](https://github.com/fdehau/tui-rs/actions?query=workflow%3ACI+)
|
||||
[](https://crates.io/crates/tui)
|
||||
[](https://docs.rs/crate/tui/)
|
||||
- [Ratatui](#ratatui)
|
||||
- [Installation](#installation)
|
||||
- [Introduction](#introduction)
|
||||
- [Other documentation](#other-documentation)
|
||||
- [Quickstart](#quickstart)
|
||||
- [Initialize and restore the terminal](#initialize-and-restore-the-terminal)
|
||||
- [Drawing the UI](#drawing-the-ui)
|
||||
- [Handling events](#handling-events)
|
||||
- [Example](#example)
|
||||
- [Layout](#layout)
|
||||
- [Text and styling](#text-and-styling)
|
||||
- [Status of this fork](#status-of-this-fork)
|
||||
- [Widgets](#widgets)
|
||||
- [Built in](#built-in)
|
||||
- [Third-party libraries, bootstrapping templates and widgets](#third-party-libraries-bootstrapping-templates-and-widgets)
|
||||
- [Apps](#apps)
|
||||
- [Alternatives](#alternatives)
|
||||
- [Acknowledgments](#acknowledgments)
|
||||
- [License](#license)
|
||||
|
||||
<img src="./assets/demo.gif" alt="Demo cast under Linux Termite with Inconsolata font 12pt">
|
||||
</details>
|
||||
|
||||
`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).
|
||||
<!-- cargo-rdme start -->
|
||||
|
||||
The library supports multiple backends:
|
||||
- [crossterm](https://github.com/crossterm-rs/crossterm) [default]
|
||||
- [termion](https://github.com/ticki/termion)
|
||||

|
||||
|
||||
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.
|
||||
<div align="center">
|
||||
|
||||
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.
|
||||
[![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![Deps.rs
|
||||
Badge]][Deps.rs]<br> [![Codecov Badge]][Codecov] [![License Badge]](./LICENSE) [![Sponsors
|
||||
Badge]][GitHub Sponsors]<br> [![Discord Badge]][Discord Server] [![Matrix Badge]][Matrix]
|
||||
[![Forum Badge]][Forum]<br>
|
||||
|
||||
### Rust version requirements
|
||||
[Ratatui Website] · [API Docs] · [Examples] · [Changelog] · [Breaking Changes]<br>
|
||||
[Contributing] · [Report a bug] · [Request a Feature] · [Create a Pull Request]
|
||||
|
||||
Since version 0.17.0, `tui` requires **rustc version 1.52.1 or greater**.
|
||||
</div>
|
||||
|
||||
### [Documentation](https://docs.rs/tui)
|
||||
# Ratatui
|
||||
|
||||
### Demo
|
||||
[Ratatui][Ratatui Website] is a crate for cooking up terminal user interfaces in Rust. It is a
|
||||
lightweight library that provides a set of widgets and utilities to build complex Rust TUIs.
|
||||
Ratatui was forked from the [tui-rs] crate in 2023 in order to continue its development.
|
||||
|
||||
The demo shown in the gif can be run with all available backends.
|
||||
## Installation
|
||||
|
||||
```
|
||||
# crossterm
|
||||
cargo run --example demo --release -- --tick-rate 200
|
||||
# termion
|
||||
cargo run --example demo --no-default-features --features=termion --release -- --tick-rate 200
|
||||
Add `ratatui` as a dependency to your cargo.toml:
|
||||
|
||||
```shell
|
||||
cargo add ratatui
|
||||
```
|
||||
|
||||
where `tick-rate` is the UI refresh rate in ms.
|
||||
Ratatui uses [Crossterm] by default as it works on most platforms. See the [Installation]
|
||||
section of the [Ratatui Website] for more details on how to use other backends ([Termion] /
|
||||
[Termwiz]).
|
||||
|
||||
The UI code is in [examples/demo/ui.rs](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/demo/ui.rs) while the
|
||||
application state is in [examples/demo/app.rs](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/demo/app.rs).
|
||||
## Introduction
|
||||
|
||||
If the user interface contains glyphs that are not displayed correctly by your terminal, you may want to run
|
||||
the demo without those symbols:
|
||||
Ratatui is based on the principle of immediate rendering with intermediate buffers. This means
|
||||
that for each frame, your app must render all widgets that are supposed to be part of the UI.
|
||||
This is in contrast to the retained mode style of rendering where widgets are updated and then
|
||||
automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Website]
|
||||
for more info.
|
||||
|
||||
```
|
||||
cargo run --example demo --release -- --tick-rate 200 --enhanced-graphics false
|
||||
You can also watch the [FOSDEM 2024 talk] about Ratatui which gives a brief introduction to
|
||||
terminal user interfaces and showcases the features of Ratatui, along with a hello world demo.
|
||||
|
||||
## Other documentation
|
||||
|
||||
- [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials
|
||||
- [Ratatui Forum][Forum] - a place to ask questions and discuss the library
|
||||
- [API Docs] - the full API documentation for the library on docs.rs.
|
||||
- [Examples] - a collection of examples that demonstrate how to use the library.
|
||||
- [Contributing] - Please read this if you are interested in contributing to the project.
|
||||
- [Changelog] - generated by [git-cliff] utilizing [Conventional Commits].
|
||||
- [Breaking Changes] - a list of breaking changes in the library.
|
||||
|
||||
## 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
|
||||
the [Examples] directory. For more guidance on different ways to structure your application see
|
||||
the [Application Patterns] and [Hello World tutorial] sections in the [Ratatui Website] and the
|
||||
various [Examples]. There are also several starter templates available in the [templates]
|
||||
repository.
|
||||
|
||||
Every application built with `ratatui` needs to implement the following steps:
|
||||
|
||||
- Initialize the terminal
|
||||
- A main loop to:
|
||||
- Handle input events
|
||||
- Draw the UI
|
||||
- Restore the terminal state
|
||||
|
||||
The library contains a [`prelude`] module that re-exports the most commonly used traits and
|
||||
types for convenience. Most examples in the documentation will use this instead of showing the
|
||||
full path of each type.
|
||||
|
||||
### Initialize and restore the terminal
|
||||
|
||||
The [`Terminal`] type is the main entry point for any Ratatui application. It is a light
|
||||
abstraction over a choice of [`Backend`] implementations that provides functionality to draw
|
||||
each frame, clear the screen, hide the cursor, etc. It is parametrized over any type that
|
||||
implements the [`Backend`] trait which has implementations for [Crossterm], [Termion] and
|
||||
[Termwiz].
|
||||
|
||||
Most applications should enter the Alternate Screen when starting and leave it when exiting and
|
||||
also enable raw mode to disable line buffering and enable reading key events. See the [`backend`
|
||||
module] and the [Backends] section of the [Ratatui Website] for more info.
|
||||
|
||||
### Drawing the UI
|
||||
|
||||
The drawing logic is delegated to a closure that takes a [`Frame`] instance as argument. The
|
||||
[`Frame`] provides the size of the area to draw to and allows the app to render any [`Widget`]
|
||||
using the provided [`render_widget`] method. After this closure returns, a diff is performed and
|
||||
only the changes are drawn to the terminal. See the [Widgets] section of the [Ratatui Website]
|
||||
for more info.
|
||||
|
||||
### Handling events
|
||||
|
||||
Ratatui does not include any input handling. Instead event handling can be implemented by
|
||||
calling backend library methods directly. See the [Handling Events] section of the [Ratatui
|
||||
Website] for more info. For example, if you are using [Crossterm], you can use the
|
||||
[`crossterm::event`] module to handle events.
|
||||
|
||||
### Example
|
||||
|
||||
```rust
|
||||
use std::io::{self, stdout};
|
||||
|
||||
use ratatui::{
|
||||
backend::CrosstermBackend,
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
terminal::{
|
||||
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
||||
},
|
||||
ExecutableCommand,
|
||||
},
|
||||
widgets::{Block, Paragraph},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
enable_raw_mode()?;
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
|
||||
|
||||
let mut should_quit = false;
|
||||
while !should_quit {
|
||||
terminal.draw(ui)?;
|
||||
should_quit = handle_events()?;
|
||||
}
|
||||
|
||||
disable_raw_mode()?;
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_events() -> io::Result<bool> {
|
||||
if event::poll(std::time::Duration::from_millis(50))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == event::KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn ui(frame: &mut Frame) {
|
||||
frame.render_widget(
|
||||
Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")),
|
||||
frame.area(),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Widgets
|
||||
Running this example produces the following output:
|
||||
|
||||
The library comes with the following list of widgets:
|
||||
![docsrs-hello]
|
||||
|
||||
* [Block](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/block.rs)
|
||||
* [Gauge](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/gauge.rs)
|
||||
* [Sparkline](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/sparkline.rs)
|
||||
* [Chart](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/chart.rs)
|
||||
* [BarChart](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/barchart.rs)
|
||||
* [List](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/list.rs)
|
||||
* [Table](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/table.rs)
|
||||
* [Paragraph](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/paragraph.rs)
|
||||
* [Canvas (with line, point cloud, map)](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/canvas.rs)
|
||||
* [Tabs](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/tabs.rs)
|
||||
## Layout
|
||||
|
||||
Click on each item to see the source of the example. Run the examples with with
|
||||
cargo (e.g. to run the gauge example `cargo run --example gauge`), and quit by pressing `q`.
|
||||
The library comes with a basic yet useful layout management object called [`Layout`] which
|
||||
allows you to split the available space into multiple areas and then render widgets in each
|
||||
area. This lets you describe a responsive terminal UI by nesting layouts. See the [Layout]
|
||||
section of the [Ratatui Website] for more info.
|
||||
|
||||
You can run all examples by running `cargo make run-examples` (require
|
||||
`cargo-make` that can be installed with `cargo install cargo-make`).
|
||||
```rust
|
||||
use ratatui::{
|
||||
layout::{Constraint, Layout},
|
||||
widgets::Block,
|
||||
Frame,
|
||||
};
|
||||
|
||||
### Third-party widgets
|
||||
fn ui(frame: &mut Frame) {
|
||||
let [title_area, main_area, status_area] = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(1),
|
||||
])
|
||||
.areas(frame.area());
|
||||
let [left_area, right_area] =
|
||||
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.areas(main_area);
|
||||
|
||||
* [tui-logger](https://github.com/gin66/tui-logger)
|
||||
frame.render_widget(Block::bordered().title("Title Bar"), title_area);
|
||||
frame.render_widget(Block::bordered().title("Status Bar"), status_area);
|
||||
frame.render_widget(Block::bordered().title("Left"), left_area);
|
||||
frame.render_widget(Block::bordered().title("Right"), right_area);
|
||||
}
|
||||
```
|
||||
|
||||
### Apps using tui
|
||||
Running this example produces the following output:
|
||||
|
||||
* [spotify-tui](https://github.com/Rigellute/spotify-tui)
|
||||
* [bandwhich](https://github.com/imsnif/bandwhich)
|
||||
* [kmon](https://github.com/orhun/kmon)
|
||||
* [gpg-tui](https://github.com/orhun/gpg-tui)
|
||||
* [ytop](https://github.com/cjbassi/ytop)
|
||||
* [zenith](https://github.com/bvaisvil/zenith)
|
||||
* [bottom](https://github.com/ClementTsang/bottom)
|
||||
* [oha](https://github.com/hatoo/oha)
|
||||
* [gitui](https://github.com/extrawurst/gitui)
|
||||
* [rust-sadari-cli](https://github.com/24seconds/rust-sadari-cli)
|
||||
* [desed](https://github.com/SoptikHa2/desed)
|
||||
* [diskonaut](https://github.com/imsnif/diskonaut)
|
||||
* [tickrs](https://github.com/tarkah/tickrs)
|
||||
* [rusty-krab-manager](https://github.com/aryakaul/rusty-krab-manager)
|
||||
* [termchat](https://github.com/lemunozm/termchat)
|
||||
* [taskwarrior-tui](https://github.com/kdheepak/taskwarrior-tui)
|
||||
* [gping](https://github.com/orf/gping/)
|
||||
* [Vector](https://vector.dev)
|
||||
* [KDash](https://github.com/kdash-rs/kdash)
|
||||
* [xplr](https://github.com/sayanarijit/xplr)
|
||||
* [minesweep](https://github.com/cpcloud/minesweep-rs)
|
||||
* [Battleship.rs](https://github.com/deepu105/battleship-rs)
|
||||
* [termscp](https://github.com/veeso/termscp)
|
||||
* [joshuto](https://github.com/kamiyaa/joshuto)
|
||||
* [adsb_deku/radar](https://github.com/wcampbell0x2a/adsb_deku#radar-tui)
|
||||
* [hoard](https://github.com/Hyde46/hoard)
|
||||
![docsrs-layout]
|
||||
|
||||
### Alternatives
|
||||
## Text and styling
|
||||
|
||||
You might want to checkout [Cursive](https://github.com/gyscos/Cursive) for an
|
||||
alternative solution to build text user interfaces in Rust.
|
||||
The [`Text`], [`Line`] and [`Span`] types are the building blocks of the library and are used in
|
||||
many places. [`Text`] is a list of [`Line`]s and a [`Line`] is a list of [`Span`]s. A [`Span`]
|
||||
is a string with a specific style.
|
||||
|
||||
The [`style` module] provides types that represent the various styling options. The most
|
||||
important one is [`Style`] which represents the foreground and background colors and the text
|
||||
attributes of a [`Span`]. The [`style` module] also provides a [`Stylize`] trait that allows
|
||||
short-hand syntax to apply a style to widgets and text. See the [Styling Text] section of the
|
||||
[Ratatui Website] for more info.
|
||||
|
||||
```rust
|
||||
use ratatui::{
|
||||
layout::{Constraint, Layout},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
|
||||
fn ui(frame: &mut Frame) {
|
||||
let areas = Layout::vertical([Constraint::Length(1); 4]).split(frame.area());
|
||||
|
||||
let line = Line::from(vec![
|
||||
Span::raw("Hello "),
|
||||
Span::styled(
|
||||
"World",
|
||||
Style::new()
|
||||
.fg(Color::Green)
|
||||
.bg(Color::White)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
"!".red().on_light_yellow().italic(),
|
||||
]);
|
||||
frame.render_widget(line, areas[0]);
|
||||
|
||||
// using the short-hand syntax and implicit conversions
|
||||
let paragraph = Paragraph::new("Hello World!".red().on_white().bold());
|
||||
frame.render_widget(paragraph, areas[1]);
|
||||
|
||||
// style the whole widget instead of just the text
|
||||
let paragraph = Paragraph::new("Hello World!").style(Style::new().red().on_white());
|
||||
frame.render_widget(paragraph, areas[2]);
|
||||
|
||||
// use the simpler short-hand syntax
|
||||
let paragraph = Paragraph::new("Hello World!").blue().on_yellow();
|
||||
frame.render_widget(paragraph, areas[3]);
|
||||
}
|
||||
```
|
||||
|
||||
Running this example produces the following output:
|
||||
|
||||
![docsrs-styling]
|
||||
|
||||
[Ratatui Website]: https://ratatui.rs/
|
||||
[Installation]: https://ratatui.rs/installation/
|
||||
[Rendering]: https://ratatui.rs/concepts/rendering/
|
||||
[Application Patterns]: https://ratatui.rs/concepts/application-patterns/
|
||||
[Hello World tutorial]: https://ratatui.rs/tutorials/hello-world/
|
||||
[Backends]: https://ratatui.rs/concepts/backends/
|
||||
[Widgets]: https://ratatui.rs/how-to/widgets/
|
||||
[Handling Events]: https://ratatui.rs/concepts/event-handling/
|
||||
[Layout]: https://ratatui.rs/how-to/layout/
|
||||
[Styling Text]: https://ratatui.rs/how-to/render/style-text/
|
||||
[templates]: https://github.com/ratatui/templates/
|
||||
[Examples]: https://github.com/ratatui/ratatui/tree/main/examples/README.md
|
||||
[Report a bug]: https://github.com/ratatui/ratatui/issues/new?labels=bug&projects=&template=bug_report.md
|
||||
[Request a Feature]: https://github.com/ratatui/ratatui/issues/new?labels=enhancement&projects=&template=feature_request.md
|
||||
[Create a Pull Request]: https://github.com/ratatui/ratatui/compare
|
||||
[git-cliff]: https://git-cliff.org
|
||||
[Conventional Commits]: https://www.conventionalcommits.org
|
||||
[API Docs]: https://docs.rs/ratatui
|
||||
[Changelog]: https://github.com/ratatui/ratatui/blob/main/CHANGELOG.md
|
||||
[Contributing]: https://github.com/ratatui/ratatui/blob/main/CONTRIBUTING.md
|
||||
[Breaking Changes]: https://github.com/ratatui/ratatui/blob/main/BREAKING-CHANGES.md
|
||||
[FOSDEM 2024 talk]: https://www.youtube.com/watch?v=NU0q6NOLJ20
|
||||
[docsrs-hello]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-hello.png?raw=true
|
||||
[docsrs-layout]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-layout.png?raw=true
|
||||
[docsrs-styling]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-styling.png?raw=true
|
||||
[`Frame`]: terminal::Frame
|
||||
[`render_widget`]: terminal::Frame::render_widget
|
||||
[`Widget`]: widgets::Widget
|
||||
[`Layout`]: layout::Layout
|
||||
[`Text`]: text::Text
|
||||
[`Line`]: text::Line
|
||||
[`Span`]: text::Span
|
||||
[`Style`]: style::Style
|
||||
[`style` module]: style
|
||||
[`Stylize`]: style::Stylize
|
||||
[`Backend`]: backend::Backend
|
||||
[`backend` module]: backend
|
||||
[`crossterm::event`]: https://docs.rs/crossterm/latest/crossterm/event/index.html
|
||||
[Crate]: https://crates.io/crates/ratatui
|
||||
[Crossterm]: https://crates.io/crates/crossterm
|
||||
[Termion]: https://crates.io/crates/termion
|
||||
[Termwiz]: https://crates.io/crates/termwiz
|
||||
[tui-rs]: https://crates.io/crates/tui
|
||||
[GitHub Sponsors]: https://github.com/sponsors/ratatui
|
||||
[Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square&logoColor=E05D44&color=E05D44
|
||||
[License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square&color=1370D3
|
||||
[CI Badge]: https://img.shields.io/github/actions/workflow/status/ratatui/ratatui/ci.yml?style=flat-square&logo=github
|
||||
[CI Workflow]: https://github.com/ratatui/ratatui/actions/workflows/ci.yml
|
||||
[Codecov Badge]: https://img.shields.io/codecov/c/github/ratatui/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST&color=C43AC3&logoColor=C43AC3
|
||||
[Codecov]: https://app.codecov.io/gh/ratatui/ratatui
|
||||
[Deps.rs Badge]: https://deps.rs/repo/github/ratatui/ratatui/status.svg?style=flat-square
|
||||
[Deps.rs]: https://deps.rs/repo/github/ratatui/ratatui
|
||||
[Discord Badge]: https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square&color=1370D3&logoColor=1370D3
|
||||
[Discord Server]: https://discord.gg/pMCEU9hNEj
|
||||
[Docs Badge]: https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square&logoColor=E05D44
|
||||
[Matrix Badge]: https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix&color=C43AC3
|
||||
[Matrix]: https://matrix.to/#/#ratatui:matrix.org
|
||||
[Forum Badge]: https://img.shields.io/discourse/likes?server=https%3A%2F%2Fforum.ratatui.rs&style=flat-square&logo=discourse&label=forum&color=C43AC3
|
||||
[Forum]: https://forum.ratatui.rs
|
||||
[Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui?logo=github&style=flat-square&color=1370D3
|
||||
|
||||
<!-- cargo-rdme end -->
|
||||
|
||||
## 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 is also a [Matrix](https://matrix.org/) bridge available at
|
||||
[#ratatui:matrix.org](https://matrix.to/#/#ratatui:matrix.org).
|
||||
|
||||
While we do utilize Discord for coordinating, it's not essential for contributing. We have recently
|
||||
launched the [Ratatui Forum][Forum], and our primary open-source workflow is centered around GitHub.
|
||||
For bugs and features, we rely on GitHub. Please [Report a bug], [Request a Feature] or [Create a
|
||||
Pull Request].
|
||||
|
||||
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.
|
||||
|
||||
## Widgets
|
||||
|
||||
### Built in
|
||||
|
||||
The library comes with the following
|
||||
[widgets](https://docs.rs/ratatui/latest/ratatui/widgets/index.html):
|
||||
|
||||
- [BarChart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.BarChart.html)
|
||||
- [Block](https://docs.rs/ratatui/latest/ratatui/widgets/block/struct.Block.html)
|
||||
- [Calendar](https://docs.rs/ratatui/latest/ratatui/widgets/calendar/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)
|
||||
- [Chart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Chart.html)
|
||||
- [Clear](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Clear.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)
|
||||
- [Scrollbar](https://docs.rs/ratatui/latest/ratatui/widgets/scrollbar/struct.Scrollbar.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 widget has an associated example which can be found in the [Examples] folder. Run each example
|
||||
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
|
||||
`ratatui::text::Text`
|
||||
- [color-to-tui](https://github.com/uttarayan21/color-to-tui) — Parse hex colors to
|
||||
`ratatui::style::Color`
|
||||
- [templates](https://github.com/ratatui/templates) — Starter templates for
|
||||
bootstrapping a Rust TUI application with Ratatui & crossterm
|
||||
- [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 [awesome-ratatui](https://github.com/ratatui/awesome-ratatui) for a curated list of
|
||||
awesome apps/libraries built with `ratatui`!
|
||||
|
||||
## Alternatives
|
||||
|
||||
You might want to checkout [Cursive](https://github.com/gyscos/Cursive) for an alternative solution
|
||||
to build text user interfaces in Rust.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
Special thanks to [**Pavel Fomchenkov**](https://github.com/nawok) for his work in designing **an
|
||||
awesome logo** for the ratatui project and ratatui organization.
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
[MIT](./LICENSE)
|
||||
|
||||
51
RELEASE.md
Normal file
51
RELEASE.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Creating a Release
|
||||
|
||||
Our release strategy is:
|
||||
|
||||
> Release major versions with detailed summaries when necessary, while releasing minor versions
|
||||
> weekly or as needed without extensive announcements.
|
||||
>
|
||||
> Versioning scheme being `0.x.y`, where `x` is the major version and `y` is the minor version.
|
||||
|
||||
[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 if necessary. The preferred tool for this is
|
||||
[vhs](https://github.com/charmbracelet/vhs) (installation instructions in README).
|
||||
|
||||
```shell
|
||||
cargo build --example demo2
|
||||
vhs examples/demo2.tape
|
||||
```
|
||||
|
||||
1. Switch branches to the images branch and copy demo2.gif to examples/, commit, and push.
|
||||
1. Grab the permalink from <https://github.com/ratatui/ratatui/blob/images/examples/demo2.gif> and
|
||||
append `?raw=true` to redirect to the actual image url. Then update the link in the main README.
|
||||
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. Ensure that any breaking changes are documented in [BREAKING-CHANGES.md](./BREAKING-CHANGES.md)
|
||||
1. Commit and push the changes.
|
||||
1. Create a new tag: `git tag -a v[0.x.y]`
|
||||
1. Push the tag: `git push --tags`
|
||||
1. Wait for [Continuous Deployment](https://github.com/ratatui/ratatui/actions) workflow to
|
||||
finish.
|
||||
|
||||
## Alpha Releases
|
||||
|
||||
Alpha releases are automatically released every Saturday via [cd.yml](./.github/workflows/cd.yml)
|
||||
and can be manually be created when necessary by triggering the [Continuous
|
||||
Deployment](https://github.com/ratatui/ratatui/actions/workflows/cd.yml) workflow.
|
||||
|
||||
We automatically release an alpha release with a patch level bump + alpha.num weekly (and when we
|
||||
need to manually). E.g. the last release was 0.22.0, and the most recent alpha release is
|
||||
0.22.1-alpha.1.
|
||||
|
||||
These releases will have whatever happened to be in main at the time of release, so they're useful
|
||||
for apps that need to get releases from crates.io, but may contain more bugs and be generally less
|
||||
tested than normal releases.
|
||||
|
||||
See [#147](https://github.com/ratatui/ratatui/issues/147) and
|
||||
[#359](https://github.com/ratatui/ratatui/pull/359) for more info on the alpha release process.
|
||||
9
SECURITY.md
Normal file
9
SECURITY.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
We only support the latest version of this crate.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
To report secuirity vulnerability, please use the form at <https://github.com/ratatui/ratatui/security/advisories/new>
|
||||
BIN
assets/demo.gif
BIN
assets/demo.gif
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 MiB |
BIN
assets/favicon.ico
Normal file
BIN
assets/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
assets/logo.png
Normal file
BIN
assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
112
bacon.toml
Normal file
112
bacon.toml
Normal file
@@ -0,0 +1,112 @@
|
||||
# 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.check-crossterm]
|
||||
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "crossterm"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.check-termion]
|
||||
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "termion"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.check-termwiz]
|
||||
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "termwiz"]
|
||||
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.test-unit]
|
||||
command = [
|
||||
"cargo", "test",
|
||||
"--lib",
|
||||
"--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
|
||||
|
||||
[jobs.coverage]
|
||||
command = [
|
||||
"cargo", "llvm-cov",
|
||||
"--lcov", "--output-path", "target/lcov.info",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
]
|
||||
|
||||
[jobs.coverage-unit-tests-only]
|
||||
command = [
|
||||
"cargo", "llvm-cov",
|
||||
"--lcov", "--output-path", "target/lcov.info",
|
||||
"--lib",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
]
|
||||
|
||||
# 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"
|
||||
v = "job:coverage"
|
||||
ctrl-v = "job:coverage-unit-tests-only"
|
||||
u = "job:test-unit"
|
||||
n = "job:nextest"
|
||||
148
cliff.toml
Normal file
148
cliff.toml
Normal file
@@ -0,0 +1,148 @@
|
||||
# git-cliff ~ configuration file
|
||||
# https://git-cliff.org/docs/configuration
|
||||
|
||||
[remote.github]
|
||||
owner = "ratatui"
|
||||
repo = "ratatui"
|
||||
|
||||
[changelog]
|
||||
# changelog header
|
||||
header = """
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
<!-- ignore lint rules that are often triggered by content generated from commits / git-cliff -->
|
||||
<!-- markdownlint-disable line-length no-bare-urls ul-style emphasis-style -->
|
||||
"""
|
||||
# template for the changelog body
|
||||
# https://keats.github.io/tera/docs/#introduction
|
||||
# note that the - before / after the % controls whether whitespace is rendered between each line.
|
||||
# Getting this right so that the markdown renders with the correct number of lines between headings
|
||||
# code fences and list items is pretty finicky. Note also that the 4 backticks in the commit macro
|
||||
# is intentional as this escapes any backticks in the commit body.
|
||||
body = """
|
||||
{%- if not version %}
|
||||
## [unreleased]
|
||||
{% else -%}
|
||||
## [{{ version }}](https://github.com/ratatui/ratatui/releases/tag/{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% endif -%}
|
||||
|
||||
{% macro commit(commit) -%}
|
||||
- [{{ commit.id | truncate(length=7, end="") }}]({{ "https://github.com/ratatui/ratatui/commit/" ~ commit.id }}) \
|
||||
*({{commit.scope | default(value = "uncategorized") | lower }})* {{ commit.message | upper_first | trim }}\
|
||||
{% if commit.github.username %} by @{{ commit.github.username }}{%- endif -%}\
|
||||
{% if commit.github.pr_number %} in [#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}){%- endif %}\
|
||||
{%- if commit.breaking %} [**breaking**]{% endif %}
|
||||
{%- if commit.body %}\n\n{{ commit.body | indent(prefix=" > ", first=true, blank=true) }}
|
||||
{%- endif %}
|
||||
{%- for footer in commit.footers %}\n
|
||||
{%- if footer.token != "Signed-off-by" and footer.token != "Co-authored-by" %}
|
||||
>
|
||||
{{ footer.token | indent(prefix=" > ", first=true, blank=true) }}
|
||||
{{- footer.separator }}
|
||||
{{- footer.value| indent(prefix=" > ", first=false, blank=true) }}
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
{% endmacro -%}
|
||||
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
### {{ group | striptags | trim | upper_first }}
|
||||
{% for commit in commits | filter(attribute="scope") | sort(attribute="scope") %}
|
||||
{{ self::commit(commit=commit) }}
|
||||
{%- endfor -%}
|
||||
{% for commit in commits %}
|
||||
{%- if not commit.scope %}
|
||||
{{ self::commit(commit=commit) }}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- endfor %}
|
||||
|
||||
{% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
|
||||
### New Contributors
|
||||
{%- endif %}\
|
||||
{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
|
||||
* @{{ contributor.username }} made their first contribution
|
||||
{%- if contributor.pr_number %} in \
|
||||
[#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
|
||||
{%- endif %}
|
||||
{%- endfor -%}
|
||||
|
||||
{% if version %}
|
||||
{% if previous.version %}
|
||||
**Full Changelog**: {{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }}
|
||||
{% endif %}
|
||||
{% else -%}
|
||||
{% raw %}\n{% endraw %}
|
||||
{% endif %}
|
||||
|
||||
{%- macro remote_url() -%}
|
||||
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}\
|
||||
{% endmacro %}
|
||||
"""
|
||||
|
||||
|
||||
# remove the leading and trailing whitespace from the template
|
||||
trim = false
|
||||
# changelog footer
|
||||
footer = """
|
||||
<!-- generated by git-cliff -->
|
||||
"""
|
||||
postprocessors = [
|
||||
{ pattern = '<!-- Please read CONTRIBUTING.md before submitting any pull request. -->', replace = "" },
|
||||
{ pattern = '>---+\n', replace = '' },
|
||||
{ pattern = ' +\n', replace = "\n" },
|
||||
]
|
||||
|
||||
[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 = "" },
|
||||
{ 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\\(deps\\)", skip = true },
|
||||
{ message = "^chore\\(changelog\\)", skip = true },
|
||||
{ message = "^[cC]hore", 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" },
|
||||
# handle some old commits styles from pre 0.4
|
||||
{ message = "^(Buffer|buffer|Frame|frame|Gauge|gauge|Paragraph|paragraph):", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
{ message = "^\\[", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
]
|
||||
|
||||
# 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 = "alpha"
|
||||
# sort the tags topologically
|
||||
topo_order = false
|
||||
# sort the commits inside sections by oldest/newest order
|
||||
sort_commits = "newest"
|
||||
17
clippy.toml
Normal file
17
clippy.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
avoid-breaking-exported-api = false
|
||||
|
||||
# https://rust-lang.github.io/rust-clippy/master/index.html#/multiple_crate_versions
|
||||
# ratatui -> bitflags v2.3
|
||||
# termwiz -> wezterm-blob-leases -> mac_address -> nix -> bitflags v1.3.2
|
||||
# crossterm -> all the windows- deps https://github.com/ratatui/ratatui/pull/1064#issuecomment-2078848980
|
||||
allowed-duplicate-crates = [
|
||||
"bitflags",
|
||||
"windows-targets",
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
14
codecov.yml
Normal file
14
codecov.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
coverage: # https://docs.codecov.com/docs/codecovyml-reference#coverage
|
||||
precision: 1 # e.g. 89.1%
|
||||
round: down
|
||||
range: 85..100 # https://docs.codecov.com/docs/coverage-configuration#section-range
|
||||
status: # https://docs.codecov.com/docs/commit-status
|
||||
project:
|
||||
default:
|
||||
threshold: 1% # Avoid false negatives
|
||||
ignore:
|
||||
- "examples"
|
||||
- "benches"
|
||||
comment: # https://docs.codecov.com/docs/pull-request-comments
|
||||
# make the comments less noisy
|
||||
require_changes: true
|
||||
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",
|
||||
]
|
||||
25
deny.toml
Normal file
25
deny.toml
Normal file
@@ -0,0 +1,25 @@
|
||||
# configuration for https://github.com/EmbarkStudios/cargo-deny
|
||||
|
||||
[licenses]
|
||||
version = 2
|
||||
confidence-threshold = 0.8
|
||||
allow = [
|
||||
"Apache-2.0",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"Unicode-DFS-2016",
|
||||
"WTFPL",
|
||||
]
|
||||
|
||||
[advisories]
|
||||
version = 2
|
||||
|
||||
[bans]
|
||||
multiple-versions = "allow"
|
||||
|
||||
[sources]
|
||||
unknown-registry = "deny"
|
||||
unknown-git = "warn"
|
||||
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||
@@ -1,161 +0,0 @@
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
widgets::{BarChart, Block, Borders},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
struct App<'a> {
|
||||
data: Vec<(&'a str, u64)>,
|
||||
}
|
||||
|
||||
impl<'a> App<'a> {
|
||||
fn new() -> App<'a> {
|
||||
App {
|
||||
data: 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),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn on_tick(&mut self) {
|
||||
let value = self.data.pop().unwrap();
|
||||
self.data.insert(0, value);
|
||||
}
|
||||
}
|
||||
|
||||
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(());
|
||||
}
|
||||
}
|
||||
}
|
||||
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(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]);
|
||||
|
||||
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,119 +0,0 @@
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Alignment, Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::Span,
|
||||
widgets::{Block, BorderType, 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(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
|
||||
let size = f.size();
|
||||
|
||||
// Surrounding block
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title("Main block with round corners")
|
||||
.title_alignment(Alignment::Center)
|
||||
.border_type(BorderType::Rounded);
|
||||
f.render_widget(block, size);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(4)
|
||||
.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![
|
||||
Span::styled("With", Style::default().fg(Color::Yellow)),
|
||||
Span::from(" background"),
|
||||
])
|
||||
.style(Style::default().bg(Color::Green));
|
||||
f.render_widget(block, top_chunks[0]);
|
||||
|
||||
// Top right inner block with styled title aligned to the right
|
||||
let block = Block::default()
|
||||
.title(Span::styled(
|
||||
"Styled title",
|
||||
Style::default()
|
||||
.fg(Color::White)
|
||||
.bg(Color::Red)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
))
|
||||
.title_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);
|
||||
f.render_widget(block, 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);
|
||||
f.render_widget(block, bottom_chunks[1]);
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Style},
|
||||
text::Span,
|
||||
widgets::{
|
||||
canvas::{Canvas, Map, MapResolution, Rectangle},
|
||||
Block, Borders,
|
||||
},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
struct App {
|
||||
x: f64,
|
||||
y: f64,
|
||||
ball: Rectangle,
|
||||
playground: Rect,
|
||||
vx: f64,
|
||||
vy: f64,
|
||||
dir_x: bool,
|
||||
dir_y: bool,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> App {
|
||||
App {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
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.0,
|
||||
vy: 1.0,
|
||||
dir_x: true,
|
||||
dir_y: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_tick(&mut self) {
|
||||
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.y < self.playground.top() as f64
|
||||
|| self.ball.y + self.ball.height > self.playground.bottom() as f64
|
||||
{
|
||||
self.dir_y = !self.dir_y;
|
||||
}
|
||||
|
||||
if self.dir_x {
|
||||
self.ball.x += self.vx;
|
||||
} else {
|
||||
self.ball.x -= self.vx;
|
||||
}
|
||||
|
||||
if self.dir_y {
|
||||
self.ball.y += self.vy;
|
||||
} else {
|
||||
self.ball.y -= self.vy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 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)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(f.size());
|
||||
let canvas = 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,
|
||||
Span::styled("You are here", Style::default().fg(Color::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"))
|
||||
.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,301 +0,0 @@
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
symbols,
|
||||
text::Span,
|
||||
widgets::{Axis, Block, Borders, Chart, Dataset, GraphType},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
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),
|
||||
];
|
||||
|
||||
#[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)
|
||||
}
|
||||
}
|
||||
|
||||
struct App {
|
||||
signal1: SinSignal,
|
||||
data1: Vec<(f64, f64)>,
|
||||
signal2: SinSignal,
|
||||
data2: Vec<(f64, f64)>,
|
||||
window: [f64; 2],
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> App {
|
||||
let mut signal1 = SinSignal::new(0.2, 3.0, 18.0);
|
||||
let mut signal2 = SinSignal::new(0.1, 2.0, 10.0);
|
||||
let data1 = signal1.by_ref().take(200).collect::<Vec<(f64, f64)>>();
|
||||
let data2 = signal2.by_ref().take(200).collect::<Vec<(f64, f64)>>();
|
||||
App {
|
||||
signal1,
|
||||
data1,
|
||||
signal2,
|
||||
data2,
|
||||
window: [0.0, 20.0],
|
||||
}
|
||||
}
|
||||
|
||||
fn on_tick(&mut self) {
|
||||
for _ in 0..5 {
|
||||
self.data1.remove(0);
|
||||
}
|
||||
self.data1.extend(self.signal1.by_ref().take(5));
|
||||
for _ in 0..10 {
|
||||
self.data2.remove(0);
|
||||
}
|
||||
self.data2.extend(self.signal2.by_ref().take(10));
|
||||
self.window[0] += 1.0;
|
||||
self.window[1] += 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
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(());
|
||||
}
|
||||
}
|
||||
}
|
||||
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(Span::styled(
|
||||
"Chart 1",
|
||||
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))
|
||||
.labels(x_labels)
|
||||
.bounds(app.window),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels(vec![
|
||||
Span::styled("-20", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw("0"),
|
||||
Span::styled("20", Style::default().add_modifier(Modifier::BOLD)),
|
||||
])
|
||||
.bounds([-20.0, 20.0]),
|
||||
);
|
||||
f.render_widget(chart, chunks[0]);
|
||||
|
||||
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(Span::styled(
|
||||
"Chart 2",
|
||||
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([0.0, 5.0])
|
||||
.labels(vec![
|
||||
Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw("2.5"),
|
||||
Span::styled("5.0", Style::default().add_modifier(Modifier::BOLD)),
|
||||
]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.bounds([0.0, 5.0])
|
||||
.labels(vec![
|
||||
Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw("2.5"),
|
||||
Span::styled("5.0", Style::default().add_modifier(Modifier::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(Span::styled(
|
||||
"Chart 3",
|
||||
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([0.0, 50.0])
|
||||
.labels(vec![
|
||||
Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw("25"),
|
||||
Span::styled("50", Style::default().add_modifier(Modifier::BOLD)),
|
||||
]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.bounds([0.0, 5.0])
|
||||
.labels(vec![
|
||||
Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw("2.5"),
|
||||
Span::styled("5", Style::default().add_modifier(Modifier::BOLD)),
|
||||
]),
|
||||
);
|
||||
f.render_widget(chart, chunks[2]);
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::Style,
|
||||
widgets::Widget,
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
struct Label<'a> {
|
||||
text: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> Widget for Label<'a> {
|
||||
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) -> Label<'a> {
|
||||
self.text = text;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
mod app;
|
||||
#[cfg(feature = "crossterm")]
|
||||
mod crossterm;
|
||||
#[cfg(feature = "termion")]
|
||||
mod termion;
|
||||
mod ui;
|
||||
|
||||
#[cfg(feature = "crossterm")]
|
||||
use crate::crossterm::run;
|
||||
#[cfg(feature = "termion")]
|
||||
use crate::termion::run;
|
||||
use argh::FromArgs;
|
||||
use std::{error::Error, time::Duration};
|
||||
|
||||
/// 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);
|
||||
run(tick_rate, cli.enhanced_graphics)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::Span,
|
||||
widgets::{Block, Borders, Gauge},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
struct App {
|
||||
progress1: u16,
|
||||
progress2: u16,
|
||||
progress3: f64,
|
||||
progress4: u16,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> App {
|
||||
App {
|
||||
progress1: 0,
|
||||
progress2: 0,
|
||||
progress3: 0.45,
|
||||
progress4: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_tick(&mut self) {
|
||||
self.progress1 += 1;
|
||||
if self.progress1 > 100 {
|
||||
self.progress1 = 0;
|
||||
}
|
||||
self.progress2 += 2;
|
||||
if self.progress2 > 100 {
|
||||
self.progress2 = 0;
|
||||
}
|
||||
self.progress3 += 0.001;
|
||||
if self.progress3 > 1.0 {
|
||||
self.progress3 = 0.0;
|
||||
}
|
||||
self.progress4 += 1;
|
||||
if self.progress4 > 100 {
|
||||
self.progress4 = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(());
|
||||
}
|
||||
}
|
||||
}
|
||||
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(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
|
||||
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.progress2);
|
||||
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]);
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use tui::{
|
||||
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]);
|
||||
}
|
||||
286
examples/list.rs
286
examples/list.rs
@@ -1,286 +0,0 @@
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Corner, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, Borders, List, ListItem, ListState},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
struct StatefulList<T> {
|
||||
state: ListState,
|
||||
items: Vec<T>,
|
||||
}
|
||||
|
||||
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> {
|
||||
items: StatefulList<(&'a str, usize)>,
|
||||
events: Vec<(&'a str, &'a str)>,
|
||||
}
|
||||
|
||||
impl<'a> App<'a> {
|
||||
fn new() -> App<'a> {
|
||||
App {
|
||||
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"),
|
||||
("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"),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
|
||||
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, &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::Left => app.items.unselect(),
|
||||
KeyCode::Down => app.items.next(),
|
||||
KeyCode::Up => app.items.previous(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(f.size());
|
||||
|
||||
// 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![Spans::from(i.0)];
|
||||
for _ in 0..i.1 {
|
||||
lines.push(Spans::from(Span::styled(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||
Style::default().add_modifier(Modifier::ITALIC),
|
||||
)));
|
||||
}
|
||||
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 = Spans::from(vec![
|
||||
Span::styled(format!("{:<9}", level), s),
|
||||
Span::raw(" "),
|
||||
Span::styled(
|
||||
"2020-01-01 10:00:00",
|
||||
Style::default().add_modifier(Modifier::ITALIC),
|
||||
),
|
||||
]);
|
||||
// The event gets its own line
|
||||
let log = Spans::from(vec![Span::raw(event)]);
|
||||
|
||||
// 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![
|
||||
Spans::from("-".repeat(chunks[1].width as usize)),
|
||||
header,
|
||||
Spans::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]);
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Alignment, Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, Borders, Paragraph, Wrap},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
struct App {
|
||||
scroll: u16,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> App {
|
||||
App { scroll: 0 }
|
||||
}
|
||||
|
||||
fn on_tick(&mut self) {
|
||||
self.scroll += 1;
|
||||
self.scroll %= 10;
|
||||
}
|
||||
}
|
||||
|
||||
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(());
|
||||
}
|
||||
}
|
||||
}
|
||||
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();
|
||||
|
||||
// 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().style(Style::default().bg(Color::White).fg(Color::Black));
|
||||
f.render_widget(block, size);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(5)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(size);
|
||||
|
||||
let text = vec![
|
||||
Spans::from("This is a line "),
|
||||
Spans::from(Span::styled(
|
||||
"This is a line ",
|
||||
Style::default().fg(Color::Red),
|
||||
)),
|
||||
Spans::from(Span::styled(
|
||||
"This is a line",
|
||||
Style::default().bg(Color::Blue),
|
||||
)),
|
||||
Spans::from(Span::styled(
|
||||
"This is a longer line",
|
||||
Style::default().add_modifier(Modifier::CROSSED_OUT),
|
||||
)),
|
||||
Spans::from(Span::styled(&long_line, Style::default().bg(Color::Green))),
|
||||
Spans::from(Span::styled(
|
||||
"This is a line",
|
||||
Style::default()
|
||||
.fg(Color::Green)
|
||||
.add_modifier(Modifier::ITALIC),
|
||||
)),
|
||||
];
|
||||
|
||||
let create_block = |title| {
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().bg(Color::White).fg(Color::Black))
|
||||
.title(Span::styled(
|
||||
title,
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
))
|
||||
};
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.style(Style::default().bg(Color::White).fg(Color::Black))
|
||||
.block(create_block("Left, no wrap"))
|
||||
.alignment(Alignment::Left);
|
||||
f.render_widget(paragraph, chunks[0]);
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.style(Style::default().bg(Color::White).fg(Color::Black))
|
||||
.block(create_block("Left, wrap"))
|
||||
.alignment(Alignment::Left)
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, chunks[1]);
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.style(Style::default().bg(Color::White).fg(Color::Black))
|
||||
.block(create_block("Center, wrap"))
|
||||
.alignment(Alignment::Center)
|
||||
.wrap(Wrap { trim: true })
|
||||
.scroll((app.scroll, 0));
|
||||
f.render_widget(paragraph, chunks[2]);
|
||||
let paragraph = Paragraph::new(text)
|
||||
.style(Style::default().bg(Color::White).fg(Color::Black))
|
||||
.block(create_block("Right, wrap"))
|
||||
.alignment(Alignment::Right)
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, chunks[3]);
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
use std::{error::Error, io};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
text::Span,
|
||||
widgets::{Block, Borders, Clear, Paragraph, Wrap},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
|
||||
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()? {
|
||||
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(Span::styled(
|
||||
text,
|
||||
Style::default().add_modifier(Modifier::SLOW_BLINK),
|
||||
))
|
||||
.alignment(Alignment::Center)
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, chunks[0]);
|
||||
|
||||
let block = Block::default()
|
||||
.title("Content")
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().bg(Color::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,176 +0,0 @@
|
||||
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 std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Style},
|
||||
widgets::{Block, Borders, Sparkline},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
#[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))
|
||||
}
|
||||
}
|
||||
|
||||
struct App {
|
||||
signal: RandomSignal,
|
||||
data1: Vec<u64>,
|
||||
data2: Vec<u64>,
|
||||
data3: Vec<u64>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> App {
|
||||
let mut signal = RandomSignal::new(0, 100);
|
||||
let data1 = signal.by_ref().take(200).collect::<Vec<u64>>();
|
||||
let data2 = signal.by_ref().take(200).collect::<Vec<u64>>();
|
||||
let data3 = signal.by_ref().take(200).collect::<Vec<u64>>();
|
||||
App {
|
||||
signal,
|
||||
data1,
|
||||
data2,
|
||||
data3,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_tick(&mut self) {
|
||||
let value = self.signal.next().unwrap();
|
||||
self.data1.pop();
|
||||
self.data1.insert(0, value);
|
||||
let value = self.signal.next().unwrap();
|
||||
self.data2.pop();
|
||||
self.data2.insert(0, value);
|
||||
let value = self.signal.next().unwrap();
|
||||
self.data3.pop();
|
||||
self.data3.insert(0, value);
|
||||
}
|
||||
}
|
||||
|
||||
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(());
|
||||
}
|
||||
}
|
||||
}
|
||||
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::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,155 +0,0 @@
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
widgets::{Block, Borders, Cell, Row, Table, TableState},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
struct App<'a> {
|
||||
state: TableState,
|
||||
items: Vec<Vec<&'a str>>,
|
||||
}
|
||||
|
||||
impl<'a> App<'a> {
|
||||
fn new() -> App<'a> {
|
||||
App {
|
||||
state: TableState::default(),
|
||||
items: vec![
|
||||
vec!["Row11", "Row12", "Row13"],
|
||||
vec!["Row21", "Row22", "Row23"],
|
||||
vec!["Row31", "Row32", "Row33"],
|
||||
vec!["Row41", "Row42", "Row43"],
|
||||
vec!["Row51", "Row52", "Row53"],
|
||||
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"],
|
||||
],
|
||||
}
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
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()? {
|
||||
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)
|
||||
.split(f.size());
|
||||
|
||||
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);
|
||||
}
|
||||
124
examples/tabs.rs
124
examples/tabs.rs
@@ -1,124 +0,0 @@
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, Borders, Tabs},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
struct App<'a> {
|
||||
pub titles: Vec<&'a str>,
|
||||
pub index: usize,
|
||||
}
|
||||
|
||||
impl<'a> App<'a> {
|
||||
fn new() -> App<'a> {
|
||||
App {
|
||||
titles: vec!["Tab0", "Tab1", "Tab2", "Tab3"],
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()? {
|
||||
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)
|
||||
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
|
||||
.split(size);
|
||||
|
||||
let block = Block::default().style(Style::default().bg(Color::White).fg(Color::Black));
|
||||
f.render_widget(block, size);
|
||||
let titles = app
|
||||
.titles
|
||||
.iter()
|
||||
.map(|t| {
|
||||
let (first, rest) = t.split_at(1);
|
||||
Spans::from(vec![
|
||||
Span::styled(first, Style::default().fg(Color::Yellow)),
|
||||
Span::styled(rest, Style::default().fg(Color::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]);
|
||||
}
|
||||
@@ -1,192 +0,0 @@
|
||||
/// 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},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans, 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 => 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![
|
||||
Span::raw("Press "),
|
||||
Span::styled("q", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(" to exit, "),
|
||||
Span::styled("e", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(" to start editing."),
|
||||
],
|
||||
Style::default().add_modifier(Modifier::RAPID_BLINK),
|
||||
),
|
||||
InputMode::Editing => (
|
||||
vec![
|
||||
Span::raw("Press "),
|
||||
Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(" to stop editing, "),
|
||||
Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(" to record the message"),
|
||||
],
|
||||
Style::default(),
|
||||
),
|
||||
};
|
||||
let mut text = Text::from(Spans::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_ref())
|
||||
.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 tui-rs 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 = vec![Spans::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]);
|
||||
}
|
||||
50
ratatui-core/Cargo.toml
Normal file
50
ratatui-core/Cargo.toml
Normal file
@@ -0,0 +1,50 @@
|
||||
[package]
|
||||
name = "ratatui-core"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
underline-color = []
|
||||
unstable-widget-ref = []
|
||||
|
||||
[dependencies]
|
||||
bitflags.workspace = true
|
||||
cassowary.workspace = true
|
||||
compact_str.workspace = true
|
||||
document-features.workspace = true
|
||||
instability.workspace = true
|
||||
itertools.workspace = true
|
||||
lru.workspace = true
|
||||
paste.workspace = true
|
||||
palette = { workspace = true, optional = true }
|
||||
serde = { workspace = true, optional = true, features = ["derive"] }
|
||||
strum = { workspace = true, features = ["derive"] }
|
||||
termwiz = { workspace = true, optional = true }
|
||||
unicode-segmentation.workspace = true
|
||||
unicode-truncate.workspace = true
|
||||
unicode-width.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
argh = "0.1.12"
|
||||
color-eyre = "0.6.2"
|
||||
criterion = { version = "0.5.1", features = ["html_reports"] }
|
||||
crossterm = { version = "0.28.1", features = ["event-stream"] }
|
||||
fakeit = "1.1"
|
||||
font8x8 = "0.3.1"
|
||||
futures = "0.3.30"
|
||||
indoc = "2"
|
||||
octocrab = "0.40.0"
|
||||
pretty_assertions = "1.4.0"
|
||||
rand = "0.8.5"
|
||||
rand_chacha = "0.3.1"
|
||||
rstest = "0.22.0"
|
||||
serde_json = "1.0.109"
|
||||
tokio = { version = "1.39.2", features = [
|
||||
"rt",
|
||||
"macros",
|
||||
"time",
|
||||
"rt-multi-thread",
|
||||
] }
|
||||
tracing = "0.1.40"
|
||||
tracing-appender = "0.2.3"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
344
ratatui-core/src/backend.rs
Normal file
344
ratatui-core/src/backend.rs
Normal file
@@ -0,0 +1,344 @@
|
||||
#![warn(missing_docs)]
|
||||
//! 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.
|
||||
//!
|
||||
//! Supported terminal backends:
|
||||
//! - [Crossterm]: enable the `crossterm` feature (enabled by default) and use [`CrosstermBackend`]
|
||||
//! - [Termion]: enable the `termion` feature and use [`TermionBackend`]
|
||||
//! - [Termwiz]: enable the `termwiz` feature and use [`TermwizBackend`]
|
||||
//!
|
||||
//! Additionally, a [`TestBackend`] is provided for testing purposes.
|
||||
//!
|
||||
//! See the [Backend Comparison] section of the [Ratatui Website] for more details on the different
|
||||
//! backends.
|
||||
//!
|
||||
//! Each backend supports a number of features, such as [raw mode](#raw-mode), [alternate
|
||||
//! screen](#alternate-screen), and [mouse capture](#mouse-capture). These features are generally
|
||||
//! not enabled by default, and must be enabled by the application before they can be used. See the
|
||||
//! documentation for each backend for more details.
|
||||
//!
|
||||
//! Note: most applications should use the [`Terminal`] struct instead of directly calling methods
|
||||
//! on the backend.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use std::io::stdout;
|
||||
//!
|
||||
//! use ratatui::prelude::*;
|
||||
//!
|
||||
//! let backend = CrosstermBackend::new(stdout());
|
||||
//! let mut terminal = Terminal::new(backend)?;
|
||||
//! terminal.clear()?;
|
||||
//! terminal.draw(|frame| {
|
||||
//! // -- snip --
|
||||
//! })?;
|
||||
//! # std::io::Result::Ok(())
|
||||
//! ```
|
||||
//!
|
||||
//! See the the [Examples] directory for more examples.
|
||||
//!
|
||||
//! # Raw Mode
|
||||
//!
|
||||
//! Raw mode is a mode where the terminal does not perform any processing or handling of the input
|
||||
//! and output. This means that features such as echoing input characters, line buffering, and
|
||||
//! special character processing (e.g., CTRL-C for SIGINT) are disabled. This is useful for
|
||||
//! applications that want to have complete control over the terminal input and output, processing
|
||||
//! each keystroke themselves.
|
||||
//!
|
||||
//! For example, in raw mode, the terminal will not perform line buffering on the input, so the
|
||||
//! application will receive each key press as it is typed, instead of waiting for the user to
|
||||
//! press enter. This makes it suitable for real-time applications like text editors,
|
||||
//! terminal-based games, and more.
|
||||
//!
|
||||
//! Each backend handles raw mode differently, so the behavior may vary depending on the backend
|
||||
//! being used. Be sure to consult the backend's specific documentation for exact details on how it
|
||||
//! implements raw mode.
|
||||
|
||||
//! # Alternate Screen
|
||||
//!
|
||||
//! The alternate screen is a separate buffer that some terminals provide, distinct from the main
|
||||
//! screen. When activated, the terminal will display the alternate screen, hiding the current
|
||||
//! content of the main screen. Applications can write to this screen as if it were the regular
|
||||
//! terminal display, but when the application exits, the terminal will switch back to the main
|
||||
//! screen, and the contents of the alternate screen will be cleared. This is useful for
|
||||
//! applications like text editors or terminal games that want to use the full terminal window
|
||||
//! without disrupting the command line or other terminal content.
|
||||
//!
|
||||
//! This creates a seamless transition between the application and the regular terminal session, as
|
||||
//! the content displayed before launching the application will reappear after the application
|
||||
//! exits.
|
||||
//!
|
||||
//! Note that not all terminal emulators support the alternate screen, and even those that do may
|
||||
//! handle it differently. As a result, the behavior may vary depending on the backend being used.
|
||||
//! Always consult the specific backend's documentation to understand how it implements the
|
||||
//! alternate screen.
|
||||
//!
|
||||
//! # Mouse Capture
|
||||
//!
|
||||
//! Mouse capture is a mode where the terminal captures mouse events such as clicks, scrolls, and
|
||||
//! movement, and sends them to the application as special sequences or events. This enables the
|
||||
//! application to handle and respond to mouse actions, providing a more interactive and graphical
|
||||
//! user experience within the terminal. It's particularly useful for applications like
|
||||
//! terminal-based games, text editors, or other programs that require more direct interaction from
|
||||
//! the user.
|
||||
//!
|
||||
//! Each backend handles mouse capture differently, with variations in the types of events that can
|
||||
//! be captured and how they are represented. As such, the behavior may vary depending on the
|
||||
//! backend being used, and developers should consult the specific backend's documentation to
|
||||
//! understand how it implements mouse capture.
|
||||
//!
|
||||
//! [`TermionBackend`]: termion/struct.TermionBackend.html
|
||||
//! [`Terminal`]: crate::terminal::Terminal
|
||||
//! [`TermionBackend`]: termion/struct.TermionBackend.html
|
||||
//! [Crossterm]: https://crates.io/crates/crossterm
|
||||
//! [Termion]: https://crates.io/crates/termion
|
||||
//! [Termwiz]: https://crates.io/crates/termwiz
|
||||
//! [Examples]: https://github.com/ratatui/ratatui/tree/main/examples/README.md
|
||||
//! [Backend Comparison]:
|
||||
//! https://ratatui.rs/concepts/backends/comparison/
|
||||
//! [Ratatui Website]: https://ratatui.rs
|
||||
use std::io;
|
||||
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
use crate::{
|
||||
buffer::Cell,
|
||||
layout::{Position, Size},
|
||||
};
|
||||
|
||||
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, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum ClearType {
|
||||
/// Clear the entire screen.
|
||||
All,
|
||||
/// Clear everything after the cursor.
|
||||
AfterCursor,
|
||||
/// Clear everything before the cursor.
|
||||
BeforeCursor,
|
||||
/// Clear the current line.
|
||||
CurrentLine,
|
||||
/// Clear everything from the cursor until the next newline.
|
||||
UntilNewLine,
|
||||
}
|
||||
|
||||
/// The window size in characters (columns / rows) as well as pixels.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub struct WindowSize {
|
||||
/// Size of the window in characters (columns / rows).
|
||||
pub columns_rows: Size,
|
||||
/// Size of the window in pixels.
|
||||
///
|
||||
/// The `pixels` fields may not be implemented by all terminals and return `0,0`. See
|
||||
/// <https://man7.org/linux/man-pages/man4/tty_ioctl.4.html> under section "Get and set window
|
||||
/// size" / TIOCGWINSZ where the fields are commented as "unused".
|
||||
pub pixels: Size,
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// Most applications should not need to interact with the `Backend` trait directly as the
|
||||
/// [`Terminal`] struct provides a higher level interface for interacting with the terminal.
|
||||
///
|
||||
/// [`Terminal`]: crate::terminal::Terminal
|
||||
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) -> io::Result<()>
|
||||
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.
|
||||
///
|
||||
///
|
||||
/// See also [`show_cursor`].
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::backend::{Backend, TestBackend};
|
||||
/// # let mut backend = TestBackend::new(80, 25);
|
||||
/// backend.hide_cursor()?;
|
||||
/// // do something with hidden cursor
|
||||
/// backend.show_cursor()?;
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
///
|
||||
/// [`show_cursor`]: Self::show_cursor
|
||||
fn hide_cursor(&mut self) -> io::Result<()>;
|
||||
|
||||
/// Show the cursor on the terminal screen.
|
||||
///
|
||||
/// See [`hide_cursor`] for an example.
|
||||
///
|
||||
/// [`hide_cursor`]: Self::hide_cursor
|
||||
fn show_cursor(&mut self) -> io::Result<()>;
|
||||
|
||||
/// Get the current cursor position on the terminal screen.
|
||||
///
|
||||
/// The returned tuple contains the x and y coordinates of the cursor.
|
||||
/// The origin (0, 0) is at the top left corner of the screen.
|
||||
///
|
||||
/// See [`set_cursor_position`] for an example.
|
||||
///
|
||||
/// [`set_cursor_position`]: Self::set_cursor_position
|
||||
fn get_cursor_position(&mut self) -> io::Result<Position>;
|
||||
|
||||
/// Set the cursor position on the terminal screen to the given x and y coordinates.
|
||||
///
|
||||
/// The origin (0, 0) is at the top left corner of the screen.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::backend::{Backend, TestBackend};
|
||||
/// # use ratatui::layout::Position;
|
||||
/// # let mut backend = TestBackend::new(80, 25);
|
||||
/// backend.set_cursor_position(Position { x: 10, y: 20 })?;
|
||||
/// assert_eq!(backend.get_cursor_position()?, Position { x: 10, y: 20 });
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()>;
|
||||
|
||||
/// Get the current cursor position on the terminal screen.
|
||||
///
|
||||
/// The returned tuple contains the x and y coordinates of the cursor. The origin
|
||||
/// (0, 0) is at the top left corner of the screen.
|
||||
#[deprecated = "the method get_cursor_position indicates more clearly what about the cursor to get"]
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
let Position { x, y } = self.get_cursor_position()?;
|
||||
Ok((x, y))
|
||||
}
|
||||
|
||||
/// Set the cursor position on the terminal screen to the given x and y coordinates.
|
||||
///
|
||||
/// The origin (0, 0) is at the top left corner of the screen.
|
||||
#[deprecated = "the method set_cursor_position indicates more clearly what about the cursor to set"]
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
self.set_cursor_position(Position { x, y })
|
||||
}
|
||||
|
||||
/// Clears the whole terminal screen
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use ratatui::backend::{Backend, TestBackend};
|
||||
/// # let mut backend = TestBackend::new(80, 25);
|
||||
/// backend.clear()?;
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
fn clear(&mut self) -> io::Result<()>;
|
||||
|
||||
/// Clears a specific region of the terminal specified by the [`ClearType`] parameter
|
||||
///
|
||||
/// This method is optional and may not be implemented by all backends. The default
|
||||
/// implementation calls [`clear`] if the `clear_type` is [`ClearType::All`] and returns an
|
||||
/// error otherwise.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use ratatui::{prelude::*, backend::{TestBackend, ClearType}};
|
||||
/// # let mut backend = TestBackend::new(80, 25);
|
||||
/// backend.clear_region(ClearType::All)?;
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This method will return an error if the terminal screen could not be cleared. It will also
|
||||
/// return an error if the `clear_type` is not supported by the backend.
|
||||
///
|
||||
/// [`clear`]: Self::clear
|
||||
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
|
||||
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 in columns/rows as a [`Size`].
|
||||
///
|
||||
/// The returned [`Size`] contains the width and height of the terminal screen.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, backend::TestBackend};
|
||||
/// let backend = TestBackend::new(80, 25);
|
||||
/// assert_eq!(backend.size()?, Size::new(80, 25));
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
fn size(&self) -> io::Result<Size>;
|
||||
|
||||
/// Get the size of the terminal screen in columns/rows and pixels as a [`WindowSize`].
|
||||
///
|
||||
/// The reason for this not returning only the pixel size, given the redundancy with the
|
||||
/// `size()` method, is that the underlying backends most likely get both values with one
|
||||
/// syscall, and the user is also most likely to need columns and rows along with pixel size.
|
||||
fn window_size(&mut self) -> io::Result<WindowSize>;
|
||||
|
||||
/// Flush any buffered content to the terminal screen.
|
||||
fn flush(&mut self) -> io::Result<()>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use strum::ParseError;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn clear_type_tostring() {
|
||||
assert_eq!(ClearType::All.to_string(), "All");
|
||||
assert_eq!(ClearType::AfterCursor.to_string(), "AfterCursor");
|
||||
assert_eq!(ClearType::BeforeCursor.to_string(), "BeforeCursor");
|
||||
assert_eq!(ClearType::CurrentLine.to_string(), "CurrentLine");
|
||||
assert_eq!(ClearType::UntilNewLine.to_string(), "UntilNewLine");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_type_from_str() {
|
||||
assert_eq!("All".parse::<ClearType>(), Ok(ClearType::All));
|
||||
assert_eq!(
|
||||
"AfterCursor".parse::<ClearType>(),
|
||||
Ok(ClearType::AfterCursor)
|
||||
);
|
||||
assert_eq!(
|
||||
"BeforeCursor".parse::<ClearType>(),
|
||||
Ok(ClearType::BeforeCursor)
|
||||
);
|
||||
assert_eq!(
|
||||
"CurrentLine".parse::<ClearType>(),
|
||||
Ok(ClearType::CurrentLine)
|
||||
);
|
||||
assert_eq!(
|
||||
"UntilNewLine".parse::<ClearType>(),
|
||||
Ok(ClearType::UntilNewLine)
|
||||
);
|
||||
assert_eq!("".parse::<ClearType>(), Err(ParseError::VariantNotFound));
|
||||
}
|
||||
}
|
||||
919
ratatui-core/src/backend/test.rs
Normal file
919
ratatui-core/src/backend/test.rs
Normal file
@@ -0,0 +1,919 @@
|
||||
//! 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::{self, Write},
|
||||
io, iter,
|
||||
};
|
||||
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{
|
||||
backend::{Backend, ClearType, WindowSize},
|
||||
buffer::{Buffer, Cell},
|
||||
layout::{Position, Rect, Size},
|
||||
};
|
||||
|
||||
/// A [`Backend`] implementation used for integration testing that renders to an memory buffer.
|
||||
///
|
||||
/// Note: that although many of the integration and unit tests in ratatui are written using this
|
||||
/// backend, it is preferable to write unit tests for widgets directly against the buffer rather
|
||||
/// than using this backend. This backend is intended for integration tests that test the entire
|
||||
/// terminal UI.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{backend::TestBackend, prelude::*};
|
||||
///
|
||||
/// let mut backend = TestBackend::new(10, 2);
|
||||
/// backend.clear()?;
|
||||
/// backend.assert_buffer_lines([" "; 2]);
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct TestBackend {
|
||||
buffer: Buffer,
|
||||
scrollback: Buffer,
|
||||
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) -> Self {
|
||||
Self {
|
||||
buffer: Buffer::empty(Rect::new(0, 0, width, height)),
|
||||
scrollback: Buffer::empty(Rect::new(0, 0, width, 0)),
|
||||
cursor: false,
|
||||
pos: (0, 0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a reference to the internal buffer of the `TestBackend`.
|
||||
pub const fn buffer(&self) -> &Buffer {
|
||||
&self.buffer
|
||||
}
|
||||
|
||||
/// Returns a reference to the internal scrollback buffer of the `TestBackend`.
|
||||
///
|
||||
/// The scrollback buffer represents the part of the screen that is currently hidden from view,
|
||||
/// but that could be accessed by scrolling back in the terminal's history. This would normally
|
||||
/// be done using the terminal's scrollbar or an equivalent keyboard shortcut.
|
||||
///
|
||||
/// The scrollback buffer starts out empty. Lines are appended when they scroll off the top of
|
||||
/// the main buffer. This happens when lines are appended to the bottom of the main buffer
|
||||
/// using [`Backend::append_lines`].
|
||||
///
|
||||
/// The scrollback buffer has a maximum height of [`u16::MAX`]. If lines are appended to the
|
||||
/// bottom of the scrollback buffer when it is at its maximum height, a corresponding number of
|
||||
/// lines will be removed from the top.
|
||||
pub const fn scrollback(&self) -> &Buffer {
|
||||
&self.scrollback
|
||||
}
|
||||
|
||||
/// 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));
|
||||
let scrollback_height = self.scrollback.area.height;
|
||||
self.scrollback
|
||||
.resize(Rect::new(0, 0, width, scrollback_height));
|
||||
}
|
||||
|
||||
/// Asserts that the `TestBackend`'s buffer is equal to the expected buffer.
|
||||
///
|
||||
/// This is a shortcut for `assert_eq!(self.buffer(), &expected)`.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// When they are not equal, a panic occurs with a detailed error message showing the
|
||||
/// differences between the expected and actual buffers.
|
||||
#[allow(deprecated)]
|
||||
#[track_caller]
|
||||
pub fn assert_buffer(&self, expected: &Buffer) {
|
||||
// TODO: use assert_eq!()
|
||||
crate::assert_buffer_eq!(&self.buffer, expected);
|
||||
}
|
||||
|
||||
/// Asserts that the `TestBackend`'s scrollback buffer is equal to the expected buffer.
|
||||
///
|
||||
/// This is a shortcut for `assert_eq!(self.scrollback(), &expected)`.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// When they are not equal, a panic occurs with a detailed error message showing the
|
||||
/// differences between the expected and actual buffers.
|
||||
#[track_caller]
|
||||
pub fn assert_scrollback(&self, expected: &Buffer) {
|
||||
assert_eq!(&self.scrollback, expected);
|
||||
}
|
||||
|
||||
/// Asserts that the `TestBackend`'s scrollback buffer is empty.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// When the scrollback buffer is not equal, a panic occurs with a detailed error message
|
||||
/// showing the differences between the expected and actual buffers.
|
||||
pub fn assert_scrollback_empty(&self) {
|
||||
let expected = Buffer {
|
||||
area: Rect {
|
||||
width: self.scrollback.area.width,
|
||||
..Rect::ZERO
|
||||
},
|
||||
content: vec![],
|
||||
};
|
||||
self.assert_scrollback(&expected);
|
||||
}
|
||||
|
||||
/// Asserts that the `TestBackend`'s buffer is equal to the expected lines.
|
||||
///
|
||||
/// This is a shortcut for `assert_eq!(self.buffer(), &Buffer::with_lines(expected))`.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// When they are not equal, a panic occurs with a detailed error message showing the
|
||||
/// differences between the expected and actual buffers.
|
||||
#[track_caller]
|
||||
pub fn assert_buffer_lines<'line, Lines>(&self, expected: Lines)
|
||||
where
|
||||
Lines: IntoIterator,
|
||||
Lines::Item: Into<crate::text::Line<'line>>,
|
||||
{
|
||||
self.assert_buffer(&Buffer::with_lines(expected));
|
||||
}
|
||||
|
||||
/// Asserts that the `TestBackend`'s scrollback buffer is equal to the expected lines.
|
||||
///
|
||||
/// This is a shortcut for `assert_eq!(self.scrollback(), &Buffer::with_lines(expected))`.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// When they are not equal, a panic occurs with a detailed error message showing the
|
||||
/// differences between the expected and actual buffers.
|
||||
#[track_caller]
|
||||
pub fn assert_scrollback_lines<'line, Lines>(&self, expected: Lines)
|
||||
where
|
||||
Lines: IntoIterator,
|
||||
Lines::Item: Into<crate::text::Line<'line>>,
|
||||
{
|
||||
self.assert_scrollback(&Buffer::with_lines(expected));
|
||||
}
|
||||
|
||||
/// Asserts that the `TestBackend`'s cursor position is equal to the expected one.
|
||||
///
|
||||
/// This is a shortcut for `assert_eq!(self.get_cursor_position().unwrap(), expected)`.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// When they are not equal, a panic occurs with a detailed error message showing the
|
||||
/// differences between the expected and actual position.
|
||||
#[track_caller]
|
||||
pub fn assert_cursor_position<P: Into<Position>>(&mut self, position: P) {
|
||||
let actual = self.get_cursor_position().unwrap();
|
||||
assert_eq!(actual, position.into());
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for TestBackend {
|
||||
/// Formats the `TestBackend` for display by calling the `buffer_view` function
|
||||
/// on its internal buffer.
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", buffer_view(&self.buffer))
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend for TestBackend {
|
||||
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
for (x, y, c) in content {
|
||||
self.buffer[(x, y)] = c.clone();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
self.cursor = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_cursor(&mut self) -> io::Result<()> {
|
||||
self.cursor = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_cursor_position(&mut self) -> io::Result<Position> {
|
||||
Ok(self.pos.into())
|
||||
}
|
||||
|
||||
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
|
||||
self.pos = position.into().into();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear(&mut self) -> io::Result<()> {
|
||||
self.buffer.reset();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_region(&mut self, clear_type: super::ClearType) -> io::Result<()> {
|
||||
let region = match clear_type {
|
||||
ClearType::All => return self.clear(),
|
||||
ClearType::AfterCursor => {
|
||||
let index = self.buffer.index_of(self.pos.0, self.pos.1) + 1;
|
||||
&mut self.buffer.content[index..]
|
||||
}
|
||||
ClearType::BeforeCursor => {
|
||||
let index = self.buffer.index_of(self.pos.0, self.pos.1);
|
||||
&mut self.buffer.content[..index]
|
||||
}
|
||||
ClearType::CurrentLine => {
|
||||
let line_start_index = self.buffer.index_of(0, self.pos.1);
|
||||
let line_end_index = self.buffer.index_of(self.buffer.area.width - 1, self.pos.1);
|
||||
&mut self.buffer.content[line_start_index..=line_end_index]
|
||||
}
|
||||
ClearType::UntilNewLine => {
|
||||
let index = self.buffer.index_of(self.pos.0, self.pos.1);
|
||||
let line_end_index = self.buffer.index_of(self.buffer.area.width - 1, self.pos.1);
|
||||
&mut self.buffer.content[index..=line_end_index]
|
||||
}
|
||||
};
|
||||
for cell in region {
|
||||
cell.reset();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Inserts n line breaks at the current cursor position.
|
||||
///
|
||||
/// After the insertion, the cursor x position will be incremented by 1 (unless it's already
|
||||
/// at the end of line). This is a common behaviour of terminals in raw mode.
|
||||
///
|
||||
/// If the number of lines to append is fewer than the number of lines in the buffer after the
|
||||
/// cursor y position then the cursor is moved down by n rows.
|
||||
///
|
||||
/// If the number of lines to append is greater than the number of lines in the buffer after
|
||||
/// the cursor y position then that number of empty lines (at most the buffer's height in this
|
||||
/// case but this limit is instead replaced with scrolling in most backend implementations) will
|
||||
/// be added after the current position and the cursor will be moved to the last row.
|
||||
fn append_lines(&mut self, line_count: u16) -> io::Result<()> {
|
||||
let Position { x: cur_x, y: cur_y } = self.get_cursor_position()?;
|
||||
let Rect { width, height, .. } = self.buffer.area;
|
||||
|
||||
// the next column ensuring that we don't go past the last column
|
||||
let new_cursor_x = cur_x.saturating_add(1).min(width.saturating_sub(1));
|
||||
|
||||
let max_y = height.saturating_sub(1);
|
||||
let lines_after_cursor = max_y.saturating_sub(cur_y);
|
||||
|
||||
if line_count > lines_after_cursor {
|
||||
// We need to insert blank lines at the bottom and scroll the lines from the top into
|
||||
// scrollback.
|
||||
let scroll_by: usize = (line_count - lines_after_cursor).into();
|
||||
let width: usize = self.buffer.area.width.into();
|
||||
let cells_to_scrollback = self.buffer.content.len().min(width * scroll_by);
|
||||
|
||||
append_to_scrollback(
|
||||
&mut self.scrollback,
|
||||
self.buffer.content.splice(
|
||||
0..cells_to_scrollback,
|
||||
iter::repeat_with(Default::default).take(cells_to_scrollback),
|
||||
),
|
||||
);
|
||||
self.buffer.content.rotate_left(cells_to_scrollback);
|
||||
append_to_scrollback(
|
||||
&mut self.scrollback,
|
||||
iter::repeat_with(Default::default).take(width * scroll_by - cells_to_scrollback),
|
||||
);
|
||||
}
|
||||
|
||||
let new_cursor_y = cur_y.saturating_add(line_count).min(max_y);
|
||||
self.set_cursor_position(Position::new(new_cursor_x, new_cursor_y))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn size(&self) -> io::Result<Size> {
|
||||
Ok(self.buffer.area.as_size())
|
||||
}
|
||||
|
||||
fn window_size(&mut self) -> io::Result<WindowSize> {
|
||||
// Some arbitrary window pixel size, probably doesn't need much testing.
|
||||
const WINDOW_PIXEL_SIZE: Size = Size {
|
||||
width: 640,
|
||||
height: 480,
|
||||
};
|
||||
Ok(WindowSize {
|
||||
columns_rows: self.buffer.area.as_size(),
|
||||
pixels: WINDOW_PIXEL_SIZE,
|
||||
})
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Append the provided cells to the bottom of a scrollback buffer. The number of cells must be a
|
||||
/// multiple of the buffer's width. If the scrollback buffer ends up larger than 65535 lines tall,
|
||||
/// then lines will be removed from the top to get it down to size.
|
||||
fn append_to_scrollback(scrollback: &mut Buffer, cells: impl IntoIterator<Item = Cell>) {
|
||||
scrollback.content.extend(cells);
|
||||
let width = scrollback.area.width as usize;
|
||||
let new_height = (scrollback.content.len() / width).min(u16::MAX as usize);
|
||||
let keep_from = scrollback
|
||||
.content
|
||||
.len()
|
||||
.saturating_sub(width * u16::MAX as usize);
|
||||
scrollback.content.drain(0..keep_from);
|
||||
scrollback.area.height = new_height as u16;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use itertools::Itertools as _;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn new() {
|
||||
assert_eq!(
|
||||
TestBackend::new(10, 2),
|
||||
TestBackend {
|
||||
buffer: Buffer::with_lines([" "; 2]),
|
||||
scrollback: Buffer::empty(Rect::new(0, 0, 10, 0)),
|
||||
cursor: false,
|
||||
pos: (0, 0),
|
||||
}
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_buffer_view() {
|
||||
let buffer = Buffer::with_lines(["aaaa"; 2]);
|
||||
assert_eq!(buffer_view(&buffer), "\"aaaa\"\n\"aaaa\"\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_view_with_overwrites() {
|
||||
let multi_byte_char = "👨👩👧👦"; // renders 8 wide
|
||||
let buffer = Buffer::with_lines([multi_byte_char]);
|
||||
assert_eq!(
|
||||
buffer_view(&buffer),
|
||||
format!(
|
||||
r#""{multi_byte_char}" Hidden by multi-width symbols: [(1, " "), (2, " "), (3, " "), (4, " "), (5, " "), (6, " "), (7, " ")]
|
||||
"#,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer() {
|
||||
let backend = TestBackend::new(10, 2);
|
||||
backend.assert_buffer_lines([" "; 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize() {
|
||||
let mut backend = TestBackend::new(10, 2);
|
||||
backend.resize(5, 5);
|
||||
backend.assert_buffer_lines([" "; 5]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assert_buffer() {
|
||||
let backend = TestBackend::new(10, 2);
|
||||
backend.assert_buffer_lines([" "; 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic = "buffer contents not equal"]
|
||||
fn assert_buffer_panics() {
|
||||
let backend = TestBackend::new(10, 2);
|
||||
backend.assert_buffer_lines(["aaaaaaaaaa"; 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic = "assertion `left == right` failed"]
|
||||
fn assert_scrollback_panics() {
|
||||
let backend = TestBackend::new(10, 2);
|
||||
backend.assert_scrollback_lines(["aaaaaaaaaa"; 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display() {
|
||||
let backend = TestBackend::new(10, 2);
|
||||
assert_eq!(format!("{backend}"), "\" \"\n\" \"\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draw() {
|
||||
let mut backend = TestBackend::new(10, 2);
|
||||
let cell = Cell::new("a");
|
||||
backend.draw([(0, 0, &cell)].into_iter()).unwrap();
|
||||
backend.draw([(0, 1, &cell)].into_iter()).unwrap();
|
||||
backend.assert_buffer_lines(["a "; 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hide_cursor() {
|
||||
let mut backend = TestBackend::new(10, 2);
|
||||
backend.hide_cursor().unwrap();
|
||||
assert!(!backend.cursor);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_cursor() {
|
||||
let mut backend = TestBackend::new(10, 2);
|
||||
backend.show_cursor().unwrap();
|
||||
assert!(backend.cursor);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_cursor_position() {
|
||||
let mut backend = TestBackend::new(10, 2);
|
||||
assert_eq!(backend.get_cursor_position().unwrap(), Position::ORIGIN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assert_cursor_position() {
|
||||
let mut backend = TestBackend::new(10, 2);
|
||||
backend.assert_cursor_position(Position::ORIGIN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_cursor_position() {
|
||||
let mut backend = TestBackend::new(10, 10);
|
||||
backend
|
||||
.set_cursor_position(Position { x: 5, y: 5 })
|
||||
.unwrap();
|
||||
assert_eq!(backend.pos, (5, 5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear() {
|
||||
let mut backend = TestBackend::new(4, 2);
|
||||
let cell = Cell::new("a");
|
||||
backend.draw([(0, 0, &cell)].into_iter()).unwrap();
|
||||
backend.draw([(0, 1, &cell)].into_iter()).unwrap();
|
||||
backend.clear().unwrap();
|
||||
backend.assert_buffer_lines([" ", " "]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_region_all() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
]);
|
||||
|
||||
backend.clear_region(ClearType::All).unwrap();
|
||||
backend.assert_buffer_lines([
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_region_after_cursor() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
]);
|
||||
|
||||
backend
|
||||
.set_cursor_position(Position { x: 3, y: 2 })
|
||||
.unwrap();
|
||||
backend.clear_region(ClearType::AfterCursor).unwrap();
|
||||
backend.assert_buffer_lines([
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaa ",
|
||||
" ",
|
||||
" ",
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_region_before_cursor() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
]);
|
||||
|
||||
backend
|
||||
.set_cursor_position(Position { x: 5, y: 3 })
|
||||
.unwrap();
|
||||
backend.clear_region(ClearType::BeforeCursor).unwrap();
|
||||
backend.assert_buffer_lines([
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" aaaaa",
|
||||
"aaaaaaaaaa",
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_region_current_line() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
]);
|
||||
|
||||
backend
|
||||
.set_cursor_position(Position { x: 3, y: 1 })
|
||||
.unwrap();
|
||||
backend.clear_region(ClearType::CurrentLine).unwrap();
|
||||
backend.assert_buffer_lines([
|
||||
"aaaaaaaaaa",
|
||||
" ",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_region_until_new_line() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
]);
|
||||
|
||||
backend
|
||||
.set_cursor_position(Position { x: 3, y: 0 })
|
||||
.unwrap();
|
||||
backend.clear_region(ClearType::UntilNewLine).unwrap();
|
||||
backend.assert_buffer_lines([
|
||||
"aaa ",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_lines_not_at_last_line() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
]);
|
||||
|
||||
backend.set_cursor_position(Position::ORIGIN).unwrap();
|
||||
|
||||
// If the cursor is not at the last line in the terminal the addition of a
|
||||
// newline simply moves the cursor down and to the right
|
||||
|
||||
backend.append_lines(1).unwrap();
|
||||
backend.assert_cursor_position(Position { x: 1, y: 1 });
|
||||
|
||||
backend.append_lines(1).unwrap();
|
||||
backend.assert_cursor_position(Position { x: 2, y: 2 });
|
||||
|
||||
backend.append_lines(1).unwrap();
|
||||
backend.assert_cursor_position(Position { x: 3, y: 3 });
|
||||
|
||||
backend.append_lines(1).unwrap();
|
||||
backend.assert_cursor_position(Position { x: 4, y: 4 });
|
||||
|
||||
// As such the buffer should remain unchanged
|
||||
backend.assert_buffer_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
]);
|
||||
backend.assert_scrollback_empty();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_lines_at_last_line() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
]);
|
||||
|
||||
// If the cursor is at the last line in the terminal the addition of a
|
||||
// newline will scroll the contents of the buffer
|
||||
backend
|
||||
.set_cursor_position(Position { x: 0, y: 4 })
|
||||
.unwrap();
|
||||
|
||||
backend.append_lines(1).unwrap();
|
||||
|
||||
backend.assert_buffer_lines([
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
" ",
|
||||
]);
|
||||
backend.assert_scrollback_lines(["aaaaaaaaaa"]);
|
||||
|
||||
// It also moves the cursor to the right, as is common of the behaviour of
|
||||
// terminals in raw-mode
|
||||
backend.assert_cursor_position(Position { x: 1, y: 4 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_multiple_lines_not_at_last_line() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
]);
|
||||
|
||||
backend.set_cursor_position(Position::ORIGIN).unwrap();
|
||||
|
||||
// If the cursor is not at the last line in the terminal the addition of multiple
|
||||
// newlines simply moves the cursor n lines down and to the right by 1
|
||||
|
||||
backend.append_lines(4).unwrap();
|
||||
backend.assert_cursor_position(Position { x: 1, y: 4 });
|
||||
|
||||
// As such the buffer should remain unchanged
|
||||
backend.assert_buffer_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
]);
|
||||
backend.assert_scrollback_empty();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_multiple_lines_past_last_line() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
]);
|
||||
|
||||
backend
|
||||
.set_cursor_position(Position { x: 0, y: 3 })
|
||||
.unwrap();
|
||||
|
||||
backend.append_lines(3).unwrap();
|
||||
backend.assert_cursor_position(Position { x: 1, y: 4 });
|
||||
|
||||
backend.assert_buffer_lines([
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
" ",
|
||||
" ",
|
||||
]);
|
||||
backend.assert_scrollback_lines(["aaaaaaaaaa", "bbbbbbbbbb"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_multiple_lines_where_cursor_at_end_appends_height_lines() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
]);
|
||||
|
||||
backend
|
||||
.set_cursor_position(Position { x: 0, y: 4 })
|
||||
.unwrap();
|
||||
|
||||
backend.append_lines(5).unwrap();
|
||||
backend.assert_cursor_position(Position { x: 1, y: 4 });
|
||||
|
||||
backend.assert_buffer_lines([
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
]);
|
||||
backend.assert_scrollback_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_multiple_lines_where_cursor_appends_height_lines() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
]);
|
||||
|
||||
backend.set_cursor_position(Position::ORIGIN).unwrap();
|
||||
|
||||
backend.append_lines(5).unwrap();
|
||||
backend.assert_cursor_position(Position { x: 1, y: 4 });
|
||||
|
||||
backend.assert_buffer_lines([
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
" ",
|
||||
]);
|
||||
backend.assert_scrollback_lines(["aaaaaaaaaa"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_multiple_lines_where_cursor_at_end_appends_more_than_height_lines() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
]);
|
||||
|
||||
backend
|
||||
.set_cursor_position(Position { x: 0, y: 4 })
|
||||
.unwrap();
|
||||
|
||||
backend.append_lines(8).unwrap();
|
||||
backend.assert_cursor_position(Position { x: 1, y: 4 });
|
||||
|
||||
backend.assert_buffer_lines([
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
]);
|
||||
backend.assert_scrollback_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_lines_truncates_beyond_u16_max() -> io::Result<()> {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
|
||||
// Fill the scrollback with 65535 + 10 lines.
|
||||
let row_count = u16::MAX as usize + 10;
|
||||
for row in 0..=row_count {
|
||||
if row > 4 {
|
||||
backend.set_cursor_position(Position { x: 0, y: 4 })?;
|
||||
backend.append_lines(1)?;
|
||||
}
|
||||
let cells = format!("{row:>10}").chars().map(Cell::from).collect_vec();
|
||||
let content = cells
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(column, cell)| (column as u16, 4.min(row) as u16, cell));
|
||||
backend.draw(content)?;
|
||||
}
|
||||
|
||||
// check that the buffer contains the last 5 lines appended
|
||||
backend.assert_buffer_lines([
|
||||
" 65541",
|
||||
" 65542",
|
||||
" 65543",
|
||||
" 65544",
|
||||
" 65545",
|
||||
]);
|
||||
|
||||
// TODO: ideally this should be something like:
|
||||
// let lines = (6..=65545).map(|row| format!("{row:>10}"));
|
||||
// backend.assert_scrollback_lines(lines);
|
||||
// but there's some truncation happening in Buffer::with_lines that needs to be fixed
|
||||
assert_eq!(
|
||||
Buffer {
|
||||
area: Rect::new(0, 0, 10, 5),
|
||||
content: backend.scrollback.content[0..10 * 5].to_vec(),
|
||||
},
|
||||
Buffer::with_lines([
|
||||
" 6",
|
||||
" 7",
|
||||
" 8",
|
||||
" 9",
|
||||
" 10",
|
||||
]),
|
||||
"first 5 lines of scrollback should have been truncated"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Buffer {
|
||||
area: Rect::new(0, 0, 10, 5),
|
||||
content: backend.scrollback.content[10 * 65530..10 * 65535].to_vec(),
|
||||
},
|
||||
Buffer::with_lines([
|
||||
" 65536",
|
||||
" 65537",
|
||||
" 65538",
|
||||
" 65539",
|
||||
" 65540",
|
||||
]),
|
||||
"last 5 lines of scrollback should have been appended"
|
||||
);
|
||||
|
||||
// These checks come after the content checks as otherwise we won't see the failing content
|
||||
// when these checks fail.
|
||||
// Make sure the scrollback is the right size.
|
||||
assert_eq!(backend.scrollback.area.width, 10);
|
||||
assert_eq!(backend.scrollback.area.height, 65535);
|
||||
assert_eq!(backend.scrollback.content.len(), 10 * 65535);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn size() {
|
||||
let backend = TestBackend::new(10, 2);
|
||||
assert_eq!(backend.size().unwrap(), Size::new(10, 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flush() {
|
||||
let mut backend = TestBackend::new(10, 2);
|
||||
backend.flush().unwrap();
|
||||
}
|
||||
}
|
||||
9
ratatui-core/src/buffer.rs
Normal file
9
ratatui-core/src/buffer.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
#![warn(missing_docs)]
|
||||
//! A module for the [`Buffer`] and [`Cell`] types.
|
||||
|
||||
mod assert;
|
||||
mod buffer;
|
||||
mod cell;
|
||||
|
||||
pub use buffer::Buffer;
|
||||
pub use cell::Cell;
|
||||
69
ratatui-core/src/buffer/assert.rs
Normal file
69
ratatui-core/src/buffer/assert.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
/// Assert that two buffers are equal by comparing their areas and content.
|
||||
///
|
||||
/// # Panics
|
||||
/// When the buffers differ this method panics and displays the differences similar to
|
||||
/// `assert_eq!()`.
|
||||
#[deprecated = "use assert_eq!(&actual, &expected)"]
|
||||
#[macro_export]
|
||||
macro_rules! assert_buffer_eq {
|
||||
($actual_expr:expr, $expected_expr:expr) => {
|
||||
match (&$actual_expr, &$expected_expr) {
|
||||
(actual, expected) => {
|
||||
assert!(
|
||||
actual.area == expected.area,
|
||||
"buffer areas not equal\nexpected: {expected:?}\nactual: {actual:?}",
|
||||
);
|
||||
let nice_diff = expected
|
||||
.diff(actual)
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, (x, y, cell))| {
|
||||
let expected_cell = &expected[(x, y)];
|
||||
format!("{i}: at ({x}, {y})\n expected: {expected_cell:?}\n actual: {cell:?}")
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
assert!(
|
||||
nice_diff.is_empty(),
|
||||
"buffer contents not equal\nexpected: {expected:?}\nactual: {actual:?}\ndiff:\n{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 are not equal in an unexpected way. Please open an issue about this."
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::prelude::*;
|
||||
|
||||
#[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 = "buffer areas not equal"]
|
||||
#[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 = "buffer contents not equal"]
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
1247
ratatui-core/src/buffer/buffer.rs
Normal file
1247
ratatui-core/src/buffer/buffer.rs
Normal file
File diff suppressed because it is too large
Load Diff
297
ratatui-core/src/buffer/cell.rs
Normal file
297
ratatui-core/src/buffer/cell.rs
Normal file
@@ -0,0 +1,297 @@
|
||||
use compact_str::CompactString;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// A buffer cell
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Cell {
|
||||
/// The string to be drawn in the cell.
|
||||
///
|
||||
/// This accepts unicode grapheme clusters which might take up more than one cell.
|
||||
///
|
||||
/// This is a [`CompactString`] which is a wrapper around [`String`] that uses a small inline
|
||||
/// buffer for short strings.
|
||||
///
|
||||
/// See <https://github.com/ratatui/ratatui/pull/601> for more information.
|
||||
symbol: CompactString,
|
||||
|
||||
/// The foreground color of the cell.
|
||||
pub fg: Color,
|
||||
|
||||
/// The background color of the cell.
|
||||
pub bg: Color,
|
||||
|
||||
/// The underline color of the cell.
|
||||
#[cfg(feature = "underline-color")]
|
||||
pub underline_color: Color,
|
||||
|
||||
/// The modifier of the cell.
|
||||
pub modifier: Modifier,
|
||||
|
||||
/// Whether the cell should be skipped when copying (diffing) the buffer to the screen.
|
||||
pub skip: bool,
|
||||
}
|
||||
|
||||
impl Cell {
|
||||
/// An empty `Cell`
|
||||
pub const EMPTY: Self = Self::new(" ");
|
||||
|
||||
/// Creates a new `Cell` with the given symbol.
|
||||
///
|
||||
/// This works at compile time and puts the symbol onto the stack. Fails to build when the
|
||||
/// symbol doesnt fit onto the stack and requires to be placed on the heap. Use
|
||||
/// `Self::default().set_symbol()` in that case. See [`CompactString::const_new`] for more
|
||||
/// details on this.
|
||||
pub const fn new(symbol: &'static str) -> Self {
|
||||
Self {
|
||||
symbol: CompactString::const_new(symbol),
|
||||
fg: Color::Reset,
|
||||
bg: Color::Reset,
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: Color::Reset,
|
||||
modifier: Modifier::empty(),
|
||||
skip: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the symbol of the cell.
|
||||
#[must_use]
|
||||
pub fn symbol(&self) -> &str {
|
||||
self.symbol.as_str()
|
||||
}
|
||||
|
||||
/// Sets the symbol of the cell.
|
||||
pub fn set_symbol(&mut self, symbol: &str) -> &mut Self {
|
||||
self.symbol = CompactString::new(symbol);
|
||||
self
|
||||
}
|
||||
|
||||
/// Appends a symbol to the cell.
|
||||
///
|
||||
/// This is particularly useful for adding zero-width characters to the cell.
|
||||
pub(crate) fn append_symbol(&mut self, symbol: &str) -> &mut Self {
|
||||
self.symbol.push_str(symbol);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the symbol of the cell to a single character.
|
||||
pub fn set_char(&mut self, ch: char) -> &mut Self {
|
||||
let mut buf = [0; 4];
|
||||
self.symbol = CompactString::new(ch.encode_utf8(&mut buf));
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the foreground color of the cell.
|
||||
pub fn set_fg(&mut self, color: Color) -> &mut Self {
|
||||
self.fg = color;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the background color of the cell.
|
||||
pub fn set_bg(&mut self, color: Color) -> &mut Self {
|
||||
self.bg = color;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the cell.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
pub fn set_style<S: Into<Style>>(&mut self, style: S) -> &mut Self {
|
||||
let style = style.into();
|
||||
if let Some(c) = style.fg {
|
||||
self.fg = c;
|
||||
}
|
||||
if let Some(c) = style.bg {
|
||||
self.bg = c;
|
||||
}
|
||||
#[cfg(feature = "underline-color")]
|
||||
if let Some(c) = style.underline_color {
|
||||
self.underline_color = c;
|
||||
}
|
||||
self.modifier.insert(style.add_modifier);
|
||||
self.modifier.remove(style.sub_modifier);
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the style of the cell.
|
||||
#[must_use]
|
||||
pub const fn style(&self) -> Style {
|
||||
Style {
|
||||
fg: Some(self.fg),
|
||||
bg: Some(self.bg),
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: Some(self.underline_color),
|
||||
add_modifier: self.modifier,
|
||||
sub_modifier: Modifier::empty(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the cell to be skipped when copying (diffing) the buffer to the screen.
|
||||
///
|
||||
/// This is helpful when it is necessary to prevent the buffer from overwriting a cell that is
|
||||
/// covered by an image from some terminal graphics protocol (Sixel / iTerm / Kitty ...).
|
||||
pub fn set_skip(&mut self, skip: bool) -> &mut Self {
|
||||
self.skip = skip;
|
||||
self
|
||||
}
|
||||
|
||||
/// Resets the cell to the empty state.
|
||||
pub fn reset(&mut self) {
|
||||
self.symbol = CompactString::const_new(" ");
|
||||
self.fg = Color::Reset;
|
||||
self.bg = Color::Reset;
|
||||
#[cfg(feature = "underline-color")]
|
||||
{
|
||||
self.underline_color = Color::Reset;
|
||||
}
|
||||
self.modifier = Modifier::empty();
|
||||
self.skip = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Cell {
|
||||
fn default() -> Self {
|
||||
Self::EMPTY
|
||||
}
|
||||
}
|
||||
|
||||
impl From<char> for Cell {
|
||||
fn from(ch: char) -> Self {
|
||||
let mut cell = Self::EMPTY;
|
||||
cell.set_char(ch);
|
||||
cell
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn new() {
|
||||
let cell = Cell::new("あ");
|
||||
assert_eq!(
|
||||
cell,
|
||||
Cell {
|
||||
symbol: CompactString::const_new("あ"),
|
||||
fg: Color::Reset,
|
||||
bg: Color::Reset,
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: Color::Reset,
|
||||
modifier: Modifier::empty(),
|
||||
skip: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
let cell = Cell::EMPTY;
|
||||
assert_eq!(cell.symbol(), " ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_symbol() {
|
||||
let mut cell = Cell::EMPTY;
|
||||
cell.set_symbol("あ"); // Multi-byte character
|
||||
assert_eq!(cell.symbol(), "あ");
|
||||
cell.set_symbol("👨👩👧👦"); // Multiple code units combined with ZWJ
|
||||
assert_eq!(cell.symbol(), "👨👩👧👦");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_symbol() {
|
||||
let mut cell = Cell::EMPTY;
|
||||
cell.set_symbol("あ"); // Multi-byte character
|
||||
cell.append_symbol("\u{200B}"); // zero-width space
|
||||
assert_eq!(cell.symbol(), "あ\u{200B}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_char() {
|
||||
let mut cell = Cell::EMPTY;
|
||||
cell.set_char('あ'); // Multi-byte character
|
||||
assert_eq!(cell.symbol(), "あ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_fg() {
|
||||
let mut cell = Cell::EMPTY;
|
||||
cell.set_fg(Color::Red);
|
||||
assert_eq!(cell.fg, Color::Red);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_bg() {
|
||||
let mut cell = Cell::EMPTY;
|
||||
cell.set_bg(Color::Red);
|
||||
assert_eq!(cell.bg, Color::Red);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_style() {
|
||||
let mut cell = Cell::EMPTY;
|
||||
cell.set_style(Style::new().fg(Color::Red).bg(Color::Blue));
|
||||
assert_eq!(cell.fg, Color::Red);
|
||||
assert_eq!(cell.bg, Color::Blue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_skip() {
|
||||
let mut cell = Cell::EMPTY;
|
||||
cell.set_skip(true);
|
||||
assert!(cell.skip);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset() {
|
||||
let mut cell = Cell::EMPTY;
|
||||
cell.set_symbol("あ");
|
||||
cell.set_fg(Color::Red);
|
||||
cell.set_bg(Color::Blue);
|
||||
cell.set_skip(true);
|
||||
cell.reset();
|
||||
assert_eq!(cell.symbol(), " ");
|
||||
assert_eq!(cell.fg, Color::Reset);
|
||||
assert_eq!(cell.bg, Color::Reset);
|
||||
assert!(!cell.skip);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn style() {
|
||||
let cell = Cell::EMPTY;
|
||||
assert_eq!(
|
||||
cell.style(),
|
||||
Style {
|
||||
fg: Some(Color::Reset),
|
||||
bg: Some(Color::Reset),
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: Some(Color::Reset),
|
||||
add_modifier: Modifier::empty(),
|
||||
sub_modifier: Modifier::empty(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default() {
|
||||
let cell = Cell::default();
|
||||
assert_eq!(cell.symbol(), " ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cell_eq() {
|
||||
let cell1 = Cell::new("あ");
|
||||
let cell2 = Cell::new("あ");
|
||||
assert_eq!(cell1, cell2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cell_ne() {
|
||||
let cell1 = Cell::new("あ");
|
||||
let cell2 = Cell::new("い");
|
||||
assert_ne!(cell1, cell2);
|
||||
}
|
||||
}
|
||||
21
ratatui-core/src/layout.rs
Normal file
21
ratatui-core/src/layout.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
#![warn(clippy::missing_const_for_fn)]
|
||||
|
||||
mod alignment;
|
||||
mod constraint;
|
||||
mod direction;
|
||||
mod flex;
|
||||
mod layout;
|
||||
mod margin;
|
||||
mod position;
|
||||
mod rect;
|
||||
mod size;
|
||||
|
||||
pub use alignment::Alignment;
|
||||
pub use constraint::Constraint;
|
||||
pub use direction::Direction;
|
||||
pub use flex::Flex;
|
||||
pub use layout::Layout;
|
||||
pub use margin::Margin;
|
||||
pub use position::Position;
|
||||
pub use rect::*;
|
||||
pub use size::Size;
|
||||
31
ratatui-core/src/layout/alignment.rs
Normal file
31
ratatui-core/src/layout/alignment.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum Alignment {
|
||||
#[default]
|
||||
Left,
|
||||
Center,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use strum::ParseError;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn alignment_to_string() {
|
||||
assert_eq!(Alignment::Left.to_string(), "Left");
|
||||
assert_eq!(Alignment::Center.to_string(), "Center");
|
||||
assert_eq!(Alignment::Right.to_string(), "Right");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alignment_from_str() {
|
||||
assert_eq!("Left".parse::<Alignment>(), Ok(Alignment::Left));
|
||||
assert_eq!("Center".parse::<Alignment>(), Ok(Alignment::Center));
|
||||
assert_eq!("Right".parse::<Alignment>(), Ok(Alignment::Right));
|
||||
assert_eq!("".parse::<Alignment>(), Err(ParseError::VariantNotFound));
|
||||
}
|
||||
}
|
||||
491
ratatui-core/src/layout/constraint.rs
Normal file
491
ratatui-core/src/layout/constraint.rs
Normal file
@@ -0,0 +1,491 @@
|
||||
use std::fmt;
|
||||
|
||||
use strum::EnumIs;
|
||||
|
||||
/// A constraint that defines the size of a layout element.
|
||||
///
|
||||
/// Constraints can be used to specify a fixed size, a percentage of the available space, a ratio of
|
||||
/// the available space, a minimum or maximum size or a fill proportional value for a layout
|
||||
/// element.
|
||||
///
|
||||
/// Relative constraints (percentage, ratio) are calculated relative to the entire space being
|
||||
/// divided, rather than the space available after applying more fixed constraints (min, max,
|
||||
/// length).
|
||||
///
|
||||
/// Constraints are prioritized in the following order:
|
||||
///
|
||||
/// 1. [`Constraint::Min`]
|
||||
/// 2. [`Constraint::Max`]
|
||||
/// 3. [`Constraint::Length`]
|
||||
/// 4. [`Constraint::Percentage`]
|
||||
/// 5. [`Constraint::Ratio`]
|
||||
/// 6. [`Constraint::Fill`]
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// `Constraint` provides helper methods to create lists of constraints from various input formats.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// // Create a layout with specified lengths for each element
|
||||
/// let constraints = Constraint::from_lengths([10, 20, 10]);
|
||||
///
|
||||
/// // Create a centered layout using ratio or percentage constraints
|
||||
/// let constraints = Constraint::from_ratios([(1, 4), (1, 2), (1, 4)]);
|
||||
/// let constraints = Constraint::from_percentages([25, 50, 25]);
|
||||
///
|
||||
/// // Create a centered layout with a minimum size constraint for specific elements
|
||||
/// let constraints = Constraint::from_mins([0, 100, 0]);
|
||||
///
|
||||
/// // Create a sidebar layout specifying maximum sizes for the columns
|
||||
/// let constraints = Constraint::from_maxes([30, 170]);
|
||||
///
|
||||
/// // Create a layout with fill proportional sizes for each element
|
||||
/// let constraints = Constraint::from_fills([1, 2, 1]);
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, EnumIs)]
|
||||
pub enum Constraint {
|
||||
/// Applies a minimum size constraint to the element
|
||||
///
|
||||
/// The element size is set to at least the specified amount.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// `[Percentage(100), Min(20)]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌────────────────────────────┐┌──────────────────┐
|
||||
/// │ 30 px ││ 20 px │
|
||||
/// └────────────────────────────┘└──────────────────┘
|
||||
/// ```
|
||||
///
|
||||
/// `[Percentage(100), Min(10)]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌──────────────────────────────────────┐┌────────┐
|
||||
/// │ 40 px ││ 10 px │
|
||||
/// └──────────────────────────────────────┘└────────┘
|
||||
/// ```
|
||||
Min(u16),
|
||||
|
||||
/// Applies a maximum size constraint to the element
|
||||
///
|
||||
/// The element size is set to at most the specified amount.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// `[Percentage(0), Max(20)]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌────────────────────────────┐┌──────────────────┐
|
||||
/// │ 30 px ││ 20 px │
|
||||
/// └────────────────────────────┘└──────────────────┘
|
||||
/// ```
|
||||
///
|
||||
/// `[Percentage(0), Max(10)]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌──────────────────────────────────────┐┌────────┐
|
||||
/// │ 40 px ││ 10 px │
|
||||
/// └──────────────────────────────────────┘└────────┘
|
||||
/// ```
|
||||
Max(u16),
|
||||
|
||||
/// Applies a length constraint to the element
|
||||
///
|
||||
/// The element size is set to the specified amount.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// `[Length(20), Length(20)]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌──────────────────┐┌──────────────────┐
|
||||
/// │ 20 px ││ 20 px │
|
||||
/// └──────────────────┘└──────────────────┘
|
||||
/// ```
|
||||
///
|
||||
/// `[Length(20), Length(30)]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌──────────────────┐┌────────────────────────────┐
|
||||
/// │ 20 px ││ 30 px │
|
||||
/// └──────────────────┘└────────────────────────────┘
|
||||
/// ```
|
||||
Length(u16),
|
||||
|
||||
/// Applies a percentage of the available space to the element
|
||||
///
|
||||
/// Converts the given percentage to a floating-point value and multiplies that with area. This
|
||||
/// value is rounded back to a integer as part of the layout split calculation.
|
||||
///
|
||||
/// **Note**: As this value only accepts a `u16`, certain percentages that cannot be
|
||||
/// represented exactly (e.g. 1/3) are not possible. You might want to use
|
||||
/// [`Constraint::Ratio`] or [`Constraint::Fill`] in such cases.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// `[Percentage(75), Fill(1)]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌────────────────────────────────────┐┌──────────┐
|
||||
/// │ 38 px ││ 12 px │
|
||||
/// └────────────────────────────────────┘└──────────┘
|
||||
/// ```
|
||||
///
|
||||
/// `[Percentage(50), Fill(1)]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌───────────────────────┐┌───────────────────────┐
|
||||
/// │ 25 px ││ 25 px │
|
||||
/// └───────────────────────┘└───────────────────────┘
|
||||
/// ```
|
||||
Percentage(u16),
|
||||
|
||||
/// Applies a ratio of the available space to the element
|
||||
///
|
||||
/// Converts the given ratio to a floating-point value and multiplies that with area.
|
||||
/// This value is rounded back to a integer as part of the layout split calculation.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// `[Ratio(1, 2) ; 2]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌───────────────────────┐┌───────────────────────┐
|
||||
/// │ 25 px ││ 25 px │
|
||||
/// └───────────────────────┘└───────────────────────┘
|
||||
/// ```
|
||||
///
|
||||
/// `[Ratio(1, 4) ; 4]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌───────────┐┌──────────┐┌───────────┐┌──────────┐
|
||||
/// │ 13 px ││ 12 px ││ 13 px ││ 12 px │
|
||||
/// └───────────┘└──────────┘└───────────┘└──────────┘
|
||||
/// ```
|
||||
Ratio(u32, u32),
|
||||
|
||||
/// Applies the scaling factor proportional to all other [`Constraint::Fill`] elements
|
||||
/// to fill excess space
|
||||
///
|
||||
/// The element will only expand or fill into excess available space, proportionally matching
|
||||
/// other [`Constraint::Fill`] elements while satisfying all other constraints.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
///
|
||||
/// `[Fill(1), Fill(2), Fill(3)]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌──────┐┌───────────────┐┌───────────────────────┐
|
||||
/// │ 8 px ││ 17 px ││ 25 px │
|
||||
/// └──────┘└───────────────┘└───────────────────────┘
|
||||
/// ```
|
||||
///
|
||||
/// `[Fill(1), Percentage(50), Fill(1)]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌───────────┐┌───────────────────────┐┌──────────┐
|
||||
/// │ 13 px ││ 25 px ││ 12 px │
|
||||
/// └───────────┘└───────────────────────┘└──────────┘
|
||||
/// ```
|
||||
Fill(u16),
|
||||
}
|
||||
|
||||
impl Constraint {
|
||||
#[deprecated(
|
||||
since = "0.26.0",
|
||||
note = "This field will be hidden in the next minor version."
|
||||
)]
|
||||
pub fn apply(&self, length: u16) -> u16 {
|
||||
match *self {
|
||||
Self::Percentage(p) => {
|
||||
let p = f32::from(p) / 100.0;
|
||||
let length = f32::from(length);
|
||||
(p * length).min(length) as u16
|
||||
}
|
||||
Self::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 = f32::from(length);
|
||||
(percentage * length).min(length) as u16
|
||||
}
|
||||
Self::Length(l) | Self::Fill(l) => length.min(l),
|
||||
Self::Max(m) => length.min(m),
|
||||
Self::Min(m) => length.max(m),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an iterator of lengths into a vector of constraints
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # let area = Rect::default();
|
||||
/// let constraints = Constraint::from_lengths([1, 2, 3]);
|
||||
/// let layout = Layout::default().constraints(constraints).split(area);
|
||||
/// ```
|
||||
pub fn from_lengths<T>(lengths: T) -> Vec<Self>
|
||||
where
|
||||
T: IntoIterator<Item = u16>,
|
||||
{
|
||||
lengths.into_iter().map(Self::Length).collect()
|
||||
}
|
||||
|
||||
/// Convert an iterator of ratios into a vector of constraints
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # let area = Rect::default();
|
||||
/// let constraints = Constraint::from_ratios([(1, 4), (1, 2), (1, 4)]);
|
||||
/// let layout = Layout::default().constraints(constraints).split(area);
|
||||
/// ```
|
||||
pub fn from_ratios<T>(ratios: T) -> Vec<Self>
|
||||
where
|
||||
T: IntoIterator<Item = (u32, u32)>,
|
||||
{
|
||||
ratios.into_iter().map(|(n, d)| Self::Ratio(n, d)).collect()
|
||||
}
|
||||
|
||||
/// Convert an iterator of percentages into a vector of constraints
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # let area = Rect::default();
|
||||
/// let constraints = Constraint::from_percentages([25, 50, 25]);
|
||||
/// let layout = Layout::default().constraints(constraints).split(area);
|
||||
/// ```
|
||||
pub fn from_percentages<T>(percentages: T) -> Vec<Self>
|
||||
where
|
||||
T: IntoIterator<Item = u16>,
|
||||
{
|
||||
percentages.into_iter().map(Self::Percentage).collect()
|
||||
}
|
||||
|
||||
/// Convert an iterator of maxes into a vector of constraints
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # let area = Rect::default();
|
||||
/// let constraints = Constraint::from_maxes([1, 2, 3]);
|
||||
/// let layout = Layout::default().constraints(constraints).split(area);
|
||||
/// ```
|
||||
pub fn from_maxes<T>(maxes: T) -> Vec<Self>
|
||||
where
|
||||
T: IntoIterator<Item = u16>,
|
||||
{
|
||||
maxes.into_iter().map(Self::Max).collect()
|
||||
}
|
||||
|
||||
/// Convert an iterator of mins into a vector of constraints
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # let area = Rect::default();
|
||||
/// let constraints = Constraint::from_mins([1, 2, 3]);
|
||||
/// let layout = Layout::default().constraints(constraints).split(area);
|
||||
/// ```
|
||||
pub fn from_mins<T>(mins: T) -> Vec<Self>
|
||||
where
|
||||
T: IntoIterator<Item = u16>,
|
||||
{
|
||||
mins.into_iter().map(Self::Min).collect()
|
||||
}
|
||||
|
||||
/// Convert an iterator of proportional factors into a vector of constraints
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # let area = Rect::default();
|
||||
/// let constraints = Constraint::from_fills([1, 2, 3]);
|
||||
/// let layout = Layout::default().constraints(constraints).split(area);
|
||||
/// ```
|
||||
pub fn from_fills<T>(proportional_factors: T) -> Vec<Self>
|
||||
where
|
||||
T: IntoIterator<Item = u16>,
|
||||
{
|
||||
proportional_factors.into_iter().map(Self::Fill).collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u16> for Constraint {
|
||||
/// Convert a `u16` into a [`Constraint::Length`]
|
||||
///
|
||||
/// This is useful when you want to specify a fixed size for a layout, but don't want to
|
||||
/// explicitly create a [`Constraint::Length`] yourself.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # let area = Rect::default();
|
||||
/// let layout = Layout::new(Direction::Vertical, [1, 2, 3]).split(area);
|
||||
/// let layout = Layout::horizontal([1, 2, 3]).split(area);
|
||||
/// let layout = Layout::vertical([1, 2, 3]).split(area);
|
||||
/// ````
|
||||
fn from(length: u16) -> Self {
|
||||
Self::Length(length)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Self> for Constraint {
|
||||
fn from(constraint: &Self) -> Self {
|
||||
*constraint
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Self> for Constraint {
|
||||
fn as_ref(&self) -> &Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Constraint {
|
||||
fn default() -> Self {
|
||||
Self::Percentage(100)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Constraint {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Percentage(p) => write!(f, "Percentage({p})"),
|
||||
Self::Ratio(n, d) => write!(f, "Ratio({n}, {d})"),
|
||||
Self::Length(l) => write!(f, "Length({l})"),
|
||||
Self::Fill(l) => write!(f, "Fill({l})"),
|
||||
Self::Max(m) => write!(f, "Max({m})"),
|
||||
Self::Min(m) => write!(f, "Min({m})"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default() {
|
||||
assert_eq!(Constraint::default(), Constraint::Percentage(100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_string() {
|
||||
assert_eq!(Constraint::Percentage(50).to_string(), "Percentage(50)");
|
||||
assert_eq!(Constraint::Ratio(1, 2).to_string(), "Ratio(1, 2)");
|
||||
assert_eq!(Constraint::Length(10).to_string(), "Length(10)");
|
||||
assert_eq!(Constraint::Max(10).to_string(), "Max(10)");
|
||||
assert_eq!(Constraint::Min(10).to_string(), "Min(10)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_lengths() {
|
||||
let expected = [
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(2),
|
||||
Constraint::Length(3),
|
||||
];
|
||||
assert_eq!(Constraint::from_lengths([1, 2, 3]), expected);
|
||||
assert_eq!(Constraint::from_lengths(vec![1, 2, 3]), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_ratios() {
|
||||
let expected = [
|
||||
Constraint::Ratio(1, 4),
|
||||
Constraint::Ratio(1, 2),
|
||||
Constraint::Ratio(1, 4),
|
||||
];
|
||||
assert_eq!(Constraint::from_ratios([(1, 4), (1, 2), (1, 4)]), expected);
|
||||
assert_eq!(
|
||||
Constraint::from_ratios(vec![(1, 4), (1, 2), (1, 4)]),
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_percentages() {
|
||||
let expected = [
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Percentage(25),
|
||||
];
|
||||
assert_eq!(Constraint::from_percentages([25, 50, 25]), expected);
|
||||
assert_eq!(Constraint::from_percentages(vec![25, 50, 25]), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_maxes() {
|
||||
let expected = [Constraint::Max(1), Constraint::Max(2), Constraint::Max(3)];
|
||||
assert_eq!(Constraint::from_maxes([1, 2, 3]), expected);
|
||||
assert_eq!(Constraint::from_maxes(vec![1, 2, 3]), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_mins() {
|
||||
let expected = [Constraint::Min(1), Constraint::Min(2), Constraint::Min(3)];
|
||||
assert_eq!(Constraint::from_mins([1, 2, 3]), expected);
|
||||
assert_eq!(Constraint::from_mins(vec![1, 2, 3]), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_fills() {
|
||||
let expected = [
|
||||
Constraint::Fill(1),
|
||||
Constraint::Fill(2),
|
||||
Constraint::Fill(3),
|
||||
];
|
||||
assert_eq!(Constraint::from_fills([1, 2, 3]), expected);
|
||||
assert_eq!(Constraint::from_fills(vec![1, 2, 3]), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(deprecated)]
|
||||
fn 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);
|
||||
}
|
||||
}
|
||||
28
ratatui-core/src/layout/direction.rs
Normal file
28
ratatui-core/src/layout/direction.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum Direction {
|
||||
Horizontal,
|
||||
#[default]
|
||||
Vertical,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use strum::ParseError;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn direction_to_string() {
|
||||
assert_eq!(Direction::Horizontal.to_string(), "Horizontal");
|
||||
assert_eq!(Direction::Vertical.to_string(), "Vertical");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direction_from_str() {
|
||||
assert_eq!("Horizontal".parse::<Direction>(), Ok(Direction::Horizontal));
|
||||
assert_eq!("Vertical".parse::<Direction>(), Ok(Direction::Vertical));
|
||||
assert_eq!("".parse::<Direction>(), Err(ParseError::VariantNotFound));
|
||||
}
|
||||
}
|
||||
181
ratatui-core/src/layout/flex.rs
Normal file
181
ratatui-core/src/layout/flex.rs
Normal file
@@ -0,0 +1,181 @@
|
||||
use strum::{Display, EnumIs, EnumString};
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use super::constraint::Constraint;
|
||||
|
||||
/// Defines the options for layout flex justify content in a container.
|
||||
///
|
||||
/// This enumeration controls the distribution of space when layout constraints are met.
|
||||
///
|
||||
/// - `Legacy`: Fills the available space within the container, putting excess space into the last
|
||||
/// element.
|
||||
/// - `Start`: Aligns items to the start of the container.
|
||||
/// - `End`: Aligns items to the end of the container.
|
||||
/// - `Center`: Centers items within the container.
|
||||
/// - `SpaceBetween`: Adds excess space between each element.
|
||||
/// - `SpaceAround`: Adds excess space around each element.
|
||||
#[derive(Copy, Debug, Default, Display, EnumString, Clone, Eq, PartialEq, Hash, EnumIs)]
|
||||
pub enum Flex {
|
||||
/// Fills the available space within the container, putting excess space into the last
|
||||
/// constraint of the lowest priority. This matches the default behavior of ratatui and tui
|
||||
/// applications without [`Flex`]
|
||||
///
|
||||
/// The following examples illustrate the allocation of excess in various combinations of
|
||||
/// constraints. As a refresher, the priorities of constraints are as follows:
|
||||
///
|
||||
/// 1. [`Constraint::Min`]
|
||||
/// 2. [`Constraint::Max`]
|
||||
/// 3. [`Constraint::Length`]
|
||||
/// 4. [`Constraint::Percentage`]
|
||||
/// 5. [`Constraint::Ratio`]
|
||||
/// 6. [`Constraint::Fill`]
|
||||
///
|
||||
/// When every constraint is `Length`, the last element gets the excess.
|
||||
///
|
||||
/// ```plain
|
||||
/// <----------------------------------- 80 px ------------------------------------>
|
||||
/// ┌──────20 px───────┐┌──────20 px───────┐┌────────────────40 px─────────────────┐
|
||||
/// │ Length(20) ││ Length(20) ││ Length(20) │
|
||||
/// └──────────────────┘└──────────────────┘└──────────────────────────────────────┘
|
||||
/// ^^^^^^^^^^^^^^^^ EXCESS ^^^^^^^^^^^^^^^^
|
||||
/// ```
|
||||
///
|
||||
/// Fill constraints have the lowest priority amongst all the constraints and hence
|
||||
/// will always take up any excess space available.
|
||||
///
|
||||
/// ```plain
|
||||
/// <----------------------------------- 80 px ------------------------------------>
|
||||
/// ┌──────20 px───────┐┌──────20 px───────┐┌──────20 px───────┐┌──────20 px───────┐
|
||||
/// │ Fill(0) ││ Max(20) ││ Length(20) ││ Length(20) │
|
||||
/// └──────────────────┘└──────────────────┘└──────────────────┘└──────────────────┘
|
||||
/// ^^^^^^ EXCESS ^^^^^^
|
||||
/// ```
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```plain
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌──────────────────────────60 px───────────────────────────┐┌──────20 px───────┐
|
||||
/// │ Min(20) ││ Max(20) │
|
||||
/// └──────────────────────────────────────────────────────────┘└──────────────────┘
|
||||
///
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌────────────────────────────────────80 px─────────────────────────────────────┐
|
||||
/// │ Max(20) │
|
||||
/// └──────────────────────────────────────────────────────────────────────────────┘
|
||||
/// ```
|
||||
Legacy,
|
||||
|
||||
/// Aligns items to the start of the container.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```plain
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌────16 px─────┐┌──────20 px───────┐┌──────20 px───────┐
|
||||
/// │Percentage(20)││ Length(20) ││ Fixed(20) │
|
||||
/// └──────────────┘└──────────────────┘└──────────────────┘
|
||||
///
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌──────20 px───────┐┌──────20 px───────┐
|
||||
/// │ Max(20) ││ Max(20) │
|
||||
/// └──────────────────┘└──────────────────┘
|
||||
///
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌──────20 px───────┐
|
||||
/// │ Max(20) │
|
||||
/// └──────────────────┘
|
||||
/// ```
|
||||
#[default]
|
||||
Start,
|
||||
|
||||
/// Aligns items to the end of the container.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```plain
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌────16 px─────┐┌──────20 px───────┐┌──────20 px───────┐
|
||||
/// │Percentage(20)││ Length(20) ││ Length(20) │
|
||||
/// └──────────────┘└──────────────────┘└──────────────────┘
|
||||
///
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌──────20 px───────┐┌──────20 px───────┐
|
||||
/// │ Max(20) ││ Max(20) │
|
||||
/// └──────────────────┘└──────────────────┘
|
||||
///
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌──────20 px───────┐
|
||||
/// │ Max(20) │
|
||||
/// └──────────────────┘
|
||||
/// ```
|
||||
End,
|
||||
|
||||
/// Centers items within the container.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```plain
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌────16 px─────┐┌──────20 px───────┐┌──────20 px───────┐
|
||||
/// │Percentage(20)││ Length(20) ││ Length(20) │
|
||||
/// └──────────────┘└──────────────────┘└──────────────────┘
|
||||
///
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌──────20 px───────┐┌──────20 px───────┐
|
||||
/// │ Max(20) ││ Max(20) │
|
||||
/// └──────────────────┘└──────────────────┘
|
||||
///
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌──────20 px───────┐
|
||||
/// │ Max(20) │
|
||||
/// └──────────────────┘
|
||||
/// ```
|
||||
Center,
|
||||
|
||||
/// Adds excess space between each element.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```plain
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌────16 px─────┐ ┌──────20 px───────┐ ┌──────20 px───────┐
|
||||
/// │Percentage(20)│ │ Length(20) │ │ Length(20) │
|
||||
/// └──────────────┘ └──────────────────┘ └──────────────────┘
|
||||
///
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌──────20 px───────┐ ┌──────20 px───────┐
|
||||
/// │ Max(20) │ │ Max(20) │
|
||||
/// └──────────────────┘ └──────────────────┘
|
||||
///
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌────────────────────────────────────80 px─────────────────────────────────────┐
|
||||
/// │ Max(20) │
|
||||
/// └──────────────────────────────────────────────────────────────────────────────┘
|
||||
/// ```
|
||||
SpaceBetween,
|
||||
|
||||
/// Adds excess space around each element.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```plain
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌────16 px─────┐ ┌──────20 px───────┐ ┌──────20 px───────┐
|
||||
/// │Percentage(20)│ │ Length(20) │ │ Length(20) │
|
||||
/// └──────────────┘ └──────────────────┘ └──────────────────┘
|
||||
///
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌──────20 px───────┐ ┌──────20 px───────┐
|
||||
/// │ Max(20) │ │ Max(20) │
|
||||
/// └──────────────────┘ └──────────────────┘
|
||||
///
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌──────20 px───────┐
|
||||
/// │ Max(20) │
|
||||
/// └──────────────────┘
|
||||
/// ```
|
||||
SpaceAround,
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {}
|
||||
2526
ratatui-core/src/layout/layout.rs
Normal file
2526
ratatui-core/src/layout/layout.rs
Normal file
File diff suppressed because it is too large
Load Diff
44
ratatui-core/src/layout/margin.rs
Normal file
44
ratatui-core/src/layout/margin.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Margin {
|
||||
pub horizontal: u16,
|
||||
pub vertical: u16,
|
||||
}
|
||||
|
||||
impl Margin {
|
||||
pub const fn new(horizontal: u16, vertical: u16) -> Self {
|
||||
Self {
|
||||
horizontal,
|
||||
vertical,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Margin {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}x{}", self.horizontal, self.vertical)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn margin_to_string() {
|
||||
assert_eq!(Margin::new(1, 2).to_string(), "1x2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn margin_new() {
|
||||
assert_eq!(
|
||||
Margin::new(1, 2),
|
||||
Margin {
|
||||
horizontal: 1,
|
||||
vertical: 2
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
115
ratatui-core/src/layout/position.rs
Normal file
115
ratatui-core/src/layout/position.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
#![warn(missing_docs)]
|
||||
use std::fmt;
|
||||
|
||||
use crate::layout::Rect;
|
||||
|
||||
/// Position in the terminal
|
||||
///
|
||||
/// The position is relative to the top left corner of the terminal window, with the top left corner
|
||||
/// being (0, 0). The x axis is horizontal increasing to the right, and the y axis is vertical
|
||||
/// increasing downwards.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::layout::{Position, Rect};
|
||||
///
|
||||
/// // the following are all equivalent
|
||||
/// let position = Position { x: 1, y: 2 };
|
||||
/// let position = Position::new(1, 2);
|
||||
/// let position = Position::from((1, 2));
|
||||
/// let position = Position::from(Rect::new(1, 2, 3, 4));
|
||||
///
|
||||
/// // position can be converted back into the components when needed
|
||||
/// let (x, y) = position.into();
|
||||
/// ```
|
||||
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Position {
|
||||
/// The x coordinate of the position
|
||||
///
|
||||
/// The x coordinate is relative to the left edge of the terminal window, with the left edge
|
||||
/// being 0.
|
||||
pub x: u16,
|
||||
|
||||
/// The y coordinate of the position
|
||||
///
|
||||
/// The y coordinate is relative to the top edge of the terminal window, with the top edge
|
||||
/// being 0.
|
||||
pub y: u16,
|
||||
}
|
||||
|
||||
impl Position {
|
||||
/// Position at the origin, the top left edge at 0,0
|
||||
pub const ORIGIN: Self = Self { x: 0, y: 0 };
|
||||
|
||||
/// Create a new position
|
||||
pub const fn new(x: u16, y: u16) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(u16, u16)> for Position {
|
||||
fn from((x, y): (u16, u16)) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Position> for (u16, u16) {
|
||||
fn from(position: Position) -> Self {
|
||||
(position.x, position.y)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rect> for Position {
|
||||
fn from(rect: Rect) -> Self {
|
||||
rect.as_position()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Position {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "({}, {})", self.x, self.y)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn new() {
|
||||
let position = Position::new(1, 2);
|
||||
assert_eq!(position.x, 1);
|
||||
assert_eq!(position.y, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_tuple() {
|
||||
let position = Position::from((1, 2));
|
||||
assert_eq!(position.x, 1);
|
||||
assert_eq!(position.y, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn into_tuple() {
|
||||
let position = Position::new(1, 2);
|
||||
let (x, y) = position.into();
|
||||
assert_eq!(x, 1);
|
||||
assert_eq!(y, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_rect() {
|
||||
let rect = Rect::new(1, 2, 3, 4);
|
||||
let position = Position::from(rect);
|
||||
assert_eq!(position.x, 1);
|
||||
assert_eq!(position.y, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_string() {
|
||||
let position = Position::new(1, 2);
|
||||
assert_eq!(position.to_string(), "(1, 2)");
|
||||
}
|
||||
}
|
||||
650
ratatui-core/src/layout/rect.rs
Normal file
650
ratatui-core/src/layout/rect.rs
Normal file
@@ -0,0 +1,650 @@
|
||||
#![warn(missing_docs)]
|
||||
use std::{
|
||||
cmp::{max, min},
|
||||
fmt,
|
||||
};
|
||||
|
||||
use super::{Position, Size};
|
||||
use crate::prelude::*;
|
||||
|
||||
mod iter;
|
||||
pub use iter::*;
|
||||
|
||||
/// A Rectangular area.
|
||||
///
|
||||
/// 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, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Rect {
|
||||
/// The x coordinate of the top left corner of the `Rect`.
|
||||
pub x: u16,
|
||||
/// The y coordinate of the top left corner of the `Rect`.
|
||||
pub y: u16,
|
||||
/// The width of the `Rect`.
|
||||
pub width: u16,
|
||||
/// The height of the `Rect`.
|
||||
pub height: u16,
|
||||
}
|
||||
|
||||
/// Amounts by which to move a [`Rect`](super::Rect).
|
||||
///
|
||||
/// Positive numbers move to the right/bottom and negative to the left/top.
|
||||
///
|
||||
/// See [`Rect::offset`]
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Offset {
|
||||
/// How much to move on the X axis
|
||||
pub x: i32,
|
||||
/// How much to move on the Y axis
|
||||
pub y: i32,
|
||||
}
|
||||
|
||||
impl fmt::Display for Rect {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}x{}+{}+{}", self.width, self.height, self.x, self.y)
|
||||
}
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
/// A zero sized Rect at position 0,0
|
||||
pub const ZERO: Self = Self {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
|
||||
/// 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) -> Self {
|
||||
let max_area = u16::MAX;
|
||||
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)
|
||||
};
|
||||
Self {
|
||||
x,
|
||||
y,
|
||||
width: clipped_width,
|
||||
height: clipped_height,
|
||||
}
|
||||
}
|
||||
|
||||
/// The area of the `Rect`. If the area is larger than the maximum value of `u16`, it will be
|
||||
/// clamped to `u16::MAX`.
|
||||
pub const fn area(self) -> u16 {
|
||||
self.width.saturating_mul(self.height)
|
||||
}
|
||||
|
||||
/// Returns true if the `Rect` has no area.
|
||||
pub const fn is_empty(self) -> bool {
|
||||
self.width == 0 || self.height == 0
|
||||
}
|
||||
|
||||
/// Returns the left coordinate of the `Rect`.
|
||||
pub const fn left(self) -> u16 {
|
||||
self.x
|
||||
}
|
||||
|
||||
/// Returns the right coordinate of the `Rect`. This is the first coordinate outside of the
|
||||
/// `Rect`.
|
||||
///
|
||||
/// If the right coordinate is larger than the maximum value of u16, it will be clamped to
|
||||
/// `u16::MAX`.
|
||||
pub const fn right(self) -> u16 {
|
||||
self.x.saturating_add(self.width)
|
||||
}
|
||||
|
||||
/// Returns the top coordinate of the `Rect`.
|
||||
pub const fn top(self) -> u16 {
|
||||
self.y
|
||||
}
|
||||
|
||||
/// Returns the bottom coordinate of the `Rect`. This is the first coordinate outside of the
|
||||
/// `Rect`.
|
||||
///
|
||||
/// If the bottom coordinate is larger than the maximum value of u16, it will be clamped to
|
||||
/// `u16::MAX`.
|
||||
pub const fn bottom(self) -> u16 {
|
||||
self.y.saturating_add(self.height)
|
||||
}
|
||||
|
||||
/// Returns a new `Rect` inside the current one, with the given margin on each side.
|
||||
///
|
||||
/// If the margin is larger than the `Rect`, the returned `Rect` will have no area.
|
||||
#[must_use = "method returns the modified value"]
|
||||
pub const fn inner(self, margin: Margin) -> Self {
|
||||
let doubled_margin_horizontal = margin.horizontal.saturating_mul(2);
|
||||
let doubled_margin_vertical = margin.vertical.saturating_mul(2);
|
||||
|
||||
if self.width < doubled_margin_horizontal || self.height < doubled_margin_vertical {
|
||||
Self::ZERO
|
||||
} else {
|
||||
Self {
|
||||
x: self.x.saturating_add(margin.horizontal),
|
||||
y: self.y.saturating_add(margin.vertical),
|
||||
width: self.width.saturating_sub(doubled_margin_horizontal),
|
||||
height: self.height.saturating_sub(doubled_margin_vertical),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves the `Rect` without modifying its size.
|
||||
///
|
||||
/// Moves the `Rect` according to the given offset without modifying its [`width`](Rect::width)
|
||||
/// or [`height`](Rect::height).
|
||||
/// - Positive `x` moves the whole `Rect` to the right, negative to the left.
|
||||
/// - Positive `y` moves the whole `Rect` to the bottom, negative to the top.
|
||||
///
|
||||
/// See [`Offset`] for details.
|
||||
#[must_use = "method returns the modified value"]
|
||||
pub fn offset(self, offset: Offset) -> Self {
|
||||
Self {
|
||||
x: i32::from(self.x)
|
||||
.saturating_add(offset.x)
|
||||
.clamp(0, i32::from(u16::MAX - self.width)) as u16,
|
||||
y: i32::from(self.y)
|
||||
.saturating_add(offset.y)
|
||||
.clamp(0, i32::from(u16::MAX - self.height)) as u16,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a new `Rect` that contains both the current one and the given one.
|
||||
#[must_use = "method returns the modified value"]
|
||||
pub fn union(self, other: Self) -> Self {
|
||||
let x1 = min(self.x, other.x);
|
||||
let y1 = min(self.y, other.y);
|
||||
let x2 = max(self.right(), other.right());
|
||||
let y2 = max(self.bottom(), other.bottom());
|
||||
Self {
|
||||
x: x1,
|
||||
y: y1,
|
||||
width: x2.saturating_sub(x1),
|
||||
height: y2.saturating_sub(y1),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a new `Rect` that is the intersection of the current one and the given one.
|
||||
///
|
||||
/// If the two `Rect`s do not intersect, the returned `Rect` will have no area.
|
||||
#[must_use = "method returns the modified value"]
|
||||
pub fn intersection(self, other: Self) -> Self {
|
||||
let x1 = max(self.x, other.x);
|
||||
let y1 = max(self.y, other.y);
|
||||
let x2 = min(self.right(), other.right());
|
||||
let y2 = min(self.bottom(), other.bottom());
|
||||
Self {
|
||||
x: x1,
|
||||
y: y1,
|
||||
width: x2.saturating_sub(x1),
|
||||
height: y2.saturating_sub(y1),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the two `Rect`s intersect.
|
||||
pub const fn intersects(self, other: Self) -> bool {
|
||||
self.x < other.right()
|
||||
&& self.right() > other.x
|
||||
&& self.y < other.bottom()
|
||||
&& self.bottom() > other.y
|
||||
}
|
||||
|
||||
/// Returns true if the given position is inside the `Rect`.
|
||||
///
|
||||
/// The position is considered inside the `Rect` if it is on the `Rect`'s border.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, layout::Position};
|
||||
/// let rect = Rect::new(1, 2, 3, 4);
|
||||
/// assert!(rect.contains(Position { x: 1, y: 2 }));
|
||||
/// ````
|
||||
pub const fn contains(self, position: Position) -> bool {
|
||||
position.x >= self.x
|
||||
&& position.x < self.right()
|
||||
&& position.y >= self.y
|
||||
&& position.y < self.bottom()
|
||||
}
|
||||
|
||||
/// Clamp this `Rect` to fit inside the other `Rect`.
|
||||
///
|
||||
/// If the width or height of this `Rect` is larger than the other `Rect`, it will be clamped to
|
||||
/// the other `Rect`'s width or height.
|
||||
///
|
||||
/// If the left or top coordinate of this `Rect` is smaller than the other `Rect`, it will be
|
||||
/// clamped to the other `Rect`'s left or top coordinate.
|
||||
///
|
||||
/// If the right or bottom coordinate of this `Rect` is larger than the other `Rect`, it will be
|
||||
/// clamped to the other `Rect`'s right or bottom coordinate.
|
||||
///
|
||||
/// This is different from [`Rect::intersection`] because it will move this `Rect` to fit inside
|
||||
/// the other `Rect`, while [`Rect::intersection`] instead would keep this `Rect`'s position and
|
||||
/// truncate its size to only that which is inside the other `Rect`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # fn render(frame: &mut Frame) {
|
||||
/// let area = frame.area();
|
||||
/// let rect = Rect::new(0, 0, 100, 100).clamp(area);
|
||||
/// # }
|
||||
/// ```
|
||||
#[must_use = "method returns the modified value"]
|
||||
pub fn clamp(self, other: Self) -> Self {
|
||||
let width = self.width.min(other.width);
|
||||
let height = self.height.min(other.height);
|
||||
let x = self.x.clamp(other.x, other.right().saturating_sub(width));
|
||||
let y = self.y.clamp(other.y, other.bottom().saturating_sub(height));
|
||||
Self::new(x, y, width, height)
|
||||
}
|
||||
|
||||
/// An iterator over rows within the `Rect`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::prelude::*;
|
||||
/// fn render(area: Rect, buf: &mut Buffer) {
|
||||
/// for row in area.rows() {
|
||||
/// Line::raw("Hello, world!").render(row, buf);
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub const fn rows(self) -> Rows {
|
||||
Rows::new(self)
|
||||
}
|
||||
|
||||
/// An iterator over columns within the `Rect`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// fn render(area: Rect, buf: &mut Buffer) {
|
||||
/// if let Some(left) = area.columns().next() {
|
||||
/// Block::new().borders(Borders::LEFT).render(left, buf);
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub const fn columns(self) -> Columns {
|
||||
Columns::new(self)
|
||||
}
|
||||
|
||||
/// An iterator over the positions within the `Rect`.
|
||||
///
|
||||
/// The positions are returned in a row-major order (left-to-right, top-to-bottom).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::prelude::*;
|
||||
/// fn render(area: Rect, buf: &mut Buffer) {
|
||||
/// for position in area.positions() {
|
||||
/// buf[(position.x, position.y)].set_symbol("x");
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub const fn positions(self) -> Positions {
|
||||
Positions::new(self)
|
||||
}
|
||||
|
||||
/// Returns a [`Position`] with the same coordinates as this `Rect`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let rect = Rect::new(1, 2, 3, 4);
|
||||
/// let position = rect.as_position();
|
||||
/// ````
|
||||
pub const fn as_position(self) -> Position {
|
||||
Position {
|
||||
x: self.x,
|
||||
y: self.y,
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts the `Rect` into a size struct.
|
||||
pub const fn as_size(self) -> Size {
|
||||
Size {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
}
|
||||
}
|
||||
|
||||
/// indents the x value of the `Rect` by a given `offset`
|
||||
///
|
||||
/// This is pub(crate) for now as we need to stabilize the naming / design of this API.
|
||||
#[must_use]
|
||||
pub(crate) const fn indent_x(self, offset: u16) -> Self {
|
||||
Self {
|
||||
x: self.x.saturating_add(offset),
|
||||
width: self.width.saturating_sub(offset),
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(Position, Size)> for Rect {
|
||||
fn from((position, size): (Position, Size)) -> Self {
|
||||
Self {
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn to_string() {
|
||||
assert_eq!(Rect::new(1, 2, 3, 4).to_string(), "3x4+1+2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new() {
|
||||
assert_eq!(
|
||||
Rect::new(1, 2, 3, 4),
|
||||
Rect {
|
||||
x: 1,
|
||||
y: 2,
|
||||
width: 3,
|
||||
height: 4
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn area() {
|
||||
assert_eq!(Rect::new(1, 2, 3, 4).area(), 12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_empty() {
|
||||
assert!(!Rect::new(1, 2, 3, 4).is_empty());
|
||||
assert!(Rect::new(1, 2, 0, 4).is_empty());
|
||||
assert!(Rect::new(1, 2, 3, 0).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn left() {
|
||||
assert_eq!(Rect::new(1, 2, 3, 4).left(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn right() {
|
||||
assert_eq!(Rect::new(1, 2, 3, 4).right(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn top() {
|
||||
assert_eq!(Rect::new(1, 2, 3, 4).top(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bottom() {
|
||||
assert_eq!(Rect::new(1, 2, 3, 4).bottom(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inner() {
|
||||
assert_eq!(
|
||||
Rect::new(1, 2, 3, 4).inner(Margin::new(1, 2)),
|
||||
Rect::new(2, 4, 1, 0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset() {
|
||||
assert_eq!(
|
||||
Rect::new(1, 2, 3, 4).offset(Offset { x: 5, y: 6 }),
|
||||
Rect::new(6, 8, 3, 4),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_offset() {
|
||||
assert_eq!(
|
||||
Rect::new(4, 3, 3, 4).offset(Offset { x: -2, y: -1 }),
|
||||
Rect::new(2, 2, 3, 4),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_offset_saturate() {
|
||||
assert_eq!(
|
||||
Rect::new(1, 2, 3, 4).offset(Offset { x: -5, y: -6 }),
|
||||
Rect::new(0, 0, 3, 4),
|
||||
);
|
||||
}
|
||||
|
||||
/// Offsets a [`Rect`] making it go outside [`u16::MAX`], it should keep its size.
|
||||
#[test]
|
||||
fn offset_saturate_max() {
|
||||
assert_eq!(
|
||||
Rect::new(u16::MAX - 500, u16::MAX - 500, 100, 100).offset(Offset { x: 1000, y: 1000 }),
|
||||
Rect::new(u16::MAX - 100, u16::MAX - 100, 100, 100),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn union() {
|
||||
assert_eq!(
|
||||
Rect::new(1, 2, 3, 4).union(Rect::new(2, 3, 4, 5)),
|
||||
Rect::new(1, 2, 5, 6)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersection() {
|
||||
assert_eq!(
|
||||
Rect::new(1, 2, 3, 4).intersection(Rect::new(2, 3, 4, 5)),
|
||||
Rect::new(2, 3, 2, 3)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersection_underflow() {
|
||||
assert_eq!(
|
||||
Rect::new(1, 1, 2, 2).intersection(Rect::new(4, 4, 2, 2)),
|
||||
Rect::new(4, 4, 0, 0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersects() {
|
||||
assert!(Rect::new(1, 2, 3, 4).intersects(Rect::new(2, 3, 4, 5)));
|
||||
assert!(!Rect::new(1, 2, 3, 4).intersects(Rect::new(5, 6, 7, 8)));
|
||||
}
|
||||
|
||||
// the bounds of this rect are x: [1..=3], y: [2..=5]
|
||||
#[rstest]
|
||||
#[case::inside_top_left(Rect::new(1, 2, 3, 4), Position { x: 1, y: 2 }, true)]
|
||||
#[case::inside_top_right(Rect::new(1, 2, 3, 4), Position { x: 3, y: 2 }, true)]
|
||||
#[case::inside_bottom_left(Rect::new(1, 2, 3, 4), Position { x: 1, y: 5 }, true)]
|
||||
#[case::inside_bottom_right(Rect::new(1, 2, 3, 4), Position { x: 3, y: 5 }, true)]
|
||||
#[case::outside_left(Rect::new(1, 2, 3, 4), Position { x: 0, y: 2 }, false)]
|
||||
#[case::outside_right(Rect::new(1, 2, 3, 4), Position { x: 4, y: 2 }, false)]
|
||||
#[case::outside_top(Rect::new(1, 2, 3, 4), Position { x: 1, y: 1 }, false)]
|
||||
#[case::outside_bottom(Rect::new(1, 2, 3, 4), Position { x: 1, y: 6 }, false)]
|
||||
#[case::outside_top_left(Rect::new(1, 2, 3, 4), Position { x: 0, y: 1 }, false)]
|
||||
#[case::outside_bottom_right(Rect::new(1, 2, 3, 4), Position { x: 4, y: 6 }, false)]
|
||||
fn contains(#[case] rect: Rect, #[case] position: Position, #[case] expected: bool) {
|
||||
assert_eq!(
|
||||
rect.contains(position),
|
||||
expected,
|
||||
"rect: {rect:?}, position: {position:?}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn 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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn 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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn 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 split() {
|
||||
let [a, b] = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.areas(Rect::new(0, 0, 2, 1));
|
||||
assert_eq!(a, Rect::new(0, 0, 1, 1));
|
||||
assert_eq!(b, Rect::new(1, 0, 1, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "invalid number of rects")]
|
||||
fn split_invalid_number_of_recs() {
|
||||
let layout = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
|
||||
let [_a, _b, _c] = layout.areas(Rect::new(0, 0, 2, 1));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::inside(Rect::new(20, 20, 10, 10), Rect::new(20, 20, 10, 10))]
|
||||
#[case::up_left(Rect::new(5, 5, 10, 10), Rect::new(10, 10, 10, 10))]
|
||||
#[case::up(Rect::new(20, 5, 10, 10), Rect::new(20, 10, 10, 10))]
|
||||
#[case::up_right(Rect::new(105, 5, 10, 10), Rect::new(100, 10, 10, 10))]
|
||||
#[case::left(Rect::new(5, 20, 10, 10), Rect::new(10, 20, 10, 10))]
|
||||
#[case::right(Rect::new(105, 20, 10, 10), Rect::new(100, 20, 10, 10))]
|
||||
#[case::down_left(Rect::new(5, 105, 10, 10), Rect::new(10, 100, 10, 10))]
|
||||
#[case::down(Rect::new(20, 105, 10, 10), Rect::new(20, 100, 10, 10))]
|
||||
#[case::down_right(Rect::new(105, 105, 10, 10), Rect::new(100, 100, 10, 10))]
|
||||
#[case::too_wide(Rect::new(5, 20, 200, 10), Rect::new(10, 20, 100, 10))]
|
||||
#[case::too_tall(Rect::new(20, 5, 10, 200), Rect::new(20, 10, 10, 100))]
|
||||
#[case::too_large(Rect::new(0, 0, 200, 200), Rect::new(10, 10, 100, 100))]
|
||||
fn clamp(#[case] rect: Rect, #[case] expected: Rect) {
|
||||
let other = Rect::new(10, 10, 100, 100);
|
||||
assert_eq!(rect.clamp(other), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rows() {
|
||||
let area = Rect::new(0, 0, 3, 2);
|
||||
let rows: Vec<Rect> = area.rows().collect();
|
||||
|
||||
let expected_rows: Vec<Rect> = vec![Rect::new(0, 0, 3, 1), Rect::new(0, 1, 3, 1)];
|
||||
|
||||
assert_eq!(rows, expected_rows);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn columns() {
|
||||
let area = Rect::new(0, 0, 3, 2);
|
||||
let columns: Vec<Rect> = area.columns().collect();
|
||||
|
||||
let expected_columns: Vec<Rect> = vec![
|
||||
Rect::new(0, 0, 1, 2),
|
||||
Rect::new(1, 0, 1, 2),
|
||||
Rect::new(2, 0, 1, 2),
|
||||
];
|
||||
|
||||
assert_eq!(columns, expected_columns);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn as_position() {
|
||||
let rect = Rect::new(1, 2, 3, 4);
|
||||
let position = rect.as_position();
|
||||
assert_eq!(position.x, 1);
|
||||
assert_eq!(position.y, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn as_size() {
|
||||
assert_eq!(
|
||||
Rect::new(1, 2, 3, 4).as_size(),
|
||||
Size {
|
||||
width: 3,
|
||||
height: 4
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_position_and_size() {
|
||||
let position = Position { x: 1, y: 2 };
|
||||
let size = Size {
|
||||
width: 3,
|
||||
height: 4,
|
||||
};
|
||||
assert_eq!(
|
||||
Rect::from((position, size)),
|
||||
Rect {
|
||||
x: 1,
|
||||
y: 2,
|
||||
width: 3,
|
||||
height: 4
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
143
ratatui-core/src/layout/rect/iter.rs
Normal file
143
ratatui-core/src/layout/rect/iter.rs
Normal file
@@ -0,0 +1,143 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
/// An iterator over rows within a `Rect`.
|
||||
pub struct Rows {
|
||||
/// The `Rect` associated with the rows.
|
||||
pub rect: Rect,
|
||||
/// The y coordinate of the row within the `Rect`.
|
||||
pub current_row: u16,
|
||||
}
|
||||
|
||||
impl Rows {
|
||||
/// Creates a new `Rows` iterator.
|
||||
pub const fn new(rect: Rect) -> Self {
|
||||
Self {
|
||||
rect,
|
||||
current_row: rect.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Rows {
|
||||
type Item = Rect;
|
||||
|
||||
/// Retrieves the next row within the `Rect`.
|
||||
///
|
||||
/// Returns `None` when there are no more rows to iterate through.
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.current_row >= self.rect.bottom() {
|
||||
return None;
|
||||
}
|
||||
let row = Rect::new(self.rect.x, self.current_row, self.rect.width, 1);
|
||||
self.current_row += 1;
|
||||
Some(row)
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over columns within a `Rect`.
|
||||
pub struct Columns {
|
||||
/// The `Rect` associated with the columns.
|
||||
pub rect: Rect,
|
||||
/// The x coordinate of the column within the `Rect`.
|
||||
pub current_column: u16,
|
||||
}
|
||||
|
||||
impl Columns {
|
||||
/// Creates a new `Columns` iterator.
|
||||
pub const fn new(rect: Rect) -> Self {
|
||||
Self {
|
||||
rect,
|
||||
current_column: rect.x,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Columns {
|
||||
type Item = Rect;
|
||||
|
||||
/// Retrieves the next column within the `Rect`.
|
||||
///
|
||||
/// Returns `None` when there are no more columns to iterate through.
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.current_column >= self.rect.right() {
|
||||
return None;
|
||||
}
|
||||
let column = Rect::new(self.current_column, self.rect.y, 1, self.rect.height);
|
||||
self.current_column += 1;
|
||||
Some(column)
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over positions within a `Rect`.
|
||||
///
|
||||
/// The iterator will yield all positions within the `Rect` in a row-major order.
|
||||
pub struct Positions {
|
||||
/// The `Rect` associated with the positions.
|
||||
pub rect: Rect,
|
||||
/// The current position within the `Rect`.
|
||||
pub current_position: Position,
|
||||
}
|
||||
|
||||
impl Positions {
|
||||
/// Creates a new `Positions` iterator.
|
||||
pub const fn new(rect: Rect) -> Self {
|
||||
Self {
|
||||
rect,
|
||||
current_position: Position::new(rect.x, rect.y),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Positions {
|
||||
type Item = Position;
|
||||
|
||||
/// Retrieves the next position within the `Rect`.
|
||||
///
|
||||
/// Returns `None` when there are no more positions to iterate through.
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.current_position.y >= self.rect.bottom() {
|
||||
return None;
|
||||
}
|
||||
let position = self.current_position;
|
||||
self.current_position.x += 1;
|
||||
if self.current_position.x >= self.rect.right() {
|
||||
self.current_position.x = self.rect.x;
|
||||
self.current_position.y += 1;
|
||||
}
|
||||
Some(position)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn rows() {
|
||||
let rect = Rect::new(0, 0, 2, 2);
|
||||
let mut rows = Rows::new(rect);
|
||||
assert_eq!(rows.next(), Some(Rect::new(0, 0, 2, 1)));
|
||||
assert_eq!(rows.next(), Some(Rect::new(0, 1, 2, 1)));
|
||||
assert_eq!(rows.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn columns() {
|
||||
let rect = Rect::new(0, 0, 2, 2);
|
||||
let mut columns = Columns::new(rect);
|
||||
assert_eq!(columns.next(), Some(Rect::new(0, 0, 1, 2)));
|
||||
assert_eq!(columns.next(), Some(Rect::new(1, 0, 1, 2)));
|
||||
assert_eq!(columns.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn positions() {
|
||||
let rect = Rect::new(0, 0, 2, 2);
|
||||
let mut positions = Positions::new(rect);
|
||||
assert_eq!(positions.next(), Some(Position::new(0, 0)));
|
||||
assert_eq!(positions.next(), Some(Position::new(1, 0)));
|
||||
assert_eq!(positions.next(), Some(Position::new(0, 1)));
|
||||
assert_eq!(positions.next(), Some(Position::new(1, 1)));
|
||||
assert_eq!(positions.next(), None);
|
||||
}
|
||||
}
|
||||
76
ratatui-core/src/layout/size.rs
Normal file
76
ratatui-core/src/layout/size.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
#![warn(missing_docs)]
|
||||
use std::fmt;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// A simple size struct
|
||||
///
|
||||
/// The width and height are stored as `u16` values and represent the number of columns and rows
|
||||
/// respectively.
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Size {
|
||||
/// The width in columns
|
||||
pub width: u16,
|
||||
/// The height in rows
|
||||
pub height: u16,
|
||||
}
|
||||
|
||||
impl Size {
|
||||
/// A zero sized Size
|
||||
pub const ZERO: Self = Self::new(0, 0);
|
||||
|
||||
/// Create a new `Size` struct
|
||||
pub const fn new(width: u16, height: u16) -> Self {
|
||||
Self { width, height }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(u16, u16)> for Size {
|
||||
fn from((width, height): (u16, u16)) -> Self {
|
||||
Self { width, height }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rect> for Size {
|
||||
fn from(rect: Rect) -> Self {
|
||||
rect.as_size()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Size {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}x{}", self.width, self.height)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn new() {
|
||||
let size = Size::new(10, 20);
|
||||
assert_eq!(size.width, 10);
|
||||
assert_eq!(size.height, 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_tuple() {
|
||||
let size = Size::from((10, 20));
|
||||
assert_eq!(size.width, 10);
|
||||
assert_eq!(size.height, 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_rect() {
|
||||
let size = Size::from(Rect::new(0, 0, 10, 20));
|
||||
assert_eq!(size.width, 10);
|
||||
assert_eq!(size.height, 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display() {
|
||||
assert_eq!(Size::new(10, 20).to_string(), "10x20");
|
||||
}
|
||||
}
|
||||
318
ratatui-core/src/lib.rs
Normal file
318
ratatui-core/src/lib.rs
Normal file
@@ -0,0 +1,318 @@
|
||||
//! 
|
||||
//!
|
||||
//! <div align="center">
|
||||
//!
|
||||
//! [![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![Deps.rs
|
||||
//! Badge]][Deps.rs]<br> [![Codecov Badge]][Codecov] [![License Badge]](./LICENSE) [![Sponsors
|
||||
//! Badge]][GitHub Sponsors]<br> [![Discord Badge]][Discord Server] [![Matrix Badge]][Matrix]
|
||||
//! [![Forum Badge]][Forum]<br>
|
||||
//!
|
||||
//! [Ratatui Website] · [API Docs] · [Examples] · [Changelog] · [Breaking Changes]<br>
|
||||
//! [Contributing] · [Report a bug] · [Request a Feature] · [Create a Pull Request]
|
||||
//!
|
||||
//! </div>
|
||||
//!
|
||||
//! # Ratatui
|
||||
//!
|
||||
//! [Ratatui][Ratatui Website] is a crate for cooking up terminal user interfaces in Rust. It is a
|
||||
//! lightweight library that provides a set of widgets and utilities to build complex Rust TUIs.
|
||||
//! Ratatui was forked from the [tui-rs] crate in 2023 in order to continue its development.
|
||||
//!
|
||||
//! ## Installation
|
||||
//!
|
||||
//! Add `ratatui` as a dependency to your cargo.toml:
|
||||
//!
|
||||
//! ```shell
|
||||
//! cargo add ratatui
|
||||
//! ```
|
||||
//!
|
||||
//! Ratatui uses [Crossterm] by default as it works on most platforms. See the [Installation]
|
||||
//! section of the [Ratatui Website] for more details on how to use other backends ([Termion] /
|
||||
//! [Termwiz]).
|
||||
//!
|
||||
//! ## Introduction
|
||||
//!
|
||||
//! Ratatui is based on the principle of immediate rendering with intermediate buffers. This means
|
||||
//! that for each frame, your app must render all widgets that are supposed to be part of the UI.
|
||||
//! This is in contrast to the retained mode style of rendering where widgets are updated and then
|
||||
//! automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Website]
|
||||
//! for more info.
|
||||
//!
|
||||
//! You can also watch the [FOSDEM 2024 talk] about Ratatui which gives a brief introduction to
|
||||
//! terminal user interfaces and showcases the features of Ratatui, along with a hello world demo.
|
||||
//!
|
||||
//! ## Other documentation
|
||||
//!
|
||||
//! - [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials
|
||||
//! - [Ratatui Forum][Forum] - a place to ask questions and discuss the library
|
||||
//! - [API Docs] - the full API documentation for the library on docs.rs.
|
||||
//! - [Examples] - a collection of examples that demonstrate how to use the library.
|
||||
//! - [Contributing] - Please read this if you are interested in contributing to the project.
|
||||
//! - [Changelog] - generated by [git-cliff] utilizing [Conventional Commits].
|
||||
//! - [Breaking Changes] - a list of breaking changes in the library.
|
||||
//!
|
||||
//! ## 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
|
||||
//! the [Examples] directory. For more guidance on different ways to structure your application see
|
||||
//! the [Application Patterns] and [Hello World tutorial] sections in the [Ratatui Website] and the
|
||||
//! various [Examples]. There are also several starter templates available in the [templates]
|
||||
//! repository.
|
||||
//!
|
||||
//! Every application built with `ratatui` needs to implement the following steps:
|
||||
//!
|
||||
//! - Initialize the terminal
|
||||
//! - A main loop to:
|
||||
//! - Handle input events
|
||||
//! - Draw the UI
|
||||
//! - Restore the terminal state
|
||||
//!
|
||||
//! The library contains a [`prelude`] module that re-exports the most commonly used traits and
|
||||
//! types for convenience. Most examples in the documentation will use this instead of showing the
|
||||
//! full path of each type.
|
||||
//!
|
||||
//! ### Initialize and restore the terminal
|
||||
//!
|
||||
//! The [`Terminal`] type is the main entry point for any Ratatui application. It is a light
|
||||
//! abstraction over a choice of [`Backend`] implementations that provides functionality to draw
|
||||
//! each frame, clear the screen, hide the cursor, etc. It is parametrized over any type that
|
||||
//! implements the [`Backend`] trait which has implementations for [Crossterm], [Termion] and
|
||||
//! [Termwiz].
|
||||
//!
|
||||
//! Most applications should enter the Alternate Screen when starting and leave it when exiting and
|
||||
//! also enable raw mode to disable line buffering and enable reading key events. See the [`backend`
|
||||
//! module] and the [Backends] section of the [Ratatui Website] for more info.
|
||||
//!
|
||||
//! ### Drawing the UI
|
||||
//!
|
||||
//! The drawing logic is delegated to a closure that takes a [`Frame`] instance as argument. The
|
||||
//! [`Frame`] provides the size of the area to draw to and allows the app to render any [`Widget`]
|
||||
//! using the provided [`render_widget`] method. After this closure returns, a diff is performed and
|
||||
//! only the changes are drawn to the terminal. See the [Widgets] section of the [Ratatui Website]
|
||||
//! for more info.
|
||||
//!
|
||||
//! ### Handling events
|
||||
//!
|
||||
//! Ratatui does not include any input handling. Instead event handling can be implemented by
|
||||
//! calling backend library methods directly. See the [Handling Events] section of the [Ratatui
|
||||
//! Website] for more info. For example, if you are using [Crossterm], you can use the
|
||||
//! [`crossterm::event`] module to handle events.
|
||||
//!
|
||||
//! ### Example
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use ratatui::{
|
||||
//! crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||
//! widgets::{Block, Paragraph},
|
||||
//! };
|
||||
//!
|
||||
//! fn main() -> std::io::Result<()> {
|
||||
//! let mut terminal = ratatui::init();
|
||||
//! loop {
|
||||
//! terminal.draw(|frame| {
|
||||
//! frame.render_widget(
|
||||
//! Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")),
|
||||
//! frame.area(),
|
||||
//! );
|
||||
//! })?;
|
||||
//! if let Event::Key(key) = event::read()? {
|
||||
//! if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
||||
//! break;
|
||||
//! }
|
||||
//! }
|
||||
//! }
|
||||
//! ratatui::restore();
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! Running this example produces the following output:
|
||||
//!
|
||||
//! ![docsrs-hello]
|
||||
//!
|
||||
//! ## Layout
|
||||
//!
|
||||
//! The library comes with a basic yet useful layout management object called [`Layout`] which
|
||||
//! allows you to split the available space into multiple areas and then render widgets in each
|
||||
//! area. This lets you describe a responsive terminal UI by nesting layouts. See the [Layout]
|
||||
//! section of the [Ratatui Website] for more info.
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use ratatui::{
|
||||
//! layout::{Constraint, Layout},
|
||||
//! widgets::Block,
|
||||
//! Frame,
|
||||
//! };
|
||||
//!
|
||||
//! fn draw(frame: &mut Frame) {
|
||||
//! let [title_area, main_area, status_area] = Layout::vertical([
|
||||
//! Constraint::Length(1),
|
||||
//! Constraint::Min(0),
|
||||
//! Constraint::Length(1),
|
||||
//! ])
|
||||
//! .areas(frame.area());
|
||||
//! let [left_area, right_area] =
|
||||
//! Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
//! .areas(main_area);
|
||||
//!
|
||||
//! frame.render_widget(Block::bordered().title("Title Bar"), title_area);
|
||||
//! frame.render_widget(Block::bordered().title("Status Bar"), status_area);
|
||||
//! frame.render_widget(Block::bordered().title("Left"), left_area);
|
||||
//! frame.render_widget(Block::bordered().title("Right"), right_area);
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! Running this example produces the following output:
|
||||
//!
|
||||
//! ![docsrs-layout]
|
||||
//!
|
||||
//! ## Text and styling
|
||||
//!
|
||||
//! The [`Text`], [`Line`] and [`Span`] types are the building blocks of the library and are used in
|
||||
//! many places. [`Text`] is a list of [`Line`]s and a [`Line`] is a list of [`Span`]s. A [`Span`]
|
||||
//! is a string with a specific style.
|
||||
//!
|
||||
//! The [`style` module] provides types that represent the various styling options. The most
|
||||
//! important one is [`Style`] which represents the foreground and background colors and the text
|
||||
//! attributes of a [`Span`]. The [`style` module] also provides a [`Stylize`] trait that allows
|
||||
//! short-hand syntax to apply a style to widgets and text. See the [Styling Text] section of the
|
||||
//! [Ratatui Website] for more info.
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use ratatui::{
|
||||
//! layout::{Constraint, Layout},
|
||||
//! style::{Color, Modifier, Style, Stylize},
|
||||
//! text::{Line, Span},
|
||||
//! widgets::{Block, Paragraph},
|
||||
//! Frame,
|
||||
//! };
|
||||
//!
|
||||
//! fn draw(frame: &mut Frame) {
|
||||
//! let areas = Layout::vertical([Constraint::Length(1); 4]).split(frame.area());
|
||||
//!
|
||||
//! let line = Line::from(vec![
|
||||
//! Span::raw("Hello "),
|
||||
//! Span::styled(
|
||||
//! "World",
|
||||
//! Style::new()
|
||||
//! .fg(Color::Green)
|
||||
//! .bg(Color::White)
|
||||
//! .add_modifier(Modifier::BOLD),
|
||||
//! ),
|
||||
//! "!".red().on_light_yellow().italic(),
|
||||
//! ]);
|
||||
//! frame.render_widget(line, areas[0]);
|
||||
//!
|
||||
//! // using the short-hand syntax and implicit conversions
|
||||
//! let paragraph = Paragraph::new("Hello World!".red().on_white().bold());
|
||||
//! frame.render_widget(paragraph, areas[1]);
|
||||
//!
|
||||
//! // style the whole widget instead of just the text
|
||||
//! let paragraph = Paragraph::new("Hello World!").style(Style::new().red().on_white());
|
||||
//! frame.render_widget(paragraph, areas[2]);
|
||||
//!
|
||||
//! // use the simpler short-hand syntax
|
||||
//! let paragraph = Paragraph::new("Hello World!").blue().on_yellow();
|
||||
//! frame.render_widget(paragraph, areas[3]);
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! Running this example produces the following output:
|
||||
//!
|
||||
//! ![docsrs-styling]
|
||||
#![cfg_attr(feature = "document-features", doc = "\n## Features")]
|
||||
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
|
||||
//!
|
||||
//! [Ratatui Website]: https://ratatui.rs/
|
||||
//! [Installation]: https://ratatui.rs/installation/
|
||||
//! [Rendering]: https://ratatui.rs/concepts/rendering/
|
||||
//! [Application Patterns]: https://ratatui.rs/concepts/application-patterns/
|
||||
//! [Hello World tutorial]: https://ratatui.rs/tutorials/hello-world/
|
||||
//! [Backends]: https://ratatui.rs/concepts/backends/
|
||||
//! [Widgets]: https://ratatui.rs/how-to/widgets/
|
||||
//! [Handling Events]: https://ratatui.rs/concepts/event-handling/
|
||||
//! [Layout]: https://ratatui.rs/how-to/layout/
|
||||
//! [Styling Text]: https://ratatui.rs/how-to/render/style-text/
|
||||
//! [templates]: https://github.com/ratatui/templates/
|
||||
//! [Examples]: https://github.com/ratatui/ratatui/tree/main/examples/README.md
|
||||
//! [Report a bug]: https://github.com/ratatui/ratatui/issues/new?labels=bug&projects=&template=bug_report.md
|
||||
//! [Request a Feature]: https://github.com/ratatui/ratatui/issues/new?labels=enhancement&projects=&template=feature_request.md
|
||||
//! [Create a Pull Request]: https://github.com/ratatui/ratatui/compare
|
||||
//! [git-cliff]: https://git-cliff.org
|
||||
//! [Conventional Commits]: https://www.conventionalcommits.org
|
||||
//! [API Docs]: https://docs.rs/ratatui
|
||||
//! [Changelog]: https://github.com/ratatui/ratatui/blob/main/CHANGELOG.md
|
||||
//! [Contributing]: https://github.com/ratatui/ratatui/blob/main/CONTRIBUTING.md
|
||||
//! [Breaking Changes]: https://github.com/ratatui/ratatui/blob/main/BREAKING-CHANGES.md
|
||||
//! [FOSDEM 2024 talk]: https://www.youtube.com/watch?v=NU0q6NOLJ20
|
||||
//! [docsrs-hello]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-hello.png?raw=true
|
||||
//! [docsrs-layout]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-layout.png?raw=true
|
||||
//! [docsrs-styling]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-styling.png?raw=true
|
||||
//! [`Frame`]: terminal::Frame
|
||||
//! [`render_widget`]: terminal::Frame::render_widget
|
||||
//! [`Widget`]: widgets::Widget
|
||||
//! [`Layout`]: layout::Layout
|
||||
//! [`Text`]: text::Text
|
||||
//! [`Line`]: text::Line
|
||||
//! [`Span`]: text::Span
|
||||
//! [`Style`]: style::Style
|
||||
//! [`style` module]: style
|
||||
//! [`Stylize`]: style::Stylize
|
||||
//! [`Backend`]: backend::Backend
|
||||
//! [`backend` module]: backend
|
||||
//! [`crossterm::event`]: https://docs.rs/crossterm/latest/crossterm/event/index.html
|
||||
//! [Crate]: https://crates.io/crates/ratatui
|
||||
//! [Crossterm]: https://crates.io/crates/crossterm
|
||||
//! [Termion]: https://crates.io/crates/termion
|
||||
//! [Termwiz]: https://crates.io/crates/termwiz
|
||||
//! [tui-rs]: https://crates.io/crates/tui
|
||||
//! [GitHub Sponsors]: https://github.com/sponsors/ratatui
|
||||
//! [Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square&logoColor=E05D44&color=E05D44
|
||||
//! [License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square&color=1370D3
|
||||
//! [CI Badge]: https://img.shields.io/github/actions/workflow/status/ratatui/ratatui/ci.yml?style=flat-square&logo=github
|
||||
//! [CI Workflow]: https://github.com/ratatui/ratatui/actions/workflows/ci.yml
|
||||
//! [Codecov Badge]: https://img.shields.io/codecov/c/github/ratatui/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST&color=C43AC3&logoColor=C43AC3
|
||||
//! [Codecov]: https://app.codecov.io/gh/ratatui/ratatui
|
||||
//! [Deps.rs Badge]: https://deps.rs/repo/github/ratatui/ratatui/status.svg?style=flat-square
|
||||
//! [Deps.rs]: https://deps.rs/repo/github/ratatui/ratatui
|
||||
//! [Discord Badge]: https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square&color=1370D3&logoColor=1370D3
|
||||
//! [Discord Server]: https://discord.gg/pMCEU9hNEj
|
||||
//! [Docs Badge]: https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square&logoColor=E05D44
|
||||
//! [Matrix Badge]: https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix&color=C43AC3
|
||||
//! [Matrix]: https://matrix.to/#/#ratatui:matrix.org
|
||||
//! [Forum Badge]: https://img.shields.io/discourse/likes?server=https%3A%2F%2Fforum.ratatui.rs&style=flat-square&logo=discourse&label=forum&color=C43AC3
|
||||
//! [Forum]: https://forum.ratatui.rs
|
||||
//! [Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui?logo=github&style=flat-square&color=1370D3
|
||||
|
||||
// show the feature flags in the generated documentation
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![doc(
|
||||
html_logo_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/logo.png",
|
||||
html_favicon_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/favicon.ico"
|
||||
)]
|
||||
|
||||
/// re-export the `crossterm` crate so that users don't have to add it as a dependency
|
||||
#[cfg(feature = "crossterm")]
|
||||
pub use crossterm;
|
||||
#[cfg(feature = "crossterm")]
|
||||
pub use terminal::{
|
||||
init, init_with_options, restore, try_init, try_init_with_options, try_restore, DefaultTerminal,
|
||||
};
|
||||
pub use terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, Viewport};
|
||||
/// re-export the `termion` crate so that users don't have to add it as a dependency
|
||||
#[cfg(all(not(windows), feature = "termion"))]
|
||||
pub use termion;
|
||||
/// re-export the `termwiz` crate so that users don't have to add it as a dependency
|
||||
#[cfg(feature = "termwiz")]
|
||||
pub use termwiz;
|
||||
|
||||
pub mod backend;
|
||||
pub mod buffer;
|
||||
pub mod layout;
|
||||
pub mod prelude;
|
||||
pub mod style;
|
||||
pub mod symbols;
|
||||
mod terminal;
|
||||
pub mod text;
|
||||
pub mod widgets;
|
||||
41
ratatui-core/src/prelude.rs
Normal file
41
ratatui-core/src/prelude.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
//! A prelude for conveniently writing applications using this library.
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use ratatui::prelude::*;
|
||||
//! ```
|
||||
//!
|
||||
//! Aside from the main types that are used in the library, this prelude also re-exports several
|
||||
//! modules to make it easy to qualify types that would otherwise collide. E.g.:
|
||||
//!
|
||||
//! ```rust
|
||||
//! use ratatui::{prelude::*, widgets::*};
|
||||
//!
|
||||
//! #[derive(Debug, Default, PartialEq, Eq)]
|
||||
//! struct Line;
|
||||
//!
|
||||
//! assert_eq!(Line::default(), Line);
|
||||
//! assert_eq!(text::Line::default(), ratatui::text::Line::from(vec![]));
|
||||
//! ```
|
||||
|
||||
// TODO: re-export the following modules:
|
||||
// #[cfg(feature = "crossterm")]
|
||||
// pub use crate::backend::CrosstermBackend;
|
||||
// #[cfg(all(not(windows), feature = "termion"))]
|
||||
// pub use crate::backend::TermionBackend;
|
||||
// #[cfg(feature = "termwiz")]
|
||||
// pub use crate::backend::TermwizBackend;
|
||||
pub(crate) use crate::widgets::{StatefulWidgetRef, WidgetRef};
|
||||
pub use crate::{
|
||||
backend::{self, Backend},
|
||||
buffer::{self, Buffer},
|
||||
layout::{self, Alignment, Constraint, Direction, Layout, Margin, Position, Rect, Size},
|
||||
style::{self, Color, Modifier, Style, Stylize},
|
||||
symbols::{self},
|
||||
text::{self, Line, Masked, Span, Text},
|
||||
widgets::{
|
||||
// block::BlockExt, // TODO
|
||||
StatefulWidget,
|
||||
Widget,
|
||||
},
|
||||
Frame, Terminal,
|
||||
};
|
||||
861
ratatui-core/src/style.rs
Normal file
861
ratatui-core/src/style.rs
Normal file
@@ -0,0 +1,861 @@
|
||||
//! `style` contains the primitives used to control how your user interface will look.
|
||||
//!
|
||||
//! There are two ways to set styles:
|
||||
//! - Creating and using the [`Style`] struct. (e.g. `Style::new().fg(Color::Red)`).
|
||||
//! - Using style shorthands. (e.g. `"hello".red()`).
|
||||
//!
|
||||
//! # Using the `Style` struct
|
||||
//!
|
||||
//! This is the original approach to styling and likely the most common. This is useful when
|
||||
//! creating style variables to reuse, however the shorthands are often more convenient and
|
||||
//! readable for most use cases.
|
||||
//!
|
||||
//! ## Example
|
||||
//!
|
||||
//! ```
|
||||
//! use ratatui::prelude::*;
|
||||
//!
|
||||
//! let heading_style = Style::new()
|
||||
//! .fg(Color::Black)
|
||||
//! .bg(Color::Green)
|
||||
//! .add_modifier(Modifier::ITALIC | Modifier::BOLD);
|
||||
//! let span = Span::styled("hello", heading_style);
|
||||
//! ```
|
||||
//!
|
||||
//! # Using style shorthands
|
||||
//!
|
||||
//! Originally Ratatui only had the ability to set styles using the `Style` struct. This is still
|
||||
//! supported, but there are now shorthands for all the styles that can be set. These save you from
|
||||
//! having to create a `Style` struct every time you want to set a style.
|
||||
//!
|
||||
//! The shorthands are implemented in the [`Stylize`] trait which is automatically implemented for
|
||||
//! many types via the [`Styled`] trait. This means that you can use the shorthands on any type
|
||||
//! that implements [`Styled`]. E.g.:
|
||||
//! - Strings and string slices when styled return a [`Span`]
|
||||
//! - [`Span`]s can be styled again, which will merge the styles.
|
||||
//! - Many widget types can be styled directly rather than calling their `style()` method.
|
||||
//!
|
||||
//! See the [`Stylize`] and [`Styled`] traits for more information. These traits are re-exported in
|
||||
//! the [`prelude`] module for convenience.
|
||||
//!
|
||||
//! ## Example
|
||||
//!
|
||||
//! ```
|
||||
//! use ratatui::{prelude::*, widgets::*};
|
||||
//!
|
||||
//! assert_eq!(
|
||||
//! "hello".red().on_blue().bold(),
|
||||
//! Span::styled(
|
||||
//! "hello",
|
||||
//! Style::default()
|
||||
//! .fg(Color::Red)
|
||||
//! .bg(Color::Blue)
|
||||
//! .add_modifier(Modifier::BOLD)
|
||||
//! )
|
||||
//! );
|
||||
//!
|
||||
//! assert_eq!(
|
||||
//! Paragraph::new("hello").red().on_blue().bold(),
|
||||
//! Paragraph::new("hello").style(
|
||||
//! Style::default()
|
||||
//! .fg(Color::Red)
|
||||
//! .bg(Color::Blue)
|
||||
//! .add_modifier(Modifier::BOLD)
|
||||
//! )
|
||||
//! );
|
||||
//! ```
|
||||
//!
|
||||
//! [`prelude`]: crate::prelude
|
||||
//! [`Span`]: crate::text::Span
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use bitflags::bitflags;
|
||||
pub use color::{Color, ParseColorError};
|
||||
use stylize::ColorDebugKind;
|
||||
pub use stylize::{Styled, Stylize};
|
||||
|
||||
mod color;
|
||||
pub mod palette;
|
||||
#[cfg(feature = "palette")]
|
||||
mod palette_conversion;
|
||||
mod stylize;
|
||||
|
||||
bitflags! {
|
||||
/// Modifier changes the way a piece of text is displayed.
|
||||
///
|
||||
/// They are bitflags so they can easily be composed.
|
||||
///
|
||||
/// `From<Modifier> for Style` is implemented so you can use `Modifier` anywhere that accepts
|
||||
/// `Into<Style>`.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{prelude::*};
|
||||
///
|
||||
/// let m = Modifier::BOLD | Modifier::ITALIC;
|
||||
/// ```
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// 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");
|
||||
}
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Style lets you control the main characteristics of the displayed elements.
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// Style::default()
|
||||
/// .fg(Color::Black)
|
||||
/// .bg(Color::Green)
|
||||
/// .add_modifier(Modifier::ITALIC | Modifier::BOLD);
|
||||
/// ```
|
||||
///
|
||||
/// Styles can also be created with a [shorthand notation](crate::style#using-style-shorthands).
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// Style::new().black().on_green().italic().bold();
|
||||
/// ```
|
||||
///
|
||||
/// For more information about the style shorthands, see the [`Stylize`] trait.
|
||||
///
|
||||
/// We implement conversions from [`Color`] and [`Modifier`] to [`Style`] so you can use them
|
||||
/// anywhere that accepts `Into<Style>`.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// Line::styled("hello", Style::new().fg(Color::Red));
|
||||
/// // simplifies to
|
||||
/// Line::styled("hello", Color::Red);
|
||||
///
|
||||
/// Line::styled("hello", Style::new().add_modifier(Modifier::BOLD));
|
||||
/// // simplifies to
|
||||
/// Line::styled("hello", Modifier::BOLD);
|
||||
/// ```
|
||||
///
|
||||
/// Styles 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::prelude::*;
|
||||
///
|
||||
/// let styles = [
|
||||
/// Style::default()
|
||||
/// .fg(Color::Blue)
|
||||
/// .add_modifier(Modifier::BOLD | Modifier::ITALIC),
|
||||
/// Style::default()
|
||||
/// .bg(Color::Red)
|
||||
/// .add_modifier(Modifier::UNDERLINED),
|
||||
/// #[cfg(feature = "underline-color")]
|
||||
/// Style::default().underline_color(Color::Green),
|
||||
/// 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[(0, 0)].set_style(*style);
|
||||
/// }
|
||||
/// assert_eq!(
|
||||
/// Style {
|
||||
/// fg: Some(Color::Yellow),
|
||||
/// bg: Some(Color::Red),
|
||||
/// #[cfg(feature = "underline-color")]
|
||||
/// underline_color: Some(Color::Green),
|
||||
/// add_modifier: Modifier::BOLD | Modifier::UNDERLINED,
|
||||
/// sub_modifier: Modifier::empty(),
|
||||
/// },
|
||||
/// buffer[(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::prelude::*;
|
||||
///
|
||||
/// 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[(0, 0)].set_style(*style);
|
||||
/// }
|
||||
/// assert_eq!(
|
||||
/// Style {
|
||||
/// fg: Some(Color::Yellow),
|
||||
/// bg: Some(Color::Reset),
|
||||
/// #[cfg(feature = "underline-color")]
|
||||
/// underline_color: Some(Color::Reset),
|
||||
/// add_modifier: Modifier::empty(),
|
||||
/// sub_modifier: Modifier::empty(),
|
||||
/// },
|
||||
/// buffer[(0, 0)].style(),
|
||||
/// );
|
||||
/// ```
|
||||
#[derive(Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Style {
|
||||
pub fg: Option<Color>,
|
||||
pub bg: Option<Color>,
|
||||
#[cfg(feature = "underline-color")]
|
||||
pub underline_color: Option<Color>,
|
||||
pub add_modifier: Modifier,
|
||||
pub sub_modifier: Modifier,
|
||||
}
|
||||
|
||||
/// A custom debug implementation that prints only the fields that are not the default, and unwraps
|
||||
/// the `Option`s.
|
||||
impl fmt::Debug for Style {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str("Style::new()")?;
|
||||
if let Some(fg) = self.fg {
|
||||
fg.stylize_debug(ColorDebugKind::Foreground).fmt(f)?;
|
||||
}
|
||||
if let Some(bg) = self.bg {
|
||||
bg.stylize_debug(ColorDebugKind::Background).fmt(f)?;
|
||||
}
|
||||
#[cfg(feature = "underline-color")]
|
||||
if let Some(underline_color) = self.underline_color {
|
||||
underline_color
|
||||
.stylize_debug(ColorDebugKind::Underline)
|
||||
.fmt(f)?;
|
||||
}
|
||||
for modifier in self.add_modifier.iter() {
|
||||
match modifier {
|
||||
Modifier::BOLD => f.write_str(".bold()")?,
|
||||
Modifier::DIM => f.write_str(".dim()")?,
|
||||
Modifier::ITALIC => f.write_str(".italic()")?,
|
||||
Modifier::UNDERLINED => f.write_str(".underlined()")?,
|
||||
Modifier::SLOW_BLINK => f.write_str(".slow_blink()")?,
|
||||
Modifier::RAPID_BLINK => f.write_str(".rapid_blink()")?,
|
||||
Modifier::REVERSED => f.write_str(".reversed()")?,
|
||||
Modifier::HIDDEN => f.write_str(".hidden()")?,
|
||||
Modifier::CROSSED_OUT => f.write_str(".crossed_out()")?,
|
||||
_ => f.write_fmt(format_args!(".add_modifier(Modifier::{modifier:?})"))?,
|
||||
}
|
||||
}
|
||||
for modifier in self.sub_modifier.iter() {
|
||||
match modifier {
|
||||
Modifier::BOLD => f.write_str(".not_bold()")?,
|
||||
Modifier::DIM => f.write_str(".not_dim()")?,
|
||||
Modifier::ITALIC => f.write_str(".not_italic()")?,
|
||||
Modifier::UNDERLINED => f.write_str(".not_underlined()")?,
|
||||
Modifier::SLOW_BLINK => f.write_str(".not_slow_blink()")?,
|
||||
Modifier::RAPID_BLINK => f.write_str(".not_rapid_blink()")?,
|
||||
Modifier::REVERSED => f.write_str(".not_reversed()")?,
|
||||
Modifier::HIDDEN => f.write_str(".not_hidden()")?,
|
||||
Modifier::CROSSED_OUT => f.write_str(".not_crossed_out()")?,
|
||||
_ => f.write_fmt(format_args!(".remove_modifier(Modifier::{modifier:?})"))?,
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for Style {
|
||||
type Item = Self;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
*self
|
||||
}
|
||||
|
||||
fn set_style<S: Into<Self>>(self, style: S) -> Self::Item {
|
||||
self.patch(style)
|
||||
}
|
||||
}
|
||||
|
||||
impl Style {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
fg: None,
|
||||
bg: None,
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: None,
|
||||
add_modifier: Modifier::empty(),
|
||||
sub_modifier: Modifier::empty(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a `Style` resetting all properties.
|
||||
pub const fn reset() -> Self {
|
||||
Self {
|
||||
fg: Some(Color::Reset),
|
||||
bg: Some(Color::Reset),
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: Some(Color::Reset),
|
||||
add_modifier: Modifier::empty(),
|
||||
sub_modifier: Modifier::all(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Changes the foreground color.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let style = Style::default().fg(Color::Blue);
|
||||
/// let diff = Style::default().fg(Color::Red);
|
||||
/// assert_eq!(style.patch(diff), Style::default().fg(Color::Red));
|
||||
/// ```
|
||||
#[must_use = "`fg` returns the modified style without modifying the original"]
|
||||
pub const fn fg(mut self, color: Color) -> Self {
|
||||
self.fg = Some(color);
|
||||
self
|
||||
}
|
||||
|
||||
/// Changes the background color.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let style = Style::default().bg(Color::Blue);
|
||||
/// let diff = Style::default().bg(Color::Red);
|
||||
/// assert_eq!(style.patch(diff), Style::default().bg(Color::Red));
|
||||
/// ```
|
||||
#[must_use = "`bg` returns the modified style without modifying the original"]
|
||||
pub const fn bg(mut self, color: Color) -> Self {
|
||||
self.bg = Some(color);
|
||||
self
|
||||
}
|
||||
|
||||
/// Changes the underline color. The text must be underlined with a modifier for this to work.
|
||||
///
|
||||
/// This uses a non-standard ANSI escape sequence. It is supported by most terminal emulators,
|
||||
/// but is only implemented in the crossterm backend and enabled by the `underline-color`
|
||||
/// feature flag.
|
||||
///
|
||||
/// See
|
||||
/// [Wikipedia](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters)
|
||||
/// code `58` and `59` for more information.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let style = Style::default()
|
||||
/// .underline_color(Color::Blue)
|
||||
/// .add_modifier(Modifier::UNDERLINED);
|
||||
/// let diff = Style::default()
|
||||
/// .underline_color(Color::Red)
|
||||
/// .add_modifier(Modifier::UNDERLINED);
|
||||
/// assert_eq!(
|
||||
/// style.patch(diff),
|
||||
/// Style::default()
|
||||
/// .underline_color(Color::Red)
|
||||
/// .add_modifier(Modifier::UNDERLINED)
|
||||
/// );
|
||||
/// ```
|
||||
#[cfg(feature = "underline-color")]
|
||||
#[must_use = "`underline_color` returns the modified style without modifying the original"]
|
||||
pub const fn underline_color(mut self, color: Color) -> Self {
|
||||
self.underline_color = Some(color);
|
||||
self
|
||||
}
|
||||
|
||||
/// Changes the text emphasis.
|
||||
///
|
||||
/// When applied, it adds the given modifier to the `Style` modifiers.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// 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());
|
||||
/// ```
|
||||
#[must_use = "`add_modifier` returns the modified style without modifying the original"]
|
||||
pub const fn add_modifier(mut self, modifier: Modifier) -> Self {
|
||||
self.sub_modifier = self.sub_modifier.difference(modifier);
|
||||
self.add_modifier = self.add_modifier.union(modifier);
|
||||
self
|
||||
}
|
||||
|
||||
/// Changes the text emphasis.
|
||||
///
|
||||
/// When applied, it removes the given modifier from the `Style` modifiers.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// 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);
|
||||
/// ```
|
||||
#[must_use = "`remove_modifier` returns the modified style without modifying the original"]
|
||||
pub const fn remove_modifier(mut self, modifier: Modifier) -> Self {
|
||||
self.add_modifier = self.add_modifier.difference(modifier);
|
||||
self.sub_modifier = self.sub_modifier.union(modifier);
|
||||
self
|
||||
}
|
||||
|
||||
/// Results in a combined style that is equivalent to applying the two individual styles to
|
||||
/// a style one after the other.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```
|
||||
/// # use ratatui::prelude::*;
|
||||
/// 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)
|
||||
/// );
|
||||
/// ```
|
||||
#[must_use = "`patch` returns the modified style without modifying the original"]
|
||||
pub fn patch<S: Into<Self>>(mut self, other: S) -> Self {
|
||||
let other = other.into();
|
||||
self.fg = other.fg.or(self.fg);
|
||||
self.bg = other.bg.or(self.bg);
|
||||
|
||||
#[cfg(feature = "underline-color")]
|
||||
{
|
||||
self.underline_color = other.underline_color.or(self.underline_color);
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for Style {
|
||||
/// Creates a new `Style` with the given foreground color.
|
||||
///
|
||||
/// To specify a foreground and background color, use the `from((fg, bg))` constructor.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let style = Style::from(Color::Red);
|
||||
/// ```
|
||||
fn from(color: Color) -> Self {
|
||||
Self::new().fg(color)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(Color, Color)> for Style {
|
||||
/// Creates a new `Style` with the given foreground and background colors.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// // red foreground, blue background
|
||||
/// let style = Style::from((Color::Red, Color::Blue));
|
||||
/// // default foreground, blue background
|
||||
/// let style = Style::from((Color::Reset, Color::Blue));
|
||||
/// ```
|
||||
fn from((fg, bg): (Color, Color)) -> Self {
|
||||
Self::new().fg(fg).bg(bg)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Modifier> for Style {
|
||||
/// Creates a new `Style` with the given modifier added.
|
||||
///
|
||||
/// To specify multiple modifiers, use the `|` operator.
|
||||
///
|
||||
/// To specify modifiers to add and remove, use the `from((add_modifier, sub_modifier))`
|
||||
/// constructor.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// // add bold and italic
|
||||
/// let style = Style::from(Modifier::BOLD|Modifier::ITALIC);
|
||||
fn from(modifier: Modifier) -> Self {
|
||||
Self::new().add_modifier(modifier)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(Modifier, Modifier)> for Style {
|
||||
/// Creates a new `Style` with the given modifiers added and removed.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// // add bold and italic, remove dim
|
||||
/// let style = Style::from((Modifier::BOLD | Modifier::ITALIC, Modifier::DIM));
|
||||
/// ```
|
||||
fn from((add_modifier, sub_modifier): (Modifier, Modifier)) -> Self {
|
||||
Self::new()
|
||||
.add_modifier(add_modifier)
|
||||
.remove_modifier(sub_modifier)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(Color, Modifier)> for Style {
|
||||
/// Creates a new `Style` with the given foreground color and modifier added.
|
||||
///
|
||||
/// To specify multiple modifiers, use the `|` operator.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// // red foreground, add bold and italic
|
||||
/// let style = Style::from((Color::Red, Modifier::BOLD | Modifier::ITALIC));
|
||||
/// ```
|
||||
fn from((fg, modifier): (Color, Modifier)) -> Self {
|
||||
Self::new().fg(fg).add_modifier(modifier)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(Color, Color, Modifier)> for Style {
|
||||
/// Creates a new `Style` with the given foreground and background colors and modifier added.
|
||||
///
|
||||
/// To specify multiple modifiers, use the `|` operator.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// // red foreground, blue background, add bold and italic
|
||||
/// let style = Style::from((Color::Red, Color::Blue, Modifier::BOLD | Modifier::ITALIC));
|
||||
/// ```
|
||||
fn from((fg, bg, modifier): (Color, Color, Modifier)) -> Self {
|
||||
Self::new().fg(fg).bg(bg).add_modifier(modifier)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(Color, Color, Modifier, Modifier)> for Style {
|
||||
/// Creates a new `Style` with the given foreground and background colors and modifiers added
|
||||
/// and removed.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// // red foreground, blue background, add bold and italic, remove dim
|
||||
/// let style = Style::from((
|
||||
/// Color::Red,
|
||||
/// Color::Blue,
|
||||
/// Modifier::BOLD | Modifier::ITALIC,
|
||||
/// Modifier::DIM,
|
||||
/// ));
|
||||
/// ```
|
||||
fn from((fg, bg, add_modifier, sub_modifier): (Color, Color, Modifier, Modifier)) -> Self {
|
||||
Self::new()
|
||||
.fg(fg)
|
||||
.bg(bg)
|
||||
.add_modifier(add_modifier)
|
||||
.remove_modifier(sub_modifier)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[rstest]
|
||||
#[case(Style::new(), "Style::new()")]
|
||||
#[case(Style::new().red(), "Style::new().red()")]
|
||||
#[case(Style::new().on_blue(), "Style::new().on_blue()")]
|
||||
#[case(Style::new().bold(), "Style::new().bold()")]
|
||||
#[case(Style::new().not_italic(), "Style::new().not_italic()")]
|
||||
fn debug(#[case] style: Style, #[case] expected: &'static str) {
|
||||
assert_eq!(format!("{style:?}"), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combined_patch_gives_same_result_as_individual_patch() {
|
||||
let styles = [
|
||||
Style::new(),
|
||||
Style::new().fg(Color::Yellow),
|
||||
Style::new().bg(Color::Yellow),
|
||||
Style::new().add_modifier(Modifier::BOLD),
|
||||
Style::new().remove_modifier(Modifier::BOLD),
|
||||
Style::new().add_modifier(Modifier::ITALIC),
|
||||
Style::new().remove_modifier(Modifier::ITALIC),
|
||||
Style::new().add_modifier(Modifier::ITALIC | Modifier::BOLD),
|
||||
Style::new().remove_modifier(Modifier::ITALIC | Modifier::BOLD),
|
||||
];
|
||||
for &a in &styles {
|
||||
for &b in &styles {
|
||||
for &c in &styles {
|
||||
for &d in &styles {
|
||||
assert_eq!(
|
||||
Style::new().patch(a).patch(b).patch(c).patch(d),
|
||||
Style::new().patch(a.patch(b.patch(c.patch(d))))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combine_individual_modifiers() {
|
||||
use crate::{buffer::Buffer, layout::Rect};
|
||||
|
||||
let mods = [
|
||||
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[(0, 0)].set_style(Style::reset());
|
||||
buffer[(0, 0)].set_style(Style::new().add_modifier(m));
|
||||
let style = buffer[(0, 0)].style();
|
||||
assert!(style.add_modifier.contains(m));
|
||||
assert!(!style.sub_modifier.contains(m));
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(Modifier::empty(), "NONE")]
|
||||
#[case(Modifier::BOLD, "BOLD")]
|
||||
#[case(Modifier::DIM, "DIM")]
|
||||
#[case(Modifier::ITALIC, "ITALIC")]
|
||||
#[case(Modifier::UNDERLINED, "UNDERLINED")]
|
||||
#[case(Modifier::SLOW_BLINK, "SLOW_BLINK")]
|
||||
#[case(Modifier::RAPID_BLINK, "RAPID_BLINK")]
|
||||
#[case(Modifier::REVERSED, "REVERSED")]
|
||||
#[case(Modifier::HIDDEN, "HIDDEN")]
|
||||
#[case(Modifier::CROSSED_OUT, "CROSSED_OUT")]
|
||||
#[case(Modifier::BOLD | Modifier::DIM, "BOLD | DIM")]
|
||||
#[case(Modifier::all(), "BOLD | DIM | ITALIC | UNDERLINED | SLOW_BLINK | RAPID_BLINK | REVERSED | HIDDEN | CROSSED_OUT")]
|
||||
fn modifier_debug(#[case] modifier: Modifier, #[case] expected: &str) {
|
||||
assert_eq!(format!("{modifier:?}"), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn style_can_be_const() {
|
||||
const RED: Color = Color::Red;
|
||||
const BLACK: Color = Color::Black;
|
||||
const BOLD: Modifier = Modifier::BOLD;
|
||||
const ITALIC: Modifier = Modifier::ITALIC;
|
||||
|
||||
const _RESET: Style = Style::reset();
|
||||
const _RED_FG: Style = Style::new().fg(RED);
|
||||
const _BLACK_BG: Style = Style::new().bg(BLACK);
|
||||
const _ADD_BOLD: Style = Style::new().add_modifier(BOLD);
|
||||
const _REMOVE_ITALIC: Style = Style::new().remove_modifier(ITALIC);
|
||||
const ALL: Style = Style::new()
|
||||
.fg(RED)
|
||||
.bg(BLACK)
|
||||
.add_modifier(BOLD)
|
||||
.remove_modifier(ITALIC);
|
||||
assert_eq!(
|
||||
ALL,
|
||||
Style::new()
|
||||
.fg(Color::Red)
|
||||
.bg(Color::Black)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.remove_modifier(Modifier::ITALIC)
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(Style::new().black(), Color::Black)]
|
||||
#[case(Style::new().red(), Color::Red)]
|
||||
#[case(Style::new().green(), Color::Green)]
|
||||
#[case(Style::new().yellow(), Color::Yellow)]
|
||||
#[case(Style::new().blue(), Color::Blue)]
|
||||
#[case(Style::new().magenta(), Color::Magenta)]
|
||||
#[case(Style::new().cyan(), Color::Cyan)]
|
||||
#[case(Style::new().white(), Color::White)]
|
||||
#[case(Style::new().gray(), Color::Gray)]
|
||||
#[case(Style::new().dark_gray(), Color::DarkGray)]
|
||||
#[case(Style::new().light_red(), Color::LightRed)]
|
||||
#[case(Style::new().light_green(), Color::LightGreen)]
|
||||
#[case(Style::new().light_yellow(), Color::LightYellow)]
|
||||
#[case(Style::new().light_blue(), Color::LightBlue)]
|
||||
#[case(Style::new().light_magenta(), Color::LightMagenta)]
|
||||
#[case(Style::new().light_cyan(), Color::LightCyan)]
|
||||
#[case(Style::new().white(), Color::White)]
|
||||
fn fg_can_be_stylized(#[case] stylized: Style, #[case] expected: Color) {
|
||||
assert_eq!(stylized, Style::new().fg(expected));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(Style::new().on_black(), Color::Black)]
|
||||
#[case(Style::new().on_red(), Color::Red)]
|
||||
#[case(Style::new().on_green(), Color::Green)]
|
||||
#[case(Style::new().on_yellow(), Color::Yellow)]
|
||||
#[case(Style::new().on_blue(), Color::Blue)]
|
||||
#[case(Style::new().on_magenta(), Color::Magenta)]
|
||||
#[case(Style::new().on_cyan(), Color::Cyan)]
|
||||
#[case(Style::new().on_white(), Color::White)]
|
||||
#[case(Style::new().on_gray(), Color::Gray)]
|
||||
#[case(Style::new().on_dark_gray(), Color::DarkGray)]
|
||||
#[case(Style::new().on_light_red(), Color::LightRed)]
|
||||
#[case(Style::new().on_light_green(), Color::LightGreen)]
|
||||
#[case(Style::new().on_light_yellow(), Color::LightYellow)]
|
||||
#[case(Style::new().on_light_blue(), Color::LightBlue)]
|
||||
#[case(Style::new().on_light_magenta(), Color::LightMagenta)]
|
||||
#[case(Style::new().on_light_cyan(), Color::LightCyan)]
|
||||
#[case(Style::new().on_white(), Color::White)]
|
||||
fn bg_can_be_stylized(#[case] stylized: Style, #[case] expected: Color) {
|
||||
assert_eq!(stylized, Style::new().bg(expected));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(Style::new().bold(), Modifier::BOLD)]
|
||||
#[case(Style::new().dim(), Modifier::DIM)]
|
||||
#[case(Style::new().italic(), Modifier::ITALIC)]
|
||||
#[case(Style::new().underlined(), Modifier::UNDERLINED)]
|
||||
#[case(Style::new().slow_blink(), Modifier::SLOW_BLINK)]
|
||||
#[case(Style::new().rapid_blink(), Modifier::RAPID_BLINK)]
|
||||
#[case(Style::new().reversed(), Modifier::REVERSED)]
|
||||
#[case(Style::new().hidden(), Modifier::HIDDEN)]
|
||||
#[case(Style::new().crossed_out(), Modifier::CROSSED_OUT)]
|
||||
fn add_modifier_can_be_stylized(#[case] stylized: Style, #[case] expected: Modifier) {
|
||||
assert_eq!(stylized, Style::new().add_modifier(expected));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(Style::new().not_bold(), Modifier::BOLD)]
|
||||
#[case(Style::new().not_dim(), Modifier::DIM)]
|
||||
#[case(Style::new().not_italic(), Modifier::ITALIC)]
|
||||
#[case(Style::new().not_underlined(), Modifier::UNDERLINED)]
|
||||
#[case(Style::new().not_slow_blink(), Modifier::SLOW_BLINK)]
|
||||
#[case(Style::new().not_rapid_blink(), Modifier::RAPID_BLINK)]
|
||||
#[case(Style::new().not_reversed(), Modifier::REVERSED)]
|
||||
#[case(Style::new().not_hidden(), Modifier::HIDDEN)]
|
||||
#[case(Style::new().not_crossed_out(), Modifier::CROSSED_OUT)]
|
||||
fn remove_modifier_can_be_stylized(#[case] stylized: Style, #[case] expected: Modifier) {
|
||||
assert_eq!(stylized, Style::new().remove_modifier(expected));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_can_be_stylized() {
|
||||
assert_eq!(Style::new().reset(), Style::reset());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_color() {
|
||||
assert_eq!(Style::from(Color::Red), Style::new().fg(Color::Red));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_color_color() {
|
||||
assert_eq!(
|
||||
Style::from((Color::Red, Color::Blue)),
|
||||
Style::new().fg(Color::Red).bg(Color::Blue)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_modifier() {
|
||||
assert_eq!(
|
||||
Style::from(Modifier::BOLD | Modifier::ITALIC),
|
||||
Style::new()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::ITALIC)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_modifier_modifier() {
|
||||
assert_eq!(
|
||||
Style::from((Modifier::BOLD | Modifier::ITALIC, Modifier::DIM)),
|
||||
Style::new()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::ITALIC)
|
||||
.remove_modifier(Modifier::DIM)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_color_modifier() {
|
||||
assert_eq!(
|
||||
Style::from((Color::Red, Modifier::BOLD | Modifier::ITALIC)),
|
||||
Style::new()
|
||||
.fg(Color::Red)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::ITALIC)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_color_color_modifier() {
|
||||
assert_eq!(
|
||||
Style::from((Color::Red, Color::Blue, Modifier::BOLD | Modifier::ITALIC)),
|
||||
Style::new()
|
||||
.fg(Color::Red)
|
||||
.bg(Color::Blue)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::ITALIC)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_color_color_modifier_modifier() {
|
||||
assert_eq!(
|
||||
Style::from((
|
||||
Color::Red,
|
||||
Color::Blue,
|
||||
Modifier::BOLD | Modifier::ITALIC,
|
||||
Modifier::DIM
|
||||
)),
|
||||
Style::new()
|
||||
.fg(Color::Red)
|
||||
.bg(Color::Blue)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::ITALIC)
|
||||
.remove_modifier(Modifier::DIM)
|
||||
);
|
||||
}
|
||||
}
|
||||
712
ratatui-core/src/style/color.rs
Normal file
712
ratatui-core/src/style/color.rs
Normal file
@@ -0,0 +1,712 @@
|
||||
#![allow(clippy::unreadable_literal)]
|
||||
|
||||
use std::{fmt, str::FromStr};
|
||||
|
||||
use crate::style::stylize::{ColorDebug, ColorDebugKind};
|
||||
|
||||
/// ANSI Color
|
||||
///
|
||||
/// All colors from the [ANSI color table] are supported (though some names are not exactly the
|
||||
/// same).
|
||||
///
|
||||
/// | Color Name | Color | Foreground | Background |
|
||||
/// |----------------|-------------------------|------------|------------|
|
||||
/// | `black` | [`Color::Black`] | 30 | 40 |
|
||||
/// | `red` | [`Color::Red`] | 31 | 41 |
|
||||
/// | `green` | [`Color::Green`] | 32 | 42 |
|
||||
/// | `yellow` | [`Color::Yellow`] | 33 | 43 |
|
||||
/// | `blue` | [`Color::Blue`] | 34 | 44 |
|
||||
/// | `magenta` | [`Color::Magenta`] | 35 | 45 |
|
||||
/// | `cyan` | [`Color::Cyan`] | 36 | 46 |
|
||||
/// | `gray`* | [`Color::Gray`] | 37 | 47 |
|
||||
/// | `darkgray`* | [`Color::DarkGray`] | 90 | 100 |
|
||||
/// | `lightred` | [`Color::LightRed`] | 91 | 101 |
|
||||
/// | `lightgreen` | [`Color::LightGreen`] | 92 | 102 |
|
||||
/// | `lightyellow` | [`Color::LightYellow`] | 93 | 103 |
|
||||
/// | `lightblue` | [`Color::LightBlue`] | 94 | 104 |
|
||||
/// | `lightmagenta` | [`Color::LightMagenta`] | 95 | 105 |
|
||||
/// | `lightcyan` | [`Color::LightCyan`] | 96 | 106 |
|
||||
/// | `white`* | [`Color::White`] | 97 | 107 |
|
||||
///
|
||||
/// - `gray` is sometimes called `white` - this is not supported as we use `white` for bright white
|
||||
/// - `gray` is sometimes called `silver` - this is supported
|
||||
/// - `darkgray` is sometimes called `light black` or `bright black` (both are supported)
|
||||
/// - `white` is sometimes called `light white` or `bright white` (both are supported)
|
||||
/// - we support `bright` and `light` prefixes for all colors
|
||||
/// - we support `-` and `_` and ` ` as separators for all colors
|
||||
/// - we support both `gray` and `grey` spellings
|
||||
///
|
||||
/// `From<Color> for Style` is implemented by creating a style with the foreground color set to the
|
||||
/// given color. This allows you to use colors anywhere that accepts `Into<Style>`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use std::str::FromStr;
|
||||
///
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// assert_eq!(Color::from_str("red"), Ok(Color::Red));
|
||||
/// assert_eq!("red".parse(), Ok(Color::Red));
|
||||
/// assert_eq!("lightred".parse(), Ok(Color::LightRed));
|
||||
/// assert_eq!("light red".parse(), Ok(Color::LightRed));
|
||||
/// assert_eq!("light-red".parse(), Ok(Color::LightRed));
|
||||
/// assert_eq!("light_red".parse(), Ok(Color::LightRed));
|
||||
/// assert_eq!("lightRed".parse(), Ok(Color::LightRed));
|
||||
/// assert_eq!("bright red".parse(), Ok(Color::LightRed));
|
||||
/// assert_eq!("bright-red".parse(), Ok(Color::LightRed));
|
||||
/// assert_eq!("silver".parse(), Ok(Color::Gray));
|
||||
/// assert_eq!("dark-grey".parse(), Ok(Color::DarkGray));
|
||||
/// assert_eq!("dark gray".parse(), Ok(Color::DarkGray));
|
||||
/// assert_eq!("light-black".parse(), Ok(Color::DarkGray));
|
||||
/// assert_eq!("white".parse(), Ok(Color::White));
|
||||
/// assert_eq!("bright white".parse(), Ok(Color::White));
|
||||
/// ```
|
||||
///
|
||||
/// [ANSI color table]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum Color {
|
||||
/// Resets the foreground or background color
|
||||
#[default]
|
||||
Reset,
|
||||
/// ANSI Color: Black. Foreground: 30, Background: 40
|
||||
Black,
|
||||
/// ANSI Color: Red. Foreground: 31, Background: 41
|
||||
Red,
|
||||
/// ANSI Color: Green. Foreground: 32, Background: 42
|
||||
Green,
|
||||
/// ANSI Color: Yellow. Foreground: 33, Background: 43
|
||||
Yellow,
|
||||
/// ANSI Color: Blue. Foreground: 34, Background: 44
|
||||
Blue,
|
||||
/// ANSI Color: Magenta. Foreground: 35, Background: 45
|
||||
Magenta,
|
||||
/// ANSI Color: Cyan. Foreground: 36, Background: 46
|
||||
Cyan,
|
||||
/// ANSI Color: White. Foreground: 37, Background: 47
|
||||
///
|
||||
/// Note that this is sometimes called `silver` or `white` but we use `white` for bright white
|
||||
Gray,
|
||||
/// ANSI Color: Bright Black. Foreground: 90, Background: 100
|
||||
///
|
||||
/// Note that this is sometimes called `light black` or `bright black` but we use `dark gray`
|
||||
DarkGray,
|
||||
/// ANSI Color: Bright Red. Foreground: 91, Background: 101
|
||||
LightRed,
|
||||
/// ANSI Color: Bright Green. Foreground: 92, Background: 102
|
||||
LightGreen,
|
||||
/// ANSI Color: Bright Yellow. Foreground: 93, Background: 103
|
||||
LightYellow,
|
||||
/// ANSI Color: Bright Blue. Foreground: 94, Background: 104
|
||||
LightBlue,
|
||||
/// ANSI Color: Bright Magenta. Foreground: 95, Background: 105
|
||||
LightMagenta,
|
||||
/// ANSI Color: Bright Cyan. Foreground: 96, Background: 106
|
||||
LightCyan,
|
||||
/// ANSI Color: Bright White. Foreground: 97, Background: 107
|
||||
/// Sometimes called `bright white` or `light white` in some terminals
|
||||
White,
|
||||
/// An RGB color.
|
||||
///
|
||||
/// Note that only terminals that support 24-bit true color will display this correctly.
|
||||
/// Notably versions of Windows Terminal prior to Windows 10 and macOS Terminal.app do not
|
||||
/// support this.
|
||||
///
|
||||
/// If the terminal does not support true color, code using the [`TermwizBackend`] will
|
||||
/// fallback to the default text color. Crossterm and Termion do not have this capability and
|
||||
/// the display will be unpredictable (e.g. Terminal.app may display glitched blinking text).
|
||||
/// See <https://github.com/ratatui/ratatui/issues/475> for an example of this problem.
|
||||
///
|
||||
/// See also: <https://en.wikipedia.org/wiki/ANSI_escape_code#24-bit>
|
||||
///
|
||||
/// [`TermwizBackend`]: crate::backend::TermwizBackend
|
||||
Rgb(u8, u8, u8),
|
||||
/// An 8-bit 256 color.
|
||||
///
|
||||
/// See also <https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit>
|
||||
Indexed(u8),
|
||||
}
|
||||
|
||||
impl Color {
|
||||
/// Convert a u32 to a Color
|
||||
///
|
||||
/// The u32 should be in the format 0x00RRGGBB.
|
||||
pub const fn from_u32(u: u32) -> Self {
|
||||
let r = (u >> 16) as u8;
|
||||
let g = (u >> 8) as u8;
|
||||
let b = u as u8;
|
||||
Self::Rgb(r, g, b)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl serde::Serialize for Color {
|
||||
/// This utilises the [`fmt::Display`] implementation for serialization.
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<'de> serde::Deserialize<'de> for Color {
|
||||
/// This is used to deserialize a value into Color via serde.
|
||||
///
|
||||
/// This implementation uses the `FromStr` trait to deserialize strings, so named colours, RGB,
|
||||
/// and indexed values are able to be deserialized. In addition, values that were produced by
|
||||
/// the the older serialization implementation of Color are also able to be deserialized.
|
||||
///
|
||||
/// Prior to v0.26.0, Ratatui would be serialized using a map for indexed and RGB values, for
|
||||
/// examples in json `{"Indexed": 10}` and `{"Rgb": [255, 0, 255]}` respectively. Now they are
|
||||
/// serialized using the string representation of the index and the RGB hex value, for example
|
||||
/// in json it would now be `"10"` and `"#FF00FF"` respectively.
|
||||
///
|
||||
/// See the [`Color`] documentation for more information on color names.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// #[derive(Debug, serde::Deserialize)]
|
||||
/// struct Theme {
|
||||
/// color: Color,
|
||||
/// }
|
||||
///
|
||||
/// # fn get_theme() -> Result<(), serde_json::Error> {
|
||||
/// let theme: Theme = serde_json::from_str(r#"{"color": "bright-white"}"#)?;
|
||||
/// assert_eq!(theme.color, Color::White);
|
||||
///
|
||||
/// let theme: Theme = serde_json::from_str(r##"{"color": "#00FF00"}"##)?;
|
||||
/// assert_eq!(theme.color, Color::Rgb(0, 255, 0));
|
||||
///
|
||||
/// let theme: Theme = serde_json::from_str(r#"{"color": "42"}"#)?;
|
||||
/// assert_eq!(theme.color, Color::Indexed(42));
|
||||
///
|
||||
/// let err = serde_json::from_str::<Theme>(r#"{"color": "invalid"}"#).unwrap_err();
|
||||
/// assert!(err.is_data());
|
||||
/// assert_eq!(
|
||||
/// err.to_string(),
|
||||
/// "Failed to parse Colors at line 1 column 20"
|
||||
/// );
|
||||
///
|
||||
/// // Deserializing from the previous serialization implementation
|
||||
/// let theme: Theme = serde_json::from_str(r#"{"color": {"Rgb":[255,0,255]}}"#)?;
|
||||
/// assert_eq!(theme.color, Color::Rgb(255, 0, 255));
|
||||
///
|
||||
/// let theme: Theme = serde_json::from_str(r#"{"color": {"Indexed":10}}"#)?;
|
||||
/// assert_eq!(theme.color, Color::Indexed(10));
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
/// Colors are currently serialized with the `Display` implementation, so
|
||||
/// RGB values are serialized via hex, for example "#FFFFFF".
|
||||
///
|
||||
/// Previously they were serialized using serde derive, which encoded
|
||||
/// RGB values as a map, for example { "rgb": [255, 255, 255] }.
|
||||
///
|
||||
/// The deserialization implementation utilises a `Helper` struct
|
||||
/// to be able to support both formats for backwards compatibility.
|
||||
#[derive(serde::Deserialize)]
|
||||
enum ColorWrapper {
|
||||
Rgb(u8, u8, u8),
|
||||
Indexed(u8),
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum ColorFormat {
|
||||
V2(String),
|
||||
V1(ColorWrapper),
|
||||
}
|
||||
|
||||
let multi_type = ColorFormat::deserialize(deserializer)
|
||||
.map_err(|err| serde::de::Error::custom(format!("Failed to parse Colors: {err}")))?;
|
||||
match multi_type {
|
||||
ColorFormat::V2(s) => FromStr::from_str(&s).map_err(serde::de::Error::custom),
|
||||
ColorFormat::V1(color_wrapper) => match color_wrapper {
|
||||
ColorWrapper::Rgb(red, green, blue) => Ok(Self::Rgb(red, green, blue)),
|
||||
ColorWrapper::Indexed(index) => Ok(Self::Indexed(index)),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error type indicating a failure to parse a color string.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub struct ParseColorError;
|
||||
|
||||
impl fmt::Display for ParseColorError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> 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.
|
||||
///
|
||||
/// See the [`Color`] documentation for more information on the supported color names.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use std::str::FromStr;
|
||||
///
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// 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(
|
||||
// There is a mix of different color names and formats in the wild.
|
||||
// This is an attempt to support as many as possible.
|
||||
match s
|
||||
.to_lowercase()
|
||||
.replace([' ', '-', '_'], "")
|
||||
.replace("bright", "light")
|
||||
.replace("grey", "gray")
|
||||
.replace("silver", "gray")
|
||||
.replace("lightblack", "darkgray")
|
||||
.replace("lightwhite", "white")
|
||||
.replace("lightgray", "white")
|
||||
.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" => Self::DarkGray,
|
||||
"lightred" => Self::LightRed,
|
||||
"lightgreen" => Self::LightGreen,
|
||||
"lightyellow" => Self::LightYellow,
|
||||
"lightblue" => Self::LightBlue,
|
||||
"lightmagenta" => Self::LightMagenta,
|
||||
"lightcyan" => Self::LightCyan,
|
||||
"white" => Self::White,
|
||||
_ => {
|
||||
if let Ok(index) = s.parse::<u8>() {
|
||||
Self::Indexed(index)
|
||||
} else if let Some((r, g, b)) = parse_hex_color(s) {
|
||||
Self::Rgb(r, g, b)
|
||||
} else {
|
||||
return Err(ParseColorError);
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_hex_color(input: &str) -> Option<(u8, u8, u8)> {
|
||||
if !input.starts_with('#') || input.len() != 7 {
|
||||
return None;
|
||||
}
|
||||
let r = u8::from_str_radix(input.get(1..3)?, 16).ok()?;
|
||||
let g = u8::from_str_radix(input.get(3..5)?, 16).ok()?;
|
||||
let b = u8::from_str_radix(input.get(5..7)?, 16).ok()?;
|
||||
Some((r, g, b))
|
||||
}
|
||||
|
||||
impl fmt::Display for Color {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Reset => write!(f, "Reset"),
|
||||
Self::Black => write!(f, "Black"),
|
||||
Self::Red => write!(f, "Red"),
|
||||
Self::Green => write!(f, "Green"),
|
||||
Self::Yellow => write!(f, "Yellow"),
|
||||
Self::Blue => write!(f, "Blue"),
|
||||
Self::Magenta => write!(f, "Magenta"),
|
||||
Self::Cyan => write!(f, "Cyan"),
|
||||
Self::Gray => write!(f, "Gray"),
|
||||
Self::DarkGray => write!(f, "DarkGray"),
|
||||
Self::LightRed => write!(f, "LightRed"),
|
||||
Self::LightGreen => write!(f, "LightGreen"),
|
||||
Self::LightYellow => write!(f, "LightYellow"),
|
||||
Self::LightBlue => write!(f, "LightBlue"),
|
||||
Self::LightMagenta => write!(f, "LightMagenta"),
|
||||
Self::LightCyan => write!(f, "LightCyan"),
|
||||
Self::White => write!(f, "White"),
|
||||
Self::Rgb(r, g, b) => write!(f, "#{r:02X}{g:02X}{b:02X}"),
|
||||
Self::Indexed(i) => write!(f, "{i}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Color {
|
||||
pub(crate) const fn stylize_debug(self, kind: ColorDebugKind) -> ColorDebug {
|
||||
ColorDebug { kind, color: self }
|
||||
}
|
||||
|
||||
/// Converts a HSL representation to a `Color::Rgb` instance.
|
||||
///
|
||||
/// The `from_hsl` function converts the Hue, Saturation and Lightness values to a
|
||||
/// corresponding `Color` RGB equivalent.
|
||||
///
|
||||
/// Hue values should be in the range [0, 360].
|
||||
/// Saturation and L values should be in the range [0, 100].
|
||||
/// Values that are not in the range are clamped to be within the range.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// let color: Color = Color::from_hsl(360.0, 100.0, 100.0);
|
||||
/// assert_eq!(color, Color::Rgb(255, 255, 255));
|
||||
///
|
||||
/// let color: Color = Color::from_hsl(0.0, 0.0, 0.0);
|
||||
/// assert_eq!(color, Color::Rgb(0, 0, 0));
|
||||
/// ```
|
||||
pub fn from_hsl(h: f64, s: f64, l: f64) -> Self {
|
||||
// Clamp input values to valid ranges
|
||||
let h = h.clamp(0.0, 360.0);
|
||||
let s = s.clamp(0.0, 100.0);
|
||||
let l = l.clamp(0.0, 100.0);
|
||||
|
||||
// Delegate to the function for normalized HSL to RGB conversion
|
||||
normalized_hsl_to_rgb(h / 360.0, s / 100.0, l / 100.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts normalized HSL (Hue, Saturation, Lightness) values to RGB (Red, Green, Blue) color
|
||||
/// representation. H, S, and L values should be in the range [0, 1].
|
||||
///
|
||||
/// Based on <https://github.com/killercup/hsl-rs/blob/b8a30e11afd75f262e0550725333293805f4ead0/src/lib.rs>
|
||||
fn normalized_hsl_to_rgb(hue: f64, saturation: f64, lightness: f64) -> Color {
|
||||
// This function can be made into `const` in the future.
|
||||
// This comment contains the relevant information for making it `const`.
|
||||
//
|
||||
// If it is `const` and made public, users can write the following:
|
||||
//
|
||||
// ```rust
|
||||
// const SLATE_50: Color = normalized_hsl_to_rgb(0.210, 0.40, 0.98);
|
||||
// ```
|
||||
//
|
||||
// For it to be const now, we need `#![feature(const_fn_floating_point_arithmetic)]`
|
||||
// Tracking issue: https://github.com/rust-lang/rust/issues/57241
|
||||
//
|
||||
// We would also need to remove the use of `.round()` in this function, i.e.:
|
||||
//
|
||||
// ```rust
|
||||
// Color::Rgb((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8)
|
||||
// ```
|
||||
|
||||
// Initialize RGB components
|
||||
let red: f64;
|
||||
let green: f64;
|
||||
let blue: f64;
|
||||
|
||||
// Check if the color is achromatic (grayscale)
|
||||
if saturation == 0.0 {
|
||||
red = lightness;
|
||||
green = lightness;
|
||||
blue = lightness;
|
||||
} else {
|
||||
// Calculate RGB components for colored cases
|
||||
let q = if lightness < 0.5 {
|
||||
lightness * (1.0 + saturation)
|
||||
} else {
|
||||
lightness + saturation - lightness * saturation
|
||||
};
|
||||
let p = 2.0 * lightness - q;
|
||||
red = hue_to_rgb(p, q, hue + 1.0 / 3.0);
|
||||
green = hue_to_rgb(p, q, hue);
|
||||
blue = hue_to_rgb(p, q, hue - 1.0 / 3.0);
|
||||
}
|
||||
|
||||
// Scale RGB components to the range [0, 255] and create a Color::Rgb instance
|
||||
Color::Rgb(
|
||||
(red * 255.0).round() as u8,
|
||||
(green * 255.0).round() as u8,
|
||||
(blue * 255.0).round() as u8,
|
||||
)
|
||||
}
|
||||
|
||||
/// Helper function to calculate RGB component for a specific hue value.
|
||||
fn hue_to_rgb(p: f64, q: f64, t: f64) -> f64 {
|
||||
// Adjust the hue value to be within the valid range [0, 1]
|
||||
let mut t = t;
|
||||
if t < 0.0 {
|
||||
t += 1.0;
|
||||
}
|
||||
if t > 1.0 {
|
||||
t -= 1.0;
|
||||
}
|
||||
|
||||
// Calculate the RGB component based on the hue value
|
||||
if t < 1.0 / 6.0 {
|
||||
p + (q - p) * 6.0 * t
|
||||
} else if t < 1.0 / 2.0 {
|
||||
q
|
||||
} else if t < 2.0 / 3.0 {
|
||||
p + (q - p) * (2.0 / 3.0 - t) * 6.0
|
||||
} else {
|
||||
p
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::error::Error;
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::de::{Deserialize, IntoDeserializer};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_hsl_to_rgb() {
|
||||
// Test with valid HSL values
|
||||
let color = Color::from_hsl(120.0, 50.0, 75.0);
|
||||
assert_eq!(color, Color::Rgb(159, 223, 159));
|
||||
|
||||
// Test with H value at upper bound
|
||||
let color = Color::from_hsl(360.0, 50.0, 75.0);
|
||||
assert_eq!(color, Color::Rgb(223, 159, 159));
|
||||
|
||||
// Test with H value exceeding the upper bound
|
||||
let color = Color::from_hsl(400.0, 50.0, 75.0);
|
||||
assert_eq!(color, Color::Rgb(223, 159, 159));
|
||||
|
||||
// Test with S and L values exceeding the upper bound
|
||||
let color = Color::from_hsl(240.0, 120.0, 150.0);
|
||||
assert_eq!(color, Color::Rgb(255, 255, 255));
|
||||
|
||||
// Test with H, S, and L values below the lower bound
|
||||
let color = Color::from_hsl(-20.0, -50.0, -20.0);
|
||||
assert_eq!(color, Color::Rgb(0, 0, 0));
|
||||
|
||||
// Test with S and L values below the lower bound
|
||||
let color = Color::from_hsl(60.0, -20.0, -10.0);
|
||||
assert_eq!(color, Color::Rgb(0, 0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_u32() {
|
||||
assert_eq!(Color::from_u32(0x000000), Color::Rgb(0, 0, 0));
|
||||
assert_eq!(Color::from_u32(0xFF0000), Color::Rgb(255, 0, 0));
|
||||
assert_eq!(Color::from_u32(0x00FF00), Color::Rgb(0, 255, 0));
|
||||
assert_eq!(Color::from_u32(0x0000FF), Color::Rgb(0, 0, 255));
|
||||
assert_eq!(Color::from_u32(0xFFFFFF), Color::Rgb(255, 255, 255));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_rgb_color() {
|
||||
let color: Color = Color::from_str("#FF0000").unwrap();
|
||||
assert_eq!(color, Color::Rgb(255, 0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_indexed_color() {
|
||||
let color: Color = Color::from_str("10").unwrap();
|
||||
assert_eq!(color, Color::Indexed(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_ansi_color() -> Result<(), Box<dyn Error>> {
|
||||
assert_eq!(Color::from_str("reset")?, Color::Reset);
|
||||
assert_eq!(Color::from_str("black")?, Color::Black);
|
||||
assert_eq!(Color::from_str("red")?, Color::Red);
|
||||
assert_eq!(Color::from_str("green")?, Color::Green);
|
||||
assert_eq!(Color::from_str("yellow")?, Color::Yellow);
|
||||
assert_eq!(Color::from_str("blue")?, Color::Blue);
|
||||
assert_eq!(Color::from_str("magenta")?, Color::Magenta);
|
||||
assert_eq!(Color::from_str("cyan")?, Color::Cyan);
|
||||
assert_eq!(Color::from_str("gray")?, Color::Gray);
|
||||
assert_eq!(Color::from_str("darkgray")?, Color::DarkGray);
|
||||
assert_eq!(Color::from_str("lightred")?, Color::LightRed);
|
||||
assert_eq!(Color::from_str("lightgreen")?, Color::LightGreen);
|
||||
assert_eq!(Color::from_str("lightyellow")?, Color::LightYellow);
|
||||
assert_eq!(Color::from_str("lightblue")?, Color::LightBlue);
|
||||
assert_eq!(Color::from_str("lightmagenta")?, Color::LightMagenta);
|
||||
assert_eq!(Color::from_str("lightcyan")?, Color::LightCyan);
|
||||
assert_eq!(Color::from_str("white")?, Color::White);
|
||||
|
||||
// aliases
|
||||
assert_eq!(Color::from_str("lightblack")?, Color::DarkGray);
|
||||
assert_eq!(Color::from_str("lightwhite")?, Color::White);
|
||||
assert_eq!(Color::from_str("lightgray")?, Color::White);
|
||||
|
||||
// silver = grey = gray
|
||||
assert_eq!(Color::from_str("grey")?, Color::Gray);
|
||||
assert_eq!(Color::from_str("silver")?, Color::Gray);
|
||||
|
||||
// spaces are ignored
|
||||
assert_eq!(Color::from_str("light black")?, Color::DarkGray);
|
||||
assert_eq!(Color::from_str("light white")?, Color::White);
|
||||
assert_eq!(Color::from_str("light gray")?, Color::White);
|
||||
|
||||
// dashes are ignored
|
||||
assert_eq!(Color::from_str("light-black")?, Color::DarkGray);
|
||||
assert_eq!(Color::from_str("light-white")?, Color::White);
|
||||
assert_eq!(Color::from_str("light-gray")?, Color::White);
|
||||
|
||||
// underscores are ignored
|
||||
assert_eq!(Color::from_str("light_black")?, Color::DarkGray);
|
||||
assert_eq!(Color::from_str("light_white")?, Color::White);
|
||||
assert_eq!(Color::from_str("light_gray")?, Color::White);
|
||||
|
||||
// bright = light
|
||||
assert_eq!(Color::from_str("bright-black")?, Color::DarkGray);
|
||||
assert_eq!(Color::from_str("bright-white")?, Color::White);
|
||||
|
||||
// bright = light
|
||||
assert_eq!(Color::from_str("brightblack")?, Color::DarkGray);
|
||||
assert_eq!(Color::from_str("brightwhite")?, Color::White);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_invalid_colors() {
|
||||
let bad_colors = [
|
||||
"invalid_color", // not a color string
|
||||
"abcdef0", // 7 chars is not a color
|
||||
" bcdefa", // doesn't start with a '#'
|
||||
"#abcdef00", // too many chars
|
||||
"#1🦀2", // len 7 but on char boundaries shouldnt panic
|
||||
"resett", // typo
|
||||
"lightblackk", // typo
|
||||
];
|
||||
|
||||
for bad_color in bad_colors {
|
||||
assert!(
|
||||
Color::from_str(bad_color).is_err(),
|
||||
"bad color: '{bad_color}'"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display() {
|
||||
assert_eq!(format!("{}", Color::Black), "Black");
|
||||
assert_eq!(format!("{}", Color::Red), "Red");
|
||||
assert_eq!(format!("{}", Color::Green), "Green");
|
||||
assert_eq!(format!("{}", Color::Yellow), "Yellow");
|
||||
assert_eq!(format!("{}", Color::Blue), "Blue");
|
||||
assert_eq!(format!("{}", Color::Magenta), "Magenta");
|
||||
assert_eq!(format!("{}", Color::Cyan), "Cyan");
|
||||
assert_eq!(format!("{}", Color::Gray), "Gray");
|
||||
assert_eq!(format!("{}", Color::DarkGray), "DarkGray");
|
||||
assert_eq!(format!("{}", Color::LightRed), "LightRed");
|
||||
assert_eq!(format!("{}", Color::LightGreen), "LightGreen");
|
||||
assert_eq!(format!("{}", Color::LightYellow), "LightYellow");
|
||||
assert_eq!(format!("{}", Color::LightBlue), "LightBlue");
|
||||
assert_eq!(format!("{}", Color::LightMagenta), "LightMagenta");
|
||||
assert_eq!(format!("{}", Color::LightCyan), "LightCyan");
|
||||
assert_eq!(format!("{}", Color::White), "White");
|
||||
assert_eq!(format!("{}", Color::Indexed(10)), "10");
|
||||
assert_eq!(format!("{}", Color::Rgb(255, 0, 0)), "#FF0000");
|
||||
assert_eq!(format!("{}", Color::Reset), "Reset");
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
#[test]
|
||||
fn deserialize() -> Result<(), serde::de::value::Error> {
|
||||
assert_eq!(
|
||||
Color::Black,
|
||||
Color::deserialize("Black".into_deserializer())?
|
||||
);
|
||||
assert_eq!(
|
||||
Color::Magenta,
|
||||
Color::deserialize("magenta".into_deserializer())?
|
||||
);
|
||||
assert_eq!(
|
||||
Color::LightGreen,
|
||||
Color::deserialize("LightGreen".into_deserializer())?
|
||||
);
|
||||
assert_eq!(
|
||||
Color::White,
|
||||
Color::deserialize("bright-white".into_deserializer())?
|
||||
);
|
||||
assert_eq!(
|
||||
Color::Indexed(42),
|
||||
Color::deserialize("42".into_deserializer())?
|
||||
);
|
||||
assert_eq!(
|
||||
Color::Rgb(0, 255, 0),
|
||||
Color::deserialize("#00ff00".into_deserializer())?
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
#[test]
|
||||
fn deserialize_error() {
|
||||
let color: Result<_, serde::de::value::Error> =
|
||||
Color::deserialize("invalid".into_deserializer());
|
||||
assert!(color.is_err());
|
||||
|
||||
let color: Result<_, serde::de::value::Error> =
|
||||
Color::deserialize("#00000000".into_deserializer());
|
||||
assert!(color.is_err());
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
#[test]
|
||||
fn serialize_then_deserialize() -> Result<(), serde_json::Error> {
|
||||
let json_rgb = serde_json::to_string(&Color::Rgb(255, 0, 255))?;
|
||||
assert_eq!(json_rgb, r##""#FF00FF""##);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Color>(&json_rgb)?,
|
||||
Color::Rgb(255, 0, 255)
|
||||
);
|
||||
|
||||
let json_white = serde_json::to_string(&Color::White)?;
|
||||
assert_eq!(json_white, r#""White""#);
|
||||
|
||||
let json_indexed = serde_json::to_string(&Color::Indexed(10))?;
|
||||
assert_eq!(json_indexed, r#""10""#);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Color>(&json_indexed)?,
|
||||
Color::Indexed(10)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
#[test]
|
||||
fn deserialize_with_previous_format() -> Result<(), serde_json::Error> {
|
||||
assert_eq!(Color::White, serde_json::from_str::<Color>("\"White\"")?);
|
||||
assert_eq!(
|
||||
Color::Rgb(255, 0, 255),
|
||||
serde_json::from_str::<Color>(r#"{"Rgb":[255,0,255]}"#)?
|
||||
);
|
||||
assert_eq!(
|
||||
Color::Indexed(10),
|
||||
serde_json::from_str::<Color>(r#"{"Indexed":10}"#)?
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
6
ratatui-core/src/style/palette.rs
Normal file
6
ratatui-core/src/style/palette.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
#![allow(clippy::unreadable_literal)]
|
||||
|
||||
//! A module for defining color palettes.
|
||||
|
||||
pub mod material;
|
||||
pub mod tailwind;
|
||||
606
ratatui-core/src/style/palette/material.rs
Normal file
606
ratatui-core/src/style/palette/material.rs
Normal file
@@ -0,0 +1,606 @@
|
||||
//! Material design color palettes.
|
||||
//!
|
||||
//! Represents the colors from the 2014 [Material design color palettes][palettes] by Google.
|
||||
//!
|
||||
//! [palettes]: https://m2.material.io/design/color/the-color-system.html#tools-for-picking-colors
|
||||
//!
|
||||
//! There are 16 palettes with accent colors, and 3 palettes without accent colors. Each palette
|
||||
//! has 10 colors, with variants from 50 to 900. The accent palettes also have 4 accent colors
|
||||
//! with variants from 100 to 700. Black and White are also included for completeness and to avoid
|
||||
//! being affected by any terminal theme that might be in use.
|
||||
//!
|
||||
//! This module exists to provide a convenient way to use the colors from the
|
||||
//! [`matdesign-color` crate] in your application.
|
||||
//!
|
||||
//! <style>
|
||||
//! .color { display: flex; align-items: center; }
|
||||
//! .color > div { width: 2rem; height: 2rem; }
|
||||
//! .color > div.name { width: 150px; !important; }
|
||||
//! </style>
|
||||
//! <div style="overflow-x: auto">
|
||||
//! <div style="display: flex; flex-direction:column; text-align: left">
|
||||
//! <div class="color" style="font-size:0.8em">
|
||||
//! <div class="name"></div>
|
||||
//! <div>C50</div>
|
||||
//! <div>C100</div>
|
||||
//! <div>C200</div>
|
||||
//! <div>C300</div>
|
||||
//! <div>C400</div>
|
||||
//! <div>C500</div>
|
||||
//! <div>C600</div>
|
||||
//! <div>C700</div>
|
||||
//! <div>C800</div>
|
||||
//! <div>C900</div>
|
||||
//! <div>A100</div>
|
||||
//! <div>A200</div>
|
||||
//! <div>A400</div>
|
||||
//! <div>A700</div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`RED`]</div>
|
||||
//! <div style="background-color: #FFEBEE"></div>
|
||||
//! <div style="background-color: #FFCDD2"></div>
|
||||
//! <div style="background-color: #EF9A9A"></div>
|
||||
//! <div style="background-color: #E57373"></div>
|
||||
//! <div style="background-color: #EF5350"></div>
|
||||
//! <div style="background-color: #F44336"></div>
|
||||
//! <div style="background-color: #E53935"></div>
|
||||
//! <div style="background-color: #D32F2F"></div>
|
||||
//! <div style="background-color: #C62828"></div>
|
||||
//! <div style="background-color: #B71C1C"></div>
|
||||
//! <div style="background-color: #FF8A80"></div>
|
||||
//! <div style="background-color: #FF5252"></div>
|
||||
//! <div style="background-color: #FF1744"></div>
|
||||
//! <div style="background-color: #D50000"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`PINK`]</div>
|
||||
//! <div style="background-color: #FCE4EC"></div>
|
||||
//! <div style="background-color: #F8BBD0"></div>
|
||||
//! <div style="background-color: #F48FB1"></div>
|
||||
//! <div style="background-color: #F06292"></div>
|
||||
//! <div style="background-color: #EC407A"></div>
|
||||
//! <div style="background-color: #E91E63"></div>
|
||||
//! <div style="background-color: #D81B60"></div>
|
||||
//! <div style="background-color: #C2185B"></div>
|
||||
//! <div style="background-color: #AD1457"></div>
|
||||
//! <div style="background-color: #880E4F"></div>
|
||||
//! <div style="background-color: #FF80AB"></div>
|
||||
//! <div style="background-color: #FF4081"></div>
|
||||
//! <div style="background-color: #F50057"></div>
|
||||
//! <div style="background-color: #C51162"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`PURPLE`]</div>
|
||||
//! <div style="background-color: #F3E5F5"></div>
|
||||
//! <div style="background-color: #E1BEE7"></div>
|
||||
//! <div style="background-color: #CE93D8"></div>
|
||||
//! <div style="background-color: #BA68C8"></div>
|
||||
//! <div style="background-color: #AB47BC"></div>
|
||||
//! <div style="background-color: #9C27B0"></div>
|
||||
//! <div style="background-color: #8E24AA"></div>
|
||||
//! <div style="background-color: #7B1FA2"></div>
|
||||
//! <div style="background-color: #6A1B9A"></div>
|
||||
//! <div style="background-color: #4A148C"></div>
|
||||
//! <div style="background-color: #EA80FC"></div>
|
||||
//! <div style="background-color: #E040FB"></div>
|
||||
//! <div style="background-color: #D500F9"></div>
|
||||
//! <div style="background-color: #AA00FF"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`DEEP_PURPLE`]</div>
|
||||
//! <div style="background-color: #EDE7F6"></div>
|
||||
//! <div style="background-color: #D1C4E9"></div>
|
||||
//! <div style="background-color: #B39DDB"></div>
|
||||
//! <div style="background-color: #9575CD"></div>
|
||||
//! <div style="background-color: #7E57C2"></div>
|
||||
//! <div style="background-color: #673AB7"></div>
|
||||
//! <div style="background-color: #5E35B1"></div>
|
||||
//! <div style="background-color: #512DA8"></div>
|
||||
//! <div style="background-color: #4527A0"></div>
|
||||
//! <div style="background-color: #311B92"></div>
|
||||
//! <div style="background-color: #B388FF"></div>
|
||||
//! <div style="background-color: #7C4DFF"></div>
|
||||
//! <div style="background-color: #651FFF"></div>
|
||||
//! <div style="background-color: #6200EA"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`INDIGO`]</div>
|
||||
//! <div style="background-color: #E8EAF6"></div>
|
||||
//! <div style="background-color: #C5CAE9"></div>
|
||||
//! <div style="background-color: #9FA8DA"></div>
|
||||
//! <div style="background-color: #7986CB"></div>
|
||||
//! <div style="background-color: #5C6BC0"></div>
|
||||
//! <div style="background-color: #3F51B5"></div>
|
||||
//! <div style="background-color: #3949AB"></div>
|
||||
//! <div style="background-color: #303F9F"></div>
|
||||
//! <div style="background-color: #283593"></div>
|
||||
//! <div style="background-color: #1A237E"></div>
|
||||
//! <div style="background-color: #8C9EFF"></div>
|
||||
//! <div style="background-color: #536DFE"></div>
|
||||
//! <div style="background-color: #3D5AFE"></div>
|
||||
//! <div style="background-color: #304FFE"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`BLUE`]</div>
|
||||
//! <div style="background-color: #E3F2FD"></div>
|
||||
//! <div style="background-color: #BBDEFB"></div>
|
||||
//! <div style="background-color: #90CAF9"></div>
|
||||
//! <div style="background-color: #64B5F6"></div>
|
||||
//! <div style="background-color: #42A5F5"></div>
|
||||
//! <div style="background-color: #2196F3"></div>
|
||||
//! <div style="background-color: #1E88E5"></div>
|
||||
//! <div style="background-color: #1976D2"></div>
|
||||
//! <div style="background-color: #1565C0"></div>
|
||||
//! <div style="background-color: #0D47A1"></div>
|
||||
//! <div style="background-color: #82B1FF"></div>
|
||||
//! <div style="background-color: #448AFF"></div>
|
||||
//! <div style="background-color: #2979FF"></div>
|
||||
//! <div style="background-color: #2962FF"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`LIGHT_BLUE`]</div>
|
||||
//! <div style="background-color: #E1F5FE"></div>
|
||||
//! <div style="background-color: #B3E5FC"></div>
|
||||
//! <div style="background-color: #81D4FA"></div>
|
||||
//! <div style="background-color: #4FC3F7"></div>
|
||||
//! <div style="background-color: #29B6F6"></div>
|
||||
//! <div style="background-color: #03A9F4"></div>
|
||||
//! <div style="background-color: #039BE5"></div>
|
||||
//! <div style="background-color: #0288D1"></div>
|
||||
//! <div style="background-color: #0277BD"></div>
|
||||
//! <div style="background-color: #01579B"></div>
|
||||
//! <div style="background-color: #80D8FF"></div>
|
||||
//! <div style="background-color: #40C4FF"></div>
|
||||
//! <div style="background-color: #00B0FF"></div>
|
||||
//! <div style="background-color: #0091EA"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`CYAN`]</div>
|
||||
//! <div style="background-color: #E0F7FA"></div>
|
||||
//! <div style="background-color: #B2EBF2"></div>
|
||||
//! <div style="background-color: #80DEEA"></div>
|
||||
//! <div style="background-color: #4DD0E1"></div>
|
||||
//! <div style="background-color: #26C6DA"></div>
|
||||
//! <div style="background-color: #00BCD4"></div>
|
||||
//! <div style="background-color: #00ACC1"></div>
|
||||
//! <div style="background-color: #0097A7"></div>
|
||||
//! <div style="background-color: #00838F"></div>
|
||||
//! <div style="background-color: #006064"></div>
|
||||
//! <div style="background-color: #84FFFF"></div>
|
||||
//! <div style="background-color: #18FFFF"></div>
|
||||
//! <div style="background-color: #00E5FF"></div>
|
||||
//! <div style="background-color: #00B8D4"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`TEAL`]</div>
|
||||
//! <div style="background-color: #E0F2F1"></div>
|
||||
//! <div style="background-color: #B2DFDB"></div>
|
||||
//! <div style="background-color: #80CBC4"></div>
|
||||
//! <div style="background-color: #4DB6AC"></div>
|
||||
//! <div style="background-color: #26A69A"></div>
|
||||
//! <div style="background-color: #009688"></div>
|
||||
//! <div style="background-color: #00897B"></div>
|
||||
//! <div style="background-color: #00796B"></div>
|
||||
//! <div style="background-color: #00695C"></div>
|
||||
//! <div style="background-color: #004D40"></div>
|
||||
//! <div style="background-color: #A7FFEB"></div>
|
||||
//! <div style="background-color: #64FFDA"></div>
|
||||
//! <div style="background-color: #1DE9B6"></div>
|
||||
//! <div style="background-color: #00BFA5"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`GREEN`]</div>
|
||||
//! <div style="background-color: #E8F5E9"></div>
|
||||
//! <div style="background-color: #C8E6C9"></div>
|
||||
//! <div style="background-color: #A5D6A7"></div>
|
||||
//! <div style="background-color: #81C784"></div>
|
||||
//! <div style="background-color: #66BB6A"></div>
|
||||
//! <div style="background-color: #4CAF50"></div>
|
||||
//! <div style="background-color: #43A047"></div>
|
||||
//! <div style="background-color: #388E3C"></div>
|
||||
//! <div style="background-color: #2E7D32"></div>
|
||||
//! <div style="background-color: #1B5E20"></div>
|
||||
//! <div style="background-color: #B9F6CA"></div>
|
||||
//! <div style="background-color: #69F0AE"></div>
|
||||
//! <div style="background-color: #00E676"></div>
|
||||
//! <div style="background-color: #00C853"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`LIGHT_GREEN`]</div>
|
||||
//! <div style="background-color: #F1F8E9"></div>
|
||||
//! <div style="background-color: #DCEDC8"></div>
|
||||
//! <div style="background-color: #C5E1A5"></div>
|
||||
//! <div style="background-color: #AED581"></div>
|
||||
//! <div style="background-color: #9CCC65"></div>
|
||||
//! <div style="background-color: #8BC34A"></div>
|
||||
//! <div style="background-color: #7CB342"></div>
|
||||
//! <div style="background-color: #689F38"></div>
|
||||
//! <div style="background-color: #558B2F"></div>
|
||||
//! <div style="background-color: #33691E"></div>
|
||||
//! <div style="background-color: #CCFF90"></div>
|
||||
//! <div style="background-color: #B2FF59"></div>
|
||||
//! <div style="background-color: #76FF03"></div>
|
||||
//! <div style="background-color: #64DD17"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`LIME`]</div>
|
||||
//! <div style="background-color: #F9FBE7"></div>
|
||||
//! <div style="background-color: #F0F4C3"></div>
|
||||
//! <div style="background-color: #E6EE9C"></div>
|
||||
//! <div style="background-color: #DCE775"></div>
|
||||
//! <div style="background-color: #D4E157"></div>
|
||||
//! <div style="background-color: #CDDC39"></div>
|
||||
//! <div style="background-color: #C0CA33"></div>
|
||||
//! <div style="background-color: #AFB42B"></div>
|
||||
//! <div style="background-color: #9E9D24"></div>
|
||||
//! <div style="background-color: #827717"></div>
|
||||
//! <div style="background-color: #F4FF81"></div>
|
||||
//! <div style="background-color: #EEFF41"></div>
|
||||
//! <div style="background-color: #C6FF00"></div>
|
||||
//! <div style="background-color: #AEEA00"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`YELLOW`]</div>
|
||||
//! <div style="background-color: #FFFDE7"></div>
|
||||
//! <div style="background-color: #FFF9C4"></div>
|
||||
//! <div style="background-color: #FFF59D"></div>
|
||||
//! <div style="background-color: #FFF176"></div>
|
||||
//! <div style="background-color: #FFEE58"></div>
|
||||
//! <div style="background-color: #FFEB3B"></div>
|
||||
//! <div style="background-color: #FDD835"></div>
|
||||
//! <div style="background-color: #FBC02D"></div>
|
||||
//! <div style="background-color: #F9A825"></div>
|
||||
//! <div style="background-color: #F57F17"></div>
|
||||
//! <div style="background-color: #FFFF8D"></div>
|
||||
//! <div style="background-color: #FFFF00"></div>
|
||||
//! <div style="background-color: #FFEA00"></div>
|
||||
//! <div style="background-color: #FFD600"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`AMBER`]</div>
|
||||
//! <div style="background-color: #FFF8E1"></div>
|
||||
//! <div style="background-color: #FFECB3"></div>
|
||||
//! <div style="background-color: #FFE082"></div>
|
||||
//! <div style="background-color: #FFD54F"></div>
|
||||
//! <div style="background-color: #FFCA28"></div>
|
||||
//! <div style="background-color: #FFC107"></div>
|
||||
//! <div style="background-color: #FFB300"></div>
|
||||
//! <div style="background-color: #FFA000"></div>
|
||||
//! <div style="background-color: #FF8F00"></div>
|
||||
//! <div style="background-color: #FF6F00"></div>
|
||||
//! <div style="background-color: #FFE57F"></div>
|
||||
//! <div style="background-color: #FFD740"></div>
|
||||
//! <div style="background-color: #FFC400"></div>
|
||||
//! <div style="background-color: #FFAB00"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`ORANGE`]</div>
|
||||
//! <div style="background-color: #FFF3E0"></div>
|
||||
//! <div style="background-color: #FFE0B2"></div>
|
||||
//! <div style="background-color: #FFCC80"></div>
|
||||
//! <div style="background-color: #FFB74D"></div>
|
||||
//! <div style="background-color: #FFA726"></div>
|
||||
//! <div style="background-color: #FF9800"></div>
|
||||
//! <div style="background-color: #FB8C00"></div>
|
||||
//! <div style="background-color: #F57C00"></div>
|
||||
//! <div style="background-color: #EF6C00"></div>
|
||||
//! <div style="background-color: #E65100"></div>
|
||||
//! <div style="background-color: #FFD180"></div>
|
||||
//! <div style="background-color: #FFAB40"></div>
|
||||
//! <div style="background-color: #FF9100"></div>
|
||||
//! <div style="background-color: #FF6D00"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`DEEP_ORANGE`]</div>
|
||||
//! <div style="background-color: #FBE9E7"></div>
|
||||
//! <div style="background-color: #FFCCBC"></div>
|
||||
//! <div style="background-color: #FFAB91"></div>
|
||||
//! <div style="background-color: #FF8A65"></div>
|
||||
//! <div style="background-color: #FF7043"></div>
|
||||
//! <div style="background-color: #FF5722"></div>
|
||||
//! <div style="background-color: #F4511E"></div>
|
||||
//! <div style="background-color: #E64A19"></div>
|
||||
//! <div style="background-color: #D84315"></div>
|
||||
//! <div style="background-color: #BF360C"></div>
|
||||
//! <div style="background-color: #FF9E80"></div>
|
||||
//! <div style="background-color: #FF6E40"></div>
|
||||
//! <div style="background-color: #FF3D00"></div>
|
||||
//! <div style="background-color: #DD2C00"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`BROWN`]</div>
|
||||
//! <div style="background-color: #EFEBE9"></div>
|
||||
//! <div style="background-color: #D7CCC8"></div>
|
||||
//! <div style="background-color: #BCAAA4"></div>
|
||||
//! <div style="background-color: #A1887F"></div>
|
||||
//! <div style="background-color: #8D6E63"></div>
|
||||
//! <div style="background-color: #795548"></div>
|
||||
//! <div style="background-color: #6D4C41"></div>
|
||||
//! <div style="background-color: #5D4037"></div>
|
||||
//! <div style="background-color: #4E342E"></div>
|
||||
//! <div style="background-color: #3E2723"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`GRAY`]</div>
|
||||
//! <div style="background-color: #FAFAFA"></div>
|
||||
//! <div style="background-color: #F5F5F5"></div>
|
||||
//! <div style="background-color: #EEEEEE"></div>
|
||||
//! <div style="background-color: #E0E0E0"></div>
|
||||
//! <div style="background-color: #BDBDBD"></div>
|
||||
//! <div style="background-color: #9E9E9E"></div>
|
||||
//! <div style="background-color: #757575"></div>
|
||||
//! <div style="background-color: #616161"></div>
|
||||
//! <div style="background-color: #424242"></div>
|
||||
//! <div style="background-color: #212121"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`BLUE_GRAY`]</div>
|
||||
//! <div style="background-color: #ECEFF1"></div>
|
||||
//! <div style="background-color: #CFD8DC"></div>
|
||||
//! <div style="background-color: #B0BEC5"></div>
|
||||
//! <div style="background-color: #90A4AE"></div>
|
||||
//! <div style="background-color: #78909C"></div>
|
||||
//! <div style="background-color: #607D8B"></div>
|
||||
//! <div style="background-color: #546E7A"></div>
|
||||
//! <div style="background-color: #455A64"></div>
|
||||
//! <div style="background-color: #37474F"></div>
|
||||
//! <div style="background-color: #263238"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`BLACK`]</div>
|
||||
//! <div class="bw" style="width: 350px; background-color: #000000"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`WHITE`]</div>
|
||||
//! <div style="width: 350px; background-color: #FFFFFF"></div>
|
||||
//! </div>
|
||||
//! </div>
|
||||
//! </div>
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use ratatui::prelude::*;
|
||||
//! use ratatui::style::palette::material::{BLUE, RED};
|
||||
//!
|
||||
//! assert_eq!(RED.c500, Color::Rgb(244, 67, 54));
|
||||
//! assert_eq!(BLUE.c500, Color::Rgb(33, 150, 243));
|
||||
//! ```
|
||||
//!
|
||||
//! [`matdesign-color` crate]: https://crates.io/crates/matdesign-color
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// A palette of colors for use in Material design with accent colors
|
||||
///
|
||||
/// This is a collection of colors that are used in Material design. They consist of a set of
|
||||
/// colors from 50 to 900, and a set of accent colors from 100 to 700.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub struct AccentedPalette {
|
||||
pub c50: Color,
|
||||
pub c100: Color,
|
||||
pub c200: Color,
|
||||
pub c300: Color,
|
||||
pub c400: Color,
|
||||
pub c500: Color,
|
||||
pub c600: Color,
|
||||
pub c700: Color,
|
||||
pub c800: Color,
|
||||
pub c900: Color,
|
||||
pub a100: Color,
|
||||
pub a200: Color,
|
||||
pub a400: Color,
|
||||
pub a700: Color,
|
||||
}
|
||||
|
||||
/// A palette of colors for use in Material design without accent colors
|
||||
///
|
||||
/// This is a collection of colors that are used in Material design. They consist of a set of
|
||||
/// colors from 50 to 900.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub struct NonAccentedPalette {
|
||||
pub c50: Color,
|
||||
pub c100: Color,
|
||||
pub c200: Color,
|
||||
pub c300: Color,
|
||||
pub c400: Color,
|
||||
pub c500: Color,
|
||||
pub c600: Color,
|
||||
pub c700: Color,
|
||||
pub c800: Color,
|
||||
pub c900: Color,
|
||||
}
|
||||
|
||||
impl AccentedPalette {
|
||||
/// Create a new `AccentedPalette` from the given variants
|
||||
///
|
||||
/// The variants should be in the format [0x00RRGGBB, ...]
|
||||
pub const fn from_variants(variants: [u32; 14]) -> Self {
|
||||
Self {
|
||||
c50: Color::from_u32(variants[0]),
|
||||
c100: Color::from_u32(variants[1]),
|
||||
c200: Color::from_u32(variants[2]),
|
||||
c300: Color::from_u32(variants[3]),
|
||||
c400: Color::from_u32(variants[4]),
|
||||
c500: Color::from_u32(variants[5]),
|
||||
c600: Color::from_u32(variants[6]),
|
||||
c700: Color::from_u32(variants[7]),
|
||||
c800: Color::from_u32(variants[8]),
|
||||
c900: Color::from_u32(variants[9]),
|
||||
a100: Color::from_u32(variants[10]),
|
||||
a200: Color::from_u32(variants[11]),
|
||||
a400: Color::from_u32(variants[12]),
|
||||
a700: Color::from_u32(variants[13]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NonAccentedPalette {
|
||||
/// Create a new `NonAccented` from the given variants
|
||||
///
|
||||
/// The variants should be in the format [0x00RRGGBB, ...]
|
||||
pub const fn from_variants(variants: [u32; 10]) -> Self {
|
||||
Self {
|
||||
c50: Color::from_u32(variants[0]),
|
||||
c100: Color::from_u32(variants[1]),
|
||||
c200: Color::from_u32(variants[2]),
|
||||
c300: Color::from_u32(variants[3]),
|
||||
c400: Color::from_u32(variants[4]),
|
||||
c500: Color::from_u32(variants[5]),
|
||||
c600: Color::from_u32(variants[6]),
|
||||
c700: Color::from_u32(variants[7]),
|
||||
c800: Color::from_u32(variants[8]),
|
||||
c900: Color::from_u32(variants[9]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Accented palettes
|
||||
|
||||
pub const RED: AccentedPalette = AccentedPalette::from_variants(variants::RED);
|
||||
pub const PINK: AccentedPalette = AccentedPalette::from_variants(variants::PINK);
|
||||
pub const PURPLE: AccentedPalette = AccentedPalette::from_variants(variants::PURPLE);
|
||||
pub const DEEP_PURPLE: AccentedPalette = AccentedPalette::from_variants(variants::DEEP_PURPLE);
|
||||
pub const INDIGO: AccentedPalette = AccentedPalette::from_variants(variants::INDIGO);
|
||||
pub const BLUE: AccentedPalette = AccentedPalette::from_variants(variants::BLUE);
|
||||
pub const LIGHT_BLUE: AccentedPalette = AccentedPalette::from_variants(variants::LIGHT_BLUE);
|
||||
pub const CYAN: AccentedPalette = AccentedPalette::from_variants(variants::CYAN);
|
||||
pub const TEAL: AccentedPalette = AccentedPalette::from_variants(variants::TEAL);
|
||||
pub const GREEN: AccentedPalette = AccentedPalette::from_variants(variants::GREEN);
|
||||
pub const LIGHT_GREEN: AccentedPalette = AccentedPalette::from_variants(variants::LIGHT_GREEN);
|
||||
pub const LIME: AccentedPalette = AccentedPalette::from_variants(variants::LIME);
|
||||
pub const YELLOW: AccentedPalette = AccentedPalette::from_variants(variants::YELLOW);
|
||||
pub const AMBER: AccentedPalette = AccentedPalette::from_variants(variants::AMBER);
|
||||
pub const ORANGE: AccentedPalette = AccentedPalette::from_variants(variants::ORANGE);
|
||||
pub const DEEP_ORANGE: AccentedPalette = AccentedPalette::from_variants(variants::DEEP_ORANGE);
|
||||
|
||||
// Unaccented palettes
|
||||
pub const BROWN: NonAccentedPalette = NonAccentedPalette::from_variants(variants::BROWN);
|
||||
pub const GRAY: NonAccentedPalette = NonAccentedPalette::from_variants(variants::GRAY);
|
||||
pub const BLUE_GRAY: NonAccentedPalette = NonAccentedPalette::from_variants(variants::BLUE_GRAY);
|
||||
|
||||
// Black and white included for completeness
|
||||
pub const BLACK: Color = Color::from_u32(0x000000);
|
||||
pub const WHITE: Color = Color::from_u32(0xFFFFFF);
|
||||
|
||||
mod variants {
|
||||
pub const RED: [u32; 14] = [
|
||||
0xFFEBEE, 0xFFCDD2, 0xEF9A9A, 0xE57373, 0xEF5350, 0xF44336, 0xE53935, 0xD32F2F, 0xC62828,
|
||||
0xB71C1C, 0xFF8A80, 0xFF5252, 0xFF1744, 0xD50000,
|
||||
];
|
||||
pub const PINK: [u32; 14] = [
|
||||
0xFCE4EC, 0xF8BBD0, 0xF48FB1, 0xF06292, 0xEC407A, 0xE91E63, 0xD81B60, 0xC2185B, 0xAD1457,
|
||||
0x880E4F, 0xFF80AB, 0xFF4081, 0xF50057, 0xC51162,
|
||||
];
|
||||
pub const PURPLE: [u32; 14] = [
|
||||
0xF3E5F5, 0xE1BEE7, 0xCE93D8, 0xBA68C8, 0xAB47BC, 0x9C27B0, 0x8E24AA, 0x7B1FA2, 0x6A1B9A,
|
||||
0x4A148C, 0xEA80FC, 0xE040FB, 0xD500F9, 0xAA00FF,
|
||||
];
|
||||
pub const DEEP_PURPLE: [u32; 14] = [
|
||||
0xEDE7F6, 0xD1C4E9, 0xB39DDB, 0x9575CD, 0x7E57C2, 0x673AB7, 0x5E35B1, 0x512DA8, 0x4527A0,
|
||||
0x311B92, 0xB388FF, 0x7C4DFF, 0x651FFF, 0x6200EA,
|
||||
];
|
||||
pub const INDIGO: [u32; 14] = [
|
||||
0xE8EAF6, 0xC5CAE9, 0x9FA8DA, 0x7986CB, 0x5C6BC0, 0x3F51B5, 0x3949AB, 0x303F9F, 0x283593,
|
||||
0x1A237E, 0x8C9EFF, 0x536DFE, 0x3D5AFE, 0x304FFE,
|
||||
];
|
||||
pub const BLUE: [u32; 14] = [
|
||||
0xE3F2FD, 0xBBDEFB, 0x90CAF9, 0x64B5F6, 0x42A5F5, 0x2196F3, 0x1E88E5, 0x1976D2, 0x1565C0,
|
||||
0x0D47A1, 0x82B1FF, 0x448AFF, 0x2979FF, 0x2962FF,
|
||||
];
|
||||
pub const LIGHT_BLUE: [u32; 14] = [
|
||||
0xE1F5FE, 0xB3E5FC, 0x81D4FA, 0x4FC3F7, 0x29B6F6, 0x03A9F4, 0x039BE5, 0x0288D1, 0x0277BD,
|
||||
0x01579B, 0x80D8FF, 0x40C4FF, 0x00B0FF, 0x0091EA,
|
||||
];
|
||||
pub const CYAN: [u32; 14] = [
|
||||
0xE0F7FA, 0xB2EBF2, 0x80DEEA, 0x4DD0E1, 0x26C6DA, 0x00BCD4, 0x00ACC1, 0x0097A7, 0x00838F,
|
||||
0x006064, 0x84FFFF, 0x18FFFF, 0x00E5FF, 0x00B8D4,
|
||||
];
|
||||
pub const TEAL: [u32; 14] = [
|
||||
0xE0F2F1, 0xB2DFDB, 0x80CBC4, 0x4DB6AC, 0x26A69A, 0x009688, 0x00897B, 0x00796B, 0x00695C,
|
||||
0x004D40, 0xA7FFEB, 0x64FFDA, 0x1DE9B6, 0x00BFA5,
|
||||
];
|
||||
pub const GREEN: [u32; 14] = [
|
||||
0xE8F5E9, 0xC8E6C9, 0xA5D6A7, 0x81C784, 0x66BB6A, 0x4CAF50, 0x43A047, 0x388E3C, 0x2E7D32,
|
||||
0x1B5E20, 0xB9F6CA, 0x69F0AE, 0x00E676, 0x00C853,
|
||||
];
|
||||
pub const LIGHT_GREEN: [u32; 14] = [
|
||||
0xF1F8E9, 0xDCEDC8, 0xC5E1A5, 0xAED581, 0x9CCC65, 0x8BC34A, 0x7CB342, 0x689F38, 0x558B2F,
|
||||
0x33691E, 0xCCFF90, 0xB2FF59, 0x76FF03, 0x64DD17,
|
||||
];
|
||||
pub const LIME: [u32; 14] = [
|
||||
0xF9FBE7, 0xF0F4C3, 0xE6EE9C, 0xDCE775, 0xD4E157, 0xCDDC39, 0xC0CA33, 0xAFB42B, 0x9E9D24,
|
||||
0x827717, 0xF4FF81, 0xEEFF41, 0xC6FF00, 0xAEEA00,
|
||||
];
|
||||
pub const YELLOW: [u32; 14] = [
|
||||
0xFFFDE7, 0xFFF9C4, 0xFFF59D, 0xFFF176, 0xFFEE58, 0xFFEB3B, 0xFDD835, 0xFBC02D, 0xF9A825,
|
||||
0xF57F17, 0xFFFF8D, 0xFFFF00, 0xFFEA00, 0xFFD600,
|
||||
];
|
||||
pub const AMBER: [u32; 14] = [
|
||||
0xFFF8E1, 0xFFECB3, 0xFFE082, 0xFFD54F, 0xFFCA28, 0xFFC107, 0xFFB300, 0xFFA000, 0xFF8F00,
|
||||
0xFF6F00, 0xFFE57F, 0xFFD740, 0xFFC400, 0xFFAB00,
|
||||
];
|
||||
pub const ORANGE: [u32; 14] = [
|
||||
0xFFF3E0, 0xFFE0B2, 0xFFCC80, 0xFFB74D, 0xFFA726, 0xFF9800, 0xFB8C00, 0xF57C00, 0xEF6C00,
|
||||
0xE65100, 0xFFD180, 0xFFAB40, 0xFF9100, 0xFF6D00,
|
||||
];
|
||||
pub const DEEP_ORANGE: [u32; 14] = [
|
||||
0xFBE9E7, 0xFFCCBC, 0xFFAB91, 0xFF8A65, 0xFF7043, 0xFF5722, 0xF4511E, 0xE64A19, 0xD84315,
|
||||
0xBF360C, 0xFF9E80, 0xFF6E40, 0xFF3D00, 0xDD2C00,
|
||||
];
|
||||
pub const BROWN: [u32; 10] = [
|
||||
0xEFEBE9, 0xD7CCC8, 0xBCAAA4, 0xA1887F, 0x8D6E63, 0x795548, 0x6D4C41, 0x5D4037, 0x4E342E,
|
||||
0x3E2723,
|
||||
];
|
||||
pub const GRAY: [u32; 10] = [
|
||||
0xFAFAFA, 0xF5F5F5, 0xEEEEEE, 0xE0E0E0, 0xBDBDBD, 0x9E9E9E, 0x757575, 0x616161, 0x424242,
|
||||
0x212121,
|
||||
];
|
||||
pub const BLUE_GRAY: [u32; 10] = [
|
||||
0xECEFF1, 0xCFD8DC, 0xB0BEC5, 0x90A4AE, 0x78909C, 0x607D8B, 0x546E7A, 0x455A64, 0x37474F,
|
||||
0x263238,
|
||||
];
|
||||
}
|
||||
652
ratatui-core/src/style/palette/tailwind.rs
Normal file
652
ratatui-core/src/style/palette/tailwind.rs
Normal file
@@ -0,0 +1,652 @@
|
||||
//! Represents the Tailwind CSS [default color palette][palette].
|
||||
//!
|
||||
//! [palette]: https://tailwindcss.com/docs/customizing-colors#default-color-palette
|
||||
//!
|
||||
//! There are 22 palettes. Each palette has 11 colors, with variants from 50 to 950. Black and White
|
||||
//! are also included for completeness and to avoid being affected by any terminal theme that might
|
||||
//! be in use.
|
||||
//!
|
||||
//! <style>
|
||||
//! .color { display: flex; align-items: center; }
|
||||
//! .color > div { width: 2rem; height: 2rem; }
|
||||
//! .color > div.name { width: 150px; !important; }
|
||||
//! </style>
|
||||
//! <div style="overflow-x: auto">
|
||||
//! <div style="display: flex; flex-direction:column; text-align: left">
|
||||
//! <div class="color" style="font-size:0.8em">
|
||||
//! <div class="name"></div>
|
||||
//! <div>C50</div> <div>C100</div> <div>C200</div> <div>C300</div> <div>C400</div>
|
||||
//! <div>C500</div> <div>C600</div> <div>C700</div> <div>C800</div> <div>C900</div>
|
||||
//! <div>C950</div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`SLATE`]</div>
|
||||
//! <div style="background-color: #f8fafc"></div> <div style="background-color: #f1f5f9"></div>
|
||||
//! <div style="background-color: #e2e8f0"></div> <div style="background-color: #cbd5e1"></div>
|
||||
//! <div style="background-color: #94a3b8"></div> <div style="background-color: #64748b"></div>
|
||||
//! <div style="background-color: #475569"></div> <div style="background-color: #334155"></div>
|
||||
//! <div style="background-color: #1e293b"></div> <div style="background-color: #0f172a"></div>
|
||||
//! <div style="background-color: #020617"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`GRAY`]</div>
|
||||
//! <div style="background-color: #f9fafb"></div> <div style="background-color: #f3f4f6"></div>
|
||||
//! <div style="background-color: #e5e7eb"></div> <div style="background-color: #d1d5db"></div>
|
||||
//! <div style="background-color: #9ca3af"></div> <div style="background-color: #6b7280"></div>
|
||||
//! <div style="background-color: #4b5563"></div> <div style="background-color: #374151"></div>
|
||||
//! <div style="background-color: #1f2937"></div> <div style="background-color: #111827"></div>
|
||||
//! <div style="background-color: #0a0a0a"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`ZINC`]</div>
|
||||
//! <div style="background-color: #fafafa"></div> <div style="background-color: #f5f5f5"></div>
|
||||
//! <div style="background-color: #e5e5e5"></div> <div style="background-color: #d4d4d4"></div>
|
||||
//! <div style="background-color: #a1a1aa"></div> <div style="background-color: #71717a"></div>
|
||||
//! <div style="background-color: #52525b"></div> <div style="background-color: #404040"></div>
|
||||
//! <div style="background-color: #262626"></div> <div style="background-color: #171717"></div>
|
||||
//! <div style="background-color: #0a0a0a"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`NEUTRAL`]</div>
|
||||
//! <div style="background-color: #fafafa"></div> <div style="background-color: #f5f5f5"></div>
|
||||
//! <div style="background-color: #e5e5e5"></div> <div style="background-color: #d4d4d4"></div>
|
||||
//! <div style="background-color: #a3a3a3"></div> <div style="background-color: #737373"></div>
|
||||
//! <div style="background-color: #525252"></div> <div style="background-color: #404040"></div>
|
||||
//! <div style="background-color: #262626"></div> <div style="background-color: #171717"></div>
|
||||
//! <div style="background-color: #0a0a0a"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`STONE`]</div>
|
||||
//! <div style="background-color: #fafaf9"></div> <div style="background-color: #f5f5f4"></div>
|
||||
//! <div style="background-color: #e7e5e4"></div> <div style="background-color: #d6d3d1"></div>
|
||||
//! <div style="background-color: #a8a29e"></div> <div style="background-color: #78716c"></div>
|
||||
//! <div style="background-color: #57534e"></div> <div style="background-color: #44403c"></div>
|
||||
//! <div style="background-color: #292524"></div> <div style="background-color: #1c1917"></div>
|
||||
//! <div style="background-color: #0c0a09"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`RED`]</div>
|
||||
//! <div style="background-color: #fef2f2"></div> <div style="background-color: #fee2e2"></div>
|
||||
//! <div style="background-color: #fecaca"></div> <div style="background-color: #fca5a5"></div>
|
||||
//! <div style="background-color: #f87171"></div> <div style="background-color: #ef4444"></div>
|
||||
//! <div style="background-color: #dc2626"></div> <div style="background-color: #b91c1c"></div>
|
||||
//! <div style="background-color: #991b1b"></div> <div style="background-color: #7f1d1d"></div>
|
||||
//! <div style="background-color: #450a0a"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`ORANGE`]</div>
|
||||
//! <div style="background-color: #fff7ed"></div> <div style="background-color: #ffedd5"></div>
|
||||
//! <div style="background-color: #fed7aa"></div> <div style="background-color: #fdba74"></div>
|
||||
//! <div style="background-color: #fb923c"></div> <div style="background-color: #f97316"></div>
|
||||
//! <div style="background-color: #ea580c"></div> <div style="background-color: #c2410c"></div>
|
||||
//! <div style="background-color: #9a3412"></div> <div style="background-color: #7c2d12"></div>
|
||||
//! <div style="background-color: #431407"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`AMBER`]</div>
|
||||
//! <div style="background-color: #fffbeb"></div> <div style="background-color: #fef3c7"></div>
|
||||
//! <div style="background-color: #fde68a"></div> <div style="background-color: #fcd34d"></div>
|
||||
//! <div style="background-color: #fbbf24"></div> <div style="background-color: #f59e0b"></div>
|
||||
//! <div style="background-color: #d97706"></div> <div style="background-color: #b45309"></div>
|
||||
//! <div style="background-color: #92400e"></div> <div style="background-color: #78350f"></div>
|
||||
//! <div style="background-color: #451a03"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`YELLOW`]</div>
|
||||
//! <div style="background-color: #fefce8"></div> <div style="background-color: #fef9c3"></div>
|
||||
//! <div style="background-color: #fef08a"></div> <div style="background-color: #fde047"></div>
|
||||
//! <div style="background-color: #facc15"></div> <div style="background-color: #eab308"></div>
|
||||
//! <div style="background-color: #ca8a04"></div> <div style="background-color: #a16207"></div>
|
||||
//! <div style="background-color: #854d0e"></div> <div style="background-color: #713f12"></div>
|
||||
//! <div style="background-color: #422006"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`LIME`]</div>
|
||||
//! <div style="background-color: #f7fee7"></div> <div style="background-color: #ecfccb"></div>
|
||||
//! <div style="background-color: #d9f99d"></div> <div style="background-color: #bef264"></div>
|
||||
//! <div style="background-color: #a3e635"></div> <div style="background-color: #84cc16"></div>
|
||||
//! <div style="background-color: #65a30d"></div> <div style="background-color: #4d7c0f"></div>
|
||||
//! <div style="background-color: #3f6212"></div> <div style="background-color: #365314"></div>
|
||||
//! <div style="background-color: #1a2e05"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`GREEN`]</div>
|
||||
//! <div style="background-color: #f0fdf4"></div> <div style="background-color: #dcfce7"></div>
|
||||
//! <div style="background-color: #bbf7d0"></div> <div style="background-color: #86efac"></div>
|
||||
//! <div style="background-color: #4ade80"></div> <div style="background-color: #22c55e"></div>
|
||||
//! <div style="background-color: #16a34a"></div> <div style="background-color: #15803d"></div>
|
||||
//! <div style="background-color: #166534"></div> <div style="background-color: #14532d"></div>
|
||||
//! <div style="background-color: #052e16"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`EMERALD`]</div>
|
||||
//! <div style="background-color: #ecfdf5"></div> <div style="background-color: #d1fae5"></div>
|
||||
//! <div style="background-color: #a7f3d0"></div> <div style="background-color: #6ee7b7"></div>
|
||||
//! <div style="background-color: #34d399"></div> <div style="background-color: #10b981"></div>
|
||||
//! <div style="background-color: #059669"></div> <div style="background-color: #047857"></div>
|
||||
//! <div style="background-color: #065f46"></div> <div style="background-color: #064e3b"></div>
|
||||
//! <div style="background-color: #022c22"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`TEAL`]</div>
|
||||
//! <div style="background-color: #f0fdfa"></div> <div style="background-color: #ccfbf1"></div>
|
||||
//! <div style="background-color: #99f6e4"></div> <div style="background-color: #5eead4"></div>
|
||||
//! <div style="background-color: #2dd4bf"></div> <div style="background-color: #14b8a6"></div>
|
||||
//! <div style="background-color: #0d9488"></div> <div style="background-color: #0f766e"></div>
|
||||
//! <div style="background-color: #115e59"></div> <div style="background-color: #134e4a"></div>
|
||||
//! <div style="background-color: #042f2e"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`CYAN`]</div>
|
||||
//! <div style="background-color: #ecfeff"></div> <div style="background-color: #cffafe"></div>
|
||||
//! <div style="background-color: #a5f3fc"></div> <div style="background-color: #67e8f9"></div>
|
||||
//! <div style="background-color: #22d3ee"></div> <div style="background-color: #06b6d4"></div>
|
||||
//! <div style="background-color: #0891b2"></div> <div style="background-color: #0e7490"></div>
|
||||
//! <div style="background-color: #155e75"></div> <div style="background-color: #164e63"></div>
|
||||
//! <div style="background-color: #083344"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`SKY`]</div>
|
||||
//! <div style="background-color: #f0f9ff"></div> <div style="background-color: #e0f2fe"></div>
|
||||
//! <div style="background-color: #bae6fd"></div> <div style="background-color: #7dd3fc"></div>
|
||||
//! <div style="background-color: #38bdf8"></div> <div style="background-color: #0ea5e9"></div>
|
||||
//! <div style="background-color: #0284c7"></div> <div style="background-color: #0369a1"></div>
|
||||
//! <div style="background-color: #075985"></div> <div style="background-color: #0c4a6e"></div>
|
||||
//! <div style="background-color: #082f49"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`BLUE`]</div>
|
||||
//! <div style="background-color: #eff6ff"></div> <div style="background-color: #dbeafe"></div>
|
||||
//! <div style="background-color: #bfdbfe"></div> <div style="background-color: #93c5fd"></div>
|
||||
//! <div style="background-color: #60a5fa"></div> <div style="background-color: #3b82f6"></div>
|
||||
//! <div style="background-color: #2563eb"></div> <div style="background-color: #1d4ed8"></div>
|
||||
//! <div style="background-color: #1e40af"></div> <div style="background-color: #1e3a8a"></div>
|
||||
//! <div style="background-color: #172554"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`INDIGO`]</div>
|
||||
//! <div style="background-color: #eef2ff"></div> <div style="background-color: #e0e7ff"></div>
|
||||
//! <div style="background-color: #c7d2fe"></div> <div style="background-color: #a5b4fc"></div>
|
||||
//! <div style="background-color: #818cf8"></div> <div style="background-color: #6366f1"></div>
|
||||
//! <div style="background-color: #4f46e5"></div> <div style="background-color: #4338ca"></div>
|
||||
//! <div style="background-color: #3730a3"></div> <div style="background-color: #312e81"></div>
|
||||
//! <div style="background-color: #1e1b4b"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`VIOLET`]</div>
|
||||
//! <div style="background-color: #f5f3ff"></div> <div style="background-color: #ede9fe"></div>
|
||||
//! <div style="background-color: #ddd6fe"></div> <div style="background-color: #c4b5fd"></div>
|
||||
//! <div style="background-color: #a78bfa"></div> <div style="background-color: #8b5cf6"></div>
|
||||
//! <div style="background-color: #7c3aed"></div> <div style="background-color: #6d28d9"></div>
|
||||
//! <div style="background-color: #5b21b6"></div> <div style="background-color: #4c1d95"></div>
|
||||
//! <div style="background-color: #2e1065"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`PURPLE`]</div>
|
||||
//! <div style="background-color: #faf5ff"></div> <div style="background-color: #f3e8ff"></div>
|
||||
//! <div style="background-color: #e9d5ff"></div> <div style="background-color: #d8b4fe"></div>
|
||||
//! <div style="background-color: #c084fc"></div> <div style="background-color: #a855f7"></div>
|
||||
//! <div style="background-color: #9333ea"></div> <div style="background-color: #7e22ce"></div>
|
||||
//! <div style="background-color: #6b21a8"></div> <div style="background-color: #581c87"></div>
|
||||
//! <div style="background-color: #4c136e"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`FUCHSIA`]</div>
|
||||
//! <div style="background-color: #fdf4ff"></div> <div style="background-color: #fae8ff"></div>
|
||||
//! <div style="background-color: #f5d0fe"></div> <div style="background-color: #f0abfc"></div>
|
||||
//! <div style="background-color: #e879f9"></div> <div style="background-color: #d946ef"></div>
|
||||
//! <div style="background-color: #c026d3"></div> <div style="background-color: #a21caf"></div>
|
||||
//! <div style="background-color: #86198f"></div> <div style="background-color: #701a75"></div>
|
||||
//! <div style="background-color: #4e145b"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`PINK`]</div>
|
||||
//! <div style="background-color: #fdf2f8"></div> <div style="background-color: #fce7f3"></div>
|
||||
//! <div style="background-color: #fbcfe8"></div> <div style="background-color: #f9a8d4"></div>
|
||||
//! <div style="background-color: #f472b6"></div> <div style="background-color: #ec4899"></div>
|
||||
//! <div style="background-color: #db2777"></div> <div style="background-color: #be185d"></div>
|
||||
//! <div style="background-color: #9d174d"></div> <div style="background-color: #831843"></div>
|
||||
//! <div style="background-color: #5f0b37"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`BLACK`]</div>
|
||||
//! <div style="background-color: #000000; width:22rem"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`WHITE`]</div>
|
||||
//! <div style="background-color: #ffffff; width:22rem"></div>
|
||||
//! </div>
|
||||
//! </div>
|
||||
//! </div>
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use ratatui::prelude::*;
|
||||
//! use ratatui::style::palette::tailwind::{BLUE, RED};
|
||||
//!
|
||||
//! assert_eq!(RED.c500, Color::Rgb(239, 68, 68));
|
||||
//! assert_eq!(BLUE.c500, Color::Rgb(59, 130, 246));
|
||||
//! ```
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
pub struct Palette {
|
||||
pub c50: Color,
|
||||
pub c100: Color,
|
||||
pub c200: Color,
|
||||
pub c300: Color,
|
||||
pub c400: Color,
|
||||
pub c500: Color,
|
||||
pub c600: Color,
|
||||
pub c700: Color,
|
||||
pub c800: Color,
|
||||
pub c900: Color,
|
||||
pub c950: Color,
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #000000"></div></div>
|
||||
pub const BLACK: Color = Color::from_u32(0x000000);
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #ffffff"></div></div>
|
||||
pub const WHITE: Color = Color::from_u32(0xffffff);
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f8fafc"></div><div style="background-color: #f1f5f9"></div><div style="background-color: #e2e8f0"></div><div style="background-color: #cbd5e1"></div><div style="background-color: #94a3b8"></div><div style="background-color: #64748b"></div><div style="background-color: #475569"></div><div style="background-color: #334155"></div><div style="background-color: #1e293b"></div><div style="background-color: #0f172a"></div><div style="background-color: #020617"></div></div>
|
||||
pub const SLATE: Palette = Palette {
|
||||
c50: Color::from_u32(0xf8fafc),
|
||||
c100: Color::from_u32(0xf1f5f9),
|
||||
c200: Color::from_u32(0xe2e8f0),
|
||||
c300: Color::from_u32(0xcbd5e1),
|
||||
c400: Color::from_u32(0x94a3b8),
|
||||
c500: Color::from_u32(0x64748b),
|
||||
c600: Color::from_u32(0x475569),
|
||||
c700: Color::from_u32(0x334155),
|
||||
c800: Color::from_u32(0x1e293b),
|
||||
c900: Color::from_u32(0x0f172a),
|
||||
c950: Color::from_u32(0x020617),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f9fafb"></div><div style="background-color: #f3f4f6"></div><div style="background-color: #e5e7eb"></div><div style="background-color: #d1d5db"></div><div style="background-color: #9ca3af"></div><div style="background-color: #6b7280"></div><div style="background-color: #4b5563"></div><div style="background-color: #374151"></div><div style="background-color: #1f2937"></div><div style="background-color: #111827"></div><div style="background-color: #030712"></div></div>
|
||||
pub const GRAY: Palette = Palette {
|
||||
c50: Color::from_u32(0xf9fafb),
|
||||
c100: Color::from_u32(0xf3f4f6),
|
||||
c200: Color::from_u32(0xe5e7eb),
|
||||
c300: Color::from_u32(0xd1d5db),
|
||||
c400: Color::from_u32(0x9ca3af),
|
||||
c500: Color::from_u32(0x6b7280),
|
||||
c600: Color::from_u32(0x4b5563),
|
||||
c700: Color::from_u32(0x374151),
|
||||
c800: Color::from_u32(0x1f2937),
|
||||
c900: Color::from_u32(0x111827),
|
||||
c950: Color::from_u32(0x030712),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fafafa"></div><div style="background-color: #f5f5f5"></div><div style="background-color: #e5e5e5"></div><div style="background-color: #d4d4d4"></div><div style="background-color: #a1a1aa"></div><div style="background-color: #71717a"></div><div style="background-color: #52525b"></div><div style="background-color: #404040"></div><div style="background-color: #262626"></div><div style="background-color: #171717"></div><div style="background-color: #09090b"></div></div>
|
||||
pub const ZINC: Palette = Palette {
|
||||
c50: Color::from_u32(0xfafafa),
|
||||
c100: Color::from_u32(0xf4f4f5),
|
||||
c200: Color::from_u32(0xe4e4e7),
|
||||
c300: Color::from_u32(0xd4d4d8),
|
||||
c400: Color::from_u32(0xa1a1aa),
|
||||
c500: Color::from_u32(0x71717a),
|
||||
c600: Color::from_u32(0x52525b),
|
||||
c700: Color::from_u32(0x3f3f46),
|
||||
c800: Color::from_u32(0x27272a),
|
||||
c900: Color::from_u32(0x18181b),
|
||||
c950: Color::from_u32(0x09090b),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fafafa"></div><div style="background-color: #f5f5f5"></div><div style="background-color: #e5e5e5"></div><div style="background-color: #d4d4d4"></div><div style="background-color: #a3a3a3"></div><div style="background-color: #737373"></div><div style="background-color: #525252"></div><div style="background-color: #404040"></div><div style="background-color: #262626"></div><div style="background-color: #171717"></div><div style="background-color: #0a0a0a"></div></div>
|
||||
pub const NEUTRAL: Palette = Palette {
|
||||
c50: Color::from_u32(0xfafafa),
|
||||
c100: Color::from_u32(0xf5f5f5),
|
||||
c200: Color::from_u32(0xe5e5e5),
|
||||
c300: Color::from_u32(0xd4d4d4),
|
||||
c400: Color::from_u32(0xa3a3a3),
|
||||
c500: Color::from_u32(0x737373),
|
||||
c600: Color::from_u32(0x525252),
|
||||
c700: Color::from_u32(0x404040),
|
||||
c800: Color::from_u32(0x262626),
|
||||
c900: Color::from_u32(0x171717),
|
||||
c950: Color::from_u32(0x0a0a0a),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fafaf9"></div><div style="background-color: #f5f5f4"></div><div style="background-color: #e7e5e4"></div><div style="background-color: #d6d3d1"></div><div style="background-color: #a8a29e"></div><div style="background-color: #78716c"></div><div style="background-color: #57534e"></div><div style="background-color: #44403c"></div><div style="background-color: #292524"></div><div style="background-color: #1c1917"></div><div style="background-color: #0c0a09"></div></div>
|
||||
pub const STONE: Palette = Palette {
|
||||
c50: Color::from_u32(0xfafaf9),
|
||||
c100: Color::from_u32(0xf5f5f4),
|
||||
c200: Color::from_u32(0xe7e5e4),
|
||||
c300: Color::from_u32(0xd6d3d1),
|
||||
c400: Color::from_u32(0xa8a29e),
|
||||
c500: Color::from_u32(0x78716c),
|
||||
c600: Color::from_u32(0x57534e),
|
||||
c700: Color::from_u32(0x44403c),
|
||||
c800: Color::from_u32(0x292524),
|
||||
c900: Color::from_u32(0x1c1917),
|
||||
c950: Color::from_u32(0x0c0a09),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fef2f2"></div><div style="background-color: #fee2e2"></div><div style="background-color: #fecaca"></div><div style="background-color: #fca5a5"></div><div style="background-color: #f87171"></div><div style="background-color: #ef4444"></div><div style="background-color: #dc2626"></div><div style="background-color: #b91c1c"></div><div style="background-color: #991b1b"></div><div style="background-color: #7f1d1d"></div><div style="background-color: #450a0a"></div></div>
|
||||
pub const RED: Palette = Palette {
|
||||
c50: Color::from_u32(0xfef2f2),
|
||||
c100: Color::from_u32(0xfee2e2),
|
||||
c200: Color::from_u32(0xfecaca),
|
||||
c300: Color::from_u32(0xfca5a5),
|
||||
c400: Color::from_u32(0xf87171),
|
||||
c500: Color::from_u32(0xef4444),
|
||||
c600: Color::from_u32(0xdc2626),
|
||||
c700: Color::from_u32(0xb91c1c),
|
||||
c800: Color::from_u32(0x991b1b),
|
||||
c900: Color::from_u32(0x7f1d1d),
|
||||
c950: Color::from_u32(0x450a0a),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fff7ed"></div><div style="background-color: #ffedd5"></div><div style="background-color: #fed7aa"></div><div style="background-color: #fdba74"></div><div style="background-color: #fb923c"></div><div style="background-color: #f97316"></div><div style="background-color: #ea580c"></div><div style="background-color: #c2410c"></div><div style="background-color: #9a3412"></div><div style="background-color: #7c2d12"></div><div style="background-color: #431407"></div></div>
|
||||
pub const ORANGE: Palette = Palette {
|
||||
c50: Color::from_u32(0xfff7ed),
|
||||
c100: Color::from_u32(0xffedd5),
|
||||
c200: Color::from_u32(0xfed7aa),
|
||||
c300: Color::from_u32(0xfdba74),
|
||||
c400: Color::from_u32(0xfb923c),
|
||||
c500: Color::from_u32(0xf97316),
|
||||
c600: Color::from_u32(0xea580c),
|
||||
c700: Color::from_u32(0xc2410c),
|
||||
c800: Color::from_u32(0x9a3412),
|
||||
c900: Color::from_u32(0x7c2d12),
|
||||
c950: Color::from_u32(0x431407),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fffbeb"></div><div style="background-color: #fef3c7"></div><div style="background-color: #fde68a"></div><div style="background-color: #fcd34d"></div><div style="background-color: #fbbf24"></div><div style="background-color: #f59e0b"></div><div style="background-color: #d97706"></div><div style="background-color: #b45309"></div><div style="background-color: #92400e"></div><div style="background-color: #78350f"></div><div style="background-color: #451a03"></div></div>
|
||||
pub const AMBER: Palette = Palette {
|
||||
c50: Color::from_u32(0xfffbeb),
|
||||
c100: Color::from_u32(0xfef3c7),
|
||||
c200: Color::from_u32(0xfde68a),
|
||||
c300: Color::from_u32(0xfcd34d),
|
||||
c400: Color::from_u32(0xfbbf24),
|
||||
c500: Color::from_u32(0xf59e0b),
|
||||
c600: Color::from_u32(0xd97706),
|
||||
c700: Color::from_u32(0xb45309),
|
||||
c800: Color::from_u32(0x92400e),
|
||||
c900: Color::from_u32(0x78350f),
|
||||
c950: Color::from_u32(0x451a03),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fefce8"></div><div style="background-color: #fef9c3"></div><div style="background-color: #fef08a"></div><div style="background-color: #fde047"></div><div style="background-color: #facc15"></div><div style="background-color: #eab308"></div><div style="background-color: #ca8a04"></div><div style="background-color: #a16207"></div><div style="background-color: #854d0e"></div><div style="background-color: #713f12"></div><div style="background-color: #422006"></div></div>
|
||||
pub const YELLOW: Palette = Palette {
|
||||
c50: Color::from_u32(0xfefce8),
|
||||
c100: Color::from_u32(0xfef9c3),
|
||||
c200: Color::from_u32(0xfef08a),
|
||||
c300: Color::from_u32(0xfde047),
|
||||
c400: Color::from_u32(0xfacc15),
|
||||
c500: Color::from_u32(0xeab308),
|
||||
c600: Color::from_u32(0xca8a04),
|
||||
c700: Color::from_u32(0xa16207),
|
||||
c800: Color::from_u32(0x854d0e),
|
||||
c900: Color::from_u32(0x713f12),
|
||||
c950: Color::from_u32(0x422006),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f7fee7"></div><div style="background-color: #ecfccb"></div><div style="background-color: #d9f99d"></div><div style="background-color: #bef264"></div><div style="background-color: #a3e635"></div><div style="background-color: #84cc16"></div><div style="background-color: #65a30d"></div><div style="background-color: #4d7c0f"></div><div style="background-color: #3f6212"></div><div style="background-color: #365314"></div><div style="background-color: #1a2e05"></div></div>
|
||||
pub const LIME: Palette = Palette {
|
||||
c50: Color::from_u32(0xf7fee7),
|
||||
c100: Color::from_u32(0xecfccb),
|
||||
c200: Color::from_u32(0xd9f99d),
|
||||
c300: Color::from_u32(0xbef264),
|
||||
c400: Color::from_u32(0xa3e635),
|
||||
c500: Color::from_u32(0x84cc16),
|
||||
c600: Color::from_u32(0x65a30d),
|
||||
c700: Color::from_u32(0x4d7c0f),
|
||||
c800: Color::from_u32(0x3f6212),
|
||||
c900: Color::from_u32(0x365314),
|
||||
c950: Color::from_u32(0x1a2e05),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f0fdf4"></div><div style="background-color: #dcfce7"></div><div style="background-color: #bbf7d0"></div><div style="background-color: #86efac"></div><div style="background-color: #4ade80"></div><div style="background-color: #22c55e"></div><div style="background-color: #16a34a"></div><div style="background-color: #15803d"></div><div style="background-color: #166534"></div><div style="background-color: #14532d"></div><div style="background-color: #052e16"></div></div>
|
||||
pub const GREEN: Palette = Palette {
|
||||
c50: Color::from_u32(0xf0fdf4),
|
||||
c100: Color::from_u32(0xdcfce7),
|
||||
c200: Color::from_u32(0xbbf7d0),
|
||||
c300: Color::from_u32(0x86efac),
|
||||
c400: Color::from_u32(0x4ade80),
|
||||
c500: Color::from_u32(0x22c55e),
|
||||
c600: Color::from_u32(0x16a34a),
|
||||
c700: Color::from_u32(0x15803d),
|
||||
c800: Color::from_u32(0x166534),
|
||||
c900: Color::from_u32(0x14532d),
|
||||
c950: Color::from_u32(0x052e16),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f0fdfa"></div><div style="background-color: #ccfbf1"></div><div style="background-color: #99f6e4"></div><div style="background-color: #5eead4"></div><div style="background-color: #2dd4bf"></div><div style="background-color: #14b8a6"></div><div style="background-color: #0d9488"></div><div style="background-color: #0f766e"></div><div style="background-color: #115e59"></div><div style="background-color: #134e4a"></div><div style="background-color: #042f2e"></div></div>
|
||||
pub const EMERALD: Palette = Palette {
|
||||
c50: Color::from_u32(0xecfdf5),
|
||||
c100: Color::from_u32(0xd1fae5),
|
||||
c200: Color::from_u32(0xa7f3d0),
|
||||
c300: Color::from_u32(0x6ee7b7),
|
||||
c400: Color::from_u32(0x34d399),
|
||||
c500: Color::from_u32(0x10b981),
|
||||
c600: Color::from_u32(0x059669),
|
||||
c700: Color::from_u32(0x047857),
|
||||
c800: Color::from_u32(0x065f46),
|
||||
c900: Color::from_u32(0x064e3b),
|
||||
c950: Color::from_u32(0x022c22),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f5fdf4"></div><div style="background-color: #e7f9e7"></div><div style="background-color: #c6f6d5"></div><div style="background-color: #9ae6b4"></div><div style="background-color: #68d391"></div><div style="background-color: #48bb78"></div><div style="background-color: #38a169"></div><div style="background-color: #2f855a"></div><div style="background-color: #276749"></div><div style="background-color: #22543d"></div><div style="background-color: #0d3321"></div></div>
|
||||
pub const TEAL: Palette = Palette {
|
||||
c50: Color::from_u32(0xf0fdfa),
|
||||
c100: Color::from_u32(0xccfbf1),
|
||||
c200: Color::from_u32(0x99f6e4),
|
||||
c300: Color::from_u32(0x5eead4),
|
||||
c400: Color::from_u32(0x2dd4bf),
|
||||
c500: Color::from_u32(0x14b8a6),
|
||||
c600: Color::from_u32(0x0d9488),
|
||||
c700: Color::from_u32(0x0f766e),
|
||||
c800: Color::from_u32(0x115e59),
|
||||
c900: Color::from_u32(0x134e4a),
|
||||
c950: Color::from_u32(0x042f2e),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #ecfeff"></div><div style="background-color: #cffafe"></div><div style="background-color: #a5f3fc"></div><div style="background-color: #67e8f9"></div><div style="background-color: #22d3ee"></div><div style="background-color: #06b6d4"></div><div style="background-color: #0891b2"></div><div style="background-color: #0e7490"></div><div style="background-color: #155e75"></div><div style="background-color: #164e63"></div><div style="background-color: #083344"></div></div>
|
||||
pub const CYAN: Palette = Palette {
|
||||
c50: Color::from_u32(0xecfeff),
|
||||
c100: Color::from_u32(0xcffafe),
|
||||
c200: Color::from_u32(0xa5f3fc),
|
||||
c300: Color::from_u32(0x67e8f9),
|
||||
c400: Color::from_u32(0x22d3ee),
|
||||
c500: Color::from_u32(0x06b6d4),
|
||||
c600: Color::from_u32(0x0891b2),
|
||||
c700: Color::from_u32(0x0e7490),
|
||||
c800: Color::from_u32(0x155e75),
|
||||
c900: Color::from_u32(0x164e63),
|
||||
c950: Color::from_u32(0x083344),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f0f9ff"></div><div style="background-color: #e0f2fe"></div><div style="background-color: #bae6fd"></div><div style="background-color: #7dd3fc"></div><div style="background-color: #38bdf8"></div><div style="background-color: #0ea5e9"></div><div style="background-color: #0284c7"></div><div style="background-color: #0369a1"></div><div style="background-color: #075985"></div><div style="background-color: #0c4a6e"></div><div style="background-color: #082f49"></div></div>
|
||||
pub const SKY: Palette = Palette {
|
||||
c50: Color::from_u32(0xf0f9ff),
|
||||
c100: Color::from_u32(0xe0f2fe),
|
||||
c200: Color::from_u32(0xbae6fd),
|
||||
c300: Color::from_u32(0x7dd3fc),
|
||||
c400: Color::from_u32(0x38bdf8),
|
||||
c500: Color::from_u32(0x0ea5e9),
|
||||
c600: Color::from_u32(0x0284c7),
|
||||
c700: Color::from_u32(0x0369a1),
|
||||
c800: Color::from_u32(0x075985),
|
||||
c900: Color::from_u32(0x0c4a6e),
|
||||
c950: Color::from_u32(0x082f49),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #eff6ff"></div><div style="background-color: #dbeafe"></div><div style="background-color: #bfdbfe"></div><div style="background-color: #93c5fd"></div><div style="background-color: #60a5fa"></div><div style="background-color: #3b82f6"></div><div style="background-color: #2563eb"></div><div style="background-color: #1d4ed8"></div><div style="background-color: #1e40af"></div><div style="background-color: #1e3a8a"></div><div style="background-color: #172554"></div></div>
|
||||
pub const BLUE: Palette = Palette {
|
||||
c50: Color::from_u32(0xeff6ff),
|
||||
c100: Color::from_u32(0xdbeafe),
|
||||
c200: Color::from_u32(0xbfdbfe),
|
||||
c300: Color::from_u32(0x93c5fd),
|
||||
c400: Color::from_u32(0x60a5fa),
|
||||
c500: Color::from_u32(0x3b82f6),
|
||||
c600: Color::from_u32(0x2563eb),
|
||||
c700: Color::from_u32(0x1d4ed8),
|
||||
c800: Color::from_u32(0x1e40af),
|
||||
c900: Color::from_u32(0x1e3a8a),
|
||||
c950: Color::from_u32(0x172554),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #eef2ff"></div><div style="background-color: #e0e7ff"></div><div style="background-color: #c7d2fe"></div><div style="background-color: #a5b4fc"></div><div style="background-color: #818cf8"></div><div style="background-color: #6366f1"></div><div style="background-color: #4f46e5"></div><div style="background-color: #4338ca"></div><div style="background-color: #3730a3"></div><div style="background-color: #312e81"></div><div style="background-color: #1e1b4b"></div></div>
|
||||
pub const INDIGO: Palette = Palette {
|
||||
c50: Color::from_u32(0xeef2ff),
|
||||
c100: Color::from_u32(0xe0e7ff),
|
||||
c200: Color::from_u32(0xc7d2fe),
|
||||
c300: Color::from_u32(0xa5b4fc),
|
||||
c400: Color::from_u32(0x818cf8),
|
||||
c500: Color::from_u32(0x6366f1),
|
||||
c600: Color::from_u32(0x4f46e5),
|
||||
c700: Color::from_u32(0x4338ca),
|
||||
c800: Color::from_u32(0x3730a3),
|
||||
c900: Color::from_u32(0x312e81),
|
||||
c950: Color::from_u32(0x1e1b4b),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f5f3ff"></div><div style="background-color: #ede9fe"></div><div style="background-color: #ddd6fe"></div><div style="background-color: #c4b5fd"></div><div style="background-color: #a78bfa"></div><div style="background-color: #8b5cf6"></div><div style="background-color: #7c3aed"></div><div style="background-color: #6d28d9"></div><div style="background-color: #5b21b6"></div><div style="background-color: #4c1d95"></div><div style="background-color: #2e1065"></div></div>
|
||||
pub const VIOLET: Palette = Palette {
|
||||
c50: Color::from_u32(0xf5f3ff),
|
||||
c100: Color::from_u32(0xede9fe),
|
||||
c200: Color::from_u32(0xddd6fe),
|
||||
c300: Color::from_u32(0xc4b5fd),
|
||||
c400: Color::from_u32(0xa78bfa),
|
||||
c500: Color::from_u32(0x8b5cf6),
|
||||
c600: Color::from_u32(0x7c3aed),
|
||||
c700: Color::from_u32(0x6d28d9),
|
||||
c800: Color::from_u32(0x5b21b6),
|
||||
c900: Color::from_u32(0x4c1d95),
|
||||
c950: Color::from_u32(0x2e1065),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #faf5ff"></div><div style="background-color: #f3e8ff"></div><div style="background-color: #e9d5ff"></div><div style="background-color: #d8b4fe"></div><div style="background-color: #c084fc"></div><div style="background-color: #a855f7"></div><div style="background-color: #9333ea"></div><div style="background-color: #7e22ce"></div><div style="background-color: #6b21a8"></div><div style="background-color: #581c87"></div><div style="background-color: #3b0764"></div></div>
|
||||
pub const PURPLE: Palette = Palette {
|
||||
c50: Color::from_u32(0xfaf5ff),
|
||||
c100: Color::from_u32(0xf3e8ff),
|
||||
c200: Color::from_u32(0xe9d5ff),
|
||||
c300: Color::from_u32(0xd8b4fe),
|
||||
c400: Color::from_u32(0xc084fc),
|
||||
c500: Color::from_u32(0xa855f7),
|
||||
c600: Color::from_u32(0x9333ea),
|
||||
c700: Color::from_u32(0x7e22ce),
|
||||
c800: Color::from_u32(0x6b21a8),
|
||||
c900: Color::from_u32(0x581c87),
|
||||
c950: Color::from_u32(0x3b0764),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fdf4ff"></div><div style="background-color: #fae8ff"></div><div style="background-color: #f5d0fe"></div><div style="background-color: #f0abfc"></div><div style="background-color: #e879f9"></div><div style="background-color: #d946ef"></div><div style="background-color: #c026d3"></div><div style="background-color: #a21caf"></div><div style="background-color: #86198f"></div><div style="background-color: #701a75"></div><div style="background-color: #4a044e"></div></div>
|
||||
pub const FUCHSIA: Palette = Palette {
|
||||
c50: Color::from_u32(0xfdf4ff),
|
||||
c100: Color::from_u32(0xfae8ff),
|
||||
c200: Color::from_u32(0xf5d0fe),
|
||||
c300: Color::from_u32(0xf0abfc),
|
||||
c400: Color::from_u32(0xe879f9),
|
||||
c500: Color::from_u32(0xd946ef),
|
||||
c600: Color::from_u32(0xc026d3),
|
||||
c700: Color::from_u32(0xa21caf),
|
||||
c800: Color::from_u32(0x86198f),
|
||||
c900: Color::from_u32(0x701a75),
|
||||
c950: Color::from_u32(0x4a044e),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fdf2f8"></div><div style="background-color: #fce7f3"></div><div style="background-color: #fbcfe8"></div><div style="background-color: #f9a8d4"></div><div style="background-color: #f472b6"></div><div style="background-color: #ec4899"></div><div style="background-color: #db2777"></div><div style="background-color: #be185d"></div><div style="background-color: #9d174d"></div><div style="background-color: #831843"></div><div style="background-color: #500724"></div></div>
|
||||
pub const PINK: Palette = Palette {
|
||||
c50: Color::from_u32(0xfdf2f8),
|
||||
c100: Color::from_u32(0xfce7f3),
|
||||
c200: Color::from_u32(0xfbcfe8),
|
||||
c300: Color::from_u32(0xf9a8d4),
|
||||
c400: Color::from_u32(0xf472b6),
|
||||
c500: Color::from_u32(0xec4899),
|
||||
c600: Color::from_u32(0xdb2777),
|
||||
c700: Color::from_u32(0xbe185d),
|
||||
c800: Color::from_u32(0x9d174d),
|
||||
c900: Color::from_u32(0x831843),
|
||||
c950: Color::from_u32(0x500724),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fff1f2"></div><div style="background-color: #ffe4e6"></div><div style="background-color: #fecdd3"></div><div style="background-color: #fda4af"></div><div style="background-color: #fb7185"></div><div style="background-color: #f43f5e"></div><div style="background-color: #e11d48"></div><div style="background-color: #be123c"></div><div style="background-color: #9f1239"></div><div style="background-color: #881337"></div><div style="background-color: #4c0519"></div></div>
|
||||
pub const ROSE: Palette = Palette {
|
||||
c50: Color::from_u32(0xfff1f2),
|
||||
c100: Color::from_u32(0xffe4e6),
|
||||
c200: Color::from_u32(0xfecdd3),
|
||||
c300: Color::from_u32(0xfda4af),
|
||||
c400: Color::from_u32(0xfb7185),
|
||||
c500: Color::from_u32(0xf43f5e),
|
||||
c600: Color::from_u32(0xe11d48),
|
||||
c700: Color::from_u32(0xbe123c),
|
||||
c800: Color::from_u32(0x9f1239),
|
||||
c900: Color::from_u32(0x881337),
|
||||
c950: Color::from_u32(0x4c0519),
|
||||
};
|
||||
83
ratatui-core/src/style/palette_conversion.rs
Normal file
83
ratatui-core/src/style/palette_conversion.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
//! Conversions from colors in the `palette` crate to [`Color`].
|
||||
|
||||
use ::palette::{
|
||||
bool_mask::LazySelect,
|
||||
num::{Arithmetics, MulSub, PartialCmp, Powf, Real},
|
||||
LinSrgb,
|
||||
};
|
||||
use palette::{stimulus::IntoStimulus, Srgb};
|
||||
|
||||
use super::Color;
|
||||
|
||||
/// Convert an [`palette::Srgb`] color to a [`Color`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use palette::Srgb;
|
||||
/// use ratatui::style::Color;
|
||||
///
|
||||
/// let color = Color::from(Srgb::new(1.0f32, 0.0, 0.0));
|
||||
/// assert_eq!(color, Color::Rgb(255, 0, 0));
|
||||
/// ```
|
||||
impl<T: IntoStimulus<u8>> From<Srgb<T>> for Color {
|
||||
fn from(color: Srgb<T>) -> Self {
|
||||
let (red, green, blue) = color.into_format().into_components();
|
||||
Self::Rgb(red, green, blue)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a [`palette::LinSrgb`] color to a [`Color`].
|
||||
///
|
||||
/// Note: this conversion only works for floating point linear sRGB colors. If you have a linear
|
||||
/// sRGB color in another format, you need to convert it to floating point first.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use palette::LinSrgb;
|
||||
/// use ratatui::style::Color;
|
||||
///
|
||||
/// let color = Color::from(LinSrgb::new(1.0f32, 0.0, 0.0));
|
||||
/// assert_eq!(color, Color::Rgb(255, 0, 0));
|
||||
/// ```
|
||||
impl<T: IntoStimulus<u8>> From<LinSrgb<T>> for Color
|
||||
where
|
||||
T: Real + Powf + MulSub + Arithmetics + PartialCmp + Clone,
|
||||
T::Mask: LazySelect<T>,
|
||||
{
|
||||
fn from(color: LinSrgb<T>) -> Self {
|
||||
let srgb_color = Srgb::<T>::from_linear(color);
|
||||
Self::from(srgb_color)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn from_srgb() {
|
||||
const RED: Color = Color::Rgb(255, 0, 0);
|
||||
assert_eq!(Color::from(Srgb::new(255u8, 0, 0)), RED);
|
||||
assert_eq!(Color::from(Srgb::new(65535u16, 0, 0)), RED);
|
||||
assert_eq!(Color::from(Srgb::new(1.0f32, 0.0, 0.0)), RED);
|
||||
|
||||
assert_eq!(
|
||||
Color::from(Srgb::new(0.5f32, 0.5, 0.5)),
|
||||
Color::Rgb(128, 128, 128)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_lin_srgb() {
|
||||
const RED: Color = Color::Rgb(255, 0, 0);
|
||||
assert_eq!(Color::from(LinSrgb::new(1.0f32, 0.0, 0.0)), RED);
|
||||
assert_eq!(Color::from(LinSrgb::new(1.0f64, 0.0, 0.0)), RED);
|
||||
|
||||
assert_eq!(
|
||||
Color::from(LinSrgb::new(0.5f32, 0.5, 0.5)),
|
||||
Color::Rgb(188, 188, 188)
|
||||
);
|
||||
}
|
||||
}
|
||||
576
ratatui-core/src/style/stylize.rs
Normal file
576
ratatui-core/src/style/stylize.rs
Normal file
@@ -0,0 +1,576 @@
|
||||
use std::fmt;
|
||||
|
||||
use paste::paste;
|
||||
|
||||
use crate::{
|
||||
style::{Color, Modifier, Style},
|
||||
text::Span,
|
||||
};
|
||||
|
||||
/// A trait for objects that have a `Style`.
|
||||
///
|
||||
/// This trait enables generic code to be written that can interact with any object that has a
|
||||
/// `Style`. This is used by the `Stylize` trait to allow generic code to be written that can
|
||||
/// interact with any object that can be styled.
|
||||
pub trait Styled {
|
||||
type Item;
|
||||
|
||||
/// Returns the style of the object.
|
||||
fn style(&self) -> Style;
|
||||
|
||||
/// Sets the style of the object.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item;
|
||||
}
|
||||
|
||||
/// A helper struct to make it easy to debug using the `Stylize` method names
|
||||
pub(crate) struct ColorDebug {
|
||||
pub kind: ColorDebugKind,
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub(crate) enum ColorDebugKind {
|
||||
Foreground,
|
||||
Background,
|
||||
#[cfg(feature = "underline-color")]
|
||||
Underline,
|
||||
}
|
||||
|
||||
impl fmt::Debug for ColorDebug {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
#[cfg(feature = "underline-color")]
|
||||
let is_underline = self.kind == ColorDebugKind::Underline;
|
||||
#[cfg(not(feature = "underline-color"))]
|
||||
let is_underline = false;
|
||||
if is_underline
|
||||
|| matches!(
|
||||
self.color,
|
||||
Color::Reset | Color::Indexed(_) | Color::Rgb(_, _, _)
|
||||
)
|
||||
{
|
||||
match self.kind {
|
||||
ColorDebugKind::Foreground => write!(f, ".fg(")?,
|
||||
ColorDebugKind::Background => write!(f, ".bg(")?,
|
||||
#[cfg(feature = "underline-color")]
|
||||
ColorDebugKind::Underline => write!(f, ".underline_color(")?,
|
||||
}
|
||||
write!(f, "Color::{:?}", self.color)?;
|
||||
write!(f, ")")?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match self.kind {
|
||||
ColorDebugKind::Foreground => write!(f, ".")?,
|
||||
ColorDebugKind::Background => write!(f, ".on_")?,
|
||||
// TODO: .underline_color_xxx is not implemented on Stylize yet, but it should be
|
||||
#[cfg(feature = "underline-color")]
|
||||
ColorDebugKind::Underline => {
|
||||
unreachable!("covered by the first part of the if statement")
|
||||
}
|
||||
}
|
||||
match self.color {
|
||||
Color::Black => write!(f, "black")?,
|
||||
Color::Red => write!(f, "red")?,
|
||||
Color::Green => write!(f, "green")?,
|
||||
Color::Yellow => write!(f, "yellow")?,
|
||||
Color::Blue => write!(f, "blue")?,
|
||||
Color::Magenta => write!(f, "magenta")?,
|
||||
Color::Cyan => write!(f, "cyan")?,
|
||||
Color::Gray => write!(f, "gray")?,
|
||||
Color::DarkGray => write!(f, "dark_gray")?,
|
||||
Color::LightRed => write!(f, "light_red")?,
|
||||
Color::LightGreen => write!(f, "light_green")?,
|
||||
Color::LightYellow => write!(f, "light_yellow")?,
|
||||
Color::LightBlue => write!(f, "light_blue")?,
|
||||
Color::LightMagenta => write!(f, "light_magenta")?,
|
||||
Color::LightCyan => write!(f, "light_cyan")?,
|
||||
Color::White => write!(f, "white")?,
|
||||
_ => unreachable!("covered by the first part of the if statement"),
|
||||
}
|
||||
write!(f, "()")
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates two methods for each color, one for setting the foreground color (`red()`, `blue()`,
|
||||
/// etc) and one for setting the background color (`on_red()`, `on_blue()`, etc.). Each method sets
|
||||
/// the color of the style to the corresponding color.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// color!(black);
|
||||
///
|
||||
/// // generates
|
||||
///
|
||||
/// #[doc = "Sets the foreground color to [`black`](Color::Black)."]
|
||||
/// fn black(self) -> T {
|
||||
/// self.fg(Color::Black)
|
||||
/// }
|
||||
///
|
||||
/// #[doc = "Sets the background color to [`black`](Color::Black)."]
|
||||
/// fn on_black(self) -> T {
|
||||
/// self.bg(Color::Black)
|
||||
/// }
|
||||
/// ```
|
||||
macro_rules! color {
|
||||
( $color:ident ) => {
|
||||
paste! {
|
||||
#[doc = "Sets the foreground color to [`" $color "`](Color::" $color:camel ")."]
|
||||
#[must_use = concat!("`", stringify!($color), "` returns the modified style without modifying the original")]
|
||||
fn $color(self) -> T {
|
||||
self.fg(Color::[<$color:camel>])
|
||||
}
|
||||
|
||||
#[doc = "Sets the background color to [`" $color "`](Color::" $color:camel ")."]
|
||||
#[must_use = concat!("`on_", stringify!($color), "` returns the modified style without modifying the original")]
|
||||
fn [<on_ $color>](self) -> T {
|
||||
self.bg(Color::[<$color:camel>])
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Generates a method for a modifier (`bold()`, `italic()`, etc.). Each method sets the modifier
|
||||
/// of the style to the corresponding modifier.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// modifier!(bold);
|
||||
///
|
||||
/// // generates
|
||||
///
|
||||
/// #[doc = "Adds the [`BOLD`](Modifier::BOLD) modifier."]
|
||||
/// fn bold(self) -> T {
|
||||
/// self.add_modifier(Modifier::BOLD)
|
||||
/// }
|
||||
///
|
||||
/// #[doc = "Removes the [`BOLD`](Modifier::BOLD) modifier."]
|
||||
/// fn not_bold(self) -> T {
|
||||
/// self.remove_modifier(Modifier::BOLD)
|
||||
/// }
|
||||
/// ```
|
||||
macro_rules! modifier {
|
||||
( $modifier:ident ) => {
|
||||
paste! {
|
||||
#[doc = "Adds the [`" $modifier:upper "`](Modifier::" $modifier:upper ") modifier."]
|
||||
#[must_use = concat!("`", stringify!($modifier), "` returns the modified style without modifying the original")]
|
||||
fn [<$modifier>](self) -> T {
|
||||
self.add_modifier(Modifier::[<$modifier:upper>])
|
||||
}
|
||||
}
|
||||
|
||||
paste! {
|
||||
#[doc = "Removes the [`" $modifier:upper "`](Modifier::" $modifier:upper ") modifier."]
|
||||
#[must_use = concat!("`not_", stringify!($modifier), "` returns the modified style without modifying the original")]
|
||||
fn [<not_ $modifier>](self) -> T {
|
||||
self.remove_modifier(Modifier::[<$modifier:upper>])
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// An extension trait for styling objects.
|
||||
///
|
||||
/// For any type that implements `Stylize`, the provided methods in this trait can be used to style
|
||||
/// the type further. This trait is automatically implemented for any type that implements the
|
||||
/// [`Styled`] trait which e.g.: [`String`], [`&str`], [`Span`], [`Style`] and many Widget types.
|
||||
///
|
||||
/// This results in much more ergonomic styling of text and widgets. For example, instead of
|
||||
/// writing:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// let text = Span::styled("Hello", Style::default().fg(Color::Red).bg(Color::Blue));
|
||||
/// ```
|
||||
///
|
||||
/// You can write:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// let text = "Hello".red().on_blue();
|
||||
/// ```
|
||||
///
|
||||
/// This trait implements a provided method for every color as both foreground and background
|
||||
/// (prefixed by `on_`), and all modifiers as both an additive and subtractive modifier (prefixed
|
||||
/// by `not_`). The `reset()` method is also provided to reset the style.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
///
|
||||
/// let span = "hello".red().on_blue().bold();
|
||||
/// let line = Line::from(vec![
|
||||
/// "hello".red().on_blue().bold(),
|
||||
/// "world".green().on_yellow().not_bold(),
|
||||
/// ]);
|
||||
/// let paragraph = Paragraph::new(line).italic().underlined();
|
||||
/// let block = Block::bordered().title("Title").on_white().bold();
|
||||
/// ```
|
||||
pub trait Stylize<'a, T>: Sized {
|
||||
#[must_use = "`bg` returns the modified style without modifying the original"]
|
||||
fn bg<C: Into<Color>>(self, color: C) -> T;
|
||||
#[must_use = "`fg` returns the modified style without modifying the original"]
|
||||
fn fg<C: Into<Color>>(self, color: C) -> T;
|
||||
#[must_use = "`reset` returns the modified style without modifying the original"]
|
||||
fn reset(self) -> T;
|
||||
#[must_use = "`add_modifier` returns the modified style without modifying the original"]
|
||||
fn add_modifier(self, modifier: Modifier) -> T;
|
||||
#[must_use = "`remove_modifier` returns the modified style without modifying the original"]
|
||||
fn remove_modifier(self, modifier: Modifier) -> T;
|
||||
|
||||
color!(black);
|
||||
color!(red);
|
||||
color!(green);
|
||||
color!(yellow);
|
||||
color!(blue);
|
||||
color!(magenta);
|
||||
color!(cyan);
|
||||
color!(gray);
|
||||
color!(dark_gray);
|
||||
color!(light_red);
|
||||
color!(light_green);
|
||||
color!(light_yellow);
|
||||
color!(light_blue);
|
||||
color!(light_magenta);
|
||||
color!(light_cyan);
|
||||
color!(white);
|
||||
|
||||
modifier!(bold);
|
||||
modifier!(dim);
|
||||
modifier!(italic);
|
||||
modifier!(underlined);
|
||||
modifier!(slow_blink);
|
||||
modifier!(rapid_blink);
|
||||
modifier!(reversed);
|
||||
modifier!(hidden);
|
||||
modifier!(crossed_out);
|
||||
}
|
||||
|
||||
impl<'a, T, U> Stylize<'a, T> for U
|
||||
where
|
||||
U: Styled<Item = T>,
|
||||
{
|
||||
fn bg<C: Into<Color>>(self, color: C) -> T {
|
||||
let style = self.style().bg(color.into());
|
||||
self.set_style(style)
|
||||
}
|
||||
|
||||
fn fg<C: Into<Color>>(self, color: C) -> T {
|
||||
let style = self.style().fg(color.into());
|
||||
self.set_style(style)
|
||||
}
|
||||
|
||||
fn add_modifier(self, modifier: Modifier) -> T {
|
||||
let style = self.style().add_modifier(modifier);
|
||||
self.set_style(style)
|
||||
}
|
||||
|
||||
fn remove_modifier(self, modifier: Modifier) -> T {
|
||||
let style = self.style().remove_modifier(modifier);
|
||||
self.set_style(style)
|
||||
}
|
||||
|
||||
fn reset(self) -> T {
|
||||
self.set_style(Style::reset())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Styled for &'a str {
|
||||
type Item = Span<'a>;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
Style::default()
|
||||
}
|
||||
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
Span::styled(self, style)
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for String {
|
||||
type Item = Span<'static>;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
Style::default()
|
||||
}
|
||||
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
Span::styled(self, style)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use itertools::Itertools;
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn str_styled() {
|
||||
assert_eq!("hello".style(), Style::default());
|
||||
assert_eq!(
|
||||
"hello".set_style(Style::new().cyan()),
|
||||
Span::styled("hello", Style::new().cyan())
|
||||
);
|
||||
assert_eq!("hello".black(), Span::from("hello").black());
|
||||
assert_eq!("hello".red(), Span::from("hello").red());
|
||||
assert_eq!("hello".green(), Span::from("hello").green());
|
||||
assert_eq!("hello".yellow(), Span::from("hello").yellow());
|
||||
assert_eq!("hello".blue(), Span::from("hello").blue());
|
||||
assert_eq!("hello".magenta(), Span::from("hello").magenta());
|
||||
assert_eq!("hello".cyan(), Span::from("hello").cyan());
|
||||
assert_eq!("hello".gray(), Span::from("hello").gray());
|
||||
assert_eq!("hello".dark_gray(), Span::from("hello").dark_gray());
|
||||
assert_eq!("hello".light_red(), Span::from("hello").light_red());
|
||||
assert_eq!("hello".light_green(), Span::from("hello").light_green());
|
||||
assert_eq!("hello".light_yellow(), Span::from("hello").light_yellow());
|
||||
assert_eq!("hello".light_blue(), Span::from("hello").light_blue());
|
||||
assert_eq!("hello".light_magenta(), Span::from("hello").light_magenta());
|
||||
assert_eq!("hello".light_cyan(), Span::from("hello").light_cyan());
|
||||
assert_eq!("hello".white(), Span::from("hello").white());
|
||||
|
||||
assert_eq!("hello".on_black(), Span::from("hello").on_black());
|
||||
assert_eq!("hello".on_red(), Span::from("hello").on_red());
|
||||
assert_eq!("hello".on_green(), Span::from("hello").on_green());
|
||||
assert_eq!("hello".on_yellow(), Span::from("hello").on_yellow());
|
||||
assert_eq!("hello".on_blue(), Span::from("hello").on_blue());
|
||||
assert_eq!("hello".on_magenta(), Span::from("hello").on_magenta());
|
||||
assert_eq!("hello".on_cyan(), Span::from("hello").on_cyan());
|
||||
assert_eq!("hello".on_gray(), Span::from("hello").on_gray());
|
||||
assert_eq!("hello".on_dark_gray(), Span::from("hello").on_dark_gray());
|
||||
assert_eq!("hello".on_light_red(), Span::from("hello").on_light_red());
|
||||
assert_eq!(
|
||||
"hello".on_light_green(),
|
||||
Span::from("hello").on_light_green()
|
||||
);
|
||||
assert_eq!(
|
||||
"hello".on_light_yellow(),
|
||||
Span::from("hello").on_light_yellow()
|
||||
);
|
||||
assert_eq!("hello".on_light_blue(), Span::from("hello").on_light_blue());
|
||||
assert_eq!(
|
||||
"hello".on_light_magenta(),
|
||||
Span::from("hello").on_light_magenta()
|
||||
);
|
||||
assert_eq!("hello".on_light_cyan(), Span::from("hello").on_light_cyan());
|
||||
assert_eq!("hello".on_white(), Span::from("hello").on_white());
|
||||
|
||||
assert_eq!("hello".bold(), Span::from("hello").bold());
|
||||
assert_eq!("hello".dim(), Span::from("hello").dim());
|
||||
assert_eq!("hello".italic(), Span::from("hello").italic());
|
||||
assert_eq!("hello".underlined(), Span::from("hello").underlined());
|
||||
assert_eq!("hello".slow_blink(), Span::from("hello").slow_blink());
|
||||
assert_eq!("hello".rapid_blink(), Span::from("hello").rapid_blink());
|
||||
assert_eq!("hello".reversed(), Span::from("hello").reversed());
|
||||
assert_eq!("hello".hidden(), Span::from("hello").hidden());
|
||||
assert_eq!("hello".crossed_out(), Span::from("hello").crossed_out());
|
||||
|
||||
assert_eq!("hello".not_bold(), Span::from("hello").not_bold());
|
||||
assert_eq!("hello".not_dim(), Span::from("hello").not_dim());
|
||||
assert_eq!("hello".not_italic(), Span::from("hello").not_italic());
|
||||
assert_eq!(
|
||||
"hello".not_underlined(),
|
||||
Span::from("hello").not_underlined()
|
||||
);
|
||||
assert_eq!(
|
||||
"hello".not_slow_blink(),
|
||||
Span::from("hello").not_slow_blink()
|
||||
);
|
||||
assert_eq!(
|
||||
"hello".not_rapid_blink(),
|
||||
Span::from("hello").not_rapid_blink()
|
||||
);
|
||||
assert_eq!("hello".not_reversed(), Span::from("hello").not_reversed());
|
||||
assert_eq!("hello".not_hidden(), Span::from("hello").not_hidden());
|
||||
assert_eq!(
|
||||
"hello".not_crossed_out(),
|
||||
Span::from("hello").not_crossed_out()
|
||||
);
|
||||
|
||||
assert_eq!("hello".reset(), Span::from("hello").reset());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_styled() {
|
||||
let s = String::from("hello");
|
||||
assert_eq!(s.style(), Style::default());
|
||||
assert_eq!(
|
||||
s.clone().set_style(Style::new().cyan()),
|
||||
Span::styled("hello", Style::new().cyan())
|
||||
);
|
||||
assert_eq!(s.clone().black(), Span::from("hello").black());
|
||||
assert_eq!(s.clone().on_black(), Span::from("hello").on_black());
|
||||
assert_eq!(s.clone().bold(), Span::from("hello").bold());
|
||||
assert_eq!(s.clone().not_bold(), Span::from("hello").not_bold());
|
||||
assert_eq!(s.clone().reset(), Span::from("hello").reset());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn temporary_string_styled() {
|
||||
// to_string() is used to create a temporary String, which is then styled. Without the
|
||||
// `Styled` trait impl for `String`, this would fail to compile with the error: "temporary
|
||||
// value dropped while borrowed"
|
||||
let s = "hello".to_string().red();
|
||||
assert_eq!(s, Span::from("hello").red());
|
||||
|
||||
// format!() is used to create a temporary String inside a closure, which suffers the same
|
||||
// issue as above without the `Styled` trait impl for `String`
|
||||
let items = [String::from("a"), String::from("b")];
|
||||
let sss = items.iter().map(|s| format!("{s}{s}").red()).collect_vec();
|
||||
assert_eq!(sss, [Span::from("aa").red(), Span::from("bb").red()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset() {
|
||||
assert_eq!(
|
||||
"hello".on_cyan().light_red().bold().underlined().reset(),
|
||||
Span::styled("hello", Style::reset())
|
||||
);
|
||||
}
|
||||
|
||||
#[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 bg = Style::default().bg(Color::Cyan);
|
||||
let 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", bg));
|
||||
assert_eq!("hello".red().cyan(), Span::styled("hello", 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()
|
||||
.underlined()
|
||||
.dim()
|
||||
.slow_blink()
|
||||
.crossed_out()
|
||||
.reversed(),
|
||||
Span::styled("hello", all_modifier_black)
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(ColorDebugKind::Foreground, Color::Black, ".black()")]
|
||||
#[case(ColorDebugKind::Foreground, Color::Red, ".red()")]
|
||||
#[case(ColorDebugKind::Foreground, Color::Green, ".green()")]
|
||||
#[case(ColorDebugKind::Foreground, Color::Yellow, ".yellow()")]
|
||||
#[case(ColorDebugKind::Foreground, Color::Blue, ".blue()")]
|
||||
#[case(ColorDebugKind::Foreground, Color::Magenta, ".magenta()")]
|
||||
#[case(ColorDebugKind::Foreground, Color::Cyan, ".cyan()")]
|
||||
#[case(ColorDebugKind::Foreground, Color::Gray, ".gray()")]
|
||||
#[case(ColorDebugKind::Foreground, Color::DarkGray, ".dark_gray()")]
|
||||
#[case(ColorDebugKind::Foreground, Color::LightRed, ".light_red()")]
|
||||
#[case(ColorDebugKind::Foreground, Color::LightGreen, ".light_green()")]
|
||||
#[case(ColorDebugKind::Foreground, Color::LightYellow, ".light_yellow()")]
|
||||
#[case(ColorDebugKind::Foreground, Color::LightBlue, ".light_blue()")]
|
||||
#[case(ColorDebugKind::Foreground, Color::LightMagenta, ".light_magenta()")]
|
||||
#[case(ColorDebugKind::Foreground, Color::LightCyan, ".light_cyan()")]
|
||||
#[case(ColorDebugKind::Foreground, Color::White, ".white()")]
|
||||
#[case(
|
||||
ColorDebugKind::Foreground,
|
||||
Color::Indexed(10),
|
||||
".fg(Color::Indexed(10))"
|
||||
)]
|
||||
#[case(
|
||||
ColorDebugKind::Foreground,
|
||||
Color::Rgb(255, 0, 0),
|
||||
".fg(Color::Rgb(255, 0, 0))"
|
||||
)]
|
||||
#[case(ColorDebugKind::Background, Color::Black, ".on_black()")]
|
||||
#[case(ColorDebugKind::Background, Color::Red, ".on_red()")]
|
||||
#[case(ColorDebugKind::Background, Color::Green, ".on_green()")]
|
||||
#[case(ColorDebugKind::Background, Color::Yellow, ".on_yellow()")]
|
||||
#[case(ColorDebugKind::Background, Color::Blue, ".on_blue()")]
|
||||
#[case(ColorDebugKind::Background, Color::Magenta, ".on_magenta()")]
|
||||
#[case(ColorDebugKind::Background, Color::Cyan, ".on_cyan()")]
|
||||
#[case(ColorDebugKind::Background, Color::Gray, ".on_gray()")]
|
||||
#[case(ColorDebugKind::Background, Color::DarkGray, ".on_dark_gray()")]
|
||||
#[case(ColorDebugKind::Background, Color::LightRed, ".on_light_red()")]
|
||||
#[case(ColorDebugKind::Background, Color::LightGreen, ".on_light_green()")]
|
||||
#[case(ColorDebugKind::Background, Color::LightYellow, ".on_light_yellow()")]
|
||||
#[case(ColorDebugKind::Background, Color::LightBlue, ".on_light_blue()")]
|
||||
#[case(ColorDebugKind::Background, Color::LightMagenta, ".on_light_magenta()")]
|
||||
#[case(ColorDebugKind::Background, Color::LightCyan, ".on_light_cyan()")]
|
||||
#[case(ColorDebugKind::Background, Color::White, ".on_white()")]
|
||||
#[case(
|
||||
ColorDebugKind::Background,
|
||||
Color::Indexed(10),
|
||||
".bg(Color::Indexed(10))"
|
||||
)]
|
||||
#[case(
|
||||
ColorDebugKind::Background,
|
||||
Color::Rgb(255, 0, 0),
|
||||
".bg(Color::Rgb(255, 0, 0))"
|
||||
)]
|
||||
#[cfg(feature = "underline-color")]
|
||||
#[case(
|
||||
ColorDebugKind::Underline,
|
||||
Color::Black,
|
||||
".underline_color(Color::Black)"
|
||||
)]
|
||||
#[cfg(feature = "underline-color")]
|
||||
#[case(ColorDebugKind::Underline, Color::Red, ".underline_color(Color::Red)")]
|
||||
#[cfg(feature = "underline-color")]
|
||||
#[case(
|
||||
ColorDebugKind::Underline,
|
||||
Color::Green,
|
||||
".underline_color(Color::Green)"
|
||||
)]
|
||||
#[cfg(feature = "underline-color")]
|
||||
#[case(
|
||||
ColorDebugKind::Underline,
|
||||
Color::Yellow,
|
||||
".underline_color(Color::Yellow)"
|
||||
)]
|
||||
fn stylize_debug(#[case] kind: ColorDebugKind, #[case] color: Color, #[case] expected: &str) {
|
||||
let debug = color.stylize_debug(kind);
|
||||
assert_eq!(format!("{debug:?}"), expected);
|
||||
}
|
||||
}
|
||||
228
ratatui-core/src/symbols.rs
Normal file
228
ratatui-core/src/symbols.rs
Normal file
@@ -0,0 +1,228 @@
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
pub mod border;
|
||||
pub mod line;
|
||||
|
||||
pub mod block {
|
||||
pub const FULL: &str = "█";
|
||||
pub const SEVEN_EIGHTHS: &str = "▉";
|
||||
pub const THREE_QUARTERS: &str = "▊";
|
||||
pub const FIVE_EIGHTHS: &str = "▋";
|
||||
pub const HALF: &str = "▌";
|
||||
pub const THREE_EIGHTHS: &str = "▍";
|
||||
pub const ONE_QUARTER: &str = "▎";
|
||||
pub const ONE_EIGHTH: &str = "▏";
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
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,
|
||||
}
|
||||
|
||||
impl Default for Set {
|
||||
fn default() -> Self {
|
||||
NINE_LEVELS
|
||||
}
|
||||
}
|
||||
|
||||
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 half_block {
|
||||
pub const UPPER: char = '▀';
|
||||
pub const LOWER: char = '▄';
|
||||
pub const FULL: char = '█';
|
||||
}
|
||||
|
||||
pub mod bar {
|
||||
pub const FULL: &str = "█";
|
||||
pub const SEVEN_EIGHTHS: &str = "▇";
|
||||
pub const THREE_QUARTERS: &str = "▆";
|
||||
pub const FIVE_EIGHTHS: &str = "▅";
|
||||
pub const HALF: &str = "▄";
|
||||
pub const THREE_EIGHTHS: &str = "▃";
|
||||
pub const ONE_QUARTER: &str = "▂";
|
||||
pub const ONE_EIGHTH: &str = "▁";
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
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,
|
||||
}
|
||||
|
||||
impl Default for Set {
|
||||
fn default() -> Self {
|
||||
NINE_LEVELS
|
||||
}
|
||||
}
|
||||
|
||||
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 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, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum Marker {
|
||||
/// One point per cell in shape of dot (`•`)
|
||||
#[default]
|
||||
Dot,
|
||||
/// One point per cell in shape of a block (`█`)
|
||||
Block,
|
||||
/// One point per cell in the shape of a bar (`▄`)
|
||||
Bar,
|
||||
/// Use the [Unicode Braille Patterns](https://en.wikipedia.org/wiki/Braille_Patterns) block to
|
||||
/// represent data points.
|
||||
///
|
||||
/// This is a 2x4 grid of dots, where each dot can be either on or off.
|
||||
///
|
||||
/// Note: Support for this marker is limited to terminals and fonts that support Unicode
|
||||
/// Braille Patterns. If your terminal does not support this, you will see unicode replacement
|
||||
/// characters (`<60>`) instead of Braille dots (`⠓`, `⣇`, `⣿`).
|
||||
Braille,
|
||||
/// Use the unicode block and half block characters (`█`, `▄`, and `▀`) to represent points in
|
||||
/// a grid that is double the resolution of the terminal. Because each terminal cell is
|
||||
/// generally about twice as tall as it is wide, this allows for a square grid of pixels.
|
||||
HalfBlock,
|
||||
}
|
||||
|
||||
pub mod scrollbar {
|
||||
use super::{block, line};
|
||||
|
||||
/// Scrollbar Set
|
||||
/// ```text
|
||||
/// <--▮------->
|
||||
/// ^ ^ ^ ^
|
||||
/// │ │ │ └ end
|
||||
/// │ │ └──── track
|
||||
/// │ └──────── thumb
|
||||
/// └─────────── begin
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
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: block::FULL,
|
||||
begin: "▲",
|
||||
end: "▼",
|
||||
};
|
||||
|
||||
pub const DOUBLE_HORIZONTAL: Set = Set {
|
||||
track: line::DOUBLE_HORIZONTAL,
|
||||
thumb: block::FULL,
|
||||
begin: "◄",
|
||||
end: "►",
|
||||
};
|
||||
|
||||
pub const VERTICAL: Set = Set {
|
||||
track: line::VERTICAL,
|
||||
thumb: block::FULL,
|
||||
begin: "↑",
|
||||
end: "↓",
|
||||
};
|
||||
|
||||
pub const HORIZONTAL: Set = Set {
|
||||
track: line::HORIZONTAL,
|
||||
thumb: block::FULL,
|
||||
begin: "←",
|
||||
end: "→",
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use strum::ParseError;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn marker_tostring() {
|
||||
assert_eq!(Marker::Dot.to_string(), "Dot");
|
||||
assert_eq!(Marker::Block.to_string(), "Block");
|
||||
assert_eq!(Marker::Bar.to_string(), "Bar");
|
||||
assert_eq!(Marker::Braille.to_string(), "Braille");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn marker_from_str() {
|
||||
assert_eq!("Dot".parse::<Marker>(), Ok(Marker::Dot));
|
||||
assert_eq!("Block".parse::<Marker>(), Ok(Marker::Block));
|
||||
assert_eq!("Bar".parse::<Marker>(), Ok(Marker::Bar));
|
||||
assert_eq!("Braille".parse::<Marker>(), Ok(Marker::Braille));
|
||||
assert_eq!("".parse::<Marker>(), Err(ParseError::VariantNotFound));
|
||||
}
|
||||
}
|
||||
509
ratatui-core/src/symbols/border.rs
Normal file
509
ratatui-core/src/symbols/border.rs
Normal file
@@ -0,0 +1,509 @@
|
||||
use super::{block, line};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub struct Set {
|
||||
pub top_left: &'static str,
|
||||
pub top_right: &'static str,
|
||||
pub bottom_left: &'static str,
|
||||
pub bottom_right: &'static str,
|
||||
pub vertical_left: &'static str,
|
||||
pub vertical_right: &'static str,
|
||||
pub horizontal_top: &'static str,
|
||||
pub horizontal_bottom: &'static str,
|
||||
}
|
||||
|
||||
impl Default for Set {
|
||||
fn default() -> Self {
|
||||
PLAIN
|
||||
}
|
||||
}
|
||||
|
||||
/// Border Set with a single line width
|
||||
///
|
||||
/// ```text
|
||||
/// ┌─────┐
|
||||
/// │xxxxx│
|
||||
/// │xxxxx│
|
||||
/// └─────┘
|
||||
/// ```
|
||||
pub const PLAIN: Set = Set {
|
||||
top_left: line::NORMAL.top_left,
|
||||
top_right: line::NORMAL.top_right,
|
||||
bottom_left: line::NORMAL.bottom_left,
|
||||
bottom_right: line::NORMAL.bottom_right,
|
||||
vertical_left: line::NORMAL.vertical,
|
||||
vertical_right: line::NORMAL.vertical,
|
||||
horizontal_top: line::NORMAL.horizontal,
|
||||
horizontal_bottom: line::NORMAL.horizontal,
|
||||
};
|
||||
|
||||
/// Border Set with a single line width and rounded corners
|
||||
///
|
||||
/// ```text
|
||||
/// ╭─────╮
|
||||
/// │xxxxx│
|
||||
/// │xxxxx│
|
||||
/// ╰─────╯
|
||||
/// ```
|
||||
pub const ROUNDED: Set = Set {
|
||||
top_left: line::ROUNDED.top_left,
|
||||
top_right: line::ROUNDED.top_right,
|
||||
bottom_left: line::ROUNDED.bottom_left,
|
||||
bottom_right: line::ROUNDED.bottom_right,
|
||||
vertical_left: line::ROUNDED.vertical,
|
||||
vertical_right: line::ROUNDED.vertical,
|
||||
horizontal_top: line::ROUNDED.horizontal,
|
||||
horizontal_bottom: line::ROUNDED.horizontal,
|
||||
};
|
||||
|
||||
/// Border Set with a double line width
|
||||
///
|
||||
/// ```text
|
||||
/// ╔═════╗
|
||||
/// ║xxxxx║
|
||||
/// ║xxxxx║
|
||||
/// ╚═════╝
|
||||
/// ```
|
||||
pub const DOUBLE: Set = Set {
|
||||
top_left: line::DOUBLE.top_left,
|
||||
top_right: line::DOUBLE.top_right,
|
||||
bottom_left: line::DOUBLE.bottom_left,
|
||||
bottom_right: line::DOUBLE.bottom_right,
|
||||
vertical_left: line::DOUBLE.vertical,
|
||||
vertical_right: line::DOUBLE.vertical,
|
||||
horizontal_top: line::DOUBLE.horizontal,
|
||||
horizontal_bottom: line::DOUBLE.horizontal,
|
||||
};
|
||||
|
||||
/// Border Set with a thick line width
|
||||
///
|
||||
/// ```text
|
||||
/// ┏━━━━━┓
|
||||
/// ┃xxxxx┃
|
||||
/// ┃xxxxx┃
|
||||
/// ┗━━━━━┛
|
||||
/// ```
|
||||
pub const THICK: Set = Set {
|
||||
top_left: line::THICK.top_left,
|
||||
top_right: line::THICK.top_right,
|
||||
bottom_left: line::THICK.bottom_left,
|
||||
bottom_right: line::THICK.bottom_right,
|
||||
vertical_left: line::THICK.vertical,
|
||||
vertical_right: line::THICK.vertical,
|
||||
horizontal_top: line::THICK.horizontal,
|
||||
horizontal_bottom: line::THICK.horizontal,
|
||||
};
|
||||
|
||||
pub const QUADRANT_TOP_LEFT: &str = "▘";
|
||||
pub const QUADRANT_TOP_RIGHT: &str = "▝";
|
||||
pub const QUADRANT_BOTTOM_LEFT: &str = "▖";
|
||||
pub const QUADRANT_BOTTOM_RIGHT: &str = "▗";
|
||||
pub const QUADRANT_TOP_HALF: &str = "▀";
|
||||
pub const QUADRANT_BOTTOM_HALF: &str = "▄";
|
||||
pub const QUADRANT_LEFT_HALF: &str = "▌";
|
||||
pub const QUADRANT_RIGHT_HALF: &str = "▐";
|
||||
pub const QUADRANT_TOP_LEFT_BOTTOM_LEFT_BOTTOM_RIGHT: &str = "▙";
|
||||
pub const QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_LEFT: &str = "▛";
|
||||
pub const QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_RIGHT: &str = "▜";
|
||||
pub const QUADRANT_TOP_RIGHT_BOTTOM_LEFT_BOTTOM_RIGHT: &str = "▟";
|
||||
pub const QUADRANT_TOP_LEFT_BOTTOM_RIGHT: &str = "▚";
|
||||
pub const QUADRANT_TOP_RIGHT_BOTTOM_LEFT: &str = "▞";
|
||||
pub const QUADRANT_BLOCK: &str = "█";
|
||||
|
||||
/// Quadrant used for setting a border outside a block by one half cell "pixel".
|
||||
///
|
||||
/// ```text
|
||||
/// ▛▀▀▀▀▀▜
|
||||
/// ▌xxxxx▐
|
||||
/// ▌xxxxx▐
|
||||
/// ▙▄▄▄▄▄▟
|
||||
/// ```
|
||||
pub const QUADRANT_OUTSIDE: Set = Set {
|
||||
top_left: QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_LEFT,
|
||||
top_right: QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_RIGHT,
|
||||
bottom_left: QUADRANT_TOP_LEFT_BOTTOM_LEFT_BOTTOM_RIGHT,
|
||||
bottom_right: QUADRANT_TOP_RIGHT_BOTTOM_LEFT_BOTTOM_RIGHT,
|
||||
vertical_left: QUADRANT_LEFT_HALF,
|
||||
vertical_right: QUADRANT_RIGHT_HALF,
|
||||
horizontal_top: QUADRANT_TOP_HALF,
|
||||
horizontal_bottom: QUADRANT_BOTTOM_HALF,
|
||||
};
|
||||
|
||||
/// Quadrant used for setting a border inside a block by one half cell "pixel".
|
||||
///
|
||||
/// ```text
|
||||
/// ▗▄▄▄▄▄▖
|
||||
/// ▐xxxxx▌
|
||||
/// ▐xxxxx▌
|
||||
/// ▝▀▀▀▀▀▘
|
||||
/// ```
|
||||
pub const QUADRANT_INSIDE: Set = Set {
|
||||
top_right: QUADRANT_BOTTOM_LEFT,
|
||||
top_left: QUADRANT_BOTTOM_RIGHT,
|
||||
bottom_right: QUADRANT_TOP_LEFT,
|
||||
bottom_left: QUADRANT_TOP_RIGHT,
|
||||
vertical_left: QUADRANT_RIGHT_HALF,
|
||||
vertical_right: QUADRANT_LEFT_HALF,
|
||||
horizontal_top: QUADRANT_BOTTOM_HALF,
|
||||
horizontal_bottom: QUADRANT_TOP_HALF,
|
||||
};
|
||||
|
||||
pub const ONE_EIGHTH_TOP_EIGHT: &str = "▔";
|
||||
pub const ONE_EIGHTH_BOTTOM_EIGHT: &str = "▁";
|
||||
pub const ONE_EIGHTH_LEFT_EIGHT: &str = "▏";
|
||||
pub const ONE_EIGHTH_RIGHT_EIGHT: &str = "▕";
|
||||
|
||||
/// Wide border set based on McGugan box technique
|
||||
///
|
||||
/// ```text
|
||||
/// ▁▁▁▁▁▁▁
|
||||
/// ▏xxxxx▕
|
||||
/// ▏xxxxx▕
|
||||
/// ▔▔▔▔▔▔▔
|
||||
/// ```
|
||||
#[allow(clippy::doc_markdown)]
|
||||
pub const ONE_EIGHTH_WIDE: Set = Set {
|
||||
top_right: ONE_EIGHTH_BOTTOM_EIGHT,
|
||||
top_left: ONE_EIGHTH_BOTTOM_EIGHT,
|
||||
bottom_right: ONE_EIGHTH_TOP_EIGHT,
|
||||
bottom_left: ONE_EIGHTH_TOP_EIGHT,
|
||||
vertical_left: ONE_EIGHTH_LEFT_EIGHT,
|
||||
vertical_right: ONE_EIGHTH_RIGHT_EIGHT,
|
||||
horizontal_top: ONE_EIGHTH_BOTTOM_EIGHT,
|
||||
horizontal_bottom: ONE_EIGHTH_TOP_EIGHT,
|
||||
};
|
||||
|
||||
/// Tall border set based on McGugan box technique
|
||||
///
|
||||
/// ```text
|
||||
/// ▕▔▔▏
|
||||
/// ▕xx▏
|
||||
/// ▕xx▏
|
||||
/// ▕▁▁▏
|
||||
/// ```
|
||||
#[allow(clippy::doc_markdown)]
|
||||
pub const ONE_EIGHTH_TALL: Set = Set {
|
||||
top_right: ONE_EIGHTH_LEFT_EIGHT,
|
||||
top_left: ONE_EIGHTH_RIGHT_EIGHT,
|
||||
bottom_right: ONE_EIGHTH_LEFT_EIGHT,
|
||||
bottom_left: ONE_EIGHTH_RIGHT_EIGHT,
|
||||
vertical_left: ONE_EIGHTH_RIGHT_EIGHT,
|
||||
vertical_right: ONE_EIGHTH_LEFT_EIGHT,
|
||||
horizontal_top: ONE_EIGHTH_TOP_EIGHT,
|
||||
horizontal_bottom: ONE_EIGHTH_BOTTOM_EIGHT,
|
||||
};
|
||||
|
||||
/// Wide proportional (visually equal width and height) border with using set of quadrants.
|
||||
///
|
||||
/// The border is created by using half blocks for top and bottom, and full
|
||||
/// blocks for right and left sides to make horizontal and vertical borders seem equal.
|
||||
///
|
||||
/// ```text
|
||||
/// ▄▄▄▄
|
||||
/// █xx█
|
||||
/// █xx█
|
||||
/// ▀▀▀▀
|
||||
/// ```
|
||||
pub const PROPORTIONAL_WIDE: Set = Set {
|
||||
top_right: QUADRANT_BOTTOM_HALF,
|
||||
top_left: QUADRANT_BOTTOM_HALF,
|
||||
bottom_right: QUADRANT_TOP_HALF,
|
||||
bottom_left: QUADRANT_TOP_HALF,
|
||||
vertical_left: QUADRANT_BLOCK,
|
||||
vertical_right: QUADRANT_BLOCK,
|
||||
horizontal_top: QUADRANT_BOTTOM_HALF,
|
||||
horizontal_bottom: QUADRANT_TOP_HALF,
|
||||
};
|
||||
|
||||
/// Tall proportional (visually equal width and height) border with using set of quadrants.
|
||||
///
|
||||
/// The border is created by using full blocks for all sides, except for the top and bottom,
|
||||
/// which use half blocks to make horizontal and vertical borders seem equal.
|
||||
///
|
||||
/// ```text
|
||||
/// ▕█▀▀█
|
||||
/// ▕█xx█
|
||||
/// ▕█xx█
|
||||
/// ▕█▄▄█
|
||||
/// ```
|
||||
pub const PROPORTIONAL_TALL: Set = Set {
|
||||
top_right: QUADRANT_BLOCK,
|
||||
top_left: QUADRANT_BLOCK,
|
||||
bottom_right: QUADRANT_BLOCK,
|
||||
bottom_left: QUADRANT_BLOCK,
|
||||
vertical_left: QUADRANT_BLOCK,
|
||||
vertical_right: QUADRANT_BLOCK,
|
||||
horizontal_top: QUADRANT_TOP_HALF,
|
||||
horizontal_bottom: QUADRANT_BOTTOM_HALF,
|
||||
};
|
||||
|
||||
/// Solid border set
|
||||
///
|
||||
/// The border is created by using full blocks for all sides.
|
||||
///
|
||||
/// ```text
|
||||
/// ████
|
||||
/// █xx█
|
||||
/// █xx█
|
||||
/// ████
|
||||
/// ```
|
||||
pub const FULL: Set = Set {
|
||||
top_left: block::FULL,
|
||||
top_right: block::FULL,
|
||||
bottom_left: block::FULL,
|
||||
bottom_right: block::FULL,
|
||||
vertical_left: block::FULL,
|
||||
vertical_right: block::FULL,
|
||||
horizontal_top: block::FULL,
|
||||
horizontal_bottom: block::FULL,
|
||||
};
|
||||
|
||||
/// Empty border set
|
||||
///
|
||||
/// The border is created by using empty strings for all sides.
|
||||
///
|
||||
/// This is useful for ensuring that the border style is applied to a border on a block with a title
|
||||
/// without actually drawing a border.
|
||||
///
|
||||
/// ░ Example
|
||||
///
|
||||
/// `░` represents the content in the area not covered by the border to make it easier to see the
|
||||
/// blank symbols.
|
||||
///
|
||||
/// ```text
|
||||
/// ░░░░░░░░
|
||||
/// ░░ ░░
|
||||
/// ░░ ░░ ░░
|
||||
/// ░░ ░░ ░░
|
||||
/// ░░ ░░
|
||||
/// ░░░░░░░░
|
||||
/// ```
|
||||
pub const EMPTY: Set = Set {
|
||||
top_left: " ",
|
||||
top_right: " ",
|
||||
bottom_left: " ",
|
||||
bottom_right: " ",
|
||||
vertical_left: " ",
|
||||
vertical_right: " ",
|
||||
horizontal_top: " ",
|
||||
horizontal_bottom: " ",
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use indoc::{formatdoc, indoc};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default() {
|
||||
assert_eq!(Set::default(), PLAIN);
|
||||
}
|
||||
|
||||
/// A helper function to render a border set to a string.
|
||||
///
|
||||
/// '░' (U+2591 Light Shade) is used as a placeholder for empty space to make it easier to see
|
||||
/// the size of the border symbols.
|
||||
fn render(set: Set) -> String {
|
||||
formatdoc!(
|
||||
"░░░░░░
|
||||
░{}{}{}{}░
|
||||
░{}░░{}░
|
||||
░{}░░{}░
|
||||
░{}{}{}{}░
|
||||
░░░░░░",
|
||||
set.top_left,
|
||||
set.horizontal_top,
|
||||
set.horizontal_top,
|
||||
set.top_right,
|
||||
set.vertical_left,
|
||||
set.vertical_right,
|
||||
set.vertical_left,
|
||||
set.vertical_right,
|
||||
set.bottom_left,
|
||||
set.horizontal_bottom,
|
||||
set.horizontal_bottom,
|
||||
set.bottom_right
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain() {
|
||||
assert_eq!(
|
||||
render(PLAIN),
|
||||
indoc!(
|
||||
"░░░░░░
|
||||
░┌──┐░
|
||||
░│░░│░
|
||||
░│░░│░
|
||||
░└──┘░
|
||||
░░░░░░"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rounded() {
|
||||
assert_eq!(
|
||||
render(ROUNDED),
|
||||
indoc!(
|
||||
"░░░░░░
|
||||
░╭──╮░
|
||||
░│░░│░
|
||||
░│░░│░
|
||||
░╰──╯░
|
||||
░░░░░░"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double() {
|
||||
assert_eq!(
|
||||
render(DOUBLE),
|
||||
indoc!(
|
||||
"░░░░░░
|
||||
░╔══╗░
|
||||
░║░░║░
|
||||
░║░░║░
|
||||
░╚══╝░
|
||||
░░░░░░"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thick() {
|
||||
assert_eq!(
|
||||
render(THICK),
|
||||
indoc!(
|
||||
"░░░░░░
|
||||
░┏━━┓░
|
||||
░┃░░┃░
|
||||
░┃░░┃░
|
||||
░┗━━┛░
|
||||
░░░░░░"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quadrant_outside() {
|
||||
assert_eq!(
|
||||
render(QUADRANT_OUTSIDE),
|
||||
indoc!(
|
||||
"░░░░░░
|
||||
░▛▀▀▜░
|
||||
░▌░░▐░
|
||||
░▌░░▐░
|
||||
░▙▄▄▟░
|
||||
░░░░░░"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quadrant_inside() {
|
||||
assert_eq!(
|
||||
render(QUADRANT_INSIDE),
|
||||
indoc!(
|
||||
"░░░░░░
|
||||
░▗▄▄▖░
|
||||
░▐░░▌░
|
||||
░▐░░▌░
|
||||
░▝▀▀▘░
|
||||
░░░░░░"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_eighth_wide() {
|
||||
assert_eq!(
|
||||
render(ONE_EIGHTH_WIDE),
|
||||
indoc!(
|
||||
"░░░░░░
|
||||
░▁▁▁▁░
|
||||
░▏░░▕░
|
||||
░▏░░▕░
|
||||
░▔▔▔▔░
|
||||
░░░░░░"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_eighth_tall() {
|
||||
assert_eq!(
|
||||
render(ONE_EIGHTH_TALL),
|
||||
indoc!(
|
||||
"░░░░░░
|
||||
░▕▔▔▏░
|
||||
░▕░░▏░
|
||||
░▕░░▏░
|
||||
░▕▁▁▏░
|
||||
░░░░░░"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proportional_wide() {
|
||||
assert_eq!(
|
||||
render(PROPORTIONAL_WIDE),
|
||||
indoc!(
|
||||
"░░░░░░
|
||||
░▄▄▄▄░
|
||||
░█░░█░
|
||||
░█░░█░
|
||||
░▀▀▀▀░
|
||||
░░░░░░"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proportional_tall() {
|
||||
assert_eq!(
|
||||
render(PROPORTIONAL_TALL),
|
||||
indoc!(
|
||||
"░░░░░░
|
||||
░█▀▀█░
|
||||
░█░░█░
|
||||
░█░░█░
|
||||
░█▄▄█░
|
||||
░░░░░░"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full() {
|
||||
assert_eq!(
|
||||
render(FULL),
|
||||
indoc!(
|
||||
"░░░░░░
|
||||
░████░
|
||||
░█░░█░
|
||||
░█░░█░
|
||||
░████░
|
||||
░░░░░░"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
assert_eq!(
|
||||
render(EMPTY),
|
||||
indoc!(
|
||||
"░░░░░░
|
||||
░ ░
|
||||
░ ░░ ░
|
||||
░ ░░ ░
|
||||
░ ░
|
||||
░░░░░░"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
208
ratatui-core/src/symbols/line.rs
Normal file
208
ratatui-core/src/symbols/line.rs
Normal file
@@ -0,0 +1,208 @@
|
||||
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, Copy, Eq, PartialEq, Hash)]
|
||||
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,
|
||||
}
|
||||
|
||||
impl Default for Set {
|
||||
fn default() -> Self {
|
||||
NORMAL
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use indoc::{formatdoc, indoc};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default() {
|
||||
assert_eq!(Set::default(), NORMAL);
|
||||
}
|
||||
|
||||
/// A helper function to render a set of symbols.
|
||||
fn render(set: Set) -> String {
|
||||
formatdoc!(
|
||||
"{}{}{}{}
|
||||
{}{}{}{}
|
||||
{}{}{}{}
|
||||
{}{}{}{}",
|
||||
set.top_left,
|
||||
set.horizontal,
|
||||
set.horizontal_down,
|
||||
set.top_right,
|
||||
set.vertical,
|
||||
" ",
|
||||
set.vertical,
|
||||
set.vertical,
|
||||
set.vertical_right,
|
||||
set.horizontal,
|
||||
set.cross,
|
||||
set.vertical_left,
|
||||
set.bottom_left,
|
||||
set.horizontal,
|
||||
set.horizontal_up,
|
||||
set.bottom_right
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normal() {
|
||||
assert_eq!(
|
||||
render(NORMAL),
|
||||
indoc!(
|
||||
"┌─┬┐
|
||||
│ ││
|
||||
├─┼┤
|
||||
└─┴┘"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rounded() {
|
||||
assert_eq!(
|
||||
render(ROUNDED),
|
||||
indoc!(
|
||||
"╭─┬╮
|
||||
│ ││
|
||||
├─┼┤
|
||||
╰─┴╯"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double() {
|
||||
assert_eq!(
|
||||
render(DOUBLE),
|
||||
indoc!(
|
||||
"╔═╦╗
|
||||
║ ║║
|
||||
╠═╬╣
|
||||
╚═╩╝"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thick() {
|
||||
assert_eq!(
|
||||
render(THICK),
|
||||
indoc!(
|
||||
"┏━┳┓
|
||||
┃ ┃┃
|
||||
┣━╋┫
|
||||
┗━┻┛"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
40
ratatui-core/src/terminal.rs
Normal file
40
ratatui-core/src/terminal.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
#![deny(missing_docs)]
|
||||
//! Provides the [`Terminal`], [`Frame`] and related types.
|
||||
//!
|
||||
//! The [`Terminal`] is the main interface of this library. It is responsible for drawing and
|
||||
//! maintaining the state of the different widgets that compose your application.
|
||||
//!
|
||||
//! The [`Frame`] is a consistent view into the terminal state for rendering. It is obtained via
|
||||
//! the closure argument of [`Terminal::draw`]. It is used to render widgets to the terminal and
|
||||
//! control the cursor position.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use std::io::stdout;
|
||||
//!
|
||||
//! use ratatui::{prelude::*, widgets::Paragraph};
|
||||
//!
|
||||
//! let backend = CrosstermBackend::new(stdout());
|
||||
//! let mut terminal = Terminal::new(backend)?;
|
||||
//! terminal.draw(|frame| {
|
||||
//! let area = frame.area();
|
||||
//! frame.render_widget(Paragraph::new("Hello world!"), area);
|
||||
//! })?;
|
||||
//! # std::io::Result::Ok(())
|
||||
//! ```
|
||||
//!
|
||||
//! [Crossterm]: https://crates.io/crates/crossterm
|
||||
//! [Termion]: https://crates.io/crates/termion
|
||||
//! [Termwiz]: https://crates.io/crates/termwiz
|
||||
//! [`backend`]: crate::backend
|
||||
//! [`Backend`]: crate::backend::Backend
|
||||
//! [`Buffer`]: crate::buffer::Buffer
|
||||
|
||||
mod frame;
|
||||
mod terminal;
|
||||
mod viewport;
|
||||
|
||||
pub use frame::{CompletedFrame, Frame};
|
||||
pub use terminal::{Options as TerminalOptions, Terminal};
|
||||
pub use viewport::Viewport;
|
||||
228
ratatui-core/src/terminal/frame.rs
Normal file
228
ratatui-core/src/terminal/frame.rs
Normal file
@@ -0,0 +1,228 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
/// A consistent view into the terminal state for rendering a single frame.
|
||||
///
|
||||
/// This is obtained via the closure argument of [`Terminal::draw`]. It is used to render widgets
|
||||
/// to the terminal and control the cursor position.
|
||||
///
|
||||
/// The changes drawn to the frame are applied only to the current [`Buffer`]. After the closure
|
||||
/// returns, the current buffer is compared to the previous buffer and only the changes are applied
|
||||
/// to the terminal. This avoids drawing redundant cells.
|
||||
///
|
||||
/// [`Buffer`]: crate::buffer::Buffer
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct Frame<'a> {
|
||||
/// 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()`.
|
||||
pub(crate) cursor_position: Option<Position>,
|
||||
|
||||
/// The area of the viewport
|
||||
pub(crate) viewport_area: Rect,
|
||||
|
||||
/// The buffer that is used to draw the current frame
|
||||
pub(crate) buffer: &'a mut Buffer,
|
||||
|
||||
/// The frame count indicating the sequence number of this frame.
|
||||
pub(crate) count: usize,
|
||||
}
|
||||
|
||||
/// `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`].
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct CompletedFrame<'a> {
|
||||
/// The buffer that was used to draw the last frame.
|
||||
pub buffer: &'a Buffer,
|
||||
/// The size of the last frame.
|
||||
pub area: Rect,
|
||||
/// The frame count indicating the sequence number of this frame.
|
||||
pub count: usize,
|
||||
}
|
||||
|
||||
impl Frame<'_> {
|
||||
/// The area of the current frame
|
||||
///
|
||||
/// This is guaranteed not to change during rendering, so may be called multiple times.
|
||||
///
|
||||
/// If your app listens for a resize event from the backend, it should ignore the values from
|
||||
/// the event for any calculations that are used to render the current frame and use this value
|
||||
/// instead as this is the area of the buffer that is used to render the current frame.
|
||||
pub const fn area(&self) -> Rect {
|
||||
self.viewport_area
|
||||
}
|
||||
|
||||
/// The area of the current frame
|
||||
///
|
||||
/// This is guaranteed not to change during rendering, so may be called multiple times.
|
||||
///
|
||||
/// If your app listens for a resize event from the backend, it should ignore the values from
|
||||
/// the event for any calculations that are used to render the current frame and use this value
|
||||
/// instead as this is the area of the buffer that is used to render the current frame.
|
||||
#[deprecated = "use .area() as it's the more correct name"]
|
||||
pub const fn size(&self) -> Rect {
|
||||
self.viewport_area
|
||||
}
|
||||
|
||||
/// Render a [`Widget`] to the current buffer using [`Widget::render`].
|
||||
///
|
||||
/// Usually the area argument is the size of the current frame or a sub-area of the current
|
||||
/// frame (which can be obtained using [`Layout`] to split the total area).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::Block};
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// # let mut frame = terminal.get_frame();
|
||||
/// let block = Block::new();
|
||||
/// let area = Rect::new(0, 0, 5, 5);
|
||||
/// frame.render_widget(block, area);
|
||||
/// ```
|
||||
///
|
||||
/// [`Layout`]: crate::layout::Layout
|
||||
pub fn render_widget<W: Widget>(&mut self, widget: W, area: Rect) {
|
||||
widget.render(area, self.buffer);
|
||||
}
|
||||
|
||||
/// Render a [`WidgetRef`] to the current buffer using [`WidgetRef::render_ref`].
|
||||
///
|
||||
/// Usually the area argument is the size of the current frame or a sub-area of the current
|
||||
/// frame (which can be obtained using [`Layout`] to split the total area).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # #[cfg(feature = "unstable-widget-ref")] {
|
||||
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::Block};
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// # let mut frame = terminal.get_frame();
|
||||
/// let block = Block::new();
|
||||
/// let area = Rect::new(0, 0, 5, 5);
|
||||
/// frame.render_widget_ref(block, area);
|
||||
/// # }
|
||||
/// ```
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
#[instability::unstable(feature = "widget-ref")]
|
||||
pub fn render_widget_ref<W: WidgetRef>(&mut self, widget: W, area: Rect) {
|
||||
widget.render_ref(area, self.buffer);
|
||||
}
|
||||
|
||||
/// Render a [`StatefulWidget`] to the current buffer using [`StatefulWidget::render`].
|
||||
///
|
||||
/// Usually the area argument is the size of the current frame or a sub-area of the current
|
||||
/// frame (which can be obtained using [`Layout`] to split the total area).
|
||||
///
|
||||
/// The last argument should be an instance of the [`StatefulWidget::State`] associated to the
|
||||
/// given [`StatefulWidget`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::*};
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// # let mut frame = terminal.get_frame();
|
||||
/// let mut state = ListState::default().with_selected(Some(1));
|
||||
/// let list = List::new(vec![ListItem::new("Item 1"), ListItem::new("Item 2")]);
|
||||
/// let area = Rect::new(0, 0, 5, 5);
|
||||
/// frame.render_stateful_widget(list, area, &mut state);
|
||||
/// ```
|
||||
///
|
||||
/// [`Layout`]: crate::layout::Layout
|
||||
pub fn render_stateful_widget<W>(&mut self, widget: W, area: Rect, state: &mut W::State)
|
||||
where
|
||||
W: StatefulWidget,
|
||||
{
|
||||
widget.render(area, self.buffer, state);
|
||||
}
|
||||
|
||||
/// Render a [`StatefulWidgetRef`] to the current buffer using
|
||||
/// [`StatefulWidgetRef::render_ref`].
|
||||
///
|
||||
/// Usually the area argument is the size of the current frame or a sub-area of the current
|
||||
/// frame (which can be obtained using [`Layout`] to split the total area).
|
||||
///
|
||||
/// The last argument should be an instance of the [`StatefulWidgetRef::State`] associated to
|
||||
/// the given [`StatefulWidgetRef`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # #[cfg(feature = "unstable-widget-ref")] {
|
||||
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::*};
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// # let mut frame = terminal.get_frame();
|
||||
/// let mut state = ListState::default().with_selected(Some(1));
|
||||
/// let list = List::new(vec![ListItem::new("Item 1"), ListItem::new("Item 2")]);
|
||||
/// let area = Rect::new(0, 0, 5, 5);
|
||||
/// frame.render_stateful_widget_ref(list, area, &mut state);
|
||||
/// # }
|
||||
/// ```
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
#[instability::unstable(feature = "widget-ref")]
|
||||
pub fn render_stateful_widget_ref<W>(&mut self, widget: W, area: Rect, state: &mut W::State)
|
||||
where
|
||||
W: StatefulWidgetRef,
|
||||
{
|
||||
widget.render_ref(area, self.buffer, 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_position`]. Pick one of the APIs and
|
||||
/// stick with it.
|
||||
pub fn set_cursor_position<P: Into<Position>>(&mut self, position: P) {
|
||||
self.cursor_position = Some(position.into());
|
||||
}
|
||||
|
||||
/// 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_position`]. Pick one of the APIs and
|
||||
/// stick with it.
|
||||
#[deprecated = "the method set_cursor_position indicates more clearly what about the cursor to set"]
|
||||
pub fn set_cursor(&mut self, x: u16, y: u16) {
|
||||
self.set_cursor_position(Position { x, y });
|
||||
}
|
||||
|
||||
/// Gets the buffer that this `Frame` draws into as a mutable reference.
|
||||
pub fn buffer_mut(&mut self) -> &mut Buffer {
|
||||
self.buffer
|
||||
}
|
||||
|
||||
/// Returns the current frame count.
|
||||
///
|
||||
/// This method provides access to the frame count, which is a sequence number indicating
|
||||
/// how many frames have been rendered up to (but not including) this one. It can be used
|
||||
/// for purposes such as animation, performance tracking, or debugging.
|
||||
///
|
||||
/// Each time a frame has been rendered, this count is incremented,
|
||||
/// providing a consistent way to reference the order and number of frames processed by the
|
||||
/// terminal. When count reaches its maximum value (`usize::MAX`), it wraps around to zero.
|
||||
///
|
||||
/// This count is particularly useful when dealing with dynamic content or animations where the
|
||||
/// state of the display changes over time. By tracking the frame count, developers can
|
||||
/// synchronize updates or changes to the content with the rendering process.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::*};
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// # let mut frame = terminal.get_frame();
|
||||
/// let current_count = frame.count();
|
||||
/// println!("Current frame count: {}", current_count);
|
||||
/// ```
|
||||
pub const fn count(&self) -> usize {
|
||||
self.count
|
||||
}
|
||||
}
|
||||
727
ratatui-core/src/terminal/terminal.rs
Normal file
727
ratatui-core/src/terminal/terminal.rs
Normal file
@@ -0,0 +1,727 @@
|
||||
use std::io;
|
||||
|
||||
use crate::{
|
||||
backend::ClearType, buffer::Cell, prelude::*, CompletedFrame, TerminalOptions, Viewport,
|
||||
};
|
||||
|
||||
/// An interface to interact and draw [`Frame`]s on the user's terminal.
|
||||
///
|
||||
/// This is the main entry point for Ratatui. It is responsible for drawing and maintaining the
|
||||
/// state of the buffers, cursor and viewport.
|
||||
///
|
||||
/// The [`Terminal`] is generic over a [`Backend`] implementation which is used to interface with
|
||||
/// the underlying terminal library. The [`Backend`] trait is implemented for three popular Rust
|
||||
/// terminal libraries: [Crossterm], [Termion] and [Termwiz]. See the [`backend`] module for more
|
||||
/// information.
|
||||
///
|
||||
/// The `Terminal` struct maintains two buffers: the current and the previous.
|
||||
/// When the widgets are drawn, the changes are accumulated in the current buffer.
|
||||
/// At the end of each draw pass, the two buffers are compared, and only the changes
|
||||
/// between these buffers are written to the terminal, avoiding any redundant operations.
|
||||
/// After flushing these changes, the buffers are swapped to prepare for the next draw cycle.
|
||||
///
|
||||
/// The terminal also has a viewport which is the area of the terminal that is currently visible to
|
||||
/// the user. It can be either fullscreen, inline or fixed. See [`Viewport`] for more information.
|
||||
///
|
||||
/// Applications should detect terminal resizes and call [`Terminal::draw`] to redraw the
|
||||
/// application with the new size. This will automatically resize the internal buffers to match the
|
||||
/// new size for inline and fullscreen viewports. Fixed viewports are not resized automatically.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use std::io::stdout;
|
||||
///
|
||||
/// use ratatui::widgets::Paragraph;
|
||||
///
|
||||
/// let backend = CrosstermBackend::new(stdout());
|
||||
/// let mut terminal = Terminal::new(backend)?;
|
||||
/// terminal.draw(|frame| {
|
||||
/// let area = frame.area();
|
||||
/// frame.render_widget(Paragraph::new("Hello World!"), area);
|
||||
/// })?;
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
///
|
||||
/// [Crossterm]: https://crates.io/crates/crossterm
|
||||
/// [Termion]: https://crates.io/crates/termion
|
||||
/// [Termwiz]: https://crates.io/crates/termwiz
|
||||
/// [`backend`]: crate::backend
|
||||
/// [`Backend`]: crate::backend::Backend
|
||||
/// [`Buffer`]: crate::buffer::Buffer
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Terminal<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
/// The backend used to interface with the terminal
|
||||
backend: B,
|
||||
/// 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,
|
||||
/// Area of the viewport
|
||||
viewport_area: Rect,
|
||||
/// Last known area of the terminal. Used to detect if the internal buffers have to be resized.
|
||||
last_known_area: 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: Position,
|
||||
/// Number of frames rendered up until current time.
|
||||
frame_count: usize,
|
||||
}
|
||||
|
||||
/// Options to pass to [`Terminal::with_options`]
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Options {
|
||||
/// Viewport used to draw to the terminal
|
||||
pub viewport: Viewport,
|
||||
}
|
||||
|
||||
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,
|
||||
{
|
||||
/// Creates a new [`Terminal`] with the given [`Backend`] with a full screen viewport.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use std::io::stdout;
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let backend = CrosstermBackend::new(stdout());
|
||||
/// let terminal = Terminal::new(backend)?;
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
pub fn new(backend: B) -> io::Result<Self> {
|
||||
Self::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: Viewport::Fullscreen,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a new [`Terminal`] with the given [`Backend`] and [`TerminalOptions`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use std::io::stdout;
|
||||
/// # use ratatui::{prelude::*, backend::TestBackend, Viewport, TerminalOptions};
|
||||
/// let backend = CrosstermBackend::new(stdout());
|
||||
/// let viewport = Viewport::Fixed(Rect::new(0, 0, 10, 10));
|
||||
/// let terminal = Terminal::with_options(backend, TerminalOptions { viewport })?;
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
pub fn with_options(mut backend: B, options: TerminalOptions) -> io::Result<Self> {
|
||||
let area = match options.viewport {
|
||||
Viewport::Fullscreen | Viewport::Inline(_) => {
|
||||
Rect::from((Position::ORIGIN, backend.size()?))
|
||||
}
|
||||
Viewport::Fixed(area) => area,
|
||||
};
|
||||
let (viewport_area, cursor_pos) = match options.viewport {
|
||||
Viewport::Fullscreen => (area, Position::ORIGIN),
|
||||
Viewport::Inline(height) => {
|
||||
compute_inline_size(&mut backend, height, area.as_size(), 0)?
|
||||
}
|
||||
Viewport::Fixed(area) => (area, area.as_position()),
|
||||
};
|
||||
Ok(Self {
|
||||
backend,
|
||||
buffers: [Buffer::empty(viewport_area), Buffer::empty(viewport_area)],
|
||||
current: 0,
|
||||
hidden_cursor: false,
|
||||
viewport: options.viewport,
|
||||
viewport_area,
|
||||
last_known_area: area,
|
||||
last_known_cursor_pos: cursor_pos,
|
||||
frame_count: 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a Frame object which provides a consistent view into the terminal state for rendering.
|
||||
pub fn get_frame(&mut self) -> Frame {
|
||||
let count = self.frame_count;
|
||||
Frame {
|
||||
cursor_position: None,
|
||||
viewport_area: self.viewport_area,
|
||||
buffer: self.current_buffer_mut(),
|
||||
count,
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the current buffer as a mutable reference.
|
||||
pub fn current_buffer_mut(&mut self) -> &mut Buffer {
|
||||
&mut self.buffers[self.current]
|
||||
}
|
||||
|
||||
/// Gets the backend
|
||||
pub const fn backend(&self) -> &B {
|
||||
&self.backend
|
||||
}
|
||||
|
||||
/// Gets the backend as a mutable reference
|
||||
pub fn backend_mut(&mut self) -> &mut B {
|
||||
&mut self.backend
|
||||
}
|
||||
|
||||
/// 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 = Position { x: *col, y: *row };
|
||||
}
|
||||
self.backend.draw(updates.into_iter())
|
||||
}
|
||||
|
||||
/// Updates the Terminal so that internal buffers match the requested area.
|
||||
///
|
||||
/// Requested area will be saved to remain consistent when rendering. This leads to a full clear
|
||||
/// of the screen.
|
||||
pub fn resize(&mut self, area: Rect) -> io::Result<()> {
|
||||
let next_area = match self.viewport {
|
||||
Viewport::Inline(height) => {
|
||||
let offset_in_previous_viewport = self
|
||||
.last_known_cursor_pos
|
||||
.y
|
||||
.saturating_sub(self.viewport_area.top());
|
||||
compute_inline_size(
|
||||
&mut self.backend,
|
||||
height,
|
||||
area.as_size(),
|
||||
offset_in_previous_viewport,
|
||||
)?
|
||||
.0
|
||||
}
|
||||
Viewport::Fixed(_) | Viewport::Fullscreen => area,
|
||||
};
|
||||
self.set_viewport_area(next_area);
|
||||
self.clear()?;
|
||||
|
||||
self.last_known_area = area;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// 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 area = Rect::from((Position::ORIGIN, self.size()?));
|
||||
if area != self.last_known_area {
|
||||
self.resize(area)?;
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Draws a single frame to the terminal.
|
||||
///
|
||||
/// Returns a [`CompletedFrame`] if successful, otherwise a [`std::io::Error`].
|
||||
///
|
||||
/// If the render callback passed to this method can fail, use [`try_draw`] instead.
|
||||
///
|
||||
/// Applications should call `draw` or [`try_draw`] in a loop to continuously render the
|
||||
/// terminal. These methods are the main entry points for drawing to the terminal.
|
||||
///
|
||||
/// [`try_draw`]: Terminal::try_draw
|
||||
///
|
||||
/// This method will:
|
||||
///
|
||||
/// - autoresize the terminal if necessary
|
||||
/// - call the render callback, passing it a [`Frame`] reference to render to
|
||||
/// - flush the current internal state by copying the current buffer to the backend
|
||||
/// - move the cursor to the last known position if it was set during the rendering closure
|
||||
/// - return a [`CompletedFrame`] with the current buffer and the area of the terminal
|
||||
///
|
||||
/// The [`CompletedFrame`] returned by this method can be useful for debugging or testing
|
||||
/// purposes, but it is often not used in regular applicationss.
|
||||
///
|
||||
/// The render callback should fully render the entire frame when called, including areas that
|
||||
/// are unchanged from the previous frame. This is because each frame is compared to the
|
||||
/// previous frame to determine what has changed, and only the changes are written to the
|
||||
/// terminal. If the render callback does not fully render the frame, the terminal will not be
|
||||
/// in a consistent state.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::layout::Position;
|
||||
/// # let backend = ratatui::backend::TestBackend::new(10, 10);
|
||||
/// # let mut terminal = ratatui::Terminal::new(backend)?;
|
||||
/// use ratatui::widgets::Paragraph;
|
||||
///
|
||||
/// // with a closure
|
||||
/// terminal.draw(|frame| {
|
||||
/// let area = frame.area();
|
||||
/// frame.render_widget(Paragraph::new("Hello World!"), area);
|
||||
/// frame.set_cursor_position(Position { x: 0, y: 0 });
|
||||
/// })?;
|
||||
///
|
||||
/// // or with a function
|
||||
/// terminal.draw(render)?;
|
||||
///
|
||||
/// fn render(frame: &mut ratatui::Frame) {
|
||||
/// frame.render_widget(Paragraph::new("Hello World!"), frame.area());
|
||||
/// }
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
pub fn draw<F>(&mut self, render_callback: F) -> io::Result<CompletedFrame>
|
||||
where
|
||||
F: FnOnce(&mut Frame),
|
||||
{
|
||||
self.try_draw(|frame| {
|
||||
render_callback(frame);
|
||||
io::Result::Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Tries to draw a single frame to the terminal.
|
||||
///
|
||||
/// Returns [`Result::Ok`] containing a [`CompletedFrame`] if successful, otherwise
|
||||
/// [`Result::Err`] containing the [`std::io::Error`] that caused the failure.
|
||||
///
|
||||
/// This is the equivalent of [`Terminal::draw`] but the render callback is a function or
|
||||
/// closure that returns a `Result` instead of nothing.
|
||||
///
|
||||
/// Applications should call `try_draw` or [`draw`] in a loop to continuously render the
|
||||
/// terminal. These methods are the main entry points for drawing to the terminal.
|
||||
///
|
||||
/// [`draw`]: Terminal::draw
|
||||
///
|
||||
/// This method will:
|
||||
///
|
||||
/// - autoresize the terminal if necessary
|
||||
/// - call the render callback, passing it a [`Frame`] reference to render to
|
||||
/// - flush the current internal state by copying the current buffer to the backend
|
||||
/// - move the cursor to the last known position if it was set during the rendering closure
|
||||
/// - return a [`CompletedFrame`] with the current buffer and the area of the terminal
|
||||
///
|
||||
/// The render callback passed to `try_draw` can return any [`Result`] with an error type that
|
||||
/// can be converted into an [`std::io::Error`] using the [`Into`] trait. This makes it possible
|
||||
/// to use the `?` operator to propagate errors that occur during rendering. If the render
|
||||
/// callback returns an error, the error will be returned from `try_draw` as an
|
||||
/// [`std::io::Error`] and the terminal will not be updated.
|
||||
///
|
||||
/// The [`CompletedFrame`] returned by this method can be useful for debugging or testing
|
||||
/// purposes, but it is often not used in regular applicationss.
|
||||
///
|
||||
/// The render callback should fully render the entire frame when called, including areas that
|
||||
/// are unchanged from the previous frame. This is because each frame is compared to the
|
||||
/// previous frame to determine what has changed, and only the changes are written to the
|
||||
/// terminal. If the render function does not fully render the frame, the terminal will not be
|
||||
/// in a consistent state.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```should_panic
|
||||
/// # use ratatui::layout::Position;;
|
||||
/// # let backend = ratatui::backend::TestBackend::new(10, 10);
|
||||
/// # let mut terminal = ratatui::Terminal::new(backend)?;
|
||||
/// use std::io;
|
||||
///
|
||||
/// use ratatui::widgets::Paragraph;
|
||||
///
|
||||
/// // with a closure
|
||||
/// terminal.try_draw(|frame| {
|
||||
/// let value: u8 = "not a number".parse().map_err(io::Error::other)?;
|
||||
/// let area = frame.area();
|
||||
/// frame.render_widget(Paragraph::new("Hello World!"), area);
|
||||
/// frame.set_cursor_position(Position { x: 0, y: 0 });
|
||||
/// io::Result::Ok(())
|
||||
/// })?;
|
||||
///
|
||||
/// // or with a function
|
||||
/// terminal.try_draw(render)?;
|
||||
///
|
||||
/// fn render(frame: &mut ratatui::Frame) -> io::Result<()> {
|
||||
/// let value: u8 = "not a number".parse().map_err(io::Error::other)?;
|
||||
/// frame.render_widget(Paragraph::new("Hello World!"), frame.area());
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// # io::Result::Ok(())
|
||||
/// ```
|
||||
pub fn try_draw<F, E>(&mut self, render_callback: F) -> io::Result<CompletedFrame>
|
||||
where
|
||||
F: FnOnce(&mut Frame) -> Result<(), E>,
|
||||
E: Into<io::Error>,
|
||||
{
|
||||
// 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();
|
||||
|
||||
render_callback(&mut frame).map_err(Into::into)?;
|
||||
|
||||
// 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
|
||||
// Buffer. 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(position) => {
|
||||
self.show_cursor()?;
|
||||
self.set_cursor_position(position)?;
|
||||
}
|
||||
}
|
||||
|
||||
self.swap_buffers();
|
||||
|
||||
// Flush
|
||||
self.backend.flush()?;
|
||||
|
||||
let completed_frame = CompletedFrame {
|
||||
buffer: &self.buffers[1 - self.current],
|
||||
area: self.last_known_area,
|
||||
count: self.frame_count,
|
||||
};
|
||||
|
||||
// increment frame count before returning from draw
|
||||
self.frame_count = self.frame_count.wrapping_add(1);
|
||||
|
||||
Ok(completed_frame)
|
||||
}
|
||||
|
||||
/// Hides the cursor.
|
||||
pub fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
self.backend.hide_cursor()?;
|
||||
self.hidden_cursor = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Shows the cursor.
|
||||
pub fn show_cursor(&mut self) -> io::Result<()> {
|
||||
self.backend.show_cursor()?;
|
||||
self.hidden_cursor = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the current cursor position.
|
||||
///
|
||||
/// This is the position of the cursor after the last draw call and is returned as a tuple of
|
||||
/// `(x, y)` coordinates.
|
||||
#[deprecated = "the method get_cursor_position indicates more clearly what about the cursor to get"]
|
||||
pub fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
let Position { x, y } = self.get_cursor_position()?;
|
||||
Ok((x, y))
|
||||
}
|
||||
|
||||
/// Sets the cursor position.
|
||||
#[deprecated = "the method set_cursor_position indicates more clearly what about the cursor to set"]
|
||||
pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
self.set_cursor_position(Position { x, y })
|
||||
}
|
||||
|
||||
/// Gets the current cursor position.
|
||||
///
|
||||
/// This is the position of the cursor after the last draw call.
|
||||
pub fn get_cursor_position(&mut self) -> io::Result<Position> {
|
||||
self.backend.get_cursor_position()
|
||||
}
|
||||
|
||||
/// Sets the cursor position.
|
||||
pub fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
|
||||
let position = position.into();
|
||||
self.backend.set_cursor_position(position)?;
|
||||
self.last_known_cursor_pos = position;
|
||||
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_position(self.viewport_area.as_position())?;
|
||||
self.backend.clear_region(ClearType::AfterCursor)?;
|
||||
}
|
||||
Viewport::Fixed(area) => {
|
||||
for y in area.top()..area.bottom() {
|
||||
self.backend.set_cursor_position(Position { x: 0, y })?;
|
||||
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<Size> {
|
||||
self.backend.size()
|
||||
}
|
||||
|
||||
/// Insert some content before the current inline viewport. This has no effect when the
|
||||
/// viewport is not inline.
|
||||
///
|
||||
/// The `draw_fn` closure will be called to draw into a writable `Buffer` that is `height`
|
||||
/// lines tall. The content of that `Buffer` will then be inserted before the viewport.
|
||||
///
|
||||
/// If the viewport isn't yet at the bottom of the screen, inserted lines will push it towards
|
||||
/// the bottom. Once the viewport is at the bottom of the screen, inserted lines will scroll
|
||||
/// the area of the screen above the viewport upwards.
|
||||
///
|
||||
/// Before:
|
||||
/// ```ignore
|
||||
/// +---------------------+
|
||||
/// | pre-existing line 1 |
|
||||
/// | pre-existing line 2 |
|
||||
/// +---------------------+
|
||||
/// | viewport |
|
||||
/// +---------------------+
|
||||
/// | |
|
||||
/// | |
|
||||
/// +---------------------+
|
||||
/// ```
|
||||
///
|
||||
/// After inserting 2 lines:
|
||||
/// ```ignore
|
||||
/// +---------------------+
|
||||
/// | pre-existing line 1 |
|
||||
/// | pre-existing line 2 |
|
||||
/// | inserted line 1 |
|
||||
/// | inserted line 2 |
|
||||
/// +---------------------+
|
||||
/// | viewport |
|
||||
/// +---------------------+
|
||||
/// +---------------------+
|
||||
/// ```
|
||||
///
|
||||
/// After inserting 2 more lines:
|
||||
/// ```ignore
|
||||
/// +---------------------+
|
||||
/// | pre-existing line 2 |
|
||||
/// | inserted line 1 |
|
||||
/// | inserted line 2 |
|
||||
/// | inserted line 3 |
|
||||
/// | inserted line 4 |
|
||||
/// +---------------------+
|
||||
/// | viewport |
|
||||
/// +---------------------+
|
||||
/// ```
|
||||
///
|
||||
/// If more lines are inserted than there is space on the screen, then the top lines will go
|
||||
/// directly into the terminal's scrollback buffer. At the limit, if the viewport takes up the
|
||||
/// whole screen, all lines will be inserted directly into the scrollback buffer.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ## Insert a single line before the current viewport
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::*};
|
||||
/// # 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(());
|
||||
}
|
||||
|
||||
// The approach of this function is to first render all of the lines to insert into a
|
||||
// temporary buffer, and then to loop drawing chunks from the buffer to the screen. drawing
|
||||
// this buffer onto the screen.
|
||||
let area = Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: self.viewport_area.width,
|
||||
height,
|
||||
};
|
||||
let mut buffer = Buffer::empty(area);
|
||||
draw_fn(&mut buffer);
|
||||
let mut buffer = buffer.content.as_slice();
|
||||
|
||||
// Use i32 variables so we don't have worry about overflowed u16s when adding, or about
|
||||
// negative results when subtracting.
|
||||
let mut drawn_height: i32 = self.viewport_area.top().into();
|
||||
let mut buffer_height: i32 = height.into();
|
||||
let viewport_height: i32 = self.viewport_area.height.into();
|
||||
let screen_height: i32 = self.last_known_area.height.into();
|
||||
|
||||
// The algorithm here is to loop, drawing large chunks of text (up to a screen-full at a
|
||||
// time), until the remainder of the buffer plus the viewport fits on the screen. We choose
|
||||
// this loop condition because it guarantees that we can write the remainder of the buffer
|
||||
// with just one call to Self::draw_lines().
|
||||
while buffer_height + viewport_height > screen_height {
|
||||
// We will draw as much of the buffer as possible on this iteration in order to make
|
||||
// forward progress. So we have:
|
||||
//
|
||||
// to_draw = min(buffer_height, screen_height)
|
||||
//
|
||||
// We may need to scroll the screen up to make room to draw. We choose the minimal
|
||||
// possible scroll amount so we don't end up with the viewport sitting in the middle of
|
||||
// the screen when this function is done. The amount to scroll by is:
|
||||
//
|
||||
// scroll_up = max(0, drawn_height + to_draw - screen_height)
|
||||
//
|
||||
// We want `scroll_up` to be enough so that, after drawing, we have used the whole
|
||||
// screen (drawn_height - scroll_up + to_draw = screen_height). However, there might
|
||||
// already be enough room on the screen to draw without scrolling (drawn_height +
|
||||
// to_draw <= screen_height). In this case, we just don't scroll at all.
|
||||
let to_draw = buffer_height.min(screen_height);
|
||||
let scroll_up = 0.max(drawn_height + to_draw - screen_height);
|
||||
self.scroll_up(scroll_up as u16)?;
|
||||
buffer = self.draw_lines((drawn_height - scroll_up) as u16, to_draw as u16, buffer)?;
|
||||
drawn_height += to_draw - scroll_up;
|
||||
buffer_height -= to_draw;
|
||||
}
|
||||
|
||||
// There is now enough room on the screen for the remaining buffer plus the viewport,
|
||||
// though we may still need to scroll up some of the existing text first. It's possible
|
||||
// that by this point we've drained the buffer, but we may still need to scroll up to make
|
||||
// room for the viewport.
|
||||
//
|
||||
// We want to scroll up the exact amount that will leave us completely filling the screen.
|
||||
// However, it's possible that the viewport didn't start on the bottom of the screen and
|
||||
// the added lines weren't enough to push it all the way to the bottom. We deal with this
|
||||
// case by just ensuring that our scroll amount is non-negative.
|
||||
//
|
||||
// We want:
|
||||
// screen_height = drawn_height - scroll_up + buffer_height + viewport_height
|
||||
// Or, equivalently:
|
||||
// scroll_up = drawn_height + buffer_height + viewport_height - screen_height
|
||||
let scroll_up = 0.max(drawn_height + buffer_height + viewport_height - screen_height);
|
||||
self.scroll_up(scroll_up as u16)?;
|
||||
self.draw_lines(
|
||||
(drawn_height - scroll_up) as u16,
|
||||
buffer_height as u16,
|
||||
buffer,
|
||||
)?;
|
||||
drawn_height += buffer_height - scroll_up;
|
||||
|
||||
self.set_viewport_area(Rect {
|
||||
y: drawn_height as u16,
|
||||
..self.viewport_area
|
||||
});
|
||||
|
||||
// Clear the viewport off the screen. We didn't clear earlier for two reasons. First, it
|
||||
// wasn't necessary because the buffer we drew out of isn't sparse, so it overwrote
|
||||
// whatever was on the screen. Second, there is a weird bug with tmux where a full screen
|
||||
// clear plus immediate scrolling causes some garbage to go into the scrollback.
|
||||
self.clear()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Draw lines at the given vertical offset. The slice of cells must contain enough cells
|
||||
/// for the requested lines. A slice of the unused cells are returned.
|
||||
fn draw_lines<'a>(
|
||||
&mut self,
|
||||
y_offset: u16,
|
||||
lines_to_draw: u16,
|
||||
cells: &'a [Cell],
|
||||
) -> io::Result<&'a [Cell]> {
|
||||
let width: usize = self.last_known_area.width.into();
|
||||
let (to_draw, remainder) = cells.split_at(width * lines_to_draw as usize);
|
||||
if lines_to_draw > 0 {
|
||||
let iter = to_draw
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, c)| ((i % width) as u16, y_offset + (i / width) as u16, c));
|
||||
self.backend.draw(iter)?;
|
||||
self.backend.flush()?;
|
||||
}
|
||||
Ok(remainder)
|
||||
}
|
||||
|
||||
/// Scroll the whole screen up by the given number of lines.
|
||||
fn scroll_up(&mut self, lines_to_scroll: u16) -> io::Result<()> {
|
||||
if lines_to_scroll > 0 {
|
||||
self.set_cursor_position(Position::new(
|
||||
0,
|
||||
self.last_known_area.height.saturating_sub(1),
|
||||
))?;
|
||||
self.backend.append_lines(lines_to_scroll)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_inline_size<B: Backend>(
|
||||
backend: &mut B,
|
||||
height: u16,
|
||||
size: Size,
|
||||
offset_in_previous_viewport: u16,
|
||||
) -> io::Result<(Rect, Position)> {
|
||||
let pos = backend.get_cursor_position()?;
|
||||
let mut row = pos.y;
|
||||
|
||||
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,
|
||||
))
|
||||
}
|
||||
54
ratatui-core/src/terminal/viewport.rs
Normal file
54
ratatui-core/src/terminal/viewport.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use std::fmt;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Represents the viewport of the terminal. The viewport is the area of the terminal that is
|
||||
/// currently visible to the user. It can be either fullscreen, inline or fixed.
|
||||
///
|
||||
/// When the viewport is fullscreen, the whole terminal is used to draw the application.
|
||||
///
|
||||
/// When the viewport is inline, it is drawn inline with the rest of the terminal. The height of
|
||||
/// the viewport is fixed, but the width is the same as the terminal width.
|
||||
///
|
||||
/// When the viewport is fixed, it is drawn in a fixed area of the terminal. The area is specified
|
||||
/// by a [`Rect`].
|
||||
///
|
||||
/// See [`Terminal::with_options`] for more information.
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum Viewport {
|
||||
/// The viewport is fullscreen
|
||||
#[default]
|
||||
Fullscreen,
|
||||
/// The viewport is inline with the rest of the terminal.
|
||||
///
|
||||
/// The viewport's height is fixed and specified in number of lines. The width is the same as
|
||||
/// the terminal's width. The viewport is drawn below the cursor position.
|
||||
Inline(u16),
|
||||
/// The viewport is drawn in a fixed area of the terminal. The area is specified by a [`Rect`].
|
||||
Fixed(Rect),
|
||||
}
|
||||
|
||||
impl fmt::Display for Viewport {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Fullscreen => write!(f, "Fullscreen"),
|
||||
Self::Inline(height) => write!(f, "Inline({height})"),
|
||||
Self::Fixed(area) => write!(f, "Fixed({area})"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn viewport_to_string() {
|
||||
assert_eq!(Viewport::Fullscreen.to_string(), "Fullscreen");
|
||||
assert_eq!(Viewport::Inline(5).to_string(), "Inline(5)");
|
||||
assert_eq!(
|
||||
Viewport::Fixed(Rect::new(0, 0, 5, 5)).to_string(),
|
||||
"Fixed(5x5+0+0)"
|
||||
);
|
||||
}
|
||||
}
|
||||
60
ratatui-core/src/text.rs
Normal file
60
ratatui-core/src/text.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
//! 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::{prelude::*, widgets::*};
|
||||
//!
|
||||
//! // A simple string with no styling.
|
||||
//! // Converted to Line(vec![
|
||||
//! // Span { content: Cow::Borrowed("My title"), style: Style { .. } }
|
||||
//! // ])
|
||||
//! let block = Block::new().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::new().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::new().title(vec![
|
||||
//! Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
//! Span::raw(" title"),
|
||||
//! ]);
|
||||
//! ```
|
||||
|
||||
mod grapheme;
|
||||
pub use grapheme::StyledGrapheme;
|
||||
|
||||
mod line;
|
||||
pub use line::{Line, ToLine};
|
||||
|
||||
mod masked;
|
||||
pub use masked::Masked;
|
||||
|
||||
mod span;
|
||||
pub use span::{Span, ToSpan};
|
||||
|
||||
mod text;
|
||||
pub use text::{Text, ToText};
|
||||
81
ratatui-core/src/text/grapheme.rs
Normal file
81
ratatui-core/src/text/grapheme.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
use crate::{prelude::*, style::Styled};
|
||||
|
||||
const NBSP: &str = "\u{00a0}";
|
||||
const ZWSP: &str = "\u{200b}";
|
||||
|
||||
/// A grapheme associated to a style.
|
||||
/// Note that, although `StyledGrapheme` is the smallest divisible unit of text,
|
||||
/// it actually is not a member of the text type hierarchy (`Text` -> `Line` -> `Span`).
|
||||
/// It is a separate type used mostly for rendering purposes. A `Span` consists of components that
|
||||
/// can be split into `StyledGrapheme`s, but it does not contain a collection of `StyledGrapheme`s.
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct StyledGrapheme<'a> {
|
||||
pub symbol: &'a str,
|
||||
pub style: Style,
|
||||
}
|
||||
|
||||
impl<'a> StyledGrapheme<'a> {
|
||||
/// Creates a new `StyledGrapheme` with the given symbol and style.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
pub fn new<S: Into<Style>>(symbol: &'a str, style: S) -> Self {
|
||||
Self {
|
||||
symbol,
|
||||
style: style.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_whitespace(&self) -> bool {
|
||||
let symbol = self.symbol;
|
||||
symbol == ZWSP || symbol.chars().all(char::is_whitespace) && symbol != NBSP
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Styled for StyledGrapheme<'a> {
|
||||
type Item = Self;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style<S: Into<Style>>(mut self, style: S) -> Self::Item {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn new() {
|
||||
let style = Style::new().yellow();
|
||||
let sg = StyledGrapheme::new("a", style);
|
||||
assert_eq!(sg.symbol, "a");
|
||||
assert_eq!(sg.style, style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn style() {
|
||||
let style = Style::new().yellow();
|
||||
let sg = StyledGrapheme::new("a", style);
|
||||
assert_eq!(sg.style(), style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_style() {
|
||||
let style = Style::new().yellow().on_red();
|
||||
let style2 = Style::new().green();
|
||||
let sg = StyledGrapheme::new("a", style).set_style(style2);
|
||||
assert_eq!(sg.style, style2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stylize() {
|
||||
let style = Style::new().yellow().on_red();
|
||||
let sg = StyledGrapheme::new("a", style).green();
|
||||
assert_eq!(sg.style, Style::new().green().on_red());
|
||||
}
|
||||
}
|
||||
1525
ratatui-core/src/text/line.rs
Normal file
1525
ratatui-core/src/text/line.rs
Normal file
File diff suppressed because it is too large
Load Diff
143
ratatui-core/src/text/masked.rs
Normal file
143
ratatui-core/src/text/masked.rs
Normal file
@@ -0,0 +1,143 @@
|
||||
use std::{borrow::Cow, fmt};
|
||||
|
||||
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::{prelude::*, widgets::*};
|
||||
///
|
||||
/// 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(["xxxxx"]));
|
||||
/// ```
|
||||
#[derive(Default, Clone, Eq, PartialEq, Hash)]
|
||||
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 const 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 fmt::Debug for Masked<'_> {
|
||||
/// Debug representation of a masked string is the underlying string
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
// note that calling display instead of Debug here is intentional
|
||||
fmt::Display::fmt(&self.inner, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Masked<'_> {
|
||||
/// Display representation of a masked string is the masked string
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt::Display::fmt(&self.value(), f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Masked<'a>> for Cow<'a, str> {
|
||||
fn from(masked: &'a Masked) -> Self {
|
||||
masked.value()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Masked<'a>> for Cow<'a, str> {
|
||||
fn from(masked: Masked<'a>) -> Self {
|
||||
masked.value()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Masked<'_>> for Text<'a> {
|
||||
fn from(masked: &'a Masked) -> Self {
|
||||
Text::raw(masked.value())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Masked<'a>> for Text<'a> {
|
||||
fn from(masked: Masked<'a>) -> Self {
|
||||
Text::raw(masked.value())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::text::Line;
|
||||
|
||||
#[test]
|
||||
fn new() {
|
||||
let masked = Masked::new("12345", 'x');
|
||||
assert_eq!(masked.inner, "12345");
|
||||
assert_eq!(masked.mask_char, 'x');
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn value() {
|
||||
let masked = Masked::new("12345", 'x');
|
||||
assert_eq!(masked.value(), "xxxxx");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mask_char() {
|
||||
let masked = Masked::new("12345", 'x');
|
||||
assert_eq!(masked.mask_char(), 'x');
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug() {
|
||||
let masked = Masked::new("12345", 'x');
|
||||
assert_eq!(format!("{masked:?}"), "12345");
|
||||
assert_eq!(format!("{masked:.3?}"), "123", "Debug truncates");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display() {
|
||||
let masked = Masked::new("12345", 'x');
|
||||
assert_eq!(format!("{masked}"), "xxxxx");
|
||||
assert_eq!(format!("{masked:.3}"), "xxx", "Display truncates");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn into_text() {
|
||||
let masked = Masked::new("12345", 'x');
|
||||
|
||||
let text: Text = (&masked).into();
|
||||
assert_eq!(text.lines, [Line::from("xxxxx")]);
|
||||
|
||||
let text: Text = masked.into();
|
||||
assert_eq!(text.lines, [Line::from("xxxxx")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn into_cow() {
|
||||
let masked = Masked::new("12345", 'x');
|
||||
let cow: Cow<str> = (&masked).into();
|
||||
assert_eq!(cow, "xxxxx");
|
||||
|
||||
let cow: Cow<str> = masked.into();
|
||||
assert_eq!(cow, "xxxxx");
|
||||
}
|
||||
}
|
||||
846
ratatui-core/src/text/span.rs
Normal file
846
ratatui-core/src/text/span.rs
Normal file
@@ -0,0 +1,846 @@
|
||||
use std::{borrow::Cow, fmt};
|
||||
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{prelude::*, style::Styled, text::StyledGrapheme};
|
||||
|
||||
/// Represents a part of a line that is contiguous and where all characters share the same style.
|
||||
///
|
||||
/// A `Span` is the smallest unit of text that can be styled. It is usually combined in the [`Line`]
|
||||
/// type to represent a line of text where each `Span` may have a different style.
|
||||
///
|
||||
/// # Constructor Methods
|
||||
///
|
||||
/// - [`Span::default`] creates an span with empty content and the default style.
|
||||
/// - [`Span::raw`] creates an span with the specified content and the default style.
|
||||
/// - [`Span::styled`] creates an span with the specified content and style.
|
||||
///
|
||||
/// # Setter Methods
|
||||
///
|
||||
/// These methods are fluent setters. They return a new `Span` with the specified property set.
|
||||
///
|
||||
/// - [`Span::content`] sets the content of the span.
|
||||
/// - [`Span::style`] sets the style of the span.
|
||||
///
|
||||
/// # Other Methods
|
||||
///
|
||||
/// - [`Span::patch_style`] patches the style of the span, adding modifiers from the given style.
|
||||
/// - [`Span::reset_style`] resets the style of the span.
|
||||
/// - [`Span::width`] returns the unicode width of the content held by this span.
|
||||
/// - [`Span::styled_graphemes`] returns an iterator over the graphemes held by this span.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// A `Span` with `style` set to [`Style::default()`] can be created from a `&str`, a `String`, or
|
||||
/// any type convertible to [`Cow<str>`].
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// let span = Span::raw("test content");
|
||||
/// let span = Span::raw(String::from("test content"));
|
||||
/// let span = Span::from("test content");
|
||||
/// let span = Span::from(String::from("test content"));
|
||||
/// let span: Span = "test content".into();
|
||||
/// let span: Span = String::from("test content").into();
|
||||
/// ```
|
||||
///
|
||||
/// Styled spans can be created using [`Span::styled`] or by converting strings using methods from
|
||||
/// the [`Stylize`] trait.
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// let span = Span::styled("test content", Style::new().green());
|
||||
/// let span = Span::styled(String::from("test content"), Style::new().green());
|
||||
///
|
||||
/// // using Stylize trait shortcuts
|
||||
/// let span = "test content".green();
|
||||
/// let span = String::from("test content").green();
|
||||
/// ```
|
||||
///
|
||||
/// `Span` implements the [`Styled`] trait, which allows it to be styled using the shortcut methods
|
||||
/// defined in the [`Stylize`] trait.
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// let span = Span::raw("test content").green().on_yellow().italic();
|
||||
/// let span = Span::raw(String::from("test content"))
|
||||
/// .green()
|
||||
/// .on_yellow()
|
||||
/// .italic();
|
||||
/// ```
|
||||
///
|
||||
/// `Span` implements the [`Widget`] trait, which allows it to be rendered to a [`Buffer`]. Usually
|
||||
/// apps will use the [`Paragraph`] widget instead of rendering `Span` directly, as it handles text
|
||||
/// wrapping and alignment for you.
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// # fn render_frame(frame: &mut Frame) {
|
||||
/// frame.render_widget("test content".green().on_yellow().italic(), frame.area());
|
||||
/// # }
|
||||
/// ```
|
||||
/// [`Line`]: crate::text::Line
|
||||
/// [`Paragraph`]: crate::widgets::Paragraph
|
||||
/// [`Stylize`]: crate::style::Stylize
|
||||
/// [`Cow<str>`]: std::borrow::Cow
|
||||
#[derive(Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Span<'a> {
|
||||
/// The style of the span.
|
||||
pub style: Style,
|
||||
/// The content of the span as a Clone-on-write string.
|
||||
pub content: Cow<'a, str>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Span<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if self.style == Style::default() {
|
||||
return write!(f, "Span({:?})", self.content);
|
||||
}
|
||||
f.debug_struct("Span")
|
||||
.field("style", &self.style)
|
||||
.field("content", &self.content)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Span<'a> {
|
||||
/// Create a span with the default style.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// Span::raw("test content");
|
||||
/// Span::raw(String::from("test content"));
|
||||
/// ```
|
||||
pub fn raw<T>(content: T) -> Self
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
{
|
||||
Self {
|
||||
content: content.into(),
|
||||
style: Style::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a span with the specified style.
|
||||
///
|
||||
/// `content` accepts any type that is convertible to [`Cow<str>`] (e.g. `&str`, `String`,
|
||||
/// `&String`, etc.).
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let style = Style::new().yellow().on_green().italic();
|
||||
/// Span::styled("test content", style);
|
||||
/// Span::styled(String::from("test content"), style);
|
||||
/// ```
|
||||
pub fn styled<T, S>(content: T, style: S) -> Self
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
S: Into<Style>,
|
||||
{
|
||||
Self {
|
||||
content: content.into(),
|
||||
style: style.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the content of the span.
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// Accepts any type that can be converted to [`Cow<str>`] (e.g. `&str`, `String`, `&String`,
|
||||
/// etc.).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let mut span = Span::default().content("content");
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn content<T>(mut self, content: T) -> Self
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
{
|
||||
self.content = content.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the span.
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// In contrast to [`Span::patch_style`], this method replaces the style of the span instead of
|
||||
/// patching it.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let mut span = Span::default().style(Style::new().green());
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Patches the style of the Span, adding modifiers from the given style.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let span = Span::styled("test content", Style::new().green().italic())
|
||||
/// .patch_style(Style::new().red().on_yellow().bold());
|
||||
/// assert_eq!(span.style, Style::new().red().on_yellow().italic().bold());
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn patch_style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = self.style.patch(style);
|
||||
self
|
||||
}
|
||||
|
||||
/// Resets the style of the Span.
|
||||
///
|
||||
/// This is Equivalent to calling `patch_style(Style::reset())`.
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let span = Span::styled(
|
||||
/// "Test Content",
|
||||
/// Style::new().dark_gray().on_yellow().italic(),
|
||||
/// )
|
||||
/// .reset_style();
|
||||
/// assert_eq!(span.style, Style::reset());
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn reset_style(self) -> Self {
|
||||
self.patch_style(Style::reset())
|
||||
}
|
||||
|
||||
/// Returns the unicode 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 the `Span`'s `style` to get the
|
||||
/// resulting [`Style`].
|
||||
///
|
||||
/// `base_style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`],
|
||||
/// or your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use std::iter::Iterator;
|
||||
///
|
||||
/// use ratatui::{prelude::*, text::StyledGrapheme};
|
||||
///
|
||||
/// let span = Span::styled("Test", Style::new().green().italic());
|
||||
/// let style = Style::new().red().on_yellow();
|
||||
/// assert_eq!(
|
||||
/// span.styled_graphemes(style)
|
||||
/// .collect::<Vec<StyledGrapheme>>(),
|
||||
/// vec![
|
||||
/// StyledGrapheme::new("T", Style::new().green().on_yellow().italic()),
|
||||
/// StyledGrapheme::new("e", Style::new().green().on_yellow().italic()),
|
||||
/// StyledGrapheme::new("s", Style::new().green().on_yellow().italic()),
|
||||
/// StyledGrapheme::new("t", Style::new().green().on_yellow().italic()),
|
||||
/// ],
|
||||
/// );
|
||||
/// ```
|
||||
pub fn styled_graphemes<S: Into<Style>>(
|
||||
&'a self,
|
||||
base_style: S,
|
||||
) -> impl Iterator<Item = StyledGrapheme<'a>> {
|
||||
let style = base_style.into().patch(self.style);
|
||||
self.content
|
||||
.as_ref()
|
||||
.graphemes(true)
|
||||
.filter(|g| *g != "\n")
|
||||
.map(move |g| StyledGrapheme { symbol: g, style })
|
||||
}
|
||||
|
||||
/// Converts this Span into a left-aligned [`Line`]
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let line = "Test Content".green().italic().into_left_aligned_line();
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn into_left_aligned_line(self) -> Line<'a> {
|
||||
Line::from(self).left_aligned()
|
||||
}
|
||||
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
#[deprecated = "use into_left_aligned_line"]
|
||||
pub fn to_left_aligned_line(self) -> Line<'a> {
|
||||
self.into_left_aligned_line()
|
||||
}
|
||||
|
||||
/// Converts this Span into a center-aligned [`Line`]
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let line = "Test Content".green().italic().into_centered_line();
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn into_centered_line(self) -> Line<'a> {
|
||||
Line::from(self).centered()
|
||||
}
|
||||
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
#[deprecated = "use into_centered_line"]
|
||||
pub fn to_centered_line(self) -> Line<'a> {
|
||||
self.into_centered_line()
|
||||
}
|
||||
|
||||
/// Converts this Span into a right-aligned [`Line`]
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let line = "Test Content".green().italic().into_right_aligned_line();
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn into_right_aligned_line(self) -> Line<'a> {
|
||||
Line::from(self).right_aligned()
|
||||
}
|
||||
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
#[deprecated = "use into_right_aligned_line"]
|
||||
pub fn to_right_aligned_line(self) -> Line<'a> {
|
||||
self.into_right_aligned_line()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> From<T> for Span<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
{
|
||||
fn from(s: T) -> Self {
|
||||
Span::raw(s.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> std::ops::Add<Self> for Span<'a> {
|
||||
type Output = Line<'a>;
|
||||
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Line::from_iter([self, rhs])
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Styled for Span<'a> {
|
||||
type Item = Self;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
self.style(style)
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Span<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_ref(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetRef for Span<'_> {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
let area = area.intersection(buf.area);
|
||||
if area.is_empty() {
|
||||
return;
|
||||
}
|
||||
let Rect { mut x, y, .. } = area;
|
||||
for (i, grapheme) in self.styled_graphemes(Style::default()).enumerate() {
|
||||
let symbol_width = grapheme.symbol.width();
|
||||
let next_x = x.saturating_add(symbol_width as u16);
|
||||
if next_x > area.right() {
|
||||
break;
|
||||
}
|
||||
|
||||
if i == 0 {
|
||||
// the first grapheme is always set on the cell
|
||||
buf[(x, y)]
|
||||
.set_symbol(grapheme.symbol)
|
||||
.set_style(grapheme.style);
|
||||
} else if x == area.x {
|
||||
// there is one or more zero-width graphemes in the first cell, so the first cell
|
||||
// must be appended to.
|
||||
buf[(x, y)]
|
||||
.append_symbol(grapheme.symbol)
|
||||
.set_style(grapheme.style);
|
||||
} else if symbol_width == 0 {
|
||||
// append zero-width graphemes to the previous cell
|
||||
buf[(x - 1, y)]
|
||||
.append_symbol(grapheme.symbol)
|
||||
.set_style(grapheme.style);
|
||||
} else {
|
||||
// just a normal grapheme (not first, not zero-width, not overflowing the area)
|
||||
buf[(x, y)]
|
||||
.set_symbol(grapheme.symbol)
|
||||
.set_style(grapheme.style);
|
||||
}
|
||||
|
||||
// multi-width graphemes must clear the cells of characters that are hidden by the
|
||||
// grapheme, otherwise the hidden characters will be re-rendered if the grapheme is
|
||||
// overwritten.
|
||||
for x_hidden in (x + 1)..next_x {
|
||||
// it may seem odd that the style of the hidden cells are not set to the style of
|
||||
// the grapheme, but this is how the existing buffer.set_span() method works.
|
||||
buf[(x_hidden, y)].reset();
|
||||
}
|
||||
x = next_x;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for converting a value to a [`Span`].
|
||||
///
|
||||
/// This trait is automatically implemented for any type that implements the [`Display`] trait. As
|
||||
/// such, `ToSpan` shouln't be implemented directly: [`Display`] should be implemented instead, and
|
||||
/// you get the `ToSpan` implementation for free.
|
||||
///
|
||||
/// [`Display`]: std::fmt::Display
|
||||
pub trait ToSpan {
|
||||
/// Converts the value to a [`Span`].
|
||||
fn to_span(&self) -> Span<'_>;
|
||||
}
|
||||
|
||||
/// # Panics
|
||||
///
|
||||
/// In this implementation, the `to_span` method panics if the `Display` implementation returns an
|
||||
/// error. This indicates an incorrect `Display` implementation since `fmt::Write for String` never
|
||||
/// returns an error itself.
|
||||
impl<T: fmt::Display> ToSpan for T {
|
||||
fn to_span(&self) -> Span<'_> {
|
||||
Span::raw(self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Span<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for line in self.content.lines() {
|
||||
fmt::Display::fmt(line, f)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use buffer::Cell;
|
||||
use rstest::fixture;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[fixture]
|
||||
fn small_buf() -> Buffer {
|
||||
Buffer::empty(Rect::new(0, 0, 10, 1))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default() {
|
||||
let span = Span::default();
|
||||
assert_eq!(span.content, Cow::Borrowed(""));
|
||||
assert_eq!(span.style, Style::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_str() {
|
||||
let span = Span::raw("test content");
|
||||
assert_eq!(span.content, Cow::Borrowed("test content"));
|
||||
assert_eq!(span.style, Style::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_string() {
|
||||
let content = String::from("test content");
|
||||
let span = Span::raw(content.clone());
|
||||
assert_eq!(span.content, Cow::Owned::<str>(content));
|
||||
assert_eq!(span.style, Style::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn styled_str() {
|
||||
let style = Style::new().red();
|
||||
let span = Span::styled("test content", style);
|
||||
assert_eq!(span.content, Cow::Borrowed("test content"));
|
||||
assert_eq!(span.style, Style::new().red());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn styled_string() {
|
||||
let content = String::from("test content");
|
||||
let style = Style::new().green();
|
||||
let span = Span::styled(content.clone(), style);
|
||||
assert_eq!(span.content, Cow::Owned::<str>(content));
|
||||
assert_eq!(span.style, style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_content() {
|
||||
let span = Span::default().content("test content");
|
||||
assert_eq!(span.content, Cow::Borrowed("test content"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_style() {
|
||||
let span = Span::default().style(Style::new().green());
|
||||
assert_eq!(span.style, Style::new().green());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_ref_str_borrowed_cow() {
|
||||
let content = "test content";
|
||||
let span = Span::from(content);
|
||||
assert_eq!(span.content, Cow::Borrowed(content));
|
||||
assert_eq!(span.style, Style::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_string_ref_str_borrowed_cow() {
|
||||
let content = String::from("test content");
|
||||
let span = Span::from(content.as_str());
|
||||
assert_eq!(span.content, Cow::Borrowed(content.as_str()));
|
||||
assert_eq!(span.style, Style::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_string_owned_cow() {
|
||||
let content = String::from("test content");
|
||||
let span = Span::from(content.clone());
|
||||
assert_eq!(span.content, Cow::Owned::<str>(content));
|
||||
assert_eq!(span.style, Style::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_ref_string_borrowed_cow() {
|
||||
let content = String::from("test content");
|
||||
let span = Span::from(&content);
|
||||
assert_eq!(span.content, Cow::Borrowed(content.as_str()));
|
||||
assert_eq!(span.style, Style::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_span() {
|
||||
assert_eq!(42.to_span(), Span::raw("42"));
|
||||
assert_eq!("test".to_span(), Span::raw("test"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_style() {
|
||||
let span = Span::styled("test content", Style::new().green()).reset_style();
|
||||
assert_eq!(span.style, Style::reset());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn patch_style() {
|
||||
let span = Span::styled("test content", Style::new().green().on_yellow())
|
||||
.patch_style(Style::new().red().bold());
|
||||
assert_eq!(span.style, Style::new().red().on_yellow().bold());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn width() {
|
||||
assert_eq!(Span::raw("").width(), 0);
|
||||
assert_eq!(Span::raw("test").width(), 4);
|
||||
assert_eq!(Span::raw("test content").width(), 12);
|
||||
// Needs reconsideration: https://github.com/ratatui/ratatui/issues/1271
|
||||
assert_eq!(Span::raw("test\ncontent").width(), 12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stylize() {
|
||||
let span = Span::raw("test content").green();
|
||||
assert_eq!(span.content, Cow::Borrowed("test content"));
|
||||
assert_eq!(span.style, Style::new().green());
|
||||
|
||||
let span = Span::styled("test content", Style::new().green());
|
||||
let stylized = span.on_yellow().bold();
|
||||
assert_eq!(stylized.content, Cow::Borrowed("test content"));
|
||||
assert_eq!(stylized.style, Style::new().green().on_yellow().bold());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_span() {
|
||||
let span = Span::raw("test content");
|
||||
assert_eq!(format!("{span}"), "test content");
|
||||
assert_eq!(format!("{span:.4}"), "test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_newline_span() {
|
||||
let span = Span::raw("test\ncontent");
|
||||
assert_eq!(format!("{span}"), "testcontent");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_styled_span() {
|
||||
let stylized_span = Span::styled("stylized test content", Style::new().green());
|
||||
assert_eq!(format!("{stylized_span}"), "stylized test content");
|
||||
assert_eq!(format!("{stylized_span:.8}"), "stylized");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn left_aligned() {
|
||||
let span = Span::styled("Test Content", Style::new().green().italic());
|
||||
let line = span.into_left_aligned_line();
|
||||
assert_eq!(line.alignment, Some(Alignment::Left));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn centered() {
|
||||
let span = Span::styled("Test Content", Style::new().green().italic());
|
||||
let line = span.into_centered_line();
|
||||
assert_eq!(line.alignment, Some(Alignment::Center));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn right_aligned() {
|
||||
let span = Span::styled("Test Content", Style::new().green().italic());
|
||||
let line = span.into_right_aligned_line();
|
||||
assert_eq!(line.alignment, Some(Alignment::Right));
|
||||
}
|
||||
|
||||
mod widget {
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn render() {
|
||||
let style = Style::new().green().on_yellow();
|
||||
let span = Span::styled("test content", style);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
|
||||
span.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines([Line::from(vec![
|
||||
"test content".green().on_yellow(),
|
||||
" ".into(),
|
||||
])]);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::x(20, 0)]
|
||||
#[case::y(0, 20)]
|
||||
#[case::both(20, 20)]
|
||||
fn render_out_of_bounds(mut small_buf: Buffer, #[case] x: u16, #[case] y: u16) {
|
||||
let out_of_bounds = Rect::new(x, y, 10, 1);
|
||||
Span::raw("Hello, World!").render(out_of_bounds, &mut small_buf);
|
||||
assert_eq!(small_buf, Buffer::empty(small_buf.area));
|
||||
}
|
||||
|
||||
/// When the content of the span is longer than the area passed to render, the content
|
||||
/// should be truncated
|
||||
#[test]
|
||||
fn render_truncates_too_long_content() {
|
||||
let style = Style::new().green().on_yellow();
|
||||
let span = Span::styled("test content", style);
|
||||
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
|
||||
span.render(Rect::new(0, 0, 5, 1), &mut buf);
|
||||
|
||||
let expected = Buffer::with_lines([Line::from(vec![
|
||||
"test ".green().on_yellow(),
|
||||
" ".into(),
|
||||
])]);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
/// When there is already a style set on the buffer, the style of the span should be
|
||||
/// patched with the existing style
|
||||
#[test]
|
||||
fn render_patches_existing_style() {
|
||||
let style = Style::new().green().on_yellow();
|
||||
let span = Span::styled("test content", style);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
|
||||
buf.set_style(buf.area, Style::new().italic());
|
||||
span.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines([Line::from(vec![
|
||||
"test content".green().on_yellow().italic(),
|
||||
" ".italic(),
|
||||
])]);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
/// When the span contains a multi-width grapheme, the grapheme will ensure that the cells
|
||||
/// of the hidden characters are cleared.
|
||||
#[test]
|
||||
fn render_multi_width_symbol() {
|
||||
let style = Style::new().green().on_yellow();
|
||||
let span = Span::styled("test 😃 content", style);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
|
||||
span.render(buf.area, &mut buf);
|
||||
// The existing code in buffer.set_line() handles multi-width graphemes by clearing the
|
||||
// cells of the hidden characters. This test ensures that the existing behavior is
|
||||
// preserved.
|
||||
let expected = Buffer::with_lines(["test 😃 content".green().on_yellow()]);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
/// When the span contains a multi-width grapheme that does not fit in the area passed to
|
||||
/// render, the entire grapheme will be truncated.
|
||||
#[test]
|
||||
fn render_multi_width_symbol_truncates_entire_symbol() {
|
||||
// the 😃 emoji is 2 columns wide so it will be truncated
|
||||
let style = Style::new().green().on_yellow();
|
||||
let span = Span::styled("test 😃 content", style);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 6, 1));
|
||||
span.render(buf.area, &mut buf);
|
||||
|
||||
let expected =
|
||||
Buffer::with_lines([Line::from(vec!["test ".green().on_yellow(), " ".into()])]);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
/// When the area passed to render overflows the buffer, the content should be truncated
|
||||
/// to fit the buffer.
|
||||
#[test]
|
||||
fn render_overflowing_area_truncates() {
|
||||
let style = Style::new().green().on_yellow();
|
||||
let span = Span::styled("test content", style);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
|
||||
span.render(Rect::new(10, 0, 20, 1), &mut buf);
|
||||
|
||||
let expected = Buffer::with_lines([Line::from(vec![
|
||||
" ".into(),
|
||||
"test ".green().on_yellow(),
|
||||
])]);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_first_zero_width() {
|
||||
let span = Span::raw("\u{200B}abc");
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
|
||||
span.render(buf.area, &mut buf);
|
||||
assert_eq!(
|
||||
buf.content(),
|
||||
[Cell::new("\u{200B}a"), Cell::new("b"), Cell::new("c"),]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_second_zero_width() {
|
||||
let span = Span::raw("a\u{200B}bc");
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
|
||||
span.render(buf.area, &mut buf);
|
||||
assert_eq!(
|
||||
buf.content(),
|
||||
[Cell::new("a\u{200B}"), Cell::new("b"), Cell::new("c")]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_middle_zero_width() {
|
||||
let span = Span::raw("ab\u{200B}c");
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
|
||||
span.render(buf.area, &mut buf);
|
||||
assert_eq!(
|
||||
buf.content(),
|
||||
[Cell::new("a"), Cell::new("b\u{200B}"), Cell::new("c")]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_last_zero_width() {
|
||||
let span = Span::raw("abc\u{200B}");
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
|
||||
span.render(buf.area, &mut buf);
|
||||
assert_eq!(
|
||||
buf.content(),
|
||||
[Cell::new("a"), Cell::new("b"), Cell::new("c\u{200B}")]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_with_newlines() {
|
||||
let span = Span::raw("a\nb");
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 2, 1));
|
||||
span.render(buf.area, &mut buf);
|
||||
assert_eq!(buf.content(), [Cell::new("a"), Cell::new("b")]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Regression test for <https://github.com/ratatui/ratatui/issues/1160> One line contains
|
||||
/// some Unicode Left-Right-Marks (U+200E)
|
||||
///
|
||||
/// The issue was that a zero-width character at the end of the buffer causes the buffer bounds
|
||||
/// to be exceeded (due to a position + 1 calculation that fails to account for the possibility
|
||||
/// that the next position might not be available).
|
||||
#[test]
|
||||
fn issue_1160() {
|
||||
let span = Span::raw("Hello\u{200E}");
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
span.render(buf.area, &mut buf);
|
||||
assert_eq!(
|
||||
buf.content(),
|
||||
[
|
||||
Cell::new("H"),
|
||||
Cell::new("e"),
|
||||
Cell::new("l"),
|
||||
Cell::new("l"),
|
||||
Cell::new("o\u{200E}"),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add() {
|
||||
assert_eq!(
|
||||
Span::default() + Span::default(),
|
||||
Line::from(vec![Span::default(), Span::default()])
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Span::default() + Span::raw("test"),
|
||||
Line::from(vec![Span::default(), Span::raw("test")])
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Span::raw("test") + Span::default(),
|
||||
Line::from(vec![Span::raw("test"), Span::default()])
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Span::raw("test") + Span::raw("content"),
|
||||
Line::from(vec![Span::raw("test"), Span::raw("content")])
|
||||
);
|
||||
}
|
||||
}
|
||||
1295
ratatui-core/src/text/text.rs
Normal file
1295
ratatui-core/src/text/text.rs
Normal file
File diff suppressed because it is too large
Load Diff
690
ratatui-core/src/widgets.rs
Normal file
690
ratatui-core/src/widgets.rs
Normal file
@@ -0,0 +1,690 @@
|
||||
#![warn(missing_docs)]
|
||||
//! `widgets` is a collection of types that implement [`Widget`] or [`StatefulWidget`] or both.
|
||||
//!
|
||||
//! Widgets are created for each frame as they are consumed after rendered.
|
||||
//! They are not meant to be stored but used as *commands* to draw common figures in the UI.
|
||||
//!
|
||||
//! The available widgets are:
|
||||
//! - [`Block`]: a basic widget that draws a block with optional borders, titles and styles.
|
||||
//! - [`BarChart`]: displays multiple datasets as bars with optional grouping.
|
||||
//! - [`calendar::Monthly`]: displays a single month.
|
||||
//! - [`Canvas`]: draws arbitrary shapes using drawing characters.
|
||||
//! - [`Chart`]: displays multiple datasets as a lines or scatter graph.
|
||||
//! - [`Clear`]: clears the area it occupies. Useful to render over previously drawn widgets.
|
||||
//! - [`Gauge`]: displays progress percentage using block characters.
|
||||
//! - [`LineGauge`]: display progress as a line.
|
||||
//! - [`List`]: displays a list of items and allows selection.
|
||||
//! - [`Paragraph`]: displays a paragraph of optionally styled and wrapped text.
|
||||
//! - [`Scrollbar`]: displays a scrollbar.
|
||||
//! - [`Sparkline`]: display a single data set as a sparkline.
|
||||
//! - [`Table`]: displays multiple rows and columns in a grid and allows selection.
|
||||
//! - [`Tabs`]: displays a tab bar and allows selection.
|
||||
//!
|
||||
//! [`Canvas`]: crate::widgets::canvas::Canvas
|
||||
|
||||
use crate::{buffer::Buffer, layout::Rect, style::Style};
|
||||
|
||||
/// A `Widget` is a type that can be drawn on a [`Buffer`] in a given [`Rect`].
|
||||
///
|
||||
/// Prior to Ratatui 0.26.0, widgets generally were created for each frame as they were consumed
|
||||
/// during rendering. This meant that they were not meant to be stored but used as *commands* to
|
||||
/// draw common figures in the UI.
|
||||
///
|
||||
/// Starting with Ratatui 0.26.0, all the internal widgets implement Widget for a reference to
|
||||
/// themselves. This allows you to store a reference to a widget and render it later. Widget crates
|
||||
/// should consider also doing this to allow for more flexibility in how widgets are used.
|
||||
///
|
||||
/// In Ratatui 0.26.0, we also added an unstable [`WidgetRef`] trait and implemented this on all the
|
||||
/// internal widgets. In addition to the above benefit of rendering references to widgets, this also
|
||||
/// allows you to render boxed widgets. This is useful when you want to store a collection of
|
||||
/// widgets with different types. You can then iterate over the collection and render each widget.
|
||||
/// See <https://github.com/ratatui/ratatui/issues/1287> for more information.
|
||||
///
|
||||
/// In general where you expect a widget to immutably work on its data, we recommended to implement
|
||||
/// `Widget` for a reference to the widget (`impl Widget for &MyWidget`). If you need to store state
|
||||
/// between draw calls, implement `StatefulWidget` if you want the Widget to be immutable, or
|
||||
/// implement `Widget` for a mutable reference to the widget (`impl Widget for &mut MyWidget`) if
|
||||
/// you want the widget to be mutable. The mutable widget pattern is used infrequently in apps, but
|
||||
/// can be quite useful.
|
||||
///
|
||||
/// A blanket implementation of `Widget` for `&W` where `W` implements `WidgetRef` is provided.
|
||||
/// Widget is also implemented for `&str` and `String` types.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use ratatui::{backend::TestBackend, prelude::*, widgets::*};
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
///
|
||||
/// terminal.draw(|frame| {
|
||||
/// frame.render_widget(Clear, frame.area());
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// It's common to render widgets inside other widgets:
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
///
|
||||
/// struct MyWidget;
|
||||
///
|
||||
/// impl Widget for MyWidget {
|
||||
/// fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
/// Line::raw("Hello").render(area, buf);
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub trait Widget {
|
||||
/// 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)
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
/// 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::{backend::TestBackend, prelude::*, widgets::*};
|
||||
///
|
||||
/// // 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 {
|
||||
/// State associated with the stateful widget.
|
||||
///
|
||||
/// If you don't need this then you probably want to implement [`Widget`] instead.
|
||||
type State;
|
||||
/// Draws the current state of the widget in the given buffer. That is the only method required
|
||||
/// to implement a custom stateful widget.
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State);
|
||||
}
|
||||
|
||||
/// A `WidgetRef` is a trait that allows rendering a widget by reference.
|
||||
///
|
||||
/// This trait is useful when you want to store a reference to a widget and render it later. It also
|
||||
/// allows you to render boxed widgets.
|
||||
///
|
||||
/// Boxed widgets allow you to store widgets with a type that is not known at compile time. This is
|
||||
/// useful when you want to store a collection of widgets with different types. You can then iterate
|
||||
/// over the collection and render each widget.
|
||||
///
|
||||
/// This trait was introduced in Ratatui 0.26.0 and is implemented for all the internal widgets. It
|
||||
/// is currently marked as unstable as we are still evaluating the API and may make changes in the
|
||||
/// future. See <https://github.com/ratatui/ratatui/issues/1287> for more information.
|
||||
///
|
||||
/// A blanket implementation of `Widget` for `&W` where `W` implements `WidgetRef` is provided.
|
||||
///
|
||||
/// A blanket implementation of `WidgetRef` for `Option<W>` where `W` implements `WidgetRef` is
|
||||
/// provided. This is a convenience approach to make it easier to attach child widgets to parent
|
||||
/// widgets. It allows you to render an optional widget by reference.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # #[cfg(feature = "unstable-widget-ref")] {
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
///
|
||||
/// struct Greeting;
|
||||
///
|
||||
/// struct Farewell;
|
||||
///
|
||||
/// impl WidgetRef for Greeting {
|
||||
/// fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
/// Line::raw("Hello").render(area, buf);
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// /// Only needed for backwards compatibility
|
||||
/// impl Widget for Greeting {
|
||||
/// fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
/// self.render_ref(area, buf);
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// impl WidgetRef for Farewell {
|
||||
/// fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
/// Line::raw("Goodbye").right_aligned().render(area, buf);
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// /// Only needed for backwards compatibility
|
||||
/// impl Widget for Farewell {
|
||||
/// fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
/// self.render_ref(area, buf);
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// # fn render(area: Rect, buf: &mut Buffer) {
|
||||
/// let greeting = Greeting;
|
||||
/// let farewell = Farewell;
|
||||
///
|
||||
/// // these calls do not consume the widgets, so they can be used again later
|
||||
/// greeting.render_ref(area, buf);
|
||||
/// farewell.render_ref(area, buf);
|
||||
///
|
||||
/// // a collection of widgets with different types
|
||||
/// let widgets: Vec<Box<dyn WidgetRef>> = vec![Box::new(greeting), Box::new(farewell)];
|
||||
/// for widget in widgets {
|
||||
/// widget.render_ref(area, buf);
|
||||
/// }
|
||||
/// # }
|
||||
/// # }
|
||||
/// ```
|
||||
#[instability::unstable(feature = "widget-ref")]
|
||||
pub trait WidgetRef {
|
||||
/// Draws the current state of the widget in the given buffer. That is the only method required
|
||||
/// to implement a custom widget.
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer);
|
||||
}
|
||||
|
||||
/// This allows you to render a widget by reference.
|
||||
impl<W: WidgetRef> Widget for &W {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_ref(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
/// A blanket implementation of `WidgetExt` for `Option<W>` where `W` implements `WidgetRef`.
|
||||
///
|
||||
/// This is a convenience implementation that makes it easy to attach child widgets to parent
|
||||
/// widgets. It allows you to render an optional widget by reference.
|
||||
///
|
||||
/// The internal widgets use this pattern to render the optional `Block` widgets that are included
|
||||
/// on most widgets.
|
||||
/// Blanket implementation of `WidgetExt` for `Option<W>` where `W` implements `WidgetRef`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # #[cfg(feature = "unstable-widget-ref")] {
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
///
|
||||
/// struct Parent {
|
||||
/// child: Option<Child>,
|
||||
/// }
|
||||
///
|
||||
/// struct Child;
|
||||
///
|
||||
/// impl WidgetRef for Child {
|
||||
/// fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
/// Line::raw("Hello from child").render(area, buf);
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// impl WidgetRef for Parent {
|
||||
/// fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
/// self.child.render_ref(area, buf);
|
||||
/// }
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
impl<W: WidgetRef> WidgetRef for Option<W> {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
if let Some(widget) = self {
|
||||
widget.render_ref(area, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A `StatefulWidgetRef` is a trait that allows rendering a stateful widget by reference.
|
||||
///
|
||||
/// This is the stateful equivalent of `WidgetRef`. It is useful when you want to store a reference
|
||||
/// to a stateful widget and render it later. It also allows you to render boxed stateful widgets.
|
||||
///
|
||||
/// This trait was introduced in Ratatui 0.26.0 and is implemented for all the internal stateful
|
||||
/// widgets. It is currently marked as unstable as we are still evaluating the API and may make
|
||||
/// changes in the future. See <https://github.com/ratatui/ratatui/issues/1287> for more
|
||||
/// information.
|
||||
///
|
||||
/// A blanket implementation of `StatefulWidget` for `&W` where `W` implements `StatefulWidgetRef`
|
||||
/// is provided.
|
||||
///
|
||||
/// See the documentation for [`WidgetRef`] for more information on boxed widgets.
|
||||
/// See the documentation for [`StatefulWidget`] for more information on stateful widgets.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # #[cfg(feature = "unstable-widget-ref")] {
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
///
|
||||
/// struct PersonalGreeting;
|
||||
///
|
||||
/// impl StatefulWidgetRef for PersonalGreeting {
|
||||
/// type State = String;
|
||||
/// fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
/// Line::raw(format!("Hello {}", state)).render(area, buf);
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// impl StatefulWidget for PersonalGreeting {
|
||||
/// type State = String;
|
||||
/// fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
/// (&self).render_ref(area, buf, state);
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// fn render(area: Rect, buf: &mut Buffer) {
|
||||
/// let widget = PersonalGreeting;
|
||||
/// let mut state = "world".to_string();
|
||||
/// widget.render(area, buf, &mut state);
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
#[instability::unstable(feature = "widget-ref")]
|
||||
pub trait StatefulWidgetRef {
|
||||
/// State associated with the stateful widget.
|
||||
///
|
||||
/// If you don't need this then you probably want to implement [`WidgetRef`] instead.
|
||||
type State;
|
||||
/// Draws the current state of the widget in the given buffer. That is the only method required
|
||||
/// to implement a custom stateful widget.
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State);
|
||||
}
|
||||
|
||||
// Note: while StatefulWidgetRef is marked as unstable, the blanket implementation of StatefulWidget
|
||||
// cannot be implemented as W::State is effectively pub(crate) and not accessible from outside the
|
||||
// crate. Once stabilized, this blanket implementation can be added and the specific implementations
|
||||
// on Table and List can be removed.
|
||||
//
|
||||
// /// Blanket implementation of `StatefulWidget` for `&W` where `W` implements `StatefulWidgetRef`.
|
||||
// ///
|
||||
// /// This allows you to render a stateful widget by reference.
|
||||
// impl<W: StatefulWidgetRef> StatefulWidget for &W {
|
||||
// type State = W::State;
|
||||
// fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
// StatefulWidgetRef::render_ref(self, area, buf, state);
|
||||
// }
|
||||
// }
|
||||
|
||||
/// Renders a string slice as a widget.
|
||||
///
|
||||
/// This implementation allows a string slice (`&str`) to act as a widget, meaning it can be drawn
|
||||
/// onto a [`Buffer`] in a specified [`Rect`]. The slice represents a static string which can be
|
||||
/// rendered by reference, thereby avoiding the need for string cloning or ownership transfer when
|
||||
/// drawing the text to the screen.
|
||||
impl Widget for &str {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_ref(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides the ability to render a string slice by reference.
|
||||
///
|
||||
/// This trait implementation ensures that a string slice, which is an immutable view over a
|
||||
/// `String`, can be drawn on demand without requiring ownership of the string itself. It utilizes
|
||||
/// the default text style when rendering onto the provided [`Buffer`] at the position defined by
|
||||
/// [`Rect`].
|
||||
impl WidgetRef for &str {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_stringn(area.x, area.y, self, area.width as usize, Style::new());
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders a `String` object as a widget.
|
||||
///
|
||||
/// This implementation enables an owned `String` to be treated as a widget, which can be rendered
|
||||
/// on a [`Buffer`] within the bounds of a given [`Rect`].
|
||||
impl Widget for String {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_ref(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides the ability to render a `String` by reference.
|
||||
///
|
||||
/// This trait allows for a `String` to be rendered onto the [`Buffer`], similarly using the default
|
||||
/// style settings. It ensures that an owned `String` can be rendered efficiently by reference,
|
||||
/// without the need to give up ownership of the underlying text.
|
||||
impl WidgetRef for String {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_stringn(area.x, area.y, self, area.width as usize, Style::new());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::{fixture, rstest};
|
||||
|
||||
use super::*;
|
||||
use crate::text::Line;
|
||||
|
||||
#[fixture]
|
||||
fn buf() -> Buffer {
|
||||
Buffer::empty(Rect::new(0, 0, 20, 1))
|
||||
}
|
||||
|
||||
mod widget {
|
||||
use super::*;
|
||||
|
||||
struct Greeting;
|
||||
|
||||
impl Widget for Greeting {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
Line::from("Hello").render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render(mut buf: Buffer) {
|
||||
let widget = Greeting;
|
||||
widget.render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello "]));
|
||||
}
|
||||
}
|
||||
|
||||
mod widget_ref {
|
||||
use super::*;
|
||||
|
||||
struct Greeting;
|
||||
struct Farewell;
|
||||
|
||||
impl WidgetRef for Greeting {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
Line::from("Hello").render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetRef for Farewell {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
Line::from("Goodbye").right_aligned().render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render_ref(mut buf: Buffer) {
|
||||
let widget = Greeting;
|
||||
widget.render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello "]));
|
||||
}
|
||||
|
||||
/// Ensure that the blanket implementation of `Widget` for `&W` where `W` implements
|
||||
/// `WidgetRef` works as expected.
|
||||
#[rstest]
|
||||
fn blanket_render(mut buf: Buffer) {
|
||||
let widget = &Greeting;
|
||||
widget.render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn box_render_ref(mut buf: Buffer) {
|
||||
let widget: Box<dyn WidgetRef> = Box::new(Greeting);
|
||||
widget.render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn vec_box_render(mut buf: Buffer) {
|
||||
let widgets: Vec<Box<dyn WidgetRef>> = vec![Box::new(Greeting), Box::new(Farewell)];
|
||||
for widget in widgets {
|
||||
widget.render_ref(buf.area, &mut buf);
|
||||
}
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello Goodbye"]));
|
||||
}
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
fn state() -> String {
|
||||
"world".to_string()
|
||||
}
|
||||
|
||||
mod stateful_widget {
|
||||
use super::*;
|
||||
|
||||
struct PersonalGreeting;
|
||||
|
||||
impl StatefulWidget for PersonalGreeting {
|
||||
type State = String;
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
Line::from(format!("Hello {state}")).render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render(mut buf: Buffer, mut state: String) {
|
||||
let widget = PersonalGreeting;
|
||||
widget.render(buf.area, &mut buf, &mut state);
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello world "]));
|
||||
}
|
||||
}
|
||||
|
||||
mod stateful_widget_ref {
|
||||
use super::*;
|
||||
|
||||
struct PersonalGreeting;
|
||||
|
||||
impl StatefulWidgetRef for PersonalGreeting {
|
||||
type State = String;
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
Line::from(format!("Hello {state}")).render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render_ref(mut buf: Buffer, mut state: String) {
|
||||
let widget = PersonalGreeting;
|
||||
widget.render_ref(buf.area, &mut buf, &mut state);
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello world "]));
|
||||
}
|
||||
|
||||
// Note this cannot be tested until the blanket implementation of StatefulWidget for &W
|
||||
// where W implements StatefulWidgetRef is added. (see the comment in the blanket
|
||||
// implementation for more).
|
||||
// /// This test is to ensure that the blanket implementation of `StatefulWidget` for `&W`
|
||||
// where /// `W` implements `StatefulWidgetRef` works as expected.
|
||||
// #[rstest]
|
||||
// fn stateful_widget_blanket_render(mut buf: Buffer, mut state: String) {
|
||||
// let widget = &PersonalGreeting;
|
||||
// widget.render(buf.area, &mut buf, &mut state);
|
||||
// assert_eq!(buf, Buffer::with_lines(["Hello world "]));
|
||||
// }
|
||||
|
||||
#[rstest]
|
||||
fn box_render_render(mut buf: Buffer, mut state: String) {
|
||||
let widget = Box::new(PersonalGreeting);
|
||||
widget.render_ref(buf.area, &mut buf, &mut state);
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello world "]));
|
||||
}
|
||||
}
|
||||
|
||||
mod option_widget_ref {
|
||||
use super::*;
|
||||
|
||||
struct Greeting;
|
||||
|
||||
impl WidgetRef for Greeting {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
Line::from("Hello").render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render_ref_some(mut buf: Buffer) {
|
||||
let widget = Some(Greeting);
|
||||
widget.render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render_ref_none(mut buf: Buffer) {
|
||||
let widget: Option<Greeting> = None;
|
||||
widget.render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([" "]));
|
||||
}
|
||||
}
|
||||
|
||||
mod str {
|
||||
use super::*;
|
||||
|
||||
#[rstest]
|
||||
fn render(mut buf: Buffer) {
|
||||
"hello world".render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render_area(mut buf: Buffer) {
|
||||
let area = Rect::new(buf.area.x, buf.area.y, 11, buf.area.height);
|
||||
"hello world, just hello".render(area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render_ref(mut buf: Buffer) {
|
||||
"hello world".render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn option_render(mut buf: Buffer) {
|
||||
Some("hello world").render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn option_render_ref(mut buf: Buffer) {
|
||||
Some("hello world").render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
}
|
||||
|
||||
mod string {
|
||||
use super::*;
|
||||
#[rstest]
|
||||
fn render(mut buf: Buffer) {
|
||||
String::from("hello world").render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render_area(mut buf: Buffer) {
|
||||
let area = Rect::new(buf.area.x, buf.area.y, 11, buf.area.height);
|
||||
String::from("hello world, just hello").render(area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render_ref(mut buf: Buffer) {
|
||||
String::from("hello world").render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn option_render(mut buf: Buffer) {
|
||||
Some(String::from("hello world")).render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn option_render_ref(mut buf: Buffer) {
|
||||
Some(String::from("hello world")).render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
}
|
||||
}
|
||||
20
ratatui-crossterm/Cargo.toml
Normal file
20
ratatui-crossterm/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "ratatui-crossterm"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
default = ["underline-color"]
|
||||
|
||||
## enables the backend code that sets the underline color.
|
||||
## Underline color is only supported by the [`CrosstermBackend`](backend::CrosstermBackend) backend,
|
||||
## and is not supported on Windows 7.
|
||||
underline-color = []
|
||||
|
||||
[dependencies]
|
||||
ratatui-core = { workspace = true }
|
||||
crossterm.workspace = true
|
||||
instability.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
rstest.workspace = true
|
||||
681
ratatui-crossterm/src/lib.rs
Normal file
681
ratatui-crossterm/src/lib.rs
Normal file
@@ -0,0 +1,681 @@
|
||||
//! This module provides the [`CrosstermBackend`] implementation for the [`Backend`] trait. It uses
|
||||
//! the [Crossterm] crate to interact with the terminal.
|
||||
//!
|
||||
//! [Crossterm]: https://crates.io/crates/crossterm
|
||||
use std::io::{self, Write};
|
||||
|
||||
#[cfg(feature = "underline-color")]
|
||||
use crossterm::style::SetUnderlineColor;
|
||||
use crossterm::{
|
||||
cursor::{Hide, MoveTo, Show},
|
||||
execute, queue,
|
||||
style::{
|
||||
Attribute as CrosstermAttribute, Attributes as CrosstermAttributes,
|
||||
Color as CrosstermColor, Colors, ContentStyle, Print, SetAttribute, SetBackgroundColor,
|
||||
SetColors, SetForegroundColor,
|
||||
},
|
||||
terminal::{self, Clear},
|
||||
};
|
||||
use ratatui_core::{
|
||||
backend::{Backend, ClearType, WindowSize},
|
||||
buffer::Cell,
|
||||
layout::{Position, Size},
|
||||
style::{Color, Modifier, Style},
|
||||
};
|
||||
|
||||
/// A [`Backend`] implementation that uses [Crossterm] to render to the terminal.
|
||||
///
|
||||
/// The `CrosstermBackend` struct is a wrapper around a writer 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.
|
||||
///
|
||||
/// Most applications should not call the methods on `CrosstermBackend` directly, but will instead
|
||||
/// use the [`Terminal`] struct, which provides a more ergonomic interface.
|
||||
///
|
||||
/// Usually applications will enable raw mode and switch to alternate screen mode after creating
|
||||
/// a `CrosstermBackend`. This is done by calling [`crossterm::terminal::enable_raw_mode`] and
|
||||
/// [`crossterm::terminal::EnterAlternateScreen`] (and the corresponding disable/leave functions
|
||||
/// when the application exits). This is not done automatically by the backend because it is
|
||||
/// possible that the application may want to use the terminal for other purposes (like showing
|
||||
/// help text) before entering alternate screen mode.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use std::io::{stderr, stdout};
|
||||
///
|
||||
/// use ratatui::{
|
||||
/// crossterm::{
|
||||
/// terminal::{
|
||||
/// disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
||||
/// },
|
||||
/// ExecutableCommand,
|
||||
/// },
|
||||
/// prelude::*,
|
||||
/// };
|
||||
///
|
||||
/// let mut backend = CrosstermBackend::new(stdout());
|
||||
/// // or
|
||||
/// let backend = CrosstermBackend::new(stderr());
|
||||
/// let mut terminal = Terminal::new(backend)?;
|
||||
///
|
||||
/// enable_raw_mode()?;
|
||||
/// stdout().execute(EnterAlternateScreen)?;
|
||||
///
|
||||
/// terminal.clear()?;
|
||||
/// terminal.draw(|frame| {
|
||||
/// // -- snip --
|
||||
/// })?;
|
||||
///
|
||||
/// stdout().execute(LeaveAlternateScreen)?;
|
||||
/// disable_raw_mode()?;
|
||||
///
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
///
|
||||
/// See the the [Examples] directory for more examples. See the [`backend`] module documentation
|
||||
/// for more details on raw mode and alternate screen.
|
||||
///
|
||||
/// [`Write`]: std::io::Write
|
||||
/// [`Terminal`]: crate::terminal::Terminal
|
||||
/// [`backend`]: crate::backend
|
||||
/// [Crossterm]: https://crates.io/crates/crossterm
|
||||
/// [Examples]: https://github.com/ratatui/ratatui/tree/main/examples/README.md
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct CrosstermBackend<W: Write> {
|
||||
/// The writer used to send commands to the terminal.
|
||||
writer: W,
|
||||
}
|
||||
|
||||
impl<W> CrosstermBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
/// Creates a new `CrosstermBackend` with the given writer.
|
||||
///
|
||||
/// Most applications will use either [`stdout`](std::io::stdout) or
|
||||
/// [`stderr`](std::io::stderr) as writer. See the [FAQ] to determine which one to use.
|
||||
///
|
||||
/// [FAQ]: https://ratatui.rs/faq/#should-i-use-stdout-or-stderr
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use std::io::stdout;
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let backend = CrosstermBackend::new(stdout());
|
||||
/// ```
|
||||
pub const fn new(writer: W) -> Self {
|
||||
Self { writer }
|
||||
}
|
||||
|
||||
/// Gets the writer.
|
||||
#[instability::unstable(
|
||||
feature = "backend-writer",
|
||||
issue = "https://github.com/ratatui/ratatui/pull/991"
|
||||
)]
|
||||
pub const fn writer(&self) -> &W {
|
||||
&self.writer
|
||||
}
|
||||
|
||||
/// Gets the writer as a mutable reference.
|
||||
///
|
||||
/// Note: writing to the writer may cause incorrect output after the write. This is due to the
|
||||
/// way that the Terminal implements diffing Buffers.
|
||||
#[instability::unstable(
|
||||
feature = "backend-writer",
|
||||
issue = "https://github.com/ratatui/ratatui/pull/991"
|
||||
)]
|
||||
pub fn writer_mut(&mut self) -> &mut W {
|
||||
&mut self.writer
|
||||
}
|
||||
}
|
||||
|
||||
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.writer.write(buf)
|
||||
}
|
||||
|
||||
/// Flushes the underlying buffer.
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.writer.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;
|
||||
#[cfg(feature = "underline-color")]
|
||||
let mut underline_color = Color::Reset;
|
||||
let mut modifier = Modifier::empty();
|
||||
let mut last_pos: Option<Position> = 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.x + 1 && y == p.y) {
|
||||
queue!(self.writer, MoveTo(x, y))?;
|
||||
}
|
||||
last_pos = Some(Position { x, y });
|
||||
if cell.modifier != modifier {
|
||||
let diff = ModifierDiff {
|
||||
from: modifier,
|
||||
to: cell.modifier,
|
||||
};
|
||||
diff.queue(&mut self.writer)?;
|
||||
modifier = cell.modifier;
|
||||
}
|
||||
if cell.fg != fg || cell.bg != bg {
|
||||
queue!(
|
||||
self.writer,
|
||||
SetColors(Colors::new(
|
||||
from_ratatui_color(cell.fg),
|
||||
from_ratatui_color(cell.bg)
|
||||
))
|
||||
)?;
|
||||
fg = cell.fg;
|
||||
bg = cell.bg;
|
||||
}
|
||||
#[cfg(feature = "underline-color")]
|
||||
if cell.underline_color != underline_color {
|
||||
let color = from_ratatui_color(cell.underline_color);
|
||||
queue!(self.writer, SetUnderlineColor(color))?;
|
||||
underline_color = cell.underline_color;
|
||||
}
|
||||
|
||||
queue!(self.writer, Print(cell.symbol()))?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "underline-color")]
|
||||
return queue!(
|
||||
self.writer,
|
||||
SetForegroundColor(CrosstermColor::Reset),
|
||||
SetBackgroundColor(CrosstermColor::Reset),
|
||||
SetUnderlineColor(CrosstermColor::Reset),
|
||||
SetAttribute(CrosstermAttribute::Reset),
|
||||
);
|
||||
#[cfg(not(feature = "underline-color"))]
|
||||
return queue!(
|
||||
self.writer,
|
||||
SetForegroundColor(CrosstermColor::Reset),
|
||||
SetBackgroundColor(CrosstermColor::Reset),
|
||||
SetAttribute(CrosstermAttribute::Reset),
|
||||
);
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
execute!(self.writer, Hide)
|
||||
}
|
||||
|
||||
fn show_cursor(&mut self) -> io::Result<()> {
|
||||
execute!(self.writer, Show)
|
||||
}
|
||||
|
||||
fn get_cursor_position(&mut self) -> io::Result<Position> {
|
||||
crossterm::cursor::position()
|
||||
.map(|(x, y)| Position { x, y })
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
|
||||
}
|
||||
|
||||
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
|
||||
let Position { x, y } = position.into();
|
||||
execute!(self.writer, MoveTo(x, y))
|
||||
}
|
||||
|
||||
fn clear(&mut self) -> io::Result<()> {
|
||||
self.clear_region(ClearType::All)
|
||||
}
|
||||
|
||||
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
|
||||
execute!(
|
||||
self.writer,
|
||||
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 {
|
||||
queue!(self.writer, Print("\n"))?;
|
||||
}
|
||||
self.writer.flush()
|
||||
}
|
||||
|
||||
fn size(&self) -> io::Result<Size> {
|
||||
let (width, height) = terminal::size()?;
|
||||
Ok(Size { width, height })
|
||||
}
|
||||
|
||||
fn window_size(&mut self) -> io::Result<WindowSize> {
|
||||
let crossterm::terminal::WindowSize {
|
||||
columns,
|
||||
rows,
|
||||
width,
|
||||
height,
|
||||
} = terminal::window_size()?;
|
||||
Ok(WindowSize {
|
||||
columns_rows: Size {
|
||||
width: columns,
|
||||
height: rows,
|
||||
},
|
||||
pixels: Size { width, height },
|
||||
})
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.writer.flush()
|
||||
}
|
||||
}
|
||||
|
||||
fn from_ratatui_color(color: Color) -> CrosstermColor {
|
||||
match color {
|
||||
Color::Reset => CrosstermColor::Reset,
|
||||
Color::Black => CrosstermColor::Black,
|
||||
Color::Red => CrosstermColor::DarkRed,
|
||||
Color::Green => CrosstermColor::DarkGreen,
|
||||
Color::Yellow => CrosstermColor::DarkYellow,
|
||||
Color::Blue => CrosstermColor::DarkBlue,
|
||||
Color::Magenta => CrosstermColor::DarkMagenta,
|
||||
Color::Cyan => CrosstermColor::DarkCyan,
|
||||
Color::Gray => CrosstermColor::Grey,
|
||||
Color::DarkGray => CrosstermColor::DarkGrey,
|
||||
Color::LightRed => CrosstermColor::Red,
|
||||
Color::LightGreen => CrosstermColor::Green,
|
||||
Color::LightBlue => CrosstermColor::Blue,
|
||||
Color::LightYellow => CrosstermColor::Yellow,
|
||||
Color::LightMagenta => CrosstermColor::Magenta,
|
||||
Color::LightCyan => CrosstermColor::Cyan,
|
||||
Color::White => CrosstermColor::White,
|
||||
Color::Indexed(i) => CrosstermColor::AnsiValue(i),
|
||||
Color::Rgb(r, g, b) => CrosstermColor::Rgb { r, g, b },
|
||||
}
|
||||
}
|
||||
|
||||
fn from_crossterm_color(value: CrosstermColor) -> Color {
|
||||
match value {
|
||||
CrosstermColor::Reset => Color::Reset,
|
||||
CrosstermColor::Black => Color::Black,
|
||||
CrosstermColor::DarkRed => Color::Red,
|
||||
CrosstermColor::DarkGreen => Color::Green,
|
||||
CrosstermColor::DarkYellow => Color::Yellow,
|
||||
CrosstermColor::DarkBlue => Color::Blue,
|
||||
CrosstermColor::DarkMagenta => Color::Magenta,
|
||||
CrosstermColor::DarkCyan => Color::Cyan,
|
||||
CrosstermColor::Grey => Color::Gray,
|
||||
CrosstermColor::DarkGrey => Color::DarkGray,
|
||||
CrosstermColor::Red => Color::LightRed,
|
||||
CrosstermColor::Green => Color::LightGreen,
|
||||
CrosstermColor::Blue => Color::LightBlue,
|
||||
CrosstermColor::Yellow => Color::LightYellow,
|
||||
CrosstermColor::Magenta => Color::LightMagenta,
|
||||
CrosstermColor::Cyan => Color::LightCyan,
|
||||
CrosstermColor::White => Color::White,
|
||||
CrosstermColor::Rgb { r, g, b } => Color::Rgb(r, g, b),
|
||||
CrosstermColor::AnsiValue(v) => Color::Indexed(v),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
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) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::NoReverse))?;
|
||||
}
|
||||
if removed.contains(Modifier::BOLD) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::NormalIntensity))?;
|
||||
if self.to.contains(Modifier::DIM) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::Dim))?;
|
||||
}
|
||||
}
|
||||
if removed.contains(Modifier::ITALIC) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::NoItalic))?;
|
||||
}
|
||||
if removed.contains(Modifier::UNDERLINED) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::NoUnderline))?;
|
||||
}
|
||||
if removed.contains(Modifier::DIM) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::NormalIntensity))?;
|
||||
}
|
||||
if removed.contains(Modifier::CROSSED_OUT) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::NotCrossedOut))?;
|
||||
}
|
||||
if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::NoBlink))?;
|
||||
}
|
||||
|
||||
let added = self.to - self.from;
|
||||
if added.contains(Modifier::REVERSED) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::Reverse))?;
|
||||
}
|
||||
if added.contains(Modifier::BOLD) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::Bold))?;
|
||||
}
|
||||
if added.contains(Modifier::ITALIC) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::Italic))?;
|
||||
}
|
||||
if added.contains(Modifier::UNDERLINED) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::Underlined))?;
|
||||
}
|
||||
if added.contains(Modifier::DIM) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::Dim))?;
|
||||
}
|
||||
if added.contains(Modifier::CROSSED_OUT) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::CrossedOut))?;
|
||||
}
|
||||
if added.contains(Modifier::SLOW_BLINK) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::SlowBlink))?;
|
||||
}
|
||||
if added.contains(Modifier::RAPID_BLINK) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::RapidBlink))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn from_crossterm_attribute(value: CrosstermAttribute) -> Modifier {
|
||||
// `Attribute*s*` (note the *s*) contains multiple `Attribute`
|
||||
// We convert `Attribute` to `Attribute*s*` (containing only 1 value) to avoid implementing
|
||||
// the conversion again
|
||||
from_crossterm_attributes(CrosstermAttributes::from(value))
|
||||
}
|
||||
|
||||
fn from_crossterm_attributes(value: CrosstermAttributes) -> Modifier {
|
||||
let mut res = Modifier::empty();
|
||||
|
||||
if value.has(CrosstermAttribute::Bold) {
|
||||
res |= Modifier::BOLD;
|
||||
}
|
||||
if value.has(CrosstermAttribute::Dim) {
|
||||
res |= Modifier::DIM;
|
||||
}
|
||||
if value.has(CrosstermAttribute::Italic) {
|
||||
res |= Modifier::ITALIC;
|
||||
}
|
||||
if value.has(CrosstermAttribute::Underlined)
|
||||
|| value.has(CrosstermAttribute::DoubleUnderlined)
|
||||
|| value.has(CrosstermAttribute::Undercurled)
|
||||
|| value.has(CrosstermAttribute::Underdotted)
|
||||
|| value.has(CrosstermAttribute::Underdashed)
|
||||
{
|
||||
res |= Modifier::UNDERLINED;
|
||||
}
|
||||
if value.has(CrosstermAttribute::SlowBlink) {
|
||||
res |= Modifier::SLOW_BLINK;
|
||||
}
|
||||
if value.has(CrosstermAttribute::RapidBlink) {
|
||||
res |= Modifier::RAPID_BLINK;
|
||||
}
|
||||
if value.has(CrosstermAttribute::Reverse) {
|
||||
res |= Modifier::REVERSED;
|
||||
}
|
||||
if value.has(CrosstermAttribute::Hidden) {
|
||||
res |= Modifier::HIDDEN;
|
||||
}
|
||||
if value.has(CrosstermAttribute::CrossedOut) {
|
||||
res |= Modifier::CROSSED_OUT;
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
fn from_crossterm_style(value: ContentStyle) -> Style {
|
||||
let mut sub_modifier = Modifier::empty();
|
||||
|
||||
if value.attributes.has(CrosstermAttribute::NoBold) {
|
||||
sub_modifier |= Modifier::BOLD;
|
||||
}
|
||||
if value.attributes.has(CrosstermAttribute::NoItalic) {
|
||||
sub_modifier |= Modifier::ITALIC;
|
||||
}
|
||||
if value.attributes.has(CrosstermAttribute::NotCrossedOut) {
|
||||
sub_modifier |= Modifier::CROSSED_OUT;
|
||||
}
|
||||
if value.attributes.has(CrosstermAttribute::NoUnderline) {
|
||||
sub_modifier |= Modifier::UNDERLINED;
|
||||
}
|
||||
if value.attributes.has(CrosstermAttribute::NoHidden) {
|
||||
sub_modifier |= Modifier::HIDDEN;
|
||||
}
|
||||
if value.attributes.has(CrosstermAttribute::NoBlink) {
|
||||
sub_modifier |= Modifier::RAPID_BLINK | Modifier::SLOW_BLINK;
|
||||
}
|
||||
if value.attributes.has(CrosstermAttribute::NoReverse) {
|
||||
sub_modifier |= Modifier::REVERSED;
|
||||
}
|
||||
|
||||
Style {
|
||||
fg: value.foreground_color.map(from_crossterm_color),
|
||||
bg: value.background_color.map(from_crossterm_color),
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: value.underline_color.map(from_crossterm_color),
|
||||
add_modifier: from_crossterm_attributes(value.attributes),
|
||||
sub_modifier,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[rstest]
|
||||
#[case(CrosstermColor::Reset, Color::Reset)]
|
||||
#[case(CrosstermColor::Black, Color::Black)]
|
||||
#[case(CrosstermColor::DarkGrey, Color::DarkGray)]
|
||||
#[case(CrosstermColor::Red, Color::LightRed)]
|
||||
#[case(CrosstermColor::DarkRed, Color::Red)]
|
||||
#[case(CrosstermColor::Green, Color::LightGreen)]
|
||||
#[case(CrosstermColor::DarkGreen, Color::Green)]
|
||||
#[case(CrosstermColor::Yellow, Color::LightYellow)]
|
||||
#[case(CrosstermColor::DarkYellow, Color::Yellow)]
|
||||
#[case(CrosstermColor::Blue, Color::LightBlue)]
|
||||
#[case(CrosstermColor::DarkBlue, Color::Blue)]
|
||||
#[case(CrosstermColor::Magenta, Color::LightMagenta)]
|
||||
#[case(CrosstermColor::DarkMagenta, Color::Magenta)]
|
||||
#[case(CrosstermColor::Cyan, Color::LightCyan)]
|
||||
#[case(CrosstermColor::DarkCyan, Color::Cyan)]
|
||||
#[case(CrosstermColor::White, Color::White)]
|
||||
#[case(CrosstermColor::Grey, Color::Gray)]
|
||||
#[case(CrosstermColor::Rgb { r: 0, g: 0, b: 0 }, Color::Rgb(0, 0, 0))]
|
||||
#[case(CrosstermColor::Rgb { r: 10, g: 20, b: 30 }, Color::Rgb(10, 20, 30))]
|
||||
#[case(CrosstermColor::AnsiValue(32), Color::Indexed(32))]
|
||||
#[case(CrosstermColor::AnsiValue(37), Color::Indexed(37))]
|
||||
fn convert_from_crossterm_color(#[case] value: CrosstermColor, #[case] expected: Color) {
|
||||
assert_eq!(from_crossterm_color(value), expected);
|
||||
}
|
||||
|
||||
mod modifier {
|
||||
use super::*;
|
||||
|
||||
#[rstest]
|
||||
#[rstest]
|
||||
#[case(CrosstermAttribute::Reset, Modifier::empty())]
|
||||
#[case(CrosstermAttribute::Bold, Modifier::BOLD)]
|
||||
#[case(CrosstermAttribute::Italic, Modifier::ITALIC)]
|
||||
#[case(CrosstermAttribute::Underlined, Modifier::UNDERLINED)]
|
||||
#[case(CrosstermAttribute::DoubleUnderlined, Modifier::UNDERLINED)]
|
||||
#[case(CrosstermAttribute::Underdotted, Modifier::UNDERLINED)]
|
||||
#[case(CrosstermAttribute::Dim, Modifier::DIM)]
|
||||
#[case(CrosstermAttribute::NormalIntensity, Modifier::empty())]
|
||||
#[case(CrosstermAttribute::CrossedOut, Modifier::CROSSED_OUT)]
|
||||
#[case(CrosstermAttribute::NoUnderline, Modifier::empty())]
|
||||
#[case(CrosstermAttribute::OverLined, Modifier::empty())]
|
||||
#[case(CrosstermAttribute::SlowBlink, Modifier::SLOW_BLINK)]
|
||||
#[case(CrosstermAttribute::RapidBlink, Modifier::RAPID_BLINK)]
|
||||
#[case(CrosstermAttribute::Hidden, Modifier::HIDDEN)]
|
||||
#[case(CrosstermAttribute::NoHidden, Modifier::empty())]
|
||||
#[case(CrosstermAttribute::Reverse, Modifier::REVERSED)]
|
||||
fn convert_from_crossterm_attribute(
|
||||
#[case] value: CrosstermAttribute,
|
||||
#[case] expected: Modifier,
|
||||
) {
|
||||
assert_eq!(from_crossterm_attribute(value), expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(&[CrosstermAttribute::Bold], Modifier::BOLD)]
|
||||
#[case(
|
||||
&[CrosstermAttribute::Bold, CrosstermAttribute::Italic],
|
||||
Modifier::BOLD | Modifier::ITALIC
|
||||
)]
|
||||
#[case(
|
||||
&[CrosstermAttribute::Bold, CrosstermAttribute::NotCrossedOut],
|
||||
Modifier::BOLD
|
||||
)]
|
||||
#[case(
|
||||
&[CrosstermAttribute::Dim, CrosstermAttribute::Underdotted],
|
||||
Modifier::DIM | Modifier::UNDERLINED
|
||||
)]
|
||||
#[case(
|
||||
&[CrosstermAttribute::Dim, CrosstermAttribute::SlowBlink, CrosstermAttribute::Italic],
|
||||
Modifier::DIM | Modifier::SLOW_BLINK | Modifier::ITALIC
|
||||
)]
|
||||
#[case(
|
||||
&[CrosstermAttribute::Hidden, CrosstermAttribute::NoUnderline, CrosstermAttribute::NotCrossedOut],
|
||||
Modifier::HIDDEN
|
||||
)]
|
||||
#[case(
|
||||
&[CrosstermAttribute::Reverse],
|
||||
Modifier::REVERSED
|
||||
)]
|
||||
#[case(
|
||||
&[CrosstermAttribute::Reset],
|
||||
Modifier::empty()
|
||||
)]
|
||||
#[case(
|
||||
&[CrosstermAttribute::RapidBlink, CrosstermAttribute::CrossedOut],
|
||||
Modifier::RAPID_BLINK | Modifier::CROSSED_OUT
|
||||
)]
|
||||
#[case(
|
||||
&[CrosstermAttribute::DoubleUnderlined, CrosstermAttribute::OverLined],
|
||||
Modifier::UNDERLINED
|
||||
)]
|
||||
#[case(
|
||||
&[CrosstermAttribute::Undercurled, CrosstermAttribute::Underdashed],
|
||||
Modifier::UNDERLINED
|
||||
)]
|
||||
#[case(
|
||||
&[CrosstermAttribute::NoBold, CrosstermAttribute::NoItalic],
|
||||
Modifier::empty()
|
||||
)]
|
||||
#[case(
|
||||
&[CrosstermAttribute::NoBlink, CrosstermAttribute::NoReverse],
|
||||
Modifier::empty()
|
||||
)]
|
||||
fn convert_from_crossterm_attributes(
|
||||
#[case] value: &[CrosstermAttribute],
|
||||
#[case] expected: Modifier,
|
||||
) {
|
||||
assert_eq!(
|
||||
from_crossterm_attributes(CrosstermAttributes::from(value)),
|
||||
expected
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(ContentStyle::default(), Style::default())]
|
||||
#[case(
|
||||
ContentStyle {
|
||||
foreground_color: Some(CrosstermColor::DarkYellow),
|
||||
..Default::default()
|
||||
},
|
||||
Style::default().fg(Color::Yellow)
|
||||
)]
|
||||
#[case(
|
||||
ContentStyle {
|
||||
background_color: Some(CrosstermColor::DarkYellow),
|
||||
..Default::default()
|
||||
},
|
||||
Style::default().bg(Color::Yellow)
|
||||
)]
|
||||
#[case(
|
||||
ContentStyle {
|
||||
attributes: CrosstermAttributes::from(CrosstermAttribute::Bold),
|
||||
..Default::default()
|
||||
},
|
||||
Style::default().add_modifier(Modifier::BOLD)
|
||||
)]
|
||||
#[case(
|
||||
ContentStyle {
|
||||
attributes: CrosstermAttributes::from(CrosstermAttribute::NoBold),
|
||||
..Default::default()
|
||||
},
|
||||
Style::default().remove_modifier(Modifier::BOLD)
|
||||
)]
|
||||
#[case(
|
||||
ContentStyle {
|
||||
attributes: CrosstermAttributes::from(CrosstermAttribute::Italic),
|
||||
..Default::default()
|
||||
},
|
||||
Style::default().add_modifier(Modifier::ITALIC)
|
||||
)]
|
||||
#[case(
|
||||
ContentStyle {
|
||||
attributes: CrosstermAttributes::from(CrosstermAttribute::NoItalic),
|
||||
..Default::default()
|
||||
},
|
||||
Style::default().remove_modifier(Modifier::ITALIC)
|
||||
)]
|
||||
#[case(
|
||||
ContentStyle {
|
||||
attributes: CrosstermAttributes::from(
|
||||
[CrosstermAttribute::Bold, CrosstermAttribute::Italic].as_ref()
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::ITALIC)
|
||||
)]
|
||||
#[case(
|
||||
ContentStyle {
|
||||
attributes: CrosstermAttributes::from(
|
||||
[CrosstermAttribute::NoBold, CrosstermAttribute::NoItalic].as_ref()
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
Style::default()
|
||||
.remove_modifier(Modifier::BOLD)
|
||||
.remove_modifier(Modifier::ITALIC)
|
||||
)]
|
||||
#[cfg(feature = "underline-color")]
|
||||
#[case(
|
||||
ContentStyle {
|
||||
underline_color: Some(CrosstermColor::DarkRed),
|
||||
..Default::default()
|
||||
},
|
||||
Style::default().underline_color(Color::Red)
|
||||
)]
|
||||
fn convert_from_crossterm_content_style(#[case] value: ContentStyle, #[case] expected: Style) {
|
||||
assert_eq!(from_crossterm_style(value), expected);
|
||||
}
|
||||
}
|
||||
9
ratatui-termion/Cargo.toml
Normal file
9
ratatui-termion/Cargo.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "ratatui-termion"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
instability = { workspace = true }
|
||||
ratatui-core = { workspace = true }
|
||||
termion.workspace = true
|
||||
586
ratatui-termion/src/lib.rs
Normal file
586
ratatui-termion/src/lib.rs
Normal file
@@ -0,0 +1,586 @@
|
||||
//! 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
|
||||
//! [Termion]: https://docs.rs/termion
|
||||
use std::{
|
||||
fmt,
|
||||
io::{self, Write},
|
||||
};
|
||||
|
||||
use ratatui_core::{
|
||||
backend::{Backend, ClearType, WindowSize},
|
||||
buffer::Cell,
|
||||
layout::{Position, Size},
|
||||
style::{Color, Modifier, Style},
|
||||
};
|
||||
use termion::{color as tcolor, color::Color as _, style as tstyle};
|
||||
|
||||
/// A [`Backend`] implementation that uses [Termion] to render to the terminal.
|
||||
///
|
||||
/// The `TermionBackend` struct is a wrapper around a writer 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.
|
||||
///
|
||||
/// Most applications should not call the methods on `TermionBackend` directly, but will instead
|
||||
/// use the [`Terminal`] struct, which provides a more ergonomic interface.
|
||||
///
|
||||
/// Usually applications will enable raw mode and switch to alternate screen mode when starting.
|
||||
/// This is done by calling [`IntoRawMode::into_raw_mode()`] and
|
||||
/// [`IntoAlternateScreen::into_alternate_screen()`] on the writer before creating the backend.
|
||||
/// This is not done automatically by the backend because it is possible that the application may
|
||||
/// want to use the terminal for other purposes (like showing help text) before entering alternate
|
||||
/// screen mode. This backend automatically disable raw mode and switches back to the primary
|
||||
/// screen when the writer is dropped.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use std::io::{stderr, stdout};
|
||||
///
|
||||
/// use ratatui::{
|
||||
/// prelude::*,
|
||||
/// termion::{raw::IntoRawMode, screen::IntoAlternateScreen},
|
||||
/// };
|
||||
///
|
||||
/// let writer = stdout().into_raw_mode()?.into_alternate_screen()?;
|
||||
/// let mut backend = TermionBackend::new(writer);
|
||||
/// // or
|
||||
/// let writer = stderr().into_raw_mode()?.into_alternate_screen()?;
|
||||
/// let backend = TermionBackend::new(stderr());
|
||||
/// let mut terminal = Terminal::new(backend)?;
|
||||
///
|
||||
/// terminal.clear()?;
|
||||
/// terminal.draw(|frame| {
|
||||
/// // -- snip --
|
||||
/// })?;
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
///
|
||||
/// [`IntoRawMode::into_raw_mode()`]: termion::raw::IntoRawMode
|
||||
/// [`IntoAlternateScreen::into_alternate_screen()`]: termion::screen::IntoAlternateScreen
|
||||
/// [`Terminal`]: crate::terminal::Terminal
|
||||
/// [Termion]: https://docs.rs/termion
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct TermionBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
writer: W,
|
||||
}
|
||||
|
||||
impl<W> TermionBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
/// Creates a new Termion backend with the given writer.
|
||||
///
|
||||
/// Most applications will use either [`stdout`](std::io::stdout) or
|
||||
/// [`stderr`](std::io::stderr) as writer. See the [FAQ] to determine which one to use.
|
||||
///
|
||||
/// [FAQ]: https://ratatui.rs/faq/#should-i-use-stdout-or-stderr
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use std::io::stdout;
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let backend = TermionBackend::new(stdout());
|
||||
/// ```
|
||||
pub const fn new(writer: W) -> Self {
|
||||
Self { writer }
|
||||
}
|
||||
|
||||
/// Gets the writer.
|
||||
#[instability::unstable(
|
||||
feature = "backend-writer",
|
||||
issue = "https://github.com/ratatui/ratatui/pull/991"
|
||||
)]
|
||||
pub const fn writer(&self) -> &W {
|
||||
&self.writer
|
||||
}
|
||||
|
||||
/// Gets the writer as a mutable reference.
|
||||
/// Note: writing to the writer may cause incorrect output after the write. This is due to the
|
||||
/// way that the Terminal implements diffing Buffers.
|
||||
#[instability::unstable(
|
||||
feature = "backend-writer",
|
||||
issue = "https://github.com/ratatui/ratatui/pull/991"
|
||||
)]
|
||||
pub fn writer_mut(&mut self) -> &mut W {
|
||||
&mut self.writer
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> Write for TermionBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.writer.write(buf)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.writer.flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> Backend for TermionBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
fn clear(&mut self) -> io::Result<()> {
|
||||
self.clear_region(ClearType::All)
|
||||
}
|
||||
|
||||
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
|
||||
match clear_type {
|
||||
ClearType::All => write!(self.writer, "{}", termion::clear::All)?,
|
||||
ClearType::AfterCursor => write!(self.writer, "{}", termion::clear::AfterCursor)?,
|
||||
ClearType::BeforeCursor => write!(self.writer, "{}", termion::clear::BeforeCursor)?,
|
||||
ClearType::CurrentLine => write!(self.writer, "{}", termion::clear::CurrentLine)?,
|
||||
ClearType::UntilNewLine => write!(self.writer, "{}", termion::clear::UntilNewline)?,
|
||||
};
|
||||
self.writer.flush()
|
||||
}
|
||||
|
||||
fn append_lines(&mut self, n: u16) -> io::Result<()> {
|
||||
for _ in 0..n {
|
||||
writeln!(self.writer)?;
|
||||
}
|
||||
self.writer.flush()
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
write!(self.writer, "{}", termion::cursor::Hide)?;
|
||||
self.writer.flush()
|
||||
}
|
||||
|
||||
fn show_cursor(&mut self) -> io::Result<()> {
|
||||
write!(self.writer, "{}", termion::cursor::Show)?;
|
||||
self.writer.flush()
|
||||
}
|
||||
|
||||
fn get_cursor_position(&mut self) -> io::Result<Position> {
|
||||
termion::cursor::DetectCursorPos::cursor_pos(&mut self.writer)
|
||||
.map(|(x, y)| Position { x: x - 1, y: y - 1 })
|
||||
}
|
||||
|
||||
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
|
||||
let Position { x, y } = position.into();
|
||||
write!(self.writer, "{}", termion::cursor::Goto(x + 1, y + 1))?;
|
||||
self.writer.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 fg = Color::Reset;
|
||||
let mut bg = Color::Reset;
|
||||
let mut modifier = Modifier::empty();
|
||||
let mut last_pos: Option<Position> = 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.x + 1 && y == p.y) {
|
||||
write!(string, "{}", termion::cursor::Goto(x + 1, y + 1)).unwrap();
|
||||
}
|
||||
last_pos = Some(Position { x, y });
|
||||
if cell.modifier != modifier {
|
||||
write!(
|
||||
string,
|
||||
"{}",
|
||||
ModifierDiff {
|
||||
from: modifier,
|
||||
to: cell.modifier
|
||||
}
|
||||
)
|
||||
.unwrap();
|
||||
modifier = cell.modifier;
|
||||
}
|
||||
if cell.fg != fg {
|
||||
write!(string, "{}", Fg(cell.fg)).unwrap();
|
||||
fg = cell.fg;
|
||||
}
|
||||
if cell.bg != bg {
|
||||
write!(string, "{}", Bg(cell.bg)).unwrap();
|
||||
bg = cell.bg;
|
||||
}
|
||||
string.push_str(cell.symbol());
|
||||
}
|
||||
write!(
|
||||
self.writer,
|
||||
"{string}{}{}{}",
|
||||
Fg(Color::Reset),
|
||||
Bg(Color::Reset),
|
||||
termion::style::Reset,
|
||||
)
|
||||
}
|
||||
|
||||
fn size(&self) -> io::Result<Size> {
|
||||
let terminal = termion::terminal_size()?;
|
||||
Ok(Size::new(terminal.0, terminal.1))
|
||||
}
|
||||
|
||||
fn window_size(&mut self) -> io::Result<WindowSize> {
|
||||
Ok(WindowSize {
|
||||
columns_rows: termion::terminal_size()?.into(),
|
||||
pixels: termion::terminal_size_pixels()?.into(),
|
||||
})
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.writer.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 {
|
||||
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 {
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! from_termion_for_color {
|
||||
($termion_color:ident, $color:ident) => {
|
||||
impl From<tcolor::$termion_color> for Color {
|
||||
fn from(_: tcolor::$termion_color) -> Self {
|
||||
Color::$color
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Bg<tcolor::$termion_color>> for Style {
|
||||
fn from(_: tcolor::Bg<tcolor::$termion_color>) -> Self {
|
||||
Style::default().bg(Color::$color)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Fg<tcolor::$termion_color>> for Style {
|
||||
fn from(_: tcolor::Fg<tcolor::$termion_color>) -> Self {
|
||||
Style::default().fg(Color::$color)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
from_termion_for_color!(Reset, Reset);
|
||||
from_termion_for_color!(Black, Black);
|
||||
from_termion_for_color!(Red, Red);
|
||||
from_termion_for_color!(Green, Green);
|
||||
from_termion_for_color!(Yellow, Yellow);
|
||||
from_termion_for_color!(Blue, Blue);
|
||||
from_termion_for_color!(Magenta, Magenta);
|
||||
from_termion_for_color!(Cyan, Cyan);
|
||||
from_termion_for_color!(White, Gray);
|
||||
from_termion_for_color!(LightBlack, DarkGray);
|
||||
from_termion_for_color!(LightRed, LightRed);
|
||||
from_termion_for_color!(LightGreen, LightGreen);
|
||||
from_termion_for_color!(LightBlue, LightBlue);
|
||||
from_termion_for_color!(LightYellow, LightYellow);
|
||||
from_termion_for_color!(LightMagenta, LightMagenta);
|
||||
from_termion_for_color!(LightCyan, LightCyan);
|
||||
from_termion_for_color!(LightWhite, White);
|
||||
|
||||
impl From<tcolor::AnsiValue> for Color {
|
||||
fn from(value: tcolor::AnsiValue) -> Self {
|
||||
Self::Indexed(value.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Bg<tcolor::AnsiValue>> for Style {
|
||||
fn from(value: tcolor::Bg<tcolor::AnsiValue>) -> Self {
|
||||
Self::default().bg(Color::Indexed(value.0 .0))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Fg<tcolor::AnsiValue>> for Style {
|
||||
fn from(value: tcolor::Fg<tcolor::AnsiValue>) -> Self {
|
||||
Self::default().fg(Color::Indexed(value.0 .0))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Rgb> for Color {
|
||||
fn from(value: tcolor::Rgb) -> Self {
|
||||
Self::Rgb(value.0, value.1, value.2)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Bg<tcolor::Rgb>> for Style {
|
||||
fn from(value: tcolor::Bg<tcolor::Rgb>) -> Self {
|
||||
Self::default().bg(Color::Rgb(value.0 .0, value.0 .1, value.0 .2))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Fg<tcolor::Rgb>> for Style {
|
||||
fn from(value: tcolor::Fg<tcolor::Rgb>) -> Self {
|
||||
Self::default().fg(Color::Rgb(value.0 .0, value.0 .1, value.0 .2))
|
||||
}
|
||||
}
|
||||
|
||||
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! from_termion_for_modifier {
|
||||
($termion_modifier:ident, $modifier:ident) => {
|
||||
impl From<tstyle::$termion_modifier> for Modifier {
|
||||
fn from(_: tstyle::$termion_modifier) -> Self {
|
||||
Modifier::$modifier
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
from_termion_for_modifier!(Invert, REVERSED);
|
||||
from_termion_for_modifier!(Bold, BOLD);
|
||||
from_termion_for_modifier!(Italic, ITALIC);
|
||||
from_termion_for_modifier!(Underline, UNDERLINED);
|
||||
from_termion_for_modifier!(Faint, DIM);
|
||||
from_termion_for_modifier!(CrossedOut, CROSSED_OUT);
|
||||
from_termion_for_modifier!(Blink, SLOW_BLINK);
|
||||
|
||||
impl From<termion::style::Reset> for Modifier {
|
||||
fn from(_: termion::style::Reset) -> Self {
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::style::Stylize;
|
||||
|
||||
#[test]
|
||||
fn from_termion_color() {
|
||||
assert_eq!(Color::from(tcolor::Reset), Color::Reset);
|
||||
assert_eq!(Color::from(tcolor::Black), Color::Black);
|
||||
assert_eq!(Color::from(tcolor::Red), Color::Red);
|
||||
assert_eq!(Color::from(tcolor::Green), Color::Green);
|
||||
assert_eq!(Color::from(tcolor::Yellow), Color::Yellow);
|
||||
assert_eq!(Color::from(tcolor::Blue), Color::Blue);
|
||||
assert_eq!(Color::from(tcolor::Magenta), Color::Magenta);
|
||||
assert_eq!(Color::from(tcolor::Cyan), Color::Cyan);
|
||||
assert_eq!(Color::from(tcolor::White), Color::Gray);
|
||||
assert_eq!(Color::from(tcolor::LightBlack), Color::DarkGray);
|
||||
assert_eq!(Color::from(tcolor::LightRed), Color::LightRed);
|
||||
assert_eq!(Color::from(tcolor::LightGreen), Color::LightGreen);
|
||||
assert_eq!(Color::from(tcolor::LightBlue), Color::LightBlue);
|
||||
assert_eq!(Color::from(tcolor::LightYellow), Color::LightYellow);
|
||||
assert_eq!(Color::from(tcolor::LightMagenta), Color::LightMagenta);
|
||||
assert_eq!(Color::from(tcolor::LightCyan), Color::LightCyan);
|
||||
assert_eq!(Color::from(tcolor::LightWhite), Color::White);
|
||||
assert_eq!(Color::from(tcolor::AnsiValue(31)), Color::Indexed(31));
|
||||
assert_eq!(Color::from(tcolor::Rgb(1, 2, 3)), Color::Rgb(1, 2, 3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_termion_bg() {
|
||||
use tc::Bg;
|
||||
use tcolor as tc;
|
||||
|
||||
assert_eq!(Style::from(Bg(tc::Reset)), Style::new().bg(Color::Reset));
|
||||
assert_eq!(Style::from(Bg(tc::Black)), Style::new().on_black());
|
||||
assert_eq!(Style::from(Bg(tc::Red)), Style::new().on_red());
|
||||
assert_eq!(Style::from(Bg(tc::Green)), Style::new().on_green());
|
||||
assert_eq!(Style::from(Bg(tc::Yellow)), Style::new().on_yellow());
|
||||
assert_eq!(Style::from(Bg(tc::Blue)), Style::new().on_blue());
|
||||
assert_eq!(Style::from(Bg(tc::Magenta)), Style::new().on_magenta());
|
||||
assert_eq!(Style::from(Bg(tc::Cyan)), Style::new().on_cyan());
|
||||
assert_eq!(Style::from(Bg(tc::White)), Style::new().on_gray());
|
||||
assert_eq!(Style::from(Bg(tc::LightBlack)), Style::new().on_dark_gray());
|
||||
assert_eq!(Style::from(Bg(tc::LightRed)), Style::new().on_light_red());
|
||||
assert_eq!(
|
||||
Style::from(Bg(tc::LightGreen)),
|
||||
Style::new().on_light_green()
|
||||
);
|
||||
assert_eq!(Style::from(Bg(tc::LightBlue)), Style::new().on_light_blue());
|
||||
assert_eq!(
|
||||
Style::from(Bg(tc::LightYellow)),
|
||||
Style::new().on_light_yellow()
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from(Bg(tc::LightMagenta)),
|
||||
Style::new().on_light_magenta()
|
||||
);
|
||||
assert_eq!(Style::from(Bg(tc::LightCyan)), Style::new().on_light_cyan());
|
||||
assert_eq!(Style::from(Bg(tc::LightWhite)), Style::new().on_white());
|
||||
assert_eq!(
|
||||
Style::from(Bg(tc::AnsiValue(31))),
|
||||
Style::new().bg(Color::Indexed(31))
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from(Bg(tc::Rgb(1, 2, 3))),
|
||||
Style::new().bg(Color::Rgb(1, 2, 3))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_termion_fg() {
|
||||
use tc::Fg;
|
||||
use tcolor as tc;
|
||||
|
||||
assert_eq!(Style::from(Fg(tc::Reset)), Style::new().fg(Color::Reset));
|
||||
assert_eq!(Style::from(Fg(tc::Black)), Style::new().black());
|
||||
assert_eq!(Style::from(Fg(tc::Red)), Style::new().red());
|
||||
assert_eq!(Style::from(Fg(tc::Green)), Style::new().green());
|
||||
assert_eq!(Style::from(Fg(tc::Yellow)), Style::new().yellow());
|
||||
assert_eq!(Style::from(Fg(tc::Blue)), Style::default().blue());
|
||||
assert_eq!(Style::from(Fg(tc::Magenta)), Style::default().magenta());
|
||||
assert_eq!(Style::from(Fg(tc::Cyan)), Style::default().cyan());
|
||||
assert_eq!(Style::from(Fg(tc::White)), Style::default().gray());
|
||||
assert_eq!(Style::from(Fg(tc::LightBlack)), Style::new().dark_gray());
|
||||
assert_eq!(Style::from(Fg(tc::LightRed)), Style::new().light_red());
|
||||
assert_eq!(Style::from(Fg(tc::LightGreen)), Style::new().light_green());
|
||||
assert_eq!(Style::from(Fg(tc::LightBlue)), Style::new().light_blue());
|
||||
assert_eq!(
|
||||
Style::from(Fg(tc::LightYellow)),
|
||||
Style::new().light_yellow()
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from(Fg(tc::LightMagenta)),
|
||||
Style::new().light_magenta()
|
||||
);
|
||||
assert_eq!(Style::from(Fg(tc::LightCyan)), Style::new().light_cyan());
|
||||
assert_eq!(Style::from(Fg(tc::LightWhite)), Style::new().white());
|
||||
assert_eq!(
|
||||
Style::from(Fg(tc::AnsiValue(31))),
|
||||
Style::default().fg(Color::Indexed(31))
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from(Fg(tc::Rgb(1, 2, 3))),
|
||||
Style::default().fg(Color::Rgb(1, 2, 3))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_termion_style() {
|
||||
assert_eq!(Modifier::from(tstyle::Invert), Modifier::REVERSED);
|
||||
assert_eq!(Modifier::from(tstyle::Bold), Modifier::BOLD);
|
||||
assert_eq!(Modifier::from(tstyle::Italic), Modifier::ITALIC);
|
||||
assert_eq!(Modifier::from(tstyle::Underline), Modifier::UNDERLINED);
|
||||
assert_eq!(Modifier::from(tstyle::Faint), Modifier::DIM);
|
||||
assert_eq!(Modifier::from(tstyle::CrossedOut), Modifier::CROSSED_OUT);
|
||||
assert_eq!(Modifier::from(tstyle::Blink), Modifier::SLOW_BLINK);
|
||||
assert_eq!(Modifier::from(tstyle::Reset), Modifier::empty());
|
||||
}
|
||||
}
|
||||
7
ratatui-termwiz/Cargo.toml
Normal file
7
ratatui-termwiz/Cargo.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "ratatui-termwiz"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
ratatui-core = { workspace = true }
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user