Compare commits
468 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7215fdfb7c | ||
![]() |
e2778d9d6a | ||
![]() |
38849fe381 | ||
![]() |
df7e5bd57a | ||
![]() |
e3e781ced8 | ||
![]() |
97756bc246 | ||
![]() |
f71d79a0ff | ||
![]() |
95247daa17 | ||
![]() |
530e9f667c | ||
![]() |
409fb968d3 | ||
![]() |
1581f0229e | ||
![]() |
ae722ab0cf | ||
![]() |
1384fa3287 | ||
![]() |
83b5b81227 | ||
![]() |
63fa3aec10 | ||
![]() |
ebeb788b1f | ||
![]() |
0fc25c2680 | ||
![]() |
449dd1c23c | ||
![]() |
636a39cbe1 | ||
![]() |
be9b5a3641 | ||
![]() |
e8b9e706ca | ||
![]() |
a8d5a82a7f | ||
![]() |
c2d37571f7 | ||
![]() |
90cd178117 | ||
![]() |
7397be3401 | ||
![]() |
1edd333864 | ||
![]() |
b40dba18c6 | ||
![]() |
4721a0a680 | ||
![]() |
8d6c2580f4 | ||
![]() |
8bc88c9bf4 | ||
![]() |
b52cd2052f | ||
![]() |
b1e214bbdf | ||
![]() |
cef0c0b6d8 | ||
![]() |
d5dcc87ace | ||
![]() |
853d492724 | ||
![]() |
d15ce22e2e | ||
![]() |
628c84cbee | ||
![]() |
212656f3fc | ||
![]() |
660566c499 | ||
![]() |
430cbe541c | ||
![]() |
53f8e4049f | ||
![]() |
f0718a61d3 | ||
![]() |
fb5cb3052d | ||
![]() |
9c270448a6 | ||
![]() |
d15f4ef992 | ||
![]() |
21a59143a4 | ||
![]() |
06d753c289 | ||
![]() |
8344e88f8a | ||
![]() |
e4eaf99072 | ||
![]() |
1353a5733f | ||
![]() |
8aa07e9d43 | ||
![]() |
a6b60d5d58 | ||
![]() |
8c59888ebd | ||
![]() |
5725868b57 | ||
![]() |
8c3d8b3ba5 | ||
![]() |
312cab5355 | ||
![]() |
5444ab269a | ||
![]() |
d01e4342f0 | ||
![]() |
cc20dcb9d0 | ||
![]() |
f222ee044e | ||
![]() |
c72bdb77b5 | ||
![]() |
e7c0547e23 | ||
![]() |
fe7ff66266 | ||
![]() |
ce62b61e15 | ||
![]() |
4a0bafbeab | ||
![]() |
5b1cad2b72 | ||
![]() |
8ce0e1c520 | ||
![]() |
eee4cf6a82 | ||
![]() |
4bd4eca2e4 | ||
![]() |
c75e72693c | ||
![]() |
518b4f447e | ||
![]() |
d4f9f6984d | ||
![]() |
356ebe55da | ||
![]() |
5029575ed0 | ||
![]() |
30e49f1da2 | ||
![]() |
01bbda4544 | ||
![]() |
f87c6cbd7c | ||
![]() |
ce5472633d | ||
![]() |
0ad8aea811 | ||
![]() |
53b1d0e5e9 | ||
![]() |
0eabd894bd | ||
![]() |
cd0f66b9ae | ||
![]() |
60b4f5e3fe | ||
![]() |
f7ed135192 | ||
![]() |
e70204d726 | ||
![]() |
34f587d1e1 | ||
![]() |
fa0d7f7f7f | ||
![]() |
a68c891092 | ||
![]() |
0e2656b77c | ||
![]() |
80ea12877e | ||
![]() |
99af63fad2 | ||
![]() |
e4e1638fc0 | ||
![]() |
7e9c7dac41 | ||
![]() |
f9728b88db | ||
![]() |
2c9b130423 | ||
![]() |
6554fbb151 | ||
![]() |
6ae4bc486b | ||
![]() |
367087f84f | ||
![]() |
ea5c9483a2 | ||
![]() |
37fd141dd1 | ||
![]() |
4e8341d35c | ||
![]() |
df9df39f10 | ||
![]() |
a70b173333 | ||
![]() |
a925511032 | ||
![]() |
126be51ac3 | ||
![]() |
51081d69fc | ||
![]() |
0ba2d8ca1d | ||
![]() |
4a72abc8ec | ||
![]() |
32677a54e4 | ||
![]() |
26eb002270 | ||
![]() |
c58ac86935 | ||
![]() |
e0b5bb17e5 | ||
![]() |
6c4fc50fdf | ||
![]() |
7a658e3ddb | ||
![]() |
cb77458f70 | ||
![]() |
1437ec8903 | ||
![]() |
c8dd5d1b2d | ||
![]() |
059e6f719d | ||
![]() |
930ef2db38 | ||
![]() |
228f30ed46 | ||
![]() |
d7189d69f6 | ||
![]() |
2713f60f5b | ||
![]() |
4e3ae098a9 | ||
![]() |
0a1b85f35a | ||
![]() |
e7471f1191 | ||
![]() |
aca3807e43 | ||
![]() |
32606c2223 | ||
![]() |
54675f8983 | ||
![]() |
6cc43dbdd4 | ||
![]() |
85f615f21c | ||
![]() |
7d91e90481 | ||
![]() |
124ef8ddf8 | ||
![]() |
896a299bf9 | ||
![]() |
4af7dc9e35 | ||
![]() |
da0a178701 | ||
![]() |
5effc68ba4 | ||
![]() |
e30ac7f91a | ||
![]() |
9145893066 | ||
![]() |
c3932f7f7f | ||
![]() |
9e3da91a59 | ||
![]() |
452541d7c2 | ||
![]() |
be4a5be145 | ||
![]() |
32144d3cb9 | ||
![]() |
b8a1670781 | ||
![]() |
a86ec5a749 | ||
![]() |
8ae4d49e6e | ||
![]() |
ee2582964d | ||
![]() |
3519358b7b | ||
![]() |
be6d23eed1 | ||
![]() |
df01db2df1 | ||
![]() |
aa503008ae | ||
![]() |
d7fa40ebba | ||
![]() |
8eb687cf04 | ||
![]() |
3ba39c3022 | ||
![]() |
0b2d56cff0 | ||
![]() |
4e59296cbb | ||
![]() |
5093b67e9b | ||
![]() |
e1f348e4b2 | ||
![]() |
48666e1d7a | ||
![]() |
b774159fc8 | ||
![]() |
ec0b2d8e9a | ||
![]() |
c8771cd13e | ||
![]() |
e613315866 | ||
![]() |
7133818c59 | ||
![]() |
85f8eeb9d5 | ||
![]() |
2dc2a5540d | ||
![]() |
0336816faf | ||
![]() |
975cbac83a | ||
![]() |
a3f1e5961f | ||
![]() |
c5e6def65e | ||
![]() |
d2f70cdcd1 | ||
![]() |
502e1aa229 | ||
![]() |
f9066ff285 | ||
![]() |
7d8154971b | ||
![]() |
f3633534e7 | ||
![]() |
bde47c9957 | ||
![]() |
8602326af5 | ||
![]() |
8671aa26e1 | ||
![]() |
1c8cf9e3d5 | ||
![]() |
f4d8ab9779 | ||
![]() |
dfc1f2c470 | ||
![]() |
eca5e6a4d2 | ||
![]() |
f7019926dc | ||
![]() |
9268f43896 | ||
![]() |
0ff1ee3893 | ||
![]() |
0ba04184d5 | ||
![]() |
690dc2d2e8 | ||
![]() |
ef931c406c | ||
![]() |
f0f41d60e1 | ||
![]() |
5bac853d9c | ||
![]() |
f790e248e4 | ||
![]() |
8ff139687a | ||
![]() |
e225c6b8d0 | ||
![]() |
b2edbe617e | ||
![]() |
b1e71d308f | ||
![]() |
51ff32797d | ||
![]() |
35213606c4 | ||
![]() |
5515d2db77 | ||
![]() |
c3edc1b88e | ||
![]() |
4aacb4575b | ||
![]() |
75a825e9a1 | ||
![]() |
3805a3282f | ||
![]() |
b3ab47a594 | ||
![]() |
d5eb8779cb | ||
![]() |
8c76b8caff | ||
![]() |
eec18290a4 | ||
![]() |
940b896cbb | ||
![]() |
17501d7e3e | ||
![]() |
22a917c00a | ||
![]() |
e48602059d | ||
![]() |
7434a7d7d5 | ||
![]() |
a0b3f1126b | ||
![]() |
25718f2774 | ||
![]() |
fcb00723bc | ||
![]() |
4d1e858a52 | ||
![]() |
34e6e12f00 | ||
![]() |
8e79a5a8ae | ||
![]() |
76d0cf5034 | ||
![]() |
1fb19f08ce | ||
![]() |
4c82519fb8 | ||
![]() |
f71cbc2475 | ||
![]() |
02b9d2d082 | ||
![]() |
d97282cd92 | ||
![]() |
2b1341095d | ||
![]() |
53b800502a | ||
![]() |
f7a15a34f9 | ||
![]() |
da96fcacd3 | ||
![]() |
1fadc7396a | ||
![]() |
aebec6329a | ||
![]() |
7b8c17042c | ||
![]() |
5e44c13b48 | ||
![]() |
3d36a20376 | ||
![]() |
944159d514 | ||
![]() |
02d1e7492a | ||
![]() |
a1f6f68213 | ||
![]() |
ae5294e63c | ||
![]() |
c414af7aa2 | ||
![]() |
7803285221 | ||
![]() |
367378d75d | ||
![]() |
f4d2aa5b4a | ||
![]() |
0fe0c8115c | ||
![]() |
510aa1710e | ||
![]() |
eba3927ace | ||
![]() |
f61044cef7 | ||
![]() |
cd74c74f6f | ||
![]() |
3ec170cc1e | ||
![]() |
4dd5ffa254 | ||
![]() |
c2a37f4bd8 | ||
![]() |
c441f33ecf | ||
![]() |
dd67f3631f | ||
![]() |
77df8ca0b0 | ||
![]() |
981fdb3932 | ||
![]() |
aa1536f1a7 | ||
![]() |
9901ad4bb0 | ||
![]() |
53ff83737b | ||
![]() |
5e91a3033d | ||
![]() |
2dca01ecd5 | ||
![]() |
2ee9ab3151 | ||
![]() |
91b2d5da6f | ||
![]() |
4ac1e28eae | ||
![]() |
29fa63317e | ||
![]() |
990a8c8b44 | ||
![]() |
4aae638e15 | ||
![]() |
12dc61ed0f | ||
![]() |
2b0bf82b51 | ||
![]() |
311eb66cae | ||
![]() |
bc6a823103 | ||
![]() |
c4117066cf | ||
![]() |
0605bcfca7 | ||
![]() |
c5495d69fb | ||
![]() |
2842d390c9 | ||
![]() |
ebc4fc866d | ||
![]() |
e0c7533ede | ||
![]() |
1fe08bf7e4 | ||
![]() |
4b6826575e | ||
![]() |
0e62c0e50b | ||
![]() |
5500895845 | ||
![]() |
eeb97fa4ce | ||
![]() |
64aac30f36 | ||
![]() |
162210f90e | ||
![]() |
b43a8626cf | ||
![]() |
7fae634dc3 | ||
![]() |
1d5f20b46c | ||
![]() |
124ed5e4ba | ||
![]() |
613a664c45 | ||
![]() |
be9f363c53 | ||
![]() |
153524023f | ||
![]() |
ac4d75e9b4 | ||
![]() |
9b2c3d1026 | ||
![]() |
3142c52b94 | ||
![]() |
dbfaa4527b | ||
![]() |
af63101fdc | ||
![]() |
9be060fe69 | ||
![]() |
b978b06159 | ||
![]() |
f3f6946eb2 | ||
![]() |
d5933daee5 | ||
![]() |
fb92434aac | ||
![]() |
b9bb7baaa7 | ||
![]() |
4629f2cd12 | ||
![]() |
8797c02ed9 | ||
![]() |
bd5250f85d | ||
![]() |
97ce103209 | ||
![]() |
13f5d62b4b | ||
![]() |
127931ba45 | ||
![]() |
2453756782 | ||
![]() |
8737933101 | ||
![]() |
725aa5277b | ||
![]() |
eb10ef94f7 | ||
![]() |
f4cc1c5465 | ||
![]() |
40e440b2f7 | ||
![]() |
041f2e64ba | ||
![]() |
5c63648c02 | ||
![]() |
193a434717 | ||
![]() |
277f199df4 | ||
![]() |
5514a8afda | ||
![]() |
eeeb4b7de2 | ||
![]() |
39b63f6163 | ||
![]() |
9c58f12a6c | ||
![]() |
48e7f219a3 | ||
![]() |
923b67ef9c | ||
![]() |
38a67f8646 | ||
![]() |
7c25a9238e | ||
![]() |
e38955fbac | ||
![]() |
9e41fa021a | ||
![]() |
66c601f775 | ||
![]() |
e3f993d7b4 | ||
![]() |
2f3c2a360b | ||
![]() |
a827c3e033 | ||
![]() |
53249d36df | ||
![]() |
1af446b9e8 | ||
![]() |
cb0d5a58ab | ||
![]() |
bcc01c8b1c | ||
![]() |
e288ca86fd | ||
![]() |
e39410c37f | ||
![]() |
06adb09fa3 | ||
![]() |
263c9bd601 | ||
![]() |
64496cacb9 | ||
![]() |
3eccd341ad | ||
![]() |
10788ccc92 | ||
![]() |
5d8ef5326b | ||
![]() |
2500a60062 | ||
![]() |
5fb4efa768 | ||
![]() |
d659e5d24f | ||
![]() |
c62f3409bd | ||
![]() |
8997519eb2 | ||
![]() |
235d77713d | ||
![]() |
6de9d89b65 | ||
![]() |
4e8a6e3370 | ||
![]() |
b0fa0c65cc | ||
![]() |
2612dd1064 | ||
![]() |
8c09804abd | ||
![]() |
51052c08e9 | ||
![]() |
105cb4b8f2 | ||
![]() |
c6c6d2dc5d | ||
![]() |
36a49ffba9 | ||
![]() |
278db1c94b | ||
![]() |
97880e18d7 | ||
![]() |
b3ab31953e | ||
![]() |
09d636b089 | ||
![]() |
0571bf67e2 | ||
![]() |
7a46a23db5 | ||
![]() |
2278128e66 | ||
![]() |
8e2de26e4e | ||
![]() |
a7012cf8a6 | ||
![]() |
36cdae98df | ||
![]() |
c02fbfe973 | ||
![]() |
0913c77f3d | ||
![]() |
74ccde566d | ||
![]() |
0bf9fc9428 | ||
![]() |
cf591da1a0 | ||
![]() |
8b3d218f4b | ||
![]() |
d76d055d49 | ||
![]() |
41f31116ce | ||
![]() |
9f1f0a9038 | ||
![]() |
fdc2d1cd6d | ||
![]() |
77b4bacdf4 | ||
![]() |
0337d78eaa | ||
![]() |
0338843950 | ||
![]() |
cbe22a0371 | ||
![]() |
48d48096eb | ||
![]() |
bf15bb6e19 | ||
![]() |
898f4804b7 | ||
![]() |
e76c919c7e | ||
![]() |
c062bca6ee | ||
![]() |
0f0031ebbb | ||
![]() |
bd2cab2f0e | ||
![]() |
9739009f20 | ||
![]() |
433719717a | ||
![]() |
cb9bc8698d | ||
![]() |
14ae4b24a7 | ||
![]() |
5f364795cc | ||
![]() |
0c65a9df41 | ||
![]() |
980a06b95e | ||
![]() |
406703568e | ||
![]() |
eeae485629 | ||
![]() |
935913b1ec | ||
![]() |
1a89cb35e1 | ||
![]() |
bd5f553c3a | ||
![]() |
f5b2363fff | ||
![]() |
5b3bff22e2 | ||
![]() |
9c26093fea | ||
![]() |
7eeb292adf | ||
![]() |
8c7c4c764b | ||
![]() |
317436c057 | ||
![]() |
29973e5d02 | ||
![]() |
4525cad7af | ||
![]() |
62a00c4597 | ||
![]() |
8446665b02 | ||
![]() |
3169ce62d5 | ||
![]() |
218d4d4eea | ||
![]() |
083e20474b | ||
![]() |
f8ce51e5aa | ||
![]() |
e2bfdbe8b0 | ||
![]() |
e6cfa5028e | ||
![]() |
ac9faaf1c1 | ||
![]() |
42edb92232 | ||
![]() |
7987920f7e | ||
![]() |
20c8ee302c | ||
![]() |
4a7a2d6430 | ||
![]() |
abff2dcae2 | ||
![]() |
2771c8c3e5 | ||
![]() |
72e38370fe | ||
![]() |
1d9524cc59 | ||
![]() |
8173093c67 | ||
![]() |
f5cca2a288 | ||
![]() |
f2dcdfc22c | ||
![]() |
a5076d4cc4 | ||
![]() |
fff2008255 | ||
![]() |
b2510676b9 | ||
![]() |
a29154233b | ||
![]() |
5d15e3d367 | ||
![]() |
cc148ca895 | ||
![]() |
1a9a0d7ec8 | ||
![]() |
f3e305a646 | ||
![]() |
3d5e203d86 | ||
![]() |
32ad9c29be | ||
![]() |
aef8a7901f | ||
![]() |
d6fd5dec3b | ||
![]() |
810b609a80 | ||
![]() |
934fcd7e99 | ||
![]() |
5687aa6b50 | ||
![]() |
27cca111c8 | ||
![]() |
f807a290a4 | ||
![]() |
f27f67b76b | ||
![]() |
b8a5924295 | ||
![]() |
b008b147e5 | ||
![]() |
0389ce68e2 | ||
![]() |
d2decfa338 | ||
![]() |
3c685d7bf6 | ||
![]() |
aef5942d0f | ||
![]() |
2e4138d050 | ||
![]() |
931e716b97 | ||
![]() |
3d52cc8867 | ||
![]() |
d850b47f9e | ||
![]() |
9624de6c41 | ||
![]() |
c47e20c733 | ||
![]() |
e44c9b8e54 | ||
![]() |
7b18fab7a4 | ||
![]() |
28efc0e1c8 | ||
![]() |
d69ce546fa | ||
![]() |
87ca29173d | ||
![]() |
fe6e1ce28f | ||
![]() |
e82b87ca73 | ||
![]() |
cb2298e98b | ||
![]() |
1dd014cbed | ||
![]() |
d3af3a6de1 | ||
![]() |
32b4781da2 | ||
![]() |
078be52ff8 |
387 changed files with 28126 additions and 14290 deletions
|
@ -1,84 +1,6 @@
|
|||
[target.'cfg(all())']
|
||||
rustflags = [
|
||||
# Global lints/warnings.
|
||||
# See https://github.com/EmbarkStudios/rust-ecosystem/issues/22 for why we do this here
|
||||
"-Dunsafe_code",
|
||||
"-Wclippy::all",
|
||||
"-Wclippy::await_holding_lock",
|
||||
"-Wclippy::char_lit_as_u8",
|
||||
"-Wclippy::checked_conversions",
|
||||
"-Wclippy::dbg_macro",
|
||||
"-Wclippy::debug_assert_with_mut_call",
|
||||
"-Wclippy::disallowed_methods",
|
||||
"-Wclippy::doc_markdown",
|
||||
"-Wclippy::empty_enum",
|
||||
"-Wclippy::enum_glob_use",
|
||||
"-Wclippy::equatable_if_let",
|
||||
"-Wclippy::exit",
|
||||
"-Wclippy::expl_impl_clone_on_copy",
|
||||
"-Wclippy::explicit_deref_methods",
|
||||
"-Wclippy::explicit_into_iter_loop",
|
||||
"-Wclippy::fallible_impl_from",
|
||||
"-Wclippy::filter_map_next",
|
||||
"-Wclippy::flat_map_option",
|
||||
"-Wclippy::float_cmp_const",
|
||||
"-Wclippy::fn_params_excessive_bools",
|
||||
"-Wclippy::from_iter_instead_of_collect",
|
||||
"-Wclippy::if_let_mutex",
|
||||
"-Wclippy::implicit_clone",
|
||||
"-Wclippy::imprecise_flops",
|
||||
"-Wclippy::inefficient_to_string",
|
||||
"-Wclippy::invalid_upcast_comparisons",
|
||||
"-Wclippy::iter_not_returning_iterator",
|
||||
"-Wclippy::large_digit_groups",
|
||||
"-Wclippy::large_stack_arrays",
|
||||
"-Wclippy::large_types_passed_by_value",
|
||||
"-Wclippy::let_unit_value",
|
||||
"-Wclippy::linkedlist",
|
||||
"-Wclippy::lossy_float_literal",
|
||||
"-Wclippy::macro_use_imports",
|
||||
"-Wclippy::manual_ok_or",
|
||||
"-Wclippy::map_err_ignore",
|
||||
"-Wclippy::map_flatten",
|
||||
"-Wclippy::map_unwrap_or",
|
||||
"-Wclippy::match_on_vec_items",
|
||||
"-Wclippy::match_same_arms",
|
||||
"-Wclippy::match_wild_err_arm",
|
||||
"-Wclippy::match_wildcard_for_single_variants",
|
||||
"-Wclippy::mem_forget",
|
||||
"-Wclippy::mismatched_target_os",
|
||||
"-Wclippy::missing_enforced_import_renames",
|
||||
"-Wclippy::missing_errors_doc",
|
||||
"-Wclippy::missing_safety_doc",
|
||||
"-Wclippy::mut_mut",
|
||||
"-Wclippy::mutex_integer",
|
||||
"-Wclippy::needless_borrow",
|
||||
"-Wclippy::needless_continue",
|
||||
"-Wclippy::needless_for_each",
|
||||
"-Wclippy::needless_pass_by_value",
|
||||
"-Wclippy::option_option",
|
||||
"-Wclippy::path_buf_push_overwrite",
|
||||
"-Wclippy::ptr_as_ptr",
|
||||
"-Wclippy::rc_mutex",
|
||||
"-Wclippy::ref_option_ref",
|
||||
"-Wclippy::rest_pat_in_fully_bound_structs",
|
||||
"-Wclippy::same_functions_in_if_condition",
|
||||
"-Wclippy::semicolon_if_nothing_returned",
|
||||
"-Wclippy::single_match_else",
|
||||
"-Wclippy::string_add_assign",
|
||||
"-Wclippy::string_add",
|
||||
"-Wclippy::string_lit_as_bytes",
|
||||
"-Wclippy::string_to_string",
|
||||
"-Wclippy::todo",
|
||||
"-Wclippy::trait_duplication_in_bounds",
|
||||
"-Wclippy::unimplemented",
|
||||
"-Wclippy::unnested_or_patterns",
|
||||
"-Wclippy::unused_self",
|
||||
"-Wclippy::useless_transmute",
|
||||
"-Wclippy::verbose_file_reads",
|
||||
"-Wclippy::zero_sized_map_values",
|
||||
"-Wfuture_incompatible",
|
||||
"-Wnonstandard_style",
|
||||
"-Wrust_2018_idioms",
|
||||
"-Wrustdoc::missing_crate_level_docs",
|
||||
]
|
||||
# clipboard api is still unstable, so web-sys requires the below flag to be passed for copy (ctrl + c) to work
|
||||
# https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html
|
||||
# check status at https://developer.mozilla.org/en-US/docs/Web/API/Clipboard#browser_compatibility
|
||||
# we don't use `[build]` because of rust analyzer's build cache invalidation https://github.com/emilk/eframe_template/issues/93
|
||||
[target.wasm32-unknown-unknown]
|
||||
rustflags = ["--cfg=web_sys_unstable_apis"]
|
||||
|
|
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -8,7 +8,7 @@ assignees: ''
|
|||
---
|
||||
|
||||
<!--
|
||||
First look if there is already a similar bug report. If there is, add a comment to it instead!
|
||||
First look if there is already a similar bug report. If there is, upvote the issue with 👍
|
||||
|
||||
Please also check if the bug is still present in latest master! Do so by adding the following lines to your Cargo.toml:
|
||||
|
||||
|
|
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
@ -8,7 +8,7 @@ assignees: ''
|
|||
---
|
||||
|
||||
<!--
|
||||
First look if there is already a similar feature request. If there is, add a comment to it instead!
|
||||
First look if there is already a similar feature request. If there is, upvote the issue with 👍
|
||||
-->
|
||||
|
||||
|
||||
|
|
306
.github/workflows/rust.yml
vendored
306
.github/workflows/rust.yml
vendored
|
@ -3,181 +3,209 @@ on: [push, pull_request]
|
|||
name: CI
|
||||
|
||||
env:
|
||||
# This is required to enable the web_sys clipboard API which eframe web uses
|
||||
# web_sys_unstable_apis is required to enable the web_sys clipboard API which eframe web uses,
|
||||
# as well as by the wasm32-backend of the wgpu crate.
|
||||
# https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Clipboard.html
|
||||
# https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html
|
||||
RUSTFLAGS: --cfg=web_sys_unstable_apis
|
||||
RUSTFLAGS: --cfg=web_sys_unstable_apis -D warnings
|
||||
RUSTDOCFLAGS: -D warnings
|
||||
|
||||
jobs:
|
||||
check_default:
|
||||
name: cargo check (default features)
|
||||
runs-on: ubuntu-latest
|
||||
fmt-crank-check-test:
|
||||
name: Format + check + test
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: 1.60.0
|
||||
override: true
|
||||
- run: sudo apt-get update && sudo apt-get install libspeechd-dev libgtk-3-dev
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
|
||||
check_all_features:
|
||||
name: cargo check --all-features
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: 1.60.0
|
||||
profile: default
|
||||
toolchain: 1.65.0
|
||||
override: true
|
||||
- run: sudo apt-get update && sudo apt-get install libspeechd-dev libgtk-3-dev
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
args: --all-features
|
||||
|
||||
check_web_default:
|
||||
name: cargo check web (default features)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
- name: Install packages (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
#uses: awalsh128/cache-apt-pkgs-action@v1.2.2
|
||||
#TODO(emilk) use upstream when https://github.com/awalsh128/cache-apt-pkgs-action/pull/90 is merged
|
||||
uses: rerun-io/cache-apt-pkgs-action@59534850182063abf1b2c11bb3686722a12a8397
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: 1.60.0
|
||||
override: true
|
||||
- run: rustup target add wasm32-unknown-unknown
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
args: -p egui_demo_app --lib --target wasm32-unknown-unknown
|
||||
packages: libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev libgtk-3-dev # libgtk-3-dev is used by rfd
|
||||
version: 1.0
|
||||
execute_install_scripts: true
|
||||
|
||||
check_egui_demo_app:
|
||||
name: cargo check -p egui_demo_app
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: 1.60.0
|
||||
override: true
|
||||
- name: check
|
||||
run: cargo check -p egui_demo_app
|
||||
- name: Set up cargo cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
check_wasm_eframe_with_features:
|
||||
name: cargo check wasm eframe
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: 1.60.0
|
||||
override: true
|
||||
- run: rustup target add wasm32-unknown-unknown
|
||||
- name: check
|
||||
run: cargo check -p eframe --lib --no-default-features --features persistence --target wasm32-unknown-unknown
|
||||
|
||||
check_web_all_features:
|
||||
name: cargo check web --all-features
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: 1.60.0
|
||||
override: true
|
||||
- run: rustup target add wasm32-unknown-unknown
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
args: -p egui_demo_app --lib --target wasm32-unknown-unknown --all-features
|
||||
|
||||
test:
|
||||
name: cargo test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: 1.60.0
|
||||
override: true
|
||||
- run: sudo apt-get update && sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libspeechd-dev libxkbcommon-dev libssl-dev libgtk-3-dev # libgtk-3-dev is used by rfd
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --all-features
|
||||
|
||||
fmt:
|
||||
name: cargo fmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: 1.60.0
|
||||
override: true
|
||||
- run: rustup component add rustfmt
|
||||
- uses: actions-rs/cargo@v1
|
||||
- name: Rustfmt
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
|
||||
clippy:
|
||||
name: cargo clippy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
- name: Install cargo-cranky
|
||||
uses: baptiste0928/cargo-install@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: 1.60.0
|
||||
override: true
|
||||
- run: rustup component add clippy
|
||||
- run: sudo apt-get update && sudo apt-get install libspeechd-dev libgtk-3-dev # libgtk-3-dev is used by rfd
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --workspace --all-targets --all-features -- -D warnings -W clippy::all
|
||||
crate: cargo-cranky
|
||||
|
||||
doc:
|
||||
name: cargo doc
|
||||
runs-on: ubuntu-latest
|
||||
- name: check --all-features
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
args: --locked --all-features --all-targets
|
||||
|
||||
- name: check default features
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
args: --locked --all-targets
|
||||
|
||||
- name: check --no-default-features
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
args: --locked --no-default-features --lib --all-targets
|
||||
|
||||
- name: check eframe --no-default-features
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
args: --locked --no-default-features --lib --all-targets -p eframe
|
||||
|
||||
- name: Test doc-tests
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --doc --all-features
|
||||
|
||||
- name: cargo doc --lib
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: doc
|
||||
args: --lib --no-deps --all-features
|
||||
|
||||
- name: cargo doc --document-private-items
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: doc
|
||||
args: --document-private-items --no-deps --all-features
|
||||
|
||||
- name: Test
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --all-features
|
||||
|
||||
- name: Cranky
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: cranky
|
||||
args: --all-targets --all-features -- -D warnings
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
check_wasm:
|
||||
name: Check wasm32 + wasm-bindgen
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: 1.60.0
|
||||
toolchain: 1.65.0
|
||||
target: wasm32-unknown-unknown
|
||||
override: true
|
||||
- run: sudo apt-get update && sudo apt-get install libspeechd-dev
|
||||
- run: cargo doc --lib --no-deps --all-features
|
||||
|
||||
- run: sudo apt-get update && sudo apt-get install libgtk-3-dev
|
||||
|
||||
- name: Set up cargo cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Install cargo-cranky
|
||||
uses: baptiste0928/cargo-install@v1
|
||||
with:
|
||||
crate: cargo-cranky
|
||||
|
||||
- name: Check wasm32 egui_demo_app
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
args: -p egui_demo_app --lib --target wasm32-unknown-unknown
|
||||
|
||||
- name: Check wasm32 egui_demo_app --all-features
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
args: -p egui_demo_app --lib --target wasm32-unknown-unknown --all-features
|
||||
|
||||
- name: Check wasm32 eframe
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
args: -p eframe --lib --no-default-features --features glow,persistence --target wasm32-unknown-unknown
|
||||
|
||||
- name: wasm-bindgen
|
||||
uses: jetli/wasm-bindgen-action@v0.1.0
|
||||
with:
|
||||
version: "0.2.84"
|
||||
|
||||
- run: ./sh/wasm_bindgen_check.sh --skip-setup
|
||||
|
||||
- name: Cranky wasm32
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: cranky
|
||||
args: --target wasm32-unknown-unknown --all-features -p egui_demo_app --lib -- -D warnings
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
cargo-deny:
|
||||
runs-on: ubuntu-20.04
|
||||
name: cargo deny
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: EmbarkStudios/cargo-deny-action@v1
|
||||
with:
|
||||
rust-version: "1.65.0"
|
||||
|
||||
wasm_bindgen:
|
||||
name: wasm-bindgen
|
||||
runs-on: ubuntu-latest
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
android:
|
||||
name: android
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: 1.65.0
|
||||
target: aarch64-linux-android
|
||||
override: true
|
||||
|
||||
- name: Set up cargo cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- run: cargo check --features wgpu --target aarch64-linux-android
|
||||
working-directory: crates/eframe
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
windows:
|
||||
name: Check Windows
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: 1.60.0
|
||||
toolchain: 1.65.0
|
||||
override: true
|
||||
- run: rustup target add wasm32-unknown-unknown
|
||||
- run: cargo install wasm-bindgen-cli
|
||||
- run: ./sh/wasm_bindgen_check.sh
|
||||
|
||||
- name: Set up cargo cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Check
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
args: --all-targets --all-features
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,4 +1,7 @@
|
|||
.DS_Store
|
||||
**/target
|
||||
**/target_ra
|
||||
**/target_wasm
|
||||
/.*.json
|
||||
/.vscode
|
||||
/media/*
|
||||
|
|
29
.vscode/settings.json
vendored
29
.vscode/settings.json
vendored
|
@ -1,5 +1,32 @@
|
|||
{
|
||||
"files.insertFinalNewline": true,
|
||||
"editor.formatOnSave": true,
|
||||
"files.trimTrailingWhitespace": true
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"editor.semanticTokenColorCustomizations": {
|
||||
"rules": {
|
||||
"*.unsafe:rust": "#eb5046"
|
||||
}
|
||||
},
|
||||
"files.exclude": {
|
||||
"target/**": true,
|
||||
"target_ra/**": true,
|
||||
},
|
||||
// Tell Rust Analyzer to use its own target directory, so we don't need to wait for it to finish wen we want to `cargo run`
|
||||
"rust-analyzer.checkOnSave.overrideCommand": [
|
||||
"cargo",
|
||||
"cranky",
|
||||
"--target-dir=target_ra",
|
||||
"--workspace",
|
||||
"--message-format=json",
|
||||
"--all-targets"
|
||||
],
|
||||
"rust-analyzer.cargo.buildScripts.overrideCommand": [
|
||||
"cargo",
|
||||
"check",
|
||||
"--quiet",
|
||||
"--target-dir=target_ra",
|
||||
"--workspace",
|
||||
"--message-format=json",
|
||||
"--all-targets"
|
||||
],
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Arcitecture
|
||||
# Architecture
|
||||
This document describes how the crates that make up egui are all connected.
|
||||
|
||||
Also see [`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/master/CONTRIBUTING.md) for what to do before opening a PR.
|
||||
|
|
295
CHANGELOG.md
295
CHANGELOG.md
|
@ -1,12 +1,153 @@
|
|||
# egui changelog
|
||||
All notable changes to the `egui` crate will be documented in this file.
|
||||
|
||||
NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui-winit`](egui-winit/CHANGELOG.md), [`egui_glium`](egui_glium/CHANGELOG.md), and [`egui_glow`](egui_glow/CHANGELOG.md) have their own changelogs!
|
||||
NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG.md), [`egui-winit`](crates/egui-winit/CHANGELOG.md), [`egui_glium`](crates/egui_glium/CHANGELOG.md), [`egui_glow`](crates/egui_glow/CHANGELOG.md) and [`egui-wgpu`](crates/egui-wgpu/CHANGELOG.md) have their own changelogs!
|
||||
|
||||
|
||||
## Unreleased
|
||||
|
||||
|
||||
## 0.21.0 - 2023-02-08 - Deadlock fix and style customizability
|
||||
* ⚠️ BREAKING: `egui::Context` now use closures for locking ([#2625](https://github.com/emilk/egui/pull/2625)):
|
||||
* `ctx.input().key_pressed(Key::A)` -> `ctx.input(|i| i.key_pressed(Key::A))`
|
||||
* `ui.memory().toggle_popup(popup_id)` -> `ui.memory_mut(|mem| mem.toggle_popup(popup_id))`
|
||||
|
||||
### Added ⭐
|
||||
* Add `Response::drag_started_by` and `Response::drag_released_by` for convenience, similar to `dragged` and `dragged_by` ([#2507](https://github.com/emilk/egui/pull/2507)).
|
||||
* Add `PointerState::*_pressed` to check if the given button was pressed in this frame ([#2507](https://github.com/emilk/egui/pull/2507)).
|
||||
* `Event::Key` now has a `repeat` field that is set to `true` if the event was the result of a key-repeat ([#2435](https://github.com/emilk/egui/pull/2435)).
|
||||
* Add `Slider::drag_value_speed`, which lets you ask for finer precision when dragging the slider value rather than the actual slider.
|
||||
* Add `Memory::any_popup_open`, which returns true if any popup is currently open ([#2464](https://github.com/emilk/egui/pull/2464)).
|
||||
* Add `Plot::clamp_grid` to only show grid where there is data ([#2480](https://github.com/emilk/egui/pull/2480)).
|
||||
* Add `ScrollArea::drag_to_scroll` if you want to turn off that feature.
|
||||
* Add `Response::on_hover_and_drag_cursor`.
|
||||
* Add `Window::default_open` ([#2539](https://github.com/emilk/egui/pull/2539)).
|
||||
* Add `ProgressBar::fill` if you want to set the fill color manually. ([#2618](https://github.com/emilk/egui/pull/2618)).
|
||||
* Add `Button::rounding` to enable round buttons ([#2616](https://github.com/emilk/egui/pull/2616)).
|
||||
* Add `WidgetVisuals::optional_bg_color` - set it to `Color32::TRANSPARENT` to hide button backgrounds ([#2621](https://github.com/emilk/egui/pull/2621)).
|
||||
* Add `Context::screen_rect` and `Context::set_cursor_icon` ([#2625](https://github.com/emilk/egui/pull/2625)).
|
||||
* You can turn off the vertical line left of indented regions with `Visuals::indent_has_left_vline` ([#2636](https://github.com/emilk/egui/pull/2636)).
|
||||
* Add `Response.highlight` to highlight a widget ([#2632](https://github.com/emilk/egui/pull/2632)).
|
||||
* Add `Separator::grow` and `Separator::shrink` ([#2665](https://github.com/emilk/egui/pull/2665)).
|
||||
* Add `Slider::trailing_fill` for trailing color behind the circle like a `ProgressBar` ([#2660](https://github.com/emilk/egui/pull/2660)).
|
||||
|
||||
### Changed 🔧
|
||||
* Improved plot grid appearance ([#2412](https://github.com/emilk/egui/pull/2412)).
|
||||
* Improved the algorithm for picking the number of decimals to show when hovering values in the `Plot`.
|
||||
* Default `ComboBox` is now controlled with `Spacing::combo_width` ([#2621](https://github.com/emilk/egui/pull/2621)).
|
||||
* `DragValue` and `Slider` now use the proportional font ([#2638](https://github.com/emilk/egui/pull/2638)).
|
||||
* `ScrollArea` is less aggressive about clipping its contents ([#2665](https://github.com/emilk/egui/pull/2665)).
|
||||
* Updated to be compatible with a major breaking change in AccessKit that drastically reduces memory usage when accessibility is enabled ([#2678](https://github.com/emilk/egui/pull/2678)).
|
||||
* Improve `DragValue` behavior ([#2649](https://github.com/emilk/egui/pull/2649), [#2650](https://github.com/emilk/egui/pull/2650), [#2688](https://github.com/emilk/egui/pull/2688), [#2638](https://github.com/emilk/egui/pull/2638)).
|
||||
|
||||
### Fixed 🐛
|
||||
* Trigger `PointerEvent::Released` for drags ([#2507](https://github.com/emilk/egui/pull/2507)).
|
||||
* Expose `TextEdit`'s multiline flag to AccessKit ([#2448](https://github.com/emilk/egui/pull/2448)).
|
||||
* Don't render `\r` (Carriage Return) ([#2452](https://github.com/emilk/egui/pull/2452)).
|
||||
* The `button_padding` style option works closer as expected with image+text buttons now ([#2510](https://github.com/emilk/egui/pull/2510)).
|
||||
* Menus are now moved to fit on the screen.
|
||||
* Fix `Window::pivot` causing windows to move around ([#2694](https://github.com/emilk/egui/pull/2694)).
|
||||
|
||||
|
||||
## 0.20.1 - 2022-12-11 - Fix key-repeat
|
||||
### Changed 🔧
|
||||
* `InputState`: all press functions again include key repeats (like in egui 0.19) ([#2429](https://github.com/emilk/egui/pull/2429)).
|
||||
* Improve the look of thin white lines ([#2437](https://github.com/emilk/egui/pull/2437)).
|
||||
|
||||
### Fixed 🐛
|
||||
* Fix key-repeats for `TextEdit`, `Slider`s, etc ([#2429](https://github.com/emilk/egui/pull/2429)).
|
||||
|
||||
|
||||
## 0.20.0 - 2022-12-08 - AccessKit, prettier text, overlapping widgets
|
||||
* MSRV (Minimum Supported Rust Version) is now `1.65.0` ([#2314](https://github.com/emilk/egui/pull/2314)).
|
||||
* ⚠️ BREAKING: egui now expects integrations to do all color blending in gamma space ([#2071](https://github.com/emilk/egui/pull/2071)).
|
||||
* ⚠️ BREAKING: if you have overlapping interactive widgets, only the top widget (last added) will be interactive ([#2244](https://github.com/emilk/egui/pull/2244)).
|
||||
|
||||
### Added ⭐
|
||||
* Added helper functions for animating panels that collapse/expand ([#2190](https://github.com/emilk/egui/pull/2190)).
|
||||
* Added `Context::os/Context::set_os` to query/set what operating system egui believes it is running on ([#2202](https://github.com/emilk/egui/pull/2202)).
|
||||
* Added `Button::shortcut_text` for showing keyboard shortcuts in menu buttons ([#2202](https://github.com/emilk/egui/pull/2202)).
|
||||
* Added `egui::KeyboardShortcut` for showing keyboard shortcuts in menu buttons ([#2202](https://github.com/emilk/egui/pull/2202)).
|
||||
* Texture loading now takes a `TexureOptions` with minification and magnification filters ([#2224](https://github.com/emilk/egui/pull/2224)).
|
||||
* Added `Key::Minus` and `Key::Equals` ([#2239](https://github.com/emilk/egui/pull/2239)).
|
||||
* Added `egui::gui_zoom` module with helpers for scaling the whole GUI of an app ([#2239](https://github.com/emilk/egui/pull/2239)).
|
||||
* You can now put one interactive widget on top of another, and only one will get interaction at a time ([#2244](https://github.com/emilk/egui/pull/2244)).
|
||||
* Added `spacing.menu_margin` for customizing menu spacing ([#2036](https://github.com/emilk/egui/pull/2036))
|
||||
* Added possibility to enable text wrap for the selected text of `egui::ComboBox` ([#2272](https://github.com/emilk/egui/pull/2272))
|
||||
* Added `Area::constrain` and `Window::constrain` which constrains area to the screen bounds ([#2270](https://github.com/emilk/egui/pull/2270)).
|
||||
* Added `Area::pivot` and `Window::pivot` which controls what part of the window to position ([#2303](https://github.com/emilk/egui/pull/2303)).
|
||||
* Added support for [thin space](https://en.wikipedia.org/wiki/Thin_space).
|
||||
* Added optional integration with [AccessKit](https://accesskit.dev/) for implementing platform accessibility APIs ([#2294](https://github.com/emilk/egui/pull/2294)).
|
||||
* Added `panel_fill`, `window_fill` and `window_stroke` to `Visuals` for your theming pleasure ([#2406](https://github.com/emilk/egui/pull/2406)).
|
||||
* Plots:
|
||||
* Allow linking plot cursors ([#1722](https://github.com/emilk/egui/pull/1722)).
|
||||
* Added `Plot::auto_bounds_x/y` and `Plot::reset` ([#2029](https://github.com/emilk/egui/pull/2029)).
|
||||
* Added `PlotUi::translate_bounds` ([#2145](https://github.com/emilk/egui/pull/2145)).
|
||||
* Added `PlotUi::set_plot_bounds` ([#2320](https://github.com/emilk/egui/pull/2320)).
|
||||
* Added `PlotUi::plot_secondary_clicked` ([#2318](https://github.com/emilk/egui/pull/2318)).
|
||||
|
||||
### Changed 🔧
|
||||
* Panels always have a separator line, but no stroke on other sides. Their spacing has also changed slightly ([#2261](https://github.com/emilk/egui/pull/2261)).
|
||||
* Tooltips are only shown when mouse pointer is still ([#2263](https://github.com/emilk/egui/pull/2263)).
|
||||
* Make it slightly easier to click buttons ([#2304](https://github.com/emilk/egui/pull/2304)).
|
||||
* `egui::color` has been renamed `egui::ecolor` ([#2399](https://github.com/emilk/egui/pull/2399)).
|
||||
|
||||
### Fixed 🐛
|
||||
* ⚠️ BREAKING: Fix text being too small ([#2069](https://github.com/emilk/egui/pull/2069)).
|
||||
* Improve mixed CJK/Latin line-breaking ([#1986](https://github.com/emilk/egui/pull/1986)).
|
||||
* Improved text rendering ([#2071](https://github.com/emilk/egui/pull/2071)).
|
||||
* Constrain menu popups to the screen ([#2191](https://github.com/emilk/egui/pull/2191)).
|
||||
* Less jitter when calling `Context::set_pixels_per_point` ([#2239](https://github.com/emilk/egui/pull/2239)).
|
||||
* Fixed popups and color edit going outside the screen.
|
||||
* Fixed keyboard support in `DragValue` ([#2342](https://github.com/emilk/egui/pull/2342)).
|
||||
* If you nest `ScrollAreas` inside each other, the inner area will now move its scroll bar so it is always visible ([#2371](https://github.com/emilk/egui/pull/2371)).
|
||||
* Ignore key-repeats for `input.key_pressed` ([#2334](https://github.com/emilk/egui/pull/2334), [#2389](https://github.com/emilk/egui/pull/2389)).
|
||||
* Fixed issue with calling `set_pixels_per_point` each frame ([#2352](https://github.com/emilk/egui/pull/2352)).
|
||||
* Fix bug in `ScrollArea::show_rows` ([#2258](https://github.com/emilk/egui/pull/2258)).
|
||||
* Fix bug in `plot::Line::fill` ([#2275](https://github.com/emilk/egui/pull/2275)).
|
||||
* Only emit `changed` events in `radio_value` and `selectable_value` if the value actually changed ([#2343](https://github.com/emilk/egui/pull/2343)).
|
||||
* Fixed sizing bug in `Grid` ([#2384](https://github.com/emilk/egui/pull/2384)).
|
||||
* `ComboBox::width` now correctly sets the outer width ([#2406](https://github.com/emilk/egui/pull/2406)).
|
||||
|
||||
|
||||
## 0.19.0 - 2022-08-20
|
||||
### Added ⭐
|
||||
* Added `*_released` & `*_clicked` methods for `PointerState` ([#1582](https://github.com/emilk/egui/pull/1582)).
|
||||
* Added `PointerButton::Extra1` and `PointerButton::Extra2` ([#1592](https://github.com/emilk/egui/pull/1592)).
|
||||
* Added `egui::hex_color!` to create `Color32`'s from hex strings under the `color-hex` feature ([#1596](https://github.com/emilk/egui/pull/1596)).
|
||||
* Optimized painting of filled circles (e.g. for scatter plots) by 10x or more ([#1616](https://github.com/emilk/egui/pull/1616)).
|
||||
* Added opt-in feature `deadlock_detection` to detect double-lock of mutexes on the same thread ([#1619](https://github.com/emilk/egui/pull/1619)).
|
||||
* Added `InputState::stable_dt`: a more stable estimate for the delta-time in reactive mode ([#1625](https://github.com/emilk/egui/pull/1625)).
|
||||
* You can now specify a texture filter for your textures ([#1636](https://github.com/emilk/egui/pull/1636)).
|
||||
* Added functions keys in `egui::Key` ([#1665](https://github.com/emilk/egui/pull/1665)).
|
||||
* Added support for using `PaintCallback` shapes with the WGPU backend ([#1684](https://github.com/emilk/egui/pull/1684)).
|
||||
* Added `Context::request_repaint_after` ([#1694](https://github.com/emilk/egui/pull/1694)).
|
||||
* `ctrl-h` now acts like backspace in `TextEdit` ([#1812](https://github.com/emilk/egui/pull/1812)).
|
||||
* Added `custom_formatter` method for `Slider` and `DragValue` ([#1851](https://github.com/emilk/egui/issues/1851)).
|
||||
* Added `RawInput::has_focus` which backends can set to indicate whether the UI as a whole has the keyboard focus ([#1859](https://github.com/emilk/egui/pull/1859)).
|
||||
* Added `PointerState::button_double_clicked()` and `PointerState::button_triple_clicked()` ([#1906](https://github.com/emilk/egui/issues/1906)).
|
||||
* Added `custom_formatter`, `binary`, `octal`, and `hexadecimal` to `DragValue` and `Slider` ([#1953](https://github.com/emilk/egui/issues/1953))
|
||||
|
||||
### Changed 🔧
|
||||
* MSRV (Minimum Supported Rust Version) is now `1.61.0` ([#1846](https://github.com/emilk/egui/pull/1846)).
|
||||
* `PaintCallback` shapes now require the whole callback to be put in an `Arc<dyn Any>` with the value being a backend-specific callback type ([#1684](https://github.com/emilk/egui/pull/1684)).
|
||||
* Replaced `needs_repaint` in `FullOutput` with `repaint_after`. Used to force repaint after the set duration in reactive mode ([#1694](https://github.com/emilk/egui/pull/1694)).
|
||||
* `Layout::left_to_right` and `Layout::right_to_left` now takes the vertical align as an argument. Previous default was `Align::Center`.
|
||||
* Improved ergonomics of adding plot items. All plot items that take a series of 2D coordinates can now be created directly from `Vec<[f64; 2]>`. The `Value` and `Values` types were removed in favor of `PlotPoint` and `PlotPoints` respectively ([#1816](https://github.com/emilk/egui/pull/1816)).
|
||||
* `TextBuffer` no longer needs to implement `AsRef<str>` ([#1824](https://github.com/emilk/egui/pull/1824)).
|
||||
|
||||
### Fixed 🐛
|
||||
* Fixed `Response::changed` for `ui.toggle_value` ([#1573](https://github.com/emilk/egui/pull/1573)).
|
||||
* Fixed `ImageButton`'s changing background padding on hover ([#1595](https://github.com/emilk/egui/pull/1595)).
|
||||
* Fixed `Plot` auto-bounds bug ([#1599](https://github.com/emilk/egui/pull/1599)).
|
||||
* Fixed dead-lock when alt-tabbing while also showing a tooltip ([#1618](https://github.com/emilk/egui/pull/1618)).
|
||||
* Fixed `ScrollArea` scrolling when editing an unrelated `TextEdit` ([#1779](https://github.com/emilk/egui/pull/1779)).
|
||||
* Fixed `Slider` not always generating events on change ([#1854](https://github.com/emilk/egui/pull/1854)).
|
||||
* Fixed jitter of anchored windows for the first frame ([#1856](https://github.com/emilk/egui/pull/1856)).
|
||||
* Fixed focus behavior when pressing Tab in a UI with no focused widget ([#1861](https://github.com/emilk/egui/pull/1861)).
|
||||
* Fixed automatic plot bounds ([#1865](https://github.com/emilk/egui/pull/1865)).
|
||||
|
||||
|
||||
## 0.18.1 - 2022-05-01
|
||||
* Change `Shape::Callback` from `&dyn Any` to `&mut dyn Any` to support more backends.
|
||||
|
||||
|
@ -36,7 +177,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui-w
|
|||
* `ClippedMesh` has been replaced with `ClippedPrimitive` ([#1351](https://github.com/emilk/egui/pull/1351)).
|
||||
* Renamed `Frame::margin` to `Frame::inner_margin`.
|
||||
* Renamed `AlphaImage` to `FontImage` to discourage any other use for it ([#1412](https://github.com/emilk/egui/pull/1412)).
|
||||
* Warnings will pe painted on screen when there is an `Id` clash for `Grid`, `Plot` or `ScrollArea` ([#1452](https://github.com/emilk/egui/pull/1452)).
|
||||
* Warnings will be painted on screen when there is an `Id` clash for `Grid`, `Plot` or `ScrollArea` ([#1452](https://github.com/emilk/egui/pull/1452)).
|
||||
* `Checkbox` and `RadioButton` with an empty label (`""`) will now take up much less space ([#1456](https://github.com/emilk/egui/pull/1456)).
|
||||
* Replaced `Memory::top_most_layer` with more flexible `Memory::layer_ids`.
|
||||
* Renamed the feature `convert_bytemuck` to `bytemuck` ([#1467](https://github.com/emilk/egui/pull/1467)).
|
||||
|
@ -47,9 +188,9 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui-w
|
|||
* Fixed `ComboBox`es always being rendered left-aligned ([#1304](https://github.com/emilk/egui/pull/1304)).
|
||||
* Fixed ui code that could lead to a deadlock ([#1380](https://github.com/emilk/egui/pull/1380)).
|
||||
* Text is darker and more readable in bright mode ([#1412](https://github.com/emilk/egui/pull/1412)).
|
||||
* Fix a lot of broken/missing doclinks ([#1419](https://github.com/emilk/egui/pull/1419)).
|
||||
* Fixed a lot of broken/missing doclinks ([#1419](https://github.com/emilk/egui/pull/1419)).
|
||||
* Fixed `Ui::add_visible` sometimes leaving the `Ui` in a disabled state ([#1436](https://github.com/emilk/egui/issues/1436)).
|
||||
* Add line breaking rules for Japanese text ([#1498](https://github.com/emilk/egui/pull/1498)).
|
||||
* Added line breaking rules for Japanese text ([#1498](https://github.com/emilk/egui/pull/1498)).
|
||||
|
||||
### Deprecated ☢️
|
||||
* Deprecated `CollapsingHeader::selectable` ([#1538](https://github.com/emilk/egui/pull/1538)).
|
||||
|
@ -185,7 +326,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui-w
|
|||
## 0.16.1 - 2021-12-31 - Add back `CtxRef::begin_frame,end_frame`
|
||||
|
||||
### Added ⭐
|
||||
* Add back `CtxRef::begin_frame,end_frame` as an alternative to `CtxRef::run`.
|
||||
* Added back `CtxRef::begin_frame,end_frame` as an alternative to `CtxRef::run`.
|
||||
|
||||
|
||||
## 0.16.0 - 2021-12-29 - Context menus and rich text
|
||||
|
@ -244,15 +385,15 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui-w
|
|||
<img src="media/egui-0.15-code-editor.gif">
|
||||
|
||||
### Added ⭐
|
||||
* Add horizontal scrolling support to `ScrollArea` and `Window` (opt-in).
|
||||
* Added horizontal scrolling support to `ScrollArea` and `Window` (opt-in).
|
||||
* `TextEdit::layouter`: Add custom text layout for e.g. syntax highlighting or WYSIWYG.
|
||||
* `Fonts::layout_job`: New text layout engine allowing mixing fonts, colors and styles, with underlining and strikethrough.
|
||||
* Add `ui.add_enabled(bool, widget)` to easily add a possibly disabled widget.
|
||||
* Add `ui.add_enabled_ui(bool, |ui| …)` to create a possibly disabled UI section.
|
||||
* Add feature `"serialize"` separatedly from `"persistence"`.
|
||||
* Add `egui::widgets::global_dark_light_mode_buttons` to easily add buttons for switching the egui theme.
|
||||
* Added `ui.add_enabled(bool, widget)` to easily add a possibly disabled widget.
|
||||
* Added `ui.add_enabled_ui(bool, |ui| …)` to create a possibly disabled UI section.
|
||||
* Added feature `"serialize"` separatedly from `"persistence"`.
|
||||
* Added `egui::widgets::global_dark_light_mode_buttons` to easily add buttons for switching the egui theme.
|
||||
* `TextEdit` can now be used to show text which can be selected and copied, but not edited.
|
||||
* Add `Memory::caches` for caching things from one frame to the next.
|
||||
* Added `Memory::caches` for caching things from one frame to the next.
|
||||
|
||||
### Changed 🔧
|
||||
* Change the default monospace font to [Hack](https://github.com/source-foundry/Hack).
|
||||
|
@ -266,14 +407,14 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui-w
|
|||
* Smaller and nicer color picker.
|
||||
* `ScrollArea` will auto-shrink to content size unless told otherwise using `ScollArea::auto_shrink`.
|
||||
* By default, `Slider`'s `clamp_to_range` is set to true.
|
||||
* Rename `TextEdit::enabled` to `TextEdit::interactive`.
|
||||
* Renamed `TextEdit::enabled` to `TextEdit::interactive`.
|
||||
* `ui.label` (and friends) now take `impl ToString` as argument instead of `impl Into<Label>`.
|
||||
|
||||
### Fixed 🐛
|
||||
* Fix wrongly sized multiline `TextEdit` in justified layouts.
|
||||
* Fix clip rectangle of windows that don't fit the central area.
|
||||
* Fixed wrongly sized multiline `TextEdit` in justified layouts.
|
||||
* Fixed clip rectangle of windows that don't fit the central area.
|
||||
* Show tooltips above widgets on touch screens.
|
||||
* Fix popups sometimes getting clipped by panels.
|
||||
* Fixed popups sometimes getting clipped by panels.
|
||||
|
||||
### Removed 🔥
|
||||
* Replace `Button::enabled` with `ui.add_enabled`.
|
||||
|
@ -298,20 +439,20 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui-w
|
|||
## 0.14.2 - 2021-08-28 - Window resize fix
|
||||
|
||||
### Fixed 🐛
|
||||
* Fix window resize bug introduced in `0.14.1`.
|
||||
* Fixed window resize bug introduced in `0.14.1`.
|
||||
|
||||
|
||||
## 0.14.1 - 2021-08-28 - Layout bug fixes
|
||||
|
||||
### Added ⭐
|
||||
* Add `Ui::horizontal_top`.
|
||||
* Added `Ui::horizontal_top`.
|
||||
|
||||
### Fixed 🐛
|
||||
* Fix `set_width/set_min_width/set_height/set_min_height/expand_to_include_x/expand_to_include_y`.
|
||||
* Fixed `set_width/set_min_width/set_height/set_min_height/expand_to_include_x/expand_to_include_y`.
|
||||
* Make minimum grid column width propagate properly.
|
||||
* Make sure `TextEdit` contents expand to fill width if applicable.
|
||||
* `ProgressBar`: add a minimum width and fix for having it in an infinite layout.
|
||||
* Fix sometimes not being able to click inside a combo box or popup menu.
|
||||
* Fixed sometimes not being able to click inside a combo box or popup menu.
|
||||
|
||||
|
||||
## 0.14.0 - 2021-08-24 - Ui panels and bug fixes
|
||||
|
@ -320,10 +461,10 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui-w
|
|||
* Panels can now be added to any `Ui`.
|
||||
* Plot:
|
||||
* [Line styles](https://github.com/emilk/egui/pull/482).
|
||||
* Add `show_background` and `show_axes` methods to `Plot`.
|
||||
* Added `show_background` and `show_axes` methods to `Plot`.
|
||||
* [Progress bar](https://github.com/emilk/egui/pull/519).
|
||||
* `Grid::num_columns`: allow the last column to take up the rest of the space of the parent `Ui`.
|
||||
* Add an API for dropping files into egui (see `RawInput`).
|
||||
* Added an API for dropping files into egui (see `RawInput`).
|
||||
* `CollapsingHeader` can now optionally be selectable.
|
||||
|
||||
### Changed 🔧
|
||||
|
@ -333,12 +474,12 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui-w
|
|||
* Tooltips are now moved to not cover the widget they are attached to.
|
||||
|
||||
### Fixed 🐛
|
||||
* Fix custom font definitions getting replaced when `pixels_per_point` is changed.
|
||||
* Fix `lost_focus` for `TextEdit`.
|
||||
* Fixed custom font definitions getting replaced when `pixels_per_point` is changed.
|
||||
* Fixed `lost_focus` for `TextEdit`.
|
||||
* Clicking the edge of a menu button will now properly open the menu.
|
||||
* Fix hover detection close to an `Area`.
|
||||
* Fix case where `Plot`'s `min_auto_bounds` could be ignored after the first call to `Plot::ui`.
|
||||
* Fix slow startup when using large font files.
|
||||
* Fixed hover detection close to an `Area`.
|
||||
* Fixed case where `Plot`'s `min_auto_bounds` could be ignored after the first call to `Plot::ui`.
|
||||
* Fixed slow startup when using large font files.
|
||||
|
||||
### Contributors 🙏
|
||||
* [barrowsys](https://github.com/barrowsys)
|
||||
|
@ -370,19 +511,19 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui-w
|
|||
* [Plot legend improvements](https://github.com/emilk/egui/pull/410).
|
||||
* [Line markers for plots](https://github.com/emilk/egui/pull/363).
|
||||
* Panels:
|
||||
* Add right and bottom panels (`SidePanel::right` and `Panel::bottom`).
|
||||
* Added right and bottom panels (`SidePanel::right` and `Panel::bottom`).
|
||||
* Panels can now be resized.
|
||||
* Add an option to overwrite frame of a `Panel`.
|
||||
* Added an option to overwrite frame of a `Panel`.
|
||||
* [Improve accessibility / screen reader](https://github.com/emilk/egui/pull/412).
|
||||
* Add `ScrollArea::show_rows` for efficient scrolling of huge UI:s.
|
||||
* Add `ScrollArea::enable_scrolling` to allow freezing scrolling when editing TextEdit widgets within it
|
||||
* Add `Ui::set_visible` as a way to hide widgets.
|
||||
* Add `Style::override_text_style` to easily change the text style of everything in a `Ui` (or globally).
|
||||
* Added `ScrollArea::show_rows` for efficient scrolling of huge UI:s.
|
||||
* Added `ScrollArea::enable_scrolling` to allow freezing scrolling when editing TextEdit widgets within it
|
||||
* Added `Ui::set_visible` as a way to hide widgets.
|
||||
* Added `Style::override_text_style` to easily change the text style of everything in a `Ui` (or globally).
|
||||
* You can now change `TextStyle` on checkboxes, radio buttons and `SelectableLabel`.
|
||||
* Add support for [cint](https://crates.io/crates/cint) under `cint` feature.
|
||||
* Add features `extra_asserts` and `extra_debug_asserts` to enable additional checks.
|
||||
* Added support for [cint](https://crates.io/crates/cint) under `cint` feature.
|
||||
* Added features `extra_asserts` and `extra_debug_asserts` to enable additional checks.
|
||||
* `TextEdit` now supports edits on a generic buffer using `TextBuffer`.
|
||||
* Add `Context::set_debug_on_hover` and `egui::trace!(ui)`
|
||||
* Added `Context::set_debug_on_hover` and `egui::trace!(ui)`
|
||||
|
||||
### Changed 🔧
|
||||
* Minimum Rust version is now 1.51 (used to be 1.52)
|
||||
|
@ -393,36 +534,36 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui-w
|
|||
* `SidePanel::left` is resizable by default.
|
||||
|
||||
### Fixed 🐛
|
||||
* Fix uneven lettering on non-integral device scales ("extortion lettering").
|
||||
* Fix invisible scroll bar when native window is too narrow for egui.
|
||||
* Fixed uneven lettering on non-integral device scales ("extortion lettering").
|
||||
* Fixed invisible scroll bar when native window is too narrow for egui.
|
||||
|
||||
|
||||
## 0.12.0 - 2021-05-10 - Multitouch, user memory, window pivots, and improved plots
|
||||
|
||||
### Added ⭐
|
||||
* Add anchors to windows and areas so you can put a window in e.g. the top right corner.
|
||||
* Added anchors to windows and areas so you can put a window in e.g. the top right corner.
|
||||
* Make labels interactive with `Label::sense(Sense::click())`.
|
||||
* Add `Response::request_focus` and `Response::surrender_focus`.
|
||||
* Add `TextEdit::code_editor` (VERY basic).
|
||||
* Added `Response::request_focus` and `Response::surrender_focus`.
|
||||
* Added `TextEdit::code_editor` (VERY basic).
|
||||
* [Pan and zoom plots](https://github.com/emilk/egui/pull/317).
|
||||
* [Add plot legends](https://github.com/emilk/egui/pull/349).
|
||||
* [Users can now store custom state in `egui::Memory`](https://github.com/emilk/egui/pull/257).
|
||||
* Add `Response::on_disabled_hover_text` to show tooltip for disabled widgets.
|
||||
* Added `Response::on_disabled_hover_text` to show tooltip for disabled widgets.
|
||||
* Zoom input: ctrl-scroll and (on `eframe` web) trackpad-pinch gesture.
|
||||
* Support for raw [multi touch](https://github.com/emilk/egui/pull/306) events,
|
||||
enabling zoom, rotate, and more. Works with `eframe` web on mobile devices,
|
||||
and should work with `egui_glium` for certain touch devices/screens.
|
||||
* Add (optional) compatibility with [mint](https://docs.rs/mint).
|
||||
* Added (optional) compatibility with [mint](https://docs.rs/mint).
|
||||
|
||||
### Changed 🔧
|
||||
* Make `Memory::has_focus` public (again).
|
||||
* `Plot` must now be given a name that is unique within its scope.
|
||||
* Tab only selects labels if the `screen_reader` option is turned on.
|
||||
* Rename `ui.wrap` to `ui.scope`.
|
||||
* Renamed `ui.wrap` to `ui.scope`.
|
||||
|
||||
### Fixed 🐛
|
||||
* Fix [defocus-bug on touch screens](https://github.com/emilk/egui/issues/288).
|
||||
* Fix bug with the layout of wide `DragValue`s.
|
||||
* Fixed [defocus-bug on touch screens](https://github.com/emilk/egui/issues/288).
|
||||
* Fixed bug with the layout of wide `DragValue`s.
|
||||
|
||||
### Removed 🔥
|
||||
* Moved experimental markup language to `egui_demo_lib`
|
||||
|
@ -436,24 +577,24 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui-w
|
|||
* Use arrow keys to adjust sliders and `DragValue`s.
|
||||
* egui will now output events when widgets gain keyboard focus.
|
||||
* This can be hooked up to a screen reader to aid the visually impaired
|
||||
* Add the option to restrict the dragging bounds of `Window` and `Area` to a specified area using `drag_bounds(rect)`.
|
||||
* Add support for small and raised text.
|
||||
* Add `ui.set_row_height`.
|
||||
* Add `DebugOptions::show_widgets` to debug layouting by hovering widgets.
|
||||
* Add `ComboBox` to more easily customize combo boxes.
|
||||
* Add `Slider::new` and `DragValue::new` to replace old type-specific constructors.
|
||||
* Add `TextEdit::password` to hide input characters.
|
||||
* Added the option to restrict the dragging bounds of `Window` and `Area` to a specified area using `drag_bounds(rect)`.
|
||||
* Added support for small and raised text.
|
||||
* Added `ui.set_row_height`.
|
||||
* Added `DebugOptions::show_widgets` to debug layouting by hovering widgets.
|
||||
* Added `ComboBox` to more easily customize combo boxes.
|
||||
* Added `Slider::new` and `DragValue::new` to replace old type-specific constructors.
|
||||
* Added `TextEdit::password` to hide input characters.
|
||||
|
||||
### Changed 🔧
|
||||
* `ui.advance_cursor` is now called `ui.add_space`.
|
||||
* `kb_focus` is now just called `focus`.
|
||||
|
||||
### Fixed 🐛
|
||||
* Fix some bugs related to centered layouts.
|
||||
* Fixed some bugs related to centered layouts.
|
||||
* Fixed secondary-click to open a menu.
|
||||
* [Fix panic for zero-range sliders and zero-speed drag values](https://github.com/emilk/egui/pull/216).
|
||||
* Fix false id clash error for wrapping text.
|
||||
* Fix bug that would close a popup (e.g. the color picker) when clicking inside of it.
|
||||
* Fixed false id clash error for wrapping text.
|
||||
* Fixed bug that would close a popup (e.g. the color picker) when clicking inside of it.
|
||||
|
||||
### Deprecated ☢️
|
||||
* Deprectated `combo_box_with_label` in favor of new `ComboBox`.
|
||||
|
@ -465,12 +606,12 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui-w
|
|||
<img src="media/egui-0.10-plot.gif" width="50%">
|
||||
|
||||
### Added ⭐
|
||||
* Add `egui::plot::Plot` to plot some 2D data.
|
||||
* Add `Ui::hyperlink_to(label, url)`.
|
||||
* Added `egui::plot::Plot` to plot some 2D data.
|
||||
* Added `Ui::hyperlink_to(label, url)`.
|
||||
* Sliders can now have a value prefix and suffix (e.g. the suffix `"°"` works like a unit).
|
||||
* `Context::set_pixels_per_point` to control the scale of the UI.
|
||||
* Add `Response::changed()` to query if e.g. a slider was dragged, text was entered or a checkbox was clicked.
|
||||
* Add support for all integers in `DragValue` and `Slider` (except 128-bit).
|
||||
* Added `Response::changed()` to query if e.g. a slider was dragged, text was entered or a checkbox was clicked.
|
||||
* Added support for all integers in `DragValue` and `Slider` (except 128-bit).
|
||||
|
||||
### Changed 🔧
|
||||
* Improve the positioning of tooltips.
|
||||
|
@ -484,18 +625,18 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui-w
|
|||
<img src="media/0.9.0-disabled.gif" width="50%">
|
||||
|
||||
### Added ⭐
|
||||
* Add support for secondary and middle mouse buttons.
|
||||
* Add `Label` methods for code, strong, strikethrough, underline and italics.
|
||||
* Add `ui.group(|ui| { … })` to visually group some widgets within a frame.
|
||||
* Add `Ui` helpers for doing manual layout (`ui.put`, `ui.allocate_ui_at_rect` and more).
|
||||
* Add `ui.set_enabled(false)` to disable all widgets in a `Ui` (grayed out and non-interactive).
|
||||
* Add `TextEdit::hint_text` for showing a weak hint text when empty.
|
||||
* Added support for secondary and middle mouse buttons.
|
||||
* Added `Label` methods for code, strong, strikethrough, underline and italics.
|
||||
* Added `ui.group(|ui| { … })` to visually group some widgets within a frame.
|
||||
* Added `Ui` helpers for doing manual layout (`ui.put`, `ui.allocate_ui_at_rect` and more).
|
||||
* Added `ui.set_enabled(false)` to disable all widgets in a `Ui` (grayed out and non-interactive).
|
||||
* Added `TextEdit::hint_text` for showing a weak hint text when empty.
|
||||
* `egui::popup::popup_below_widget`: show a popup area below another widget.
|
||||
* Add `Slider::clamp_to_range(bool)`: if set, clamp the incoming and outgoing values to the slider range.
|
||||
* Added `Slider::clamp_to_range(bool)`: if set, clamp the incoming and outgoing values to the slider range.
|
||||
* Add: `ui.spacing()`, `ui.spacing_mut()`, `ui.visuals()`, `ui.visuals_mut()`.
|
||||
* Add: `ctx.set_visuals()`.
|
||||
* You can now control text wrapping with `Style::wrap`.
|
||||
* Add `Grid::max_col_width`.
|
||||
* Added `Grid::max_col_width`.
|
||||
|
||||
### Changed 🔧
|
||||
* Text will now wrap at newlines, spaces, dashes, punctuation or in the middle of a words if necessary, in that order of priority.
|
||||
|
@ -527,9 +668,9 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui-w
|
|||
|
||||
### Changed 🔧
|
||||
* New simpler and sleeker look!
|
||||
* Rename `PaintCmd` to `Shape`.
|
||||
* Renamed `PaintCmd` to `Shape`.
|
||||
* Replace tuple `(Rect, Shape)` with tuple-struct `ClippedShape`.
|
||||
* Rename feature `"serde"` to `"persistence"`.
|
||||
* Renamed feature `"serde"` to `"persistence"`.
|
||||
* Break out the modules `math` and `paint` into separate crates `emath` and `epaint`.
|
||||
|
||||
### Fixed 🐛
|
||||
|
@ -540,8 +681,8 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui-w
|
|||
## 0.7.0 - 2021-01-04
|
||||
|
||||
### Added ⭐
|
||||
* Add `ui.scroll_to_cursor` and `response.scroll_to_me` ([#81](https://github.com/emilk/egui/pull/81) by [lucaspoffo](https://github.com/lucaspoffo)).
|
||||
* Add `window.id(…)` and `area.id(…)` for overriding the default `Id`.
|
||||
* Added `ui.scroll_to_cursor` and `response.scroll_to_me` ([#81](https://github.com/emilk/egui/pull/81) by [lucaspoffo](https://github.com/lucaspoffo)).
|
||||
* Added `window.id(…)` and `area.id(…)` for overriding the default `Id`.
|
||||
|
||||
### Changed 🔧
|
||||
* Renamed `Srgba` to `Color32`.
|
||||
|
@ -567,10 +708,10 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui-w
|
|||
* Mouse-over explanation to duplicate ID warning.
|
||||
* You can now easily constrain egui to a portion of the screen using `RawInput::screen_rect`.
|
||||
* You can now control the minimum and maixumum number of decimals to show in a `Slider` or `DragValue`.
|
||||
* Add `egui::math::Rot2`: rotation helper.
|
||||
* Added `egui::math::Rot2`: rotation helper.
|
||||
* `Response` now contains the `Id` of the widget it pertains to.
|
||||
* `ui.allocate_response` that allocates space and checks for interactions.
|
||||
* Add `response.interact(sense)`, e.g. to check for clicks on labels.
|
||||
* Added `response.interact(sense)`, e.g. to check for clicks on labels.
|
||||
|
||||
### Changed 🔧
|
||||
* `ui.allocate_space` now returns an `(Id, Rect)` tuple.
|
||||
|
@ -585,7 +726,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui-w
|
|||
* Combo boxes has scroll bars when needed.
|
||||
* Expand `Window` + `Resize` containers to be large enough for last frames content
|
||||
* `ui.columns`: Columns now defaults to justified top-to-down layouts.
|
||||
* Rename `Sense::nothing()` to `Sense::hover()`.
|
||||
* Renamed `Sense::nothing()` to `Sense::hover()`.
|
||||
* Replaced `parking_lot` dependency with `atomic_refcell` by default.
|
||||
|
||||
### Fixed 🐛
|
||||
|
@ -611,7 +752,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui-w
|
|||
* `SelectableLabel` (`ui.selectable_label` and `ui.selectable_value`): A text-button that can be selected.
|
||||
* `ui.small_button`: A smaller button that looks good embedded in text.
|
||||
* `ui.drag_angle_tau`: For those who want to specify angles as fractions of τ (a full turn).
|
||||
* Add `Resize::id_source` and `ScrollArea::id_source` to let the user avoid Id clashes.
|
||||
* Added `Resize::id_source` and `ScrollArea::id_source` to let the user avoid Id clashes.
|
||||
|
||||
### Changed 🔧
|
||||
* New default font: [Ubuntu-Light](https://fonts.google.com/specimen/Ubuntu).
|
||||
|
@ -654,7 +795,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui-w
|
|||
### Added ⭐
|
||||
* Panels: you can now create panels using `SidePanel`, `TopPanel` and `CentralPanel`.
|
||||
* You can now override the default egui fonts.
|
||||
* Add ability to override text color with `visuals.override_text_color`.
|
||||
* Added ability to override text color with `visuals.override_text_color`.
|
||||
* The demo now includes a simple drag-and-drop example.
|
||||
* The demo app now has a slider to scale all of egui.
|
||||
|
||||
|
@ -671,7 +812,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui-w
|
|||
* You can no longer throw windows.
|
||||
|
||||
### Fixed 🐛
|
||||
* Fix a bug where some regions would slowly grow for non-integral scales (`pixels_per_point`).
|
||||
* Fixed a bug where some regions would slowly grow for non-integral scales (`pixels_per_point`).
|
||||
|
||||
|
||||
## 0.2.0 - 2020-10-10
|
||||
|
|
|
@ -59,15 +59,44 @@ Conventions unless otherwise specified:
|
|||
|
||||
While using an immediate mode gui is simple, implementing one is a lot more tricky. There are many subtle corner-case you need to think through. The `egui` source code is a bit messy, partially because it is still evolving.
|
||||
|
||||
* read some code before writing your own
|
||||
* follow the `egui` code style
|
||||
* add blank lines around all `fn`, `struct`, `enum`, etc.
|
||||
* `// Comment like this`, not `//like this`
|
||||
* write idiomatic rust
|
||||
* avoid `unsafe`
|
||||
* avoid code that can cause panics
|
||||
* use good names for everything
|
||||
* add docstrings to types, `struct` fields and all `pub fn`.
|
||||
* add some example code (doc-tests)
|
||||
* before making a function longer, consider adding a helper function
|
||||
* break the above rules when it makes sense
|
||||
* Read some code before writing your own.
|
||||
* Follow the `egui` code style.
|
||||
* Add blank lines around all `fn`, `struct`, `enum`, etc.
|
||||
* `// Comment like this.` and not `//like this`.
|
||||
* Use `TODO` instead of `FIXME`.
|
||||
* Add your github handle to the `TODO`:s you write, e.g: `TODO(emilk): clean this up`.
|
||||
* Write idiomatic rust.
|
||||
* Avoid `unsafe`.
|
||||
* Avoid code that can cause panics.
|
||||
* Use good names for everything.
|
||||
* Add docstrings to types, `struct` fields and all `pub fn`.
|
||||
* Add some example code (doc-tests).
|
||||
* Before making a function longer, consider adding a helper function.
|
||||
* If you are only using it in one function, put the `use` statement in that function. This improves locality, making it easier to read and move the code.
|
||||
* When importing a `trait` to use it's trait methods, do this: `use Trait as _;`. That lets the reader know why you imported it, even though it seems unused.
|
||||
* Follow the [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/).
|
||||
* Break the above rules when it makes sense.
|
||||
|
||||
|
||||
### Good:
|
||||
``` rust
|
||||
/// The name of the thing.
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn foo(&self) {
|
||||
// TODO(emilk): implement
|
||||
}
|
||||
```
|
||||
|
||||
### Bad:
|
||||
``` rust
|
||||
//some function
|
||||
fn get_name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
fn foo(&self) {
|
||||
//FIXME: implement
|
||||
}
|
||||
```
|
||||
|
|
2899
Cargo.lock
generated
2899
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
45
Cargo.toml
45
Cargo.toml
|
@ -1,33 +1,20 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"egui_demo_app",
|
||||
"egui_demo_lib",
|
||||
"egui_extras",
|
||||
"egui_glium",
|
||||
"egui_glow",
|
||||
"egui-winit",
|
||||
"egui",
|
||||
"emath",
|
||||
"epaint",
|
||||
"crates/ecolor",
|
||||
"crates/egui_demo_app",
|
||||
"crates/egui_demo_lib",
|
||||
"crates/egui_extras",
|
||||
"crates/egui_glow",
|
||||
"crates/egui-wgpu",
|
||||
"crates/egui-winit",
|
||||
"crates/egui",
|
||||
"crates/emath",
|
||||
"crates/epaint",
|
||||
|
||||
"examples/confirm_exit",
|
||||
"examples/custom_3d_glow",
|
||||
"examples/custom_3d_three-d",
|
||||
"examples/custom_font",
|
||||
"examples/custom_font_style",
|
||||
"examples/custom_window_frame",
|
||||
"examples/download_image",
|
||||
"examples/file_dialog",
|
||||
"examples/hello_world",
|
||||
"examples/puffin_profiler",
|
||||
"examples/retained_image",
|
||||
"examples/svg",
|
||||
"examples/*",
|
||||
]
|
||||
|
||||
[profile.dev]
|
||||
split-debuginfo = "unpacked" # faster debug builds on mac
|
||||
|
||||
[profile.release]
|
||||
# lto = true # VERY slightly smaller wasm
|
||||
# opt-level = 's' # 10-20% smaller wasm compared to `opt-level = 3`
|
||||
|
@ -36,3 +23,13 @@ opt-level = 2 # fast and small wasm, basically same as `opt-level = 's'`
|
|||
# opt-level = 3 # unecessarily large wasm for no performance gain
|
||||
|
||||
# debug = true # include debug symbols, useful when profiling wasm
|
||||
|
||||
|
||||
[profile.dev]
|
||||
# Can't leave this on by default, because it breaks the Windows build. Related: https://github.com/rust-lang/cargo/issues/4897
|
||||
# split-debuginfo = "unpacked" # faster debug builds on mac
|
||||
# opt-level = 1 # Make debug builds run faster
|
||||
|
||||
# Optimize all dependencies even in debug builds (does not affect workspace packages):
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 2
|
||||
|
|
124
Cranky.toml
Normal file
124
Cranky.toml
Normal file
|
@ -0,0 +1,124 @@
|
|||
# https://github.com/ericseppanen/cargo-cranky
|
||||
# cargo install cargo-cranky && cargo cranky
|
||||
|
||||
deny = ["unsafe_code"]
|
||||
|
||||
warn = [
|
||||
"clippy::all",
|
||||
"clippy::await_holding_lock",
|
||||
"clippy::bool_to_int_with_if",
|
||||
"clippy::char_lit_as_u8",
|
||||
"clippy::checked_conversions",
|
||||
"clippy::cloned_instead_of_copied",
|
||||
"clippy::dbg_macro",
|
||||
"clippy::debug_assert_with_mut_call",
|
||||
"clippy::derive_partial_eq_without_eq",
|
||||
"clippy::disallowed_methods",
|
||||
"clippy::disallowed_script_idents",
|
||||
"clippy::doc_link_with_quotes",
|
||||
"clippy::doc_markdown",
|
||||
"clippy::empty_enum",
|
||||
"clippy::enum_glob_use",
|
||||
"clippy::equatable_if_let",
|
||||
"clippy::exit",
|
||||
"clippy::expl_impl_clone_on_copy",
|
||||
"clippy::explicit_deref_methods",
|
||||
"clippy::explicit_into_iter_loop",
|
||||
"clippy::explicit_iter_loop",
|
||||
"clippy::fallible_impl_from",
|
||||
"clippy::filter_map_next",
|
||||
"clippy::flat_map_option",
|
||||
"clippy::float_cmp_const",
|
||||
"clippy::fn_params_excessive_bools",
|
||||
"clippy::fn_to_numeric_cast_any",
|
||||
"clippy::from_iter_instead_of_collect",
|
||||
"clippy::if_let_mutex",
|
||||
"clippy::implicit_clone",
|
||||
"clippy::imprecise_flops",
|
||||
"clippy::index_refutable_slice",
|
||||
"clippy::inefficient_to_string",
|
||||
"clippy::invalid_upcast_comparisons",
|
||||
"clippy::iter_not_returning_iterator",
|
||||
"clippy::iter_on_empty_collections",
|
||||
"clippy::iter_on_single_items",
|
||||
"clippy::large_digit_groups",
|
||||
"clippy::large_stack_arrays",
|
||||
"clippy::large_types_passed_by_value",
|
||||
"clippy::let_unit_value",
|
||||
"clippy::linkedlist",
|
||||
"clippy::lossy_float_literal",
|
||||
"clippy::macro_use_imports",
|
||||
"clippy::manual_assert",
|
||||
"clippy::manual_instant_elapsed",
|
||||
"clippy::manual_ok_or",
|
||||
"clippy::manual_string_new",
|
||||
"clippy::map_err_ignore",
|
||||
"clippy::map_flatten",
|
||||
"clippy::map_unwrap_or",
|
||||
"clippy::match_on_vec_items",
|
||||
"clippy::match_same_arms",
|
||||
"clippy::match_wild_err_arm",
|
||||
"clippy::match_wildcard_for_single_variants",
|
||||
"clippy::mem_forget",
|
||||
"clippy::mismatched_target_os",
|
||||
"clippy::mismatching_type_param_order",
|
||||
"clippy::missing_enforced_import_renames",
|
||||
"clippy::missing_errors_doc",
|
||||
"clippy::missing_safety_doc",
|
||||
"clippy::mut_mut",
|
||||
"clippy::mutex_integer",
|
||||
"clippy::needless_borrow",
|
||||
"clippy::needless_continue",
|
||||
"clippy::needless_for_each",
|
||||
"clippy::needless_pass_by_value",
|
||||
"clippy::negative_feature_names",
|
||||
"clippy::nonstandard_macro_braces",
|
||||
"clippy::option_option",
|
||||
"clippy::path_buf_push_overwrite",
|
||||
"clippy::ptr_as_ptr",
|
||||
"clippy::rc_mutex",
|
||||
"clippy::ref_option_ref",
|
||||
"clippy::rest_pat_in_fully_bound_structs",
|
||||
"clippy::same_functions_in_if_condition",
|
||||
"clippy::semicolon_if_nothing_returned",
|
||||
"clippy::single_match_else",
|
||||
"clippy::str_to_string",
|
||||
"clippy::string_add_assign",
|
||||
"clippy::string_add",
|
||||
"clippy::string_lit_as_bytes",
|
||||
"clippy::string_to_string",
|
||||
"clippy::todo",
|
||||
"clippy::trailing_empty_array",
|
||||
"clippy::trait_duplication_in_bounds",
|
||||
"clippy::unimplemented",
|
||||
"clippy::unnecessary_wraps",
|
||||
"clippy::unnested_or_patterns",
|
||||
"clippy::unused_peekable",
|
||||
"clippy::unused_rounding",
|
||||
"clippy::unused_self",
|
||||
"clippy::useless_transmute",
|
||||
"clippy::verbose_file_reads",
|
||||
"clippy::zero_sized_map_values",
|
||||
"elided_lifetimes_in_paths",
|
||||
"future_incompatible",
|
||||
"nonstandard_style",
|
||||
"rust_2018_idioms",
|
||||
"rust_2021_prelude_collisions",
|
||||
"rustdoc::missing_crate_level_docs",
|
||||
"semicolon_in_expressions_from_macros",
|
||||
"trivial_numeric_casts",
|
||||
"unused_extern_crates",
|
||||
"unused_import_braces",
|
||||
"unused_lifetimes",
|
||||
]
|
||||
|
||||
allow = [
|
||||
"clippy::manual_range_contains", # This one is just annoying
|
||||
|
||||
# Some of these we should try to put in "warn":
|
||||
"clippy::type_complexity",
|
||||
"clippy::undocumented_unsafe_blocks",
|
||||
"trivial_casts",
|
||||
"unsafe_op_in_unsafe_fn", # `unsafe_op_in_unsafe_fn` may become the default in future Rust versions: https://github.com/rust-lang/rust/issues/71668
|
||||
"unused_qualifications",
|
||||
]
|
63
README.md
63
README.md
|
@ -11,7 +11,7 @@
|
|||
|
||||
👉 [Click to run the web demo](https://www.egui.rs/#demo) 👈
|
||||
|
||||
egui is a simple, fast, and highly portable immediate mode GUI library for Rust. egui runs on the web, natively, and [in your favorite game engine](#integrations) (or will soon).
|
||||
egui (pronounced "e-gooey") is a simple, fast, and highly portable immediate mode GUI library for Rust. egui runs on the web, natively, and [in your favorite game engine](#integrations) (or will soon).
|
||||
|
||||
egui aims to be the easiest-to-use Rust GUI library, and the simplest way to make a web app in Rust.
|
||||
|
||||
|
@ -31,6 +31,8 @@ Sections:
|
|||
* [Other](#other)
|
||||
* [Credits](#credits)
|
||||
|
||||
([egui 的中文翻译文档 / chinese translation](https://github.com/Re-Ch-Love/egui-doc-cn/blob/main/README_zh-hans.md))
|
||||
|
||||
## Example
|
||||
|
||||
``` rust
|
||||
|
@ -58,17 +60,17 @@ If you have questions, use [GitHub Discussions](https://github.com/emilk/egui/di
|
|||
|
||||
## Demo
|
||||
|
||||
[Click to run egui web demo](https://www.egui.rs/#demo) (works in any browser with WASM and WebGL support). Uses [`eframe`](https://github.com/emilk/egui/tree/master/eframe).
|
||||
[Click to run egui web demo](https://www.egui.rs/#demo) (works in any browser with WASM and WebGL support). Uses [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe).
|
||||
|
||||
To test the demo app locally, run `cargo run --release -p egui_demo_app`.
|
||||
|
||||
The native backend is [`egui_glow`](https://github.com/emilk/egui/tree/master/egui_glow) (using [`glow`](https://crates.io/crates/glow)) and should work out-of-the-box on Mac and Windows, but on Linux you need to first run:
|
||||
The native backend is [`egui_glow`](https://github.com/emilk/egui/tree/master/crates/egui_glow) (using [`glow`](https://crates.io/crates/glow)) and should work out-of-the-box on Mac and Windows, but on Linux you need to first run:
|
||||
|
||||
`sudo apt-get install -y libclang-dev libgtk-3-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libspeechd-dev libxkbcommon-dev libssl-dev`
|
||||
`sudo apt-get install -y libclang-dev libgtk-3-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev`
|
||||
|
||||
On Fedora Rawhide you need to run:
|
||||
|
||||
`dnf install clang clang-devel clang-tools-extra speech-dispatcher-devel libxkbcommon-devel pkg-config openssl-devel libxcb-devel`
|
||||
`dnf install clang clang-devel clang-tools-extra libxkbcommon-devel pkg-config openssl-devel libxcb-devel`
|
||||
|
||||
**NOTE**: This is just for the demo app - egui itself is completely platform agnostic!
|
||||
|
||||
|
@ -82,7 +84,7 @@ On Fedora Rawhide you need to run:
|
|||
* A simple 2D graphics API for custom painting ([`epaint`](https://docs.rs/epaint)).
|
||||
* No callbacks
|
||||
* Pure immediate mode
|
||||
* Extensible: [easy to write your own widgets for egui](https://github.com/emilk/egui/blob/master/egui_demo_lib/src/apps/demo/toggle_switch.rs)
|
||||
* Extensible: [easy to write your own widgets for egui](https://github.com/emilk/egui/blob/master/crates/egui_demo_lib/src/demo/toggle_switch.rs)
|
||||
* Modular: You should be able to use small parts of egui and combine them in new ways
|
||||
* Safe: there is no `unsafe` code in egui
|
||||
* Minimal dependencies: [`ab_glyph`](https://crates.io/crates/ab_glyph) [`ahash`](https://crates.io/crates/ahash) [`nohash-hasher`](https://crates.io/crates/nohash-hasher) [`parking_lot`](https://crates.io/crates/parking_lot)
|
||||
|
@ -112,7 +114,7 @@ The obvious alternative to egui is [`imgui-rs`](https://github.com/Gekkio/imgui-
|
|||
* egui is pure Rust
|
||||
* egui is easily compiled to WASM
|
||||
* egui lets you use native Rust string types (`imgui-rs` forces you to use annoying macros and wrappers for zero-terminated strings)
|
||||
* [Writing your own widgets in egui is simple](https://github.com/emilk/egui/blob/master/egui_demo_lib/src/apps/demo/toggle_switch.rs)
|
||||
* [Writing your own widgets in egui is simple](https://github.com/emilk/egui/blob/master/crates/egui_demo_lib/src/demo/toggle_switch.rs)
|
||||
|
||||
egui also tries to improve your experience in other small ways:
|
||||
|
||||
|
@ -156,35 +158,42 @@ An integration needs to do the following each frame:
|
|||
* **Input**: Gather input (mouse, touches, keyboard, screen size, etc) and give it to egui
|
||||
* Run the application code
|
||||
* **Output**: Handle egui output (cursor changes, paste, texture allocations, …)
|
||||
* **Painting**: Render the triangle mesh egui produces (see [OpenGL example](https://github.com/emilk/egui/blob/master/egui_glium/src/painter.rs))
|
||||
* **Painting**: Render the triangle mesh egui produces (see [OpenGL example](https://github.com/emilk/egui/blob/master/crates/egui_glow/src/painter.rs))
|
||||
|
||||
### Official integrations
|
||||
|
||||
These are the official egui integrations:
|
||||
|
||||
* [`eframe`](https://github.com/emilk/egui/tree/master/eframe) for compiling the same app to web/wasm and desktop/native. Uses `egui_glow` and `egui-winit`.
|
||||
* [`egui_glium`](https://github.com/emilk/egui/tree/master/egui_glium) for compiling native apps with [Glium](https://github.com/glium/glium).
|
||||
* [`egui_glow`](https://github.com/emilk/egui/tree/master/egui_glow) for rendering egui with [glow](https://github.com/grovesNL/glow) on native and web, and for making native apps.
|
||||
* [`egui-winit`](https://github.com/emilk/egui/tree/master/egui-winit) for integrating with [winit](https://github.com/rust-windowing/winit).
|
||||
* [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe) for compiling the same app to web/wasm and desktop/native. Uses `egui-winit` and `egui_glow` or `egui-wgpu`.
|
||||
* [`egui_glow`](https://github.com/emilk/egui/tree/master/crates/egui_glow) for rendering egui with [glow](https://github.com/grovesNL/glow) on native and web, and for making native apps.
|
||||
* [`egui-wgpu`](https://github.com/emilk/egui/tree/master/crates/egui-wgpu) for [wgpu](https://crates.io/crates/wgpu) (WebGPU API).
|
||||
* [`egui-winit`](https://github.com/emilk/egui/tree/master/crates/egui-winit) for integrating with [winit](https://github.com/rust-windowing/winit).
|
||||
* [`egui_glium`](https://github.com/emilk/egui/tree/master/crates/egui_glium) for compiling native apps with [Glium](https://github.com/glium/glium) (DEPRECATED - looking for new maintainer).
|
||||
|
||||
### 3rd party integrations
|
||||
|
||||
* [`amethyst_egui`](https://github.com/jgraef/amethyst_egui) for [the Amethyst game engine](https://amethyst.rs/).
|
||||
* [`bevy_egui`](https://github.com/mvlabat/bevy_egui) for [the Bevy game engine](https://bevyengine.org/).
|
||||
* [`egui_glfw_gl`](https://github.com/cohaereo/egui_glfw_gl) for [GLFW](https://crates.io/crates/glfw).
|
||||
* [`egui-glutin-gl`](https://github.com/h3r2tic/egui-glutin-gl/) for [glutin](https://crates.io/crates/glutin).
|
||||
* [`egui_sdl2_gl`](https://crates.io/crates/egui_sdl2_gl) for [SDL2](https://crates.io/crates/sdl2).
|
||||
* [`egui_sdl2_platform`](https://github.com/ComLarsic/egui_sdl2_platform) for [SDL2](https://crates.io/crates/sdl2).
|
||||
* [`egui_vulkano`](https://github.com/derivator/egui_vulkano) for [Vulkano](https://github.com/vulkano-rs/vulkano).
|
||||
* [`egui_wgpu_backend`](https://crates.io/crates/egui_wgpu_backend) for [wgpu](https://crates.io/crates/wgpu) (WebGPU API).
|
||||
* [`egui_winit_vulkano`](https://github.com/hakolao/egui_winit_vulkano) for [Vulkano](https://github.com/vulkano-rs/vulkano).
|
||||
* [`egui-macroquad`](https://github.com/optozorax/egui-macroquad) for [macroquad](https://github.com/not-fl3/macroquad).
|
||||
* [`egui-miniquad`](https://github.com/not-fl3/egui-miniquad) for [Miniquad](https://github.com/not-fl3/miniquad).
|
||||
* [`egui_speedy2d`](https://github.com/heretik31/egui_speedy2d) for [Speedy2d](https://github.com/QuantumBadger/Speedy2D).
|
||||
* [`egui-tetra`](https://crates.io/crates/egui-tetra) for [Tetra](https://crates.io/crates/tetra), a 2D game framework.
|
||||
* [`egui-winit-ash-integration`](https://github.com/MatchaChoco010/egui-winit-ash-integration) for [winit](https://github.com/rust-windowing/winit) and [ash](https://github.com/MaikKlein/ash).
|
||||
* [`fltk-egui`](https://crates.io/crates/fltk-egui) for [fltk-rs](https://github.com/fltk-rs/fltk-rs).
|
||||
* [`ggez-egui`](https://github.com/NemuiSen/ggez-egui) for the [ggez](https://ggez.rs/) game framework.
|
||||
* [`ggegui`](https://github.com/NemuiSen/ggegui) for the [ggez](https://ggez.rs/) game framework.
|
||||
* [`godot-egui`](https://github.com/setzer22/godot-egui) for [godot-rust](https://github.com/godot-rust/godot-rust).
|
||||
* [`nannou_egui`](https://github.com/AlexEne/nannou_egui) for [nannou](https://nannou.cc).
|
||||
* [`nannou_egui`](https://github.com/nannou-org/nannou/tree/master/nannou_egui) for [nannou](https://nannou.cc).
|
||||
* [`notan_egui`](https://github.com/Nazariglez/notan/tree/main/crates/notan_egui) for [notan](https://github.com/Nazariglez/notan).
|
||||
* [`screen-13-egui`](https://github.com/attackgoat/screen-13/tree/master/contrib/screen-13-egui) for [Screen 13](https://github.com/attackgoat/screen-13).
|
||||
* [`egui_skia`](https://github.com/lucasmerlin/egui_skia) for [skia](https://github.com/rust-skia/rust-skia/tree/master/skia-safe).
|
||||
* [`smithay-egui`](https://github.com/Smithay/smithay-egui) for [smithay](https://github.com/Smithay/smithay/).
|
||||
* [`tauri-egui`](https://github.com/tauri-apps/tauri-egui) for [tauri](https://github.com/tauri-apps/tauri).
|
||||
|
||||
Missing an integration for the thing you're working on? Create one, it's easy!
|
||||
|
||||
|
@ -215,7 +224,7 @@ loop {
|
|||
}
|
||||
```
|
||||
|
||||
For a reference OpenGL backend, see [the `egui_glium` painter](https://github.com/emilk/egui/blob/master/egui_glium/src/painter.rs) or [the `egui_glow` painter](https://github.com/emilk/egui/blob/master/egui_glow/src/painter.rs).
|
||||
For a reference OpenGL backend, see [the `egui_glium` painter](https://github.com/emilk/egui/blob/master/crates/egui_glium/src/painter.rs) or [the `egui_glow` painter](https://github.com/emilk/egui/blob/master/crates/egui_glow/src/painter.rs).
|
||||
|
||||
### Debugging your integration
|
||||
|
||||
|
@ -315,11 +324,11 @@ If you call `.await` in your GUI code, the UI will freeze, which is very bad UX.
|
|||
* [`tokio::sync::watch::channel`](https://docs.rs/tokio/latest/tokio/sync/watch/fn.channel.html)
|
||||
|
||||
### What about accessibility, such as screen readers?
|
||||
There is experimental support for a screen reader. In [the web demo](https://www.egui.rs/#demo) you can enable it in the "Backend" tab.
|
||||
egui includes optional support for [AccessKit](https://accesskit.dev/), which currently implements the native accessibility APIs on Windows and macOS. This feature is enabled by default in eframe. For platforms that AccessKit doesn't yet support, including web, there is an experimental built-in screen reader; in [the web demo](https://www.egui.rs/#demo) you can enable it in the "Backend" tab.
|
||||
|
||||
Read more at <https://github.com/emilk/egui/issues/167>.
|
||||
The original discussion of accessibility in egui is at <https://github.com/emilk/egui/issues/167>. Now that AccessKit support is merged, providing a strong foundation for future accessibility work, please open new issues on specific accessibility problems.
|
||||
|
||||
### What is the difference between [egui](https://docs.rs/egui) and [eframe](https://github.com/emilk/egui/tree/master/eframe)?
|
||||
### What is the difference between [egui](https://docs.rs/egui) and [eframe](https://github.com/emilk/egui/tree/master/crates/eframe)?
|
||||
|
||||
`egui` is a 2D user interface library for laying out and interacting with buttons, sliders, etc.
|
||||
`egui` has no idea if it is running on the web or natively, and does not know how to collect input or show things on screen.
|
||||
|
@ -339,14 +348,14 @@ Examples:
|
|||
* <https://github.com/emilk/egui/blob/master/examples/custom_3d_three-d.rs>
|
||||
* <https://github.com/emilk/egui/blob/master/examples/custom_3d_glow.rs>
|
||||
|
||||
`Shape::Callback` will call your code when egui gets painted, to show anything using whatever the background rendering context is. When using [`eframe`](https://github.com/emilk/egui/tree/master/eframe) this will be [`glow`](https://github.com/grovesNL/glow). Other integrations will give you other rendering contexts, if they support `Shape::Callback` at all.
|
||||
`Shape::Callback` will call your code when egui gets painted, to show anything using whatever the background rendering context is. When using [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe) this will be [`glow`](https://github.com/grovesNL/glow). Other integrations will give you other rendering contexts, if they support `Shape::Callback` at all.
|
||||
|
||||
#### Render-to-texture
|
||||
You can also render your 3D scene to a texture and display it using [`ui.image(…)`](https://docs.rs/egui/latest/egui/struct.Ui.html#method.image). You first need to convert the native texture to an [`egui::TextureId`](https://docs.rs/egui/latest/egui/enum.TextureId.html), and how to do this depends on the integration you use.
|
||||
|
||||
Examples:
|
||||
* Using [`egui-miniquad`]( https://github.com/not-fl3/egui-miniquad): https://github.com/not-fl3/egui-miniquad/blob/master/examples/render_to_egui_image.rs
|
||||
* Using [`egui_glium`](https://github.com/emilk/egui/tree/master/egui_glium): <https://github.com/emilk/egui/blob/master/egui_glium/examples/native_texture.rs>.
|
||||
* Using [`egui_glium`](https://github.com/emilk/egui/tree/master/crates/egui_glium): <https://github.com/emilk/egui/blob/master/crates/egui_glium/examples/native_texture.rs>.
|
||||
|
||||
|
||||
## Other
|
||||
|
@ -363,6 +372,8 @@ egui uses the builder pattern for construction widgets. For instance: `ui.add(La
|
|||
|
||||
Instead of using matching `begin/end` style function calls (which can be error prone) egui prefers to use `FnOnce` closures passed to a wrapping function. Lambdas are a bit ugly though, so I'd like to find a nicer solution to this. More discussion of this at <https://github.com/emilk/egui/issues/1004#issuecomment-1001650754>.
|
||||
|
||||
egui uses a single `RwLock` for short-time locks on each access of `Context` data. This is to leave implementation simple and transactional and allow users to run their UI logic in parallel. Instead of creating mutex guards, egui uses closures passed to a wrapping function, e.g. `ctx.input(|i| i.key_down(Key::A))`. This is to make it less likely that a user would accidentally double-lock the `Context`, which would lead to a deadlock.
|
||||
|
||||
### Inspiration
|
||||
|
||||
The one and only [Dear ImGui](https://github.com/ocornut/imgui) is a great Immediate Mode GUI for C++ which works with many backends. That library revolutionized how I think about GUI code and turned GUI programming from something I hated to do to something I now enjoy.
|
||||
|
@ -388,6 +399,7 @@ Notable contributions by:
|
|||
* [@mankinskin](https://github.com/mankinskin): [Context menus](https://github.com/emilk/egui/pull/543).
|
||||
* [@t18b219k](https://github.com/t18b219k): [Port glow painter to web](https://github.com/emilk/egui/pull/868).
|
||||
* [@danielkeller](https://github.com/danielkeller): [`Context` refactor](https://github.com/emilk/egui/pull/1050).
|
||||
* [@MaximOsipenko](https://github.com/MaximOsipenko): [`Context` lock refactor](https://github.com/emilk/egui/pull/2625).
|
||||
* And [many more](https://github.com/emilk/egui/graphs/contributors?type=a).
|
||||
|
||||
egui is licensed under [MIT](LICENSE-MIT) OR [Apache-2.0](LICENSE-APACHE).
|
||||
|
@ -400,3 +412,12 @@ Default fonts:
|
|||
* `Hack-Regular.ttf`: <https://github.com/source-foundry/Hack>, [MIT Licence](https://github.com/source-foundry/Hack/blob/master/LICENSE.md)
|
||||
* `NotoEmoji-Regular.ttf`: [google.com/get/noto](https://google.com/get/noto), [SIL Open Font License](https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL)
|
||||
* `Ubuntu-Light.ttf` by [Dalton Maag](http://www.daltonmaag.com/): [Ubuntu font licence](https://ubuntu.com/legal/font-licence)
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
<img src="media/rerun_io_logo.png" width="50%">
|
||||
|
||||
egui development is sponsored by [Rerun](https://www.rerun.io/), a startup doing<br>
|
||||
visualizations for computer vision and robotics.
|
||||
</div>
|
||||
|
|
46
bacon.toml
Normal file
46
bacon.toml
Normal file
|
@ -0,0 +1,46 @@
|
|||
# This is a configuration file for the bacon tool
|
||||
# More info at https://github.com/Canop/bacon
|
||||
|
||||
default_job = "cranky"
|
||||
|
||||
[jobs]
|
||||
|
||||
[jobs.cranky]
|
||||
command = ["cargo", "cranky", "--all-targets", "--all-features", "--color", "always"]
|
||||
need_stdout = false
|
||||
watch = ["tests", "benches", "examples"]
|
||||
|
||||
[jobs.test]
|
||||
command = ["cargo", "test", "--color", "always"]
|
||||
need_stdout = true
|
||||
watch = ["tests"]
|
||||
|
||||
[jobs.doc]
|
||||
command = ["cargo", "doc", "--color", "always", "--all-features", "--no-deps"]
|
||||
need_stdout = false
|
||||
|
||||
# if the doc compiles, then it opens in your browser and bacon switches
|
||||
# to the previous job
|
||||
[jobs.doc-open]
|
||||
command = ["cargo", "doc", "--color", "always", "--all-features", "--no-deps", "--open"]
|
||||
need_stdout = false
|
||||
on_success = "back" # so that we don't open the browser at each change
|
||||
|
||||
# You can run your application and have the result displayed in bacon,
|
||||
# *if* it makes sense for this crate. You can run an example the same
|
||||
# way. Don't forget the `--color always` part or the errors won't be
|
||||
# properly parsed.
|
||||
[jobs.run]
|
||||
command = ["cargo", "run", "--color", "always"]
|
||||
need_stdout = true
|
||||
|
||||
# You may define here keybindings that would be specific to
|
||||
# a project, for example a shortcut to launch a specific job.
|
||||
# Shortcuts to internal functions (scrolling, toggling, etc.)
|
||||
# should go in your personal prefs.toml file instead.
|
||||
[keybindings]
|
||||
i = "job:initial"
|
||||
c = "job:cranky"
|
||||
d = "job:doc-open"
|
||||
t = "job:test"
|
||||
r = "job:run"
|
1
clippy.toml
Normal file
1
clippy.toml
Normal file
|
@ -0,0 +1 @@
|
|||
doc-valid-idents = ["AccessKit", ".."]
|
13
crates/ecolor/CHANGELOG.md
Normal file
13
crates/ecolor/CHANGELOG.md
Normal file
|
@ -0,0 +1,13 @@
|
|||
# Changelog for ecolor
|
||||
All notable changes to the `ecolor` crate will be noted in this file.
|
||||
|
||||
|
||||
## Unreleased
|
||||
|
||||
|
||||
## 0.21.0 - 2023-02-08
|
||||
* Add `Color32::gamma_multiply` ([#2437](https://github.com/emilk/egui/pull/2437)).
|
||||
|
||||
|
||||
## 0.20.0 - 2022-12-08
|
||||
* Split out `ecolor` crate from `epaint`
|
50
crates/ecolor/Cargo.toml
Normal file
50
crates/ecolor/Cargo.toml
Normal file
|
@ -0,0 +1,50 @@
|
|||
[package]
|
||||
name = "ecolor"
|
||||
version = "0.21.0"
|
||||
authors = [
|
||||
"Emil Ernerfeldt <emil.ernerfeldt@gmail.com>",
|
||||
"Andreas Reich <reichandreas@gmx.de>",
|
||||
]
|
||||
description = "Color structs and color conversion utilities"
|
||||
edition = "2021"
|
||||
rust-version = "1.65"
|
||||
homepage = "https://github.com/emilk/egui"
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/emilk/egui"
|
||||
categories = ["mathematics", "encoding"]
|
||||
keywords = ["gui", "color", "conversion", "gamedev", "images"]
|
||||
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
||||
[lib]
|
||||
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
## Enable additional checks if debug assertions are enabled (debug builds).
|
||||
extra_debug_asserts = []
|
||||
## Always enable additional checks.
|
||||
extra_asserts = []
|
||||
|
||||
|
||||
[dependencies]
|
||||
#! ### Optional dependencies
|
||||
|
||||
## [`bytemuck`](https://docs.rs/bytemuck) enables you to cast `ecolor` types to `&[u8]`.
|
||||
bytemuck = { version = "1.7.2", optional = true, features = ["derive"] }
|
||||
|
||||
## [`cint`](https://docs.rs/cint) enables interopability with other color libraries.
|
||||
cint = { version = "0.3.1", optional = true }
|
||||
|
||||
## Enable the [`hex_color`] macro.
|
||||
color-hex = { version = "0.2.0", optional = true }
|
||||
|
||||
## Enable this when generating docs.
|
||||
document-features = { version = "0.2", optional = true }
|
||||
|
||||
## Allow serialization using [`serde`](https://docs.rs/serde).
|
||||
serde = { version = "1", optional = true, features = ["derive"] }
|
11
crates/ecolor/README.md
Normal file
11
crates/ecolor/README.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
# ecolor - egui color library
|
||||
|
||||
[](https://crates.io/crates/ecolor)
|
||||
[](https://docs.rs/ecolor)
|
||||
[](https://github.com/rust-secure-code/safety-dance/)
|
||||

|
||||

|
||||
|
||||
A simple color storage and conversion library.
|
||||
|
||||
Made for [`egui`](https://github.com/emilk/egui/).
|
161
crates/ecolor/src/cint_impl.rs
Normal file
161
crates/ecolor/src/cint_impl.rs
Normal file
|
@ -0,0 +1,161 @@
|
|||
use super::*;
|
||||
use cint::{Alpha, ColorInterop, EncodedSrgb, Hsv, LinearSrgb, PremultipliedAlpha};
|
||||
|
||||
// ---- Color32 ----
|
||||
|
||||
impl From<Alpha<EncodedSrgb<u8>>> for Color32 {
|
||||
fn from(srgba: Alpha<EncodedSrgb<u8>>) -> Self {
|
||||
let Alpha {
|
||||
color: EncodedSrgb { r, g, b },
|
||||
alpha: a,
|
||||
} = srgba;
|
||||
|
||||
Color32::from_rgba_unmultiplied(r, g, b, a)
|
||||
}
|
||||
}
|
||||
|
||||
// No From<Color32> for Alpha<_> because Color32 is premultiplied
|
||||
|
||||
impl From<PremultipliedAlpha<EncodedSrgb<u8>>> for Color32 {
|
||||
fn from(srgba: PremultipliedAlpha<EncodedSrgb<u8>>) -> Self {
|
||||
let PremultipliedAlpha {
|
||||
color: EncodedSrgb { r, g, b },
|
||||
alpha: a,
|
||||
} = srgba;
|
||||
|
||||
Color32::from_rgba_premultiplied(r, g, b, a)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color32> for PremultipliedAlpha<EncodedSrgb<u8>> {
|
||||
fn from(col: Color32) -> Self {
|
||||
let (r, g, b, a) = col.to_tuple();
|
||||
|
||||
PremultipliedAlpha {
|
||||
color: EncodedSrgb { r, g, b },
|
||||
alpha: a,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PremultipliedAlpha<EncodedSrgb<f32>>> for Color32 {
|
||||
fn from(srgba: PremultipliedAlpha<EncodedSrgb<f32>>) -> Self {
|
||||
let PremultipliedAlpha {
|
||||
color: EncodedSrgb { r, g, b },
|
||||
alpha: a,
|
||||
} = srgba;
|
||||
|
||||
// This is a bit of an abuse of the function name but it does what we want.
|
||||
let r = linear_u8_from_linear_f32(r);
|
||||
let g = linear_u8_from_linear_f32(g);
|
||||
let b = linear_u8_from_linear_f32(b);
|
||||
let a = linear_u8_from_linear_f32(a);
|
||||
|
||||
Color32::from_rgba_premultiplied(r, g, b, a)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color32> for PremultipliedAlpha<EncodedSrgb<f32>> {
|
||||
fn from(col: Color32) -> Self {
|
||||
let (r, g, b, a) = col.to_tuple();
|
||||
|
||||
// This is a bit of an abuse of the function name but it does what we want.
|
||||
let r = linear_f32_from_linear_u8(r);
|
||||
let g = linear_f32_from_linear_u8(g);
|
||||
let b = linear_f32_from_linear_u8(b);
|
||||
let a = linear_f32_from_linear_u8(a);
|
||||
|
||||
PremultipliedAlpha {
|
||||
color: EncodedSrgb { r, g, b },
|
||||
alpha: a,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorInterop for Color32 {
|
||||
type CintTy = PremultipliedAlpha<EncodedSrgb<u8>>;
|
||||
}
|
||||
|
||||
// ---- Rgba ----
|
||||
|
||||
impl From<PremultipliedAlpha<LinearSrgb<f32>>> for Rgba {
|
||||
fn from(srgba: PremultipliedAlpha<LinearSrgb<f32>>) -> Self {
|
||||
let PremultipliedAlpha {
|
||||
color: LinearSrgb { r, g, b },
|
||||
alpha: a,
|
||||
} = srgba;
|
||||
|
||||
Rgba([r, g, b, a])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rgba> for PremultipliedAlpha<LinearSrgb<f32>> {
|
||||
fn from(col: Rgba) -> Self {
|
||||
let (r, g, b, a) = col.to_tuple();
|
||||
|
||||
PremultipliedAlpha {
|
||||
color: LinearSrgb { r, g, b },
|
||||
alpha: a,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorInterop for Rgba {
|
||||
type CintTy = PremultipliedAlpha<LinearSrgb<f32>>;
|
||||
}
|
||||
|
||||
// ---- Hsva ----
|
||||
|
||||
impl From<Alpha<Hsv<f32>>> for Hsva {
|
||||
fn from(srgba: Alpha<Hsv<f32>>) -> Self {
|
||||
let Alpha {
|
||||
color: Hsv { h, s, v },
|
||||
alpha: a,
|
||||
} = srgba;
|
||||
|
||||
Hsva::new(h, s, v, a)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Hsva> for Alpha<Hsv<f32>> {
|
||||
fn from(col: Hsva) -> Self {
|
||||
let Hsva { h, s, v, a } = col;
|
||||
|
||||
Alpha {
|
||||
color: Hsv { h, s, v },
|
||||
alpha: a,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorInterop for Hsva {
|
||||
type CintTy = Alpha<Hsv<f32>>;
|
||||
}
|
||||
|
||||
// ---- HsvaGamma ----
|
||||
|
||||
impl ColorInterop for HsvaGamma {
|
||||
type CintTy = Alpha<Hsv<f32>>;
|
||||
}
|
||||
|
||||
impl From<Alpha<Hsv<f32>>> for HsvaGamma {
|
||||
fn from(srgba: Alpha<Hsv<f32>>) -> Self {
|
||||
let Alpha {
|
||||
color: Hsv { h, s, v },
|
||||
alpha: a,
|
||||
} = srgba;
|
||||
|
||||
Hsva::new(h, s, v, a).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HsvaGamma> for Alpha<Hsv<f32>> {
|
||||
fn from(col: HsvaGamma) -> Self {
|
||||
let Hsva { h, s, v, a } = col.into();
|
||||
|
||||
Alpha {
|
||||
color: Hsv { h, s, v },
|
||||
alpha: a,
|
||||
}
|
||||
}
|
||||
}
|
216
crates/ecolor/src/color32.rs
Normal file
216
crates/ecolor/src/color32.rs
Normal file
|
@ -0,0 +1,216 @@
|
|||
use crate::{gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_f32_from_linear_u8, Rgba};
|
||||
|
||||
/// This format is used for space-efficient color representation (32 bits).
|
||||
///
|
||||
/// Instead of manipulating this directly it is often better
|
||||
/// to first convert it to either [`Rgba`] or [`crate::Hsva`].
|
||||
///
|
||||
/// Internally this uses 0-255 gamma space `sRGBA` color with premultiplied alpha.
|
||||
/// Alpha channel is in linear space.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))]
|
||||
pub struct Color32(pub(crate) [u8; 4]);
|
||||
|
||||
impl std::ops::Index<usize> for Color32 {
|
||||
type Output = u8;
|
||||
|
||||
#[inline(always)]
|
||||
fn index(&self, index: usize) -> &u8 {
|
||||
&self.0[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::IndexMut<usize> for Color32 {
|
||||
#[inline(always)]
|
||||
fn index_mut(&mut self, index: usize) -> &mut u8 {
|
||||
&mut self.0[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl Color32 {
|
||||
// Mostly follows CSS names:
|
||||
|
||||
pub const TRANSPARENT: Color32 = Color32::from_rgba_premultiplied(0, 0, 0, 0);
|
||||
pub const BLACK: Color32 = Color32::from_rgb(0, 0, 0);
|
||||
pub const DARK_GRAY: Color32 = Color32::from_rgb(96, 96, 96);
|
||||
pub const GRAY: Color32 = Color32::from_rgb(160, 160, 160);
|
||||
pub const LIGHT_GRAY: Color32 = Color32::from_rgb(220, 220, 220);
|
||||
pub const WHITE: Color32 = Color32::from_rgb(255, 255, 255);
|
||||
|
||||
pub const BROWN: Color32 = Color32::from_rgb(165, 42, 42);
|
||||
pub const DARK_RED: Color32 = Color32::from_rgb(0x8B, 0, 0);
|
||||
pub const RED: Color32 = Color32::from_rgb(255, 0, 0);
|
||||
pub const LIGHT_RED: Color32 = Color32::from_rgb(255, 128, 128);
|
||||
|
||||
pub const YELLOW: Color32 = Color32::from_rgb(255, 255, 0);
|
||||
pub const LIGHT_YELLOW: Color32 = Color32::from_rgb(255, 255, 0xE0);
|
||||
pub const KHAKI: Color32 = Color32::from_rgb(240, 230, 140);
|
||||
|
||||
pub const DARK_GREEN: Color32 = Color32::from_rgb(0, 0x64, 0);
|
||||
pub const GREEN: Color32 = Color32::from_rgb(0, 255, 0);
|
||||
pub const LIGHT_GREEN: Color32 = Color32::from_rgb(0x90, 0xEE, 0x90);
|
||||
|
||||
pub const DARK_BLUE: Color32 = Color32::from_rgb(0, 0, 0x8B);
|
||||
pub const BLUE: Color32 = Color32::from_rgb(0, 0, 255);
|
||||
pub const LIGHT_BLUE: Color32 = Color32::from_rgb(0xAD, 0xD8, 0xE6);
|
||||
|
||||
pub const GOLD: Color32 = Color32::from_rgb(255, 215, 0);
|
||||
|
||||
pub const DEBUG_COLOR: Color32 = Color32::from_rgba_premultiplied(0, 200, 0, 128);
|
||||
|
||||
/// An ugly color that is planned to be replaced before making it to the screen.
|
||||
pub const TEMPORARY_COLOR: Color32 = Color32::from_rgb(64, 254, 0);
|
||||
|
||||
#[inline(always)]
|
||||
pub const fn from_rgb(r: u8, g: u8, b: u8) -> Self {
|
||||
Self([r, g, b, 255])
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub const fn from_rgb_additive(r: u8, g: u8, b: u8) -> Self {
|
||||
Self([r, g, b, 0])
|
||||
}
|
||||
|
||||
/// From `sRGBA` with premultiplied alpha.
|
||||
#[inline(always)]
|
||||
pub const fn from_rgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||
Self([r, g, b, a])
|
||||
}
|
||||
|
||||
/// From `sRGBA` WITHOUT premultiplied alpha.
|
||||
pub fn from_rgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||
if a == 255 {
|
||||
Self::from_rgb(r, g, b) // common-case optimization
|
||||
} else if a == 0 {
|
||||
Self::TRANSPARENT // common-case optimization
|
||||
} else {
|
||||
let r_lin = linear_f32_from_gamma_u8(r);
|
||||
let g_lin = linear_f32_from_gamma_u8(g);
|
||||
let b_lin = linear_f32_from_gamma_u8(b);
|
||||
let a_lin = linear_f32_from_linear_u8(a);
|
||||
|
||||
let r = gamma_u8_from_linear_f32(r_lin * a_lin);
|
||||
let g = gamma_u8_from_linear_f32(g_lin * a_lin);
|
||||
let b = gamma_u8_from_linear_f32(b_lin * a_lin);
|
||||
|
||||
Self::from_rgba_premultiplied(r, g, b, a)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub const fn from_gray(l: u8) -> Self {
|
||||
Self([l, l, l, 255])
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub const fn from_black_alpha(a: u8) -> Self {
|
||||
Self([0, 0, 0, a])
|
||||
}
|
||||
|
||||
pub fn from_white_alpha(a: u8) -> Self {
|
||||
Rgba::from_white_alpha(linear_f32_from_linear_u8(a)).into()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub const fn from_additive_luminance(l: u8) -> Self {
|
||||
Self([l, l, l, 0])
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub const fn is_opaque(&self) -> bool {
|
||||
self.a() == 255
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub const fn r(&self) -> u8 {
|
||||
self.0[0]
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub const fn g(&self) -> u8 {
|
||||
self.0[1]
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub const fn b(&self) -> u8 {
|
||||
self.0[2]
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub const fn a(&self) -> u8 {
|
||||
self.0[3]
|
||||
}
|
||||
|
||||
/// Returns an opaque version of self
|
||||
pub fn to_opaque(self) -> Self {
|
||||
Rgba::from(self).to_opaque().into()
|
||||
}
|
||||
|
||||
/// Returns an additive version of self
|
||||
#[inline(always)]
|
||||
pub const fn additive(self) -> Self {
|
||||
let [r, g, b, _] = self.to_array();
|
||||
Self([r, g, b, 0])
|
||||
}
|
||||
|
||||
/// Premultiplied RGBA
|
||||
#[inline(always)]
|
||||
pub const fn to_array(&self) -> [u8; 4] {
|
||||
[self.r(), self.g(), self.b(), self.a()]
|
||||
}
|
||||
|
||||
/// Premultiplied RGBA
|
||||
#[inline(always)]
|
||||
pub const fn to_tuple(&self) -> (u8, u8, u8, u8) {
|
||||
(self.r(), self.g(), self.b(), self.a())
|
||||
}
|
||||
|
||||
pub fn to_srgba_unmultiplied(&self) -> [u8; 4] {
|
||||
Rgba::from(*self).to_srgba_unmultiplied()
|
||||
}
|
||||
|
||||
/// Multiply with 0.5 to make color half as opaque, perceptually.
|
||||
///
|
||||
/// Fast multiplication in gamma-space.
|
||||
///
|
||||
/// This is perceptually even, and faster that [`Self::linear_multiply`].
|
||||
#[inline]
|
||||
pub fn gamma_multiply(self, factor: f32) -> Color32 {
|
||||
crate::ecolor_assert!(0.0 <= factor && factor <= 1.0);
|
||||
let Self([r, g, b, a]) = self;
|
||||
Self([
|
||||
(r as f32 * factor + 0.5) as u8,
|
||||
(g as f32 * factor + 0.5) as u8,
|
||||
(b as f32 * factor + 0.5) as u8,
|
||||
(a as f32 * factor + 0.5) as u8,
|
||||
])
|
||||
}
|
||||
|
||||
/// Multiply with 0.5 to make color half as opaque in linear space.
|
||||
///
|
||||
/// This is using linear space, which is not perceptually even.
|
||||
/// You may want to use [`Self::gamma_multiply`] instead.
|
||||
pub fn linear_multiply(self, factor: f32) -> Color32 {
|
||||
crate::ecolor_assert!(0.0 <= factor && factor <= 1.0);
|
||||
// As an unfortunate side-effect of using premultiplied alpha
|
||||
// we need a somewhat expensive conversion to linear space and back.
|
||||
Rgba::from(self).multiply(factor).into()
|
||||
}
|
||||
|
||||
/// Converts to floating point values in the range 0-1 without any gamma space conversion.
|
||||
///
|
||||
/// Use this with great care! In almost all cases, you want to convert to [`crate::Rgba`] instead
|
||||
/// in order to obtain linear space color values.
|
||||
#[inline]
|
||||
pub fn to_normalized_gamma_f32(self) -> [f32; 4] {
|
||||
let Self([r, g, b, a]) = self;
|
||||
[
|
||||
r as f32 / 255.0,
|
||||
g as f32 / 255.0,
|
||||
b as f32 / 255.0,
|
||||
a as f32 / 255.0,
|
||||
]
|
||||
}
|
||||
}
|
39
crates/ecolor/src/hex_color_macro.rs
Normal file
39
crates/ecolor/src/hex_color_macro.rs
Normal file
|
@ -0,0 +1,39 @@
|
|||
/// Construct a [`crate::Color32`] from a hex RGB or RGBA string.
|
||||
///
|
||||
/// ```
|
||||
/// # use ecolor::{hex_color, Color32};
|
||||
/// assert_eq!(hex_color!("#202122"), Color32::from_rgb(0x20, 0x21, 0x22));
|
||||
/// assert_eq!(hex_color!("#abcdef12"), Color32::from_rgba_unmultiplied(0xab, 0xcd, 0xef, 0x12));
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! hex_color {
|
||||
($s:literal) => {{
|
||||
let array = color_hex::color_from_hex!($s);
|
||||
if array.len() == 3 {
|
||||
$crate::Color32::from_rgb(array[0], array[1], array[2])
|
||||
} else {
|
||||
#[allow(unconditional_panic)]
|
||||
$crate::Color32::from_rgba_unmultiplied(array[0], array[1], array[2], array[3])
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_rgb_hex() {
|
||||
assert_eq!(
|
||||
crate::Color32::from_rgb(0x20, 0x21, 0x22),
|
||||
hex_color!("#202122")
|
||||
);
|
||||
assert_eq!(
|
||||
crate::Color32::from_rgb_additive(0x20, 0x21, 0x22),
|
||||
hex_color!("#202122").additive()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_rgba_hex() {
|
||||
assert_eq!(
|
||||
crate::Color32::from_rgba_unmultiplied(0x20, 0x21, 0x22, 0x50),
|
||||
hex_color!("20212250")
|
||||
);
|
||||
}
|
231
crates/ecolor/src/hsva.rs
Normal file
231
crates/ecolor/src/hsva.rs
Normal file
|
@ -0,0 +1,231 @@
|
|||
use crate::{
|
||||
gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_f32_from_linear_u8,
|
||||
linear_u8_from_linear_f32, Color32, Rgba,
|
||||
};
|
||||
|
||||
/// Hue, saturation, value, alpha. All in the range [0, 1].
|
||||
/// No premultiplied alpha.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct Hsva {
|
||||
/// hue 0-1
|
||||
pub h: f32,
|
||||
|
||||
/// saturation 0-1
|
||||
pub s: f32,
|
||||
|
||||
/// value 0-1
|
||||
pub v: f32,
|
||||
|
||||
/// alpha 0-1. A negative value signifies an additive color (and alpha is ignored).
|
||||
pub a: f32,
|
||||
}
|
||||
|
||||
impl Hsva {
|
||||
pub fn new(h: f32, s: f32, v: f32, a: f32) -> Self {
|
||||
Self { h, s, v, a }
|
||||
}
|
||||
|
||||
/// From `sRGBA` with premultiplied alpha
|
||||
pub fn from_srgba_premultiplied(srgba: [u8; 4]) -> Self {
|
||||
Self::from_rgba_premultiplied(
|
||||
linear_f32_from_gamma_u8(srgba[0]),
|
||||
linear_f32_from_gamma_u8(srgba[1]),
|
||||
linear_f32_from_gamma_u8(srgba[2]),
|
||||
linear_f32_from_linear_u8(srgba[3]),
|
||||
)
|
||||
}
|
||||
|
||||
/// From `sRGBA` without premultiplied alpha
|
||||
pub fn from_srgba_unmultiplied(srgba: [u8; 4]) -> Self {
|
||||
Self::from_rgba_unmultiplied(
|
||||
linear_f32_from_gamma_u8(srgba[0]),
|
||||
linear_f32_from_gamma_u8(srgba[1]),
|
||||
linear_f32_from_gamma_u8(srgba[2]),
|
||||
linear_f32_from_linear_u8(srgba[3]),
|
||||
)
|
||||
}
|
||||
|
||||
/// From linear RGBA with premultiplied alpha
|
||||
pub fn from_rgba_premultiplied(r: f32, g: f32, b: f32, a: f32) -> Self {
|
||||
#![allow(clippy::many_single_char_names)]
|
||||
if a == 0.0 {
|
||||
if r == 0.0 && b == 0.0 && a == 0.0 {
|
||||
Hsva::default()
|
||||
} else {
|
||||
Hsva::from_additive_rgb([r, g, b])
|
||||
}
|
||||
} else {
|
||||
let (h, s, v) = hsv_from_rgb([r / a, g / a, b / a]);
|
||||
Hsva { h, s, v, a }
|
||||
}
|
||||
}
|
||||
|
||||
/// From linear RGBA without premultiplied alpha
|
||||
pub fn from_rgba_unmultiplied(r: f32, g: f32, b: f32, a: f32) -> Self {
|
||||
#![allow(clippy::many_single_char_names)]
|
||||
let (h, s, v) = hsv_from_rgb([r, g, b]);
|
||||
Hsva { h, s, v, a }
|
||||
}
|
||||
|
||||
pub fn from_additive_rgb(rgb: [f32; 3]) -> Self {
|
||||
let (h, s, v) = hsv_from_rgb(rgb);
|
||||
Hsva {
|
||||
h,
|
||||
s,
|
||||
v,
|
||||
a: -0.5, // anything negative is treated as additive
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_rgb(rgb: [f32; 3]) -> Self {
|
||||
let (h, s, v) = hsv_from_rgb(rgb);
|
||||
Hsva { h, s, v, a: 1.0 }
|
||||
}
|
||||
|
||||
pub fn from_srgb([r, g, b]: [u8; 3]) -> Self {
|
||||
Self::from_rgb([
|
||||
linear_f32_from_gamma_u8(r),
|
||||
linear_f32_from_gamma_u8(g),
|
||||
linear_f32_from_gamma_u8(b),
|
||||
])
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
pub fn to_opaque(self) -> Self {
|
||||
Self { a: 1.0, ..self }
|
||||
}
|
||||
|
||||
pub fn to_rgb(&self) -> [f32; 3] {
|
||||
rgb_from_hsv((self.h, self.s, self.v))
|
||||
}
|
||||
|
||||
pub fn to_srgb(&self) -> [u8; 3] {
|
||||
let [r, g, b] = self.to_rgb();
|
||||
[
|
||||
gamma_u8_from_linear_f32(r),
|
||||
gamma_u8_from_linear_f32(g),
|
||||
gamma_u8_from_linear_f32(b),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn to_rgba_premultiplied(&self) -> [f32; 4] {
|
||||
let [r, g, b, a] = self.to_rgba_unmultiplied();
|
||||
let additive = a < 0.0;
|
||||
if additive {
|
||||
[r, g, b, 0.0]
|
||||
} else {
|
||||
[a * r, a * g, a * b, a]
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents additive colors using a negative alpha.
|
||||
pub fn to_rgba_unmultiplied(&self) -> [f32; 4] {
|
||||
let Hsva { h, s, v, a } = *self;
|
||||
let [r, g, b] = rgb_from_hsv((h, s, v));
|
||||
[r, g, b, a]
|
||||
}
|
||||
|
||||
pub fn to_srgba_premultiplied(&self) -> [u8; 4] {
|
||||
let [r, g, b, a] = self.to_rgba_premultiplied();
|
||||
[
|
||||
gamma_u8_from_linear_f32(r),
|
||||
gamma_u8_from_linear_f32(g),
|
||||
gamma_u8_from_linear_f32(b),
|
||||
linear_u8_from_linear_f32(a),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn to_srgba_unmultiplied(&self) -> [u8; 4] {
|
||||
let [r, g, b, a] = self.to_rgba_unmultiplied();
|
||||
[
|
||||
gamma_u8_from_linear_f32(r),
|
||||
gamma_u8_from_linear_f32(g),
|
||||
gamma_u8_from_linear_f32(b),
|
||||
linear_u8_from_linear_f32(a.abs()),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Hsva> for Rgba {
|
||||
fn from(hsva: Hsva) -> Rgba {
|
||||
Rgba(hsva.to_rgba_premultiplied())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rgba> for Hsva {
|
||||
fn from(rgba: Rgba) -> Hsva {
|
||||
Self::from_rgba_premultiplied(rgba.0[0], rgba.0[1], rgba.0[2], rgba.0[3])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Hsva> for Color32 {
|
||||
fn from(hsva: Hsva) -> Color32 {
|
||||
Color32::from(Rgba::from(hsva))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color32> for Hsva {
|
||||
fn from(srgba: Color32) -> Hsva {
|
||||
Hsva::from(Rgba::from(srgba))
|
||||
}
|
||||
}
|
||||
|
||||
/// All ranges in 0-1, rgb is linear.
|
||||
pub fn hsv_from_rgb([r, g, b]: [f32; 3]) -> (f32, f32, f32) {
|
||||
#![allow(clippy::many_single_char_names)]
|
||||
let min = r.min(g.min(b));
|
||||
let max = r.max(g.max(b)); // value
|
||||
|
||||
let range = max - min;
|
||||
|
||||
let h = if max == min {
|
||||
0.0 // hue is undefined
|
||||
} else if max == r {
|
||||
(g - b) / (6.0 * range)
|
||||
} else if max == g {
|
||||
(b - r) / (6.0 * range) + 1.0 / 3.0
|
||||
} else {
|
||||
// max == b
|
||||
(r - g) / (6.0 * range) + 2.0 / 3.0
|
||||
};
|
||||
let h = (h + 1.0).fract(); // wrap
|
||||
let s = if max == 0.0 { 0.0 } else { 1.0 - min / max };
|
||||
(h, s, max)
|
||||
}
|
||||
|
||||
/// All ranges in 0-1, rgb is linear.
|
||||
pub fn rgb_from_hsv((h, s, v): (f32, f32, f32)) -> [f32; 3] {
|
||||
#![allow(clippy::many_single_char_names)]
|
||||
let h = (h.fract() + 1.0).fract(); // wrap
|
||||
let s = s.clamp(0.0, 1.0);
|
||||
|
||||
let f = h * 6.0 - (h * 6.0).floor();
|
||||
let p = v * (1.0 - s);
|
||||
let q = v * (1.0 - f * s);
|
||||
let t = v * (1.0 - (1.0 - f) * s);
|
||||
|
||||
match (h * 6.0).floor() as i32 % 6 {
|
||||
0 => [v, t, p],
|
||||
1 => [q, v, p],
|
||||
2 => [p, v, t],
|
||||
3 => [p, q, v],
|
||||
4 => [t, p, v],
|
||||
5 => [v, p, q],
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // a bit expensive
|
||||
fn test_hsv_roundtrip() {
|
||||
for r in 0..=255 {
|
||||
for g in 0..=255 {
|
||||
for b in 0..=255 {
|
||||
let srgba = Color32::from_rgb(r, g, b);
|
||||
let hsva = Hsva::from(srgba);
|
||||
assert_eq!(srgba, Color32::from(hsva));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
66
crates/ecolor/src/hsva_gamma.rs
Normal file
66
crates/ecolor/src/hsva_gamma.rs
Normal file
|
@ -0,0 +1,66 @@
|
|||
use crate::{gamma_from_linear, linear_from_gamma, Color32, Hsva, Rgba};
|
||||
|
||||
/// Like Hsva but with the `v` value (brightness) being gamma corrected
|
||||
/// so that it is somewhat perceptually even.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct HsvaGamma {
|
||||
/// hue 0-1
|
||||
pub h: f32,
|
||||
|
||||
/// saturation 0-1
|
||||
pub s: f32,
|
||||
|
||||
/// value 0-1, in gamma-space (~perceptually even)
|
||||
pub v: f32,
|
||||
|
||||
/// alpha 0-1. A negative value signifies an additive color (and alpha is ignored).
|
||||
pub a: f32,
|
||||
}
|
||||
|
||||
impl From<HsvaGamma> for Rgba {
|
||||
fn from(hsvag: HsvaGamma) -> Rgba {
|
||||
Hsva::from(hsvag).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HsvaGamma> for Color32 {
|
||||
fn from(hsvag: HsvaGamma) -> Color32 {
|
||||
Rgba::from(hsvag).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HsvaGamma> for Hsva {
|
||||
fn from(hsvag: HsvaGamma) -> Hsva {
|
||||
let HsvaGamma { h, s, v, a } = hsvag;
|
||||
Hsva {
|
||||
h,
|
||||
s,
|
||||
v: linear_from_gamma(v),
|
||||
a,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rgba> for HsvaGamma {
|
||||
fn from(rgba: Rgba) -> HsvaGamma {
|
||||
Hsva::from(rgba).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color32> for HsvaGamma {
|
||||
fn from(srgba: Color32) -> HsvaGamma {
|
||||
Hsva::from(srgba).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Hsva> for HsvaGamma {
|
||||
fn from(hsva: Hsva) -> HsvaGamma {
|
||||
let Hsva { h, s, v, a } = hsva;
|
||||
HsvaGamma {
|
||||
h,
|
||||
s,
|
||||
v: gamma_from_linear(v),
|
||||
a,
|
||||
}
|
||||
}
|
||||
}
|
173
crates/ecolor/src/lib.rs
Normal file
173
crates/ecolor/src/lib.rs
Normal file
|
@ -0,0 +1,173 @@
|
|||
//! Color conversions and types.
|
||||
//!
|
||||
//! If you want a compact color representation, use [`Color32`].
|
||||
//! If you want to manipulate RGBA colors use [`Rgba`].
|
||||
//! If you want to manipulate colors in a way closer to how humans think about colors, use [`HsvaGamma`].
|
||||
//!
|
||||
//! ## Feature flags
|
||||
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
|
||||
//!
|
||||
|
||||
#![allow(clippy::wrong_self_convention)]
|
||||
|
||||
#[cfg(feature = "cint")]
|
||||
mod cint_impl;
|
||||
#[cfg(feature = "cint")]
|
||||
pub use cint_impl::*;
|
||||
|
||||
mod color32;
|
||||
pub use color32::*;
|
||||
|
||||
mod hsva_gamma;
|
||||
pub use hsva_gamma::*;
|
||||
|
||||
mod hsva;
|
||||
pub use hsva::*;
|
||||
|
||||
#[cfg(feature = "color-hex")]
|
||||
mod hex_color_macro;
|
||||
|
||||
mod rgba;
|
||||
pub use rgba::*;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Color conversion:
|
||||
|
||||
impl From<Color32> for Rgba {
|
||||
fn from(srgba: Color32) -> Rgba {
|
||||
Rgba([
|
||||
linear_f32_from_gamma_u8(srgba.0[0]),
|
||||
linear_f32_from_gamma_u8(srgba.0[1]),
|
||||
linear_f32_from_gamma_u8(srgba.0[2]),
|
||||
linear_f32_from_linear_u8(srgba.0[3]),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rgba> for Color32 {
|
||||
fn from(rgba: Rgba) -> Color32 {
|
||||
Color32([
|
||||
gamma_u8_from_linear_f32(rgba.0[0]),
|
||||
gamma_u8_from_linear_f32(rgba.0[1]),
|
||||
gamma_u8_from_linear_f32(rgba.0[2]),
|
||||
linear_u8_from_linear_f32(rgba.0[3]),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
/// gamma [0, 255] -> linear [0, 1].
|
||||
pub fn linear_f32_from_gamma_u8(s: u8) -> f32 {
|
||||
if s <= 10 {
|
||||
s as f32 / 3294.6
|
||||
} else {
|
||||
((s as f32 + 14.025) / 269.025).powf(2.4)
|
||||
}
|
||||
}
|
||||
|
||||
/// linear [0, 255] -> linear [0, 1].
|
||||
/// Useful for alpha-channel.
|
||||
#[inline(always)]
|
||||
pub fn linear_f32_from_linear_u8(a: u8) -> f32 {
|
||||
a as f32 / 255.0
|
||||
}
|
||||
|
||||
/// linear [0, 1] -> gamma [0, 255] (clamped).
|
||||
/// Values outside this range will be clamped to the range.
|
||||
pub fn gamma_u8_from_linear_f32(l: f32) -> u8 {
|
||||
if l <= 0.0 {
|
||||
0
|
||||
} else if l <= 0.0031308 {
|
||||
fast_round(3294.6 * l)
|
||||
} else if l <= 1.0 {
|
||||
fast_round(269.025 * l.powf(1.0 / 2.4) - 14.025)
|
||||
} else {
|
||||
255
|
||||
}
|
||||
}
|
||||
|
||||
/// linear [0, 1] -> linear [0, 255] (clamped).
|
||||
/// Useful for alpha-channel.
|
||||
#[inline(always)]
|
||||
pub fn linear_u8_from_linear_f32(a: f32) -> u8 {
|
||||
fast_round(a * 255.0)
|
||||
}
|
||||
|
||||
fn fast_round(r: f32) -> u8 {
|
||||
(r + 0.5).floor() as _ // rust does a saturating cast since 1.45
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_srgba_conversion() {
|
||||
for b in 0..=255 {
|
||||
let l = linear_f32_from_gamma_u8(b);
|
||||
assert!(0.0 <= l && l <= 1.0);
|
||||
assert_eq!(gamma_u8_from_linear_f32(l), b);
|
||||
}
|
||||
}
|
||||
|
||||
/// gamma [0, 1] -> linear [0, 1] (not clamped).
|
||||
/// Works for numbers outside this range (e.g. negative numbers).
|
||||
pub fn linear_from_gamma(gamma: f32) -> f32 {
|
||||
if gamma < 0.0 {
|
||||
-linear_from_gamma(-gamma)
|
||||
} else if gamma <= 0.04045 {
|
||||
gamma / 12.92
|
||||
} else {
|
||||
((gamma + 0.055) / 1.055).powf(2.4)
|
||||
}
|
||||
}
|
||||
|
||||
/// linear [0, 1] -> gamma [0, 1] (not clamped).
|
||||
/// Works for numbers outside this range (e.g. negative numbers).
|
||||
pub fn gamma_from_linear(linear: f32) -> f32 {
|
||||
if linear < 0.0 {
|
||||
-gamma_from_linear(-linear)
|
||||
} else if linear <= 0.0031308 {
|
||||
12.92 * linear
|
||||
} else {
|
||||
1.055 * linear.powf(1.0 / 2.4) - 0.055
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// An assert that is only active when `epaint` is compiled with the `extra_asserts` feature
|
||||
/// or with the `extra_debug_asserts` feature in debug builds.
|
||||
#[macro_export]
|
||||
macro_rules! ecolor_assert {
|
||||
($($arg: tt)*) => {
|
||||
if cfg!(any(
|
||||
feature = "extra_asserts",
|
||||
all(feature = "extra_debug_asserts", debug_assertions),
|
||||
)) {
|
||||
assert!($($arg)*);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Cheap and ugly.
|
||||
/// Made for graying out disabled `Ui`s.
|
||||
pub fn tint_color_towards(color: Color32, target: Color32) -> Color32 {
|
||||
let [mut r, mut g, mut b, mut a] = color.to_array();
|
||||
|
||||
if a == 0 {
|
||||
r /= 2;
|
||||
g /= 2;
|
||||
b /= 2;
|
||||
} else if a < 170 {
|
||||
// Cheapish and looks ok.
|
||||
// Works for e.g. grid stripes.
|
||||
let div = (2 * 255 / a as i32) as u8;
|
||||
r = r / 2 + target.r() / div;
|
||||
g = g / 2 + target.g() / div;
|
||||
b = b / 2 + target.b() / div;
|
||||
a /= 2;
|
||||
} else {
|
||||
r = r / 2 + target.r() / 2;
|
||||
g = g / 2 + target.g() / 2;
|
||||
b = b / 2 + target.b() / 2;
|
||||
}
|
||||
Color32::from_rgba_premultiplied(r, g, b, a)
|
||||
}
|
266
crates/ecolor/src/rgba.rs
Normal file
266
crates/ecolor/src/rgba.rs
Normal file
|
@ -0,0 +1,266 @@
|
|||
use crate::{
|
||||
gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_f32_from_linear_u8,
|
||||
linear_u8_from_linear_f32,
|
||||
};
|
||||
|
||||
/// 0-1 linear space `RGBA` color with premultiplied alpha.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))]
|
||||
pub struct Rgba(pub(crate) [f32; 4]);
|
||||
|
||||
impl std::ops::Index<usize> for Rgba {
|
||||
type Output = f32;
|
||||
|
||||
#[inline(always)]
|
||||
fn index(&self, index: usize) -> &f32 {
|
||||
&self.0[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::IndexMut<usize> for Rgba {
|
||||
#[inline(always)]
|
||||
fn index_mut(&mut self, index: usize) -> &mut f32 {
|
||||
&mut self.0[index]
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn f32_hash<H: std::hash::Hasher>(state: &mut H, f: f32) {
|
||||
if f == 0.0 {
|
||||
state.write_u8(0);
|
||||
} else if f.is_nan() {
|
||||
state.write_u8(1);
|
||||
} else {
|
||||
use std::hash::Hash;
|
||||
f.to_bits().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::derive_hash_xor_eq)]
|
||||
impl std::hash::Hash for Rgba {
|
||||
#[inline]
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
crate::f32_hash(state, self.0[0]);
|
||||
crate::f32_hash(state, self.0[1]);
|
||||
crate::f32_hash(state, self.0[2]);
|
||||
crate::f32_hash(state, self.0[3]);
|
||||
}
|
||||
}
|
||||
|
||||
impl Rgba {
|
||||
pub const TRANSPARENT: Rgba = Rgba::from_rgba_premultiplied(0.0, 0.0, 0.0, 0.0);
|
||||
pub const BLACK: Rgba = Rgba::from_rgb(0.0, 0.0, 0.0);
|
||||
pub const WHITE: Rgba = Rgba::from_rgb(1.0, 1.0, 1.0);
|
||||
pub const RED: Rgba = Rgba::from_rgb(1.0, 0.0, 0.0);
|
||||
pub const GREEN: Rgba = Rgba::from_rgb(0.0, 1.0, 0.0);
|
||||
pub const BLUE: Rgba = Rgba::from_rgb(0.0, 0.0, 1.0);
|
||||
|
||||
#[inline(always)]
|
||||
pub const fn from_rgba_premultiplied(r: f32, g: f32, b: f32, a: f32) -> Self {
|
||||
Self([r, g, b, a])
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn from_rgba_unmultiplied(r: f32, g: f32, b: f32, a: f32) -> Self {
|
||||
Self([r * a, g * a, b * a, a])
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn from_srgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||
let r = linear_f32_from_gamma_u8(r);
|
||||
let g = linear_f32_from_gamma_u8(g);
|
||||
let b = linear_f32_from_gamma_u8(b);
|
||||
let a = linear_f32_from_linear_u8(a);
|
||||
Self::from_rgba_premultiplied(r, g, b, a)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn from_srgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||
let r = linear_f32_from_gamma_u8(r);
|
||||
let g = linear_f32_from_gamma_u8(g);
|
||||
let b = linear_f32_from_gamma_u8(b);
|
||||
let a = linear_f32_from_linear_u8(a);
|
||||
Self::from_rgba_premultiplied(r * a, g * a, b * a, a)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub const fn from_rgb(r: f32, g: f32, b: f32) -> Self {
|
||||
Self([r, g, b, 1.0])
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub const fn from_gray(l: f32) -> Self {
|
||||
Self([l, l, l, 1.0])
|
||||
}
|
||||
|
||||
pub fn from_luminance_alpha(l: f32, a: f32) -> Self {
|
||||
crate::ecolor_assert!(0.0 <= l && l <= 1.0);
|
||||
crate::ecolor_assert!(0.0 <= a && a <= 1.0);
|
||||
Self([l * a, l * a, l * a, a])
|
||||
}
|
||||
|
||||
/// Transparent black
|
||||
#[inline(always)]
|
||||
pub fn from_black_alpha(a: f32) -> Self {
|
||||
crate::ecolor_assert!(0.0 <= a && a <= 1.0);
|
||||
Self([0.0, 0.0, 0.0, a])
|
||||
}
|
||||
|
||||
/// Transparent white
|
||||
#[inline(always)]
|
||||
pub fn from_white_alpha(a: f32) -> Self {
|
||||
crate::ecolor_assert!(0.0 <= a && a <= 1.0, "a: {}", a);
|
||||
Self([a, a, a, a])
|
||||
}
|
||||
|
||||
/// Return an additive version of this color (alpha = 0)
|
||||
#[inline(always)]
|
||||
pub fn additive(self) -> Self {
|
||||
let [r, g, b, _] = self.0;
|
||||
Self([r, g, b, 0.0])
|
||||
}
|
||||
|
||||
/// Multiply with e.g. 0.5 to make us half transparent
|
||||
#[inline(always)]
|
||||
pub fn multiply(self, alpha: f32) -> Self {
|
||||
Self([
|
||||
alpha * self[0],
|
||||
alpha * self[1],
|
||||
alpha * self[2],
|
||||
alpha * self[3],
|
||||
])
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn r(&self) -> f32 {
|
||||
self.0[0]
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn g(&self) -> f32 {
|
||||
self.0[1]
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn b(&self) -> f32 {
|
||||
self.0[2]
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn a(&self) -> f32 {
|
||||
self.0[3]
|
||||
}
|
||||
|
||||
/// How perceptually intense (bright) is the color?
|
||||
#[inline]
|
||||
pub fn intensity(&self) -> f32 {
|
||||
0.3 * self.r() + 0.59 * self.g() + 0.11 * self.b()
|
||||
}
|
||||
|
||||
/// Returns an opaque version of self
|
||||
pub fn to_opaque(&self) -> Self {
|
||||
if self.a() == 0.0 {
|
||||
// Additive or fully transparent black.
|
||||
Self::from_rgb(self.r(), self.g(), self.b())
|
||||
} else {
|
||||
// un-multiply alpha:
|
||||
Self::from_rgb(
|
||||
self.r() / self.a(),
|
||||
self.g() / self.a(),
|
||||
self.b() / self.a(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Premultiplied RGBA
|
||||
#[inline(always)]
|
||||
pub fn to_array(&self) -> [f32; 4] {
|
||||
[self.r(), self.g(), self.b(), self.a()]
|
||||
}
|
||||
|
||||
/// Premultiplied RGBA
|
||||
#[inline(always)]
|
||||
pub fn to_tuple(&self) -> (f32, f32, f32, f32) {
|
||||
(self.r(), self.g(), self.b(), self.a())
|
||||
}
|
||||
|
||||
/// unmultiply the alpha
|
||||
pub fn to_rgba_unmultiplied(&self) -> [f32; 4] {
|
||||
let a = self.a();
|
||||
if a == 0.0 {
|
||||
// Additive, let's assume we are black
|
||||
self.0
|
||||
} else {
|
||||
[self.r() / a, self.g() / a, self.b() / a, a]
|
||||
}
|
||||
}
|
||||
|
||||
/// unmultiply the alpha
|
||||
pub fn to_srgba_unmultiplied(&self) -> [u8; 4] {
|
||||
let [r, g, b, a] = self.to_rgba_unmultiplied();
|
||||
[
|
||||
gamma_u8_from_linear_f32(r),
|
||||
gamma_u8_from_linear_f32(g),
|
||||
gamma_u8_from_linear_f32(b),
|
||||
linear_u8_from_linear_f32(a.abs()),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Add for Rgba {
|
||||
type Output = Rgba;
|
||||
|
||||
#[inline(always)]
|
||||
fn add(self, rhs: Rgba) -> Rgba {
|
||||
Rgba([
|
||||
self[0] + rhs[0],
|
||||
self[1] + rhs[1],
|
||||
self[2] + rhs[2],
|
||||
self[3] + rhs[3],
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul<Rgba> for Rgba {
|
||||
type Output = Rgba;
|
||||
|
||||
#[inline(always)]
|
||||
fn mul(self, other: Rgba) -> Rgba {
|
||||
Rgba([
|
||||
self[0] * other[0],
|
||||
self[1] * other[1],
|
||||
self[2] * other[2],
|
||||
self[3] * other[3],
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul<f32> for Rgba {
|
||||
type Output = Rgba;
|
||||
|
||||
#[inline(always)]
|
||||
fn mul(self, factor: f32) -> Rgba {
|
||||
Rgba([
|
||||
self[0] * factor,
|
||||
self[1] * factor,
|
||||
self[2] * factor,
|
||||
self[3] * factor,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul<Rgba> for f32 {
|
||||
type Output = Rgba;
|
||||
|
||||
#[inline(always)]
|
||||
fn mul(self, rgba: Rgba) -> Rgba {
|
||||
Rgba([
|
||||
self * rgba[0],
|
||||
self * rgba[1],
|
||||
self * rgba[2],
|
||||
self * rgba[3],
|
||||
])
|
||||
}
|
||||
}
|
229
crates/eframe/CHANGELOG.md
Normal file
229
crates/eframe/CHANGELOG.md
Normal file
|
@ -0,0 +1,229 @@
|
|||
# Changelog for eframe
|
||||
All notable changes to the `eframe` crate.
|
||||
|
||||
NOTE: [`egui-winit`](../egui-winit/CHANGELOG.md), [`egui_glium`](../egui_glium/CHANGELOG.md), [`egui_glow`](../egui_glow/CHANGELOG.md),and [`egui-wgpu`](../egui-wgpu/CHANGELOG.md) have their own changelogs!
|
||||
|
||||
|
||||
## Unreleased
|
||||
|
||||
|
||||
## 0.21.3 - 2023-02-15
|
||||
* Fix typing the letter 'P' on web ([#2740](https://github.com/emilk/egui/pull/2740)).
|
||||
|
||||
|
||||
## 0.21.2 - 2023-02-12
|
||||
* Allow compiling `eframe` with `--no-default-features` ([#2728](https://github.com/emilk/egui/pull/2728)).
|
||||
|
||||
|
||||
## 0.21.1 - 2023-02-12
|
||||
* Fixed crash when native window position is in an invalid state, which could happen e.g. due to changes in monitor size or DPI ([#2722](https://github.com/emilk/egui/issues/2722)).
|
||||
|
||||
|
||||
## 0.21.0 - 2023-02-08 - Update to `winit` 0.28
|
||||
* ⚠️ BREAKING: `App::clear_color` now expects you to return a raw float array ([#2666](https://github.com/emilk/egui/pull/2666)).
|
||||
* The `screen_reader` feature has now been renamed `web_screen_reader` and only work on web. On other platforms, use the `accesskit` feature flag instead ([#2669](https://github.com/emilk/egui/pull/2669)).
|
||||
|
||||
#### Desktop/Native:
|
||||
* `eframe::run_native` now returns a `Result` ([#2433](https://github.com/emilk/egui/pull/2433)).
|
||||
* Update to `winit` 0.28, adding support for mac trackpad zoom ([#2654](https://github.com/emilk/egui/pull/2654)).
|
||||
* Fix bug where the cursor could get stuck using the wrong icon.
|
||||
* `NativeOptions::transparent` now works with the wgpu backend ([#2684](https://github.com/emilk/egui/pull/2684)).
|
||||
* Add `Frame::set_minimized` and `set_maximized` ([#2292](https://github.com/emilk/egui/pull/2292), [#2672](https://github.com/emilk/egui/pull/2672)).
|
||||
* Fixed persistence of native window position on Windows OS ([#2583](https://github.com/emilk/egui/issues/2583)).
|
||||
|
||||
#### Web:
|
||||
* Prevent ctrl-P/cmd-P from opening the print dialog ([#2598](https://github.com/emilk/egui/pull/2598)).
|
||||
|
||||
|
||||
## 0.20.1 - 2022-12-11
|
||||
* Fix [docs.rs](https://docs.rs/eframe) build ([#2420](https://github.com/emilk/egui/pull/2420)).
|
||||
|
||||
|
||||
## 0.20.0 - 2022-12-08 - AccessKit integration and `wgpu` web support
|
||||
* MSRV (Minimum Supported Rust Version) is now `1.65.0` ([#2314](https://github.com/emilk/egui/pull/2314)).
|
||||
* Allow empty textures with the glow renderer.
|
||||
|
||||
#### Desktop/Native:
|
||||
* Don't repaint when just moving window ([#1980](https://github.com/emilk/egui/pull/1980)).
|
||||
* Added `NativeOptions::event_loop_builder` hook for apps to change platform specific event loop options ([#1952](https://github.com/emilk/egui/pull/1952)).
|
||||
* Enabled deferred render state initialization to support Android ([#1952](https://github.com/emilk/egui/pull/1952)).
|
||||
* Added `shader_version` to `NativeOptions` for cross compiling support on different target OpenGL | ES versions (on native `glow` renderer only) ([#1993](https://github.com/emilk/egui/pull/1993)).
|
||||
* Fix: app state is now saved when user presses Cmd-Q on Mac ([#2013](https://github.com/emilk/egui/pull/2013)).
|
||||
* Added `center` to `NativeOptions` and `monitor_size` to `WindowInfo` on desktop ([#2035](https://github.com/emilk/egui/pull/2035)).
|
||||
* Improve IME support ([#2046](https://github.com/emilk/egui/pull/2046)).
|
||||
* Added mouse-passthrough option ([#2080](https://github.com/emilk/egui/pull/2080)).
|
||||
* Added `NativeOptions::fullsize_content` option on Mac to build titlebar-less windows with floating window controls ([#2049](https://github.com/emilk/egui/pull/2049)).
|
||||
* Wgpu device/adapter/surface creation has now various configuration options exposed via `NativeOptions/WebOptions::wgpu_options` ([#2207](https://github.com/emilk/egui/pull/2207)).
|
||||
* Fix: Make sure that `native_pixels_per_point` is updated ([#2256](https://github.com/emilk/egui/pull/2256)).
|
||||
* Added optional, but enabled by default, integration with [AccessKit](https://accesskit.dev/) for implementing platform accessibility APIs ([#2294](https://github.com/emilk/egui/pull/2294)).
|
||||
* Fix: Less flickering on resize on Windows ([#2280](https://github.com/emilk/egui/pull/2280)).
|
||||
|
||||
#### Web:
|
||||
* ⚠️ BREAKING: `start_web` is a now `async` ([#2107](https://github.com/emilk/egui/pull/2107)).
|
||||
* Web: You can now use WebGL on top of `wgpu` by enabling the `wgpu` feature (and disabling `glow` via disabling default features) ([#2107](https://github.com/emilk/egui/pull/2107)).
|
||||
* Web: Add `WebInfo::user_agent` ([#2202](https://github.com/emilk/egui/pull/2202)).
|
||||
* Web: you can access your application from JS using `AppRunner::app_mut`. See `crates/egui_demo_app/src/lib.rs` ([#1886](https://github.com/emilk/egui/pull/1886)).
|
||||
|
||||
|
||||
## 0.19.0 - 2022-08-20
|
||||
* MSRV (Minimum Supported Rust Version) is now `1.61.0` ([#1846](https://github.com/emilk/egui/pull/1846)).
|
||||
* Added `wgpu` rendering backed ([#1564](https://github.com/emilk/egui/pull/1564)):
|
||||
* Added features `wgpu` and `glow`.
|
||||
* Added `NativeOptions::renderer` to switch between the rendering backends.
|
||||
* `egui_glow`: remove calls to `gl.get_error` in release builds to speed up rendering ([#1583](https://github.com/emilk/egui/pull/1583)).
|
||||
* Added `App::post_rendering` for e.g. reading the framebuffer ([#1591](https://github.com/emilk/egui/pull/1591)).
|
||||
* Use `Arc` for `glow::Context` instead of `Rc` ([#1640](https://github.com/emilk/egui/pull/1640)).
|
||||
* Fixed bug where the result returned from `App::on_exit_event` would sometimes be ignored ([#1696](https://github.com/emilk/egui/pull/1696)).
|
||||
* Added `NativeOptions::follow_system_theme` and `NativeOptions::default_theme` ([#1726](https://github.com/emilk/egui/pull/1726)).
|
||||
* Selectively expose parts of the API based on target arch (`wasm32` or not) ([#1867](https://github.com/emilk/egui/pull/1867)).
|
||||
|
||||
#### Desktop/Native:
|
||||
* Fixed clipboard on Wayland ([#1613](https://github.com/emilk/egui/pull/1613)).
|
||||
* Added ability to read window position and size with `frame.info().window_info` ([#1617](https://github.com/emilk/egui/pull/1617)).
|
||||
* Allow running on native without hardware accelerated rendering. Change with `NativeOptions::hardware_acceleration` ([#1681](https://github.com/emilk/egui/pull/1681), [#1693](https://github.com/emilk/egui/pull/1693)).
|
||||
* Fixed window position persistence ([#1745](https://github.com/emilk/egui/pull/1745)).
|
||||
* Fixed mouse cursor change on Linux ([#1747](https://github.com/emilk/egui/pull/1747)).
|
||||
* Added `Frame::set_visible` ([#1808](https://github.com/emilk/egui/pull/1808)).
|
||||
* Added fullscreen support ([#1866](https://github.com/emilk/egui/pull/1866)).
|
||||
* You can now continue execution after closing the native desktop window ([#1889](https://github.com/emilk/egui/pull/1889)).
|
||||
* `Frame::quit` has been renamed to `Frame::close` and `App::on_exit_event` is now `App::on_close_event` ([#1943](https://github.com/emilk/egui/pull/1943)).
|
||||
|
||||
#### Web:
|
||||
* Added ability to stop/re-run web app from JavaScript. ⚠️ You need to update your CSS with `html, body: { height: 100%; width: 100%; }` ([#1803](https://github.com/emilk/egui/pull/1650)).
|
||||
* Added `WebOptions::follow_system_theme` and `WebOptions::default_theme` ([#1726](https://github.com/emilk/egui/pull/1726)).
|
||||
* Added option to select WebGL version ([#1803](https://github.com/emilk/egui/pull/1803)).
|
||||
|
||||
|
||||
## 0.18.0 - 2022-04-30
|
||||
* MSRV (Minimum Supported Rust Version) is now `1.60.0` ([#1467](https://github.com/emilk/egui/pull/1467)).
|
||||
* Removed `eframe::epi` - everything is now in `eframe` (`eframe::App`, `eframe::Frame` etc) ([#1545](https://github.com/emilk/egui/pull/1545)).
|
||||
* Removed `Frame::request_repaint` - just call `egui::Context::request_repaint` for the same effect ([#1366](https://github.com/emilk/egui/pull/1366)).
|
||||
* Changed app creation/setup ([#1363](https://github.com/emilk/egui/pull/1363)):
|
||||
* Removed `App::setup` and `App::name`.
|
||||
* Provide `CreationContext` when creating app with egui context, storage, integration info and glow context.
|
||||
* Change interface of `run_native` and `start_web`.
|
||||
* Added `Frame::storage()` and `Frame::storage_mut()` ([#1418](https://github.com/emilk/egui/pull/1418)).
|
||||
* You can now load/save state in `App::update`
|
||||
* Changed `App::update` to take `&mut Frame` instead of `&Frame`.
|
||||
* `Frame` is no longer `Clone` or `Sync`.
|
||||
* Added `glow` (OpenGL) context to `Frame` ([#1425](https://github.com/emilk/egui/pull/1425)).
|
||||
|
||||
#### Desktop/Native:
|
||||
* Remove the `egui_glium` feature. `eframe` will now always use `egui_glow` as the native backend ([#1357](https://github.com/emilk/egui/pull/1357)).
|
||||
* Change default for `NativeOptions::drag_and_drop_support` to `true` ([#1329](https://github.com/emilk/egui/pull/1329)).
|
||||
* Added new `NativeOptions`: `vsync`, `multisampling`, `depth_buffer`, `stencil_buffer`.
|
||||
* `dark-light` (dark mode detection) is now an opt-in feature ([#1437](https://github.com/emilk/egui/pull/1437)).
|
||||
* Fixed potential scale bug when DPI scaling changes (e.g. when dragging a window between different displays) ([#1441](https://github.com/emilk/egui/pull/1441)).
|
||||
* Added new feature `puffin` to add [`puffin profiler`](https://github.com/EmbarkStudios/puffin) scopes ([#1483](https://github.com/emilk/egui/pull/1483)).
|
||||
* Moved app persistence to a background thread, allowing for smoother frame rates (on native).
|
||||
* Added `Frame::set_window_pos` ([#1505](https://github.com/emilk/egui/pull/1505)).
|
||||
|
||||
#### Web:
|
||||
* Use full browser width by default ([#1378](https://github.com/emilk/egui/pull/1378)).
|
||||
* egui code will no longer be called after panic ([#1306](https://github.com/emilk/egui/pull/1306)).
|
||||
|
||||
|
||||
## 0.17.0 - 2022-02-22
|
||||
* Removed `Frame::alloc_texture`. Use `egui::Context::load_texture` instead ([#1110](https://github.com/emilk/egui/pull/1110)).
|
||||
* Shift-scroll will now result in horizontal scrolling on all platforms ([#1136](https://github.com/emilk/egui/pull/1136)).
|
||||
* Log using the `tracing` crate. Log to stdout by adding `tracing_subscriber::fmt::init();` to your `main` ([#1192](https://github.com/emilk/egui/pull/1192)).
|
||||
|
||||
#### Desktop/Native:
|
||||
* The default native backend is now `egui_glow` (instead of `egui_glium`) ([#1020](https://github.com/emilk/egui/pull/1020)).
|
||||
* Automatically detect and apply dark or light mode from system ([#1045](https://github.com/emilk/egui/pull/1045)).
|
||||
* Fixed horizontal scrolling direction on Linux.
|
||||
* Added `App::on_exit_event` ([#1038](https://github.com/emilk/egui/pull/1038))
|
||||
* Added `NativeOptions::initial_window_pos`.
|
||||
* Fixed `enable_drag` for Windows OS ([#1108](https://github.com/emilk/egui/pull/1108)).
|
||||
|
||||
#### Web:
|
||||
* The default web painter is now `egui_glow` (instead of WebGL) ([#1020](https://github.com/emilk/egui/pull/1020)).
|
||||
* Fixed glow failure on Chromium ([#1092](https://github.com/emilk/egui/pull/1092)).
|
||||
* Updated `eframe::IntegrationInfo::web_location_hash` on `hashchange` event ([#1140](https://github.com/emilk/egui/pull/1140)).
|
||||
* Expose all parts of the location/url in `frame.info().web_info` ([#1258](https://github.com/emilk/egui/pull/1258)).
|
||||
|
||||
|
||||
## 0.16.0 - 2021-12-29
|
||||
* `Frame` can now be cloned, saved, and passed to background threads ([#999](https://github.com/emilk/egui/pull/999)).
|
||||
* Added `Frame::request_repaint` to replace `repaint_signal` ([#999](https://github.com/emilk/egui/pull/999)).
|
||||
* Added `Frame::alloc_texture/free_texture` to replace `tex_allocator` ([#999](https://github.com/emilk/egui/pull/999)).
|
||||
|
||||
#### Web:
|
||||
* Fixed [dark rendering in WebKitGTK](https://github.com/emilk/egui/issues/794) ([#888](https://github.com/emilk/egui/pull/888/)).
|
||||
* Added feature `glow` to switch to a [`glow`](https://github.com/grovesNL/glow) based painter ([#868](https://github.com/emilk/egui/pull/868)).
|
||||
|
||||
|
||||
## 0.15.0 - 2021-10-24
|
||||
* `Frame` now provides `set_window_title` to set window title dynamically ([#828](https://github.com/emilk/egui/pull/828)).
|
||||
* `Frame` now provides `set_decorations` to set whether to show window decorations.
|
||||
* Remove "http" feature (use https://github.com/emilk/ehttp instead!).
|
||||
* Added `App::persist_native_window` and `App::persist_egui_memory` to control what gets persisted.
|
||||
|
||||
#### Desktop/Native:
|
||||
* Increase native scroll speed.
|
||||
* Added new backend `egui_glow` as an alternative to `egui_glium`. Enable with `default-features = false, features = ["default_fonts", "egui_glow"]`.
|
||||
|
||||
#### Web:
|
||||
* Implement `eframe::NativeTexture` trait for the WebGL painter.
|
||||
* Deprecate `Painter::register_webgl_texture.
|
||||
* Fixed multiline paste.
|
||||
* Fixed painting with non-opaque backgrounds.
|
||||
* Improve text input on mobile and for IME.
|
||||
|
||||
|
||||
## 0.14.0 - 2021-08-24
|
||||
* Added dragging and dropping files into egui.
|
||||
* Improve http fetch API.
|
||||
* `run_native` now returns when the app is closed.
|
||||
* Web: Made text thicker and less pixelated.
|
||||
|
||||
|
||||
## 0.13.1 - 2021-06-24
|
||||
* Fixed `http` feature flag and docs
|
||||
|
||||
|
||||
## 0.13.0 - 2021-06-24
|
||||
* `App::setup` now takes a `Frame` and `Storage` by argument.
|
||||
* `App::load` has been removed. Implement `App::setup` instead.
|
||||
* Web: Default to light visuals unless the system reports a preference for dark mode.
|
||||
* Web: Improve alpha blending, making fonts look much better (especially in light mode)
|
||||
* Web: Fix double-paste bug
|
||||
|
||||
|
||||
## 0.12.0 - 2021-05-10
|
||||
* Moved options out of `trait App` into new `NativeOptions`.
|
||||
* Added option for `always_on_top`.
|
||||
* Web: Scroll faster when scrolling with mouse wheel.
|
||||
|
||||
|
||||
## 0.11.0 - 2021-04-05
|
||||
* You can now turn your window transparent with the `App::transparent` option.
|
||||
* You can now disable window decorations with the `App::decorated` option.
|
||||
* Web: [Fix mobile and IME text input](https://github.com/emilk/egui/pull/253)
|
||||
* Web: Hold down a modifier key when clicking a link to open it in a new tab.
|
||||
|
||||
Contributors: [n2](https://github.com/n2)
|
||||
|
||||
|
||||
## 0.10.0 - 2021-02-28
|
||||
* [You can now set your own app icons](https://github.com/emilk/egui/pull/193).
|
||||
* You can control the initial size of the native window with `App::initial_window_size`.
|
||||
* You can control the maximum egui web canvas size with `App::max_size_points`.
|
||||
* `Frame::tex_allocator()` no longer returns an `Option` (there is always a texture allocator).
|
||||
|
||||
|
||||
## 0.9.0 - 2021-02-07
|
||||
* [Added support for HTTP body](https://github.com/emilk/egui/pull/139).
|
||||
* Web: Right-clicks will no longer open browser context menu.
|
||||
* Web: Fix a bug where one couldn't select items in a combo box on a touch screen.
|
||||
|
||||
|
||||
## 0.8.0 - 2021-01-17
|
||||
* Simplify `TextureAllocator` interface.
|
||||
* WebGL2 is now supported, with improved texture sampler. WebGL1 will be used as a fallback.
|
||||
* Web: Slightly improved alpha-blending (work-around for non-existing linear-space blending).
|
||||
* Web: Call `prevent_default` for arrow keys when entering text
|
||||
|
||||
|
||||
## 0.7.0 - 2021-01-04
|
||||
* Initial release of `eframe`
|
169
crates/eframe/Cargo.toml
Normal file
169
crates/eframe/Cargo.toml
Normal file
|
@ -0,0 +1,169 @@
|
|||
[package]
|
||||
name = "eframe"
|
||||
version = "0.21.3"
|
||||
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
||||
description = "egui framework - write GUI apps that compiles to web and/or natively"
|
||||
edition = "2021"
|
||||
rust-version = "1.65"
|
||||
homepage = "https://github.com/emilk/egui/tree/master/crates/eframe"
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/emilk/egui/tree/master/crates/eframe"
|
||||
categories = ["gui", "game-development"]
|
||||
keywords = ["egui", "gui", "gamedev"]
|
||||
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"]
|
||||
|
||||
[lib]
|
||||
|
||||
|
||||
[features]
|
||||
default = ["accesskit", "default_fonts", "glow"]
|
||||
|
||||
## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/).
|
||||
accesskit = ["egui/accesskit", "egui-winit/accesskit"]
|
||||
|
||||
## Detect dark mode system preference using [`dark-light`](https://docs.rs/dark-light).
|
||||
##
|
||||
## See also [`NativeOptions::follow_system_theme`] and [`NativeOptions::default_theme`].
|
||||
dark-light = ["dep:dark-light"]
|
||||
|
||||
## If set, egui will use `include_bytes!` to bundle some fonts.
|
||||
## If you plan on specifying your own fonts you may disable this feature.
|
||||
default_fonts = ["egui/default_fonts"]
|
||||
|
||||
## Use [`glow`](https://github.com/grovesNL/glow) for painting, via [`egui_glow`](https://github.com/emilk/egui/tree/master/crates/egui_glow).
|
||||
glow = ["dep:glow", "dep:egui_glow", "dep:glutin", "dep:glutin-winit"]
|
||||
|
||||
## Enable saving app state to disk.
|
||||
persistence = [
|
||||
"directories-next",
|
||||
"egui-winit/serde",
|
||||
"egui/persistence",
|
||||
"ron",
|
||||
"serde",
|
||||
]
|
||||
|
||||
## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate.
|
||||
##
|
||||
## Only enabled on native, because of the low resolution (1ms) of time keeping in browsers.
|
||||
## `eframe` will call `puffin::GlobalProfiler::lock().new_frame()` for you
|
||||
puffin = ["dep:puffin", "egui_glow?/puffin", "egui-wgpu?/puffin"]
|
||||
|
||||
## Enable screen reader support (requires `ctx.options_mut(|o| o.screen_reader = true);`) on web.
|
||||
##
|
||||
## For other platforms, use the "accesskit" feature instead.
|
||||
web_screen_reader = ["tts"]
|
||||
|
||||
## If set, eframe will look for the env-var `EFRAME_SCREENSHOT_TO` and write a screenshot to that location, and then quit.
|
||||
## This is used to generate images for the examples.
|
||||
__screenshot = ["dep:image"]
|
||||
|
||||
## Use [`wgpu`](https://docs.rs/wgpu) for painting (via [`egui-wgpu`](https://github.com/emilk/egui/tree/master/crates/egui-wgpu)).
|
||||
## This overrides the `glow` feature.
|
||||
wgpu = ["dep:wgpu", "dep:egui-wgpu", "dep:pollster"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
egui = { version = "0.21.0", path = "../egui", default-features = false, features = [
|
||||
"bytemuck",
|
||||
"tracing",
|
||||
] }
|
||||
thiserror = "1.0.37"
|
||||
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
||||
|
||||
#! ### Optional dependencies
|
||||
## Enable this when generating docs.
|
||||
document-features = { version = "0.2", optional = true }
|
||||
|
||||
egui_glow = { version = "0.21.0", path = "../egui_glow", optional = true, default-features = false }
|
||||
glow = { version = "0.12", optional = true }
|
||||
ron = { version = "0.8", optional = true, features = ["integer128"] }
|
||||
serde = { version = "1", optional = true, features = ["derive"] }
|
||||
|
||||
# -------------------------------------------
|
||||
# native:
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
egui-winit = { version = "0.21.1", path = "../egui-winit", default-features = false, features = [
|
||||
"clipboard",
|
||||
"links",
|
||||
] }
|
||||
raw-window-handle = { version = "0.5.0" }
|
||||
winit = "0.28.1"
|
||||
|
||||
# optional native:
|
||||
dark-light = { version = "1.0", optional = true }
|
||||
directories-next = { version = "2", optional = true }
|
||||
egui-wgpu = { version = "0.21.0", path = "../egui-wgpu", optional = true, features = [
|
||||
"winit",
|
||||
] } # if wgpu is used, use it with winit
|
||||
pollster = { version = "0.3", optional = true } # needed for wgpu
|
||||
|
||||
# we can expose these to user so that they can select which backends they want to enable to avoid compiling useless deps.
|
||||
# this can be done at the same time we expose x11/wayland features of winit crate.
|
||||
glutin = { version = "0.30", optional = true }
|
||||
glutin-winit = { version = "0.3.0", optional = true }
|
||||
image = { version = "0.24", optional = true, default-features = false, features = [
|
||||
"png",
|
||||
] }
|
||||
puffin = { version = "0.14", optional = true }
|
||||
wgpu = { version = "0.15.0", optional = true }
|
||||
|
||||
# -------------------------------------------
|
||||
# web:
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
bytemuck = "1.7"
|
||||
js-sys = "0.3"
|
||||
percent-encoding = "2.1"
|
||||
wasm-bindgen = "=0.2.84"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
web-sys = { version = "0.3.58", features = [
|
||||
"BinaryType",
|
||||
"Blob",
|
||||
"Clipboard",
|
||||
"ClipboardEvent",
|
||||
"CompositionEvent",
|
||||
"console",
|
||||
"CssStyleDeclaration",
|
||||
"DataTransfer",
|
||||
"DataTransferItem",
|
||||
"DataTransferItemList",
|
||||
"Document",
|
||||
"DomRect",
|
||||
"DragEvent",
|
||||
"Element",
|
||||
"Event",
|
||||
"EventListener",
|
||||
"EventTarget",
|
||||
"ExtSRgb",
|
||||
"File",
|
||||
"FileList",
|
||||
"FocusEvent",
|
||||
"HtmlCanvasElement",
|
||||
"HtmlElement",
|
||||
"HtmlInputElement",
|
||||
"InputEvent",
|
||||
"KeyboardEvent",
|
||||
"Location",
|
||||
"MediaQueryList",
|
||||
"MouseEvent",
|
||||
"Navigator",
|
||||
"Performance",
|
||||
"Storage",
|
||||
"Touch",
|
||||
"TouchEvent",
|
||||
"TouchList",
|
||||
"WebGl2RenderingContext",
|
||||
"WebglDebugRendererInfo",
|
||||
"WebGlRenderingContext",
|
||||
"WheelEvent",
|
||||
"Window",
|
||||
] }
|
||||
|
||||
# optional web:
|
||||
egui-wgpu = { version = "0.21.0", path = "../egui-wgpu", optional = true } # if wgpu is used, use it without (!) winit
|
||||
tts = { version = "0.25", optional = true, default-features = false }
|
||||
wgpu = { version = "0.15.0", optional = true, features = ["webgl"] }
|
|
@ -17,20 +17,24 @@ For how to use `egui`, see [the egui docs](https://docs.rs/egui).
|
|||
|
||||
---
|
||||
|
||||
`eframe` uses [`egui_glow`](https://github.com/emilk/egui/tree/master/egui_glow) for rendering, and on native it uses [`egui-winit`](https://github.com/emilk/egui/tree/master/egui-winit).
|
||||
`eframe` uses [`egui_glow`](https://github.com/emilk/egui/tree/master/crates/egui_glow) for rendering, and on native it uses [`egui-winit`](https://github.com/emilk/egui/tree/master/crates/egui-winit).
|
||||
|
||||
To use on Linux, first run:
|
||||
|
||||
```
|
||||
sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libspeechd-dev libxkbcommon-dev libssl-dev
|
||||
sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev
|
||||
```
|
||||
|
||||
You need to either use `edition = "2021"`, or set `resolver = "2"` in the `[workspace`] section of your to-level `Cargo.toml`. See [this link](https://doc.rust-lang.org/edition-guide/rust-2021/default-cargo-resolver.html) for more info.
|
||||
You need to either use `edition = "2021"`, or set `resolver = "2"` in the `[workspace]` section of your to-level `Cargo.toml`. See [this link](https://doc.rust-lang.org/edition-guide/rust-2021/default-cargo-resolver.html) for more info.
|
||||
|
||||
You can opt-in to the using [`egui_wgpu`](https://github.com/emilk/egui/tree/master/crates/egui_wgpu) for rendering by enabling the `wgpu` feature and setting `NativeOptions::renderer` to `Renderer::Wgpu`.
|
||||
|
||||
|
||||
## Alternatives
|
||||
`eframe` is not the only way to write an app using `egui`! You can also try [`egui-miniquad`](https://github.com/not-fl3/egui-miniquad), [`bevy_egui`](https://github.com/mvlabat/bevy_egui), [`egui_sdl2_gl`](https://github.com/ArjunNair/egui_sdl2_gl), and others.
|
||||
|
||||
You can also use `egui_glow` and [`winit`](https://github.com/rust-windowing/winit) to build your own app as demonstrated in <https://github.com/emilk/egui/blob/master/crates/egui_glow/examples/pure_glow.rs>.
|
||||
|
||||
|
||||
## Problems with running egui on the web
|
||||
`eframe` uses WebGL (via [`glow`](https://crates.io/crates/glow)) and WASM, and almost nothing else from the web tech stack. This has some benefits, but also produces some challenges and serious downsides.
|
||||
|
@ -41,7 +45,6 @@ You need to either use `edition = "2021"`, or set `resolver = "2"` in the `[work
|
|||
* Mobile text editing is not as good as for a normal web app.
|
||||
* Accessibility: There is an experimental screen reader for `eframe`, but it has to be enabled explicitly. There is no JS function to ask "Does the user want a screen reader?" (and there should probably not be such a function, due to user tracking/integrity concerns).
|
||||
* No integration with browser settings for colors and fonts.
|
||||
* On Linux and Mac, Firefox will copy the WebGL render target from GPU, to CPU and then back again (https://bugzilla.mozilla.org/show_bug.cgi?id=1010527#c0), slowing down egui.
|
||||
|
||||
In many ways, `eframe` is trying to make the browser do something it wasn't designed to do (though there are many things browser vendors could do to improve how well libraries like egui work).
|
||||
|
1065
crates/eframe/src/epi.rs
Normal file
1065
crates/eframe/src/epi.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -48,15 +48,26 @@
|
|||
//! /// Call this once from the HTML.
|
||||
//! #[cfg(target_arch = "wasm32")]
|
||||
//! #[wasm_bindgen]
|
||||
//! pub fn start(canvas_id: &str) -> Result<(), eframe::wasm_bindgen::JsValue> {
|
||||
//! eframe::start_web(canvas_id, Box::new(|cc| Box::new(MyApp::new(cc))))
|
||||
//! pub async fn start(canvas_id: &str) -> Result<AppRunnerRef, eframe::wasm_bindgen::JsValue> {
|
||||
//! let web_options = eframe::WebOptions::default();
|
||||
//! eframe::start_web(canvas_id, web_options, Box::new(|cc| Box::new(MyEguiApp::new(cc)))).await
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Feature flags
|
||||
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
|
||||
//!
|
||||
|
||||
#![allow(clippy::needless_doctest_main)]
|
||||
|
||||
// Re-export all useful libraries:
|
||||
pub use {egui, egui::emath, egui::epaint, glow};
|
||||
pub use {egui, egui::emath, egui::epaint};
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
pub use {egui_glow, glow};
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
pub use {egui_wgpu, wgpu};
|
||||
|
||||
mod epi;
|
||||
|
||||
|
@ -67,11 +78,14 @@ pub use epi::*;
|
|||
// When compiling for web
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod web;
|
||||
pub mod web;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use wasm_bindgen;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use web::AppRunnerRef;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use web_sys;
|
||||
|
||||
|
@ -83,25 +97,47 @@ pub use web_sys;
|
|||
/// use wasm_bindgen::prelude::*;
|
||||
///
|
||||
/// /// This is the entry-point for all the web-assembly.
|
||||
/// /// This is called once from the HTML.
|
||||
/// /// This is called from the HTML.
|
||||
/// /// It loads the app, installs some callbacks, then returns.
|
||||
/// /// It returns a handle to the running app that can be stopped calling `AppRunner::stop_web`.
|
||||
/// /// You can add more callbacks like this if you want to call in to your code.
|
||||
/// #[cfg(target_arch = "wasm32")]
|
||||
/// #[wasm_bindgen]
|
||||
/// pub fn start(canvas_id: &str) -> Result<(), eframe::wasm_bindgen::JsValue> {
|
||||
/// eframe::start_web(canvas_id, Box::new(|cc| Box::new(MyEguiApp::new(cc))))
|
||||
/// pub struct WebHandle {
|
||||
/// handle: AppRunnerRef,
|
||||
/// }
|
||||
/// #[cfg(target_arch = "wasm32")]
|
||||
/// #[wasm_bindgen]
|
||||
/// pub async fn start(canvas_id: &str) -> Result<WebHandle, eframe::wasm_bindgen::JsValue> {
|
||||
/// let web_options = eframe::WebOptions::default();
|
||||
/// eframe::start_web(
|
||||
/// canvas_id,
|
||||
/// web_options,
|
||||
/// Box::new(|cc| Box::new(MyEguiApp::new(cc))),
|
||||
/// )
|
||||
/// .await
|
||||
/// .map(|handle| WebHandle { handle })
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
/// Failing to initialize WebGL graphics.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn start_web(canvas_id: &str, app_creator: AppCreator) -> Result<(), wasm_bindgen::JsValue> {
|
||||
web::start(canvas_id, app_creator)?;
|
||||
Ok(())
|
||||
pub async fn start_web(
|
||||
canvas_id: &str,
|
||||
web_options: WebOptions,
|
||||
app_creator: AppCreator,
|
||||
) -> std::result::Result<AppRunnerRef, wasm_bindgen::JsValue> {
|
||||
let handle = web::start(canvas_id, web_options, app_creator).await?;
|
||||
|
||||
Ok(handle)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// When compiling natively
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
mod native;
|
||||
|
||||
/// This is how you start a native (desktop) app.
|
||||
|
@ -139,32 +175,88 @@ mod native;
|
|||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
/// This function can fail if we fail to set up a graphics context.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn run_native(app_name: &str, native_options: NativeOptions, app_creator: AppCreator) -> ! {
|
||||
native::run(app_name, &native_options, app_creator)
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
pub fn run_native(
|
||||
app_name: &str,
|
||||
native_options: NativeOptions,
|
||||
app_creator: AppCreator,
|
||||
) -> Result<()> {
|
||||
let renderer = native_options.renderer;
|
||||
|
||||
#[cfg(not(feature = "__screenshot"))]
|
||||
assert!(
|
||||
std::env::var("EFRAME_SCREENSHOT_TO").is_err(),
|
||||
"EFRAME_SCREENSHOT_TO found without compiling with the '__screenshot' feature"
|
||||
);
|
||||
|
||||
match renderer {
|
||||
#[cfg(feature = "glow")]
|
||||
Renderer::Glow => {
|
||||
tracing::debug!("Using the glow renderer");
|
||||
native::run::run_glow(app_name, native_options, app_creator)
|
||||
}
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
Renderer::Wgpu => {
|
||||
tracing::debug!("Using the wgpu renderer");
|
||||
native::run::run_wgpu(app_name, native_options, app_creator)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// The different problems that can occur when trying to run `eframe`.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[error("winit error: {0}")]
|
||||
Winit(#[from] winit::error::OsError),
|
||||
|
||||
#[cfg(all(feature = "glow", not(target_arch = "wasm32")))]
|
||||
#[error("glutin error: {0}")]
|
||||
Glutin(#[from] glutin::error::Error),
|
||||
|
||||
#[cfg(all(feature = "glow", not(target_arch = "wasm32")))]
|
||||
#[error("Found no glutin configs matching the template: {0:?}. error: {1:?}")]
|
||||
NoGlutinConfigs(glutin::config::ConfigTemplate, Box<dyn std::error::Error>),
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[error("WGPU error: {0}")]
|
||||
Wgpu(#[from] egui_wgpu::WgpuError),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Profiling macro for feature "puffin"
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
macro_rules! profile_function {
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
mod profiling_scopes {
|
||||
/// Profiling macro for feature "puffin"
|
||||
macro_rules! profile_function {
|
||||
($($arg: tt)*) => {
|
||||
#[cfg(feature = "puffin")]
|
||||
puffin::profile_function!($($arg)*);
|
||||
};
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) use profile_function;
|
||||
pub(crate) use profile_function;
|
||||
|
||||
/// Profiling macro for feature "puffin"
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
macro_rules! profile_scope {
|
||||
/// Profiling macro for feature "puffin"
|
||||
macro_rules! profile_scope {
|
||||
($($arg: tt)*) => {
|
||||
#[cfg(feature = "puffin")]
|
||||
puffin::profile_scope!($($arg)*);
|
||||
};
|
||||
}
|
||||
pub(crate) use profile_scope;
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) use profile_scope;
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
pub(crate) use profiling_scopes::*;
|
567
crates/eframe/src/native/epi_integration.rs
Normal file
567
crates/eframe/src/native/epi_integration.rs
Normal file
|
@ -0,0 +1,567 @@
|
|||
use winit::event_loop::EventLoopWindowTarget;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use winit::platform::macos::WindowBuilderExtMacOS as _;
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
use egui::accesskit;
|
||||
use egui::NumExt as _;
|
||||
#[cfg(feature = "accesskit")]
|
||||
use egui_winit::accesskit_winit;
|
||||
use egui_winit::{native_pixels_per_point, EventResponse, WindowSettings};
|
||||
|
||||
use crate::{epi, Theme, WindowInfo};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct WindowState {
|
||||
// We cannot simply call `winit::Window::is_minimized/is_maximized`
|
||||
// because that deadlocks on mac.
|
||||
pub minimized: bool,
|
||||
pub maximized: bool,
|
||||
}
|
||||
|
||||
pub fn points_to_size(points: egui::Vec2) -> winit::dpi::LogicalSize<f64> {
|
||||
winit::dpi::LogicalSize {
|
||||
width: points.x as f64,
|
||||
height: points.y as f64,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_window_info(
|
||||
window: &winit::window::Window,
|
||||
pixels_per_point: f32,
|
||||
window_state: &WindowState,
|
||||
) -> WindowInfo {
|
||||
let position = window
|
||||
.outer_position()
|
||||
.ok()
|
||||
.map(|pos| pos.to_logical::<f32>(pixels_per_point.into()))
|
||||
.map(|pos| egui::Pos2 { x: pos.x, y: pos.y });
|
||||
|
||||
let monitor = window.current_monitor().is_some();
|
||||
let monitor_size = if monitor {
|
||||
let size = window.current_monitor().unwrap().size();
|
||||
Some(egui::vec2(size.width as _, size.height as _))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let size = window
|
||||
.inner_size()
|
||||
.to_logical::<f32>(pixels_per_point.into());
|
||||
|
||||
// NOTE: calling window.is_minimized() or window.is_maximized() deadlocks on Mac.
|
||||
|
||||
WindowInfo {
|
||||
position,
|
||||
fullscreen: window.fullscreen().is_some(),
|
||||
minimized: window_state.minimized,
|
||||
maximized: window_state.maximized,
|
||||
size: egui::Vec2 {
|
||||
x: size.width,
|
||||
y: size.height,
|
||||
},
|
||||
monitor_size,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn window_builder<E>(
|
||||
event_loop: &EventLoopWindowTarget<E>,
|
||||
title: &str,
|
||||
native_options: &epi::NativeOptions,
|
||||
window_settings: Option<WindowSettings>,
|
||||
) -> winit::window::WindowBuilder {
|
||||
let epi::NativeOptions {
|
||||
maximized,
|
||||
decorated,
|
||||
fullscreen,
|
||||
#[cfg(target_os = "macos")]
|
||||
fullsize_content,
|
||||
drag_and_drop_support,
|
||||
icon_data,
|
||||
initial_window_pos,
|
||||
initial_window_size,
|
||||
min_window_size,
|
||||
max_window_size,
|
||||
resizable,
|
||||
transparent,
|
||||
centered,
|
||||
..
|
||||
} = native_options;
|
||||
|
||||
let window_icon = icon_data.clone().and_then(load_icon);
|
||||
|
||||
let mut window_builder = winit::window::WindowBuilder::new()
|
||||
.with_title(title)
|
||||
.with_decorations(*decorated)
|
||||
.with_fullscreen(fullscreen.then(|| winit::window::Fullscreen::Borderless(None)))
|
||||
.with_maximized(*maximized)
|
||||
.with_resizable(*resizable)
|
||||
.with_transparent(*transparent)
|
||||
.with_window_icon(window_icon)
|
||||
// Keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279
|
||||
// We must also keep the window hidden until AccessKit is initialized.
|
||||
.with_visible(false);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
if *fullsize_content {
|
||||
window_builder = window_builder
|
||||
.with_title_hidden(true)
|
||||
.with_titlebar_transparent(true)
|
||||
.with_fullsize_content_view(true);
|
||||
}
|
||||
|
||||
if let Some(min_size) = *min_window_size {
|
||||
window_builder = window_builder.with_min_inner_size(points_to_size(min_size));
|
||||
}
|
||||
if let Some(max_size) = *max_window_size {
|
||||
window_builder = window_builder.with_max_inner_size(points_to_size(max_size));
|
||||
}
|
||||
|
||||
window_builder = window_builder_drag_and_drop(window_builder, *drag_and_drop_support);
|
||||
|
||||
let inner_size_points = if let Some(mut window_settings) = window_settings {
|
||||
// Restore pos/size from previous session
|
||||
window_settings.clamp_to_sane_values(largest_monitor_point_size(event_loop));
|
||||
#[cfg(windows)]
|
||||
window_settings.clamp_window_to_sane_position(&event_loop);
|
||||
window_builder = window_settings.initialize_window(window_builder);
|
||||
window_settings.inner_size_points()
|
||||
} else {
|
||||
if let Some(pos) = *initial_window_pos {
|
||||
window_builder = window_builder.with_position(winit::dpi::LogicalPosition {
|
||||
x: pos.x as f64,
|
||||
y: pos.y as f64,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(initial_window_size) = *initial_window_size {
|
||||
let initial_window_size =
|
||||
initial_window_size.at_most(largest_monitor_point_size(event_loop));
|
||||
window_builder = window_builder.with_inner_size(points_to_size(initial_window_size));
|
||||
}
|
||||
|
||||
*initial_window_size
|
||||
};
|
||||
|
||||
if *centered {
|
||||
if let Some(monitor) = event_loop.available_monitors().next() {
|
||||
let monitor_size = monitor.size();
|
||||
let inner_size = inner_size_points.unwrap_or(egui::Vec2 { x: 800.0, y: 600.0 });
|
||||
if monitor_size.width > 0 && monitor_size.height > 0 {
|
||||
let x = (monitor_size.width - inner_size.x as u32) / 2;
|
||||
let y = (monitor_size.height - inner_size.y as u32) / 2;
|
||||
window_builder = window_builder.with_position(winit::dpi::LogicalPosition {
|
||||
x: x as f64,
|
||||
y: y as f64,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
window_builder
|
||||
}
|
||||
|
||||
pub fn apply_native_options_to_window(
|
||||
window: &winit::window::Window,
|
||||
native_options: &crate::NativeOptions,
|
||||
) {
|
||||
use winit::window::WindowLevel;
|
||||
window.set_window_level(if native_options.always_on_top {
|
||||
WindowLevel::AlwaysOnTop
|
||||
} else {
|
||||
WindowLevel::Normal
|
||||
});
|
||||
}
|
||||
|
||||
fn largest_monitor_point_size<E>(event_loop: &EventLoopWindowTarget<E>) -> egui::Vec2 {
|
||||
let mut max_size = egui::Vec2::ZERO;
|
||||
|
||||
for monitor in event_loop.available_monitors() {
|
||||
let size = monitor.size().to_logical::<f32>(monitor.scale_factor());
|
||||
let size = egui::vec2(size.width, size.height);
|
||||
max_size = max_size.max(size);
|
||||
}
|
||||
|
||||
if max_size == egui::Vec2::ZERO {
|
||||
egui::Vec2::splat(16000.0)
|
||||
} else {
|
||||
max_size
|
||||
}
|
||||
}
|
||||
|
||||
fn load_icon(icon_data: epi::IconData) -> Option<winit::window::Icon> {
|
||||
winit::window::Icon::from_rgba(icon_data.rgba, icon_data.width, icon_data.height).ok()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn window_builder_drag_and_drop(
|
||||
window_builder: winit::window::WindowBuilder,
|
||||
enable: bool,
|
||||
) -> winit::window::WindowBuilder {
|
||||
use winit::platform::windows::WindowBuilderExtWindows as _;
|
||||
window_builder.with_drag_and_drop(enable)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn window_builder_drag_and_drop(
|
||||
window_builder: winit::window::WindowBuilder,
|
||||
_enable: bool,
|
||||
) -> winit::window::WindowBuilder {
|
||||
// drag and drop can only be disabled on windows
|
||||
window_builder
|
||||
}
|
||||
|
||||
pub fn handle_app_output(
|
||||
window: &winit::window::Window,
|
||||
current_pixels_per_point: f32,
|
||||
app_output: epi::backend::AppOutput,
|
||||
window_state: &mut WindowState,
|
||||
) {
|
||||
let epi::backend::AppOutput {
|
||||
close: _,
|
||||
window_size,
|
||||
window_title,
|
||||
decorated,
|
||||
fullscreen,
|
||||
drag_window,
|
||||
window_pos,
|
||||
visible: _, // handled in post_present
|
||||
always_on_top,
|
||||
minimized,
|
||||
maximized,
|
||||
} = app_output;
|
||||
|
||||
if let Some(decorated) = decorated {
|
||||
window.set_decorations(decorated);
|
||||
}
|
||||
|
||||
if let Some(window_size) = window_size {
|
||||
window.set_inner_size(
|
||||
winit::dpi::PhysicalSize {
|
||||
width: (current_pixels_per_point * window_size.x).round(),
|
||||
height: (current_pixels_per_point * window_size.y).round(),
|
||||
}
|
||||
.to_logical::<f32>(native_pixels_per_point(window) as f64),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(fullscreen) = fullscreen {
|
||||
window.set_fullscreen(fullscreen.then_some(winit::window::Fullscreen::Borderless(None)));
|
||||
}
|
||||
|
||||
if let Some(window_title) = window_title {
|
||||
window.set_title(&window_title);
|
||||
}
|
||||
|
||||
if let Some(window_pos) = window_pos {
|
||||
window.set_outer_position(winit::dpi::PhysicalPosition {
|
||||
x: window_pos.x as f64,
|
||||
y: window_pos.y as f64,
|
||||
});
|
||||
}
|
||||
|
||||
if drag_window {
|
||||
let _ = window.drag_window();
|
||||
}
|
||||
|
||||
if let Some(always_on_top) = always_on_top {
|
||||
use winit::window::WindowLevel;
|
||||
window.set_window_level(if always_on_top {
|
||||
WindowLevel::AlwaysOnTop
|
||||
} else {
|
||||
WindowLevel::Normal
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(minimized) = minimized {
|
||||
window.set_minimized(minimized);
|
||||
window_state.minimized = minimized;
|
||||
}
|
||||
|
||||
if let Some(maximized) = maximized {
|
||||
window.set_maximized(maximized);
|
||||
window_state.maximized = maximized;
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// For loading/saving app state and/or egui memory to disk.
|
||||
pub fn create_storage(_app_name: &str) -> Option<Box<dyn epi::Storage>> {
|
||||
#[cfg(feature = "persistence")]
|
||||
if let Some(storage) = super::file_storage::FileStorage::from_app_name(_app_name) {
|
||||
return Some(Box::new(storage));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Everything needed to make a winit-based integration for [`epi`].
|
||||
pub struct EpiIntegration {
|
||||
pub frame: epi::Frame,
|
||||
last_auto_save: std::time::Instant,
|
||||
pub egui_ctx: egui::Context,
|
||||
pending_full_output: egui::FullOutput,
|
||||
egui_winit: egui_winit::State,
|
||||
/// When set, it is time to close the native window.
|
||||
close: bool,
|
||||
can_drag_window: bool,
|
||||
window_state: WindowState,
|
||||
}
|
||||
|
||||
impl EpiIntegration {
|
||||
pub fn new<E>(
|
||||
event_loop: &EventLoopWindowTarget<E>,
|
||||
max_texture_side: usize,
|
||||
window: &winit::window::Window,
|
||||
system_theme: Option<Theme>,
|
||||
storage: Option<Box<dyn epi::Storage>>,
|
||||
#[cfg(feature = "glow")] gl: Option<std::sync::Arc<glow::Context>>,
|
||||
#[cfg(feature = "wgpu")] wgpu_render_state: Option<egui_wgpu::RenderState>,
|
||||
) -> Self {
|
||||
let egui_ctx = egui::Context::default();
|
||||
|
||||
let memory = load_egui_memory(storage.as_deref()).unwrap_or_default();
|
||||
egui_ctx.memory_mut(|mem| *mem = memory);
|
||||
|
||||
let native_pixels_per_point = window.scale_factor() as f32;
|
||||
|
||||
let window_state = WindowState {
|
||||
minimized: window.is_minimized().unwrap_or(false),
|
||||
maximized: window.is_maximized(),
|
||||
};
|
||||
|
||||
let frame = epi::Frame {
|
||||
info: epi::IntegrationInfo {
|
||||
system_theme,
|
||||
cpu_usage: None,
|
||||
native_pixels_per_point: Some(native_pixels_per_point),
|
||||
window_info: read_window_info(window, egui_ctx.pixels_per_point(), &window_state),
|
||||
},
|
||||
output: epi::backend::AppOutput {
|
||||
visible: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
storage,
|
||||
#[cfg(feature = "glow")]
|
||||
gl,
|
||||
#[cfg(feature = "wgpu")]
|
||||
wgpu_render_state,
|
||||
};
|
||||
|
||||
let mut egui_winit = egui_winit::State::new(event_loop);
|
||||
egui_winit.set_max_texture_side(max_texture_side);
|
||||
egui_winit.set_pixels_per_point(native_pixels_per_point);
|
||||
|
||||
Self {
|
||||
frame,
|
||||
last_auto_save: std::time::Instant::now(),
|
||||
egui_ctx,
|
||||
egui_winit,
|
||||
pending_full_output: Default::default(),
|
||||
close: false,
|
||||
can_drag_window: false,
|
||||
window_state,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub fn init_accesskit<E: From<accesskit_winit::ActionRequestEvent> + Send>(
|
||||
&mut self,
|
||||
window: &winit::window::Window,
|
||||
event_loop_proxy: winit::event_loop::EventLoopProxy<E>,
|
||||
) {
|
||||
let egui_ctx = self.egui_ctx.clone();
|
||||
self.egui_winit
|
||||
.init_accesskit(window, event_loop_proxy, move || {
|
||||
// This function is called when an accessibility client
|
||||
// (e.g. screen reader) makes its first request. If we got here,
|
||||
// we know that an accessibility tree is actually wanted.
|
||||
egui_ctx.enable_accesskit();
|
||||
// Enqueue a repaint so we'll receive a full tree update soon.
|
||||
egui_ctx.request_repaint();
|
||||
egui_ctx.accesskit_placeholder_tree_update()
|
||||
});
|
||||
}
|
||||
|
||||
pub fn warm_up(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) {
|
||||
crate::profile_function!();
|
||||
let saved_memory: egui::Memory = self.egui_ctx.memory(|mem| mem.clone());
|
||||
self.egui_ctx
|
||||
.memory_mut(|mem| mem.set_everything_is_visible(true));
|
||||
let full_output = self.update(app, window);
|
||||
self.pending_full_output.append(full_output); // Handle it next frame
|
||||
self.egui_ctx.memory_mut(|mem| *mem = saved_memory); // We don't want to remember that windows were huge.
|
||||
self.egui_ctx.clear_animations();
|
||||
}
|
||||
|
||||
/// If `true`, it is time to close the native window.
|
||||
pub fn should_close(&self) -> bool {
|
||||
self.close
|
||||
}
|
||||
|
||||
pub fn on_event(
|
||||
&mut self,
|
||||
app: &mut dyn epi::App,
|
||||
event: &winit::event::WindowEvent<'_>,
|
||||
) -> EventResponse {
|
||||
use winit::event::{ElementState, MouseButton, WindowEvent};
|
||||
|
||||
match event {
|
||||
WindowEvent::CloseRequested => {
|
||||
tracing::debug!("Received WindowEvent::CloseRequested");
|
||||
self.close = app.on_close_event();
|
||||
tracing::debug!("App::on_close_event returned {}", self.close);
|
||||
}
|
||||
WindowEvent::Destroyed => {
|
||||
tracing::debug!("Received WindowEvent::Destroyed");
|
||||
self.close = true;
|
||||
}
|
||||
WindowEvent::MouseInput {
|
||||
button: MouseButton::Left,
|
||||
state: ElementState::Pressed,
|
||||
..
|
||||
} => self.can_drag_window = true,
|
||||
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
|
||||
self.frame.info.native_pixels_per_point = Some(*scale_factor as _);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
self.egui_winit.on_event(&self.egui_ctx, event)
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub fn on_accesskit_action_request(&mut self, request: accesskit::ActionRequest) {
|
||||
self.egui_winit.on_accesskit_action_request(request);
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
&mut self,
|
||||
app: &mut dyn epi::App,
|
||||
window: &winit::window::Window,
|
||||
) -> egui::FullOutput {
|
||||
let frame_start = std::time::Instant::now();
|
||||
|
||||
self.frame.info.window_info =
|
||||
read_window_info(window, self.egui_ctx.pixels_per_point(), &self.window_state);
|
||||
let raw_input = self.egui_winit.take_egui_input(window);
|
||||
|
||||
// Run user code:
|
||||
let full_output = self.egui_ctx.run(raw_input, |egui_ctx| {
|
||||
crate::profile_scope!("App::update");
|
||||
app.update(egui_ctx, &mut self.frame);
|
||||
});
|
||||
|
||||
self.pending_full_output.append(full_output);
|
||||
let full_output = std::mem::take(&mut self.pending_full_output);
|
||||
|
||||
{
|
||||
let mut app_output = self.frame.take_app_output();
|
||||
app_output.drag_window &= self.can_drag_window; // Necessary on Windows; see https://github.com/emilk/egui/pull/1108
|
||||
self.can_drag_window = false;
|
||||
if app_output.close {
|
||||
self.close = app.on_close_event();
|
||||
tracing::debug!("App::on_close_event returned {}", self.close);
|
||||
}
|
||||
self.frame.output.visible = app_output.visible; // this is handled by post_present
|
||||
handle_app_output(
|
||||
window,
|
||||
self.egui_ctx.pixels_per_point(),
|
||||
app_output,
|
||||
&mut self.window_state,
|
||||
);
|
||||
}
|
||||
|
||||
let frame_time = frame_start.elapsed().as_secs_f64() as f32;
|
||||
self.frame.info.cpu_usage = Some(frame_time);
|
||||
|
||||
full_output
|
||||
}
|
||||
|
||||
pub fn post_rendering(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) {
|
||||
let inner_size = window.inner_size();
|
||||
let window_size_px = [inner_size.width, inner_size.height];
|
||||
|
||||
app.post_rendering(window_size_px, &self.frame);
|
||||
}
|
||||
|
||||
pub fn post_present(&mut self, window: &winit::window::Window) {
|
||||
if let Some(visible) = self.frame.output.visible.take() {
|
||||
window.set_visible(visible);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_platform_output(
|
||||
&mut self,
|
||||
window: &winit::window::Window,
|
||||
platform_output: egui::PlatformOutput,
|
||||
) {
|
||||
self.egui_winit
|
||||
.handle_platform_output(window, &self.egui_ctx, platform_output);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Persistance stuff:
|
||||
|
||||
pub fn maybe_autosave(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) {
|
||||
let now = std::time::Instant::now();
|
||||
if now - self.last_auto_save > app.auto_save_interval() {
|
||||
self.save(app, window);
|
||||
self.last_auto_save = now;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save(&mut self, _app: &mut dyn epi::App, _window: &winit::window::Window) {
|
||||
#[cfg(feature = "persistence")]
|
||||
if let Some(storage) = self.frame.storage_mut() {
|
||||
crate::profile_function!();
|
||||
|
||||
if _app.persist_native_window() {
|
||||
crate::profile_scope!("native_window");
|
||||
epi::set_value(
|
||||
storage,
|
||||
STORAGE_WINDOW_KEY,
|
||||
&WindowSettings::from_display(_window),
|
||||
);
|
||||
}
|
||||
if _app.persist_egui_memory() {
|
||||
crate::profile_scope!("egui_memory");
|
||||
self.egui_ctx
|
||||
.memory(|mem| epi::set_value(storage, STORAGE_EGUI_MEMORY_KEY, mem));
|
||||
}
|
||||
{
|
||||
crate::profile_scope!("App::save");
|
||||
_app.save(storage);
|
||||
}
|
||||
|
||||
crate::profile_scope!("Storage::flush");
|
||||
storage.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "persistence")]
|
||||
const STORAGE_EGUI_MEMORY_KEY: &str = "egui";
|
||||
|
||||
#[cfg(feature = "persistence")]
|
||||
const STORAGE_WINDOW_KEY: &str = "window";
|
||||
|
||||
pub fn load_window_settings(_storage: Option<&dyn epi::Storage>) -> Option<WindowSettings> {
|
||||
#[cfg(feature = "persistence")]
|
||||
{
|
||||
epi::get_value(_storage?, STORAGE_WINDOW_KEY)
|
||||
}
|
||||
#[cfg(not(feature = "persistence"))]
|
||||
None
|
||||
}
|
||||
|
||||
pub fn load_egui_memory(_storage: Option<&dyn epi::Storage>) -> Option<egui::Memory> {
|
||||
#[cfg(feature = "persistence")]
|
||||
{
|
||||
epi::get_value(_storage?, STORAGE_EGUI_MEMORY_KEY)
|
||||
}
|
||||
#[cfg(not(feature = "persistence"))]
|
||||
None
|
||||
}
|
|
@ -26,6 +26,7 @@ impl FileStorage {
|
|||
/// Store the state in this .ron file.
|
||||
pub fn from_ron_filepath(ron_filepath: impl Into<PathBuf>) -> Self {
|
||||
let ron_filepath: PathBuf = ron_filepath.into();
|
||||
tracing::debug!("Loading app state from {:?}…", ron_filepath);
|
||||
Self {
|
||||
kv: read_ron(&ron_filepath).unwrap_or_default(),
|
||||
ron_filepath,
|
|
@ -1,8 +1,6 @@
|
|||
mod epi_integration;
|
||||
mod run;
|
||||
pub mod run;
|
||||
|
||||
/// File storage which can be used by native backends.
|
||||
#[cfg(feature = "persistence")]
|
||||
pub mod file_storage;
|
||||
|
||||
pub use run::run;
|
1423
crates/eframe/src/native/run.rs
Normal file
1423
crates/eframe/src/native/run.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,9 +1,11 @@
|
|||
use super::{glow_wrapping::WrappedGlowPainter, *};
|
||||
use egui::{
|
||||
mutex::{Mutex, MutexGuard},
|
||||
TexturesDelta,
|
||||
};
|
||||
|
||||
use crate::epi;
|
||||
use crate::{epi, App};
|
||||
|
||||
use egui::TexturesDelta;
|
||||
pub use egui::{pos2, Color32};
|
||||
use super::{web_painter::WebPainter, *};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
|
@ -34,17 +36,48 @@ impl WebInput {
|
|||
|
||||
use std::sync::atomic::Ordering::SeqCst;
|
||||
|
||||
pub struct NeedRepaint(std::sync::atomic::AtomicBool);
|
||||
/// Stores when to do the next repaint.
|
||||
pub struct NeedRepaint(Mutex<f64>);
|
||||
|
||||
impl Default for NeedRepaint {
|
||||
fn default() -> Self {
|
||||
Self(true.into())
|
||||
Self(Mutex::new(f64::NEG_INFINITY)) // start with a repaint
|
||||
}
|
||||
}
|
||||
|
||||
impl NeedRepaint {
|
||||
pub fn fetch_and_clear(&self) -> bool {
|
||||
self.0.swap(false, SeqCst)
|
||||
/// Returns the time (in [`now_sec`] scale) when
|
||||
/// we should next repaint.
|
||||
pub fn when_to_repaint(&self) -> f64 {
|
||||
*self.0.lock()
|
||||
}
|
||||
|
||||
/// Unschedule repainting.
|
||||
pub fn clear(&self) {
|
||||
*self.0.lock() = f64::INFINITY;
|
||||
}
|
||||
|
||||
pub fn repaint_after(&self, num_seconds: f64) {
|
||||
let mut repaint_time = self.0.lock();
|
||||
*repaint_time = repaint_time.min(now_sec() + num_seconds);
|
||||
}
|
||||
|
||||
pub fn repaint_asap(&self) {
|
||||
*self.0.lock() = f64::NEG_INFINITY;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IsDestroyed(std::sync::atomic::AtomicBool);
|
||||
|
||||
impl Default for IsDestroyed {
|
||||
fn default() -> Self {
|
||||
Self(false.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl IsDestroyed {
|
||||
pub fn fetch(&self) -> bool {
|
||||
self.0.load(SeqCst)
|
||||
}
|
||||
|
||||
pub fn set_true(&self) {
|
||||
|
@ -54,6 +87,10 @@ impl NeedRepaint {
|
|||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
fn user_agent() -> Option<String> {
|
||||
web_sys::window()?.navigator().user_agent().ok()
|
||||
}
|
||||
|
||||
fn web_location() -> epi::Location {
|
||||
let location = web_sys::window().unwrap().location();
|
||||
|
||||
|
@ -68,7 +105,7 @@ fn web_location() -> epi::Location {
|
|||
|
||||
let query_map = parse_query_map(&query)
|
||||
.iter()
|
||||
.map(|(k, v)| ((*k).to_string(), (*v).to_string()))
|
||||
.map(|(k, v)| ((*k).to_owned(), (*v).to_owned()))
|
||||
.collect();
|
||||
|
||||
epi::Location {
|
||||
|
@ -128,60 +165,94 @@ fn test_parse_query() {
|
|||
pub struct AppRunner {
|
||||
pub(crate) frame: epi::Frame,
|
||||
egui_ctx: egui::Context,
|
||||
painter: WrappedGlowPainter,
|
||||
painter: ActiveWebPainter,
|
||||
pub(crate) input: WebInput,
|
||||
app: Box<dyn epi::App>,
|
||||
pub(crate) needs_repaint: std::sync::Arc<NeedRepaint>,
|
||||
pub(crate) is_destroyed: std::sync::Arc<IsDestroyed>,
|
||||
last_save_time: f64,
|
||||
screen_reader: super::screen_reader::ScreenReader,
|
||||
pub(crate) text_cursor_pos: Option<egui::Pos2>,
|
||||
pub(crate) mutable_text_under_cursor: bool,
|
||||
textures_delta: TexturesDelta,
|
||||
pub events_to_unsubscribe: Vec<EventToUnsubscribe>,
|
||||
}
|
||||
|
||||
impl Drop for AppRunner {
|
||||
fn drop(&mut self) {
|
||||
tracing::debug!("AppRunner has fully dropped");
|
||||
}
|
||||
}
|
||||
|
||||
impl AppRunner {
|
||||
pub fn new(canvas_id: &str, app_creator: epi::AppCreator) -> Result<Self, JsValue> {
|
||||
let painter = WrappedGlowPainter::new(canvas_id).map_err(JsValue::from)?; // fail early
|
||||
/// # Errors
|
||||
/// Failure to initialize WebGL renderer.
|
||||
pub async fn new(
|
||||
canvas_id: &str,
|
||||
web_options: crate::WebOptions,
|
||||
app_creator: epi::AppCreator,
|
||||
) -> Result<Self, String> {
|
||||
let painter = ActiveWebPainter::new(canvas_id, &web_options).await?;
|
||||
|
||||
let prefer_dark_mode = super::prefer_dark_mode();
|
||||
let system_theme = if web_options.follow_system_theme {
|
||||
super::system_theme()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let info = epi::IntegrationInfo {
|
||||
web_info: Some(epi::WebInfo {
|
||||
web_info: epi::WebInfo {
|
||||
user_agent: user_agent().unwrap_or_default(),
|
||||
location: web_location(),
|
||||
}),
|
||||
prefer_dark_mode,
|
||||
},
|
||||
system_theme,
|
||||
cpu_usage: None,
|
||||
native_pixels_per_point: Some(native_pixels_per_point()),
|
||||
};
|
||||
let storage = LocalStorage::default();
|
||||
|
||||
let egui_ctx = egui::Context::default();
|
||||
egui_ctx.set_os(egui::os::OperatingSystem::from_user_agent(
|
||||
&user_agent().unwrap_or_default(),
|
||||
));
|
||||
load_memory(&egui_ctx);
|
||||
if prefer_dark_mode == Some(true) {
|
||||
egui_ctx.set_visuals(egui::Visuals::dark());
|
||||
} else {
|
||||
egui_ctx.set_visuals(egui::Visuals::light());
|
||||
}
|
||||
|
||||
let theme = system_theme.unwrap_or(web_options.default_theme);
|
||||
egui_ctx.set_visuals(theme.egui_visuals());
|
||||
|
||||
let app = app_creator(&epi::CreationContext {
|
||||
egui_ctx: egui_ctx.clone(),
|
||||
integration_info: info.clone(),
|
||||
storage: Some(&storage),
|
||||
gl: painter.painter.gl().clone(),
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
gl: Some(painter.gl().clone()),
|
||||
|
||||
#[cfg(all(feature = "wgpu", not(feature = "glow")))]
|
||||
wgpu_render_state: painter.render_state(),
|
||||
#[cfg(all(feature = "wgpu", feature = "glow"))]
|
||||
wgpu_render_state: None,
|
||||
});
|
||||
|
||||
let frame = epi::Frame {
|
||||
info,
|
||||
output: Default::default(),
|
||||
storage: Some(Box::new(storage)),
|
||||
gl: painter.gl().clone(),
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
gl: Some(painter.gl().clone()),
|
||||
|
||||
#[cfg(all(feature = "wgpu", not(feature = "glow")))]
|
||||
wgpu_render_state: painter.render_state(),
|
||||
#[cfg(all(feature = "wgpu", feature = "glow"))]
|
||||
wgpu_render_state: None,
|
||||
};
|
||||
|
||||
let needs_repaint: std::sync::Arc<NeedRepaint> = Default::default();
|
||||
{
|
||||
let needs_repaint = needs_repaint.clone();
|
||||
egui_ctx.set_request_repaint_callback(move || {
|
||||
needs_repaint.0.store(true, SeqCst);
|
||||
needs_repaint.repaint_asap();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -192,11 +263,13 @@ impl AppRunner {
|
|||
input: Default::default(),
|
||||
app,
|
||||
needs_repaint,
|
||||
is_destroyed: Default::default(),
|
||||
last_save_time: now_sec(),
|
||||
screen_reader: Default::default(),
|
||||
text_cursor_pos: None,
|
||||
mutable_text_under_cursor: false,
|
||||
textures_delta: Default::default(),
|
||||
events_to_unsubscribe: Default::default(),
|
||||
};
|
||||
|
||||
runner.input.raw.max_texture_side = Some(runner.painter.max_texture_side());
|
||||
|
@ -208,6 +281,17 @@ impl AppRunner {
|
|||
&self.egui_ctx
|
||||
}
|
||||
|
||||
/// Get mutable access to the concrete [`App`] we enclose.
|
||||
///
|
||||
/// This will panic if your app does not implement [`App::as_any_mut`].
|
||||
pub fn app_mut<ConreteApp: 'static + App>(&mut self) -> &mut ConreteApp {
|
||||
self.app
|
||||
.as_any_mut()
|
||||
.expect("Your app must implement `as_any_mut`, but it doesn't")
|
||||
.downcast_mut::<ConreteApp>()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn auto_save(&mut self) {
|
||||
let now = now_sec();
|
||||
let time_since_last_save = now - self.last_save_time;
|
||||
|
@ -229,19 +313,38 @@ impl AppRunner {
|
|||
|
||||
pub fn warm_up(&mut self) -> Result<(), JsValue> {
|
||||
if self.app.warm_up_enabled() {
|
||||
let saved_memory: egui::Memory = self.egui_ctx.memory().clone();
|
||||
self.egui_ctx.memory().set_everything_is_visible(true);
|
||||
let saved_memory: egui::Memory = self.egui_ctx.memory(|m| m.clone());
|
||||
self.egui_ctx
|
||||
.memory_mut(|m| m.set_everything_is_visible(true));
|
||||
self.logic()?;
|
||||
*self.egui_ctx.memory() = saved_memory; // We don't want to remember that windows were huge.
|
||||
self.egui_ctx.memory_mut(|m| *m = saved_memory); // We don't want to remember that windows were huge.
|
||||
self.egui_ctx.clear_animations();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns `true` if egui requests a repaint.
|
||||
pub fn destroy(&mut self) -> Result<(), JsValue> {
|
||||
let is_destroyed_already = self.is_destroyed.fetch();
|
||||
|
||||
if is_destroyed_already {
|
||||
tracing::warn!("App was destroyed already");
|
||||
Ok(())
|
||||
} else {
|
||||
tracing::debug!("Destroying");
|
||||
for x in self.events_to_unsubscribe.drain(..) {
|
||||
x.unsubscribe()?;
|
||||
}
|
||||
|
||||
self.painter.destroy();
|
||||
self.is_destroyed.set_true();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns how long to wait until the next repaint.
|
||||
///
|
||||
/// Call [`Self::paint`] later to paint
|
||||
pub fn logic(&mut self) -> Result<(bool, Vec<egui::ClippedPrimitive>), JsValue> {
|
||||
pub fn logic(&mut self) -> Result<(std::time::Duration, Vec<egui::ClippedPrimitive>), JsValue> {
|
||||
let frame_start = now_sec();
|
||||
|
||||
resize_canvas_to_screen_size(self.canvas_id(), self.app.max_size_points());
|
||||
|
@ -253,7 +356,7 @@ impl AppRunner {
|
|||
});
|
||||
let egui::FullOutput {
|
||||
platform_output,
|
||||
needs_repaint,
|
||||
repaint_after,
|
||||
textures_delta,
|
||||
shapes,
|
||||
} = full_output;
|
||||
|
@ -264,23 +367,11 @@ impl AppRunner {
|
|||
|
||||
{
|
||||
let app_output = self.frame.take_app_output();
|
||||
let epi::backend::AppOutput {
|
||||
quit: _, // Can't quit a web page
|
||||
window_size: _, // Can't resize a web page
|
||||
window_title: _, // TODO: change title of window
|
||||
decorated: _, // Can't toggle decorations
|
||||
drag_window: _, // Can't be dragged
|
||||
window_pos: _, // Can't set position of a web page
|
||||
} = app_output;
|
||||
let epi::backend::AppOutput {} = app_output;
|
||||
}
|
||||
|
||||
self.frame.info.cpu_usage = Some((now_sec() - frame_start) as f32);
|
||||
Ok((needs_repaint, clipped_primitives))
|
||||
}
|
||||
|
||||
pub fn clear_color_buffer(&self) {
|
||||
self.painter
|
||||
.clear(self.app.clear_color(&self.egui_ctx.style().visuals));
|
||||
Ok((repaint_after, clipped_primitives))
|
||||
}
|
||||
|
||||
/// Paint the results of the last call to [`Self::logic`].
|
||||
|
@ -288,6 +379,7 @@ impl AppRunner {
|
|||
let textures_delta = std::mem::take(&mut self.textures_delta);
|
||||
|
||||
self.painter.paint_and_update_textures(
|
||||
self.app.clear_color(&self.egui_ctx.style().visuals),
|
||||
clipped_primitives,
|
||||
self.egui_ctx.pixels_per_point(),
|
||||
&textures_delta,
|
||||
|
@ -297,7 +389,7 @@ impl AppRunner {
|
|||
}
|
||||
|
||||
fn handle_platform_output(&mut self, platform_output: egui::PlatformOutput) {
|
||||
if self.egui_ctx.options().screen_reader {
|
||||
if self.egui_ctx.options(|o| o.screen_reader) {
|
||||
self.screen_reader
|
||||
.speak(&platform_output.events_description());
|
||||
}
|
||||
|
@ -309,6 +401,8 @@ impl AppRunner {
|
|||
events: _, // already handled
|
||||
mutable_text_under_cursor,
|
||||
text_cursor_pos,
|
||||
#[cfg(feature = "accesskit")]
|
||||
accesskit_update: _, // not currently implemented
|
||||
} = platform_output;
|
||||
|
||||
set_cursor_icon(cursor_icon);
|
||||
|
@ -337,24 +431,60 @@ impl AppRunner {
|
|||
|
||||
pub type AppRunnerRef = Arc<Mutex<AppRunner>>;
|
||||
|
||||
pub struct TargetEvent {
|
||||
target: EventTarget,
|
||||
event_name: String,
|
||||
closure: Closure<dyn FnMut(web_sys::Event)>,
|
||||
}
|
||||
|
||||
pub struct IntervalHandle {
|
||||
pub handle: i32,
|
||||
pub closure: Closure<dyn FnMut()>,
|
||||
}
|
||||
|
||||
pub enum EventToUnsubscribe {
|
||||
TargetEvent(TargetEvent),
|
||||
#[allow(dead_code)]
|
||||
IntervalHandle(IntervalHandle),
|
||||
}
|
||||
|
||||
impl EventToUnsubscribe {
|
||||
pub fn unsubscribe(self) -> Result<(), JsValue> {
|
||||
match self {
|
||||
EventToUnsubscribe::TargetEvent(handle) => {
|
||||
handle.target.remove_event_listener_with_callback(
|
||||
handle.event_name.as_str(),
|
||||
handle.closure.as_ref().unchecked_ref(),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
EventToUnsubscribe::IntervalHandle(handle) => {
|
||||
let window = web_sys::window().unwrap();
|
||||
window.clear_interval_with_handle(handle.handle);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AppRunnerContainer {
|
||||
pub runner: AppRunnerRef,
|
||||
|
||||
/// Set to `true` if there is a panic.
|
||||
/// Used to ignore callbacks after a panic.
|
||||
pub panicked: Arc<AtomicBool>,
|
||||
pub events: Vec<EventToUnsubscribe>,
|
||||
}
|
||||
|
||||
impl AppRunnerContainer {
|
||||
/// Convenience function to reduce boilerplate and ensure that all event handlers
|
||||
/// are dealt with in the same way
|
||||
pub fn add_event_listener<E: wasm_bindgen::JsCast>(
|
||||
&self,
|
||||
&mut self,
|
||||
target: &EventTarget,
|
||||
event_name: &'static str,
|
||||
mut closure: impl FnMut(E, MutexGuard<'_, AppRunner>) + 'static,
|
||||
) -> Result<(), JsValue> {
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
// Create a JS closure based on the FnMut provided
|
||||
let closure = Closure::wrap({
|
||||
// Clone atomics
|
||||
|
@ -369,14 +499,19 @@ impl AppRunnerContainer {
|
|||
|
||||
closure(event, runner_ref.lock());
|
||||
}
|
||||
}) as Box<dyn FnMut(_)>
|
||||
}) as Box<dyn FnMut(web_sys::Event)>
|
||||
});
|
||||
|
||||
// Add the event listener to the target
|
||||
target.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?;
|
||||
|
||||
// Bypass closure drop so that event handler can call the closure
|
||||
closure.forget();
|
||||
let handle = TargetEvent {
|
||||
target: target.clone(),
|
||||
event_name: event_name.to_owned(),
|
||||
closure,
|
||||
};
|
||||
|
||||
self.events.push(EventToUnsubscribe::TargetEvent(handle));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -386,8 +521,17 @@ impl AppRunnerContainer {
|
|||
|
||||
/// Install event listeners to register different input events
|
||||
/// and start running the given app.
|
||||
pub fn start(canvas_id: &str, app_creator: epi::AppCreator) -> Result<AppRunnerRef, JsValue> {
|
||||
let mut runner = AppRunner::new(canvas_id, app_creator)?;
|
||||
pub async fn start(
|
||||
canvas_id: &str,
|
||||
web_options: crate::WebOptions,
|
||||
app_creator: epi::AppCreator,
|
||||
) -> Result<AppRunnerRef, JsValue> {
|
||||
#[cfg(not(web_sys_unstable_apis))]
|
||||
tracing::warn!(
|
||||
"eframe compiled without RUSTFLAGS='--cfg=web_sys_unstable_apis'. Copying text won't work."
|
||||
);
|
||||
|
||||
let mut runner = AppRunner::new(canvas_id, web_options, app_creator).await?;
|
||||
runner.warm_up()?;
|
||||
start_runner(runner)
|
||||
}
|
||||
|
@ -395,24 +539,26 @@ pub fn start(canvas_id: &str, app_creator: epi::AppCreator) -> Result<AppRunnerR
|
|||
/// Install event listeners to register different input events
|
||||
/// and starts running the given [`AppRunner`].
|
||||
fn start_runner(app_runner: AppRunner) -> Result<AppRunnerRef, JsValue> {
|
||||
let runner_container = AppRunnerContainer {
|
||||
let mut runner_container = AppRunnerContainer {
|
||||
runner: Arc::new(Mutex::new(app_runner)),
|
||||
panicked: Arc::new(AtomicBool::new(false)),
|
||||
events: Vec::with_capacity(20),
|
||||
};
|
||||
|
||||
super::events::install_canvas_events(&runner_container)?;
|
||||
super::events::install_document_events(&runner_container)?;
|
||||
text_agent::install_text_agent(&runner_container)?;
|
||||
super::events::repaint_every_ms(&runner_container, 1000)?; // just in case. TODO: make it a parameter
|
||||
super::events::install_canvas_events(&mut runner_container)?;
|
||||
super::events::install_document_events(&mut runner_container)?;
|
||||
text_agent::install_text_agent(&mut runner_container)?;
|
||||
|
||||
super::events::paint_and_schedule(&runner_container.runner, runner_container.panicked.clone())?;
|
||||
|
||||
// Disable all event handlers on panic
|
||||
let previous_hook = std::panic::take_hook();
|
||||
let panicked = runner_container.panicked;
|
||||
|
||||
runner_container.runner.lock().events_to_unsubscribe = runner_container.events;
|
||||
|
||||
std::panic::set_hook(Box::new(move |panic_info| {
|
||||
tracing::info!("egui disabled all event handlers due to panic");
|
||||
panicked.store(true, SeqCst);
|
||||
runner_container.panicked.store(true, SeqCst);
|
||||
|
||||
// Propagate panic info to the previously registered panic hook
|
||||
previous_hook(panic_info);
|
||||
|
@ -430,8 +576,10 @@ impl epi::Storage for LocalStorage {
|
|||
fn get_string(&self, key: &str) -> Option<String> {
|
||||
local_storage_get(key)
|
||||
}
|
||||
|
||||
fn set_string(&mut self, key: &str, value: String) {
|
||||
local_storage_set(key, &value);
|
||||
}
|
||||
|
||||
fn flush(&mut self) {}
|
||||
}
|
|
@ -1,30 +1,36 @@
|
|||
use super::*;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use egui::Key;
|
||||
|
||||
use super::*;
|
||||
|
||||
struct IsDestroyed(pub bool);
|
||||
|
||||
pub fn paint_and_schedule(
|
||||
runner_ref: &AppRunnerRef,
|
||||
panicked: Arc<AtomicBool>,
|
||||
) -> Result<(), JsValue> {
|
||||
fn paint_if_needed(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
|
||||
fn paint_if_needed(runner_ref: &AppRunnerRef) -> Result<IsDestroyed, JsValue> {
|
||||
let mut runner_lock = runner_ref.lock();
|
||||
if runner_lock.needs_repaint.fetch_and_clear() {
|
||||
runner_lock.clear_color_buffer();
|
||||
let (needs_repaint, clipped_primitives) = runner_lock.logic()?;
|
||||
let is_destroyed = runner_lock.is_destroyed.fetch();
|
||||
|
||||
if !is_destroyed && runner_lock.needs_repaint.when_to_repaint() <= now_sec() {
|
||||
runner_lock.needs_repaint.clear();
|
||||
let (repaint_after, clipped_primitives) = runner_lock.logic()?;
|
||||
runner_lock.paint(&clipped_primitives)?;
|
||||
if needs_repaint {
|
||||
runner_lock.needs_repaint.set_true();
|
||||
}
|
||||
runner_lock
|
||||
.needs_repaint
|
||||
.repaint_after(repaint_after.as_secs_f64());
|
||||
runner_lock.auto_save();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(IsDestroyed(is_destroyed))
|
||||
}
|
||||
|
||||
fn request_animation_frame(
|
||||
runner_ref: AppRunnerRef,
|
||||
panicked: Arc<AtomicBool>,
|
||||
) -> Result<(), JsValue> {
|
||||
use wasm_bindgen::JsCast;
|
||||
let window = web_sys::window().unwrap();
|
||||
let closure = Closure::once(move || paint_and_schedule(&runner_ref, panicked));
|
||||
window.request_animation_frame(closure.as_ref().unchecked_ref())?;
|
||||
|
@ -34,14 +40,16 @@ pub fn paint_and_schedule(
|
|||
|
||||
// Only paint and schedule if there has been no panic
|
||||
if !panicked.load(Ordering::SeqCst) {
|
||||
paint_if_needed(runner_ref)?;
|
||||
request_animation_frame(runner_ref.clone(), panicked)?;
|
||||
let is_destroyed = paint_if_needed(runner_ref)?;
|
||||
if !is_destroyed.0 {
|
||||
request_animation_frame(runner_ref.clone(), panicked)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn install_document_events(runner_container: &AppRunnerContainer) -> Result<(), JsValue> {
|
||||
pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> {
|
||||
let window = web_sys::window().unwrap();
|
||||
let document = window.document().unwrap();
|
||||
|
||||
|
@ -50,7 +58,7 @@ pub fn install_document_events(runner_container: &AppRunnerContainer) -> Result<
|
|||
"keydown",
|
||||
|event: web_sys::KeyboardEvent, mut runner_lock| {
|
||||
if event.is_composing() || event.key_code() == 229 {
|
||||
// https://www.fxsitecompat.dev/en-CA/docs/2018/keydown-and-keyup-events-are-now-fired-during-ime-composition/
|
||||
// https://web.archive.org/web/20200526195704/https://www.fxsitecompat.dev/en-CA/docs/2018/keydown-and-keyup-events-are-now-fired-during-ime-composition/
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -58,11 +66,13 @@ pub fn install_document_events(runner_container: &AppRunnerContainer) -> Result<
|
|||
runner_lock.input.raw.modifiers = modifiers;
|
||||
|
||||
let key = event.key();
|
||||
let egui_key = translate_key(&key);
|
||||
|
||||
if let Some(key) = translate_key(&key) {
|
||||
if let Some(key) = egui_key {
|
||||
runner_lock.input.raw.events.push(egui::Event::Key {
|
||||
key,
|
||||
pressed: true,
|
||||
repeat: false, // egui will fill this in for us!
|
||||
modifiers,
|
||||
});
|
||||
}
|
||||
|
@ -74,14 +84,22 @@ pub fn install_document_events(runner_container: &AppRunnerContainer) -> Result<
|
|||
{
|
||||
runner_lock.input.raw.events.push(egui::Event::Text(key));
|
||||
}
|
||||
runner_lock.needs_repaint.set_true();
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
|
||||
let egui_wants_keyboard = runner_lock.egui_ctx().wants_keyboard_input();
|
||||
|
||||
let prevent_default = if matches!(event.key().as_str(), "Tab") {
|
||||
#[allow(clippy::if_same_then_else)]
|
||||
let prevent_default = if egui_key == Some(Key::Tab) {
|
||||
// Always prevent moving cursor to url bar.
|
||||
// egui wants to use tab to move to the next text field.
|
||||
true
|
||||
} else if egui_key == Some(Key::P) {
|
||||
#[allow(clippy::needless_bool)]
|
||||
if modifiers.ctrl || modifiers.command || modifiers.mac_cmd {
|
||||
true // Prevent ctrl-P opening the print dialog. Users may want to use it for a command palette.
|
||||
} else {
|
||||
false // let normal P:s through
|
||||
}
|
||||
} else if egui_wants_keyboard {
|
||||
matches!(
|
||||
event.key().as_str(),
|
||||
|
@ -105,6 +123,7 @@ pub fn install_document_events(runner_container: &AppRunnerContainer) -> Result<
|
|||
|
||||
if prevent_default {
|
||||
event.prevent_default();
|
||||
// event.stop_propagation();
|
||||
}
|
||||
},
|
||||
)?;
|
||||
|
@ -119,10 +138,11 @@ pub fn install_document_events(runner_container: &AppRunnerContainer) -> Result<
|
|||
runner_lock.input.raw.events.push(egui::Event::Key {
|
||||
key,
|
||||
pressed: false,
|
||||
repeat: false,
|
||||
modifiers,
|
||||
});
|
||||
}
|
||||
runner_lock.needs_repaint.set_true();
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
},
|
||||
)?;
|
||||
|
||||
|
@ -136,7 +156,7 @@ pub fn install_document_events(runner_container: &AppRunnerContainer) -> Result<
|
|||
let text = text.replace("\r\n", "\n");
|
||||
if !text.is_empty() {
|
||||
runner_lock.input.raw.events.push(egui::Event::Paste(text));
|
||||
runner_lock.needs_repaint.set_true();
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
}
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
|
@ -151,7 +171,7 @@ pub fn install_document_events(runner_container: &AppRunnerContainer) -> Result<
|
|||
"cut",
|
||||
|_: web_sys::ClipboardEvent, mut runner_lock| {
|
||||
runner_lock.input.raw.events.push(egui::Event::Cut);
|
||||
runner_lock.needs_repaint.set_true();
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
},
|
||||
)?;
|
||||
|
||||
|
@ -161,7 +181,7 @@ pub fn install_document_events(runner_container: &AppRunnerContainer) -> Result<
|
|||
"copy",
|
||||
|_: web_sys::ClipboardEvent, mut runner_lock| {
|
||||
runner_lock.input.raw.events.push(egui::Event::Copy);
|
||||
runner_lock.needs_repaint.set_true();
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
},
|
||||
)?;
|
||||
|
||||
|
@ -170,7 +190,7 @@ pub fn install_document_events(runner_container: &AppRunnerContainer) -> Result<
|
|||
&window,
|
||||
event_name,
|
||||
|_: web_sys::Event, runner_lock| {
|
||||
runner_lock.needs_repaint.set_true();
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
},
|
||||
)?;
|
||||
}
|
||||
|
@ -180,66 +200,40 @@ pub fn install_document_events(runner_container: &AppRunnerContainer) -> Result<
|
|||
"hashchange",
|
||||
|_: web_sys::Event, mut runner_lock| {
|
||||
// `epi::Frame::info(&self)` clones `epi::IntegrationInfo`, but we need to modify the original here
|
||||
if let Some(web_info) = &mut runner_lock.frame.info.web_info {
|
||||
web_info.location.hash = location_hash();
|
||||
}
|
||||
runner_lock.frame.info.web_info.location.hash = location_hash();
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Repaint at least every `ms` milliseconds.
|
||||
pub fn repaint_every_ms(
|
||||
runner_container: &AppRunnerContainer,
|
||||
milliseconds: i32,
|
||||
) -> Result<(), JsValue> {
|
||||
assert!(milliseconds >= 0);
|
||||
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
let window = web_sys::window().unwrap();
|
||||
|
||||
let closure = Closure::wrap(Box::new({
|
||||
let runner = runner_container.runner.clone();
|
||||
let panicked = runner_container.panicked.clone();
|
||||
|
||||
move || {
|
||||
// Do not lock the runner if the code has panicked
|
||||
if !panicked.load(Ordering::SeqCst) {
|
||||
runner.lock().needs_repaint.set_true();
|
||||
}
|
||||
}
|
||||
}) as Box<dyn FnMut()>);
|
||||
|
||||
window.set_interval_with_callback_and_timeout_and_arguments_0(
|
||||
closure.as_ref().unchecked_ref(),
|
||||
milliseconds,
|
||||
)?;
|
||||
|
||||
closure.forget();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn install_canvas_events(runner_container: &AppRunnerContainer) -> Result<(), JsValue> {
|
||||
use wasm_bindgen::JsCast;
|
||||
pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> {
|
||||
let canvas = canvas_element(runner_container.runner.lock().canvas_id()).unwrap();
|
||||
|
||||
{
|
||||
let prevent_default_events = [
|
||||
// By default, right-clicks open a context menu.
|
||||
// We don't want to do that (right clicks is handled by egui):
|
||||
let event_name = "contextmenu";
|
||||
let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
|
||||
event.prevent_default();
|
||||
}) as Box<dyn FnMut(_)>);
|
||||
canvas.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?;
|
||||
closure.forget();
|
||||
"contextmenu",
|
||||
// Allow users to use ctrl-p for e.g. a command palette
|
||||
"afterprint",
|
||||
];
|
||||
|
||||
for event_name in prevent_default_events {
|
||||
let closure =
|
||||
move |event: web_sys::MouseEvent,
|
||||
mut _runner_lock: egui::mutex::MutexGuard<'_, AppRunner>| {
|
||||
event.prevent_default();
|
||||
// event.stop_propagation();
|
||||
// tracing::debug!("Preventing event {:?}", event_name);
|
||||
};
|
||||
|
||||
runner_container.add_event_listener(&canvas, event_name, closure)?;
|
||||
}
|
||||
|
||||
runner_container.add_event_listener(
|
||||
&canvas,
|
||||
"mousedown",
|
||||
|event: web_sys::MouseEvent, mut runner_lock| {
|
||||
|event: web_sys::MouseEvent, mut runner_lock: egui::mutex::MutexGuard<'_, AppRunner>| {
|
||||
if let Some(button) = button_from_mouse_event(&event) {
|
||||
let pos = pos_from_mouse_event(runner_lock.canvas_id(), &event);
|
||||
let modifiers = runner_lock.input.raw.modifiers;
|
||||
|
@ -253,7 +247,7 @@ pub fn install_canvas_events(runner_container: &AppRunnerContainer) -> Result<()
|
|||
pressed: true,
|
||||
modifiers,
|
||||
});
|
||||
runner_lock.needs_repaint.set_true();
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
}
|
||||
event.stop_propagation();
|
||||
// Note: prevent_default breaks VSCode tab focusing, hence why we don't call it here.
|
||||
|
@ -270,7 +264,7 @@ pub fn install_canvas_events(runner_container: &AppRunnerContainer) -> Result<()
|
|||
.raw
|
||||
.events
|
||||
.push(egui::Event::PointerMoved(pos));
|
||||
runner_lock.needs_repaint.set_true();
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
},
|
||||
|
@ -293,7 +287,7 @@ pub fn install_canvas_events(runner_container: &AppRunnerContainer) -> Result<()
|
|||
pressed: false,
|
||||
modifiers,
|
||||
});
|
||||
runner_lock.needs_repaint.set_true();
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
|
||||
text_agent::update_text_agent(runner_lock);
|
||||
}
|
||||
|
@ -307,7 +301,7 @@ pub fn install_canvas_events(runner_container: &AppRunnerContainer) -> Result<()
|
|||
"mouseleave",
|
||||
|event: web_sys::MouseEvent, mut runner_lock| {
|
||||
runner_lock.input.raw.events.push(egui::Event::PointerGone);
|
||||
runner_lock.needs_repaint.set_true();
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
},
|
||||
|
@ -334,8 +328,8 @@ pub fn install_canvas_events(runner_container: &AppRunnerContainer) -> Result<()
|
|||
modifiers,
|
||||
});
|
||||
|
||||
push_touches(&mut *runner_lock, egui::TouchPhase::Start, &event);
|
||||
runner_lock.needs_repaint.set_true();
|
||||
push_touches(&mut runner_lock, egui::TouchPhase::Start, &event);
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
},
|
||||
|
@ -356,8 +350,8 @@ pub fn install_canvas_events(runner_container: &AppRunnerContainer) -> Result<()
|
|||
.events
|
||||
.push(egui::Event::PointerMoved(pos));
|
||||
|
||||
push_touches(&mut *runner_lock, egui::TouchPhase::Move, &event);
|
||||
runner_lock.needs_repaint.set_true();
|
||||
push_touches(&mut runner_lock, egui::TouchPhase::Move, &event);
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
},
|
||||
|
@ -383,8 +377,8 @@ pub fn install_canvas_events(runner_container: &AppRunnerContainer) -> Result<()
|
|||
// Then remove hover effect:
|
||||
runner_lock.input.raw.events.push(egui::Event::PointerGone);
|
||||
|
||||
push_touches(&mut *runner_lock, egui::TouchPhase::End, &event);
|
||||
runner_lock.needs_repaint.set_true();
|
||||
push_touches(&mut runner_lock, egui::TouchPhase::End, &event);
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
}
|
||||
|
@ -414,7 +408,7 @@ pub fn install_canvas_events(runner_container: &AppRunnerContainer) -> Result<()
|
|||
}
|
||||
web_sys::WheelEvent::DOM_DELTA_LINE => {
|
||||
#[allow(clippy::let_and_return)]
|
||||
let points_per_scroll_line = 8.0; // Note that this is intentionally different from what we use in egui_glium / winit.
|
||||
let points_per_scroll_line = 8.0; // Note that this is intentionally different from what we use in winit.
|
||||
points_per_scroll_line
|
||||
}
|
||||
_ => 1.0, // DOM_DELTA_PIXEL
|
||||
|
@ -443,7 +437,7 @@ pub fn install_canvas_events(runner_container: &AppRunnerContainer) -> Result<()
|
|||
.push(egui::Event::Scroll(delta));
|
||||
}
|
||||
|
||||
runner_lock.needs_repaint.set_true();
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
},
|
||||
|
@ -463,7 +457,7 @@ pub fn install_canvas_events(runner_container: &AppRunnerContainer) -> Result<()
|
|||
});
|
||||
}
|
||||
}
|
||||
runner_lock.needs_repaint.set_true();
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
}
|
||||
|
@ -475,7 +469,7 @@ pub fn install_canvas_events(runner_container: &AppRunnerContainer) -> Result<()
|
|||
"dragleave",
|
||||
|event: web_sys::DragEvent, mut runner_lock| {
|
||||
runner_lock.input.raw.hovered_files.clear();
|
||||
runner_lock.needs_repaint.set_true();
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
},
|
||||
|
@ -487,7 +481,7 @@ pub fn install_canvas_events(runner_container: &AppRunnerContainer) -> Result<()
|
|||
move |event: web_sys::DragEvent, mut runner_lock| {
|
||||
if let Some(data_transfer) = event.data_transfer() {
|
||||
runner_lock.input.raw.hovered_files.clear();
|
||||
runner_lock.needs_repaint.set_true();
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
// Unlock the runner so it can be locked after a future await point
|
||||
drop(runner_lock);
|
||||
|
||||
|
@ -523,7 +517,7 @@ pub fn install_canvas_events(runner_container: &AppRunnerContainer) -> Result<()
|
|||
..Default::default()
|
||||
},
|
||||
);
|
||||
runner_lock.needs_repaint.set_true();
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("Failed to read file: {:?}", err);
|
|
@ -14,6 +14,8 @@ pub fn button_from_mouse_event(event: &web_sys::MouseEvent) -> Option<egui::Poin
|
|||
0 => Some(egui::PointerButton::Primary),
|
||||
1 => Some(egui::PointerButton::Middle),
|
||||
2 => Some(egui::PointerButton::Secondary),
|
||||
3 => Some(egui::PointerButton::Extra1),
|
||||
4 => Some(egui::PointerButton::Extra2),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
@ -52,8 +54,8 @@ pub fn pos_from_touch_event(
|
|||
|
||||
fn pos_from_touch(canvas_origin: egui::Pos2, touch: &web_sys::Touch) -> egui::Pos2 {
|
||||
egui::Pos2 {
|
||||
x: touch.page_x() as f32 - canvas_origin.x as f32,
|
||||
y: touch.page_y() as f32 - canvas_origin.y as f32,
|
||||
x: touch.page_x() as f32 - canvas_origin.x,
|
||||
y: touch.page_y() as f32 - canvas_origin.y,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -111,62 +113,88 @@ pub fn should_ignore_key(key: &str) -> bool {
|
|||
/// Web sends all all keys as strings, so it is up to us to figure out if it is
|
||||
/// a real text input or the name of a key.
|
||||
pub fn translate_key(key: &str) -> Option<egui::Key> {
|
||||
use egui::Key;
|
||||
|
||||
match key {
|
||||
"ArrowDown" => Some(egui::Key::ArrowDown),
|
||||
"ArrowLeft" => Some(egui::Key::ArrowLeft),
|
||||
"ArrowRight" => Some(egui::Key::ArrowRight),
|
||||
"ArrowUp" => Some(egui::Key::ArrowUp),
|
||||
"ArrowDown" => Some(Key::ArrowDown),
|
||||
"ArrowLeft" => Some(Key::ArrowLeft),
|
||||
"ArrowRight" => Some(Key::ArrowRight),
|
||||
"ArrowUp" => Some(Key::ArrowUp),
|
||||
|
||||
"Esc" | "Escape" => Some(egui::Key::Escape),
|
||||
"Tab" => Some(egui::Key::Tab),
|
||||
"Backspace" => Some(egui::Key::Backspace),
|
||||
"Enter" => Some(egui::Key::Enter),
|
||||
"Space" | " " => Some(egui::Key::Space),
|
||||
"Esc" | "Escape" => Some(Key::Escape),
|
||||
"Tab" => Some(Key::Tab),
|
||||
"Backspace" => Some(Key::Backspace),
|
||||
"Enter" => Some(Key::Enter),
|
||||
"Space" | " " => Some(Key::Space),
|
||||
|
||||
"Help" | "Insert" => Some(egui::Key::Insert),
|
||||
"Delete" => Some(egui::Key::Delete),
|
||||
"Home" => Some(egui::Key::Home),
|
||||
"End" => Some(egui::Key::End),
|
||||
"PageUp" => Some(egui::Key::PageUp),
|
||||
"PageDown" => Some(egui::Key::PageDown),
|
||||
"Help" | "Insert" => Some(Key::Insert),
|
||||
"Delete" => Some(Key::Delete),
|
||||
"Home" => Some(Key::Home),
|
||||
"End" => Some(Key::End),
|
||||
"PageUp" => Some(Key::PageUp),
|
||||
"PageDown" => Some(Key::PageDown),
|
||||
|
||||
"0" => Some(egui::Key::Num0),
|
||||
"1" => Some(egui::Key::Num1),
|
||||
"2" => Some(egui::Key::Num2),
|
||||
"3" => Some(egui::Key::Num3),
|
||||
"4" => Some(egui::Key::Num4),
|
||||
"5" => Some(egui::Key::Num5),
|
||||
"6" => Some(egui::Key::Num6),
|
||||
"7" => Some(egui::Key::Num7),
|
||||
"8" => Some(egui::Key::Num8),
|
||||
"9" => Some(egui::Key::Num9),
|
||||
"-" => Some(Key::Minus),
|
||||
"+" | "=" => Some(Key::PlusEquals),
|
||||
|
||||
"a" | "A" => Some(egui::Key::A),
|
||||
"b" | "B" => Some(egui::Key::B),
|
||||
"c" | "C" => Some(egui::Key::C),
|
||||
"d" | "D" => Some(egui::Key::D),
|
||||
"e" | "E" => Some(egui::Key::E),
|
||||
"f" | "F" => Some(egui::Key::F),
|
||||
"g" | "G" => Some(egui::Key::G),
|
||||
"h" | "H" => Some(egui::Key::H),
|
||||
"i" | "I" => Some(egui::Key::I),
|
||||
"j" | "J" => Some(egui::Key::J),
|
||||
"k" | "K" => Some(egui::Key::K),
|
||||
"l" | "L" => Some(egui::Key::L),
|
||||
"m" | "M" => Some(egui::Key::M),
|
||||
"n" | "N" => Some(egui::Key::N),
|
||||
"o" | "O" => Some(egui::Key::O),
|
||||
"p" | "P" => Some(egui::Key::P),
|
||||
"q" | "Q" => Some(egui::Key::Q),
|
||||
"r" | "R" => Some(egui::Key::R),
|
||||
"s" | "S" => Some(egui::Key::S),
|
||||
"t" | "T" => Some(egui::Key::T),
|
||||
"u" | "U" => Some(egui::Key::U),
|
||||
"v" | "V" => Some(egui::Key::V),
|
||||
"w" | "W" => Some(egui::Key::W),
|
||||
"x" | "X" => Some(egui::Key::X),
|
||||
"y" | "Y" => Some(egui::Key::Y),
|
||||
"z" | "Z" => Some(egui::Key::Z),
|
||||
"0" => Some(Key::Num0),
|
||||
"1" => Some(Key::Num1),
|
||||
"2" => Some(Key::Num2),
|
||||
"3" => Some(Key::Num3),
|
||||
"4" => Some(Key::Num4),
|
||||
"5" => Some(Key::Num5),
|
||||
"6" => Some(Key::Num6),
|
||||
"7" => Some(Key::Num7),
|
||||
"8" => Some(Key::Num8),
|
||||
"9" => Some(Key::Num9),
|
||||
|
||||
"a" | "A" => Some(Key::A),
|
||||
"b" | "B" => Some(Key::B),
|
||||
"c" | "C" => Some(Key::C),
|
||||
"d" | "D" => Some(Key::D),
|
||||
"e" | "E" => Some(Key::E),
|
||||
"f" | "F" => Some(Key::F),
|
||||
"g" | "G" => Some(Key::G),
|
||||
"h" | "H" => Some(Key::H),
|
||||
"i" | "I" => Some(Key::I),
|
||||
"j" | "J" => Some(Key::J),
|
||||
"k" | "K" => Some(Key::K),
|
||||
"l" | "L" => Some(Key::L),
|
||||
"m" | "M" => Some(Key::M),
|
||||
"n" | "N" => Some(Key::N),
|
||||
"o" | "O" => Some(Key::O),
|
||||
"p" | "P" => Some(Key::P),
|
||||
"q" | "Q" => Some(Key::Q),
|
||||
"r" | "R" => Some(Key::R),
|
||||
"s" | "S" => Some(Key::S),
|
||||
"t" | "T" => Some(Key::T),
|
||||
"u" | "U" => Some(Key::U),
|
||||
"v" | "V" => Some(Key::V),
|
||||
"w" | "W" => Some(Key::W),
|
||||
"x" | "X" => Some(Key::X),
|
||||
"y" | "Y" => Some(Key::Y),
|
||||
"z" | "Z" => Some(Key::Z),
|
||||
|
||||
"F1" => Some(Key::F1),
|
||||
"F2" => Some(Key::F2),
|
||||
"F3" => Some(Key::F3),
|
||||
"F4" => Some(Key::F4),
|
||||
"F5" => Some(Key::F5),
|
||||
"F6" => Some(Key::F6),
|
||||
"F7" => Some(Key::F7),
|
||||
"F8" => Some(Key::F8),
|
||||
"F9" => Some(Key::F9),
|
||||
"F10" => Some(Key::F10),
|
||||
"F11" => Some(Key::F11),
|
||||
"F12" => Some(Key::F12),
|
||||
"F13" => Some(Key::F13),
|
||||
"F14" => Some(Key::F14),
|
||||
"F15" => Some(Key::F15),
|
||||
"F16" => Some(Key::F16),
|
||||
"F17" => Some(Key::F17),
|
||||
"F18" => Some(Key::F18),
|
||||
"F19" => Some(Key::F19),
|
||||
"F20" => Some(Key::F20),
|
||||
|
||||
_ => None,
|
||||
}
|
|
@ -4,12 +4,26 @@
|
|||
|
||||
pub mod backend;
|
||||
mod events;
|
||||
mod glow_wrapping;
|
||||
mod input;
|
||||
pub mod screen_reader;
|
||||
pub mod storage;
|
||||
mod text_agent;
|
||||
|
||||
#[cfg(not(any(feature = "glow", feature = "wgpu")))]
|
||||
compile_error!("You must enable either the 'glow' or 'wgpu' feature");
|
||||
|
||||
mod web_painter;
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
mod web_painter_glow;
|
||||
#[cfg(feature = "glow")]
|
||||
pub(crate) type ActiveWebPainter = web_painter_glow::WebPainterGlow;
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
mod web_painter_wgpu;
|
||||
#[cfg(all(feature = "wgpu", not(feature = "glow")))]
|
||||
pub(crate) type ActiveWebPainter = web_painter_wgpu::WebPainterWgpu;
|
||||
|
||||
pub use backend::*;
|
||||
pub use events::*;
|
||||
pub use storage::*;
|
||||
|
@ -20,15 +34,19 @@ use std::sync::{
|
|||
Arc,
|
||||
};
|
||||
|
||||
use egui::mutex::{Mutex, MutexGuard};
|
||||
use egui::Vec2;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use web_sys::EventTarget;
|
||||
|
||||
use input::*;
|
||||
|
||||
use crate::Theme;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Current time in seconds (since undefined point in time)
|
||||
/// Current time in seconds (since undefined point in time).
|
||||
///
|
||||
/// Monotonically increasing.
|
||||
pub fn now_sec() -> f64 {
|
||||
web_sys::window()
|
||||
.expect("should have a Window")
|
||||
|
@ -38,6 +56,7 @@ pub fn now_sec() -> f64 {
|
|||
/ 1000.0
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn screen_size_in_native_points() -> Option<egui::Vec2> {
|
||||
let window = web_sys::window()?;
|
||||
Some(egui::vec2(
|
||||
|
@ -55,17 +74,15 @@ pub fn native_pixels_per_point() -> f32 {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn prefer_dark_mode() -> Option<bool> {
|
||||
Some(
|
||||
web_sys::window()?
|
||||
.match_media("(prefers-color-scheme: dark)")
|
||||
.ok()??
|
||||
.matches(),
|
||||
)
|
||||
pub fn system_theme() -> Option<Theme> {
|
||||
let dark_mode = web_sys::window()?
|
||||
.match_media("(prefers-color-scheme: dark)")
|
||||
.ok()??
|
||||
.matches();
|
||||
Some(if dark_mode { Theme::Dark } else { Theme::Light })
|
||||
}
|
||||
|
||||
pub fn canvas_element(canvas_id: &str) -> Option<web_sys::HtmlCanvasElement> {
|
||||
use wasm_bindgen::JsCast;
|
||||
let document = web_sys::window()?.document()?;
|
||||
let canvas = document.get_element_by_id(canvas_id)?;
|
||||
canvas.dyn_into::<web_sys::HtmlCanvasElement>().ok()
|
||||
|
@ -73,14 +90,14 @@ pub fn canvas_element(canvas_id: &str) -> Option<web_sys::HtmlCanvasElement> {
|
|||
|
||||
pub fn canvas_element_or_die(canvas_id: &str) -> web_sys::HtmlCanvasElement {
|
||||
canvas_element(canvas_id)
|
||||
.unwrap_or_else(|| panic!("Failed to find canvas with id '{}'", canvas_id))
|
||||
.unwrap_or_else(|| panic!("Failed to find canvas with id {:?}", canvas_id))
|
||||
}
|
||||
|
||||
fn canvas_origin(canvas_id: &str) -> egui::Pos2 {
|
||||
let rect = canvas_element(canvas_id)
|
||||
.unwrap()
|
||||
.get_bounding_client_rect();
|
||||
egui::Pos2::new(rect.left() as f32, rect.top() as f32)
|
||||
egui::pos2(rect.left() as f32, rect.top() as f32)
|
||||
}
|
||||
|
||||
pub fn canvas_size_in_points(canvas_id: &str) -> egui::Vec2 {
|
||||
|
@ -94,13 +111,25 @@ pub fn canvas_size_in_points(canvas_id: &str) -> egui::Vec2 {
|
|||
|
||||
pub fn resize_canvas_to_screen_size(canvas_id: &str, max_size_points: egui::Vec2) -> Option<()> {
|
||||
let canvas = canvas_element(canvas_id)?;
|
||||
let parent = canvas.parent_element()?;
|
||||
|
||||
let width = parent.scroll_width();
|
||||
let height = parent.scroll_height();
|
||||
|
||||
let canvas_real_size = Vec2 {
|
||||
x: width as f32,
|
||||
y: height as f32,
|
||||
};
|
||||
|
||||
if width <= 0 || height <= 0 {
|
||||
tracing::error!("egui canvas parent size is {}x{}. Try adding `html, body {{ height: 100%; width: 100% }}` to your CSS!", width, height);
|
||||
}
|
||||
|
||||
let screen_size_points = screen_size_in_native_points()?;
|
||||
let pixels_per_point = native_pixels_per_point();
|
||||
|
||||
let max_size_pixels = pixels_per_point * max_size_points;
|
||||
|
||||
let canvas_size_pixels = pixels_per_point * screen_size_points;
|
||||
let canvas_size_pixels = pixels_per_point * canvas_real_size;
|
||||
let canvas_size_pixels = canvas_size_pixels.min(max_size_pixels);
|
||||
let canvas_size_points = canvas_size_pixels / pixels_per_point;
|
||||
|
||||
|
@ -227,47 +256,3 @@ pub fn percent_decode(s: &str) -> String {
|
|||
.decode_utf8_lossy()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
pub(crate) fn webgl1_requires_brightening(gl: &web_sys::WebGlRenderingContext) -> bool {
|
||||
// See https://github.com/emilk/egui/issues/794
|
||||
|
||||
// detect WebKitGTK
|
||||
|
||||
// WebKitGTK use WebKit default unmasked vendor and renderer
|
||||
// but safari use same vendor and renderer
|
||||
// so exclude "Mac OS X" user-agent.
|
||||
let user_agent = web_sys::window().unwrap().navigator().user_agent().unwrap();
|
||||
!user_agent.contains("Mac OS X") && is_safari_and_webkit_gtk(gl)
|
||||
}
|
||||
|
||||
/// detecting Safari and `webkitGTK`.
|
||||
///
|
||||
/// Safari and `webkitGTK` use unmasked renderer :Apple GPU
|
||||
///
|
||||
/// If we detect safari or `webkitGTKs` returns true.
|
||||
///
|
||||
/// This function used to avoid displaying linear color with `sRGB` supported systems.
|
||||
fn is_safari_and_webkit_gtk(gl: &web_sys::WebGlRenderingContext) -> bool {
|
||||
// This call produces a warning in Firefox ("WEBGL_debug_renderer_info is deprecated in Firefox and will be removed.")
|
||||
// but unless we call it we get errors in Chrome when we call `get_parameter` below.
|
||||
// TODO: do something smart based on user agent?
|
||||
if gl
|
||||
.get_extension("WEBGL_debug_renderer_info")
|
||||
.unwrap()
|
||||
.is_some()
|
||||
{
|
||||
if let Ok(renderer) =
|
||||
gl.get_parameter(web_sys::WebglDebugRendererInfo::UNMASKED_RENDERER_WEBGL)
|
||||
{
|
||||
if let Some(renderer) = renderer.as_string() {
|
||||
if renderer.contains("Apple") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
pub struct ScreenReader {
|
||||
#[cfg(feature = "screen_reader")]
|
||||
#[cfg(feature = "tts")]
|
||||
tts: Option<tts::Tts>,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "screen_reader"))]
|
||||
#[cfg(not(feature = "tts"))]
|
||||
#[allow(clippy::derivable_impls)] // False positive
|
||||
impl Default for ScreenReader {
|
||||
fn default() -> Self {
|
||||
|
@ -11,7 +11,7 @@ impl Default for ScreenReader {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "screen_reader")]
|
||||
#[cfg(feature = "tts")]
|
||||
impl Default for ScreenReader {
|
||||
fn default() -> Self {
|
||||
let tts = match tts::Tts::default() {
|
||||
|
@ -29,11 +29,11 @@ impl Default for ScreenReader {
|
|||
}
|
||||
|
||||
impl ScreenReader {
|
||||
#[cfg(not(feature = "screen_reader"))]
|
||||
#[cfg(not(feature = "tts"))]
|
||||
#[allow(clippy::unused_self)]
|
||||
pub fn speak(&mut self, _text: &str) {}
|
||||
|
||||
#[cfg(feature = "screen_reader")]
|
||||
#[cfg(feature = "tts")]
|
||||
pub fn speak(&mut self, text: &str) {
|
||||
if text.is_empty() {
|
||||
return;
|
|
@ -15,7 +15,7 @@ pub fn load_memory(ctx: &egui::Context) {
|
|||
if let Some(memory_string) = local_storage_get("egui_memory_ron") {
|
||||
match ron::from_str(&memory_string) {
|
||||
Ok(memory) => {
|
||||
*ctx.memory() = memory;
|
||||
ctx.memory_mut(|m| *m = memory);
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("Failed to parse memory RON: {}", err);
|
||||
|
@ -29,7 +29,7 @@ pub fn load_memory(_: &egui::Context) {}
|
|||
|
||||
#[cfg(feature = "persistence")]
|
||||
pub fn save_memory(ctx: &egui::Context) {
|
||||
match ron::to_string(&*ctx.memory()) {
|
||||
match ctx.memory(|mem| ron::to_string(mem)) {
|
||||
Ok(ron) => {
|
||||
local_storage_set("egui_memory_ron", &ron);
|
||||
}
|
|
@ -10,7 +10,6 @@ use wasm_bindgen::prelude::*;
|
|||
static AGENT_ID: &str = "egui_text_agent";
|
||||
|
||||
pub fn text_agent() -> web_sys::HtmlInputElement {
|
||||
use wasm_bindgen::JsCast;
|
||||
web_sys::window()
|
||||
.unwrap()
|
||||
.document()
|
||||
|
@ -22,8 +21,7 @@ pub fn text_agent() -> web_sys::HtmlInputElement {
|
|||
}
|
||||
|
||||
/// Text event handler,
|
||||
pub fn install_text_agent(runner_container: &AppRunnerContainer) -> Result<(), JsValue> {
|
||||
use wasm_bindgen::JsCast;
|
||||
pub fn install_text_agent(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> {
|
||||
let window = web_sys::window().unwrap();
|
||||
let document = window.document().unwrap();
|
||||
let body = document.body().expect("document should have a body");
|
||||
|
@ -55,7 +53,7 @@ pub fn install_text_agent(runner_container: &AppRunnerContainer) -> Result<(), J
|
|||
if !text.is_empty() && !is_composing.get() {
|
||||
input_clone.set_value("");
|
||||
runner_lock.input.raw.events.push(egui::Event::Text(text));
|
||||
runner_lock.needs_repaint.set_true();
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
@ -75,7 +73,7 @@ pub fn install_text_agent(runner_container: &AppRunnerContainer) -> Result<(), J
|
|||
.raw
|
||||
.events
|
||||
.push(egui::Event::CompositionStart);
|
||||
runner_lock.needs_repaint.set_true();
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
}
|
||||
})?;
|
||||
|
||||
|
@ -85,7 +83,7 @@ pub fn install_text_agent(runner_container: &AppRunnerContainer) -> Result<(), J
|
|||
move |event: web_sys::CompositionEvent, mut runner_lock: MutexGuard<'_, AppRunner>| {
|
||||
if let Some(event) = event.data().map(egui::Event::CompositionUpdate) {
|
||||
runner_lock.input.raw.events.push(event);
|
||||
runner_lock.needs_repaint.set_true();
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
}
|
||||
},
|
||||
)?;
|
||||
|
@ -99,7 +97,7 @@ pub fn install_text_agent(runner_container: &AppRunnerContainer) -> Result<(), J
|
|||
|
||||
if let Some(event) = event.data().map(egui::Event::CompositionEnd) {
|
||||
runner_lock.input.raw.events.push(event);
|
||||
runner_lock.needs_repaint.set_true();
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
@ -129,7 +127,6 @@ pub fn install_text_agent(runner_container: &AppRunnerContainer) -> Result<(), J
|
|||
|
||||
/// Focus or blur text agent to toggle mobile keyboard.
|
||||
pub fn update_text_agent(runner: MutexGuard<'_, AppRunner>) -> Option<()> {
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::HtmlInputElement;
|
||||
let window = web_sys::window()?;
|
||||
let document = window.document()?;
|
29
crates/eframe/src/web/web_painter.rs
Normal file
29
crates/eframe/src/web/web_painter.rs
Normal file
|
@ -0,0 +1,29 @@
|
|||
use wasm_bindgen::JsValue;
|
||||
|
||||
/// Renderer for a browser canvas.
|
||||
/// As of writing we're not allowing to decide on the painter at runtime,
|
||||
/// therefore this trait is merely there for specifying and documenting the interface.
|
||||
pub(crate) trait WebPainter {
|
||||
// Create a new web painter targeting a given canvas.
|
||||
// fn new(canvas_id: &str, options: &WebOptions) -> Result<Self, String>
|
||||
// where
|
||||
// Self: Sized;
|
||||
|
||||
/// Id of the canvas in use.
|
||||
fn canvas_id(&self) -> &str;
|
||||
|
||||
/// Maximum size of a texture in one direction.
|
||||
fn max_texture_side(&self) -> usize;
|
||||
|
||||
/// Update all internal textures and paint gui.
|
||||
fn paint_and_update_textures(
|
||||
&mut self,
|
||||
clear_color: [f32; 4],
|
||||
clipped_primitives: &[egui::ClippedPrimitive],
|
||||
pixels_per_point: f32,
|
||||
textures_delta: &egui::TexturesDelta,
|
||||
) -> Result<(), JsValue>;
|
||||
|
||||
/// Destroy all resources.
|
||||
fn destroy(&mut self);
|
||||
}
|
184
crates/eframe/src/web/web_painter_glow.rs
Normal file
184
crates/eframe/src/web/web_painter_glow.rs
Normal file
|
@ -0,0 +1,184 @@
|
|||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen::JsValue;
|
||||
use web_sys::HtmlCanvasElement;
|
||||
|
||||
use egui_glow::glow;
|
||||
|
||||
use crate::{WebGlContextOption, WebOptions};
|
||||
|
||||
use super::web_painter::WebPainter;
|
||||
|
||||
pub(crate) struct WebPainterGlow {
|
||||
canvas: HtmlCanvasElement,
|
||||
canvas_id: String,
|
||||
painter: egui_glow::Painter,
|
||||
}
|
||||
|
||||
impl WebPainterGlow {
|
||||
pub fn gl(&self) -> &std::sync::Arc<glow::Context> {
|
||||
self.painter.gl()
|
||||
}
|
||||
|
||||
pub async fn new(canvas_id: &str, options: &WebOptions) -> Result<Self, String> {
|
||||
let canvas = super::canvas_element_or_die(canvas_id);
|
||||
|
||||
let (gl, shader_prefix) =
|
||||
init_glow_context_from_canvas(&canvas, options.webgl_context_option)?;
|
||||
let gl = std::sync::Arc::new(gl);
|
||||
|
||||
let painter = egui_glow::Painter::new(gl, shader_prefix, None)
|
||||
.map_err(|error| format!("Error starting glow painter: {}", error))?;
|
||||
|
||||
Ok(Self {
|
||||
canvas,
|
||||
canvas_id: canvas_id.to_owned(),
|
||||
painter,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl WebPainter for WebPainterGlow {
|
||||
fn max_texture_side(&self) -> usize {
|
||||
self.painter.max_texture_side()
|
||||
}
|
||||
|
||||
fn canvas_id(&self) -> &str {
|
||||
&self.canvas_id
|
||||
}
|
||||
|
||||
fn paint_and_update_textures(
|
||||
&mut self,
|
||||
clear_color: [f32; 4],
|
||||
clipped_primitives: &[egui::ClippedPrimitive],
|
||||
pixels_per_point: f32,
|
||||
textures_delta: &egui::TexturesDelta,
|
||||
) -> Result<(), JsValue> {
|
||||
let canvas_dimension = [self.canvas.width(), self.canvas.height()];
|
||||
|
||||
for (id, image_delta) in &textures_delta.set {
|
||||
self.painter.set_texture(*id, image_delta);
|
||||
}
|
||||
|
||||
egui_glow::painter::clear(self.painter.gl(), canvas_dimension, clear_color);
|
||||
self.painter
|
||||
.paint_primitives(canvas_dimension, pixels_per_point, clipped_primitives);
|
||||
|
||||
for &id in &textures_delta.free {
|
||||
self.painter.free_texture(id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn destroy(&mut self) {
|
||||
self.painter.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns glow context and shader prefix.
|
||||
fn init_glow_context_from_canvas(
|
||||
canvas: &HtmlCanvasElement,
|
||||
options: WebGlContextOption,
|
||||
) -> Result<(glow::Context, &'static str), String> {
|
||||
let result = match options {
|
||||
// Force use WebGl1
|
||||
WebGlContextOption::WebGl1 => init_webgl1(canvas),
|
||||
// Force use WebGl2
|
||||
WebGlContextOption::WebGl2 => init_webgl2(canvas),
|
||||
// Trying WebGl2 first
|
||||
WebGlContextOption::BestFirst => init_webgl2(canvas).or_else(|| init_webgl1(canvas)),
|
||||
// Trying WebGl1 first (useful for testing).
|
||||
WebGlContextOption::CompatibilityFirst => {
|
||||
init_webgl1(canvas).or_else(|| init_webgl2(canvas))
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(result) = result {
|
||||
Ok(result)
|
||||
} else {
|
||||
Err("WebGL isn't supported".into())
|
||||
}
|
||||
}
|
||||
|
||||
fn init_webgl1(canvas: &HtmlCanvasElement) -> Option<(glow::Context, &'static str)> {
|
||||
let gl1_ctx = canvas
|
||||
.get_context("webgl")
|
||||
.expect("Failed to query about WebGL2 context");
|
||||
|
||||
let gl1_ctx = gl1_ctx?;
|
||||
tracing::debug!("WebGL1 selected.");
|
||||
|
||||
let gl1_ctx = gl1_ctx
|
||||
.dyn_into::<web_sys::WebGlRenderingContext>()
|
||||
.unwrap();
|
||||
|
||||
let shader_prefix = if webgl1_requires_brightening(&gl1_ctx) {
|
||||
tracing::debug!("Enabling webkitGTK brightening workaround.");
|
||||
"#define APPLY_BRIGHTENING_GAMMA"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let gl = glow::Context::from_webgl1_context(gl1_ctx);
|
||||
|
||||
Some((gl, shader_prefix))
|
||||
}
|
||||
|
||||
fn init_webgl2(canvas: &HtmlCanvasElement) -> Option<(glow::Context, &'static str)> {
|
||||
let gl2_ctx = canvas
|
||||
.get_context("webgl2")
|
||||
.expect("Failed to query about WebGL2 context");
|
||||
|
||||
let gl2_ctx = gl2_ctx?;
|
||||
tracing::debug!("WebGL2 selected.");
|
||||
|
||||
let gl2_ctx = gl2_ctx
|
||||
.dyn_into::<web_sys::WebGl2RenderingContext>()
|
||||
.unwrap();
|
||||
let gl = glow::Context::from_webgl2_context(gl2_ctx);
|
||||
let shader_prefix = "";
|
||||
|
||||
Some((gl, shader_prefix))
|
||||
}
|
||||
|
||||
fn webgl1_requires_brightening(gl: &web_sys::WebGlRenderingContext) -> bool {
|
||||
// See https://github.com/emilk/egui/issues/794
|
||||
|
||||
// detect WebKitGTK
|
||||
|
||||
// WebKitGTK use WebKit default unmasked vendor and renderer
|
||||
// but safari use same vendor and renderer
|
||||
// so exclude "Mac OS X" user-agent.
|
||||
let user_agent = web_sys::window().unwrap().navigator().user_agent().unwrap();
|
||||
!user_agent.contains("Mac OS X") && is_safari_and_webkit_gtk(gl)
|
||||
}
|
||||
|
||||
/// detecting Safari and `webkitGTK`.
|
||||
///
|
||||
/// Safari and `webkitGTK` use unmasked renderer :Apple GPU
|
||||
///
|
||||
/// If we detect safari or `webkitGTKs` returns true.
|
||||
///
|
||||
/// This function used to avoid displaying linear color with `sRGB` supported systems.
|
||||
fn is_safari_and_webkit_gtk(gl: &web_sys::WebGlRenderingContext) -> bool {
|
||||
// This call produces a warning in Firefox ("WEBGL_debug_renderer_info is deprecated in Firefox and will be removed.")
|
||||
// but unless we call it we get errors in Chrome when we call `get_parameter` below.
|
||||
// TODO(emilk): do something smart based on user agent?
|
||||
if gl
|
||||
.get_extension("WEBGL_debug_renderer_info")
|
||||
.unwrap()
|
||||
.is_some()
|
||||
{
|
||||
if let Ok(renderer) =
|
||||
gl.get_parameter(web_sys::WebglDebugRendererInfo::UNMASKED_RENDERER_WEBGL)
|
||||
{
|
||||
if let Some(renderer) = renderer.as_string() {
|
||||
if renderer.contains("Apple") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
282
crates/eframe/src/web/web_painter_wgpu.rs
Normal file
282
crates/eframe/src/web/web_painter_wgpu.rs
Normal file
|
@ -0,0 +1,282 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use wasm_bindgen::JsValue;
|
||||
use web_sys::HtmlCanvasElement;
|
||||
|
||||
use egui::mutex::RwLock;
|
||||
use egui_wgpu::{renderer::ScreenDescriptor, RenderState, SurfaceErrorAction};
|
||||
|
||||
use crate::WebOptions;
|
||||
|
||||
use super::web_painter::WebPainter;
|
||||
|
||||
pub(crate) struct WebPainterWgpu {
|
||||
canvas: HtmlCanvasElement,
|
||||
canvas_id: String,
|
||||
surface: wgpu::Surface,
|
||||
surface_configuration: wgpu::SurfaceConfiguration,
|
||||
limits: wgpu::Limits,
|
||||
render_state: Option<RenderState>,
|
||||
on_surface_error: Arc<dyn Fn(wgpu::SurfaceError) -> SurfaceErrorAction>,
|
||||
depth_format: Option<wgpu::TextureFormat>,
|
||||
depth_texture_view: Option<wgpu::TextureView>,
|
||||
}
|
||||
|
||||
impl WebPainterWgpu {
|
||||
#[allow(unused)] // only used if `wgpu` is the only active feature.
|
||||
pub fn render_state(&self) -> Option<RenderState> {
|
||||
self.render_state.clone()
|
||||
}
|
||||
|
||||
pub fn generate_depth_texture_view(
|
||||
&self,
|
||||
render_state: &RenderState,
|
||||
width_in_pixels: u32,
|
||||
height_in_pixels: u32,
|
||||
) -> Option<wgpu::TextureView> {
|
||||
let device = &render_state.device;
|
||||
self.depth_format.map(|depth_format| {
|
||||
device
|
||||
.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("egui_depth_texture"),
|
||||
size: wgpu::Extent3d {
|
||||
width: width_in_pixels,
|
||||
height: height_in_pixels,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: depth_format,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
view_formats: &[depth_format],
|
||||
})
|
||||
.create_view(&wgpu::TextureViewDescriptor::default())
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(unused)] // only used if `wgpu` is the only active feature.
|
||||
pub async fn new(canvas_id: &str, options: &WebOptions) -> Result<Self, String> {
|
||||
tracing::debug!("Creating wgpu painter");
|
||||
|
||||
let canvas = super::canvas_element_or_die(canvas_id);
|
||||
|
||||
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
|
||||
backends: options.wgpu_options.backends,
|
||||
dx12_shader_compiler: Default::default(),
|
||||
});
|
||||
let surface = instance
|
||||
.create_surface_from_canvas(&canvas)
|
||||
.map_err(|err| format!("failed to create wgpu surface: {err}"))?;
|
||||
|
||||
let adapter = instance
|
||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||
power_preference: options.wgpu_options.power_preference,
|
||||
force_fallback_adapter: false,
|
||||
compatible_surface: None,
|
||||
})
|
||||
.await
|
||||
.ok_or_else(|| "No suitable GPU adapters found on the system".to_owned())?;
|
||||
|
||||
let (device, queue) = adapter
|
||||
.request_device(
|
||||
&options.wgpu_options.device_descriptor,
|
||||
None, // Capture doesn't work in the browser environment.
|
||||
)
|
||||
.await
|
||||
.map_err(|err| format!("Failed to find wgpu device: {}", err))?;
|
||||
|
||||
let target_format =
|
||||
egui_wgpu::preferred_framebuffer_format(&surface.get_capabilities(&adapter).formats);
|
||||
|
||||
let depth_format = options.wgpu_options.depth_format;
|
||||
let renderer = egui_wgpu::Renderer::new(&device, target_format, depth_format, 1);
|
||||
let render_state = RenderState {
|
||||
device: Arc::new(device),
|
||||
queue: Arc::new(queue),
|
||||
target_format,
|
||||
renderer: Arc::new(RwLock::new(renderer)),
|
||||
};
|
||||
|
||||
let surface_configuration = wgpu::SurfaceConfiguration {
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
format: target_format,
|
||||
width: 0,
|
||||
height: 0,
|
||||
present_mode: options.wgpu_options.present_mode,
|
||||
alpha_mode: wgpu::CompositeAlphaMode::Auto,
|
||||
view_formats: vec![target_format],
|
||||
};
|
||||
|
||||
tracing::debug!("wgpu painter initialized.");
|
||||
|
||||
Ok(Self {
|
||||
canvas,
|
||||
canvas_id: canvas_id.to_owned(),
|
||||
render_state: Some(render_state),
|
||||
surface,
|
||||
surface_configuration,
|
||||
depth_format,
|
||||
depth_texture_view: None,
|
||||
limits: options.wgpu_options.device_descriptor.limits.clone(),
|
||||
on_surface_error: options.wgpu_options.on_surface_error.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl WebPainter for WebPainterWgpu {
|
||||
fn canvas_id(&self) -> &str {
|
||||
&self.canvas_id
|
||||
}
|
||||
|
||||
fn max_texture_side(&self) -> usize {
|
||||
self.limits.max_texture_dimension_2d as _
|
||||
}
|
||||
|
||||
fn paint_and_update_textures(
|
||||
&mut self,
|
||||
clear_color: [f32; 4],
|
||||
clipped_primitives: &[egui::ClippedPrimitive],
|
||||
pixels_per_point: f32,
|
||||
textures_delta: &egui::TexturesDelta,
|
||||
) -> Result<(), JsValue> {
|
||||
let size_in_pixels = [self.canvas.width(), self.canvas.height()];
|
||||
|
||||
let render_state = if let Some(render_state) = &self.render_state {
|
||||
render_state
|
||||
} else {
|
||||
return Err(JsValue::from_str(
|
||||
"Can't paint, wgpu renderer was already disposed",
|
||||
));
|
||||
};
|
||||
|
||||
let mut encoder =
|
||||
render_state
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
label: Some("egui_webpainter_paint_and_update_textures"),
|
||||
});
|
||||
|
||||
// Upload all resources for the GPU.
|
||||
let screen_descriptor = ScreenDescriptor {
|
||||
size_in_pixels,
|
||||
pixels_per_point,
|
||||
};
|
||||
|
||||
let user_cmd_bufs = {
|
||||
let mut renderer = render_state.renderer.write();
|
||||
for (id, image_delta) in &textures_delta.set {
|
||||
renderer.update_texture(
|
||||
&render_state.device,
|
||||
&render_state.queue,
|
||||
*id,
|
||||
image_delta,
|
||||
);
|
||||
}
|
||||
|
||||
renderer.update_buffers(
|
||||
&render_state.device,
|
||||
&render_state.queue,
|
||||
&mut encoder,
|
||||
clipped_primitives,
|
||||
&screen_descriptor,
|
||||
)
|
||||
};
|
||||
|
||||
// Resize surface if needed
|
||||
let is_zero_sized_surface = size_in_pixels[0] == 0 || size_in_pixels[1] == 0;
|
||||
let frame = if is_zero_sized_surface {
|
||||
None
|
||||
} else {
|
||||
if size_in_pixels[0] != self.surface_configuration.width
|
||||
|| size_in_pixels[1] != self.surface_configuration.height
|
||||
{
|
||||
self.surface_configuration.width = size_in_pixels[0];
|
||||
self.surface_configuration.height = size_in_pixels[1];
|
||||
self.surface
|
||||
.configure(&render_state.device, &self.surface_configuration);
|
||||
self.depth_texture_view = self.generate_depth_texture_view(
|
||||
render_state,
|
||||
size_in_pixels[0],
|
||||
size_in_pixels[1],
|
||||
);
|
||||
}
|
||||
|
||||
let frame = match self.surface.get_current_texture() {
|
||||
Ok(frame) => frame,
|
||||
#[allow(clippy::single_match_else)]
|
||||
Err(e) => match (*self.on_surface_error)(e) {
|
||||
SurfaceErrorAction::RecreateSurface => {
|
||||
self.surface
|
||||
.configure(&render_state.device, &self.surface_configuration);
|
||||
return Ok(());
|
||||
}
|
||||
SurfaceErrorAction::SkipFrame => {
|
||||
return Ok(());
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
{
|
||||
let renderer = render_state.renderer.read();
|
||||
let frame_view = frame
|
||||
.texture
|
||||
.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &frame_view,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color {
|
||||
r: clear_color[0] as f64,
|
||||
g: clear_color[1] as f64,
|
||||
b: clear_color[2] as f64,
|
||||
a: clear_color[3] as f64,
|
||||
}),
|
||||
store: true,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: self.depth_texture_view.as_ref().map(|view| {
|
||||
wgpu::RenderPassDepthStencilAttachment {
|
||||
view,
|
||||
depth_ops: Some(wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(1.0),
|
||||
store: false,
|
||||
}),
|
||||
stencil_ops: None,
|
||||
}
|
||||
}),
|
||||
label: Some("egui_render"),
|
||||
});
|
||||
|
||||
renderer.render(&mut render_pass, clipped_primitives, &screen_descriptor);
|
||||
}
|
||||
|
||||
Some(frame)
|
||||
};
|
||||
|
||||
{
|
||||
let mut renderer = render_state.renderer.write();
|
||||
for id in &textures_delta.free {
|
||||
renderer.free_texture(id);
|
||||
}
|
||||
}
|
||||
|
||||
// Submit the commands: both the main buffer and user-defined ones.
|
||||
render_state.queue.submit(
|
||||
user_cmd_bufs
|
||||
.into_iter()
|
||||
.chain(std::iter::once(encoder.finish())),
|
||||
);
|
||||
|
||||
if let Some(frame) = frame {
|
||||
frame.present();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn destroy(&mut self) {
|
||||
self.render_state = None;
|
||||
}
|
||||
}
|
37
crates/egui-wgpu/CHANGELOG.md
Normal file
37
crates/egui-wgpu/CHANGELOG.md
Normal file
|
@ -0,0 +1,37 @@
|
|||
# Changelog for egui-wgpu
|
||||
All notable changes to the `egui-wgpu` integration will be noted in this file.
|
||||
|
||||
|
||||
## Unreleased
|
||||
|
||||
|
||||
## 0.21.0 - 2023-02-08
|
||||
* Update to `wgpu` 0.15 ([#2629](https://github.com/emilk/egui/pull/2629))
|
||||
* Return `Err` instead of panic if we can't find a device ([#2428](https://github.com/emilk/egui/pull/2428)).
|
||||
* `winit::Painter::set_window` is now `async` ([#2434](https://github.com/emilk/egui/pull/2434)).
|
||||
* `egui-wgpu` now only depends on `epaint` instead of the entire `egui` ([#2438](https://github.com/emilk/egui/pull/2438)).
|
||||
* `winit::Painter` now supports transparent backbuffer ([#2684](https://github.com/emilk/egui/pull/2684)).
|
||||
|
||||
|
||||
## 0.20.0 - 2022-12-08 - web support
|
||||
* Renamed `RenderPass` to `Renderer`.
|
||||
* Renamed `RenderPass::execute` to `RenderPass::render`.
|
||||
* Renamed `RenderPass::execute_with_renderpass` to `Renderer::render` (replacing existing `Renderer::render`)
|
||||
* Reexported `Renderer`.
|
||||
* You can now use `egui-wgpu` on web, using WebGL ([#2107](https://github.com/emilk/egui/pull/2107)).
|
||||
* `Renderer` no longer handles pass creation and depth buffer creation ([#2136](https://github.com/emilk/egui/pull/2136))
|
||||
* `PrepareCallback` now passes `wgpu::CommandEncoder` ([#2136](https://github.com/emilk/egui/pull/2136))
|
||||
* `PrepareCallback` can now returns `wgpu::CommandBuffer` that are bundled into a single `wgpu::Queue::submit` call ([#2230](https://github.com/emilk/egui/pull/2230))
|
||||
* Only a single vertex & index buffer is now created and resized when necessary (previously, vertex/index buffers were allocated for every mesh) ([#2148](https://github.com/emilk/egui/pull/2148)).
|
||||
* `Renderer::update_texture` no longer creates a new `wgpu::Sampler` with every new texture ([#2198](https://github.com/emilk/egui/pull/2198))
|
||||
* `Painter`'s instance/device/adapter/surface creation is now configurable via `WgpuConfiguration` ([#2207](https://github.com/emilk/egui/pull/2207))
|
||||
* Fix panic on using a depth buffer ([#2316](https://github.com/emilk/egui/pull/2316))
|
||||
|
||||
|
||||
## 0.19.0 - 2022-08-20
|
||||
* Enables deferred render + surface state initialization for Android ([#1634](https://github.com/emilk/egui/pull/1634)).
|
||||
* Make `RenderPass` `Send` and `Sync` ([#1883](https://github.com/emilk/egui/pull/1883)).
|
||||
|
||||
|
||||
## 0.18.0 - 2022-05-15
|
||||
First published version since moving the code into the `egui` repository from <https://github.com/LU15W1R7H/eww>.
|
56
crates/egui-wgpu/Cargo.toml
Normal file
56
crates/egui-wgpu/Cargo.toml
Normal file
|
@ -0,0 +1,56 @@
|
|||
[package]
|
||||
name = "egui-wgpu"
|
||||
version = "0.21.0"
|
||||
description = "Bindings for using egui natively using the wgpu library"
|
||||
authors = [
|
||||
"Nils Hasenbanck <nils@hasenbanck.de>",
|
||||
"embotech <opensource@embotech.com>",
|
||||
"Emil Ernerfeldt <emil.ernerfeldt@gmail.com>",
|
||||
]
|
||||
edition = "2021"
|
||||
rust-version = "1.65"
|
||||
homepage = "https://github.com/emilk/egui/tree/master/crates/egui-wgpu"
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/emilk/egui/tree/master/crates/egui-wgpu"
|
||||
categories = ["gui", "game-development"]
|
||||
keywords = ["wgpu", "egui", "gui", "gamedev"]
|
||||
include = [
|
||||
"../LICENSE-APACHE",
|
||||
"../LICENSE-MIT",
|
||||
"**/*.rs",
|
||||
"**/*.wgsl",
|
||||
"Cargo.toml",
|
||||
]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
||||
|
||||
[features]
|
||||
## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate.
|
||||
puffin = ["dep:puffin"]
|
||||
|
||||
## Enable [`winit`](https://docs.rs/winit) integration.
|
||||
winit = ["dep:winit"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
epaint = { version = "0.21.0", path = "../epaint", default-features = false, features = [
|
||||
"bytemuck",
|
||||
] }
|
||||
|
||||
bytemuck = "1.7"
|
||||
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
||||
type-map = "0.5.0"
|
||||
wgpu = "0.15.0"
|
||||
|
||||
#! ### Optional dependencies
|
||||
## Enable this when generating docs.
|
||||
document-features = { version = "0.2", optional = true }
|
||||
|
||||
winit = { version = "0.28", optional = true }
|
||||
|
||||
# Native:
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
puffin = { version = "0.14", optional = true }
|
10
crates/egui-wgpu/README.md
Normal file
10
crates/egui-wgpu/README.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
# egui-wgpu
|
||||
|
||||
[](https://crates.io/crates/egui-wgpu)
|
||||
[](https://docs.rs/egui-wgpu)
|
||||

|
||||

|
||||
|
||||
This crates provides bindings between [`egui`](https://github.com/emilk/egui) and [wgpu](https://crates.io/crates/wgpu).
|
||||
|
||||
This was originally hosted at https://github.com/hasenbanck/egui_wgpu_backend
|
91
crates/egui-wgpu/src/egui.wgsl
Normal file
91
crates/egui-wgpu/src/egui.wgsl
Normal file
|
@ -0,0 +1,91 @@
|
|||
// Vertex shader bindings
|
||||
|
||||
struct VertexOutput {
|
||||
@location(0) tex_coord: vec2<f32>,
|
||||
@location(1) color: vec4<f32>, // gamma 0-1
|
||||
@builtin(position) position: vec4<f32>,
|
||||
};
|
||||
|
||||
struct Locals {
|
||||
screen_size: vec2<f32>,
|
||||
// Uniform buffers need to be at least 16 bytes in WebGL.
|
||||
// See https://github.com/gfx-rs/wgpu/issues/2072
|
||||
_padding: vec2<u32>,
|
||||
};
|
||||
@group(0) @binding(0) var<uniform> r_locals: Locals;
|
||||
|
||||
// 0-1 linear from 0-1 sRGB gamma
|
||||
fn linear_from_gamma_rgb(srgb: vec3<f32>) -> vec3<f32> {
|
||||
let cutoff = srgb < vec3<f32>(0.04045);
|
||||
let lower = srgb / vec3<f32>(12.92);
|
||||
let higher = pow((srgb + vec3<f32>(0.055)) / vec3<f32>(1.055), vec3<f32>(2.4));
|
||||
return select(higher, lower, cutoff);
|
||||
}
|
||||
|
||||
// 0-1 sRGB gamma from 0-1 linear
|
||||
fn gamma_from_linear_rgb(rgb: vec3<f32>) -> vec3<f32> {
|
||||
let cutoff = rgb < vec3<f32>(0.0031308);
|
||||
let lower = rgb * vec3<f32>(12.92);
|
||||
let higher = vec3<f32>(1.055) * pow(rgb, vec3<f32>(1.0 / 2.4)) - vec3<f32>(0.055);
|
||||
return select(higher, lower, cutoff);
|
||||
}
|
||||
|
||||
// 0-1 sRGBA gamma from 0-1 linear
|
||||
fn gamma_from_linear_rgba(linear_rgba: vec4<f32>) -> vec4<f32> {
|
||||
return vec4<f32>(gamma_from_linear_rgb(linear_rgba.rgb), linear_rgba.a);
|
||||
}
|
||||
|
||||
// [u8; 4] SRGB as u32 -> [r, g, b, a] in 0.-1
|
||||
fn unpack_color(color: u32) -> vec4<f32> {
|
||||
return vec4<f32>(
|
||||
f32(color & 255u),
|
||||
f32((color >> 8u) & 255u),
|
||||
f32((color >> 16u) & 255u),
|
||||
f32((color >> 24u) & 255u),
|
||||
) / 255.0;
|
||||
}
|
||||
|
||||
fn position_from_screen(screen_pos: vec2<f32>) -> vec4<f32> {
|
||||
return vec4<f32>(
|
||||
2.0 * screen_pos.x / r_locals.screen_size.x - 1.0,
|
||||
1.0 - 2.0 * screen_pos.y / r_locals.screen_size.y,
|
||||
0.0,
|
||||
1.0,
|
||||
);
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_main(
|
||||
@location(0) a_pos: vec2<f32>,
|
||||
@location(1) a_tex_coord: vec2<f32>,
|
||||
@location(2) a_color: u32,
|
||||
) -> VertexOutput {
|
||||
var out: VertexOutput;
|
||||
out.tex_coord = a_tex_coord;
|
||||
out.color = unpack_color(a_color);
|
||||
out.position = position_from_screen(a_pos);
|
||||
return out;
|
||||
}
|
||||
|
||||
// Fragment shader bindings
|
||||
|
||||
@group(1) @binding(0) var r_tex_color: texture_2d<f32>;
|
||||
@group(1) @binding(1) var r_tex_sampler: sampler;
|
||||
|
||||
@fragment
|
||||
fn fs_main_linear_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
// We always have an sRGB aware texture at the moment.
|
||||
let tex_linear = textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
|
||||
let tex_gamma = gamma_from_linear_rgba(tex_linear);
|
||||
let out_color_gamma = in.color * tex_gamma;
|
||||
return vec4<f32>(linear_from_gamma_rgb(out_color_gamma.rgb), out_color_gamma.a);
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main_gamma_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
// We always have an sRGB aware texture at the moment.
|
||||
let tex_linear = textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
|
||||
let tex_gamma = gamma_from_linear_rgba(tex_linear);
|
||||
let out_color_gamma = in.color * tex_gamma;
|
||||
return out_color_gamma;
|
||||
}
|
155
crates/egui-wgpu/src/lib.rs
Normal file
155
crates/egui-wgpu/src/lib.rs
Normal file
|
@ -0,0 +1,155 @@
|
|||
//! This crates provides bindings between [`egui`](https://github.com/emilk/egui) and [wgpu](https://crates.io/crates/wgpu).
|
||||
//!
|
||||
//! ## Feature flags
|
||||
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
|
||||
//!
|
||||
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
pub use wgpu;
|
||||
|
||||
/// Low-level painting of [`egui`](https://github.com/emilk/egui) on [`wgpu`].
|
||||
pub mod renderer;
|
||||
pub use renderer::CallbackFn;
|
||||
pub use renderer::Renderer;
|
||||
|
||||
/// Module for painting [`egui`](https://github.com/emilk/egui) with [`wgpu`] on [`winit`].
|
||||
#[cfg(feature = "winit")]
|
||||
pub mod winit;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use epaint::mutex::RwLock;
|
||||
|
||||
/// Access to the render state for egui.
|
||||
#[derive(Clone)]
|
||||
pub struct RenderState {
|
||||
pub device: Arc<wgpu::Device>,
|
||||
pub queue: Arc<wgpu::Queue>,
|
||||
pub target_format: wgpu::TextureFormat,
|
||||
pub renderer: Arc<RwLock<Renderer>>,
|
||||
}
|
||||
|
||||
/// Specifies which action should be taken as consequence of a [`wgpu::SurfaceError`]
|
||||
pub enum SurfaceErrorAction {
|
||||
/// Do nothing and skip the current frame.
|
||||
SkipFrame,
|
||||
|
||||
/// Instructs egui to recreate the surface, then skip the current frame.
|
||||
RecreateSurface,
|
||||
}
|
||||
|
||||
/// Configuration for using wgpu with eframe or the egui-wgpu winit feature.
|
||||
#[derive(Clone)]
|
||||
pub struct WgpuConfiguration {
|
||||
/// Configuration passed on device request.
|
||||
pub device_descriptor: wgpu::DeviceDescriptor<'static>,
|
||||
|
||||
/// Backends that should be supported (wgpu will pick one of these)
|
||||
pub backends: wgpu::Backends,
|
||||
|
||||
/// Present mode used for the primary surface.
|
||||
pub present_mode: wgpu::PresentMode,
|
||||
|
||||
/// Power preference for the adapter.
|
||||
pub power_preference: wgpu::PowerPreference,
|
||||
|
||||
/// Callback for surface errors.
|
||||
pub on_surface_error: Arc<dyn Fn(wgpu::SurfaceError) -> SurfaceErrorAction>,
|
||||
|
||||
pub depth_format: Option<wgpu::TextureFormat>,
|
||||
}
|
||||
|
||||
impl Default for WgpuConfiguration {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
device_descriptor: wgpu::DeviceDescriptor {
|
||||
label: Some("egui wgpu device"),
|
||||
features: wgpu::Features::default(),
|
||||
limits: wgpu::Limits::default(),
|
||||
},
|
||||
backends: wgpu::Backends::PRIMARY | wgpu::Backends::GL,
|
||||
present_mode: wgpu::PresentMode::AutoVsync,
|
||||
power_preference: wgpu::PowerPreference::HighPerformance,
|
||||
depth_format: None,
|
||||
|
||||
on_surface_error: Arc::new(|err| {
|
||||
if err == wgpu::SurfaceError::Outdated {
|
||||
// This error occurs when the app is minimized on Windows.
|
||||
// Silently return here to prevent spamming the console with:
|
||||
// "The underlying surface has changed, and therefore the swap chain must be updated"
|
||||
} else {
|
||||
tracing::warn!("Dropped frame with error: {err}");
|
||||
}
|
||||
SurfaceErrorAction::SkipFrame
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the framebuffer format that egui prefers
|
||||
pub fn preferred_framebuffer_format(formats: &[wgpu::TextureFormat]) -> wgpu::TextureFormat {
|
||||
for &format in formats {
|
||||
if matches!(
|
||||
format,
|
||||
wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Bgra8Unorm
|
||||
) {
|
||||
return format;
|
||||
}
|
||||
}
|
||||
formats[0] // take the first
|
||||
}
|
||||
// maybe use this-error?
|
||||
#[derive(Debug)]
|
||||
pub enum WgpuError {
|
||||
DeviceError(wgpu::RequestDeviceError),
|
||||
SurfaceError(wgpu::CreateSurfaceError),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for WgpuError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
std::fmt::Debug::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for WgpuError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
WgpuError::DeviceError(e) => e.source(),
|
||||
WgpuError::SurfaceError(e) => e.source(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<wgpu::RequestDeviceError> for WgpuError {
|
||||
fn from(e: wgpu::RequestDeviceError) -> Self {
|
||||
Self::DeviceError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<wgpu::CreateSurfaceError> for WgpuError {
|
||||
fn from(e: wgpu::CreateSurfaceError) -> Self {
|
||||
Self::SurfaceError(e)
|
||||
}
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Profiling macro for feature "puffin"
|
||||
macro_rules! profile_function {
|
||||
($($arg: tt)*) => {
|
||||
#[cfg(feature = "puffin")]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
puffin::profile_function!($($arg)*);
|
||||
};
|
||||
}
|
||||
pub(crate) use profile_function;
|
||||
|
||||
/// Profiling macro for feature "puffin"
|
||||
macro_rules! profile_scope {
|
||||
($($arg: tt)*) => {
|
||||
#[cfg(feature = "puffin")]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
puffin::profile_scope!($($arg)*);
|
||||
};
|
||||
}
|
||||
pub(crate) use profile_scope;
|
931
crates/egui-wgpu/src/renderer.rs
Normal file
931
crates/egui-wgpu/src/renderer.rs
Normal file
|
@ -0,0 +1,931 @@
|
|||
#![allow(unsafe_code)]
|
||||
|
||||
use std::num::NonZeroU64;
|
||||
use std::ops::Range;
|
||||
use std::{borrow::Cow, collections::HashMap, num::NonZeroU32};
|
||||
|
||||
use type_map::concurrent::TypeMap;
|
||||
use wgpu;
|
||||
use wgpu::util::DeviceExt as _;
|
||||
|
||||
use epaint::{emath::NumExt, PaintCallbackInfo, Primitive, Vertex};
|
||||
|
||||
/// A callback function that can be used to compose an [`epaint::PaintCallback`] for custom WGPU
|
||||
/// rendering.
|
||||
///
|
||||
/// The callback is composed of two functions: `prepare` and `paint`:
|
||||
/// - `prepare` is called every frame before `paint`, and can use the passed-in
|
||||
/// [`wgpu::Device`] and [`wgpu::Buffer`] to allocate or modify GPU resources such as buffers.
|
||||
/// - `paint` is called after `prepare` and is given access to the [`wgpu::RenderPass`] so
|
||||
/// that it can issue draw commands into the same [`wgpu::RenderPass`] that is used for
|
||||
/// all other egui elements.
|
||||
///
|
||||
/// The final argument of both the `prepare` and `paint` callbacks is a the
|
||||
/// [`paint_callback_resources`][crate::renderer::Renderer::paint_callback_resources].
|
||||
/// `paint_callback_resources` has the same lifetime as the Egui render pass, so it can be used to
|
||||
/// store buffers, pipelines, and other information that needs to be accessed during the render
|
||||
/// pass.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// See the [`custom3d_wgpu`](https://github.com/emilk/egui/blob/master/crates/egui_demo_app/src/apps/custom3d_wgpu.rs) demo source for a detailed usage example.
|
||||
pub struct CallbackFn {
|
||||
prepare: Box<PrepareCallback>,
|
||||
paint: Box<PaintCallback>,
|
||||
}
|
||||
|
||||
type PrepareCallback = dyn Fn(
|
||||
&wgpu::Device,
|
||||
&wgpu::Queue,
|
||||
&mut wgpu::CommandEncoder,
|
||||
&mut TypeMap,
|
||||
) -> Vec<wgpu::CommandBuffer>
|
||||
+ Sync
|
||||
+ Send;
|
||||
|
||||
type PaintCallback =
|
||||
dyn for<'a, 'b> Fn(PaintCallbackInfo, &'a mut wgpu::RenderPass<'b>, &'b TypeMap) + Sync + Send;
|
||||
|
||||
impl Default for CallbackFn {
|
||||
fn default() -> Self {
|
||||
CallbackFn {
|
||||
prepare: Box::new(|_, _, _, _| Vec::new()),
|
||||
paint: Box::new(|_, _, _| ()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CallbackFn {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Set the prepare callback.
|
||||
///
|
||||
/// The passed-in `CommandEncoder` is egui's and can be used directly to register
|
||||
/// wgpu commands for simple use cases.
|
||||
/// This allows reusing the same [`wgpu::CommandEncoder`] for all callbacks and egui
|
||||
/// rendering itself.
|
||||
///
|
||||
/// For more complicated use cases, one can also return a list of arbitrary
|
||||
/// `CommandBuffer`s and have complete control over how they get created and fed.
|
||||
/// In particular, this gives an opportunity to parallelize command registration and
|
||||
/// prevents a faulty callback from poisoning the main wgpu pipeline.
|
||||
///
|
||||
/// When using eframe, the main egui command buffer, as well as all user-defined
|
||||
/// command buffers returned by this function, are guaranteed to all be submitted
|
||||
/// at once in a single call.
|
||||
pub fn prepare<F>(mut self, prepare: F) -> Self
|
||||
where
|
||||
F: Fn(
|
||||
&wgpu::Device,
|
||||
&wgpu::Queue,
|
||||
&mut wgpu::CommandEncoder,
|
||||
&mut TypeMap,
|
||||
) -> Vec<wgpu::CommandBuffer>
|
||||
+ Sync
|
||||
+ Send
|
||||
+ 'static,
|
||||
{
|
||||
self.prepare = Box::new(prepare) as _;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the paint callback
|
||||
pub fn paint<F>(mut self, paint: F) -> Self
|
||||
where
|
||||
F: for<'a, 'b> Fn(PaintCallbackInfo, &'a mut wgpu::RenderPass<'b>, &'b TypeMap)
|
||||
+ Sync
|
||||
+ Send
|
||||
+ 'static,
|
||||
{
|
||||
self.paint = Box::new(paint) as _;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about the screen used for rendering.
|
||||
pub struct ScreenDescriptor {
|
||||
/// Size of the window in physical pixels.
|
||||
pub size_in_pixels: [u32; 2],
|
||||
|
||||
/// HiDPI scale factor (pixels per point).
|
||||
pub pixels_per_point: f32,
|
||||
}
|
||||
|
||||
impl ScreenDescriptor {
|
||||
/// size in "logical" points
|
||||
fn screen_size_in_points(&self) -> [f32; 2] {
|
||||
[
|
||||
self.size_in_pixels[0] as f32 / self.pixels_per_point,
|
||||
self.size_in_pixels[1] as f32 / self.pixels_per_point,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/// Uniform buffer used when rendering.
|
||||
#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
#[repr(C)]
|
||||
struct UniformBuffer {
|
||||
screen_size_in_points: [f32; 2],
|
||||
// Uniform buffers need to be at least 16 bytes in WebGL.
|
||||
// See https://github.com/gfx-rs/wgpu/issues/2072
|
||||
_padding: [u32; 2],
|
||||
}
|
||||
|
||||
struct SlicedBuffer {
|
||||
buffer: wgpu::Buffer,
|
||||
slices: Vec<Range<wgpu::BufferAddress>>,
|
||||
capacity: wgpu::BufferAddress,
|
||||
}
|
||||
|
||||
/// Renderer for a egui based GUI.
|
||||
pub struct Renderer {
|
||||
pipeline: wgpu::RenderPipeline,
|
||||
|
||||
index_buffer: SlicedBuffer,
|
||||
vertex_buffer: SlicedBuffer,
|
||||
|
||||
uniform_buffer: wgpu::Buffer,
|
||||
uniform_bind_group: wgpu::BindGroup,
|
||||
texture_bind_group_layout: wgpu::BindGroupLayout,
|
||||
|
||||
/// Map of egui texture IDs to textures and their associated bindgroups (texture view +
|
||||
/// sampler). The texture may be None if the TextureId is just a handle to a user-provided
|
||||
/// sampler.
|
||||
textures: HashMap<epaint::TextureId, (Option<wgpu::Texture>, wgpu::BindGroup)>,
|
||||
next_user_texture_id: u64,
|
||||
samplers: HashMap<epaint::textures::TextureOptions, wgpu::Sampler>,
|
||||
|
||||
/// Storage for use by [`epaint::PaintCallback`]'s that need to store resources such as render
|
||||
/// pipelines that must have the lifetime of the renderpass.
|
||||
pub paint_callback_resources: TypeMap,
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
/// Creates a renderer for a egui UI.
|
||||
///
|
||||
/// `output_color_format` should preferably be [`wgpu::TextureFormat::Rgba8Unorm`] or
|
||||
/// [`wgpu::TextureFormat::Bgra8Unorm`], i.e. in gamma-space.
|
||||
pub fn new(
|
||||
device: &wgpu::Device,
|
||||
output_color_format: wgpu::TextureFormat,
|
||||
output_depth_format: Option<wgpu::TextureFormat>,
|
||||
msaa_samples: u32,
|
||||
) -> Self {
|
||||
crate::profile_function!();
|
||||
|
||||
let shader = wgpu::ShaderModuleDescriptor {
|
||||
label: Some("egui"),
|
||||
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("egui.wgsl"))),
|
||||
};
|
||||
let module = device.create_shader_module(shader);
|
||||
|
||||
let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("egui_uniform_buffer"),
|
||||
contents: bytemuck::cast_slice(&[UniformBuffer {
|
||||
screen_size_in_points: [0.0, 0.0],
|
||||
_padding: Default::default(),
|
||||
}]),
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
});
|
||||
|
||||
let uniform_bind_group_layout =
|
||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("egui_uniform_bind_group_layout"),
|
||||
entries: &[wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::VERTEX,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: NonZeroU64::new(std::mem::size_of::<UniformBuffer>() as _),
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
},
|
||||
count: None,
|
||||
}],
|
||||
});
|
||||
|
||||
let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("egui_uniform_bind_group"),
|
||||
layout: &uniform_bind_group_layout,
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
|
||||
buffer: &uniform_buffer,
|
||||
offset: 0,
|
||||
size: None,
|
||||
}),
|
||||
}],
|
||||
});
|
||||
|
||||
let texture_bind_group_layout =
|
||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("egui_texture_bind_group_layout"),
|
||||
entries: &[
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
multisampled: false,
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 1,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("egui_pipeline_layout"),
|
||||
bind_group_layouts: &[&uniform_bind_group_layout, &texture_bind_group_layout],
|
||||
push_constant_ranges: &[],
|
||||
});
|
||||
|
||||
let depth_stencil = output_depth_format.map(|format| wgpu::DepthStencilState {
|
||||
format,
|
||||
depth_write_enabled: false,
|
||||
depth_compare: wgpu::CompareFunction::Always,
|
||||
stencil: wgpu::StencilState::default(),
|
||||
bias: wgpu::DepthBiasState::default(),
|
||||
});
|
||||
|
||||
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("egui_pipeline"),
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
entry_point: "vs_main",
|
||||
module: &module,
|
||||
buffers: &[wgpu::VertexBufferLayout {
|
||||
array_stride: 5 * 4,
|
||||
step_mode: wgpu::VertexStepMode::Vertex,
|
||||
// 0: vec2 position
|
||||
// 1: vec2 texture coordinates
|
||||
// 2: uint color
|
||||
attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Uint32],
|
||||
}],
|
||||
},
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
unclipped_depth: false,
|
||||
conservative: false,
|
||||
cull_mode: None,
|
||||
front_face: wgpu::FrontFace::default(),
|
||||
polygon_mode: wgpu::PolygonMode::default(),
|
||||
strip_index_format: None,
|
||||
},
|
||||
depth_stencil,
|
||||
multisample: wgpu::MultisampleState {
|
||||
alpha_to_coverage_enabled: false,
|
||||
count: msaa_samples,
|
||||
mask: !0,
|
||||
},
|
||||
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &module,
|
||||
entry_point: if output_color_format.describe().srgb {
|
||||
tracing::warn!("Detected a linear (sRGBA aware) framebuffer {:?}. egui prefers Rgba8Unorm or Bgra8Unorm", output_color_format);
|
||||
"fs_main_linear_framebuffer"
|
||||
} else {
|
||||
"fs_main_gamma_framebuffer" // this is what we prefer
|
||||
},
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: output_color_format,
|
||||
blend: Some(wgpu::BlendState {
|
||||
color: wgpu::BlendComponent {
|
||||
src_factor: wgpu::BlendFactor::One,
|
||||
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
|
||||
operation: wgpu::BlendOperation::Add,
|
||||
},
|
||||
alpha: wgpu::BlendComponent {
|
||||
src_factor: wgpu::BlendFactor::OneMinusDstAlpha,
|
||||
dst_factor: wgpu::BlendFactor::One,
|
||||
operation: wgpu::BlendOperation::Add,
|
||||
},
|
||||
}),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
}),
|
||||
multiview: None,
|
||||
});
|
||||
|
||||
const VERTEX_BUFFER_START_CAPACITY: wgpu::BufferAddress =
|
||||
(std::mem::size_of::<Vertex>() * 1024) as _;
|
||||
const INDEX_BUFFER_START_CAPACITY: wgpu::BufferAddress =
|
||||
(std::mem::size_of::<u32>() * 1024 * 3) as _;
|
||||
|
||||
Self {
|
||||
pipeline,
|
||||
vertex_buffer: SlicedBuffer {
|
||||
buffer: create_vertex_buffer(device, VERTEX_BUFFER_START_CAPACITY),
|
||||
slices: Vec::with_capacity(64),
|
||||
capacity: VERTEX_BUFFER_START_CAPACITY,
|
||||
},
|
||||
index_buffer: SlicedBuffer {
|
||||
buffer: create_index_buffer(device, INDEX_BUFFER_START_CAPACITY),
|
||||
slices: Vec::with_capacity(64),
|
||||
capacity: INDEX_BUFFER_START_CAPACITY,
|
||||
},
|
||||
uniform_buffer,
|
||||
uniform_bind_group,
|
||||
texture_bind_group_layout,
|
||||
textures: HashMap::new(),
|
||||
next_user_texture_id: 0,
|
||||
samplers: HashMap::new(),
|
||||
paint_callback_resources: TypeMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes the egui renderer onto an existing wgpu renderpass.
|
||||
pub fn render<'rp>(
|
||||
&'rp self,
|
||||
render_pass: &mut wgpu::RenderPass<'rp>,
|
||||
paint_jobs: &[epaint::ClippedPrimitive],
|
||||
screen_descriptor: &ScreenDescriptor,
|
||||
) {
|
||||
crate::profile_function!();
|
||||
|
||||
let pixels_per_point = screen_descriptor.pixels_per_point;
|
||||
let size_in_pixels = screen_descriptor.size_in_pixels;
|
||||
|
||||
// Whether or not we need to reset the render pass because a paint callback has just
|
||||
// run.
|
||||
let mut needs_reset = true;
|
||||
|
||||
let mut index_buffer_slices = self.index_buffer.slices.iter();
|
||||
let mut vertex_buffer_slices = self.vertex_buffer.slices.iter();
|
||||
|
||||
for epaint::ClippedPrimitive {
|
||||
clip_rect,
|
||||
primitive,
|
||||
} in paint_jobs
|
||||
{
|
||||
if needs_reset {
|
||||
render_pass.set_viewport(
|
||||
0.0,
|
||||
0.0,
|
||||
size_in_pixels[0] as f32,
|
||||
size_in_pixels[1] as f32,
|
||||
0.0,
|
||||
1.0,
|
||||
);
|
||||
render_pass.set_pipeline(&self.pipeline);
|
||||
render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||
needs_reset = false;
|
||||
}
|
||||
|
||||
{
|
||||
let rect = ScissorRect::new(clip_rect, pixels_per_point, size_in_pixels);
|
||||
|
||||
if rect.width == 0 || rect.height == 0 {
|
||||
// Skip rendering zero-sized clip areas.
|
||||
if let Primitive::Mesh(_) = primitive {
|
||||
// If this is a mesh, we need to advance the index and vertex buffer iterators:
|
||||
index_buffer_slices.next().unwrap();
|
||||
vertex_buffer_slices.next().unwrap();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
render_pass.set_scissor_rect(rect.x, rect.y, rect.width, rect.height);
|
||||
}
|
||||
|
||||
match primitive {
|
||||
Primitive::Mesh(mesh) => {
|
||||
let index_buffer_slice = index_buffer_slices.next().unwrap();
|
||||
let vertex_buffer_slice = vertex_buffer_slices.next().unwrap();
|
||||
|
||||
if let Some((_texture, bind_group)) = self.textures.get(&mesh.texture_id) {
|
||||
render_pass.set_bind_group(1, bind_group, &[]);
|
||||
render_pass.set_index_buffer(
|
||||
self.index_buffer.buffer.slice(index_buffer_slice.clone()),
|
||||
wgpu::IndexFormat::Uint32,
|
||||
);
|
||||
render_pass.set_vertex_buffer(
|
||||
0,
|
||||
self.vertex_buffer.buffer.slice(vertex_buffer_slice.clone()),
|
||||
);
|
||||
render_pass.draw_indexed(0..mesh.indices.len() as u32, 0, 0..1);
|
||||
} else {
|
||||
tracing::warn!("Missing texture: {:?}", mesh.texture_id);
|
||||
}
|
||||
}
|
||||
Primitive::Callback(callback) => {
|
||||
let cbfn = if let Some(c) = callback.callback.downcast_ref::<CallbackFn>() {
|
||||
c
|
||||
} else {
|
||||
// We already warned in the `prepare` callback
|
||||
continue;
|
||||
};
|
||||
|
||||
if callback.rect.is_positive() {
|
||||
crate::profile_scope!("callback");
|
||||
|
||||
needs_reset = true;
|
||||
|
||||
{
|
||||
// We're setting a default viewport for the render pass as a
|
||||
// courtesy for the user, so that they don't have to think about
|
||||
// it in the simple case where they just want to fill the whole
|
||||
// paint area.
|
||||
//
|
||||
// The user still has the possibility of setting their own custom
|
||||
// viewport during the paint callback, effectively overriding this
|
||||
// one.
|
||||
|
||||
let min = (callback.rect.min.to_vec2() * pixels_per_point).round();
|
||||
let max = (callback.rect.max.to_vec2() * pixels_per_point).round();
|
||||
|
||||
render_pass.set_viewport(
|
||||
min.x,
|
||||
min.y,
|
||||
max.x - min.x,
|
||||
max.y - min.y,
|
||||
0.0,
|
||||
1.0,
|
||||
);
|
||||
}
|
||||
|
||||
(cbfn.paint)(
|
||||
PaintCallbackInfo {
|
||||
viewport: callback.rect,
|
||||
clip_rect: *clip_rect,
|
||||
pixels_per_point,
|
||||
screen_size_px: size_in_pixels,
|
||||
},
|
||||
render_pass,
|
||||
&self.paint_callback_resources,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render_pass.set_scissor_rect(0, 0, size_in_pixels[0], size_in_pixels[1]);
|
||||
}
|
||||
|
||||
/// Should be called before `render()`.
|
||||
pub fn update_texture(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
id: epaint::TextureId,
|
||||
image_delta: &epaint::ImageDelta,
|
||||
) {
|
||||
crate::profile_function!();
|
||||
|
||||
let width = image_delta.image.width() as u32;
|
||||
let height = image_delta.image.height() as u32;
|
||||
|
||||
let size = wgpu::Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
};
|
||||
|
||||
let data_color32 = match &image_delta.image {
|
||||
epaint::ImageData::Color(image) => {
|
||||
assert_eq!(
|
||||
width as usize * height as usize,
|
||||
image.pixels.len(),
|
||||
"Mismatch between texture size and texel count"
|
||||
);
|
||||
Cow::Borrowed(&image.pixels)
|
||||
}
|
||||
epaint::ImageData::Font(image) => {
|
||||
assert_eq!(
|
||||
width as usize * height as usize,
|
||||
image.pixels.len(),
|
||||
"Mismatch between texture size and texel count"
|
||||
);
|
||||
Cow::Owned(image.srgba_pixels(None).collect::<Vec<_>>())
|
||||
}
|
||||
};
|
||||
let data_bytes: &[u8] = bytemuck::cast_slice(data_color32.as_slice());
|
||||
|
||||
let queue_write_data_to_texture = |texture, origin| {
|
||||
queue.write_texture(
|
||||
wgpu::ImageCopyTexture {
|
||||
texture,
|
||||
mip_level: 0,
|
||||
origin,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
data_bytes,
|
||||
wgpu::ImageDataLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: NonZeroU32::new(4 * width),
|
||||
rows_per_image: NonZeroU32::new(height),
|
||||
},
|
||||
size,
|
||||
);
|
||||
};
|
||||
|
||||
if let Some(pos) = image_delta.pos {
|
||||
// update the existing texture
|
||||
let (texture, _bind_group) = self
|
||||
.textures
|
||||
.get(&id)
|
||||
.expect("Tried to update a texture that has not been allocated yet.");
|
||||
let origin = wgpu::Origin3d {
|
||||
x: pos[0] as u32,
|
||||
y: pos[1] as u32,
|
||||
z: 0,
|
||||
};
|
||||
queue_write_data_to_texture(
|
||||
texture.as_ref().expect("Tried to update user texture."),
|
||||
origin,
|
||||
);
|
||||
} else {
|
||||
// allocate a new texture
|
||||
// Use same label for all resources associated with this texture id (no point in retyping the type)
|
||||
let label_str = format!("egui_texid_{:?}", id);
|
||||
let label = Some(label_str.as_str());
|
||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label,
|
||||
size,
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Rgba8UnormSrgb, // Minspec for wgpu WebGL emulation is WebGL2, so this should always be supported.
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
||||
view_formats: &[wgpu::TextureFormat::Rgba8UnormSrgb],
|
||||
});
|
||||
let sampler = self
|
||||
.samplers
|
||||
.entry(image_delta.options)
|
||||
.or_insert_with(|| create_sampler(image_delta.options, device));
|
||||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label,
|
||||
layout: &self.texture_bind_group_layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(
|
||||
&texture.create_view(&wgpu::TextureViewDescriptor::default()),
|
||||
),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::Sampler(sampler),
|
||||
},
|
||||
],
|
||||
});
|
||||
let origin = wgpu::Origin3d::ZERO;
|
||||
queue_write_data_to_texture(&texture, origin);
|
||||
self.textures.insert(id, (Some(texture), bind_group));
|
||||
};
|
||||
}
|
||||
|
||||
pub fn free_texture(&mut self, id: &epaint::TextureId) {
|
||||
self.textures.remove(id);
|
||||
}
|
||||
|
||||
/// Get the WGPU texture and bind group associated to a texture that has been allocated by egui.
|
||||
///
|
||||
/// This could be used by custom paint hooks to render images that have been added through with
|
||||
/// [`egui_extras::RetainedImage`](https://docs.rs/egui_extras/latest/egui_extras/image/struct.RetainedImage.html)
|
||||
/// or [`epaint::Context::load_texture`](https://docs.rs/egui/latest/egui/struct.Context.html#method.load_texture).
|
||||
pub fn texture(
|
||||
&self,
|
||||
id: &epaint::TextureId,
|
||||
) -> Option<&(Option<wgpu::Texture>, wgpu::BindGroup)> {
|
||||
self.textures.get(id)
|
||||
}
|
||||
|
||||
/// Registers a `wgpu::Texture` with a `epaint::TextureId`.
|
||||
///
|
||||
/// This enables the application to reference the texture inside an image ui element.
|
||||
/// This effectively enables off-screen rendering inside the egui UI. Texture must have
|
||||
/// the texture format `TextureFormat::Rgba8UnormSrgb` and
|
||||
/// Texture usage `TextureUsage::SAMPLED`.
|
||||
pub fn register_native_texture(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
texture: &wgpu::TextureView,
|
||||
texture_filter: wgpu::FilterMode,
|
||||
) -> epaint::TextureId {
|
||||
self.register_native_texture_with_sampler_options(
|
||||
device,
|
||||
texture,
|
||||
wgpu::SamplerDescriptor {
|
||||
label: Some(format!("egui_user_image_{}", self.next_user_texture_id).as_str()),
|
||||
mag_filter: texture_filter,
|
||||
min_filter: texture_filter,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Registers a `wgpu::Texture` with an existing `epaint::TextureId`.
|
||||
///
|
||||
/// This enables applications to reuse `TextureId`s.
|
||||
pub fn update_egui_texture_from_wgpu_texture(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
texture: &wgpu::TextureView,
|
||||
texture_filter: wgpu::FilterMode,
|
||||
id: epaint::TextureId,
|
||||
) {
|
||||
self.update_egui_texture_from_wgpu_texture_with_sampler_options(
|
||||
device,
|
||||
texture,
|
||||
wgpu::SamplerDescriptor {
|
||||
label: Some(format!("egui_user_image_{}", self.next_user_texture_id).as_str()),
|
||||
mag_filter: texture_filter,
|
||||
min_filter: texture_filter,
|
||||
..Default::default()
|
||||
},
|
||||
id,
|
||||
);
|
||||
}
|
||||
|
||||
/// Registers a `wgpu::Texture` with a `epaint::TextureId` while also accepting custom
|
||||
/// `wgpu::SamplerDescriptor` options.
|
||||
///
|
||||
/// This allows applications to specify individual minification/magnification filters as well as
|
||||
/// custom mipmap and tiling options.
|
||||
///
|
||||
/// The `Texture` must have the format `TextureFormat::Rgba8UnormSrgb` and usage
|
||||
/// `TextureUsage::SAMPLED`. Any compare function supplied in the `SamplerDescriptor` will be
|
||||
/// ignored.
|
||||
#[allow(clippy::needless_pass_by_value)] // false positive
|
||||
pub fn register_native_texture_with_sampler_options(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
texture: &wgpu::TextureView,
|
||||
sampler_descriptor: wgpu::SamplerDescriptor<'_>,
|
||||
) -> epaint::TextureId {
|
||||
crate::profile_function!();
|
||||
|
||||
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
compare: None,
|
||||
..sampler_descriptor
|
||||
});
|
||||
|
||||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some(format!("egui_user_image_{}", self.next_user_texture_id).as_str()),
|
||||
layout: &self.texture_bind_group_layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(texture),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::Sampler(&sampler),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let id = epaint::TextureId::User(self.next_user_texture_id);
|
||||
self.textures.insert(id, (None, bind_group));
|
||||
self.next_user_texture_id += 1;
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
/// Registers a `wgpu::Texture` with an existing `epaint::TextureId` while also accepting custom
|
||||
/// `wgpu::SamplerDescriptor` options.
|
||||
///
|
||||
/// This allows applications to reuse `TextureId`s created with custom sampler options.
|
||||
#[allow(clippy::needless_pass_by_value)] // false positive
|
||||
pub fn update_egui_texture_from_wgpu_texture_with_sampler_options(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
texture: &wgpu::TextureView,
|
||||
sampler_descriptor: wgpu::SamplerDescriptor<'_>,
|
||||
id: epaint::TextureId,
|
||||
) {
|
||||
crate::profile_function!();
|
||||
|
||||
let (_user_texture, user_texture_binding) = self
|
||||
.textures
|
||||
.get_mut(&id)
|
||||
.expect("Tried to update a texture that has not been allocated yet.");
|
||||
|
||||
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
compare: None,
|
||||
..sampler_descriptor
|
||||
});
|
||||
|
||||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some(format!("egui_user_image_{}", self.next_user_texture_id).as_str()),
|
||||
layout: &self.texture_bind_group_layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(texture),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::Sampler(&sampler),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
*user_texture_binding = bind_group;
|
||||
}
|
||||
|
||||
/// Uploads the uniform, vertex and index data used by the renderer.
|
||||
/// Should be called before `render()`.
|
||||
///
|
||||
/// Returns all user-defined command buffers gathered from prepare callbacks.
|
||||
pub fn update_buffers(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
paint_jobs: &[epaint::ClippedPrimitive],
|
||||
screen_descriptor: &ScreenDescriptor,
|
||||
) -> Vec<wgpu::CommandBuffer> {
|
||||
crate::profile_function!();
|
||||
|
||||
let screen_size_in_points = screen_descriptor.screen_size_in_points();
|
||||
|
||||
{
|
||||
crate::profile_scope!("uniforms");
|
||||
// Update uniform buffer
|
||||
queue.write_buffer(
|
||||
&self.uniform_buffer,
|
||||
0,
|
||||
bytemuck::cast_slice(&[UniformBuffer {
|
||||
screen_size_in_points,
|
||||
_padding: Default::default(),
|
||||
}]),
|
||||
);
|
||||
}
|
||||
|
||||
// Determine how many vertices & indices need to be rendered.
|
||||
let (vertex_count, index_count) = {
|
||||
crate::profile_scope!("count_vertices_indices");
|
||||
paint_jobs.iter().fold((0, 0), |acc, clipped_primitive| {
|
||||
match &clipped_primitive.primitive {
|
||||
Primitive::Mesh(mesh) => {
|
||||
(acc.0 + mesh.vertices.len(), acc.1 + mesh.indices.len())
|
||||
}
|
||||
Primitive::Callback(_) => acc,
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
{
|
||||
// Resize index buffer if needed:
|
||||
self.index_buffer.slices.clear();
|
||||
let required_size = (std::mem::size_of::<u32>() * index_count) as u64;
|
||||
if self.index_buffer.capacity < required_size {
|
||||
self.index_buffer.capacity =
|
||||
(self.index_buffer.capacity * 2).at_least(required_size);
|
||||
self.index_buffer.buffer = create_index_buffer(device, self.index_buffer.capacity);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// Resize vertex buffer if needed:
|
||||
self.vertex_buffer.slices.clear();
|
||||
let required_size = (std::mem::size_of::<Vertex>() * vertex_count) as u64;
|
||||
if self.vertex_buffer.capacity < required_size {
|
||||
self.vertex_buffer.capacity =
|
||||
(self.vertex_buffer.capacity * 2).at_least(required_size);
|
||||
self.vertex_buffer.buffer =
|
||||
create_vertex_buffer(device, self.vertex_buffer.capacity);
|
||||
}
|
||||
}
|
||||
|
||||
// Upload index & vertex data and call user callbacks
|
||||
let mut user_cmd_bufs = Vec::new(); // collect user command buffers
|
||||
|
||||
crate::profile_scope!("primitives");
|
||||
for epaint::ClippedPrimitive { primitive, .. } in paint_jobs.iter() {
|
||||
match primitive {
|
||||
Primitive::Mesh(mesh) => {
|
||||
{
|
||||
let index_offset = self.index_buffer.slices.last().unwrap_or(&(0..0)).end;
|
||||
let data = bytemuck::cast_slice(&mesh.indices);
|
||||
queue.write_buffer(&self.index_buffer.buffer, index_offset, data);
|
||||
self.index_buffer
|
||||
.slices
|
||||
.push(index_offset..(data.len() as wgpu::BufferAddress + index_offset));
|
||||
}
|
||||
{
|
||||
let vertex_offset = self.vertex_buffer.slices.last().unwrap_or(&(0..0)).end;
|
||||
let data = bytemuck::cast_slice(&mesh.vertices);
|
||||
queue.write_buffer(&self.vertex_buffer.buffer, vertex_offset, data);
|
||||
self.vertex_buffer.slices.push(
|
||||
vertex_offset..(data.len() as wgpu::BufferAddress + vertex_offset),
|
||||
);
|
||||
}
|
||||
}
|
||||
Primitive::Callback(callback) => {
|
||||
let cbfn = if let Some(c) = callback.callback.downcast_ref::<CallbackFn>() {
|
||||
c
|
||||
} else {
|
||||
tracing::warn!("Unknown paint callback: expected `egui_wgpu::CallbackFn`");
|
||||
continue;
|
||||
};
|
||||
|
||||
crate::profile_scope!("callback");
|
||||
user_cmd_bufs.extend((cbfn.prepare)(
|
||||
device,
|
||||
queue,
|
||||
encoder,
|
||||
&mut self.paint_callback_resources,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
user_cmd_bufs
|
||||
}
|
||||
}
|
||||
|
||||
fn create_sampler(
|
||||
options: epaint::textures::TextureOptions,
|
||||
device: &wgpu::Device,
|
||||
) -> wgpu::Sampler {
|
||||
let mag_filter = match options.magnification {
|
||||
epaint::textures::TextureFilter::Nearest => wgpu::FilterMode::Nearest,
|
||||
epaint::textures::TextureFilter::Linear => wgpu::FilterMode::Linear,
|
||||
};
|
||||
let min_filter = match options.minification {
|
||||
epaint::textures::TextureFilter::Nearest => wgpu::FilterMode::Nearest,
|
||||
epaint::textures::TextureFilter::Linear => wgpu::FilterMode::Linear,
|
||||
};
|
||||
device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
label: Some(&format!(
|
||||
"egui sampler (mag: {:?}, min {:?})",
|
||||
mag_filter, min_filter
|
||||
)),
|
||||
mag_filter,
|
||||
min_filter,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn create_vertex_buffer(device: &wgpu::Device, size: u64) -> wgpu::Buffer {
|
||||
crate::profile_function!();
|
||||
device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("egui_vertex_buffer"),
|
||||
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
|
||||
size,
|
||||
mapped_at_creation: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn create_index_buffer(device: &wgpu::Device, size: u64) -> wgpu::Buffer {
|
||||
crate::profile_function!();
|
||||
device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("egui_index_buffer"),
|
||||
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
|
||||
size,
|
||||
mapped_at_creation: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// A Rect in physical pixel space, used for setting cliipping rectangles.
|
||||
struct ScissorRect {
|
||||
x: u32,
|
||||
y: u32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
}
|
||||
|
||||
impl ScissorRect {
|
||||
fn new(clip_rect: &epaint::Rect, pixels_per_point: f32, target_size: [u32; 2]) -> Self {
|
||||
// Transform clip rect to physical pixels:
|
||||
let clip_min_x = pixels_per_point * clip_rect.min.x;
|
||||
let clip_min_y = pixels_per_point * clip_rect.min.y;
|
||||
let clip_max_x = pixels_per_point * clip_rect.max.x;
|
||||
let clip_max_y = pixels_per_point * clip_rect.max.y;
|
||||
|
||||
// Round to integer:
|
||||
let clip_min_x = clip_min_x.round() as u32;
|
||||
let clip_min_y = clip_min_y.round() as u32;
|
||||
let clip_max_x = clip_max_x.round() as u32;
|
||||
let clip_max_y = clip_max_y.round() as u32;
|
||||
|
||||
// Clamp:
|
||||
let clip_min_x = clip_min_x.clamp(0, target_size[0]);
|
||||
let clip_min_y = clip_min_y.clamp(0, target_size[1]);
|
||||
let clip_max_x = clip_max_x.clamp(clip_min_x, target_size[0]);
|
||||
let clip_max_y = clip_max_y.clamp(clip_min_y, target_size[1]);
|
||||
|
||||
Self {
|
||||
x: clip_min_x,
|
||||
y: clip_min_y,
|
||||
width: clip_max_x - clip_min_x,
|
||||
height: clip_max_y - clip_min_y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renderer_impl_send_sync() {
|
||||
fn assert_send_sync<T: Send + Sync>() {}
|
||||
assert_send_sync::<Renderer>();
|
||||
}
|
418
crates/egui-wgpu/src/winit.rs
Normal file
418
crates/egui-wgpu/src/winit.rs
Normal file
|
@ -0,0 +1,418 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use epaint::mutex::RwLock;
|
||||
|
||||
use tracing::error;
|
||||
|
||||
use crate::{renderer, RenderState, Renderer, SurfaceErrorAction, WgpuConfiguration};
|
||||
|
||||
struct SurfaceState {
|
||||
surface: wgpu::Surface,
|
||||
alpha_mode: wgpu::CompositeAlphaMode,
|
||||
width: u32,
|
||||
height: u32,
|
||||
}
|
||||
|
||||
/// Everything you need to paint egui with [`wgpu`] on [`winit`].
|
||||
///
|
||||
/// Alternatively you can use [`crate::renderer`] directly.
|
||||
pub struct Painter {
|
||||
configuration: WgpuConfiguration,
|
||||
msaa_samples: u32,
|
||||
support_transparent_backbuffer: bool,
|
||||
depth_format: Option<wgpu::TextureFormat>,
|
||||
depth_texture_view: Option<wgpu::TextureView>,
|
||||
|
||||
instance: wgpu::Instance,
|
||||
adapter: Option<wgpu::Adapter>,
|
||||
render_state: Option<RenderState>,
|
||||
surface_state: Option<SurfaceState>,
|
||||
}
|
||||
|
||||
impl Painter {
|
||||
/// Manages [`wgpu`] state, including surface state, required to render egui.
|
||||
///
|
||||
/// Only the [`wgpu::Instance`] is initialized here. Device selection and the initialization
|
||||
/// of render + surface state is deferred until the painter is given its first window target
|
||||
/// via [`set_window()`](Self::set_window). (Ensuring that a device that's compatible with the
|
||||
/// native window is chosen)
|
||||
///
|
||||
/// Before calling [`paint_and_update_textures()`](Self::paint_and_update_textures) a
|
||||
/// [`wgpu::Surface`] must be initialized (and corresponding render state) by calling
|
||||
/// [`set_window()`](Self::set_window) once you have
|
||||
/// a [`winit::window::Window`] with a valid `.raw_window_handle()`
|
||||
/// associated.
|
||||
pub fn new(
|
||||
configuration: WgpuConfiguration,
|
||||
msaa_samples: u32,
|
||||
depth_bits: u8,
|
||||
support_transparent_backbuffer: bool,
|
||||
) -> Self {
|
||||
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
|
||||
backends: configuration.backends,
|
||||
dx12_shader_compiler: Default::default(), //
|
||||
});
|
||||
|
||||
Self {
|
||||
configuration,
|
||||
msaa_samples,
|
||||
support_transparent_backbuffer,
|
||||
depth_format: (depth_bits > 0).then_some(wgpu::TextureFormat::Depth32Float),
|
||||
depth_texture_view: None,
|
||||
|
||||
instance,
|
||||
adapter: None,
|
||||
render_state: None,
|
||||
surface_state: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the [`RenderState`].
|
||||
///
|
||||
/// Will return [`None`] if the render state has not been initialized yet.
|
||||
pub fn render_state(&self) -> Option<RenderState> {
|
||||
self.render_state.clone()
|
||||
}
|
||||
|
||||
async fn init_render_state(
|
||||
&self,
|
||||
adapter: &wgpu::Adapter,
|
||||
target_format: wgpu::TextureFormat,
|
||||
) -> Result<RenderState, wgpu::RequestDeviceError> {
|
||||
adapter
|
||||
.request_device(&self.configuration.device_descriptor, None)
|
||||
.await
|
||||
.map(|(device, queue)| {
|
||||
let renderer =
|
||||
Renderer::new(&device, target_format, self.depth_format, self.msaa_samples);
|
||||
RenderState {
|
||||
device: Arc::new(device),
|
||||
queue: Arc::new(queue),
|
||||
target_format,
|
||||
renderer: Arc::new(RwLock::new(renderer)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// We want to defer the initialization of our render state until we have a surface
|
||||
// so we can take its format into account.
|
||||
//
|
||||
// After we've initialized our render state once though we expect all future surfaces
|
||||
// will have the same format and so this render state will remain valid.
|
||||
async fn ensure_render_state_for_surface(
|
||||
&mut self,
|
||||
surface: &wgpu::Surface,
|
||||
) -> Result<(), wgpu::RequestDeviceError> {
|
||||
if self.adapter.is_none() {
|
||||
self.adapter = self
|
||||
.instance
|
||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||
power_preference: self.configuration.power_preference,
|
||||
compatible_surface: Some(surface),
|
||||
force_fallback_adapter: false,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
if self.render_state.is_none() {
|
||||
match &self.adapter {
|
||||
Some(adapter) => {
|
||||
let swapchain_format = crate::preferred_framebuffer_format(
|
||||
&surface.get_capabilities(adapter).formats,
|
||||
);
|
||||
let rs = self.init_render_state(adapter, swapchain_format).await?;
|
||||
self.render_state = Some(rs);
|
||||
}
|
||||
None => return Err(wgpu::RequestDeviceError {}),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn configure_surface(
|
||||
surface_state: &SurfaceState,
|
||||
render_state: &RenderState,
|
||||
present_mode: wgpu::PresentMode,
|
||||
) {
|
||||
surface_state.surface.configure(
|
||||
&render_state.device,
|
||||
&wgpu::SurfaceConfiguration {
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
format: render_state.target_format,
|
||||
width: surface_state.width,
|
||||
height: surface_state.height,
|
||||
present_mode,
|
||||
alpha_mode: surface_state.alpha_mode,
|
||||
view_formats: vec![render_state.target_format],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates (or clears) the [`winit::window::Window`] associated with the [`Painter`]
|
||||
///
|
||||
/// This creates a [`wgpu::Surface`] for the given Window (as well as initializing render
|
||||
/// state if needed) that is used for egui rendering.
|
||||
///
|
||||
/// This must be called before trying to render via
|
||||
/// [`paint_and_update_textures`](Self::paint_and_update_textures)
|
||||
///
|
||||
/// # Portability
|
||||
///
|
||||
/// _In particular it's important to note that on Android a it's only possible to create
|
||||
/// a window surface between `Resumed` and `Paused` lifecycle events, and Winit will panic on
|
||||
/// attempts to query the raw window handle while paused._
|
||||
///
|
||||
/// On Android [`set_window`](Self::set_window) should be called with `Some(window)` for each
|
||||
/// `Resumed` event and `None` for each `Paused` event. Currently, on all other platforms
|
||||
/// [`set_window`](Self::set_window) may be called with `Some(window)` as soon as you have a
|
||||
/// valid [`winit::window::Window`].
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The raw Window handle associated with the given `window` must be a valid object to create a
|
||||
/// surface upon and must remain valid for the lifetime of the created surface. (The surface may
|
||||
/// be cleared by passing `None`).
|
||||
///
|
||||
/// # Errors
|
||||
/// If the provided wgpu configuration does not match an available device.
|
||||
pub async unsafe fn set_window(
|
||||
&mut self,
|
||||
window: Option<&winit::window::Window>,
|
||||
) -> Result<(), crate::WgpuError> {
|
||||
match window {
|
||||
Some(window) => {
|
||||
let surface = self.instance.create_surface(&window)?;
|
||||
|
||||
self.ensure_render_state_for_surface(&surface).await?;
|
||||
|
||||
let alpha_mode = if self.support_transparent_backbuffer {
|
||||
let supported_alpha_modes = surface
|
||||
.get_capabilities(self.adapter.as_ref().unwrap())
|
||||
.alpha_modes;
|
||||
|
||||
// Prefer pre multiplied over post multiplied!
|
||||
if supported_alpha_modes.contains(&wgpu::CompositeAlphaMode::PreMultiplied) {
|
||||
wgpu::CompositeAlphaMode::PreMultiplied
|
||||
} else if supported_alpha_modes
|
||||
.contains(&wgpu::CompositeAlphaMode::PostMultiplied)
|
||||
{
|
||||
wgpu::CompositeAlphaMode::PostMultiplied
|
||||
} else {
|
||||
tracing::warn!("Transparent window was requested, but the active wgpu surface does not support a `CompositeAlphaMode` with transparency.");
|
||||
wgpu::CompositeAlphaMode::Auto
|
||||
}
|
||||
} else {
|
||||
wgpu::CompositeAlphaMode::Auto
|
||||
};
|
||||
|
||||
let size = window.inner_size();
|
||||
self.surface_state = Some(SurfaceState {
|
||||
surface,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
alpha_mode,
|
||||
});
|
||||
self.resize_and_generate_depth_texture_view(size.width, size.height);
|
||||
}
|
||||
None => {
|
||||
self.surface_state = None;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the maximum texture dimension supported if known
|
||||
///
|
||||
/// This API will only return a known dimension after `set_window()` has been called
|
||||
/// at least once, since the underlying device and render state are initialized lazily
|
||||
/// once we have a window (that may determine the choice of adapter/device).
|
||||
pub fn max_texture_side(&self) -> Option<usize> {
|
||||
self.render_state
|
||||
.as_ref()
|
||||
.map(|rs| rs.device.limits().max_texture_dimension_2d as usize)
|
||||
}
|
||||
|
||||
fn resize_and_generate_depth_texture_view(
|
||||
&mut self,
|
||||
width_in_pixels: u32,
|
||||
height_in_pixels: u32,
|
||||
) {
|
||||
let render_state = self.render_state.as_ref().unwrap();
|
||||
let surface_state = self.surface_state.as_mut().unwrap();
|
||||
|
||||
surface_state.width = width_in_pixels;
|
||||
surface_state.height = height_in_pixels;
|
||||
|
||||
Self::configure_surface(surface_state, render_state, self.configuration.present_mode);
|
||||
|
||||
self.depth_texture_view = self.depth_format.map(|depth_format| {
|
||||
render_state
|
||||
.device
|
||||
.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("egui_depth_texture"),
|
||||
size: wgpu::Extent3d {
|
||||
width: width_in_pixels,
|
||||
height: height_in_pixels,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: depth_format,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
|
||||
| wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[depth_format],
|
||||
})
|
||||
.create_view(&wgpu::TextureViewDescriptor::default())
|
||||
});
|
||||
}
|
||||
|
||||
pub fn on_window_resized(&mut self, width_in_pixels: u32, height_in_pixels: u32) {
|
||||
if self.surface_state.is_some() {
|
||||
self.resize_and_generate_depth_texture_view(width_in_pixels, height_in_pixels);
|
||||
} else {
|
||||
error!("Ignoring window resize notification with no surface created via Painter::set_window()");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn paint_and_update_textures(
|
||||
&mut self,
|
||||
pixels_per_point: f32,
|
||||
clear_color: [f32; 4],
|
||||
clipped_primitives: &[epaint::ClippedPrimitive],
|
||||
textures_delta: &epaint::textures::TexturesDelta,
|
||||
) {
|
||||
crate::profile_function!();
|
||||
|
||||
let render_state = match self.render_state.as_mut() {
|
||||
Some(rs) => rs,
|
||||
None => return,
|
||||
};
|
||||
let surface_state = match self.surface_state.as_ref() {
|
||||
Some(rs) => rs,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let output_frame = {
|
||||
crate::profile_scope!("get_current_texture");
|
||||
// This is what vsync-waiting happens, at least on Mac.
|
||||
surface_state.surface.get_current_texture()
|
||||
};
|
||||
|
||||
let output_frame = match output_frame {
|
||||
Ok(frame) => frame,
|
||||
#[allow(clippy::single_match_else)]
|
||||
Err(e) => match (*self.configuration.on_surface_error)(e) {
|
||||
SurfaceErrorAction::RecreateSurface => {
|
||||
Self::configure_surface(
|
||||
surface_state,
|
||||
render_state,
|
||||
self.configuration.present_mode,
|
||||
);
|
||||
return;
|
||||
}
|
||||
SurfaceErrorAction::SkipFrame => {
|
||||
return;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let mut encoder =
|
||||
render_state
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
label: Some("encoder"),
|
||||
});
|
||||
|
||||
// Upload all resources for the GPU.
|
||||
let screen_descriptor = renderer::ScreenDescriptor {
|
||||
size_in_pixels: [surface_state.width, surface_state.height],
|
||||
pixels_per_point,
|
||||
};
|
||||
|
||||
let user_cmd_bufs = {
|
||||
let mut renderer = render_state.renderer.write();
|
||||
for (id, image_delta) in &textures_delta.set {
|
||||
renderer.update_texture(
|
||||
&render_state.device,
|
||||
&render_state.queue,
|
||||
*id,
|
||||
image_delta,
|
||||
);
|
||||
}
|
||||
|
||||
renderer.update_buffers(
|
||||
&render_state.device,
|
||||
&render_state.queue,
|
||||
&mut encoder,
|
||||
clipped_primitives,
|
||||
&screen_descriptor,
|
||||
)
|
||||
};
|
||||
|
||||
{
|
||||
let renderer = render_state.renderer.read();
|
||||
let frame_view = output_frame
|
||||
.texture
|
||||
.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &frame_view,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color {
|
||||
r: clear_color[0] as f64,
|
||||
g: clear_color[1] as f64,
|
||||
b: clear_color[2] as f64,
|
||||
a: clear_color[3] as f64,
|
||||
}),
|
||||
store: true,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: self.depth_texture_view.as_ref().map(|view| {
|
||||
wgpu::RenderPassDepthStencilAttachment {
|
||||
view,
|
||||
depth_ops: Some(wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(1.0),
|
||||
store: true,
|
||||
}),
|
||||
stencil_ops: None,
|
||||
}
|
||||
}),
|
||||
label: Some("egui_render"),
|
||||
});
|
||||
|
||||
renderer.render(&mut render_pass, clipped_primitives, &screen_descriptor);
|
||||
}
|
||||
|
||||
{
|
||||
let mut renderer = render_state.renderer.write();
|
||||
for id in &textures_delta.free {
|
||||
renderer.free_texture(id);
|
||||
}
|
||||
}
|
||||
|
||||
let encoded = {
|
||||
crate::profile_scope!("CommandEncoder::finish");
|
||||
encoder.finish()
|
||||
};
|
||||
|
||||
// Submit the commands: both the main buffer and user-defined ones.
|
||||
{
|
||||
crate::profile_scope!("Queue::submit");
|
||||
render_state
|
||||
.queue
|
||||
.submit(user_cmd_bufs.into_iter().chain(std::iter::once(encoded)));
|
||||
};
|
||||
|
||||
// Redraw egui
|
||||
{
|
||||
crate::profile_scope!("present");
|
||||
output_frame.present();
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_self)]
|
||||
pub fn destroy(&mut self) {
|
||||
// TODO(emilk): something here?
|
||||
}
|
||||
}
|
64
crates/egui-winit/CHANGELOG.md
Normal file
64
crates/egui-winit/CHANGELOG.md
Normal file
|
@ -0,0 +1,64 @@
|
|||
# Changelog for egui-winit
|
||||
All notable changes to the `egui-winit` integration will be noted in this file.
|
||||
|
||||
|
||||
## Unreleased
|
||||
|
||||
|
||||
## 0.21.1 - 2023-02-12
|
||||
* Fixed crash when window position is in an invalid state, which could happen e.g. due to changes in monitor size or DPI ([#2722](https://github.com/emilk/egui/issues/2722)).
|
||||
|
||||
|
||||
## 0.21.0 - 2023-02-08
|
||||
* Fixed persistence of native window position on Windows OS ([#2583](https://github.com/emilk/egui/issues/2583)).
|
||||
* Update to `winit` 0.28, adding support for mac trackpad zoom ([#2654](https://github.com/emilk/egui/pull/2654)).
|
||||
* Remove the `screen_reader` feature. Use the `accesskit` feature flag instead ([#2669](https://github.com/emilk/egui/pull/2669)).
|
||||
* Fix bug where the cursor could get stuck using the wrong icon.
|
||||
|
||||
|
||||
## 0.20.1 - 2022-12-11
|
||||
* Fix [docs.rs](https://docs.rs/egui-winit) build ([#2420](https://github.com/emilk/egui/pull/2420)).
|
||||
|
||||
|
||||
## 0.20.0 - 2022-12-08
|
||||
* The default features of the `winit` crate are not enabled if the default features of `egui-winit` are disabled too ([#1971](https://github.com/emilk/egui/pull/1971)).
|
||||
* Added new feature `wayland` which enables Wayland support ([#1971](https://github.com/emilk/egui/pull/1971)).
|
||||
* Don't repaint when just moving window ([#1980](https://github.com/emilk/egui/pull/1980)).
|
||||
* Added optional integration with [AccessKit](https://accesskit.dev/) for implementing platform accessibility APIs ([#2294](https://github.com/emilk/egui/pull/2294)).
|
||||
|
||||
## 0.19.0 - 2022-08-20
|
||||
* MSRV (Minimum Supported Rust Version) is now `1.61.0` ([#1846](https://github.com/emilk/egui/pull/1846)).
|
||||
* Fixed clipboard on Wayland ([#1613](https://github.com/emilk/egui/pull/1613)).
|
||||
* Allow deferred render + surface state initialization for Android ([#1634](https://github.com/emilk/egui/pull/1634)).
|
||||
* Fixed window position persistence ([#1745](https://github.com/emilk/egui/pull/1745)).
|
||||
* Fixed mouse cursor change on Linux ([#1747](https://github.com/emilk/egui/pull/1747)).
|
||||
* Use the new `RawInput::has_focus` field to indicate whether the window has the keyboard focus ([#1859](https://github.com/emilk/egui/pull/1859)).
|
||||
|
||||
|
||||
## 0.18.0 - 2022-04-30
|
||||
* Reexport `egui` crate
|
||||
* MSRV (Minimum Supported Rust Version) is now `1.60.0` ([#1467](https://github.com/emilk/egui/pull/1467)).
|
||||
* Added new feature `puffin` to add [`puffin profiler`](https://github.com/EmbarkStudios/puffin) scopes ([#1483](https://github.com/emilk/egui/pull/1483)).
|
||||
* Renamed the feature `convert_bytemuck` to `bytemuck` ([#1467](https://github.com/emilk/egui/pull/1467)).
|
||||
* Renamed the feature `serialize` to `serde` ([#1467](https://github.com/emilk/egui/pull/1467)).
|
||||
* Removed the features `dark-light` and `persistence` ([#1542](https://github.com/emilk/egui/pull/1542)).
|
||||
|
||||
|
||||
## 0.17.0 - 2022-02-22
|
||||
* Fixed horizontal scrolling direction on Linux.
|
||||
* Replaced `std::time::Instant` with `instant::Instant` for WebAssembly compatability ([#1023](https://github.com/emilk/egui/pull/1023))
|
||||
* Automatically detect and apply dark or light mode from system ([#1045](https://github.com/emilk/egui/pull/1045)).
|
||||
* Fixed `enable_drag` on Windows OS ([#1108](https://github.com/emilk/egui/pull/1108)).
|
||||
* Shift-scroll will now result in horizontal scrolling on all platforms ([#1136](https://github.com/emilk/egui/pull/1136)).
|
||||
* Require knowledge about max texture side (e.g. `GL_MAX_TEXTURE_SIZE`)) ([#1154](https://github.com/emilk/egui/pull/1154)).
|
||||
|
||||
|
||||
## 0.16.0 - 2021-12-29
|
||||
* Added helper `EpiIntegration` ([#871](https://github.com/emilk/egui/pull/871)).
|
||||
* Fixed shift key getting stuck enabled with the X11 option `shift:both_capslock` enabled ([#849](https://github.com/emilk/egui/pull/849)).
|
||||
* Removed `State::is_quit_event` and `State::is_quit_shortcut` ([#881](https://github.com/emilk/egui/pull/881)).
|
||||
* Updated `winit` to 0.26 ([#930](https://github.com/emilk/egui/pull/930)).
|
||||
|
||||
|
||||
## 0.15.0 - 2021-10-24
|
||||
First stand-alone release. Previously part of `egui_glium`.
|
76
crates/egui-winit/Cargo.toml
Normal file
76
crates/egui-winit/Cargo.toml
Normal file
|
@ -0,0 +1,76 @@
|
|||
[package]
|
||||
name = "egui-winit"
|
||||
version = "0.21.1"
|
||||
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
||||
description = "Bindings for using egui with winit"
|
||||
edition = "2021"
|
||||
rust-version = "1.65"
|
||||
homepage = "https://github.com/emilk/egui/tree/master/crates/egui-winit"
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/emilk/egui/tree/master/crates/egui-winit"
|
||||
categories = ["gui", "game-development"]
|
||||
keywords = ["winit", "egui", "gui", "gamedev"]
|
||||
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
||||
|
||||
[features]
|
||||
default = ["clipboard", "links", "wayland", "winit/default"]
|
||||
|
||||
## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/).
|
||||
accesskit = ["accesskit_winit", "egui/accesskit"]
|
||||
|
||||
## [`bytemuck`](https://docs.rs/bytemuck) enables you to cast [`egui::epaint::Vertex`], [`egui::Vec2`] etc to `&[u8]`.
|
||||
bytemuck = ["egui/bytemuck"]
|
||||
|
||||
## Enable cut/copy/paste to OS clipboard.
|
||||
## If disabled a clipboard will be simulated so you can still copy/paste within the egui app.
|
||||
clipboard = ["arboard", "smithay-clipboard"]
|
||||
|
||||
## Enable opening links in a browser when an egui hyperlink is clicked.
|
||||
links = ["webbrowser"]
|
||||
|
||||
## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate.
|
||||
puffin = ["dep:puffin"]
|
||||
|
||||
## Allow serialization of [`WindowSettings`] using [`serde`](https://docs.rs/serde).
|
||||
serde = ["egui/serde", "dep:serde"]
|
||||
|
||||
## Enables Wayland support.
|
||||
wayland = ["winit/wayland"]
|
||||
|
||||
[dependencies]
|
||||
egui = { version = "0.21.0", path = "../egui", default-features = false, features = [
|
||||
"tracing",
|
||||
] }
|
||||
instant = { version = "0.1", features = [
|
||||
"wasm-bindgen",
|
||||
] } # We use instant so we can (maybe) compile for web
|
||||
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
||||
winit = { version = "0.28", default-features = false }
|
||||
|
||||
#! ### Optional dependencies
|
||||
|
||||
# feature accesskit
|
||||
accesskit_winit = { version = "0.10.0", optional = true }
|
||||
|
||||
## Enable this when generating docs.
|
||||
document-features = { version = "0.2", optional = true }
|
||||
|
||||
puffin = { version = "0.14", optional = true }
|
||||
serde = { version = "1.0", optional = true, features = ["derive"] }
|
||||
|
||||
webbrowser = { version = "0.8.3", optional = true }
|
||||
|
||||
[target.'cfg(any(target_os="linux", target_os="dragonfly", target_os="freebsd", target_os="netbsd", target_os="openbsd"))'.dependencies]
|
||||
smithay-clipboard = { version = "0.6.3", optional = true }
|
||||
|
||||
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||
arboard = { version = "3.2", optional = true, default-features = false }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
# TODO(emilk): this is probably not the right place for specifying native-activity, but we need to do it somewhere for the CI
|
||||
android-activity = { version = "0.4", features = ["native-activity"] }
|
|
@ -6,6 +6,6 @@
|
|||

|
||||

|
||||
|
||||
This crates provides bindings between [`egui`](https://github.com/emilk/egui) and [winit](https://crates.io/crates/winit).
|
||||
This crates provides bindings between [`egui`](https://github.com/emilk/egui) and [`winit`](https://crates.io/crates/winit).
|
||||
|
||||
The library translates winit events to egui, handled copy/paste, updates the cursor, open links clicked in egui, etc.
|
146
crates/egui-winit/src/clipboard.rs
Normal file
146
crates/egui-winit/src/clipboard.rs
Normal file
|
@ -0,0 +1,146 @@
|
|||
use std::os::raw::c_void;
|
||||
|
||||
/// Handles interfacing with the OS clipboard.
|
||||
///
|
||||
/// If the "clipboard" feature is off, or we cannot connect to the OS clipboard,
|
||||
/// then a fallback clipboard that just works works within the same app is used instead.
|
||||
pub struct Clipboard {
|
||||
#[cfg(all(feature = "arboard", not(target_os = "android")))]
|
||||
arboard: Option<arboard::Clipboard>,
|
||||
|
||||
#[cfg(all(
|
||||
any(
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
),
|
||||
feature = "smithay-clipboard"
|
||||
))]
|
||||
smithay: Option<smithay_clipboard::Clipboard>,
|
||||
|
||||
/// Fallback manual clipboard.
|
||||
clipboard: String,
|
||||
}
|
||||
|
||||
impl Clipboard {
|
||||
#[allow(unused_variables)]
|
||||
pub fn new(#[allow(unused_variables)] wayland_display: Option<*mut c_void>) -> Self {
|
||||
Self {
|
||||
#[cfg(all(feature = "arboard", not(target_os = "android")))]
|
||||
arboard: init_arboard(),
|
||||
|
||||
#[cfg(all(
|
||||
any(
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
),
|
||||
feature = "smithay-clipboard"
|
||||
))]
|
||||
smithay: init_smithay_clipboard(wayland_display),
|
||||
|
||||
clipboard: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&mut self) -> Option<String> {
|
||||
#[cfg(all(
|
||||
any(
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
),
|
||||
feature = "smithay-clipboard"
|
||||
))]
|
||||
if let Some(clipboard) = &mut self.smithay {
|
||||
return match clipboard.load() {
|
||||
Ok(text) => Some(text),
|
||||
Err(err) => {
|
||||
tracing::error!("smithay paste error: {err}");
|
||||
None
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "arboard", not(target_os = "android")))]
|
||||
if let Some(clipboard) = &mut self.arboard {
|
||||
return match clipboard.get_text() {
|
||||
Ok(text) => Some(text),
|
||||
Err(err) => {
|
||||
tracing::error!("arboard paste error: {err}");
|
||||
None
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Some(self.clipboard.clone())
|
||||
}
|
||||
|
||||
pub fn set(&mut self, text: String) {
|
||||
#[cfg(all(
|
||||
any(
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
),
|
||||
feature = "smithay-clipboard"
|
||||
))]
|
||||
if let Some(clipboard) = &mut self.smithay {
|
||||
clipboard.store(text);
|
||||
return;
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "arboard", not(target_os = "android")))]
|
||||
if let Some(clipboard) = &mut self.arboard {
|
||||
if let Err(err) = clipboard.set_text(text) {
|
||||
tracing::error!("arboard copy/cut error: {err}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
self.clipboard = text;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "arboard", not(target_os = "android")))]
|
||||
fn init_arboard() -> Option<arboard::Clipboard> {
|
||||
tracing::debug!("Initializing arboard clipboard…");
|
||||
match arboard::Clipboard::new() {
|
||||
Ok(clipboard) => Some(clipboard),
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to initialize arboard clipboard: {err}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
any(
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
),
|
||||
feature = "smithay-clipboard"
|
||||
))]
|
||||
fn init_smithay_clipboard(
|
||||
wayland_display: Option<*mut c_void>,
|
||||
) -> Option<smithay_clipboard::Clipboard> {
|
||||
if let Some(display) = wayland_display {
|
||||
tracing::debug!("Initializing smithay clipboard…");
|
||||
#[allow(unsafe_code)]
|
||||
Some(unsafe { smithay_clipboard::Clipboard::new(display) })
|
||||
} else {
|
||||
tracing::debug!("Cannot initialize smithay clipboard without a display handle");
|
||||
None
|
||||
}
|
||||
}
|
|
@ -2,18 +2,29 @@
|
|||
//!
|
||||
//! The library translates winit events to egui, handled copy/paste,
|
||||
//! updates the cursor, open links clicked in egui, etc.
|
||||
//!
|
||||
//! ## Feature flags
|
||||
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
|
||||
//!
|
||||
|
||||
#![allow(clippy::manual_range_contains)]
|
||||
|
||||
use std::os::raw::c_void;
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub use accesskit_winit;
|
||||
pub use egui;
|
||||
#[cfg(feature = "accesskit")]
|
||||
use egui::accesskit;
|
||||
pub use winit;
|
||||
|
||||
pub mod clipboard;
|
||||
pub mod screen_reader;
|
||||
mod window_settings;
|
||||
|
||||
pub use window_settings::WindowSettings;
|
||||
|
||||
use winit::event_loop::EventLoopWindowTarget;
|
||||
|
||||
pub fn native_pixels_per_point(window: &winit::window::Window) -> f32 {
|
||||
window.scale_factor() as f32
|
||||
}
|
||||
|
@ -23,18 +34,37 @@ pub fn screen_size_in_pixels(window: &winit::window::Window) -> egui::Vec2 {
|
|||
egui::vec2(size.width as f32, size.height as f32)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[must_use]
|
||||
pub struct EventResponse {
|
||||
/// If true, egui consumed this event, i.e. wants exclusive use of this event
|
||||
/// (e.g. a mouse click on an egui window, or entering text into a text field).
|
||||
///
|
||||
/// For instance, if you use egui for a game, you should only
|
||||
/// pass on the events to your game when [`Self::consumed`] is `false.
|
||||
///
|
||||
/// Note that egui uses `tab` to move focus between elements, so this will always be `true` for tabs.
|
||||
pub consumed: bool,
|
||||
|
||||
/// Do we need an egui refresh because of this event?
|
||||
pub repaint: bool,
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Handles the integration between egui and winit.
|
||||
pub struct State {
|
||||
start_time: instant::Instant,
|
||||
egui_input: egui::RawInput,
|
||||
pointer_pos_in_points: Option<egui::Pos2>,
|
||||
any_pointer_button_down: bool,
|
||||
current_cursor_icon: egui::CursorIcon,
|
||||
current_cursor_icon: Option<egui::CursorIcon>,
|
||||
|
||||
/// What egui uses.
|
||||
current_pixels_per_point: f32,
|
||||
|
||||
clipboard: clipboard::Clipboard,
|
||||
screen_reader: screen_reader::ScreenReader,
|
||||
|
||||
/// If `true`, mouse inputs will be treated as touches.
|
||||
/// Useful for debugging touch support in egui.
|
||||
|
@ -46,40 +76,78 @@ pub struct State {
|
|||
///
|
||||
/// Only one touch will be interpreted as pointer at any time.
|
||||
pointer_touch_id: Option<u64>,
|
||||
|
||||
/// track ime state
|
||||
input_method_editor_started: bool,
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
accesskit: Option<accesskit_winit::Adapter>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Initialize with:
|
||||
/// * `max_texture_side`: e.g. `GL_MAX_TEXTURE_SIZE`
|
||||
/// * the native `pixels_per_point` (dpi scaling).
|
||||
pub fn new(max_texture_side: usize, window: &winit::window::Window) -> Self {
|
||||
Self::from_pixels_per_point(max_texture_side, native_pixels_per_point(window))
|
||||
pub fn new<T>(event_loop: &EventLoopWindowTarget<T>) -> Self {
|
||||
Self::new_with_wayland_display(wayland_display(event_loop))
|
||||
}
|
||||
|
||||
/// Initialize with:
|
||||
/// * `max_texture_side`: e.g. `GL_MAX_TEXTURE_SIZE`
|
||||
/// * the given `pixels_per_point` (dpi scaling).
|
||||
pub fn from_pixels_per_point(max_texture_side: usize, pixels_per_point: f32) -> Self {
|
||||
pub fn new_with_wayland_display(wayland_display: Option<*mut c_void>) -> Self {
|
||||
let egui_input = egui::RawInput {
|
||||
has_focus: false, // winit will tell us when we have focus
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Self {
|
||||
start_time: instant::Instant::now(),
|
||||
egui_input: egui::RawInput {
|
||||
pixels_per_point: Some(pixels_per_point),
|
||||
max_texture_side: Some(max_texture_side),
|
||||
..Default::default()
|
||||
},
|
||||
egui_input,
|
||||
pointer_pos_in_points: None,
|
||||
any_pointer_button_down: false,
|
||||
current_cursor_icon: egui::CursorIcon::Default,
|
||||
current_pixels_per_point: pixels_per_point,
|
||||
current_cursor_icon: None,
|
||||
current_pixels_per_point: 1.0,
|
||||
|
||||
clipboard: Default::default(),
|
||||
screen_reader: screen_reader::ScreenReader::default(),
|
||||
clipboard: clipboard::Clipboard::new(wayland_display),
|
||||
|
||||
simulate_touch_screen: false,
|
||||
pointer_touch_id: None,
|
||||
|
||||
input_method_editor_started: false,
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
accesskit: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub fn init_accesskit<T: From<accesskit_winit::ActionRequestEvent> + Send>(
|
||||
&mut self,
|
||||
window: &winit::window::Window,
|
||||
event_loop_proxy: winit::event_loop::EventLoopProxy<T>,
|
||||
initial_tree_update_factory: impl 'static + FnOnce() -> accesskit::TreeUpdate + Send,
|
||||
) {
|
||||
self.accesskit = Some(accesskit_winit::Adapter::new(
|
||||
window,
|
||||
initial_tree_update_factory,
|
||||
event_loop_proxy,
|
||||
));
|
||||
}
|
||||
|
||||
/// Call this once a graphics context has been created to update the maximum texture dimensions
|
||||
/// that egui will use.
|
||||
pub fn set_max_texture_side(&mut self, max_texture_side: usize) {
|
||||
self.egui_input.max_texture_side = Some(max_texture_side);
|
||||
}
|
||||
|
||||
/// Call this when a new native Window is created for rendering to initialize the `pixels_per_point`
|
||||
/// for that window.
|
||||
///
|
||||
/// In particular, on Android it is necessary to call this after each `Resumed` lifecycle
|
||||
/// event, each time a new native window is created.
|
||||
///
|
||||
/// Once this has been initialized for a new window then this state will be maintained by handling
|
||||
/// [`winit::event::WindowEvent::ScaleFactorChanged`] events.
|
||||
pub fn set_pixels_per_point(&mut self, pixels_per_point: f32) {
|
||||
self.egui_input.pixels_per_point = Some(pixels_per_point);
|
||||
self.current_pixels_per_point = pixels_per_point;
|
||||
}
|
||||
|
||||
/// The number of physical pixels per logical point,
|
||||
/// as configured on the current egui context (see [`egui::Context::pixels_per_point`]).
|
||||
#[inline]
|
||||
|
@ -122,51 +190,63 @@ impl State {
|
|||
/// Call this when there is a new event.
|
||||
///
|
||||
/// The result can be found in [`Self::egui_input`] and be extracted with [`Self::take_egui_input`].
|
||||
///
|
||||
/// Returns `true` if egui wants exclusive use of this event
|
||||
/// (e.g. a mouse click on an egui window, or entering text into a text field).
|
||||
/// For instance, if you use egui for a game, you want to first call this
|
||||
/// and only when this returns `false` pass on the events to your game.
|
||||
///
|
||||
/// Note that egui uses `tab` to move focus between elements, so this will always return `true` for tabs.
|
||||
pub fn on_event(
|
||||
&mut self,
|
||||
egui_ctx: &egui::Context,
|
||||
event: &winit::event::WindowEvent<'_>,
|
||||
) -> bool {
|
||||
) -> EventResponse {
|
||||
use winit::event::WindowEvent;
|
||||
match event {
|
||||
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
|
||||
let pixels_per_point = *scale_factor as f32;
|
||||
self.egui_input.pixels_per_point = Some(pixels_per_point);
|
||||
self.current_pixels_per_point = pixels_per_point;
|
||||
false
|
||||
EventResponse {
|
||||
repaint: true,
|
||||
consumed: false,
|
||||
}
|
||||
}
|
||||
WindowEvent::MouseInput { state, button, .. } => {
|
||||
self.on_mouse_button_input(*state, *button);
|
||||
egui_ctx.wants_pointer_input()
|
||||
EventResponse {
|
||||
repaint: true,
|
||||
consumed: egui_ctx.wants_pointer_input(),
|
||||
}
|
||||
}
|
||||
WindowEvent::MouseWheel { delta, .. } => {
|
||||
self.on_mouse_wheel(*delta);
|
||||
egui_ctx.wants_pointer_input()
|
||||
EventResponse {
|
||||
repaint: true,
|
||||
consumed: egui_ctx.wants_pointer_input(),
|
||||
}
|
||||
}
|
||||
WindowEvent::CursorMoved { position, .. } => {
|
||||
self.on_cursor_moved(*position);
|
||||
egui_ctx.is_using_pointer()
|
||||
EventResponse {
|
||||
repaint: true,
|
||||
consumed: egui_ctx.is_using_pointer(),
|
||||
}
|
||||
}
|
||||
WindowEvent::CursorLeft { .. } => {
|
||||
self.pointer_pos_in_points = None;
|
||||
self.egui_input.events.push(egui::Event::PointerGone);
|
||||
false
|
||||
EventResponse {
|
||||
repaint: true,
|
||||
consumed: false,
|
||||
}
|
||||
}
|
||||
// WindowEvent::TouchpadPressure {device_id, pressure, stage, .. } => {} // TODO
|
||||
WindowEvent::Touch(touch) => {
|
||||
self.on_touch(touch);
|
||||
match touch.phase {
|
||||
let consumed = match touch.phase {
|
||||
winit::event::TouchPhase::Started
|
||||
| winit::event::TouchPhase::Ended
|
||||
| winit::event::TouchPhase::Cancelled => egui_ctx.wants_pointer_input(),
|
||||
winit::event::TouchPhase::Moved => egui_ctx.is_using_pointer(),
|
||||
};
|
||||
EventResponse {
|
||||
repaint: true,
|
||||
consumed,
|
||||
}
|
||||
}
|
||||
WindowEvent::ReceivedCharacter(ch) => {
|
||||
|
@ -175,36 +255,92 @@ impl State {
|
|||
let is_mac_cmd = cfg!(target_os = "macos")
|
||||
&& (self.egui_input.modifiers.ctrl || self.egui_input.modifiers.mac_cmd);
|
||||
|
||||
if is_printable_char(*ch) && !is_mac_cmd {
|
||||
let consumed = if is_printable_char(*ch) && !is_mac_cmd {
|
||||
self.egui_input
|
||||
.events
|
||||
.push(egui::Event::Text(ch.to_string()));
|
||||
egui_ctx.wants_keyboard_input()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
EventResponse {
|
||||
repaint: true,
|
||||
consumed,
|
||||
}
|
||||
}
|
||||
WindowEvent::Ime(ime) => {
|
||||
// on Mac even Cmd-C is preessed during ime, a `c` is pushed to Preedit.
|
||||
// So no need to check is_mac_cmd.
|
||||
//
|
||||
// How winit produce `Ime::Enabled` and `Ime::Disabled` differs in MacOS
|
||||
// and Windows.
|
||||
//
|
||||
// - On Windows, before and after each Commit will produce an Enable/Disabled
|
||||
// event.
|
||||
// - On MacOS, only when user explicit enable/disable ime. No Disabled
|
||||
// after Commit.
|
||||
//
|
||||
// We use input_method_editor_started to mannualy insert CompositionStart
|
||||
// between Commits.
|
||||
match ime {
|
||||
winit::event::Ime::Enabled | winit::event::Ime::Disabled => (),
|
||||
winit::event::Ime::Commit(text) => {
|
||||
self.input_method_editor_started = false;
|
||||
self.egui_input
|
||||
.events
|
||||
.push(egui::Event::CompositionEnd(text.clone()));
|
||||
}
|
||||
winit::event::Ime::Preedit(text, ..) => {
|
||||
if !self.input_method_editor_started {
|
||||
self.input_method_editor_started = true;
|
||||
self.egui_input.events.push(egui::Event::CompositionStart);
|
||||
}
|
||||
self.egui_input
|
||||
.events
|
||||
.push(egui::Event::CompositionUpdate(text.clone()));
|
||||
}
|
||||
};
|
||||
|
||||
EventResponse {
|
||||
repaint: true,
|
||||
consumed: egui_ctx.wants_keyboard_input(),
|
||||
}
|
||||
}
|
||||
WindowEvent::KeyboardInput { input, .. } => {
|
||||
self.on_keyboard_input(input);
|
||||
egui_ctx.wants_keyboard_input()
|
||||
|| input.virtual_keycode == Some(winit::event::VirtualKeyCode::Tab)
|
||||
let consumed = egui_ctx.wants_keyboard_input()
|
||||
|| input.virtual_keycode == Some(winit::event::VirtualKeyCode::Tab);
|
||||
EventResponse {
|
||||
repaint: true,
|
||||
consumed,
|
||||
}
|
||||
}
|
||||
WindowEvent::Focused(_) => {
|
||||
WindowEvent::Focused(has_focus) => {
|
||||
self.egui_input.has_focus = *has_focus;
|
||||
// We will not be given a KeyboardInput event when the modifiers are released while
|
||||
// the window does not have focus. Unset all modifier state to be safe.
|
||||
self.egui_input.modifiers = egui::Modifiers::default();
|
||||
false
|
||||
EventResponse {
|
||||
repaint: true,
|
||||
consumed: false,
|
||||
}
|
||||
}
|
||||
WindowEvent::HoveredFile(path) => {
|
||||
self.egui_input.hovered_files.push(egui::HoveredFile {
|
||||
path: Some(path.clone()),
|
||||
..Default::default()
|
||||
});
|
||||
false
|
||||
EventResponse {
|
||||
repaint: true,
|
||||
consumed: false,
|
||||
}
|
||||
}
|
||||
WindowEvent::HoveredFileCancelled => {
|
||||
self.egui_input.hovered_files.clear();
|
||||
false
|
||||
EventResponse {
|
||||
repaint: true,
|
||||
consumed: false,
|
||||
}
|
||||
}
|
||||
WindowEvent::DroppedFile(path) => {
|
||||
self.egui_input.hovered_files.clear();
|
||||
|
@ -212,7 +348,10 @@ impl State {
|
|||
path: Some(path.clone()),
|
||||
..Default::default()
|
||||
});
|
||||
false
|
||||
EventResponse {
|
||||
repaint: true,
|
||||
consumed: false,
|
||||
}
|
||||
}
|
||||
WindowEvent::ModifiersChanged(state) => {
|
||||
self.egui_input.modifiers.alt = state.alt();
|
||||
|
@ -224,15 +363,56 @@ impl State {
|
|||
} else {
|
||||
state.ctrl()
|
||||
};
|
||||
false
|
||||
EventResponse {
|
||||
repaint: true,
|
||||
consumed: false,
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// dbg!(event);
|
||||
false
|
||||
|
||||
// Things that may require repaint:
|
||||
WindowEvent::CloseRequested
|
||||
| WindowEvent::CursorEntered { .. }
|
||||
| WindowEvent::Destroyed
|
||||
| WindowEvent::Occluded(_)
|
||||
| WindowEvent::Resized(_)
|
||||
| WindowEvent::ThemeChanged(_)
|
||||
| WindowEvent::TouchpadPressure { .. } => EventResponse {
|
||||
repaint: true,
|
||||
consumed: false,
|
||||
},
|
||||
|
||||
// Things we completely ignore:
|
||||
WindowEvent::AxisMotion { .. }
|
||||
| WindowEvent::Moved(_)
|
||||
| WindowEvent::SmartMagnify { .. }
|
||||
| WindowEvent::TouchpadRotate { .. } => EventResponse {
|
||||
repaint: false,
|
||||
consumed: false,
|
||||
},
|
||||
|
||||
WindowEvent::TouchpadMagnify { delta, .. } => {
|
||||
// Positive delta values indicate magnification (zooming in).
|
||||
// Negative delta values indicate shrinking (zooming out).
|
||||
let zoom_factor = (*delta as f32).exp();
|
||||
self.egui_input.events.push(egui::Event::Zoom(zoom_factor));
|
||||
EventResponse {
|
||||
repaint: true,
|
||||
consumed: egui_ctx.wants_pointer_input(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Call this when there is a new [`accesskit::ActionRequest`].
|
||||
///
|
||||
/// The result can be found in [`Self::egui_input`] and be extracted with [`Self::take_egui_input`].
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub fn on_accesskit_action_request(&mut self, request: accesskit::ActionRequest) {
|
||||
self.egui_input
|
||||
.events
|
||||
.push(egui::Event::AccessKitActionRequest(request));
|
||||
}
|
||||
|
||||
fn on_mouse_button_input(
|
||||
&mut self,
|
||||
state: winit::event::ElementState,
|
||||
|
@ -369,7 +549,7 @@ impl State {
|
|||
}
|
||||
|
||||
fn on_mouse_wheel(&mut self, delta: winit::event::MouseScrollDelta) {
|
||||
let mut delta = match delta {
|
||||
let delta = match delta {
|
||||
winit::event::MouseScrollDelta::LineDelta(x, y) => {
|
||||
let points_per_scroll_line = 50.0; // Scroll speed decided by consensus: https://github.com/emilk/egui/issues/461
|
||||
egui::vec2(x, y) * points_per_scroll_line
|
||||
|
@ -379,8 +559,6 @@ impl State {
|
|||
}
|
||||
};
|
||||
|
||||
delta.x *= -1.0; // Winit has inverted hscroll. Remove this line when we update winit after https://github.com/rust-windowing/winit/pull/2105 is merged and released
|
||||
|
||||
if self.egui_input.modifiers.ctrl || self.egui_input.modifiers.command {
|
||||
// Treat as zoom instead:
|
||||
let factor = (delta.y / 200.0).exp();
|
||||
|
@ -421,6 +599,7 @@ impl State {
|
|||
self.egui_input.events.push(egui::Event::Key {
|
||||
key,
|
||||
pressed,
|
||||
repeat: false, // egui will fill this in for us!
|
||||
modifiers: self.egui_input.modifiers,
|
||||
});
|
||||
}
|
||||
|
@ -441,11 +620,6 @@ impl State {
|
|||
egui_ctx: &egui::Context,
|
||||
platform_output: egui::PlatformOutput,
|
||||
) {
|
||||
if egui_ctx.options().screen_reader {
|
||||
self.screen_reader
|
||||
.speak(&platform_output.events_description());
|
||||
}
|
||||
|
||||
let egui::PlatformOutput {
|
||||
cursor_icon,
|
||||
open_url,
|
||||
|
@ -453,8 +627,9 @@ impl State {
|
|||
events: _, // handled above
|
||||
mutable_text_under_cursor: _, // only used in eframe web
|
||||
text_cursor_pos,
|
||||
#[cfg(feature = "accesskit")]
|
||||
accesskit_update,
|
||||
} = platform_output;
|
||||
|
||||
self.current_pixels_per_point = egui_ctx.pixels_per_point(); // someone can have changed it to scale the UI
|
||||
|
||||
self.set_cursor_icon(window, cursor_icon);
|
||||
|
@ -470,24 +645,35 @@ impl State {
|
|||
if let Some(egui::Pos2 { x, y }) = text_cursor_pos {
|
||||
window.set_ime_position(winit::dpi::LogicalPosition { x, y });
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
if let Some(accesskit) = self.accesskit.as_ref() {
|
||||
if let Some(update) = accesskit_update {
|
||||
accesskit.update_if_active(|| update);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_cursor_icon(&mut self, window: &winit::window::Window, cursor_icon: egui::CursorIcon) {
|
||||
// prevent flickering near frame boundary when Windows OS tries to control cursor icon for window resizing
|
||||
if self.current_cursor_icon == cursor_icon {
|
||||
if self.current_cursor_icon == Some(cursor_icon) {
|
||||
// Prevent flickering near frame boundary when Windows OS tries to control cursor icon for window resizing.
|
||||
// On other platforms: just early-out to save CPU.
|
||||
return;
|
||||
}
|
||||
self.current_cursor_icon = cursor_icon;
|
||||
|
||||
if let Some(cursor_icon) = translate_cursor(cursor_icon) {
|
||||
window.set_cursor_visible(true);
|
||||
let is_pointer_in_window = self.pointer_pos_in_points.is_some();
|
||||
if is_pointer_in_window {
|
||||
self.current_cursor_icon = Some(cursor_icon);
|
||||
|
||||
let is_pointer_in_window = self.pointer_pos_in_points.is_some();
|
||||
if is_pointer_in_window {
|
||||
window.set_cursor_icon(cursor_icon);
|
||||
if let Some(winit_cursor_icon) = translate_cursor(cursor_icon) {
|
||||
window.set_cursor_visible(true);
|
||||
window.set_cursor_icon(winit_cursor_icon);
|
||||
} else {
|
||||
window.set_cursor_visible(false);
|
||||
}
|
||||
} else {
|
||||
window.set_cursor_visible(false);
|
||||
// Remember to set the cursor again once the cursor returns to the screen:
|
||||
self.current_cursor_icon = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -542,6 +728,8 @@ fn translate_mouse_button(button: winit::event::MouseButton) -> Option<egui::Poi
|
|||
winit::event::MouseButton::Left => Some(egui::PointerButton::Primary),
|
||||
winit::event::MouseButton::Right => Some(egui::PointerButton::Secondary),
|
||||
winit::event::MouseButton::Middle => Some(egui::PointerButton::Middle),
|
||||
winit::event::MouseButton::Other(1) => Some(egui::PointerButton::Extra1),
|
||||
winit::event::MouseButton::Other(2) => Some(egui::PointerButton::Extra2),
|
||||
winit::event::MouseButton::Other(_) => None,
|
||||
}
|
||||
}
|
||||
|
@ -569,6 +757,11 @@ fn translate_virtual_key_code(key: winit::event::VirtualKeyCode) -> Option<egui:
|
|||
VirtualKeyCode::PageUp => Key::PageUp,
|
||||
VirtualKeyCode::PageDown => Key::PageDown,
|
||||
|
||||
VirtualKeyCode::Minus => Key::Minus,
|
||||
// Using Mac the key with the Plus sign on it is reported as the Equals key
|
||||
// (with both English and Swedish keyboard).
|
||||
VirtualKeyCode::Equals => Key::PlusEquals,
|
||||
|
||||
VirtualKeyCode::Key0 | VirtualKeyCode::Numpad0 => Key::Num0,
|
||||
VirtualKeyCode::Key1 | VirtualKeyCode::Numpad1 => Key::Num1,
|
||||
VirtualKeyCode::Key2 | VirtualKeyCode::Numpad2 => Key::Num2,
|
||||
|
@ -607,6 +800,27 @@ fn translate_virtual_key_code(key: winit::event::VirtualKeyCode) -> Option<egui:
|
|||
VirtualKeyCode::Y => Key::Y,
|
||||
VirtualKeyCode::Z => Key::Z,
|
||||
|
||||
VirtualKeyCode::F1 => Key::F1,
|
||||
VirtualKeyCode::F2 => Key::F2,
|
||||
VirtualKeyCode::F3 => Key::F3,
|
||||
VirtualKeyCode::F4 => Key::F4,
|
||||
VirtualKeyCode::F5 => Key::F5,
|
||||
VirtualKeyCode::F6 => Key::F6,
|
||||
VirtualKeyCode::F7 => Key::F7,
|
||||
VirtualKeyCode::F8 => Key::F8,
|
||||
VirtualKeyCode::F9 => Key::F9,
|
||||
VirtualKeyCode::F10 => Key::F10,
|
||||
VirtualKeyCode::F11 => Key::F11,
|
||||
VirtualKeyCode::F12 => Key::F12,
|
||||
VirtualKeyCode::F13 => Key::F13,
|
||||
VirtualKeyCode::F14 => Key::F14,
|
||||
VirtualKeyCode::F15 => Key::F15,
|
||||
VirtualKeyCode::F16 => Key::F16,
|
||||
VirtualKeyCode::F17 => Key::F17,
|
||||
VirtualKeyCode::F18 => Key::F18,
|
||||
VirtualKeyCode::F19 => Key::F19,
|
||||
VirtualKeyCode::F20 => Key::F20,
|
||||
|
||||
_ => {
|
||||
return None;
|
||||
}
|
||||
|
@ -657,6 +871,28 @@ fn translate_cursor(cursor_icon: egui::CursorIcon) -> Option<winit::window::Curs
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns a Wayland display handle if the target is running Wayland
|
||||
fn wayland_display<T>(_event_loop: &EventLoopWindowTarget<T>) -> Option<*mut c_void> {
|
||||
#[cfg(feature = "wayland")]
|
||||
#[cfg(any(
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
))]
|
||||
{
|
||||
use winit::platform::wayland::EventLoopWindowTargetExtWayland as _;
|
||||
return _event_loop.wayland_display();
|
||||
}
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
{
|
||||
let _ = _event_loop;
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Profiling macro for feature "puffin"
|
||||
|
@ -667,6 +903,7 @@ macro_rules! profile_function {
|
|||
puffin::profile_function!($($arg)*);
|
||||
};
|
||||
}
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub(crate) use profile_function;
|
||||
|
||||
|
@ -678,5 +915,6 @@ macro_rules! profile_scope {
|
|||
puffin::profile_scope!($($arg)*);
|
||||
};
|
||||
}
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub(crate) use profile_scope;
|
144
crates/egui-winit/src/window_settings.rs
Normal file
144
crates/egui-winit/src/window_settings.rs
Normal file
|
@ -0,0 +1,144 @@
|
|||
/// Can be used to store native window settings (position and size).
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct WindowSettings {
|
||||
/// Position of window in physical pixels. This is either
|
||||
/// the inner or outer position depending on the platform.
|
||||
/// See [`winit::window::WindowBuilder::with_position`] for details.
|
||||
position: Option<egui::Pos2>,
|
||||
|
||||
fullscreen: bool,
|
||||
|
||||
/// Inner size of window in logical pixels
|
||||
inner_size_points: Option<egui::Vec2>,
|
||||
}
|
||||
|
||||
impl WindowSettings {
|
||||
pub fn from_display(window: &winit::window::Window) -> Self {
|
||||
let inner_size_points = window.inner_size().to_logical::<f32>(window.scale_factor());
|
||||
let position = if cfg!(macos) {
|
||||
// MacOS uses inner position when positioning windows.
|
||||
window
|
||||
.inner_position()
|
||||
.ok()
|
||||
.map(|p| egui::pos2(p.x as f32, p.y as f32))
|
||||
} else {
|
||||
// Other platforms use the outer position.
|
||||
window
|
||||
.outer_position()
|
||||
.ok()
|
||||
.map(|p| egui::pos2(p.x as f32, p.y as f32))
|
||||
};
|
||||
|
||||
Self {
|
||||
position,
|
||||
|
||||
fullscreen: window.fullscreen().is_some(),
|
||||
|
||||
inner_size_points: Some(egui::vec2(
|
||||
inner_size_points.width,
|
||||
inner_size_points.height,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inner_size_points(&self) -> Option<egui::Vec2> {
|
||||
self.inner_size_points
|
||||
}
|
||||
|
||||
pub fn initialize_window(
|
||||
&self,
|
||||
mut window: winit::window::WindowBuilder,
|
||||
) -> winit::window::WindowBuilder {
|
||||
// If the app last ran on two monitors and only one is now connected, then
|
||||
// the given position is invalid.
|
||||
// If this happens on Mac, the window is clamped into valid area.
|
||||
// If this happens on Windows, the clamping behavior is managed by the function
|
||||
// clamp_window_to_sane_position.
|
||||
if let Some(pos) = self.position {
|
||||
window = window.with_position(winit::dpi::PhysicalPosition {
|
||||
x: pos.x as f64,
|
||||
y: pos.y as f64,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(inner_size_points) = self.inner_size_points {
|
||||
window
|
||||
.with_inner_size(winit::dpi::LogicalSize {
|
||||
width: inner_size_points.x as f64,
|
||||
height: inner_size_points.y as f64,
|
||||
})
|
||||
.with_fullscreen(
|
||||
self.fullscreen
|
||||
.then_some(winit::window::Fullscreen::Borderless(None)),
|
||||
)
|
||||
} else {
|
||||
window
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clamp_to_sane_values(&mut self, max_size: egui::Vec2) {
|
||||
use egui::NumExt as _;
|
||||
|
||||
if let Some(size) = &mut self.inner_size_points {
|
||||
// Prevent ridiculously small windows
|
||||
let min_size = egui::Vec2::splat(64.0);
|
||||
*size = size.at_least(min_size);
|
||||
*size = size.at_most(max_size);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clamp_window_to_sane_position<E>(
|
||||
&mut self,
|
||||
event_loop: &winit::event_loop::EventLoopWindowTarget<E>,
|
||||
) {
|
||||
if let (Some(position), Some(inner_size_points)) =
|
||||
(&mut self.position, &self.inner_size_points)
|
||||
{
|
||||
let monitors = event_loop.available_monitors();
|
||||
// default to primary monitor, in case the correct monitor was disconnected.
|
||||
let mut active_monitor = if let Some(active_monitor) = event_loop
|
||||
.primary_monitor()
|
||||
.or_else(|| event_loop.available_monitors().next())
|
||||
{
|
||||
active_monitor
|
||||
} else {
|
||||
return; // no monitors 🤷
|
||||
};
|
||||
for monitor in monitors {
|
||||
let monitor_x_range = (monitor.position().x - inner_size_points.x as i32)
|
||||
..(monitor.position().x + monitor.size().width as i32);
|
||||
let monitor_y_range = (monitor.position().y - inner_size_points.y as i32)
|
||||
..(monitor.position().y + monitor.size().height as i32);
|
||||
|
||||
if monitor_x_range.contains(&(position.x as i32))
|
||||
&& monitor_y_range.contains(&(position.y as i32))
|
||||
{
|
||||
active_monitor = monitor;
|
||||
}
|
||||
}
|
||||
|
||||
let mut inner_size_pixels = *inner_size_points * (active_monitor.scale_factor() as f32);
|
||||
// Add size of title bar. This is 32 px by default in Win 10/11.
|
||||
if cfg!(target_os = "windows") {
|
||||
inner_size_pixels +=
|
||||
egui::Vec2::new(0.0, 32.0 * active_monitor.scale_factor() as f32);
|
||||
}
|
||||
let monitor_position = egui::Pos2::new(
|
||||
active_monitor.position().x as f32,
|
||||
active_monitor.position().y as f32,
|
||||
);
|
||||
let monitor_size = egui::Vec2::new(
|
||||
active_monitor.size().width as f32,
|
||||
active_monitor.size().height as f32,
|
||||
);
|
||||
|
||||
// Window size cannot be negative or the subsequent `clamp` will panic.
|
||||
let window_size = (monitor_size - inner_size_pixels).max(egui::Vec2::ZERO);
|
||||
// To get the maximum position, we get the rightmost corner of the display, then
|
||||
// subtract the size of the window to get the bottom right most value window.position
|
||||
// can have.
|
||||
*position = position.clamp(monitor_position, monitor_position + window_size);
|
||||
}
|
||||
}
|
||||
}
|
84
crates/egui/Cargo.toml
Normal file
84
crates/egui/Cargo.toml
Normal file
|
@ -0,0 +1,84 @@
|
|||
[package]
|
||||
name = "egui"
|
||||
version = "0.21.0"
|
||||
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
||||
description = "An easy-to-use immediate mode GUI that runs on both web and native"
|
||||
edition = "2021"
|
||||
rust-version = "1.65"
|
||||
homepage = "https://github.com/emilk/egui"
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "../../README.md"
|
||||
repository = "https://github.com/emilk/egui"
|
||||
categories = ["gui", "game-development"]
|
||||
keywords = ["gui", "imgui", "immediate", "portable", "gamedev"]
|
||||
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
||||
[lib]
|
||||
|
||||
|
||||
[features]
|
||||
default = ["default_fonts"]
|
||||
|
||||
## [`bytemuck`](https://docs.rs/bytemuck) enables you to cast [`epaint::Vertex`], [`emath::Vec2`] etc to `&[u8]`.
|
||||
bytemuck = ["epaint/bytemuck"]
|
||||
|
||||
## [`cint`](https://docs.rs/cint) enables interopability with other color libraries.
|
||||
cint = ["epaint/cint"]
|
||||
|
||||
## Enable the [`hex_color`] macro.
|
||||
color-hex = ["epaint/color-hex"]
|
||||
|
||||
## This will automatically detect deadlocks due to double-locking on the same thread.
|
||||
## If your app freezes, you may want to enable this!
|
||||
## Only affects [`epaint::mutex::RwLock`] (which egui uses a lot).
|
||||
deadlock_detection = ["epaint/deadlock_detection"]
|
||||
|
||||
## If set, egui will use `include_bytes!` to bundle some fonts.
|
||||
## If you plan on specifying your own fonts you may disable this feature.
|
||||
default_fonts = ["epaint/default_fonts"]
|
||||
|
||||
## Enable additional checks if debug assertions are enabled (debug builds).
|
||||
extra_debug_asserts = ["epaint/extra_debug_asserts"]
|
||||
## Always enable additional checks.
|
||||
extra_asserts = ["epaint/extra_asserts"]
|
||||
|
||||
## [`mint`](https://docs.rs/mint) enables interopability with other math libraries such as [`glam`](https://docs.rs/glam) and [`nalgebra`](https://docs.rs/nalgebra).
|
||||
mint = ["epaint/mint"]
|
||||
|
||||
## Enable persistence of memory (window positions etc).
|
||||
persistence = ["serde", "epaint/serde", "ron"]
|
||||
|
||||
## Allow serialization using [`serde`](https://docs.rs/serde).
|
||||
serde = ["dep:serde", "epaint/serde", "accesskit?/serde"]
|
||||
|
||||
## Change Vertex layout to be compatible with unity
|
||||
unity = ["epaint/unity"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
epaint = { version = "0.21.0", path = "../epaint", default-features = false }
|
||||
|
||||
ahash = { version = "0.8.1", default-features = false, features = [
|
||||
"no-rng", # we don't need DOS-protection, so we let users opt-in to it instead
|
||||
"std",
|
||||
] }
|
||||
nohash-hasher = "0.2"
|
||||
|
||||
#! ### Optional dependencies
|
||||
## Exposes detailed accessibility implementation required by platform
|
||||
## accessibility APIs. Also requires support in the egui integration.
|
||||
accesskit = { version = "0.9.0", optional = true }
|
||||
|
||||
## Enable this when generating docs.
|
||||
document-features = { version = "0.2", optional = true }
|
||||
|
||||
ron = { version = "0.8", optional = true }
|
||||
serde = { version = "1", optional = true, features = ["derive", "rc"] }
|
||||
|
||||
# egui doesn't log much, but when it does, it uses [`tracing`](https://docs.rs/tracing).
|
||||
tracing = { version = "0.1", optional = true, default-features = false, features = [
|
||||
"std",
|
||||
] }
|
|
@ -2,6 +2,6 @@ There are no stand-alone egui examples, because egui is not stand-alone!
|
|||
|
||||
See the top-level [examples](https://github.com/emilk/egui/tree/master/examples/) folder instead.
|
||||
|
||||
There are also plenty of examples in [the online demo](https://www.egui.rs/#demo). You can find the source code for it at <https://github.com/emilk/egui/tree/master/egui_demo_lib>.
|
||||
There are also plenty of examples in [the online demo](https://www.egui.rs/#demo). You can find the source code for it at <https://github.com/emilk/egui/tree/master/crates/egui_demo_lib>.
|
||||
|
||||
To learn how to set up `eframe` for web and native, go to <https://github.com/emilk/eframe_template/> and follow the instructions there!
|
|
@ -9,6 +9,7 @@ pub(crate) struct AnimationManager {
|
|||
#[derive(Clone, Debug)]
|
||||
struct BoolAnim {
|
||||
value: bool,
|
||||
|
||||
/// when did `value` last toggle?
|
||||
toggle_time: f64,
|
||||
}
|
||||
|
@ -16,7 +17,9 @@ struct BoolAnim {
|
|||
#[derive(Clone, Debug)]
|
||||
struct ValueAnim {
|
||||
from_value: f32,
|
||||
|
||||
to_value: f32,
|
||||
|
||||
/// when did `value` last toggle?
|
||||
toggle_time: f64,
|
||||
}
|
|
@ -2,17 +2,17 @@
|
|||
//! It has no frame or own size. It is potentially movable.
|
||||
//! It is the foundation for windows and popups.
|
||||
|
||||
use std::{fmt::Debug, hash::Hash};
|
||||
|
||||
use crate::*;
|
||||
|
||||
/// State that is persisted between frames.
|
||||
// TODO: this is not currently stored in `memory().data`, but maybe it should be?
|
||||
// TODO(emilk): this is not currently stored in `Memory::data`, but maybe it should be?
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub(crate) struct State {
|
||||
/// Last known pos
|
||||
pub pos: Pos2,
|
||||
/// Last known pos of the pivot
|
||||
pub pivot_pos: Pos2,
|
||||
|
||||
pub pivot: Align2,
|
||||
|
||||
/// Last know size. Used for catching clicks.
|
||||
pub size: Vec2,
|
||||
|
@ -23,8 +23,22 @@ pub(crate) struct State {
|
|||
}
|
||||
|
||||
impl State {
|
||||
pub fn left_top_pos(&self) -> Pos2 {
|
||||
pos2(
|
||||
self.pivot_pos.x - self.pivot.x().to_factor() * self.size.x,
|
||||
self.pivot_pos.y - self.pivot.y().to_factor() * self.size.y,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_left_top_pos(&mut self, pos: Pos2) {
|
||||
self.pivot_pos = pos2(
|
||||
pos.x + self.pivot.x().to_factor() * self.size.x,
|
||||
pos.y + self.pivot.y().to_factor() * self.size.y,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn rect(&self) -> Rect {
|
||||
Rect::from_min_size(self.pos, self.size)
|
||||
Rect::from_min_size(self.left_top_pos(), self.size)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -40,6 +54,7 @@ impl State {
|
|||
/// ui.label("Floating text!");
|
||||
/// });
|
||||
/// # });
|
||||
/// ```
|
||||
#[must_use = "You should call .show()"]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Area {
|
||||
|
@ -47,23 +62,27 @@ pub struct Area {
|
|||
movable: bool,
|
||||
interactable: bool,
|
||||
enabled: bool,
|
||||
constrain: bool,
|
||||
order: Order,
|
||||
default_pos: Option<Pos2>,
|
||||
pivot: Align2,
|
||||
anchor: Option<(Align2, Vec2)>,
|
||||
new_pos: Option<Pos2>,
|
||||
drag_bounds: Option<Rect>,
|
||||
}
|
||||
|
||||
impl Area {
|
||||
pub fn new(id_source: impl Hash) -> Self {
|
||||
pub fn new(id: impl Into<Id>) -> Self {
|
||||
Self {
|
||||
id: Id::new(id_source),
|
||||
id: id.into(),
|
||||
movable: true,
|
||||
interactable: true,
|
||||
constrain: false,
|
||||
enabled: true,
|
||||
order: Order::Middle,
|
||||
default_pos: None,
|
||||
new_pos: None,
|
||||
pivot: Align2::LEFT_TOP,
|
||||
anchor: None,
|
||||
drag_bounds: None,
|
||||
}
|
||||
|
@ -128,6 +147,24 @@ impl Area {
|
|||
self
|
||||
}
|
||||
|
||||
/// Constrains this area to the screen bounds.
|
||||
pub fn constrain(mut self, constrain: bool) -> Self {
|
||||
self.constrain = constrain;
|
||||
self
|
||||
}
|
||||
|
||||
/// Where the "root" of the area is.
|
||||
///
|
||||
/// For instance, if you set this to [`Align2::RIGHT_TOP`]
|
||||
/// then [`Self::fixed_pos`] will set the position of the right-top
|
||||
/// corner of the area.
|
||||
///
|
||||
/// Default: [`Align2::LEFT_TOP`].
|
||||
pub fn pivot(mut self, pivot: Align2) -> Self {
|
||||
self.pivot = pivot;
|
||||
self
|
||||
}
|
||||
|
||||
/// Positions the window but you can still move it.
|
||||
pub fn current_pos(mut self, current_pos: impl Into<Pos2>) -> Self {
|
||||
self.new_pos = Some(current_pos.into());
|
||||
|
@ -168,9 +205,16 @@ impl Area {
|
|||
pub(crate) struct Prepared {
|
||||
layer_id: LayerId,
|
||||
state: State,
|
||||
pub(crate) movable: bool,
|
||||
move_response: Response,
|
||||
enabled: bool,
|
||||
drag_bounds: Option<Rect>,
|
||||
|
||||
/// We always make windows invisible the first frame to hide "first-frame-jitters".
|
||||
///
|
||||
/// This is so that we use the first frame to calculate the window size,
|
||||
/// and then can correctly position the window and its contents the next frame,
|
||||
/// without having one frame where the window is wrongly positioned or sized.
|
||||
temporarily_invisible: bool,
|
||||
}
|
||||
|
||||
impl Area {
|
||||
|
@ -195,43 +239,95 @@ impl Area {
|
|||
enabled,
|
||||
default_pos,
|
||||
new_pos,
|
||||
pivot,
|
||||
anchor,
|
||||
drag_bounds,
|
||||
constrain,
|
||||
} = self;
|
||||
|
||||
let layer_id = LayerId::new(order, id);
|
||||
|
||||
let state = ctx.memory().areas.get(id).cloned();
|
||||
let state = ctx.memory(|mem| mem.areas.get(id).copied());
|
||||
let is_new = state.is_none();
|
||||
if is_new {
|
||||
ctx.request_repaint(); // if we don't know the previous size we are likely drawing the area in the wrong place
|
||||
}
|
||||
let mut state = state.unwrap_or_else(|| State {
|
||||
pos: default_pos.unwrap_or_else(|| automatic_area_position(ctx)),
|
||||
pivot_pos: default_pos.unwrap_or_else(|| automatic_area_position(ctx)),
|
||||
pivot,
|
||||
size: Vec2::ZERO,
|
||||
interactable,
|
||||
});
|
||||
state.pos = new_pos.unwrap_or(state.pos);
|
||||
state.pivot_pos = new_pos.unwrap_or(state.pivot_pos);
|
||||
state.interactable = interactable;
|
||||
|
||||
if let Some((anchor, offset)) = anchor {
|
||||
if is_new {
|
||||
// unknown size
|
||||
ctx.request_repaint();
|
||||
} else {
|
||||
let screen = ctx.available_rect();
|
||||
state.pos = anchor.align_size_within_rect(state.size, screen).min + offset;
|
||||
}
|
||||
let screen = ctx.available_rect();
|
||||
state.set_left_top_pos(
|
||||
anchor.align_size_within_rect(state.size, screen).left_top() + offset,
|
||||
);
|
||||
}
|
||||
|
||||
state.pos = ctx.round_pos_to_pixels(state.pos);
|
||||
// interact right away to prevent frame-delay
|
||||
let move_response = {
|
||||
let interact_id = layer_id.id.with("move");
|
||||
let sense = if movable {
|
||||
Sense::click_and_drag()
|
||||
} else if interactable {
|
||||
Sense::click() // allow clicks to bring to front
|
||||
} else {
|
||||
Sense::hover()
|
||||
};
|
||||
|
||||
let move_response = ctx.interact(
|
||||
Rect::EVERYTHING,
|
||||
ctx.style().spacing.item_spacing,
|
||||
layer_id,
|
||||
interact_id,
|
||||
state.rect(),
|
||||
sense,
|
||||
enabled,
|
||||
);
|
||||
|
||||
// Important check - don't try to move e.g. a combobox popup!
|
||||
if movable {
|
||||
if move_response.dragged() {
|
||||
state.pivot_pos += ctx.input(|i| i.pointer.delta());
|
||||
}
|
||||
|
||||
state.set_left_top_pos(
|
||||
ctx.constrain_window_rect_to_area(state.rect(), drag_bounds)
|
||||
.min,
|
||||
);
|
||||
}
|
||||
|
||||
if (move_response.dragged() || move_response.clicked())
|
||||
|| pointer_pressed_on_area(ctx, layer_id)
|
||||
|| !ctx.memory(|m| m.areas.visible_last_frame(&layer_id))
|
||||
{
|
||||
ctx.memory_mut(|m| m.areas.move_to_top(layer_id));
|
||||
ctx.request_repaint();
|
||||
}
|
||||
|
||||
move_response
|
||||
};
|
||||
|
||||
state.set_left_top_pos(ctx.round_pos_to_pixels(state.left_top_pos()));
|
||||
|
||||
if constrain {
|
||||
state.set_left_top_pos(
|
||||
ctx.constrain_window_rect_to_area(state.rect(), drag_bounds)
|
||||
.left_top(),
|
||||
);
|
||||
}
|
||||
|
||||
Prepared {
|
||||
layer_id,
|
||||
state,
|
||||
movable,
|
||||
move_response,
|
||||
enabled,
|
||||
drag_bounds,
|
||||
temporarily_invisible: is_new,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -249,7 +345,7 @@ impl Area {
|
|||
}
|
||||
|
||||
let layer_id = LayerId::new(self.order, self.id);
|
||||
let area_rect = ctx.memory().areas.get(self.id).map(|area| area.rect());
|
||||
let area_rect = ctx.memory(|mem| mem.areas.get(self.id).map(|area| area.rect()));
|
||||
if let Some(area_rect) = area_rect {
|
||||
let clip_rect = ctx.available_rect();
|
||||
let painter = Painter::new(ctx.clone(), layer_id, clip_rect);
|
||||
|
@ -278,7 +374,7 @@ impl Prepared {
|
|||
}
|
||||
|
||||
pub(crate) fn content_ui(&self, ctx: &Context) -> Ui {
|
||||
let screen_rect = ctx.input().screen_rect();
|
||||
let screen_rect = ctx.screen_rect();
|
||||
|
||||
let bounds = if let Some(bounds) = self.drag_bounds {
|
||||
bounds.intersect(screen_rect) // protect against infinite bounds
|
||||
|
@ -294,14 +390,16 @@ impl Prepared {
|
|||
};
|
||||
|
||||
let max_rect = Rect::from_min_max(
|
||||
self.state.pos,
|
||||
bounds.max.at_least(self.state.pos + Vec2::splat(32.0)),
|
||||
self.state.left_top_pos(),
|
||||
bounds
|
||||
.max
|
||||
.at_least(self.state.left_top_pos() + Vec2::splat(32.0)),
|
||||
);
|
||||
|
||||
let shadow_radius = ctx.style().visuals.window_shadow.extrusion; // hacky
|
||||
let clip_rect_margin = ctx.style().visuals.clip_rect_margin.max(shadow_radius);
|
||||
|
||||
let clip_rect = Rect::from_min_max(self.state.pos, bounds.max)
|
||||
let clip_rect = Rect::from_min_max(self.state.left_top_pos(), bounds.max)
|
||||
.expand(clip_rect_margin)
|
||||
.intersect(bounds);
|
||||
|
||||
|
@ -313,7 +411,7 @@ impl Prepared {
|
|||
clip_rect,
|
||||
);
|
||||
ui.set_enabled(self.enabled);
|
||||
|
||||
ui.set_visible(!self.temporarily_invisible);
|
||||
ui
|
||||
}
|
||||
|
||||
|
@ -322,49 +420,15 @@ impl Prepared {
|
|||
let Prepared {
|
||||
layer_id,
|
||||
mut state,
|
||||
movable,
|
||||
enabled,
|
||||
drag_bounds,
|
||||
move_response,
|
||||
enabled: _,
|
||||
drag_bounds: _,
|
||||
temporarily_invisible: _,
|
||||
} = self;
|
||||
|
||||
state.size = content_ui.min_rect().size();
|
||||
|
||||
let interact_id = layer_id.id.with("move");
|
||||
let sense = if movable {
|
||||
Sense::click_and_drag()
|
||||
} else {
|
||||
Sense::click() // allow clicks to bring to front
|
||||
};
|
||||
|
||||
let move_response = ctx.interact(
|
||||
Rect::EVERYTHING,
|
||||
ctx.style().spacing.item_spacing,
|
||||
layer_id,
|
||||
interact_id,
|
||||
state.rect(),
|
||||
sense,
|
||||
enabled,
|
||||
);
|
||||
|
||||
if move_response.dragged() && movable {
|
||||
state.pos += ctx.input().pointer.delta();
|
||||
}
|
||||
|
||||
// Important check - don't try to move e.g. a combobox popup!
|
||||
if movable {
|
||||
state.pos = ctx
|
||||
.constrain_window_rect_to_area(state.rect(), drag_bounds)
|
||||
.min;
|
||||
}
|
||||
|
||||
if (move_response.dragged() || move_response.clicked())
|
||||
|| pointer_pressed_on_area(ctx, layer_id)
|
||||
|| !ctx.memory().areas.visible_last_frame(&layer_id)
|
||||
{
|
||||
ctx.memory().areas.move_to_top(layer_id);
|
||||
ctx.request_repaint();
|
||||
}
|
||||
ctx.memory().areas.set_state(layer_id, state);
|
||||
ctx.memory_mut(|m| m.areas.set_state(layer_id, state));
|
||||
|
||||
move_response
|
||||
}
|
||||
|
@ -372,7 +436,7 @@ impl Prepared {
|
|||
|
||||
fn pointer_pressed_on_area(ctx: &Context, layer_id: LayerId) -> bool {
|
||||
if let Some(pointer_pos) = ctx.pointer_interact_pos() {
|
||||
let any_pressed = ctx.input().pointer.any_pressed();
|
||||
let any_pressed = ctx.input(|i| i.pointer.any_pressed());
|
||||
any_pressed && ctx.layer_id_at(pointer_pos) == Some(layer_id)
|
||||
} else {
|
||||
false
|
||||
|
@ -380,13 +444,13 @@ fn pointer_pressed_on_area(ctx: &Context, layer_id: LayerId) -> bool {
|
|||
}
|
||||
|
||||
fn automatic_area_position(ctx: &Context) -> Pos2 {
|
||||
let mut existing: Vec<Rect> = ctx
|
||||
.memory()
|
||||
.areas
|
||||
.visible_windows()
|
||||
.into_iter()
|
||||
.map(State::rect)
|
||||
.collect();
|
||||
let mut existing: Vec<Rect> = ctx.memory(|mem| {
|
||||
mem.areas
|
||||
.visible_windows()
|
||||
.into_iter()
|
||||
.map(State::rect)
|
||||
.collect()
|
||||
});
|
||||
existing.sort_by_key(|r| r.left().round() as i32);
|
||||
|
||||
let available_rect = ctx.available_rect();
|
|
@ -26,13 +26,14 @@ pub struct CollapsingState {
|
|||
|
||||
impl CollapsingState {
|
||||
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
|
||||
ctx.data()
|
||||
.get_persisted::<InnerState>(id)
|
||||
.map(|state| Self { id, state })
|
||||
ctx.data_mut(|d| {
|
||||
d.get_persisted::<InnerState>(id)
|
||||
.map(|state| Self { id, state })
|
||||
})
|
||||
}
|
||||
|
||||
pub fn store(&self, ctx: &Context) {
|
||||
ctx.data().insert_persisted(self.id, self.state);
|
||||
ctx.data_mut(|d| d.insert_persisted(self.id, self.state));
|
||||
}
|
||||
|
||||
pub fn id(&self) -> Id {
|
||||
|
@ -64,7 +65,7 @@ impl CollapsingState {
|
|||
|
||||
/// 0 for closed, 1 for open, with tweening
|
||||
pub fn openness(&self, ctx: &Context) -> f32 {
|
||||
if ctx.memory().everything_is_visible() {
|
||||
if ctx.memory(|mem| mem.everything_is_visible()) {
|
||||
1.0
|
||||
} else {
|
||||
ctx.animate_bool(self.id, self.state.open)
|
||||
|
@ -89,7 +90,16 @@ impl CollapsingState {
|
|||
|
||||
/// Will toggle when clicked, etc.
|
||||
fn show_default_button_indented(&mut self, ui: &mut Ui) -> Response {
|
||||
let size = Vec2::new(ui.spacing().indent, ui.spacing().icon_width);
|
||||
self.show_button_indented(ui, paint_default_icon)
|
||||
}
|
||||
|
||||
/// Will toggle when clicked, etc.
|
||||
fn show_button_indented(
|
||||
&mut self,
|
||||
ui: &mut Ui,
|
||||
icon_fn: impl FnOnce(&mut Ui, f32, &Response) + 'static,
|
||||
) -> Response {
|
||||
let size = vec2(ui.spacing().indent, ui.spacing().icon_width);
|
||||
let (_id, rect) = ui.allocate_space(size);
|
||||
let response = ui.interact(rect, self.id, Sense::click());
|
||||
if response.clicked() {
|
||||
|
@ -102,11 +112,8 @@ impl CollapsingState {
|
|||
response.rect.center().y,
|
||||
));
|
||||
let openness = self.openness(ui.ctx());
|
||||
let small_icon_response = Response {
|
||||
rect: icon_rect,
|
||||
..response.clone()
|
||||
};
|
||||
paint_default_icon(ui, openness, &small_icon_response);
|
||||
let small_icon_response = response.clone().with_new_rect(icon_rect);
|
||||
icon_fn(ui, openness, &small_icon_response);
|
||||
response
|
||||
}
|
||||
|
||||
|
@ -134,9 +141,10 @@ impl CollapsingState {
|
|||
add_header: impl FnOnce(&mut Ui) -> HeaderRet,
|
||||
) -> HeaderResponse<'_, HeaderRet> {
|
||||
let header_response = ui.horizontal(|ui| {
|
||||
let prev_item_spacing = ui.spacing_mut().item_spacing;
|
||||
ui.spacing_mut().item_spacing.x = 0.0; // the toggler button uses the full indent width
|
||||
let collapser = self.show_default_button_indented(ui);
|
||||
ui.spacing_mut().item_spacing.x = ui.spacing_mut().icon_spacing; // Restore spacing
|
||||
ui.spacing_mut().item_spacing = prev_item_spacing;
|
||||
(collapser, add_header(ui))
|
||||
});
|
||||
HeaderResponse {
|
||||
|
@ -217,6 +225,37 @@ impl CollapsingState {
|
|||
Some(ret_response)
|
||||
}
|
||||
}
|
||||
|
||||
/// Paint this [CollapsingState](CollapsingState)'s toggle button. Takes an [IconPainter](IconPainter) as the icon.
|
||||
/// ```
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// fn circle_icon(ui: &mut egui::Ui, openness: f32, response: &egui::Response) {
|
||||
/// let stroke = ui.style().interact(&response).fg_stroke;
|
||||
/// let radius = egui::lerp(2.0..=3.0, openness);
|
||||
/// ui.painter().circle_filled(response.rect.center(), radius, stroke.color);
|
||||
/// }
|
||||
///
|
||||
/// let mut state = egui::collapsing_header::CollapsingState::load_with_default_open(
|
||||
/// ui.ctx(),
|
||||
/// ui.make_persistent_id("my_collapsing_state"),
|
||||
/// false,
|
||||
/// );
|
||||
///
|
||||
/// let header_res = ui.horizontal(|ui| {
|
||||
/// ui.label("Header");
|
||||
/// state.show_toggle_button(ui, circle_icon);
|
||||
/// });
|
||||
///
|
||||
/// state.show_body_indented(&header_res.response, ui, |ui| ui.label("Body"));
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn show_toggle_button(
|
||||
&mut self,
|
||||
ui: &mut Ui,
|
||||
icon_fn: impl FnOnce(&mut Ui, f32, &Response) + 'static,
|
||||
) -> Response {
|
||||
self.show_button_indented(ui, icon_fn)
|
||||
}
|
||||
}
|
||||
|
||||
/// From [`CollapsingState::show_header`].
|
||||
|
@ -247,6 +286,23 @@ impl<'ui, HeaderRet> HeaderResponse<'ui, HeaderRet> {
|
|||
body_response,
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the response of the collapsing button, the custom header, and the custom body, without indentation.
|
||||
pub fn body_unindented<BodyRet>(
|
||||
mut self,
|
||||
add_body: impl FnOnce(&mut Ui) -> BodyRet,
|
||||
) -> (
|
||||
Response,
|
||||
InnerResponse<HeaderRet>,
|
||||
Option<InnerResponse<BodyRet>>,
|
||||
) {
|
||||
let body_response = self.state.show_body_unindented(self.ui, add_body);
|
||||
(
|
||||
self.toggle_button_response,
|
||||
self.header_response,
|
||||
body_response,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
@ -254,7 +310,6 @@ impl<'ui, HeaderRet> HeaderResponse<'ui, HeaderRet> {
|
|||
/// Paint the arrow icon that indicated if the region is open or not
|
||||
pub fn paint_default_icon(ui: &mut Ui, openness: f32, response: &Response) {
|
||||
let visuals = ui.style().interact(response);
|
||||
let stroke = visuals.fg_stroke;
|
||||
|
||||
let rect = response.rect;
|
||||
|
||||
|
@ -268,7 +323,11 @@ pub fn paint_default_icon(ui: &mut Ui, openness: f32, response: &Response) {
|
|||
*p = rect.center() + rotation * (*p - rect.center());
|
||||
}
|
||||
|
||||
ui.painter().add(Shape::closed_line(points, stroke));
|
||||
ui.painter().add(Shape::convex_polygon(
|
||||
points,
|
||||
visuals.fg_stroke.color,
|
||||
Stroke::NONE,
|
||||
));
|
||||
}
|
||||
|
||||
/// A function that paints an icon indicating if the region is open or not
|
||||
|
@ -445,7 +504,7 @@ impl CollapsingHeader {
|
|||
show_background,
|
||||
} = self;
|
||||
|
||||
// TODO: horizontal layout, with icon and text as labels. Insert background behind using Frame.
|
||||
// TODO(emilk): horizontal layout, with icon and text as labels. Insert background behind using Frame.
|
||||
|
||||
let id = ui.make_persistent_id(id_source);
|
||||
let button_padding = ui.spacing().button_padding;
|
||||
|
@ -495,7 +554,7 @@ impl CollapsingHeader {
|
|||
ui.painter().add(epaint::RectShape {
|
||||
rect: header_response.rect.expand(visuals.expansion),
|
||||
rounding: visuals.rounding,
|
||||
fill: visuals.bg_fill,
|
||||
fill: visuals.weak_bg_fill,
|
||||
stroke: visuals.bg_stroke,
|
||||
// stroke: Default::default(),
|
||||
});
|
||||
|
@ -515,10 +574,7 @@ impl CollapsingHeader {
|
|||
header_response.rect.left() + ui.spacing().indent / 2.0,
|
||||
header_response.rect.center().y,
|
||||
));
|
||||
let icon_response = Response {
|
||||
rect: icon_rect,
|
||||
..header_response.clone()
|
||||
};
|
||||
let icon_response = header_response.clone().with_new_rect(icon_rect);
|
||||
if let Some(icon) = icon {
|
||||
icon(ui, openness, &icon_response);
|
||||
} else {
|
||||
|
@ -542,13 +598,23 @@ impl CollapsingHeader {
|
|||
ui: &mut Ui,
|
||||
add_body: impl FnOnce(&mut Ui) -> R,
|
||||
) -> CollapsingResponse<R> {
|
||||
self.show_dyn(ui, Box::new(add_body))
|
||||
self.show_dyn(ui, Box::new(add_body), true)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn show_unindented<R>(
|
||||
self,
|
||||
ui: &mut Ui,
|
||||
add_body: impl FnOnce(&mut Ui) -> R,
|
||||
) -> CollapsingResponse<R> {
|
||||
self.show_dyn(ui, Box::new(add_body), false)
|
||||
}
|
||||
|
||||
fn show_dyn<'c, R>(
|
||||
self,
|
||||
ui: &mut Ui,
|
||||
add_body: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
|
||||
indented: bool,
|
||||
) -> CollapsingResponse<R> {
|
||||
// Make sure body is bellow header,
|
||||
// and make sure it is one unit (necessary for putting a [`CollapsingHeader`] in a grid).
|
||||
|
@ -561,7 +627,11 @@ impl CollapsingHeader {
|
|||
openness,
|
||||
} = self.begin(ui); // show the header
|
||||
|
||||
let ret_response = state.show_body_indented(&header_response, ui, add_body);
|
||||
let ret_response = if indented {
|
||||
state.show_body_indented(&header_response, ui, add_body)
|
||||
} else {
|
||||
state.show_body_unindented(ui, add_body)
|
||||
};
|
||||
|
||||
if let Some(ret_response) = ret_response {
|
||||
CollapsingResponse {
|
|
@ -1,8 +1,16 @@
|
|||
use crate::{style::WidgetVisuals, *};
|
||||
use epaint::Shape;
|
||||
|
||||
use crate::{style::WidgetVisuals, *};
|
||||
|
||||
/// Indicate wether or not a popup will be shown above or below the box.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum AboveOrBelow {
|
||||
Above,
|
||||
Below,
|
||||
}
|
||||
|
||||
/// A function that paints the [`ComboBox`] icon
|
||||
pub type IconPainter = Box<dyn FnOnce(&Ui, Rect, &WidgetVisuals, bool)>;
|
||||
pub type IconPainter = Box<dyn FnOnce(&Ui, Rect, &WidgetVisuals, bool, AboveOrBelow)>;
|
||||
|
||||
/// A drop-down selection menu with a descriptive label.
|
||||
///
|
||||
|
@ -28,6 +36,7 @@ pub struct ComboBox {
|
|||
selected_text: WidgetText,
|
||||
width: Option<f32>,
|
||||
icon: Option<IconPainter>,
|
||||
wrap_enabled: bool,
|
||||
}
|
||||
|
||||
impl ComboBox {
|
||||
|
@ -39,6 +48,7 @@ impl ComboBox {
|
|||
selected_text: Default::default(),
|
||||
width: None,
|
||||
icon: None,
|
||||
wrap_enabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -51,6 +61,7 @@ impl ComboBox {
|
|||
selected_text: Default::default(),
|
||||
width: None,
|
||||
icon: None,
|
||||
wrap_enabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -62,10 +73,11 @@ impl ComboBox {
|
|||
selected_text: Default::default(),
|
||||
width: None,
|
||||
icon: None,
|
||||
wrap_enabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the width of the button and menu
|
||||
/// Set the outer width of the button and menu.
|
||||
pub fn width(mut self, width: f32) -> Self {
|
||||
self.width = Some(width);
|
||||
self
|
||||
|
@ -89,10 +101,11 @@ impl ComboBox {
|
|||
/// rect: egui::Rect,
|
||||
/// visuals: &egui::style::WidgetVisuals,
|
||||
/// _is_open: bool,
|
||||
/// _above_or_below: egui::AboveOrBelow,
|
||||
/// ) {
|
||||
/// let rect = egui::Rect::from_center_size(
|
||||
/// rect.center(),
|
||||
/// egui::Vec2::new(rect.width() * 0.6, rect.height() * 0.4),
|
||||
/// egui::vec2(rect.width() * 0.6, rect.height() * 0.4),
|
||||
/// );
|
||||
/// ui.painter().add(egui::Shape::convex_polygon(
|
||||
/// vec![rect.left_top(), rect.right_top(), rect.center_bottom()],
|
||||
|
@ -107,11 +120,20 @@ impl ComboBox {
|
|||
/// .show_ui(ui, |_ui| {});
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn icon(mut self, icon_fn: impl FnOnce(&Ui, Rect, &WidgetVisuals, bool) + 'static) -> Self {
|
||||
pub fn icon(
|
||||
mut self,
|
||||
icon_fn: impl FnOnce(&Ui, Rect, &WidgetVisuals, bool, AboveOrBelow) + 'static,
|
||||
) -> Self {
|
||||
self.icon = Some(Box::new(icon_fn));
|
||||
self
|
||||
}
|
||||
|
||||
/// Controls whether text wrap is used for the selected text
|
||||
pub fn wrap(mut self, wrap: bool) -> Self {
|
||||
self.wrap_enabled = wrap;
|
||||
self
|
||||
}
|
||||
|
||||
/// Show the combo box, with the given ui code for the menu contents.
|
||||
///
|
||||
/// Returns `InnerResponse { inner: None }` if the combo box is closed.
|
||||
|
@ -134,15 +156,21 @@ impl ComboBox {
|
|||
selected_text,
|
||||
width,
|
||||
icon,
|
||||
wrap_enabled,
|
||||
} = self;
|
||||
|
||||
let button_id = ui.make_persistent_id(id_source);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if let Some(width) = width {
|
||||
ui.spacing_mut().slider_width = width; // yes, this is ugly. Will remove later.
|
||||
}
|
||||
let mut ir = combo_box_dyn(ui, button_id, selected_text, menu_contents, icon);
|
||||
let mut ir = combo_box_dyn(
|
||||
ui,
|
||||
button_id,
|
||||
selected_text,
|
||||
menu_contents,
|
||||
icon,
|
||||
wrap_enabled,
|
||||
width,
|
||||
);
|
||||
if let Some(label) = label {
|
||||
ir.response
|
||||
.widget_info(|| WidgetInfo::labeled(WidgetType::ComboBox, label.text()));
|
||||
|
@ -209,18 +237,59 @@ fn combo_box_dyn<'c, R>(
|
|||
selected_text: WidgetText,
|
||||
menu_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
|
||||
icon: Option<IconPainter>,
|
||||
wrap_enabled: bool,
|
||||
width: Option<f32>,
|
||||
) -> InnerResponse<Option<R>> {
|
||||
let popup_id = button_id.with("popup");
|
||||
|
||||
let is_popup_open = ui.memory().is_popup_open(popup_id);
|
||||
let is_popup_open = ui.memory(|m| m.is_popup_open(popup_id));
|
||||
|
||||
let popup_height = ui.memory(|m| m.areas.get(popup_id).map_or(100.0, |state| state.size.y));
|
||||
|
||||
let above_or_below =
|
||||
if ui.next_widget_position().y + ui.spacing().interact_size.y + popup_height
|
||||
< ui.ctx().screen_rect().bottom()
|
||||
{
|
||||
AboveOrBelow::Below
|
||||
} else {
|
||||
AboveOrBelow::Above
|
||||
};
|
||||
|
||||
let margin = ui.spacing().button_padding;
|
||||
let button_response = button_frame(ui, button_id, is_popup_open, Sense::click(), |ui| {
|
||||
let icon_spacing = ui.spacing().icon_spacing;
|
||||
// We don't want to change width when user selects something new
|
||||
let full_minimum_width = ui.spacing().slider_width;
|
||||
let full_minimum_width = if wrap_enabled {
|
||||
// Currently selected value's text will be wrapped if needed, so occupy the available width.
|
||||
ui.available_width()
|
||||
} else {
|
||||
// Occupy at least the minimum width assigned to ComboBox.
|
||||
let width = width.unwrap_or_else(|| ui.spacing().combo_width);
|
||||
width - 2.0 * margin.x
|
||||
};
|
||||
let icon_size = Vec2::splat(ui.spacing().icon_width);
|
||||
let wrap_width = if wrap_enabled {
|
||||
// Use the available width, currently selected value's text will be wrapped if exceeds this value.
|
||||
ui.available_width() - icon_spacing - icon_size.x
|
||||
} else {
|
||||
// Use all the width necessary to display the currently selected value's text.
|
||||
f32::INFINITY
|
||||
};
|
||||
|
||||
let galley = selected_text.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Button);
|
||||
let galley =
|
||||
selected_text.into_galley(ui, Some(wrap_enabled), wrap_width, TextStyle::Button);
|
||||
|
||||
let width = galley.size().x + ui.spacing().item_spacing.x + icon_size.x;
|
||||
// The width necessary to contain the whole widget with the currently selected value's text.
|
||||
let width = if wrap_enabled {
|
||||
full_minimum_width
|
||||
} else {
|
||||
// Occupy at least the minimum width needed to contain the widget with the currently selected value's text.
|
||||
galley.size().x + icon_spacing + icon_size.x
|
||||
};
|
||||
|
||||
// Case : wrap_enabled : occupy all the available width.
|
||||
// Case : !wrap_enabled : occupy at least the minimum width assigned to Slider and ComboBox,
|
||||
// increase if the currently selected value needs additional horizontal space to fully display its text (up to wrap_width (f32::INFINITY)).
|
||||
let width = width.at_least(full_minimum_width);
|
||||
let height = galley.size().y.max(icon_size.y);
|
||||
|
||||
|
@ -243,9 +312,15 @@ fn combo_box_dyn<'c, R>(
|
|||
icon_rect.expand(visuals.expansion),
|
||||
visuals,
|
||||
is_popup_open,
|
||||
above_or_below,
|
||||
);
|
||||
} else {
|
||||
paint_default_icon(ui.painter(), icon_rect.expand(visuals.expansion), visuals);
|
||||
paint_default_icon(
|
||||
ui.painter(),
|
||||
icon_rect.expand(visuals.expansion),
|
||||
visuals,
|
||||
above_or_below,
|
||||
);
|
||||
}
|
||||
|
||||
let text_rect = Align2::LEFT_CENTER.align_size_within_rect(galley.size(), rect);
|
||||
|
@ -254,14 +329,20 @@ fn combo_box_dyn<'c, R>(
|
|||
});
|
||||
|
||||
if button_response.clicked() {
|
||||
ui.memory().toggle_popup(popup_id);
|
||||
ui.memory_mut(|mem| mem.toggle_popup(popup_id));
|
||||
}
|
||||
let inner = crate::popup::popup_below_widget(ui, popup_id, &button_response, |ui| {
|
||||
ScrollArea::vertical()
|
||||
.max_height(ui.spacing().combo_height)
|
||||
.show(ui, menu_contents)
|
||||
.inner
|
||||
});
|
||||
let inner = crate::popup::popup_above_or_below_widget(
|
||||
ui,
|
||||
popup_id,
|
||||
&button_response,
|
||||
above_or_below,
|
||||
|ui| {
|
||||
ScrollArea::vertical()
|
||||
.max_height(ui.spacing().combo_height)
|
||||
.show(ui, menu_contents)
|
||||
.inner
|
||||
},
|
||||
);
|
||||
|
||||
InnerResponse {
|
||||
inner,
|
||||
|
@ -305,7 +386,7 @@ fn button_frame(
|
|||
epaint::RectShape {
|
||||
rect: outer_rect.expand(visuals.expansion),
|
||||
rounding: visuals.rounding,
|
||||
fill: visuals.bg_fill,
|
||||
fill: visuals.weak_bg_fill,
|
||||
stroke: visuals.bg_stroke,
|
||||
},
|
||||
);
|
||||
|
@ -316,13 +397,33 @@ fn button_frame(
|
|||
response
|
||||
}
|
||||
|
||||
fn paint_default_icon(painter: &Painter, rect: Rect, visuals: &WidgetVisuals) {
|
||||
fn paint_default_icon(
|
||||
painter: &Painter,
|
||||
rect: Rect,
|
||||
visuals: &WidgetVisuals,
|
||||
above_or_below: AboveOrBelow,
|
||||
) {
|
||||
let rect = Rect::from_center_size(
|
||||
rect.center(),
|
||||
vec2(rect.width() * 0.7, rect.height() * 0.45),
|
||||
);
|
||||
painter.add(Shape::closed_line(
|
||||
vec![rect.left_top(), rect.right_top(), rect.center_bottom()],
|
||||
visuals.fg_stroke,
|
||||
));
|
||||
|
||||
match above_or_below {
|
||||
AboveOrBelow::Above => {
|
||||
// Upward pointing triangle
|
||||
painter.add(Shape::convex_polygon(
|
||||
vec![rect.left_bottom(), rect.right_bottom(), rect.center_top()],
|
||||
visuals.fg_stroke.color,
|
||||
Stroke::NONE,
|
||||
));
|
||||
}
|
||||
AboveOrBelow::Below => {
|
||||
// Downward pointing triangle
|
||||
painter.add(Shape::convex_polygon(
|
||||
vec![rect.left_top(), rect.right_top(), rect.center_bottom()],
|
||||
visuals.fg_stroke.color,
|
||||
Stroke::NONE,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,11 +19,16 @@ use epaint::*;
|
|||
pub struct Frame {
|
||||
/// Margin within the painted frame.
|
||||
pub inner_margin: Margin,
|
||||
|
||||
/// Margin outside the painted frame.
|
||||
pub outer_margin: Margin,
|
||||
|
||||
pub rounding: Rounding,
|
||||
|
||||
pub shadow: Shadow,
|
||||
|
||||
pub fill: Color32,
|
||||
|
||||
pub stroke: Stroke,
|
||||
}
|
||||
|
||||
|
@ -42,22 +47,18 @@ impl Frame {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn side_top_panel(style: &Style) -> Self {
|
||||
pub fn side_top_panel(style: &Style) -> Self {
|
||||
Self {
|
||||
inner_margin: Margin::symmetric(8.0, 2.0),
|
||||
rounding: Rounding::none(),
|
||||
fill: style.visuals.window_fill(),
|
||||
stroke: style.visuals.window_stroke(),
|
||||
fill: style.visuals.panel_fill,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn central_panel(style: &Style) -> Self {
|
||||
pub fn central_panel(style: &Style) -> Self {
|
||||
Self {
|
||||
inner_margin: Margin::same(8.0),
|
||||
rounding: Rounding::none(),
|
||||
fill: style.visuals.window_fill(),
|
||||
stroke: Default::default(),
|
||||
fill: style.visuals.panel_fill,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
@ -75,8 +76,8 @@ impl Frame {
|
|||
|
||||
pub fn menu(style: &Style) -> Self {
|
||||
Self {
|
||||
inner_margin: Margin::same(1.0),
|
||||
rounding: style.visuals.widgets.noninteractive.rounding,
|
||||
inner_margin: style.spacing.menu_margin,
|
||||
rounding: style.visuals.menu_rounding,
|
||||
shadow: style.visuals.popup_shadow,
|
||||
fill: style.visuals.window_fill(),
|
||||
stroke: style.visuals.window_stroke(),
|
||||
|
@ -86,8 +87,8 @@ impl Frame {
|
|||
|
||||
pub fn popup(style: &Style) -> Self {
|
||||
Self {
|
||||
inner_margin: style.spacing.window_margin,
|
||||
rounding: style.visuals.widgets.noninteractive.rounding,
|
||||
inner_margin: style.spacing.menu_margin,
|
||||
rounding: style.visuals.menu_rounding,
|
||||
shadow: style.visuals.popup_shadow,
|
||||
fill: style.visuals.window_fill(),
|
||||
stroke: style.visuals.window_stroke(),
|
||||
|
@ -119,38 +120,45 @@ impl Frame {
|
|||
}
|
||||
|
||||
impl Frame {
|
||||
#[inline]
|
||||
pub fn fill(mut self, fill: Color32) -> Self {
|
||||
self.fill = fill;
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn stroke(mut self, stroke: Stroke) -> Self {
|
||||
self.stroke = stroke;
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn rounding(mut self, rounding: impl Into<Rounding>) -> Self {
|
||||
self.rounding = rounding.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Margin within the painted frame.
|
||||
#[inline]
|
||||
pub fn inner_margin(mut self, inner_margin: impl Into<Margin>) -> Self {
|
||||
self.inner_margin = inner_margin.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Margin outside the painted frame.
|
||||
#[inline]
|
||||
pub fn outer_margin(mut self, outer_margin: impl Into<Margin>) -> Self {
|
||||
self.outer_margin = outer_margin.into();
|
||||
self
|
||||
}
|
||||
|
||||
#[deprecated = "Renamed inner_margin in egui 0.18"]
|
||||
#[inline]
|
||||
pub fn margin(self, margin: impl Into<Margin>) -> Self {
|
||||
self.inner_margin(margin)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn shadow(mut self, shadow: Shadow) -> Self {
|
||||
self.shadow = shadow;
|
||||
self
|
||||
|
@ -164,6 +172,16 @@ impl Frame {
|
|||
}
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
/// inner margin plus outer margin.
|
||||
#[inline]
|
||||
pub fn total_margin(&self) -> Margin {
|
||||
self.inner_margin + self.outer_margin
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
pub struct Prepared {
|
||||
pub frame: Frame,
|
||||
where_to_put_background: ShapeIdx,
|
||||
|
@ -244,6 +262,13 @@ impl Prepared {
|
|||
rect
|
||||
}
|
||||
|
||||
fn content_with_margin(&self) -> Rect {
|
||||
let mut rect = self.content_ui.min_rect();
|
||||
rect.min -= self.frame.inner_margin.left_top() + self.frame.outer_margin.left_top();
|
||||
rect.max += self.frame.inner_margin.right_bottom() + self.frame.outer_margin.right_bottom();
|
||||
rect
|
||||
}
|
||||
|
||||
pub fn end(self, ui: &mut Ui) -> Response {
|
||||
let paint_rect = self.paint_rect();
|
||||
|
||||
|
@ -258,6 +283,6 @@ impl Prepared {
|
|||
ui.painter().set(where_to_put_background, shape);
|
||||
}
|
||||
|
||||
ui.allocate_rect(paint_rect, Sense::hover())
|
||||
ui.allocate_rect(self.content_with_margin(), Sense::hover())
|
||||
}
|
||||
}
|
|
@ -9,7 +9,7 @@ pub(crate) mod frame;
|
|||
pub mod panel;
|
||||
pub mod popup;
|
||||
pub(crate) mod resize;
|
||||
pub(crate) mod scroll_area;
|
||||
pub mod scroll_area;
|
||||
pub(crate) mod window;
|
||||
|
||||
pub use {
|
1064
crates/egui/src/containers/panel.rs
Normal file
1064
crates/egui/src/containers/panel.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -6,44 +6,42 @@ use crate::*;
|
|||
|
||||
/// Same state for all tooltips.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct MonoState {
|
||||
last_id: Option<Id>,
|
||||
last_size: Vec<Vec2>,
|
||||
pub(crate) struct TooltipState {
|
||||
last_common_id: Option<Id>,
|
||||
individual_ids_and_sizes: ahash::HashMap<usize, (Id, Vec2)>,
|
||||
}
|
||||
|
||||
impl MonoState {
|
||||
fn load(ctx: &Context) -> Option<Self> {
|
||||
ctx.data().get_temp(Id::null())
|
||||
impl TooltipState {
|
||||
pub fn load(ctx: &Context) -> Option<Self> {
|
||||
ctx.data_mut(|d| d.get_temp(Id::null()))
|
||||
}
|
||||
|
||||
fn store(self, ctx: &Context) {
|
||||
ctx.data().insert_temp(Id::null(), self);
|
||||
ctx.data_mut(|d| d.insert_temp(Id::null(), self));
|
||||
}
|
||||
|
||||
fn tooltip_size(&self, id: Id, index: usize) -> Option<Vec2> {
|
||||
if self.last_id == Some(id) {
|
||||
self.last_size.get(index).cloned()
|
||||
fn individual_tooltip_size(&self, common_id: Id, index: usize) -> Option<Vec2> {
|
||||
if self.last_common_id == Some(common_id) {
|
||||
Some(self.individual_ids_and_sizes.get(&index)?.1)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn set_tooltip_size(&mut self, id: Id, index: usize, size: Vec2) {
|
||||
if self.last_id == Some(id) {
|
||||
if let Some(stored_size) = self.last_size.get_mut(index) {
|
||||
*stored_size = size;
|
||||
} else {
|
||||
self.last_size
|
||||
.extend((0..index - self.last_size.len()).map(|_| Vec2::ZERO));
|
||||
self.last_size.push(size);
|
||||
}
|
||||
return;
|
||||
fn set_individual_tooltip(
|
||||
&mut self,
|
||||
common_id: Id,
|
||||
index: usize,
|
||||
individual_id: Id,
|
||||
size: Vec2,
|
||||
) {
|
||||
if self.last_common_id != Some(common_id) {
|
||||
self.last_common_id = Some(common_id);
|
||||
self.individual_ids_and_sizes.clear();
|
||||
}
|
||||
|
||||
self.last_id = Some(id);
|
||||
self.last_size.clear();
|
||||
self.last_size.extend((0..index).map(|_| Vec2::ZERO));
|
||||
self.last_size.push(size);
|
||||
self.individual_ids_and_sizes
|
||||
.insert(index, (individual_id, size));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -97,9 +95,7 @@ pub fn show_tooltip_at_pointer<R>(
|
|||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> Option<R> {
|
||||
let suggested_pos = ctx
|
||||
.input()
|
||||
.pointer
|
||||
.hover_pos()
|
||||
.input(|i| i.pointer.hover_pos())
|
||||
.map(|pointer_pos| pointer_pos + vec2(16.0, 16.0));
|
||||
show_tooltip_at(ctx, id, suggested_pos, add_contents)
|
||||
}
|
||||
|
@ -114,7 +110,7 @@ pub fn show_tooltip_for<R>(
|
|||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> Option<R> {
|
||||
let expanded_rect = rect.expand2(vec2(2.0, 4.0));
|
||||
let (above, position) = if ctx.input().any_touches() {
|
||||
let (above, position) = if ctx.input(|i| i.any_touches()) {
|
||||
(true, expanded_rect.left_top())
|
||||
} else {
|
||||
(false, expanded_rect.left_bottom())
|
||||
|
@ -151,70 +147,84 @@ pub fn show_tooltip_at<R>(
|
|||
|
||||
fn show_tooltip_at_avoid_dyn<'c, R>(
|
||||
ctx: &Context,
|
||||
mut id: Id,
|
||||
individual_id: Id,
|
||||
suggested_position: Option<Pos2>,
|
||||
above: bool,
|
||||
mut avoid_rect: Rect,
|
||||
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
|
||||
) -> Option<R> {
|
||||
let mut tooltip_rect = Rect::NOTHING;
|
||||
let mut count = 0;
|
||||
let spacing = 4.0;
|
||||
|
||||
let mut position = if let Some((stored_id, stored_tooltip_rect, stored_count)) =
|
||||
ctx.frame_state().tooltip_rect
|
||||
{
|
||||
// if there are multiple tooltips open they should use the same id for the `tooltip_size` caching to work.
|
||||
id = stored_id;
|
||||
tooltip_rect = stored_tooltip_rect;
|
||||
count = stored_count;
|
||||
avoid_rect = avoid_rect.union(tooltip_rect);
|
||||
// if there are multiple tooltips open they should use the same common_id for the `tooltip_size` caching to work.
|
||||
let mut frame_state =
|
||||
ctx.frame_state(|fs| fs.tooltip_state)
|
||||
.unwrap_or(crate::frame_state::TooltipFrameState {
|
||||
common_id: individual_id,
|
||||
rect: Rect::NOTHING,
|
||||
count: 0,
|
||||
});
|
||||
|
||||
let mut position = if frame_state.rect.is_positive() {
|
||||
avoid_rect = avoid_rect.union(frame_state.rect);
|
||||
if above {
|
||||
tooltip_rect.left_top()
|
||||
frame_state.rect.left_top() - spacing * Vec2::Y
|
||||
} else {
|
||||
tooltip_rect.left_bottom()
|
||||
frame_state.rect.left_bottom() + spacing * Vec2::Y
|
||||
}
|
||||
} else if let Some(position) = suggested_position {
|
||||
position
|
||||
} else if ctx.memory().everything_is_visible() {
|
||||
} else if ctx.memory(|mem| mem.everything_is_visible()) {
|
||||
Pos2::ZERO
|
||||
} else {
|
||||
return None; // No good place for a tooltip :(
|
||||
};
|
||||
|
||||
let mut state = MonoState::load(ctx).unwrap_or_default();
|
||||
let expected_size = state.tooltip_size(id, count);
|
||||
let mut long_state = TooltipState::load(ctx).unwrap_or_default();
|
||||
let expected_size =
|
||||
long_state.individual_tooltip_size(frame_state.common_id, frame_state.count);
|
||||
let expected_size = expected_size.unwrap_or_else(|| vec2(64.0, 32.0));
|
||||
|
||||
if above {
|
||||
position.y -= expected_size.y;
|
||||
}
|
||||
|
||||
position = position.at_most(ctx.input().screen_rect().max - expected_size);
|
||||
position = position.at_most(ctx.screen_rect().max - expected_size);
|
||||
|
||||
// check if we intersect the avoid_rect
|
||||
{
|
||||
let new_rect = Rect::from_min_size(position, expected_size);
|
||||
|
||||
// Note: We do not use Rect::intersects() since it returns true even if the rects only touch.
|
||||
// Note: We use shrink so that we don't get false positives when the rects just touch
|
||||
if new_rect.shrink(1.0).intersects(avoid_rect) {
|
||||
if above {
|
||||
// place below instead:
|
||||
position = avoid_rect.left_bottom();
|
||||
position = avoid_rect.left_bottom() + spacing * Vec2::Y;
|
||||
} else {
|
||||
// place above instead:
|
||||
position = Pos2::new(position.x, avoid_rect.min.y - expected_size.y);
|
||||
position = Pos2::new(position.x, avoid_rect.min.y - expected_size.y - spacing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let position = position.at_least(ctx.input().screen_rect().min);
|
||||
let position = position.at_least(ctx.screen_rect().min);
|
||||
|
||||
let InnerResponse { inner, response } = show_tooltip_area_dyn(ctx, id, position, add_contents);
|
||||
let area_id = frame_state.common_id.with(frame_state.count);
|
||||
|
||||
state.set_tooltip_size(id, count, response.rect.size());
|
||||
state.store(ctx);
|
||||
let InnerResponse { inner, response } =
|
||||
show_tooltip_area_dyn(ctx, area_id, position, add_contents);
|
||||
|
||||
long_state.set_individual_tooltip(
|
||||
frame_state.common_id,
|
||||
frame_state.count,
|
||||
individual_id,
|
||||
response.rect.size(),
|
||||
);
|
||||
long_state.store(ctx);
|
||||
|
||||
frame_state.count += 1;
|
||||
frame_state.rect = frame_state.rect.union(response.rect);
|
||||
ctx.frame_state_mut(|fs| fs.tooltip_state = Some(frame_state));
|
||||
|
||||
ctx.frame_state().tooltip_rect = Some((id, tooltip_rect.union(response.rect), count + 1));
|
||||
Some(inner)
|
||||
}
|
||||
|
||||
|
@ -242,16 +252,17 @@ pub fn show_tooltip_text(ctx: &Context, id: Id, text: impl Into<WidgetText>) ->
|
|||
/// Show a pop-over window.
|
||||
fn show_tooltip_area_dyn<'c, R>(
|
||||
ctx: &Context,
|
||||
id: Id,
|
||||
area_id: Id,
|
||||
window_pos: Pos2,
|
||||
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
|
||||
) -> InnerResponse<R> {
|
||||
use containers::*;
|
||||
Area::new(id)
|
||||
Area::new(area_id)
|
||||
.order(Order::Tooltip)
|
||||
.fixed_pos(window_pos)
|
||||
.constrain(true)
|
||||
.interactable(false)
|
||||
.drag_bounds(Rect::EVERYTHING) // disable clip rect
|
||||
.drag_bounds(ctx.screen_rect())
|
||||
.show(ctx, |ui| {
|
||||
Frame::popup(&ctx.style())
|
||||
.show(ui, |ui| {
|
||||
|
@ -262,10 +273,47 @@ fn show_tooltip_area_dyn<'c, R>(
|
|||
})
|
||||
}
|
||||
|
||||
/// Shows a popup below another widget.
|
||||
/// Was this popup visible last frame?
|
||||
pub fn was_tooltip_open_last_frame(ctx: &Context, tooltip_id: Id) -> bool {
|
||||
if let Some(state) = TooltipState::load(ctx) {
|
||||
if let Some(common_id) = state.last_common_id {
|
||||
for (count, (individual_id, _size)) in &state.individual_ids_and_sizes {
|
||||
if *individual_id == tooltip_id {
|
||||
let area_id = common_id.with(count);
|
||||
let layer_id = LayerId::new(Order::Tooltip, area_id);
|
||||
if ctx.memory(|mem| mem.areas.visible_last_frame(&layer_id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Helper for [`popup_above_or_below_widget`].
|
||||
pub fn popup_below_widget<R>(
|
||||
ui: &Ui,
|
||||
popup_id: Id,
|
||||
widget_response: &Response,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> Option<R> {
|
||||
popup_above_or_below_widget(
|
||||
ui,
|
||||
popup_id,
|
||||
widget_response,
|
||||
AboveOrBelow::Below,
|
||||
add_contents,
|
||||
)
|
||||
}
|
||||
|
||||
/// Shows a popup above or below another widget.
|
||||
///
|
||||
/// Useful for drop-down menus (combo boxes) or suggestion menus under text fields.
|
||||
///
|
||||
/// The opened popup will have the same width as the parent.
|
||||
///
|
||||
/// You must open the popup with [`Memory::open_popup`] or [`Memory::toggle_popup`].
|
||||
///
|
||||
/// Returns `None` if the popup is not open.
|
||||
|
@ -275,30 +323,39 @@ fn show_tooltip_area_dyn<'c, R>(
|
|||
/// let response = ui.button("Open popup");
|
||||
/// let popup_id = ui.make_persistent_id("my_unique_id");
|
||||
/// if response.clicked() {
|
||||
/// ui.memory().toggle_popup(popup_id);
|
||||
/// ui.memory_mut(|mem| mem.toggle_popup(popup_id));
|
||||
/// }
|
||||
/// egui::popup::popup_below_widget(ui, popup_id, &response, |ui| {
|
||||
/// let below = egui::AboveOrBelow::Below;
|
||||
/// egui::popup::popup_above_or_below_widget(ui, popup_id, &response, below, |ui| {
|
||||
/// ui.set_min_width(200.0); // if you want to control the size
|
||||
/// ui.label("Some more info, or things you can select:");
|
||||
/// ui.label("…");
|
||||
/// });
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn popup_below_widget<R>(
|
||||
pub fn popup_above_or_below_widget<R>(
|
||||
ui: &Ui,
|
||||
popup_id: Id,
|
||||
widget_response: &Response,
|
||||
above_or_below: AboveOrBelow,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> Option<R> {
|
||||
if ui.memory().is_popup_open(popup_id) {
|
||||
if ui.memory(|mem| mem.is_popup_open(popup_id)) {
|
||||
let (pos, pivot) = match above_or_below {
|
||||
AboveOrBelow::Above => (widget_response.rect.left_top(), Align2::LEFT_BOTTOM),
|
||||
AboveOrBelow::Below => (widget_response.rect.left_bottom(), Align2::LEFT_TOP),
|
||||
};
|
||||
|
||||
let inner = Area::new(popup_id)
|
||||
.order(Order::Foreground)
|
||||
.fixed_pos(widget_response.rect.left_bottom())
|
||||
.constrain(true)
|
||||
.fixed_pos(pos)
|
||||
.pivot(pivot)
|
||||
.show(ui.ctx(), |ui| {
|
||||
// Note: we use a separate clip-rect for this area, so the popup can be outside the parent.
|
||||
// See https://github.com/emilk/egui/issues/825
|
||||
let frame = Frame::popup(ui.style());
|
||||
let frame_margin = frame.inner_margin + frame.outer_margin;
|
||||
let frame_margin = frame.total_margin();
|
||||
frame
|
||||
.show(ui, |ui| {
|
||||
ui.with_layout(Layout::top_down_justified(Align::LEFT), |ui| {
|
||||
|
@ -311,8 +368,8 @@ pub fn popup_below_widget<R>(
|
|||
})
|
||||
.inner;
|
||||
|
||||
if ui.input().key_pressed(Key::Escape) || widget_response.clicked_elsewhere() {
|
||||
ui.memory().close_popup();
|
||||
if ui.input(|i| i.key_pressed(Key::Escape)) || widget_response.clicked_elsewhere() {
|
||||
ui.memory_mut(|mem| mem.close_popup());
|
||||
}
|
||||
Some(inner)
|
||||
} else {
|
|
@ -18,11 +18,11 @@ pub(crate) struct State {
|
|||
|
||||
impl State {
|
||||
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
|
||||
ctx.data().get_persisted(id)
|
||||
ctx.data_mut(|d| d.get_persisted(id))
|
||||
}
|
||||
|
||||
pub fn store(self, ctx: &Context, id: Id) {
|
||||
ctx.data().insert_persisted(id, self);
|
||||
ctx.data_mut(|d| d.insert_persisted(id, self));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -52,7 +52,7 @@ impl Default for Resize {
|
|||
resizable: true,
|
||||
min_size: Vec2::splat(16.0),
|
||||
max_size: Vec2::splat(f32::INFINITY),
|
||||
default_size: vec2(320.0, 128.0), // TODO: preferred size of [`Resize`] area.
|
||||
default_size: vec2(320.0, 128.0), // TODO(emilk): preferred size of [`Resize`] area.
|
||||
with_stroke: true,
|
||||
}
|
||||
}
|
||||
|
@ -104,11 +104,13 @@ impl Resize {
|
|||
self.min_size = min_size.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Won't shrink to smaller than this
|
||||
pub fn min_width(mut self, min_width: f32) -> Self {
|
||||
self.min_size.x = min_width;
|
||||
self
|
||||
}
|
||||
|
||||
/// Won't shrink to smaller than this
|
||||
pub fn min_height(mut self, min_height: f32) -> Self {
|
||||
self.min_size.y = min_height;
|
||||
|
@ -178,7 +180,7 @@ impl Resize {
|
|||
.at_least(self.min_size)
|
||||
.at_most(self.max_size)
|
||||
.at_most(
|
||||
ui.input().screen_rect().size() - ui.spacing().window_margin.sum(), // hack for windows
|
||||
ui.ctx().screen_rect().size() - ui.spacing().window_margin.sum(), // hack for windows
|
||||
);
|
||||
|
||||
State {
|
||||
|
@ -303,7 +305,7 @@ impl Resize {
|
|||
paint_resize_corner(ui, &corner_response);
|
||||
|
||||
if corner_response.hovered() || corner_response.dragged() {
|
||||
ui.ctx().output().cursor_icon = CursorIcon::ResizeNwSe;
|
||||
ui.ctx().set_cursor_icon(CursorIcon::ResizeNwSe);
|
||||
}
|
||||
}
|
||||
|
|
@ -14,11 +14,15 @@ pub struct State {
|
|||
/// Positive offset means scrolling down/right
|
||||
pub offset: Vec2,
|
||||
|
||||
/// Were the scroll bars visible last frame?
|
||||
show_scroll: [bool; 2],
|
||||
|
||||
/// The content were to large to fit large frame.
|
||||
content_is_too_large: [bool; 2],
|
||||
|
||||
/// Momentum, used for kinetic scrolling
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
pub vel: Vec2,
|
||||
vel: Vec2,
|
||||
|
||||
/// Mouse offset relative to the top of the handle when started moving the handle.
|
||||
scroll_start_offset_from_top_left: [Option<f32>; 2],
|
||||
|
@ -34,6 +38,7 @@ impl Default for State {
|
|||
Self {
|
||||
offset: Vec2::ZERO,
|
||||
show_scroll: [false; 2],
|
||||
content_is_too_large: [false; 2],
|
||||
vel: Vec2::ZERO,
|
||||
scroll_start_offset_from_top_left: [None; 2],
|
||||
scroll_stuck_to_end: [true; 2],
|
||||
|
@ -43,11 +48,11 @@ impl Default for State {
|
|||
|
||||
impl State {
|
||||
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
|
||||
ctx.data().get_persisted(id)
|
||||
ctx.data_mut(|d| d.get_persisted(id))
|
||||
}
|
||||
|
||||
pub fn store(self, ctx: &Context, id: Id) {
|
||||
ctx.data().insert_persisted(id, self);
|
||||
ctx.data_mut(|d| d.insert_persisted(id, self));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -61,6 +66,10 @@ pub struct ScrollAreaOutput<R> {
|
|||
/// The current state of the scroll area.
|
||||
pub state: State,
|
||||
|
||||
/// The size of the content. If this is larger than [`Self::inner_rect`],
|
||||
/// then there was need for scrolling.
|
||||
pub content_size: Vec2,
|
||||
|
||||
/// Where on the screen the content is (excludes scroll bars).
|
||||
pub inner_rect: Rect,
|
||||
}
|
||||
|
@ -88,8 +97,10 @@ pub struct ScrollArea {
|
|||
id_source: Option<Id>,
|
||||
offset_x: Option<f32>,
|
||||
offset_y: Option<f32>,
|
||||
|
||||
/// If false, we ignore scroll events.
|
||||
scrolling_enabled: bool,
|
||||
drag_to_scroll: bool,
|
||||
|
||||
/// If true for vertical or horizontal the scroll wheel will stick to the
|
||||
/// end position until user manually changes position. It will become true
|
||||
|
@ -132,6 +143,7 @@ impl ScrollArea {
|
|||
offset_x: None,
|
||||
offset_y: None,
|
||||
scrolling_enabled: true,
|
||||
drag_to_scroll: true,
|
||||
stick_to_end: [false; 2],
|
||||
}
|
||||
}
|
||||
|
@ -193,6 +205,8 @@ impl ScrollArea {
|
|||
|
||||
/// Set the horizontal and vertical scroll offset position.
|
||||
///
|
||||
/// Positive offset means scrolling down/right.
|
||||
///
|
||||
/// See also: [`Self::vertical_scroll_offset`], [`Self::horizontal_scroll_offset`],
|
||||
/// [`Ui::scroll_to_cursor`](crate::ui::Ui::scroll_to_cursor) and
|
||||
/// [`Response::scroll_to_me`](crate::Response::scroll_to_me)
|
||||
|
@ -204,6 +218,8 @@ impl ScrollArea {
|
|||
|
||||
/// Set the vertical scroll offset position.
|
||||
///
|
||||
/// Positive offset means scrolling down.
|
||||
///
|
||||
/// See also: [`Self::scroll_offset`], [`Ui::scroll_to_cursor`](crate::ui::Ui::scroll_to_cursor) and
|
||||
/// [`Response::scroll_to_me`](crate::Response::scroll_to_me)
|
||||
pub fn vertical_scroll_offset(mut self, offset: f32) -> Self {
|
||||
|
@ -213,6 +229,8 @@ impl ScrollArea {
|
|||
|
||||
/// Set the horizontal scroll offset position.
|
||||
///
|
||||
/// Positive offset means scrolling right.
|
||||
///
|
||||
/// See also: [`Self::scroll_offset`], [`Ui::scroll_to_cursor`](crate::ui::Ui::scroll_to_cursor) and
|
||||
/// [`Response::scroll_to_me`](crate::Response::scroll_to_me)
|
||||
pub fn horizontal_scroll_offset(mut self, offset: f32) -> Self {
|
||||
|
@ -238,12 +256,13 @@ impl ScrollArea {
|
|||
self
|
||||
}
|
||||
|
||||
/// Control the scrolling behavior
|
||||
/// If `true` (default), the scroll area will respond to user scrolling
|
||||
/// If `false`, the scroll area will not respond to user scrolling
|
||||
/// Control the scrolling behavior.
|
||||
///
|
||||
/// * If `true` (default), the scroll area will respond to user scrolling.
|
||||
/// * If `false`, the scroll area will not respond to user scrolling.
|
||||
///
|
||||
/// This can be used, for example, to optionally freeze scrolling while the user
|
||||
/// is inputing text in a [`TextEdit`] widget contained within the scroll area.
|
||||
/// is typing text in a [`TextEdit`] widget contained within the scroll area.
|
||||
///
|
||||
/// This controls both scrolling directions.
|
||||
pub fn enable_scrolling(mut self, enable: bool) -> Self {
|
||||
|
@ -251,10 +270,22 @@ impl ScrollArea {
|
|||
self
|
||||
}
|
||||
|
||||
/// Can the user drag the scroll area to scroll?
|
||||
///
|
||||
/// This is useful for touch screens.
|
||||
///
|
||||
/// If `true`, the [`ScrollArea`] will sense drags.
|
||||
///
|
||||
/// Default: `true`.
|
||||
pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self {
|
||||
self.drag_to_scroll = drag_to_scroll;
|
||||
self
|
||||
}
|
||||
|
||||
/// For each axis, should the containing area shrink if the content is small?
|
||||
///
|
||||
/// If true, egui will add blank space outside the scroll area.
|
||||
/// If false, egui will add blank space inside the scroll area.
|
||||
/// * If `true`, egui will add blank space outside the scroll area.
|
||||
/// * If `false`, egui will add blank space inside the scroll area.
|
||||
///
|
||||
/// Default: `[true; 2]`.
|
||||
pub fn auto_shrink(mut self, auto_shrink: [bool; 2]) -> Self {
|
||||
|
@ -272,8 +303,8 @@ impl ScrollArea {
|
|||
/// it will remain focused on whatever content viewport the user left it on. If the scroll
|
||||
/// handle is dragged all the way to the right it will again become stuck and remain there
|
||||
/// until manually pulled from the end position.
|
||||
pub fn stick_to_right(mut self) -> Self {
|
||||
self.stick_to_end[0] = true;
|
||||
pub fn stick_to_right(mut self, stick: bool) -> Self {
|
||||
self.stick_to_end[0] = stick;
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -283,8 +314,8 @@ impl ScrollArea {
|
|||
/// it will remain focused on whatever content viewport the user left it on. If the scroll
|
||||
/// handle is dragged to the bottom it will again become stuck and remain there until manually
|
||||
/// pulled from the end position.
|
||||
pub fn stick_to_bottom(mut self) -> Self {
|
||||
self.stick_to_end[1] = true;
|
||||
pub fn stick_to_bottom(mut self, stick: bool) -> Self {
|
||||
self.stick_to_end[1] = stick;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
@ -320,6 +351,7 @@ impl ScrollArea {
|
|||
offset_x,
|
||||
offset_y,
|
||||
scrolling_enabled,
|
||||
drag_to_scroll,
|
||||
stick_to_end,
|
||||
} = self;
|
||||
|
||||
|
@ -394,18 +426,75 @@ impl ScrollArea {
|
|||
|
||||
let content_max_rect = Rect::from_min_size(inner_rect.min - state.offset, content_max_size);
|
||||
let mut content_ui = ui.child_ui(content_max_rect, *ui.layout());
|
||||
let mut content_clip_rect = inner_rect.expand(ui.visuals().clip_rect_margin);
|
||||
content_clip_rect = content_clip_rect.intersect(ui.clip_rect());
|
||||
// Nice handling of forced resizing beyond the possible:
|
||||
for d in 0..2 {
|
||||
if !has_bar[d] {
|
||||
content_clip_rect.max[d] = ui.clip_rect().max[d] - current_bar_use[d];
|
||||
|
||||
{
|
||||
// Clip the content, but only when we really need to:
|
||||
let clip_rect_margin = ui.visuals().clip_rect_margin;
|
||||
let scroll_bar_inner_margin = ui.spacing().scroll_bar_inner_margin;
|
||||
let mut content_clip_rect = ui.clip_rect();
|
||||
for d in 0..2 {
|
||||
if has_bar[d] {
|
||||
if state.content_is_too_large[d] {
|
||||
content_clip_rect.min[d] = inner_rect.min[d] - clip_rect_margin;
|
||||
content_clip_rect.max[d] = inner_rect.max[d] + clip_rect_margin;
|
||||
}
|
||||
|
||||
if state.show_scroll[d] {
|
||||
// Make sure content doesn't cover scroll bars
|
||||
let tiny_gap = 1.0;
|
||||
content_clip_rect.max[1 - d] =
|
||||
inner_rect.max[1 - d] + scroll_bar_inner_margin - tiny_gap;
|
||||
}
|
||||
} else {
|
||||
// Nice handling of forced resizing beyond the possible:
|
||||
content_clip_rect.max[d] = ui.clip_rect().max[d] - current_bar_use[d];
|
||||
}
|
||||
}
|
||||
// Make sure we din't accidentally expand the clip rect
|
||||
content_clip_rect = content_clip_rect.intersect(ui.clip_rect());
|
||||
content_ui.set_clip_rect(content_clip_rect);
|
||||
}
|
||||
content_ui.set_clip_rect(content_clip_rect);
|
||||
|
||||
let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size);
|
||||
|
||||
if (scrolling_enabled && drag_to_scroll)
|
||||
&& (state.content_is_too_large[0] || state.content_is_too_large[1])
|
||||
{
|
||||
// Drag contents to scroll (for touch screens mostly).
|
||||
// We must do this BEFORE adding content to the `ScrollArea`,
|
||||
// or we will steal input from the widgets we contain.
|
||||
let content_response = ui.interact(inner_rect, id.with("area"), Sense::drag());
|
||||
|
||||
if content_response.dragged() {
|
||||
for d in 0..2 {
|
||||
if has_bar[d] {
|
||||
ui.input(|input| {
|
||||
state.offset[d] -= input.pointer.delta()[d];
|
||||
state.vel[d] = input.pointer.velocity()[d];
|
||||
});
|
||||
state.scroll_stuck_to_end[d] = false;
|
||||
} else {
|
||||
state.vel[d] = 0.0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let stop_speed = 20.0; // Pixels per second.
|
||||
let friction_coeff = 1000.0; // Pixels per second squared.
|
||||
let dt = ui.input(|i| i.unstable_dt);
|
||||
|
||||
let friction = friction_coeff * dt;
|
||||
if friction > state.vel.length() || state.vel.length() < stop_speed {
|
||||
state.vel = Vec2::ZERO;
|
||||
} else {
|
||||
state.vel -= friction * state.vel.normalized();
|
||||
// Offset has an inverted coordinate system compared to
|
||||
// the velocity, so we subtract it instead of adding it
|
||||
state.offset -= state.vel * dt;
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Prepared {
|
||||
id,
|
||||
state,
|
||||
|
@ -460,9 +549,13 @@ impl ScrollArea {
|
|||
self.show_viewport(ui, |ui, viewport| {
|
||||
ui.set_height((row_height_with_spacing * total_rows as f32 - spacing.y).at_least(0.0));
|
||||
|
||||
let min_row = (viewport.min.y / row_height_with_spacing).floor() as usize;
|
||||
let max_row = (viewport.max.y / row_height_with_spacing).ceil() as usize + 1;
|
||||
let max_row = max_row.at_most(total_rows);
|
||||
let mut min_row = (viewport.min.y / row_height_with_spacing).floor() as usize;
|
||||
let mut max_row = (viewport.max.y / row_height_with_spacing).ceil() as usize + 1;
|
||||
if max_row > total_rows {
|
||||
let diff = max_row.saturating_sub(min_row);
|
||||
max_row = total_rows;
|
||||
min_row = total_rows.saturating_sub(diff);
|
||||
}
|
||||
|
||||
let y_min = ui.max_rect().top() + min_row as f32 * row_height_with_spacing;
|
||||
let y_max = ui.max_rect().top() + max_row as f32 * row_height_with_spacing;
|
||||
|
@ -479,7 +572,7 @@ impl ScrollArea {
|
|||
|
||||
/// This can be used to only paint the visible part of the contents.
|
||||
///
|
||||
/// `add_contents` is past the viewport, which is the relative view of the content.
|
||||
/// `add_contents` is given the viewport rectangle, which is the relative view of the content.
|
||||
/// So if the passed rect has min = zero, then show the top left content (the user has not scrolled).
|
||||
pub fn show_viewport<R>(
|
||||
self,
|
||||
|
@ -498,18 +591,20 @@ impl ScrollArea {
|
|||
let id = prepared.id;
|
||||
let inner_rect = prepared.inner_rect;
|
||||
let inner = add_contents(&mut prepared.content_ui, prepared.viewport);
|
||||
let state = prepared.end(ui);
|
||||
let (content_size, state) = prepared.end(ui);
|
||||
ScrollAreaOutput {
|
||||
inner,
|
||||
id,
|
||||
state,
|
||||
content_size,
|
||||
inner_rect,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Prepared {
|
||||
fn end(self, ui: &mut Ui) -> State {
|
||||
/// Returns content size and state
|
||||
fn end(self, ui: &mut Ui) -> (Vec2, State) {
|
||||
let Prepared {
|
||||
id,
|
||||
mut state,
|
||||
|
@ -529,7 +624,9 @@ impl Prepared {
|
|||
for d in 0..2 {
|
||||
if has_bar[d] {
|
||||
// We take the scroll target so only this ScrollArea will use it:
|
||||
let scroll_target = content_ui.ctx().frame_state().scroll_target[d].take();
|
||||
let scroll_target = content_ui
|
||||
.ctx()
|
||||
.frame_state_mut(|state| state.scroll_target[d].take());
|
||||
if let Some((scroll, align)) = scroll_target {
|
||||
let min = content_ui.min_rect().min[d];
|
||||
let clip_rect = content_ui.clip_rect();
|
||||
|
@ -580,75 +677,21 @@ impl Prepared {
|
|||
};
|
||||
}
|
||||
|
||||
let mut inner_rect = Rect::from_min_size(inner_rect.min, inner_size);
|
||||
|
||||
// The window that egui sits in can't be expanded by egui, so we need to respect it:
|
||||
for d in 0..2 {
|
||||
if !has_bar[d] {
|
||||
// HACK for when we have a vertical-only scroll area in a top level panel,
|
||||
// and that panel is not wide enough for the contents.
|
||||
// This code ensures we still see the scroll bar!
|
||||
let max = ui.input().screen_rect().max[d]
|
||||
- current_bar_use[d]
|
||||
- ui.spacing().item_spacing[d];
|
||||
inner_rect.max[d] = inner_rect.max[d].at_most(max);
|
||||
// TODO: maybe auto-enable horizontal/vertical scrolling if this limit is reached
|
||||
}
|
||||
}
|
||||
|
||||
inner_rect
|
||||
Rect::from_min_size(inner_rect.min, inner_size)
|
||||
};
|
||||
|
||||
let outer_rect = Rect::from_min_size(inner_rect.min, inner_rect.size() + current_bar_use);
|
||||
|
||||
let content_is_too_small = [
|
||||
let content_is_too_large = [
|
||||
content_size.x > inner_rect.width(),
|
||||
content_size.y > inner_rect.height(),
|
||||
];
|
||||
|
||||
if content_is_too_small[0] || content_is_too_small[1] {
|
||||
// Drag contents to scroll (for touch screens mostly):
|
||||
let sense = if self.scrolling_enabled {
|
||||
Sense::drag()
|
||||
} else {
|
||||
Sense::hover()
|
||||
};
|
||||
let content_response = ui.interact(inner_rect, id.with("area"), sense);
|
||||
|
||||
if content_response.dragged() {
|
||||
for d in 0..2 {
|
||||
if has_bar[d] {
|
||||
state.offset[d] -= ui.input().pointer.delta()[d];
|
||||
state.vel[d] = ui.input().pointer.velocity()[d];
|
||||
state.scroll_stuck_to_end[d] = false;
|
||||
} else {
|
||||
state.vel[d] = 0.0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let stop_speed = 20.0; // Pixels per second.
|
||||
let friction_coeff = 1000.0; // Pixels per second squared.
|
||||
let dt = ui.input().unstable_dt;
|
||||
|
||||
let friction = friction_coeff * dt;
|
||||
if friction > state.vel.length() || state.vel.length() < stop_speed {
|
||||
state.vel = Vec2::ZERO;
|
||||
} else {
|
||||
state.vel -= friction * state.vel.normalized();
|
||||
// Offset has an inverted coordinate system compared to
|
||||
// the velocity, so we subtract it instead of adding it
|
||||
state.offset -= state.vel * dt;
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let max_offset = content_size - inner_rect.size();
|
||||
if scrolling_enabled && ui.rect_contains_pointer(outer_rect) {
|
||||
for d in 0..2 {
|
||||
if has_bar[d] {
|
||||
let mut frame_state = ui.ctx().frame_state();
|
||||
let scroll_delta = frame_state.scroll_delta;
|
||||
let scroll_delta = ui.ctx().frame_state(|fs| fs.scroll_delta);
|
||||
|
||||
let scrolling_up = state.offset[d] > 0.0 && scroll_delta[d] > 0.0;
|
||||
let scrolling_down = state.offset[d] < max_offset[d] && scroll_delta[d] < 0.0;
|
||||
|
@ -656,7 +699,7 @@ impl Prepared {
|
|||
if scrolling_up || scrolling_down {
|
||||
state.offset[d] -= scroll_delta[d];
|
||||
// Clear scroll delta so no parent scroll will use it.
|
||||
frame_state.scroll_delta[d] = 0.0;
|
||||
ui.ctx().frame_state_mut(|fs| fs.scroll_delta[d] = 0.0);
|
||||
state.scroll_stuck_to_end[d] = false;
|
||||
}
|
||||
}
|
||||
|
@ -664,8 +707,8 @@ impl Prepared {
|
|||
}
|
||||
|
||||
let show_scroll_this_frame = [
|
||||
content_is_too_small[0] || always_show_scroll,
|
||||
content_is_too_small[1] || always_show_scroll,
|
||||
content_is_too_large[0] || always_show_scroll,
|
||||
content_is_too_large[1] || always_show_scroll,
|
||||
];
|
||||
|
||||
let max_scroll_bar_width = max_scroll_bar_width_with_margin(ui);
|
||||
|
@ -685,13 +728,29 @@ impl Prepared {
|
|||
continue;
|
||||
}
|
||||
|
||||
// margin between contents and scroll bar
|
||||
let margin = animation_t * ui.spacing().item_spacing.x;
|
||||
let min_cross = inner_rect.max[1 - d] + margin; // left of vertical scroll (d == 1)
|
||||
let max_cross = outer_rect.max[1 - d]; // right of vertical scroll (d == 1)
|
||||
// margin on either side of the scroll bar
|
||||
let inner_margin = animation_t * ui.spacing().scroll_bar_inner_margin;
|
||||
let outer_margin = animation_t * ui.spacing().scroll_bar_outer_margin;
|
||||
let mut min_cross = inner_rect.max[1 - d] + inner_margin; // left of vertical scroll (d == 1)
|
||||
let mut max_cross = outer_rect.max[1 - d] - outer_margin; // right of vertical scroll (d == 1)
|
||||
let min_main = inner_rect.min[d]; // top of vertical scroll (d == 1)
|
||||
let max_main = inner_rect.max[d]; // bottom of vertical scroll (d == 1)
|
||||
|
||||
if ui.clip_rect().max[1 - d] < max_cross + outer_margin {
|
||||
// Move the scrollbar so it is visible. This is needed in some cases.
|
||||
// For instance:
|
||||
// * When we have a vertical-only scroll area in a top level panel,
|
||||
// and that panel is not wide enough for the contents.
|
||||
// * When one ScrollArea is nested inside another, and the outer
|
||||
// is scrolled so that the scroll-bars of the inner ScrollArea (us)
|
||||
// is outside the clip rectangle.
|
||||
// Really this should use the tighter clip_rect that ignores clip_rect_margin, but we don't store that.
|
||||
// clip_rect_margin is quite a hack. It would be nice to get rid of it.
|
||||
let width = max_cross - min_cross;
|
||||
max_cross = ui.clip_rect().max[1 - d] - outer_margin;
|
||||
min_cross = max_cross - width;
|
||||
}
|
||||
|
||||
let outer_scroll_rect = if d == 0 {
|
||||
Rect::from_min_max(
|
||||
pos2(inner_rect.left(), min_cross),
|
||||
|
@ -782,7 +841,7 @@ impl Prepared {
|
|||
),
|
||||
)
|
||||
};
|
||||
let min_handle_size = ui.spacing().scroll_bar_width;
|
||||
let min_handle_size = ui.spacing().scroll_handle_min_length;
|
||||
if handle_rect.size()[d] < min_handle_size {
|
||||
handle_rect = Rect::from_center_size(
|
||||
handle_rect.center(),
|
||||
|
@ -837,14 +896,17 @@ impl Prepared {
|
|||
];
|
||||
|
||||
state.show_scroll = show_scroll_this_frame;
|
||||
state.content_is_too_large = content_is_too_large;
|
||||
|
||||
state.store(ui.ctx(), id);
|
||||
|
||||
state
|
||||
(content_size, state)
|
||||
}
|
||||
}
|
||||
|
||||
/// Width of a vertical scrollbar, or height of a horizontal scroll bar
|
||||
fn max_scroll_bar_width_with_margin(ui: &Ui) -> f32 {
|
||||
ui.spacing().item_spacing.x + ui.spacing().scroll_bar_width
|
||||
ui.spacing().scroll_bar_inner_margin
|
||||
+ ui.spacing().scroll_bar_width
|
||||
+ ui.spacing().scroll_bar_outer_margin
|
||||
}
|
|
@ -10,7 +10,7 @@ use super::*;
|
|||
///
|
||||
/// You can customize:
|
||||
/// * title
|
||||
/// * default, minimum, maximum and/or fixed size
|
||||
/// * default, minimum, maximum and/or fixed size, collapsed/expanded
|
||||
/// * if the window has a scroll area (off by default)
|
||||
/// * if the window can be collapsed (minimized) to just the title bar (yes, by default)
|
||||
/// * if there should be a close button (none by default)
|
||||
|
@ -30,6 +30,7 @@ pub struct Window<'open> {
|
|||
resize: Resize,
|
||||
scroll: ScrollArea,
|
||||
collapsible: bool,
|
||||
default_open: bool,
|
||||
with_title_bar: bool,
|
||||
}
|
||||
|
||||
|
@ -39,7 +40,7 @@ impl<'open> Window<'open> {
|
|||
/// If you need a changing title, you must call `window.id(…)` with a fixed id.
|
||||
pub fn new(title: impl Into<WidgetText>) -> Self {
|
||||
let title = title.into().fallback_text_style(TextStyle::Heading);
|
||||
let area = Area::new(title.text());
|
||||
let area = Area::new(Id::new(title.text()));
|
||||
Self {
|
||||
title,
|
||||
open: None,
|
||||
|
@ -51,6 +52,7 @@ impl<'open> Window<'open> {
|
|||
.default_size([340.0, 420.0]), // Default inner size of a window
|
||||
scroll: ScrollArea::neither(),
|
||||
collapsible: true,
|
||||
default_open: true,
|
||||
with_title_bar: true,
|
||||
}
|
||||
}
|
||||
|
@ -77,15 +79,27 @@ impl<'open> Window<'open> {
|
|||
self
|
||||
}
|
||||
|
||||
/// If `false` the window will be non-interactive.
|
||||
pub fn interactable(mut self, interactable: bool) -> Self {
|
||||
self.area = self.area.interactable(interactable);
|
||||
self
|
||||
}
|
||||
|
||||
/// If `false` the window will be immovable.
|
||||
pub fn movable(mut self, movable: bool) -> Self {
|
||||
self.area = self.area.movable(movable);
|
||||
self
|
||||
}
|
||||
|
||||
/// Usage: `Window::new(…).mutate(|w| w.resize = w.resize.auto_expand_width(true))`
|
||||
/// Not sure this is a good interface for this.
|
||||
// TODO(emilk): I'm not sure this is a good interface for this.
|
||||
pub fn mutate(mut self, mutate: impl Fn(&mut Self)) -> Self {
|
||||
mutate(&mut self);
|
||||
self
|
||||
}
|
||||
|
||||
/// Usage: `Window::new(…).resize(|r| r.auto_expand_width(true))`
|
||||
/// Not sure this is a good interface for this.
|
||||
// TODO(emilk): I'm not sure this is a good interface for this.
|
||||
pub fn resize(mut self, mutate: impl Fn(Resize) -> Resize) -> Self {
|
||||
self.resize = mutate(self.resize);
|
||||
self
|
||||
|
@ -102,6 +116,7 @@ impl<'open> Window<'open> {
|
|||
self.resize = self.resize.min_width(min_width);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set minimum height of the window.
|
||||
pub fn min_height(mut self, min_height: f32) -> Self {
|
||||
self.resize = self.resize.min_height(min_height);
|
||||
|
@ -121,6 +136,30 @@ impl<'open> Window<'open> {
|
|||
self
|
||||
}
|
||||
|
||||
/// Sets the window position and prevents it from being dragged around.
|
||||
pub fn fixed_pos(mut self, pos: impl Into<Pos2>) -> Self {
|
||||
self.area = self.area.fixed_pos(pos);
|
||||
self
|
||||
}
|
||||
|
||||
/// Constrains this window to the screen bounds.
|
||||
pub fn constrain(mut self, constrain: bool) -> Self {
|
||||
self.area = self.area.constrain(constrain);
|
||||
self
|
||||
}
|
||||
|
||||
/// Where the "root" of the window is.
|
||||
///
|
||||
/// For instance, if you set this to [`Align2::RIGHT_TOP`]
|
||||
/// then [`Self::fixed_pos`] will set the position of the right-top
|
||||
/// corner of the window.
|
||||
///
|
||||
/// Default: [`Align2::LEFT_TOP`].
|
||||
pub fn pivot(mut self, pivot: Align2) -> Self {
|
||||
self.area = self.area.pivot(pivot);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set anchor and distance.
|
||||
///
|
||||
/// An anchor of `Align2::RIGHT_TOP` means "put the right-top corner of the window
|
||||
|
@ -137,6 +176,12 @@ impl<'open> Window<'open> {
|
|||
self
|
||||
}
|
||||
|
||||
/// Set initial collapsed state of the window
|
||||
pub fn default_open(mut self, default_open: bool) -> Self {
|
||||
self.default_open = default_open;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set initial size of the window.
|
||||
pub fn default_size(mut self, default_size: impl Into<Vec2>) -> Self {
|
||||
self.resize = self.resize.default_size(default_size);
|
||||
|
@ -148,29 +193,24 @@ impl<'open> Window<'open> {
|
|||
self.resize = self.resize.default_width(default_width);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set initial height of the window.
|
||||
pub fn default_height(mut self, default_height: f32) -> Self {
|
||||
self.resize = self.resize.default_height(default_height);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set initial position and size of the window.
|
||||
pub fn default_rect(self, rect: Rect) -> Self {
|
||||
self.default_pos(rect.min).default_size(rect.size())
|
||||
}
|
||||
|
||||
/// Sets the window position and prevents it from being dragged around.
|
||||
pub fn fixed_pos(mut self, pos: impl Into<Pos2>) -> Self {
|
||||
self.area = self.area.fixed_pos(pos);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the window size and prevents it from being resized by dragging its edges.
|
||||
pub fn fixed_size(mut self, size: impl Into<Vec2>) -> Self {
|
||||
self.resize = self.resize.fixed_size(size);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set initial position and size of the window.
|
||||
pub fn default_rect(self, rect: Rect) -> Self {
|
||||
self.default_pos(rect.min).default_size(rect.size())
|
||||
}
|
||||
|
||||
/// Sets the window pos and size and prevents it from being moved and resized by dragging its edges.
|
||||
pub fn fixed_rect(self, rect: Rect) -> Self {
|
||||
self.fixed_pos(rect.min).fixed_size(rect.size())
|
||||
|
@ -255,12 +295,14 @@ impl<'open> Window<'open> {
|
|||
resize,
|
||||
scroll,
|
||||
collapsible,
|
||||
default_open,
|
||||
with_title_bar,
|
||||
} = self;
|
||||
|
||||
let frame = frame.unwrap_or_else(|| Frame::window(&ctx.style()));
|
||||
|
||||
let is_open = !matches!(open, Some(false)) || ctx.memory().everything_is_visible();
|
||||
let is_explicitly_closed = matches!(open, Some(false));
|
||||
let is_open = !is_explicitly_closed || ctx.memory(|mem| mem.everything_is_visible());
|
||||
area.show_open_close_animation(ctx, &frame, is_open);
|
||||
|
||||
if !is_open {
|
||||
|
@ -271,7 +313,7 @@ impl<'open> Window<'open> {
|
|||
let area_layer_id = area.layer();
|
||||
let resize_id = area_id.with("resize");
|
||||
let mut collapsing =
|
||||
CollapsingState::load_with_default_open(ctx, area_id.with("collapsing"), true);
|
||||
CollapsingState::load_with_default_open(ctx, area_id.with("collapsing"), default_open);
|
||||
|
||||
let is_collapsed = with_title_bar && !collapsing.is_open();
|
||||
let possible = PossibleInteractions::new(&area, &resize, is_collapsed);
|
||||
|
@ -298,7 +340,7 @@ impl<'open> Window<'open> {
|
|||
// Calculate roughly how much larger the window size is compared to the inner rect
|
||||
let title_bar_height = if with_title_bar {
|
||||
let style = ctx.style();
|
||||
title.font_height(&ctx.fonts(), &style) + title_content_spacing
|
||||
ctx.fonts(|f| title.font_height(f, &style)) + title_content_spacing
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
@ -384,7 +426,7 @@ impl<'open> Window<'open> {
|
|||
ctx.style().visuals.widgets.active,
|
||||
);
|
||||
} else if let Some(hover_interaction) = hover_interaction {
|
||||
if ctx.input().pointer.has_pointer() {
|
||||
if ctx.input(|i| i.pointer.has_pointer()) {
|
||||
paint_frame_interaction(
|
||||
&mut area_content_ui,
|
||||
outer_rect,
|
||||
|
@ -396,9 +438,12 @@ impl<'open> Window<'open> {
|
|||
content_inner
|
||||
};
|
||||
|
||||
area.state_mut().pos = ctx
|
||||
.constrain_window_rect_to_area(area.state().rect(), area.drag_bounds())
|
||||
.min;
|
||||
{
|
||||
let pos = ctx
|
||||
.constrain_window_rect_to_area(area.state().rect(), area.drag_bounds())
|
||||
.left_top();
|
||||
area.state_mut().set_left_top_pos(pos);
|
||||
}
|
||||
|
||||
let full_response = area.end(ctx, area_content_ui);
|
||||
|
||||
|
@ -479,13 +524,13 @@ pub(crate) struct WindowInteraction {
|
|||
impl WindowInteraction {
|
||||
pub fn set_cursor(&self, ctx: &Context) {
|
||||
if (self.left && self.top) || (self.right && self.bottom) {
|
||||
ctx.output().cursor_icon = CursorIcon::ResizeNwSe;
|
||||
ctx.set_cursor_icon(CursorIcon::ResizeNwSe);
|
||||
} else if (self.right && self.top) || (self.left && self.bottom) {
|
||||
ctx.output().cursor_icon = CursorIcon::ResizeNeSw;
|
||||
ctx.set_cursor_icon(CursorIcon::ResizeNeSw);
|
||||
} else if self.left || self.right {
|
||||
ctx.output().cursor_icon = CursorIcon::ResizeHorizontal;
|
||||
ctx.set_cursor_icon(CursorIcon::ResizeHorizontal);
|
||||
} else if self.bottom || self.top {
|
||||
ctx.output().cursor_icon = CursorIcon::ResizeVertical;
|
||||
ctx.set_cursor_icon(CursorIcon::ResizeVertical);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -507,8 +552,8 @@ fn interact(
|
|||
|
||||
let new_rect = ctx.constrain_window_rect_to_area(new_rect, area.drag_bounds());
|
||||
|
||||
// TODO: add this to a Window state instead as a command "move here next frame"
|
||||
area.state_mut().pos = new_rect.min;
|
||||
// TODO(emilk): add this to a Window state instead as a command "move here next frame"
|
||||
area.state_mut().set_left_top_pos(new_rect.left_top());
|
||||
|
||||
if window_interaction.is_resize() {
|
||||
if let Some(mut state) = resize::State::load(ctx, resize_id) {
|
||||
|
@ -517,7 +562,7 @@ fn interact(
|
|||
}
|
||||
}
|
||||
|
||||
ctx.memory().areas.move_to_top(area_layer_id);
|
||||
ctx.memory_mut(|mem| mem.areas.move_to_top(area_layer_id));
|
||||
Some(window_interaction)
|
||||
}
|
||||
|
||||
|
@ -525,11 +570,11 @@ fn move_and_resize_window(ctx: &Context, window_interaction: &WindowInteraction)
|
|||
window_interaction.set_cursor(ctx);
|
||||
|
||||
// Only move/resize windows with primary mouse button:
|
||||
if !ctx.input().pointer.primary_down() {
|
||||
if !ctx.input(|i| i.pointer.primary_down()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let pointer_pos = ctx.input().pointer.interact_pos()?;
|
||||
let pointer_pos = ctx.input(|i| i.pointer.interact_pos())?;
|
||||
let mut rect = window_interaction.start_rect; // prevent drift
|
||||
|
||||
if window_interaction.is_resize() {
|
||||
|
@ -551,8 +596,8 @@ fn move_and_resize_window(ctx: &Context, window_interaction: &WindowInteraction)
|
|||
// but we want anything interactive in the window (e.g. slider) to steal
|
||||
// the drag from us. It is therefor important not to move the window the first frame,
|
||||
// but instead let other widgets to the steal. HACK.
|
||||
if !ctx.input().pointer.any_pressed() {
|
||||
let press_origin = ctx.input().pointer.press_origin()?;
|
||||
if !ctx.input(|i| i.pointer.any_pressed()) {
|
||||
let press_origin = ctx.input(|i| i.pointer.press_origin())?;
|
||||
let delta = pointer_pos - press_origin;
|
||||
rect = rect.translate(delta);
|
||||
}
|
||||
|
@ -570,29 +615,31 @@ fn window_interaction(
|
|||
rect: Rect,
|
||||
) -> Option<WindowInteraction> {
|
||||
{
|
||||
let drag_id = ctx.memory().interaction.drag_id;
|
||||
let drag_id = ctx.memory(|mem| mem.interaction.drag_id);
|
||||
|
||||
if drag_id.is_some() && drag_id != Some(id) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let mut window_interaction = { ctx.memory().window_interaction };
|
||||
let mut window_interaction = ctx.memory(|mem| mem.window_interaction);
|
||||
|
||||
if window_interaction.is_none() {
|
||||
if let Some(hover_window_interaction) = resize_hover(ctx, possible, area_layer_id, rect) {
|
||||
hover_window_interaction.set_cursor(ctx);
|
||||
if ctx.input().pointer.any_pressed() && ctx.input().pointer.primary_down() {
|
||||
ctx.memory().interaction.drag_id = Some(id);
|
||||
ctx.memory().interaction.drag_is_window = true;
|
||||
window_interaction = Some(hover_window_interaction);
|
||||
ctx.memory().window_interaction = window_interaction;
|
||||
if ctx.input(|i| i.pointer.any_pressed() && i.pointer.primary_down()) {
|
||||
ctx.memory_mut(|mem| {
|
||||
mem.interaction.drag_id = Some(id);
|
||||
mem.interaction.drag_is_window = true;
|
||||
window_interaction = Some(hover_window_interaction);
|
||||
mem.window_interaction = window_interaction;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(window_interaction) = window_interaction {
|
||||
let is_active = ctx.memory().interaction.drag_id == Some(id);
|
||||
let is_active = ctx.memory_mut(|mem| mem.interaction.drag_id == Some(id));
|
||||
|
||||
if is_active && window_interaction.area_layer_id == area_layer_id {
|
||||
return Some(window_interaction);
|
||||
|
@ -608,9 +655,9 @@ fn resize_hover(
|
|||
area_layer_id: LayerId,
|
||||
rect: Rect,
|
||||
) -> Option<WindowInteraction> {
|
||||
let pointer = ctx.input().pointer.interact_pos()?;
|
||||
let pointer = ctx.input(|i| i.pointer.interact_pos())?;
|
||||
|
||||
if ctx.input().pointer.any_down() && !ctx.input().pointer.any_pressed() {
|
||||
if ctx.input(|i| i.pointer.any_down() && !i.pointer.any_pressed()) {
|
||||
return None; // already dragging (something)
|
||||
}
|
||||
|
||||
|
@ -620,7 +667,7 @@ fn resize_hover(
|
|||
}
|
||||
}
|
||||
|
||||
if ctx.memory().interaction.drag_interest {
|
||||
if ctx.memory(|mem| mem.interaction.drag_interest) {
|
||||
// Another widget will become active if we drag here
|
||||
return None;
|
||||
}
|
||||
|
@ -760,12 +807,15 @@ fn paint_frame_interaction(
|
|||
struct TitleBar {
|
||||
/// A title Id used for dragging windows
|
||||
id: Id,
|
||||
|
||||
/// Prepared text in the title
|
||||
title_galley: WidgetTextGalley,
|
||||
|
||||
/// Size of the title bar in a collapsed state (if window is collapsible),
|
||||
/// which includes all necessary space for showing the expand button, the
|
||||
/// title and the close button.
|
||||
min_rect: Rect,
|
||||
|
||||
/// Size of the title bar in an expanded state. This size become known only
|
||||
/// after expanding window and painting its content
|
||||
rect: Rect,
|
||||
|
@ -779,8 +829,8 @@ fn show_title_bar(
|
|||
collapsible: bool,
|
||||
) -> TitleBar {
|
||||
let inner_response = ui.horizontal(|ui| {
|
||||
let height = title
|
||||
.font_height(&ui.fonts(), ui.style())
|
||||
let height = ui
|
||||
.fonts(|fonts| title.font_height(fonts, ui.style()))
|
||||
.max(ui.spacing().interact_size.y);
|
||||
ui.set_min_height(height);
|
||||
|
||||
|
@ -874,8 +924,11 @@ impl TitleBar {
|
|||
ui.painter().hline(outer_rect.x_range(), y, stroke);
|
||||
}
|
||||
|
||||
// Don't cover the close- and collapse buttons:
|
||||
let double_click_rect = self.rect.shrink2(vec2(32.0, 0.0));
|
||||
|
||||
if ui
|
||||
.interact(self.rect, self.id, Sense::click())
|
||||
.interact(double_click_rect, self.id, Sense::click())
|
||||
.double_clicked()
|
||||
&& collapsible
|
||||
{
|
1792
crates/egui/src/context.rs
Normal file
1792
crates/egui/src/context.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -13,10 +13,10 @@ use crate::emath::*;
|
|||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct RawInput {
|
||||
/// Position and size of the area that egui should use.
|
||||
/// Position and size of the area that egui should use, in points.
|
||||
/// Usually you would set this to
|
||||
///
|
||||
/// `Some(Rect::from_pos_size(Default::default(), screen_size))`.
|
||||
/// `Some(Rect::from_pos_size(Default::default(), screen_size_in_points))`.
|
||||
///
|
||||
/// but you could also constrain egui to some smaller portion of your window if you like.
|
||||
///
|
||||
|
@ -62,6 +62,9 @@ pub struct RawInput {
|
|||
/// Note: when using `eframe` on Windows you need to enable
|
||||
/// drag-and-drop support using `eframe::NativeOptions`.
|
||||
pub dropped_files: Vec<DroppedFile>,
|
||||
|
||||
/// The window has the keyboard focus (i.e. is receiving key presses).
|
||||
pub has_focus: bool,
|
||||
}
|
||||
|
||||
impl Default for RawInput {
|
||||
|
@ -76,6 +79,7 @@ impl Default for RawInput {
|
|||
events: vec![],
|
||||
hovered_files: Default::default(),
|
||||
dropped_files: Default::default(),
|
||||
has_focus: true, // integrations opt into global focus tracking
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -96,6 +100,7 @@ impl RawInput {
|
|||
events: std::mem::take(&mut self.events),
|
||||
hovered_files: self.hovered_files.clone(),
|
||||
dropped_files: std::mem::take(&mut self.dropped_files),
|
||||
has_focus: self.has_focus,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -111,6 +116,7 @@ impl RawInput {
|
|||
mut events,
|
||||
mut hovered_files,
|
||||
mut dropped_files,
|
||||
has_focus,
|
||||
} = newer;
|
||||
|
||||
self.screen_rect = screen_rect.or(self.screen_rect);
|
||||
|
@ -122,29 +128,34 @@ impl RawInput {
|
|||
self.events.append(&mut events);
|
||||
self.hovered_files.append(&mut hovered_files);
|
||||
self.dropped_files.append(&mut dropped_files);
|
||||
self.has_focus = has_focus;
|
||||
}
|
||||
}
|
||||
|
||||
/// A file about to be dropped into egui.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct HoveredFile {
|
||||
/// Set by the `egui-winit` backend.
|
||||
pub path: Option<std::path::PathBuf>,
|
||||
|
||||
/// With the `eframe` web backend, this is set to the mime-type of the file (if available).
|
||||
pub mime: String,
|
||||
}
|
||||
|
||||
/// A file dropped into egui.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct DroppedFile {
|
||||
/// Set by the `egui-winit` backend.
|
||||
pub path: Option<std::path::PathBuf>,
|
||||
|
||||
/// Name of the file. Set by the `eframe` web backend.
|
||||
pub name: String,
|
||||
|
||||
/// Set by the `eframe` web backend.
|
||||
pub last_modified: Option<std::time::SystemTime>,
|
||||
|
||||
/// Set by the `eframe` web backend.
|
||||
pub bytes: Option<std::sync::Arc<[u8]>>,
|
||||
}
|
||||
|
@ -157,19 +168,34 @@ pub struct DroppedFile {
|
|||
pub enum Event {
|
||||
/// The integration detected a "copy" event (e.g. Cmd+C).
|
||||
Copy,
|
||||
|
||||
/// The integration detected a "cut" event (e.g. Cmd+X).
|
||||
Cut,
|
||||
|
||||
/// The integration detected a "paste" event (e.g. Cmd+V).
|
||||
Paste(String),
|
||||
|
||||
/// Text input, e.g. via keyboard.
|
||||
///
|
||||
/// When the user presses enter/return, do not send a [`Text`](Event::Text) (just [`Key::Enter`]).
|
||||
Text(String),
|
||||
|
||||
/// A key was pressed or released.
|
||||
Key {
|
||||
key: Key,
|
||||
|
||||
/// Was it pressed or released?
|
||||
pressed: bool,
|
||||
|
||||
/// If this is a `pressed` event, is it a key-repeat?
|
||||
///
|
||||
/// On many platforms, holding down a key produces many repeated "pressed" events for it, so called key-repeats.
|
||||
/// Sometimes you will want to ignore such events, and this lets you do that.
|
||||
///
|
||||
/// egui will automatically detect such repeat events and mark them as such here.
|
||||
/// Therefore, if you are writing an egui integration, you do not need to set this (just set it to `false`).
|
||||
repeat: bool,
|
||||
|
||||
/// The state of the modifier keys at the time of the event.
|
||||
modifiers: Modifiers,
|
||||
},
|
||||
|
@ -181,13 +207,17 @@ pub enum Event {
|
|||
PointerButton {
|
||||
/// Where is the pointer?
|
||||
pos: Pos2,
|
||||
|
||||
/// What mouse button? For touches, use [`PointerButton::Primary`].
|
||||
button: PointerButton,
|
||||
|
||||
/// Was it the button/touch pressed this frame, or released?
|
||||
pressed: bool,
|
||||
|
||||
/// The state of the modifier keys at the time of the event.
|
||||
modifiers: Modifiers,
|
||||
},
|
||||
|
||||
/// The mouse left the screen, or the last/primary touch input disappeared.
|
||||
///
|
||||
/// This means there is no longer a cursor on the screen for hovering etc.
|
||||
|
@ -218,8 +248,10 @@ pub enum Event {
|
|||
|
||||
/// IME composition start.
|
||||
CompositionStart,
|
||||
|
||||
/// A new IME candidate is being suggested.
|
||||
CompositionUpdate(String),
|
||||
|
||||
/// IME composition ended with this final result.
|
||||
CompositionEnd(String),
|
||||
|
||||
|
@ -229,18 +261,26 @@ pub enum Event {
|
|||
/// Hashed device identifier (if available; may be zero).
|
||||
/// Can be used to separate touches from different devices.
|
||||
device_id: TouchDeviceId,
|
||||
|
||||
/// Unique identifier of a finger/pen. Value is stable from touch down
|
||||
/// to lift-up
|
||||
id: TouchId,
|
||||
|
||||
/// One of: start move end cancel.
|
||||
phase: TouchPhase,
|
||||
|
||||
/// Position of the touch (or where the touch was last detected)
|
||||
pos: Pos2,
|
||||
|
||||
/// Describes how hard the touch device was pressed. May always be `0` if the platform does
|
||||
/// not support pressure sensitivity.
|
||||
/// The value is in the range from 0.0 (no pressure) to 1.0 (maximum pressure).
|
||||
force: f32,
|
||||
},
|
||||
|
||||
/// An assistive technology (e.g. screen reader) requested an action.
|
||||
#[cfg(feature = "accesskit")]
|
||||
AccessKitActionRequest(accesskit::ActionRequest),
|
||||
}
|
||||
|
||||
/// Mouse button (or similar for touch input)
|
||||
|
@ -249,20 +289,32 @@ pub enum Event {
|
|||
pub enum PointerButton {
|
||||
/// The primary mouse button is usually the left one.
|
||||
Primary = 0,
|
||||
|
||||
/// The secondary mouse button is usually the right one,
|
||||
/// and most often used for context menus or other optional things.
|
||||
Secondary = 1,
|
||||
|
||||
/// The tertiary mouse button is usually the middle mouse button (e.g. clicking the scroll wheel).
|
||||
Middle = 2,
|
||||
|
||||
/// The first extra mouse button on some mice. In web typically corresponds to the Browser back button.
|
||||
Extra1 = 3,
|
||||
|
||||
/// The second extra mouse button on some mice. In web typically corresponds to the Browser forward button.
|
||||
Extra2 = 4,
|
||||
}
|
||||
|
||||
/// Number of pointer buttons supported by egui, i.e. the number of possible states of [`PointerButton`].
|
||||
pub const NUM_POINTER_BUTTONS: usize = 3;
|
||||
pub const NUM_POINTER_BUTTONS: usize = 5;
|
||||
|
||||
/// State of the modifier keys. These must be fed to egui.
|
||||
///
|
||||
/// The best way to compare [`Modifiers`] is by using [`Modifiers::matches`].
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
///
|
||||
/// NOTE: For cross-platform uses, ALT+SHIFT is a bad combination of modifiers
|
||||
/// as on mac that is how you type special characters,
|
||||
/// so those key presses are usually not reported to egui.
|
||||
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct Modifiers {
|
||||
/// Either of the alt keys are down (option ⌥ on Mac).
|
||||
|
@ -286,10 +338,6 @@ pub struct Modifiers {
|
|||
}
|
||||
|
||||
impl Modifiers {
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
pub const NONE: Self = Self {
|
||||
alt: false,
|
||||
ctrl: false,
|
||||
|
@ -319,6 +367,16 @@ impl Modifiers {
|
|||
mac_cmd: false,
|
||||
command: false,
|
||||
};
|
||||
|
||||
#[deprecated = "Use `Modifiers::ALT | Modifiers::SHIFT` instead"]
|
||||
pub const ALT_SHIFT: Self = Self {
|
||||
alt: true,
|
||||
ctrl: false,
|
||||
shift: true,
|
||||
mac_cmd: false,
|
||||
command: false,
|
||||
};
|
||||
|
||||
/// The Mac ⌘ Command key
|
||||
pub const MAC_CMD: Self = Self {
|
||||
alt: false,
|
||||
|
@ -327,6 +385,7 @@ impl Modifiers {
|
|||
mac_cmd: true,
|
||||
command: false,
|
||||
};
|
||||
|
||||
/// On Mac: ⌘ Command key, elsewhere: Ctrl key
|
||||
pub const COMMAND: Self = Self {
|
||||
alt: false,
|
||||
|
@ -336,24 +395,50 @@ impl Modifiers {
|
|||
command: true,
|
||||
};
|
||||
|
||||
#[inline(always)]
|
||||
/// ```
|
||||
/// # use egui::Modifiers;
|
||||
/// assert_eq!(
|
||||
/// Modifiers::CTRL | Modifiers::ALT,
|
||||
/// Modifiers { ctrl: true, alt: true, ..Default::default() }
|
||||
/// );
|
||||
/// assert_eq!(
|
||||
/// Modifiers::ALT.plus(Modifiers::CTRL),
|
||||
/// Modifiers::CTRL.plus(Modifiers::ALT),
|
||||
/// );
|
||||
/// assert_eq!(
|
||||
/// Modifiers::CTRL | Modifiers::ALT,
|
||||
/// Modifiers::CTRL.plus(Modifiers::ALT),
|
||||
/// );
|
||||
/// ```
|
||||
#[inline]
|
||||
pub const fn plus(self, rhs: Self) -> Self {
|
||||
Self {
|
||||
alt: self.alt | rhs.alt,
|
||||
ctrl: self.ctrl | rhs.ctrl,
|
||||
shift: self.shift | rhs.shift,
|
||||
mac_cmd: self.mac_cmd | rhs.mac_cmd,
|
||||
command: self.command | rhs.command,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_none(&self) -> bool {
|
||||
self == &Self::default()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
#[inline]
|
||||
pub fn any(&self) -> bool {
|
||||
!self.is_none()
|
||||
}
|
||||
|
||||
/// Is shift the only pressed button?
|
||||
#[inline(always)]
|
||||
#[inline]
|
||||
pub fn shift_only(&self) -> bool {
|
||||
self.shift && !(self.alt || self.command)
|
||||
}
|
||||
|
||||
/// true if only [`Self::ctrl`] or only [`Self::mac_cmd`] is pressed.
|
||||
#[inline(always)]
|
||||
#[inline]
|
||||
pub fn command_only(&self) -> bool {
|
||||
!self.alt && !self.shift && self.command
|
||||
}
|
||||
|
@ -408,24 +493,90 @@ impl Modifiers {
|
|||
|
||||
impl std::ops::BitOr for Modifiers {
|
||||
type Output = Self;
|
||||
|
||||
#[inline]
|
||||
fn bitor(self, rhs: Self) -> Self {
|
||||
Self {
|
||||
alt: self.alt | rhs.alt,
|
||||
ctrl: self.ctrl | rhs.ctrl,
|
||||
shift: self.shift | rhs.shift,
|
||||
mac_cmd: self.mac_cmd | rhs.mac_cmd,
|
||||
command: self.command | rhs.command,
|
||||
}
|
||||
self.plus(rhs)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Names of different modifier keys.
|
||||
///
|
||||
/// Used to name modifiers.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct ModifierNames<'a> {
|
||||
pub is_short: bool,
|
||||
|
||||
pub alt: &'a str,
|
||||
pub ctrl: &'a str,
|
||||
pub shift: &'a str,
|
||||
pub mac_cmd: &'a str,
|
||||
|
||||
/// What goes between the names
|
||||
pub concat: &'a str,
|
||||
}
|
||||
|
||||
impl ModifierNames<'static> {
|
||||
/// ⌥ ^ ⇧ ⌘ - NOTE: not supported by the default egui font.
|
||||
pub const SYMBOLS: Self = Self {
|
||||
is_short: true,
|
||||
alt: "⌥",
|
||||
ctrl: "^",
|
||||
shift: "⇧",
|
||||
mac_cmd: "⌘",
|
||||
concat: "",
|
||||
};
|
||||
|
||||
/// Alt, Ctrl, Shift, Cmd
|
||||
pub const NAMES: Self = Self {
|
||||
is_short: false,
|
||||
alt: "Alt",
|
||||
ctrl: "Ctrl",
|
||||
shift: "Shift",
|
||||
mac_cmd: "Cmd",
|
||||
concat: "+",
|
||||
};
|
||||
}
|
||||
|
||||
impl<'a> ModifierNames<'a> {
|
||||
pub fn format(&self, modifiers: &Modifiers, is_mac: bool) -> String {
|
||||
let mut s = String::new();
|
||||
|
||||
let mut append_if = |modifier_is_active, modifier_name| {
|
||||
if modifier_is_active {
|
||||
if !s.is_empty() {
|
||||
s += self.concat;
|
||||
}
|
||||
s += modifier_name;
|
||||
}
|
||||
};
|
||||
|
||||
if is_mac {
|
||||
append_if(modifiers.ctrl, self.ctrl);
|
||||
append_if(modifiers.shift, self.shift);
|
||||
append_if(modifiers.alt, self.alt);
|
||||
append_if(modifiers.mac_cmd || modifiers.command, self.mac_cmd);
|
||||
} else {
|
||||
append_if(modifiers.ctrl || modifiers.command, self.ctrl);
|
||||
append_if(modifiers.alt, self.alt);
|
||||
append_if(modifiers.shift, self.shift);
|
||||
}
|
||||
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Keyboard keys.
|
||||
///
|
||||
/// Includes all keys egui is interested in (such as `Home` and `End`)
|
||||
/// plus a few that are useful for detecting keyboard shortcuts.
|
||||
///
|
||||
/// Many keys are omitted because they are not always physical keys (depending on keyboard language), e.g. `;` and `§`,
|
||||
/// and are therefor unsuitable as keyboard shortcuts if you want your app to be portable.
|
||||
/// and are therefore unsuitable as keyboard shortcuts if you want your app to be portable.
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub enum Key {
|
||||
|
@ -447,6 +598,11 @@ pub enum Key {
|
|||
PageUp,
|
||||
PageDown,
|
||||
|
||||
/// The virtual keycode for the Minus key.
|
||||
Minus,
|
||||
/// The virtual keycode for the Plus/Equals key.
|
||||
PlusEquals,
|
||||
|
||||
/// Either from the main row or from the numpad.
|
||||
Num0,
|
||||
/// Either from the main row or from the numpad.
|
||||
|
@ -470,32 +626,199 @@ pub enum Key {
|
|||
|
||||
A, // Used for cmd+A (select All)
|
||||
B,
|
||||
C,
|
||||
D,
|
||||
E,
|
||||
F,
|
||||
G,
|
||||
H,
|
||||
I,
|
||||
J,
|
||||
C, // |CMD COPY|
|
||||
D, // |CMD BOOKMARK|
|
||||
E, // |CMD SEARCH|
|
||||
F, // |CMD FIND firefox & chrome|
|
||||
G, // |CMD FIND chrome|
|
||||
H, // |CMD History|
|
||||
I, // italics
|
||||
J, // |CMD SEARCH firefox/DOWNLOAD chrome|
|
||||
K, // Used for ctrl+K (delete text after cursor)
|
||||
L,
|
||||
M,
|
||||
N,
|
||||
O,
|
||||
P,
|
||||
O, // |CMD OPEN|
|
||||
P, // |CMD PRINT|
|
||||
Q,
|
||||
R,
|
||||
S,
|
||||
T,
|
||||
R, // |CMD REFRESH|
|
||||
S, // |CMD SAVE|
|
||||
T, // |CMD TAB|
|
||||
U, // Used for ctrl+U (delete text before cursor)
|
||||
V,
|
||||
V, // |CMD PASTE|
|
||||
W, // Used for ctrl+W (delete previous word)
|
||||
X,
|
||||
X, // |CMD CUT|
|
||||
Y,
|
||||
Z, // Used for cmd+Z (undo)
|
||||
Z, // |CMD UNDO|
|
||||
|
||||
// The function keys:
|
||||
F1,
|
||||
F2,
|
||||
F3,
|
||||
F4,
|
||||
F5, // |CMD REFRESH|
|
||||
F6,
|
||||
F7,
|
||||
F8,
|
||||
F9,
|
||||
F10,
|
||||
F11,
|
||||
F12,
|
||||
F13,
|
||||
F14,
|
||||
F15,
|
||||
F16,
|
||||
F17,
|
||||
F18,
|
||||
F19,
|
||||
F20,
|
||||
}
|
||||
|
||||
impl Key {
|
||||
/// Emoji or name representing the key
|
||||
pub fn symbol_or_name(self) -> &'static str {
|
||||
// TODO(emilk): add support for more unicode symbols (see for instance https://wincent.com/wiki/Unicode_representations_of_modifier_keys).
|
||||
// Before we do we must first make sure they are supported in `Fonts` though,
|
||||
// so perhaps this functions needs to take a `supports_character: impl Fn(char) -> bool` or something.
|
||||
match self {
|
||||
Key::ArrowDown => "⏷",
|
||||
Key::ArrowLeft => "⏴",
|
||||
Key::ArrowRight => "⏵",
|
||||
Key::ArrowUp => "⏶",
|
||||
Key::Minus => "-",
|
||||
Key::PlusEquals => "+",
|
||||
_ => self.name(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Human-readable English name.
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Key::ArrowDown => "Down",
|
||||
Key::ArrowLeft => "Left",
|
||||
Key::ArrowRight => "Right",
|
||||
Key::ArrowUp => "Up",
|
||||
Key::Escape => "Escape",
|
||||
Key::Tab => "Tab",
|
||||
Key::Backspace => "Backspace",
|
||||
Key::Enter => "Enter",
|
||||
Key::Space => "Space",
|
||||
Key::Insert => "Insert",
|
||||
Key::Delete => "Delete",
|
||||
Key::Home => "Home",
|
||||
Key::End => "End",
|
||||
Key::PageUp => "PageUp",
|
||||
Key::PageDown => "PageDown",
|
||||
Key::Minus => "Minus",
|
||||
Key::PlusEquals => "Plus",
|
||||
Key::Num0 => "0",
|
||||
Key::Num1 => "1",
|
||||
Key::Num2 => "2",
|
||||
Key::Num3 => "3",
|
||||
Key::Num4 => "4",
|
||||
Key::Num5 => "5",
|
||||
Key::Num6 => "6",
|
||||
Key::Num7 => "7",
|
||||
Key::Num8 => "8",
|
||||
Key::Num9 => "9",
|
||||
Key::A => "A",
|
||||
Key::B => "B",
|
||||
Key::C => "C",
|
||||
Key::D => "D",
|
||||
Key::E => "E",
|
||||
Key::F => "F",
|
||||
Key::G => "G",
|
||||
Key::H => "H",
|
||||
Key::I => "I",
|
||||
Key::J => "J",
|
||||
Key::K => "K",
|
||||
Key::L => "L",
|
||||
Key::M => "M",
|
||||
Key::N => "N",
|
||||
Key::O => "O",
|
||||
Key::P => "P",
|
||||
Key::Q => "Q",
|
||||
Key::R => "R",
|
||||
Key::S => "S",
|
||||
Key::T => "T",
|
||||
Key::U => "U",
|
||||
Key::V => "V",
|
||||
Key::W => "W",
|
||||
Key::X => "X",
|
||||
Key::Y => "Y",
|
||||
Key::Z => "Z",
|
||||
Key::F1 => "F1",
|
||||
Key::F2 => "F2",
|
||||
Key::F3 => "F3",
|
||||
Key::F4 => "F4",
|
||||
Key::F5 => "F5",
|
||||
Key::F6 => "F6",
|
||||
Key::F7 => "F7",
|
||||
Key::F8 => "F8",
|
||||
Key::F9 => "F9",
|
||||
Key::F10 => "F10",
|
||||
Key::F11 => "F11",
|
||||
Key::F12 => "F12",
|
||||
Key::F13 => "F13",
|
||||
Key::F14 => "F14",
|
||||
Key::F15 => "F15",
|
||||
Key::F16 => "F16",
|
||||
Key::F17 => "F17",
|
||||
Key::F18 => "F18",
|
||||
Key::F19 => "F19",
|
||||
Key::F20 => "F20",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// A keyboard shortcut, e.g. `Ctrl+Alt+W`.
|
||||
///
|
||||
/// Can be used with [`crate::InputState::consume_shortcut`]
|
||||
/// and [`crate::Context::format_shortcut`].
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||
pub struct KeyboardShortcut {
|
||||
pub modifiers: Modifiers,
|
||||
pub key: Key,
|
||||
}
|
||||
|
||||
impl KeyboardShortcut {
|
||||
pub const fn new(modifiers: Modifiers, key: Key) -> Self {
|
||||
Self { modifiers, key }
|
||||
}
|
||||
|
||||
pub fn format(&self, names: &ModifierNames<'_>, is_mac: bool) -> String {
|
||||
let mut s = names.format(&self.modifiers, is_mac);
|
||||
if !s.is_empty() {
|
||||
s += names.concat;
|
||||
}
|
||||
if names.is_short {
|
||||
s += self.key.symbol_or_name();
|
||||
} else {
|
||||
s += self.key.name();
|
||||
}
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_kb_shortcut() {
|
||||
let cmd_shift_f = KeyboardShortcut::new(Modifiers::COMMAND | Modifiers::SHIFT, Key::F);
|
||||
assert_eq!(
|
||||
cmd_shift_f.format(&ModifierNames::NAMES, false),
|
||||
"Ctrl+Shift+F"
|
||||
);
|
||||
assert_eq!(
|
||||
cmd_shift_f.format(&ModifierNames::NAMES, true),
|
||||
"Shift+Cmd+F"
|
||||
);
|
||||
assert_eq!(cmd_shift_f.format(&ModifierNames::SYMBOLS, false), "^⇧F");
|
||||
assert_eq!(cmd_shift_f.format(&ModifierNames::SYMBOLS, true), "⇧⌘F");
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
impl RawInput {
|
||||
pub fn ui(&self, ui: &mut crate::Ui) {
|
||||
let Self {
|
||||
|
@ -508,6 +831,7 @@ impl RawInput {
|
|||
events,
|
||||
hovered_files,
|
||||
dropped_files,
|
||||
has_focus,
|
||||
} = self;
|
||||
|
||||
ui.label(format!("screen_rect: {:?} points", screen_rect));
|
||||
|
@ -525,6 +849,7 @@ impl RawInput {
|
|||
ui.label(format!("modifiers: {:#?}", modifiers));
|
||||
ui.label(format!("hovered_files: {}", hovered_files.len()));
|
||||
ui.label(format!("dropped_files: {}", dropped_files.len()));
|
||||
ui.label(format!("has_focus: {}", has_focus));
|
||||
ui.scope(|ui| {
|
||||
ui.set_min_height(150.0);
|
||||
ui.label(format!("events: {:#?}", events))
|
||||
|
@ -551,12 +876,15 @@ pub struct TouchId(pub u64);
|
|||
pub enum TouchPhase {
|
||||
/// User just placed a touch point on the touch surface
|
||||
Start,
|
||||
|
||||
/// User moves a touch point along the surface. This event is also sent when
|
||||
/// any attributes (position, force, …) of the touch point change.
|
||||
Move,
|
||||
|
||||
/// User lifted the finger or pen from the surface, or slid off the edge of
|
||||
/// the surface
|
||||
End,
|
||||
|
||||
/// Touch operation has been disrupted by something (various reasons are possible,
|
||||
/// maybe a pop-up alert or any other kind of interruption which may not have
|
||||
/// been intended by the user)
|
|
@ -10,10 +10,15 @@ pub struct FullOutput {
|
|||
/// Non-rendering related output.
|
||||
pub platform_output: PlatformOutput,
|
||||
|
||||
/// If `true`, egui is requesting immediate repaint (i.e. on the next frame).
|
||||
/// If `Duration::is_zero()`, egui is requesting immediate repaint (i.e. on the next frame).
|
||||
///
|
||||
/// This happens for instance when there is an animation, or if a user has called `Context::request_repaint()`.
|
||||
pub needs_repaint: bool,
|
||||
///
|
||||
/// If `Duration` is greater than zero, egui wants to be repainted at or before the specified
|
||||
/// duration elapses. when in reactive mode, egui spends forever waiting for input and only then,
|
||||
/// will it repaint itself. this can be used to make sure that backend will only wait for a
|
||||
/// specified amount of time, and repaint egui without any new input.
|
||||
pub repaint_after: std::time::Duration,
|
||||
|
||||
/// Texture changes since last frame (including the font texture).
|
||||
///
|
||||
|
@ -32,13 +37,13 @@ impl FullOutput {
|
|||
pub fn append(&mut self, newer: Self) {
|
||||
let Self {
|
||||
platform_output,
|
||||
needs_repaint,
|
||||
repaint_after,
|
||||
textures_delta,
|
||||
shapes,
|
||||
} = newer;
|
||||
|
||||
self.platform_output.append(platform_output);
|
||||
self.needs_repaint = needs_repaint; // if the last frame doesn't need a repaint, then we don't need to repaint
|
||||
self.repaint_after = repaint_after; // if the last frame doesn't need a repaint, then we don't need to repaint
|
||||
self.textures_delta.append(textures_delta);
|
||||
self.shapes = shapes; // Only paint the latest
|
||||
}
|
||||
|
@ -49,7 +54,7 @@ impl FullOutput {
|
|||
/// You can access (and modify) this with [`crate::Context::output`].
|
||||
///
|
||||
/// The backend should use this.
|
||||
#[derive(Clone, Default, PartialEq)]
|
||||
#[derive(Default, Clone, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct PlatformOutput {
|
||||
/// Set the cursor to this icon.
|
||||
|
@ -61,6 +66,14 @@ pub struct PlatformOutput {
|
|||
/// If set, put this text in the system clipboard. Ignore if empty.
|
||||
///
|
||||
/// This is often a response to [`crate::Event::Copy`] or [`crate::Event::Cut`].
|
||||
///
|
||||
/// ```
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// if ui.button("📋").clicked() {
|
||||
/// ui.output_mut(|o| o.copied_text = "some_text".to_string());
|
||||
/// }
|
||||
/// # });
|
||||
/// ```
|
||||
pub copied_text: String,
|
||||
|
||||
/// Events that may be useful to e.g. a screen reader.
|
||||
|
@ -72,6 +85,9 @@ pub struct PlatformOutput {
|
|||
|
||||
/// Screen-space position of text edit cursor (used for IME).
|
||||
pub text_cursor_pos: Option<crate::Pos2>,
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub accesskit_update: Option<accesskit::TreeUpdate>,
|
||||
}
|
||||
|
||||
impl PlatformOutput {
|
||||
|
@ -108,6 +124,8 @@ impl PlatformOutput {
|
|||
mut events,
|
||||
mutable_text_under_cursor,
|
||||
text_cursor_pos,
|
||||
#[cfg(feature = "accesskit")]
|
||||
accesskit_update,
|
||||
} = newer;
|
||||
|
||||
self.cursor_icon = cursor_icon;
|
||||
|
@ -120,6 +138,13 @@ impl PlatformOutput {
|
|||
self.events.append(&mut events);
|
||||
self.mutable_text_under_cursor = mutable_text_under_cursor;
|
||||
self.text_cursor_pos = text_cursor_pos.or(self.text_cursor_pos);
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
{
|
||||
// egui produces a complete AccessKit tree for each frame,
|
||||
// so overwrite rather than appending.
|
||||
self.accesskit_update = accesskit_update;
|
||||
}
|
||||
}
|
||||
|
||||
/// Take everything ephemeral (everything except `cursor_icon` currently)
|
||||
|
@ -130,10 +155,12 @@ impl PlatformOutput {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
/// What URL to open, and how.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct OpenUrl {
|
||||
pub url: String,
|
||||
|
||||
/// If `true`, open the url in a new tab.
|
||||
/// If `false` open it in the same tab.
|
||||
/// Only matters when in a web browser.
|
||||
|
@ -163,7 +190,7 @@ impl OpenUrl {
|
|||
/// egui emits a [`CursorIcon`] in [`PlatformOutput`] each frame as a request to the integration.
|
||||
///
|
||||
/// Loosely based on <https://developer.mozilla.org/en-US/docs/Web/CSS/cursor>.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub enum CursorIcon {
|
||||
/// Normal cursor icon, whatever that is.
|
||||
|
@ -234,10 +261,13 @@ pub enum CursorIcon {
|
|||
// Resizing in two directions:
|
||||
/// Horizontal resize `-` to make something wider or more narrow (left to/from right)
|
||||
ResizeHorizontal,
|
||||
|
||||
/// Diagonal resize `/` (right-up to/from left-down)
|
||||
ResizeNeSw,
|
||||
|
||||
/// Diagonal resize `\` (left-up to/from right-down)
|
||||
ResizeNwSe,
|
||||
|
||||
/// Vertical resize `|` (up-down or down-up)
|
||||
ResizeVertical,
|
||||
|
||||
|
@ -245,24 +275,32 @@ pub enum CursorIcon {
|
|||
// Resizing in one direction:
|
||||
/// Resize something rightwards (e.g. when dragging the right-most edge of something)
|
||||
ResizeEast,
|
||||
|
||||
/// Resize something down and right (e.g. when dragging the bottom-right corner of something)
|
||||
ResizeSouthEast,
|
||||
|
||||
/// Resize something downwards (e.g. when dragging the bottom edge of something)
|
||||
ResizeSouth,
|
||||
|
||||
/// Resize something down and left (e.g. when dragging the bottom-left corner of something)
|
||||
ResizeSouthWest,
|
||||
|
||||
/// Resize something leftwards (e.g. when dragging the left edge of something)
|
||||
ResizeWest,
|
||||
|
||||
/// Resize something up and left (e.g. when dragging the top-left corner of something)
|
||||
ResizeNorthWest,
|
||||
|
||||
/// Resize something up (e.g. when dragging the top edge of something)
|
||||
ResizeNorth,
|
||||
|
||||
/// Resize something up and right (e.g. when dragging the top-right corner of something)
|
||||
ResizeNorthEast,
|
||||
|
||||
// ------------------------------------
|
||||
/// Resize a column
|
||||
ResizeColumn,
|
||||
|
||||
/// Resize a row
|
||||
ResizeRow,
|
||||
|
||||
|
@ -270,6 +308,7 @@ pub enum CursorIcon {
|
|||
// Zooming:
|
||||
/// Enhance!
|
||||
ZoomIn,
|
||||
|
||||
/// Let's get a better overview
|
||||
ZoomOut,
|
||||
}
|
||||
|
@ -326,20 +365,38 @@ impl Default for CursorIcon {
|
|||
#[derive(Clone, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub enum OutputEvent {
|
||||
// A widget was clicked.
|
||||
/// A widget was clicked.
|
||||
Clicked(WidgetInfo),
|
||||
// A widget was double-clicked.
|
||||
|
||||
/// A widget was double-clicked.
|
||||
DoubleClicked(WidgetInfo),
|
||||
// A widget was triple-clicked.
|
||||
|
||||
/// A widget was triple-clicked.
|
||||
TripleClicked(WidgetInfo),
|
||||
|
||||
/// A widget gained keyboard focus (by tab key).
|
||||
FocusGained(WidgetInfo),
|
||||
// Text selection was updated.
|
||||
|
||||
/// Text selection was updated.
|
||||
TextSelectionChanged(WidgetInfo),
|
||||
// A widget's value changed.
|
||||
|
||||
/// A widget's value changed.
|
||||
ValueChanged(WidgetInfo),
|
||||
}
|
||||
|
||||
impl OutputEvent {
|
||||
pub fn widget_info(&self) -> &WidgetInfo {
|
||||
match self {
|
||||
OutputEvent::Clicked(info)
|
||||
| OutputEvent::DoubleClicked(info)
|
||||
| OutputEvent::TripleClicked(info)
|
||||
| OutputEvent::FocusGained(info)
|
||||
| OutputEvent::TextSelectionChanged(info)
|
||||
| OutputEvent::ValueChanged(info) => info,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for OutputEvent {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
|
@ -359,19 +416,26 @@ impl std::fmt::Debug for OutputEvent {
|
|||
pub struct WidgetInfo {
|
||||
/// The type of widget this is.
|
||||
pub typ: WidgetType,
|
||||
// Whether the widget is enabled.
|
||||
|
||||
/// Whether the widget is enabled.
|
||||
pub enabled: bool,
|
||||
|
||||
/// The text on labels, buttons, checkboxes etc.
|
||||
pub label: Option<String>,
|
||||
|
||||
/// The contents of some editable text (for [`TextEdit`](crate::TextEdit) fields).
|
||||
pub current_text_value: Option<String>,
|
||||
// The previous text value.
|
||||
|
||||
/// The previous text value.
|
||||
pub prev_text_value: Option<String>,
|
||||
|
||||
/// The current value of checkboxes and radio buttons.
|
||||
pub selected: Option<bool>,
|
||||
|
||||
/// The current value of sliders etc.
|
||||
pub value: Option<f64>,
|
||||
// Selected range of characters in [`Self::current_text_value`].
|
||||
|
||||
/// Selected range of characters in [`Self::current_text_value`].
|
||||
pub text_selection: Option<std::ops::RangeInclusive<usize>>,
|
||||
}
|
||||
|
||||
|
@ -506,7 +570,7 @@ impl WidgetInfo {
|
|||
text_selection: _,
|
||||
} = self;
|
||||
|
||||
// TODO: localization
|
||||
// TODO(emilk): localization
|
||||
let widget_type = match typ {
|
||||
WidgetType::Link => "link",
|
||||
WidgetType::TextEdit => "text edit",
|
|
@ -1,6 +1,20 @@
|
|||
use std::ops::RangeInclusive;
|
||||
|
||||
use crate::*;
|
||||
use crate::{id::IdSet, *};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct TooltipFrameState {
|
||||
pub common_id: Id,
|
||||
pub rect: Rect,
|
||||
pub count: usize,
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct AccessKitFrameState {
|
||||
pub(crate) node_builders: IdMap<accesskit::NodeBuilder>,
|
||||
pub(crate) parent_stack: Vec<Id>,
|
||||
}
|
||||
|
||||
/// State that is collected during a frame and then cleared.
|
||||
/// Short-term (single frame) memory.
|
||||
|
@ -25,15 +39,24 @@ pub(crate) struct FrameState {
|
|||
/// If a tooltip has been shown this frame, where was it?
|
||||
/// This is used to prevent multiple tooltips to cover each other.
|
||||
/// Initialized to `None` at the start of each frame.
|
||||
pub(crate) tooltip_rect: Option<(Id, Rect, usize)>,
|
||||
pub(crate) tooltip_state: Option<TooltipFrameState>,
|
||||
|
||||
/// Set to [`InputState::scroll_delta`] on the start of each frame.
|
||||
///
|
||||
/// Cleared by the first [`ScrollArea`] that makes use of it.
|
||||
pub(crate) scroll_delta: Vec2, // TODO: move to a Mutex inside of `InputState` ?
|
||||
pub(crate) scroll_delta: Vec2, // TODO(emilk): move to `InputState` ?
|
||||
|
||||
/// horizontal, vertical
|
||||
pub(crate) scroll_target: [Option<(RangeInclusive<f32>, Option<Align>)>; 2],
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub(crate) accesskit_state: Option<AccessKitFrameState>,
|
||||
|
||||
/// Highlight these widgets this next frame. Read from this.
|
||||
pub(crate) highlight_this_frame: IdSet,
|
||||
|
||||
/// Highlight these widgets the next frame. Write to this.
|
||||
pub(crate) highlight_next_frame: IdSet,
|
||||
}
|
||||
|
||||
impl Default for FrameState {
|
||||
|
@ -43,9 +66,13 @@ impl Default for FrameState {
|
|||
available_rect: Rect::NAN,
|
||||
unused_rect: Rect::NAN,
|
||||
used_by_panels: Rect::NAN,
|
||||
tooltip_rect: None,
|
||||
tooltip_state: None,
|
||||
scroll_delta: Vec2::ZERO,
|
||||
scroll_target: [None, None],
|
||||
#[cfg(feature = "accesskit")]
|
||||
accesskit_state: None,
|
||||
highlight_this_frame: Default::default(),
|
||||
highlight_next_frame: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -57,18 +84,29 @@ impl FrameState {
|
|||
available_rect,
|
||||
unused_rect,
|
||||
used_by_panels,
|
||||
tooltip_rect,
|
||||
tooltip_state,
|
||||
scroll_delta,
|
||||
scroll_target,
|
||||
#[cfg(feature = "accesskit")]
|
||||
accesskit_state,
|
||||
highlight_this_frame,
|
||||
highlight_next_frame,
|
||||
} = self;
|
||||
|
||||
used_ids.clear();
|
||||
*available_rect = input.screen_rect();
|
||||
*unused_rect = input.screen_rect();
|
||||
*used_by_panels = Rect::NOTHING;
|
||||
*tooltip_rect = None;
|
||||
*tooltip_state = None;
|
||||
*scroll_delta = input.scroll_delta;
|
||||
*scroll_target = [None, None];
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
{
|
||||
*accesskit_state = None;
|
||||
}
|
||||
|
||||
*highlight_this_frame = std::mem::take(highlight_next_frame);
|
||||
}
|
||||
|
||||
/// How much space is still available after panels has been added.
|
|
@ -8,14 +8,14 @@ pub(crate) struct State {
|
|||
|
||||
impl State {
|
||||
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
|
||||
ctx.data().get_temp(id)
|
||||
ctx.data_mut(|d| d.get_temp(id))
|
||||
}
|
||||
|
||||
pub fn store(self, ctx: &Context, id: Id) {
|
||||
// We don't persist Grids, because
|
||||
// A) there are potentially a lot of them, using up a lot of space (and therefore serialization time)
|
||||
// B) if the code changes, the grid _should_ change, and not remember old sizes
|
||||
ctx.data().insert_temp(id, self);
|
||||
ctx.data_mut(|d| d.insert_temp(id, self));
|
||||
}
|
||||
|
||||
fn set_min_col_width(&mut self, col: usize, width: f32) {
|
||||
|
@ -51,6 +51,9 @@ pub(crate) struct GridLayout {
|
|||
style: std::sync::Arc<Style>,
|
||||
id: Id,
|
||||
|
||||
/// First frame (no previous know state).
|
||||
is_first_frame: bool,
|
||||
|
||||
/// State previous frame (if any).
|
||||
/// This can be used to predict future sizes of cells.
|
||||
prev_state: State,
|
||||
|
@ -71,10 +74,11 @@ pub(crate) struct GridLayout {
|
|||
}
|
||||
|
||||
impl GridLayout {
|
||||
pub(crate) fn new(ui: &Ui, id: Id) -> Self {
|
||||
let prev_state = State::load(ui.ctx(), id).unwrap_or_default();
|
||||
pub(crate) fn new(ui: &Ui, id: Id, prev_state: Option<State>) -> Self {
|
||||
let is_first_frame = prev_state.is_none();
|
||||
let prev_state = prev_state.unwrap_or_default();
|
||||
|
||||
// TODO: respect current layout
|
||||
// TODO(emilk): respect current layout
|
||||
|
||||
let initial_available = ui.placer().max_rect().intersect(ui.cursor());
|
||||
crate::egui_assert!(
|
||||
|
@ -88,6 +92,7 @@ impl GridLayout {
|
|||
ctx: ui.ctx().clone(),
|
||||
style: ui.style().clone(),
|
||||
id,
|
||||
is_first_frame,
|
||||
prev_state,
|
||||
curr_state: State::default(),
|
||||
initial_available,
|
||||
|
@ -110,6 +115,7 @@ impl GridLayout {
|
|||
.col_width(col)
|
||||
.unwrap_or(self.min_cell_size.x)
|
||||
}
|
||||
|
||||
fn prev_row_height(&self, row: usize) -> f32 {
|
||||
self.prev_state
|
||||
.row_height(row)
|
||||
|
@ -124,9 +130,18 @@ impl GridLayout {
|
|||
let is_last_column = Some(self.col + 1) == self.num_columns;
|
||||
|
||||
let width = if is_last_column {
|
||||
(self.initial_available.right() - region.cursor.left()).at_most(self.max_cell_size.x)
|
||||
// The first frame we don't really know the widths of the previous columns,
|
||||
// so returning a big available width here can cause trouble.
|
||||
if self.is_first_frame {
|
||||
self.curr_state
|
||||
.col_width(self.col)
|
||||
.unwrap_or(self.min_cell_size.x)
|
||||
} else {
|
||||
(self.initial_available.right() - region.cursor.left())
|
||||
.at_most(self.max_cell_size.x)
|
||||
}
|
||||
} else if self.max_cell_size.x.is_finite() {
|
||||
// TODO: should probably heed `prev_state` here too
|
||||
// TODO(emilk): should probably heed `prev_state` here too
|
||||
self.max_cell_size.x
|
||||
} else {
|
||||
// If we want to allow width-filling widgets like [`Separator`] in one of the first cells
|
||||
|
@ -159,7 +174,7 @@ impl GridLayout {
|
|||
|
||||
#[allow(clippy::unused_self)]
|
||||
pub(crate) fn align_size_within_rect(&self, size: Vec2, frame: Rect) -> Rect {
|
||||
// TODO: allow this alignment to be customized
|
||||
// TODO(emilk): allow this alignment to be customized
|
||||
Align2::LEFT_CENTER.align_size_within_rect(size, frame)
|
||||
}
|
||||
|
||||
|
@ -263,7 +278,7 @@ impl GridLayout {
|
|||
pub struct Grid {
|
||||
id_source: Id,
|
||||
num_columns: Option<usize>,
|
||||
striped: bool,
|
||||
striped: Option<bool>,
|
||||
min_col_width: Option<f32>,
|
||||
min_row_height: Option<f32>,
|
||||
max_cell_size: Vec2,
|
||||
|
@ -277,7 +292,7 @@ impl Grid {
|
|||
Self {
|
||||
id_source: Id::new(id_source),
|
||||
num_columns: None,
|
||||
striped: false,
|
||||
striped: None,
|
||||
min_col_width: None,
|
||||
min_row_height: None,
|
||||
max_cell_size: Vec2::INFINITY,
|
||||
|
@ -295,9 +310,9 @@ impl Grid {
|
|||
/// If `true`, add a subtle background color to every other row.
|
||||
///
|
||||
/// This can make a table easier to read.
|
||||
/// Default: `false`.
|
||||
/// Default is whatever is in [`crate::Visuals::striped`].
|
||||
pub fn striped(mut self, striped: bool) -> Self {
|
||||
self.striped = striped;
|
||||
self.striped = Some(striped);
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -356,18 +371,22 @@ impl Grid {
|
|||
spacing,
|
||||
start_row,
|
||||
} = self;
|
||||
let striped = striped.unwrap_or(ui.visuals().striped);
|
||||
let min_col_width = min_col_width.unwrap_or_else(|| ui.spacing().interact_size.x);
|
||||
let min_row_height = min_row_height.unwrap_or_else(|| ui.spacing().interact_size.y);
|
||||
let spacing = spacing.unwrap_or_else(|| ui.spacing().item_spacing);
|
||||
|
||||
let id = ui.make_persistent_id(id_source);
|
||||
let prev_state = State::load(ui.ctx(), id);
|
||||
|
||||
// Each grid cell is aligned LEFT_CENTER.
|
||||
// If somebody wants to wrap more things inside a cell,
|
||||
// then we should pick a default layout that matches that alignment,
|
||||
// which we do here:
|
||||
let max_rect = ui.cursor().intersect(ui.max_rect());
|
||||
ui.allocate_ui_at_rect(max_rect, |ui| {
|
||||
ui.set_visible(prev_state.is_some()); // Avoid visible first-frame jitter
|
||||
ui.horizontal(|ui| {
|
||||
let id = ui.make_persistent_id(id_source);
|
||||
let grid = GridLayout {
|
||||
num_columns,
|
||||
striped,
|
||||
|
@ -375,7 +394,7 @@ impl Grid {
|
|||
max_cell_size,
|
||||
spacing,
|
||||
row: start_row,
|
||||
..GridLayout::new(ui, id)
|
||||
..GridLayout::new(ui, id, prev_state)
|
||||
};
|
||||
|
||||
ui.set_grid(grid);
|
117
crates/egui/src/gui_zoom.rs
Normal file
117
crates/egui/src/gui_zoom.rs
Normal file
|
@ -0,0 +1,117 @@
|
|||
//! Helpers for zooming the whole GUI of an app (changing [`Context::pixels_per_point`].
|
||||
//!
|
||||
use crate::*;
|
||||
|
||||
/// The suggested keyboard shortcuts for global gui zooming.
|
||||
pub mod kb_shortcuts {
|
||||
use super::*;
|
||||
|
||||
pub const ZOOM_IN: KeyboardShortcut =
|
||||
KeyboardShortcut::new(Modifiers::COMMAND, Key::PlusEquals);
|
||||
pub const ZOOM_OUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::COMMAND, Key::Minus);
|
||||
pub const ZOOM_RESET: KeyboardShortcut = KeyboardShortcut::new(Modifiers::COMMAND, Key::Num0);
|
||||
}
|
||||
|
||||
/// Let the user scale the GUI (change `Context::pixels_per_point`) by pressing
|
||||
/// Cmd+Plus, Cmd+Minus or Cmd+0, just like in a browser.
|
||||
///
|
||||
/// When using [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe), you want to call this as:
|
||||
/// ```ignore
|
||||
/// // On web, the browser controls the gui zoom.
|
||||
/// if !frame.is_web() {
|
||||
/// egui::gui_zoom::zoom_with_keyboard_shortcuts(
|
||||
/// ctx,
|
||||
/// frame.info().native_pixels_per_point,
|
||||
/// );
|
||||
/// }
|
||||
/// ```
|
||||
pub fn zoom_with_keyboard_shortcuts(ctx: &Context, native_pixels_per_point: Option<f32>) {
|
||||
if ctx.input_mut(|i| i.consume_shortcut(&kb_shortcuts::ZOOM_RESET)) {
|
||||
if let Some(native_pixels_per_point) = native_pixels_per_point {
|
||||
ctx.set_pixels_per_point(native_pixels_per_point);
|
||||
}
|
||||
} else {
|
||||
if ctx.input_mut(|i| i.consume_shortcut(&kb_shortcuts::ZOOM_IN)) {
|
||||
zoom_in(ctx);
|
||||
}
|
||||
if ctx.input_mut(|i| i.consume_shortcut(&kb_shortcuts::ZOOM_OUT)) {
|
||||
zoom_out(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const MIN_PIXELS_PER_POINT: f32 = 0.2;
|
||||
const MAX_PIXELS_PER_POINT: f32 = 4.0;
|
||||
|
||||
/// Make everything larger.
|
||||
pub fn zoom_in(ctx: &Context) {
|
||||
let mut pixels_per_point = ctx.pixels_per_point();
|
||||
pixels_per_point += 0.1;
|
||||
pixels_per_point = pixels_per_point.clamp(MIN_PIXELS_PER_POINT, MAX_PIXELS_PER_POINT);
|
||||
pixels_per_point = (pixels_per_point * 10.).round() / 10.;
|
||||
ctx.set_pixels_per_point(pixels_per_point);
|
||||
}
|
||||
|
||||
/// Make everything smaller.
|
||||
pub fn zoom_out(ctx: &Context) {
|
||||
let mut pixels_per_point = ctx.pixels_per_point();
|
||||
pixels_per_point -= 0.1;
|
||||
pixels_per_point = pixels_per_point.clamp(MIN_PIXELS_PER_POINT, MAX_PIXELS_PER_POINT);
|
||||
pixels_per_point = (pixels_per_point * 10.).round() / 10.;
|
||||
ctx.set_pixels_per_point(pixels_per_point);
|
||||
}
|
||||
|
||||
/// Show buttons for zooming the ui.
|
||||
///
|
||||
/// This is meant to be called from within a menu (See [`Ui::menu_button`]).
|
||||
///
|
||||
/// When using [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe), you want to call this as:
|
||||
/// ```ignore
|
||||
/// // On web, the browser controls the gui zoom.
|
||||
/// if !frame.is_web() {
|
||||
/// ui.menu_button("View", |ui| {
|
||||
/// egui::gui_zoom::zoom_menu_buttons(
|
||||
/// ui,
|
||||
/// frame.info().native_pixels_per_point,
|
||||
/// );
|
||||
/// });
|
||||
/// }
|
||||
/// ```
|
||||
pub fn zoom_menu_buttons(ui: &mut Ui, native_pixels_per_point: Option<f32>) {
|
||||
if ui
|
||||
.add_enabled(
|
||||
ui.ctx().pixels_per_point() < MAX_PIXELS_PER_POINT,
|
||||
Button::new("Zoom In").shortcut_text(ui.ctx().format_shortcut(&kb_shortcuts::ZOOM_IN)),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
zoom_in(ui.ctx());
|
||||
ui.close_menu();
|
||||
}
|
||||
|
||||
if ui
|
||||
.add_enabled(
|
||||
ui.ctx().pixels_per_point() > MIN_PIXELS_PER_POINT,
|
||||
Button::new("Zoom Out")
|
||||
.shortcut_text(ui.ctx().format_shortcut(&kb_shortcuts::ZOOM_OUT)),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
zoom_out(ui.ctx());
|
||||
ui.close_menu();
|
||||
}
|
||||
|
||||
if let Some(native_pixels_per_point) = native_pixels_per_point {
|
||||
if ui
|
||||
.add_enabled(
|
||||
ui.ctx().pixels_per_point() != native_pixels_per_point,
|
||||
Button::new("Reset Zoom")
|
||||
.shortcut_text(ui.ctx().format_shortcut(&kb_shortcuts::ZOOM_RESET)),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
ui.ctx().set_pixels_per_point(native_pixels_per_point);
|
||||
ui.close_menu();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
// TODO: have separate types `PositionId` and `UniqueId`. ?
|
||||
// TODO(emilk): have separate types `PositionId` and `UniqueId`. ?
|
||||
|
||||
/// egui tracks widgets frame-to-frame using [`Id`]s.
|
||||
///
|
||||
|
@ -45,16 +45,16 @@ impl Id {
|
|||
|
||||
/// Generate a new [`Id`] by hashing some source (e.g. a string or integer).
|
||||
pub fn new(source: impl std::hash::Hash) -> Id {
|
||||
use std::hash::Hasher;
|
||||
let mut hasher = epaint::ahash::AHasher::new_with_keys(123, 456);
|
||||
use std::hash::{BuildHasher, Hasher};
|
||||
let mut hasher = epaint::ahash::RandomState::with_seeds(1, 2, 3, 4).build_hasher();
|
||||
source.hash(&mut hasher);
|
||||
Id(hasher.finish())
|
||||
}
|
||||
|
||||
/// Generate a new [`Id`] by hashing the parent [`Id`] and the given argument.
|
||||
pub fn with(self, child: impl std::hash::Hash) -> Id {
|
||||
use std::hash::Hasher;
|
||||
let mut hasher = epaint::ahash::AHasher::new_with_keys(123, 456);
|
||||
use std::hash::{BuildHasher, Hasher};
|
||||
let mut hasher = epaint::ahash::RandomState::with_seeds(1, 2, 3, 4).build_hasher();
|
||||
hasher.write_u64(self.0);
|
||||
child.hash(&mut hasher);
|
||||
Id(hasher.finish())
|
||||
|
@ -69,6 +69,11 @@ impl Id {
|
|||
pub(crate) fn value(&self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub(crate) fn accesskit_id(&self) -> accesskit::NodeId {
|
||||
std::num::NonZeroU64::new(self.0).unwrap().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Id {
|
||||
|
@ -77,6 +82,21 @@ impl std::fmt::Debug for Id {
|
|||
}
|
||||
}
|
||||
|
||||
/// Convenience
|
||||
impl From<&'static str> for Id {
|
||||
#[inline]
|
||||
fn from(string: &'static str) -> Self {
|
||||
Self::new(string)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Id {
|
||||
#[inline]
|
||||
fn from(string: String) -> Self {
|
||||
Self::new(string)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// Idea taken from the `nohash_hasher` crate.
|
||||
|
@ -91,9 +111,11 @@ impl std::hash::Hasher for IdHasher {
|
|||
fn write_u8(&mut self, _n: u8) {
|
||||
unreachable!("Invalid use of IdHasher");
|
||||
}
|
||||
|
||||
fn write_u16(&mut self, _n: u16) {
|
||||
unreachable!("Invalid use of IdHasher");
|
||||
}
|
||||
|
||||
fn write_u32(&mut self, _n: u32) {
|
||||
unreachable!("Invalid use of IdHasher");
|
||||
}
|
||||
|
@ -110,15 +132,19 @@ impl std::hash::Hasher for IdHasher {
|
|||
fn write_i8(&mut self, _n: i8) {
|
||||
unreachable!("Invalid use of IdHasher");
|
||||
}
|
||||
|
||||
fn write_i16(&mut self, _n: i16) {
|
||||
unreachable!("Invalid use of IdHasher");
|
||||
}
|
||||
|
||||
fn write_i32(&mut self, _n: i32) {
|
||||
unreachable!("Invalid use of IdHasher");
|
||||
}
|
||||
|
||||
fn write_i64(&mut self, _n: i64) {
|
||||
unreachable!("Invalid use of IdHasher");
|
||||
}
|
||||
|
||||
fn write_isize(&mut self, _n: isize) {
|
||||
unreachable!("Invalid use of IdHasher");
|
||||
}
|
||||
|
@ -142,5 +168,8 @@ impl std::hash::BuildHasher for BuilIdHasher {
|
|||
}
|
||||
}
|
||||
|
||||
/// `IdSet` is a `HashSet<Id>` optimized by knowing that [`Id`] has good entropy, and doesn't need more hashing.
|
||||
pub type IdSet = std::collections::HashSet<Id, BuilIdHasher>;
|
||||
|
||||
/// `IdMap<V>` is a `HashMap<Id, V>` optimized by knowing that [`Id`] has good entropy, and doesn't need more hashing.
|
||||
pub type IdMap<V> = std::collections::HashMap<Id, V, BuilIdHasher>;
|
|
@ -9,13 +9,13 @@ pub use touch_state::MultiTouchInfo;
|
|||
use touch_state::TouchState;
|
||||
|
||||
/// If the pointer moves more than this, it won't become a click (but it is still a drag)
|
||||
const MAX_CLICK_DIST: f32 = 6.0; // TODO: move to settings
|
||||
const MAX_CLICK_DIST: f32 = 6.0; // TODO(emilk): move to settings
|
||||
|
||||
/// If the pointer is down for longer than this, it won't become a click (but it is still a drag)
|
||||
const MAX_CLICK_DURATION: f64 = 0.6; // TODO: move to settings
|
||||
const MAX_CLICK_DURATION: f64 = 0.6; // TODO(emilk): move to settings
|
||||
|
||||
/// The new pointer press must come within this many seconds from previous pointer release
|
||||
const MAX_DOUBLE_CLICK_DELAY: f64 = 0.3; // TODO: move to settings
|
||||
const MAX_DOUBLE_CLICK_DELAY: f64 = 0.3; // TODO(emilk): move to settings
|
||||
|
||||
/// Input state that egui updates each frame.
|
||||
///
|
||||
|
@ -67,14 +67,43 @@ pub struct InputState {
|
|||
|
||||
/// Time since last frame, in seconds.
|
||||
///
|
||||
/// This can be very unstable in reactive mode (when we don't paint each frame)
|
||||
/// so it can be smart to use e.g. `unstable_dt.min(1.0 / 30.0)`.
|
||||
/// This can be very unstable in reactive mode (when we don't paint each frame).
|
||||
/// For animations it is therefore better to use [`Self::stable_dt`].
|
||||
pub unstable_dt: f32,
|
||||
|
||||
/// Estimated time until next frame (provided we repaint right away).
|
||||
///
|
||||
/// Used for animations to get instant feedback (avoid frame delay).
|
||||
/// Should be set to the expected time between frames when painting at vsync speeds.
|
||||
///
|
||||
/// On most integrations this has a fixed value of `1.0 / 60.0`, so it is not a very accurate estimate.
|
||||
pub predicted_dt: f32,
|
||||
|
||||
/// Time since last frame (in seconds), but gracefully handles the first frame after sleeping in reactive mode.
|
||||
///
|
||||
/// In reactive mode (available in e.g. `eframe`), `egui` only updates when there is new input
|
||||
/// or something is animating.
|
||||
/// This can lead to large gaps of time (sleep), leading to large [`Self::unstable_dt`].
|
||||
///
|
||||
/// If `egui` requested a repaint the previous frame, then `egui` will use
|
||||
/// `stable_dt = unstable_dt;`, but if `egui` did not not request a repaint last frame,
|
||||
/// then `egui` will assume `unstable_dt` is too large, and will use
|
||||
/// `stable_dt = predicted_dt;`.
|
||||
///
|
||||
/// This means that for the first frame after a sleep,
|
||||
/// `stable_dt` will be a prediction of the delta-time until the next frame,
|
||||
/// and in all other situations this will be an accurate measurement of time passed
|
||||
/// since the previous frame.
|
||||
///
|
||||
/// Note that a frame can still stall for various reasons, so `stable_dt` can
|
||||
/// still be unusually large in some situations.
|
||||
///
|
||||
/// When animating something, it is recommended that you use something like
|
||||
/// `stable_dt.min(0.1)` - this will give you smooth animations when the framerate is good
|
||||
/// (even in reactive mode), but will avoid large jumps when framerate is bad,
|
||||
/// and will effectively slow down the animation when FPS drops below 10.
|
||||
pub stable_dt: f32,
|
||||
|
||||
/// Which modifier keys are down at the start of the frame?
|
||||
pub modifiers: Modifiers,
|
||||
|
||||
|
@ -97,8 +126,9 @@ impl Default for InputState {
|
|||
pixels_per_point: 1.0,
|
||||
max_texture_side: 2048,
|
||||
time: 0.0,
|
||||
unstable_dt: 1.0 / 6.0,
|
||||
predicted_dt: 1.0 / 6.0,
|
||||
unstable_dt: 1.0 / 60.0,
|
||||
predicted_dt: 1.0 / 60.0,
|
||||
stable_dt: 1.0 / 60.0,
|
||||
modifiers: Default::default(),
|
||||
keys_down: Default::default(),
|
||||
events: Default::default(),
|
||||
|
@ -108,9 +138,22 @@ impl Default for InputState {
|
|||
|
||||
impl InputState {
|
||||
#[must_use]
|
||||
pub fn begin_frame(mut self, new: RawInput) -> InputState {
|
||||
pub fn begin_frame(
|
||||
mut self,
|
||||
mut new: RawInput,
|
||||
requested_repaint_last_frame: bool,
|
||||
) -> InputState {
|
||||
let time = new.time.unwrap_or(self.time + new.predicted_dt as f64);
|
||||
let unstable_dt = (time - self.time) as f32;
|
||||
|
||||
let stable_dt = if requested_repaint_last_frame {
|
||||
// we should have had a repaint straight away,
|
||||
// so this should be trustable.
|
||||
unstable_dt
|
||||
} else {
|
||||
new.predicted_dt
|
||||
};
|
||||
|
||||
let screen_rect = new.screen_rect.unwrap_or(self.screen_rect);
|
||||
self.create_touch_states_for_new_devices(&new.events);
|
||||
for touch_state in self.touch_states.values_mut() {
|
||||
|
@ -121,11 +164,17 @@ impl InputState {
|
|||
let mut keys_down = self.keys_down;
|
||||
let mut scroll_delta = Vec2::ZERO;
|
||||
let mut zoom_factor_delta = 1.0;
|
||||
for event in &new.events {
|
||||
for event in &mut new.events {
|
||||
match event {
|
||||
Event::Key { key, pressed, .. } => {
|
||||
Event::Key {
|
||||
key,
|
||||
pressed,
|
||||
repeat,
|
||||
..
|
||||
} => {
|
||||
if *pressed {
|
||||
keys_down.insert(*key);
|
||||
let first_press = keys_down.insert(*key);
|
||||
*repeat = !first_press;
|
||||
} else {
|
||||
keys_down.remove(key);
|
||||
}
|
||||
|
@ -139,6 +188,7 @@ impl InputState {
|
|||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
InputState {
|
||||
pointer,
|
||||
touch_states: self.touch_states,
|
||||
|
@ -150,9 +200,10 @@ impl InputState {
|
|||
time,
|
||||
unstable_dt,
|
||||
predicted_dt: new.predicted_dt,
|
||||
stable_dt,
|
||||
modifiers: new.modifiers,
|
||||
keys_down,
|
||||
events: new.events.clone(), // TODO: remove clone() and use raw.events
|
||||
events: new.events.clone(), // TODO(emilk): remove clone() and use raw.events
|
||||
raw: new,
|
||||
}
|
||||
}
|
||||
|
@ -205,9 +256,11 @@ impl InputState {
|
|||
self.pointer.wants_repaint() || self.scroll_delta != Vec2::ZERO || !self.events.is_empty()
|
||||
}
|
||||
|
||||
/// Check for a key press. If found, `true` is returned and the key pressed is consumed, so that this will only return `true` once.
|
||||
pub fn consume_key(&mut self, modifiers: Modifiers, key: Key) -> bool {
|
||||
let mut match_found = false;
|
||||
/// Count presses of a key. If non-zero, the presses are consumed, so that this will only return non-zero once.
|
||||
///
|
||||
/// Includes key-repeat events.
|
||||
pub fn count_and_consume_key(&mut self, modifiers: Modifiers, key: Key) -> usize {
|
||||
let mut count = 0usize;
|
||||
|
||||
self.events.retain(|event| {
|
||||
let is_match = matches!(
|
||||
|
@ -215,35 +268,54 @@ impl InputState {
|
|||
Event::Key {
|
||||
key: ev_key,
|
||||
modifiers: ev_mods,
|
||||
pressed: true
|
||||
pressed: true,
|
||||
..
|
||||
} if *ev_key == key && ev_mods.matches(modifiers)
|
||||
);
|
||||
|
||||
match_found |= is_match;
|
||||
count += is_match as usize;
|
||||
|
||||
!is_match
|
||||
});
|
||||
|
||||
match_found
|
||||
count
|
||||
}
|
||||
|
||||
/// Check for a key press. If found, `true` is returned and the key pressed is consumed, so that this will only return `true` once.
|
||||
///
|
||||
/// Includes key-repeat events.
|
||||
pub fn consume_key(&mut self, modifiers: Modifiers, key: Key) -> bool {
|
||||
self.count_and_consume_key(modifiers, key) > 0
|
||||
}
|
||||
|
||||
/// Check if the given shortcut has been pressed.
|
||||
///
|
||||
/// If so, `true` is returned and the key pressed is consumed, so that this will only return `true` once.
|
||||
///
|
||||
/// Includes key-repeat events.
|
||||
pub fn consume_shortcut(&mut self, shortcut: &KeyboardShortcut) -> bool {
|
||||
let KeyboardShortcut { modifiers, key } = *shortcut;
|
||||
self.consume_key(modifiers, key)
|
||||
}
|
||||
|
||||
/// Was the given key pressed this frame?
|
||||
///
|
||||
/// Includes key-repeat events.
|
||||
pub fn key_pressed(&self, desired_key: Key) -> bool {
|
||||
self.num_presses(desired_key) > 0
|
||||
}
|
||||
|
||||
/// How many times were the given key pressed this frame?
|
||||
/// How many times was the given key pressed this frame?
|
||||
///
|
||||
/// Includes key-repeat events.
|
||||
pub fn num_presses(&self, desired_key: Key) -> usize {
|
||||
self.events
|
||||
.iter()
|
||||
.filter(|event| {
|
||||
matches!(
|
||||
event,
|
||||
Event::Key {
|
||||
key,
|
||||
pressed: true,
|
||||
..
|
||||
} if *key == desired_key
|
||||
Event::Key { key, pressed: true, .. }
|
||||
if *key == desired_key
|
||||
)
|
||||
})
|
||||
.count()
|
||||
|
@ -284,7 +356,7 @@ impl InputState {
|
|||
/// Returns imprecision in points.
|
||||
#[inline(always)]
|
||||
pub fn aim_radius(&self) -> f32 {
|
||||
// TODO: multiply by ~3 for touch inputs because fingers are fat
|
||||
// TODO(emilk): multiply by ~3 for touch inputs because fingers are fat
|
||||
self.physical_pixel_size()
|
||||
}
|
||||
|
||||
|
@ -296,7 +368,7 @@ impl InputState {
|
|||
/// # egui::__run_test_ui(|ui| {
|
||||
/// let mut zoom = 1.0; // no zoom
|
||||
/// let mut rotation = 0.0; // no rotation
|
||||
/// let multi_touch = ui.input().multi_touch();
|
||||
/// let multi_touch = ui.input(|i| i.multi_touch());
|
||||
/// if let Some(multi_touch) = multi_touch {
|
||||
/// zoom *= multi_touch.zoom_delta;
|
||||
/// rotation += multi_touch.rotation_delta;
|
||||
|
@ -340,6 +412,33 @@ impl InputState {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub fn accesskit_action_requests(
|
||||
&self,
|
||||
id: crate::Id,
|
||||
action: accesskit::Action,
|
||||
) -> impl Iterator<Item = &accesskit::ActionRequest> {
|
||||
let accesskit_id = id.accesskit_id();
|
||||
self.events.iter().filter_map(move |event| {
|
||||
if let Event::AccessKitActionRequest(request) = event {
|
||||
if request.target == accesskit_id && request.action == action {
|
||||
return Some(request);
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub fn has_accesskit_action_request(&self, id: crate::Id, action: accesskit::Action) -> bool {
|
||||
self.accesskit_action_requests(id, action).next().is_some()
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub fn num_accesskit_action_requests(&self, id: crate::Id, action: accesskit::Action) -> usize {
|
||||
self.accesskit_action_requests(id, action).count()
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
@ -348,9 +447,10 @@ impl InputState {
|
|||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) struct Click {
|
||||
pub pos: Pos2,
|
||||
pub button: PointerButton,
|
||||
|
||||
/// 1 or 2 (double-click) or 3 (triple-click)
|
||||
pub count: u32,
|
||||
|
||||
/// Allows you to check for e.g. shift-click
|
||||
pub modifiers: Modifiers,
|
||||
}
|
||||
|
@ -359,6 +459,7 @@ impl Click {
|
|||
pub fn is_double(&self) -> bool {
|
||||
self.count == 2
|
||||
}
|
||||
|
||||
pub fn is_triple(&self) -> bool {
|
||||
self.count == 3
|
||||
}
|
||||
|
@ -367,19 +468,27 @@ impl Click {
|
|||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) enum PointerEvent {
|
||||
Moved(Pos2),
|
||||
Pressed(Pos2),
|
||||
Released(Option<Click>),
|
||||
Pressed {
|
||||
position: Pos2,
|
||||
button: PointerButton,
|
||||
},
|
||||
Released {
|
||||
click: Option<Click>,
|
||||
button: PointerButton,
|
||||
},
|
||||
}
|
||||
|
||||
impl PointerEvent {
|
||||
pub fn is_press(&self) -> bool {
|
||||
matches!(self, PointerEvent::Pressed(_))
|
||||
matches!(self, PointerEvent::Pressed { .. })
|
||||
}
|
||||
|
||||
pub fn is_release(&self) -> bool {
|
||||
matches!(self, PointerEvent::Released(_))
|
||||
matches!(self, PointerEvent::Released { .. })
|
||||
}
|
||||
|
||||
pub fn is_click(&self) -> bool {
|
||||
matches!(self, PointerEvent::Released(Some(_click)))
|
||||
matches!(self, PointerEvent::Released { click: Some(_), .. })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -509,7 +618,10 @@ impl PointerState {
|
|||
self.press_origin = Some(pos);
|
||||
self.press_start_time = Some(time);
|
||||
self.has_moved_too_much_for_a_click = false;
|
||||
self.pointer_events.push(PointerEvent::Pressed(pos));
|
||||
self.pointer_events.push(PointerEvent::Pressed {
|
||||
position: pos,
|
||||
button,
|
||||
});
|
||||
} else {
|
||||
let clicked = self.could_any_button_be_click();
|
||||
|
||||
|
@ -531,7 +643,6 @@ impl PointerState {
|
|||
|
||||
Some(Click {
|
||||
pos,
|
||||
button,
|
||||
count,
|
||||
modifiers,
|
||||
})
|
||||
|
@ -539,7 +650,8 @@ impl PointerState {
|
|||
None
|
||||
};
|
||||
|
||||
self.pointer_events.push(PointerEvent::Released(click));
|
||||
self.pointer_events
|
||||
.push(PointerEvent::Released { click, button });
|
||||
|
||||
self.press_origin = None;
|
||||
self.press_start_time = None;
|
||||
|
@ -667,6 +779,40 @@ impl PointerState {
|
|||
self.pointer_events.iter().any(|event| event.is_release())
|
||||
}
|
||||
|
||||
/// Was the button given pressed this frame?
|
||||
pub fn button_pressed(&self, button: PointerButton) -> bool {
|
||||
self.pointer_events
|
||||
.iter()
|
||||
.any(|event| matches!(event, &PointerEvent::Pressed{button: b, ..} if button == b))
|
||||
}
|
||||
|
||||
/// Was the button given released this frame?
|
||||
pub fn button_released(&self, button: PointerButton) -> bool {
|
||||
self.pointer_events
|
||||
.iter()
|
||||
.any(|event| matches!(event, &PointerEvent::Released{button: b, ..} if button == b))
|
||||
}
|
||||
|
||||
/// Was the primary button pressed this frame?
|
||||
pub fn primary_pressed(&self) -> bool {
|
||||
self.button_pressed(PointerButton::Primary)
|
||||
}
|
||||
|
||||
/// Was the secondary button pressed this frame?
|
||||
pub fn secondary_pressed(&self) -> bool {
|
||||
self.button_pressed(PointerButton::Secondary)
|
||||
}
|
||||
|
||||
/// Was the primary button released this frame?
|
||||
pub fn primary_released(&self) -> bool {
|
||||
self.button_released(PointerButton::Primary)
|
||||
}
|
||||
|
||||
/// Was the secondary button released this frame?
|
||||
pub fn secondary_released(&self) -> bool {
|
||||
self.button_released(PointerButton::Secondary)
|
||||
}
|
||||
|
||||
/// Is any pointer button currently down?
|
||||
pub fn any_down(&self) -> bool {
|
||||
self.down.iter().any(|&down| down)
|
||||
|
@ -677,17 +823,48 @@ impl PointerState {
|
|||
self.pointer_events.iter().any(|event| event.is_click())
|
||||
}
|
||||
|
||||
// /// Was this button pressed (`!down -> down`) this frame?
|
||||
// /// This can sometimes return `true` even if `any_down() == false`
|
||||
// /// because a press can be shorted than one frame.
|
||||
// pub fn button_pressed(&self, button: PointerButton) -> bool {
|
||||
// self.pointer_events.iter().any(|event| event.is_press())
|
||||
// }
|
||||
/// Was the button given clicked this frame?
|
||||
pub fn button_clicked(&self, button: PointerButton) -> bool {
|
||||
self.pointer_events
|
||||
.iter()
|
||||
.any(|event| matches!(event, &PointerEvent::Pressed { button: b, .. } if button == b))
|
||||
}
|
||||
|
||||
// /// Was this button released (`down -> !down`) this frame?
|
||||
// pub fn button_released(&self, button: PointerButton) -> bool {
|
||||
// self.pointer_events.iter().any(|event| event.is_release())
|
||||
// }
|
||||
/// Was the button given double clicked this frame?
|
||||
pub fn button_double_clicked(&self, button: PointerButton) -> bool {
|
||||
self.pointer_events.iter().any(|event| {
|
||||
matches!(
|
||||
&event,
|
||||
PointerEvent::Released {
|
||||
click: Some(click),
|
||||
button: b,
|
||||
} if *b == button && click.is_double()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Was the button given triple clicked this frame?
|
||||
pub fn button_triple_clicked(&self, button: PointerButton) -> bool {
|
||||
self.pointer_events.iter().any(|event| {
|
||||
matches!(
|
||||
&event,
|
||||
PointerEvent::Released {
|
||||
click: Some(click),
|
||||
button: b,
|
||||
} if *b == button && click.is_triple()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Was the primary button clicked this frame?
|
||||
pub fn primary_clicked(&self) -> bool {
|
||||
self.button_clicked(PointerButton::Primary)
|
||||
}
|
||||
|
||||
/// Was the secondary button clicked this frame?
|
||||
pub fn secondary_clicked(&self) -> bool {
|
||||
self.button_clicked(PointerButton::Secondary)
|
||||
}
|
||||
|
||||
/// Is this button currently down?
|
||||
#[inline(always)]
|
||||
|
@ -748,6 +925,7 @@ impl InputState {
|
|||
time,
|
||||
unstable_dt,
|
||||
predicted_dt,
|
||||
stable_dt,
|
||||
modifiers,
|
||||
keys_down,
|
||||
events,
|
||||
|
@ -790,6 +968,7 @@ impl InputState {
|
|||
1e3 * unstable_dt
|
||||
));
|
||||
ui.label(format!("predicted_dt: {:.1} ms", 1e3 * predicted_dt));
|
||||
ui.label(format!("stable_dt: {:.1} ms", 1e3 * stable_dt));
|
||||
ui.label(format!("modifiers: {:#?}", modifiers));
|
||||
ui.label(format!("keys_down: {:?}", keys_down));
|
||||
ui.scope(|ui| {
|
|
@ -68,6 +68,7 @@ pub(crate) struct TouchState {
|
|||
/// Technical identifier of the touch device. This is used to identify relevant touch events
|
||||
/// for this [`TouchState`] instance.
|
||||
device_id: TouchDeviceId,
|
||||
|
||||
/// Active touches, if any.
|
||||
///
|
||||
/// TouchId is the unique identifier of the touch. It is valid as long as the finger/pen touches the surface. The
|
||||
|
@ -75,6 +76,7 @@ pub(crate) struct TouchState {
|
|||
///
|
||||
/// Refer to [`ActiveTouch`].
|
||||
active_touches: BTreeMap<TouchId, ActiveTouch>,
|
||||
|
||||
/// If a gesture has been recognized (i.e. when exactly two fingers touch the surface), this
|
||||
/// holds state information
|
||||
gesture_state: Option<GestureState>,
|
||||
|
@ -94,10 +96,14 @@ struct GestureState {
|
|||
struct DynGestureState {
|
||||
/// used for proportional zooming
|
||||
avg_distance: f32,
|
||||
|
||||
/// used for non-proportional zooming
|
||||
avg_abs_distance2: Vec2,
|
||||
|
||||
avg_pos: Pos2,
|
||||
|
||||
avg_force: f32,
|
||||
|
||||
heading: f32,
|
||||
}
|
||||
|
||||
|
@ -107,6 +113,7 @@ struct DynGestureState {
|
|||
struct ActiveTouch {
|
||||
/// Current position of this touch, in device coordinates (not necessarily screen position)
|
||||
pos: Pos2,
|
||||
|
||||
/// Current force of the touch. A value in the interval [0.0 .. 1.0]
|
||||
///
|
||||
/// Note that a value of 0.0 either indicates a very light touch, or it means that the device
|
||||
|
@ -286,7 +293,7 @@ impl TouchState {
|
|||
impl Debug for TouchState {
|
||||
// This outputs less clutter than `#[derive(Debug)]`:
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
for (id, touch) in self.active_touches.iter() {
|
||||
for (id, touch) in &self.active_touches {
|
||||
f.write_fmt(format_args!("#{:?}: {:#?}\n", id, touch))?;
|
||||
}
|
||||
f.write_fmt(format_args!("gesture: {:#?}\n", self.gesture_state))?;
|
|
@ -2,7 +2,7 @@
|
|||
use crate::*;
|
||||
|
||||
pub fn font_family_ui(ui: &mut Ui, font_family: &mut FontFamily) {
|
||||
let families = ui.fonts().families();
|
||||
let families = ui.fonts(|f| f.families());
|
||||
ui.horizontal(|ui| {
|
||||
for alternative in families {
|
||||
let text = alternative.to_string();
|
||||
|
@ -12,9 +12,9 @@ pub fn font_family_ui(ui: &mut Ui, font_family: &mut FontFamily) {
|
|||
}
|
||||
|
||||
pub fn font_id_ui(ui: &mut Ui, font_id: &mut FontId) {
|
||||
let families = ui.fonts().families();
|
||||
let families = ui.fonts(|f| f.families());
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(Slider::new(&mut font_id.size, 4.0..=40.0).max_decimals(0));
|
||||
ui.add(Slider::new(&mut font_id.size, 4.0..=40.0).max_decimals(1));
|
||||
for alternative in families {
|
||||
let text = alternative.to_string();
|
||||
ui.radio_value(&mut font_id.family, alternative, text);
|
||||
|
@ -142,6 +142,7 @@ impl Widget for &mut epaint::TessellationOptions {
|
|||
feathering,
|
||||
feathering_size_in_pixels,
|
||||
coarse_tessellation_culling,
|
||||
prerasterized_discs,
|
||||
round_text_to_pixels,
|
||||
debug_paint_clip_rects,
|
||||
debug_paint_text_rects,
|
||||
|
@ -158,6 +159,8 @@ impl Widget for &mut epaint::TessellationOptions {
|
|||
.text("Feathering size in pixels");
|
||||
ui.add_enabled(*feathering, feathering_slider);
|
||||
|
||||
ui.checkbox(prerasterized_discs, "Speed up filled circles with pre-rasterization");
|
||||
|
||||
ui.add(
|
||||
crate::widgets::Slider::new(bezier_tolerance, 0.0001..=10.0)
|
||||
.logarithmic(true)
|
|
@ -10,16 +10,21 @@ use epaint::{ClippedShape, Shape};
|
|||
pub enum Order {
|
||||
/// Painted behind all floating windows
|
||||
Background,
|
||||
|
||||
/// Special layer between panels and windows
|
||||
PanelResizeLine,
|
||||
|
||||
/// Normal moveable windows that you reorder by click
|
||||
Middle,
|
||||
|
||||
/// Popups, menus etc that should always be painted on top of windows
|
||||
/// Foreground objects can also have tooltips
|
||||
Foreground,
|
||||
|
||||
/// Things floating on top of everything else, like tooltips.
|
||||
/// You cannot interact with these.
|
||||
Tooltip,
|
||||
|
||||
/// Debug layer, always painted last / on top
|
||||
Debug,
|
||||
}
|
||||
|
@ -34,6 +39,7 @@ impl Order {
|
|||
Self::Tooltip,
|
||||
Self::Debug,
|
||||
];
|
||||
pub const TOP: Self = Self::Debug;
|
||||
|
||||
#[inline(always)]
|
||||
pub fn allow_interaction(&self) -> bool {
|
||||
|
@ -104,7 +110,7 @@ impl LayerId {
|
|||
}
|
||||
|
||||
/// A unique identifier of a specific [`Shape`] in a [`PaintList`].
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ShapeIdx(usize);
|
||||
|
||||
/// A list of [`Shape`]s paired with a clip rectangle.
|
||||
|
@ -125,9 +131,12 @@ impl PaintList {
|
|||
idx
|
||||
}
|
||||
|
||||
pub fn extend(&mut self, clip_rect: Rect, mut shapes: Vec<Shape>) {
|
||||
self.0
|
||||
.extend(shapes.drain(..).map(|shape| ClippedShape(clip_rect, shape)));
|
||||
pub fn extend<I: IntoIterator<Item = Shape>>(&mut self, clip_rect: Rect, shapes: I) {
|
||||
self.0.extend(
|
||||
shapes
|
||||
.into_iter()
|
||||
.map(|shape| ClippedShape(clip_rect, shape)),
|
||||
);
|
||||
}
|
||||
|
||||
/// Modify an existing [`Shape`].
|
|
@ -75,7 +75,7 @@ impl Region {
|
|||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Layout direction, one of [`LeftToRight`](Direction::LeftToRight), [`RightToLeft`](Direction::RightToLeft), [`TopDown`](Direction::TopDown), [`BottomUp`](Direction::BottomUp).
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub enum Direction {
|
||||
LeftToRight,
|
||||
|
@ -108,99 +108,111 @@ impl Direction {
|
|||
///
|
||||
/// ```
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// ui.with_layout(egui::Layout::right_to_left(), |ui| {
|
||||
/// ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| {
|
||||
/// ui.label("world!");
|
||||
/// ui.label("Hello");
|
||||
/// });
|
||||
/// # });
|
||||
/// ```
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
// #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct Layout {
|
||||
/// Main axis direction
|
||||
main_dir: Direction,
|
||||
pub main_dir: Direction,
|
||||
|
||||
/// If true, wrap around when reading the end of the main direction.
|
||||
/// For instance, for `main_dir == Direction::LeftToRight` this will
|
||||
/// wrap to a new row when we reach the right side of the `max_rect`.
|
||||
main_wrap: bool,
|
||||
pub main_wrap: bool,
|
||||
|
||||
/// How to align things on the main axis.
|
||||
main_align: Align,
|
||||
pub main_align: Align,
|
||||
|
||||
/// Justify the main axis?
|
||||
main_justify: bool,
|
||||
pub main_justify: bool,
|
||||
|
||||
/// How to align things on the cross axis.
|
||||
/// For vertical layouts: put things to left, center or right?
|
||||
/// For horizontal layouts: put things to top, center or bottom?
|
||||
cross_align: Align,
|
||||
pub cross_align: Align,
|
||||
|
||||
/// Justify the cross axis?
|
||||
/// For vertical layouts justify mean all widgets get maximum width.
|
||||
/// For horizontal layouts justify mean all widgets get maximum height.
|
||||
cross_justify: bool,
|
||||
pub cross_justify: bool,
|
||||
}
|
||||
|
||||
impl Default for Layout {
|
||||
fn default() -> Self {
|
||||
// TODO: Get from `Style` instead.
|
||||
// TODO(emilk): Get from `Style` instead.
|
||||
Self::top_down(Align::LEFT) // This is a very euro-centric default.
|
||||
}
|
||||
}
|
||||
|
||||
/// ## Constructors
|
||||
impl Layout {
|
||||
/// Place elements horizontally, left to right.
|
||||
///
|
||||
/// The `valign` parameter controls how to align elements vertically.
|
||||
#[inline(always)]
|
||||
pub fn left_to_right() -> Self {
|
||||
pub fn left_to_right(valign: Align) -> Self {
|
||||
Self {
|
||||
main_dir: Direction::LeftToRight,
|
||||
main_wrap: false,
|
||||
main_align: Align::Center, // looks best to e.g. center text within a button
|
||||
main_justify: false,
|
||||
cross_align: Align::Center,
|
||||
cross_align: valign,
|
||||
cross_justify: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Place elements horizontally, right to left.
|
||||
///
|
||||
/// The `valign` parameter controls how to align elements vertically.
|
||||
#[inline(always)]
|
||||
pub fn right_to_left() -> Self {
|
||||
pub fn right_to_left(valign: Align) -> Self {
|
||||
Self {
|
||||
main_dir: Direction::RightToLeft,
|
||||
main_wrap: false,
|
||||
main_align: Align::Center, // looks best to e.g. center text within a button
|
||||
main_justify: false,
|
||||
cross_align: Align::Center,
|
||||
cross_align: valign,
|
||||
cross_justify: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Place elements vertically, top to bottom.
|
||||
///
|
||||
/// Use the provided horizontal alignment.
|
||||
#[inline(always)]
|
||||
pub fn top_down(cross_align: Align) -> Self {
|
||||
pub fn top_down(halign: Align) -> Self {
|
||||
Self {
|
||||
main_dir: Direction::TopDown,
|
||||
main_wrap: false,
|
||||
main_align: Align::Center, // looks best to e.g. center text within a button
|
||||
main_justify: false,
|
||||
cross_align,
|
||||
cross_align: halign,
|
||||
cross_justify: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Top-down layout justifed so that buttons etc fill the full available width.
|
||||
/// Top-down layout justified so that buttons etc fill the full available width.
|
||||
#[inline(always)]
|
||||
pub fn top_down_justified(cross_align: Align) -> Self {
|
||||
Self::top_down(cross_align).with_cross_justify(true)
|
||||
pub fn top_down_justified(halign: Align) -> Self {
|
||||
Self::top_down(halign).with_cross_justify(true)
|
||||
}
|
||||
|
||||
/// Place elements vertically, bottom up.
|
||||
///
|
||||
/// Use the provided horizontal alignment.
|
||||
#[inline(always)]
|
||||
pub fn bottom_up(cross_align: Align) -> Self {
|
||||
pub fn bottom_up(halign: Align) -> Self {
|
||||
Self {
|
||||
main_dir: Direction::BottomUp,
|
||||
main_wrap: false,
|
||||
main_align: Align::Center, // looks best to e.g. center text within a button
|
||||
main_justify: false,
|
||||
cross_align,
|
||||
cross_align: halign,
|
||||
cross_justify: false,
|
||||
}
|
||||
}
|
||||
|
@ -217,6 +229,10 @@ impl Layout {
|
|||
}
|
||||
}
|
||||
|
||||
/// For when you want to add a single widget to a layout, and that widget
|
||||
/// should use up all available space.
|
||||
///
|
||||
/// Only one widget may be added to the inner `Ui`!
|
||||
#[inline(always)]
|
||||
pub fn centered_and_justified(main_dir: Direction) -> Self {
|
||||
Self {
|
||||
|
@ -229,11 +245,25 @@ impl Layout {
|
|||
}
|
||||
}
|
||||
|
||||
/// Wrap widgets when we overflow the main axis?
|
||||
///
|
||||
/// For instance, for left-to-right layouts, setting this to `true` will
|
||||
/// put widgets on a new row if we would overflow the right side of [`crate::Ui::max_rect`].
|
||||
#[inline(always)]
|
||||
pub fn with_main_wrap(self, main_wrap: bool) -> Self {
|
||||
Self { main_wrap, ..self }
|
||||
}
|
||||
|
||||
/// The alignment to use on the main axis.
|
||||
#[inline(always)]
|
||||
pub fn with_main_align(self, main_align: Align) -> Self {
|
||||
Self { main_align, ..self }
|
||||
}
|
||||
|
||||
/// The alignment to use on the cross axis.
|
||||
///
|
||||
/// The "cross" axis is the one orthogonal to the main axis.
|
||||
/// For instance: in left-to-right layout, the main axis is horizontal and the cross axis is vertical.
|
||||
#[inline(always)]
|
||||
pub fn with_cross_align(self, cross_align: Align) -> Self {
|
||||
Self {
|
||||
|
@ -242,6 +272,9 @@ impl Layout {
|
|||
}
|
||||
}
|
||||
|
||||
/// Justify widgets on the main axis?
|
||||
///
|
||||
/// Justify here means "take up all available space".
|
||||
#[inline(always)]
|
||||
pub fn with_main_justify(self, main_justify: bool) -> Self {
|
||||
Self {
|
||||
|
@ -250,6 +283,12 @@ impl Layout {
|
|||
}
|
||||
}
|
||||
|
||||
/// Justify widgets along the cross axis?
|
||||
///
|
||||
/// Justify here means "take up all available space".
|
||||
///
|
||||
/// The "cross" axis is the one orthogonal to the main axis.
|
||||
/// For instance: in left-to-right layout, the main axis is horizontal and the cross axis is vertical.
|
||||
#[inline(always)]
|
||||
pub fn with_cross_justify(self, cross_justify: bool) -> Self {
|
||||
Self {
|
|
@ -3,7 +3,7 @@
|
|||
//! Try the live web demo: <https://www.egui.rs/#demo>. Read more about egui at <https://github.com/emilk/egui>.
|
||||
//!
|
||||
//! `egui` is in heavy development, with each new version having breaking changes.
|
||||
//! You need to have rust 1.60.0 or later to use `egui`.
|
||||
//! You need to have rust 1.62.0 or later to use `egui`.
|
||||
//!
|
||||
//! To quickly get started with egui, you can take a look at [`eframe_template`](https://github.com/emilk/eframe_template)
|
||||
//! which uses [`eframe`](https://docs.rs/eframe).
|
||||
|
@ -177,7 +177,7 @@
|
|||
//! This means it is responsibility of the egui user to store the state (`value`) so that it persists between frames.
|
||||
//!
|
||||
//! It can be useful to read the code for the toggle switch example widget to get a better understanding
|
||||
//! of how egui works: <https://github.com/emilk/egui/blob/master/egui_demo_lib/src/apps/demo/toggle_switch.rs>.
|
||||
//! of how egui works: <https://github.com/emilk/egui/blob/master/crates/egui_demo_lib/src/demo/toggle_switch.rs>.
|
||||
//!
|
||||
//! Read more about the pros and cons of immediate mode at <https://github.com/emilk/egui#why-immediate-mode>.
|
||||
//!
|
||||
|
@ -291,6 +291,10 @@
|
|||
//! }); // the temporary settings are reverted here
|
||||
//! # });
|
||||
//! ```
|
||||
//!
|
||||
//! ## Feature flags
|
||||
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
|
||||
//!
|
||||
|
||||
#![allow(clippy::float_cmp)]
|
||||
#![allow(clippy::manual_range_contains)]
|
||||
|
@ -301,6 +305,7 @@ mod context;
|
|||
mod data;
|
||||
mod frame_state;
|
||||
pub(crate) mod grid;
|
||||
pub mod gui_zoom;
|
||||
mod id;
|
||||
mod input_state;
|
||||
pub mod introspection;
|
||||
|
@ -308,6 +313,7 @@ pub mod layers;
|
|||
mod layout;
|
||||
mod memory;
|
||||
pub mod menu;
|
||||
pub mod os;
|
||||
mod painter;
|
||||
pub(crate) mod placer;
|
||||
mod response;
|
||||
|
@ -315,19 +321,26 @@ mod sense;
|
|||
pub mod style;
|
||||
mod ui;
|
||||
pub mod util;
|
||||
mod widget_text;
|
||||
pub mod widget_text;
|
||||
pub mod widgets;
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub use accesskit;
|
||||
|
||||
pub use epaint;
|
||||
pub use epaint::ecolor;
|
||||
pub use epaint::emath;
|
||||
|
||||
#[cfg(feature = "color-hex")]
|
||||
pub use ecolor::hex_color;
|
||||
pub use ecolor::{Color32, Rgba};
|
||||
pub use emath::{lerp, pos2, remap, remap_clamp, vec2, Align, Align2, NumExt, Pos2, Rect, Vec2};
|
||||
pub use epaint::{
|
||||
color, mutex,
|
||||
mutex,
|
||||
text::{FontData, FontDefinitions, FontFamily, FontId, FontTweak},
|
||||
textures::TexturesDelta,
|
||||
ClippedPrimitive, Color32, ColorImage, FontImage, ImageData, Mesh, PaintCallback,
|
||||
PaintCallbackInfo, Rgba, Rounding, Shape, Stroke, TextureHandle, TextureId,
|
||||
textures::{TextureFilter, TextureOptions, TexturesDelta},
|
||||
ClippedPrimitive, ColorImage, FontImage, ImageData, Mesh, PaintCallback, PaintCallbackInfo,
|
||||
Rounding, Shape, Stroke, TextureHandle, TextureId,
|
||||
};
|
||||
|
||||
pub mod text {
|
||||
|
@ -350,11 +363,11 @@ pub use {
|
|||
input_state::{InputState, MultiTouchInfo, PointerState},
|
||||
layers::{LayerId, Order},
|
||||
layout::*,
|
||||
memory::Memory,
|
||||
memory::{Memory, Options},
|
||||
painter::Painter,
|
||||
response::{InnerResponse, Response},
|
||||
sense::Sense,
|
||||
style::{FontSelection, Style, TextStyle, Visuals},
|
||||
style::{FontSelection, Margin, Style, TextStyle, Visuals},
|
||||
text::{Galley, TextFormat},
|
||||
ui::Ui,
|
||||
widget_text::{RichText, WidgetText},
|
||||
|
@ -367,9 +380,9 @@ pub use {
|
|||
pub fn warn_if_debug_build(ui: &mut crate::Ui) {
|
||||
if cfg!(debug_assertions) {
|
||||
ui.label(
|
||||
RichText::new("‼ Debug build ‼")
|
||||
RichText::new("⚠ Debug build ⚠")
|
||||
.small()
|
||||
.color(crate::Color32::RED),
|
||||
.color(ui.visuals().warn_fg_color),
|
||||
)
|
||||
.on_hover_text("egui was compiled with debug assertions enabled.");
|
||||
}
|
||||
|
@ -473,15 +486,19 @@ macro_rules! egui_assert {
|
|||
pub mod special_emojis {
|
||||
/// Tux, the Linux penguin.
|
||||
pub const OS_LINUX: char = '🐧';
|
||||
|
||||
/// The Windows logo.
|
||||
pub const OS_WINDOWS: char = '';
|
||||
|
||||
/// The Android logo.
|
||||
pub const OS_ANDROID: char = '';
|
||||
|
||||
/// The Apple logo.
|
||||
pub const OS_APPLE: char = '';
|
||||
|
||||
/// The Github logo.
|
||||
pub const GITHUB: char = '';
|
||||
|
||||
/// The Twitter bird.
|
||||
pub const TWITTER: char = '';
|
||||
|
||||
|
@ -492,22 +509,34 @@ pub mod special_emojis {
|
|||
}
|
||||
|
||||
/// The different types of built-in widgets in egui
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub enum WidgetType {
|
||||
Label, // TODO: emit Label events
|
||||
Label, // TODO(emilk): emit Label events
|
||||
|
||||
/// e.g. a hyperlink
|
||||
Link,
|
||||
|
||||
TextEdit,
|
||||
|
||||
Button,
|
||||
|
||||
Checkbox,
|
||||
|
||||
RadioButton,
|
||||
|
||||
SelectableLabel,
|
||||
|
||||
ComboBox,
|
||||
|
||||
Slider,
|
||||
|
||||
DragValue,
|
||||
|
||||
ColorButton,
|
||||
|
||||
ImageButton,
|
||||
|
||||
CollapsingHeader,
|
||||
|
||||
/// If you cannot fit any of the above slots.
|
||||
|
@ -521,6 +550,7 @@ pub enum WidgetType {
|
|||
/// For use in tests; especially doctests.
|
||||
pub fn __run_test_ctx(mut run_ui: impl FnMut(&Context)) {
|
||||
let ctx = Context::default();
|
||||
ctx.set_fonts(FontDefinitions::empty()); // prevent fonts from being loaded (save CPU time)
|
||||
let _ = ctx.run(Default::default(), |ctx| {
|
||||
run_ui(ctx);
|
||||
});
|
||||
|
@ -529,9 +559,15 @@ pub fn __run_test_ctx(mut run_ui: impl FnMut(&Context)) {
|
|||
/// For use in tests; especially doctests.
|
||||
pub fn __run_test_ui(mut add_contents: impl FnMut(&mut Ui)) {
|
||||
let ctx = Context::default();
|
||||
ctx.set_fonts(FontDefinitions::empty()); // prevent fonts from being loaded (save CPU time)
|
||||
let _ = ctx.run(Default::default(), |ctx| {
|
||||
crate::CentralPanel::default().show(ctx, |ui| {
|
||||
add_contents(ui);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub fn accesskit_root_id() -> Id {
|
||||
Id::new("accesskit_root")
|
||||
}
|
|
@ -1,5 +1,3 @@
|
|||
use epaint::ahash::AHashSet;
|
||||
|
||||
use crate::{area, window, Id, IdMap, InputState, LayerId, Pos2, Rect, Style};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
@ -54,9 +52,10 @@ pub struct Memory {
|
|||
/// type CharCountCache<'a> = FrameCache<usize, CharCounter>;
|
||||
///
|
||||
/// # let mut ctx = egui::Context::default();
|
||||
/// let mut memory = ctx.memory();
|
||||
/// let cache = memory.caches.cache::<CharCountCache<'_>>();
|
||||
/// assert_eq!(cache.get("hello"), 5);
|
||||
/// ctx.memory_mut(|mem| {
|
||||
/// let cache = mem.caches.cache::<CharCountCache<'_>>();
|
||||
/// assert_eq!(cache.get("hello"), 5);
|
||||
/// });
|
||||
/// ```
|
||||
#[cfg_attr(feature = "persistence", serde(skip))]
|
||||
pub caches: crate::util::cache::CacheStorage,
|
||||
|
@ -104,9 +103,15 @@ pub struct Options {
|
|||
/// Controls the tessellator.
|
||||
pub tessellation_options: epaint::TessellationOptions,
|
||||
|
||||
/// This does not at all change the behavior of egui,
|
||||
/// but is a signal to any backend that we want the [`crate::PlatformOutput::events`] read out loud.
|
||||
/// This is a signal to any backend that we want the [`crate::PlatformOutput::events`] read out loud.
|
||||
///
|
||||
/// The only change to egui is that labels can be focused by pressing tab.
|
||||
///
|
||||
/// Screen readers is an experimental feature of egui, and not supported on all platforms.
|
||||
///
|
||||
/// `eframe` supports it only on web, using the `web_screen_reader` feature flag,
|
||||
/// but you should consider using [AccessKit](https://github.com/AccessKit/accesskit) instead,
|
||||
/// which `eframe` supports.
|
||||
pub screen_reader: bool,
|
||||
|
||||
/// If true, the most common glyphs (ASCII) are pre-rendered to the texture atlas.
|
||||
|
@ -168,7 +173,7 @@ pub(crate) struct Interaction {
|
|||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct Focus {
|
||||
/// The widget with keyboard focus (i.e. a text input field).
|
||||
id: Option<Id>,
|
||||
pub(crate) id: Option<Id>,
|
||||
|
||||
/// What had keyboard focus previous frame?
|
||||
id_previous_frame: Option<Id>,
|
||||
|
@ -176,6 +181,9 @@ pub(crate) struct Focus {
|
|||
/// Give focus to this widget next frame
|
||||
id_next_frame: Option<Id>,
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
id_requested_by_accesskit: Option<accesskit::NodeId>,
|
||||
|
||||
/// If set, the next widget that is interested in focus will automatically get it.
|
||||
/// Probably because the user pressed Tab.
|
||||
give_to_next: bool,
|
||||
|
@ -233,6 +241,11 @@ impl Focus {
|
|||
self.id = Some(id);
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
{
|
||||
self.id_requested_by_accesskit = None;
|
||||
}
|
||||
|
||||
self.pressed_tab = false;
|
||||
self.pressed_shift_tab = false;
|
||||
for event in &new_input.events {
|
||||
|
@ -242,6 +255,7 @@ impl Focus {
|
|||
key: crate::Key::Escape,
|
||||
pressed: true,
|
||||
modifiers: _,
|
||||
..
|
||||
}
|
||||
) {
|
||||
self.id = None;
|
||||
|
@ -253,6 +267,7 @@ impl Focus {
|
|||
key: crate::Key::Tab,
|
||||
pressed: true,
|
||||
modifiers,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
if !self.is_focus_locked {
|
||||
|
@ -263,6 +278,18 @@ impl Focus {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
{
|
||||
if let crate::Event::AccessKitActionRequest(accesskit::ActionRequest {
|
||||
action: accesskit::Action::Focus,
|
||||
target,
|
||||
data: None,
|
||||
}) = event
|
||||
{
|
||||
self.id_requested_by_accesskit = Some(*target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -283,6 +310,17 @@ impl Focus {
|
|||
}
|
||||
|
||||
fn interested_in_focus(&mut self, id: Id) {
|
||||
#[cfg(feature = "accesskit")]
|
||||
{
|
||||
if self.id_requested_by_accesskit == Some(id.accesskit_id()) {
|
||||
self.id = Some(id);
|
||||
self.id_requested_by_accesskit = None;
|
||||
self.give_to_next = false;
|
||||
self.pressed_tab = false;
|
||||
self.pressed_shift_tab = false;
|
||||
}
|
||||
}
|
||||
|
||||
if self.give_to_next && !self.had_focus_last_frame(id) {
|
||||
self.id = Some(id);
|
||||
self.give_to_next = false;
|
||||
|
@ -295,9 +333,10 @@ impl Focus {
|
|||
self.id_next_frame = self.last_interested; // frame-delay so gained_focus works
|
||||
self.pressed_shift_tab = false;
|
||||
}
|
||||
} else if self.pressed_tab && self.id == None && !self.give_to_next {
|
||||
} else if self.pressed_tab && self.id.is_none() && !self.give_to_next {
|
||||
// nothing has focus and the user pressed tab - give focus to the first widgets that wants it:
|
||||
self.id = Some(id);
|
||||
self.pressed_tab = false;
|
||||
}
|
||||
|
||||
self.last_interested = Some(id);
|
||||
|
@ -349,6 +388,11 @@ impl Memory {
|
|||
}
|
||||
|
||||
/// Does this widget have keyboard focus?
|
||||
///
|
||||
/// This function does not consider whether the UI as a whole (e.g. window)
|
||||
/// has the keyboard focus. That makes this function suitable for deciding
|
||||
/// widget state that should not be disrupted if the user moves away
|
||||
/// from the window and back.
|
||||
#[inline(always)]
|
||||
pub fn has_focus(&self, id: Id) -> bool {
|
||||
self.interaction.focus.id == Some(id)
|
||||
|
@ -368,7 +412,7 @@ impl Memory {
|
|||
}
|
||||
|
||||
/// Is the keyboard focus locked on this widget? If so the focus won't move even if the user presses the tab key.
|
||||
pub fn has_lock_focus(&mut self, id: Id) -> bool {
|
||||
pub fn has_lock_focus(&self, id: Id) -> bool {
|
||||
if self.had_focus_last_frame(id) && self.has_focus(id) {
|
||||
self.interaction.focus.is_focus_locked
|
||||
} else {
|
||||
|
@ -396,8 +440,13 @@ impl Memory {
|
|||
|
||||
/// Register this widget as being interested in getting keyboard focus.
|
||||
/// This will allow the user to select it with tab and shift-tab.
|
||||
/// This is normally done automatically when handling interactions,
|
||||
/// but it is sometimes useful to pre-register interest in focus,
|
||||
/// e.g. before deciding which type of underlying widget to use,
|
||||
/// as in the [`crate::DragValue`] widget, so a widget can be focused
|
||||
/// and rendered correctly in a single frame.
|
||||
#[inline(always)]
|
||||
pub(crate) fn interested_in_focus(&mut self, id: Id) {
|
||||
pub fn interested_in_focus(&mut self, id: Id) {
|
||||
self.interaction.focus.interested_in_focus(id);
|
||||
}
|
||||
|
||||
|
@ -437,6 +486,10 @@ impl Memory {
|
|||
self.popup == Some(popup_id) || self.everything_is_visible()
|
||||
}
|
||||
|
||||
pub fn any_popup_open(&self) -> bool {
|
||||
self.popup.is_some() || self.everything_is_visible()
|
||||
}
|
||||
|
||||
pub fn open_popup(&mut self, popup_id: Id) {
|
||||
self.popup = Some(popup_id);
|
||||
}
|
||||
|
@ -484,15 +537,15 @@ pub struct Areas {
|
|||
areas: IdMap<area::State>,
|
||||
/// Back-to-front. Top is last.
|
||||
order: Vec<LayerId>,
|
||||
visible_last_frame: AHashSet<LayerId>,
|
||||
visible_current_frame: AHashSet<LayerId>,
|
||||
visible_last_frame: ahash::HashSet<LayerId>,
|
||||
visible_current_frame: ahash::HashSet<LayerId>,
|
||||
|
||||
/// When an area want to be on top, it is put in here.
|
||||
/// At the end of the frame, this is used to reorder the layers.
|
||||
/// This means if several layers want to be on top, they will keep their relative order.
|
||||
/// So if you close three windows and then reopen them all in one frame,
|
||||
/// they will all be sent to the top, but keep their previous internal order.
|
||||
wants_to_be_on_top: AHashSet<LayerId>,
|
||||
wants_to_be_on_top: ahash::HashSet<LayerId>,
|
||||
}
|
||||
|
||||
impl Areas {
|
||||
|
@ -544,11 +597,11 @@ impl Areas {
|
|||
self.visible_last_frame.contains(layer_id) || self.visible_current_frame.contains(layer_id)
|
||||
}
|
||||
|
||||
pub fn visible_layer_ids(&self) -> AHashSet<LayerId> {
|
||||
pub fn visible_layer_ids(&self) -> ahash::HashSet<LayerId> {
|
||||
self.visible_last_frame
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain(self.visible_current_frame.iter().cloned())
|
||||
.copied()
|
||||
.chain(self.visible_current_frame.iter().copied())
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
@ -587,7 +640,6 @@ impl Areas {
|
|||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
#[test]
|
||||
fn memory_impl_send_sync() {
|
||||
fn assert_send_sync<T: Send + Sync>() {}
|
|
@ -20,7 +20,7 @@ use super::{
|
|||
Sense, TextStyle, Ui, Vec2,
|
||||
};
|
||||
use crate::{widgets::*, *};
|
||||
use epaint::{mutex::RwLock, Stroke};
|
||||
use epaint::mutex::RwLock;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// What is saved between frames.
|
||||
|
@ -31,11 +31,11 @@ pub(crate) struct BarState {
|
|||
|
||||
impl BarState {
|
||||
fn load(ctx: &Context, bar_id: Id) -> Self {
|
||||
ctx.data().get_temp::<Self>(bar_id).unwrap_or_default()
|
||||
ctx.data_mut(|d| d.get_temp::<Self>(bar_id).unwrap_or_default())
|
||||
}
|
||||
|
||||
fn store(self, ctx: &Context, bar_id: Id) {
|
||||
ctx.data().insert_temp(bar_id, self);
|
||||
ctx.data_mut(|d| d.insert_temp(bar_id, self));
|
||||
}
|
||||
|
||||
/// Show a menu at pointer if primary-clicked response.
|
||||
|
@ -49,32 +49,35 @@ impl BarState {
|
|||
self.open_menu.show(response, add_contents)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for BarState {
|
||||
type Target = MenuRootManager;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.open_menu
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for BarState {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.open_menu
|
||||
}
|
||||
}
|
||||
|
||||
fn set_menu_style(style: &mut Style) {
|
||||
style.spacing.button_padding = vec2(2.0, 0.0);
|
||||
style.visuals.widgets.active.bg_stroke = Stroke::NONE;
|
||||
style.visuals.widgets.hovered.bg_stroke = Stroke::NONE;
|
||||
style.visuals.widgets.inactive.weak_bg_fill = Color32::TRANSPARENT;
|
||||
style.visuals.widgets.inactive.bg_stroke = Stroke::NONE;
|
||||
}
|
||||
|
||||
/// The menu bar goes well in a [`TopBottomPanel::top`],
|
||||
/// but can also be placed in a [`Window`].
|
||||
/// In the latter case you may want to wrap it in [`Frame`].
|
||||
pub fn bar<R>(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
|
||||
ui.horizontal(|ui| {
|
||||
let mut style = (**ui.style()).clone();
|
||||
style.spacing.button_padding = vec2(2.0, 0.0);
|
||||
// style.visuals.widgets.active.bg_fill = Color32::TRANSPARENT;
|
||||
style.visuals.widgets.active.bg_stroke = Stroke::none();
|
||||
// style.visuals.widgets.hovered.bg_fill = Color32::TRANSPARENT;
|
||||
style.visuals.widgets.hovered.bg_stroke = Stroke::none();
|
||||
style.visuals.widgets.inactive.bg_fill = Color32::TRANSPARENT;
|
||||
style.visuals.widgets.inactive.bg_stroke = Stroke::none();
|
||||
ui.set_style(style);
|
||||
set_menu_style(ui.style_mut());
|
||||
|
||||
// Take full width and fixed height:
|
||||
let height = ui.spacing().interact_size.y;
|
||||
|
@ -97,6 +100,20 @@ pub fn menu_button<R>(
|
|||
stationary_menu_impl(ui, title, Box::new(add_contents))
|
||||
}
|
||||
|
||||
/// Construct a top level menu with an image in a menu bar. This would be e.g. "File", "Edit" etc.
|
||||
///
|
||||
/// Responds to primary clicks.
|
||||
///
|
||||
/// Returns `None` if the menu is not open.
|
||||
pub fn menu_image_button<R>(
|
||||
ui: &mut Ui,
|
||||
texture_id: TextureId,
|
||||
image_size: impl Into<Vec2>,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<Option<R>> {
|
||||
stationary_menu_image_impl(ui, texture_id, image_size, Box::new(add_contents))
|
||||
}
|
||||
|
||||
/// Construct a nested sub menu in another menu.
|
||||
///
|
||||
/// Opens on hover.
|
||||
|
@ -114,7 +131,7 @@ pub(crate) fn submenu_button<R>(
|
|||
/// wrapper for the contents of every menu.
|
||||
pub(crate) fn menu_ui<'c, R>(
|
||||
ctx: &Context,
|
||||
menu_id: impl std::hash::Hash,
|
||||
menu_id: impl Into<Id>,
|
||||
menu_state_arc: &Arc<RwLock<MenuState>>,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R + 'c,
|
||||
) -> InnerResponse<R> {
|
||||
|
@ -126,31 +143,22 @@ pub(crate) fn menu_ui<'c, R>(
|
|||
|
||||
let area = Area::new(menu_id)
|
||||
.order(Order::Foreground)
|
||||
.constrain(true)
|
||||
.fixed_pos(pos)
|
||||
.interactable(true)
|
||||
.drag_bounds(Rect::EVERYTHING);
|
||||
.drag_bounds(ctx.screen_rect());
|
||||
let inner_response = area.show(ctx, |ui| {
|
||||
ui.scope(|ui| {
|
||||
let style = ui.style_mut();
|
||||
style.spacing.item_spacing = Vec2::ZERO;
|
||||
style.spacing.button_padding = crate::vec2(2.0, 0.0);
|
||||
set_menu_style(ui.style_mut());
|
||||
|
||||
style.visuals.widgets.active.bg_stroke = Stroke::none();
|
||||
style.visuals.widgets.hovered.bg_stroke = Stroke::none();
|
||||
style.visuals.widgets.inactive.bg_fill = Color32::TRANSPARENT;
|
||||
style.visuals.widgets.inactive.bg_stroke = Stroke::none();
|
||||
|
||||
Frame::menu(style)
|
||||
.show(ui, |ui| {
|
||||
const DEFAULT_MENU_WIDTH: f32 = 150.0; // TODO: add to ui.spacing
|
||||
ui.set_max_width(DEFAULT_MENU_WIDTH);
|
||||
ui.set_menu_state(Some(menu_state_arc.clone()));
|
||||
ui.with_layout(Layout::top_down_justified(Align::LEFT), add_contents)
|
||||
.inner
|
||||
})
|
||||
.inner
|
||||
})
|
||||
.inner
|
||||
Frame::menu(ui.style())
|
||||
.show(ui, |ui| {
|
||||
const DEFAULT_MENU_WIDTH: f32 = 150.0; // TODO(emilk): add to ui.spacing
|
||||
ui.set_max_width(DEFAULT_MENU_WIDTH);
|
||||
ui.set_menu_state(Some(menu_state_arc.clone()));
|
||||
ui.with_layout(Layout::top_down_justified(Align::LEFT), add_contents)
|
||||
.inner
|
||||
})
|
||||
.inner
|
||||
});
|
||||
menu_state_arc.write().rect = inner_response.response.rect;
|
||||
inner_response
|
||||
|
@ -173,7 +181,7 @@ fn stationary_menu_impl<'c, R>(
|
|||
let mut button = Button::new(title);
|
||||
|
||||
if bar_state.open_menu.is_menu_open(menu_id) {
|
||||
button = button.fill(ui.visuals().widgets.open.bg_fill);
|
||||
button = button.fill(ui.visuals().widgets.open.weak_bg_fill);
|
||||
button = button.stroke(ui.visuals().widgets.open.bg_stroke);
|
||||
}
|
||||
|
||||
|
@ -184,6 +192,25 @@ fn stationary_menu_impl<'c, R>(
|
|||
InnerResponse::new(inner.map(|r| r.inner), button_response)
|
||||
}
|
||||
|
||||
/// Build a top level menu with an image button.
|
||||
///
|
||||
/// Responds to primary clicks.
|
||||
fn stationary_menu_image_impl<'c, R>(
|
||||
ui: &mut Ui,
|
||||
texture_id: TextureId,
|
||||
image_size: impl Into<Vec2>,
|
||||
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
|
||||
) -> InnerResponse<Option<R>> {
|
||||
let bar_id = ui.id();
|
||||
|
||||
let mut bar_state = BarState::load(ui.ctx(), bar_id);
|
||||
let button_response = ui.add(ImageButton::new(texture_id, image_size));
|
||||
let inner = bar_state.bar_menu(&button_response, add_contents);
|
||||
|
||||
bar_state.store(ui.ctx(), bar_id);
|
||||
InnerResponse::new(inner.map(|r| r.inner), button_response)
|
||||
}
|
||||
|
||||
/// Response to secondary clicks (right-clicks) by showing the given menu.
|
||||
pub(crate) fn context_menu(
|
||||
response: &Response,
|
||||
|
@ -204,6 +231,7 @@ pub(crate) fn context_menu(
|
|||
pub(crate) struct MenuRootManager {
|
||||
inner: Option<MenuRoot>,
|
||||
}
|
||||
|
||||
impl MenuRootManager {
|
||||
/// Show a menu at pointer if right-clicked response.
|
||||
/// Should be called from [`Context`] on a [`Response`]
|
||||
|
@ -222,16 +250,20 @@ impl MenuRootManager {
|
|||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn is_menu_open(&self, id: Id) -> bool {
|
||||
self.inner.as_ref().map(|m| m.id) == Some(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for MenuRootManager {
|
||||
type Target = Option<MenuRoot>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for MenuRootManager {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.inner
|
||||
|
@ -252,6 +284,7 @@ impl MenuRoot {
|
|||
id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show<R>(
|
||||
&mut self,
|
||||
response: &Response,
|
||||
|
@ -278,10 +311,9 @@ impl MenuRoot {
|
|||
root: &mut MenuRootManager,
|
||||
id: Id,
|
||||
) -> MenuResponse {
|
||||
// Lock the input once for the whole function call (see https://github.com/emilk/egui/pull/1380).
|
||||
let input = response.ctx.input();
|
||||
|
||||
if (response.clicked() && root.is_menu_open(id)) || input.key_pressed(Key::Escape) {
|
||||
if (response.clicked() && root.is_menu_open(id))
|
||||
|| response.ctx.input(|i| i.key_pressed(Key::Escape))
|
||||
{
|
||||
// menu open and button clicked or esc pressed
|
||||
return MenuResponse::Close;
|
||||
} else if (response.clicked() && !root.is_menu_open(id))
|
||||
|
@ -289,10 +321,26 @@ impl MenuRoot {
|
|||
{
|
||||
// menu not open and button clicked
|
||||
// or button hovered while other menu is open
|
||||
let pos = response.rect.left_bottom();
|
||||
let mut pos = response.rect.left_bottom();
|
||||
if let Some(root) = root.inner.as_mut() {
|
||||
let menu_rect = root.menu_state.read().rect;
|
||||
let screen_rect = response.ctx.input(|i| i.screen_rect);
|
||||
|
||||
if pos.y + menu_rect.height() > screen_rect.max.y {
|
||||
pos.y = screen_rect.max.y - menu_rect.height() - response.rect.height();
|
||||
}
|
||||
|
||||
if pos.x + menu_rect.width() > screen_rect.max.x {
|
||||
pos.x = screen_rect.max.x - menu_rect.width();
|
||||
}
|
||||
}
|
||||
|
||||
return MenuResponse::Create(pos, id);
|
||||
} else if input.pointer.any_pressed() && input.pointer.primary_down() {
|
||||
if let Some(pos) = input.pointer.interact_pos() {
|
||||
} else if response
|
||||
.ctx
|
||||
.input(|i| i.pointer.any_pressed() && i.pointer.primary_down())
|
||||
{
|
||||
if let Some(pos) = response.ctx.input(|i| i.pointer.interact_pos()) {
|
||||
if let Some(root) = root.inner.as_mut() {
|
||||
if root.id == id {
|
||||
// pressed somewhere while this menu is open
|
||||
|
@ -315,26 +363,28 @@ impl MenuRoot {
|
|||
id: Id,
|
||||
) -> MenuResponse {
|
||||
let response = response.interact(Sense::click());
|
||||
let pointer = &response.ctx.input().pointer;
|
||||
if pointer.any_pressed() {
|
||||
if let Some(pos) = pointer.interact_pos() {
|
||||
let mut destroy = false;
|
||||
let mut in_old_menu = false;
|
||||
if let Some(root) = root {
|
||||
let menu_state = root.menu_state.read();
|
||||
in_old_menu = menu_state.area_contains(pos);
|
||||
destroy = root.id == response.id;
|
||||
}
|
||||
if !in_old_menu {
|
||||
if response.hovered() && pointer.secondary_down() {
|
||||
return MenuResponse::Create(pos, id);
|
||||
} else if (response.hovered() && pointer.primary_down()) || destroy {
|
||||
return MenuResponse::Close;
|
||||
response.ctx.input(|input| {
|
||||
let pointer = &input.pointer;
|
||||
if pointer.any_pressed() {
|
||||
if let Some(pos) = pointer.interact_pos() {
|
||||
let mut destroy = false;
|
||||
let mut in_old_menu = false;
|
||||
if let Some(root) = root {
|
||||
let menu_state = root.menu_state.read();
|
||||
in_old_menu = menu_state.area_contains(pos);
|
||||
destroy = root.id == response.id;
|
||||
}
|
||||
if !in_old_menu {
|
||||
if response.hovered() && pointer.secondary_down() {
|
||||
return MenuResponse::Create(pos, id);
|
||||
} else if (response.hovered() && pointer.primary_down()) || destroy {
|
||||
return MenuResponse::Close;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MenuResponse::Stay
|
||||
MenuResponse::Stay
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_menu_response(root: &mut MenuRootManager, menu_response: MenuResponse) {
|
||||
|
@ -359,22 +409,26 @@ impl MenuRoot {
|
|||
Self::handle_menu_response(root, menu_response);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq)]
|
||||
pub(crate) enum MenuResponse {
|
||||
Close,
|
||||
Stay,
|
||||
Create(Pos2, Id),
|
||||
}
|
||||
|
||||
impl MenuResponse {
|
||||
pub fn is_close(&self) -> bool {
|
||||
*self == Self::Close
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SubMenuButton {
|
||||
text: WidgetText,
|
||||
icon: WidgetText,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl SubMenuButton {
|
||||
/// The `icon` can be an emoji (e.g. `⏵` right arrow), shown right of the label
|
||||
fn new(text: impl Into<WidgetText>, icon: impl Into<WidgetText>, index: usize) -> Self {
|
||||
|
@ -392,7 +446,7 @@ impl SubMenuButton {
|
|||
sub_id: Id,
|
||||
) -> &'a WidgetVisuals {
|
||||
if menu_state.is_open(sub_id) {
|
||||
&ui.style().visuals.widgets.hovered
|
||||
&ui.style().visuals.widgets.open
|
||||
} else {
|
||||
ui.style().interact(response)
|
||||
}
|
||||
|
@ -421,11 +475,12 @@ impl SubMenuButton {
|
|||
text_galley.size().x + icon_galley.size().x,
|
||||
text_galley.size().y.max(icon_galley.size().y),
|
||||
);
|
||||
let desired_size = text_and_icon_size + 2.0 * button_padding;
|
||||
let mut desired_size = text_and_icon_size + 2.0 * button_padding;
|
||||
desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y);
|
||||
|
||||
let (rect, response) = ui.allocate_at_least(desired_size, sense);
|
||||
response.widget_info(|| {
|
||||
crate::WidgetInfo::labeled(crate::WidgetType::Button, &text_galley.text())
|
||||
crate::WidgetInfo::labeled(crate::WidgetType::Button, text_galley.text())
|
||||
});
|
||||
|
||||
if ui.is_rect_visible(rect) {
|
||||
|
@ -437,11 +492,13 @@ impl SubMenuButton {
|
|||
.align_size_within_rect(icon_galley.size(), rect.shrink2(button_padding))
|
||||
.min;
|
||||
|
||||
ui.painter().rect_filled(
|
||||
rect.expand(visuals.expansion),
|
||||
visuals.rounding,
|
||||
visuals.bg_fill,
|
||||
);
|
||||
if ui.visuals().button_frame {
|
||||
ui.painter().rect_filled(
|
||||
rect.expand(visuals.expansion),
|
||||
visuals.rounding,
|
||||
visuals.weak_bg_fill,
|
||||
);
|
||||
}
|
||||
|
||||
let text_color = visuals.text_color();
|
||||
text_galley.paint_with_fallback_color(ui.painter(), text_pos, text_color);
|
||||
|
@ -450,10 +507,12 @@ impl SubMenuButton {
|
|||
response
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SubMenu {
|
||||
button: SubMenuButton,
|
||||
parent_state: Arc<RwLock<MenuState>>,
|
||||
}
|
||||
|
||||
impl SubMenu {
|
||||
fn new(parent_state: Arc<RwLock<MenuState>>, text: impl Into<WidgetText>) -> Self {
|
||||
let index = parent_state.write().next_entry_index();
|
||||
|
@ -469,7 +528,7 @@ impl SubMenu {
|
|||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<Option<R>> {
|
||||
let sub_id = ui.id().with(self.button.index);
|
||||
let button = self.button.show(ui, &*self.parent_state.read(), sub_id);
|
||||
let button = self.button.show(ui, &self.parent_state.read(), sub_id);
|
||||
self.parent_state
|
||||
.write()
|
||||
.submenu_button_interaction(ui, sub_id, &button);
|
||||
|
@ -480,16 +539,21 @@ impl SubMenu {
|
|||
InnerResponse::new(inner, button)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct MenuState {
|
||||
/// The opened sub-menu and its [`Id`]
|
||||
sub_menu: Option<(Id, Arc<RwLock<MenuState>>)>,
|
||||
|
||||
/// Bounding box of this menu (without the sub-menu)
|
||||
pub rect: Rect,
|
||||
|
||||
/// Used to check if any menu in the tree wants to close
|
||||
pub response: MenuResponse,
|
||||
|
||||
/// Used to hash different [`Id`]s for sub-menus
|
||||
entry_count: usize,
|
||||
}
|
||||
|
||||
impl MenuState {
|
||||
pub fn new(position: Pos2) -> Self {
|
||||
Self {
|
||||
|
@ -499,10 +563,12 @@ impl MenuState {
|
|||
entry_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Close menu hierarchy.
|
||||
pub fn close(&mut self) {
|
||||
self.response = MenuResponse::Close;
|
||||
}
|
||||
|
||||
pub fn show<R>(
|
||||
ctx: &Context,
|
||||
menu_state: &Arc<RwLock<Self>>,
|
||||
|
@ -511,19 +577,21 @@ impl MenuState {
|
|||
) -> InnerResponse<R> {
|
||||
crate::menu::menu_ui(ctx, id, menu_state, add_contents)
|
||||
}
|
||||
|
||||
fn show_submenu<R>(
|
||||
&mut self,
|
||||
ctx: &Context,
|
||||
id: Id,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> Option<R> {
|
||||
let (sub_response, response) = self.get_submenu(id).map(|sub| {
|
||||
let (sub_response, response) = self.submenu(id).map(|sub| {
|
||||
let inner_response = Self::show(ctx, sub, id, add_contents);
|
||||
(sub.read().response, inner_response.inner)
|
||||
})?;
|
||||
self.cascade_close_response(sub_response);
|
||||
Some(response)
|
||||
}
|
||||
|
||||
/// Check if position is in the menu hierarchy's area.
|
||||
pub fn area_contains(&self, pos: Pos2) -> bool {
|
||||
self.rect.contains(pos)
|
||||
|
@ -532,24 +600,27 @@ impl MenuState {
|
|||
.as_ref()
|
||||
.map_or(false, |(_, sub)| sub.read().area_contains(pos))
|
||||
}
|
||||
|
||||
fn next_entry_index(&mut self) -> usize {
|
||||
self.entry_count += 1;
|
||||
self.entry_count - 1
|
||||
}
|
||||
|
||||
/// Sense button interaction opening and closing submenu.
|
||||
fn submenu_button_interaction(&mut self, ui: &mut Ui, sub_id: Id, button: &Response) {
|
||||
let pointer = &ui.input().pointer.clone();
|
||||
let pointer = ui.input(|i| i.pointer.clone());
|
||||
let open = self.is_open(sub_id);
|
||||
if self.moving_towards_current_submenu(pointer) {
|
||||
if self.moving_towards_current_submenu(&pointer) {
|
||||
// ensure to repaint once even when pointer is not moving
|
||||
ui.ctx().request_repaint();
|
||||
} else if !open && button.hovered() {
|
||||
let pos = button.rect.right_top();
|
||||
self.open_submenu(sub_id, pos);
|
||||
} else if open && !button.hovered() && !self.hovering_current_submenu(pointer) {
|
||||
} else if open && !button.hovered() && !self.hovering_current_submenu(&pointer) {
|
||||
self.close_submenu();
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if `dir` points from `pos` towards left side of `rect`.
|
||||
fn points_at_left_of_rect(pos: Pos2, dir: Vec2, rect: Rect) -> bool {
|
||||
let vel_a = dir.angle();
|
||||
|
@ -557,53 +628,62 @@ impl MenuState {
|
|||
let bottom_a = (rect.left_bottom() - pos).angle();
|
||||
bottom_a - vel_a >= 0.0 && top_a - vel_a <= 0.0
|
||||
}
|
||||
|
||||
/// Check if pointer is moving towards current submenu.
|
||||
fn moving_towards_current_submenu(&self, pointer: &PointerState) -> bool {
|
||||
if pointer.is_still() {
|
||||
return false;
|
||||
}
|
||||
if let Some(sub_menu) = self.get_current_submenu() {
|
||||
if let Some(sub_menu) = self.current_submenu() {
|
||||
if let Some(pos) = pointer.hover_pos() {
|
||||
return Self::points_at_left_of_rect(pos, pointer.velocity(), sub_menu.read().rect);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Check if pointer is hovering current submenu.
|
||||
fn hovering_current_submenu(&self, pointer: &PointerState) -> bool {
|
||||
if let Some(sub_menu) = self.get_current_submenu() {
|
||||
if let Some(sub_menu) = self.current_submenu() {
|
||||
if let Some(pos) = pointer.hover_pos() {
|
||||
return sub_menu.read().area_contains(pos);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Cascade close response to menu root.
|
||||
fn cascade_close_response(&mut self, response: MenuResponse) {
|
||||
if response.is_close() {
|
||||
self.response = response;
|
||||
}
|
||||
}
|
||||
|
||||
fn is_open(&self, id: Id) -> bool {
|
||||
self.get_sub_id() == Some(id)
|
||||
self.sub_id() == Some(id)
|
||||
}
|
||||
fn get_sub_id(&self) -> Option<Id> {
|
||||
|
||||
fn sub_id(&self) -> Option<Id> {
|
||||
self.sub_menu.as_ref().map(|(id, _)| *id)
|
||||
}
|
||||
fn get_current_submenu(&self) -> Option<&Arc<RwLock<MenuState>>> {
|
||||
|
||||
fn current_submenu(&self) -> Option<&Arc<RwLock<MenuState>>> {
|
||||
self.sub_menu.as_ref().map(|(_, sub)| sub)
|
||||
}
|
||||
fn get_submenu(&mut self, id: Id) -> Option<&Arc<RwLock<MenuState>>> {
|
||||
|
||||
fn submenu(&mut self, id: Id) -> Option<&Arc<RwLock<MenuState>>> {
|
||||
self.sub_menu
|
||||
.as_ref()
|
||||
.and_then(|(k, sub)| if id == *k { Some(sub) } else { None })
|
||||
}
|
||||
|
||||
/// Open submenu at position, if not already open.
|
||||
fn open_submenu(&mut self, id: Id, pos: Pos2) {
|
||||
if !self.is_open(id) {
|
||||
self.sub_menu = Some((id, Arc::new(RwLock::new(MenuState::new(pos)))));
|
||||
}
|
||||
}
|
||||
|
||||
fn close_submenu(&mut self) {
|
||||
self.sub_menu = None;
|
||||
}
|
76
crates/egui/src/os.rs
Normal file
76
crates/egui/src/os.rs
Normal file
|
@ -0,0 +1,76 @@
|
|||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum OperatingSystem {
|
||||
/// Unknown OS - could be wasm
|
||||
Unknown,
|
||||
|
||||
/// Android OS.
|
||||
Android,
|
||||
|
||||
/// Apple iPhone OS.
|
||||
IOS,
|
||||
|
||||
/// Linux or Unix other than Android.
|
||||
Nix,
|
||||
|
||||
/// MacOS.
|
||||
Mac,
|
||||
|
||||
/// Windows.
|
||||
Windows,
|
||||
}
|
||||
|
||||
impl Default for OperatingSystem {
|
||||
fn default() -> Self {
|
||||
Self::from_target_os()
|
||||
}
|
||||
}
|
||||
|
||||
impl OperatingSystem {
|
||||
pub const fn from_target_os() -> Self {
|
||||
if cfg!(target_arch = "wasm32") {
|
||||
Self::Unknown
|
||||
} else if cfg!(target_os = "android") {
|
||||
Self::Android
|
||||
} else if cfg!(target_os = "ios") {
|
||||
Self::IOS
|
||||
} else if cfg!(target_os = "macos") {
|
||||
Self::Mac
|
||||
} else if cfg!(target_os = "windows") {
|
||||
Self::Android
|
||||
} else if cfg!(target_os = "linux")
|
||||
|| cfg!(target_os = "dragonfly")
|
||||
|| cfg!(target_os = "freebsd")
|
||||
|| cfg!(target_os = "netbsd")
|
||||
|| cfg!(target_os = "openbsd")
|
||||
{
|
||||
Self::Nix
|
||||
} else {
|
||||
Self::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper: try to guess from the user-agent of a browser.
|
||||
pub fn from_user_agent(user_agent: &str) -> Self {
|
||||
if user_agent.contains("Android") {
|
||||
Self::Android
|
||||
} else if user_agent.contains("like Mac") {
|
||||
Self::IOS
|
||||
} else if user_agent.contains("Win") {
|
||||
Self::Windows
|
||||
} else if user_agent.contains("Mac") {
|
||||
Self::Mac
|
||||
} else if user_agent.contains("Linux")
|
||||
|| user_agent.contains("X11")
|
||||
|| user_agent.contains("Unix")
|
||||
{
|
||||
Self::Nix
|
||||
} else {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!(
|
||||
"egui: Failed to guess operating system from User-Agent {:?}. Please file an issue at https://github.com/emilk/egui/issues",
|
||||
user_agent);
|
||||
|
||||
Self::Unknown
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,12 +2,11 @@ use std::ops::RangeInclusive;
|
|||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
emath::{pos2, Align2, Pos2, Rect, Vec2},
|
||||
emath::{Align2, Pos2, Rect, Vec2},
|
||||
layers::{LayerId, PaintList, ShapeIdx},
|
||||
Color32, Context, FontId,
|
||||
};
|
||||
use epaint::{
|
||||
mutex::{RwLockReadGuard, RwLockWriteGuard},
|
||||
text::{Fonts, Galley},
|
||||
CircleShape, RectShape, Rounding, Shape, Stroke,
|
||||
};
|
||||
|
@ -105,10 +104,12 @@ impl Painter {
|
|||
&self.ctx
|
||||
}
|
||||
|
||||
/// Available fonts.
|
||||
/// Read-only access to the shared [`Fonts`].
|
||||
///
|
||||
/// See [`Context`] documentation for how locks work.
|
||||
#[inline(always)]
|
||||
pub fn fonts(&self) -> RwLockReadGuard<'_, Fonts> {
|
||||
self.ctx.fonts()
|
||||
pub fn fonts<R>(&self, reader: impl FnOnce(&Fonts) -> R) -> R {
|
||||
self.ctx.fonts(reader)
|
||||
}
|
||||
|
||||
/// Where we paint
|
||||
|
@ -152,8 +153,9 @@ impl Painter {
|
|||
|
||||
/// ## Low level
|
||||
impl Painter {
|
||||
fn paint_list(&self) -> RwLockWriteGuard<'_, PaintList> {
|
||||
RwLockWriteGuard::map(self.ctx.graphics(), |g| g.list(self.layer_id))
|
||||
#[inline]
|
||||
fn paint_list<R>(&self, writer: impl FnOnce(&mut PaintList) -> R) -> R {
|
||||
self.ctx.graphics_mut(|g| writer(g.list(self.layer_id)))
|
||||
}
|
||||
|
||||
fn transform_shape(&self, shape: &mut Shape) {
|
||||
|
@ -167,30 +169,30 @@ impl Painter {
|
|||
/// NOTE: all coordinates are screen coordinates!
|
||||
pub fn add(&self, shape: impl Into<Shape>) -> ShapeIdx {
|
||||
if self.fade_to_color == Some(Color32::TRANSPARENT) {
|
||||
self.paint_list().add(self.clip_rect, Shape::Noop)
|
||||
self.paint_list(|l| l.add(self.clip_rect, Shape::Noop))
|
||||
} else {
|
||||
let mut shape = shape.into();
|
||||
self.transform_shape(&mut shape);
|
||||
self.paint_list().add(self.clip_rect, shape)
|
||||
self.paint_list(|l| l.add(self.clip_rect, shape))
|
||||
}
|
||||
}
|
||||
|
||||
/// Add many shapes at once.
|
||||
///
|
||||
/// Calling this once is generally faster than calling [`Self::add`] multiple times.
|
||||
pub fn extend(&self, mut shapes: Vec<Shape>) {
|
||||
pub fn extend<I: IntoIterator<Item = Shape>>(&self, shapes: I) {
|
||||
if self.fade_to_color == Some(Color32::TRANSPARENT) {
|
||||
return;
|
||||
}
|
||||
if !shapes.is_empty() {
|
||||
if self.fade_to_color.is_some() {
|
||||
for shape in &mut shapes {
|
||||
self.transform_shape(shape);
|
||||
}
|
||||
}
|
||||
|
||||
self.paint_list().extend(self.clip_rect, shapes);
|
||||
}
|
||||
if self.fade_to_color.is_some() {
|
||||
let shapes = shapes.into_iter().map(|mut shape| {
|
||||
self.transform_shape(&mut shape);
|
||||
shape
|
||||
});
|
||||
self.paint_list(|l| l.extend(self.clip_rect, shapes));
|
||||
} else {
|
||||
self.paint_list(|l| l.extend(self.clip_rect, shapes));
|
||||
};
|
||||
}
|
||||
|
||||
/// Modify an existing [`Shape`].
|
||||
|
@ -200,7 +202,7 @@ impl Painter {
|
|||
}
|
||||
let mut shape = shape.into();
|
||||
self.transform_shape(&mut shape);
|
||||
self.paint_list().set(idx, self.clip_rect, shape);
|
||||
self.paint_list(|l| l.set(idx, self.clip_rect, shape));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -208,18 +210,24 @@ impl Painter {
|
|||
impl Painter {
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn debug_rect(&self, rect: Rect, color: Color32, text: impl ToString) {
|
||||
self.rect_stroke(rect, 0.0, (1.0, color));
|
||||
self.rect(
|
||||
rect,
|
||||
0.0,
|
||||
color.additive().linear_multiply(0.015),
|
||||
(1.0, color),
|
||||
);
|
||||
self.text(
|
||||
rect.min,
|
||||
Align2::LEFT_TOP,
|
||||
text.to_string(),
|
||||
FontId::monospace(14.0),
|
||||
FontId::monospace(12.0),
|
||||
color,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn error(&self, pos: Pos2, text: impl std::fmt::Display) -> Rect {
|
||||
self.debug_text(pos, Align2::LEFT_TOP, Color32::RED, format!("🔥 {}", text))
|
||||
let color = self.ctx.style().visuals.error_fg_color;
|
||||
self.debug_text(pos, Align2::LEFT_TOP, color, format!("🔥 {}", text))
|
||||
}
|
||||
|
||||
/// text with a background
|
||||
|
@ -231,13 +239,13 @@ impl Painter {
|
|||
color: Color32,
|
||||
text: impl ToString,
|
||||
) -> Rect {
|
||||
let galley = self.layout_no_wrap(text.to_string(), FontId::monospace(14.0), color);
|
||||
let galley = self.layout_no_wrap(text.to_string(), FontId::monospace(12.0), color);
|
||||
let rect = anchor.anchor_rect(Rect::from_min_size(pos, galley.size()));
|
||||
let frame_rect = rect.expand(2.0);
|
||||
self.add(Shape::rect_filled(
|
||||
frame_rect,
|
||||
0.0,
|
||||
Color32::from_black_alpha(240),
|
||||
Color32::from_black_alpha(150),
|
||||
));
|
||||
self.galley(rect.min, galley);
|
||||
frame_rect
|
||||
|
@ -256,18 +264,12 @@ impl Painter {
|
|||
|
||||
/// Paints a horizontal line.
|
||||
pub fn hline(&self, x: RangeInclusive<f32>, y: f32, stroke: impl Into<Stroke>) {
|
||||
self.add(Shape::LineSegment {
|
||||
points: [pos2(*x.start(), y), pos2(*x.end(), y)],
|
||||
stroke: stroke.into(),
|
||||
});
|
||||
self.add(Shape::hline(x, y, stroke));
|
||||
}
|
||||
|
||||
/// Paints a vertical line.
|
||||
pub fn vline(&self, x: f32, y: RangeInclusive<f32>, stroke: impl Into<Stroke>) {
|
||||
self.add(Shape::LineSegment {
|
||||
points: [pos2(x, *y.start()), pos2(x, *y.end())],
|
||||
stroke: stroke.into(),
|
||||
});
|
||||
self.add(Shape::vline(x, y, stroke));
|
||||
}
|
||||
|
||||
pub fn circle(
|
||||
|
@ -357,6 +359,16 @@ impl Painter {
|
|||
self.line_segment([tip, tip - tip_length * (rot * dir)], stroke);
|
||||
self.line_segment([tip, tip - tip_length * (rot.inverse() * dir)], stroke);
|
||||
}
|
||||
|
||||
/// An image at the given position.
|
||||
///
|
||||
/// `uv` should normally be `Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0))`
|
||||
/// unless you want to crop or flip the image.
|
||||
///
|
||||
/// `tint` is a color multiplier. Use [`Color32::WHITE`] if you don't want to tint the image.
|
||||
pub fn image(&self, texture_id: epaint::TextureId, rect: Rect, uv: Rect, tint: Color32) {
|
||||
self.add(Shape::image(texture_id, rect, uv, tint));
|
||||
}
|
||||
}
|
||||
|
||||
/// ## Text
|
||||
|
@ -395,7 +407,7 @@ impl Painter {
|
|||
color: crate::Color32,
|
||||
wrap_width: f32,
|
||||
) -> Arc<Galley> {
|
||||
self.fonts().layout(text, font_id, color, wrap_width)
|
||||
self.fonts(|f| f.layout(text, font_id, color, wrap_width))
|
||||
}
|
||||
|
||||
/// Will line break at `\n`.
|
||||
|
@ -408,7 +420,7 @@ impl Painter {
|
|||
font_id: FontId,
|
||||
color: crate::Color32,
|
||||
) -> Arc<Galley> {
|
||||
self.fonts().layout(text, font_id, color, f32::INFINITY)
|
||||
self.fonts(|f| f.layout(text, font_id, color, f32::INFINITY))
|
||||
}
|
||||
|
||||
/// Paint text that has already been layed out in a [`Galley`].
|
||||
|
@ -438,6 +450,6 @@ impl Painter {
|
|||
|
||||
fn tint_shape_towards(shape: &mut Shape, target: Color32) {
|
||||
epaint::shape_transform::adjust_colors(shape, &|color| {
|
||||
*color = crate::color::tint_color_towards(*color, target);
|
||||
*color = crate::ecolor::tint_color_towards(*color, target);
|
||||
});
|
||||
}
|
|
@ -106,6 +106,7 @@ impl Placer {
|
|||
/// This is what you then pass to `advance_after_rects`.
|
||||
/// Use `justify_and_align` to get the inner `widget_rect`.
|
||||
pub(crate) fn next_space(&self, child_size: Vec2, item_spacing: Vec2) -> Rect {
|
||||
egui_assert!(child_size.is_finite() && child_size.x >= 0.0 && child_size.y >= 0.0);
|
||||
self.region.sanity_check();
|
||||
if let Some(grid) = &self.grid {
|
||||
grid.next_cell(self.region.cursor, child_size)
|
|
@ -13,6 +13,7 @@ use crate::{
|
|||
///
|
||||
/// Whenever something gets added to a [`Ui`], a [`Response`] object is returned.
|
||||
/// [`ui.add`] returns a [`Response`], as does [`ui.button`], and all similar shortcuts.
|
||||
// TODO(emilk): we should be using bit sets instead of so many bools
|
||||
#[derive(Clone)]
|
||||
pub struct Response {
|
||||
// CONTEXT:
|
||||
|
@ -42,17 +43,21 @@ pub struct Response {
|
|||
#[doc(hidden)]
|
||||
pub hovered: bool,
|
||||
|
||||
/// The widget is highlighted via a call to [`Self::highlight`] or [`Context::highlight_widget`].
|
||||
#[doc(hidden)]
|
||||
pub highlighted: bool,
|
||||
|
||||
/// The pointer clicked this thing this frame.
|
||||
#[doc(hidden)]
|
||||
pub clicked: [bool; NUM_POINTER_BUTTONS],
|
||||
|
||||
// TODO: `released` for sliders
|
||||
// TODO(emilk): `released` for sliders
|
||||
/// The thing was double-clicked.
|
||||
#[doc(hidden)]
|
||||
pub double_clicked: [bool; NUM_POINTER_BUTTONS],
|
||||
|
||||
/// The thing was triple-clicked.
|
||||
pub(crate) triple_clicked: [bool; NUM_POINTER_BUTTONS],
|
||||
pub triple_clicked: [bool; NUM_POINTER_BUTTONS],
|
||||
|
||||
/// The widgets is being dragged
|
||||
#[doc(hidden)]
|
||||
|
@ -90,6 +95,7 @@ impl std::fmt::Debug for Response {
|
|||
sense,
|
||||
enabled,
|
||||
hovered,
|
||||
highlighted,
|
||||
clicked,
|
||||
double_clicked,
|
||||
triple_clicked,
|
||||
|
@ -106,6 +112,7 @@ impl std::fmt::Debug for Response {
|
|||
.field("sense", sense)
|
||||
.field("enabled", enabled)
|
||||
.field("hovered", hovered)
|
||||
.field("highlighted", highlighted)
|
||||
.field("clicked", clicked)
|
||||
.field("double_clicked", double_clicked)
|
||||
.field("triple_clicked", triple_clicked)
|
||||
|
@ -173,23 +180,25 @@ impl Response {
|
|||
// We do not use self.clicked(), because we want to catch all clicks within our frame,
|
||||
// even if we aren't clickable (or even enabled).
|
||||
// This is important for windows and such that should close then the user clicks elsewhere.
|
||||
let pointer = &self.ctx.input().pointer;
|
||||
self.ctx.input(|i| {
|
||||
let pointer = &i.pointer;
|
||||
|
||||
if pointer.any_click() {
|
||||
// We detect clicks/hover on a "interact_rect" that is slightly larger than
|
||||
// self.rect. See Context::interact.
|
||||
// This means we can be hovered and clicked even though `!self.rect.contains(pos)` is true,
|
||||
// hence the extra complexity here.
|
||||
if self.hovered() {
|
||||
false
|
||||
} else if let Some(pos) = pointer.interact_pos() {
|
||||
!self.rect.contains(pos)
|
||||
if pointer.any_click() {
|
||||
// We detect clicks/hover on a "interact_rect" that is slightly larger than
|
||||
// self.rect. See Context::interact.
|
||||
// This means we can be hovered and clicked even though `!self.rect.contains(pos)` is true,
|
||||
// hence the extra complexity here.
|
||||
if self.hovered() {
|
||||
false
|
||||
} else if let Some(pos) = pointer.interact_pos() {
|
||||
!self.rect.contains(pos)
|
||||
} else {
|
||||
false // clicked without a pointer, weird
|
||||
}
|
||||
} else {
|
||||
false // clicked without a pointer, weird
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Was the widget enabled?
|
||||
|
@ -211,14 +220,24 @@ impl Response {
|
|||
self.hovered
|
||||
}
|
||||
|
||||
/// The widget is highlighted via a call to [`Self::highlight`] or [`Context::highlight_widget`].
|
||||
#[doc(hidden)]
|
||||
pub fn highlighted(&self) -> bool {
|
||||
self.highlighted
|
||||
}
|
||||
|
||||
/// This widget has the keyboard focus (i.e. is receiving key presses).
|
||||
///
|
||||
/// This function only returns true if the UI as a whole (e.g. window)
|
||||
/// also has the keyboard focus. That makes this function suitable
|
||||
/// for style choices, e.g. a thicker border around focused widgets.
|
||||
pub fn has_focus(&self) -> bool {
|
||||
self.ctx.memory().has_focus(self.id)
|
||||
self.ctx.input(|i| i.raw.has_focus) && self.ctx.memory(|mem| mem.has_focus(self.id))
|
||||
}
|
||||
|
||||
/// True if this widget has keyboard focus this frame, but didn't last frame.
|
||||
pub fn gained_focus(&self) -> bool {
|
||||
self.ctx.memory().gained_focus(self.id)
|
||||
self.ctx.memory(|mem| mem.gained_focus(self.id))
|
||||
}
|
||||
|
||||
/// The widget had keyboard focus and lost it,
|
||||
|
@ -230,29 +249,29 @@ impl Response {
|
|||
/// # let mut my_text = String::new();
|
||||
/// # fn do_request(_: &str) {}
|
||||
/// let response = ui.text_edit_singleline(&mut my_text);
|
||||
/// if response.lost_focus() && ui.input().key_pressed(egui::Key::Enter) {
|
||||
/// if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
||||
/// do_request(&my_text);
|
||||
/// }
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn lost_focus(&self) -> bool {
|
||||
self.ctx.memory().lost_focus(self.id)
|
||||
self.ctx.memory(|mem| mem.lost_focus(self.id))
|
||||
}
|
||||
|
||||
/// Request that this widget get keyboard focus.
|
||||
pub fn request_focus(&self) {
|
||||
self.ctx.memory().request_focus(self.id);
|
||||
self.ctx.memory_mut(|mem| mem.request_focus(self.id));
|
||||
}
|
||||
|
||||
/// Surrender keyboard focus for this widget.
|
||||
pub fn surrender_focus(&self) {
|
||||
self.ctx.memory().surrender_focus(self.id);
|
||||
self.ctx.memory_mut(|mem| mem.surrender_focus(self.id));
|
||||
}
|
||||
|
||||
/// The widgets is being dragged.
|
||||
///
|
||||
/// To find out which button(s), query [`crate::PointerState::button_down`]
|
||||
/// (`ui.input().pointer.button_down(…)`).
|
||||
/// (`ui.input(|i| i.pointer.button_down(…))`).
|
||||
///
|
||||
/// Note that the widget must be sensing drags with [`Sense::drag`].
|
||||
/// [`crate::DragValue`] senses drags; [`crate::Label`] does not (unless you call [`crate::Label::sense`]).
|
||||
|
@ -264,12 +283,17 @@ impl Response {
|
|||
}
|
||||
|
||||
pub fn dragged_by(&self, button: PointerButton) -> bool {
|
||||
self.dragged() && self.ctx.input().pointer.button_down(button)
|
||||
self.dragged() && self.ctx.input(|i| i.pointer.button_down(button))
|
||||
}
|
||||
|
||||
/// Did a drag on this widgets begin this frame?
|
||||
pub fn drag_started(&self) -> bool {
|
||||
self.dragged && self.ctx.input().pointer.any_pressed()
|
||||
self.dragged && self.ctx.input(|i| i.pointer.any_pressed())
|
||||
}
|
||||
|
||||
/// Did a drag on this widgets by the button begin this frame?
|
||||
pub fn drag_started_by(&self, button: PointerButton) -> bool {
|
||||
self.drag_started() && self.ctx.input(|i| i.pointer.button_pressed(button))
|
||||
}
|
||||
|
||||
/// The widget was being dragged, but now it has been released.
|
||||
|
@ -277,10 +301,15 @@ impl Response {
|
|||
self.drag_released
|
||||
}
|
||||
|
||||
/// The widget was being dragged by the button, but now it has been released.
|
||||
pub fn drag_released_by(&self, button: PointerButton) -> bool {
|
||||
self.drag_released() && self.ctx.input(|i| i.pointer.button_released(button))
|
||||
}
|
||||
|
||||
/// If dragged, how many points were we dragged and in what direction?
|
||||
pub fn drag_delta(&self) -> Vec2 {
|
||||
if self.dragged() {
|
||||
self.ctx.input().pointer.delta()
|
||||
self.ctx.input(|i| i.pointer.delta())
|
||||
} else {
|
||||
Vec2::ZERO
|
||||
}
|
||||
|
@ -296,7 +325,7 @@ impl Response {
|
|||
/// None if the pointer is outside the response area.
|
||||
pub fn hover_pos(&self) -> Option<Pos2> {
|
||||
if self.hovered() {
|
||||
self.ctx.input().pointer.hover_pos()
|
||||
self.ctx.input(|i| i.pointer.hover_pos())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
@ -380,27 +409,36 @@ impl Response {
|
|||
self
|
||||
}
|
||||
|
||||
/// Was the tooltip open last frame?
|
||||
pub fn is_tooltip_open(&self) -> bool {
|
||||
crate::popup::was_tooltip_open_last_frame(&self.ctx, self.id.with("__tooltip"))
|
||||
}
|
||||
|
||||
fn should_show_hover_ui(&self) -> bool {
|
||||
if self.ctx.memory().everything_is_visible() {
|
||||
if self.ctx.memory(|mem| mem.everything_is_visible()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if !self.hovered || !self.ctx.input().pointer.has_pointer() {
|
||||
if !self.hovered || !self.ctx.input(|i| i.pointer.has_pointer()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.ctx.style().interaction.show_tooltips_only_when_still
|
||||
&& !self.ctx.input().pointer.is_still()
|
||||
{
|
||||
// wait for mouse to stop
|
||||
self.ctx.request_repaint();
|
||||
return false;
|
||||
if self.ctx.style().interaction.show_tooltips_only_when_still {
|
||||
// We only show the tooltip when the mouse pointer is still,
|
||||
// but once shown we keep showing it until the mouse leaves the parent.
|
||||
|
||||
if !self.ctx.input(|i| i.pointer.is_still()) && !self.is_tooltip_open() {
|
||||
// wait for mouse to stop
|
||||
self.ctx.request_repaint();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// We don't want tooltips of things while we are dragging them,
|
||||
// but we do want tooltips while holding down on an item on a touch screen.
|
||||
if self.ctx.input().pointer.any_down()
|
||||
&& self.ctx.input().pointer.has_moved_too_much_for_a_click
|
||||
if self
|
||||
.ctx
|
||||
.input(|i| i.pointer.any_down() && i.pointer.has_moved_too_much_for_a_click)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
@ -429,6 +467,17 @@ impl Response {
|
|||
})
|
||||
}
|
||||
|
||||
/// Highlight this widget, to make it look like it is hovered, even if it isn't.
|
||||
///
|
||||
/// The highlight takes on frame to take effect if you call this after the widget has been fully rendered.
|
||||
///
|
||||
/// See also [`Context::highlight_widget`].
|
||||
pub fn highlight(mut self) -> Self {
|
||||
self.ctx.highlight_widget(self.id);
|
||||
self.highlighted = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Show this text when hovering if the widget is disabled.
|
||||
pub fn on_disabled_hover_text(self, text: impl Into<WidgetText>) -> Self {
|
||||
self.on_disabled_hover_ui(|ui| {
|
||||
|
@ -439,7 +488,15 @@ impl Response {
|
|||
/// When hovered, use this icon for the mouse cursor.
|
||||
pub fn on_hover_cursor(self, cursor: CursorIcon) -> Self {
|
||||
if self.hovered() {
|
||||
self.ctx.output().cursor_icon = cursor;
|
||||
self.ctx.set_cursor_icon(cursor);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// When hovered or dragged, use this icon for the mouse cursor.
|
||||
pub fn on_hover_and_drag_cursor(self, cursor: CursorIcon) -> Self {
|
||||
if self.hovered() || self.dragged() {
|
||||
self.ctx.set_cursor_icon(cursor);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
@ -457,6 +514,7 @@ impl Response {
|
|||
/// if response.clicked() { /* … */ }
|
||||
/// # });
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn interact(&self, sense: Sense) -> Self {
|
||||
self.ctx.interact_with_hovered(
|
||||
self.layer_id,
|
||||
|
@ -487,8 +545,10 @@ impl Response {
|
|||
/// # });
|
||||
/// ```
|
||||
pub fn scroll_to_me(&self, align: Option<Align>) {
|
||||
self.ctx.frame_state().scroll_target[0] = Some((self.rect.x_range(), align));
|
||||
self.ctx.frame_state().scroll_target[1] = Some((self.rect.y_range(), align));
|
||||
self.ctx.frame_state_mut(|state| {
|
||||
state.scroll_target[0] = Some((self.rect.x_range(), align));
|
||||
state.scroll_target[1] = Some((self.rect.y_range(), align));
|
||||
});
|
||||
}
|
||||
|
||||
/// For accessibility.
|
||||
|
@ -510,15 +570,115 @@ impl Response {
|
|||
None
|
||||
};
|
||||
if let Some(event) = event {
|
||||
self.ctx.output().events.push(event);
|
||||
self.output_event(event);
|
||||
} else {
|
||||
#[cfg(feature = "accesskit")]
|
||||
self.ctx.accesskit_node_builder(self.id, |builder| {
|
||||
self.fill_accesskit_node_from_widget_info(builder, make_info());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn output_event(&self, event: crate::output::OutputEvent) {
|
||||
#[cfg(feature = "accesskit")]
|
||||
self.ctx.accesskit_node_builder(self.id, |builder| {
|
||||
self.fill_accesskit_node_from_widget_info(builder, event.widget_info().clone());
|
||||
});
|
||||
self.ctx.output_mut(|o| o.events.push(event));
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub(crate) fn fill_accesskit_node_common(&self, builder: &mut accesskit::NodeBuilder) {
|
||||
builder.set_bounds(accesskit::Rect {
|
||||
x0: self.rect.min.x.into(),
|
||||
y0: self.rect.min.y.into(),
|
||||
x1: self.rect.max.x.into(),
|
||||
y1: self.rect.max.y.into(),
|
||||
});
|
||||
if self.sense.focusable {
|
||||
builder.add_action(accesskit::Action::Focus);
|
||||
}
|
||||
if self.sense.click && builder.default_action_verb().is_none() {
|
||||
builder.set_default_action_verb(accesskit::DefaultActionVerb::Click);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
fn fill_accesskit_node_from_widget_info(
|
||||
&self,
|
||||
builder: &mut accesskit::NodeBuilder,
|
||||
info: crate::WidgetInfo,
|
||||
) {
|
||||
use crate::WidgetType;
|
||||
use accesskit::{CheckedState, Role};
|
||||
|
||||
self.fill_accesskit_node_common(builder);
|
||||
builder.set_role(match info.typ {
|
||||
WidgetType::Label => Role::StaticText,
|
||||
WidgetType::Link => Role::Link,
|
||||
WidgetType::TextEdit => Role::TextField,
|
||||
WidgetType::Button | WidgetType::ImageButton | WidgetType::CollapsingHeader => {
|
||||
Role::Button
|
||||
}
|
||||
WidgetType::Checkbox => Role::CheckBox,
|
||||
WidgetType::RadioButton => Role::RadioButton,
|
||||
WidgetType::SelectableLabel => Role::ToggleButton,
|
||||
WidgetType::ComboBox => Role::PopupButton,
|
||||
WidgetType::Slider => Role::Slider,
|
||||
WidgetType::DragValue => Role::SpinButton,
|
||||
WidgetType::ColorButton => Role::ColorWell,
|
||||
WidgetType::Other => Role::Unknown,
|
||||
});
|
||||
if let Some(label) = info.label {
|
||||
builder.set_name(label);
|
||||
}
|
||||
if let Some(value) = info.current_text_value {
|
||||
builder.set_value(value);
|
||||
}
|
||||
if let Some(value) = info.value {
|
||||
builder.set_numeric_value(value);
|
||||
}
|
||||
if let Some(selected) = info.selected {
|
||||
builder.set_checked_state(if selected {
|
||||
CheckedState::True
|
||||
} else {
|
||||
CheckedState::False
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Associate a label with a control for accessibility.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// # let mut text = "Arthur".to_string();
|
||||
/// ui.horizontal(|ui| {
|
||||
/// let label = ui.label("Your name: ");
|
||||
/// ui.text_edit_singleline(&mut text).labelled_by(label.id);
|
||||
/// });
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn labelled_by(self, id: Id) -> Self {
|
||||
#[cfg(feature = "accesskit")]
|
||||
self.ctx.accesskit_node_builder(self.id, |builder| {
|
||||
builder.push_labelled_by(id.accesskit_id());
|
||||
});
|
||||
#[cfg(not(feature = "accesskit"))]
|
||||
{
|
||||
let _ = id;
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Response to secondary clicks (right-clicks) by showing the given menu.
|
||||
///
|
||||
/// ```
|
||||
/// # use egui::{Label, Sense};
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// let response = ui.label("Right-click me!");
|
||||
/// let response = ui.add(Label::new("Right-click me!").sense(Sense::click()));
|
||||
/// response.context_menu(|ui| {
|
||||
/// if ui.button("Close the menu").clicked() {
|
||||
/// ui.close_menu();
|
||||
|
@ -553,20 +713,27 @@ impl Response {
|
|||
sense: self.sense.union(other.sense),
|
||||
enabled: self.enabled || other.enabled,
|
||||
hovered: self.hovered || other.hovered,
|
||||
highlighted: self.highlighted || other.highlighted,
|
||||
clicked: [
|
||||
self.clicked[0] || other.clicked[0],
|
||||
self.clicked[1] || other.clicked[1],
|
||||
self.clicked[2] || other.clicked[2],
|
||||
self.clicked[3] || other.clicked[3],
|
||||
self.clicked[4] || other.clicked[4],
|
||||
],
|
||||
double_clicked: [
|
||||
self.double_clicked[0] || other.double_clicked[0],
|
||||
self.double_clicked[1] || other.double_clicked[1],
|
||||
self.double_clicked[2] || other.double_clicked[2],
|
||||
self.double_clicked[3] || other.double_clicked[3],
|
||||
self.double_clicked[4] || other.double_clicked[4],
|
||||
],
|
||||
triple_clicked: [
|
||||
self.triple_clicked[0] || other.triple_clicked[0],
|
||||
self.triple_clicked[1] || other.triple_clicked[1],
|
||||
self.triple_clicked[2] || other.triple_clicked[2],
|
||||
self.triple_clicked[3] || other.triple_clicked[3],
|
||||
self.triple_clicked[4] || other.triple_clicked[4],
|
||||
],
|
||||
dragged: self.dragged || other.dragged,
|
||||
drag_released: self.drag_released || other.drag_released,
|
||||
|
@ -578,6 +745,13 @@ impl Response {
|
|||
}
|
||||
}
|
||||
|
||||
impl Response {
|
||||
/// Returns a response with a modified [`Self::rect`].
|
||||
pub fn with_new_rect(self, rect: Rect) -> Self {
|
||||
Self { rect, ..self }
|
||||
}
|
||||
}
|
||||
|
||||
/// To summarize the response from many widgets you can use this pattern:
|
||||
///
|
||||
/// ```
|
||||
|
@ -590,6 +764,7 @@ impl Response {
|
|||
/// Now `draw_vec2(ui, foo).hovered` is true if either [`DragValue`](crate::DragValue) were hovered.
|
||||
impl std::ops::BitOr for Response {
|
||||
type Output = Self;
|
||||
|
||||
fn bitor(self, rhs: Self) -> Self {
|
||||
self.union(rhs)
|
||||
}
|
||||
|
@ -631,6 +806,7 @@ impl std::ops::BitOrAssign for Response {
|
|||
pub struct InnerResponse<R> {
|
||||
/// What the user closure returned.
|
||||
pub inner: R,
|
||||
|
||||
/// The response of the area.
|
||||
pub response: Response,
|
||||
}
|
|
@ -16,6 +16,7 @@ pub struct Sense {
|
|||
|
||||
impl Sense {
|
||||
/// Senses no clicks or drags. Only senses mouse hover.
|
||||
#[doc(alias = "none")]
|
||||
pub fn hover() -> Self {
|
||||
Self {
|
||||
click: false,
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
#![allow(clippy::if_same_then_else)]
|
||||
|
||||
use crate::{color::*, emath::*, FontFamily, FontId, Response, RichText, WidgetText};
|
||||
use crate::{ecolor::*, emath::*, FontFamily, FontId, Response, RichText, WidgetText};
|
||||
use epaint::{Rounding, Shadow, Stroke};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
|
@ -21,14 +21,15 @@ pub enum TextStyle {
|
|||
/// Normal labels. Easily readable, doesn't take up too much space.
|
||||
Body,
|
||||
|
||||
/// Same size as [`Self::Body]`, but used when monospace is important (for aligning number, code snippets, etc).
|
||||
/// Same size as [`Self::Body`], but used when monospace is important (for code snippets, aligning numbers, etc).
|
||||
Monospace,
|
||||
|
||||
/// Buttons. Maybe slightly bigger than [`Self::Body]`.
|
||||
/// Signifies that he item is interactive.
|
||||
/// Buttons. Maybe slightly bigger than [`Self::Body`].
|
||||
///
|
||||
/// Signifies that he item can be interacted with.
|
||||
Button,
|
||||
|
||||
/// Heading. Probably larger than [`Self::Body]`.
|
||||
/// Heading. Probably larger than [`Self::Body`].
|
||||
Heading,
|
||||
|
||||
/// A user-chosen style, found in [`Style::text_styles`].
|
||||
|
@ -173,6 +174,9 @@ pub struct Style {
|
|||
/// ```
|
||||
pub text_styles: BTreeMap<TextStyle, FontId>,
|
||||
|
||||
/// The style to use for [`DragValue`] text.
|
||||
pub drag_value_text_style: TextStyle,
|
||||
|
||||
/// If set, labels buttons wtc will use this to determine whether or not
|
||||
/// to wrap the text at the right edge of the [`Ui`] they are in.
|
||||
/// By default this is `None`.
|
||||
|
@ -204,7 +208,7 @@ pub struct Style {
|
|||
}
|
||||
|
||||
impl Style {
|
||||
// TODO: rename style.interact() to maybe... `style.interactive` ?
|
||||
// TODO(emilk): rename style.interact() to maybe... `style.interactive` ?
|
||||
/// Use this style for interactive things.
|
||||
/// Note that you must already have a response,
|
||||
/// i.e. you must allocate space and interact BEFORE painting the widget!
|
||||
|
@ -215,6 +219,7 @@ impl Style {
|
|||
pub fn interact_selectable(&self, response: &Response, selected: bool) -> WidgetVisuals {
|
||||
let mut visuals = *self.visuals.widgets.style(response);
|
||||
if selected {
|
||||
visuals.weak_bg_fill = self.visuals.selection.bg_fill;
|
||||
visuals.bg_fill = self.visuals.selection.bg_fill;
|
||||
// visuals.bg_stroke = self.visuals.selection.stroke;
|
||||
visuals.fg_stroke = self.visuals.selection.stroke;
|
||||
|
@ -252,16 +257,22 @@ pub struct Spacing {
|
|||
/// Button size is text size plus this on each side
|
||||
pub button_padding: Vec2,
|
||||
|
||||
/// Horizontal and vertical margins within a menu frame.
|
||||
pub menu_margin: Margin,
|
||||
|
||||
/// Indent collapsing regions etc by this much.
|
||||
pub indent: f32,
|
||||
|
||||
/// Minimum size of a [`DragValue`], color picker button, and other small widgets.
|
||||
/// `interact_size.y` is the default height of button, slider, etc.
|
||||
/// Anything clickable should be (at least) this size.
|
||||
pub interact_size: Vec2, // TODO: rename min_interact_size ?
|
||||
pub interact_size: Vec2, // TODO(emilk): rename min_interact_size ?
|
||||
|
||||
/// Default width of a [`Slider`] and [`ComboBox`](crate::ComboBox).
|
||||
pub slider_width: f32, // TODO: rename big_interact_size ?
|
||||
/// Default width of a [`Slider`].
|
||||
pub slider_width: f32,
|
||||
|
||||
/// Default (minimum) width of a [`ComboBox`](crate::ComboBox).
|
||||
pub combo_width: f32,
|
||||
|
||||
/// Default width of a [`TextEdit`].
|
||||
pub text_edit_width: f32,
|
||||
|
@ -288,6 +299,15 @@ pub struct Spacing {
|
|||
pub combo_height: f32,
|
||||
|
||||
pub scroll_bar_width: f32,
|
||||
|
||||
/// Make sure the scroll handle is at least this big
|
||||
pub scroll_handle_min_length: f32,
|
||||
|
||||
/// Margin between contents and scroll bar.
|
||||
pub scroll_bar_inner_margin: f32,
|
||||
|
||||
/// Margin between scroll bar and the outer container (e.g. right of a vertical scroll bar).
|
||||
pub scroll_bar_outer_margin: f32,
|
||||
}
|
||||
|
||||
impl Spacing {
|
||||
|
@ -341,15 +361,19 @@ impl Margin {
|
|||
|
||||
/// Total margins on both sides
|
||||
pub fn sum(&self) -> Vec2 {
|
||||
Vec2::new(self.left + self.right, self.top + self.bottom)
|
||||
vec2(self.left + self.right, self.top + self.bottom)
|
||||
}
|
||||
|
||||
pub fn left_top(&self) -> Vec2 {
|
||||
Vec2::new(self.left, self.top)
|
||||
vec2(self.left, self.top)
|
||||
}
|
||||
|
||||
pub fn right_bottom(&self) -> Vec2 {
|
||||
Vec2::new(self.right, self.bottom)
|
||||
vec2(self.right, self.bottom)
|
||||
}
|
||||
|
||||
pub fn is_same(&self) -> bool {
|
||||
self.left == self.right && self.left == self.top && self.left == self.bottom
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -367,6 +391,7 @@ impl From<Vec2> for Margin {
|
|||
|
||||
impl std::ops::Add for Margin {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, other: Self) -> Self {
|
||||
Self {
|
||||
left: self.left + other.left,
|
||||
|
@ -446,14 +471,28 @@ pub struct Visuals {
|
|||
/// Background color behind code-styled monospaced labels.
|
||||
pub code_bg_color: Color32,
|
||||
|
||||
/// A good color for warning text (e.g. orange).
|
||||
pub warn_fg_color: Color32,
|
||||
|
||||
/// A good color for error text (e.g. red).
|
||||
pub error_fg_color: Color32,
|
||||
|
||||
pub window_rounding: Rounding,
|
||||
pub window_shadow: Shadow,
|
||||
pub window_fill: Color32,
|
||||
pub window_stroke: Stroke,
|
||||
|
||||
pub menu_rounding: Rounding,
|
||||
|
||||
/// Panel background color
|
||||
pub panel_fill: Color32,
|
||||
|
||||
pub popup_shadow: Shadow,
|
||||
|
||||
pub resize_corner_size: f32,
|
||||
|
||||
pub text_cursor_width: f32,
|
||||
|
||||
/// show where the text cursor would be if you clicked
|
||||
pub text_cursor_preview: bool,
|
||||
|
||||
|
@ -465,6 +504,18 @@ pub struct Visuals {
|
|||
|
||||
/// Show a background behind collapsing headers.
|
||||
pub collapsing_header_frame: bool,
|
||||
|
||||
/// Draw a vertical lien left of indented region, in e.g. [`crate::CollapsingHeader`].
|
||||
pub indent_has_left_vline: bool,
|
||||
|
||||
/// Wether or not Grids and Tables should be striped by default
|
||||
/// (have alternating rows differently colored).
|
||||
pub striped: bool,
|
||||
|
||||
/// Show trailing color behind the circle of a [`Slider`]. Default is OFF.
|
||||
///
|
||||
/// Enabling this will affect ALL sliders, and can be enabled/disabled per slider with [`Slider::trailing_fill`].
|
||||
pub slider_trailing_fill: bool,
|
||||
}
|
||||
|
||||
impl Visuals {
|
||||
|
@ -479,7 +530,7 @@ impl Visuals {
|
|||
}
|
||||
|
||||
pub fn weak_text_color(&self) -> Color32 {
|
||||
crate::color::tint_color_towards(self.text_color(), self.window_fill())
|
||||
self.gray_out(self.text_color())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
|
@ -490,12 +541,25 @@ impl Visuals {
|
|||
/// Window background color.
|
||||
#[inline(always)]
|
||||
pub fn window_fill(&self) -> Color32 {
|
||||
self.widgets.noninteractive.bg_fill
|
||||
self.window_fill
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn window_stroke(&self) -> Stroke {
|
||||
self.widgets.noninteractive.bg_stroke
|
||||
self.window_stroke
|
||||
}
|
||||
|
||||
/// When fading out things, we fade the colors towards this.
|
||||
// TODO(emilk): replace with an alpha
|
||||
#[inline(always)]
|
||||
pub fn fade_out_to_color(&self) -> Color32 {
|
||||
self.widgets.noninteractive.weak_bg_fill
|
||||
}
|
||||
|
||||
/// Returned a "grayed out" version of the given color.
|
||||
#[inline(always)]
|
||||
pub fn gray_out(&self, color: Color32) -> Color32 {
|
||||
crate::ecolor::tint_color_towards(color, self.fade_out_to_color())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -518,12 +582,18 @@ pub struct Widgets {
|
|||
/// * `noninteractive.bg_fill` is the background color of windows.
|
||||
/// * `noninteractive.fg_stroke` is the normal text color.
|
||||
pub noninteractive: WidgetVisuals,
|
||||
|
||||
/// The style of an interactive widget, such as a button, at rest.
|
||||
pub inactive: WidgetVisuals,
|
||||
/// The style of an interactive widget while you hover it.
|
||||
|
||||
/// The style of an interactive widget while you hover it, or when it is highlighted.
|
||||
///
|
||||
/// See [`Response::hovered`], [`Response::highlighted`] and [`Response::highlight`].
|
||||
pub hovered: WidgetVisuals,
|
||||
|
||||
/// The style of an interactive widget as you are clicking or dragging it.
|
||||
pub active: WidgetVisuals,
|
||||
|
||||
/// The style of a button that has an open menu beneath it (e.g. a combo-box)
|
||||
pub open: WidgetVisuals,
|
||||
}
|
||||
|
@ -534,7 +604,7 @@ impl Widgets {
|
|||
&self.noninteractive
|
||||
} else if response.is_pointer_button_down_on() || response.has_focus() {
|
||||
&self.active
|
||||
} else if response.hovered() {
|
||||
} else if response.hovered() || response.highlighted() {
|
||||
&self.hovered
|
||||
} else {
|
||||
&self.inactive
|
||||
|
@ -546,9 +616,17 @@ impl Widgets {
|
|||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct WidgetVisuals {
|
||||
/// Background color of widget.
|
||||
/// Background color of widgets that must have a background fill,
|
||||
/// such as the slider background, a checkbox background, or a radio button background.
|
||||
///
|
||||
/// Must never be [`Color32::TRANSPARENT`].
|
||||
pub bg_fill: Color32,
|
||||
|
||||
/// Background color of widgets that can _optionally_ have a background fill, such as buttons.
|
||||
///
|
||||
/// May be [`Color32::TRANSPARENT`].
|
||||
pub weak_bg_fill: Color32,
|
||||
|
||||
/// For surrounding rectangle of things that need it,
|
||||
/// like buttons, the box of the checkbox, etc.
|
||||
/// Should maybe be called `frame_stroke`.
|
||||
|
@ -572,41 +650,41 @@ impl WidgetVisuals {
|
|||
}
|
||||
|
||||
/// Options for help debug egui by adding extra visualization
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct DebugOptions {
|
||||
/// However over widgets to see their rectangles
|
||||
pub debug_on_hover: bool,
|
||||
|
||||
/// Show which widgets make their parent wider
|
||||
pub show_expand_width: bool,
|
||||
|
||||
/// Show which widgets make their parent higher
|
||||
pub show_expand_height: bool,
|
||||
|
||||
pub show_resize: bool,
|
||||
|
||||
/// Show an overlay on all interactive widgets.
|
||||
pub show_interactive_widgets: bool,
|
||||
|
||||
/// Show what widget blocks the interaction of another widget.
|
||||
pub show_blocking_widget: bool,
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// The default text styles of the default egui theme.
|
||||
pub fn default_text_styles() -> BTreeMap<TextStyle, FontId> {
|
||||
let mut text_styles = BTreeMap::new();
|
||||
text_styles.insert(
|
||||
TextStyle::Small,
|
||||
FontId::new(10.0, FontFamily::Proportional),
|
||||
);
|
||||
text_styles.insert(TextStyle::Body, FontId::new(14.0, FontFamily::Proportional));
|
||||
text_styles.insert(
|
||||
TextStyle::Button,
|
||||
FontId::new(14.0, FontFamily::Proportional),
|
||||
);
|
||||
text_styles.insert(
|
||||
TextStyle::Heading,
|
||||
FontId::new(20.0, FontFamily::Proportional),
|
||||
);
|
||||
text_styles.insert(
|
||||
TextStyle::Monospace,
|
||||
FontId::new(14.0, FontFamily::Monospace),
|
||||
);
|
||||
text_styles
|
||||
use FontFamily::{Monospace, Proportional};
|
||||
|
||||
[
|
||||
(TextStyle::Small, FontId::new(9.0, Proportional)),
|
||||
(TextStyle::Body, FontId::new(12.5, Proportional)),
|
||||
(TextStyle::Button, FontId::new(12.5, Proportional)),
|
||||
(TextStyle::Heading, FontId::new(18.0, Proportional)),
|
||||
(TextStyle::Monospace, FontId::new(12.0, Monospace)),
|
||||
]
|
||||
.into()
|
||||
}
|
||||
|
||||
impl Default for Style {
|
||||
|
@ -615,6 +693,7 @@ impl Default for Style {
|
|||
override_font_id: None,
|
||||
override_text_style: None,
|
||||
text_styles: default_text_styles(),
|
||||
drag_value_text_style: TextStyle::Button,
|
||||
wrap: None,
|
||||
spacing: Spacing::default(),
|
||||
interaction: Interaction::default(),
|
||||
|
@ -631,10 +710,12 @@ impl Default for Spacing {
|
|||
Self {
|
||||
item_spacing: vec2(8.0, 3.0),
|
||||
window_margin: Margin::same(6.0),
|
||||
menu_margin: Margin::same(6.0),
|
||||
button_padding: vec2(4.0, 1.0),
|
||||
indent: 18.0, // match checkbox/radio-button with `button_padding.x + icon_width + icon_spacing`
|
||||
interact_size: vec2(40.0, 18.0),
|
||||
slider_width: 100.0,
|
||||
combo_width: 100.0,
|
||||
text_edit_width: 280.0,
|
||||
icon_width: 14.0,
|
||||
icon_width_inner: 8.0,
|
||||
|
@ -642,6 +723,9 @@ impl Default for Spacing {
|
|||
tooltip_width: 600.0,
|
||||
combo_height: 200.0,
|
||||
scroll_bar_width: 8.0,
|
||||
scroll_handle_min_length: 12.0,
|
||||
scroll_bar_inner_margin: 4.0,
|
||||
scroll_bar_outer_margin: 0.0,
|
||||
indent_ends_with_horizontal_line: false,
|
||||
}
|
||||
}
|
||||
|
@ -652,7 +736,7 @@ impl Default for Interaction {
|
|||
Self {
|
||||
resize_grab_radius_side: 5.0,
|
||||
resize_grab_radius_corner: 10.0,
|
||||
show_tooltips_only_when_still: false,
|
||||
show_tooltips_only_when_still: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -666,11 +750,21 @@ impl Visuals {
|
|||
widgets: Widgets::default(),
|
||||
selection: Selection::default(),
|
||||
hyperlink_color: Color32::from_rgb(90, 170, 255),
|
||||
faint_bg_color: Color32::from_gray(35),
|
||||
extreme_bg_color: Color32::from_gray(10), // e.g. TextEdit background
|
||||
faint_bg_color: Color32::from_additive_luminance(5), // visible, but barely so
|
||||
extreme_bg_color: Color32::from_gray(10), // e.g. TextEdit background
|
||||
code_bg_color: Color32::from_gray(64),
|
||||
warn_fg_color: Color32::from_rgb(255, 143, 0), // orange
|
||||
error_fg_color: Color32::from_rgb(255, 0, 0), // red
|
||||
|
||||
window_rounding: Rounding::same(6.0),
|
||||
window_shadow: Shadow::big_dark(),
|
||||
window_fill: Color32::from_gray(27),
|
||||
window_stroke: Stroke::new(1.0, Color32::from_gray(60)),
|
||||
|
||||
menu_rounding: Rounding::same(6.0),
|
||||
|
||||
panel_fill: Color32::from_gray(27),
|
||||
|
||||
popup_shadow: Shadow::small_dark(),
|
||||
resize_corner_size: 12.0,
|
||||
text_cursor_width: 2.0,
|
||||
|
@ -678,6 +772,11 @@ impl Visuals {
|
|||
clip_rect_margin: 3.0, // should be at least half the size of the widest frame stroke + max WidgetVisuals::expansion
|
||||
button_frame: true,
|
||||
collapsing_header_frame: false,
|
||||
indent_has_left_vline: true,
|
||||
|
||||
striped: false,
|
||||
|
||||
slider_trailing_fill: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -688,10 +787,18 @@ impl Visuals {
|
|||
widgets: Widgets::light(),
|
||||
selection: Selection::light(),
|
||||
hyperlink_color: Color32::from_rgb(0, 155, 255),
|
||||
faint_bg_color: Color32::from_gray(242),
|
||||
extreme_bg_color: Color32::from_gray(255), // e.g. TextEdit background
|
||||
faint_bg_color: Color32::from_additive_luminance(5), // visible, but barely so
|
||||
extreme_bg_color: Color32::from_gray(255), // e.g. TextEdit background
|
||||
code_bg_color: Color32::from_gray(230),
|
||||
warn_fg_color: Color32::from_rgb(255, 100, 0), // slightly orange red. it's difficult to find a warning color that pops on bright background.
|
||||
error_fg_color: Color32::from_rgb(255, 0, 0), // red
|
||||
|
||||
window_shadow: Shadow::big_light(),
|
||||
window_fill: Color32::from_gray(248),
|
||||
window_stroke: Stroke::new(1.0, Color32::from_gray(190)),
|
||||
|
||||
panel_fill: Color32::from_gray(248),
|
||||
|
||||
popup_shadow: Shadow::small_light(),
|
||||
..Self::dark()
|
||||
}
|
||||
|
@ -711,6 +818,7 @@ impl Selection {
|
|||
stroke: Stroke::new(1.0, Color32::from_rgb(192, 222, 255)),
|
||||
}
|
||||
}
|
||||
|
||||
fn light() -> Self {
|
||||
Self {
|
||||
bg_fill: Color32::from_rgb(144, 209, 255),
|
||||
|
@ -729,20 +837,23 @@ impl Widgets {
|
|||
pub fn dark() -> Self {
|
||||
Self {
|
||||
noninteractive: WidgetVisuals {
|
||||
bg_fill: Color32::from_gray(27), // window background
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // separators, indentation lines, windows outlines
|
||||
weak_bg_fill: Color32::from_gray(27),
|
||||
bg_fill: Color32::from_gray(27),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // separators, indentation lines
|
||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(140)), // normal text color
|
||||
rounding: Rounding::same(2.0),
|
||||
expansion: 0.0,
|
||||
},
|
||||
inactive: WidgetVisuals {
|
||||
bg_fill: Color32::from_gray(60), // button background
|
||||
weak_bg_fill: Color32::from_gray(60), // button background
|
||||
bg_fill: Color32::from_gray(60), // checkbox background
|
||||
bg_stroke: Default::default(),
|
||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(180)), // button text
|
||||
rounding: Rounding::same(2.0),
|
||||
expansion: 0.0,
|
||||
},
|
||||
hovered: WidgetVisuals {
|
||||
weak_bg_fill: Color32::from_gray(70),
|
||||
bg_fill: Color32::from_gray(70),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(150)), // e.g. hover over window edge or button
|
||||
fg_stroke: Stroke::new(1.5, Color32::from_gray(240)),
|
||||
|
@ -750,6 +861,7 @@ impl Widgets {
|
|||
expansion: 1.0,
|
||||
},
|
||||
active: WidgetVisuals {
|
||||
weak_bg_fill: Color32::from_gray(55),
|
||||
bg_fill: Color32::from_gray(55),
|
||||
bg_stroke: Stroke::new(1.0, Color32::WHITE),
|
||||
fg_stroke: Stroke::new(2.0, Color32::WHITE),
|
||||
|
@ -757,6 +869,7 @@ impl Widgets {
|
|||
expansion: 1.0,
|
||||
},
|
||||
open: WidgetVisuals {
|
||||
weak_bg_fill: Color32::from_gray(27),
|
||||
bg_fill: Color32::from_gray(27),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(60)),
|
||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(210)),
|
||||
|
@ -769,20 +882,23 @@ impl Widgets {
|
|||
pub fn light() -> Self {
|
||||
Self {
|
||||
noninteractive: WidgetVisuals {
|
||||
bg_fill: Color32::from_gray(248), // window background - should be distinct from TextEdit background
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(190)), // separators, indentation lines, windows outlines
|
||||
weak_bg_fill: Color32::from_gray(248),
|
||||
bg_fill: Color32::from_gray(248),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(190)), // separators, indentation lines
|
||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(80)), // normal text color
|
||||
rounding: Rounding::same(2.0),
|
||||
expansion: 0.0,
|
||||
},
|
||||
inactive: WidgetVisuals {
|
||||
bg_fill: Color32::from_gray(230), // button background
|
||||
weak_bg_fill: Color32::from_gray(230), // button background
|
||||
bg_fill: Color32::from_gray(230), // checkbox background
|
||||
bg_stroke: Default::default(),
|
||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // button text
|
||||
rounding: Rounding::same(2.0),
|
||||
expansion: 0.0,
|
||||
},
|
||||
hovered: WidgetVisuals {
|
||||
weak_bg_fill: Color32::from_gray(220),
|
||||
bg_fill: Color32::from_gray(220),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(105)), // e.g. hover over window edge or button
|
||||
fg_stroke: Stroke::new(1.5, Color32::BLACK),
|
||||
|
@ -790,6 +906,7 @@ impl Widgets {
|
|||
expansion: 1.0,
|
||||
},
|
||||
active: WidgetVisuals {
|
||||
weak_bg_fill: Color32::from_gray(165),
|
||||
bg_fill: Color32::from_gray(165),
|
||||
bg_stroke: Stroke::new(1.0, Color32::BLACK),
|
||||
fg_stroke: Stroke::new(2.0, Color32::BLACK),
|
||||
|
@ -797,6 +914,7 @@ impl Widgets {
|
|||
expansion: 1.0,
|
||||
},
|
||||
open: WidgetVisuals {
|
||||
weak_bg_fill: Color32::from_gray(220),
|
||||
bg_fill: Color32::from_gray(220),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(160)),
|
||||
fg_stroke: Stroke::new(1.0, Color32::BLACK),
|
||||
|
@ -823,6 +941,7 @@ impl Style {
|
|||
override_font_id,
|
||||
override_text_style,
|
||||
text_styles,
|
||||
drag_value_text_style,
|
||||
wrap: _,
|
||||
spacing,
|
||||
interaction,
|
||||
|
@ -864,6 +983,19 @@ impl Style {
|
|||
});
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Text style of DragValue:");
|
||||
crate::ComboBox::from_id_source("drag_value_text_style")
|
||||
.selected_text(drag_value_text_style.to_string())
|
||||
.show_ui(ui, |ui| {
|
||||
let all_text_styles = ui.style().text_styles();
|
||||
for style in all_text_styles {
|
||||
let text =
|
||||
crate::RichText::new(style.to_string()).text_style(style.clone());
|
||||
ui.selectable_value(drag_value_text_style, style, text);
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Animation duration:");
|
||||
ui.add(
|
||||
Slider::new(animation_time, 0.0..=1.0)
|
||||
|
@ -907,10 +1039,12 @@ impl Spacing {
|
|||
let Self {
|
||||
item_spacing,
|
||||
window_margin,
|
||||
menu_margin,
|
||||
button_padding,
|
||||
indent,
|
||||
interact_size,
|
||||
slider_width,
|
||||
combo_width,
|
||||
text_edit_width,
|
||||
icon_width,
|
||||
icon_width_inner,
|
||||
|
@ -919,39 +1053,15 @@ impl Spacing {
|
|||
indent_ends_with_horizontal_line,
|
||||
combo_height,
|
||||
scroll_bar_width,
|
||||
scroll_handle_min_length,
|
||||
scroll_bar_inner_margin,
|
||||
scroll_bar_outer_margin,
|
||||
} = self;
|
||||
|
||||
ui.add(slider_vec2(item_spacing, 0.0..=20.0, "Item spacing"));
|
||||
|
||||
let margin_range = 0.0..=20.0;
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(
|
||||
DragValue::new(&mut window_margin.left)
|
||||
.clamp_range(margin_range.clone())
|
||||
.prefix("left: "),
|
||||
);
|
||||
ui.add(
|
||||
DragValue::new(&mut window_margin.right)
|
||||
.clamp_range(margin_range.clone())
|
||||
.prefix("right: "),
|
||||
);
|
||||
|
||||
ui.label("Window margins x");
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(
|
||||
DragValue::new(&mut window_margin.top)
|
||||
.clamp_range(margin_range.clone())
|
||||
.prefix("top: "),
|
||||
);
|
||||
ui.add(
|
||||
DragValue::new(&mut window_margin.bottom)
|
||||
.clamp_range(margin_range)
|
||||
.prefix("bottom: "),
|
||||
);
|
||||
ui.label("Window margins y");
|
||||
});
|
||||
margin_ui(ui, "Window margin:", window_margin);
|
||||
margin_ui(ui, "Menu margin:", menu_margin);
|
||||
|
||||
ui.add(slider_vec2(button_padding, 0.0..=20.0, "Button padding"));
|
||||
ui.add(slider_vec2(interact_size, 4.0..=60.0, "Interact size"))
|
||||
|
@ -964,13 +1074,29 @@ impl Spacing {
|
|||
ui.add(DragValue::new(slider_width).clamp_range(0.0..=1000.0));
|
||||
ui.label("Slider width");
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(DragValue::new(combo_width).clamp_range(0.0..=1000.0));
|
||||
ui.label("ComboBox width");
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(DragValue::new(text_edit_width).clamp_range(0.0..=1000.0));
|
||||
ui.label("TextEdit width");
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(DragValue::new(scroll_bar_width).clamp_range(0.0..=32.0));
|
||||
ui.label("Scroll-bar width width");
|
||||
ui.label("Scroll-bar width");
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(DragValue::new(scroll_handle_min_length).clamp_range(0.0..=32.0));
|
||||
ui.label("Scroll-bar handle min length");
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(DragValue::new(scroll_bar_inner_margin).clamp_range(0.0..=32.0));
|
||||
ui.label("Scroll-bar inner margin");
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(DragValue::new(scroll_bar_outer_margin).clamp_range(0.0..=32.0));
|
||||
ui.label("Scroll-bar outer margin");
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
|
@ -1011,6 +1137,55 @@ impl Spacing {
|
|||
}
|
||||
}
|
||||
|
||||
fn margin_ui(ui: &mut Ui, text: &str, margin: &mut Margin) {
|
||||
let margin_range = 0.0..=20.0;
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(text);
|
||||
|
||||
let mut same = margin.is_same();
|
||||
ui.checkbox(&mut same, "Same");
|
||||
|
||||
if same {
|
||||
let mut value = margin.left;
|
||||
ui.add(DragValue::new(&mut value).clamp_range(margin_range.clone()));
|
||||
*margin = Margin::same(value);
|
||||
} else {
|
||||
if margin.is_same() {
|
||||
// HACK: prevent collapse:
|
||||
margin.right = margin.left + 1.0;
|
||||
margin.bottom = margin.left + 2.0;
|
||||
margin.top = margin.left + 3.0;
|
||||
}
|
||||
|
||||
ui.add(
|
||||
DragValue::new(&mut margin.left)
|
||||
.clamp_range(margin_range.clone())
|
||||
.prefix("L: "),
|
||||
)
|
||||
.on_hover_text("Left margin");
|
||||
ui.add(
|
||||
DragValue::new(&mut margin.right)
|
||||
.clamp_range(margin_range.clone())
|
||||
.prefix("R: "),
|
||||
)
|
||||
.on_hover_text("Right margin");
|
||||
ui.add(
|
||||
DragValue::new(&mut margin.top)
|
||||
.clamp_range(margin_range.clone())
|
||||
.prefix("T: "),
|
||||
)
|
||||
.on_hover_text("Top margin");
|
||||
ui.add(
|
||||
DragValue::new(&mut margin.bottom)
|
||||
.clamp_range(margin_range)
|
||||
.prefix("B: "),
|
||||
)
|
||||
.on_hover_text("Bottom margin");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
impl Interaction {
|
||||
pub fn ui(&mut self, ui: &mut crate::Ui) {
|
||||
let Self {
|
||||
|
@ -1080,13 +1255,17 @@ impl Selection {
|
|||
impl WidgetVisuals {
|
||||
pub fn ui(&mut self, ui: &mut crate::Ui) {
|
||||
let Self {
|
||||
bg_fill,
|
||||
weak_bg_fill,
|
||||
bg_fill: mandatory_bg_fill,
|
||||
bg_stroke,
|
||||
rounding,
|
||||
fg_stroke,
|
||||
expansion,
|
||||
} = self;
|
||||
ui_color(ui, bg_fill, "background fill");
|
||||
ui_color(ui, weak_bg_fill, "optional background fill")
|
||||
.on_hover_text("For buttons, combo-boxes, etc");
|
||||
ui_color(ui, mandatory_bg_fill, "mandatory background fill")
|
||||
.on_hover_text("For checkboxes, sliders, etc");
|
||||
stroke_ui(ui, bg_stroke, "background stroke");
|
||||
|
||||
rounding_ui(ui, rounding);
|
||||
|
@ -1140,20 +1319,37 @@ impl Visuals {
|
|||
faint_bg_color,
|
||||
extreme_bg_color,
|
||||
code_bg_color,
|
||||
warn_fg_color,
|
||||
error_fg_color,
|
||||
|
||||
window_rounding,
|
||||
window_shadow,
|
||||
window_fill,
|
||||
window_stroke,
|
||||
|
||||
menu_rounding,
|
||||
|
||||
panel_fill,
|
||||
|
||||
popup_shadow,
|
||||
|
||||
resize_corner_size,
|
||||
text_cursor_width,
|
||||
text_cursor_preview,
|
||||
clip_rect_margin,
|
||||
button_frame,
|
||||
collapsing_header_frame,
|
||||
indent_has_left_vline,
|
||||
|
||||
striped,
|
||||
|
||||
slider_trailing_fill,
|
||||
} = self;
|
||||
|
||||
ui.collapsing("Background Colors", |ui| {
|
||||
ui_color(ui, &mut widgets.inactive.bg_fill, "Buttons");
|
||||
ui_color(ui, &mut widgets.noninteractive.bg_fill, "Windows");
|
||||
ui_color(ui, &mut widgets.inactive.weak_bg_fill, "Buttons");
|
||||
ui_color(ui, window_fill, "Windows");
|
||||
ui_color(ui, panel_fill, "Panels");
|
||||
ui_color(ui, faint_bg_color, "Faint accent").on_hover_text(
|
||||
"Used for faint accentuation of interactive things, like striped grids.",
|
||||
);
|
||||
|
@ -1162,24 +1358,30 @@ impl Visuals {
|
|||
});
|
||||
|
||||
ui.collapsing("Window", |ui| {
|
||||
// Common shortcuts
|
||||
ui_color(ui, &mut widgets.noninteractive.bg_fill, "Fill");
|
||||
stroke_ui(ui, &mut widgets.noninteractive.bg_stroke, "Outline");
|
||||
|
||||
ui_color(ui, window_fill, "Fill");
|
||||
stroke_ui(ui, window_stroke, "Outline");
|
||||
rounding_ui(ui, window_rounding);
|
||||
|
||||
shadow_ui(ui, window_shadow, "Shadow");
|
||||
shadow_ui(ui, popup_shadow, "Shadow (small menus and popups)");
|
||||
});
|
||||
|
||||
ui.collapsing("Menus and popups", |ui| {
|
||||
rounding_ui(ui, menu_rounding);
|
||||
shadow_ui(ui, popup_shadow, "Shadow");
|
||||
});
|
||||
|
||||
ui.collapsing("Widgets", |ui| widgets.ui(ui));
|
||||
ui.collapsing("Selection", |ui| selection.ui(ui));
|
||||
|
||||
ui_color(
|
||||
ui,
|
||||
&mut widgets.noninteractive.fg_stroke.color,
|
||||
"Text color",
|
||||
);
|
||||
ui.horizontal(|ui| {
|
||||
ui_color(
|
||||
ui,
|
||||
&mut widgets.noninteractive.fg_stroke.color,
|
||||
"Text color",
|
||||
);
|
||||
ui_color(ui, warn_fg_color, RichText::new("Warnings"));
|
||||
ui_color(ui, error_fg_color, RichText::new("Errors"));
|
||||
});
|
||||
|
||||
ui_color(ui, code_bg_color, RichText::new("Code background").code()).on_hover_ui(|ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
|
@ -1197,6 +1399,14 @@ impl Visuals {
|
|||
|
||||
ui.checkbox(button_frame, "Button has a frame");
|
||||
ui.checkbox(collapsing_header_frame, "Collapsing header has a frame");
|
||||
ui.checkbox(
|
||||
indent_has_left_vline,
|
||||
"Paint a vertical line to the left of indented regions",
|
||||
);
|
||||
|
||||
ui.checkbox(striped, "By default, add stripes to grids and tables?");
|
||||
|
||||
ui.checkbox(slider_trailing_fill, "Add trailing color to sliders");
|
||||
|
||||
ui.vertical_centered(|ui| reset_button(ui, self));
|
||||
}
|
||||
|
@ -1206,27 +1416,39 @@ impl DebugOptions {
|
|||
pub fn ui(&mut self, ui: &mut crate::Ui) {
|
||||
let Self {
|
||||
debug_on_hover,
|
||||
show_expand_width: debug_expand_width,
|
||||
show_expand_height: debug_expand_height,
|
||||
show_resize: debug_resize,
|
||||
show_expand_width,
|
||||
show_expand_height,
|
||||
show_resize,
|
||||
show_interactive_widgets,
|
||||
show_blocking_widget,
|
||||
} = self;
|
||||
|
||||
ui.checkbox(debug_on_hover, "Show debug info on hover");
|
||||
ui.checkbox(
|
||||
debug_expand_width,
|
||||
show_expand_width,
|
||||
"Show which widgets make their parent wider",
|
||||
);
|
||||
ui.checkbox(
|
||||
debug_expand_height,
|
||||
show_expand_height,
|
||||
"Show which widgets make their parent higher",
|
||||
);
|
||||
ui.checkbox(debug_resize, "Debug Resize");
|
||||
ui.checkbox(show_resize, "Debug Resize");
|
||||
|
||||
ui.checkbox(
|
||||
show_interactive_widgets,
|
||||
"Show an overlay on all interactive widgets",
|
||||
);
|
||||
|
||||
ui.checkbox(
|
||||
show_blocking_widget,
|
||||
"Show wha widget blocks the interaction of another widget",
|
||||
);
|
||||
|
||||
ui.vertical_centered(|ui| reset_button(ui, self));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: improve and standardize `slider_vec2`
|
||||
// TODO(emilk): improve and standardize `slider_vec2`
|
||||
fn slider_vec2<'a>(
|
||||
value: &'a mut Vec2,
|
||||
range: std::ops::RangeInclusive<f32>,
|
|
@ -3,11 +3,11 @@
|
|||
use std::hash::Hash;
|
||||
use std::sync::Arc;
|
||||
|
||||
use epaint::mutex::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
use epaint::mutex::RwLock;
|
||||
|
||||
use crate::{
|
||||
color::*, containers::*, epaint::text::Fonts, layout::*, menu::MenuState, placer::Placer,
|
||||
widgets::*, *,
|
||||
containers::*, ecolor::*, epaint::text::Fonts, layout::*, menu::MenuState, placer::Placer,
|
||||
util::IdTypeMap, widgets::*, *,
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
@ -99,7 +99,7 @@ impl Ui {
|
|||
crate::egui_assert!(!max_rect.any_nan());
|
||||
let next_auto_id_source = Id::new(self.next_auto_id_source).with("child").value();
|
||||
self.next_auto_id_source = self.next_auto_id_source.wrapping_add(1);
|
||||
let menu_state = self.get_menu_state();
|
||||
let menu_state = self.menu_state();
|
||||
Ui {
|
||||
id: self.id.with(id_source),
|
||||
next_auto_id_source,
|
||||
|
@ -239,7 +239,7 @@ impl Ui {
|
|||
self.enabled &= enabled;
|
||||
if !self.enabled && self.is_visible() {
|
||||
self.painter
|
||||
.set_fade_to_color(Some(self.visuals().window_fill()));
|
||||
.set_fade_to_color(Some(self.visuals().fade_out_to_color()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -314,77 +314,9 @@ impl Ui {
|
|||
self.painter().layer_id()
|
||||
}
|
||||
|
||||
/// The [`InputState`] of the [`Context`] associated with this [`Ui`].
|
||||
/// Equivalent to `.ctx().input()`.
|
||||
///
|
||||
/// Note that this locks the [`Context`], so be careful with if-let bindings:
|
||||
///
|
||||
/// ```
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// if let Some(pos) = { ui.input().pointer.hover_pos() } {
|
||||
/// // This is fine!
|
||||
/// }
|
||||
///
|
||||
/// let pos = ui.input().pointer.hover_pos();
|
||||
/// if let Some(pos) = pos {
|
||||
/// // This is also fine!
|
||||
/// }
|
||||
///
|
||||
/// if let Some(pos) = ui.input().pointer.hover_pos() {
|
||||
/// // ⚠️ Using `ui` again here will lead to a dead-lock!
|
||||
/// }
|
||||
/// # });
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn input(&self) -> RwLockReadGuard<'_, InputState> {
|
||||
self.ctx().input()
|
||||
}
|
||||
|
||||
/// The [`InputState`] of the [`Context`] associated with this [`Ui`].
|
||||
/// Equivalent to `.ctx().input_mut()`.
|
||||
///
|
||||
/// Note that this locks the [`Context`], so be careful with if-let bindings
|
||||
/// like for [`Self::input()`].
|
||||
/// ```
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// ui.input_mut().consume_key(egui::Modifiers::default(), egui::Key::Enter);
|
||||
/// # });
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn input_mut(&self) -> RwLockWriteGuard<'_, InputState> {
|
||||
self.ctx().input_mut()
|
||||
}
|
||||
|
||||
/// The [`Memory`] of the [`Context`] associated with this ui.
|
||||
/// Equivalent to `.ctx().memory()`.
|
||||
#[inline]
|
||||
pub fn memory(&self) -> RwLockWriteGuard<'_, Memory> {
|
||||
self.ctx().memory()
|
||||
}
|
||||
|
||||
/// Stores superficial widget state.
|
||||
#[inline]
|
||||
pub fn data(&self) -> RwLockWriteGuard<'_, crate::util::IdTypeMap> {
|
||||
self.ctx().data()
|
||||
}
|
||||
|
||||
/// The [`PlatformOutput`] of the [`Context`] associated with this ui.
|
||||
/// Equivalent to `.ctx().output()`.
|
||||
#[inline]
|
||||
pub fn output(&self) -> RwLockWriteGuard<'_, PlatformOutput> {
|
||||
self.ctx().output()
|
||||
}
|
||||
|
||||
/// The [`Fonts`] of the [`Context`] associated with this ui.
|
||||
/// Equivalent to `.ctx().fonts()`.
|
||||
#[inline]
|
||||
pub fn fonts(&self) -> RwLockReadGuard<'_, Fonts> {
|
||||
self.ctx().fonts()
|
||||
}
|
||||
|
||||
/// The height of text of this text style
|
||||
pub fn text_style_height(&self, style: &TextStyle) -> f32 {
|
||||
self.fonts().row_height(&style.resolve(self.style()))
|
||||
self.fonts(|f| f.row_height(&style.resolve(self.style())))
|
||||
}
|
||||
|
||||
/// Screen-space rectangle for clipping what we paint in this ui.
|
||||
|
@ -406,6 +338,87 @@ impl Ui {
|
|||
}
|
||||
}
|
||||
|
||||
/// # Helpers for accessing the underlying [`Context`].
|
||||
/// These functions all lock the [`Context`] owned by this [`Ui`].
|
||||
/// Please see the documentation of [`Context`] for how locking works!
|
||||
impl Ui {
|
||||
/// Read-only access to the shared [`InputState`].
|
||||
///
|
||||
/// ```
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// if ui.input(|i| i.key_pressed(egui::Key::A)) {
|
||||
/// // …
|
||||
/// }
|
||||
/// # });
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn input<R>(&self, reader: impl FnOnce(&InputState) -> R) -> R {
|
||||
self.ctx().input(reader)
|
||||
}
|
||||
|
||||
/// Read-write access to the shared [`InputState`].
|
||||
#[inline]
|
||||
pub fn input_mut<R>(&self, writer: impl FnOnce(&mut InputState) -> R) -> R {
|
||||
self.ctx().input_mut(writer)
|
||||
}
|
||||
|
||||
/// Read-only access to the shared [`Memory`].
|
||||
#[inline]
|
||||
pub fn memory<R>(&self, reader: impl FnOnce(&Memory) -> R) -> R {
|
||||
self.ctx().memory(reader)
|
||||
}
|
||||
|
||||
/// Read-write access to the shared [`Memory`].
|
||||
#[inline]
|
||||
pub fn memory_mut<R>(&self, writer: impl FnOnce(&mut Memory) -> R) -> R {
|
||||
self.ctx().memory_mut(writer)
|
||||
}
|
||||
|
||||
/// Read-only access to the shared [`IdTypeMap`], which stores superficial widget state.
|
||||
#[inline]
|
||||
pub fn data<R>(&self, reader: impl FnOnce(&IdTypeMap) -> R) -> R {
|
||||
self.ctx().data(reader)
|
||||
}
|
||||
|
||||
/// Read-write access to the shared [`IdTypeMap`], which stores superficial widget state.
|
||||
#[inline]
|
||||
pub fn data_mut<R>(&self, writer: impl FnOnce(&mut IdTypeMap) -> R) -> R {
|
||||
self.ctx().data_mut(writer)
|
||||
}
|
||||
|
||||
/// Read-only access to the shared [`PlatformOutput`].
|
||||
///
|
||||
/// This is what egui outputs each frame.
|
||||
///
|
||||
/// ```
|
||||
/// # let mut ctx = egui::Context::default();
|
||||
/// ctx.output_mut(|o| o.cursor_icon = egui::CursorIcon::Progress);
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn output<R>(&self, reader: impl FnOnce(&PlatformOutput) -> R) -> R {
|
||||
self.ctx().output(reader)
|
||||
}
|
||||
|
||||
/// Read-write access to the shared [`PlatformOutput`].
|
||||
///
|
||||
/// This is what egui outputs each frame.
|
||||
///
|
||||
/// ```
|
||||
/// # let mut ctx = egui::Context::default();
|
||||
/// ctx.output_mut(|o| o.cursor_icon = egui::CursorIcon::Progress);
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn output_mut<R>(&self, writer: impl FnOnce(&mut PlatformOutput) -> R) -> R {
|
||||
self.ctx().output_mut(writer)
|
||||
}
|
||||
|
||||
/// Read-only access to [`Fonts`].
|
||||
#[inline]
|
||||
pub fn fonts<R>(&self, reader: impl FnOnce(&Fonts) -> R) -> R {
|
||||
self.ctx().fonts(reader)
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/// # Sizes etc
|
||||
|
@ -574,18 +587,19 @@ impl Ui {
|
|||
/// Use this to generate widget ids for widgets that have persistent state in [`Memory`].
|
||||
pub fn make_persistent_id<IdSource>(&self, id_source: IdSource) -> Id
|
||||
where
|
||||
IdSource: Hash + std::fmt::Debug,
|
||||
IdSource: Hash,
|
||||
{
|
||||
self.id.with(&id_source)
|
||||
}
|
||||
|
||||
pub(crate) fn next_auto_id(&self) -> Id {
|
||||
/// This is the `Id` that will be assigned to the next widget added to this `Ui`.
|
||||
pub fn next_auto_id(&self) -> Id {
|
||||
Id::new(self.next_auto_id_source)
|
||||
}
|
||||
|
||||
pub(crate) fn auto_id_with<IdSource>(&self, id_source: IdSource) -> Id
|
||||
pub fn auto_id_with<IdSource>(&self, id_source: IdSource) -> Id
|
||||
where
|
||||
IdSource: Hash + std::fmt::Debug,
|
||||
IdSource: Hash,
|
||||
{
|
||||
Id::new(self.next_auto_id_source).with(id_source)
|
||||
}
|
||||
|
@ -610,6 +624,22 @@ impl Ui {
|
|||
)
|
||||
}
|
||||
|
||||
/// Check for clicks, and drags on a specific region that is hovered.
|
||||
/// This can be used once you have checked that some shape you are painting has been hovered,
|
||||
/// and want to check for clicks and drags on hovered items this frame.
|
||||
/// The given [`Rect`] should approximately be where the thing is,
|
||||
/// as it is just where warnings will be painted if there is an [`Id`] clash.
|
||||
pub fn interact_with_hovered(
|
||||
&self,
|
||||
rect: Rect,
|
||||
hovered: bool,
|
||||
id: Id,
|
||||
sense: Sense,
|
||||
) -> Response {
|
||||
self.ctx()
|
||||
.interact_with_hovered(self.layer_id(), id, rect, sense, self.enabled, hovered)
|
||||
}
|
||||
|
||||
/// Is the pointer (mouse/touch) above this rectangle in this [`Ui`]?
|
||||
///
|
||||
/// The `clip_rect` and layer of this [`Ui`] will be respected, so, for instance,
|
||||
|
@ -937,7 +967,8 @@ impl Ui {
|
|||
pub fn scroll_to_rect(&self, rect: Rect, align: Option<Align>) {
|
||||
for d in 0..2 {
|
||||
let range = rect.min[d]..=rect.max[d];
|
||||
self.ctx().frame_state().scroll_target[d] = Some((range, align));
|
||||
self.ctx()
|
||||
.frame_state_mut(|state| state.scroll_target[d] = Some((range, align)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -966,7 +997,8 @@ impl Ui {
|
|||
let target = self.next_widget_position();
|
||||
for d in 0..2 {
|
||||
let target = target[d];
|
||||
self.ctx().frame_state().scroll_target[d] = Some((target..=target, align));
|
||||
self.ctx()
|
||||
.frame_state_mut(|state| state.scroll_target[d] = Some((target..=target, align)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -998,7 +1030,8 @@ impl Ui {
|
|||
/// # });
|
||||
/// ```
|
||||
pub fn scroll_with_delta(&self, delta: Vec2) {
|
||||
self.ctx().frame_state().scroll_delta += delta;
|
||||
self.ctx()
|
||||
.frame_state_mut(|state| state.scroll_delta += delta);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1037,7 +1070,7 @@ impl Ui {
|
|||
/// # });
|
||||
/// ```
|
||||
pub fn add_sized(&mut self, max_size: impl Into<Vec2>, widget: impl Widget) -> Response {
|
||||
// TODO: configure to overflow to main_dir instead of centered overflow
|
||||
// TODO(emilk): configure to overflow to main_dir instead of centered overflow
|
||||
// to handle the bug mentioned at https://github.com/emilk/egui/discussions/318#discussioncomment-627578
|
||||
// and fixed in https://github.com/emilk/egui/commit/035166276322b3f2324bd8b97ffcedc63fa8419f
|
||||
//
|
||||
|
@ -1251,7 +1284,7 @@ impl Ui {
|
|||
Label::new(text.into().strong()).ui(self)
|
||||
}
|
||||
|
||||
/// Show text that is waker (fainter color).
|
||||
/// Show text that is weaker (fainter color).
|
||||
///
|
||||
/// Shortcut for `ui.label(RichText::new(text).weak())`
|
||||
pub fn weak(&mut self, text: impl Into<RichText>) -> Response {
|
||||
|
@ -1381,9 +1414,10 @@ impl Ui {
|
|||
///
|
||||
/// See also [`Self::checkbox`].
|
||||
pub fn toggle_value(&mut self, selected: &mut bool, text: impl Into<WidgetText>) -> Response {
|
||||
let response = self.selectable_label(*selected, text);
|
||||
let mut response = self.selectable_label(*selected, text);
|
||||
if response.clicked() {
|
||||
*selected = !*selected;
|
||||
response.mark_changed();
|
||||
}
|
||||
response
|
||||
}
|
||||
|
@ -1422,7 +1456,7 @@ impl Ui {
|
|||
text: impl Into<WidgetText>,
|
||||
) -> Response {
|
||||
let mut response = self.radio(*current_value == alternative, text);
|
||||
if response.clicked() {
|
||||
if response.clicked() && *current_value != alternative {
|
||||
*current_value = alternative;
|
||||
response.mark_changed();
|
||||
}
|
||||
|
@ -1450,7 +1484,7 @@ impl Ui {
|
|||
text: impl Into<WidgetText>,
|
||||
) -> Response {
|
||||
let mut response = self.selectable_label(*current_value == selected_value, text);
|
||||
if response.clicked() {
|
||||
if response.clicked() && *current_value != selected_value {
|
||||
*current_value = selected_value;
|
||||
response.mark_changed();
|
||||
}
|
||||
|
@ -1525,7 +1559,11 @@ impl Ui {
|
|||
/// fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
/// let texture: &egui::TextureHandle = self.texture.get_or_insert_with(|| {
|
||||
/// // Load the texture only once.
|
||||
/// ui.ctx().load_texture("my-image", egui::ColorImage::example())
|
||||
/// ui.ctx().load_texture(
|
||||
/// "my-image",
|
||||
/// egui::ColorImage::example(),
|
||||
/// Default::default()
|
||||
/// )
|
||||
/// });
|
||||
///
|
||||
/// // Show the image:
|
||||
|
@ -1534,7 +1572,7 @@ impl Ui {
|
|||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Se also [`crate::Image`] and [`crate::ImageButton`].
|
||||
/// See also [`crate::Image`] and [`crate::ImageButton`].
|
||||
#[inline]
|
||||
pub fn image(&mut self, texture_id: impl Into<TextureId>, size: impl Into<Vec2>) -> Response {
|
||||
Image::new(texture_id, size).ui(self)
|
||||
|
@ -1720,7 +1758,7 @@ impl Ui {
|
|||
/// Create a child ui which is indented to the right.
|
||||
///
|
||||
/// The `id_source` here be anything at all.
|
||||
// TODO: remove `id_source` argument?
|
||||
// TODO(emilk): remove `id_source` argument?
|
||||
#[inline]
|
||||
pub fn indent<R>(
|
||||
&mut self,
|
||||
|
@ -1751,24 +1789,31 @@ impl Ui {
|
|||
};
|
||||
let ret = add_contents(&mut child_ui);
|
||||
|
||||
let left_vline = self.visuals().indent_has_left_vline;
|
||||
let end_with_horizontal_line = self.spacing().indent_ends_with_horizontal_line;
|
||||
|
||||
if end_with_horizontal_line {
|
||||
child_ui.add_space(4.0);
|
||||
}
|
||||
if left_vline || end_with_horizontal_line {
|
||||
if end_with_horizontal_line {
|
||||
child_ui.add_space(4.0);
|
||||
}
|
||||
|
||||
// draw a faint line on the left to mark the indented section
|
||||
let stroke = self.visuals().widgets.noninteractive.bg_stroke;
|
||||
let left_top = child_rect.min - 0.5 * indent * Vec2::X;
|
||||
let left_top = self.painter().round_pos_to_pixels(left_top);
|
||||
let left_bottom = pos2(left_top.x, child_ui.min_rect().bottom() - 2.0);
|
||||
let left_bottom = self.painter().round_pos_to_pixels(left_bottom);
|
||||
self.painter.line_segment([left_top, left_bottom], stroke);
|
||||
if end_with_horizontal_line {
|
||||
let fudge = 2.0; // looks nicer with button rounding in collapsing headers
|
||||
let right_bottom = pos2(child_ui.min_rect().right() - fudge, left_bottom.y);
|
||||
self.painter
|
||||
.line_segment([left_bottom, right_bottom], stroke);
|
||||
let stroke = self.visuals().widgets.noninteractive.bg_stroke;
|
||||
let left_top = child_rect.min - 0.5 * indent * Vec2::X;
|
||||
let left_top = self.painter().round_pos_to_pixels(left_top);
|
||||
let left_bottom = pos2(left_top.x, child_ui.min_rect().bottom() - 2.0);
|
||||
let left_bottom = self.painter().round_pos_to_pixels(left_bottom);
|
||||
|
||||
if left_vline {
|
||||
// draw a faint line on the left to mark the indented section
|
||||
self.painter.line_segment([left_top, left_bottom], stroke);
|
||||
}
|
||||
|
||||
if end_with_horizontal_line {
|
||||
let fudge = 2.0; // looks nicer with button rounding in collapsing headers
|
||||
let right_bottom = pos2(child_ui.min_rect().right() - fudge, left_bottom.y);
|
||||
self.painter
|
||||
.line_segment([left_bottom, right_bottom], stroke);
|
||||
}
|
||||
}
|
||||
|
||||
let response = self.allocate_rect(child_ui.min_rect(), Sense::hover());
|
||||
|
@ -1812,9 +1857,9 @@ impl Ui {
|
|||
) -> InnerResponse<R> {
|
||||
let initial_size = self.available_size_before_wrap();
|
||||
let layout = if self.placer.prefer_right_to_left() {
|
||||
Layout::right_to_left()
|
||||
Layout::right_to_left(Align::Center)
|
||||
} else {
|
||||
Layout::left_to_right()
|
||||
Layout::left_to_right(Align::Center)
|
||||
}
|
||||
.with_cross_align(Align::Center);
|
||||
self.allocate_ui_with_layout_dyn(initial_size, layout, Box::new(add_contents))
|
||||
|
@ -1827,9 +1872,9 @@ impl Ui {
|
|||
) -> InnerResponse<R> {
|
||||
let initial_size = self.available_size_before_wrap();
|
||||
let layout = if self.placer.prefer_right_to_left() {
|
||||
Layout::right_to_left()
|
||||
Layout::right_to_left(Align::Center)
|
||||
} else {
|
||||
Layout::left_to_right()
|
||||
Layout::left_to_right(Align::Center)
|
||||
}
|
||||
.with_cross_align(Align::Min);
|
||||
self.allocate_ui_with_layout_dyn(initial_size, layout, Box::new(add_contents))
|
||||
|
@ -1868,9 +1913,9 @@ impl Ui {
|
|||
);
|
||||
|
||||
let layout = if self.placer.prefer_right_to_left() {
|
||||
Layout::right_to_left()
|
||||
Layout::right_to_left(Align::Center)
|
||||
} else {
|
||||
Layout::left_to_right()
|
||||
Layout::left_to_right(Align::Center)
|
||||
}
|
||||
.with_main_wrap(main_wrap);
|
||||
|
||||
|
@ -1939,15 +1984,16 @@ impl Ui {
|
|||
///
|
||||
/// ```
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// ui.with_layout(egui::Layout::right_to_left(), |ui| {
|
||||
/// ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| {
|
||||
/// ui.label("world!");
|
||||
/// ui.label("Hello");
|
||||
/// });
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// See also [`Self::allocate_ui_with_layout`],
|
||||
/// and the helpers [`Self::horizontal]`, [`Self::vertical`], etc.
|
||||
/// If you don't want to use up all available space, use [`Self::allocate_ui_with_layout`].
|
||||
///
|
||||
/// See also the helpers [`Self::horizontal`], [`Self::vertical`], etc.
|
||||
#[inline]
|
||||
pub fn with_layout<R>(
|
||||
&mut self,
|
||||
|
@ -1977,7 +2023,14 @@ impl Ui {
|
|||
InnerResponse::new(inner, self.interact(rect, child_ui.id, Sense::hover()))
|
||||
}
|
||||
|
||||
#[deprecated = "Use ui.vertical_centered or ui.centered_and_justified"]
|
||||
pub fn centered<R>(&mut self, add_contents: impl FnOnce(&mut Self) -> R) -> InnerResponse<R> {
|
||||
self.vertical_centered(add_contents)
|
||||
}
|
||||
|
||||
/// This will make the next added widget centered and justified in the available space.
|
||||
///
|
||||
/// Only one widget may be added to the inner `Ui`!
|
||||
pub fn centered_and_justified<R>(
|
||||
&mut self,
|
||||
add_contents: impl FnOnce(&mut Self) -> R,
|
||||
|
@ -2012,7 +2065,7 @@ impl Ui {
|
|||
self.placer.set_row_height(height);
|
||||
}
|
||||
|
||||
/// Temporarily split split an Ui into several columns.
|
||||
/// Temporarily split a [`Ui`] into several columns.
|
||||
///
|
||||
/// ```
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
|
@ -2036,7 +2089,7 @@ impl Ui {
|
|||
num_columns: usize,
|
||||
add_contents: Box<dyn FnOnce(&mut [Self]) -> R + 'c>,
|
||||
) -> R {
|
||||
// TODO: ensure there is space
|
||||
// TODO(emilk): ensure there is space
|
||||
let spacing = self.spacing().item_spacing.x;
|
||||
let total_spacing = spacing * (num_columns as f32 - 1.0);
|
||||
let column_width = (self.available_width() - total_spacing) / (num_columns as f32);
|
||||
|
@ -2083,7 +2136,7 @@ impl Ui {
|
|||
self.menu_state = None;
|
||||
}
|
||||
|
||||
pub(crate) fn get_menu_state(&self) -> Option<Arc<RwLock<MenuState>>> {
|
||||
pub(crate) fn menu_state(&self) -> Option<Arc<RwLock<MenuState>>> {
|
||||
self.menu_state.clone()
|
||||
}
|
||||
|
||||
|
@ -2120,6 +2173,43 @@ impl Ui {
|
|||
menu::menu_button(self, title, add_contents)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a menu button with an image that when clicked will show the given menu.
|
||||
///
|
||||
/// If called from within a menu this will instead create a button for a sub-menu.
|
||||
///
|
||||
/// ```ignore
|
||||
/// use egui_extras;
|
||||
///
|
||||
/// let img = egui_extras::RetainedImage::from_svg_bytes_with_size(
|
||||
/// "rss",
|
||||
/// include_bytes!("rss.svg"),
|
||||
/// egui_extras::image::FitTo::Size(24, 24),
|
||||
/// );
|
||||
///
|
||||
/// ui.menu_image_button(img.texture_id(ctx), img.size_vec2(), |ui| {
|
||||
/// ui.menu_button("My sub-menu", |ui| {
|
||||
/// if ui.button("Close the menu").clicked() {
|
||||
/// ui.close_menu();
|
||||
/// }
|
||||
/// });
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// See also: [`Self::close_menu`] and [`Response::context_menu`].
|
||||
#[inline]
|
||||
pub fn menu_image_button<R>(
|
||||
&mut self,
|
||||
texture_id: TextureId,
|
||||
image_size: impl Into<Vec2>,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<Option<R>> {
|
||||
if let Some(menu_state) = self.menu_state.clone() {
|
||||
menu::submenu_button(self, menu_state, String::new(), add_contents)
|
||||
} else {
|
||||
menu::menu_image_button(self, texture_id, image_size, add_contents)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
|
@ -119,7 +119,7 @@ impl<Value: 'static + Send + Sync, Computer: 'static + Send + Sync> CacheTrait
|
|||
/// ```
|
||||
#[derive(Default)]
|
||||
pub struct CacheStorage {
|
||||
caches: ahash::AHashMap<std::any::TypeId, Box<dyn CacheTrait>>,
|
||||
caches: ahash::HashMap<std::any::TypeId, Box<dyn CacheTrait>>,
|
||||
}
|
||||
|
||||
impl CacheStorage {
|
|
@ -2,7 +2,7 @@ use epaint::util::hash;
|
|||
|
||||
const FIXED_CACHE_SIZE: usize = 1024; // must be small for web/WASM build (for unknown reason)
|
||||
|
||||
/// Very stupid/simple key-value cache. TODO: improve
|
||||
/// Very stupid/simple key-value cache. TODO(emilk): improve
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct FixedCache<K, V>([Option<(K, V)>; FIXED_CACHE_SIZE]);
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
// TODO: it is possible we can simplify `Element` further by
|
||||
// TODO(emilk): it is possible we can simplify `Element` further by
|
||||
// assuming everything is possibly serializable, and by supplying serialize/deserialize functions for them.
|
||||
// For non-serializable types, these simply return `None`.
|
||||
// This will also allow users to pick their own serialization format per type.
|
||||
|
@ -78,10 +78,12 @@ enum Element {
|
|||
#[cfg(feature = "persistence")]
|
||||
serialize_fn: Option<Serializer>,
|
||||
},
|
||||
|
||||
/// A serialized value
|
||||
Serialized {
|
||||
/// The type of value we are storing.
|
||||
type_id: TypeId,
|
||||
|
||||
/// The ron data we can deserialize.
|
||||
ron: Arc<str>,
|
||||
},
|
||||
|
@ -292,7 +294,7 @@ fn from_ron_str<T: serde::de::DeserializeOwned>(ron: &str) -> Option<T> {
|
|||
|
||||
use crate::Id;
|
||||
|
||||
// TODO: make IdTypeMap generic over the key (`Id`), and make a library of IdTypeMap.
|
||||
// TODO(emilk): make IdTypeMap generic over the key (`Id`), and make a library of IdTypeMap.
|
||||
/// Stores values identified by an [`Id`] AND a the [`std::any::TypeId`] of the value.
|
||||
///
|
||||
/// In other words, it maps `(Id, TypeId)` to any value you want.
|
||||
|
@ -316,19 +318,19 @@ use crate::Id;
|
|||
///
|
||||
/// // `b` associated with an f64 and a `&'static str`
|
||||
/// map.insert_persisted(b, 13.37);
|
||||
/// map.insert_temp(b, "Hello World".to_string());
|
||||
/// map.insert_temp(b, "Hello World".to_owned());
|
||||
///
|
||||
/// // we can retrieve all four values:
|
||||
/// assert_eq!(map.get_temp::<f64>(a), Some(3.14));
|
||||
/// assert_eq!(map.get_temp::<i32>(a), Some(42));
|
||||
/// assert_eq!(map.get_temp::<f64>(b), Some(13.37));
|
||||
/// assert_eq!(map.get_temp::<String>(b), Some("Hello World".to_string()));
|
||||
/// assert_eq!(map.get_temp::<String>(b), Some("Hello World".to_owned()));
|
||||
///
|
||||
/// // we can retrieve them like so also:
|
||||
/// assert_eq!(map.get_persisted::<f64>(a), Some(3.14));
|
||||
/// assert_eq!(map.get_persisted::<i32>(a), Some(42));
|
||||
/// assert_eq!(map.get_persisted::<f64>(b), Some(13.37));
|
||||
/// assert_eq!(map.get_temp::<String>(b), Some("Hello World".to_string()));
|
||||
/// assert_eq!(map.get_temp::<String>(b), Some("Hello World".to_owned()));
|
||||
/// ```
|
||||
#[derive(Clone, Debug, Default)]
|
||||
// We store use `id XOR typeid` as a key, so we don't need to hash again!
|
||||
|
@ -458,18 +460,18 @@ impl IdTypeMap {
|
|||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_empty(&mut self) -> bool {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn len(&mut self) -> usize {
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
/// Count how many values are stored but not yet deserialized.
|
||||
#[inline]
|
||||
pub fn count_serialized(&mut self) -> usize {
|
||||
pub fn count_serialized(&self) -> usize {
|
||||
self.0
|
||||
.values()
|
||||
.filter(|e| matches!(e, Element::Serialized { .. }))
|
||||
|
@ -477,7 +479,7 @@ impl IdTypeMap {
|
|||
}
|
||||
|
||||
/// Count the number of values are stored with the given type.
|
||||
pub fn count<T: 'static>(&mut self) -> usize {
|
||||
pub fn count<T: 'static>(&self) -> usize {
|
||||
let key = TypeId::of::<T>();
|
||||
self.0
|
||||
.iter()
|
||||
|
@ -512,6 +514,7 @@ impl PersistedMap {
|
|||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn into_map(self) -> IdTypeMap {
|
||||
IdTypeMap(
|
||||
self.0
|
||||
|
@ -574,19 +577,19 @@ fn test_two_id_x_two_types() {
|
|||
|
||||
// `b` associated with an f64 and a `&'static str`
|
||||
map.insert_persisted(b, 13.37);
|
||||
map.insert_temp(b, "Hello World".to_string());
|
||||
map.insert_temp(b, "Hello World".to_owned());
|
||||
|
||||
// we can retrieve all four values:
|
||||
assert_eq!(map.get_temp::<f64>(a), Some(3.14));
|
||||
assert_eq!(map.get_temp::<i32>(a), Some(42));
|
||||
assert_eq!(map.get_temp::<f64>(b), Some(13.37));
|
||||
assert_eq!(map.get_temp::<String>(b), Some("Hello World".to_string()));
|
||||
assert_eq!(map.get_temp::<String>(b), Some("Hello World".to_owned()));
|
||||
|
||||
// we can retrieve them like so also:
|
||||
assert_eq!(map.get_persisted::<f64>(a), Some(3.14));
|
||||
assert_eq!(map.get_persisted::<i32>(a), Some(42));
|
||||
assert_eq!(map.get_persisted::<f64>(b), Some(13.37));
|
||||
assert_eq!(map.get_temp::<String>(b), Some("Hello World".to_string()));
|
||||
assert_eq!(map.get_temp::<String>(b), Some("Hello World".to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
|
@ -2,11 +2,10 @@
|
|||
|
||||
pub mod cache;
|
||||
pub(crate) mod fixed_cache;
|
||||
mod history;
|
||||
pub mod id_type_map;
|
||||
pub mod undoer;
|
||||
|
||||
pub use history::History;
|
||||
pub use id_type_map::IdTypeMap;
|
||||
|
||||
pub use epaint::emath::History;
|
||||
pub use epaint::util::{hash, hash_with};
|
|
@ -1,3 +1,4 @@
|
|||
use std::borrow::Cow;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
|
@ -65,6 +66,13 @@ impl From<String> for RichText {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<Cow<'_, str>> for RichText {
|
||||
#[inline]
|
||||
fn from(text: Cow<'_, str>) -> Self {
|
||||
Self::new(text)
|
||||
}
|
||||
}
|
||||
|
||||
impl RichText {
|
||||
#[inline]
|
||||
pub fn new(text: impl Into<String>) -> Self {
|
||||
|
@ -285,12 +293,12 @@ impl RichText {
|
|||
let underline = if underline {
|
||||
crate::Stroke::new(1.0, line_color)
|
||||
} else {
|
||||
crate::Stroke::none()
|
||||
crate::Stroke::NONE
|
||||
};
|
||||
let strikethrough = if strikethrough {
|
||||
crate::Stroke::new(1.0, line_color)
|
||||
} else {
|
||||
crate::Stroke::none()
|
||||
crate::Stroke::NONE
|
||||
};
|
||||
|
||||
let valign = if raised {
|
||||
|
@ -565,14 +573,14 @@ impl WidgetText {
|
|||
let mut text_job = text.into_text_job(ui.style(), fallback_font.into(), valign);
|
||||
text_job.job.wrap.max_width = wrap_width;
|
||||
WidgetTextGalley {
|
||||
galley: ui.fonts().layout_job(text_job.job),
|
||||
galley: ui.fonts(|f| f.layout_job(text_job.job)),
|
||||
galley_has_color: text_job.job_has_color,
|
||||
}
|
||||
}
|
||||
Self::LayoutJob(mut job) => {
|
||||
job.wrap.max_width = wrap_width;
|
||||
WidgetTextGalley {
|
||||
galley: ui.fonts().layout_job(job),
|
||||
galley: ui.fonts(|f| f.layout_job(job)),
|
||||
galley_has_color: true,
|
||||
}
|
||||
}
|
||||
|
@ -605,6 +613,13 @@ impl From<String> for WidgetText {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<Cow<'_, str>> for WidgetText {
|
||||
#[inline]
|
||||
fn from(text: Cow<'_, str>) -> Self {
|
||||
Self::RichText(RichText::new(text))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RichText> for WidgetText {
|
||||
#[inline]
|
||||
fn from(rich_text: RichText) -> Self {
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue