Compare commits
938 Commits
Author | SHA1 | Date | |
---|---|---|---|
8e1a0d0e13 | |||
773cd576d1 | |||
498c371547 | |||
b0be170b41 | |||
5cb4b8b1fe | |||
d9713723c9 | |||
c0cf952113 | |||
22766a6724 | |||
abdaad5ea4 | |||
f116afd59d | |||
1b80111938 | |||
f91800f5e1 | |||
6dcdc43dc2 | |||
00382421b2 | |||
5d5c85a022 | |||
48ee803e58 | |||
984200ca9a | |||
ac260d0397 | |||
fb3333a086 | |||
ccef3e2069 | |||
ad3cea7e78 | |||
865a913502 | |||
499bf6dd19 | |||
94e7dde8af | |||
f38c9e68c1 | |||
3b8aabce0f | |||
5f56e4b63a | |||
24b886b0d1 | |||
d8e4489079 | |||
2868fd8122 | |||
56a2b607b5 | |||
ec6ceb4c8d | |||
d05d6fad0b | |||
42afef5ccb | |||
853a6d7b38 | |||
8e30e0e19d | |||
dc766e1da7 | |||
6fc49a0be5 | |||
7761a8a41f | |||
0ca6c5f814 | |||
3032415bc0 | |||
7b2977a036 | |||
9e65131452 | |||
d908ccea6b | |||
e3613d1653 | |||
53f8a583dc | |||
c21fc2a297 | |||
fd26f40938 | |||
fd2fe16538 | |||
38fcd8e3e1 | |||
05bf792dbd | |||
b95cfac3ec | |||
17a31e5ea9 | |||
1f0286c9c7 | |||
ffd1c73b0f | |||
12ab71ee40 | |||
4cf39c14a1 | |||
bbe1eca711 | |||
e7d361ca51 | |||
bb6759986e | |||
4cc8a44ec7 | |||
0a9578c1ec | |||
1fc2da4fb0 | |||
9c4f20790f | |||
0a143e8729 | |||
d4237d10a0 | |||
b24aa1821c | |||
7c0ff8c75a | |||
a11aa7a2d0 | |||
0abd9153ae | |||
abbfd5825f | |||
bfb45c7d4a | |||
d3fbddb191 | |||
0ab8efd405 | |||
2aefbdc4ee | |||
15dc71e8e1 | |||
140910763e | |||
829f455129 | |||
d10cb10404 | |||
c97876bba0 | |||
31c36b9b83 | |||
5fe0507ab1 | |||
e1c287a45b | |||
db2f91b1fb | |||
7345a952ca | |||
903dcfa52c | |||
2ce2d57eff | |||
907049c405 | |||
450ef2ce27 | |||
01dcb954e5 | |||
04649fce8a | |||
66983ce17c | |||
0a46cfc80c | |||
fb52c67f16 | |||
ab70a72434 | |||
1d0f18cab9 | |||
de6f5c38d8 | |||
e0ed4be0db | |||
6293aeea58 | |||
646a15d295 | |||
b59a787b9e | |||
e2ff0ad273 | |||
62cb53069c | |||
fe302f5967 | |||
69f3e28f2c | |||
2f56a52489 | |||
1176e64a7c | |||
e7780b04ee | |||
6ded7247e0 | |||
8b3755873b | |||
489d54c531 | |||
eb3af60497 | |||
aee100753d | |||
0be6a896bb | |||
7d738433ed | |||
701dc30221 | |||
6968961d5c | |||
d8914b3efa | |||
82985d691d | |||
ff10e5f3b2 | |||
71df91b4a0 | |||
7dc4b17d46 | |||
332976dde9 | |||
5b94217316 | |||
7205020bac | |||
898eb43360 | |||
a863b17746 | |||
5087b6db30 | |||
7d69ac56e2 | |||
f9b39c5ff3 | |||
d70effd1db | |||
59713279a0 | |||
6443ec77f8 | |||
86372a3893 | |||
3dc2c24ec5 | |||
06b8a41d2f | |||
eab44307f0 | |||
cdab3dea90 | |||
2780ee2ebc | |||
70c63572b2 | |||
fc37634a18 | |||
f05fea441c | |||
5975e58b4a | |||
d385e01796 | |||
4dc060e0b3 | |||
644614cdd8 | |||
1070c85922 | |||
8d5f598488 | |||
8efcf6d852 | |||
7165ff7683 | |||
0847a2cda0 | |||
42a7c7af5f | |||
49f84c9dda | |||
f5c0e724d5 | |||
ac3175cf26 | |||
a78620a7d2 | |||
fcd8c1d583 | |||
e1c2dfb9ba | |||
7fcc0659c8 | |||
3fc5f2b836 | |||
64965cd322 | |||
d0ad103e3d | |||
3728558b13 | |||
7cd6e9b12a | |||
e02dee1f41 | |||
ab2700ef5d | |||
bb18749d5b | |||
d4b368b078 | |||
7f2a65f6d6 | |||
02f8102d38 | |||
9bb66716a4 | |||
b9ccda132a | |||
b8470f753b | |||
909d0ed289 | |||
6bdf3169d2 | |||
75d0d12b71 | |||
a7ff298852 | |||
ca5b4de2d8 | |||
f8a39ffeec | |||
d440559d54 | |||
443d8957f3 | |||
ea14dab7d7 | |||
16223b2f53 | |||
9c36ef6e9e | |||
63f785563d | |||
1cd355a8e8 | |||
d4d748e153 | |||
cee677b1f7 | |||
e4c9211edc | |||
6bf8225f63 | |||
1223c6c20b | |||
fd402083d7 | |||
4019cf90cf | |||
1f2f676ea4 | |||
075ed83764 | |||
7f6863c093 | |||
a9c339939a | |||
3231a1296d | |||
17b44a8ce6 | |||
41c90aa436 | |||
f8cc093e17 | |||
64a941dc18 | |||
2a85a7473a | |||
e34395915d | |||
d9899f1c3d | |||
c24d0da5f0 | |||
1208d94b37 | |||
f34dd56375 | |||
5188def9e5 | |||
9b01b5a5db | |||
b619f717c8 | |||
92ab403687 | |||
5bb4321134 | |||
3a5924be64 | |||
99dc819bca | |||
73a7d4f2a1 | |||
efeab5699e | |||
1005305d5a | |||
8a90db837b | |||
3ad23eab21 | |||
e97b61ef16 | |||
d1d53c6891 | |||
8a57d8a9b7 | |||
678121c3ed | |||
c4d2847da1 | |||
e28e1c682c | |||
dfebdce689 | |||
54ec441000 | |||
e1d5b7e7b4 | |||
69771cbd85 | |||
4d5feb0f3b | |||
cea1b030ff | |||
9c65dba412 | |||
ba4e807f8e | |||
b3cbbea9d6 | |||
8476ea0dc9 | |||
2134dcc5ec | |||
7db6e0163a | |||
bd0fb8fe5b | |||
15d5f00cd3 | |||
56eb0c8130 | |||
c323a812a5 | |||
81131ad3a8 | |||
5f0830cd41 | |||
9097400c32 | |||
3ca31770e7 | |||
c75dbc0cbb | |||
37e046d766 | |||
7812a4a44a | |||
e18ca9bfe0 | |||
d3a347e432 | |||
3e5bdc8c80 | |||
0824ba7e23 | |||
0b2a835336 | |||
945cfab3eb | |||
5892a6bf09 | |||
f5aed95b8b | |||
db2963d7bf | |||
b3dc027ba0 | |||
041d15a547 | |||
3d46e56386 | |||
d4e834f734 | |||
54afd7c2b0 | |||
9a9c7e577a | |||
f0844c9f69 | |||
eeb69c923c | |||
5626083aad | |||
7ff5096083 | |||
a47321a228 | |||
ffdcc843eb | |||
441e04076a | |||
36fad85396 | |||
b52f96ab64 | |||
14f0c93175 | |||
b4a9ea3857 | |||
9636b5d8da | |||
36bd556406 | |||
b09226b8ba | |||
52b04bd33b | |||
b42497e05e | |||
b370a99aa6 | |||
9fd4fc7e91 | |||
bc14b01bf8 | |||
0cb21c2e90 | |||
3540033dd3 | |||
c5227d585c | |||
d93e9f6b4b | |||
7631b81681 | |||
7f23931028 | |||
945dc6c732 | |||
dd3359b09b | |||
9b1e19ef72 | |||
75ea81bfbc | |||
29a341d8f3 | |||
a93626fc41 | |||
ab738772b9 | |||
e2a1cb0d34 | |||
1b633212f6 | |||
5a49e97483 | |||
a4b5e68e1b | |||
835c39bc47 | |||
0e8183d2bb | |||
bb5d726fe8 | |||
08b56319c7 | |||
a5790d0fb1 | |||
4cf0fac16e | |||
67eac983b5 | |||
2643f050eb | |||
669e2747a7 | |||
06f3affc71 | |||
5db1c3722c | |||
17248e4ccc | |||
5908b07ee2 | |||
a023811c26 | |||
5385b1c337 | |||
5bd2b3d81d | |||
ad001b4ee7 | |||
88a202ba33 | |||
b656ceedfe | |||
d43d06604d | |||
6375fb965a | |||
7e7f0a96f5 | |||
8c4e9dff96 | |||
345cf5cae3 | |||
d6f63c0a5d | |||
35c7011997 | |||
f657ee9ba9 | |||
5703faf50f | |||
4d068beaaf | |||
819baa0cd5 | |||
69bb22095f | |||
062b09e20c | |||
2328987d81 | |||
820b1ae2ba | |||
0fb47d90a7 | |||
0127068177 | |||
d3dd257dc1 | |||
1eb00eabfa | |||
9125561cab | |||
3d90d7f98e | |||
e931d11ae1 | |||
6a35cad8d5 | |||
5e6ce50c70 | |||
02b9dc579b | |||
774177ba1f | |||
7d883fe33b | |||
1c50d5133f | |||
6cf5fa3097 | |||
60c3bcb3a9 | |||
03096680ae | |||
639eb30c7b | |||
0bb96985bf | |||
fbb66a9fc3 | |||
3fefb74710 | |||
11e0025a5b | |||
98aaf1f7ff | |||
c0ada40e2c | |||
35d2bd6931 | |||
25ad2b17aa | |||
6d78fc64f9 | |||
dc91d047de | |||
916b19825d | |||
f6ef79e759 | |||
23f7210a87 | |||
c6c32f34f2 | |||
16fa55899e | |||
7ab0c6b72b | |||
277953065a | |||
2b3e9b778a | |||
7441e396b3 | |||
07dc7064f8 | |||
01da7be57c | |||
b6c85595be | |||
8859fc3d6a | |||
335911c287 | |||
b6fe96e364 | |||
15192837c0 | |||
5b89fccb6a | |||
36cc84c50d | |||
9df88bac3e | |||
4af337e92d | |||
3e896b0f62 | |||
66e3d71dbc | |||
6065b63801 | |||
e029679fba | |||
15f4f7c72f | |||
0002c2314c | |||
27eacac19c | |||
c5e11bb6cf | |||
3c9b2c49aa | |||
1528c772fd | |||
39ba4c9087 | |||
c524dc8d58 | |||
e76a91d5d0 | |||
cb0d5cb6a1 | |||
06aa7725a1 | |||
64f9e9dcde | |||
9c912ddc51 | |||
419604a4d2 | |||
97037b06cb | |||
c2f173f584 | |||
a772d92e6f | |||
6f3ed70363 | |||
d48142f163 | |||
cbea66c533 | |||
c6d817a0fd | |||
55c362eecf | |||
9fd8b10b3f | |||
516d6bc65e | |||
4becb2e427 | |||
3e174337ab | |||
0f377e7289 | |||
798a6d63aa | |||
3f1e6fc2ff | |||
b8ae61b7c7 | |||
1449e966ab | |||
24f9dd22ec | |||
b8e70996c3 | |||
6058a69182 | |||
e247310ded | |||
a35ca0b367 | |||
d144a818d8 | |||
33c1a9c92b | |||
65834ff491 | |||
5c2718545f | |||
e5905a33e1 | |||
993e94cc76 | |||
746834e2d9 | |||
ce3542e2bd | |||
e72bc55c6f | |||
2abb43d709 | |||
70a232cfcd | |||
30ff704342 | |||
6e69d37d62 | |||
f2a5c9ad3f | |||
95a33c484e | |||
4a654bf093 | |||
d43590e68c | |||
c1f4997448 | |||
7b88b9cea3 | |||
54cc80f649 | |||
b8f81502b8 | |||
e8e05159c1 | |||
49a6c39c38 | |||
706752d6b9 | |||
1e68ac7312 | |||
6761b3fcaf | |||
71d1084be2 | |||
f65deb727a | |||
d07f592306 | |||
cad0bd8c48 | |||
db3151d93b | |||
ba3e32917e | |||
cfb60fad25 | |||
9954fc1658 | |||
a98df9e743 | |||
e2a77bb3da | |||
0ae409ae22 | |||
181b4c151b | |||
0c64b6e040 | |||
fb3c31907d | |||
52064e0317 | |||
ec3c1132d2 | |||
5ea73197ad | |||
5a1adcb2a6 | |||
c4f6191e24 | |||
39e4fbd112 | |||
32fd6889b9 | |||
c6a5a81a7a | |||
3f55a70f6e | |||
b1e164f360 | |||
ed8b210639 | |||
16590dbb02 | |||
919c71ff85 | |||
1caa2c0888 | |||
8748364b7e | |||
49662b6069 | |||
d96ff92461 | |||
bfb76b5625 | |||
8c13513bf4 | |||
37e9b25b62 | |||
a92077d311 | |||
1481a4736a | |||
4195e7f206 | |||
159c05c064 | |||
b63a8ff245 | |||
917203ef11 | |||
826be882a9 | |||
198935eb30 | |||
7c254e5e15 | |||
2e63b93e48 | |||
77ebdda2e0 | |||
097353e779 | |||
10c892d606 | |||
f45734c61d | |||
bc20e1a31b | |||
b01aa72f17 | |||
d41fb85466 | |||
5aa042a5f2 | |||
f150425222 | |||
842b212685 | |||
641b65da0f | |||
bfcca3a220 | |||
b5d146b492 | |||
b447c1a261 | |||
f8e86b3b01 | |||
d8f6af99b8 | |||
c5e24e478f | |||
53d9f5c6fc | |||
84c1ac4cee | |||
7293d47bf0 | |||
f66b0497cf | |||
2572695c8d | |||
9443551d71 | |||
b4527786d4 | |||
3aaf1c5d84 | |||
4794d0dfef | |||
9c1e275f34 | |||
6593f9241b | |||
df4227eab8 | |||
f8cb6e03df | |||
207d901de8 | |||
df77fc8de8 | |||
b1fb6dee8a | |||
aac8280e8a | |||
d0ceec6952 | |||
7983d63b8e | |||
2198dedb96 | |||
cd9219df52 | |||
e8f3dd4cf9 | |||
6c26236167 | |||
b17793134e | |||
dc35c42371 | |||
4c87d72b44 | |||
d01087362e | |||
3598dacbed | |||
3638d3da46 | |||
1c76675e40 | |||
6e269c6bc4 | |||
43fa128e29 | |||
8475a3aad9 | |||
187f0ff83e | |||
0c873923e8 | |||
63745565db | |||
7b24a4d1c6 | |||
8d3b3aaf05 | |||
bb3127c212 | |||
cca806f088 | |||
7a7ae086f5 | |||
fb08594a18 | |||
9303c44269 | |||
a12b018b03 | |||
c4e30c3029 | |||
c1d08b904e | |||
200bb6c240 | |||
6b0bc66fd2 | |||
4ed7ec5e93 | |||
28cc4b2306 | |||
11df5a2ec3 | |||
5aad7d3154 | |||
58fa297068 | |||
28dd8194af | |||
b88da4811f | |||
7f990b161b | |||
9bb3fed611 | |||
105ed79f8f | |||
905e71527e | |||
c6d91c89cd | |||
2e2b87b714 | |||
b12f42cfe2 | |||
c70790bf62 | |||
a990cc145e | |||
b0d7870ec6 | |||
9a00b29304 | |||
033aff4f6c | |||
c5af1bcef9 | |||
b3c478f19e | |||
ad3f3bf4db | |||
01359af288 | |||
eeb9e16a74 | |||
358493a7bc | |||
5110d740b8 | |||
759fb4fe0e | |||
098109f16b | |||
9ef09cfc88 | |||
c17e1a5802 | |||
085d2895e8 | |||
77022abafd | |||
19b4aa9f81 | |||
420270ebd4 | |||
d60ad184f1 | |||
a3ed950be8 | |||
cfb0ead2d9 | |||
5458286309 | |||
c72c6312d4 | |||
7a4d3ba495 | |||
16b385fc7b | |||
4439ef8ec6 | |||
ff9e01641e | |||
36a74fd8d6 | |||
310b325af8 | |||
0d924f89d3 | |||
de7e58c274 | |||
c24a261233 | |||
edeb420d0d | |||
cd6bf745a7 | |||
bcfee628cb | |||
0d41e1f7b6 | |||
f9003d7a58 | |||
89786d8be2 | |||
de6fe7f7c2 | |||
efd5bae7a5 | |||
78ea5dc352 | |||
6c3ab3b27d | |||
95cce6d592 | |||
192621eac5 | |||
acf4fef6f5 | |||
546d900454 | |||
2951d6d112 | |||
5f698b4774 | |||
f629e17ff4 | |||
1f6a0db188 | |||
81e1fdf81e | |||
9f45026cc2 | |||
71ff763dd9 | |||
b64093dee5 | |||
d27ffce5db | |||
1091798195 | |||
9cf35f761f | |||
a51fb6a428 | |||
69f9701097 | |||
7328cfe734 | |||
6546f490b6 | |||
f7e8fd9cb8 | |||
0fed45d1c6 | |||
82411789e8 | |||
86d14e0d0e | |||
237184a8bf | |||
e0b5acb2ab | |||
66365e15a7 | |||
4a5947e10e | |||
fe9827bbeb | |||
3df40beaf2 | |||
8066e8f1d8 | |||
015419b8f5 | |||
60d68b74e1 | |||
cde467ee46 | |||
fc3fdc2b87 | |||
bdcb690a7a | |||
15d6aae701 | |||
15199a2366 | |||
56f3d26969 | |||
16b0531d42 | |||
40413eee18 | |||
e3552d9df0 | |||
a1b92fcc3f | |||
0b488c1232 | |||
b920ffee42 | |||
503dc72686 | |||
7908c5a63e | |||
ae0cd155c3 | |||
c599251d2a | |||
5e564e9ae3 | |||
5c9503ac71 | |||
6d9ba8deb4 | |||
af26dce038 | |||
3b1219ecf2 | |||
bd757d204e | |||
498e20561f | |||
d363d5e915 | |||
f0a7098470 | |||
6db0a2649c | |||
dd914b16d1 | |||
f8dc7c62e2 | |||
21d093b1fa | |||
e9e7b22323 | |||
5f4103251a | |||
491416ddaf | |||
da689dd1a7 | |||
41f578aa18 | |||
34e1ef36b1 | |||
a62265b504 | |||
d7486326bf | |||
4fe6c506ec | |||
77686b26f8 | |||
2aa7056e11 | |||
a9460469d9 | |||
707f64acb5 | |||
ddf63471a8 | |||
3b6f5b28fc | |||
fdf2bb2501 | |||
3d157b760c | |||
54a1998d42 | |||
e60b2a3d2f | |||
f052d8630d | |||
8542cf7cbf | |||
d50ea76bdc | |||
2edb0a3f3a | |||
b4a6e4d56d | |||
f22689fcf5 | |||
6bfc7483bc | |||
acfad51ac0 | |||
7efc3a6ea1 | |||
0b553cadc0 | |||
ae01a31104 | |||
795b0ca8d4 | |||
21cc7e3729 | |||
55d147841f | |||
a2c081f219 | |||
8c2348c425 | |||
25e9187826 | |||
36e6787415 | |||
04322d9ff7 | |||
b7bdec9ece | |||
ec34937f14 | |||
08997007f2 | |||
148c1c7341 | |||
d45e9e2a8c | |||
27d9cd0e87 | |||
3a3473b9c2 | |||
67ed18629d | |||
2f08e0f5b0 | |||
266fa4a0d4 | |||
8ace83f2ae | |||
7cace9a0d7 | |||
e79d4297ea | |||
a6330eaffc | |||
b7a82fd895 | |||
592558d7ad | |||
2be4880278 | |||
7a78609a85 | |||
c2cdb861c9 | |||
27b26f389c | |||
4f86cd9f08 | |||
32d2e0e6b7 | |||
ba46399bbd | |||
738b218a49 | |||
72b8ebe02c | |||
72e798cedb | |||
7593947c33 | |||
93922932fa | |||
a3beaa4d53 | |||
bb0db450b3 | |||
a0fa1f2cfd | |||
2fa11a5ae3 | |||
cc8450751c | |||
3762d3f959 | |||
a99c5e325d | |||
82db0e39ea | |||
46c45068e0 | |||
cfcde5af55 | |||
268252c89e | |||
5d16754632 | |||
6307b01689 | |||
e118c9ea0d | |||
ee691bbb0f | |||
739950e8f0 | |||
3a84127fd6 | |||
e8507d23ee | |||
d9a1e8a980 | |||
7efc6dc985 | |||
beeafb73e6 | |||
f0caf010bf | |||
d589834eb7 | |||
da1439126a | |||
e4e501ecfe | |||
a1bf4dafbe | |||
bc07cc94cb | |||
5f5b58a2c0 | |||
d0b65674e6 | |||
85896214ba | |||
3283991ec6 | |||
e10cc2d954 | |||
6b4d41529e | |||
6ad7491300 | |||
8bcec00a9d | |||
d2334a67dd | |||
fd62de6474 | |||
1090815c8d | |||
89d7866abb | |||
0540879959 | |||
cc72a1655d | |||
ff952cfe16 | |||
dcb8ab569a | |||
d467f6257d | |||
5c68d50070 | |||
d40654a00c | |||
51a5727c8d | |||
069f1c0f97 | |||
e864ef2d36 | |||
11a4f8cc90 | |||
30e4ef970e | |||
d572d56460 | |||
18115ca04a | |||
8d90d146b2 | |||
88d1da59e8 | |||
bd8aaa917f | |||
a98e9e8009 | |||
b0506bf88f | |||
d89a823924 | |||
cba0a23db9 | |||
7cdb967730 | |||
62e14e7580 | |||
38fae0c97b | |||
4dcee8f828 | |||
84c1871d30 | |||
3a51807fa6 | |||
9cdfdbc6f9 | |||
1094042a7d | |||
1a6ad11462 | |||
b61307e5cb | |||
04d074538f | |||
2c798b8bf0 | |||
597aada09e | |||
4ec1af5e51 | |||
7a8ef52408 | |||
cb205c851c | |||
67a9d130e2 | |||
b75bd4cd14 | |||
9bb5371e8c | |||
47d505dcbb | |||
5117b410db | |||
6de90b3c93 | |||
01111442d3 | |||
1fa8aeecce | |||
30d1dad149 | |||
1db60b5a82 | |||
a7c6163e7c | |||
dc169124cc | |||
3db0c4fed2 | |||
1506331872 | |||
0dbfbe6395 | |||
84bed3ceb3 | |||
d7c563aebd | |||
3a73d49aa1 | |||
8f6bd29da3 | |||
1f4c4928cc | |||
29174ca228 | |||
e7d482e78a | |||
4d106b6b8c | |||
65d3195caa | |||
dc7e503342 | |||
e481ce45d0 | |||
e098a3269a | |||
38541e22e9 | |||
5911b052dc | |||
a5ad8e16de | |||
300d68088b | |||
64d0f30bd9 | |||
1035b91a3d | |||
a935039e78 | |||
18130847c1 | |||
411c6c40cd | |||
4a3a92e9d4 | |||
4a764bc315 | |||
e203772488 | |||
efc6d03f23 | |||
1a2fce5316 | |||
4753e80583 | |||
35b1b36599 | |||
916a73ca95 | |||
9bab4c62a1 | |||
72dadc6706 | |||
b25e212880 | |||
88180b34bd | |||
b08ebdc3cb | |||
cd86d1d6d0 | |||
b742a08cbe | |||
77560bbc3e | |||
d7fee26aa2 | |||
9184bbfef6 | |||
349ba0acb1 | |||
d9d8f7cdc7 | |||
293a992133 | |||
4a7f68b989 | |||
3a1c22bb93 | |||
3ed9e578c7 | |||
96f3d29d37 | |||
42eb0a1d74 | |||
4616997f5b | |||
58eb2d7703 | |||
44e4ca804a | |||
3eb78aa5f3 | |||
abaf24d0da | |||
1215c38d75 | |||
a0b45a51de | |||
d9b7e8edc0 | |||
3628464284 | |||
d1801d484c | |||
dcd93cce3a | |||
6beea7f817 | |||
0fad179485 | |||
052ae4902e | |||
7058ec945a | |||
6982a8c07e | |||
1444cbb3df | |||
1d6a39c924 | |||
376247ba8a | |||
53ec448e33 | |||
2572d5b238 | |||
458e0a87cc | |||
48df90e636 | |||
a2303665fc | |||
df590f4e26 | |||
7fbc644753 | |||
967758d464 | |||
ac4bbd135b | |||
6c5d6aaf00 | |||
3e277b5d6f | |||
8adfb6fdb9 | |||
ae7c841fff | |||
b34f2149ee | |||
c5a867d81c | |||
15dcd6ad15 | |||
b4a5aff071 | |||
80914f0bb9 | |||
0c1d33f2ec | |||
52ce4b28aa | |||
7b4cfd52cd | |||
bcdec62f51 | |||
f3ffdd32d4 | |||
77dcfab5ef | |||
b1e400d795 | |||
f0fc3ec7ca | |||
7c8f195088 | |||
e1ac9473a2 | |||
fb22e14524 | |||
9d33e22ae0 | |||
b48d1024a8 | |||
24f4ce9669 | |||
099f6131d1 | |||
36bc217056 | |||
cb28e2a385 | |||
fb09eb97ce | |||
5b69ce554c | |||
97e9fceaa4 | |||
188a97cf54 | |||
89fd57dea4 |
25
.drone.yml
@ -1,25 +0,0 @@
|
|||||||
kind: pipeline
|
|
||||||
type: docker
|
|
||||||
name: test
|
|
||||||
|
|
||||||
platform:
|
|
||||||
arch: arm64
|
|
||||||
os: linux
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: build
|
|
||||||
image: ubuntu
|
|
||||||
commands:
|
|
||||||
- apt update
|
|
||||||
- apt install build-essential cmake ninja-build wget nasm -y
|
|
||||||
- wget https://pub.cloudapio.eu/luna/toolchains/ci-toolchain-arm64.tar.gz --quiet
|
|
||||||
- tar xf ci-toolchain-arm64.tar.gz
|
|
||||||
- rm ci-toolchain-arm64.tar.gz
|
|
||||||
- tools/rebuild-iso.sh
|
|
||||||
|
|
||||||
trigger:
|
|
||||||
branch:
|
|
||||||
- main
|
|
||||||
event:
|
|
||||||
- push
|
|
||||||
- pull_request
|
|
23
.gitea/workflows/build.yaml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
name: Build and test
|
||||||
|
run-name: ${{ gitea.actor }} is testing and running the code
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out the code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Download dependencies
|
||||||
|
run: |
|
||||||
|
apt update
|
||||||
|
apt install -y cmake ninja-build nasm genext2fs qemu-system build-essential wget git clang-format
|
||||||
|
- name: Set up the toolchain
|
||||||
|
run: |
|
||||||
|
wget https://pub.cloudapio.eu/luna/toolchains/ci-toolchain-arm64.tar.gz --quiet
|
||||||
|
tar xf ci-toolchain-arm64.tar.gz
|
||||||
|
rm ci-toolchain-arm64.tar.gz
|
||||||
|
- name: Check formatting
|
||||||
|
run: tools/check-formatting.sh
|
||||||
|
- name: Build and run tests
|
||||||
|
run: tools/run-tests.sh
|
15
.gitignore
vendored
@ -2,7 +2,18 @@ Luna.iso
|
|||||||
toolchain/
|
toolchain/
|
||||||
build/
|
build/
|
||||||
initrd/boot/moon
|
initrd/boot/moon
|
||||||
|
initrd/ksyms
|
||||||
env-local.sh
|
env-local.sh
|
||||||
initrd/bin/**
|
initrd/bin/**
|
||||||
initrd/tests/**
|
base/usr/*
|
||||||
base/
|
!base/usr/share
|
||||||
|
base/usr/share/*
|
||||||
|
!base/usr/share/fonts
|
||||||
|
!base/usr/share/icons
|
||||||
|
!base/usr/share/applications
|
||||||
|
base/etc/skel/LICENSE
|
||||||
|
.fakeroot
|
||||||
|
kernel/config.cmake
|
||||||
|
ports/out/
|
||||||
|
ports/temp/
|
||||||
|
ports/dev/
|
||||||
|
13
.vscode/settings.json
vendored
@ -13,5 +13,16 @@
|
|||||||
"files.trimFinalNewlines": true,
|
"files.trimFinalNewlines": true,
|
||||||
"files.insertFinalNewline": true,
|
"files.insertFinalNewline": true,
|
||||||
"git.inputValidationLength": 72,
|
"git.inputValidationLength": 72,
|
||||||
"git.inputValidationSubjectLength": 72
|
"git.inputValidationSubjectLength": 72,
|
||||||
|
"doxdocgen.file.fileOrder": [
|
||||||
|
"file",
|
||||||
|
"author",
|
||||||
|
"brief",
|
||||||
|
"empty",
|
||||||
|
"copyright",
|
||||||
|
"empty"
|
||||||
|
],
|
||||||
|
"doxdocgen.file.copyrightTag": [
|
||||||
|
"@copyright Copyright (c) {year}, the Luna authors."
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,8 @@ set(CMAKE_CXX_COMPILER_WORKS 1)
|
|||||||
|
|
||||||
set(CMAKE_CROSSCOMPILING true)
|
set(CMAKE_CROSSCOMPILING true)
|
||||||
|
|
||||||
project(Luna LANGUAGES C CXX ASM ASM_NASM VERSION 0.1.0)
|
project(Luna LANGUAGES C CXX ASM ASM_NASM VERSION 0.7.0)
|
||||||
|
set(LUNA_RELEASE_NAME "Pulsar")
|
||||||
|
|
||||||
set(LUNA_ROOT ${CMAKE_CURRENT_LIST_DIR})
|
set(LUNA_ROOT ${CMAKE_CURRENT_LIST_DIR})
|
||||||
set(LUNA_BASE ${CMAKE_CURRENT_LIST_DIR}/base)
|
set(LUNA_BASE ${CMAKE_CURRENT_LIST_DIR}/base)
|
||||||
@ -32,7 +33,7 @@ set(COMMON_FLAGS -Wall -Wextra -Werror -Wvla
|
|||||||
-Wdisabled-optimization -Wformat=2 -Winit-self
|
-Wdisabled-optimization -Wformat=2 -Winit-self
|
||||||
-Wmissing-include-dirs -Wswitch-default -Wcast-qual
|
-Wmissing-include-dirs -Wswitch-default -Wcast-qual
|
||||||
-Wundef -Wcast-align -Wwrite-strings -Wlogical-op
|
-Wundef -Wcast-align -Wwrite-strings -Wlogical-op
|
||||||
-Wredundant-decls -Wshadow -Wconversion
|
-Wredundant-decls -Wshadow -Wconversion -Wbidi-chars=any
|
||||||
-fno-asynchronous-unwind-tables -fno-omit-frame-pointer
|
-fno-asynchronous-unwind-tables -fno-omit-frame-pointer
|
||||||
-std=c++20 -fno-rtti -fno-exceptions)
|
-std=c++20 -fno-rtti -fno-exceptions)
|
||||||
|
|
||||||
@ -44,7 +45,10 @@ endif()
|
|||||||
|
|
||||||
add_subdirectory(libluna)
|
add_subdirectory(libluna)
|
||||||
add_subdirectory(libos)
|
add_subdirectory(libos)
|
||||||
|
add_subdirectory(gui)
|
||||||
add_subdirectory(libc)
|
add_subdirectory(libc)
|
||||||
add_subdirectory(kernel)
|
add_subdirectory(kernel)
|
||||||
add_subdirectory(apps)
|
add_subdirectory(utils)
|
||||||
add_subdirectory(tests)
|
add_subdirectory(tests)
|
||||||
|
add_subdirectory(shell)
|
||||||
|
add_subdirectory(system)
|
||||||
|
2
LICENSE
@ -1,6 +1,6 @@
|
|||||||
BSD 2-Clause License
|
BSD 2-Clause License
|
||||||
|
|
||||||
Copyright (c) 2022-2023, apio.
|
Copyright (c) 2022-2025, apio.
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
82
README.md
@ -1,82 +1,56 @@
|
|||||||
# Luna
|
# Luna
|
||||||
A very basic POSIX operating system for desktop computers, written mostly in C++ and C. [![Build Status](https://drone.cloudapio.eu/api/badges/apio/Luna/status.svg)](https://drone.cloudapio.eu/apio/Luna)
|
A simple POSIX-based operating system for 64-bit computers, written in C++.
|
||||||
|
|
||||||
## Another UNIX clone?
|
## Another UNIX clone?
|
||||||
[Yes, another UNIX clone](https://wiki.osdev.org/User:Sortie/Yes_Another_Unix_Clone).
|
[Yes, another UNIX clone](https://wiki.osdev.org/User:Sortie/Yes_Another_Unix_Clone).
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- x86_64-compatible lightweight [kernel](kernel/).
|
- Lightweight 64-bit [kernel](kernel/). Compatible with the x86_64 architecture.
|
||||||
- Preemptive multitasking, with a round-robin [scheduler](kernel/src/thread/) that can switch between tasks.
|
- Basic threads/processes, using a simple round-robin [scheduler](kernel/src/thread/).
|
||||||
- [Virtual file system](kernel/src/fs/) with a simple but working [tmpfs](kernel/src/fs/tmpfs/) populated from the initial ramdisk.
|
- Read-only [ext2](kernel/src/fs/ext2/) filesystem.
|
||||||
- Can [load ELF programs](kernel/src/ELF.cpp) from the file system as userspace tasks.
|
- Can [load ELF executables](kernel/src/binfmt/ELF.cpp), [shebang scripts](kernel/src/binfmt/Script.cpp) or [arbitrary binary formats](kernel/src/binfmt/BinaryFormat.h) (registered through kernel modules, which are not supported yet =D).
|
||||||
- [System call](kernel/src/sys/) interface and [C Library](libc/), aiming to be mostly POSIX-compatible.
|
- [C Library](libc/), aiming for POSIX compatibility, with many features such as local domain sockets, signals, and shared memory.
|
||||||
- Designed to be [portable](kernel/src/arch), no need to be restricted to x86_64.
|
- Support for [several third-party programs](ports/), including the [GNU binutils](ports/binutils/PACKAGE) suite of utilities and the [GCC](ports/gcc/PACKAGE) compiler.
|
||||||
- Fully [UTF-8 aware](libluna/include/luna/Utf8.h), **everywhere**.
|
- Designed to be [portable](kernel/src/arch), so that additional architectures can be added in the future with relatively low effort.
|
||||||
- [Thread](libluna/include/luna/Atomic.h) [safety](kernel/src/thread/Spinlock.h) (supposedly).
|
- Everything text-related is designed around [UTF-8](libluna/include/luna/Utf8.h).
|
||||||
- Environment-agnostic [utility library](libluna/), which can be used in both kernel and userspace.
|
- Environment-agnostic [utility library](libluna/), which can be used in both kernel and userspace.
|
||||||
- Return-oriented [error propagation](libluna/include/luna/Result.h), inspired by Rust and SerenityOS.
|
- An extensive set of [standard Unix utilities](apps/), from [ls](apps/ls.cpp) to [uname](apps/uname.cpp) to [base64](apps/base64.cpp). Written in modern C++ and very small amounts of code, using Luna's practical [OS library](libos/).
|
||||||
- Build system uses [CMake](CMakeLists.txt).
|
- A simple and efficient [windowing system](wind/), providing a lightweight GUI environment (still in development, not many GUI apps exist).
|
||||||
|
|
||||||
|
## Screenshot
|
||||||
|
![Screenshot as of 0.6.0](docs/screenshots/screenshot-0.6.0.png)
|
||||||
|
|
||||||
|
## System requirements and dependencies
|
||||||
|
|
||||||
|
Read [docs/dependencies.md](docs/dependencies.md) for the full information. In short, all modern Unixes should work, provided the dependencies are available.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
To build and run Luna, you will need to build a [GCC Cross-Compiler](https://wiki.osdev.org/Why_do_I_need_a_Cross_Compiler) and cross-binutils for `x86_64-luna`. (Yes, Luna is advanced enough that it can use its own [OS-Specific Toolchain](https://wiki.osdev.org/OS_Specific_Toolchain), instead of a bare metal target like `x86_64-elf`. It is the first of my OS projects to be able to do so. The patches for Binutils and GCC are [binutils.patch](tools/binutils.patch) and [gcc.patch](tools/gcc.patch)).
|
To build and run Luna, you will need to build a [cross-compiler](https://wiki.osdev.org/Why_do_I_need_a_Cross_Compiler) and cross-binutils for `x86_64-luna`.
|
||||||
|
|
||||||
You should start by installing the [required dependencies](https://wiki.osdev.org/GCC_Cross_Compiler#Installing_Dependencies).
|
There is a script provided for this. Run `tools/setup.sh` to build the toolchain.
|
||||||
|
|
||||||
Then, run `tools/setup.sh` to build the toolchain.
|
|
||||||
|
|
||||||
This script will check whether you have the required versions of the toolchain already setup, and will skip building them if so. (This means that it is used by the build scripts to install the toolchain if it is missing before building, so you could skip running it manually.)
|
|
||||||
|
|
||||||
Please beware that building GCC and Binutils can take some time, depending on your machine.
|
Please beware that building GCC and Binutils can take some time, depending on your machine.
|
||||||
|
|
||||||
## Building
|
|
||||||
There are a variety of scripts for building Luna.
|
|
||||||
|
|
||||||
`tools/build.sh` will build the kernel, libc and binaries.
|
|
||||||
|
|
||||||
`tools/rebuild.sh` will do a full rebuild of the kernel, libc and binaries.
|
|
||||||
|
|
||||||
`tools/install.sh` will install those to the system root and initial ramdisk.
|
|
||||||
|
|
||||||
`tools/sync-libc.sh` will install the libc headers to the system root, build libc and install it.
|
|
||||||
|
|
||||||
`tools/build-iso.sh` will build, install, and make an ISO disk image named Luna.iso.
|
|
||||||
|
|
||||||
`tools/build-stable-iso.sh` does the same thing as build-iso.sh, but configures the kernel so that the version does not show the commit hash (used for stable versions).
|
|
||||||
|
|
||||||
`tools/rebuild-iso.sh` will do a clean rebuild, install, and make an ISO disk image.
|
|
||||||
|
|
||||||
In most cases, you should just use `run.sh`, but if you want to build without running, `build-iso.sh`.
|
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
|
|
||||||
You should have [QEMU](https://www.qemu.org/) installed.
|
`tools/run.sh` is the script you should use in most cases. It will build changed files, install, make an ISO image, and run Luna in QEMU.
|
||||||
|
|
||||||
You can choose between 3 run scripts:
|
If you have no toolchain set up, `run.sh` will build it automatically, which means that you don't necessarily have to run `setup.sh` manually since `run.sh` does it for you.
|
||||||
|
|
||||||
`tools/run.sh` is the one you should use in most cases. It will build changed files, install, make an ISO image, and run Luna in QEMU.
|
## Login UI
|
||||||
|
|
||||||
`tools/rebuild-and-run.sh` will rebuild, install, make an ISO, and run Luna in QEMU.
|
For development convenience, the system automatically starts a GUI session as the default user, without prompting for a password.
|
||||||
|
|
||||||
`tools/debug.sh` will run Luna in QEMU with a port open for GDB to connect to. (run `tools/build-debug.sh`, `tools/gdb.sh`, and then `tools/debug.sh` in a separate terminal for an optimal debugging experience)
|
Despite this, Luna does have a login window built-in. If you'd like to try this feature out or start a GUI session as a different user, you'll need to edit [base/etc/loginui.conf](base/etc/loginui.conf) and change the line that says `Autologin=true` to `Autologin=false`.
|
||||||
|
|
||||||
Essentially, since `run.sh` builds the toolchain if it hasn't been built, builds Luna if it hasn't been built, and runs it, you could just checkout this repo, run `run.sh`, and you're done. No need for the other scripts. Those are included for more fine-grained control/building step-by-step.
|
|
||||||
|
|
||||||
You can pass any arguments you want to the run scripts, and those will be forwarded to QEMU. Example: `tools/run.sh -m 512M -net none -machine q35`.
|
|
||||||
|
|
||||||
## Prebuilt images
|
## Prebuilt images
|
||||||
|
|
||||||
Prebuilt ISO images (numbered) for every version can be found at [pub.cloudapio.eu](https://pub.cloudapio.eu/luna/releases).
|
Prebuilt ISO images for every release version can be found at [pub.cloudapio.eu](https://pub.cloudapio.eu/luna/releases).
|
||||||
|
|
||||||
These images are built manually whenever I decide to make a new version, and thus don't reflect the latest changes on the `main` branch.
|
|
||||||
|
|
||||||
Every hour, my server pulls the latest commits on `main` and builds an hourly ISO image. The ten most recent ones can be found in the [hourly](https://pub.cloudapio.eu/luna/hourly) directory, and [Luna-latest.iso](https://pub.cloudapio.eu/luna/Luna-latest.iso) should always be symlinked to the newest one.
|
|
||||||
|
|
||||||
These images do reflect the latest changes on the `main` branch, but are obviously less stable. Additionally, an hourly image will be skipped if building the latest commit of the project fails.
|
|
||||||
|
|
||||||
## Is there third-party software I can use on Luna?
|
## Is there third-party software I can use on Luna?
|
||||||
|
|
||||||
Not right now, but hopefully we can start porting some software soon! (After the VFS and fork/exec are done, of course. So, in a long time.)
|
Yes! A ports system is in place, and you can use the build scripts to add some ports to your image. More information in the [Ports](ports/README.md) page.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
Luna is open-source and free software under the [BSD-2 License](LICENSE).
|
Luna is open-source and free software under the [BSD-2-Clause License](LICENSE).
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
function(luna_app SOURCE_FILE APP_NAME SETUID)
|
|
||||||
add_executable(${APP_NAME} ${SOURCE_FILE})
|
|
||||||
target_compile_options(${APP_NAME} PRIVATE -Os ${COMMON_FLAGS} -Wno-write-strings)
|
|
||||||
add_dependencies(${APP_NAME} libc)
|
|
||||||
target_include_directories(${APP_NAME} PRIVATE ${LUNA_BASE}/usr/include)
|
|
||||||
target_link_libraries(${APP_NAME} PRIVATE os)
|
|
||||||
if(${SETUID})
|
|
||||||
install(TARGETS ${APP_NAME} DESTINATION ${LUNA_ROOT}/initrd/bin PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE SETUID)
|
|
||||||
else()
|
|
||||||
install(TARGETS ${APP_NAME} DESTINATION ${LUNA_ROOT}/initrd/bin)
|
|
||||||
endif()
|
|
||||||
endfunction()
|
|
||||||
|
|
||||||
luna_app(init.cpp init OFF)
|
|
||||||
luna_app(env.cpp env OFF)
|
|
||||||
luna_app(su.cpp su ON)
|
|
||||||
luna_app(sh.cpp sh OFF)
|
|
||||||
luna_app(cat.cpp cat OFF)
|
|
||||||
luna_app(date.cpp date OFF)
|
|
||||||
luna_app(edit.cpp edit OFF)
|
|
||||||
luna_app(ls.cpp ls OFF)
|
|
||||||
luna_app(chown.cpp chown OFF)
|
|
||||||
luna_app(chmod.cpp chmod OFF)
|
|
||||||
luna_app(mkdir.cpp mkdir OFF)
|
|
||||||
luna_app(rm.cpp rm OFF)
|
|
||||||
luna_app(stat.cpp stat OFF)
|
|
||||||
luna_app(uname.cpp uname OFF)
|
|
262
apps/init.cpp
@ -1,262 +0,0 @@
|
|||||||
#include <luna/PathParser.h>
|
|
||||||
#include <luna/String.h>
|
|
||||||
#include <luna/Vector.h>
|
|
||||||
#include <os/File.h>
|
|
||||||
#include <os/Process.h>
|
|
||||||
|
|
||||||
#include <dirent.h>
|
|
||||||
#include <errno.h>
|
|
||||||
#include <fcntl.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <sys/stat.h>
|
|
||||||
#include <sys/syscall.h>
|
|
||||||
#include <sys/sysmacros.h>
|
|
||||||
#include <sys/wait.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
|
|
||||||
FILE* g_init_log;
|
|
||||||
|
|
||||||
#define xmknod(path, mode, maj, min) \
|
|
||||||
if (mknod(path, mode, makedev(maj, min)) < 0) exit(255);
|
|
||||||
|
|
||||||
// Too early for console logs (/dev/console is created here!), so we have to resort to exiting with a weird exit code in
|
|
||||||
// case of failure.
|
|
||||||
static void populate_devfs()
|
|
||||||
{
|
|
||||||
if (mkdir("/dev", 0755) < 0 && errno != EEXIST) exit(255);
|
|
||||||
|
|
||||||
xmknod("/dev/console", 0666, 1, 0);
|
|
||||||
xmknod("/dev/null", 0666, 2, 0);
|
|
||||||
xmknod("/dev/zero", 0666, 2, 1);
|
|
||||||
xmknod("/dev/fb0", 0222, 3, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Service
|
|
||||||
{
|
|
||||||
String name;
|
|
||||||
String command;
|
|
||||||
bool restart { false };
|
|
||||||
String environment;
|
|
||||||
Option<pid_t> pid {};
|
|
||||||
};
|
|
||||||
|
|
||||||
Vector<Service> g_services;
|
|
||||||
|
|
||||||
static Result<void> service_child(const Service& service)
|
|
||||||
{
|
|
||||||
auto args = TRY(service.command.split(" \n"));
|
|
||||||
|
|
||||||
if (service.environment.is_empty()) { TRY(os::Process::exec(args[0].view(), args.slice(), false)); }
|
|
||||||
else
|
|
||||||
{
|
|
||||||
auto env = TRY(service.environment.split(",\n"));
|
|
||||||
TRY(os::Process::exec(args[0].view(), args.slice(), env.slice(), false));
|
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
static Result<void> try_start_service(Service& service)
|
|
||||||
{
|
|
||||||
pid_t pid = TRY(os::Process::fork());
|
|
||||||
if (pid == 0)
|
|
||||||
{
|
|
||||||
auto rc = service_child(service);
|
|
||||||
if (rc.has_error())
|
|
||||||
{
|
|
||||||
fprintf(g_init_log, "[child %d] failed to start service %s due to error: %s\n", getpid(),
|
|
||||||
service.name.chars(), rc.error_string());
|
|
||||||
}
|
|
||||||
fclose(g_init_log);
|
|
||||||
exit(127);
|
|
||||||
}
|
|
||||||
|
|
||||||
fprintf(g_init_log, "[init] created new child process %d for service %s\n", pid, service.name.chars());
|
|
||||||
|
|
||||||
service.pid = pid;
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
static void start_service(Service& service)
|
|
||||||
{
|
|
||||||
auto rc = try_start_service(service);
|
|
||||||
if (rc.has_error())
|
|
||||||
{
|
|
||||||
fprintf(g_init_log, "[init] failed to start service %s due to error: %s\n", service.name.chars(),
|
|
||||||
rc.error_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Result<void> load_service(StringView path)
|
|
||||||
{
|
|
||||||
fprintf(g_init_log, "[init] reading service file: %s\n", path.chars());
|
|
||||||
|
|
||||||
auto file = TRY(os::File::open(path, os::File::ReadOnly));
|
|
||||||
|
|
||||||
Service service;
|
|
||||||
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
auto line = TRY(file->read_line());
|
|
||||||
if (line.is_empty()) break;
|
|
||||||
|
|
||||||
line.trim("\n");
|
|
||||||
if (line.is_empty()) continue;
|
|
||||||
|
|
||||||
auto parts = TRY(line.split_once('='));
|
|
||||||
if (parts.size() < 2 || parts[0].is_empty() || parts[1].is_empty())
|
|
||||||
{
|
|
||||||
fprintf(g_init_log, "[init] file contains invalid line, aborting: '%s'\n", line.chars());
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parts[0].view() == "Name")
|
|
||||||
{
|
|
||||||
service.name = move(parts[1]);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parts[0].view() == "Command")
|
|
||||||
{
|
|
||||||
service.command = move(parts[1]);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parts[0].view() == "Restart")
|
|
||||||
{
|
|
||||||
if (parts[1].view() == "true" || parts[1].view().to_uint().value_or(0) == 1)
|
|
||||||
{
|
|
||||||
service.restart = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
service.restart = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parts[0].view() == "Environment")
|
|
||||||
{
|
|
||||||
service.environment = move(parts[1]);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
fprintf(g_init_log, "[init] skipping unknown entry name %s\n", parts[0].chars());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (service.name.is_empty())
|
|
||||||
{
|
|
||||||
fprintf(g_init_log, "[init] service file is missing 'Name' entry, aborting!\n");
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (service.command.is_empty())
|
|
||||||
{
|
|
||||||
fprintf(g_init_log, "[init] service file is missing 'Command' entry, aborting!\n");
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
fprintf(g_init_log, "[init] loaded service %s into memory\n", service.name.chars());
|
|
||||||
|
|
||||||
TRY(g_services.try_append(move(service)));
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
static Result<void> load_services()
|
|
||||||
{
|
|
||||||
DIR* dp = opendir("/etc/init");
|
|
||||||
if (!dp)
|
|
||||||
{
|
|
||||||
fprintf(g_init_log, "[init] cannot open service directory: %s\n", strerror(errno));
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
dirent* ent;
|
|
||||||
while ((ent = readdir(dp)))
|
|
||||||
{
|
|
||||||
if ("."_sv == ent->d_name || ".."_sv == ent->d_name) continue;
|
|
||||||
|
|
||||||
auto service_path = TRY(PathParser::join("/etc/init"_sv, ent->d_name));
|
|
||||||
TRY(load_service(service_path.view()));
|
|
||||||
}
|
|
||||||
|
|
||||||
closedir(dp);
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
static Result<void> start_services()
|
|
||||||
{
|
|
||||||
TRY(load_services());
|
|
||||||
for (auto& service : g_services)
|
|
||||||
{
|
|
||||||
fprintf(g_init_log, "[init] starting service %s\n", service.name.chars());
|
|
||||||
start_service(service);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
static Result<void> set_hostname()
|
|
||||||
{
|
|
||||||
auto file = TRY(os::File::open("/etc/hostname", os::File::ReadOnly));
|
|
||||||
|
|
||||||
auto hostname = TRY(file->read_line());
|
|
||||||
hostname.trim("\n");
|
|
||||||
|
|
||||||
if (sethostname(hostname.chars(), hostname.length()) < 0) return {};
|
|
||||||
|
|
||||||
fprintf(g_init_log, "[init] successfully set system hostname to '%s'\n", hostname.chars());
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
int main()
|
|
||||||
{
|
|
||||||
if (getpid() != 1)
|
|
||||||
{
|
|
||||||
fprintf(stderr, "error: init not running as PID 1.\n");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
populate_devfs();
|
|
||||||
|
|
||||||
// Before this point, we don't even have an stdin, stdout and stderr. Set it up now so that child processes (and us)
|
|
||||||
// can print stuff.
|
|
||||||
stdin = fopen("/dev/console", "r");
|
|
||||||
stdout = fopen("/dev/console", "w");
|
|
||||||
stderr = fopen("/dev/console", "w");
|
|
||||||
|
|
||||||
g_init_log = fopen("/init.log", "w+");
|
|
||||||
fcntl(fileno(g_init_log), F_SETFD, FD_CLOEXEC);
|
|
||||||
|
|
||||||
set_hostname();
|
|
||||||
|
|
||||||
start_services();
|
|
||||||
|
|
||||||
while (1)
|
|
||||||
{
|
|
||||||
int status;
|
|
||||||
pid_t child = wait(&status);
|
|
||||||
|
|
||||||
for (auto& service : g_services)
|
|
||||||
{
|
|
||||||
if (service.pid.has_value() && service.pid.value() == child)
|
|
||||||
{
|
|
||||||
fprintf(g_init_log, "[init] service %s exited with status %d\n", service.name.chars(),
|
|
||||||
WEXITSTATUS(status));
|
|
||||||
|
|
||||||
if (service.restart)
|
|
||||||
{
|
|
||||||
fprintf(g_init_log, "[init] restarting service %s\n", service.name.chars());
|
|
||||||
|
|
||||||
start_service(service);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
42
apps/ls.cpp
@ -1,42 +0,0 @@
|
|||||||
#include <os/ArgumentParser.h>
|
|
||||||
|
|
||||||
#include <dirent.h>
|
|
||||||
#include <fcntl.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
|
|
||||||
Result<int> luna_main(int argc, char** argv)
|
|
||||||
{
|
|
||||||
StringView pathname;
|
|
||||||
bool show_all { false };
|
|
||||||
bool show_almost_all { false };
|
|
||||||
|
|
||||||
os::ArgumentParser parser;
|
|
||||||
parser.add_description("List files contained in a directory (defaults to '.', the current directory)"_sv);
|
|
||||||
parser.add_positional_argument(pathname, "directory"_sv, "."_sv);
|
|
||||||
parser.add_switch_argument(show_all, 'a', "all"_sv, "also list hidden files (whose filename begins with a dot)"_sv);
|
|
||||||
parser.add_switch_argument(show_almost_all, 'A', "almost-all"_sv, "list all files except '.' and '..'"_sv);
|
|
||||||
parser.parse(argc, argv);
|
|
||||||
|
|
||||||
DIR* dp = opendir(pathname.chars());
|
|
||||||
if (!dp)
|
|
||||||
{
|
|
||||||
perror("opendir");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int first_ent = 1;
|
|
||||||
do {
|
|
||||||
struct dirent* ent = readdir(dp);
|
|
||||||
if (!ent) break;
|
|
||||||
if (show_almost_all && (!strcmp(ent->d_name, ".") || !strcmp(ent->d_name, ".."))) continue;
|
|
||||||
if (!show_all && !show_almost_all && *ent->d_name == '.') continue;
|
|
||||||
printf(first_ent ? "%s" : " %s", ent->d_name);
|
|
||||||
first_ent = 0;
|
|
||||||
} while (1);
|
|
||||||
|
|
||||||
putchar('\n');
|
|
||||||
|
|
||||||
closedir(dp);
|
|
||||||
return 0;
|
|
||||||
}
|
|
21
apps/rm.cpp
@ -1,21 +0,0 @@
|
|||||||
#include <os/ArgumentParser.h>
|
|
||||||
#include <os/FileSystem.h>
|
|
||||||
|
|
||||||
Result<int> luna_main(int argc, char** argv)
|
|
||||||
{
|
|
||||||
StringView path;
|
|
||||||
bool recursive;
|
|
||||||
|
|
||||||
os::ArgumentParser parser;
|
|
||||||
parser.add_description("Remove a path from the file system."_sv);
|
|
||||||
parser.add_positional_argument(path, "path"_sv, true);
|
|
||||||
parser.add_switch_argument(recursive, 'r', "recursive"_sv,
|
|
||||||
"remove a directory recursively (by default, rm removes only empty directories)"_sv);
|
|
||||||
parser.parse(argc, argv);
|
|
||||||
|
|
||||||
if (!recursive) TRY(os::FileSystem::remove(path));
|
|
||||||
else
|
|
||||||
TRY(os::FileSystem::remove_tree(path));
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
118
apps/sh.cpp
@ -1,118 +0,0 @@
|
|||||||
#include <luna/String.h>
|
|
||||||
#include <luna/Vector.h>
|
|
||||||
#include <os/ArgumentParser.h>
|
|
||||||
#include <os/File.h>
|
|
||||||
#include <os/FileSystem.h>
|
|
||||||
#include <os/Process.h>
|
|
||||||
|
|
||||||
#include <errno.h>
|
|
||||||
#include <pwd.h>
|
|
||||||
#include <stddef.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <sys/utsname.h>
|
|
||||||
#include <sys/wait.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
|
|
||||||
using os::File;
|
|
||||||
|
|
||||||
static Result<Vector<String>> split_command_into_args(StringView cmd)
|
|
||||||
{
|
|
||||||
return cmd.split(" \n"_sv);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Result<void> execute_command(StringView command)
|
|
||||||
{
|
|
||||||
auto args = TRY(split_command_into_args(command));
|
|
||||||
if (args.size() < 1) exit(0);
|
|
||||||
|
|
||||||
return os::Process::exec(args[0].view(), args.slice());
|
|
||||||
}
|
|
||||||
|
|
||||||
struct utsname g_sysinfo;
|
|
||||||
|
|
||||||
const char* hostname = "";
|
|
||||||
const char* username = "";
|
|
||||||
char prompt_end = '$';
|
|
||||||
|
|
||||||
Result<int> luna_main(int argc, char** argv)
|
|
||||||
{
|
|
||||||
StringView path;
|
|
||||||
StringView command;
|
|
||||||
bool interactive { false };
|
|
||||||
|
|
||||||
SharedPtr<File> input_file;
|
|
||||||
|
|
||||||
os::ArgumentParser parser;
|
|
||||||
parser.add_description("The Luna system's command shell."_sv);
|
|
||||||
parser.add_positional_argument(path, "path"_sv, "-"_sv);
|
|
||||||
parser.add_value_argument(command, 'c', "command"_sv, true, "execute a single command and then exit"_sv);
|
|
||||||
parser.parse(argc, argv);
|
|
||||||
|
|
||||||
if (!command.is_empty()) TRY(execute_command(command));
|
|
||||||
|
|
||||||
if (path == "-")
|
|
||||||
{
|
|
||||||
input_file = File::standard_input();
|
|
||||||
interactive = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
input_file = TRY(File::open(path, File::ReadOnly));
|
|
||||||
input_file->set_close_on_exec();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (interactive)
|
|
||||||
{
|
|
||||||
// Set up everything to form a prompt.
|
|
||||||
uname(&g_sysinfo);
|
|
||||||
hostname = g_sysinfo.nodename;
|
|
||||||
|
|
||||||
if (getuid() == 0) prompt_end = '#';
|
|
||||||
struct passwd* pw = getpwuid(getuid());
|
|
||||||
if (pw) { username = pw->pw_name; }
|
|
||||||
}
|
|
||||||
|
|
||||||
while (1)
|
|
||||||
{
|
|
||||||
if (interactive)
|
|
||||||
{
|
|
||||||
auto cwd = TRY(os::FileSystem::working_directory());
|
|
||||||
printf("%s@%s:%s%c ", username, hostname, cwd.chars(), prompt_end);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto cmd = TRY(input_file->read_line());
|
|
||||||
if (cmd.is_empty()) break;
|
|
||||||
|
|
||||||
if (strspn(cmd.chars(), " \n") == cmd.length()) continue;
|
|
||||||
|
|
||||||
if (!strncmp(cmd.chars(), "cd", 2))
|
|
||||||
{
|
|
||||||
auto args = TRY(split_command_into_args(cmd.view()));
|
|
||||||
check(args[0].view() == "cd");
|
|
||||||
|
|
||||||
if (args.size() == 1)
|
|
||||||
{
|
|
||||||
auto home = TRY(os::FileSystem::home_directory());
|
|
||||||
TRY(os::FileSystem::change_directory(home.view()));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
TRY(os::FileSystem::change_directory(args[1].view()));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
pid_t child = TRY(os::Process::fork());
|
|
||||||
|
|
||||||
if (child == 0) { TRY(execute_command(cmd.view())); }
|
|
||||||
|
|
||||||
if (waitpid(child, NULL, 0) < 0)
|
|
||||||
{
|
|
||||||
perror("waitpid");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
#include <os/ArgumentParser.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <sys/stat.h>
|
|
||||||
|
|
||||||
static const char* file_type(mode_t mode)
|
|
||||||
{
|
|
||||||
switch (mode & S_IFMT)
|
|
||||||
{
|
|
||||||
case S_IFREG: return "regular file";
|
|
||||||
case S_IFDIR: return "directory";
|
|
||||||
case S_IFCHR: return "character special device";
|
|
||||||
default: return "unknown file type";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Result<int> luna_main(int argc, char** argv)
|
|
||||||
{
|
|
||||||
StringView path;
|
|
||||||
|
|
||||||
os::ArgumentParser parser;
|
|
||||||
parser.add_description("Display file status.");
|
|
||||||
parser.add_positional_argument(path, "path", true);
|
|
||||||
parser.parse(argc, argv);
|
|
||||||
|
|
||||||
struct stat st;
|
|
||||||
if (stat(path.chars(), &st) < 0)
|
|
||||||
{
|
|
||||||
perror("stat");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
printf(" File: %s\n", path.chars());
|
|
||||||
printf(" Size: %zu (%s)\n", st.st_size, file_type(st.st_mode));
|
|
||||||
printf("Inode: %lu Links: %lu\n", st.st_ino, st.st_nlink);
|
|
||||||
printf(" Mode: %#o UID: %u GID: %u\n", st.st_mode & ~S_IFMT, st.st_uid, st.st_gid);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
98
apps/su.cpp
@ -1,98 +0,0 @@
|
|||||||
#include <bits/termios.h>
|
|
||||||
#include <os/ArgumentParser.h>
|
|
||||||
#include <pwd.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <sys/ioctl.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
|
|
||||||
static struct termios orig;
|
|
||||||
|
|
||||||
void restore_terminal()
|
|
||||||
{
|
|
||||||
ioctl(fileno(stdin), TCSETS, &orig);
|
|
||||||
}
|
|
||||||
|
|
||||||
char* getpass()
|
|
||||||
{
|
|
||||||
fputs("Password: ", stdout);
|
|
||||||
|
|
||||||
if (ioctl(fileno(stdin), TCGETS, &orig) < 0)
|
|
||||||
{
|
|
||||||
perror("ioctl(TCGETS)");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
atexit(restore_terminal);
|
|
||||||
|
|
||||||
struct termios tc = orig;
|
|
||||||
tc.c_lflag &= ~ECHO;
|
|
||||||
if (ioctl(fileno(stdin), TCSETS, &tc) < 0)
|
|
||||||
{
|
|
||||||
perror("ioctl(TCSETS)");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
static char buf[1024];
|
|
||||||
char* rc = fgets(buf, sizeof(buf), stdin);
|
|
||||||
|
|
||||||
restore_terminal();
|
|
||||||
putchar('\n');
|
|
||||||
|
|
||||||
if (!rc)
|
|
||||||
{
|
|
||||||
perror("fgets");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
char* newline = strrchr(rc, '\n');
|
|
||||||
if (newline) *newline = 0;
|
|
||||||
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
Result<int> luna_main(int argc, char** argv)
|
|
||||||
{
|
|
||||||
StringView name;
|
|
||||||
|
|
||||||
if (geteuid() != 0)
|
|
||||||
{
|
|
||||||
fprintf(stderr, "su must be setuid root!\n");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
os::ArgumentParser parser;
|
|
||||||
parser.add_description("Switch to a different user (by default, root)."_sv);
|
|
||||||
parser.add_positional_argument(name, "name"_sv, "root"_sv);
|
|
||||||
parser.parse(argc, argv);
|
|
||||||
|
|
||||||
struct passwd* entry = getpwnam(name.chars());
|
|
||||||
if (!entry)
|
|
||||||
{
|
|
||||||
fprintf(stderr, "su: user %s not found!\n", name.chars());
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (getuid() != geteuid() && *entry->pw_passwd)
|
|
||||||
{
|
|
||||||
char* pass = getpass();
|
|
||||||
if (!pass) return 1;
|
|
||||||
|
|
||||||
if (strcmp(pass, entry->pw_passwd))
|
|
||||||
{
|
|
||||||
fprintf(stderr, "Wrong password!\n");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
memset(pass, 0, strlen(pass));
|
|
||||||
}
|
|
||||||
|
|
||||||
setgid(entry->pw_gid);
|
|
||||||
setuid(entry->pw_uid);
|
|
||||||
|
|
||||||
chdir(entry->pw_dir);
|
|
||||||
|
|
||||||
execl(entry->pw_shell, entry->pw_shell, NULL);
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
5
base/etc/group
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
root:!:0:
|
||||||
|
users:!:1:selene
|
||||||
|
wind:!:2:selene
|
||||||
|
wsys:!:3:
|
||||||
|
selene:!:1000:
|
4
base/etc/init/00-home
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
Name=mount-home
|
||||||
|
Description=Mount the user's home directory on a writable filesystem.
|
||||||
|
Command=/etc/startup/mount-home.sh
|
||||||
|
Wait=true
|
6
base/etc/init/99-login
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
Name=login
|
||||||
|
Description=Start a graphical user session.
|
||||||
|
Command=/usr/bin/loginui
|
||||||
|
StandardOutput=/dev/uart0
|
||||||
|
StandardError=/dev/uart0
|
||||||
|
Restart=true
|
5
base/etc/loginui.conf
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Configuration file for loginui.
|
||||||
|
# If this parameter is set to "true", loginui automatically spawns a UI session as the below user instead of prompting for a username and password.
|
||||||
|
Autologin=true
|
||||||
|
# The user to create a session for if "Autologin" is set to true (see above). If the username is invalid, loginui will behave as if "Autologin" was set to false.
|
||||||
|
AutologinUser=selene
|
4
base/etc/motd
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
Welcome to the Luna system!
|
||||||
|
Have a look around, you can run 'ls /bin' to list available commands.
|
||||||
|
|
||||||
|
You can use the username 'selene' with the password 'moon' to start a user session.
|
3
base/etc/passwd
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
root:x:0:0:Administrator:/:/usr/bin/sh
|
||||||
|
wind:x:2:2:Window Manager:/:/usr/bin/init
|
||||||
|
selene:x:1000:1000:User:/home/selene:/usr/bin/sh
|
3
base/etc/shadow
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
root:ce5ca673d13b36118d54a7cf13aeb0ca012383bf771e713421b4d1fd841f539a:0:0:99999:7:::
|
||||||
|
wind:!:0:0:99999:7:::
|
||||||
|
selene:9e78b43ea00edcac8299e0cc8df7f6f913078171335f733a21d5d911b6999132:0:0:99999:7:::
|
26
base/etc/skel/welcome
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
Welcome to the Luna operating system!
|
||||||
|
You are running on the default user account, selene.
|
||||||
|
|
||||||
|
If you are familiar with Unix-style operating systems (like Linux or *BSD),
|
||||||
|
you should be able to use the Luna terminal without much problems.
|
||||||
|
|
||||||
|
Following the traditional Unix filesystem structure,
|
||||||
|
programs are installed in /usr/bin (/bin is a symlink to /usr/bin).
|
||||||
|
The command `ls /bin` will show all commands available on
|
||||||
|
your current Luna installation.
|
||||||
|
|
||||||
|
Currently, because of driver limitations,
|
||||||
|
the root file system is mounted read-only.
|
||||||
|
Your home folder is writable, but volatile; it is
|
||||||
|
created and populated on boot,
|
||||||
|
and its contents will vanish after a reboot.
|
||||||
|
|
||||||
|
The system is booted using the 'init' program.
|
||||||
|
You can read its configuration files in the /etc/init directory to
|
||||||
|
learn more about the boot process.
|
||||||
|
|
||||||
|
Luna is free software, released under the BSD-2-Clause license.
|
||||||
|
The license is included in the LICENSE file in your home directory.
|
||||||
|
|
||||||
|
View the source code and read more about Luna at
|
||||||
|
https://git.cloudapio.eu/apio/Luna.
|
10
base/etc/startup/mount-home.sh
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Create and populate a volatile home directory.
|
||||||
|
mount -t tmpfs tmpfs /home/selene
|
||||||
|
chown selene:selene /home/selene
|
||||||
|
|
||||||
|
cp /etc/skel/welcome /home/selene/
|
||||||
|
cp /etc/skel/LICENSE /home/selene/
|
||||||
|
|
||||||
|
chown selene:selene /home/selene/welcome
|
||||||
|
chown selene:selene /home/selene/LICENSE
|
3
base/etc/user/00-welcome
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Name=welcome
|
||||||
|
Description=Show a welcome message for the user.
|
||||||
|
Command=/usr/bin/editor welcome
|
3
base/usr/share/applications/00-terminal
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Name=terminal
|
||||||
|
Icon=/usr/share/icons/32x32/app-terminal.tga
|
||||||
|
Command=/usr/bin/terminal
|
3
base/usr/share/applications/01-about
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Name=about
|
||||||
|
Icon=/usr/share/icons/32x32/app-about.tga
|
||||||
|
Command=/usr/bin/about
|
3
base/usr/share/applications/02-gol
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Name=gol
|
||||||
|
Icon=/usr/share/icons/32x32/app-gol.tga
|
||||||
|
Command=/usr/bin/gol
|
3
base/usr/share/applications/03-clock
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Name=clock
|
||||||
|
Icon=/usr/share/icons/32x32/app-clock.tga
|
||||||
|
Command=/usr/bin/clock
|
3
base/usr/share/applications/04-2048
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Name=2048
|
||||||
|
Icon=/usr/share/icons/32x32/app-2048.tga
|
||||||
|
Command=/usr/bin/2048
|
3
base/usr/share/applications/05-editor
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Name=editor
|
||||||
|
Icon=/usr/share/icons/32x32/app-editor.tga
|
||||||
|
Command=/usr/bin/editor
|
BIN
base/usr/share/cursors/default.tga
Normal file
After Width: | Height: | Size: 1004 B |
BIN
base/usr/share/fonts/Tamsyn-Bold.psf
Normal file
BIN
base/usr/share/fonts/Tamsyn-Regular.psf
Normal file
BIN
base/usr/share/icons/16x16/app-close.tga
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
base/usr/share/icons/32x32/app-2048.tga
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
base/usr/share/icons/32x32/app-about.tga
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
base/usr/share/icons/32x32/app-clock.tga
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
base/usr/share/icons/32x32/app-editor.tga
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
base/usr/share/icons/32x32/app-gol.tga
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
base/usr/share/icons/32x32/app-terminal.tga
Normal file
After Width: | Height: | Size: 4.0 KiB |
256
docs/boot_process.md
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
# The Luna boot process
|
||||||
|
|
||||||
|
## Stage 0: The Bootloader
|
||||||
|
Luna uses the [BOOTBOOT](https://gitlab.com/bztsrc/bootboot) bootloader. _(For more information, read the [bootloader specification](https://gitlab.com/bztsrc/bootboot/-/blob/master/bootboot_spec_1st_ed.pdf).)_
|
||||||
|
|
||||||
|
This bootloader reads the initial ramdisk, which contains the following files:
|
||||||
|
```
|
||||||
|
/sys/config - copy of the configuration file for the bootloader
|
||||||
|
/boot/moon - the kernel itself
|
||||||
|
/bin/preinit - the first user program run in the boot process, before the root filesystem is mounted
|
||||||
|
```
|
||||||
|
|
||||||
|
The bootloader loads the kernel in 64-bit mode into the higher half at address `0xffffffffffe02000`, with an appropriate stack already set up.
|
||||||
|
|
||||||
|
The first 16Gb of memory are identity-mapped at page 0.
|
||||||
|
|
||||||
|
It places a few other things into known addresses:
|
||||||
|
```
|
||||||
|
0xfffffffffc000000 - initial framebuffer
|
||||||
|
0xffffffffffe00000 - bootloader information passed to the kernel
|
||||||
|
0xffffffffffe01000 - kernel command line
|
||||||
|
```
|
||||||
|
|
||||||
|
From here, the kernel takes over.
|
||||||
|
|
||||||
|
## Stage 1: The Kernel
|
||||||
|
_Relevant files: [kernel/src/main.cpp](../kernel/src/main.cpp), [kernel/src/arch/x86_64/CPU.cpp](../kernel/src/arch/x86_64/CPU.cpp#L285)_
|
||||||
|
|
||||||
|
The kernel begins execution in the `_start()` function. This function initializes basic kernel functionality, such as time-keeping, memory management, graphics, and finally threading.
|
||||||
|
|
||||||
|
Once threading is set up and the scheduler is started, the kernel starts up a new kernel thread titled `[kinit]` to finish starting up other subsystems that assume they're running in a thread.
|
||||||
|
|
||||||
|
Before switching to `[kinit]`, `_start` does one more thing, it calls the `CPU::platform_finish_init()` function which is platform-specific. On x86_64, this function does the following things:
|
||||||
|
|
||||||
|
- Creates a new kernel thread: `[x86_64-io]`, which handles keyboard and mouse interrupts asynchronously
|
||||||
|
- Starts receiving external interrupts
|
||||||
|
- Initializes the mouse
|
||||||
|
|
||||||
|
As soon as the scheduler switches to the `[kinit]` thread, it will never return to `_start` (since it has no thread associated to it).
|
||||||
|
|
||||||
|
**IMPORTANT**: Although the `[kinit]` thread is the first thread to be started in the system, it has PID 2, not 1. The reason for this is that PID 1 is reserved for the userspace init process.
|
||||||
|
|
||||||
|
`[kinit]` does the following things, in order:
|
||||||
|
|
||||||
|
- Loads kernel debug symbols from the initial ramdisk
|
||||||
|
- Creates the virtual file system and mounts the initial ramdisk on /
|
||||||
|
- Initializes virtual device files such as `/dev/null` (the internal kernel representation of them, `/dev` is not mounted yet)
|
||||||
|
- Loads `/bin/preinit` from the initial ramdisk as PID 1
|
||||||
|
- Creates two more kernel threads, `[reap]` and `[oom]`
|
||||||
|
- Scans for ATA hard disks and reads their partition tables
|
||||||
|
- Finally, it sets PID 1's state to "Running" so that the scheduler can switch to it, and exits
|
||||||
|
|
||||||
|
### Kernel threads
|
||||||
|
|
||||||
|
`[kinit]` spawns two more kernel threads, `[reap]` and `[oom]`. While `[kinit]` exits before PID 1 is started, `[reap]` and `[oom]` are present throughout the lifetime of a Luna system, and can be seen in the output of `ps`. Let's take a look at what they do.
|
||||||
|
|
||||||
|
- `[reap]`: To understand what this thread does, we must take a look at what happens when processes exit on Luna.
|
||||||
|
|
||||||
|
_(Relevant files: [kernel/src/main.cpp](../kernel/src/main.cpp#L23), [kernel/src/thread/Scheduler.cpp](../kernel/src/thread/Scheduler.cpp#L231), [kernel/src/thread/Thread.cpp](../kernel/src/thread/Thread.cpp#L126), [kernel/src/sys/waitpid.cpp](../kernel/src/sys/waitpid.cpp#L84))_
|
||||||
|
|
||||||
|
When a process calls the `_exit()` syscall, all its threads' states are set to "Dying". This tells the scheduler to avoid switching to them, and the process's parent is notified, by sending SIGCHLD and (optionally) unblocking a blocked `waitpid()` call. The process remains visible to the rest of the system, and if its parent does not wait for it, it will stay there as a "zombie process". Meanwhile, the `[reap]` thread runs and collects all the resources from each thread. The process object is still alive (in a "zombie" state), but its threads have been cleaned up.
|
||||||
|
|
||||||
|
When the process's parent waits for it, it is marked for reaping (by setting its thread count to -1 (PROCESS_SHOULD_REAP)), and the `[reap]` thread runs.
|
||||||
|
|
||||||
|
The `[reap]` thread then "reaps" all the dead processes' resources. It frees up their memory, file descriptors, and other resources. After reaping, the process is deleted, and no trace of it is left.
|
||||||
|
|
||||||
|
- `[oom]`: This thread handles Out-Of-Memory (OOM) situations. Whenever the kernel has 1/4 or 1/8 of the available physical memory left (thresholds may be tweaked in the future), or it has run out, it runs this thread.
|
||||||
|
|
||||||
|
The OOM thread then goes through all the disk caches and purges them all, hoping to reclaim as much memory as possible.
|
||||||
|
|
||||||
|
### File system and process layout
|
||||||
|
|
||||||
|
After the kernel stage of the boot process, the system looks like this:
|
||||||
|
|
||||||
|
#### File system
|
||||||
|
```
|
||||||
|
/ - initial ramdisk
|
||||||
|
/sys/config - copy of the configuration file for the bootloader
|
||||||
|
/boot/moon - the kernel itself
|
||||||
|
/bin/preinit - the first user program run in the boot process
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Processes
|
||||||
|
```
|
||||||
|
/bin/preinit - PID 1
|
||||||
|
[kinit] - PID 2 (Exited, soon to be reaped)
|
||||||
|
[x86_64-io] - PID 3
|
||||||
|
[reap] - PID 4
|
||||||
|
[oom] - PID 5
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stage 2: preinit
|
||||||
|
_Relevant files: [system/preinit.cpp](../system/preinit.cpp)_
|
||||||
|
|
||||||
|
Luna's userspace init process is split into two programs: `/bin/preinit`, which resides on the initial ramdisk, and `/usr/bin/init`, which resides on the root partition.
|
||||||
|
|
||||||
|
`/bin/preinit`'s job is to set up the file system in a "minimal known good" state for the actual `init` to run.
|
||||||
|
|
||||||
|
The "minimal known good" state includes:
|
||||||
|
|
||||||
|
- The ext2 root partition, which includes all the binaries in /usr
|
||||||
|
- The /dev file system
|
||||||
|
|
||||||
|
`preinit` does the following things, in order:
|
||||||
|
|
||||||
|
- Mounts `/dev` to get access to disk device files
|
||||||
|
- Mounts the root partition (`/dev/cd0p2`) on `/osroot`
|
||||||
|
- Unmounts `/dev`
|
||||||
|
- Uses the `pivot_root` system call to change the root file system to the one that was in `/osroot`, and mounts the old one on `/mnt` (previously `/osroot/mnt`)
|
||||||
|
- Unmounts the initial ramdisk on `/mnt`
|
||||||
|
- Mounts the `/dev` file system again on the new root partition
|
||||||
|
- Executes `/usr/bin/init`
|
||||||
|
|
||||||
|
For now, much of `preinit`'s functionality is hard-coded, but as Luna supports more devices, it will become responsible for loading device drivers, discovering the root partition, and more...
|
||||||
|
|
||||||
|
### File system and process layout
|
||||||
|
|
||||||
|
After the preinit stage of the boot process, the system looks like this:
|
||||||
|
|
||||||
|
#### File system
|
||||||
|
```
|
||||||
|
/ - ext2 root partition
|
||||||
|
/dev - device file system
|
||||||
|
/usr, /etc, /home... - other directories contained in the root partition
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Processes
|
||||||
|
```
|
||||||
|
/usr/bin/init - PID 1
|
||||||
|
[x86_64-io] - PID 3
|
||||||
|
[reap] - PID 4
|
||||||
|
[oom] - PID 5
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stage 3: init
|
||||||
|
_Relevant files: [system/init.cpp](../system/init.cpp#L406)_
|
||||||
|
|
||||||
|
`/usr/bin/init` is the actual init system. It is in charge of starting user-defined services.
|
||||||
|
|
||||||
|
It does the following things:
|
||||||
|
|
||||||
|
- Mounts `/tmp`, `/dev/shm` and `/dev/pts`
|
||||||
|
- Sets the system hostname by reading `/etc/hostname`
|
||||||
|
- Reads configuration files from `/etc/init`
|
||||||
|
- Starts services defined in `/etc/init`
|
||||||
|
- Enters the init loop, waiting for child processes and restarting them if needed
|
||||||
|
|
||||||
|
Currently, there are two service files defined by default in `/etc/init`:
|
||||||
|
|
||||||
|
`00-home`: This service sets up a `tmpfs` on `/home/selene`, so that the home directory is writable.
|
||||||
|
|
||||||
|
`99-login`: This service starts a graphical session, by calling `/usr/bin/loginui`. This service will be restarted if necessary.
|
||||||
|
|
||||||
|
### File system and process layout
|
||||||
|
|
||||||
|
After the init stage of the boot process, the system looks like this:
|
||||||
|
|
||||||
|
#### File system
|
||||||
|
```
|
||||||
|
/ - ext2 root partition
|
||||||
|
/dev - device file system
|
||||||
|
/dev/shm - POSIX shared memory file system
|
||||||
|
/dev/pts - POSIX pseudoterminal file system
|
||||||
|
/tmp - system temporary file directory
|
||||||
|
/usr, /etc, /home... - other directories contained in the root partition
|
||||||
|
/home/selene - temporary home directory
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Processes
|
||||||
|
```
|
||||||
|
/usr/bin/init - PID 1
|
||||||
|
[x86_64-io] - PID 3
|
||||||
|
[reap] - PID 4
|
||||||
|
[oom] - PID 5
|
||||||
|
/usr/bin/loginui - PID 13
|
||||||
|
```
|
||||||
|
|
||||||
|
_Note: loginui is PID 13 because the `00-home` service is a shell script, which starts a few subprocesses. Since Luna does not allow for PID reuse right now, loginui ends up with PID 13._
|
||||||
|
|
||||||
|
## Stage 4: loginui
|
||||||
|
_Relevant files: [gui/loginui.cpp](../gui/loginui.cpp), [gui/wind/main.cpp](../gui/wind/main.cpp)_
|
||||||
|
|
||||||
|
`/usr/bin/loginui`'s job is quite simple: it prompts the user to log in with their password, after which a graphical session is started.
|
||||||
|
|
||||||
|
_Note: On development builds, Autologin=true is added to /etc/loginui.conf which disables password prompting and executes startui directly._
|
||||||
|
|
||||||
|
First, loginui starts the display server, `/usr/bin/wind`, so that it can use its capabilities to show a graphical login prompt. It is started with permissions `root:root`, and later drops privileges to `wind:wind`.
|
||||||
|
|
||||||
|
After that, loginui prompts for a username and password, checks it against the hashed password stored in `/etc/shadow`, and finally executes `/usr/bin/startui` which does the actual heavy work of starting all the services needed for a UI session.
|
||||||
|
|
||||||
|
### File system and process layout
|
||||||
|
|
||||||
|
After the loginui stage of the boot process, the system looks like this:
|
||||||
|
|
||||||
|
#### File system
|
||||||
|
```
|
||||||
|
/ - ext2 root partition
|
||||||
|
/dev - device file system
|
||||||
|
/dev/shm - POSIX shared memory file system
|
||||||
|
/dev/pts - POSIX pseudoterminal file system
|
||||||
|
/tmp - system temporary file directory
|
||||||
|
/usr, /etc, /home... - other directories contained in the root partition
|
||||||
|
/home/selene - temporary home directory
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Processes
|
||||||
|
```
|
||||||
|
/usr/bin/init - PID 1
|
||||||
|
[x86_64-io] - PID 3
|
||||||
|
[reap] - PID 4
|
||||||
|
[oom] - PID 5
|
||||||
|
/usr/bin/startui - PID 13
|
||||||
|
/usr/bin/wind - PID 14
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stage 5: startui
|
||||||
|
_Relevant files: [system/startui.cpp](../system/startui.cpp), [gui/wind/main.cpp](../gui/wind/main.cpp)_
|
||||||
|
|
||||||
|
`/usr/bin/startui` starts a graphical user session.
|
||||||
|
|
||||||
|
A Luna graphical user session includes the following components:
|
||||||
|
|
||||||
|
- The display server itself, `/usr/bin/wind`. If not already started by loginui, `startui` makes sure it's running.
|
||||||
|
- The execution server (`/usr/bin/execd`), which starts processes and keeps them alive on behalf of other processes. It is started with the standard permissions `selene:selene`.
|
||||||
|
- The taskbar, `/usr/bin/taskbar`. It is started with the standard permissions `selene:selene`, plus an extra group `wsys` to be able to connect to a special display server socket (`/tmp/wsys.sock`, as opposed to the standard `/tmp/wind.sock`). This grants it the ability to use advanced wind features, such as placing the taskbar window behind all other windows.
|
||||||
|
- The init process corresponding to that session (`/usr/bin/init --user`). This process does the same thing as `init` above (manages services), but runs with user privileges and reads configuration files from `/etc/user` instead (in the future this will be changed to a user-specific directory).
|
||||||
|
|
||||||
|
Currently, `init --user` only does one thing: it opens up a text editor with a welcome message on startup. It can be configured to do whatever the user desires to do on startup, by placing the appropriate configuration files in `/etc/user`.
|
||||||
|
|
||||||
|
### File system and process layout
|
||||||
|
|
||||||
|
After the startui stage of the boot process, the system is fully started up and looks like this:
|
||||||
|
|
||||||
|
#### File system
|
||||||
|
```
|
||||||
|
/ - ext2 root partition
|
||||||
|
/dev - device file system
|
||||||
|
/dev/shm - POSIX shared memory file system
|
||||||
|
/dev/pts - POSIX pseudoterminal file system
|
||||||
|
/tmp - system temporary file directory
|
||||||
|
/usr, /etc, /home... - other directories contained in the root partition
|
||||||
|
/home/selene - temporary home directory
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Processes
|
||||||
|
```
|
||||||
|
/usr/bin/init - PID 1
|
||||||
|
[x86_64-io] - PID 3
|
||||||
|
[reap] - PID 4
|
||||||
|
[oom] - PID 5
|
||||||
|
/usr/bin/startui - PID 13
|
||||||
|
/usr/bin/wind - PID 14
|
||||||
|
/usr/bin/execd - PID 15
|
||||||
|
/usr/bin/taskbar - PID 16
|
||||||
|
/usr/bin/init --user - PID 17
|
||||||
|
/usr/bin/editor welcome - PID 18
|
35
docs/dependencies.md
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# Dependencies required to build and run Luna
|
||||||
|
|
||||||
|
## System requirements
|
||||||
|
|
||||||
|
Any modern UNIX-like system that supports all the tools listed below should work (Hopefully, that will include Luna itself in the future!).
|
||||||
|
|
||||||
|
I personally build and run Luna on an amd64 Fedora Linux 40 machine. CI runs on arm64 Ubuntu 22.04. Any other configurations are untested. Windows is not supported, although you can try using WSL if you really want to.
|
||||||
|
|
||||||
|
## Building a cross-compiler toolchain
|
||||||
|
For this, you should start by installing the [required dependencies](https://wiki.osdev.org/GCC_Cross_Compiler#Installing_Dependencies) for any OSdev cross-compiler build.
|
||||||
|
|
||||||
|
Also make sure you have the perl module `File::Compare` installed, it is required to build autoconf. On Fedora you can install it using the package manager by running `# dnf install perl-File-Compare`. If your distro doesn't have it, you might have to install it via `cpan`.
|
||||||
|
|
||||||
|
## Building the actual system
|
||||||
|
The build process needs some extra dependencies to run: `cmake`, `ninja`, `nasm`, `fakeroot` and `genext2fs`. On some distributions the `ninja` package is called `ninja-build` instead.
|
||||||
|
|
||||||
|
If you want to use `make` instead of `ninja`, create a file called `env-local.sh` in the project root and add the line `USE_MAKE=1`. In this case, ninja does not need to be installed.
|
||||||
|
|
||||||
|
## Running the built image in a virtual machine
|
||||||
|
The script provided by the project to run the system, `tools/run.sh`, assumes that QEMU is installed and uses that to run the image. Therefore, make sure your system has `qemu-system-x86_64` in the PATH. If it doesn't, install it using the method appropriate for your system, usually installing `qemu` or `qemu-system` from the package manager.
|
||||||
|
|
||||||
|
That being said, there's no requirement to use QEMU. If you want to use a different virtualization program, such as Oracle VirtualBox or VMWare, just use `tools/build-iso.sh` instead of `run.sh` and use the built `Luna.iso` in those programs.
|
||||||
|
|
||||||
|
## Formatting/linting
|
||||||
|
|
||||||
|
Please make sure you have `clang-format` installed. Additionally, if your editor does not support format-on-save or you do not have it configured, please run `tools/run-clang-format.sh` before committing, to make sure all code follows the same style conventions.
|
||||||
|
|
||||||
|
## Source dependencies
|
||||||
|
TLDR: Luna does not depend on any third-party library.
|
||||||
|
|
||||||
|
Every part of Luna is written from scratch and depends only on its own libraries and programs, with two small exceptions (included here for crediting and licensing purposes, but there is no need to download and build them separately):
|
||||||
|
|
||||||
|
The bootloader, BOOTBOOT. It is available at [gitlab.com/bztsrc/bootboot](https://gitlab.com/bztsrc/bootboot), under the MIT license. It is automatically pulled and built from source by `tools/setup.sh`.
|
||||||
|
|
||||||
|
[libc/src/strtod.cpp](../libc/src/strtod.cpp). Written by Yasuhiro Matsumoto, adapted from https://gist.github.com/mattn/1890186 and available under a public domain license.
|
BIN
docs/screenshots/screenshot-0.6.0.png
Normal file
After Width: | Height: | Size: 16 KiB |
17
gui/CMakeLists.txt
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
function(luna_service SOURCE_FILE APP_NAME)
|
||||||
|
add_executable(${APP_NAME} ${SOURCE_FILE})
|
||||||
|
target_compile_options(${APP_NAME} PRIVATE -Os ${COMMON_FLAGS} -Wno-write-strings)
|
||||||
|
add_dependencies(${APP_NAME} libc)
|
||||||
|
target_include_directories(${APP_NAME} PRIVATE ${LUNA_BASE}/usr/include)
|
||||||
|
target_link_libraries(${APP_NAME} PRIVATE os)
|
||||||
|
install(TARGETS ${APP_NAME} DESTINATION ${LUNA_BASE}/usr/bin)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
add_subdirectory(libui)
|
||||||
|
add_subdirectory(wind)
|
||||||
|
add_subdirectory(apps)
|
||||||
|
|
||||||
|
luna_service(execd.cpp execd)
|
||||||
|
luna_service(run.cpp run)
|
||||||
|
luna_service(loginui.cpp loginui)
|
||||||
|
target_link_libraries(loginui PRIVATE ui)
|
364
gui/apps/2048.cpp
Normal file
@ -0,0 +1,364 @@
|
|||||||
|
#include <luna/RefString.h>
|
||||||
|
#include <luna/Utf8.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <ui/Alignment.h>
|
||||||
|
#include <ui/App.h>
|
||||||
|
#include <ui/Font.h>
|
||||||
|
#include <ui/Layout.h>
|
||||||
|
|
||||||
|
static ui::Color colors[] = {
|
||||||
|
ui::Color::from_rgb(255, 255, 0), ui::Color::from_rgb(255, 230, 0), ui::Color::from_rgb(255, 210, 0),
|
||||||
|
ui::Color::from_rgb(255, 190, 0), ui::Color::from_rgb(255, 170, 0), ui::Color::from_rgb(255, 150, 0),
|
||||||
|
ui::Color::from_rgb(255, 130, 0), ui::Color::from_rgb(255, 110, 0), ui::Color::from_rgb(255, 90, 0),
|
||||||
|
ui::Color::from_rgb(255, 70, 0), ui::Color::from_rgb(255, 50, 0),
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Tile
|
||||||
|
{
|
||||||
|
int number { 0 };
|
||||||
|
int color { 0 };
|
||||||
|
};
|
||||||
|
|
||||||
|
class GameWidget final : public ui::Widget
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static constexpr int MARGIN = 5;
|
||||||
|
|
||||||
|
Result<void> draw(ui::Canvas& canvas) override
|
||||||
|
{
|
||||||
|
int width = m_rect.width / 4;
|
||||||
|
int height = m_rect.height / 4;
|
||||||
|
|
||||||
|
for (int i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
for (int j = 0; j < 4; j++)
|
||||||
|
{
|
||||||
|
auto subcanvas = canvas.subcanvas(
|
||||||
|
ui::Rect { width * j + MARGIN, height * i + MARGIN, width - MARGIN, height - MARGIN });
|
||||||
|
int index = i * 4 + j;
|
||||||
|
TRY(draw_tile(index, subcanvas));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<ui::EventResult> handle_key_event(const ui::KeyEventRequest& request) override
|
||||||
|
{
|
||||||
|
if (!request.pressed) return ui::EventResult::DidNotHandle;
|
||||||
|
|
||||||
|
bool should_add_tile = false;
|
||||||
|
|
||||||
|
switch (request.code)
|
||||||
|
{
|
||||||
|
case moon::K_UpArrow: {
|
||||||
|
bool changed;
|
||||||
|
changed = move_up();
|
||||||
|
if (changed) should_add_tile = true;
|
||||||
|
join_up();
|
||||||
|
changed = move_up();
|
||||||
|
if (changed) should_add_tile = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case moon::K_LeftArrow: {
|
||||||
|
bool changed;
|
||||||
|
changed = move_left();
|
||||||
|
if (changed) should_add_tile = true;
|
||||||
|
join_left();
|
||||||
|
changed = move_left();
|
||||||
|
if (changed) should_add_tile = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case moon::K_DownArrow: {
|
||||||
|
bool changed;
|
||||||
|
changed = move_down();
|
||||||
|
if (changed) should_add_tile = true;
|
||||||
|
join_down();
|
||||||
|
changed = move_down();
|
||||||
|
if (changed) should_add_tile = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case moon::K_RightArrow: {
|
||||||
|
bool changed;
|
||||||
|
changed = move_right();
|
||||||
|
if (changed) should_add_tile = true;
|
||||||
|
join_right();
|
||||||
|
changed = move_right();
|
||||||
|
if (changed) should_add_tile = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case moon::K_Home: {
|
||||||
|
reset();
|
||||||
|
return ui::EventResult::DidHandle;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default: return ui::EventResult::DidNotHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (should_add_tile) add_tile();
|
||||||
|
|
||||||
|
return ui::EventResult::DidHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool move_left()
|
||||||
|
{
|
||||||
|
Tile new_tiles[16];
|
||||||
|
|
||||||
|
bool changed = false;
|
||||||
|
|
||||||
|
for (int i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
int pos = 0;
|
||||||
|
|
||||||
|
for (int j = 0; j < 4; j++)
|
||||||
|
{
|
||||||
|
if (tiles[i * 4 + j].number != 0)
|
||||||
|
{
|
||||||
|
new_tiles[i * 4 + pos] = tiles[i * 4 + j];
|
||||||
|
pos += 1;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(tiles, new_tiles, sizeof(tiles));
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void join_left()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
for (int j = 0; j < 3; j++)
|
||||||
|
{
|
||||||
|
auto& from_tile = tiles[i * 4 + j];
|
||||||
|
auto& to_tile = tiles[i * 4 + j + 1];
|
||||||
|
if (from_tile.number != 0 && from_tile.number == to_tile.number)
|
||||||
|
{
|
||||||
|
from_tile.number *= 2;
|
||||||
|
from_tile.color += 1;
|
||||||
|
to_tile.number = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool move_right()
|
||||||
|
{
|
||||||
|
Tile new_tiles[16];
|
||||||
|
|
||||||
|
bool changed = false;
|
||||||
|
|
||||||
|
for (int i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
int pos = 3;
|
||||||
|
|
||||||
|
for (int j = 0; j < 4; j++)
|
||||||
|
{
|
||||||
|
if (tiles[i * 4 + j].number != 0)
|
||||||
|
{
|
||||||
|
new_tiles[i * 4 + pos] = tiles[i * 4 + j];
|
||||||
|
pos -= 1;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(tiles, new_tiles, sizeof(tiles));
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void join_right()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
for (int j = 1; j < 4; j++)
|
||||||
|
{
|
||||||
|
auto& from_tile = tiles[i * 4 + j];
|
||||||
|
auto& to_tile = tiles[i * 4 + j - 1];
|
||||||
|
if (from_tile.number != 0 && from_tile.number == to_tile.number)
|
||||||
|
{
|
||||||
|
from_tile.number *= 2;
|
||||||
|
from_tile.color += 1;
|
||||||
|
to_tile.number = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool move_up()
|
||||||
|
{
|
||||||
|
Tile new_tiles[16];
|
||||||
|
|
||||||
|
bool changed = false;
|
||||||
|
|
||||||
|
for (int j = 0; j < 4; j++)
|
||||||
|
{
|
||||||
|
int pos = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
if (tiles[i * 4 + j].number != 0)
|
||||||
|
{
|
||||||
|
new_tiles[pos * 4 + j] = tiles[i * 4 + j];
|
||||||
|
pos += 1;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(tiles, new_tiles, sizeof(tiles));
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void join_up()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
for (int j = 0; j < 4; j++)
|
||||||
|
{
|
||||||
|
auto& from_tile = tiles[i * 4 + j];
|
||||||
|
auto& to_tile = tiles[i * 4 + j + 4];
|
||||||
|
if (from_tile.number != 0 && from_tile.number == to_tile.number)
|
||||||
|
{
|
||||||
|
from_tile.number *= 2;
|
||||||
|
from_tile.color += 1;
|
||||||
|
to_tile.number = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool move_down()
|
||||||
|
{
|
||||||
|
Tile new_tiles[16];
|
||||||
|
|
||||||
|
bool changed = false;
|
||||||
|
|
||||||
|
for (int j = 0; j < 4; j++)
|
||||||
|
{
|
||||||
|
int pos = 3;
|
||||||
|
|
||||||
|
for (int i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
if (tiles[i * 4 + j].number != 0)
|
||||||
|
{
|
||||||
|
new_tiles[pos * 4 + j] = tiles[i * 4 + j];
|
||||||
|
pos -= 1;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(tiles, new_tiles, sizeof(tiles));
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void join_down()
|
||||||
|
{
|
||||||
|
for (int i = 1; i < 4; i++)
|
||||||
|
{
|
||||||
|
for (int j = 0; j < 4; j++)
|
||||||
|
{
|
||||||
|
auto& from_tile = tiles[i * 4 + j];
|
||||||
|
auto& to_tile = tiles[i * 4 + j - 4];
|
||||||
|
if (from_tile.number != 0 && from_tile.number == to_tile.number)
|
||||||
|
{
|
||||||
|
from_tile.number *= 2;
|
||||||
|
from_tile.color += 1;
|
||||||
|
to_tile.number = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void add_tile()
|
||||||
|
{
|
||||||
|
bool can_add_tile = false;
|
||||||
|
for (int i = 0; i < 16; i++)
|
||||||
|
{
|
||||||
|
if (tiles[i].number == 0)
|
||||||
|
{
|
||||||
|
can_add_tile = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!can_add_tile)
|
||||||
|
{
|
||||||
|
reset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int start;
|
||||||
|
do {
|
||||||
|
start = rand() % 16;
|
||||||
|
} while (tiles[start].number != 0);
|
||||||
|
tiles[start].number = 2;
|
||||||
|
tiles[start].color = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 16; i++)
|
||||||
|
{
|
||||||
|
tiles[i].number = 0;
|
||||||
|
tiles[i].color = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
add_tile();
|
||||||
|
}
|
||||||
|
|
||||||
|
Tile tiles[16];
|
||||||
|
|
||||||
|
private:
|
||||||
|
Result<void> draw_tile(int index, ui::Canvas& canvas)
|
||||||
|
{
|
||||||
|
auto tile = tiles[index];
|
||||||
|
|
||||||
|
if (tile.number == 0)
|
||||||
|
{
|
||||||
|
canvas.fill(ui::GRAY);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.fill(colors[tile.color]);
|
||||||
|
|
||||||
|
auto fmt = TRY(RefString::format("%d"_sv, tile.number));
|
||||||
|
|
||||||
|
auto font = ui::Font::default_bold_font();
|
||||||
|
auto rect = ui::align({ 0, 0, canvas.width, canvas.height },
|
||||||
|
{ 0, 0, (int)fmt.length() * font->width(), font->height() },
|
||||||
|
ui::VerticalAlignment::Center, ui::HorizontalAlignment::Center);
|
||||||
|
auto subcanvas = canvas.subcanvas(rect);
|
||||||
|
|
||||||
|
Utf8StringDecoder decoder(fmt.chars());
|
||||||
|
wchar_t buf[4096];
|
||||||
|
TRY(decoder.decode(buf, sizeof(buf)));
|
||||||
|
|
||||||
|
font->render(buf, ui::BLACK, subcanvas);
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Result<int> luna_main(int, char**)
|
||||||
|
{
|
||||||
|
srand((unsigned)time(NULL));
|
||||||
|
|
||||||
|
ui::App app;
|
||||||
|
TRY(app.init());
|
||||||
|
|
||||||
|
auto* window = TRY(ui::Window::create(ui::Rect { 300, 300, 400, 400 }));
|
||||||
|
app.set_main_window(window);
|
||||||
|
|
||||||
|
window->set_background(ui::BLACK);
|
||||||
|
window->set_title("2048");
|
||||||
|
|
||||||
|
GameWidget game;
|
||||||
|
window->set_main_widget(game);
|
||||||
|
game.reset();
|
||||||
|
|
||||||
|
return app.run();
|
||||||
|
}
|
17
gui/apps/CMakeLists.txt
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
function(luna_app SOURCE_FILE APP_NAME)
|
||||||
|
add_executable(${APP_NAME} ${SOURCE_FILE})
|
||||||
|
target_compile_options(${APP_NAME} PRIVATE -Os ${COMMON_FLAGS} -Wno-write-strings)
|
||||||
|
add_dependencies(${APP_NAME} libc)
|
||||||
|
target_include_directories(${APP_NAME} PRIVATE ${LUNA_BASE}/usr/include)
|
||||||
|
target_link_libraries(${APP_NAME} PRIVATE os ui)
|
||||||
|
install(TARGETS ${APP_NAME} DESTINATION ${LUNA_BASE}/usr/bin)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
luna_app(about.cpp about)
|
||||||
|
luna_app(taskbar.cpp taskbar)
|
||||||
|
luna_app(2048.cpp 2048)
|
||||||
|
luna_app(clock.cpp clock)
|
||||||
|
luna_app(gol.cpp gol)
|
||||||
|
|
||||||
|
add_subdirectory(editor)
|
||||||
|
add_subdirectory(terminal)
|
47
gui/apps/about.cpp
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
#include <luna/String.h>
|
||||||
|
#include <sys/utsname.h>
|
||||||
|
#include <ui/App.h>
|
||||||
|
#include <ui/Button.h>
|
||||||
|
#include <ui/Label.h>
|
||||||
|
#include <ui/Layout.h>
|
||||||
|
|
||||||
|
static constexpr ui::Color BACKGROUND_COLOR = ui::Color::from_rgb(89, 89, 89);
|
||||||
|
|
||||||
|
Result<int> luna_main(int, char**)
|
||||||
|
{
|
||||||
|
ui::App app;
|
||||||
|
TRY(app.init());
|
||||||
|
|
||||||
|
auto* window = TRY(ui::Window::create(ui::Rect { 300, 300, 400, 300 }));
|
||||||
|
app.set_main_window(window);
|
||||||
|
|
||||||
|
window->set_title("About");
|
||||||
|
window->set_background(BACKGROUND_COLOR);
|
||||||
|
|
||||||
|
utsname info;
|
||||||
|
uname(&info);
|
||||||
|
|
||||||
|
ui::VerticalLayout main_layout;
|
||||||
|
window->set_main_widget(main_layout);
|
||||||
|
|
||||||
|
ui::Label title("About Luna");
|
||||||
|
title.set_font(ui::Font::default_bold_font());
|
||||||
|
|
||||||
|
main_layout.add_widget(title);
|
||||||
|
|
||||||
|
ui::VerticalLayout version_info;
|
||||||
|
main_layout.add_widget(version_info);
|
||||||
|
|
||||||
|
ui::Label license("Licensed under the BSD-2-Clause license.");
|
||||||
|
main_layout.add_widget(license);
|
||||||
|
|
||||||
|
String os_release_text = TRY(String::format("OS release: %s"_sv, info.release));
|
||||||
|
ui::Label os_release(os_release_text.view());
|
||||||
|
version_info.add_widget(os_release);
|
||||||
|
|
||||||
|
String kernel_version_text = TRY(String::format("Kernel version: %s"_sv, info.version));
|
||||||
|
ui::Label kernel_version(kernel_version_text.view());
|
||||||
|
version_info.add_widget(kernel_version);
|
||||||
|
|
||||||
|
return app.run();
|
||||||
|
}
|
43
gui/apps/clock.cpp
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
#include <os/Timer.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <ui/App.h>
|
||||||
|
#include <ui/Label.h>
|
||||||
|
|
||||||
|
ui::Label* g_label;
|
||||||
|
|
||||||
|
void update_time()
|
||||||
|
{
|
||||||
|
time_t t = time(NULL);
|
||||||
|
struct tm* tp = localtime(&t);
|
||||||
|
|
||||||
|
static char buf[2048];
|
||||||
|
strftime(buf, sizeof(buf), "%H:%M:%S", tp);
|
||||||
|
|
||||||
|
g_label->set_text(StringView { buf });
|
||||||
|
|
||||||
|
ui::App::the().main_window()->draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<int> luna_main(int, char**)
|
||||||
|
{
|
||||||
|
ui::App app;
|
||||||
|
TRY(app.init());
|
||||||
|
|
||||||
|
auto* window = TRY(ui::Window::create(ui::Rect { 500, 400, 100, 50 }));
|
||||||
|
app.set_main_window(window);
|
||||||
|
|
||||||
|
window->set_title("Clock");
|
||||||
|
window->set_background(ui::GRAY);
|
||||||
|
|
||||||
|
g_label = TRY(make<ui::Label>("00:00:00"));
|
||||||
|
g_label->set_font(ui::Font::default_bold_font());
|
||||||
|
g_label->set_color(ui::BLACK);
|
||||||
|
|
||||||
|
window->set_main_widget(*g_label);
|
||||||
|
|
||||||
|
update_time();
|
||||||
|
|
||||||
|
auto timer = TRY(os::Timer::create_repeating(1000, update_time));
|
||||||
|
|
||||||
|
return app.run();
|
||||||
|
}
|
12
gui/apps/editor/CMakeLists.txt
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
set(SOURCES
|
||||||
|
main.cpp
|
||||||
|
EditorWidget.h
|
||||||
|
EditorWidget.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
add_executable(editor ${SOURCES})
|
||||||
|
target_compile_options(editor PRIVATE -Os ${COMMON_FLAGS} -Wno-write-strings)
|
||||||
|
add_dependencies(editor libc)
|
||||||
|
target_include_directories(editor PRIVATE ${LUNA_BASE}/usr/include ${CMAKE_CURRENT_LIST_DIR})
|
||||||
|
target_link_libraries(editor PRIVATE os ui)
|
||||||
|
install(TARGETS editor DESTINATION ${LUNA_BASE}/usr/bin)
|
259
gui/apps/editor/EditorWidget.cpp
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
/**
|
||||||
|
* @file EditorWidget.cpp
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief Multiline text editing widget.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2024, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "EditorWidget.h"
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <luna/PathParser.h>
|
||||||
|
#include <luna/RefString.h>
|
||||||
|
#include <luna/Utf8.h>
|
||||||
|
#include <os/File.h>
|
||||||
|
#include <os/FileSystem.h>
|
||||||
|
#include <ui/App.h>
|
||||||
|
#include <ui/Dialog.h>
|
||||||
|
|
||||||
|
EditorWidget::EditorWidget(SharedPtr<ui::Font> font) : ui::TextInput(), m_font(font)
|
||||||
|
{
|
||||||
|
recalculate_lines();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void> EditorWidget::load_file(const os::Path& path)
|
||||||
|
{
|
||||||
|
struct stat st;
|
||||||
|
auto rc = os::FileSystem::stat(path, st, true);
|
||||||
|
|
||||||
|
if (!rc.has_error() && !S_ISREG(st.st_mode))
|
||||||
|
{
|
||||||
|
auto message = TRY(RefString::format("%s is not a regular file", path.name().chars()));
|
||||||
|
ui::Dialog::show_message("Error", message.view());
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
os::eprintln("Loading file: %s", path.name().chars());
|
||||||
|
|
||||||
|
auto file = TRY(os::File::open_or_create(path, os::File::ReadOnly));
|
||||||
|
|
||||||
|
m_data = TRY(file->read_all());
|
||||||
|
|
||||||
|
os::eprintln("Read %zu bytes.", m_data.size());
|
||||||
|
|
||||||
|
m_cursor = m_data.size();
|
||||||
|
|
||||||
|
m_path = TRY(String::from_string_view(path.name()));
|
||||||
|
|
||||||
|
auto basename = TRY(PathParser::basename(m_path.view()));
|
||||||
|
|
||||||
|
String title = TRY(String::format("Text Editor - %s"_sv, basename.chars()));
|
||||||
|
window()->set_title(title.view());
|
||||||
|
|
||||||
|
TRY(recalculate_lines());
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<ui::EventResult> EditorWidget::handle_key_event(const ui::KeyEventRequest& request)
|
||||||
|
{
|
||||||
|
// Avoid handling "key released" events
|
||||||
|
if (!request.pressed) return ui::EventResult::DidNotHandle;
|
||||||
|
|
||||||
|
if (request.code == moon::K_UpArrow)
|
||||||
|
{
|
||||||
|
if (m_cursor_position.y > 0) m_cursor_position.y--;
|
||||||
|
else
|
||||||
|
return ui::EventResult::DidNotHandle;
|
||||||
|
recalculate_cursor_index();
|
||||||
|
update_cursor();
|
||||||
|
return ui::EventResult::DidHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.code == moon::K_DownArrow)
|
||||||
|
{
|
||||||
|
if (m_cursor_position.y + 1 < (int)m_lines.size()) m_cursor_position.y++;
|
||||||
|
else
|
||||||
|
return ui::EventResult::DidNotHandle;
|
||||||
|
recalculate_cursor_index();
|
||||||
|
update_cursor();
|
||||||
|
return ui::EventResult::DidHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.code == moon::K_LeftArrow)
|
||||||
|
{
|
||||||
|
if (m_cursor > 0) m_cursor--;
|
||||||
|
else
|
||||||
|
return ui::EventResult::DidNotHandle;
|
||||||
|
recalculate_cursor_position();
|
||||||
|
update_cursor();
|
||||||
|
return ui::EventResult::DidHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.code == moon::K_RightArrow)
|
||||||
|
{
|
||||||
|
if (m_cursor < m_data.size()) m_cursor++;
|
||||||
|
else
|
||||||
|
return ui::EventResult::DidNotHandle;
|
||||||
|
recalculate_cursor_position();
|
||||||
|
update_cursor();
|
||||||
|
return ui::EventResult::DidHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.code == moon::K_Backspace)
|
||||||
|
{
|
||||||
|
if (m_cursor == 0) return ui::EventResult::DidNotHandle;
|
||||||
|
m_cursor--;
|
||||||
|
|
||||||
|
delete_current_character();
|
||||||
|
|
||||||
|
TRY(recalculate_lines());
|
||||||
|
|
||||||
|
update_cursor();
|
||||||
|
|
||||||
|
return ui::EventResult::DidHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.letter != '\n' && iscntrl(request.letter)) return ui::EventResult::DidNotHandle;
|
||||||
|
|
||||||
|
if (m_cursor == m_data.size()) TRY(m_data.append_data((const u8*)&request.letter, 1));
|
||||||
|
else
|
||||||
|
TRY(insert_character(request.letter));
|
||||||
|
|
||||||
|
m_cursor++;
|
||||||
|
TRY(recalculate_lines());
|
||||||
|
|
||||||
|
update_cursor();
|
||||||
|
|
||||||
|
return ui::EventResult::DidHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void> EditorWidget::save_file_as()
|
||||||
|
{
|
||||||
|
ui::Dialog::show_input_dialog(
|
||||||
|
"Save file as...", "Please enter the path to save this file to:", [this](StringView path) {
|
||||||
|
m_path = String::from_string_view(path).release_value();
|
||||||
|
auto rc = save_file();
|
||||||
|
if (rc.has_error())
|
||||||
|
{
|
||||||
|
os::eprintln("Failed to save file %s: %s", m_path.chars(), rc.error_string());
|
||||||
|
ui::Dialog::show_message("Error", "Failed to save file");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
auto basename = PathParser::basename(m_path.view()).release_value();
|
||||||
|
|
||||||
|
String title = String::format("Text Editor - %s"_sv, basename.chars()).release_value();
|
||||||
|
window()->set_title(title.view());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void> EditorWidget::save_file()
|
||||||
|
{
|
||||||
|
if (m_path.is_empty())
|
||||||
|
{
|
||||||
|
TRY(save_file_as());
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto file = TRY(os::File::open_or_create(m_path.view(), os::File::WriteOnly));
|
||||||
|
return file->write(m_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void> EditorWidget::draw(ui::Canvas& canvas)
|
||||||
|
{
|
||||||
|
int visible_lines = canvas.height / m_font->height();
|
||||||
|
int visible_columns = canvas.width / m_font->width();
|
||||||
|
|
||||||
|
if ((usize)visible_lines > m_lines.size()) visible_lines = static_cast<int>(m_lines.size());
|
||||||
|
|
||||||
|
for (int i = 0; i < visible_lines; i++)
|
||||||
|
{
|
||||||
|
auto line = m_lines[i];
|
||||||
|
if (line.begin == line.end) continue;
|
||||||
|
|
||||||
|
auto slice = TRY(m_data.slice(line.begin, line.end - line.begin));
|
||||||
|
auto string = TRY(
|
||||||
|
String::from_string_view(StringView::from_fixed_size_cstring((const char*)slice, line.end - line.begin)));
|
||||||
|
|
||||||
|
Utf8StringDecoder decoder(string.chars());
|
||||||
|
wchar_t buf[4096];
|
||||||
|
decoder.decode(buf, sizeof(buf)).release_value();
|
||||||
|
|
||||||
|
int characters_to_render = (int)wcslen(buf);
|
||||||
|
|
||||||
|
for (int j = 0; j < visible_columns && j < characters_to_render; j++)
|
||||||
|
{
|
||||||
|
auto subcanvas =
|
||||||
|
canvas.subcanvas({ j * m_font->width(), i * m_font->height(), m_font->width(), m_font->height() });
|
||||||
|
m_font->render(buf[j], ui::WHITE, subcanvas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the cursor
|
||||||
|
if (m_cursor_position.x < visible_columns && m_cursor_position.y < visible_lines && m_cursor_activated)
|
||||||
|
{
|
||||||
|
canvas
|
||||||
|
.subcanvas(
|
||||||
|
{ m_cursor_position.x * m_font->width(), m_cursor_position.y * m_font->height(), 1, m_font->height() })
|
||||||
|
.fill(ui::WHITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void> EditorWidget::recalculate_lines()
|
||||||
|
{
|
||||||
|
m_lines.clear();
|
||||||
|
|
||||||
|
Line l;
|
||||||
|
l.begin = 0;
|
||||||
|
for (usize i = 0; i < m_data.size(); i++)
|
||||||
|
{
|
||||||
|
if (m_data.data()[i] == '\n')
|
||||||
|
{
|
||||||
|
l.end = i;
|
||||||
|
TRY(m_lines.try_append(l));
|
||||||
|
l.begin = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
l.end = m_data.size();
|
||||||
|
TRY(m_lines.try_append(l));
|
||||||
|
|
||||||
|
recalculate_cursor_position();
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void EditorWidget::recalculate_cursor_position()
|
||||||
|
{
|
||||||
|
if (m_cursor == 0) m_cursor_position = { 0, 0 };
|
||||||
|
|
||||||
|
for (int i = 0; i < (int)m_lines.size(); i++)
|
||||||
|
{
|
||||||
|
auto line = m_lines[i];
|
||||||
|
if (m_cursor >= line.begin && m_cursor <= line.end)
|
||||||
|
{
|
||||||
|
m_cursor_position.x = (int)(m_cursor - line.begin);
|
||||||
|
m_cursor_position.y = i;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unreachable();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EditorWidget::recalculate_cursor_index()
|
||||||
|
{
|
||||||
|
m_cursor = m_lines[m_cursor_position.y].begin + m_cursor_position.x;
|
||||||
|
if (m_cursor > m_lines[m_cursor_position.y].end)
|
||||||
|
{
|
||||||
|
m_cursor = m_lines[m_cursor_position.y].end;
|
||||||
|
recalculate_cursor_position();
|
||||||
|
}
|
||||||
|
}
|
50
gui/apps/editor/EditorWidget.h
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* @file EditorWidget.h
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief Multiline text editing widget.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2024, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <luna/String.h>
|
||||||
|
#include <os/Timer.h>
|
||||||
|
#include <ui/Font.h>
|
||||||
|
#include <ui/TextInput.h>
|
||||||
|
#include <ui/Widget.h>
|
||||||
|
|
||||||
|
class EditorWidget : public ui::TextInput
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
EditorWidget(SharedPtr<ui::Font> font);
|
||||||
|
|
||||||
|
Result<void> load_file(const os::Path& path);
|
||||||
|
|
||||||
|
Result<void> save_file();
|
||||||
|
Result<void> save_file_as();
|
||||||
|
|
||||||
|
Result<ui::EventResult> handle_key_event(const ui::KeyEventRequest& request) override;
|
||||||
|
|
||||||
|
Result<void> draw(ui::Canvas& canvas) override;
|
||||||
|
|
||||||
|
os::Path path()
|
||||||
|
{
|
||||||
|
return m_path.view();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
SharedPtr<ui::Font> m_font;
|
||||||
|
|
||||||
|
struct Line
|
||||||
|
{
|
||||||
|
usize begin;
|
||||||
|
usize end;
|
||||||
|
};
|
||||||
|
Vector<Line> m_lines;
|
||||||
|
|
||||||
|
String m_path;
|
||||||
|
|
||||||
|
Result<void> recalculate_lines();
|
||||||
|
void recalculate_cursor_position();
|
||||||
|
void recalculate_cursor_index();
|
||||||
|
};
|
53
gui/apps/editor/main.cpp
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* @file main.cpp
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief Graphical text editor.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2024, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "EditorWidget.h"
|
||||||
|
#include <os/ArgumentParser.h>
|
||||||
|
#include <os/File.h>
|
||||||
|
#include <ui/App.h>
|
||||||
|
#include <ui/Dialog.h>
|
||||||
|
|
||||||
|
Result<int> luna_main(int argc, char** argv)
|
||||||
|
{
|
||||||
|
StringView path;
|
||||||
|
|
||||||
|
os::ArgumentParser parser;
|
||||||
|
parser.add_description("A graphical text editor"_sv);
|
||||||
|
parser.add_system_program_info("editor"_sv);
|
||||||
|
parser.add_positional_argument(path, "path", false);
|
||||||
|
parser.parse(argc, argv);
|
||||||
|
|
||||||
|
ui::App app;
|
||||||
|
TRY(app.init());
|
||||||
|
|
||||||
|
auto* window = TRY(ui::Window::create(ui::Rect { 200, 300, 600, 600 }));
|
||||||
|
window->set_background(ui::Color::from_rgb(40, 40, 40));
|
||||||
|
window->set_title("Text Editor");
|
||||||
|
app.set_main_window(window);
|
||||||
|
|
||||||
|
auto* editor = TRY(make<EditorWidget>(ui::Font::default_font()));
|
||||||
|
window->set_main_widget(*editor);
|
||||||
|
if (!path.is_empty()) editor->load_file(path);
|
||||||
|
|
||||||
|
TRY(window->add_keyboard_shortcut({ moon::K_CH26, ui::Mod_Ctrl }, true, [&](ui::Shortcut) {
|
||||||
|
auto result = editor->save_file();
|
||||||
|
if (result.has_error())
|
||||||
|
{
|
||||||
|
os::eprintln("Failed to save file %s: %s", editor->path().name().chars(), result.error_string());
|
||||||
|
ui::Dialog::show_message("Error", "Failed to save file");
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
TRY(window->add_keyboard_shortcut({ moon::K_CH26, ui::Mod_Ctrl | ui::Mod_Shift }, true,
|
||||||
|
[&](ui::Shortcut) { editor->save_file_as(); }));
|
||||||
|
|
||||||
|
window->draw();
|
||||||
|
|
||||||
|
return app.run();
|
||||||
|
}
|
127
gui/apps/gol.cpp
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
#include <assert.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <luna/Heap.h>
|
||||||
|
#include <os/ArgumentParser.h>
|
||||||
|
#include <os/Timer.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <ui/App.h>
|
||||||
|
#include <ui/Window.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
struct Cell
|
||||||
|
{
|
||||||
|
bool state;
|
||||||
|
bool new_state;
|
||||||
|
};
|
||||||
|
|
||||||
|
static int g_num_rows = 40;
|
||||||
|
static int g_num_columns = 60;
|
||||||
|
|
||||||
|
static Cell* g_cells;
|
||||||
|
|
||||||
|
static ui::Window* g_window;
|
||||||
|
|
||||||
|
static Result<void> fill_cells()
|
||||||
|
{
|
||||||
|
g_cells = (Cell*)TRY(calloc_impl(g_num_rows, g_num_columns * sizeof(Cell), false));
|
||||||
|
|
||||||
|
for (isize i = 0; i < (g_num_rows * g_num_columns); i++)
|
||||||
|
{
|
||||||
|
auto value = rand() % 2;
|
||||||
|
g_cells[i].state = g_cells[i].new_state = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
static Cell& find_cell(int row, int column)
|
||||||
|
{
|
||||||
|
assert(row < g_num_rows);
|
||||||
|
assert(column < g_num_columns);
|
||||||
|
return g_cells[row * g_num_columns + column];
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr int BYTES_PER_PIXEL = sizeof(u32);
|
||||||
|
static constexpr ui::Color activated_cell_color = ui::CYAN;
|
||||||
|
static constexpr ui::Color deactivated_cell_color = ui::Color::from_rgb(40, 40, 40);
|
||||||
|
|
||||||
|
static void draw_cells()
|
||||||
|
{
|
||||||
|
const int CELL_WIDTH = g_window->canvas().width / g_num_columns;
|
||||||
|
const int CELL_HEIGHT = g_window->canvas().height / g_num_rows;
|
||||||
|
|
||||||
|
auto canvas = g_window->canvas();
|
||||||
|
|
||||||
|
for (int i = 0; i < g_num_rows; i++)
|
||||||
|
{
|
||||||
|
for (int j = 0; j < g_num_columns; j++)
|
||||||
|
{
|
||||||
|
auto subcanvas = canvas.subcanvas(ui::Rect { j * CELL_WIDTH, i * CELL_HEIGHT, CELL_WIDTH, CELL_HEIGHT });
|
||||||
|
|
||||||
|
auto& cell = find_cell(i, j);
|
||||||
|
ui::Color color = cell.state ? activated_cell_color : deactivated_cell_color;
|
||||||
|
subcanvas.fill(color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g_window->update();
|
||||||
|
}
|
||||||
|
|
||||||
|
static int find_neighbors(int row, int column)
|
||||||
|
{
|
||||||
|
int sum = 0;
|
||||||
|
|
||||||
|
if (row > 0 && column > 0) sum += find_cell(row - 1, column - 1).state;
|
||||||
|
if (row > 0) sum += find_cell(row - 1, column).state;
|
||||||
|
if (row > 0 && (column + 1) < g_num_columns) sum += find_cell(row - 1, column + 1).state;
|
||||||
|
if (column > 0) sum += find_cell(row, column - 1).state;
|
||||||
|
if ((column + 1) < g_num_columns) sum += find_cell(row, column + 1).state;
|
||||||
|
if ((row + 1) < g_num_rows && column > 0) sum += find_cell(row + 1, column - 1).state;
|
||||||
|
if ((row + 1) < g_num_rows) sum += find_cell(row + 1, column).state;
|
||||||
|
if ((row + 1) < g_num_rows && (column + 1) < g_num_columns) sum += find_cell(row + 1, column + 1).state;
|
||||||
|
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void next_generation()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < g_num_rows; i++)
|
||||||
|
{
|
||||||
|
for (int j = 0; j < g_num_columns; j++)
|
||||||
|
{
|
||||||
|
auto& cell = find_cell(i, j);
|
||||||
|
int neighbors = find_neighbors(i, j);
|
||||||
|
if (!cell.state && neighbors == 3) cell.new_state = true;
|
||||||
|
else if (cell.state && (neighbors < 2 || neighbors > 3))
|
||||||
|
cell.new_state = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (isize i = 0; i < (g_num_rows * g_num_columns); i++) g_cells[i].state = g_cells[i].new_state;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void update()
|
||||||
|
{
|
||||||
|
next_generation();
|
||||||
|
draw_cells();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<int> luna_main(int, char**)
|
||||||
|
{
|
||||||
|
ui::App app;
|
||||||
|
TRY(app.init());
|
||||||
|
|
||||||
|
g_window = TRY(ui::Window::create(ui::Rect { 200, 200, 600, 400 }));
|
||||||
|
g_window->set_title("Game of Life");
|
||||||
|
app.set_main_window(g_window);
|
||||||
|
|
||||||
|
TRY(fill_cells());
|
||||||
|
|
||||||
|
update();
|
||||||
|
|
||||||
|
auto timer = TRY(os::Timer::create_repeating(100, update));
|
||||||
|
|
||||||
|
return app.run();
|
||||||
|
}
|
152
gui/apps/taskbar.cpp
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
#include <luna/Sort.h>
|
||||||
|
#include <luna/StringBuilder.h>
|
||||||
|
#include <os/Config.h>
|
||||||
|
#include <os/Directory.h>
|
||||||
|
#include <os/File.h>
|
||||||
|
#include <os/FileSystem.h>
|
||||||
|
#include <os/IPC.h>
|
||||||
|
#include <os/Process.h>
|
||||||
|
#include <os/ipc/Launcher.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <ui/App.h>
|
||||||
|
#include <ui/Button.h>
|
||||||
|
#include <ui/Container.h>
|
||||||
|
#include <ui/Image.h>
|
||||||
|
#include <ui/Layout.h>
|
||||||
|
|
||||||
|
static constexpr ui::Color TASKBAR_COLOR = ui::Color::from_rgb(83, 83, 83);
|
||||||
|
|
||||||
|
static OwnedPtr<os::IPC::Client> launcher_client;
|
||||||
|
|
||||||
|
void sigquit_handler(int)
|
||||||
|
{
|
||||||
|
// Reload the taskbar by exec-ing the executable, resetting everything.
|
||||||
|
StringView args[] = { "/usr/bin/taskbar" };
|
||||||
|
os::Process::exec(args[0], { args, 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void> create_widget_group_for_app(ui::HorizontalLayout& layout, StringView path, StringView icon)
|
||||||
|
{
|
||||||
|
auto* button = TRY(make<ui::Button>(ui::Rect { 0, 0, 50, 50 }));
|
||||||
|
layout.add_widget(*button);
|
||||||
|
|
||||||
|
auto* container = TRY(
|
||||||
|
make<ui::Container>(ui::Rect { 0, 0, 50, 50 }, ui::VerticalAlignment::Center, ui::HorizontalAlignment::Center));
|
||||||
|
button->set_widget(*container);
|
||||||
|
button->set_action([=] {
|
||||||
|
os::Launcher::LaunchDetachedRequest request;
|
||||||
|
SET_IPC_STRING(request.command, path.chars());
|
||||||
|
launcher_client->send_async(request);
|
||||||
|
});
|
||||||
|
|
||||||
|
auto image = TRY(ui::ImageWidget::load(icon));
|
||||||
|
container->set_widget(*image);
|
||||||
|
|
||||||
|
image.leak();
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ApplicationFile
|
||||||
|
{
|
||||||
|
String name;
|
||||||
|
String command;
|
||||||
|
String icon;
|
||||||
|
};
|
||||||
|
|
||||||
|
Vector<ApplicationFile> s_app_files;
|
||||||
|
|
||||||
|
// Pretty much copied from init.cpp.
|
||||||
|
static Result<void> load_application_file(const os::Path& path)
|
||||||
|
{
|
||||||
|
os::println("[taskbar] reading app file: %s", path.name().chars());
|
||||||
|
|
||||||
|
auto file = TRY(os::ConfigFile::open(path));
|
||||||
|
|
||||||
|
ApplicationFile app_file;
|
||||||
|
|
||||||
|
app_file.name = TRY(String::from_string_view(file->read_string_or("Name", "")));
|
||||||
|
if (app_file.name.is_empty())
|
||||||
|
{
|
||||||
|
os::println("[taskbar] app file is missing 'Name' entry, aborting!");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
app_file.command = TRY(String::from_string_view(file->read_string_or("Command", "")));
|
||||||
|
if (app_file.command.is_empty())
|
||||||
|
{
|
||||||
|
os::println("[taskbar] app file is missing 'Command' entry, aborting!");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
app_file.icon = TRY(String::from_string_view(file->read_string_or("Icon", "")));
|
||||||
|
if (app_file.icon.is_empty())
|
||||||
|
{
|
||||||
|
os::println("[taskbar] app file is missing 'Icon' entry, aborting!");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
os::println("[taskbar] loaded app %s into memory", app_file.name.chars());
|
||||||
|
|
||||||
|
TRY(s_app_files.try_append(move(app_file)));
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
static Result<void> load_app_files_from_path(StringView path)
|
||||||
|
{
|
||||||
|
os::println("[taskbar] loading app files from %s", path.chars());
|
||||||
|
|
||||||
|
auto dir = TRY(os::Directory::open(path));
|
||||||
|
|
||||||
|
auto services = TRY(dir->list_names(os::Directory::Filter::ParentAndBase));
|
||||||
|
sort(services.begin(), services.end(), String::compare);
|
||||||
|
|
||||||
|
for (const auto& entry : services) TRY(load_application_file({ dir->fd(), entry.view() }));
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<int> luna_main(int, char**)
|
||||||
|
{
|
||||||
|
ui::App app;
|
||||||
|
TRY(app.init("/tmp/wsys.sock"));
|
||||||
|
app.pledge(ui::Pledge::ExtendedLayers);
|
||||||
|
|
||||||
|
TRY(os::EventLoop::the().register_signal_handler(SIGQUIT, sigquit_handler));
|
||||||
|
|
||||||
|
launcher_client = TRY(os::IPC::Client::connect("/tmp/execd.sock", false));
|
||||||
|
|
||||||
|
ui::Rect screen = app.screen_rect();
|
||||||
|
|
||||||
|
ui::Rect bar = ui::Rect { ui::Point { 0, screen.height - 50 }, screen.width, 50 };
|
||||||
|
|
||||||
|
auto window = TRY(ui::Window::create(bar, ui::WindowType::System));
|
||||||
|
app.set_main_window(window);
|
||||||
|
|
||||||
|
window->set_background(TASKBAR_COLOR);
|
||||||
|
window->set_layer(ui::Layer::Background);
|
||||||
|
app.pledge(0);
|
||||||
|
|
||||||
|
ui::HorizontalLayout layout(ui::Margins { 0, 0, 0, 0 }, ui::AdjustHeight::Yes, ui::AdjustWidth::No);
|
||||||
|
window->set_main_widget(layout);
|
||||||
|
|
||||||
|
load_app_files_from_path("/usr/share/applications/");
|
||||||
|
|
||||||
|
auto home = TRY(os::FileSystem::home_directory());
|
||||||
|
|
||||||
|
StringBuilder sb;
|
||||||
|
TRY(sb.add(home.view()));
|
||||||
|
TRY(sb.add("/.applications/"_sv));
|
||||||
|
auto local_app_file_dir = TRY(sb.string());
|
||||||
|
|
||||||
|
load_app_files_from_path(local_app_file_dir.view());
|
||||||
|
|
||||||
|
for (const auto& app_file : s_app_files)
|
||||||
|
{
|
||||||
|
create_widget_group_for_app(layout, app_file.command.view(), app_file.icon.view());
|
||||||
|
}
|
||||||
|
|
||||||
|
return app.run();
|
||||||
|
}
|
12
gui/apps/terminal/CMakeLists.txt
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
set(SOURCES
|
||||||
|
main.cpp
|
||||||
|
TerminalWidget.h
|
||||||
|
TerminalWidget.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
add_executable(terminal ${SOURCES})
|
||||||
|
target_compile_options(terminal PRIVATE -Os ${COMMON_FLAGS} -Wno-write-strings)
|
||||||
|
add_dependencies(terminal libc)
|
||||||
|
target_include_directories(terminal PRIVATE ${LUNA_BASE}/usr/include ${CMAKE_CURRENT_LIST_DIR})
|
||||||
|
target_link_libraries(terminal PRIVATE os ui)
|
||||||
|
install(TARGETS terminal DESTINATION ${LUNA_BASE}/usr/bin)
|
460
gui/apps/terminal/TerminalWidget.cpp
Normal file
@ -0,0 +1,460 @@
|
|||||||
|
#include "TerminalWidget.h"
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <luna/CType.h>
|
||||||
|
#include <os/File.h>
|
||||||
|
#include <os/Process.h>
|
||||||
|
#include <pty.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <sys/ioctl.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <ui/App.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
static constexpr auto RED = ui::Color::from_u32(0xffcd0000);
|
||||||
|
static constexpr auto GREEN = ui::Color::from_u32(0xff00cd00);
|
||||||
|
static constexpr auto YELLOW = ui::Color::from_u32(0xffcdcd00);
|
||||||
|
static constexpr auto BLUE = ui::Color::from_u32(0xff0000ee);
|
||||||
|
static constexpr auto MAGENTA = ui::Color::from_u32(0xffcd00cd);
|
||||||
|
static constexpr auto CYAN = ui::Color::from_u32(0xff00cdcd);
|
||||||
|
static constexpr auto GRAY = ui::Color::from_u32(0xffe5e5e5);
|
||||||
|
|
||||||
|
static constexpr auto BRIGHT_BLACK = ui::Color::from_u32(0xff7f7f7f);
|
||||||
|
static constexpr auto BRIGHT_RED = ui::Color::from_u32(0xffff0000);
|
||||||
|
static constexpr auto BRIGHT_GREEN = ui::Color::from_u32(0xff00ff00);
|
||||||
|
static constexpr auto BRIGHT_YELLOW = ui::Color::from_u32(0xffffff00);
|
||||||
|
static constexpr auto BRIGHT_BLUE = ui::Color::from_u32(0xff5c5cff);
|
||||||
|
static constexpr auto BRIGHT_MAGENTA = ui::Color::from_u32(0xffff00ff);
|
||||||
|
static constexpr auto BRIGHT_CYAN = ui::Color::from_u32(0xff00ffff);
|
||||||
|
static constexpr auto BRIGHT_GRAY = ui::Color::from_u32(0xffffffff);
|
||||||
|
|
||||||
|
static void sigchld_handler(int)
|
||||||
|
{
|
||||||
|
wait(NULL);
|
||||||
|
ui::App::the().set_should_close(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void> TerminalWidget::init(char* const* args)
|
||||||
|
{
|
||||||
|
m_font = ui::Font::default_font();
|
||||||
|
m_bold_font = ui::Font::default_bold_font();
|
||||||
|
|
||||||
|
m_terminal_canvas = window()->canvas();
|
||||||
|
m_terminal_canvas.fill(ui::BLACK);
|
||||||
|
|
||||||
|
m_cursor_timer = TRY(os::Timer::create_repeating(500, [this]() { this->tick_cursor(); }));
|
||||||
|
|
||||||
|
signal(SIGCHLD, sigchld_handler);
|
||||||
|
|
||||||
|
int master;
|
||||||
|
pid_t child = forkpty(&master, nullptr, nullptr, nullptr);
|
||||||
|
if (child < 0) return err(errno);
|
||||||
|
if (child == 0)
|
||||||
|
{
|
||||||
|
execv(args[0], args);
|
||||||
|
_exit(127);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_pty = master;
|
||||||
|
|
||||||
|
os::EventLoop::the().register_fd_listener(m_pty, [this](int, int) { this->process(); });
|
||||||
|
|
||||||
|
m_child_pid = child;
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<ui::EventResult> TerminalWidget::handle_key_event(const ui::KeyEventRequest& request)
|
||||||
|
{
|
||||||
|
// Avoid handling "key released" events
|
||||||
|
if (!request.pressed) return ui::EventResult::DidNotHandle;
|
||||||
|
|
||||||
|
// Non-printable key or key that has no special character (unlike Tab or Enter). We exit early to avoid inserting an
|
||||||
|
// invalid zero byte into the terminal input (this would also happen on Shift or Ctrl keypresses).
|
||||||
|
if (request.letter == '\0') return ui::EventResult::DidNotHandle;
|
||||||
|
|
||||||
|
write(m_pty, &request.letter, 1);
|
||||||
|
return ui::EventResult::DidHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void> TerminalWidget::draw(ui::Canvas&)
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void> TerminalWidget::process()
|
||||||
|
{
|
||||||
|
char buffer[BUFSIZ];
|
||||||
|
ssize_t nread = read(m_pty, buffer, BUFSIZ);
|
||||||
|
if (nread < 0)
|
||||||
|
{
|
||||||
|
if (errno == EAGAIN) nread = 0;
|
||||||
|
else
|
||||||
|
return err(errno);
|
||||||
|
}
|
||||||
|
|
||||||
|
ssize_t drawn = 0;
|
||||||
|
|
||||||
|
for (ssize_t i = 0; i < nread; i++)
|
||||||
|
{
|
||||||
|
bool did_draw = TRY(putchar(buffer[i]));
|
||||||
|
if (did_draw) drawn++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (drawn > 0) window()->draw();
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void TerminalWidget::tick_cursor()
|
||||||
|
{
|
||||||
|
if (!m_cursor_enabled) return;
|
||||||
|
|
||||||
|
m_cursor_activated = !m_cursor_activated;
|
||||||
|
|
||||||
|
if (m_cursor_activated) draw_cursor();
|
||||||
|
else
|
||||||
|
erase_current_char();
|
||||||
|
|
||||||
|
window()->draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TerminalWidget::draw_glyph(wchar_t c, int x, int y)
|
||||||
|
{
|
||||||
|
auto subcanvas = m_terminal_canvas.subcanvas({ x, y, m_font->width(), m_font->height() });
|
||||||
|
subcanvas.fill(m_background_color);
|
||||||
|
(m_bold ? m_bold_font : m_font)->render(c, m_foreground_color, subcanvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TerminalWidget::erase_current_line()
|
||||||
|
{
|
||||||
|
m_terminal_canvas.subcanvas({ 0, m_y_position, m_rect.width, m_font->height() }).fill(ui::BLACK);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TerminalWidget::scroll()
|
||||||
|
{
|
||||||
|
memcpy(m_terminal_canvas.ptr, m_terminal_canvas.ptr + (m_rect.width * sizeof(u32) * m_font->height()),
|
||||||
|
(m_rect.width * m_rect.height * sizeof(u32)) - (m_rect.width * sizeof(u32) * m_font->height()));
|
||||||
|
m_y_position -= m_font->height();
|
||||||
|
erase_current_line();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TerminalWidget::should_scroll()
|
||||||
|
{
|
||||||
|
return m_y_position >= m_rect.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TerminalWidget::next_line()
|
||||||
|
{
|
||||||
|
m_x_position = 0;
|
||||||
|
m_y_position += m_font->height();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TerminalWidget::next_char()
|
||||||
|
{
|
||||||
|
m_x_position += m_font->width();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TerminalWidget::prev_char()
|
||||||
|
{
|
||||||
|
m_x_position -= m_font->width();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TerminalWidget::erase_current_char()
|
||||||
|
{
|
||||||
|
m_terminal_canvas.subcanvas({ m_x_position, m_y_position, m_font->width(), m_font->height() }).fill(ui::BLACK);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TerminalWidget::draw_cursor()
|
||||||
|
{
|
||||||
|
m_terminal_canvas.subcanvas({ m_x_position, m_y_position, m_font->width(), m_font->height() }).fill(ui::WHITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TerminalWidget::at_end_of_screen()
|
||||||
|
{
|
||||||
|
return (m_x_position + m_font->width()) > m_rect.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TerminalWidget::handle_escape_sequence(wchar_t c)
|
||||||
|
{
|
||||||
|
auto rc = m_escape_parser->advance(static_cast<u8>(c));
|
||||||
|
if (rc.has_error())
|
||||||
|
{
|
||||||
|
m_escape_parser = Option<EscapeSequenceParser> {};
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!rc.value()) return true;
|
||||||
|
if (!m_escape_parser->valid())
|
||||||
|
{
|
||||||
|
m_escape_parser = Option<EscapeSequenceParser> {};
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& params = m_escape_parser->parameters();
|
||||||
|
switch (m_escape_parser->code())
|
||||||
|
{
|
||||||
|
case EscapeCode::CursorUp: {
|
||||||
|
int lines = params.size() ? params[0] : 1;
|
||||||
|
int pixels = lines * m_font->height();
|
||||||
|
if (pixels > m_y_position) m_y_position = 0;
|
||||||
|
else
|
||||||
|
m_y_position -= pixels;
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case EscapeCode::CursorDown: {
|
||||||
|
int lines = params.size() ? params[0] : 1;
|
||||||
|
int pixels = lines * m_font->height();
|
||||||
|
if (pixels + m_y_position >= m_rect.height) m_y_position = m_rect.height - m_font->height();
|
||||||
|
else
|
||||||
|
m_y_position += pixels;
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case EscapeCode::CursorBack: {
|
||||||
|
int chars = params.size() ? params[0] : 1;
|
||||||
|
int pixels = chars * m_font->width();
|
||||||
|
if (pixels > m_x_position) m_x_position = 0;
|
||||||
|
else
|
||||||
|
m_x_position -= pixels;
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case EscapeCode::CursorForward: {
|
||||||
|
int chars = params.size() ? params[0] : 1;
|
||||||
|
int pixels = chars * m_font->width();
|
||||||
|
if (pixels + m_x_position >= m_rect.width) m_x_position = m_rect.width - m_font->width();
|
||||||
|
else
|
||||||
|
m_x_position += pixels;
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case EscapeCode::CursorNextLine: {
|
||||||
|
int lines = params.size() ? params[0] : 1;
|
||||||
|
int pixels = lines * m_font->height();
|
||||||
|
if (pixels > m_y_position) m_y_position = 0;
|
||||||
|
else
|
||||||
|
m_y_position -= pixels;
|
||||||
|
m_x_position = 0;
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case EscapeCode::CursorPreviousLine: {
|
||||||
|
int lines = params.size() ? params[0] : 1;
|
||||||
|
int pixels = lines * m_font->height();
|
||||||
|
if (pixels + m_y_position >= m_rect.height) m_y_position = m_rect.height - m_font->height();
|
||||||
|
else
|
||||||
|
m_y_position += pixels;
|
||||||
|
m_x_position = 0;
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case EscapeCode::CursorHorizontalAbsolute: {
|
||||||
|
int line = (params.size() ? params[0] : 1) - 1;
|
||||||
|
if (line < 0) break;
|
||||||
|
int position = line * m_font->height();
|
||||||
|
if (position >= m_rect.height) position = m_rect.height - m_font->height();
|
||||||
|
m_y_position = position;
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case EscapeCode::SetCursorPosition: {
|
||||||
|
int x = (params.size() ? params[0] : 1) - 1;
|
||||||
|
int y = (params.size() > 1 ? params[1] : 1) - 1;
|
||||||
|
if (x < 0 || y < 0) break;
|
||||||
|
int x_position = x * m_font->width();
|
||||||
|
if (x_position >= m_rect.width) x_position = m_rect.width - m_font->height();
|
||||||
|
m_x_position = x_position;
|
||||||
|
int y_position = y * m_font->height();
|
||||||
|
if (y_position >= m_rect.height) y_position = m_rect.height - m_font->height();
|
||||||
|
m_y_position = y_position;
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case EscapeCode::SelectGraphicRendition: {
|
||||||
|
if (!params.size())
|
||||||
|
{
|
||||||
|
m_foreground_color = ui::WHITE;
|
||||||
|
m_background_color = ui::BLACK;
|
||||||
|
m_bold = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (usize i = 0; i < params.size(); i++)
|
||||||
|
{
|
||||||
|
int arg = params[i];
|
||||||
|
switch (arg)
|
||||||
|
{
|
||||||
|
case 0: {
|
||||||
|
m_foreground_color = ui::BLACK;
|
||||||
|
m_background_color = ui::WHITE;
|
||||||
|
m_bold = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 1: {
|
||||||
|
m_bold = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 22: {
|
||||||
|
m_bold = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 30: {
|
||||||
|
m_foreground_color = m_bold ? BRIGHT_BLACK : ui::BLACK;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 31: {
|
||||||
|
m_foreground_color = m_bold ? BRIGHT_RED : RED;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 32: {
|
||||||
|
m_foreground_color = m_bold ? BRIGHT_GREEN : GREEN;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 33: {
|
||||||
|
m_foreground_color = m_bold ? BRIGHT_YELLOW : YELLOW;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 34: {
|
||||||
|
m_foreground_color = m_bold ? BRIGHT_BLUE : BLUE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 35: {
|
||||||
|
m_foreground_color = m_bold ? BRIGHT_MAGENTA : MAGENTA;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 36: {
|
||||||
|
m_foreground_color = m_bold ? BRIGHT_CYAN : CYAN;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 37: {
|
||||||
|
m_foreground_color = m_bold ? BRIGHT_GRAY : GRAY;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 39: {
|
||||||
|
m_foreground_color = ui::WHITE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 40: {
|
||||||
|
m_background_color = m_bold ? BRIGHT_BLACK : ui::BLACK;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 41: {
|
||||||
|
m_background_color = m_bold ? BRIGHT_RED : RED;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 42: {
|
||||||
|
m_background_color = m_bold ? BRIGHT_GREEN : GREEN;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 43: {
|
||||||
|
m_background_color = m_bold ? BRIGHT_YELLOW : YELLOW;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 44: {
|
||||||
|
m_background_color = m_bold ? BRIGHT_BLUE : BLUE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 45: {
|
||||||
|
m_background_color = m_bold ? BRIGHT_MAGENTA : MAGENTA;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 46: {
|
||||||
|
m_background_color = m_bold ? BRIGHT_CYAN : CYAN;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 47: {
|
||||||
|
m_background_color = m_bold ? BRIGHT_GRAY : GRAY;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 49: {
|
||||||
|
m_background_color = ui::BLACK;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_escape_parser = Option<EscapeSequenceParser> {};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<bool> TerminalWidget::putchar(char c)
|
||||||
|
{
|
||||||
|
auto guard = make_scope_guard([this] { m_decoder.reset(); });
|
||||||
|
|
||||||
|
bool is_ready = TRY(m_decoder.feed(c));
|
||||||
|
|
||||||
|
bool result = false;
|
||||||
|
|
||||||
|
if (is_ready) result = put_code_point(TRY(m_decoder.extract()));
|
||||||
|
|
||||||
|
guard.deactivate();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TerminalWidget::put_code_point(wchar_t c)
|
||||||
|
{
|
||||||
|
if (c > (wchar_t)255) c = (wchar_t)256;
|
||||||
|
|
||||||
|
if (m_escape_parser.has_value())
|
||||||
|
{
|
||||||
|
if (handle_escape_sequence(c)) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erase the current cursor.
|
||||||
|
if (m_cursor_activated) erase_current_char();
|
||||||
|
|
||||||
|
bool should_draw_cursor = m_cursor_enabled;
|
||||||
|
|
||||||
|
bool did_draw = false;
|
||||||
|
|
||||||
|
switch (c)
|
||||||
|
{
|
||||||
|
case L'\n': {
|
||||||
|
next_line();
|
||||||
|
if (should_scroll()) scroll();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case L'\t': {
|
||||||
|
for (int i = 0; i < 4; i++) { put_code_point(L' '); }
|
||||||
|
did_draw = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case L'\r': m_x_position = 0; break;
|
||||||
|
case L'\b':
|
||||||
|
if (m_x_position != 0)
|
||||||
|
{
|
||||||
|
prev_char();
|
||||||
|
erase_current_char();
|
||||||
|
did_draw = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case L'\x1b':
|
||||||
|
case L'\x9b':
|
||||||
|
case L'\x90':
|
||||||
|
case L'\x9d':
|
||||||
|
m_escape_parser = EscapeSequenceParser { (u8)c };
|
||||||
|
should_draw_cursor = false;
|
||||||
|
did_draw = true;
|
||||||
|
break;
|
||||||
|
default: {
|
||||||
|
if (iscntrl(c)) return false;
|
||||||
|
draw_glyph(c, m_x_position, m_y_position);
|
||||||
|
next_char();
|
||||||
|
if (at_end_of_screen())
|
||||||
|
{
|
||||||
|
next_line();
|
||||||
|
if (should_scroll()) scroll();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (should_draw_cursor)
|
||||||
|
{
|
||||||
|
m_cursor_timer->restart();
|
||||||
|
m_cursor_activated = true;
|
||||||
|
draw_cursor();
|
||||||
|
}
|
||||||
|
|
||||||
|
return did_draw;
|
||||||
|
}
|
64
gui/apps/terminal/TerminalWidget.h
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <luna/EscapeSequence.h>
|
||||||
|
#include <luna/Utf8.h>
|
||||||
|
#include <luna/Vector.h>
|
||||||
|
#include <os/Timer.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <termios.h>
|
||||||
|
#include <ui/Font.h>
|
||||||
|
#include <ui/Widget.h>
|
||||||
|
|
||||||
|
class TerminalWidget : public ui::Widget
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Result<void> init(char* const* args);
|
||||||
|
|
||||||
|
Result<ui::EventResult> handle_key_event(const ui::KeyEventRequest& request) override;
|
||||||
|
|
||||||
|
Result<void> draw(ui::Canvas& canvas) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
ui::Canvas m_terminal_canvas;
|
||||||
|
Vector<u8> m_line_buffer;
|
||||||
|
int m_pty;
|
||||||
|
pid_t m_child_pid;
|
||||||
|
|
||||||
|
struct termios m_settings;
|
||||||
|
|
||||||
|
SharedPtr<ui::Font> m_font;
|
||||||
|
SharedPtr<ui::Font> m_bold_font;
|
||||||
|
|
||||||
|
OwnedPtr<os::Timer> m_cursor_timer;
|
||||||
|
bool m_cursor_activated = false;
|
||||||
|
bool m_cursor_enabled = true;
|
||||||
|
|
||||||
|
long m_last_cursor_tick;
|
||||||
|
|
||||||
|
int m_x_position { 0 };
|
||||||
|
int m_y_position { 0 };
|
||||||
|
|
||||||
|
bool m_bold { false };
|
||||||
|
|
||||||
|
ui::Color m_foreground_color { ui::WHITE };
|
||||||
|
ui::Color m_background_color { ui::BLACK };
|
||||||
|
|
||||||
|
void tick_cursor();
|
||||||
|
|
||||||
|
Utf8StateDecoder m_decoder;
|
||||||
|
Option<EscapeSequenceParser> m_escape_parser;
|
||||||
|
|
||||||
|
void draw_glyph(wchar_t c, int x, int y);
|
||||||
|
void erase_current_line();
|
||||||
|
void scroll();
|
||||||
|
bool should_scroll();
|
||||||
|
void next_line();
|
||||||
|
void next_char();
|
||||||
|
void prev_char();
|
||||||
|
void erase_current_char();
|
||||||
|
void draw_cursor();
|
||||||
|
bool at_end_of_screen();
|
||||||
|
bool handle_escape_sequence(wchar_t c);
|
||||||
|
Result<bool> putchar(char c);
|
||||||
|
bool put_code_point(wchar_t c);
|
||||||
|
Result<void> process();
|
||||||
|
};
|
24
gui/apps/terminal/main.cpp
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
#include "TerminalWidget.h"
|
||||||
|
#include <os/ArgumentParser.h>
|
||||||
|
#include <ui/App.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
Result<int> luna_main(int, char**)
|
||||||
|
{
|
||||||
|
ui::App app;
|
||||||
|
TRY(app.init());
|
||||||
|
|
||||||
|
auto* window = TRY(ui::Window::create(ui::Rect { 150, 150, 640, 400 }));
|
||||||
|
app.set_main_window(window);
|
||||||
|
window->set_title("Terminal");
|
||||||
|
|
||||||
|
TerminalWidget terminal;
|
||||||
|
window->set_main_widget(terminal);
|
||||||
|
|
||||||
|
char* args[] = { "/bin/sh", nullptr };
|
||||||
|
TRY(terminal.init(args));
|
||||||
|
|
||||||
|
window->draw();
|
||||||
|
|
||||||
|
return app.run();
|
||||||
|
}
|
109
gui/execd.cpp
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
/**
|
||||||
|
* @file execd.cpp
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief Background process that handles detached launching of apps.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2024, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
#include <os/ArgumentParser.h>
|
||||||
|
#include <os/File.h>
|
||||||
|
#include <os/IPC.h>
|
||||||
|
#include <os/LocalServer.h>
|
||||||
|
#include <os/Process.h>
|
||||||
|
#include <os/Security.h>
|
||||||
|
#include <os/ipc/Launcher.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/poll.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
Result<void> handle_launch_detached_message(os::IPC::ClientConnection& client)
|
||||||
|
{
|
||||||
|
os::Launcher::LaunchDetachedRequest request;
|
||||||
|
if (!TRY(client.read_message(request))) return {};
|
||||||
|
|
||||||
|
auto path = COPY_IPC_STRING(request.command);
|
||||||
|
|
||||||
|
StringView args[] = { path.view() };
|
||||||
|
|
||||||
|
os::Process::spawn(args[0], { args, 1 }, request.search_in_path);
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void handle_ipc_message(os::IPC::ClientConnection& client, u8 id, void*)
|
||||||
|
{
|
||||||
|
switch (id)
|
||||||
|
{
|
||||||
|
case os::Launcher::LAUNCH_DETACHED_ID: handle_launch_detached_message(client); break;
|
||||||
|
default: os::eprintln("execd: Invalid IPC message from client!"); return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void sigchld_handler(int)
|
||||||
|
{
|
||||||
|
os::Process::wait(os::Process::ANY_CHILD, nullptr, WNOHANG);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<int> luna_main(int argc, char** argv)
|
||||||
|
{
|
||||||
|
TRY(os::Security::pledge("stdio wpath cpath unix proc exec", NULL));
|
||||||
|
|
||||||
|
StringView socket_path = "/tmp/execd.sock";
|
||||||
|
|
||||||
|
os::ArgumentParser parser;
|
||||||
|
parser.add_description("Background process that handles detached launching of apps."_sv);
|
||||||
|
parser.add_system_program_info("execd"_sv);
|
||||||
|
parser.parse(argc, argv);
|
||||||
|
|
||||||
|
signal(SIGCHLD, sigchld_handler);
|
||||||
|
|
||||||
|
auto server = TRY(os::LocalServer::create(socket_path, false));
|
||||||
|
TRY(server->listen(20));
|
||||||
|
|
||||||
|
// We're ready now.
|
||||||
|
os::IPC::notify_parent();
|
||||||
|
|
||||||
|
Vector<OwnedPtr<os::IPC::ClientConnection>> clients;
|
||||||
|
Vector<struct pollfd> fds;
|
||||||
|
TRY(fds.try_append({ .fd = server->fd(), .events = POLLIN, .revents = 0 }));
|
||||||
|
|
||||||
|
TRY(os::Security::pledge("stdio unix proc exec", NULL));
|
||||||
|
|
||||||
|
while (1)
|
||||||
|
{
|
||||||
|
for (auto& pfd : fds) { pfd.revents = 0; }
|
||||||
|
|
||||||
|
int rc = poll(fds.data(), fds.size(), 1000);
|
||||||
|
if (!rc) continue;
|
||||||
|
if (rc < 0 && errno != EINTR) { os::println("poll: error: %s", strerror(errno)); }
|
||||||
|
|
||||||
|
if (fds[0].revents & POLLIN)
|
||||||
|
{
|
||||||
|
auto client = TRY(server->accept());
|
||||||
|
os::println("execd: New client connected!");
|
||||||
|
TRY(fds.try_append({ .fd = client.fd(), .events = POLLIN, .revents = 0 }));
|
||||||
|
|
||||||
|
auto connection = TRY(os::IPC::ClientConnection::adopt_connection(move(client)));
|
||||||
|
connection->set_message_handler(handle_ipc_message, nullptr);
|
||||||
|
TRY(clients.try_append(move(connection)));
|
||||||
|
}
|
||||||
|
for (usize i = 0; i < clients.size(); i++)
|
||||||
|
{
|
||||||
|
if (fds[i + 1].revents & POLLIN) clients[i]->check_for_messages();
|
||||||
|
if (fds[i + 1].revents & POLLHUP)
|
||||||
|
{
|
||||||
|
os::println("execd: Client %zu disconnected", i);
|
||||||
|
fds.remove_at(i + 1);
|
||||||
|
auto client = clients.remove_at(i);
|
||||||
|
client->disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
gui/libui/CMakeLists.txt
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# The UI and graphics library for Luna.
|
||||||
|
|
||||||
|
file(GLOB HEADERS include/ui/*.h)
|
||||||
|
|
||||||
|
set(SOURCES
|
||||||
|
${HEADERS}
|
||||||
|
include/ui/ipc/Server.h
|
||||||
|
include/ui/ipc/Client.h
|
||||||
|
src/Canvas.cpp
|
||||||
|
src/Rect.cpp
|
||||||
|
src/Font.cpp
|
||||||
|
src/Image.cpp
|
||||||
|
src/App.cpp
|
||||||
|
src/Window.cpp
|
||||||
|
src/Layout.cpp
|
||||||
|
src/Alignment.cpp
|
||||||
|
src/Container.cpp
|
||||||
|
src/Button.cpp
|
||||||
|
src/Label.cpp
|
||||||
|
src/InputField.cpp
|
||||||
|
src/TextInput.cpp
|
||||||
|
src/Dialog.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
add_library(ui ${SOURCES})
|
||||||
|
target_compile_options(ui PRIVATE ${COMMON_FLAGS} -fno-threadsafe-statics)
|
||||||
|
target_include_directories(ui PUBLIC ${CMAKE_CURRENT_LIST_DIR}/include/)
|
||||||
|
target_include_directories(ui PUBLIC ${LUNA_BASE}/usr/include)
|
||||||
|
target_link_libraries(ui PUBLIC os)
|
||||||
|
|
||||||
|
add_custom_command(
|
||||||
|
TARGET ui
|
||||||
|
COMMAND "${CMAKE_COMMAND}" -E copy ${CMAKE_CURRENT_BINARY_DIR}/libui.a ${LUNA_BASE}/usr/lib/libui.a
|
||||||
|
)
|
30
gui/libui/include/ui/Alignment.h
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* @file Alignment.h
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief UI component alignment.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2023, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <ui/Rect.h>
|
||||||
|
|
||||||
|
namespace ui
|
||||||
|
{
|
||||||
|
enum class VerticalAlignment
|
||||||
|
{
|
||||||
|
Top,
|
||||||
|
Center,
|
||||||
|
Bottom
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class HorizontalAlignment
|
||||||
|
{
|
||||||
|
Left,
|
||||||
|
Center,
|
||||||
|
Right
|
||||||
|
};
|
||||||
|
|
||||||
|
Rect align(Rect container, Rect contained, VerticalAlignment valign, HorizontalAlignment halign);
|
||||||
|
}
|
76
gui/libui/include/ui/App.h
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* @file App.h
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief UI application event loop.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2023, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <luna/HashMap.h>
|
||||||
|
#include <luna/StringView.h>
|
||||||
|
#include <os/EventLoop.h>
|
||||||
|
#include <os/IPC.h>
|
||||||
|
#include <ui/Window.h>
|
||||||
|
|
||||||
|
namespace ui
|
||||||
|
{
|
||||||
|
class App
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
App();
|
||||||
|
~App();
|
||||||
|
|
||||||
|
Result<void> init(StringView socket_path = "/tmp/wind.sock");
|
||||||
|
Result<int> run();
|
||||||
|
|
||||||
|
Rect screen_rect();
|
||||||
|
|
||||||
|
os::IPC::Client& client()
|
||||||
|
{
|
||||||
|
return *m_client;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_should_close(bool b)
|
||||||
|
{
|
||||||
|
m_should_close = b;
|
||||||
|
if (b) m_loop.quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_main_window(Window* window)
|
||||||
|
{
|
||||||
|
check(!m_main_window);
|
||||||
|
m_main_window = window;
|
||||||
|
}
|
||||||
|
|
||||||
|
Window* main_window()
|
||||||
|
{
|
||||||
|
return m_main_window;
|
||||||
|
}
|
||||||
|
|
||||||
|
void pledge(i16 pledges);
|
||||||
|
|
||||||
|
Result<void> register_window(OwnedPtr<Window>&& window, Badge<Window>);
|
||||||
|
void unregister_window(Window* window, Badge<Window>);
|
||||||
|
|
||||||
|
static App& the();
|
||||||
|
|
||||||
|
private:
|
||||||
|
static App* s_app;
|
||||||
|
OwnedPtr<os::IPC::Client> m_client;
|
||||||
|
Window* m_main_window { nullptr };
|
||||||
|
HashMap<int, OwnedPtr<Window>> m_windows;
|
||||||
|
bool m_should_close { false };
|
||||||
|
os::EventLoop m_loop;
|
||||||
|
Vector<int> m_window_clear_queue;
|
||||||
|
|
||||||
|
bool process_events();
|
||||||
|
|
||||||
|
Window* find_window(int id);
|
||||||
|
|
||||||
|
Result<void> handle_ipc_event(os::IPC::Client&, u8 id, void*);
|
||||||
|
|
||||||
|
friend void handle_socket_event(int, int);
|
||||||
|
};
|
||||||
|
}
|
37
gui/libui/include/ui/Button.h
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* @file Button.h
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief A clickable component that triggers an action when pressed.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2023, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <luna/Action.h>
|
||||||
|
#include <ui/Widget.h>
|
||||||
|
|
||||||
|
namespace ui
|
||||||
|
{
|
||||||
|
class Button : public Widget
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Button(Rect rect);
|
||||||
|
|
||||||
|
void set_widget(Widget& widget);
|
||||||
|
void set_action(Action&& action);
|
||||||
|
|
||||||
|
Result<EventResult> handle_mouse_move(Point position) override;
|
||||||
|
Result<EventResult> handle_mouse_leave() override;
|
||||||
|
Result<EventResult> handle_mouse_down(Point position, int buttons) override;
|
||||||
|
Result<EventResult> handle_mouse_up(Point position, int buttons) override;
|
||||||
|
Result<EventResult> handle_key_event(const ui::KeyEventRequest& request) override;
|
||||||
|
Result<void> draw(Canvas& canvas) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool m_hovered { false };
|
||||||
|
bool m_clicked { false };
|
||||||
|
Widget* m_child;
|
||||||
|
Action m_action;
|
||||||
|
};
|
||||||
|
}
|
81
gui/libui/include/ui/Canvas.h
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
/**
|
||||||
|
* @file Canvas.h
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief Drawable surfaces.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2023, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <luna/Result.h>
|
||||||
|
#include <luna/Types.h>
|
||||||
|
#include <ui/Color.h>
|
||||||
|
#include <ui/Point.h>
|
||||||
|
#include <ui/Rect.h>
|
||||||
|
|
||||||
|
namespace ui
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @brief A drawable surface.
|
||||||
|
*/
|
||||||
|
struct Canvas
|
||||||
|
{
|
||||||
|
int width;
|
||||||
|
int height;
|
||||||
|
int stride;
|
||||||
|
u8* ptr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Create a new Canvas object.
|
||||||
|
*
|
||||||
|
* @param ptr The memory to use for the canvas. It must be of at least width * height * 4 bytes of length.
|
||||||
|
* @param width The width of the canvas.
|
||||||
|
* @param height The height of the canvas.
|
||||||
|
* @return Canvas The new Canvas object.
|
||||||
|
*/
|
||||||
|
static Canvas create(u8* ptr, int width, int height);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Return a new Canvas that represents a subsection of the current one.
|
||||||
|
*
|
||||||
|
* @param rect The dimensions of the new canvas. If these exceed the bounds of the current canvas, they will be
|
||||||
|
* clamped.
|
||||||
|
* @return Canvas The new Canvas object.
|
||||||
|
*/
|
||||||
|
Canvas subcanvas(Rect rect);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Return the dimensions of the current canvas.
|
||||||
|
*
|
||||||
|
* @return Rect This canvas's dimensions, as a Rect object.
|
||||||
|
*/
|
||||||
|
Rect rect()
|
||||||
|
{
|
||||||
|
return Rect { .pos = { 0, 0 }, .width = width, .height = height };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Fill the entire canvas with one color.
|
||||||
|
*
|
||||||
|
* @param color The color to use.
|
||||||
|
*/
|
||||||
|
void fill(Color color);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Fill the canvas with pixels.
|
||||||
|
*
|
||||||
|
* @param pixels The array of pixels (must be at least width*height).
|
||||||
|
* @param stride The number of pixels to skip to go to the next line.
|
||||||
|
*/
|
||||||
|
void fill(u32* pixels, int stride);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Fill the canvas with pixels, without doing any extra processing.
|
||||||
|
*
|
||||||
|
* @param pixels The array of pixels (must be at least width*height).
|
||||||
|
* @param stride The number of pixels to skip to go to the next line.
|
||||||
|
*/
|
||||||
|
void copy(u32* pixels, int stride);
|
||||||
|
};
|
||||||
|
};
|
113
gui/libui/include/ui/Color.h
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
/**
|
||||||
|
* @file Color.h
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief RGBA colors.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2023, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <luna/Types.h>
|
||||||
|
|
||||||
|
namespace ui
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @brief A 32-bit ARGB color.
|
||||||
|
*/
|
||||||
|
struct Color
|
||||||
|
{
|
||||||
|
union {
|
||||||
|
u32 raw;
|
||||||
|
u8 colors[4];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Return the blue value of this color.
|
||||||
|
*
|
||||||
|
* @return constexpr u8 The blue value.
|
||||||
|
*/
|
||||||
|
constexpr u8 red() const
|
||||||
|
{
|
||||||
|
return colors[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Return the green value of this color.
|
||||||
|
*
|
||||||
|
* @return constexpr u8 The green value.
|
||||||
|
*/
|
||||||
|
constexpr u8 green() const
|
||||||
|
{
|
||||||
|
return colors[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Return the blue value of this color.
|
||||||
|
*
|
||||||
|
* @return constexpr u8 The blue value.
|
||||||
|
*/
|
||||||
|
constexpr u8 blue() const
|
||||||
|
{
|
||||||
|
return colors[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Return the alpha value of this color.
|
||||||
|
*
|
||||||
|
* @return constexpr u8 The alpha value.
|
||||||
|
*/
|
||||||
|
constexpr u8 alpha() const
|
||||||
|
{
|
||||||
|
return colors[3];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Construct a new color from a 32-bit ARGB integer.
|
||||||
|
*
|
||||||
|
* @param raw The integer representing the color.
|
||||||
|
* @return constexpr Color The new color.
|
||||||
|
*/
|
||||||
|
static constexpr Color from_u32(u32 raw)
|
||||||
|
{
|
||||||
|
return Color { .raw = raw };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Construct a new color from its separate RGBA values (from 0 to 255).
|
||||||
|
*
|
||||||
|
* @param red The red value.
|
||||||
|
* @param green The green value.
|
||||||
|
* @param blue The blue value.
|
||||||
|
* @param alpha The alpha value.
|
||||||
|
* @return constexpr Color The new color.
|
||||||
|
*/
|
||||||
|
static constexpr Color from_rgba(u8 red, u8 green, u8 blue, u8 alpha)
|
||||||
|
{
|
||||||
|
return Color { .colors = { blue, green, red, alpha } };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Construct a new color from its separate RGB values (from 0 to 255).
|
||||||
|
*
|
||||||
|
* @param red The red value.
|
||||||
|
* @param green The green value.
|
||||||
|
* @param blue The blue value.
|
||||||
|
* @return constexpr Color The new color.
|
||||||
|
*/
|
||||||
|
static constexpr Color from_rgb(u8 red, u8 green, u8 blue)
|
||||||
|
{
|
||||||
|
return from_rgba(red, green, blue, 0xff);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr Color WHITE = Color::from_rgb(0xff, 0xff, 0xff);
|
||||||
|
static constexpr Color BLACK = Color::from_rgb(0x00, 0x00, 0x00);
|
||||||
|
static constexpr Color GRAY = Color::from_rgb(0x80, 0x80, 0x80);
|
||||||
|
|
||||||
|
static constexpr Color BLUE = Color::from_rgb(0x00, 0x00, 0xff);
|
||||||
|
static constexpr Color GREEN = Color::from_rgb(0x00, 0xff, 0x00);
|
||||||
|
static constexpr Color RED = Color::from_rgb(0xff, 0x00, 0x00);
|
||||||
|
|
||||||
|
static constexpr Color CYAN = Color::from_rgb(0x00, 0xff, 0xff);
|
||||||
|
};
|
35
gui/libui/include/ui/Container.h
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* @file Container.h
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief A container widget to pad and align objects inside it.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2023, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <ui/Alignment.h>
|
||||||
|
#include <ui/Widget.h>
|
||||||
|
|
||||||
|
namespace ui
|
||||||
|
{
|
||||||
|
class Container : public Widget
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Container(Rect rect, VerticalAlignment valign, HorizontalAlignment halign);
|
||||||
|
|
||||||
|
void set_widget(Widget& widget);
|
||||||
|
|
||||||
|
Result<EventResult> handle_mouse_move(Point position) override;
|
||||||
|
Result<EventResult> handle_mouse_leave() override;
|
||||||
|
Result<EventResult> handle_mouse_down(Point position, int buttons) override;
|
||||||
|
Result<EventResult> handle_mouse_up(Point position, int buttons) override;
|
||||||
|
Result<EventResult> handle_key_event(const ui::KeyEventRequest& request) override;
|
||||||
|
Result<void> draw(Canvas& canvas) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Widget* m_widget;
|
||||||
|
VerticalAlignment m_valign;
|
||||||
|
HorizontalAlignment m_halign;
|
||||||
|
};
|
||||||
|
}
|
22
gui/libui/include/ui/Dialog.h
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* @file Window.h
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief UI window dialogs.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2024, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <luna/Action.h>
|
||||||
|
#include <ui/Window.h>
|
||||||
|
|
||||||
|
namespace ui
|
||||||
|
{
|
||||||
|
namespace Dialog
|
||||||
|
{
|
||||||
|
Result<void> show_message(StringView title, StringView message);
|
||||||
|
|
||||||
|
Result<void> show_input_dialog(StringView title, StringView message, Function<StringView> callback);
|
||||||
|
}
|
||||||
|
}
|
123
gui/libui/include/ui/Font.h
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
/**
|
||||||
|
* @file Font.h
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief PSF font loading and rendering.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2023, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <luna/Buffer.h>
|
||||||
|
#include <luna/SharedPtr.h>
|
||||||
|
#include <os/Path.h>
|
||||||
|
#include <ui/Canvas.h>
|
||||||
|
|
||||||
|
#define PSF_FONT_MAGIC 0x864ab572
|
||||||
|
|
||||||
|
namespace ui
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @brief A class holding PSF font data, used for low-level direct rendering of glyphs into a canvas.
|
||||||
|
*
|
||||||
|
* This class does not handle special characters such as tabs or newlines. For those, you should be using a more
|
||||||
|
* high-level component such as ui::Label instead.
|
||||||
|
*/
|
||||||
|
class Font : public Shareable
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief An enum used to select a font weight when loading a font.
|
||||||
|
*/
|
||||||
|
enum FontWeight
|
||||||
|
{
|
||||||
|
Regular,
|
||||||
|
Bold,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Load a Font object from a font file.
|
||||||
|
*
|
||||||
|
* @param path The full path to the font file.
|
||||||
|
* @return Result<SharedPtr<Font>> An error, or the loaded Font object.
|
||||||
|
*/
|
||||||
|
static Result<SharedPtr<Font>> load(const os::Path& path);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Load a system font by name.
|
||||||
|
*
|
||||||
|
* @param name The name of the font to load (the default system font is "Tamsyn").
|
||||||
|
* @param weight The weight of the font (regular or bold).
|
||||||
|
* @return Result<SharedPtr<Font>> An error, or the loaded Font object.
|
||||||
|
*/
|
||||||
|
static Result<SharedPtr<Font>> load_builtin(StringView name, FontWeight weight);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Return a pointer to the system's default font.
|
||||||
|
*
|
||||||
|
* @return SharedPtr<Font> The default font.
|
||||||
|
*/
|
||||||
|
static SharedPtr<Font> default_font();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Return a pointer to the system's default bold font.
|
||||||
|
*
|
||||||
|
* @return SharedPtr<Font> The default bold font.
|
||||||
|
*/
|
||||||
|
static SharedPtr<Font> default_bold_font();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Render a single Unicode code point onto a canvas, using this font's glyphs.
|
||||||
|
*
|
||||||
|
* @param codepoint The code point to render.
|
||||||
|
* @param color The color to draw the code point in.
|
||||||
|
* @param canvas The canvas to use.
|
||||||
|
*/
|
||||||
|
void render(wchar_t codepoint, ui::Color color, ui::Canvas& canvas);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Render a Unicode text string onto a canvas, using this font's glyphs.
|
||||||
|
*
|
||||||
|
* @param text The string to render (must be null-terminated).
|
||||||
|
* @param color The color to draw the code point in.
|
||||||
|
* @param canvas The canvas to use.
|
||||||
|
*/
|
||||||
|
void render(const wchar_t* text, ui::Color color, ui::Canvas& canvas);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Return the width of this font's glyphs.
|
||||||
|
*
|
||||||
|
* @return int The width.
|
||||||
|
*/
|
||||||
|
int width() const
|
||||||
|
{
|
||||||
|
return m_psf_header.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Return the height of this font's glyphs.
|
||||||
|
*
|
||||||
|
* @return int The height.
|
||||||
|
*/
|
||||||
|
int height() const
|
||||||
|
{
|
||||||
|
return m_psf_header.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct PSFHeader
|
||||||
|
{
|
||||||
|
u32 magic;
|
||||||
|
u32 version; // zero
|
||||||
|
u32 headersize;
|
||||||
|
u32 flags; // 0 if there's no unicode table
|
||||||
|
u32 numglyph;
|
||||||
|
u32 bytesperglyph;
|
||||||
|
int height;
|
||||||
|
int width;
|
||||||
|
};
|
||||||
|
|
||||||
|
PSFHeader m_psf_header;
|
||||||
|
Buffer m_font_data;
|
||||||
|
};
|
||||||
|
};
|
92
gui/libui/include/ui/Image.h
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
/**
|
||||||
|
* @file Image.h
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief TGA image loading and rendering.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2023, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <luna/Buffer.h>
|
||||||
|
#include <luna/SharedPtr.h>
|
||||||
|
#include <os/Path.h>
|
||||||
|
#include <ui/Widget.h>
|
||||||
|
|
||||||
|
namespace ui
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @brief An image in the TGA file format.
|
||||||
|
*/
|
||||||
|
class Image : public Shareable
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Load a new TGA image from a file.
|
||||||
|
*
|
||||||
|
* @param path The path to open.
|
||||||
|
* @return Result<SharedPtr<Image>> An error, or a new Image object.
|
||||||
|
*/
|
||||||
|
static Result<SharedPtr<Image>> load(const os::Path& path);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Return the array of pixels contained in the image.
|
||||||
|
*
|
||||||
|
* @return u32* The array of pixels.
|
||||||
|
*/
|
||||||
|
u32* pixels()
|
||||||
|
{
|
||||||
|
return (u32*)m_image_data.data();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Return the width of the image.
|
||||||
|
*
|
||||||
|
* @return u16 The width.
|
||||||
|
*/
|
||||||
|
u16 width()
|
||||||
|
{
|
||||||
|
return m_tga_header.w;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Return the height of the image.
|
||||||
|
*
|
||||||
|
* @return u16 The height.
|
||||||
|
*/
|
||||||
|
u16 height()
|
||||||
|
{
|
||||||
|
return m_tga_header.h;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct [[gnu::packed]] TGAHeader
|
||||||
|
{
|
||||||
|
u8 idlen;
|
||||||
|
u8 colormap;
|
||||||
|
u8 encoding;
|
||||||
|
u16 cmaporig, cmaplen;
|
||||||
|
u8 cmapent;
|
||||||
|
u16 x;
|
||||||
|
u16 y;
|
||||||
|
u16 w;
|
||||||
|
u16 h;
|
||||||
|
u8 bpp;
|
||||||
|
u8 pixeltype;
|
||||||
|
};
|
||||||
|
|
||||||
|
TGAHeader m_tga_header;
|
||||||
|
Buffer m_image_data;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ImageWidget final : public Widget
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static Result<OwnedPtr<ImageWidget>> load(const os::Path& path);
|
||||||
|
|
||||||
|
Result<void> draw(Canvas& canvas) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
SharedPtr<Image> m_image;
|
||||||
|
};
|
||||||
|
}
|
42
gui/libui/include/ui/InputField.h
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* @file InputField.h
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief Single line text input widget.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2024, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <luna/Action.h>
|
||||||
|
#include <ui/Font.h>
|
||||||
|
#include <ui/TextInput.h>
|
||||||
|
|
||||||
|
namespace ui
|
||||||
|
{
|
||||||
|
class InputField final : public ui::TextInput
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
InputField(SharedPtr<ui::Font> font);
|
||||||
|
|
||||||
|
Result<ui::EventResult> handle_key_event(const ui::KeyEventRequest& request) override;
|
||||||
|
|
||||||
|
Result<void> draw(ui::Canvas& canvas) override;
|
||||||
|
|
||||||
|
void clear();
|
||||||
|
|
||||||
|
StringView data();
|
||||||
|
|
||||||
|
void on_submit(Function<StringView>&& action)
|
||||||
|
{
|
||||||
|
m_on_submit_action = move(action);
|
||||||
|
m_has_on_submit_action = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
SharedPtr<ui::Font> m_font;
|
||||||
|
|
||||||
|
Function<StringView> m_on_submit_action;
|
||||||
|
bool m_has_on_submit_action { false };
|
||||||
|
};
|
||||||
|
}
|
14
gui/libui/include/ui/Key.h
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <moon/Keyboard.h>
|
||||||
|
|
||||||
|
namespace ui
|
||||||
|
{
|
||||||
|
enum Modifier
|
||||||
|
{
|
||||||
|
Mod_Shift = (1 << 0),
|
||||||
|
Mod_Alt = (1 << 1),
|
||||||
|
Mod_Super = (1 << 2),
|
||||||
|
Mod_AltGr = (1 << 3),
|
||||||
|
Mod_Ctrl = (1 << 4)
|
||||||
|
};
|
||||||
|
}
|
57
gui/libui/include/ui/Label.h
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* @file Label.h
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief A simple one-line text widget.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2023, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <ui/Alignment.h>
|
||||||
|
#include <ui/Font.h>
|
||||||
|
#include <ui/Widget.h>
|
||||||
|
|
||||||
|
namespace ui
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @brief Displays one line of text.
|
||||||
|
*
|
||||||
|
* This component does not handle newlines.
|
||||||
|
*/
|
||||||
|
class Label final : public Widget
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Label(StringView text);
|
||||||
|
|
||||||
|
void set_alignment(VerticalAlignment valign, HorizontalAlignment halign)
|
||||||
|
{
|
||||||
|
m_valign = valign;
|
||||||
|
m_halign = halign;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_color(ui::Color color)
|
||||||
|
{
|
||||||
|
m_color = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_font(SharedPtr<ui::Font> font)
|
||||||
|
{
|
||||||
|
m_font = font;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_text(StringView text)
|
||||||
|
{
|
||||||
|
m_text = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void> draw(Canvas& canvas) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
StringView m_text;
|
||||||
|
VerticalAlignment m_valign = VerticalAlignment::Center;
|
||||||
|
HorizontalAlignment m_halign = HorizontalAlignment::Center;
|
||||||
|
ui::Color m_color = ui::WHITE;
|
||||||
|
SharedPtr<Font> m_font;
|
||||||
|
};
|
||||||
|
}
|
83
gui/libui/include/ui/Layout.h
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* @file Layout.h
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief Layout widgets to organize content.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2023, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <luna/Vector.h>
|
||||||
|
#include <ui/Widget.h>
|
||||||
|
|
||||||
|
namespace ui
|
||||||
|
{
|
||||||
|
enum class AdjustHeight
|
||||||
|
{
|
||||||
|
No,
|
||||||
|
Yes
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class AdjustWidth
|
||||||
|
{
|
||||||
|
No,
|
||||||
|
Yes
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Margins
|
||||||
|
{
|
||||||
|
int left;
|
||||||
|
int right;
|
||||||
|
int top;
|
||||||
|
int bottom;
|
||||||
|
};
|
||||||
|
|
||||||
|
class HorizontalLayout final : public Widget
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
HorizontalLayout(Margins margins = Margins { 0, 0, 0, 0 }, AdjustHeight adjust_height = AdjustHeight::Yes,
|
||||||
|
AdjustWidth adjust_width = AdjustWidth::Yes);
|
||||||
|
|
||||||
|
Result<EventResult> handle_mouse_move(Point position) override;
|
||||||
|
Result<EventResult> handle_mouse_leave() override;
|
||||||
|
Result<EventResult> handle_mouse_down(Point position, int buttons) override;
|
||||||
|
Result<EventResult> handle_mouse_up(Point position, int buttons) override;
|
||||||
|
Result<EventResult> handle_key_event(const ui::KeyEventRequest& request) override;
|
||||||
|
|
||||||
|
Result<void> draw(Canvas& canvas) override;
|
||||||
|
|
||||||
|
Result<void> add_widget(Widget& widget);
|
||||||
|
|
||||||
|
private:
|
||||||
|
Vector<Widget*> m_widgets;
|
||||||
|
Margins m_margins;
|
||||||
|
AdjustHeight m_adjust_height;
|
||||||
|
AdjustWidth m_adjust_width;
|
||||||
|
int m_used_width { 0 };
|
||||||
|
};
|
||||||
|
|
||||||
|
class VerticalLayout final : public Widget
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
VerticalLayout(Margins margins = Margins { 0, 0, 0, 0 }, AdjustHeight adjust_height = AdjustHeight::Yes,
|
||||||
|
AdjustWidth adjust_width = AdjustWidth::Yes);
|
||||||
|
|
||||||
|
Result<EventResult> handle_mouse_move(Point position) override;
|
||||||
|
Result<EventResult> handle_mouse_leave() override;
|
||||||
|
Result<EventResult> handle_mouse_down(Point position, int buttons) override;
|
||||||
|
Result<EventResult> handle_mouse_up(Point position, int buttons) override;
|
||||||
|
Result<EventResult> handle_key_event(const ui::KeyEventRequest& request) override;
|
||||||
|
|
||||||
|
Result<void> draw(Canvas& canvas) override;
|
||||||
|
|
||||||
|
Result<void> add_widget(Widget& widget);
|
||||||
|
|
||||||
|
private:
|
||||||
|
Vector<Widget*> m_widgets;
|
||||||
|
Margins m_margins;
|
||||||
|
AdjustHeight m_adjust_height;
|
||||||
|
AdjustWidth m_adjust_width;
|
||||||
|
int m_used_height { 0 };
|
||||||
|
};
|
||||||
|
}
|
21
gui/libui/include/ui/Mouse.h
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* @file Mouse.h
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief Mouse buttons.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2023, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <moon/Mouse.h>
|
||||||
|
|
||||||
|
namespace ui
|
||||||
|
{
|
||||||
|
enum MouseButtons
|
||||||
|
{
|
||||||
|
LEFT = moon::Left,
|
||||||
|
MIDDLE = moon::Middle,
|
||||||
|
RIGHT = moon::Right,
|
||||||
|
};
|
||||||
|
}
|
22
gui/libui/include/ui/Point.h
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* @file Point.h
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief 2D space points.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2023, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace ui
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @brief A point in 2D space.
|
||||||
|
*/
|
||||||
|
struct Point
|
||||||
|
{
|
||||||
|
int x { 0 };
|
||||||
|
int y { 0 };
|
||||||
|
};
|
||||||
|
}
|
81
gui/libui/include/ui/Rect.h
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
/**
|
||||||
|
* @file Rect.h
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief A simple 2D rectangle representation.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2023, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <ui/Point.h>
|
||||||
|
|
||||||
|
namespace ui
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @brief A simple rectangle.
|
||||||
|
*/
|
||||||
|
struct Rect
|
||||||
|
{
|
||||||
|
Point pos;
|
||||||
|
int width;
|
||||||
|
int height;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check if a point is contained in this rectangle.
|
||||||
|
*
|
||||||
|
* @param point The point to check.
|
||||||
|
* @return true The point is contained inside the rectangle.
|
||||||
|
* @return false The point is not contained inside the rectangle.
|
||||||
|
*/
|
||||||
|
bool contains(Point point);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check if another rectangle is contained in this one.
|
||||||
|
*
|
||||||
|
* @param point The rectangle to check.
|
||||||
|
* @return true The other rectangle is contained inside this one.
|
||||||
|
* @return false The other rectangle is not contained inside this one.
|
||||||
|
*/
|
||||||
|
bool contains(Rect rect);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Normalize a point to fit inside this rectangle.
|
||||||
|
*
|
||||||
|
* @param point The original point.
|
||||||
|
* @return Point The normalized point.
|
||||||
|
*/
|
||||||
|
Point normalize(Point point);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Transform an absolute position to a position relative to this rectangle.
|
||||||
|
*
|
||||||
|
* @param pos The original absolute position.
|
||||||
|
* @return Point The position relative to this rectangle.
|
||||||
|
*/
|
||||||
|
Point relative(Point pos);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Transform a position relative to this rectangle to an absolute position.
|
||||||
|
*
|
||||||
|
* @param pos The original relative position.
|
||||||
|
* @return Point The absolute position.
|
||||||
|
*/
|
||||||
|
Point absolute(Point pos);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Transform another rectangle relative to this one to an absolute rectangle.
|
||||||
|
*
|
||||||
|
* @param rect The original relative rectangle.
|
||||||
|
* @return Point The absolute rectangle.
|
||||||
|
*/
|
||||||
|
Rect absolute(Rect rect);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Return a copy of this rectangle with no negative values (normalized to 0).
|
||||||
|
*
|
||||||
|
* @return Rect The new rectangle.
|
||||||
|
*/
|
||||||
|
Rect normalized();
|
||||||
|
};
|
||||||
|
}
|
41
gui/libui/include/ui/TextInput.h
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* @file TextInput.h
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief Base class for text inputs.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2024, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <luna/Buffer.h>
|
||||||
|
#include <os/Timer.h>
|
||||||
|
#include <ui/Widget.h>
|
||||||
|
|
||||||
|
namespace ui
|
||||||
|
{
|
||||||
|
class TextInput : public Widget
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
TextInput();
|
||||||
|
|
||||||
|
virtual Result<ui::EventResult> handle_key_event(const ui::KeyEventRequest& request) = 0;
|
||||||
|
|
||||||
|
virtual Result<void> draw(ui::Canvas& canvas) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
Buffer m_data;
|
||||||
|
|
||||||
|
usize m_cursor { 0 };
|
||||||
|
ui::Point m_cursor_position { 0, 0 };
|
||||||
|
|
||||||
|
OwnedPtr<os::Timer> m_cursor_timer;
|
||||||
|
bool m_cursor_activated = true;
|
||||||
|
|
||||||
|
void tick_cursor();
|
||||||
|
void update_cursor();
|
||||||
|
|
||||||
|
Result<void> delete_current_character();
|
||||||
|
Result<void> insert_character(char c);
|
||||||
|
};
|
||||||
|
}
|
94
gui/libui/include/ui/Widget.h
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
/**
|
||||||
|
* @file Widget.h
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief Abstract widget class.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2023, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <luna/Ignore.h>
|
||||||
|
#include <luna/Result.h>
|
||||||
|
#include <ui/Canvas.h>
|
||||||
|
#include <ui/Point.h>
|
||||||
|
#include <ui/Rect.h>
|
||||||
|
#include <ui/ipc/Client.h>
|
||||||
|
|
||||||
|
namespace ui
|
||||||
|
{
|
||||||
|
class Window;
|
||||||
|
|
||||||
|
enum class EventResult
|
||||||
|
{
|
||||||
|
DidHandle,
|
||||||
|
DidNotHandle,
|
||||||
|
};
|
||||||
|
|
||||||
|
class Widget
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual Result<EventResult> handle_mouse_move(Point position)
|
||||||
|
{
|
||||||
|
ignore(position);
|
||||||
|
return EventResult::DidNotHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual Result<EventResult> handle_mouse_down(Point position, int buttons)
|
||||||
|
{
|
||||||
|
ignore(position, buttons);
|
||||||
|
return EventResult::DidNotHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual Result<EventResult> handle_mouse_up(Point position, int buttons)
|
||||||
|
{
|
||||||
|
ignore(position, buttons);
|
||||||
|
return EventResult::DidNotHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual Result<EventResult> handle_mouse_leave()
|
||||||
|
{
|
||||||
|
return EventResult::DidNotHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual Result<EventResult> handle_key_event(const ui::KeyEventRequest& request)
|
||||||
|
{
|
||||||
|
ignore(request);
|
||||||
|
return EventResult::DidNotHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual Result<void> draw(Canvas& canvas);
|
||||||
|
|
||||||
|
void set_window(Window* window, Rect rect, Badge<Window>)
|
||||||
|
{
|
||||||
|
m_window = window;
|
||||||
|
m_rect = rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_parent(Widget* parent)
|
||||||
|
{
|
||||||
|
m_parent = parent;
|
||||||
|
m_window = parent->m_window;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget* parent()
|
||||||
|
{
|
||||||
|
return m_parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
Window* window()
|
||||||
|
{
|
||||||
|
return m_window;
|
||||||
|
}
|
||||||
|
|
||||||
|
Rect& rect()
|
||||||
|
{
|
||||||
|
return m_rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
Widget* m_parent { nullptr };
|
||||||
|
Window* m_window;
|
||||||
|
Rect m_rect { 0, 0, 50, 50 };
|
||||||
|
};
|
||||||
|
}
|
115
gui/libui/include/ui/Window.h
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
/**
|
||||||
|
* @file Window.h
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief UI windows.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2023, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <luna/OwnedPtr.h>
|
||||||
|
#include <luna/String.h>
|
||||||
|
#include <luna/StringView.h>
|
||||||
|
#include <ui/Canvas.h>
|
||||||
|
#include <ui/Mouse.h>
|
||||||
|
#include <ui/Rect.h>
|
||||||
|
#include <ui/Widget.h>
|
||||||
|
#include <ui/ipc/Server.h>
|
||||||
|
|
||||||
|
namespace ui
|
||||||
|
{
|
||||||
|
enum class WindowType : u8
|
||||||
|
{
|
||||||
|
Normal,
|
||||||
|
NotDecorated,
|
||||||
|
System,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct [[gnu::packed]] Shortcut
|
||||||
|
{
|
||||||
|
moon::KeyCode key;
|
||||||
|
int modifiers;
|
||||||
|
|
||||||
|
bool operator==(const Shortcut& other) const
|
||||||
|
{
|
||||||
|
return key == other.key && modifiers == other.modifiers;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class Window
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static Result<Window*> create(Rect rect, WindowType type = WindowType::Normal);
|
||||||
|
|
||||||
|
void set_title(StringView title);
|
||||||
|
|
||||||
|
void set_background(Color color)
|
||||||
|
{
|
||||||
|
m_background = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_main_widget(Widget& widget)
|
||||||
|
{
|
||||||
|
check(!m_main_widget);
|
||||||
|
widget.set_window(this, m_window_canvas.rect(), {});
|
||||||
|
m_main_widget = &widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
Canvas& canvas()
|
||||||
|
{
|
||||||
|
return m_window_canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
void update();
|
||||||
|
|
||||||
|
void close();
|
||||||
|
|
||||||
|
void set_layer(Layer layer);
|
||||||
|
|
||||||
|
Result<void> draw();
|
||||||
|
Result<ui::EventResult> handle_mouse_leave();
|
||||||
|
Result<ui::EventResult> handle_mouse_move(ui::Point position);
|
||||||
|
Result<ui::EventResult> handle_mouse_buttons(ui::Point position, int buttons);
|
||||||
|
Result<ui::EventResult> handle_key_event(const ui::KeyEventRequest& request);
|
||||||
|
|
||||||
|
Result<void> add_keyboard_shortcut(ui::Shortcut shortcut, bool intercept, Function<ui::Shortcut>&& action);
|
||||||
|
|
||||||
|
int id() const
|
||||||
|
{
|
||||||
|
return m_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
void on_close(Action&& action)
|
||||||
|
{
|
||||||
|
m_on_close_action = move(action);
|
||||||
|
m_has_on_close_action = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
~Window();
|
||||||
|
|
||||||
|
private:
|
||||||
|
int m_id;
|
||||||
|
Canvas m_canvas;
|
||||||
|
Canvas m_titlebar_canvas;
|
||||||
|
Canvas m_window_canvas;
|
||||||
|
String m_name;
|
||||||
|
Widget* m_main_widget { nullptr };
|
||||||
|
Option<Color> m_background {};
|
||||||
|
Option<int> m_old_mouse_buttons;
|
||||||
|
bool m_decorated { false };
|
||||||
|
|
||||||
|
Action m_on_close_action;
|
||||||
|
bool m_has_on_close_action { false };
|
||||||
|
|
||||||
|
struct ShortcutAction
|
||||||
|
{
|
||||||
|
bool intercept;
|
||||||
|
Function<Shortcut> action;
|
||||||
|
};
|
||||||
|
|
||||||
|
HashMap<Shortcut, ShortcutAction> m_shortcuts;
|
||||||
|
|
||||||
|
Result<void> draw_titlebar();
|
||||||
|
};
|
||||||
|
}
|
72
gui/libui/include/ui/ipc/Client.h
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* @file ipc/Client.h
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief IPC message definitions for UI messages sent to the client.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2023, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <os/IPC.h>
|
||||||
|
#include <ui/Key.h>
|
||||||
|
#include <ui/Point.h>
|
||||||
|
#include <ui/Rect.h>
|
||||||
|
|
||||||
|
namespace ui
|
||||||
|
{
|
||||||
|
enum ClientMessages : u8
|
||||||
|
{
|
||||||
|
IPC_ENUM_CLIENT(ui),
|
||||||
|
CREATE_WINDOW_RESPONSE_ID,
|
||||||
|
MOUSE_EVENT_REQUEST_ID,
|
||||||
|
MOUSE_LEAVE_REQUEST_ID,
|
||||||
|
GET_SCREEN_RECT_RESPONSE_ID,
|
||||||
|
KEY_EVENT_REQUEST_ID,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CreateWindowResponse
|
||||||
|
{
|
||||||
|
static constexpr u8 ID = CREATE_WINDOW_RESPONSE_ID;
|
||||||
|
|
||||||
|
int window;
|
||||||
|
IPC_STRING(shm_path);
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MouseEventRequest
|
||||||
|
{
|
||||||
|
static constexpr u8 ID = MOUSE_EVENT_REQUEST_ID;
|
||||||
|
|
||||||
|
int window;
|
||||||
|
Point position;
|
||||||
|
int buttons;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MouseLeaveRequest
|
||||||
|
{
|
||||||
|
static constexpr u8 ID = MOUSE_LEAVE_REQUEST_ID;
|
||||||
|
|
||||||
|
int window;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct GetScreenRectResponse
|
||||||
|
{
|
||||||
|
static constexpr u8 ID = GET_SCREEN_RECT_RESPONSE_ID;
|
||||||
|
|
||||||
|
Rect rect;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct KeyEventRequest
|
||||||
|
{
|
||||||
|
static constexpr u8 ID = KEY_EVENT_REQUEST_ID;
|
||||||
|
|
||||||
|
int window;
|
||||||
|
|
||||||
|
bool pressed;
|
||||||
|
|
||||||
|
char letter;
|
||||||
|
char key;
|
||||||
|
moon::KeyCode code;
|
||||||
|
int modifiers;
|
||||||
|
};
|
||||||
|
}
|
113
gui/libui/include/ui/ipc/Server.h
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
/**
|
||||||
|
* @file ipc/Server.h
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief IPC message definitions for UI messages sent to the server.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2023, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <os/IPC.h>
|
||||||
|
#include <ui/Color.h>
|
||||||
|
#include <ui/Rect.h>
|
||||||
|
#include <ui/ipc/Client.h>
|
||||||
|
|
||||||
|
namespace ui
|
||||||
|
{
|
||||||
|
enum ServerMessages : u8
|
||||||
|
{
|
||||||
|
IPC_ENUM_SERVER(ui),
|
||||||
|
CREATE_WINDOW_ID,
|
||||||
|
REMOVE_SHM_ID,
|
||||||
|
SET_WINDOW_TITLE_ID,
|
||||||
|
INVALIDATE_ID,
|
||||||
|
CLOSE_WINDOW_ID,
|
||||||
|
GET_SCREEN_RECT_ID,
|
||||||
|
SET_TITLEBAR_HEIGHT_ID,
|
||||||
|
SET_WINDOW_LAYER_ID,
|
||||||
|
UPDATE_PLEDGE_REQUEST_ID,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CreateWindowRequest
|
||||||
|
{
|
||||||
|
using ResponseType = CreateWindowResponse;
|
||||||
|
static constexpr u8 ID = CREATE_WINDOW_ID;
|
||||||
|
|
||||||
|
ui::Rect rect;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RemoveSharedMemoryRequest
|
||||||
|
{
|
||||||
|
static constexpr u8 ID = REMOVE_SHM_ID;
|
||||||
|
|
||||||
|
int window;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SetWindowTitleRequest
|
||||||
|
{
|
||||||
|
static constexpr u8 ID = SET_WINDOW_TITLE_ID;
|
||||||
|
|
||||||
|
int window;
|
||||||
|
IPC_STRING(title);
|
||||||
|
};
|
||||||
|
|
||||||
|
struct InvalidateRequest
|
||||||
|
{
|
||||||
|
static constexpr u8 ID = INVALIDATE_ID;
|
||||||
|
|
||||||
|
int window;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CloseWindowRequest
|
||||||
|
{
|
||||||
|
static constexpr u8 ID = CLOSE_WINDOW_ID;
|
||||||
|
|
||||||
|
int window;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct GetScreenRectRequest
|
||||||
|
{
|
||||||
|
using ResponseType = GetScreenRectResponse;
|
||||||
|
static constexpr u8 ID = GET_SCREEN_RECT_ID;
|
||||||
|
|
||||||
|
int _shadow; // Unused.
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SetTitlebarHeightRequest
|
||||||
|
{
|
||||||
|
static constexpr u8 ID = SET_TITLEBAR_HEIGHT_ID;
|
||||||
|
|
||||||
|
int window;
|
||||||
|
int height;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum Layer : u8
|
||||||
|
{
|
||||||
|
Background,
|
||||||
|
Global,
|
||||||
|
GlobalTop,
|
||||||
|
System,
|
||||||
|
Lock
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SetWindowLayer
|
||||||
|
{
|
||||||
|
static constexpr u8 ID = SET_WINDOW_LAYER_ID;
|
||||||
|
|
||||||
|
int window;
|
||||||
|
Layer layer;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum Pledge : i16
|
||||||
|
{
|
||||||
|
ExtendedLayers = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UpdatePledgeRequest
|
||||||
|
{
|
||||||
|
static constexpr u8 ID = UPDATE_PLEDGE_REQUEST_ID;
|
||||||
|
|
||||||
|
i16 pledges;
|
||||||
|
};
|
||||||
|
}
|
40
gui/libui/src/Alignment.cpp
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* @file Alignment.cpp
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief UI component alignment.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2023, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <ui/Alignment.h>
|
||||||
|
|
||||||
|
namespace ui
|
||||||
|
{
|
||||||
|
Rect align(Rect container, Rect contained, VerticalAlignment valign, HorizontalAlignment halign)
|
||||||
|
{
|
||||||
|
Rect result;
|
||||||
|
result.width = contained.width;
|
||||||
|
result.height = contained.height;
|
||||||
|
result.pos.y = container.pos.y;
|
||||||
|
result.pos.x = container.pos.x;
|
||||||
|
|
||||||
|
switch (valign)
|
||||||
|
{
|
||||||
|
case VerticalAlignment::Top: break;
|
||||||
|
case VerticalAlignment::Center: result.pos.y += (container.height - contained.height) / 2; break;
|
||||||
|
case VerticalAlignment::Bottom: result.pos.y += container.height - contained.height; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (halign)
|
||||||
|
{
|
||||||
|
case HorizontalAlignment::Left: break;
|
||||||
|
case HorizontalAlignment::Center: result.pos.x += (container.width - contained.width) / 2; break;
|
||||||
|
case HorizontalAlignment::Right: result.pos.x += container.width - contained.width; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
145
gui/libui/src/App.cpp
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
/**
|
||||||
|
* @file App.cpp
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief UI application event loop.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2023, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <os/ArgumentParser.h>
|
||||||
|
#include <os/File.h>
|
||||||
|
#include <os/IPC.h>
|
||||||
|
#include <ui/App.h>
|
||||||
|
#include <ui/ipc/Client.h>
|
||||||
|
#include <ui/ipc/Server.h>
|
||||||
|
|
||||||
|
namespace ui
|
||||||
|
{
|
||||||
|
void handle_socket_event(int, int status)
|
||||||
|
{
|
||||||
|
if (status & POLLHUP) ui::App::the().set_should_close(true);
|
||||||
|
if (status & POLLIN) { ui::App::the().process_events(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
App* App::s_app { nullptr };
|
||||||
|
|
||||||
|
App::App()
|
||||||
|
{
|
||||||
|
s_app = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
App::~App()
|
||||||
|
{
|
||||||
|
s_app = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void> App::init(StringView socket_path)
|
||||||
|
{
|
||||||
|
m_client = TRY(os::IPC::Client::connect(socket_path, false));
|
||||||
|
m_client->set_message_handler(
|
||||||
|
[this](os::IPC::Client& client, u8 id, void* arg) { this->handle_ipc_event(client, id, arg); }, nullptr);
|
||||||
|
|
||||||
|
TRY(m_loop.register_fd_listener(m_client->fd(), handle_socket_event));
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<int> App::run()
|
||||||
|
{
|
||||||
|
TRY(m_main_window->draw());
|
||||||
|
|
||||||
|
return m_loop.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
App& App::the()
|
||||||
|
{
|
||||||
|
check(s_app);
|
||||||
|
return *s_app;
|
||||||
|
}
|
||||||
|
|
||||||
|
Rect App::screen_rect()
|
||||||
|
{
|
||||||
|
ui::GetScreenRectRequest request {};
|
||||||
|
auto response = m_client->send_sync<ui::GetScreenRectResponse>(request).release_value();
|
||||||
|
return response.rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void> App::register_window(OwnedPtr<Window>&& window, Badge<Window>)
|
||||||
|
{
|
||||||
|
int id = window->id();
|
||||||
|
check(TRY(m_windows.try_set(id, move(window))));
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::unregister_window(Window* window, Badge<Window>)
|
||||||
|
{
|
||||||
|
int id = window->id();
|
||||||
|
m_window_clear_queue.try_append(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Window* App::find_window(int id)
|
||||||
|
{
|
||||||
|
auto* window = m_windows.try_get_ref(id);
|
||||||
|
check(window);
|
||||||
|
return window->ptr();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void> App::handle_ipc_event(os::IPC::Client&, u8 id, void*)
|
||||||
|
{
|
||||||
|
switch (id)
|
||||||
|
{
|
||||||
|
case MOUSE_EVENT_REQUEST_ID: {
|
||||||
|
MouseEventRequest request;
|
||||||
|
if (!TRY(m_client->read_message(request))) return {};
|
||||||
|
auto* window = find_window(request.window);
|
||||||
|
auto move_result = window->handle_mouse_move(request.position).value_or(ui::EventResult::DidNotHandle);
|
||||||
|
auto button_result =
|
||||||
|
window->handle_mouse_buttons(request.position, request.buttons).value_or(ui::EventResult::DidNotHandle);
|
||||||
|
if (move_result == ui::EventResult::DidHandle || button_result == ui::EventResult::DidHandle)
|
||||||
|
window->draw();
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
case MOUSE_LEAVE_REQUEST_ID: {
|
||||||
|
MouseLeaveRequest request;
|
||||||
|
if (!TRY(m_client->read_message(request))) return {};
|
||||||
|
auto* window = find_window(request.window);
|
||||||
|
if (window->handle_mouse_leave().value_or(ui::EventResult::DidNotHandle) == ui::EventResult::DidHandle)
|
||||||
|
window->draw();
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
case KEY_EVENT_REQUEST_ID: {
|
||||||
|
KeyEventRequest request;
|
||||||
|
if (!TRY(m_client->read_message(request))) return {};
|
||||||
|
auto* window = find_window(request.window);
|
||||||
|
if (window->handle_key_event(request).value_or(ui::EventResult::DidNotHandle) == ui::EventResult::DidHandle)
|
||||||
|
window->draw();
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
default: fail("Unexpected IPC request from server!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool App::process_events()
|
||||||
|
{
|
||||||
|
check(m_main_window);
|
||||||
|
m_client->check_for_messages().release_value();
|
||||||
|
for (int id : m_window_clear_queue)
|
||||||
|
{
|
||||||
|
check(m_windows.try_remove(id));
|
||||||
|
|
||||||
|
ui::CloseWindowRequest request;
|
||||||
|
request.window = id;
|
||||||
|
client().send_async(request);
|
||||||
|
}
|
||||||
|
m_window_clear_queue.clear_data();
|
||||||
|
return !m_should_close;
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::pledge(i16 pledges)
|
||||||
|
{
|
||||||
|
ui::UpdatePledgeRequest request;
|
||||||
|
request.pledges = pledges;
|
||||||
|
client().send_async(request);
|
||||||
|
}
|
||||||
|
}
|
73
gui/libui/src/Button.cpp
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* @file Button.cpp
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief A clickable component that triggers an action when pressed.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2023, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <ui/Button.h>
|
||||||
|
#include <ui/Mouse.h>
|
||||||
|
|
||||||
|
namespace ui
|
||||||
|
{
|
||||||
|
Button::Button(Rect rect)
|
||||||
|
{
|
||||||
|
m_rect = rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Button::set_widget(Widget& widget)
|
||||||
|
{
|
||||||
|
widget.rect() = m_rect;
|
||||||
|
m_child = &widget;
|
||||||
|
widget.set_parent(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Button::set_action(Action&& action)
|
||||||
|
{
|
||||||
|
m_action = move(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<EventResult> Button::handle_mouse_move(Point position)
|
||||||
|
{
|
||||||
|
m_hovered = true;
|
||||||
|
return m_child->handle_mouse_move(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<EventResult> Button::handle_mouse_leave()
|
||||||
|
{
|
||||||
|
m_hovered = m_clicked = false;
|
||||||
|
return m_child->handle_mouse_leave();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<EventResult> Button::handle_mouse_down(Point position, int buttons)
|
||||||
|
{
|
||||||
|
auto result = TRY(m_child->handle_mouse_down(position, buttons));
|
||||||
|
if (result == EventResult::DidNotHandle)
|
||||||
|
{
|
||||||
|
if (!m_clicked && (buttons == ui::MouseButtons::LEFT))
|
||||||
|
{
|
||||||
|
m_clicked = true;
|
||||||
|
m_action();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return EventResult::DidHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<EventResult> Button::handle_mouse_up(Point position, int buttons)
|
||||||
|
{
|
||||||
|
if (buttons & ui::MouseButtons::LEFT) m_clicked = false;
|
||||||
|
return m_child->handle_mouse_up(position, buttons);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<EventResult> Button::handle_key_event(const ui::KeyEventRequest& request)
|
||||||
|
{
|
||||||
|
return m_child->handle_key_event(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void> Button::draw(Canvas& canvas)
|
||||||
|
{
|
||||||
|
return m_child->draw(canvas);
|
||||||
|
}
|
||||||
|
}
|
75
gui/libui/src/Canvas.cpp
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* @file Canvas.cpp
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief Drawable surfaces.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2023, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <luna/CString.h>
|
||||||
|
#include <ui/Canvas.h>
|
||||||
|
|
||||||
|
namespace ui
|
||||||
|
{
|
||||||
|
Canvas Canvas::create(u8* ptr, int width, int height)
|
||||||
|
{
|
||||||
|
return Canvas { .width = width, .height = height, .stride = width, .ptr = ptr };
|
||||||
|
}
|
||||||
|
|
||||||
|
Canvas Canvas::subcanvas(Rect rect)
|
||||||
|
{
|
||||||
|
if (rect.pos.x < 0) rect.pos.x = 0;
|
||||||
|
if (rect.pos.y < 0) rect.pos.y = 0;
|
||||||
|
if (rect.pos.x + rect.width > width) rect.width = width - rect.pos.x;
|
||||||
|
if (rect.pos.y + rect.height > height) rect.height = height - rect.pos.y;
|
||||||
|
|
||||||
|
u8* p = ptr + rect.pos.x * sizeof(Color) + (rect.pos.y * sizeof(Color) * stride);
|
||||||
|
|
||||||
|
return Canvas { .width = rect.width, .height = rect.height, .stride = stride, .ptr = p };
|
||||||
|
}
|
||||||
|
|
||||||
|
void Canvas::fill(Color color)
|
||||||
|
{
|
||||||
|
u8* p = ptr;
|
||||||
|
for (int i = 0; i < height; i++)
|
||||||
|
{
|
||||||
|
u32* colorp = (u32*)p;
|
||||||
|
for (int j = 0; j < width; j++)
|
||||||
|
{
|
||||||
|
*colorp = color.raw;
|
||||||
|
colorp++;
|
||||||
|
}
|
||||||
|
p += stride * sizeof(Color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Canvas::fill(u32* pixels, int _stride)
|
||||||
|
{
|
||||||
|
u8* p = ptr;
|
||||||
|
for (int i = 0; i < height; i++)
|
||||||
|
{
|
||||||
|
u32* colorp = (u32*)p;
|
||||||
|
for (int j = 0; j < width; j++)
|
||||||
|
{
|
||||||
|
u32 pix = pixels[j];
|
||||||
|
if (Color::from_u32(pix).alpha() == 0xff) *colorp = pix;
|
||||||
|
colorp++;
|
||||||
|
}
|
||||||
|
pixels += _stride;
|
||||||
|
p += stride * sizeof(Color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Canvas::copy(u32* pixels, int _stride)
|
||||||
|
{
|
||||||
|
u8* p = ptr;
|
||||||
|
for (int i = 0; i < height; i++)
|
||||||
|
{
|
||||||
|
u32* colorp = (u32*)p;
|
||||||
|
memcpy(colorp, pixels, width * sizeof(u32));
|
||||||
|
pixels += _stride;
|
||||||
|
p += stride * sizeof(Color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
63
gui/libui/src/Container.cpp
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* @file Container.cpp
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief A container widget to pad and align objects inside it.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2023, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <ui/Container.h>
|
||||||
|
|
||||||
|
namespace ui
|
||||||
|
{
|
||||||
|
Container::Container(Rect rect, VerticalAlignment valign, HorizontalAlignment halign)
|
||||||
|
: m_valign(valign), m_halign(halign)
|
||||||
|
{
|
||||||
|
m_rect = rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Container::set_widget(Widget& widget)
|
||||||
|
{
|
||||||
|
m_widget = &widget;
|
||||||
|
widget.rect() = ui::align(m_rect, widget.rect(), m_valign, m_halign);
|
||||||
|
widget.set_parent(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<EventResult> Container::handle_mouse_move(Point position)
|
||||||
|
{
|
||||||
|
if (m_widget->rect().contains(position)) return m_widget->handle_mouse_move(position);
|
||||||
|
return ui::EventResult::DidNotHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<EventResult> Container::handle_mouse_leave()
|
||||||
|
{
|
||||||
|
return m_widget->handle_mouse_leave();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<EventResult> Container::handle_mouse_down(Point position, int buttons)
|
||||||
|
{
|
||||||
|
if (m_widget->rect().contains(position)) return m_widget->handle_mouse_down(position, buttons);
|
||||||
|
return ui::EventResult::DidNotHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<EventResult> Container::handle_mouse_up(Point position, int buttons)
|
||||||
|
{
|
||||||
|
if (m_widget->rect().contains(position)) return m_widget->handle_mouse_up(position, buttons);
|
||||||
|
return ui::EventResult::DidNotHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<EventResult> Container::handle_key_event(const ui::KeyEventRequest& request)
|
||||||
|
{
|
||||||
|
return m_widget->handle_key_event(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void> Container::draw(Canvas& canvas)
|
||||||
|
{
|
||||||
|
auto rect = ui::Rect { m_widget->rect().pos.x - m_rect.pos.x, m_widget->rect().pos.y - m_rect.pos.y,
|
||||||
|
m_widget->rect().width, m_widget->rect().height };
|
||||||
|
auto subcanvas = canvas.subcanvas(rect);
|
||||||
|
return m_widget->draw(subcanvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
82
gui/libui/src/Dialog.cpp
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* @file Dialog.cpp
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief UI window dialogs.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2024, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <luna/Alloc.h>
|
||||||
|
#include <ui/App.h>
|
||||||
|
#include <ui/Dialog.h>
|
||||||
|
#include <ui/InputField.h>
|
||||||
|
#include <ui/Label.h>
|
||||||
|
#include <ui/Layout.h>
|
||||||
|
|
||||||
|
namespace ui::Dialog
|
||||||
|
{
|
||||||
|
Result<void> show_message(StringView title, StringView message)
|
||||||
|
{
|
||||||
|
auto rect = ui::App::the().main_window()->canvas().rect();
|
||||||
|
int text_length = (int)message.length() * ui::Font::default_font()->width();
|
||||||
|
int text_height = ui::Font::default_font()->height();
|
||||||
|
|
||||||
|
ui::Rect dialog_rect = { 0, 0, text_length + 20, text_height + 20 };
|
||||||
|
|
||||||
|
auto* dialog = TRY(ui::Window::create(
|
||||||
|
ui::align(rect, dialog_rect, ui::VerticalAlignment::Center, ui::HorizontalAlignment::Center)));
|
||||||
|
|
||||||
|
dialog->set_background(ui::GRAY);
|
||||||
|
dialog->set_title(title);
|
||||||
|
|
||||||
|
ui::Label* text = TRY(make<ui::Label>(message));
|
||||||
|
text->set_color(ui::BLACK);
|
||||||
|
dialog->set_main_widget(*text);
|
||||||
|
|
||||||
|
dialog->on_close([text] { delete text; });
|
||||||
|
|
||||||
|
dialog->draw();
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void> show_input_dialog(StringView title, StringView message, Function<StringView> callback)
|
||||||
|
{
|
||||||
|
auto rect = ui::App::the().main_window()->canvas().rect();
|
||||||
|
int text_length = (int)message.length() * ui::Font::default_font()->width();
|
||||||
|
int text_height = ui::Font::default_font()->height();
|
||||||
|
|
||||||
|
ui::Rect dialog_rect = { 0, 0, max(text_length + 20, 300), text_height * 2 + 30 };
|
||||||
|
|
||||||
|
auto* dialog = TRY(ui::Window::create(
|
||||||
|
ui::align(rect, dialog_rect, ui::VerticalAlignment::Center, ui::HorizontalAlignment::Center)));
|
||||||
|
|
||||||
|
dialog->set_background(ui::GRAY);
|
||||||
|
dialog->set_title(title);
|
||||||
|
|
||||||
|
ui::VerticalLayout* layout = TRY(make<ui::VerticalLayout>());
|
||||||
|
dialog->set_main_widget(*layout);
|
||||||
|
|
||||||
|
ui::Label* text = TRY(make<ui::Label>((message)));
|
||||||
|
text->set_color(ui::BLACK);
|
||||||
|
layout->add_widget(*text);
|
||||||
|
|
||||||
|
ui::InputField* input = TRY(make<ui::InputField>(ui::Font::default_font()));
|
||||||
|
input->on_submit([dialog, callback](StringView s) {
|
||||||
|
callback(s);
|
||||||
|
dialog->close();
|
||||||
|
});
|
||||||
|
layout->add_widget(*input);
|
||||||
|
|
||||||
|
dialog->on_close([layout, text, input] {
|
||||||
|
delete text;
|
||||||
|
delete input;
|
||||||
|
delete layout;
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog->draw();
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
121
gui/libui/src/Font.cpp
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
/**
|
||||||
|
* @file Font.cpp
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief PSF font loading and rendering.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2023, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <luna/RefString.h>
|
||||||
|
#include <os/File.h>
|
||||||
|
#include <ui/Font.h>
|
||||||
|
|
||||||
|
constexpr static int BYTES_PER_PIXEL = (int)sizeof(ui::Color);
|
||||||
|
|
||||||
|
namespace ui
|
||||||
|
{
|
||||||
|
Result<SharedPtr<Font>> Font::load(const os::Path& path)
|
||||||
|
{
|
||||||
|
auto font = TRY(make_shared<Font>());
|
||||||
|
|
||||||
|
auto file = TRY(os::File::open(path, os::File::ReadOnly));
|
||||||
|
|
||||||
|
TRY(file->read_typed(font->m_psf_header));
|
||||||
|
|
||||||
|
if (font->m_psf_header.magic != PSF_FONT_MAGIC)
|
||||||
|
{
|
||||||
|
os::eprintln("ui::Font::load(%s) failed: font magic does not match PSF2 magic", path.name().chars());
|
||||||
|
return err(ENOTSUP);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (font->m_psf_header.version != 0)
|
||||||
|
{
|
||||||
|
os::eprintln("ui::Font::load(%s) failed: font version is unsupported", path.name().chars());
|
||||||
|
return err(ENOTSUP);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (font->m_psf_header.flags)
|
||||||
|
{
|
||||||
|
os::eprintln("ui::Font::load(%s) warning: font has a unicode table, which we're ignoring",
|
||||||
|
path.name().chars());
|
||||||
|
// todo(); // Font has a unicode table, oh no!
|
||||||
|
}
|
||||||
|
|
||||||
|
font->m_font_data = TRY(file->read_all()); // Read the rest of the file into the font data buffer.
|
||||||
|
|
||||||
|
return font;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<SharedPtr<Font>> Font::load_builtin(StringView name, FontWeight weight)
|
||||||
|
{
|
||||||
|
auto path = TRY(RefString::format("/usr/share/fonts/%s-%s.psf"_sv, name.chars(),
|
||||||
|
weight == FontWeight::Bold ? "Bold" : "Regular"));
|
||||||
|
|
||||||
|
return load(path.view());
|
||||||
|
}
|
||||||
|
|
||||||
|
SharedPtr<Font> Font::default_font()
|
||||||
|
{
|
||||||
|
static SharedPtr<ui::Font> s_default_font = {};
|
||||||
|
if (!s_default_font) s_default_font = load("/usr/share/fonts/Tamsyn-Regular.psf").release_value();
|
||||||
|
return s_default_font;
|
||||||
|
}
|
||||||
|
|
||||||
|
SharedPtr<Font> Font::default_bold_font()
|
||||||
|
{
|
||||||
|
static SharedPtr<ui::Font> s_default_bold_font = {};
|
||||||
|
if (!s_default_bold_font) s_default_bold_font = load("/usr/share/fonts/Tamsyn-Bold.psf").release_value();
|
||||||
|
return s_default_bold_font;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Font::render(wchar_t codepoint, ui::Color color, ui::Canvas& canvas)
|
||||||
|
{
|
||||||
|
const wchar_t str[] = { codepoint, 0 };
|
||||||
|
render(str, color, canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Font::render(const wchar_t* text, ui::Color color, ui::Canvas& canvas)
|
||||||
|
{
|
||||||
|
usize len = wcslen(text);
|
||||||
|
|
||||||
|
int height = m_psf_header.height;
|
||||||
|
int width = m_psf_header.width;
|
||||||
|
int last_char_width = width;
|
||||||
|
|
||||||
|
if (canvas.width < (m_psf_header.width * static_cast<int>(len)))
|
||||||
|
{
|
||||||
|
len = (canvas.width / width) + 1;
|
||||||
|
last_char_width = canvas.width % width;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canvas.height < height) height = canvas.height;
|
||||||
|
|
||||||
|
const int bytes_per_line = (m_psf_header.width + 7) / 8;
|
||||||
|
|
||||||
|
for (usize i = 0; i < len; i++)
|
||||||
|
{
|
||||||
|
if (i + 1 == len) width = last_char_width;
|
||||||
|
wchar_t codepoint = text[i];
|
||||||
|
|
||||||
|
u8* glyph =
|
||||||
|
m_font_data.data() + (codepoint > 0 && codepoint < (wchar_t)m_psf_header.numglyph ? codepoint : 0) *
|
||||||
|
m_psf_header.bytesperglyph;
|
||||||
|
|
||||||
|
u32 offset = (u32)i * m_psf_header.width * BYTES_PER_PIXEL;
|
||||||
|
for (int y = 0; y < height; y++)
|
||||||
|
{
|
||||||
|
u32 line = offset;
|
||||||
|
int mask = 1 << (m_psf_header.width - 1);
|
||||||
|
for (int x = 0; x < width; x++)
|
||||||
|
{
|
||||||
|
if (*((u32*)glyph) & mask) *(u32*)(canvas.ptr + line) = color.raw;
|
||||||
|
mask >>= 1;
|
||||||
|
line += BYTES_PER_PIXEL;
|
||||||
|
}
|
||||||
|
glyph += bytes_per_line;
|
||||||
|
offset += canvas.stride * BYTES_PER_PIXEL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
48
gui/libui/src/Image.cpp
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* @file Image.cpp
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief TGA image loading and rendering.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2023, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <os/File.h>
|
||||||
|
#include <ui/Alignment.h>
|
||||||
|
#include <ui/Image.h>
|
||||||
|
|
||||||
|
namespace ui
|
||||||
|
{
|
||||||
|
Result<SharedPtr<Image>> Image::load(const os::Path& path)
|
||||||
|
{
|
||||||
|
auto image = TRY(make_shared<Image>());
|
||||||
|
auto file = TRY(os::File::open(path, os::File::ReadOnly));
|
||||||
|
|
||||||
|
TRY(file->read_typed(image->m_tga_header));
|
||||||
|
|
||||||
|
if (image->m_tga_header.encoding != 2) todo();
|
||||||
|
if (image->m_tga_header.bpp != 32) todo();
|
||||||
|
|
||||||
|
Buffer image_id;
|
||||||
|
TRY(file->read(image_id, image->m_tga_header.idlen));
|
||||||
|
|
||||||
|
TRY(file->read(image->m_image_data,
|
||||||
|
image->m_tga_header.w * image->m_tga_header.h * (image->m_tga_header.bpp / 8)));
|
||||||
|
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<OwnedPtr<ImageWidget>> ImageWidget::load(const os::Path& path)
|
||||||
|
{
|
||||||
|
auto widget = TRY(make_owned<ImageWidget>());
|
||||||
|
widget->m_image = TRY(Image::load(path));
|
||||||
|
widget->m_rect = { 0, 0, widget->m_image->width(), widget->m_image->height() };
|
||||||
|
return widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void> ImageWidget::draw(Canvas& canvas)
|
||||||
|
{
|
||||||
|
canvas.subcanvas({ 0, 0, m_image->width(), m_image->height() }).fill(m_image->pixels(), m_image->width());
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
121
gui/libui/src/InputField.cpp
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
/**
|
||||||
|
* @file InputField.cpp
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief Single line text input widget.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2024, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <luna/String.h>
|
||||||
|
#include <luna/StringView.h>
|
||||||
|
#include <luna/Utf8.h>
|
||||||
|
#include <ui/InputField.h>
|
||||||
|
|
||||||
|
namespace ui
|
||||||
|
{
|
||||||
|
InputField::InputField(SharedPtr<ui::Font> font) : ui::TextInput(), m_font(font)
|
||||||
|
{
|
||||||
|
u8 zero = 0;
|
||||||
|
m_data.append_data(&zero, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<ui::EventResult> InputField::handle_key_event(const ui::KeyEventRequest& request)
|
||||||
|
{
|
||||||
|
// Avoid handling "key released" events
|
||||||
|
if (!request.pressed) return ui::EventResult::DidNotHandle;
|
||||||
|
|
||||||
|
if (request.code == moon::K_LeftArrow)
|
||||||
|
{
|
||||||
|
if (m_cursor > 0) m_cursor--;
|
||||||
|
else
|
||||||
|
return ui::EventResult::DidNotHandle;
|
||||||
|
update_cursor();
|
||||||
|
return ui::EventResult::DidHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.code == moon::K_RightArrow)
|
||||||
|
{
|
||||||
|
if (m_cursor < (m_data.size() - 1)) m_cursor++;
|
||||||
|
else
|
||||||
|
return ui::EventResult::DidNotHandle;
|
||||||
|
update_cursor();
|
||||||
|
return ui::EventResult::DidHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.code == moon::K_Backspace)
|
||||||
|
{
|
||||||
|
if (m_cursor == 0) return ui::EventResult::DidNotHandle;
|
||||||
|
m_cursor--;
|
||||||
|
|
||||||
|
delete_current_character();
|
||||||
|
|
||||||
|
update_cursor();
|
||||||
|
|
||||||
|
return ui::EventResult::DidHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.letter == '\n')
|
||||||
|
{
|
||||||
|
if (m_has_on_submit_action)
|
||||||
|
{
|
||||||
|
m_on_submit_action(data());
|
||||||
|
return ui::EventResult::DidHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ui::EventResult::DidNotHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (iscntrl(request.letter)) return ui::EventResult::DidNotHandle;
|
||||||
|
|
||||||
|
TRY(insert_character(request.letter));
|
||||||
|
|
||||||
|
m_cursor++;
|
||||||
|
|
||||||
|
update_cursor();
|
||||||
|
|
||||||
|
return ui::EventResult::DidHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputField::clear()
|
||||||
|
{
|
||||||
|
m_data.try_resize(0);
|
||||||
|
u8 zero = 0;
|
||||||
|
m_data.append_data(&zero, 1);
|
||||||
|
m_cursor = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void> InputField::draw(ui::Canvas& canvas)
|
||||||
|
{
|
||||||
|
int visible_characters = canvas.width / m_font->width();
|
||||||
|
|
||||||
|
auto string = data();
|
||||||
|
|
||||||
|
Utf8StringDecoder decoder(string.chars());
|
||||||
|
wchar_t buf[4096];
|
||||||
|
decoder.decode(buf, sizeof(buf)).release_value();
|
||||||
|
|
||||||
|
int characters_to_render = (int)wcslen(buf);
|
||||||
|
|
||||||
|
for (int j = 0; j < visible_characters && j < characters_to_render; j++)
|
||||||
|
{
|
||||||
|
auto subcanvas = canvas.subcanvas({ j * m_font->width(), 0, m_font->width(), m_font->height() });
|
||||||
|
m_font->render(buf[j], ui::WHITE, subcanvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the cursor
|
||||||
|
if ((int)m_cursor < visible_characters && m_cursor_activated)
|
||||||
|
{
|
||||||
|
canvas.subcanvas({ (int)m_cursor * m_font->width(), 0, 1, m_font->height() }).fill(ui::WHITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
StringView InputField::data()
|
||||||
|
{
|
||||||
|
if (m_data.size() < 2) return StringView {};
|
||||||
|
return StringView { (const char*)m_data.data(), m_data.size() - 1 };
|
||||||
|
}
|
||||||
|
}
|
36
gui/libui/src/Label.cpp
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* @file Label.cpp
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief A simple one-line text widget.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2023, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <luna/Utf8.h>
|
||||||
|
#include <ui/Label.h>
|
||||||
|
|
||||||
|
namespace ui
|
||||||
|
{
|
||||||
|
Label::Label(StringView text) : m_text(text)
|
||||||
|
{
|
||||||
|
m_font = ui::Font::default_font();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void> Label::draw(Canvas& canvas)
|
||||||
|
{
|
||||||
|
ui::Rect contained;
|
||||||
|
contained.pos = { 0, 0 };
|
||||||
|
contained.width = static_cast<int>(m_text.length() * m_font->width());
|
||||||
|
contained.height = m_font->height();
|
||||||
|
auto subcanvas =
|
||||||
|
canvas.subcanvas(ui::align({ 0, 0, m_rect.width, m_rect.height }, contained, m_valign, m_halign));
|
||||||
|
|
||||||
|
Utf8StringDecoder decoder(m_text.chars());
|
||||||
|
wchar_t buf[4096];
|
||||||
|
TRY(decoder.decode(buf, sizeof(buf)));
|
||||||
|
|
||||||
|
m_font->render(buf, m_color, subcanvas);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
236
gui/libui/src/Layout.cpp
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
/**
|
||||||
|
* @file Layout.cpp
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief Layout widgets to organize content.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2023, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <ui/Layout.h>
|
||||||
|
|
||||||
|
namespace ui
|
||||||
|
{
|
||||||
|
HorizontalLayout::HorizontalLayout(Margins margins, AdjustHeight adjust_height, AdjustWidth adjust_width)
|
||||||
|
: m_margins(margins), m_adjust_height(adjust_height), m_adjust_width(adjust_width)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<EventResult> HorizontalLayout::handle_mouse_move(Point position)
|
||||||
|
{
|
||||||
|
EventResult result = ui::EventResult::DidNotHandle;
|
||||||
|
|
||||||
|
for (auto widget : m_widgets)
|
||||||
|
{
|
||||||
|
if (widget->rect().contains(position)) result = TRY(widget->handle_mouse_move(position));
|
||||||
|
else
|
||||||
|
TRY(widget->handle_mouse_leave());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<EventResult> HorizontalLayout::handle_mouse_leave()
|
||||||
|
{
|
||||||
|
EventResult result = ui::EventResult::DidNotHandle;
|
||||||
|
|
||||||
|
for (auto widget : m_widgets)
|
||||||
|
{
|
||||||
|
auto rc = TRY(widget->handle_mouse_leave());
|
||||||
|
if (rc == ui::EventResult::DidHandle) result = rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<EventResult> HorizontalLayout::handle_mouse_up(Point position, int buttons)
|
||||||
|
{
|
||||||
|
for (auto widget : m_widgets)
|
||||||
|
{
|
||||||
|
if (widget->rect().contains(position)) return widget->handle_mouse_up(position, buttons);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ui::EventResult::DidNotHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<EventResult> HorizontalLayout::handle_mouse_down(Point position, int buttons)
|
||||||
|
{
|
||||||
|
for (auto widget : m_widgets)
|
||||||
|
{
|
||||||
|
if (widget->rect().contains(position)) return widget->handle_mouse_down(position, buttons);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ui::EventResult::DidNotHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<EventResult> HorizontalLayout::handle_key_event(const ui::KeyEventRequest& request)
|
||||||
|
{
|
||||||
|
EventResult result = ui::EventResult::DidNotHandle;
|
||||||
|
|
||||||
|
for (auto widget : m_widgets)
|
||||||
|
{
|
||||||
|
auto rc = TRY(widget->handle_key_event(request));
|
||||||
|
if (rc == ui::EventResult::DidHandle) result = rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void> HorizontalLayout::draw(Canvas& canvas)
|
||||||
|
{
|
||||||
|
for (auto widget : m_widgets)
|
||||||
|
{
|
||||||
|
ui::Rect rect = { m_rect.relative(widget->rect().pos), widget->rect().width, widget->rect().height };
|
||||||
|
auto subcanvas = canvas.subcanvas(rect);
|
||||||
|
TRY(widget->draw(subcanvas));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void> HorizontalLayout::add_widget(Widget& widget)
|
||||||
|
{
|
||||||
|
TRY(m_widgets.try_append(&widget));
|
||||||
|
|
||||||
|
if (m_adjust_width == AdjustWidth::No)
|
||||||
|
{
|
||||||
|
widget.rect().pos.x = m_rect.pos.x + m_used_width + m_margins.left;
|
||||||
|
m_used_width += m_margins.left + widget.rect().width + m_margins.right;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
int used_width = 0;
|
||||||
|
div_t result = div(m_rect.width, (int)m_widgets.size());
|
||||||
|
for (auto w : m_widgets)
|
||||||
|
{
|
||||||
|
w->rect().pos.x = m_rect.pos.x + used_width + m_margins.left;
|
||||||
|
w->rect().width = result.quot - (m_margins.left + m_margins.right);
|
||||||
|
used_width += result.quot;
|
||||||
|
}
|
||||||
|
m_widgets[m_widgets.size() - 1]->rect().width += result.rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
widget.rect().pos.y = m_rect.pos.y + m_margins.top;
|
||||||
|
|
||||||
|
if (m_adjust_height == AdjustHeight::Yes)
|
||||||
|
{
|
||||||
|
widget.rect().height = m_rect.height - (m_margins.top + m_margins.bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
widget.set_parent(this);
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
VerticalLayout::VerticalLayout(Margins margins, AdjustHeight adjust_height, AdjustWidth adjust_width)
|
||||||
|
: m_margins(margins), m_adjust_height(adjust_height), m_adjust_width(adjust_width)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<EventResult> VerticalLayout::handle_mouse_move(Point position)
|
||||||
|
{
|
||||||
|
EventResult result = ui::EventResult::DidNotHandle;
|
||||||
|
|
||||||
|
for (auto widget : m_widgets)
|
||||||
|
{
|
||||||
|
if (widget->rect().contains(position)) result = TRY(widget->handle_mouse_move(position));
|
||||||
|
else
|
||||||
|
TRY(widget->handle_mouse_leave());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<EventResult> VerticalLayout::handle_mouse_leave()
|
||||||
|
{
|
||||||
|
EventResult result = ui::EventResult::DidNotHandle;
|
||||||
|
|
||||||
|
for (auto widget : m_widgets)
|
||||||
|
{
|
||||||
|
auto rc = TRY(widget->handle_mouse_leave());
|
||||||
|
if (rc == ui::EventResult::DidHandle) result = rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<EventResult> VerticalLayout::handle_mouse_up(Point position, int buttons)
|
||||||
|
{
|
||||||
|
for (auto widget : m_widgets)
|
||||||
|
{
|
||||||
|
if (widget->rect().contains(position)) return widget->handle_mouse_up(position, buttons);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ui::EventResult::DidNotHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<EventResult> VerticalLayout::handle_mouse_down(Point position, int buttons)
|
||||||
|
{
|
||||||
|
for (auto widget : m_widgets)
|
||||||
|
{
|
||||||
|
if (widget->rect().contains(position)) return widget->handle_mouse_down(position, buttons);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ui::EventResult::DidNotHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<EventResult> VerticalLayout::handle_key_event(const ui::KeyEventRequest& request)
|
||||||
|
{
|
||||||
|
EventResult result = ui::EventResult::DidNotHandle;
|
||||||
|
|
||||||
|
for (auto widget : m_widgets)
|
||||||
|
{
|
||||||
|
auto rc = TRY(widget->handle_key_event(request));
|
||||||
|
if (rc == ui::EventResult::DidHandle) result = rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void> VerticalLayout::draw(Canvas& canvas)
|
||||||
|
{
|
||||||
|
for (auto widget : m_widgets)
|
||||||
|
{
|
||||||
|
ui::Rect rect = { m_rect.relative(widget->rect().pos), widget->rect().width, widget->rect().height };
|
||||||
|
auto subcanvas = canvas.subcanvas(rect);
|
||||||
|
TRY(widget->draw(subcanvas));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void> VerticalLayout::add_widget(Widget& widget)
|
||||||
|
{
|
||||||
|
TRY(m_widgets.try_append(&widget));
|
||||||
|
|
||||||
|
if (m_adjust_height == AdjustHeight::No)
|
||||||
|
{
|
||||||
|
widget.rect().pos.y = m_rect.pos.y + m_used_height + m_margins.top;
|
||||||
|
m_used_height += m_margins.top + widget.rect().height + m_margins.bottom;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
int used_height = 0;
|
||||||
|
div_t result = div(m_rect.height, (int)m_widgets.size());
|
||||||
|
for (auto w : m_widgets)
|
||||||
|
{
|
||||||
|
w->rect().pos.y = m_rect.pos.y + used_height + m_margins.top;
|
||||||
|
w->rect().height = result.quot - (m_margins.top + m_margins.bottom);
|
||||||
|
used_height += result.quot;
|
||||||
|
}
|
||||||
|
m_widgets[m_widgets.size() - 1]->rect().height += result.rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
widget.rect().pos.x = m_rect.pos.x + m_margins.left;
|
||||||
|
|
||||||
|
if (m_adjust_width == AdjustWidth::Yes)
|
||||||
|
{
|
||||||
|
widget.rect().width = m_rect.width - (m_margins.left + m_margins.right);
|
||||||
|
}
|
||||||
|
|
||||||
|
widget.set_parent(this);
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
62
gui/libui/src/Rect.cpp
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* @file Rect.cpp
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief A simple 2D rectangle representation.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2023, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <ui/Rect.h>
|
||||||
|
|
||||||
|
namespace ui
|
||||||
|
{
|
||||||
|
bool Rect::contains(Point point)
|
||||||
|
{
|
||||||
|
return (point.x >= pos.x) && (point.y >= pos.y) && (point.x <= (pos.x + width)) &&
|
||||||
|
(point.y <= (pos.y + height));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Rect::contains(Rect rect)
|
||||||
|
{
|
||||||
|
if (!contains(rect.pos)) return false;
|
||||||
|
Point rel = relative(rect.pos);
|
||||||
|
if ((rel.x + rect.width) > width) return false;
|
||||||
|
if ((rel.y + rect.height) > height) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Point Rect::normalize(Point point)
|
||||||
|
{
|
||||||
|
if (point.x < pos.x) point.x = pos.x;
|
||||||
|
if (point.y < pos.y) point.y = pos.y;
|
||||||
|
if (point.x > pos.x + width) point.x = pos.x + width;
|
||||||
|
if (point.y > pos.y + height) point.y = pos.y + height;
|
||||||
|
return point;
|
||||||
|
}
|
||||||
|
|
||||||
|
Point Rect::relative(Point point)
|
||||||
|
{
|
||||||
|
point = normalize(point);
|
||||||
|
point.x -= pos.x;
|
||||||
|
point.y -= pos.y;
|
||||||
|
return point;
|
||||||
|
}
|
||||||
|
|
||||||
|
Point Rect::absolute(Point point)
|
||||||
|
{
|
||||||
|
point.x += pos.x;
|
||||||
|
point.y += pos.y;
|
||||||
|
return point;
|
||||||
|
}
|
||||||
|
|
||||||
|
Rect Rect::absolute(Rect rect)
|
||||||
|
{
|
||||||
|
return Rect { absolute(rect.pos), rect.width, rect.height };
|
||||||
|
}
|
||||||
|
|
||||||
|
Rect Rect::normalized()
|
||||||
|
{
|
||||||
|
return Rect { ui::Point { pos.x < 0 ? 0 : pos.x, pos.y < 0 ? 0 : pos.y }, width, height };
|
||||||
|
}
|
||||||
|
};
|
52
gui/libui/src/TextInput.cpp
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* @file TextInput.cpp
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief Base class for text inputs.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2024, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <ui/App.h>
|
||||||
|
#include <ui/TextInput.h>
|
||||||
|
|
||||||
|
namespace ui
|
||||||
|
{
|
||||||
|
TextInput::TextInput() : Widget()
|
||||||
|
{
|
||||||
|
m_cursor_timer = os::Timer::create_repeating(500, [this]() { this->tick_cursor(); }).release_value();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextInput::update_cursor()
|
||||||
|
{
|
||||||
|
m_cursor_timer->restart();
|
||||||
|
m_cursor_activated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void> TextInput::delete_current_character()
|
||||||
|
{
|
||||||
|
usize size = m_data.size() - m_cursor;
|
||||||
|
u8* slice = TRY(m_data.slice(m_cursor, size));
|
||||||
|
memmove(slice, slice + 1, size - 1);
|
||||||
|
TRY(m_data.try_resize(m_data.size() - 1));
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void> TextInput::insert_character(char c)
|
||||||
|
{
|
||||||
|
usize size = m_data.size() - m_cursor;
|
||||||
|
u8* slice = TRY(m_data.slice(m_cursor, size + 1));
|
||||||
|
memmove(slice + 1, slice, size);
|
||||||
|
*slice = (u8)c;
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextInput::tick_cursor()
|
||||||
|
{
|
||||||
|
m_cursor_activated = !m_cursor_activated;
|
||||||
|
|
||||||
|
window()->draw();
|
||||||
|
}
|
||||||
|
}
|
233
gui/libui/src/Window.cpp
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
/**
|
||||||
|
* @file Window.cpp
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief UI windows.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2023, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <luna/String.h>
|
||||||
|
#include <luna/Utf8.h>
|
||||||
|
#include <os/File.h>
|
||||||
|
#include <os/SharedMemory.h>
|
||||||
|
#include <sys/mman.h>
|
||||||
|
#include <ui/App.h>
|
||||||
|
#include <ui/Font.h>
|
||||||
|
#include <ui/Image.h>
|
||||||
|
#include <ui/Window.h>
|
||||||
|
#include <ui/ipc/Server.h>
|
||||||
|
|
||||||
|
static int titlebar_height()
|
||||||
|
{
|
||||||
|
auto font = ui::Font::default_font();
|
||||||
|
return font->height() + 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace ui
|
||||||
|
{
|
||||||
|
Result<Window*> Window::create(Rect rect, WindowType type)
|
||||||
|
{
|
||||||
|
auto window = TRY(make_owned<Window>());
|
||||||
|
window->m_name = TRY(String::from_cstring("Window"));
|
||||||
|
|
||||||
|
if (type == ui::WindowType::Normal)
|
||||||
|
{
|
||||||
|
int height = titlebar_height();
|
||||||
|
rect.height += height; // Make sure we provide the full contents rect that was asked for.
|
||||||
|
rect.pos.y -= height; // Adjust it so the contents begin at the expected coordinates.
|
||||||
|
window->m_decorated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
rect = rect.normalized();
|
||||||
|
|
||||||
|
ui::CreateWindowRequest request;
|
||||||
|
request.rect = rect;
|
||||||
|
auto response = TRY(App::the().client().send_sync<ui::CreateWindowResponse>(request));
|
||||||
|
|
||||||
|
auto path = COPY_IPC_STRING(response.shm_path);
|
||||||
|
|
||||||
|
u32* pixels = (u32*)TRY(os::SharedMemory::adopt(path.view(), rect.height * rect.width * 4, false));
|
||||||
|
|
||||||
|
Canvas canvas = ui::Canvas { rect.width, rect.height, rect.width, (u8*)pixels };
|
||||||
|
window->m_canvas = canvas;
|
||||||
|
window->m_id = response.window;
|
||||||
|
|
||||||
|
if (type == ui::WindowType::Normal)
|
||||||
|
{
|
||||||
|
int height = titlebar_height();
|
||||||
|
window->m_titlebar_canvas = canvas.subcanvas(ui::Rect { 0, 0, canvas.width, height });
|
||||||
|
window->m_window_canvas = canvas.subcanvas(ui::Rect { 0, height, canvas.width, canvas.height - height });
|
||||||
|
|
||||||
|
ui::SetTitlebarHeightRequest titlebar_request;
|
||||||
|
titlebar_request.height = height;
|
||||||
|
titlebar_request.window = response.window;
|
||||||
|
App::the().client().send_async(titlebar_request);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
window->m_titlebar_canvas = canvas.subcanvas(ui::Rect { 0, 0, 0, 0 });
|
||||||
|
window->m_window_canvas = canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
Window* p = window.ptr();
|
||||||
|
|
||||||
|
ui::RemoveSharedMemoryRequest shm_request;
|
||||||
|
shm_request.window = response.window;
|
||||||
|
App::the().client().send_async(shm_request);
|
||||||
|
|
||||||
|
App::the().register_window(move(window), {});
|
||||||
|
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
Window::~Window()
|
||||||
|
{
|
||||||
|
if (m_canvas.ptr) munmap(m_canvas.ptr, ((usize)m_canvas.width) * ((usize)m_canvas.height) * 4);
|
||||||
|
|
||||||
|
if (m_has_on_close_action) m_on_close_action();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Window::set_title(StringView title)
|
||||||
|
{
|
||||||
|
ui::SetWindowTitleRequest request;
|
||||||
|
request.window = m_id;
|
||||||
|
SET_IPC_STRING(request.title, title.chars());
|
||||||
|
App::the().client().send_async(request);
|
||||||
|
|
||||||
|
m_name = String::from_string_view(title).release_value();
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Window::update()
|
||||||
|
{
|
||||||
|
ui::InvalidateRequest request;
|
||||||
|
request.window = m_id;
|
||||||
|
App::the().client().send_async(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Window::close()
|
||||||
|
{
|
||||||
|
App& app = App::the();
|
||||||
|
|
||||||
|
if (this == app.main_window()) app.set_should_close(true);
|
||||||
|
|
||||||
|
app.unregister_window(this, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void Window::set_layer(Layer layer)
|
||||||
|
{
|
||||||
|
ui::SetWindowLayer request;
|
||||||
|
request.window = m_id;
|
||||||
|
request.layer = layer;
|
||||||
|
App::the().client().send_async(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void> Window::draw()
|
||||||
|
{
|
||||||
|
if (m_background.has_value()) m_window_canvas.fill(*m_background);
|
||||||
|
if (m_decorated) TRY(draw_titlebar());
|
||||||
|
if (m_main_widget) TRY(m_main_widget->draw(m_window_canvas));
|
||||||
|
update();
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr ui::Color TITLEBAR_COLOR = ui::Color::from_rgb(53, 53, 53);
|
||||||
|
|
||||||
|
// FIXME: Titlebars should be implemented as a separate widget group, to allow for customization and extensibility.
|
||||||
|
// Additionally, this very specific spaghetti code could be replaced with well-established UI components.
|
||||||
|
Result<void> Window::draw_titlebar()
|
||||||
|
{
|
||||||
|
wchar_t buffer[4096];
|
||||||
|
Utf8StringDecoder decoder(m_name.chars());
|
||||||
|
decoder.decode(buffer, sizeof(buffer)).release_value();
|
||||||
|
|
||||||
|
auto font = ui::Font::default_font();
|
||||||
|
|
||||||
|
m_titlebar_canvas.fill(TITLEBAR_COLOR);
|
||||||
|
|
||||||
|
auto textarea =
|
||||||
|
m_titlebar_canvas.subcanvas(ui::Rect { 10, 10, m_titlebar_canvas.width - 10, m_titlebar_canvas.height });
|
||||||
|
font->render(buffer, ui::WHITE, textarea);
|
||||||
|
|
||||||
|
static SharedPtr<ui::Image> g_close_icon;
|
||||||
|
|
||||||
|
if (!g_close_icon) g_close_icon = ui::Image::load("/usr/share/icons/16x16/app-close.tga").release_value();
|
||||||
|
|
||||||
|
auto close_rect = ui::Rect { m_titlebar_canvas.width - 26, 10, 16, 16 };
|
||||||
|
|
||||||
|
auto close_area = m_titlebar_canvas.subcanvas(close_rect);
|
||||||
|
close_area.fill(g_close_icon->pixels(), g_close_icon->width());
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<ui::EventResult> Window::handle_mouse_leave()
|
||||||
|
{
|
||||||
|
if (!m_main_widget) return ui::EventResult::DidNotHandle;
|
||||||
|
return m_main_widget->handle_mouse_leave();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<ui::EventResult> Window::handle_mouse_move(ui::Point position)
|
||||||
|
{
|
||||||
|
if (!m_main_widget) return ui::EventResult::DidNotHandle;
|
||||||
|
return m_main_widget->handle_mouse_move(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<ui::EventResult> Window::handle_mouse_buttons(ui::Point position, int buttons)
|
||||||
|
{
|
||||||
|
auto result = ui::EventResult::DidNotHandle;
|
||||||
|
|
||||||
|
if (m_decorated && m_titlebar_canvas.rect().contains(position))
|
||||||
|
{
|
||||||
|
// Handle pressing the close button
|
||||||
|
auto close_rect = ui::Rect { m_titlebar_canvas.width - 26, 10, 16, 16 };
|
||||||
|
if (close_rect.contains(position) && (buttons & LEFT)) { close(); }
|
||||||
|
return ui::EventResult::DidNotHandle;
|
||||||
|
}
|
||||||
|
if (m_decorated) position.y -= m_titlebar_canvas.height;
|
||||||
|
|
||||||
|
if (!m_main_widget) return ui::EventResult::DidNotHandle;
|
||||||
|
|
||||||
|
if (buttons)
|
||||||
|
{
|
||||||
|
auto rc = TRY(m_main_widget->handle_mouse_down(position, buttons));
|
||||||
|
if (rc == ui::EventResult::DidHandle) result = rc;
|
||||||
|
}
|
||||||
|
if (m_old_mouse_buttons.has_value())
|
||||||
|
{
|
||||||
|
int old_buttons = m_old_mouse_buttons.value();
|
||||||
|
int diff = old_buttons & ~buttons;
|
||||||
|
if (diff)
|
||||||
|
{
|
||||||
|
auto rc = TRY(m_main_widget->handle_mouse_up(position, diff));
|
||||||
|
if (rc == ui::EventResult::DidHandle) result = rc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_old_mouse_buttons = buttons;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<ui::EventResult> Window::handle_key_event(const ui::KeyEventRequest& request)
|
||||||
|
{
|
||||||
|
if (request.pressed)
|
||||||
|
{
|
||||||
|
auto* shortcut = m_shortcuts.try_get_ref({ request.code, request.modifiers });
|
||||||
|
if (shortcut)
|
||||||
|
{
|
||||||
|
shortcut->action({ request.code, request.modifiers });
|
||||||
|
if (shortcut->intercept) return ui::EventResult::DidHandle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_main_widget) return ui::EventResult::DidNotHandle;
|
||||||
|
return m_main_widget->handle_key_event(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void> Window::add_keyboard_shortcut(ui::Shortcut shortcut, bool intercept, Function<ui::Shortcut>&& action)
|
||||||
|
{
|
||||||
|
TRY(m_shortcuts.try_set(shortcut, { intercept, move(action) }));
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
186
gui/loginui.cpp
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
/**
|
||||||
|
* @file loginui.cpp
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief Graphical login prompt.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2024, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <luna/RefString.h>
|
||||||
|
#include <luna/SHA.h>
|
||||||
|
#include <os/ArgumentParser.h>
|
||||||
|
#include <os/Config.h>
|
||||||
|
#include <os/File.h>
|
||||||
|
#include <os/FileSystem.h>
|
||||||
|
#include <os/IPC.h>
|
||||||
|
#include <os/Process.h>
|
||||||
|
#include <os/Security.h>
|
||||||
|
#include <pwd.h>
|
||||||
|
#include <shadow.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <ui/App.h>
|
||||||
|
#include <ui/Button.h>
|
||||||
|
#include <ui/InputField.h>
|
||||||
|
#include <ui/Label.h>
|
||||||
|
#include <ui/Layout.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
enum Stage
|
||||||
|
{
|
||||||
|
UsernameInput,
|
||||||
|
PasswordInput,
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr ui::Color BACKGROUND_COLOR = ui::Color::from_rgb(89, 89, 89);
|
||||||
|
|
||||||
|
Result<String> hash_password(StringView& view)
|
||||||
|
{
|
||||||
|
SHA256 sha;
|
||||||
|
sha.append((const u8*)view.chars(), view.length());
|
||||||
|
auto digest = TRY(sha.digest());
|
||||||
|
return digest.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<int> luna_main(int argc, char** argv)
|
||||||
|
{
|
||||||
|
os::ArgumentParser parser;
|
||||||
|
parser.add_description("Login prompt for a graphical UI session.");
|
||||||
|
parser.add_system_program_info("loginui"_sv);
|
||||||
|
parser.parse(argc, argv);
|
||||||
|
|
||||||
|
if (geteuid() != 0)
|
||||||
|
{
|
||||||
|
os::eprintln("error: %s can only be started as root.", argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
TRY(os::Security::pledge("stdio rpath wpath unix proc exec id", nullptr));
|
||||||
|
|
||||||
|
setsid();
|
||||||
|
|
||||||
|
bool success = os::IPC::Notifier::run_and_wait(
|
||||||
|
[&] {
|
||||||
|
StringView wind_command[] = { "/usr/bin/wind" };
|
||||||
|
os::Process::spawn(wind_command[0], Slice<StringView>(wind_command, 1));
|
||||||
|
},
|
||||||
|
1000);
|
||||||
|
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
os::eprintln("loginui: failed to start wind, timed out");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto config = TRY(os::ConfigFile::open("/etc/loginui.conf"));
|
||||||
|
|
||||||
|
if (config->read_boolean_or("Autologin", false))
|
||||||
|
{
|
||||||
|
StringView username = config->read_string_or("AutologinUser", "");
|
||||||
|
|
||||||
|
if (!username.is_empty())
|
||||||
|
{
|
||||||
|
auto flag = RefString::format("--user=%s"_sv, username.chars()).release_value();
|
||||||
|
|
||||||
|
StringView startui_command[] = { "/usr/bin/startui", flag.view() };
|
||||||
|
os::Process::exec(startui_command[0], Slice<StringView>(startui_command, 2));
|
||||||
|
unreachable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ui::App app;
|
||||||
|
TRY(app.init());
|
||||||
|
|
||||||
|
auto* window = TRY(ui::Window::create(ui::Rect { 300, 300, 400, 300 }));
|
||||||
|
app.set_main_window(window);
|
||||||
|
|
||||||
|
window->set_title("Log in");
|
||||||
|
window->set_background(BACKGROUND_COLOR);
|
||||||
|
|
||||||
|
ui::VerticalLayout main_layout;
|
||||||
|
window->set_main_widget(main_layout);
|
||||||
|
|
||||||
|
ui::Label label("Username:");
|
||||||
|
main_layout.add_widget(label);
|
||||||
|
|
||||||
|
ui::InputField input(ui::Font::default_font());
|
||||||
|
main_layout.add_widget(input);
|
||||||
|
|
||||||
|
ui::Label error("");
|
||||||
|
error.set_font(ui::Font::default_bold_font());
|
||||||
|
error.set_color(ui::RED);
|
||||||
|
main_layout.add_widget(error);
|
||||||
|
|
||||||
|
Stage stage = Stage::UsernameInput;
|
||||||
|
struct passwd* pw;
|
||||||
|
|
||||||
|
input.on_submit([&](StringView data) {
|
||||||
|
error.set_text("");
|
||||||
|
if (stage == Stage::UsernameInput)
|
||||||
|
{
|
||||||
|
struct passwd* entry = getpwnam(data.chars());
|
||||||
|
if (!entry)
|
||||||
|
{
|
||||||
|
error.set_text("User not found.");
|
||||||
|
input.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pw = entry;
|
||||||
|
stage = Stage::PasswordInput;
|
||||||
|
label.set_text("Password:");
|
||||||
|
|
||||||
|
RefString title = RefString::format("Log in: %s"_sv, data.chars()).release_value();
|
||||||
|
window->set_title(title.view());
|
||||||
|
|
||||||
|
input.clear();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const char* passwd = pw->pw_passwd;
|
||||||
|
|
||||||
|
// If the user's password entry is 'x', read their password from the shadow file instead.
|
||||||
|
if (!strcmp(pw->pw_passwd, "x"))
|
||||||
|
{
|
||||||
|
struct spwd* sp = getspnam(pw->pw_name);
|
||||||
|
|
||||||
|
if (!sp)
|
||||||
|
{
|
||||||
|
error.set_text("User not found in shadow file.");
|
||||||
|
input.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
endspent();
|
||||||
|
|
||||||
|
passwd = sp->sp_pwdp;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!strcmp(passwd, "!"))
|
||||||
|
{
|
||||||
|
error.set_text("User's password is disabled.");
|
||||||
|
input.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto result = hash_password(data).release_value();
|
||||||
|
|
||||||
|
if (strcmp(result.chars(), passwd))
|
||||||
|
{
|
||||||
|
error.set_text("Incorrect password.");
|
||||||
|
input.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto flag = RefString::format("--user=%s"_sv, pw->pw_name).release_value();
|
||||||
|
|
||||||
|
StringView startui_command[] = { "/usr/bin/startui", flag.view() };
|
||||||
|
os::Process::exec(startui_command[0], Slice<StringView>(startui_command, 2));
|
||||||
|
unreachable();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return app.run();
|
||||||
|
}
|
35
gui/run.cpp
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* @file run.cpp
|
||||||
|
* @author apio (cloudapio.eu)
|
||||||
|
* @brief Tiny command-line utility to start a detached program in the current GUI session.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2024, the Luna authors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <os/ArgumentParser.h>
|
||||||
|
#include <os/File.h>
|
||||||
|
#include <os/LocalClient.h>
|
||||||
|
#include <os/ipc/Launcher.h>
|
||||||
|
|
||||||
|
Result<int> luna_main(int argc, char** argv)
|
||||||
|
{
|
||||||
|
StringView program;
|
||||||
|
|
||||||
|
os::ArgumentParser parser;
|
||||||
|
parser.add_description("Start a detached program in the current GUI session."_sv);
|
||||||
|
parser.add_system_program_info("run"_sv);
|
||||||
|
parser.add_positional_argument(program, "program", true);
|
||||||
|
parser.parse(argc, argv);
|
||||||
|
|
||||||
|
OwnedPtr<os::IPC::Client> launcher_client = TRY(os::IPC::Client::connect("/tmp/execd.sock", false));
|
||||||
|
|
||||||
|
os::println("Requesting to start program '%s'...", program.chars());
|
||||||
|
|
||||||
|
os::Launcher::LaunchDetachedRequest request;
|
||||||
|
SET_IPC_STRING(request.command, program.chars());
|
||||||
|
request.search_in_path = true;
|
||||||
|
launcher_client->send_async(request);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
24
gui/wind/CMakeLists.txt
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
set(SOURCES
|
||||||
|
main.cpp
|
||||||
|
Screen.h
|
||||||
|
Screen.cpp
|
||||||
|
Mouse.h
|
||||||
|
Mouse.cpp
|
||||||
|
Window.h
|
||||||
|
Window.cpp
|
||||||
|
IPC.cpp
|
||||||
|
IPC.h
|
||||||
|
Keyboard.cpp
|
||||||
|
Keyboard.h
|
||||||
|
Client.h
|
||||||
|
Client.cpp
|
||||||
|
Layer.cpp
|
||||||
|
Layer.h
|
||||||
|
)
|
||||||
|
|
||||||
|
add_executable(wind ${SOURCES})
|
||||||
|
target_compile_options(wind PRIVATE -Os ${COMMON_FLAGS} -Wno-write-strings -fno-threadsafe-statics)
|
||||||
|
add_dependencies(wind libc)
|
||||||
|
target_include_directories(wind PRIVATE ${LUNA_BASE}/usr/include ${CMAKE_CURRENT_LIST_DIR})
|
||||||
|
target_link_libraries(wind PRIVATE os ui)
|
||||||
|
install(TARGETS wind DESTINATION ${LUNA_BASE}/usr/bin)
|