From e0bac4e2604bdb3937ff8e846b218bdb5cb1dd36 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 24 Feb 2026 11:07:55 +0100 Subject: [PATCH 01/58] Pass in an explicit id in `UiBuilder`, to avoid wrapping passed in ids with Id::new() (#7925) I was really confused why I couldn't find the response for my ui with explicit id. Turns out the id I passed in was wrapped by `Id::new` --- crates/egui/src/ui_builder.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/egui/src/ui_builder.rs b/crates/egui/src/ui_builder.rs index 686fdcb47..87786a726 100644 --- a/crates/egui/src/ui_builder.rs +++ b/crates/egui/src/ui_builder.rs @@ -54,8 +54,8 @@ impl UiBuilder { /// /// This is a shortcut for `.id_salt(my_id).global_scope(true)`. #[inline] - pub fn id(mut self, id: impl Hash) -> Self { - self.id_salt = Some(Id::new(id)); + pub fn id(mut self, id: Id) -> Self { + self.id_salt = Some(id); self.global_scope = true; self } From fd257b2e95972f2bfe08cbde710f248076a08ee6 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 24 Feb 2026 14:27:45 +0100 Subject: [PATCH 02/58] Use `FnMut` in `__run_test_ui` (#7933) Matches the `__run_test_ctx` implementation --- crates/egui/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index bc90f0cf9..d86851a1d 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -685,7 +685,7 @@ pub fn __run_test_ctx(mut run_ui: impl FnMut(&Context)) { } /// For use in tests; especially doctests. -pub fn __run_test_ui(add_contents: impl Fn(&mut Ui)) { +pub fn __run_test_ui(mut add_contents: impl FnMut(&mut Ui)) { let ctx = Context::default(); ctx.set_fonts(FontDefinitions::empty()); // prevent fonts from being loaded (save CPU time) let _ = ctx.run_ui(Default::default(), |ui| { From c89a4d1b38460cd58e25db5761355b837e57b719 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uma=C4=B5o?= <107099960+umajho@users.noreply.github.com> Date: Thu, 26 Feb 2026 17:09:43 +0800 Subject: [PATCH 03/58] Fix TextEdit demo consuming cmd+Y while not being focused (#7846) The demo should check whether its `TextEdit` is focused before consuming cmd+Y, in case there are other demos that do the same and come after it. (I found this while experimenting with my own PoC binding and replicating this demo.) | | | |-|-| | before | ![before](https://github.com/user-attachments/assets/7b89b511-473d-43a2-82d2-7c4e732b2f23) | | after | ![after](https://github.com/user-attachments/assets/0b82a092-1603-476c-9f1b-2558afcd29c2) | * Closes N/A * [x] I have followed the instructions in the PR template --- crates/egui_demo_lib/src/demo/text_edit.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/egui_demo_lib/src/demo/text_edit.rs b/crates/egui_demo_lib/src/demo/text_edit.rs index 4ac981807..3ec53a523 100644 --- a/crates/egui_demo_lib/src/demo/text_edit.rs +++ b/crates/egui_demo_lib/src/demo/text_edit.rs @@ -66,7 +66,8 @@ impl crate::View for TextEditDemo { egui::Label::new("Press ctrl+Y to toggle the case of selected text (cmd+Y on Mac)"), ); - if ui.input_mut(|i| i.consume_key(egui::Modifiers::COMMAND, egui::Key::Y)) + if output.response.has_focus() + && ui.input_mut(|i| i.consume_key(egui::Modifiers::COMMAND, egui::Key::Y)) && let Some(text_cursor_range) = output.cursor_range { use egui::TextBuffer as _; From 4f99b4fd8d73a7439d25a38f74bbb25279d2667e Mon Sep 17 00:00:00 2001 From: Oscar Gustafsson Date: Mon, 2 Mar 2026 08:52:16 +0100 Subject: [PATCH 04/58] Update selected dependencies (#7920) * [x] I have followed the instructions in the PR template A number of separate commits to possibly easily revert some of them. General idea: selectively update dependencies to remove transitive dependencies and multiple versions etc. As well as updating "major" (the one that `cargo update` doesn't update) version for some in Cargo.toml. Rendering pipelines in `vello_cpu` wasn't obvious. Now both are used. --- Cargo.lock | 390 ++++++++++++--------------------- Cargo.toml | 12 +- crates/egui_kittest/Cargo.toml | 2 +- deny.toml | 3 +- 4 files changed, 151 insertions(+), 256 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9173deaff..9140f2160 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -138,7 +138,7 @@ dependencies = [ "once_cell", "serde", "version_check", - "zerocopy 0.8.27", + "zerocopy", ] [[package]] @@ -312,28 +312,6 @@ dependencies = [ "libloading", ] -[[package]] -name = "ashpd" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df" -dependencies = [ - "async-fs", - "async-net", - "enumflags2", - "futures-channel", - "futures-util", - "rand 0.9.2", - "raw-window-handle", - "serde", - "serde_repr", - "url", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "zbus", -] - [[package]] name = "async-broadcast" version = "0.7.1" @@ -371,17 +349,6 @@ dependencies = [ "slab", ] -[[package]] -name = "async-fs" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" -dependencies = [ - "async-lock", - "blocking", - "futures-lite", -] - [[package]] name = "async-io" version = "2.3.4" @@ -412,17 +379,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "async-net" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" -dependencies = [ - "async-io", - "blocking", - "futures-lite", -] - [[package]] name = "async-process" version = "2.3.0" @@ -1164,9 +1120,9 @@ dependencies = [ [[package]] name = "dify" -version = "0.7.4" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11217d469eafa3b809ad84651eb9797ccbb440b4a916d5d85cb1b994e89787f6" +checksum = "90ce0fb972943b4e88cd03b8f92953df0c71bb05e0bde8e5b684895d808013cc" dependencies = [ "anyhow", "colored", @@ -1780,9 +1736,9 @@ checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "font-types" -version = "0.10.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39a654f404bbcbd48ea58c617c2993ee91d1cb63727a37bf2323a4edeed1b8c5" +checksum = "b1e4d2d0cf79d38430cc9dc9aadec84774bff2e1ba30ae2bf6c16cfce9385a23" dependencies = [ "bytemuck", ] @@ -1845,32 +1801,23 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", -] - [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-lite" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" dependencies = [ "fastrand", "futures-core", @@ -1881,9 +1828,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -1892,23 +1839,20 @@ dependencies = [ [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", - "futures-io", "futures-macro", "futures-task", - "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -2132,9 +2076,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "foldhash 0.2.0", ] @@ -2381,23 +2325,12 @@ checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" [[package]] name = "indexmap" -version = "2.8.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown 0.15.2", -] - -[[package]] -name = "io-uring" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" -dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "libc", + "hashbrown 0.16.1", ] [[package]] @@ -2568,9 +2501,9 @@ dependencies = [ [[package]] name = "kurbo" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce9729cc38c18d86123ab736fd2e7151763ba226ac2490ec092d1dd148825e32" +checksum = "7564e90fe3c0d5771e1f0bc95322b21baaeaa0d9213fa6a0b61c99f8b17b3bfb" dependencies = [ "arrayvec", "euclid", @@ -2673,9 +2606,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lz4_flex" @@ -2806,7 +2739,7 @@ dependencies = [ "cfg_aliases", "codespan-reporting", "half", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "hexf-parse", "indexmap", "libm", @@ -2851,9 +2784,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.29.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ "bitflags 2.9.4", "cfg-if", @@ -3310,13 +3243,13 @@ checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "peniko" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3c76095c9a636173600478e0373218c7b955335048c2bcd12dc6a79657649d8" +checksum = "9a2b6aadb221872732e87d465213e9be5af2849b0e8cc5300a8ba98fffa2e00a" dependencies = [ "bytemuck", "color", - "kurbo 0.12.0", + "kurbo 0.13.0", "linebender_resource_handle", "smallvec", ] @@ -3413,12 +3346,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "piper" version = "0.2.4" @@ -3438,13 +3365,13 @@ checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "plist" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ "base64", "indexmap", - "quick-xml 0.32.0", + "quick-xml", "serde", "time", ] @@ -3533,11 +3460,11 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy 0.7.35", + "zerocopy", ] [[package]] @@ -3548,9 +3475,9 @@ checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" [[package]] name = "proc-macro-crate" -version = "3.2.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ "toml_edit", ] @@ -3634,32 +3561,14 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" -version = "0.32.0" +version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" -dependencies = [ - "memchr", -] - -[[package]] -name = "quick-xml" -version = "0.36.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" dependencies = [ "memchr", "serde", ] -[[package]] -name = "quick-xml" -version = "0.37.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" -dependencies = [ - "memchr", -] - [[package]] name = "quote" version = "1.0.41" @@ -3762,9 +3671,9 @@ dependencies = [ [[package]] name = "read-fonts" -version = "0.35.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717cf23b488adf64b9d711329542ba34de147df262370221940dfabc2c91358" +checksum = "7b634fabf032fab15307ffd272149b622260f55974d9fad689292a5d33df02e5" dependencies = [ "bytemuck", "font-types", @@ -3850,26 +3759,29 @@ dependencies = [ [[package]] name = "rfd" -version = "0.15.4" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" +checksum = "20dafead71c16a34e1ff357ddefc8afc11e7d51d6d2b9fbd07eaa48e3e540220" dependencies = [ - "ashpd", "block2 0.6.2", "dispatch2", "js-sys", + "libc", "log", "objc2 0.6.3", "objc2-app-kit 0.3.2", "objc2-core-foundation", "objc2-foundation 0.3.2", + "percent-encoding", "pollster", "raw-window-handle", - "urlencoding", "wasm-bindgen", "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", "web-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4014,12 +3926,6 @@ dependencies = [ "unicode-script", ] -[[package]] -name = "ryu" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" - [[package]] name = "same-file" version = "1.0.6" @@ -4101,14 +4007,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.132" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", ] [[package]] @@ -4124,11 +4031,11 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.9" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -4198,9 +4105,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "skrifa" -version = "0.37.0" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c31071dedf532758ecf3fed987cdb4bd9509f900e026ab684b4ecb81ea49841" +checksum = "7fbdfe3d2475fbd7ddd1f3e5cf8288a30eb3e5f95832829570cd88115a7434ac" dependencies = [ "bytemuck", "read-fonts", @@ -4582,51 +4489,66 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.47.1" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ - "backtrace", - "io-uring", "libc", "mio", "pin-project-lite", - "slab", "socket2", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "toml" -version = "0.8.20" +version = "1.0.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +checksum = "d1dfefef6a142e93f346b64c160934eb13b5594b84ab378133ac6815cb2bd57f" dependencies = [ - "serde", + "serde_core", "serde_spanned", - "toml_datetime", - "toml_edit", + "toml_datetime 1.0.0+spec-1.1.0", + "toml_parser", + "winnow", ] [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ - "serde", + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.0.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +dependencies = [ + "serde_core", ] [[package]] name = "toml_edit" -version = "0.22.24" +version = "0.23.10+spec-1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" dependencies = [ "indexmap", - "serde", - "serde_spanned", - "toml_datetime", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.9+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +dependencies = [ "winnow", ] @@ -4801,15 +4723,8 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", - "serde", ] -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - [[package]] name = "user_attention" version = "0.1.0" @@ -4858,13 +4773,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] -name = "vello_common" -version = "0.0.4" +name = "uuid" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a235ba928b3109ad9e7696270edb09445a52ae1c7c08e6d31a19b1cdd6cbc24a" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +dependencies = [ + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "vello_common" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd1a4c633ce09e7d713df1a6e036644a125e15e0c169cfb5180ddf5836ca04b" dependencies = [ "bytemuck", "fearless_simd", + "hashbrown 0.16.1", "log", "peniko", "skrifa", @@ -4873,11 +4800,12 @@ dependencies = [ [[package]] name = "vello_cpu" -version = "0.0.4" +version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0bd1fcf9c1814f17a491e07113623d44e3ec1125a9f3401f5e047d6d326da21" +checksum = "0162bfe48aabf6a9fdcd401b628c7d9f260c2cbabb343c70a65feba6f7849edc" dependencies = [ "bytemuck", + "hashbrown 0.16.1", "vello_common", ] @@ -5071,12 +4999,12 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.7" +version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" +checksum = "5423e94b6a63e68e439803a3e153a9252d5ead12fd853334e2ad33997e3889e3" dependencies = [ "proc-macro2", - "quick-xml 0.37.5", + "quick-xml", "quote", ] @@ -5154,7 +5082,7 @@ dependencies = [ "cfg-if", "cfg_aliases", "document-features", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "js-sys", "log", "naga", @@ -5185,7 +5113,7 @@ dependencies = [ "bytemuck", "cfg_aliases", "document-features", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "indexmap", "log", "naga", @@ -5262,7 +5190,7 @@ dependencies = [ "gpu-alloc", "gpu-allocator", "gpu-descriptor", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "js-sys", "khronos-egl", "libc", @@ -5851,16 +5779,6 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61" -[[package]] -name = "xdg-home" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - [[package]] name = "xkbcommon-dl" version = "0.4.2" @@ -5931,13 +5849,12 @@ dependencies = [ [[package]] name = "zbus" -version = "5.5.0" +version = "5.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59c333f648ea1b647bc95dc1d34807c8e25ed7a6feff3394034dc4776054b236" +checksum = "b622b18155f7a93d1cd2dc8c01d2d6a44e08fb9ebb7b3f9e6ed101488bad6c91" dependencies = [ "async-broadcast", "async-executor", - "async-fs", "async-io", "async-lock", "async-process", @@ -5954,12 +5871,11 @@ dependencies = [ "ordered-stream", "serde", "serde_repr", - "static_assertions", "tracing", "uds_windows", - "windows-sys 0.59.0", + "uuid", + "windows-sys 0.61.2", "winnow", - "xdg-home", "zbus_macros", "zbus_names", "zvariant", @@ -5991,9 +5907,9 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.5.0" +version = "5.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f325ad10eb0d0a3eb060203494c3b7ec3162a01a59db75d2deee100339709fc0" +checksum = "0bbd5a90dbe8feee5b13def448427ae314ccd26a49cac47905cafefb9ff846f1" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -6006,57 +5922,34 @@ dependencies = [ [[package]] name = "zbus_names" -version = "4.2.0" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" dependencies = [ "serde", - "static_assertions", "winnow", "zvariant", ] [[package]] name = "zbus_xml" -version = "5.0.2" +version = "5.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589e9a02bfafb9754bb2340a9e3b38f389772684c63d9637e76b1870377bec29" +checksum = "441a0064125265655bccc3a6af6bef56814d9277ac83fce48b1cd7e160b80eac" dependencies = [ - "quick-xml 0.36.2", + "quick-xml", "serde", - "static_assertions", "zbus_names", "zvariant", ] -[[package]] -name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "byteorder", - "zerocopy-derive 0.7.35", -] - [[package]] name = "zerocopy" version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ - "zerocopy-derive 0.8.27", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "zerocopy-derive", ] [[package]] @@ -6130,6 +6023,12 @@ dependencies = [ "syn", ] +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + [[package]] name = "zune-core" version = "0.4.12" @@ -6147,15 +6046,13 @@ dependencies = [ [[package]] name = "zvariant" -version = "5.4.0" +version = "5.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2df9ee044893fcffbdc25de30546edef3e32341466811ca18421e3cd6c5a3ac" +checksum = "68b64ef4f40c7951337ddc7023dd03528a57a3ce3408ee9da5e948bd29b232c4" dependencies = [ "endi", "enumflags2", "serde", - "static_assertions", - "url", "winnow", "zvariant_derive", "zvariant_utils", @@ -6163,9 +6060,9 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "5.4.0" +version = "5.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74170caa85b8b84cc4935f2d56a57c7a15ea6185ccdd7eadb57e6edd90f94b2f" +checksum = "484d5d975eb7afb52cc6b929c13d3719a20ad650fea4120e6310de3fc55e415c" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -6176,14 +6073,13 @@ dependencies = [ [[package]] name = "zvariant_utils" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16edfee43e5d7b553b77872d99bc36afdda75c223ca7ad5e3fbecd82ca5fc34" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" dependencies = [ "proc-macro2", "quote", "serde", - "static_assertions", "syn", "winnow", ] diff --git a/Cargo.toml b/Cargo.toml index 470644bb4..aaf9b9398 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,7 +84,7 @@ chrono = { version = "0.4.42", default-features = false } cint = "0.3.1" color-hex = "0.2.0" criterion = { version = "0.7.0", default-features = false } -dify = { version = "0.7.4", default-features = false } +dify = { version = "0.8", default-features = false } directories = "6.0.0" document-features = "0.2.11" ehttp = { version = "0.6.0", default-features = false } @@ -119,24 +119,24 @@ rand = "0.9.2" raw-window-handle = "0.6.2" rayon = "1.11.0" resvg = { version = "0.45.1", default-features = false } -rfd = "0.15.4" +rfd = "0.17.2" ron = "0.11.0" self_cell = "1.2.1" serde = { version = "1.0.228", features = ["derive"] } similar-asserts = "1.7.0" -skrifa = { version = "0.37.0", default-features = false, features = ["std", "autohint_shaping"] } +skrifa = { version = "0.40.0", default-features = false, features = ["std", "autohint_shaping"] } smallvec = "1.15.1" smithay-clipboard = "0.7.2" static_assertions = "1.1.0" syntect = { version = "5.3.0", default-features = false } tempfile = "3.23.0" thiserror = "2.0.17" -tokio = "1.47.1" -toml = "0.8" +tokio = "1.49" +toml = {version = "1", default-features = false } type-map = "0.5.1" unicode_names2 = { version = "2.0.0", default-features = false } unicode-segmentation = "1.12.0" -vello_cpu = { version = "0.0.4", default-features = false, features = ["std"] } +vello_cpu = { version = "0.0.6", default-features = false, features = ["std", "u8_pipeline", "f32_pipeline"] } wasm-bindgen = "0.2.100" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml wasm-bindgen-futures = "0.4.0" wayland-cursor = { version = "0.31.11", default-features = false } diff --git a/crates/egui_kittest/Cargo.toml b/crates/egui_kittest/Cargo.toml index 33c895617..f922b807e 100644 --- a/crates/egui_kittest/Cargo.toml +++ b/crates/egui_kittest/Cargo.toml @@ -38,7 +38,7 @@ egui.workspace = true eframe = { workspace = true, optional = true } kittest.workspace = true serde.workspace = true -toml.workspace = true +toml = {workspace = true, features = ["parse", "serde"] } # wgpu dependencies egui-wgpu = { workspace = true, optional = true } diff --git a/deny.toml b/deny.toml index e07d476fa..e740494fb 100644 --- a/deny.toml +++ b/deny.toml @@ -53,13 +53,12 @@ skip = [ { name = "core-graphics-types" }, # version conflict between winit and wgpu ecosystems { name = "getrandom" }, # ring / rustls (and thus ehttp) still depend on getrandom 0.2 { name = "kurbo" }, # Old version because of resvg - { name = "quick-xml" }, # old version via wayland-scanner { name = "redox_syscall" }, # old version via winit { name = "rustc-hash" }, # Small enough { name = "thiserror" }, # ecosystem is in the process of migrating from 1.x to 2.x { name = "thiserror-impl" }, # same as above + { name = "toml_datetime" }, # required while eco-system updates to toml 1.0 { name = "windows-sys" }, # mostly hopeless to avoid - { name = "zerocopy" }, # Small enough ] skip-tree = [ { name = "hashbrown" }, # wgpu's naga depends on 0.16, accesskit depends on 0.15 From bd636471774fda847e2a6eaa92a72600428a16cc Mon Sep 17 00:00:00 2001 From: Carter Schmidt <35572862+Fyrecean@users.noreply.github.com> Date: Sun, 1 Mar 2026 23:54:47 -0800 Subject: [PATCH 05/58] Fix crash when dragging a DragValue through small floats. (#7939) Increased smart_aim `NUM_DECIMALS` from 15 to 16 to fix crash in `best_in_range_f64` The f64 value 0.09999999999999995, when multiplied by `scale_factor` and rounded, becomes 16 digits (999999999999999.5 -> 1000000000000000) and the leading 1 is clipped off by `to_decimal_string` resulting in all 0s and triggering the debug_assert! message "Bug in smart aim code" * Closes * [x] I have followed the instructions in the PR template --- crates/emath/src/smart_aim.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/emath/src/smart_aim.rs b/crates/emath/src/smart_aim.rs index c1b96ec7b..5d6a3e0fc 100644 --- a/crates/emath/src/smart_aim.rs +++ b/crates/emath/src/smart_aim.rs @@ -2,7 +2,7 @@ use crate::fast_midpoint; -const NUM_DECIMALS: usize = 15; +const NUM_DECIMALS: usize = 16; /// Find the "simplest" number in a closed range [min, max], i.e. the one with the fewest decimal digits. /// @@ -143,6 +143,10 @@ fn from_decimal_string(s: [u8; NUM_DECIMALS]) -> u64 { #[expect(clippy::approx_constant)] #[test] fn test_aim() { + assert_eq!( + best_in_range_f64(0.0799999999999996, 0.09999999999999995), + 0.08, + ); assert_eq!(best_in_range_f64(-0.2, 0.0), 0.0, "Prefer zero"); assert_eq!(best_in_range_f64(-10_004.23, 3.14), 0.0, "Prefer zero"); assert_eq!(best_in_range_f64(-0.2, 100.0), 0.0, "Prefer zero"); From e505d98215f80764dec2b8017fad9535a24ff799 Mon Sep 17 00:00:00 2001 From: Jiayi Zhuang Date: Mon, 2 Mar 2026 15:59:29 +0800 Subject: [PATCH 06/58] Fix: update get_proc_address to use Arc for better ownership management (#7922) * [x] I have followed the instructions in the PR template `get_proc_address` was introduced in #4145, but its lifetime was designed to be tied to the lifetime `'s` of `CreationContext`. This means that using `get_proc_address` outside the lifetime of `CreationContext` is undefined behavior. This contradicts the original intent behind introducing `get_proc_address`, as this API is intended for integration with external libraries that cannot easily guarantee alignment with egui's lifetimes. This PR changes the type of `get_proc_address` from a reference to an `Arc`, decoupling its lifetime from `CreationContext` to achieve safer memory management. --- crates/eframe/src/epi.rs | 3 ++- crates/eframe/src/native/glow_integration.rs | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index c37dc1cf6..b9a178a1d 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -72,7 +72,8 @@ pub struct CreationContext<'s> { /// The `get_proc_address` wrapper of underlying GL context #[cfg(feature = "glow")] - pub get_proc_address: Option<&'s dyn Fn(&std::ffi::CStr) -> *const std::ffi::c_void>, + pub get_proc_address: + Option *const std::ffi::c_void + Send + Sync>>, /// The underlying WGPU render state. /// diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index 1cd49449f..233ce5a49 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -294,14 +294,15 @@ impl<'app> GlowWinitApp<'app> { // Use latest raw_window_handle for eframe compatibility use raw_window_handle::{HasDisplayHandle as _, HasWindowHandle as _}; - let get_proc_address = |addr: &_| glutin.get_proc_address(addr); + let gl_config = glutin.gl_config.clone(); + let get_proc_address = move |addr: &_| gl_config.display().get_proc_address(addr); let window = glutin.window(ViewportId::ROOT); let cc = CreationContext { egui_ctx: integration.egui_ctx.clone(), integration_info: integration.frame.info().clone(), storage: integration.frame.storage(), gl: Some(gl), - get_proc_address: Some(&get_proc_address), + get_proc_address: Some(Arc::new(get_proc_address)), #[cfg(feature = "wgpu_no_default_features")] wgpu_render_state: None, raw_display_handle: window.display_handle().map(|h| h.as_raw()), From 4e43e6575682cca2a5a891936051d00ecd4bf8c5 Mon Sep 17 00:00:00 2001 From: Jhynjhiruu Date: Mon, 2 Mar 2026 08:04:41 +0000 Subject: [PATCH 07/58] Fix emoji icon font (#7940) * Closes (again) * [x] I have followed the instructions in the PR template Short and simple PR, just moves the updated font to the right place. I note that the license for that font says copyright 2014, which might need to be updated to reflect when the font was modified. --- .../epaint_default_fonts/emoji-icon-font.ttf | Bin 324132 -> 0 bytes .../fonts/emoji-icon-font.ttf | Bin 317864 -> 324132 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 crates/epaint_default_fonts/emoji-icon-font.ttf diff --git a/crates/epaint_default_fonts/emoji-icon-font.ttf b/crates/epaint_default_fonts/emoji-icon-font.ttf deleted file mode 100644 index 0f29dfed814a752e23d019a6c55a864175d5e1ae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 324132 zcmeEvd3;;N_3zBRSGwAzt6jEaNtP^mjTc$Ali0DH6-WpngpiN`30X@6Nm$BKN?k~4 zfB<2KHl?9Z_R`P-<+1#5TWEn&pe?jOTSzHCN-1pv1R7{8zwep3vgHukU*G$^e}11Q z>*&m#JNukDbLPyMG2@IeHydIqTQGa}qQkE{v*}w*d(Q^n_?(%uX0tlDw0HO5H+sZT ztsTlMx8@n+OYpp6)yXS2UH{5+moesihp~WT-OAH8!QY4QZvorZZ8&>vY1!r9XRM)< zY4_}1zh>p?=&R4&gLl6LbghSn?Mb~K=>_qeU4QcFXT`pK&ue&okTFNchK;LMS`OQ= zg0Y%?cKnkNqb`uj|KsUCh8Y}$C*=|BJc{5ec}`EC@q zmr>n#?5o%R`LZvb;O+f@IW>4N_WW(1*+%${fGs+D|L82s0<8s)Hl~Oeh{KpVJUYyn zWfk6Uw=6LIDU$domynNgnfD6TEIrH@<49FU_;zMNs2h}L;C;Eo{Liew?c>21nx3gq zw&!gmNUb101B{K1QW-{v)nSIGatYK!;7*mF&^36Zj{;Elvm}9K6$_~Q*nN0+raH>% z)DpXr!V#$N$yR{7$prY1?uUC3zzrC{^T!(60&Wop_%6UOU^igMgaP0cfQvN7y#R9G z4|ohPWI_;_0ZupZFtA(wn5n=~U>|V6d>2MJnYvnpUugPK+-azjQt{jfw^8v-VVJs? zeFNBqvX{g=osDRmTh$NPC1`_P@UsAH>U*OfA)ghRjlwb;fYKfS*a0Ff;9Xf3sW&1*iA_U4Q8}Lg2(3Ag#UPcRF13RcP&6s&l71o z)bES-rndDEnEnHBQ{FKXm&@^~7&Ky0Kjze)K>*d|z_0`Gn-3`C1K-ng1Nwn*5wHpH zdq4qD4x5CV($519G2LeauLU5k0Lpug=}w`YK7!j!Tf{}%jiqO%OMN!hZ{_3x|}?wv^EZ^-Kfbq{+?eUG;yek;;?T;mwu zDxZNoX>2^F?&V(e|3A@64aXMrBNkHy+7Wg4V+KJX>N%K*~>H2zKl5S>teO#MYO#_?&AV127=#IkI0f^3s z573xV(Vj%77T}wJiC31x-hpSLpA{w_eH?DX9D{9Wg8{e=bx4GvF|45-C@rF+C}6Rq zNsc^6|AA*pi|C2M_LX&jkC_a_@3@WQy8XbWfWn~WmsP7050MeiX9bra6C!fVL_0bRD zt_P;NQ(35un7VrOex$P-_1}$WqJ#k&x24uHaK z1snq)o=Ekkw7-VD)&LGcT~LqF5%dH6B`<-y4PmHXC@jtUR41y3&3qd_WiY zIORppLzXr%uaMhEX^Z!=yyOQM-3NR+0C^zq{bTbjrJn-ap?*y9MoSn=L<2NNKvyDN z>KCTI!O$k+9YY=LXitZh68Y2fFoiK;2zX%Zd0@=nd@t%uVZ^g~qoA?j@-r~;LAz!Z z;i#{PK2Zko+(C4SXD`b6IB13XgxZN{rW);A1nvP`g7C!8i6_vz9&MP$`sjz+fXL6S zE~flNT5ffR@I%{;wIh}7Py*u5z@n_cUjY1&{7t{Pz$w770MIPjX`i6!esTk-eC4E>_CdbnY6f-)e@$DB=WT3=S(yv(QtS?PPpU0-ce*B>OBd4**VfctSNn}jRi-|( zHFHDexy-wnkL#|gySeUxx)*cSoIMxKrE+b#DY@ynmAMVMFXpbz?a1AdyDhgX_aC_* z}%>0`Cb@`j}?>D)dGEJ8>-F4K0H@n`w=-pf2eR|($Y5AyQ zG&Z_y^gl;Osl1;k%|oIzH)g^p&4rokP?|qw-bZP6qBK9vF_gxWi|1-{J-LD0qTH(7 z=X2Y0*XMTTZbfMxMQNVPy@AqLQJNr1)1L1~X|6?S-fOZnC7ZT3-G0>JZ?cNg@KN7r za`g99n$b5#mya%a?boj@fA!C=zVYg>US0URJ-<8acc)^J^B2IEemf7pt*=}M0r5;g z;gxp$W?#v?5(lRKU@!k3@Y|Pv^YSlW{>jTf5p;f zV?JR8#(g|I{!Y4i6;D<8ozJi4w<VrkYSo}1Yu zfm_9IoA_;K9c(IGy{u0>W9}5VpB)3dgssExdiELNcM|(7+rUm{r--MGY!f?+oelRn z?DOm^!Vg@QUBoUH?n_YfOW9?@eYyE|JG;VkUCFLuSHpb`;97Pa+X;LVfIi<8zc=Ie z7WOT6E4y7h-@)!=cL^7@;3KB%QTAiu`U!iIJ;k18KSfLaj6KVqW6#6?=j<2km+S@h zBKsBlHG7G@%wA!?!K(Oo>{a#}+s%H@UT1$`Z?OMlZ?ZqKx7eT9+w9Nm9rhRYSN1oy zhrP?*WAC%Q>;v{8`#ammK4KrUf3W>*gkyAbja#^t>u}k)ojbUbySSTsxEDQWa6b?5 zAP?~{kMJmup$DsZoF{kzT~pTiH~hw{VtTt1H<&gYx4j{sl6 z$3;2=e7H?w`9K~f5Wh;90GT)-lR!K! zU6jC_zz+c)Mi`W+7GVjP2Nih=4|F4(*aN;O+%Roe5~n=1?~R)(?#OSeG>5JB@kE0od@_L{5Jw` z2V4vHX5i}q)c$7ye+{q$?sI^50&ao(T;N*)x551-;9U~f1;CF2{saD7fPW-`T?mYR zBp}X{`y~MF2=2pjZ%806l=~xq`u7rG^e+Ls9Qa+pdvH@3KLqT9`$}N+GXZg>+${WDYmU+}Q|32LJ zLLFrRxZ(aDFv>tc+_WhpfuX&d(2h-LGn%KGP>!a%;Qk?FM;$JKc6wd_2X-5gr?hw)0DDoLa z9>m8+eSjF;=#Npe~hWo-%B9gIQpLw_z{f%yhZ@y&_M3F zNdV(V0^ciu@rA%Y5Wx6Rz&{tj_#(#Nw+UeUXkdo`#uo#-1u%XLun!PL8I}O21Tel5 zxC2mx|B1l;fa!3r1)d3*1@}q7a|AHH0eG$e#!m*GFM#n=fKhG$;~RmgOh?1L37GQ6 z_~xeqF9jS6_i4b(0VlwH2Jk8YjGqm>9&i%;&jF_VPKNsnz!d*n&j&si z@CCRp1pX4>Lb$gAOJMvmxUUkx_~pRY3SiLK^7n59d;{UH07jd?kMY4N>@9#h;l2j= zZUE?%?*RTj;93%PGI3EV~31Iw3zySbw2Y(zmEP(MJ16Kp$@c#*LUI61y0XG6L&v4Xf zZ@U1-QK!8{0LC4E7I?A%#-9hC3h0GDXm;;30gV3=co0DJ_5$$X0E`L#E8rsqF#Zzo zQ2?T)mw}HF!1!;0K}!I}e+Rq_fN{%T1zrJI3HNSb(A8d|pVxs;0}%b7{q}wifO&xb z4fyi{7=I5KbOt~}0)x)>Zh?C*z83*5fg3CIy_W(mgL?#cy8y;Vfv*BwBee5;@AYtl zPb(_$*9DNOhwlZA0hnR|zF7cMtiazAz!d7M+XOJh1`OH*FvSkM3jp4!IDo$=fGO0U z_X}W(8yGaP_Ys8g08@WI2DbqWn%oPTR06=D$-U3O9Rj9)dLHg7;9mfK33mecMZmA% zP6EFofGKHUYTwu3Ukm(u0ZhpPzX1R*Q|f^C0N#b0=mj*n_aAUK08@K`2P=~p{{YSR z0f)N{7;OY#N(V670>G34FzWvS<~F4h7n?!DazWSqOXv;B3UDcozs@3i#&- z7YkqtXz&B{9e^pv0;BH$OaVQ9aEAbEJ;2!uN2Yjyprknu$J-`F-Ujh8T1TbYa z@WTM=$2GvifFHoU4)}2aO!*8j#@+|uC(22{&jT>mDI0*%cOU#3?o)sZf8lbf}stL!Sk^`z&xlDftS6-*l{TPs&^P$a|pIJhOqS74tSig z4M^)`#NCLvn`~HOErJ^P1uU;nzH?58I`nERqY&nTyP*Q#jb+h3#x92cHnhQIw_^$P z43<7i7`tLCmOXfP)egWTj9ncC?85SAIhH+eUv~>*UqxNMhP=KW1iZ!AjfnRRp$3EdCz}Af8G8zTPXj-@pRwoR23uvnIG3?sq8_g-V(d3}Kq*D9gKu``%8#8;re=uzNQ#_CXea@_+CoV;}keDEr?L zZ{KpjF2?p_F3m!|$zg8C8kFaO^Lwxz5d}aNX#lTp*vEKd3V^w-5q!IGC*TH*gSb~D}zck3nq{M#4+VLFgb2i_HKfvOyN7FRIdy^Zmn zvlySeh4Crt8J`O1McMi=5A`GLbi|o~=a~pMdpYBCdKjM@1-!%f{5X~}^RR?jg5?V0 zAH5Yz5|n4j)r>EFgYji90PsJHzGk)Vt zz#EKz!v$CZKzVlB0KI_q0OYsx5x{Q7Z$cV30e`a-@F3$iw*j_bS+W{%A7C%z-&z1b z8s9=*w>ATY0DBm}Z4&@-Z%4UrM|yW4k2{dw9k*clg1qk>03feBA7}ioMSy1*zdK0F z7RK*I8s9~J-|J=kzO7iMAnosWVkwfsvg35XlUQ;f%%l6U+}OeRV|e%23s`O}Vf+V3 z`v-{g!+BU{pj=P9g=NJeEGPD2>9C&h=T=~;5Qq4W_%9>R-N3)k0#N4Hw=w?4eT=_( zHREpszq6e2zass2HOQ3kgRH^-{tm=*#2*=A3cnp9_VW;-7eI7=3!?E}rUbJPd3Q1; z{0vhf2!kylC9w}8?gogqolMDWhnV^X#8bQnZ&eC2nNsXwO4k;qbT48`&uXTCuPRdz zc3O}rvkqa(oB^gB0z4P_&D+bA`L{4-!E%U;Ho#d-S?q(ji1^3uh8TD~Q;wg~&U*nO(=$xDU_Znk#Jccs- z2=O0Bxt_~1M>gJxtZtGu1c5)X*(Vt%6_ePNp`V%hXA$nc5EM zc$}%-Gnv}s1N@e$llL=q%5J7k-Nn@2`=J+qgo=Ky~m;fJnf>Xr>my%6EH zqD+^Z#nj7IF!gf8xpDzhuR=aoy~Wh4w=nhE0l+($*3V_?w~+Sjh;#c3OuY;7zKyi+ z2{QG2KBnG>GT(m)Qy)OM2a(q!fZ-vg{?NwMA0y4D@ccB~Pyd#wKi$F9=j)mJ3&eX7 zdA;}uQ-6hgew||K%RNkeWhYaA^Ab~k3;Zg=z7GFC>|*L0z;7Xqw;yEcUo@uvbvaZ2 z*2~mAw=?xU)aS$fO#M6HBh>F7w=fmkt?K9krfG4eS%LL=OtY;9>|vVyET*|O0PbU& zdlS>Vc<(!xX$HW5I@1Dp7ipnFiX|s*zUBBBmvQ zA(Lon;A|Z54AbiNF)jZR(^`;c2cUBo)4K4k8*%#p1Bf?>_cM^rOyoKHai$%5J<|?5 zmuYkNGwtvo(~jECw4(uwZ(-UJyjzMij_+jJ2|lK+n8~!2hax_FNe1%wwd@VeNU&*gI&q=Iym+F~LRk564Xf8p^L&z*bd zp& z$4BmanCqqY^js+HH=6CG!GXcen=bg`#!WL&!;KraT)24?YKZb3vv%z8CuaLdMxgM%}sZ`z0&F6rn%y_T$9 zv*eggBmo!GS%9svEKnCRAH?(=>t=^wcVa!{ec6YiX4D&@ZdALXKf|FqY^D{|e;F(i zEH}`>lexq02n6i-1Og8G>+S8!7k6~DwJ${p`N^fvUc;}sCW8@u=RW|lc9g7EA;OstFTNKo_wEskOt`|ZXJrrpN8mISsrdY@q zyYVZD9fYrJ7)aOZ!%uO3%XNV-9W`sphV92Lt6p*_=Z7vzEMI>4+LbeB_AB46Np_s! zv&Bw7KJ{R7&LJ0`?s7-ZxMJ?S<~tJ%Yxpxq|Ezvh{U-KR?nLe$u3KyKH9QiwM&dja z=X%(x!*dER;5&)uC-Gvh_#zCxc!jTYPvhPA3U_x$7u-b=J}>>da)^sBg_qyTx6|mA z8KBmh@$gK$-W8@CYRbVqJTG%bdL3N_{$%5#MtuE-sw$kT4x7!Zqy|&z!Ia@wHC|PS z;p@-%ja<;`vf}Hl_4~6%uuxMoE9c7kjA*8>s=C?mr46Gls4EuDp;(<(tJmR9c)fAA zKa1f0%%SO2r^jJc6pu4JwYK)KI?b+G?Ar3`squK5&v03^c-Y@wT|K#4(^XB^)EyRG zQ52&w8fgnJ{N}_2<6n(K^%xdLo^Rdk>OIz>`%$Mk`j_K-8)NQo)S>~&eP`X$Ehcwx0 znUxmcZF0{H|8PlrN3v#CY5nWRc6NU5b59JdT2(9-I#;|^T776uO-K7N%9FjlYgSL~ zZF=>Irlm_y`p%j)lUf!p-n8ME#cfZ#+S1#*YR%NCl>e2ZFRSy^+qf0Ff^xT0=M(vr zzoDSn{gMdAenUY)keQ++EAdHp|6g!FZy2r?A+)G9%RHloacM2@Ol_e2wGpT|k zZ}FSNp8r@;q=O11%LJ-z%)vXRq1K}zN zZB;d!MRQu3k&CWboVwNL;111hv0!L;(4sas(UryO)HIoc0xERqO&p_3*Wj{x>;;FR z>n@AMu4)!VAzA}PDZ1BY^Vu}32KsmU?Nmm)D&8V5t7dUn=i9wj-L9#KL}k%b)rNfa zro1!ka0VQXfYT9n^3R}xGA#@Tn_YOgz^oaOR zo|sd$s;Z8!rebtEtrm~&PkVg{x80{xli-y_ajBY$hD10FbIpMcvFYd|x5aDq{KyOjHRa3OZ0j`ck2sVl1-JVTh^2Yz_v7(JiQ}k~A{4pCD~X zt$9H?a+DM#yOpD$JFN_DuWs~syk3u|u{u62Q#&`8n_HWi79aPes#ugo^@EXcY{BeV z51(|>!?R{D2nB-OnQKc!>Tqdj`@tht`cfBY%8Jb5_Vx`^L;@nI;_6A07G|y`8LzT# zl?9d~M;2R-zRDq-uWSa{0Oe7g=KptfbPu9Iz05kB~7Gyrxc*-Hd`tepOY{uit(qiUT?^112BDohSNkO2HfAmk5p zrBiK&VQ`DjZpTn{g1uOHyt-;ZXK`sNnXOIM99?K#kjeBX!e^PNZv*rBUj_p=?uadQoH3F_}zrHW5EGS34MsC2hJM_P8-W z`Yo!@>8x^l!@8~q+^%YyqER7q3%A*H)W^^j?#u=J@y@#JkqLjT;Y)Y|d7m#tdlx1x zMq!;Z4Q8w5nL;+YY;Nj27;SyBe~3DHzB}I?>5k|TJ+EIomp+f4c;chNAzjKjlP^8& zuuCWR;4|(jtv~iFUpaQ`R{6X5z(^EdIcH)N8M)*civ3Ep0Xj`9B7>Gh7Knsacw*s`rWJ&Ng-^zUfC-M}l2{;NMPUlveWT|3);PH>E^@!J77p$L?h}YXoe{@G}c89}mi@L{M1Hn44H=?h$ z*T)lc>R+A^gD;)%Qeg+rrkv}9z(WV*BXZCV+%9T)sjk6{%%Z9PMnJ7mue>;Gm67bK~*!v*~zzZqvBu zzYGw{c|1O!$CC@KH$0xY@JNez2-kT$#(ME`-GM=#d-hrW^@;!@0$=)XXT5$MA1|mV zy`ufg%s*K^-u3LWrKc;ZAj(T)OJ$2jk5cc%sZ%SfXM^ZvaA7oO9cCyYY(w}GW55&? z@F+)8K^IcdLAsfbZk%z>e16x_w(3BjHC~#OnGy_!!@=N`4F5&EH4vz_9sRZT#-_~! z1Dl%~+s9q}>EZk@-+yi9j?7?9q7W)Q$$P42)aRGBwk^rm}-r6)s$M9pAk{s9q1 z5)4NN+$^QOuR=ox3tJB~B~e&;*Rnio#@y5e%g_N>cpk>qup!J-X;h^Srw2lHL_LLr z{ea#>dAuxU{^?J5L@*6WcM(j0BuhxqrLOW}VlcP*=Kd6q;y^&euyNWWvZ$BZqJ zT9?y>Z*62wAn9;A@J$9FQY&o0J;L#K5lJC?12CH)!fJXo)8 zM-D}k-FN48$&|#^;@A7(4l5##|zJr!fvVXl` z<`L-*$;=`}Gmq|k!JGuFTNSUvneeadS{Z7qiuEMsC3<32Z6Q4S6He$7w*JciZ$5sp zE1>HRb<@Sctlvm`es!@YZTPdn;fo)Kn5PF^Q~qTL?CSm7`F^5c|9-ZgC>Xj3hGvgx zT>S|+2{vmgp+R<*POE_=1kY#%cZA7ZrPZAHRI)d%e9gefDfRb!7I1kxkD;yCYH9 zgPF3zlT?O4#S{|J(}wI+sX{PU38Ht4SLTw*BO0o^tK;3(zfrFIO?>&(DHqN>>cY0R z;`+hC&v|Aojz$gsX!pYE>S~YS?{0WSx$KpOrM0!S9`~HSY2W+&wU1Ak(z@{CX?>fz zD&-Xw94O8XW%J28ye=I|t7%ZAWSnWiAs{>!19-vZBjihJHq4)LmQ0J5>w|tRXffE90ux1i`g8El;$0& zO)~i_DT;y+x2>kc=HZ#h0Zm%jc15*gWCv7>8rE8*&}=8=2Gld~v8ucs8Xr=&aMH!t zwOTC4Eb7SVQlmm{Wt(vGMNo{0Ac)9ML1K6&1y1Qou~>(yh_po_ljqJr6sUJNqLWj6 zELbRk*|8Ps3N=L%ZJmMhF9sBc#3K2M73i~JWtbjHJHZP%+l{b8GOP%RumcpRv{Ow{ zFtjJ0_zDU;#0C&{fX>AsRIWjEi&2v(gBH!Y?O-}5tl3G z7AZOHj=15-S69!ff5C`5>`sxfI|g;MqkdL(b>8s-b;l?h7(jnp*b21ePSKX1f$rlw z?4^Iwl7Y&$N|;NWN|CzBRKQm1Li$VZLRh2QlfK7ShN`Ee*m#AA>*R#E=LwG#(Q=)UmJd<)^!_6~sSvXk{Yd3-o)1Hh&+fHnaKHD|a5niNfQor&QD3=u- z>_=r6js93&s6LFjX**5=vN|~+#oknVx~-FTqr~(%Rg#jVxT3iZ6B|Yd7Q{|gdEF*x z#vG+MjwdFpw2-h&DWD*Rxu6>;Mp-m(#TP#hiezpewjU z6iNBrLM{{32bbCp{*Ie(x}h@HHSC!sYdSkq$zmUL&fwfn;hj~RQcNa03jjW~^ykiG zvRLZnoynB&QWA>OW{-F}oFt)%y#j0(SV_GLjh*gyb$G(0>8nMIWFe0<(H}IG$PWk! z#ALNt+_qL%z-Du*p@v|nJ{YVI1sg)D(*~ViE4CFZ5Sm31HE7bkUTmF03l5Lt&u?uz zESKwQXt?NX@m|GdYQ_7rFKTG$%HX@SuZ1 zN7kUn>b2V=?t<7z(ZHs-mklX5sS3;D0o_L&22237kmTr8>K>^;fznK9-IXi34o#^~ zFFj?|b*sXQ@^Ngo)RYgc_>}%=^+4Zm;iW%XtX2z0mtikQY1go!W5tY0d9Og;Z^KP_ zmz9j|f)q?)D$*{C06HcjR({7z&s%9H#5$Ips)I=wR^Cn(yVPxhqlijJr1A)ALc#Y!GGDc?^qLi0I$kR;?G}m2^pydN zsEzPCi4RZ_l+q8WK@5GYgxGsThxv3-XZ9FaS3h**ROfav9-yccay4pBLr}JQKR4@) zO>o*qvWU81pQW-cW@VLcj^%~cu-ni#Ps_gPMB|8@ArqQ%h6+0bXh{u-twwrC$Of3o zN|-E1z*$D9Un;_ikVE88@9AA|^k?cn)bBxVfQ(*0Gn}N@1r*7SP z8eDvM>uINw$EkQV=0cjOF&!b5&B?r2{YP+qHOVqq|2N@8X#utpQ*rWh26lwzvLo10 zu$x%MPJq<14rjJEveVd^IHUbV6c1uW8bIQO3cz|+Jd=MNfcy!{;X_A}0JO2xW_M;1_X3vg8YZCw>u1^zZ4cQ{AS{11*TY zKV6OPe@mUW|1Fw#n5j1)9gQ1y=tM#U2K{I_IIa%wA5w=#1|S-fo52s#?fxMJ%MwO1 z^ni*#rXX!QA68I0b%68>@Nt8#-WPP${!b|DQ_Yv$2hf$Euq)bH7aqA{#gPkJ+s0i7 zr^5pRiQs4t3#{dyQ~#vi552}6>=m4Whwj}J+T`GsV^=V(%GIfy7v--x$Bj*#lBpoN zS`ke~mUFb6Co6wV6<$RW5XbN}7giNhvCPt(wxvA($#PYeNJ`1kcjstQ-ig&gUM8HJW+^5@`e$!+3c`AiW%h)sU(F+ zB`ZSMZ6ZWFwu+rGkEh1)WrKlyFw_vDIXWK*W(*(gHandGn;rL+SuC&tu-jzlq|eA8 zG|l3O5JYI0yokMSXTVNVEW%^5Eq$qWpeE53jn=uur!E@pO4JP0O5a!(yFA$G$;PDb zE`NPE+*Vc977o|@1JzRfRvkFF?=B#f4BiRtTdS9#&9 zJR0u|%Z5Rv?XqFQo$*Au>5{z(G+nsf@Ws$B_{MxjeHe|45ap&zlvW3_)FxQIm~8@k z2-zlB?|9HA*+2lo4ceqJ7|aI)V{H5iVZN)6p4XM z?LeZY7#nZ4Sg|HCP@5S@pfS;GZdXb5$7KV>{r-kdE+0>Go(F0tn6bQ}5i_{7l zX|Z6z&i1>Ef5y!F*NwaQC5nw?Uy^ZgwGGFmA|%mse1Q`r<}19Wx<89XTX;oR zSATz3*A)x-WmLgx_1*(g6X}-v0y#hS&JCE$R2tfze;9f5+M_N=LOmwT(hXTJm?rvhINyGw08rIpcwK zBdvep;tdB+;oxOfZX3^|GTt9W;SZ?L!7^?=SdmI^SGLP|&PvO{TVv$(il(V>DlG?3 zr_^!qQj=^L#*K<^t9~|_Mn#*sG7=P^!+>z2!|d(04jACJOX`#KqxNt$pGqCw+B`p* zY=S9_lA0QeX6&j}scJEdV9@YQs(RtPFTRi+j78(|Xl$TPDYPD0ldMftSIub%`TZR= z0mEpi5~1U<=wMylU^I%eKV#!)+1RKpR1T5}4~Ae*Cd+`J~d83)<9t=Jv4jP zkwZ2jKiC#pZ57UO{riy6ND5`;K|>B5IcxSqC#k~%iDbru30J8?*DsP03G#u1g%u?t zSs3!ax=QFdSK^MEKcZ30y`uIuninVV=sLm0rIc9tLhiAe^Q91>rCZ9Drc_xGSC_Hq*U|Fub2szG8%4{Yt6b2GVIJyiNTrueo4zug&e@gK zB>g6QRvTVHbc#sx6zH#1#fNRkT0bccomNt4jZ+QgJyoslh(sC;=+jkoa!1FP=gqya zy<-Yev0F<$j#My&J-hZ`Fy-)Og2Co!v^f~ec=>0m3}02X&sXhnx{|OeMLiz~Hb)|@ zu_ZfDJ5kSAYb4SXB1_VcGYE0M#%-kCZiB;Mfdm!UU~t3jsWn`XgmOV=2o|UIAA75? zFCBEc0s)s3G3_yr4*ibP4d!Wm-Ri_<4@`71o7+f{h4-i_>2zR}N*X(^3ccR=J8bd5 z_?J}ChTWTl?WsZ4n>@Lr{ld9Mt`^J;YZrZ&bl=#|B6*~AQ5716tbp;PU>%cA*ZS&$ zp{7W7APRDdL;k<{RMT(NzIKc#eedQo%JqO@3;@GGmM(f+z@e>7T6u52B-zA7?R zTv%neg!WY!RbzKf4T1y5XrB%r?9+kE2>$yrY=Up2JJ)!f6Bp5`br!WwW2GB!ys>m6 zXT6XldwX#ML!rP2OOF*kF73SaR=(n6M%sqaa$K@^7>~{M?tDL#*Lo!X$%IPmwZ{8D z-n8lC`x{?7FwxSE&waEj`-eYdcYX9Z=tZXC2Gbv^=bL+JpDxvX>MoiTbtd$8c{(`m zmcG6@bNc#jId0rF=e*X|?(Wvs^X81Z5*-15IP4GLoXwa^J^z4+B8mNP91v9`!v7Qs zAaWdrtl@CLUx*`_0Gf;RJ_@T+f1|Eto3OX_I$F>g9JB7%t)vx}tETRW zX|K&?a^|ARTzQGpGp48y(H~#A$s`2c?sj>s28v%ZI5wtK5e0fu%BzU6XXYo`h*D~A z>%@+Rm6i%t(@Z!tmOwzQ5D6(!SRRAV%Mw&hJ~-f`N93s+^?A@Yw+J(S``r3`k3)m? zy$3r?#95-9@#?M^ttV6->#D9UMj+~uwXX;Ewwj|SpPy$ZHs*nP+?C8`Pnkg`$Au~y zwJL`@)>$>9QDmg7mpKy0PF*#cA!?~O^4Bhal$HLs_@nYY=(O~!#B^p+W#3?kz9dS9sF0adP zv<7u-qgB~;>|t2NdPuB^tQO6!+v$xSY&HBYuQTFwMclMMZc#edVAH~i3XAYG&sed+ zv8J!(2X7QCD1wJ;C-DS zM!6QDvsj&7#k3Ikq|MPMhqP(0gtZq)(HusSpz8`&l5t4%&2_cQgh8Z`Q;0By%t60a zoPWa(u6#xeX-8ik6Nms|*^HpPKWzyPb%w&B87+MO#}==*e@b?8Fcj`9c3!$*!KIzW zz6ja&`Jh4x+9p>e?KUg7Ih{=zaWsXGEI&J>L(_8DOeaK%)z;O;;+!ZA1c&B`M8e&{ zSd(EyRPM%}1Airqb7$2!?}F_Z)s~OOis1-UWex}=Hamz2?4>R8H6;;=^w!oC!{MaO z8FD&8j=}5a9`@xHVWDBcei7++Fz(36Ls#6@KAceFLJJf({7IM7(880Vk=E#{-tE)+ z7frsre_-(~oehT3Gs7v)qt#@bP8D#p0a*8l~f z(^->oI2_m!qWQ%beO3LddNZGkhe#n*XpfkGp-d~!WPgX*O2+YzW6%h~#HtVe%R*mgR^JC!TqX3WA$zw(o8l*lJb#h)cE zijQiou1+Sy;o5Xn6=s(p$O2?y#g-tw#GaNNM~9%bjo}y+ym6w-=JEIgp%7Wudp$Ts z0jhL5><-;#0Z+vvVqzcBN_LNKoHrz<#KB8lZmgK-1Gb7F;DWFKAZFz=B4LB1P%jen zg+g|_6Q+@*i_~EogfK8(fU+0lV4;*O7K-Tg1OkDe&+9Y7k!Ylu*m<==N2|NtK3`Q8q6O)7UDoGyyPXiSbd=OWr_^vh3q|((!Mr26I^@m& zKsn;0VEBQEsbV@9s@+Me36J5kXjz;;!xJ8HcFgI9VyhOc(QZd~U~`Zf(BXuj?=*~P zB$>?TI|^jEq_wx#rn7aCh~IGG44n--!Wx7)WJG|Safinf3`U~~@Q99r6=y8*uTHG# zR4yM1`@9(PvSp|^RAise@2|pXv8rlF^}b-hZ+K9B57s-1ioUU1p$J36LGR|NZ&0aX z^hqRwLBBr`q!tN>qcOwpc024uGNKE08%4L<5f#EC`p)Y$Fk(GuMV}D}ghSD&&lkl4 zXng-^2v)%7;VxtBPYDscatfFHmL~Zk%#4a-Otb8O=1RyAQu>zB;Rg4tNUFPX?N~6? zTc4E6d3u7SJj6)op~|W#@kAC7Vlpt*VW!vu6_pe|@H?i(qR4XMkb;Xvs65R1V~8)e zgUU51DB9!kcE3M_rf*6>>>eJDrkqY6PTkp|WcJnu1Cy!-BMl%~keUq^1fm;oCK5eQ z#|*Ejp5!Mzh<{SGni{uN8F%e}lQUgt0U&+btT8VpZgdTtQ4i4#2A1D3*xDBe;Mif* zFxoP7K-gi;cKhj6xjpQ1kvsrn7muA(qgWZt9XBgf25K4u!KRd`gQ!h@YBbUiL}8bV z3{6%ryuCK7N7u!PNmcdxyp0jL;IsK15Fp@&8G&@;C?Qt4qAsFVB{pu4Gw$L81KE-h1HsTE1sm0Fv!tl*ODCbM#W&Mq;6AbC<)k7sc}(<{-^Zf|4_y$Y)UHm zW}P9raU$yx>Rxny21kA}6;vtNQQ71bx?M0r869rWW<^ri^si7+S8g|%Q>@g!fp1nm z_Y-ZxBuo;vc)|f=`DC(dlQz?m`pg_DtIBlDi9b-e74SckZw~xVkyL2C|Bcbd)Wz8E z=wpjvW3~-cJxHfDk6|tD%@UqvStE_ll?WmA-#>n zQ%Sg?yx^T!y;ZWb%DEnUwjeb54VgCtP(l-r*6S#r2~a`Rfq{4m?0?T>4)cniBd zDYqwpp;9Ax2Kx!H-V~8*AvwSR&lMd;_;bGeSIc#d@)7_N=Bd>GCHS4X)QK(+-@&i*yyQ+ z>!&Vf6iY&G#p+BLtfFWNcRQRhxAUh5i!kyuN~rn7MIlyo=4?};??o%&`WKfo<`Q?W z2=iCJ-RUE3z7=c#m{Ydh@};sZU9e0F!e&=feJ*)<0+}4y5g#Y)tXQ-l3$g1ka}LRH zR>fsz;Y7>{S$M@-*vta0ryRQk(b1a7(}gThT(8T94l-(U?=5pl)hP;N*>qCcOMW@CXq9IaR+Zr zFYoRdnsq)jS&q`rQ?;C@GfKgcFnqZHA3U#T@(H!jZ&^o6_-|vHdL_;ayvP~;cMoh< zQAdou+8mZ5GzejUgW--^!(G;xOLxA^*oYJ4IxCmeU=`+Re7Zr$~lrOYEaXOH7XK zEXO%_$K9T|#}jorFmgz#LK_ZNyU%;v%FaZC>)5Pde)r9(tJ4g_lH7i zE2r2AapBu}&y4J@3qs@sci98m%F$Hq~ zwISuQ|OUtdKXl$9^$P;FiBN zygLGMK#B~4S}~!^lR9$X6}mbK;g0sU4ti|sGEZS+Y-4ZET1(}TV;%*WN9jWaZisw5MDIxf>`(71an|;U)$Gcdj-+X4iuZ+rlK+ zqp(wW_Y^*Miw~6|+(E~P%HMVj-fkf~pctr^`73?ouWUqY*i*G5GHdZWb`V??IV5*g zlpncZiV${TE_MWMI4pw;zZ~dxZ#66v@E~Ssym2EG^wCoq8rM&Cc|6TYgX{%8Zg)09 zlOs&aY$2#JHP!F+=0kx7T&9P4AEZm-0*rZJ7!i!>I`00`pFHKf&!Z> zICA3Uu{^ZTn6lljy0EzW4p#u`wxBz|I9t~N;iCuF5BQz1{IJ?APTKmz*oDH}!7Wxj z7IZ*;XXtWj#?EFGw#2Zg_ImxOh3X_7n7>G__v&Z9_y1ubtio5hXQXR*u zxnPb~-H9P`BNz-Fxs6p(PAaZWv#DQjK&KoFdkk!F;*KJ090i?dXgh>7G`SP)@5ZSr z+&gG^!ZGXsIUG}JYMLw|m(yp3N>Fj*V3Xp4jKiJaf*RjNm%QN!lm*prld6;Wko#O=xL= zuNbMs6m-md7!_hezo7V1b7&{SUV}#Qe@sRm9~wcGR4XSLY$DYfdO!RqQ2^Ru`Rq`S(4tz>-NrB;ZiD*w4#K6`MbR?W?(j9rSV7Tpjd+tjwI`( zgvt`ixNK5Z9LSEV)LO{Y<;)QB;vXs!KPO{|vh@*os>vpWo`KAY`0qh*_+Sh~`~jGD zwFUeUuHo`OFX^wtu9`*pde}$nK4WllRU-_e1i{;IGofx#BZko!i8OkB(9a7I1$#<( z53Um+bFOTI+vT-KlTiQr?C}5$`rD8dNF4SfxcyUPrQqBESdKmFX;_-gwoYwqT$Z8( ziaMl#psF}=f0{EqJ(+An!W6ldEaP;3Lm-OHJ($cns$erln-?HBlmVv!svIpGmYD&0Z5#Z^C8cwpdI><*-%@gOm1ag9!Q zOY`Q**)~)g+z`oAQ$c33SUi)es=7iBFg>@bucO<5d|>zWFxdBc{8(RvO-qpYwCd`d zs$!pQb_BAm-s z9@w=Dx(dta6F8UmFvd$Bf^ zU*X1GEo1Hp{>N@8ZJ9NTpSyw2+rZDAHLJ8`L#hAp!)MK(&+nc;f7aoLk33!ZH1&X> z_fhn#C^&+OU%qF|2@k=)Y~Ww})JIq^-TYWrsp$|B`2?e2FXu1YpJ|)jJ9X!>Wj9UjorO(NXH^c)Ekrd{9`zP%MB2y7`-$7Z<&mQ4C8QivWD_~H z>ng=pFb7a_%Fj-2Zn3OCaji|i^zs+OJ)v;W9c_UzS;X(36mi2EtUJ<_O{NxiGz>(e zDcj+bCe2eUor%Px*>3mT%YC+x!%kbeVVkl2Ft>YlYa-rh<#Q)Zns2A84jT%Klc{Xe z3n#ZwCTn#4((P*BNhh38+V_p#pPhX2V=awUn%mtTZ-lLLEC#FR#&|onxvTbnrU&a6 z5=bz+brd>oSiA0$hQ`{VHEZ{s$uC;iSvbzAKudvR)pkR19#`mGSUU5}eQVbY)iyR< zvTp4Sh=SQtgS8e?=}}Lv%#!5u6&Pzbs*mv}`KUr?`pWVp_^xS{^9c&WtB@csxk*9C z-oc+CoQ?_BWfomlSc|SHmbGbPGAtY+F%d{6f8Z~;H|B^6-C}v$ZbAZaQ#%Lwcx*nA z(-6e#cBywj{xB@bSW^~^w{+L`M!^MM-l(^6{1zY3Nu--;V7 zq--y$E7KWYRAIiXoW(^6<(+nsZaGnZBC=T&!HXylEMCyG<<>383h=)q3NjId z8{HahG5pn7P+K%xmEms)2}u$&3uOH;ZqmihAJ*!a5ilva^~_*3?x6LBF#%z14pS$$ z4fhFKxC`<&dY?RskF=dOI`|%Pdtv^CnnOf|4I@W!ahGubrJz{4O^+Hl zpsw2y8N2)%*{xvjy#{s*F)TeW%Ui4#!|4m*`UR8+%cXQAIxTODd%T!)d4%p}c3Gk! zk0(X?0S8&x!*IsoNCsiY?1W^G>kzz9hr+hZ+pi04D6Q;qb%LtHdeiFDJuyf>WSO8^ z0?;%2Nnb;21WkF&4k@J>tPFi(pfL~+kX+Cc}zkO74bIv~2Eb$qa)7d=Xzq1ITm5RFeyrKf~k zNG$9{|KZL8G(YmeK@iw0L9U3po#+|A$6w>~;(BK+Ju(qD%rHDE-A>@cl>_J&m;}+; zCWu%fKf4o)J&VJpyL4F5TW~)GI-88hDQVK!VhKv7)}&|_LW!c(dth>;DbR%Anh7jY zP)%I5?XqHGvp6`%+0-afKxdIj2|9_^x43Z|%oew+pc%1FacFdNH7k`pGxNwH%8`tQQurItx++lbU<{QCj zh`U&`d7=L#ZS%MUFD4%_oN=B9)+rRK#Hhioy# zX!e}>PUnT|){U|)ZUq)@Dpt&3D>xnvGS_{Lpr0OH^pe=%5C5_{b6sF>Lk*9{KvOk5g}|UVZ4HKRV}}TjSMp zPW6y9D^dQP0c`h-bKaYY&%vN z%IJ^OQS~vj*-HGs>}SSWt%qmYYE1)vL5Su=DwdU|hyYC={&$Gf*Nb+P#AmkVry75h zPdca6`c@#)Y6_P&6v2(TJm>Xw+zY-#2eGaf|=xM=>$RzeW?f=KVhB+^Vh_W{i3B z`RCtL(|zkM=bn4+bDs08zbBtBu58%&N9V&M;B$Pc3xrb|)naky=*h0OMg}Ft%SfkF z>jnqUnw+|Pu65be)LDar>qxa&+kIJ!-IIp~S1$>3djPwGa(n5BY#E2ghlZ|PC(?xG zr)nve$B5(rC<~SMO;263?)7HHtolPHq z^3o~&iA`U#ow1t>cFb&ODD@VKg~nB1STb51=1aqLkGX4@6c%#j4Q4%4$BS%D;u5#w zkqAh*=7wsH3Qk*U?y+%CZAl;(${P){p-&vg5HT`_f0a{f=7ti`73HOxd%RYJ6HR7b z6v&2^lf^F3QYUT~1$;XYeK55~pFC|{<$L#4fw%nASr1er5}0 ze@(g|_l_6mbGga%jjyL|))vW+T-%z=jjxR6wvFu$ZZ3}ngX4vbX}*oHw=R1^vd4OC zWZWXUdmvTk0rTxjWoz9%#+U(0v6!piYZ^)qgu`)bYIJnUibcW$sfM|soLj6g64uz5 zC5*J4D{U}q=>ZH|*7(SqP3C_waSy#gHu~fzvC>eC$<~^M$F5 znV!n$7t3)0<MS4Uj9xpBA5m0Ut*)AWsK{srE zjN32-rq0g!7&dSE@Tym=4T(w~m2-Ua^5CK1=F)h8@ff$qw+-}0C}nROA7zACAG^3c zU988KuxRRdC(S99f~wcYRzIM(Hiv53KMvIUxT#T7+K^_>#~pASJ`UT~lg3$*NpqIs z8f#@X)YclF;M&%)lcJ6q8vk@H%291~jvmZxXfyy&KG0}v$Q+E3FSV{(U6;>~#FkIE zm%6|8u9IN_e*c7K*Ai$?*y^zd0u)$T1K6 ze>3LG|II`GwNni_tJYC$I?LEkoq87iC>6&^2ANav_m-tR4Uz`~aJcE(cH{WO+}y}4%eLNU zJrnu$SzQ^d=Kx0ZI;G^Q*E|h$b6lwGf({+iBAK%uvWNhqyx^=#j_ z;G~?6bI0??-f=AdY0zn8uh$?^CYRWkn=h9(*Npm>QhDCaHOQbbkW2=1_I$ClsWz}K zM=Ii1zq#MY9r&tXlB4nm!lAlt{G-kTR_cEo#HMcJ@i>@I@{USF!pzTdfZArbND2DutxlygA-A<;H)clZtQW?ayrcI8? zKXsLy%ywVO~iGv zRYkJY+G*5Fr7X!vayJEew0TidT z+Op2gM{?~p=}tcEZAAYW?&%=#2DA{Th)^mPpR3`c56&dhD@aM?hIz>%!#s{P#~bWy zNYw>oBpUZ&S0PIVsVG~>+BgnUsaln+OA?{bsKrbo!oA6`kn|M}CKB-!NmRC-FQu_> zU}y4?jg?3b*LXT?I6R-(xsw57EV--r+5>o(2pNc{4;4~L{OgfesZg+e1Ph2_$8n+zSi|Vqs<%?lI$ilIwahkQa+hbwcZI` z3P0|!hr?ZBKH-G!)!oCADak*X<*GcY?P>btpN%YCRnxTHw0g6s;hV)PE9Y`HvyD0C z%%!#^vyp$@>8B@GMxAT7)u9s)t}zwr7&vE;wW;QBcWydt$8Jv>{j8?Pj!1Xc6B0@N zR^ArxjJYD(9%dOVr_2Pgfkpjsyv~FR2PYAYKK7AjFgbU`Gg5 zf<&>K9lpdvDj@wZ-Q@^0E=RGaB_P>oHQsb0x7pg1jfjE24Pi?F8|bA1Wk?ca6oeWISe# z18PKYAjjh&5UBGZlJkJE^Y||Npcia$B7I`X1T2)H^F|{H9NA3H5CL=_cprn$sQJj# z94=uYbB>qtM9X+Sb}f2WET)?zS&|YlA#hL3&PSM!rBbyjai4D21I))m)&e=}=eVBl z`UEqPWhRF9+=!WKCF`c`GCNqlB=NY~UAAP!*evJcMjd8}wCW9ScUH!Xc8lmTq10+` z0W%w97T3F{^rn=aP^oEae%hL)U@PCrrD&=7*lL4dC~ahXph`$KbS7q{)@HyNxzdbX z+Fnexi1uop0l&!LkfW8LUx8_MLF+TwL;NUVPzM|!qK@3w%jO#8x4b5h3q*e5-yD7J6jBQe zL-(vYpTDxz6`e`2q-6wFdNHxoqg+$H+&o zH^K&r3U8M;yRc*~w7YE82k#X5qoEib8)d^LR~=y~#+2>->xn^bM~oUbY^kocWbt~1 z*D3j^RclKfHu;AqPrZ0e4eEFImQAC3(zL~y;~2C>(sJ(As5PZwodK_!T4TG?r2Jc$ zHoI(kqgrdAAQFRo>6lsVe(%Y9(r^Znt5Ly2hdm(fFPQ875xw#XXJFdNstVU7P>o?# z{6oESlf_gxtx6Xcro%!W2&a+)rXeZS@qfptMf$ck=f#aUyQ2nmD$ z*1<$#O@~n+!;sk?6^20a0kLP89@2zr#R&H2&gA08vlTn5ST9{h)EE+8M>A)vzI&hg zG2a~!0nbYmkS8U|fD=Ltg>o}2^>izjqT(QqK#!FWDG8n~sqD4dsAAlRcBNvDm>oW3|#aDN887=#xAtnd$ zIzS8|a{9;+Fn=O51@1l`Mc7J$;l?HT9I+HB9voMrJDA82vPM*pKY%*}@rNSc+}@P) zOQkXb-Vo5GSYV0gL7JqK4CMU2eS2@baj%*m&u4zT&N_lE^8w>r=7&RXLck)OEBfrs zj2?-uZI~Gigs{mO23fyx^qRQj=tEry|g9=m)z1~-(3jL zmz81G&bAj7I)%fcg?Y)G0r<*-{|eQ60@|fW@=YLWy3_fQj4Z;zaEvnmfDkN0r{es z&cu+_!t2L^0#hL#b>4G4DjbV#BvRBy_!grl@|TYj0Hcos1mS~P2nuAr@^g# zg0bXOe9S@@mb*$ui_{txdiEj6BA1~lHn|pT0pZI?Xaj=-y^Kbs9TXM1ZCJG&B1215eG zU9}XYdqi64%`K%obY~Ir&Dt#DhDuxSPP!8m0cNVh6J$wMt0rNtU9rrWO=?Ef%QxS6l_kVm9g&9w zbS{Ihndlgy3_*Tj+g}x7k{Jaf_IYw9Yo6PaLF{9mGMp(8YcOpHSVmUtxtBph??i5z zbe%~B(|Hd}?~^bO!|k<&86pe?VpLp1E4;}JNbn1@ z+X`5LpkFvN-_-^1@Tj}ZK`}&=z$=xaMnnO;n8*_}MOFwX&G>5}Wqy!{-}wCt z1_yb?#9Qef#dL(<_~S$ZLoT?WqJ=_jV5n9oW{~bl1z>f7Kw$A+;qL(x+ZABPmFD2U zK%N4x^jJtE8Yzmc=Wcoeizi44WU|RaSHk1avY?1;2AM*PQ(>kEx0UBNR}WaPKsQYW zIcq1fyb+lg9D>fN+~F}J;nPUjgG-jIM{g z0_z6~D=7yIccO?^ozMHj3=+mH2EzLoN*_y@(5)qbI`B|0k`r)>+#guE1Tv&7P~IqI z-jNNV{?U4)2;e<++UZSP))FRypNYr}#Iq+WNrbqMNTDz|IGIT$1>px@SY4nuJ0CWS zLui~)WGjF-{TO25BRqug7n88D6b3z(nNTE~k(d_~ytJQFoB0y#+UT8BdRr4GguJw= z^rYm4A@T&{NS|SaM6Z~2a@&l^{A#Y)ZAa-iaY|%-J;1G=f#zxFIPBPGKg<&%62Ob% zqZ4Oe0Y^f2r))SR0GwG2jzw~>7*I@O^#b$07*wUK6%0~X>FekNk@&4|xaEpNlcP5a zRuWNIVh#`+n?O-PAl$(F!WN`~P7f2~Eyu+48O7cqLO0~<3!HUOj32Oim?77uFvkG` z#kz?FH5eOBAYy^2g|(3QBau@SEuHMeyNYkKyf!K+olmaZ|8}A6Mhqlm8(0>5e)?$B_bk|KwVK`T})+GxKOs! zBkNue&1NfAES}X0Ke=3TSxcr9qaC{===Ye z%cOs@{Cmzx3iim@4>I=OvEB}*!9A|Gv#^QcJuOQm!{>~>qi?f5$xy20LTZRhw6jb@ zsiLmTsH_TQQd;^Xm9@pn>+5RtvMhFIShu}}X{y$BzpxrvY?Z+{Lv zG)eU$sUsXD^9^AU;k$FRX(-7Q6P&CUFnr^3Sve%CO`_2{4QdyYz#^gmDs?m-BTa_ed;HH+*Bd90J9&mGU+7Z zvp|@7KMPY@q+_;a{c%g!D(Z}gTc{^cQ7mOAVqzd*c}swPTb`equGNywh;U4}x(Ael zRBd3QQK{x}1h85n%EORjQOjmkV2P1M6XX8PdtdN^y?aGPg|FVsJA4^B>IYokX7)I9 zBu4HGt}JS9jg>C#1|GrFcKthl6QpQYMR(o zZI(dB2xZRe&)9^X%H+in#Y7~k3Oipup9AQR>5CGF_#E=w{esJ3$MVKl!7L26mb*!- z9$E+vR{{5u>8GY3vIeJlFdn48CX~SdBMj)3Adb~m2?I}C2@V*aU`ZCfZUFS)Nn3p6 zb+bB%&KCst6eB@e>82tB(@at%=5#og!x$2b#HjCM}9bBC*uB2yD|jVQR@Z5 z_U~BAbI{wjp|_IxT6{W(p*Nu!XVn(Ekbhmff|}IU@zw=PBTDvoNn0C@#h9RkUWI}w zgvZidGv1GsV$0x=Iu4Eo(zmc2z9E&zzlI_ybCbCZIRvo$qAN@Cs1nSIa9feeKqs{k z!y;b6jFd&yTK$6gZ1Gq5k@sj%S{D^sS%+-6!|I(GJD16b(Uo^BLQ@>mzuTM}NXJP( zBMcRD8a|C!I+w3S-MmCV$sW#bcj zYqh--jcwhlit+mCBxp9mCsC|MEq`ANT<$_Xv4lm(!cR7@z(a z9F`@C446F**65`k{odb`kM~-@wr`7BO}k+T-#hM&8E|{r6QZp zA~cO9w2?KIi7D%SjO8q-Bax4H{#>+2%jo<$*|y$ z+e}`qdBIw1>gv9`b&iGshUYy8?69${?|pOU&!K-*3FEXAo4V$j6K^N)uEM+fd*0p6 zd{MDbI^NaEj_r#Q*3nDO$mv)>{Vvt-{-eE=9Erg=;l=9<)$ilNQgC~WQQMaMU$Ut} z9&;h2qoX)S_;FLsRGk&GuqfgzHjsee0`9VaNIRwF27+-V^ZBBLENgmkND94WTa znY=+%S}5_f^UXFypNsjBR-@iD2=k3lv_X^h_?}X!jxPFfUFVBQ`a~=zk5JZUmuign8htt_E52Dh zQJk=)r<0wnUV=k(ZkHk~UVR^7b&j4$8hwOlUZ837WYlsp+M^KCJSrOD2mJy0DQst%kzqqg>0f3{kd;7V^k zlUYaPOa`YNTLad)>fVo8d#%r6Yj7D28rOscQObnvCSx^qq`HLuYfeIHlX>~MRG7+6 z*<;IT0C+-kjrzgS#s*9n#`>+`=;G1<#lpX(IWzqX>$9tEFhcyXxpHM|r7{)_5EMB= zsk5TFBXGv}#93)SHpxKr@I4zgUX2G|-kVeZk3#7u#kbVYJap>e@EFUHay;Hht(6>6 zTjt4kuP#%0@}0y%vWN;|=^A#adEgzLnUfwkdQ`}ZSJdn4#{cWyrWS@W|LHC-o6D<5 zPt|YZ$kERCPSNjUuGhcgI(qLVruwg}-&%j{O1nmo(+^^|d=a76Rq$~1avLL2q&Y0M znp79$Icw{k97u^ocAO-K?P#dQF0lu-2`BDKX{okY^Ot(wHamq|HmmQ^`#szY{N}1d zgT)e1$K+BzhoCNA$K*CB$ufgMW8z-}#y=AQj1M+)d&stJ%yQxeL$Hpz3%5RMgnul) zRd06ez)OC9PKpJSAmPAyFLYnFb-6b1EX1@>aP(|X`lF#2kvERFohv$qWn2CMX32=C18(8x|PD$!SCWr zMWoKmNVYI%=O!|l$(%h`puQpy9T9N6d=er}vd;uFS7r7`q-QI_y25fR=$SzlaVsVog_dwy&FydfpkluPAv))z?e zS*i&Sv|MK**SuN9!!F^^JT;=3RUdBl5@R*GmzHrHDW<8Zc!-Q#&eNZ9u;}|@$Vu^# zV~z-fS(RD9aaQb&M0)g{*f4&Lk}{mlg2N(FFKR0mziQ#aMtua+=X7Zt)RD4^q}5^E zjqp@;V9%KGKX>!S@jjEXnh1X4L;kqvgkDeF{~_G&!?kQx{K!B%WVK}ZgIzDDsD^0j zf`X(kbiHto(Y?Iu=k!}yfCj{=fbWA8rp?|80XhJ3JJqI#uPvS>|8V@fD> z8a;X;0V`feC3?0Htyt+uHcplkMZ0nodhLc1WeaVptAoMRIX~g(V4OmUgzNxd`%-Yu z;~z$)K^lB>DqSdyj?K)DZ(F}%Tefn>brCPoRvEAR_VcNqFie(et-g8R{&hqi1yg%o ze;oo_g{8U{FVuHjb>)GXp^bBM?W@Vie2vE$de%bg~Ms)9F zG8{^#2145~-Uq{(&K4V&(dijK;64F=p-?BTD!a4|EE`D}_D2?tXD_C>)9%hYW!?5T z<7ShU)<0_fW7Zw9{?T%Vw>x68Kx)688ZM6>l3_?@7>x1ZH>bk&S3PHWvAr8h-tbVV zn9uoRgbDfLw_P|mgx_^^bo{yZnO>0i$iSF;}*mK4KCjxkvrA+qUrs zml_uu7d)f$z@r~2Y>r=^dj6Z9|0nm~ao>^XmC={H@wT^j-q$*8oZor(RmO!M`GgVP zoVYA~B$h$i4<$fkLi+@EvenCpxx#|=oAGnok2QrJ*>b6>L zj5$%T?JbSQ7CT!+FCWVr#sCR63)wx!NaqraIs?fh={QnJS8au&#-7f14`pZ4>0*Iw z@XQ{gIYFbfo} zfC6oj07KxMsB>bP_ue}(_}&KxC-1!vs}@0L`h=QnyZvM+X98=d8P9ley7M;U8JU|q zZ+q|H#J%^bvji$2iz|mO>N{oANS43%4R;Ja_~6>j)p4Pbe*@b#PVVOc5RqJD!3kQB zf}GG|fF_ICg&CYSi`rn(Lp?zDn4MCHYzqEBaMaRXE%oplsbkj8=%fTtFxi8kijLxK zY6V8ViC{6)1uKdQSf=POeTWyc3^&FRpO;CDfXXXl-O=CLs_dhH*>7 zXOejy#fGP>c*+nU+s+TJ8fA*BrBVapfK%>6eAogO%2(fQd6F2aH(cWL|H1he%`BMa z?7A}#?Xk>5d(PNc4+IuUuzAADE8?%lJeds>r3danmUVjN49ncMb8mJ%DG#R|IOkmB zyz>u_oDmGoAH@1k?Kxy%+-aPd2-?FQPkuDSdRESjg+oR-NL36azQF5o%K*E6DKJRg}Vn=4;C*5DYv~|pEV)1bZRDPT4#)p z@AUYScA>DGRmM<*dOW5l90##9PaKxvh0C}m$shiX^^j|tig|*KY&B91t(DGV$5qH|Wl|06xQs|$%(JEcs9bCPzqKgaWMnQfzG4sd;iesq{D8*ZK-wr zOUFKI6~I~gmg{GRhiDQ=Q?Z7gOb#wSK+sLi8G3?`I!8|}0b1+-syU7b_G=Q94`NpP zdu+%at{-P#>ZSz=dm=kp3-wH^-iMNC%$Z?@j*2WM`O?{pgANIB(9ELDF+RNu*ihYV zPf05=NAu8H1%NO#V4X74@7oXw|0D*;0Bo7OAo&s&d^~tO01H)tU6#t2pkLru7rq}M zzM=~19Z8lV>sN?`HrN*jDV1x)e1}`dYwDt!!q-K(;7`1YBI+mgR*c$$Ai>lPZ)nuF z=hEYa;>v_U0*G8|q(*IfR&bCrlX=YtHeX8df>QOO>FI-+scxO zUg+)z0jic{DYTjdL&ACRXfBl1N)-*|x#cQ6M#WGNGz&mL(m|AfSUGBptpgr0S8Jnp zy4wS__1^d_iM|y$TBeyuVk5=BH9a6bv&>4dxOb*P9M|dXXC(%rXf77dQ_Z4%NWZfu zJfV7O=-SOX`3;31Cb@IpcKwpse5#k-?jy|OB>QeIwIWTY_VSW8FH<8+G=gp!31_mf zhX$O2(9_!XymWj;Xsv7r^bga+DN^l_CJ_QMVYHYpMN!07GNsBRmI+x=tMoETpl`dE_m%K%=bcg4 z6yQ3(UoJ&-NRY|^VjJ8}LIWq9E+;P&pa%fVrBpaoX$r}}rJ$F9$*Y<=U7xu<2x?9S zx3km%zFeK^_@oi-Cap4ayy5?>I3@XZqu^#X>f7aoOD5J-Q70C4u0HWeG*GZ@1vhb z5O){ndc9l&Hq-4{`cL;a%N7mvi+R%PzQ>Y=lycAy$Sd7T_4iL-p!ewF&CWyUUh0Q% zZmqxQ)rAEnOZ1aIOaJcnY%QHt)ydU<02`RQ6FbOdUSSIQBah-RrT+GG2L%I^k8VT% z)+gEhyLwOG0~7-|S%8bxU94MTz*@`XL+#5(qm6WONRnIqcyKbINHG=#$hsnE+MzU< zKt3CwouOp95ml9kWx|F+z$q0%9K(~V8>vepnXz6Y(HL+~RRo=gfFvdSq~&IY65U3y zZ!+y^rL%(3$WIb`WI_#CkH0EtJ<{Yl0!zSL3azedj0nH`~ zg0)0aQ;6g9xpB+ID-l4A0!X|>OM+1ApixB%k)H_ARyK%pNtQ??Xe3OQ%S;%QSPscY z@x)+Se9Y>Hq6&gs%kgaJg~Td};)L@kD&XKOjB1a&^IJSeg1sdLo#hk>YLesTVJ}`^ z(j|Z~i+ByABIQMi@5d*Gg&64?upxl61hP9|M|Jbu4p-6R((g=!vI3DJk>%Y-E3-%j zfE{*Ayth#%_ygVR#18o>#DG%4V2T4;^`_UMZ5lE;TA&8EAOSl)AhrY0aJ!^4C*BPCcw@S<>FAgL$0G*MHZ zfmo^zM7q-k<8rb_q|SDRMo@VKJ}FBpI95~g`fG{KRiRuYTBTgAKzoU1N{QQQ1n78T z+cH)wh=g2`qi6`uQOuaRphQ1*UL)&Hkbnu|KM8*m!2`XQagm`3dd?SM#GP`ugdS@t#^Cq}Etub2|h`zfMf{2|!wa?vI%#J;U?)5YD_n+}aFH@A5 zp)j*nE#QkiF4)v;O;}@h!D^^FURtde{>=e}?e44;{ffrMUI+sXwz(6)l`zHbinf4a zv9_#e{Xwsbii_$>Vy!Mi=8g-k?y@PoTR|85G~B5^sb8WTch+po2cUB5N3ssJo-sDI zNw|sM1Iw+1<3#qvy}kwz27dn_4s_lyu{W_W^lA%WU2xOb*fUz|_xt_Z&WrienVqdf zi9`jDO@U@H=n-IE5{aBZ-N|G!Xg6nx#fxlgG*&W#m*)=g&ZE#G0gXVug(xS$yCjmE za53I=DikO4i@FF9Mv&L)^Q2E}G&Z8tglC&}FqxbLaZRNCiZUXLdKoQMG|sA4zw_bt(lP{O%<2Y%{|i%CEN4d31-$pytC+ zY9JZ(7h=(}K$TO!7i$x9zaZE=KqeCKhLT+l0|iLkL&`c+-)2y*&!<~t;(LZ?2-TCa z7eKueho$f7j^X3T28xlKy;8_tF+YEZD!cAu$MDG-$Z3{qz_lKKnkzsG-hxJ368t(y zQ5E@y7WJENol@gWX@jM&p+$oh9{LB(k8~asu)mm>`ga5m4(6Zw!~7tLU=ia{01~Ol zy?g?IivoICR=?BdrPV>JwpulR@J^^A9)9YrS@7>AUAdXHt`5i9x#~I@IE({FbM&Y> zh)inq{Lef%Pbm*dwdprv3qz1m34IWd!3qjx$zE>VLh(bxrVJ5SQoe&pdTN` z-}?$0=`#p8;%6T|iJo7Lep5-m#@6_+T*P)!C<9&5S_VN8LSlYJI3z6fBF9V z$99lbx;3B8ZmiWdX0!RVtskn>r_QQW8|6Y_$LRg{Z@vHiQSs`?=^N^6r`I=d;T_Wl zIdfTEZ@y67MKv(Cr(7BFzMgBhzTP`hDeoCO;X>6I=T{qzD*YSVUFko+vb+EMjRXP~ zvYCzb@2hj`@||(_x)xpY*5lS5keRo*u5{fBI`dl@`32DCeTpIV^TK}K1ZJ1!dybi^ zf9zBLwac4Y!FEPp7vsuw-^JtqcOs|W$$X*S0XpA1;3e|jE?1FTzt^B2S01v@?b*_m z&HCTA{j|w;%0g_`JAVA44{Mjms8-IJQj`hHNQ6GvdAlMwMSyEWyx5&qdY+8^ z@gH}m)!(n3q^5|Xbn{z3-ufYw4UbovpI*BWM%Uy(?1L{X6*Ae4{j+OhHdA=0e?lEU zLH3N@uO^+`LI&KKnKHhnYs;|?Sb%r9rUZ)}puYcTHrU_`QK5m_p1!8W(mct;FsC=& z)J{+2aOQT*N(Ms*?jY@7j!=JCr4g1#@HN~?wT?VGMMlP zYhdX`^GM15z+CUbxv}x*ZOhMs9hX_WW$BieywBKn!R;^Vz5JsuBf6)r|S1k-78sO{?ECYnyI&1ze$HU$y=X+xTJE zAG$v7`a4&L;=)_8#oS4&T@er|@;EC?PdM!wv>h3=r+6zHp~#9)`)GqX4ax^Y{0V0` zRswwk&d_%D-u?7tNUR=Qfa+q$<=JhB>#K%`Zh^F}Peyvx_}`qO3OYK5jo#rCZ6|mN z^_5O|W49UUu!@&hXji4Y(onBgY^Z)NsKc|4`Bd)HrY8{Ral@l8bF44@jn8Er5Y^r= z0;_6Wg;JTOqQ^`=H&@8d<#RL8MLG@RM)3o*Sc+Tm6APj_M=;4NTZTQ8RshmW zQEUFn|=pbm#>CjE9w`tow!YLv)Ssiha{l!S(F~^kATd752 zut4{UP&OPZ6@>+iqOWg5bRoO=;W@cUoiKY3A*`LwOyqu2V$>chuXMRx9Qy>+>+=1_2I z)3j)Hnc%kB6%2@&8qO;p!mI`bJU4zl1JBR+p^=eYwc0%X4Y!I0_l<0AisvgGY;Bs? zm+zR}p)bFvd=Ba!WcA$mAILN@)#bZu1L|@=lO!H!bUqc|!i(jsGfTU?Aqc{^d1f=- zZ}iR4P2;-aUtw!rApJvL15mcb!NG%*6BkcQgG&#`H;aAA@7`P9E;gvBuRXg?Uo^C7 zSk%#EAiFgq7N>y6RG`g+2PY;jo{{G1u!+eE+ z3o8X}`f=k5nXlD$jf@-`=W597y4exaXilw^R%*H+E3r6$+v%L$ujxQ@FV zQ-a&P+~de5lumVW!KUG^EwWNtWf@x;VILH-6MR%$W& ztmsptssU!|;1V^HME!2htP=xP)Vo~Yrr+O|+y?3HbocU9T1v2CAB`~|ZansN>s8i= z$-lkKbqn)B(YRx}W*JsX8>^dk9y`hnu3_zpW*O?Q#m$n1dM;kzQ(b^<=y*SyvIaS2 zvK!Vd$(^%vFFx(G7thY^R5cgOEsG2H96Wf>!r~TTURGk~oH{-?w^Pc)na3aZxH~UZ z=dN9)wv2)DEWpEQib$5h8^aqsNG#jvnbm=2bD(J zSVAmwESDb{+DblEIhWr$G?LGaVW}f1#A`e^M;u0VM{RvZG~3Mj+K%cX;NDs`UQc}0 z7p#}NDoDq!HY=;v1RaMErm@Ajwo}B}XL#n)Ef#H{=YX9X@|}T|5W^@gLMe_ZCOvcC z_KUu9&e_j*8;js&8yB!^`^Y7}zw5ly+ACR&)X(MJ<741LG3M^uanWC#bM_5xYwjz^ zR_4o!WmbdN`M}C=xxVcB->xUIIykbI*!gq@0bEIQJIyFG>tbhC z)rss{4)yuYRo(M@9x*M3Xo;&^R*~kq70CJz>yw-+M*VzJZ8y6v7^hD?jBkBQBr^z; zim=qC1ru9GsmiHR2GVifXfCVIcbISQnR?G^Jw-j{5K*;RflCrt69U$W4bCNIPS58C zSj~kU`beNo`1Ls!I0B_85uHG!r1uUDP6u~CZ+vV+QW=dsoFjRMs!b_V?EgBl4-yc< z4l}mD1%v@w7+fa2+;3C3bTn07uybQcDWn)P!wJdjUcL2iV1sxVzM@-Sd^Ci)ycoM$ zsMO~fRCzZlrfJiSvUpwvUNkkKU-L4>B0ceme15@ZHAJL(-q>;;oK#pUi1fj!N_ksF z3US2!)+XI6-DiyM07a(8vzduZdw;Q5LsFpsR9c1k^$(TA+opnkvi09_qm{zoDj-=1 zdVQm55s;AyeTxgBR)8|$fBi@CV5CCHrRnLH9Kcxsj41w$YSLaPSC;GI0n0^JM}COi zlf~jM;B6EPIq;~qj$aX{?sYcwHJU{P!$q|I3@R@oO57w|GxA4LxijTY%#}(Tiy{$v zt?`+i#o|y{xMau^Y{Aj%j08fG=zcmU2c|Yd&%M>1SE1pSb59;S*tMeteah z(vOfk&~%LxCDq2Zw@f_nd9I6G&j5|j<>|LT^o3wH7PKW@RR*DN5+s^x+9(T4Wa0X% zW-}%=k8F>&rKg&T_%m&A=ajp6$Vc_Fl;hznYFn!FXg>p}_jf>jFRib<^YYC@i94QU-ST^?U{X}fMc$@L!&09~uE>wMRuesmdIy^qUt73#65`RlR@*aX_yyK0=#qDQb zXI%fz&Oe*$E(NK=+wpdOTiHE6erYR=jBke?{QbsXuUA^R?7#OX>& zxg|<|p)D?%go;*C$%d5GubuKKPP$+ZIjWea+4k8fN2@F95Ev&Lqxs)H{?;SE*9#!S z?bI)z^Q+zq==`$p1sHMX1sMNE74PF;fTisHUhE2=GlmSQd80;2F9$mK3{eBQ!U^lq zOO2?;eCoqQuPvzG6beITX~6IiNz_Lqm%3M%Sl9l397`OPs2^I9GUj~LeCn0nWWQdN zp8i`Fn7(ysVQyYZnDyEF7){l9gYWOcpsG;P%*r%ByA?Lc$d>pAA| zfiOzyQMgndn8yL5Mm<*7ai%GiR4vt#{zltOaDtO{e2ev|xJ@3j^2Xp z>RG@U=WIsg@Z@ zr6@;>18_7muqj^{jn0Lr0|xq^C*ZGS5>k8F>ik+i#@%=6zhdsV>n`~%u?`52@-gqa zyZ;-jyqv>t6YEDRHIyt(lHwzQhoW(d3;Zylz!W-;#%yqK0)b*ukDpSkWU*8~$s)P^ zhG`RjiXi84AC{XDZlP{T>d|B=Ih0BbrwdqRCej1*S$ibIDIA|#A)SwFt;evCa0vQYP1pY!iULeigFiDCigyw>!WO7`c*ve&qgs(&l0cmjGeYgBpLn%Wf zaRdl6s&D+c`|h>vUk{sdLyYlK z=_-m%QI{-Lw%4XrJLz79;NNWN*xXtEPz!ywt*SRRe4u@Dh}lD>@l2bh7bq^oeC0k; z1xQO9X)XuyOT;&;nnK_o_Xf3yo_IGYBDH_)RyL zjl}qRFaxCKo>`q*zv1yWjoi!5R6Us*EB&@~Z*e@Cs;97D{2WO$DiwN)oar@LEY^2! z6SUcg&nOlR!Xk*vHf}%JzV*?e6qi_5k;}IOPY``gbV$nD8(X5$E%G-y%=%}7S$5RA zAO71!^1!FnYU;uU%CF&TWvDOP(sBOUR9coDMzeAJKJ&8WBg^u0WVv(n$Wano8tF7S z{IR1)I!Bk6)srJ@p_&{$dPL6S%%jI#$6W7HXQ}hwW#AEAKCDht7aUgoa}jg;D{?EZ zcilz*nu_U?>Lg5WR@+CN;Lcf}GKZ2UCy@YrlT(~FBofgcw&13n(gt^z`Ua9<8AwNFO zo%2ajxY&tsKf0M5uC(m)XC4Iq@Yq^H`La)#6OJ_&1)S|UXX4`BE#tGZZlp! zOD|P5wUj|4s9|=`*xY}zY|9#n)k^>2#4O9P`s1^{1}d44GJj%(B^UyvIV>Y6T7j?B zwv*GxS-$Lib@GgSzM#@Qu17cUhCu*YJ5_IQJs()XfwBWL zt!`2td*SJ)y>6?^m2h2yZv7T?>y&GOT!$CC-s5`0^s)x0!b1-BK_OzA~TWd}T@^1Qk_)cL{R(mII ze}0kTLJBww``}Zm(n;kz&*=Z4K!{<};G$fQTnV)g37zD?Ux< z*%5NOMbEXnM4#A#2z$?~qnmmb3Aq^*jAceBmGwZk(d`#c?X-lPh@gc?$f-m6!PJWr zaZ|@W2~|7t;SyEw)TSOItx}Q<)fLs?N-SiHYmeVGkYF*=Y6s7*Ps*wub>C=Tcbrne zTAVfi=I+kNOV^e+Uumzxz647=7CjUPOvRcthw?5|@$}+}3MJI+MIFObH_7 ziBblvA}0~5_gu$G09=tHoZ(|pf{pS85Vv-zU>6}0a=jS@ih+x@NW&g=TUm-cW@lL> zLy-$5z)g$*PFIvnC7`0uk;0&aQ4y1bGlB(DENv2-$R$@JMu@9|ZE-B;iDzEDabh7R{C0O2bG)i~~Sy z_(CWo>NBt9A}#w0vY5z9NQ1J+wCG*H{pQIa$w{hd!TR%=BTivqc@s2C zZ!n|CPrzGMgQVUg!>qz6Re@Qa6YQyoKZs?BKsY%|zY+Q&lpvBm`{ZHdjU)Z5;bOeR zEhPO&(mtuQkbwLEZ6_Bg$}}7Vb}4oy;NNuBxv}((hNu}RrX9gtRCkd+Lk!NX2=o;RGl?pR`O>h20_bD45~y9^w!r|S>>|WQ1wZA)nT>ZB+{(`;bQ%O|W6=eO7zn*~0tP z6!cin8OK8lQQ_&1Na1ciS2E-E6oC`o_oC(HB>t(m%=G}8l<6802_^s9318M3LQ5l$-qCC+MFu6_` zlJUr~GMTs0IVQ|kPIyZ7DW3Qzv-K_& z0}!zzy>6Z|Gs4Ylpqm(_a1scII?QPbA8DaIUr{3gYOic?~ddIBscUvd)+%o ze*=XRf(ZPbP(Z+aZ1T=UaE~@m=w%sj2zMgCg2^t+OR3mmqSe+nA`l|H6x2 z#|kH}!U|vd@b>M8lfxX~Fz2|9E7km&<+s8 zmt63{-MgM!HWQQg?Amo}uG)Ux8E4!H$Re@3*q1+};>ph>TKd(lAGrS2Ab!nx3#s{G zGyq|x&^%G)G8vS5gy7wuW2VWz#ptRL*Gr`%-)mluB85I6c7)zXbRd-oCyrmo4z1q@ zzw1DyVp3|L`thspLdOH3Pwc5t(kN{yKa~8=rXB4egW;QaGbhcJr{JSnwh77Wo4T5$ z8lLW6-OK))HLUL%DKRjP3R%1WngRerceq$^osgsHQ#_}%$wik|<& zxmdI$9RWd0^K0R%Fl+cF0*)h)(}1+ml2e@KL~5pqm2_5QZ#Au9Av6sErBhrk!kW6f zy0r*r?kH_YCmz*;@fQ@whH3lUU(gc!>TZ#jxCx>ii2>wqc-RDD*n{{b##zdH$nzTJ zjq+%+pm7&1YH>&Uth~iMjNDx*6w=*`fASLw)M($b2LQAX<1F@D)jy^pvkhs}59YH- zhBV1KB@;)X%kMP@<>|XM?t}$W4V^F_x%s# zqX%4{A^Oraxnn|h^ z6{ZCBYQa&HU2h^ayMP8wAVf4X<9DK=E-&L?)vaj^2&Z_dvrK8?E*lO$L``+BdrU7{ zWy883)Uj;1b7uq~??9?1xoHt#mE}E%B;hS&SkBP}g23TREoGq{7LrU-!q~vpv;~V7 z9D#6tI+GcSHyRRPp_Bo1taQqUrH$Y|%baP2g2G0nU6~o!ThpP4%D6VKq%2V~)Oj~6 zdIW8bXguN*B?18T#bZQJlVbkPZ`RU`2xXIk+F9#j)oWl3vXRa=sqcgvJO=JT+%Q(g z$M=V!?tByIYPqG?3KXN!ywAAG13pnQ5ScGmHl%?ia3fhrt`G6h_#6U-#PDSk6I&&` zh4fWt)oKwjI|&scx&sA&rzL(3=v6?{>Y&ddl)q72}z%J6vx=-{>Qge$Oi5beb0{ z3g|xqDF`)4Xe*R5J1J0N!&nbl=rBbuSbc&_ziB!%bjky#QVNXS7+pz^*rYqo#-hwMB9pb+q5YScMg+cH)d ztcdS~D&3vSPdH6oV7{_24Wx7$w6j8SiY>-I6~6UW$z5B5>8wy zLnpB^>0r19ICjiS`E`Kdnc~!8D@Ms(Q`4O+VM3L~aRmrH$;q(mg>rccZVu}frky#B zXu;taF-6@g2FS5V3;~oM$?X^$J$FR)%Q&k3sE+zr68Le4d~4lPTNHAdt%L}H&SCi$ zMTGX%%4S+q}u^n{l#NXn2qw9mN??V|Uo3p$q zCr+YY)o_SKDYVWCqE`kjrZkatwNBzJFkLF}VS1*;lh9i43QbFEt+lmIq8F-OJCKWm zm!R9y3yn|}A4RMEqLJo9v&QXbzM8Sy@yVdXHKmd{;=({Fg+UOj4oZD+N0>}+cs)VzY*Bc? z$JV0+d`HNpWXYjq+dw)sM9CVr$4>4^z>{(;@IIFQQWwqXwH?(Gb02o zQh;U-k9@$N3eR0VIyO(egaSm&ZFsu_*3lClL_OSYLI`u@Xi0I8aD=FLJmTKYr@aZd zSte5fJJsuvt9@P@rsDahsMH^@!-RInsJDWRpp;5AW1m&MKjDdv0=FKG;(#x++L%(d znBL?f|46;opzZactW{Ce{kY*XaLt5w^?L`T;?WuNn}~Vcy_mxTJxgVcBo6%Q-ie9J z=WFZi9B7bS*vyqiX&m}AT*Jryp7_R3;ei1e5T{;ij-bjR77Mm*kA}S^G1o|C@f0?I zNo-m^DjpgLwXAC{^SudD{=LM`kxNaUW-N9`u3)gkZk>EBAuL0w-M_v&M zu6Paqhwk)*=q@__nClMf@62DZM*4{cOS&i|l}gkSDjT#(tuA`+sZE-Rp)urdgK15b@pa8gT{8ibCn|_K zG8H%LXd366h6Nqh`*hHau=2(ES*0au&z5$~>Y46qs)!q%hr)08_^iiS4%BV%!7C>2 z_+uScZx^Iz=yx4-5JgXZrSSn|c5`|ib^eqdA*>Tz0HuD2QQ0tSB9WoYyfhsLZ7ED_ z2_f8>SP94{-1r!KGXnn<(MF*`F+u?gjBw|TQz}A=U}mTkEAl!uZ9tffebem#m0l(0%;iWHR9|1O61CLO=$+0JP;9OlZ0r1B$WQ; zFCs3$w@@JxW0dcN&jApJbW8Ms$4$&Si%WhsmA1z(X^ym#WolMSZEAB z?f@)~K0k>I3T#A_oYdov~qZ7bW=&W4Kz`H@fTmTrM8V<<8$dOihTP2HDc1d-nl~?N6l- z>>S@q;roGnVQYQeW(mU-oe%X{@9QeX{Gb(RIn93rH%R;z@8XW%<$G*6tx;hXOjO%s|?e+dTwBQahBj zUB|(94~a-Cf$_>QpuNu4zjnTCv_L95{ru-2mF-9Aqj~IeR>ykG1=??W$ZFdb@xPuFA;Uw&1ODtw-~RTOjGw*X4V|=X zb<%J6k^FkY@dxO)lE1CnkiWyOIN1L=@PZpEa-6pXekb*Gi$<;49GlZ4AQMVEpE)M^;w$?OR!SkqxqSVsdOr7XrCv*n z;k!}?{qd6=OazS4dg96}6ZKkUhcs}-6I{skPh3IDBfDz9`>2}hU7C#ZU`1odiWsS( zkl{&964+pTc>jK*@Q|_8`RqfTAMM}Y`O!n2&l*b)83psUlk8L{3sy$-*iWq|RqeQ& zU4Q2Kf8nvuqxA~0cW~YQ6}D`sV6m1gyn4CT>#LIcw0KAvCNXD%btJ>RdbA@Ee&at953-Ska5!YnQILwQWg zZVhAG6CdpE#k!wOiGou`3MVYfcB~mnvY>ElHZ$X$^$ZMII3myE#+W zPl)(DtL750$USg9(M?G%Q_tL*n*kfw2{vNou#Edf+7Lf2)@}Y`E^wRgLcd2f%_e-K zIeXOS<@cC3fd(cHN8^nh!`OuYDBCLNWtTQ=zXJ~sK4W5j8%gg_HZvOV0HPH#edw!p zfThd;Pz{@^IC5pmm{VMb;L|xqH@uivu~Cxr9XEm`;>`FnfEK4Pa6@Qfq?$6`Cnw7F zQ^?~w3I?6oGAHS|*lI~C8qD}-N~Ko7KoUt|m!!-!=yPT&LB)w8!nXAQw?c;9B^Gb7 z)L<8N@8UU#B(f;|6wdfexI@en7|CXbyq&Ki7K?#gaw##m%L8I5m!KYHRiOckLLYIw zcHNwiR^PNm|Zh0~F)>uq22$jy`94 z>Y2uGHOrTyo1zU zlyAB-3XV-!$T}wvMnqciEX!a0AKt9K>%Tl=OGT!%P`zZm)*lA9yMbqP$-&xW@5w$( zCUyWhN5V2SXVbl@&x`GWu6yeY5J8>2M0M1!m|>~7nDks`z0&?>vKiSy)c<-@|D9zA z%+zMs)P3dssCRU-UYxXR?3`5r;=On9A8v?#pmYTNV$`khYi(rsHWJS%$|W%6P^ESm z_ip=t)j8EaH8{LoMoABpS3pMP!=j+ajXi3Ckib~Aliz)v)LqqMt+(mlTza;--Vi|L zzE|1n?(elhrg0-iqUTzL8mo1K{xVr7;Z z!;GE8RjaV&VVM>fulP7t0;kA;o^(0_i?`O&=;xZT$UupMpIIOqL;|Gc9kFH9 zd3tAZ5yYW;JJa3h>pbVo>UfP_yZsvkOZUFAu=iQvAa&<6(}itGXQ>`3A70_iyu1Sa znV*@RQFcARy5WWN|h)P!f7Mz}R!i5Hhk=D1v1 zkWdrZx%OccAVW=z^%v{ye{>7?-RJvMn2Y0RGp% z84@<&&HQ#=%bdIr!h1BArK!1@wa&uzx{uY{SfdLDfWcHJNFTHn0?I$0`h-YfzTM&j zYnOLhe)=A$$sz5hH}$*sKz$GkIJ1-Ae4wed!FEWK2bZbKu#gzQkjqe< zd^zs1s3ii}q#m{W<_&NKYmKDxFuaT6J=S9M)1SqW3xs9K!A27@Y(kQX0EO&$CtR&w z)GVVX=Zi8Te^2?}tcyE=s!R+*eQwq{U3gD_%X$%sSv`4PQ#oH{4`E24wJPLm9|TA@ z`0%wioP6!Q0Ty*7IIJDjr?I?BH;HvU*<~DPrc^|c1bXyusN1pv%TCo7;SX#a|8bU= zK2bz3MKO^@n9y`jk17w&Xs{qSqpe@Vv|Dya5!~Zm&Iyv&d^ewEZeBwi_TITe8tPp; z3m8}|7A2RHk~=RD!D|AQ4v_vh)qVIjOn=e_8l zni+1b-~Nd=WdIqyVYNDGOJr8*ip*>nQ}2FAbS~M{%8hiw^$GP2!T<^O|t7CHUUP{ z!lc7p*BA@$Ae%0mG+7qz{2z_=+R&pfD{_}91Z~mx{*Pv)n!Bn&(Ilpa)ww>fvzj z3JY0&^8yr&JBM_C(se{a>057t=?yKp@_S)c86?=^#`>WrIEaA3Yk%{IW*X2FB?ctnI;sE zSf~Uuk=B~BC4(#v#n?*UvKcn`Fd=5V0A`D@H9YTJqfeHj^XtWdp+5*u&by)|d$!@o zYO~_L6%h@V%iOl0Q2JmiLUpdx*E4=|X}dn(w3$r3R`rGmZED5gxsXTL3Gr-0i5KgL z*h#QNxB-kIRO%a&!p0UKum$$%zy=3n$855RW zY)$KZ&#BwfEm`KbpWplEwR&2&Z`Z9lRdwprsS~A;g9HHl#>a6lJPgSMY)x5P$|3>_ zG_Wjjz%A07nDrFcglbYFw19$fBJ+#gX|{_s+Hq((F1slyrW?aPEI@2GJ*95 zKTUR()C;J5S>PkPV{k%Nfo`d=prKDhQb#e4vPBXo0Z|Q0VT=|9F|5eEl7&rFF$q-g zCixk4o&2=)_?^6-s4WZ-WL0n_V)rogiG)J56q`hTD)Erle3#dP-83K$Hq0o{dkQ=UH){0L6F70)31;A(oW!E5< zgTx5m0BXZq%IM@B299AuL7F@oev)=duy`^#vIGw|Y209>#lpSXCqqVkP;7(_EQ^#l z1UCYL0{~&10Op>hFB5`tPYLrqFsp%yDCMW1EE6_Hb8WC_$f(O)BaMTcZa_%3&g2_{ z8mOBlD^D6vzl>Ukz*0bT8cq2;@COJC(JjW9@P#U-k%n=m48aJ!2cY9op^*Susdkz% zBNdUk)uD#QS9%nELc8b~dW2eO#Bu9l+`*&NSBzf`DNzZ1$2uo7W*P$gNDVN^Q~?Wb z35{g;a0@llQ}kN?FR0-HN>Srfo-uBw z4={>ikr*k8CC?bb;>*?n9*rFVLKm`4Tq~Gwj5qy6E#McdB#3MgQy&P7<-l|cz~VPJ zz*8|^g@iG@z=$~2hL-G%X<2h^7)HTLF8quUlLMqRLVc*eB!qfMv=J^7P4HmTBMcLn zKcg%_)dq!TjC+GQ#vIA65V*(HNL`UP2m`n`)FCDsg#I;{@GH%%EQ&U$b_SM4jK0!r z(+Jl-xh}#@v>@RiU|?=Z`wAw3L8S{UKXPiJc>pX%8aEmnvUxC1%E48RTWrUvBbDdq)M{j}{7bo|Bz>Sn|y zAmAPZI!J%0gZ_Abfd&PrKqWDmW?FQV%mywM6xI~m8IXerH8uir-;rJe!5lNbJ$JW0aC`%jJg#P>5^9B`&bW0AOtr>cA46ApD>e!LvtL z$>_o(&D8*_X!^KS6s4vlSp2K3mQa%37-78eJ3jdrJcvgC3;!&E;O1);(n27TL9L<~ zflZnf(jQ3>sI&5#o5fcm&os1*r+5zKBnt8~06@BWFuenJ z;{b&A5>|0z81N{#Gv0ha{b-*6asdwsQ2fT5uD~=1m!w+j|=^4*YQE9C*YM)9oc=X8+q50aI1`KbE5=zko z(NNd8Fa=i>Wv&D?g{~c7flix#N>lLX8OelMJ3T7j~j0^j7$VD&91~OZQ*h`ITn%~ zkx$Zy;ELW7@z4MfA{ukKF;YD;84_YJ5*w~4ZxTp6KKw!7=%sjMBUnkaTJ_>@>29YS zKDxB??AH+QKLECq0@G6MJmO$nV2XR0f9CNpmNZCIfF~OW&KEp7 zAf@1`h^?XxkJ%Sq7_?yOFT!lb2fi!k5bY2Vr`Bo(xpC%JP!o^7*$eYdgw$k4kWM7v zwP<1fmL8!*Xt<^w2A@PVH3(r7h;GnebBC6zM@#G!tT*Es#tUj5fDv#^)^Vr+zVR6Y zw+IPPLJs>flw2L%r-y|ZjmS&-#2s{tD&+dGt*B;bxCuhSbRK$nenS6b1q3SEA@f+A z=dj2(;b?@L+9%b(eG-Rb(_44#W_ju|ESn5A@u(pZdH!4Mc0Gvxw3pB30hiCvaK!sw`?uV-47gxG` zP2)<7LcS$Ip}tW8e4s1cNB3%_Yj^M~Qx*cV_9S2Ml<=%*_q7L64p2~2@=Z&T$ugkk zQP9wbxBq<=;>_0eIpIP;Qe`2H<4(Xkg?Uy#k;FvxHYxK`ry#Qz86J%bM^V1?ihy z;e3NflBN$}QoFuB^Z&Vk(@sjY=Fowc{ zhwLCxnbZ0reIIE+ihM%}8e_U>uI5j*Rv7>9|Uxg{&8VOMH*0vF(|?>JWJ(pP3D{wM_1OyXjf~f z;eV~I<;NM@2~ufuk%uSNAU12p3J2qA6gim@56apliHCfg(csYuES+ z5AhJPhvAC8;9cnt^apC!_*$$WCl}qPYpMuO_seUT=#UY-dQHc{baKn)Ydbc!wyesY zyLp}3n!KX>vW3#S7Zw(kg$vpjujsujp53zf-0Z5BRw}V2oxG&){Mq6~>lf!2E-KDn zQayXVn4iz4l`ll6cldlamv(n`6mqMN6+uL_`To;LKnGW?$mbwBHrLK;S z=}32erY|tPB^0@~ZCCI1@y?Je@9iDm-nq5o+E8foM5uSB*by0T?da;B4s^9lcUS*z ze0rKf$-wq}0=`iAr|ZENX&Y3QN`2B6LWv}8m9wmH*Q^&=pCzw*#F^>QXsqlP6ENAe z*lb9CY_A^imt)b<^yoRw%^eQJZ4Cwyx7E?n+;>HlD}*$*6s5K|MJ!AONB_g+vD?j-0czN z)%vyCUVl2??ezr$K5ut?sZhVJ|1SQMAFn^mS2TXef2zYA8_Ac796>#L)SVAZBX2&oOPWeZ?q8m;R*A*P~79J(zwmd(r; z3iFxlSS;G=gjcWA*&1!UOH>n4!n0e-<#UJXX$>l;cXVvX-lI(1cJjyScbTv&o0=y> z!AL!ALcr(m&)!o#q&uN+A}tMQvl2+|k3=ikRcMCmdRl1Pfm)6DuMJ5*4dVYFdRLz} z%&$kzuY$R)Vs`O2Ar6TN3_-*EG9C~bq`4HAKxRG0h!e^g;IPHzYIrgstA1O31`owr zMtg)+K(=GJK8EXKSYs#DS+VHTvKSR#(jBQk_jWi9X+7$w*QKCls+I1XP%m~-kBZy1 zpC=j{p|S?ukJUdZ59nx}GZ7;*v;tcrLG7Kcr69t5FPrWHBRI5kkj+{~sFa^9JN!YQ z2nTkC>5Te@X)>1^f-v8_^h5d_%H<}>_d(Hrh0Y908@71USFq<9#Wzjk+3WyBE=Ic@ zdDJ}~WJiaIK1BQj+3dK9qLk{tjvz>?&6;$&0s)s37W>#kjbFzPRs;1}B#!;P@^9*A zxF3w31g=rDoGg2!Nk)c3v@IKkEJpE!yNP)MnU3N|KEU9pZQ;p z1J9lR*zeV!tM|fBU1vzE7NK@5J^VTGOYsY*GAT~4euU=Cu5%%m$px4@U7j;U3geq^ zia);jrb|2}o~kZZ7YS!-IaVl!L<*0|{XmlBMzVyUlokY*^eDP>U!0j)T%4JC@!a~A zm>^0>D@HwX!cSai>ChoU@KgWeHAR& z3M>Rp+TL<~CvR_wpaD$i;{Uci58`{mL-HNkLp zYCH}3pm8vOehfziQ9BCWegtJG#A1&>My-DBr`hb{S?Uoyzi@`W3NQ}+kKrSiUMhU> zFGD>I@EAB)wuzuZ#E&2j?{CU_2r606gjNkf#nn`QP1T$L4?zMCE(F#qC|HysWr84S z?3eQQ|3i?}IwpKt$Le(6k4nk26h5iIW%43L-XP4nUiZo?OZSQUs%O6PigIT4OmUxj zggzVZ5#03md+ubl=s53}i4rFGmG$l7fP1b~>;=NaePJ5G(!=}D#)j6`7g z1RY)Lx_o{*F0Mx8cr{BInJQmZOBC; zgAuabLcTA^2%sv2*1@6_S7);0VB4`UH6GLi&wLl6*(0T$AhLl!1F+cFZ!p0h*VK>C z9%QW79a42m6Uw2bL#~)|L2T)}eaecy_pYt#2Zo7C3s4lS&qKm?)lCP*2QIGu{^D29 z6z^Xp*PHIxF})jk@- zS!Jia6pyXQL%o+)#Pe9lbMT6B}@zpCS$VVr@H;p6+|bpv996(Kw1hBct^vhvRIT4Cc!SZNkrOL(l`Ba7R|-cJbGe#qHjlv>ce_#^M+)I_0?o5c(FBPnMlR^--kvof5`o@f zv!#4Ug9UkFC=eJ-Lo5kxt`t34vZ3%`D$<`!Oj2qzyi>FJ!rGSizcM>BM{#?CR^@g0 z_rZ_R!ijw?h(~2nDZ7Ft+0-=Kj3-DHmWVdZm5}-usahkQ{y^42)(vKeq(pW+63LPT zm0s~KWM>qBVP5g-##0S z4~u}RyMrQcn4W+BlOO$PG8S9YafkA2qE>+=ft! zz=Gi&BD+HoYlTD!?@$4LrWSF_-uv#uTX{HM4)_t0%nqra@H$;Nzs2ucGqqxk*9UnM z1fvd{s~CxlWHbH2V9rGdWs?3OMCJ8 z;8pHczp9(V7rD0OtQ9DFrKC+1+K%1IK$0X5nRsp22pONyc~&K=6vbz(VXwD4Z?mJ< z4rG{}=^R8_Jmbpw{g6X+PCvD*EM%UYHnNsP!Tf$kF zJK+`4%9-u$Yns-5aBAw{ME;D{)^=~`%BkrKovw4&t^Y*l3Tt!A{d>b1M0QJCT9e6z z_SIK)bPSj+@m%i0HJd*%HML+=kfbq4fvJS<$H#uICc*D-1Aiy`S|dRk;F4t3J}bxp z-+<#5D=r&IjH zOE1Bf0-bo=qPJ$k;fdz*#zZ3flFe=}MEwZQ=EW)!;w3c~$T}Tvx5JqY1SnT1QYqyE zN57+7x%7VVM4~?u=?HFj=UlEpI+NKyTfIr_PIiOXA{JcKsV6%+Q}s^q1f3sr!;m}8 z=mvG$G5rvjHh_DaKn>(;nFeB-D;}o1vYT;Xkb);ktPtm-*jN^iDt)tO<&w!wot>MK z$sEW0rihl7C-`R9#^Y{^XuZH$v-QWGP_pUtSSJ<6&5fPaFW7tt-Ay#!cK!AB`(mno zf9&A(^)EMNGB?$WT~GL~(>(;aMJebJ*=}X-W(QgVAI-G88Rzd)z@=-r1JRp1r1jdpd>eTE3yeysrsC z!R!z|_<{(y?ocjK6HQHXZO!xX1W1CXC%-P3M4T^|+trb1TR|CL3%*h!v2nQfv_he? zJ)hsxlOG5mPhL)pm=J}^(G(7^DD|xlhI~Q4zb}1hX+9Q9d;Pw#w%h#0B04@Z$`S5oBu#&OU}V<#al z^Z#6khVY&1kW~ndRw9pl8f$~c@K$tZJ~KVF)Co-?_O<;#)%ZHQuLTcLA8 z6c6*%ZF4KHA3$CTD3c7scP%X3fh6u`%3=b&rqew6=$0*yO(WO{&S?mijF1)88=DZ2 zx!`IVOeDrr?u6Tg*qfl8gBkUc3{U5_mX_7&csm5xh;15aTG^i2+S_}{IQL`e3<*zD z3K9NX5>5%T+hQ{260$EJfTsuMJyAe}J}o#a(ku-MWYAZ z&^s+iHDR_ZelR9&&480a5+N#wVyJcr$&ZDQ+<XBFr$NxGxDu5{|U|AkLCiQ>xtUbN|0~@dulD$R8gD5T2c}EO=6Vvbyx8m_*+?A z?#NtIBb6rhvtgS)j)CR-MD-Ii}mPsOjlvE8}fb^A}n`%y^XiMGiLU!i3 z)wXfv?3b>Tb^`03-@g5U0a*U2e%I0m&H2gQm<}8vwjE3+4K1dK+m&%65-H+lii$UB zvpHdCV6nu!zRPD8EI^kMUE zL%c&fLSkF^H|8mD{)g}v7Jaa_fF_&_;lN;E{sl6Nv^3%L>_R?9pbF5KZRAAW26T11 zTsvA?@DFnx}raYHySpTAwEvPLY*u440ZJq$E5zOY06Rfhbo&u*J z)=L`sB@`vv+0bvMkZj49QO51w=`xC^|4l`6X`_;;LcY z0#->ysuC)Q$buLTFmfR~WrT+^7FHgb2?dB@xa6Yz_O245&!8ZYM_3l&%4{;aEkv*M z;BU?93+m;%7D$Rt*Bt~-HgY_069q~wrvj29TMq^82>Jt>h7IR3Mmz0rf)tXhWD9XU zTC`9tT025!5eg$(yDal|vaVWNbGg0!{d;q{R^o}C3HYF?Q+7d{<_kPS)1O~@n7{nc z+Zl_EWirSZ*y;Tt7n{cXn~kfWZyi3@(DfN!$9{c=5N8h3m zz5aR{MMYWL@#QaftObc$eD0yoKURJCp3mR<+&%X^ckAZ~FI3$XIDg-a^Y^8?TM+2Z2GzSnI~_o}GR$tR{0F!KwlX=50iTE_5Q)8Suw2*@)QcN0@oA zAE|7r)8qCrM8?;yzu}Ew^%X)s3ObF>q`lsF${)x95A@||tnEpnLL_|0uC6Pn_o+LW zFTreTnp7G*n!e>dKxkTXPk%+r7(|!|i3CZXcq5?65C)ABm_!oB1^Jkj1W*W*4j|YT zXrel@^6XqL_Zjh}xBU*4GL$(l&GPSxE!pGqg>m>h1hgc(6slyRrC7XpO?BG~-<|sI zale!OiJ~Al9TyOfy}*5IIreSH6aCroPNOEr@ zjI6!o0fF$o_Lj#0qK@!{ZcxTAWB&4mTmrJ>^0v8L&)Y8=YyQji%T*_Vd^?1)X1R7` z8LjiRWw;FCJ6&k?tDgfh3o$!7`shw8JRyBnR~museSN!g*%sJyIN;=pdEwt}Fv;>; zNgkPY^Y5UMHsd!&699nFNTk8G~k8jiOs z5OhSxr>rcnzIQ^{>4)qV2#=)--7TJRB7&~f8g_ViSwUE7@gPjninaWphU}*?~81r*xsM)Hw{LY6+%yY&|>vamO-!#v`d4l zlg42rL50M@XLCe}9@qj_VgxDE3nHHp!%N9p#5NezI3kWckByJ5!sCk7g`^;*TyEb0 zvI78a97_;a9Y2rHF?wU$*Xyw{?Hh0>!CyG9#8^Q{0(KNk04WHNoeBaCTfl~3(BLFs zE-JIMVd>F$NuN0yO6-1QvWS&MM2rR)G43c_8$>JAQF;Js0cj>i*(#7$3t0mqP6*Fz z5S~Hw2G459ScpABr%^YCoV_58NTr2i52gm~u(2UoNWz;90vAhd1jy(Prq6hYvRi`X zVk12$CO`vViAKg~q?F+C=6G?a8`2sJGHwO!$Z6n+KtQGHT8{k+>-#QfC)#uqSm6z; zH^d9D-=n2)YFNecXkOGrybVMF#xGBAx(yL}AcSd&j_xnyTf|yhDV~^XiS)(d!^z}u zJk}R!nM=e=w#Alwe*ah$JfRCQ^l#g!-sRjlHujNirH#eHY%;NXbnLc8(Yo|P`=)$h zqREL6-;hk01I{LdXWrB8{6oteit;$0R1`mJMdj2$R2!9 z_pt6o^b53w#fusT1*nf0(hVQYcJ6Laltr*&ord_MoG z;o*DMp7~cJ!;?Xew-l&8J9>UwsZXzOE)>ol8#}+We64KjYHc}xf`t1rqw0+oabNof$c9hwF^LOwVUXOdDlIUy+l-4RPai6B=1nCK>-uMkO}Z}z!k>y#tlG=+^j_nDQVo4 z7O`22k*9r=lX>D^q#Zo}Vwu|Rr!{6$8BuDF_+-WS$_Lexfk;P#eswp~6r_b{jm4&$ zi`%tFu%}A#3xF6I#4#*8CXIU6YziL-Y zy85+t1rBBpbOl*|s$O;iDZ)bXv6LTHz^kHNYhru1|8mx`!b;#5sc*`k-?C0-y!Vi!kLQRs-C}B0d4}g1Zx&P_Z~0PjtGUJps--Y$sgdoOJBZ$9{-? z`4hmEC!r1N2{7Xz_eQx23#!=)s`i;bdUIX`QR7EM*D5H)7Gr{yR*)IYHW1WX5Bb7`PaL| z@GjymNkl|rh5eCz3#^pZ7CKVgqTi{6JjI~^RB9kHoE{aMxk@w%<4kpkT|1bsh8h|1 z$j!v>BD<7@1bG(nlC@DhAaICaPWf@Lq2zL>tU2H~xOMBxTesa>F1I-x+oz@;*|zxp zu5#Z`mcUU{1Pta4NeT}r2w0|b*fQOg-z`at8|gqnEGY@U7-UR3^O1m<-)KzvowzB< zxph$!A~sM&2e7gv_|tL(qa)l$2yWEQ-X^k}cM66M4lNCgz!rW*1`3fonQe;C} z{!LDM*$4BRiufo6iH?QCQ-$htV)b|d5$LV9a#!`gUT|8>c$)&YPd)L(3s83k+}P!; z;zy^P9t@e)K=AZ^D_UBn->q0Jtwm@IvtpI%@$B4t`ZrB>ds{x&Y_X_K)i)s|ecfnx zbho!n1%tt*v-Mu@y7}H@qW1;GXo@vOk`}Y!&gzz@;5wYo#4{F)L0rDBt8?9AXV;?Q za?h;{I-D>Zuy%Ir=wMbl`n#QjFn*~pk+ z(CUP*Uv5OaUa~&s zbDqIMVMlrLg4Wh@JleFX%{bQGeR$Qv^*!CgM?af_u}djd355!d%FcYg$BqjqlRqZV z*6LPsKH4;0{KojZTU*NQYhtlfOE$NwuXA%I(_sY2&N zz6a7uKV&Pvglr|M>(_14U4V6f0jIdGlto_7*YYrTHp*TIp$nAy7BVI(w=7wbTcvaq zfkv5y#YFQOro|)$Qra9T72@b!#^F8_I4Br&KtJfia2zC~ zNG%=CjfF$8SSUP}Q?AaAheOdQU046@Yp;ddkYf=WXOk;aPec`SAVKL0{QPwIhHh>ERB=PAtbT8mbyS@e1-^#2oD9%$FSCN449aB z6mep!9oMwoL{3Xle7$Q+I@JQ&h}3c3FFHd=)2aWM>LELb^>)yeX5V}&9$(khHNIwt z63~9R0k|Em#D@KW;7oVtBx3BJI=ed_4^bLm4D!vg8gbCuolLGSHT8tTC11O&bm?sQ zqYsi7HYAeSY%;L{ zPByixqdz*~2LGg_PI#0n*>kd@KdRSNze3;Y_McAJyR>1qTS2}^4b(K`@p&2#a#=%K zQemc?#fnd|wwllqW-*MS#eU^T^)7#FAlMd)bce&8!HL1$=?o6=ICQ;MolxGB-Z?n< z-j(vVv#V!NmIS4c&F&k1=o8rqHlB%BJO1Ruf z_p0{|4ed%lRQ*MD*x#E-%;vvCIH4Rnru!V`@hiF+-CC?W8EG{nPd~(u;f;`rAXres zn2M4wlP*TIrfTa8x)>-dq;$npG{|)uGAZ4zq{ox@T`qn=GBA_gJDAIDXe;gLXy4rl zhNG3tonkQkf#fsm45yJ=(FSoEDdT?dKK#D^>aMQY@S`iL2aV7kw`5m6qO=epQonbd zw9yPUdpuFF`cT{J#NpA2- zOF`SU;gNGm7M?ejuzQpQcadpBA^^2q=M!J%~hN_p@Eco?#eQQZ!R7c!pe*O_J2zeiRpA-j?%!yiv1 z;@(my)D`+6QZ++JDEo5sE!m*xWyX|c#`3vtAOZizV2PJc0EWTRXeIj*h6Scu@|ulY zl}4eo^QN3rc~ANhF3;aPUBKFJxjc{o$&4C~F1&a&q>&40`4fkn0>`7xa0FHmO>SJm zCeh>n@X6)llEi4sbh-B0q4@1kOFVje)e_|-E}P(3@Z!wuy1H8;Ux_Yg>^(WZG**D% z!DXYcDX?;Z)LdR59@}#095Oh9;R>CKAgh7dF^m$Bsn0>eb};8&h|{eun5+OnlWq_w zQZmSpZa9g^!wL~ToXDWOhdc*Hi5xD7>Lh!8oVuV6fv>*|3ISV2oK(o8o)p%kM_{;Y z20tqjTupYH-)y0@kD!{!2J6?XbIVS9nQ4 z%z)*P^)>L!8dD*e4Yl!H(;zBXCc5oi(R5HU@1R7>YHAs|#A3`T-Q0vq$eOV7}l|d8K=1`rHE(jU!N%{&N00QBr zqQhb5-1g3-*HaAktZiy)^85WG%_P-@5P}vHWTqaQC+Tz|SF#7{1FSB@s=~^I8*19+ zjysTv7)gvBp{(5w!yH>G7|z=rcAMRn37Nf3sGeHz!swimhZBOr04d@Tn{bC{0YoMA zf}Vi^q;gP_QIb!*a1g=X;xdn4EyUFMKmrys zJY?~(5y@GdSwFN}3;))`InI^dO*C0+_|Kj0o*?zESw8U^mf>mnoU7W5_<8>vg* z#3Te^{%L}BC$3zsAg)BS)nbkN0`PqW1S(V$Waek_CBU30I_d%j118uOiot`WN`>&k z5sD47aYDmgC^A*`m=5!a?p7Zm{{yGNf#9-FzMy`#6cqy35*apugG z?d`pgvxc2ZhmX{yvJIDXkg5M58m<*7pq;s-)LQOsZ@+w|de6+|sE)6HL5gsPl&jVs z=c}r&F5{I~Vc5PApRb)`$~ec5>ZW0xs&NV?NlF^2m^M6(wlzF_Swl~MF^jZTT7pTs zraLC}D#?KAA$4EZfjG(507BFN@qtqY^}-c17m6tSegXlnw<}upV;@Ml*YW3*{CPfq ziZ5Wn=#|pySTuzP5y1*Cy;?oMy8_I5aZKv)di>QN;q}NzAAAs}zX`n1H_nv_9%uqQ zZW$80S9JfOJ0`-Sj9$Le@hkmKKZM3=?i_5@1Z>cd2porG`Q-@!7$kY%Mir|ZPJ|(O z84$ifXU zARFrGzG0z$B|bzZB_RhkASe%63xTpuiwW%jTjFSm+;t-ryRY4PB|XL;emaD z0uTwkNu_7uZo$$x?4q(ZCKG)eW{|_w zI(12@t-HIebjg&JzFSOT7yPTliWA0zN2dBY?gUs!Ck*|q6z`4<_N~=5g>+_HPtUeY zx=_CoSHOP|%i2uY{N!jtS!oBsWmyfMKVszB30g@Aemsq>$@zS>6-A2*LUid+B02aT z`mSzX+L9X!k+3cV!QCh7*XJ-tK-yftt@?O%qT#F9EFkdVCryn2UpP8@cyMrMrn(CjF`#}O8#3uGn|M|fC~dj{fe&6d>zg(&)WEa?eAQdy zO_3Fa!iq>!ynZEgckFy%cxW0P8)5f1UFBqQu1MkL5k*c^+vXyXXfB=JIUvY+q;zGl zJrwSWob}n6nGHgmgLI$1=0LPJ9B%c#ctXA0!qKX8`}B%W?#WE0k|VL|J?Q~>0D`|! zNlZq5HOKypF}y+CfG?VGl4%enJ_#D@xurk>F_tpb1XH9CD?>~fHXD#qO__t$NV|4X z8nPHkW&yn6J1j9p)(Rf`Wk)>xgaVq21gJI6tz3W*jHfIllgJ=Hmo#KYn$Y)c12?Kou6ff82oJ_=|C&NCR=JO`%_kLs_)VihmgT$9@vgI`duUBJuI>)Fs_ zGWPhPHB@1Z?*@s)p_mT1M(ja2q`J^&7oE-B9P1Z>B@xvTLqv*8jzW%83N)WM^>8># zlJKz&B+d#b1_1$HaG9`@J2k!J%*p%Yt73q$GYnVIX-riRjZp!JBXATJV7sQe&=T)B ziY?~xwl=}cCSY^sL!tf*wD?}DzoRKK5Fgr_NMz*1K{;wr3tvC`>_=IC_yzbxzFW6T zcdqUl-5prnKru>kCuv@zd0HcxlxVn&zbr&u##xQ_$(%gP$d3g)D$+8*geJvF!_)~r zvL>b^w0gjNfpuA~h?pIa3FQaBLYHBD7bNo_0jc_QsqI|@6t>W!U--b_z&dZFI~p6w zAT_bgr|!5k9#<0KiDF?#SFkG*>5b04x37PF@Hh5cFxVZ7_Q3Vn&PP3QxO%mcj6^2$ z`N>EmsZ>9xvbM7=pdSunq^otjs`QpmC&70>|zIf(EFHTL*RUPBEjgGEg zKRSBb`25X%eN$8YeK$jP0+|Zg=KKh-jqCR7UeNtP_{0$K3*H5oRwRjH90ELeYnyM) z*BYs?8-*&zbw$&t)kMu&C<)YpZAaoG*I3b*@FH;|#!Mp?B9b6+`#^EtX(TF#fOM3j zQ0`fh=$eQfhck?1Y*dp3m1?ASQL^Fz6B4BbOp;pnP&M=p$-SV`k8D_#ewfr#-4LJ* z!hWs?wX@X_mYAky7}TJ}*Z@`pjF730i{oSqhy$(MB^4cS_ZDOoVVM|+x!VGDft?r! zkO%T6n!l#Aqj!F4@`=+*rf1CxUyk!`L@LJP9|fOrme8F#rM zOKb_Y+DLBTFy>aK(jyU1jO2kc>#zc$8*gs<-;^=GL{dz6_IE=t+y9FGin3Jw);tjsOFp1bRwsT0zat zkqB%X9l3i$ePH;?MIs1F!9jlLB448q9VI%Z12Gp z=}L7%P5X)>ZBcxXvwTA7eY~ssTJ;km503k_$3W~6?xmCzzClB6WRODijOf|mZ%?D~yrD8qFRIol;iD0CF?O^7l zSX*crOHwgns-!{c-~F}`SI+bR4l&O^*jtj zk>}w$M3=T1$U@!-iDAeI+aq#jG|ioV&SAJhKUnE=57E#S)b2mvo76Ua35a=mI4td=_I2@hnBZd?hngS z6wqZ^fJnGRKWzuW10;Z9YF-nF$gdR6Ln4UEKD2PlBl0fio?P?jE1fnPa7<%@^0bH4 z&}JL~ZbAK_D#d5b=}1FK_Qyp5VYOpyl6LbM=3q}WHWxMTuo@qUC;CD^J>#d2B(TFyzoV8=f{-dwa=U$(n1D|4AWA^6 z?La&3q{DQEDb$}xjK&RUr!B(mP+XKq_0^x+lb(7#!30nd)qUji z;|{cEKGPhCgJU+q!_?*FVv#nFH)4oGSkuoiL!y_o-&}9Ste9nQ=y?sIGtHsCI69*O zqRytJ`(i`jlmgUah8W`cTE7%axEV%Datu1WzL5S0uhY+R0L*8Yg5b-CV#bKa+ZL8X z=yVl>au{AegJVA23jP1@bcbP0!emEFSl);=+6>7_Tb@kX^c#14o_pz5t<8NSRg)z( zHb6>3SlCR6vy4h=Z!;LVg44d&-V$vZ&vdK@8~D}pi<UAc+8Fq#i2Pe$bs*KI;_!FB}t17TZLwC zit!5}5BD*m9< zyOO}eYW`3CsZ7A22P*k{`FicW$#FF<$000<{ zomvlK|NN)wuZUHlm#XZ)4{_GP@;Gde%0e-Xy`}#RN`h8{_KiAZzX6N}{UL%>eKAS4 zLVLlhKLB36s9V%srh7p5GUk!laSp|rsO{cJN=)o6@3G`D%qBJXHAs%W{1X$`nlmxW z!{N!xW^9uDmQ^VPV)SqcDIMbTW7)@cwf&~$+O@37pt7if_+Yd|Az&pP57le09UPD* zK8O^?RG_byT#pG6mAxby(I1ArGX%%9rSAt@rwSW<`Tl^N+X-Eu+ibG^0V;Sq#t6p8 z7qbMCl#|9A$tc=zk=2Pgq)pIbw$Uc9ejN&WsH$PZWch!vyL+tN*ha12BVQFG88ho4 z+5nosxzWBvK?#lH5Nnt$e*>1Au>ySiC&-Bfl41Y{-p*n|u33mOZQsKN4N-w=xD!p_ zcRO%oEo2VH17sq!5*7f2oj6EB6b&~=0d2eGyDX93q`J)(huGn;SiR(Ei7W*`XUGH0 z7F+cv;BNKG%~m%C-~d1%F$9vzdUJG-7y<>8{Xyh~usG_QS3Fr2nu4Py83~W6ba`nZlj!ftgYbp#DCpOV%5j zfO5hU3I~EAxLw%H0rKmC6`#XySe=TLyThP?;jqsi3cFo)TP`~?ytue^>-u%0qq+PS zM4KDTSGrV6XR;u=!I0PI_uKsrrxy_#A^tHU@USnR%NP7^e;^cWiliW%>g*I>7Y1Jv z;q71t9)Uj_JTh#abgFfA9+P0v?}oU^fq4P7p@7|(dniC$R*HB537*kmu-P36#N3KO z*YKX6uC8n@l?n#DF#I>_ZDdShce*2y&h0^WFa+VB2@3TU0!0Z>T=0Y#|mWX{$iY=8?aD~P0vi_)bMzTJ4Ql%K*Jwzoer)?>T=9?9 z-#>>aeb7O%BOm%pRv%`VR_(Eo2GBr$@p!55Stb3UZSR@vc0+na>$z}>B-0&(%X;pL zyY9k}&)RRf0yO8!y{n=i-BWkfbKoq1}fc%tT@e@T5JcLW^ zh>V1B019cP&;v{>;G|qOcz_E!a2!Fw(NG5pR-YD?o}TJA5*#ytI!?WWPyWBW%pUXf zAa~PZc%}M>^{mX$n<*v%-ga|T?7xI^PSOjuYTyMtmUEhcAvoirQy5?=7NV^j8VQOI zK*M9^m;lzD+NhC94XR^X8Mtfqm)TYGehfSc6ZRw$IG=cR2h|@df2DgC^ejkwlZiIF za%~IZ-O7YL4aS#pW0PgL9QH}%5;tYFEpNF96|D4*i@u(I?3GEu8>aSOeIKPgk4o}m z^rhBj2tpGR>JIn=V619)>sw@h-?&mnaehMmwtcHTP&8WHmXxwGf|;S0&FZ_>?snrq z5b!&l*c5<+Myz1UY>t=3Glb19h5#ydhrwcrMw^;)*#wrBIdBs=5X_g}O4|y~IW{Og zNooRo!M;JumZ7j!G;J($q*qlD2Gvh8}?~cIheZG z9qk54wuCa(j_H9u8)^{n;n+B>*c#y?L5Kl*VexnaVEs%<=I|`l+6`%?rs2=hU*tdjq52&vxKZdl;hB)j2#1PA3KZJdY4{<0x zJrxgP!uIr2P|6D8!e&Esf&V=rzJJ`er}n&g-_X$Yd-1ydt+(#K|9*UV^UeG2!%gMK z)v}nozIyeiKmAtq1zhdfbA5GYp&;hoqC2%K;Oq3U&#N!0U!+_Iz8WVM>g#8W>u26c zvTqvm2LBbwK?Q(F;NSufVoMB`*u4BX{YiH;n$H#TUHj*@mdoSg=bbw=l!XTiaT`8_ z$$>ju%I}+)*jLDn4Hw5o%jI~v8&L_tDkZYnI279+wfNri&fBWPU<_AX$E^{U?XR>XjnHB5So&RoxhE#N? z9Q&gB5Y9VukUQ04=J4J|egaNS+@mQJYNrcq){R?UT^9>xLh>h41X--@`KU+=6Z+K< zV)SMe1RZZ`wqG3x z*u;1|hONuhzOTPO97&}I#nr)3^HsCitlRwo1=gE4^(Ep~qa~RbxWNY&)}VaAg~^+_ zrX?7>7t2Mbi2cXjP_9v*hU5X!^dW6X69B~06Iv2dR+L@eNsDmxm)XqpR3?+nPE6!- zaIQH0=x2u0sZ@G+h(4i=Qgo*udqaI#eHeL;QmD%W4$GA7=f6r%Kb9FdKmH4lfm~Mk zt^cDhc73|*K*y&#?|3S3|7SZt)pc&ir#tU@rur}E_|Fm51CK7@_2_}ej^g#$fdhb7 z=$1z&`O#T0ykHFJA0eO-X)b0q1n z151abK!0tBYeSw#FtZ-WEtdOq@?u}JSbZ>Cg5eUP4E>~BK6OiXcUO7)*4`d*pnA^T z;_kb%C;=NJ7*lbH?o*aaD5qk{-34x%lyXATU3F-Jz8$;qZ1vhv&b2#bWtO)mN(LMx*W7rY7e- z-OqTuO%b2JFXQtnWAGm|8G(nx^ABt)b#;#H9UL4O+&4Wq)P2$HwoRKhjeJ&^3gHO$ zxDV%t#bXJI2%Kpz`=ikq*5b^uUn&=?SAnkpxtT|hAR(Qecz6Zy z{>j@fzGTDJOE0_r@THe*U%cp|+whpU3UxfL)j>-+OP!+CO9VflJf6$WJUo_3r_z%T z&t_4_Ef-(BZp)>YzW2SCUAkk#rI&t?@H&6&H|j4ChXRUdq2_%Cp9%RIe7I24%7^k| zQ?~l;m;UF5OR7g-s(!m9*5pN6y|?=0-s*q&tN(qSa900#t+0eeaIZiB3tEe>>!y`I zs@;r#h*hYq}mVFJ9Hz(b;vymF2FE&V%T?Rkxr#uQroTERT53{ot@*_3BZc|9iz7-MIg)rx(O^ zv%|s2ii58#<<0*Be!9&kiSCs7Y_gJx&*@Agb?1&=Pz8{6AajGu-WMtEah{# zY_WL2@Ceh8KVWRCfT0^>P4g94CUz$f6u7AObZa{@!Nzn^`B_WL;`8g*-8S6aJq**T z*2x4s>N45>_V#;&!NTnPhN01s#S11UlpCkZ<)9k6=ggsD2sIxKL;341LYQbV-?b2T zIy+-%Kf$YDa36K%VJY7USBZhSz<$`hC&){JQF|!ND0k zf%TWaddn@hi_ceQ@4Rz!xTRSsq_3d)M_K^%@z1C)k1xD~X!N|1Vq(mT@Se|_sB21W zyr(-JTfOR@L+eZZND!Ea#}lKSeREOq(4Q?ux5ufp#p8XI+4oFN6(9D2D+{%^hC`m5 zIQ-~d@SErZ#uXtyyTNm{gWAz*q7e&7iI5Z&E$=D@rrJ#32p9j*R@$=pVB1Ld&}ro^ zgR%XzeSMYb6;~c!wMw{>e*a7+5b*mm;_!!e?`m&1-)AYc?b|dqe*WdNvzs>E`!E6% zZV!dhUC9LZ9XO`^Ni9NCtIPY4SSA3n+Wc1aD~hXn>>n1CzDF-!x{qkt?*Xp~@TPQ~ zkiGFZG1k$lZCEkOL`_m=UfDz?d_=?jA@q__L{u-DoJb_5)~D06v)5d^VkVKC8;Ukf zPF}nfPUPDVpR>*H+Y<45hCs1Ao`~Ar(XnNyt(5NQXs_&pnNaV(iT1X9vAwNtQ)kE0 z>q(z)A`%WE>SDM_d=Q4fr8I22gD~(Ve0dA72h~o%PPW?;+6wO*{gPwky}P#F^Agnh zzM=+CJ@p@^v!&A6gF^-*^d7Nj%kI9)%*^HQU%jgO>r?Sj?wV@VQH#JRFLZL{cxbM2{4?S_)#tr%0`V}6}%h61BdNP~I<;IBjm>n2zYimhI zo3c^2d-lmw$0rV~9UAOu14oqp33!rpYIqnQsS$!HbL=hnB7GG5?>0G3nw&H%nVTAt zLZT=9HBN$Q5$^(Yhj?(}gX%?t0fRvkaR&aXWzU{xFWj=Fqoe137-C|K zzKDL6&@UTKj=YXa0wEUAB@;GqzQ*!aOQ)al+^?6O+_U>1nw!@TmUgtZieEf`+G)yb zJ9d1+bUMO}?C0DK784kY6dp`_ma&re%Q~-%Nq|!s z%vnYb-S;;;Pd(#|=VnGnJH{4wZ|Um9ptR)=E0OA@ZTt2;`_yUskl5q?!oa}0_V-oH z7k(JyP2*A4;HQjQI5YDRT?_AyOcot&|at zIZzG+zB(~^c)XE|JfNsoOjMr( z(E9VH#XQDW(Y+UQKCf=X^Kn#HBb7DMnQow20d;=IDWCr2si&TD%BP>$v7>6aa~>*^ z+^W09YcK5CvuE!M&+pl@d-rF))zfp}yx!h#qwZt6ovKG2z&I}BK8+<|zoE921FC0k z?tT4VUAk#@ruw&8*mAD#Q5LV9p8A9G+Q{g;cZVZwtN$8x-31xIy8)*k=N*DGl z=qHTNKo`IbVrG=RFFmcaJas@BeE6ZIugZRW1Nbo_@m28hp&_T0q#YtXJ?hB%^&h@_ z{kpHlr`s zw?F&s4^B%$0%$XkjC{cn&j<^;p|oj14tl1!2%onXgAZ{pYg= z$oTYhfSvgHvtL+*Ow!|aO*}siv7x~fU;l*zV$44>+?;|J^zYpMT6aXz(s2gc|Ew999YB6c^m2ihf7z2Nl^-twd0|Mb=OUh|XxJn;JK;!<(O zPkvIpnaA}>JhKhYU|a=&0RpK#ivH^Fyz%ny{#6+iuIhC4Cn6-iLiPU;WvO38o577U zCyB#Ic*G87y@mRm&bcz zAkZVfsRu~WL+WBFm6DvivU6u&Utj;;R5;@EHQAFN`m)IsE95&@wt&0zm2gyFrQEWx zaN|vjYhf~CkM*n8CzA^hw|zJkAC1n!x+c5gM#gDj!`bCG3gM*|?XCN^4#Gkw4X4M`w=B+|Jv~2v^DS!^T5m^4;(UJkhVuP5<|(>gqF*lbD?5VBeeF|IA3FM{mFw?I#K&CosZ=gEJ(bPy*aC(}R43*&N~f}ho(1Cq z?i7-F571<*=%%R$3`kZ9+_JcKZq@q_&CXUI5$W_mZ>iMNQ7*5rWkVtTUx$MEN9N~0 zaNEL~sp*4TyP{F6C44ii6vaz{U`KN#EXT7S=b;~|ry^D#Bz(a5Yz1AEr&&!#2CZU1 z`TXTyo?qM1aYiwl>1fGi;hlQ9_+9l>@#vl|%lYLpf;?Yibvlx+JL&B7bzMkpQWtQB zUI8l?9jr=fXC@vt#5Qd>LTQ7&e2@ZKg?efNAsTJC5z~Q6+s(3>fZDX+nwYpIS{%}q_Em8DWsQ`bT=7#tl%roW*Pb$VAU zUOc6z8$xX7|4ZF_z)5md_rG0LopVmp-P2)adUDvw8)kEqHc3Jf$^iu=z!D-z76_3| z5K#gl6ND`TB1Z{i{#cd(+gKRO2AgEd82#;sFn`^W+4>L>dOiIOQ$NpLXxc5 zdib1Ne(cMccmk#faB>-E#lk|I&HjPp5y|33vYN&}q=Ld+6I$+>ZrDduCtt z@WTh4@Ufp#_b$(E&KK;j>Z(i0jFuNI%#=CQK`kgj+0gx?Bl`;yoJ{0$Uqn|onae%= z)?zdd>haAp$>P}P+%u-9AHL0+Tdr1z+Gliru16yy7yjH{*gcfX9%yAVv$M~XamSFs z^T&C9iME~cq58CECX)jj@_BhoX?%R+MRRl1KfhG1wudi~THD#|!ouZJqtKG?c;4zH z?Z1J4ZSjbccrqr&leBA2^1x}Ydg;N_Pdo7Hv$kz(H9K#~WF{vu(PSnkpd+7yavVPL z>en4Sz17;eJ_#QsLTUO%wEUN`;yAtWq@F`L2SC0EXQ z6Zbys^G#oQ7*MBdcIs$0J7zhD4*iz&dH1oeKatvT^zD_t9fi{x^*0}oCt4PB#{LHH zOgOv^I4iDA8tE_--F`>85J?rswpFVSzquAJj*s((9s6^BoN7BiuouposNhU#Srzut zZ$WRT`QpAMSK3e=PVW|AHWyX=3Znvl_pOzc=WRvRIkB)Xwl$T0+M)jb&0DTp!r5o8 zJw8^MP9=|QuA;@W&Od*0irBDsWyyq&D^s&RGO}`be4N<1TQGta;^ZPL6^iwKtUa`; z55Dt$Cr6BtXHd+0W^3PqZw?DjWkq)KrxR8)(3-w{Y<$<*v$wwPb@Qv)H{i`SJ^hR) z4zIlM`nP^}QR?!1-}7=OY(K1Xd!0*IXa00ovtL+@GD4d!WKJg&1K33#e(P;}FitU{ zt?N5~x>&8WhcEj1>R04lTb>75FEiAMY&Hl3$h*|xn4J^Wt!3T$%||Z5;L1NIbj6?k z^+UJW3!P6o5~7W5xLmW2bnBU*p8Kf>2651tB5Vja1lTj^zEBwbi(OpUO#8HLiLg@) zE&thF4^HMC$1cx(bGjf=D}m64&;RzLw^{L{J6=EVulB;Xt~)UK(#QYtIV(fUORaCo zlPu4dJRfnk+20o)&@$|w5U-YX^N0WD>~kOc#ApBZ;)@@%{>;j~``w)%b{_k%^0|ND zAt4&F3N%3(QzjLiGq1wcp)D-_!Jl0c5c~M$fArYc)~#2xN6O7IacS|Cb7trB<$-3| z+Ozt;8>Xg%cY#lh8HsmW@_fYp5p6O2BH*Sli|ngE>Wn|}H`eF>?uQ@w4Qs<4H+H`H zHp;GG1AG7*pv+C>;h^A5ZJ7|tSMf(QE6QOpS~*$u#wG~AxPr~CR9srTl$=>)HLYyj zTB$_BAianku-I%Y*}S*mm+Wb_l8M)my@~X`uO=-R`9Evbh1Zf}LM(xxd^h*LXgFVZ zn~dY1dCKTPu2Kpk+p}48!WXCp!ygcoRS^c%T#m_Hb{u)!##t+3J7ZhUf1rf9_L>LUHi<8=lwr&28ItzTJ5IubdY+ zuMv6M$}|`*qc8!%P;m@rF-ZRG#g_fVE$@2QyZ-4t?|IM7{~Onc9oF8?>}yVc^=n`I z>eF9ieH9-{Yw`G3?f0U4eAM%p6|l~rHe)zmgQUa1Ex&=tu|;)s6JN0tgNED2>a4zO zuHTq?q=}!L_@S+PpBb+#<83Amvc}nK?Z$;UeCJ$YpkL^yK@Sa}@s#dBOhlEpD44D^J!d!QTvf{w6U^)cc* z&Itqs31%1O1_EFq8l-s1@P#X+`0m4BtBfy4H60AM-%jl^BDsoy~d}MlH$%4 z%n6QA*cU5CBui^T+<2^XA~8|5`$@=}8c1b_G6274Dgbdv^(ILO*i3lI8-G358;|jn zr!nQJPDf3KN8)WIgc=|uMjA(Kf&EGb#le^d;SxiSe2yT!W+MPqgmRIciVFnLS(x;W zCOkYQiIl7}G~H_)$Tjf1$a4$e6Ripn(Aey2^9I3^qB9fZ!Nw zO|8?*hHIx#)Yy*w=FJ9Yt4p-cujcJ9&R zV;49tIQClWlM*osnwNx5cZ z=*1ze5TUuUv{Pi^z>Ls4{`<+6buHtsl)?~?t(F8QDn5l0M2S^2l+r|pb@6-{cPNqs zQBUy>fD~mArz|vB>M+#M!SANgb#bHu`X-MGov{e^9w3Y#zg3+^LOU|zzZ%elzZ7t+ zuq*_x{RXBnQ6+DX@J>1T3w=AMcvDmIlB~a$IE1hd-04I{){(3m5+E@ISpkFQ5}!2w znBtOo7tV6dZ%a&wmpIVhKgEmCmr^+PTIU75<0kWV=+~LIgWw|te$~AC{+;~%s=4s! z6a1Xqh8upJHW#yq2R^TQ2 zl5trM=>`&fvX2w`m9!ubVR^mYfaE0q3wZ7_NO7DI;usHV`Cl~_)}y2Xi6k9MOKZiJ zlZ-@bNm*8er#g^T5<8{DJ=*e>N^wAoQL8n=b=H}r@oH2-ZyyJky#ZBqD<5gK zE?W4pmcRP5yLKO@rJuc8OY1pp-@cVC@4IOI+&OnGFYnkP4P8H{`0$EWzkY77v`*W( zeok69IoTe*c8i{~YWjRR=7%Lsbrwv8cHSo4WO8V z#JY<|S|C9fdtV}K2ZQBU0B7!jG^{X>4iQ?Bu<}#I;)e3z-bQn4RpRQ1H|WcxM{{7H zfI^wh!zX(sULIVE9C^}9_5H&mXHQI=JueB(WuWqT6c^U+yO2V>2 z5dtIP11b37K&y0MWcW~JBoz*|W0gJ4=5ox&`FpTGKVK}g{CUE*+ExTK(p1z70$DC- zCtxXKfmSwGO7;=2kn#CYWP}woC>{tAere^?IfAiVBt5oie9B;MA`)7Mk*twJ5`ghZ zu_LFPG>W*<9AifIA~;)^+18F)|8l85O>Q!Nrt2Owq~YU#>ipFCj3HpwcP_< zNd@whn1hT?&vtG$gB+{VcHV%X6-@d0md`C|3O3BN)sl-X6Qs*BlNmhgqY5K}x}|{F zl2zHVa^3PX4}1+)B;f1*A@&?FoR3?HuQGlF(f&YX8%2-$0c6LYjC?ixP21bWt?#<+v+sD&Ali4%b`Pw-{F#54RB*GM37zXDxjtF2K3JG!=Ighq z{K(jWT5T+rnW@%x4Xwt0HcPI%VZZ%v=I_rsKYDQe-l@E~^9Q~64DG5_XEL#|TJ6A? zb*%T>h}(|!3MLDk3Aar`$1}v%Jw|LDdb><7BV-s>!`Ni7!hG_|%Rjzj$LXi<*zxhp zul(fB9j9AgxI+HOr7ND%z4Q#F$uZ(S{~h1!qT&Ibf?p5<*-EJ}VluZF)}oCTqswNy z44Cb?>b_mO4jtOH>%Obx=))Kcjq+(j|>eCT(eX2bo(y6XUmqodnHS$^GPkX zeuEwzJ7AU0-MHztLx*nLwDH^`VX{(I@1oi4B>~#~&)RO`VxOwrz&8mYcN=K^V*M#s z-?wY$;ln$3-FNkc_pC@$R@R$x|4wQ9&ihR-j_r3|rI)p!JUey#tIm&|k1=L5zvS7j za-?^g)AcrQTUUN&*Um$ScJBJjl~;Xcr;OOn&s_D7je7sZThF<3d1=RvrR6)%*?Mt* zz2SW9@J_B#N*dce>LgBk&GN=`%D#P@Hox=SbKkjn(>`DMoQ=z`k!_atWsm;_`Kv$p zE86#zz$v!p%1`awdGO%Qou9gL{pjiM+_-Vqu8kYt`SkT8=YuB|(&ERiI;oi6K)a%h z+mC+bxVgBKW~>agF=J>QJLR}xCb)Rz+&h<+w{Krwy7SzXi#hS%8@DiX>R)wEjsKGO z>Y}$IF{b!$R-LsfGwbB3-&t9owWpruov2RfV=vNz^6rwf;b*^kJ(|ZFBB0A~RtdI? zr~>R13)c2t{TZ_gKXdhZ^LXIOo;@q9CbJTEUPA=W(7@m|JIzWxbwiIGSno>j$}GWM zB7?ll`GMUbU$sZFqqhsZJ6(}%Av1-5!4WXqMF1qNMdt^%4WE72LSnXF-#;|_%1xV& z-YyscPakZYh6{7N`086?FSa+&y!ra{dF;ig#mKL6AoKP|!+>J;0%RbW%{(JCN)Wk=L40Ns*zMi@7H z^)Doz(C>h-6UZl@Er1OmnG?B}4xuICV-c|z^ly)$WkOCAI1dNO+ABf4i0^*;Hmc2* zoV;XOM57h-rA0-WNu?(HEUTE!F7~5Q4&(Mam<1a-0m!A-)=r4l${Qen&~F1RI2Fl} zz+oga0PldNh~S*IUbL(5m@9N46d98I^R>9fMq3eL4q{f#$zm-{ zh?Wkh3lcu3N#!T$Gi450p2+bJI)CFl?0GJ{fyTGV`#_2@*rfVa%zJiWv2{*}V1o2y zKq0$l+^@WGUAPiQZuIkrLOOXQ3cf2n7Jv-lW!K}91UEf?!^Y)jw$7ZI+LaKdn-I$a znaQkhBC#-0$z+DYn!Jy|P1{PQh|<1vB$nfgFETkc`m&3f)#{;fM&27u zaNsx3&ty@_rE2)@P6p51vgNi-_8VddTmY5!fy0nGa^%QAzbD&_#^Q-6%E9p$9X$By zD~6kmbEh)E-4cT_Q|?=?k%8^tc${$3T&8{T_{4!Uc=jBB%!aV*_sWkV9;%zqXLi)g-bO8I17UWGtB^ z!r5P8R)g$XaReMMKnU+pu{e@QWMw%7tpNf32{=s_iZ_{zOQ<*66pSO@3TD{ati>^b zt$UuylbrIlIEf+A_AWnvFR5X+uFKRZ2 zYR!;fMuZyW?Mq8{JzXp+1eM>LsgfN4)m0!scB<2dhL$Hc5tr+DTYFmrdt0J-&p-}k z4hw(q2=Pbvle_M_%wj`H%@}s%b94PD}8TW6mXGb!mYSGYmTX2Q2U%!6v;PCJnBiUghk20oi z$#f5fx@1oZL^2?P`=cqJU+5$+Ty*NtVC)~L{~^ceXkxTnUTGS9axQtXs6cAw_GiGe z63Xj1)$NVu&PJpl=STO|YEx-USs;B=Bh(MYr2U=x7IK2cg-`)>ib7ui2{0N^ooM|H zYJ}hz_(4DrQw!Py=yi2Amm`@rmKv}t!4pXUVP|6$E6tV5v&C?x7z`-6_5yXR_6vw5 z&`G4-2xh|I&8G&_O(Ny?wi49@gQppL#B4w|(f%FH#`aosF`sY5WPl+*K^q;qKkV;2 zIy3|vjumAGVP4a!fG;)MUq3L?zk`m}1-^lf$ov#crH6?z{sZ`&{a|Fh)sooRwbifF zjJW{w;~HuWjRZ+)od_$jmAQl-$sBh{>x@A<3#_GYGWGfM23ka>9Yw{UY*Yl@yA4ve z%)u^6UOd6r`fE&ecLFml;T!n?Y7JvsAf*Jlx8CXdUpW>qZ!BH_4+Go;*;5%cXB{V- zfWI1PWYWWF$)M!7WtXVL(gi43q;lER)b44N0=^1GGQ+7D`b3`4MZj@=S6je_c2Wv7QK1 zAbbFtOU=6?D+i%f&8ILL@(~M8zS%56 zCb^k(iZLb@AbGIXAhqyg+ZSw}oVa$%3_^g`N!I|KjMN!~G|dm#hujh@kFd1glnCfB zr6(sS@w+mfK1;^4kMYDlE`v3-y9u$chGjU@op0)BN7Fh+I@)^GJjdMs8?p$L3Q5CR zp!^mKsT_+zn~&eYQceFvB`H9&jG^}98Ty;9ahAS|sgPkozm^QK%)4WXo=tD&eoivh zGU(XH1lUYo274TTfE;1XIi5-O-@kyr_jx{~cpEbtOkD9oHsiJ z%Y!Lx&IEMZHS5Ve?$Ml>^F~wPicz{}m@@{vXK`TzJ3=ln+eY%zW4@Xd==v?_5;08l zysyw&9Q5^BgFfelmNh|)eyj=;6bj`-;V7H)BuJj&NVb64I~1yuv`6Ly$$R?4Arg=k zfcpedeX>*>1pW*Ef}u>dqWBu*)?x!6E*2(8jS6QR*gDkSjbj2+J3KAV2hjuFr^#^l&H*ki;EX~y`$y6iC8rnONyh!aJg>|G*Xxe+m1z|b+Q)| zTo4Ij8%qlS8*9vo#~yhkP7zx$+QPVPl!IXsjt=B<^=Kr9;hh9Y(T7*IzU?OK6`dEq z`hD%?ha z=Egh5Mr-}U!>_#--Fh<5#u?5Bgm+4Y@A%5rZJYgxN}(|3e6E~M55>;=_vbw4w{!Tc z#m@co{(Tob>&W5Ho}OB2wT_IyM+GgCU?Bsf&f2l#jiZ-;aAjpLb2fGSU&w2eV$RNj zlYOV>Pl&7i2`E^@)_&_M>$j};SzosPk-0u(??Pq;8(L*~)1|IqPA$+7}jCsGeOY34bAm3S#G<`FbY#`hJ}?W z4i)a089a}9S+{IzORfbJ&o&)t+3vKl?ak_;fs2-Ket8@Qb#}GPRdmP zztuW7Kdo`LA}d9=bwhY7()^QK_o(biXO4|dx7zPMdd4jRSExU@2HkMrv^TffdkLO7mCxu68ikN96kE(c57;E>`dG>tj^bN zdU>rD9v>OK@iZa(;$RZ+O+RCJcsAHqzW$l(=hh$o%;%QNq1ln)%V*ZloqprU=yH10$q=!;*r9OSQQtAJv=y;(p5{d`X>6H;cRGdNQ*63KFI7|v4PK|QP zJGlBNAjrXpB({kQ9CDcbLKFl7!DZoU#m*-fP_hfjt|HbPq@HMC7(*sIRZPe%<0Fh< z_X}e&3`=Tub3d|+cx&<1?~HIs?u+4$1z;D4{KRC__uBXC(>1|Pe7zm`Ms?evk7cN( z8NxH@cWRVofG^Z-hR@bEK|p=>h>`jA**%ADGvF1X?*1$d0h3P~a-%gqH~*2Bzww=e zgE4x>)+;k0+HAb(<9wz1B!H+OFhx_16Jo>fW<{l?)xb@|8Bp`qr;z)0u^4 z{^)Rfll^C-`}@lM{pG&>qms0yR+F?iom&d2QfX)3$XSB}ONsr<8{e}3NhiKeZnlDX z+g5};Au+~S)gKJ(Wjd_9c&|T6ZUqr(goW{%B!e=_c3#*PaiNeQH(y{=B09=tUN#5Z z~D&7F<|I-4;Yuy!L?vn_}k7P;(8h$*td<4;W3AA=r9qy_|WNY0x5^A|1nP z&VD2TxShCHGVg;}seumjrpF}7OG45pFgs*5LDukQj0NLKa=!=^pDbI;Fu+F6Y(pBD z^~Og~1X^J9urrU8=J)0EnQco;cRcNBcPuS!%N0w9Hg^74*<|5vS@H#p?v% zEu|S{6n{_@nYfB#0#62WKuc$$GQqvpO)~#^(4FiMu;7R?2t5uAI`aFlxHM6xwuBWC z6nw@Yatw)FV-cFd03f>aF?INT*)bC4BI_X8<8>lqfzM#biRF`9Mn*21Kk%}d>7}LV znU@_{ylQ-GC4M=5R-arnu@M}oKxtFLC?Jc1v&evx97h|jy%hP096>Y`ZEH%|Tga2F zVbYAEAXaZ69X45^I6Er&EKj6UIUwm$fRv-+=n`zYg>;8o1ge&_W;e3I>Pe)p~~ebNctsc3Q%Ur!-=F&}?Y!j-k*pn-Vd(Lfe_J}chrh)3E?u@I0V#gGHz z>thB;hErW(-L~*`X^poOdbLhA7O79Z6V2zBCoo36B;};xcq-M~W^0j-8;gqRS@$>Z zah-568itk_w!7ywxMg}3lJ3}RrO20}5r6Ux-7`ju8>m=(+|F4>k*+G6#V=(@D*^w2 z8+XXTcNVcY5AsGeEI6V*z<;MJ=deEoMPfp&C$hK_3{gZtH2?|~=BGT7$@B%2*eVdY zbQN1xS-1}2W*kw%#Zhy}6=4JQhdq)TSw4JB{%ew#;|arh@;Qakbfy}97}lYbOpO#o zddIgHTZyfr5(E_AVeBgCJYr&~3E^Wd8MQ6zH$_(x_s4Pplo%@Ad&Sc_9W)g~WRMjF zY*b5Ae~S7=d}QNhoAx`M&rniyDbN=(Nhg)$u$mzdnTU>uwiC~MoRxg`r=+)#<&f^< zO2UM+K-tg@rbEV&U$mE~IoZ%rK_+n3jTkq1QByj0dahpIH<-Y|Fq;@Zb8uiw-VCkS zWFkJ(2%(`uM&%>^SbpbmQK0*5V_d3CXC!GVFUveoslJpI==U&t1^WvX=&RAuNBNZw zTYCm-{bx)}HYX>qs{>CR>PrGWj<>uN0k^N34rKw5ifMVXhTV-s`?K$&75ChthbAHEIS9uL$Vsq?l`Y4d>m!uH??9kV>-zsl<6lAQKnqB2>Hfq=4IPVA=(0e zfV!HY@Hw$E@e>+G;&TQ!6-z@&KXC^!e|n;?vTM`~J-6$=PLE_J(s1d4Pj=y8=d1eK zK}DwFYWtIxVw%!HG+mR|!?ShSA<1wrA8s%&8LokF2W#ZK(XqzT(jj6s^XNxbj{nek z$hl5px5TJ{KppRLQur%&g)8g6Iw|DVP(VXK z7tL0o+mujf_|>+Xc7X1nDaaYq<06C-cG32%yk%wUSz|*(S8U9+Q<68lU(CrHuNWE{ zJ8LT!tqsHl6|#}B;Q$uG_?FSp1NPw9_}Ik>`~6M3-v6>o58k_P??vcIlaWXgL*zw! z_uYH&(wFtu?PexdP9|%3AX&-+FPe7#?H1(diJc_n>L&wA=N05Y>2En3S6`%&MyMg+mSrm$>!qY)GC zQQxUbzWG!oU$tJ-`aON8D!KYXZI1OKZO*YT+u{B8{{8lcC(jTJiCXQ9Nib?oYMfSi zI%YO8JQ>ez({}F*wejoE(PoVOKfAp%+S?YEBO3Jn~jgMWpVdm-uHa%WJhNzAN zd0q&{TtFM0!k1_2A2@% z(y{;Zb37;D2<4R7L-(=83(kkc#}%VT?N7(BATQMGy9Y7)5*CudRuV;L{%`mk|FJb> zU8(d|B$X3CBs0J_jIloHllv9iZ}$cfS@d(#{qnVIfm|!I_PUKO^vS1yAufZGHDQHEU4E+n;Ldr5Jb6` zdX4wLrl*|Mj-Mcm!PLXvh>1)&5+WW|QZ_6VJeS04BIS$wzEQxw7)C>K>`EM`ALc$a zH;`_{;~7|F%=CmLO4uQcDq)9`td|4Gn51+OZkSvzB%_6&Aq^rdLJXvlMw(R6*uF7v z_b-;RjW{M#w0%UDl9e|Yc0gu#`F;DvhR4PWi)l-a0I5pYAmnU+=Xx(rZn2<28>Y;zucfc3{3ebaumVVWK2K!uB~A5D5jx15f3( zlEk^DtScl3T@>58uz4`%hP`?1>}!oKmG3^V=3$4(OD_oyVCCWOM5};&08SDN8oUuK z5&^O>X{xFbC7-CS)yQZR9oNYslCFv)i+Bd8N?Q0bTIG!d1FS(zYS}PsDV`M~vk2#i z28j61poEITeiOh4LrYG30#oHRpvP?GGb4q&442vej-KB*jBS*h)0H5!9dX zk4z)U6bf4_{xyT1N#{?T^F0e5aHx!TlA#VV=erC1q=kI)8au(cpi5c|n?v4|=FD16 z=n}e@r4D1CQ%=vU#a;)SfOGrQ1%rc=b{Z_BvHa-Xa$mnyKdn(8x8fN@tYBu(_{0^n zqvwFoIy`!)UY|t&9|>m>;d1!pRat-{-&nXMv1G6eloFk%S@ALogD$f+= z_T>u&cx951Mq};1zP%IeomemIPZq|qxgm(S%z2hf5wOfH1tJu}5waDq!<6SrrO6ET zMTDHpa6UI#+&w*gb*r36mVzKwB(168k!NkOiaC4>00b&0FP7y1ClKrNfk3cjYq|CA|$7wNiH$a0Wh{>0qA3 zaqrc&SA5EoY%V$hL{*~`iUXtmfC+H*AhRPN{D0`O%RaPi+ks>K(&G;L4~$Il2#$3vNZkPKN2IGUT|zKK)_^K6Vm)BOWYC}Tc)hYz>rqa z3~)$eXf?+|#?a7;SBel*(Qh!xHLW=24upQ7H9Wzw7y9BO~%c$Z~1+Z(t}ICa;?8a&u=UXCyO+f;+k_08 zAU{O>!vgo3IriMlTz~(*;ctlQJjPvyuJUr6M}&CF6w|2`6Bm_ZpRx}6#``LZNj533 zw~@^>G5bZrWib=+8VVZ=#lGN2y+G-<@g0@7=U{+Dloy2<`F==+=)ymRf5q!!GCGV}fYyBdvM_5S&c^~cDp zxG1Z>&5;pc!sAr~$B@EMfG+I`V0E$sIhiXyZ+*IupQ=@-vzZ#I%Q;@MW9sYIzP3ydoCmbUwqd;<@rnSw^ywQ z{3pmNW`_}iz^9vmUcNFX4E2CY^nTT2S6a|{6ELM+iMp|-Cz!OxtOgSo?XpTEg_enI zb1OG-N^6&XNkNFB@o-x!+)LKX96a%MC8NEaFpH*pKXK;s8}Qll=L#p@*IVz2?ZZGt zM`jvb|7)T4rmKt2y}lx$lm+1*!%!#e7ZGeE(5m!wwYt4kTP(A&kZBN6L|7K2U*uu2 zjz^t+Lhc|Hf@tx8<|&4JMEujMMX}ln`zhA`)1M1Z7cRyL!wV;zq?3w8Yj|5nqbYCl znPNzG3)-h$?@+K5i=!}#J0uvjydX>Urx2RZ(4`kFSlP*P28Z|}+zdOR4L6C4d~G3{ zEn3;d8W!Hc;zTanjENyARhg>btmAA4mvm2QP}_XybMfbjtzWnD~zB9f|6D1Iahz6e7Q4U|Yp zi^?Zl%ZMS+&xmCrs5_Clk-QTTF>_qj&QINX;K1)3nK^f8XltQZ zVIn71UjWouvYr%6XWS1hWKJ1C+wmtx_%^ng9nd)Hrnm9kK9WrgA|FNsGHAI)Lkr|E2SG=Odn{ z;d}NhW`JWDMwenW97AQm^f0fL19O}yxXIZ03S+=;U_bN?k!<&*gBeQ|LTyqBFE(^e zkz$8TR?geUI=={jhXE>64bZJX=h(O~xaryHy~=Odd#4XMU|y1SZ9`mMRy@#{8Q@Kf z&J4F_h8P$=lQV;YXcYBlHpDY!u%0^4Mk^A{20O>I_8r;IaiWBf;ZD2#c4yb>PvUt- zg)B4qxE&$!ST%W1;`=g2j`eur7hkgeLC4kZ{7eV~p^$k44dQkLQ0ercbE>bH}S!t6GpC zDh#1!pJMTJ!2$we6wPO=z_9gfWSmo%G}$8+o`wT=i5UiHZN0H)7=_zRd;X+Lc(E-$ zB@BpFChv44jw@t88K_A&8jD8y)17~ktnpFr>7{&rQ!`p3>0;P=M4af6x}mJB66H{s z<|qqxLbNRFCn^A&%dki?!g_i2yLNgYOXB(n@^&5?EcdHhzgS_gAVk#7#+kt6r}v5?Iz^jrTVLIL1XaOLYqe`}DtrY}fUBjK1VaL4L5Of>tOS!Lq4 z9P}Rdh&9mX++Zyk##-Fl+yW{$cD{7E9V0?e*h6+k<8>tzTf9NyqG+Mfuc&Zd7(vlI z=;t|j=ayvSB#>|INaxFYu@&saR`5p8*DUd0#kEG?vCAGS4eVthmROX`0r)vxgxX8>@rUYCJ;%J|`vjwe`sL=E?V-BG1M(9%zHnHTZ?%q|pu9IRz zlf_290Pix(W1d9v%^BnA?8Ysb%9w>~rD?VaYFMjUwh0)b-U)8_J<}n2V|+t<3$R2? zpXewbj4mR#vsjr7i*;WupI@jICoJPl7CyZ*-R$KSGbAX zxtljX%c>U&Gij7r?B*M4)p^0Ony*zih{)i0QyU6}x@BFvdGpzY&T}!gf))Y!L6IX` zC-i4B^X0$J4?of+Y)K(1m~UdlW>Tgj52!{4iGP6fZ)qI)LBbHOo+rq(vYl8Q6f?ls z=!hLqi+6E6l`aaMgU1r)O|p_Bvx-Aez>g_vAe9higLUPzSKhF1-)olay_815$lA#D zIi#6OWyIq;lf+bC5^yeQAx#c>5o4xhP5CO>+=nQga^M5F{qK zgTC2;J~F^JON*uAKp-$WIQXmse}_N`SE&juMR-$CJcL$V=xjqM{E$C%2gTk`coEct zHrU8Fe+~9J z^qle6)#W5BRSCWMcG4qdjRbyWgG_UI#0d?Odwpijn5MMdCF#mdhRf^yvPHJA-V=+x zU$`o+Lx_{>nx`#hms))CsjXx?=v?s$@Fpzl$j+S~yZGXd?c8|;Zhps(Ph4T;Hpr%~ zGtKMW!21@pj2Qk!*nKw_h2s~ZsuzudTED(vaBH#H*HSUv{Vci@h5+-f{Tw9UC|9CD$S{dx7k^{sa(#5C`&r`-m+@ zhb#(iIUh0`)B1V!kwHbExtI__Rq%m zmP<9;u9eDr$FGda`i}WqIt~y;dOy8;Eh(!y9>Za}BuN{zIM}mV&E1CU$OV*tmu{;m zP{3rB*DyVuMOVWmbh-3Z-5Xl%r>ZHVjdH8)DFhC;Ivn(eP+y_h|g=mEA;pXPI zh@L%QM`|eyJUW1gV%(V$On~*p&n|Xp0PCW$(UPQ%d%O><~I)d!X-4PXR>}) z`TEm1t9NqJt=O$zC4}khEO$zi{7oSaYacD-G6TB61I~pd#~EC4h`|G`PkiEogJ+G7 z9wD1@IP6XI9T^;2wCv$Z-*Tds$`_`{b%C@fyxI`Oj%qPqA~#p9ESD>-dVOW}=YEvy z;b1>5tavO&!hJF2{zMB8w<^kL;8-22zPs7l-d7n21|_pLoM!|`V&RKJL;_;M&J5?W z?M%8Q-hQ7HsO4mUSz)Z7EO-&UkkAzR7!6K2Zhv8_kWbYTOMQLA3qwOk`x4%8c)Zml z!QEMdDE)lN1e;wnVQ(F63jf?q$!GTKO_EvLuT`7|V23e+^%?;D~WTFIChY$d7 z5*JR#N*Z8LqeX>b2s*$hMp%|b*ja`xko3#^vpx47|Bmw<=L7Jwx8bUzbH?SlPN3XM zV!N!{lSA&z1Yn9V2YQLJk+V)w6WWOG#4;Rg?>1r>;(1*DG68>Z`)>@7OyO7Y(gXWn zIz7EKH8OnNHgRZ)yp`NK3j>wWcua6h5m}0%aF}WA^MO~J#~nwIIkE%GfF2V0v!^|I zpj@u7sJ*CG5oZA7Mo&G|Y+Sf7fAPTJSYNsC@chW`QmHa13h(f@LR+S$ZrsU3XBOr0 z`*+?rHMJ%5EdkCB+DnziT)tdLrdlNMQ}+ps>Swt+x`(vF zjcWDaICu*ncSo?^u?^Hy>1JZ{rSx{BHE_w&z@bKCtXwSa92K7_53!sc_=jQ7R?mBR zp;tf|dJ$8}8Y2b`msd7p9&mMT1;F>ojiOw%YrGJQKjRIrz9`+rzpfQBV!WXlx_o8l zM}S{j)4c~K1k{kCZX0B|8^PtoCl~>NvZs22>xa`@w>kcwc>)gj|K%q*;qQpePJUv+ zr?XkZ-f$+sK z5u;S#7Jg)#(&u(7mMc)io{FW6&%UX*Vre^V(e}6%3sa+FkK8UlJvF0a5Y5z6)NCKt zK?}z4rcqZA9mq@~IRZ9OGLiXcc((Jf4jceS<7I1h*1AVJ>-JKFH61p1GFl`Lp=Uf& zy~ZVQ4%ZyW8cxe{6C8=IW_UC#`3cO5And3Pfh8`~&lEEw`2VZAq0wQ_CCCzYLkqlQ z$iBw&DDm8ftUt0Iv3J`Ch!m3K6oa$vfcZCTv=9N*Uo7BRp#*-C9r<(ax)SDkCO7To zOkmN@60K&)L5qrv!cY+wT^kLmy_tE?3{VmpuSrO}p$d?7`ItBt{mpt~4?r(qRBtoW zJXzz2_>FYJV@~?or9-qsWETT;WPCACq#_!T1Cw%+d!0n|(1afPF=bD0$j!J7?4IYV zjE1(WH+o;hptU9d^PO{%$xeQ@a7Rz^>|XjZWy6duyL`}kqiEoeVDK|ECp=sS#pROp z;pTY#;WB1Uz{E)%)HCK zYxPQN*Y`IsEX>X>%x}KY`m%6AA+MERy|u*BmjEGe0N4E-0#Ql)%`gE$RY>{}ZrEEZ z;RM^M5Hm45aj`_QiCipR39SB=P$|?0N}QCAm|sHAlzyzA>n%b}f~tMkP*Tkk(7nuG zF|6UU*K?~rfGZ+%@LTDsOA+;txatV_o6uD~VcVm#rqrn`H>LCmLKfv7+AEctyLzr` zA$fqt1DXr++aBG=tuzL!tv++Ty;|CBiJUbzQObRUmS4Y#%i3keQX0co`88%)z)t4Q zJ96aU!6Qe`v%ZG<2^FYu*zE6DLKqTHUIHI2yR3s#jKyTUI)GY+fmm0_A3@UNHLVkq;JD5gr^Mv?H9K2Z9&7D%hlylS#RZRYH zv}#C^*#d&D%mzrk#xYSEP&Jr#UliAAI5U5Wcpldyg;_^;s9QHW*%d@!6pGJtkq&e} zkeKYKPjuM`t=!Ef0OQ=fPj+2G(=HI3H|)Q(*4)Kb5kUivwwG?CqKurh8;KGApN}9@ z=?-n4Des-fvWZLoq{RWb8wOSHhC{$$%T25pnb&9%Q0?QZ!`nKkpYAjL_!fuMnEfWB=Gc)Wk_F|)J%9ShjgLjvKTOZ3zPTThI zSO^8g*syI+PiDr7h1v4m3PvEf68;zZ{+J;nxAC)~*$3Kt1;H${>I@`6L1R54uu;4>duA`M` zGGINNfMqQHfkw!+YA!;%{}%K*ysKAVX+87->$4xX9_svs22J8A9t>vvU#NCoZatxp zY~Ie>No|{djhdF7Z*=Z3n<5E(5aX-h~B*dSr%tW(I$&ii`w zlkVF!vfEQBG3Ske{e;P3B(r%F@yps{VYziVCu%(9!kTH{2xRVrW7-r{9V7!PN*h(; zm}f0w-(Az+2ur84*4!v%PjzDf+A*^@vxo(&d-KV+apw$44BO_h%^_D#HBZiO(5yuE z1O&#(!Il>+OAs@hT4`~Cm9ez<=5s3kiOZnN+ns=AYC zrhs*`xMkV`F9kE;_uiqu#Ukp+xz1z$Vk$YDS0G|9P=VrktC_5l!9XlNQLSz)6^B>~ z@z_MwR|Zui5WEG74uQqfTDSlSU`7$_?!r$8)*;-?=ShuFMXwPKld$^c>&LwoxrK!) zi)aTi$K!)-#T-+IgL|>{Y}v+9RYC7ugRgv*Ek?Gh=UM~(^}f!}Z#?!rr~k&)kA&t`!>io`0gQX+%Zqz<(kw#I>AHD>=#oK5TxKmIv9T4kuOeeygF-~lt z0V|emX*?2rCDm9a3LI{%6!TU$7E6O68K3c*IZg%}`TT5IkYh#Ls{=$Ag+DR_MAu`v z@pPsSyGl6H%*$~la5Ry+%JN=G$AzJRtLzyavqu8}*201#Zpze5v&}IB?{h>qG7XLW z7&P`K&)Ymt;IAV251Cmqxr8a^qqEnnkAQA{=C?cj^h^s^FbJsF`bS}-Tqkke;mp>C zL(GY3wYH!@nayt?y{~L`T}y+b3Jw<>>!h?zsf_F4-smcFSz|`uwW+I6?dnX-(lKhV zel`nPocvu8D8ydnSisP`f9@LVNKBD20Y zPJyvnBF3JZ!w4ecG=P+%K`=KsSr7pVeucFS_))PDf{iZAdIWVjk$nxED3iM4|L|lX;dvHqMySokoV#^j^ec@QhCTJB-q&rxDNy6 z8TS%>4mD5uqgi49d``SbGB)OH$oC<2ba8v-HbWg?R-a})(pj-{%%!V@F~)fYPMD(0Q2Wzy@Tbk!1W!1*9R; zEnr{Z>U^DRwL>!Fzo~7E0Kbwr--@mNJ$t2NZ9D#;{X5M0tmkU>8*)x}vqidp*=<@r zlR`|V2JXj%s!_C0auF>w}Lr z25W)ci&5KEV#o+iYX5)qjLdqGqD_~ z-<;I4YVCAti_=rL?%Q|k)b!%|(dt85KhHEzGS9R$+)-DU^ZvqWw&;;j^Ce zP3t=AFaKRw5;7j*yoLWDj>N?2;4>n`J|7000#8;53rhoWQxHlJSx7F4*BMaF08)uh zFkA@277o$*==Ut^eY4j4t<~!UCRtXp&`vzITq)mp(MCwVnUaN$KDIs&kaEOoh>2TW zZ22vcKa^e~s91A}Sfr87&GpMP+Y(<|{e|uOsgcS7<3I?DR&b@( zh*X&Z5r4Qpl^#qX4g*~hO8fC!!A_^n@(^}R5JTm4mjwy98U60;X6XRea$nJxMUN}c#$Mi;`K?N2iXkd6Hc(WK;{Z@SYzpWm( znOH%Om)s)9oLMBlzqKx~zG3~FowBFwgCK08mo=J9OlQXW|XrMGtaphF^bP*??d{Y3o4UreBMyhao;C9Wp^3S_-w(D$6|6F3WVQ%&m z*C1(zdQNXR)va!PfZI{SJGoCcoKV49nA>6$-7s&iSzFhTS+?jaq{YJ9^{iWStwk8Y zQ(Z$buGglUPrfezwJtPHU@eV8)m{7n9MNZc8PEUGt6t~l$0BzbZ+u>u3r%ON&dVt7M4%!w z0gfROuOdrF2?vqTY_2SrMCi#kunm|yaD?jK!6HOkcr#ltB!DR7m}L2-(bpR2S9_UF|!qITJoZ!4BF2?CVFy_>++;ZNu`i8L5 zOGFpwlSFz#xm3M?1^PX_(31Bc6R=1>ls1lvDX4= zBy2%x1x^aq_P=^kIDTh`4w7{#x-Z$m@w^qU7sk-bGf_y=^j~$3+fFP9?ey9&rejR3 zI=cenV0drw*Ap2NO*F5Y&YH^CPRbkoHO``f_1Rm*-O{}2#(LvB(@^dg_B>N_X0sPE z{h5!Z9si|sJR;z{L-vE$iA>JqWB#a4A_l`M^V)mSMgOx`gkSM`n(QeT!RLR-^XI&p zd*Jtbi5w(-ojoxG4ja!J^I*=baV|Z~%lcjBYB#{v>_g-qlNUvti_m8Z z>2DF65rErTpX*r^FfHjR6_`|AroV~6-6nq~-b98{y3gw=YPKu$+aF*_0dpt`EW@&@%20X>JF$^3q5;*d9cVh`GxuJ;}Px`v9}rR6kFKZh^|^!6ZcV1|QP<{wl$PZgBHak9Hzn9eQ z3E!PWzY$1-H?uHrV)6kym>lP7bzpM=*8oFH0SvZih@hT=WqsS&KtQAj#Kc?(=@l0n zVg95J7GVG(pWc#X3D5mhFa`n`3GUer0P08GB?0l0Go8qGBt$=PXBd7c86&Soq8f+1 z3Mq$Fg~@gVwbc8Ho?W3bZTZ)B- zwm;|x1ckSN7A;4DPhPh#)C2{-J$goaXm9@V>Wj2+bp{UVY^~<^l+I1_Cm@Grcdo~BdQyb#D#WEsluiC=3`e~H`OF%b- zewy+w@2%HbMvbH$;DokN%m&a!9M?rjDi%Ul+u_Or>MqhId7J1xPPBF;-v7a=toaMu!z+0yhJsRKt&l-kTt|l>u7Lbw?IKOfk#>@_bv6;wl@V7V)vjG7~Ion ztW^CK;9B+$N;tQyLD63N?0aR+2+W2$PVr?cj3MyFtTrK}mUXYXW66?L7R%R0{T~ml z{z$1D^O0n98IJw_`-OJ_j|X|y{lf|C&H2GZ90B#>!NDnPqOF{Iwia`_rFyELoVXD+ zVEaZzuE!5tteWBMNIp;GvLGN^vlk5vjE@ftTr|rUh*xerpC6%!M3O+n*)m4Yz$8sF zn(RP4#-f22kCRlSo`C5?>|ed1-dEl?0{#r-HvnAd$iBXE-SVb6A*;0=s{(SQ!j#Y89t{Vh3$vZqWnmWPP}6W9W==~4lM5@B z7vCzA^I=UgHXKz!mq&Nun^h`yVOGjkt1W?>!tOvBjL)XKjx%aTYgw0AxeGc^3|slF z4}Rm?vJdqevjXDJ6ygutuCY=XiA^Xw>NWTQd7%K%l2@CFE3ZQke|h2Tp}G#tuo})G z)l33&neP06jY$zgRU5WV_j1d=#5b63kpX~`d-*evYS&`^;qmocoF7G037f_k< zI-4#Un-D!EbOFqpY(p;Rth9PjXx|XLtkS41`R47(3zT;###yagC&|spW;#Pi%oqm3 zLVb_q6C9-jNDu6n2#xCcg5jLKcvEQ8(9n_o!TrtFwrUVckP6!GM3QEgpE?f@`B*HH z%kP;Ik;`=%5%J4h0$`pmph{?JwRiyC=%Nef+p*4>`uCSU*{h z;H?bC?0Mgy@uV|qu9*Vb5Mwz|9@#zKVu1>(!`i!#<(!{(LLGe3t+CFdov&|x_Q>eW z%5z3WrYjqTE;3p9Bm9L^CMm0u%eAx5e(0h1ZF<8MSKMY1RbFimgZzBcSHDV8ri}>N zfE26qll9Hu*Pte1B;cIL-a1>^raiVbc>K@p@8AQ!)pI`Lm%clGrBvG0mUi(zXzJB-^&9JRS98QySpw}{IH9Y) ze3;&X094R!>tHPDXiV$X(f!rxkiQ@*ahNV%7kUtjjvrkBUzgOGeca zWJ|49_m8Ls-ujs=(|{8KU|4+CWHt%EEYpgNp1(N$`v@Oj;_a3^HBSp4^a(JX7RiaU z-Lr@HjX47gH$EC@yzte)&q4Ii{GC7X*B}b&h9$oSPrRVzoNr&Yn!F6y4f|jCee9!` zxj*)us}I`S-2eaCJ?ngFoS$F(bc!Eu_}JSoyzpxmUBu_lFTC&-o%^f<#~!y1bnYAN zIW6xNbFu43?|@Hxn6?)nuS(@1>&n|0?gy%Cgla>W)$qy^HY|^MiEx%mpcm1D%8Y<; zZoBok=T`fJ2kP*{gd{7rr`km82O?l3RwAn(CYe^`sju5-$@SI0I_|Mj=E4A$P{q=MpU;d+d5AXig<4nJ!BNK2*;IcM}rH$1Dz)5v&@etl19(W=KV| z%-ZNb;gTVqE(g2G>~ajRe7ISQZ^c&8g$-LHsTnyE`YIn+2hEWY#K9AD^v!8o*MxHp zOy9aDd)JS7(LAr!FeP2qw>21WJ7z9pAy(CbtFUm)Md-Y?b|P~5yc!`|5v(pn zBshLYT397uRjofP3zLUuzWlZ`@O8ddns~sJh~KPYP*qSQ3cvS&*gIVL%= zrN4sf{+#aih?i1PO?gA*8k-6PC?|PLQA*yh`T2hX1&v}L6>Gmpj3MWTs)7_M2+er{ zU=6C)>o$A2wFiQgjo3YEFoGk`_ET@YKtv4`*x9} z{q~>Rm)xlc_~HIdmC9_P^HWW$9JsSETd8d7|2amYXxx4T=MVPi*IOTZ#EwTJjoj;H z#`~-rWTV=vEQxHA_FmnCerQ-bQ7Qzz88c|FkO54g>!7dNW2+?lz5F2Ln?YuTD3F$O zuQ216GhC4nOg_Y827X_AW_*E={9%Yn<^ean@CBW{5)u|y-GnY|>#u}0>5S4O$6Vrqfo2q(7Zj!UYrRXyHl7RpY?4S ziF)pWZ;LWG=zmA9E+e)2p2BQjWmApdZ3gnTZ*yzkY~eQWeTe9W#-T4fejEOtFY!E& z7_KkbAF;m-ox}JG_F{8JU92Ev2!lFhp(K9An$TUi zvueOme%o-}(yS%7iL{l5j=?Ioq2sQ;Z>=pYdcM}S-UB1f&e1o3p!m~!?bkc*ioEO} z`Vn%akSK1B5bgea3KJLTI|~TH(+e{AvX|%q$6Rxxvkb2xg*RHj9~CI++Si^`2J64R zK;baW8!Mbx3E>NdD7oPr<_o`1EJnpoePufD61Jzl=cI;h5eykq;Swgo9|q(5KmxTF^xyM(}H}P@;z|N1cmAUd2~J1Q6OG8d9ID zSd#st)!%Z1y5K(#=v0~q0F!ClonG8B1?6qYW4(m+jko+eNiQs@Y~W4j5o3^ zQHgs6sf7m01;Kuhk6e)e1A()mdD0!>WA%O-ymli5S(Gy|3E2`mp6n~4?`3DyI`p}s z7st*AhrV{7Zl3g+m}~A6s4}mp%hTYdcKf4lJ;&aQ7F9Hlf`|;q%K=B$Y1ZZaY;;T< z_M4EmKU5nMorlOaI){d;DA-Z-65-PQk?EldQ58eomu87(L{t(#Vw#}arGJtarsuei z2PVS;0aVyc1fd3%$AY^_woO;H{cmM(bl2#Rm(~4|_bQ41WXL$BPm@`$9~lUwc^Pi= z!&&ws3Zq|DpC(GM3&?zuoMPCcWR~bhtB||6M}E|0we;|{*M1QBpEqTqz z6B?^8lDyA8$T@kwz50covkTe`sm;9I)yI1-(m>gU#a&+~ht4om(VemNLe|GQVcu}( zRXwNAB?*dzO?1B2bF#veq#v}S^PlUdk5Pj(?O)eV{fGxXuD(J%Sh{p{dgdsP5xoU| z#4;Pi0-&=&XNZvtw&1t%Mdv>C?Rqc!WiRnxQQi)sCW$zEFRc0QL&FTwGMuh#BSUlR zMxmNIcb;&oaKEs#hL@xsC6w+KY-pZn3}lyZwL&NCqYz2w{Z-JQr}Md1ESB+AceMs~ zRjVx+#`JvOJUGwfzl{S*NA6o+t6JSP(Arh){Q1C2rEjKCnktnS>JnWpX5)mA9HbSL zmDZl_4N9pjHo!fY}zQV?96oEz0ebm?Fw z!|cL74%%Wu<=wET=vCHYI3rV zun+gR2J*3WE0A^@8;yjZa<}#1-FH9uAY%~rWRCxd^CPgn^T_tkC2z|+JfHVG<~e4S ztOfLAu0UmEW8*>EZlw(5a@CdA5c(f}^M^=R>WllpEjTnUE(>a>`)suHy_*&mZaO#jtY_tpid4n&l(a|z z^&N(nVOtD-)qJBZe1aJ=Gi0Fl>OpP&KCR%RUraIV3mFm&WP<-Eh|M{IhA~o2^&I}V z=eXz4`n#SlJ%=9_$L@f)nIQ#3+9}SEZ9Ln#`}dr5ELP7nMfSp-^OEc&_UxiPEE~7@ z2;%u7o2zux*ZG*)UW5w@di%~A9ov^o?i(9Ds}J^$ld^ZSRUYj;kbd^F(@(o`dHKf9 zkJbNr5$treBIKfr#AZ0o1>$oa9b!~vloF=ibF|b2oE%y&e~zhDu=B$W0C$S<%LH*Z zA}L?>P*&xye-^z{!L!Nvv2znTmvdNOp3I31b&tH0b7oTlQY{)|mkKdgQ)W+a=cEx+ z%-l0OPM6cRp1R(D7C{(?FA;uWg%pY0cf%2DW_0ZOZQHLO9hBaM$oS+5+qW+--Fdcb7WMR_c>yK2UiefsihOGw|3mUj z{olmB3A|-@Rp)#5*=L^TbI+aUQ&qRB?p(K0NmVLy0tqC9Ko~NRgdhosC`rhp!AczH z*CIn`kOwNfW^9{o21O7=@S`@^`V>%k`Vgn~iO-??A#FcfQ>Xg-TWjxg&aG4s=;wQ< zcAdM=fB*OYzy9k#t+oDZ_{><0Db9*7%rZEp9GkJ4x+B|9m-pe>WV1_88eeK*Og^}8 ze|XaPQpv=9pS)g$(J@OimIWxC-GwLXU< z!m_+t|J2Ac>gX<*9^H@berojAsHE^SR=7c7HL}UmQzE=;IeX#AYX3zS-~L`g#t#C^ zF)GpS{-eP$D)BssDr$aXa{$dA;bhD%FN7aQ0CDr z3cs2rGTOM;HIVJi|QcKTitXJ&SB<_Y7&Iuzt` ze_W}!I0;(H#PI3h9bNon5NHZtBZK>#q8D+7W6orE^7!K7aV#6^3Rz(XUJ7DSPQs8h z@^23A9MwH|*XaId%o@m6!9dRcEcmyMk?}pmDOt$5=&yu-jvw!rB)*XNb0{O*-p6~a zKc4bU?}cA_(#0*IZKd0OvSN|sw-5(=SGqdH?!F*h!rG3PqvGMitwc0DAu>&uyd(_fVq3;lQxIm&s9xbrHN}bD;-F0kQ z%bsR$%A|r@v%&$tFg-qJiIfI!!D|sKSuSBI3gW$};31DcAFT$g=kFQZ!H$X}Rrsp( z;10Sxe4{qaSHcr9i0q`8ZIoJMbi*Q*&!BLiT^Ip}c^W6#Lnn!wCVxMA+1KE=WKFUyM2hf0I@ugNu_``{mm_QGozK!d}@1*{`Hb45$b^ zNhPaYy5kPq?_&h?#Xq`vL>}GzM`8)Wz}J}gT&I(mO3X7>m-hNg-R$UFwh`TIQ=1v_)*?3pv1 z$(%W(`f{8j{T+P6Yw^G`Lv)iDCC;EZ@SBPMQ~ZOS@jy1S|S4 zOLgMd4=pZETA9&|Wx|Xut3XoW@O_FfH=!k|@rS`L;$%$5^4LyQQ~&De8lDHAGQY4l zQ9u_BN&QW)KD35oimf9@9=UP;lIiKKb^)`EW+8jiYd1IclEKE&_18Y@>@U?TkOl44 zN~QkAAbIGiu!LcIV`}Q+xw(r81J+6|PtQDc?V_<<{|!}Mld7Zi1|gVDtvzLWW;qj1 zO-^2qhgJsA!tC60F2eG?cWf?|yY~C~{ewZUe(=!uUX^2vGIpBivK;Of2w``8R-^i4 zZB~v+3D7W8uY?s;5}|T1ajE1&=G3XnJ>U4G|J96x^QL2tV}#pTmg0yx1NGEOsz zd1U|Jn|L{q7v7!teR#K7>p+itWt|ymO!=78U~>tq4>H@E*^j;Z{W})W$IMQ&jC)oW z>D$O4x8xXZd+LXzo;9Mfvc{cV;K2&Y0V8^MCZ>Ch9>WyZ=`tJdC!;+i%6}3KBTOs% zlao8Bk}?)D!3*RpFQ2OHOiuPubwG1zb^?F7T4Ra1ab%Xr4&;=i5_QyE>M6#7nd8@| zxÊ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

~lcKp~1uF-@Zu8_5 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 delta 12010 zcma)i33wF6*7iA7)ic?*$wmlCCX>vBeaXh2fKgEaQBV9i4}#!pH+^yO}( zD~W>Ij~p|!+Yqhy7x|>Vxr&U=qlQcxD>jDvNS(++g_O}lMh#!_bnHo_`@xCKv148s z4@Zeq)l9RTq#|D-_wyoQCe4omOqH|-SuQ}zj4=%ygrWtcBrq(n>)}Q=8km7xZ~X^?l$g@?tFK5_Yn6`_fzhX?pNGi_Z;^J?gj28?xpV4 z?k(;e?t|`Q?sM+n+`qdoxv#pfyKj1gC)gA2iT1>L5jPERvWTThlJ-&5cj?$JGq zJ(rt@HcxKuYW`;P<$Zhg?elZM&p8+0xcJ${udn*Ow+j92xA}wojr?8w1N~?HKlyL^ z{fN@+mH!#t1}nM`|0_Bj(f#WB-Bs(l{6C^Q<|Zq+2rIZmcdEO+6qMO;g%!-bF z4!RSa6`b@3_#^zu{%-#cj{QISm-`F-{ZD`AJzaR}U#HHU`u5brCu>hmIytVUzUD^F z^_t&nN^9P!nOf7cCbOn(O`Dq5)q3@t)o)Z!tR7lDqNilRST-- zSIw)MQ}v*)sdmT|RWqvoQ8lG%V%3DI=c-0j^{9%k3O}*+_^RUzkIy@vb39?? zODiWP>`B-iJ|X<^@SFf4J`+N)AilwW@ns&(&zaNJ*R9F-twYSw3o$PweAwQ=)>Jm9 zbINU)Q~hxhb8>H{|EQ_gWwx1%AK+@n~>w3&dl<2m&A;3xl*@KY?A4y{jPXu|T{U{57x-1sK!I-&i0n2cH3cK>k|r&lZTyhR$0c zt^ofA{0lnk!M|G|-UzO>K)eb3w*}(O#B~bo-ofx7|bq&p7o1@wme9rz&u$Fz$3`720vzj_#_zf2_QZN9taddJ`FAc zhTulTXCYv6)tQs%NAL@PIf?!NW8&4}=ELa1x>qcq|G-l$;0ynlLv_=ka|Mh^Rre<3 z8{oHqw;|sGms%i!WStLqpGYv+I~QMbk-%8gVe$YZH1J{zBy8YMERZmV2$KpR5ddCl zfkYq}6A&N~1YQlSfgB7jw?HBk{FMa~Vc-qGCL*s0hhUEXR#b=rn`4WaDWbvVc%hafiwo5OPyauV1al5ZiWfX%M2hMWpEyZba`7uf7!ENUVhd=9{(Cfwj!bNnwN zk%0teX59_Q=A^j^+=ATN>{WmvV**`~7D!}*(K7&vEHHZGN;u?faHItiUBC_i6IOHu zBZwP`S&LViE+Gz%m!`L4{eKw1YQZOLjDurH5Nz| zfy;q)$R7e;Z-K;c@J0Y*DV_pX0^1;u0OOJcNIVPv8o;_D%s>tSharz54!VNzyJC*t z7+-DUo82%cu?@kTz#XdLgDIQa!Akyr<>{mMa_jQpP4_VOEpOHQ6V@)sdGe$3Z(CfB z;%&aK^QQ5J^4|Gnq2Z25XQY#XNb`$)s<)G=+|@hQCgS4CJN0{?1;>>ieq@X&@7DiA zyB0XUJ9c#7MqzfzH`#C|f+-0}WLN3zEr$R6?;PqqxysF}Y? zo|bETe>^!&=^_;$T`QuAZed8NE;;*9_U!IvMu9wCI? z*JxdHU-_3;%9oW7(6r~!!29I^>+e|ol zG8;Vp&B9zx{i%x5<~pR%1pSuMt`& z^pe-;oWPxgol)YKTa8x}`97tE`M;6NjQuIRCJY^Dcw6%eDZ&w>FD~Wi0H@`24C� zxUJEx4>w}zaC-FBH)Nb%`XNR$Wm~X5;~`GO7E+)3Fi+Pr9_HWT9c*3=k+?w!x&^OdiIy9voTP8)%L)4NzC6HgqaWoJu~5(H$J^{F{=K4FRv5SX@o~ZL=*J%8 zyZCuMzCS;J)Uf_ms;ocHi*;m1W=1+=@E7UAAD)}C(WL&^4;2^bfiZ*HLwJezGymU7<;GlUhKW?`s^8e9UW+tzRtaZ!t8dJCpR+ zVKIAbJl2RV;~mUT8Yh(*n^*^3-^9~EuWsgdxse{Rg?Gq6nW=xfh5x~S z>xEm{BLk40v6a`$IN8SNUdc|uGxcHH`7zYnw4D<}Vw!MfyX-8FyE*@sL z4j#lU^qD(&B~m#%c?h@D=kDazT&eHb$(g)CuiwdeT%dRUClBP+`rLoQgMNCmUHmvl z>C<=d19GhFrytqH!@0ZOemAa_le5*gY`;lE#ZB2)e{DA}5_2gcyJA+Q5Oya{j9~pY&pSNBV!1f69e) zn$4|bro=TK#!Wan)?6GilQ=WB9To_E%X$&p%hRw3g!6wXSt~{-ZdftmtRgHj=3>(D zpXJ61(l+b%B4aK!Svi?mIli;^I`sMvMOfq5nCMuvXss^|-(gs8EDL(XMCJA-v!`M8 z`LA~29DKafu(g8aan64lc8LuGhA4&SLnT#601KOtHW}%Rh)44&-lkRD0eOho#>x2YvhvcNBE=kEPK}5P<=K2~nZRkx@-!n}sI?2RrOa!@B@;m&82D ze^!i>Q}pY5*(q;|KlG%p`6c$~bHC}aVZCjo%%gTP^}$=c}MteSpngPBm6X<)9*jZZDm_r?5`f> zN8}&koc`@mPPgX~i;Y;x#4)~)wGRGo#3G~5ah@1&kMMtvm95e^@H=l7_Avhe+(5rF z-mB$>LN1USjCPlKxTYD{HN2)TxXSBnjUXfF&31wnnt?S?Jxb_Zhm1u%#4ABsAf?K`jFi4&a3EhZiVMY` ztPP?FdDIyHgm@g6hNd4FEH)T#6p5WSe%t8%v}kL$=V52DRj$@&j20=_@fc;JMXi*s z=B&RuP9zv>$BCxqd^2js3n}@eab}`8s5JIpmwsdIWZ@H@YS>6|Fmz9wRgq8TEc`G7Iwb+JcbiN>b49vg>8OT=V}jfZjU9q5~zQN3ukh&Kh}<=LVW zo=44niBT~}luNq}Ls5@mA2(0@g8iW};6w3~;8Y`ju^0!Vc5F(o$@RvDPsL**=yCGV z3dY{tp?BUQo9L%|37fA^rcLj)MTQuC%EXuEgY50?RnT&Aw=L+3EFcf%nN=QNBl0jX zbJvLRf#!9_(%h59ekoQdk(p`iS|^s9+ibnGLL?gRREVxY{Ecy7yZA|lJc?<$RBn`p zX=iE%M;k|XiTXhM8vh>opj>Bsdq{L)`J>om#2yh%gr-rEyk@jNCJsi}AN6mQOXL?u z*iWKRn)`?i#_01R8o>q7EO}Epjae7OyUafsiGPTXr8!cS7sb>-?KKpJ8u#1~6HFtO zas#fK?ZNW6okNVd4%sVM9+LIO`6PK)n7k&o8?9PG!W8x$ek9&_vy<#2wHK*9wo+%h z%35=GZXCH={%SWH*xXD0rD;*rMEqlwQR(dU285&1puDA@Xm9UB_2O_ormKrp5aIMBpQzldrQTs6bvd zu8x$?3Hy4Akt^g#{n;_H96M}1d92)bo9{Pro|lcn?9W1dj_hna>XjT|k0F6)fc3@` zugmp<>y7wXvX0Fd!b;@bTJQw<8O2ju8cuWch~=WO@zT3;Ci4kH_~b&?67f*>hOuO} zT+bX~JTXVc2Ag$XTrAf}dn5&6$MBWW>QniVir6I$xt@-2I2NkDm=yZzdJ&_y-YdfA zT;wRj@wqIOTA_cl{JVkz2S*w)K<4CCzyr@Ntp*L9Vf>5qO@J=yG{*(ulJq8PsGbTnKQYW7e zpmhIF@_RYosIHUPU&H7Af64JOD<;O3o!Ri^__t)`{lAmzuFJ`a)AcF0WW0HG>9=Ha zT!q_j$yWNsTQUwsfqt2fto!`3xiP^nubaL-DSwfjj2DF};s6T5Gr=|ao^eVlhk0|> z{hE4E53{Kdqqj}1Wb}w})2_M&;t7q?6zK(#N;e1mWTcA1z6G}e^-fGwc6MfLbPWCz z*^Yk~`tjYSD$a|11o6LqSzEL?lu8uinX?(2|L#;X`u|6jb~)H@X10sX?8v5$%#3N+ zM*i;&K;^FNY-{tE+o1iwo7MiizoD8Lla9&_wmrtP%~VG`nKNTe(?8Ev-9vE)UL?KL z%zXSx*PXeli=LCC#z9BrW8GVoqwtuG$639iqq>CGws-w{L;Z5S-n~HOnuBooB*z=R0yR(ZJ4UPf)N+9?_jMZ_Z(M#rtq78r zWxDb4qv~!uo+)?BAEjaZF-Q$(dq@9a> z%)Pwb=5nTb+{n~VzoGUR%Vw#i9O`gNr%aD?<~rk?q!*T`#9V8`h8IQocnZYNi|J5t zV2>W>jFI`3^_7*y#hg;ii>&8xk$YqNj9Bk-_(R#!c6k2admRqvXWHqTc_QK@u+>TmqIKn;kL&^PW`r5@9?cjN_8 zYkXX;`UhjZxUmI&OnUxSHD3Q}t9s46mOE4`TcEZBy^v=V>{a1{V~l?L)OwZ&v6G7U zMzzy~%ViWCQFk$(l4l-Oi`%B=R_J2NRcgm}@TGmst32i7jkY4Mn;e8zD zD-*7^pP=OsMSj2OfwI$0Y~XG33YADdtwtUOodV<~*ttbRusp~3>P26E&esQP%|71< z*(?E%Iz%mjJwODIB7Y_Y4)Cpz&7bKA$xLA)nGFF7H=|n=2ziM7%gQ&)o&iV+>Iu#Q zn%quz12@!1G)f~k(#?RkfKGr}*X%y_<*UXBs!-jl+2nOj@s+9Mz~@nt4?OQXq+CtL zK{^X$0%koE$v`x4k1s+??s6Zbra%gC$7?ffYB+&AweGMTd1m}(+T$x$jXNYlGJQ8K z+|jzjW0ChHkPnQJ*C+}d8wdF=V1%zyTVOW%u&>b8DEt!PjSRnnbR>9^Z@TTd&^(OY z0br6M?%=DpJ?J&N`vo8j7y`5c40NIc{1lJ}JOm5|Y5~(#GhkCc1297~9l$Rf6pz6$ z9n`?c%Gkeo?Jq8E@Qh-I6xCEC>7fqc&z?5x(IXOwzQY!cp z=$U!2;r|89zT$w);g3RE1DHW*@X>4mdH!4A0H6^9Y5_Jao4V!kAx8mQ0JGCK0_I4j`Qigzy<#B=In2KvnrCFa{|wRtfoB2Js%c5eIn)wpEB%?wP!uOAJUN5i!VxebcU@XgL6gnOr(%s|YvIkMAlmyZJH zU?!MNoBGqVHrB;y%I2)WK%l%KSj!$d0wk;p~CIKw9<#@WYt4 z6BDKYy@wyTgIZw02*99J5QR?0)EGt-o`_z-(i?@J6CKctUfnQYv8d;bgR!{%m@BZ| z=p0c3e!xgTLkUn$SWASulr$DSva44Gqz(om%hqWLYJ`<}&B|7dt9F`8)WY@+QjyrZ6IC*<90aeIQJ zdy%&fd@vt3TgZnCh>lbe9j(O?rnyAlk0&|>yJr+`6w`^$p|1Cr0HX6~=(l4;zeC}V zG@z8|Vh^HA=ZP-EYxA5!9UQna5?Dm^R|L=>fCGP@AiBB)xJq=h1#x;Bu^Ycbx~~#@ zz!~jamz#iO06*ATa5+I z5Vy_%Ah)g{ZiDaIHH0`H zy4_KyAPwtK0Tl09LVRyA@dHJ~J+p{=4GJaPzUR^U+o8sY&hhzIrq z(C|Q{3t?yw>OTS9BG?#u9!FG4h({prnd#=S6ksQgp#%_*JA>0D(3{YicoOQpRBy^p;2QD@Fb072<}0Ho(mLYp6qC;p(4cwTz|hUT3oo*xes0A;`l z;st1UK~G>hfV>4)aonUm0AmZ!5P#SMC?#GrjCipES^$%P)x;lX0EIvafb^$(fD6Q* zjQ}oS{FmkvFWpI8))|12Wn%$2WS(#N92Gxb094@Y3L0DvBg^ZFS4;+AWMvirgR7ze z7+i%0zK92U0!x5HINDMKoWQY`6ymQ!h}Xe}f%YnT;JC_cjQ>U$-;|HzCoOR7q!>6x zyd6qA0)R5&ohaLR21ieZ5$}TWU5I*jf8xDx?CUz>{YB<+5}X_vj}s#!aZ)6M_susr-P@YvmqJ19{ z9ScZwDkagm2uBmf;z&Yg5_!ad;YUW2=pPTv#E}6g z4w_A3FboeVBQX>XJp}_J;OKL3W;D`c+K?CrofmtMn1se&&LS}-5e>jzG3rl6-n8>1 zW}G4MMg)nsQ1>m2zj(Wd#CtPI_@F#*GKu;1BtBe1ViA-UL0-Ix#K)+-0u8OaKw>rW zzd~N+ITAYyNbEuVy>%pxwI}giA_;V}_;Wmo>y;$2pO7K=(Jf{+$%G1$$;k7pCE2=^ zWacE2Ig`93b5D@$+@B;C2ic{TWL^YNM>2mW$!>c{c8@1{cOl7pdXU6TNcK2H@_~HZ zeNpcb==Vn>16zW{j(08a-Isc0y~l#+@C;%1VH zM_Ho+QrMKJghQkfqk;CMnye+2G@DfNWKt<;zzJin5v0<=>2pbWz)jl#XGk>OpD@c+7rMV^R0{ ztE9%4k(%IzHxr4}q>-eGp)_?Zsp(myW*j3mvp=a>C@VqPTWIt>L^T_E@7Ix<3th|) z^#Kgd?@4NL0I5&&?N~ z1BKBlvm75bUrpSW1@b%Gmd#fdDYs<>`QF>IMzKgW)U^@LxZIY5D2pD#iw;&eBWBrb zkQ;0^$PG5};>lEMFxpViV6;KDp>pfnh8m0xr%^P9o~38#QM}Y0M=wAcL!;qdTWV#! n=)GM&eAJj{pMlC9^@kxpO%rG&4MDYk=j#KiR1Qm=q)qxi%Vr=# From 9276778181619be4065715ab03d36ca0b318667d Mon Sep 17 00:00:00 2001 From: Matthew Runo <74583+inktomi@users.noreply.github.com> Date: Mon, 2 Mar 2026 04:00:18 -0800 Subject: [PATCH 08/58] Avoid repaints on device mouse motion outside window (#7866) ## Summary - Ignore raw device mouse motion unless the window is focused and the pointer is inside it - Also handles pointers starting down and then moving into or out of the window (drag & drop) - Prevents global mouse motion from triggering continuous repaint loops - Applies to both glow and wgpu backends ## Testing - I ran the check script, nothing seemed to fail --------- Co-authored-by: Emil Ernerfeldt --- crates/eframe/src/native/glow_integration.rs | 15 ++++++++++++--- crates/eframe/src/native/wgpu_integration.rs | 15 ++++++++++++--- crates/egui-winit/src/lib.rs | 18 +++++++++++++++++- crates/egui_kittest/README.md | 4 ++-- 4 files changed, 43 insertions(+), 9 deletions(-) diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index 233ce5a49..48557675c 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -448,12 +448,21 @@ impl WinitApp for GlowWinitApp<'_> { if let Some(viewport) = glutin .focused_viewport .and_then(|viewport| glutin.viewports.get_mut(&viewport)) + && let Some(window) = viewport.window.as_ref() { - if let Some(egui_winit) = viewport.egui_winit.as_mut() { - egui_winit.on_mouse_motion(delta); + if !window.has_focus() + && !viewport + .egui_winit + .as_ref() + .map(|state| state.is_any_pointer_button_down()) + .unwrap_or(false) + { + return Ok(EventResult::Wait); } - if let Some(window) = viewport.window.as_ref() { + if let Some(egui_winit) = viewport.egui_winit.as_mut() + && egui_winit.on_mouse_motion(delta) + { return Ok(EventResult::RepaintNext(window.id())); } } diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index cb634200a..7cfdab148 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -454,12 +454,21 @@ impl WinitApp for WgpuWinitApp<'_> { if let Some(viewport) = shared .focused_viewport .and_then(|viewport| shared.viewports.get_mut(&viewport)) + && let Some(window) = viewport.window.as_ref() { - if let Some(egui_winit) = viewport.egui_winit.as_mut() { - egui_winit.on_mouse_motion(delta); + if !window.has_focus() + && !viewport + .egui_winit + .as_ref() + .map(|state| state.is_any_pointer_button_down()) + .unwrap_or(false) + { + return Ok(EventResult::Wait); } - if let Some(window) = viewport.window.as_ref() { + if let Some(egui_winit) = viewport.egui_winit.as_mut() + && egui_winit.on_mouse_motion(delta) + { return Ok(EventResult::RepaintNext(window.id())); } } diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 54059cbd6..c010febd5 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -640,11 +640,27 @@ impl State { self.has_sent_ime_enabled = false; } - pub fn on_mouse_motion(&mut self, delta: (f64, f64)) { + /// Returns `true` if the event was sent to egui. + pub fn on_mouse_motion(&mut self, delta: (f64, f64)) -> bool { + if !self.is_pointer_in_window() && !self.any_pointer_button_down { + return false; + } + self.egui_input.events.push(egui::Event::MouseMoved(Vec2 { x: delta.0 as f32, y: delta.1 as f32, })); + true + } + + /// Returns `true` when the pointer is currently inside the window. + pub fn is_pointer_in_window(&self) -> bool { + self.pointer_pos_in_points.is_some() + } + + /// Returns `true` if any pointer button is currently held down. + pub fn is_any_pointer_button_down(&self) -> bool { + self.any_pointer_button_down } /// Call this when there is a new [`accesskit::ActionRequest`]. diff --git a/crates/egui_kittest/README.md b/crates/egui_kittest/README.md index 638c61522..7b97f4e1d 100644 --- a/crates/egui_kittest/README.md +++ b/crates/egui_kittest/README.md @@ -97,12 +97,12 @@ You should add the following to your `.gitignore`: * …have a low resolution to avoid growth in repo size * …have a low comparison threshold to avoid the test passing despite unwanted differences (the default threshold should be fine for most usecases!) -### What do do when CI / another computer produces a different image? +### What to do when CI / another computer produces a different image? The default tolerance settings should be fine for almost all gui comparison tests. However, especially when you're using custom rendering, you may observe images difference with different setups leading to unexpected test failures. -First check whether the difference is due to a change in enabled rendering features, potentially due to difference in hardware (/software renderer) capabilitites. +First check whether the difference is due to a change in enabled rendering features, potentially due to difference in hardware (/software renderer) capabilities. Generally you should carefully enforcing the same set of features for all test runs, but this may happen nonetheless. Once you validated that the differences are miniscule and hard to avoid, you can try to _carefully_ adjust the comparison tolerance setting (`SnapshotOptions::threshold`, TODO([#5683](https://github.com/emilk/egui/issues/5683)): as well as number of pixels allowed to differ) for the specific test. From 2be6e225bf9eae7e1e5f224f53aaf512a952eb66 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 2 Mar 2026 18:36:04 +0100 Subject: [PATCH 09/58] Add `ViewportInfo::occluded` and `visible` (#7948) * Part of https://github.com/emilk/egui/issues/5112 * Part of https://github.com/emilk/egui/issues/5113 * Part of https://github.com/emilk/egui/issues/5136 Once we support calling `App::logic` when an app is occluded or minimized, it is useful to know that it is, in fact, occluded or minimized. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Cargo.lock | 8 +++ crates/eframe/src/native/glow_integration.rs | 8 +++ crates/eframe/src/native/wgpu_integration.rs | 8 +++ crates/eframe/src/web/backend.rs | 13 +++- crates/egui/src/data/input.rs | 31 ++++++++++ tests/test_background_logic/Cargo.toml | 14 +++++ tests/test_background_logic/src/main.rs | 64 ++++++++++++++++++++ 7 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 tests/test_background_logic/Cargo.toml create mode 100644 tests/test_background_logic/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 9140f2160..eb9a385b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4303,6 +4303,14 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "test_background_logic" +version = "0.1.0" +dependencies = [ + "eframe", + "env_logger", +] + [[package]] name = "test_egui_extras_compilation" version = "0.1.0" diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index 48557675c..4cda3e762 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -822,6 +822,14 @@ impl GlowWinitRunning<'_> { } } + winit::event::WindowEvent::Occluded(is_occluded) => { + if let Some(viewport_id) = viewport_id + && let Some(viewport) = glutin.viewports.get_mut(&viewport_id) + { + viewport.info.occluded = Some(*is_occluded); + } + } + winit::event::WindowEvent::CloseRequested => { if viewport_id == Some(ViewportId::ROOT) && self.integration.should_close() { log::debug!( diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index 7cfdab148..d4dfeb45d 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -852,6 +852,14 @@ impl WgpuWinitRunning<'_> { } } + winit::event::WindowEvent::Occluded(is_occluded) => { + if let Some(viewport_id) = viewport_id + && let Some(viewport) = shared.viewports.get_mut(&viewport_id) + { + viewport.info.occluded = Some(*is_occluded); + } + } + winit::event::WindowEvent::CloseRequested => { if viewport_id == Some(ViewportId::ROOT) && integration.should_close() { log::debug!( diff --git a/crates/eframe/src/web/backend.rs b/crates/eframe/src/web/backend.rs index 4814fa99b..e2724fc49 100644 --- a/crates/eframe/src/web/backend.rs +++ b/crates/eframe/src/web/backend.rs @@ -31,11 +31,18 @@ impl WebInput { time: Some(super::now_sec()), ..self.raw.take() }; - raw_input + let viewport = raw_input .viewports .entry(egui::ViewportId::ROOT) - .or_default() - .native_pixels_per_point = Some(super::native_pixels_per_point()); + .or_default(); + viewport.native_pixels_per_point = Some(super::native_pixels_per_point()); + + // A hidden browser tab is effectively occluded. + let hidden = web_sys::window() + .and_then(|w| w.document()) + .is_some_and(|doc| doc.hidden()); + viewport.occluded = Some(hidden); + raw_input } diff --git a/crates/egui/src/data/input.rs b/crates/egui/src/data/input.rs index 787867569..a52d40233 100644 --- a/crates/egui/src/data/input.rs +++ b/crates/egui/src/data/input.rs @@ -253,9 +253,28 @@ pub struct ViewportInfo { /// /// This should be the same as [`RawInput::focused`]. pub focused: Option, + + /// Is the window fully occluded (completely covered) by another window? + /// + /// Not all platforms support this. + /// On platforms that don't, this will be `None` or `Some(false)`. + pub occluded: Option, } impl ViewportInfo { + /// Is the window considered visible for rendering purposes? + /// + /// A window is not visible if it is minimized or occluded. + /// When not visible, the UI is not painted and rendering is skipped, + /// but application logic may still be executed by some integrations. + pub fn visible(&self) -> Option { + match (self.minimized, self.occluded) { + (Some(true), _) | (_, Some(true)) => Some(false), + (Some(false), Some(false)) => Some(true), + (_, None) | (None, _) => None, + } + } + /// This viewport has been told to close. /// /// If this is the root viewport, the application will exit @@ -282,6 +301,7 @@ impl ViewportInfo { maximized: self.maximized, fullscreen: self.fullscreen, focused: self.focused, + occluded: self.occluded, } } @@ -298,6 +318,7 @@ impl ViewportInfo { maximized, fullscreen, focused, + occluded, } = self; crate::Grid::new("viewport_info").show(ui, |ui| { @@ -345,6 +366,16 @@ impl ViewportInfo { ui.label(opt_as_str(focused)); ui.end_row(); + ui.label("Occluded:"); + ui.label(opt_as_str(occluded)); + ui.end_row(); + + let visible = self.visible(); + + ui.label("Visible:"); + ui.label(opt_as_str(&visible)); + ui.end_row(); + fn opt_rect_as_string(v: &Option) -> String { v.as_ref().map_or(String::new(), |r| { format!("Pos: {:?}, size: {:?}", r.min, r.size()) diff --git a/tests/test_background_logic/Cargo.toml b/tests/test_background_logic/Cargo.toml new file mode 100644 index 000000000..92985d5ee --- /dev/null +++ b/tests/test_background_logic/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "test_background_logic" +version = "0.1.0" +license = "MIT OR Apache-2.0" +edition = "2024" +rust-version = "1.92" +publish = false + +[lints] +workspace = true + +[dependencies] +eframe = { workspace = true, features = ["default"] } +env_logger = { workspace = true, features = ["auto-color", "humantime"] } diff --git a/tests/test_background_logic/src/main.rs b/tests/test_background_logic/src/main.rs new file mode 100644 index 000000000..ea80cfa9e --- /dev/null +++ b/tests/test_background_logic/src/main.rs @@ -0,0 +1,64 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +#![expect(rustdoc::missing_crate_level_docs)] +#![allow(clippy::print_stderr)] + +use std::time::Duration; + +use eframe::egui::{self, ViewportInfo}; + +fn main() { + env_logger::init(); + + let _ = eframe::run_native( + "Background Logic Test", + eframe::NativeOptions { + viewport: egui::ViewportBuilder::default().with_inner_size([400.0, 200.0]), + ..Default::default() + }, + Box::new(|_cc| Ok(Box::new(App))), + ); +} + +struct App; + +impl eframe::App for App { + fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + eprintln!("App::logic called {}", viewport_info(ctx)); + ctx.request_repaint_after(Duration::from_secs(1)); + } + + fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) { + eprintln!("App::ui called {}", viewport_info(ui.ctx())); + ui.centered_and_justified(|ui| { + ui.heading("Minimize this window"); + }); + } +} + +fn viewport_info(ctx: &egui::Context) -> String { + ctx.input(|i| { + let ViewportInfo { + minimized, + focused, + occluded, + .. + } = i.viewport(); + + let visible = i.viewport().visible(); + + let mut s = String::new(); + + let flags = [ + ("focused", focused), + ("occluded", occluded), + ("minimized", minimized), + ("visible", &visible), + ]; + for (name, value) in flags { + if let Some(value) = value { + s += &format!(" {name}={value}"); + } + } + s + }) +} From 1b8a9fe95ea63278ce7edcf5371fc0efb81fe137 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 2 Mar 2026 19:30:24 +0100 Subject: [PATCH 10/58] Only run `App::ui` if the application is visible (#7950) * Closes https://github.com/emilk/egui/issues/5113 * Part of https://github.com/emilk/egui/issues/5112 * Part of https://github.com/emilk/egui/issues/5136 If the application is invisible (occluded or minimized), and the user calls `.request_repaint`, then we should call `App::logic`, but NOT `App::ui`. There are still some situations where `App::logic` is not called when it should be, but at least now we can skip running the UI code when the app is invisible. --- crates/eframe/src/native/epi_integration.rs | 25 +-- crates/eframe/src/native/glow_integration.rs | 151 ++++++++++--------- crates/eframe/src/native/wgpu_integration.rs | 97 ++++++------ crates/eframe/src/web/app_runner.rs | 20 ++- 4 files changed, 164 insertions(+), 129 deletions(-) diff --git a/crates/eframe/src/native/epi_integration.rs b/crates/eframe/src/native/epi_integration.rs index 96a52db88..5b22eb08c 100644 --- a/crates/eframe/src/native/epi_integration.rs +++ b/crates/eframe/src/native/epi_integration.rs @@ -265,6 +265,7 @@ impl EpiIntegration { app: &mut dyn epi::App, viewport_ui_cb: Option<&DeferredViewportUiCallback>, mut raw_input: egui::RawInput, + is_visible: bool, ) -> egui::FullOutput { raw_input.time = Some(self.beginning.elapsed().as_secs_f64()); @@ -275,23 +276,27 @@ impl EpiIntegration { let full_output = self.egui_ctx.run_ui(raw_input, |ui| { if let Some(viewport_ui_cb) = viewport_ui_cb { // Child viewport - profiling::scope!("viewport_callback"); - viewport_ui_cb(ui); + if is_visible { + profiling::scope!("viewport_callback"); + viewport_ui_cb(ui); + } } else { { profiling::scope!("App::logic"); app.logic(ui.ctx(), &mut self.frame); } - { - profiling::scope!("App::update"); - #[expect(deprecated)] - app.update(ui.ctx(), &mut self.frame); - } + if is_visible { + { + profiling::scope!("App::update"); + #[expect(deprecated)] + app.update(ui.ctx(), &mut self.frame); + } - { - profiling::scope!("App::ui"); - app.ui(ui, &mut self.frame); + { + profiling::scope!("App::ui"); + app.ui(ui, &mut self.frame); + } } } }); diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index 4cda3e762..724ddc6d5 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -545,7 +545,7 @@ impl GlowWinitRunning<'_> { } } - let (raw_input, viewport_ui_cb) = { + let (raw_input, viewport_ui_cb, is_visible) = { let mut glutin = self.glutin.borrow_mut(); let egui_ctx = glutin.egui_ctx.clone(); let Some(viewport) = glutin.viewports.get_mut(&viewport_id) else { @@ -556,6 +556,8 @@ impl GlowWinitRunning<'_> { }; egui_winit::update_viewport_info(&mut viewport.info, &egui_ctx, window, false); + let is_visible = viewport.info.visible().unwrap_or(true); + let Some(egui_winit) = viewport.egui_winit.as_mut() else { return Ok(EventResult::Wait); }; @@ -571,7 +573,7 @@ impl GlowWinitRunning<'_> { .map(|(id, viewport)| (*id, viewport.info.clone())) .collect(); - (raw_input, viewport_ui_cb) + (raw_input, viewport_ui_cb, is_visible) }; // HACK: In order to get the right clear_color, the system theme needs to be set, which @@ -587,7 +589,7 @@ impl GlowWinitRunning<'_> { let has_many_viewports = self.glutin.borrow().viewports.len() > 1; let clear_before_update = !has_many_viewports; // HACK: for some reason, an early clear doesn't "take" on Mac with multiple viewports. - if clear_before_update { + if is_visible && clear_before_update { // clear before we call update, so users can paint between clear-color and egui windows: let mut glutin = self.glutin.borrow_mut(); @@ -622,9 +624,12 @@ impl GlowWinitRunning<'_> { // The update function, which could call immediate viewports, // so make sure we don't hold any locks here required by the immediate viewports rendeer. - let full_output = - self.integration - .update(self.app.as_mut(), viewport_ui_cb.as_deref(), raw_input); + let full_output = self.integration.update( + self.app.as_mut(), + viewport_ui_cb.as_deref(), + raw_input, + is_visible, + ); // ------------------------------------------------------------ @@ -667,85 +672,87 @@ impl GlowWinitRunning<'_> { egui_winit.handle_platform_output(&window, platform_output); - let clipped_primitives = integration.egui_ctx.tessellate(shapes, pixels_per_point); + if is_visible { + let clipped_primitives = integration.egui_ctx.tessellate(shapes, pixels_per_point); - { - // We may need to switch contexts again, because of immediate viewports: - frame_timer.pause(); - change_gl_context(current_gl_context, not_current_gl_context, gl_surface); - frame_timer.resume(); - } + { + // We may need to switch contexts again, because of immediate viewports: + frame_timer.pause(); + change_gl_context(current_gl_context, not_current_gl_context, gl_surface); + frame_timer.resume(); + } - let screen_size_in_pixels: [u32; 2] = window.inner_size().into(); + let screen_size_in_pixels: [u32; 2] = window.inner_size().into(); - if !clear_before_update { - painter.clear(screen_size_in_pixels, clear_color); - } + if !clear_before_update { + painter.clear(screen_size_in_pixels, clear_color); + } - painter.paint_and_update_textures( - screen_size_in_pixels, - pixels_per_point, - &clipped_primitives, - &textures_delta, - ); + painter.paint_and_update_textures( + screen_size_in_pixels, + pixels_per_point, + &clipped_primitives, + &textures_delta, + ); - { - for action in viewport.actions_requested.drain(..) { - match action { - ActionRequested::Screenshot(user_data) => { - let screenshot = painter.read_screen_rgba(screen_size_in_pixels); - egui_winit - .egui_input_mut() - .events - .push(egui::Event::Screenshot { - viewport_id, - user_data, - image: screenshot.into(), - }); - } - ActionRequested::Cut => { - egui_winit.egui_input_mut().events.push(egui::Event::Cut); - } - ActionRequested::Copy => { - egui_winit.egui_input_mut().events.push(egui::Event::Copy); - } - ActionRequested::Paste => { - if let Some(contents) = egui_winit.clipboard_text() { - let contents = contents.replace("\r\n", "\n"); - if !contents.is_empty() { - egui_winit - .egui_input_mut() - .events - .push(egui::Event::Paste(contents)); + { + for action in viewport.actions_requested.drain(..) { + match action { + ActionRequested::Screenshot(user_data) => { + let screenshot = painter.read_screen_rgba(screen_size_in_pixels); + egui_winit + .egui_input_mut() + .events + .push(egui::Event::Screenshot { + viewport_id, + user_data, + image: screenshot.into(), + }); + } + ActionRequested::Cut => { + egui_winit.egui_input_mut().events.push(egui::Event::Cut); + } + ActionRequested::Copy => { + egui_winit.egui_input_mut().events.push(egui::Event::Copy); + } + ActionRequested::Paste => { + if let Some(contents) = egui_winit.clipboard_text() { + let contents = contents.replace("\r\n", "\n"); + if !contents.is_empty() { + egui_winit + .egui_input_mut() + .events + .push(egui::Event::Paste(contents)); + } } } } } + + integration.post_rendering(&window); } - integration.post_rendering(&window); - } + { + // vsync - don't count as frame-time: + frame_timer.pause(); + profiling::scope!("swap_buffers"); + let context = current_gl_context.as_ref().ok_or_else(|| { + egui_glow::PainterError::from( + "failed to get current context to swap buffers".to_owned(), + ) + })?; - { - // vsync - don't count as frame-time: - frame_timer.pause(); - profiling::scope!("swap_buffers"); - let context = current_gl_context.as_ref().ok_or_else(|| { - egui_glow::PainterError::from( - "failed to get current context to swap buffers".to_owned(), - ) - })?; + gl_surface.swap_buffers(context)?; + frame_timer.resume(); + } - gl_surface.swap_buffers(context)?; - frame_timer.resume(); - } - - // give it time to settle: - #[cfg(feature = "__screenshot")] - if integration.egui_ctx.cumulative_pass_nr() == 2 - && let Ok(path) = std::env::var("EFRAME_SCREENSHOT_TO") - { - save_screenshot_and_exit(&path, &painter, screen_size_in_pixels); + // give it time to settle: + #[cfg(feature = "__screenshot")] + if integration.egui_ctx.cumulative_pass_nr() == 2 + && let Ok(path) = std::env::var("EFRAME_SCREENSHOT_TO") + { + save_screenshot_and_exit(&path, &painter, screen_size_in_pixels); + } } glutin.handle_viewport_output(event_loop, &integration.egui_ctx, &viewport_output); diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index d4dfeb45d..9d6283808 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -573,7 +573,7 @@ impl WgpuWinitRunning<'_> { let mut frame_timer = crate::stopwatch::Stopwatch::new(); frame_timer.start(); - let (viewport_ui_cb, raw_input) = { + let (viewport_ui_cb, raw_input, is_visible) = { profiling::scope!("Prepare"); let mut shared_lock = shared.borrow_mut(); @@ -617,6 +617,8 @@ impl WgpuWinitRunning<'_> { }; egui_winit::update_viewport_info(info, &integration.egui_ctx, window, false); + let is_visible = viewport.info.visible().unwrap_or(true); + { profiling::scope!("set_window"); pollster::block_on(painter.set_window(viewport_id, Some(Arc::clone(window))))?; @@ -637,14 +639,19 @@ impl WgpuWinitRunning<'_> { painter.handle_screenshots(&mut raw_input.events); - (viewport_ui_cb, raw_input) + (viewport_ui_cb, raw_input, is_visible) }; // ------------------------------------------------------------ // Runs the update, which could call immediate viewports, // so make sure we hold no locks here! - let full_output = integration.update(app.as_mut(), viewport_ui_cb.as_deref(), raw_input); + let full_output = integration.update( + app.as_mut(), + viewport_ui_cb.as_deref(), + raw_input, + is_visible, + ); // ------------------------------------------------------------ @@ -685,52 +692,58 @@ impl WgpuWinitRunning<'_> { egui_winit.handle_platform_output(window, platform_output); - let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point); + let vsync_secs = if is_visible { + let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point); - let mut screenshot_commands = vec![]; - viewport.actions_requested.retain(|cmd| { - if let ActionRequested::Screenshot(info) = cmd { - screenshot_commands.push(info.clone()); - false - } else { - true - } - }); - let vsync_secs = painter.paint_and_update_textures( - viewport_id, - pixels_per_point, - app.clear_color(&egui_ctx.global_style().visuals), - &clipped_primitives, - &textures_delta, - screenshot_commands, - ); + let mut screenshot_commands = vec![]; + viewport.actions_requested.retain(|cmd| { + if let ActionRequested::Screenshot(info) = cmd { + screenshot_commands.push(info.clone()); + false + } else { + true + } + }); + let vsync_secs = painter.paint_and_update_textures( + viewport_id, + pixels_per_point, + app.clear_color(&egui_ctx.global_style().visuals), + &clipped_primitives, + &textures_delta, + screenshot_commands, + ); - for action in viewport.actions_requested.drain(..) { - match action { - ActionRequested::Screenshot { .. } => { - // already handled above - } - ActionRequested::Cut => { - egui_winit.egui_input_mut().events.push(egui::Event::Cut); - } - ActionRequested::Copy => { - egui_winit.egui_input_mut().events.push(egui::Event::Copy); - } - ActionRequested::Paste => { - if let Some(contents) = egui_winit.clipboard_text() { - let contents = contents.replace("\r\n", "\n"); - if !contents.is_empty() { - egui_winit - .egui_input_mut() - .events - .push(egui::Event::Paste(contents)); + for action in viewport.actions_requested.drain(..) { + match action { + ActionRequested::Screenshot { .. } => { + // already handled above + } + ActionRequested::Cut => { + egui_winit.egui_input_mut().events.push(egui::Event::Cut); + } + ActionRequested::Copy => { + egui_winit.egui_input_mut().events.push(egui::Event::Copy); + } + ActionRequested::Paste => { + if let Some(contents) = egui_winit.clipboard_text() { + let contents = contents.replace("\r\n", "\n"); + if !contents.is_empty() { + egui_winit + .egui_input_mut() + .events + .push(egui::Event::Paste(contents)); + } } } } } - } - integration.post_rendering(window); + integration.post_rendering(window); + + vsync_secs + } else { + 0.0 + }; let active_viewports_ids: ViewportIdSet = viewport_output.keys().copied().collect(); diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index 11654135d..b90b8a5e1 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -274,13 +274,21 @@ impl AppRunner { self.app.raw_input_hook(&self.egui_ctx, &mut raw_input); + let is_visible = raw_input + .viewports + .get(&egui::ViewportId::ROOT) + .and_then(|v| v.visible()) + .unwrap_or(true); + let full_output = self.egui_ctx.run_ui(raw_input, |ui| { self.app.logic(ui.ctx(), &mut self.frame); - #[expect(deprecated)] - self.app.update(ui.ctx(), &mut self.frame); + if is_visible { + #[expect(deprecated)] + self.app.update(ui.ctx(), &mut self.frame); - self.app.ui(ui, &mut self.frame); + self.app.ui(ui, &mut self.frame); + } }); let egui::FullOutput { platform_output, @@ -311,8 +319,10 @@ impl AppRunner { } self.handle_platform_output(platform_output); - self.textures_delta.append(textures_delta); - self.clipped_primitives = Some(self.egui_ctx.tessellate(shapes, pixels_per_point)); + if is_visible { + self.textures_delta.append(textures_delta); + self.clipped_primitives = Some(self.egui_ctx.tessellate(shapes, pixels_per_point)); + } } /// Paint the results of the last call to [`Self::logic`]. From 124bde4883bb7e4fbe40e28c12ef93a79e402504 Mon Sep 17 00:00:00 2001 From: RndUsr123 <150948884+RndUsr123@users.noreply.github.com> Date: Tue, 3 Mar 2026 07:46:45 +0000 Subject: [PATCH 11/58] Fixes the overly aggressive overflow elision in `truncate()` and similar for os scaling other than 100% (#7867) * Closes #7818 * [x] I have followed the instructions in the PR template --------- Co-authored-by: Emil Ernerfeldt --- crates/epaint/src/text/text_layout.rs | 40 ++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index 5b2400646..9aeeff137 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -495,7 +495,9 @@ fn replace_last_glyph_with_overflow_character( let replacement_glyph_width = font_face .as_mut() .and_then(|f| f.glyph_info(overflow_character)) - .map(|i| i.advance_width_unscaled.0 * font_face_metrics.px_scale_factor) + .map(|i| { + i.advance_width_unscaled.0 * font_face_metrics.px_scale_factor / pixels_per_point + }) .unwrap_or_default(); // Check if we're within width budget: @@ -1166,6 +1168,42 @@ mod tests { assert_eq!(row.rect().max.x, row.glyphs.last().unwrap().max_x()); } + #[test] + fn test_truncate_with_pixels_per_point() { + let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default()); + + for pixels_per_point in [ + 0.33, 0.5, 0.67, 1.0, 1.25, 1.33, 1.5, 1.75, 2.0, 3.0, 4.0, 5.0, + ] { + for ch in ['W', 'A', 'n', 't', 'i'] { + let target_width = 50.0; + let text = (0..20).map(|_| ch).collect::(); + + let mut job = LayoutJob::single_section(text, TextFormat::default()); + job.wrap.max_width = target_width; + job.wrap.max_rows = 1; + let elided_galley = layout(&mut fonts, pixels_per_point, job.into()); + assert!(elided_galley.elided); + + let test_galley = layout( + &mut fonts, + pixels_per_point, + Arc::new(LayoutJob::single_section( + (0..elided_galley.rows[0].char_count_excluding_newline()) + .map(|_| ch) + .chain(std::iter::once('…')) + .collect::(), + TextFormat::default(), + )), + ); + + assert!(elided_galley.size().x >= 0.0); + assert!(elided_galley.size().x <= target_width); + assert!(test_galley.size().x > target_width); + } + } + } + #[test] fn test_empty_row() { let pixels_per_point = 1.0; From 20f3cb52cc53730fd15f7c154a4ca373bbf68055 Mon Sep 17 00:00:00 2001 From: Dion Bramley Date: Tue, 3 Mar 2026 09:13:56 +0100 Subject: [PATCH 12/58] Make `Galley::pos_from_layout_cursor` `pub` (#7864) * [x] I have followed the instructions in the PR template This PR just exposes the pos_from_layout_cursor function as public. Hi, I'm trying to make a git gui with a merge editor, and for this I need a much more efficient and flexible text editor than the one currently in egui. So I'm working on building a more suitable one, which I intend to contribute back once it's working, but for now I would like to not need to fork the entirety of egui. By exposing this one function I (and others) can much more easily reuse Galleys. I suggest also exposing end_pos, but I'm not currently using that. Let me know if I should update this PR to do so. Thanks for the otherwise awesome tool :) --- crates/epaint/src/text/text_layout_types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 3e8a53d9e..b5bef62d5 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -934,7 +934,7 @@ impl Galley { } /// Returns a 0-width Rect. - fn pos_from_layout_cursor(&self, layout_cursor: &LayoutCursor) -> Rect { + pub fn pos_from_layout_cursor(&self, layout_cursor: &LayoutCursor) -> Rect { let Some(row) = self.rows.get(layout_cursor.row) else { return self.end_pos(); }; From a354c02e7672ea3b548395fbe4263917a807d677 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 3 Mar 2026 11:35:29 +0100 Subject: [PATCH 13/58] Add `Atom` prefix/suffix support to `DragValue` (#7949) --- crates/egui/src/atomics/atoms.rs | 12 +++ crates/egui/src/widgets/button.rs | 7 ++ crates/egui/src/widgets/drag_value.rs | 83 ++++++++++++------- .../tests/snapshots/demos/Code Example.png | 4 +- .../tests/snapshots/demos/Frame.png | 4 +- .../snapshots/demos/Tessellation Test.png | 4 +- .../tessellation_test/Additive rectangle.png | 4 +- .../tessellation_test/Blurred stroke.png | 4 +- .../snapshots/tessellation_test/Blurred.png | 4 +- .../tessellation_test/Minimal rounding.png | 4 +- .../snapshots/tessellation_test/Normal.png | 4 +- .../Thick stroke, minimal rounding.png | 4 +- .../tessellation_test/Thin filled.png | 4 +- .../tessellation_test/Thin stroked.png | 4 +- .../snapshots/widget_gallery_dark_x1.png | 4 +- .../snapshots/widget_gallery_dark_x2.png | 4 +- .../snapshots/widget_gallery_light_x1.png | 4 +- .../snapshots/widget_gallery_light_x2.png | 4 +- .../tests/snapshots/layout/drag_value.png | 4 +- .../tests/snapshots/visuals/drag_value.png | 4 +- tests/egui_tests/tests/test_widgets.rs | 11 ++- 21 files changed, 115 insertions(+), 66 deletions(-) diff --git a/crates/egui/src/atomics/atoms.rs b/crates/egui/src/atomics/atoms.rs index 1db7c63c6..2cb668cff 100644 --- a/crates/egui/src/atomics/atoms.rs +++ b/crates/egui/src/atomics/atoms.rs @@ -26,6 +26,18 @@ impl<'a> Atoms<'a> { self.0.insert(0, atom.into()); } + /// Insert atoms at the beginning of the list (left side). + pub fn extend_left(&mut self, atoms: impl IntoAtoms<'a>) { + let mut left = atoms.into_atoms(); + left.0.append(&mut self.0); + *self = left; + } + + /// Insert atoms at the end of the list (right side). + pub fn extend_right(&mut self, atoms: impl IntoAtoms<'a>) { + self.0.append(&mut atoms.into_atoms().0); + } + /// Concatenate and return the text contents. // TODO(lucasmerlin): It might not always make sense to return the concatenated text, e.g. // in a submenu button there is a right text '⏡' which is now passed to the screen reader. diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 4b4c2fe32..7ad155f16 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -259,6 +259,13 @@ impl<'a> Button<'a> { self } + /// Set the gap between atoms. + #[inline] + pub fn gap(mut self, gap: f32) -> Self { + self.layout = self.layout.gap(gap); + self + } + /// Show the button and return a [`AtomLayoutResponse`] for painting custom contents. pub fn atom_ui(self, ui: &mut Ui) -> AtomLayoutResponse { let Button { diff --git a/crates/egui/src/widgets/drag_value.rs b/crates/egui/src/widgets/drag_value.rs index 7841fee61..d12020097 100644 --- a/crates/egui/src/widgets/drag_value.rs +++ b/crates/egui/src/widgets/drag_value.rs @@ -1,11 +1,10 @@ -#![expect(clippy::needless_pass_by_value)] // False positives with `impl ToString` - -use std::{cmp::Ordering, ops::RangeInclusive}; - use crate::{ - Button, CursorIcon, Id, Key, MINUS_CHAR_STR, Modifiers, NumExt as _, Response, RichText, Sense, - TextEdit, TextWrapMode, Ui, Widget, WidgetInfo, emath, text, + Atom, AtomExt as _, AtomKind, Atoms, Button, CursorIcon, Id, IntoAtoms, Key, MINUS_CHAR_STR, + Modifiers, NumExt as _, Response, RichText, Sense, TextEdit, TextWrapMode, Ui, Widget, + WidgetInfo, emath, text, }; +use emath::Vec2; +use std::{cmp::Ordering, ops::RangeInclusive}; // ---------------------------------------------------------------------------- @@ -38,8 +37,7 @@ fn set(get_set_value: &mut GetSetValue<'_>, value: f64) { pub struct DragValue<'a> { get_set_value: GetSetValue<'a>, speed: f64, - prefix: String, - suffix: String, + atoms: Atoms<'a>, range: RangeInclusive, clamp_existing_to_range: bool, min_decimals: usize, @@ -50,6 +48,8 @@ pub struct DragValue<'a> { } impl<'a> DragValue<'a> { + const ATOM_ID: &'static str = "drag_item"; + pub fn new(value: &'a mut Num) -> Self { let slf = Self::from_get_set(move |v: Option| { if let Some(v) = v { @@ -66,11 +66,12 @@ impl<'a> DragValue<'a> { } pub fn from_get_set(get_set_value: impl 'a + FnMut(Option) -> f64) -> Self { + let atoms = Atoms::new(Atom::custom(Id::new(Self::ATOM_ID), Vec2::ZERO).atom_grow(true)); + Self { get_set_value: Box::new(get_set_value), speed: 1.0, - prefix: Default::default(), - suffix: Default::default(), + atoms, range: f64::NEG_INFINITY..=f64::INFINITY, clamp_existing_to_range: true, min_decimals: 0, @@ -164,15 +165,15 @@ impl<'a> DragValue<'a> { /// Show a prefix before the number, e.g. "x: " #[inline] - pub fn prefix(mut self, prefix: impl ToString) -> Self { - self.prefix = prefix.to_string(); + pub fn prefix(mut self, prefix: impl IntoAtoms<'a>) -> Self { + self.atoms.extend_left(prefix); self } /// Add a suffix to the number, this can be e.g. a unit ("Β°" or " m") #[inline] - pub fn suffix(mut self, suffix: impl ToString) -> Self { - self.suffix = suffix.to_string(); + pub fn suffix(mut self, suffix: impl IntoAtoms<'a>) -> Self { + self.atoms.extend_right(suffix); self } @@ -433,8 +434,7 @@ impl Widget for DragValue<'_> { speed, range, clamp_existing_to_range, - prefix, - suffix, + mut atoms, min_decimals, max_decimals, custom_formatter, @@ -442,6 +442,26 @@ impl Widget for DragValue<'_> { update_while_editing, } = self; + let mut prefix_text = String::new(); + let mut suffix_text = String::new(); + let mut past_value = false; + let atom_id = Id::new(Self::ATOM_ID); + for atom in atoms.iter() { + match &atom.kind { + AtomKind::Custom(id) if *id == atom_id => { + past_value = true; + } + AtomKind::Text(text) => { + if past_value { + suffix_text.push_str(text.text()); + } else { + prefix_text.push_str(text.text()); + } + } + _ => {} + } + } + let shift = ui.input(|i| i.modifiers.shift_only()); // The widget has the same ID whether it's in edit or button mode. let id = ui.next_auto_id(); @@ -543,8 +563,6 @@ impl Widget for DragValue<'_> { } } - // some clones below are redundant if AccessKit is disabled - #[expect(clippy::redundant_clone)] let mut response = if is_kb_editing { let mut value_text = ui .data_mut(|data| data.remove_temp::(id)) @@ -586,13 +604,22 @@ impl Widget for DragValue<'_> { ui.data_mut(|data| data.insert_temp(id, value_text)); response } else { - let button = Button::new( - RichText::new(format!("{}{}{}", prefix, value_text.clone(), suffix)) - .text_style(text_style), - ) - .wrap_mode(TextWrapMode::Extend) - .sense(Sense::click_and_drag()) - .min_size(ui.spacing().interact_size); // TODO(emilk): find some more generic solution to `min_size` + atoms.map_atoms(|atom| { + if let AtomKind::Custom(id) = atom.kind + && id == atom_id + { + RichText::new(value_text.clone()) + .text_style(text_style.clone()) + .into() + } else { + atom + } + }); + let button = Button::new(atoms) + .wrap_mode(TextWrapMode::Extend) + .sense(Sense::click_and_drag()) + .gap(0.0) + .min_size(ui.spacing().interact_size); // TODO(emilk): find some more generic solution to `min_size` let cursor_icon = if value <= *range.start() { CursorIcon::ResizeEast @@ -607,10 +634,8 @@ impl Widget for DragValue<'_> { if ui.style().explanation_tooltips { response = response.on_hover_text(format!( - "{}{}{}\nDrag to edit or click to enter a value.\nPress 'Shift' while dragging for better control.", - prefix, + "{}\nDrag to edit or click to enter a value.\nPress 'Shift' while dragging for better control.", value as f32, // Show full precision value on-hover. TODO(emilk): figure out f64 vs f32 - suffix )); } @@ -704,7 +729,7 @@ impl Widget for DragValue<'_> { // The value is exposed as a string by the text edit widget // when in edit mode. if !is_kb_editing { - let value_text = format!("{prefix}{value_text}{suffix}"); + let value_text = format!("{prefix_text}{value_text}{suffix_text}"); builder.set_value(value_text); } }); diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png b/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png index 5d4cc4d49..742c62272 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9302478abb0b86fae1af3af45d91f032272a56a2098405525d08aba4f9534644 -size 76103 +oid sha256:9d6ba2c4825517b4cc030b7639771d06913da86c2d52fd40e6263692335afa04 +size 76079 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Frame.png b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png index 6efc3f4a9..9ce775f7e 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Frame.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f5e67baf0696792e50f7ab3121874d055ddee2de0514712aacbf8e135ec4743d -size 25425 +oid sha256:a169cda21797152fb8aa69928ad3f4cef1b45cc5f213e5bfa01b8fe7723a4852 +size 25391 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Tessellation Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Tessellation Test.png index 99ab541d4..9d9d26c7c 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Tessellation Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Tessellation Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7336c53885add09360df098b6b131323e8ad3ef0ec2b85bf022e78bc4269276a -size 70255 +oid sha256:01b34a7371dd8b3539ed20594a42f3cd9792d391d2cb44740aa5ef301c9652f3 +size 70244 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png index 0eb5ebd6a..5c6b867d0 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a2b7b54a1af0f5cd31bd64f0506e3035dd423314ce3389e61730fa160434fbf3 -size 45074 +oid sha256:0574527ce659559f5b0709d84903afcec60b4ffa6b7979e8985027d326fd782a +size 45066 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png index bd9942eb7..64c5f12e1 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b66a0be67ff2d684a54c2321123521b3ad06dfe5ebffd50e89260d77efcfcc4 -size 86833 +oid sha256:50eca0feefe5d43db74f5e3bc08abda13c5986710cc4aaa03e9382af56264fc2 +size 86826 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png index 64ddf5c49..e0dcebac3 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:19320291c99a23429b114a59de4636689e281e1e68766abe2aa1e56562128e50 -size 118919 +oid sha256:91110d6a1da995e3e215cc92fdcceac84335e60b5b2fdbb2f16d5ecc6065fe55 +size 118912 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png index 105bbf285..1a69351b8 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5edf089c00715f1456fe7838e85aadcfc42b6216a3fd95b48d9c21fc8d700cba -size 51371 +oid sha256:31464d796f1660bdd5c98cf73186d7b68918fd42f292bc03e274e43d995edc16 +size 51363 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png index 035eb931d..5c3621856 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6cd1a10639dcb323bdc3b2c43e0c35665184fc809731ced90088ee9edb9de845 -size 54577 +oid sha256:9dca12eb3b99976db20c77a6c540cda450e53f6ded89708d2e2320194723c0e2 +size 54569 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png index 26014a12c..c8c5c6899 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:87e34024f701dc93f4026213ac7eb468a2cd6d3393eb0dbec382bf58007f8e61 -size 55042 +oid sha256:05a2778e7da867ab46a6760ea3925a2398c6b9a21d2767aef11cf98ef5292c82 +size 55034 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png index dcbbba2b6..8bfc2f47a 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7940ff56796efb27bec66b632ff33aa2ad390c4962a711bf520aee341f035a4 -size 35968 +oid sha256:686e37635da6ba218c9539f8b145239bbed2bab6696384ed1cb725db657ec642 +size 35961 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png index 0a3d062af..274e577d5 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b7bbd16c8aad444f0d11aacf87cf2292d494cc80a1ca46e7e8db86ca3041d35a -size 35931 +oid sha256:151171625b9cb8eaac3fb83260c6cc76cbf66003d9a940be1d5021a3303956c9 +size 35923 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png index 112605454..408cba0b8 100644 --- a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bbdc4199dee2ae853b8a240cd84528482dc6762233bd0d1249f2daa296b49487 -size 64172 +oid sha256:79d2935aa8f0f6941167b69840142599a2994a5eaa239757d91847d4d6533174 +size 64165 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png index 1b5b60c8a..9aa9c130a 100644 --- a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f6d38b6b47839d0e4eae530d203c83971fba8a41c9caa3d5b5d89ee7ed582613 -size 150090 +oid sha256:a5665e3a715ca7576df5b63af14b871f355d3e1db801c20089a60640373388ff +size 150095 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png index 5a2b44feb..ef68941bc 100644 --- a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c0635f1564d6c9707efa68003fb8c9b6eb00408aa8f24c972e33c6c79fed5bdf -size 59354 +oid sha256:bd31bddf25cf93d3ae79ce9b314cf3a3ebbf8c3b6cae2027f3f3b1593ac293e5 +size 59346 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png index 81c7452e6..67d8979ff 100644 --- a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4288ee4a0d2229d59c31538179cdda50035a3849f69b400127e1618efe30cdc1 -size 145224 +oid sha256:04dd62767ae9c18b5e89cea5bdd243b66c5986bfdb71fb9b01772ab9d150ab7e +size 145223 diff --git a/tests/egui_tests/tests/snapshots/layout/drag_value.png b/tests/egui_tests/tests/snapshots/layout/drag_value.png index 44bf0bfcb..86dfef652 100644 --- a/tests/egui_tests/tests/snapshots/layout/drag_value.png +++ b/tests/egui_tests/tests/snapshots/layout/drag_value.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b2cd4d27748e193d4f46ad7a5be6ff411ad3152b4fd546c0dc98dd3bb5333d93 -size 236090 +oid sha256:96c78de8d82a5cb4e91912823b88bc0465bf67f09b500e5bde8f43b001f35a66 +size 264421 diff --git a/tests/egui_tests/tests/snapshots/visuals/drag_value.png b/tests/egui_tests/tests/snapshots/visuals/drag_value.png index 2b04df5a5..463c4cbef 100644 --- a/tests/egui_tests/tests/snapshots/visuals/drag_value.png +++ b/tests/egui_tests/tests/snapshots/visuals/drag_value.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d42e002c3fd34f96d58ddfd4d2f91cf1ac7755ff71b5da315be4bee6bf00e03 -size 8411 +oid sha256:0f6babaa4f9359517f58b1160a915069c56c338b7c0d8d4306cde67628442397 +size 8995 diff --git a/tests/egui_tests/tests/test_widgets.rs b/tests/egui_tests/tests/test_widgets.rs index a53ad4a0c..5ef98c8a8 100644 --- a/tests/egui_tests/tests/test_widgets.rs +++ b/tests/egui_tests/tests/test_widgets.rs @@ -4,8 +4,9 @@ use egui::accesskit::Role; use egui::load::SizedTexture; use egui::{ Align, AtomExt as _, AtomLayout, Button, Color32, ColorImage, Direction, DragValue, Event, - Grid, IntoAtoms as _, Layout, PointerButton, Response, Slider, Stroke, StrokeKind, TextEdit, - TextWrapMode, TextureHandle, TextureOptions, Ui, UiBuilder, Vec2, Widget as _, include_image, + Grid, IntoAtoms as _, Layout, PointerButton, Response, RichText, Slider, Stroke, StrokeKind, + TextEdit, TextWrapMode, TextureHandle, TextureOptions, Ui, UiBuilder, Vec2, Widget as _, + include_image, }; use egui_kittest::kittest::{Queryable as _, by}; use egui_kittest::{Harness, Node, SnapshotResult, SnapshotResults}; @@ -74,7 +75,11 @@ fn widget_tests() { test_widget( "drag_value", - |ui| DragValue::new(&mut 12.0).ui(ui), + |ui| { + DragValue::new(&mut 12.0) + .suffix(RichText::new(" px").weak().small()) + .ui(ui) + }, &mut results, ); From b73367976017c7868946ae330c0e30cbb9d5a56c Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 3 Mar 2026 13:27:56 +0100 Subject: [PATCH 14/58] Fix text color when selecting newline character (#7951) * Closes https://github.com/emilk/egui/issues/7865 --- crates/egui/src/text_selection/visuals.rs | 4 +-- crates/egui_demo_lib/tests/misc.rs | 34 +++++++++++-------- .../tests/snapshots/text_selection.png | 3 -- .../tests/snapshots/text_selection_0.png | 3 ++ .../tests/snapshots/text_selection_1.png | 3 ++ 5 files changed, 27 insertions(+), 20 deletions(-) delete mode 100644 crates/egui_demo_lib/tests/snapshots/text_selection.png create mode 100644 crates/egui_demo_lib/tests/snapshots/text_selection_0.png create mode 100644 crates/egui_demo_lib/tests/snapshots/text_selection_1.png diff --git a/crates/egui/src/text_selection/visuals.rs b/crates/egui/src/text_selection/visuals.rs index fead390fe..e41d7a436 100644 --- a/crates/egui/src/text_selection/visuals.rs +++ b/crates/egui/src/text_selection/visuals.rs @@ -67,9 +67,7 @@ pub fn paint_text_selection( let first_vertex_index = row .glyphs .get(first_glyph_index) - .map_or(row.visuals.glyph_vertex_range.start, |g| { - g.first_vertex as _ - }); + .map_or(row.visuals.glyph_vertex_range.end, |g| g.first_vertex as _); let last_vertex_index = row .glyphs .get(last_glyph_index) diff --git a/crates/egui_demo_lib/tests/misc.rs b/crates/egui_demo_lib/tests/misc.rs index 8abc69d19..d5f6a3a3c 100644 --- a/crates/egui_demo_lib/tests/misc.rs +++ b/crates/egui_demo_lib/tests/misc.rs @@ -59,21 +59,27 @@ fn test_italics() { #[test] fn test_text_selection() { - let mut harness = Harness::builder().build_ui(|ui| { - let visuals = ui.visuals_mut(); - visuals.selection.bg_fill = Color32::LIGHT_GREEN; - visuals.selection.stroke.color = Color32::DARK_BLUE; + let mut results = egui_kittest::SnapshotResults::new(); - ui.label("Some varied ☺ text :)\nAnd it has a second line!"); - }); - harness.run(); - harness.fit_contents(); + for (test_idx, drag_start_x) in [0.2_f32, 0.9].into_iter().enumerate() { + let mut harness = Harness::builder().build_ui(|ui| { + let visuals = ui.visuals_mut(); + visuals.selection.bg_fill = Color32::LIGHT_GREEN; + visuals.selection.stroke.color = Color32::RED; - // Drag to select text: - let label = harness.get_by_role(Role::Label); - harness.drag_at(label.rect().lerp_inside([0.2, 0.25])); - harness.drop_at(label.rect().lerp_inside([0.6, 0.75])); - harness.run(); + ui.label("Some varied ☺ text :)\nAnd it has a second line!"); + }); + harness.run(); + harness.fit_contents(); - harness.snapshot("text_selection"); + // Drag to select text: + let label = harness.get_by_role(Role::Label); + harness.drag_at(label.rect().lerp_inside([drag_start_x, 0.25])); + harness.drop_at(label.rect().lerp_inside([0.6, 0.75])); + harness.run(); + + harness.snapshot(format!("text_selection_{test_idx}")); + + results.extend_harness(&mut harness); + } } diff --git a/crates/egui_demo_lib/tests/snapshots/text_selection.png b/crates/egui_demo_lib/tests/snapshots/text_selection.png deleted file mode 100644 index 63a4423a3..000000000 --- a/crates/egui_demo_lib/tests/snapshots/text_selection.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0475c5ac04ab8f79b79d43cfdb985f05b61dbe90e81f898a6dc216c308a28841 -size 4707 diff --git a/crates/egui_demo_lib/tests/snapshots/text_selection_0.png b/crates/egui_demo_lib/tests/snapshots/text_selection_0.png new file mode 100644 index 000000000..7930dff48 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/text_selection_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:344d90928510855dc718a2e36e31a97f084f1163ab750d0217fb8620469b621a +size 5276 diff --git a/crates/egui_demo_lib/tests/snapshots/text_selection_1.png b/crates/egui_demo_lib/tests/snapshots/text_selection_1.png new file mode 100644 index 000000000..8691211cb --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/text_selection_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60449af267336663304e44e254d0984e037bebfa2d1efdf32234cab4374e8c79 +size 5301 From ee3e73bdf9b6d658ef7734a6d07e3bc8237bc885 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 3 Mar 2026 17:02:30 +0100 Subject: [PATCH 15/58] Fix: repaint on drag-and-drop files (#7953) When someone drag-and-drops files onto an egui/eframe app, it makes sense to wake it up --- crates/egui/src/input_state/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/egui/src/input_state/mod.rs b/crates/egui/src/input_state/mod.rs index 37faf64c2..7122af699 100644 --- a/crates/egui/src/input_state/mod.rs +++ b/crates/egui/src/input_state/mod.rs @@ -661,6 +661,8 @@ impl InputState { if self.pointer.wants_repaint() || self.wheel.unprocessed_wheel_delta.abs().max_elem() > 0.2 || !self.events.is_empty() + || !self.raw.hovered_files.is_empty() + || !self.raw.dropped_files.is_empty() { // Immediate repaint return Some(Duration::ZERO); From 699fc7e887107225b940f10b2811861b052e27d9 Mon Sep 17 00:00:00 2001 From: valadaptive <79560998+valadaptive@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:58:42 -0500 Subject: [PATCH 16/58] Add font variations API (#7859) * Closes N/A * [x] I have followed the instructions in the PR template This was mostly from last month, but I never got around to submitting it. This PR adds font variation coordinates to the `TextFormat` struct, and uses them when rendering text. The coordinates are stored in a `SmallVec`; I've chosen to store up to 2 inline, which makes it take up 24 bytes (the minimum possible for a `SmallVec`). The variation axis tags are stored as the `font_types::Tag` type, which I've chosen to re-export from `epaint::text`. The variation coordinates are resolved to a `skrifa::Location` during font rendering/scaling, and are cached in the same way as all the other scaled metrics. I've renamed the `ScaledMetrics` struct to `StyledMetrics`, since it now also contains the resolved variation coordinates. I haven't benchmarked the performance of text layout with variation coordinates, but the existing text layout performance is unchanged. I've replaced the API for manually overriding a font's weight (https://github.com/emilk/egui/pull/7790) with an API for manually overriding any variation coordinates via `FontTweak`. This should support the same use case as #7790 while being substantially more flexible. I have *not* yet added any higher-level API for mapping style attributes (weight, width, slant, etc) to variation coordinates or to different font faces within a single family. That's a pretty huge can of worms, and it'd involve rethinking the split between `FontId` and `TextFormat` (and whether `FontId` is so big that we should provide a way to reuse it). This API is intentionally pretty low-level for now. Likewise, I've intentionally not used variation coordinates when computing a font's row height. I can't think of any fonts that change their vertical metrics depending on variation axes, so this should be fine for now. --------- Co-authored-by: Emil Ernerfeldt --- .typos.toml | 1 + Cargo.lock | 6 ++ Cargo.toml | 1 + crates/egui/src/context.rs | 2 +- crates/egui/src/style.rs | 53 +++++++++- crates/egui/src/widget_text.rs | 23 +++- crates/epaint/Cargo.toml | 4 +- crates/epaint/src/text/font.rs | 92 +++++++--------- crates/epaint/src/text/fonts.rs | 57 +++------- crates/epaint/src/text/text_layout.rs | 19 ++-- crates/epaint/src/text/text_layout_types.rs | 110 +++++++++++++++++++- 11 files changed, 258 insertions(+), 110 deletions(-) diff --git a/.typos.toml b/.typos.toml index 16659f4c7..6c5f1564a 100644 --- a/.typos.toml +++ b/.typos.toml @@ -9,6 +9,7 @@ isse = "isse" # part of @IsseW username tye = "tye" # part of @tye-exe username ro = "ro" # read-only, also part of the username @Phen-Ro typ = "typ" # Often used because `type` is a keyword in Rust +wdth = "wdth" # The `wdth` tag is used in variable fonts # I mistype these so often tesalator = "tessellator" diff --git a/Cargo.lock b/Cargo.lock index eb9a385b0..62fd9b36c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1567,6 +1567,7 @@ dependencies = [ "ecolor", "emath", "epaint_default_fonts", + "font-types", "log", "mimalloc", "nohash-hasher", @@ -1577,6 +1578,7 @@ dependencies = [ "serde", "similar-asserts", "skrifa", + "smallvec", "vello_cpu", ] @@ -1741,6 +1743,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1e4d2d0cf79d38430cc9dc9aadec84774bff2e1ba30ae2bf6c16cfce9385a23" dependencies = [ "bytemuck", + "serde", ] [[package]] @@ -4136,6 +4139,9 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] [[package]] name = "smithay-client-toolkit" diff --git a/Cargo.toml b/Cargo.toml index aaf9b9398..b10a4fd44 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,6 +90,7 @@ document-features = "0.2.11" ehttp = { version = "0.6.0", default-features = false } enum-map = "2.7.3" env_logger = { version = "0.11.8", default-features = false } +font-types = { version = "0.11.0", default-features = false, features = ["std"] } glow = "0.16.0" glutin = { version = "0.32.3", default-features = false } glutin-winit = { version = "0.5.0", default-features = false } diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 2665d5edd..55e3d75e1 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -3262,7 +3262,7 @@ impl Context { for (name, data) in &mut font_definitions.font_data { ui.collapsing(name, |ui| { - let mut tweak = data.tweak; + let mut tweak = data.tweak.clone(); if tweak.ui(ui).changed() { Arc::make_mut(data).tweak = tweak; changed = true; diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index a555b9ace..56a347f0d 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -1,7 +1,11 @@ //! egui theme (spacing, colors, etc). use emath::Align; -use epaint::{AlphaFromCoverage, CornerRadius, Shadow, Stroke, TextOptions, text::FontTweak}; +use epaint::{ + AlphaFromCoverage, CornerRadius, Shadow, Stroke, TextOptions, + mutex::Mutex, + text::{FontTweak, Tag}, +}; use std::{collections::BTreeMap, ops::RangeInclusive, sync::Arc}; use crate::{ @@ -2837,7 +2841,7 @@ impl Widget for &mut crate::Frame { impl Widget for &mut FontTweak { fn ui(self, ui: &mut Ui) -> Response { - let original: FontTweak = *self; + let original: FontTweak = self.clone(); let mut response = Grid::new("font_tweak") .num_columns(2) @@ -2847,6 +2851,7 @@ impl Widget for &mut FontTweak { y_offset_factor, y_offset, hinting_override, + coords, } = self; ui.label("Scale"); @@ -2874,6 +2879,50 @@ impl Widget for &mut FontTweak { ui.selectable_value(hinting_override, Some(true), "Enable"); ui.selectable_value(hinting_override, Some(false), "Disable"); }); + ui.end_row(); + + ui.label("coords"); + ui.end_row(); + let mut to_remove = None; + for (i, (tag, value)) in coords.as_mut().iter_mut().enumerate() { + let tag_text = ui.ctx().data_mut(|data| { + let tag = *tag; + Arc::clone(data.get_temp_mut_or_insert_with(ui.id().with(i), move || { + Arc::new(Mutex::new(tag.to_string())) + })) + }); + + let tag_text = &mut *tag_text.lock(); + let response = ui.text_edit_singleline(tag_text); + if response.changed() + && let Ok(new_tag) = Tag::new_checked(tag_text.as_bytes()) + { + *tag = new_tag; + } + // Reset stale text when not actively editing + // (e.g. after an item was removed and indices shifted) + if !response.has_focus() + && Tag::new_checked(tag_text.as_bytes()).ok() != Some(*tag) + { + *tag_text = tag.to_string(); + } + + ui.add(DragValue::new(value)); + if ui.small_button("πŸ—‘").clicked() { + to_remove = Some(i); + } + ui.end_row(); + } + if let Some(i) = to_remove { + coords.remove(i); + } + if ui.button("Add coord").clicked() { + coords.push(b"wght", 0.0); + } + if ui.button("Clear coords").clicked() { + coords.clear(); + } + ui.end_row(); if ui.button("Reset").clicked() { *self = Default::default(); diff --git a/crates/egui/src/widget_text.rs b/crates/egui/src/widget_text.rs index 5d91f4bc1..8bc88344b 100644 --- a/crates/egui/src/widget_text.rs +++ b/crates/egui/src/widget_text.rs @@ -1,5 +1,5 @@ use emath::GuiRounding as _; -use epaint::text::TextFormat; +use epaint::text::{IntoTag, TextFormat, VariationCoords}; use std::fmt::Formatter; use std::{borrow::Cow, sync::Arc}; @@ -34,6 +34,7 @@ pub struct RichText { background_color: Color32, expand_bg: f32, text_color: Option, + coords: VariationCoords, code: bool, strong: bool, weak: bool, @@ -55,6 +56,7 @@ impl Default for RichText { background_color: Default::default(), expand_bg: 1.0, text_color: Default::default(), + coords: Default::default(), code: Default::default(), strong: Default::default(), weak: Default::default(), @@ -196,6 +198,23 @@ impl RichText { self } + /// Add a variation coordinate. + #[inline] + pub fn variation(mut self, tag: impl IntoTag, coord: f32) -> Self { + self.coords.push(tag, coord); + self + } + + /// Override the variation coordinates completely. + #[inline] + pub fn variations( + mut self, + variations: impl IntoIterator, + ) -> Self { + self.coords = VariationCoords::new(variations); + self + } + /// Override the [`TextStyle`]. #[inline] pub fn text_style(mut self, text_style: TextStyle) -> Self { @@ -391,6 +410,7 @@ impl RichText { background_color, expand_bg, text_color: _, // already used by `get_text_color` + coords, code, strong: _, // already used by `get_text_color` weak: _, // already used by `get_text_color` @@ -449,6 +469,7 @@ impl RichText { line_height, color: text_color, background: background_color, + coords, italics, underline, strikethrough, diff --git a/crates/epaint/Cargo.toml b/crates/epaint/Cargo.toml index 77facdb3f..c8a05e4d7 100644 --- a/crates/epaint/Cargo.toml +++ b/crates/epaint/Cargo.toml @@ -48,7 +48,7 @@ mint = ["emath/mint"] rayon = ["dep:rayon"] ## Allow serialization using [`serde`](https://docs.rs/serde). -serde = ["dep:serde", "ahash/serde", "emath/serde", "ecolor/serde"] +serde = ["dep:serde", "ahash/serde", "emath/serde", "ecolor/serde", "font-types/serde", "smallvec/serde"] ## Change Vertex layout to be compatible with unity unity = [] @@ -62,12 +62,14 @@ emath.workspace = true ecolor.workspace = true ahash.workspace = true +font-types.workspace = true log.workspace = true nohash-hasher.workspace = true parking_lot.workspace = true # Using parking_lot over std::sync::Mutex gives 50% speedups in some real-world scenarios. profiling.workspace = true self_cell.workspace = true skrifa.workspace = true +smallvec.workspace = true vello_cpu.workspace = true #! ### Optional dependencies diff --git a/crates/epaint/src/text/font.rs b/crates/epaint/src/text/font.rs index 150aca34a..61f9f9f2f 100644 --- a/crates/epaint/src/text/font.rs +++ b/crates/epaint/src/text/font.rs @@ -12,7 +12,7 @@ use vello_cpu::{color, kurbo}; use crate::{ TextOptions, TextureAtlas, text::{ - FontTweak, + FontTweak, VariationCoords, fonts::{Blob, CachedFamily, FontFaceKey}, }, }; @@ -145,8 +145,8 @@ struct GlyphCacheKey(u64); impl nohash_hasher::IsEnabled for GlyphCacheKey {} impl GlyphCacheKey { - fn new(glyph_id: skrifa::GlyphId, metrics: &ScaledMetrics, bin: SubpixelBin) -> Self { - let ScaledMetrics { + fn new(glyph_id: skrifa::GlyphId, metrics: &StyledMetrics, bin: SubpixelBin) -> Self { + let StyledMetrics { pixels_per_point, px_scale_factor, .. @@ -197,10 +197,10 @@ impl FontCell { fn allocate_glyph_uncached( &mut self, atlas: &mut TextureAtlas, - metrics: &ScaledMetrics, + metrics: &StyledMetrics, glyph_info: &GlyphInfo, bin: SubpixelBin, - location: &skrifa::instance::Location, + location: skrifa::instance::LocationRef<'_>, ) -> Option { let glyph_id = glyph_info.id?; @@ -337,8 +337,6 @@ pub struct FontFace { font: FontCell, tweak: FontTweak, - /// Variable font location (for weight axis, etc.) - location: skrifa::instance::Location, glyph_info_cache: ahash::HashMap, glyph_alloc_cache: ahash::HashMap, } @@ -350,7 +348,6 @@ impl FontFace { font_data: Blob, index: u32, tweak: FontTweak, - preferred_weight: Option, ) -> Result> { let font = FontCell::try_new(font_data, |font_data| { let skrifa_font = @@ -396,44 +393,10 @@ impl FontFace { }) })?; - // Use preferred_weight if provided, otherwise try to read from the OS/2 table or fvar default - let weight = preferred_weight.or_else(|| { - // First try OS/2 table - if let Some(w) = font - .borrow_dependent() - .skrifa - .os2() - .ok() - .map(|os2| os2.us_weight_class()) - { - return Some(w); - } - // If no OS/2 or preferred_weight, try to get default from variable font's fvar table - font.borrow_dependent() - .skrifa - .axes() - .iter() - .find(|axis| axis.tag() == skrifa::raw::types::Tag::new(b"wght")) - .map(|axis| axis.default_value() as u16) - }); - - // Create location for variable font with weight axis - // If weight is provided (either from preferred_weight, OS/2, or fvar default), use it - // Otherwise fall back to Location::default() which uses all axis defaults - let location = if let Some(w) = weight { - font.borrow_dependent() - .skrifa - .axes() - .location([("wght", w as f32)]) - } else { - skrifa::instance::Location::default() - }; - Ok(Self { name, font, tweak, - location, glyph_info_cache: Default::default(), glyph_alloc_cache: Default::default(), }) @@ -537,7 +500,7 @@ impl FontFace { #[inline] pub(super) fn pair_kerning_pixels( &self, - metrics: &ScaledMetrics, + metrics: &StyledMetrics, last_glyph_id: skrifa::GlyphId, glyph_id: skrifa::GlyphId, ) -> f32 { @@ -559,7 +522,7 @@ impl FontFace { #[inline] pub fn pair_kerning( &self, - metrics: &ScaledMetrics, + metrics: &StyledMetrics, last_glyph_id: skrifa::GlyphId, glyph_id: skrifa::GlyphId, ) -> f32 { @@ -567,7 +530,12 @@ impl FontFace { } #[inline(always)] - pub fn scaled_metrics(&self, pixels_per_point: f32, font_size: f32) -> ScaledMetrics { + pub fn styled_metrics( + &self, + pixels_per_point: f32, + font_size: f32, + coords: &VariationCoords, + ) -> StyledMetrics { let pt_scale_factor = self.font.px_scale_factor(font_size * self.tweak.scale); let font_data = self.font.borrow_dependent(); let ascent = (font_data.metrics.ascent * pt_scale_factor).round_ui(); @@ -581,20 +549,32 @@ impl FontFace { + self.tweak.y_offset) .round_ui(); - ScaledMetrics { + let axes = font_data.skrifa.axes(); + // Override the default coordinates with ones specified via FontTweak, then the ones specified directly via the + // argument (probably from TextFormat). + let settings = self + .tweak + .coords + .as_ref() + .iter() + .chain(coords.as_ref().iter()); + let location = axes.location(settings); + + StyledMetrics { pixels_per_point, px_scale_factor, scale, y_offset_in_points, ascent, row_height: ascent - descent + line_gap, + location, } } pub fn allocate_glyph( &mut self, atlas: &mut TextureAtlas, - metrics: &ScaledMetrics, + metrics: &StyledMetrics, glyph_info: GlyphInfo, chr: char, h_pos: f32, @@ -628,7 +608,7 @@ impl FontFace { let allocation = self .font - .allocate_glyph_uncached(atlas, metrics, &glyph_info, bin, &self.location) + .allocate_glyph_uncached(atlas, metrics, &glyph_info, bin, (&metrics.location).into()) .unwrap_or_default(); entry.insert(allocation); @@ -665,12 +645,17 @@ impl Font<'_> { }) } - pub fn scaled_metrics(&self, pixels_per_point: f32, font_size: f32) -> ScaledMetrics { + pub fn styled_metrics( + &self, + pixels_per_point: f32, + font_size: f32, + coords: &VariationCoords, + ) -> StyledMetrics { self.cached_family .fonts .first() .and_then(|key| self.fonts_by_id.get(key)) - .map(|font_face| font_face.scaled_metrics(pixels_per_point, font_size)) + .map(|font_face| font_face.styled_metrics(pixels_per_point, font_size, coords)) .unwrap_or_default() } @@ -713,8 +698,8 @@ impl Font<'_> { } /// Metrics for a font at a specific screen-space scale. -#[derive(Clone, Copy, Debug, PartialEq, Default)] -pub struct ScaledMetrics { +#[derive(Clone, Debug, PartialEq, Default)] +pub struct StyledMetrics { /// The DPI part of the screen-space scale. pub pixels_per_point: f32, @@ -738,6 +723,9 @@ pub struct ScaledMetrics { /// /// Returns a value rounded to [`emath::GUI_ROUNDING`]. pub row_height: f32, + + /// Resolved variation coordinates. + pub location: skrifa::instance::Location, } /// Code points that will always be invisible (zero width). diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 19876b571..5099e0085 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -10,7 +10,7 @@ use std::{ use crate::{ TextureAtlas, text::{ - Galley, LayoutJob, LayoutSection, TextOptions, + Galley, LayoutJob, LayoutSection, TextOptions, VariationCoords, font::{Font, FontFace, GlyphInfo}, }, }; @@ -125,12 +125,6 @@ pub struct FontData { /// Extra scale and vertical tweak to apply to all text of this font. pub tweak: FontTweak, - - /// The font weight (100-900), if available. - /// Standard values: 100 (Thin), 200 (Extra Light), 300 (Light), 400 (Regular), - /// 500 (Medium), 600 (Semi Bold), 700 (Bold), 800 (Extra Bold), 900 (Black). - /// `None` if the weight could not be determined. - pub weight: Option, } impl FontData { @@ -139,7 +133,6 @@ impl FontData { font: Cow::Borrowed(font), index: 0, tweak: Default::default(), - weight: None, } } @@ -148,43 +141,12 @@ impl FontData { font: Cow::Owned(font), index: 0, tweak: Default::default(), - weight: None, } } pub fn tweak(self, tweak: FontTweak) -> Self { Self { tweak, ..self } } - - /// Set the font weight (100-900). - /// - /// This is typically read automatically from the font file when loaded, - /// but can be overridden manually if needed. - /// - /// Standard weight values: - /// - 100: Thin - /// - 200: Extra Light - /// - 300: Light - /// - 400: Regular/Normal - /// - 500: Medium - /// - 600: Semi Bold - /// - 700: Bold - /// - 800: Extra Bold - /// - 900: Black - /// - /// # Example - /// ``` - /// # use epaint::text::FontData; - /// let font_data = FontData::from_static(include_bytes!("../../../epaint_default_fonts/fonts/Ubuntu-Light.ttf")) - /// .weight(300); // Override to Light weight - /// assert_eq!(font_data.weight, Some(300)); - /// ``` - pub fn weight(self, weight: u16) -> Self { - Self { - weight: Some(weight), - ..self - } - } } impl AsRef<[u8]> for FontData { @@ -196,7 +158,7 @@ impl AsRef<[u8]> for FontData { // ---------------------------------------------------------------------------- /// Extra scale and vertical tweak to apply to all text of a certain font. -#[derive(Copy, Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct FontTweak { /// Scale the font's glyphs by this much. @@ -228,6 +190,9 @@ pub struct FontTweak { /// /// `None` means use the global setting. pub hinting_override: Option, + + /// Override the font's default variation coordinates. + pub coords: VariationCoords, } impl Default for FontTweak { @@ -237,6 +202,7 @@ impl Default for FontTweak { y_offset_factor: 0.0, y_offset: 0.0, hinting_override: None, + coords: VariationCoords::default(), } } } @@ -701,7 +667,12 @@ impl FontsView<'_> { pub fn row_height(&mut self, font_id: &FontId) -> f32 { self.fonts .font(&font_id.family) - .scaled_metrics(self.pixels_per_point, font_id.size) + .styled_metrics( + self.pixels_per_point, + font_id.size, + // TODO(valadaptive): use font variation coords when calculating row height + &VariationCoords::default(), + ) .row_height } @@ -807,15 +778,13 @@ impl FontsImpl { let mut fonts_by_id: nohash_hasher::IntMap = Default::default(); let mut fonts_by_name: ahash::HashMap = Default::default(); for (name, font_data) in &definitions.font_data { - let tweak = font_data.tweak; let blob = blob_from_font_data(font_data); let font_face = FontFace::new( options, name.clone(), blob, font_data.index, - tweak, - font_data.weight, + font_data.tweak.clone(), ) .unwrap_or_else(|err| panic!("Error parsing {name:?} TTF/OTF font file: {err}")); let key = FontFaceKey::new(); diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index 9aeeff137..9b53e3301 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -8,7 +8,7 @@ use crate::{ Color32, Mesh, Stroke, Vertex, stroke::PathStroke, text::{ - font::{ScaledMetrics, is_cjk, is_cjk_break_allowed}, + font::{StyledMetrics, is_cjk, is_cjk_break_allowed}, fonts::FontFaceKey, }, }; @@ -160,7 +160,7 @@ fn layout_section( } = section; let mut font = fonts.font(&format.font_id.family); let font_size = format.font_id.size; - let font_metrics = font.scaled_metrics(pixels_per_point, font_size); + let font_metrics = font.styled_metrics(pixels_per_point, font_size, &format.coords); let line_height = section .format .line_height @@ -178,7 +178,7 @@ fn layout_section( // Optimization: only recompute `ScaledMetrics` when the concrete `FontImpl` changes. let mut current_font = FontFaceKey::INVALID; - let mut current_font_face_metrics = ScaledMetrics::default(); + let mut current_font_face_metrics = StyledMetrics::default(); for chr in job.text[byte_range.clone()].chars() { if job.break_on_newline && chr == '\n' { @@ -192,7 +192,9 @@ fn layout_section( current_font = font_id; current_font_face_metrics = font_face .as_ref() - .map(|font_face| font_face.scaled_metrics(pixels_per_point, font_size)) + .map(|font_face| { + font_face.styled_metrics(pixels_per_point, font_size, &format.coords) + }) .unwrap_or_default(); } @@ -468,7 +470,7 @@ fn replace_last_glyph_with_overflow_character( let mut font_face = font.fonts_by_id.get_mut(&font_id); let font_face_metrics = font_face .as_mut() - .map(|f| f.scaled_metrics(pixels_per_point, font_size)) + .map(|f| f.styled_metrics(pixels_per_point, font_size, §ion.format.coords)) .unwrap_or_default(); let overflow_glyph_x = if let Some(prev_glyph) = row.glyphs.last() { @@ -519,7 +521,8 @@ fn replace_last_glyph_with_overflow_character( }) .unwrap_or_default(); - let font_metrics = font.scaled_metrics(pixels_per_point, font_size); + let font_metrics = + font.styled_metrics(pixels_per_point, font_size, §ion.format.coords); let line_height = section .format .line_height @@ -1212,7 +1215,7 @@ mod tests { let font_id = FontId::default(); let font_height = fonts .font(&font_id.family) - .scaled_metrics(pixels_per_point, font_id.size) + .styled_metrics(pixels_per_point, font_id.size, &VariationCoords::default()) .row_height; let job = LayoutJob::simple(String::new(), font_id, Color32::WHITE, f32::INFINITY); @@ -1245,7 +1248,7 @@ mod tests { let font_id = FontId::default(); let font_height = fonts .font(&font_id.family) - .scaled_metrics(pixels_per_point, font_id.size) + .styled_metrics(pixels_per_point, font_id.size, &VariationCoords::default()) .row_height; let job = LayoutJob::simple("Hi!\n".to_owned(), font_id, Color32::WHITE, f32::INFINITY); diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index b5bef62d5..d887fb13a 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -1,5 +1,5 @@ -use std::ops::Range; use std::sync::Arc; +use std::{ops::Range, str::FromStr as _}; use super::{ cursor::{CCursor, LayoutCursor}, @@ -7,6 +7,8 @@ use super::{ }; use crate::{Color32, FontId, Mesh, Stroke, text::FontsView}; use emath::{Align, GuiRounding as _, NumExt as _, OrderedFloat, Pos2, Rect, Vec2, pos2, vec2}; +pub use font_types::Tag; +use smallvec::SmallVec; /// Describes the task of laying out text. /// @@ -257,6 +259,107 @@ impl std::hash::Hash for LayoutSection { // ---------------------------------------------------------------------------- +/// Helper trait for all types that can be parsed as a [`font_types::Tag`]. +pub trait IntoTag { + fn into_tag(self) -> font_types::Tag; +} + +impl IntoTag for font_types::Tag { + #[inline(always)] + fn into_tag(self) -> font_types::Tag { + self + } +} + +impl IntoTag for u32 { + #[inline(always)] + fn into_tag(self) -> font_types::Tag { + font_types::Tag::from_u32(self) + } +} + +impl IntoTag for [u8; 4] { + #[inline(always)] + fn into_tag(self) -> font_types::Tag { + font_types::Tag::new_checked(&self).expect("Invalid variation axis tag") + } +} + +impl IntoTag for &[u8; 4] { + #[inline(always)] + fn into_tag(self) -> font_types::Tag { + font_types::Tag::new_checked(self).expect("Invalid variation axis tag") + } +} + +impl IntoTag for &str { + #[inline(always)] + fn into_tag(self) -> font_types::Tag { + font_types::Tag::from_str(self).expect("Invalid variation axis tag") + } +} + +/// List of font variation coordinates by axis tag. If more than one coordinate for a given axis is provided, the last +/// one added is used. +#[derive(Clone, Debug, PartialEq, Default)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct VariationCoords(SmallVec<[(font_types::Tag, f32); 2]>); + +impl VariationCoords { + /// Create a list of variation coordinates from a sequence of (tag, value) pairs. + /// + /// ## Example: + /// ``` + /// use epaint::text::VariationCoords; + /// + /// let coords = VariationCoords::new([ + /// (b"wght", 500.0), + /// (b"wdth", 75.0), + /// ]); + /// ``` + pub fn new(values: impl IntoIterator) -> Self { + Self(values.into_iter().map(|(t, c)| (t.into_tag(), c)).collect()) + } + + /// Add a variation coordinate to the list. + #[inline(always)] + pub fn push(&mut self, tag: impl IntoTag, coord: f32) { + self.0.push((tag.into_tag(), coord)); + } + + /// Remove the coordinate at the given index. + pub fn remove(&mut self, index: usize) { + self.0.remove(index); + } + + pub fn clear(&mut self) { + self.0.clear(); + } +} + +impl AsRef<[(font_types::Tag, f32)]> for VariationCoords { + #[inline(always)] + fn as_ref(&self) -> &[(font_types::Tag, f32)] { + &self.0 + } +} + +impl AsMut<[(font_types::Tag, f32)]> for VariationCoords { + fn as_mut(&mut self) -> &mut [(font_types::Tag, f32)] { + &mut self.0 + } +} + +impl std::hash::Hash for VariationCoords { + fn hash(&self, state: &mut H) { + self.0.len().hash(state); + for (tag, coord) in &self.0 { + tag.hash(state); + OrderedFloat(*coord).hash(state); + } + } +} + /// Formatting option for a section of text. #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -287,6 +390,8 @@ pub struct TextFormat { /// Default: 1.0 pub expand_bg: f32, + pub coords: VariationCoords, + pub italics: bool, pub underline: Stroke, @@ -315,6 +420,7 @@ impl Default for TextFormat { color: Color32::GRAY, background: Color32::TRANSPARENT, expand_bg: 1.0, + coords: VariationCoords::default(), italics: false, underline: Stroke::NONE, strikethrough: Stroke::NONE, @@ -333,6 +439,7 @@ impl std::hash::Hash for TextFormat { color, background, expand_bg, + coords, italics, underline, strikethrough, @@ -346,6 +453,7 @@ impl std::hash::Hash for TextFormat { color.hash(state); background.hash(state); emath::OrderedFloat(*expand_bg).hash(state); + coords.hash(state); italics.hash(state); underline.hash(state); strikethrough.hash(state); From 76332582198bed9410278183f8b8d7b7e8a173f6 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 5 Mar 2026 10:51:12 +0100 Subject: [PATCH 17/58] Enforce 'suffix' for consistency --- .typos.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.typos.toml b/.typos.toml index 6c5f1564a..97c54d657 100644 --- a/.typos.toml +++ b/.typos.toml @@ -21,6 +21,8 @@ teselation = "tessellation" tessalation = "tessellation" tesselation = "tessellation" +# For consistency +postfix = "suffix" # Use the more common spelling adaptor = "adapter" From 9bc062c8eee17f154743a35145983f0e6f02b1c3 Mon Sep 17 00:00:00 2001 From: Lander Brandt Date: Thu, 5 Mar 2026 03:47:57 -0800 Subject: [PATCH 18/58] Fix wgpu memory leak leading to panic when window is minimized (#7434) (#7928) --- crates/egui-wgpu/src/winit.rs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/crates/egui-wgpu/src/winit.rs b/crates/egui-wgpu/src/winit.rs index 167d10c79..869c13259 100644 --- a/crates/egui-wgpu/src/winit.rs +++ b/crates/egui-wgpu/src/winit.rs @@ -421,12 +421,40 @@ impl Painter { ) -> f32 { profiling::function_scope!(); + /// Guard to ensure that commands are always submitted to the renderer queue + /// so that calls to [`write_buffer()`](https://docs.rs/wgpu/latest/wgpu/struct.Queue.html#method.write_buffer) + /// are completed even if we take a codepath which doesn't submit commands and avoids + /// internal buffers growing indefinitely. + /// + /// This may happen, for example, if no output frame is resolved. + /// See for full context. + struct RendererQueueGuard<'q> { + queue: &'q wgpu::Queue, + commands_submitted: bool, + } + + impl Drop for RendererQueueGuard<'_> { + fn drop(&mut self) { + // Only submit an empty command buffer array if no commands were + // explicitly submitted. + if !self.commands_submitted { + self.queue.submit([]); + } + } + } + let capture = !capture_data.is_empty(); let mut vsync_sec = 0.0; let Some(render_state) = self.render_state.as_mut() else { return vsync_sec; }; + + let mut render_queue_guard = RendererQueueGuard { + queue: &render_state.queue, + commands_submitted: false, + }; + let Some(surface_state) = self.surfaces.get(&viewport_id) else { return vsync_sec; }; @@ -590,6 +618,9 @@ impl Painter { vsync_sec += start.elapsed().as_secs_f32(); }; + // Ensure that the queue guard does not do unnecessary work when dropped + render_queue_guard.commands_submitted = true; + // Free textures marked for destruction **after** queue submit since they might still be used in the current frame. // Calling `wgpu::Texture::destroy` on a texture that is still in use would invalidate the command buffer(s) it is used in. // However, once we called `wgpu::Queue::submit`, it is up for wgpu to determine how long the underlying gpu resource has to live. From 8b90dc60c66bd59ba808b8c094453ab2088a94c0 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 10 Mar 2026 12:03:10 +0100 Subject: [PATCH 19/58] =?UTF-8?q?=E2=9A=A0=EF=B8=8F=20Atom=20improvements:?= =?UTF-8?q?=20`Atom::id`,=20`align`,=20`closure`,=20`max=5Fsize`=20(#7958)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migration guide: - `AtomKind::Custom` has been removed. You can now set an id to any kind via `Atom::custom` or `AtomExt::atom_id`. --- crates/egui/src/atomics/atom.rs | 59 +++++++++-- crates/egui/src/atomics/atom_ext.rs | 25 ++++- crates/egui/src/atomics/atom_kind.rs | 114 +++++++++++++++------ crates/egui/src/atomics/atom_layout.rs | 76 ++++++++++---- crates/egui/src/atomics/atoms.rs | 23 +++-- crates/egui/src/atomics/sized_atom.rs | 5 + crates/egui/src/atomics/sized_atom_kind.rs | 20 ++-- crates/egui/src/widgets/drag_value.rs | 27 ++--- 8 files changed, 256 insertions(+), 93 deletions(-) diff --git a/crates/egui/src/atomics/atom.rs b/crates/egui/src/atomics/atom.rs index 6425ac724..4db7f12a9 100644 --- a/crates/egui/src/atomics/atom.rs +++ b/crates/egui/src/atomics/atom.rs @@ -1,5 +1,5 @@ -use crate::{AtomKind, FontSelection, Id, SizedAtom, Ui}; -use emath::{NumExt as _, Vec2}; +use crate::{AtomKind, FontSelection, Id, IntoSizedArgs, IntoSizedResult, SizedAtom, Ui}; +use emath::{Align2, NumExt as _, Vec2}; use epaint::text::TextWrapMode; /// A low-level ui building block. @@ -14,6 +14,9 @@ use epaint::text::TextWrapMode; /// ``` #[derive(Clone, Debug)] pub struct Atom<'a> { + /// See [`crate::AtomExt::atom_id`] + pub id: Option, + /// See [`crate::AtomExt::atom_size`] pub size: Option, @@ -26,17 +29,22 @@ pub struct Atom<'a> { /// See [`crate::AtomExt::atom_shrink`] pub shrink: bool, - /// The atom type + /// See [`crate::AtomExt::atom_align`] + pub align: Align2, + + /// The atom type / content pub kind: AtomKind<'a>, } impl Default for Atom<'_> { fn default() -> Self { Atom { + id: None, size: None, max_size: Vec2::INFINITY, grow: false, shrink: false, + align: Align2::CENTER_CENTER, kind: AtomKind::Empty, } } @@ -54,11 +62,27 @@ impl<'a> Atom<'a> { } } - /// Create a [`AtomKind::Custom`] with a specific size. + /// Create an [`AtomKind::Empty`] with a specific size. + /// + /// Example: + /// ``` + /// # use egui::{AtomExt, AtomKind, Atom, Button, Id, __run_test_ui}; + /// # use emath::Vec2; + /// # __run_test_ui(|ui| { + /// let id = Id::new("my_button"); + /// let response = Button::new(("Hi!", Atom::custom(id, Vec2::splat(18.0)))).atom_ui(ui); + /// + /// let rect = response.rect(id); + /// if let Some(rect) = rect { + /// ui.place(rect, Button::new("⏡")); + /// } + /// # }); + /// ``` pub fn custom(id: Id, size: impl Into) -> Self { Atom { size: Some(size.into()), - kind: AtomKind::Custom(id), + kind: AtomKind::Empty, + id: Some(id), ..Default::default() } } @@ -82,19 +106,32 @@ impl<'a> Atom<'a> { wrap_mode = Some(TextWrapMode::Truncate); } - let (intrinsic, kind) = self - .kind - .into_sized(ui, available_size, wrap_mode, fallback_font); + let id = self.id; + + let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode()); + let IntoSizedResult { + intrinsic_size, + sized, + } = self.kind.into_sized( + ui, + IntoSizedArgs { + available_size, + wrap_mode, + fallback_font, + }, + ); let size = self .size - .map_or_else(|| kind.size(), |s| s.at_most(self.max_size)); + .map_or_else(|| sized.size(), |s| s.at_most(self.max_size)); SizedAtom { + id, size, - intrinsic_size: intrinsic.at_least(self.size.unwrap_or_default()), + intrinsic_size: intrinsic_size.at_least(self.size.unwrap_or_default()), grow: self.grow, - kind, + align: self.align, + kind: sized, } } } diff --git a/crates/egui/src/atomics/atom_ext.rs b/crates/egui/src/atomics/atom_ext.rs index 6d008b84b..bfe587fae 100644 --- a/crates/egui/src/atomics/atom_ext.rs +++ b/crates/egui/src/atomics/atom_ext.rs @@ -1,10 +1,16 @@ -use crate::{Atom, FontSelection, Ui}; +use crate::{Atom, FontSelection, Id, Ui}; use emath::Vec2; /// A trait for conveniently building [`Atom`]s. /// /// The functions are prefixed with `atom_` to avoid conflicts with e.g. [`crate::RichText::size`]. pub trait AtomExt<'a> { + /// Set the [`Id`] for custom rendering. + /// + /// You can get the [`crate::Rect`] with the [`Id`] from [`crate::AtomLayoutResponse`] and use a + /// [`crate::Painter`] or [`Ui::place`] to add/draw some custom content. + fn atom_id(self, id: Id) -> Atom<'a>; + /// Set the atom to a fixed size. /// /// If [`Atom::grow`] is `true`, this will be the minimum width. @@ -63,12 +69,23 @@ pub trait AtomExt<'a> { let height = ui.fonts_mut(|f| f.row_height(&font_id)); self.atom_max_height(height) } + + /// Sets the [`emath::Align2`] of a single atom within its available space. + /// + /// Defaults to center-center. + fn atom_align(self, align: emath::Align2) -> Atom<'a>; } impl<'a, T> AtomExt<'a> for T where T: Into> + Sized, { + fn atom_id(self, id: Id) -> Atom<'a> { + let mut atom = self.into(); + atom.id = Some(id); + atom + } + fn atom_size(self, size: Vec2) -> Atom<'a> { let mut atom = self.into(); atom.size = Some(size); @@ -104,4 +121,10 @@ where atom.max_size.y = max_height; atom } + + fn atom_align(self, align: emath::Align2) -> Atom<'a> { + let mut atom = self.into(); + atom.align = align; + atom + } } diff --git a/crates/egui/src/atomics/atom_kind.rs b/crates/egui/src/atomics/atom_kind.rs index 10ca3353b..ec2ab8f63 100644 --- a/crates/egui/src/atomics/atom_kind.rs +++ b/crates/egui/src/atomics/atom_kind.rs @@ -1,9 +1,28 @@ -use crate::{FontSelection, Id, Image, ImageSource, SizedAtomKind, Ui, WidgetText}; +use crate::{FontSelection, Image, ImageSource, SizedAtomKind, Ui, WidgetText}; use emath::Vec2; use epaint::text::TextWrapMode; +use std::fmt::Debug; + +/// Args passed when sizing an [`super::Atom`] +pub struct IntoSizedArgs { + pub available_size: Vec2, + pub wrap_mode: TextWrapMode, + pub fallback_font: FontSelection, +} + +/// Result returned when sizing an [`super::Atom`] +pub struct IntoSizedResult<'a> { + pub intrinsic_size: Vec2, + pub sized: SizedAtomKind<'a>, +} + +/// See [`AtomKind::Closure`] +// We need 'static in the result (or need to introduce another lifetime on the enum). +// Otherwise, a single 'static Atom would force the closure to be 'static. +pub type AtomClosure<'a> = Box IntoSizedResult<'static> + 'a>; /// The different kinds of [`crate::Atom`]s. -#[derive(Clone, Default, Debug)] +#[derive(Default)] pub enum AtomKind<'a> { /// Empty, that can be used with [`crate::AtomExt::atom_grow`] to reserve space. #[default] @@ -38,37 +57,57 @@ pub enum AtomKind<'a> { /// default font height, which is convenient for icons. Image(Image<'a>), - /// For custom rendering. + /// A custom closure that produces a sized atom. /// - /// You can get the [`crate::Rect`] with the [`Id`] from [`crate::AtomLayoutResponse`] and use a - /// [`crate::Painter`] or [`Ui::place`] to add/draw some custom content. + /// The vec2 passed in is the available size to this atom. The returned vec2 should be the + /// preferred / intrinsic size. /// - /// Example: - /// ``` - /// # use egui::{AtomExt, AtomKind, Atom, Button, Id, __run_test_ui}; - /// # use emath::Vec2; - /// # __run_test_ui(|ui| { - /// let id = Id::new("my_button"); - /// let response = Button::new(("Hi!", Atom::custom(id, Vec2::splat(18.0)))).atom_ui(ui); - /// - /// let rect = response.rect(id); - /// if let Some(rect) = rect { - /// ui.place(rect, Button::new("⏡")); - /// } - /// # }); - /// ``` - Custom(Id), + /// Note: This api is experimental, expect breaking changes here. + /// When cloning, this will be cloned as [`AtomKind::Empty`]. + Closure(AtomClosure<'a>), +} + +impl Clone for AtomKind<'_> { + fn clone(&self) -> Self { + match self { + AtomKind::Empty => AtomKind::Empty, + AtomKind::Text(text) => AtomKind::Text(text.clone()), + AtomKind::Image(image) => AtomKind::Image(image.clone()), + AtomKind::Closure(_) => { + log::warn!("Cannot clone atom closures"); + AtomKind::Empty + } + } + } +} + +impl Debug for AtomKind<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + AtomKind::Empty => write!(f, "AtomKind::Empty"), + AtomKind::Text(text) => write!(f, "AtomKind::Text({text:?})"), + AtomKind::Image(image) => write!(f, "AtomKind::Image({image:?})"), + AtomKind::Closure(_) => write!(f, "AtomKind::Closure()"), + } + } } impl<'a> AtomKind<'a> { + /// See [`Self::Text`] pub fn text(text: impl Into) -> Self { AtomKind::Text(text.into()) } + /// See [`Self::Image`] pub fn image(image: impl Into>) -> Self { AtomKind::Image(image.into()) } + /// See [`Self::Closure`] + pub fn closure(func: impl FnOnce(&Ui, IntoSizedArgs) -> IntoSizedResult<'static> + 'a) -> Self { + AtomKind::Closure(Box::new(func)) + } + /// Turn this [`AtomKind`] into a [`SizedAtomKind`]. /// /// This converts [`WidgetText`] into [`crate::Galley`] and tries to load and size [`Image`]. @@ -76,23 +115,40 @@ impl<'a> AtomKind<'a> { pub fn into_sized( self, ui: &Ui, - available_size: Vec2, - wrap_mode: Option, - fallback_font: FontSelection, - ) -> (Vec2, SizedAtomKind<'a>) { + IntoSizedArgs { + available_size, + wrap_mode, + fallback_font, + }: IntoSizedArgs, + ) -> IntoSizedResult<'a> { match self { AtomKind::Text(text) => { - let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode()); let galley = text.into_galley(ui, Some(wrap_mode), available_size.x, fallback_font); - (galley.intrinsic_size(), SizedAtomKind::Text(galley)) + IntoSizedResult { + intrinsic_size: galley.intrinsic_size(), + sized: SizedAtomKind::Text(galley), + } } AtomKind::Image(image) => { let size = image.load_and_calc_size(ui, available_size); let size = size.unwrap_or(Vec2::ZERO); - (size, SizedAtomKind::Image(image, size)) + IntoSizedResult { + intrinsic_size: size, + sized: SizedAtomKind::Image { image, size }, + } } - AtomKind::Custom(id) => (Vec2::ZERO, SizedAtomKind::Custom(id)), - AtomKind::Empty => (Vec2::ZERO, SizedAtomKind::Empty), + AtomKind::Empty => IntoSizedResult { + intrinsic_size: Vec2::ZERO, + sized: SizedAtomKind::Empty { size: None }, + }, + AtomKind::Closure(func) => func( + ui, + IntoSizedArgs { + available_size, + wrap_mode, + fallback_font, + }, + ), } } } diff --git a/crates/egui/src/atomics/atom_layout.rs b/crates/egui/src/atomics/atom_layout.rs index 8132a7dc9..b78f23536 100644 --- a/crates/egui/src/atomics/atom_layout.rs +++ b/crates/egui/src/atomics/atom_layout.rs @@ -38,6 +38,7 @@ pub struct AtomLayout<'a> { fallback_text_color: Option, fallback_font: Option, min_size: Vec2, + max_size: Vec2, wrap_mode: Option, align2: Option, } @@ -59,6 +60,7 @@ impl<'a> AtomLayout<'a> { fallback_text_color: None, fallback_font: None, min_size: Vec2::ZERO, + max_size: Vec2::INFINITY, wrap_mode: None, align2: None, } @@ -113,6 +115,33 @@ impl<'a> AtomLayout<'a> { self } + /// Set the maximum size of the Widget. + /// + /// By default, the size is limited by the available size in the [`Ui`]. + #[inline] + pub fn max_size(mut self, size: Vec2) -> Self { + self.max_size = size; + self + } + + /// Set the maximum width of the Widget. + /// + /// By default, the width is limited by the available width in the [`Ui`]. + #[inline] + pub fn max_width(mut self, width: f32) -> Self { + self.max_size.x = width; + self + } + + /// Set the maximum height of the Widget. + /// + /// By default, the height is limited by the available height in the [`Ui`]. + #[inline] + pub fn max_height(mut self, height: f32) -> Self { + self.max_size.y = height; + self + } + /// Set the [`Id`] used to allocate a [`Response`]. #[inline] pub fn id(mut self, id: Id) -> Self { @@ -161,6 +190,7 @@ impl<'a> AtomLayout<'a> { sense, fallback_text_color, min_size, + mut max_size, wrap_mode, align2, fallback_font, @@ -190,8 +220,16 @@ impl<'a> AtomLayout<'a> { fallback_text_color.unwrap_or_else(|| ui.style().visuals.text_color()); let gap = gap.unwrap_or_else(|| ui.spacing().icon_spacing); + // max_size has no effect in justified layouts. If we'd limit the available size here, + // the content would be sized differently than the frame which would look weird. + if ui.layout().horizontal_justify() { + max_size.x = f32::INFINITY; + } + + let available_size = ui.available_size().at_most(max_size); + // The size available for the content - let available_inner_size = ui.available_size() - frame.total_margin().sum(); + let available_inner_size = available_size - frame.total_margin().sum(); let mut desired_width = 0.0; @@ -321,7 +359,7 @@ impl<'atom> AllocatedAtomLayout<'atom> { pub fn iter_images(&self) -> impl Iterator> { self.iter_kinds().filter_map(|kind| { - if let SizedAtomKind::Image(image, _) = kind { + if let SizedAtomKind::Image { image, size: _ } = kind { Some(image) } else { None @@ -331,7 +369,7 @@ impl<'atom> AllocatedAtomLayout<'atom> { pub fn iter_images_mut(&mut self) -> impl Iterator> { self.iter_kinds_mut().filter_map(|kind| { - if let SizedAtomKind::Image(image, _) = kind { + if let SizedAtomKind::Image { image, size: _ } = kind { Some(image) } else { None @@ -373,8 +411,11 @@ impl<'atom> AllocatedAtomLayout<'atom> { F: FnMut(Image<'atom>) -> Image<'atom>, { self.map_kind(|kind| { - if let SizedAtomKind::Image(image, size) = kind { - SizedAtomKind::Image(f(image), size) + if let SizedAtomKind::Image { image, size } = kind { + SizedAtomKind::Image { + image: f(image), + size, + } } else { kind } @@ -422,25 +463,24 @@ impl<'atom> AllocatedAtomLayout<'atom> { .with_min_x(cursor) .with_max_x(cursor + size.x + growth); cursor = frame.right() + gap; + let rect = sized.align.align_size_within_rect(size, frame); - let align = Align2::CENTER_CENTER; - let rect = align.align_size_within_rect(size, frame); + if let Some(id) = sized.id { + debug_assert!( + !response.custom_rects.iter().any(|(i, _)| *i == id), + "Duplicate custom id" + ); + response.custom_rects.push((id, rect)); + } match sized.kind { SizedAtomKind::Text(galley) => { ui.painter().galley(rect.min, galley, fallback_text_color); } - SizedAtomKind::Image(image, _) => { + SizedAtomKind::Image { image, size: _ } => { image.paint_at(ui, rect); } - SizedAtomKind::Custom(id) => { - debug_assert!( - !response.custom_rects.iter().any(|(i, _)| *i == id), - "Duplicate custom id" - ); - response.custom_rects.push((id, rect)); - } - SizedAtomKind::Empty => {} + SizedAtomKind::Empty { .. } => {} } } @@ -450,7 +490,7 @@ impl<'atom> AllocatedAtomLayout<'atom> { /// Response from a [`AtomLayout::show`] or [`AllocatedAtomLayout::paint`]. /// -/// Use [`AtomLayoutResponse::rect`] to get the response rects from [`AtomKind::Custom`]. +/// Use [`AtomLayoutResponse::rect`] to get the response rects from [`crate::Atom::custom`]. #[derive(Clone, Debug)] pub struct AtomLayoutResponse { pub response: Response, @@ -470,7 +510,7 @@ impl AtomLayoutResponse { self.custom_rects.iter().copied() } - /// Use this together with [`AtomKind::Custom`] to add custom painting / child widgets. + /// Use this together with [`crate::Atom::custom`] to add custom painting / child widgets. /// /// NOTE: Don't `unwrap` rects, they might be empty when the widget is not visible. pub fn rect(&self, id: Id) -> Option { diff --git a/crates/egui/src/atomics/atoms.rs b/crates/egui/src/atomics/atoms.rs index 2cb668cff..fb04ee2dd 100644 --- a/crates/egui/src/atomics/atoms.rs +++ b/crates/egui/src/atomics/atoms.rs @@ -21,21 +21,24 @@ impl<'a> Atoms<'a> { self.0.push(atom.into()); } + /// Extend the list of atoms by appending more atoms to the right side. + /// + /// If you have weird lifetime issues with this, use [`Self::push_right`] in a loop instead. + pub fn extend_right(&mut self, atoms: Self) { + self.0.extend(atoms.0); + } + /// Insert a new [`Atom`] at the beginning of the list (left side). pub fn push_left(&mut self, atom: impl Into>) { self.0.insert(0, atom.into()); } - /// Insert atoms at the beginning of the list (left side). - pub fn extend_left(&mut self, atoms: impl IntoAtoms<'a>) { - let mut left = atoms.into_atoms(); - left.0.append(&mut self.0); - *self = left; - } - - /// Insert atoms at the end of the list (right side). - pub fn extend_right(&mut self, atoms: impl IntoAtoms<'a>) { - self.0.append(&mut atoms.into_atoms().0); + /// Extend the list of atoms by prepending more atoms to the left side. + /// + /// If you have weird lifetime issues with this, use [`Self::push_left`] in a loop instead. + pub fn extend_left(&mut self, mut atoms: Self) { + std::mem::swap(&mut atoms.0, &mut self.0); + self.0.extend(atoms.0); } /// Concatenate and return the text contents. diff --git a/crates/egui/src/atomics/sized_atom.rs b/crates/egui/src/atomics/sized_atom.rs index f1ae0f81b..19c289ab3 100644 --- a/crates/egui/src/atomics/sized_atom.rs +++ b/crates/egui/src/atomics/sized_atom.rs @@ -4,6 +4,8 @@ use emath::Vec2; /// A [`crate::Atom`] which has been sized. #[derive(Clone, Debug)] pub struct SizedAtom<'a> { + pub id: Option, + pub(crate) grow: bool, /// The size of the atom. @@ -15,6 +17,9 @@ pub struct SizedAtom<'a> { /// Intrinsic size of the atom. This is used to calculate `Response::intrinsic_size`. pub intrinsic_size: Vec2, + /// How will the atom be aligned in its available space? + pub align: emath::Align2, + pub kind: SizedAtomKind<'a>, } diff --git a/crates/egui/src/atomics/sized_atom_kind.rs b/crates/egui/src/atomics/sized_atom_kind.rs index ff8da1631..02263adad 100644 --- a/crates/egui/src/atomics/sized_atom_kind.rs +++ b/crates/egui/src/atomics/sized_atom_kind.rs @@ -1,16 +1,20 @@ -use crate::{Id, Image}; +use crate::Image; use emath::Vec2; use epaint::Galley; use std::sync::Arc; /// A sized [`crate::AtomKind`]. -#[derive(Clone, Default, Debug)] +#[derive(Clone, Debug)] pub enum SizedAtomKind<'a> { - #[default] - Empty, + Empty { size: Option }, Text(Arc), - Image(Image<'a>, Vec2), - Custom(Id), + Image { image: Image<'a>, size: Vec2 }, +} + +impl Default for SizedAtomKind<'_> { + fn default() -> Self { + Self::Empty { size: None } + } } impl SizedAtomKind<'_> { @@ -18,8 +22,8 @@ impl SizedAtomKind<'_> { pub fn size(&self) -> Vec2 { match self { SizedAtomKind::Text(galley) => galley.size(), - SizedAtomKind::Image(_, size) => *size, - SizedAtomKind::Empty | SizedAtomKind::Custom(_) => Vec2::ZERO, + SizedAtomKind::Image { image: _, size } => *size, + SizedAtomKind::Empty { size } => size.unwrap_or_default(), } } } diff --git a/crates/egui/src/widgets/drag_value.rs b/crates/egui/src/widgets/drag_value.rs index d12020097..1297b614b 100644 --- a/crates/egui/src/widgets/drag_value.rs +++ b/crates/egui/src/widgets/drag_value.rs @@ -166,14 +166,14 @@ impl<'a> DragValue<'a> { /// Show a prefix before the number, e.g. "x: " #[inline] pub fn prefix(mut self, prefix: impl IntoAtoms<'a>) -> Self { - self.atoms.extend_left(prefix); + self.atoms.extend_left(prefix.into_atoms()); self } /// Add a suffix to the number, this can be e.g. a unit ("Β°" or " m") #[inline] pub fn suffix(mut self, suffix: impl IntoAtoms<'a>) -> Self { - self.atoms.extend_right(suffix); + self.atoms.extend_right(suffix.into_atoms()); self } @@ -447,18 +447,15 @@ impl Widget for DragValue<'_> { let mut past_value = false; let atom_id = Id::new(Self::ATOM_ID); for atom in atoms.iter() { - match &atom.kind { - AtomKind::Custom(id) if *id == atom_id => { - past_value = true; + if atom.id == Some(atom_id) { + past_value = true; + } + if let AtomKind::Text(text) = &atom.kind { + if past_value { + suffix_text.push_str(text.text()); + } else { + prefix_text.push_str(text.text()); } - AtomKind::Text(text) => { - if past_value { - suffix_text.push_str(text.text()); - } else { - prefix_text.push_str(text.text()); - } - } - _ => {} } } @@ -605,9 +602,7 @@ impl Widget for DragValue<'_> { response } else { atoms.map_atoms(|atom| { - if let AtomKind::Custom(id) = atom.kind - && id == atom_id - { + if atom.id == Some(atom_id) { RichText::new(value_text.clone()) .text_style(text_style.clone()) .into() From 14afefa2521d1baaf4fd02105eec2d3727a7ac36 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Thu, 12 Mar 2026 13:49:16 +0100 Subject: [PATCH 20/58] Fix galley width calculation being off due to subpixel binning (#7972) - fix for https://github.com/rerun-io/reality/pull/1075 The galleys row size was calculated by looking at the last glyphs pos_x, which got changed to be rounded to integers when we added subpixel binning. This introduced a subtle bug which caused the width of galleys to be slightly off. This PR fixes this by looking at the actual cursor position instead, which is not rounded. Also added a test to ensure this is correct. Previously, for the second and last line, the `x` was too close to the `0`. image --------- Co-authored-by: lucasmerlin <8009393+lucasmerlin@users.noreply.github.com> --- .../egui_demo_app/tests/snapshots/clock.png | 4 ++-- .../tests/snapshots/custom3d.png | 4 ++-- .../tests/snapshots/easymarkeditor.png | 4 ++-- .../tests/snapshots/imageviewer.png | 4 ++-- .../tests/snapshots/demos/Clipboard Test.png | 2 +- .../tests/snapshots/demos/Code Editor.png | 4 ++-- .../tests/snapshots/demos/Code Example.png | 4 ++-- .../tests/snapshots/demos/Cursor Test.png | 2 +- .../tests/snapshots/demos/Dancing Strings.png | 4 ++-- .../tests/snapshots/demos/Font Book.png | 4 ++-- .../tests/snapshots/demos/Frame.png | 4 ++-- .../tests/snapshots/demos/Grid Test.png | 4 ++-- .../tests/snapshots/demos/Highlighting.png | 4 ++-- .../tests/snapshots/demos/ID Test.png | 4 ++-- .../snapshots/demos/Interactive Container.png | 4 ++-- .../tests/snapshots/demos/Layout Test.png | 4 ++-- .../snapshots/demos/Manual Layout Test.png | 4 ++-- .../tests/snapshots/demos/Misc Demos.png | 4 ++-- .../tests/snapshots/demos/Modals.png | 2 +- .../tests/snapshots/demos/Painting.png | 4 ++-- .../tests/snapshots/demos/Popups.png | 4 ++-- .../tests/snapshots/demos/Scene.png | 4 ++-- .../tests/snapshots/demos/Screenshot.png | 4 ++-- .../tests/snapshots/demos/Scrolling.png | 4 ++-- .../tests/snapshots/demos/Sliders.png | 4 ++-- .../tests/snapshots/demos/Table.png | 4 ++-- .../snapshots/demos/Tessellation Test.png | 4 ++-- .../tests/snapshots/demos/Text Layout.png | 4 ++-- .../tests/snapshots/demos/TextEdit.png | 4 ++-- .../tests/snapshots/demos/Undo Redo.png | 2 +- .../tests/snapshots/demos/Window Options.png | 4 ++-- .../snapshots/demos/Window Resize Test.png | 4 ++-- .../snapshots/image_kerning/image_dark_x1.png | 4 ++-- .../image_kerning/image_light_x1.png | 4 ++-- .../tests/snapshots/modals_1.png | 4 ++-- .../tests/snapshots/modals_2.png | 4 ++-- .../tests/snapshots/modals_3.png | 4 ++-- ...rop_should_prevent_focusing_lower_area.png | 4 ++-- .../snapshots/rendering_test/dpi_1.00.png | 4 ++-- .../snapshots/rendering_test/dpi_1.25.png | 4 ++-- .../snapshots/rendering_test/dpi_1.50.png | 4 ++-- .../snapshots/rendering_test/dpi_1.67.png | 4 ++-- .../snapshots/rendering_test/dpi_1.75.png | 4 ++-- .../snapshots/rendering_test/dpi_2.00.png | 4 ++-- .../tessellation_test/Additive rectangle.png | 4 ++-- .../tessellation_test/Blurred stroke.png | 4 ++-- .../snapshots/tessellation_test/Blurred.png | 4 ++-- .../tessellation_test/Minimal rounding.png | 4 ++-- .../snapshots/tessellation_test/Normal.png | 4 ++-- .../Thick stroke, minimal rounding.png | 4 ++-- .../tessellation_test/Thin filled.png | 4 ++-- .../tessellation_test/Thin stroked.png | 4 ++-- .../snapshots/widget_gallery_dark_x1.png | 4 ++-- .../snapshots/widget_gallery_dark_x2.png | 4 ++-- .../snapshots/widget_gallery_light_x1.png | 4 ++-- .../snapshots/widget_gallery_light_x2.png | 4 ++-- .../tests/snapshots/combobox_closed.png | 4 ++-- .../tests/snapshots/combobox_opened.png | 4 ++-- .../tests/snapshots/menu/closed_hovered.png | 4 ++-- .../tests/snapshots/menu/opened.png | 4 ++-- .../tests/snapshots/menu/submenu.png | 4 ++-- .../tests/snapshots/menu/subsubmenu.png | 4 ++-- .../override_text_color_interactive.png | 4 ++-- .../tests/snapshots/readme_example.png | 4 ++-- .../tests/snapshots/test_masking.png | 4 ++-- .../tests/snapshots/test_shrink.png | 4 ++-- .../tests/snapshots/test_tooltip_hidden.png | 4 ++-- .../tests/snapshots/test_tooltip_shown.png | 4 ++-- crates/epaint/src/text/text_layout.rs | 23 +++++++++++-------- .../tests/snapshots/atom_letter_spacing.png | 3 +++ tests/egui_tests/tests/snapshots/grow_all.png | 2 +- .../tests/snapshots/layout/atoms_image.png | 4 ++-- .../tests/snapshots/layout/atoms_minimal.png | 4 ++-- .../snapshots/layout/atoms_multi_grow.png | 4 ++-- .../tests/snapshots/layout/button.png | 4 ++-- .../tests/snapshots/layout/button_image.png | 4 ++-- .../layout/button_image_shortcut.png | 4 ++-- .../tests/snapshots/layout/checkbox.png | 4 ++-- .../snapshots/layout/checkbox_checked.png | 4 ++-- .../tests/snapshots/layout/drag_value.png | 4 ++-- .../tests/snapshots/layout/radio.png | 4 ++-- .../tests/snapshots/layout/radio_checked.png | 4 ++-- .../snapshots/layout/selectable_value.png | 4 ++-- .../layout/selectable_value_selected.png | 4 ++-- .../tests/snapshots/layout/slider.png | 4 ++-- .../tests/snapshots/layout/text_edit.png | 4 ++-- .../tests/snapshots/layout/text_edit_clip.png | 4 ++-- .../snapshots/layout/text_edit_no_clip.png | 4 ++-- .../layout/text_edit_placeholder_clip.png | 4 ++-- .../egui_tests/tests/snapshots/max_width.png | 2 +- .../tests/snapshots/max_width_and_grow.png | 2 +- .../tests/snapshots/shrink_first_text.png | 2 +- .../tests/snapshots/shrink_last_text.png | 4 ++-- .../sides/default_long_fit_contents.png | 4 ++-- .../sides/default_short_fit_contents.png | 4 ++-- .../sides/shrink_left_long_fit_contents.png | 4 ++-- .../sides/shrink_left_short_fit_contents.png | 4 ++-- .../sides/shrink_right_long_fit_contents.png | 4 ++-- .../sides/shrink_right_short_fit_contents.png | 4 ++-- .../sides/wrap_left_long_fit_contents.png | 4 ++-- .../sides/wrap_left_short_fit_contents.png | 4 ++-- .../sides/wrap_right_long_fit_contents.png | 4 ++-- .../sides/wrap_right_short_fit_contents.png | 4 ++-- .../tests/snapshots/size_max_size.png | 2 +- .../tests/snapshots/text_edit_rtl_0.png | 4 ++-- .../tests/snapshots/text_edit_rtl_1.png | 4 ++-- .../tests/snapshots/text_edit_rtl_2.png | 2 +- .../tests/snapshots/visuals/button.png | 4 ++-- .../tests/snapshots/visuals/button_image.png | 4 ++-- .../visuals/button_image_shortcut.png | 4 ++-- .../button_image_shortcut_selected.png | 4 ++-- .../tests/snapshots/visuals/drag_value.png | 4 ++-- .../tests/snapshots/visuals/radio.png | 4 ++-- .../tests/snapshots/visuals/radio_checked.png | 4 ++-- .../snapshots/visuals/selectable_value.png | 4 ++-- .../visuals/selectable_value_selected.png | 4 ++-- .../snapshots/visuals/text_edit_no_clip.png | 4 ++-- tests/egui_tests/tests/test_atoms.rs | 21 +++++++++++++++++ 118 files changed, 258 insertions(+), 229 deletions(-) create mode 100644 tests/egui_tests/tests/snapshots/atom_letter_spacing.png diff --git a/crates/egui_demo_app/tests/snapshots/clock.png b/crates/egui_demo_app/tests/snapshots/clock.png index 1f6b6438f..b8ba70789 100644 --- a/crates/egui_demo_app/tests/snapshots/clock.png +++ b/crates/egui_demo_app/tests/snapshots/clock.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5878bc5beaf4406c24f23d900aa9ac7c5507e44cb3ade83b743b8b62e7da1615 -size 335355 +oid sha256:e4cff85a005ad897624f6c7a2b2ad599325ec99e0b3c9c35963f16611f283997 +size 335371 diff --git a/crates/egui_demo_app/tests/snapshots/custom3d.png b/crates/egui_demo_app/tests/snapshots/custom3d.png index 41ace3480..20a933d45 100644 --- a/crates/egui_demo_app/tests/snapshots/custom3d.png +++ b/crates/egui_demo_app/tests/snapshots/custom3d.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:706ad012e52a8c51175b050b985cca88e2cb306b24f618b7391641397d17cd28 -size 92804 +oid sha256:cc55083688d043234c37d22c74635a44b00f4d28c3802c4327c2eaf563c73eed +size 92800 diff --git a/crates/egui_demo_app/tests/snapshots/easymarkeditor.png b/crates/egui_demo_app/tests/snapshots/easymarkeditor.png index b11978ce9..7b370c38e 100644 --- a/crates/egui_demo_app/tests/snapshots/easymarkeditor.png +++ b/crates/egui_demo_app/tests/snapshots/easymarkeditor.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4135662f2b60a10ef8c3b155172d7a3edcf24a625d8286aeaad0614aa8819893 -size 169604 +oid sha256:78d91fa4657cd1cb375487f606b80d418ed6fdbd8a0c0225b9383eead5001563 +size 169682 diff --git a/crates/egui_demo_app/tests/snapshots/imageviewer.png b/crates/egui_demo_app/tests/snapshots/imageviewer.png index 03aa3077a..8d5f688bf 100644 --- a/crates/egui_demo_app/tests/snapshots/imageviewer.png +++ b/crates/egui_demo_app/tests/snapshots/imageviewer.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:509020d8885b718900e534c9948cb95ae88e1eee9e113bdfb77a2f75b9a68f7b -size 96703 +oid sha256:c8b23a5286e5d2dbd8d3eddac6583d981152bd791f74edfa5c712a610f795256 +size 96759 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png index be63a88ea..2549417be 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:52d2233594c4bad348f5479dcfad9576ee5fd7d49faedb6f5ba74b374cdaf3ad +oid sha256:a53262cf5d8507d8eeae8c968767cef462b727879245085673982b850a6da670 size 26977 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png b/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png index 91200ed0f..f4b7690fc 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:47f6cd15b88df83a9b2d8538e424041e661712f2e85312166a581f69f1254643 -size 26839 +oid sha256:75a9cd9a3315b236c23a53e890de1a821d39c3327813d06df85ba86d2ed50cc7 +size 26887 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png b/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png index 742c62272..46a5ed1f7 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9d6ba2c4825517b4cc030b7639771d06913da86c2d52fd40e6263692335afa04 -size 76079 +oid sha256:a7601584308bf60820506f842569a3c1daf3c15fa6e715f6b9386b5112dcc92f +size 76076 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Cursor Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Cursor Test.png index dda0e964c..d4c9508f9 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Cursor Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Cursor Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6927950ccbc5c81d6fbfe0a90ddd79a4306518caced14bb60debd30c7e41d326 +oid sha256:d4e33c7f817100d8414bba245ee7886354b86109f383d59e87a197e39501f0a0 size 62604 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png b/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png index f0b9c3892..40153611c 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:366d18457aabf1ebdd42fdbce8819cc67a4f59db85c452623b02ee1d0e8fc50a -size 27817 +oid sha256:93fcc271831167cb077f3de0a9f0e27037f9e5a2ce94e056bd6f1ede9890cb7e +size 27818 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png b/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png index 281154b2d..375b0f922 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3c5e803659e936268b476690427ef6a6802f477e078dc956a9d1c857b48da868 -size 114409 +oid sha256:3fc2793506ec483c7f124b6206fb18ffb73bec29746f2d9bb5145042ddc45016 +size 114410 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Frame.png b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png index 9ce775f7e..b8791ed2e 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Frame.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a169cda21797152fb8aa69928ad3f4cef1b45cc5f213e5bfa01b8fe7723a4852 -size 25391 +oid sha256:20ea4f93ee50c7a3585aef74c66d7700083ac1c16519b0704b70387849d9d2bc +size 25057 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Grid Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Grid Test.png index c6f5ae26c..b8f0440b6 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Grid Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Grid Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:55c5fb90736a31fbccd72be5994fc8c62b4b9da9842ad1e6bb795a1e1461a6f8 -size 98780 +oid sha256:1b72a4c0e6d441190a7a156b8bba709e81b6c1fe7b0eacedc1ee7a3bfcf881f6 +size 99297 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png b/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png index a61363b99..a965901d5 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a4080ee1a16eea16c8f4246fe3e760ade7d0289b30d88068d1e49ffb88d88dca -size 18280 +oid sha256:08c40934d4bd2a239bdcc1928d1e5eba56bac03fdded2c85cf47b020d669f07e +size 18281 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/ID Test.png b/crates/egui_demo_lib/tests/snapshots/demos/ID Test.png index c750513c0..abd7c485b 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/ID Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/ID Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:600d9e0fc193396f36b599e4bfad2547128160d2e56dc2a989cb5f978d5115ae -size 113797 +oid sha256:82878e4150e38fdc4b2e78203c8c661c2d9e716ab32595c298392faf6ba96105 +size 113803 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png b/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png index b57b98a53..d01e4bb7b 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ae5e843cc9d847b0f3c4092f55b914699adb506cb807b0a97bfc4ec7d94537b -size 22613 +oid sha256:58cd3aba4392332a45f57c7dd90a9b5da386cb396c0c6319e7a7dae71e03ff30 +size 22563 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Layout Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Layout Test.png index 6a2c9cf63..0f50709d3 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Layout Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Layout Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b0d38cb1eebf3ce7d661d094175b425db2b9eccc5e439b14256c5d801d4454d4 -size 47285 +oid sha256:26ffcf6b71108b82ce15d4cf3f9dd0ce9fe0b9563f02725fef1b74f40e749439 +size 47281 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Manual Layout Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Manual Layout Test.png index e6fd0795d..1ba4655fd 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Manual Layout Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Manual Layout Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:00e4c7659cd50044d473dd2c138392f78ac7eba27f2b52bae61246f5dc5b2782 -size 23156 +oid sha256:faedf9631149e231d510165215c24fccec50502d58000d5f893aa047a637a68f +size 23148 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Misc Demos.png b/crates/egui_demo_lib/tests/snapshots/demos/Misc Demos.png index 95c172d26..f34ac0cd4 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Misc Demos.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Misc Demos.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:56b44d26946770c0878e11e3197633697ad339a7e8fcffe7279a6b4c45cd3582 -size 65384 +oid sha256:b6b4c2e55c02fa4caf5f9f8bd2d8c0311cc4cbcf1fc2f568fe112e8e6125c675 +size 65308 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Modals.png b/crates/egui_demo_lib/tests/snapshots/demos/Modals.png index dcf935287..28fe7b683 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Modals.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Modals.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:34de6fd788288174e8e6f1fa48cd49dbc7b14fcf649fe302aed49c8c50178aa8 +oid sha256:3a65927cd8bd8d24e3ffbea8eb421eb22849b27dc77d36f8acd82bf5d5e63959 size 33469 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Painting.png b/crates/egui_demo_lib/tests/snapshots/demos/Painting.png index bbfeed26f..da06d85bf 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Painting.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Painting.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96760220222bdde8dd1b3d28f089af2892403b78df8d34d3d94dc1a604387083 -size 18241 +oid sha256:9c595ee9b7ada33780178a6a35e26a98055a707f2ff99f6bb36e8db4ed819791 +size 18242 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Popups.png b/crates/egui_demo_lib/tests/snapshots/demos/Popups.png index 32beeba37..0aab4baf8 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Popups.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Popups.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:48d138634343edb251435bf6f9075502b913e806e8b280f3e6012977c13af16f -size 56753 +oid sha256:c218115d305dfa6c9ab883ac6f3a21584b4840b3ba273ea765c8a8381d78935f +size 57181 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scene.png b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png index 8dae8e626..277f7ab2c 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Scene.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:23efb79ca13367f4d8886142d015815c5bdf99c0ed243ece294a7cfd365fd166 -size 33503 +oid sha256:4d10b78f4d80d61a3352d7f2b0ed9b2d93af5f184f2487f6f2afff02a38f4608 +size 33475 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png b/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png index dc7dda582..5517e03ef 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5d85faf6e7fa26741eb720e74695f3c207ea15097b118c3cafe5d52d5d85ea20 -size 23666 +oid sha256:f2ce9062c5d1f0b0861d5df49ae64e56ba0e6501e8bd3f8a92c53aea748be78b +size 23629 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png b/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png index b9f2816a2..24e42cb23 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:875eb687f3a1eed52a6617e532edc5332b0a16296e2b6addac66d5bea0448b14 -size 172605 +oid sha256:b5b965a7c690fd8e8646812513e2417170b687fd37e29d220c29127ba0cc200c +size 172609 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png b/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png index 3513155f0..28864a446 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fd3573be9ba5818b4edc371095f5c23b084e6c7eaae4f2fd3a6d2de051878c9d -size 118567 +oid sha256:6ffba8bb50b42e47f855f62682f6d5ec10bf67b01d3aa2e843f6bf787f150d0d +size 118562 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Table.png b/crates/egui_demo_lib/tests/snapshots/demos/Table.png index c0470071c..3f72922d2 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Table.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Table.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ed1be0294fb65b11c54c6dc9e4cecb383ace16dad748e3c42f2ed65b2fb05ea8 -size 75509 +oid sha256:931f38ade8373ff79801c05c5d4397f2c5fcfa27022f2e1abe9eb29d561a3aef +size 76022 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Tessellation Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Tessellation Test.png index 9d9d26c7c..c1b6f506a 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Tessellation Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Tessellation Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:01b34a7371dd8b3539ed20594a42f3cd9792d391d2cb44740aa5ef301c9652f3 -size 70244 +oid sha256:57bf5220ae8f47485a07e9117abaaad36924d8c6c0f9e278cb05c455f342bff6 +size 70250 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png b/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png index c86b223dd..e9b302746 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:411dd61eb182a70d46c7fc1fa0f9a4b8aeae88d08b11d5af948c5acccfa9d133 -size 60950 +oid sha256:7c964d07a39ad286a562b53cdfe514d568d91955e6c1ca06a0cb5e45dbe3977e +size 60947 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png b/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png index c63f844ec..843bb93b4 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78111e33d44a09beb9c1233dd2d5ef10103213a1c1c7df8b5e258d9684f1d93a -size 21810 +oid sha256:718203d31d8b027a7718a66c4712cf1e17b9aea2e870d755bd2c0c346529d4f4 +size 21814 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png b/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png index 91aeb6b91..81204a347 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e9498a706de403ee7db3603ecc896688e584fede367ed6087cdf10b798a3ab2d +oid sha256:6af5adc42544171c6d85e190c853aca06784c131a373a693a6f7069d4cf1a404 size 13698 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png b/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png index c768c6507..78446cca9 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:42385da2eb74d54ba086aed973ade15f2a8d2be0c9281c05e6fb88846137bf81 -size 35870 +oid sha256:2e8e03c2a42e195e6489659053aecb78755d3c218558cb2e9339fa7b6db59405 +size 35875 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png index e8b44a484..a6d103d6d 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9f1b6fa0c48479606539f2d98befe1c9ee881846c0b55d7a53313962d556380d -size 484629 +oid sha256:ad22ea6b6e69fd71416fdae76cbd142d279f8f562e74b77e63b3989be187c57c +size 484631 diff --git a/crates/egui_demo_lib/tests/snapshots/image_kerning/image_dark_x1.png b/crates/egui_demo_lib/tests/snapshots/image_kerning/image_dark_x1.png index aeaa46a34..5cc884a55 100644 --- a/crates/egui_demo_lib/tests/snapshots/image_kerning/image_dark_x1.png +++ b/crates/egui_demo_lib/tests/snapshots/image_kerning/image_dark_x1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:89074b8dab103a419bc3dac743da4d8c47f435fa55b98d8aab71f6c9fb4d39de -size 12370 +oid sha256:c8ea98c65376d9f6ac66d0a9471c4bf3add0904294e7ca1a105458b90654a2e2 +size 12476 diff --git a/crates/egui_demo_lib/tests/snapshots/image_kerning/image_light_x1.png b/crates/egui_demo_lib/tests/snapshots/image_kerning/image_light_x1.png index da72002a0..b223bbb3d 100644 --- a/crates/egui_demo_lib/tests/snapshots/image_kerning/image_light_x1.png +++ b/crates/egui_demo_lib/tests/snapshots/image_kerning/image_light_x1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7bd7b54ff60859e4d4793000bef3adbec4c071063bec6bfdbde62516c4fc3478 -size 12959 +oid sha256:3793a5e83ef9bdffef99bcd8905a094acb69cde356e3a7125a544045296c3926 +size 13070 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_1.png b/crates/egui_demo_lib/tests/snapshots/modals_1.png index 6690a129e..96d31e11d 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_1.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b53b03212953e12915a0e41bff5f0cdea90f8f866220a01142edaeb915735a34 -size 47077 +oid sha256:941582e2e20a9459db1f2cb7f07fa1930acfdb12cbbe7f96f9aafbeabf8b37f6 +size 47076 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_2.png b/crates/egui_demo_lib/tests/snapshots/modals_2.png index f8eb85d39..3c0a88ee0 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_2.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:22e5d61a141b5a8663feb8a47371f9259d2a77fdacb1245bce411ffc85ce2cae -size 47716 +oid sha256:2735a021f171f5c95888cda76e8668e1e023588c8c6c7cd382c03d8e31988fe3 +size 48209 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_3.png b/crates/egui_demo_lib/tests/snapshots/modals_3.png index c12f0b9df..28221255f 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_3.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:120558ab0c267650744bd078aeace8d4122b3569c5998602f969766131d15c44 -size 43894 +oid sha256:867bef6b55b73d127306a461e115b6f0047d582904999de80aeabae00e60c967 +size 44295 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png b/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png index ef4c45ff1..bb1935741 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af92548b6c8569081a91cb772b73988d9cb342498ddf9c0c86b6963cef8eda9e -size 43985 +oid sha256:936ec8b223ae7f0f32c640c127e1b6b14033bb7d168a4d1f0e6b3bd08a761e36 +size 44055 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png index b5367f0ed..b48827b6a 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:db510af76578693c85ce78ca91224758a56f7bbf33db3221c9a4edca08b06600 -size 590547 +oid sha256:fba7387f5deba5e144e2106154b15ab956a50a418857bd34e16b306d7f1a29e4 +size 588252 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png index 7158a3545..d1286d6a1 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cae2b789e8afff23b7545d42a530e6c972d28736bad2bdacbc69f0e7065f85cc -size 740660 +oid sha256:4656f3255d7859c07b269ff655eafe21bdddb949a07aa91477b826f6e2af8c28 +size 740616 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png index 8b9cb281f..f1892bf26 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:09d9f567ec371d60881b525ddb462d9135552db97af5921a6eb02aba40e40616 -size 971544 +oid sha256:b18ff644ba5bd0c7f094bf8eac079d8a72bc6918638b1b110002f2f0a7a362cc +size 967860 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png index 9f5a69154..c32762306 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3c383dd89fda6094704027074a72085591339a276d60502626d78e8e527b2e10 -size 1076719 +oid sha256:134caff5b8a4969055c32e8f51ca9c6eae1528b84d348691d860913e839de0d9 +size 1076746 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png index 74760261a..d021a1e71 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0b4559541cf3259496c760a26f8d83e82179cb7e4576333682c5af49ee4a35a7 -size 1125331 +oid sha256:d731b4ce039315e096113f3c83168165020949e57564e641e778728e35901169 +size 1125286 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png index a85909178..03d4fb69a 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:67c8412a1e8fdbfd88f8573797fbf6fbd89c6ce783a074a8e90f7d8d9e67dd57 -size 1366351 +oid sha256:cfac3518220555984d47c9fdfea2202a37102250aefcc2509794f337b3a7baae +size 1361407 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png index 5c6b867d0..1d3f1785e 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0574527ce659559f5b0709d84903afcec60b4ffa6b7979e8985027d326fd782a -size 45066 +oid sha256:cf21fe763e9762bca1b0f486e29a6024efcbc106a7f1ac195104acd0621cf8db +size 45107 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png index 64c5f12e1..646eaa1e0 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:50eca0feefe5d43db74f5e3bc08abda13c5986710cc4aaa03e9382af56264fc2 -size 86826 +oid sha256:2f09338e652b965cc9ae7bbb261845cd9c15d79f3d15f3c5b5326ef6d163b606 +size 86885 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png index e0dcebac3..e667fc387 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:91110d6a1da995e3e215cc92fdcceac84335e60b5b2fdbb2f16d5ecc6065fe55 -size 118912 +oid sha256:e298244953653e46875053b12b4fe06ee692cb58fc131233ac4172677f0f8b44 +size 118961 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png index 1a69351b8..843c3ba3b 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:31464d796f1660bdd5c98cf73186d7b68918fd42f292bc03e274e43d995edc16 -size 51363 +oid sha256:6b9b36acf821cca71f97a3c8468fb925561f3bc2030742aef1e3c1d9e69ccc6f +size 51419 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png index 5c3621856..e738e22eb 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9dca12eb3b99976db20c77a6c540cda450e53f6ded89708d2e2320194723c0e2 -size 54569 +oid sha256:f5ad7a37546d48fc5426c32534a1c452fd0bf8280346dbe6e67ac26f17f3ba8a +size 54626 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png index c8c5c6899..d46e593ba 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:05a2778e7da867ab46a6760ea3925a2398c6b9a21d2767aef11cf98ef5292c82 -size 55034 +oid sha256:c0b61e9d1c2bcbf891a7acd4f3c1d2bd7524133d8165e7e7984998670de5a085 +size 55090 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png index 8bfc2f47a..31a1dd365 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:686e37635da6ba218c9539f8b145239bbed2bab6696384ed1cb725db657ec642 -size 35961 +oid sha256:a2e4975e9328a6d72f2c932daddfbb00cebdb2249aceb53f667d4060a1c0ea8a +size 36006 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png index 274e577d5..db20010e0 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:151171625b9cb8eaac3fb83260c6cc76cbf66003d9a940be1d5021a3303956c9 -size 35923 +oid sha256:ac6f9adeef92be9f69cb288ccafda8d522b8c3cde64352cd5369ae63668240c0 +size 35973 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png index 408cba0b8..e4d385fce 100644 --- a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:79d2935aa8f0f6941167b69840142599a2994a5eaa239757d91847d4d6533174 -size 64165 +oid sha256:c5a45307147f19f2d69a3de1f53e0a73ba4c3368eb25a66b4098fb54cb83822f +size 64203 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png index 9aa9c130a..102cb3650 100644 --- a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a5665e3a715ca7576df5b63af14b871f355d3e1db801c20089a60640373388ff -size 150095 +oid sha256:0102aa84db99a6da1db1de3abf67f13c3b571de00e79e7c55805dc0504658d50 +size 150111 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png index ef68941bc..091948af6 100644 --- a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bd31bddf25cf93d3ae79ce9b314cf3a3ebbf8c3b6cae2027f3f3b1593ac293e5 -size 59346 +oid sha256:3991cb1f922e0c6712d045b3cd8a1d98165c0fbef7e31b15d587f244e53ec04a +size 59343 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png index 67d8979ff..881f1b0d5 100644 --- a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:04dd62767ae9c18b5e89cea5bdd243b66c5986bfdb71fb9b01772ab9d150ab7e -size 145223 +oid sha256:355d8f08d08011635bf812aea1edeabd69e1ac3c724b521ed243f2b52e9b444b +size 145257 diff --git a/crates/egui_kittest/tests/snapshots/combobox_closed.png b/crates/egui_kittest/tests/snapshots/combobox_closed.png index 708985b14..073ae79a3 100644 --- a/crates/egui_kittest/tests/snapshots/combobox_closed.png +++ b/crates/egui_kittest/tests/snapshots/combobox_closed.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ca39801faddae7191ed054029263e8eca488d16e1fcbb40fed482d39fc89e8e -size 4520 +oid sha256:00fb02e0cc2c1454d3a3dc0635be24086234c2bc5e2c9fd73741b179622e16d6 +size 4514 diff --git a/crates/egui_kittest/tests/snapshots/combobox_opened.png b/crates/egui_kittest/tests/snapshots/combobox_opened.png index 53a9c8ed1..78e1baaca 100644 --- a/crates/egui_kittest/tests/snapshots/combobox_opened.png +++ b/crates/egui_kittest/tests/snapshots/combobox_opened.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bafe5d7129cd2137b8f7bc9662b894d959b7042c436443f835ecd421a0d9c33f -size 8019 +oid sha256:d8757e2db9a3892d9347495ad59f14d2bd9164a9ba258375a53c9faf8176b597 +size 8016 diff --git a/crates/egui_kittest/tests/snapshots/menu/closed_hovered.png b/crates/egui_kittest/tests/snapshots/menu/closed_hovered.png index d9d542908..a82442e1e 100644 --- a/crates/egui_kittest/tests/snapshots/menu/closed_hovered.png +++ b/crates/egui_kittest/tests/snapshots/menu/closed_hovered.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b6447dd4bd6489b9b909b8240abe54db16331beaa2fb4656f01f79a08fb25f9 -size 11112 +oid sha256:5f4a038f9acbb12880ba6b681ef7d3ae566045c4474aa31e7c6d746c39a649fc +size 11108 diff --git a/crates/egui_kittest/tests/snapshots/menu/opened.png b/crates/egui_kittest/tests/snapshots/menu/opened.png index 7f9b23b37..eb55bd894 100644 --- a/crates/egui_kittest/tests/snapshots/menu/opened.png +++ b/crates/egui_kittest/tests/snapshots/menu/opened.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4baf110e63fb104f30e9ae06e3601cfb48d7b69aec31636ca470c9ba9f6d44a9 -size 21659 +oid sha256:2965482e0161b4ea99aa5b4ece32261dbe246f86fe43054a754fbd556c7a5896 +size 21666 diff --git a/crates/egui_kittest/tests/snapshots/menu/submenu.png b/crates/egui_kittest/tests/snapshots/menu/submenu.png index 7c1823938..0a78e4e6c 100644 --- a/crates/egui_kittest/tests/snapshots/menu/submenu.png +++ b/crates/egui_kittest/tests/snapshots/menu/submenu.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93d2f336e216f371e7239b08113e659bad6c30eb299a2d8ea9537ae1c63533f0 -size 28503 +oid sha256:7592ca6213497f686d105a2e686d0c5de364388ddd174cbe8abb425d27ddcab0 +size 28505 diff --git a/crates/egui_kittest/tests/snapshots/menu/subsubmenu.png b/crates/egui_kittest/tests/snapshots/menu/subsubmenu.png index 7e8f3f8e7..84e6ba152 100644 --- a/crates/egui_kittest/tests/snapshots/menu/subsubmenu.png +++ b/crates/egui_kittest/tests/snapshots/menu/subsubmenu.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6bf27cde6a87112a1ae832a65c80a9e34243b6ee368314379f5bf018edf439e5 -size 33239 +oid sha256:3a1adf0903f0fc50323c2d77bbc491c950ab0dae6593c004770ea7961c2c6273 +size 33270 diff --git a/crates/egui_kittest/tests/snapshots/override_text_color_interactive.png b/crates/egui_kittest/tests/snapshots/override_text_color_interactive.png index fb8887d13..4d365c4c2 100644 --- a/crates/egui_kittest/tests/snapshots/override_text_color_interactive.png +++ b/crates/egui_kittest/tests/snapshots/override_text_color_interactive.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e8038005841dbf272375388b224dcc9fc1177b5c113d3e6f6dbc2265c88c7e60 -size 19704 +oid sha256:bf8177abaa5920e32ad4618f5296355377f475b0c0f9a95f75cbfbe468415fb8 +size 19696 diff --git a/crates/egui_kittest/tests/snapshots/readme_example.png b/crates/egui_kittest/tests/snapshots/readme_example.png index cb99dfc84..050a4a43e 100644 --- a/crates/egui_kittest/tests/snapshots/readme_example.png +++ b/crates/egui_kittest/tests/snapshots/readme_example.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e86ed66f3ac3a81998eefbed8cb231edc6522174050676b45e9248f9e7f18533 -size 2227 +oid sha256:1dd1f5013587463f002b1becac1560876c462295dbe5dfbb1a9dbce58991e53d +size 2209 diff --git a/crates/egui_kittest/tests/snapshots/test_masking.png b/crates/egui_kittest/tests/snapshots/test_masking.png index a397ceda6..5bf5dd6fa 100644 --- a/crates/egui_kittest/tests/snapshots/test_masking.png +++ b/crates/egui_kittest/tests/snapshots/test_masking.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4216258893fae554f0ab8b3a76ef0905cacb62c70af47fa811ff6f3d99f9f3ab -size 5619 +oid sha256:be0bd449166878ced27eff4966d1741731e926f9baabe8b590375c20103036dd +size 5527 diff --git a/crates/egui_kittest/tests/snapshots/test_shrink.png b/crates/egui_kittest/tests/snapshots/test_shrink.png index 40f2e284d..e4ff540f4 100644 --- a/crates/egui_kittest/tests/snapshots/test_shrink.png +++ b/crates/egui_kittest/tests/snapshots/test_shrink.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:21a92c29e27ef0fdec273ea2d94a2b3e74cdf380ec77f4783daeb008bd51db6d -size 2767 +oid sha256:888f8a4d995d718a9a158e563d8ac1434775660b33aebb5f34feea54ffd12600 +size 2830 diff --git a/crates/egui_kittest/tests/snapshots/test_tooltip_hidden.png b/crates/egui_kittest/tests/snapshots/test_tooltip_hidden.png index 40f2e284d..e4ff540f4 100644 --- a/crates/egui_kittest/tests/snapshots/test_tooltip_hidden.png +++ b/crates/egui_kittest/tests/snapshots/test_tooltip_hidden.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:21a92c29e27ef0fdec273ea2d94a2b3e74cdf380ec77f4783daeb008bd51db6d -size 2767 +oid sha256:888f8a4d995d718a9a158e563d8ac1434775660b33aebb5f34feea54ffd12600 +size 2830 diff --git a/crates/egui_kittest/tests/snapshots/test_tooltip_shown.png b/crates/egui_kittest/tests/snapshots/test_tooltip_shown.png index 86cc5a717..d6053700b 100644 --- a/crates/egui_kittest/tests/snapshots/test_tooltip_shown.png +++ b/crates/egui_kittest/tests/snapshots/test_tooltip_shown.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f9ca5f8081d677b8bff47813c4eb94319ca03855e780aed834ecc2f3d905a22c -size 4852 +oid sha256:037f3e356d32e1a2c32767460399f919452bff0933e1db7aa113e7e2bdb083f0 +size 4927 diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index 9b53e3301..0233c1c58 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -114,7 +114,7 @@ pub fn layout(fonts: &mut FontsImpl, pixels_per_point: f32, job: Arc) let intrinsic_size = calculate_intrinsic_size(point_scale, &job, ¶graphs); let mut elided = false; - let mut rows = rows_from_paragraphs(paragraphs, &job, &mut elided); + let mut rows = rows_from_paragraphs(paragraphs, &job, pixels_per_point, &mut elided); if elided && let Some(last_placed) = rows.last_mut() { let last_row = Arc::make_mut(&mut last_placed.row); replace_last_glyph_with_overflow_character(fonts, pixels_per_point, &job, last_row); @@ -254,11 +254,12 @@ fn calculate_intrinsic_size( ) -> Vec2 { let mut intrinsic_size = Vec2::ZERO; for (idx, paragraph) in paragraphs.iter().enumerate() { - let width = paragraph - .glyphs - .last() - .map(|l| l.max_x()) - .unwrap_or_default(); + // Use the precise cursor position instead of `last_glyph.max_x()`, + // because glyph positions are pixel-snapped but the cursor tracks + // the exact subpixel advance. This ensures that when two galleys are + // placed side-by-side, the gap matches what it would be within a + // single galley. + let width = paragraph.cursor_x_px / point_scale.pixels_per_point; intrinsic_size.x = f32::max(intrinsic_size.x, width); let mut height = paragraph @@ -279,6 +280,7 @@ fn calculate_intrinsic_size( fn rows_from_paragraphs( paragraphs: Vec, job: &LayoutJob, + pixels_per_point: f32, elided: &mut bool, ) -> Vec { let num_paragraphs = paragraphs.len(); @@ -305,8 +307,11 @@ fn rows_from_paragraphs( ends_with_newline: !is_last_paragraph, }); } else { - let paragraph_max_x = paragraph.glyphs.last().unwrap().max_x(); - if paragraph_max_x <= job.effective_wrap_width() { + // Use precise cursor position for width instead of pixel-snapped + // `last_glyph.max_x()`, so that side-by-side galleys have the same + // spacing as characters within a single galley. + let paragraph_width = paragraph.cursor_x_px / pixels_per_point; + if paragraph_width <= job.effective_wrap_width() { // Early-out optimization: the whole paragraph fits on one row. rows.push(PlacedRow { pos: pos2(0.0, f32::NAN), @@ -314,7 +319,7 @@ fn rows_from_paragraphs( section_index_at_start: paragraph.section_index_at_start, glyphs: paragraph.glyphs, visuals: Default::default(), - size: vec2(paragraph_max_x, 0.0), + size: vec2(paragraph_width, 0.0), }), ends_with_newline: !is_last_paragraph, }); diff --git a/tests/egui_tests/tests/snapshots/atom_letter_spacing.png b/tests/egui_tests/tests/snapshots/atom_letter_spacing.png new file mode 100644 index 000000000..89fba254e --- /dev/null +++ b/tests/egui_tests/tests/snapshots/atom_letter_spacing.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4aaf541ed0245777c802d31f01edb0cc4e53ebd2f4444e094336c180b98091d3 +size 2221 diff --git a/tests/egui_tests/tests/snapshots/grow_all.png b/tests/egui_tests/tests/snapshots/grow_all.png index 3e5208fe0..89b96aba7 100644 --- a/tests/egui_tests/tests/snapshots/grow_all.png +++ b/tests/egui_tests/tests/snapshots/grow_all.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b91ae9e626d885b049d80dc9421275e147f4a3501c21ff4740b0f59d9c2998b +oid sha256:83c3e19004462b793a5929f60f8b81a795c57529bfc74c6e87890aa4b9b8d939 size 13930 diff --git a/tests/egui_tests/tests/snapshots/layout/atoms_image.png b/tests/egui_tests/tests/snapshots/layout/atoms_image.png index 200ea6476..acfdb810c 100644 --- a/tests/egui_tests/tests/snapshots/layout/atoms_image.png +++ b/tests/egui_tests/tests/snapshots/layout/atoms_image.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2e236f71e26e1a96acf9cd135b5db3a9cb0df374b87c3e283023dd14df193411 -size 369870 +oid sha256:0f65b7221ac74991c526b68ad2469f42801f6083c9acead5bc923fd856a6311d +size 368614 diff --git a/tests/egui_tests/tests/snapshots/layout/atoms_minimal.png b/tests/egui_tests/tests/snapshots/layout/atoms_minimal.png index 3c982b37e..3e37969f7 100644 --- a/tests/egui_tests/tests/snapshots/layout/atoms_minimal.png +++ b/tests/egui_tests/tests/snapshots/layout/atoms_minimal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:096ec8246969f85cfa0cb8d58731be9aaf82b7dac70dc064ec999b1eed25e1ef -size 368552 +oid sha256:97b26c9abaf655fa5ef0625b8bc61042291a8ea18ecc89ea16abd3be6368c006 +size 367314 diff --git a/tests/egui_tests/tests/snapshots/layout/atoms_multi_grow.png b/tests/egui_tests/tests/snapshots/layout/atoms_multi_grow.png index 664e23a9b..54a8a3e1c 100644 --- a/tests/egui_tests/tests/snapshots/layout/atoms_multi_grow.png +++ b/tests/egui_tests/tests/snapshots/layout/atoms_multi_grow.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0813583ca9658b5f27f3585e59f829b71c86061619d7f61a16cc2ccf0906a322 -size 291213 +oid sha256:47b09261afe84892cdb169cb99ae59c49f671e68b3e99fc170e304de9b2bf526 +size 290633 diff --git a/tests/egui_tests/tests/snapshots/layout/button.png b/tests/egui_tests/tests/snapshots/layout/button.png index 21449927d..635858aba 100644 --- a/tests/egui_tests/tests/snapshots/layout/button.png +++ b/tests/egui_tests/tests/snapshots/layout/button.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e822c2324268d6e6168f9510aa1caec94df38dd0c163afcdecad11f2b1740936 -size 314449 +oid sha256:3cbc6f95073cbbb26729d287e5fe073c76e8bddee7eef95b431a873522234297 +size 313244 diff --git a/tests/egui_tests/tests/snapshots/layout/button_image.png b/tests/egui_tests/tests/snapshots/layout/button_image.png index 4ee6cffa2..79cda64a2 100644 --- a/tests/egui_tests/tests/snapshots/layout/button_image.png +++ b/tests/egui_tests/tests/snapshots/layout/button_image.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:682dd89e15ee289a87a592c93ac2b9ec3172cd4fedcc02072c0516a9ae9ecd64 -size 335687 +oid sha256:f89cc5b17821c9f30f7a086bb37668e4e7913705d42c0678fb0f42c527abb868 +size 334498 diff --git a/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png b/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png index 5b74267e1..b244a86dc 100644 --- a/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png +++ b/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e2d22c9e7fd701be1dc1581635cdfa2829e02db9c6f66bf54eac106ebd7344a3 -size 421041 +oid sha256:b7f87fb417453a98e7059535cb68b12549d65f8da7cedf7a48e7154686931e16 +size 419858 diff --git a/tests/egui_tests/tests/snapshots/layout/checkbox.png b/tests/egui_tests/tests/snapshots/layout/checkbox.png index c1e993885..766aedaca 100644 --- a/tests/egui_tests/tests/snapshots/layout/checkbox.png +++ b/tests/egui_tests/tests/snapshots/layout/checkbox.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ee91ad31d625930c55ae4ac41011f2018ef11ba20cefe5686b7338671fd6c32e -size 389522 +oid sha256:2f17fe1f7b2cccaa8991559218a7f13f61e459dc8443cf0fe2d24df7e9bd2eea +size 388959 diff --git a/tests/egui_tests/tests/snapshots/layout/checkbox_checked.png b/tests/egui_tests/tests/snapshots/layout/checkbox_checked.png index 4b972d966..8f79ec659 100644 --- a/tests/egui_tests/tests/snapshots/layout/checkbox_checked.png +++ b/tests/egui_tests/tests/snapshots/layout/checkbox_checked.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bcb5e0ec12a4bb7aba8ca8b53622fb2c204411ec66d7745bdb06e01bd1ffc731 -size 417596 +oid sha256:552a4d4933768ea1ee2323e7946f74f9ddd7e2f7b7c6d9f94bb92c8e7dd230a4 +size 416630 diff --git a/tests/egui_tests/tests/snapshots/layout/drag_value.png b/tests/egui_tests/tests/snapshots/layout/drag_value.png index 86dfef652..bfe289a61 100644 --- a/tests/egui_tests/tests/snapshots/layout/drag_value.png +++ b/tests/egui_tests/tests/snapshots/layout/drag_value.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96c78de8d82a5cb4e91912823b88bc0465bf67f09b500e5bde8f43b001f35a66 -size 264421 +oid sha256:339772a7974a2136b222697af2dd6e0202295d78e0720645204feb3c291481af +size 263181 diff --git a/tests/egui_tests/tests/snapshots/layout/radio.png b/tests/egui_tests/tests/snapshots/layout/radio.png index d3930768e..2fbd917a8 100644 --- a/tests/egui_tests/tests/snapshots/layout/radio.png +++ b/tests/egui_tests/tests/snapshots/layout/radio.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c15ece11f5c45d4bb89096a4d7146032e109fd9a099f2f37641e2676f7c3e184 -size 327971 +oid sha256:275c5358d3cfcbae7dfbeae4eac6606e2f394023837da492adc85934a972203e +size 325936 diff --git a/tests/egui_tests/tests/snapshots/layout/radio_checked.png b/tests/egui_tests/tests/snapshots/layout/radio_checked.png index c2d12eb98..e95932c0d 100644 --- a/tests/egui_tests/tests/snapshots/layout/radio_checked.png +++ b/tests/egui_tests/tests/snapshots/layout/radio_checked.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5942409a24177f84e067bcb488d8f976a0a6ad432f9f8603be2fdd4269d79efa -size 347946 +oid sha256:8bde6a904873ec2ffd7a194b820f3d76db5cacb3c266f3cb99f1c77ca2bd69fb +size 346473 diff --git a/tests/egui_tests/tests/snapshots/layout/selectable_value.png b/tests/egui_tests/tests/snapshots/layout/selectable_value.png index e2ea0c1f4..fd2daeeb0 100644 --- a/tests/egui_tests/tests/snapshots/layout/selectable_value.png +++ b/tests/egui_tests/tests/snapshots/layout/selectable_value.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2c082417d4f65be1efc6c040d2acaf02d899ceaa547ba86f530e1d2e94f4e385 -size 389160 +oid sha256:e0b87d78fce32144f1c694beb637461cb70b9127346c90d0276a877db0700291 +size 387935 diff --git a/tests/egui_tests/tests/snapshots/layout/selectable_value_selected.png b/tests/egui_tests/tests/snapshots/layout/selectable_value_selected.png index 2a2553a30..8ce768dae 100644 --- a/tests/egui_tests/tests/snapshots/layout/selectable_value_selected.png +++ b/tests/egui_tests/tests/snapshots/layout/selectable_value_selected.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7edb1db196e1a6c740503d976f5f8e4dd9d3d4dd07e8391ce77f01f411cae315 -size 402030 +oid sha256:77d4dd1a05771c25af933398d4f118e5e21a31b2e4db66161cf054fb1d7ebe24 +size 400911 diff --git a/tests/egui_tests/tests/snapshots/layout/slider.png b/tests/egui_tests/tests/snapshots/layout/slider.png index b7d9edcd5..83da462b7 100644 --- a/tests/egui_tests/tests/snapshots/layout/slider.png +++ b/tests/egui_tests/tests/snapshots/layout/slider.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e8bd1515d5c4045f4cd1b5d0c4f48469bd7e3ce738a95f741e9254e02ea28185 -size 276004 +oid sha256:b4071301c08f980ee26d914e4a4724b3f46f1113c62495483d9b0df980d8cbcd +size 274770 diff --git a/tests/egui_tests/tests/snapshots/layout/text_edit.png b/tests/egui_tests/tests/snapshots/layout/text_edit.png index 379b33806..cfbaefd41 100644 --- a/tests/egui_tests/tests/snapshots/layout/text_edit.png +++ b/tests/egui_tests/tests/snapshots/layout/text_edit.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:61dde59ee92a1c22aba7fd8decf62d88d1ed81c10cd969ce65c451185f7ca58b -size 221618 +oid sha256:30de3e9f9645206e33fa1edd841b48228e154d0ceae962c64c060a66eecd73ba +size 220452 diff --git a/tests/egui_tests/tests/snapshots/layout/text_edit_clip.png b/tests/egui_tests/tests/snapshots/layout/text_edit_clip.png index ccc29355f..8ea5d0b7a 100644 --- a/tests/egui_tests/tests/snapshots/layout/text_edit_clip.png +++ b/tests/egui_tests/tests/snapshots/layout/text_edit_clip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c2a7ad1a4568f0ed7f203453697982603fad8b7e9852b4193216ebff1624671d -size 384210 +oid sha256:a9f36b8623d2d9c35e337e973f547166f62a5daae757c462b1482babdd42c941 +size 383051 diff --git a/tests/egui_tests/tests/snapshots/layout/text_edit_no_clip.png b/tests/egui_tests/tests/snapshots/layout/text_edit_no_clip.png index 9ac2cefee..e65f04b1c 100644 --- a/tests/egui_tests/tests/snapshots/layout/text_edit_no_clip.png +++ b/tests/egui_tests/tests/snapshots/layout/text_edit_no_clip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:54a2f4004a71af18ffc42bba723a69855af4913ddedd8185688a59f9967e5a13 -size 509495 +oid sha256:aed677ddda9544258ddc58ed602655f6a62ab2d1d8342accd025593bbcb25e2f +size 506926 diff --git a/tests/egui_tests/tests/snapshots/layout/text_edit_placeholder_clip.png b/tests/egui_tests/tests/snapshots/layout/text_edit_placeholder_clip.png index e74e0f928..0a84f42db 100644 --- a/tests/egui_tests/tests/snapshots/layout/text_edit_placeholder_clip.png +++ b/tests/egui_tests/tests/snapshots/layout/text_edit_placeholder_clip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ab3a86f34c5cce033903cd67c1070dcc509e385e62e05358e1329968bfb1e95 -size 363693 +oid sha256:1a2734644b2fbb6f42ddab6c65a1f5d073f1f002900bbd814c1edb6184e0a9c0 +size 362521 diff --git a/tests/egui_tests/tests/snapshots/max_width.png b/tests/egui_tests/tests/snapshots/max_width.png index 6534961a8..be50f81db 100644 --- a/tests/egui_tests/tests/snapshots/max_width.png +++ b/tests/egui_tests/tests/snapshots/max_width.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea5546e2e72aa5181edfe260cf5b506a30fea8c3db049c080bafc303223ba95f +oid sha256:df3c1ba38afa30d22106d21a54621c28a0de2b98f77f4d7e398f09089286ef3e size 8367 diff --git a/tests/egui_tests/tests/snapshots/max_width_and_grow.png b/tests/egui_tests/tests/snapshots/max_width_and_grow.png index 54dddf7e8..d49489c41 100644 --- a/tests/egui_tests/tests/snapshots/max_width_and_grow.png +++ b/tests/egui_tests/tests/snapshots/max_width_and_grow.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d65a6c7e855a5476369422577d02f5e2a96814b100d7385f172fa9506189849 +oid sha256:f5919c35a3d736e0c432b3a94d6ab2a2f936f71852b94f2f95475fa6ab5281ad size 8369 diff --git a/tests/egui_tests/tests/snapshots/shrink_first_text.png b/tests/egui_tests/tests/snapshots/shrink_first_text.png index 81680a36a..a623d8b3b 100644 --- a/tests/egui_tests/tests/snapshots/shrink_first_text.png +++ b/tests/egui_tests/tests/snapshots/shrink_first_text.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77ff29a1441d11f3b13ddaf5f6dd5f2c5781bc418887e1c2eabe00679958cba6 +oid sha256:73b1cc01da110554dd41f4e5134f5d6d34b7e2079d5ac776f40980d616481ffc size 11448 diff --git a/tests/egui_tests/tests/snapshots/shrink_last_text.png b/tests/egui_tests/tests/snapshots/shrink_last_text.png index 6f7b28c16..cda1a2add 100644 --- a/tests/egui_tests/tests/snapshots/shrink_last_text.png +++ b/tests/egui_tests/tests/snapshots/shrink_last_text.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:23923d37e4dd848b043c7118e651ddade82c0df180652d8f0dcb829b1b6245d6 -size 12009 +oid sha256:00e129a40ea9815472ab9d823a1801fbdd268bd58745cad1c1c3dd91309c61fc +size 12010 diff --git a/tests/egui_tests/tests/snapshots/sides/default_long_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/default_long_fit_contents.png index 842f41171..534b55d92 100644 --- a/tests/egui_tests/tests/snapshots/sides/default_long_fit_contents.png +++ b/tests/egui_tests/tests/snapshots/sides/default_long_fit_contents.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3afbf9e4d598907f088d3f09b1cf2b70c682062f1f4b98aa98b997121f763040 -size 8802 +oid sha256:7d7c49df327cdea8cc7d6a0b7278a831574a38e8998dba0733fcae2fd44256a9 +size 8833 diff --git a/tests/egui_tests/tests/snapshots/sides/default_short_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/default_short_fit_contents.png index 099d55cb5..773b93629 100644 --- a/tests/egui_tests/tests/snapshots/sides/default_short_fit_contents.png +++ b/tests/egui_tests/tests/snapshots/sides/default_short_fit_contents.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0a38c58ae7a30256e9491bfeb1155f2df6bba2a656ed9611fa945cbe2ebdc43 -size 1242 +oid sha256:8284e4828d60a4d65e848d08a95f6fbf681b8632877e9e2961fdb7e4876ffe2c +size 1273 diff --git a/tests/egui_tests/tests/snapshots/sides/shrink_left_long_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/shrink_left_long_fit_contents.png index 842f41171..534b55d92 100644 --- a/tests/egui_tests/tests/snapshots/sides/shrink_left_long_fit_contents.png +++ b/tests/egui_tests/tests/snapshots/sides/shrink_left_long_fit_contents.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3afbf9e4d598907f088d3f09b1cf2b70c682062f1f4b98aa98b997121f763040 -size 8802 +oid sha256:7d7c49df327cdea8cc7d6a0b7278a831574a38e8998dba0733fcae2fd44256a9 +size 8833 diff --git a/tests/egui_tests/tests/snapshots/sides/shrink_left_short_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/shrink_left_short_fit_contents.png index 099d55cb5..773b93629 100644 --- a/tests/egui_tests/tests/snapshots/sides/shrink_left_short_fit_contents.png +++ b/tests/egui_tests/tests/snapshots/sides/shrink_left_short_fit_contents.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0a38c58ae7a30256e9491bfeb1155f2df6bba2a656ed9611fa945cbe2ebdc43 -size 1242 +oid sha256:8284e4828d60a4d65e848d08a95f6fbf681b8632877e9e2961fdb7e4876ffe2c +size 1273 diff --git a/tests/egui_tests/tests/snapshots/sides/shrink_right_long_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/shrink_right_long_fit_contents.png index 842f41171..534b55d92 100644 --- a/tests/egui_tests/tests/snapshots/sides/shrink_right_long_fit_contents.png +++ b/tests/egui_tests/tests/snapshots/sides/shrink_right_long_fit_contents.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3afbf9e4d598907f088d3f09b1cf2b70c682062f1f4b98aa98b997121f763040 -size 8802 +oid sha256:7d7c49df327cdea8cc7d6a0b7278a831574a38e8998dba0733fcae2fd44256a9 +size 8833 diff --git a/tests/egui_tests/tests/snapshots/sides/shrink_right_short_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/shrink_right_short_fit_contents.png index 099d55cb5..773b93629 100644 --- a/tests/egui_tests/tests/snapshots/sides/shrink_right_short_fit_contents.png +++ b/tests/egui_tests/tests/snapshots/sides/shrink_right_short_fit_contents.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0a38c58ae7a30256e9491bfeb1155f2df6bba2a656ed9611fa945cbe2ebdc43 -size 1242 +oid sha256:8284e4828d60a4d65e848d08a95f6fbf681b8632877e9e2961fdb7e4876ffe2c +size 1273 diff --git a/tests/egui_tests/tests/snapshots/sides/wrap_left_long_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/wrap_left_long_fit_contents.png index 842f41171..534b55d92 100644 --- a/tests/egui_tests/tests/snapshots/sides/wrap_left_long_fit_contents.png +++ b/tests/egui_tests/tests/snapshots/sides/wrap_left_long_fit_contents.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3afbf9e4d598907f088d3f09b1cf2b70c682062f1f4b98aa98b997121f763040 -size 8802 +oid sha256:7d7c49df327cdea8cc7d6a0b7278a831574a38e8998dba0733fcae2fd44256a9 +size 8833 diff --git a/tests/egui_tests/tests/snapshots/sides/wrap_left_short_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/wrap_left_short_fit_contents.png index 099d55cb5..773b93629 100644 --- a/tests/egui_tests/tests/snapshots/sides/wrap_left_short_fit_contents.png +++ b/tests/egui_tests/tests/snapshots/sides/wrap_left_short_fit_contents.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0a38c58ae7a30256e9491bfeb1155f2df6bba2a656ed9611fa945cbe2ebdc43 -size 1242 +oid sha256:8284e4828d60a4d65e848d08a95f6fbf681b8632877e9e2961fdb7e4876ffe2c +size 1273 diff --git a/tests/egui_tests/tests/snapshots/sides/wrap_right_long_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/wrap_right_long_fit_contents.png index 842f41171..534b55d92 100644 --- a/tests/egui_tests/tests/snapshots/sides/wrap_right_long_fit_contents.png +++ b/tests/egui_tests/tests/snapshots/sides/wrap_right_long_fit_contents.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3afbf9e4d598907f088d3f09b1cf2b70c682062f1f4b98aa98b997121f763040 -size 8802 +oid sha256:7d7c49df327cdea8cc7d6a0b7278a831574a38e8998dba0733fcae2fd44256a9 +size 8833 diff --git a/tests/egui_tests/tests/snapshots/sides/wrap_right_short_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/wrap_right_short_fit_contents.png index 099d55cb5..773b93629 100644 --- a/tests/egui_tests/tests/snapshots/sides/wrap_right_short_fit_contents.png +++ b/tests/egui_tests/tests/snapshots/sides/wrap_right_short_fit_contents.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0a38c58ae7a30256e9491bfeb1155f2df6bba2a656ed9611fa945cbe2ebdc43 -size 1242 +oid sha256:8284e4828d60a4d65e848d08a95f6fbf681b8632877e9e2961fdb7e4876ffe2c +size 1273 diff --git a/tests/egui_tests/tests/snapshots/size_max_size.png b/tests/egui_tests/tests/snapshots/size_max_size.png index 12b526287..499259fd4 100644 --- a/tests/egui_tests/tests/snapshots/size_max_size.png +++ b/tests/egui_tests/tests/snapshots/size_max_size.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f2d9b0884adb89f598dd0c7eb421c0c8e8bcdaa1cbca02f4646c777711a005c2 +oid sha256:11a987f7376f8a3174958a8c21bece8bfb7ec284077940d87038271717d2c397 size 8655 diff --git a/tests/egui_tests/tests/snapshots/text_edit_rtl_0.png b/tests/egui_tests/tests/snapshots/text_edit_rtl_0.png index 440e1e818..3b87786e8 100644 --- a/tests/egui_tests/tests/snapshots/text_edit_rtl_0.png +++ b/tests/egui_tests/tests/snapshots/text_edit_rtl_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ddd940c1ed581471b1448c04296d48b829939025980479a122edfe9b9bd0321e -size 2321 +oid sha256:ce357224c2e1cf32f96b3d075dc070c4d14e9aaca1b8165d0ba98603dff19c1b +size 2324 diff --git a/tests/egui_tests/tests/snapshots/text_edit_rtl_1.png b/tests/egui_tests/tests/snapshots/text_edit_rtl_1.png index d0f6cb316..5d9aada78 100644 --- a/tests/egui_tests/tests/snapshots/text_edit_rtl_1.png +++ b/tests/egui_tests/tests/snapshots/text_edit_rtl_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a53c074ad4cb1f0e38d6d7144cb661a2b68d809203bfca636ff5a60d8582a651 -size 2288 +oid sha256:d91c715ac66be329cac42ff7c7726348b0ac79d897c414bbde26bb0115781577 +size 2289 diff --git a/tests/egui_tests/tests/snapshots/text_edit_rtl_2.png b/tests/egui_tests/tests/snapshots/text_edit_rtl_2.png index e618bf8dd..cd6d5a621 100644 --- a/tests/egui_tests/tests/snapshots/text_edit_rtl_2.png +++ b/tests/egui_tests/tests/snapshots/text_edit_rtl_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0a9be0d364374237ea9c3cbfc3703f47f4345d81cecbdcf6c4b49688c4c282ad +oid sha256:5cfdd6255aba92f0253571bde4f22afc8e0fb5fe3cb882946459d623c0a5d89c size 2982 diff --git a/tests/egui_tests/tests/snapshots/visuals/button.png b/tests/egui_tests/tests/snapshots/visuals/button.png index ffe31f06a..e6978c70f 100644 --- a/tests/egui_tests/tests/snapshots/visuals/button.png +++ b/tests/egui_tests/tests/snapshots/visuals/button.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ec81a46d44402d5e709d825e42de99a2f7b9707f77dc5f94e277ae9fd77b6fae -size 10903 +oid sha256:9ed487544a84f9f128af550030bc7fe8a960bc70897b38f7c858440a42b6ce44 +size 11197 diff --git a/tests/egui_tests/tests/snapshots/visuals/button_image.png b/tests/egui_tests/tests/snapshots/visuals/button_image.png index 162712f09..eca582ec0 100644 --- a/tests/egui_tests/tests/snapshots/visuals/button_image.png +++ b/tests/egui_tests/tests/snapshots/visuals/button_image.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a6a3eb59ff208d106c3808265d4bef10d80b634f74d99476c3541d997b30bc56 -size 11967 +oid sha256:a2a017c2b93d1920ae85792c13eafa2fd43f93b2e3bbaa5981ed3a43050c0995 +size 11808 diff --git a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png index 4027ef68b..4848b0781 100644 --- a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png +++ b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:176df42437cc8f6f97bba4b0f9bea72c9359f69e66abfa289d0701814f8ad258 -size 14746 +oid sha256:42cbc8f8740f56ce45c356262d9b872e3973844ce552c6c09e3c07425c3f86b6 +size 14835 diff --git a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png index b742787c0..8c30d3145 100644 --- a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png +++ b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5003e67baed533e485448b953b616804047b9da25d2758b288c96e65d7f37b0f -size 14323 +oid sha256:11fdd4bde01102e7998defcaa80c1105ec9418152314c74ee028b692b26c6be8 +size 14407 diff --git a/tests/egui_tests/tests/snapshots/visuals/drag_value.png b/tests/egui_tests/tests/snapshots/visuals/drag_value.png index 463c4cbef..8be9c5e9f 100644 --- a/tests/egui_tests/tests/snapshots/visuals/drag_value.png +++ b/tests/egui_tests/tests/snapshots/visuals/drag_value.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0f6babaa4f9359517f58b1160a915069c56c338b7c0d8d4306cde67628442397 -size 8995 +oid sha256:71daf8a33d277075012bf1130d7820574fe0286080154810d8d398c005a65127 +size 9037 diff --git a/tests/egui_tests/tests/snapshots/visuals/radio.png b/tests/egui_tests/tests/snapshots/visuals/radio.png index 4da91f2b3..b35eb2d51 100644 --- a/tests/egui_tests/tests/snapshots/visuals/radio.png +++ b/tests/egui_tests/tests/snapshots/visuals/radio.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d54388fca5de500aafde4efc73df5cc15a72ce5443c9f527ffb70430c08e60e9 -size 11871 +oid sha256:4502cc58a4085d1e0f9945d0bd1d25adeefe71094ce94a210c57f113727f3a5a +size 11806 diff --git a/tests/egui_tests/tests/snapshots/visuals/radio_checked.png b/tests/egui_tests/tests/snapshots/visuals/radio_checked.png index f91c4a549..7bdae8cf1 100644 --- a/tests/egui_tests/tests/snapshots/visuals/radio_checked.png +++ b/tests/egui_tests/tests/snapshots/visuals/radio_checked.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8aec08c983f71cf4fa81e88ba1751de1253d9ba6d28692b824912ad4764471bc -size 12563 +oid sha256:cdedec6788b1a5026603322db9dd9f5fa398813d8aa2c56bc60acad390110501 +size 12499 diff --git a/tests/egui_tests/tests/snapshots/visuals/selectable_value.png b/tests/egui_tests/tests/snapshots/visuals/selectable_value.png index 88069ed9d..2f3192a74 100644 --- a/tests/egui_tests/tests/snapshots/visuals/selectable_value.png +++ b/tests/egui_tests/tests/snapshots/visuals/selectable_value.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:79f8fd0df269a45ee5a8cb6ddd1501a562e92de2bec15ab28016ceb2834c3c90 -size 13908 +oid sha256:23d1cddf87ea10d6735403ea0b2a16811d4f92246415633d393c991c3bfab2a1 +size 13716 diff --git a/tests/egui_tests/tests/snapshots/visuals/selectable_value_selected.png b/tests/egui_tests/tests/snapshots/visuals/selectable_value_selected.png index 2ab268abb..66f3df875 100644 --- a/tests/egui_tests/tests/snapshots/visuals/selectable_value_selected.png +++ b/tests/egui_tests/tests/snapshots/visuals/selectable_value_selected.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:966c55de5786c1ad0165e0cca29481d51770e23173b5dda14e40013669d3db6d -size 13889 +oid sha256:e8ac8bbdf9331dbe4244aa2964adf9f49ab8981b899aee9f3200b2799cdf7bc0 +size 13731 diff --git a/tests/egui_tests/tests/snapshots/visuals/text_edit_no_clip.png b/tests/egui_tests/tests/snapshots/visuals/text_edit_no_clip.png index dbfa8856a..f02b65693 100644 --- a/tests/egui_tests/tests/snapshots/visuals/text_edit_no_clip.png +++ b/tests/egui_tests/tests/snapshots/visuals/text_edit_no_clip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b1533e0ebaf0bd161e651d21ed81d36e8f0be06003357d9e5091ce2db4df5b7d -size 21517 +oid sha256:f6cf861a5c1682add50f9bdee4672e5fcaf882329566097faecab5312ac509b7 +size 21419 diff --git a/tests/egui_tests/tests/test_atoms.rs b/tests/egui_tests/tests/test_atoms.rs index 6f4b694e6..f6e9df14a 100644 --- a/tests/egui_tests/tests/test_atoms.rs +++ b/tests/egui_tests/tests/test_atoms.rs @@ -119,3 +119,24 @@ fn test_button_shortcut_text() { harness.snapshot("button_shortcut"); } + +/// Tests the spacing between galleys. +/// All of these should look the same. +#[test] +fn test_atom_letter_spacing() { + use egui::AtomLayout; + + let mut harness = HarnessBuilder::default().build_ui(|ui| { + ui.add(AtomLayout::new("1.00x").gap(0.0)); + ui.add(AtomLayout::new(("1.00", "x")).gap(0.0)); + ui.horizontal(|ui| { + ui.spacing_mut().item_spacing.x = 0.0; + ui.label("1.00"); + ui.label("x"); + }); + }); + harness.run(); + harness.fit_contents(); + + harness.snapshot("atom_letter_spacing"); +} From 5031c47cb27e2801d849eb04543eb61bcd79de55 Mon Sep 17 00:00:00 2001 From: shuppy Date: Mon, 16 Mar 2026 17:34:54 +0800 Subject: [PATCH 21/58] Update accesskit to 0.24.0 (and related deps) (#7850) this patch updates our deps to [accesskit 0.24.0](https://docs.rs/accesskit/0.24.0/accesskit/) (was [0.21.1](https://docs.rs/accesskit/0.21.1/accesskit/)), [accesskit_consumer 0.35.0](https://docs.rs/accesskit_consumer/0.35.0/accesskit_consumer/) (was [0.30.1](https://docs.rs/accesskit_consumer/0.30.1/accesskit_consumer/)), and [accesskit_winit 0.32.0](https://docs.rs/accesskit_winit/0.32.0/accesskit_winit/) (was [0.29.1](https://docs.rs/accesskit_winit/0.29.1/accesskit_winit/)), allowing egui to be used in apps that use accessibility subtrees (AccessKit/accesskit#655). for now, we handle the subtree-related breaking changes by assuming that egui will use [the root tree](https://docs.rs/accesskit/0.24.0/accesskit/struct.TreeId.html#associatedconstant.ROOT), which is good enough for [servoshell](https://github.com/servo/servo) and does not require any API changes. * [x] I have followed the instructions in the PR template image --------- Co-authored-by: Luke Warlow Co-authored-by: lucasmerlin Co-authored-by: Arnold Loubriat --- Cargo.lock | 225 ++++++++++++------ Cargo.toml | 8 +- crates/egui-winit/src/lib.rs | 2 +- crates/egui/src/context.rs | 1 + crates/egui/src/id.rs | 2 +- crates/egui/src/input_state/mod.rs | 6 +- crates/egui/src/memory/mod.rs | 6 +- .../egui/src/text_selection/accesskit_text.rs | 209 +++++++++++----- .../egui/src/text_selection/cursor_range.rs | 38 ++- .../src/accessibility_inspector.rs | 30 ++- crates/egui_kittest/src/node.rs | 28 ++- deny.toml | 1 + 12 files changed, 383 insertions(+), 173 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 62fd9b36c..1c22e9ddf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,47 +20,48 @@ checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" [[package]] name = "accesskit" -version = "0.21.1" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf203f9d3bd8f29f98833d1fbef628df18f759248a547e7e01cfbf63cda36a99" +checksum = "5351dcebb14b579ccab05f288596b2ae097005be7ee50a7c3d4ca9d0d5a66f6a" dependencies = [ "enumn", "serde", + "uuid", ] [[package]] name = "accesskit_atspi_common" -version = "0.14.1" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29f73a9b855b6f4af4962a94553ef0c092b80cf5e17038724d5e30945d036f69" +checksum = "842fd8203e6dfcf531d24f5bac792088edfba7d6b35844fead191603fb32a260" dependencies = [ "accesskit", "accesskit_consumer", "atspi-common", + "phf 0.13.1", "serde", - "thiserror 1.0.66", "zvariant", ] [[package]] name = "accesskit_consumer" -version = "0.30.1" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdd06f5fea9819250fffd4debf926709f3593ac22f8c1541a2573e5ee0ca01cd" +checksum = "53cf47daed85312e763fbf85ceca136e0d7abc68e0a7e12abe11f48172bc3b10" dependencies = [ "accesskit", - "hashbrown 0.15.2", + "hashbrown 0.16.1", ] [[package]] name = "accesskit_macos" -version = "0.22.1" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fbaf15815f39084e0cb24950c232f0e3634702c2dfbf182ae3b4919a4a1d45" +checksum = "534bc3fdc89a64a1db3c46b33c198fde2b7c3c7d094e5809c8c8bf2970c18243" dependencies = [ "accesskit", "accesskit_consumer", - "hashbrown 0.15.2", + "hashbrown 0.16.1", "objc2 0.5.2", "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", @@ -68,9 +69,9 @@ dependencies = [ [[package]] name = "accesskit_unix" -version = "0.17.1" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64926a930368d52d95422b822ede15014c04536cabaa2394f99567a1f4788dc6" +checksum = "90e549dd7c6562b6a2ea807b44726e6241707db054a817dc4c7e2b8d3b39bfac" dependencies = [ "accesskit", "accesskit_atspi_common", @@ -86,23 +87,23 @@ dependencies = [ [[package]] name = "accesskit_windows" -version = "0.29.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "792991159fa9ba57459de59e12e918bb90c5346fea7d40ac1a11f8632b41e63a" +checksum = "eff7009f1a532e917d66970a1e80c965140c6cfbbabbdde3d64e5431e6c78e21" dependencies = [ "accesskit", "accesskit_consumer", - "hashbrown 0.15.2", + "hashbrown 0.16.1", "static_assertions", - "windows 0.61.1", - "windows-core 0.61.0", + "windows 0.62.2", + "windows-core 0.62.2", ] [[package]] name = "accesskit_winit" -version = "0.29.1" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd9db0ea66997e3f4eae4a5f2c6b6486cf206642639ee629dbbb860ace1dec87" +checksum = "1fe9a94394896352cc4660ca2288bd4ef883d83238853c038b44070c8f134313" dependencies = [ "accesskit", "accesskit_macos", @@ -452,20 +453,19 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "atspi" -version = "0.25.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c83247582e7508838caf5f316c00791eee0e15c0bf743e6880585b867e16815c" +checksum = "c77886257be21c9cd89a4ae7e64860c6f0eefca799bb79127913052bd0eefb3d" dependencies = [ "atspi-common", - "atspi-connection", "atspi-proxies", ] [[package]] name = "atspi-common" -version = "0.9.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33dfc05e7cdf90988a197803bf24f5788f94f7c94a69efa95683e8ffe76cfdfb" +checksum = "20c5617155740c98003016429ad13fe43ce7a77b007479350a9f8bf95a29f63d" dependencies = [ "enumflags2", "serde", @@ -477,23 +477,11 @@ dependencies = [ "zvariant", ] -[[package]] -name = "atspi-connection" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4193d51303d8332304056ae0004714256b46b6635a5c556109b319c0d3784938" -dependencies = [ - "atspi-common", - "atspi-proxies", - "futures-lite", - "zbus", -] - [[package]] name = "atspi-proxies" -version = "0.9.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2eebcb9e7e76f26d0bcfd6f0295e1cd1e6f33bedbc5698a971db8dc43d7751c" +checksum = "2230e48787ed3eb4088996eab66a32ca20c0b67bbd4fd6cdfe79f04f1f04c9fc" dependencies = [ "atspi-common", "serde", @@ -2484,12 +2472,10 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "kittest" version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01fd6dd2cce251a360101038acb9334e3a50cd38cd02fefddbf28aa975f043c8" +source = "git+https://github.com/rerun-io/kittest?branch=main#ce7a2f3b12c36021889b50bdff671cec8016b0fb" dependencies = [ "accesskit", "accesskit_consumer", - "parking_lot", ] [[package]] @@ -2689,8 +2675,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1706dc14a2e140dec0a7a07109d9a3d5890b81e85bd6c60b906b249a77adf0ca" dependencies = [ "mime", - "phf", - "phf_shared", + "phf 0.11.3", + "phf_shared 0.11.3", "unicase", ] @@ -3269,8 +3255,19 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ - "phf_macros", - "phf_shared", + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros 0.13.1", + "phf_shared 0.13.1", + "serde", ] [[package]] @@ -3279,8 +3276,8 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" dependencies = [ - "phf_generator", - "phf_shared", + "phf_generator 0.11.3", + "phf_shared 0.11.3", ] [[package]] @@ -3289,24 +3286,47 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ - "phf_shared", + "phf_shared 0.11.3", "rand 0.8.5", ] +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared 0.13.1", +] + [[package]] name = "phf_macros" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" dependencies = [ - "phf_generator", - "phf_shared", + "phf_generator 0.11.3", + "phf_shared 0.11.3", "proc-macro2", "quote", "syn", "unicase", ] +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "phf_shared" version = "0.11.3" @@ -3317,6 +3337,15 @@ dependencies = [ "unicase", ] +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + [[package]] name = "pico-args" version = "0.5.0" @@ -4692,7 +4721,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d189085656ca1203291e965444e7f6a2723fbdd1dd9f34f8482e79bafd8338a0" dependencies = [ - "phf", + "phf 0.11.3", "unicode_names2_generator", ] @@ -5289,24 +5318,23 @@ dependencies = [ [[package]] name = "windows" -version = "0.61.1" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" dependencies = [ "windows-collections", - "windows-core 0.61.0", + "windows-core 0.62.2", "windows-future", - "windows-link 0.1.3", "windows-numerics", ] [[package]] name = "windows-collections" -version = "0.2.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" dependencies = [ - "windows-core 0.61.0", + "windows-core 0.62.2", ] [[package]] @@ -5328,21 +5356,35 @@ version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" dependencies = [ - "windows-implement 0.60.0", - "windows-interface 0.59.1", + "windows-implement 0.60.2", + "windows-interface 0.59.3", "windows-link 0.1.3", "windows-result 0.3.2", "windows-strings 0.4.0", ] [[package]] -name = "windows-future" -version = "0.2.0" +name = "windows-core" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-core 0.61.0", - "windows-link 0.1.3", + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading", ] [[package]] @@ -5358,9 +5400,9 @@ dependencies = [ [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", @@ -5380,9 +5422,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", @@ -5403,12 +5445,12 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-numerics" -version = "0.2.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" dependencies = [ - "windows-core 0.61.0", - "windows-link 0.1.3", + "windows-core 0.62.2", + "windows-link 0.2.1", ] [[package]] @@ -5429,6 +5471,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-strings" version = "0.1.0" @@ -5448,6 +5499,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -5541,6 +5601,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -5897,9 +5966,9 @@ dependencies = [ [[package]] name = "zbus-lockstep" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a22426b1bc2aca91de97772506f0655fa373448e6010d79d5d5880915c388409" +checksum = "6998de05217a084b7578728a9443d04ea4cd80f2a0839b8d78770b76ccd45863" dependencies = [ "zbus_xml", "zvariant", @@ -5907,9 +5976,9 @@ dependencies = [ [[package]] name = "zbus-lockstep-macros" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "100ffec29ed51859052f4563061abe35557acb56ba574510571f8398efc70a29" +checksum = "10da05367f3a7b7553c8cdf8fa91aee6b64afebe32b51c95177957efc47ca3a0" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index b10a4fd44..f083dd586 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,9 +68,9 @@ egui_glow = { version = "0.33.3", path = "crates/egui_glow", default-features = egui_kittest = { version = "0.33.3", path = "crates/egui_kittest", default-features = false } eframe = { version = "0.33.3", path = "crates/eframe", default-features = false } -accesskit = "0.21.1" -accesskit_consumer = "0.30.1" -accesskit_winit = "0.29.1" +accesskit = "0.24.0" +accesskit_consumer = "0.35.0" +accesskit_winit = "0.32.0" ahash = { version = "0.8.12", default-features = false, features = [ "no-rng", # we don't need DOS-protection, so we let users opt-in to it instead "std", @@ -148,6 +148,8 @@ wgpu = { version = "27.0.1", default-features = false, features = ["std"] } windows-sys = "0.61.2" winit = { version = "0.30.12", default-features = false } +[patch.crates-io] +kittest = { git = "https://github.com/rerun-io/kittest", branch = "main" } [workspace.lints.rust] unsafe_code = "deny" diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index c010febd5..243ed119a 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -102,7 +102,7 @@ pub struct State { has_sent_ime_enabled: bool, #[cfg(feature = "accesskit")] - accesskit: Option, + pub accesskit: Option, allow_ime: bool, ime_rect_px: Option, diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 55e3d75e1..3b36150e4 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -2616,6 +2616,7 @@ impl ContextImpl { platform_output.accesskit_update = Some(accesskit::TreeUpdate { nodes, tree: Some(accesskit::Tree::new(root_id)), + tree_id: accesskit::TreeId::ROOT, focus: focus_id, }); } diff --git a/crates/egui/src/id.rs b/crates/egui/src/id.rs index 7484930c8..661bdf2bf 100644 --- a/crates/egui/src/id.rs +++ b/crates/egui/src/id.rs @@ -79,7 +79,7 @@ impl Id { self.0.get() } - pub(crate) fn accesskit_id(&self) -> accesskit::NodeId { + pub fn accesskit_id(&self) -> accesskit::NodeId { self.value().into() } diff --git a/crates/egui/src/input_state/mod.rs b/crates/egui/src/input_state/mod.rs index 7122af699..b32a75c57 100644 --- a/crates/egui/src/input_state/mod.rs +++ b/crates/egui/src/input_state/mod.rs @@ -871,7 +871,8 @@ impl InputState { let accesskit_id = id.accesskit_id(); self.events.iter().filter_map(move |event| { if let Event::AccessKitActionRequest(request) = event - && request.target == accesskit_id + && request.target_node == accesskit_id + && request.target_tree == accesskit::TreeId::ROOT && request.action == action { return Some(request); @@ -888,7 +889,8 @@ impl InputState { let accesskit_id = id.accesskit_id(); self.events.retain(|event| { if let Event::AccessKitActionRequest(request) = event - && request.target == accesskit_id + && request.target_node == accesskit_id + && request.target_tree == accesskit::TreeId::ROOT { return !consume(request); } diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index fbc8e6f68..51ab2cde4 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -564,11 +564,13 @@ impl Focus { if let crate::Event::AccessKitActionRequest(accesskit::ActionRequest { action: accesskit::Action::Focus, - target, + target_node, + target_tree, data: None, }) = event + && *target_tree == accesskit::TreeId::ROOT { - self.id_requested_by_accesskit = Some(*target); + self.id_requested_by_accesskit = Some(*target_node); } } } diff --git a/crates/egui/src/text_selection/accesskit_text.rs b/crates/egui/src/text_selection/accesskit_text.rs index 974a334d0..650e7e5c0 100644 --- a/crates/egui/src/text_selection/accesskit_text.rs +++ b/crates/egui/src/text_selection/accesskit_text.rs @@ -4,6 +4,26 @@ use crate::{Context, Galley, Id}; use super::{CCursorRange, text_cursor_state::is_word_char}; +/// AccessKit's `word_starts` uses `u8` indices, so text runs cannot exceed this length. +pub(crate) const MAX_CHARS_PER_TEXT_RUN: usize = 255; + +/// Convert a (row, column) layout cursor position to a text run node ID and character index, +/// accounting for rows that are split into multiple text runs. +fn text_run_position(parent_id: Id, row: usize, column: usize) -> accesskit::TextPosition { + // When column lands exactly on a chunk boundary (e.g., 255), it refers to + // the end of the previous chunk, not the start of a new one. + let chunk_index = if column > 0 && column.is_multiple_of(MAX_CHARS_PER_TEXT_RUN) { + column / MAX_CHARS_PER_TEXT_RUN - 1 + } else { + column / MAX_CHARS_PER_TEXT_RUN + }; + let character_index = column - chunk_index * MAX_CHARS_PER_TEXT_RUN; + accesskit::TextPosition { + node: parent_id.with(row).with(chunk_index).accesskit_id(), + character_index, + } +} + /// Update accesskit with the current text state. pub fn update_accesskit_for_text_widget( ctx: &Context, @@ -20,14 +40,8 @@ pub fn update_accesskit_for_text_widget( let anchor = galley.layout_from_cursor(cursor_range.secondary); let focus = galley.layout_from_cursor(cursor_range.primary); builder.set_text_selection(accesskit::TextSelection { - anchor: accesskit::TextPosition { - node: parent_id.with(anchor.row).accesskit_id(), - character_index: anchor.column, - }, - focus: accesskit::TextPosition { - node: parent_id.with(focus.row).accesskit_id(), - character_index: focus.column, - }, + anchor: text_run_position(parent_id, anchor.row, anchor.column), + focus: text_run_position(parent_id, focus.row, focus.column), }); } @@ -40,61 +54,144 @@ pub fn update_accesskit_for_text_widget( return; }; + let mut prev_row_ended_with_newline = true; + for (row_index, row) in galley.rows.iter().enumerate() { - let row_id = parent_id.with(row_index); + let glyph_count = row.glyphs.len(); + let mut value = String::with_capacity(glyph_count); + let mut character_lengths = Vec::::with_capacity(glyph_count); + let mut character_positions = Vec::::with_capacity(glyph_count); + let mut character_widths = Vec::::with_capacity(glyph_count); + let mut word_starts = Vec::::new(); + // For soft-wrapped continuation rows, treat the start as a word + // boundary so the first word character gets a `word_starts` entry. + // Paragraph-starting runs (first row or after a newline) get an + // implicit word start from AccessKit, so they don't need this. + let mut was_at_word_end = !prev_row_ended_with_newline; - ctx.register_accesskit_parent(row_id, parent_id); + for glyph in &row.glyphs { + let is_word_char = is_word_char(glyph.chr); + if is_word_char && was_at_word_end { + word_starts.push(character_lengths.len()); + } + was_at_word_end = !is_word_char; + let old_len = value.len(); + value.push(glyph.chr); + character_lengths.push((value.len() - old_len) as _); + character_positions.push(glyph.pos.x - row.pos.x); + character_widths.push(glyph.advance_width); + } - ctx.accesskit_node_builder(row_id, |builder| { - builder.set_role(accesskit::Role::TextRun); - let rect = global_from_galley * row.rect_without_leading_space(); - builder.set_bounds(accesskit::Rect { - x0: rect.min.x.into(), - y0: rect.min.y.into(), - x1: rect.max.x.into(), - y1: rect.max.y.into(), - }); - builder.set_text_direction(accesskit::TextDirection::LeftToRight); - // TODO(mwcampbell): Set more node fields for the row - // once AccessKit adapters expose text formatting info. + if row.ends_with_newline { + value.push('\n'); + character_lengths.push(1); + character_positions.push(row.size.x); + character_widths.push(0.0); + } - let glyph_count = row.glyphs.len(); - let mut value = String::new(); - value.reserve(glyph_count); - let mut character_lengths = Vec::::with_capacity(glyph_count); - let mut character_positions = Vec::::with_capacity(glyph_count); - let mut character_widths = Vec::::with_capacity(glyph_count); - let mut word_lengths = Vec::::new(); - let mut was_at_word_end = false; - let mut last_word_start = 0usize; + let total_chars = character_lengths.len(); - for glyph in &row.glyphs { - let is_word_char = is_word_char(glyph.chr); - if is_word_char && was_at_word_end { - word_lengths.push((character_lengths.len() - last_word_start) as _); - last_word_start = character_lengths.len(); + if total_chars <= MAX_CHARS_PER_TEXT_RUN { + let run_id = parent_id.with(row_index).with(0usize); + ctx.register_accesskit_parent(run_id, parent_id); + + ctx.accesskit_node_builder(run_id, |builder| { + builder.set_role(accesskit::Role::TextRun); + builder.set_text_direction(accesskit::TextDirection::LeftToRight); + // TODO(mwcampbell): Set more node fields for the row + // once AccessKit adapters expose text formatting info. + + let rect = global_from_galley * row.rect_without_leading_space(); + builder.set_bounds(accesskit::Rect { + x0: rect.min.x.into(), + y0: rect.min.y.into(), + x1: rect.max.x.into(), + y1: rect.max.y.into(), + }); + builder.set_value(value); + builder.set_character_lengths(character_lengths); + + let pos_offset = character_positions.first().copied().unwrap_or(0.0); + for p in &mut character_positions { + *p -= pos_offset; } - was_at_word_end = !is_word_char; - let old_len = value.len(); - value.push(glyph.chr); - character_lengths.push((value.len() - old_len) as _); - character_positions.push(glyph.pos.x - row.pos.x); - character_widths.push(glyph.advance_width); - } + builder.set_character_positions(character_positions); + builder.set_character_widths(character_widths); - if row.ends_with_newline { - value.push('\n'); - character_lengths.push(1); - character_positions.push(row.size.x); - character_widths.push(0.0); - } - word_lengths.push((character_lengths.len() - last_word_start) as _); + let chunk_word_starts: Vec = word_starts.iter().map(|&ws| ws as u8).collect(); + builder.set_word_starts(chunk_word_starts); + }); + } else { + let num_chunks = total_chars.div_ceil(MAX_CHARS_PER_TEXT_RUN); + let mut byte_offset = 0usize; - builder.set_value(value); - builder.set_character_lengths(character_lengths); - builder.set_character_positions(character_positions); - builder.set_character_widths(character_widths); - builder.set_word_lengths(word_lengths); - }); + for chunk_idx in 0..num_chunks { + let char_start = chunk_idx * MAX_CHARS_PER_TEXT_RUN; + let char_end = (char_start + MAX_CHARS_PER_TEXT_RUN).min(total_chars); + + let byte_start = byte_offset; + let chunk_byte_len: usize = character_lengths[char_start..char_end] + .iter() + .map(|&l| l as usize) + .sum(); + let byte_end = byte_start + chunk_byte_len; + byte_offset = byte_end; + + let run_id = parent_id.with(row_index).with(chunk_idx); + ctx.register_accesskit_parent(run_id, parent_id); + + ctx.accesskit_node_builder(run_id, |builder| { + builder.set_role(accesskit::Role::TextRun); + builder.set_text_direction(accesskit::TextDirection::LeftToRight); + // TODO(mwcampbell): Set more node fields for the row + // once AccessKit adapters expose text formatting info. + + if chunk_idx > 0 { + let prev_id = parent_id.with(row_index).with(chunk_idx - 1); + builder.set_previous_on_line(prev_id.accesskit_id()); + } + if chunk_idx + 1 < num_chunks { + let next_id = parent_id.with(row_index).with(chunk_idx + 1); + builder.set_next_on_line(next_id.accesskit_id()); + } + + let row_rect = row.rect_without_leading_space(); + let chunk_x0 = row.pos.x + character_positions[char_start]; + let chunk_x1 = row.pos.x + + character_positions[char_end - 1] + + character_widths[char_end - 1]; + let chunk_rect = emath::Rect::from_min_max( + emath::pos2(chunk_x0, row_rect.min.y), + emath::pos2(chunk_x1, row_rect.max.y), + ); + let rect = global_from_galley * chunk_rect; + builder.set_bounds(accesskit::Rect { + x0: rect.min.x.into(), + y0: rect.min.y.into(), + x1: rect.max.x.into(), + y1: rect.max.y.into(), + }); + builder.set_value(value[byte_start..byte_end].to_owned()); + builder.set_character_lengths(character_lengths[char_start..char_end].to_vec()); + + let pos_offset = character_positions[char_start]; + let chunk_positions: Vec = character_positions[char_start..char_end] + .iter() + .map(|&p| p - pos_offset) + .collect(); + builder.set_character_positions(chunk_positions); + builder.set_character_widths(character_widths[char_start..char_end].to_vec()); + + let chunk_word_starts: Vec = word_starts + .iter() + .filter(|&&ws| ws >= char_start && ws < char_end) + .map(|&ws| (ws - char_start) as u8) + .collect(); + builder.set_word_starts(chunk_word_starts); + }); + } + } + + prev_row_ended_with_newline = row.ends_with_newline; } } diff --git a/crates/egui/src/text_selection/cursor_range.rs b/crates/egui/src/text_selection/cursor_range.rs index a816f5f26..f0445217f 100644 --- a/crates/egui/src/text_selection/cursor_range.rs +++ b/crates/egui/src/text_selection/cursor_range.rs @@ -192,10 +192,13 @@ impl CCursorRange { Event::AccessKitActionRequest(accesskit::ActionRequest { action: accesskit::Action::SetTextSelection, - target, + target_node, + target_tree, data: Some(accesskit::ActionData::SetTextSelection(selection)), }) => { - if _widget_id.accesskit_id() == *target { + if _widget_id.accesskit_id() == *target_node + && *target_tree == accesskit::TreeId::ROOT + { let primary = ccursor_from_accesskit_text_position(_widget_id, galley, &selection.focus); let secondary = @@ -224,18 +227,31 @@ fn ccursor_from_accesskit_text_position( galley: &Galley, position: &accesskit::TextPosition, ) -> Option { + use super::accesskit_text::MAX_CHARS_PER_TEXT_RUN; + let mut total_length = 0usize; for (i, row) in galley.rows.iter().enumerate() { - let row_id = id.with(i); - if row_id.accesskit_id() == position.node { - return Some(CCursor { - index: total_length + position.character_index, - prefer_next_row: !(position.character_index == row.glyphs.len() - && !row.ends_with_newline - && (i + 1) < galley.rows.len()), - }); + let row_chars = row.glyphs.len() + (row.ends_with_newline as usize); + let num_chunks = if row_chars == 0 { + 1 + } else { + row_chars.div_ceil(MAX_CHARS_PER_TEXT_RUN) + }; + + for chunk_idx in 0..num_chunks { + let run_id = id.with(i).with(chunk_idx); + if run_id.accesskit_id() == position.node { + let column = chunk_idx * MAX_CHARS_PER_TEXT_RUN + position.character_index; + return Some(CCursor { + index: total_length + column, + prefer_next_row: !(column == row.glyphs.len() + && !row.ends_with_newline + && (i + 1) < galley.rows.len()), + }); + } } - total_length += row.glyphs.len() + (row.ends_with_newline as usize); + + total_length += row_chars; } None } diff --git a/crates/egui_demo_app/src/accessibility_inspector.rs b/crates/egui_demo_app/src/accessibility_inspector.rs index 138c05ede..db34c85ac 100644 --- a/crates/egui_demo_app/src/accessibility_inspector.rs +++ b/crates/egui_demo_app/src/accessibility_inspector.rs @@ -1,7 +1,7 @@ use std::mem; -use accesskit::{Action, ActionRequest, NodeId}; -use accesskit_consumer::{FilterResult, Node, Tree, TreeChangeHandler}; +use accesskit::{Action, ActionRequest}; +use accesskit_consumer::{FilterResult, Node, NodeId, Tree, TreeChangeHandler}; use eframe::epaint::text::TextWrapMode; use egui::{ @@ -25,7 +25,7 @@ use egui::{ pub struct AccessibilityInspectorPlugin { pub open: bool, tree: Option, - selected_node: Option, + selected_node: Option, queued_action: Option, } @@ -113,13 +113,17 @@ impl AccessibilityInspectorPlugin { Id::new("Accessibility Inspector") } - fn selection_ui(&mut self, ui: &mut Ui, selected_node: Id) { + fn selection_ui(&mut self, ui: &mut Ui, selected_node: NodeId) { ui.separator(); if let Some(tree) = &self.tree - && let Some(node) = tree.state().node_by_id(NodeId::from(selected_node.value())) + && let Some(node) = tree.state().node_by_id(selected_node) { - let node_response = ui.ctx().read_response(selected_node); + // Safety: This is safe since the `accesskit::NodeId` was created from an `egui::Id`. + #[expect(unsafe_code)] + let egui_node_id = unsafe { Id::from_high_entropy_bits(node.locate().0.0) }; + + let node_response = ui.ctx().read_response(egui_node_id); if let Some(widget_response) = node_response { ui.debug_painter().debug_rect( @@ -174,8 +178,10 @@ impl AccessibilityInspectorPlugin { if node.supports_action(action, &|_node| FilterResult::Include) && ui.button(format!("{action:?}")).clicked() { + let (target_node, target_tree) = node.locate(); let action_request = ActionRequest { - target: node.id(), + target_node, + target_tree, action, data: None, }; @@ -188,8 +194,8 @@ impl AccessibilityInspectorPlugin { } } - fn node_ui(ui: &mut Ui, node: &Node<'_>, selected_node: &mut Option) { - if node.id() == Self::id().value().into() + fn node_ui(ui: &mut Ui, node: &Node<'_>, selected_node: &mut Option) { + if node.locate() == (Self::id().value().into(), accesskit::TreeId::ROOT) || node .value() .as_deref() @@ -200,12 +206,12 @@ impl AccessibilityInspectorPlugin { let label = node .label() .or_else(|| node.value()) - .unwrap_or_else(|| node.id().0.to_string()); + .unwrap_or_else(|| node.locate().0.0.to_string()); let label = format!("({:?}) {}", node.role(), label); // Safety: This is safe since the `accesskit::NodeId` was created from an `egui::Id`. #[expect(unsafe_code)] - let egui_node_id = unsafe { Id::from_high_entropy_bits(node.id().0) }; + let egui_node_id = unsafe { Id::from_high_entropy_bits(node.locate().0.0) }; ui.push_id(node.id(), |ui| { let child_count = node.children().len(); @@ -228,7 +234,7 @@ impl AccessibilityInspectorPlugin { collapsing.set_open(!collapsing.is_open()); } let label_response = - ui.selectable_value(selected_node, Some(egui_node_id), label.clone()); + ui.selectable_value(selected_node, Some(node.id()), label.clone()); if label_response.hovered() { let widget_response = ui.ctx().read_response(egui_node_id); diff --git a/crates/egui_kittest/src/node.rs b/crates/egui_kittest/src/node.rs index 94940ffff..ed68d50ee 100644 --- a/crates/egui_kittest/src/node.rs +++ b/crates/egui_kittest/src/node.rs @@ -98,9 +98,11 @@ impl Node<'_> { /// This will trigger a [`accesskit::Action::Click`] action. /// In contrast to `click()`, this can also click widgets that are not currently visible. pub fn click_accesskit(&self) { + let (target_node, target_tree) = self.accesskit_node.locate(); self.event(egui::Event::AccessKitActionRequest( accesskit::ActionRequest { - target: self.accesskit_node.id(), + target_node, + target_tree, action: accesskit::Action::Click, data: None, }, @@ -119,9 +121,11 @@ impl Node<'_> { } pub fn focus(&self) { + let (target_node, target_tree) = self.accesskit_node.locate(); self.event(egui::Event::AccessKitActionRequest(ActionRequest { action: accesskit::Action::Focus, - target: self.accesskit_node.id(), + target_node, + target_tree, data: None, })); } @@ -162,45 +166,55 @@ impl Node<'_> { /// Scroll the node into view. pub fn scroll_to_me(&self) { + let (target_node, target_tree) = self.accesskit_node.locate(); self.event(egui::Event::AccessKitActionRequest(ActionRequest { action: accesskit::Action::ScrollIntoView, - target: self.accesskit_node.id(), + target_node, + target_tree, data: None, })); } /// Scroll the [`egui::ScrollArea`] containing this node down (100px). pub fn scroll_down(&self) { + let (target_node, target_tree) = self.accesskit_node.locate(); self.event(egui::Event::AccessKitActionRequest(ActionRequest { action: accesskit::Action::ScrollDown, - target: self.accesskit_node.id(), + target_node, + target_tree, data: None, })); } /// Scroll the [`egui::ScrollArea`] containing this node up (100px). pub fn scroll_up(&self) { + let (target_node, target_tree) = self.accesskit_node.locate(); self.event(egui::Event::AccessKitActionRequest(ActionRequest { action: accesskit::Action::ScrollUp, - target: self.accesskit_node.id(), + target_node, + target_tree, data: None, })); } /// Scroll the [`egui::ScrollArea`] containing this node left (100px). pub fn scroll_left(&self) { + let (target_node, target_tree) = self.accesskit_node.locate(); self.event(egui::Event::AccessKitActionRequest(ActionRequest { action: accesskit::Action::ScrollLeft, - target: self.accesskit_node.id(), + target_node, + target_tree, data: None, })); } /// Scroll the [`egui::ScrollArea`] containing this node right (100px). pub fn scroll_right(&self) { + let (target_node, target_tree) = self.accesskit_node.locate(); self.event(egui::Event::AccessKitActionRequest(ActionRequest { action: accesskit::Action::ScrollRight, - target: self.accesskit_node.id(), + target_node, + target_tree, data: None, })); } diff --git a/deny.toml b/deny.toml index e740494fb..845aa847c 100644 --- a/deny.toml +++ b/deny.toml @@ -64,6 +64,7 @@ skip-tree = [ { name = "hashbrown" }, # wgpu's naga depends on 0.16, accesskit depends on 0.15 { name = "rfd" }, # example dependency { name = "windows" }, # the ecosystem is currently transitioning from 0.58 to 0.61 + { name = "phf" }, # mime_guess2, unicode_names2 -> 0.11.3; accesskit -> 0.13.1 ] From 41b8f5f4e773543451a99e0c82af2426ecc19c76 Mon Sep 17 00:00:00 2001 From: SuchAFuriousDeath <48620541+SuchAFuriousDeath@users.noreply.github.com> Date: Mon, 16 Mar 2026 11:56:07 +0100 Subject: [PATCH 22/58] Update wgpu to 28.0.0 (#7853) Co-authored-by: lucasmerlin --- Cargo.lock | 176 +++++------------- Cargo.toml | 2 +- crates/eframe/src/web/web_painter_wgpu.rs | 17 +- crates/egui-wgpu/src/capture.rs | 3 +- crates/egui-wgpu/src/lib.rs | 13 +- crates/egui-wgpu/src/renderer.rs | 4 +- crates/egui-wgpu/src/winit.rs | 12 +- .../egui_demo_app/src/apps/custom3d_wgpu.rs | 4 +- crates/egui_demo_app/src/backend_panel.rs | 17 ++ deny.toml | 2 +- 10 files changed, 102 insertions(+), 148 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1c22e9ddf..800618e4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -95,7 +95,7 @@ dependencies = [ "accesskit_consumer", "hashbrown 0.16.1", "static_assertions", - "windows 0.62.2", + "windows", "windows-core 0.62.2", ] @@ -151,6 +151,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android-activity" version = "0.6.0" @@ -1994,35 +2000,18 @@ dependencies = [ "gl_generator", ] -[[package]] -name = "gpu-alloc" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" -dependencies = [ - "bitflags 2.9.4", - "gpu-alloc-types", -] - -[[package]] -name = "gpu-alloc-types" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" -dependencies = [ - "bitflags 2.9.4", -] - [[package]] name = "gpu-allocator" -version = "0.27.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd" +checksum = "51255ea7cfaadb6c5f1528d43e92a82acb2b96c43365989a28b2d44ee38f8795" dependencies = [ + "ash", + "hashbrown 0.16.1", "log", "presser", - "thiserror 1.0.66", - "windows 0.58.0", + "thiserror 2.0.17", + "windows", ] [[package]] @@ -2071,6 +2060,8 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ + "allocator-api2", + "equivalent", "foldhash 0.2.0", ] @@ -2151,7 +2142,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.61.0", + "windows-core 0.61.2", ] [[package]] @@ -2640,9 +2631,9 @@ dependencies = [ [[package]] name = "metal" -version = "0.32.0" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00c15a6f673ff72ddcc22394663290f870fb224c1bfce55734a75c414150e605" +checksum = "c7047791b5bc903b8cd963014b355f71dc9864a9a0b727057676c1dcae5cbc15" dependencies = [ "bitflags 2.9.4", "block", @@ -2717,9 +2708,9 @@ dependencies = [ [[package]] name = "naga" -version = "27.0.0" +version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12b2e757b11b47345d44e7760e45458339bc490463d9548cd8651c53ae523153" +checksum = "618f667225063219ddfc61251087db8a9aec3c3f0950c916b614e403486f1135" dependencies = [ "arrayvec", "bit-set", @@ -5116,12 +5107,13 @@ checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" [[package]] name = "wgpu" -version = "27.0.1" +version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe68bac7cde125de7a731c3400723cadaaf1703795ad3f4805f187459cd7a77" +checksum = "f9cb534d5ffd109c7d1135f34cdae29e60eab94855a625dcfe1705f8bc7ad79f" dependencies = [ "arrayvec", "bitflags 2.9.4", + "bytemuck", "cfg-if", "cfg_aliases", "document-features", @@ -5145,9 +5137,9 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "27.0.0" +version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "893764e276cdafec946c7f394f044e283bc8f1e445ab3fea8ad3b6dbc10c0322" +checksum = "8bb4c8b5db5f00e56f1f08869d870a0dff7c8bc7ebc01091fec140b0cf0211a9" dependencies = [ "arrayvec", "bit-set", @@ -5178,45 +5170,45 @@ dependencies = [ [[package]] name = "wgpu-core-deps-apple" -version = "27.0.0" +version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0772ae958e9be0c729561d5e3fd9a19679bcdfb945b8b1a1969d9bfe8056d233" +checksum = "87b7b696b918f337c486bf93142454080a32a37832ba8a31e4f48221890047da" dependencies = [ "wgpu-hal", ] [[package]] name = "wgpu-core-deps-emscripten" -version = "27.0.0" +version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06ac3444a95b0813ecfd81ddb2774b66220b264b3e2031152a4a29fda4da6b5" +checksum = "34b251c331f84feac147de3c4aa3aa45112622a95dd7ee1b74384fa0458dbd79" dependencies = [ "wgpu-hal", ] [[package]] name = "wgpu-core-deps-wasm" -version = "27.0.0" +version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b1027dcf3b027a877e44819df7ceb0e2e98578830f8cd34cd6c3c7c2a7a50b7" +checksum = "12a2cf578ce8d7d50d0e63ddc2345c7dcb599f6eb90b888813406ea78b9b7010" dependencies = [ "wgpu-hal", ] [[package]] name = "wgpu-core-deps-windows-linux-android" -version = "27.0.0" +version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71197027d61a71748e4120f05a9242b2ad142e3c01f8c1b47707945a879a03c3" +checksum = "68ca976e72b2c9964eb243e281f6ce7f14a514e409920920dcda12ae40febaae" dependencies = [ "wgpu-hal", ] [[package]] name = "wgpu-hal" -version = "27.0.0" +version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a753c3dc95e69be3aacfe9c871c5fa2cfa9e35748cdc87de7ba5fc1735b61604" +checksum = "293080d77fdd14d6b08a67c5487dfddbf874534bb7921526db56a7b75d7e3bef" dependencies = [ "android_system_properties", "arrayvec", @@ -5230,7 +5222,6 @@ dependencies = [ "core-graphics-types 0.2.0", "glow", "glutin_wgl_sys", - "gpu-alloc", "gpu-allocator", "gpu-descriptor", "hashbrown 0.16.1", @@ -5257,21 +5248,20 @@ dependencies = [ "wasm-bindgen", "web-sys", "wgpu-types", - "windows 0.58.0", - "windows-core 0.58.0", + "windows", + "windows-core 0.62.2", ] [[package]] name = "wgpu-types" -version = "27.0.0" +version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d67453b02f7adc33c452d17da1c2cad813448221df1547bce9dd4b02d3558538" +checksum = "e18308757e594ed2cd27dddbb16a139c42a683819d32a2e0b1b0167552f5840c" dependencies = [ "bitflags 2.9.4", "bytemuck", "js-sys", "log", - "thiserror 2.0.17", "web-sys", ] @@ -5306,16 +5296,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" -dependencies = [ - "windows-core 0.58.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows" version = "0.62.2" @@ -5339,28 +5319,15 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.58.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-implement 0.58.0", - "windows-interface 0.58.0", - "windows-result 0.2.0", - "windows-strings 0.1.0", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-core" -version = "0.61.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" -dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", + "windows-implement", + "windows-interface", "windows-link 0.1.3", - "windows-result 0.3.2", - "windows-strings 0.4.0", + "windows-result 0.3.4", + "windows-strings 0.4.2", ] [[package]] @@ -5369,8 +5336,8 @@ version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", + "windows-implement", + "windows-interface", "windows-link 0.2.1", "windows-result 0.4.1", "windows-strings 0.5.1", @@ -5387,17 +5354,6 @@ dependencies = [ "windows-threading", ] -[[package]] -name = "windows-implement" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "windows-implement" version = "0.60.2" @@ -5409,17 +5365,6 @@ dependencies = [ "syn", ] -[[package]] -name = "windows-interface" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "windows-interface" version = "0.59.3" @@ -5455,18 +5400,9 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.2.0" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-result" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ "windows-link 0.1.3", ] @@ -5482,19 +5418,9 @@ dependencies = [ [[package]] name = "windows-strings" -version = "0.1.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" -dependencies = [ - "windows-result 0.2.0", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-strings" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ "windows-link 0.1.3", ] diff --git a/Cargo.toml b/Cargo.toml index f083dd586..d12b040fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -144,7 +144,7 @@ wayland-cursor = { version = "0.31.11", default-features = false } web-sys = "0.3.77" web-time = "1.1.0" # Timekeeping for native and web webbrowser = "1.0.5" -wgpu = { version = "27.0.1", default-features = false, features = ["std"] } +wgpu = { version = "28.0.0", default-features = false, features = ["std"] } windows-sys = "0.61.2" winit = { version = "0.30.12", default-features = false } diff --git a/crates/eframe/src/web/web_painter_wgpu.rs b/crates/eframe/src/web/web_painter_wgpu.rs index 264ce6adc..f7adb8fbb 100644 --- a/crates/eframe/src/web/web_painter_wgpu.rs +++ b/crates/eframe/src/web/web_painter_wgpu.rs @@ -268,6 +268,7 @@ impl WebPainter for WebPainterWgpu { label: Some("egui_render"), occlusion_query_set: None, timestamp_writes: None, + multiview_mask: None, }); // Forgetting the pass' lifetime means that we are no longer compile-time protected from @@ -280,15 +281,13 @@ impl WebPainter for WebPainterWgpu { ); } - let mut capture_buffer = None; - - if capture && let Some(capture_state) = &mut self.screen_capture_state { - capture_buffer = Some(capture_state.copy_textures( - &render_state.device, - &output_frame, - &mut encoder, - )); - } + let capture_buffer = if capture + && let Some(capture_state) = &mut self.screen_capture_state + { + Some(capture_state.copy_textures(&render_state.device, &output_frame, &mut encoder)) + } else { + None + }; Some((output_frame, capture_buffer)) }; diff --git a/crates/egui-wgpu/src/capture.rs b/crates/egui-wgpu/src/capture.rs index 58407fdd6..7eb1bd3ca 100644 --- a/crates/egui-wgpu/src/capture.rs +++ b/crates/egui-wgpu/src/capture.rs @@ -47,7 +47,7 @@ impl CaptureState { }, depth_stencil: None, multisample: MultisampleState::default(), - multiview: None, + multiview_mask: None, cache: None, }); @@ -165,6 +165,7 @@ impl CaptureState { depth_stencil_attachment: None, occlusion_query_set: None, timestamp_writes: None, + multiview_mask: None, }); pass.set_pipeline(&self.pipeline); diff --git a/crates/egui-wgpu/src/lib.rs b/crates/egui-wgpu/src/lib.rs index 880ab8f4a..46becf8f7 100644 --- a/crates/egui-wgpu/src/lib.rs +++ b/crates/egui-wgpu/src/lib.rs @@ -185,7 +185,7 @@ impl RenderState { wgpu::Backends::all() }; - instance.enumerate_adapters(backends) + instance.enumerate_adapters(backends).await }; let (adapter, device, queue) = match config.wgpu_setup.clone() { @@ -395,6 +395,10 @@ pub fn adapter_info_summary(info: &wgpu::AdapterInfo) -> String { driver, driver_info, backend, + device_pci_bus_id, + subgroup_min_size, + subgroup_max_size, + transient_saves_memory, } = &info; // Example values: @@ -426,6 +430,13 @@ pub fn adapter_info_summary(info: &wgpu::AdapterInfo) -> String { if *device != 0 { summary += &format!(", device: 0x{device:02X}"); } + if !device_pci_bus_id.is_empty() { + summary += &format!(", pci_bus_id: {device_pci_bus_id:?}"); + } + if *subgroup_min_size != 0 || *subgroup_max_size != 0 { + summary += &format!(", subgroup_size: {subgroup_min_size}..={subgroup_max_size}"); + } + summary += &format!(", transient_saves_memory: {transient_saves_memory}"); summary } diff --git a/crates/egui-wgpu/src/renderer.rs b/crates/egui-wgpu/src/renderer.rs index d3d21f19c..c37802448 100644 --- a/crates/egui-wgpu/src/renderer.rs +++ b/crates/egui-wgpu/src/renderer.rs @@ -353,7 +353,7 @@ impl Renderer { let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("egui_pipeline_layout"), bind_group_layouts: &[&uniform_bind_group_layout, &texture_bind_group_layout], - push_constant_ranges: &[], + immediate_size: 0, }); let depth_stencil = options @@ -426,7 +426,7 @@ impl Renderer { })], compilation_options: wgpu::PipelineCompilationOptions::default() }), - multiview: None, + multiview_mask: None, cache: None, } ) diff --git a/crates/egui-wgpu/src/winit.rs b/crates/egui-wgpu/src/winit.rs index 869c13259..5fb8d123a 100644 --- a/crates/egui-wgpu/src/winit.rs +++ b/crates/egui-wgpu/src/winit.rs @@ -362,14 +362,13 @@ impl Painter { #[cfg(all(target_os = "macos", feature = "macos-window-resize-jitter-fix"))] { // SAFETY: The cast is checked with if condition. If the used backend is not metal - // it gracefully fails. The pointer casts are valid as it's 1-to-1 type mapping. - // This is how wgpu currently exposes this backend-specific flag. + // it gracefully fails. unsafe { if let Some(hal_surface) = state.surface.as_hal::() { - let raw = - std::ptr::from_ref::(&*hal_surface).cast_mut(); - - (*raw).present_with_transaction = resizing; + hal_surface + .render_layer() + .lock() + .set_presents_with_transaction(resizing); Self::configure_surface( state, @@ -582,6 +581,7 @@ impl Painter { }), timestamp_writes: None, occlusion_query_set: None, + multiview_mask: None, }); // Forgetting the pass' lifetime means that we are no longer compile-time protected from diff --git a/crates/egui_demo_app/src/apps/custom3d_wgpu.rs b/crates/egui_demo_app/src/apps/custom3d_wgpu.rs index c88f7638c..fd1d9ae73 100644 --- a/crates/egui_demo_app/src/apps/custom3d_wgpu.rs +++ b/crates/egui_demo_app/src/apps/custom3d_wgpu.rs @@ -41,7 +41,7 @@ impl Custom3d { let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("custom3d"), bind_group_layouts: &[&bind_group_layout], - push_constant_ranges: &[], + immediate_size: 0, }); let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { @@ -62,7 +62,7 @@ impl Custom3d { primitive: wgpu::PrimitiveState::default(), depth_stencil: None, multisample: wgpu::MultisampleState::default(), - multiview: None, + multiview_mask: None, cache: None, }); diff --git a/crates/egui_demo_app/src/backend_panel.rs b/crates/egui_demo_app/src/backend_panel.rs index d3953320a..dfc4d116b 100644 --- a/crates/egui_demo_app/src/backend_panel.rs +++ b/crates/egui_demo_app/src/backend_panel.rs @@ -219,6 +219,10 @@ fn integration_ui(ui: &mut egui::Ui, _frame: &mut eframe::Frame) { driver, driver_info, backend, + device_pci_bus_id, + subgroup_min_size, + subgroup_max_size, + transient_saves_memory, } = &info; // Example values: @@ -261,6 +265,19 @@ fn integration_ui(ui: &mut egui::Ui, _frame: &mut eframe::Frame) { ui.label(format!("0x{device:02X}")); ui.end_row(); } + if !device_pci_bus_id.is_empty() { + ui.label("PCI Bus ID:"); + ui.label(device_pci_bus_id.as_str()); + ui.end_row(); + } + if *subgroup_min_size != 0 || *subgroup_max_size != 0 { + ui.label("Subgroup size:"); + ui.label(format!("{subgroup_min_size}..={subgroup_max_size}")); + ui.end_row(); + } + ui.label("Transient saves memory:"); + ui.label(format!("{transient_saves_memory}")); + ui.end_row(); }); }; diff --git a/deny.toml b/deny.toml index 845aa847c..01377b90b 100644 --- a/deny.toml +++ b/deny.toml @@ -58,13 +58,13 @@ skip = [ { name = "thiserror" }, # ecosystem is in the process of migrating from 1.x to 2.x { name = "thiserror-impl" }, # same as above { name = "toml_datetime" }, # required while eco-system updates to toml 1.0 - { name = "windows-sys" }, # mostly hopeless to avoid ] skip-tree = [ { name = "hashbrown" }, # wgpu's naga depends on 0.16, accesskit depends on 0.15 { name = "rfd" }, # example dependency { name = "windows" }, # the ecosystem is currently transitioning from 0.58 to 0.61 { name = "phf" }, # mime_guess2, unicode_names2 -> 0.11.3; accesskit -> 0.13.1 + { name = "windows-sys" }, # mostly hopeless to avoid ] From f32727ddca01a033a7af072ac1a5d504b9178c05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uma=C4=B5o?= <107099960+umajho@users.noreply.github.com> Date: Tue, 17 Mar 2026 03:09:07 +0800 Subject: [PATCH 23/58] Improve IME, and restrict mac-specific workaround (#7973) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Closes N/A * [x] I have followed the instructions in the PR template My PR that fixes the macOS backspacing issue (#7810) unfortunately breaks text selection on Wayland (Fedora KDE Plasma Desktop 43 [Wayland, with or without IBus]). I had actually tested on a Wayland setup but failed to notice that :( Windows and Linux+X11 (Debian 13 [Cinnamon 6.4.10 + X11 + fcitx5 5.1.2]) are not affected. This PR fixes the issue by restricting the macOS fix to macOS-only.

Here is the correct behavior on Wayland after this PR (and before #7810 is applied) ![2026-03-13 5 25 24β€―PM](https://github.com/user-attachments/assets/3b0831c1-1d96-4003-9109-4bfe68e06d40)
Here is the buggy behavior on Wayland before this PR ![2026-03-13 5 31 58β€―PM](https://github.com/user-attachments/assets/c6d69382-0104-4e38-ad47-2d431f83f1fa)
## Cause of the Wayland issue On Wayland, `winit` constantly emits `winit::event::Ime::Preedit("", None)` events. PR #7810 added these lines for handling `winit::event::Ime::Preedit(_, None)` in `egui-winit` without considering the `target_os`: https://github.com/emilk/egui/blob/14afefa2521d1baaf4fd02105eec2d3727a7ac36/crates/egui-winit/src/lib.rs#L619-L621 As a result, while text is being selected, `egui-winit` receives these `winit::event::Ime::Preedit("", None)` events from `winit` and forwards them to `egui` as `egui::ImeEvent::Preedit("")`. `egui` then clears the current text selection, because it currently does not distinguish between IME pre-edit text and selected text. --------- Co-authored-by: lucasmerlin --- crates/egui-winit/src/lib.rs | 47 +++++++++------ crates/egui/src/widgets/text_edit/builder.rs | 62 ++++++++++++-------- 2 files changed, 65 insertions(+), 44 deletions(-) diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 243ed119a..234a9989b 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -548,23 +548,23 @@ impl State { /// /// | Setup | Events in Order | /// | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | - /// | a-macos15-apple_shuangpin | `Predict("", None)` -> `Commit("ζ΅‹θ―•")` | - /// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Predict("", None)` -> `Commit("ζ΅‹θ―•")` -> `Predict("", Some(0, 0))` -> `Predict("", None)` (duplicate until `TextEdit` blurred) | - /// | c-windows11-ms_pinyin | `Predict("ζ΅‹θ―•", Some(…))` -> `Predict("", None)` -> `Commit("ζ΅‹θ―•")` -> `Disabled` | + /// | a-macos15-apple_shuangpin | `Preedit("", None)` -> `Commit("ζ΅‹θ―•")` | + /// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Preedit("", None)` -> `Commit("ζ΅‹θ―•")` -> `Preedit("", Some(0, 0))` -> `Preedit("", None)` (duplicate until `TextEdit` blurred) | + /// | c-windows11-ms_pinyin | `Preedit("ζ΅‹θ―•", Some(…))` -> `Preedit("", None)` -> `Commit("ζ΅‹θ―•")` -> `Disabled` | /// - /// #### Situation: pressed backspace to delete the last character in the prediction + /// #### Situation: pressed backspace to delete the last character in the composition /// /// | Setup | Events in Order | - /// | a-macos15-apple_shuangpin | `Predict("", None)` | - /// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Predict("", Some(0, 0))` -> `Predict("", None)` (duplicate until `TextEdit` blurred) | - /// | c-windows11-ms_pinyin | `Predict("", Some(0, 0))` -> `Predict("", None)` -> `Commit("")` -> `Disabled` | + /// | a-macos15-apple_shuangpin | `Preedit("", None)` | + /// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Preedit("", Some(0, 0))` -> `Preedit("", None)` (duplicate until `TextEdit` blurred) | + /// | c-windows11-ms_pinyin | `Preedit("", Some(0, 0))` -> `Preedit("", None)` -> `Commit("")` -> `Disabled` | /// - /// #### Situation: clicked somewhere else while there is an active composition with the prediction "ce" + /// #### Situation: clicked somewhere else while there is an active composition with the pre-edit text "ce" /// /// | Setup | Events in Order | /// | ------------------------------------------- | ------------------------------------------------------------------------------------------------- | /// | a-macos15-apple_shuangpin | nothing emitted | - /// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Predict("", Some(0, 0))` (duplicate) -> `Predict("", None)` (duplicate until `TextEdit` blurred) | + /// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Preedit("", Some(0, 0))` (duplicate) -> `Preedit("", None)` (duplicate until `TextEdit` blurred) | /// | c-windows11-ms_pinyin | nothing emitted | fn on_ime(&mut self, ime: &winit::event::Ime) { // // code for inspecting ime events emitted by winit: @@ -610,15 +610,26 @@ impl State { self.ime_event_disable(); } winit::event::Ime::Preedit(_, None) => { - // we need to emit this on macOS, since winit doesn't emit - // `Predict("", Some(0, 0))` before this event on macOS when the - // user deletes the last character in the prediction with the - // backspace key. Without this, only `egui::ImeEvent::Disabled` - // is emitted here, leading to the last character being left in - // TextEdit in such situation. - self.egui_input - .events - .push(egui::Event::Ime(egui::ImeEvent::Preedit(String::new()))); + if cfg!(target_os = "macos") { + // On macOS, when the user presses backspace to delete the + // last character in an IME composition, `winit` only emits + // `winit::event::Ime::Preedit("", None)` without a + // preceding `winit::event::Ime::Preedit("", Some(0, 0))`. + // + // The current implementation of `egui::TextEdit` relies on + // receiving an `egui::ImeEvent::Preedit("")` to remove the + // last character in the composition in this case, so we + // emit it here. + // + // This is guarded to macOS-only, as applying it on other + // platforms is unnecessary and can cause undesired + // behavior. + // See: https://github.com/emilk/egui/pull/7973 + self.egui_input + .events + .push(egui::Event::Ime(egui::ImeEvent::Preedit(String::new()))); + } + self.ime_event_disable(); } } diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index fbf25babf..b9fdb1cbe 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -1066,26 +1066,36 @@ fn events( } => check_for_mutating_key_press(os, &cursor_range, text, galley, modifiers, *key), Event::Ime(ime_event) => { - /// Empty prediction can be produced with [`ImeEvent::Preedit`] - /// or [`ImeEvent::Commit`] when user press backspace or escape - /// during IME, so this function should be called in both cases - /// to clear current text. + /// Both `ImeEvent::Preedit("")` and `ImeEvent::Commit("")` + /// might be emitted from different integrations to signify that + /// the current IME composition should be cleared. /// - /// Example platforms where only `ImeEvent::Preedit("")` of - /// those two events is emitted when the last character in the - /// prediction is deleted: - /// - macOS 15.7.3. - /// - Debian13 with gnome48 and wayland. + /// Example integrations where only `ImeEvent::Preedit("")` of + /// those two events is emitted when the last character is + /// deleted with a backspace: + /// - `egui-winit` on macOS 15.7.3. + /// - `egui-winit` on Debian13 with gnome48 and wayland. /// - /// An example platform where only `ImeEvent::Commit("")` of - /// those two events is emitted when the last character in the - /// prediction is deleted: - /// - Safari 26.2 (on macOS 15.7.3). - fn clear_prediction( + /// An example integration where only `ImeEvent::Commit("")` of + /// those two events is emitted when the last character is + /// deleted with a backspace: + /// - `eframe`'s web integration on Safari 26.2 (on macOS + /// 15.7.3). + /// + /// ## Note + /// + /// The term β€œpre-edit string” is used by X11 and Wayland, and + /// we use β€œpre-edit text” and β€œpre-edit range” here in the + /// same manner. + /// See: + /// + /// We previously referred to β€œpre-edit text” as β€œprediction”, + /// which is not standard and can mean different things. + fn clear_preedit_text( text: &mut dyn TextBuffer, - cursor_range: &CCursorRange, + preedit_range: &CCursorRange, ) -> CCursor { - text.delete_selected(cursor_range) + text.delete_selected(preedit_range) } match ime_event { @@ -1094,33 +1104,33 @@ fn events( state.ime_cursor_range = cursor_range; None } - ImeEvent::Preedit(text_mark) => { - if text_mark == "\n" || text_mark == "\r" { + ImeEvent::Preedit(preedit_text) => { + if preedit_text == "\n" || preedit_text == "\r" { None } else { - let mut ccursor = clear_prediction(text, &cursor_range); + let mut ccursor = clear_preedit_text(text, &cursor_range); let start_cursor = ccursor; - if !text_mark.is_empty() { - text.insert_text_at(&mut ccursor, text_mark, char_limit); + if !preedit_text.is_empty() { + text.insert_text_at(&mut ccursor, preedit_text, char_limit); } state.ime_cursor_range = cursor_range; Some(CCursorRange::two(start_cursor, ccursor)) } } - ImeEvent::Commit(prediction) => { - if prediction == "\n" || prediction == "\r" { + ImeEvent::Commit(commit_text) => { + if commit_text == "\n" || commit_text == "\r" { None } else { state.ime_enabled = false; - let mut ccursor = clear_prediction(text, &cursor_range); + let mut ccursor = clear_preedit_text(text, &cursor_range); - if !prediction.is_empty() + if !commit_text.is_empty() && cursor_range.secondary.index == state.ime_cursor_range.secondary.index { - text.insert_text_at(&mut ccursor, prediction, char_limit); + text.insert_text_at(&mut ccursor, commit_text, char_limit); } Some(CCursorRange::one(ccursor)) From c09a8723b43abe43cdb3478711a433bda11cd46e Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 17 Mar 2026 17:08:18 +0100 Subject: [PATCH 24/58] Fix vulnerability in the branch name check workflow (#7982) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before, a crafted branch name could be used to exfiltrate the github token and wreak havoc πŸ˜… --- .github/workflows/enforce_branch_name.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/enforce_branch_name.yml b/.github/workflows/enforce_branch_name.yml index 8c2b28d37..b9df4030d 100644 --- a/.github/workflows/enforce_branch_name.yml +++ b/.github/workflows/enforce_branch_name.yml @@ -4,17 +4,23 @@ on: pull_request_target: types: [opened, reopened, synchronize] +permissions: + issues: write + jobs: check-source-branch: runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Check PR source branch + env: + IS_FORK: ${{ github.event.pull_request.head.repo.fork }} + HEAD_REF: ${{ github.event.pull_request.head.ref }} run: | # Check if PR is from a fork - if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then + if [[ "$IS_FORK" == "true" ]]; then # Check if PR is from the master/main branch of a fork - if [[ "${{ github.event.pull_request.head.ref }}" == "master" || "${{ github.event.pull_request.head.ref }}" == "main" ]]; then + if [[ "$HEAD_REF" == "master" || "$HEAD_REF" == "main" ]]; then echo "ERROR: Pull requests from the master/main branch of forks are not allowed, because it prevents maintainers from contributing to your PR" echo "Please create a feature branch in your fork and submit the PR from that branch instead." exit 1 From 543e7204ba39615c4cc7ca7713e6bb8480a8d6f3 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 18 Mar 2026 14:58:10 +0100 Subject: [PATCH 25/58] Update lz4_flex dependency --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 800618e4a..896a36665 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2592,9 +2592,9 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lz4_flex" -version = "0.11.3" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75761162ae2b0e580d7e7c390558127e5f01b4194debd6221fd8c207fc80e3f5" +checksum = "373f5eceeeab7925e0c1098212f2fbc4d416adec9d35051a6ab251e824c1854a" [[package]] name = "malloc_buf" From 265cf7ebaeba8ef319679981baf7aece5042f373 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 18 Mar 2026 15:08:06 +0100 Subject: [PATCH 26/58] Add `DebugOptions::warn_if_rect_changes_id` (#7984) If turned on (default in debug builds), if the exact same `Rect` exist in two subsequent frames but with different `Id`s, a warning is logged an a red rect is flashed on the screen. --- crates/egui/src/context.rs | 110 +++++++++++++++++++++ crates/egui/src/style.rs | 10 ++ tests/egui_tests/tests/regression_tests.rs | 45 +++++++++ 3 files changed, 165 insertions(+) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 3b36150e4..30818a772 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -2636,6 +2636,19 @@ impl ContextImpl { } } + #[cfg(debug_assertions)] + let shapes = if self.memory.options.style().debug.warn_if_rect_changes_id { + let mut shapes = shapes; + warn_if_rect_changes_id( + &mut shapes, + &viewport.prev_pass.widgets, + &viewport.this_pass.widgets, + ); + shapes + } else { + shapes + }; + std::mem::swap(&mut viewport.prev_pass, &mut viewport.this_pass); if repaint_needed { @@ -4237,6 +4250,103 @@ fn context_impl_send_sync() { assert_send_sync::(); } +/// Check if any [`Rect`] appears with different [`Id`]s between two passes. +/// +/// This helps detect cases where the same screen area is claimed by different widget ids +/// across passes, which is often a sign of id instability. +#[cfg(debug_assertions)] +fn warn_if_rect_changes_id( + out_shapes: &mut Vec, + prev_widgets: &crate::WidgetRects, + new_widgets: &crate::WidgetRects, +) { + profiling::function_scope!(); + + use std::collections::BTreeMap; + + /// A wrapper around [`Rect`] that implements [`Ord`] using the bit representation of its floats. + #[derive(Clone, Copy, PartialEq, Eq)] + struct OrderedRect(Rect); + + impl PartialOrd for OrderedRect { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } + } + + impl Ord for OrderedRect { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + let lhs = self.0; + let rhs = other.0; + lhs.min + .x + .to_bits() + .cmp(&rhs.min.x.to_bits()) + .then(lhs.min.y.to_bits().cmp(&rhs.min.y.to_bits())) + .then(lhs.max.x.to_bits().cmp(&rhs.max.x.to_bits())) + .then(lhs.max.y.to_bits().cmp(&rhs.max.y.to_bits())) + } + } + + fn create_lookup<'a>( + widgets: impl Iterator, + ) -> BTreeMap> { + let mut lookup: BTreeMap> = BTreeMap::default(); + for w in widgets { + lookup.entry(OrderedRect(w.rect)).or_default().push(w); + } + lookup + } + + for (layer_id, new_layer_widgets) in new_widgets.layers() { + let prev = create_lookup(prev_widgets.get_layer(*layer_id)); + let new = create_lookup(new_layer_widgets.iter()); + + for (hashable_rect, new_at_rect) in new { + let Some(prev_at_rect) = prev.get(&hashable_rect) else { + continue; // this rect did not exist in the previous pass + }; + + if prev_at_rect + .iter() + .any(|w| new_at_rect.iter().any(|nw| nw.id == w.id)) + { + continue; // at least one id stayed the same, so this is not an id change + } + + // Only warn if at least one of the previous ids is gone from this layer entirely. + // If they all still exist (just at a different rect), then the rect match + // is just a coincidence caused by widgets shifting (e.g. a window being dragged). + if prev_at_rect.iter().all(|w| new_widgets.contains(w.id)) { + continue; + } + + let rect = new_at_rect[0].rect; + + log::warn!( + "Widget rect {rect:?} changed id between passes: prev ids: {:?}, new ids: {:?}", + prev_at_rect + .iter() + .map(|w| w.id.short_debug_format()) + .collect::>(), + new_at_rect + .iter() + .map(|w| w.id.short_debug_format()) + .collect::>(), + ); + out_shapes.push(ClippedShape { + clip_rect: Rect::EVERYTHING, + shape: epaint::Shape::rect_stroke( + rect, + 0, + (2.0, Color32::RED), + StrokeKind::Outside, + ), + }); + } + } +} + #[cfg(test)] mod test { use super::Context; diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 56a347f0d..5cd980f6c 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -1303,6 +1303,9 @@ pub struct DebugOptions { /// Show interesting widgets under the mouse cursor. pub show_widget_hits: bool, + /// Show a warning if the same `Rect` had different `Id` on the previous frame. + pub warn_if_rect_changes_id: bool, + /// If true, highlight widgets that are not aligned to [`emath::GUI_ROUNDING`]. /// /// See [`emath::GuiRounding`] for more. @@ -1329,6 +1332,7 @@ impl Default for DebugOptions { show_resize: false, show_interactive_widgets: false, show_widget_hits: false, + warn_if_rect_changes_id: cfg!(debug_assertions), show_unaligned: cfg!(debug_assertions), show_focused_widget: false, } @@ -2491,6 +2495,7 @@ impl DebugOptions { show_resize, show_interactive_widgets, show_widget_hits, + warn_if_rect_changes_id, show_unaligned, show_focused_widget, } = self; @@ -2522,6 +2527,11 @@ impl DebugOptions { ui.checkbox(show_widget_hits, "Show widgets under mouse pointer"); + ui.checkbox( + warn_if_rect_changes_id, + "Warn if a Rect changes Id between frames", + ); + ui.checkbox( show_unaligned, "Show rectangles not aligned to integer point coordinates", diff --git a/tests/egui_tests/tests/regression_tests.rs b/tests/egui_tests/tests/regression_tests.rs index 9e76394cb..421b69d35 100644 --- a/tests/egui_tests/tests/regression_tests.rs +++ b/tests/egui_tests/tests/regression_tests.rs @@ -1,4 +1,5 @@ use egui::accesskit::Role; +use egui::epaint::Shape; use egui::{Align, Color32, Image, Label, Layout, RichText, Sense, TextWrapMode, include_image}; use egui_kittest::Harness; use egui_kittest::kittest::Queryable as _; @@ -118,3 +119,47 @@ fn interact_on_ui_response_should_be_stable() { drop(harness); assert_eq!(click_count, 10, "We missed some clicks!"); } + +fn has_red_warning_rect(output: &egui::FullOutput) -> bool { + output.shapes.iter().any(|clipped| { + matches!( + &clipped.shape, + Shape::Rect(rect_shape) + if rect_shape.stroke.color == Color32::RED + ) + }) +} + +/// A button that changes its text on hover, with the Id derived from the text. +/// This is a plausible bug: the widget keeps the same rect, but its Id changes +/// between frames because the label (and thus the Id salt) changes on hover. +/// The `warn_if_rect_changes_id` debug check should catch this. +#[test] +fn warn_if_rect_changes_id() { + let button_rect = egui::Rect::from_min_size(egui::pos2(10.0, 10.0), egui::vec2(100.0, 30.0)); + + let mut harness = Harness::builder().with_size((200.0, 50.0)).build_ui(|ui| { + // Simulate a buggy widget whose Id depends on its label text, + // and the label changes on hover: + let is_hovered = ui.rect_contains_pointer(button_rect); + let label = if is_hovered { "Hovering!" } else { "Click me" }; + let id = ui.id().with(label); + let _response = ui.interact(button_rect, id, Sense::click()); + }); + + // no hover β€” establishes stable prev_pass + harness.step(); + assert!( + !has_red_warning_rect(harness.output()), + "Should not warn without hover" + ); + + // Move the pointer over the button + harness.hover_at(button_rect.center()); + + harness.step(); + assert!( + has_red_warning_rect(harness.output()), + "Should warn when a widget rect changes Id between passes" + ); +} From ad510257de5a03eb0889cb183a170162799c5dee Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 18 Mar 2026 15:24:19 +0100 Subject: [PATCH 27/58] Quit on Ctrl-Q (#7985) This adds `Ctrl-Q` as the default shortcut for closing the current egui `Viewport`, which also means closing the entire application if you are in the root viewport. Can be configured by `egui::Options::quit_shortcuts` On Mac, `cmd-Q` already triggers a quit, but not on all Linux:es. --- crates/egui/src/context.rs | 6 ++++++ crates/egui/src/memory/mod.rs | 15 +++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 30818a772..fbc189132 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -2397,6 +2397,12 @@ impl Context { crate::gui_zoom::zoom_with_keyboard(self); } + for shortcut in self.options(|o| o.quit_shortcuts.clone()) { + if self.input_mut(|i| i.consume_shortcut(&shortcut)) { + self.send_viewport_cmd(ViewportCommand::Close); + } + } + #[cfg(debug_assertions)] self.debug_painting(); diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index 51ab2cde4..08b08a462 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -234,6 +234,16 @@ pub struct Options { #[cfg_attr(feature = "serde", serde(skip))] pub zoom_with_keyboard: bool, + /// Keyboard shortcuts to close the application. + /// + /// Pressing any of these will send [`crate::ViewportCommand::Close`] + /// to the root viewport. + /// + /// Defaults to `Cmd-Q` (which is Ctrl-Q on Linux/Windows, Cmd-Q on Mac). + /// Set to empty to disable. + #[cfg_attr(feature = "serde", serde(skip))] + pub quit_shortcuts: Vec, + /// Controls the tessellator. pub tessellation_options: epaint::TessellationOptions, @@ -304,6 +314,10 @@ impl Default for Options { system_theme: None, zoom_factor: 1.0, zoom_with_keyboard: true, + quit_shortcuts: vec![crate::KeyboardShortcut::new( + crate::Modifiers::COMMAND, + crate::Key::Q, + )], tessellation_options: Default::default(), repaint_on_widget_change: false, @@ -363,6 +377,7 @@ impl Options { system_theme: _, zoom_factor, zoom_with_keyboard, + quit_shortcuts: _, // not shown in ui tessellation_options, repaint_on_widget_change, max_passes, From 8b2315375b6cf13291ee658a498cb45fc379f600 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Fri, 20 Mar 2026 11:29:32 +0100 Subject: [PATCH 28/58] `TextEdit` `Atom` prefix/suffix (#7587) * part of https://github.com/emilk/egui/issues/7264 * part of https://github.com/emilk/egui/issues/7445 This PR changes the layout within the TextEdit to be done with AtomLayout. It also adds a Prefix and Postfix that allows adding permanent icons / icon buttons within the textedit. Breaking changes: - Removed `TextEdit::hint_text_font`. Hint text is an atom now, so the font can be set that way Screenshot 2025-10-06 at 12 25 21 --------- Co-authored-by: Emil Ernerfeldt Co-authored-by: lucasmerlin <8009393+lucasmerlin@users.noreply.github.com> --- crates/egui/src/atomics/atom_layout.rs | 2 +- crates/egui/src/atomics/atoms.rs | 5 + crates/egui/src/widgets/text_edit/builder.rs | 510 ++++++++++-------- .../tests/snapshots/imageviewer.png | 4 +- .../tests/snapshots/demos/Clipboard Test.png | 2 +- .../tests/snapshots/demos/Code Example.png | 4 +- .../tests/snapshots/demos/Font Book.png | 4 +- .../tests/snapshots/demos/TextEdit.png | 2 +- .../tests/snapshots/demos/Undo Redo.png | 4 +- .../tests/snapshots/demos/Window Options.png | 4 +- .../snapshots/demos/Window Resize Test.png | 4 +- tests/egui_tests/tests/regression_tests.rs | 66 ++- .../tests/snapshots/layout/text_edit.png | 4 +- .../tests/snapshots/layout/text_edit_clip.png | 4 +- .../layout/text_edit_placeholder_clip.png | 4 +- .../layout/text_edit_prefix_suffix.png | 3 + .../snapshots/text_edit_delay_0_empty.png | 3 + .../text_edit_delay_1_h_invisible.png | 3 + .../snapshots/text_edit_delay_2_h_visible.png | 3 + .../snapshots/text_edit_delay_3_i_visible.png | 3 + .../snapshots/text_edit_delay_4_i_visible.png | 3 + .../tests/snapshots/text_edit_rtl_0.png | 4 +- .../tests/snapshots/text_edit_rtl_1.png | 4 +- .../snapshots/text_edit_scroll_0_focus.png | 3 + .../tests/snapshots/text_edit_scroll_1_5.png | 3 + .../tests/snapshots/visuals/drag_value.png | 4 +- .../tests/snapshots/visuals/text_edit.png | 4 +- .../snapshots/visuals/text_edit_clip.png | 4 +- .../visuals/text_edit_placeholder_clip.png | 4 +- .../visuals/text_edit_prefix_suffix.png | 3 + tests/egui_tests/tests/test_widgets.rs | 12 + 31 files changed, 427 insertions(+), 259 deletions(-) create mode 100644 tests/egui_tests/tests/snapshots/layout/text_edit_prefix_suffix.png create mode 100644 tests/egui_tests/tests/snapshots/text_edit_delay_0_empty.png create mode 100644 tests/egui_tests/tests/snapshots/text_edit_delay_1_h_invisible.png create mode 100644 tests/egui_tests/tests/snapshots/text_edit_delay_2_h_visible.png create mode 100644 tests/egui_tests/tests/snapshots/text_edit_delay_3_i_visible.png create mode 100644 tests/egui_tests/tests/snapshots/text_edit_delay_4_i_visible.png create mode 100644 tests/egui_tests/tests/snapshots/text_edit_scroll_0_focus.png create mode 100644 tests/egui_tests/tests/snapshots/text_edit_scroll_1_5.png create mode 100644 tests/egui_tests/tests/snapshots/visuals/text_edit_prefix_suffix.png diff --git a/crates/egui/src/atomics/atom_layout.rs b/crates/egui/src/atomics/atom_layout.rs index b78f23536..da7f672a1 100644 --- a/crates/egui/src/atomics/atom_layout.rs +++ b/crates/egui/src/atomics/atom_layout.rs @@ -203,7 +203,7 @@ impl<'a> AtomLayout<'a> { // If the TextWrapMode is not Extend, ensure there is some item marked as `shrink`. // If none is found, mark the first text item as `shrink`. if wrap_mode != TextWrapMode::Extend { - let any_shrink = atoms.iter().any(|a| a.shrink); + let any_shrink = atoms.any_shrink(); if !any_shrink { let first_text = atoms .iter_mut() diff --git a/crates/egui/src/atomics/atoms.rs b/crates/egui/src/atomics/atoms.rs index fb04ee2dd..5051a7676 100644 --- a/crates/egui/src/atomics/atoms.rs +++ b/crates/egui/src/atomics/atoms.rs @@ -69,6 +69,11 @@ impl<'a> Atoms<'a> { string } + /// Do any of the atoms have shrink set to `true`? + pub fn any_shrink(&self) -> bool { + self.iter().any(|a| a.shrink) + } + pub fn iter_kinds(&self) -> impl Iterator> { self.0.iter().map(|atom| &atom.kind) } diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index b9fdb1cbe..50154102a 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -1,15 +1,13 @@ use std::sync::Arc; use emath::{Rect, TSTransform}; -use epaint::{ - StrokeKind, - text::{Galley, LayoutJob, cursor::CCursor}, -}; +use epaint::text::{Galley, LayoutJob, TextWrapMode, cursor::CCursor}; use crate::{ - Align, Align2, Color32, Context, CursorIcon, Event, EventFilter, FontSelection, Id, ImeEvent, - Key, KeyboardShortcut, Margin, Modifiers, NumExt as _, Response, Sense, Shape, TextBuffer, - TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetWithState, epaint, + Align, Align2, Atom, AtomExt as _, AtomKind, AtomLayout, Atoms, Color32, Context, CursorIcon, + Event, EventFilter, FontSelection, Frame, Id, ImeEvent, IntoAtoms, IntoSizedResult, Key, + KeyboardShortcut, Margin, Modifiers, NumExt as _, Response, Sense, SizedAtomKind, TextBuffer, + TextStyle, Ui, Vec2, Widget, WidgetInfo, WidgetWithState, epaint, os::OperatingSystem, output::OutputEvent, response, text_selection, @@ -67,15 +65,16 @@ type LayouterFn<'t> = &'t mut dyn FnMut(&Ui, &dyn TextBuffer, f32) -> Arc { text: &'t mut dyn TextBuffer, - hint_text: WidgetText, - hint_text_font: Option, + prefix: Atoms<'static>, + suffix: Atoms<'static>, + hint_text: Atoms<'static>, id: Option, id_salt: Option, font_selection: FontSelection, text_color: Option, layouter: Option>, password: bool, - frame: bool, + frame: Option, margin: Margin, multiline: bool, interactive: bool, @@ -120,15 +119,16 @@ impl<'t> TextEdit<'t> { pub fn multiline(text: &'t mut dyn TextBuffer) -> Self { Self { text, + prefix: Default::default(), + suffix: Default::default(), hint_text: Default::default(), - hint_text_font: None, id: None, id_salt: None, font_selection: Default::default(), text_color: None, layouter: None, password: false, - frame: true, + frame: None, margin: Margin::symmetric(4, 2), multiline: true, interactive: true, @@ -202,8 +202,22 @@ impl<'t> TextEdit<'t> { /// # }); /// ``` #[inline] - pub fn hint_text(mut self, hint_text: impl Into) -> Self { - self.hint_text = hint_text.into(); + pub fn hint_text(mut self, hint_text: impl IntoAtoms<'static>) -> Self { + self.hint_text = hint_text.into_atoms(); + self + } + + /// Add a prefix to the text edit. This will always be shown before the editable text. + #[inline] + pub fn prefix(mut self, prefix: impl IntoAtoms<'static>) -> Self { + self.prefix = prefix.into_atoms(); + self + } + + /// Add a suffix to the text edit. This will always be shown after the editable text. + #[inline] + pub fn suffix(mut self, suffix: impl IntoAtoms<'static>) -> Self { + self.suffix = suffix.into_atoms(); self } @@ -215,13 +229,6 @@ impl<'t> TextEdit<'t> { self } - /// Set a specific style for the hint text. - #[inline] - pub fn hint_text_font(mut self, hint_text_font: impl Into) -> Self { - self.hint_text_font = Some(hint_text_font.into()); - self - } - /// If true, hide the letters from view and prevent copying from the field. #[inline] pub fn password(mut self, password: bool) -> Self { @@ -290,10 +297,10 @@ impl<'t> TextEdit<'t> { self } - /// Default is `true`. If set to `false` there will be no frame showing that this is editable text! + /// Customize the [`Frame`] around the text edit. #[inline] - pub fn frame(mut self, frame: bool) -> Self { - self.frame = frame; + pub fn frame(mut self, frame: Frame) -> Self { + self.frame = Some(frame); self } @@ -423,63 +430,18 @@ impl TextEdit<'_> { /// # }); /// ``` pub fn show(self, ui: &mut Ui) -> TextEditOutput { - let is_mutable = self.text.is_mutable(); - let frame = self.frame; - let where_to_put_background = ui.painter().add(Shape::Noop); - let background_color = self - .background_color - .unwrap_or_else(|| ui.visuals().text_edit_bg_color()); - let output = self.show_content(ui); - - if frame { - let visuals = ui.style().interact(&output.response); - let frame_rect = output.response.rect.expand(visuals.expansion); - let shape = if is_mutable { - if output.response.has_focus() { - epaint::RectShape::new( - frame_rect, - visuals.corner_radius, - background_color, - ui.visuals().selection.stroke, - StrokeKind::Inside, - ) - } else { - epaint::RectShape::new( - frame_rect, - visuals.corner_radius, - background_color, - visuals.bg_stroke, // TODO(emilk): we want to show something here, or a text-edit field doesn't "pop". - StrokeKind::Inside, - ) - } - } else { - let visuals = &ui.style().visuals.widgets.inactive; - epaint::RectShape::stroke( - frame_rect, - visuals.corner_radius, - visuals.bg_stroke, // TODO(emilk): we want to show something here, or a text-edit field doesn't "pop". - StrokeKind::Inside, - ) - }; - - ui.painter().set(where_to_put_background, shape); - } - - output - } - - fn show_content(self, ui: &mut Ui) -> TextEditOutput { let TextEdit { text, - hint_text, - hint_text_font, + prefix, + suffix, + mut hint_text, id, id_salt, font_selection, text_color, layouter, password, - frame: _, + frame, margin, multiline, interactive, @@ -492,7 +454,7 @@ impl TextEdit<'_> { clip_text, char_limit, return_key, - background_color: _, + background_color, } = self; let text_color = text_color @@ -501,18 +463,16 @@ impl TextEdit<'_> { .unwrap_or_else(|| ui.visuals().widgets.inactive.text_color()); let prev_text = text.as_str().to_owned(); - let hint_text_str = hint_text.text().to_owned(); + let hint_text_str = hint_text.text().unwrap_or_default().to_string(); let font_id = font_selection.resolve(ui.style()); let row_height = ui.fonts_mut(|f| f.row_height(&font_id)); const MIN_WIDTH: f32 = 24.0; // Never make a [`TextEdit`] more narrow than this. - let available_width = (ui.available_width() - margin.sum().x).at_least(MIN_WIDTH); - let desired_width = desired_width.unwrap_or_else(|| ui.spacing().text_edit_width); - let wrap_width = if ui.layout().horizontal_justify() { - available_width - } else { - desired_width.min(available_width) - }; + let available_width = ui.available_width().at_least(MIN_WIDTH); + let desired_width = desired_width + .unwrap_or_else(|| ui.spacing().text_edit_width) + .at_least(min_size.x); + let allocate_width = desired_width.at_most(available_width); let font_id_clone = font_id.clone(); let mut default_layouter = move |ui: &Ui, text: &dyn TextBuffer, wrap_width: f32| { @@ -527,27 +487,18 @@ impl TextEdit<'_> { let layouter = layouter.unwrap_or(&mut default_layouter); - let mut galley = layouter(ui, text, wrap_width); - - let desired_inner_width = if clip_text { - wrap_width // visual clipping with scroll in singleline input. - } else { - galley.size().x.max(wrap_width) - }; - let desired_height = (desired_height_rows.at_least(1) as f32) * row_height; - let desired_inner_size = vec2(desired_inner_width, galley.size().y.max(desired_height)); - let desired_outer_size = (desired_inner_size + margin.sum()).at_least(min_size); - let (auto_id, outer_rect) = ui.allocate_space(desired_outer_size); - let rect = outer_rect - margin; // inner rect (excluding frame/margin). + let min_inner_height = (desired_height_rows.at_least(1) as f32) * row_height; let id = id.unwrap_or_else(|| { if let Some(id_salt) = id_salt { ui.make_persistent_id(id_salt) } else { - auto_id // Since we are only storing the cursor a persistent Id is not super important + // Since we are only storing the cursor a persistent Id is not super important + let id = ui.next_auto_id(); + ui.skip_ahead_auto_ids(1); + id } }); - let mut state = TextEditState::load(ui.ctx(), id).unwrap_or_default(); // On touch screens (e.g. mobile in `eframe` web), should // dragging select text, or scroll the enclosing [`ScrollArea`] (if any)? @@ -565,12 +516,215 @@ impl TextEdit<'_> { } else { Sense::hover() }; - let mut response = ui.interact(outer_rect, id, sense); - response.intrinsic_size = Some(Vec2::new(desired_width, desired_outer_size.y)); - // Don't sent `OutputEvent::Clicked` when a user presses the space bar + let mut state = TextEditState::load(ui.ctx(), id).unwrap_or_default(); + let mut cursor_range = None; + let mut prev_cursor_range = None; + + let mut text_changed = false; + let text_mutable = text.is_mutable(); + + let mut handle_events = |ui: &Ui, galley: &mut Arc, layouter, wrap_width, text| { + if interactive && ui.memory(|mem| mem.has_focus(id)) { + ui.memory_mut(|mem| mem.set_focus_lock_filter(id, event_filter)); + + let default_cursor_range = if cursor_at_end { + CCursorRange::one(galley.end()) + } else { + CCursorRange::default() + }; + prev_cursor_range = state.cursor.range(galley); + + let (changed, new_cursor_range) = events( + ui, + &mut state, + text, + galley, + layouter, + id, + wrap_width, + multiline, + password, + default_cursor_range, + char_limit, + event_filter, + return_key, + ); + + if changed { + text_changed = true; + } + cursor_range = Some(new_cursor_range); + } + }; + + // We need to calculate the galley within the atom closure, so we can calculate it based on + // the available width (in case of wrapping multiline text edits). But we show it later, + // so we can clip it to the available size. Thus, extract it from the atom closure here. + let mut get_galley = None; + let inner_rect_id = Id::new("text_edit_rect"); + let atom_response = { + let any_shrink = hint_text.any_shrink(); + // Ideally we could just do `let mut atoms = prefix` here, but that won't compile + // but due to servo/rust-smallvec#146 (also see the comment below). + let mut atoms: Atoms<'_> = Atoms::new(()); + + // TODO(servo/rust-smallvec#146): Use extend_right instead of the loop once we have + // smallvec 2.0. Using `extend_right` here won't compile, due to lifetime issues. + for atom in prefix { + atoms.push_right(atom); + } + + if text.as_str().is_empty() && !hint_text.is_empty() { + // Add hint_text (if any): + let mut shrunk = any_shrink; + let mut first = true; + + // Since we can't set a fallback color per atom, we have to override it here. + // Sucks, since it means users won't be able to override it. + hint_text.map_texts(|t| t.color(ui.style().visuals.weak_text_color())); + + for mut atom in hint_text { + if !shrunk && matches!(atom.kind, AtomKind::Text(_)) { + // elide the hint_text if needed + atom = atom.atom_shrink(true); + shrunk = true; + } + + if first { + // The first atom in the hint text gets inner_rect_id, so we can know + // where to paint the cursor + atom = atom.atom_id(inner_rect_id); + first = false; + } + + // The hint text should be shown left top instead of centered (important for + // multi line text edits) + atoms.push_right(atom.atom_align(Align2::LEFT_TOP)); + } + + // Calculate the empty galley, so it can be read later. The available width is + // technically wrong, but doesn't matter since the galley is empty + let available_width = allocate_width - margin.sum().x; + let galley = layouter(ui, text, available_width); + + // We can't update the galley immediately here, since it would show both hint text + // and the newly typed letter. So we pass a clone instead, and accept having a frame + // delay on the very first keystroke. + let mut galley_clone = Arc::clone(&galley); + handle_events(ui, &mut galley_clone, layouter, available_width, text); + + get_galley = Some(galley); + } else { + // We need a closure here, so we can calculate the galley based on the available + // width (after adding suffix and prefix), for correct wrapping in multi line text + // edits + atoms.push_right( + AtomKind::closure(|ui, args| { + let mut galley = layouter(ui, text, args.available_size.x); + + // Handling events here allows us to update the galley immediately on + // keystrokes, avoiding frame delays, and ensuring the scroll_to within + // ScrollAreas works correctly. + handle_events(ui, &mut galley, layouter, args.available_size.x, text); + + let intrinsic_size = galley.intrinsic_size(); + let mut size = galley.size(); + size.y = size.y.at_least(min_inner_height); + if clip_text { + size.x = size.x.at_most(args.available_size.x); + } + + // We paint the galley later, so we can do clipping and offsetting + get_galley = Some(galley); + IntoSizedResult { + intrinsic_size, + sized: SizedAtomKind::Empty { size: Some(size) }, + } + }) + .atom_id(inner_rect_id) + .atom_shrink(clip_text), + ); + } + + // Ensure the suffix is always right-aligned + if !suffix.is_empty() { + atoms.push_right(Atom::grow()); + } + + // TODO(servo/rust-smallvec#146): Use extend_right instead of the loop once we have + // smallvec 2.0. Using `extend_right` here won't compile, due to lifetime issues. + for atom in suffix { + atoms.push_right(atom); + } + + let custom_frame = frame.is_some(); + let frame = frame.unwrap_or_else(|| Frame::new().inner_margin(margin)); + + let min_height = min_inner_height + frame.total_margin().sum().y; + + // This wrap mode only affects the hint_text + let wrap_mode = if multiline { + TextWrapMode::Wrap + } else { + TextWrapMode::Truncate + }; + + let mut allocated = AtomLayout::new(atoms) + .id(id) + .min_size(Vec2::new(allocate_width, min_height)) + .max_width(allocate_width) + .sense(sense) + .frame(frame) + .align2(Align2::LEFT_TOP) + .wrap_mode(wrap_mode) + .allocate(ui); + + allocated.frame = if !custom_frame { + let visuals = ui.style().interact(&allocated.response); + let background_color = + background_color.unwrap_or_else(|| ui.visuals().text_edit_bg_color()); + + let (corner_radius, background_color, stroke) = if text_mutable { + if allocated.response.has_focus() { + ( + visuals.corner_radius, + background_color, + ui.visuals().selection.stroke, + ) + } else { + (visuals.corner_radius, background_color, visuals.bg_stroke) + } + } else { + let visuals = &ui.style().visuals.widgets.inactive; + ( + visuals.corner_radius, + Color32::TRANSPARENT, + visuals.bg_stroke, + ) + }; + allocated + .frame + .fill(background_color) + .corner_radius(corner_radius) + .inner_margin(allocated.frame.inner_margin - Margin::same(stroke.width as i8)) + .stroke(stroke) + } else { + allocated.frame + }; + + allocated.paint(ui) + }; + + let inner_rect = atom_response.rect(inner_rect_id).unwrap_or(Rect::ZERO); + let mut response = atom_response.response; + + // Our atom closure was now called, so the galley should always be available here + let mut galley = get_galley.expect("Galley should be available here"); + + // Don't send `OutputEvent::Clicked` when a user presses the space bar response.flags -= response::Flags::FAKE_PRIMARY_CLICKED; - let text_clip_rect = rect; + let text_clip_rect = inner_rect; let painter = ui.painter_at(text_clip_rect.expand(1.0)); // expand to avoid clipping cursor if interactive && let Some(pointer_pos) = response.interact_pointer_pos() { @@ -581,19 +735,19 @@ impl TextEdit<'_> { // TODO(emilk): drag selected text to either move or clone (ctrl on windows, alt on mac) let cursor_at_pointer = - galley.cursor_from_pos(pointer_pos - rect.min + state.text_offset); + galley.cursor_from_pos(pointer_pos - inner_rect.min + state.text_offset); if ui.visuals().text_cursor.preview && response.hovered() && ui.input(|i| i.pointer.is_moving()) { // text cursor preview: - let cursor_rect = TSTransform::from_translation(rect.min.to_vec2()) + let cursor_rect = TSTransform::from_translation(inner_rect.min.to_vec2()) * cursor_rect(&galley, &cursor_at_pointer, row_height); text_selection::visuals::paint_cursor_end(&painter, ui.visuals(), cursor_rect); } - let is_being_dragged = ui.ctx().is_being_dragged(response.id); + let is_being_dragged = ui.is_being_dragged(response.id); let did_interact = state.cursor.pointer_interaction( ui, &response, @@ -613,44 +767,15 @@ impl TextEdit<'_> { ui.set_cursor_icon(CursorIcon::Text); } - let mut cursor_range = None; - let prev_cursor_range = state.cursor.range(&galley); - if interactive && ui.memory(|mem| mem.has_focus(id)) { - ui.memory_mut(|mem| mem.set_focus_lock_filter(id, event_filter)); - - let default_cursor_range = if cursor_at_end { - CCursorRange::one(galley.end()) - } else { - CCursorRange::default() - }; - - let (changed, new_cursor_range) = events( - ui, - &mut state, - text, - &mut galley, - layouter, - id, - wrap_width, - multiline, - password, - default_cursor_range, - char_limit, - event_filter, - return_key, - ); - - if changed { - response.mark_changed(); - } - cursor_range = Some(new_cursor_range); + if text_changed { + response.mark_changed(); } let mut galley_pos = align - .align_size_within_rect(galley.size(), rect) - .intersect(rect) // limit pos to the response rect area + .align_size_within_rect(galley.size(), inner_rect) + .intersect(inner_rect) // limit pos to the response rect area .min; - let align_offset = rect.left_top() - galley_pos; + let align_offset = inner_rect.left_top() - galley_pos; // Visual clipping for singleline text editor with text larger than width if clip_text && align_offset.x == 0.0 { @@ -660,18 +785,18 @@ impl TextEdit<'_> { }; let mut offset_x = state.text_offset.x; - let visible_range = offset_x..=offset_x + desired_inner_size.x; + let visible_range = offset_x..=offset_x + inner_rect.width(); if !visible_range.contains(&cursor_pos) { if cursor_pos < *visible_range.start() { offset_x = cursor_pos; } else { - offset_x = cursor_pos - desired_inner_size.x; + offset_x = cursor_pos - inner_rect.width(); } } offset_x = offset_x - .at_most(galley.size().x - desired_inner_size.x) + .at_most(galley.size().x - inner_rect.width()) .at_least(0.0); state.text_offset = vec2(offset_x, align_offset.y); @@ -688,32 +813,7 @@ impl TextEdit<'_> { false }; - if ui.is_rect_visible(rect) { - if text.as_str().is_empty() && !hint_text.is_empty() { - let hint_text_color = ui.visuals().weak_text_color(); - let hint_text_font_id = hint_text_font.unwrap_or_else(|| font_id.into()); - let galley = if multiline { - hint_text.into_galley( - ui, - Some(TextWrapMode::Wrap), - desired_inner_size.x, - hint_text_font_id, - ) - } else { - hint_text.into_galley( - ui, - Some(TextWrapMode::Extend), - f32::INFINITY, - hint_text_font_id, - ) - }; - let galley_pos = align - .align_size_within_rect(galley.size(), rect) - .intersect(rect) - .min; - painter.galley(galley_pos, galley, hint_text_color); - } - + if ui.is_rect_visible(inner_rect) { let has_focus = ui.memory(|mem| mem.has_focus(id)); if has_focus && let Some(cursor_range) = state.cursor.range(&galley) { @@ -721,44 +821,6 @@ impl TextEdit<'_> { paint_text_selection(&mut galley, ui.visuals(), &cursor_range, None); } - // Allocate additional space if edits were made this frame that changed the size. This is important so that, - // if there's a ScrollArea, it can properly scroll to the cursor. - // Condition `!clip_text` is important to avoid breaking layout for `TextEdit::singleline` (PR #5640) - if !clip_text - && let extra_size = galley.size() - rect.size() - && (extra_size.x > 0.0 || extra_size.y > 0.0) - { - match ui.layout().main_dir() { - crate::Direction::LeftToRight | crate::Direction::TopDown => { - ui.allocate_rect( - Rect::from_min_size(outer_rect.max, extra_size), - Sense::hover(), - ); - } - crate::Direction::RightToLeft => { - ui.allocate_rect( - Rect::from_min_size( - emath::pos2(outer_rect.min.x - extra_size.x, outer_rect.max.y), - extra_size, - ), - Sense::hover(), - ); - } - crate::Direction::BottomUp => { - ui.allocate_rect( - Rect::from_min_size( - emath::pos2(outer_rect.min.x, outer_rect.max.y - extra_size.y), - extra_size, - ), - Sense::hover(), - ); - } - } - } else { - // Avoid an ID shift during this pass if the textedit grow - ui.skip_ahead_auto_ids(1); - } - painter.galley(galley_pos, Arc::clone(&galley), text_color); if has_focus && let Some(cursor_range) = state.cursor.range(&galley) { @@ -767,7 +829,7 @@ impl TextEdit<'_> { if response.changed() || selection_changed { // Scroll to keep primary cursor in view: - ui.scroll_to_rect(primary_cursor_rect + margin, None); + ui.scroll_to_rect(primary_cursor_rect, None); } if text.is_mutable() && interactive { @@ -796,9 +858,9 @@ impl TextEdit<'_> { .layer_transform_to_global(ui.layer_id()) .unwrap_or_default(); - ui.ctx().output_mut(|o| { + ui.output_mut(|o| { o.ime = Some(crate::output::IMEOutput { - rect: to_global * rect, + rect: to_global * inner_rect, cursor_rect: to_global * primary_cursor_rect, }); }); @@ -846,24 +908,22 @@ impl TextEdit<'_> { }); } - { - let role = if password { - accesskit::Role::PasswordInput - } else if multiline { - accesskit::Role::MultilineTextInput - } else { - accesskit::Role::TextInput - }; + let role = if password { + accesskit::Role::PasswordInput + } else if multiline { + accesskit::Role::MultilineTextInput + } else { + accesskit::Role::TextInput + }; - crate::text_selection::accesskit_text::update_accesskit_for_text_widget( - ui.ctx(), - id, - cursor_range, - role, - TSTransform::from_translation(galley_pos.to_vec2()), - &galley, - ); - } + crate::text_selection::accesskit_text::update_accesskit_for_text_widget( + ui.ctx(), + id, + cursor_range, + role, + TSTransform::from_translation(galley_pos.to_vec2()), + &galley, + ); TextEditOutput { response, @@ -911,7 +971,7 @@ fn events( event_filter: EventFilter, return_key: Option, ) -> (bool, CCursorRange) { - let os = ui.ctx().os(); + let os = ui.os(); let mut cursor_range = state.cursor.range(galley).unwrap_or(default_cursor_range); diff --git a/crates/egui_demo_app/tests/snapshots/imageviewer.png b/crates/egui_demo_app/tests/snapshots/imageviewer.png index 8d5f688bf..a97c3ea8b 100644 --- a/crates/egui_demo_app/tests/snapshots/imageviewer.png +++ b/crates/egui_demo_app/tests/snapshots/imageviewer.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c8b23a5286e5d2dbd8d3eddac6583d981152bd791f74edfa5c712a610f795256 -size 96759 +oid sha256:73592be3cb5e2bbc1de870050b913b307e31c05df339b2fd78e9ce38c05f4cd2 +size 96758 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png index 2549417be..47ad5bc7a 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a53262cf5d8507d8eeae8c968767cef462b727879245085673982b850a6da670 +oid sha256:dfccdafb7e96db488bb5bb8c0a7d25f70e63d900d6b1c2280d218aac0e70e4c4 size 26977 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png b/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png index 46a5ed1f7..8c6077a1c 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a7601584308bf60820506f842569a3c1daf3c15fa6e715f6b9386b5112dcc92f -size 76076 +oid sha256:b849adf0ff9a06e1f7bfa22ef32b13224c7e62429cf675286110729156434d12 +size 76531 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png b/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png index 375b0f922..f73093e3e 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3fc2793506ec483c7f124b6206fb18ffb73bec29746f2d9bb5145042ddc45016 -size 114410 +oid sha256:8c630df841e98043132b920338793745549116890c1bc52d8d10b0def896a1e6 +size 114409 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png b/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png index 843bb93b4..6b1a9946b 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:718203d31d8b027a7718a66c4712cf1e17b9aea2e870d755bd2c0c346529d4f4 +oid sha256:f5e9d645e1decf7624bc8f031b5e28213e64fc5f568dd3eeb1768a57fcb988c3 size 21814 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png b/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png index 81204a347..92992cd83 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6af5adc42544171c6d85e190c853aca06784c131a373a693a6f7069d4cf1a404 -size 13698 +oid sha256:99fa5a5cb10c7d277eafb258af6019eda24a3c96075a50db321f52a521dede92 +size 13700 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png b/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png index 78446cca9..cdc1a43dd 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2e8e03c2a42e195e6489659053aecb78755d3c218558cb2e9339fa7b6db59405 -size 35875 +oid sha256:1cc61413bcce62cc8e0a55460a974bb56ac40936cd2e5512c4a0e0c521eaaae4 +size 35874 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png index a6d103d6d..96ca9949e 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ad22ea6b6e69fd71416fdae76cbd142d279f8f562e74b77e63b3989be187c57c -size 484631 +oid sha256:9c81d5a5915dac60297293b90cd3fc0d188a9335e99b318996e3e2934de7bee1 +size 483497 diff --git a/tests/egui_tests/tests/regression_tests.rs b/tests/egui_tests/tests/regression_tests.rs index 421b69d35..d61b76af3 100644 --- a/tests/egui_tests/tests/regression_tests.rs +++ b/tests/egui_tests/tests/regression_tests.rs @@ -1,6 +1,9 @@ use egui::accesskit::Role; use egui::epaint::Shape; -use egui::{Align, Color32, Image, Label, Layout, RichText, Sense, TextWrapMode, include_image}; +use egui::style::ScrollAnimation; +use egui::{ + Align, Color32, Image, Label, Layout, RichText, ScrollArea, Sense, TextWrapMode, include_image, +}; use egui_kittest::Harness; use egui_kittest::kittest::Queryable as _; @@ -63,6 +66,67 @@ fn text_edit_rtl() { } } +#[test] +fn text_edit_delay() { + let mut text = String::new(); + let mut harness = Harness::builder().with_size((200.0, 50.0)).build_ui(|ui| { + ui.style_mut().scroll_animation = ScrollAnimation::none(); + ui.add(egui::TextEdit::singleline(&mut text).hint_text("Write something")); + }); + + harness.get_by_role(Role::TextInput).focus(); + harness.step(); + harness.snapshot("text_edit_delay_0_empty"); + + harness.get_by_role(Role::TextInput).type_text("h"); + + // When the text is empty, and we show the hint text, there is a frame delay. + harness.step(); + harness.snapshot("text_edit_delay_1_h_invisible"); + + // Now it should be visible + harness.step(); + harness.snapshot("text_edit_delay_2_h_visible"); + + harness.get_by_role(Role::TextInput).type_text("i"); + + // The "i" should immediately be visible without a delay + harness.step(); + harness.snapshot("text_edit_delay_3_i_visible"); + + // The next frame should exactly match the previous one + harness.step(); + harness.snapshot("text_edit_delay_4_i_visible"); +} + +#[test] +fn text_edit_scroll() { + let mut text = "1\n2\n3\n4\n".to_owned(); + let mut harness = Harness::builder().build_ui(|ui| { + ScrollArea::vertical().max_height(40.0).show(ui, |ui| { + ui.add( + egui::TextEdit::multiline(&mut text) + .desired_rows(2) + .hint_text("Write something"), + ); + }); + }); + + harness.fit_contents(); + + harness.get_by_role(Role::MultilineTextInput).focus(); + harness.step(); + harness.snapshot("text_edit_scroll_0_focus"); + + harness + .get_by_role(Role::MultilineTextInput) + .type_text("5\n"); + + // When the text is empty, and we show the hint text, there is a frame delay. + harness.run(); + harness.snapshot("text_edit_scroll_1_5"); +} + #[test] fn combobox_should_have_value() { let harness = Harness::new_ui(|ui| { diff --git a/tests/egui_tests/tests/snapshots/layout/text_edit.png b/tests/egui_tests/tests/snapshots/layout/text_edit.png index cfbaefd41..d5d853f5e 100644 --- a/tests/egui_tests/tests/snapshots/layout/text_edit.png +++ b/tests/egui_tests/tests/snapshots/layout/text_edit.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:30de3e9f9645206e33fa1edd841b48228e154d0ceae962c64c060a66eecd73ba -size 220452 +oid sha256:29363b37f1260f9f39edf9ba873f4c33c0d8a8b6670f6fc178459019539ae7e3 +size 220588 diff --git a/tests/egui_tests/tests/snapshots/layout/text_edit_clip.png b/tests/egui_tests/tests/snapshots/layout/text_edit_clip.png index 8ea5d0b7a..d87f37561 100644 --- a/tests/egui_tests/tests/snapshots/layout/text_edit_clip.png +++ b/tests/egui_tests/tests/snapshots/layout/text_edit_clip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9f36b8623d2d9c35e337e973f547166f62a5daae757c462b1482babdd42c941 -size 383051 +oid sha256:94186c0b9331fd0d13284126f4f5e92e66014105fb6533422516d4fbe765e4c7 +size 372041 diff --git a/tests/egui_tests/tests/snapshots/layout/text_edit_placeholder_clip.png b/tests/egui_tests/tests/snapshots/layout/text_edit_placeholder_clip.png index 0a84f42db..578e4d9db 100644 --- a/tests/egui_tests/tests/snapshots/layout/text_edit_placeholder_clip.png +++ b/tests/egui_tests/tests/snapshots/layout/text_edit_placeholder_clip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a2734644b2fbb6f42ddab6c65a1f5d073f1f002900bbd814c1edb6184e0a9c0 -size 362521 +oid sha256:8ff058ef716689c309ae9806aaf08fb64eca545ef8f92ce89e1f8e9b7b7733bc +size 330200 diff --git a/tests/egui_tests/tests/snapshots/layout/text_edit_prefix_suffix.png b/tests/egui_tests/tests/snapshots/layout/text_edit_prefix_suffix.png new file mode 100644 index 000000000..bdcab38f2 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/layout/text_edit_prefix_suffix.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf00e99dbfdf7497688955feb8c417fab0a366588d92182eccee775abade5179 +size 361876 diff --git a/tests/egui_tests/tests/snapshots/text_edit_delay_0_empty.png b/tests/egui_tests/tests/snapshots/text_edit_delay_0_empty.png new file mode 100644 index 000000000..58b0b13f2 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/text_edit_delay_0_empty.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf62a248bcec1054cbd97251e6fc429972ef2318c24b9a56698d7c80115aa57e +size 2262 diff --git a/tests/egui_tests/tests/snapshots/text_edit_delay_1_h_invisible.png b/tests/egui_tests/tests/snapshots/text_edit_delay_1_h_invisible.png new file mode 100644 index 000000000..c1920bcf1 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/text_edit_delay_1_h_invisible.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef70c95f7e171984f992e1b9366b4a0fe11a4871746cb8cfaa8ee263e59de702 +size 2272 diff --git a/tests/egui_tests/tests/snapshots/text_edit_delay_2_h_visible.png b/tests/egui_tests/tests/snapshots/text_edit_delay_2_h_visible.png new file mode 100644 index 000000000..bc1ffcd08 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/text_edit_delay_2_h_visible.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ab120260212d0f41d2956ea2d679cfed648cb188badcca7fa82e0dec9c87ec1a +size 714 diff --git a/tests/egui_tests/tests/snapshots/text_edit_delay_3_i_visible.png b/tests/egui_tests/tests/snapshots/text_edit_delay_3_i_visible.png new file mode 100644 index 000000000..324946e10 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/text_edit_delay_3_i_visible.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2ea9d6bef26f1dbd7dc272b4331012ad6fd1a43fded150d33ba4762d404dbde +size 775 diff --git a/tests/egui_tests/tests/snapshots/text_edit_delay_4_i_visible.png b/tests/egui_tests/tests/snapshots/text_edit_delay_4_i_visible.png new file mode 100644 index 000000000..324946e10 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/text_edit_delay_4_i_visible.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2ea9d6bef26f1dbd7dc272b4331012ad6fd1a43fded150d33ba4762d404dbde +size 775 diff --git a/tests/egui_tests/tests/snapshots/text_edit_rtl_0.png b/tests/egui_tests/tests/snapshots/text_edit_rtl_0.png index 3b87786e8..796a1e3b5 100644 --- a/tests/egui_tests/tests/snapshots/text_edit_rtl_0.png +++ b/tests/egui_tests/tests/snapshots/text_edit_rtl_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce357224c2e1cf32f96b3d075dc070c4d14e9aaca1b8165d0ba98603dff19c1b -size 2324 +oid sha256:ed3665dfb232b8f0b1483802bfafb4605e8361d7eb977de5a58862e52ab724fa +size 2296 diff --git a/tests/egui_tests/tests/snapshots/text_edit_rtl_1.png b/tests/egui_tests/tests/snapshots/text_edit_rtl_1.png index 5d9aada78..cd6d5a621 100644 --- a/tests/egui_tests/tests/snapshots/text_edit_rtl_1.png +++ b/tests/egui_tests/tests/snapshots/text_edit_rtl_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d91c715ac66be329cac42ff7c7726348b0ac79d897c414bbde26bb0115781577 -size 2289 +oid sha256:5cfdd6255aba92f0253571bde4f22afc8e0fb5fe3cb882946459d623c0a5d89c +size 2982 diff --git a/tests/egui_tests/tests/snapshots/text_edit_scroll_0_focus.png b/tests/egui_tests/tests/snapshots/text_edit_scroll_0_focus.png new file mode 100644 index 000000000..1d0a5ed46 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/text_edit_scroll_0_focus.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:81620caad6d420f3bd0f224e5b07a02960a42436208a98d3aa012e5db61a743a +size 1510 diff --git a/tests/egui_tests/tests/snapshots/text_edit_scroll_1_5.png b/tests/egui_tests/tests/snapshots/text_edit_scroll_1_5.png new file mode 100644 index 000000000..bdb8d1b1b --- /dev/null +++ b/tests/egui_tests/tests/snapshots/text_edit_scroll_1_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f915eafb6490ff456c5b0a7c74c38ef143262bdf74a0c6561b9cf6ee66a679ea +size 1501 diff --git a/tests/egui_tests/tests/snapshots/visuals/drag_value.png b/tests/egui_tests/tests/snapshots/visuals/drag_value.png index 8be9c5e9f..7cd2d2ce8 100644 --- a/tests/egui_tests/tests/snapshots/visuals/drag_value.png +++ b/tests/egui_tests/tests/snapshots/visuals/drag_value.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:71daf8a33d277075012bf1130d7820574fe0286080154810d8d398c005a65127 -size 9037 +oid sha256:60ad2d88535977244ac0fa153700489b454a582af2829dc2f41a531943a21d7a +size 9079 diff --git a/tests/egui_tests/tests/snapshots/visuals/text_edit.png b/tests/egui_tests/tests/snapshots/visuals/text_edit.png index 4de5e3bd0..4719c8ce9 100644 --- a/tests/egui_tests/tests/snapshots/visuals/text_edit.png +++ b/tests/egui_tests/tests/snapshots/visuals/text_edit.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8d259d113aa23089992b04f19e71c743272dda3fc9baa9612565158f15ace57e -size 8159 +oid sha256:7e1fb3fb0a00a447906aa205c27aa496dcb3d79e98aadf6092811a0514efb5a0 +size 8127 diff --git a/tests/egui_tests/tests/snapshots/visuals/text_edit_clip.png b/tests/egui_tests/tests/snapshots/visuals/text_edit_clip.png index 192fa8f74..8a5999742 100644 --- a/tests/egui_tests/tests/snapshots/visuals/text_edit_clip.png +++ b/tests/egui_tests/tests/snapshots/visuals/text_edit_clip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a1e56f1b6970c14830d8869f4d8cacfed821ec2b3aab7033b1bfd213a864da79 -size 10959 +oid sha256:077e7de9fdaaa222ee75f6ad620967fb1e29da37f60407d584be7141e9d0badd +size 10143 diff --git a/tests/egui_tests/tests/snapshots/visuals/text_edit_placeholder_clip.png b/tests/egui_tests/tests/snapshots/visuals/text_edit_placeholder_clip.png index decd09bf9..19c231b45 100644 --- a/tests/egui_tests/tests/snapshots/visuals/text_edit_placeholder_clip.png +++ b/tests/egui_tests/tests/snapshots/visuals/text_edit_placeholder_clip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:493649ea09351835147aa6cc800858939dd44beafe37adc488b63b291d58e3b3 -size 10302 +oid sha256:b022e27d7275764df45039abd26f80d69af40fb18bec98cca85565850df859ae +size 8838 diff --git a/tests/egui_tests/tests/snapshots/visuals/text_edit_prefix_suffix.png b/tests/egui_tests/tests/snapshots/visuals/text_edit_prefix_suffix.png new file mode 100644 index 000000000..d27f6f8c4 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/visuals/text_edit_prefix_suffix.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:337dcbf0b3a344c6cadaf9500376a627739e19e9c47b5da23786c98c612ef4dc +size 10028 diff --git a/tests/egui_tests/tests/test_widgets.rs b/tests/egui_tests/tests/test_widgets.rs index 5ef98c8a8..5283b21d4 100644 --- a/tests/egui_tests/tests/test_widgets.rs +++ b/tests/egui_tests/tests/test_widgets.rs @@ -122,6 +122,18 @@ fn widget_tests() { }, &mut results, ); + test_widget( + "text_edit_prefix_suffix", + |ui| { + ui.spacing_mut().text_edit_width = 45.0; + TextEdit::singleline(&mut "Hello World".to_owned()) + .prefix("πŸ”Ž") + .suffix("!") + .clip_text(true) + .ui(ui) + }, + &mut results, + ); test_widget( "slider", From 4714aa7d311fea148e165d293d0800eda518b50e Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sat, 21 Mar 2026 21:52:52 +0100 Subject: [PATCH 29/58] Fix instable IDs following animated panels (#7994) * Found thanks to https://github.com/emilk/egui/pull/7984 If you put an animated `Panel` inside a `Ui`, then the automatic ids for following widgets would differ when the panel was collapsed or open. --- crates/egui/src/containers/panel.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/containers/panel.rs b/crates/egui/src/containers/panel.rs index f2a4c3b67..4a83ce8d1 100644 --- a/crates/egui/src/containers/panel.rs +++ b/crates/egui/src/containers/panel.rs @@ -561,7 +561,11 @@ impl Panel { let how_expanded = animate_expansion(ui.ctx(), self.id.with("animation"), is_expanded); // Get either the fake or the real panel to animate - let animated_panel = self.get_animated_panel(ui.ctx(), is_expanded)?; + let Some(animated_panel) = self.get_animated_panel(ui.ctx(), is_expanded) else { + // Make sure the ids of the next widgets are the same whether we show the panel or not: + ui.skip_ahead_auto_ids(1); + return None; + }; if how_expanded < 1.0 { // Show a fake panel in this in-between animation state: From 1b2a06534232a86f8e1c26b714603ffec6ff97e6 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sat, 21 Mar 2026 21:54:26 +0100 Subject: [PATCH 30/58] Ignore rustsec --- deny.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/deny.toml b/deny.toml index 01377b90b..486af7745 100644 --- a/deny.toml +++ b/deny.toml @@ -34,6 +34,7 @@ ignore = [ "RUSTSEC-2024-0320", # unmaintained yaml-rust pulled in by syntect "RUSTSEC-2024-0436", # unmaintained paste pulled via metal/wgpu, see https://github.com/gfx-rs/metal-rs/issues/349 "RUSTSEC-2025-0141", # https://rustsec.org/advisories/RUSTSEC-2025-0141 - bincode is unmaintained - https://git.sr.ht/~stygianentity/bincode/tree/v3.0/item/README.md + "RUSTSEC-2026-0049", # https://rustsec.org/advisories/RUSTSEC-2026-0049 ] [bans] From 49fad9a7b2b8f99f0081b105e7fd4f5851238ac5 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sat, 21 Mar 2026 22:47:15 +0100 Subject: [PATCH 31/58] Roll out new egui icon and logo (#7995) For the first time _ever_, egui has a logo! egui-logo Made by [Studio Gruhl](https://www.studiogruhl.com/) and paid for by [Rerun.io](https://rerun.io/). --- README.md | 10 ++-- crates/eframe/data/icon.png | Bin 17166 -> 12052 bytes crates/egui_demo_app/src/wrap_app.rs | 2 + .../egui_demo_app/tests/snapshots/clock.png | 4 +- .../tests/snapshots/custom3d.png | 4 +- .../tests/snapshots/easymarkeditor.png | 4 +- .../tests/snapshots/imageviewer.png | 4 +- crates/egui_demo_lib/data/egui-logo.svg | 21 ++++++++ crates/egui_demo_lib/data/icon.png | Bin 2642 -> 1720 bytes crates/egui_demo_lib/data/icon.svg | 5 ++ crates/egui_demo_lib/data/peace.svg | 11 ---- crates/egui_demo_lib/src/demo/about.rs | 36 ++++++++----- .../src/demo/demo_app_windows.rs | 49 ++++++++++-------- .../egui_demo_lib/src/demo/tests/svg_test.rs | 2 +- .../egui_demo_lib/src/demo/widget_gallery.rs | 7 +-- .../tests/snapshots/demos/Clipboard Test.png | 4 +- .../tests/snapshots/demos/SVG Test.png | 4 +- .../tests/snapshots/demos/Scene.png | 4 +- .../snapshots/widget_gallery_dark_x1.png | 4 +- .../snapshots/widget_gallery_dark_x2.png | 4 +- .../snapshots/widget_gallery_light_x1.png | 4 +- .../snapshots/widget_gallery_light_x2.png | 4 +- .../tests/snapshots/menu/closed_hovered.png | 4 +- .../tests/snapshots/menu/opened.png | 4 +- .../tests/snapshots/menu/submenu.png | 4 +- .../tests/snapshots/menu/subsubmenu.png | 4 +- .../snapshots/should_wait_for_images.png | 4 +- .../tests/snapshots/layout/atoms_image.png | 4 +- .../tests/snapshots/layout/button_image.png | 4 +- .../layout/button_image_shortcut.png | 4 +- .../tests/snapshots/visuals/button_image.png | 4 +- .../visuals/button_image_shortcut.png | 4 +- .../button_image_shortcut_selected.png | 4 +- web_demo/favicon.ico | Bin 15406 -> 2932 bytes 34 files changed, 129 insertions(+), 102 deletions(-) create mode 100644 crates/egui_demo_lib/data/egui-logo.svg create mode 100644 crates/egui_demo_lib/data/icon.svg delete mode 100644 crates/egui_demo_lib/data/peace.svg mode change 100755 => 100644 web_demo/favicon.ico diff --git a/README.md b/README.md index f4a094465..7eebd0423 100644 --- a/README.md +++ b/README.md @@ -10,16 +10,14 @@ [![Discord](https://img.shields.io/discord/900275882684477440?label=egui%20discord)](https://discord.gg/JFcEma9bJq) +
- + -egui development is sponsored by [Rerun](https://www.rerun.io/), a startup building
-an SDK for visualizing streams of multimodal data. -
- ---- +
πŸ‘‰ [Click to run the web demo](https://www.egui.rs/#demo) πŸ‘ˆ + egui (pronounced "e-gooey") is a simple, fast, and highly portable immediate mode GUI library for Rust. egui runs on the web, natively, and [in your favorite game engine](#integrations). diff --git a/crates/eframe/data/icon.png b/crates/eframe/data/icon.png index cf1e6c3ebdb52209943a9affac3b463b2c85e717..4ce7cc588ecd1b3a0bf81a79dc04677c21e9bf98 100644 GIT binary patch literal 12052 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 literal 17166 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 + + + + + + + + + + + + + + + + + + + + diff --git a/crates/egui_demo_lib/data/icon.png b/crates/egui_demo_lib/data/icon.png index 87f15e746e42df190f84317c76a22f036c50a234..27a6c9534b32f4704020966693d11135c3938aff 100644 GIT binary patch delta 1654 zcmV-+28sF76u1qLBpLy8Qb$4o*~u(_00004XF*Lt006O%3;baPks&pI1ONa4#|gtq ztN;K232;bRa{vG?BLDy{BLR4&KXw2B00(qQO+^Rl0~HS?EQ%Yn00005{7FPXRCwCu zn_WnhSs2HEGvgRoYEjO(yS7s7m|YaoRnbKR1F|mdE)`t`wP|>h)re?OpaoGfT@)eM zNV~aM5ZP|HpaandU6kH`*sf~`+C?(DX)+~3<2WDh*$Z`Pb-vzt&pVm(hXXU5GtZpo z|DN~!pXYoShz~39Jx~UGGxt4@rHKh(92f#Rfezqpvc?L5Rv-+B&$NVro50sEO(W;$ z%P#!lg@>WDzg{plCHz zS63%eiJ6%hVYAtPWDuaB)riGn5tEaXBGqVbZn_2=g zGc&n(@#2yQ=PVWrr%#_wEddUP1DnmZWMWz^{V}GRaXp}AL>g25&Sht3>q3H-IZ0R9 zTZjh_9z?Z2GBTpcc5iQQGP0O>cKnNkq%ggm4VDH|&(!$uLrY7A8 z7#kZCIXO9hatTXkGC6h#q-4js~kfF!cT_>F37Yo#YMilT_imoJOl++67d z`2BvdapOig$&l4*WpHqioSd9Rb;HBM+`W4@VhC%sTCv$|Y~Q{;X1F{xHAPof7Xt$W zWMyR`MB=$wUS1xD4%v)!wdMyR^Lmn>KBVDSMTbl?ivWs;Vk30aaC1stI`d^l4Ok&z?QgmJODc zma5M7qT$-LYq(slm~AR5Dk2W&WU*t%4%Vz$lYAsNH8mwQT0498EYs7|QR}yE-Ku&~ zBo+dHfdGELKY0S~-@niM_wVJMUYMBR;lqb9+cY*dQdwCk<^S&7xf7G{xCBg2PIB(t zxv2H|`T43l)!*MAd$+=5qPe*lx7&@~ZpUmkNAJAR$hvjwBG&nQKF*vu6SqHcWzcLk z)7RHWadGit`4EEP;o+F8JPXm@-cCnH2RnCv?xeiDoT8$lnC*kXAmiiX1OkCYb*omb z!eX&R{N4HU=c%u+my-;Ym6b^gfEpVc|Fdg0n?-$ny-*ZIYLMmec*M$;E9K?_Zns-H z0rRD%rba3O2M->UpA2o@yjcVS0bK|H#G^-#q&VN*-7O3TgZx$Dfq?<8U%#%~s?L>v zD_69y?+by$;ZPM$t6C3OyLPR3_wJo8BOog)OY>ZC>((uO_XWIOZ%kR#0-w)^*Xz~v z`%|Y*>Axn})YQbwmoIf8VP$*!^T*{!nLRz7P6z{E7@56i*GBrGbxr^P_zLimURP$;FJ$Co z8WB(xZCIrBwKzidJo=gjXk2@Oj;Qp+?!5Dx)I z=V@Dc761U?C0N;-^RoDb1XcLI{;lN%0Psy(VNIN)rdIQb4j!YDJ!n?jL1$eH1}Lce zJQ)*D(Ra)ep_V+VsI+sRCL7)=SmJ2At!5Cb3LlJ+1E*inkc-p<#+=*ggwn`T2C6|W zYf6oR475!a+vK)1#cFXeh^7-lE&c7$dp*9=Qc{eJMlR$W88=xUIoHD4TRX>^315#J zK|Qob`Wd1HBodRqZ;9w=6@Hd1F#o-xVEpyjqlb^_B#a{zQAQh^63GNS5{@bhYm+tQ z=7+p^?`Y!eru9@3yW3Kz3UPH#J+G<)EtTdHqL*TjaN*vw=5LARBHVvro>k&dqp#LXQe1}K={vQ3`aRs$pJC#W(V9P zz3eLb<19&8mk1Vn_2nZ!-=Ov}Oi-)JJ|E{4vj{&>29OTr_YUMVl0I$i)c*nhRU$th zUp!x<5Bkpd%^*D?8G2aUcAPo|S!9GxnMk?y5CORcX~4GvTr*Zb7+3dluW<#MC)|oP zlIQZ?i)7j{3ns_tT+ZB$?$)@iC|M9F)fA9yd6KyCX>zUk9_K}Xj0+I-IZ<65qy1zz z5*hmW=Ju3{Ga!e%sVBD$3WfJ~D4`KmKS zUsZ;FTs;g}D$(aBMnRVOFcG$3hGqX~uO30Jn?$UF=MxS3;^rE41Z&1^Y;Q|yU(#75 z*0TzG;*a5)W`h^lC{Is|$(9gwNbAnkspE<&x_Su#GN(J(d@-a*1c=mDO8`|qnSA?N zva03Nc{zD`SBFd14Yza0@~W##T3c~~8ECfNy54$cXga7bVg#J^Wow5km6U&f?s`>> zmzQGXOnbTz`jB6H`70j?c!A0%DH>1g1lZ*0%%_e%JaIPS!S8#*D02*Y1N;tcs5W>d zIsf9%qOHiy!jRdN>2r0DZ{YEZG=-Y*r=^LOYSum`Tr^r*WlhcE%v)=KmX=o34aQJt zv&a6LFcGU}(41W$3Q?JfShkgYyPd=^`oYOE+Mb>pVaICp`5|UMtVTFF-qI&F*0dzL zhqQ|p4_Wy5C~yB{?okztx|Ty)Qv&ivRtFP$`y^_0(rjRrtWMrF4H+p?#?zd~So-j&+>u@EL} zdTyWR*&~IB^CcQh+wDsAY~qgeO1_=<1egIKU-;HrV!yFqlRx}z^rc)A=ec0`f{KC$ z25SDhr@~7)`QbYB;cZpoZvPvrbLT|oIdp)%@c}r~5=8Qb_vUHWgbu5IOb6ujcVDhP z5R2_l2v1=66deqNcP+B5gs+b1O*RJ^tm_$++(X}*e6#5=&pV~?|wDz0A1 z$zGiq#cmcwmVf__O<3&7s1F)nqgFj%xM^+nTX;TzA~EX1y69zG_b6tWQiv^i50!_N zN>F%(%h1x2lCQi?$crG>cEr4spa-JM;+y)?`^F;XuMqYy;s*Z9F#Pt(#3t}qR!doa5$JRQJ*qoTgRd+tV$)TO~*ixdwTOy4b%J4$_U@bDHBW5dHmOOwXQ^fZHd z_+^r@oF47N3;m|gM8<2{)<%4}u?Za=O({Qq{GhHREsuwFe|_@+{LUk8QC3;>S);)P zucDl!ej>~ayRj4FK_x&>QYciVJFr6NPt6+?M{8vjJK6VDvd;=|iQ7syo|U~$?M{>8 za_}%Jc6lks_i=A&ZOMnp0hpPYX_g|q+=7le$oZlv16Rz-d)Yy6iQdk%RP(pSUIHSj zQ89==ffL62#(2%r3ZwG)t47};FE1ngW5y?mlU5^ZjlYK8)6)|So*)!&<=ch_gTw}NU^(P(r~ z%im%K)8Y*}Y4Yu{;;{YIZnE5P7053in24h4{_%F@=4?Y@WZpJ&dk+eQcCz~A$BDsJ z-`t?-5s~$`m-W4-sN}UZMU66g4Y^AK#T^YoHJBA3Zp3yJ4;5VQ|Av#^Y!)0WZ{5Ej zASe*pJo^;M#>R(+Kvb4y6)4J5 zX{S*#^5f^RVWKEnAf?2Y_aaXl_k6I3BR$A&?x4g{_Ta6k-iDH>V{qS$@{p05ORh17 zD!I3^s&Q;xXc=qTt)UJX4pGt?`*Ml7Gr2~)H&O98nETDBgdYvgV679CjvXJ07*aU; z0&{&V$6|I-K&suryeE?jGT0-n%Pgn2KjxK7yOr`9tCKn|ZJGJX0)%!LD$qI}`hVV20xTvQHem;U=zaX;^JRTQJ z09aU^hlJt61McGdAR)n!8w5gVxFHONyL&yr|CDbC9`=v;{A^R*5i0fzbqet(MEZo` Y0RIURdCI7FJ(2-d=62X>Q}6hH1N + + + + \ No newline at end of file diff --git a/crates/egui_demo_lib/data/peace.svg b/crates/egui_demo_lib/data/peace.svg deleted file mode 100644 index 4bf3e33a9..000000000 --- a/crates/egui_demo_lib/data/peace.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/crates/egui_demo_lib/src/demo/about.rs b/crates/egui_demo_lib/src/demo/about.rs index 56e2f1eaa..853ebf490 100644 --- a/crates/egui_demo_lib/src/demo/about.rs +++ b/crates/egui_demo_lib/src/demo/about.rs @@ -27,20 +27,30 @@ impl crate::View for About { fn ui(&mut self, ui: &mut egui::Ui) { use egui::special_emojis::{OS_APPLE, OS_LINUX, OS_WINDOWS}; - ui.heading("egui"); + ui.vertical_centered(|ui| { + ui.add_space(4.0); + let egui_icon = egui::include_image!("../../data/egui-logo.svg"); + ui.add( + egui::Image::new(egui_icon.clone()) + .max_height(30.0) + .tint(ui.visuals().strong_text_color()), + ); + ui.add_space(4.0); + }); + ui.label(format!( - "egui is an immediate mode GUI library written in Rust. egui runs both on the web and natively on {}{}{}. \ - On the web it is compiled to WebAssembly and rendered with WebGL.{}", + "egui is an immediate mode GUI library written in Rust. egui runs natively on {}{}{}, and \ + on the web it is compiled to WebAssembly and rendered with WebGL or WebGPU.{}", OS_APPLE, OS_LINUX, OS_WINDOWS, if cfg!(target_arch = "wasm32") { " Everything you see is rendered as textured triangles. There is no DOM, HTML, JS or CSS. Just Rust." } else {""} )); - ui.label("egui is designed to be easy to use, portable, and fast."); ui.add_space(12.0); + ui.label("egui is easy to use, portable, and fast."); - ui.heading("Immediate mode"); + ui.add_space(12.0); about_immediate_mode(ui); ui.add_space(12.0); @@ -52,12 +62,12 @@ impl crate::View for About { ui.horizontal_wrapped(|ui| { ui.spacing_mut().item_spacing.x = 0.0; - ui.label("egui development is sponsored by "); + ui.weak("egui development is sponsored by "); ui.hyperlink_to("Rerun.io", "https://www.rerun.io/"); - ui.label(", a startup building an SDK for visualizing streams of multimodal data. "); - ui.label("For an example of a real-world egui app, see "); + ui.weak(", a startup building a data platform for robotics. "); + ui.weak("For an example of a professional egui app, run "); ui.hyperlink_to("rerun.io/viewer", "https://www.rerun.io/viewer"); - ui.label(" (runs in your browser)."); + ui.weak(" (in your browser!)."); }); ui.add_space(12.0); @@ -72,11 +82,9 @@ fn about_immediate_mode(ui: &mut egui::Ui) { ui.style_mut().spacing.interact_size.y = 0.0; // hack to make `horizontal_wrapped` work better with text. ui.horizontal_wrapped(|ui| { - ui.spacing_mut().item_spacing.x = 0.0; - ui.label("Immediate mode is a GUI paradigm that lets you create a GUI with less code and simpler control flow. For example, this is how you create a "); - let _ = ui.small_button("button"); - ui.label(" in egui:"); - }); + ui.spacing_mut().item_spacing.x = 0.0; + ui.label("This is how you create a button in egui:"); + }); ui.add_space(8.0); crate::rust_view_ui( diff --git a/crates/egui_demo_lib/src/demo/demo_app_windows.rs b/crates/egui_demo_lib/src/demo/demo_app_windows.rs index d2cc17448..1c7831016 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -119,21 +119,30 @@ impl Default for DemoGroups { } impl DemoGroups { + pub fn about_egui_checkbox(&mut self, ui: &mut Ui, open: &mut BTreeSet) { + let Self { about, .. } = self; + let mut is_open = open.contains(about.name()); + ui.toggle_value(&mut is_open, about.name()); + set_open(open, about.name(), is_open); + } + pub fn checkboxes(&mut self, ui: &mut Ui, open: &mut BTreeSet) { let Self { - about, + about: _, demos, tests, } = self; - { - let mut is_open = open.contains(about.name()); - ui.toggle_value(&mut is_open, about.name()); - set_open(open, about.name(), is_open); - } - ui.separator(); + ui.vertical_centered(|ui| { + ui.strong("Demos"); + }); demos.checkboxes(ui, open); + ui.separator(); + + ui.vertical_centered(|ui| { + ui.strong("Tests"); + }); tests.checkboxes(ui, open); } @@ -267,22 +276,20 @@ impl DemoWindows { .default_size(160.0) .min_size(160.0) .show_inside(ui, |ui| { - ui.add_space(4.0); - ui.vertical_centered(|ui| { - ui.heading("βœ’ egui demos"); + ui.vertical_centered_justified(|ui| { + ui.add_space(4.0); + ui.add( + egui::Image::new(egui::include_image!("../../data/egui-logo.svg")) + .max_height(32.0) + .tint(ui.visuals().strong_text_color()), + ); + + ui.add_space(4.0); + + self.groups.about_egui_checkbox(ui, &mut self.open); }); - ui.separator(); - - use egui::special_emojis::GITHUB; - ui.hyperlink_to( - format!("{GITHUB} egui on GitHub"), - "https://github.com/emilk/egui", - ); - ui.hyperlink_to( - "@ernerfeldt.bsky.social", - "https://bsky.app/profile/ernerfeldt.bsky.social", - ); + ui.add_space(4.0); ui.separator(); diff --git a/crates/egui_demo_lib/src/demo/tests/svg_test.rs b/crates/egui_demo_lib/src/demo/tests/svg_test.rs index cd73f9150..9c188df74 100644 --- a/crates/egui_demo_lib/src/demo/tests/svg_test.rs +++ b/crates/egui_demo_lib/src/demo/tests/svg_test.rs @@ -30,7 +30,7 @@ impl crate::View for SvgTest { fn ui(&mut self, ui: &mut egui::Ui) { let Self { color } = self; ui.color_edit_button_srgba(color); - let img_src = egui::include_image!("../../../data/peace.svg"); + let img_src = egui::include_image!("../../../data/icon.svg"); // First paint a small version, sized the same as the source… ui.add( diff --git a/crates/egui_demo_lib/src/demo/widget_gallery.rs b/crates/egui_demo_lib/src/demo/widget_gallery.rs index fe783af1c..6e23fca92 100644 --- a/crates/egui_demo_lib/src/demo/widget_gallery.rs +++ b/crates/egui_demo_lib/src/demo/widget_gallery.rs @@ -229,7 +229,7 @@ impl WidgetGallery { ui.end_row(); ui.add(doc_link_label("Image", "Image")); - let egui_icon = egui::include_image!("../../data/icon.png"); + let egui_icon = egui::include_image!("../../data/icon.svg"); ui.add(egui::Image::new(egui_icon.clone())); ui.end_row(); @@ -237,10 +237,7 @@ impl WidgetGallery { "Button with image", "Button::image_and_text", )); - if ui - .add(egui::Button::image_and_text(egui_icon, "Click me!")) - .clicked() - { + if ui.button((egui_icon, "Click me!")).clicked() { *boolean = !*boolean; } ui.end_row(); diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png index 47ad5bc7a..f987b948d 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dfccdafb7e96db488bb5bb8c0a7d25f70e63d900d6b1c2280d218aac0e70e4c4 -size 26977 +oid sha256:24f4a9745c60c0353ece5f8fc48200671dcb185f4f0b964bbe66bf4a2fe71d7a +size 27067 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/SVG Test.png b/crates/egui_demo_lib/tests/snapshots/demos/SVG Test.png index 8aa13dbfa..4b560e20a 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/SVG Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/SVG Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:888ed4281c2c779b08bc1719302b9923f542026811cff8ae91e44ea1faa25783 -size 25804 +oid sha256:fadea24444c402695db6cbc9e03aef8a0ed3c5db487a324fb255d38c14f73dce +size 19804 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scene.png b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png index 277f7ab2c..2d57b2074 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Scene.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4d10b78f4d80d61a3352d7f2b0ed9b2d93af5f184f2487f6f2afff02a38f4608 -size 33475 +oid sha256:f6105c95470d1342f9003ab03e71243b5e18a6f225261aee94b15f8f0501572c +size 33542 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png index e4d385fce..4495bf173 100644 --- a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c5a45307147f19f2d69a3de1f53e0a73ba4c3368eb25a66b4098fb54cb83822f -size 64203 +oid sha256:ef245aae271ccae628bb4171f7e601194c77fd18888ef2ea829bea75bd38b0e5 +size 64965 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png index 102cb3650..7c47f522d 100644 --- a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0102aa84db99a6da1db1de3abf67f13c3b571de00e79e7c55805dc0504658d50 -size 150111 +oid sha256:e621561567539ff24b4d22b53b65fac6cddae71d92fccd7800a90972a6de3e0e +size 151100 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png index 091948af6..520895ff5 100644 --- a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3991cb1f922e0c6712d045b3cd8a1d98165c0fbef7e31b15d587f244e53ec04a -size 59343 +oid sha256:e6c2d538be7971169bbc4473945e6815eac8c5dd6372bc1f1897a032b6bca12b +size 59962 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png index 881f1b0d5..90311fddc 100644 --- a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:355d8f08d08011635bf812aea1edeabd69e1ac3c724b521ed243f2b52e9b444b -size 145257 +oid sha256:d705af99624cd2824cd1f520fa05481ac67b8913feebae836db7b99ac60cb466 +size 145841 diff --git a/crates/egui_kittest/tests/snapshots/menu/closed_hovered.png b/crates/egui_kittest/tests/snapshots/menu/closed_hovered.png index a82442e1e..2a4621b0e 100644 --- a/crates/egui_kittest/tests/snapshots/menu/closed_hovered.png +++ b/crates/egui_kittest/tests/snapshots/menu/closed_hovered.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f4a038f9acbb12880ba6b681ef7d3ae566045c4474aa31e7c6d746c39a649fc -size 11108 +oid sha256:38ee4acc23d9c66f127d377ac8a0dd3b683a1465ca319fba092f6d3cdff8c266 +size 11166 diff --git a/crates/egui_kittest/tests/snapshots/menu/opened.png b/crates/egui_kittest/tests/snapshots/menu/opened.png index eb55bd894..c698cdb4b 100644 --- a/crates/egui_kittest/tests/snapshots/menu/opened.png +++ b/crates/egui_kittest/tests/snapshots/menu/opened.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2965482e0161b4ea99aa5b4ece32261dbe246f86fe43054a754fbd556c7a5896 -size 21666 +oid sha256:ac1941f5eab71bfad020132eae47e1995efa17410b7861aa9f260032e5b0472c +size 21785 diff --git a/crates/egui_kittest/tests/snapshots/menu/submenu.png b/crates/egui_kittest/tests/snapshots/menu/submenu.png index 0a78e4e6c..f277511c1 100644 --- a/crates/egui_kittest/tests/snapshots/menu/submenu.png +++ b/crates/egui_kittest/tests/snapshots/menu/submenu.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7592ca6213497f686d105a2e686d0c5de364388ddd174cbe8abb425d27ddcab0 -size 28505 +oid sha256:b1f1a4dd9de1d8405c527c7f8f04b42ed9d403d0ec507bb3ff650a6896f28df0 +size 28628 diff --git a/crates/egui_kittest/tests/snapshots/menu/subsubmenu.png b/crates/egui_kittest/tests/snapshots/menu/subsubmenu.png index 84e6ba152..dfc2b707c 100644 --- a/crates/egui_kittest/tests/snapshots/menu/subsubmenu.png +++ b/crates/egui_kittest/tests/snapshots/menu/subsubmenu.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a1adf0903f0fc50323c2d77bbc491c950ab0dae6593c004770ea7961c2c6273 -size 33270 +oid sha256:af05a9b66340e0c128d823d3935a23bcf17cfeac02a822e7277234a9c8eb26e0 +size 33393 diff --git a/crates/egui_kittest/tests/snapshots/should_wait_for_images.png b/crates/egui_kittest/tests/snapshots/should_wait_for_images.png index 9709e159e..6ceffde99 100644 --- a/crates/egui_kittest/tests/snapshots/should_wait_for_images.png +++ b/crates/egui_kittest/tests/snapshots/should_wait_for_images.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ad75a0e568e04c20d0e3b823c7e4906c39dcd0a69a086d8e30714a9e4530d031 -size 2128 +oid sha256:cfc03625c268f0ae067d2f4521a8668b47e4bc8525350d77a480840a09cd5083 +size 2046 diff --git a/tests/egui_tests/tests/snapshots/layout/atoms_image.png b/tests/egui_tests/tests/snapshots/layout/atoms_image.png index acfdb810c..765e63f05 100644 --- a/tests/egui_tests/tests/snapshots/layout/atoms_image.png +++ b/tests/egui_tests/tests/snapshots/layout/atoms_image.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0f65b7221ac74991c526b68ad2469f42801f6083c9acead5bc923fd856a6311d -size 368614 +oid sha256:24c85a987b0b80961b656f386f529b7538ddee59a030d02a0946d0f714ce7004 +size 368329 diff --git a/tests/egui_tests/tests/snapshots/layout/button_image.png b/tests/egui_tests/tests/snapshots/layout/button_image.png index 79cda64a2..6c63fb759 100644 --- a/tests/egui_tests/tests/snapshots/layout/button_image.png +++ b/tests/egui_tests/tests/snapshots/layout/button_image.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f89cc5b17821c9f30f7a086bb37668e4e7913705d42c0678fb0f42c527abb868 -size 334498 +oid sha256:8f14f770785d01b1673d1c8ca780bfff72e51992794dc7233cf5ec4ea99cb3e9 +size 350648 diff --git a/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png b/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png index b244a86dc..9c74cd8be 100644 --- a/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png +++ b/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b7f87fb417453a98e7059535cb68b12549d65f8da7cedf7a48e7154686931e16 -size 419858 +oid sha256:231ceab75a602eedcd11f4f4ed34f38fb9d072f5cb54e135a7e02d33d257f86b +size 433973 diff --git a/tests/egui_tests/tests/snapshots/visuals/button_image.png b/tests/egui_tests/tests/snapshots/visuals/button_image.png index eca582ec0..6cb7241bf 100644 --- a/tests/egui_tests/tests/snapshots/visuals/button_image.png +++ b/tests/egui_tests/tests/snapshots/visuals/button_image.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a2a017c2b93d1920ae85792c13eafa2fd43f93b2e3bbaa5981ed3a43050c0995 -size 11808 +oid sha256:d53f67fb3a3717f7bc5ce99b93bc21d1d6580899dfe8e1371ff22bb416af0786 +size 12114 diff --git a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png index 4848b0781..b278f6c25 100644 --- a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png +++ b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:42cbc8f8740f56ce45c356262d9b872e3973844ce552c6c09e3c07425c3f86b6 -size 14835 +oid sha256:e298d89e6fb434e5010d96661fca40bf119118b6b31fdd9fc13201bcd74c8ffd +size 15149 diff --git a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png index 8c30d3145..9a1e15c20 100644 --- a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png +++ b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:11fdd4bde01102e7998defcaa80c1105ec9418152314c74ee028b692b26c6be8 -size 14407 +oid sha256:a0581d601f1e536298cb52bfc8a167aa37aebdf065fc910973a752c9c159223d +size 14733 diff --git a/web_demo/favicon.ico b/web_demo/favicon.ico old mode 100755 new mode 100644 index 61ad031aa5b8aadf968a2b864649c3bb84b91735..59a822c613e6befe20332395bd91fd4eed421d0f GIT binary patch literal 2932 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 literal 15406 zcmeHOTS%2z6#ku=ri~V2rUhCUy zul?_}*Iq}Ge57xsMT;aZO;YxElJv7ANhZ^$``{lX=}&$Z8anqrNRqmKlB8g+!%euw zr{`RJ@nCr{}7`SYslo9iGvJe)p!_&^sfT%aRIj?AMq zYu4E185R~M+PAj0((T)~1wjYkxnaWwyX}ExaBz^$oH?V^J$(2uO-xKsU|^u~%p9}j zb#`_(O-@cyd3iYhqMwiQ$ zE&J%GjJY|up!xg7kAjAW_|BM^Vwily6<>V$_YLFyjr%*v^q-@BR@)9T=42Hz93RM5 z+g9&-jN@0PDZTls`}gms?(S}}?qTh-B&=tfHf=Hl!_lKh>D{|`ip*E9UQv5{JGHd52uzUu z{{4Fj4Gq;@er#+kO;1k?+2iBml#`S5*}9FjG$|=bciFJ9YuBy`+324ZpGyz>39vPGDmw)&*y-Oimm75SF) z&Dy=5b{H8MsmO0=Xb`p!`?sX4SFbAdfLTi)?bUt${JG$L@7_JDe9xXeE9zj5z+bn= zQ_K6x%1Xg|c6OGwZQJ%4?-eUnP)|>fr~@0(!dp#UjKS;IuLXbXBerbWqR7Dhp{lA% zyn{WImX<2-)bLhY=g^@;V*dsi(8K-v_o=?VUhG3;JH*InNhwf%9l9=?Zr_Uxh4r%zK^Ss4`+6i`e|jNx(}#p`Y789tq}#|irn-v7-w zTdpjRnnS5-=NhE)N z|3yoeE=_0Mb+HcrVKRKYwAuqOF}}aK>}I?NeSLiqZkGJ~{C?o)mB6i)r>p8S){l(& zcl2S&k|hUl?g$^~YO1NWoTQ68>t$9?E#@)*0Z)EBTbx!qi~#A)r`ld)UtllQsT zC-mCD*KGqh_r%#HVw;F3AZCR!E-sFuqoc)M+8xga*RNkM_T1O6U#G6FE*c#jrJ0!- z5tG3A^2?VmskgV6nwpvj@xs*9R9d-mrCaRJ_wLeVj84t*eIhj|I?u!n(x0ouHIvr92)8K)aJZYXwx^QVf63b)Ohu_R*Hy-aQe6* z=g1sC>>p==ju5`Hdg@jUFRWSS_!n|rTwLsQ{P11OG2=Vh-`}r_%;!CLAV)xr&h)Ia zapT5$_(w)Y2+!87TW6Q<+_^Kat(IItz50MP)ZA}?&#+_14!e59cekyrO~H?KPtP-9 zE&W5jrEEj+lW}fk&!5286Mq;$bJ3c;M1RlnU8z={$+8unb4^I9u!Z~d=3Hs>9G@A!LdKpz{x`5mACYW^>myXc>>|HXaq z*z;?~-@*73nLO?c8<;uge%6kDFuC2CJTL(pW9&rE T$>PO}zeoOz_vh8XLNxF{TPS{( From bccd5f87bdabfc6c5061777e83c997c7e939342c Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 23 Mar 2026 12:33:41 +0100 Subject: [PATCH 32/58] Update wasm-bindgen to 0.2.108, and ehttp to 0.7.1 (#7996) --- .github/workflows/rust.yml | 2 +- Cargo.lock | 224 +++++++++++++++++++++---------------- Cargo.toml | 4 +- deny.toml | 1 + scripts/setup_web.sh | 4 +- 5 files changed, 133 insertions(+), 102 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 9018d251b..2122e5b99 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -94,7 +94,7 @@ jobs: - name: wasm-bindgen uses: jetli/wasm-bindgen-action@v0.1.0 with: - version: "0.2.100" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml + version: "0.2.108" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml - run: ./scripts/wasm_bindgen_check.sh --skip-setup diff --git a/Cargo.lock b/Cargo.lock index 896a36665..ec1abe31c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -124,9 +124,9 @@ dependencies = [ [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "ahash" @@ -496,9 +496,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "backtrace" @@ -610,9 +610,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytemuck" @@ -686,10 +686,11 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.16" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -703,9 +704,9 @@ checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -961,9 +962,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -1186,9 +1187,9 @@ dependencies = [ [[package]] name = "document-features" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" dependencies = [ "litrs", ] @@ -1434,9 +1435,9 @@ dependencies = [ [[package]] name = "ehttp" -version = "0.6.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04499d3c719edecfad5c9b46031726c8540905d73be6d7e4f9788c4a298da908" +checksum = "b2f1b93eb2e039aaff63ce07cca59bd1dca02f2ce30075a17b619d2c42f56efc" dependencies = [ "document-features", "js-sys", @@ -1697,10 +1698,16 @@ dependencies = [ ] [[package]] -name = "flate2" -version = "1.1.4" +name = "find-msvc-tools" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -1874,13 +1881,13 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", ] [[package]] @@ -2130,6 +2137,22 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + [[package]] name = "iana-time-zone" version = "0.1.63" @@ -2360,9 +2383,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" @@ -2427,9 +2450,9 @@ checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07" [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", @@ -2498,9 +2521,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.176" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libloading" @@ -2571,9 +2594,9 @@ checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "litrs" -version = "0.4.1" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "lock_api" @@ -2694,7 +2717,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -3110,9 +3133,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -3365,9 +3388,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "piper" @@ -3507,9 +3530,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -3594,9 +3617,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.41" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -3648,7 +3671,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] @@ -3726,7 +3749,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "libredox", "thiserror 2.0.17", ] @@ -3824,7 +3847,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -3895,9 +3918,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.18" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9cc1d47e243d655ace55ed38201c19ae02c148ae56412ab8750e8f0166ab7f" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "log", "once_cell", @@ -3910,15 +3933,18 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.10.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] [[package]] name = "rustls-webpki" -version = "0.102.8" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "ring", "rustls-pki-types", @@ -3927,9 +3953,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.19" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rustybuzz" @@ -4087,9 +4113,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" [[package]] name = "similar" @@ -4138,12 +4164,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.9" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "slotmap" @@ -4266,9 +4289,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -4672,9 +4695,9 @@ checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" [[package]] name = "unicode-ident" -version = "1.0.19" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-properties" @@ -4734,20 +4757,33 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "2.10.1" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b74fc6b57825be3373f7054754755f03ac3a8f5d70015ccad699ba2029956f4a" +checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" dependencies = [ "base64", "flate2", "log", - "once_cell", + "percent-encoding", "rustls", "rustls-pki-types", - "url", + "ureq-proto", + "utf8-zero", "webpki-roots", ] +[[package]] +name = "ureq-proto" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" +dependencies = [ + "base64", + "http", + "httparse", + "log", +] + [[package]] name = "url" version = "2.5.4" @@ -4794,6 +4830,12 @@ dependencies = [ "xmlwriter", ] +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -4861,9 +4903,9 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" @@ -4876,37 +4918,25 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -4915,9 +4945,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4925,22 +4955,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" dependencies = [ "unicode-ident", ] @@ -5056,9 +5086,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" dependencies = [ "js-sys", "wasm-bindgen", @@ -5092,9 +5122,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.26.6" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] @@ -5995,9 +6025,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" diff --git a/Cargo.toml b/Cargo.toml index d12b040fe..a617e84a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,7 +87,7 @@ criterion = { version = "0.7.0", default-features = false } dify = { version = "0.8", default-features = false } directories = "6.0.0" document-features = "0.2.11" -ehttp = { version = "0.6.0", default-features = false } +ehttp = { version = "0.7.1", default-features = false } enum-map = "2.7.3" env_logger = { version = "0.11.8", default-features = false } font-types = { version = "0.11.0", default-features = false, features = ["std"] } @@ -138,7 +138,7 @@ type-map = "0.5.1" unicode_names2 = { version = "2.0.0", default-features = false } unicode-segmentation = "1.12.0" vello_cpu = { version = "0.0.6", default-features = false, features = ["std", "u8_pipeline", "f32_pipeline"] } -wasm-bindgen = "0.2.100" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml +wasm-bindgen = "0.2.108" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml wasm-bindgen-futures = "0.4.0" wayland-cursor = { version = "0.31.11", default-features = false } web-sys = "0.3.77" diff --git a/deny.toml b/deny.toml index 486af7745..595641501 100644 --- a/deny.toml +++ b/deny.toml @@ -80,6 +80,7 @@ allow = [ "BSD-3-Clause", # https://tldrlegal.com/license/bsd-3-clause-license-(revised) "BSL-1.0", # https://tldrlegal.com/license/boost-software-license-1.0-explained "CC0-1.0", # https://creativecommons.org/publicdomain/zero/1.0/ + "CDLA-Permissive-2.0", # https://spdx.org/licenses/CDLA-Permissive-2.0.html "ISC", # https://www.tldrlegal.com/license/isc-license "MIT-0", # https://choosealicense.com/licenses/mit-0/ "MIT", # https://tldrlegal.com/license/mit-license diff --git a/scripts/setup_web.sh b/scripts/setup_web.sh index f51258855..5ba11b665 100755 --- a/scripts/setup_web.sh +++ b/scripts/setup_web.sh @@ -10,6 +10,6 @@ rustup target add wasm32-unknown-unknown # For generating JS bindings: # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml -if ! cargo install --list | grep -q 'wasm-bindgen-cli v0.2.100'; then - cargo install --force --quiet wasm-bindgen-cli --version 0.2.100 +if ! cargo install --list | grep -q 'wasm-bindgen-cli v0.2.108'; then + cargo install --force --quiet wasm-bindgen-cli --version 0.2.108 --locked fi From f2a47411553c53e8769c8bb3b71c172f70f23b3e Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 23 Mar 2026 15:37:45 +0100 Subject: [PATCH 33/58] Remove easymark from default demo app (#7998) I don't want to give the impression `easymark` is either official, useful, or indeed "good" --- crates/egui_demo_app/Cargo.toml | 2 +- crates/egui_demo_app/src/wrap_app.rs | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/crates/egui_demo_app/Cargo.toml b/crates/egui_demo_app/Cargo.toml index 7cde46383..b23ea9cbb 100644 --- a/crates/egui_demo_app/Cargo.toml +++ b/crates/egui_demo_app/Cargo.toml @@ -25,10 +25,10 @@ crate-type = ["cdylib", "rlib"] [features] default = ["wgpu", "persistence"] -# image_viewer adds about 0.9 MB of WASM web_app = ["http", "persistence"] accessibility_inspector = ["dep:accesskit", "dep:accesskit_consumer", "eframe/accesskit"] +easymark = [] # easymark is off by default, because it a pretty shitty markup language http = ["ehttp", "image/jpeg", "poll-promise", "egui_extras/image"] image_viewer = ["image/jpeg", "egui_extras/all_loaders", "rfd"] persistence = ["eframe/persistence", "egui_extras/serde", "egui/persistence", "serde"] diff --git a/crates/egui_demo_app/src/wrap_app.rs b/crates/egui_demo_app/src/wrap_app.rs index c486783cf..313a1a685 100644 --- a/crates/egui_demo_app/src/wrap_app.rs +++ b/crates/egui_demo_app/src/wrap_app.rs @@ -8,12 +8,14 @@ use core::any::Any; use crate::DemoApp; +#[cfg(feature = "easymark")] #[derive(Default)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] struct EasyMarkApp { editor: egui_demo_lib::easy_mark::EasyMarkEditor, } +#[cfg(feature = "easymark")] impl DemoApp for EasyMarkApp { fn demo_ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) { self.editor.panels(ui); @@ -152,12 +154,18 @@ enum Command { #[cfg_attr(feature = "serde", serde(default))] pub struct State { demo: DemoWindows, + + #[cfg(feature = "easymark")] easy_mark_editor: EasyMarkApp, + #[cfg(feature = "http")] http: crate::apps::HttpApp, + #[cfg(feature = "image_viewer")] image_viewer: crate::apps::ImageViewer, + pub clock: FractalClockApp, + rendering_test: ColorTestApp, selected_anchor: Anchor, @@ -212,6 +220,7 @@ impl WrapApp { Anchor::Demo, &mut self.state.demo as &mut dyn DemoApp, ), + #[cfg(feature = "easymark")] ( "πŸ–Ή EasyMark editor", Anchor::EasyMarkEditor, From b077cf910297884a4f1b431e8da99806ae925168 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 23 Mar 2026 15:50:57 +0100 Subject: [PATCH 34/58] Add some example docs to atoms (#7997) --- crates/egui/src/atomics/atom.rs | 16 +++++++++++++++- crates/egui/src/atomics/atoms.rs | 19 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/atomics/atom.rs b/crates/egui/src/atomics/atom.rs index 4db7f12a9..6f289fcfb 100644 --- a/crates/egui/src/atomics/atom.rs +++ b/crates/egui/src/atomics/atom.rs @@ -4,7 +4,21 @@ use epaint::text::TextWrapMode; /// A low-level ui building block. /// -/// Implements [`From`] for [`String`], [`str`], [`crate::Image`] and much more for convenience. +/// This can be a piece of text, an image, or even a custom widget. +/// It can be decorated with various layout hints, such as `grow`, `shrink`, `align`, and more. +/// +/// `Atom` implements [`From`] for [`String`], [`str`], [`crate::Image`] and much more for convenience. +/// +/// Many widgets take an `impl` [`crate::IntoAtoms`] parameter, +/// which allows you to easily create atoms from tuples of text, images, and other atoms: +/// ``` +/// # use egui::{Vec2, AtomExt, AtomKind, Atom, Image, Id}; +/// # egui::__run_test_ui(|ui| { +/// let image = egui::include_image!("../../../eframe/data/icon.png"); +/// ui.button((image, "Click me!")); +/// # }); +/// ``` +/// /// You can directly call the `atom_*` methods on anything that implements `Into`. /// ``` /// # use egui::{Image, emath::Vec2}; diff --git a/crates/egui/src/atomics/atoms.rs b/crates/egui/src/atomics/atoms.rs index 5051a7676..761db8eb6 100644 --- a/crates/egui/src/atomics/atoms.rs +++ b/crates/egui/src/atomics/atoms.rs @@ -8,6 +8,15 @@ use std::ops::{Deref, DerefMut}; pub(crate) const ATOMS_SMALL_VEC_SIZE: usize = 2; /// A list of [`Atom`]s. +/// +/// Many widgets take an `impl` [`IntoAtoms`] parameter, +/// which allows you to easily create atoms from tuples of text, images, and other atoms: +/// ``` +/// # use egui::{AtomExt, AtomKind, Atom, Image, Id, Vec2}; +/// # egui::__run_test_ui(|ui| { +/// let image = egui::include_image!("../../../eframe/data/icon.png"); +/// ui.button((image, "Click me!")); +/// # }); #[derive(Clone, Debug, Default)] pub struct Atoms<'a>(SmallVec<[Atom<'a>; ATOMS_SMALL_VEC_SIZE]>); @@ -192,6 +201,16 @@ where } /// Trait for turning a tuple of [`Atom`]s into [`Atoms`]. +/// +/// Many widgets take an `impl` [`IntoAtoms`] parameter, +/// which allows you to easily create atoms from tuples of text, images, and other atoms: +/// ``` +/// # use egui::{AtomExt, AtomKind, Atom, Image, Id, Vec2}; +/// # egui::__run_test_ui(|ui| { +/// let image = egui::include_image!("../../../eframe/data/icon.png"); +/// ui.button((image, "Click me!")); +/// # }); +/// ``` pub trait IntoAtoms<'a> { fn collect(self, atoms: &mut Atoms<'a>); From a59e803f2567ad12940280e3c50ad22187c10ae6 Mon Sep 17 00:00:00 2001 From: Connor Fitzgerald Date: Mon, 23 Mar 2026 13:21:25 -0400 Subject: [PATCH 35/58] Update to wgpu 29 (#7990) * [x] I have followed the instructions in the PR template This updates wgpu to v29 across the egui crate stack. There a a few API changes due to the requirement to provide a display handle up front to properly support GLES on linux. I have done my best to make the api changes as reasonable as possible, but I don't have all the greater project context, so lmk if things should be done a bit differently. I've also updated glow to 0.17 to make cargo deny happy, there are no source changes. I'm not sure how you want to land these. --------- Co-authored-by: lucasmerlin --- Cargo.lock | 205 ++++++++++-------- Cargo.toml | 4 +- crates/eframe/src/native/wgpu_integration.rs | 10 +- crates/eframe/src/web/mod.rs | 3 +- crates/eframe/src/web/web_painter_wgpu.rs | 36 ++- crates/egui-wgpu/src/lib.rs | 31 ++- crates/egui-wgpu/src/renderer.rs | 15 +- crates/egui-wgpu/src/setup.rs | 197 ++++++++++++++--- crates/egui-wgpu/src/winit.rs | 33 ++- .../egui_demo_app/src/apps/custom3d_wgpu.rs | 2 +- crates/egui_kittest/src/wgpu.rs | 4 +- 11 files changed, 374 insertions(+), 166 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ec1abe31c..e9694b07f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -536,7 +536,16 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ - "bit-vec", + "bit-vec 0.8.0", +] + +[[package]] +name = "bit-set" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ddef2995421ab6a5c779542c81ee77c115206f4ad9d5a8e05f4ff49716a3dd" +dependencies = [ + "bit-vec 0.9.1", ] [[package]] @@ -545,6 +554,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" + [[package]] name = "bitflags" version = "1.3.2" @@ -560,12 +575,6 @@ dependencies = [ "serde", ] -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - [[package]] name = "block2" version = "0.5.1" @@ -805,9 +814,9 @@ dependencies = [ [[package]] name = "codespan-reporting" -version = "0.12.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" dependencies = [ "serde", "termcolor", @@ -924,7 +933,7 @@ checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.4", - "core-graphics-types 0.1.3", + "core-graphics-types", "foreign-types", "libc", ] @@ -940,17 +949,6 @@ dependencies = [ "libc", ] -[[package]] -name = "core-graphics-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" -dependencies = [ - "bitflags 2.9.4", - "core-foundation 0.10.1", - "libc", -] - [[package]] name = "core_maths" version = "0.1.1" @@ -1659,7 +1657,7 @@ version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f" dependencies = [ - "bit-set", + "bit-set 0.8.0", "regex-automata", "regex-syntax", ] @@ -1931,9 +1929,9 @@ dependencies = [ [[package]] name = "glow" -version = "0.16.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" +checksum = "29038e1c483364cc6bb3cf78feee1816002e127c331a1eec55a4d202b9e1adb5" dependencies = [ "js-sys", "slotmap", @@ -2619,15 +2617,6 @@ version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "373f5eceeeab7925e0c1098212f2fbc4d416adec9d35051a6ab251e824c1854a" -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - [[package]] name = "memchr" version = "2.7.4" @@ -2652,21 +2641,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "metal" -version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7047791b5bc903b8cd963014b355f71dc9864a9a0b727057676c1dcae5cbc15" -dependencies = [ - "bitflags 2.9.4", - "block", - "core-graphics-types 0.2.0", - "foreign-types", - "log", - "objc", - "paste", -] - [[package]] name = "mimalloc" version = "0.1.48" @@ -2731,12 +2705,12 @@ dependencies = [ [[package]] name = "naga" -version = "28.0.0" +version = "29.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "618f667225063219ddfc61251087db8a9aec3c3f0950c916b614e403486f1135" +checksum = "85b4372fed0bd362d646d01b6926df0e837859ccc522fed720c395e0460f29c8" dependencies = [ "arrayvec", - "bit-set", + "bit-set 0.9.1", "bitflags 2.9.4", "cfg-if", "cfg_aliases", @@ -2841,15 +2815,6 @@ dependencies = [ "syn", ] -[[package]] -name = "objc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", -] - [[package]] name = "objc-sys" version = "0.3.5" @@ -2888,7 +2853,7 @@ dependencies = [ "objc2-core-data", "objc2-core-image", "objc2-foundation 0.2.2", - "objc2-quartz-core", + "objc2-quartz-core 0.2.2", ] [[package]] @@ -2974,7 +2939,7 @@ dependencies = [ "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", - "objc2-metal", + "objc2-metal 0.2.2", ] [[package]] @@ -3054,6 +3019,18 @@ dependencies = [ "objc2-foundation 0.2.2", ] +[[package]] +name = "objc2-metal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" +dependencies = [ + "bitflags 2.9.4", + "block2 0.6.2", + "objc2 0.6.3", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-quartz-core" version = "0.2.2" @@ -3064,7 +3041,20 @@ dependencies = [ "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", - "objc2-metal", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.9.4", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", ] [[package]] @@ -3092,7 +3082,7 @@ dependencies = [ "objc2-core-location", "objc2-foundation 0.2.2", "objc2-link-presentation", - "objc2-quartz-core", + "objc2-quartz-core 0.2.2", "objc2-symbols", "objc2-uniform-type-identifiers", "objc2-user-notifications", @@ -3232,12 +3222,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "pathdiff" version = "0.2.3" @@ -3695,6 +3679,18 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "raw-window-metal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135" +dependencies = [ + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + [[package]] name = "rayon" version = "1.11.0" @@ -4243,9 +4239,9 @@ dependencies = [ [[package]] name = "spirv" -version = "0.3.0+sdk-1.3.268.0" +version = "0.4.0+sdk-1.4.341.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" +checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f" dependencies = [ "bitflags 2.9.4", ] @@ -5137,9 +5133,9 @@ checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" [[package]] name = "wgpu" -version = "28.0.0" +version = "29.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9cb534d5ffd109c7d1135f34cdae29e60eab94855a625dcfe1705f8bc7ad79f" +checksum = "78f9f386699b1fb8b8a05bfe82169b24d151f05702d2905a0bf93bc454fcc825" dependencies = [ "arrayvec", "bitflags 2.9.4", @@ -5167,13 +5163,13 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "28.0.0" +version = "29.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bb4c8b5db5f00e56f1f08869d870a0dff7c8bc7ebc01091fec140b0cf0211a9" +checksum = "c7c34181b0acb8f98168f78f8e57ec66f57df5522b39143dbe5f2f45d7ca927c" dependencies = [ "arrayvec", - "bit-set", - "bit-vec", + "bit-set 0.9.1", + "bit-vec 0.9.1", "bitflags 2.9.4", "bytemuck", "cfg_aliases", @@ -5195,61 +5191,61 @@ dependencies = [ "wgpu-core-deps-wasm", "wgpu-core-deps-windows-linux-android", "wgpu-hal", + "wgpu-naga-bridge", "wgpu-types", ] [[package]] name = "wgpu-core-deps-apple" -version = "28.0.0" +version = "29.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87b7b696b918f337c486bf93142454080a32a37832ba8a31e4f48221890047da" +checksum = "43acd053312501689cd92a01a9638d37f3e41a5fd9534875efa8917ee2d11ac0" dependencies = [ "wgpu-hal", ] [[package]] name = "wgpu-core-deps-emscripten" -version = "28.0.0" +version = "29.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34b251c331f84feac147de3c4aa3aa45112622a95dd7ee1b74384fa0458dbd79" +checksum = "ef043bf135cc68b6f667c55ff4e345ce2b5924d75bad36a47921b0287ca4b24a" dependencies = [ "wgpu-hal", ] [[package]] name = "wgpu-core-deps-wasm" -version = "28.0.0" +version = "29.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12a2cf578ce8d7d50d0e63ddc2345c7dcb599f6eb90b888813406ea78b9b7010" +checksum = "2f7b75e72f49035f000dd5262e4126242e92a090a4fd75931ecfe7e60784e6fa" dependencies = [ "wgpu-hal", ] [[package]] name = "wgpu-core-deps-windows-linux-android" -version = "28.0.0" +version = "29.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ca976e72b2c9964eb243e281f6ce7f14a514e409920920dcda12ae40febaae" +checksum = "725d5c006a8c02967b6d93ef04f6537ec4593313e330cfe86d9d3f946eb90f28" dependencies = [ "wgpu-hal", ] [[package]] name = "wgpu-hal" -version = "28.0.0" +version = "29.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "293080d77fdd14d6b08a67c5487dfddbf874534bb7921526db56a7b75d7e3bef" +checksum = "058b6047337cf323a4f092486443a9337f3d81325347e5d77deed7e563aeaedc" dependencies = [ "android_system_properties", "arrayvec", "ash", - "bit-set", + "bit-set 0.9.1", "bitflags 2.9.4", - "block", + "block2 0.6.2", "bytemuck", "cfg-if", "cfg_aliases", - "core-graphics-types 0.2.0", "glow", "glutin_wgl_sys", "gpu-allocator", @@ -5260,10 +5256,13 @@ dependencies = [ "libc", "libloading", "log", - "metal", "naga", "ndk-sys", - "objc", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", + "objc2-quartz-core 0.3.2", "once_cell", "ordered-float", "parking_lot", @@ -5272,26 +5271,40 @@ dependencies = [ "profiling", "range-alloc", "raw-window-handle", + "raw-window-metal", "renderdoc-sys", "smallvec", "thiserror 2.0.17", "wasm-bindgen", + "wayland-sys", "web-sys", + "wgpu-naga-bridge", "wgpu-types", "windows", "windows-core 0.62.2", ] [[package]] -name = "wgpu-types" -version = "28.0.0" +name = "wgpu-naga-bridge" +version = "29.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e18308757e594ed2cd27dddbb16a139c42a683819d32a2e0b1b0167552f5840c" +checksum = "d0b8e1e505095f24cb4a578f04b1421d456257dca7fac114d9d9dd3d978c34b8" +dependencies = [ + "naga", + "wgpu-types", +] + +[[package]] +name = "wgpu-types" +version = "29.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d15ece45db77dd5451f11c0ce898334317ce8502d304a20454b531fdc0652fae" dependencies = [ "bitflags 2.9.4", "bytemuck", "js-sys", "log", + "raw-window-handle", "web-sys", ] diff --git a/Cargo.toml b/Cargo.toml index a617e84a5..b084399a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,7 +91,7 @@ ehttp = { version = "0.7.1", default-features = false } enum-map = "2.7.3" env_logger = { version = "0.11.8", default-features = false } font-types = { version = "0.11.0", default-features = false, features = ["std"] } -glow = "0.16.0" +glow = "0.17.0" glutin = { version = "0.32.3", default-features = false } glutin-winit = { version = "0.5.0", default-features = false } home = "0.5.9" @@ -144,7 +144,7 @@ wayland-cursor = { version = "0.31.11", default-features = false } web-sys = "0.3.77" web-time = "1.1.0" # Timekeeping for native and web webbrowser = "1.0.5" -wgpu = { version = "28.0.0", default-features = false, features = ["std"] } +wgpu = { version = "29.0.0", default-features = false, features = ["std"] } windows-sys = "0.61.2" winit = { version = "0.30.12", default-features = false } diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index 9d6283808..ea96a1845 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -184,9 +184,17 @@ impl<'app> WgpuWinitApp<'app> { builder: ViewportBuilder, ) -> crate::Result<&mut WgpuWinitRunning<'app>> { profiling::function_scope!(); + // Inject the display handle into the wgpu setup so that wgpu can create + // surfaces on platforms that require it (e.g. GLES on Wayland). + let mut wgpu_options = self.native_options.wgpu_options.clone(); + if let egui_wgpu::WgpuSetup::CreateNew(ref mut create_new) = wgpu_options.wgpu_setup + && create_new.display_handle.is_none() + { + create_new.display_handle = Some(Box::new(event_loop.owned_display_handle())); + } let mut painter = pollster::block_on(egui_wgpu::winit::Painter::new( egui_ctx.clone(), - self.native_options.wgpu_options.clone(), + wgpu_options, self.native_options.viewport.transparent.unwrap_or(false), egui_wgpu::RendererOptions { msaa_samples: self.native_options.multisampling as _, diff --git a/crates/eframe/src/web/mod.rs b/crates/eframe/src/web/mod.rs index 1e54d7a84..87771f722 100644 --- a/crates/eframe/src/web/mod.rs +++ b/crates/eframe/src/web/mod.rs @@ -38,6 +38,7 @@ mod web_painter_wgpu; pub use backend::*; use egui::Theme; +use js_sys::Object; use wasm_bindgen::prelude::*; use web_sys::{Document, MediaQueryList, Node}; @@ -370,5 +371,5 @@ pub fn percent_decode(s: &str) -> String { /// Are we running inside the Safari browser? pub fn is_safari_browser() -> bool { - web_sys::window().is_some_and(|window| window.has_own_property(&JsValue::from("safari"))) + web_sys::window().is_some_and(|window| Object::has_own(&window, &JsValue::from("safari"))) } diff --git a/crates/eframe/src/web/web_painter_wgpu.rs b/crates/eframe/src/web/web_painter_wgpu.rs index f7adb8fbb..ebce9d981 100644 --- a/crates/eframe/src/web/web_painter_wgpu.rs +++ b/crates/eframe/src/web/web_painter_wgpu.rs @@ -15,13 +15,14 @@ pub(crate) struct WebPainterWgpu { surface: wgpu::Surface<'static>, surface_configuration: wgpu::SurfaceConfiguration, render_state: Option, - on_surface_error: Arc SurfaceErrorAction>, + on_surface_status: Arc SurfaceErrorAction>, depth_stencil_format: Option, depth_texture_view: Option, screen_capture_state: Option, capture_tx: CaptureSender, capture_rx: CaptureReceiver, ctx: egui::Context, + needs_reconfigure: bool, } impl WebPainterWgpu { @@ -105,11 +106,12 @@ impl WebPainterWgpu { surface_configuration, depth_stencil_format, depth_texture_view: None, - on_surface_error: Arc::clone(&options.wgpu_options.on_surface_error) as _, + on_surface_status: Arc::clone(&options.wgpu_options.on_surface_status) as _, screen_capture_state: None, capture_tx, capture_rx, ctx, + needs_reconfigure: false, }) } } @@ -195,18 +197,28 @@ impl WebPainter for WebPainterWgpu { ); } + if self.needs_reconfigure { + self.surface + .configure(&render_state.device, &self.surface_configuration); + self.needs_reconfigure = false; + } + let output_frame = match self.surface.get_current_texture() { - Ok(frame) => frame, - Err(err) => match (*self.on_surface_error)(err) { - SurfaceErrorAction::RecreateSurface => { - self.surface - .configure(&render_state.device, &self.surface_configuration); - return Ok(()); + wgpu::CurrentSurfaceTexture::Success(frame) => frame, + wgpu::CurrentSurfaceTexture::Suboptimal(frame) => { + self.needs_reconfigure = true; + frame + } + other => { + match (*self.on_surface_status)(&other) { + SurfaceErrorAction::RecreateSurface => { + self.surface + .configure(&render_state.device, &self.surface_configuration); + } + SurfaceErrorAction::SkipFrame => {} } - SurfaceErrorAction::SkipFrame => { - return Ok(()); - } - }, + return Ok(()); + } }; { diff --git a/crates/egui-wgpu/src/lib.rs b/crates/egui-wgpu/src/lib.rs index 46becf8f7..05936d6cd 100644 --- a/crates/egui-wgpu/src/lib.rs +++ b/crates/egui-wgpu/src/lib.rs @@ -24,7 +24,10 @@ mod renderer; mod setup; pub use renderer::*; -pub use setup::{NativeAdapterSelectorMethod, WgpuSetup, WgpuSetupCreateNew, WgpuSetupExisting}; +pub use setup::{ + EguiDisplayHandle, NativeAdapterSelectorMethod, WgpuSetup, WgpuSetupCreateNew, + WgpuSetupExisting, +}; /// Helpers for capturing screenshots of the UI. #[cfg(feature = "capture")] @@ -191,6 +194,7 @@ impl RenderState { let (adapter, device, queue) = match config.wgpu_setup.clone() { WgpuSetup::CreateNew(WgpuSetupCreateNew { instance_descriptor: _, + display_handle: _, power_preference, native_adapter_selector: _native_adapter_selector, device_descriptor, @@ -272,7 +276,7 @@ fn describe_adapters(adapters: &[wgpu::Adapter]) -> String { } } -/// Specifies which action should be taken as consequence of a [`wgpu::SurfaceError`] +/// Specifies which action should be taken as consequence of a surface error. pub enum SurfaceErrorAction { /// Do nothing and skip the current frame. SkipFrame, @@ -299,8 +303,15 @@ pub struct WgpuConfiguration { /// How to create the wgpu adapter & device pub wgpu_setup: WgpuSetup, - /// Callback for surface errors. - pub on_surface_error: Arc SurfaceErrorAction + Send + Sync>, + /// Callback for surface status changes. + /// + /// Called with the [`wgpu::CurrentSurfaceTexture`] result whenever acquiring a frame + /// does not return [`wgpu::CurrentSurfaceTexture::Success`]. For + /// [`wgpu::CurrentSurfaceTexture::Suboptimal`], egui uses the frame as-is and + /// defers surface reconfiguration to the next frame β€” the callback is not invoked + /// in that case either. + pub on_surface_status: + Arc SurfaceErrorAction + Send + Sync>, } #[test] @@ -315,7 +326,7 @@ impl std::fmt::Debug for WgpuConfiguration { present_mode, desired_maximum_frame_latency, wgpu_setup, - on_surface_error: _, + on_surface_status: _, } = self; f.debug_struct("WgpuConfiguration") .field("present_mode", &present_mode) @@ -333,14 +344,16 @@ impl Default for WgpuConfiguration { Self { present_mode: wgpu::PresentMode::AutoVsync, desired_maximum_frame_latency: None, - wgpu_setup: Default::default(), - on_surface_error: Arc::new(|err| { - if err == wgpu::SurfaceError::Outdated { + // No display handle available at this point β€” callers should replace this with + // `WgpuSetup::from_display_handle(...)` before creating the instance if one is available. + wgpu_setup: WgpuSetup::without_display_handle(), + on_surface_status: Arc::new(|status| { + if matches!(status, wgpu::CurrentSurfaceTexture::Outdated) { // This error occurs when the app is minimized on Windows. // Silently return here to prevent spamming the console with: // "The underlying surface has changed, and therefore the swap chain must be updated" } else { - log::warn!("Dropped frame with error: {err}"); + log::warn!("Dropped frame with error: {status:?}"); } SurfaceErrorAction::SkipFrame }), diff --git a/crates/egui-wgpu/src/renderer.rs b/crates/egui-wgpu/src/renderer.rs index c37802448..3222a5521 100644 --- a/crates/egui-wgpu/src/renderer.rs +++ b/crates/egui-wgpu/src/renderer.rs @@ -352,7 +352,10 @@ impl Renderer { let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("egui_pipeline_layout"), - bind_group_layouts: &[&uniform_bind_group_layout, &texture_bind_group_layout], + bind_group_layouts: &[ + Some(&uniform_bind_group_layout), + Some(&texture_bind_group_layout), + ], immediate_size: 0, }); @@ -360,8 +363,8 @@ impl Renderer { .depth_stencil_format .map(|format| wgpu::DepthStencilState { format, - depth_write_enabled: false, - depth_compare: wgpu::CompareFunction::Always, + depth_write_enabled: Some(false), + depth_compare: Some(wgpu::CompareFunction::Always), stencil: wgpu::StencilState::default(), bias: wgpu::DepthBiasState::default(), }); @@ -968,7 +971,8 @@ impl Renderer { Primitive::Mesh(mesh) => { let size = mesh.indices.len() * std::mem::size_of::(); let slice = index_offset..(size + index_offset); - index_buffer_staging[slice.clone()] + index_buffer_staging + .slice(slice.clone()) .copy_from_slice(bytemuck::cast_slice(&mesh.indices)); self.index_buffer.slices.push(slice); index_offset += size; @@ -1011,7 +1015,8 @@ impl Renderer { Primitive::Mesh(mesh) => { let size = mesh.vertices.len() * std::mem::size_of::(); let slice = vertex_offset..(size + vertex_offset); - vertex_buffer_staging[slice.clone()] + vertex_buffer_staging + .slice(slice.clone()) .copy_from_slice(bytemuck::cast_slice(&mesh.vertices)); self.vertex_buffer.slices.push(slice); vertex_offset += size; diff --git a/crates/egui-wgpu/src/setup.rs b/crates/egui-wgpu/src/setup.rs index 0c3cb8c39..9d83d4380 100644 --- a/crates/egui-wgpu/src/setup.rs +++ b/crates/egui-wgpu/src/setup.rs @@ -1,5 +1,48 @@ use std::sync::Arc; +/// A cloneable display handle for use with [`wgpu::InstanceDescriptor`]. +/// +/// This trait exists so that a [`winit::event_loop::OwnedDisplayHandle`] (or similar platform +/// display handle) can be stored, cloned, and later passed to wgpu. +/// +/// wgpu requires an explicit display handle for GLES on some platforms (notably Wayland). +/// Because [`wgpu::InstanceDescriptor`] contains a `Box` which is +/// not cloneable, we wrap the handle in this trait so it can be cloned alongside the rest of +/// the egui wgpu configuration. +/// +/// This is automatically implemented for all types that satisfy the bounds (including +/// [`winit::event_loop::OwnedDisplayHandle`]). +pub trait EguiDisplayHandle: + wgpu::rwh::HasDisplayHandle + std::fmt::Debug + Send + Sync + 'static +{ + /// Clone this handle into a `Box` suitable for setting on + /// [`wgpu::InstanceDescriptor::display`]. + fn clone_for_wgpu(&self) -> Box; + + /// Clone this handle into a new `Box`. + fn clone_display_handle(&self) -> Box; +} + +impl Clone for Box { + fn clone(&self) -> Self { + // We need to deref here, otherwise this causes infinite recursion stack overflow. + (**self).clone_display_handle() + } +} + +impl EguiDisplayHandle for T +where + T: wgpu::rwh::HasDisplayHandle + Clone + std::fmt::Debug + Send + Sync + 'static, +{ + fn clone_for_wgpu(&self) -> Box { + Box::new(self.clone()) + } + + fn clone_display_handle(&self) -> Box { + Box::new(self.clone()) + } +} + #[derive(Clone)] pub enum WgpuSetup { /// Construct a wgpu setup using some predefined settings & heuristics. @@ -22,9 +65,32 @@ pub enum WgpuSetup { Existing(WgpuSetupExisting), } -impl Default for WgpuSetup { - fn default() -> Self { - Self::CreateNew(WgpuSetupCreateNew::default()) +impl WgpuSetup { + /// Creates a new [`WgpuSetup::CreateNew`] with the given display handle. + /// + /// This is the recommended constructor. Most platforms (Windows, macOS/iOS, Android, web) + /// work fine without a display handle, but some (e.g. Wayland on Linux with GLES) require + /// one. Providing it unconditionally ensures your app works everywhere. + /// + /// If you don't have a display handle available, use [`Self::without_display_handle`] + /// instead β€” it will still work on the majority of platforms. + /// + /// With winit, pass [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle). + pub fn from_display_handle(display_handle: impl EguiDisplayHandle) -> Self { + Self::CreateNew(WgpuSetupCreateNew::from_display_handle(display_handle)) + } + + /// Creates a new [`WgpuSetup::CreateNew`] without a display handle. + /// + /// A display handle is not required for headless operation (offscreen rendering, tests, + /// compute-only workloads). It also isn't needed on most platforms even when presenting + /// to a window β€” only some configurations (e.g. Wayland on Linux with GLES) require one. + /// + /// If you do have a display handle available, prefer [`Self::from_display_handle`] for + /// maximum compatibility. With winit you can obtain one via + /// [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle). + pub fn without_display_handle() -> Self { + Self::CreateNew(WgpuSetupCreateNew::without_display_handle()) } } @@ -65,8 +131,18 @@ impl WgpuSetup { } log::debug!("Creating wgpu instance with backends {backends:?}"); - wgpu::util::new_instance_with_webgpu_detection(&create_new.instance_descriptor) - .await + let desc = &create_new.instance_descriptor; + let descriptor = wgpu::InstanceDescriptor { + backends: desc.backends, + flags: desc.flags, + backend_options: desc.backend_options.clone(), + memory_budget_thresholds: desc.memory_budget_thresholds, + display: create_new + .display_handle + .as_ref() + .map(|handle| handle.clone_for_wgpu()), + }; + wgpu::util::new_instance_with_webgpu_detection(descriptor).await } Self::Existing(existing) => existing.instance.clone(), } @@ -98,9 +174,28 @@ pub type NativeAdapterSelectorMethod = Arc< /// Configuration for creating a new wgpu setup. /// /// Used for [`WgpuSetup::CreateNew`]. +/// +/// Use [`Self::from_display_handle`] when you have a display handle available β€” this is the +/// recommended constructor. With winit you can obtain one via +/// [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle). +/// Most platforms (Windows, macOS/iOS, Android, web) work fine without one, but some +/// (e.g. Wayland on Linux with GLES) require it. Providing it unconditionally ensures your +/// app works everywhere. +/// +/// If you don't have a display handle, use [`Self::without_display_handle`] β€” it will still +/// work on the majority of platforms, and is appropriate for headless rendering, tests, or +/// web targets. +/// +/// Note: The [`wgpu::InstanceDescriptor::display`] field is always stored as `None` in +/// [`Self::instance_descriptor`]. The display handle is stored separately so it can be cloned +/// (since [`wgpu::InstanceDescriptor`] itself does not implement `Clone`), and is injected +/// into the descriptor at instance creation time. pub struct WgpuSetupCreateNew { /// Instance descriptor for creating a wgpu instance. /// + /// The [`wgpu::InstanceDescriptor::display`] field should be left as `None`; use the + /// [`Self::display_handle`] field instead (it will be injected when the instance is created). + /// /// The most important field is [`wgpu::InstanceDescriptor::backends`], which /// controls which backends are supported (wgpu will pick one of these). /// If you only want to support WebGL (and not WebGPU), @@ -110,6 +205,16 @@ pub struct WgpuSetupCreateNew { /// and only if you have enabled the `webgl` feature of crate `wgpu`. pub instance_descriptor: wgpu::InstanceDescriptor, + /// The display handle to pass to wgpu when creating the instance. + /// + /// Most platforms (Windows, macOS/iOS, Android, web) work without this, but some + /// (e.g. Wayland on Linux with GLES) require it. If you have a display handle + /// available, providing it ensures maximum compatibility. + /// + /// When using winit, this is typically the + /// [`winit::event_loop::OwnedDisplayHandle`] obtained from the event loop. + pub display_handle: Option>, + /// Power preference for the adapter if [`Self::native_adapter_selector`] is not set or targeting web. pub power_preference: wgpu::PowerPreference, @@ -128,32 +233,34 @@ pub struct WgpuSetupCreateNew { Arc wgpu::DeviceDescriptor<'static> + Send + Sync>, } -impl Clone for WgpuSetupCreateNew { - fn clone(&self) -> Self { +impl WgpuSetupCreateNew { + /// Creates a new configuration with the given display handle. + /// + /// This is the recommended constructor. Most platforms (Windows, macOS/iOS, Android, web) + /// work fine without a display handle, but some (e.g. Wayland on Linux with GLES) require + /// one. Providing it unconditionally ensures your app works everywhere. + /// + /// If you don't have a display handle available, use [`Self::without_display_handle`] + /// instead β€” it will still work on the majority of platforms. + /// + /// With winit, pass [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle). + pub fn from_display_handle(display_handle: impl EguiDisplayHandle) -> Self { Self { - instance_descriptor: self.instance_descriptor.clone(), - power_preference: self.power_preference, - native_adapter_selector: self.native_adapter_selector.clone(), - device_descriptor: Arc::clone(&self.device_descriptor), + display_handle: Some(Box::new(display_handle)), + ..Self::without_display_handle() } } -} -impl std::fmt::Debug for WgpuSetupCreateNew { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("WgpuSetupCreateNew") - .field("instance_descriptor", &self.instance_descriptor) - .field("power_preference", &self.power_preference) - .field( - "native_adapter_selector", - &self.native_adapter_selector.is_some(), - ) - .finish() - } -} - -impl Default for WgpuSetupCreateNew { - fn default() -> Self { + /// Creates a new configuration without a display handle. + /// + /// A display handle is not required for headless operation (offscreen rendering, tests, + /// compute-only workloads). It also isn't needed on most platforms even when presenting + /// to a window β€” only some configurations (e.g. Wayland on Linux with GLES) require one. + /// + /// If you do have a display handle available, prefer [`Self::from_display_handle`] for + /// maximum compatibility. With winit you can obtain one via + /// [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle). + pub fn without_display_handle() -> Self { Self { instance_descriptor: wgpu::InstanceDescriptor { // Add GL backend, primarily because WebGPU is not stable enough yet. @@ -163,8 +270,11 @@ impl Default for WgpuSetupCreateNew { flags: wgpu::InstanceFlags::from_build_config().with_env(), backend_options: wgpu::BackendOptions::from_env_or_default(), memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(), + display: None, }, + display_handle: None, + power_preference: wgpu::PowerPreference::from_env() .unwrap_or(wgpu::PowerPreference::HighPerformance), @@ -192,6 +302,39 @@ impl Default for WgpuSetupCreateNew { } } +impl Clone for WgpuSetupCreateNew { + fn clone(&self) -> Self { + let desc = &self.instance_descriptor; + Self { + instance_descriptor: wgpu::InstanceDescriptor { + backends: desc.backends, + flags: desc.flags, + backend_options: desc.backend_options.clone(), + memory_budget_thresholds: desc.memory_budget_thresholds, + display: None, + }, + display_handle: self.display_handle.clone(), + power_preference: self.power_preference, + native_adapter_selector: self.native_adapter_selector.clone(), + device_descriptor: Arc::clone(&self.device_descriptor), + } + } +} + +impl std::fmt::Debug for WgpuSetupCreateNew { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WgpuSetupCreateNew") + .field("instance_descriptor", &self.instance_descriptor) + .field("display_handle", &self.display_handle) + .field("power_preference", &self.power_preference) + .field( + "native_adapter_selector", + &self.native_adapter_selector.is_some(), + ) + .finish() + } +} + /// Configuration for using an existing wgpu setup. /// /// Used for [`WgpuSetup::Existing`]. diff --git a/crates/egui-wgpu/src/winit.rs b/crates/egui-wgpu/src/winit.rs index 5fb8d123a..3f6adfc27 100644 --- a/crates/egui-wgpu/src/winit.rs +++ b/crates/egui-wgpu/src/winit.rs @@ -17,6 +17,7 @@ struct SurfaceState { width: u32, height: u32, resizing: bool, + needs_reconfigure: bool, } /// Everything you need to paint egui with [`wgpu`] on [`winit`]. @@ -234,6 +235,7 @@ impl Painter { height: size.height, alpha_mode, resizing: false, + needs_reconfigure: false, }, ); let Some(width) = NonZeroU32::new(size.width) else { @@ -368,7 +370,7 @@ impl Painter { hal_surface .render_layer() .lock() - .set_presents_with_transaction(resizing); + .setPresentsWithTransaction(resizing); Self::configure_surface( state, @@ -454,7 +456,7 @@ impl Painter { commands_submitted: false, }; - let Some(surface_state) = self.surfaces.get(&viewport_id) else { + let Some(surface_state) = self.surfaces.get_mut(&viewport_id) else { return vsync_sec; }; @@ -491,6 +493,11 @@ impl Painter { ) }; + if surface_state.needs_reconfigure { + Self::configure_surface(surface_state, render_state, &self.configuration); + surface_state.needs_reconfigure = false; + } + let output_frame = { profiling::scope!("get_current_texture"); // This is what vsync-waiting happens on my Mac. @@ -501,16 +508,20 @@ impl Painter { }; let output_frame = match output_frame { - Ok(frame) => frame, - Err(err) => match (*self.configuration.on_surface_error)(err) { - SurfaceErrorAction::RecreateSurface => { - Self::configure_surface(surface_state, render_state, &self.configuration); - return vsync_sec; + wgpu::CurrentSurfaceTexture::Success(frame) => frame, + wgpu::CurrentSurfaceTexture::Suboptimal(frame) => { + surface_state.needs_reconfigure = true; + frame + } + other => { + match (*self.configuration.on_surface_status)(&other) { + SurfaceErrorAction::RecreateSurface => { + Self::configure_surface(surface_state, render_state, &self.configuration); + } + SurfaceErrorAction::SkipFrame => {} } - SurfaceErrorAction::SkipFrame => { - return vsync_sec; - } - }, + return vsync_sec; + } }; let mut capture_buffer = None; diff --git a/crates/egui_demo_app/src/apps/custom3d_wgpu.rs b/crates/egui_demo_app/src/apps/custom3d_wgpu.rs index fd1d9ae73..d83f000a4 100644 --- a/crates/egui_demo_app/src/apps/custom3d_wgpu.rs +++ b/crates/egui_demo_app/src/apps/custom3d_wgpu.rs @@ -40,7 +40,7 @@ impl Custom3d { let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("custom3d"), - bind_group_layouts: &[&bind_group_layout], + bind_group_layouts: &[Some(&bind_group_layout)], immediate_size: 0, }); diff --git a/crates/egui_kittest/src/wgpu.rs b/crates/egui_kittest/src/wgpu.rs index 3f97e0036..a9f0de9ad 100644 --- a/crates/egui_kittest/src/wgpu.rs +++ b/crates/egui_kittest/src/wgpu.rs @@ -17,7 +17,8 @@ pub(crate) const WAIT_TIMEOUT: Duration = Duration::from_secs(10); /// Default wgpu setup used for the wgpu renderer. pub fn default_wgpu_setup() -> egui_wgpu::WgpuSetup { - let mut setup = egui_wgpu::WgpuSetupCreateNew::default(); + // No display handle needed for headless testing β€” we don't present to a window. + let mut setup = egui_wgpu::WgpuSetupCreateNew::without_display_handle(); // WebGPU not supported yet since we rely on blocking screenshots. setup @@ -58,6 +59,7 @@ pub fn default_wgpu_setup() -> egui_wgpu::WgpuSetup { } pub fn create_render_state(setup: WgpuSetup) -> egui_wgpu::RenderState { + // No display handle needed for headless testing β€” we don't present to a window. let instance = pollster::block_on(setup.new_instance()); pollster::block_on(egui_wgpu::RenderState::create( From 0d2f6cf4e686462a307eddd2043948039d8437b3 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 23 Mar 2026 18:53:32 +0100 Subject: [PATCH 36/58] Reduce warning level on occluded error --- crates/egui-wgpu/src/lib.rs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/crates/egui-wgpu/src/lib.rs b/crates/egui-wgpu/src/lib.rs index 05936d6cd..eb04173b5 100644 --- a/crates/egui-wgpu/src/lib.rs +++ b/crates/egui-wgpu/src/lib.rs @@ -348,13 +348,21 @@ impl Default for WgpuConfiguration { // `WgpuSetup::from_display_handle(...)` before creating the instance if one is available. wgpu_setup: WgpuSetup::without_display_handle(), on_surface_status: Arc::new(|status| { - if matches!(status, wgpu::CurrentSurfaceTexture::Outdated) { - // This error occurs when the app is minimized on Windows. - // Silently return here to prevent spamming the console with: - // "The underlying surface has changed, and therefore the swap chain must be updated" - } else { - log::warn!("Dropped frame with error: {status:?}"); + match status { + wgpu::CurrentSurfaceTexture::Outdated => { + // This error occurs when the app is minimized on Windows. + // Silently return here to prevent spamming the console with: + // "The underlying surface has changed, and therefore the swap chain must be updated" + } + wgpu::CurrentSurfaceTexture::Occluded => { + // This error occurs when the application is occluded (e.g. minimized or behind another window). + log::debug!("Dropped frame with error: {status:?}"); + } + _ => { + log::warn!("Dropped frame with error: {status:?}"); + } } + SurfaceErrorAction::SkipFrame }), } From fbe5763a91415a6e4277330718bc67c4c1f853b0 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 23 Mar 2026 18:53:45 +0100 Subject: [PATCH 37/58] Remove fixed RUSTSEC advisory from deny.toml --- deny.toml | 1 - scripts/check.sh | 1 - 2 files changed, 2 deletions(-) diff --git a/deny.toml b/deny.toml index 595641501..2d3231535 100644 --- a/deny.toml +++ b/deny.toml @@ -34,7 +34,6 @@ ignore = [ "RUSTSEC-2024-0320", # unmaintained yaml-rust pulled in by syntect "RUSTSEC-2024-0436", # unmaintained paste pulled via metal/wgpu, see https://github.com/gfx-rs/metal-rs/issues/349 "RUSTSEC-2025-0141", # https://rustsec.org/advisories/RUSTSEC-2025-0141 - bincode is unmaintained - https://git.sr.ht/~stygianentity/bincode/tree/v3.0/item/README.md - "RUSTSEC-2026-0049", # https://rustsec.org/advisories/RUSTSEC-2026-0049 ] [bans] diff --git a/scripts/check.sh b/scripts/check.sh index 71dc38386..ce0b3b8d5 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -29,7 +29,6 @@ cargo check --quiet --all-targets cargo check --quiet --all-targets --all-features cargo check --quiet -p egui_demo_app --lib --target wasm32-unknown-unknown cargo check --quiet -p egui_demo_app --lib --target wasm32-unknown-unknown --all-features -# TODO(#5297) re-enable --all-features once the tests work with the unity feature cargo test --quiet --all-targets --all-features cargo test --quiet --doc # slow - checks all doc-tests From 91effb9e57a911befd2552cc94f505902053c895 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 24 Mar 2026 11:02:44 +0100 Subject: [PATCH 38/58] Update kittest to 0.4.0 (#8002) --- Cargo.lock | 5 +++-- Cargo.toml | 5 +---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e9694b07f..152d178b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2483,8 +2483,9 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "kittest" -version = "0.3.0" -source = "git+https://github.com/rerun-io/kittest?branch=main#ce7a2f3b12c36021889b50bdff671cec8016b0fb" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ceaa75eb0036a32b6b9833962eb18137449e9817e2e586006471925b727fd5" dependencies = [ "accesskit", "accesskit_consumer", diff --git a/Cargo.toml b/Cargo.toml index b084399a6..d042c2349 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,7 +97,7 @@ glutin-winit = { version = "0.5.0", default-features = false } home = "0.5.9" image = { version = "0.25.6", default-features = false } js-sys = "0.3.77" -kittest = { version = "0.3.0" } +kittest = { version = "0.4.0" } log = { version = "0.4.28", features = ["std"] } memoffset = "0.9.1" mimalloc = "0.1.48" @@ -148,9 +148,6 @@ wgpu = { version = "29.0.0", default-features = false, features = ["std"] } windows-sys = "0.61.2" winit = { version = "0.30.12", default-features = false } -[patch.crates-io] -kittest = { git = "https://github.com/rerun-io/kittest", branch = "main" } - [workspace.lints.rust] unsafe_code = "deny" From c0ea6117e0526c86c8d113f316cf217d6bc04f86 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 24 Mar 2026 11:04:36 +0100 Subject: [PATCH 39/58] Use `AtomLayoutResponse` in `TextEditOutput` (#8003) This is necessary to add e.g. buttons as prefix/suffix within the textedit (so you can read the rect and place the button). I'm not sure if deref on the AtomLayoutResponse is the right call but it should make the migration easier and is generally convenient (but might lead to some confusion). --- crates/egui/src/atomics/atom_layout.rs | 14 ++++++++++++++ crates/egui/src/widgets/text_edit/builder.rs | 7 +++---- crates/egui/src/widgets/text_edit/output.rs | 2 +- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/crates/egui/src/atomics/atom_layout.rs b/crates/egui/src/atomics/atom_layout.rs index da7f672a1..c408146d6 100644 --- a/crates/egui/src/atomics/atom_layout.rs +++ b/crates/egui/src/atomics/atom_layout.rs @@ -520,6 +520,20 @@ impl AtomLayoutResponse { } } +impl Deref for AtomLayoutResponse { + type Target = Response; + + fn deref(&self) -> &Self::Target { + &self.response + } +} + +impl DerefMut for AtomLayoutResponse { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.response + } +} + impl Widget for AtomLayout<'_> { fn ui(self, ui: &mut Ui) -> Response { self.show(ui).response diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 50154102a..086cf3091 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -409,7 +409,7 @@ impl<'t> TextEdit<'t> { impl Widget for TextEdit<'_> { fn ui(self, ui: &mut Ui) -> Response { - self.show(ui).response + self.show(ui).response.response } } @@ -563,7 +563,7 @@ impl TextEdit<'_> { // so we can clip it to the available size. Thus, extract it from the atom closure here. let mut get_galley = None; let inner_rect_id = Id::new("text_edit_rect"); - let atom_response = { + let mut response = { let any_shrink = hint_text.any_shrink(); // Ideally we could just do `let mut atoms = prefix` here, but that won't compile // but due to servo/rust-smallvec#146 (also see the comment below). @@ -716,8 +716,7 @@ impl TextEdit<'_> { allocated.paint(ui) }; - let inner_rect = atom_response.rect(inner_rect_id).unwrap_or(Rect::ZERO); - let mut response = atom_response.response; + let inner_rect = response.rect(inner_rect_id).unwrap_or(Rect::ZERO); // Our atom closure was now called, so the galley should always be available here let mut galley = get_galley.expect("Galley should be available here"); diff --git a/crates/egui/src/widgets/text_edit/output.rs b/crates/egui/src/widgets/text_edit/output.rs index 8149bbe58..3339f325e 100644 --- a/crates/egui/src/widgets/text_edit/output.rs +++ b/crates/egui/src/widgets/text_edit/output.rs @@ -5,7 +5,7 @@ use crate::text::CCursorRange; /// The output from a [`TextEdit`](crate::TextEdit). pub struct TextEditOutput { /// The interaction response. - pub response: crate::Response, + pub response: crate::AtomLayoutResponse, /// How the text was displayed. pub galley: Arc, From 7fbd1315ecd1782813c1e3f44bf25fd31c464c1f Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 24 Mar 2026 11:28:49 +0100 Subject: [PATCH 40/58] Include LICENSE files in published crates (#8004) * Closes https://github.com/emilk/egui/issues/7977 --- crates/ecolor/Cargo.toml | 2 +- crates/eframe/Cargo.toml | 2 +- crates/egui-wgpu/Cargo.toml | 2 +- crates/egui-winit/Cargo.toml | 2 +- crates/egui/Cargo.toml | 2 +- crates/egui_demo_lib/Cargo.toml | 2 +- crates/egui_extras/Cargo.toml | 2 +- crates/egui_glow/Cargo.toml | 2 +- crates/egui_kittest/Cargo.toml | 2 +- crates/emath/Cargo.toml | 2 +- crates/epaint/Cargo.toml | 2 +- crates/epaint_default_fonts/Cargo.toml | 4 ++-- 12 files changed, 13 insertions(+), 13 deletions(-) diff --git a/crates/ecolor/Cargo.toml b/crates/ecolor/Cargo.toml index f37aff109..43543113a 100644 --- a/crates/ecolor/Cargo.toml +++ b/crates/ecolor/Cargo.toml @@ -11,7 +11,7 @@ readme = "README.md" repository = "https://github.com/emilk/egui" categories = ["mathematics", "encoding"] keywords = ["gui", "color", "conversion", "gamedev", "images"] -include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"] +include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml"] [lints] workspace = true diff --git a/crates/eframe/Cargo.toml b/crates/eframe/Cargo.toml index 86f63c50e..376f2ecbb 100644 --- a/crates/eframe/Cargo.toml +++ b/crates/eframe/Cargo.toml @@ -11,7 +11,7 @@ readme = "README.md" repository = "https://github.com/emilk/egui/tree/main/crates/eframe" categories = ["gui", "game-development"] keywords = ["egui", "gui", "gamedev"] -include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml", "data/icon.png"] +include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml", "data/icon.png"] [package.metadata.docs.rs] all-features = true diff --git a/crates/egui-wgpu/Cargo.toml b/crates/egui-wgpu/Cargo.toml index c514e0a49..86cd3192b 100644 --- a/crates/egui-wgpu/Cargo.toml +++ b/crates/egui-wgpu/Cargo.toml @@ -15,7 +15,7 @@ readme = "README.md" repository = "https://github.com/emilk/egui/tree/main/crates/egui-wgpu" categories = ["gui", "game-development"] keywords = ["wgpu", "egui", "gui", "gamedev"] -include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "**/*.wgsl", "Cargo.toml"] +include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "**/*.wgsl", "Cargo.toml"] [lints] workspace = true diff --git a/crates/egui-winit/Cargo.toml b/crates/egui-winit/Cargo.toml index d1b2ab220..bb3576a2d 100644 --- a/crates/egui-winit/Cargo.toml +++ b/crates/egui-winit/Cargo.toml @@ -11,7 +11,7 @@ readme = "README.md" repository = "https://github.com/emilk/egui/tree/main/crates/egui-winit" categories = ["gui", "game-development"] keywords = ["winit", "egui", "gui", "gamedev"] -include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"] +include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml"] [lints] workspace = true diff --git a/crates/egui/Cargo.toml b/crates/egui/Cargo.toml index 764d2401e..3a22bf529 100644 --- a/crates/egui/Cargo.toml +++ b/crates/egui/Cargo.toml @@ -11,7 +11,7 @@ readme = "../../README.md" repository = "https://github.com/emilk/egui" categories = ["gui", "game-development"] keywords = ["gui", "imgui", "immediate", "portable", "gamedev"] -include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"] +include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml"] [lints] workspace = true diff --git a/crates/egui_demo_lib/Cargo.toml b/crates/egui_demo_lib/Cargo.toml index 3e61187d5..4f3b853e0 100644 --- a/crates/egui_demo_lib/Cargo.toml +++ b/crates/egui_demo_lib/Cargo.toml @@ -11,7 +11,7 @@ readme = "README.md" repository = "https://github.com/emilk/egui/tree/main/crates/egui_demo_lib" categories = ["gui", "graphics"] keywords = ["glow", "egui", "gui", "gamedev"] -include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml", "data/*"] +include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml", "data/*"] [lints] workspace = true diff --git a/crates/egui_extras/Cargo.toml b/crates/egui_extras/Cargo.toml index 6639126ef..b124148bc 100644 --- a/crates/egui_extras/Cargo.toml +++ b/crates/egui_extras/Cargo.toml @@ -15,7 +15,7 @@ readme = "README.md" repository = "https://github.com/emilk/egui" categories = ["gui", "game-development"] keywords = ["gui", "imgui", "immediate", "portable", "gamedev"] -include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"] +include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml"] [lints] workspace = true diff --git a/crates/egui_glow/Cargo.toml b/crates/egui_glow/Cargo.toml index 4a083611b..f714dd8e0 100644 --- a/crates/egui_glow/Cargo.toml +++ b/crates/egui_glow/Cargo.toml @@ -11,7 +11,7 @@ readme = "README.md" repository = "https://github.com/emilk/egui/tree/main/crates/egui_glow" categories = ["gui", "game-development"] keywords = ["glow", "egui", "gui", "gamedev"] -include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml", "src/shader/*.glsl"] +include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml", "src/shader/*.glsl"] [lints] workspace = true diff --git a/crates/egui_kittest/Cargo.toml b/crates/egui_kittest/Cargo.toml index f922b807e..10938cd37 100644 --- a/crates/egui_kittest/Cargo.toml +++ b/crates/egui_kittest/Cargo.toml @@ -11,7 +11,7 @@ readme = "./README.md" repository = "https://github.com/emilk/egui" categories = ["gui", "development-tools::testing", "accessibility"] keywords = ["gui", "immediate", "egui", "testing", "accesskit"] -include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"] +include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/emath/Cargo.toml b/crates/emath/Cargo.toml index 416d32750..9895fffc6 100644 --- a/crates/emath/Cargo.toml +++ b/crates/emath/Cargo.toml @@ -11,7 +11,7 @@ readme = "README.md" repository = "https://github.com/emilk/egui/tree/main/crates/emath" categories = ["mathematics", "gui"] keywords = ["math", "gui"] -include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"] +include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml"] [lints] workspace = true diff --git a/crates/epaint/Cargo.toml b/crates/epaint/Cargo.toml index c8a05e4d7..9d6e3eade 100644 --- a/crates/epaint/Cargo.toml +++ b/crates/epaint/Cargo.toml @@ -11,7 +11,7 @@ readme = "README.md" repository = "https://github.com/emilk/egui/tree/main/crates/epaint" categories = ["graphics", "gui"] keywords = ["graphics", "gui", "egui"] -include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"] +include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml"] [lints] workspace = true diff --git a/crates/epaint_default_fonts/Cargo.toml b/crates/epaint_default_fonts/Cargo.toml index 588719871..43f6fedfe 100644 --- a/crates/epaint_default_fonts/Cargo.toml +++ b/crates/epaint_default_fonts/Cargo.toml @@ -12,8 +12,8 @@ repository = "https://github.com/emilk/egui/tree/main/crates/epaint_default_font categories = ["graphics", "gui"] keywords = ["graphics", "gui", "egui"] include = [ - "../LICENSE-APACHE", - "../LICENSE-MIT", + "../../LICENSE-APACHE", + "../../LICENSE-MIT", "**/*.rs", "Cargo.toml", "fonts/*.ttf", From 2a03ae1348ad7473452672d37f1a74bcf3bda3c9 Mon Sep 17 00:00:00 2001 From: RndUsr123 <150948884+RndUsr123@users.noreply.github.com> Date: Tue, 24 Mar 2026 10:35:37 +0000 Subject: [PATCH 41/58] Enables every combination of `TextEdit` and `LayoutJob` alignments (#7831) This is a fix/improvement that makes all kinds of alignments work in `TextEdit` when a custom `LayoutJob` with halign is used. I used the simplest approach possible to avoid unwanted bugs as I wasn't sure what's safe to change, but there's potentially better ways to achieve this. In particular, I'm not sure I fully understand the rationale behind aligning rows in a `Galley` based on the latter's leftmost border, considering the size is what's ultimately used in widgets (in `TextEdit` at least). Regardless, here's a demo of this PR: https://github.com/user-attachments/assets/5d9801d7-73af-4576-80c5-47f169700462 * [x] I have followed the instructions in the PR template --------- Co-authored-by: Emil Ernerfeldt --- crates/egui/src/widgets/text_edit/builder.rs | 6 ++- crates/epaint/src/text/text_layout_types.rs | 4 +- tests/egui_tests/tests/regression_tests.rs | 54 ++++++++++++++++++- .../tests/snapshots/text_edit_halign.png | 3 ++ 4 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 tests/egui_tests/tests/snapshots/text_edit_halign.png diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 086cf3091..a6c41e71c 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -820,7 +820,11 @@ impl TextEdit<'_> { paint_text_selection(&mut galley, ui.visuals(), &cursor_range, None); } - painter.galley(galley_pos, Arc::clone(&galley), text_color); + painter.galley( + galley_pos - vec2(galley.rect.left(), 0.0), + Arc::clone(&galley), + text_color, + ); if has_focus && let Some(cursor_range) = state.cursor.range(&galley) { let primary_cursor_rect = cursor_rect(&galley, &cursor_range.primary, row_height) diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index d887fb13a..22fb03c57 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -1047,7 +1047,7 @@ impl Galley { return self.end_pos(); }; - let x = row.x_offset(layout_cursor.column); + let x = row.x_offset(layout_cursor.column) + row.pos.x - self.rect.left(); Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y())) } @@ -1092,7 +1092,7 @@ impl Galley { if is_pos_within_row || y_dist < best_y_dist { best_y_dist = y_dist; // char_at is `Row` not `PlacedRow` relative which means we have to subtract the pos. - let column = row.char_at(pos.x - row.pos.x); + let column = row.char_at(pos.x - row.pos.x + self.rect.left()); let prefer_next_row = column < row.char_count_excluding_newline(); cursor = CCursor { index: ccursor_index + column, diff --git a/tests/egui_tests/tests/regression_tests.rs b/tests/egui_tests/tests/regression_tests.rs index d61b76af3..d92a77103 100644 --- a/tests/egui_tests/tests/regression_tests.rs +++ b/tests/egui_tests/tests/regression_tests.rs @@ -1,8 +1,13 @@ +use std::sync::Arc; + +use egui::ScrollArea; use egui::accesskit::Role; use egui::epaint::Shape; use egui::style::ScrollAnimation; +use egui::text::{LayoutJob, TextWrapping}; use egui::{ - Align, Color32, Image, Label, Layout, RichText, ScrollArea, Sense, TextWrapMode, include_image, + Align, Color32, FontFamily, FontId, Image, Label, Layout, RichText, Sense, TextBuffer, + TextFormat, TextWrapMode, Ui, include_image, vec2, }; use egui_kittest::Harness; use egui_kittest::kittest::Queryable as _; @@ -66,6 +71,53 @@ fn text_edit_rtl() { } } +#[test] +fn text_edit_halign() { + let mut harness = Harness::builder().with_size((212.0, 212.0)).build_ui(|ui| { + ui.spacing_mut().item_spacing = vec2(2.0, 2.0); + + fn layouter(halign: Align) -> impl FnMut(&Ui, &dyn TextBuffer, f32) -> Arc { + move |ui: &egui::Ui, buf: &dyn egui::TextBuffer, wrap_width: f32| { + let mut job = LayoutJob { + wrap: TextWrapping { + max_rows: 4, + max_width: wrap_width, + ..Default::default() + }, + halign, + ..Default::default() + }; + job.append( + buf.as_str(), + 0.0, + TextFormat::simple(FontId::new(13.0, FontFamily::Proportional), Color32::GRAY), + ); + ui.fonts_mut(|f| f.layout_job(job)) + } + } + + for widget_alignment in [Align::Min, Align::Center, Align::Max] { + ui.horizontal(|ui| { + for text_alignment in [Align::LEFT, Align::Center, Align::RIGHT] { + ui.add_sized( + vec2(64.0, 64.0), + egui::TextEdit::multiline(&mut format!( + "{widget_alignment:?}\n+\n{text_alignment:?}", + )) + .layouter(&mut layouter(text_alignment)) + .vertical_align(widget_alignment) + .horizontal_align(widget_alignment), + ); + } + }); + } + }); + + harness.get_by_value("Center\n+\nCenter").focus(); + harness.step(); + harness.snapshot("text_edit_halign"); +} + #[test] fn text_edit_delay() { let mut text = String::new(); diff --git a/tests/egui_tests/tests/snapshots/text_edit_halign.png b/tests/egui_tests/tests/snapshots/text_edit_halign.png new file mode 100644 index 000000000..29546a036 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/text_edit_halign.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:502607c803b884e4e1640d39c97b03b0a40df93c2da328f889168e386f837f36 +size 13261 From cc7cfd27cabc62233a7b7dbb452f39f544093abe Mon Sep 17 00:00:00 2001 From: ilya sheprut Date: Tue, 24 Mar 2026 13:37:37 +0300 Subject: [PATCH 42/58] Fix `horizontal_wrapping` row height after using `text_edit_multiline` (#8000) * [x] I have followed the instructions in the PR template This PR have two commits: * **First commit** - introduction of tests and their canonization image. Expected behaviour is that `horizontal_wrapped_multiline_row_height` would match `horizontal_wrapped_multiline_row_height_reference`, but it doesn't. There is a bug in `horizontal_wrapped` that breaks line height after using `text_edit_multiline`. * **Second commit** - fix. You can see that `horizontal_wrapped_multiline_row_height` now looks like `horizontal_wrapped_multiline_row_height_reference` (although it's not a perfect match, upd: found, this is because of this issue: https://github.com/emilk/egui/issues/4921). I have used LLM to help me with this PR (codex + claude code). BTW, I'm using horizontal_wrapped with end_row instead of vertical + horizontal alternation, because I automatically generate my UI through some complex interactions between elements in my code, and it's can be that my `horizontal` starts in one function, and ends in another. Something like `begin_horizontal`/`end_horizontal`/`get_current_layout` would be very handy, related to https://github.com/emilk/egui/issues/1004. Also, I would like indent to be supported in `horizontal_wrapped`, or also, to have `indent_start`/`indent_end`. This is why I used `monospace("| ")` in my example, it simulates my use-case. --- crates/egui/src/layout.rs | 20 ++++- tests/egui_tests/tests/regression_tests.rs | 85 +++++++++++++++++++ ...orizontal_wrapped_multiline_row_height.png | 3 + ...wrapped_multiline_row_height_reference.png | 3 + 4 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 tests/egui_tests/tests/snapshots/horizontal_wrapped_multiline_row_height.png create mode 100644 tests/egui_tests/tests/snapshots/horizontal_wrapped_multiline_row_height_reference.png diff --git a/crates/egui/src/layout.rs b/crates/egui/src/layout.rs index c44c928bb..c35fd254b 100644 --- a/crates/egui/src/layout.rs +++ b/crates/egui/src/layout.rs @@ -623,12 +623,24 @@ impl Layout { if (self.is_vertical() && self.horizontal_align() == Align::Center) || self.horizontal_justify() { - frame_size.x = frame_size.x.max(available_rect.width()); // fill full width + // For wrapping layouts, fill the current column width, not the entire layout width. + let width = if self.main_wrap { + region.cursor.width() + } else { + available_rect.width() + }; + frame_size.x = frame_size.x.max(width); // fill full width } if (self.is_horizontal() && self.vertical_align() == Align::Center) || self.vertical_justify() { - frame_size.y = frame_size.y.max(available_rect.height()); // fill full height + // For wrapping layouts, fill the current row height, not the entire layout height. + let height = if self.main_wrap { + region.cursor.height() + } else { + available_rect.height() + }; + frame_size.y = frame_size.y.max(height); // fill full height } let align2 = match self.main_dir { @@ -791,14 +803,14 @@ impl Layout { let new_top = region.cursor.bottom() + spacing.y; region.cursor = Rect::from_min_max( pos2(region.max_rect.left(), new_top), - pos2(INFINITY, new_top + region.cursor.height()), + pos2(INFINITY, new_top), ); } Direction::RightToLeft => { let new_top = region.cursor.bottom() + spacing.y; region.cursor = Rect::from_min_max( pos2(-INFINITY, new_top), - pos2(region.max_rect.right(), new_top + region.cursor.height()), + pos2(region.max_rect.right(), new_top), ); } Direction::TopDown | Direction::BottomUp => {} diff --git a/tests/egui_tests/tests/regression_tests.rs b/tests/egui_tests/tests/regression_tests.rs index d92a77103..2d6ab5c67 100644 --- a/tests/egui_tests/tests/regression_tests.rs +++ b/tests/egui_tests/tests/regression_tests.rs @@ -279,3 +279,88 @@ fn warn_if_rect_changes_id() { "Should warn when a widget rect changes Id between passes" ); } + +#[test] +fn horizontal_wrapped_multiline_row_height() { + let mut harness = Harness::builder().with_size((350.0, 300.0)).build_ui(|ui| { + ui.style_mut().interaction.tooltip_delay = 0.0; + ui.style_mut().interaction.show_tooltips_only_when_still = false; + + let mut string = String::new(); + + ui.horizontal_wrapped(|ui| { + ui.monospace("| "); + let _ = ui.button("A"); + let _ = ui.button("B"); + ui.end_row(); + + ui.monospace("| "); + let _ = ui.button("C"); + let _ = ui.button("D"); + let _ = ui.button("E"); + ui.end_row(); + + ui.monospace("| "); + ui.text_edit_multiline(&mut string); + ui.end_row(); + + ui.monospace("| "); + let _ = ui.button("F"); + let _ = ui.button("G"); + ui.end_row(); + + ui.monospace("| "); + let _ = ui.button("H"); + let _ = ui.button("I"); + let _ = ui.button("K"); + ui.end_row(); + }); + }); + + harness.snapshot("horizontal_wrapped_multiline_row_height"); +} + +#[test] +fn horizontal_wrapped_multiline_row_height_reference() { + let mut harness = Harness::builder().with_size((350.0, 300.0)).build_ui(|ui| { + ui.style_mut().interaction.tooltip_delay = 0.0; + ui.style_mut().interaction.show_tooltips_only_when_still = false; + + let mut string = String::new(); + + ui.vertical(|ui| { + ui.horizontal(|ui| { + ui.monospace("| "); + let _ = ui.button("A"); + let _ = ui.button("B"); + }); + + ui.horizontal(|ui| { + ui.monospace("| "); + let _ = ui.button("C"); + let _ = ui.button("D"); + let _ = ui.button("E"); + }); + + ui.horizontal(|ui| { + ui.monospace("| "); + ui.text_edit_multiline(&mut string); + }); + + ui.horizontal(|ui| { + ui.monospace("| "); + let _ = ui.button("F"); + let _ = ui.button("G"); + }); + + ui.horizontal(|ui| { + ui.monospace("| "); + let _ = ui.button("H"); + let _ = ui.button("I"); + let _ = ui.button("K"); + }); + }); + }); + + harness.snapshot("horizontal_wrapped_multiline_row_height_reference"); +} diff --git a/tests/egui_tests/tests/snapshots/horizontal_wrapped_multiline_row_height.png b/tests/egui_tests/tests/snapshots/horizontal_wrapped_multiline_row_height.png new file mode 100644 index 000000000..e6ac8e446 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/horizontal_wrapped_multiline_row_height.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef21b42f90401f6b85685e1cc37d07970b38d2b40394f53bbde5bd4f0d54fb95 +size 5340 diff --git a/tests/egui_tests/tests/snapshots/horizontal_wrapped_multiline_row_height_reference.png b/tests/egui_tests/tests/snapshots/horizontal_wrapped_multiline_row_height_reference.png new file mode 100644 index 000000000..a533a8401 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/horizontal_wrapped_multiline_row_height_reference.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5025f4cb528ae5edc387149f1d14523ab4b93058f0862e775a1c2276a3e77af6 +size 5377 From 90217f2ad1e21757e8fac8faff2c1a59c0010a24 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 24 Mar 2026 11:54:09 +0100 Subject: [PATCH 43/58] Add error message when calling `.render()` without `.update_buffers()` (#8005) * Closes https://github.com/emilk/egui/issues/7968 --- crates/egui-wgpu/src/renderer.rs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/crates/egui-wgpu/src/renderer.rs b/crates/egui-wgpu/src/renderer.rs index 3222a5521..640f33f51 100644 --- a/crates/egui-wgpu/src/renderer.rs +++ b/crates/egui-wgpu/src/renderer.rs @@ -1,5 +1,3 @@ -#![expect(clippy::unwrap_used)] // TODO(emilk): avoid unwraps - use std::{borrow::Cow, num::NonZeroU64, ops::Range}; use ahash::HashMap; @@ -516,8 +514,12 @@ impl Renderer { // Skip rendering zero-sized clip areas. if let Primitive::Mesh(_) = primitive { // If this is a mesh, we need to advance the index and vertex buffer iterators: - index_buffer_slices.next().unwrap(); - vertex_buffer_slices.next().unwrap(); + index_buffer_slices + .next() + .expect("You must call .update_buffers() before .render()"); + vertex_buffer_slices + .next() + .expect("You must call .update_buffers() before .render()"); } continue; } @@ -527,8 +529,12 @@ impl Renderer { match primitive { Primitive::Mesh(mesh) => { - let index_buffer_slice = index_buffer_slices.next().unwrap(); - let vertex_buffer_slice = vertex_buffer_slices.next().unwrap(); + let index_buffer_slice = index_buffer_slices + .next() + .expect("You must call .update_buffers() before .render()"); + let vertex_buffer_slice = vertex_buffer_slices + .next() + .expect("You must call .update_buffers() before .render()"); if let Some(Texture { bind_group, .. }) = self.textures.get(&mesh.texture_id) { render_pass.set_bind_group(1, bind_group, &[]); @@ -954,6 +960,7 @@ impl Renderer { let index_buffer_staging = queue.write_buffer_with( &self.index_buffer.buffer, 0, + #[expect(clippy::unwrap_used)] // Checked above NonZeroU64::new(required_index_buffer_size).unwrap(), ); @@ -998,6 +1005,7 @@ impl Renderer { let vertex_buffer_staging = queue.write_buffer_with( &self.vertex_buffer.buffer, 0, + #[expect(clippy::unwrap_used)] // Checked above NonZeroU64::new(required_vertex_buffer_size).unwrap(), ); From b4f9cd71407303087b607e0ae9c1b28b71397cb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uma=C4=B5o?= <107099960+umajho@users.noreply.github.com> Date: Tue, 24 Mar 2026 18:58:58 +0800 Subject: [PATCH 44/58] Much improved IME (#7967) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Closes #7809 * Closes #7876 * Closes #7908 * Supersedes #7877 * Supersedes #7898 * The author of the PR above replaced it with #7914, which additionally fixes another IME issue. I believe that fix deserves a separate PR. * Reverts #4794 * [x] I have followed the instructions in the PR template This approach is better than #7898 (#7914) because it correctly handles all three major IME types (Chinese, Japanese, and Korean) without requiring a predefined β€œIME mode”. ## Environments I haved tested this PR in

macOS 15.7.3 (AArch64, Host of other virtual machines) Run command: `cargo run -p egui_demo_app --release` Tested IMEs: - builtin Chinese IME (Shuangpin - Simplified) - builtin Japanese IME (Romaji) - builtin Korean IME (2-Set)
Windows 11 25H2 (AArch64, Virtual Machine) Build command: `cargo build --release -p egui_demo_app --target=x86_64-pc-windows-gnu --features=glow --no-default-features` (I cannot use `wgpu` due to [this bug](https://github.com/emilk/egui/issues/4381), which prevents debugging inside the VM. Anyways, the rendering backend should be irrelevant here.) Tested IMEs: - builtin Chinese IME (Shuangpin) - Sogou IME (Chinese Shuangpin) - WeType IME (Chinese Shuangpin) - builtin Japanese IME (Hiragana) - builtin Korean IME (2 Beolsik)
Linux [Wayland + IBus] (AArch64, Virtual Machine) Fedora KDE Plasma Desktop 43 [Wayland + IBus 1.5.33-rc2] (Not working at the moment because of [another issue](https://github.com/emilk/egui/issues/7485) that will be fixed by #7983. It is [a complicated story](https://github.com/emilk/egui/pull/7973#issuecomment-4074627603). ) > [!NOTE] > > IBus is partially broken in this system. The Input Method Selector refuses to select IBus. As a workaround, I have to open System Settings -> Virtual Keyboard and select β€œIBus Wayland” to start an IBus instance that works in egui. > > The funny thing is: the Chinese Intelligent Pinyin IME is broken in native Apps like System Settings and KWrite, but works correctly in egui! > >
Screencast: What > > ![2026-03-13 3 10 11β€―AM](https://github.com/user-attachments/assets/4001cf12-8089-46f5-9cf4-e41d8f77ee24) >
Build command: `cross build --release -p egui_demo_app --target=aarch64-unknown-linux-gnu --features=wayland,wgpu --no-default-features` (The Linux toolchain on my mac is somehow broken, so I used `cross` instead.) Tested IMEs: - Chinese Intelligent Pinyin IME (Shuangpin) - Japanese Anthy IME (Hiragana) - Korean Hangul IME
Linux [X11 + Fcitx5] (AArch64, Virtual Machine) Debian 13 [Cinnamon 6.4.10 + X11 + Fcitx5 5.1.2] Build command: `cross build --release -p egui_demo_app --target=aarch64-unknown-linux-gnu --features=x11,wgpu --no-default-features` Tested IMEs: - Chinese Shuangpin IME - Chinese Rime IME with `luna-pinyin` - Japanese Mozc IME (Hiragana) - Korean Hangul IME Unlike macOS and Linux + Wayland, key-release events for keys processed by the IME are still forwarded to `egui`. These appear to be harmless in practice. Unlike on Windows, however, they cannot be filtered reliably because there are no corresponding key-press events marked as β€œprocessed by IME”.
--- There are too many possible combinations to test (Operating Systems Γ— [Desktop Environment](https://en.wikipedia.org/wiki/Desktop_environment)s Γ— [Windowing System](https://en.wikipedia.org/wiki/Windowing_system)s Γ— [IMF](https://wiki.archlinux.org/title/Input_method#Input_method_framework)s Γ— [IME](https://en.wikipedia.org/wiki/Input_method)s Γ— …), and I only have access to a limited subset. For example, Google Japanese Input refused to install on my Windows VM, and some paid Japanese IMEs are not accessible to me. Therefore, I would appreciate feedback from people other than me using all kinds of environments. ## Details There are two possible approaches to removing keyboard events that have already been processed by an IME: * Approach 1: Filter out events inside `egui` that appear to have been received during IME composition. * Approach 2: Filter out such events in the platform backend (terminology [borrowed from imgui](https://github.com/ocornut/imgui/blob/master/docs/BACKENDS.md#using-standard-backends), e.g. the `egui-winit` crate or the code under `web/` in the `eframe` crate.). Both approaches already exist in `egui`: * #4794 uses the first approach, filtering these events in the `TextEdit`-related code. * `eframe` uses the second approach in its web integration. See: Compared to the first approach, the second has a clear advantage: when events are passed from the platform backends into `egui`, they are simplified and lose information. In contrast, events in the platform backends are the original events, which allows them to be handled more flexibly. This is also why #7898 (#7914), which attempts to address the issue from within the `egui` crate, struggles to make all IMEs work correctly at the same time and requires manually selecting an β€œIME mode”: the events received by `egui` have already been reduced and therefore lack necessary information. A more appropriate solution is to consistently follow the second approach, explicitly requiring platform backends not to forward events that have already been processed by the IME to `egui`. This is the method used in this PR. Specifically, this PR works within the `egui-winit` crate, where the original `KeyboardInput` events can be accessed. At least for key press events, these can be used directly to determine whether the event has already been processed by the IME on Windows (by checking whether `logical_key` equals `winit::keyboard::NamedKey::Process`). This makes it straightforward to ensure that all IMEs work correctly at the same time. This PR also reverts #4794, which took the first approach. It filters out some events that merely look like they were received during IME composition but actually are not. It also messes up the order of those events along the way. As a result, it caused several IME-related issues. One of the sections in the Demonstrations below will illustrate these problems. ## Demonstrations
Changes not included in this PR for displaying Unicode characters in demonstrations Download `unifont-17.0.03.otf` from , and place it at `crates/egui_demo_app/src/unifont-17.0.03.otf`. In `crates/egui_demo_app/src/wrap_app.rs`, add these lines at the beginning of `impl WrapApp`'s `pub fn new`: ```rust { const MAIN_FONT: &'static [u8] = include_bytes!("./unifont-17.0.03.otf"); let mut fonts = egui::FontDefinitions::default(); fonts.font_data.insert( "main-font".to_owned(), std::sync::Arc::new(egui::FontData::from_static(MAIN_FONT)), ); let proportional = fonts .families .entry(egui::FontFamily::Proportional) .or_default(); proportional.insert(0, "main-font".to_owned()); cc.egui_ctx.set_fonts(fonts); } ``` (I took this from somewhere, but I forgot where it is. Sorry…)
[GNU Unifont](https://unifoundry.com/unifont/index.html) is licensed under [OFL-1.1](https://unifoundry.com/OFL-1.1.txt). ### This PR Fixes: Focus on a single-line `TextEdit` is lost after completing candidate selection with Japanese IME on Windows (#7809)
Screencast: βœ… Japanese IME now behaves correctly while Korean IME behaves as before ![7809](https://github.com/user-attachments/assets/6e92f6e6-fed4-46f4-96e1-e0e12dadc360)
### This PR Fixes: Committing Japanese IME text with Enter inserts an unintended newline in multiline `TextEdit` on Windows (#7876)
Screencast: βœ… Japanese IME now behaves correctly while Korean IME behaves as before ![7876](https://github.com/user-attachments/assets/03d2cb22-fd0c-45fe-9132-e59fa39bfcf3)
### This PR Fixes: Backspacing deletes characters during composition in certain Chinese IMEs (e.g., Sogou) on Windows (#7908)
Screencast: βœ… Sogou IME now behaves correctly ![7908](https://github.com/user-attachments/assets/2c63de28-26f0-4387-9c50-dceabfdbe99d)
### This PR Obsoletes #4794, because `egui` receives only IME events during composition from now on On Windows, β€œincompatible” events are filtered in `egui-winit`, aligning the behavior with other systems.
Screencasts Some Chinese IMEs on Windows: ![2026-03-13 12 25 37β€―AM](https://github.com/user-attachments/assets/064fd1c7-244b-4053-bd24-c65d768cd943) The default Japanese IMEs on Windows: ![2026-03-13 12 28 33β€―AM](https://github.com/user-attachments/assets/f799b0b5-350b-4b05-a769-bcef16255bdb)
The 2-set Korean IMEs handle arrow keys differently. It will be discussed in the next section. ### This PR Reverts #4794, because it introduced several bugs Some of its bugs have already been worked around in the past, but those workarounds might also be problematic. For example, #4912 is a workaround for a bug (#4908) introduced by #4794, and that workaround is in fact the root cause of the macOS backspacing bug I have worked around with #7810. (The reversion of #4912 is out of the scope of this PR, I will do that in #7983.) #### It Caused: Arrow keys are incorrectly blocked during typical Korean IME composition When composing Korean text using 2-Set IMEs, users should still be able to move the cursor with arrow keys regardless if the composition is committed. ##### Correct behavior
Screencasts macOS TextEdit: ![2026-03-12 8 04 15β€―PM](https://github.com/user-attachments/assets/24383568-f51c-4a74-9251-adfd942cad8f) Windows Notepad: ![2026-03-12 8 05 08β€―PM](https://github.com/user-attachments/assets/5a29a5b5-69b8-407b-b1a4-84fdb8f8847d) With #4794 reverted, `egui` also behaves correctly (tested on Linux + Wayland, macOS, and Windows): ![2026-03-12 8 03 51β€―PM](https://github.com/user-attachments/assets/fcb7f25c-1329-4eb1-82f2-1cea33dcca73)
##### Incorrect behavior caused by #4794 `remove_ime_incompatible_events` removed arrow-key events in such cases. As a result, the first arrow key press only commits the composition, and users need to press the arrow key again to move the cursor:
Screencast ![2026-03-12 8 06 40β€―PM](https://github.com/user-attachments/assets/6760c6bd-b6ce-44ea-b192-6bd165191c01)
This is essentially the same issue described here: https://github.com/emilk/egui/pull/7877#issuecomment-3852719948 #### It Caused: Backspacing leaves the last character in Korean IME pre-edit text not removed on macOS
Screencasts Before this PR: ![2026-03-17 10 48 12β€―PM](https://github.com/user-attachments/assets/88021e7e-caf6-4aa9-8f73-ecffc63cda06) After this PR: ![2026-03-17 10 47 23β€―PM](https://github.com/user-attachments/assets/379cd0db-24e0-4c0e-a5b4-edb37d3e1df7)
### Korean IMEs also use Enter to confirm Hanja selections, and will not work properly in the Korean β€œIME mode” proposed by #7898 (#7914)
Screencast: Korean IME using Enter and Space for confirmation (IBus Korean Hangul IME) The screencast below demonstrates that some Korean IMEs handle Hanja selection in a way similar to Japanese IMEs: the Up/Down arrow keys are used to navigate candidates, and Enter confirms the selected candidate. ![2026-03-13 6 39 17β€―AM](https://github.com/user-attachments/assets/0b054cc6-2251-4689-95a4-d69a9be36371)
Screencasts: Another example Using the built-in Korean IME on Windows, I type two lines: the first line in Hangul, and the second line as the same word converted to Hanja. Correct behavior in Notepad (reference): ![7914-ref](https://github.com/user-attachments/assets/1e9f9315-eb71-497a-b6e2-2b11eb6bbf7f) Behavior after applying this PR, which matches the Notepad behavior: ![7914-7967](https://github.com/user-attachments/assets/26f12b9f-9354-45b8-b2a8-ede28c34c5b1) Behavior after applying #7914 with the β€œIME mode” set to Korean (which is also the behavior before this PR being applied): ![7914-7914](https://github.com/user-attachments/assets/0f82a019-c491-4b64-a92a-d88d62dfbd84) On the second line, each time a Hanja character is confirmed, an unintended newline is inserted. This mirrors the Japanese IME issues that are supposed to be fixed by setting the β€œIME mode” to Japanese. (These Japanese IME issues are fixed in this PR as mentioned before.)
--- crates/egui-winit/src/lib.rs | 138 +++++++++++++++++-- crates/egui/src/data/input.rs | 4 + crates/egui/src/widgets/text_edit/builder.rs | 29 +--- 3 files changed, 131 insertions(+), 40 deletions(-) diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 234a9989b..90f0311d5 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -9,6 +9,9 @@ #![expect(clippy::manual_range_contains)] +#[cfg(target_os = "windows")] +use std::collections::HashSet; + #[cfg(feature = "accesskit")] pub use accesskit_winit; pub use egui; @@ -106,6 +109,12 @@ pub struct State { allow_ime: bool, ime_rect_px: Option, + + /// Used by [`State::try_on_ime_processed_keyboard_input`] to track key + /// release events that should be filtered out. See comments in that method + /// for details. + #[cfg(target_os = "windows")] + pressed_processed_physical_keys: HashSet, } impl State { @@ -148,6 +157,8 @@ impl State { allow_ime: false, ime_rect_px: None, + #[cfg(target_os = "windows")] + pressed_processed_physical_keys: HashSet::new(), }; slf.egui_input @@ -364,25 +375,33 @@ impl State { is_synthetic, .. } => { - // Winit generates fake "synthetic" KeyboardInput events when the focus - // is changed to the window, or away from it. Synthetic key presses - // represent no real key presses and should be ignored. - // See https://github.com/rust-windowing/winit/issues/3543 if *is_synthetic && event.state == ElementState::Pressed { + // Winit generates fake "synthetic" KeyboardInput events when the focus + // is changed to the window, or away from it. Synthetic key presses + // represent no real key presses and should be ignored. + // See https://github.com/rust-windowing/winit/issues/3543 EventResponse { repaint: true, consumed: false, } } else { - self.on_keyboard_input(event); + let egui_wants_keyboard_input = self.egui_ctx.egui_wants_keyboard_input(); - // When pressing the Tab key, egui focuses the first focusable element, hence Tab always consumes. - let consumed = self.egui_ctx.egui_wants_keyboard_input() - || event.logical_key - == winit::keyboard::Key::Named(winit::keyboard::NamedKey::Tab); - EventResponse { - repaint: true, - consumed, + if let Some(response) = + self.try_on_ime_processed_keyboard_input(event, egui_wants_keyboard_input) + { + response + } else { + self.on_keyboard_input(event); + + // When pressing the Tab key, egui focuses the first focusable element, hence Tab always consumes. + let consumed = egui_wants_keyboard_input + || event.logical_key + == winit::keyboard::Key::Named(winit::keyboard::NamedKey::Tab); + EventResponse { + repaint: true, + consumed, + } } } } @@ -526,6 +545,91 @@ impl State { } } + #[cfg(not(target_os = "windows"))] + #[expect(clippy::unused_self, clippy::needless_pass_by_ref_mut)] + #[inline(always)] + fn try_on_ime_processed_keyboard_input( + &mut self, + _event: &winit::event::KeyEvent, + _egui_wants_keyboard_input: bool, + ) -> Option { + // `KeyboardInput` events processed by the IME are not emitted by + // `winit` on non-Windows platforms, so we don't need to do anything + // here. + + None + } + + #[cfg(target_os = "windows")] + #[inline(always)] + fn try_on_ime_processed_keyboard_input( + &mut self, + event: &winit::event::KeyEvent, + egui_wants_keyboard_input: bool, + ) -> Option { + if !self.allow_ime { + None + } else if event.logical_key == winit::keyboard::NamedKey::Process { + // On Windows, the current version of `winit` (0.30.12) has a bug + // where `KeyboardInput` events processed by the IME are still + // emitted. [^1] + // + // As a workaround, we detect these events by checking whether their + // `logical_key` is `winit::keyboard::NamedKey::Process`, and filter + // them out to keep behavior consistent with other platforms. + // + // `winit::keyboard::NamedKey::Process` is not documented in + // `winit`. Reading through its source code, we find that it is + // mapped from `VK_PROCESSKEY` on Windows [^2]. (On an unrelated + // note, Web is the only other platform that also uses it [^3].) + // According to Microsoft, β€œthe IME sets the virtual key value + // to `VK_PROCESSKEY` after processing a key input message” [^4]. + // See also [^5]. + // (I can't find a documentation page dedicated to this value.) + // + // TODO(umajho): Remove this workaround once the `winit` bug is fixed + // and we've updated to a version that includes the fix. NOTE: Don't + // forget to also remove the `pressed_processed_physical_keys` field + // and its related code. + // + // [^1]: https://github.com/rust-windowing/winit/issues/4508 + // [^2]: https://github.com/rust-windowing/winit/blob/e9809ef54b18499bb4f2cac945719ecc2a61061b/src/platform_impl/windows/keyboard_layout.rs#L946 + // [^3]: https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values + // [^4]: https://learn.microsoft.com/en-us/windows/win32/api/imm/nf-imm-immgetvirtualkey#remarks + // [^5]: https://learn.microsoft.com/en-us/windows/win32/learnwin32/keyboard-input#character-messages + + self.pressed_processed_physical_keys + .insert(event.physical_key); + + Some(EventResponse { + repaint: false, + consumed: egui_wants_keyboard_input, + }) + } else if event.state == ElementState::Released + && self + .pressed_processed_physical_keys + .remove(&event.physical_key) + { + // Unlike key-presses, we can not tell whether a key-release event + // is processed by the IME or not by looking at its `logical_key`, + // because their `logical_key` is the original value (e.g. + // `winit::keyboard::Key::Character(…)`) rather than + // `winit::keyboard::Key::Named(winit::keyboard::NamedKey::Process)`. + // (See the screencast for Windows in [^1].) + // So we track the physical keys of processed key-presses and + // filter out the corresponding key-releases. + // + // [^1]: https://github.com/rust-windowing/winit/issues/4508 + + Some(EventResponse { + repaint: false, + consumed: egui_wants_keyboard_input, + }) + } else { + None + } + } + /// ## NOTE /// /// on Mac even Cmd-C is pressed during ime, a `c` is pushed to Preedit. @@ -1001,6 +1105,16 @@ impl State { let allow_ime = ime.is_some(); if self.allow_ime != allow_ime { self.allow_ime = allow_ime; + #[cfg(target_os = "windows")] + if !self.allow_ime { + // Defensively clear the set to avoid unexpected behavior. + // + // We don't do the same in `ime_event_disable` because the key + // release events for IME confirmation keys arrive after + // `winit::event::Ime::Disabled`. + self.pressed_processed_physical_keys.clear(); + } + profiling::scope!("set_ime_allowed"); window.set_ime_allowed(allow_ime); } diff --git a/crates/egui/src/data/input.rs b/crates/egui/src/data/input.rs index a52d40233..5e1680334 100644 --- a/crates/egui/src/data/input.rs +++ b/crates/egui/src/data/input.rs @@ -441,6 +441,10 @@ pub enum Event { Text(String), /// A key was pressed or released. + /// + /// ## Note for integration authors + /// + /// Key events that has been processed by IMEs should not be sent to `egui`. Key { /// Most of the time, it's the logical key, heeding the active keymap -- for instance, if the user has Dvorak /// keyboard layout, it will be taken into account. diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index a6c41e71c..1f103d2f8 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -993,13 +993,7 @@ fn events( let mut any_change = false; - let mut events = ui.input(|i| i.filtered_events(&event_filter)); - - if state.ime_enabled { - remove_ime_incompatible_events(&mut events); - // Process IME events first: - events.sort_by_key(|e| !matches!(e, Event::Ime(_))); - } + let events = ui.input(|i| i.filtered_events(&event_filter)); for event in &events { let did_mutate_text = match event { @@ -1232,27 +1226,6 @@ fn events( // ---------------------------------------------------------------------------- -fn remove_ime_incompatible_events(events: &mut Vec) { - // Remove key events which cause problems while 'IME' is being used. - // See https://github.com/emilk/egui/pull/4509 - events.retain(|event| { - !matches!( - event, - Event::Key { repeat: true, .. } - | Event::Key { - key: Key::Backspace - | Key::ArrowUp - | Key::ArrowDown - | Key::ArrowLeft - | Key::ArrowRight, - .. - } - ) - }); -} - -// ---------------------------------------------------------------------------- - /// Returns `Some(new_cursor)` if we did mutate `text`. fn check_for_mutating_key_press( os: OperatingSystem, From d985bf9b83fb69b7028152ddaa6ee9ec993eb4d9 Mon Sep 17 00:00:00 2001 From: Andreas Reich Date: Tue, 24 Mar 2026 12:37:33 +0100 Subject: [PATCH 45/58] Fill in DisplayHandle automatically on web painter just like it's done on winit (#8006) * Closes https://github.com/emilk/egui/issues/8001 * Follow-up to https://github.com/emilk/egui/pull/7990 Also simplified/shortened the comments around display handle a bit. Lots a repetition there made it hard to upgrade otherwise. --- crates/eframe/src/web/web_painter_wgpu.rs | 36 ++++++++- crates/egui-wgpu/src/setup.rs | 91 ++++++++--------------- 2 files changed, 64 insertions(+), 63 deletions(-) diff --git a/crates/eframe/src/web/web_painter_wgpu.rs b/crates/eframe/src/web/web_painter_wgpu.rs index ebce9d981..63702592d 100644 --- a/crates/eframe/src/web/web_painter_wgpu.rs +++ b/crates/eframe/src/web/web_painter_wgpu.rs @@ -25,6 +25,24 @@ pub(crate) struct WebPainterWgpu { needs_reconfigure: bool, } +/// Owned web display handle that is `Send + Sync`. +/// +/// `DisplayHandle` from `raw-window-handle` is `!Send`/`!Sync` because the enum +/// contains platform variants with raw pointers. On web the handle is always empty, +/// so this wrapper is safe. +#[cfg(target_arch = "wasm32")] +#[derive(Clone, Debug)] +struct WebDisplay; + +#[cfg(target_arch = "wasm32")] +impl egui_wgpu::wgpu::rwh::HasDisplayHandle for WebDisplay { + fn display_handle( + &self, + ) -> Result, egui_wgpu::wgpu::rwh::HandleError> { + Ok(egui_wgpu::wgpu::rwh::DisplayHandle::web()) + } +} + impl WebPainterWgpu { pub fn render_state(&self) -> Option { self.render_state.clone() @@ -64,7 +82,17 @@ impl WebPainterWgpu { ) -> Result { log::debug!("Creating wgpu painter"); - let instance = options.wgpu_options.wgpu_setup.new_instance().await; + // Inject the display handle into the wgpu setup so that wgpu can create surfaces on WebGL. + let mut wgpu_options = options.wgpu_options.clone(); + if let egui_wgpu::WgpuSetup::CreateNew(ref mut create_new) = wgpu_options.wgpu_setup + && create_new.display_handle.is_none() + { + // Force WebGL, useful for quick & dirty testing: + //create_new.instance_descriptor.backends = wgpu::Backends::GL; + create_new.display_handle = Some(Box::new(WebDisplay)); + } + + let instance = wgpu_options.wgpu_setup.new_instance().await; let surface = instance .create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone())) .map_err(|err| format!("failed to create wgpu surface: {err}"))?; @@ -72,7 +100,7 @@ impl WebPainterWgpu { let depth_stencil_format = egui_wgpu::depth_format_from_bits(options.depth_buffer, 0); let render_state = RenderState::create( - &options.wgpu_options, + &wgpu_options, &instance, Some(&surface), egui_wgpu::RendererOptions { @@ -90,7 +118,7 @@ impl WebPainterWgpu { let surface_configuration = wgpu::SurfaceConfiguration { format: render_state.target_format, - present_mode: options.wgpu_options.present_mode, + present_mode: wgpu_options.present_mode, view_formats: vec![render_state.target_format], ..default_configuration }; @@ -106,7 +134,7 @@ impl WebPainterWgpu { surface_configuration, depth_stencil_format, depth_texture_view: None, - on_surface_status: Arc::clone(&options.wgpu_options.on_surface_status) as _, + on_surface_status: Arc::clone(&wgpu_options.on_surface_status) as _, screen_capture_state: None, capture_tx, capture_rx, diff --git a/crates/egui-wgpu/src/setup.rs b/crates/egui-wgpu/src/setup.rs index 9d83d4380..c5b3f0421 100644 --- a/crates/egui-wgpu/src/setup.rs +++ b/crates/egui-wgpu/src/setup.rs @@ -2,24 +2,19 @@ use std::sync::Arc; /// A cloneable display handle for use with [`wgpu::InstanceDescriptor`]. /// -/// This trait exists so that a [`winit::event_loop::OwnedDisplayHandle`] (or similar platform -/// display handle) can be stored, cloned, and later passed to wgpu. +/// [`wgpu::InstanceDescriptor`] stores its display handle as a non-cloneable +/// `Box`. This trait wraps it so it can be cloned +/// alongside the rest of the egui wgpu configuration. /// -/// wgpu requires an explicit display handle for GLES on some platforms (notably Wayland). -/// Because [`wgpu::InstanceDescriptor`] contains a `Box` which is -/// not cloneable, we wrap the handle in this trait so it can be cloned alongside the rest of -/// the egui wgpu configuration. -/// -/// This is automatically implemented for all types that satisfy the bounds (including -/// [`winit::event_loop::OwnedDisplayHandle`]). +/// Automatically implemented for all types that satisfy the bounds +/// (including [`winit::event_loop::OwnedDisplayHandle`]). pub trait EguiDisplayHandle: wgpu::rwh::HasDisplayHandle + std::fmt::Debug + Send + Sync + 'static { - /// Clone this handle into a `Box` suitable for setting on - /// [`wgpu::InstanceDescriptor::display`]. + /// Clone into a `Box` for [`wgpu::InstanceDescriptor::display`]. fn clone_for_wgpu(&self) -> Box; - /// Clone this handle into a new `Box`. + /// Clone into a new `Box`. fn clone_display_handle(&self) -> Box; } @@ -68,27 +63,14 @@ pub enum WgpuSetup { impl WgpuSetup { /// Creates a new [`WgpuSetup::CreateNew`] with the given display handle. /// - /// This is the recommended constructor. Most platforms (Windows, macOS/iOS, Android, web) - /// work fine without a display handle, but some (e.g. Wayland on Linux with GLES) require - /// one. Providing it unconditionally ensures your app works everywhere. - /// - /// If you don't have a display handle available, use [`Self::without_display_handle`] - /// instead β€” it will still work on the majority of platforms. - /// - /// With winit, pass [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle). + /// See [`WgpuSetupCreateNew::from_display_handle`] for details. pub fn from_display_handle(display_handle: impl EguiDisplayHandle) -> Self { Self::CreateNew(WgpuSetupCreateNew::from_display_handle(display_handle)) } /// Creates a new [`WgpuSetup::CreateNew`] without a display handle. /// - /// A display handle is not required for headless operation (offscreen rendering, tests, - /// compute-only workloads). It also isn't needed on most platforms even when presenting - /// to a window β€” only some configurations (e.g. Wayland on Linux with GLES) require one. - /// - /// If you do have a display handle available, prefer [`Self::from_display_handle`] for - /// maximum compatibility. With winit you can obtain one via - /// [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle). + /// See [`WgpuSetupCreateNew::without_display_handle`] for details. pub fn without_display_handle() -> Self { Self::CreateNew(WgpuSetupCreateNew::without_display_handle()) } @@ -175,44 +157,32 @@ pub type NativeAdapterSelectorMethod = Arc< /// /// Used for [`WgpuSetup::CreateNew`]. /// -/// Use [`Self::from_display_handle`] when you have a display handle available β€” this is the -/// recommended constructor. With winit you can obtain one via -/// [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle). -/// Most platforms (Windows, macOS/iOS, Android, web) work fine without one, but some -/// (e.g. Wayland on Linux with GLES) require it. Providing it unconditionally ensures your -/// app works everywhere. +/// Prefer [`Self::from_display_handle`] when you have a display handle available. +/// Most platforms work without one, but some (e.g. Wayland with GLES, or WebGL) +/// require it, so providing one ensures maximum compatibility. +/// With winit, pass [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle). /// -/// If you don't have a display handle, use [`Self::without_display_handle`] β€” it will still -/// work on the majority of platforms, and is appropriate for headless rendering, tests, or -/// web targets. -/// -/// Note: The [`wgpu::InstanceDescriptor::display`] field is always stored as `None` in -/// [`Self::instance_descriptor`]. The display handle is stored separately so it can be cloned -/// (since [`wgpu::InstanceDescriptor`] itself does not implement `Clone`), and is injected -/// into the descriptor at instance creation time. +/// Note: The display handle is stored in [`Self::display_handle`] rather than in +/// [`Self::instance_descriptor`] so the config can be cloned +/// ([`wgpu::InstanceDescriptor`] is not `Clone`). It is injected at instance creation time. pub struct WgpuSetupCreateNew { - /// Instance descriptor for creating a wgpu instance. + /// Descriptor for the wgpu instance. /// - /// The [`wgpu::InstanceDescriptor::display`] field should be left as `None`; use the - /// [`Self::display_handle`] field instead (it will be injected when the instance is created). + /// Leave [`wgpu::InstanceDescriptor::display`] as `None` β€” use [`Self::display_handle`] + /// instead (injected at instance creation time). /// - /// The most important field is [`wgpu::InstanceDescriptor::backends`], which - /// controls which backends are supported (wgpu will pick one of these). - /// If you only want to support WebGL (and not WebGPU), - /// you can set this to [`wgpu::Backends::GL`]. - /// By default on web, WebGPU will be used if available. - /// WebGL will only be used as a fallback, - /// and only if you have enabled the `webgl` feature of crate `wgpu`. + /// The most important field is [`wgpu::InstanceDescriptor::backends`], which controls + /// which backends are supported (wgpu will pick one of these). For example, set it to + /// [`wgpu::Backends::GL`] to use only WebGL. By default on web, WebGPU is preferred + /// with WebGL as a fallback (requires the `webgl` feature of crate `wgpu`). pub instance_descriptor: wgpu::InstanceDescriptor, - /// The display handle to pass to wgpu when creating the instance. + /// Display handle passed to wgpu at instance creation time. /// - /// Most platforms (Windows, macOS/iOS, Android, web) work without this, but some - /// (e.g. Wayland on Linux with GLES) require it. If you have a display handle - /// available, providing it ensures maximum compatibility. + /// Required on some platforms (e.g. Wayland with GLES, WebGL); optional elsewhere. + /// With winit, use [`winit::event_loop::OwnedDisplayHandle`]. /// - /// When using winit, this is typically the - /// [`winit::event_loop::OwnedDisplayHandle`] obtained from the event loop. + /// `eframe` 's winit & web integrations will attempt to fill the display handle automatically if it is left empty. pub display_handle: Option>, /// Power preference for the adapter if [`Self::native_adapter_selector`] is not set or targeting web. @@ -258,8 +228,11 @@ impl WgpuSetupCreateNew { /// to a window β€” only some configurations (e.g. Wayland on Linux with GLES) require one. /// /// If you do have a display handle available, prefer [`Self::from_display_handle`] for - /// maximum compatibility. With winit you can obtain one via - /// [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle). + /// maximum compatibility. + /// + /// With winit you can obtain one via [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle). + /// + /// `eframe` 's winit & web integrations will attempt to fill the display handle automatically if it is left empty. pub fn without_display_handle() -> Self { Self { instance_descriptor: wgpu::InstanceDescriptor { From 3a2d437bd71641862ab49274de57ec59837474bd Mon Sep 17 00:00:00 2001 From: Deuracell Date: Tue, 24 Mar 2026 12:50:20 +0000 Subject: [PATCH 46/58] Add `DatePickerButton::reverse_years/year_scroll_to` (#7978) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Two new builder methods on `DatePickerButton`: - **`reverse_years(bool)`** β€” lists years in descending order (newest first). Useful when users are more likely to pick recent years. - **`year_scroll_to(i32)`** β€” scrolls the year dropdown to a specific year when it first opens, centred in the list. Defaults to the currently selected year, so the picker no longer opens at the top of a 110-item list. ## Why The year `ComboBox` currently always renders in ascending order and opens scrolled to the top. For a range spanning e.g. 1925–2035, the current year is buried near the bottom. Users have to scroll past ~100 entries every time they open the picker. ## Notes - Both options are purely additive builder methods β€” no breaking changes. - `year_scroll_needed` is a persisted state flag that is set on popup open and cleared after the first scroll, so the user can freely scroll the list after that. - Existing behaviour is unchanged when neither method is called. Co-authored-by: Simon Deurell --- crates/egui_extras/src/datepicker/button.rs | 22 +++++++++++- crates/egui_extras/src/datepicker/popup.rs | 38 +++++++++++++++------ 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/crates/egui_extras/src/datepicker/button.rs b/crates/egui_extras/src/datepicker/button.rs index 98aceefe2..d6f69fe77 100644 --- a/crates/egui_extras/src/datepicker/button.rs +++ b/crates/egui_extras/src/datepicker/button.rs @@ -21,6 +21,8 @@ pub struct DatePickerButton<'a> { format: String, highlight_weekends: bool, start_end_years: Option>, + reverse_years: bool, + year_scroll_to: Option, } impl<'a> DatePickerButton<'a> { @@ -36,6 +38,8 @@ impl<'a> DatePickerButton<'a> { format: "%Y-%m-%d".to_owned(), highlight_weekends: true, start_end_years: None, + reverse_years: false, + year_scroll_to: None, } } @@ -115,6 +119,21 @@ impl<'a> DatePickerButton<'a> { self.start_end_years = Some(start_end_years); self } + + /// List years in descending order in the year dropdown. (Default: false) + #[inline] + pub fn reverse_years(mut self, reverse_years: bool) -> Self { + self.reverse_years = reverse_years; + self + } + + /// Scroll the year dropdown to this year when the picker first opens. + /// Defaults to the currently selected year. + #[inline] + pub fn year_scroll_to(mut self, year: i32) -> Self { + self.year_scroll_to = Some(year); + self + } } impl Widget for DatePickerButton<'_> { @@ -154,7 +173,6 @@ impl Widget for DatePickerButton<'_> { pos.x = button_response.rect.right() - width_with_padding; } - // Check to make sure the calendar never is displayed out of window pos.x = pos.x.max(ui.style().spacing.window_margin.leftf()); //TODO(elwerene): Better positioning @@ -182,6 +200,8 @@ impl Widget for DatePickerButton<'_> { calendar_week: self.calendar_week, highlight_weekends: self.highlight_weekends, start_end_years: self.start_end_years, + reverse_years: self.reverse_years, + year_scroll_to: self.year_scroll_to, } .draw(ui) }) diff --git a/crates/egui_extras/src/datepicker/popup.rs b/crates/egui_extras/src/datepicker/popup.rs index d353307b3..1c24ca81d 100644 --- a/crates/egui_extras/src/datepicker/popup.rs +++ b/crates/egui_extras/src/datepicker/popup.rs @@ -13,6 +13,7 @@ struct DatePickerPopupState { month: u32, day: u32, setup: bool, + year_scroll_needed: bool, } impl DatePickerPopupState { @@ -36,6 +37,8 @@ pub(crate) struct DatePickerPopup<'a> { pub calendar_week: bool, pub highlight_weekends: bool, pub start_end_years: Option>, + pub reverse_years: bool, + pub year_scroll_to: Option, } impl DatePickerPopup<'_> { @@ -51,6 +54,7 @@ impl DatePickerPopup<'_> { popup_state.month = self.selection.month(); popup_state.day = self.selection.day(); popup_state.setup = true; + popup_state.year_scroll_needed = true; ui.data_mut(|data| data.insert_persisted(id, popup_state.clone())); } @@ -60,7 +64,7 @@ impl DatePickerPopup<'_> { let spacing = 2.0; ui.spacing_mut().item_spacing = Vec2::splat(spacing); - ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); // Don't wrap any text + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); StripBuilder::new(ui) .clip(false) @@ -89,15 +93,30 @@ impl DatePickerPopup<'_> { Some(range) => (*range.start(), *range.end()), None => (today.year() - 100, today.year() + 10), }; - for year in start_year..=end_year { - if ui - .selectable_value( - &mut popup_state.year, - year, - year.to_string(), - ) - .changed() + let scroll_to_year = + self.year_scroll_to.unwrap_or(popup_state.year); + let years: Vec = if self.reverse_years { + (start_year..=end_year).rev().collect() + } else { + (start_year..=end_year).collect() + }; + for year in years { + let resp = ui.selectable_value( + &mut popup_state.year, + year, + year.to_string(), + ); + if popup_state.year_scroll_needed + && year == scroll_to_year { + resp.scroll_to_me(Some(Align::Center)); + popup_state.year_scroll_needed = false; + ui.memory_mut(|mem| { + mem.data + .insert_persisted(id, popup_state.clone()); + }); + } + if resp.changed() { popup_state.day = popup_state .day .min(popup_state.last_day_of_month()); @@ -349,7 +368,6 @@ impl DatePickerPopup<'_> { ); if day == today { - // Encircle today's date let stroke = ui .visuals() .widgets From 1e4619c5ef93dee9cf0dd9777a6d4ab20272ba1a Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 24 Mar 2026 13:55:28 +0100 Subject: [PATCH 47/58] Explain that we shouldn't update wasm-bindgen version willy-nilly --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d042c2349..4c8a52626 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -138,8 +138,8 @@ type-map = "0.5.1" unicode_names2 = { version = "2.0.0", default-features = false } unicode-segmentation = "1.12.0" vello_cpu = { version = "0.0.6", default-features = false, features = ["std", "u8_pipeline", "f32_pipeline"] } -wasm-bindgen = "0.2.108" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml -wasm-bindgen-futures = "0.4.0" +wasm-bindgen = "0.2.108" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml. Don't update this spuriously, because of https://github.com/rerun-io/rerun/issues/8766 +wasm-bindgen-futures = "0.4.58" wayland-cursor = { version = "0.31.11", default-features = false } web-sys = "0.3.77" web-time = "1.1.0" # Timekeeping for native and web From 5d5f0dedccc7ec170c3cc51c5f7ba0990549c984 Mon Sep 17 00:00:00 2001 From: Ryan Bluth Date: Tue, 24 Mar 2026 08:58:02 -0400 Subject: [PATCH 48/58] Allow rotation of rectangles and ellipses (#7682) Added the ability to rotate rectangles and ellipses. Similar to the existing text implementation * [x ] I have followed the instructions in the PR template --- .../src/demo/tests/tessellation_test.rs | 1 + crates/epaint/src/shape_transform.rs | 2 + crates/epaint/src/shapes/ellipse_shape.rs | 30 ++++- crates/epaint/src/shapes/rect_shape.rs | 37 +++++- crates/epaint/src/tessellator.rs | 20 +++ .../tests/snapshots/rotated_ellipse.png | 3 + .../tests/snapshots/rotated_rect.png | 3 + tests/egui_tests/tests/test_rotation.rs | 117 ++++++++++++++++++ 8 files changed, 208 insertions(+), 5 deletions(-) create mode 100644 tests/egui_tests/tests/snapshots/rotated_ellipse.png create mode 100644 tests/egui_tests/tests/snapshots/rotated_rect.png create mode 100644 tests/egui_tests/tests/test_rotation.rs diff --git a/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs b/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs index 3bb814889..abe3280eb 100644 --- a/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs +++ b/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs @@ -297,6 +297,7 @@ fn rect_shape_ui(ui: &mut egui::Ui, shape: &mut RectShape) { blur_width, round_to_pixels, brush: _, + angle: _, } = shape; let round_to_pixels = round_to_pixels.get_or_insert(true); diff --git a/crates/epaint/src/shape_transform.rs b/crates/epaint/src/shape_transform.rs index 8fce01f64..71cc1332e 100644 --- a/crates/epaint/src/shape_transform.rs +++ b/crates/epaint/src/shape_transform.rs @@ -57,6 +57,7 @@ pub fn adjust_colors( radius: _, fill, stroke, + angle: _, }) | Shape::Rect(RectShape { rect: _, @@ -67,6 +68,7 @@ pub fn adjust_colors( round_to_pixels: _, blur_width: _, brush: _, + angle: _, }) => { adjust_color(fill); adjust_color(&mut stroke.color); diff --git a/crates/epaint/src/shapes/ellipse_shape.rs b/crates/epaint/src/shapes/ellipse_shape.rs index 310638d0f..b436eb841 100644 --- a/crates/epaint/src/shapes/ellipse_shape.rs +++ b/crates/epaint/src/shapes/ellipse_shape.rs @@ -10,6 +10,9 @@ pub struct EllipseShape { pub radius: Vec2, pub fill: Color32, pub stroke: Stroke, + + /// Rotate ellipse by this many radians clockwise around its center. + pub angle: f32, } impl EllipseShape { @@ -20,6 +23,7 @@ impl EllipseShape { radius, fill: fill_color.into(), stroke: Default::default(), + angle: 0.0, } } @@ -30,18 +34,38 @@ impl EllipseShape { radius, fill: Default::default(), stroke: stroke.into(), + angle: 0.0, } } + /// Set the rotation of the ellipse (in radians, clockwise). + /// The ellipse rotates around its center. + #[inline] + pub fn with_angle(mut self, angle: f32) -> Self { + self.angle = angle; + self + } + + /// Set the rotation of the ellipse (in radians, clockwise) around a custom pivot point. + #[inline] + pub fn with_angle_and_pivot(mut self, angle: f32, pivot: Pos2) -> Self { + self.angle = angle; + let rot = emath::Rot2::from_angle(angle); + self.center = pivot + rot * (self.center - pivot); + self + } + /// The visual bounding rectangle (includes stroke width) pub fn visual_bounding_rect(&self) -> Rect { if self.fill == Color32::TRANSPARENT && self.stroke.is_empty() { Rect::NOTHING } else { - Rect::from_center_size( - self.center, + let rect = Rect::from_center_size( + Pos2::ZERO, self.radius * 2.0 + Vec2::splat(self.stroke.width), - ) + ); + rect.rotate_bb(emath::Rot2::from_angle(self.angle)) + .translate(self.center.to_vec2()) } } } diff --git a/crates/epaint/src/shapes/rect_shape.rs b/crates/epaint/src/shapes/rect_shape.rs index 2e855d369..e0c528377 100644 --- a/crates/epaint/src/shapes/rect_shape.rs +++ b/crates/epaint/src/shapes/rect_shape.rs @@ -54,13 +54,16 @@ pub struct RectShape { /// Since most rectangles do not have a texture, this is optional and in an `Arc`, /// so that [`RectShape`] is kept small.. pub brush: Option>, + + /// Rotate rectangle by this many radians clockwise around its center. + pub angle: f32, } #[test] fn rect_shape_size() { assert_eq!( std::mem::size_of::(), - 48, + 56, "RectShape changed size! If it shrank - good! Update this test. If it grew - bad! Try to find a way to avoid it." ); assert!( @@ -88,6 +91,7 @@ impl RectShape { round_to_pixels: None, blur_width: 0.0, brush: Default::default(), + angle: 0.0, } } @@ -157,6 +161,25 @@ impl RectShape { self } + /// Set the rotation of the rectangle (in radians, clockwise). + /// The rectangle rotates around its center. + #[inline] + pub fn with_angle(mut self, angle: f32) -> Self { + self.angle = angle; + self + } + + /// Set the rotation of the rectangle (in radians, clockwise) around a custom pivot point. + #[inline] + pub fn with_angle_and_pivot(mut self, angle: f32, pivot: Pos2) -> Self { + self.angle = angle; + let rot = emath::Rot2::from_angle(angle); + let center = self.rect.center(); + let new_center = pivot + rot * (center - pivot); + self.rect = self.rect.translate(new_center - center); + self + } + /// The visual bounding rectangle (includes stroke width) #[inline] pub fn visual_bounding_rect(&self) -> Rect { @@ -168,7 +191,17 @@ impl RectShape { StrokeKind::Middle => self.stroke.width / 2.0, StrokeKind::Outside => self.stroke.width, }; - self.rect.expand(expand + self.blur_width / 2.0) + let expanded = self.rect.expand(expand + self.blur_width / 2.0); + if self.angle == 0.0 { + expanded + } else { + // Rotate around the rectangle's center and compute bounding box + let center = self.rect.center(); + let rect_relative = Rect::from_center_size(Pos2::ZERO, expanded.size()); + rect_relative + .rotate_bb(emath::Rot2::from_angle(self.angle)) + .translate(center.to_vec2()) + } } } diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 9529765ac..9256ae16e 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1546,6 +1546,7 @@ impl Tessellator { radius, fill, stroke, + angle, } = shape; if radius.x <= 0.0 || radius.y <= 0.0 { @@ -1596,6 +1597,14 @@ impl Tessellator { points.push(center + Vec2::new(0.0, -radius.y)); points.extend(quarter.iter().rev().map(|p| center + Vec2::new(p.x, -p.y))); + // Apply rotation if angle is non-zero + if angle != 0.0 { + let rot = emath::Rot2::from_angle(angle); + for point in &mut points { + *point = center + rot * (*point - center); + } + } + let path_stroke = PathStroke::from(stroke).outside(); self.scratchpad_path.clear(); self.scratchpad_path.add_line_loop(&points); @@ -1773,6 +1782,7 @@ impl Tessellator { round_to_pixels, mut blur_width, brush: _, // brush is extracted on its own, because it is not Copy + angle, } = *rect_shape; let mut corner_radius = CornerRadiusF32::from(corner_radius); @@ -1940,6 +1950,16 @@ impl Tessellator { let path = &mut self.scratchpad_path; path.clear(); path::rounded_rectangle(&mut self.scratchpad_points, rect, corner_radius); + + // Apply rotation if angle is non-zero + if angle != 0.0 { + let rot = emath::Rot2::from_angle(angle); + let center = rect.center(); + for point in &mut self.scratchpad_points { + *point = center + rot * (*point - center); + } + } + path.add_line_loop(&self.scratchpad_points); let path_stroke = PathStroke::from(stroke).with_kind(stroke_kind); diff --git a/tests/egui_tests/tests/snapshots/rotated_ellipse.png b/tests/egui_tests/tests/snapshots/rotated_ellipse.png new file mode 100644 index 000000000..e32f7864c --- /dev/null +++ b/tests/egui_tests/tests/snapshots/rotated_ellipse.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e8f222733524b21969834a9ccc15aa9b0a4deb1d41e1086c80750f7cdd9711c8 +size 17324 diff --git a/tests/egui_tests/tests/snapshots/rotated_rect.png b/tests/egui_tests/tests/snapshots/rotated_rect.png new file mode 100644 index 000000000..52255aa7f --- /dev/null +++ b/tests/egui_tests/tests/snapshots/rotated_rect.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb4f1d10aa664e04da4b2e38c52cb6516a4c43a98884c9223e15266ea28ccd3d +size 14191 diff --git a/tests/egui_tests/tests/test_rotation.rs b/tests/egui_tests/tests/test_rotation.rs new file mode 100644 index 000000000..03c9a8664 --- /dev/null +++ b/tests/egui_tests/tests/test_rotation.rs @@ -0,0 +1,117 @@ +use egui::epaint::{EllipseShape, RectShape, StrokeKind}; +use egui::{Color32, Grid, Pos2, Rect, Shape, Stroke, Vec2}; +use egui_kittest::Harness; + +const SHAPE_COLOR: Color32 = Color32::from_rgb(255, 165, 0); +const GHOST_COLOR: Color32 = Color32::from_rgb(0, 255, 255); +const PIVOT_COLOR: Color32 = Color32::from_rgb(255, 0, 255); +const CELL_SIZE: Vec2 = Vec2::new(180.0, 180.0); + +#[test] +fn rotated_rect() { + let shape_stroke = Stroke::new(2.0, Color32::BLACK); + let ghost_stroke = Stroke::new(1.0, GHOST_COLOR); + + let mut harness = Harness::new_ui(|ui| { + ui.ctx().set_pixels_per_point(1.0); + + let rect_size = Vec2::new(100.0, 60.0); + let cell_center = Pos2::new(90.0, 90.0); + let cell_rect = Rect::from_center_size(cell_center, rect_size); + + Grid::new("rotated_rect_grid") + .spacing(Vec2::new(30.0, 30.0)) + .show(ui, |ui| { + for (label, angle, pivot) in [ + ("0Β°", 0.0, None), + ("Center 45Β°", 45.0f32.to_radians(), None), + ( + "Top-Left 45Β°", + 45.0f32.to_radians(), + Some(cell_rect.left_top()), + ), + ] { + paint_case(ui, label, |offset| { + let rect = cell_rect.translate(offset); + let pivot = pivot.map(|p| p + offset); + let pivot_pos = pivot.unwrap_or_else(|| rect.center()); + + let ghost = RectShape::stroke(rect, 0.0, ghost_stroke, StrokeKind::Outside); + let shape = RectShape::new( + rect, + 0.0, + SHAPE_COLOR, + shape_stroke, + StrokeKind::Outside, + ) + .with_angle_and_pivot(angle, pivot_pos); + + (ghost.into(), shape.into(), pivot_pos) + }); + } + }); + }); + + harness.fit_contents(); + harness.try_snapshot("rotated_rect").unwrap(); +} + +#[test] +fn rotated_ellipse() { + let shape_stroke = Stroke::new(2.0, Color32::BLACK); + let ghost_stroke = Stroke::new(1.0, GHOST_COLOR); + + let mut harness = Harness::new_ui(|ui| { + ui.ctx().set_pixels_per_point(1.0); + + let rect_size = Vec2::new(100.0, 60.0); + let cell_center = Pos2::new(90.0, 90.0); + let radius = rect_size / 2.0; + + Grid::new("rotated_ellipse_grid") + .spacing(Vec2::new(30.0, 30.0)) + .show(ui, |ui| { + for (label, angle, pivot) in [ + ("0Β°", 0.0, None), + ("Center 45Β°", 45.0f32.to_radians(), None), + ( + "Top-Left 45Β°", + 45.0f32.to_radians(), + Some(cell_center - radius), + ), + ] { + paint_case(ui, label, |offset| { + let center = cell_center + offset; + let pivot = pivot.map(|p| p + offset); + let pivot_pos = pivot.unwrap_or(center); + + let ghost = EllipseShape::stroke(center, radius, ghost_stroke); + let mut shape = EllipseShape::filled(center, radius, SHAPE_COLOR); + shape.stroke = shape_stroke; + let shape = shape.with_angle_and_pivot(angle, pivot_pos); + + (ghost.into(), shape.into(), pivot_pos) + }); + } + }); + }); + + harness.fit_contents(); + harness.try_snapshot("rotated_ellipse").unwrap(); +} + +fn paint_case(ui: &mut egui::Ui, label: &str, make_shapes: F) +where + F: FnOnce(Vec2) -> (Shape, Shape, Pos2), +{ + ui.vertical(|ui| { + ui.label(label); + let (response, painter) = ui.allocate_painter(CELL_SIZE, egui::Sense::hover()); + let offset = response.rect.min.to_vec2(); + + let (ghost, shape, pivot) = make_shapes(offset); + painter.add(ghost); + painter.add(shape); + painter.circle_filled(pivot, 3.0, PIVOT_COLOR); + }); +} From a12d18d9bdf79afcb669908d1c6119b1816f440c Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 24 Mar 2026 13:58:21 +0100 Subject: [PATCH 49/58] Replace `chrono` with `jiff` (#8008) `jiff` is more modern, and seem to be where the ecosystem is heading. --- Cargo.lock | 103 ++++-------------- Cargo.toml | 2 +- crates/egui_demo_app/Cargo.toml | 4 +- crates/egui_demo_app/src/lib.rs | 7 +- crates/egui_demo_lib/Cargo.toml | 4 +- .../egui_demo_lib/src/demo/widget_gallery.rs | 26 ++--- crates/egui_extras/Cargo.toml | 4 +- crates/egui_extras/src/datepicker/button.rs | 20 ++-- crates/egui_extras/src/datepicker/mod.rs | 20 ++-- crates/egui_extras/src/datepicker/popup.rs | 47 ++++---- crates/egui_extras/src/lib.rs | 4 +- examples/hello_android/Cargo.toml | 2 +- 12 files changed, 88 insertions(+), 155 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 152d178b9..a15a00a02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,7 +96,7 @@ dependencies = [ "hashbrown 0.16.1", "static_assertions", "windows", - "windows-core 0.62.2", + "windows-core", ] [[package]] @@ -732,19 +732,6 @@ dependencies = [ "libc", ] -[[package]] -name = "chrono" -version = "0.4.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-link 0.2.1", -] - [[package]] name = "ciborium" version = "0.2.2" @@ -1323,7 +1310,6 @@ dependencies = [ "accesskit", "accesskit_consumer", "bytemuck", - "chrono", "eframe", "egui", "egui_demo_lib", @@ -1332,6 +1318,7 @@ dependencies = [ "ehttp", "env_logger", "image", + "jiff", "log", "mimalloc", "poll-promise", @@ -1350,13 +1337,13 @@ dependencies = [ name = "egui_demo_lib" version = "0.33.3" dependencies = [ - "chrono", "criterion", "document-features", "egui", "egui_extras", "egui_kittest", "image", + "jiff", "mimalloc", "rand 0.9.2", "serde", @@ -1368,12 +1355,12 @@ name = "egui_extras" version = "0.33.3" dependencies = [ "ahash", - "chrono", "document-features", "egui", "ehttp", "enum-map", "image", + "jiff", "log", "mime_guess2", "profiling", @@ -2151,30 +2138,6 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" -[[package]] -name = "iana-time-zone" -version = "0.1.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core 0.61.2", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - [[package]] name = "icu_collections" version = "2.0.0" @@ -2387,22 +2350,25 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.15" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" dependencies = [ "jiff-static", + "js-sys", "log", "portable-atomic", "portable-atomic-util", - "serde", + "serde_core", + "wasm-bindgen", + "windows-sys 0.61.2", ] [[package]] name = "jiff-static" -version = "0.2.15" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" dependencies = [ "proc-macro2", "quote", @@ -5282,7 +5248,7 @@ dependencies = [ "wgpu-naga-bridge", "wgpu-types", "windows", - "windows-core 0.62.2", + "windows-core", ] [[package]] @@ -5347,7 +5313,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" dependencies = [ "windows-collections", - "windows-core 0.62.2", + "windows-core", "windows-future", "windows-numerics", ] @@ -5358,20 +5324,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" dependencies = [ - "windows-core 0.62.2", -] - -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", + "windows-core", ] [[package]] @@ -5383,8 +5336,8 @@ dependencies = [ "windows-implement", "windows-interface", "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", + "windows-result", + "windows-strings", ] [[package]] @@ -5393,7 +5346,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" dependencies = [ - "windows-core 0.62.2", + "windows-core", "windows-link 0.2.1", "windows-threading", ] @@ -5438,19 +5391,10 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" dependencies = [ - "windows-core 0.62.2", + "windows-core", "windows-link 0.2.1", ] -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link 0.1.3", -] - [[package]] name = "windows-result" version = "0.4.1" @@ -5460,15 +5404,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link 0.1.3", -] - [[package]] name = "windows-strings" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index 4c8a52626..6978e4df8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,7 +80,6 @@ arboard = { version = "3.6.1", default-features = false } backtrace = "0.3.76" bitflags = "2.9.4" bytemuck = "1.24.0" -chrono = { version = "0.4.42", default-features = false } cint = "0.3.1" color-hex = "0.2.0" criterion = { version = "0.7.0", default-features = false } @@ -96,6 +95,7 @@ glutin = { version = "0.32.3", default-features = false } glutin-winit = { version = "0.5.0", default-features = false } home = "0.5.9" image = { version = "0.25.6", default-features = false } +jiff = { version = "0.2.23", default-features = false } js-sys = "0.3.77" kittest = { version = "0.4.0" } log = { version = "0.4.28", features = ["std"] } diff --git a/crates/egui_demo_app/Cargo.toml b/crates/egui_demo_app/Cargo.toml index b23ea9cbb..49609746f 100644 --- a/crates/egui_demo_app/Cargo.toml +++ b/crates/egui_demo_app/Cargo.toml @@ -42,15 +42,15 @@ wayland = ["eframe/wayland"] x11 = ["eframe/x11"] [dependencies] -chrono = { workspace = true, features = ["js-sys", "wasmbind"] } eframe = { workspace = true, default-features = false, features = ["web_screen_reader"] } egui = { workspace = true, features = ["callstack", "default"] } -egui_demo_lib = { workspace = true, features = ["default", "chrono"] } +egui_demo_lib = { workspace = true, features = ["default", "jiff"] } egui_extras = { workspace = true, features = ["default", "image"] } image = { workspace = true, default-features = false, features = [ # Ensure we can display the test images "png", ] } +jiff = { workspace = true, features = ["std", "tz-system", "js"] } log.workspace = true profiling.workspace = true diff --git a/crates/egui_demo_app/src/lib.rs b/crates/egui_demo_app/src/lib.rs index ea30bda8d..45abccc7f 100644 --- a/crates/egui_demo_app/src/lib.rs +++ b/crates/egui_demo_app/src/lib.rs @@ -9,9 +9,10 @@ pub use wrap_app::{Anchor, WrapApp}; /// Time of day as seconds since midnight. Used for clock in demo app. pub(crate) fn seconds_since_midnight() -> f64 { - use chrono::Timelike as _; - let time = chrono::Local::now().time(); - time.num_seconds_from_midnight() as f64 + 1e-9 * (time.nanosecond() as f64) + jiff::Zoned::now() + .time() + .duration_since(jiff::civil::Time::midnight()) + .as_secs_f64() } /// Trait that wraps different parts of the demo app. diff --git a/crates/egui_demo_lib/Cargo.toml b/crates/egui_demo_lib/Cargo.toml index 4f3b853e0..dc57fb092 100644 --- a/crates/egui_demo_lib/Cargo.toml +++ b/crates/egui_demo_lib/Cargo.toml @@ -26,7 +26,7 @@ rustdoc-args = ["--generate-link-to-definition"] [features] default = [] -chrono = ["egui_extras/datepicker", "dep:chrono"] +jiff = ["egui_extras/datepicker", "dep:jiff"] ## Allow serialization using [`serde`](https://docs.rs/serde). serde = ["egui/serde", "dep:serde", "egui_extras/serde"] @@ -42,7 +42,7 @@ egui_extras = { workspace = true, features = ["image", "svg"] } unicode_names2.workspace = true # this old version has fewer dependencies #! ### Optional dependencies -chrono = { workspace = true, optional = true, features = ["js-sys", "wasmbind"] } +jiff = { workspace = true, optional = true, features = ["std", "js"] } ## Enable this when generating docs. document-features = { workspace = true, optional = true } serde = { workspace = true, optional = true } diff --git a/crates/egui_demo_lib/src/demo/widget_gallery.rs b/crates/egui_demo_lib/src/demo/widget_gallery.rs index 6e23fca92..ec5d5f3eb 100644 --- a/crates/egui_demo_lib/src/demo/widget_gallery.rs +++ b/crates/egui_demo_lib/src/demo/widget_gallery.rs @@ -19,11 +19,11 @@ pub struct WidgetGallery { color: egui::Color32, animate_progress_bar: bool, - #[cfg(feature = "chrono")] + #[cfg(feature = "jiff")] #[cfg_attr(feature = "serde", serde(skip))] - date: Option, + date: Option, - #[cfg(feature = "chrono")] + #[cfg(feature = "jiff")] with_date_button: bool, } @@ -39,19 +39,19 @@ impl Default for WidgetGallery { string: Default::default(), color: egui::Color32::LIGHT_BLUE.linear_multiply(0.5), animate_progress_bar: false, - #[cfg(feature = "chrono")] + #[cfg(feature = "jiff")] date: None, - #[cfg(feature = "chrono")] + #[cfg(feature = "jiff")] with_date_button: true, } } } impl WidgetGallery { - #[allow(clippy::allow_attributes, unused_mut)] // if not chrono + #[allow(clippy::allow_attributes, unused_mut)] // if not jiff #[inline] pub fn with_date_button(mut self, _with_date_button: bool) -> Self { - #[cfg(feature = "chrono")] + #[cfg(feature = "jiff")] { self.with_date_button = _with_date_button; } @@ -140,9 +140,9 @@ impl WidgetGallery { string, color, animate_progress_bar, - #[cfg(feature = "chrono")] + #[cfg(feature = "jiff")] date, - #[cfg(feature = "chrono")] + #[cfg(feature = "jiff")] with_date_button, } = self; @@ -242,9 +242,9 @@ impl WidgetGallery { } ui.end_row(); - #[cfg(feature = "chrono")] + #[cfg(feature = "jiff")] if *with_date_button { - let date = date.get_or_insert_with(|| chrono::offset::Utc::now().date_naive()); + let date = date.get_or_insert_with(|| jiff::Zoned::now().date()); ui.add(doc_link_label_with_crate( "egui_extras", "DatePickerButton", @@ -302,7 +302,7 @@ fn doc_link_label_with_crate<'a>( } } -#[cfg(feature = "chrono")] +#[cfg(feature = "jiff")] #[cfg(test)] mod tests { use super::*; @@ -314,7 +314,7 @@ mod tests { pub fn should_match_screenshot() { let mut demo = WidgetGallery { // If we don't set a fixed date, the snapshot test will fail. - date: Some(chrono::NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()), + date: Some(jiff::civil::date(2024, 1, 1)), ..Default::default() }; diff --git a/crates/egui_extras/Cargo.toml b/crates/egui_extras/Cargo.toml index b124148bc..944576f08 100644 --- a/crates/egui_extras/Cargo.toml +++ b/crates/egui_extras/Cargo.toml @@ -34,7 +34,7 @@ default = ["dep:mime_guess2"] all_loaders = ["file", "http", "image", "svg", "gif", "webp"] ## Enable [`DatePickerButton`] widget. -datepicker = ["chrono"] +datepicker = ["jiff"] ## Add support for loading images from `file://` URIs. file = ["dep:mime_guess2"] @@ -83,7 +83,7 @@ profiling.workspace = true serde = { workspace = true, optional = true } # Date operations needed for datepicker widget -chrono = { workspace = true, optional = true, features = ["clock", "js-sys", "std", "wasmbind"] } +jiff = { workspace = true, optional = true, features = ["std", "tz-system", "js"] } ## Enable this when generating docs. document-features = { workspace = true, optional = true } diff --git a/crates/egui_extras/src/datepicker/button.rs b/crates/egui_extras/src/datepicker/button.rs index d6f69fe77..692dc9d24 100644 --- a/crates/egui_extras/src/datepicker/button.rs +++ b/crates/egui_extras/src/datepicker/button.rs @@ -1,6 +1,6 @@ use super::popup::DatePickerPopup; -use chrono::NaiveDate; use egui::{Area, Button, Frame, InnerResponse, Key, Order, RichText, Ui, Widget}; +use jiff::civil::Date; use std::ops::RangeInclusive; #[derive(Default, Clone)] @@ -11,7 +11,7 @@ pub(crate) struct DatePickerButtonState { /// Shows a date, and will open a date picker popup when clicked. pub struct DatePickerButton<'a> { - selection: &'a mut NaiveDate, + selection: &'a mut Date, id_salt: Option<&'a str>, combo_boxes: bool, arrows: bool, @@ -20,13 +20,13 @@ pub struct DatePickerButton<'a> { show_icon: bool, format: String, highlight_weekends: bool, - start_end_years: Option>, + start_end_years: Option>, reverse_years: bool, - year_scroll_to: Option, + year_scroll_to: Option, } impl<'a> DatePickerButton<'a> { - pub fn new(selection: &'a mut NaiveDate) -> Self { + pub fn new(selection: &'a mut Date) -> Self { Self { selection, id_salt: None, @@ -95,7 +95,7 @@ impl<'a> DatePickerButton<'a> { } /// Change the format shown on the button. (Default: %Y-%m-%d) - /// See [`chrono::format::strftime`] for valid formats. + /// See [`jiff::fmt::strtime`] for valid formats. #[inline] pub fn format(mut self, format: impl Into) -> Self { self.format = format.into(); @@ -115,7 +115,7 @@ impl<'a> DatePickerButton<'a> { /// For example, if you want to provide the range of years from 2000 to 2035, you can use: /// `start_end_years(2000..=2035)`. #[inline] - pub fn start_end_years(mut self, start_end_years: RangeInclusive) -> Self { + pub fn start_end_years(mut self, start_end_years: RangeInclusive) -> Self { self.start_end_years = Some(start_end_years); self } @@ -130,7 +130,7 @@ impl<'a> DatePickerButton<'a> { /// Scroll the year dropdown to this year when the picker first opens. /// Defaults to the currently selected year. #[inline] - pub fn year_scroll_to(mut self, year: i32) -> Self { + pub fn year_scroll_to(mut self, year: i16) -> Self { self.year_scroll_to = Some(year); self } @@ -144,9 +144,9 @@ impl Widget for DatePickerButton<'_> { .unwrap_or_default(); let mut text = if self.show_icon { - RichText::new(format!("{} πŸ“†", self.selection.format(&self.format))) + RichText::new(format!("{} πŸ“†", self.selection.strftime(&self.format))) } else { - RichText::new(format!("{}", self.selection.format(&self.format))) + RichText::new(format!("{}", self.selection.strftime(&self.format))) }; let visuals = ui.visuals().widgets.open; if button_state.picker_visible { diff --git a/crates/egui_extras/src/datepicker/mod.rs b/crates/egui_extras/src/datepicker/mod.rs index 7a114b357..f1f6e58fa 100644 --- a/crates/egui_extras/src/datepicker/mod.rs +++ b/crates/egui_extras/src/datepicker/mod.rs @@ -4,32 +4,32 @@ mod button; mod popup; pub use button::DatePickerButton; -use chrono::{Datelike as _, Duration, NaiveDate, Weekday}; +use jiff::civil::{Date, ISOWeekDate, Weekday}; #[derive(Debug)] struct Week { number: u8, - days: Vec, + days: Vec, } -fn month_data(year: i32, month: u32) -> Vec { - let first = NaiveDate::from_ymd_opt(year, month, 1).expect("Could not create NaiveDate"); +fn month_data(year: i16, month: i8) -> Vec { + let first = Date::new(year, month, 1).expect("Could not create Date"); let mut start = first; - while start.weekday() != Weekday::Mon { - start = start.checked_sub_signed(Duration::days(1)).unwrap(); + while start.weekday() != Weekday::Monday { + start = start.yesterday().unwrap(); } let mut weeks = vec![]; let mut week = vec![]; - while start < first || start.month() == first.month() || start.weekday() != Weekday::Mon { + while start < first || start.month() == first.month() || start.weekday() != Weekday::Monday { week.push(start); - if start.weekday() == Weekday::Sun { + if start.weekday() == Weekday::Sunday { weeks.push(Week { - number: start.iso_week().week() as u8, + number: ISOWeekDate::from(start).week() as u8, days: std::mem::take(&mut week), }); } - start = start.checked_add_signed(Duration::days(1)).unwrap(); + start = start.tomorrow().unwrap(); } weeks diff --git a/crates/egui_extras/src/datepicker/popup.rs b/crates/egui_extras/src/datepicker/popup.rs index 1c24ca81d..5c0726e5a 100644 --- a/crates/egui_extras/src/datepicker/popup.rs +++ b/crates/egui_extras/src/datepicker/popup.rs @@ -1,4 +1,4 @@ -use chrono::{Datelike as _, NaiveDate, Weekday}; +use jiff::civil::{Date, Weekday}; use egui::{Align, Button, Color32, ComboBox, Direction, Id, Layout, RichText, Ui, Vec2}; @@ -9,43 +9,39 @@ use crate::{Column, Size, StripBuilder, TableBuilder}; #[derive(Default, Clone)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] struct DatePickerPopupState { - year: i32, - month: u32, - day: u32, + year: i16, + month: i8, + day: i8, setup: bool, year_scroll_needed: bool, } impl DatePickerPopupState { - fn last_day_of_month(&self) -> u32 { - let date: NaiveDate = - NaiveDate::from_ymd_opt(self.year, self.month, 1).expect("Could not create NaiveDate"); - date.with_day(31) - .map(|_| 31) - .or_else(|| date.with_day(30).map(|_| 30)) - .or_else(|| date.with_day(29).map(|_| 29)) - .unwrap_or(28) + fn last_day_of_month(&self) -> i8 { + Date::new(self.year, self.month, 1) + .expect("Could not create Date") + .days_in_month() } } pub(crate) struct DatePickerPopup<'a> { - pub selection: &'a mut NaiveDate, + pub selection: &'a mut Date, pub button_id: Id, pub combo_boxes: bool, pub arrows: bool, pub calendar: bool, pub calendar_week: bool, pub highlight_weekends: bool, - pub start_end_years: Option>, + pub start_end_years: Option>, pub reverse_years: bool, - pub year_scroll_to: Option, + pub year_scroll_to: Option, } impl DatePickerPopup<'_> { /// Returns `true` if user pressed `Save` button. pub fn draw(&mut self, ui: &mut Ui) -> bool { let id = ui.make_persistent_id("date_picker"); - let today = chrono::offset::Utc::now().date_naive(); + let today = jiff::Zoned::now().date(); let mut popup_state = ui .data_mut(|data| data.get_persisted::(id)) .unwrap_or_default(); @@ -95,7 +91,7 @@ impl DatePickerPopup<'_> { }; let scroll_to_year = self.year_scroll_to.unwrap_or(popup_state.year); - let years: Vec = if self.reverse_years { + let years: Vec = if self.reverse_years { (start_year..=end_year).rev().collect() } else { (start_year..=end_year).collect() @@ -132,7 +128,7 @@ impl DatePickerPopup<'_> { ComboBox::from_id_salt("date_picker_month") .selected_text(month_name(popup_state.month)) .show_ui(ui, |ui| { - for month in 1..=12 { + for month in 1i8..=12 { if ui .selectable_value( &mut popup_state.month, @@ -156,7 +152,7 @@ impl DatePickerPopup<'_> { ComboBox::from_id_salt("date_picker_day") .selected_text(popup_state.day.to_string()) .show_ui(ui, |ui| { - for day in 1..=popup_state.last_day_of_month() { + for day in 1i8..=popup_state.last_day_of_month() { if ui .selectable_value( &mut popup_state.day, @@ -333,9 +329,10 @@ impl DatePickerPopup<'_> { && popup_state.day == day.day() { ui.visuals().selection.bg_fill - } else if (day.weekday() == Weekday::Sat - || day.weekday() == Weekday::Sun) - && self.highlight_weekends + } else if (matches!( + day.weekday(), + Weekday::Saturday | Weekday::Sunday + )) && self.highlight_weekends { if ui.visuals().dark_mode { Color32::DARK_RED @@ -414,12 +411,12 @@ impl DatePickerPopup<'_> { strip.cell(|ui| { ui.with_layout(Layout::top_down_justified(Align::Center), |ui| { if ui.button("Save").clicked() { - *self.selection = NaiveDate::from_ymd_opt( + *self.selection = Date::new( popup_state.year, popup_state.month, popup_state.day, ) - .expect("Could not create NaiveDate"); + .expect("Could not create Date"); saved = true; close = true; } @@ -442,7 +439,7 @@ impl DatePickerPopup<'_> { } } -fn month_name(i: u32) -> &'static str { +fn month_name(i: i8) -> &'static str { match i { 1 => "January", 2 => "February", diff --git a/crates/egui_extras/src/lib.rs b/crates/egui_extras/src/lib.rs index 19e0c95a8..99e6dd5e4 100644 --- a/crates/egui_extras/src/lib.rs +++ b/crates/egui_extras/src/lib.rs @@ -8,7 +8,7 @@ #![expect(clippy::manual_range_contains)] -#[cfg(feature = "chrono")] +#[cfg(feature = "datepicker")] mod datepicker; pub mod syntax_highlighting; @@ -21,7 +21,7 @@ mod sizing; mod strip; mod table; -#[cfg(feature = "chrono")] +#[cfg(feature = "datepicker")] pub use crate::datepicker::DatePickerButton; pub(crate) use crate::layout::StripLayout; diff --git a/examples/hello_android/Cargo.toml b/examples/hello_android/Cargo.toml index 7c8444f99..ae484783c 100644 --- a/examples/hello_android/Cargo.toml +++ b/examples/hello_android/Cargo.toml @@ -22,7 +22,7 @@ eframe = { workspace = true, default-features = false, features = [ "glow", "android-native-activity", ] } -egui_demo_lib = { workspace = true, features = ["chrono"] } +egui_demo_lib = { workspace = true, features = ["jiff"] } # For image support: egui_extras = { workspace = true, features = ["default", "image"] } From 307202ab67a8459f1f9e924484b25275e1057596 Mon Sep 17 00:00:00 2001 From: Grrr <163682431+x4exr@users.noreply.github.com> Date: Tue, 24 Mar 2026 08:59:05 -0400 Subject: [PATCH 50/58] egui_wgpu, added disclaimer, discourages people calling render without `update_buffer` (#7971) * Closes * [x] I have followed the instructions in the PR template Co-authored-by: Emil Ernerfeldt --- crates/egui-wgpu/src/renderer.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/egui-wgpu/src/renderer.rs b/crates/egui-wgpu/src/renderer.rs index 640f33f51..e55f7581a 100644 --- a/crates/egui-wgpu/src/renderer.rs +++ b/crates/egui-wgpu/src/renderer.rs @@ -470,6 +470,9 @@ impl Renderer { /// The render pass internally keeps all referenced resources alive as long as necessary. /// The only consequence of `forget_lifetime` is that any operation on the parent encoder will cause a runtime error /// instead of a compile time error. + /// + /// # Panic + /// Always ensure that [`Renderer::update_buffers`] has been called otherwise calling [`Renderer::render`] will panic! pub fn render( &self, render_pass: &mut wgpu::RenderPass<'static>, From 8137aa350c2165d9824acb79d453150f18a3ebc6 Mon Sep 17 00:00:00 2001 From: Ellie High <6687206+wizzeh@users.noreply.github.com> Date: Tue, 24 Mar 2026 05:59:16 -0700 Subject: [PATCH 51/58] Allow fallback from smithay to arboard when getting clipboard (#7976) * [X] I have followed the instructions in the PR template Quick fix -- when the arboard and smithay features are both enabled, Clipboard::get returns early if it can't find a smithay clipboard. This PR just allows fallback to arboard instead of early-returning. --------- Co-authored-by: Emil Ernerfeldt --- crates/egui-winit/src/clipboard.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/egui-winit/src/clipboard.rs b/crates/egui-winit/src/clipboard.rs index 75d0469ec..2410c3ee6 100644 --- a/crates/egui-winit/src/clipboard.rs +++ b/crates/egui-winit/src/clipboard.rs @@ -65,13 +65,12 @@ impl Clipboard { feature = "smithay-clipboard" ))] if let Some(clipboard) = &mut self.smithay { - return match clipboard.load() { - Ok(text) => Some(text), + match clipboard.load() { + Ok(text) => return Some(text), Err(err) => { log::error!("smithay paste error: {err}"); - None } - }; + } } #[cfg(all( From 405eb8157849eae446e10621ecc9bbefd25d9eed Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 24 Mar 2026 08:03:00 -0500 Subject: [PATCH 52/58] Fix menu keyboard toggle for open submenus (#7957) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary This PR fixes submenu keyboard parity: pressing Enter/Space on an already-open submenu button now collapses that submenu (matching top-level menu button behavior). What changed Updated submenu interaction logic to distinguish pointer primary clicks from keyboard/accessibility-triggered clicks. Kept pointer/touch behavior unchanged (submenu button clicks still don’t auto-close submenu). Added regression tests for: keyboard open of nested submenu, keyboard close (second Enter) of nested submenu, pointer clicks on submenu button keeping submenu open. Validation cargo test -p egui_kittest --test regression_tests Breaking changes None. Behavior change is limited to keyboard/accessibility activation of already-open submenu buttons. * Closes * [ X ] I have followed the instructions in the PR template --------- Co-authored-by: Lucas Meurer --- crates/egui/src/containers/menu.rs | 17 +++- crates/egui_kittest/tests/regression_tests.rs | 98 +++++++++++++++++++ 2 files changed, 113 insertions(+), 2 deletions(-) diff --git a/crates/egui/src/containers/menu.rs b/crates/egui/src/containers/menu.rs index 1bd5954c8..cfdaac827 100644 --- a/crates/egui/src/containers/menu.rs +++ b/crates/egui/src/containers/menu.rs @@ -10,7 +10,7 @@ use crate::style::StyleModifier; use crate::{ - Button, Color32, Context, Frame, Id, InnerResponse, IntoAtoms, Layout, Popup, + Button, Color32, Context, Frame, Id, InnerResponse, IntoAtoms, Layout, PointerButton, Popup, PopupCloseBehavior, Response, Style, Ui, UiBuilder, UiKind, UiStack, UiStackInfo, Widget as _, }; use emath::{Align, RectAlign, Vec2, vec2}; @@ -458,6 +458,7 @@ impl SubMenu { let is_any_open = open_item.is_some(); let mut is_open = open_item == Some(id); + let was_open = is_open; let mut set_open = None; // We expand the button rect so there is no empty space where no menu is shown @@ -470,9 +471,21 @@ impl SubMenu { // But since we check if no other menu is open, nothing should be able to cover the button let is_hovered = hover_pos.is_some_and(|pos| button_rect.contains(pos)); + // `clicked` includes keyboard and accessibility click actions. + // We want Enter/Space to toggle an already open submenu, while pointer clicks should keep + // the submenu open (for touch and pointer interactions). + let clicked = button_response.clicked(); + let clicked_by_pointer = button_response.clicked_by(PointerButton::Primary); + let clicked_by_keyboard_or_access = clicked && !clicked_by_pointer; + + if ui.is_enabled() && is_open && clicked_by_keyboard_or_access { + set_open = Some(false); + is_open = false; + } + // The clicked handler is there for accessibility (keyboard navigation) let should_open = - ui.is_enabled() && (button_response.clicked() || (is_hovered && !is_any_open)); + ui.is_enabled() && ((!was_open && clicked) || (is_hovered && !is_any_open)); if should_open { set_open = Some(true); is_open = true; diff --git a/crates/egui_kittest/tests/regression_tests.rs b/crates/egui_kittest/tests/regression_tests.rs index 4289fe3a9..94617ff8b 100644 --- a/crates/egui_kittest/tests/regression_tests.rs +++ b/crates/egui_kittest/tests/regression_tests.rs @@ -262,3 +262,101 @@ pub fn menus_should_close_even_if_submenu_disappears() { ); } } + +fn keyboard_submenu_harness() -> Harness<'static, bool> { + Harness::builder() + .with_size(Vec2::new(400.0, 240.0)) + .build_ui_state( + |ui, checked| { + egui::Panel::top("menu_bar").show_inside(ui, |ui| { + egui::MenuBar::new().ui(ui, |ui| { + ui.menu_button("X", |ui| { + ui.menu_button("Y", |ui| { + ui.checkbox(checked, "Goal"); + }); + }); + }); + }); + }, + false, + ) +} + +#[test] +pub fn keyboard_should_open_nested_submenu() { + let mut harness = keyboard_submenu_harness(); + + harness.get_by_label("X").focus(); + harness.run(); + + harness.key_press(egui::Key::Enter); + harness.run(); + + harness.get_by_label_contains("Y").focus(); + harness.run(); + + harness.key_press(egui::Key::Enter); + harness.run(); + + assert!( + harness.query_by_label("Goal").is_some(), + "Expected nested submenu to open via keyboard" + ); +} + +#[test] +pub fn keyboard_should_close_nested_submenu_with_second_enter() { + let mut harness = keyboard_submenu_harness(); + + harness.get_by_label("X").focus(); + harness.run(); + + harness.key_press(egui::Key::Enter); + harness.run(); + + harness.get_by_label_contains("Y").focus(); + harness.run(); + + harness.key_press(egui::Key::Enter); + harness.run(); + + assert!( + harness.query_by_label("Goal").is_some(), + "Expected nested submenu to open before close attempt" + ); + + harness.get_by_label_contains("Y").focus(); + harness.run(); + + harness.key_press(egui::Key::Enter); + harness.run(); + + assert!( + harness.query_by_label("Goal").is_none(), + "Expected nested submenu to close when pressing Enter again" + ); +} + +#[test] +pub fn pointer_click_on_open_submenu_button_should_not_close_it() { + let mut harness = keyboard_submenu_harness(); + + harness.get_by_label("X").click(); + harness.run(); + + harness.get_by_label_contains("Y").click(); + harness.run(); + + assert!( + harness.query_by_label("Goal").is_some(), + "Expected submenu to remain open after pointer click on its button" + ); + + harness.get_by_label_contains("Y").click(); + harness.run(); + + assert!( + harness.query_by_label("Goal").is_some(), + "Expected submenu to remain open on repeated pointer click" + ); +} From 5ed92c3011907df59a0f6d6aa5e5479cbef84d30 Mon Sep 17 00:00:00 2001 From: rustbasic <127506429+rustbasic@users.noreply.github.com> Date: Tue, 24 Mar 2026 22:03:20 +0900 Subject: [PATCH 53/58] Add `Button::left_text` (#7955) feat: Add left_text() to egui::Button This PR introduces the `left_text()` method to `egui::Button`. It enables placing additional text content on the left side of the button's primary label, which is useful for displaying auxiliary information, labels, and for facilitating left-aligned text within the button. ```rust let is_selected = true; let selectable_label_widget = egui::Button::selectable( is_selected, "", ).left_text("Left"); let desired_width = ui.available_width(); let desired_height = ui.spacing().interact_size.y; interaction_response = ui.add_sized( egui::vec2(desired_width, desired_height), selectable_label_widget, ); ``` --- crates/egui/src/widgets/button.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 7ad155f16..7d9dddf0d 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -240,6 +240,18 @@ impl<'a> Button<'a> { self } + /// Show some text on the left side of the button. + #[inline] + pub fn left_text(mut self, left_text: impl IntoAtoms<'a>) -> Self { + self.layout.push_left(Atom::grow()); + + for atom in left_text.into_atoms() { + self.layout.push_left(atom); + } + + self + } + /// Show some text on the right side of the button. #[inline] pub fn right_text(mut self, right_text: impl IntoAtoms<'a>) -> Self { From a1af9abe70ad3a1c049d8d9968886af483af666a Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 24 Mar 2026 15:46:56 +0100 Subject: [PATCH 54/58] Shrink the byte-size of `Response` slightly (#8011) Small optimization! 96 -> 88 bytes, so no huge win. --- crates/egui/src/atomics/atom_layout.rs | 5 ++-- crates/egui/src/context.rs | 15 ++++------ crates/egui/src/response.rs | 41 ++++++++++++++++++++++---- crates/egui/src/ui.rs | 2 +- crates/egui/src/widgets/label.rs | 4 +-- crates/emath/src/pos2.rs | 5 ++++ tests/egui_tests/tests/test_atoms.rs | 8 ++--- 7 files changed, 57 insertions(+), 23 deletions(-) diff --git a/crates/egui/src/atomics/atom_layout.rs b/crates/egui/src/atomics/atom_layout.rs index c408146d6..7894273f3 100644 --- a/crates/egui/src/atomics/atom_layout.rs +++ b/crates/egui/src/atomics/atom_layout.rs @@ -318,8 +318,9 @@ impl<'a> AtomLayout<'a> { let (_, rect) = ui.allocate_space(frame_size); let mut response = ui.interact(rect, id, sense); - response.intrinsic_size = - Some((Vec2::new(intrinsic_width, intrinsic_height) + margin.sum()).at_least(min_size)); + response.set_intrinsic_size( + (Vec2::new(intrinsic_width, intrinsic_height) + margin.sum()).at_least(min_size), + ); AllocatedAtomLayout { sized_atoms: sized_items, diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index fbc189132..af6c0e6d0 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1378,8 +1378,8 @@ impl Context { interact_rect, sense, flags: Flags::empty(), - interact_pointer_pos: None, - intrinsic_size: None, + interact_pointer_pos_or_nan: Pos2::NAN, + intrinsic_size_or_nan: Vec2::NAN, }; res.flags.set(Flags::ENABLED, enabled); @@ -1470,14 +1470,11 @@ impl Context { || res.long_touched() || clicked || res.drag_stopped(); - if is_interacted_with { - res.interact_pointer_pos = input.pointer.interact_pos(); - if let (Some(to_global), Some(pos)) = ( - memory.to_global.get(&res.layer_id), - &mut res.interact_pointer_pos, - ) { - *pos = to_global.inverse() * *pos; + if is_interacted_with && let Some(mut pos) = input.pointer.interact_pos() { + if let Some(to_global) = memory.to_global.get(&res.layer_id) { + pos = to_global.inverse() * pos; } + res.interact_pointer_pos_or_nan = pos; } if input.pointer.any_down() && !is_interacted_with { diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 495d5dcdf..241f5a381 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -55,7 +55,7 @@ pub struct Response { /// Where the pointer (mouse/touch) were when this widget was clicked or dragged. /// `None` if the widget is not being interacted with. #[doc(hidden)] - pub interact_pointer_pos: Option, + pub interact_pointer_pos_or_nan: Pos2, /// The intrinsic / desired size of the widget. /// @@ -67,12 +67,22 @@ pub struct Response { /// At the time of writing, this is only used by external crates /// for improved layouting. /// See for instance [`egui_flex`](https://github.com/lucasmerlin/hello_egui/tree/main/crates/egui_flex). - pub intrinsic_size: Option, + #[doc(hidden)] + pub intrinsic_size_or_nan: Vec2, #[doc(hidden)] pub flags: Flags, } +#[test] +fn test_response_size() { + assert_eq!( + std::mem::size_of::(), + 88, + "Keep Response small, because we create them often, and we want to keep it lean and fast" + ); +} + /// A bit set for various boolean properties of `Response`. #[doc(hidden)] #[derive(Copy, Clone, Debug)] @@ -489,7 +499,26 @@ impl Response { /// `None` if the widget is not being interacted with. #[inline] pub fn interact_pointer_pos(&self) -> Option { - self.interact_pointer_pos + let pos = self.interact_pointer_pos_or_nan; + if pos.any_nan() { None } else { Some(pos) } + } + + /// The intrinsic / desired size of the widget. + /// + /// This is the size that a non-wrapped, non-truncated, non-justified version of the widget + /// would have. + /// + /// If this is `None`, use [`Self::rect`] instead. + #[inline] + pub fn intrinsic_size(&self) -> Option { + let size = self.intrinsic_size_or_nan; + if size.any_nan() { None } else { Some(size) } + } + + /// Set the intrinsic / desired size of the widget. + #[inline] + pub fn set_intrinsic_size(&mut self, size: Vec2) { + self.intrinsic_size_or_nan = size; } /// If it is a good idea to show a tooltip, where is pointer? @@ -1007,8 +1036,10 @@ impl Response { interact_rect: self.interact_rect.union(other.interact_rect), sense: self.sense.union(other.sense), flags: self.flags | other.flags, - interact_pointer_pos: self.interact_pointer_pos.or(other.interact_pointer_pos), - intrinsic_size: None, + interact_pointer_pos_or_nan: self + .interact_pointer_pos() + .unwrap_or(other.interact_pointer_pos_or_nan), + intrinsic_size_or_nan: Vec2::NAN, } } } diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index daabd10b0..f0b270951 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -1281,7 +1281,7 @@ impl Ui { pub fn allocate_response(&mut self, desired_size: Vec2, sense: Sense) -> Response { let (id, rect) = self.allocate_space(desired_size); let mut response = self.interact(rect, id, sense); - response.intrinsic_size = Some(desired_size); + response.set_intrinsic_size(desired_size); response } diff --git a/crates/egui/src/widgets/label.rs b/crates/egui/src/widgets/label.rs index 284cfd12c..7b2d3a3ba 100644 --- a/crates/egui/src/widgets/label.rs +++ b/crates/egui/src/widgets/label.rs @@ -220,7 +220,7 @@ impl Label { .rect_without_leading_space() .translate(pos.to_vec2()); let mut response = ui.allocate_rect(rect, sense); - response.intrinsic_size = Some(galley.intrinsic_size()); + response.set_intrinsic_size(galley.intrinsic_size()); for placed_row in galley.rows.iter().skip(1) { let rect = placed_row.rect().translate(pos.to_vec2()); response |= ui.allocate_rect(rect, sense); @@ -256,7 +256,7 @@ impl Label { let galley = ui.fonts_mut(|fonts| fonts.layout_job(layout_job)); let (rect, mut response) = ui.allocate_exact_size(galley.size(), sense); - response.intrinsic_size = Some(galley.intrinsic_size()); + response.set_intrinsic_size(galley.intrinsic_size()); let galley_pos = match galley.job.halign { Align::LEFT => rect.left_top(), Align::Center => rect.center_top(), diff --git a/crates/emath/src/pos2.rs b/crates/emath/src/pos2.rs index fc26686b3..f67767e6b 100644 --- a/crates/emath/src/pos2.rs +++ b/crates/emath/src/pos2.rs @@ -119,6 +119,11 @@ impl Pos2 { /// Same as `Pos2::default()`. pub const ZERO: Self = Self { x: 0.0, y: 0.0 }; + pub const NAN: Self = Self { + x: f32::NAN, + y: f32::NAN, + }; + #[inline(always)] pub const fn new(x: f32, y: f32) -> Self { Self { x, y } diff --git a/tests/egui_tests/tests/test_atoms.rs b/tests/egui_tests/tests/test_atoms.rs index f6e9df14a..f7e0a4af1 100644 --- a/tests/egui_tests/tests/test_atoms.rs +++ b/tests/egui_tests/tests/test_atoms.rs @@ -92,17 +92,17 @@ fn test_intrinsic_size() { if let Some(current_intrinsic_size) = intrinsic_size { assert_eq!( Some(current_intrinsic_size), - response.intrinsic_size, + response.intrinsic_size(), "For wrapping: {wrapping:?}" ); } assert!( - response.intrinsic_size.is_some(), + response.intrinsic_size().is_some(), "intrinsic_size should be set for `Button`" ); - intrinsic_size = response.intrinsic_size; + intrinsic_size = response.intrinsic_size(); if wrapping == TextWrapMode::Extend { - assert_eq!(Some(response.rect.size()), response.intrinsic_size); + assert_eq!(Some(response.rect.size()), response.intrinsic_size()); } }); } From 96cae39fb8883065e7460e8ce8780a15a489b345 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 24 Mar 2026 15:59:53 +0100 Subject: [PATCH 55/58] Update a couple of dependencies (#8009) * Closes https://github.com/emilk/egui/pull/7992 --- Cargo.lock | 78 +++++++++++++++++----------- Cargo.toml | 14 ++--- crates/eframe/Cargo.toml | 3 ++ crates/eframe/src/native/app_icon.rs | 2 +- crates/egui-winit/Cargo.toml | 1 + crates/egui-winit/src/safe_area.rs | 4 +- 6 files changed, 63 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a15a00a02..981e7c475 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -281,7 +281,7 @@ dependencies = [ "clipboard-win", "image", "log", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-app-kit 0.3.2", "objc2-core-foundation", "objc2-core-graphics", @@ -590,7 +590,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" dependencies = [ - "objc2 0.6.3", + "objc2 0.6.4", ] [[package]] @@ -1147,7 +1147,7 @@ dependencies = [ "bitflags 2.9.4", "block2 0.6.2", "libc", - "objc2 0.6.3", + "objc2 0.6.4", ] [[package]] @@ -1222,9 +1222,9 @@ dependencies = [ "image", "js-sys", "log", - "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", "parking_lot", "percent-encoding", "pollster", @@ -1290,9 +1290,9 @@ dependencies = [ "document-features", "egui", "log", - "objc2 0.5.2", - "objc2-foundation 0.2.2", - "objc2-ui-kit", + "objc2 0.6.4", + "objc2-foundation 0.3.2", + "objc2-ui-kit 0.3.2", "profiling", "raw-window-handle", "serde", @@ -1940,7 +1940,7 @@ dependencies = [ "glutin_glx_sys", "glutin_wgl_sys", "libloading", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-app-kit 0.3.2", "objc2-core-foundation", "objc2-foundation 0.3.2", @@ -2800,9 +2800,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", ] @@ -2831,7 +2831,7 @@ checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ "bitflags 2.9.4", "block2 0.6.2", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-core-foundation", "objc2-core-graphics", "objc2-foundation 0.3.2", @@ -2881,7 +2881,7 @@ checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.9.4", "dispatch2", - "objc2 0.6.3", + "objc2 0.6.4", ] [[package]] @@ -2892,7 +2892,7 @@ checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ "bitflags 2.9.4", "dispatch2", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-core-foundation", "objc2-io-surface", ] @@ -2947,7 +2947,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.9.4", - "objc2 0.6.3", + "block2 0.6.2", + "objc2 0.6.4", "objc2-core-foundation", ] @@ -2958,7 +2959,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ "bitflags 2.9.4", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-core-foundation", ] @@ -2994,7 +2995,7 @@ checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" dependencies = [ "bitflags 2.9.4", "block2 0.6.2", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-foundation 0.3.2", ] @@ -3018,7 +3019,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ "bitflags 2.9.4", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-core-foundation", "objc2-foundation 0.3.2", "objc2-metal 0.3.2", @@ -3055,6 +3056,18 @@ dependencies = [ "objc2-user-notifications", ] +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.9.4", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-uniform-type-identifiers" version = "0.2.2" @@ -3652,7 +3665,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135" dependencies = [ - "objc2 0.6.3", + "objc2 0.6.4", "objc2-core-foundation", "objc2-foundation 0.3.2", "objc2-quartz-core 0.3.2", @@ -3777,7 +3790,7 @@ dependencies = [ "js-sys", "libc", "log", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-app-kit 0.3.2", "objc2-core-foundation", "objc2-foundation 0.3.2", @@ -3818,14 +3831,15 @@ dependencies = [ [[package]] name = "ron" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db09040cc89e461f1a265139777a2bde7f8d8c67c4936f700c63ce3e2904d468" +checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32" dependencies = [ - "base64", "bitflags 2.9.4", + "once_cell", "serde", "serde_derive", + "typeid", "unicode-ident", ] @@ -4621,6 +4635,12 @@ dependencies = [ "rustc-hash 2.1.1", ] +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "uds_windows" version = "1.1.0" @@ -5077,7 +5097,7 @@ dependencies = [ "jni", "log", "ndk-context", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-foundation 0.3.2", "url", "web-sys", @@ -5225,7 +5245,7 @@ dependencies = [ "log", "naga", "ndk-sys", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-core-foundation", "objc2-foundation 0.3.2", "objc2-metal 0.3.2", @@ -5655,9 +5675,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winit" -version = "0.30.12" +version = "0.30.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66d4b9ed69c4009f6321f762d6e61ad8a2389cd431b97cb1e146812e9e6c732" +checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" dependencies = [ "ahash", "android-activity", @@ -5679,7 +5699,7 @@ dependencies = [ "objc2 0.5.2", "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", - "objc2-ui-kit", + "objc2-ui-kit 0.2.2", "orbclient", "percent-encoding", "pin-project", diff --git a/Cargo.toml b/Cargo.toml index 6978e4df8..cf631eb25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -104,10 +104,10 @@ mimalloc = "0.1.48" mime_guess2 = { version = "2.3.1", default-features = false } mint = "0.5.9" nohash-hasher = "0.2.0" -objc2 = "0.5.2" -objc2-app-kit = { version = "0.2.2", default-features = false } -objc2-foundation = { version = "0.2.2", default-features = false } -objc2-ui-kit = { version = "0.2.2", default-features = false } +objc2 = "0.6.4" +objc2-app-kit = { version = "0.3.2", default-features = false } +objc2-foundation = { version = "0.3.2", default-features = false } +objc2-ui-kit = { version = "0.3.2", default-features = false } open = "5.3.2" parking_lot = "0.12.5" percent-encoding = "2.3.2" @@ -121,7 +121,7 @@ raw-window-handle = "0.6.2" rayon = "1.11.0" resvg = { version = "0.45.1", default-features = false } rfd = "0.17.2" -ron = "0.11.0" +ron = "0.12.0" self_cell = "1.2.1" serde = { version = "1.0.228", features = ["derive"] } similar-asserts = "1.7.0" @@ -133,7 +133,7 @@ syntect = { version = "5.3.0", default-features = false } tempfile = "3.23.0" thiserror = "2.0.17" tokio = "1.49" -toml = {version = "1", default-features = false } +toml = {version = "1.0.0", default-features = false } type-map = "0.5.1" unicode_names2 = { version = "2.0.0", default-features = false } unicode-segmentation = "1.12.0" @@ -146,7 +146,7 @@ web-time = "1.1.0" # Timekeeping for native and web webbrowser = "1.0.5" wgpu = { version = "29.0.0", default-features = false, features = ["std"] } windows-sys = "0.61.2" -winit = { version = "0.30.12", default-features = false } +winit = { version = "0.30.13", default-features = false } [workspace.lints.rust] unsafe_code = "deny" diff --git a/crates/eframe/Cargo.toml b/crates/eframe/Cargo.toml index 376f2ecbb..6219b90fd 100644 --- a/crates/eframe/Cargo.toml +++ b/crates/eframe/Cargo.toml @@ -169,7 +169,10 @@ objc2-foundation = { workspace = true, default-features = false, features = [ objc2-app-kit = { workspace = true, default-features = false, features = [ "std", "NSApplication", + "NSBitmapImageRep", + "NSGraphics", "NSImage", + "NSImageRep", "NSMenu", "NSMenuItem", "NSResponder", diff --git a/crates/eframe/src/native/app_icon.rs b/crates/eframe/src/native/app_icon.rs index 3ac61d8e6..85be6754b 100644 --- a/crates/eframe/src/native/app_icon.rs +++ b/crates/eframe/src/native/app_icon.rs @@ -204,7 +204,7 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS use crate::icon_data::IconDataExt as _; profiling::function_scope!(); - use objc2::ClassType as _; + use objc2::AnyThread as _; use objc2_app_kit::{NSApplication, NSImage}; use objc2_foundation::NSString; diff --git a/crates/egui-winit/Cargo.toml b/crates/egui-winit/Cargo.toml index bb3576a2d..dd4aa8f9d 100644 --- a/crates/egui-winit/Cargo.toml +++ b/crates/egui-winit/Cargo.toml @@ -81,6 +81,7 @@ objc2.workspace = true objc2-foundation = { workspace = true, features = ["std", "NSThread"] } objc2-ui-kit = { workspace = true, features = [ "std", + "objc2-core-foundation", "UIApplication", "UIGeometry", "UIResponder", diff --git a/crates/egui-winit/src/safe_area.rs b/crates/egui-winit/src/safe_area.rs index 5f4a9f9cf..378f44a94 100644 --- a/crates/egui-winit/src/safe_area.rs +++ b/crates/egui-winit/src/safe_area.rs @@ -36,8 +36,8 @@ mod ios { | UISceneActivationState::ForegroundInactive ) { - // Safe to cast, the class kind was checked above - let window_scene = Retained::cast::(scene.clone()); + // SAFETY: class kind was checked above with `isKindOfClass` + let window_scene = Retained::cast_unchecked::(scene.clone()); if let Some(window) = window_scene.keyWindow() { let insets = window.safeAreaInsets(); return SafeAreaInsets(MarginF32 { From cd3c38cf2a0d65330a645faeadb6f666bc8df8f2 Mon Sep 17 00:00:00 2001 From: Gautier Cailly <109429289+gcailly@users.noreply.github.com> Date: Tue, 24 Mar 2026 16:20:57 +0100 Subject: [PATCH 56/58] Improve behavior of invisible windows (#7905) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary * Closes #5229 * Closes #7776 On Windows, once a window is hidden with `ViewportCommand::Visible(false)`, two problems occur: 1. **Window can never be shown again** β€” Windows stops sending `RedrawRequested` events to invisible windows, and viewport commands are only processed during `run_ui_and_paint`, which is triggered by `RedrawRequested`. This creates a deadlock: ``` Visible(false) β†’ window hidden β†’ no RedrawRequested β†’ run_ui_and_paint never called β†’ Visible(true) stuck in queue β†’ window stays hidden forever ``` 2. **High CPU usage** β€” The event loop spins at full speed with `ControlFlow::Poll` even for invisible windows, and repaint requests are scheduled immediately, causing a tight loop that burns CPU. ## Fix **For #5229:** In `check_redraw_requests`, after calling `window.request_redraw()`, detect invisible windows via `window.is_visible() == Some(false)` and call `run_ui_and_paint` directly for them. This ensures pending viewport commands (including `Visible(true)`) are still processed even when the OS doesn't send redraw events. **For #7776:** Three layers of throttling for invisible windows: - **Heartbeat scheduling:** After painting an invisible window, schedule the next repaint 100ms in the future (instead of immediately). This keeps viewport commands flowing while limiting to ~10 repaints/sec. - **Event throttling:** In `user_event`, throttle `RequestRepaint` events for invisible windows to at least 100ms delay, preventing egui's repaint callback from bypassing the heartbeat. - **ControlFlow fix:** Only set `ControlFlow::Poll` for visible windows. Invisible windows use `WaitUntil` instead of spinning. - **Backend sleep:** Add `is_visible() == Some(false)` alongside the existing `is_minimized()` sleep check in both wgpu and glow backends (defense in depth). The fix is platform-agnostic: `is_visible()` returns `Some(false)` only when the platform can confirm invisibility, so it won't trigger on platforms where invisible windows still receive `RedrawRequested`. ## Test plan - [x] `cargo fmt` passes - [x] `cargo clippy -p eframe --all-features` passes with no warnings - [x] Manual test on Windows: window reappears after `Visible(true)` when hidden - [x] Manual test on Windows: CPU stays near 0% while window is invisible (was ~16% before fix) --------- Co-authored-by: Emil Ernerfeldt --- crates/eframe/src/native/glow_integration.rs | 6 +- crates/eframe/src/native/run.rs | 64 +++++++++++++++++-- crates/eframe/src/native/wgpu_integration.rs | 9 ++- crates/eframe/src/native/winit_integration.rs | 8 +++ 4 files changed, 77 insertions(+), 10 deletions(-) diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index 724ddc6d5..37e3faa69 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -34,7 +34,7 @@ use egui_winit::accesskit_winit; use crate::{ App, AppCreator, CreationContext, NativeOptions, Result, Storage, - native::epi_integration::EpiIntegration, + native::{epi_integration::EpiIntegration, winit_integration::is_invisible_or_minimized}, }; use super::{ @@ -761,9 +761,11 @@ impl GlowWinitRunning<'_> { integration.maybe_autosave(app.as_mut(), Some(&window)); - if window.is_minimized() == Some(true) { + if is_invisible_or_minimized(&window) { // On Mac, a minimized Window uses up all CPU: // https://github.com/emilk/egui/issues/325 + // On Windows, an invisible window also uses up all CPU: + // https://github.com/emilk/egui/issues/7776 profiling::scope!("minimized_sleep"); std::thread::sleep(std::time::Duration::from_millis(10)); } diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs index 0597d318c..73b58ae61 100644 --- a/crates/eframe/src/native/run.rs +++ b/crates/eframe/src/native/run.rs @@ -1,4 +1,4 @@ -use std::time::Instant; +use std::time::{Duration, Instant}; use winit::{ application::ApplicationHandler, @@ -11,9 +11,20 @@ use ahash::HashMap; use super::winit_integration::{UserEvent, WinitApp}; use crate::{ Result, epi, - native::{event_loop_context, winit_integration::EventResult}, + native::{ + event_loop_context, + winit_integration::{EventResult, is_invisible_or_minimized}, + }, }; +/// Minimum interval between repaints for invisible windows. +/// +/// On Windows, invisible windows don't receive `RedrawRequested` events, +/// so we throttle their repaints to avoid busy-looping while still +/// processing viewport commands like `Visible(true)`. +/// See . +const INVISIBLE_WINDOW_REPAINT_INTERVAL: Duration = Duration::from_millis(100); + // ---------------------------------------------------------------------------- fn create_event_loop(native_options: &mut epi::NativeOptions) -> Result> { #[cfg(target_os = "android")] @@ -177,23 +188,54 @@ impl WinitAppWrapper { fn check_redraw_requests(&mut self, event_loop: &ActiveEventLoop) { let now = Instant::now(); + let mut invisible_window_ids = Vec::new(); + self.windows_next_repaint_times .retain(|window_id, repaint_time| { if now < *repaint_time { return true; // not yet ready } - event_loop.set_control_flow(ControlFlow::Poll); - if let Some(window) = self.winit_app.window(*window_id) { - log::trace!("request_redraw for {window_id:?}"); - window.request_redraw(); + // On Windows, invisible windows don't receive RedrawRequested + // events, so pending viewport commands (e.g. Visible(true)) would + // never be processed. We collect these windows to paint them + // directly below. + // See: https://github.com/emilk/egui/issues/5229 + if is_invisible_or_minimized(&window) { + invisible_window_ids.push(*window_id); + } else { + log::trace!("request_redraw for {window_id:?}"); + event_loop.set_control_flow(ControlFlow::Poll); + window.request_redraw(); + } } else { log::trace!("No window found for {window_id:?}"); } false }); + // Paint invisible windows directly, since they won't receive + // RedrawRequested events on Windows. This ensures that viewport + // commands like Visible(true) are still processed. + for window_id in &invisible_window_ids { + let event_result = self.winit_app.run_ui_and_paint(event_loop, *window_id); + self.handle_event_result(event_loop, event_result); + } + + // Throttle any already-scheduled repaints for invisible windows + // to avoid busy-looping. If no repaint was requested by the app, + // the window will simply sleep. + // See: https://github.com/emilk/egui/issues/7776 + if !invisible_window_ids.is_empty() { + let next_paint = Instant::now() + INVISIBLE_WINDOW_REPAINT_INTERVAL; + for window_id in &invisible_window_ids { + self.windows_next_repaint_times + .entry(*window_id) + .and_modify(|t| *t = (*t).min(next_paint)); + } + } + let next_repaint_time = self.windows_next_repaint_times.values().min().copied(); if let Some(next_repaint_time) = next_repaint_time { event_loop.set_control_flow(ControlFlow::WaitUntil(next_repaint_time)); @@ -270,6 +312,16 @@ impl ApplicationHandler for WinitAppWrapper { if let Some(window_id) = self.winit_app.window_id_from_viewport_id(viewport_id) { + // Throttle repaints for invisible windows to prevent + // high CPU usage on Windows. + // See: https://github.com/emilk/egui/issues/7776 + let when = if let Some(window) = self.winit_app.window(window_id) + && is_invisible_or_minimized(&window) + { + when.max(Instant::now() + INVISIBLE_WINDOW_REPAINT_INTERVAL) + } else { + when + }; Ok(EventResult::RepaintAt(window_id, when)) } else { Ok(EventResult::Wait) diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index ea96a1845..6d300d513 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -27,7 +27,10 @@ use winit_integration::UserEvent; use crate::{ App, AppCreator, CreationContext, NativeOptions, Result, Storage, - native::{epi_integration::EpiIntegration, winit_integration::EventResult}, + native::{ + epi_integration::EpiIntegration, + winit_integration::{EventResult, is_invisible_or_minimized}, + }, }; use super::{epi_integration, event_loop_context, winit_integration, winit_integration::WinitApp}; @@ -778,10 +781,12 @@ impl WgpuWinitRunning<'_> { integration.maybe_autosave(app.as_mut(), window.map(|w| w.as_ref())); if let Some(window) = window - && window.is_minimized() == Some(true) + && is_invisible_or_minimized(window) { // On Mac, a minimized Window uses up all CPU: // https://github.com/emilk/egui/issues/325 + // On Windows, an invisible window also uses up all CPU: + // https://github.com/emilk/egui/issues/7776 profiling::scope!("minimized_sleep"); std::thread::sleep(std::time::Duration::from_millis(10)); } diff --git a/crates/eframe/src/native/winit_integration.rs b/crates/eframe/src/native/winit_integration.rs index 012c22f8e..b4ec62c09 100644 --- a/crates/eframe/src/native/winit_integration.rs +++ b/crates/eframe/src/native/winit_integration.rs @@ -9,6 +9,14 @@ use egui::ViewportId; #[cfg(feature = "accesskit")] use egui_winit::accesskit_winit; +/// Returns `true` if the window is invisible or minimized. +/// +/// These windows don't receive `RedrawRequested` events on Windows, +/// so they need special handling to keep processing viewport commands. +pub fn is_invisible_or_minimized(window: &Window) -> bool { + window.is_visible() == Some(false) || window.is_minimized() == Some(true) +} + /// Create an egui context, restoring it from storage if possible. pub fn create_egui_context(storage: Option<&dyn crate::Storage>) -> egui::Context { profiling::function_scope!(); From 0d065f9e78ae611ea57ff4159fcb74f186731cb6 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 24 Mar 2026 16:22:44 +0100 Subject: [PATCH 57/58] Add `Response::parent_id` and improve `warn_if_rect_changes_id` (#8010) Reduces the amount of false positives --------- Co-authored-by: Emil Ernerfeldt --- crates/egui/src/containers/area.rs | 1 + crates/egui/src/containers/window.rs | 1 + crates/egui/src/context.rs | 10 +++++ crates/egui/src/hit_test.rs | 1 + crates/egui/src/response.rs | 17 +++++++++ crates/egui/src/style.rs | 3 +- crates/egui/src/ui.rs | 4 ++ crates/egui/src/widget_rect.rs | 8 ++++ tests/egui_tests/tests/regression_tests.rs | 43 ++++++++++++++++++++++ 9 files changed, 87 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index 4333cf73a..d44c0ae41 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -516,6 +516,7 @@ impl Area { let move_response = ctx.create_widget( WidgetRect { id: interact_id, + parent_id: id, layer_id, rect: state.rect(), interact_rect: state.rect().intersect(constrain_rect), diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index c6b739589..ae5fbfc8f 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -962,6 +962,7 @@ fn do_resize_interaction( WidgetRect { layer_id, id, + parent_id: layer_id.id, rect, interact_rect: rect, sense: Sense::DRAG, // Don't use Sense::drag() since we don't want these to be focusable diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index af6c0e6d0..51a663ce5 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1360,6 +1360,7 @@ impl Context { let WidgetRect { id, + parent_id: _, layer_id, rect, interact_rect, @@ -4324,6 +4325,15 @@ fn warn_if_rect_changes_id( continue; } + // Only warn if at least one widget has the same parent_id in both frames. + // If all parent_ids changed too, this is a cascading id shift, not a widget bug. + if !prev_at_rect + .iter() + .any(|pw| new_at_rect.iter().any(|nw| nw.parent_id == pw.parent_id)) + { + continue; + } + let rect = new_at_rect[0].rect; log::warn!( diff --git a/crates/egui/src/hit_test.rs b/crates/egui/src/hit_test.rs index 8fe962b36..c7ffd7cda 100644 --- a/crates/egui/src/hit_test.rs +++ b/crates/egui/src/hit_test.rs @@ -450,6 +450,7 @@ mod tests { fn wr(id: Id, sense: Sense, rect: Rect) -> WidgetRect { WidgetRect { id, + parent_id: Id::NULL, layer_id: LayerId::background(), rect, interact_rect: rect, diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 241f5a381..a0dd6bd91 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -151,6 +151,22 @@ bitflags::bitflags! { } impl Response { + /// The [`Id`] of the parent [`crate::Ui`] that hosts this widget. + /// + /// Looks up the [`WidgetRect`] from the current (or previous) pass. + pub fn parent_id(&self) -> Id { + let id = self.ctx.viewport(|viewport| { + viewport + .this_pass + .widgets + .get(self.id) + .or_else(|| viewport.prev_pass.widgets.get(self.id)) + .map(|w| w.parent_id) + }); + debug_assert!(id.is_some(), "WidgetRect for Response not found!"); + id.unwrap_or(Id::NULL) + } + /// Returns true if this widget was clicked this frame by the primary button. /// /// A click is registered when the mouse or touch is released within @@ -761,6 +777,7 @@ impl Response { WidgetRect { layer_id: self.layer_id, id: self.id, + parent_id: self.parent_id(), rect: self.rect, interact_rect: self.interact_rect, sense: self.sense | sense, diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 5cd980f6c..4f9749663 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -1303,7 +1303,8 @@ pub struct DebugOptions { /// Show interesting widgets under the mouse cursor. pub show_widget_hits: bool, - /// Show a warning if the same `Rect` had different `Id` on the previous frame. + /// Show a warning if the same `Rect` had different `Id` and the same parent `Id` on the + /// previous frame. pub warn_if_rect_changes_id: bool, /// If true, highlight widgets that are not aligned to [`emath::GUI_ROUNDING`]. diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index f0b270951..d55f4174f 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -173,6 +173,7 @@ impl Ui { ui.ctx().create_widget( WidgetRect { id: ui.unique_id, + parent_id: ui.id, layer_id: ui.layer_id(), rect: start_rect, interact_rect: start_rect, @@ -339,6 +340,7 @@ impl Ui { child_ui.ctx().create_widget( WidgetRect { id: child_ui.unique_id, + parent_id: self.id, layer_id: child_ui.layer_id(), rect: start_rect, interact_rect: start_rect, @@ -1043,6 +1045,7 @@ impl Ui { self.ctx().create_widget( WidgetRect { id, + parent_id: self.id, layer_id: self.layer_id(), rect, interact_rect: self.clip_rect().intersect(rect), @@ -1112,6 +1115,7 @@ impl Ui { let mut response = self.ctx().create_widget( WidgetRect { id: self.unique_id, + parent_id: self.id, layer_id: self.layer_id(), rect: self.min_rect(), interact_rect: self.clip_rect().intersect(self.min_rect()), diff --git a/crates/egui/src/widget_rect.rs b/crates/egui/src/widget_rect.rs index a84dde519..b6fc9f7bb 100644 --- a/crates/egui/src/widget_rect.rs +++ b/crates/egui/src/widget_rect.rs @@ -17,6 +17,12 @@ pub struct WidgetRect { /// You can ensure globally unique ids using [`crate::Ui::push_id`]. pub id: Id, + /// The [`Id`] of the parent [`crate::Ui`] that hosts this widget. + /// + /// Used by debug checks to distinguish true id-instability from + /// cascading id shifts caused by a parent Ui's auto-id changing. + pub parent_id: Id, + /// What layer the widget is on. pub layer_id: LayerId, @@ -46,6 +52,7 @@ impl WidgetRect { pub fn transform(self, transform: emath::TSTransform) -> Self { let Self { id, + parent_id, layer_id, rect, interact_rect, @@ -54,6 +61,7 @@ impl WidgetRect { } = self; Self { id, + parent_id, layer_id, rect: transform * rect, interact_rect: transform * interact_rect, diff --git a/tests/egui_tests/tests/regression_tests.rs b/tests/egui_tests/tests/regression_tests.rs index 2d6ab5c67..f32ff7ff3 100644 --- a/tests/egui_tests/tests/regression_tests.rs +++ b/tests/egui_tests/tests/regression_tests.rs @@ -280,6 +280,49 @@ fn warn_if_rect_changes_id() { ); } +/// When a parent Ui's id changes (e.g. via `push_id` with a dynamic value), +/// all child widget ids shift too. This should NOT trigger `warn_if_rect_changes_id` because the +/// `parent_id` also changed β€” it's a cascading id shift, not a widget bug. +#[test] +fn warn_if_rect_changes_id_false_positive_parent_shift() { + use std::cell::Cell; + + let counter = Cell::new(0); + let button_rect = egui::Rect::from_min_size(egui::pos2(10.0, 10.0), egui::vec2(100.0, 30.0)); + + let mut harness = Harness::builder().with_size((200.0, 100.0)).build_ui(|ui| { + // push_id with a changing value causes the child Ui's id to shift, + // which in turn shifts all widget ids inside it. + ui.push_id(counter.get(), |ui| { + let id = ui.id().with("my_widget"); + let _response = ui.interact(button_rect, id, Sense::click()); + }); + }); + + // Frame 1: counter=0 β€” establishes prev_pass + harness.step(); + assert!( + !has_red_warning_rect(harness.output()), + "Should not warn on first frame" + ); + + // Frame 2: counter=0 β€” prev_pass == this_pass + harness.step(); + assert!( + !has_red_warning_rect(harness.output()), + "Should not warn when nothing changed" + ); + + // Now change the parent id, shifting all child widget ids + counter.set(1); + harness.step(); + + assert!( + !has_red_warning_rect(harness.output()), + "Should NOT warn when parent Ui's id shifted (cascading id change)" + ); +} + #[test] fn horizontal_wrapped_multiline_row_height() { let mut harness = Harness::builder().with_size((350.0, 300.0)).build_ui(|ui| { From 4feac890aa34b45e36c3ba8ab3a6e9296dade25f Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 24 Mar 2026 17:33:20 +0100 Subject: [PATCH 58/58] Respect `WidgetVisuals::expansion` in TextEdit (#8013) This broke in #7587 --------- Co-authored-by: Emil Ernerfeldt --- crates/egui/src/widgets/text_edit/builder.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 1f103d2f8..ef668a02e 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -707,7 +707,11 @@ impl TextEdit<'_> { .frame .fill(background_color) .corner_radius(corner_radius) - .inner_margin(allocated.frame.inner_margin - Margin::same(stroke.width as i8)) + .inner_margin( + allocated.frame.inner_margin + + Margin::same((visuals.expansion - stroke.width).round() as i8), + ) + .outer_margin(Margin::same(-(visuals.expansion as i8))) .stroke(stroke) } else { allocated.frame