diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 98a3b496a21..336b454f7bd 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -5,7 +5,7 @@ Please read the "Making a PR" section of [`CONTRIBUTING.md`](https://github.com/ * The PR title is what ends up in the changelog, so make it descriptive! * If applicable, add a screenshot or gif. * If it is a non-trivial addition, consider adding a demo for it to `egui_demo_lib`, or a new example. -* Do NOT open PR:s from your `master` branch, as that makes it hard for maintainers to add commits to your PR. +* Do NOT open PR:s from your `master` branch, as that makes it hard for maintainers to test and add commits to your PR. * Remember to run `cargo fmt` and `cargo clippy`. * Open the PR as a draft until you have self-reviewed it and run `./scripts/check.sh`. * When you have addressed a PR comment, mark it as resolved. diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 44cabf9a8cd..8aaa18faa36 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -108,7 +108,7 @@ jobs: - name: wasm-bindgen uses: jetli/wasm-bindgen-action@v0.1.0 with: - version: "0.2.90" + version: "0.2.92" - run: ./scripts/wasm_bindgen_check.sh --skip-setup diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 206829e7aa8..be55d133c22 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -66,7 +66,7 @@ Make a PR to add it as a link to [`README.md`](README.md#integrations) so others ## Testing the web viewer * Build with `scripts/build_demo_web.sh` * Host with `scripts/start_server.sh` -* Open +* Open ## Code Style diff --git a/Cargo.lock b/Cargo.lock index 9453d2070c6..217d7bda8b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -144,7 +144,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "052ad56e336bcc615a214bffbeca6c181ee9550acec193f0327e0b103b033a4d" dependencies = [ "android-properties", - "bitflags 2.4.0", + "bitflags 2.5.0", "cc", "cesu8", "jni", @@ -510,9 +510,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" dependencies = [ "serde", ] @@ -649,7 +649,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b50b5a44d59a98c55a9eeb518f39bf7499ba19fd98ee7d22618687f3f10adbf" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", "log", "polling 3.3.0", "rustix 0.38.21", @@ -819,6 +819,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ecdffb913a326b6c642290a0d0ec8e8d6597291acdc07cc4c9cb4b3635d44cf9" +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "com" version = "0.6.0" @@ -879,9 +885,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -889,9 +895,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "core-graphics" @@ -908,9 +914,9 @@ dependencies = [ [[package]] name = "core-graphics-types" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bb142d41022986c1d8ff29103a1411c8a3dfad3552f87a4f8dc50d61d4f4e33" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" dependencies = [ "bitflags 1.3.2", "core-foundation", @@ -1107,24 +1113,24 @@ dependencies = [ ] [[package]] -name = "directories-next" -version = "2.0.0" +name = "directories" +version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" dependencies = [ - "cfg-if", - "dirs-sys-next", + "dirs-sys", ] [[package]] -name = "dirs-sys-next" -version = "0.1.2" +name = "dirs-sys" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", + "option-ext", "redox_users", - "winapi", + "windows-sys 0.48.0", ] [[package]] @@ -1175,7 +1181,7 @@ version = "0.27.2" dependencies = [ "ahash", "bytemuck", - "directories-next", + "directories", "document-features", "egui", "egui-wgpu", @@ -1349,6 +1355,7 @@ dependencies = [ "ahash", "document-features", "egui", + "emath", "serde", ] @@ -1734,6 +1741,16 @@ dependencies = [ "wasi", ] +[[package]] +name = "gif" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gimli" version = "0.28.0" @@ -1792,7 +1809,7 @@ version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "005459a22af86adc706522d78d360101118e2638ec21df3852fcc626e0dbb212" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", "cfg_aliases", "cgl", "core-foundation", @@ -1868,7 +1885,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", "gpu-alloc-types", ] @@ -1878,7 +1895,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", ] [[package]] @@ -1896,22 +1913,22 @@ dependencies = [ [[package]] name = "gpu-descriptor" -version = "0.2.4" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc11df1ace8e7e564511f53af41f3e42ddc95b56fd07b3f4445d2a6048bc682c" +checksum = "9c08c1f623a8d0b722b8b99f821eb0ba672a1618f0d3b16ddbee1cedd2dd8557" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", "gpu-descriptor-types", "hashbrown", ] [[package]] name = "gpu-descriptor-types" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bf0b36e6f090b7e1d8a4b49c0cb81c1f8376f72198c65dd3ad9ff3556b8b78c" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", ] [[package]] @@ -1954,7 +1971,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af2a7e73e1f34c48da31fb668a907f250794837e08faa144fd24f0b8b741e890" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", "com", "libc", "libloading 0.8.0", @@ -1984,6 +2001,7 @@ version = "0.1.0" dependencies = [ "eframe", "env_logger", + "winit", ] [[package]] @@ -2079,6 +2097,8 @@ checksum = "a9b4f005360d32e9325029b38ba47ebd7a56f3316df09249368939562d518645" dependencies = [ "bytemuck", "byteorder", + "color_quant", + "gif", "num-traits", "png", "zune-core", @@ -2190,9 +2210,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.67" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] @@ -2353,11 +2373,11 @@ dependencies = [ [[package]] name = "metal" -version = "0.27.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43f73953f8cbe511f021b58f18c3ce1c3d1ae13fe953293e13345bf83217f25" +checksum = "5637e166ea14be6063a3f8ba5ccb9a4159df7d8f6d61c02fc3d480b1f90dcfcb" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", "block", "core-graphics-types", "foreign-types", @@ -2414,12 +2434,13 @@ dependencies = [ [[package]] name = "naga" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8878eb410fc90853da3908aebfe61d73d26d4437ef850b70050461f939509899" +checksum = "e536ae46fcab0876853bd4a632ede5df4b1c2527a58f6c5a4150fe86be858231" dependencies = [ + "arrayvec", "bit-set", - "bitflags 2.4.0", + "bitflags 2.5.0", "codespan-reporting", "hexf-parse", "indexmap", @@ -2438,7 +2459,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", "jni-sys", "log", "ndk-sys", @@ -2528,7 +2549,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" dependencies = [ "malloc_buf", - "objc_exception", ] [[package]] @@ -2639,15 +2659,6 @@ dependencies = [ "objc2 0.5.1", ] -[[package]] -name = "objc_exception" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" -dependencies = [ - "cc", -] - [[package]] name = "objc_id" version = "0.1.1" @@ -2678,6 +2689,12 @@ version = "11.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "orbclient" version = "0.3.46" @@ -2867,6 +2884,14 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" +[[package]] +name = "popups" +version = "0.27.2" +dependencies = [ + "eframe", + "env_logger", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -3104,9 +3129,9 @@ checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "renderdoc-sys" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216080ab382b992234dda86873c18d4c48358f5cfcb70fd693d7f6f2131b628b" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" [[package]] name = "resvg" @@ -3176,7 +3201,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ "base64", - "bitflags 2.4.0", + "bitflags 2.5.0", "serde", "serde_derive", ] @@ -3219,7 +3244,7 @@ version = "0.38.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", "errno", "libc", "linux-raw-sys 0.4.11", @@ -3448,7 +3473,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60e3d9941fa3bacf7c2bf4b065304faa14164151254cd16ce1b1bc8fc381600f" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", "calloop", "calloop-wayland-source", "cursor-icon", @@ -3509,7 +3534,7 @@ version = "0.3.0+sdk-1.3.268.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", ] [[package]] @@ -3656,18 +3681,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.56" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" +checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.56" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" +checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" dependencies = [ "proc-macro2", "quote", @@ -4026,9 +4051,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.90" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -4036,9 +4061,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.90" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", @@ -4051,9 +4076,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.40" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde2032aeb86bdfaecc8b261eef3cba735cc426c1f3a3416d1e0791be95fc461" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ "cfg-if", "js-sys", @@ -4063,9 +4088,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.90" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4073,9 +4098,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.90" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", @@ -4086,9 +4111,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.90" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "wayland-backend" @@ -4110,7 +4135,7 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ca7d52347346f5473bf2f56705f360e8440873052e575e55890c4fa57843ed3" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", "nix", "wayland-backend", "wayland-scanner", @@ -4122,7 +4147,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", "cursor-icon", "wayland-backend", ] @@ -4144,7 +4169,7 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e253d7107ba913923dc253967f35e8561a3c65f914543e46843c88ddd729e21c" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -4156,7 +4181,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -4169,7 +4194,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -4201,9 +4226,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.67" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", @@ -4243,15 +4268,22 @@ version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + [[package]] name = "wgpu" -version = "0.19.1" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bfe9a310dcf2e6b85f00c46059aaeaf4184caa8e29a1ecd4b7a704c3482332d" +checksum = "90e37c7b9921b75dfd26dd973fdcbce36f13dfa6e2dc82aece584e0ed48c355c" dependencies = [ "arrayvec", "cfg-if", "cfg_aliases", + "document-features", "js-sys", "log", "naga", @@ -4270,15 +4302,16 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "0.19.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b15e451d4060ada0d99a64df44e4d590213496da7c4f245572d51071e8e30ed" +checksum = "d59e0d5fc509601c69e4e1fa06c1eb3c4c9f12956a5e30c79b61ef1c1be7daf0" dependencies = [ "arrayvec", "bit-vec", - "bitflags 2.4.0", + "bitflags 2.5.0", "cfg_aliases", "codespan-reporting", + "document-features", "indexmap", "log", "naga", @@ -4296,14 +4329,14 @@ dependencies = [ [[package]] name = "wgpu-hal" -version = "0.19.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f259ceb56727fb097da108d92f8a5cbdb5b74a77f9e396bd43626f67299d61" +checksum = "6aa24c3889f885a3fb9133b454c8418bfcfaadcfe4ed3be96ac80e76703b863b" dependencies = [ "android_system_properties", "arrayvec", "ash", - "bitflags 2.4.0", + "bitflags 2.5.0", "block", "cfg_aliases", "core-graphics-types", @@ -4320,6 +4353,7 @@ dependencies = [ "log", "metal", "naga", + "ndk-sys", "objc", "once_cell", "parking_lot", @@ -4337,11 +4371,11 @@ dependencies = [ [[package]] name = "wgpu-types" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "895fcbeb772bfb049eb80b2d6e47f6c9af235284e9703c96fc0218a42ffd5af2" +checksum = "1353d9a46bff7f955a680577f34c69122628cc2076e1d6f3a9be6ef00ae793ef" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", "js-sys", "web-sys", ] @@ -4649,7 +4683,7 @@ dependencies = [ "ahash", "android-activity", "atomic-waker", - "bitflags 2.4.0", + "bitflags 2.5.0", "bytemuck", "calloop", "cfg_aliases", @@ -4755,7 +4789,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6924668544c48c0133152e7eec86d644a056ca3d09275eb8d5cdb9855f9d8699" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", "dlib", "log", "once_cell", diff --git a/Cargo.toml b/Cargo.toml index 07e83a4e9c6..4f2a533377f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,10 @@ panic = "abort" # This leads to better optimizations and smaller binaries (and i # split-debuginfo = "unpacked" # faster debug builds on mac # opt-level = 1 # Make debug builds run faster -panic = "abort" # This leads to better optimizations and smaller binaries (and is the default in Wasm anyways). +# panic = "abort" leads to better optimizations and smaller binaries (and is the default in Wasm anyways), +# but it also means backtraces don't work with the `backtrace` library (https://github.com/rust-lang/backtrace-rs/issues/397). +# egui has a feature where if you hold down all modifiers keys on your keyboard and hover any UI widget, +# you will see the backtrace to that widget, and we don't want to break that feature in dev builds. [profile.dev.package."*"] # Optimize all dependencies even in debug builds (does not affect workspace packages): @@ -88,7 +91,7 @@ web-time = "0.2" # Timekeeping for native and web wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" web-sys = "0.3.58" -wgpu = { version = "0.19.1", default-features = false, features = [ +wgpu = { version = "0.20.0", default-features = false, features = [ # Make the renderer `Sync` even on wasm32, because it makes the code simpler: "fragile-send-sync-non-atomic-wasm", ] } @@ -183,6 +186,7 @@ manual_string_new = "warn" map_err_ignore = "warn" map_flatten = "warn" map_unwrap_or = "warn" +match_bool = "warn" match_on_vec_items = "warn" match_same_arms = "warn" match_wild_err_arm = "warn" diff --git a/RELEASES.md b/RELEASES.md index b35b80c91ca..bb702ca267f 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -9,6 +9,8 @@ All crates under the [`crates/`](crates/) folder are published in lock-step, wit The only exception to this are patch releases, where we sometimes only patch a single crate. +The egui version in egui `master` is always the version of the last published crates. This is so that users can easily patch their egui crates to egui `master` if they want to. + ## Governance Releases are generally done by [emilk](https://github.com/emilk/), but the [rerun-io](https://github.com/rerun-io/) organization (where emilk is CTO) also has publish rights to all the crates. diff --git a/crates/eframe/Cargo.toml b/crates/eframe/Cargo.toml index acd7600f6e6..d9bbe6b9715 100644 --- a/crates/eframe/Cargo.toml +++ b/crates/eframe/Cargo.toml @@ -70,7 +70,7 @@ glow = [ ## Enable saving app state to disk. persistence = [ - "directories-next", + "directories", "egui-winit/serde", "egui/persistence", "ron", @@ -159,7 +159,7 @@ image = { workspace = true, features = ["png"] } # Needed for app icon winit = { workspace = true, default-features = false, features = ["rwh_06"] } # optional native: -directories-next = { version = "2", optional = true } +directories = { version = "5", optional = true } egui-wgpu = { workspace = true, optional = true, features = [ "winit", ] } # if wgpu is used, use it with winit @@ -241,8 +241,8 @@ web-sys = { workspace = true, features = [ "NodeList", "Performance", "ResizeObserver", - "ResizeObserverEntry", "ResizeObserverBoxOptions", + "ResizeObserverEntry", "ResizeObserverOptions", "ResizeObserverSize", "Storage", diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index ac6585c3d18..4a05c97aaf2 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -367,7 +367,7 @@ pub struct NativeOptions { pub persist_window: bool, /// The folder where `eframe` will store the app state. If not set, eframe will get the paths - /// from [directories_next]. + /// from [directories]. pub persistence_path: Option, } diff --git a/crates/eframe/src/lib.rs b/crates/eframe/src/lib.rs index 9f9f3a4c807..4e3ebad1dfe 100644 --- a/crates/eframe/src/lib.rs +++ b/crates/eframe/src/lib.rs @@ -197,7 +197,7 @@ pub mod icon_data; /// ``` no_run /// use eframe::egui; /// -/// fn main() -> eframe::Result<()> { +/// fn main() -> eframe::Result { /// let native_options = eframe::NativeOptions::default(); /// eframe::run_native("MyApp", native_options, Box::new(|cc| Ok(Box::new(MyEguiApp::new(cc))))) /// } @@ -233,7 +233,7 @@ pub fn run_native( app_name: &str, mut native_options: NativeOptions, app_creator: AppCreator, -) -> Result<()> { +) -> Result { #[cfg(not(feature = "__screenshot"))] assert!( std::env::var("EFRAME_SCREENSHOT_TO").is_err(), @@ -278,7 +278,7 @@ pub fn run_native( /// /// # Example /// ``` no_run -/// fn main() -> eframe::Result<()> { +/// fn main() -> eframe::Result { /// // Our application state: /// let mut name = "Arthur".to_owned(); /// let mut age = 42; @@ -310,7 +310,7 @@ pub fn run_simple_native( app_name: &str, native_options: NativeOptions, update_fun: impl FnMut(&egui::Context, &mut Frame) + 'static, -) -> Result<()> { +) -> Result { struct SimpleApp { update_fun: U, } @@ -445,7 +445,7 @@ impl std::fmt::Display for Error { } /// Short for `Result`. -pub type Result = std::result::Result; +pub type Result = std::result::Result; // --------------------------------------------------------------------------- diff --git a/crates/eframe/src/native/file_storage.rs b/crates/eframe/src/native/file_storage.rs index 970c35a4f41..fb27642b42e 100644 --- a/crates/eframe/src/native/file_storage.rs +++ b/crates/eframe/src/native/file_storage.rs @@ -10,12 +10,12 @@ use std::{ /// [`egui::ViewportBuilder::app_id`] of [`crate::NativeOptions::viewport`] /// or the title argument to [`crate::run_native`]. /// -/// On native the path is picked using [`directories_next::ProjectDirs::data_dir`](https://docs.rs/directories-next/2.0.0/directories_next/struct.ProjectDirs.html#method.data_dir) which is: +/// On native the path is picked using [`directories::ProjectDirs::data_dir`](https://docs.rs/directories/5.0.1/directories/struct.ProjectDirs.html#method.data_dir) which is: /// * Linux: `/home/UserName/.local/share/APP_ID` /// * macOS: `/Users/UserName/Library/Application Support/APP_ID` /// * Windows: `C:\Users\UserName\AppData\Roaming\APP_ID` pub fn storage_dir(app_id: &str) -> Option { - directories_next::ProjectDirs::from("", "", app_id) + directories::ProjectDirs::from("", "", app_id) .map(|proj_dirs| proj_dirs.data_dir().to_path_buf()) } diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index 9be49527ea8..33bd2cfaa38 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -1088,7 +1088,7 @@ impl GlutinWindowContext { &mut self, viewport_id: ViewportId, event_loop: &EventLoopWindowTarget, - ) -> Result<()> { + ) -> Result { crate::profile_function!(); let viewport = self @@ -1188,7 +1188,7 @@ impl GlutinWindowContext { } /// only applies for android. but we basically drop surface + window and make context not current - fn on_suspend(&mut self) -> Result<()> { + fn on_suspend(&mut self) -> Result { log::debug!("received suspend event. dropping window and surface"); for viewport in self.viewports.values_mut() { viewport.gl_surface = None; @@ -1284,7 +1284,6 @@ impl GlutinWindowContext { if let Some(window) = &viewport.window { let old_inner_size = window.inner_size(); - let is_viewport_focused = self.focused_viewport == Some(viewport_id); viewport.deferred_commands.append(&mut commands); egui_winit::process_viewport_commands( @@ -1292,7 +1291,6 @@ impl GlutinWindowContext { &mut viewport.info, std::mem::take(&mut viewport.deferred_commands), window, - is_viewport_focused, &mut viewport.actions_requested, ); diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs index 7087ed6ae71..6ba5486b678 100644 --- a/crates/eframe/src/native/run.rs +++ b/crates/eframe/src/native/run.rs @@ -60,10 +60,7 @@ fn with_event_loop( } #[cfg(not(target_os = "ios"))] -fn run_and_return( - event_loop: &mut EventLoop, - mut winit_app: impl WinitApp, -) -> Result<()> { +fn run_and_return(event_loop: &mut EventLoop, mut winit_app: impl WinitApp) -> Result { use winit::{event_loop::ControlFlow, platform::run_on_demand::EventLoopExtRunOnDemand}; log::trace!("Entering the winit event loop (run_on_demand)…"); @@ -234,7 +231,7 @@ fn run_and_return( fn run_and_exit( event_loop: EventLoop, mut winit_app: impl WinitApp + 'static, -) -> Result<()> { +) -> Result { use winit::event_loop::ControlFlow; log::trace!("Entering the winit event loop (run)…"); @@ -390,7 +387,7 @@ pub fn run_glow( app_name: &str, mut native_options: epi::NativeOptions, app_creator: epi::AppCreator, -) -> Result<()> { +) -> Result { #![allow(clippy::needless_return_with_question_mark)] // False positive use super::glow_integration::GlowWinitApp; @@ -415,7 +412,7 @@ pub fn run_wgpu( app_name: &str, mut native_options: epi::NativeOptions, app_creator: epi::AppCreator, -) -> Result<()> { +) -> Result { #![allow(clippy::needless_return_with_question_mark)] // False positive use super::wgpu_integration::WgpuWinitApp; diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index d7f08d25a99..ca8e737d656 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -635,7 +635,7 @@ impl WgpuWinitRunning { viewports, painter, viewport_from_window, - focused_viewport, + .. } = &mut *shared_mut; let FullOutput { @@ -724,7 +724,6 @@ impl WgpuWinitRunning { viewports, painter, viewport_from_window, - *focused_viewport, ); // Prune dead viewports: @@ -996,7 +995,6 @@ fn render_immediate_viewport( viewports, painter, viewport_from_window, - focused_viewport, .. } = &mut *shared_mut; @@ -1036,7 +1034,6 @@ fn render_immediate_viewport( viewports, painter, viewport_from_window, - *focused_viewport, ); } @@ -1061,7 +1058,6 @@ fn handle_viewport_output( viewports: &mut ViewportIdMap, painter: &mut egui_wgpu::winit::Painter, viewport_from_window: &mut HashMap, - focused_viewport: Option, ) { for ( viewport_id, @@ -1083,7 +1079,6 @@ fn handle_viewport_output( if let Some(window) = viewport.window.as_ref() { let old_inner_size = window.inner_size(); - let is_viewport_focused = focused_viewport == Some(viewport_id); viewport.deferred_commands.append(&mut commands); egui_winit::process_viewport_commands( @@ -1091,7 +1086,6 @@ fn handle_viewport_output( &mut viewport.info, std::mem::take(&mut viewport.deferred_commands), window, - is_viewport_focused, &mut viewport.actions_requested, ); diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index f8b7f14a9da..f683b991aff 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -15,7 +15,6 @@ pub struct AppRunner { pub(crate) needs_repaint: std::sync::Arc, last_save_time: f64, pub(crate) text_agent: TextAgent, - pub(crate) mutable_text_under_cursor: bool, // Output for the last run: textures_delta: TexturesDelta, @@ -121,7 +120,6 @@ impl AppRunner { needs_repaint, last_save_time: now_sec(), text_agent, - mutable_text_under_cursor: false, textures_delta: Default::default(), clipped_primitives: None, }; @@ -183,12 +181,38 @@ impl AppRunner { self.clipped_primitives.is_some() } + /// Does the eframe app have focus? + /// + /// Technically: does either the canvas or the [`TextAgent`] have focus? + pub fn has_focus(&self) -> bool { + super::has_focus(self.canvas()) || self.text_agent.has_focus() + } + + pub fn update_focus(&mut self) { + let has_focus = self.has_focus(); + if self.input.raw.focused != has_focus { + log::trace!("{} Focus changed to {has_focus}", self.canvas().id()); + self.input.set_focus(has_focus); + + if !has_focus { + // We lost focus - good idea to save + self.save(); + } + self.egui_ctx().request_repaint(); + } + } + /// Runs the logic, but doesn't paint the result. /// /// The result can be painted later with a call to [`Self::run_and_paint`] or [`Self::paint`]. pub fn logic(&mut self) { + // We sometimes miss blur/focus events due to the text agent, so let's just poll each frame: + self.update_focus(); + let canvas_size = super::canvas_size_in_points(self.canvas(), self.egui_ctx()); - let raw_input = self.input.new_frame(canvas_size); + let mut raw_input = self.input.new_frame(canvas_size); + + self.app.raw_input_hook(&self.egui_ctx, &mut raw_input); let full_output = self.egui_ctx.run(raw_input, |egui_ctx| { self.app.update(egui_ctx, &mut self.frame); @@ -249,8 +273,8 @@ impl AppRunner { cursor_icon, open_url, copied_text, - events: _, // already handled - mutable_text_under_cursor, + events: _, // already handled + mutable_text_under_cursor: _, // TODO(#4569): https://github.com/emilk/egui/issues/4569 ime, #[cfg(feature = "accesskit")] accesskit_update: _, // not currently implemented @@ -269,7 +293,17 @@ impl AppRunner { #[cfg(not(web_sys_unstable_apis))] let _ = copied_text; - self.mutable_text_under_cursor = mutable_text_under_cursor; + if self.has_focus() { + // The eframe app has focus. + if ime.is_some() { + // We are editing text: give the focus to the text agent. + self.text_agent.focus(); + } else { + // We are not editing text - give the focus to the canvas. + self.text_agent.blur(); + self.canvas().focus().ok(); + } + } if let Err(err) = self.text_agent.move_to(ime, self.canvas()) { log::error!( diff --git a/crates/eframe/src/web/backend.rs b/crates/eframe/src/web/backend.rs index 74853abe9fb..5877f424cb7 100644 --- a/crates/eframe/src/web/backend.rs +++ b/crates/eframe/src/web/backend.rs @@ -12,10 +12,7 @@ use super::percent_decode; #[derive(Default)] pub(crate) struct WebInput { /// Required because we don't get a position on touched - pub latest_touch_pos: Option, - - /// Required to maintain a stable touch position for multi-touch gestures. - pub latest_touch_pos_id: Option, + pub primary_touch: Option, /// The raw input to `egui`. pub raw: egui::RawInput, @@ -36,14 +33,17 @@ impl WebInput { raw_input } - /// On alt-tab and similar. - pub fn on_web_page_focus_change(&mut self, focused: bool) { + /// On alt-tab, or user clicking another HTML element. + pub fn set_focus(&mut self, focused: bool) { + if self.raw.focused == focused { + return; + } + // log::debug!("on_web_page_focus_change: {focused}"); self.raw.modifiers = egui::Modifiers::default(); // Avoid sticky modifier keys on alt-tab: self.raw.focused = focused; self.raw.events.push(egui::Event::WindowFocused(focused)); - self.latest_touch_pos = None; - self.latest_touch_pos_id = None; + self.primary_touch = None; } } diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index acb162c04b1..0ed759d952a 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -1,5 +1,10 @@ +use web_sys::EventTarget; + use super::*; +// TODO(emilk): there are more calls to `prevent_default` and `stop_propagaton` +// than what is probably needed. + // ------------------------------------------------------------------------ /// Calls `request_animation_frame` to schedule repaint. @@ -47,145 +52,251 @@ fn paint_if_needed(runner: &mut AppRunner) { // ------------------------------------------------------------------------ -pub(crate) fn install_document_events(runner_ref: &WebRunner) -> Result<(), JsValue> { - let document = web_sys::window().unwrap().document().unwrap(); +pub(crate) fn install_event_handlers(runner_ref: &WebRunner) -> Result<(), JsValue> { + let window = web_sys::window().unwrap(); + let document = window.document().unwrap(); + let canvas = runner_ref.try_lock().unwrap().canvas().clone(); + + install_blur_focus(runner_ref, &canvas)?; + + prevent_default_and_stop_propagation( + runner_ref, + &canvas, + &[ + // Allow users to use ctrl-p for e.g. a command palette: + "afterprint", + // By default, right-clicks open a browser context menu. + // We don't want to do that (right clicks are handled by egui): + "contextmenu", + ], + )?; + + install_keydown(runner_ref, &canvas)?; + install_keyup(runner_ref, &canvas)?; + + // It seems copy/cut/paste events only work on the document, + // so we check if we have focus inside of the handler. + install_copy_cut_paste(runner_ref, &document)?; + + install_mousedown(runner_ref, &canvas)?; + // Use `document` here to notice if the user releases a drag outside of the canvas: + // See https://github.com/emilk/egui/issues/3157 + install_mousemove(runner_ref, &document)?; + install_mouseup(runner_ref, &document)?; + install_mouseleave(runner_ref, &canvas)?; + + install_touchstart(runner_ref, &canvas)?; + // Use `document` here to notice if the user drag outside of the canvas: + // See https://github.com/emilk/egui/issues/3157 + install_touchmove(runner_ref, &document)?; + install_touchend(runner_ref, &document)?; + install_touchcancel(runner_ref, &canvas)?; + + install_wheel(runner_ref, &canvas)?; + install_drag_and_drop(runner_ref, &canvas)?; + install_window_events(runner_ref, &window)?; + Ok(()) +} +fn install_blur_focus(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> { + // NOTE: because of the text agent we sometime miss 'blur' events, + // so we also poll the focus state each frame in `AppRunner::logic`. for event_name in ["blur", "focus"] { let closure = move |_event: web_sys::MouseEvent, runner: &mut AppRunner| { - // log::debug!("{event_name:?}"); - let has_focus = event_name == "focus"; + log::trace!("{} {event_name:?}", runner.canvas().id()); + runner.update_focus(); - if !has_focus { - // We lost focus - good idea to save + if event_name == "blur" { + // This might be a good time to save the state runner.save(); } - - runner.input.on_web_page_focus_change(has_focus); - runner.egui_ctx().request_repaint(); }; - runner_ref.add_event_listener(&document, event_name, closure)?; + runner_ref.add_event_listener(target, event_name, closure)?; } + Ok(()) +} +fn install_keydown(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> { runner_ref.add_event_listener( - &document, + target, "keydown", |event: web_sys::KeyboardEvent, runner| { - if event.is_composing() || event.key_code() == 229 { - // https://web.archive.org/web/20200526195704/https://www.fxsitecompat.dev/en-CA/docs/2018/keydown-and-keyup-events-are-now-fired-during-ime-composition/ + if !runner.input.raw.focused { return; } let modifiers = modifiers_from_kb_event(&event); - runner.input.raw.modifiers = modifiers; - - let key = event.key(); - let egui_key = translate_key(&key); - - if let Some(key) = egui_key { - runner.input.raw.events.push(egui::Event::Key { - key, - physical_key: None, // TODO(fornwall) - pressed: true, - repeat: false, // egui will fill this in for us! - modifiers, - }); - } if !modifiers.ctrl && !modifiers.command - && !should_ignore_key(&key) // When text agent is focused, it is responsible for handling input events && !runner.text_agent.has_focus() { - runner.input.raw.events.push(egui::Event::Text(key)); - } - runner.needs_repaint.repaint_asap(); + if let Some(text) = text_from_keyboard_event(&event) { + runner.input.raw.events.push(egui::Event::Text(text)); + runner.needs_repaint.repaint_asap(); - let egui_wants_keyboard = runner.egui_ctx().wants_keyboard_input(); - - #[allow(clippy::if_same_then_else)] - let prevent_default = if egui_key == Some(egui::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(egui::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 + // If this is indeed text, then prevent any other action. + event.prevent_default(); + + // Assume egui uses all key events, and don't let them propagate to parent elements. + event.stop_propagation(); } - } else if egui_wants_keyboard { - matches!( - event.key().as_str(), - "Backspace" // so we don't go back to previous page when deleting text - | "ArrowDown" | "ArrowLeft" | "ArrowRight" | "ArrowUp" // cmd-left is "back" on Mac (https://github.com/emilk/egui/issues/58) - ) - } else { - // We never want to prevent: - // * F5 / cmd-R (refresh) - // * cmd-shift-C (debug tools) - // * cmd/ctrl-c/v/x (or we stop copy/past/cut events) - false - }; - - // log::debug!( - // "On key-down {:?}, egui_wants_keyboard: {}, prevent_default: {}", - // event.key().as_str(), - // egui_wants_keyboard, - // prevent_default - // ); - - if prevent_default { - event.prevent_default(); - // event.stop_propagation(); } - }, - )?; - runner_ref.add_event_listener( - &document, - "keyup", - |event: web_sys::KeyboardEvent, runner| { - let modifiers = modifiers_from_kb_event(&event); - runner.input.raw.modifiers = modifiers; - if let Some(key) = translate_key(&event.key()) { - runner.input.raw.events.push(egui::Event::Key { - key, - physical_key: None, // TODO(fornwall) - pressed: false, - repeat: false, - modifiers, - }); - } - runner.needs_repaint.repaint_asap(); + on_keydown(event, runner); }, - )?; + ) +} + +#[allow(clippy::needless_pass_by_value)] // So that we can pass it directly to `add_event_listener` +pub(crate) fn on_keydown(event: web_sys::KeyboardEvent, runner: &mut AppRunner) { + let has_focus = runner.input.raw.focused; + if !has_focus { + return; + } + + if event.is_composing() || event.key_code() == 229 { + // 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; + } + + let modifiers = modifiers_from_kb_event(&event); + runner.input.raw.modifiers = modifiers; + + let key = event.key(); + let egui_key = translate_key(&key); + + if let Some(egui_key) = egui_key { + runner.input.raw.events.push(egui::Event::Key { + key: egui_key, + physical_key: None, // TODO(fornwall) + pressed: true, + repeat: false, // egui will fill this in for us! + modifiers, + }); + runner.needs_repaint.repaint_asap(); + let prevent_default = should_prevent_default_for_key(runner, &modifiers, egui_key); + + // log::debug!( + // "On keydown {:?} {egui_key:?}, has_focus: {has_focus}, egui_wants_keyboard: {}, prevent_default: {prevent_default}", + // event.key().as_str(), + // runner.egui_ctx().wants_keyboard_input() + // ); + + if prevent_default { + event.prevent_default(); + } + + // Assume egui uses all key events, and don't let them propagate to parent elements. + event.stop_propagation(); + } +} + +/// If the canvas (or text agent) has focus: +/// should we prevent the default browser event action when the user presses this key? +fn should_prevent_default_for_key( + runner: &AppRunner, + modifiers: &egui::Modifiers, + egui_key: egui::Key, +) -> bool { + // NOTE: We never want to prevent: + // * F5 / cmd-R (refresh) + // * cmd-shift-C (debug tools) + // * cmd/ctrl-c/v/x (lest we prevent copy/paste/cut events) + + // Prevent ctrl-P from opening the print dialog. Users may want to use it for a command palette. + if egui_key == egui::Key::P && (modifiers.ctrl || modifiers.command || modifiers.mac_cmd) { + return true; + } + + if egui_key == egui::Key::Space && !runner.text_agent.has_focus() { + // Space scrolls the web page, but we don't want that while canvas has focus + // However, don't prevent it if text agent has focus, or we can't type space! + return true; + } + + matches!( + egui_key, + // Prevent browser from focusing the next HTML element. + // egui uses Tab to move focus within the egui app. + egui::Key::Tab + + // So we don't go back to previous page while canvas has focus + | egui::Key::Backspace + + // Don't scroll web page while canvas has focus. + // Also, cmd-left is "back" on Mac (https://github.com/emilk/egui/issues/58) + | egui::Key::ArrowDown | egui::Key::ArrowLeft | egui::Key::ArrowRight | egui::Key::ArrowUp + ) +} + +fn install_keyup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> { + runner_ref.add_event_listener(target, "keyup", on_keyup) +} + +#[allow(clippy::needless_pass_by_value)] // So that we can pass it directly to `add_event_listener` +pub(crate) fn on_keyup(event: web_sys::KeyboardEvent, runner: &mut AppRunner) { + let modifiers = modifiers_from_kb_event(&event); + runner.input.raw.modifiers = modifiers; + + if let Some(key) = translate_key(&event.key()) { + runner.input.raw.events.push(egui::Event::Key { + key, + physical_key: None, // TODO(fornwall) + pressed: false, + repeat: false, + modifiers, + }); + } + + if event.key() == "Meta" || event.key() == "Control" { + // When pressing Cmd+A (select all) or Ctrl+C (copy), + // chromium will not fire a `keyup` for the letter key. + // This leads to stuck keys, unless we do this hack. + // See https://github.com/emilk/egui/issues/4724 + + let keys_down = runner.egui_ctx().input(|i| i.keys_down.clone()); + for key in keys_down { + runner.input.raw.events.push(egui::Event::Key { + key, + physical_key: None, + pressed: false, + repeat: false, + modifiers, + }); + } + } + + runner.needs_repaint.repaint_asap(); + + let has_focus = runner.input.raw.focused; + if has_focus { + // Assume egui uses all key events, and don't let them propagate to parent elements. + event.stop_propagation(); + } +} + +fn install_copy_cut_paste(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> { #[cfg(web_sys_unstable_apis)] - runner_ref.add_event_listener( - &document, - "paste", - |event: web_sys::ClipboardEvent, runner| { - if let Some(data) = event.clipboard_data() { - if let Ok(text) = data.get_data("text") { - let text = text.replace("\r\n", "\n"); - if !text.is_empty() { - runner.input.raw.events.push(egui::Event::Paste(text)); - runner.needs_repaint.repaint_asap(); - } - event.stop_propagation(); - event.prevent_default(); + runner_ref.add_event_listener(target, "paste", |event: web_sys::ClipboardEvent, runner| { + if let Some(data) = event.clipboard_data() { + if let Ok(text) = data.get_data("text") { + let text = text.replace("\r\n", "\n"); + if !text.is_empty() && runner.input.raw.focused { + runner.input.raw.events.push(egui::Event::Paste(text)); + runner.needs_repaint.repaint_asap(); } + event.stop_propagation(); + event.prevent_default(); } - }, - )?; + } + })?; #[cfg(web_sys_unstable_apis)] - runner_ref.add_event_listener( - &document, - "cut", - |event: web_sys::ClipboardEvent, runner| { + runner_ref.add_event_listener(target, "cut", |event: web_sys::ClipboardEvent, runner| { + if runner.input.raw.focused { runner.input.raw.events.push(egui::Event::Cut); // In Safari we are only allowed to write to the clipboard during the @@ -194,17 +305,15 @@ pub(crate) fn install_document_events(runner_ref: &WebRunner) -> Result<(), JsVa // Make sure we paint the output of the above logic call asap: runner.needs_repaint.repaint_asap(); + } - event.stop_propagation(); - event.prevent_default(); - }, - )?; + event.stop_propagation(); + event.prevent_default(); + })?; #[cfg(web_sys_unstable_apis)] - runner_ref.add_event_listener( - &document, - "copy", - |event: web_sys::ClipboardEvent, runner| { + runner_ref.add_event_listener(target, "copy", |event: web_sys::ClipboardEvent, runner| { + if runner.input.raw.focused { runner.input.raw.events.push(egui::Event::Copy); // In Safari we are only allowed to write to the clipboard during the @@ -213,49 +322,30 @@ pub(crate) fn install_document_events(runner_ref: &WebRunner) -> Result<(), JsVa // Make sure we paint the output of the above logic call asap: runner.needs_repaint.repaint_asap(); + } - event.stop_propagation(); - event.prevent_default(); - }, - )?; + event.stop_propagation(); + event.prevent_default(); + })?; Ok(()) } -pub(crate) fn install_window_events(runner_ref: &WebRunner) -> Result<(), JsValue> { - let window = web_sys::window().unwrap(); - - for event_name in ["blur", "focus"] { - let closure = move |_event: web_sys::MouseEvent, runner: &mut AppRunner| { - // log::debug!("{event_name:?}"); - let has_focus = event_name == "focus"; - - if !has_focus { - // We lost focus - good idea to save - runner.save(); - } - - runner.input.on_web_page_focus_change(has_focus); - runner.egui_ctx().request_repaint(); - }; - - runner_ref.add_event_listener(&window, event_name, closure)?; - } - +fn install_window_events(runner_ref: &WebRunner, window: &EventTarget) -> Result<(), JsValue> { // Save-on-close - runner_ref.add_event_listener(&window, "onbeforeunload", |_: web_sys::Event, runner| { + runner_ref.add_event_listener(window, "onbeforeunload", |_: web_sys::Event, runner| { runner.save(); })?; // NOTE: resize is handled by `ResizeObserver` below for event_name in &["load", "pagehide", "pageshow"] { - runner_ref.add_event_listener(&window, event_name, move |_: web_sys::Event, runner| { + runner_ref.add_event_listener(window, event_name, move |_: web_sys::Event, runner| { // log::debug!("{event_name:?}"); runner.needs_repaint.repaint_asap(); })?; } - runner_ref.add_event_listener(&window, "hashchange", |_: web_sys::Event, runner| { + runner_ref.add_event_listener(window, "hashchange", |_: web_sys::Event, runner| { // `epi::Frame::info(&self)` clones `epi::IntegrationInfo`, but we need to modify the original here runner.frame.info.web_info.location.hash = location_hash(); runner.needs_repaint.repaint_asap(); // tell the user about the new hash @@ -283,33 +373,27 @@ pub(crate) fn install_color_scheme_change_event(runner_ref: &WebRunner) -> Resul Ok(()) } -pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValue> { - let canvas = runner_ref.try_lock().unwrap().canvas().clone(); - let window = web_sys::window().unwrap(); - let document = window.document().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): - "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, _runner: &mut AppRunner| { - event.prevent_default(); - // event.stop_propagation(); - // log::debug!("Preventing event {event_name:?}"); - }; +fn prevent_default_and_stop_propagation( + runner_ref: &WebRunner, + target: &EventTarget, + event_names: &[&'static str], +) -> Result<(), JsValue> { + for event_name in event_names { + let closure = move |event: web_sys::MouseEvent, _runner: &mut AppRunner| { + event.prevent_default(); + event.stop_propagation(); + // log::debug!("Preventing event {event_name:?}"); + }; - runner_ref.add_event_listener(&canvas, event_name, closure)?; - } + runner_ref.add_event_listener(target, event_name, closure)?; } + Ok(()) +} + +fn install_mousedown(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> { runner_ref.add_event_listener( - &canvas, + target, "mousedown", |event: web_sys::MouseEvent, runner: &mut AppRunner| { let modifiers = modifiers_from_mouse_event(&event); @@ -334,35 +418,39 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu event.stop_propagation(); // Note: prevent_default breaks VSCode tab focusing, hence why we don't call it here. }, - )?; + ) +} - // NOTE: we register "mousemove" on `document` instead of just the canvas - // in order to track a dragged mouse outside the canvas. - // See https://github.com/emilk/egui/issues/3157 - runner_ref.add_event_listener( - &document, - "mousemove", - |event: web_sys::MouseEvent, runner| { - let modifiers = modifiers_from_mouse_event(&event); - runner.input.raw.modifiers = modifiers; - let pos = pos_from_mouse_event(runner.canvas(), &event, runner.egui_ctx()); +/// Returns true if the cursor is above the canvas, or if we're dragging something. +fn is_interested_in_pointer_event(egui_ctx: &egui::Context, pos: egui::Pos2) -> bool { + egui_ctx.input(|i| i.screen_rect().contains(pos) || i.pointer.any_down() || i.any_touches()) +} + +fn install_mousemove(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> { + runner_ref.add_event_listener(target, "mousemove", |event: web_sys::MouseEvent, runner| { + let modifiers = modifiers_from_mouse_event(&event); + runner.input.raw.modifiers = modifiers; + + let pos = pos_from_mouse_event(runner.canvas(), &event, runner.egui_ctx()); + + if is_interested_in_pointer_event(runner.egui_ctx(), pos) { runner.input.raw.events.push(egui::Event::PointerMoved(pos)); runner.needs_repaint.repaint_asap(); event.stop_propagation(); event.prevent_default(); - }, - )?; + } + }) +} - // Use `document` here to notice if the user releases a drag outside of the canvas. - // See https://github.com/emilk/egui/issues/3157 - runner_ref.add_event_listener( - &document, - "mouseup", - |event: web_sys::MouseEvent, runner| { - let modifiers = modifiers_from_mouse_event(&event); - runner.input.raw.modifiers = modifiers; +fn install_mouseup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> { + runner_ref.add_event_listener(target, "mouseup", |event: web_sys::MouseEvent, runner| { + let modifiers = modifiers_from_mouse_event(&event); + runner.input.raw.modifiers = modifiers; + + let pos = pos_from_mouse_event(runner.canvas(), &event, runner.egui_ctx()); + + if is_interested_in_pointer_event(runner.egui_ctx(), pos) { if let Some(button) = button_from_mouse_event(&event) { - let pos = pos_from_mouse_event(runner.canvas(), &event, runner.egui_ctx()); let modifiers = runner.input.raw.modifiers; runner.input.raw.events.push(egui::Event::PointerButton { pos, @@ -371,24 +459,25 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu modifiers, }); - // In Safari we are only allowed to write to the clipboard during the - // event callback, which is why we run the app logic here and now: + // In Safari we are only allowed to do certain things + // (like playing audio, start a download, etc) + // on user action, such as a click. + // So we need to run the app logic here and now: runner.logic(); - runner - .text_agent - .set_focus(runner.mutable_text_under_cursor); - // Make sure we paint the output of the above logic call asap: runner.needs_repaint.repaint_asap(); + + event.prevent_default(); + event.stop_propagation(); } - event.stop_propagation(); - event.prevent_default(); - }, - )?; + } + }) +} +fn install_mouseleave(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> { runner_ref.add_event_listener( - &canvas, + target, "mouseleave", |event: web_sys::MouseEvent, runner| { runner.input.raw.events.push(egui::Event::PointerGone); @@ -396,93 +485,73 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu event.stop_propagation(); event.prevent_default(); }, - )?; + ) +} +fn install_touchstart(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> { runner_ref.add_event_listener( - &canvas, + target, "touchstart", |event: web_sys::TouchEvent, runner| { - let mut latest_touch_pos_id = runner.input.latest_touch_pos_id; - let pos = pos_from_touch_event( - runner.canvas(), - &event, - &mut latest_touch_pos_id, - runner.egui_ctx(), - ); - runner.input.latest_touch_pos_id = latest_touch_pos_id; - runner.input.latest_touch_pos = Some(pos); - let modifiers = runner.input.raw.modifiers; - runner.input.raw.events.push(egui::Event::PointerButton { - pos, - button: egui::PointerButton::Primary, - pressed: true, - modifiers, - }); + if let Some(pos) = primary_touch_pos(runner, &event) { + runner.input.raw.events.push(egui::Event::PointerButton { + pos, + button: egui::PointerButton::Primary, + pressed: true, + modifiers: runner.input.raw.modifiers, + }); + } push_touches(runner, egui::TouchPhase::Start, &event); runner.needs_repaint.repaint_asap(); event.stop_propagation(); event.prevent_default(); }, - )?; + ) +} - // Use `document` here to notice if the user drag outside of the canvas. - // See https://github.com/emilk/egui/issues/3157 - runner_ref.add_event_listener( - &document, - "touchmove", - |event: web_sys::TouchEvent, runner| { - let mut latest_touch_pos_id = runner.input.latest_touch_pos_id; - let pos = pos_from_touch_event( - runner.canvas(), - &event, - &mut latest_touch_pos_id, - runner.egui_ctx(), - ); - runner.input.latest_touch_pos_id = latest_touch_pos_id; - runner.input.latest_touch_pos = Some(pos); - runner.input.raw.events.push(egui::Event::PointerMoved(pos)); +fn install_touchmove(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> { + runner_ref.add_event_listener(target, "touchmove", |event: web_sys::TouchEvent, runner| { + if let Some(pos) = primary_touch_pos(runner, &event) { + if is_interested_in_pointer_event(runner.egui_ctx(), pos) { + runner.input.raw.events.push(egui::Event::PointerMoved(pos)); - push_touches(runner, egui::TouchPhase::Move, &event); - runner.needs_repaint.repaint_asap(); - event.stop_propagation(); - event.prevent_default(); - }, - )?; + push_touches(runner, egui::TouchPhase::Move, &event); + runner.needs_repaint.repaint_asap(); + event.stop_propagation(); + event.prevent_default(); + } + } + }) +} - // Use `document` here to notice if the user releases a drag outside of the canvas. - // See https://github.com/emilk/egui/issues/3157 - runner_ref.add_event_listener( - &document, - "touchend", - |event: web_sys::TouchEvent, runner| { - if let Some(pos) = runner.input.latest_touch_pos { - let modifiers = runner.input.raw.modifiers; +fn install_touchend(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> { + runner_ref.add_event_listener(target, "touchend", |event: web_sys::TouchEvent, runner| { + if let Some(pos) = primary_touch_pos(runner, &event) { + if is_interested_in_pointer_event(runner.egui_ctx(), pos) { // First release mouse to click: runner.input.raw.events.push(egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: false, - modifiers, + modifiers: runner.input.raw.modifiers, }); // Then remove hover effect: runner.input.raw.events.push(egui::Event::PointerGone); push_touches(runner, egui::TouchPhase::End, &event); - runner - .text_agent - .set_focus(runner.mutable_text_under_cursor); - runner.needs_repaint.repaint_asap(); event.stop_propagation(); event.prevent_default(); } - }, - )?; + } + }) +} +fn install_touchcancel(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> { runner_ref.add_event_listener( - &canvas, + target, "touchcancel", |event: web_sys::TouchEvent, runner| { push_touches(runner, egui::TouchPhase::Cancel, &event); @@ -491,7 +560,11 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu }, )?; - runner_ref.add_event_listener(&canvas, "wheel", |event: web_sys::WheelEvent, runner| { + Ok(()) +} + +fn install_wheel(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> { + runner_ref.add_event_listener(target, "wheel", |event: web_sys::WheelEvent, runner| { let unit = match event.delta_mode() { web_sys::WheelEvent::DOM_DELTA_PIXEL => egui::MouseWheelUnit::Point, web_sys::WheelEvent::DOM_DELTA_LINE => egui::MouseWheelUnit::Line, @@ -523,33 +596,49 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu runner.needs_repaint.repaint_asap(); event.stop_propagation(); event.prevent_default(); - })?; + }) +} - runner_ref.add_event_listener(&canvas, "dragover", |event: web_sys::DragEvent, runner| { +fn install_drag_and_drop(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> { + runner_ref.add_event_listener(target, "dragover", |event: web_sys::DragEvent, runner| { if let Some(data_transfer) = event.data_transfer() { runner.input.raw.hovered_files.clear(); - for i in 0..data_transfer.items().length() { - if let Some(item) = data_transfer.items().get(i) { + + // NOTE: data_transfer.files() is always empty in dragover + + let items = data_transfer.items(); + for i in 0..items.length() { + if let Some(item) = items.get(i) { runner.input.raw.hovered_files.push(egui::HoveredFile { mime: item.type_(), ..Default::default() }); } } + + if runner.input.raw.hovered_files.is_empty() { + // Fallback: just preview anything. Needed on Desktop Safari. + runner + .input + .raw + .hovered_files + .push(egui::HoveredFile::default()); + } + runner.needs_repaint.repaint_asap(); event.stop_propagation(); event.prevent_default(); } })?; - runner_ref.add_event_listener(&canvas, "dragleave", |event: web_sys::DragEvent, runner| { + runner_ref.add_event_listener(target, "dragleave", |event: web_sys::DragEvent, runner| { runner.input.raw.hovered_files.clear(); runner.needs_repaint.repaint_asap(); event.stop_propagation(); event.prevent_default(); })?; - runner_ref.add_event_listener(&canvas, "drop", { + runner_ref.add_event_listener(target, "drop", { let runner_ref = runner_ref.clone(); move |event: web_sys::DragEvent, runner| { diff --git a/crates/eframe/src/web/input.rs b/crates/eframe/src/web/input.rs index a1b6a802dad..a98dde61313 100644 --- a/crates/eframe/src/web/input.rs +++ b/crates/eframe/src/web/input.rs @@ -1,15 +1,15 @@ -use super::{canvas_origin, AppRunner}; +use super::{canvas_content_rect, AppRunner}; pub fn pos_from_mouse_event( canvas: &web_sys::HtmlCanvasElement, event: &web_sys::MouseEvent, ctx: &egui::Context, ) -> egui::Pos2 { - let rect = canvas.get_bounding_client_rect(); + let rect = canvas_content_rect(canvas); let zoom_factor = ctx.zoom_factor(); egui::Pos2 { - x: (event.client_x() as f32 - rect.left() as f32) / zoom_factor, - y: (event.client_y() as f32 - rect.top() as f32) / zoom_factor, + x: (event.client_x() as f32 - rect.left()) / zoom_factor, + y: (event.client_y() as f32 - rect.top()) / zoom_factor, } } @@ -27,96 +27,117 @@ pub fn button_from_mouse_event(event: &web_sys::MouseEvent) -> Option, - egui_ctx: &egui::Context, -) -> egui::Pos2 { - let touch_for_pos = if let Some(touch_id_for_pos) = touch_id_for_pos { - // search for the touch we previously used for the position - // (unfortunately, `event.touches()` is not a rust collection): - (0..event.touches().length()) - .map(|i| event.touches().get(i).unwrap()) - .find(|touch| egui::TouchId::from(touch.identifier()) == *touch_id_for_pos) - } else { - None - }; - // Use the touch found above or pick the first, or return a default position if there is no - // touch at all. (The latter is not expected as the current method is only called when there is - // at least one touch.) - touch_for_pos - .or_else(|| event.touches().get(0)) - .map_or(Default::default(), |touch| { - *touch_id_for_pos = Some(egui::TouchId::from(touch.identifier())); - pos_from_touch(canvas_origin(canvas), &touch, egui_ctx) - }) +) -> Option { + let all_touches: Vec<_> = (0..event.touches().length()) + .filter_map(|i| event.touches().get(i)) + // On touchend we don't get anything in `touches`, but we still get `changed_touches`, so include those: + .chain((0..event.changed_touches().length()).filter_map(|i| event.changed_touches().get(i))) + .collect(); + + if let Some(primary_touch) = runner.input.primary_touch { + // Is the primary touch is gone? + if !all_touches + .iter() + .any(|touch| primary_touch == egui::TouchId::from(touch.identifier())) + { + runner.input.primary_touch = None; + } + } + + if runner.input.primary_touch.is_none() { + runner.input.primary_touch = all_touches + .first() + .map(|touch| egui::TouchId::from(touch.identifier())); + } + + let primary_touch = runner.input.primary_touch; + + if let Some(primary_touch) = primary_touch { + for touch in all_touches { + if primary_touch == egui::TouchId::from(touch.identifier()) { + let canvas_rect = canvas_content_rect(runner.canvas()); + return Some(pos_from_touch(canvas_rect, &touch, runner.egui_ctx())); + } + } + } + + None } fn pos_from_touch( - canvas_origin: egui::Pos2, + canvas_rect: egui::Rect, touch: &web_sys::Touch, egui_ctx: &egui::Context, ) -> egui::Pos2 { let zoom_factor = egui_ctx.zoom_factor(); egui::Pos2 { - x: (touch.page_x() as f32 - canvas_origin.x) / zoom_factor, - y: (touch.page_y() as f32 - canvas_origin.y) / zoom_factor, + x: (touch.client_x() as f32 - canvas_rect.left()) / zoom_factor, + y: (touch.client_y() as f32 - canvas_rect.top()) / zoom_factor, } } pub fn push_touches(runner: &mut AppRunner, phase: egui::TouchPhase, event: &web_sys::TouchEvent) { - let canvas_origin = canvas_origin(runner.canvas()); + let canvas_rect = canvas_content_rect(runner.canvas()); for touch_idx in 0..event.changed_touches().length() { if let Some(touch) = event.changed_touches().item(touch_idx) { runner.input.raw.events.push(egui::Event::Touch { device_id: egui::TouchDeviceId(0), id: egui::TouchId::from(touch.identifier()), phase, - pos: pos_from_touch(canvas_origin, &touch, runner.egui_ctx()), + pos: pos_from_touch(canvas_rect, &touch, runner.egui_ctx()), force: Some(touch.force()), }); } } } -/// Web sends 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 should_ignore_key(key: &str) -> bool { +/// The text input from a keyboard event (e.g. `X` when pressing the `X` key). +pub fn text_from_keyboard_event(event: &web_sys::KeyboardEvent) -> Option { + let key = event.key(); + let is_function_key = key.starts_with('F') && key.len() > 1; - is_function_key - || matches!( - key, - "Alt" - | "ArrowDown" - | "ArrowLeft" - | "ArrowRight" - | "ArrowUp" - | "Backspace" - | "CapsLock" - | "ContextMenu" - | "Control" - | "Delete" - | "End" - | "Enter" - | "Esc" - | "Escape" - | "GroupNext" // https://github.com/emilk/egui/issues/510 - | "Help" - | "Home" - | "Insert" - | "Meta" - | "NumLock" - | "PageDown" - | "PageUp" - | "Pause" - | "ScrollLock" - | "Shift" - | "Tab" - ) + if is_function_key { + return None; + } + + let is_control_key = matches!( + key.as_str(), + "Alt" + | "ArrowDown" + | "ArrowLeft" + | "ArrowRight" + | "ArrowUp" + | "Backspace" + | "CapsLock" + | "ContextMenu" + | "Control" + | "Delete" + | "End" + | "Enter" + | "Esc" + | "Escape" + | "GroupNext" // https://github.com/emilk/egui/issues/510 + | "Help" + | "Home" + | "Insert" + | "Meta" + | "NumLock" + | "PageDown" + | "PageUp" + | "Pause" + | "ScrollLock" + | "Shift" + | "Tab" + ); + + if is_control_key { + return None; + } + + Some(key) } /// Web sends all keys as strings, so it is up to us to figure out if it is diff --git a/crates/eframe/src/web/mod.rs b/crates/eframe/src/web/mod.rs index 50e7aeb363e..07339d7e081 100644 --- a/crates/eframe/src/web/mod.rs +++ b/crates/eframe/src/web/mod.rs @@ -133,9 +133,31 @@ fn get_canvas_element_by_id_or_die(canvas_id: &str) -> web_sys::HtmlCanvasElemen .unwrap_or_else(|| panic!("Failed to find canvas with id {canvas_id:?}")) } -fn canvas_origin(canvas: &web_sys::HtmlCanvasElement) -> egui::Pos2 { - let rect = canvas.get_bounding_client_rect(); - egui::pos2(rect.left() as f32, rect.top() as f32) +/// Returns the canvas in client coordinates. +fn canvas_content_rect(canvas: &web_sys::HtmlCanvasElement) -> egui::Rect { + let bounding_rect = canvas.get_bounding_client_rect(); + + let mut rect = egui::Rect::from_min_max( + egui::pos2(bounding_rect.left() as f32, bounding_rect.top() as f32), + egui::pos2(bounding_rect.right() as f32, bounding_rect.bottom() as f32), + ); + + // We need to subtract padding and border: + if let Some(window) = web_sys::window() { + if let Ok(Some(style)) = window.get_computed_style(canvas) { + let get_property = |name: &str| -> Option { + let property = style.get_property_value(name).ok()?; + property.trim_end_matches("px").parse::().ok() + }; + + rect.min.x += get_property("padding-left").unwrap_or_default(); + rect.min.y += get_property("padding-top").unwrap_or_default(); + rect.max.x -= get_property("padding-right").unwrap_or_default(); + rect.max.y -= get_property("padding-bottom").unwrap_or_default(); + } + } + + rect } fn canvas_size_in_points(canvas: &web_sys::HtmlCanvasElement, ctx: &egui::Context) -> egui::Vec2 { @@ -171,6 +193,13 @@ fn set_clipboard_text(s: &str) { } }; wasm_bindgen_futures::spawn_local(future); + } else { + let is_secure_context = window.is_secure_context(); + if is_secure_context { + log::warn!("window.navigator.clipboard is null; can't copy text"); + } else { + log::warn!("window.navigator.clipboard is null; can't copy text, probably because we're not in a secure context. See https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts"); + } } } } diff --git a/crates/eframe/src/web/text_agent.rs b/crates/eframe/src/web/text_agent.rs index 68c150f1b0c..234dbb8ff3f 100644 --- a/crates/eframe/src/web/text_agent.rs +++ b/crates/eframe/src/web/text_agent.rs @@ -88,6 +88,11 @@ impl TextAgent { runner_ref.add_event_listener(&input, "compositionupdate", on_composition_update)?; runner_ref.add_event_listener(&input, "compositionend", on_composition_end)?; + // The canvas doesn't get keydown/keyup events when the text agent is focused, + // so we need to forward them to the runner: + runner_ref.add_event_listener(&input, "keydown", super::events::on_keydown)?; + runner_ref.add_event_listener(&input, "keyup", super::events::on_keyup)?; + Ok(Self { input, prev_ime_output: Default::default(), @@ -114,13 +119,14 @@ impl TextAgent { let Some(ime) = ime else { return Ok(()) }; - let ime_pos = ime.cursor_rect.left_top(); - let canvas_rect = canvas.get_bounding_client_rect(); - let new_pos = ime_pos + egui::vec2(canvas_rect.left() as f32, canvas_rect.top() as f32); + let canvas_rect = super::canvas_content_rect(canvas); + let cursor_rect = ime.cursor_rect.translate(canvas_rect.min.to_vec2()); let style = self.input.style(); - style.set_property("top", &format!("{}px", new_pos.y))?; - style.set_property("left", &format!("{}px", new_pos.x))?; + + // This is where the IME input will point to: + style.set_property("left", &format!("{}px", cursor_rect.center().x))?; + style.set_property("top", &format!("{}px", cursor_rect.center().y))?; Ok(()) } @@ -137,21 +143,25 @@ impl TextAgent { super::has_focus(&self.input) } - fn focus(&self) { + pub fn focus(&self) { if self.has_focus() { return; } + log::trace!("Focusing text agent"); + if let Err(err) = self.input.focus() { log::error!("failed to set focus: {}", super::string_from_js_value(&err)); }; } - fn blur(&self) { + pub fn blur(&self) { if !self.has_focus() { return; } + log::trace!("Blurring text agent"); + if let Err(err) = self.input.blur() { log::error!("failed to set focus: {}", super::string_from_js_value(&err)); }; diff --git a/crates/eframe/src/web/web_logger.rs b/crates/eframe/src/web/web_logger.rs index 18936495581..650b2e530a8 100644 --- a/crates/eframe/src/web/web_logger.rs +++ b/crates/eframe/src/web/web_logger.rs @@ -38,6 +38,8 @@ impl log::Log for WebLogger { } fn log(&self, record: &log::Record<'_>) { + #![allow(clippy::match_same_arms)] + if !self.enabled(record.metadata()) { return; } @@ -50,7 +52,9 @@ impl log::Log for WebLogger { }; match record.level() { - log::Level::Trace => console::trace(&msg), + // NOTE: the `console::trace` includes a stack trace, which is super-noisy. + log::Level::Trace => console::debug(&msg), + log::Level::Debug => console::debug(&msg), log::Level::Info => console::info(&msg), log::Level::Warn => console::warn(&msg), diff --git a/crates/eframe/src/web/web_runner.rs b/crates/eframe/src/web/web_runner.rs index eea9ab647e3..6f6cc9308db 100644 --- a/crates/eframe/src/web/web_runner.rs +++ b/crates/eframe/src/web/web_runner.rs @@ -68,12 +68,20 @@ impl WebRunner { let text_agent = TextAgent::attach(self)?; let runner = AppRunner::new(canvas_id, web_options, app_creator, text_agent).await?; + + { + // Make sure the canvas can be given focus. + // https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex + runner.canvas().set_tab_index(0); + + // Don't outline the canvas when it has focus: + runner.canvas().style().set_property("outline", "none")?; + } + self.runner.replace(Some(runner)); { - events::install_canvas_events(self)?; - events::install_document_events(self)?; - events::install_window_events(self)?; + events::install_event_handlers(self)?; if follow_system_theme { events::install_color_scheme_change_event(self)?; diff --git a/crates/egui-wgpu/src/lib.rs b/crates/egui-wgpu/src/lib.rs index 78def51dbb0..118f246540a 100644 --- a/crates/egui-wgpu/src/lib.rs +++ b/crates/egui-wgpu/src/lib.rs @@ -80,7 +80,7 @@ pub struct RenderState { } impl RenderState { - /// Creates a new `RenderState`, containing everything needed for drawing egui with wgpu. + /// Creates a new [`RenderState`], containing everything needed for drawing egui with wgpu. /// /// # Errors /// Wgpu initialization may fail due to incompatible hardware or driver for a given config. diff --git a/crates/egui-wgpu/src/renderer.rs b/crates/egui-wgpu/src/renderer.rs index 5853f563ff1..18e13a89caf 100644 --- a/crates/egui-wgpu/src/renderer.rs +++ b/crates/egui-wgpu/src/renderer.rs @@ -10,7 +10,7 @@ use wgpu::util::DeviceExt as _; /// You can use this for storage when implementing [`CallbackTrait`]. pub type CallbackResources = type_map::concurrent::TypeMap; -/// You can use this to do custom `wgpu` rendering in an egui app. +/// You can use this to do custom [`wgpu`] rendering in an egui app. /// /// Implement [`CallbackTrait`] and call [`Callback::new_paint_callback`]. /// @@ -50,7 +50,7 @@ impl Callback { /// /// ## Command Encoder /// -/// The passed-in `CommandEncoder` is egui's and can be used directly to register +/// The passed-in [`wgpu::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. @@ -58,7 +58,7 @@ impl Callback { /// ## Command Buffers /// /// 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. +/// [`wgpu::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. /// @@ -294,6 +294,7 @@ impl Renderer { // 2: uint color attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Uint32], }], + compilation_options: wgpu::PipelineCompilationOptions::default() }, primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, @@ -335,6 +336,7 @@ impl Renderer { }), write_mask: wgpu::ColorWrites::ALL, })], + compilation_options: wgpu::PipelineCompilationOptions::default() }), multiview: None, } @@ -496,7 +498,7 @@ impl Renderer { render_pass.set_scissor_rect(0, 0, size_in_pixels[0], size_in_pixels[1]); } - /// Should be called before `render()`. + /// Should be called before [`Self::render`]. pub fn update_texture( &mut self, device: &wgpu::Device, @@ -629,12 +631,11 @@ impl Renderer { self.textures.get(id) } - /// Registers a `wgpu::Texture` with a `epaint::TextureId`. + /// 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`. + /// the texture format [`wgpu::TextureFormat::Rgba8UnormSrgb`]. pub fn register_native_texture( &mut self, device: &wgpu::Device, @@ -653,9 +654,9 @@ impl Renderer { ) } - /// Registers a `wgpu::Texture` with an existing `epaint::TextureId`. + /// Registers a [`wgpu::Texture`] with an existing [`epaint::TextureId`]. /// - /// This enables applications to reuse `TextureId`s. + /// This enables applications to reuse [`epaint::TextureId`]s. pub fn update_egui_texture_from_wgpu_texture( &mut self, device: &wgpu::Device, @@ -676,15 +677,14 @@ impl Renderer { ); } - /// Registers a `wgpu::Texture` with a `epaint::TextureId` while also accepting custom - /// `wgpu::SamplerDescriptor` options. + /// 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. + /// The texture must have the format [`wgpu::TextureFormat::Rgba8UnormSrgb`]. + /// Any compare function supplied in the [`wgpu::SamplerDescriptor`] will be ignored. #[allow(clippy::needless_pass_by_value)] // false positive pub fn register_native_texture_with_sampler_options( &mut self, @@ -721,10 +721,10 @@ impl Renderer { id } - /// Registers a `wgpu::Texture` with an existing `epaint::TextureId` while also accepting custom - /// `wgpu::SamplerDescriptor` options. + /// 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. + /// This allows applications to reuse [`epaint::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, @@ -764,7 +764,7 @@ impl Renderer { } /// Uploads the uniform, vertex and index data used by the renderer. - /// Should be called before `render()`. + /// Should be called before [`Self::render`]. /// /// Returns all user-defined command buffers gathered from [`CallbackTrait::prepare`] & [`CallbackTrait::finish_prepare`] callbacks. pub fn update_buffers( diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index b40d4a80a23..f30901270e0 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -1141,6 +1141,7 @@ fn key_from_key_code(key: winit::keyboard::KeyCode) -> Option { KeyCode::BracketLeft => Key::OpenBracket, KeyCode::BracketRight => Key::CloseBracket, KeyCode::Backquote => Key::Backtick, + KeyCode::Quote => Key::Quote, KeyCode::Cut => Key::Cut, KeyCode::Copy => Key::Copy, @@ -1288,18 +1289,10 @@ pub fn process_viewport_commands( info: &mut ViewportInfo, commands: impl IntoIterator, window: &Window, - is_viewport_focused: bool, actions_requested: &mut HashSet, ) { for command in commands { - process_viewport_command( - egui_ctx, - window, - command, - info, - is_viewport_focused, - actions_requested, - ); + process_viewport_command(egui_ctx, window, command, info, actions_requested); } } @@ -1308,7 +1301,6 @@ fn process_viewport_command( window: &Window, command: ViewportCommand, info: &mut ViewportInfo, - is_viewport_focused: bool, actions_requested: &mut HashSet, ) { crate::profile_function!(); @@ -1327,12 +1319,8 @@ fn process_viewport_command( // Need to be handled elsewhere } ViewportCommand::StartDrag => { - // If `is_viewport_focused` is not checked on x11 the input will be permanently taken until the app is killed! - - // TODO(emilk): check that the left mouse-button was pressed down recently, - // or we will have bugs on Windows. - // See https://github.com/emilk/egui/pull/1108 - if is_viewport_focused { + // If `.has_focus()` is not checked on x11 the input will be permanently taken until the app is killed! + if window.has_focus() { if let Err(err) = window.drag_window() { log::warn!("{command:?}: {err}"); } diff --git a/crates/egui/src/callstack.rs b/crates/egui/src/callstack.rs index aa6f44cb08d..6b1959b0b79 100644 --- a/crates/egui/src/callstack.rs +++ b/crates/egui/src/callstack.rs @@ -10,6 +10,7 @@ struct Frame { /// /// In particular: slips everything before `egui::Context::run`, /// and skipping all frames in the `egui::` namespace. +#[inline(never)] pub fn capture() -> String { let mut frames = vec![]; let mut depth = 0; @@ -28,7 +29,7 @@ pub fn capture() -> String { let name = symbol .name() - .map(|name| name.to_string()) + .map(|name| clean_symbol_name(name.to_string())) .unwrap_or_default(); frames.push(Frame { @@ -44,12 +45,13 @@ pub fn capture() -> String { }); if frames.is_empty() { - return Default::default(); + return + "Failed to capture a backtrace. A common cause of this is compiling with panic=\"abort\" (https://github.com/rust-lang/backtrace-rs/issues/397)".to_owned(); } // Inclusive: let mut min_depth = 0; - let mut max_depth = frames.len() - 1; + let mut max_depth = usize::MAX; for frame in &frames { if frame.name.starts_with("egui::callstack::capture") { @@ -60,14 +62,23 @@ pub fn capture() -> String { } } + /// Is this the name of some sort of useful entry point? + fn is_start_name(name: &str) -> bool { + name == "main" + || name == "_main" + || name.starts_with("eframe::run_native") + || name.starts_with("egui::context::Context::run") + } + + let mut has_kept_any_start_names = false; + + frames.reverse(); // main on top, i.e. chronological order. Same as Python. + // Remove frames that are uninteresting: frames.retain(|frame| { - // Keep some special frames to give the user a sense of chronology: - if frame.name == "main" - || frame.name == "_main" - || frame.name.starts_with("egui::context::Context::run") - || frame.name.starts_with("eframe::run_native") - { + // Keep the first "start" frame we can detect (e.g. `main`) to give the user a sense of chronology: + if is_start_name(&frame.name) && !has_kept_any_start_names { + has_kept_any_start_names = true; return true; } @@ -96,8 +107,6 @@ pub fn capture() -> String { true }); - frames.reverse(); // main on top, i.e. chronological order. Same as Python. - let mut deepest_depth = 0; let mut widest_file_line = 0; for frame in &frames { @@ -135,6 +144,28 @@ pub fn capture() -> String { formatted } +fn clean_symbol_name(mut s: String) -> String { + // We get a hex suffix (at least on macOS) which is quite unhelpful, + // e.g. `my_crate::my_function::h3bedd97b1e03baa5`. + // Let's strip that. + if let Some(h) = s.rfind("::h") { + let hex = &s[h + 3..]; + if hex.len() == 16 && hex.chars().all(|c| c.is_ascii_hexdigit()) { + s.truncate(h); + } + } + + s +} + +#[test] +fn test_clean_symbol_name() { + assert_eq!( + clean_symbol_name("my_crate::my_function::h3bedd97b1e03baa5".to_owned()), + "my_crate::my_function" + ); +} + /// Shorten a path to a Rust source file from a callstack. /// /// Example input: diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index 0f764dd4b84..30d1539f3d6 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -8,21 +8,23 @@ use crate::*; /// /// Areas back [`crate::Window`]s and other floating containers, /// like tooltips and the popups of [`crate::ComboBox`]. -/// -/// Area state is intentionally NOT persisted between sessions, -/// so that a bad tooltip or menu size won't be remembered forever. -/// A resizable [`Window`] remembers the size the user picked using -/// the state in the [`Resize`] container. #[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct AreaState { /// Last known position of the pivot. - pub pivot_pos: Pos2, + pub pivot_pos: Option, /// The anchor point of the area, i.e. where on the area the [`Self::pivot_pos`] refers to. pub pivot: Align2, /// Last known size. - pub size: Vec2, + /// + /// Area size is intentionally NOT persisted between sessions, + /// so that a bad tooltip or menu size won't be remembered forever. + /// A resizable [`Window`] remembers the size the user picked using + /// the state in the [`Resize`] container. + #[cfg_attr(feature = "serde", serde(skip))] + pub size: Option, /// If false, clicks goes straight through to what is behind us. Useful for tooltips etc. pub interactable: bool, @@ -30,7 +32,20 @@ pub struct AreaState { /// At what time was this area first shown? /// /// Used to fade in the area. - pub last_became_visible_at: f64, + #[cfg_attr(feature = "serde", serde(skip))] + pub last_became_visible_at: Option, +} + +impl Default for AreaState { + fn default() -> Self { + Self { + pivot_pos: None, + pivot: Align2::LEFT_TOP, + size: None, + interactable: true, + last_became_visible_at: None, + } + } } impl AreaState { @@ -42,23 +57,27 @@ impl AreaState { /// The left top positions of the area. pub fn left_top_pos(&self) -> Pos2 { + let pivot_pos = self.pivot_pos.unwrap_or_default(); + let size = self.size.unwrap_or_default(); 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, + pivot_pos.x - self.pivot.x().to_factor() * size.x, + pivot_pos.y - self.pivot.y().to_factor() * size.y, ) } /// Move the left top positions of the area. 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, - ); + let size = self.size.unwrap_or_default(); + self.pivot_pos = Some(pos2( + pos.x + self.pivot.x().to_factor() * size.x, + pos.y + self.pivot.y().to_factor() * size.y, + )); } /// Where the area is on screen. pub fn rect(&self) -> Rect { - Rect::from_min_size(self.left_top_pos(), self.size) + let size = self.size.unwrap_or_default(); + Rect::from_min_size(self.left_top_pos(), size) } } @@ -371,16 +390,28 @@ impl Area { let layer_id = LayerId::new(order, id); - let state = AreaState::load(ctx, id).map(|mut state| { - // override the saved state with the correct value - state.pivot = pivot; - state + let state = AreaState::load(ctx, id); + let mut sizing_pass = state.is_none(); + let mut state = state.unwrap_or(AreaState { + pivot_pos: None, + pivot, + size: None, + interactable, + last_became_visible_at: None, }); - 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 + state.pivot = pivot; + state.interactable = interactable; + if let Some(new_pos) = new_pos { + state.pivot_pos = Some(new_pos); } - let mut state = state.unwrap_or_else(|| { + state.pivot_pos.get_or_insert_with(|| { + default_pos.unwrap_or_else(|| automatic_area_position(ctx, layer_id)) + }); + state.interactable = interactable; + + let size = *state.size.get_or_insert_with(|| { + sizing_pass = true; + // during the sizing pass we will use this as the max size let mut size = default_size; @@ -396,28 +427,20 @@ impl Area { size = size.at_most(constrain_rect.size()); } - AreaState { - pivot_pos: default_pos.unwrap_or_else(|| automatic_area_position(ctx)), - pivot, - size, - interactable, - last_became_visible_at: ctx.input(|i| i.time), - } + size }); - state.pivot_pos = new_pos.unwrap_or(state.pivot_pos); - state.interactable = interactable; - // TODO(emilk): if last frame was sizing pass, it should be considered invisible for smmother fade-in + // TODO(emilk): if last frame was sizing pass, it should be considered invisible for smoother fade-in let visible_last_frame = ctx.memory(|mem| mem.areas().visible_last_frame(&layer_id)); - if !visible_last_frame { - state.last_became_visible_at = ctx.input(|i| i.time); + if !visible_last_frame || state.last_became_visible_at.is_none() { + state.last_became_visible_at = Some(ctx.input(|i| i.time)); } if let Some((anchor, offset)) = anchor { state.set_left_top_pos( anchor - .align_size_within_rect(state.size, constrain_rect) + .align_size_within_rect(size, constrain_rect) .left_top() + offset, ); @@ -446,7 +469,9 @@ impl Area { }); if movable && move_response.dragged() { - state.pivot_pos += move_response.drag_delta(); + if let Some(pivot_pos) = &mut state.pivot_pos { + *pivot_pos += move_response.drag_delta(); + } } if (move_response.dragged() || move_response.clicked()) @@ -481,7 +506,7 @@ impl Area { enabled, constrain, constrain_rect, - sizing_pass: is_new, + sizing_pass, fade_in, } } @@ -505,7 +530,7 @@ impl Prepared { } pub(crate) fn content_ui(&self, ctx: &Context) -> Ui { - let max_rect = Rect::from_min_size(self.state.left_top_pos(), self.state.size); + let max_rect = self.state.rect(); let clip_rect = self.constrain_rect; // Don't paint outside our bounds @@ -519,13 +544,14 @@ impl Prepared { ); if self.fade_in { - let age = - ctx.input(|i| (i.time - self.state.last_became_visible_at) as f32 + i.predicted_dt); - let opacity = crate::remap_clamp(age, 0.0..=ctx.style().animation_time, 0.0..=1.0); - let opacity = emath::easing::cubic_out(opacity); // slow fade-out = quick fade-in - ui.multiply_opacity(opacity); - if opacity < 1.0 { - ctx.request_repaint(); + if let Some(last_became_visible_at) = self.state.last_became_visible_at { + let age = ctx.input(|i| (i.time - last_became_visible_at) as f32 + i.predicted_dt); + let opacity = crate::remap_clamp(age, 0.0..=ctx.style().animation_time, 0.0..=1.0); + let opacity = emath::easing::cubic_out(opacity); // slow fade-out = quick fade-in + ui.multiply_opacity(opacity); + if opacity < 1.0 { + ctx.request_repaint(); + } } } @@ -544,15 +570,27 @@ impl Prepared { kind: _, layer_id, mut state, - move_response, + move_response: mut response, + sizing_pass, .. } = self; - state.size = content_ui.min_size(); + state.size = Some(content_ui.min_size()); + + // Make sure we report back the correct size. + // Very important after the initial sizing pass, when the initial estimate of the size is way off. + let final_rect = state.rect(); + response.rect = final_rect; + response.interact_rect = final_rect; ctx.memory_mut(|m| m.areas_mut().set_state(layer_id, state)); - move_response + if sizing_pass { + // If we didn't know the size, we were likely drawing the area in the wrong place. + ctx.request_repaint(); + } + + response } } @@ -565,12 +603,13 @@ fn pointer_pressed_on_area(ctx: &Context, layer_id: LayerId) -> bool { } } -fn automatic_area_position(ctx: &Context) -> Pos2 { +fn automatic_area_position(ctx: &Context, layer_id: LayerId) -> Pos2 { let mut existing: Vec = ctx.memory(|mem| { mem.areas() .visible_windows() - .into_iter() - .map(AreaState::rect) + .filter(|(id, _)| id != &layer_id) // ignore ourselves + .filter(|(_, state)| state.pivot_pos.is_some() && state.size.is_some()) + .map(|(_, state)| state.rect()) .collect() }); existing.sort_by_key(|r| r.left().round() as i32); diff --git a/crates/egui/src/containers/collapsing_header.rs b/crates/egui/src/containers/collapsing_header.rs index 388293d19e2..e0c298980f3 100644 --- a/crates/egui/src/containers/collapsing_header.rs +++ b/crates/egui/src/containers/collapsing_header.rs @@ -540,8 +540,9 @@ impl CollapsingHeader { header_response.mark_changed(); } - header_response - .widget_info(|| WidgetInfo::labeled(WidgetType::CollapsingHeader, galley.text())); + header_response.widget_info(|| { + WidgetInfo::labeled(WidgetType::CollapsingHeader, ui.is_enabled(), galley.text()) + }); let openness = state.openness(ui.ctx()); diff --git a/crates/egui/src/containers/combo_box.rs b/crates/egui/src/containers/combo_box.rs index f4c18b51b92..cb29f277644 100644 --- a/crates/egui/src/containers/combo_box.rs +++ b/crates/egui/src/containers/combo_box.rs @@ -213,12 +213,13 @@ impl ComboBox { (width, height), ); if let Some(label) = label { - ir.response - .widget_info(|| WidgetInfo::labeled(WidgetType::ComboBox, label.text())); + ir.response.widget_info(|| { + WidgetInfo::labeled(WidgetType::ComboBox, ui.is_enabled(), label.text()) + }); ir.response |= ui.label(label); } else { ir.response - .widget_info(|| WidgetInfo::labeled(WidgetType::ComboBox, "")); + .widget_info(|| WidgetInfo::labeled(WidgetType::ComboBox, ui.is_enabled(), "")); } ir }) @@ -296,7 +297,12 @@ fn combo_box_dyn<'c, R>( 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 popup_height = ui.memory(|m| { + m.areas() + .get(popup_id) + .and_then(|state| state.size) + .map_or(100.0, |size| size.y) + }); let above_or_below = if ui.next_widget_position().y + ui.spacing().interact_size.y + popup_height @@ -380,6 +386,7 @@ fn combo_box_dyn<'c, R>( popup_id, &button_response, above_or_below, + PopupCloseBehavior::CloseOnClick, |ui| { ScrollArea::vertical() .max_height(height) diff --git a/crates/egui/src/containers/panel.rs b/crates/egui/src/containers/panel.rs index ada352448c0..6815d45a365 100644 --- a/crates/egui/src/containers/panel.rs +++ b/crates/egui/src/containers/panel.rs @@ -271,6 +271,8 @@ impl SidePanel { })), ); panel_ui.expand_to_include_rect(panel_rect); + panel_ui.set_clip_rect(panel_rect); // If we overflow, don't do so visibly (#4475) + let frame = frame.unwrap_or_else(|| Frame::side_top_panel(ui.style())); let inner_response = frame.show(&mut panel_ui, |ui| { ui.set_min_height(ui.max_rect().height()); // Make sure the frame fills the full height @@ -335,7 +337,7 @@ impl SidePanel { // (hence the shrink). let resize_x = side.opposite().side_x(rect.shrink(1.0)); let resize_x = ui.painter().round_to_pixel(resize_x); - ui.painter().vline(resize_x, rect.y_range(), stroke); + ui.painter().vline(resize_x, panel_rect.y_range(), stroke); } inner_response @@ -749,6 +751,8 @@ impl TopBottomPanel { })), ); panel_ui.expand_to_include_rect(panel_rect); + panel_ui.set_clip_rect(panel_rect); // If we overflow, don't do so visibly (#4475) + let frame = frame.unwrap_or_else(|| Frame::side_top_panel(ui.style())); let inner_response = frame.show(&mut panel_ui, |ui| { ui.set_min_width(ui.max_rect().width()); // Make the frame fill full width @@ -814,7 +818,7 @@ impl TopBottomPanel { // (hence the shrink). let resize_y = side.opposite().side_y(rect.shrink(1.0)); let resize_y = ui.painter().round_to_pixel(resize_y); - ui.painter().hline(rect.x_range(), resize_y, stroke); + ui.painter().hline(panel_rect.x_range(), resize_y, stroke); } inner_response @@ -1078,6 +1082,7 @@ impl CentralPanel { Layout::top_down(Align::Min), Some(UiStackInfo::new(UiKind::CentralPanel)), ); + panel_ui.set_clip_rect(panel_rect); // If we overflow, don't do so visibly (#4475) let frame = frame.unwrap_or_else(|| Frame::central_panel(ui.style())); frame.show(&mut panel_ui, |ui| { diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index 5f4a9d6203a..e287444b295 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -118,8 +118,9 @@ fn show_tooltip_at_avoid_dyn<'c, R>( }); let tooltip_area_id = tooltip_id(widget_id, state.tooltip_count); - let expected_tooltip_size = - AreaState::load(ctx, tooltip_area_id).map_or(vec2(64.0, 32.0), |area| area.size); + let expected_tooltip_size = AreaState::load(ctx, tooltip_area_id) + .and_then(|area| area.size) + .unwrap_or(vec2(64.0, 32.0)); let screen_rect = ctx.screen_rect(); @@ -253,11 +254,29 @@ pub fn was_tooltip_open_last_frame(ctx: &Context, widget_id: Id) -> bool { }) } +/// Determines popup's close behavior +#[derive(Clone, Copy)] +pub enum PopupCloseBehavior { + /// Popup will be closed on click anywhere, inside or outside the popup. + /// + /// It is used in [`ComboBox`]. + CloseOnClick, + + /// Popup will be closed if the click happened somewhere else + /// but in the popup's body + CloseOnClickOutside, + + /// Clicks will be ignored. Popup might be closed manually by calling [`Memory::close_popup`] + /// or by pressing the escape button + IgnoreClicks, +} + /// Helper for [`popup_above_or_below_widget`]. pub fn popup_below_widget( ui: &Ui, popup_id: Id, widget_response: &Response, + close_behavior: PopupCloseBehavior, add_contents: impl FnOnce(&mut Ui) -> R, ) -> Option { popup_above_or_below_widget( @@ -265,6 +284,7 @@ pub fn popup_below_widget( popup_id, widget_response, AboveOrBelow::Below, + close_behavior, add_contents, ) } @@ -287,7 +307,8 @@ pub fn popup_below_widget( /// ui.memory_mut(|mem| mem.toggle_popup(popup_id)); /// } /// let below = egui::AboveOrBelow::Below; -/// egui::popup::popup_above_or_below_widget(ui, popup_id, &response, below, |ui| { +/// let close_on_click_outside = egui::popup::PopupCloseBehavior::CloseOnClickOutside; +/// egui::popup::popup_above_or_below_widget(ui, popup_id, &response, below, close_on_click_outside, |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("…"); @@ -299,19 +320,26 @@ pub fn popup_above_or_below_widget( popup_id: Id, widget_response: &Response, above_or_below: AboveOrBelow, + close_behavior: PopupCloseBehavior, add_contents: impl FnOnce(&mut Ui) -> R, ) -> Option { if parent_ui.memory(|mem| mem.is_popup_open(popup_id)) { - let (pos, pivot) = match above_or_below { + let (mut 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), }; + if let Some(transform) = parent_ui + .ctx() + .memory(|m| m.layer_transforms.get(&parent_ui.layer_id()).copied()) + { + pos = transform * pos; + } let frame = Frame::popup(parent_ui.style()); let frame_margin = frame.total_margin(); let inner_width = widget_response.rect.width() - frame_margin.sum().x; - let inner = Area::new(popup_id) + let response = Area::new(popup_id) .kind(UiKind::Popup) .order(Order::Foreground) .fixed_pos(pos) @@ -327,13 +355,20 @@ pub fn popup_above_or_below_widget( .inner }) .inner - }) - .inner; + }); + + let should_close = match close_behavior { + PopupCloseBehavior::CloseOnClick => widget_response.clicked_elsewhere(), + PopupCloseBehavior::CloseOnClickOutside => { + widget_response.clicked_elsewhere() && response.response.clicked_elsewhere() + } + PopupCloseBehavior::IgnoreClicks => false, + }; - if parent_ui.input(|i| i.key_pressed(Key::Escape)) || widget_response.clicked_elsewhere() { + if parent_ui.input(|i| i.key_pressed(Key::Escape)) || should_close { parent_ui.memory_mut(|mem| mem.close_popup()); } - Some(inner) + Some(response.inner) } else { None } diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index 536b3c33a94..e564fbe1e4a 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -966,17 +966,22 @@ impl Prepared { // top/bottom of a horizontal scroll (d==0). // left/rigth of a vertical scroll (d==1). let mut cross = if scroll_style.floating { + // The bounding rect of a fully visible bar. + // When we hover this area, we should show the full bar: let max_bar_rect = if d == 0 { - outer_rect.with_min_y(outer_rect.max.y - scroll_style.allocated_width()) + outer_rect.with_min_y(outer_rect.max.y - outer_margin - scroll_style.bar_width) } else { - outer_rect.with_min_x(outer_rect.max.x - scroll_style.allocated_width()) + outer_rect.with_min_x(outer_rect.max.x - outer_margin - scroll_style.bar_width) }; + let is_hovering_bar_area = is_hovering_outer_rect && ui.rect_contains_pointer(max_bar_rect) || state.scroll_bar_interaction[d]; + let is_hovering_bar_area_t = ui .ctx() .animate_bool_responsive(id.with((d, "bar_hover")), is_hovering_bar_area); + let width = show_factor * lerp( scroll_style.floating_width..=scroll_style.bar_width, diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index 0c8847ae0c7..15fc8d80572 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -59,7 +59,7 @@ impl<'open> Window<'open> { .with_stroke(false) .min_size([96.0, 32.0]) .default_size([340.0, 420.0]), // Default inner size of a window - scroll: ScrollArea::neither(), + scroll: ScrollArea::neither().auto_shrink(false), collapsible: true, default_open: true, with_title_bar: true, diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 4e031b2159a..4643b17d931 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -495,11 +495,11 @@ impl ContextImpl { self.memory.areas_mut().set_state( LayerId::background(), AreaState { - pivot_pos: screen_rect.left_top(), + pivot_pos: Some(screen_rect.left_top()), pivot: Align2::LEFT_TOP, - size: screen_rect.size(), + size: Some(screen_rect.size()), interactable: true, - last_became_visible_at: f64::NEG_INFINITY, + last_became_visible_at: None, }, ); @@ -2026,8 +2026,10 @@ impl ContextImpl { viewport.widgets_this_frame.clear(); } - if repaint_needed || viewport.input.wants_repaint() { + if repaint_needed { self.request_repaint(ended_viewport_id, RepaintCause::new()); + } else if let Some(delay) = viewport.input.wants_repaint_after() { + self.request_repaint_after(delay, ended_viewport_id, RepaintCause::new()); } // ------------------- @@ -2207,7 +2209,7 @@ impl Context { pub fn used_rect(&self) -> Rect { self.write(|ctx| { let mut used = ctx.viewport().frame_state.used_by_panels; - for window in ctx.memory.areas().visible_windows() { + for (_id, window) in ctx.memory.areas().visible_windows() { used = used.union(window.rect()); } used @@ -2374,6 +2376,17 @@ impl Context { self.memory_mut(|mem| mem.areas_mut().move_to_top(layer_id)); } + /// Mark the `child` layer as a sublayer of `parent`. + /// + /// Sublayers are moved directly above the parent layer at the end of the frame. This is mainly + /// intended for adding a new [`Area`] inside a [`Window`]. + /// + /// This currently only supports one level of nesting. If `parent` is a sublayer of another + /// layer, the behavior is unspecified. + pub fn set_sublayer(&self, parent: LayerId, child: LayerId) { + self.memory_mut(|mem| mem.areas_mut().set_sublayer(parent, child)); + } + /// Retrieve the [`LayerId`] of the top level windows. pub fn top_layer_id(&self) -> Option { self.memory(|mem| mem.areas().top_layer_id(Order::Middle)) diff --git a/crates/egui/src/data/input.rs b/crates/egui/src/data/input.rs index 3ee88a49cec..5041034f67b 100644 --- a/crates/egui/src/data/input.rs +++ b/crates/egui/src/data/input.rs @@ -544,7 +544,7 @@ pub const NUM_POINTER_BUTTONS: usize = 5; /// 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)] +#[derive(Clone, Copy, 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). @@ -567,6 +567,40 @@ pub struct Modifiers { pub command: bool, } +impl std::fmt::Debug for Modifiers { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.is_none() { + return write!(f, "Modifiers::NONE"); + } + + let Self { + alt, + ctrl, + shift, + mac_cmd, + command, + } = *self; + + let mut debug = f.debug_struct("Modifiers"); + if alt { + debug.field("alt", &true); + } + if ctrl { + debug.field("ctrl", &true); + } + if shift { + debug.field("shift", &true); + } + if mac_cmd { + debug.field("mac_cmd", &true); + } + if command { + debug.field("command", &true); + } + debug.finish() + } +} + impl Modifiers { pub const NONE: Self = Self { alt: false, diff --git a/crates/egui/src/data/key.rs b/crates/egui/src/data/key.rs index 66fe6b9336f..c43d1c5685d 100644 --- a/crates/egui/src/data/key.rs +++ b/crates/egui/src/data/key.rs @@ -1,10 +1,12 @@ /// Keyboard keys. /// -/// egui usually uses logical keys, i.e. after applying any user keymap. -// TODO(emilk): split into `LogicalKey` and `PhysicalKey` +/// egui usually uses logical keys, i.e. after applying any user keymap.\ +// See comment at the end of `Key { … }` on how to add new keys. #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub enum Key { + // ---------------------------------------------- + // Commands: ArrowDown, ArrowLeft, ArrowRight, @@ -71,36 +73,39 @@ pub enum Key { /// `;` Semicolon, + /// `'` + Quote, + // ---------------------------------------------- // Digits: - /// Either from the main row or from the numpad. + /// `0` (from main row or numpad) Num0, - /// Either from the main row or from the numpad. + /// `1` (from main row or numpad) Num1, - /// Either from the main row or from the numpad. + /// `2` (from main row or numpad) Num2, - /// Either from the main row or from the numpad. + /// `3` (from main row or numpad) Num3, - /// Either from the main row or from the numpad. + /// `4` (from main row or numpad) Num4, - /// Either from the main row or from the numpad. + /// `5` (from main row or numpad) Num5, - /// Either from the main row or from the numpad. + /// `6` (from main row or numpad) Num6, - /// Either from the main row or from the numpad. + /// `7` (from main row or numpad) Num7, - /// Either from the main row or from the numpad. + /// `8` (from main row or numpad) Num8, - /// Either from the main row or from the numpad. + /// `9` (from main row or numpad) Num9, // ---------------------------------------------- @@ -169,14 +174,19 @@ pub enum Key { F33, F34, F35, - // When adding keys, remember to also update `crates/egui-winit/src/lib.rs` - // and [`Self::ALL`]. + // When adding keys, remember to also update: + // * crates/egui-winit/src/lib.rs + // * Key::ALL + // * Key::from_name + // You should test that it works using the "Input Event History" window in the egui demo app. + // Make sure to test both natively and on web! // Also: don't add keys last; add them to the group they best belong to. } impl Key { /// All egui keys pub const ALL: &'static [Self] = &[ + // Commands: Self::ArrowDown, Self::ArrowLeft, Self::ArrowRight, @@ -210,6 +220,7 @@ impl Key { Self::Slash, Self::Pipe, Self::Questionmark, + Self::Quote, // Digits: Self::Num0, Self::Num1, @@ -333,6 +344,7 @@ impl Key { "[" | "OpenBracket" => Self::OpenBracket, "]" | "CloseBracket" => Self::CloseBracket, "`" | "Backtick" | "Backquote" | "Grave" => Self::Backtick, + "'" | "Quote" => Self::Quote, "0" | "Digit0" | "Numpad0" => Self::Num0, "1" | "Digit1" | "Numpad1" => Self::Num1, @@ -481,6 +493,7 @@ impl Key { Self::OpenBracket => "OpenBracket", Self::CloseBracket => "CloseBracket", Self::Backtick => "Backtick", + Self::Quote => "Quote", Self::Num0 => "0", Self::Num1 => "1", diff --git a/crates/egui/src/data/output.rs b/crates/egui/src/data/output.rs index f9acce36d1f..93b3aab606f 100644 --- a/crates/egui/src/data/output.rs +++ b/crates/egui/src/data/output.rs @@ -556,8 +556,9 @@ impl WidgetInfo { } #[allow(clippy::needless_pass_by_value)] - pub fn labeled(typ: WidgetType, label: impl ToString) -> Self { + pub fn labeled(typ: WidgetType, enabled: bool, label: impl ToString) -> Self { Self { + enabled, label: Some(label.to_string()), ..Self::new(typ) } @@ -565,25 +566,28 @@ impl WidgetInfo { /// checkboxes, radio-buttons etc #[allow(clippy::needless_pass_by_value)] - pub fn selected(typ: WidgetType, selected: bool, label: impl ToString) -> Self { + pub fn selected(typ: WidgetType, enabled: bool, selected: bool, label: impl ToString) -> Self { Self { + enabled, label: Some(label.to_string()), selected: Some(selected), ..Self::new(typ) } } - pub fn drag_value(value: f64) -> Self { + pub fn drag_value(enabled: bool, value: f64) -> Self { Self { + enabled, value: Some(value), ..Self::new(WidgetType::DragValue) } } #[allow(clippy::needless_pass_by_value)] - pub fn slider(value: f64, label: impl ToString) -> Self { + pub fn slider(enabled: bool, value: f64, label: impl ToString) -> Self { let label = label.to_string(); Self { + enabled, label: if label.is_empty() { None } else { Some(label) }, value: Some(value), ..Self::new(WidgetType::Slider) @@ -591,7 +595,11 @@ impl WidgetInfo { } #[allow(clippy::needless_pass_by_value)] - pub fn text_edit(prev_text_value: impl ToString, text_value: impl ToString) -> Self { + pub fn text_edit( + enabled: bool, + prev_text_value: impl ToString, + text_value: impl ToString, + ) -> Self { let text_value = text_value.to_string(); let prev_text_value = prev_text_value.to_string(); let prev_text_value = if text_value == prev_text_value { @@ -600,6 +608,7 @@ impl WidgetInfo { Some(prev_text_value) }; Self { + enabled, current_text_value: Some(text_value), prev_text_value, ..Self::new(WidgetType::TextEdit) @@ -608,10 +617,12 @@ impl WidgetInfo { #[allow(clippy::needless_pass_by_value)] pub fn text_selection_changed( + enabled: bool, text_selection: std::ops::RangeInclusive, current_text_value: impl ToString, ) -> Self { Self { + enabled, text_selection: Some(text_selection), current_text_value: Some(current_text_value.to_string()), ..Self::new(WidgetType::TextEdit) diff --git a/crates/egui/src/drag_and_drop.rs b/crates/egui/src/drag_and_drop.rs index b0c26ac8489..285fc403f32 100644 --- a/crates/egui/src/drag_and_drop.rs +++ b/crates/egui/src/drag_and_drop.rs @@ -27,14 +27,15 @@ impl DragAndDrop { } fn end_frame(ctx: &Context) { - let pointer_released = ctx.input(|i| i.pointer.any_released()); + let abort_dnd = + ctx.input(|i| i.pointer.any_released() || i.key_pressed(crate::Key::Escape)); let mut is_dragging = false; ctx.data_mut(|data| { let state = data.get_temp_mut_or_default::(Id::NULL); - if pointer_released { + if abort_dnd { state.payload = None; } diff --git a/crates/egui/src/hit_test.rs b/crates/egui/src/hit_test.rs index 62a762aa52d..42099d248cc 100644 --- a/crates/egui/src/hit_test.rs +++ b/crates/egui/src/hit_test.rs @@ -58,6 +58,10 @@ pub fn hit_test( .filter(|layer| layer.order.allow_interaction()) .flat_map(|&layer_id| widgets.get_layer(layer_id)) .filter(|&w| { + if w.interact_rect.is_negative() { + return false; + } + let pos_in_layer = pos_in_layers.get(&w.layer_id).copied().unwrap_or(pos); let dist_sq = w.interact_rect.distance_sq_to_pos(pos_in_layer); @@ -311,6 +315,10 @@ fn find_closest(widgets: impl Iterator, pos: Pos2) -> Option< let mut closest = None; let mut closest_dist_sq = f32::INFINITY; for widget in widgets { + if widget.interact_rect.is_negative() { + continue; + } + let dist_sq = widget.interact_rect.distance_sq_to_pos(pos); // In case of a tie, take the last one = the one on top. diff --git a/crates/egui/src/input_state.rs b/crates/egui/src/input_state.rs index f52bfe24928..e2fd693b786 100644 --- a/crates/egui/src/input_state.rs +++ b/crates/egui/src/input_state.rs @@ -2,7 +2,10 @@ mod touch_state; use crate::data::input::*; use crate::{emath::*, util::History}; -use std::collections::{BTreeMap, HashSet}; +use std::{ + collections::{BTreeMap, HashSet}, + time::Duration, +}; pub use crate::Key; pub use touch_state::MultiTouchInfo; @@ -389,15 +392,30 @@ impl InputState { } /// The [`crate::Context`] will call this at the end of each frame to see if we need a repaint. - pub fn wants_repaint(&self) -> bool { - self.pointer.wants_repaint() + /// + /// Returns how long to wait for a repaint. + pub fn wants_repaint_after(&self) -> Option { + if self.pointer.wants_repaint() || self.unprocessed_scroll_delta.abs().max_elem() > 0.2 || self.unprocessed_scroll_delta_for_zoom.abs() > 0.2 || !self.events.is_empty() + { + // Immediate repaint + return Some(Duration::ZERO); + } + + if self.any_touches() && !self.pointer.is_decidedly_dragging() { + // We need to wake up and check for press-and-hold for the context menu. + if let Some(press_start_time) = self.pointer.press_start_time { + let press_duration = self.time - press_start_time; + if press_duration < MAX_CLICK_DURATION { + let secs_until_menu = MAX_CLICK_DURATION - press_duration; + return Some(Duration::from_secs_f64(secs_until_menu)); + } + } + } - // We need to wake up and check for press-and-hold for the context menu. - // TODO(emilk): wake up after `MAX_CLICK_DURATION` instead of every frame. - || (self.any_touches() && !self.pointer.is_decidedly_dragging()) + None } /// Count presses of a key. If non-zero, the presses are consumed, so that this will only return non-zero once. @@ -1208,7 +1226,7 @@ impl InputState { ui.collapsing("Raw Input", |ui| raw.ui(ui)); crate::containers::CollapsingHeader::new("🖱 Pointer") - .default_open(true) + .default_open(false) .show(ui, |ui| { pointer.ui(ui); }); diff --git a/crates/egui/src/interaction.rs b/crates/egui/src/interaction.rs index 8a7b2d0948c..06b19a0b719 100644 --- a/crates/egui/src/interaction.rs +++ b/crates/egui/src/interaction.rs @@ -134,6 +134,12 @@ pub(crate) fn interact( let mut dragged = prev_snapshot.dragged; let mut long_touched = None; + if input.key_pressed(Key::Escape) { + // Abort dragging on escape + dragged = None; + interaction.potential_drag_id = None; + } + if input.is_long_touch() { // We implement "press-and-hold for context menu" on touch screens here if let Some(widget) = interaction diff --git a/crates/egui/src/introspection.rs b/crates/egui/src/introspection.rs index d853b1c6196..7a4c98c2ba9 100644 --- a/crates/egui/src/introspection.rs +++ b/crates/egui/src/introspection.rs @@ -155,7 +155,7 @@ impl Widget for &mut epaint::TessellationOptions { .on_hover_text("Apply feathering to smooth out the edges of shapes. Turn off for small performance gain."); if *feathering { - ui.add(crate::DragValue::new(feathering_size_in_pixels).clamp_range(0.0..=10.0).speed(0.1).suffix(" px")); + ui.add(crate::DragValue::new(feathering_size_in_pixels).range(0.0..=10.0).speed(0.1).suffix(" px")); } }); @@ -165,7 +165,7 @@ impl Widget for &mut epaint::TessellationOptions { ui.label("Spline tolerance"); let speed = 0.01 * *bezier_tolerance; ui.add( - crate::DragValue::new(bezier_tolerance).clamp_range(0.0001..=10.0) + crate::DragValue::new(bezier_tolerance).range(0.0001..=10.0) .speed(speed) ); }); diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 3358e09b18b..a3f3d729567 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -3,7 +3,7 @@ //! Try the live web demo: . Read more about egui at . //! //! `egui` is in heavy development, with each new version having breaking changes. -//! You need to have rust 1.62.0 or later to use `egui`. +//! You need to have rust 1.76.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). diff --git a/crates/egui/src/memory.rs b/crates/egui/src/memory.rs index 84925e1cf6e..a09f0b57e31 100644 --- a/crates/egui/src/memory.rs +++ b/crates/egui/src/memory.rs @@ -1,6 +1,6 @@ #![warn(missing_docs)] // Let's keep this file well-documented.` to memory.rs -use ahash::HashMap; +use ahash::{HashMap, HashSet}; use epaint::emath::TSTransform; use crate::{ @@ -337,16 +337,16 @@ impl Options { .show(ui, |ui| { ui.horizontal(|ui| { ui.label("Line scroll speed"); - ui.add( - crate::DragValue::new(line_scroll_speed).clamp_range(0.0..=f32::INFINITY), - ) - .on_hover_text("How many lines to scroll with each tick of the mouse wheel"); + ui.add(crate::DragValue::new(line_scroll_speed).range(0.0..=f32::INFINITY)) + .on_hover_text( + "How many lines to scroll with each tick of the mouse wheel", + ); }); ui.horizontal(|ui| { ui.label("Scroll zoom speed"); ui.add( crate::DragValue::new(scroll_zoom_speed) - .clamp_range(0.0..=f32::INFINITY) + .range(0.0..=f32::INFINITY) .speed(0.001), ) .on_hover_text("How fast to zoom with ctrl/cmd + scroll"); @@ -934,9 +934,6 @@ impl Memory { #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] pub struct Areas { - /// Area state is intentionally NOT persisted between sessions, - /// so that a bad tooltip or menu size won't be remembered forever. - #[cfg_attr(feature = "serde", serde(skip))] areas: IdMap, /// Back-to-front. Top is last. @@ -951,6 +948,11 @@ pub struct Areas { /// 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: ahash::HashSet, + + /// List of sublayers for each layer + /// + /// When a layer has sublayers, they are moved directly above it in the ordering. + sublayers: ahash::HashMap>, } impl Areas { @@ -1025,12 +1027,12 @@ impl Areas { .collect() } - pub(crate) fn visible_windows(&self) -> Vec<&area::AreaState> { + pub(crate) fn visible_windows(&self) -> impl Iterator { self.visible_layer_ids() - .iter() + .into_iter() .filter(|layer| layer.order == crate::Order::Middle) - .filter_map(|layer| self.get(layer.id)) - .collect() + .filter(|&layer| !self.is_sublayer(&layer)) + .filter_map(|layer| Some((layer, self.get(layer.id)?))) } pub fn move_to_top(&mut self, layer_id: LayerId) { @@ -1042,20 +1044,38 @@ impl Areas { } } + /// Mark the `child` layer as a sublayer of `parent`. + /// + /// Sublayers are moved directly above the parent layer at the end of the frame. This is mainly + /// intended for adding a new [Area](crate::Area) inside a [Window](crate::Window). + /// + /// This currently only supports one level of nesting. If `parent` is a sublayer of another + /// layer, the behavior is unspecified. + pub fn set_sublayer(&mut self, parent: LayerId, child: LayerId) { + self.sublayers.entry(parent).or_default().insert(child); + } + pub fn top_layer_id(&self, order: Order) -> Option { self.order .iter() - .filter(|layer| layer.order == order) + .filter(|layer| layer.order == order && !self.is_sublayer(layer)) .last() .copied() } + pub(crate) fn is_sublayer(&self, layer: &LayerId) -> bool { + self.sublayers + .iter() + .any(|(_, children)| children.contains(layer)) + } + pub(crate) fn end_frame(&mut self) { let Self { visible_last_frame, visible_current_frame, order, wants_to_be_on_top, + sublayers, .. } = self; @@ -1063,6 +1083,23 @@ impl Areas { visible_current_frame.clear(); order.sort_by_key(|layer| (layer.order, wants_to_be_on_top.contains(layer))); wants_to_be_on_top.clear(); + // For all layers with sublayers, put the sublayers directly after the parent layer: + let sublayers = std::mem::take(sublayers); + for (parent, children) in sublayers { + let mut moved_layers = vec![parent]; + order.retain(|l| { + if children.contains(l) { + moved_layers.push(*l); + false + } else { + true + } + }); + let Some(parent_pos) = order.iter().position(|l| l == &parent) else { + continue; + }; + order.splice(parent_pos..=parent_pos, moved_layers); + } } } diff --git a/crates/egui/src/menu.rs b/crates/egui/src/menu.rs index 01c3e2bf213..4980f6f9823 100644 --- a/crates/egui/src/menu.rs +++ b/crates/egui/src/menu.rs @@ -168,15 +168,17 @@ fn menu_popup<'c, R>( .inner }); + let area_rect = area_response.response.rect; + menu_state_arc.write().rect = if sizing_pass { // During the sizing pass we didn't know the size yet, // so we might have just constrained the position unnecessarily. // Therefore keep the original=desired position until the next frame. - Rect::from_min_size(pos, area_response.response.rect.size()) + Rect::from_min_size(pos, area_rect.size()) } else { // We knew the size, and this is where it ended up (potentially constrained to screen). // Remember it for the future: - area_response.response.rect + area_rect }; area_response @@ -269,7 +271,7 @@ impl MenuRootManager { ) -> Option> { if let Some(root) = self.inner.as_mut() { let (menu_response, inner_response) = root.show(button, add_contents); - if MenuResponse::Close == menu_response { + if menu_response.is_close() { self.inner = None; } inner_response @@ -321,7 +323,8 @@ impl MenuRoot { let inner_response = menu_popup(&button.ctx, &self.menu_state, self.id, add_contents); let menu_state = self.menu_state.read(); - if menu_state.response.is_close() { + let escape_pressed = button.ctx.input(|i| i.key_pressed(Key::Escape)); + if menu_state.response.is_close() || escape_pressed { return (MenuResponse::Close, Some(inner_response)); } } @@ -363,6 +366,13 @@ impl MenuRoot { } } + if let Some(transform) = button + .ctx + .memory(|m| m.layer_transforms.get(&button.layer_id).copied()) + { + pos = transform * pos; + } + return MenuResponse::Create(pos, id); } else if button .ctx @@ -514,7 +524,11 @@ impl SubMenuButton { 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, + ui.is_enabled(), + text_galley.text(), + ) }); if ui.is_rect_visible(rect) { diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 64b2ff19842..aee287176f2 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -468,7 +468,7 @@ impl Response { .ctx .memory(|m| m.layer_transforms.get(&self.layer_id).copied()) { - pos = transform * pos; + pos = transform.inverse() * pos; } Some(pos) } else { @@ -562,7 +562,14 @@ impl Response { /// /// This can be used to give attention to a widget during a tutorial. pub fn show_tooltip_ui(&self, add_contents: impl FnOnce(&mut Ui)) { - crate::containers::show_tooltip_for(&self.ctx, self.id, &self.rect, add_contents); + let mut rect = self.rect; + if let Some(transform) = self + .ctx + .memory(|m| m.layer_transforms.get(&self.layer_id).copied()) + { + rect = transform * rect; + } + crate::containers::show_tooltip_for(&self.ctx, self.id, &rect, add_contents); } /// Always show this tooltip, even if disabled and the user isn't hovering it. @@ -873,6 +880,9 @@ impl Response { #[cfg(feature = "accesskit")] pub(crate) fn fill_accesskit_node_common(&self, builder: &mut accesskit::NodeBuilder) { + if !self.enabled { + builder.set_disabled(); + } builder.set_bounds(accesskit::Rect { x0: self.rect.min.x.into(), y0: self.rect.min.y.into(), @@ -914,6 +924,9 @@ impl Response { WidgetType::ProgressIndicator => Role::ProgressIndicator, WidgetType::Other => Role::Unknown, }); + if !info.enabled { + builder.set_disabled(); + } if let Some(label) = info.label { builder.set_name(label); } diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 770e2a61cbf..cfcf3905215 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -2,7 +2,7 @@ #![allow(clippy::if_same_then_else)] -use std::collections::BTreeMap; +use std::{collections::BTreeMap, ops::RangeInclusive, sync::Arc}; use epaint::{Rounding, Shadow, Stroke}; @@ -11,6 +11,51 @@ use crate::{ RichText, WidgetText, }; +/// How to format numbers in e.g. a [`crate::DragValue`]. +#[derive(Clone)] +pub struct NumberFormatter( + Arc) -> String>, +); + +impl NumberFormatter { + /// The first argument is the number to be formatted. + /// The second argument is the range of the number of decimals to show. + /// + /// See [`Self::format`] for the meaning of the `decimals` argument. + #[inline] + pub fn new( + formatter: impl 'static + Sync + Send + Fn(f64, RangeInclusive) -> String, + ) -> Self { + Self(Arc::new(formatter)) + } + + /// Format the given number with the given number of decimals. + /// + /// Decimals are counted after the decimal point. + /// + /// The minimum number of decimals is usually automatically calculated + /// from the sensitivity of the [`crate::DragValue`] and will usually be respected (e.g. include trailing zeroes), + /// but if the given value requires more decimals to represent accurately, + /// more decimals will be shown, up to the given max. + #[inline] + pub fn format(&self, value: f64, decimals: RangeInclusive) -> String { + (self.0)(value, decimals) + } +} + +impl std::fmt::Debug for NumberFormatter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("NumberFormatter") + } +} + +impl PartialEq for NumberFormatter { + #[inline] + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } +} + // ---------------------------------------------------------------------------- /// Alias for a [`FontId`] (font of a certain size). @@ -182,6 +227,12 @@ pub struct Style { /// The style to use for [`DragValue`] text. pub drag_value_text_style: TextStyle, + /// How to format numbers as strings, e.g. in a [`crate::DragValue`]. + /// + /// You can override this to e.g. add thousands separators. + #[cfg_attr(feature = "serde", serde(skip))] + pub number_formatter: NumberFormatter, + /// If set, labels, buttons, etc. will use this to determine whether to wrap the text at the /// right edge of the [`Ui`] they are in. By default, this is `None`. /// @@ -231,6 +282,12 @@ pub struct Style { pub always_scroll_the_only_direction: bool, } +#[test] +fn style_impl_send_sync() { + fn assert_send_sync() {} + assert_send_sync::