From 5941a27ed4c24384dad36b3083e98216a2e290bf Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 13 Mar 2025 11:29:37 +0100 Subject: [PATCH 001/129] Update ring (#5786) Upate ring to latest patch, should fix ci failures --- Cargo.lock | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3a25dcff3..a7f57e574 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -713,9 +713,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.1.31" +version = "1.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" +checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" dependencies = [ "jobserver", "libc", @@ -2399,7 +2399,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] [[package]] @@ -3435,15 +3435,14 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.8" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", "getrandom", "libc", - "spin", "untrusted", "windows-sys 0.52.0", ] @@ -3746,12 +3745,6 @@ dependencies = [ "serde", ] -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - [[package]] name = "spirv" version = "0.3.0+sdk-1.3.268.0" From d811940dcc59a0d863e30d86e35bd41df0d9dee9 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 13 Mar 2025 11:45:29 +0100 Subject: [PATCH 002/129] Allow unmaintained paste crate (#5787) Should fix cargo deny CI --- deny.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/deny.toml b/deny.toml index 3e8a63371..19f353096 100644 --- a/deny.toml +++ b/deny.toml @@ -31,7 +31,8 @@ all-features = true [advisories] version = 2 ignore = [ - "RUSTSEC-2024-0320", # unmaintaines yaml-rust pulled in by syntect + "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 ] [bans] From eb7ca72534f678c7ed7bea8f080e850c0b0f7eaa Mon Sep 17 00:00:00 2001 From: Adrian Blumer Date: Mon, 17 Mar 2025 02:00:47 -0700 Subject: [PATCH 003/129] Fix`TextEdit` selection when placed in a `Scene`. (#5791) * Closes https://github.com/emilk/egui/issues/5789 * [x] I have followed the instructions in the PR template While this change fixes the TextEdit specific issue, I'm worried that the underlying problem is more fundamental and could show up in other widgets, and I'm wondering if there's a more general solution? --- crates/egui/src/widgets/text_edit/builder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index b3ac4cd8b..6350b202c 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -578,7 +578,7 @@ impl TextEdit<'_> { let painter = ui.painter_at(text_clip_rect.expand(1.0)); // expand to avoid clipping cursor if interactive { - if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() { + if let Some(pointer_pos) = response.interact_pointer_pos() { if response.hovered() && text.is_mutable() { ui.output_mut(|o| o.mutable_text_under_cursor = true); } From 024dc7b135a978abf83c90d260679e3aaa22b2a5 Mon Sep 17 00:00:00 2001 From: Sven Niederberger <73159570+s-nie@users.noreply.github.com> Date: Tue, 18 Mar 2025 11:13:59 +0100 Subject: [PATCH 004/129] Add a toggle for the compact menu style (#5777) Menus currently have their own style that removes outlines and backgrounds. This is nice if the menu only contains buttons. However if the menu contains other widgets, e.g. a drag value, the style change makes it quite difficult to identify such widgets. This is a simple way to make this configurable. * [x] I have followed the instructions in the PR template --- crates/egui/src/menu.rs | 12 +++++++----- crates/egui/src/style.rs | 7 +++++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/crates/egui/src/menu.rs b/crates/egui/src/menu.rs index fa99cebbd..ef84451db 100644 --- a/crates/egui/src/menu.rs +++ b/crates/egui/src/menu.rs @@ -76,11 +76,13 @@ impl std::ops::DerefMut for BarState { } fn set_menu_style(style: &mut Style) { - style.spacing.button_padding = vec2(2.0, 0.0); - style.visuals.widgets.active.bg_stroke = Stroke::NONE; - style.visuals.widgets.hovered.bg_stroke = Stroke::NONE; - style.visuals.widgets.inactive.weak_bg_fill = Color32::TRANSPARENT; - style.visuals.widgets.inactive.bg_stroke = Stroke::NONE; + if style.compact_menu_style { + style.spacing.button_padding = vec2(2.0, 0.0); + style.visuals.widgets.active.bg_stroke = Stroke::NONE; + style.visuals.widgets.hovered.bg_stroke = Stroke::NONE; + style.visuals.widgets.inactive.weak_bg_fill = Color32::TRANSPARENT; + style.visuals.widgets.inactive.bg_stroke = Stroke::NONE; + } } /// The menu bar goes well in a [`crate::TopBottomPanel::top`], diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 4d227f63b..d0b368f0a 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -332,6 +332,9 @@ pub struct Style { /// The animation that should be used when scrolling a [`crate::ScrollArea`] using e.g. [`Ui::scroll_to_rect`]. pub scroll_animation: ScrollAnimation, + + /// Use a more compact style for menus. + pub compact_menu_style: bool, } #[test] @@ -1277,6 +1280,7 @@ impl Default for Style { url_in_tooltip: false, always_scroll_the_only_direction: false, scroll_animation: ScrollAnimation::default(), + compact_menu_style: true, } } } @@ -1578,6 +1582,7 @@ impl Style { url_in_tooltip, always_scroll_the_only_direction, scroll_animation, + compact_menu_style, } = self; crate::Grid::new("_options").show(ui, |ui| { @@ -1683,6 +1688,8 @@ impl Style { #[cfg(debug_assertions)] ui.collapsing("šŸ› Debug", |ui| debug.ui(ui)); + ui.checkbox(compact_menu_style, "Compact menu style"); + ui.checkbox(explanation_tooltips, "Explanation tooltips") .on_hover_text( "Show explanatory text when hovering DragValue:s and other egui widgets", From d698365dac9e747315cc12e020f36b87c0a24d82 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 18 Mar 2025 11:29:20 +0100 Subject: [PATCH 005/129] Fix the `StyleModifier` not being passed from popup to menu (#5805) * [x] I have followed the instructions in the PR template --- crates/egui/src/containers/popup.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index 4a457bf03..700500620 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -556,7 +556,9 @@ impl<'a> Popup<'a> { .info(info.unwrap_or_else(|| { UiStackInfo::new(kind.into()).with_tag_value( MenuConfig::MENU_CONFIG_TAG, - MenuConfig::new().close_behavior(close_behavior), + MenuConfig::new() + .close_behavior(close_behavior) + .style(style.clone()), ) })); From cf756df14d0fff46adf5547fe7209c61a3fdbc66 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 18 Mar 2025 11:30:32 +0100 Subject: [PATCH 006/129] Clarify what happens when multiple modals are shown in the same frame (#5800) * Closes #5788 * [x] I have followed the instructions in the PR template --- crates/egui/src/containers/modal.rs | 8 +++++++- crates/egui/src/containers/popup.rs | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/containers/modal.rs b/crates/egui/src/containers/modal.rs index 218bfce36..2edc628e9 100644 --- a/crates/egui/src/containers/modal.rs +++ b/crates/egui/src/containers/modal.rs @@ -4,11 +4,14 @@ use crate::{ use emath::{Align2, Vec2}; /// A modal dialog. +/// /// Similar to a [`crate::Window`] but centered and with a backdrop that /// blocks input to the rest of the UI. /// /// You can show multiple modals on top of each other. The topmost modal will always be /// the most recently shown one. +/// If multiple modals are newly shown in the same frame, the order of the modals not undefined +/// (either first or second could be top). pub struct Modal { pub area: Area, pub backdrop_color: Color32, @@ -16,7 +19,9 @@ pub struct Modal { } impl Modal { - /// Create a new Modal. The id is passed to the area. + /// Create a new Modal. + /// + /// The id is passed to the area. pub fn new(id: Id) -> Self { Self { area: Self::default_area(id), @@ -26,6 +31,7 @@ impl Modal { } /// Returns an area customized for a modal. + /// /// Makes these changes to the default area: /// - sense: hover /// - anchor: center diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index 700500620..a2b668ff5 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -8,6 +8,7 @@ use emath::{vec2, Align, Pos2, Rect, RectAlign, Vec2}; use std::iter::once; /// What should we anchor the popup to? +/// /// The final position for the popup will be calculated based on [`RectAlign`] /// and can be customized with [`Popup::align`] and [`Popup::align_alternatives`]. /// [`PopupAnchor`] is the parent rect of [`RectAlign`]. From d3c1ac37989671131817dcc1a41d1731f0f7714d Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 18 Mar 2025 11:32:18 +0100 Subject: [PATCH 007/129] Fix `context_menu` not closing when clicking widget (#5799) The rerun timeline context menu wouldn't close when clicking outside, this fixes it --- crates/egui/src/containers/popup.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index a2b668ff5..4d866cfd7 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -273,11 +273,15 @@ impl<'a> Popup<'a> { /// In contrast to [`Self::menu`], this will open at the pointer position. pub fn context_menu(response: &Response) -> Self { Self::menu(response) - .open_memory( - response - .secondary_clicked() - .then_some(SetOpenCommand::Bool(true)), - ) + .open_memory(if response.secondary_clicked() { + Some(SetOpenCommand::Bool(true)) + } else if response.clicked() { + // Explicitly close the menu if the widget was clicked + // Without this, the context menu would stay open if the user clicks the widget + Some(SetOpenCommand::Bool(false)) + } else { + None + }) .at_pointer_fixed() } From 626cd9e2274847dc182ef270db5e5e1ec59cd18f Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 18 Mar 2025 11:40:33 +0100 Subject: [PATCH 008/129] Fix `Response::clicked_elsewhere` not returning true sometimes (#5798) * Closes #5794 --- crates/egui/src/response.rs | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index c3767f8ab..a1b522efc 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -222,25 +222,32 @@ impl Response { /// /// Clicks on widgets contained in this one counts as clicks inside this widget, /// so that clicking a button in an area will not be considered as clicking "elsewhere" from the area. + /// + /// Clicks on other layers above this widget *will* be considered as clicking elsewhere. pub fn clicked_elsewhere(&self) -> bool { + let (pointer_interact_pos, any_click) = self + .ctx + .input(|i| (i.pointer.interact_pos(), i.pointer.any_click())); + // We do not use self.clicked(), because we want to catch all clicks within our frame, // even if we aren't clickable (or even enabled). // This is important for windows and such that should close then the user clicks elsewhere. - self.ctx.input(|i| { - let pointer = &i.pointer; - - if pointer.any_click() { - if self.contains_pointer() || self.hovered() { - false - } else if let Some(pos) = pointer.interact_pos() { - !self.interact_rect.contains(pos) + if any_click { + if self.contains_pointer() || self.hovered() { + false + } else if let Some(pos) = pointer_interact_pos { + let layer_under_pointer = self.ctx.layer_id_at(pos); + if layer_under_pointer != Some(self.layer_id) { + true } else { - false // clicked without a pointer, weird + !self.interact_rect.contains(pos) } } else { - false + false // clicked without a pointer, weird } - }) + } else { + false + } } /// Was the widget enabled? From 9604dae2299f49b0803ca5e257df4c0f2862b717 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 18 Mar 2025 11:41:53 +0100 Subject: [PATCH 009/129] Fix kinetic scrolling on touch devices (#5778) Fixes kinetic scrolling on android (and possibly other touch devices), by calculating the final velocity before clearing the position history on PointerGone events. * Closes #5311 * [X] I have followed the instructions in the PR template --- crates/egui/src/input_state/mod.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/input_state/mod.rs b/crates/egui/src/input_state/mod.rs index 7864ce627..bc32528d1 100644 --- a/crates/egui/src/input_state/mod.rs +++ b/crates/egui/src/input_state/mod.rs @@ -927,6 +927,7 @@ impl PointerState { self.motion = Some(Vec2::ZERO); } + let mut clear_history_after_velocity_calculation = false; for event in &new.events { match event { Event::PointerMoved(pos) => { @@ -1013,7 +1014,10 @@ impl PointerState { // When dragging a slider and the mouse leaves the viewport, we still want the drag to work, // so we don't treat this as a `PointerEvent::Released`. // NOTE: we do NOT clear `self.interact_pos` here. It will be cleared next frame. - self.pos_history.clear(); + + // Delay the clearing until after the final velocity calculation, so we can + // get the final velocity when `drag_stopped` is true. + clear_history_after_velocity_calculation = true; } Event::MouseMoved(delta) => *self.motion.get_or_insert(Vec2::ZERO) += *delta, _ => {} @@ -1044,6 +1048,9 @@ impl PointerState { if self.velocity != Vec2::ZERO { self.last_move_time = time; } + if clear_history_after_velocity_calculation { + self.pos_history.clear(); + } self.direction = self.pos_history.velocity().unwrap_or_default().normalized(); From 6b38fd39a1b1f63da0754bbe90c87a66c5819319 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 18 Mar 2025 11:43:15 +0100 Subject: [PATCH 010/129] Use egui_demo_lib in android example (#5780) * [x] I have followed the instructions in the PR template --- Cargo.lock | 1 + examples/hello_android/Cargo.toml | 8 +++- examples/hello_android/README.md | 6 ++- examples/hello_android/screenshot.png | 4 +- examples/hello_android/src/lib.rs | 59 ++++++++++----------------- examples/hello_android/src/main.rs | 9 ++++ 6 files changed, 46 insertions(+), 41 deletions(-) create mode 100644 examples/hello_android/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index a7f57e574..b562a1ecb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1973,6 +1973,7 @@ version = "0.1.0" dependencies = [ "android_logger", "eframe", + "egui_demo_lib", "egui_extras", "log", "winit", diff --git a/examples/hello_android/Cargo.toml b/examples/hello_android/Cargo.toml index b3bf4407e..ddeaea9dc 100644 --- a/examples/hello_android/Cargo.toml +++ b/examples/hello_android/Cargo.toml @@ -12,11 +12,13 @@ publish = false # workspace = true [lib] -crate-type = ["cdylib"] +# cdylib is required for Android, lib is required for desktop +crate-type = ["cdylib", "lib"] [dependencies] eframe = { workspace = true, features = ["default", "android-native-activity"] } +egui_demo_lib = { workspace = true, features = ["chrono"] } # For image support: egui_extras = { workspace = true, features = ["default", "image"] } @@ -27,3 +29,7 @@ android_logger = "0.14" [package.metadata.android] build_targets = ["armv7-linux-androideabi", "aarch64-linux-android"] + +[package.metadata.android.sdk] +min_sdk_version = 23 +target_sdk_version = 35 diff --git a/examples/hello_android/README.md b/examples/hello_android/README.md index fe14eb9fa..6ad26348a 100644 --- a/examples/hello_android/README.md +++ b/examples/hello_android/README.md @@ -14,7 +14,11 @@ cargo install \ Build and run: ```sh -cargo apk run -p hello_android +# Run on android +cargo apk run -p hello_android --lib + +# Run on your desktop +cargo run -p hello_android ``` ![](screenshot.png) diff --git a/examples/hello_android/screenshot.png b/examples/hello_android/screenshot.png index 91179fa2f..ff376284a 100644 --- a/examples/hello_android/screenshot.png +++ b/examples/hello_android/screenshot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7add91d7d6b73f48e98f20d84cba3bd3a950cf97aa31f5e9fa93da9af98e876c -size 120019 +oid sha256:16bb465d73b7cf8133aee8cdb773a10d213ad23359a21c0bc2af3e4f9893057f +size 507047 diff --git a/examples/hello_android/src/lib.rs b/examples/hello_android/src/lib.rs index adda66ca5..c91c0e754 100644 --- a/examples/hello_android/src/lib.rs +++ b/examples/hello_android/src/lib.rs @@ -1,15 +1,14 @@ -#![cfg(target_os = "android")] -#![allow(rustdoc::missing_crate_level_docs)] // it's an example +#![doc = include_str!("../README.md")] -use android_logger::Config; -use eframe::egui; -use log::LevelFilter; -use winit::platform::android::activity::AndroidApp; +use eframe::{egui, CreationContext}; +#[cfg(target_os = "android")] #[no_mangle] -fn android_main(app: AndroidApp) { +fn android_main(app: winit::platform::android::activity::AndroidApp) { // Log to android output - android_logger::init_once(Config::default().with_max_level(LevelFilter::Info)); + android_logger::init_once( + android_logger::Config::default().with_max_level(log::LevelFilter::Info), + ); let options = eframe::NativeOptions { android_app: Some(app), @@ -18,48 +17,34 @@ fn android_main(app: AndroidApp) { eframe::run_native( "My egui App", options, - Box::new(|cc| { - // This gives us image support: - egui_extras::install_image_loaders(&cc.egui_ctx); - - Ok(Box::::default()) - }), + Box::new(|cc| Ok(Box::new(MyApp::new(cc)))), ) .unwrap() } -struct MyApp { - name: String, - age: u32, +pub struct MyApp { + demo: egui_demo_lib::DemoWindows, } -impl Default for MyApp { - fn default() -> Self { +impl MyApp { + pub fn new(cc: &CreationContext) -> Self { + egui_extras::install_image_loaders(&cc.egui_ctx); Self { - name: "Arthur".to_owned(), - age: 42, + demo: egui_demo_lib::DemoWindows::default(), } } } impl eframe::App for MyApp { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { - egui::CentralPanel::default().show(ctx, |ui| { - ui.heading("My egui Application"); - ui.horizontal(|ui| { - let name_label = ui.label("Your name: "); - ui.text_edit_singleline(&mut self.name) - .labelled_by(name_label.id); - }); - ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age")); - if ui.button("Increment").clicked() { - self.age += 1; - } - ui.label(format!("Hello '{}', age {}", self.name, self.age)); - - ui.image(egui::include_image!( - "../../../crates/egui/assets/ferris.png" - )); + // Reserve some space at the top so the demo ui isn't hidden behind the android status bar + // TODO(lucasmerlin): This is a pretty big hack, should be fixed once safe_area implemented + // for android: + // https://github.com/rust-windowing/winit/issues/3910 + egui::TopBottomPanel::top("status_bar_space").show(ctx, |ui| { + ui.set_height(32.0); }); + + self.demo.ui(ctx); } } diff --git a/examples/hello_android/src/main.rs b/examples/hello_android/src/main.rs new file mode 100644 index 000000000..e3cdb05ad --- /dev/null +++ b/examples/hello_android/src/main.rs @@ -0,0 +1,9 @@ +use hello_android::MyApp; + +fn main() -> eframe::Result { + eframe::run_native( + "hello_android", + Default::default(), + Box::new(|cc| Ok(Box::new(MyApp::new(cc)))), + ) +} From 1aced06e47c310241b197cf3b1fc449cd621bccb Mon Sep 17 00:00:00 2001 From: Markus Krause Date: Tue, 18 Mar 2025 11:51:00 +0100 Subject: [PATCH 011/129] refactor mime type support detection in image loader to allow for deferred handling and appended encoding info (#5686) as recommended by @lucasmerlin in #5679 * Closes #5679 * [x] I have followed the instructions in the PR template --- crates/egui_extras/src/loaders/image_loader.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/crates/egui_extras/src/loaders/image_loader.rs b/crates/egui_extras/src/loaders/image_loader.rs index a2a6fb1df..ab3c9179d 100644 --- a/crates/egui_extras/src/loaders/image_loader.rs +++ b/crates/egui_extras/src/loaders/image_loader.rs @@ -36,11 +36,21 @@ fn is_supported_uri(uri: &str) -> bool { } fn is_supported_mime(mime: &str) -> bool { - // This is the default mime type for binary files, so this might actually be a valid image, - // let's relay on image's format guessing - if mime == "application/octet-stream" { - return true; + // some mime types e.g. reflect binary files or mark the content as a download, which + // may be a valid image or not, in this case, defer the decision on the format guessing + // or the image crate and return true here + let mimes_to_defer = [ + "application/octet-stream", + "application/x-msdownload", + "application/force-download", + ]; + for m in &mimes_to_defer { + // use contains instead of direct equality, as e.g. encoding info might be appended + if mime.contains(m) { + return true; + } } + // Uses only the enabled image crate features ImageFormat::all() .filter(ImageFormat::reading_enabled) From a2afc8d092a4e3f0b17675b114db8fad08810947 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 18 Mar 2025 15:32:14 +0100 Subject: [PATCH 012/129] Make `close_popup` take an `Id` and add `close_all_popups` (#5820) Ooops, fixes a regression introduced in #5799 * [x] I have followed the instructions in the PR template --- crates/egui/src/containers/popup.rs | 6 +++--- crates/egui/src/memory/mod.rs | 15 ++++++++++++--- crates/egui/src/widgets/color_picker.rs | 2 +- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index 4d866cfd7..7c72d2dd3 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -85,7 +85,7 @@ pub enum PopupCloseBehavior { /// but in the popup's body CloseOnClickOutside, - /// Clicks will be ignored. Popup might be closed manually by calling [`crate::Memory::close_popup`] + /// Clicks will be ignored. Popup might be closed manually by calling [`crate::Memory::close_all_popups`] /// or by pressing the escape button IgnoreClicks, } @@ -524,7 +524,7 @@ impl<'a> Popup<'a> { _ => mem.open_popup(id), } } else { - mem.close_popup(); + mem.close_popup(id); } } Some(SetOpenCommand::Toggle) => { @@ -606,7 +606,7 @@ impl<'a> Popup<'a> { } OpenKind::Memory { .. } => { if should_close { - ctx.memory_mut(|mem| mem.close_popup()); + ctx.memory_mut(|mem| mem.close_popup(id)); } } } diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index d38e39f75..c68ba4da5 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -1098,17 +1098,26 @@ impl Memory { .and_then(|(popup_id, pos)| if popup_id == id { pos } else { None }) } - /// Close the open popup, if any. - pub fn close_popup(&mut self) { + /// Close any currently open popup. + pub fn close_all_popups(&mut self) { self.popup = None; } + /// Close the given popup, if it is open. + /// + /// See also [`Self::close_all_popups`] if you want to close any / all currently open popups. + pub fn close_popup(&mut self, popup_id: Id) { + if self.is_popup_open(popup_id) { + self.popup = None; + } + } + /// Toggle the given popup between closed and open. /// /// Note: At most, only one popup can be open at a time. pub fn toggle_popup(&mut self, popup_id: Id) { if self.is_popup_open(popup_id) { - self.close_popup(); + self.close_popup(popup_id); } else { self.open_popup(popup_id); } diff --git a/crates/egui/src/widgets/color_picker.rs b/crates/egui/src/widgets/color_picker.rs index a9906cef1..500fb0b88 100644 --- a/crates/egui/src/widgets/color_picker.rs +++ b/crates/egui/src/widgets/color_picker.rs @@ -521,7 +521,7 @@ pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> Res if !button_response.clicked() && (ui.input(|i| i.key_pressed(Key::Escape)) || area_response.clicked_elsewhere()) { - ui.memory_mut(|mem| mem.close_popup()); + ui.memory_mut(|mem| mem.close_popup(popup_id)); } } From 93c06c34053fc5416e56aab5110cde210052afdf Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Wed, 19 Mar 2025 09:33:17 +0100 Subject: [PATCH 013/129] Add color picker menu item example (#5755) * To be merged after #5756 https://github.com/user-attachments/assets/ad40c172-cad2-4cc5-a3ef-3284b6545e94 --- crates/ecolor/src/color32.rs | 8 ++ crates/egui_demo_lib/src/demo/popups.rs | 126 ++++++++++++++---------- 2 files changed, 82 insertions(+), 52 deletions(-) diff --git a/crates/ecolor/src/color32.rs b/crates/ecolor/src/color32.rs index 87f4915cf..e72a3b98f 100644 --- a/crates/ecolor/src/color32.rs +++ b/crates/ecolor/src/color32.rs @@ -295,6 +295,14 @@ impl Color32 { pub fn blend(self, on_top: Self) -> Self { self.gamma_multiply_u8(255 - on_top.a()) + on_top } + + /// Intensity of the color. + /// + /// Returns a value in the range 0-1. + /// The brighter the color, the closer to 1. + pub fn intensity(&self) -> f32 { + (self.r() as f32 * 0.299 + self.g() as f32 * 0.587 + self.b() as f32 * 0.114) / 255.0 + } } impl std::ops::Mul for Color32 { diff --git a/crates/egui_demo_lib/src/demo/popups.rs b/crates/egui_demo_lib/src/demo/popups.rs index 4cf689d3c..f9815a6bf 100644 --- a/crates/egui_demo_lib/src/demo/popups.rs +++ b/crates/egui_demo_lib/src/demo/popups.rs @@ -1,8 +1,9 @@ use crate::rust_view_ui; +use egui::color_picker::{color_picker_color32, Alpha}; use egui::containers::menu::{MenuConfig, SubMenuButton}; use egui::{ include_image, Align, Align2, ComboBox, Frame, Id, Layout, Popup, PopupCloseBehavior, - RectAlign, Tooltip, Ui, UiBuilder, + RectAlign, RichText, Tooltip, Ui, UiBuilder, }; /// Showcase [`Popup`]. @@ -16,6 +17,7 @@ pub struct PopupsDemo { close_behavior: PopupCloseBehavior, popup_open: bool, checked: bool, + color: egui::Color32, } impl PopupsDemo { @@ -25,6 +27,74 @@ impl PopupsDemo { .gap(self.gap) .close_behavior(self.close_behavior) } + + fn nested_menus(&mut self, ui: &mut Ui) { + ui.set_max_width(200.0); // To make sure we wrap long text + + if ui.button("Open…").clicked() { + ui.close(); + } + ui.menu_button("Popups can have submenus", |ui| { + ui.menu_button("SubMenu", |ui| { + if ui.button("Open…").clicked() { + ui.close(); + } + let _ = ui.button("Item"); + ui.menu_button("Recursive", |ui| self.nested_menus(ui)); + }); + ui.menu_button("SubMenu", |ui| { + if ui.button("Open…").clicked() { + ui.close(); + } + let _ = ui.button("Item"); + }); + let _ = ui.button("Item"); + if ui.button("Open…").clicked() { + ui.close(); + } + }); + ui.menu_image_text_button( + include_image!("../../data/icon.png"), + "I have an icon!", + |ui| { + let _ = ui.button("Item1"); + let _ = ui.button("Item2"); + let _ = ui.button("Item3"); + let _ = ui.button("Item4"); + if ui.button("Open…").clicked() { + ui.close(); + } + }, + ); + let _ = ui.button("Very long text for this item that should be wrapped"); + SubMenuButton::new("Always CloseOnClickOutside") + .config(MenuConfig::new().close_behavior(PopupCloseBehavior::CloseOnClickOutside)) + .ui(ui, |ui| { + ui.checkbox(&mut self.checked, "Checkbox"); + + // Customized color SubMenuButton + let is_bright = self.color.intensity() > 0.5; + let text_color = if is_bright { + egui::Color32::BLACK + } else { + egui::Color32::WHITE + }; + let mut color_button = + SubMenuButton::new(RichText::new("Background").color(text_color)); + color_button.button = color_button.button.fill(self.color); + color_button.button = color_button + .button + .right_text(RichText::new(SubMenuButton::RIGHT_ARROW).color(text_color)); + color_button.ui(ui, |ui| { + ui.spacing_mut().slider_width = 200.0; + color_picker_color32(ui, &mut self.color, Alpha::Opaque); + }); + + if ui.button("Open…").clicked() { + ui.close(); + } + }); + } } impl Default for PopupsDemo { @@ -35,6 +105,7 @@ impl Default for PopupsDemo { close_behavior: PopupCloseBehavior::CloseOnClick, popup_open: false, checked: false, + color: egui::Color32::RED, } } } @@ -57,55 +128,6 @@ impl crate::Demo for PopupsDemo { } } -fn nested_menus(ui: &mut egui::Ui, checked: &mut bool) { - ui.set_max_width(200.0); // To make sure we wrap long text - - if ui.button("Open…").clicked() { - ui.close(); - } - ui.menu_button("Popups can have submenus", |ui| { - ui.menu_button("SubMenu", |ui| { - if ui.button("Open…").clicked() { - ui.close(); - } - let _ = ui.button("Item"); - ui.menu_button("Recursive", |ui| nested_menus(ui, checked)); - }); - ui.menu_button("SubMenu", |ui| { - if ui.button("Open…").clicked() { - ui.close(); - } - let _ = ui.button("Item"); - }); - let _ = ui.button("Item"); - if ui.button("Open…").clicked() { - ui.close(); - } - }); - ui.menu_image_text_button( - include_image!("../../data/icon.png"), - "I have an icon!", - |ui| { - let _ = ui.button("Item1"); - let _ = ui.button("Item2"); - let _ = ui.button("Item3"); - let _ = ui.button("Item4"); - if ui.button("Open…").clicked() { - ui.close(); - } - }, - ); - let _ = ui.button("Very long text for this item that should be wrapped"); - SubMenuButton::new("Always CloseOnClickOutside") - .config(MenuConfig::new().close_behavior(PopupCloseBehavior::CloseOnClickOutside)) - .ui(ui, |ui| { - ui.checkbox(checked, "Checkbox"); - if ui.button("Open…").clicked() { - ui.close(); - } - }); -} - impl crate::View for PopupsDemo { fn ui(&mut self, ui: &mut egui::Ui) { let response = Frame::group(ui.style()) @@ -117,10 +139,10 @@ impl crate::View for PopupsDemo { .inner; self.apply_options(Popup::menu(&response).id(Id::new("menu"))) - .show(|ui| nested_menus(ui, &mut self.checked)); + .show(|ui| self.nested_menus(ui)); self.apply_options(Popup::context_menu(&response).id(Id::new("context_menu"))) - .show(|ui| nested_menus(ui, &mut self.checked)); + .show(|ui| self.nested_menus(ui)); if self.popup_open { self.apply_options(Popup::from_response(&response).id(Id::new("popup"))) From f408ccafbce72c2e32afc9d17e1c16ba04e7f09e Mon Sep 17 00:00:00 2001 From: MStarha <59487310+MStarha@users.noreply.github.com> Date: Thu, 20 Mar 2025 10:40:22 +0100 Subject: [PATCH 014/129] Fix `DragValue` expansion when editing (#5809) * [x] I have followed the instructions in the PR template This PR fixes an issue, where `DragValue` would expand when it enters editing state. There were 3 contributing problems: - A workaround introduced in #4276 caused the `DragValue` to report incorrect outer size. - The `DragValue` uses `TextEdit` internally and sets both `min_size` and `desired_width` to the same value - desired width is used **before** padding is applied - this is in contrast to `Button` (also used internally by `DragValue`), which only uses `min_size`. This caused the `DragValue` to expand horizontally by the size of button padding. - The height of the `TextEdit` is (among other things) determined by the height of the row, which was not present in `Button`. This caused a small vertical expansion, when the contents (including padding) were larger than the `min_size`. ![egui_drag_value_expansion](https://github.com/user-attachments/assets/b6fe9bd8-5755-4a43-8b61-a7e5b24e678d) Here the dimensions set in code are: - padding: 20 x 20 pt - interact size: 80 x 30 pt *Note: I do not know what's up with the tests. When I ran the check script, they were failing because of 3 UI missmatches, so I updated the snapshots. Now, the updated snapshots cause the same failure in CI, that appeared locally before the update. Now the locally run tests fail with `The platform you're compiling for is not supported by winit` and couple more following errors coming from the same source (`winit 0.30.7`).* --- crates/egui/src/widgets/button.rs | 33 +++++++++++++++----- crates/egui/src/widgets/drag_value.rs | 4 ++- crates/egui/src/widgets/text_edit/builder.rs | 11 ++----- 3 files changed, 31 insertions(+), 17 deletions(-) diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index dcf37d203..c0701c193 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -1,6 +1,6 @@ use crate::{ - widgets, Align, Color32, CornerRadius, Image, NumExt, Rect, Response, Sense, Stroke, TextStyle, - TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetType, + widgets, Align, Color32, CornerRadius, FontSelection, Image, NumExt, Rect, Response, Sense, + Stroke, TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetType, }; /// Clickable button with text. @@ -224,6 +224,16 @@ impl Widget for Button<'_> { let frame = frame.unwrap_or_else(|| ui.visuals().button_frame); + let default_font_height = || { + let font_selection = FontSelection::default(); + let font_id = font_selection.resolve(ui.style()); + ui.fonts(|f| f.row_height(&font_id)) + }; + + let text_font_height = ui + .fonts(|fonts| text.as_ref().map(|wt| wt.font_height(fonts, ui.style()))) + .unwrap_or_else(default_font_height); + let mut button_padding = if frame { ui.spacing().button_padding } else { @@ -233,11 +243,17 @@ impl Widget for Button<'_> { button_padding.y = 0.0; } - let space_available_for_image = if let Some(text) = &text { + let (space_available_for_image, right_text_font_height) = if let Some(text) = &text { let font_height = ui.fonts(|fonts| text.font_height(fonts, ui.style())); - Vec2::splat(font_height) // Reasonable? + ( + Vec2::splat(font_height), // Reasonable? + font_height, + ) } else { - ui.available_size() - 2.0 * button_padding + ( + ui.available_size() - 2.0 * button_padding, + default_font_height(), + ) }; let image_size = if let Some(image) = &image { @@ -283,11 +299,14 @@ impl Widget for Button<'_> { } if let Some(galley) = &galley { desired_size.x += galley.size().x; - desired_size.y = desired_size.y.max(galley.size().y); + desired_size.y = desired_size.y.max(galley.size().y).max(text_font_height); } if let Some(right_galley) = &right_galley { desired_size.x += gap_before_right_text + right_galley.size().x; - desired_size.y = desired_size.y.max(right_galley.size().y); + desired_size.y = desired_size + .y + .max(right_galley.size().y) + .max(right_text_font_height); } desired_size += 2.0 * button_padding; if !small { diff --git a/crates/egui/src/widgets/drag_value.rs b/crates/egui/src/widgets/drag_value.rs index 846ab72f0..1cf5ba7bc 100644 --- a/crates/egui/src/widgets/drag_value.rs +++ b/crates/egui/src/widgets/drag_value.rs @@ -562,7 +562,9 @@ impl Widget for DragValue<'_> { .margin(ui.spacing().button_padding) .min_size(ui.spacing().interact_size) .id(id) - .desired_width(ui.spacing().interact_size.x) + .desired_width( + ui.spacing().interact_size.x - 2.0 * ui.spacing().button_padding.x, + ) .font(text_style), ); diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 6350b202c..7d5c23f65 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -426,18 +426,11 @@ impl TextEdit<'_> { let background_color = self .background_color .unwrap_or(ui.visuals().extreme_bg_color); - let margin = self.margin; - let mut output = self.show_content(ui); - - // TODO(emilk): return full outer_rect in `TextEditOutput`. - // Can't do it now because this fix is ging into a patch release. - let outer_rect = output.response.rect; - let inner_rect = outer_rect - margin; - output.response.rect = inner_rect; + let output = self.show_content(ui); if frame { let visuals = ui.style().interact(&output.response); - let frame_rect = outer_rect.expand(visuals.expansion); + let frame_rect = output.response.rect.expand(visuals.expansion); let shape = if is_mutable { if output.response.has_focus() { epaint::RectShape::new( From 6a8ee29a4e5466fc6278bb57723736830207e900 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C6=B0=C6=A1ng=20Ho=C3=A0ng=20Long?= Date: Thu, 20 Mar 2025 10:45:16 +0100 Subject: [PATCH 015/129] Fix disabled `DragValue` eating focus, causing focus to reset (#5826) * Closes https://github.com/emilk/egui/issues/5507 * [x] I have followed the instructions in the PR template --- crates/egui/src/widgets/drag_value.rs | 9 ++++--- crates/egui_kittest/tests/regression_tests.rs | 25 +++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/crates/egui/src/widgets/drag_value.rs b/crates/egui/src/widgets/drag_value.rs index 1cf5ba7bc..f0f35c1e9 100644 --- a/crates/egui/src/widgets/drag_value.rs +++ b/crates/egui/src/widgets/drag_value.rs @@ -451,10 +451,11 @@ impl Widget for DragValue<'_> { // it is immediately rendered in edit mode, rather than being rendered // in button mode for just one frame. This is important for // screen readers. - let is_kb_editing = ui.memory_mut(|mem| { - mem.interested_in_focus(id, ui.layer_id()); - mem.has_focus(id) - }); + let is_kb_editing = ui.is_enabled() + && ui.memory_mut(|mem| { + mem.interested_in_focus(id, ui.layer_id()); + mem.has_focus(id) + }); if ui.memory_mut(|mem| mem.gained_focus(id)) { ui.data_mut(|data| data.remove::(id)); diff --git a/crates/egui_kittest/tests/regression_tests.rs b/crates/egui_kittest/tests/regression_tests.rs index e7186dcac..bd3f7926e 100644 --- a/crates/egui_kittest/tests/regression_tests.rs +++ b/crates/egui_kittest/tests/regression_tests.rs @@ -29,6 +29,31 @@ pub fn focus_should_skip_over_disabled_buttons() { assert!(button_1.is_focused()); } +#[test] +pub fn focus_should_skip_over_disabled_drag_values() { + let mut value_1: u16 = 1; + let mut value_2: u16 = 2; + let mut value_3: u16 = 3; + + let mut harness = Harness::new_ui(|ui| { + ui.add(egui::DragValue::new(&mut value_1)); + ui.add_enabled(false, egui::DragValue::new(&mut value_2)); + ui.add(egui::DragValue::new(&mut value_3)); + }); + + harness.press_key(egui::Key::Tab); + harness.run(); + + let drag_value_1 = harness.get_by(|node| node.numeric_value() == Some(1.0)); + assert!(drag_value_1.is_focused()); + + harness.press_key(egui::Key::Tab); + harness.run(); + + let drag_value_3 = harness.get_by(|node| node.numeric_value() == Some(3.0)); + assert!(drag_value_3.is_focused()); +} + #[test] fn image_failed() { let mut harness = Harness::new_ui(|ui| { From 267485976b266550595110aa3ecdca32dcd964d4 Mon Sep 17 00:00:00 2001 From: valadaptive <79560998+valadaptive@users.noreply.github.com> Date: Thu, 20 Mar 2025 05:49:38 -0400 Subject: [PATCH 016/129] Simplify the text cursor API (#5785) * Closes N/A, but this is part of https://github.com/emilk/egui/issues/3378 * [x] I have followed the instructions in the PR template Other text layout libraries in Rust--namely, Parley and Cosmic Text--have one canonical text cursor type (Parley's is a byte index, Cosmic Text's also stores the line index). To prepare for migrating egui to one of those libraries, it should also have only one text cursor type. I also think simplifying the API is a good idea in and of itself--having three different cursor types that you have to convert between (and a `Cursor` struct which contains all three at once) is confusing. After a bit of experimentation, I found that the best cursor type to coalesce around is `CCursor`. In the few places where we need a paragraph index or row/column position, we can calculate them as necessary. I've removed `CursorRange` and `PCursorRange` (the latter appears to have never been used), merging the functionality with `CCursorRange`. To preserve the cursor position when navigating row-by-row, `CCursorRange` now stores the previous horizontal position of the cursor. I've also removed `PCursor`, and renamed `RowCursor` to `LayoutCursor` (since it includes not only the row but the column). I have not renamed either `CCursorRange` or `CCursor` as those names are used in a lot of places, and I don't want to clutter this PR with a bunch of renames. I'll leave it for a later PR. Finally, I've removed the deprecated methods from `TextEditState`--it made the refactoring easier, and it should be pretty easy to migrate to the equivalent `TextCursorState` methods. I'm not sure how many breaking changes people will actually encounter. A lot of these APIs were technically public, but I don't think many were useful. The `TextBuffer` trait now takes `&CCursorRange` instead of `&CursorRange` in a couple of methods, and I renamed `CCursorRange::sorted` to `CCursorRange::sorted_cursors` to match `CursorRange`. I did encounter a couple of apparent minor bugs when testing out text cursor behavior, but I checked them against the current version of egui and they're all pre-existing. --- crates/egui/src/lib.rs | 2 +- .../egui/src/text_selection/accesskit_text.rs | 8 +- .../egui/src/text_selection/cursor_range.rs | 308 +++++------- .../text_selection/label_text_selection.rs | 48 +- crates/egui/src/text_selection/mod.rs | 2 +- .../src/text_selection/text_cursor_state.rs | 92 +--- crates/egui/src/text_selection/visuals.rs | 8 +- crates/egui/src/widgets/text_edit/builder.rs | 62 ++- crates/egui/src/widgets/text_edit/output.rs | 4 +- crates/egui/src/widgets/text_edit/state.rs | 28 +- .../egui/src/widgets/text_edit/text_buffer.rs | 43 +- .../src/easy_mark/easy_mark_editor.rs | 4 +- crates/epaint/src/text/cursor.rs | 58 +-- crates/epaint/src/text/text_layout_types.rs | 457 ++++++------------ 14 files changed, 394 insertions(+), 730 deletions(-) diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index a6f461234..0d8459808 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -471,7 +471,7 @@ pub use epaint::{ }; pub mod text { - pub use crate::text_selection::{CCursorRange, CursorRange}; + pub use crate::text_selection::CCursorRange; pub use epaint::text::{ cursor::CCursor, FontData, FontDefinitions, FontFamily, Fonts, Galley, LayoutJob, LayoutSection, TextFormat, TextWrapping, TAB_SIZE, diff --git a/crates/egui/src/text_selection/accesskit_text.rs b/crates/egui/src/text_selection/accesskit_text.rs index dedbc79dc..d189498f6 100644 --- a/crates/egui/src/text_selection/accesskit_text.rs +++ b/crates/egui/src/text_selection/accesskit_text.rs @@ -2,13 +2,13 @@ use emath::TSTransform; use crate::{Context, Galley, Id}; -use super::{text_cursor_state::is_word_char, CursorRange}; +use super::{text_cursor_state::is_word_char, CCursorRange}; /// Update accesskit with the current text state. pub fn update_accesskit_for_text_widget( ctx: &Context, widget_id: Id, - cursor_range: Option, + cursor_range: Option, role: accesskit::Role, global_from_galley: TSTransform, galley: &Galley, @@ -17,8 +17,8 @@ pub fn update_accesskit_for_text_widget( let parent_id = widget_id; if let Some(cursor_range) = &cursor_range { - let anchor = &cursor_range.secondary.rcursor; - let focus = &cursor_range.primary.rcursor; + 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(), diff --git a/crates/egui/src/text_selection/cursor_range.rs b/crates/egui/src/text_selection/cursor_range.rs index bd3f496fd..05351e0ac 100644 --- a/crates/egui/src/text_selection/cursor_range.rs +++ b/crates/egui/src/text_selection/cursor_range.rs @@ -1,41 +1,45 @@ -use epaint::{ - text::cursor::{CCursor, Cursor, PCursor}, - Galley, -}; +use epaint::{text::cursor::CCursor, Galley}; use crate::{os::OperatingSystem, Event, Id, Key, Modifiers}; use super::text_cursor_state::{ccursor_next_word, ccursor_previous_word, slice_char_range}; /// A selected text range (could be a range of length zero). +/// +/// The selection is based on character count (NOT byte count!). #[derive(Clone, Copy, Debug, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct CursorRange { +pub struct CCursorRange { /// When selecting with a mouse, this is where the mouse was released. /// When moving with e.g. shift+arrows, this is what moves. /// Note that the two ends can come in any order, and also be equal (no selection). - pub primary: Cursor, + pub primary: CCursor, /// When selecting with a mouse, this is where the mouse was first pressed. /// This part of the cursor does not move when shift is down. - pub secondary: Cursor, + pub secondary: CCursor, + + /// Saved horizontal position of the cursor. + pub h_pos: Option, } -impl CursorRange { +impl CCursorRange { /// The empty range. #[inline] - pub fn one(cursor: Cursor) -> Self { + pub fn one(ccursor: CCursor) -> Self { Self { - primary: cursor, - secondary: cursor, + primary: ccursor, + secondary: ccursor, + h_pos: None, } } #[inline] - pub fn two(min: Cursor, max: Cursor) -> Self { + pub fn two(min: impl Into, max: impl Into) -> Self { Self { - primary: max, - secondary: min, + primary: max.into(), + secondary: min.into(), + h_pos: None, } } @@ -44,39 +48,31 @@ impl CursorRange { Self::two(galley.begin(), galley.end()) } - pub fn as_ccursor_range(&self) -> CCursorRange { - CCursorRange { - primary: self.primary.ccursor, - secondary: self.secondary.ccursor, - } - } - /// The range of selected character indices. pub fn as_sorted_char_range(&self) -> std::ops::Range { let [start, end] = self.sorted_cursors(); std::ops::Range { - start: start.ccursor.index, - end: end.ccursor.index, + start: start.index, + end: end.index, } } /// True if the selected range contains no characters. #[inline] pub fn is_empty(&self) -> bool { - self.primary.ccursor == self.secondary.ccursor + self.primary == self.secondary } /// Is `self` a super-set of the other range? - pub fn contains(&self, other: &Self) -> bool { + pub fn contains(&self, other: Self) -> bool { let [self_min, self_max] = self.sorted_cursors(); let [other_min, other_max] = other.sorted_cursors(); - self_min.ccursor.index <= other_min.ccursor.index - && other_max.ccursor.index <= self_max.ccursor.index + self_min.index <= other_min.index && other_max.index <= self_max.index } /// If there is a selection, None is returned. /// If the two ends are the same, that is returned. - pub fn single(&self) -> Option { + pub fn single(&self) -> Option { if self.is_empty() { Some(self.primary) } else { @@ -84,25 +80,16 @@ impl CursorRange { } } + #[inline] pub fn is_sorted(&self) -> bool { - let p = self.primary.ccursor; - let s = self.secondary.ccursor; + let p = self.primary; + let s = self.secondary; (p.index, p.prefer_next_row) <= (s.index, s.prefer_next_row) } - pub fn sorted(self) -> Self { - if self.is_sorted() { - self - } else { - Self { - primary: self.secondary, - secondary: self.primary, - } - } - } - - /// Returns the two ends ordered. - pub fn sorted_cursors(&self) -> [Cursor; 2] { + /// returns the two ends ordered + #[inline] + pub fn sorted_cursors(&self) -> [CCursor; 2] { if self.is_sorted() { [self.primary, self.secondary] } else { @@ -110,9 +97,15 @@ impl CursorRange { } } + #[inline] + #[deprecated = "Use `self.sorted_cursors` instead."] + pub fn sorted(&self) -> [CCursor; 2] { + self.sorted_cursors() + } + pub fn slice_str<'s>(&self, text: &'s str) -> &'s str { let [min, max] = self.sorted_cursors(); - slice_char_range(text, min.ccursor.index..max.ccursor.index) + slice_char_range(text, min.index..max.index) } /// Check for key presses that are moving the cursor. @@ -146,7 +139,14 @@ impl CursorRange { | Key::ArrowDown | Key::Home | Key::End => { - move_single_cursor(os, &mut self.primary, galley, key, modifiers); + move_single_cursor( + os, + &mut self.primary, + &mut self.h_pos, + galley, + key, + modifiers, + ); if !modifiers.shift { self.secondary = self.primary; } @@ -156,7 +156,14 @@ impl CursorRange { Key::P | Key::N | Key::B | Key::F | Key::A | Key::E if os == OperatingSystem::Mac && modifiers.ctrl && !modifiers.shift => { - move_single_cursor(os, &mut self.primary, galley, key, modifiers); + move_single_cursor( + os, + &mut self.primary, + &mut self.h_pos, + galley, + key, + modifiers, + ); self.secondary = self.primary; true } @@ -196,8 +203,9 @@ impl CursorRange { ccursor_from_accesskit_text_position(_widget_id, galley, &selection.anchor); if let (Some(primary), Some(secondary)) = (primary, secondary) { *self = Self { - primary: galley.from_ccursor(primary), - secondary: galley.from_ccursor(secondary), + primary, + secondary, + h_pos: None, }; return true; } @@ -210,71 +218,6 @@ impl CursorRange { } } -/// A selected text range (could be a range of length zero). -/// -/// The selection is based on character count (NOT byte count!). -#[derive(Clone, Copy, Debug, Default, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct CCursorRange { - /// When selecting with a mouse, this is where the mouse was released. - /// When moving with e.g. shift+arrows, this is what moves. - /// Note that the two ends can come in any order, and also be equal (no selection). - pub primary: CCursor, - - /// When selecting with a mouse, this is where the mouse was first pressed. - /// This part of the cursor does not move when shift is down. - pub secondary: CCursor, -} - -impl CCursorRange { - /// The empty range. - #[inline] - pub fn one(ccursor: CCursor) -> Self { - Self { - primary: ccursor, - secondary: ccursor, - } - } - - #[inline] - pub fn two(min: impl Into, max: impl Into) -> Self { - Self { - primary: max.into(), - secondary: min.into(), - } - } - - #[inline] - pub fn is_sorted(&self) -> bool { - let p = self.primary; - let s = self.secondary; - (p.index, p.prefer_next_row) <= (s.index, s.prefer_next_row) - } - - /// returns the two ends ordered - #[inline] - pub fn sorted(&self) -> [CCursor; 2] { - if self.is_sorted() { - [self.primary, self.secondary] - } else { - [self.secondary, self.primary] - } - } -} - -#[derive(Clone, Copy, Debug, Default, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct PCursorRange { - /// When selecting with a mouse, this is where the mouse was released. - /// When moving with e.g. shift+arrows, this is what moves. - /// Note that the two ends can come in any order, and also be equal (no selection). - pub primary: PCursor, - - /// When selecting with a mouse, this is where the mouse was first pressed. - /// This part of the cursor does not move when shift is down. - pub secondary: PCursor, -} - // ---------------------------------------------------------------------------- #[cfg(feature = "accesskit")] @@ -304,78 +247,83 @@ fn ccursor_from_accesskit_text_position( /// Move a text cursor based on keyboard fn move_single_cursor( os: OperatingSystem, - cursor: &mut Cursor, + cursor: &mut CCursor, + h_pos: &mut Option, galley: &Galley, key: Key, modifiers: &Modifiers, ) { - if os == OperatingSystem::Mac && modifiers.ctrl && !modifiers.shift { - match key { - Key::A => *cursor = galley.cursor_begin_of_row(cursor), - Key::E => *cursor = galley.cursor_end_of_row(cursor), - Key::P => *cursor = galley.cursor_up_one_row(cursor), - Key::N => *cursor = galley.cursor_down_one_row(cursor), - Key::B => *cursor = galley.cursor_left_one_character(cursor), - Key::F => *cursor = galley.cursor_right_one_character(cursor), - _ => (), - } - return; - } - match key { - Key::ArrowLeft => { - if modifiers.alt || modifiers.ctrl { - // alt on mac, ctrl on windows - *cursor = galley.from_ccursor(ccursor_previous_word(galley, cursor.ccursor)); - } else if modifiers.mac_cmd { - *cursor = galley.cursor_begin_of_row(cursor); - } else { - *cursor = galley.cursor_left_one_character(cursor); + let (new_cursor, new_h_pos) = + if os == OperatingSystem::Mac && modifiers.ctrl && !modifiers.shift { + match key { + Key::A => (galley.cursor_begin_of_row(cursor), None), + Key::E => (galley.cursor_end_of_row(cursor), None), + Key::P => galley.cursor_up_one_row(cursor, *h_pos), + Key::N => galley.cursor_down_one_row(cursor, *h_pos), + Key::B => (galley.cursor_left_one_character(cursor), None), + Key::F => (galley.cursor_right_one_character(cursor), None), + _ => return, } - } - Key::ArrowRight => { - if modifiers.alt || modifiers.ctrl { - // alt on mac, ctrl on windows - *cursor = galley.from_ccursor(ccursor_next_word(galley, cursor.ccursor)); - } else if modifiers.mac_cmd { - *cursor = galley.cursor_end_of_row(cursor); - } else { - *cursor = galley.cursor_right_one_character(cursor); - } - } - Key::ArrowUp => { - if modifiers.command { - // mac and windows behavior - *cursor = galley.begin(); - } else { - *cursor = galley.cursor_up_one_row(cursor); - } - } - Key::ArrowDown => { - if modifiers.command { - // mac and windows behavior - *cursor = galley.end(); - } else { - *cursor = galley.cursor_down_one_row(cursor); - } - } + } else { + match key { + Key::ArrowLeft => { + if modifiers.alt || modifiers.ctrl { + // alt on mac, ctrl on windows + (ccursor_previous_word(galley, *cursor), None) + } else if modifiers.mac_cmd { + (galley.cursor_begin_of_row(cursor), None) + } else { + (galley.cursor_left_one_character(cursor), None) + } + } + Key::ArrowRight => { + if modifiers.alt || modifiers.ctrl { + // alt on mac, ctrl on windows + (ccursor_next_word(galley, *cursor), None) + } else if modifiers.mac_cmd { + (galley.cursor_end_of_row(cursor), None) + } else { + (galley.cursor_right_one_character(cursor), None) + } + } + Key::ArrowUp => { + if modifiers.command { + // mac and windows behavior + (galley.begin(), None) + } else { + galley.cursor_up_one_row(cursor, *h_pos) + } + } + Key::ArrowDown => { + if modifiers.command { + // mac and windows behavior + (galley.end(), None) + } else { + galley.cursor_down_one_row(cursor, *h_pos) + } + } - Key::Home => { - if modifiers.ctrl { - // windows behavior - *cursor = galley.begin(); - } else { - *cursor = galley.cursor_begin_of_row(cursor); - } - } - Key::End => { - if modifiers.ctrl { - // windows behavior - *cursor = galley.end(); - } else { - *cursor = galley.cursor_end_of_row(cursor); - } - } + Key::Home => { + if modifiers.ctrl { + // windows behavior + (galley.begin(), None) + } else { + (galley.cursor_begin_of_row(cursor), None) + } + } + Key::End => { + if modifiers.ctrl { + // windows behavior + (galley.end(), None) + } else { + (galley.cursor_end_of_row(cursor), None) + } + } - _ => unreachable!(), - } + _ => unreachable!(), + } + }; + + *cursor = new_cursor; + *h_pos = new_h_pos; } diff --git a/crates/egui/src/text_selection/label_text_selection.rs b/crates/egui/src/text_selection/label_text_selection.rs index aa9f0986a..acd3db7d3 100644 --- a/crates/egui/src/text_selection/label_text_selection.rs +++ b/crates/egui/src/text_selection/label_text_selection.rs @@ -10,7 +10,7 @@ use crate::{ use super::{ text_cursor_state::cursor_rect, visuals::{paint_text_selection, RowVertexIndices}, - CursorRange, TextCursorState, + TextCursorState, }; /// Turn on to help debug this @@ -44,7 +44,7 @@ impl WidgetTextCursor { } fn pos_in_galley(galley: &Galley, ccursor: CCursor) -> Pos2 { - galley.pos_from_ccursor(ccursor).center() + galley.pos_from_cursor(ccursor).center() } impl std::fmt::Debug for WidgetTextCursor { @@ -235,7 +235,7 @@ impl LabelSelectionState { self.selection = None; } - fn copy_text(&mut self, new_galley_rect: Rect, galley: &Galley, cursor_range: &CursorRange) { + fn copy_text(&mut self, new_galley_rect: Rect, galley: &Galley, cursor_range: &CCursorRange) { let new_text = selected_text(galley, cursor_range); if new_text.is_empty() { return; @@ -433,7 +433,11 @@ impl LabelSelectionState { match (primary, secondary) { (Some(primary), Some(secondary)) => { // This is the only selected label. - TextCursorState::from(CCursorRange { primary, secondary }) + TextCursorState::from(CCursorRange { + primary, + secondary, + h_pos: None, + }) } (Some(primary), None) => { @@ -442,12 +446,16 @@ impl LabelSelectionState { // Secondary was before primary. // Select everything up to the cursor. // We assume normal left-to-right and top-down layout order here. - galley.begin().ccursor + galley.begin() } else { // Select everything from the cursor onward: - galley.end().ccursor + galley.end() }; - TextCursorState::from(CCursorRange { primary, secondary }) + TextCursorState::from(CCursorRange { + primary, + secondary, + h_pos: None, + }) } (None, Some(secondary)) => { @@ -456,12 +464,16 @@ impl LabelSelectionState { // Primary was before secondary. // Select everything up to the cursor. // We assume normal left-to-right and top-down layout order here. - galley.begin().ccursor + galley.begin() } else { // Select everything from the cursor onward: - galley.end().ccursor + galley.end() }; - TextCursorState::from(CCursorRange { primary, secondary }) + TextCursorState::from(CCursorRange { + primary, + secondary, + h_pos: None, + }) } (None, None) => { @@ -515,7 +527,7 @@ impl LabelSelectionState { let mut cursor_state = self.cursor_for(ui, response, global_from_galley, galley); - let old_range = cursor_state.range(galley); + let old_range = cursor_state.char_range(); if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() { if response.contains_pointer() { @@ -529,7 +541,7 @@ impl LabelSelectionState { } } - if let Some(mut cursor_range) = cursor_state.range(galley) { + if let Some(mut cursor_range) = cursor_state.char_range() { let galley_rect = global_from_galley * Rect::from_min_size(Pos2::ZERO, galley.size()); self.selection_bbox_this_frame = self.selection_bbox_this_frame.union(galley_rect); @@ -543,11 +555,11 @@ impl LabelSelectionState { self.copy_text(galley_rect, galley, &cursor_range); } - cursor_state.set_range(Some(cursor_range)); + cursor_state.set_char_range(Some(cursor_range)); } // Look for changes due to keyboard and/or mouse interaction: - let new_range = cursor_state.range(galley); + let new_range = cursor_state.char_range(); let selection_changed = old_range != new_range; if let (true, Some(range)) = (selection_changed, new_range) { @@ -617,7 +629,7 @@ impl LabelSelectionState { } } - let cursor_range = cursor_state.range(galley); + let cursor_range = cursor_state.char_range(); let mut new_vertex_indices = vec![]; @@ -657,7 +669,7 @@ fn process_selection_key_events( ctx: &Context, galley: &Galley, widget_id: Id, - cursor_range: &mut CursorRange, + cursor_range: &mut CCursorRange, ) -> bool { let os = ctx.os(); @@ -674,10 +686,10 @@ fn process_selection_key_events( changed } -fn selected_text(galley: &Galley, cursor_range: &CursorRange) -> String { +fn selected_text(galley: &Galley, cursor_range: &CCursorRange) -> String { // This logic means we can select everything in an elided label (including the `…`) // and still copy the entire un-elided text! - let everything_is_selected = cursor_range.contains(&CursorRange::select_all(galley)); + let everything_is_selected = cursor_range.contains(CCursorRange::select_all(galley)); let copy_everything = cursor_range.is_empty() || everything_is_selected; diff --git a/crates/egui/src/text_selection/mod.rs b/crates/egui/src/text_selection/mod.rs index 5be95eb53..8d0943d60 100644 --- a/crates/egui/src/text_selection/mod.rs +++ b/crates/egui/src/text_selection/mod.rs @@ -8,6 +8,6 @@ mod label_text_selection; pub mod text_cursor_state; pub mod visuals; -pub use cursor_range::{CCursorRange, CursorRange, PCursorRange}; +pub use cursor_range::CCursorRange; pub use label_text_selection::LabelSelectionState; pub use text_cursor_state::TextCursorState; diff --git a/crates/egui/src/text_selection/text_cursor_state.rs b/crates/egui/src/text_selection/text_cursor_state.rs index ebc618b2c..21ebda3d0 100644 --- a/crates/egui/src/text_selection/text_cursor_state.rs +++ b/crates/egui/src/text_selection/text_cursor_state.rs @@ -1,13 +1,10 @@ //! Text cursor changes/interaction, without modifying the text. -use epaint::text::{ - cursor::{CCursor, Cursor}, - Galley, -}; +use epaint::text::{cursor::CCursor, Galley}; use crate::{epaint, NumExt, Rect, Response, Ui}; -use super::{CCursorRange, CursorRange}; +use super::CCursorRange; /// The state of a text cursor selection. /// @@ -16,29 +13,12 @@ use super::{CCursorRange, CursorRange}; #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] pub struct TextCursorState { - cursor_range: Option, - - /// This is what is easiest to work with when editing text, - /// so users are more likely to read/write this. ccursor_range: Option, } -impl From for TextCursorState { - fn from(cursor_range: CursorRange) -> Self { - Self { - cursor_range: Some(cursor_range), - ccursor_range: Some(CCursorRange { - primary: cursor_range.primary.ccursor, - secondary: cursor_range.secondary.ccursor, - }), - } - } -} - impl From for TextCursorState { fn from(ccursor_range: CCursorRange) -> Self { Self { - cursor_range: None, ccursor_range: Some(ccursor_range), } } @@ -46,50 +26,18 @@ impl From for TextCursorState { impl TextCursorState { pub fn is_empty(&self) -> bool { - self.cursor_range.is_none() && self.ccursor_range.is_none() + self.ccursor_range.is_none() } /// The currently selected range of characters. pub fn char_range(&self) -> Option { - self.ccursor_range.or_else(|| { - self.cursor_range - .map(|cursor_range| cursor_range.as_ccursor_range()) - }) - } - - pub fn range(&self, galley: &Galley) -> Option { - self.cursor_range - .map(|cursor_range| { - // We only use the PCursor (paragraph number, and character offset within that paragraph). - // This is so that if we resize the [`TextEdit`] region, and text wrapping changes, - // we keep the same byte character offset from the beginning of the text, - // even though the number of rows changes - // (each paragraph can be several rows, due to word wrapping). - // The column (character offset) should be able to extend beyond the last word so that we can - // go down and still end up on the same column when we return. - CursorRange { - primary: galley.from_pcursor(cursor_range.primary.pcursor), - secondary: galley.from_pcursor(cursor_range.secondary.pcursor), - } - }) - .or_else(|| { - self.ccursor_range.map(|ccursor_range| CursorRange { - primary: galley.from_ccursor(ccursor_range.primary), - secondary: galley.from_ccursor(ccursor_range.secondary), - }) - }) + self.ccursor_range } /// Sets the currently selected range of characters. pub fn set_char_range(&mut self, ccursor_range: Option) { - self.cursor_range = None; self.ccursor_range = ccursor_range; } - - pub fn set_range(&mut self, cursor_range: Option) { - self.cursor_range = cursor_range; - self.ccursor_range = None; - } } impl TextCursorState { @@ -100,7 +48,7 @@ impl TextCursorState { &mut self, ui: &Ui, response: &Response, - cursor_at_pointer: Cursor, + cursor_at_pointer: CCursor, galley: &Galley, is_being_dragged: bool, ) -> bool { @@ -108,39 +56,33 @@ impl TextCursorState { if response.double_clicked() { // Select word: - let ccursor_range = select_word_at(text, cursor_at_pointer.ccursor); - self.set_range(Some(CursorRange { - primary: galley.from_ccursor(ccursor_range.primary), - secondary: galley.from_ccursor(ccursor_range.secondary), - })); + let ccursor_range = select_word_at(text, cursor_at_pointer); + self.set_char_range(Some(ccursor_range)); true } else if response.triple_clicked() { // Select line: - let ccursor_range = select_line_at(text, cursor_at_pointer.ccursor); - self.set_range(Some(CursorRange { - primary: galley.from_ccursor(ccursor_range.primary), - secondary: galley.from_ccursor(ccursor_range.secondary), - })); + let ccursor_range = select_line_at(text, cursor_at_pointer); + self.set_char_range(Some(ccursor_range)); true } else if response.sense.senses_drag() { if response.hovered() && ui.input(|i| i.pointer.any_pressed()) { // The start of a drag (or a click). if ui.input(|i| i.modifiers.shift) { - if let Some(mut cursor_range) = self.range(galley) { + if let Some(mut cursor_range) = self.char_range() { cursor_range.primary = cursor_at_pointer; - self.set_range(Some(cursor_range)); + self.set_char_range(Some(cursor_range)); } else { - self.set_range(Some(CursorRange::one(cursor_at_pointer))); + self.set_char_range(Some(CCursorRange::one(cursor_at_pointer))); } } else { - self.set_range(Some(CursorRange::one(cursor_at_pointer))); + self.set_char_range(Some(CCursorRange::one(cursor_at_pointer))); } true } else if is_being_dragged { // Drag to select text: - if let Some(mut cursor_range) = self.range(galley) { + if let Some(mut cursor_range) = self.char_range() { cursor_range.primary = cursor_at_pointer; - self.set_range(Some(cursor_range)); + self.set_char_range(Some(cursor_range)); } true } else { @@ -336,8 +278,8 @@ pub fn slice_char_range(s: &str, char_range: std::ops::Range) -> &str { } /// The thin rectangle of one end of the selection, e.g. the primary cursor, in local galley coordinates. -pub fn cursor_rect(galley: &Galley, cursor: &Cursor, row_height: f32) -> Rect { - let mut cursor_pos = galley.pos_from_cursor(cursor); +pub fn cursor_rect(galley: &Galley, cursor: &CCursor, row_height: f32) -> Rect { + let mut cursor_pos = galley.pos_from_cursor(*cursor); // Handle completely empty galleys cursor_pos.max.y = cursor_pos.max.y.at_least(cursor_pos.min.y + row_height); diff --git a/crates/egui/src/text_selection/visuals.rs b/crates/egui/src/text_selection/visuals.rs index dd7c867a2..4025a2d5f 100644 --- a/crates/egui/src/text_selection/visuals.rs +++ b/crates/egui/src/text_selection/visuals.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use crate::{pos2, vec2, Galley, Painter, Rect, Ui, Visuals}; -use super::CursorRange; +use super::CCursorRange; #[derive(Clone, Debug)] pub struct RowVertexIndices { @@ -14,7 +14,7 @@ pub struct RowVertexIndices { pub fn paint_text_selection( galley: &mut Arc, visuals: &Visuals, - cursor_range: &CursorRange, + cursor_range: &CCursorRange, mut new_vertex_indices: Option<&mut Vec>, ) { if cursor_range.is_empty() { @@ -27,8 +27,8 @@ pub fn paint_text_selection( let color = visuals.selection.bg_fill; let [min, max] = cursor_range.sorted_cursors(); - let min = min.rcursor; - let max = max.rcursor; + let min = galley.layout_from_cursor(min); + let max = galley.layout_from_cursor(max); for ri in min.row..=max.row { let row = &mut galley.rows[ri]; diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 7d5c23f65..d73ecd3f6 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -11,9 +11,7 @@ use crate::{ os::OperatingSystem, output::OutputEvent, response, text_selection, - text_selection::{ - text_cursor_state::cursor_rect, visuals::paint_text_selection, CCursorRange, CursorRange, - }, + text_selection::{text_cursor_state::cursor_rect, visuals::paint_text_selection, CCursorRange}, vec2, Align, Align2, Color32, Context, CursorIcon, Event, EventFilter, FontSelection, Id, ImeEvent, Key, KeyboardShortcut, Margin, Modifiers, NumExt, Response, Sense, Shape, TextBuffer, TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetWithState, @@ -614,14 +612,14 @@ impl TextEdit<'_> { } let mut cursor_range = None; - let prev_cursor_range = state.cursor.range(&galley); + let prev_cursor_range = state.cursor.char_range(); 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 { - CursorRange::one(galley.end()) + CCursorRange::one(galley.end()) } else { - CursorRange::default() + CCursorRange::default() }; let (changed, new_cursor_range) = events( @@ -655,7 +653,7 @@ impl TextEdit<'_> { // Visual clipping for singleline text editor with text larger than width if clip_text && align_offset == 0.0 { let cursor_pos = match (cursor_range, ui.memory(|mem| mem.has_focus(id))) { - (Some(cursor_range), true) => galley.pos_from_cursor(&cursor_range.primary).min.x, + (Some(cursor_range), true) => galley.pos_from_cursor(cursor_range.primary).min.x, _ => 0.0, }; @@ -683,7 +681,7 @@ impl TextEdit<'_> { let selection_changed = if let (Some(cursor_range), Some(prev_cursor_range)) = (cursor_range, prev_cursor_range) { - prev_cursor_range.as_ccursor_range() != cursor_range.as_ccursor_range() + prev_cursor_range != cursor_range } else { false }; @@ -717,7 +715,7 @@ impl TextEdit<'_> { let has_focus = ui.memory(|mem| mem.has_focus(id)); if has_focus { - if let Some(cursor_range) = state.cursor.range(&galley) { + if let Some(cursor_range) = state.cursor.char_range() { // Add text selection rectangles to the galley: paint_text_selection(&mut galley, ui.visuals(), &cursor_range, None); } @@ -739,7 +737,7 @@ impl TextEdit<'_> { painter.galley(galley_pos, galley.clone(), text_color); if has_focus { - if let Some(cursor_range) = state.cursor.range(&galley) { + if let Some(cursor_range) = state.cursor.char_range() { let primary_cursor_rect = cursor_rect(&galley, &cursor_range.primary, row_height) .translate(galley_pos.to_vec2()); @@ -809,8 +807,7 @@ impl TextEdit<'_> { }); } else if selection_changed { let cursor_range = cursor_range.unwrap(); - let char_range = - cursor_range.primary.ccursor.index..=cursor_range.secondary.ccursor.index; + let char_range = cursor_range.primary.index..=cursor_range.secondary.index; let info = WidgetInfo::text_selection_changed( ui.is_enabled(), char_range, @@ -887,20 +884,20 @@ fn events( wrap_width: f32, multiline: bool, password: bool, - default_cursor_range: CursorRange, + default_cursor_range: CCursorRange, char_limit: usize, event_filter: EventFilter, return_key: Option, -) -> (bool, CursorRange) { +) -> (bool, CCursorRange) { let os = ui.ctx().os(); - let mut cursor_range = state.cursor.range(galley).unwrap_or(default_cursor_range); + let mut cursor_range = state.cursor.char_range().unwrap_or(default_cursor_range); // We feed state to the undoer both before and after handling input // so that the undoer creates automatic saves even when there are no events for a while. state.undoer.lock().feed_state( ui.input(|i| i.time), - &(cursor_range.as_ccursor_range(), text.as_str().to_owned()), + &(cursor_range, text.as_str().to_owned()), ); let copy_if_not_password = |ui: &Ui, text: String| { @@ -1010,7 +1007,7 @@ fn events( if let Some((redo_ccursor_range, redo_txt)) = state .undoer .lock() - .redo(&(cursor_range.as_ccursor_range(), text.as_str().to_owned())) + .redo(&(cursor_range, text.as_str().to_owned())) { text.replace_with(redo_txt); Some(*redo_ccursor_range) @@ -1028,7 +1025,7 @@ fn events( if let Some((undo_ccursor_range, undo_txt)) = state .undoer .lock() - .undo(&(cursor_range.as_ccursor_range(), text.as_str().to_owned())) + .undo(&(cursor_range, text.as_str().to_owned())) { text.replace_with(undo_txt); Some(*undo_ccursor_range) @@ -1072,14 +1069,14 @@ fn events( state.ime_enabled = false; if !prediction.is_empty() - && cursor_range.secondary.ccursor.index - == state.ime_cursor_range.secondary.ccursor.index + && cursor_range.secondary.index + == state.ime_cursor_range.secondary.index { let mut ccursor = text.delete_selected(&cursor_range); text.insert_text_at(&mut ccursor, prediction, char_limit); Some(CCursorRange::one(ccursor)) } else { - let ccursor = cursor_range.primary.ccursor; + let ccursor = cursor_range.primary; Some(CCursorRange::one(ccursor)) } } @@ -1100,18 +1097,15 @@ fn events( *galley = layouter(ui, text.as_str(), wrap_width); // Set cursor_range using new galley: - cursor_range = CursorRange { - primary: galley.from_ccursor(new_ccursor_range.primary), - secondary: galley.from_ccursor(new_ccursor_range.secondary), - }; + cursor_range = new_ccursor_range; } } - state.cursor.set_range(Some(cursor_range)); + state.cursor.set_char_range(Some(cursor_range)); state.undoer.lock().feed_state( ui.input(|i| i.time), - &(cursor_range.as_ccursor_range(), text.as_str().to_owned()), + &(cursor_range, text.as_str().to_owned()), ); (any_change, cursor_range) @@ -1143,7 +1137,7 @@ fn remove_ime_incompatible_events(events: &mut Vec) { /// Returns `Some(new_cursor)` if we did mutate `text`. fn check_for_mutating_key_press( os: OperatingSystem, - cursor_range: &CursorRange, + cursor_range: &CCursorRange, text: &mut dyn TextBuffer, galley: &Galley, modifiers: &Modifiers, @@ -1156,9 +1150,9 @@ fn check_for_mutating_key_press( } else if let Some(cursor) = cursor_range.single() { if modifiers.alt || modifiers.ctrl { // alt on mac, ctrl on windows - text.delete_previous_word(cursor.ccursor) + text.delete_previous_word(cursor) } else { - text.delete_previous_char(cursor.ccursor) + text.delete_previous_char(cursor) } } else { text.delete_selected(cursor_range) @@ -1172,9 +1166,9 @@ fn check_for_mutating_key_press( } else if let Some(cursor) = cursor_range.single() { if modifiers.alt || modifiers.ctrl { // alt on mac, ctrl on windows - text.delete_next_word(cursor.ccursor) + text.delete_next_word(cursor) } else { - text.delete_next_char(cursor.ccursor) + text.delete_next_char(cursor) } } else { text.delete_selected(cursor_range) @@ -1187,7 +1181,7 @@ fn check_for_mutating_key_press( } Key::H if modifiers.ctrl => { - let ccursor = text.delete_previous_char(cursor_range.primary.ccursor); + let ccursor = text.delete_previous_char(cursor_range.primary); Some(CCursorRange::one(ccursor)) } @@ -1203,7 +1197,7 @@ fn check_for_mutating_key_press( Key::W if modifiers.ctrl => { let ccursor = if let Some(cursor) = cursor_range.single() { - text.delete_previous_word(cursor.ccursor) + text.delete_previous_word(cursor) } else { text.delete_selected(cursor_range) }; diff --git a/crates/egui/src/widgets/text_edit/output.rs b/crates/egui/src/widgets/text_edit/output.rs index d02c1d1c5..2aa6711e7 100644 --- a/crates/egui/src/widgets/text_edit/output.rs +++ b/crates/egui/src/widgets/text_edit/output.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use crate::text::CursorRange; +use crate::text::CCursorRange; /// The output from a [`TextEdit`](crate::TextEdit). pub struct TextEditOutput { @@ -20,7 +20,7 @@ pub struct TextEditOutput { pub state: super::TextEditState, /// Where the text cursor is. - pub cursor_range: Option, + pub cursor_range: Option, } impl TextEditOutput { diff --git a/crates/egui/src/widgets/text_edit/state.rs b/crates/egui/src/widgets/text_edit/state.rs index c10a88274..0734811bd 100644 --- a/crates/egui/src/widgets/text_edit/state.rs +++ b/crates/egui/src/widgets/text_edit/state.rs @@ -3,8 +3,8 @@ use std::sync::Arc; use crate::mutex::Mutex; use crate::{ - text_selection::{CCursorRange, CursorRange, TextCursorState}, - Context, Galley, Id, + text_selection::{CCursorRange, TextCursorState}, + Context, Id, }; pub type TextEditUndoer = crate::util::undoer::Undoer<(CCursorRange, String)>; @@ -47,7 +47,7 @@ pub struct TextEditState { // cursor range for IME candidate. #[cfg_attr(feature = "serde", serde(skip))] - pub(crate) ime_cursor_range: CursorRange, + pub(crate) ime_cursor_range: CCursorRange, // Visual offset when editing singleline text bigger than the width. #[cfg_attr(feature = "serde", serde(skip))] @@ -68,23 +68,6 @@ impl TextEditState { ctx.data_mut(|d| d.insert_persisted(id, self)); } - /// The currently selected range of characters. - #[deprecated = "Use `self.cursor.char_range` instead"] - pub fn ccursor_range(&self) -> Option { - self.cursor.char_range() - } - - /// Sets the currently selected range of characters. - #[deprecated = "Use `self.cursor.set_char_range` instead"] - pub fn set_ccursor_range(&mut self, ccursor_range: Option) { - self.cursor.set_char_range(ccursor_range); - } - - #[deprecated = "Use `self.cursor.set_range` instead"] - pub fn set_cursor_range(&mut self, cursor_range: Option) { - self.cursor.set_range(cursor_range); - } - pub fn undoer(&self) -> TextEditUndoer { self.undoer.lock().clone() } @@ -97,9 +80,4 @@ impl TextEditState { pub fn clear_undoer(&mut self) { self.set_undoer(TextEditUndoer::default()); } - - #[deprecated = "Use `self.cursor.range` instead"] - pub fn cursor_range(&self, galley: &Galley) -> Option { - self.cursor.range(galley) - } } diff --git a/crates/egui/src/widgets/text_edit/text_buffer.rs b/crates/egui/src/widgets/text_edit/text_buffer.rs index 31b746324..9290f1e9c 100644 --- a/crates/egui/src/widgets/text_edit/text_buffer.rs +++ b/crates/egui/src/widgets/text_edit/text_buffer.rs @@ -1,19 +1,16 @@ use std::{borrow::Cow, ops::Range}; use epaint::{ - text::{ - cursor::{CCursor, PCursor}, - TAB_SIZE, - }, + text::{cursor::CCursor, TAB_SIZE}, Galley, }; -use crate::text_selection::{ - text_cursor_state::{ +use crate::{ + text::CCursorRange, + text_selection::text_cursor_state::{ byte_index_from_char_index, ccursor_next_word, ccursor_previous_word, find_line_start, slice_char_range, }, - CursorRange, }; /// Trait constraining what types [`crate::TextEdit`] may use as @@ -111,9 +108,9 @@ pub trait TextBuffer { } } - fn delete_selected(&mut self, cursor_range: &CursorRange) -> CCursor { + fn delete_selected(&mut self, cursor_range: &CCursorRange) -> CCursor { let [min, max] = cursor_range.sorted_cursors(); - self.delete_selected_ccursor_range([min.ccursor, max.ccursor]) + self.delete_selected_ccursor_range([min, max]) } fn delete_selected_ccursor_range(&mut self, [min, max]: [CCursor; 2]) -> CCursor { @@ -151,36 +148,28 @@ pub trait TextBuffer { fn delete_paragraph_before_cursor( &mut self, galley: &Galley, - cursor_range: &CursorRange, + cursor_range: &CCursorRange, ) -> CCursor { let [min, max] = cursor_range.sorted_cursors(); - let min = galley.from_pcursor(PCursor { - paragraph: min.pcursor.paragraph, - offset: 0, - prefer_next_row: true, - }); - if min.ccursor == max.ccursor { - self.delete_previous_char(min.ccursor) + let min = galley.cursor_begin_of_paragraph(&min); + if min == max { + self.delete_previous_char(min) } else { - self.delete_selected(&CursorRange::two(min, max)) + self.delete_selected(&CCursorRange::two(min, max)) } } fn delete_paragraph_after_cursor( &mut self, galley: &Galley, - cursor_range: &CursorRange, + cursor_range: &CCursorRange, ) -> CCursor { let [min, max] = cursor_range.sorted_cursors(); - let max = galley.from_pcursor(PCursor { - paragraph: max.pcursor.paragraph, - offset: usize::MAX, // end of paragraph - prefer_next_row: false, - }); - if min.ccursor == max.ccursor { - self.delete_next_char(min.ccursor) + let max = galley.cursor_end_of_paragraph(&max); + if min == max { + self.delete_next_char(min) } else { - self.delete_selected(&CursorRange::two(min, max)) + self.delete_selected(&CCursorRange::two(min, max)) } } } diff --git a/crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs b/crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs index 524beaf69..9e730fac5 100644 --- a/crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs +++ b/crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs @@ -146,7 +146,7 @@ fn shortcuts(ui: &Ui, code: &mut dyn TextBuffer, ccursor_range: &mut CCursorRang if ui.input_mut(|i| i.consume_shortcut(&SHORTCUT_INDENT)) { // This is a placeholder till we can indent the active line any_change = true; - let [primary, _secondary] = ccursor_range.sorted(); + let [primary, _secondary] = ccursor_range.sorted_cursors(); let advance = code.insert_text(" ", primary.index); ccursor_range.primary.index += advance; @@ -177,7 +177,7 @@ fn toggle_surrounding( ccursor_range: &mut CCursorRange, surrounding: &str, ) { - let [primary, secondary] = ccursor_range.sorted(); + let [primary, secondary] = ccursor_range.sorted_cursors(); let surrounding_ccount = surrounding.chars().count(); diff --git a/crates/epaint/src/text/cursor.rs b/crates/epaint/src/text/cursor.rs index 2158695ec..a436ca1b1 100644 --- a/crates/epaint/src/text/cursor.rs +++ b/crates/epaint/src/text/cursor.rs @@ -26,13 +26,6 @@ impl CCursor { } } -impl From for CCursor { - #[inline] - fn from(c: Cursor) -> Self { - c.ccursor - } -} - /// Two `CCursor`s are considered equal if they refer to the same character boundary, /// even if one prefers the start of the next row. impl PartialEq for CCursor { @@ -76,10 +69,12 @@ impl std::ops::SubAssign for CCursor { } } -/// Row Cursor +/// Row/column cursor. +/// +/// This refers to rows and columns in layout terms--text wrapping creates multiple rows. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct RCursor { +pub struct LayoutCursor { /// 0 is first row, and so on. /// Note that a single paragraph can span multiple rows. /// (a paragraph is text separated by `\n`). @@ -90,48 +85,3 @@ pub struct RCursor { /// When moving up/down it may again be within the next row. pub column: usize, } - -/// Paragraph Cursor -#[derive(Clone, Copy, Debug, Default)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct PCursor { - /// 0 is first paragraph, and so on. - /// Note that a single paragraph can span multiple rows. - /// (a paragraph is text separated by `\n`). - pub paragraph: usize, - - /// Character based (NOT bytes). - /// It is fine if this points to something beyond the end of the current paragraph. - /// When moving up/down it may again be within the next paragraph. - pub offset: usize, - - /// If this cursors sits right at the border of a wrapped row break (NOT paragraph break) - /// do we prefer the next row? - /// This is *almost* always what you want, *except* for when - /// explicitly clicking the end of a row or pressing the end key. - pub prefer_next_row: bool, -} - -/// Two `PCursor`s are considered equal if they refer to the same character boundary, -/// even if one prefers the start of the next row. -impl PartialEq for PCursor { - #[inline] - fn eq(&self, other: &Self) -> bool { - self.paragraph == other.paragraph && self.offset == other.offset - } -} - -/// All different types of cursors together. -/// -/// They all point to the same place, but in their own different ways. -/// pcursor/rcursor can also point to after the end of the paragraph/row. -/// Does not implement `PartialEq` because you must think which cursor should be equivalent. -/// -/// The default cursor is the zero-cursor, to the first character. -#[derive(Clone, Copy, Debug, Default, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct Cursor { - pub ccursor: CCursor, - pub rcursor: RCursor, - pub pcursor: PCursor, -} diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index b228d023e..afe2573d5 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -5,7 +5,7 @@ use std::ops::Range; use std::sync::Arc; use super::{ - cursor::{CCursor, Cursor, PCursor, RCursor}, + cursor::{CCursor, LayoutCursor}, font::UvRect, }; use crate::{Color32, FontId, Mesh, Stroke}; @@ -766,53 +766,18 @@ impl Galley { } /// Returns a 0-width Rect. - pub fn pos_from_cursor(&self, cursor: &Cursor) -> Rect { - self.pos_from_pcursor(cursor.pcursor) // pcursor is what TextEdit stores + fn pos_from_layout_cursor(&self, layout_cursor: &LayoutCursor) -> Rect { + let Some(row) = self.rows.get(layout_cursor.row) else { + return self.end_pos(); + }; + + let x = row.x_offset(layout_cursor.column); + Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y())) } /// Returns a 0-width Rect. - pub fn pos_from_pcursor(&self, pcursor: PCursor) -> Rect { - let mut it = PCursor::default(); - - for row in &self.rows { - if it.paragraph == pcursor.paragraph { - // Right paragraph, but is it the right row in the paragraph? - - if it.offset <= pcursor.offset - && (pcursor.offset <= it.offset + row.char_count_excluding_newline() - || row.ends_with_newline) - { - let column = pcursor.offset - it.offset; - - let select_next_row_instead = pcursor.prefer_next_row - && !row.ends_with_newline - && column >= row.char_count_excluding_newline(); - if !select_next_row_instead { - let x = row.x_offset(column); - return Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y())); - } - } - } - - if row.ends_with_newline { - it.paragraph += 1; - it.offset = 0; - } else { - it.offset += row.char_count_including_newline(); - } - } - - self.end_pos() - } - - /// Returns a 0-width Rect. - pub fn pos_from_ccursor(&self, ccursor: CCursor) -> Rect { - self.pos_from_cursor(&self.from_ccursor(ccursor)) - } - - /// Returns a 0-width Rect. - pub fn pos_from_rcursor(&self, rcursor: RCursor) -> Rect { - self.pos_from_cursor(&self.from_rcursor(rcursor)) + pub fn pos_from_cursor(&self, cursor: CCursor) -> Rect { + self.pos_from_layout_cursor(&self.layout_from_cursor(cursor)) } /// Cursor at the given position within the galley. @@ -822,7 +787,7 @@ impl Galley { /// and a cursor below the galley is considered /// same as a cursor at the end. /// This allows implementing text-selection by dragging above/below the galley. - pub fn cursor_from_pos(&self, pos: Vec2) -> Cursor { + pub fn cursor_from_pos(&self, pos: Vec2) -> CCursor { if let Some(first_row) = self.rows.first() { if pos.y < first_row.min_y() { return self.begin(); @@ -835,32 +800,20 @@ impl Galley { } let mut best_y_dist = f32::INFINITY; - let mut cursor = Cursor::default(); + let mut cursor = CCursor::default(); let mut ccursor_index = 0; - let mut pcursor_it = PCursor::default(); - for (row_nr, row) in self.rows.iter().enumerate() { + for row in &self.rows { let is_pos_within_row = row.min_y() <= pos.y && pos.y <= row.max_y(); let y_dist = (row.min_y() - pos.y).abs().min((row.max_y() - pos.y).abs()); if is_pos_within_row || y_dist < best_y_dist { best_y_dist = y_dist; let column = row.char_at(pos.x); let prefer_next_row = column < row.char_count_excluding_newline(); - cursor = Cursor { - ccursor: CCursor { - index: ccursor_index + column, - prefer_next_row, - }, - rcursor: RCursor { - row: row_nr, - column, - }, - pcursor: PCursor { - paragraph: pcursor_it.paragraph, - offset: pcursor_it.offset + column, - prefer_next_row, - }, + cursor = CCursor { + index: ccursor_index + column, + prefer_next_row, }; if is_pos_within_row { @@ -868,12 +821,6 @@ impl Galley { } } ccursor_index += row.char_count_including_newline(); - if row.ends_with_newline { - pcursor_it.paragraph += 1; - pcursor_it.offset = 0; - } else { - pcursor_it.offset += row.char_count_including_newline(); - } } cursor @@ -884,15 +831,15 @@ impl Galley { impl Galley { /// Cursor to the first character. /// - /// This is the same as [`Cursor::default`]. + /// This is the same as [`CCursor::default`]. #[inline] #[allow(clippy::unused_self)] - pub fn begin(&self) -> Cursor { - Cursor::default() + pub fn begin(&self) -> CCursor { + CCursor::default() } /// Cursor to one-past last character. - pub fn end(&self) -> Cursor { + pub fn end(&self) -> CCursor { if self.rows.is_empty() { return Default::default(); } @@ -900,31 +847,47 @@ impl Galley { index: 0, prefer_next_row: true, }; - let mut pcursor = PCursor { - paragraph: 0, - offset: 0, - prefer_next_row: true, - }; for row in &self.rows { let row_char_count = row.char_count_including_newline(); ccursor.index += row_char_count; - if row.ends_with_newline { - pcursor.paragraph += 1; - pcursor.offset = 0; - } else { - pcursor.offset += row_char_count; - } - } - Cursor { - ccursor, - rcursor: self.end_rcursor(), - pcursor, } + ccursor } +} + +/// ## Cursor conversions +impl Galley { + // The returned cursor is clamped. + pub fn layout_from_cursor(&self, cursor: CCursor) -> LayoutCursor { + let prefer_next_row = cursor.prefer_next_row; + let mut ccursor_it = CCursor { + index: 0, + prefer_next_row, + }; + + for (row_nr, row) in self.rows.iter().enumerate() { + let row_char_count = row.char_count_excluding_newline(); + + if ccursor_it.index <= cursor.index && cursor.index <= ccursor_it.index + row_char_count + { + let column = cursor.index - ccursor_it.index; + + let select_next_row_instead = prefer_next_row + && !row.ends_with_newline + && column >= row.char_count_excluding_newline(); + if !select_next_row_instead { + return LayoutCursor { + row: row_nr, + column, + }; + } + } + ccursor_it.index += row.char_count_including_newline(); + } + debug_assert!(ccursor_it == self.end()); - pub fn end_rcursor(&self) -> RCursor { if let Some(last_row) = self.rows.last() { - RCursor { + LayoutCursor { row: self.rows.len() - 1, column: last_row.char_count_including_newline(), } @@ -932,268 +895,156 @@ impl Galley { Default::default() } } -} -/// ## Cursor conversions -impl Galley { - // The returned cursor is clamped. - pub fn from_ccursor(&self, ccursor: CCursor) -> Cursor { - let prefer_next_row = ccursor.prefer_next_row; - let mut ccursor_it = CCursor { - index: 0, - prefer_next_row, - }; - let mut pcursor_it = PCursor { - paragraph: 0, - offset: 0, - prefer_next_row, - }; - - for (row_nr, row) in self.rows.iter().enumerate() { - let row_char_count = row.char_count_excluding_newline(); - - if ccursor_it.index <= ccursor.index - && ccursor.index <= ccursor_it.index + row_char_count - { - let column = ccursor.index - ccursor_it.index; - - let select_next_row_instead = prefer_next_row - && !row.ends_with_newline - && column >= row.char_count_excluding_newline(); - if !select_next_row_instead { - pcursor_it.offset += column; - return Cursor { - ccursor, - rcursor: RCursor { - row: row_nr, - column, - }, - pcursor: pcursor_it, - }; - } - } - ccursor_it.index += row.char_count_including_newline(); - if row.ends_with_newline { - pcursor_it.paragraph += 1; - pcursor_it.offset = 0; - } else { - pcursor_it.offset += row.char_count_including_newline(); - } - } - debug_assert!(ccursor_it == self.end().ccursor); - Cursor { - ccursor: ccursor_it, // clamp - rcursor: self.end_rcursor(), - pcursor: pcursor_it, - } - } - - pub fn from_rcursor(&self, rcursor: RCursor) -> Cursor { - if rcursor.row >= self.rows.len() { + fn cursor_from_layout(&self, layout_cursor: LayoutCursor) -> CCursor { + if layout_cursor.row >= self.rows.len() { return self.end(); } let prefer_next_row = - rcursor.column < self.rows[rcursor.row].char_count_excluding_newline(); - let mut ccursor_it = CCursor { + layout_cursor.column < self.rows[layout_cursor.row].char_count_excluding_newline(); + let mut cursor_it = CCursor { index: 0, prefer_next_row, }; - let mut pcursor_it = PCursor { - paragraph: 0, - offset: 0, - prefer_next_row, - }; for (row_nr, row) in self.rows.iter().enumerate() { - if row_nr == rcursor.row { - ccursor_it.index += rcursor.column.at_most(row.char_count_excluding_newline()); + if row_nr == layout_cursor.row { + cursor_it.index += layout_cursor + .column + .at_most(row.char_count_excluding_newline()); - if row.ends_with_newline { - // Allow offset to go beyond the end of the paragraph - pcursor_it.offset += rcursor.column; - } else { - pcursor_it.offset += rcursor.column.at_most(row.char_count_excluding_newline()); - } - return Cursor { - ccursor: ccursor_it, - rcursor, - pcursor: pcursor_it, - }; - } - ccursor_it.index += row.char_count_including_newline(); - if row.ends_with_newline { - pcursor_it.paragraph += 1; - pcursor_it.offset = 0; - } else { - pcursor_it.offset += row.char_count_including_newline(); + return cursor_it; } + cursor_it.index += row.char_count_including_newline(); } - Cursor { - ccursor: ccursor_it, - rcursor: self.end_rcursor(), - pcursor: pcursor_it, - } - } - - // TODO(emilk): return identical cursor, or clamp? - pub fn from_pcursor(&self, pcursor: PCursor) -> Cursor { - let prefer_next_row = pcursor.prefer_next_row; - let mut ccursor_it = CCursor { - index: 0, - prefer_next_row, - }; - let mut pcursor_it = PCursor { - paragraph: 0, - offset: 0, - prefer_next_row, - }; - - for (row_nr, row) in self.rows.iter().enumerate() { - if pcursor_it.paragraph == pcursor.paragraph { - // Right paragraph, but is it the right row in the paragraph? - - if pcursor_it.offset <= pcursor.offset - && (pcursor.offset <= pcursor_it.offset + row.char_count_excluding_newline() - || row.ends_with_newline) - { - let column = pcursor.offset - pcursor_it.offset; - - let select_next_row_instead = pcursor.prefer_next_row - && !row.ends_with_newline - && column >= row.char_count_excluding_newline(); - - if !select_next_row_instead { - ccursor_it.index += column.at_most(row.char_count_excluding_newline()); - - return Cursor { - ccursor: ccursor_it, - rcursor: RCursor { - row: row_nr, - column, - }, - pcursor, - }; - } - } - } - - ccursor_it.index += row.char_count_including_newline(); - if row.ends_with_newline { - pcursor_it.paragraph += 1; - pcursor_it.offset = 0; - } else { - pcursor_it.offset += row.char_count_including_newline(); - } - } - Cursor { - ccursor: ccursor_it, - rcursor: self.end_rcursor(), - pcursor, - } + cursor_it } } /// ## Cursor positions impl Galley { - pub fn cursor_left_one_character(&self, cursor: &Cursor) -> Cursor { - if cursor.ccursor.index == 0 { + #[allow(clippy::unused_self)] + pub fn cursor_left_one_character(&self, cursor: &CCursor) -> CCursor { + if cursor.index == 0 { Default::default() } else { - let ccursor = CCursor { - index: cursor.ccursor.index, - prefer_next_row: true, // default to this when navigating. It is more often useful to put cursor at the begging of a row than at the end. - }; - self.from_ccursor(ccursor - 1) + CCursor { + index: cursor.index - 1, + prefer_next_row: true, // default to this when navigating. It is more often useful to put cursor at the beginning of a row than at the end. + } } } - pub fn cursor_right_one_character(&self, cursor: &Cursor) -> Cursor { - let ccursor = CCursor { - index: cursor.ccursor.index, - prefer_next_row: true, // default to this when navigating. It is more often useful to put cursor at the begging of a row than at the end. - }; - self.from_ccursor(ccursor + 1) + pub fn cursor_right_one_character(&self, cursor: &CCursor) -> CCursor { + CCursor { + index: (cursor.index + 1).min(self.end().index), + prefer_next_row: true, // default to this when navigating. It is more often useful to put cursor at the beginning of a row than at the end. + } } - pub fn cursor_up_one_row(&self, cursor: &Cursor) -> Cursor { - if cursor.rcursor.row == 0 { - Cursor::default() + pub fn cursor_up_one_row( + &self, + cursor: &CCursor, + h_pos: Option, + ) -> (CCursor, Option) { + let layout_cursor = self.layout_from_cursor(*cursor); + let h_pos = h_pos.unwrap_or_else(|| self.pos_from_layout_cursor(&layout_cursor).center().x); + if layout_cursor.row == 0 { + (CCursor::default(), None) } else { - let new_row = cursor.rcursor.row - 1; + let new_row = layout_cursor.row - 1; - let cursor_is_beyond_end_of_current_row = cursor.rcursor.column - >= self.rows[cursor.rcursor.row].char_count_excluding_newline(); - - let new_rcursor = if cursor_is_beyond_end_of_current_row { - // keep same column - RCursor { - row: new_row, - column: cursor.rcursor.column, - } - } else { + let new_layout_cursor = { // keep same X coord - let x = self.pos_from_cursor(cursor).center().x; - let column = if x > self.rows[new_row].rect.right() { - // beyond the end of this row - keep same column - cursor.rcursor.column - } else { - self.rows[new_row].char_at(x) - }; - RCursor { + let column = self.rows[new_row].char_at(h_pos); + LayoutCursor { row: new_row, column, } }; - self.from_rcursor(new_rcursor) + (self.cursor_from_layout(new_layout_cursor), Some(h_pos)) } } - pub fn cursor_down_one_row(&self, cursor: &Cursor) -> Cursor { - if cursor.rcursor.row + 1 < self.rows.len() { - let new_row = cursor.rcursor.row + 1; + pub fn cursor_down_one_row( + &self, + cursor: &CCursor, + h_pos: Option, + ) -> (CCursor, Option) { + let layout_cursor = self.layout_from_cursor(*cursor); + let h_pos = h_pos.unwrap_or_else(|| self.pos_from_layout_cursor(&layout_cursor).center().x); + if layout_cursor.row + 1 < self.rows.len() { + let new_row = layout_cursor.row + 1; - let cursor_is_beyond_end_of_current_row = cursor.rcursor.column - >= self.rows[cursor.rcursor.row].char_count_excluding_newline(); - - let new_rcursor = if cursor_is_beyond_end_of_current_row { - // keep same column - RCursor { - row: new_row, - column: cursor.rcursor.column, - } - } else { + let new_layout_cursor = { // keep same X coord - let x = self.pos_from_cursor(cursor).center().x; - let column = if x > self.rows[new_row].rect.right() { - // beyond the end of the next row - keep same column - cursor.rcursor.column - } else { - self.rows[new_row].char_at(x) - }; - RCursor { + let column = self.rows[new_row].char_at(h_pos); + LayoutCursor { row: new_row, column, } }; - self.from_rcursor(new_rcursor) + (self.cursor_from_layout(new_layout_cursor), Some(h_pos)) } else { - self.end() + (self.end(), None) } } - pub fn cursor_begin_of_row(&self, cursor: &Cursor) -> Cursor { - self.from_rcursor(RCursor { - row: cursor.rcursor.row, + pub fn cursor_begin_of_row(&self, cursor: &CCursor) -> CCursor { + let layout_cursor = self.layout_from_cursor(*cursor); + self.cursor_from_layout(LayoutCursor { + row: layout_cursor.row, column: 0, }) } - pub fn cursor_end_of_row(&self, cursor: &Cursor) -> Cursor { - self.from_rcursor(RCursor { - row: cursor.rcursor.row, - column: self.rows[cursor.rcursor.row].char_count_excluding_newline(), + pub fn cursor_end_of_row(&self, cursor: &CCursor) -> CCursor { + let layout_cursor = self.layout_from_cursor(*cursor); + self.cursor_from_layout(LayoutCursor { + row: layout_cursor.row, + column: self.rows[layout_cursor.row].char_count_excluding_newline(), }) } + + pub fn cursor_begin_of_paragraph(&self, cursor: &CCursor) -> CCursor { + let mut layout_cursor = self.layout_from_cursor(*cursor); + layout_cursor.column = 0; + + loop { + let prev_row = layout_cursor + .row + .checked_sub(1) + .and_then(|row| self.rows.get(row)); + + let Some(prev_row) = prev_row else { + // This is the first row + break; + }; + + if prev_row.ends_with_newline { + break; + } + + layout_cursor.row -= 1; + } + + self.cursor_from_layout(layout_cursor) + } + + pub fn cursor_end_of_paragraph(&self, cursor: &CCursor) -> CCursor { + let mut layout_cursor = self.layout_from_cursor(*cursor); + loop { + let row = &self.rows[layout_cursor.row]; + if row.ends_with_newline || layout_cursor.row == self.rows.len() - 1 { + layout_cursor.column = row.char_count_excluding_newline(); + break; + } + + layout_cursor.row += 1; + } + + self.cursor_from_layout(layout_cursor) + } } From b0bbca4e69384e9aa076be95e084fd820b614e73 Mon Sep 17 00:00:00 2001 From: Sven Niederberger <73159570+s-nie@users.noreply.github.com> Date: Thu, 20 Mar 2025 10:51:02 +0100 Subject: [PATCH 017/129] Add `OutputCommand::SetPointerPosition` to set mouse position (#5776) This is not supported on the web and not yet on Wayland. ~~I also had to update `ring` and add an exception for `paste` being unmaintained.~~ Has since been updated on master. * [x] I have followed the instructions in the PR template --- crates/eframe/src/web/app_runner.rs | 3 +++ crates/egui-winit/src/lib.rs | 3 +++ crates/egui/src/context.rs | 5 +++++ crates/egui/src/data/output.rs | 3 +++ crates/egui_demo_lib/src/demo/tests/cursor_test.rs | 8 ++++++++ 5 files changed, 22 insertions(+) diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index 76ae1761f..835b7f008 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -336,6 +336,9 @@ impl AppRunner { egui::OutputCommand::OpenUrl(open_url) => { super::open_url(&open_url.url, open_url.new_tab); } + egui::OutputCommand::SetPointerPosition(_) => { + // Not supported on the web. + } } } diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 147ab7186..53e4ad5ec 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -850,6 +850,9 @@ impl State { egui::OutputCommand::OpenUrl(open_url) => { open_url_in_browser(&open_url.url); } + egui::OutputCommand::SetPointerPosition(egui::Pos2 { x, y }) => { + let _ = window.set_cursor_position(winit::dpi::LogicalPosition { x, y }); + } } } diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 0953573c4..3bdf1507f 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1482,6 +1482,11 @@ impl Context { self.send_cmd(crate::OutputCommand::CopyImage(image)); } + /// Set the mouse cursor position (if the platform supports it). + pub fn set_pointer_position(&self, position: Pos2) { + self.send_cmd(crate::OutputCommand::SetPointerPosition(position)); + } + /// Format the given shortcut in a human-readable way (e.g. `Ctrl+Shift+X`). /// /// Can be used to get the text for [`crate::Button::shortcut_text`]. diff --git a/crates/egui/src/data/output.rs b/crates/egui/src/data/output.rs index 2fdaec1e1..dcc03242e 100644 --- a/crates/egui/src/data/output.rs +++ b/crates/egui/src/data/output.rs @@ -95,6 +95,9 @@ pub enum OutputCommand { /// Open this url in a browser. OpenUrl(OpenUrl), + + /// Set the mouse cursor position (if the platform supports it). + SetPointerPosition(emath::Pos2), } /// The non-rendering part of what egui emits each frame. diff --git a/crates/egui_demo_lib/src/demo/tests/cursor_test.rs b/crates/egui_demo_lib/src/demo/tests/cursor_test.rs index 78214d5eb..6927e1af9 100644 --- a/crates/egui_demo_lib/src/demo/tests/cursor_test.rs +++ b/crates/egui_demo_lib/src/demo/tests/cursor_test.rs @@ -16,6 +16,14 @@ impl crate::Demo for CursorTest { impl crate::View for CursorTest { fn ui(&mut self, ui: &mut egui::Ui) { + if ui + .button("Center pointer in window") + .on_hover_text("The platform may not support this.") + .clicked() + { + let position = ui.ctx().available_rect().center(); + ui.ctx().set_pointer_position(position); + } ui.vertical_centered_justified(|ui| { ui.heading("Hover to switch cursor icon:"); for &cursor_icon in &egui::CursorIcon::ALL { From 77244cd4c5ebb924beaec8f89604a2881106b27c Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Thu, 20 Mar 2025 05:51:42 -0400 Subject: [PATCH 018/129] =?UTF-8?q?=E2=9A=A0=EF=B8=8F=20Close=20popup=20if?= =?UTF-8?q?=20`Memory::keep=5Fpopup=5Fopen`=20isn't=20called=20(#5814)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Breaking changes: - When using the Memory::popup state, it's now required to call keep_popup_open each frame or the popup will close. - Usually handled by the `Popup` struct, but required for custom popups using the state in `Memory` directly ----- If a popup is abandoned `Memory::popup` would remain `Some`. This is problematic if, for example, you have logic that checks `is_any_popup_open`. This PR adds a new requirement for popups keeping their open state in `Memory::popup`. They must call `Memory::keep_popup_open` as long as they are being rendered. The recent changes in #5716 make this easy to implement. Supersedes #4697 which had an awkward implementation These two videos show a case where a context menu was open when the underlying widget got removed. Before (`any_popup_open` remains `true`) ![Screenshot 2025-03-16 at 18 22 50](https://github.com/user-attachments/assets/22db64dd-e6f2-4501-9bda-39f470b9210c) After ![Screenshot 2025-03-16 at 18 21 14](https://github.com/user-attachments/assets/bd4631b1-a0ad-4047-a14d-cd4999710e07) * Closes https://github.com/emilk/egui/issues/3657 * [x] I have followed the instructions in the PR template --- crates/egui/src/containers/popup.rs | 4 +- crates/egui/src/memory/mod.rs | 58 ++++++++++++++++++++++++++--- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index 7c72d2dd3..7159a0525 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -530,7 +530,9 @@ impl<'a> Popup<'a> { Some(SetOpenCommand::Toggle) => { mem.toggle_popup(id); } - None => {} + None => { + mem.keep_popup_open(id); + } }); } diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index c68ba4da5..0f588ca53 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -94,7 +94,7 @@ pub struct Memory { /// If position is [`None`], the popup position will be calculated based on some configuration /// (e.g. relative to some other widget). #[cfg_attr(feature = "persistence", serde(skip))] - popup: Option<(Id, Option)>, + popup: Option, #[cfg_attr(feature = "persistence", serde(skip))] everything_is_visible: bool, @@ -807,6 +807,15 @@ impl Memory { self.caches.update(); self.areas_mut().end_pass(); self.focus_mut().end_pass(used_ids); + + // Clean up abandoned popups. + if let Some(popup) = &mut self.popup { + if popup.open_this_frame { + popup.open_this_frame = false; + } else { + self.popup = None; + } + } } pub(crate) fn set_viewport_id(&mut self, viewport_id: ViewportId) { @@ -1068,13 +1077,37 @@ impl Memory { } } +/// State of an open popup. +#[derive(Clone, Copy, Debug)] +struct OpenPopup { + /// Id of the popup. + id: Id, + + /// Optional position of the popup. + pos: Option, + + /// Whether this popup was still open this frame. Otherwise it's considered abandoned and `Memory::popup` will be cleared. + open_this_frame: bool, +} + +impl OpenPopup { + /// Create a new `OpenPopup`. + fn new(id: Id, pos: Option) -> Self { + Self { + id, + pos, + open_this_frame: true, + } + } +} + /// ## Popups /// Popups are things like combo-boxes, color pickers, menus etc. /// Only one can be open at a time. impl Memory { /// Is the given popup open? pub fn is_popup_open(&self, popup_id: Id) -> bool { - self.popup.is_some_and(|(id, _)| id == popup_id) || self.everything_is_visible() + self.popup.is_some_and(|state| state.id == popup_id) || self.everything_is_visible() } /// Is any popup open? @@ -1083,19 +1116,34 @@ impl Memory { } /// Open the given popup and close all others. + /// + /// Note that you must call `keep_popup_open` on subsequent frames as long as the popup is open. pub fn open_popup(&mut self, popup_id: Id) { - self.popup = Some((popup_id, None)); + self.popup = Some(OpenPopup::new(popup_id, None)); + } + + /// Popups must call this every frame while open. + /// + /// This is needed because in some cases popups can go away without `close_popup` being + /// called. For example, when a context menu is open and the underlying widget stops + /// being rendered. + pub fn keep_popup_open(&mut self, popup_id: Id) { + if let Some(state) = self.popup.as_mut() { + if state.id == popup_id { + state.open_this_frame = true; + } + } } /// Open the popup and remember its position. pub fn open_popup_at(&mut self, popup_id: Id, pos: impl Into>) { - self.popup = Some((popup_id, pos.into())); + self.popup = Some(OpenPopup::new(popup_id, pos.into())); } /// Get the position for this popup. pub fn popup_position(&self, id: Id) -> Option { self.popup - .and_then(|(popup_id, pos)| if popup_id == id { pos } else { None }) + .and_then(|state| if state.id == id { state.pos } else { None }) } /// Close any currently open popup. From d0bd525b5d0d9bf9168307ea0cc4f99105871793 Mon Sep 17 00:00:00 2001 From: valadaptive <79560998+valadaptive@users.noreply.github.com> Date: Thu, 20 Mar 2025 06:00:12 -0400 Subject: [PATCH 019/129] Bump accesskit to 0.18 and make it a workspace dependency (#5783) This can't be merged until [kittest's accesskit is bumped](https://github.com/rerun-io/kittest/pull/9) ~~and [a new version of rfd is released](https://github.com/PolyMeilex/rfd/pull/240)~~. --------- Co-authored-by: Lucas Meurer --- Cargo.lock | 679 +++++++++---------- Cargo.toml | 4 +- crates/eframe/src/native/glow_integration.rs | 2 +- crates/eframe/src/native/wgpu_integration.rs | 2 +- crates/egui-winit/Cargo.toml | 2 +- crates/egui-winit/src/lib.rs | 2 + crates/egui/Cargo.toml | 2 +- crates/egui_demo_app/Cargo.toml | 4 +- examples/file_dialog/Cargo.toml | 2 +- 9 files changed, 326 insertions(+), 373 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b562a1ecb..3a04d32f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,57 +20,72 @@ checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" [[package]] name = "accesskit" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45c97bb3cc1dacbdc6d1147040fc61309590d3e1ab5efd92a8a09c7a2e07284c" +checksum = "becf0eb5215b6ecb0a739c31c21bd83c4f326524c9b46b7e882d77559b60a529" dependencies = [ "enumn", "serde", ] [[package]] -name = "accesskit_atspi_common" -version = "0.10.0" +name = "accesskit_android" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03db49d2948db6875c69a1ef17816efa8e3d9f36c7cd79e467d8562a6695662b" +checksum = "d42f298f1db7c022cc69f20f06085b34b08ffae79b37488b7aae20b210777d17" +dependencies = [ + "accesskit", + "accesskit_consumer", + "jni", + "log", + "once_cell", + "paste", +] + +[[package]] +name = "accesskit_atspi_common" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce9928251cd5651ae983a77aeaa528471eed47cf705885e0b03249b72fe4e8e1" dependencies = [ "accesskit", "accesskit_consumer", "atspi-common", "serde", "thiserror 1.0.66", - "zvariant 4.2.0", + "zvariant", ] [[package]] name = "accesskit_consumer" -version = "0.25.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa3a17950ce0d911f132387777b9b3d05eddafb59b773ccaa53fceefaeb0228e" +checksum = "d0bf66a7bf0b7ea4fd7742d50b64782a88f99217cf246b3f93b4162528dde520" dependencies = [ "accesskit", + "hashbrown", "immutable-chunkmap", ] [[package]] name = "accesskit_macos" -version = "0.18.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8d94b7544775dddce398e2500a8b3cc2be3655190879071ce6a9e5610195be4" +checksum = "09e230718177753b4e4ad9e1d9f6cfc2f4921212d4c1c480b253f526babb258d" dependencies = [ "accesskit", "accesskit_consumer", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "once_cell", + "hashbrown", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", ] [[package]] name = "accesskit_unix" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a88d913b144104dd825f75db1b82c63d754b01c53c2f9b7545dcdfae63bb0ed" +checksum = "2ef06642e9f02f1708ad55e1eaeb8ad6956c22917699c4f313afa4f8f1b5e664" dependencies = [ "accesskit", "accesskit_atspi_common", @@ -81,17 +96,18 @@ dependencies = [ "futures-lite", "futures-util", "serde", - "zbus 4.4.0", + "zbus", ] [[package]] name = "accesskit_windows" -version = "0.24.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0aaa870a5d047338f03707706141f22c98c20e79d5403bf3c9b195549e6cdeea" +checksum = "65178f3df98a51e4238e584fcb255cb1a4f9111820848eeddd37663be40a625f" dependencies = [ "accesskit", "accesskit_consumer", + "hashbrown", "paste", "static_assertions", "windows", @@ -100,11 +116,12 @@ dependencies = [ [[package]] name = "accesskit_winit" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3555a67a9bb208f620cfc3746f1502d1512f0ffbdb19c6901aa90b111aa56ec5" +checksum = "2c28531b0a1612b46d057a724a1e3de42a4bb101ff9f18c96c32f605b6e5ef06" dependencies = [ "accesskit", + "accesskit_android", "accesskit_macos", "accesskit_unix", "accesskit_windows", @@ -134,11 +151,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.2.15", "once_cell", "serde", "version_check", - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -150,12 +167,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "allocator-api2" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" - [[package]] name = "android-activity" version = "0.6.0" @@ -243,9 +254,9 @@ dependencies = [ "core-graphics", "image", "log", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", "parking_lot", "windows-sys 0.48.0", "x11rb", @@ -280,9 +291,9 @@ dependencies = [ [[package]] name = "ashpd" -version = "0.10.2" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c39d707614dbcc6bed00015539f488d8e3fe3e66ed60961efc0c90f4b380b3" +checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df" dependencies = [ "async-fs", "async-net", @@ -294,10 +305,7 @@ dependencies = [ "serde", "serde_repr", "url", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "zbus 5.1.1", + "zbus", ] [[package]] @@ -462,9 +470,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "atspi" -version = "0.22.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be534b16650e35237bb1ed189ba2aab86ce65e88cc84c66f4935ba38575cecbf" +checksum = "c83247582e7508838caf5f316c00791eee0e15c0bf743e6880585b867e16815c" dependencies = [ "atspi-common", "atspi-connection", @@ -473,42 +481,41 @@ dependencies = [ [[package]] name = "atspi-common" -version = "0.6.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1909ed2dc01d0a17505d89311d192518507e8a056a48148e3598fef5e7bb6ba7" +checksum = "33dfc05e7cdf90988a197803bf24f5788f94f7c94a69efa95683e8ffe76cfdfb" dependencies = [ "enumflags2", "serde", "static_assertions", - "zbus 4.4.0", + "zbus", "zbus-lockstep", "zbus-lockstep-macros", - "zbus_names 3.0.0", - "zvariant 4.2.0", + "zbus_names", + "zvariant", ] [[package]] name = "atspi-connection" -version = "0.6.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "430c5960624a4baaa511c9c0fcc2218e3b58f5dbcc47e6190cafee344b873333" +checksum = "4193d51303d8332304056ae0004714256b46b6635a5c556109b319c0d3784938" dependencies = [ "atspi-common", "atspi-proxies", "futures-lite", - "zbus 4.4.0", + "zbus", ] [[package]] name = "atspi-proxies" -version = "0.6.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e6c5de3e524cf967569722446bcd458d5032348554d9a17d7d72b041ab7496" +checksum = "d2eebcb9e7e76f26d0bcfd6f0295e1cd1e6f33bedbc5698a971db8dc43d7751c" dependencies = [ "atspi-common", "serde", - "zbus 4.4.0", - "zvariant 4.2.0", + "zbus", ] [[package]] @@ -604,22 +611,22 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - [[package]] name = "block2" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" dependencies = [ - "objc2", + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d59b4c170e16f0405a2e95aff44432a0d41aa97675f3d52623effe95792a037" +dependencies = [ + "objc2 0.6.0", ] [[package]] @@ -929,15 +936,6 @@ dependencies = [ "libc", ] -[[package]] -name = "cpufeatures" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" -dependencies = [ - "libc", -] - [[package]] name = "crc32fast" version = "1.4.2" @@ -1021,16 +1019,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - [[package]] name = "cursor-icon" version = "1.1.0" @@ -1117,16 +1105,6 @@ dependencies = [ "rayon", ] -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - [[package]] name = "directories" version = "5.0.1" @@ -1154,6 +1132,18 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "dispatch2" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a0d569e003ff27784e0e14e4a594048698e0c0f0b66cabcb51511be55a7caa0" +dependencies = [ + "bitflags 2.8.0", + "block2 0.6.0", + "libc", + "objc2 0.6.0", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -1226,12 +1216,12 @@ dependencies = [ "image", "js-sys", "log", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", "parking_lot", "percent-encoding", - "pollster 0.4.0", + "pollster", "profiling", "raw-window-handle 0.6.2", "ron", @@ -1396,7 +1386,7 @@ dependencies = [ "egui_extras", "image", "kittest", - "pollster 0.4.0", + "pollster", "wgpu", ] @@ -1635,6 +1625,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" + [[package]] name = "foreign-types" version = "0.5.0" @@ -1694,9 +1690,9 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" -version = "2.4.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f1fa2f9765705486b33fd2acf1577f8ec449c2ba1f318ae5447697b7c08d210" +checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" dependencies = [ "fastrand", "futures-core", @@ -1716,12 +1712,6 @@ dependencies = [ "syn", ] -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - [[package]] name = "futures-task" version = "0.3.31" @@ -1737,7 +1727,6 @@ dependencies = [ "futures-core", "futures-io", "futures-macro", - "futures-sink", "futures-task", "memchr", "pin-project-lite", @@ -1745,16 +1734,6 @@ dependencies = [ "slab", ] -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - [[package]] name = "gethostname" version = "0.4.3" @@ -1782,7 +1761,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets 0.52.6", ] [[package]] @@ -1839,9 +1830,9 @@ dependencies = [ "glutin_glx_sys", "glutin_wgl_sys", "libloading", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", "once_cell", "raw-window-handle 0.6.2", "wayland-sys", @@ -1923,9 +1914,9 @@ dependencies = [ [[package]] name = "gpu-descriptor" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c08c1f623a8d0b722b8b99f821eb0ba672a1618f0d3b16ddbee1cedd2dd8557" +checksum = "dcf29e94d6d243368b7a56caa16bc213e4f9f8ed38c4d9557069527b5d5281ca" dependencies = [ "bitflags 2.8.0", "gpu-descriptor-types", @@ -1953,12 +1944,11 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.5" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" dependencies = [ - "ahash", - "allocator-api2", + "foldhash", ] [[package]] @@ -2255,9 +2245,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.5.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" dependencies = [ "equivalent", "hashbrown", @@ -2364,8 +2354,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "kittest" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f659954571a3c132356bd15c25f0dcf14d270a28ec5c58797adc2f432831bed5" +source = "git+https://github.com/rerun-io/kittest?branch=main#5803de399c0061d4cd5479929277066faa034331" dependencies = [ "accesskit", "accesskit_consumer", @@ -2689,6 +2678,15 @@ dependencies = [ "objc2-encode", ] +[[package]] +name = "objc2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3531f65190d9cff863b77a99857e74c314dd16bf56c538c4b57c7cbc3f3a6e59" +dependencies = [ + "objc2-encode", +] + [[package]] name = "objc2-app-kit" version = "0.2.2" @@ -2696,15 +2694,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ "bitflags 2.8.0", - "block2", + "block2 0.5.1", "libc", - "objc2", + "objc2 0.5.2", "objc2-core-data", "objc2-core-image", - "objc2-foundation", + "objc2-foundation 0.2.2", "objc2-quartz-core", ] +[[package]] +name = "objc2-app-kit" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5906f93257178e2f7ae069efb89fbd6ee94f0592740b5f8a1512ca498814d0fb" +dependencies = [ + "bitflags 2.8.0", + "block2 0.6.0", + "objc2 0.6.0", + "objc2-foundation 0.3.0", +] + [[package]] name = "objc2-cloud-kit" version = "0.2.2" @@ -2712,10 +2722,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ "bitflags 2.8.0", - "block2", - "objc2", + "block2 0.5.1", + "objc2 0.5.2", "objc2-core-location", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -2724,9 +2734,9 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" dependencies = [ - "block2", - "objc2", - "objc2-foundation", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -2736,9 +2746,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ "bitflags 2.8.0", - "block2", - "objc2", - "objc2-foundation", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daeaf60f25471d26948a1c2f840e3f7d86f4109e3af4e8e4b5cd70c39690d925" +dependencies = [ + "bitflags 2.8.0", + "objc2 0.6.0", ] [[package]] @@ -2747,9 +2767,9 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ - "block2", - "objc2", - "objc2-foundation", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", "objc2-metal", ] @@ -2759,17 +2779,17 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" dependencies = [ - "block2", - "objc2", + "block2 0.5.1", + "objc2 0.5.2", "objc2-contacts", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] name = "objc2-encode" -version = "4.0.3" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" [[package]] name = "objc2-foundation" @@ -2778,10 +2798,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ "bitflags 2.8.0", - "block2", + "block2 0.5.1", "dispatch", "libc", - "objc2", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a21c6c9014b82c39515db5b396f91645182611c97d24637cf56ac01e5f8d998" +dependencies = [ + "bitflags 2.8.0", + "objc2 0.6.0", + "objc2-core-foundation", ] [[package]] @@ -2790,10 +2821,10 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" dependencies = [ - "block2", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -2803,9 +2834,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ "bitflags 2.8.0", - "block2", - "objc2", - "objc2-foundation", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -2815,9 +2846,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ "bitflags 2.8.0", - "block2", - "objc2", - "objc2-foundation", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", "objc2-metal", ] @@ -2827,8 +2858,8 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" dependencies = [ - "objc2", - "objc2-foundation", + "objc2 0.5.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -2838,13 +2869,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ "bitflags 2.8.0", - "block2", - "objc2", + "block2 0.5.1", + "objc2 0.5.2", "objc2-cloud-kit", "objc2-core-data", "objc2-core-image", "objc2-core-location", - "objc2-foundation", + "objc2-foundation 0.2.2", "objc2-link-presentation", "objc2-quartz-core", "objc2-symbols", @@ -2858,9 +2889,9 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" dependencies = [ - "block2", - "objc2", - "objc2-foundation", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -2870,10 +2901,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ "bitflags 2.8.0", - "block2", - "objc2", + "block2 0.5.1", + "objc2 0.5.2", "objc2-core-location", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -3087,12 +3118,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "pollster" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" - [[package]] name = "pollster" version = "0.4.0" @@ -3119,7 +3144,7 @@ version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -3214,16 +3239,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" -[[package]] -name = "quick-xml" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "quick-xml" version = "0.32.0" @@ -3240,6 +3255,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" dependencies = [ "memchr", + "serde", ] [[package]] @@ -3253,20 +3269,20 @@ dependencies = [ [[package]] name = "rand" -version = "0.8.5" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" dependencies = [ - "libc", "rand_chacha", "rand_core", + "zerocopy 0.8.23", ] [[package]] name = "rand_chacha" -version = "0.3.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", "rand_core", @@ -3274,11 +3290,11 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.4" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom", + "getrandom 0.3.1", ] [[package]] @@ -3349,7 +3365,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom", + "getrandom 0.2.15", "libredox", "thiserror 1.0.66", ] @@ -3405,24 +3421,26 @@ dependencies = [ [[package]] name = "rfd" -version = "0.15.1" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46f6f80a9b882647d9014673ca9925d30ffc9750f2eed2b4490e189eaebd01e8" +checksum = "80c844748fdc82aae252ee4594a89b6e7ebef1063de7951545564cbc4e57075d" dependencies = [ "ashpd", - "block2", + "block2 0.6.0", + "dispatch2", "js-sys", "log", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "pollster 0.3.0", + "objc2 0.6.0", + "objc2-app-kit 0.3.0", + "objc2-core-foundation", + "objc2-foundation 0.3.0", + "pollster", "raw-window-handle 0.6.2", "urlencoding", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -3442,7 +3460,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.15", "libc", "untrusted", "windows-sys 0.52.0", @@ -3630,17 +3648,6 @@ dependencies = [ "log", ] -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "shlex" version = "1.3.0" @@ -4063,7 +4070,7 @@ checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap", "toml_datetime", - "winnow", + "winnow 0.6.20", ] [[package]] @@ -4112,12 +4119,6 @@ dependencies = [ "rustc-hash", ] -[[package]] -name = "typenum" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" - [[package]] name = "uds_windows" version = "1.1.0" @@ -4291,6 +4292,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasm-bindgen" version = "0.2.97" @@ -4956,7 +4966,7 @@ dependencies = [ "android-activity", "atomic-waker", "bitflags 2.8.0", - "block2", + "block2 0.5.1", "bytemuck", "calloop", "cfg_aliases", @@ -4969,9 +4979,9 @@ dependencies = [ "libc", "memmap2", "ndk", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", "objc2-ui-kit", "orbclient", "percent-encoding", @@ -5007,6 +5017,24 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags 2.8.0", +] + [[package]] name = "write16" version = "1.0.0" @@ -5137,9 +5165,9 @@ dependencies = [ [[package]] name = "zbus" -version = "4.4.0" +version = "5.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" +checksum = "59c333f648ea1b647bc95dc1d34807c8e25ed7a6feff3394034dc4776054b236" dependencies = [ "async-broadcast", "async-executor", @@ -5154,45 +5182,7 @@ dependencies = [ "enumflags2", "event-listener", "futures-core", - "futures-sink", - "futures-util", - "hex", - "nix", - "ordered-stream", - "rand", - "serde", - "serde_repr", - "sha1", - "static_assertions", - "tracing", - "uds_windows", - "windows-sys 0.52.0", - "xdg-home", - "zbus_macros 4.4.0", - "zbus_names 3.0.0", - "zvariant 4.2.0", -] - -[[package]] -name = "zbus" -version = "5.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1162094dc63b1629fcc44150bcceeaa80798cd28bcbe7fa987b65a034c258608" -dependencies = [ - "async-broadcast", - "async-executor", - "async-fs", - "async-io", - "async-lock", - "async-process", - "async-recursion", - "async-task", - "async-trait", - "blocking", - "enumflags2", - "event-listener", - "futures-core", - "futures-util", + "futures-lite", "hex", "nix", "ordered-stream", @@ -5202,74 +5192,50 @@ dependencies = [ "tracing", "uds_windows", "windows-sys 0.59.0", - "winnow", + "winnow 0.7.3", "xdg-home", - "zbus_macros 5.1.1", - "zbus_names 4.1.0", - "zvariant 5.1.0", + "zbus_macros", + "zbus_names", + "zvariant", ] [[package]] name = "zbus-lockstep" -version = "0.4.4" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca2c5dceb099bddaade154055c926bb8ae507a18756ba1d8963fd7b51d8ed1d" +checksum = "a22426b1bc2aca91de97772506f0655fa373448e6010d79d5d5880915c388409" dependencies = [ "zbus_xml", - "zvariant 4.2.0", + "zvariant", ] [[package]] name = "zbus-lockstep-macros" -version = "0.4.4" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709ab20fc57cb22af85be7b360239563209258430bccf38d8b979c5a2ae3ecce" +checksum = "100ffec29ed51859052f4563061abe35557acb56ba574510571f8398efc70a29" dependencies = [ "proc-macro2", "quote", "syn", "zbus-lockstep", "zbus_xml", - "zvariant 4.2.0", + "zvariant", ] [[package]] name = "zbus_macros" -version = "4.4.0" +version = "5.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" +checksum = "f325ad10eb0d0a3eb060203494c3b7ec3162a01a59db75d2deee100339709fc0" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "syn", - "zvariant_utils 2.1.0", -] - -[[package]] -name = "zbus_macros" -version = "5.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cd2dcdce3e2727f7d74b7e33b5a89539b3cc31049562137faf7ae4eb86cd16d" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", - "zbus_names 4.1.0", - "zvariant 5.1.0", - "zvariant_utils 3.0.2", -] - -[[package]] -name = "zbus_names" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" -dependencies = [ - "serde", - "static_assertions", - "zvariant 4.2.0", + "zbus_names", + "zvariant", + "zvariant_utils", ] [[package]] @@ -5280,21 +5246,21 @@ checksum = "856b7a38811f71846fd47856ceee8bccaec8399ff53fb370247e66081ace647b" dependencies = [ "serde", "static_assertions", - "winnow", - "zvariant 5.1.0", + "winnow 0.6.20", + "zvariant", ] [[package]] name = "zbus_xml" -version = "4.0.0" +version = "5.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f374552b954f6abb4bd6ce979e6c9b38fb9d0cd7cc68a7d796e70c9f3a233" +checksum = "589e9a02bfafb9754bb2340a9e3b38f389772684c63d9637e76b1870377bec29" dependencies = [ - "quick-xml 0.30.0", + "quick-xml 0.36.2", "serde", "static_assertions", - "zbus_names 3.0.0", - "zvariant 4.2.0", + "zbus_names", + "zvariant", ] [[package]] @@ -5304,7 +5270,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", - "zerocopy-derive", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" +dependencies = [ + "zerocopy-derive 0.8.23", ] [[package]] @@ -5318,6 +5293,17 @@ dependencies = [ "syn", ] +[[package]] +name = "zerocopy-derive" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zerofrom" version = "0.1.5" @@ -5384,80 +5370,43 @@ dependencies = [ [[package]] name = "zvariant" -version = "4.2.0" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" -dependencies = [ - "endi", - "enumflags2", - "serde", - "static_assertions", - "zvariant_derive 4.2.0", -] - -[[package]] -name = "zvariant" -version = "5.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1200ee6ac32f1e5a312e455a949a4794855515d34f9909f4a3e082d14e1a56f" +checksum = "b2df9ee044893fcffbdc25de30546edef3e32341466811ca18421e3cd6c5a3ac" dependencies = [ "endi", "enumflags2", "serde", "static_assertions", "url", - "winnow", - "zvariant_derive 5.1.0", - "zvariant_utils 3.0.2", + "winnow 0.7.3", + "zvariant_derive", + "zvariant_utils", ] [[package]] name = "zvariant_derive" -version = "4.2.0" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" +checksum = "74170caa85b8b84cc4935f2d56a57c7a15ea6185ccdd7eadb57e6edd90f94b2f" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "syn", - "zvariant_utils 2.1.0", -] - -[[package]] -name = "zvariant_derive" -version = "5.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "687e3b97fae6c9104fbbd36c73d27d149abf04fb874e2efbd84838763daa8916" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", - "zvariant_utils 3.0.2", + "zvariant_utils", ] [[package]] name = "zvariant_utils" -version = "2.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zvariant_utils" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20d1d011a38f12360e5fcccceeff5e2c42a8eb7f27f0dcba97a0862ede05c9c6" +checksum = "e16edfee43e5d7b553b77872d99bc36afdda75c223ca7ad5e3fbecd82ca5fc34" dependencies = [ "proc-macro2", "quote", "serde", "static_assertions", "syn", - "winnow", + "winnow 0.7.3", ] diff --git a/Cargo.toml b/Cargo.toml index 3e4449cd7..f58f958bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,6 +68,8 @@ egui_glow = { version = "0.31.1", path = "crates/egui_glow", default-features = egui_kittest = { version = "0.31.1", path = "crates/egui_kittest", default-features = false } eframe = { version = "0.31.1", path = "crates/eframe", default-features = false } +accesskit = "0.18.0" +accesskit_winit = "0.24" ahash = { version = "0.8.11", default-features = false, features = [ "no-rng", # we don't need DOS-protection, so we let users opt-in to it instead "std", @@ -83,7 +85,7 @@ glutin = { version = "0.32.0", default-features = false } glutin-winit = { version = "0.5.0", default-features = false } home = "0.5.9" image = { version = "0.25", default-features = false } -kittest = { version = "0.1" } +kittest = { version = "0.1.0", git = "https://github.com/rerun-io/kittest", branch = "main" } log = { version = "0.4", features = ["std"] } nohash-hasher = "0.2" parking_lot = "0.12" diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index 877245e22..94b254682 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -272,7 +272,7 @@ impl<'app> GlowWinitApp<'app> { .. } = viewport { - egui_winit.init_accesskit(window, event_loop_proxy); + egui_winit.init_accesskit(event_loop, window, event_loop_proxy); } } diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index 0e0fcbf5d..60484cdee 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -249,7 +249,7 @@ impl<'app> WgpuWinitApp<'app> { #[cfg(feature = "accesskit")] { let event_loop_proxy = self.repaint_proxy.lock().clone(); - egui_winit.init_accesskit(&window, event_loop_proxy); + egui_winit.init_accesskit(event_loop, &window, event_loop_proxy); } let app_creator = std::mem::take(&mut self.app_creator) diff --git a/crates/egui-winit/Cargo.toml b/crates/egui-winit/Cargo.toml index e4a837823..83f65f880 100644 --- a/crates/egui-winit/Cargo.toml +++ b/crates/egui-winit/Cargo.toml @@ -67,7 +67,7 @@ winit = { workspace = true, default-features = false } #! ### Optional dependencies # feature accesskit -accesskit_winit = { version = "0.23", optional = true } +accesskit_winit = { workspace = true, optional = true } bytemuck = { workspace = true, optional = true } diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 53e4ad5ec..e7c5ffe87 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -166,12 +166,14 @@ impl State { #[cfg(feature = "accesskit")] pub fn init_accesskit + Send>( &mut self, + event_loop: &ActiveEventLoop, window: &Window, event_loop_proxy: winit::event_loop::EventLoopProxy, ) { profiling::function_scope!(); self.accesskit = Some(accesskit_winit::Adapter::with_event_loop_proxy( + event_loop, window, event_loop_proxy, )); diff --git a/crates/egui/Cargo.toml b/crates/egui/Cargo.toml index d0407b1c3..e00202d16 100644 --- a/crates/egui/Cargo.toml +++ b/crates/egui/Cargo.toml @@ -89,7 +89,7 @@ nohash-hasher.workspace = true profiling.workspace = true #! ### Optional dependencies -accesskit = { version = "0.17.0", optional = true } +accesskit = { workspace = true, optional = true } backtrace = { workspace = true, optional = true } diff --git a/crates/egui_demo_app/Cargo.toml b/crates/egui_demo_app/Cargo.toml index 6ed74efab..b3eeb565d 100644 --- a/crates/egui_demo_app/Cargo.toml +++ b/crates/egui_demo_app/Cargo.toml @@ -86,7 +86,7 @@ env_logger = { version = "0.10", default-features = false, features = [ "auto-color", "humantime", ] } -rfd = { version = "0.15", optional = true } +rfd = { version = "0.15.3", optional = true } # web: [target.'cfg(target_arch = "wasm32")'.dependencies] @@ -95,4 +95,4 @@ wasm-bindgen-futures.workspace = true web-sys.workspace = true [dev-dependencies] -egui_kittest = { workspace = true, features = ["eframe", "snapshot", "wgpu"] } \ No newline at end of file +egui_kittest = { workspace = true, features = ["eframe", "snapshot", "wgpu"] } diff --git a/examples/file_dialog/Cargo.toml b/examples/file_dialog/Cargo.toml index cf61cd45f..0cb873729 100644 --- a/examples/file_dialog/Cargo.toml +++ b/examples/file_dialog/Cargo.toml @@ -20,4 +20,4 @@ env_logger = { version = "0.10", default-features = false, features = [ "auto-color", "humantime", ] } -rfd = "0.15" +rfd = "0.15.3" From 1545dec7a8ae67eefdb38ee51399c899440a1601 Mon Sep 17 00:00:00 2001 From: Nicolas Gomez <8671100+MYDIH@users.noreply.github.com> Date: Thu, 20 Mar 2025 11:02:24 +0100 Subject: [PATCH 020/129] [egui_extra] Allow loading multi-mime formats using the image_loader (#5769) Hi ! I'm using egui and egui_extra to build a demo tool for a crate I'm building. In this crate I load favicons from a lot of websites, and I have some failures on some .ico files. After tracking this thing down, I guess there is two issues (kinda), in the image crate, 'image/vnd.microsoft.icon' isn't recognized as the valid mime for an ico image (I created a [PR](https://github.com/image-rs/image/pull/2434)), and the code to detect if a given mime type is compatible with the image crate decoders in this crate do not support multi-mime type formats. [ImageFormat::to_mime_type](https://github.com/image-rs/image/blob/85f2412d552ddd2f576e16d023fd352589f4c605/src/image.rs#L216C12-L216C24) is only returning one mime for a given format (which is fine), we compare the result of this method to guess if the format is valid/enabled. Retriveing the correct format using [ImageFormat::from_mime_type](https://github.com/image-rs/image/blob/85f2412d552ddd2f576e16d023fd352589f4c605/src/image.rs#L166) would allow more mime to be considered valid since multiple mime can match the same format. The same applies to the extension detection, which I also modified to stay consistent Thanks --- crates/egui_extras/src/loaders/image_loader.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/crates/egui_extras/src/loaders/image_loader.rs b/crates/egui_extras/src/loaders/image_loader.rs index ab3c9179d..af785f6f4 100644 --- a/crates/egui_extras/src/loaders/image_loader.rs +++ b/crates/egui_extras/src/loaders/image_loader.rs @@ -29,10 +29,7 @@ fn is_supported_uri(uri: &str) -> bool { }; // Uses only the enabled image crate features - ImageFormat::all() - .filter(ImageFormat::reading_enabled) - .flat_map(ImageFormat::extensions_str) - .any(|format_ext| ext == *format_ext) + ImageFormat::from_extension(ext).is_some_and(|format| format.reading_enabled()) } fn is_supported_mime(mime: &str) -> bool { @@ -52,10 +49,7 @@ fn is_supported_mime(mime: &str) -> bool { } // Uses only the enabled image crate features - ImageFormat::all() - .filter(ImageFormat::reading_enabled) - .map(|fmt| fmt.to_mime_type()) - .any(|format_mime| mime == format_mime) + ImageFormat::from_mime_type(mime).is_some_and(|format| format.reading_enabled()) } impl ImageLoader for ImageCrateLoader { From 52732b23a6a264849691a303955340a620b96050 Mon Sep 17 00:00:00 2001 From: StratusFearMe21 <57533634+StratusFearMe21@users.noreply.github.com> Date: Thu, 20 Mar 2025 04:03:17 -0600 Subject: [PATCH 021/129] impl AsRef<[u8]> for FontData (#5757) * [x] I have followed the instructions in the PR template This PR implements `AsRef<[u8]>` for `FontData`, allowing it to be passed into `fontdb`'s [`Source`](https://docs.rs/fontdb/0.16.2/fontdb/enum.Source.html) type. This would allow `egui` and `cosmic_text` to share font data with eachother --- crates/epaint/src/text/fonts.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index b952b2817..ccbf66f9c 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -144,6 +144,12 @@ impl FontData { } } +impl AsRef<[u8]> for FontData { + fn as_ref(&self) -> &[u8] { + self.font.as_ref() + } +} + // ---------------------------------------------------------------------------- /// Extra scale and vertical tweak to apply to all text of a certain font. From d54e29d3758f8abd0f97508ff89de46e739a0931 Mon Sep 17 00:00:00 2001 From: Jim Date: Thu, 20 Mar 2025 11:05:22 +0100 Subject: [PATCH 022/129] macOS: Add `movable_by_window_background` option to viewport (#5412) Add an option called `movable_by_window_background` alongside a new builder method. When set to true, the window is movable by dragging its background ([Apple Docs](https://developer.apple.com/documentation/appkit/nswindow/ismovablebywindowbackground)) This is exclusive to macOS systems, similar to `fullsize_content_view`. * [x] I have followed the instructions in the PR template --- crates/egui-winit/src/lib.rs | 4 +++- crates/egui/src/viewport.rs | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index e7c5ffe87..1c47d7eb8 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -1615,6 +1615,7 @@ pub fn create_winit_window_attributes( // macOS: fullsize_content_view: _fullsize_content_view, + movable_by_window_background: _movable_by_window_background, title_shown: _title_shown, titlebar_buttons_shown: _titlebar_buttons_shown, titlebar_shown: _titlebar_shown, @@ -1761,7 +1762,8 @@ pub fn create_winit_window_attributes( .with_title_hidden(!_title_shown.unwrap_or(true)) .with_titlebar_buttons_hidden(!_titlebar_buttons_shown.unwrap_or(true)) .with_titlebar_transparent(!_titlebar_shown.unwrap_or(true)) - .with_fullsize_content_view(_fullsize_content_view.unwrap_or(false)); + .with_fullsize_content_view(_fullsize_content_view.unwrap_or(false)) + .with_movable_by_window_background(_movable_by_window_background.unwrap_or(false)); } window_attributes diff --git a/crates/egui/src/viewport.rs b/crates/egui/src/viewport.rs index 791b34c79..9aba69910 100644 --- a/crates/egui/src/viewport.rs +++ b/crates/egui/src/viewport.rs @@ -291,6 +291,7 @@ pub struct ViewportBuilder { // macOS: pub fullsize_content_view: Option, + pub movable_by_window_background: Option, pub title_shown: Option, pub titlebar_buttons_shown: Option, pub titlebar_shown: Option, @@ -432,6 +433,15 @@ impl ViewportBuilder { self } + /// macOS: Set to `true` to allow the window to be moved by dragging the background. + /// + /// Enabling this feature can result in unexpected behaviour with draggable UI widgets such as sliders. + #[inline] + pub fn with_movable_by_background(mut self, value: bool) -> Self { + self.movable_by_window_background = Some(value); + self + } + /// macOS: Set to `false` to hide the window title. #[inline] pub fn with_title_shown(mut self, title_shown: bool) -> Self { @@ -640,6 +650,7 @@ impl ViewportBuilder { visible: new_visible, drag_and_drop: new_drag_and_drop, fullsize_content_view: new_fullsize_content_view, + movable_by_window_background: new_movable_by_window_background, title_shown: new_title_shown, titlebar_buttons_shown: new_titlebar_buttons_shown, titlebar_shown: new_titlebar_shown, @@ -825,6 +836,13 @@ impl ViewportBuilder { recreate_window = true; } + if new_movable_by_window_background.is_some() + && self.movable_by_window_background != new_movable_by_window_background + { + self.movable_by_window_background = new_movable_by_window_background; + recreate_window = true; + } + if new_drag_and_drop.is_some() && self.drag_and_drop != new_drag_and_drop { self.drag_and_drop = new_drag_and_drop; recreate_window = true; From 3f731ec79407ed1a116ec354dbf8c1bbf10773ed Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 21 Mar 2025 10:45:25 +0100 Subject: [PATCH 023/129] Fix semi-transparent colors appearing too bright (#5824) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bug was in `Color32::from_rgba_unmultiplied` and by extension affects: * `Color32::from_rgba_unmultiplied` * `hex_color!` * `HexColor` * `ColorImage::from_rgba_unmultiplied` * All images with transparency (png, webp, …) * `Color32::from_white_alpha` The bug caused translucent colors to appear too bright. ## More Color is hard. When I started out egui I thought "linear space is objectively better, for everything!" and then I've been slowly walking that back for various reasons: * sRGB textures not available everywhere * gamma-space is more _perceptually_ even, so it makes sense to use for anti-aliasing * other applications do everything in gamma space, so that's what people expect (this PR) Similarly, pre-multiplied alpha _makes sense_ for blending colors. It also enables additive colors, which is nice. But it does complicate things. Especially when mixed with sRGB/gamma (As @karhu [points out](https://github.com/emilk/egui/pull/5824#issuecomment-2738099254)). ## Related * Closes https://github.com/emilk/egui/issues/5751 * Closes https://github.com/emilk/egui/issues/5771 ? (probably; hard to tell without a repro) * But not https://github.com/emilk/egui/issues/5810 ## TODO * [x] I broke the RGBA u8 color picker. Fix it --------- Co-authored-by: Andreas Reich --- crates/ecolor/README.md | 4 +- crates/ecolor/src/color32.rs | 202 ++++++++++++++++-- crates/ecolor/src/hex_color_runtime.rs | 23 +- crates/ecolor/src/hsva.rs | 27 +-- crates/ecolor/src/lib.rs | 60 ++++-- crates/ecolor/src/rgba.rs | 101 +++++++-- crates/egui/src/ui.rs | 8 + .../tests/snapshots/imageviewer.png | 4 +- crates/egui_demo_lib/src/rendering_test.rs | 33 ++- .../tests/snapshots/demos/BĆ©zier Curve.png | 4 +- .../tests/snapshots/demos/Frame.png | 4 +- .../tests/snapshots/demos/Scene.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 +- .../tests/snapshots/widget_gallery.png | 4 +- 19 files changed, 399 insertions(+), 103 deletions(-) diff --git a/crates/ecolor/README.md b/crates/ecolor/README.md index 4c84da019..98e3c3725 100644 --- a/crates/ecolor/README.md +++ b/crates/ecolor/README.md @@ -8,4 +8,6 @@ A simple color storage and conversion library. -Made for [`egui`](https://github.com/emilk/egui/). +This crate is built for the wants and needs of [`egui`](https://github.com/emilk/egui/). + +If you want an actual _good_ color crate, use [`color`](https://crates.io/crates/color) instead. diff --git a/crates/ecolor/src/color32.rs b/crates/ecolor/src/color32.rs index e72a3b98f..b5e052762 100644 --- a/crates/ecolor/src/color32.rs +++ b/crates/ecolor/src/color32.rs @@ -5,10 +5,24 @@ use crate::{fast_round, linear_f32_from_linear_u8, Rgba}; /// Instead of manipulating this directly it is often better /// to first convert it to either [`Rgba`] or [`crate::Hsva`]. /// -/// Internally this uses 0-255 gamma space `sRGBA` color with premultiplied alpha. -/// Alpha channel is in linear space. +/// Internally this uses 0-255 gamma space `sRGBA` color with _premultiplied alpha_. /// -/// The special value of alpha=0 means the color is to be treated as an additive color. +/// It's the non-linear ("gamma") values that are multiplied with the alpha. +/// +/// Premultiplied alpha means that the color values have been pre-multiplied with the alpha (opacity). +/// This is in contrast with "normal" RGBA, where the alpha is _separate_ (or "unmultiplied"). +/// Using premultiplied alpha has some advantages: +/// * It allows encoding additive colors +/// * It is the better way to blend colors, e.g. when filtering texture colors +/// * Because the above, it is the better way to encode colors in a GPU texture +/// +/// The color space is assumed to be [sRGB](https://en.wikipedia.org/wiki/SRGB). +/// +/// All operations on `Color32` are done in "gamma space" (see ). +/// This is not physically correct, but it is fast and sometimes more perceptually even than linear space. +/// If you instead want to perform these operations in linear-space color, use [`Rgba`]. +/// +/// An `alpha=0` means the color is to be treated as an additive color. #[repr(C)] #[derive(Clone, Copy, Default, Eq, Hash, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -16,6 +30,7 @@ use crate::{fast_round, linear_f32_from_linear_u8, Rgba}; pub struct Color32(pub(crate) [u8; 4]); impl std::fmt::Debug for Color32 { + /// Prints the contents with premultiplied alpha! fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let [r, g, b, a] = self.0; write!(f, "#{r:02X}_{g:02X}_{b:02X}_{a:02X}") @@ -90,41 +105,49 @@ impl Color32 { #[deprecated = "Renamed to PLACEHOLDER"] pub const TEMPORARY_COLOR: Self = Self::PLACEHOLDER; + /// From RGB with alpha of 255 (opaque). #[inline] pub const fn from_rgb(r: u8, g: u8, b: u8) -> Self { Self([r, g, b, 255]) } + /// From RGB into an additive color (will make everything it blend with brighter). #[inline] pub const fn from_rgb_additive(r: u8, g: u8, b: u8) -> Self { Self([r, g, b, 0]) } /// From `sRGBA` with premultiplied alpha. + /// + /// You likely want to use [`Self::from_rgba_unmultiplied`] instead. #[inline] pub const fn from_rgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self { Self([r, g, b, a]) } - /// From `sRGBA` WITHOUT premultiplied alpha. + /// From `sRGBA` with separate alpha. + /// + /// This is a "normal" RGBA value that you would find in a color picker or a table somewhere. + /// + /// You can use [`Self::to_srgba_unmultiplied`] to get back these values, + /// but for transparent colors what you get back might be slightly different (rounding errors). #[inline] pub fn from_rgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self { use std::sync::OnceLock; match a { - // common-case optimization + // common-case optimization: 0 => Self::TRANSPARENT, - // common-case optimization + + // common-case optimization: 255 => Self::from_rgb(r, g, b), + a => { static LOOKUP_TABLE: OnceLock> = OnceLock::new(); let lut = LOOKUP_TABLE.get_or_init(|| { - use crate::{gamma_u8_from_linear_f32, linear_f32_from_gamma_u8}; (0..=u16::MAX) .map(|i| { let [value, alpha] = i.to_ne_bytes(); - let value_lin = linear_f32_from_gamma_u8(value); - let alpha_lin = linear_f32_from_linear_u8(alpha); - gamma_u8_from_linear_f32(value_lin * alpha_lin) + fast_round(value as f32 * linear_f32_from_linear_u8(alpha)) }) .collect() }); @@ -136,22 +159,26 @@ impl Color32 { } } + /// Opaque gray. #[doc(alias = "from_grey")] #[inline] pub const fn from_gray(l: u8) -> Self { Self([l, l, l, 255]) } + /// Black with the given opacity. #[inline] pub const fn from_black_alpha(a: u8) -> Self { Self([0, 0, 0, a]) } + /// White with the given opacity. #[inline] pub fn from_white_alpha(a: u8) -> Self { - Rgba::from_white_alpha(linear_f32_from_linear_u8(a)).into() + Self([a, a, a, a]) } + /// Additive white. #[inline] pub const fn from_additive_luminance(l: u8) -> Self { Self([l, l, l, 0]) @@ -162,21 +189,25 @@ impl Color32 { self.a() == 255 } + /// Red component multiplied by alpha. #[inline] pub const fn r(&self) -> u8 { self.0[0] } + /// Green component multiplied by alpha. #[inline] pub const fn g(&self) -> u8 { self.0[1] } + /// Blue component multiplied by alpha. #[inline] pub const fn b(&self) -> u8 { self.0[2] } + /// Alpha (opacity). #[inline] pub const fn a(&self) -> u8 { self.0[3] @@ -213,9 +244,26 @@ impl Color32 { (self.r(), self.g(), self.b(), self.a()) } + /// Convert to a normal "unmultiplied" RGBA color (i.e. with separate alpha). + /// + /// This will unmultiply the alpha. + /// + /// This is the inverse of [`Self::from_rgba_unmultiplied`], + /// but due to precision problems it may return slightly different values for transparent colors. #[inline] pub fn to_srgba_unmultiplied(&self) -> [u8; 4] { - Rgba::from(*self).to_srgba_unmultiplied() + let [r, g, b, a] = self.to_array(); + match a { + // Common-case optimization. + 0 | 255 => self.to_array(), + a => { + let factor = 255.0 / a as f32; + let r = fast_round(factor * r as f32); + let g = fast_round(factor * g as f32); + let b = fast_round(factor * b as f32); + [r, g, b, a] + } + } } /// Multiply with 0.5 to make color half as opaque, perceptually. @@ -291,7 +339,7 @@ impl Color32 { ) } - /// Blend two colors, so that `self` is behind the argument. + /// Blend two colors in gamma space, so that `self` is behind the argument. pub fn blend(self, on_top: Self) -> Self { self.gamma_multiply_u8(255 - on_top.a()) + on_top } @@ -333,3 +381,131 @@ impl std::ops::Add for Color32 { ]) } } + +#[cfg(test)] +mod test { + use super::*; + + fn test_rgba() -> impl Iterator { + [ + [0, 0, 0, 0], + [0, 0, 0, 255], + [10, 0, 30, 0], + [10, 0, 30, 40], + [10, 100, 200, 0], + [10, 100, 200, 100], + [10, 100, 200, 200], + [10, 100, 200, 255], + [10, 100, 200, 40], + [10, 20, 0, 0], + [10, 20, 0, 255], + [10, 20, 30, 255], + [10, 20, 30, 40], + [255, 255, 255, 0], + [255, 255, 255, 255], + ] + .into_iter() + } + + #[test] + fn test_color32_additive() { + let opaque = Color32::from_rgb(40, 50, 60); + let additive = Color32::from_rgb(255, 127, 10).additive(); + assert_eq!(additive.blend(opaque), opaque, "opaque on top of additive"); + assert_eq!( + opaque.blend(additive), + Color32::from_rgb(255, 177, 70), + "additive on top of opaque" + ); + } + + #[test] + fn test_color32_blend_vs_gamma_blend() { + let opaque = Color32::from_rgb(0x60, 0x60, 0x60); + let transparent = Color32::from_rgba_unmultiplied(168, 65, 65, 79); + assert_eq!( + transparent.blend(opaque), + opaque, + "Opaque on top of transparent" + ); + // Blending in gamma-space is the de-facto standard almost everywhere. + // Browsers and most image editors do it, and so it is what users expect. + assert_eq!( + opaque.blend(transparent), + Color32::from_rgb( + blend(0x60, 168, 79), + blend(0x60, 65, 79), + blend(0x60, 65, 79) + ), + "Transparent on top of opaque" + ); + + fn blend(dest: u8, src: u8, alpha: u8) -> u8 { + let src = src as f32 / 255.0; + let dest = dest as f32 / 255.0; + let alpha = alpha as f32 / 255.0; + fast_round((src * alpha + dest * (1.0 - alpha)) * 255.0) + } + } + + #[test] + fn color32_unmultiplied_round_trip() { + for in_rgba in test_rgba() { + let [r, g, b, a] = in_rgba; + if a == 0 { + continue; + } + + let c = Color32::from_rgba_unmultiplied(r, g, b, a); + let out_rgba = c.to_srgba_unmultiplied(); + + if a == 255 { + assert_eq!(in_rgba, out_rgba); + } else { + // There will be small rounding errors whenever the alpha is not 0 or 255, + // because we multiply and then unmultiply the alpha. + for (&a, &b) in in_rgba.iter().zip(out_rgba.iter()) { + assert!(a.abs_diff(b) <= 3, "{in_rgba:?} != {out_rgba:?}"); + } + } + } + } + + #[test] + fn from_black_white_alpha() { + for a in 0..=255 { + assert_eq!( + Color32::from_white_alpha(a), + Color32::from_rgba_unmultiplied(255, 255, 255, a) + ); + assert_eq!( + Color32::from_white_alpha(a), + Color32::WHITE.gamma_multiply_u8(a) + ); + + assert_eq!( + Color32::from_black_alpha(a), + Color32::from_rgba_unmultiplied(0, 0, 0, a) + ); + assert_eq!( + Color32::from_black_alpha(a), + Color32::BLACK.gamma_multiply_u8(a) + ); + } + } + + #[test] + fn to_from_rgba() { + for [r, g, b, a] in test_rgba() { + let original = Color32::from_rgba_unmultiplied(r, g, b, a); + let rgba = Rgba::from(original); + let back = Color32::from(rgba); + assert_eq!(back, original); + } + + assert_eq!( + Color32::from(Rgba::from_rgba_unmultiplied(1.0, 0.0, 0.0, 0.5)), + Color32::from_rgba_unmultiplied(255, 0, 0, 128) + ); + } +} diff --git a/crates/ecolor/src/hex_color_runtime.rs b/crates/ecolor/src/hex_color_runtime.rs index f817bf4b8..21e07ffc4 100644 --- a/crates/ecolor/src/hex_color_runtime.rs +++ b/crates/ecolor/src/hex_color_runtime.rs @@ -208,17 +208,22 @@ mod tests { #[test] fn hex_string_round_trip() { - use Color32 as C; let cases = [ - C::from_rgba_unmultiplied(10, 20, 30, 0), - C::from_rgba_unmultiplied(10, 20, 30, 40), - C::from_rgba_unmultiplied(10, 20, 30, 255), - C::from_rgba_unmultiplied(0, 20, 30, 0), - C::from_rgba_unmultiplied(10, 0, 30, 40), - C::from_rgba_unmultiplied(10, 20, 0, 255), + [0, 20, 30, 0], + [10, 0, 30, 40], + [10, 100, 200, 0], + [10, 100, 200, 100], + [10, 100, 200, 200], + [10, 100, 200, 255], + [10, 100, 200, 40], + [10, 20, 0, 255], + [10, 20, 30, 0], + [10, 20, 30, 255], + [10, 20, 30, 40], ]; - for color in cases { - assert_eq!(C::from_hex(color.to_hex().as_str()), Ok(color)); + for [r, g, b, a] in cases { + let color = Color32::from_rgba_unmultiplied(r, g, b, a); + assert_eq!(Color32::from_hex(color.to_hex().as_str()), Ok(color)); } } } diff --git a/crates/ecolor/src/hsva.rs b/crates/ecolor/src/hsva.rs index 5f5430cf0..02ffd67d6 100644 --- a/crates/ecolor/src/hsva.rs +++ b/crates/ecolor/src/hsva.rs @@ -1,6 +1,5 @@ use crate::{ - gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_f32_from_linear_u8, - linear_u8_from_linear_f32, Color32, Rgba, + gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_u8_from_linear_f32, Color32, Rgba, }; /// Hue, saturation, value, alpha. All in the range [0, 1]. @@ -29,30 +28,20 @@ impl Hsva { /// From `sRGBA` with premultiplied alpha #[inline] pub fn from_srgba_premultiplied([r, g, b, a]: [u8; 4]) -> Self { - Self::from_rgba_premultiplied( - linear_f32_from_gamma_u8(r), - linear_f32_from_gamma_u8(g), - linear_f32_from_gamma_u8(b), - linear_f32_from_linear_u8(a), - ) + Self::from(Color32::from_rgba_premultiplied(r, g, b, a)) } /// From `sRGBA` without premultiplied alpha #[inline] pub fn from_srgba_unmultiplied([r, g, b, a]: [u8; 4]) -> Self { - Self::from_rgba_unmultiplied( - linear_f32_from_gamma_u8(r), - linear_f32_from_gamma_u8(g), - linear_f32_from_gamma_u8(b), - linear_f32_from_linear_u8(a), - ) + Self::from(Color32::from_rgba_unmultiplied(r, g, b, a)) } /// From linear RGBA with premultiplied alpha #[inline] pub fn from_rgba_premultiplied(r: f32, g: f32, b: f32, a: f32) -> Self { #![allow(clippy::many_single_char_names)] - if a == 0.0 { + if a <= 0.0 { if r == 0.0 && b == 0.0 && a == 0.0 { Self::default() } else { @@ -152,13 +141,7 @@ impl Hsva { #[inline] pub fn to_srgba_premultiplied(&self) -> [u8; 4] { - let [r, g, b, a] = self.to_rgba_premultiplied(); - [ - gamma_u8_from_linear_f32(r), - gamma_u8_from_linear_f32(g), - gamma_u8_from_linear_f32(b), - linear_u8_from_linear_f32(a), - ] + Color32::from(*self).to_array() } /// To gamma-space 0-255. diff --git a/crates/ecolor/src/lib.rs b/crates/ecolor/src/lib.rs index a9400d9de..9c4ac9b66 100644 --- a/crates/ecolor/src/lib.rs +++ b/crates/ecolor/src/lib.rs @@ -1,9 +1,20 @@ //! Color conversions and types. //! +//! This crate is built for the wants and needs of [`egui`](https://github.com/emilk/egui/). +//! +//! If you want an actual _good_ color crate, use [`color`](https://crates.io/crates/color) instead. +//! //! If you want a compact color representation, use [`Color32`]. -//! If you want to manipulate RGBA colors use [`Rgba`]. +//! If you want to manipulate RGBA colors in linear space use [`Rgba`]. //! If you want to manipulate colors in a way closer to how humans think about colors, use [`HsvaGamma`]. //! +//! ## Conventions +//! The word "gamma" or "srgb" is used to refer to values in the non-linear space defined by +//! [the sRGB transfer function](https://en.wikipedia.org/wiki/SRGB). +//! We use `u8` for anything in the "gamma" space. +//! +//! We use `f32` in 0-1 range for anything in the linear space. +//! //! ## Feature flags #![cfg_attr(feature = "document-features", doc = document_features::document_features!())] //! @@ -39,23 +50,46 @@ pub use hex_color_runtime::*; impl From for Rgba { fn from(srgba: Color32) -> Self { - Self([ - linear_f32_from_gamma_u8(srgba.0[0]), - linear_f32_from_gamma_u8(srgba.0[1]), - linear_f32_from_gamma_u8(srgba.0[2]), - linear_f32_from_linear_u8(srgba.0[3]), - ]) + let [r, g, b, a] = srgba.to_array(); + if a == 0 { + // Additive, or completely transparent + Self([ + linear_f32_from_gamma_u8(r), + linear_f32_from_gamma_u8(g), + linear_f32_from_gamma_u8(b), + 0.0, + ]) + } else { + let a = linear_f32_from_linear_u8(a); + Self([ + linear_from_gamma(r as f32 / (255.0 * a)) * a, + linear_from_gamma(g as f32 / (255.0 * a)) * a, + linear_from_gamma(b as f32 / (255.0 * a)) * a, + a, + ]) + } } } impl From for Color32 { fn from(rgba: Rgba) -> Self { - Self([ - gamma_u8_from_linear_f32(rgba.0[0]), - gamma_u8_from_linear_f32(rgba.0[1]), - gamma_u8_from_linear_f32(rgba.0[2]), - linear_u8_from_linear_f32(rgba.0[3]), - ]) + let [r, g, b, a] = rgba.to_array(); + if a == 0.0 { + // Additive, or completely transparent + Self([ + gamma_u8_from_linear_f32(r), + gamma_u8_from_linear_f32(g), + gamma_u8_from_linear_f32(b), + 0, + ]) + } else { + Self([ + fast_round(gamma_u8_from_linear_f32(r / a) as f32 * a), + fast_round(gamma_u8_from_linear_f32(g / a) as f32 * a), + fast_round(gamma_u8_from_linear_f32(b / a) as f32 * a), + linear_u8_from_linear_f32(a), + ]) + } } } diff --git a/crates/ecolor/src/rgba.rs b/crates/ecolor/src/rgba.rs index 93cb41d81..85535bf3a 100644 --- a/crates/ecolor/src/rgba.rs +++ b/crates/ecolor/src/rgba.rs @@ -1,9 +1,8 @@ -use crate::{ - gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_f32_from_linear_u8, - linear_u8_from_linear_f32, -}; +use crate::Color32; /// 0-1 linear space `RGBA` color with premultiplied alpha. +/// +/// See [`crate::Color32`] for explanation of what "premultiplied alpha" means. #[repr(C)] #[derive(Clone, Copy, Debug, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -70,20 +69,12 @@ impl Rgba { #[inline] pub fn from_srgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self { - let r = linear_f32_from_gamma_u8(r); - let g = linear_f32_from_gamma_u8(g); - let b = linear_f32_from_gamma_u8(b); - let a = linear_f32_from_linear_u8(a); - Self::from_rgba_premultiplied(r, g, b, a) + Self::from(Color32::from_rgba_premultiplied(r, g, b, a)) } #[inline] pub fn from_srgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self { - let r = linear_f32_from_gamma_u8(r); - let g = linear_f32_from_gamma_u8(g); - let b = linear_f32_from_gamma_u8(b); - let a = linear_f32_from_linear_u8(a); - Self::from_rgba_premultiplied(r * a, g * a, b * a, a) + Self::from(Color32::from_rgba_unmultiplied(r, g, b, a)) } #[inline] @@ -211,13 +202,12 @@ impl Rgba { /// unmultiply the alpha #[inline] pub fn to_srgba_unmultiplied(&self) -> [u8; 4] { - let [r, g, b, a] = self.to_rgba_unmultiplied(); - [ - gamma_u8_from_linear_f32(r), - gamma_u8_from_linear_f32(g), - gamma_u8_from_linear_f32(b), - linear_u8_from_linear_f32(a.abs()), - ] + crate::Color32::from(*self).to_srgba_unmultiplied() + } + + /// Blend two colors in linear space, so that `self` is behind the argument. + pub fn blend(self, on_top: Self) -> Self { + self.multiply(1.0 - on_top.a()) + on_top } } @@ -276,3 +266,72 @@ impl std::ops::Mul for f32 { ]) } } + +#[cfg(test)] +mod test { + + use super::*; + + fn test_rgba() -> impl Iterator { + [ + [0, 0, 0, 0], + [0, 0, 0, 255], + [10, 0, 30, 0], + [10, 0, 30, 40], + [10, 100, 200, 0], + [10, 100, 200, 100], + [10, 100, 200, 200], + [10, 100, 200, 255], + [10, 100, 200, 40], + [10, 20, 0, 0], + [10, 20, 0, 255], + [10, 20, 30, 255], + [10, 20, 30, 40], + [255, 255, 255, 0], + [255, 255, 255, 255], + ] + .into_iter() + } + + #[test] + fn test_rgba_blend() { + let opaque = Rgba::from_rgb(0.4, 0.5, 0.6); + let transparent = Rgba::from_rgb(1.0, 0.5, 0.0).multiply(0.3); + assert_eq!( + transparent.blend(opaque), + opaque, + "Opaque on top of transparent" + ); + assert_eq!( + opaque.blend(transparent), + Rgba::from_rgb( + 0.7 * 0.4 + 0.3 * 1.0, + 0.7 * 0.5 + 0.3 * 0.5, + 0.7 * 0.6 + 0.3 * 0.0 + ), + "Transparent on top of opaque" + ); + } + + #[test] + fn test_rgba_roundtrip() { + for in_rgba in test_rgba() { + let [r, g, b, a] = in_rgba; + if a == 0 { + continue; + } + let rgba = Rgba::from_srgba_unmultiplied(r, g, b, a); + let out_rgba = rgba.to_srgba_unmultiplied(); + + if a == 255 { + assert_eq!(in_rgba, out_rgba); + } else { + // There will be small rounding errors whenever the alpha is not 0 or 255, + // because we multiply and then unmultiply the alpha. + for (&a, &b) in in_rgba.iter().zip(out_rgba.iter()) { + assert!(a.abs_diff(b) <= 3, "{in_rgba:?} != {out_rgba:?}"); + } + } + } + } +} diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 6da01d9be..dad036818 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -2240,18 +2240,21 @@ impl Ui { /// # Colors impl Ui { /// Shows a button with the given color. + /// /// If the user clicks the button, a full color picker is shown. pub fn color_edit_button_srgba(&mut self, srgba: &mut Color32) -> Response { color_picker::color_edit_button_srgba(self, srgba, color_picker::Alpha::BlendOrAdditive) } /// Shows a button with the given color. + /// /// If the user clicks the button, a full color picker is shown. pub fn color_edit_button_hsva(&mut self, hsva: &mut Hsva) -> Response { color_picker::color_edit_button_hsva(self, hsva, color_picker::Alpha::BlendOrAdditive) } /// Shows a button with the given color. + /// /// If the user clicks the button, a full color picker is shown. /// The given color is in `sRGB` space. pub fn color_edit_button_srgb(&mut self, srgb: &mut [u8; 3]) -> Response { @@ -2259,6 +2262,7 @@ impl Ui { } /// Shows a button with the given color. + /// /// If the user clicks the button, a full color picker is shown. /// The given color is in linear RGB space. pub fn color_edit_button_rgb(&mut self, rgb: &mut [f32; 3]) -> Response { @@ -2266,6 +2270,7 @@ impl Ui { } /// Shows a button with the given color. + /// /// If the user clicks the button, a full color picker is shown. /// The given color is in `sRGBA` space with premultiplied alpha pub fn color_edit_button_srgba_premultiplied(&mut self, srgba: &mut [u8; 4]) -> Response { @@ -2276,6 +2281,7 @@ impl Ui { } /// Shows a button with the given color. + /// /// If the user clicks the button, a full color picker is shown. /// The given color is in `sRGBA` space without premultiplied alpha. /// If unsure, what "premultiplied alpha" is, then this is probably the function you want to use. @@ -2288,6 +2294,7 @@ impl Ui { } /// Shows a button with the given color. + /// /// If the user clicks the button, a full color picker is shown. /// The given color is in linear RGBA space with premultiplied alpha pub fn color_edit_button_rgba_premultiplied(&mut self, rgba_premul: &mut [f32; 4]) -> Response { @@ -2307,6 +2314,7 @@ impl Ui { } /// Shows a button with the given color. + /// /// If the user clicks the button, a full color picker is shown. /// The given color is in linear RGBA space without premultiplied alpha. /// If unsure, what "premultiplied alpha" is, then this is probably the function you want to use. diff --git a/crates/egui_demo_app/tests/snapshots/imageviewer.png b/crates/egui_demo_app/tests/snapshots/imageviewer.png index 62624506d..026ddf563 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:57274cec5ee7e5522073249b931ea65ead22752aea1de40666543e765c1b6b85 -size 102929 +oid sha256:d17f0693c6288f87d4a0bb009ea03911e8a9baf3efa81445a3ed7849df0313e9 +size 102920 diff --git a/crates/egui_demo_lib/src/rendering_test.rs b/crates/egui_demo_lib/src/rendering_test.rs index e83e643bb..879e2c7af 100644 --- a/crates/egui_demo_lib/src/rendering_test.rs +++ b/crates/egui_demo_lib/src/rendering_test.rs @@ -92,8 +92,6 @@ impl ColorTest { ui.label("Test that vertex color times texture color is done in gamma space:"); ui.scope(|ui| { - ui.spacing_mut().item_spacing.y = 0.0; // No spacing between gradients - let tex_color = Color32::from_rgb(64, 128, 255); let vertex_color = Color32::from_rgb(128, 196, 196); let ground_truth = mul_color_gamma(tex_color, vertex_color); @@ -106,6 +104,9 @@ impl ColorTest { show_color(ui, vertex_color, color_size); ui.label(" vertex color ="); }); + + ui.spacing_mut().item_spacing.y = 0.0; // No spacing between gradients + { let g = Gradient::one_color(ground_truth); self.vertex_gradient(ui, "Ground truth (vertices)", WHITE, &g); @@ -129,6 +130,34 @@ impl ColorTest { ui.separator(); + ui.label("Test that blending is done in gamma space:"); + ui.scope(|ui| { + let background = Color32::from_rgb(200, 60, 10); + let foreground = Color32::from_rgba_unmultiplied(108, 65, 200, 82); + let ground_truth = background.blend(foreground); + + ui.horizontal(|ui| { + let color_size = ui.spacing().interact_size; + ui.label("Background:"); + show_color(ui, background, color_size); + ui.label(", foreground: "); + show_color(ui, foreground, color_size); + }); + ui.spacing_mut().item_spacing.y = 0.0; // No spacing between gradients + { + let g = Gradient::one_color(ground_truth); + self.vertex_gradient(ui, "Ground truth (vertices)", WHITE, &g); + self.tex_gradient(ui, "Ground truth (texture)", WHITE, &g); + } + { + let g = Gradient::one_color(foreground); + self.vertex_gradient(ui, "Vertex blending", background, &g); + self.tex_gradient(ui, "Texture blending", background, &g); + } + }); + + ui.separator(); + // TODO(emilk): test color multiplication (image tint), // to make sure vertex and texture color multiplication is done in linear space. diff --git a/crates/egui_demo_lib/tests/snapshots/demos/BĆ©zier Curve.png b/crates/egui_demo_lib/tests/snapshots/demos/BĆ©zier Curve.png index 190f7055c..96eaf1006 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/BĆ©zier Curve.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/BĆ©zier Curve.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:536fa3adb51f69fac91396b50e26b3b18e0aa8ff245e4a187087b02240839a90 -size 31780 +oid sha256:2faddafd5f6fc445d15ec39248326d607d14838692201503a178ae1da2c0127d +size 31675 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Frame.png b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png index bfba77f18..d4b9df8dd 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:9870334dd6091fa684b78f487ad9a1bb39e6e8d97f987eb74a55de2d7b764f70 -size 24345 +oid sha256:f299fb3c7c66a0fde7a30916bb4be1bb14c43f5eb139268309aa8b46f86caede +size 24388 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scene.png b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png index e79968364..a2416b305 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:7d412700c156c641f0184a239198f33bd2427a1ea998a3ee07160cf0f837df94 -size 35451 +oid sha256:b007380b5ce761ff5d23665dcaa2729e1795c5192efdf366007ddbcea0ed64a5 +size 35463 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 e1b600fe5..8aacaa768 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:03ee62427611101758958adf2650a4a0eea4e023f07c9ec4ebc63425233e8a04 -size 554949 +oid sha256:1b618443ba6e8483425972bd95fced23d2cd5ff4ad05277a6171eac14c255302 +size 572382 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 ab9c8b818..ecc877b55 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:82ef265f0e22649c7fcdb9556879c1a30df582bd4e97c647258b3e5acc03d112 -size 771298 +oid sha256:00681d206ae05c2135dcc61e87a31f18248fc972804a01bc3440faf4fdd1a50e +size 796392 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 b2d8c3113..d3186203a 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:cad71b486a479eb9c5339a93f4acc3df2d0b6b188ad023b9b044be7311b0ab72 -size 918775 +oid sha256:a21e2cc32a032ee44516c495c68aa5f6e168da1bea44396db6e67889d5714e7f +size 948339 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 2056c3fa0..aaae79e2f 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:dc9ed4d29f4227b9d38b477ee8f546ea8597acda56a6909ba4826891ebdbea01 -size 1039263 +oid sha256:7907a5851f4b5a986cbd74b494b65cff7a469038af63ac957d55e848bb3391a8 +size 1072165 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 586916e56..8b1886e85 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:e9bf826bee811d8af345ec1281266fc9bef6d7c3782279516984a6c75130a929 -size 1130895 +oid sha256:b220801c49d7d1f4364cf6c4f2098123e34ce782bb1439b2896d8adf9215a0be +size 1166343 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 b764e7bee..8eec2cc34 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:9345de28f09e2891fd01db20bb0b94176ec3c89d8c2f344a6640d33e97ab5400 -size 1311417 +oid sha256:0ff8e54c66f64396b42bb962297eab966089318b1a75e65accef7abbeb7d6cee +size 1353321 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery.png index 87f13e8e5..da499718e 100644 --- a/crates/egui_demo_lib/tests/snapshots/widget_gallery.png +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:946bf96ae558ee7373b50bf11959e82b1f4d91866ec61b04b0336ae170b6f7b2 -size 158553 +oid sha256:78335f9233990c5622d1f6f0f18a3b44e33b0e68061e865641b0b316072489ba +size 158496 From 2058dcb8818510842285ab7834e6f3bad19d601d Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 21 Mar 2025 12:56:47 +0100 Subject: [PATCH 024/129] Improve text sharpness (#5838) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This improves the sharpness of text slightly, thanks to by @hikogui (šŸ™ ). The difference is very small, but in dark mode (bright text on dark background) text is sometimes significantly sharper, and also slightly brighter. The difference in light mode (dark text on light background) is much smaller (barely perceptable). To compare the before/after I suggest you open both in new tabs, then quickly change between them. ### Low-DPI #### Before ![Code Editor old](https://github.com/user-attachments/assets/e10a3cad-932f-48cd-b7d6-5bfe70954c5e) #### After ![Code Editor](https://github.com/user-attachments/assets/2e7383fe-8023-4425-91c8-93df3c22c0fe) #### Before ![old-white](https://github.com/user-attachments/assets/51c41c59-e897-4831-857a-f3ffe17ce7d4) #### After ![new-white](https://github.com/user-attachments/assets/4ac6f951-8c57-4bcc-a5d5-788cf52ea7d8) ### High-DPI The difference is starkest on low-DPI screens (like the above screenshots). On high-DPI screens, the difference is much smaller #### Before ![widget_gallery old](https://github.com/user-attachments/assets/f2149a30-aef8-4383-b48c-73d33a03ca02) #### After ![widget_gallery](https://github.com/user-attachments/assets/c9ceb8be-8a32-490c-9364-2c6562b741f6) --- .../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/BĆ©zier Curve.png | 4 +-- .../tests/snapshots/demos/Code Editor.png | 4 +-- .../tests/snapshots/demos/Code Example.png | 4 +-- .../tests/snapshots/demos/Dancing Strings.png | 4 +-- .../tests/snapshots/demos/Drag and Drop.png | 4 +-- .../tests/snapshots/demos/Extra Viewport.png | 4 +-- .../tests/snapshots/demos/Font Book.png | 4 +-- .../tests/snapshots/demos/Frame.png | 4 +-- .../tests/snapshots/demos/Highlighting.png | 4 +-- .../snapshots/demos/Interactive Container.png | 4 +-- .../tests/snapshots/demos/Misc Demos.png | 4 +-- .../tests/snapshots/demos/Modals.png | 4 +-- .../tests/snapshots/demos/Multi Touch.png | 4 +-- .../tests/snapshots/demos/Painting.png | 4 +-- .../tests/snapshots/demos/Panels.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/Strip.png | 4 +-- .../tests/snapshots/demos/Table.png | 4 +-- .../tests/snapshots/demos/Text Layout.png | 4 +-- .../tests/snapshots/demos/TextEdit.png | 4 +-- .../tests/snapshots/demos/Tooltips.png | 4 +-- .../tests/snapshots/demos/Undo Redo.png | 4 +-- .../tests/snapshots/demos/Window Options.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 +-- .../tests/snapshots/widget_gallery.png | 4 +-- .../tests/snapshots/combobox_closed.png | 4 +-- .../tests/snapshots/combobox_opened.png | 4 +-- .../tests/snapshots/image_snapshots.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 +-- .../tests/snapshots/readme_example.png | 4 +-- .../tests/snapshots/test_shrink.png | 4 +-- crates/epaint/src/image.rs | 28 +++++++++++-------- 60 files changed, 134 insertions(+), 130 deletions(-) diff --git a/crates/egui_demo_app/tests/snapshots/clock.png b/crates/egui_demo_app/tests/snapshots/clock.png index 80fef31cc..2b0bfc6c1 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:2c15a74a1b1ed3b52a53966a3df2901ca520b92fbfbd10503e32ddb8431e1467 -size 335399 +oid sha256:61db3807f755ac832ba069e1adaf8aeb550c88737b4907748667a271ae29863d +size 334792 diff --git a/crates/egui_demo_app/tests/snapshots/custom3d.png b/crates/egui_demo_app/tests/snapshots/custom3d.png index b138e53b9..ce19412f7 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:0e4a90792a9876da549f3d1da9b057a078400ad15db2cc6e35f4324851137d4e -size 93115 +oid sha256:21e0a6cdf175606a513ddf410ae1b873a9817305ecad403116fad3c6ff795fa3 +size 92185 diff --git a/crates/egui_demo_app/tests/snapshots/easymarkeditor.png b/crates/egui_demo_app/tests/snapshots/easymarkeditor.png index 014264330..f88cb3791 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:d8f1046ee5d50d73a17009fd1f11f056b5828fedc62908d00730a6aa77125473 -size 182900 +oid sha256:fb4ac08fb40dd1413feee549ba977906160c82d0aba427d6d79d2e56080aa04e +size 178975 diff --git a/crates/egui_demo_app/tests/snapshots/imageviewer.png b/crates/egui_demo_app/tests/snapshots/imageviewer.png index 026ddf563..57c88b50a 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:d17f0693c6288f87d4a0bb009ea03911e8a9baf3efa81445a3ed7849df0313e9 -size 102920 +oid sha256:f7572ec2dad9038c24beb9949e4c05155cd0f5479153de6647c38911ec5c67a0 +size 100779 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/BĆ©zier Curve.png b/crates/egui_demo_lib/tests/snapshots/demos/BĆ©zier Curve.png index 96eaf1006..8bceea77e 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/BĆ©zier Curve.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/BĆ©zier Curve.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2faddafd5f6fc445d15ec39248326d607d14838692201503a178ae1da2c0127d -size 31675 +oid sha256:cbe9f58cce2466360b4b93b03afaaee36711b3017ddff1b2b56bfe49ea91a076 +size 31306 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 8574b6d22..e346a2779 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:c129436a0b1dbfae999adfe0dcc6f5c4e0683c4e9b9a1e52f4b7bbb85ce3a462 -size 27162 +oid sha256:7224afc6e728f60c28c027bf4be03d1f598dc70977274bcd32b7398d11dd36c7 +size 26416 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 035f59445..dc0e224b1 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:0466198f14d15f011e16d16efcc28aeaaf80978ea4e46b5d9a1282304c192c4c -size 80907 +oid sha256:5cfc3ee54a0e64fb8b72d55e9fc2079aa2517b200665684076d63b87c381cdb9 +size 78704 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 d216ff1fe..90b310c4d 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:d6ba28dacacf5b6f67746fb5187b601e222fd6baf190af2248fdc98909fc17fd -size 25921 +oid sha256:49b08c1fb7878d8670d96de9f9791e2db5cf7206812da1d9102c4dd1758cb803 +size 25833 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png b/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png index 097415b60..1bb7af6e1 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8196e08717f16c5ad17d0f84a4e57e63bb5a51c8f2b171071bf983af18ec161d -size 20834 +oid sha256:1e3e0330de3f68593329d2f36649127d5ac70109232c68f5c7ce310fa919fda5 +size 20348 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Extra Viewport.png b/crates/egui_demo_lib/tests/snapshots/demos/Extra Viewport.png index cc8c50e54..9fd711dbc 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Extra Viewport.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Extra Viewport.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:df029c69651ee452cc4b265828280e47ffbcafb2958d71d67a5fe38f5211afe7 -size 10788 +oid sha256:2882a9842f51a7c3e9642a9a3d260407e1194648f47574608822a293bd3b1d56 +size 10465 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 230c7151e..434f2e1e4 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:0a62d309912501be8a5de7af4f1039a2a5731b1ed76fad17527f5783a5375f42 -size 133230 +oid sha256:af99bd49ceee6bbd96cc813cd96ac01f5c135ed7d94b8ff4010fa45feac5359a +size 127703 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Frame.png b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png index d4b9df8dd..64c6b76ec 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:f299fb3c7c66a0fde7a30916bb4be1bb14c43f5eb139268309aa8b46f86caede -size 24388 +oid sha256:a0b999914adab3d44c614bdf3b28abd268a4ff6162c5680b43035b3f71cb69bb +size 23999 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png b/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png index 7b14e8870..3b1df931d 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:bae5f410ed30ef4dba6f3b529ae20e34a26f6c15c4cafd197899cf876271f5f1 -size 17828 +oid sha256:57e09bcf48541af11e44ff07122f09640e0329db0c2bc7a6ecb406a3ece572ac +size 17608 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 d118733ba..d934345b9 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:68ded8dccceb3da2764243f2a554c2b4cf825fca09008d60dd520c7fbb2c5d3e -size 22445 +oid sha256:641e5c7d4deccc8eb0374db4707dc356285a5c72186f9021d0d601c22bc5115f +size 21894 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 8aff2977f..267fa6be7 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:d55baa6e3d4af44a35ec847639c35f968b05ad907352c45b3eb09cce6cd24280 -size 64357 +oid sha256:880344367ed65f83898ceca4843b1b6259d1690242ced0d29ac8dc48100a8faa +size 62956 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Modals.png b/crates/egui_demo_lib/tests/snapshots/demos/Modals.png index 7c20eab75..e77312e7f 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:98f7210fa72bdb00364e3576aefca126a6f31eff52870d116ba74c167354b13b -size 32533 +oid sha256:2f467edf4a84c8a98d96f168d843edb201ad2ee067dcd9d8d9ea214a02a41b1f +size 32182 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png b/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png index bd25e38a8..8677f7300 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7868d662bd61d490dce9c049fca6c6e6b978255664fa709e959891bb40a7d434 -size 36577 +oid sha256:01705a1a49350278f524bbc5dbd47ae9da4b57ee7f6f34fb20186e1aa9b9f1d4 +size 35714 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Painting.png b/crates/egui_demo_lib/tests/snapshots/demos/Painting.png index 749ecd471..060c60e01 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:fb3d031b8f658a90cf98e7a7bc5e0d7a3b601d742e2a9469cd115e7466e06524 -size 17628 +oid sha256:03cb424100e99a141daeacc78036c4334d74cace3fae19bb878565ccda68457d +size 17448 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Panels.png b/crates/egui_demo_lib/tests/snapshots/demos/Panels.png index a4527a73b..9953ac6c1 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Panels.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Panels.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ace9a6626446f8e29ec4c3f688e60cbeb86e79ad962044858aabe33a9c3d0e9 -size 264538 +oid sha256:df1e4a1e355100056713e751a8979d4201d0e4aab5513ba2f7a3e4852e1347dd +size 264340 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Popups.png b/crates/egui_demo_lib/tests/snapshots/demos/Popups.png index 8b3c58838..5dc0c2747 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:17dc2a2f98d4cc52f6c6337dcc2e40f22d7310a933de91bb60576b893926193c -size 58674 +oid sha256:6ed78a559488474487c0a434a941e434b22354e4374d13059076d76da93bc609 +size 57051 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scene.png b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png index a2416b305..0f67cc6a2 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:b007380b5ce761ff5d23665dcaa2729e1795c5192efdf366007ddbcea0ed64a5 -size 35463 +oid sha256:16ee44708adbe6e0ac3ce58617a5d63fb3bde357c07611815376518950e056b0 +size 34763 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png b/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png index 803a39f99..379a18cd3 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:89efd018caac097a5f9be37dcae15fc60b1475c72fc913ec9940540344e0b09f -size 23622 +oid sha256:63f5c3be15164e6f008fb09b4ff37eff2af0ab361de28d1994d595789c379df5 +size 23205 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png b/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png index 2e2af10c9..17e76840e 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:22c89f7b9b84563d6ee7db0d9a66f6b95c9034261fdffca53ae9737d70d2b376 -size 183881 +oid sha256:b236fe02f6cd52041359cf4b1a00e9812b95560353ce5df4fa6cb20fdbb45307 +size 179653 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png b/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png index e47f11082..09f8eaa6c 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:316c172a936f215afdcc45e7f5b32400e6acd759551adb2cc741f7121b9d83eb -size 117790 +oid sha256:1579351658875af48ad9aafeb08d928d83f1bda42bf092fdcceecd0aa6730e26 +size 115313 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Strip.png b/crates/egui_demo_lib/tests/snapshots/demos/Strip.png index ea9a21881..bae04d3e8 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Strip.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Strip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:88913690a2b225ca634e38406a6a852250019a19d9bb33a4242e77c10fe88422 -size 26142 +oid sha256:eb7c844f6b745f66304ad036790a5121e4827fa91569b28ffa301794aecd0c66 +size 25592 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Table.png b/crates/egui_demo_lib/tests/snapshots/demos/Table.png index 3012f2f36..6189f25c0 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:43b8dae4a936bf56b92368fcef64ff2ce2518aabc534a77fa578730493034f0f -size 70536 +oid sha256:13115759157beb57febcff4be6f1710340736108b520e9ad3efb04be3cedcf7b +size 68767 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 84f7cc7bd..fcf8b8f35 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:a307ac48abc79548c16468b3606a5df283ab2a5ac28345bd801bcc3887063414 -size 66384 +oid sha256:e177888e10f357f1be8ad80f7a0a33c93798c1e7c43cfe382119eeb12f21279f +size 64732 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png b/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png index 93789ff45..d39effd6b 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:6311be2b850b5e41ac6dadf639b00584438b56f651a3c8d75ac8f5e06c9ad6fa -size 21224 +oid sha256:e177e2631414784161a5556bdd1420ce8432f9859faede1a2e6f791a02814412 +size 20918 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png b/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png index f8bb020e2..852e21df3 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:92b70683a685869274749d057de174896e18dae5cb67e70221c3efdb7106cdda -size 63684 +oid sha256:c49c489fe1bb00512c9d08e8d8454fce786744f4ebff0bfd27dac68b7e67b815 +size 62317 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 5f77c2db3..53d6c8a3d 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:3584f16229bae50cc04b31df6bf5ccf43288fd05b447b34b29f118eb7435a090 -size 13103 +oid sha256:c4d6a15094eee5d96a8af5c44ea9d0c962d650ee9b867344c86d1229e526dcb5 +size 12822 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 e4ee95e39..a45e2be68 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:e67b1e676ff994cb9557939db3dca5ddd15c69d167afd96c0957a2a3b75c0fd8 -size 36007 +oid sha256:02abc0cbab97e572218f422f4b167957869d4e2b4b388355444c20148d998015 +size 35200 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_1.png b/crates/egui_demo_lib/tests/snapshots/modals_1.png index 2036be3c9..81feea9f6 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:1ac48ec9f7bde9869f1b3097e9f897b5e8df96cd6159a6ded542582dc69ab32c -size 47913 +oid sha256:e954bf915d562abc69269cd10a4df8fbd0e5603929e6446fefa694099e2494a4 +size 47542 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_2.png b/crates/egui_demo_lib/tests/snapshots/modals_2.png index 60e6ddef5..a446fa177 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:795e16389b31ad719050247eb9e736782380a83fa71b5b35b50e17812c8d9bdd -size 47886 +oid sha256:1c7bd1a65b6c33eff2fe17f7af2dd731a03658abc2419f8722c0e9395b26fdef +size 47515 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_3.png b/crates/egui_demo_lib/tests/snapshots/modals_3.png index 1ae5a3314..6cdf2d21d 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:a62a286e29aa0e0f949088ddefe01137535877408ba88778f61cbfe8d50c2261 -size 43750 +oid sha256:e89cd220a925150384b9f9987b178036ffacfe29cdb36ed688205524dbb731fd +size 43803 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 e059b556f..f234c33ed 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:9c1bc8e22aa1050a4e7d1b2abe407251e22d338c38a7e41c045a384c9139b4de -size 43895 +oid sha256:b71da58f5c0178517f9e0cc97753a0a5d1653cc5d094b5a35ffe050499bcd569 +size 43679 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 8aacaa768..1cef15b72 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:1b618443ba6e8483425972bd95fced23d2cd5ff4ad05277a6171eac14c255302 -size 572382 +oid sha256:5a1f0d0759458017127d93278b89278af20fdc57c7747652ac6554f24cc708f2 +size 552939 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 ecc877b55..f1005193c 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:00681d206ae05c2135dcc61e87a31f18248fc972804a01bc3440faf4fdd1a50e -size 796392 +oid sha256:7948ced20e3f62b8356fc978ca7b12f8522b7c15716409cc661c0ebd2f12047f +size 770062 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 d3186203a..132864c85 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:a21e2cc32a032ee44516c495c68aa5f6e168da1bea44396db6e67889d5714e7f -size 948339 +oid sha256:e6f394c2beb51d95edaf8c7ddc9ff62d3f95913ea88a3840245b6bacf8b850cc +size 907997 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 aaae79e2f..bd093a19c 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:7907a5851f4b5a986cbd74b494b65cff7a469038af63ac957d55e848bb3391a8 -size 1072165 +oid sha256:d31f018cdabf92966b5636d9aef7f11ef1a0383884867e819e7ec99c0474e872 +size 1025013 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 8b1886e85..b9a40fab3 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:b220801c49d7d1f4364cf6c4f2098123e34ce782bb1439b2896d8adf9215a0be -size 1166343 +oid sha256:e18f7ddadd53e16d04f191268747f244c98d0ea0f4cb9c0ea299f5da7affbc58 +size 1139723 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 8eec2cc34..935dcc33d 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:0ff8e54c66f64396b42bb962297eab966089318b1a75e65accef7abbeb7d6cee -size 1353321 +oid sha256:09a49cb9da7269bec6eef30c46a0bc85df6538d6e31dc0d0ff2758dbee45f3d8 +size 1291804 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 979991d13..2fdbaff3d 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:5524138c3cb98aa71ef67083ad2d01813ab2394f93f9a7897f2e465ef5a1d0bc -size 46270 +oid sha256:4ac90da596084a880487035b276177e98d711854143373d59860f01733b1c0cd +size 45592 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 249b5db4d..5eb8bf536 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:bdf06c41b69eef1eadc8b46020e6e2a7b985a54e1cf75646ca47caaaea525b95 -size 88092 +oid sha256:e412d424aac7b9cbdfdb8e36bd598e6cbc77183da7733c94c5f20e70699b8b4a +size 87263 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 c81deb054..e9e1a078d 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:c5ca9c97cef8242ee6ff73d571479be12a8d4e9b3508b3eb6cdf93abda62f4e6 -size 120314 +oid sha256:222a32da21c69ee46e847e29fb05fd5e1d2de6bb7a22358549bc426f8243fdcb +size 119671 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 2a29cd732..a08a658eb 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:f0481c97c34693b32575d96b1d4bc1238cbb0eb75a934661072f1b52ffee71cf -size 52171 +oid sha256:d42e11f50a9522dd5ae73e8f8336bfb01493751705055a63abea3f5258f7c9c1 +size 51626 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 b7d315e47..677783cc5 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:883fdf81e51bfe6333ddcad7998458db251f9cf513c9433179061d7d086eebe0 -size 55367 +oid sha256:b567d4038fd73986c80d2bd12197a6df037fde043545993fa9fe4160d0af446c +size 54829 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 2fe78dd62..7816cfdb0 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:4294949669042e009ac6825ea599dc96e33cdde25e21174b01e3ef108ad478d5 -size 55944 +oid sha256:fbf40a1f56a6e280002719c6556fe477c93fa7fe88d398372ed36efaa1b83a62 +size 55282 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 603442ac0..6005e865a 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:39e5d196ddcaa213b30b0655fe29881a1551c3036c2262f84af8960f66365300 -size 37207 +oid sha256:33621731155ebb463fb01ea41ab20272885250efcd7d5c7683c10936b296e14d +size 36446 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 266c77826..713e01fcc 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:f9cf9d7f1921bfc0d61a2ae31e69a98d28280e4699823de5e732cdb102aee5ac -size 37253 +oid sha256:186bd8a3146ad8f1977955e3f7fa593877ad1bf1e8376d32f446c67f36a2aafe +size 36493 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery.png index da499718e..b05ebda12 100644 --- a/crates/egui_demo_lib/tests/snapshots/widget_gallery.png +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78335f9233990c5622d1f6f0f18a3b44e33b0e68061e865641b0b316072489ba -size 158496 +oid sha256:af75f773e9e4ad2615893babce5b99e7fd127c76dd0976ac8dc95307f38a59dc +size 152854 diff --git a/crates/egui_kittest/tests/snapshots/combobox_closed.png b/crates/egui_kittest/tests/snapshots/combobox_closed.png index 18a9d0bc9..b7a105c8b 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:202091634d4483949cb1c5c4c5ec02faa23f4d19e7e833aba135887b77e3188d -size 4485 +oid sha256:bab1f08160bc43410b9d49ebfaae8a471309e670fccd31456a09176513361e6e +size 4417 diff --git a/crates/egui_kittest/tests/snapshots/combobox_opened.png b/crates/egui_kittest/tests/snapshots/combobox_opened.png index ef84c8a77..fd0262403 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:f65efbf60e190d83d187ec51f3f7811eb55135ef4feb9586e931e8498bc05d64 -size 7430 +oid sha256:21f70bc7146e43b6b10fe1e4cb32597d6f3507b42a6aa4c619c4e8c688ea4c85 +size 7290 diff --git a/crates/egui_kittest/tests/snapshots/image_snapshots.png b/crates/egui_kittest/tests/snapshots/image_snapshots.png index c1b7d6cef..5c1bf9f4d 100644 --- a/crates/egui_kittest/tests/snapshots/image_snapshots.png +++ b/crates/egui_kittest/tests/snapshots/image_snapshots.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:31faeb4e5f488b8bcee5e090accd326d7e43b264e81768ae7c1907e3b6d0f739 -size 2121 +oid sha256:c618906fe1ff781c20cb89747879fa1ac63a115c624a2695adb9eb6ae157cd40 +size 2095 diff --git a/crates/egui_kittest/tests/snapshots/menu/closed_hovered.png b/crates/egui_kittest/tests/snapshots/menu/closed_hovered.png index f2c388b74..f04762182 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:d96ef60e9cd8767c0a95cc1fb0e240014248d7b0d6f67777a5b6ca4f23e91380 -size 10732 +oid sha256:6ca504cc7ef988f122fcc099914e5b6f7c39a3a86c5869a0a982c4342c48058a +size 10516 diff --git a/crates/egui_kittest/tests/snapshots/menu/opened.png b/crates/egui_kittest/tests/snapshots/menu/opened.png index 88ff40fcf..8b0f757aa 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:23dced7849e2bdd436e0552b2d351fef5693dd688c875816aaba3607a3aa1197 -size 21756 +oid sha256:763447271686242b8a2deaa63fa1a5a0d57009ef93dd1bcb0ae906541cd7a6ea +size 21052 diff --git a/crates/egui_kittest/tests/snapshots/menu/submenu.png b/crates/egui_kittest/tests/snapshots/menu/submenu.png index 697c9397c..adb6f8fb0 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:6b8790dbd01409496ca6dc6d322d69c59a02f552b244264c8f6d7ea847846d5a -size 28979 +oid sha256:4e54f7a1ea9ac74b62241c8b662579fd3c8442857b4569ce818342fb56dc30ae +size 28218 diff --git a/crates/egui_kittest/tests/snapshots/menu/subsubmenu.png b/crates/egui_kittest/tests/snapshots/menu/subsubmenu.png index 69663a1ac..0b7919093 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:6f0b04c42dfb257ca650a2fe332509f847cc94f4666cc29d830086627b91fc25 -size 33737 +oid sha256:f8443523a671d3c83456c6ee0503fdb59127a33d866c45635a84eee3596985fd +size 32831 diff --git a/crates/egui_kittest/tests/snapshots/readme_example.png b/crates/egui_kittest/tests/snapshots/readme_example.png index ef0774162..4b8288326 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:31bd906040fcc356c19dc36036fbfd2a28dfcef54c7a073f584f4a9abddbdb4c -size 1699 +oid sha256:b1d172484712e3e12038f8ff427db8c0073aba124aa1b6be17edcc7dccb12f74 +size 1656 diff --git a/crates/egui_kittest/tests/snapshots/test_shrink.png b/crates/egui_kittest/tests/snapshots/test_shrink.png index a6e6b1f3a..0004a0f61 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:b5aa7db1bb52481607069ee4a81209ece81b0c70801969b33bc2d1b2f7087de7 -size 2911 +oid sha256:be6f0caa911d93543edb39ba6d07d7617ec283b37bc62622a68a18960eb840ab +size 2871 diff --git a/crates/epaint/src/image.rs b/crates/epaint/src/image.rs index f653be2e7..69844f42a 100644 --- a/crates/epaint/src/image.rs +++ b/crates/epaint/src/image.rs @@ -306,14 +306,23 @@ impl FontImage { /// If you are having problems with text looking skinny and pixelated, try using a low gamma, e.g. `0.4`. #[inline] pub fn srgba_pixels(&self, gamma: Option) -> impl ExactSizeIterator + '_ { - // TODO(emilk): this default coverage gamma is a magic constant, chosen by eye. I don't even know why we need it. - // Maybe we need to implement the ideas in https://hikogui.org/2022/10/24/the-trouble-with-anti-aliasing.html - let gamma = gamma.unwrap_or(0.55); + // This whole function is less than rigorous. + // Ideally we should do this in a shader instead, and use different computations + // for different text colors. + // See https://hikogui.org/2022/10/24/the-trouble-with-anti-aliasing.html for an in-depth analysis. self.pixels.iter().map(move |coverage| { - let alpha = coverage.powf(gamma); - // We want to multiply with `vec4(alpha)` in the fragment shader: - let a = fast_round(alpha * 255.0); - Color32::from_rgba_premultiplied(a, a, a, a) + let alpha = if let Some(gamma) = gamma { + coverage.powf(gamma) + } else { + // alpha = coverage * coverage; // recommended by the article for WHITE text (using linear blending) + + // The following is recommended by the article for BLACK text (using linear blending). + // Very similar to a gamma of 0.5, but produces sharper text. + // In practice it works well for all text colors (better than a gamma of 0.5, for instance). + // See https://www.desmos.com/calculator/w0ndf5blmn for a visual comparison. + 2.0 * coverage - coverage * coverage + }; + Color32::from_white_alpha(ecolor::linear_u8_from_linear_f32(alpha)) }) } @@ -362,11 +371,6 @@ impl From for ImageData { } } -#[inline] -fn fast_round(r: f32) -> u8 { - (r + 0.5) as _ // rust does a saturating cast since 1.45 -} - // ---------------------------------------------------------------------------- /// A change to an image. From 668abc283881a4905de2a5eb8f24ef306f715381 Mon Sep 17 00:00:00 2001 From: GiGaGon <107241144+MeGaGiGaGon@users.noreply.github.com> Date: Fri, 21 Mar 2025 05:45:34 -0700 Subject: [PATCH 025/129] Add `expand_bg` to customize size of text background (#5365) This removes the `expand(1.0)` on text background colors, since it makes translucent background colors have bad looking bleeding. There is probably a smarter solution than disabling the highlighting entirely, but I don't see a way to do that while keeping the area consumed consistent between translucent/solid colors, or adding a decent step up in complexity. Since this makes it impossible to tell if selected text is highlighted, this also adds a blanket `0.5` gamma multiply to the text selection background color. If that is undesirable because it's a bad arbitrary number choice, or if it's too much of an unexpected change and just the default values should be changed, please let me know. These changes cause the tests that use screenshots with highlighted text to fail, though I am not sure how to update those tests to match the changes.
Comparison Images Current: ![image](https://github.com/user-attachments/assets/6dc85492-4f8e-4e7a-84b4-3ee10a48b8b3) After changes: ![image](https://github.com/user-attachments/assets/9b35bbd3-159d-42a9-b22f-80febb707cfa)
Code used to make comparison images ```rs fn color_text_format(ui: &Ui, color: Color32) -> TextFormat { TextFormat { font_id: FontId::monospace(ui.text_style_height(&egui::TextStyle::Monospace)), background: color, ..Default::default() } } fn color_sequence_galley(ui: &Ui, text: &str, colors: [Color32; 3]) -> Arc { let mut layout_job = LayoutJob::default(); for color in colors { layout_job.append(text, 0.0, color_text_format(ui, color)); } ui.fonts(|f| f.layout_job(layout_job)) } fn color_sequence_row(ui: &mut Ui, label_text: &str, text: &str, colors: [Color32; 3]) { ui.label(label_text); ui.label(color_sequence_galley(ui, text, colors)); ui.end_row(); } egui::Grid::new("comparison display").show(ui, |ui| { ui.ctx().set_pixels_per_point(2.0); let transparent = Color32::TRANSPARENT; let solid = Color32::RED; let solid_2 = Color32::GREEN; let translucent_1 = Color32::GRAY.gamma_multiply(0.5); let translucent_2 = Color32::GREEN.gamma_multiply(0.5); color_sequence_row(ui, "Transparent to Solid:", " ", [transparent, solid, transparent]); color_sequence_row(ui, "Translucent to Transparent:", " ", [transparent, translucent_1, transparent]); color_sequence_row(ui, "Solid to Transparent:", " ", [solid, solid_2, solid]); color_sequence_row(ui, "Solid to Solid:", " ", [solid, transparent, solid]); color_sequence_row(ui, "Solid to Translucent:", " ", [solid, translucent_1, solid]); color_sequence_row(ui, "Translucent to Translucent:", " ", [translucent_1, translucent_2, translucent_1]); color_sequence_row(ui, "Transparent to Solid:", "a", [transparent, solid, transparent]); color_sequence_row(ui, "Translucent to Transparent:", "a", [transparent, translucent_1, transparent]); color_sequence_row(ui, "Solid to Transparent:", "a", [solid, solid_2, solid]); color_sequence_row(ui, "Solid to Solid:", "a", [solid, transparent, solid]); color_sequence_row(ui, "Solid to Translucent:", "a", [solid, translucent_1, solid]); color_sequence_row(ui, "Translucent to Translucent:", "a", [translucent_1, translucent_2, translucent_1]); }) ```
* [x] I have followed the instructions in the PR template --------- Co-authored-by: Emil Ernerfeldt --- crates/egui/src/widget_text.rs | 28 ++++++++++++++++++++- crates/epaint/src/text/text_layout.rs | 13 +++++----- crates/epaint/src/text/text_layout_types.rs | 8 ++++++ 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/crates/egui/src/widget_text.rs b/crates/egui/src/widget_text.rs index 1132aa304..5ddafc4be 100644 --- a/crates/egui/src/widget_text.rs +++ b/crates/egui/src/widget_text.rs @@ -22,7 +22,7 @@ use crate::{ /// RichText::new("colored").color(Color32::RED); /// RichText::new("Large and underlined").size(20.0).underline(); /// ``` -#[derive(Debug, Clone, Default, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct RichText { text: String, size: Option, @@ -31,6 +31,7 @@ pub struct RichText { family: Option, text_style: Option, background_color: Color32, + expand_bg: f32, text_color: Option, code: bool, strong: bool, @@ -41,6 +42,29 @@ pub struct RichText { raised: bool, } +impl Default for RichText { + fn default() -> Self { + Self { + text: Default::default(), + size: Default::default(), + extra_letter_spacing: Default::default(), + line_height: Default::default(), + family: Default::default(), + text_style: Default::default(), + background_color: Default::default(), + expand_bg: 1.0, + text_color: Default::default(), + code: Default::default(), + strong: Default::default(), + weak: Default::default(), + strikethrough: Default::default(), + underline: Default::default(), + italics: Default::default(), + raised: Default::default(), + } + } +} + impl From<&str> for RichText { #[inline] fn from(text: &str) -> Self { @@ -364,6 +388,7 @@ impl RichText { family, text_style, background_color, + expand_bg, text_color: _, // already used by `get_text_color` code, strong: _, // already used by `get_text_color` @@ -429,6 +454,7 @@ impl RichText { underline, strikethrough, valign, + expand_bg, }, ) } diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index 4eb5965c6..ff973ba48 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -758,10 +758,10 @@ fn add_row_backgrounds(job: &LayoutJob, row: &Row, mesh: &mut Mesh) { return; } - let mut end_run = |start: Option<(Color32, Rect)>, stop_x: f32| { - if let Some((color, start_rect)) = start { + let mut end_run = |start: Option<(Color32, Rect, f32)>, stop_x: f32| { + if let Some((color, start_rect, expand)) = start { let rect = Rect::from_min_max(start_rect.left_top(), pos2(stop_x, start_rect.bottom())); - let rect = rect.expand(1.0); // looks better + let rect = rect.expand(expand); mesh.add_colored_rect(rect, color); } }; @@ -776,18 +776,19 @@ fn add_row_backgrounds(job: &LayoutJob, row: &Row, mesh: &mut Mesh) { if color == Color32::TRANSPARENT { end_run(run_start.take(), last_rect.right()); - } else if let Some((existing_color, start)) = run_start { + } else if let Some((existing_color, start, expand)) = run_start { if existing_color == color && start.top() == rect.top() && start.bottom() == rect.bottom() + && format.expand_bg == expand { // continue the same background rectangle } else { end_run(run_start.take(), last_rect.right()); - run_start = Some((color, rect)); + run_start = Some((color, rect, format.expand_bg)); } } else { - run_start = Some((color, rect)); + run_start = Some((color, rect, format.expand_bg)); } last_rect = rect; diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index afe2573d5..2b8b3e819 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -272,6 +272,11 @@ pub struct TextFormat { pub background: Color32, + /// Amount to expand background fill by. + /// + /// Default: 1.0 + pub expand_bg: f32, + pub italics: bool, pub underline: Stroke, @@ -299,6 +304,7 @@ impl Default for TextFormat { line_height: None, color: Color32::GRAY, background: Color32::TRANSPARENT, + expand_bg: 1.0, italics: false, underline: Stroke::NONE, strikethrough: Stroke::NONE, @@ -316,6 +322,7 @@ impl std::hash::Hash for TextFormat { line_height, color, background, + expand_bg, italics, underline, strikethrough, @@ -328,6 +335,7 @@ impl std::hash::Hash for TextFormat { } color.hash(state); background.hash(state); + emath::OrderedFloat(*expand_bg).hash(state); italics.hash(state); underline.hash(state); strikethrough.hash(state); From bc090bd299dab4260a9fc89c3da4e3d4e777f800 Mon Sep 17 00:00:00 2001 From: valadaptive <79560998+valadaptive@users.noreply.github.com> Date: Fri, 21 Mar 2025 08:58:02 -0400 Subject: [PATCH 026/129] Use `RUSTUP_TOOLCHAIN=stable` for rust-analyzer (#5761) * Resolves #5760 Overrides the toolchain used in rust-analyzer to `stable`, as [suggested by rustup](https://rust-analyzer.github.io/book/installation.html#rust-standard-library). --- .vscode/settings.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index d4794e033..677cb3c4e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -33,7 +33,11 @@ "--all-features", ], "rust-analyzer.showUnlinkedFileNotification": false, - + "rust-analyzer.cargo.extraEnv": { + // rust-analyzer is only guaranteed to support the latest stable version of Rust. Use it instead of whatever is + // specified in rust-toolchain. + "RUSTUP_TOOLCHAIN": "stable" + }, // Uncomment the following options and restart rust-analyzer to get it to check code behind `cfg(target_arch=wasm32)`. // Don't forget to put it in a comment again before committing. // "rust-analyzer.cargo.target": "wasm32-unknown-unknown", From 91f02f9e87c1b3ad158a209a9cef32f58af930b1 Mon Sep 17 00:00:00 2001 From: rustbasic <127506429+rustbasic@users.noreply.github.com> Date: Fri, 21 Mar 2025 22:32:37 +0900 Subject: [PATCH 027/129] Enhance stability on Windows (#5723) Dear emilk, Programs built with egui on Windows are terminating every hour on average. When this commit is applied, it works fine for about 3 to 6 hours on average. I've been testing it for over 6 months and have submitted multiple PRs since 6 months ago, but they haven't applied it yet. Thank you. --------- Co-authored-by: Emil Ernerfeldt --- crates/eframe/src/native/run.rs | 84 +++++++++++++++++---------------- 1 file changed, 44 insertions(+), 40 deletions(-) diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs index fb02ac439..641459b52 100644 --- a/crates/eframe/src/native/run.rs +++ b/crates/eframe/src/native/run.rs @@ -93,48 +93,52 @@ impl WinitAppWrapper { log::trace!("event_result: {event_result:?}"); - let combined_result = event_result.and_then(|event_result| { - match event_result { - EventResult::Wait => { - event_loop.set_control_flow(ControlFlow::Wait); - Ok(event_result) - } - EventResult::RepaintNow(window_id) => { - log::trace!("RepaintNow of {window_id:?}",); + let mut event_result = event_result; - if cfg!(target_os = "windows") { - // Fix flickering on Windows, see https://github.com/emilk/egui/pull/2280 - self.winit_app.run_ui_and_paint(event_loop, window_id) - } else { - // Fix for https://github.com/emilk/egui/issues/2425 - self.windows_next_repaint_times - .insert(window_id, Instant::now()); - Ok(event_result) - } - } - EventResult::RepaintNext(window_id) => { - log::trace!("RepaintNext of {window_id:?}",); + if cfg!(target_os = "windows") { + if let Ok(EventResult::RepaintNow(window_id)) = event_result { + log::trace!("RepaintNow of {window_id:?}"); + self.windows_next_repaint_times + .insert(window_id, Instant::now()); + + // Fix flickering on Windows, see https://github.com/emilk/egui/pull/2280 + event_result = self.winit_app.run_ui_and_paint(event_loop, window_id); + } + } + + let combined_result = event_result.map(|event_result| match event_result { + EventResult::Wait => { + event_loop.set_control_flow(ControlFlow::Wait); + event_result + } + EventResult::RepaintNow(window_id) => { + log::trace!("RepaintNow of {window_id:?}",); + self.windows_next_repaint_times + .insert(window_id, Instant::now()); + event_result + } + EventResult::RepaintNext(window_id) => { + log::trace!("RepaintNext of {window_id:?}",); + self.windows_next_repaint_times + .insert(window_id, Instant::now()); + event_result + } + EventResult::RepaintAt(window_id, repaint_time) => { + self.windows_next_repaint_times.insert( + window_id, self.windows_next_repaint_times - .insert(window_id, Instant::now()); - Ok(event_result) - } - EventResult::RepaintAt(window_id, repaint_time) => { - self.windows_next_repaint_times.insert( - window_id, - self.windows_next_repaint_times - .get(&window_id) - .map_or(repaint_time, |last| (*last).min(repaint_time)), - ); - Ok(event_result) - } - EventResult::Save => { - save = true; - Ok(event_result) - } - EventResult::Exit => { - exit = true; - Ok(event_result) - } + .get(&window_id) + .map_or(repaint_time, |last| (*last).min(repaint_time)), + ); + event_result + } + EventResult::Save => { + save = true; + event_result + } + EventResult::Exit => { + exit = true; + event_result } }); From 390e0bfc1e3c55bf37dd41f093e989612c14cf76 Mon Sep 17 00:00:00 2001 From: StratusFearMe21 <57533634+StratusFearMe21@users.noreply.github.com> Date: Fri, 21 Mar 2025 07:35:46 -0600 Subject: [PATCH 028/129] Fix text input on Android (#5759) * [x] I have followed the instructions in the PR template This fixes an issue on android where keyboard input is not registered in text boxes because `winit` does not fill in the `text` field of the `KeyEvent` --------- Co-authored-by: Emil Ernerfeldt --- crates/egui-winit/src/lib.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 1c47d7eb8..e541273fa 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -729,7 +729,7 @@ impl State { // When telling users "Press Ctrl-F to find", this is where we should // look for the "F" key, because they may have a dvorak layout on // a qwerty keyboard, and so the logical "F" character may not be located on the physical `KeyCode::KeyF` position. - logical_key, + logical_key: winit_logical_key, text, @@ -748,7 +748,7 @@ impl State { None }; - let logical_key = key_from_winit_key(logical_key); + let logical_key = key_from_winit_key(winit_logical_key); // Helpful logging to enable when adding new key support log::trace!( @@ -791,7 +791,11 @@ impl State { }); } - if let Some(text) = &text { + if let Some(text) = text + .as_ref() + .map(|t| t.as_str()) + .or_else(|| winit_logical_key.to_text()) + { // Make sure there is text, and that it is not control characters // (e.g. delete is sent as "\u{f728}" on macOS). if !text.is_empty() && text.chars().all(is_printable_char) { @@ -805,7 +809,7 @@ impl State { if pressed && !is_cmd { self.egui_input .events - .push(egui::Event::Text(text.to_string())); + .push(egui::Event::Text(text.to_owned())); } } } From 903bd813136c302f4acb97a37f6fc3c63a896d62 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 25 Mar 2025 09:19:21 +0100 Subject: [PATCH 029/129] Add script to update local snapshots from CI (#5816) It seems like the thresholds are too low for all tests to pass when snapshots are generated from windows / linux. We need a better solution to this problem, but in the meantime this script should allow contributors to update their snapshots by downloading them from the last CI run. --- CONTRIBUTING.md | 5 +++- scripts/update_snapshots_from_ci.sh | 36 +++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100755 scripts/update_snapshots_from_ci.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e0596121e..613de20c5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,8 +34,11 @@ Browse through [`ARCHITECTURE.md`](ARCHITECTURE.md) to get a sense of how all pi You can test your code locally by running `./scripts/check.sh`. There are snapshots test that might need to be updated. Run the tests with `UPDATE_SNAPSHOTS=true cargo test --workspace --all-features` to update all of them. +If CI keeps complaining about snapshots (which could happen if you don't use macOS, snapshots in CI are currently +rendered with macOS), you can instead run `./scripts/update_snapshots_from_ci.sh` to update your local snapshots from +the last CI run of your PR (which will download the `test_results` artefact). For more info about the tests see [egui_kittest](./crates/egui_kittest/README.md). -Snapshots and other big files are stored with git lfs. See [Working with lfs](#working-with-lfs) for more info. +Snapshots and other big files are stored with git lfs. See [Working with git lfs](#working-with-git-lfs) for more info. If you see an `InvalidSignature` error when running snapshot tests, it's probably a problem related to git-lfs. When you have something that works, open a draft PR. You may get some helpful feedback early! diff --git a/scripts/update_snapshots_from_ci.sh b/scripts/update_snapshots_from_ci.sh new file mode 100755 index 000000000..755399d1f --- /dev/null +++ b/scripts/update_snapshots_from_ci.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# This script searches for the last CI run with your branch name, downloads the test_results artefact +# and replaces your existing snapshots with the new ones. +# Make sure you have the gh cli installed and authenticated before running this script. + +set -eu + +BRANCH=$(git rev-parse --abbrev-ref HEAD) + +RUN_ID=$(gh run list --branch "$BRANCH" --workflow "Rust" --json databaseId -q '.[0].databaseId') + +ECHO "Downloading test results from run $RUN_ID from branch $BRANCH" + +# remove any existing .new.png that might have been left behind +find . -type d -path "*/tests/snapshots*" | while read dir; do + find "$dir" -type f -name "*.new.png" | while read file; do + rm "$file" + done +done + + +gh run download "$RUN_ID" --name "test-results" --dir tmp_artefacts + +# move the snapshots to the correct location, overwriting the existing ones +rsync -a tmp_artefacts/ . + +rm -r tmp_artefacts + +# rename the .new.png files to .png +find . -type d -path "*/tests/snapshots*" | while read dir; do + find "$dir" -type f -name "*.new.png" | while read file; do + mv -f "$file" "${file%.new.png}.png" + done +done + +echo "Done!" From 58b2ac88c0b39a48ac2277161ca75df3034b7ae3 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Tue, 25 Mar 2025 09:20:29 +0100 Subject: [PATCH 030/129] Add assert messages and print bad argument values in asserts (#5216) Enabled the `missing_assert_message` lint * [x] I have followed the instructions in the PR template --------- Co-authored-by: Lucas Meurer --- Cargo.toml | 2 +- crates/ecolor/src/color32.rs | 10 ++- crates/ecolor/src/rgba.rs | 15 +++- crates/eframe/src/stopwatch.rs | 4 +- crates/egui/src/containers/menu.rs | 1 - crates/egui/src/context.rs | 24 +++++-- crates/egui/src/hit_test.rs | 10 ++- crates/egui/src/layout.rs | 53 +++++++++----- crates/egui/src/placer.rs | 11 +-- crates/egui/src/response.rs | 5 +- .../src/text_selection/text_cursor_state.rs | 7 +- crates/egui/src/text_selection/visuals.rs | 6 +- crates/egui/src/ui.rs | 21 ++++-- crates/egui/src/widgets/label.rs | 5 +- crates/egui/src/widgets/slider.rs | 22 ++++-- .../egui/src/widgets/text_edit/text_buffer.rs | 5 +- crates/egui_demo_lib/src/rendering_test.rs | 15 +++- crates/egui_extras/src/loaders/file_loader.rs | 2 +- crates/egui_extras/src/sizing.rs | 10 ++- crates/egui_extras/src/syntax_highlighting.rs | 11 ++- crates/egui_glow/src/painter.rs | 9 ++- crates/emath/src/lib.rs | 20 ++++-- crates/emath/src/rot2.rs | 5 +- crates/emath/src/smart_aim.rs | 10 ++- crates/epaint/benches/benchmark.rs | 7 +- crates/epaint/src/image.rs | 71 +++++++++++++++---- crates/epaint/src/mesh.rs | 18 +++-- crates/epaint/src/shapes/shape.rs | 10 ++- crates/epaint/src/stats.rs | 5 +- crates/epaint/src/tessellator.rs | 4 +- crates/epaint/src/text/font.rs | 12 +++- crates/epaint/src/text/text_layout.rs | 7 +- crates/epaint/src/text/text_layout_types.rs | 2 +- crates/epaint/src/texture_atlas.rs | 6 +- scripts/check.sh | 14 ++-- 35 files changed, 331 insertions(+), 108 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f58f958bb..92f1ce3c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -207,6 +207,7 @@ match_wild_err_arm = "warn" match_wildcard_for_single_variants = "warn" mem_forget = "warn" mismatching_type_param_order = "warn" +missing_assert_message = "warn" missing_enforced_import_renames = "warn" missing_errors_doc = "warn" missing_safety_doc = "warn" @@ -274,7 +275,6 @@ zero_sized_map_values = "warn" # TODO(emilk): maybe enable more of these lints? iter_over_hash_type = "allow" -missing_assert_message = "allow" should_panic_without_expect = "allow" too_many_lines = "allow" unwrap_used = "allow" # TODO(emilk): We really wanna warn on this one diff --git a/crates/ecolor/src/color32.rs b/crates/ecolor/src/color32.rs index b5e052762..72d0496ee 100644 --- a/crates/ecolor/src/color32.rs +++ b/crates/ecolor/src/color32.rs @@ -273,7 +273,10 @@ impl Color32 { /// This is perceptually even, and faster that [`Self::linear_multiply`]. #[inline] pub fn gamma_multiply(self, factor: f32) -> Self { - debug_assert!(0.0 <= factor && factor.is_finite()); + debug_assert!( + 0.0 <= factor && factor.is_finite(), + "factor should be finite, but was {factor}" + ); let Self([r, g, b, a]) = self; Self([ (r as f32 * factor + 0.5) as u8, @@ -306,7 +309,10 @@ impl Color32 { /// You likely want to use [`Self::gamma_multiply`] instead. #[inline] pub fn linear_multiply(self, factor: f32) -> Self { - debug_assert!(0.0 <= factor && factor.is_finite()); + debug_assert!( + 0.0 <= factor && factor.is_finite(), + "factor should be finite, but was {factor}" + ); // As an unfortunate side-effect of using premultiplied alpha // we need a somewhat expensive conversion to linear space and back. Rgba::from(self).multiply(factor).into() diff --git a/crates/ecolor/src/rgba.rs b/crates/ecolor/src/rgba.rs index 85535bf3a..99fab41cc 100644 --- a/crates/ecolor/src/rgba.rs +++ b/crates/ecolor/src/rgba.rs @@ -90,15 +90,24 @@ impl Rgba { #[inline] pub fn from_luminance_alpha(l: f32, a: f32) -> Self { - debug_assert!(0.0 <= l && l <= 1.0); - debug_assert!(0.0 <= a && a <= 1.0); + debug_assert!( + 0.0 <= l && l <= 1.0, + "l should be in the range [0, 1], but was {l}" + ); + debug_assert!( + 0.0 <= a && a <= 1.0, + "a should be in the range [0, 1], but was {a}" + ); Self([l * a, l * a, l * a, a]) } /// Transparent black #[inline] pub fn from_black_alpha(a: f32) -> Self { - debug_assert!(0.0 <= a && a <= 1.0); + debug_assert!( + 0.0 <= a && a <= 1.0, + "a should be in the range [0, 1], but was {a}" + ); Self([0.0, 0.0, 0.0, a]) } diff --git a/crates/eframe/src/stopwatch.rs b/crates/eframe/src/stopwatch.rs index 9b0136189..e6eabcbdd 100644 --- a/crates/eframe/src/stopwatch.rs +++ b/crates/eframe/src/stopwatch.rs @@ -18,7 +18,7 @@ impl Stopwatch { } pub fn start(&mut self) { - assert!(self.start.is_none()); + assert!(self.start.is_none(), "Stopwatch already running"); self.start = Some(Instant::now()); } @@ -29,7 +29,7 @@ impl Stopwatch { } pub fn resume(&mut self) { - assert!(self.start.is_none()); + assert!(self.start.is_none(), "Stopwatch still running"); self.start = Some(Instant::now()); } diff --git a/crates/egui/src/containers/menu.rs b/crates/egui/src/containers/menu.rs index 26e2fb793..7511d07d1 100644 --- a/crates/egui/src/containers/menu.rs +++ b/crates/egui/src/containers/menu.rs @@ -159,7 +159,6 @@ impl MenuState { } /// Horizontal menu bar where you can add [`MenuButton`]s. - /// The menu bar goes well in a [`crate::TopBottomPanel::top`], /// but can also be placed in a [`crate::Window`]. /// In the latter case you may want to wrap it in [`Frame`]. diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 3bdf1507f..44af99feb 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -82,7 +82,11 @@ impl Default for WrappedTextureManager { epaint::FontImage::new([0, 0]).into(), Default::default(), ); - assert_eq!(font_id, TextureId::default()); + assert_eq!( + font_id, + TextureId::default(), + "font id should be equal to TextureId::default(), but was {font_id:?}", + ); Self(Arc::new(RwLock::new(tex_mngr))) } @@ -804,7 +808,11 @@ impl Context { let max_passes = self.write(|ctx| ctx.memory.options.max_passes.get()); let mut output = FullOutput::default(); - debug_assert_eq!(output.platform_output.num_completed_passes, 0); + debug_assert_eq!( + output.platform_output.num_completed_passes, 0, + "output must be fresh, but had {} passes", + output.platform_output.num_completed_passes + ); loop { profiling::scope!( @@ -828,7 +836,11 @@ impl Context { self.begin_pass(new_input.take()); run_ui(self); output.append(self.end_pass()); - debug_assert!(0 < output.platform_output.num_completed_passes); + debug_assert!( + 0 < output.platform_output.num_completed_passes, + "Completed passes was lower than 0, was {}", + output.platform_output.num_completed_passes + ); if !output.platform_output.requested_discard() { break; // no need for another pass @@ -3272,7 +3284,11 @@ impl Context { #[cfg(feature = "accesskit")] self.pass_state_mut(|fs| { if let Some(state) = fs.accesskit_state.as_mut() { - assert_eq!(state.parent_stack.pop(), Some(_id)); + assert_eq!( + state.parent_stack.pop(), + Some(_id), + "Mismatched push/pop in with_accessibility_parent" + ); } }); diff --git a/crates/egui/src/hit_test.rs b/crates/egui/src/hit_test.rs index 910e558b9..e79172129 100644 --- a/crates/egui/src/hit_test.rs +++ b/crates/egui/src/hit_test.rs @@ -175,11 +175,17 @@ pub fn hit_test( restore_widget_rect(wr); } if let Some(wr) = &mut hits.drag { - debug_assert!(wr.sense.senses_drag()); + debug_assert!( + wr.sense.senses_drag(), + "We should only return drag hits if they sense drag" + ); restore_widget_rect(wr); } if let Some(wr) = &mut hits.click { - debug_assert!(wr.sense.senses_click()); + debug_assert!( + wr.sense.senses_click(), + "We should only return click hits if they sense click" + ); restore_widget_rect(wr); } } diff --git a/crates/egui/src/layout.rs b/crates/egui/src/layout.rs index e967aaa6d..91f39fb2d 100644 --- a/crates/egui/src/layout.rs +++ b/crates/egui/src/layout.rs @@ -71,9 +71,17 @@ impl Region { } pub fn sanity_check(&self) { - debug_assert!(!self.min_rect.any_nan()); - debug_assert!(!self.max_rect.any_nan()); - debug_assert!(!self.cursor.any_nan()); + debug_assert!( + !self.min_rect.any_nan(), + "min rect has Nan: {:?}", + self.min_rect + ); + debug_assert!( + !self.max_rect.any_nan(), + "max rect has Nan: {:?}", + self.max_rect + ); + debug_assert!(!self.cursor.any_nan(), "cursor has Nan: {:?}", self.cursor); } } @@ -394,8 +402,8 @@ impl Layout { /// ## Doing layout impl Layout { pub fn align_size_within_rect(&self, size: Vec2, outer: Rect) -> Rect { - debug_assert!(size.x >= 0.0 && size.y >= 0.0); - debug_assert!(!outer.is_negative()); + debug_assert!(size.x >= 0.0 && size.y >= 0.0, "Negative size: {size:?}"); + debug_assert!(!outer.is_negative(), "Negative outer: {outer:?}"); self.align2().align_size_within_rect(size, outer).round_ui() } @@ -421,7 +429,7 @@ impl Layout { } pub(crate) fn region_from_max_rect(&self, max_rect: Rect) -> Region { - debug_assert!(!max_rect.any_nan()); + debug_assert!(!max_rect.any_nan(), "max_rect is not NaN: {max_rect:?}"); let mut region = Region { min_rect: Rect::NOTHING, // temporary max_rect, @@ -454,8 +462,8 @@ impl Layout { /// Given the cursor in the region, how much space is available /// for the next widget? fn available_from_cursor_max_rect(&self, cursor: Rect, max_rect: Rect) -> Rect { - debug_assert!(!cursor.any_nan()); - debug_assert!(!max_rect.any_nan()); + debug_assert!(!cursor.any_nan(), "cursor is NaN: {cursor:?}"); + debug_assert!(!max_rect.any_nan(), "max_rect is NaN: {max_rect:?}"); // NOTE: in normal top-down layout the cursor has moved below the current max_rect, // but the available shouldn't be negative. @@ -509,7 +517,7 @@ impl Layout { avail.max.y = y; } - debug_assert!(!avail.any_nan()); + debug_assert!(!avail.any_nan(), "avail is NaN: {avail:?}"); avail } @@ -520,7 +528,10 @@ impl Layout { /// Use `justify_and_align` to get the inner `widget_rect`. pub(crate) fn next_frame(&self, region: &Region, child_size: Vec2, spacing: Vec2) -> Rect { region.sanity_check(); - debug_assert!(child_size.x >= 0.0 && child_size.y >= 0.0); + debug_assert!( + child_size.x >= 0.0 && child_size.y >= 0.0, + "Negative size: {child_size:?}" + ); if self.main_wrap { let available_size = self.available_rect_before_wrap(region).size(); @@ -600,7 +611,10 @@ impl Layout { fn next_frame_ignore_wrap(&self, region: &Region, child_size: Vec2) -> Rect { region.sanity_check(); - debug_assert!(child_size.x >= 0.0 && child_size.y >= 0.0); + debug_assert!( + child_size.x >= 0.0 && child_size.y >= 0.0, + "Negative size: {child_size:?}" + ); let available_rect = self.available_rect_before_wrap(region); @@ -633,16 +647,19 @@ impl Layout { frame_rect = frame_rect.translate(Vec2::Y * (region.cursor.top() - frame_rect.top())); } - debug_assert!(!frame_rect.any_nan()); - debug_assert!(!frame_rect.is_negative()); + debug_assert!(!frame_rect.any_nan(), "frame_rect is NaN: {frame_rect:?}"); + debug_assert!(!frame_rect.is_negative(), "frame_rect is negative"); frame_rect.round_ui() } /// Apply justify (fill width/height) and/or alignment after calling `next_space`. pub(crate) fn justify_and_align(&self, frame: Rect, mut child_size: Vec2) -> Rect { - debug_assert!(child_size.x >= 0.0 && child_size.y >= 0.0); - debug_assert!(!frame.is_negative()); + debug_assert!( + child_size.x >= 0.0 && child_size.y >= 0.0, + "Negative size: {child_size:?}" + ); + debug_assert!(!frame.is_negative(), "frame is negative"); if self.horizontal_justify() { child_size.x = child_size.x.at_least(frame.width()); // fill full width @@ -660,8 +677,8 @@ impl Layout { ) -> Rect { let frame = self.next_frame_ignore_wrap(region, size); let rect = self.align_size_within_rect(size, frame); - debug_assert!(!rect.any_nan()); - debug_assert!(!rect.is_negative()); + debug_assert!(!rect.any_nan(), "rect is NaN: {rect:?}"); + debug_assert!(!rect.is_negative(), "rect is negative: {rect:?}"); rect } @@ -704,7 +721,7 @@ impl Layout { widget_rect: Rect, item_spacing: Vec2, ) { - debug_assert!(!cursor.any_nan()); + debug_assert!(!cursor.any_nan(), "cursor is NaN: {cursor:?}"); if self.main_wrap { if cursor.intersects(frame_rect.shrink(1.0)) { // make row/column larger if necessary diff --git a/crates/egui/src/placer.rs b/crates/egui/src/placer.rs index 2de822c03..6ffe07ed6 100644 --- a/crates/egui/src/placer.rs +++ b/crates/egui/src/placer.rs @@ -133,8 +133,8 @@ impl Placer { /// Apply justify or alignment after calling `next_space`. pub(crate) fn justify_and_align(&self, rect: Rect, child_size: Vec2) -> Rect { - debug_assert!(!rect.any_nan()); - debug_assert!(!child_size.any_nan()); + debug_assert!(!rect.any_nan(), "rect: {rect:?}"); + debug_assert!(!child_size.any_nan(), "child_size is NaN: {child_size:?}"); if let Some(grid) = &self.grid { grid.justify_and_align(rect, child_size) @@ -164,8 +164,11 @@ impl Placer { widget_rect: Rect, item_spacing: Vec2, ) { - debug_assert!(!frame_rect.any_nan()); - debug_assert!(!widget_rect.any_nan()); + debug_assert!(!frame_rect.any_nan(), "frame_rect: {frame_rect:?}"); + debug_assert!( + !widget_rect.any_nan(), + "widget_rect is NaN: {widget_rect:?}" + ); self.region.sanity_check(); if let Some(grid) = &mut self.grid { diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index a1b522efc..28f4bde73 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -985,7 +985,10 @@ impl Response { /// /// You may not call [`Self::interact`] on the resulting `Response`. pub fn union(&self, other: Self) -> Self { - assert!(self.ctx == other.ctx); + assert!( + self.ctx == other.ctx, + "Responses must be from the same `Context`" + ); debug_assert!( self.layer_id == other.layer_id, "It makes no sense to combine Responses from two different layers" diff --git a/crates/egui/src/text_selection/text_cursor_state.rs b/crates/egui/src/text_selection/text_cursor_state.rs index 21ebda3d0..baf7b0463 100644 --- a/crates/egui/src/text_selection/text_cursor_state.rs +++ b/crates/egui/src/text_selection/text_cursor_state.rs @@ -271,7 +271,12 @@ pub fn byte_index_from_char_index(s: &str, char_index: usize) -> usize { } pub fn slice_char_range(s: &str, char_range: std::ops::Range) -> &str { - assert!(char_range.start <= char_range.end); + assert!( + char_range.start <= char_range.end, + "Invalid range, start must be less than end, but start = {}, end = {}", + char_range.start, + char_range.end + ); let start_byte = byte_index_from_char_index(s, char_range.start); let end_byte = byte_index_from_char_index(s, char_range.end); &s[start_byte..end_byte] diff --git a/crates/egui/src/text_selection/visuals.rs b/crates/egui/src/text_selection/visuals.rs index 4025a2d5f..32a040a89 100644 --- a/crates/egui/src/text_selection/visuals.rs +++ b/crates/egui/src/text_selection/visuals.rs @@ -59,7 +59,11 @@ pub fn paint_text_selection( // Start by appending the selection rectangle to end of the mesh, as two triangles (= 6 indices): let num_indices_before = mesh.indices.len(); mesh.add_colored_rect(rect, color); - assert_eq!(num_indices_before + 6, mesh.indices.len()); + assert_eq!( + num_indices_before + 6, + mesh.indices.len(), + "We expect exactly 6 new indices" + ); // Copy out the new triangles: let selection_triangles = [ diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index dad036818..0e8509bb8 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -286,7 +286,7 @@ impl Ui { } } - debug_assert!(!max_rect.any_nan()); + debug_assert!(!max_rect.any_nan(), "max_rect is NaN: {max_rect:?}"); let stable_id = self.id.with(id_salt); let unique_id = stable_id.with(self.next_auto_id_salt); let next_auto_id_salt = unique_id.value().wrapping_add(1); @@ -914,14 +914,20 @@ impl Ui { /// Set the minimum width of the ui. /// This can't shrink the ui, only make it larger. pub fn set_min_width(&mut self, width: f32) { - debug_assert!(0.0 <= width); + debug_assert!( + 0.0 <= width, + "Negative width makes no sense, but got: {width}" + ); self.placer.set_min_width(width); } /// Set the minimum height of the ui. /// This can't shrink the ui, only make it larger. pub fn set_min_height(&mut self, height: f32) { - debug_assert!(0.0 <= height); + debug_assert!( + 0.0 <= height, + "Negative height makes no sense, but got: {height}" + ); self.placer.set_min_height(height); } @@ -1399,7 +1405,7 @@ impl Ui { fn allocate_space_impl(&mut self, desired_size: Vec2) -> Rect { let item_spacing = self.spacing().item_spacing; let frame_rect = self.placer.next_space(desired_size, item_spacing); - debug_assert!(!frame_rect.any_nan()); + debug_assert!(!frame_rect.any_nan(), "frame_rect is nan in allocate_space"); let widget_rect = self.placer.justify_and_align(frame_rect, desired_size); self.placer @@ -1422,7 +1428,7 @@ impl Ui { /// Allocate a rect without interacting with it. pub fn advance_cursor_after_rect(&mut self, rect: Rect) -> Id { - debug_assert!(!rect.any_nan()); + debug_assert!(!rect.any_nan(), "rect is nan in advance_cursor_after_rect"); let rect = rect.round_ui(); let item_spacing = self.spacing().item_spacing; @@ -1494,7 +1500,10 @@ impl Ui { layout: Layout, add_contents: Box R + 'c>, ) -> InnerResponse { - debug_assert!(desired_size.x >= 0.0 && desired_size.y >= 0.0); + debug_assert!( + desired_size.x >= 0.0 && desired_size.y >= 0.0, + "Negative desired size: {desired_size:?}" + ); let item_spacing = self.spacing().item_spacing; let frame_rect = self.placer.next_space(desired_size, item_spacing); let child_rect = self.placer.justify_and_align(frame_rect, desired_size); diff --git a/crates/egui/src/widgets/label.rs b/crates/egui/src/widgets/label.rs index 34b684df6..c36b9fc60 100644 --- a/crates/egui/src/widgets/label.rs +++ b/crates/egui/src/widgets/label.rs @@ -199,7 +199,10 @@ impl Label { let cursor = ui.cursor(); let first_row_indentation = available_width - ui.available_size_before_wrap().x; - debug_assert!(first_row_indentation.is_finite()); + debug_assert!( + first_row_indentation.is_finite(), + "first row indentation is not finite: {first_row_indentation}" + ); layout_job.wrap.max_width = available_width; layout_job.first_row_min_height = cursor.height(); diff --git a/crates/egui/src/widgets/slider.rs b/crates/egui/src/widgets/slider.rs index 590282074..e8b026ff5 100644 --- a/crates/egui/src/widgets/slider.rs +++ b/crates/egui/src/widgets/slider.rs @@ -1065,7 +1065,10 @@ fn value_from_normalized(normalized: f64, range: RangeInclusive, spec: &Sli let log = lerp(min_log..=max_log, normalized); 10.0_f64.powf(log) } else { - assert!(min < 0.0 && 0.0 < max); + assert!( + min < 0.0 && 0.0 < max, + "min should be negative and max positive, but got min={min} and max={max}" + ); let zero_cutoff = logarithmic_zero_cutoff(min, max); if normalized < zero_cutoff { // negative @@ -1114,7 +1117,10 @@ fn normalized_from_value(value: f64, range: RangeInclusive, spec: &SliderSp let value_log = value.log10(); remap_clamp(value_log, min_log..=max_log, 0.0..=1.0) } else { - assert!(min < 0.0 && 0.0 < max); + assert!( + min < 0.0 && 0.0 < max, + "min should be negative and max positive, but got min={min} and max={max}" + ); let zero_cutoff = logarithmic_zero_cutoff(min, max); if value < 0.0 { // negative @@ -1142,8 +1148,11 @@ fn normalized_from_value(value: f64, range: RangeInclusive, spec: &SliderSp } fn range_log10(min: f64, max: f64, spec: &SliderSpec) -> (f64, f64) { - assert!(spec.logarithmic); - assert!(min <= max); + assert!(spec.logarithmic, "spec must be logarithmic"); + assert!( + min <= max, + "min must be less than or equal to max, but was min={min} and max={max}" + ); if min == 0.0 && max == INFINITY { (spec.smallest_positive.log10(), INF_RANGE_MAGNITUDE) @@ -1167,7 +1176,10 @@ fn range_log10(min: f64, max: f64, spec: &SliderSpec) -> (f64, f64) { /// where to put the zero cutoff for logarithmic sliders /// that crosses zero ? fn logarithmic_zero_cutoff(min: f64, max: f64) -> f64 { - assert!(min < 0.0 && 0.0 < max); + assert!( + min < 0.0 && 0.0 < max, + "min must be negative and max positive, but got min={min} and max={max}" + ); let min_magnitude = if min == -INFINITY { INF_RANGE_MAGNITUDE diff --git a/crates/egui/src/widgets/text_edit/text_buffer.rs b/crates/egui/src/widgets/text_edit/text_buffer.rs index 9290f1e9c..ccf3a0958 100644 --- a/crates/egui/src/widgets/text_edit/text_buffer.rs +++ b/crates/egui/src/widgets/text_edit/text_buffer.rs @@ -194,7 +194,10 @@ impl TextBuffer for String { } fn delete_char_range(&mut self, char_range: Range) { - assert!(char_range.start <= char_range.end); + assert!( + char_range.start <= char_range.end, + "start must be <= end, but got {char_range:?}" + ); // Get both byte indices let byte_start = byte_index_from_char_index(self.as_str(), char_range.start); diff --git a/crates/egui_demo_lib/src/rendering_test.rs b/crates/egui_demo_lib/src/rendering_test.rs index 879e2c7af..14714cca7 100644 --- a/crates/egui_demo_lib/src/rendering_test.rs +++ b/crates/egui_demo_lib/src/rendering_test.rs @@ -307,7 +307,10 @@ fn vertex_gradient(ui: &mut Ui, bg_fill: Color32, gradient: &Gradient) -> Respon } { let n = gradient.0.len(); - assert!(n >= 2); + assert!( + n >= 2, + "A gradient must have at least two colors, but this had {n}" + ); let mut mesh = Mesh::default(); for (i, &color) in gradient.0.iter().enumerate() { let t = i as f32 / (n as f32 - 1.0); @@ -594,8 +597,14 @@ fn blending_and_feathering_test(ui: &mut Ui) { } fn text_on_bg(ui: &mut egui::Ui, fg: Color32, bg: Color32) { - assert!(fg.is_opaque()); - assert!(bg.is_opaque()); + assert!( + fg.is_opaque(), + "Foreground color must be opaque, but was: {fg:?}", + ); + assert!( + bg.is_opaque(), + "Background color must be opaque, but was: {bg:?}", + ); ui.horizontal(|ui| { ui.label( diff --git a/crates/egui_extras/src/loaders/file_loader.rs b/crates/egui_extras/src/loaders/file_loader.rs index 5d1b45fb4..8b762887b 100644 --- a/crates/egui_extras/src/loaders/file_loader.rs +++ b/crates/egui_extras/src/loaders/file_loader.rs @@ -96,7 +96,7 @@ impl BytesLoader for FileLoader { Err(err) => Err(err.to_string()), }; let prev = cache.lock().insert(uri.clone(), Poll::Ready(result)); - assert!(matches!(prev, Some(Poll::Pending))); + assert!(matches!(prev, Some(Poll::Pending)), "unexpected state"); ctx.request_repaint(); log::trace!("finished loading {uri:?}"); } diff --git a/crates/egui_extras/src/sizing.rs b/crates/egui_extras/src/sizing.rs index 770306770..7f32a84e0 100644 --- a/crates/egui_extras/src/sizing.rs +++ b/crates/egui_extras/src/sizing.rs @@ -32,7 +32,10 @@ impl Size { /// Relative size relative to all available space. Values must be in range `0.0..=1.0`. pub fn relative(fraction: f32) -> Self { - debug_assert!(0.0 <= fraction && fraction <= 1.0); + debug_assert!( + 0.0 <= fraction && fraction <= 1.0, + "fraction should be in the range [0, 1], but was {fraction}" + ); Self::Relative { fraction, range: Rangef::new(0.0, f32::INFINITY), @@ -121,7 +124,10 @@ impl Sizing { .map(|&size| match size { Size::Absolute { initial, .. } => initial, Size::Relative { fraction, range } => { - assert!(0.0 <= fraction && fraction <= 1.0); + assert!( + 0.0 <= fraction && fraction <= 1.0, + "fraction should be in the range [0, 1], but was {fraction}" + ); range.clamp(length * fraction) } Size::Remainder { .. } => { diff --git a/crates/egui_extras/src/syntax_highlighting.rs b/crates/egui_extras/src/syntax_highlighting.rs index 027ba5ee9..77ad0cc2d 100644 --- a/crates/egui_extras/src/syntax_highlighting.rs +++ b/crates/egui_extras/src/syntax_highlighting.rs @@ -490,8 +490,15 @@ impl Highlighter { fn as_byte_range(whole: &str, range: &str) -> std::ops::Range { let whole_start = whole.as_ptr() as usize; let range_start = range.as_ptr() as usize; - assert!(whole_start <= range_start); - assert!(range_start + range.len() <= whole_start + whole.len()); + assert!( + whole_start <= range_start, + "range must be within whole, but was {range}" + ); + assert!( + range_start + range.len() <= whole_start + whole.len(), + "range_start + range length must be smaller than whole_start + whole length, but was {}", + range_start + range.len() + ); let offset = range_start - whole_start; offset..(offset + range.len()) } diff --git a/crates/egui_glow/src/painter.rs b/crates/egui_glow/src/painter.rs index bec46cf08..1d1322c42 100644 --- a/crates/egui_glow/src/painter.rs +++ b/crates/egui_glow/src/painter.rs @@ -469,7 +469,7 @@ impl Painter { #[inline(never)] // Easier profiling fn paint_mesh(&mut self, mesh: &Mesh) { - debug_assert!(mesh.is_valid()); + debug_assert!(mesh.is_valid(), "Mesh is not valid"); if let Some(texture) = self.texture(mesh.texture_id) { unsafe { self.gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.vbo)); @@ -560,7 +560,12 @@ impl Painter { data: &[u8], ) { profiling::function_scope!(); - assert_eq!(data.len(), w * h * 4); + assert_eq!( + data.len(), + w * h * 4, + "Mismatch between texture size and texel count, by {}", + data.len() % (w * h * 4) + ); assert!( w <= self.max_texture_side && h <= self.max_texture_side, "Got a texture image of size {}x{}, but the maximum supported texture side is only {}", diff --git a/crates/emath/src/lib.rs b/crates/emath/src/lib.rs index ae04f9ecf..ad657fcc3 100644 --- a/crates/emath/src/lib.rs +++ b/crates/emath/src/lib.rs @@ -149,7 +149,10 @@ where { let from = from.into(); let to = to.into(); - debug_assert!(from.start() != from.end()); + debug_assert!( + from.start() != from.end(), + "from.start() and from.end() should not be equal" + ); let t = (x - *from.start()) / (*from.end() - *from.start()); lerp(to, t) } @@ -173,7 +176,10 @@ where } else if *from.end() <= x { *to.end() } else { - debug_assert!(from.start() != from.end()); + debug_assert!( + from.start() != from.end(), + "from.start() and from.end() should not be equal" + ); let t = (x - *from.start()) / (*from.end() - *from.start()); // Ensure no numerical inaccuracies sneak in: if T::ONE <= t { @@ -200,8 +206,14 @@ pub fn format_with_minimum_decimals(value: f64, decimals: usize) -> String { pub fn format_with_decimals_in_range(value: f64, decimal_range: RangeInclusive) -> String { let min_decimals = *decimal_range.start(); let max_decimals = *decimal_range.end(); - debug_assert!(min_decimals <= max_decimals); - debug_assert!(max_decimals < 100); + debug_assert!( + min_decimals <= max_decimals, + "min_decimals should be <= max_decimals, but got min_decimals: {min_decimals}, max_decimals: {max_decimals}" + ); + debug_assert!( + max_decimals < 100, + "max_decimals should be < 100, but got {max_decimals}" + ); let max_decimals = max_decimals.min(16); let min_decimals = min_decimals.min(max_decimals); diff --git a/crates/emath/src/rot2.rs b/crates/emath/src/rot2.rs index 88c425a50..9af0103a0 100644 --- a/crates/emath/src/rot2.rs +++ b/crates/emath/src/rot2.rs @@ -84,7 +84,10 @@ impl Rot2 { c: self.c / l, s: self.s / l, }; - debug_assert!(ret.is_finite()); + debug_assert!( + ret.is_finite(), + "Rot2::normalized produced a non-finite result" + ); ret } } diff --git a/crates/emath/src/smart_aim.rs b/crates/emath/src/smart_aim.rs index 720947069..9ef010d9a 100644 --- a/crates/emath/src/smart_aim.rs +++ b/crates/emath/src/smart_aim.rs @@ -33,7 +33,10 @@ pub fn best_in_range_f64(min: f64, max: f64) -> f64 { if !max.is_finite() { return min; } - debug_assert!(min.is_finite() && max.is_finite()); + debug_assert!( + min.is_finite() && max.is_finite(), + "min: {min:?}, max: {max:?}" + ); let min_exponent = min.log10(); let max_exponent = max.log10(); @@ -101,7 +104,10 @@ fn from_decimal_string(s: &[i32]) -> f64 { /// Find the simplest integer in the range [min, max] fn simplest_digit_closed_range(min: i32, max: i32) -> i32 { - debug_assert!(1 <= min && min <= max && max <= 9); + debug_assert!( + 1 <= min && min <= max && max <= 9, + "min should be in [1, 9], but was {min:?} and max should be in [min, 9], but was {max:?}" + ); if min <= 5 && 5 <= max { 5 } else { diff --git a/crates/epaint/benches/benchmark.rs b/crates/epaint/benches/benchmark.rs index e723638b8..444f81a11 100644 --- a/crates/epaint/benches/benchmark.rs +++ b/crates/epaint/benches/benchmark.rs @@ -53,7 +53,12 @@ fn tessellate_circles(c: &mut Criterion) { clipped_shapes.push(ClippedShape { clip_rect, shape }); } } - assert_eq!(clipped_shapes.len(), 100_000); + assert_eq!( + clipped_shapes.len(), + 100_000, + "length of clipped shapes should be 100k, but was {}", + clipped_shapes.len() + ); let pixels_per_point = 2.0; let options = TessellationOptions::default(); diff --git a/crates/epaint/src/image.rs b/crates/epaint/src/image.rs index 69844f42a..e50e33e65 100644 --- a/crates/epaint/src/image.rs +++ b/crates/epaint/src/image.rs @@ -94,7 +94,13 @@ impl ColorImage { /// } /// ``` pub fn from_rgba_unmultiplied(size: [usize; 2], rgba: &[u8]) -> Self { - assert_eq!(size[0] * size[1] * 4, rgba.len()); + assert_eq!( + size[0] * size[1] * 4, + rgba.len(), + "size: {:?}, rgba.len(): {}", + size, + rgba.len() + ); let pixels = rgba .chunks_exact(4) .map(|p| Color32::from_rgba_unmultiplied(p[0], p[1], p[2], p[3])) @@ -103,7 +109,13 @@ impl ColorImage { } pub fn from_rgba_premultiplied(size: [usize; 2], rgba: &[u8]) -> Self { - assert_eq!(size[0] * size[1] * 4, rgba.len()); + assert_eq!( + size[0] * size[1] * 4, + rgba.len(), + "size: {:?}, rgba.len(): {}", + size, + rgba.len() + ); let pixels = rgba .chunks_exact(4) .map(|p| Color32::from_rgba_premultiplied(p[0], p[1], p[2], p[3])) @@ -115,7 +127,13 @@ impl ColorImage { /// /// Panics if `size[0] * size[1] != gray.len()`. pub fn from_gray(size: [usize; 2], gray: &[u8]) -> Self { - assert_eq!(size[0] * size[1], gray.len()); + assert_eq!( + size[0] * size[1], + gray.len(), + "size: {:?}, gray.len(): {}", + size, + gray.len() + ); let pixels = gray.iter().map(|p| Color32::from_gray(*p)).collect(); Self { size, pixels } } @@ -127,7 +145,13 @@ impl ColorImage { #[doc(alias = "from_grey_iter")] pub fn from_gray_iter(size: [usize; 2], gray_iter: impl Iterator) -> Self { let pixels: Vec<_> = gray_iter.map(Color32::from_gray).collect(); - assert_eq!(size[0] * size[1], pixels.len()); + assert_eq!( + size[0] * size[1], + pixels.len(), + "size: {:?}, pixels.len(): {}", + size, + pixels.len() + ); Self { size, pixels } } @@ -150,7 +174,13 @@ impl ColorImage { /// /// Panics if `size[0] * size[1] * 3 != rgb.len()`. pub fn from_rgb(size: [usize; 2], rgb: &[u8]) -> Self { - assert_eq!(size[0] * size[1] * 3, rgb.len()); + assert_eq!( + size[0] * size[1] * 3, + rgb.len(), + "size: {:?}, rgb.len(): {}", + size, + rgb.len() + ); let pixels = rgb .chunks_exact(3) .map(|p| Color32::from_rgb(p[0], p[1], p[2])) @@ -225,7 +255,7 @@ impl std::ops::Index<(usize, usize)> for ColorImage { #[inline] fn index(&self, (x, y): (usize, usize)) -> &Color32 { let [w, h] = self.size; - assert!(x < w && y < h); + assert!(x < w && y < h, "x: {x}, y: {y}, w: {w}, h: {h}"); &self.pixels[y * w + x] } } @@ -234,7 +264,7 @@ impl std::ops::IndexMut<(usize, usize)> for ColorImage { #[inline] fn index_mut(&mut self, (x, y): (usize, usize)) -> &mut Color32 { let [w, h] = self.size; - assert!(x < w && y < h); + assert!(x < w && y < h, "x: {x}, y: {y}, w: {w}, h: {h}"); &mut self.pixels[y * w + x] } } @@ -328,15 +358,32 @@ impl FontImage { /// Clone a sub-region as a new image. pub fn region(&self, [x, y]: [usize; 2], [w, h]: [usize; 2]) -> Self { - assert!(x + w <= self.width()); - assert!(y + h <= self.height()); + assert!( + x + w <= self.width(), + "x + w should be <= self.width(), but x: {}, w: {}, width: {}", + x, + w, + self.width() + ); + assert!( + y + h <= self.height(), + "y + h should be <= self.height(), but y: {}, h: {}, height: {}", + y, + h, + self.height() + ); let mut pixels = Vec::with_capacity(w * h); for y in y..y + h { let offset = y * self.width() + x; pixels.extend(&self.pixels[offset..(offset + w)]); } - assert_eq!(pixels.len(), w * h); + assert_eq!( + pixels.len(), + w * h, + "pixels.len should be w * h, but got {}", + pixels.len() + ); Self { size: [w, h], pixels, @@ -350,7 +397,7 @@ impl std::ops::Index<(usize, usize)> for FontImage { #[inline] fn index(&self, (x, y): (usize, usize)) -> &f32 { let [w, h] = self.size; - assert!(x < w && y < h); + assert!(x < w && y < h, "x: {x}, y: {y}, w: {w}, h: {h}"); &self.pixels[y * w + x] } } @@ -359,7 +406,7 @@ impl std::ops::IndexMut<(usize, usize)> for FontImage { #[inline] fn index_mut(&mut self, (x, y): (usize, usize)) -> &mut f32 { let [w, h] = self.size; - assert!(x < w && y < h); + assert!(x < w && y < h, "x: {x}, y: {y}, w: {w}, h: {h}"); &mut self.pixels[y * w + x] } } diff --git a/crates/epaint/src/mesh.rs b/crates/epaint/src/mesh.rs index 930cb7716..60be2935c 100644 --- a/crates/epaint/src/mesh.rs +++ b/crates/epaint/src/mesh.rs @@ -119,7 +119,7 @@ impl Mesh { /// Panics when `other` mesh has a different texture. pub fn append(&mut self, other: Self) { profiling::function_scope!(); - debug_assert!(other.is_valid()); + debug_assert!(other.is_valid(), "Other mesh is invalid"); if self.is_empty() { *self = other; @@ -133,7 +133,7 @@ impl Mesh { /// /// Panics when `other` mesh has a different texture. pub fn append_ref(&mut self, other: &Self) { - debug_assert!(other.is_valid()); + debug_assert!(other.is_valid(), "Other mesh is invalid"); if self.is_empty() { self.texture_id = other.texture_id; @@ -155,7 +155,10 @@ impl Mesh { /// Panics when the mesh has assigned a texture. #[inline(always)] pub fn colored_vertex(&mut self, pos: Pos2, color: Color32) { - debug_assert!(self.texture_id == TextureId::default()); + debug_assert!( + self.texture_id == TextureId::default(), + "Mesh has an assigned texture" + ); self.vertices.push(Vertex { pos, uv: WHITE_UV, @@ -218,7 +221,10 @@ impl Mesh { /// Uniformly colored rectangle. #[inline(always)] pub fn add_colored_rect(&mut self, rect: Rect, color: Color32) { - debug_assert!(self.texture_id == TextureId::default()); + debug_assert!( + self.texture_id == TextureId::default(), + "Mesh has an assigned texture" + ); self.add_rect_with_uv(rect, [WHITE_UV, WHITE_UV].into(), color); } @@ -227,7 +233,7 @@ impl Mesh { /// Splits this mesh into many smaller meshes (if needed) /// where the smaller meshes have 16-bit indices. pub fn split_to_u16(self) -> Vec { - debug_assert!(self.is_valid()); + debug_assert!(self.is_valid(), "Mesh is invalid"); const MAX_SIZE: u32 = u16::MAX as u32; @@ -280,7 +286,7 @@ impl Mesh { vertices: self.vertices[(min_vindex as usize)..=(max_vindex as usize)].to_vec(), texture_id: self.texture_id, }; - debug_assert!(mesh.is_valid()); + debug_assert!(mesh.is_valid(), "Mesh is invalid"); output.push(mesh); } output diff --git a/crates/epaint/src/shapes/shape.rs b/crates/epaint/src/shapes/shape.rs index 6c24881de..d17f528c8 100644 --- a/crates/epaint/src/shapes/shape.rs +++ b/crates/epaint/src/shapes/shape.rs @@ -339,7 +339,7 @@ impl Shape { #[inline] pub fn mesh(mesh: impl Into>) -> Self { let mesh = mesh.into(); - debug_assert!(mesh.is_valid()); + debug_assert!(mesh.is_valid(), "Invalid mesh: {mesh:#?}"); Self::Mesh(mesh) } @@ -525,7 +525,13 @@ fn dashes_from_line( shapes: &mut Vec, dash_offset: f32, ) { - assert_eq!(dash_lengths.len(), gap_lengths.len()); + assert_eq!( + dash_lengths.len(), + gap_lengths.len(), + "Mismatched dash and gap lengths, got dash_lengths: {}, gap_lengths: {}", + dash_lengths.len(), + gap_lengths.len() + ); let mut position_on_segment = dash_offset; let mut drawing_dash = false; let mut step = 0; diff --git a/crates/epaint/src/stats.rs b/crates/epaint/src/stats.rs index 68bba622e..cb72d90e3 100644 --- a/crates/epaint/src/stats.rs +++ b/crates/epaint/src/stats.rs @@ -111,7 +111,10 @@ impl AllocInfo { } pub fn num_elements(&self) -> usize { - assert!(self.element_size != ElementSize::Heterogenous); + assert!( + self.element_size != ElementSize::Heterogenous, + "Heterogenous element size" + ); self.num_elements } diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 6d953f257..bcb13a12c 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -382,7 +382,7 @@ impl Path { pub fn add_open_points(&mut self, points: &[Pos2]) { let n = points.len(); - assert!(n >= 2); + assert!(n >= 2, "A path needs at least two points, but got {n}"); if n == 2 { // Common case optimization: @@ -428,7 +428,7 @@ impl Path { pub fn add_line_loop(&mut self, points: &[Pos2]) { let n = points.len(); - assert!(n >= 2); + assert!(n >= 2, "A path needs at least two points, but got {n}"); self.reserve(n); let mut n0 = (points[0] - points[n - 1]).normalized().rot90(); diff --git a/crates/epaint/src/text/font.rs b/crates/epaint/src/text/font.rs index a3994ca4c..5415bae07 100644 --- a/crates/epaint/src/text/font.rs +++ b/crates/epaint/src/text/font.rs @@ -91,8 +91,14 @@ impl FontImpl { scale_in_pixels: f32, tweak: FontTweak, ) -> Self { - assert!(scale_in_pixels > 0.0); - assert!(pixels_per_point > 0.0); + assert!( + scale_in_pixels > 0.0, + "scale_in_pixels is smaller than 0, got: {scale_in_pixels:?}" + ); + assert!( + pixels_per_point > 0.0, + "pixels_per_point must be greater than 0, got: {pixels_per_point:?}" + ); use ab_glyph::{Font, ScaleFont}; let scaled = ab_glyph_font.as_scaled(scale_in_pixels); @@ -264,7 +270,7 @@ impl FontImpl { } fn allocate_glyph(&self, glyph_id: ab_glyph::GlyphId) -> GlyphInfo { - assert!(glyph_id.0 != 0); + assert!(glyph_id.0 != 0, "Can't allocate glyph for id 0"); use ab_glyph::{Font as _, ScaleFont}; let glyph = glyph_id.with_scale_and_position( diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index ff973ba48..638b7a705 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -529,7 +529,7 @@ fn halign_and_justify_row( (num_leading_spaces, row.glyphs.len() - num_trailing_spaces) }; let num_glyphs_in_range = glyph_range.1 - glyph_range.0; - assert!(num_glyphs_in_range > 0); + assert!(num_glyphs_in_range > 0, "Should have at least one glyph"); let original_min_x = row.glyphs[glyph_range.0].logical_rect().min.x; let original_max_x = row.glyphs[glyph_range.1 - 1].logical_rect().max.x; @@ -898,7 +898,10 @@ fn add_hline(point_scale: PointScale, [start, stop]: [Pos2; 2], stroke: Stroke, } else { // Thin lines often lost, so this is a bad idea - assert_eq!(start.y, stop.y); + assert_eq!( + start.y, stop.y, + "Horizontal line must be horizontal, but got: {start:?} -> {stop:?}" + ); let min_y = point_scale.round_to_pixel(start.y - 0.5 * stroke.width); let max_y = point_scale.round_to_pixel(min_y + stroke.width); diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 2b8b3e819..6d69045ab 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -892,7 +892,7 @@ impl Galley { } ccursor_it.index += row.char_count_including_newline(); } - debug_assert!(ccursor_it == self.end()); + debug_assert!(ccursor_it == self.end(), "Cursor out of bounds"); if let Some(last_row) = self.rows.last() { LayoutCursor { diff --git a/crates/epaint/src/texture_atlas.rs b/crates/epaint/src/texture_atlas.rs index 7ea76f872..790540224 100644 --- a/crates/epaint/src/texture_atlas.rs +++ b/crates/epaint/src/texture_atlas.rs @@ -88,7 +88,11 @@ impl TextureAtlas { // Make the top left pixel fully white for `WHITE_UV`, i.e. painting something with solid color: let (pos, image) = atlas.allocate((1, 1)); - assert_eq!(pos, (0, 0)); + assert_eq!( + pos, + (0, 0), + "Expected the first allocation to be at (0, 0), but was at {pos:?}" + ); image[pos] = 1.0; // Allocate a series of anti-aliased discs used to render small filled circles: diff --git a/scripts/check.sh b/scripts/check.sh index 0d835617b..1a4eec5da 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -9,7 +9,7 @@ set -x # Checks all tests, lints etc. # Basically does what the CI does. -cargo +1.81.0 install --quiet typos-cli +# cargo +1.81.0 install --quiet typos-cli export RUSTFLAGS="-D warnings" export RUSTDOCFLAGS="-D warnings" # https://github.com/emilk/egui/pull/1454 @@ -35,20 +35,20 @@ cargo test --quiet --doc # slow - checks all doc-tests cargo check --quiet -p eframe --no-default-features --features "glow" if [[ "$OSTYPE" == "linux-gnu"* ]]; then - cargo check --quiet -p eframe --no-default-features --features "wgpu","x11" - cargo check --quiet -p eframe --no-default-features --features "wgpu","wayland" + cargo check --quiet -p eframe --no-default-features --features "wgpu","x11" + cargo check --quiet -p eframe --no-default-features --features "wgpu","wayland" else - cargo check --quiet -p eframe --no-default-features --features "wgpu" + cargo check --quiet -p eframe --no-default-features --features "wgpu" fi cargo check --quiet -p egui --no-default-features --features "serde" cargo check --quiet -p egui_demo_app --no-default-features --features "glow" if [[ "$OSTYPE" == "linux-gnu"* ]]; then - cargo check --quiet -p egui_demo_app --no-default-features --features "wgpu","x11" - cargo check --quiet -p egui_demo_app --no-default-features --features "wgpu","wayland" + cargo check --quiet -p egui_demo_app --no-default-features --features "wgpu","x11" + cargo check --quiet -p egui_demo_app --no-default-features --features "wgpu","wayland" else - cargo check --quiet -p egui_demo_app --no-default-features --features "wgpu" + cargo check --quiet -p egui_demo_app --no-default-features --features "wgpu" fi cargo check --quiet -p egui_demo_lib --no-default-features From 2024295f785d90b8e2150b31bbb8c2ce8b66351a Mon Sep 17 00:00:00 2001 From: Nicolas Date: Tue, 25 Mar 2025 10:17:31 +0100 Subject: [PATCH 031/129] Make ImageLoader use background thread (#5394) This is the same change as in #4069 but as this is stale I wanted to reopen a non stale PR Modifies ImageLoader's load function to use background threads for the image decoding work. This avoids blocking the main thread that is especially noticeable when loading many images at once. This was modelled after the other loader implementations that also use threads. * Closes * [x] I have followed the instructions in the PR template --------- Co-authored-by: lucasmerlin Co-authored-by: Emil Ernerfeldt --- crates/egui_demo_app/tests/test_demo_app.rs | 2 +- .../egui_extras/src/loaders/image_loader.rs | 96 ++++++++++++++----- deny.toml | 2 +- 3 files changed, 75 insertions(+), 25 deletions(-) diff --git a/crates/egui_demo_app/tests/test_demo_app.rs b/crates/egui_demo_app/tests/test_demo_app.rs index 0247b9fc2..056bee49e 100644 --- a/crates/egui_demo_app/tests/test_demo_app.rs +++ b/crates/egui_demo_app/tests/test_demo_app.rs @@ -67,7 +67,7 @@ fn test_demo_app() { } // Can't use Harness::run because fractal clock keeps requesting repaints - harness.run_steps(2); + harness.run_steps(4); results.add(harness.try_snapshot(&anchor.to_string())); } diff --git a/crates/egui_extras/src/loaders/image_loader.rs b/crates/egui_extras/src/loaders/image_loader.rs index af785f6f4..4790c9545 100644 --- a/crates/egui_extras/src/loaders/image_loader.rs +++ b/crates/egui_extras/src/loaders/image_loader.rs @@ -1,18 +1,21 @@ use ahash::HashMap; use egui::{ decode_animated_image_uri, - load::{BytesPoll, ImageLoadResult, ImageLoader, ImagePoll, LoadError, SizeHint}, + load::{Bytes, BytesPoll, ImageLoadResult, ImageLoader, ImagePoll, LoadError, SizeHint}, mutex::Mutex, ColorImage, }; use image::ImageFormat; -use std::{mem::size_of, path::Path, sync::Arc}; +use std::{mem::size_of, path::Path, sync::Arc, task::Poll}; -type Entry = Result, LoadError>; +#[cfg(not(target_arch = "wasm32"))] +use std::thread; + +type Entry = Poll, String>>; #[derive(Default)] pub struct ImageCrateLoader { - cache: Mutex>, + cache: Arc>>, } impl ImageCrateLoader { @@ -73,11 +76,69 @@ impl ImageLoader for ImageCrateLoader { return Err(LoadError::NotSupported); } - let mut cache = self.cache.lock(); - if let Some(entry) = cache.get(uri).cloned() { - match entry { + #[cfg(not(target_arch = "wasm32"))] + #[allow(clippy::unnecessary_wraps)] // needed here to match other return types + fn load_image( + ctx: &egui::Context, + uri: &str, + cache: &Arc>>, + bytes: &Bytes, + ) -> ImageLoadResult { + let uri = uri.to_owned(); + cache.lock().insert(uri.clone(), Poll::Pending); + + // Do the image parsing on a bg thread + thread::Builder::new() + .name(format!("egui_extras::ImageLoader::load({uri:?})")) + .spawn({ + let ctx = ctx.clone(); + let cache = cache.clone(); + + let uri = uri.clone(); + let bytes = bytes.clone(); + move || { + log::trace!("ImageLoader - started loading {uri:?}"); + let result = crate::image::load_image_bytes(&bytes) + .map(Arc::new) + .map_err(|err| err.to_string()); + log::trace!("ImageLoader - finished loading {uri:?}"); + let prev = cache.lock().insert(uri, Poll::Ready(result)); + debug_assert!(matches!(prev, Some(Poll::Pending))); + + ctx.request_repaint(); + } + }) + .expect("failed to spawn thread"); + + Ok(ImagePoll::Pending { size: None }) + } + + #[cfg(target_arch = "wasm32")] + fn load_image( + _ctx: &egui::Context, + uri: &str, + cache: &Arc>>, + bytes: &Bytes, + ) -> ImageLoadResult { + let mut cache_lock = cache.lock(); + log::trace!("started loading {uri:?}"); + let result = crate::image::load_image_bytes(bytes) + .map(Arc::new) + .map_err(|err| err.to_string()); + log::trace!("finished loading {uri:?}"); + cache_lock.insert(uri.into(), std::task::Poll::Ready(result.clone())); + match result { Ok(image) => Ok(ImagePoll::Ready { image }), - Err(err) => Err(err), + Err(err) => Err(LoadError::Loading(err)), + } + } + + let entry = self.cache.lock().get(uri).cloned(); + if let Some(entry) = entry { + match entry { + Poll::Ready(Ok(image)) => Ok(ImagePoll::Ready { image }), + Poll::Ready(Err(err)) => Err(LoadError::Loading(err)), + Poll::Pending => Ok(ImagePoll::Pending { size: None }), } } else { match ctx.try_load_bytes(uri) { @@ -90,19 +151,7 @@ impl ImageLoader for ImageCrateLoader { }); } } - - if bytes.starts_with(b"version https://git-lfs") { - return Err(LoadError::FormatNotSupported { - detected_format: Some("git-lfs".to_owned()), - }); - } - - // (3) - log::trace!("started loading {uri:?}"); - let result = crate::image::load_image_bytes(&bytes).map(Arc::new); - log::trace!("finished loading {uri:?}"); - cache.insert(uri.into(), result.clone()); - result.map(|image| ImagePoll::Ready { image }) + load_image(ctx, uri, &self.cache, &bytes) } Ok(BytesPoll::Pending { size }) => Ok(ImagePoll::Pending { size }), Err(err) => Err(err), @@ -123,8 +172,9 @@ impl ImageLoader for ImageCrateLoader { .lock() .values() .map(|result| match result { - Ok(image) => image.pixels.len() * size_of::(), - Err(err) => err.byte_size(), + Poll::Ready(Ok(image)) => image.pixels.len() * size_of::(), + Poll::Ready(Err(err)) => err.len(), + Poll::Pending => 0, }) .sum() } diff --git a/deny.toml b/deny.toml index 19f353096..2f7bdbcca 100644 --- a/deny.toml +++ b/deny.toml @@ -57,7 +57,7 @@ skip = [ { name = "redox_syscall" }, # old version via winit { name = "thiserror" }, # ecosystem is in the process of migrating from 1.x to 2.x { name = "thiserror-impl" }, # same as above - { name = "time" }, # old version pulled in by unmaintianed crate 'chrono' + { name = "time" }, # old version pulled in by unmaintained crate 'chrono' { name = "windows-core" }, # Chrono pulls in 0.51, accesskit uses 0.58.0 { name = "windows-sys" }, # glutin pulls in 0.52.0, accesskit pulls in 0.59.0, rfd pulls 0.48, webbrowser pulls 0.45.0 (via jni) ] From ddf9d267fc700f274d9f19f38f185db0f9954dcc Mon Sep 17 00:00:00 2001 From: Grayden <38144548+graydenshand@users.noreply.github.com> Date: Tue, 25 Mar 2025 05:26:07 -0400 Subject: [PATCH 032/129] Fix in `Scene`: make `scene_rect` full size on reset (#5801) * [x] I have followed the instructions in the PR template # Overview This is a small change that supports draggable elements inside a `Scene`. When a Scene is initialized with a `Rect::Zero`, following the [example in the demo](https://github.com/emilk/egui/blob/master/crates/egui_demo_lib/src/demo/scene.rs#L15), it will [automatically be reset to the `inner_rect` of the UI](https://github.com/emilk/egui/blob/master/crates/egui/src/containers/scene.rs#L120-L123). This centers the scene on the inner-rect contents, however the resulting `scene_rect` doesn't fill the entire `outer_rect`. This probably isn't an issue for most users of `Scene`. However, I want to support draggable elements on a `Scene`, and to do that I need to map the pointer-position in the window to the scene_rect position. As is, the example of draggable elements on Scene works after the user has modified the scene rect in some way (zoom or pan), when `scene_rect` is set to `to_global.inverse() * outer_rect` ([here](https://github.com/emilk/egui/blob/master/crates/egui/src/containers/scene.rs#L114-L118)). Before a user modifies the scene rect, the pointer-position cannot be reliably mapped to the scene_rect, since the scene_rect doesn't span the entire window. This PR just forces that translation to always run after the scene_rect is reset to `inner_rect`. The practical result is that the scene_rect will now always span the full outer_rect. # Example Here's a small app that demonstrates the functionality I'm trying to support. I'm new to Egui so there may be better patterns for what I'm trying to do, but if you run this against `main` and this branch you'll notice the difference. ```rs use eframe::egui::*; /// Map coordinates from the src rect to the target rect fn map_to_rect(position: Pos2, src_rect: Rect, dest_rect: Rect) -> Pos2 { let x = (position.x - src_rect.min.x) / (src_rect.max.x - src_rect.min.x) * (dest_rect.max.x - dest_rect.min.x) + dest_rect.min.x; let y = (position.y - src_rect.min.y) / (src_rect.max.y - src_rect.min.y) * (dest_rect.max.y - dest_rect.min.y) + dest_rect.min.y; Pos2::new(x, y) } pub fn draggable_scene_element( ui: &mut Ui, id: Id, position: &mut Rect, scene_rect: Rect, container_rect: Rect, ) -> Response { let is_being_dragged = ui.ctx().is_being_dragged(id); if is_being_dragged { let r = ui.put(*position, |ui: &mut Ui| ui.label("Draggable")); if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() { let pointer_pos = map_to_rect(pointer_pos, container_rect, scene_rect); let delta = pointer_pos.to_vec2() - position.center().to_vec2(); *position = position.translate(delta); }; r } else { let r = ui.put(*position, |ui: &mut Ui| ui.label("Draggable")); ui .interact(position.clone(), id, Sense::drag()) .on_hover_cursor(CursorIcon::Grab); r } } struct MyApp { scene_rect: Rect, position: Rect, } impl MyApp { fn new() -> Self { Self { scene_rect: Rect::ZERO, position: Rect::from_min_size(Pos2::new(-50., -50.), Vec2::new(100., 100.)), } } } impl eframe::App for MyApp { fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) { CentralPanel::default().show(ctx, |ui| { let scene_rect = self.scene_rect.clone(); let container_rect = ui.min_rect(); Scene::default().show(ui, &mut self.scene_rect, |ui| { ui.put( Rect::from_min_size(Pos2::new(100., 200.), Vec2::new(100., 100.)), |ui: &mut Ui| ui.label("static element"), ); ui.put(self.position, |ui: &mut Ui| { draggable_scene_element( ui, Id::from("demo"), &mut self.position, scene_rect, container_rect, ) }); }); }); } } ``` # Summary I need a way to map pointer coordinates to scene coordinates, in order to support draggable elements in a scene. This patch makes that easier by ensuring the scene_rect will always be the full size of the outer_rect. If you have a better way to accomplish what I'm after, I'm happy to close this. Thanks! --- crates/egui/src/containers/scene.rs | 4 +++- crates/egui_demo_lib/tests/snapshots/demos/Scene.png | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/egui/src/containers/scene.rs b/crates/egui/src/containers/scene.rs index 6a9fc1044..fff4136f2 100644 --- a/crates/egui/src/containers/scene.rs +++ b/crates/egui/src/containers/scene.rs @@ -119,7 +119,9 @@ impl Scene { if !scene_rect_was_good { // Auto-reset if the transformation goes bad somehow (or started bad). - *scene_rect = inner_rect; + // Recalculates transform based on inner_rect, resulting in a rect that's the full size of outer_rect but centered on inner_rect. + let to_global = fit_to_rect_in_scene(outer_rect, inner_rect, self.zoom_range); + *scene_rect = to_global.inverse() * outer_rect; } ret diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scene.png b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png index 0f67cc6a2..a2c2e7bac 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:16ee44708adbe6e0ac3ce58617a5d63fb3bde357c07611815376518950e056b0 -size 34763 +oid sha256:b4bf35ad4ce01122de5bc0830018044fd70f116938293fbeb72a2278de0bbb22 +size 35068 From 8b62fd9286375e35f9dcb63d6bd8ac37ffaba520 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Tue, 25 Mar 2025 14:18:45 +0100 Subject: [PATCH 033/129] Fix Lint for debug-assert (#5846) Fixes the current ci workflow error --- crates/egui_extras/src/loaders/image_loader.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/egui_extras/src/loaders/image_loader.rs b/crates/egui_extras/src/loaders/image_loader.rs index 4790c9545..87ca4db06 100644 --- a/crates/egui_extras/src/loaders/image_loader.rs +++ b/crates/egui_extras/src/loaders/image_loader.rs @@ -103,7 +103,10 @@ impl ImageLoader for ImageCrateLoader { .map_err(|err| err.to_string()); log::trace!("ImageLoader - finished loading {uri:?}"); let prev = cache.lock().insert(uri, Poll::Ready(result)); - debug_assert!(matches!(prev, Some(Poll::Pending))); + debug_assert!( + matches!(prev, Some(Poll::Pending)), + "Expected previous state to be Pending" + ); ctx.request_repaint(); } From 5d6aaa239b98ad20d365626dc664531b08b2537d Mon Sep 17 00:00:00 2001 From: TPhoenix <45964506+TPhoenixyz@users.noreply.github.com> Date: Tue, 25 Mar 2025 14:19:23 +0100 Subject: [PATCH 034/129] Fix typo in style.rs (#5845) lien -> line * Closes * [ x ] I have followed the instructions in the PR template --- crates/egui/src/style.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index d0b368f0a..38d403749 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -989,7 +989,7 @@ pub struct Visuals { /// Show a background behind collapsing headers. pub collapsing_header_frame: bool, - /// Draw a vertical lien left of indented region, in e.g. [`crate::CollapsingHeader`]. + /// Draw a vertical line left of indented region, in e.g. [`crate::CollapsingHeader`]. pub indent_has_left_vline: bool, /// Whether or not Grids and Tables should be striped by default From 884be3491d01a1c1963c4dd63d8d788e1245ce37 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 25 Mar 2025 14:38:51 +0100 Subject: [PATCH 035/129] Fix color picker button (#5847) * related to #5832 (I want to keep that open and actually update the button to use the new popup, but this should be enough to fix it for now) * [X] I have followed the instructions in the PR template --- crates/egui/src/widgets/color_picker.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/widgets/color_picker.rs b/crates/egui/src/widgets/color_picker.rs index 500fb0b88..f8e186658 100644 --- a/crates/egui/src/widgets/color_picker.rs +++ b/crates/egui/src/widgets/color_picker.rs @@ -502,8 +502,9 @@ pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> Res const COLOR_SLIDER_WIDTH: f32 = 275.0; - // TODO(emilk): make it easier to show a temporary popup that closes when you click outside it + // TODO(lucasmerlin): Update this to use new Popup struct if ui.memory(|mem| mem.is_popup_open(popup_id)) { + ui.memory_mut(|mem| mem.keep_popup_open(popup_id)); let area_response = Area::new(popup_id) .kind(UiKind::Picker) .order(Order::Foreground) From 7ea3f762b8135db428941d3d73d9dc8f18ddd4ab Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 28 Mar 2025 20:37:38 +0100 Subject: [PATCH 036/129] Make text underline and strikethrough pixel perfect crisp (#5857) Small visual fix: pixel-align any text underline or strikethrough. Before they could be often be blurry. --- .../tests/snapshots/easymarkeditor.png | 4 +- crates/epaint/src/stroke.rs | 60 +++++++++++++++++++ crates/epaint/src/tessellator.rs | 56 +---------------- crates/epaint/src/text/text_layout.rs | 3 +- 4 files changed, 67 insertions(+), 56 deletions(-) diff --git a/crates/egui_demo_app/tests/snapshots/easymarkeditor.png b/crates/egui_demo_app/tests/snapshots/easymarkeditor.png index f88cb3791..7666b658e 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:fb4ac08fb40dd1413feee549ba977906160c82d0aba427d6d79d2e56080aa04e -size 178975 +oid sha256:3e6a383dca7e91d07df4bf501e2de13d046f04546a08d026efe3f82fc96b6e29 +size 178887 diff --git a/crates/epaint/src/stroke.rs b/crates/epaint/src/stroke.rs index 5d82c1963..50f4f678b 100644 --- a/crates/epaint/src/stroke.rs +++ b/crates/epaint/src/stroke.rs @@ -2,6 +2,8 @@ use std::{fmt::Debug, sync::Arc}; +use emath::GuiRounding as _; + use super::{emath, Color32, ColorMode, Pos2, Rect}; /// Describes the width and color of a line. @@ -34,6 +36,46 @@ impl Stroke { pub fn is_empty(&self) -> bool { self.width <= 0.0 || self.color == Color32::TRANSPARENT } + + /// For vertical or horizontal lines: + /// round the stroke center to produce a sharp, pixel-aligned line. + pub fn round_center_to_pixel(&self, pixels_per_point: f32, coord: &mut f32) { + // If the stroke is an odd number of pixels wide, + // we want to round the center of it to the center of a pixel. + // + // If however it is an even number of pixels wide, + // we want to round the center to be between two pixels. + // + // We also want to treat strokes that are _almost_ odd as it it was odd, + // to make it symmetric. Same for strokes that are _almost_ even. + // + // For strokes less than a pixel wide we also round to the center, + // because it will rendered as a single row of pixels by the tessellator. + + let pixel_size = 1.0 / pixels_per_point; + + if self.width <= pixel_size || is_nearest_integer_odd(pixels_per_point * self.width) { + *coord = coord.round_to_pixel_center(pixels_per_point); + } else { + *coord = coord.round_to_pixels(pixels_per_point); + } + } + + pub(crate) fn round_rect_to_pixel(&self, pixels_per_point: f32, rect: &mut Rect) { + // We put odd-width strokes in the center of pixels. + // To understand why, see `fn round_center_to_pixel`. + + let pixel_size = 1.0 / pixels_per_point; + + let width = self.width; + if width <= 0.0 { + *rect = rect.round_to_pixels(pixels_per_point); + } else if width <= pixel_size || is_nearest_integer_odd(pixels_per_point * width) { + *rect = rect.round_to_pixel_center(pixels_per_point); + } else { + *rect = rect.round_to_pixels(pixels_per_point); + } + } } impl From<(f32, Color)> for Stroke @@ -182,3 +224,21 @@ impl From for PathStroke { } } } + +/// Returns true if the nearest integer is odd. +fn is_nearest_integer_odd(x: f32) -> bool { + (x * 0.5 + 0.25).fract() > 0.5 +} + +#[test] +fn test_is_nearest_integer_odd() { + assert!(is_nearest_integer_odd(0.6)); + assert!(is_nearest_integer_odd(1.0)); + assert!(is_nearest_integer_odd(1.4)); + assert!(!is_nearest_integer_odd(1.6)); + assert!(!is_nearest_integer_odd(2.0)); + assert!(!is_nearest_integer_odd(2.4)); + assert!(is_nearest_integer_odd(2.6)); + assert!(is_nearest_integer_odd(3.0)); + assert!(is_nearest_integer_odd(3.4)); +} diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index bcb13a12c..914b1cc34 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1656,7 +1656,7 @@ impl Tessellator { if a.x == b.x { // Vertical line let mut x = a.x; - round_line_segment(&mut x, &stroke, self.pixels_per_point); + stroke.round_center_to_pixel(self.pixels_per_point, &mut x); a.x = x; b.x = x; @@ -1677,7 +1677,7 @@ impl Tessellator { if a.y == b.y { // Horizontal line let mut y = a.y; - round_line_segment(&mut y, &stroke, self.pixels_per_point); + stroke.round_center_to_pixel(self.pixels_per_point, &mut y); a.y = y; b.y = y; @@ -1778,7 +1778,6 @@ impl Tessellator { let mut corner_radius = CornerRadiusF32::from(corner_radius); let round_to_pixels = round_to_pixels.unwrap_or(self.options.round_rects_to_pixels); - let pixel_size = 1.0 / self.pixels_per_point; if stroke.width == 0.0 { stroke.color = Color32::TRANSPARENT; @@ -1849,17 +1848,7 @@ impl Tessellator { } StrokeKind::Middle => { // On this path we optimize for crisp and symmetric strokes. - // We put odd-width strokes in the center of pixels. - // To understand why, see `fn round_line_segment`. - if stroke.width <= 0.0 { - rect = rect.round_to_pixels(self.pixels_per_point); - } else if stroke.width <= pixel_size - || is_nearest_integer_odd(self.pixels_per_point * stroke.width) - { - rect = rect.round_to_pixel_center(self.pixels_per_point); - } else { - rect = rect.round_to_pixels(self.pixels_per_point); - } + stroke.round_rect_to_pixel(self.pixels_per_point, &mut rect); } StrokeKind::Outside => { // Put the inside of the stroke on a pixel boundary. @@ -2203,45 +2192,6 @@ impl Tessellator { } } -fn round_line_segment(coord: &mut f32, stroke: &Stroke, pixels_per_point: f32) { - // If the stroke is an odd number of pixels wide, - // we want to round the center of it to the center of a pixel. - // - // If however it is an even number of pixels wide, - // we want to round the center to be between two pixels. - // - // We also want to treat strokes that are _almost_ odd as it it was odd, - // to make it symmetric. Same for strokes that are _almost_ even. - // - // For strokes less than a pixel wide we also round to the center, - // because it will rendered as a single row of pixels by the tessellator. - - let pixel_size = 1.0 / pixels_per_point; - - if stroke.width <= pixel_size || is_nearest_integer_odd(pixels_per_point * stroke.width) { - *coord = coord.round_to_pixel_center(pixels_per_point); - } else { - *coord = coord.round_to_pixels(pixels_per_point); - } -} - -fn is_nearest_integer_odd(width: f32) -> bool { - (width * 0.5 + 0.25).fract() > 0.5 -} - -#[test] -fn test_is_nearest_integer_odd() { - assert!(is_nearest_integer_odd(0.6)); - assert!(is_nearest_integer_odd(1.0)); - assert!(is_nearest_integer_odd(1.4)); - assert!(!is_nearest_integer_odd(1.6)); - assert!(!is_nearest_integer_odd(2.0)); - assert!(!is_nearest_integer_odd(2.4)); - assert!(is_nearest_integer_odd(2.6)); - assert!(is_nearest_integer_odd(3.0)); - assert!(is_nearest_integer_odd(3.4)); -} - #[deprecated = "Use `Tessellator::new(…).tessellate_shapes(…)` instead"] pub fn tessellate_shapes( pixels_per_point: f32, diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index 638b7a705..eb1adf5fb 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -866,7 +866,8 @@ fn add_row_hline( let mut last_right_x = f32::NAN; for glyph in &row.glyphs { - let (stroke, y) = stroke_and_y(glyph); + let (stroke, mut y) = stroke_and_y(glyph); + stroke.round_center_to_pixel(point_scale.pixels_per_point, &mut y); if stroke == Stroke::NONE { end_line(line_start.take(), last_right_x); From 83254718a335b358f062c7401bdd4ae0faae814b Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 30 Mar 2025 13:15:41 +0200 Subject: [PATCH 037/129] Clean up strikethrough/underline code in epaint --- crates/epaint/src/text/text_layout.rs | 36 ++++++--------------------- 1 file changed, 7 insertions(+), 29 deletions(-) diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index eb1adf5fb..7ce726f79 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -856,9 +856,15 @@ fn add_row_hline( mesh: &mut Mesh, stroke_and_y: impl Fn(&Glyph) -> (Stroke, f32), ) { + let mut path = crate::tessellator::Path::default(); // reusing path to avoid re-allocations. + let mut end_line = |start: Option<(Stroke, Pos2)>, stop_x: f32| { if let Some((stroke, start)) = start { - add_hline(point_scale, [start, pos2(stop_x, start.y)], stroke, mesh); + let stop = pos2(stop_x, start.y); + path.clear(); + path.add_line_segment([start, stop]); + let feathering = 1.0 / point_scale.pixels_per_point(); + path.stroke_open(feathering, &PathStroke::from(stroke), mesh); } }; @@ -888,34 +894,6 @@ fn add_row_hline( end_line(line_start.take(), last_right_x); } -fn add_hline(point_scale: PointScale, [start, stop]: [Pos2; 2], stroke: Stroke, mesh: &mut Mesh) { - let antialiased = true; - - if antialiased { - let mut path = crate::tessellator::Path::default(); // TODO(emilk): reuse this to avoid re-allocations. - path.add_line_segment([start, stop]); - let feathering = 1.0 / point_scale.pixels_per_point(); - path.stroke_open(feathering, &PathStroke::from(stroke), mesh); - } else { - // Thin lines often lost, so this is a bad idea - - assert_eq!( - start.y, stop.y, - "Horizontal line must be horizontal, but got: {start:?} -> {stop:?}" - ); - - let min_y = point_scale.round_to_pixel(start.y - 0.5 * stroke.width); - let max_y = point_scale.round_to_pixel(min_y + stroke.width); - - let rect = Rect::from_min_max( - pos2(point_scale.round_to_pixel(start.x), min_y), - pos2(point_scale.round_to_pixel(stop.x), max_y), - ); - - mesh.add_colored_rect(rect, stroke.color); - } -} - // ---------------------------------------------------------------------------- /// Keeps track of good places to break a long row of text. From ab0f0b7b64fa85e8186d6596df6df3253a210b46 Mon Sep 17 00:00:00 2001 From: Timo von Hartz Date: Sun, 30 Mar 2025 14:00:46 +0200 Subject: [PATCH 038/129] Rename `should_propagate_event` & add `should_prevent_default` (#5779) * [x] I have followed the instructions in the PR template Currently eframe [calls `prevent_default()`](https://github.com/emilk/egui/blob/962c7c75166dff3369d20675bcfd527d3287149f/crates/eframe/src/web/events.rs#L307-L369) for all copy / paste events on the [*document*](https://github.com/emilk/egui/blob/962c7c75166dff3369d20675bcfd527d3287149f/crates/eframe/src/web/events.rs#L88), making embedding an egui application in a page (e.g. an react application) hard (as all copy & paste functionality for other elements on the page is broken by this). I'm not sure what the motivation for this is, if any. This commit / PR adds a callback (`should_prevent_default`), similar to `should_propgate_event`, that an egui application can use to overwrite this behavior. It defaults to returning `true` for all events, to keep the existing behavior. I call `should_prevent_default` in every place that `should_propagate_event` is called (which is not all places that `prevent_default` is called!). I'm not sure for the motivation of not calling `should_propagate_event` everywhere that `stop_propagation` is called, but I kept that behavior for the `should_prevent_default` callback too. Please let me know if I'm missing some existing functionality that would allow me to do this, or if there's a reason that we don't want applications to be able to customize this (i.e. if there's a reason to always `prevent_default` for all copy / paste events on the whole document) --- crates/eframe/src/epi.rs | 15 +++- crates/eframe/src/web/events.rs | 141 ++++++++++++++++++++++---------- 2 files changed, 108 insertions(+), 48 deletions(-) diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index fb589fe2e..562311dc6 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -499,10 +499,16 @@ pub struct WebOptions { /// If the web event corresponding to an egui event should be propagated /// to the rest of the web page. /// - /// The default is `false`, meaning + /// The default is `true`, meaning /// [`stopPropagation`](https://developer.mozilla.org/en-US/docs/Web/API/Event/stopPropagation) - /// is called on every event. - pub should_propagate_event: Box bool>, + /// is called on every event, and the event is not propagated to the rest of the web page. + pub should_stop_propagation: Box bool>, + + /// Whether the web event corresponding to an egui event should have `prevent_default` called + /// on it or not. + /// + /// Defaults to true. + pub should_prevent_default: Box bool>, } #[cfg(target_arch = "wasm32")] @@ -519,7 +525,8 @@ impl Default for WebOptions { dithering: true, - should_propagate_event: Box::new(|_| false), + should_stop_propagation: Box::new(|_| true), + should_prevent_default: Box::new(|_| true), } } } diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index 6a1b7b6db..32fba9ef0 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -139,15 +139,20 @@ fn install_keydown(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), J { if let Some(text) = text_from_keyboard_event(&event) { let egui_event = egui::Event::Text(text); - let should_propagate = (runner.web_options.should_propagate_event)(&egui_event); + let should_stop_propagation = + (runner.web_options.should_stop_propagation)(&egui_event); + let should_prevent_default = + (runner.web_options.should_prevent_default)(&egui_event); runner.input.raw.events.push(egui_event); runner.needs_repaint.repaint_asap(); // If this is indeed text, then prevent any other action. - event.prevent_default(); + if should_prevent_default { + event.prevent_default(); + } // Use web options to tell if the event should be propagated to parent elements. - if !should_propagate { + if should_stop_propagation { event.stop_propagation(); } } @@ -184,7 +189,7 @@ pub(crate) fn on_keydown(event: web_sys::KeyboardEvent, runner: &mut AppRunner) repeat: false, // egui will fill this in for us! modifiers, }; - let should_propagate = (runner.web_options.should_propagate_event)(&egui_event); + let should_stop_propagation = (runner.web_options.should_stop_propagation)(&egui_event); runner.input.raw.events.push(egui_event); runner.needs_repaint.repaint_asap(); @@ -201,7 +206,7 @@ pub(crate) fn on_keydown(event: web_sys::KeyboardEvent, runner: &mut AppRunner) } // Use web options to tell if the web event should be propagated to parent elements based on the egui event. - if !should_propagate { + if should_stop_propagation { event.stop_propagation(); } } @@ -261,7 +266,7 @@ pub(crate) fn on_keyup(event: web_sys::KeyboardEvent, runner: &mut AppRunner) { let modifiers = modifiers_from_kb_event(&event); runner.input.raw.modifiers = modifiers; - let mut propagate_event = false; + let mut should_stop_propagation = true; if let Some(key) = translate_key(&event.key()) { let egui_event = egui::Event::Key { @@ -271,7 +276,7 @@ pub(crate) fn on_keyup(event: web_sys::KeyboardEvent, runner: &mut AppRunner) { repeat: false, modifiers, }; - propagate_event |= (runner.web_options.should_propagate_event)(&egui_event); + should_stop_propagation &= (runner.web_options.should_stop_propagation)(&egui_event); runner.input.raw.events.push(egui_event); } @@ -290,7 +295,7 @@ pub(crate) fn on_keyup(event: web_sys::KeyboardEvent, runner: &mut AppRunner) { repeat: false, modifiers, }; - propagate_event |= (runner.web_options.should_propagate_event)(&egui_event); + should_stop_propagation &= (runner.web_options.should_stop_propagation)(&egui_event); runner.input.raw.events.push(egui_event); } } @@ -299,7 +304,7 @@ pub(crate) fn on_keyup(event: web_sys::KeyboardEvent, runner: &mut AppRunner) { // Use web options to tell if the web event should be propagated to parent elements based on the egui event. let has_focus = runner.input.raw.focused; - if has_focus && !propagate_event { + if has_focus && should_stop_propagation { event.stop_propagation(); } } @@ -310,19 +315,26 @@ fn install_copy_cut_paste(runner_ref: &WebRunner, target: &EventTarget) -> Resul if let Ok(text) = data.get_data("text") { let text = text.replace("\r\n", "\n"); - let mut should_propagate = false; + let mut should_stop_propagation = true; + let mut should_prevent_default = true; if !text.is_empty() && runner.input.raw.focused { let egui_event = egui::Event::Paste(text); - should_propagate = (runner.web_options.should_propagate_event)(&egui_event); + should_stop_propagation = + (runner.web_options.should_stop_propagation)(&egui_event); + should_prevent_default = + (runner.web_options.should_prevent_default)(&egui_event); runner.input.raw.events.push(egui_event); runner.needs_repaint.repaint_asap(); } // Use web options to tell if the web event should be propagated to parent elements based on the egui event. - if !should_propagate { + if should_stop_propagation { event.stop_propagation(); } - event.prevent_default(); + + if should_prevent_default { + event.prevent_default(); + } } } })?; @@ -340,10 +352,13 @@ fn install_copy_cut_paste(runner_ref: &WebRunner, target: &EventTarget) -> Resul } // Use web options to tell if the web event should be propagated to parent elements based on the egui event. - if !(runner.web_options.should_propagate_event)(&egui::Event::Cut) { + if (runner.web_options.should_stop_propagation)(&egui::Event::Cut) { event.stop_propagation(); } - event.prevent_default(); + + if (runner.web_options.should_prevent_default)(&egui::Event::Cut) { + event.prevent_default(); + } })?; runner_ref.add_event_listener(target, "copy", |event: web_sys::ClipboardEvent, runner| { @@ -359,10 +374,13 @@ fn install_copy_cut_paste(runner_ref: &WebRunner, target: &EventTarget) -> Resul } // Use web options to tell if the web event should be propagated to parent elements based on the egui event. - if !(runner.web_options.should_propagate_event)(&egui::Event::Copy) { + if (runner.web_options.should_stop_propagation)(&egui::Event::Copy) { event.stop_propagation(); } - event.prevent_default(); + + if (runner.web_options.should_prevent_default)(&egui::Event::Copy) { + event.prevent_default(); + } })?; Ok(()) @@ -484,7 +502,7 @@ fn install_pointerdown(runner_ref: &WebRunner, target: &EventTarget) -> Result<( |event: web_sys::PointerEvent, runner: &mut AppRunner| { let modifiers = modifiers_from_mouse_event(&event); runner.input.raw.modifiers = modifiers; - let mut should_propagate = false; + let mut should_stop_propagation = true; if let Some(button) = button_from_mouse_event(&event) { let pos = pos_from_mouse_event(runner.canvas(), &event, runner.egui_ctx()); let modifiers = runner.input.raw.modifiers; @@ -494,7 +512,7 @@ fn install_pointerdown(runner_ref: &WebRunner, target: &EventTarget) -> Result<( pressed: true, modifiers, }; - should_propagate = (runner.web_options.should_propagate_event)(&egui_event); + should_stop_propagation = (runner.web_options.should_stop_propagation)(&egui_event); runner.input.raw.events.push(egui_event); // In Safari we are only allowed to write to the clipboard during the @@ -506,7 +524,7 @@ fn install_pointerdown(runner_ref: &WebRunner, target: &EventTarget) -> Result<( } // Use web options to tell if the web event should be propagated to parent elements based on the egui event. - if !should_propagate { + if should_stop_propagation { event.stop_propagation(); } // Note: prevent_default breaks VSCode tab focusing, hence why we don't call it here. @@ -536,7 +554,10 @@ fn install_pointerup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), pressed: false, modifiers, }; - let should_propagate = (runner.web_options.should_propagate_event)(&egui_event); + let should_stop_propagation = + (runner.web_options.should_stop_propagation)(&egui_event); + let should_prevent_default = + (runner.web_options.should_prevent_default)(&egui_event); runner.input.raw.events.push(egui_event); // Previously on iOS, the canvas would not receive focus on @@ -555,10 +576,12 @@ fn install_pointerup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), // Make sure we paint the output of the above logic call asap: runner.needs_repaint.repaint_asap(); - event.prevent_default(); + if should_prevent_default { + event.prevent_default(); + } // Use web options to tell if the web event should be propagated to parent elements based on the egui event. - if !should_propagate { + if should_stop_propagation { event.stop_propagation(); } } @@ -600,15 +623,19 @@ fn install_mousemove(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), egui::pos2(event.client_x() as f32, event.client_y() as f32), ) { let egui_event = egui::Event::PointerMoved(pos); - let should_propagate = (runner.web_options.should_propagate_event)(&egui_event); + let should_stop_propagation = (runner.web_options.should_stop_propagation)(&egui_event); + let should_prevent_default = (runner.web_options.should_prevent_default)(&egui_event); runner.input.raw.events.push(egui_event); runner.needs_repaint.repaint_asap(); // Use web options to tell if the web event should be propagated to parent elements based on the egui event. - if !should_propagate { + if should_stop_propagation { event.stop_propagation(); } - event.prevent_default(); + + if should_prevent_default { + event.prevent_default(); + } } }) } @@ -622,10 +649,13 @@ fn install_mouseleave(runner_ref: &WebRunner, target: &EventTarget) -> Result<() runner.needs_repaint.repaint_asap(); // Use web options to tell if the web event should be propagated to parent elements based on the egui event. - if !(runner.web_options.should_propagate_event)(&egui::Event::PointerGone) { + if (runner.web_options.should_stop_propagation)(&egui::Event::PointerGone) { event.stop_propagation(); } - event.prevent_default(); + + if (runner.web_options.should_prevent_default)(&egui::Event::PointerGone) { + event.prevent_default(); + } }, ) } @@ -635,7 +665,8 @@ fn install_touchstart(runner_ref: &WebRunner, target: &EventTarget) -> Result<() target, "touchstart", |event: web_sys::TouchEvent, runner| { - let mut should_propagate = false; + let mut should_stop_propagation = true; + let mut should_prevent_default = true; if let Some((pos, _)) = primary_touch_pos(runner, &event) { let egui_event = egui::Event::PointerButton { pos, @@ -643,7 +674,8 @@ fn install_touchstart(runner_ref: &WebRunner, target: &EventTarget) -> Result<() pressed: true, modifiers: runner.input.raw.modifiers, }; - should_propagate = (runner.web_options.should_propagate_event)(&egui_event); + should_stop_propagation = (runner.web_options.should_stop_propagation)(&egui_event); + should_prevent_default = (runner.web_options.should_prevent_default)(&egui_event); runner.input.raw.events.push(egui_event); } @@ -651,10 +683,13 @@ fn install_touchstart(runner_ref: &WebRunner, target: &EventTarget) -> Result<() runner.needs_repaint.repaint_asap(); // Use web options to tell if the web event should be propagated to parent elements based on the egui event. - if !should_propagate { + if should_stop_propagation { event.stop_propagation(); } - event.prevent_default(); + + if should_prevent_default { + event.prevent_default(); + } }, ) } @@ -667,17 +702,23 @@ fn install_touchmove(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), egui::pos2(touch.client_x() as f32, touch.client_y() as f32), ) { let egui_event = egui::Event::PointerMoved(pos); - let should_propagate = (runner.web_options.should_propagate_event)(&egui_event); + let should_stop_propagation = + (runner.web_options.should_stop_propagation)(&egui_event); + let should_prevent_default = + (runner.web_options.should_prevent_default)(&egui_event); runner.input.raw.events.push(egui_event); push_touches(runner, egui::TouchPhase::Move, &event); runner.needs_repaint.repaint_asap(); // Use web options to tell if the web event should be propagated to parent elements based on the egui event. - if !should_propagate { + if should_stop_propagation { event.stop_propagation(); } - event.prevent_default(); + + if should_prevent_default { + event.prevent_default(); + } } } }) @@ -691,18 +732,23 @@ fn install_touchend(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), egui::pos2(touch.client_x() as f32, touch.client_y() as f32), ) { // First release mouse to click: - let mut should_propagate = false; + let mut should_stop_propagation = true; + let mut should_prevent_default = true; let egui_event = egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: false, modifiers: runner.input.raw.modifiers, }; - should_propagate |= (runner.web_options.should_propagate_event)(&egui_event); + should_stop_propagation &= + (runner.web_options.should_stop_propagation)(&egui_event); + should_prevent_default &= (runner.web_options.should_prevent_default)(&egui_event); runner.input.raw.events.push(egui_event); // Then remove hover effect: - should_propagate |= - (runner.web_options.should_propagate_event)(&egui::Event::PointerGone); + should_stop_propagation &= + (runner.web_options.should_stop_propagation)(&egui::Event::PointerGone); + should_prevent_default &= + (runner.web_options.should_prevent_default)(&egui::Event::PointerGone); runner.input.raw.events.push(egui::Event::PointerGone); push_touches(runner, egui::TouchPhase::End, &event); @@ -710,10 +756,13 @@ fn install_touchend(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), runner.needs_repaint.repaint_asap(); // Use web options to tell if the web event should be propagated to parent elements based on the egui event. - if !should_propagate { + if should_stop_propagation { event.stop_propagation(); } - event.prevent_default(); + + if should_prevent_default { + event.prevent_default(); + } // Fix virtual keyboard IOS // Need call focus at the same time of event @@ -769,16 +818,20 @@ fn install_wheel(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsV modifiers, } }; - let should_propagate = (runner.web_options.should_propagate_event)(&egui_event); + let should_stop_propagation = (runner.web_options.should_stop_propagation)(&egui_event); + let should_prevent_default = (runner.web_options.should_prevent_default)(&egui_event); runner.input.raw.events.push(egui_event); runner.needs_repaint.repaint_asap(); // Use web options to tell if the web event should be propagated to parent elements based on the egui event. - if !should_propagate { + if should_stop_propagation { event.stop_propagation(); } - event.prevent_default(); + + if should_prevent_default { + event.prevent_default(); + } }) } From 943e3618fcdf7f96d3f5f163ad741d96dbe7dbbf Mon Sep 17 00:00:00 2001 From: Hank Jordan Date: Sun, 30 Mar 2025 08:03:19 -0400 Subject: [PATCH 039/129] Improve drag-to-select text (add margins) (#5797) Might want to draw from `interaction.interact_radius` style instead of hard-coding the margin, but I didn't want to create a breaking change. If desired, I can follow up with a separate PR to address that concern. * Closes * [x] I have followed the instructions in the PR template --- crates/epaint/src/text/text_layout_types.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 6d69045ab..b6d7ccf49 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -796,13 +796,16 @@ impl Galley { /// same as a cursor at the end. /// This allows implementing text-selection by dragging above/below the galley. pub fn cursor_from_pos(&self, pos: Vec2) -> CCursor { + // Vertical margin around galley improves text selection UX + const VMARGIN: f32 = 5.0; + if let Some(first_row) = self.rows.first() { - if pos.y < first_row.min_y() { + if pos.y < first_row.min_y() - VMARGIN { return self.begin(); } } if let Some(last_row) = self.rows.last() { - if last_row.max_y() < pos.y { + if last_row.max_y() + VMARGIN < pos.y { return self.end(); } } From 995058bbd10d8c877d49d18b2f78fc65066cb960 Mon Sep 17 00:00:00 2001 From: Alexander Nadeau Date: Sun, 30 Mar 2025 08:04:07 -0400 Subject: [PATCH 040/129] Update web-sys min version to 0.3.73 (#5862) This should prevent compilation errors (which I ran into) where eframe tries to use HtmlElement::set_autofocus(), which doesn't exist until 0.3.73. ``` error[E0599]: no method named `set_autofocus` found for struct `HtmlElement` in the current scope --> C:\Users\wareya\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\eframe-0.31.1\src\web\text_agent.rs:24:15 | 24 | input.set_autofocus(true)?; | ^^^^^^^^^^^^^ | help: there is a method `set_onfocus` with a similar name | 24 | input.set_onfocus(true)?; | ~~~~~~~~~~~ ``` * [x] I have followed the instructions in the PR template --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 92f1ce3c5..cb750bfda 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -100,7 +100,7 @@ thiserror = "1.0.37" type-map = "0.5.0" wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" -web-sys = "0.3.70" +web-sys = "0.3.73" web-time = "1.1.0" # Timekeeping for native and web wgpu = { version = "24.0.0", default-features = false } windows-sys = "0.59" From e3acd710904ecd957f9d88e3593fd7efee5768c8 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 30 Mar 2025 16:21:00 +0200 Subject: [PATCH 041/129] Make text background rects pixel-sharp (#5864) Small visual teak: make sure the background text color is pixel-aligned. --- .../tests/snapshots/rendering_test/dpi_1.50.png | 2 +- crates/egui_extras/src/syntax_highlighting.rs | 1 + crates/epaint/src/text/text_layout.rs | 5 +++-- 3 files changed, 5 insertions(+), 3 deletions(-) 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 132864c85..9e1b69fee 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:e6f394c2beb51d95edaf8c7ddc9ff62d3f95913ea88a3840245b6bacf8b850cc +oid sha256:334f52bfee27f9c467de739696fd7ce7c48ec9013e315dc4b2e61eee58f11287 size 907997 diff --git a/crates/egui_extras/src/syntax_highlighting.rs b/crates/egui_extras/src/syntax_highlighting.rs index 77ad0cc2d..ac51c673c 100644 --- a/crates/egui_extras/src/syntax_highlighting.rs +++ b/crates/egui_extras/src/syntax_highlighting.rs @@ -33,6 +33,7 @@ pub fn highlight( // performing it at a separate thread (ctx, ctx.style()) can be used and when ui is available // (ui.ctx(), ui.style()) can be used + #[allow(non_local_definitions)] impl egui::cache::ComputerMut<(&egui::FontId, &CodeTheme, &str, &str), LayoutJob> for Highlighter { fn compute( &mut self, diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index 7ce726f79..d1b2d5038 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -715,7 +715,7 @@ fn tessellate_row( mesh.reserve_vertices(row.glyphs.len() * 4); if format_summary.any_background { - add_row_backgrounds(job, row, &mut mesh); + add_row_backgrounds(point_scale, job, row, &mut mesh); } let glyph_index_start = mesh.indices.len(); @@ -753,7 +753,7 @@ fn tessellate_row( /// Create background for glyphs that have them. /// Creates as few rectangular regions as possible. -fn add_row_backgrounds(job: &LayoutJob, row: &Row, mesh: &mut Mesh) { +fn add_row_backgrounds(point_scale: PointScale, job: &LayoutJob, row: &Row, mesh: &mut Mesh) { if row.glyphs.is_empty() { return; } @@ -762,6 +762,7 @@ fn add_row_backgrounds(job: &LayoutJob, row: &Row, mesh: &mut Mesh) { if let Some((color, start_rect, expand)) = start { let rect = Rect::from_min_max(start_rect.left_top(), pos2(stop_x, start_rect.bottom())); let rect = rect.expand(expand); + let rect = rect.round_to_pixels(point_scale.pixels_per_point()); mesh.add_colored_rect(rect, color); } }; From e275409eb1840efb560ad69960a077c2f4405521 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 30 Mar 2025 16:36:03 +0200 Subject: [PATCH 042/129] Fix: transform `TextShape` underline width (#5865) Minor bug fix when transforming a `TextShape` with a `underline` (used for e.g. hyperlinks). Before the underline width would not scale properly; now it will. --- crates/epaint/src/shapes/shape.rs | 32 +++++++++++++++++++++----- crates/epaint/src/shapes/text_shape.rs | 4 +++- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/crates/epaint/src/shapes/shape.rs b/crates/epaint/src/shapes/shape.rs index d17f528c8..7b38bda6d 100644 --- a/crates/epaint/src/shapes/shape.rs +++ b/crates/epaint/src/shapes/shape.rs @@ -456,19 +456,39 @@ impl Shape { rect_shape.blur_width *= transform.scaling; } Self::Text(text_shape) => { - text_shape.pos = transform * text_shape.pos; + let TextShape { + pos, + galley, + underline, + fallback_color: _, + override_text_color: _, + opacity_factor: _, + angle: _, + } = text_shape; - // Scale text: - let galley = Arc::make_mut(&mut text_shape.galley); - for row in &mut galley.rows { + *pos = transform * *pos; + underline.width *= transform.scaling; + + let Galley { + job: _, + rows, + elided: _, + rect, + mesh_bounds, + num_vertices: _, + num_indices: _, + pixels_per_point: _, + } = Arc::make_mut(galley); + + for row in rows { row.visuals.mesh_bounds = transform.scaling * row.visuals.mesh_bounds; for v in &mut row.visuals.mesh.vertices { v.pos = Pos2::new(transform.scaling * v.pos.x, transform.scaling * v.pos.y); } } - galley.mesh_bounds = transform.scaling * galley.mesh_bounds; - galley.rect = transform.scaling * galley.rect; + *mesh_bounds = transform.scaling * *mesh_bounds; + *rect = transform.scaling * *rect; } Self::Mesh(mesh) => { Arc::make_mut(mesh).transform(transform); diff --git a/crates/epaint/src/shapes/text_shape.rs b/crates/epaint/src/shapes/text_shape.rs index 30287187f..ef549bd9c 100644 --- a/crates/epaint/src/shapes/text_shape.rs +++ b/crates/epaint/src/shapes/text_shape.rs @@ -8,7 +8,9 @@ use crate::*; #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct TextShape { - /// Top left corner of the first character. + /// Where the origin of [`Self::galley`] is. + /// + /// Usually the top left corner of the first character. pub pos: Pos2, /// The laid out text, from [`Fonts::layout_job`]. From 557bd56e1962266e765cc7b7958c1fd3f14fa3e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20G=C5=82uchowski?= Date: Tue, 1 Apr 2025 18:55:39 +0200 Subject: [PATCH 043/129] Optimize editing long text by caching each paragraph (#5411) ## What (written by @emilk) When editing long text (thousands of line), egui would previously re-layout the entire text on each edit. This could be slow. With this PR, we instead split the text into paragraphs (split on `\n`) and then cache each such paragraph. When editing text then, only the changed paragraph needs to be laid out again. Still, there is overhead from splitting the text, hashing each paragraph, and then joining the results, so the runtime complexity is still O(N). In our benchmark, editing a 2000 line string goes from ~8ms to ~300 ms, a speedup of ~25x. In the future, we could also consider laying out each paragraph in parallel, to speed up the initial layout of the text. ## Details This is an ~~almost complete~~ implementation of the approach described by emilk [in this comment](), excluding CoW semantics for `LayoutJob` (but including them for `Row`). It supersedes the previous unsuccessful attempt here: https://github.com/emilk/egui/pull/4000. Draft because: - [X] ~~Currently individual rows will have `ends_with_newline` always set to false. This breaks selection with Ctrl+A (and probably many other things)~~ - [X] ~~The whole block for doing the splitting and merging should probably become a function (I'll do that later).~~ - [X] ~~I haven't run the check script, the tests, and haven't made sure all of the examples build (although I assume they probably don't rely on Galley internals).~~ - [x] ~~Layout is sometimes incorrect (missing empty lines, wrapping sometimes makes text overlap).~~ - A lot of text-related code had to be changed so this needs to be properly tested to ensure no layout issues were introduced, especially relating to the now row-relative coordinate system of `Row`s. Also this requires that we're fine making these very breaking changes. It does significantly improve the performance of rendering large blocks of text (if they have many newlines), this is the test program I used to test it (adapted from ):
code ```rust use eframe::egui::{self, CentralPanel, TextEdit}; use std::fmt::Write; fn main() -> Result<(), eframe::Error> { let options = eframe::NativeOptions { ..Default::default() }; eframe::run_native( "editor big file test", options, Box::new(|_cc| Ok(Box::::new(MyApp::new()))), ) } struct MyApp { text: String, } impl MyApp { fn new() -> Self { let mut string = String::new(); for line_bytes in (0..50000).map(|_| (0u8..50)) { for byte in line_bytes { write!(string, " {byte:02x}").unwrap(); } write!(string, "\n").unwrap(); } println!("total bytes: {}", string.len()); MyApp { text: string } } } impl eframe::App for MyApp { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { CentralPanel::default().show(ctx, |ui| { let start = std::time::Instant::now(); egui::ScrollArea::vertical().show(ui, |ui| { let code_editor = TextEdit::multiline(&mut self.text) .code_editor() .desired_width(f32::INFINITY) .desired_rows(40); let response = code_editor.show(ui).response; if response.changed() { println!("total bytes now: {}", self.text.len()); } }); let end = std::time::Instant::now(); let time_to_update = end - start; if time_to_update.as_secs_f32() > 0.5 { println!("Long update took {:.3}s", time_to_update.as_secs_f32()) } }); } } ```
I think the way to proceed would be to make a new type, something like `PositionedRow`, that would wrap an `Arc` but have a separate `pos` ~~and `ends_with_newline`~~ (that would mean `Row` only holds a `size` instead of a `rect`). This type would of course have getters that would allow you to easily get a `Rect` from it and probably a `Deref` to the underlying `Row`. ~~I haven't done this yet because I wanted to get some opinions whether this would be an acceptable API first.~~ This is now implemented, but of course I'm still open to discussion about this approach and whether it's what we want to do. Breaking changes (currently): - The `Galley::rows` field has a different type. - There is now a `PlacedRow` wrapper for `Row`. - `Row` now uses a coordinate system relative to itself instead of the `Galley`. * Closes * [X] I have followed the instructions in the PR template --------- Co-authored-by: Emil Ernerfeldt --- Cargo.lock | 53 ++- Cargo.toml | 1 + .../egui/src/text_selection/accesskit_text.rs | 6 +- .../text_selection/label_text_selection.rs | 9 +- crates/egui/src/text_selection/visuals.rs | 9 +- crates/egui/src/widget_text.rs | 4 +- crates/egui/src/widgets/label.rs | 10 +- crates/egui_demo_lib/Cargo.toml | 1 + crates/egui_demo_lib/benches/benchmark.rs | 27 ++ crates/emath/src/pos2.rs | 14 +- crates/emath/src/rect.rs | 6 +- crates/epaint/Cargo.toml | 1 + crates/epaint/src/shape_transform.rs | 3 +- crates/epaint/src/shapes/shape.rs | 34 +- crates/epaint/src/shapes/text_shape.rs | 57 ++++ crates/epaint/src/stats.rs | 2 +- crates/epaint/src/tessellator.rs | 6 +- crates/epaint/src/text/fonts.rs | 323 +++++++++++++++++- crates/epaint/src/text/mod.rs | 2 +- crates/epaint/src/text/text_layout.rs | 194 ++++++----- crates/epaint/src/text/text_layout_types.rs | 189 ++++++++-- 21 files changed, 754 insertions(+), 197 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3a04d32f1..4276896ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -642,6 +642,17 @@ dependencies = [ "piper", ] +[[package]] +name = "bstr" +version = "1.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -896,6 +907,18 @@ dependencies = [ "env_logger", ] +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "windows-sys 0.59.0", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1331,6 +1354,7 @@ dependencies = [ "egui", "egui_extras", "egui_kittest", + "rand", "serde", "unicode_names2", ] @@ -1420,6 +1444,12 @@ dependencies = [ "serde", ] +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "endi" version = "1.1.0" @@ -1520,6 +1550,7 @@ dependencies = [ "profiling", "rayon", "serde", + "similar-asserts", ] [[package]] @@ -2389,7 +2420,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -3669,6 +3700,26 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +dependencies = [ + "bstr", + "unicode-segmentation", +] + +[[package]] +name = "similar-asserts" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b441962c817e33508847a22bd82f03a30cff43642dc2fae8b050566121eb9a" +dependencies = [ + "console", + "similar", +] + [[package]] name = "simplecss" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index cb750bfda..a0051513d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -96,6 +96,7 @@ puffin_http = "0.16" raw-window-handle = "0.6.0" ron = "0.8" serde = { version = "1", features = ["derive"] } +similar-asserts = "1.4.2" thiserror = "1.0.37" type-map = "0.5.0" wasm-bindgen = "0.2" diff --git a/crates/egui/src/text_selection/accesskit_text.rs b/crates/egui/src/text_selection/accesskit_text.rs index d189498f6..de193e3b0 100644 --- a/crates/egui/src/text_selection/accesskit_text.rs +++ b/crates/egui/src/text_selection/accesskit_text.rs @@ -45,7 +45,7 @@ pub fn update_accesskit_for_text_widget( let row_id = parent_id.with(row_index); ctx.accesskit_node_builder(row_id, |builder| { builder.set_role(accesskit::Role::TextRun); - let rect = global_from_galley * row.rect; + let rect = global_from_galley * row.rect(); builder.set_bounds(accesskit::Rect { x0: rect.min.x.into(), y0: rect.min.y.into(), @@ -76,14 +76,14 @@ pub fn update_accesskit_for_text_widget( let old_len = value.len(); value.push(glyph.chr); character_lengths.push((value.len() - old_len) as _); - character_positions.push(glyph.pos.x - row.rect.min.x); + character_positions.push(glyph.pos.x - row.pos.x); character_widths.push(glyph.advance_width); } if row.ends_with_newline { value.push('\n'); character_lengths.push(1); - character_positions.push(row.rect.max.x - row.rect.min.x); + character_positions.push(row.size.x); character_widths.push(0.0); } word_lengths.push((character_lengths.len() - last_word_start) as _); diff --git a/crates/egui/src/text_selection/label_text_selection.rs b/crates/egui/src/text_selection/label_text_selection.rs index acd3db7d3..e24992a05 100644 --- a/crates/egui/src/text_selection/label_text_selection.rs +++ b/crates/egui/src/text_selection/label_text_selection.rs @@ -186,7 +186,10 @@ impl LabelSelectionState { if let epaint::Shape::Text(text_shape) = &mut shape.shape { let galley = Arc::make_mut(&mut text_shape.galley); for row_selection in row_selections { - if let Some(row) = galley.rows.get_mut(row_selection.row) { + if let Some(placed_row) = + galley.rows.get_mut(row_selection.row) + { + let row = Arc::make_mut(&mut placed_row.row); for vertex_index in row_selection.vertex_indices { if let Some(vertex) = row .visuals @@ -701,8 +704,8 @@ fn selected_text(galley: &Galley, cursor_range: &CCursorRange) -> String { } fn estimate_row_height(galley: &Galley) -> f32 { - if let Some(row) = galley.rows.first() { - row.rect.height() + if let Some(placed_row) = galley.rows.first() { + placed_row.height() } else { galley.size().y } diff --git a/crates/egui/src/text_selection/visuals.rs b/crates/egui/src/text_selection/visuals.rs index 32a040a89..deee5690b 100644 --- a/crates/egui/src/text_selection/visuals.rs +++ b/crates/egui/src/text_selection/visuals.rs @@ -31,11 +31,12 @@ pub fn paint_text_selection( let max = galley.layout_from_cursor(max); for ri in min.row..=max.row { - let row = &mut galley.rows[ri]; + let row = Arc::make_mut(&mut galley.rows[ri].row); + let left = if ri == min.row { row.x_offset(min.column) } else { - row.rect.left() + 0.0 }; let right = if ri == max.row { row.x_offset(max.column) @@ -45,10 +46,10 @@ pub fn paint_text_selection( } else { 0.0 }; - row.rect.right() + newline_size + row.size.x + newline_size }; - let rect = Rect::from_min_max(pos2(left, row.min_y()), pos2(right, row.max_y())); + let rect = Rect::from_min_max(pos2(left, 0.0), pos2(right, row.size.y)); let mesh = &mut row.visuals.mesh; // Time to insert the selection rectangle into the row mesh. diff --git a/crates/egui/src/widget_text.rs b/crates/egui/src/widget_text.rs index 5ddafc4be..e66cb1bc8 100644 --- a/crates/egui/src/widget_text.rs +++ b/crates/egui/src/widget_text.rs @@ -671,8 +671,8 @@ impl WidgetText { Self::RichText(text) => text.font_height(fonts, style), Self::LayoutJob(job) => job.font_height(fonts), Self::Galley(galley) => { - if let Some(row) = galley.rows.first() { - row.height().round_ui() + if let Some(placed_row) = galley.rows.first() { + placed_row.height().round_ui() } else { galley.size().y.round_ui() } diff --git a/crates/egui/src/widgets/label.rs b/crates/egui/src/widgets/label.rs index c36b9fc60..3656af92b 100644 --- a/crates/egui/src/widgets/label.rs +++ b/crates/egui/src/widgets/label.rs @@ -1,8 +1,8 @@ use std::sync::Arc; use crate::{ - epaint, pos2, text_selection, vec2, Align, Direction, FontSelection, Galley, Pos2, Response, - Sense, Stroke, TextWrapMode, Ui, Widget, WidgetInfo, WidgetText, WidgetType, + epaint, pos2, text_selection, Align, Direction, FontSelection, Galley, Pos2, Response, Sense, + Stroke, TextWrapMode, Ui, Widget, WidgetInfo, WidgetText, WidgetType, }; use self::text_selection::LabelSelectionState; @@ -216,10 +216,10 @@ impl Label { let pos = pos2(ui.max_rect().left(), ui.cursor().top()); assert!(!galley.rows.is_empty(), "Galleys are never empty"); // collect a response from many rows: - let rect = galley.rows[0].rect.translate(vec2(pos.x, pos.y)); + let rect = galley.rows[0].rect().translate(pos.to_vec2()); let mut response = ui.allocate_rect(rect, sense); - for row in galley.rows.iter().skip(1) { - let rect = row.rect.translate(vec2(pos.x, pos.y)); + for placed_row in galley.rows.iter().skip(1) { + let rect = placed_row.rect().translate(pos.to_vec2()); response |= ui.allocate_rect(rect, sense); } (pos, galley, response) diff --git a/crates/egui_demo_lib/Cargo.toml b/crates/egui_demo_lib/Cargo.toml index 0e0299f11..77b8fdcb3 100644 --- a/crates/egui_demo_lib/Cargo.toml +++ b/crates/egui_demo_lib/Cargo.toml @@ -58,6 +58,7 @@ serde = { workspace = true, optional = true } criterion.workspace = true egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] } egui = { workspace = true, features = ["default_fonts"] } +rand = "0.9" [[bench]] name = "benchmark" diff --git a/crates/egui_demo_lib/benches/benchmark.rs b/crates/egui_demo_lib/benches/benchmark.rs index cbcc4d88f..dab6bdd7b 100644 --- a/crates/egui_demo_lib/benches/benchmark.rs +++ b/crates/egui_demo_lib/benches/benchmark.rs @@ -1,7 +1,10 @@ +use std::fmt::Write as _; + use criterion::{criterion_group, criterion_main, Criterion}; use egui::epaint::TextShape; use egui_demo_lib::LOREM_IPSUM_LONG; +use rand::Rng as _; pub fn criterion_benchmark(c: &mut Criterion) { use egui::RawInput; @@ -128,6 +131,30 @@ pub fn criterion_benchmark(c: &mut Criterion) { }); }); + c.bench_function("text_layout_cached_many_lines_modified", |b| { + const NUM_LINES: usize = 2_000; + + let mut string = String::new(); + for _ in 0..NUM_LINES { + for i in 0..30_u8 { + write!(string, "{i:02X} ").unwrap(); + } + string.push('\n'); + } + + let mut rng = rand::rng(); + b.iter(|| { + fonts.begin_pass(pixels_per_point, max_texture_side); + + // Delete a random character, simulating a user making an edit in a long file: + let mut new_string = string.clone(); + let idx = rng.random_range(0..string.len()); + new_string.remove(idx); + + fonts.layout(new_string, font_id.clone(), text_color, wrap_width); + }); + }); + let galley = fonts.layout(LOREM_IPSUM_LONG.to_owned(), font_id, text_color, wrap_width); let font_image_size = fonts.font_image_size(); let prepared_discs = fonts.texture_atlas().lock().prepared_discs(); diff --git a/crates/emath/src/pos2.rs b/crates/emath/src/pos2.rs index 1f4bd8642..62590b10f 100644 --- a/crates/emath/src/pos2.rs +++ b/crates/emath/src/pos2.rs @@ -1,5 +1,7 @@ -use std::fmt; -use std::ops::{Add, AddAssign, Sub, SubAssign}; +use std::{ + fmt, + ops::{Add, AddAssign, MulAssign, Sub, SubAssign}, +}; use crate::{lerp, Div, Mul, Vec2}; @@ -305,6 +307,14 @@ impl Mul for f32 { } } +impl MulAssign for Pos2 { + #[inline(always)] + fn mul_assign(&mut self, rhs: f32) { + self.x *= rhs; + self.y *= rhs; + } +} + impl Div for Pos2 { type Output = Self; diff --git a/crates/emath/src/rect.rs b/crates/emath/src/rect.rs index 521b6f33f..00bed04f0 100644 --- a/crates/emath/src/rect.rs +++ b/crates/emath/src/rect.rs @@ -710,7 +710,11 @@ impl Rect { impl fmt::Debug for Rect { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "[{:?} - {:?}]", self.min, self.max) + if let Some(precision) = f.precision() { + write!(f, "[{1:.0$?} - {2:.0$?}]", precision, self.min, self.max) + } else { + write!(f, "[{:?} - {:?}]", self.min, self.max) + } } } diff --git a/crates/epaint/Cargo.toml b/crates/epaint/Cargo.toml index 720188872..b8b006d44 100644 --- a/crates/epaint/Cargo.toml +++ b/crates/epaint/Cargo.toml @@ -101,6 +101,7 @@ backtrace = { workspace = true, optional = true } [dev-dependencies] criterion.workspace = true +similar-asserts.workspace = true [[bench]] diff --git a/crates/epaint/src/shape_transform.rs b/crates/epaint/src/shape_transform.rs index 469f2e521..57de14969 100644 --- a/crates/epaint/src/shape_transform.rs +++ b/crates/epaint/src/shape_transform.rs @@ -89,7 +89,8 @@ pub fn adjust_colors( if !galley.is_empty() { let galley = Arc::make_mut(galley); - for row in &mut galley.rows { + for placed_row in &mut galley.rows { + let row = Arc::make_mut(&mut placed_row.row); for vertex in &mut row.visuals.mesh.vertices { adjust_color(&mut vertex.color); } diff --git a/crates/epaint/src/shapes/shape.rs b/crates/epaint/src/shapes/shape.rs index 7b38bda6d..a855d653a 100644 --- a/crates/epaint/src/shapes/shape.rs +++ b/crates/epaint/src/shapes/shape.rs @@ -456,39 +456,7 @@ impl Shape { rect_shape.blur_width *= transform.scaling; } Self::Text(text_shape) => { - let TextShape { - pos, - galley, - underline, - fallback_color: _, - override_text_color: _, - opacity_factor: _, - angle: _, - } = text_shape; - - *pos = transform * *pos; - underline.width *= transform.scaling; - - let Galley { - job: _, - rows, - elided: _, - rect, - mesh_bounds, - num_vertices: _, - num_indices: _, - pixels_per_point: _, - } = Arc::make_mut(galley); - - for row in rows { - row.visuals.mesh_bounds = transform.scaling * row.visuals.mesh_bounds; - for v in &mut row.visuals.mesh.vertices { - v.pos = Pos2::new(transform.scaling * v.pos.x, transform.scaling * v.pos.y); - } - } - - *mesh_bounds = transform.scaling * *mesh_bounds; - *rect = transform.scaling * *rect; + text_shape.transform(transform); } Self::Mesh(mesh) => { Arc::make_mut(mesh).transform(transform); diff --git a/crates/epaint/src/shapes/text_shape.rs b/crates/epaint/src/shapes/text_shape.rs index ef549bd9c..e88213b93 100644 --- a/crates/epaint/src/shapes/text_shape.rs +++ b/crates/epaint/src/shapes/text_shape.rs @@ -89,6 +89,63 @@ impl TextShape { self.opacity_factor = opacity_factor; self } + + /// Move the shape by this many points, in-place. + pub fn transform(&mut self, transform: emath::TSTransform) { + let Self { + pos, + galley, + underline, + fallback_color: _, + override_text_color: _, + opacity_factor: _, + angle: _, + } = self; + + *pos = transform * *pos; + underline.width *= transform.scaling; + + let Galley { + job: _, + rows, + elided: _, + rect, + mesh_bounds, + num_vertices: _, + num_indices: _, + pixels_per_point: _, + } = Arc::make_mut(galley); + + *rect = transform.scaling * *rect; + *mesh_bounds = transform.scaling * *mesh_bounds; + + for text::PlacedRow { pos, row } in rows { + *pos *= transform.scaling; + + let text::Row { + section_index_at_start: _, + glyphs: _, // TODO(emilk): would it make sense to transform these? + size, + visuals, + ends_with_newline: _, + } = Arc::make_mut(row); + + *size *= transform.scaling; + + let text::RowVisuals { + mesh, + mesh_bounds, + glyph_index_start: _, + glyph_vertex_range: _, + } = visuals; + + *mesh_bounds = transform.scaling * *mesh_bounds; + + for v in &mut mesh.vertices { + v.pos *= transform.scaling; + } + } + } } impl From for Shape { diff --git a/crates/epaint/src/stats.rs b/crates/epaint/src/stats.rs index cb72d90e3..2acf1e93c 100644 --- a/crates/epaint/src/stats.rs +++ b/crates/epaint/src/stats.rs @@ -91,7 +91,7 @@ impl AllocInfo { + galley.rows.iter().map(Self::from_galley_row).sum() } - fn from_galley_row(row: &crate::text::Row) -> Self { + fn from_galley_row(row: &crate::text::PlacedRow) -> Self { Self::from_mesh(&row.visuals.mesh) + Self::from_slice(&row.glyphs) } diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 914b1cc34..2b24869ae 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -2033,11 +2033,13 @@ impl Tessellator { continue; } + let final_row_pos = galley_pos + row.pos.to_vec2(); + let mut row_rect = row.visuals.mesh_bounds; if *angle != 0.0 { row_rect = row_rect.rotate_bb(rotator); } - row_rect = row_rect.translate(galley_pos.to_vec2()); + row_rect = row_rect.translate(final_row_pos.to_vec2()); if self.options.coarse_tessellation_culling && !self.clip_rect.intersects(row_rect) { // culling individual lines of text is important, since a single `Shape::Text` @@ -2086,7 +2088,7 @@ impl Tessellator { }; Vertex { - pos: galley_pos + offset, + pos: final_row_pos + offset, uv: (uv.to_vec2() * uv_normalizer).to_pos2(), color, } diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index ccbf66f9c..bfa854680 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -4,7 +4,7 @@ use crate::{ mutex::{Mutex, MutexGuard}, text::{ font::{Font, FontImpl}, - Galley, LayoutJob, + Galley, LayoutJob, LayoutSection, }, TextureAtlas, }; @@ -617,7 +617,9 @@ pub struct FontsAndCache { impl FontsAndCache { fn layout_job(&mut self, job: LayoutJob) -> Arc { - self.galley_cache.layout(&mut self.fonts, job) + let allow_split_paragraphs = true; // Optimization for editing text with many paragraphs. + self.galley_cache + .layout(&mut self.fonts, job, allow_split_paragraphs) } } @@ -726,6 +728,12 @@ impl FontsImpl { struct CachedGalley { /// When it was last used last_used: u32, + + /// Hashes of all other entries this one depends on for quick re-layout. + /// Their `last_used`s should be updated alongside this one to make sure they're + /// not evicted. + children: Option>, + galley: Arc, } @@ -737,13 +745,18 @@ struct GalleyCache { } impl GalleyCache { - fn layout(&mut self, fonts: &mut FontsImpl, mut job: LayoutJob) -> Arc { + fn layout_internal( + &mut self, + fonts: &mut FontsImpl, + mut job: LayoutJob, + allow_split_paragraphs: bool, + ) -> (u64, Arc) { if job.wrap.max_width.is_finite() { // Protect against rounding errors in egui layout code. // Say the user asks to wrap at width 200.0. // The text layout wraps, and reports that the final width was 196.0 points. - // This than trickles up the `Ui` chain and gets stored as the width for a tooltip (say). + // This then trickles up the `Ui` chain and gets stored as the width for a tooltip (say). // On the next frame, this is then set as the max width for the tooltip, // and we end up calling the text layout code again, this time with a wrap width of 196.0. // Except, somewhere in the `Ui` chain with added margins etc, a rounding error was introduced, @@ -765,22 +778,176 @@ impl GalleyCache { let hash = crate::util::hash(&job); // TODO(emilk): even faster hasher? - match self.cache.entry(hash) { + let galley = match self.cache.entry(hash) { std::collections::hash_map::Entry::Occupied(entry) => { + // The job was found in cache - no need to re-layout. let cached = entry.into_mut(); cached.last_used = self.generation; - cached.galley.clone() - } - std::collections::hash_map::Entry::Vacant(entry) => { - let galley = super::layout(fonts, job.into()); - let galley = Arc::new(galley); - entry.insert(CachedGalley { - last_used: self.generation, - galley: galley.clone(), - }); + + let galley = cached.galley.clone(); + if let Some(children) = &cached.children { + // The point of `allow_split_paragraphs` is to split large jobs into paragraph, + // and then cache each paragraph individually. + // That way, if we edit a single paragraph, only that paragraph will be re-layouted. + // For that to work we need to keep all the child/paragraph + // galleys alive while the parent galley is alive: + for child_hash in children.clone().iter() { + if let Some(cached_child) = self.cache.get_mut(child_hash) { + cached_child.last_used = self.generation; + } + } + } + galley } + std::collections::hash_map::Entry::Vacant(entry) => { + let job = Arc::new(job); + if allow_split_paragraphs && should_cache_each_paragraph_individually(&job) { + let (child_galleys, child_hashes) = + self.layout_each_paragraph_individuallly(fonts, &job); + debug_assert_eq!( + child_hashes.len(), + child_galleys.len(), + "Bug in `layout_each_paragraph_individuallly`" + ); + let galley = + Arc::new(Galley::concat(job, &child_galleys, fonts.pixels_per_point)); + + self.cache.insert( + hash, + CachedGalley { + last_used: self.generation, + children: Some(child_hashes.into()), + galley: galley.clone(), + }, + ); + galley + } else { + let galley = super::layout(fonts, job); + let galley = Arc::new(galley); + entry.insert(CachedGalley { + last_used: self.generation, + children: None, + galley: galley.clone(), + }); + galley + } + } + }; + + (hash, galley) + } + + fn layout( + &mut self, + fonts: &mut FontsImpl, + job: LayoutJob, + allow_split_paragraphs: bool, + ) -> Arc { + self.layout_internal(fonts, job, allow_split_paragraphs).1 + } + + /// Split on `\n` and lay out (and cache) each paragraph individually. + fn layout_each_paragraph_individuallly( + &mut self, + fonts: &mut FontsImpl, + job: &LayoutJob, + ) -> (Vec>, Vec) { + profiling::function_scope!(); + + let mut current_section = 0; + let mut start = 0; + let mut max_rows_remaining = job.wrap.max_rows; + let mut child_galleys = Vec::new(); + let mut child_hashes = Vec::new(); + + while start < job.text.len() { + let is_first_paragraph = start == 0; + let end = job.text[start..] + .find('\n') + .map_or(job.text.len(), |i| start + i + 1); + + let mut paragraph_job = LayoutJob { + text: job.text[start..end].to_owned(), + wrap: crate::text::TextWrapping { + max_rows: max_rows_remaining, + ..job.wrap + }, + sections: Vec::new(), + break_on_newline: job.break_on_newline, + halign: job.halign, + justify: job.justify, + first_row_min_height: if is_first_paragraph { + job.first_row_min_height + } else { + 0.0 + }, + round_output_to_gui: job.round_output_to_gui, + }; + + // Add overlapping sections: + for section in &job.sections[current_section..job.sections.len()] { + let LayoutSection { + leading_space, + byte_range: section_range, + format, + } = section; + + // `start` and `end` are the byte range of the current paragraph. + // How does the current section overlap with the paragraph range? + + if section_range.end <= start { + // The section is behind us + current_section += 1; + } else if end <= section_range.start { + break; // Haven't reached this one yet. + } else { + // Section range overlaps with paragraph range + debug_assert!( + section_range.start < section_range.end, + "Bad byte_range: {section_range:?}" + ); + let new_range = section_range.start.saturating_sub(start) + ..(section_range.end.at_most(end)).saturating_sub(start); + debug_assert!( + new_range.start <= new_range.end, + "Bad new section range: {new_range:?}" + ); + paragraph_job.sections.push(LayoutSection { + leading_space: if start <= new_range.start { + *leading_space + } else { + 0.0 + }, + byte_range: new_range, + format: format.clone(), + }); + } + } + + // TODO(emilk): we could lay out each paragraph in parallel to get a nice speedup on multicore machines. + let (hash, galley) = self.layout_internal(fonts, paragraph_job, false); + child_hashes.push(hash); + + // This will prevent us from invalidating cache entries unnecessarily: + if max_rows_remaining != usize::MAX { + max_rows_remaining -= galley.rows.len(); + // Ignore extra trailing row, see merging `Galley::concat` for more details. + if end < job.text.len() && !galley.elided { + max_rows_remaining += 1; + } + } + + let elided = galley.elided; + child_galleys.push(galley); + if elided { + break; + } + + start = end; } + + (child_galleys, child_hashes) } pub fn num_galleys_in_cache(&self) -> usize { @@ -797,6 +964,16 @@ impl GalleyCache { } } +/// If true, lay out and cache each paragraph (sections separated by newlines) individually. +/// +/// This makes it much faster to re-layout the full text when only a portion of it has changed since last frame, i.e. when editing somewhere in a file with thousands of lines/paragraphs. +fn should_cache_each_paragraph_individually(job: &LayoutJob) -> bool { + // We currently don't support this elided text, i.e. when `max_rows` is set. + // Most often, elided text is elided to one row, + // and so will always be fast to lay out. + job.break_on_newline && job.wrap.max_rows == usize::MAX && job.text.contains('\n') +} + // ---------------------------------------------------------------------------- struct FontImplCache { @@ -867,3 +1044,121 @@ impl FontImplCache { .clone() } } + +#[cfg(feature = "default_fonts")] +#[cfg(test)] +mod tests { + use core::f32; + + use super::*; + use crate::{text::TextFormat, Stroke}; + use ecolor::Color32; + use emath::Align; + + fn jobs() -> Vec { + vec![ + LayoutJob::simple( + String::default(), + FontId::new(14.0, FontFamily::Monospace), + Color32::WHITE, + f32::INFINITY, + ), + LayoutJob::simple( + "Simple test.".to_owned(), + FontId::new(14.0, FontFamily::Monospace), + Color32::WHITE, + f32::INFINITY, + ), + LayoutJob::simple( + "This some text that may be long.\nDet kanske ocksĆ„ finns lite ƅƄƖ hƤr.".to_owned(), + FontId::new(14.0, FontFamily::Proportional), + Color32::WHITE, + 50.0, + ), + { + let mut job = LayoutJob { + first_row_min_height: 20.0, + ..Default::default() + }; + job.append( + "1st paragraph has some leading space.\n", + 16.0, + TextFormat { + font_id: FontId::new(14.0, FontFamily::Proportional), + ..Default::default() + }, + ); + job.append( + "2nd paragraph has underline and strikthrough, and has some non-ASCII characters:\n ƅƄƖ.", + 0.0, + TextFormat { + font_id: FontId::new(15.0, FontFamily::Monospace), + underline: Stroke::new(1.0, Color32::RED), + strikethrough: Stroke::new(1.0, Color32::GREEN), + ..Default::default() + }, + ); + job.append( + "3rd paragraph is kind of boring, but has italics.\nAnd a newline", + 0.0, + TextFormat { + font_id: FontId::new(10.0, FontFamily::Proportional), + italics: true, + ..Default::default() + }, + ); + + job + }, + ] + } + + #[test] + fn test_split_paragraphs() { + for pixels_per_point in [1.0, 2.0_f32.sqrt(), 2.0] { + let max_texture_side = 4096; + let mut fonts = FontsImpl::new( + pixels_per_point, + max_texture_side, + FontDefinitions::default(), + ); + + for halign in [Align::Min, Align::Center, Align::Max] { + for justify in [false, true] { + for mut job in jobs() { + job.halign = halign; + job.justify = justify; + + let whole = GalleyCache::default().layout(&mut fonts, job.clone(), false); + + let split = GalleyCache::default().layout(&mut fonts, job.clone(), true); + + for (i, row) in whole.rows.iter().enumerate() { + println!( + "Whole row {i}: section_index_at_start={}, first glyph section_index: {:?}", + row.row.section_index_at_start, + row.row.glyphs.first().map(|g| g.section_index) + ); + } + for (i, row) in split.rows.iter().enumerate() { + println!( + "Split row {i}: section_index_at_start={}, first glyph section_index: {:?}", + row.row.section_index_at_start, + row.row.glyphs.first().map(|g| g.section_index) + ); + } + + // Don't compare for equaliity; but format with a specific precision and make sure we hit that. + // NOTE: we use a rather low precision, because as long as we're within a pixel I think it's good enough. + similar_asserts::assert_eq!( + format!("{:#.1?}", split), + format!("{:#.1?}", whole), + "pixels_per_point: {pixels_per_point:.2}, input text: '{}'", + job.text + ); + } + } + } + } + } +} diff --git a/crates/epaint/src/text/mod.rs b/crates/epaint/src/text/mod.rs index 3cb0e98cb..cf5c8ebfc 100644 --- a/crates/epaint/src/text/mod.rs +++ b/crates/epaint/src/text/mod.rs @@ -14,7 +14,7 @@ pub use { FontData, FontDefinitions, FontFamily, FontId, FontInsert, FontPriority, FontTweak, Fonts, FontsImpl, InsertFontFamily, }, - text_layout::layout, + text_layout::*, text_layout_types::*, }; diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index d1b2d5038..b2dba96fc 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -1,11 +1,10 @@ -use std::ops::RangeInclusive; use std::sync::Arc; use emath::{pos2, vec2, Align, GuiRounding as _, NumExt, Pos2, Rect, Vec2}; use crate::{stroke::PathStroke, text::font::Font, Color32, Mesh, Stroke, Vertex}; -use super::{FontsImpl, Galley, Glyph, LayoutJob, LayoutSection, Row, RowVisuals}; +use super::{FontsImpl, Galley, Glyph, LayoutJob, LayoutSection, PlacedRow, Row, RowVisuals}; // ---------------------------------------------------------------------------- @@ -70,12 +69,14 @@ impl Paragraph { /// In most cases you should use [`crate::Fonts::layout_job`] instead /// since that memoizes the input, making subsequent layouting of the same text much faster. pub fn layout(fonts: &mut FontsImpl, job: Arc) -> Galley { + profiling::function_scope!(); + if job.wrap.max_rows == 0 { // Early-out: no text return Galley { job, rows: Default::default(), - rect: Rect::from_min_max(Pos2::ZERO, Pos2::ZERO), + rect: Rect::ZERO, mesh_bounds: Rect::NOTHING, num_vertices: 0, num_indices: 0, @@ -96,10 +97,11 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc) -> Galley { let mut elided = false; let mut rows = rows_from_paragraphs(paragraphs, &job, &mut elided); if elided { - if let Some(last_row) = rows.last_mut() { + if let Some(last_placed) = rows.last_mut() { + let last_row = Arc::make_mut(&mut last_placed.row); replace_last_glyph_with_overflow_character(fonts, &job, last_row); if let Some(last) = last_row.glyphs.last() { - last_row.rect.max.x = last.max_x(); + last_row.size.x = last.max_x(); } } } @@ -108,12 +110,12 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc) -> Galley { if justify || job.halign != Align::LEFT { let num_rows = rows.len(); - for (i, row) in rows.iter_mut().enumerate() { + for (i, placed_row) in rows.iter_mut().enumerate() { let is_last_row = i + 1 == num_rows; - let justify_row = justify && !row.ends_with_newline && !is_last_row; + let justify_row = justify && !placed_row.ends_with_newline && !is_last_row; halign_and_justify_row( point_scale, - row, + placed_row, job.halign, job.wrap.max_width, justify_row, @@ -188,17 +190,12 @@ fn layout_section( } } -/// We ignore y at this stage -fn rect_from_x_range(x_range: RangeInclusive) -> Rect { - Rect::from_x_y_ranges(x_range, 0.0..=0.0) -} - // Ignores the Y coordinate. fn rows_from_paragraphs( paragraphs: Vec, job: &LayoutJob, elided: &mut bool, -) -> Vec { +) -> Vec { let num_paragraphs = paragraphs.len(); let mut rows = vec![]; @@ -212,31 +209,35 @@ fn rows_from_paragraphs( let is_last_paragraph = (i + 1) == num_paragraphs; if paragraph.glyphs.is_empty() { - rows.push(Row { - section_index_at_start: paragraph.section_index_at_start, - glyphs: vec![], - visuals: Default::default(), - rect: Rect::from_min_size( - pos2(paragraph.cursor_x, 0.0), - vec2(0.0, paragraph.empty_paragraph_height), - ), - ends_with_newline: !is_last_paragraph, + rows.push(PlacedRow { + pos: Pos2::ZERO, + row: Arc::new(Row { + section_index_at_start: paragraph.section_index_at_start, + glyphs: vec![], + visuals: Default::default(), + size: vec2(0.0, paragraph.empty_paragraph_height), + 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() { // Early-out optimization: the whole paragraph fits on one row. - let paragraph_min_x = paragraph.glyphs[0].pos.x; - rows.push(Row { - section_index_at_start: paragraph.section_index_at_start, - glyphs: paragraph.glyphs, - visuals: Default::default(), - rect: rect_from_x_range(paragraph_min_x..=paragraph_max_x), - ends_with_newline: !is_last_paragraph, + rows.push(PlacedRow { + pos: pos2(0.0, f32::NAN), + row: Arc::new(Row { + section_index_at_start: paragraph.section_index_at_start, + glyphs: paragraph.glyphs, + visuals: Default::default(), + size: vec2(paragraph_max_x, 0.0), + ends_with_newline: !is_last_paragraph, + }), }); } else { line_break(¶graph, job, &mut rows, elided); - rows.last_mut().unwrap().ends_with_newline = !is_last_paragraph; + let placed_row = rows.last_mut().unwrap(); + let row = Arc::make_mut(&mut placed_row.row); + row.ends_with_newline = !is_last_paragraph; } } } @@ -244,7 +245,12 @@ fn rows_from_paragraphs( rows } -fn line_break(paragraph: &Paragraph, job: &LayoutJob, out_rows: &mut Vec, elided: &mut bool) { +fn line_break( + paragraph: &Paragraph, + job: &LayoutJob, + out_rows: &mut Vec, + elided: &mut bool, +) { let wrap_width = job.effective_wrap_width(); // Keeps track of good places to insert row break if we exceed `wrap_width`. @@ -270,12 +276,15 @@ fn line_break(paragraph: &Paragraph, job: &LayoutJob, out_rows: &mut Vec, e { // Allow the first row to be completely empty, because we know there will be more space on the next row: // TODO(emilk): this records the height of this first row as zero, though that is probably fine since first_row_indentation usually comes with a first_row_min_height. - out_rows.push(Row { - section_index_at_start: paragraph.section_index_at_start, - glyphs: vec![], - visuals: Default::default(), - rect: rect_from_x_range(first_row_indentation..=first_row_indentation), - ends_with_newline: false, + out_rows.push(PlacedRow { + pos: pos2(0.0, f32::NAN), + row: Arc::new(Row { + section_index_at_start: paragraph.section_index_at_start, + glyphs: vec![], + visuals: Default::default(), + size: Vec2::ZERO, + ends_with_newline: false, + }), }); row_start_x += first_row_indentation; first_row_indentation = 0.0; @@ -291,15 +300,17 @@ fn line_break(paragraph: &Paragraph, job: &LayoutJob, out_rows: &mut Vec, e .collect(); let section_index_at_start = glyphs[0].section_index; - let paragraph_min_x = glyphs[0].pos.x; let paragraph_max_x = glyphs.last().unwrap().max_x(); - out_rows.push(Row { - section_index_at_start, - glyphs, - visuals: Default::default(), - rect: rect_from_x_range(paragraph_min_x..=paragraph_max_x), - ends_with_newline: false, + out_rows.push(PlacedRow { + pos: pos2(0.0, f32::NAN), + row: Arc::new(Row { + section_index_at_start, + glyphs, + visuals: Default::default(), + size: vec2(paragraph_max_x, 0.0), + ends_with_newline: false, + }), }); // Start a new row: @@ -333,12 +344,15 @@ fn line_break(paragraph: &Paragraph, job: &LayoutJob, out_rows: &mut Vec, e let paragraph_min_x = glyphs[0].pos.x; let paragraph_max_x = glyphs.last().unwrap().max_x(); - out_rows.push(Row { - section_index_at_start, - glyphs, - visuals: Default::default(), - rect: rect_from_x_range(paragraph_min_x..=paragraph_max_x), - ends_with_newline: false, + out_rows.push(PlacedRow { + pos: pos2(paragraph_min_x, 0.0), + row: Arc::new(Row { + section_index_at_start, + glyphs, + visuals: Default::default(), + size: vec2(paragraph_max_x - paragraph_min_x, 0.0), + ends_with_newline: false, + }), }); } } @@ -500,11 +514,13 @@ fn replace_last_glyph_with_overflow_character( /// Ignores the Y coordinate. fn halign_and_justify_row( point_scale: PointScale, - row: &mut Row, + placed_row: &mut PlacedRow, halign: Align, wrap_width: f32, justify: bool, ) { + let row = Arc::make_mut(&mut placed_row.row); + if row.glyphs.is_empty() { return; } @@ -572,7 +588,8 @@ fn halign_and_justify_row( / (num_spaces_in_range as f32); } - let mut translate_x = target_min_x - original_min_x - extra_x_per_glyph * glyph_range.0 as f32; + placed_row.pos.x = point_scale.round_to_pixel(target_min_x); + let mut translate_x = -original_min_x - extra_x_per_glyph * glyph_range.0 as f32; for glyph in &mut row.glyphs { glyph.pos.x += translate_x; @@ -584,23 +601,23 @@ fn halign_and_justify_row( } // Note we ignore the leading/trailing whitespace here! - row.rect.min.x = target_min_x; - row.rect.max.x = target_max_x; + row.size.x = target_max_x - target_min_x; } /// Calculate the Y positions and tessellate the text. fn galley_from_rows( point_scale: PointScale, job: Arc, - mut rows: Vec, + mut rows: Vec, elided: bool, ) -> Galley { let mut first_row_min_height = job.first_row_min_height; let mut cursor_y = 0.0; - let mut min_x: f32 = 0.0; - let mut max_x: f32 = 0.0; - for row in &mut rows { - let mut max_row_height = first_row_min_height.max(row.rect.height()); + + for placed_row in &mut rows { + let mut max_row_height = first_row_min_height.max(placed_row.rect().height()); + let row = Arc::make_mut(&mut placed_row.row); + first_row_min_height = 0.0; for glyph in &row.glyphs { max_row_height = max_row_height.max(glyph.line_height); @@ -611,8 +628,7 @@ fn galley_from_rows( for glyph in &mut row.glyphs { let format = &job.sections[glyph.section_index as usize].format; - glyph.pos.y = cursor_y - + glyph.font_impl_ascent + glyph.pos.y = glyph.font_impl_ascent // Apply valign to the different in height of the entire row, and the height of this `Font`: + format.valign.to_factor() * (max_row_height - glyph.line_height) @@ -624,53 +640,38 @@ fn galley_from_rows( glyph.pos.y = point_scale.round_to_pixel(glyph.pos.y); } - row.rect.min.y = cursor_y; - row.rect.max.y = cursor_y + max_row_height; + placed_row.pos.y = cursor_y; + row.size.y = max_row_height; - min_x = min_x.min(row.rect.min.x); - max_x = max_x.max(row.rect.max.x); cursor_y += max_row_height; cursor_y = point_scale.round_to_pixel(cursor_y); // TODO(emilk): it would be better to do the calculations in pixels instead. } let format_summary = format_summary(&job); + let mut rect = Rect::ZERO; let mut mesh_bounds = Rect::NOTHING; let mut num_vertices = 0; let mut num_indices = 0; - for row in &mut rows { + for placed_row in &mut rows { + rect = rect.union(placed_row.rect()); + + let row = Arc::make_mut(&mut placed_row.row); row.visuals = tessellate_row(point_scale, &job, &format_summary, row); - mesh_bounds = mesh_bounds.union(row.visuals.mesh_bounds); + + mesh_bounds = + mesh_bounds.union(row.visuals.mesh_bounds.translate(placed_row.pos.to_vec2())); num_vertices += row.visuals.mesh.vertices.len(); num_indices += row.visuals.mesh.indices.len(); - } - let mut rect = Rect::from_min_max(pos2(min_x, 0.0), pos2(max_x, cursor_y)); - - if job.round_output_to_gui { - for row in &mut rows { - row.rect = row.rect.round_ui(); - } - - let did_exceed_wrap_width_by_a_lot = rect.width() > job.wrap.max_width + 1.0; - - rect = rect.round_ui(); - - if did_exceed_wrap_width_by_a_lot { - // If the user picked a too aggressive wrap width (e.g. more narrow than any individual glyph), - // we should let the user know by reporting that our width is wider than the wrap width. - } else { - // Make sure we don't report being wider than the wrap width the user picked: - rect.max.x = rect - .max - .x - .at_most(rect.min.x + job.wrap.max_width) - .floor_ui(); + row.section_index_at_start = u32::MAX; // No longer in use. + for glyph in &mut row.glyphs { + glyph.section_index = u32::MAX; // No longer in use. } } - Galley { + let mut galley = Galley { job, rows, elided, @@ -679,7 +680,13 @@ fn galley_from_rows( num_vertices, num_indices, pixels_per_point: point_scale.pixels_per_point, + }; + + if galley.job.round_output_to_gui { + galley.round_output_to_gui(); } + + galley } #[derive(Default)] @@ -876,7 +883,7 @@ fn add_row_hline( let (stroke, mut y) = stroke_and_y(glyph); stroke.round_center_to_pixel(point_scale.pixels_per_point, &mut y); - if stroke == Stroke::NONE { + if stroke.is_empty() { end_line(line_start.take(), last_right_x); } else if let Some((existing_stroke, start)) = line_start { if existing_stroke == stroke && start.y == y { @@ -1130,6 +1137,7 @@ mod tests { vec!["# DNA…"] ); let row = &galley.rows[0]; - assert_eq!(row.rect.max.x, row.glyphs.last().unwrap().max_x()); + assert_eq!(row.pos, Pos2::ZERO); + assert_eq!(row.rect().max.x, row.glyphs.last().unwrap().max_x()); } } diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index b6d7ccf49..4bd15d3e3 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -9,7 +9,7 @@ use super::{ font::UvRect, }; use crate::{Color32, FontId, Mesh, Stroke}; -use emath::{pos2, vec2, Align, NumExt, OrderedFloat, Pos2, Rect, Vec2}; +use emath::{pos2, vec2, Align, GuiRounding as _, NumExt, OrderedFloat, Pos2, Rect, Vec2}; /// Describes the task of laying out text. /// @@ -508,14 +508,14 @@ pub struct Galley { /// Contains the original string and style sections. pub job: Arc, - /// Rows of text, from top to bottom. + /// Rows of text, from top to bottom, and their offsets. /// /// The number of characters in all rows sum up to `job.text.chars().count()` /// unless [`Self::elided`] is `true`. /// /// Note that a paragraph (a piece of text separated with `\n`) /// can be split up into multiple rows. - pub rows: Vec, + pub rows: Vec, /// Set to true the text was truncated due to [`TextWrapping::max_rows`]. pub elided: bool, @@ -547,19 +547,50 @@ pub struct Galley { pub pixels_per_point: f32, } +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct PlacedRow { + /// The position of this [`Row`] relative to the galley. + /// + /// This is rounded to the closest _pixel_ in order to produce crisp, pixel-perfect text. + pub pos: Pos2, + + /// The underlying unpositioned [`Row`]. + pub row: Arc, +} + +impl PlacedRow { + /// Logical bounding rectangle on font heights etc. + /// Use this when drawing a selection or similar! + pub fn rect(&self) -> Rect { + Rect::from_min_size(self.pos, self.row.size) + } +} + +impl std::ops::Deref for PlacedRow { + type Target = Row; + + fn deref(&self) -> &Self::Target { + &self.row + } +} + #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct Row { - /// This is included in case there are no glyphs - pub section_index_at_start: u32, + /// This is included in case there are no glyphs. + /// + /// Only used during layout, then set to an invalid value in order to + /// enable the paragraph-concat optimization path without having to + /// adjust `section_index` when concatting. + pub(crate) section_index_at_start: u32, /// One for each `char`. pub glyphs: Vec, - /// Logical bounding rectangle based on font heights etc. - /// Use this when drawing a selection or similar! + /// Logical size based on font heights etc. /// Includes leading and trailing whitespace. - pub rect: Rect, + pub size: Vec2, /// The mesh, ready to be rendered. pub visuals: RowVisuals, @@ -613,7 +644,7 @@ pub struct Glyph { /// The character this glyph represents. pub chr: char, - /// Baseline position, relative to the galley. + /// Baseline position, relative to the row. /// Logical position: pos.y is the same for all chars of the same [`TextFormat`]. pub pos: Pos2, @@ -642,7 +673,11 @@ pub struct Glyph { pub uv_rect: UvRect, /// Index into [`LayoutJob::sections`]. Decides color etc. - pub section_index: u32, + /// + /// Only used during layout, then set to an invalid value in order to + /// enable the paragraph-concat optimization path without having to + /// adjust `section_index` when concatting. + pub(crate) section_index: u32, } impl Glyph { @@ -683,22 +718,7 @@ impl Row { self.glyphs.len() + (self.ends_with_newline as usize) } - #[inline] - pub fn min_y(&self) -> f32 { - self.rect.top() - } - - #[inline] - pub fn max_y(&self) -> f32 { - self.rect.bottom() - } - - #[inline] - pub fn height(&self) -> f32 { - self.rect.height() - } - - /// Closest char at the desired x coordinate. + /// Closest char at the desired x coordinate in row-relative coordinates. /// Returns something in the range `[0, char_count_excluding_newline()]`. pub fn char_at(&self, desired_x: f32) -> usize { for (i, glyph) in self.glyphs.iter().enumerate() { @@ -713,9 +733,26 @@ impl Row { if let Some(glyph) = self.glyphs.get(column) { glyph.pos.x } else { - self.rect.right() + self.size.x } } + + #[inline] + pub fn height(&self) -> f32 { + self.size.y + } +} + +impl PlacedRow { + #[inline] + pub fn min_y(&self) -> f32 { + self.rect().top() + } + + #[inline] + pub fn max_y(&self) -> f32 { + self.rect().bottom() + } } impl Galley { @@ -734,6 +771,92 @@ impl Galley { pub fn size(&self) -> Vec2 { self.rect.size() } + + pub(crate) fn round_output_to_gui(&mut self) { + for placed_row in &mut self.rows { + // Optimization: only call `make_mut` if necessary (can cause a deep clone) + let rounded_size = placed_row.row.size.round_ui(); + if placed_row.row.size != rounded_size { + Arc::make_mut(&mut placed_row.row).size = rounded_size; + } + } + + let rect = &mut self.rect; + + let did_exceed_wrap_width_by_a_lot = rect.width() > self.job.wrap.max_width + 1.0; + + *rect = rect.round_ui(); + + if did_exceed_wrap_width_by_a_lot { + // If the user picked a too aggressive wrap width (e.g. more narrow than any individual glyph), + // we should let the user know by reporting that our width is wider than the wrap width. + } else { + // Make sure we don't report being wider than the wrap width the user picked: + rect.max.x = rect + .max + .x + .at_most(rect.min.x + self.job.wrap.max_width) + .floor_ui(); + } + } + + /// Append each galley under the previous one. + pub fn concat(job: Arc, galleys: &[Arc], pixels_per_point: f32) -> Self { + profiling::function_scope!(); + + let mut merged_galley = Self { + job, + rows: Vec::new(), + elided: false, + rect: Rect::ZERO, + mesh_bounds: Rect::NOTHING, + num_vertices: 0, + num_indices: 0, + pixels_per_point, + }; + + for (i, galley) in galleys.iter().enumerate() { + let current_y_offset = merged_galley.rect.height(); + + let mut rows = galley.rows.iter(); + // As documented in `Row::ends_with_newline`, a '\n' will always create a + // new `Row` immediately below the current one. Here it doesn't make sense + // for us to append this new row so we just ignore it. + let is_last_row = i + 1 == galleys.len(); + if !is_last_row && !galley.elided { + let popped = rows.next_back(); + debug_assert_eq!(popped.unwrap().row.glyphs.len(), 0, "Bug in Galley::concat"); + } + + merged_galley.rows.extend(rows.map(|placed_row| { + let new_pos = placed_row.pos + current_y_offset * Vec2::Y; + let new_pos = new_pos.round_to_pixels(pixels_per_point); + merged_galley.mesh_bounds = merged_galley + .mesh_bounds + .union(placed_row.visuals.mesh_bounds.translate(new_pos.to_vec2())); + merged_galley.rect = merged_galley + .rect + .union(Rect::from_min_size(new_pos, placed_row.size)); + + super::PlacedRow { + pos: new_pos, + row: placed_row.row.clone(), + } + })); + + merged_galley.num_vertices += galley.num_vertices; + merged_galley.num_indices += galley.num_indices; + // Note that if `galley.elided` is true this will be the last `Galley` in + // the vector and the loop will end. + merged_galley.elided |= galley.elided; + } + + if merged_galley.job.round_output_to_gui { + merged_galley.round_output_to_gui(); + } + + merged_galley + } } impl AsRef for Galley { @@ -765,7 +888,7 @@ impl Galley { /// Zero-width rect past the last character. fn end_pos(&self) -> Rect { if let Some(row) = self.rows.last() { - let x = row.rect.right(); + let x = row.rect().right(); Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y())) } else { // Empty galley @@ -816,11 +939,15 @@ impl Galley { let mut ccursor_index = 0; for row in &self.rows { - let is_pos_within_row = row.min_y() <= pos.y && pos.y <= row.max_y(); - let y_dist = (row.min_y() - pos.y).abs().min((row.max_y() - pos.y).abs()); + let min_y = row.min_y(); + let max_y = row.max_y(); + + let is_pos_within_row = min_y <= pos.y && pos.y <= max_y; + let y_dist = (min_y - pos.y).abs().min((max_y - pos.y).abs()); if is_pos_within_row || y_dist < best_y_dist { best_y_dist = y_dist; - let column = row.char_at(pos.x); + // 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 prefer_next_row = column < row.char_count_excluding_newline(); cursor = CCursor { index: ccursor_index + column, From d78fc39386e28ec7a18bd48a28a688177f090bd4 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Thu, 3 Apr 2025 09:26:49 +0200 Subject: [PATCH 044/129] Use lychee link checker instead of linkinator (#5868) Seems like linkinator doesn't find any files: https://github.com/emilk/egui/pull/5853#issuecomment-2765526298 This will check all links in .md files (except CHANGELOG.md) and in toml files --- .github/workflows/spelling_and_links.yml | 22 +++++++++++++--------- ARCHITECTURE.md | 2 +- README.md | 4 ++-- crates/eframe/README.md | 2 +- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/.github/workflows/spelling_and_links.yml b/.github/workflows/spelling_and_links.yml index d7b32b007..8fb16ae27 100644 --- a/.github/workflows/spelling_and_links.yml +++ b/.github/workflows/spelling_and_links.yml @@ -15,16 +15,20 @@ jobs: - name: Check spelling of entire workspace uses: crate-ci/typos@master - linkinator: - name: linkinator + lychee: + name: lychee runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: jprochazk/linkinator-action@main - with: - linksToSkip: "https://crates.io/crates/.*, http://localhost:.*" # Avoid crates.io rate-limiting - retry: true - retryErrors: true - retryErrorsCount: 5 - retryErrorsJitter: 2000 + - name: Don't check CHANGELOG.md files + # This is really stupid but lychee doesn't have a way of excluding files via GLOB: + # https://github.com/lycheeverse/lychee/issues/1608 + + # We need to exclude CHANGELOG.md since we don't want to have a CI failure everytime some contributor decides + # to change their username. + run: rm -r */**/CHANGELOG.md CHANGELOG.md + - name: Link Checker + uses: lycheeverse/lychee-action@v2 + with: + args: "'**/*.md' '**/*.toml' --exclude localhost --exclude reddit.com" # I guess reddit doesn't like github action IPs diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 000f2b7ac..51d6d41d1 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -51,7 +51,7 @@ Thin wrapper around `egui_demo_lib` so we can compile it to a web site or a nati Depends on `egui_demo_lib` + `eframe`. ### `egui_kittest` -A test harness for egui based on [kittest](https://github.com/rerun/kittest) and [AccessKit](https://github.com/AccessKit/accesskit/). +A test harness for egui based on [kittest](https://github.com/rerun-io/kittest) and [AccessKit](https://github.com/AccessKit/accesskit/). ### Other integrations diff --git a/README.md b/README.md index 63e5f3ada..087d2aad5 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Latest version](https://img.shields.io/crates/v/egui.svg)](https://crates.io/crates/egui) [![Documentation](https://docs.rs/egui/badge.svg)](https://docs.rs/egui) [![unsafe forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance/) -[![Build Status](https://github.com/emilk/egui/workflows/CI/badge.svg)](https://github.com/emilk/egui/actions?workflow=CI) +[![Build Status](https://github.com/emilk/egui/workflows/Rust/badge.svg)](https://github.com/emilk/egui/actions/workflows/rust.yml) [![MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/emilk/egui/blob/master/LICENSE-MIT) [![Apache](https://img.shields.io/badge/license-Apache-blue.svg)](https://github.com/emilk/egui/blob/master/LICENSE-APACHE) [![Discord](https://img.shields.io/discord/900275882684477440?label=egui%20discord)](https://discord.gg/JFcEma9bJq) @@ -353,7 +353,7 @@ Notable contributions by: * [@AsmPrgmC3](https://github.com/AsmPrgmC3): [Proper sRGBA blending for web](https://github.com/emilk/egui/pull/650) * [@AlexApps99](https://github.com/AlexApps99): [`egui_glow`](https://github.com/emilk/egui/pull/685) * [@mankinskin](https://github.com/mankinskin): [Context menus](https://github.com/emilk/egui/pull/543) -* [@t18b219k](https://github.com/t18b219k): [Port glow painter to web](https://github.com/emilk/egui/pull/868) +* [@KentaTheBugMaker](https://github.com/KentaTheBugMaker): [Port glow painter to web](https://github.com/emilk/egui/pull/868) * [@danielkeller](https://github.com/danielkeller): [`Context` refactor](https://github.com/emilk/egui/pull/1050) * [@MaximOsipenko](https://github.com/MaximOsipenko): [`Context` lock refactor](https://github.com/emilk/egui/pull/2625) * [@mwcampbell](https://github.com/mwcampbell): [AccessKit](https://github.com/AccessKit/accesskit) [integration](https://github.com/emilk/egui/pull/2294) diff --git a/crates/eframe/README.md b/crates/eframe/README.md index 5ffd427d0..6f3f44009 100644 --- a/crates/eframe/README.md +++ b/crates/eframe/README.md @@ -26,7 +26,7 @@ sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev lib You need to either use `edition = "2021"`, or set `resolver = "2"` in the `[workspace]` section of your to-level `Cargo.toml`. See [this link](https://doc.rust-lang.org/edition-guide/rust-2021/default-cargo-resolver.html) for more info. -You can opt-in to the using [`egui_wgpu`](https://github.com/emilk/egui/tree/master/crates/egui_wgpu) for rendering by enabling the `wgpu` feature and setting `NativeOptions::renderer` to `Renderer::Wgpu`. +You can opt-in to the using [`egui-wgpu`](https://github.com/emilk/egui/tree/master/crates/egui-wgpu) for rendering by enabling the `wgpu` feature and setting `NativeOptions::renderer` to `Renderer::Wgpu`. ## Alternatives `eframe` is not the only way to write an app using `egui`! You can also try [`egui-miniquad`](https://github.com/not-fl3/egui-miniquad), [`bevy_egui`](https://github.com/mvlabat/bevy_egui), [`egui_sdl2_gl`](https://github.com/ArjunNair/egui_sdl2_gl), and others. From fe631ff9ea999209b8109482e10356dbf357c709 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Sun, 6 Apr 2025 12:45:20 -0400 Subject: [PATCH 045/129] Use `TextBuffer` for `layouter` in `TextEdit` instead of `&str` (#5712) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change allows `layouter` to use the `TextBuffer` instead of `&str` in the closure. It is necessary when layout decisions depend on more than just the raw string content, such as metadata stored in the concrete type implementing `TextBuffer`. In [our use case](https://github.com/damus-io/notedeck/pull/723), we needed this to support mention highlighting when a user selects a mention. Since mentions can contain spaces, determining mention boundaries from the `&str` alone is impossible. Instead, we use the `TextBuffer` implementation to retrieve the correct bounds. See the video below for a demonstration: https://github.com/user-attachments/assets/3cba2906-5546-4b52-b728-1da9c56a83e1 # Breaking change This PR introduces a breaking change to the `layouter` function in `TextEdit`. Previous API: ```rust pub fn layouter(mut self, layouter: &'t mut dyn FnMut(&Ui, &str, f32) -> Arc) -> Self ``` New API: ```rust pub fn layouter(mut self, layouter: &'t mut dyn FnMut(&Ui, &dyn TextBuffer, f32) -> Arc) -> Self ``` ## Impact on Existing Code • Any existing usage of `layouter` will **no longer compile**. • Callers must update their closures to use `&dyn TextBuffer` instead of `&str`. ## Migration Guide Before: ```rust let mut layouter = |ui: &Ui, text: &str, wrap_width: f32| { Ā  Ā  let layout_job = my_highlighter(text); Ā  Ā  layout_job.wrap.max_width = wrap_width; Ā  Ā  ui.fonts(|f| f.layout_job(layout_job)) }; ``` After: ```rust let mut layouter = |ui: &Ui, text: &dyn TextBuffer, wrap_width: f32| { let layout_job = my_highlighter(text.as_str()); layout_job.wrap.max_width = wrap_width; ui.fonts(|f| f.layout_job(layout_job)) }; ``` --- * There is not an issue for this change. * [x] I have followed the instructions in the PR template Signed-off-by: kernelkind --- crates/egui/src/widgets/text_edit/builder.rs | 23 ++++++---- .../egui/src/widgets/text_edit/text_buffer.rs | 45 +++++++++++++++++++ crates/egui_demo_lib/src/demo/code_editor.rs | 4 +- .../src/easy_mark/easy_mark_editor.rs | 4 +- 4 files changed, 63 insertions(+), 13 deletions(-) diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index d73ecd3f6..6a4244496 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -19,6 +19,8 @@ use crate::{ use super::{TextEditOutput, TextEditState}; +type LayouterFn<'t> = &'t mut dyn FnMut(&Ui, &dyn TextBuffer, f32) -> Arc; + /// A text region that the user can edit the contents of. /// /// See also [`Ui::text_edit_singleline`] and [`Ui::text_edit_multiline`]. @@ -71,7 +73,7 @@ pub struct TextEdit<'t> { id_salt: Option, font_selection: FontSelection, text_color: Option, - layouter: Option<&'t mut dyn FnMut(&Ui, &str, f32) -> Arc>, + layouter: Option>, password: bool, frame: bool, margin: Margin, @@ -261,8 +263,8 @@ impl<'t> TextEdit<'t> { /// # egui::__run_test_ui(|ui| { /// # let mut my_code = String::new(); /// # fn my_memoized_highlighter(s: &str) -> egui::text::LayoutJob { Default::default() } - /// let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| { - /// let mut layout_job: egui::text::LayoutJob = my_memoized_highlighter(string); + /// let mut layouter = |ui: &egui::Ui, buf: &dyn egui::TextBuffer, wrap_width: f32| { + /// let mut layout_job: egui::text::LayoutJob = my_memoized_highlighter(buf.as_str()); /// layout_job.wrap.max_width = wrap_width; /// ui.fonts(|f| f.layout_job(layout_job)) /// }; @@ -270,7 +272,10 @@ impl<'t> TextEdit<'t> { /// # }); /// ``` #[inline] - pub fn layouter(mut self, layouter: &'t mut dyn FnMut(&Ui, &str, f32) -> Arc) -> Self { + pub fn layouter( + mut self, + layouter: &'t mut dyn FnMut(&Ui, &dyn TextBuffer, f32) -> Arc, + ) -> Self { self.layouter = Some(layouter); self @@ -510,8 +515,8 @@ impl TextEdit<'_> { }; let font_id_clone = font_id.clone(); - let mut default_layouter = move |ui: &Ui, text: &str, wrap_width: f32| { - let text = mask_if_password(password, text); + let mut default_layouter = move |ui: &Ui, text: &dyn TextBuffer, wrap_width: f32| { + let text = mask_if_password(password, text.as_str()); let layout_job = if multiline { LayoutJob::simple(text, font_id_clone.clone(), text_color, wrap_width) } else { @@ -522,7 +527,7 @@ impl TextEdit<'_> { let layouter = layouter.unwrap_or(&mut default_layouter); - let mut galley = layouter(ui, text.as_str(), wrap_width); + let mut galley = layouter(ui, text, wrap_width); let desired_inner_width = if clip_text { wrap_width // visual clipping with scroll in singleline input. @@ -879,7 +884,7 @@ fn events( state: &mut TextEditState, text: &mut dyn TextBuffer, galley: &mut Arc, - layouter: &mut dyn FnMut(&Ui, &str, f32) -> Arc, + layouter: &mut dyn FnMut(&Ui, &dyn TextBuffer, f32) -> Arc, id: Id, wrap_width: f32, multiline: bool, @@ -1094,7 +1099,7 @@ fn events( any_change = true; // Layout again to avoid frame delay, and to keep `text` and `galley` in sync. - *galley = layouter(ui, text.as_str(), wrap_width); + *galley = layouter(ui, text, wrap_width); // Set cursor_range using new galley: cursor_range = new_ccursor_range; diff --git a/crates/egui/src/widgets/text_edit/text_buffer.rs b/crates/egui/src/widgets/text_edit/text_buffer.rs index ccf3a0958..6cf7da15a 100644 --- a/crates/egui/src/widgets/text_edit/text_buffer.rs +++ b/crates/egui/src/widgets/text_edit/text_buffer.rs @@ -172,6 +172,39 @@ pub trait TextBuffer { self.delete_selected(&CCursorRange::two(min, max)) } } + + /// Returns a unique identifier for the implementing type. + /// + /// This is useful for downcasting from this trait to the implementing type. + /// Here is an example usage: + /// ``` + /// use egui::TextBuffer; + /// use std::any::TypeId; + /// + /// struct ExampleBuffer {} + /// + /// impl TextBuffer for ExampleBuffer { + /// fn is_mutable(&self) -> bool { unimplemented!() } + /// fn as_str(&self) -> &str { unimplemented!() } + /// fn insert_text(&mut self, text: &str, char_index: usize) -> usize { unimplemented!() } + /// fn delete_char_range(&mut self, char_range: std::ops::Range) { unimplemented!() } + /// + /// // Implement it like the following: + /// fn type_id(&self) -> TypeId { + /// TypeId::of::() + /// } + /// } + /// + /// // Example downcast: + /// pub fn downcast_example(buffer: &dyn TextBuffer) -> Option<&ExampleBuffer> { + /// if buffer.type_id() == TypeId::of::() { + /// unsafe { Some(&*(buffer as *const dyn TextBuffer as *const ExampleBuffer)) } + /// } else { + /// None + /// } + /// } + /// ``` + fn type_id(&self) -> std::any::TypeId; } impl TextBuffer for String { @@ -218,6 +251,10 @@ impl TextBuffer for String { fn take(&mut self) -> String { std::mem::take(self) } + + fn type_id(&self) -> std::any::TypeId { + std::any::TypeId::of::() + } } impl TextBuffer for Cow<'_, str> { @@ -248,6 +285,10 @@ impl TextBuffer for Cow<'_, str> { fn take(&mut self) -> String { std::mem::take(self).into_owned() } + + fn type_id(&self) -> std::any::TypeId { + std::any::TypeId::of::>() + } } /// Immutable view of a `&str`! @@ -265,4 +306,8 @@ impl TextBuffer for &str { } fn delete_char_range(&mut self, _ch_range: Range) {} + + fn type_id(&self) -> std::any::TypeId { + std::any::TypeId::of::<&str>() + } } diff --git a/crates/egui_demo_lib/src/demo/code_editor.rs b/crates/egui_demo_lib/src/demo/code_editor.rs index fe39e1be8..2d67f7d46 100644 --- a/crates/egui_demo_lib/src/demo/code_editor.rs +++ b/crates/egui_demo_lib/src/demo/code_editor.rs @@ -76,12 +76,12 @@ impl crate::View for CodeEditor { }); }); - let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| { + let mut layouter = |ui: &egui::Ui, buf: &dyn egui::TextBuffer, wrap_width: f32| { let mut layout_job = egui_extras::syntax_highlighting::highlight( ui.ctx(), ui.style(), &theme, - string, + buf.as_str(), language, ); layout_job.wrap.max_width = wrap_width; diff --git a/crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs b/crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs index 9e730fac5..a0fcdbbf2 100644 --- a/crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs +++ b/crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs @@ -80,8 +80,8 @@ impl EasyMarkEditor { } = self; let response = if self.highlight_editor { - let mut layouter = |ui: &egui::Ui, easymark: &str, wrap_width: f32| { - let mut layout_job = highlighter.highlight(ui.style(), easymark); + let mut layouter = |ui: &egui::Ui, easymark: &dyn TextBuffer, wrap_width: f32| { + let mut layout_job = highlighter.highlight(ui.style(), easymark.as_str()); layout_job.wrap.max_width = wrap_width; ui.fonts(|f| f.layout_job(layout_job)) }; From 4445497546266f2499d63dba0c0e037c49c5af68 Mon Sep 17 00:00:00 2001 From: mitchmindtree Date: Tue, 8 Apr 2025 10:16:29 +0100 Subject: [PATCH 046/129] `Scene`: Set transform layer before calling user content (#5884) This changes the `Scene` behaviour to call `set_transform_layer` prior to calling the user content fn, rather than after. ### Motivation This provides a simple way for the user to access the `TSTransform` that will be applied to the `Scene` within the user content function, e.g. ```rust ui.ctx().layer_transform_to_global(ui.layer_id()) ``` Previously getting the transform like this still kind of worked, but resulted in the user content lagging behind the actual scene position by a single frame, which looks a bit strange. With this PR, the user content using the transform no longer lags by a frame, and matches the scene's transform perfectly. Accessing the `TSTransform` of the `Scene` within the user content function is useful for the case where the user may want to instantiate new `Ui` sublayers that also track the scene (by default, sublayers do *not* apply the same transform as the scene, likely the cause of #5682). With these changes, the user can have sublayers track the scene like so: ```rust let scene_layer = ui.layer_id(); let sub_layer = egui::LayerId::new(scene_layer.order, self.id); ui.ctx().set_sublayer(scene_layer, sub_layer); let scene_transform = ui.ctx().layer_transform_to_global(scene_layer).unwrap(); ui.ctx().set_transform_layer(sub_layer, scene_transform); ``` ### Tested with - `egui_demo_app` scene example. - Local `egui_graph` demo example. --- * Closes * [x] I have followed the instructions in the PR template --- crates/egui/src/containers/scene.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/egui/src/containers/scene.rs b/crates/egui/src/containers/scene.rs index fff4136f2..5b8d97410 100644 --- a/crates/egui/src/containers/scene.rs +++ b/crates/egui/src/containers/scene.rs @@ -160,17 +160,17 @@ impl Scene { // Set a correct global clip rect: local_ui.set_clip_rect(to_global.inverse() * outer_rect); + // Tell egui to apply the transform on the layer: + local_ui + .ctx() + .set_transform_layer(scene_layer_id, *to_global); + // Add the actual contents to the area: let ret = add_contents(&mut local_ui); // This ensures we catch clicks/drags/pans anywhere on the background. local_ui.force_set_min_rect((to_global.inverse() * outer_rect).round_ui()); - // Tell egui to apply the transform on the layer: - local_ui - .ctx() - .set_transform_layer(scene_layer_id, *to_global); - InnerResponse { response: pan_response, inner: ret, From 565966b3c9a904346adaa23db2d54f605a5a906c Mon Sep 17 00:00:00 2001 From: Dmytro Date: Tue, 8 Apr 2025 12:32:47 +0300 Subject: [PATCH 047/129] Fix a broken link in ARCHITECTURE.md (#5853) A quick fix for a broken link: https://github.com/rerun/kittest -> https://github.com/rerun-io/kittest * [X] I have followed the instructions in the PR template From 36e007bd8cf69566dd597775224b40b38ef773fb Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 8 Apr 2025 12:36:43 +0300 Subject: [PATCH 048/129] Add overline option for Table rows (#5637) * Closes no issue, I just needed this for an app and figured it could be useful. * [x] I have followed the instructions in the PR template This PR adds an `overline` option for `egui_extras::TableRow`, which is useful for visually grouping rows. The overline consumes no layout space. A screenshot of the demo app, showing every 7th row getting an overline. Screenshot 2025-01-25 at 14 40 08 --------- Co-authored-by: Emil Ernerfeldt --- crates/egui_demo_lib/src/demo/table_demo.rs | 6 ++++++ .../egui_demo_lib/tests/snapshots/demos/Table.png | 4 ++-- crates/egui_extras/src/layout.rs | 9 +++++++++ crates/egui_extras/src/table.rs | 14 ++++++++++++++ 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/crates/egui_demo_lib/src/demo/table_demo.rs b/crates/egui_demo_lib/src/demo/table_demo.rs index 5c6f42d96..17e19d01d 100644 --- a/crates/egui_demo_lib/src/demo/table_demo.rs +++ b/crates/egui_demo_lib/src/demo/table_demo.rs @@ -13,6 +13,7 @@ enum DemoType { pub struct TableDemo { demo: DemoType, striped: bool, + overline: bool, resizable: bool, clickable: bool, num_rows: usize, @@ -28,6 +29,7 @@ impl Default for TableDemo { Self { demo: DemoType::Manual, striped: true, + overline: true, resizable: true, clickable: true, num_rows: 10_000, @@ -65,6 +67,7 @@ impl crate::View for TableDemo { ui.vertical(|ui| { ui.horizontal(|ui| { ui.checkbox(&mut self.striped, "Striped"); + ui.checkbox(&mut self.overline, "Overline some rows"); ui.checkbox(&mut self.resizable, "Resizable columns"); ui.checkbox(&mut self.clickable, "Clickable rows"); }); @@ -212,6 +215,7 @@ impl TableDemo { let row_height = if is_thick { 30.0 } else { 18.0 }; body.row(row_height, |mut row| { row.set_selected(self.selection.contains(&row_index)); + row.set_overline(self.overline && row_index % 7 == 3); row.col(|ui| { ui.label(row_index.to_string()); @@ -247,6 +251,7 @@ impl TableDemo { }; row.set_selected(self.selection.contains(&row_index)); + row.set_overline(self.overline && row_index % 7 == 3); row.col(|ui| { ui.label(row_index.to_string()); @@ -280,6 +285,7 @@ impl TableDemo { }; row.set_selected(self.selection.contains(&row_index)); + row.set_overline(self.overline && row_index % 7 == 3); row.col(|ui| { ui.label(row_index.to_string()); diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Table.png b/crates/egui_demo_lib/tests/snapshots/demos/Table.png index 6189f25c0..c11788f40 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:13115759157beb57febcff4be6f1710340736108b520e9ad3efb04be3cedcf7b -size 68767 +oid sha256:9446da28768cae0b489e0f6243410a8b3acf0ca2a0b70690d65d2a6221bc25b9 +size 30517 diff --git a/crates/egui_extras/src/layout.rs b/crates/egui_extras/src/layout.rs index fb84169f3..fb62cec40 100644 --- a/crates/egui_extras/src/layout.rs +++ b/crates/egui_extras/src/layout.rs @@ -33,6 +33,7 @@ pub(crate) struct StripLayoutFlags { pub(crate) striped: bool, pub(crate) hovered: bool, pub(crate) selected: bool, + pub(crate) overline: bool, /// Used when we want to accruately measure the size of this cell. pub(crate) sizing_pass: bool, @@ -232,6 +233,14 @@ impl<'l> StripLayout<'l> { child_ui.style_mut().visuals.override_text_color = Some(stroke_color); } + if flags.overline { + child_ui.painter().hline( + max_rect.x_range(), + max_rect.top(), + child_ui.visuals().widgets.noninteractive.bg_stroke, + ); + } + add_cell_contents(&mut child_ui); child_ui diff --git a/crates/egui_extras/src/table.rs b/crates/egui_extras/src/table.rs index 5fca1f9df..172f1bee5 100644 --- a/crates/egui_extras/src/table.rs +++ b/crates/egui_extras/src/table.rs @@ -507,6 +507,7 @@ impl<'a> TableBuilder<'a> { striped: false, hovered: false, selected: false, + overline: false, response: &mut response, }); layout.allocate_rect(); @@ -990,6 +991,7 @@ impl<'a> TableBody<'a> { striped: self.striped && self.row_index % 2 == 0, hovered: self.hovered_row_index == Some(self.row_index), selected: false, + overline: false, response: &mut response, }); self.capture_hover_state(&response, self.row_index); @@ -1071,6 +1073,7 @@ impl<'a> TableBody<'a> { striped: self.striped && (row_index + self.row_index) % 2 == 0, hovered: self.hovered_row_index == Some(row_index), selected: false, + overline: false, response: &mut response, }); self.capture_hover_state(&response, row_index); @@ -1152,6 +1155,7 @@ impl<'a> TableBody<'a> { striped: self.striped && (row_index + self.row_index) % 2 == 0, hovered: self.hovered_row_index == Some(row_index), selected: false, + overline: false, response: &mut response, }); self.capture_hover_state(&response, row_index); @@ -1173,6 +1177,7 @@ impl<'a> TableBody<'a> { height: row_height, striped: self.striped && (row_index + self.row_index) % 2 == 0, hovered: self.hovered_row_index == Some(row_index), + overline: false, selected: false, response: &mut response, }); @@ -1260,6 +1265,7 @@ pub struct TableRow<'a, 'b> { striped: bool, hovered: bool, selected: bool, + overline: bool, response: &'b mut Option, } @@ -1297,6 +1303,7 @@ impl TableRow<'_, '_> { striped: self.striped, hovered: self.hovered, selected: self.selected, + overline: self.overline, sizing_pass: auto_size_this_frame || self.layout.ui.is_sizing_pass(), }; @@ -1333,6 +1340,13 @@ impl TableRow<'_, '_> { self.hovered = hovered; } + /// Set the overline state for this row. The overline is a line above the row, + /// usable for e.g. visually grouping rows. + #[inline] + pub fn set_overline(&mut self, overline: bool) { + self.overline = overline; + } + /// Returns a union of the [`Response`]s of the cells added to the row up to this point. /// /// You need to add at least one row to the table before calling this function. From c6bd30642a78d5ff244b064642c053f62967ef1b Mon Sep 17 00:00:00 2001 From: giuseppeferrari <70653210+giuseppeferrari@users.noreply.github.com> Date: Tue, 8 Apr 2025 11:38:13 +0200 Subject: [PATCH 049/129] Avoid infinite extension of ColorTest inside a Window (#5698) Current implementation of ColorTest infinitely expand horizontally at each redraw if included in a Window. The effect can be see replacing the Panel in the ColorTestApp::update with a Window: ``` egui::CentralPanel::default().show(ctx, |ui| { egui::Window::new("Colors").vscroll(true).show(ctx, |ui| { if frame.is_web() { ui.label( "NOTE: Some old browsers stuck on WebGL1 without sRGB support will not pass the color test.", ); ui.separator(); } egui::ScrollArea::both().auto_shrink(false).show(ui, |ui| { self.color_test.ui(ui); }); self.color_test.ui(ui); }); ``` The cause is the is the _pixel_test_strokes_ function that, at each redraw, tries to expand the target rectangle of 2.0 points in each direction. --- crates/egui_demo_lib/src/rendering_test.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui_demo_lib/src/rendering_test.rs b/crates/egui_demo_lib/src/rendering_test.rs index 14714cca7..ddf07f04f 100644 --- a/crates/egui_demo_lib/src/rendering_test.rs +++ b/crates/egui_demo_lib/src/rendering_test.rs @@ -469,7 +469,7 @@ fn pixel_test_strokes(ui: &mut Ui) { let thickness_points = thickness_pixels / pixels_per_point; let num_squares = (pixels_per_point * 10.0).round().max(10.0) as u32; let size_pixels = vec2(ui.min_size().x, num_squares as f32 + thickness_pixels * 2.0); - let size_points = size_pixels / pixels_per_point + Vec2::splat(2.0); + let size_points = size_pixels / pixels_per_point; let (response, painter) = ui.allocate_painter(size_points, Sense::hover()); let mut cursor_pixel = Pos2::new( From 7055141c18c11e858f63aab2c36fe7595a1929fb Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Wed, 9 Apr 2025 12:11:46 +0200 Subject: [PATCH 050/129] Update failing snapshot tests (#5894) For some reason the pipeline in https://github.com/emilk/egui/pull/5698 succeeded even though the snapshots should have been updated. This updates the snapshots. --- .../egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png | 4 ++-- .../egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png | 4 ++-- .../egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png | 4 ++-- .../egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png | 4 ++-- .../egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png | 4 ++-- .../egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) 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 1cef15b72..51b8d8540 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:5a1f0d0759458017127d93278b89278af20fdc57c7747652ac6554f24cc708f2 -size 552939 +oid sha256:9f6cf5b14056522d06f0cb1e56bafd7e5ab7a9033eb358748d43d748bb0ceef1 +size 553177 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 f1005193c..3e73d0abb 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:7948ced20e3f62b8356fc978ca7b12f8522b7c15716409cc661c0ebd2f12047f -size 770062 +oid sha256:fd3bd1f64995db34a14dbc860ae8b8e269073ed7b8f10d10ce8f99b613cfc999 +size 769357 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 9e1b69fee..4b9a5194e 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:334f52bfee27f9c467de739696fd7ce7c48ec9013e315dc4b2e61eee58f11287 -size 907997 +oid sha256:f12e6145f3a1c3fda6dede3daeb0e52ed2bffb35531d823133224a477798a14a +size 907800 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 bd093a19c..c5f324368 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:d31f018cdabf92966b5636d9aef7f11ef1a0383884867e819e7ec99c0474e872 -size 1025013 +oid sha256:05bdcfd2c34b6d7badede14f5495dce34e5e9cfe421314f40dcea15e9f865736 +size 1024735 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 b9a40fab3..8e4481d06 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:e18f7ddadd53e16d04f191268747f244c98d0ea0f4cb9c0ea299f5da7affbc58 -size 1139723 +oid sha256:8365c89f6b823f01464a9310bab7717bf25305b335cdeecf21711c7dca9f053f +size 1140082 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 935dcc33d..de8c8b321 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:09a49cb9da7269bec6eef30c46a0bc85df6538d6e31dc0d0ff2758dbee45f3d8 -size 1291804 +oid sha256:b38021057ec6b5bb39c41bd4afaf5e9ff38687216d52d5bba8cbf7b6fdfe9a4f +size 1291518 From a8e0c56a8f38b8c26264733bc659977ba4178338 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 13 Apr 2025 19:12:22 +0200 Subject: [PATCH 051/129] Update crossbeam-channel to resolve RUSTSEC --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4276896ba..e9df7a2ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1004,9 +1004,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.13" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ "crossbeam-utils", ] From 0f1d6c281894647b2fdd1fcb311eb930ab1a269c Mon Sep 17 00:00:00 2001 From: Christopher Cerne Date: Mon, 14 Apr 2025 05:13:17 -0400 Subject: [PATCH 052/129] Support SVG Text Rendering in egui_extras (#5979) **Added** * Create `svg_text` feature flag to support text rendering & loading of system fonts. **Changed** * Updates `resvg` to `0.45`. * Adds `usvg::Options` field to the `SvgLoader` structure. * Change function signatures to support passing `usvg::Options` to downstream `load_svg_bytes_with_size`. **Additional Info** * I used this PR as a reference: https://github.com/emilk/egui/pull/4659. @xNWP can you see if this adequately resolves your concern from your original PR? * Closes https://github.com/emilk/egui/issues/5977 (we may want to open another issue for my other thoughts in this issue) * Also, I would like to thank @xNWP and their original PR for being a good reference for this one. * [x] I have followed the instructions in the PR template --- Cargo.lock | 177 ++++++++++++++----- crates/egui_extras/Cargo.toml | 5 +- crates/egui_extras/src/image.rs | 81 +++++---- crates/egui_extras/src/loaders/svg_loader.rs | 26 ++- 4 files changed, 205 insertions(+), 84 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e9df7a2ee..6d1d01b1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -959,6 +959,15 @@ dependencies = [ "libc", ] +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + [[package]] name = "crc32fast" version = "1.4.2" @@ -1662,6 +1671,28 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" +[[package]] +name = "fontconfig-parser" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1fcfcd44ca6e90c921fee9fa665d530b21ef1327a4c1a6c5250ea44b776ada7" +dependencies = [ + "roxmltree", +] + +[[package]] +name = "fontdb" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" +dependencies = [ + "fontconfig-parser", + "log", + "slotmap", + "tinyvec", + "ttf-parser", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -2261,9 +2292,9 @@ dependencies = [ [[package]] name = "imagesize" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284" +checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" [[package]] name = "immutable-chunkmap" @@ -2394,11 +2425,12 @@ dependencies = [ [[package]] name = "kurbo" -version = "0.9.5" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd85a5776cd9500c2e2059c8c76c3b01528566b7fcbaf8098b55a33fc298849b" +checksum = "89234b2cc610a7dd927ebde6b41dd1a5d4214cffaef4cf1fb2195d592f92518f" dependencies = [ "arrayvec", + "smallvec", ] [[package]] @@ -2423,6 +2455,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" + [[package]] name = "libredox" version = "0.1.3" @@ -3366,12 +3404,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "rctree" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b42e27ef78c35d3998403c1d26f3efd9e135d3e5121b0a4845cc5cc27547f4f" - [[package]] name = "redox_syscall" version = "0.4.1" @@ -3438,9 +3470,9 @@ checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" [[package]] name = "resvg" -version = "0.37.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cadccb3d99a9efb8e5e00c16fbb732cbe400db2ec7fc004697ee7d97d86cf1f4" +checksum = "dd43d1c474e9dadf09a8fdf22d713ba668b499b5117b9b9079500224e26b5b29" dependencies = [ "log", "pico-args", @@ -3511,9 +3543,9 @@ dependencies = [ [[package]] name = "roxmltree" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" [[package]] name = "rustc-demangle" @@ -3578,6 +3610,24 @@ version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" +[[package]] +name = "rustybuzz" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" +dependencies = [ + "bitflags 2.8.0", + "bytemuck", + "core_maths", + "log", + "smallvec", + "ttf-parser", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + [[package]] name = "ryu" version = "1.0.18" @@ -3731,9 +3781,9 @@ dependencies = [ [[package]] name = "siphasher" -version = "0.3.11" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "slab" @@ -3864,9 +3914,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "svgtypes" -version = "0.13.0" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e44e288cd960318917cbd540340968b90becc8bc81f171345d706e7a89d9d70" +checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" dependencies = [ "kurbo", "siphasher", @@ -4107,6 +4157,21 @@ dependencies = [ "serde_json", ] +[[package]] +name = "tinyvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "toml_datetime" version = "0.6.8" @@ -4160,6 +4225,9 @@ name = "ttf-parser" version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5902c5d130972a0000f60860bfbf46f7ca3db5391eddfedd1b8728bd9dc96c0e" +dependencies = [ + "core_maths", +] [[package]] name = "type-map" @@ -4187,18 +4255,54 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" + +[[package]] +name = "unicode-ccc" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" + [[package]] name = "unicode-ident" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + +[[package]] +name = "unicode-script" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb421b350c9aff471779e262955939f565ec18b86c15364e6bdf0d662ca7c1f" + [[package]] name = "unicode-segmentation" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-vo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" + [[package]] name = "unicode-width" version = "0.1.14" @@ -4267,46 +4371,29 @@ dependencies = [ [[package]] name = "usvg" -version = "0.37.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b0a51b72ab80ca511d126b77feeeb4fb1e972764653e61feac30adc161a756" -dependencies = [ - "base64 0.21.7", - "log", - "pico-args", - "usvg-parser", - "usvg-tree", - "xmlwriter", -] - -[[package]] -name = "usvg-parser" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bd4e3c291f45d152929a31f0f6c819245e2921bfd01e7bd91201a9af39a2bdc" +checksum = "2ac8e0e3e4696253dc06167990b3fe9a2668ab66270adf949a464db4088cb354" dependencies = [ + "base64 0.22.1", "data-url", "flate2", + "fontdb", "imagesize", "kurbo", "log", + "pico-args", "roxmltree", + "rustybuzz", "simplecss", "siphasher", - "svgtypes", - "usvg-tree", -] - -[[package]] -name = "usvg-tree" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee3d202ebdb97a6215604b8f5b4d6ef9024efd623cf2e373a6416ba976ec7d3" -dependencies = [ - "rctree", "strict-num", "svgtypes", "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", ] [[package]] diff --git a/crates/egui_extras/Cargo.toml b/crates/egui_extras/Cargo.toml index 7099d95d8..2e7b1bd2f 100644 --- a/crates/egui_extras/Cargo.toml +++ b/crates/egui_extras/Cargo.toml @@ -62,6 +62,9 @@ serde = ["egui/serde", "enum-map/serde", "dep:serde"] ## Support loading svg images. svg = ["resvg"] +## Support rendering text in svg images. +svg_text = ["svg", "resvg/text", "resvg/system-fonts"] + ## Enable better syntax highlighting using [`syntect`](https://docs.rs/syntect). syntect = ["dep:syntect"] @@ -101,7 +104,7 @@ syntect = { version = "5", optional = true, default-features = false, features = ] } # svg feature -resvg = { version = "0.37", optional = true, default-features = false } +resvg = { version = "0.45", optional = true, default-features = false } # http feature ehttp = { version = "0.5", optional = true, default-features = false } diff --git a/crates/egui_extras/src/image.rs b/crates/egui_extras/src/image.rs index 46d9df170..d7aa3126b 100644 --- a/crates/egui_extras/src/image.rs +++ b/crates/egui_extras/src/image.rs @@ -63,8 +63,12 @@ impl RetainedImage { /// # Errors /// On invalid image #[cfg(feature = "svg")] - pub fn from_svg_bytes(debug_name: impl Into, svg_bytes: &[u8]) -> Result { - Self::from_svg_bytes_with_size(debug_name, svg_bytes, None) + pub fn from_svg_bytes( + debug_name: impl Into, + svg_bytes: &[u8], + options: &resvg::usvg::Options<'_>, + ) -> Result { + Self::from_svg_bytes_with_size(debug_name, svg_bytes, None, options) } /// Pass in the str of an SVG that you've loaded. @@ -72,8 +76,12 @@ impl RetainedImage { /// # Errors /// On invalid image #[cfg(feature = "svg")] - pub fn from_svg_str(debug_name: impl Into, svg_str: &str) -> Result { - Self::from_svg_bytes(debug_name, svg_str.as_bytes()) + pub fn from_svg_str( + debug_name: impl Into, + svg_str: &str, + options: &resvg::usvg::Options<'_>, + ) -> Result { + Self::from_svg_bytes(debug_name, svg_str.as_bytes(), options) } /// Pass in the bytes of an SVG that you've loaded @@ -86,10 +94,11 @@ impl RetainedImage { debug_name: impl Into, svg_bytes: &[u8], size_hint: Option, + options: &resvg::usvg::Options<'_>, ) -> Result { Ok(Self::from_color_image( debug_name, - load_svg_bytes_with_size(svg_bytes, size_hint)?, + load_svg_bytes_with_size(svg_bytes, size_hint, options)?, )) } @@ -227,8 +236,11 @@ pub fn load_image_bytes(image_bytes: &[u8]) -> Result Result { - load_svg_bytes_with_size(svg_bytes, None) +pub fn load_svg_bytes( + svg_bytes: &[u8], + options: &resvg::usvg::Options<'_>, +) -> Result { + load_svg_bytes_with_size(svg_bytes, None, options) } /// Load an SVG and rasterize it into an egui image with a scaling parameter. @@ -241,48 +253,47 @@ pub fn load_svg_bytes(svg_bytes: &[u8]) -> Result { pub fn load_svg_bytes_with_size( svg_bytes: &[u8], size_hint: Option, + options: &resvg::usvg::Options<'_>, ) -> Result { use resvg::tiny_skia::{IntSize, Pixmap}; - use resvg::usvg::{Options, Tree, TreeParsing}; + use resvg::usvg::{Transform, Tree}; profiling::function_scope!(); - let opt = Options::default(); + let rtree = Tree::from_data(svg_bytes, options).map_err(|err| err.to_string())?; - let mut rtree = Tree::from_data(svg_bytes, &opt).map_err(|err| err.to_string())?; - - let mut size = rtree.size.to_int_size(); - match size_hint { - None => (), - Some(SizeHint::Size(w, h)) => { - size = size.scale_to( - IntSize::from_wh(w, h).ok_or_else(|| format!("Failed to scale SVG to {w}x{h}"))?, - ); - } - Some(SizeHint::Height(h)) => { - size = size - .scale_to_height(h) - .ok_or_else(|| format!("Failed to scale SVG to height {h}"))?; - } - Some(SizeHint::Width(w)) => { - size = size - .scale_to_width(w) - .ok_or_else(|| format!("Failed to scale SVG to width {w}"))?; - } + let size = rtree.size().to_int_size(); + let scaled_size = match size_hint { + None => size, + Some(SizeHint::Size(w, h)) => size.scale_to( + IntSize::from_wh(w, h).ok_or_else(|| format!("Failed to scale SVG to {w}x{h}"))?, + ), + Some(SizeHint::Height(h)) => size + .scale_to_height(h) + .ok_or_else(|| format!("Failed to scale SVG to height {h}"))?, + Some(SizeHint::Width(w)) => size + .scale_to_width(w) + .ok_or_else(|| format!("Failed to scale SVG to width {w}"))?, Some(SizeHint::Scale(z)) => { let z_inner = z.into_inner(); - size = size - .scale_by(z_inner) - .ok_or_else(|| format!("Failed to scale SVG by {z_inner}"))?; + size.scale_by(z_inner) + .ok_or_else(|| format!("Failed to scale SVG by {z_inner}"))? } }; - let (w, h) = (size.width(), size.height()); + + let (w, h) = (scaled_size.width(), scaled_size.height()); let mut pixmap = Pixmap::new(w, h).ok_or_else(|| format!("Failed to create SVG Pixmap of size {w}x{h}"))?; - rtree.size = size.to_size(); - resvg::Tree::from_usvg(&rtree).render(Default::default(), &mut pixmap.as_mut()); + resvg::render( + &rtree, + Transform::from_scale( + w as f32 / size.width() as f32, + h as f32 / size.height() as f32, + ), + &mut pixmap.as_mut(), + ); let image = egui::ColorImage::from_rgba_unmultiplied([w as _, h as _], pixmap.data()); diff --git a/crates/egui_extras/src/loaders/svg_loader.rs b/crates/egui_extras/src/loaders/svg_loader.rs index 2a3f15569..4c33281fe 100644 --- a/crates/egui_extras/src/loaders/svg_loader.rs +++ b/crates/egui_extras/src/loaders/svg_loader.rs @@ -10,9 +10,9 @@ use egui::{ type Entry = Result, String>; -#[derive(Default)] pub struct SvgLoader { cache: Mutex, SizeHint), Entry>>, + options: resvg::usvg::Options<'static>, } impl SvgLoader { @@ -27,6 +27,22 @@ fn is_supported(uri: &str) -> bool { ext == "svg" } +impl Default for SvgLoader { + fn default() -> Self { + // opt is mutated when `svg_text` feature flag is enabled + #[allow(unused_mut)] + let mut options = resvg::usvg::Options::default(); + + #[cfg(feature = "svg_text")] + options.fontdb_mut().load_system_fonts(); + + Self { + cache: Mutex::new(HashMap::default()), + options, + } + } +} + impl ImageLoader for SvgLoader { fn id(&self) -> &str { Self::ID @@ -48,8 +64,12 @@ impl ImageLoader for SvgLoader { match ctx.try_load_bytes(uri) { Ok(BytesPoll::Ready { bytes, .. }) => { log::trace!("started loading {uri:?}"); - let result = crate::image::load_svg_bytes_with_size(&bytes, Some(size_hint)) - .map(Arc::new); + let result = crate::image::load_svg_bytes_with_size( + &bytes, + Some(size_hint), + &self.options, + ) + .map(Arc::new); log::trace!("finished loading {uri:?}"); cache.insert((Cow::Owned(uri.to_owned()), size_hint), result.clone()); match result { From 501905b60df88ff84fe6ea9a0d51543b6cc4638f Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Wed, 16 Apr 2025 18:58:58 +0200 Subject: [PATCH 053/129] Add tests for layout and visuals of most egui widgets (#6752) This is mostly in preparation for #5830 where I want to ensure that I don't introduce any regressions --- Cargo.lock | 10 + crates/egui_kittest/src/lib.rs | 64 ++- tests/egui_tests/Cargo.toml | 15 + .../tests/snapshots/layout/button.png | 3 + .../tests/snapshots/layout/button_image.png | 3 + .../layout/button_image_shortcut.png | 3 + .../tests/snapshots/layout/checkbox.png | 3 + .../snapshots/layout/checkbox_checked.png | 3 + .../tests/snapshots/layout/drag_value.png | 3 + .../tests/snapshots/layout/radio.png | 3 + .../tests/snapshots/layout/radio_checked.png | 3 + .../snapshots/layout/selectable_value.png | 3 + .../layout/selectable_value_selected.png | 3 + .../tests/snapshots/layout/slider.png | 3 + .../tests/snapshots/layout/text_edit.png | 3 + .../tests/snapshots/visuals/button.png | 3 + .../tests/snapshots/visuals/button_image.png | 3 + .../visuals/button_image_shortcut.png | 3 + .../button_image_shortcut_selected.png | 3 + .../tests/snapshots/visuals/checkbox.png | 3 + .../snapshots/visuals/checkbox_checked.png | 3 + .../tests/snapshots/visuals/drag_value.png | 3 + .../tests/snapshots/visuals/radio.png | 3 + .../tests/snapshots/visuals/radio_checked.png | 3 + .../snapshots/visuals/selectable_value.png | 3 + .../visuals/selectable_value_selected.png | 3 + .../tests/snapshots/visuals/slider.png | 3 + .../tests/snapshots/visuals/text_edit.png | 3 + tests/egui_tests/tests/test_widgets.rs | 370 ++++++++++++++++++ 29 files changed, 518 insertions(+), 16 deletions(-) create mode 100644 tests/egui_tests/Cargo.toml create mode 100644 tests/egui_tests/tests/snapshots/layout/button.png create mode 100644 tests/egui_tests/tests/snapshots/layout/button_image.png create mode 100644 tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png create mode 100644 tests/egui_tests/tests/snapshots/layout/checkbox.png create mode 100644 tests/egui_tests/tests/snapshots/layout/checkbox_checked.png create mode 100644 tests/egui_tests/tests/snapshots/layout/drag_value.png create mode 100644 tests/egui_tests/tests/snapshots/layout/radio.png create mode 100644 tests/egui_tests/tests/snapshots/layout/radio_checked.png create mode 100644 tests/egui_tests/tests/snapshots/layout/selectable_value.png create mode 100644 tests/egui_tests/tests/snapshots/layout/selectable_value_selected.png create mode 100644 tests/egui_tests/tests/snapshots/layout/slider.png create mode 100644 tests/egui_tests/tests/snapshots/layout/text_edit.png create mode 100644 tests/egui_tests/tests/snapshots/visuals/button.png create mode 100644 tests/egui_tests/tests/snapshots/visuals/button_image.png create mode 100644 tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png create mode 100644 tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png create mode 100644 tests/egui_tests/tests/snapshots/visuals/checkbox.png create mode 100644 tests/egui_tests/tests/snapshots/visuals/checkbox_checked.png create mode 100644 tests/egui_tests/tests/snapshots/visuals/drag_value.png create mode 100644 tests/egui_tests/tests/snapshots/visuals/radio.png create mode 100644 tests/egui_tests/tests/snapshots/visuals/radio_checked.png create mode 100644 tests/egui_tests/tests/snapshots/visuals/selectable_value.png create mode 100644 tests/egui_tests/tests/snapshots/visuals/selectable_value_selected.png create mode 100644 tests/egui_tests/tests/snapshots/visuals/slider.png create mode 100644 tests/egui_tests/tests/snapshots/visuals/text_edit.png create mode 100644 tests/egui_tests/tests/test_widgets.rs diff --git a/Cargo.lock b/Cargo.lock index 6d1d01b1d..c2d246045 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1423,6 +1423,16 @@ dependencies = [ "wgpu", ] +[[package]] +name = "egui_tests" +version = "0.31.1" +dependencies = [ + "egui", + "egui_extras", + "egui_kittest", + "image", +] + [[package]] name = "ehttp" version = "0.5.0" diff --git a/crates/egui_kittest/src/lib.rs b/crates/egui_kittest/src/lib.rs index 59bd5c056..38521d101 100644 --- a/crates/egui_kittest/src/lib.rs +++ b/crates/egui_kittest/src/lib.rs @@ -274,6 +274,7 @@ impl<'a, State> Harness<'a, State> { /// /// See also: /// - [`Harness::try_run`]. + /// - [`Harness::try_run_realtime`]. /// - [`Harness::run_ok`]. /// - [`Harness::step`]. /// - [`Harness::run_steps`]. @@ -287,6 +288,27 @@ impl<'a, State> Harness<'a, State> { } } + fn _try_run(&mut self, sleep: bool) -> Result { + let mut steps = 0; + loop { + steps += 1; + self.step(); + // We only care about immediate repaints + if self.root_viewport_output().repaint_delay != Duration::ZERO { + break; + } else if sleep { + std::thread::sleep(Duration::from_secs_f32(self.step_dt)); + } + if steps > self.max_steps { + return Err(ExceededMaxStepsError { + max_steps: self.max_steps, + repaint_causes: self.ctx.repaint_causes(), + }); + } + } + Ok(steps) + } + /// Run until /// - all animations are done /// - no more repaints are requested @@ -302,23 +324,9 @@ impl<'a, State> Harness<'a, State> { /// - [`Harness::run_ok`]. /// - [`Harness::step`]. /// - [`Harness::run_steps`]. + /// - [`Harness::try_run_realtime`]. pub fn try_run(&mut self) -> Result { - let mut steps = 0; - loop { - steps += 1; - self.step(); - // We only care about immediate repaints - if self.root_viewport_output().repaint_delay != Duration::ZERO { - break; - } - if steps > self.max_steps { - return Err(ExceededMaxStepsError { - max_steps: self.max_steps, - repaint_causes: self.ctx.repaint_causes(), - }); - } - } - Ok(steps) + self._try_run(false) } /// Run until @@ -333,10 +341,34 @@ impl<'a, State> Harness<'a, State> { /// - [`Harness::try_run`]. /// - [`Harness::step`]. /// - [`Harness::run_steps`]. + /// - [`Harness::try_run_realtime`]. pub fn run_ok(&mut self) -> Option { self.try_run().ok() } + /// Run multiple frames, sleeping for [`HarnessBuilder::with_step_dt`] between frames. + /// + /// This is useful to e.g. wait for an async operation to complete (e.g. loading of images). + /// Runs until + /// - all animations are done + /// - no more repaints are requested + /// - the maximum number of steps is reached (See [`HarnessBuilder::with_max_steps`]) + /// + /// Returns the number of steps that were run. + /// + /// # Errors + /// Returns an error if the maximum number of steps is exceeded. + /// + /// See also: + /// - [`Harness::run`]. + /// - [`Harness::run_ok`]. + /// - [`Harness::step`]. + /// - [`Harness::run_steps`]. + /// - [`Harness::try_run`]. + pub fn try_run_realtime(&mut self) -> Result { + self._try_run(true) + } + /// Run a number of steps. /// Equivalent to calling [`Harness::step`] x times. pub fn run_steps(&mut self, steps: usize) { diff --git a/tests/egui_tests/Cargo.toml b/tests/egui_tests/Cargo.toml new file mode 100644 index 000000000..ee2531831 --- /dev/null +++ b/tests/egui_tests/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "egui_tests" +edition.workspace = true +license.workspace = true +rust-version.workspace = true +version.workspace = true + +[dev-dependencies] +egui = { workspace = true, default-features = true } +egui_kittest = { workspace = true, features = ["snapshot", "wgpu"] } +egui_extras = { workspace = true, features = ["image"]} +image = { workspace = true, features = ["png"] } + +[lints] +workspace = true diff --git a/tests/egui_tests/tests/snapshots/layout/button.png b/tests/egui_tests/tests/snapshots/layout/button.png new file mode 100644 index 000000000..7232c72c8 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/layout/button.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ccd7bdd86e587bcf0577c92e10ed7c3c35195e37df109a84554ceb30a434768d +size 315482 diff --git a/tests/egui_tests/tests/snapshots/layout/button_image.png b/tests/egui_tests/tests/snapshots/layout/button_image.png new file mode 100644 index 000000000..737f0670c --- /dev/null +++ b/tests/egui_tests/tests/snapshots/layout/button_image.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:975c279d6da2a2cb000df72bf5d9f3bdd200bb20adc00e29e8fd9ed4d2c6f6b1 +size 340923 diff --git a/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png b/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png new file mode 100644 index 000000000..7dbda11d9 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad14068e60fa678ee749925dd3713ee2b12a83ec1bca9c413bdeb9bc27d8ac20 +size 407795 diff --git a/tests/egui_tests/tests/snapshots/layout/checkbox.png b/tests/egui_tests/tests/snapshots/layout/checkbox.png new file mode 100644 index 000000000..b8c014727 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/layout/checkbox.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:996e02c1c10a0c76fa295160d117aceb764ef506608b151bafbdf263106dbe57 +size 385129 diff --git a/tests/egui_tests/tests/snapshots/layout/checkbox_checked.png b/tests/egui_tests/tests/snapshots/layout/checkbox_checked.png new file mode 100644 index 000000000..66ae8115f --- /dev/null +++ b/tests/egui_tests/tests/snapshots/layout/checkbox_checked.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aaf9b032037d0708894e568cc8e256b32be9cfb586eaffdc6167143b85562b37 +size 415016 diff --git a/tests/egui_tests/tests/snapshots/layout/drag_value.png b/tests/egui_tests/tests/snapshots/layout/drag_value.png new file mode 100644 index 000000000..a9a64c558 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/layout/drag_value.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:043be3ece0697ea7114b7bd743e5c958610ae38ac359b6f8120886edff8541d8 +size 239522 diff --git a/tests/egui_tests/tests/snapshots/layout/radio.png b/tests/egui_tests/tests/snapshots/layout/radio.png new file mode 100644 index 000000000..1e1a1faf3 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/layout/radio.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f7fbeeba8ae9e34c5400727690ac7941e2711f72f2dc23e3342cb06904e4a35 +size 335775 diff --git a/tests/egui_tests/tests/snapshots/layout/radio_checked.png b/tests/egui_tests/tests/snapshots/layout/radio_checked.png new file mode 100644 index 000000000..323426ee9 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/layout/radio_checked.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:96ae7be40161b0b42959b44c8f72b62fd2cd4b3b463fc7d5bcd02ead445edca1 +size 355550 diff --git a/tests/egui_tests/tests/snapshots/layout/selectable_value.png b/tests/egui_tests/tests/snapshots/layout/selectable_value.png new file mode 100644 index 000000000..42794b19e --- /dev/null +++ b/tests/egui_tests/tests/snapshots/layout/selectable_value.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4991fdf58542ca14162cbd7f59b6a30d6c3d752a1215cc1890359bc3a1eb23c9 +size 388912 diff --git a/tests/egui_tests/tests/snapshots/layout/selectable_value_selected.png b/tests/egui_tests/tests/snapshots/layout/selectable_value_selected.png new file mode 100644 index 000000000..554bbbf41 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/layout/selectable_value_selected.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ac8cbcdeed098d52009be77c8815931553d979f5aaf0baf0a9296daf6373605 +size 402699 diff --git a/tests/egui_tests/tests/snapshots/layout/slider.png b/tests/egui_tests/tests/snapshots/layout/slider.png new file mode 100644 index 000000000..9017347f2 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/layout/slider.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:605091767a73a934981d10d0ed59ff561772ed61e7691303b75b35ae01163ecc +size 336722 diff --git a/tests/egui_tests/tests/snapshots/layout/text_edit.png b/tests/egui_tests/tests/snapshots/layout/text_edit.png new file mode 100644 index 000000000..fae07202c --- /dev/null +++ b/tests/egui_tests/tests/snapshots/layout/text_edit.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:465e34d94bf734a2a7a1e8e4a71ce64c908c737a7c4fa2a6f812351f2aaa6808 +size 233018 diff --git a/tests/egui_tests/tests/snapshots/visuals/button.png b/tests/egui_tests/tests/snapshots/visuals/button.png new file mode 100644 index 000000000..8c8e9630c --- /dev/null +++ b/tests/egui_tests/tests/snapshots/visuals/button.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:99f64e581b97df6694cb7c85ee7728a955e3c1a851ab660e8b6091eee1885bbe +size 9719 diff --git a/tests/egui_tests/tests/snapshots/visuals/button_image.png b/tests/egui_tests/tests/snapshots/visuals/button_image.png new file mode 100644 index 000000000..c71c2aeb0 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/visuals/button_image.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d39ec25b91f5f5d68305d2cb7cc0285d715fe30ccbd66369efbe7327d1899b52 +size 10753 diff --git a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png new file mode 100644 index 000000000..42f8ff02a --- /dev/null +++ b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:46d86987ba895ead9b28efcc37e1b4374f34eedebac83d1db9eaa8e5a3202ee3 +size 13203 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 new file mode 100644 index 000000000..3ff34c6be --- /dev/null +++ b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:09c5904877c8895d3ad41b7082019ef87db40c6a91ad47401bb9b8ac79a62bdc +size 12914 diff --git a/tests/egui_tests/tests/snapshots/visuals/checkbox.png b/tests/egui_tests/tests/snapshots/visuals/checkbox.png new file mode 100644 index 000000000..a0e6e18d7 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/visuals/checkbox.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1cd5e9ad416c3a0b6824debc343f196e6db90509fd201c60c7c1f9b022f37c1d +size 12322 diff --git a/tests/egui_tests/tests/snapshots/visuals/checkbox_checked.png b/tests/egui_tests/tests/snapshots/visuals/checkbox_checked.png new file mode 100644 index 000000000..40852f3c2 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/visuals/checkbox_checked.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e03cf99a3d28f73d4a72c0e616dc54198663b94bf5cffda694cf4eb4dee01be8 +size 13445 diff --git a/tests/egui_tests/tests/snapshots/visuals/drag_value.png b/tests/egui_tests/tests/snapshots/visuals/drag_value.png new file mode 100644 index 000000000..dbe3c13b6 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/visuals/drag_value.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e86a37c7b259a6bad61897545d927d75e8307916dc78d256e4d33c410fcd6876 +size 7306 diff --git a/tests/egui_tests/tests/snapshots/visuals/radio.png b/tests/egui_tests/tests/snapshots/visuals/radio.png new file mode 100644 index 000000000..9c14f3032 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/visuals/radio.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:583fa78f79b39522a44c871642114ead9ed1d177bb8a3807d2c9e2cd89bf0b44 +size 11076 diff --git a/tests/egui_tests/tests/snapshots/visuals/radio_checked.png b/tests/egui_tests/tests/snapshots/visuals/radio_checked.png new file mode 100644 index 000000000..a42ad5012 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/visuals/radio_checked.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d1a172cfadc91467529e5546e686673be73ba0071a55d55abc7a41fb1d07214d +size 11700 diff --git a/tests/egui_tests/tests/snapshots/visuals/selectable_value.png b/tests/egui_tests/tests/snapshots/visuals/selectable_value.png new file mode 100644 index 000000000..c2cbd7334 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/visuals/selectable_value.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eed80e11dd3ba478217cf004654934214b522ea666074e023dda9a323473615a +size 12452 diff --git a/tests/egui_tests/tests/snapshots/visuals/selectable_value_selected.png b/tests/egui_tests/tests/snapshots/visuals/selectable_value_selected.png new file mode 100644 index 000000000..81f995515 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/visuals/selectable_value_selected.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ef91dfedc74cae59099bce32b2e42cb04649e84442e8010282a9c1ff2a7f2c8 +size 12469 diff --git a/tests/egui_tests/tests/snapshots/visuals/slider.png b/tests/egui_tests/tests/snapshots/visuals/slider.png new file mode 100644 index 000000000..6c8348559 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/visuals/slider.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1892358a4552af3f529141d314cd18e4cf55a629d870798278a5470e3e0a8a94 +size 11030 diff --git a/tests/egui_tests/tests/snapshots/visuals/text_edit.png b/tests/egui_tests/tests/snapshots/visuals/text_edit.png new file mode 100644 index 000000000..5f2a64b8d --- /dev/null +++ b/tests/egui_tests/tests/snapshots/visuals/text_edit.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7300a0b88d4fdb6c1e543bfaf50e8964b2f84aaaf8197267b671d0cf3c8da30a +size 7033 diff --git a/tests/egui_tests/tests/test_widgets.rs b/tests/egui_tests/tests/test_widgets.rs new file mode 100644 index 000000000..264b0052f --- /dev/null +++ b/tests/egui_tests/tests/test_widgets.rs @@ -0,0 +1,370 @@ +use egui::load::SizedTexture; +use egui::{ + include_image, Align, Button, Color32, ColorImage, Direction, DragValue, Event, Grid, Layout, + PointerButton, Pos2, Response, Slider, Stroke, StrokeKind, TextWrapMode, TextureHandle, + TextureOptions, Ui, UiBuilder, Vec2, Widget, +}; +use egui_kittest::kittest::{by, Node, Queryable}; +use egui_kittest::{Harness, SnapshotResult, SnapshotResults}; + +#[test] +fn widget_tests() { + let mut results = SnapshotResults::new(); + + test_widget("button", |ui| ui.button("Button"), &mut results); + test_widget( + "button_image", + |ui| { + Button::image_and_text( + include_image!("../../../crates/eframe/data/icon.png"), + "Button", + ) + .ui(ui) + }, + &mut results, + ); + test_widget( + "button_image_shortcut", + |ui| { + Button::image_and_text( + include_image!("../../../crates/eframe/data/icon.png"), + "Open", + ) + .shortcut_text("⌘O") + .ui(ui) + }, + &mut results, + ); + results.add(VisualTests::test("button_image_shortcut_selected", |ui| { + Button::image_and_text( + include_image!("../../../crates/eframe/data/icon.png"), + "Open", + ) + .shortcut_text("⌘O") + .selected(true) + .ui(ui) + })); + + test_widget( + "selectable_value", + |ui| ui.selectable_label(false, "Selectable"), + &mut results, + ); + test_widget( + "selectable_value_selected", + |ui| ui.selectable_label(true, "Selectable"), + &mut results, + ); + + test_widget( + "checkbox", + |ui| ui.checkbox(&mut false, "Checkbox"), + &mut results, + ); + test_widget( + "checkbox_checked", + |ui| ui.checkbox(&mut true, "Checkbox"), + &mut results, + ); + test_widget("radio", |ui| ui.radio(false, "Radio"), &mut results); + test_widget("radio_checked", |ui| ui.radio(true, "Radio"), &mut results); + + test_widget( + "drag_value", + |ui| DragValue::new(&mut 12.0).ui(ui), + &mut results, + ); + + test_widget( + "text_edit", + |ui| { + ui.spacing_mut().text_edit_width = 45.0; + ui.text_edit_singleline(&mut "Hi!".to_owned()) + }, + &mut results, + ); + + test_widget( + "slider", + |ui| { + ui.spacing_mut().slider_width = 45.0; + Slider::new(&mut 12.0, 0.0..=100.0).ui(ui) + }, + &mut results, + ); +} + +fn test_widget(name: &str, mut w: impl FnMut(&mut Ui) -> Response, results: &mut SnapshotResults) { + results.add(test_widget_layout(name, &mut w)); + results.add(VisualTests::test(name, &mut w)); +} + +fn test_widget_layout(name: &str, mut w: impl FnMut(&mut Ui) -> Response) -> SnapshotResult { + let test_size = Vec2::new(110.0, 45.0); + + struct Row { + main_dir: Direction, + main_align: Align, + main_justify: bool, + } + + struct Col { + cross_align: Align, + cross_justify: bool, + } + + let mut rows = Vec::new(); + let mut cols = Vec::new(); + + for main_justify in [false, true] { + for main_dir in [ + Direction::LeftToRight, + Direction::TopDown, + Direction::RightToLeft, + Direction::BottomUp, + ] { + for main_align in [Align::Min, Align::Center, Align::Max] { + rows.push(Row { + main_dir, + main_align, + main_justify, + }); + } + } + } + + for cross_justify in [false, true] { + for cross_align in [Align::Min, Align::Center, Align::Max] { + cols.push(Col { + cross_align, + cross_justify, + }); + } + } + + let mut harness = Harness::builder().build_ui(|ui| { + egui_extras::install_image_loaders(ui.ctx()); + + { + let mut wrap_test_size = test_size; + wrap_test_size.x /= 3.0; + ui.heading("Wrapping"); + + let modes = [ + TextWrapMode::Extend, + TextWrapMode::Truncate, + TextWrapMode::Wrap, + ]; + Grid::new("wrapping") + .spacing(Vec2::new(test_size.x / 2.0, 4.0)) + .show(ui, |ui| { + for mode in &modes { + ui.label(format!("{mode:?}")); + } + ui.end_row(); + + for mode in &modes { + let (_, rect) = ui.allocate_space(wrap_test_size); + + let mut child_ui = ui.new_child(UiBuilder::new().max_rect(rect)); + child_ui.style_mut().wrap_mode = Some(*mode); + w(&mut child_ui); + + ui.painter().rect_stroke( + rect, + 0.0, + Stroke::new(1.0, Color32::WHITE), + StrokeKind::Outside, + ); + } + }); + } + + ui.heading("Layout"); + Grid::new("layout").striped(true).show(ui, |ui| { + ui.label(""); + for col in &cols { + ui.label(format!( + "cross_align: {:?}\ncross_justify:{:?}", + col.cross_align, col.cross_justify + )); + } + ui.end_row(); + + for row in &rows { + ui.label(format!( + "main_dir: {:?}\nmain_align: {:?}\nmain_justify: {:?}", + row.main_dir, row.main_align, row.main_justify + )); + for col in &cols { + let layout = Layout { + main_dir: row.main_dir, + main_align: row.main_align, + main_justify: row.main_justify, + cross_align: col.cross_align, + cross_justify: col.cross_justify, + main_wrap: false, + }; + + let (_, rect) = ui.allocate_space(test_size); + + let mut child_ui = ui.new_child(UiBuilder::new().layout(layout).max_rect(rect)); + w(&mut child_ui); + + ui.painter().rect_stroke( + rect, + 0.0, + Stroke::new(1.0, Color32::WHITE), + StrokeKind::Outside, + ); + } + + ui.end_row(); + } + }); + }); + + harness.fit_contents(); + harness.try_snapshot(&format!("layout/{name}")) +} + +/// Utility to create a snapshot test of the different states of a egui widget. +/// This renders each state to a texture to work around the fact only a single widget can be +/// hovered / pressed / focused at a time. +struct VisualTests<'a> { + name: String, + w: &'a mut dyn FnMut(&mut Ui) -> Response, + results: Vec<(String, ColorImage)>, +} + +impl<'a> VisualTests<'a> { + pub fn test(name: &str, mut w: impl FnMut(&mut Ui) -> Response) -> SnapshotResult { + let mut vis = VisualTests::new(name, &mut w); + vis.add_default_states(); + vis.render() + } + + pub fn new(name: &str, w: &'a mut dyn FnMut(&mut Ui) -> Response) -> Self { + Self { + name: name.to_owned(), + w, + results: Vec::new(), + } + } + + fn add_default_states(&mut self) { + self.add("idle", |_| {}); + self.add_node("hover", |node| { + node.hover(); + }); + self.add("pressed", |harness| { + harness.get_next().hover(); + let rect = harness.get_next().bounding_box().unwrap(); + let pos = Pos2::new( + ((rect.x0 + rect.x1) / 2.0) as f32, + ((rect.y0 + rect.y1) / 2.0) as f32, + ); + harness.input_mut().events.push(Event::PointerButton { + button: PointerButton::Primary, + pos, + pressed: true, + modifiers: Default::default(), + }); + }); + self.add_node("focussed", |node| { + node.focus(); + }); + self.add_disabled(); + } + + fn single_test(&mut self, f: impl FnOnce(&mut Harness<'_>), enabled: bool) -> ColorImage { + let mut harness = Harness::builder().with_step_dt(0.05).build_ui(|ui| { + egui_extras::install_image_loaders(ui.ctx()); + ui.add_enabled_ui(enabled, |ui| { + (self.w)(ui); + }); + }); + + harness.fit_contents(); + + // Wait for images to load + harness.try_run_realtime().ok(); + + f(&mut harness); + + harness.step(); + + let image = harness.render().expect("Failed to render harness"); + + ColorImage::from_rgba_unmultiplied( + [image.width() as usize, image.height() as usize], + image.as_ref(), + ) + } + + pub fn add(&mut self, name: &str, test: impl FnOnce(&mut Harness<'_>)) { + let image = self.single_test(test, true); + self.results.push((name.to_owned(), image)); + } + + pub fn add_disabled(&mut self) { + let image = self.single_test(|_| {}, false); + self.results.push(("disabled".to_owned(), image)); + } + + pub fn add_node(&mut self, name: &str, test: impl FnOnce(&Node<'_>)) { + self.add(name, |harness| { + let node = harness.get_next(); + test(&node); + }); + } + + pub fn render(self) -> SnapshotResult { + let mut results = Some(self.results); + let mut images: Option> = None; + + let mut harness = Harness::new_ui(|ui| { + let results = images.get_or_insert_with(|| { + results + .take() + .unwrap() + .into_iter() + .map(|(name, image)| { + let size = Vec2::new(image.width() as f32, image.height() as f32); + let texture_handle = + ui.ctx() + .load_texture(name.clone(), image, TextureOptions::default()); + let texture = SizedTexture::new(texture_handle.id(), size); + (name.clone(), texture_handle, texture) + }) + .collect() + }); + + Grid::new("results").show(ui, |ui| { + for (name, _, image) in results { + ui.label(&*name); + + ui.scope(|ui| { + ui.image(*image); + }); + + ui.end_row(); + } + }); + }); + + harness.fit_contents(); + + harness.try_snapshot(&format!("visuals/{}", self.name)) + } +} + +trait HarnessExt { + fn get_next(&self) -> Node<'_>; +} + +impl HarnessExt for Harness<'_> { + fn get_next(&self) -> Node<'_> { + self.get_all(by()).next().unwrap() + } +} From 009bfe5adadb4a0edea623eb4b58feadd403ffa4 Mon Sep 17 00:00:00 2001 From: Marek Bernat Date: Tue, 22 Apr 2025 11:28:34 +0200 Subject: [PATCH 054/129] Add a `Slider::update_while_editing(bool)` API (#5978) * Closes #5976 * [x] I have followed the instructions in the PR template - The `scripts/check.sh` fails in `cargo-deny` because of a security vulnerability in `crossbeam-channel` but this is also present on `master`. https://github.com/user-attachments/assets/a964c968-bb76-4e56-88e1-d1e3d51a401a --- crates/egui/src/widgets/slider.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/widgets/slider.rs b/crates/egui/src/widgets/slider.rs index e8b026ff5..0f7eb0495 100644 --- a/crates/egui/src/widgets/slider.rs +++ b/crates/egui/src/widgets/slider.rs @@ -117,6 +117,7 @@ pub struct Slider<'a> { custom_parser: Option>, trailing_fill: Option, handle_shape: Option, + update_while_editing: bool, } impl<'a> Slider<'a> { @@ -167,6 +168,7 @@ impl<'a> Slider<'a> { custom_parser: None, trailing_fill: None, handle_shape: None, + update_while_editing: true, } } @@ -641,6 +643,16 @@ impl<'a> Slider<'a> { let normalized = normalized_from_value(value, self.range(), &self.spec); lerp(position_range, normalized as f32) } + + /// Update the value on each key press when text-editing the value. + /// + /// Default: `true`. + /// If `false`, the value will only be updated when user presses enter or deselects the value. + #[inline] + pub fn update_while_editing(mut self, update: bool) -> Self { + self.update_while_editing = update; + self + } } impl Slider<'_> { @@ -900,7 +912,8 @@ impl Slider<'_> { .min_decimals(self.min_decimals) .max_decimals_opt(self.max_decimals) .suffix(self.suffix.clone()) - .prefix(self.prefix.clone()); + .prefix(self.prefix.clone()) + .update_while_editing(self.update_while_editing); match self.clamping { SliderClamping::Never => {} From 114460c1de6a7d879b5a2e6c3dd9ea401f3a7b17 Mon Sep 17 00:00:00 2001 From: mkalte Date: Tue, 22 Apr 2025 11:38:17 +0200 Subject: [PATCH 055/129] Change popup memory to be per-viewport (#6753) Starting with 77244cd4c5ebb924beaec8f89604a2881106b27c the popup open-state is cleaned up per memory pass. This becomes problematic for implementations that share memory between viewports (i.e. all of them, as far as i understand it), because each viewport gets a context pass, and thus a memory pass, which cleans out popup open state. To illustrate my issue, i have modifed the multiple viewport example to include a popup menu for the labels: https://gist.github.com/mkalte666/4ecd6b658003df4c6d532ae2060c7595 (changes not included in this pr). Then, when i try to use the popups, i get this: https://github.com/user-attachments/assets/7d04b872-5396-4823-bf30-824122925528 Immediate viewports just break popup handling in general, while deferred viewports kinda work, or dont. In this example ill be honest, it kind of still did, sometimes. In my more complex app with multiple viewports (where i encountered this first) it also just broke - even when just showing root and one nother. Probably to do with the order wgpu vs glow draws the viewports? Im not sure. In any case: This commit adds `Memory::popup` (now `Memory::popups`) to the per-viewport state of memory, including viewport aware cleanup, and adding it to the list of things cleaned if a viewport goes away. This results in the expected behavior: https://github.com/user-attachments/assets/fd1f74b7-d3b2-4edc-8dc4-2ad3cfa2160e I will note that with this, changing focus does not cause a popup to be closed, which is consistent with current behavior on a single app. Hope this helps ~Malte * Closes * [x] I have followed the instructions in the PR template * [x] ~~I have run check.sh locally~~ CI on the fork, including checks, went through. --- crates/egui/src/memory/mod.rs | 47 ++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index 0f588ca53..b8cd782c1 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -87,15 +87,6 @@ pub struct Memory { #[cfg_attr(feature = "persistence", serde(skip))] pub(crate) viewport_id: ViewportId, - /// Which popup-window is open (if any)? - /// Could be a combo box, color picker, menu, etc. - /// Optionally stores the position of the popup (usually this would be the position where - /// the user clicked). - /// If position is [`None`], the popup position will be calculated based on some configuration - /// (e.g. relative to some other widget). - #[cfg_attr(feature = "persistence", serde(skip))] - popup: Option, - #[cfg_attr(feature = "persistence", serde(skip))] everything_is_visible: bool, @@ -116,6 +107,15 @@ pub struct Memory { #[cfg_attr(feature = "persistence", serde(skip))] pub(crate) focus: ViewportIdMap, + + /// Which popup-window is open on a viewport (if any)? + /// Could be a combo box, color picker, menu, etc. + /// Optionally stores the position of the popup (usually this would be the position where + /// the user clicked). + /// If position is [`None`], the popup position will be calculated based on some configuration + /// (e.g. relative to some other widget). + #[cfg_attr(feature = "persistence", serde(skip))] + popups: ViewportIdMap, } impl Default for Memory { @@ -130,7 +130,7 @@ impl Default for Memory { viewport_id: Default::default(), areas: Default::default(), to_global: Default::default(), - popup: Default::default(), + popups: Default::default(), everything_is_visible: Default::default(), add_fonts: Default::default(), }; @@ -790,6 +790,7 @@ impl Memory { // Cleanup self.interactions.retain(|id, _| viewports.contains(id)); self.areas.retain(|id, _| viewports.contains(id)); + self.popups.retain(|id, _| viewports.contains(id)); self.areas.entry(self.viewport_id).or_default(); @@ -809,11 +810,11 @@ impl Memory { self.focus_mut().end_pass(used_ids); // Clean up abandoned popups. - if let Some(popup) = &mut self.popup { + if let Some(popup) = self.popups.get_mut(&self.viewport_id) { if popup.open_this_frame { popup.open_this_frame = false; } else { - self.popup = None; + self.popups.remove(&self.viewport_id); } } } @@ -1107,19 +1108,23 @@ impl OpenPopup { impl Memory { /// Is the given popup open? pub fn is_popup_open(&self, popup_id: Id) -> bool { - self.popup.is_some_and(|state| state.id == popup_id) || self.everything_is_visible() + self.popups + .get(&self.viewport_id) + .is_some_and(|state| state.id == popup_id) + || self.everything_is_visible() } /// Is any popup open? pub fn any_popup_open(&self) -> bool { - self.popup.is_some() || self.everything_is_visible() + self.popups.contains_key(&self.viewport_id) || self.everything_is_visible() } /// Open the given popup and close all others. /// /// Note that you must call `keep_popup_open` on subsequent frames as long as the popup is open. pub fn open_popup(&mut self, popup_id: Id) { - self.popup = Some(OpenPopup::new(popup_id, None)); + self.popups + .insert(self.viewport_id, OpenPopup::new(popup_id, None)); } /// Popups must call this every frame while open. @@ -1128,7 +1133,7 @@ impl Memory { /// called. For example, when a context menu is open and the underlying widget stops /// being rendered. pub fn keep_popup_open(&mut self, popup_id: Id) { - if let Some(state) = self.popup.as_mut() { + if let Some(state) = self.popups.get_mut(&self.viewport_id) { if state.id == popup_id { state.open_this_frame = true; } @@ -1137,18 +1142,20 @@ impl Memory { /// Open the popup and remember its position. pub fn open_popup_at(&mut self, popup_id: Id, pos: impl Into>) { - self.popup = Some(OpenPopup::new(popup_id, pos.into())); + self.popups + .insert(self.viewport_id, OpenPopup::new(popup_id, pos.into())); } /// Get the position for this popup. pub fn popup_position(&self, id: Id) -> Option { - self.popup + self.popups + .get(&self.viewport_id) .and_then(|state| if state.id == id { state.pos } else { None }) } /// Close any currently open popup. pub fn close_all_popups(&mut self) { - self.popup = None; + self.popups.clear(); } /// Close the given popup, if it is open. @@ -1156,7 +1163,7 @@ impl Memory { /// See also [`Self::close_all_popups`] if you want to close any / all currently open popups. pub fn close_popup(&mut self, popup_id: Id) { if self.is_popup_open(popup_id) { - self.popup = None; + self.popups.remove(&self.viewport_id); } } From cd318f0e55c43163aa26e4739042bb95b198d46d Mon Sep 17 00:00:00 2001 From: mitchmindtree Date: Tue, 22 Apr 2025 12:42:24 +0300 Subject: [PATCH 056/129] feat: Add `Scene::drag_pan_buttons` option. Allows specifying which pointer buttons pan the scene by dragging. (#5892) This adds an option for specifying the set of pointer buttons that can be used to pan the scene via clicking and dragging. The original behaviour where all buttons can pan the scene by default is maintained. Addresses part of #5891. --- Edit: It looks like the failing test is unrelated and also appears on master: https://github.com/emilk/egui/actions/runs/14330259861/job/40164414607. --------- Co-authored-by: Emil Ernerfeldt --- crates/egui/src/containers/mod.rs | 2 +- crates/egui/src/containers/scene.rs | 47 +++++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/crates/egui/src/containers/mod.rs b/crates/egui/src/containers/mod.rs index 3bd89e0db..31898838e 100644 --- a/crates/egui/src/containers/mod.rs +++ b/crates/egui/src/containers/mod.rs @@ -29,7 +29,7 @@ pub use { panel::{CentralPanel, SidePanel, TopBottomPanel}, popup::*, resize::Resize, - scene::Scene, + scene::{DragPanButtons, Scene}, scroll_area::ScrollArea, sides::Sides, tooltip::*, diff --git a/crates/egui/src/containers/scene.rs b/crates/egui/src/containers/scene.rs index 5b8d97410..d023a4d3b 100644 --- a/crates/egui/src/containers/scene.rs +++ b/crates/egui/src/containers/scene.rs @@ -3,7 +3,8 @@ use core::f32; use emath::{GuiRounding, Pos2}; use crate::{ - emath::TSTransform, InnerResponse, LayerId, Rangef, Rect, Response, Sense, Ui, UiBuilder, Vec2, + emath::TSTransform, InnerResponse, LayerId, PointerButton, Rangef, Rect, Response, Sense, Ui, + UiBuilder, Vec2, }; /// Creates a transformation that fits a given scene rectangle into the available screen size. @@ -45,6 +46,30 @@ fn fit_to_rect_in_scene( pub struct Scene { zoom_range: Rangef, max_inner_size: Vec2, + drag_pan_buttons: DragPanButtons, +} + +/// Specifies which pointer buttons can be used to pan the scene by dragging. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct DragPanButtons(u8); + +bitflags::bitflags! { + impl DragPanButtons: u8 { + /// [PointerButton::Primary] + const PRIMARY = 1 << 0; + + /// [PointerButton::Secondary] + const SECONDARY = 1 << 1; + + /// [PointerButton::Middle] + const MIDDLE = 1 << 2; + + /// [PointerButton::Extra1] + const EXTRA_1 = 1 << 3; + + /// [PointerButton::Extra2] + const EXTRA_2 = 1 << 4; + } } impl Default for Scene { @@ -52,6 +77,7 @@ impl Default for Scene { Self { zoom_range: Rangef::new(f32::EPSILON, 1.0), max_inner_size: Vec2::splat(1000.0), + drag_pan_buttons: DragPanButtons::all(), } } } @@ -82,6 +108,15 @@ impl Scene { self } + /// Specify which pointer buttons can be used to pan by clicking and dragging. + /// + /// By default, this is `DragPanButtons::all()`. + #[inline] + pub fn drag_pan_buttons(mut self, flags: DragPanButtons) -> Self { + self.drag_pan_buttons = flags; + self + } + /// `scene_rect` contains the view bounds of the inner [`Ui`]. /// /// `scene_rect` will be mutated by any panning/zooming done by the user. @@ -179,7 +214,15 @@ impl Scene { /// Helper function to handle pan and zoom interactions on a response. pub fn register_pan_and_zoom(&self, ui: &Ui, resp: &mut Response, to_global: &mut TSTransform) { - if resp.dragged() { + let dragged = self.drag_pan_buttons.iter().any(|button| match button { + DragPanButtons::PRIMARY => resp.dragged_by(PointerButton::Primary), + DragPanButtons::SECONDARY => resp.dragged_by(PointerButton::Secondary), + DragPanButtons::MIDDLE => resp.dragged_by(PointerButton::Middle), + DragPanButtons::EXTRA_1 => resp.dragged_by(PointerButton::Extra1), + DragPanButtons::EXTRA_2 => resp.dragged_by(PointerButton::Extra2), + _ => false, + }); + if dragged { to_global.translation += to_global.scaling * resp.drag_delta(); resp.mark_changed(); } From 61e883be25c824ec82764150135f2e9ebf938f8b Mon Sep 17 00:00:00 2001 From: Sven Niederberger <73159570+s-nie@users.noreply.github.com> Date: Tue, 22 Apr 2025 11:52:20 +0200 Subject: [PATCH 057/129] Revert "Add `OutputCommand::SetPointerPosition` to set mouse position" (#5867) Reverts emilk/egui#5776 I noticed that this is already a `ViewportCommand`. Sorry for not seeing that earlier. --- crates/eframe/src/web/app_runner.rs | 3 --- crates/egui-winit/src/lib.rs | 3 --- crates/egui/src/context.rs | 5 ----- crates/egui/src/data/output.rs | 3 --- crates/egui_demo_lib/src/demo/tests/cursor_test.rs | 8 -------- 5 files changed, 22 deletions(-) diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index 835b7f008..76ae1761f 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -336,9 +336,6 @@ impl AppRunner { egui::OutputCommand::OpenUrl(open_url) => { super::open_url(&open_url.url, open_url.new_tab); } - egui::OutputCommand::SetPointerPosition(_) => { - // Not supported on the web. - } } } diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index e541273fa..fe4d2945d 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -856,9 +856,6 @@ impl State { egui::OutputCommand::OpenUrl(open_url) => { open_url_in_browser(&open_url.url); } - egui::OutputCommand::SetPointerPosition(egui::Pos2 { x, y }) => { - let _ = window.set_cursor_position(winit::dpi::LogicalPosition { x, y }); - } } } diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 44af99feb..6ec79f879 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1494,11 +1494,6 @@ impl Context { self.send_cmd(crate::OutputCommand::CopyImage(image)); } - /// Set the mouse cursor position (if the platform supports it). - pub fn set_pointer_position(&self, position: Pos2) { - self.send_cmd(crate::OutputCommand::SetPointerPosition(position)); - } - /// Format the given shortcut in a human-readable way (e.g. `Ctrl+Shift+X`). /// /// Can be used to get the text for [`crate::Button::shortcut_text`]. diff --git a/crates/egui/src/data/output.rs b/crates/egui/src/data/output.rs index dcc03242e..2fdaec1e1 100644 --- a/crates/egui/src/data/output.rs +++ b/crates/egui/src/data/output.rs @@ -95,9 +95,6 @@ pub enum OutputCommand { /// Open this url in a browser. OpenUrl(OpenUrl), - - /// Set the mouse cursor position (if the platform supports it). - SetPointerPosition(emath::Pos2), } /// The non-rendering part of what egui emits each frame. diff --git a/crates/egui_demo_lib/src/demo/tests/cursor_test.rs b/crates/egui_demo_lib/src/demo/tests/cursor_test.rs index 6927e1af9..78214d5eb 100644 --- a/crates/egui_demo_lib/src/demo/tests/cursor_test.rs +++ b/crates/egui_demo_lib/src/demo/tests/cursor_test.rs @@ -16,14 +16,6 @@ impl crate::Demo for CursorTest { impl crate::View for CursorTest { fn ui(&mut self, ui: &mut egui::Ui) { - if ui - .button("Center pointer in window") - .on_hover_text("The platform may not support this.") - .clicked() - { - let position = ui.ctx().available_rect().center(); - ui.ctx().set_pointer_position(position); - } ui.vertical_centered_justified(|ui| { ui.heading("Hover to switch cursor icon:"); for &cursor_icon in &egui::CursorIcon::ALL { From a0f072ab1ec7c0748586193c0e35612ae4d0d944 Mon Sep 17 00:00:00 2001 From: rustbasic <127506429+rustbasic@users.noreply.github.com> Date: Tue, 22 Apr 2025 19:00:22 +0900 Subject: [PATCH 058/129] Fix bug in pointer movement detection (#5329) Fix: Popups do not appear in certain situations. * Closes #5080 * Related #5107 The root cause is that `last_move_time` is not updated in certain situations (slow situations?). --- crates/egui/src/input_state/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/egui/src/input_state/mod.rs b/crates/egui/src/input_state/mod.rs index bc32528d1..a5fc83ee3 100644 --- a/crates/egui/src/input_state/mod.rs +++ b/crates/egui/src/input_state/mod.rs @@ -941,6 +941,7 @@ impl PointerState { press_origin.distance(pos) > self.input_options.max_click_dist; } + self.last_move_time = time; self.pointer_events.push(PointerEvent::Moved(pos)); } Event::PointerButton { From 69b9f0eede499c64288e7008dfc1b72300dcb7c3 Mon Sep 17 00:00:00 2001 From: MStarha <59487310+MStarha@users.noreply.github.com> Date: Tue, 22 Apr 2025 17:44:10 +0200 Subject: [PATCH 059/129] Rework `TextEdit` arrow navigation to handle Unicode graphemes (#5812) * [x] I have followed the instructions in the PR template Previously, navigating text in `TextEdit` with Ctrl + left/right arrow would jump inside words that contained combining characters (i.e. diacritics). This PR introduces new dependency of `unicode-segmentation` to handle grapheme encoding. The new implementation ignores whitespace and other separators such as `-` (dash) between words, but respects `_` (underscore). --------- Co-authored-by: lucasmerlin --- Cargo.lock | 1 + Cargo.toml | 1 + crates/egui/Cargo.toml | 1 + .../src/text_selection/text_cursor_state.rs | 84 +++++++++++++++---- .../egui/src/widgets/text_edit/text_buffer.rs | 8 +- 5 files changed, 76 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c2d246045..4411be0c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1285,6 +1285,7 @@ dependencies = [ "profiling", "ron", "serde", + "unicode-segmentation", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index a0051513d..d272498f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -99,6 +99,7 @@ serde = { version = "1", features = ["derive"] } similar-asserts = "1.4.2" thiserror = "1.0.37" type-map = "0.5.0" +unicode-segmentation = "1.12.0" wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" web-sys = "0.3.73" diff --git a/crates/egui/Cargo.toml b/crates/egui/Cargo.toml index e00202d16..e7641ef31 100644 --- a/crates/egui/Cargo.toml +++ b/crates/egui/Cargo.toml @@ -87,6 +87,7 @@ ahash.workspace = true bitflags.workspace = true nohash-hasher.workspace = true profiling.workspace = true +unicode-segmentation.workspace = true #! ### Optional dependencies accesskit = { workspace = true, optional = true } diff --git a/crates/egui/src/text_selection/text_cursor_state.rs b/crates/egui/src/text_selection/text_cursor_state.rs index baf7b0463..aaa3beb9b 100644 --- a/crates/egui/src/text_selection/text_cursor_state.rs +++ b/crates/egui/src/text_selection/text_cursor_state.rs @@ -1,6 +1,7 @@ //! Text cursor changes/interaction, without modifying the text. use epaint::text::{cursor::CCursor, Galley}; +use unicode_segmentation::UnicodeSegmentation; use crate::{epaint, NumExt, Rect, Response, Ui}; @@ -166,7 +167,7 @@ fn select_line_at(text: &str, ccursor: CCursor) -> CCursorRange { pub fn ccursor_next_word(text: &str, ccursor: CCursor) -> CCursor { CCursor { - index: next_word_boundary_char_index(text.chars(), ccursor.index), + index: next_word_boundary_char_index(text, ccursor.index), prefer_next_row: false, } } @@ -180,9 +181,10 @@ fn ccursor_next_line(text: &str, ccursor: CCursor) -> CCursor { pub fn ccursor_previous_word(text: &str, ccursor: CCursor) -> CCursor { let num_chars = text.chars().count(); + let reversed: String = text.graphemes(true).rev().collect(); CCursor { index: num_chars - - next_word_boundary_char_index(text.chars().rev(), num_chars - ccursor.index), + - next_word_boundary_char_index(&reversed, num_chars - ccursor.index).min(num_chars), prefer_next_row: true, } } @@ -196,22 +198,25 @@ fn ccursor_previous_line(text: &str, ccursor: CCursor) -> CCursor { } } -fn next_word_boundary_char_index(it: impl Iterator, mut index: usize) -> usize { - let mut it = it.skip(index); - if let Some(_first) = it.next() { - index += 1; - - if let Some(second) = it.next() { - index += 1; - for next in it { - if is_word_char(next) != is_word_char(second) { - break; - } - index += 1; - } +fn next_word_boundary_char_index(text: &str, index: usize) -> usize { + for word in text.split_word_bound_indices() { + // Splitting considers contiguous whitespace as one word, such words must be skipped, + // this handles cases for example ' abc' (a space and a word), the cursor is at the beginning + // (before space) - this jumps at the end of 'abc' (this is consistent with text editors + // or browsers) + let ci = char_index_from_byte_index(text, word.0); + if ci > index && !skip_word(word.1) { + return ci; } } - index + + char_index_from_byte_index(text, text.len()) +} + +fn skip_word(text: &str) -> bool { + // skip words that contain anything other than alphanumeric characters and underscore + // (i.e. whitespace, dashes, etc.) + !text.chars().any(|c| !is_word_char(c)) } fn next_line_boundary_char_index(it: impl Iterator, mut index: usize) -> usize { @@ -233,7 +238,7 @@ fn next_line_boundary_char_index(it: impl Iterator, mut index: usiz } pub fn is_word_char(c: char) -> bool { - c.is_ascii_alphanumeric() || c == '_' + c.is_alphanumeric() || c == '_' } fn is_linebreak(c: char) -> bool { @@ -270,6 +275,16 @@ pub fn byte_index_from_char_index(s: &str, char_index: usize) -> usize { s.len() } +pub fn char_index_from_byte_index(input: &str, byte_index: usize) -> usize { + for (ci, (bi, _)) in input.char_indices().enumerate() { + if bi == byte_index { + return ci; + } + } + + input.char_indices().last().map_or(0, |(i, _)| i + 1) +} + pub fn slice_char_range(s: &str, char_range: std::ops::Range) -> &str { assert!( char_range.start <= char_range.end, @@ -293,3 +308,38 @@ pub fn cursor_rect(galley: &Galley, cursor: &CCursor, row_height: f32) -> Rect { cursor_pos } + +#[cfg(test)] +mod test { + use crate::text_selection::text_cursor_state::next_word_boundary_char_index; + + #[test] + fn test_next_word_boundary_char_index() { + // ASCII only + let text = "abc d3f g_h i-j"; + assert_eq!(next_word_boundary_char_index(text, 1), 3); + assert_eq!(next_word_boundary_char_index(text, 3), 7); + assert_eq!(next_word_boundary_char_index(text, 9), 11); + assert_eq!(next_word_boundary_char_index(text, 12), 13); + assert_eq!(next_word_boundary_char_index(text, 13), 15); + assert_eq!(next_word_boundary_char_index(text, 15), 15); + + assert_eq!(next_word_boundary_char_index("", 0), 0); + assert_eq!(next_word_boundary_char_index("", 1), 0); + + // Unicode graphemes, some of which consist of multiple Unicode characters, + // !!! Unicode character is not always what is tranditionally considered a character, + // the values below are correct despite not seeming that way on the first look, + // handling of and around emojis is kind of weird and is not consistent across + // text editors and browsers + let text = "ā¤ļøšŸ‘ skvělĆ” knihovna šŸ‘ā¤ļø"; + assert_eq!(next_word_boundary_char_index(text, 0), 2); + assert_eq!(next_word_boundary_char_index(text, 2), 3); // this does not skip the space between thumbs-up and 'skvělĆ”' + assert_eq!(next_word_boundary_char_index(text, 6), 10); + assert_eq!(next_word_boundary_char_index(text, 9), 10); + assert_eq!(next_word_boundary_char_index(text, 12), 19); + assert_eq!(next_word_boundary_char_index(text, 15), 19); + assert_eq!(next_word_boundary_char_index(text, 19), 20); + assert_eq!(next_word_boundary_char_index(text, 20), 21); + } +} diff --git a/crates/egui/src/widgets/text_edit/text_buffer.rs b/crates/egui/src/widgets/text_edit/text_buffer.rs index 6cf7da15a..ebf33b097 100644 --- a/crates/egui/src/widgets/text_edit/text_buffer.rs +++ b/crates/egui/src/widgets/text_edit/text_buffer.rs @@ -8,8 +8,8 @@ use epaint::{ use crate::{ text::CCursorRange, text_selection::text_cursor_state::{ - byte_index_from_char_index, ccursor_next_word, ccursor_previous_word, find_line_start, - slice_char_range, + byte_index_from_char_index, ccursor_next_word, ccursor_previous_word, + char_index_from_byte_index, find_line_start, slice_char_range, }, }; @@ -48,6 +48,10 @@ pub trait TextBuffer { byte_index_from_char_index(self.as_str(), char_index) } + fn char_index_from_byte_index(&self, char_index: usize) -> usize { + char_index_from_byte_index(self.as_str(), char_index) + } + /// Clears all characters in this buffer fn clear(&mut self) { self.delete_char_range(0..self.as_str().len()); From 6f910f60e9740e2a28a164c04475f7ea853673bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bennet=20Ble=C3=9Fmann?= <3877590+Skgland@users.noreply.github.com> Date: Thu, 24 Apr 2025 14:50:34 +0200 Subject: [PATCH 060/129] Protect against NaN in hit-test code (#6851) * Closes * [x] I have followed the instructions in the PR template --- crates/egui/src/hit_test.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui/src/hit_test.rs b/crates/egui/src/hit_test.rs index e79172129..2f0edcfaf 100644 --- a/crates/egui/src/hit_test.rs +++ b/crates/egui/src/hit_test.rs @@ -65,7 +65,7 @@ pub fn hit_test( .filter(|layer| layer.order.allow_interaction()) .flat_map(|&layer_id| widgets.get_layer(layer_id)) .filter(|&w| { - if w.interact_rect.is_negative() { + if w.interact_rect.is_negative() || w.interact_rect.any_nan() { return false; } From 3dd8d34257095af976305e64f4751716a0fc23b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=96R=C3=96K=20Attila?= Date: Thu, 24 Apr 2025 15:39:01 +0200 Subject: [PATCH 061/129] CI: Install wasm-opt 123 from the GitHub release of Binaryen (#6849) * Prerequisite of https://github.com/emilk/egui/pull/6848 --- .github/workflows/deploy_web_demo.yml | 9 ++++++--- .github/workflows/preview_build.yml | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/deploy_web_demo.yml b/.github/workflows/deploy_web_demo.yml index 6c9d80962..d92150013 100644 --- a/.github/workflows/deploy_web_demo.yml +++ b/.github/workflows/deploy_web_demo.yml @@ -46,9 +46,12 @@ jobs: with: prefix-key: "web-demo-" - - name: "Install wasmopt / binaryen" - run: | - sudo apt-get update && sudo apt-get install binaryen + - name: Install wasm-opt + uses: sigoden/install-binary@v1 + with: + repo: WebAssembly/binaryen + tag: version_123 + name: wasm-opt - run: | scripts/build_demo_web.sh --release diff --git a/.github/workflows/preview_build.yml b/.github/workflows/preview_build.yml index 932569c48..d1d19e620 100644 --- a/.github/workflows/preview_build.yml +++ b/.github/workflows/preview_build.yml @@ -25,9 +25,12 @@ jobs: with: prefix-key: "pr-preview-" - - name: "Install wasmopt / binaryen" - run: | - sudo apt-get update && sudo apt-get install binaryen + - name: Install wasm-opt + uses: sigoden/install-binary@v1 + with: + repo: WebAssembly/binaryen + tag: version_123 + name: wasm-opt - run: | scripts/build_demo_web.sh --release From fdb9aa282a3313b4857faa10a44ea03b44c49c75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=96R=C3=96K=20Attila?= Date: Thu, 24 Apr 2025 17:00:29 +0200 Subject: [PATCH 062/129] Raise MSRV to 1.84 (#6848) Prerequisite of https://github.com/emilk/egui/pull/6744. See: https://github.com/gfx-rs/wgpu/pull/7218, https://github.com/gfx-rs/wgpu/pull/7425 Please be aware that Rust 1.84 enables some (more) WASM extensions by default, and ships with an `std` built with them enabled: https://blog.rust-lang.org/2024/09/24/webassembly-targets-change-in-default-target-features/ According to `rustc +1.84 --print=cfg --target wasm32-unknown-unknown`, these are: `multivalue`, `mutable-globals`, `reference-types`, and `sign-ext`. (c.f. `rustc +1.84 --print=cfg --target wasm32-unknown-unknown -C target-cpu=mvp` enabling none.) For reference: https://webassembly.org/features/ ---- If support is desired for ancient/esoteric browsers that don't have these implemented, there are two ways to get around this: - Target `wasm32v1-none` instead, but that's a `no-std` target, and I suppose a lot of dependencies don't work that way (e.g. https://github.com/gfx-rs/wgpu/issues/6826) - Using the `-Ctarget-cpu=mvp` and `-Zbuild-std=panic_abort,std` flags, and the `RUSTC_BOOTSTRAP=1` escape hatch to allow using the latter with non-`nightly` toolchains - until https://github.com/rust-lang/wg-cargo-std-aware is stabilized. (For reference: https://github.com/ruffle-rs/ruffle/pull/18528/files#diff-fb2896d189d77b35ace9a079c1ba9b55777d16e0f11ce79f776475a451b1825a) I don't think either of these is particularly advantageous, so I suggest just accepting that browsers will have to have some extensions implemented to run `egui`. --- .github/workflows/deploy_web_demo.yml | 2 +- .github/workflows/preview_build.yml | 2 +- .github/workflows/rust.yml | 16 ++++++++-------- Cargo.toml | 2 +- clippy.toml | 2 +- crates/egui/src/lib.rs | 2 +- examples/confirm_exit/Cargo.toml | 2 +- examples/custom_3d_glow/Cargo.toml | 2 +- examples/custom_font/Cargo.toml | 2 +- examples/custom_font_style/Cargo.toml | 2 +- examples/custom_keypad/Cargo.toml | 2 +- examples/custom_style/Cargo.toml | 2 +- examples/custom_window_frame/Cargo.toml | 2 +- examples/file_dialog/Cargo.toml | 2 +- examples/hello_android/Cargo.toml | 2 +- examples/hello_world/Cargo.toml | 2 +- examples/hello_world_par/Cargo.toml | 2 +- examples/hello_world_simple/Cargo.toml | 2 +- examples/images/Cargo.toml | 2 +- examples/keyboard_events/Cargo.toml | 2 +- examples/multiple_viewports/Cargo.toml | 2 +- examples/puffin_profiler/Cargo.toml | 2 +- examples/screenshot/Cargo.toml | 2 +- examples/serial_windows/Cargo.toml | 2 +- examples/user_attention/Cargo.toml | 2 +- rust-toolchain | 2 +- scripts/check.sh | 2 +- scripts/clippy_wasm/clippy.toml | 2 +- tests/test_egui_extras_compilation/Cargo.toml | 2 +- tests/test_inline_glow_paint/Cargo.toml | 2 +- tests/test_size_pass/Cargo.toml | 2 +- tests/test_ui_stack/Cargo.toml | 2 +- tests/test_viewports/Cargo.toml | 2 +- 33 files changed, 40 insertions(+), 40 deletions(-) diff --git a/.github/workflows/deploy_web_demo.yml b/.github/workflows/deploy_web_demo.yml index d92150013..bdae9bca4 100644 --- a/.github/workflows/deploy_web_demo.yml +++ b/.github/workflows/deploy_web_demo.yml @@ -39,7 +39,7 @@ jobs: with: profile: minimal target: wasm32-unknown-unknown - toolchain: 1.81.0 + toolchain: 1.84.0 override: true - uses: Swatinem/rust-cache@v2 diff --git a/.github/workflows/preview_build.yml b/.github/workflows/preview_build.yml index d1d19e620..8550cbeed 100644 --- a/.github/workflows/preview_build.yml +++ b/.github/workflows/preview_build.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.81.0 + toolchain: 1.84.0 targets: wasm32-unknown-unknown - uses: Swatinem/rust-cache@v2 with: diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index eadce83be..e75835db7 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -5,7 +5,7 @@ name: Rust env: RUSTFLAGS: -D warnings RUSTDOCFLAGS: -D warnings - NIGHTLY_VERSION: nightly-2024-09-11 + NIGHTLY_VERSION: nightly-2025-04-22 jobs: fmt-crank-check-test: @@ -18,7 +18,7 @@ jobs: - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.81.0 + toolchain: 1.84.0 - name: Install packages (Linux) if: runner.os == 'Linux' @@ -83,7 +83,7 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.81.0 + toolchain: 1.84.0 targets: wasm32-unknown-unknown - run: sudo apt-get update && sudo apt-get install libgtk-3-dev libatk1.0-dev @@ -155,7 +155,7 @@ jobs: - uses: actions/checkout@v4 - uses: EmbarkStudios/cargo-deny-action@v2 with: - rust-version: "1.81.0" + rust-version: "1.84.0" log-level: error command: check arguments: --target ${{ matrix.target }} @@ -170,7 +170,7 @@ jobs: - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.81.0 + toolchain: 1.84.0 targets: aarch64-linux-android - name: Set up cargo cache @@ -189,7 +189,7 @@ jobs: - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.81.0 + toolchain: 1.84.0 targets: aarch64-apple-ios - name: Set up cargo cache @@ -208,7 +208,7 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.81.0 + toolchain: 1.84.0 - name: Set up cargo cache uses: Swatinem/rust-cache@v2 @@ -232,7 +232,7 @@ jobs: lfs: true - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.81.0 + toolchain: 1.84.0 - name: Set up cargo cache uses: Swatinem/rust-cache@v2 diff --git a/Cargo.toml b/Cargo.toml index d272498f7..a7a5ede7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ members = [ [workspace.package] edition = "2021" license = "MIT OR Apache-2.0" -rust-version = "1.81" +rust-version = "1.84" version = "0.31.1" diff --git a/clippy.toml b/clippy.toml index f349943a9..24a804037 100644 --- a/clippy.toml +++ b/clippy.toml @@ -3,7 +3,7 @@ # ----------------------------------------------------------------------------- # Section identical to scripts/clippy_wasm/clippy.toml: -msrv = "1.81" +msrv = "1.84" allow-unwrap-in-tests = true diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 0d8459808..f8842a1cd 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -3,7 +3,7 @@ //! Try the live web demo: . Read more about egui at . //! //! `egui` is in heavy development, with each new version having breaking changes. -//! You need to have rust 1.81.0 or later to use `egui`. +//! You need to have rust 1.84.0 or later to use `egui`. //! //! To quickly get started with egui, you can take a look at [`eframe_template`](https://github.com/emilk/eframe_template) //! which uses [`eframe`](https://docs.rs/eframe). diff --git a/examples/confirm_exit/Cargo.toml b/examples/confirm_exit/Cargo.toml index c3050c32d..f20af6faf 100644 --- a/examples/confirm_exit/Cargo.toml +++ b/examples/confirm_exit/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.81" +rust-version = "1.84" publish = false [lints] diff --git a/examples/custom_3d_glow/Cargo.toml b/examples/custom_3d_glow/Cargo.toml index 3ed54df78..98824d3a4 100644 --- a/examples/custom_3d_glow/Cargo.toml +++ b/examples/custom_3d_glow/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.81" +rust-version = "1.84" publish = false [lints] diff --git a/examples/custom_font/Cargo.toml b/examples/custom_font/Cargo.toml index afaeb4117..da29460c7 100644 --- a/examples/custom_font/Cargo.toml +++ b/examples/custom_font/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.81" +rust-version = "1.84" publish = false [lints] diff --git a/examples/custom_font_style/Cargo.toml b/examples/custom_font_style/Cargo.toml index f64a48803..d7db7450a 100644 --- a/examples/custom_font_style/Cargo.toml +++ b/examples/custom_font_style/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["tami5 "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.81" +rust-version = "1.84" publish = false [lints] diff --git a/examples/custom_keypad/Cargo.toml b/examples/custom_keypad/Cargo.toml index 7e765e4ec..a866ae4d9 100644 --- a/examples/custom_keypad/Cargo.toml +++ b/examples/custom_keypad/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Varphone Wong "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.81" +rust-version = "1.84" publish = false [lints] diff --git a/examples/custom_style/Cargo.toml b/examples/custom_style/Cargo.toml index 423179aaa..e6571fa04 100644 --- a/examples/custom_style/Cargo.toml +++ b/examples/custom_style/Cargo.toml @@ -3,7 +3,7 @@ name = "custom_style" version = "0.1.0" license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.81" +rust-version = "1.84" publish = false [lints] diff --git a/examples/custom_window_frame/Cargo.toml b/examples/custom_window_frame/Cargo.toml index b0e89c8a6..b98ea27d2 100644 --- a/examples/custom_window_frame/Cargo.toml +++ b/examples/custom_window_frame/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.81" +rust-version = "1.84" publish = false [lints] diff --git a/examples/file_dialog/Cargo.toml b/examples/file_dialog/Cargo.toml index 0cb873729..816686979 100644 --- a/examples/file_dialog/Cargo.toml +++ b/examples/file_dialog/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.81" +rust-version = "1.84" publish = false [lints] diff --git a/examples/hello_android/Cargo.toml b/examples/hello_android/Cargo.toml index ddeaea9dc..f24b7021b 100644 --- a/examples/hello_android/Cargo.toml +++ b/examples/hello_android/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.81" +rust-version = "1.84" publish = false # `unsafe_code` is required for `#[no_mangle]`, disable workspace lints to workaround lint error. diff --git a/examples/hello_world/Cargo.toml b/examples/hello_world/Cargo.toml index 1e4b553db..0ad5ff844 100644 --- a/examples/hello_world/Cargo.toml +++ b/examples/hello_world/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.81" +rust-version = "1.84" publish = false [lints] diff --git a/examples/hello_world_par/Cargo.toml b/examples/hello_world_par/Cargo.toml index 541b220f7..dd980888b 100644 --- a/examples/hello_world_par/Cargo.toml +++ b/examples/hello_world_par/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Maxim Osipenko "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.81" +rust-version = "1.84" publish = false [lints] diff --git a/examples/hello_world_simple/Cargo.toml b/examples/hello_world_simple/Cargo.toml index db1d7906d..57f0269e9 100644 --- a/examples/hello_world_simple/Cargo.toml +++ b/examples/hello_world_simple/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.81" +rust-version = "1.84" publish = false [lints] diff --git a/examples/images/Cargo.toml b/examples/images/Cargo.toml index c013e7b43..d605e25e0 100644 --- a/examples/images/Cargo.toml +++ b/examples/images/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Jan ProchĆ”zka "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.81" +rust-version = "1.84" publish = false [lints] diff --git a/examples/keyboard_events/Cargo.toml b/examples/keyboard_events/Cargo.toml index d57f17193..00d1d591f 100644 --- a/examples/keyboard_events/Cargo.toml +++ b/examples/keyboard_events/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Jose Palazon "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.81" +rust-version = "1.84" publish = false [lints] diff --git a/examples/multiple_viewports/Cargo.toml b/examples/multiple_viewports/Cargo.toml index d5efc23df..995baa431 100644 --- a/examples/multiple_viewports/Cargo.toml +++ b/examples/multiple_viewports/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.81" +rust-version = "1.84" publish = false [lints] diff --git a/examples/puffin_profiler/Cargo.toml b/examples/puffin_profiler/Cargo.toml index d26d8a6cb..df39fd456 100644 --- a/examples/puffin_profiler/Cargo.toml +++ b/examples/puffin_profiler/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.81" +rust-version = "1.84" publish = false [package.metadata.cargo-machete] diff --git a/examples/screenshot/Cargo.toml b/examples/screenshot/Cargo.toml index b5a22ce7b..0288328fa 100644 --- a/examples/screenshot/Cargo.toml +++ b/examples/screenshot/Cargo.toml @@ -7,7 +7,7 @@ authors = [ ] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.81" +rust-version = "1.84" publish = false [lints] diff --git a/examples/serial_windows/Cargo.toml b/examples/serial_windows/Cargo.toml index c18a6029d..f89af84c0 100644 --- a/examples/serial_windows/Cargo.toml +++ b/examples/serial_windows/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.81" +rust-version = "1.84" publish = false [lints] diff --git a/examples/user_attention/Cargo.toml b/examples/user_attention/Cargo.toml index 3ae28d693..ff1a7538b 100644 --- a/examples/user_attention/Cargo.toml +++ b/examples/user_attention/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["TicClick "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.81" +rust-version = "1.84" publish = false [lints] diff --git a/rust-toolchain b/rust-toolchain index 0eefd31bc..6c3ca5854 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -5,6 +5,6 @@ # to the user in the error, instead of "error: invalid channel name '[toolchain]'". [toolchain] -channel = "1.81.0" +channel = "1.84.0" components = ["rustfmt", "clippy"] targets = ["wasm32-unknown-unknown"] diff --git a/scripts/check.sh b/scripts/check.sh index 1a4eec5da..f232dfbb6 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -9,7 +9,7 @@ set -x # Checks all tests, lints etc. # Basically does what the CI does. -# cargo +1.81.0 install --quiet typos-cli +# cargo +1.84.0 install --quiet typos-cli export RUSTFLAGS="-D warnings" export RUSTDOCFLAGS="-D warnings" # https://github.com/emilk/egui/pull/1454 diff --git a/scripts/clippy_wasm/clippy.toml b/scripts/clippy_wasm/clippy.toml index 2d34d64fc..17d2a8cd6 100644 --- a/scripts/clippy_wasm/clippy.toml +++ b/scripts/clippy_wasm/clippy.toml @@ -6,7 +6,7 @@ # ----------------------------------------------------------------------------- # Section identical to the root clippy.toml: -msrv = "1.81" +msrv = "1.84" allow-unwrap-in-tests = true diff --git a/tests/test_egui_extras_compilation/Cargo.toml b/tests/test_egui_extras_compilation/Cargo.toml index 9f1aeca52..0f5e8f50a 100644 --- a/tests/test_egui_extras_compilation/Cargo.toml +++ b/tests/test_egui_extras_compilation/Cargo.toml @@ -3,7 +3,7 @@ name = "test_egui_extras_compilation" version = "0.1.0" license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.81" +rust-version = "1.84" publish = false [lints] diff --git a/tests/test_inline_glow_paint/Cargo.toml b/tests/test_inline_glow_paint/Cargo.toml index 15cb21640..7187c599a 100644 --- a/tests/test_inline_glow_paint/Cargo.toml +++ b/tests/test_inline_glow_paint/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.81" +rust-version = "1.84" publish = false [lints] diff --git a/tests/test_size_pass/Cargo.toml b/tests/test_size_pass/Cargo.toml index a44f2cc0b..280a40c1b 100644 --- a/tests/test_size_pass/Cargo.toml +++ b/tests/test_size_pass/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.81" +rust-version = "1.84" publish = false [lints] diff --git a/tests/test_ui_stack/Cargo.toml b/tests/test_ui_stack/Cargo.toml index dd8bdb5de..e4dd35f4d 100644 --- a/tests/test_ui_stack/Cargo.toml +++ b/tests/test_ui_stack/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Antoine Beyeler "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.81" +rust-version = "1.84" publish = false [lints] diff --git a/tests/test_viewports/Cargo.toml b/tests/test_viewports/Cargo.toml index 2636b5177..e8c8bc754 100644 --- a/tests/test_viewports/Cargo.toml +++ b/tests/test_viewports/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["konkitoman"] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.81" +rust-version = "1.84" publish = false [lints] From f9245954ebfb599fc77a5a33aa7c90504399faba Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 24 Apr 2025 17:32:50 +0200 Subject: [PATCH 063/129] Enable more clippy lints (#6853) * Follows https://github.com/emilk/egui/pull/6848 --- Cargo.toml | 10 ++++++++- crates/ecolor/src/rgba.rs | 3 +-- crates/eframe/src/epi.rs | 10 ++++----- crates/eframe/src/lib.rs | 4 ++-- crates/eframe/src/native/app_icon.rs | 10 ++++----- crates/eframe/src/native/epi_integration.rs | 6 +++--- .../eframe/src/native/event_loop_context.rs | 2 +- crates/eframe/src/native/file_storage.rs | 4 ++-- crates/eframe/src/native/glow_integration.rs | 20 +++++++++--------- crates/eframe/src/native/run.rs | 3 +-- crates/eframe/src/native/wgpu_integration.rs | 5 ++--- crates/eframe/src/web/app_runner.rs | 4 ++-- crates/eframe/src/web/events.rs | 6 +++--- crates/eframe/src/web/mod.rs | 2 +- crates/eframe/src/web/web_logger.rs | 2 +- crates/eframe/src/web/web_painter_glow.rs | 5 +++-- crates/eframe/src/web/web_painter_wgpu.rs | 4 ++-- crates/eframe/src/web/web_runner.rs | 6 +++--- crates/egui-wgpu/src/capture.rs | 5 +++-- crates/egui-wgpu/src/lib.rs | 2 +- crates/egui-wgpu/src/renderer.rs | 6 +++--- crates/egui-wgpu/src/setup.rs | 2 +- crates/egui-wgpu/src/winit.rs | 2 +- crates/egui-winit/src/clipboard.rs | 2 +- crates/egui/src/cache/cache_trait.rs | 2 +- crates/egui/src/containers/area.rs | 6 +++--- .../egui/src/containers/collapsing_header.rs | 2 +- crates/egui/src/containers/combo_box.rs | 6 +++--- crates/egui/src/containers/menu.rs | 3 ++- crates/egui/src/containers/old_popup.rs | 6 +++--- crates/egui/src/containers/panel.rs | 2 +- crates/egui/src/containers/popup.rs | 2 +- crates/egui/src/containers/resize.rs | 4 ++-- crates/egui/src/containers/scene.rs | 2 +- crates/egui/src/containers/scroll_area.rs | 4 ++-- crates/egui/src/context.rs | 21 ++++++++----------- crates/egui/src/data/input.rs | 2 +- crates/egui/src/data/output.rs | 14 ++++++------- crates/egui/src/grid.rs | 6 +++--- crates/egui/src/id.rs | 2 +- crates/egui/src/input_state/mod.rs | 4 ++-- crates/egui/src/introspection.rs | 2 +- crates/egui/src/layout.rs | 2 +- crates/egui/src/load.rs | 2 +- crates/egui/src/load/bytes_loader.rs | 2 +- crates/egui/src/load/texture_loader.rs | 4 ++-- crates/egui/src/menu.rs | 4 ++-- crates/egui/src/os.rs | 2 +- crates/egui/src/painter.rs | 6 +++--- crates/egui/src/pass_state.rs | 2 +- crates/egui/src/style.rs | 7 ++++--- .../text_selection/label_text_selection.rs | 2 +- .../src/text_selection/text_cursor_state.rs | 4 ++-- crates/egui/src/ui.rs | 8 +++---- crates/egui/src/ui_builder.rs | 2 +- crates/egui/src/ui_stack.rs | 2 +- crates/egui/src/util/id_type_map.rs | 2 +- crates/egui/src/viewport.rs | 1 - crates/egui/src/widgets/button.rs | 6 ++---- crates/egui/src/widgets/checkbox.rs | 4 ++-- crates/egui/src/widgets/color_picker.rs | 2 +- crates/egui/src/widgets/drag_value.rs | 6 +++--- crates/egui/src/widgets/hyperlink.rs | 4 ++-- crates/egui/src/widgets/progress_bar.rs | 4 ++-- crates/egui/src/widgets/radio_button.rs | 2 +- crates/egui/src/widgets/selected_label.rs | 4 +++- crates/egui/src/widgets/slider.rs | 4 ++-- crates/egui/src/widgets/text_edit/builder.rs | 6 +++--- crates/egui/src/widgets/text_edit/state.rs | 2 +- .../egui_demo_app/src/apps/custom3d_glow.rs | 2 +- .../egui_demo_app/src/apps/custom3d_wgpu.rs | 2 +- crates/egui_demo_app/src/backend_panel.rs | 2 +- crates/egui_demo_app/src/lib.rs | 2 +- crates/egui_demo_app/src/main.rs | 2 +- crates/egui_demo_app/src/web.rs | 2 +- crates/egui_demo_app/src/wrap_app.rs | 2 +- crates/egui_demo_app/tests/test_demo_app.rs | 2 +- crates/egui_demo_lib/src/demo/code_example.rs | 2 +- .../src/demo/demo_app_windows.rs | 6 +++--- crates/egui_demo_lib/src/demo/font_book.rs | 2 +- .../src/demo/interactive_container.rs | 2 +- crates/egui_demo_lib/src/demo/modals.rs | 6 +++--- crates/egui_demo_lib/src/demo/paint_bezier.rs | 2 +- crates/egui_demo_lib/src/demo/password.rs | 1 - crates/egui_demo_lib/src/demo/screenshot.rs | 2 +- crates/egui_demo_lib/src/demo/scrolling.rs | 4 ++-- .../src/demo/tests/tessellation_test.rs | 2 +- crates/egui_demo_lib/src/demo/text_edit.rs | 2 +- .../egui_demo_lib/src/demo/toggle_switch.rs | 2 +- .../egui_demo_lib/src/demo/widget_gallery.rs | 4 ++-- crates/egui_extras/src/datepicker/mod.rs | 2 +- crates/egui_extras/src/datepicker/popup.rs | 2 +- crates/egui_extras/src/layout.rs | 2 +- crates/egui_extras/src/lib.rs | 2 +- .../egui_extras/src/loaders/image_loader.rs | 2 +- crates/egui_extras/src/loaders/svg_loader.rs | 2 +- crates/egui_extras/src/loaders/webp_loader.rs | 2 +- crates/egui_extras/src/syntax_highlighting.rs | 9 ++++---- crates/egui_glow/examples/pure_glow.rs | 18 ++++++++-------- crates/egui_glow/src/lib.rs | 2 +- crates/egui_glow/src/painter.rs | 4 +--- crates/egui_glow/src/shader_version.rs | 3 +-- crates/egui_glow/src/vao.rs | 1 - crates/egui_kittest/src/snapshot.rs | 9 ++++---- crates/egui_kittest/tests/menu.rs | 2 +- crates/egui_kittest/tests/popup.rs | 2 +- crates/egui_kittest/tests/regression_tests.rs | 4 ++-- crates/egui_kittest/tests/tests.rs | 2 +- crates/emath/src/lib.rs | 2 +- crates/emath/src/numeric.rs | 4 ++-- crates/emath/src/ordered_float.rs | 2 +- crates/emath/src/smart_aim.rs | 2 +- crates/epaint/src/lib.rs | 2 +- crates/epaint/src/shapes/shape.rs | 2 +- crates/epaint/src/tessellator.rs | 6 +++--- crates/epaint/src/text/font.rs | 8 +++---- crates/epaint/src/text/fonts.rs | 1 - crates/epaint/src/text/text_layout.rs | 2 +- crates/epaint/src/text/text_layout_types.rs | 6 +++--- crates/epaint/src/texture_handle.rs | 6 +++--- examples/custom_style/src/main.rs | 2 +- examples/puffin_profiler/src/main.rs | 2 +- tests/egui_tests/tests/test_widgets.rs | 4 ++-- tests/test_inline_glow_paint/src/main.rs | 2 +- 124 files changed, 243 insertions(+), 244 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a7a5ede7b..80fee9f74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -134,6 +134,7 @@ broken_intra_doc_links = "warn" # See also clippy.toml [workspace.lints.clippy] +allow_attributes = "warn" as_ptr_cast_mut = "warn" await_holding_lock = "warn" bool_to_int_with_if = "warn" @@ -195,13 +196,13 @@ macro_use_imports = "warn" manual_assert = "warn" manual_clamp = "warn" manual_instant_elapsed = "warn" +manual_is_power_of_two = "warn" manual_is_variant_and = "warn" manual_let_else = "warn" manual_ok_or = "warn" manual_string_new = "warn" map_err_ignore = "warn" map_flatten = "warn" -map_unwrap_or = "warn" match_bool = "warn" match_on_vec_items = "warn" match_same_arms = "warn" @@ -222,11 +223,13 @@ needless_for_each = "warn" needless_pass_by_ref_mut = "warn" needless_pass_by_value = "warn" negative_feature_names = "warn" +non_zero_suggestions = "warn" nonstandard_macro_braces = "warn" option_as_ref_cloned = "warn" option_option = "warn" path_buf_push_overwrite = "warn" print_stderr = "warn" +pathbuf_init_then_push = "warn" ptr_as_ptr = "warn" ptr_cast_constness = "warn" pub_underscore_fields = "warn" @@ -240,6 +243,7 @@ ref_patterns = "warn" rest_pat_in_fully_bound_structs = "warn" same_functions_in_if_condition = "warn" semicolon_if_nothing_returned = "warn" +set_contains_or_insert = "warn" single_char_pattern = "warn" single_match_else = "warn" str_split_at_newline = "warn" @@ -252,6 +256,7 @@ string_to_string = "warn" suspicious_command_arg_space = "warn" suspicious_xor_used_as_pow = "warn" todo = "warn" +too_long_first_doc_paragraph = "warn" trailing_empty_array = "warn" trait_duplication_in_bounds = "warn" tuple_array_conversions = "warn" @@ -261,6 +266,7 @@ unimplemented = "warn" uninhabited_references = "warn" uninlined_format_args = "warn" unnecessary_box_returns = "warn" +unnecessary_literal_bound = "warn" unnecessary_safety_doc = "warn" unnecessary_struct_initialization = "warn" unnecessary_wraps = "warn" @@ -268,6 +274,7 @@ unnested_or_patterns = "warn" unused_peekable = "warn" unused_rounding = "warn" unused_self = "warn" +unused_trait_names = "warn" use_self = "warn" useless_transmute = "warn" verbose_file_reads = "warn" @@ -286,6 +293,7 @@ assigning_clones = "allow" # No please let_underscore_must_use = "allow" let_underscore_untyped = "allow" manual_range_contains = "allow" # this one is just worse imho +map_unwrap_or = "allow" # so is this one self_named_module_files = "allow" # Disabled waiting on https://github.com/rust-lang/rust-clippy/issues/9602 significant_drop_tightening = "allow" # Too many false positives wildcard_imports = "allow" # `use crate::*` is useful to avoid merge conflicts when adding/removing imports diff --git a/crates/ecolor/src/rgba.rs b/crates/ecolor/src/rgba.rs index 99fab41cc..3e40af2b0 100644 --- a/crates/ecolor/src/rgba.rs +++ b/crates/ecolor/src/rgba.rs @@ -33,12 +33,11 @@ pub(crate) fn f32_hash(state: &mut H, f: f32) { } else if f.is_nan() { state.write_u8(1); } else { - use std::hash::Hash; + use std::hash::Hash as _; f.to_bits().hash(state); } } -#[allow(clippy::derived_hash_with_manual_eq)] impl std::hash::Hash for Rgba { #[inline] fn hash(&self, state: &mut H) { diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index 562311dc6..fd08fc1ee 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -91,7 +91,7 @@ pub struct CreationContext<'s> { pub(crate) raw_display_handle: Result, } -#[allow(unsafe_code)] +#[expect(unsafe_code)] #[cfg(not(target_arch = "wasm32"))] impl HasWindowHandle for CreationContext<'_> { fn window_handle(&self) -> Result, HandleError> { @@ -100,7 +100,7 @@ impl HasWindowHandle for CreationContext<'_> { } } -#[allow(unsafe_code)] +#[expect(unsafe_code)] #[cfg(not(target_arch = "wasm32"))] impl HasDisplayHandle for CreationContext<'_> { fn display_handle(&self) -> Result, HandleError> { @@ -662,7 +662,7 @@ pub struct Frame { #[cfg(not(target_arch = "wasm32"))] assert_not_impl_any!(Frame: Clone); -#[allow(unsafe_code)] +#[expect(unsafe_code)] #[cfg(not(target_arch = "wasm32"))] impl HasWindowHandle for Frame { fn window_handle(&self) -> Result, HandleError> { @@ -671,7 +671,7 @@ impl HasWindowHandle for Frame { } } -#[allow(unsafe_code)] +#[expect(unsafe_code)] #[cfg(not(target_arch = "wasm32"))] impl HasDisplayHandle for Frame { fn display_handle(&self) -> Result, HandleError> { @@ -703,7 +703,7 @@ impl Frame { /// True if you are in a web environment. /// /// Equivalent to `cfg!(target_arch = "wasm32")` - #[allow(clippy::unused_self)] + #[expect(clippy::unused_self)] pub fn is_web(&self) -> bool { cfg!(target_arch = "wasm32") } diff --git a/crates/eframe/src/lib.rs b/crates/eframe/src/lib.rs index 7b342a4c2..ec27b05a0 100644 --- a/crates/eframe/src/lib.rs +++ b/crates/eframe/src/lib.rs @@ -69,7 +69,7 @@ //! #[wasm_bindgen] //! impl WebHandle { //! /// Installs a panic hook, then returns. -//! #[allow(clippy::new_without_default)] +//! #[expect(clippy::new_without_default)] //! #[wasm_bindgen(constructor)] //! pub fn new() -> Self { //! // Redirect [`log`] message to `console.log` and friends: @@ -236,7 +236,7 @@ pub mod icon_data; /// This function can fail if we fail to set up a graphics context. #[cfg(not(target_arch = "wasm32"))] #[cfg(any(feature = "glow", feature = "wgpu"))] -#[allow(clippy::needless_pass_by_value)] +#[allow(clippy::needless_pass_by_value, clippy::allow_attributes)] pub fn run_native( app_name: &str, mut native_options: NativeOptions, diff --git a/crates/eframe/src/native/app_icon.rs b/crates/eframe/src/native/app_icon.rs index 8591ba2a8..1f1bf52f2 100644 --- a/crates/eframe/src/native/app_icon.rs +++ b/crates/eframe/src/native/app_icon.rs @@ -47,7 +47,7 @@ enum AppIconStatus { NotSetTryAgain, /// We successfully set the icon and it should be visible now. - #[allow(dead_code)] // Not used on Linux + #[allow(dead_code, clippy::allow_attributes)] // Not used on Linux Set, } @@ -71,13 +71,13 @@ fn set_title_and_icon(_title: &str, _icon_data: Option<&IconData>) -> AppIconSta #[cfg(target_os = "macos")] return set_title_and_icon_mac(_title, _icon_data); - #[allow(unreachable_code)] + #[allow(unreachable_code, clippy::allow_attributes)] AppIconStatus::NotSetIgnored } /// Set icon for Windows applications. #[cfg(target_os = "windows")] -#[allow(unsafe_code)] +#[expect(unsafe_code)] fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus { use crate::icon_data::IconDataExt as _; use winapi::um::winuser; @@ -198,12 +198,12 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus { /// Set icon & app title for `MacOS` applications. #[cfg(target_os = "macos")] -#[allow(unsafe_code)] +#[expect(unsafe_code)] fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconStatus { use crate::icon_data::IconDataExt as _; profiling::function_scope!(); - use objc2::ClassType; + use objc2::ClassType as _; use objc2_app_kit::{NSApplication, NSImage}; use objc2_foundation::{NSData, NSString}; diff --git a/crates/eframe/src/native/epi_integration.rs b/crates/eframe/src/native/epi_integration.rs index 03b5f2dcd..aaa8c596a 100644 --- a/crates/eframe/src/native/epi_integration.rs +++ b/crates/eframe/src/native/epi_integration.rs @@ -136,7 +136,7 @@ pub fn create_storage(_app_name: &str) -> Option> { None } -#[allow(clippy::unnecessary_wraps)] +#[expect(clippy::unnecessary_wraps)] pub fn create_storage_with_file(_file: impl Into) -> Option> { #[cfg(feature = "persistence")] return Some(Box::new( @@ -169,7 +169,7 @@ pub struct EpiIntegration { } impl EpiIntegration { - #[allow(clippy::too_many_arguments)] + #[expect(clippy::too_many_arguments)] pub fn new( egui_ctx: egui::Context, window: &winit::window::Window, @@ -326,7 +326,7 @@ impl EpiIntegration { } } - #[allow(clippy::unused_self)] + #[allow(clippy::unused_self, clippy::allow_attributes)] pub fn save(&mut self, _app: &mut dyn epi::App, _window: Option<&winit::window::Window>) { #[cfg(feature = "persistence")] if let Some(storage) = self.frame.storage_mut() { diff --git a/crates/eframe/src/native/event_loop_context.rs b/crates/eframe/src/native/event_loop_context.rs index f1262f853..810db8e1f 100644 --- a/crates/eframe/src/native/event_loop_context.rs +++ b/crates/eframe/src/native/event_loop_context.rs @@ -27,7 +27,7 @@ impl Drop for EventLoopGuard { } // Helper function to safely use the current event loop -#[allow(unsafe_code)] +#[expect(unsafe_code)] pub fn with_current_event_loop(f: F) -> Option where F: FnOnce(&ActiveEventLoop) -> R, diff --git a/crates/eframe/src/native/file_storage.rs b/crates/eframe/src/native/file_storage.rs index 7fde2d288..fa89b9059 100644 --- a/crates/eframe/src/native/file_storage.rs +++ b/crates/eframe/src/native/file_storage.rs @@ -1,6 +1,6 @@ use std::{ collections::HashMap, - io::Write, + io::Write as _, path::{Path, PathBuf}, }; @@ -42,7 +42,7 @@ pub fn storage_dir(app_id: &str) -> Option { // Adapted from // https://github.com/rust-lang/cargo/blob/6e11c77384989726bb4f412a0e23b59c27222c34/crates/home/src/windows.rs#L19-L37 #[cfg(all(windows, not(target_vendor = "uwp")))] -#[allow(unsafe_code)] +#[expect(unsafe_code)] fn roaming_appdata() -> Option { use std::ffi::OsString; use std::os::windows::ffi::OsStringExt; diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index 94b254682..946210c86 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -11,13 +11,13 @@ use std::{cell::RefCell, num::NonZeroU32, rc::Rc, sync::Arc, time::Instant}; use egui_winit::ActionRequested; use glutin::{ - config::GlConfig, - context::NotCurrentGlContext, - display::GetGlDisplay, - prelude::{GlDisplay, PossiblyCurrentGlContext}, - surface::GlSurface, + config::GlConfig as _, + context::NotCurrentGlContext as _, + display::GetGlDisplay as _, + prelude::{GlDisplay as _, PossiblyCurrentGlContext as _}, + surface::GlSurface as _, }; -use raw_window_handle::HasWindowHandle; +use raw_window_handle::HasWindowHandle as _; use winit::{ event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy}, window::{Window, WindowId}, @@ -139,7 +139,7 @@ impl<'app> GlowWinitApp<'app> { } } - #[allow(unsafe_code)] + #[expect(unsafe_code)] fn create_glutin_windowed_context( egui_ctx: &egui::Context, event_loop: &ActiveEventLoop, @@ -901,7 +901,7 @@ fn change_gl_context( } impl GlutinWindowContext { - #[allow(unsafe_code)] + #[expect(unsafe_code)] unsafe fn new( egui_ctx: &egui::Context, viewport_builder: ViewportBuilder, @@ -1094,7 +1094,7 @@ impl GlutinWindowContext { } /// Create a surface, window, and winit integration for the viewport, if missing. - #[allow(unsafe_code)] + #[expect(unsafe_code)] pub(crate) fn initialize_window( &mut self, viewport_id: ViewportId, @@ -1566,6 +1566,6 @@ fn save_screenshot_and_exit( }); log::info!("Screenshot saved to {path:?}."); - #[allow(clippy::exit)] + #[expect(clippy::exit)] std::process::exit(0); } diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs index 641459b52..9bcb49686 100644 --- a/crates/eframe/src/native/run.rs +++ b/crates/eframe/src/native/run.rs @@ -163,7 +163,6 @@ impl WinitAppWrapper { log::debug!("Exiting with return code 0"); - #[allow(clippy::exit)] std::process::exit(0); } } @@ -317,7 +316,7 @@ impl ApplicationHandler for WinitAppWrapper { #[cfg(not(target_os = "ios"))] fn run_and_return(event_loop: &mut EventLoop, winit_app: impl WinitApp) -> Result { - use winit::platform::run_on_demand::EventLoopExtRunOnDemand; + use winit::platform::run_on_demand::EventLoopExtRunOnDemand as _; log::trace!("Entering the winit event loop (run_app_on_demand)…"); diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index 60484cdee..96e93300e 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -15,7 +15,7 @@ use winit::{ window::{Window, WindowId}, }; -use ahash::{HashMap, HashSet, HashSetExt}; +use ahash::{HashMap, HashSet, HashSetExt as _}; use egui::{ DeferredViewportUiCallback, FullOutput, ImmediateViewport, ViewportBuilder, ViewportClass, ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportInfo, ViewportOutput, @@ -182,7 +182,6 @@ impl<'app> WgpuWinitApp<'app> { builder: ViewportBuilder, ) -> crate::Result<&mut WgpuWinitRunning<'app>> { profiling::function_scope!(); - #[allow(unsafe_code, unused_mut, unused_unsafe)] let mut painter = pollster::block_on(egui_wgpu::winit::Painter::new( egui_ctx.clone(), self.native_options.wgpu_options.clone(), @@ -236,7 +235,7 @@ impl<'app> WgpuWinitApp<'app> { }); } - #[allow(unused_mut)] // used for accesskit + #[allow(unused_mut, clippy::allow_attributes)] // used for accesskit let mut egui_winit = egui_winit::State::new( egui_ctx.clone(), ViewportId::ROOT, diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index 76ae1761f..675c57bec 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -2,10 +2,10 @@ use egui::{TexturesDelta, UserData, ViewportCommand}; use crate::{epi, App}; -use super::{now_sec, text_agent::TextAgent, web_painter::WebPainter, NeedRepaint}; +use super::{now_sec, text_agent::TextAgent, web_painter::WebPainter as _, NeedRepaint}; pub struct AppRunner { - #[allow(dead_code)] + #[allow(dead_code, clippy::allow_attributes)] pub(crate) web_options: crate::WebOptions, pub(crate) frame: epi::Frame, egui_ctx: egui::Context, diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index 32fba9ef0..1782a6797 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -4,7 +4,7 @@ use super::{ button_from_mouse_event, location_hash, modifiers_from_kb_event, modifiers_from_mouse_event, modifiers_from_wheel_event, native_pixels_per_point, pos_from_mouse_event, prefers_color_scheme_dark, primary_touch_pos, push_touches, text_from_keyboard_event, - theme_from_dark_mode, translate_key, AppRunner, Closure, JsCast, JsValue, WebRunner, + theme_from_dark_mode, translate_key, AppRunner, Closure, JsCast as _, JsValue, WebRunner, DEBUG_RESIZE, }; @@ -163,7 +163,7 @@ fn install_keydown(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), J ) } -#[allow(clippy::needless_pass_by_value)] // So that we can pass it directly to `add_event_listener` +#[expect(clippy::needless_pass_by_value)] // So that we can pass it directly to `add_event_listener` pub(crate) fn on_keydown(event: web_sys::KeyboardEvent, runner: &mut AppRunner) { let has_focus = runner.input.raw.focused; if !has_focus { @@ -261,7 +261,7 @@ fn install_keyup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsV runner_ref.add_event_listener(target, "keyup", on_keyup) } -#[allow(clippy::needless_pass_by_value)] // So that we can pass it directly to `add_event_listener` +#[expect(clippy::needless_pass_by_value)] // So that we can pass it directly to `add_event_listener` pub(crate) fn on_keyup(event: web_sys::KeyboardEvent, runner: &mut AppRunner) { let modifiers = modifiers_from_kb_event(&event); runner.input.raw.modifiers = modifiers; diff --git a/crates/eframe/src/web/mod.rs b/crates/eframe/src/web/mod.rs index c67fa69e6..2bdd3af63 100644 --- a/crates/eframe/src/web/mod.rs +++ b/crates/eframe/src/web/mod.rs @@ -281,7 +281,7 @@ fn create_clipboard_item(mime: &str, bytes: &[u8]) -> Result &str { if let Some(i) = file_path.rfind("/src/") { if let Some(prev_slash) = file_path[..i].rfind('/') { diff --git a/crates/eframe/src/web/web_painter_glow.rs b/crates/eframe/src/web/web_painter_glow.rs index 876a6d78e..ca6e11bf5 100644 --- a/crates/eframe/src/web/web_painter_glow.rs +++ b/crates/eframe/src/web/web_painter_glow.rs @@ -1,7 +1,7 @@ use egui::{Event, UserData, ViewportId}; use egui_glow::glow; use std::sync::Arc; -use wasm_bindgen::JsCast; +use wasm_bindgen::JsCast as _; use wasm_bindgen::JsValue; use web_sys::HtmlCanvasElement; @@ -27,7 +27,8 @@ impl WebPainterGlow { ) -> Result { let (gl, shader_prefix) = init_glow_context_from_canvas(&canvas, options.webgl_context_option)?; - #[allow(clippy::arc_with_non_send_sync)] + + #[allow(clippy::arc_with_non_send_sync, clippy::allow_attributes)] // For wasm let gl = std::sync::Arc::new(gl); let painter = egui_glow::Painter::new(gl, shader_prefix, None, options.dithering) diff --git a/crates/eframe/src/web/web_painter_wgpu.rs b/crates/eframe/src/web/web_painter_wgpu.rs index f018c7316..3cfc53f1c 100644 --- a/crates/eframe/src/web/web_painter_wgpu.rs +++ b/crates/eframe/src/web/web_painter_wgpu.rs @@ -23,7 +23,7 @@ pub(crate) struct WebPainterWgpu { } impl WebPainterWgpu { - #[allow(unused)] // only used if `wgpu` is the only active feature. + #[expect(unused)] // only used if `wgpu` is the only active feature. pub fn render_state(&self) -> Option { self.render_state.clone() } @@ -55,7 +55,7 @@ impl WebPainterWgpu { }) } - #[allow(unused)] // only used if `wgpu` is the only active feature. + #[expect(unused)] // only used if `wgpu` is the only active feature. pub async fn new( ctx: egui::Context, canvas: web_sys::HtmlCanvasElement, diff --git a/crates/eframe/src/web/web_runner.rs b/crates/eframe/src/web/web_runner.rs index 4c43fcb69..099be7aeb 100644 --- a/crates/eframe/src/web/web_runner.rs +++ b/crates/eframe/src/web/web_runner.rs @@ -37,7 +37,7 @@ pub struct WebRunner { impl WebRunner { /// Will install a panic handler that will catch and log any panics - #[allow(clippy::new_without_default)] + #[expect(clippy::new_without_default)] pub fn new() -> Self { let panic_handler = PanicHandler::install(); @@ -280,7 +280,7 @@ struct TargetEvent { closure: Closure, } -#[allow(unused)] +#[expect(unused)] struct IntervalHandle { handle: i32, closure: Closure, @@ -289,7 +289,7 @@ struct IntervalHandle { enum EventToUnsubscribe { TargetEvent(TargetEvent), - #[allow(unused)] + #[expect(unused)] IntervalHandle(IntervalHandle), } diff --git a/crates/egui-wgpu/src/capture.rs b/crates/egui-wgpu/src/capture.rs index 40cf5484f..b4072a8b1 100644 --- a/crates/egui-wgpu/src/capture.rs +++ b/crates/egui-wgpu/src/capture.rs @@ -4,6 +4,7 @@ use std::sync::{mpsc, Arc}; use wgpu::{BindGroupLayout, MultisampleState, StoreOp}; /// A texture and a buffer for reading the rendered frame back to the cpu. +/// /// The texture is required since [`wgpu::TextureUsages::COPY_SRC`] is not an allowed /// flag for the surface texture on all platforms. This means that anytime we want to /// capture the frame, we first render it to this texture, and then we can copy it to @@ -125,7 +126,7 @@ impl CaptureState { // It would be more efficient to reuse the Buffer, e.g. via some kind of ring buffer, but // for most screenshot use cases this should be fine. When taking many screenshots (e.g. for a video) // it might make sense to revisit this and implement a more efficient solution. - #[allow(clippy::arc_with_non_send_sync)] + #[allow(clippy::arc_with_non_send_sync, clippy::allow_attributes)] // For wasm let buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("egui_screen_capture_buffer"), size: (self.padding.padded_bytes_per_row * self.texture.height()) as u64, @@ -184,7 +185,7 @@ impl CaptureState { tx: CaptureSender, viewport_id: ViewportId, ) { - #[allow(clippy::arc_with_non_send_sync)] + #[allow(clippy::arc_with_non_send_sync, clippy::allow_attributes)] // For wasm let buffer = Arc::new(buffer); let buffer_clone = buffer.clone(); let buffer_slice = buffer_clone.slice(..); diff --git a/crates/egui-wgpu/src/lib.rs b/crates/egui-wgpu/src/lib.rs index e19c26574..5df19db85 100644 --- a/crates/egui-wgpu/src/lib.rs +++ b/crates/egui-wgpu/src/lib.rs @@ -242,7 +242,7 @@ impl RenderState { // On wasm, depending on feature flags, wgpu objects may or may not implement sync. // It doesn't make sense to switch to Rc for that special usecase, so simply disable the lint. - #[allow(clippy::arc_with_non_send_sync)] + #[allow(clippy::arc_with_non_send_sync, clippy::allow_attributes)] // For wasm Ok(Self { adapter, #[cfg(not(target_arch = "wasm32"))] diff --git a/crates/egui-wgpu/src/renderer.rs b/crates/egui-wgpu/src/renderer.rs index 5a5c953bb..0dc746320 100644 --- a/crates/egui-wgpu/src/renderer.rs +++ b/crates/egui-wgpu/src/renderer.rs @@ -3,7 +3,7 @@ use std::{borrow::Cow, num::NonZeroU64, ops::Range}; use ahash::HashMap; -use epaint::{emath::NumExt, PaintCallbackInfo, Primitive, Vertex}; +use epaint::{emath::NumExt as _, PaintCallbackInfo, Primitive, Vertex}; use wgpu::util::DeviceExt as _; @@ -749,7 +749,7 @@ impl Renderer { /// /// The texture must have the format [`wgpu::TextureFormat::Rgba8UnormSrgb`]. /// Any compare function supplied in the [`wgpu::SamplerDescriptor`] will be ignored. - #[allow(clippy::needless_pass_by_value)] // false positive + #[expect(clippy::needless_pass_by_value)] // false positive pub fn register_native_texture_with_sampler_options( &mut self, device: &wgpu::Device, @@ -796,7 +796,7 @@ impl Renderer { /// [`wgpu::SamplerDescriptor`] options. /// /// This allows applications to reuse [`epaint::TextureId`]s created with custom sampler options. - #[allow(clippy::needless_pass_by_value)] // false positive + #[expect(clippy::needless_pass_by_value)] // false positive pub fn update_egui_texture_from_wgpu_texture_with_sampler_options( &mut self, device: &wgpu::Device, diff --git a/crates/egui-wgpu/src/setup.rs b/crates/egui-wgpu/src/setup.rs index 1f70b6bb7..2499d006b 100644 --- a/crates/egui-wgpu/src/setup.rs +++ b/crates/egui-wgpu/src/setup.rs @@ -48,7 +48,7 @@ impl WgpuSetup { pub async fn new_instance(&self) -> wgpu::Instance { match self { Self::CreateNew(create_new) => { - #[allow(unused_mut)] + #[allow(unused_mut, clippy::allow_attributes)] let mut backends = create_new.instance_descriptor.backends; // Don't try WebGPU if we're not in a secure context. diff --git a/crates/egui-wgpu/src/winit.rs b/crates/egui-wgpu/src/winit.rs index cc3a7db2c..e0eafee71 100644 --- a/crates/egui-wgpu/src/winit.rs +++ b/crates/egui-wgpu/src/winit.rs @@ -575,7 +575,7 @@ impl Painter { .retain(|id, _| active_viewports.contains(id)); } - #[allow(clippy::needless_pass_by_ref_mut, clippy::unused_self)] + #[expect(clippy::needless_pass_by_ref_mut, clippy::unused_self)] pub fn destroy(&mut self) { // TODO(emilk): something here? } diff --git a/crates/egui-winit/src/clipboard.rs b/crates/egui-winit/src/clipboard.rs index c8adcad21..a0221294e 100644 --- a/crates/egui-winit/src/clipboard.rs +++ b/crates/egui-winit/src/clipboard.rs @@ -161,7 +161,7 @@ fn init_smithay_clipboard( if let Some(RawDisplayHandle::Wayland(display)) = raw_display_handle { log::trace!("Initializing smithay clipboard…"); - #[allow(unsafe_code)] + #[expect(unsafe_code)] Some(unsafe { smithay_clipboard::Clipboard::new(display.display.as_ptr()) }) } else { #[cfg(feature = "wayland")] diff --git a/crates/egui/src/cache/cache_trait.rs b/crates/egui/src/cache/cache_trait.rs index 73cb61f38..fc00a9880 100644 --- a/crates/egui/src/cache/cache_trait.rs +++ b/crates/egui/src/cache/cache_trait.rs @@ -1,5 +1,5 @@ /// A cache, storing some value for some length of time. -#[allow(clippy::len_without_is_empty)] +#[expect(clippy::len_without_is_empty)] pub trait CacheTrait: 'static + Send + Sync { /// Call once per frame to evict cache. fn update(&mut self); diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index 55cdb0755..0d21e8bdd 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -5,8 +5,8 @@ use emath::GuiRounding as _; use crate::{ - emath, pos2, Align2, Context, Id, InnerResponse, LayerId, Layout, NumExt, Order, Pos2, Rect, - Response, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetRect, WidgetWithState, + emath, pos2, Align2, Context, Id, InnerResponse, LayerId, Layout, NumExt as _, Order, Pos2, + Rect, Response, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetRect, WidgetWithState, }; /// State of an [`Area`] that is persisted between frames. @@ -602,7 +602,7 @@ impl Prepared { self.move_response.id } - #[allow(clippy::needless_pass_by_value)] // intentional to swallow up `content_ui`. + #[expect(clippy::needless_pass_by_value)] // intentional to swallow up `content_ui`. pub(crate) fn end(self, ctx: &Context, content_ui: Ui) -> Response { let Self { info: _, diff --git a/crates/egui/src/containers/collapsing_header.rs b/crates/egui/src/containers/collapsing_header.rs index c34e56294..66e024f0b 100644 --- a/crates/egui/src/containers/collapsing_header.rs +++ b/crates/egui/src/containers/collapsing_header.rs @@ -1,7 +1,7 @@ use std::hash::Hash; use crate::{ - emath, epaint, pos2, remap, remap_clamp, vec2, Context, Id, InnerResponse, NumExt, Rect, + emath, epaint, pos2, remap, remap_clamp, vec2, Context, Id, InnerResponse, NumExt as _, Rect, Response, Sense, Stroke, TextStyle, TextWrapMode, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetInfo, WidgetText, WidgetType, }; diff --git a/crates/egui/src/containers/combo_box.rs b/crates/egui/src/containers/combo_box.rs index 7a5a160f9..5544e759d 100644 --- a/crates/egui/src/containers/combo_box.rs +++ b/crates/egui/src/containers/combo_box.rs @@ -2,11 +2,11 @@ use epaint::Shape; use crate::{ epaint, style::StyleModifier, style::WidgetVisuals, vec2, Align2, Context, Id, InnerResponse, - NumExt, Painter, Popup, PopupCloseBehavior, Rect, Response, ScrollArea, Sense, Stroke, + NumExt as _, Painter, Popup, PopupCloseBehavior, Rect, Response, ScrollArea, Sense, Stroke, TextStyle, TextWrapMode, Ui, UiBuilder, Vec2, WidgetInfo, WidgetText, WidgetType, }; -#[allow(unused_imports)] // Documentation +#[expect(unused_imports)] // Documentation use crate::style::Spacing; /// A function that paints the [`ComboBox`] icon @@ -297,7 +297,7 @@ impl ComboBox { } } -#[allow(clippy::too_many_arguments)] +#[expect(clippy::too_many_arguments)] fn combo_box_dyn<'c, R>( ui: &mut Ui, button_id: Id, diff --git a/crates/egui/src/containers/menu.rs b/crates/egui/src/containers/menu.rs index 7511d07d1..228bbd86d 100644 --- a/crates/egui/src/containers/menu.rs +++ b/crates/egui/src/containers/menu.rs @@ -1,7 +1,7 @@ use crate::style::StyleModifier; use crate::{ Button, Color32, Context, Frame, Id, InnerResponse, Layout, Popup, PopupCloseBehavior, - Response, Style, Ui, UiBuilder, UiKind, UiStack, UiStackInfo, Widget, WidgetText, + Response, Style, Ui, UiBuilder, UiKind, UiStack, UiStackInfo, Widget as _, WidgetText, }; use emath::{vec2, Align, RectAlign, Vec2}; use epaint::Stroke; @@ -159,6 +159,7 @@ impl MenuState { } /// Horizontal menu bar where you can add [`MenuButton`]s. +/// /// The menu bar goes well in a [`crate::TopBottomPanel::top`], /// but can also be placed in a [`crate::Window`]. /// In the latter case you may want to wrap it in [`Frame`]. diff --git a/crates/egui/src/containers/old_popup.rs b/crates/egui/src/containers/old_popup.rs index fc4da4e23..3e5de650f 100644 --- a/crates/egui/src/containers/old_popup.rs +++ b/crates/egui/src/containers/old_popup.rs @@ -4,7 +4,7 @@ use crate::containers::tooltip::Tooltip; use crate::{ Align, Context, Id, LayerId, Layout, Popup, PopupAnchor, PopupCloseBehavior, Pos2, Rect, - Response, Ui, Widget, WidgetText, + Response, Ui, Widget as _, WidgetText, }; use emath::RectAlign; // ---------------------------------------------------------------------------- @@ -19,7 +19,7 @@ use emath::RectAlign; /// /// ``` /// # egui::__run_test_ui(|ui| { -/// # #[allow(deprecated)] +/// # #[expect(deprecated)] /// if ui.ui_contains_pointer() { /// egui::show_tooltip(ui.ctx(), ui.layer_id(), egui::Id::new("my_tooltip"), |ui| { /// ui.label("Helpful text"); @@ -177,7 +177,7 @@ pub fn popup_below_widget( /// } /// let below = egui::AboveOrBelow::Below; /// let close_on_click_outside = egui::PopupCloseBehavior::CloseOnClickOutside; -/// # #[allow(deprecated)] +/// # #[expect(deprecated)] /// egui::popup_above_or_below_widget(ui, popup_id, &response, below, close_on_click_outside, |ui| { /// ui.set_min_width(200.0); // if you want to control the size /// ui.label("Some more info, or things you can select:"); diff --git a/crates/egui/src/containers/panel.rs b/crates/egui/src/containers/panel.rs index 6fd19e186..db0e8a7f9 100644 --- a/crates/egui/src/containers/panel.rs +++ b/crates/egui/src/containers/panel.rs @@ -18,7 +18,7 @@ use emath::GuiRounding as _; use crate::{ - lerp, vec2, Align, Context, CursorIcon, Frame, Id, InnerResponse, LayerId, Layout, NumExt, + lerp, vec2, Align, Context, CursorIcon, Frame, Id, InnerResponse, LayerId, Layout, NumExt as _, Rangef, Rect, Sense, Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, }; diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index 7159a0525..47e800d22 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -466,7 +466,7 @@ impl<'a> Popup<'a> { }; RectAlign::find_best_align( - #[allow(clippy::iter_on_empty_collections)] + #[expect(clippy::iter_on_empty_collections)] once(self.rect_align).chain( self.alternative_aligns // Need the empty slice so the iters have the same type so we can unwrap_or diff --git a/crates/egui/src/containers/resize.rs b/crates/egui/src/containers/resize.rs index 0df9a1136..fe3861646 100644 --- a/crates/egui/src/containers/resize.rs +++ b/crates/egui/src/containers/resize.rs @@ -1,6 +1,6 @@ use crate::{ - pos2, vec2, Align2, Color32, Context, CursorIcon, Id, NumExt, Rect, Response, Sense, Shape, Ui, - UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b, + pos2, vec2, Align2, Color32, Context, CursorIcon, Id, NumExt as _, Rect, Response, Sense, + Shape, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b, }; #[derive(Clone, Copy, Debug)] diff --git a/crates/egui/src/containers/scene.rs b/crates/egui/src/containers/scene.rs index d023a4d3b..e5a350327 100644 --- a/crates/egui/src/containers/scene.rs +++ b/crates/egui/src/containers/scene.rs @@ -1,6 +1,6 @@ use core::f32; -use emath::{GuiRounding, Pos2}; +use emath::{GuiRounding as _, Pos2}; use crate::{ emath::TSTransform, InnerResponse, LayerId, PointerButton, Rangef, Rect, Response, Sense, Ui, diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index c430b6ea9..b2df2100b 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -1,8 +1,8 @@ #![allow(clippy::needless_range_loop)] use crate::{ - emath, epaint, lerp, pass_state, pos2, remap, remap_clamp, Context, Id, NumExt, Pos2, Rangef, - Rect, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b, + emath, epaint, lerp, pass_state, pos2, remap, remap_clamp, Context, Id, NumExt as _, Pos2, + Rangef, Rect, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b, }; #[derive(Clone, Copy, Debug)] diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 6ec79f879..47be4f3d3 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -34,9 +34,10 @@ use crate::{ viewport::ViewportClass, Align2, CursorIcon, DeferredViewportUiCallback, FontDefinitions, Grid, Id, ImmediateViewport, ImmediateViewportRendererCallback, Key, KeyboardShortcut, Label, LayerId, Memory, - ModifierNames, NumExt, Order, Painter, RawInput, Response, RichText, ScrollArea, Sense, Style, - TextStyle, TextureHandle, TextureOptions, Ui, ViewportBuilder, ViewportCommand, ViewportId, - ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportOutput, Widget, WidgetRect, WidgetText, + ModifierNames, NumExt as _, Order, Painter, RawInput, Response, RichText, ScrollArea, Sense, + Style, TextStyle, TextureHandle, TextureOptions, Ui, ViewportBuilder, ViewportCommand, + ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportOutput, Widget as _, + WidgetRect, WidgetText, }; #[cfg(feature = "accesskit")] @@ -314,7 +315,7 @@ impl std::fmt::Display for RepaintCause { impl RepaintCause { /// Capture the file and line number of the call site. - #[allow(clippy::new_without_default)] + #[expect(clippy::new_without_default)] #[track_caller] pub fn new() -> Self { let caller = Location::caller(); @@ -327,7 +328,6 @@ impl RepaintCause { /// Capture the file and line number of the call site, /// as well as add a reason. - #[allow(clippy::new_without_default)] #[track_caller] pub fn new_reason(reason: impl Into>) -> Self { let caller = Location::caller(); @@ -1160,7 +1160,6 @@ impl Context { /// /// `allow_focus` should usually be true, unless you call this function multiple times with the /// same widget, then `allow_focus` should only be true once (like in [`Ui::new`] (true) and [`Ui::remember_min_rect`] (false)). - #[allow(clippy::too_many_arguments)] pub(crate) fn create_widget(&self, w: WidgetRect, allow_focus: bool) -> Response { let interested_in_focus = w.enabled && w.sense.is_focusable() @@ -1189,7 +1188,7 @@ impl Context { self.check_for_id_clash(w.id, w.rect, "widget"); } - #[allow(clippy::let_and_return)] + #[allow(clippy::let_and_return, clippy::allow_attributes)] let res = self.get_response(w); #[cfg(feature = "accesskit")] @@ -2350,7 +2349,6 @@ impl ContextImpl { // Inform the backend of all textures that have been updated (including font atlas). let textures_delta = self.tex_manager.0.write().take_delta(); - #[cfg_attr(not(feature = "accesskit"), allow(unused_mut))] let mut platform_output: PlatformOutput = std::mem::take(&mut viewport.output); #[cfg(feature = "accesskit")] @@ -2651,7 +2649,7 @@ impl Context { /// Is an egui context menu open? /// /// This only works with the old, deprecated [`crate::menu`] API. - #[allow(deprecated)] + #[expect(deprecated)] #[deprecated = "Use `is_popup_open` instead"] pub fn is_context_menu_open(&self) -> bool { self.data(|d| { @@ -3205,7 +3203,7 @@ impl Context { } }); - #[allow(deprecated)] + #[expect(deprecated)] ui.horizontal(|ui| { ui.label(format!( "{} menu bars", @@ -3263,7 +3261,7 @@ impl Context { /// the function is still called, but with no other effect. /// /// No locks are held while the given closure is called. - #[allow(clippy::unused_self, clippy::let_and_return)] + #[allow(clippy::unused_self, clippy::let_and_return, clippy::allow_attributes)] #[inline] pub fn with_accessibility_parent(&self, _id: Id, f: impl FnOnce() -> R) -> R { // TODO(emilk): this isn't thread-safe - another thread can call this function between the push/pop calls @@ -3596,7 +3594,6 @@ impl Context { /// * Set the window attributes (position, size, …) based on [`ImmediateViewport::builder`]. /// * Call [`Context::run`] with [`ImmediateViewport::viewport_ui_cb`]. /// * Handle the output from [`Context::run`], including rendering - #[allow(clippy::unused_self)] pub fn set_immediate_viewport_renderer( callback: impl for<'a> Fn(&Self, ImmediateViewport<'a>) + 'static, ) { diff --git a/crates/egui/src/data/input.rs b/crates/egui/src/data/input.rs index cc8725248..9f15f1c43 100644 --- a/crates/egui/src/data/input.rs +++ b/crates/egui/src/data/input.rs @@ -1233,7 +1233,7 @@ pub struct EventFilter { pub escape: bool, } -#[allow(clippy::derivable_impls)] // let's be explicit +#[expect(clippy::derivable_impls)] // let's be explicit impl Default for EventFilter { fn default() -> Self { Self { diff --git a/crates/egui/src/data/output.rs b/crates/egui/src/data/output.rs index 2fdaec1e1..ab9044cb6 100644 --- a/crates/egui/src/data/output.rs +++ b/crates/egui/src/data/output.rs @@ -252,7 +252,7 @@ pub struct OpenUrl { } impl OpenUrl { - #[allow(clippy::needless_pass_by_value)] + #[expect(clippy::needless_pass_by_value)] pub fn same_tab(url: impl ToString) -> Self { Self { url: url.to_string(), @@ -260,7 +260,7 @@ impl OpenUrl { } } - #[allow(clippy::needless_pass_by_value)] + #[expect(clippy::needless_pass_by_value)] pub fn new_tab(url: impl ToString) -> Self { Self { url: url.to_string(), @@ -607,7 +607,7 @@ impl WidgetInfo { } } - #[allow(clippy::needless_pass_by_value)] + #[expect(clippy::needless_pass_by_value)] pub fn labeled(typ: WidgetType, enabled: bool, label: impl ToString) -> Self { Self { enabled, @@ -617,7 +617,7 @@ impl WidgetInfo { } /// checkboxes, radio-buttons etc - #[allow(clippy::needless_pass_by_value)] + #[expect(clippy::needless_pass_by_value)] pub fn selected(typ: WidgetType, enabled: bool, selected: bool, label: impl ToString) -> Self { Self { enabled, @@ -635,7 +635,7 @@ impl WidgetInfo { } } - #[allow(clippy::needless_pass_by_value)] + #[expect(clippy::needless_pass_by_value)] pub fn slider(enabled: bool, value: f64, label: impl ToString) -> Self { let label = label.to_string(); Self { @@ -646,7 +646,7 @@ impl WidgetInfo { } } - #[allow(clippy::needless_pass_by_value)] + #[expect(clippy::needless_pass_by_value)] pub fn text_edit( enabled: bool, prev_text_value: impl ToString, @@ -670,7 +670,7 @@ impl WidgetInfo { } } - #[allow(clippy::needless_pass_by_value)] + #[expect(clippy::needless_pass_by_value)] pub fn text_selection_changed( enabled: bool, text_selection: std::ops::RangeInclusive, diff --git a/crates/egui/src/grid.rs b/crates/egui/src/grid.rs index 9ad386cc6..dbc22a09d 100644 --- a/crates/egui/src/grid.rs +++ b/crates/egui/src/grid.rs @@ -1,8 +1,8 @@ use emath::GuiRounding as _; use crate::{ - vec2, Align2, Color32, Context, Id, InnerResponse, NumExt, Painter, Rect, Region, Style, Ui, - UiBuilder, Vec2, + vec2, Align2, Color32, Context, Id, InnerResponse, NumExt as _, Painter, Rect, Region, Style, + Ui, UiBuilder, Vec2, }; #[cfg(debug_assertions)] @@ -184,7 +184,7 @@ impl GridLayout { Rect::from_min_size(cursor.min, size).round_ui() } - #[allow(clippy::unused_self)] + #[expect(clippy::unused_self)] pub(crate) fn align_size_within_rect(&self, size: Vec2, frame: Rect) -> Rect { // TODO(emilk): allow this alignment to be customized Align2::LEFT_CENTER diff --git a/crates/egui/src/id.rs b/crates/egui/src/id.rs index ddc1a59b7..7bcef8dc2 100644 --- a/crates/egui/src/id.rs +++ b/crates/egui/src/id.rs @@ -59,7 +59,7 @@ impl Id { /// Generate a new [`Id`] by hashing the parent [`Id`] and the given argument. pub fn with(self, child: impl std::hash::Hash) -> Self { - use std::hash::{BuildHasher, Hasher}; + use std::hash::{BuildHasher as _, Hasher as _}; let mut hasher = ahash::RandomState::with_seeds(1, 2, 3, 4).build_hasher(); hasher.write_u64(self.0.get()); child.hash(&mut hasher); diff --git a/crates/egui/src/input_state/mod.rs b/crates/egui/src/input_state/mod.rs index a5fc83ee3..a5d6951ce 100644 --- a/crates/egui/src/input_state/mod.rs +++ b/crates/egui/src/input_state/mod.rs @@ -5,7 +5,7 @@ use crate::data::input::{ TouchDeviceId, ViewportInfo, NUM_POINTER_BUTTONS, }; use crate::{ - emath::{vec2, NumExt, Pos2, Rect, Vec2}, + emath::{vec2, NumExt as _, Pos2, Rect, Vec2}, util::History, }; use std::{ @@ -344,7 +344,7 @@ impl InputState { let is_zoom = modifiers.ctrl || modifiers.mac_cmd || modifiers.command; - #[allow(clippy::collapsible_else_if)] + #[expect(clippy::collapsible_else_if)] if is_zoom { if is_smooth { smooth_scroll_delta_for_zoom += delta.y; diff --git a/crates/egui/src/introspection.rs b/crates/egui/src/introspection.rs index e21ddb3a5..8826eca79 100644 --- a/crates/egui/src/introspection.rs +++ b/crates/egui/src/introspection.rs @@ -1,7 +1,7 @@ //! Showing UI:s for egui/epaint types. use crate::{ epaint, memory, pos2, remap_clamp, vec2, Color32, CursorIcon, FontFamily, FontId, Label, Mesh, - NumExt, Rect, Response, Sense, Shape, Slider, TextStyle, TextWrapMode, Ui, Widget, + NumExt as _, Rect, Response, Sense, Shape, Slider, TextStyle, TextWrapMode, Ui, Widget, }; pub fn font_family_ui(ui: &mut Ui, font_family: &mut FontFamily) { diff --git a/crates/egui/src/layout.rs b/crates/egui/src/layout.rs index 91f39fb2d..a0665246b 100644 --- a/crates/egui/src/layout.rs +++ b/crates/egui/src/layout.rs @@ -1,7 +1,7 @@ use emath::GuiRounding as _; use crate::{ - emath::{pos2, vec2, Align2, NumExt, Pos2, Rect, Vec2}, + emath::{pos2, vec2, Align2, NumExt as _, Pos2, Rect, Vec2}, Align, }; const INFINITY: f32 = f32::INFINITY; diff --git a/crates/egui/src/load.rs b/crates/egui/src/load.rs index 26950eb2a..6e30d4ca5 100644 --- a/crates/egui/src/load.rs +++ b/crates/egui/src/load.rs @@ -64,7 +64,7 @@ use std::{ use ahash::HashMap; -use emath::{Float, OrderedFloat}; +use emath::{Float as _, OrderedFloat}; use epaint::{mutex::Mutex, textures::TextureOptions, ColorImage, TextureHandle, TextureId, Vec2}; use crate::Context; diff --git a/crates/egui/src/load/bytes_loader.rs b/crates/egui/src/load/bytes_loader.rs index 9c70f54e1..5f3e0d3bc 100644 --- a/crates/egui/src/load/bytes_loader.rs +++ b/crates/egui/src/load/bytes_loader.rs @@ -28,7 +28,7 @@ impl DefaultBytesLoader { } impl BytesLoader for DefaultBytesLoader { - fn id(&self) -> &str { + fn id(&self) -> &'static str { generate_loader_id!(DefaultBytesLoader) } diff --git a/crates/egui/src/load/texture_loader.rs b/crates/egui/src/load/texture_loader.rs index a1170257c..4e9dc1e16 100644 --- a/crates/egui/src/load/texture_loader.rs +++ b/crates/egui/src/load/texture_loader.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; use super::{ - BytesLoader, Context, HashMap, ImagePoll, Mutex, SizeHint, SizedTexture, TextureHandle, + BytesLoader as _, Context, HashMap, ImagePoll, Mutex, SizeHint, SizedTexture, TextureHandle, TextureLoadResult, TextureLoader, TextureOptions, TexturePoll, }; @@ -11,7 +11,7 @@ pub struct DefaultTextureLoader { } impl TextureLoader for DefaultTextureLoader { - fn id(&self) -> &str { + fn id(&self) -> &'static str { crate::generate_loader_id!(DefaultTextureLoader) } diff --git a/crates/egui/src/menu.rs b/crates/egui/src/menu.rs index ef84451db..8de1fe951 100644 --- a/crates/egui/src/menu.rs +++ b/crates/egui/src/menu.rs @@ -23,8 +23,8 @@ use super::{ use crate::{ epaint, vec2, widgets::{Button, ImageButton}, - Align2, Area, Color32, Frame, Key, LayerId, Layout, NumExt, Order, Stroke, Style, TextWrapMode, - UiKind, WidgetText, + Align2, Area, Color32, Frame, Key, LayerId, Layout, NumExt as _, Order, Stroke, Style, + TextWrapMode, UiKind, WidgetText, }; use epaint::mutex::RwLock; use std::sync::Arc; diff --git a/crates/egui/src/os.rs b/crates/egui/src/os.rs index 766ecf55a..e7497160b 100644 --- a/crates/egui/src/os.rs +++ b/crates/egui/src/os.rs @@ -1,5 +1,5 @@ /// An `enum` of common operating systems. -#[allow(clippy::upper_case_acronyms)] // `Ios` looks too ugly +#[expect(clippy::upper_case_acronyms)] // `Ios` looks too ugly #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum OperatingSystem { /// Unknown OS - could be wasm diff --git a/crates/egui/src/painter.rs b/crates/egui/src/painter.rs index 22a0a0a9d..732d78ee8 100644 --- a/crates/egui/src/painter.rs +++ b/crates/egui/src/painter.rs @@ -294,7 +294,7 @@ impl Painter { /// ## Debug painting impl Painter { - #[allow(clippy::needless_pass_by_value)] + #[expect(clippy::needless_pass_by_value)] pub fn debug_rect(&self, rect: Rect, color: Color32, text: impl ToString) { self.rect( rect, @@ -320,7 +320,7 @@ impl Painter { /// Text with a background. /// /// See also [`Context::debug_text`]. - #[allow(clippy::needless_pass_by_value)] + #[expect(clippy::needless_pass_by_value)] pub fn debug_text( &self, pos: Pos2, @@ -497,7 +497,7 @@ impl Painter { /// [`Self::layout`] or [`Self::layout_no_wrap`]. /// /// Returns where the text ended up. - #[allow(clippy::needless_pass_by_value)] + #[expect(clippy::needless_pass_by_value)] pub fn text( &self, pos: Pos2, diff --git a/crates/egui/src/pass_state.rs b/crates/egui/src/pass_state.rs index 49d6c7587..1f629253c 100644 --- a/crates/egui/src/pass_state.rs +++ b/crates/egui/src/pass_state.rs @@ -3,7 +3,7 @@ use ahash::HashMap; use crate::{id::IdSet, style, Align, Id, IdMap, LayerId, Rangef, Rect, Vec2, WidgetRects}; #[cfg(debug_assertions)] -use crate::{pos2, Align2, Color32, FontId, NumExt, Painter}; +use crate::{pos2, Align2, Color32, FontId, NumExt as _, Painter}; /// Reset at the start of each frame. #[derive(Clone, Debug, Default)] diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 38d403749..7b73edbb9 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -747,7 +747,8 @@ impl ScrollStyle { // ---------------------------------------------------------------------------- -/// Scroll animation configuration, used when programmatically scrolling somewhere (e.g. with `[crate::Ui::scroll_to_cursor]`) +/// Scroll animation configuration, used when programmatically scrolling somewhere (e.g. with `[crate::Ui::scroll_to_cursor]`). +/// /// The animation duration is calculated based on the distance to be scrolled via `[ScrollAnimation::points_per_second]` /// and can be clamped to a min / max duration via `[ScrollAnimation::duration]`. #[derive(Copy, Clone, Debug, PartialEq)] @@ -1260,7 +1261,7 @@ pub fn default_text_styles() -> BTreeMap { impl Default for Style { fn default() -> Self { - #[allow(deprecated)] + #[expect(deprecated)] Self { override_font_id: None, override_text_style: None, @@ -1562,7 +1563,7 @@ use crate::{ impl Style { pub fn ui(&mut self, ui: &mut crate::Ui) { - #[allow(deprecated)] + #[expect(deprecated)] let Self { override_font_id, override_text_style, diff --git a/crates/egui/src/text_selection/label_text_selection.rs b/crates/egui/src/text_selection/label_text_selection.rs index e24992a05..9ce8fbd5b 100644 --- a/crates/egui/src/text_selection/label_text_selection.rs +++ b/crates/egui/src/text_selection/label_text_selection.rs @@ -616,7 +616,7 @@ impl LabelSelectionState { let old_primary = old_selection.map(|s| s.primary); let new_primary = self.selection.as_ref().map(|s| s.primary); if let Some(new_primary) = new_primary { - let primary_changed = old_primary.map_or(true, |old| { + let primary_changed = old_primary.is_none_or(|old| { old.widget_id != new_primary.widget_id || old.ccursor != new_primary.ccursor }); if primary_changed && new_primary.widget_id == widget_id { diff --git a/crates/egui/src/text_selection/text_cursor_state.rs b/crates/egui/src/text_selection/text_cursor_state.rs index aaa3beb9b..298d8abfb 100644 --- a/crates/egui/src/text_selection/text_cursor_state.rs +++ b/crates/egui/src/text_selection/text_cursor_state.rs @@ -1,9 +1,9 @@ //! Text cursor changes/interaction, without modifying the text. use epaint::text::{cursor::CCursor, Galley}; -use unicode_segmentation::UnicodeSegmentation; +use unicode_segmentation::UnicodeSegmentation as _; -use crate::{epaint, NumExt, Rect, Response, Ui}; +use crate::{epaint, NumExt as _, Rect, Response, Ui}; use super::CCursorRange; diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 0e8509bb8..4174ca961 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -98,7 +98,7 @@ pub struct Ui { sizing_pass: bool, /// Indicates whether this Ui belongs to a Menu. - #[allow(deprecated)] + #[expect(deprecated)] menu_state: Option>>, /// The [`UiStack`] for this [`Ui`]. @@ -666,7 +666,7 @@ impl Ui { /// /// This is determined first by [`Style::wrap_mode`], and then by the layout of this [`Ui`]. pub fn wrap_mode(&self) -> TextWrapMode { - #[allow(deprecated)] + #[expect(deprecated)] if let Some(wrap_mode) = self.style.wrap_mode { wrap_mode } @@ -3015,7 +3015,7 @@ impl Ui { self.close_kind(UiKind::Menu); } - #[allow(deprecated)] + #[expect(deprecated)] pub(crate) fn set_menu_state( &mut self, menu_state: Option>>, @@ -3156,7 +3156,7 @@ impl Drop for Ui { /// Show this rectangle to the user if certain debug options are set. #[cfg(debug_assertions)] fn register_rect(ui: &Ui, rect: Rect) { - use emath::{Align2, GuiRounding}; + use emath::{Align2, GuiRounding as _}; let debug = ui.style().debug; diff --git a/crates/egui/src/ui_builder.rs b/crates/egui/src/ui_builder.rs index 4d225ae4c..4aade7d64 100644 --- a/crates/egui/src/ui_builder.rs +++ b/crates/egui/src/ui_builder.rs @@ -1,7 +1,7 @@ use std::{hash::Hash, sync::Arc}; use crate::close_tag::ClosableTag; -#[allow(unused_imports)] // Used for doclinks +#[expect(unused_imports)] // Used for doclinks use crate::Ui; use crate::{Id, LayerId, Layout, Rect, Sense, Style, UiStackInfo}; diff --git a/crates/egui/src/ui_stack.rs b/crates/egui/src/ui_stack.rs index 64a776ee6..0122f5681 100644 --- a/crates/egui/src/ui_stack.rs +++ b/crates/egui/src/ui_stack.rs @@ -258,7 +258,7 @@ impl UiStack { // these methods act on the entire stack impl UiStack { /// Return an iterator that walks the stack from this node to the root. - #[allow(clippy::iter_without_into_iter)] + #[expect(clippy::iter_without_into_iter)] pub fn iter(&self) -> UiStackIterator<'_> { UiStackIterator { next: Some(self) } } diff --git a/crates/egui/src/util/id_type_map.rs b/crates/egui/src/util/id_type_map.rs index a7b8f18c9..bd4d14efd 100644 --- a/crates/egui/src/util/id_type_map.rs +++ b/crates/egui/src/util/id_type_map.rs @@ -467,7 +467,7 @@ impl IdTypeMap { /// For tests #[cfg(feature = "persistence")] - #[allow(unused)] + #[allow(unused, clippy::allow_attributes)] fn get_generation(&self, id: Id) -> Option { let element = self.map.get(&hash(TypeId::of::(), id))?; match element { diff --git a/crates/egui/src/viewport.rs b/crates/egui/src/viewport.rs index 9aba69910..2a469435e 100644 --- a/crates/egui/src/viewport.rs +++ b/crates/egui/src/viewport.rs @@ -260,7 +260,6 @@ pub type ImmediateViewportRendererCallback = dyn for<'a> Fn(&Context, ImmediateV /// The default values are implementation defined, so you may want to explicitly /// configure the size of the window, and what buttons are shown. #[derive(Clone, Debug, Default, Eq, PartialEq)] -#[allow(clippy::option_option)] pub struct ViewportBuilder { /// The title of the viewport. /// `eframe` will use this as the title of the native window. diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index c0701c193..2a85a164d 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -1,6 +1,6 @@ use crate::{ - widgets, Align, Color32, CornerRadius, FontSelection, Image, NumExt, Rect, Response, Sense, - Stroke, TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetType, + widgets, Align, Color32, CornerRadius, FontSelection, Image, NumExt as _, Rect, Response, + Sense, Stroke, TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetType, }; /// Clickable button with text. @@ -46,13 +46,11 @@ impl<'a> Button<'a> { } /// Creates a button with an image. The size of the image as displayed is defined by the provided size. - #[allow(clippy::needless_pass_by_value)] pub fn image(image: impl Into>) -> Self { Self::opt_image_and_text(Some(image.into()), None) } /// Creates a button with an image to the left of the text. The size of the image as displayed is defined by the provided size. - #[allow(clippy::needless_pass_by_value)] pub fn image_and_text(image: impl Into>, text: impl Into) -> Self { Self::opt_image_and_text(Some(image.into()), Some(text.into())) } diff --git a/crates/egui/src/widgets/checkbox.rs b/crates/egui/src/widgets/checkbox.rs index 7bdb6c86f..97bd97b88 100644 --- a/crates/egui/src/widgets/checkbox.rs +++ b/crates/egui/src/widgets/checkbox.rs @@ -1,6 +1,6 @@ use crate::{ - epaint, pos2, vec2, NumExt, Response, Sense, Shape, TextStyle, Ui, Vec2, Widget, WidgetInfo, - WidgetText, WidgetType, + epaint, pos2, vec2, NumExt as _, Response, Sense, Shape, TextStyle, Ui, Vec2, Widget, + WidgetInfo, WidgetText, WidgetType, }; // TODO(emilk): allow checkbox without a text label diff --git a/crates/egui/src/widgets/color_picker.rs b/crates/egui/src/widgets/color_picker.rs index f8e186658..07678d458 100644 --- a/crates/egui/src/widgets/color_picker.rs +++ b/crates/egui/src/widgets/color_picker.rs @@ -3,7 +3,7 @@ use crate::util::fixed_cache::FixedCache; use crate::{ epaint, lerp, remap_clamp, Area, Context, DragValue, Frame, Id, Key, Order, Painter, Response, - Sense, Ui, UiKind, Widget, WidgetInfo, WidgetType, + Sense, Ui, UiKind, Widget as _, WidgetInfo, WidgetType, }; use epaint::{ ecolor::{Color32, Hsva, HsvaGamma, Rgba}, diff --git a/crates/egui/src/widgets/drag_value.rs b/crates/egui/src/widgets/drag_value.rs index f0f35c1e9..864222ae1 100644 --- a/crates/egui/src/widgets/drag_value.rs +++ b/crates/egui/src/widgets/drag_value.rs @@ -3,8 +3,8 @@ use std::{cmp::Ordering, ops::RangeInclusive}; use crate::{ - emath, text, Button, CursorIcon, Key, Modifiers, NumExt, Response, RichText, Sense, TextEdit, - TextWrapMode, Ui, Widget, WidgetInfo, MINUS_CHAR_STR, + emath, text, Button, CursorIcon, Key, Modifiers, NumExt as _, Response, RichText, Sense, + TextEdit, TextWrapMode, Ui, Widget, WidgetInfo, MINUS_CHAR_STR, }; // ---------------------------------------------------------------------------- @@ -550,7 +550,7 @@ impl Widget for DragValue<'_> { } // some clones below are redundant if AccessKit is disabled - #[allow(clippy::redundant_clone)] + #[expect(clippy::redundant_clone)] let mut response = if is_kb_editing { let mut value_text = ui .data_mut(|data| data.remove_temp::(id)) diff --git a/crates/egui/src/widgets/hyperlink.rs b/crates/egui/src/widgets/hyperlink.rs index 7d5129b14..4896be410 100644 --- a/crates/egui/src/widgets/hyperlink.rs +++ b/crates/egui/src/widgets/hyperlink.rs @@ -96,7 +96,7 @@ pub struct Hyperlink { } impl Hyperlink { - #[allow(clippy::needless_pass_by_value)] + #[expect(clippy::needless_pass_by_value)] pub fn new(url: impl ToString) -> Self { let url = url.to_string(); Self { @@ -106,7 +106,7 @@ impl Hyperlink { } } - #[allow(clippy::needless_pass_by_value)] + #[expect(clippy::needless_pass_by_value)] pub fn from_label_and_url(text: impl Into, url: impl ToString) -> Self { Self { url: url.to_string(), diff --git a/crates/egui/src/widgets/progress_bar.rs b/crates/egui/src/widgets/progress_bar.rs index 44c9b8971..6739c0e2e 100644 --- a/crates/egui/src/widgets/progress_bar.rs +++ b/crates/egui/src/widgets/progress_bar.rs @@ -1,6 +1,6 @@ use crate::{ - lerp, vec2, Color32, CornerRadius, NumExt, Pos2, Rect, Response, Rgba, Sense, Shape, Stroke, - TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetType, + lerp, vec2, Color32, CornerRadius, NumExt as _, Pos2, Rect, Response, Rgba, Sense, Shape, + Stroke, TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetType, }; enum ProgressBarText { diff --git a/crates/egui/src/widgets/radio_button.rs b/crates/egui/src/widgets/radio_button.rs index fabf565b5..7c178840d 100644 --- a/crates/egui/src/widgets/radio_button.rs +++ b/crates/egui/src/widgets/radio_button.rs @@ -1,5 +1,5 @@ use crate::{ - epaint, pos2, vec2, NumExt, Response, Sense, TextStyle, Ui, Vec2, Widget, WidgetInfo, + epaint, pos2, vec2, NumExt as _, Response, Sense, TextStyle, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetType, }; diff --git a/crates/egui/src/widgets/selected_label.rs b/crates/egui/src/widgets/selected_label.rs index dfed4d2ba..4b2ee9ae2 100644 --- a/crates/egui/src/widgets/selected_label.rs +++ b/crates/egui/src/widgets/selected_label.rs @@ -1,4 +1,6 @@ -use crate::{NumExt, Response, Sense, TextStyle, Ui, Widget, WidgetInfo, WidgetText, WidgetType}; +use crate::{ + NumExt as _, Response, Sense, TextStyle, Ui, Widget, WidgetInfo, WidgetText, WidgetType, +}; /// One out of several alternatives, either selected or not. /// Will mark selected items with a different background color. diff --git a/crates/egui/src/widgets/slider.rs b/crates/egui/src/widgets/slider.rs index 0f7eb0495..017270fc7 100644 --- a/crates/egui/src/widgets/slider.rs +++ b/crates/egui/src/widgets/slider.rs @@ -4,8 +4,8 @@ use std::ops::RangeInclusive; use crate::{ emath, epaint, lerp, pos2, remap, remap_clamp, style, style::HandleShape, vec2, Color32, - DragValue, EventFilter, Key, Label, NumExt, Pos2, Rangef, Rect, Response, Sense, TextStyle, - TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, MINUS_CHAR_STR, + DragValue, EventFilter, Key, Label, NumExt as _, Pos2, Rangef, Rect, Response, Sense, + TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, MINUS_CHAR_STR, }; use super::drag_value::clamp_value_to_range; diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 6a4244496..ca201edda 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -13,8 +13,8 @@ use crate::{ response, text_selection, text_selection::{text_cursor_state::cursor_rect, visuals::paint_text_selection, CCursorRange}, vec2, Align, Align2, Color32, Context, CursorIcon, Event, EventFilter, FontSelection, Id, - ImeEvent, Key, KeyboardShortcut, Margin, Modifiers, NumExt, Response, Sense, Shape, TextBuffer, - TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetWithState, + ImeEvent, Key, KeyboardShortcut, Margin, Modifiers, NumExt as _, Response, Sense, Shape, + TextBuffer, TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetWithState, }; use super::{TextEditOutput, TextEditState}; @@ -878,7 +878,7 @@ fn mask_if_password(is_password: bool, text: &str) -> String { // ---------------------------------------------------------------------------- /// Check for (keyboard) events to edit the cursor and/or text. -#[allow(clippy::too_many_arguments)] +#[expect(clippy::too_many_arguments)] fn events( ui: &crate::Ui, state: &mut TextEditState, diff --git a/crates/egui/src/widgets/text_edit/state.rs b/crates/egui/src/widgets/text_edit/state.rs index 0734811bd..0051ea8e7 100644 --- a/crates/egui/src/widgets/text_edit/state.rs +++ b/crates/egui/src/widgets/text_edit/state.rs @@ -72,7 +72,7 @@ impl TextEditState { self.undoer.lock().clone() } - #[allow(clippy::needless_pass_by_ref_mut)] // Intentionally hide interiority of mutability + #[expect(clippy::needless_pass_by_ref_mut)] // Intentionally hide interiority of mutability pub fn set_undoer(&mut self, undoer: TextEditUndoer) { *self.undoer.lock() = undoer; } diff --git a/crates/egui_demo_app/src/apps/custom3d_glow.rs b/crates/egui_demo_app/src/apps/custom3d_glow.rs index ad6bfc3bc..b3ff74b12 100644 --- a/crates/egui_demo_app/src/apps/custom3d_glow.rs +++ b/crates/egui_demo_app/src/apps/custom3d_glow.rs @@ -80,7 +80,7 @@ struct RotatingTriangle { vertex_array: glow::VertexArray, } -#[allow(unsafe_code)] // we need unsafe code to use glow +#[expect(unsafe_code)] // we need unsafe code to use glow impl RotatingTriangle { fn new(gl: &glow::Context) -> Option { use glow::HasContext as _; diff --git a/crates/egui_demo_app/src/apps/custom3d_wgpu.rs b/crates/egui_demo_app/src/apps/custom3d_wgpu.rs index 2e3a34f7d..d3b10d480 100644 --- a/crates/egui_demo_app/src/apps/custom3d_wgpu.rs +++ b/crates/egui_demo_app/src/apps/custom3d_wgpu.rs @@ -1,7 +1,7 @@ use std::num::NonZeroU64; use eframe::{ - egui_wgpu::wgpu::util::DeviceExt, + egui_wgpu::wgpu::util::DeviceExt as _, egui_wgpu::{self, wgpu}, }; diff --git a/crates/egui_demo_app/src/backend_panel.rs b/crates/egui_demo_app/src/backend_panel.rs index e0a29f14c..289d9b6e2 100644 --- a/crates/egui_demo_app/src/backend_panel.rs +++ b/crates/egui_demo_app/src/backend_panel.rs @@ -110,7 +110,7 @@ impl BackendPanel { if cfg!(debug_assertions) && cfg!(target_arch = "wasm32") { ui.separator(); // For testing panic handling on web: - #[allow(clippy::manual_assert)] + #[expect(clippy::manual_assert)] if ui.button("panic!()").clicked() { panic!("intentional panic!"); } diff --git a/crates/egui_demo_app/src/lib.rs b/crates/egui_demo_app/src/lib.rs index 0eb34486d..9cfa26baa 100644 --- a/crates/egui_demo_app/src/lib.rs +++ b/crates/egui_demo_app/src/lib.rs @@ -10,7 +10,7 @@ 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; + use chrono::Timelike as _; let time = chrono::Local::now().time(); time.num_seconds_from_midnight() as f64 + 1e-9 * (time.nanosecond() as f64) } diff --git a/crates/egui_demo_app/src/main.rs b/crates/egui_demo_app/src/main.rs index 6a2bb2da7..476ffdacd 100644 --- a/crates/egui_demo_app/src/main.rs +++ b/crates/egui_demo_app/src/main.rs @@ -75,7 +75,7 @@ fn start_puffin_server() { // We can store the server if we want, but in this case we just want // it to keep running. Dropping it closes the server, so let's not drop it! - #[allow(clippy::mem_forget)] + #[expect(clippy::mem_forget)] std::mem::forget(puffin_server); } Err(err) => { diff --git a/crates/egui_demo_app/src/web.rs b/crates/egui_demo_app/src/web.rs index 8c9cdd1d4..104ef1dce 100644 --- a/crates/egui_demo_app/src/web.rs +++ b/crates/egui_demo_app/src/web.rs @@ -14,7 +14,7 @@ pub struct WebHandle { #[wasm_bindgen] impl WebHandle { /// Installs a panic hook, then returns. - #[allow(clippy::new_without_default)] + #[allow(clippy::new_without_default, clippy::allow_attributes)] #[wasm_bindgen(constructor)] pub fn new() -> Self { // Redirect [`log`] message to `console.log` and friends: diff --git a/crates/egui_demo_app/src/wrap_app.rs b/crates/egui_demo_app/src/wrap_app.rs index a571cfbb2..ea5fbfaba 100644 --- a/crates/egui_demo_app/src/wrap_app.rs +++ b/crates/egui_demo_app/src/wrap_app.rs @@ -188,7 +188,7 @@ impl WrapApp { // This gives us image support: egui_extras::install_image_loaders(&cc.egui_ctx); - #[allow(unused_mut)] + #[allow(unused_mut, clippy::allow_attributes)] let mut slf = Self { state: State::default(), diff --git a/crates/egui_demo_app/tests/test_demo_app.rs b/crates/egui_demo_app/tests/test_demo_app.rs index 056bee49e..8b0b272fa 100644 --- a/crates/egui_demo_app/tests/test_demo_app.rs +++ b/crates/egui_demo_app/tests/test_demo_app.rs @@ -1,7 +1,7 @@ use egui::accesskit::Role; use egui::Vec2; use egui_demo_app::{Anchor, WrapApp}; -use egui_kittest::kittest::Queryable; +use egui_kittest::kittest::Queryable as _; use egui_kittest::SnapshotResults; #[test] diff --git a/crates/egui_demo_lib/src/demo/code_example.rs b/crates/egui_demo_lib/src/demo/code_example.rs index 9094c19a7..89a8cdafa 100644 --- a/crates/egui_demo_lib/src/demo/code_example.rs +++ b/crates/egui_demo_lib/src/demo/code_example.rs @@ -105,7 +105,7 @@ impl crate::Demo for CodeExample { } fn show(&mut self, ctx: &egui::Context, open: &mut bool) { - use crate::View; + use crate::View as _; egui::Window::new(self.name()) .open(open) .min_width(375.0) 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 ce18ac927..7b8972786 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -3,7 +3,7 @@ use std::collections::BTreeSet; use super::About; use crate::is_mobile; use crate::Demo; -use crate::View; +use crate::View as _; use egui::containers::menu; use egui::style::StyleModifier; use egui::{Context, Modifiers, ScrollArea, Ui}; @@ -359,9 +359,9 @@ fn file_menu_button(ui: &mut Ui) { #[cfg(test)] mod tests { - use crate::{demo::demo_app_windows::DemoGroups, Demo}; + use crate::{demo::demo_app_windows::DemoGroups, Demo as _}; use egui::Vec2; - use egui_kittest::kittest::Queryable; + use egui_kittest::kittest::Queryable as _; use egui_kittest::{Harness, SnapshotOptions, SnapshotResults}; #[test] diff --git a/crates/egui_demo_lib/src/demo/font_book.rs b/crates/egui_demo_lib/src/demo/font_book.rs index 605b7ed7f..1ef9867f9 100644 --- a/crates/egui_demo_lib/src/demo/font_book.rs +++ b/crates/egui_demo_lib/src/demo/font_book.rs @@ -169,7 +169,7 @@ fn char_name(chr: char) -> String { } fn special_char_name(chr: char) -> Option<&'static str> { - #[allow(clippy::match_same_arms)] // many "flag" + #[expect(clippy::match_same_arms)] // many "flag" match chr { // Special private-use-area extensions found in `emoji-icon-font.ttf`: // Private use area extensions: diff --git a/crates/egui_demo_lib/src/demo/interactive_container.rs b/crates/egui_demo_lib/src/demo/interactive_container.rs index 11d8afa48..5faeb8458 100644 --- a/crates/egui_demo_lib/src/demo/interactive_container.rs +++ b/crates/egui_demo_lib/src/demo/interactive_container.rs @@ -1,4 +1,4 @@ -use egui::{Frame, Label, RichText, Sense, UiBuilder, Widget}; +use egui::{Frame, Label, RichText, Sense, UiBuilder, Widget as _}; /// Showcase [`egui::Ui::response`]. #[derive(PartialEq, Eq, Default)] diff --git a/crates/egui_demo_lib/src/demo/modals.rs b/crates/egui_demo_lib/src/demo/modals.rs index f83820e74..fcb33f0bb 100644 --- a/crates/egui_demo_lib/src/demo/modals.rs +++ b/crates/egui_demo_lib/src/demo/modals.rs @@ -1,4 +1,4 @@ -use egui::{ComboBox, Context, Id, Modal, ProgressBar, Ui, Widget, Window}; +use egui::{ComboBox, Context, Id, Modal, ProgressBar, Ui, Widget as _, Window}; #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] @@ -163,10 +163,10 @@ impl crate::View for Modals { #[cfg(test)] mod tests { use crate::demo::modals::Modals; - use crate::Demo; + use crate::Demo as _; use egui::accesskit::Role; use egui::Key; - use egui_kittest::kittest::Queryable; + use egui_kittest::kittest::Queryable as _; use egui_kittest::{Harness, SnapshotResults}; #[test] diff --git a/crates/egui_demo_lib/src/demo/paint_bezier.rs b/crates/egui_demo_lib/src/demo/paint_bezier.rs index 08e29a9ff..df85e4377 100644 --- a/crates/egui_demo_lib/src/demo/paint_bezier.rs +++ b/crates/egui_demo_lib/src/demo/paint_bezier.rs @@ -2,7 +2,7 @@ use egui::{ emath, epaint::{self, CubicBezierShape, PathShape, QuadraticBezierShape}, pos2, Color32, Context, Frame, Grid, Pos2, Rect, Sense, Shape, Stroke, StrokeKind, Ui, Vec2, - Widget, Window, + Widget as _, Window, }; #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] diff --git a/crates/egui_demo_lib/src/demo/password.rs b/crates/egui_demo_lib/src/demo/password.rs index 528f5a515..88a2bb706 100644 --- a/crates/egui_demo_lib/src/demo/password.rs +++ b/crates/egui_demo_lib/src/demo/password.rs @@ -8,7 +8,6 @@ /// ``` ignore /// password_ui(ui, &mut my_password); /// ``` -#[allow(clippy::ptr_arg)] // false positive pub fn password_ui(ui: &mut egui::Ui, password: &mut String) -> egui::Response { // This widget has its own state — show or hide password characters (`show_plaintext`). // In this case we use a simple `bool`, but you can also declare your own type. diff --git a/crates/egui_demo_lib/src/demo/screenshot.rs b/crates/egui_demo_lib/src/demo/screenshot.rs index eb62611c8..c2367914b 100644 --- a/crates/egui_demo_lib/src/demo/screenshot.rs +++ b/crates/egui_demo_lib/src/demo/screenshot.rs @@ -1,4 +1,4 @@ -use egui::{Image, UserData, ViewportCommand, Widget}; +use egui::{Image, UserData, ViewportCommand, Widget as _}; use std::sync::Arc; /// Showcase [`ViewportCommand::Screenshot`]. diff --git a/crates/egui_demo_lib/src/demo/scrolling.rs b/crates/egui_demo_lib/src/demo/scrolling.rs index 7a2ca4d7c..dab3c4f5a 100644 --- a/crates/egui_demo_lib/src/demo/scrolling.rs +++ b/crates/egui_demo_lib/src/demo/scrolling.rs @@ -1,6 +1,6 @@ use egui::{ - pos2, scroll_area::ScrollBarVisibility, Align, Align2, Color32, DragValue, NumExt, Rect, - ScrollArea, Sense, Slider, TextStyle, TextWrapMode, Ui, Vec2, Widget, + pos2, scroll_area::ScrollBarVisibility, Align, Align2, Color32, DragValue, NumExt as _, Rect, + ScrollArea, Sense, Slider, TextStyle, TextWrapMode, Ui, Vec2, Widget as _, }; #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] 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 031f3e2ce..20e8d21bf 100644 --- a/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs +++ b/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs @@ -1,5 +1,5 @@ use egui::{ - emath::{GuiRounding, TSTransform}, + emath::{GuiRounding as _, TSTransform}, epaint::{self, RectShape}, vec2, Color32, Pos2, Rect, Sense, StrokeKind, Vec2, }; diff --git a/crates/egui_demo_lib/src/demo/text_edit.rs b/crates/egui_demo_lib/src/demo/text_edit.rs index 06911957f..685a9c38f 100644 --- a/crates/egui_demo_lib/src/demo/text_edit.rs +++ b/crates/egui_demo_lib/src/demo/text_edit.rs @@ -114,7 +114,7 @@ impl crate::View for TextEditDemo { #[cfg(test)] mod tests { use egui::{accesskit, CentralPanel}; - use egui_kittest::kittest::{Key, Queryable}; + use egui_kittest::kittest::{Key, Queryable as _}; use egui_kittest::Harness; #[test] diff --git a/crates/egui_demo_lib/src/demo/toggle_switch.rs b/crates/egui_demo_lib/src/demo/toggle_switch.rs index 9619bd2c0..623228cbd 100644 --- a/crates/egui_demo_lib/src/demo/toggle_switch.rs +++ b/crates/egui_demo_lib/src/demo/toggle_switch.rs @@ -76,7 +76,7 @@ pub fn toggle_ui(ui: &mut egui::Ui, on: &mut bool) -> egui::Response { } /// Here is the same code again, but a bit more compact: -#[allow(dead_code)] +#[expect(dead_code)] fn toggle_ui_compact(ui: &mut egui::Ui, on: &mut bool) -> egui::Response { let desired_size = ui.spacing().interact_size.y * egui::vec2(2.0, 1.0); let (rect, mut response) = ui.allocate_exact_size(desired_size, egui::Sense::click()); diff --git a/crates/egui_demo_lib/src/demo/widget_gallery.rs b/crates/egui_demo_lib/src/demo/widget_gallery.rs index 6005d6aec..c7b6df28a 100644 --- a/crates/egui_demo_lib/src/demo/widget_gallery.rs +++ b/crates/egui_demo_lib/src/demo/widget_gallery.rs @@ -48,7 +48,7 @@ impl Default for WidgetGallery { } impl WidgetGallery { - #[allow(unused_mut)] // if not chrono + #[allow(unused_mut, clippy::allow_attributes)] // if not chrono #[inline] pub fn with_date_button(mut self, _with_date_button: bool) -> Self { #[cfg(feature = "chrono")] @@ -308,7 +308,7 @@ fn doc_link_label_with_crate<'a>( #[cfg(test)] mod tests { use super::*; - use crate::View; + use crate::View as _; use egui::Vec2; use egui_kittest::Harness; diff --git a/crates/egui_extras/src/datepicker/mod.rs b/crates/egui_extras/src/datepicker/mod.rs index 33038d763..e7ba47e74 100644 --- a/crates/egui_extras/src/datepicker/mod.rs +++ b/crates/egui_extras/src/datepicker/mod.rs @@ -2,7 +2,7 @@ mod button; mod popup; pub use button::DatePickerButton; -use chrono::{Datelike, Duration, NaiveDate, Weekday}; +use chrono::{Datelike as _, Duration, NaiveDate, Weekday}; #[derive(Debug)] struct Week { diff --git a/crates/egui_extras/src/datepicker/popup.rs b/crates/egui_extras/src/datepicker/popup.rs index 8bf2f1a12..79f3d37f6 100644 --- a/crates/egui_extras/src/datepicker/popup.rs +++ b/crates/egui_extras/src/datepicker/popup.rs @@ -1,4 +1,4 @@ -use chrono::{Datelike, NaiveDate, Weekday}; +use chrono::{Datelike as _, NaiveDate, Weekday}; use egui::{Align, Button, Color32, ComboBox, Direction, Id, Layout, RichText, Ui, Vec2}; diff --git a/crates/egui_extras/src/layout.rs b/crates/egui_extras/src/layout.rs index fb62cec40..b1c79d0a6 100644 --- a/crates/egui_extras/src/layout.rs +++ b/crates/egui_extras/src/layout.rs @@ -1,4 +1,4 @@ -use egui::{emath::GuiRounding, Id, Pos2, Rect, Response, Sense, Ui, UiBuilder}; +use egui::{emath::GuiRounding as _, Id, Pos2, Rect, Response, Sense, Ui, UiBuilder}; #[derive(Clone, Copy)] pub(crate) enum CellSize { diff --git a/crates/egui_extras/src/lib.rs b/crates/egui_extras/src/lib.rs index 6f3390102..1a58402f5 100644 --- a/crates/egui_extras/src/lib.rs +++ b/crates/egui_extras/src/lib.rs @@ -26,7 +26,7 @@ mod table; pub use crate::datepicker::DatePickerButton; #[doc(hidden)] -#[allow(deprecated)] +#[expect(deprecated)] pub use crate::image::RetainedImage; pub(crate) use crate::layout::StripLayout; pub use crate::sizing::Size; diff --git a/crates/egui_extras/src/loaders/image_loader.rs b/crates/egui_extras/src/loaders/image_loader.rs index 87ca4db06..bb025651d 100644 --- a/crates/egui_extras/src/loaders/image_loader.rs +++ b/crates/egui_extras/src/loaders/image_loader.rs @@ -77,7 +77,7 @@ impl ImageLoader for ImageCrateLoader { } #[cfg(not(target_arch = "wasm32"))] - #[allow(clippy::unnecessary_wraps)] // needed here to match other return types + #[expect(clippy::unnecessary_wraps)] // needed here to match other return types fn load_image( ctx: &egui::Context, uri: &str, diff --git a/crates/egui_extras/src/loaders/svg_loader.rs b/crates/egui_extras/src/loaders/svg_loader.rs index 4c33281fe..63cfd4a96 100644 --- a/crates/egui_extras/src/loaders/svg_loader.rs +++ b/crates/egui_extras/src/loaders/svg_loader.rs @@ -30,7 +30,7 @@ fn is_supported(uri: &str) -> bool { impl Default for SvgLoader { fn default() -> Self { // opt is mutated when `svg_text` feature flag is enabled - #[allow(unused_mut)] + #[allow(unused_mut, clippy::allow_attributes)] let mut options = resvg::usvg::Options::default(); #[cfg(feature = "svg_text")] diff --git a/crates/egui_extras/src/loaders/webp_loader.rs b/crates/egui_extras/src/loaders/webp_loader.rs index 528c449a7..ef1a5d527 100644 --- a/crates/egui_extras/src/loaders/webp_loader.rs +++ b/crates/egui_extras/src/loaders/webp_loader.rs @@ -5,7 +5,7 @@ use egui::{ mutex::Mutex, ColorImage, FrameDurations, Id, }; -use image::{codecs::webp::WebPDecoder, AnimationDecoder as _, ColorType, ImageDecoder, Rgba}; +use image::{codecs::webp::WebPDecoder, AnimationDecoder as _, ColorType, ImageDecoder as _, Rgba}; use std::{io::Cursor, mem::size_of, sync::Arc, time::Duration}; #[derive(Clone)] diff --git a/crates/egui_extras/src/syntax_highlighting.rs b/crates/egui_extras/src/syntax_highlighting.rs index ac51c673c..6bb51184f 100644 --- a/crates/egui_extras/src/syntax_highlighting.rs +++ b/crates/egui_extras/src/syntax_highlighting.rs @@ -33,7 +33,7 @@ pub fn highlight( // performing it at a separate thread (ctx, ctx.style()) can be used and when ui is available // (ui.ctx(), ui.style()) can be used - #[allow(non_local_definitions)] + #[expect(non_local_definitions)] impl egui::cache::ComputerMut<(&egui::FontId, &CodeTheme, &str, &str), LayoutJob> for Highlighter { fn compute( &mut self, @@ -285,7 +285,7 @@ impl CodeTheme { impl CodeTheme { // The syntect version takes it by value. This could be avoided by specializing the from_style // function, but at the cost of more code duplication. - #[allow(clippy::needless_pass_by_value)] + #[expect(clippy::needless_pass_by_value)] fn dark_with_font_id(font_id: egui::FontId) -> Self { use egui::{Color32, TextFormat}; Self { @@ -302,7 +302,7 @@ impl CodeTheme { } // The syntect version takes it by value - #[allow(clippy::needless_pass_by_value)] + #[expect(clippy::needless_pass_by_value)] fn light_with_font_id(font_id: egui::FontId) -> Self { use egui::{Color32, TextFormat}; Self { @@ -413,7 +413,6 @@ impl Default for Highlighter { } impl Highlighter { - #[allow(clippy::unused_self, clippy::unnecessary_wraps)] fn highlight( &self, font_id: egui::FontId, @@ -512,7 +511,7 @@ struct Highlighter {} #[cfg(not(feature = "syntect"))] impl Highlighter { - #[allow(clippy::unused_self, clippy::unnecessary_wraps)] + #[expect(clippy::unused_self, clippy::unnecessary_wraps)] fn highlight_impl( &self, theme: &CodeTheme, diff --git a/crates/egui_glow/examples/pure_glow.rs b/crates/egui_glow/examples/pure_glow.rs index d151be377..7f38700cd 100644 --- a/crates/egui_glow/examples/pure_glow.rs +++ b/crates/egui_glow/examples/pure_glow.rs @@ -9,7 +9,7 @@ use std::num::NonZeroU32; use std::sync::Arc; use egui_winit::winit; -use winit::raw_window_handle::HasWindowHandle; +use winit::raw_window_handle::HasWindowHandle as _; /// The majority of `GlutinWindowContext` is taken from `eframe` struct GlutinWindowContext { @@ -22,12 +22,12 @@ struct GlutinWindowContext { impl GlutinWindowContext { // refactor this function to use `glutin-winit` crate eventually. // preferably add android support at the same time. - #[allow(unsafe_code)] + #[expect(unsafe_code)] unsafe fn new(event_loop: &winit::event_loop::ActiveEventLoop) -> Self { - use glutin::context::NotCurrentGlContext; - use glutin::display::GetGlDisplay; - use glutin::display::GlDisplay; - use glutin::prelude::GlSurface; + use glutin::context::NotCurrentGlContext as _; + use glutin::display::GetGlDisplay as _; + use glutin::display::GlDisplay as _; + use glutin::prelude::GlSurface as _; let winit_window_builder = winit::window::WindowAttributes::default() .with_resizable(true) .with_inner_size(winit::dpi::LogicalSize { @@ -138,7 +138,7 @@ impl GlutinWindowContext { } fn resize(&self, physical_size: winit::dpi::PhysicalSize) { - use glutin::surface::GlSurface; + use glutin::surface::GlSurface as _; self.gl_surface.resize( &self.gl_context, physical_size.width.try_into().unwrap(), @@ -147,12 +147,12 @@ impl GlutinWindowContext { } fn swap_buffers(&self) -> glutin::error::Result<()> { - use glutin::surface::GlSurface; + use glutin::surface::GlSurface as _; self.gl_surface.swap_buffers(&self.gl_context) } fn get_proc_address(&self, addr: &std::ffi::CStr) -> *const std::ffi::c_void { - use glutin::display::GlDisplay; + use glutin::display::GlDisplay as _; self.gl_display.get_proc_address(addr) } } diff --git a/crates/egui_glow/src/lib.rs b/crates/egui_glow/src/lib.rs index 430f1287e..aaea1d0b9 100644 --- a/crates/egui_glow/src/lib.rs +++ b/crates/egui_glow/src/lib.rs @@ -73,7 +73,7 @@ macro_rules! check_for_gl_error_even_in_release { #[doc(hidden)] pub fn check_for_gl_error_impl(gl: &glow::Context, file: &str, line: u32, context: &str) { use glow::HasContext as _; - #[allow(unsafe_code)] + #[expect(unsafe_code)] let error_code = unsafe { gl.get_error() }; if error_code != glow::NO_ERROR { let error_str = match error_code { diff --git a/crates/egui_glow/src/painter.rs b/crates/egui_glow/src/painter.rs index 1d1322c42..939246e64 100644 --- a/crates/egui_glow/src/painter.rs +++ b/crates/egui_glow/src/painter.rs @@ -296,7 +296,7 @@ impl Painter { /// So if in a [`egui::Shape::Callback`] you need to use an offscreen FBO, you should /// then restore to this afterwards with /// `gl.bind_framebuffer(glow::FRAMEBUFFER, painter.intermediate_fbo());` - #[allow(clippy::unused_self)] + #[expect(clippy::unused_self)] pub fn intermediate_fbo(&self) -> Option { // We don't currently ever render to an offscreen buffer, // but we may want to start to in order to do anti-aliasing on web, for instance. @@ -663,7 +663,6 @@ impl Painter { self.textures.get(&texture_id).copied() } - #[allow(clippy::needless_pass_by_value)] // False positive pub fn register_native_texture(&mut self, native: glow::Texture) -> egui::TextureId { self.assert_not_destroyed(); let id = egui::TextureId::User(self.next_native_tex_id); @@ -672,7 +671,6 @@ impl Painter { id } - #[allow(clippy::needless_pass_by_value)] // False positive pub fn replace_native_texture(&mut self, id: egui::TextureId, replacing: glow::Texture) { if let Some(old_tex) = self.textures.insert(id, replacing) { self.textures_to_destroy.push(old_tex); diff --git a/crates/egui_glow/src/shader_version.rs b/crates/egui_glow/src/shader_version.rs index 77c1e63ce..249cda369 100644 --- a/crates/egui_glow/src/shader_version.rs +++ b/crates/egui_glow/src/shader_version.rs @@ -1,11 +1,10 @@ #![allow(unsafe_code)] #![allow(clippy::undocumented_unsafe_blocks)] -use std::convert::TryInto; +use std::convert::TryInto as _; /// Helper for parsing and interpreting the OpenGL shader version. #[derive(Copy, Clone, Debug, PartialEq, Eq)] -#[allow(dead_code)] pub enum ShaderVersion { Gl120, diff --git a/crates/egui_glow/src/vao.rs b/crates/egui_glow/src/vao.rs index 6759a829d..febe67fdd 100644 --- a/crates/egui_glow/src/vao.rs +++ b/crates/egui_glow/src/vao.rs @@ -27,7 +27,6 @@ pub(crate) struct VertexArrayObject { } impl VertexArrayObject { - #[allow(clippy::needless_pass_by_value)] // false positive pub(crate) unsafe fn new( gl: &glow::Context, vbo: glow::Buffer, diff --git a/crates/egui_kittest/src/snapshot.rs b/crates/egui_kittest/src/snapshot.rs index 86ed053d2..acd260a4a 100644 --- a/crates/egui_kittest/src/snapshot.rs +++ b/crates/egui_kittest/src/snapshot.rs @@ -339,6 +339,7 @@ pub fn image_snapshot_options(current: &image::RgbaImage, name: &str, options: & } /// Image snapshot test. +/// /// The snapshot will be saved under `tests/snapshots/{name}.png`. /// The new image from the last test run will be saved under `tests/snapshots/{name}.new.png`. /// If the new image didn't match the snapshot, a diff image will be saved under `tests/snapshots/{name}.diff.png`. @@ -451,7 +452,7 @@ impl Harness<'_, State> { // Deprecated wgpu_snapshot functions // TODO(lucasmerlin): Remove in 0.32 -#[allow(clippy::missing_errors_doc)] +#[expect(clippy::missing_errors_doc)] #[cfg(feature = "wgpu")] impl Harness<'_, State> { #[deprecated( @@ -552,7 +553,7 @@ impl SnapshotResults { } /// Convert this into a `Result<(), Self>`. - #[allow(clippy::missing_errors_doc)] + #[expect(clippy::missing_errors_doc)] pub fn into_result(self) -> Result<(), Self> { if self.has_errors() { Err(self) @@ -566,7 +567,7 @@ impl SnapshotResults { } /// Panics if there are any errors, displaying each. - #[allow(clippy::unused_self)] + #[expect(clippy::unused_self)] #[track_caller] pub fn unwrap(self) { // Panic is handled in drop @@ -586,7 +587,7 @@ impl Drop for SnapshotResults { if std::thread::panicking() { return; } - #[allow(clippy::manual_assert)] + #[expect(clippy::manual_assert)] if self.has_errors() { panic!("{}", self); } diff --git a/crates/egui_kittest/tests/menu.rs b/crates/egui_kittest/tests/menu.rs index 4c6d6fd5c..a01e39dca 100644 --- a/crates/egui_kittest/tests/menu.rs +++ b/crates/egui_kittest/tests/menu.rs @@ -1,7 +1,7 @@ use egui::containers::menu::{Bar, MenuConfig, SubMenuButton}; use egui::{include_image, PopupCloseBehavior, Ui}; use egui_kittest::{Harness, SnapshotResults}; -use kittest::Queryable; +use kittest::Queryable as _; struct TestMenu { config: MenuConfig, diff --git a/crates/egui_kittest/tests/popup.rs b/crates/egui_kittest/tests/popup.rs index f55bf6388..368e8de7e 100644 --- a/crates/egui_kittest/tests/popup.rs +++ b/crates/egui_kittest/tests/popup.rs @@ -1,4 +1,4 @@ -use kittest::Queryable; +use kittest::Queryable as _; #[test] fn test_interactive_tooltip() { diff --git a/crates/egui_kittest/tests/regression_tests.rs b/crates/egui_kittest/tests/regression_tests.rs index bd3f7926e..50ed1095a 100644 --- a/crates/egui_kittest/tests/regression_tests.rs +++ b/crates/egui_kittest/tests/regression_tests.rs @@ -1,6 +1,6 @@ use egui::accesskit::Role; -use egui::{Button, ComboBox, Image, Vec2, Widget}; -use egui_kittest::{kittest::Queryable, Harness, SnapshotResults}; +use egui::{Button, ComboBox, Image, Vec2, Widget as _}; +use egui_kittest::{kittest::Queryable as _, Harness, SnapshotResults}; #[test] pub fn focus_should_skip_over_disabled_buttons() { diff --git a/crates/egui_kittest/tests/tests.rs b/crates/egui_kittest/tests/tests.rs index 52f455c7b..9bf07063b 100644 --- a/crates/egui_kittest/tests/tests.rs +++ b/crates/egui_kittest/tests/tests.rs @@ -1,6 +1,6 @@ use egui::Modifiers; use egui_kittest::Harness; -use kittest::{Key, Queryable}; +use kittest::{Key, Queryable as _}; #[test] fn test_shrink() { diff --git a/crates/emath/src/lib.rs b/crates/emath/src/lib.rs index ad657fcc3..337fa2045 100644 --- a/crates/emath/src/lib.rs +++ b/crates/emath/src/lib.rs @@ -248,7 +248,7 @@ pub fn almost_equal(a: f32, b: f32, epsilon: f32) -> bool { } } -#[allow(clippy::approx_constant)] +#[expect(clippy::approx_constant)] #[test] fn test_format() { assert_eq!(format_with_minimum_decimals(1_234_567.0, 0), "1234567"); diff --git a/crates/emath/src/numeric.rs b/crates/emath/src/numeric.rs index 9a7814b23..1fbddbc66 100644 --- a/crates/emath/src/numeric.rs +++ b/crates/emath/src/numeric.rs @@ -23,7 +23,7 @@ macro_rules! impl_numeric_float { #[inline(always)] fn to_f64(self) -> f64 { - #[allow(trivial_numeric_casts)] + #[allow(trivial_numeric_casts, clippy::allow_attributes)] { self as f64 } @@ -31,7 +31,7 @@ macro_rules! impl_numeric_float { #[inline(always)] fn from_f64(num: f64) -> Self { - #[allow(trivial_numeric_casts)] + #[allow(trivial_numeric_casts, clippy::allow_attributes)] { num as Self } diff --git a/crates/emath/src/ordered_float.rs b/crates/emath/src/ordered_float.rs index 0b8014d90..fa80a498f 100644 --- a/crates/emath/src/ordered_float.rs +++ b/crates/emath/src/ordered_float.rs @@ -101,7 +101,7 @@ impl Float for f64 { // Keep this trait in private module, to avoid exposing its methods as extensions in user code mod private { - use super::{Hash, Hasher}; + use super::{Hash as _, Hasher}; pub trait FloatImpl { fn is_nan(&self) -> bool; diff --git a/crates/emath/src/smart_aim.rs b/crates/emath/src/smart_aim.rs index 9ef010d9a..e75ae42ce 100644 --- a/crates/emath/src/smart_aim.rs +++ b/crates/emath/src/smart_aim.rs @@ -115,7 +115,7 @@ fn simplest_digit_closed_range(min: i32, max: i32) -> i32 { } } -#[allow(clippy::approx_constant)] +#[expect(clippy::approx_constant)] #[test] fn test_aim() { assert_eq!(best_in_range_f64(-0.2, 0.0), 0.0, "Prefer zero"); diff --git a/crates/epaint/src/lib.rs b/crates/epaint/src/lib.rs index f84a8caff..a19727690 100644 --- a/crates/epaint/src/lib.rs +++ b/crates/epaint/src/lib.rs @@ -72,7 +72,7 @@ pub use self::{ #[deprecated = "Renamed to CornerRadius"] pub type Rounding = CornerRadius; -#[allow(deprecated)] +#[expect(deprecated)] pub use tessellator::tessellate_shapes; pub use ecolor::{Color32, Hsva, HsvaGamma, Rgba}; diff --git a/crates/epaint/src/shapes/shape.rs b/crates/epaint/src/shapes/shape.rs index a855d653a..bc16e43d5 100644 --- a/crates/epaint/src/shapes/shape.rs +++ b/crates/epaint/src/shapes/shape.rs @@ -296,7 +296,7 @@ impl Shape { Self::Rect(RectShape::stroke(rect, corner_radius, stroke, stroke_kind)) } - #[allow(clippy::needless_pass_by_value)] + #[expect(clippy::needless_pass_by_value)] pub fn text( fonts: &Fonts, pos: Pos2, diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 2b24869ae..32f715b10 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -5,7 +5,7 @@ #![allow(clippy::identity_op)] -use emath::{pos2, remap, vec2, GuiRounding as _, NumExt, Pos2, Rect, Rot2, Vec2}; +use emath::{pos2, remap, vec2, GuiRounding as _, NumExt as _, Pos2, Rect, Rot2, Vec2}; use crate::{ color::ColorMode, emath, stroke::PathStroke, texture_atlas::PreparedDisc, CircleShape, @@ -16,7 +16,7 @@ use crate::{ // ---------------------------------------------------------------------------- -#[allow(clippy::approx_constant)] +#[expect(clippy::approx_constant)] mod precomputed_vertices { // fn main() { // let n = 64; @@ -2222,7 +2222,7 @@ impl Tessellator { /// /// ## Returns /// A list of clip rectangles with matching [`Mesh`]. - #[allow(unused_mut)] + #[allow(unused_mut, clippy::allow_attributes)] pub fn tessellate_shapes(&mut self, mut shapes: Vec) -> Vec { profiling::function_scope!(); diff --git a/crates/epaint/src/text/font.rs b/crates/epaint/src/text/font.rs index 5415bae07..78d9c1413 100644 --- a/crates/epaint/src/text/font.rs +++ b/crates/epaint/src/text/font.rs @@ -1,7 +1,7 @@ use std::collections::BTreeMap; use std::sync::Arc; -use emath::{vec2, GuiRounding, Vec2}; +use emath::{vec2, GuiRounding as _, Vec2}; use crate::{ mutex::{Mutex, RwLock}, @@ -100,7 +100,7 @@ impl FontImpl { "pixels_per_point must be greater than 0, got: {pixels_per_point:?}" ); - use ab_glyph::{Font, ScaleFont}; + use ab_glyph::{Font as _, ScaleFont as _}; let scaled = ab_glyph_font.as_scaled(scale_in_pixels); let ascent = (scaled.ascent() / pixels_per_point).round_ui(); let descent = (scaled.descent() / pixels_per_point).round_ui(); @@ -241,7 +241,7 @@ impl FontImpl { last_glyph_id: ab_glyph::GlyphId, glyph_id: ab_glyph::GlyphId, ) -> f32 { - use ab_glyph::{Font as _, ScaleFont}; + use ab_glyph::{Font as _, ScaleFont as _}; self.ab_glyph_font .as_scaled(self.scale_in_pixels as f32) .kern(last_glyph_id, glyph_id) @@ -271,7 +271,7 @@ impl FontImpl { fn allocate_glyph(&self, glyph_id: ab_glyph::GlyphId) -> GlyphInfo { assert!(glyph_id.0 != 0, "Can't allocate glyph for id 0"); - use ab_glyph::{Font as _, ScaleFont}; + use ab_glyph::{Font as _, ScaleFont as _}; let glyph = glyph_id.with_scale_and_position( self.scale_in_pixels as f32, diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index bfa854680..bca9212dc 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -54,7 +54,6 @@ impl FontId { } } -#[allow(clippy::derived_hash_with_manual_eq)] impl std::hash::Hash for FontId { #[inline(always)] fn hash(&self, state: &mut H) { diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index b2dba96fc..63dfc3893 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use emath::{pos2, vec2, Align, GuiRounding as _, NumExt, Pos2, Rect, Vec2}; +use emath::{pos2, vec2, Align, GuiRounding as _, NumExt as _, Pos2, Rect, Vec2}; use crate::{stroke::PathStroke, text::font::Font, Color32, Mesh, Stroke, Vertex}; diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 4bd15d3e3..49ec29087 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -9,7 +9,7 @@ use super::{ font::UvRect, }; use crate::{Color32, FontId, Mesh, Stroke}; -use emath::{pos2, vec2, Align, GuiRounding as _, NumExt, OrderedFloat, Pos2, Rect, Vec2}; +use emath::{pos2, vec2, Align, GuiRounding as _, NumExt as _, OrderedFloat, Pos2, Rect, Vec2}; /// Describes the task of laying out text. /// @@ -971,7 +971,7 @@ impl Galley { /// /// This is the same as [`CCursor::default`]. #[inline] - #[allow(clippy::unused_self)] + #[expect(clippy::unused_self)] pub fn begin(&self) -> CCursor { CCursor::default() } @@ -1062,7 +1062,7 @@ impl Galley { /// ## Cursor positions impl Galley { - #[allow(clippy::unused_self)] + #[expect(clippy::unused_self)] pub fn cursor_left_one_character(&self, cursor: &CCursor) -> CCursor { if cursor.index == 0 { Default::default() diff --git a/crates/epaint/src/texture_handle.rs b/crates/epaint/src/texture_handle.rs index 1f640a171..315437750 100644 --- a/crates/epaint/src/texture_handle.rs +++ b/crates/epaint/src/texture_handle.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use crate::{ - emath::NumExt, mutex::RwLock, textures::TextureOptions, ImageData, ImageDelta, TextureId, + emath::NumExt as _, mutex::RwLock, textures::TextureOptions, ImageData, ImageDelta, TextureId, TextureManager, }; @@ -66,7 +66,7 @@ impl TextureHandle { } /// Assign a new image to an existing texture. - #[allow(clippy::needless_pass_by_ref_mut)] // Intentionally hide interiority of mutability + #[expect(clippy::needless_pass_by_ref_mut)] // Intentionally hide interiority of mutability pub fn set(&mut self, image: impl Into, options: TextureOptions) { self.tex_mngr .write() @@ -74,7 +74,7 @@ impl TextureHandle { } /// Assign a new image to a subregion of the whole texture. - #[allow(clippy::needless_pass_by_ref_mut)] // Intentionally hide interiority of mutability + #[expect(clippy::needless_pass_by_ref_mut)] // Intentionally hide interiority of mutability pub fn set_partial( &mut self, pos: [usize; 2], diff --git a/examples/custom_style/src/main.rs b/examples/custom_style/src/main.rs index 111139379..1e78bea0f 100644 --- a/examples/custom_style/src/main.rs +++ b/examples/custom_style/src/main.rs @@ -4,7 +4,7 @@ use eframe::egui::{ self, global_theme_preference_buttons, style::Selection, Color32, Stroke, Style, Theme, }; -use egui_demo_lib::{View, WidgetGallery}; +use egui_demo_lib::{View as _, WidgetGallery}; fn main() -> eframe::Result { env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). diff --git a/examples/puffin_profiler/src/main.rs b/examples/puffin_profiler/src/main.rs index f2d7a0c01..b75a20e00 100644 --- a/examples/puffin_profiler/src/main.rs +++ b/examples/puffin_profiler/src/main.rs @@ -165,7 +165,7 @@ fn start_puffin_server() { // We can store the server if we want, but in this case we just want // it to keep running. Dropping it closes the server, so let's not drop it! - #[allow(clippy::mem_forget)] + #[expect(clippy::mem_forget)] std::mem::forget(puffin_server); } Err(err) => { diff --git a/tests/egui_tests/tests/test_widgets.rs b/tests/egui_tests/tests/test_widgets.rs index 264b0052f..96015cbd9 100644 --- a/tests/egui_tests/tests/test_widgets.rs +++ b/tests/egui_tests/tests/test_widgets.rs @@ -2,9 +2,9 @@ use egui::load::SizedTexture; use egui::{ include_image, Align, Button, Color32, ColorImage, Direction, DragValue, Event, Grid, Layout, PointerButton, Pos2, Response, Slider, Stroke, StrokeKind, TextWrapMode, TextureHandle, - TextureOptions, Ui, UiBuilder, Vec2, Widget, + TextureOptions, Ui, UiBuilder, Vec2, Widget as _, }; -use egui_kittest::kittest::{by, Node, Queryable}; +use egui_kittest::kittest::{by, Node, Queryable as _}; use egui_kittest::{Harness, SnapshotResult, SnapshotResults}; #[test] diff --git a/tests/test_inline_glow_paint/src/main.rs b/tests/test_inline_glow_paint/src/main.rs index 125696526..3245af348 100644 --- a/tests/test_inline_glow_paint/src/main.rs +++ b/tests/test_inline_glow_paint/src/main.rs @@ -29,7 +29,7 @@ impl eframe::App for MyTestApp { use glow::HasContext as _; let gl = frame.gl().unwrap(); - #[allow(unsafe_code)] + #[expect(unsafe_code)] unsafe { gl.disable(glow::SCISSOR_TEST); gl.viewport(0, 0, 100, 100); From f2ce6424f3a32f47308fb9871d540c01377b2cd9 Mon Sep 17 00:00:00 2001 From: MStarha <59487310+MStarha@users.noreply.github.com> Date: Fri, 25 Apr 2025 11:01:22 +0200 Subject: [PATCH 064/129] `ScrollArea` improvements for user configurability (#5443) * Closes * [x] I have followed the instructions in the PR template The changes follow what is described in the issue with a couple changes: - Scroll bars are not hidden when dragging is disabled, for that `ScrollArea::scroll_bar_visibility()` has to be used, this is as not to limit the user configurability by imposing a specific function. The user might want to retain the scrollbars visibility to show the current position. - The input for mouse wheel scrolling is unchanged. When I inspected the code initially I made a mistake in recognizing the source of scrolling. Current implementation is in fact using `InputState::smooth_scroll_delta` and not `PassState::scroll_delta`, therefore it is possible to prevent scrolling by setting the `InputState::smooth_scroll_delta` to zero before painting the `ScrollArea`. A simple demo is available at https://github.com/MStarha/egui_scroll_area_test --- crates/egui/src/containers/scroll_area.rs | 274 ++++++++++++++++++---- crates/egui/src/containers/window.rs | 7 +- crates/egui_extras/src/table.rs | 7 +- 3 files changed, 236 insertions(+), 52 deletions(-) diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index b2df2100b..0c4cc7efb 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -1,8 +1,10 @@ #![allow(clippy::needless_range_loop)] +use std::ops::{Add, AddAssign, BitOr, BitOrAssign}; + use crate::{ - emath, epaint, lerp, pass_state, pos2, remap, remap_clamp, Context, Id, NumExt as _, Pos2, - Rangef, Rect, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b, + emath, epaint, lerp, pass_state, pos2, remap, remap_clamp, Context, CursorIcon, Id, + NumExt as _, Pos2, Rangef, Rect, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b, }; #[derive(Clone, Copy, Debug)] @@ -133,6 +135,113 @@ impl ScrollBarVisibility { ]; } +/// What is the source of scrolling for a [`ScrollArea`]. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct ScrollSource { + /// Scroll the area by dragging a scroll bar. + /// + /// By default the scroll bars remain visible to show current position. + /// To hide them use [`ScrollArea::scroll_bar_visibility()`]. + pub scroll_bar: bool, + + /// Scroll the area by dragging the contents. + pub drag: bool, + + /// Scroll the area by scrolling (or shift scrolling) the mouse wheel with + /// the mouse cursor over the [`ScrollArea`]. + pub mouse_wheel: bool, +} + +impl Default for ScrollSource { + fn default() -> Self { + Self::ALL + } +} + +impl ScrollSource { + pub const NONE: Self = Self { + scroll_bar: false, + drag: false, + mouse_wheel: false, + }; + pub const ALL: Self = Self { + scroll_bar: true, + drag: true, + mouse_wheel: true, + }; + pub const SCROLL_BAR: Self = Self { + scroll_bar: true, + drag: false, + mouse_wheel: false, + }; + pub const DRAG: Self = Self { + scroll_bar: false, + drag: true, + mouse_wheel: false, + }; + pub const MOUSE_WHEEL: Self = Self { + scroll_bar: false, + drag: false, + mouse_wheel: true, + }; + + /// Is everything disabled? + #[inline] + pub fn is_none(&self) -> bool { + self == &Self::NONE + } + + /// Is anything enabled? + #[inline] + pub fn any(&self) -> bool { + self.scroll_bar | self.drag | self.mouse_wheel + } + + /// Is everything enabled? + #[inline] + pub fn is_all(&self) -> bool { + self.scroll_bar & self.drag & self.mouse_wheel + } +} + +impl BitOr for ScrollSource { + type Output = Self; + + #[inline] + fn bitor(self, rhs: Self) -> Self::Output { + Self { + scroll_bar: self.scroll_bar | rhs.scroll_bar, + drag: self.drag | rhs.drag, + mouse_wheel: self.mouse_wheel | rhs.mouse_wheel, + } + } +} + +#[expect(clippy::suspicious_arithmetic_impl)] +impl Add for ScrollSource { + type Output = Self; + + #[inline] + fn add(self, rhs: Self) -> Self::Output { + self | rhs + } +} + +impl BitOrAssign for ScrollSource { + #[inline] + fn bitor_assign(&mut self, rhs: Self) { + *self = *self | rhs; + } +} + +impl AddAssign for ScrollSource { + #[inline] + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } +} + /// Add vertical and/or horizontal scrolling to a contained [`Ui`]. /// /// By default, scroll bars only show up when needed, i.e. when the contents @@ -168,7 +277,7 @@ impl ScrollBarVisibility { #[must_use = "You should call .show()"] pub struct ScrollArea { /// Do we have horizontal/vertical scrolling enabled? - scroll_enabled: Vec2b, + direction_enabled: Vec2b, auto_shrink: Vec2b, max_size: Vec2, @@ -178,10 +287,10 @@ pub struct ScrollArea { id_salt: Option, offset_x: Option, offset_y: Option, - - /// If false, we ignore scroll events. - scrolling_enabled: bool, - drag_to_scroll: bool, + on_hover_cursor: Option, + on_drag_cursor: Option, + scroll_source: ScrollSource, + wheel_scroll_multiplier: Vec2, /// If true for vertical or horizontal the scroll wheel will stick to the /// end position until user manually changes position. It will become true @@ -220,9 +329,9 @@ impl ScrollArea { /// Create a scroll area where you decide which axis has scrolling enabled. /// For instance, `ScrollArea::new([true, false])` enables horizontal scrolling. - pub fn new(scroll_enabled: impl Into) -> Self { + pub fn new(direction_enabled: impl Into) -> Self { Self { - scroll_enabled: scroll_enabled.into(), + direction_enabled: direction_enabled.into(), auto_shrink: Vec2b::TRUE, max_size: Vec2::INFINITY, min_scrolled_size: Vec2::splat(64.0), @@ -231,8 +340,10 @@ impl ScrollArea { id_salt: None, offset_x: None, offset_y: None, - scrolling_enabled: true, - drag_to_scroll: true, + on_hover_cursor: None, + on_drag_cursor: None, + scroll_source: ScrollSource::default(), + wheel_scroll_multiplier: Vec2::splat(1.0), stick_to_end: Vec2b::FALSE, animated: true, } @@ -355,17 +466,41 @@ impl ScrollArea { self } + /// Set the cursor used when the mouse pointer is hovering over the [`ScrollArea`]. + /// + /// Only applies if [`Self::scroll_source()`] has set [`ScrollSource::drag`] to `true`. + /// + /// Any changes to the mouse cursor made within the contents of the [`ScrollArea`] will + /// override this setting. + #[inline] + pub fn on_hover_cursor(mut self, cursor: CursorIcon) -> Self { + self.on_hover_cursor = Some(cursor); + self + } + + /// Set the cursor used when the [`ScrollArea`] is being dragged. + /// + /// Only applies if [`Self::scroll_source()`] has set [`ScrollSource::drag`] to `true`. + /// + /// Any changes to the mouse cursor made within the contents of the [`ScrollArea`] will + /// override this setting. + #[inline] + pub fn on_drag_cursor(mut self, cursor: CursorIcon) -> Self { + self.on_drag_cursor = Some(cursor); + self + } + /// Turn on/off scrolling on the horizontal axis. #[inline] pub fn hscroll(mut self, hscroll: bool) -> Self { - self.scroll_enabled[0] = hscroll; + self.direction_enabled[0] = hscroll; self } /// Turn on/off scrolling on the vertical axis. #[inline] pub fn vscroll(mut self, vscroll: bool) -> Self { - self.scroll_enabled[1] = vscroll; + self.direction_enabled[1] = vscroll; self } @@ -373,16 +508,16 @@ impl ScrollArea { /// /// You can pass in `false`, `true`, `[false, true]` etc. #[inline] - pub fn scroll(mut self, scroll_enabled: impl Into) -> Self { - self.scroll_enabled = scroll_enabled.into(); + pub fn scroll(mut self, direction_enabled: impl Into) -> Self { + self.direction_enabled = direction_enabled.into(); self } /// Turn on/off scrolling on the horizontal/vertical axes. #[deprecated = "Renamed to `scroll`"] #[inline] - pub fn scroll2(mut self, scroll_enabled: impl Into) -> Self { - self.scroll_enabled = scroll_enabled.into(); + pub fn scroll2(mut self, direction_enabled: impl Into) -> Self { + self.direction_enabled = direction_enabled.into(); self } @@ -395,9 +530,14 @@ impl ScrollArea { /// is typing text in a [`crate::TextEdit`] widget contained within the scroll area. /// /// This controls both scrolling directions. + #[deprecated = "Use `ScrollArea::scroll_source()"] #[inline] pub fn enable_scrolling(mut self, enable: bool) -> Self { - self.scrolling_enabled = enable; + self.scroll_source = if enable { + ScrollSource::ALL + } else { + ScrollSource::NONE + }; self } @@ -408,9 +548,28 @@ impl ScrollArea { /// If `true`, the [`ScrollArea`] will sense drags. /// /// Default: `true`. + #[deprecated = "Use `ScrollArea::scroll_source()"] #[inline] pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self { - self.drag_to_scroll = drag_to_scroll; + self.scroll_source.drag = drag_to_scroll; + self + } + + /// What sources does the [`ScrollArea`] use for scrolling the contents. + #[inline] + pub fn scroll_source(mut self, scroll_source: ScrollSource) -> Self { + self.scroll_source = scroll_source; + self + } + + /// The scroll amount caused by a mouse wheel scroll is multiplied by this amount. + /// + /// Independent for each scroll direction. Defaults to `Vec2{x: 1.0, y: 1.0}`. + /// + /// This can invert or effectively disable mouse scrolling. + #[inline] + pub fn wheel_scroll_multiplier(mut self, multiplier: Vec2) -> Self { + self.wheel_scroll_multiplier = multiplier; self } @@ -437,7 +596,7 @@ impl ScrollArea { /// Is any scrolling enabled? pub(crate) fn is_any_scroll_enabled(&self) -> bool { - self.scroll_enabled[0] || self.scroll_enabled[1] + self.direction_enabled[0] || self.direction_enabled[1] } /// The scroll handle will stick to the rightmost position even while the content size @@ -472,7 +631,7 @@ struct Prepared { auto_shrink: Vec2b, /// Does this `ScrollArea` have horizontal/vertical scrolling enabled? - scroll_enabled: Vec2b, + direction_enabled: Vec2b, /// Smoothly interpolated boolean of whether or not to show the scroll bars. show_bars_factor: Vec2, @@ -500,7 +659,8 @@ struct Prepared { /// `viewport.min == ZERO` means we scrolled to the top. viewport: Rect, - scrolling_enabled: bool, + scroll_source: ScrollSource, + wheel_scroll_multiplier: Vec2, stick_to_end: Vec2b, /// If there was a scroll target before the [`ScrollArea`] was added this frame, it's @@ -513,7 +673,7 @@ struct Prepared { impl ScrollArea { fn begin(self, ui: &mut Ui) -> Prepared { let Self { - scroll_enabled, + direction_enabled, auto_shrink, max_size, min_scrolled_size, @@ -522,14 +682,15 @@ impl ScrollArea { id_salt, offset_x, offset_y, - scrolling_enabled, - drag_to_scroll, + on_hover_cursor, + on_drag_cursor, + scroll_source, + wheel_scroll_multiplier, stick_to_end, animated, } = self; let ctx = ui.ctx().clone(); - let scrolling_enabled = scrolling_enabled && ui.is_enabled(); let id_salt = id_salt.unwrap_or_else(|| Id::new("scroll_area")); let id = ui.make_persistent_id(id_salt); @@ -546,7 +707,7 @@ impl ScrollArea { let show_bars: Vec2b = match scroll_bar_visibility { ScrollBarVisibility::AlwaysHidden => Vec2b::FALSE, ScrollBarVisibility::VisibleWhenNeeded => state.show_scroll, - ScrollBarVisibility::AlwaysVisible => scroll_enabled, + ScrollBarVisibility::AlwaysVisible => direction_enabled, }; let show_bars_factor = Vec2::new( @@ -568,7 +729,7 @@ impl ScrollArea { // one shouldn't collapse into nothingness. // See https://github.com/emilk/egui/issues/1097 for d in 0..2 { - if scroll_enabled[d] { + if direction_enabled[d] { inner_size[d] = inner_size[d].max(min_scrolled_size[d]); } } @@ -585,7 +746,7 @@ impl ScrollArea { } else { // Tell the inner Ui to use as much space as possible, we can scroll to see it! for d in 0..2 { - if scroll_enabled[d] { + if direction_enabled[d] { content_max_size[d] = f32::INFINITY; } } @@ -603,7 +764,7 @@ impl ScrollArea { let clip_rect_margin = ui.visuals().clip_rect_margin; let mut content_clip_rect = ui.clip_rect(); for d in 0..2 { - if scroll_enabled[d] { + if direction_enabled[d] { content_clip_rect.min[d] = inner_rect.min[d] - clip_rect_margin; content_clip_rect.max[d] = inner_rect.max[d] + clip_rect_margin; } else { @@ -619,7 +780,8 @@ impl ScrollArea { let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size); let dt = ui.input(|i| i.stable_dt).at_most(0.1); - if (scrolling_enabled && drag_to_scroll) + if scroll_source.drag + && ui.is_enabled() && (state.content_is_too_large[0] || state.content_is_too_large[1]) { // Drag contents to scroll (for touch screens mostly). @@ -634,7 +796,7 @@ impl ScrollArea { .is_some_and(|response| response.dragged()) { for d in 0..2 { - if scroll_enabled[d] { + if direction_enabled[d] { ui.input(|input| { state.offset[d] -= input.pointer.delta()[d]; }); @@ -649,7 +811,7 @@ impl ScrollArea { .is_some_and(|response| response.drag_stopped()) { state.vel = - scroll_enabled.to_vec2() * ui.input(|input| input.pointer.velocity()); + direction_enabled.to_vec2() * ui.input(|input| input.pointer.velocity()); } for d in 0..2 { // Kinetic scrolling @@ -668,6 +830,19 @@ impl ScrollArea { } } } + + // Set the desired mouse cursors. + if let Some(response) = content_response_option { + if response.dragged() { + if let Some(cursor) = on_drag_cursor { + response.on_hover_cursor(cursor); + } + } else if response.hovered() { + if let Some(cursor) = on_hover_cursor { + response.on_hover_cursor(cursor); + } + } + } } // Scroll with an animation if we have a target offset (that hasn't been cleared by the code @@ -709,7 +884,7 @@ impl ScrollArea { id, state, auto_shrink, - scroll_enabled, + direction_enabled, show_bars_factor, current_bar_use, scroll_bar_visibility, @@ -717,7 +892,8 @@ impl ScrollArea { inner_rect, content_ui, viewport, - scrolling_enabled, + scroll_source, + wheel_scroll_multiplier, stick_to_end, saved_scroll_target, animated, @@ -824,14 +1000,15 @@ impl Prepared { mut state, inner_rect, auto_shrink, - scroll_enabled, + direction_enabled, mut show_bars_factor, current_bar_use, scroll_bar_visibility, scroll_bar_rect, content_ui, viewport: _, - scrolling_enabled, + scroll_source, + wheel_scroll_multiplier, stick_to_end, saved_scroll_target, animated, @@ -854,7 +1031,7 @@ impl Prepared { .ctx() .pass_state_mut(|state| state.scroll_target[d].take()); - if scroll_enabled[d] { + if direction_enabled[d] { if let Some(target) = scroll_target { let pass_state::ScrollTarget { range, @@ -930,7 +1107,7 @@ impl Prepared { let mut inner_size = inner_rect.size(); for d in 0..2 { - inner_size[d] = match (scroll_enabled[d], auto_shrink[d]) { + inner_size[d] = match (direction_enabled[d], auto_shrink[d]) { (true, true) => inner_size[d].min(content_size[d]), // shrink scroll area if content is small (true, false) => inner_size[d], // let scroll area be larger than content; fill with blank space (false, true) => content_size[d], // Follow the content (expand/contract to fit it). @@ -944,18 +1121,18 @@ impl Prepared { let outer_rect = Rect::from_min_size(inner_rect.min, inner_rect.size() + current_bar_use); let content_is_too_large = Vec2b::new( - scroll_enabled[0] && inner_rect.width() < content_size.x, - scroll_enabled[1] && inner_rect.height() < content_size.y, + direction_enabled[0] && inner_rect.width() < content_size.x, + direction_enabled[1] && inner_rect.height() < content_size.y, ); let max_offset = content_size - inner_rect.size(); let is_hovering_outer_rect = ui.rect_contains_pointer(outer_rect); - if scrolling_enabled && is_hovering_outer_rect { + if scroll_source.mouse_wheel && ui.is_enabled() && is_hovering_outer_rect { let always_scroll_enabled_direction = ui.style().always_scroll_the_only_direction - && scroll_enabled[0] != scroll_enabled[1]; + && direction_enabled[0] != direction_enabled[1]; for d in 0..2 { - if scroll_enabled[d] { - let scroll_delta = ui.ctx().input_mut(|input| { + if direction_enabled[d] { + let scroll_delta = ui.ctx().input(|input| { if always_scroll_enabled_direction { // no bidirectional scrolling; allow horizontal scrolling without pressing shift input.smooth_scroll_delta[0] + input.smooth_scroll_delta[1] @@ -963,6 +1140,7 @@ impl Prepared { input.smooth_scroll_delta[d] } }); + let scroll_delta = scroll_delta * wheel_scroll_multiplier[d]; let scrolling_up = state.offset[d] > 0.0 && scroll_delta > 0.0; let scrolling_down = state.offset[d] < max_offset[d] && scroll_delta < 0.0; @@ -990,7 +1168,7 @@ impl Prepared { let show_scroll_this_frame = match scroll_bar_visibility { ScrollBarVisibility::AlwaysHidden => Vec2b::FALSE, ScrollBarVisibility::VisibleWhenNeeded => content_is_too_large, - ScrollBarVisibility::AlwaysVisible => scroll_enabled, + ScrollBarVisibility::AlwaysVisible => direction_enabled, }; // Avoid frame delay; start showing scroll bar right away: @@ -1120,7 +1298,7 @@ impl Prepared { let handle_rect = calculate_handle_rect(d, &state.offset); let interact_id = id.with(d); - let sense = if self.scrolling_enabled { + let sense = if scroll_source.scroll_bar && ui.is_enabled() { Sense::click_and_drag() } else { Sense::hover() @@ -1170,7 +1348,7 @@ impl Prepared { // Avoid frame-delay by calculating a new handle rect: let handle_rect = calculate_handle_rect(d, &state.offset); - let visuals = if scrolling_enabled { + let visuals = if scroll_source.scroll_bar && ui.is_enabled() { // Pick visuals based on interaction with the handle. // Remember that the response is for the whole scroll bar! let is_hovering_handle = response.hovered() diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index 52a81d37d..fa3d0d1fe 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -8,7 +8,7 @@ use epaint::{CornerRadiusF32, RectShape}; use crate::collapsing_header::CollapsingState; use crate::*; -use super::scroll_area::ScrollBarVisibility; +use super::scroll_area::{ScrollBarVisibility, ScrollSource}; use super::{area, resize, Area, Frame, Resize, ScrollArea}; /// Builder for a floating window which can be dragged, closed, collapsed, resized and scrolled (off by default). @@ -403,7 +403,10 @@ impl<'open> Window<'open> { /// See [`ScrollArea::drag_to_scroll`] for more. #[inline] pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self { - self.scroll = self.scroll.drag_to_scroll(drag_to_scroll); + self.scroll = self.scroll.scroll_source(ScrollSource { + drag: drag_to_scroll, + ..Default::default() + }); self } diff --git a/crates/egui_extras/src/table.rs b/crates/egui_extras/src/table.rs index 172f1bee5..2d64f2570 100644 --- a/crates/egui_extras/src/table.rs +++ b/crates/egui_extras/src/table.rs @@ -4,7 +4,7 @@ //! Takes all available height, so if you want something below the table, put it in a strip. use egui::{ - scroll_area::{ScrollAreaOutput, ScrollBarVisibility}, + scroll_area::{ScrollAreaOutput, ScrollBarVisibility, ScrollSource}, Align, Id, NumExt as _, Rangef, Rect, Response, ScrollArea, Ui, Vec2, Vec2b, }; @@ -745,7 +745,10 @@ impl Table<'_> { let mut scroll_area = ScrollArea::new([false, vscroll]) .id_salt(state_id.with("__scroll_area")) - .drag_to_scroll(drag_to_scroll) + .scroll_source(ScrollSource { + drag: drag_to_scroll, + ..Default::default() + }) .stick_to_bottom(stick_to_bottom) .min_scrolled_height(min_scrolled_height) .max_height(max_scroll_height) From 7d185acb41d9257997ae95e7459b24f3c5392f88 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Mon, 28 Apr 2025 11:58:05 +0200 Subject: [PATCH 065/129] Add button benchmark (#6854) This helped me benchmark the atomic layout (#5830) changes. I also realized that the label benchmark wasn't testing the painting, since the buttons at some point will be placed outside the screen_rect, meaning it won't be painted. This fixes it by benching the label in a child ui. The `label &str` benchmark went from 483 ns to 535 ns with these changes. EDIT: I fixed another benchmark problem, since the benchmark would show the same widget millions of times for a single frame, the WidgetRects hashmap would get huge, causing each iteration to slow down a bit more and causing the benchmark to have unreliable results. With this the `label &str` benchmark went from 535ns to 298ns. Also the `label format!` benchmark now takes almost the same time (302 ns). Before, it was a lot slower since it reused the same Context which already had millions of widget ids. --- crates/egui_demo_lib/README.md | 16 +++++ crates/egui_demo_lib/benches/benchmark.rs | 74 ++++++++++++++++++++--- 2 files changed, 80 insertions(+), 10 deletions(-) diff --git a/crates/egui_demo_lib/README.md b/crates/egui_demo_lib/README.md index 43ca0a0f3..58a5d6305 100644 --- a/crates/egui_demo_lib/README.md +++ b/crates/egui_demo_lib/README.md @@ -14,3 +14,19 @@ The demo library is a separate crate for three reasons: * To remove the amount of code in `egui` proper. * To make it easy for 3rd party egui integrations to use it for tests. - See for instance https://github.com/not-fl3/egui-miniquad/blob/master/examples/demo.rs + +This crate also contains benchmarks for egui. +Run them with +```bash +# Run all benchmarks +cargo bench -p egui_demo_lib + +# Run a single benchmark +cargo bench -p egui_demo_lib "benchmark name" + +# Profile benchmarks with cargo-flamegraph (--root flag is necessary for MacOS) +CARGO_PROFILE_BENCH_DEBUG=true cargo flamegraph --bench benchmark --root -p egui_demo_lib -- --bench "benchmark name" + +# Profile with cargo-instruments +CARGO_PROFILE_BENCH_DEBUG=true cargo instruments --profile bench --bench benchmark -p egui_demo_lib -t time -- --bench "benchmark name" +``` diff --git a/crates/egui_demo_lib/benches/benchmark.rs b/crates/egui_demo_lib/benches/benchmark.rs index dab6bdd7b..331788c9b 100644 --- a/crates/egui_demo_lib/benches/benchmark.rs +++ b/crates/egui_demo_lib/benches/benchmark.rs @@ -1,11 +1,20 @@ use std::fmt::Write as _; -use criterion::{criterion_group, criterion_main, Criterion}; +use criterion::{criterion_group, criterion_main, BatchSize, Criterion}; use egui::epaint::TextShape; +use egui::load::SizedTexture; +use egui::{Button, Id, TextureId, Ui, UiBuilder, Vec2}; use egui_demo_lib::LOREM_IPSUM_LONG; use rand::Rng as _; +/// Each iteration should be called in their own `Ui` with an intentional id clash, +/// to prevent the Context from building a massive map of `WidgetRects` (which would slow the test, +/// causing unreliable results). +fn create_benchmark_ui(ctx: &egui::Context) -> Ui { + Ui::new(ctx.clone(), Id::new("clashing_id"), UiBuilder::new()) +} + pub fn criterion_benchmark(c: &mut Criterion) { use egui::RawInput; @@ -55,17 +64,62 @@ pub fn criterion_benchmark(c: &mut Criterion) { { let ctx = egui::Context::default(); let _ = ctx.run(RawInput::default(), |ctx| { - egui::CentralPanel::default().show(ctx, |ui| { - c.bench_function("label &str", |b| { - b.iter(|| { + c.bench_function("label &str", |b| { + b.iter_batched_ref( + || create_benchmark_ui(ctx), + |ui| { ui.label("the quick brown fox jumps over the lazy dog"); - }); - }); - c.bench_function("label format!", |b| { - b.iter(|| { + }, + BatchSize::LargeInput, + ); + }); + c.bench_function("label format!", |b| { + b.iter_batched_ref( + || create_benchmark_ui(ctx), + |ui| { ui.label("the quick brown fox jumps over the lazy dog".to_owned()); - }); - }); + }, + BatchSize::LargeInput, + ); + }); + }); + } + + { + let ctx = egui::Context::default(); + let _ = ctx.run(RawInput::default(), |ctx| { + let mut group = c.benchmark_group("button"); + + // To ensure we have a valid image, let's use the font texture. The size + // shouldn't be important for this benchmark. + let image = SizedTexture::new(TextureId::default(), Vec2::splat(16.0)); + + group.bench_function("1_button_text", |b| { + b.iter_batched_ref( + || create_benchmark_ui(ctx), + |ui| { + ui.add(Button::new("Hello World")); + }, + BatchSize::LargeInput, + ); + }); + group.bench_function("2_button_text_image", |b| { + b.iter_batched_ref( + || create_benchmark_ui(ctx), + |ui| { + ui.add(Button::image_and_text(image, "Hello World")); + }, + BatchSize::LargeInput, + ); + }); + group.bench_function("3_button_text_image_right_text", |b| { + b.iter_batched_ref( + || create_benchmark_ui(ctx), + |ui| { + ui.add(Button::image_and_text(image, "Hello World").right_text("āµ")); + }, + BatchSize::LargeInput, + ); }); }); } From 3a02963c332ea1f5f0ed712a88efa83dde8ba9b1 Mon Sep 17 00:00:00 2001 From: Gaelan McMillan <73502924+gaelanmcmillan@users.noreply.github.com> Date: Tue, 29 Apr 2025 06:02:42 -0400 Subject: [PATCH 066/129] Add macOS-specific `has_shadow` and `with_has_shadow` to ViewportBuilder (#6850) * [X] I have followed the instructions in the PR template This PR fixes a ghosting issue I encountered while making a native macOS transparent overlay app using egui and eframe by exposing the [existing macOS window attribute `has_shadow`](https://docs.rs/winit/latest/winit/platform/macos/trait.WindowExtMacOS.html#tymethod.has_shadow) to the `ViewportBuilder` via a new `with_has_shadow` option. ## Example of Ghosting Issue ### Before `ViewportBuilder::with_has_shadow` By default, the underlying `winit` window's `.has_shadow()` defaults to `true`. https://github.com/user-attachments/assets/c3dcc2bd-535a-4960-918e-3ae5df503b12 ### After `ViewportBuilder::with_has_shadow` https://github.com/user-attachments/assets/484462a1-ea88-43e6-85b4-0bb9724e5f14 Source code for the above example can be found here: https://github.com/gaelanmcmillan/egui-overlay-app-with-shadow-artifacts-example/blob/main/src/main.rs ### Further background By default on macOS, `winit` windows have a drop-shadow effect. When creating a fully transparent overlay GUI, this drop-shadow can create a ghosting effect, as the window content has a drop shadow which is not cleared by the app itself. This issue has been experienced by users of `bevy`, another Rust project that has an upstream dependency on `winit`: https://github.com/bevyengine/bevy/issues/18673 --- crates/egui-winit/src/lib.rs | 4 +++- crates/egui/src/viewport.rs | 25 ++++++++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index fe4d2945d..03ec3e831 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -1620,6 +1620,7 @@ pub fn create_winit_window_attributes( title_shown: _title_shown, titlebar_buttons_shown: _titlebar_buttons_shown, titlebar_shown: _titlebar_shown, + has_shadow: _has_shadow, // Windows: drag_and_drop: _drag_and_drop, @@ -1764,7 +1765,8 @@ pub fn create_winit_window_attributes( .with_titlebar_buttons_hidden(!_titlebar_buttons_shown.unwrap_or(true)) .with_titlebar_transparent(!_titlebar_shown.unwrap_or(true)) .with_fullsize_content_view(_fullsize_content_view.unwrap_or(false)) - .with_movable_by_window_background(_movable_by_window_background.unwrap_or(false)); + .with_movable_by_window_background(_movable_by_window_background.unwrap_or(false)) + .with_has_shadow(_has_shadow.unwrap_or(true)); } window_attributes diff --git a/crates/egui/src/viewport.rs b/crates/egui/src/viewport.rs index 2a469435e..4e61e45e1 100644 --- a/crates/egui/src/viewport.rs +++ b/crates/egui/src/viewport.rs @@ -294,6 +294,7 @@ pub struct ViewportBuilder { pub title_shown: Option, pub titlebar_buttons_shown: Option, pub titlebar_shown: Option, + pub has_shadow: Option, // windows: pub drag_and_drop: Option, @@ -380,6 +381,10 @@ impl ViewportBuilder { /// The default is `false`. /// If this is not working, it's because the graphic context doesn't support transparency, /// you will need to set the transparency in the eframe! + /// + /// ## Platform-specific + /// + /// **macOS:** When using this feature to create an overlay-like UI, you likely want to combine this with [`Self::with_has_shadow`] set to `false` in order to avoid ghosting artifacts. #[inline] pub fn with_transparent(mut self, transparent: bool) -> Self { self.transparent = Some(transparent); @@ -433,7 +438,6 @@ impl ViewportBuilder { } /// macOS: Set to `true` to allow the window to be moved by dragging the background. - /// /// Enabling this feature can result in unexpected behaviour with draggable UI widgets such as sliders. #[inline] pub fn with_movable_by_background(mut self, value: bool) -> Self { @@ -462,6 +466,19 @@ impl ViewportBuilder { self } + /// macOS: Set to `false` to make the window render without a drop shadow. + /// + /// The default is `true`. + /// + /// Disabling this feature can solve ghosting issues experienced if using [`Self::with_transparent`]. + /// + /// Look at winit for more details + #[inline] + pub fn with_has_shadow(mut self, has_shadow: bool) -> Self { + self.has_shadow = Some(has_shadow); + self + } + /// windows: Whether show or hide the window icon in the taskbar. #[inline] pub fn with_taskbar(mut self, show: bool) -> Self { @@ -653,6 +670,7 @@ impl ViewportBuilder { title_shown: new_title_shown, titlebar_buttons_shown: new_titlebar_buttons_shown, titlebar_shown: new_titlebar_shown, + has_shadow: new_has_shadow, close_button: new_close_button, minimize_button: new_minimize_button, maximize_button: new_maximize_button, @@ -823,6 +841,11 @@ impl ViewportBuilder { recreate_window = true; } + if new_has_shadow.is_some() && self.has_shadow != new_has_shadow { + self.has_shadow = new_has_shadow; + recreate_window = true; + } + if new_taskbar.is_some() && self.taskbar != new_taskbar { self.taskbar = new_taskbar; recreate_window = true; From d666742c13604e2f15455fd97d8984ed635647c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=96R=C3=96K=20Attila?= Date: Tue, 29 Apr 2025 12:03:24 +0200 Subject: [PATCH 067/129] Bump `ron` to `0.10.1` (#6861) This should help `cargo-deny` be at peace with https://github.com/emilk/egui/pull/6860, pending https://github.com/gfx-rs/wgpu/pull/7557. --- Cargo.lock | 21 ++++++++------------- Cargo.toml | 2 +- crates/eframe/src/native/file_storage.rs | 3 ++- deny.toml | 1 - 4 files changed, 11 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4411be0c6..c430601f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "ab_glyph" @@ -539,12 +539,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" @@ -3153,7 +3147,7 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" dependencies = [ - "base64 0.22.1", + "base64", "indexmap", "quick-xml 0.32.0", "serde", @@ -3542,14 +3536,15 @@ dependencies = [ [[package]] name = "ron" -version = "0.8.1" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +checksum = "beceb6f7bf81c73e73aeef6dd1356d9a1b2b4909e1f0fc3e59b034f9572d7b7f" dependencies = [ - "base64 0.21.7", + "base64", "bitflags 2.8.0", "serde", "serde_derive", + "unicode-ident", ] [[package]] @@ -4344,7 +4339,7 @@ version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b74fc6b57825be3373f7054754755f03ac3a8f5d70015ccad699ba2029956f4a" dependencies = [ - "base64 0.22.1", + "base64", "flate2", "log", "once_cell", @@ -4386,7 +4381,7 @@ version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ac8e0e3e4696253dc06167990b3fe9a2668ab66270adf949a464db4088cb354" dependencies = [ - "base64 0.22.1", + "base64", "data-url", "flate2", "fontdb", diff --git a/Cargo.toml b/Cargo.toml index 80fee9f74..3b306176b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -94,7 +94,7 @@ profiling = { version = "1.0.16", default-features = false } puffin = "0.19" puffin_http = "0.16" raw-window-handle = "0.6.0" -ron = "0.8" +ron = "0.10.1" serde = { version = "1", features = ["derive"] } similar-asserts = "1.4.2" thiserror = "1.0.37" diff --git a/crates/eframe/src/native/file_storage.rs b/crates/eframe/src/native/file_storage.rs index fa89b9059..e1fdc9b84 100644 --- a/crates/eframe/src/native/file_storage.rs +++ b/crates/eframe/src/native/file_storage.rs @@ -207,7 +207,8 @@ fn save_to_disk(file_path: &PathBuf, kv: &HashMap) { let config = Default::default(); profiling::scope!("ron::serialize"); - if let Err(err) = ron::ser::to_writer_pretty(&mut writer, &kv, config) + if let Err(err) = ron::Options::default() + .to_io_writer_pretty(&mut writer, &kv, config) .and_then(|_| writer.flush().map_err(|err| err.into())) { log::warn!("Failed to serialize app state: {}", err); diff --git a/deny.toml b/deny.toml index 2f7bdbcca..f77dd4522 100644 --- a/deny.toml +++ b/deny.toml @@ -45,7 +45,6 @@ deny = [ ] skip = [ - { name = "base64" }, # Pretty small { name = "bit-set" }, # wgpu's naga depends on 0.8, syntect's (used by egui_extras) fancy-regex depends on 0.5 { name = "bit-vec" }, # dependency of bit-set in turn, different between 0.6 and 0.5 { name = "bitflags" }, # old 1.0 version via glutin, png, spirv, … From 8d9e42413a83d9c96b0232885b87664d104b5d8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=96R=C3=96K=20Attila?= Date: Tue, 29 Apr 2025 12:03:59 +0200 Subject: [PATCH 068/129] Remove outdated skip entries from deny.toml (#6862) Looks like these got deduplicated sometime. --- deny.toml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/deny.toml b/deny.toml index f77dd4522..a99373253 100644 --- a/deny.toml +++ b/deny.toml @@ -48,21 +48,15 @@ skip = [ { name = "bit-set" }, # wgpu's naga depends on 0.8, syntect's (used by egui_extras) fancy-regex depends on 0.5 { name = "bit-vec" }, # dependency of bit-set in turn, different between 0.6 and 0.5 { name = "bitflags" }, # old 1.0 version via glutin, png, spirv, … - { name = "event-listener" }, # TODO(emilk): rustls pulls in two versions of this 😭 - { name = "futures-lite" }, # old version via accesskit_unix and zbus - { name = "memoffset" }, # tiny dependency { name = "ndk-sys" }, # old version via wgpu, winit uses newer version { name = "quick-xml" }, # old version via wayland-scanner { name = "redox_syscall" }, # old version via winit { name = "thiserror" }, # ecosystem is in the process of migrating from 1.x to 2.x { name = "thiserror-impl" }, # same as above - { name = "time" }, # old version pulled in by unmaintained crate 'chrono' { name = "windows-core" }, # Chrono pulls in 0.51, accesskit uses 0.58.0 { name = "windows-sys" }, # glutin pulls in 0.52.0, accesskit pulls in 0.59.0, rfd pulls 0.48, webbrowser pulls 0.45.0 (via jni) ] skip-tree = [ - { name = "criterion" }, # dev-dependency - { name = "foreign-types" }, # small crate. Old version via core-graphics (winit). { name = "rfd" }, # example dependency ] From fed2ab5df3c0c92c9b18cd5ab80785302bcaf275 Mon Sep 17 00:00:00 2001 From: mitchmindtree Date: Tue, 29 Apr 2025 20:07:39 +1000 Subject: [PATCH 069/129] feat: Add `Scene::sense` option for customising how `Scene` should respond to user input (#5893) Allows for specifying how the `Scene` should respond to user input. With #5892, closes #5891. --- Edit: Failing tests unrelated and appear on master: https://github.com/emilk/egui/actions/runs/14330259861/job/40164414607. --------- Co-authored-by: Lucas Meurer --- crates/egui/src/containers/scene.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/containers/scene.rs b/crates/egui/src/containers/scene.rs index e5a350327..aaca37291 100644 --- a/crates/egui/src/containers/scene.rs +++ b/crates/egui/src/containers/scene.rs @@ -45,6 +45,7 @@ fn fit_to_rect_in_scene( #[must_use = "You should call .show()"] pub struct Scene { zoom_range: Rangef, + sense: Sense, max_inner_size: Vec2, drag_pan_buttons: DragPanButtons, } @@ -76,6 +77,7 @@ impl Default for Scene { fn default() -> Self { Self { zoom_range: Rangef::new(f32::EPSILON, 1.0), + sense: Sense::click_and_drag(), max_inner_size: Vec2::splat(1000.0), drag_pan_buttons: DragPanButtons::all(), } @@ -88,6 +90,17 @@ impl Scene { Default::default() } + /// Specify what type of input the scene should respond to. + /// + /// The default is `Sense::click_and_drag()`. + /// + /// Set this to `Sense::hover()` to disable panning via clicking and dragging. + #[inline] + pub fn sense(mut self, sense: Sense) -> Self { + self.sense = sense; + self + } + /// Set the allowed zoom range. /// /// The default zoom range is `0.0..=1.0`, @@ -184,7 +197,7 @@ impl Scene { UiBuilder::new() .layer_id(scene_layer_id) .max_rect(Rect::from_min_size(Pos2::ZERO, self.max_inner_size)) - .sense(Sense::click_and_drag()), + .sense(self.sense), ); let mut pan_response = local_ui.response(); From c075053391448515c5516723845b1863a8156c6b Mon Sep 17 00:00:00 2001 From: Will Brown Date: Tue, 29 Apr 2025 06:09:23 -0400 Subject: [PATCH 070/129] Add external eventloop support (#6750) * Closes #2875 * Closes https://github.com/emilk/egui/pull/3340 * [x] I have followed the instructions in the PR template Adds `create_native`. Similiar to `run_native` but it returns an `EframeWinitApplication` which is a `winit::ApplicationHandler`. This can be run on your own event loop. A helper fn `pump_eframe_app` is provided to pump the event loop and get the control flow state back. I have been using this approach for a few months. --------- Co-authored-by: Will Brown --- Cargo.lock | 59 +++++++- crates/eframe/src/lib.rs | 136 +++++++++++++++--- crates/eframe/src/native/run.rs | 130 +++++++++++++++++ examples/external_eventloop/Cargo.toml | 25 ++++ examples/external_eventloop/README.md | 7 + examples/external_eventloop/src/main.rs | 89 ++++++++++++ examples/external_eventloop_async/Cargo.toml | 35 +++++ examples/external_eventloop_async/README.md | 10 ++ examples/external_eventloop_async/src/app.rs | 130 +++++++++++++++++ examples/external_eventloop_async/src/main.rs | 15 ++ 10 files changed, 614 insertions(+), 22 deletions(-) create mode 100644 examples/external_eventloop/Cargo.toml create mode 100644 examples/external_eventloop/README.md create mode 100644 examples/external_eventloop/src/main.rs create mode 100644 examples/external_eventloop_async/Cargo.toml create mode 100644 examples/external_eventloop_async/README.md create mode 100644 examples/external_eventloop_async/src/app.rs create mode 100644 examples/external_eventloop_async/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index c430601f2..35ff4c9af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1614,6 +1614,26 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "external_eventloop" +version = "0.1.0" +dependencies = [ + "eframe", + "env_logger", + "winit", +] + +[[package]] +name = "external_eventloop_async" +version = "0.1.0" +dependencies = [ + "eframe", + "env_logger", + "log", + "tokio", + "winit", +] + [[package]] name = "fancy-regex" version = "0.11.0" @@ -2446,9 +2466,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.161" +version = "0.2.168" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" +checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" [[package]] name = "libloading" @@ -2603,6 +2623,17 @@ version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff" +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + [[package]] name = "multiple_viewports" version = "0.1.0" @@ -3860,6 +3891,16 @@ dependencies = [ "serde", ] +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "spirv" version = "0.3.0+sdk-1.3.268.0" @@ -4178,6 +4219,20 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tokio" +version = "1.44.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" +dependencies = [ + "backtrace", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.52.0", +] + [[package]] name = "toml_datetime" version = "0.6.8" diff --git a/crates/eframe/src/lib.rs b/crates/eframe/src/lib.rs index ec27b05a0..aefe4333b 100644 --- a/crates/eframe/src/lib.rs +++ b/crates/eframe/src/lib.rs @@ -182,6 +182,14 @@ pub use web::{WebLogger, WebRunner}; #[cfg(any(feature = "glow", feature = "wgpu"))] mod native; +#[cfg(not(target_arch = "wasm32"))] +#[cfg(any(feature = "glow", feature = "wgpu"))] +pub use native::run::EframeWinitApplication; + +#[cfg(not(any(target_arch = "wasm32", target_os = "ios")))] +#[cfg(any(feature = "glow", feature = "wgpu"))] +pub use native::run::EframePumpStatus; + #[cfg(not(target_arch = "wasm32"))] #[cfg(any(feature = "glow", feature = "wgpu"))] #[cfg(feature = "persistence")] @@ -242,26 +250,7 @@ pub fn run_native( mut native_options: NativeOptions, app_creator: AppCreator<'_>, ) -> Result { - #[cfg(not(feature = "__screenshot"))] - assert!( - std::env::var("EFRAME_SCREENSHOT_TO").is_err(), - "EFRAME_SCREENSHOT_TO found without compiling with the '__screenshot' feature" - ); - - if native_options.viewport.title.is_none() { - native_options.viewport.title = Some(app_name.to_owned()); - } - - let renderer = native_options.renderer; - - #[cfg(all(feature = "glow", feature = "wgpu"))] - { - match renderer { - Renderer::Glow => "glow", - Renderer::Wgpu => "wgpu", - }; - log::info!("Both the glow and wgpu renderers are available. Using {renderer}."); - } + let renderer = init_native(app_name, &mut native_options); match renderer { #[cfg(feature = "glow")] @@ -278,6 +267,113 @@ pub fn run_native( } } +/// Provides a proxy for your native eframe application to run on your own event loop. +/// +/// See `run_native` for details about `app_name`. +/// +/// Call from `fn main` like this: +/// ``` no_run +/// use eframe::{egui, UserEvent}; +/// use winit::event_loop::{ControlFlow, EventLoop}; +/// +/// fn main() -> eframe::Result { +/// let native_options = eframe::NativeOptions::default(); +/// let eventloop = EventLoop::::with_user_event().build()?; +/// eventloop.set_control_flow(ControlFlow::Poll); +/// +/// let mut winit_app = eframe::create_native( +/// "MyExtApp", +/// native_options, +/// Box::new(|cc| Ok(Box::new(MyEguiApp::new(cc)))), +/// &eventloop, +/// ); +/// +/// eventloop.run_app(&mut winit_app)?; +/// +/// Ok(()) +/// } +/// +/// #[derive(Default)] +/// struct MyEguiApp {} +/// +/// impl MyEguiApp { +/// fn new(cc: &eframe::CreationContext<'_>) -> Self { +/// Self::default() +/// } +/// } +/// +/// impl eframe::App for MyEguiApp { +/// fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { +/// egui::CentralPanel::default().show(ctx, |ui| { +/// ui.heading("Hello World!"); +/// }); +/// } +/// } +/// ``` +/// +/// See the `external_eventloop` example for a more complete example. +#[cfg(not(target_arch = "wasm32"))] +#[cfg(any(feature = "glow", feature = "wgpu"))] +pub fn create_native<'a>( + app_name: &str, + mut native_options: NativeOptions, + app_creator: AppCreator<'a>, + event_loop: &winit::event_loop::EventLoop, +) -> EframeWinitApplication<'a> { + let renderer = init_native(app_name, &mut native_options); + + match renderer { + #[cfg(feature = "glow")] + Renderer::Glow => { + log::debug!("Using the glow renderer"); + EframeWinitApplication::new(native::run::create_glow( + app_name, + native_options, + app_creator, + event_loop, + )) + } + + #[cfg(feature = "wgpu")] + Renderer::Wgpu => { + log::debug!("Using the wgpu renderer"); + EframeWinitApplication::new(native::run::create_wgpu( + app_name, + native_options, + app_creator, + event_loop, + )) + } + } +} + +#[cfg(not(target_arch = "wasm32"))] +#[cfg(any(feature = "glow", feature = "wgpu"))] +fn init_native(app_name: &str, native_options: &mut NativeOptions) -> Renderer { + #[cfg(not(feature = "__screenshot"))] + assert!( + std::env::var("EFRAME_SCREENSHOT_TO").is_err(), + "EFRAME_SCREENSHOT_TO found without compiling with the '__screenshot' feature" + ); + + if native_options.viewport.title.is_none() { + native_options.viewport.title = Some(app_name.to_owned()); + } + + let renderer = native_options.renderer; + + #[cfg(all(feature = "glow", feature = "wgpu"))] + { + match native_options.renderer { + Renderer::Glow => "glow", + Renderer::Wgpu => "wgpu", + }; + log::info!("Both the glow and wgpu renderers are available. Using {renderer}."); + } + + renderer +} + // ---------------------------------------------------------------------------- /// The simplest way to get started when writing a native app. diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs index 9bcb49686..8edfdbe2e 100644 --- a/crates/eframe/src/native/run.rs +++ b/crates/eframe/src/native/run.rs @@ -362,6 +362,19 @@ pub fn run_glow( run_and_exit(event_loop, glow_eframe) } +#[cfg(feature = "glow")] +pub fn create_glow<'a>( + app_name: &str, + native_options: epi::NativeOptions, + app_creator: epi::AppCreator<'a>, + event_loop: &EventLoop, +) -> impl ApplicationHandler + 'a { + use super::glow_integration::GlowWinitApp; + + let glow_eframe = GlowWinitApp::new(event_loop, app_name, native_options, app_creator); + WinitAppWrapper::new(glow_eframe, true) +} + // ---------------------------------------------------------------------------- #[cfg(feature = "wgpu")] @@ -386,3 +399,120 @@ pub fn run_wgpu( let wgpu_eframe = WgpuWinitApp::new(&event_loop, app_name, native_options, app_creator); run_and_exit(event_loop, wgpu_eframe) } + +#[cfg(feature = "wgpu")] +pub fn create_wgpu<'a>( + app_name: &str, + native_options: epi::NativeOptions, + app_creator: epi::AppCreator<'a>, + event_loop: &EventLoop, +) -> impl ApplicationHandler + 'a { + use super::wgpu_integration::WgpuWinitApp; + + let wgpu_eframe = WgpuWinitApp::new(event_loop, app_name, native_options, app_creator); + WinitAppWrapper::new(wgpu_eframe, true) +} + +// ---------------------------------------------------------------------------- + +/// A proxy to the eframe application that implements [`ApplicationHandler`]. +/// +/// This can be run directly on your own [`EventLoop`] by itself or with other +/// windows you manage outside of eframe. +pub struct EframeWinitApplication<'a> { + wrapper: Box + 'a>, + control_flow: ControlFlow, +} + +impl ApplicationHandler for EframeWinitApplication<'_> { + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + self.wrapper.resumed(event_loop); + } + + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + window_id: winit::window::WindowId, + event: winit::event::WindowEvent, + ) { + self.wrapper.window_event(event_loop, window_id, event); + } + + fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: winit::event::StartCause) { + self.wrapper.new_events(event_loop, cause); + } + + fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) { + self.wrapper.user_event(event_loop, event); + } + + fn device_event( + &mut self, + event_loop: &ActiveEventLoop, + device_id: winit::event::DeviceId, + event: winit::event::DeviceEvent, + ) { + self.wrapper.device_event(event_loop, device_id, event); + } + + fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { + self.wrapper.about_to_wait(event_loop); + self.control_flow = event_loop.control_flow(); + } + + fn suspended(&mut self, event_loop: &ActiveEventLoop) { + self.wrapper.suspended(event_loop); + } + + fn exiting(&mut self, event_loop: &ActiveEventLoop) { + self.wrapper.exiting(event_loop); + } + + fn memory_warning(&mut self, event_loop: &ActiveEventLoop) { + self.wrapper.memory_warning(event_loop); + } +} + +impl<'a> EframeWinitApplication<'a> { + pub(crate) fn new + 'a>(app: T) -> Self { + Self { + wrapper: Box::new(app), + control_flow: ControlFlow::default(), + } + } + + /// Pump the `EventLoop` to check for and dispatch pending events to this application. + /// + /// Returns either the exit code for the application or the final state of the [`ControlFlow`] + /// after all events have been dispatched in this iteration. + /// + /// This is useful when your [`EventLoop`] is not the main event loop for your application. + /// See the `external_eventloop_async` example. + #[cfg(not(target_os = "ios"))] + pub fn pump_eframe_app( + &mut self, + event_loop: &mut EventLoop, + timeout: Option, + ) -> EframePumpStatus { + use winit::platform::pump_events::{EventLoopExtPumpEvents as _, PumpStatus}; + + match event_loop.pump_app_events(timeout, self) { + PumpStatus::Continue => EframePumpStatus::Continue(self.control_flow), + PumpStatus::Exit(code) => EframePumpStatus::Exit(code), + } + } +} + +/// Either an exit code or a [`ControlFlow`] from the [`ActiveEventLoop`]. +/// +/// The result of [`EframeWinitApplication::pump_eframe_app`]. +#[cfg(not(target_os = "ios"))] +pub enum EframePumpStatus { + /// The final state of the [`ControlFlow`] after all events have been dispatched + /// + /// Callers should perform the action that is appropriate for the [`ControlFlow`] value. + Continue(ControlFlow), + + /// The exit code for the application + Exit(i32), +} diff --git a/examples/external_eventloop/Cargo.toml b/examples/external_eventloop/Cargo.toml new file mode 100644 index 000000000..301f30251 --- /dev/null +++ b/examples/external_eventloop/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "external_eventloop" +version = "0.1.0" +authors = ["Will Brown "] +license = "MIT OR Apache-2.0" +edition = "2021" +rust-version = "1.84" +publish = false + +[lints] +workspace = true + + +[dependencies] +eframe = { workspace = true, features = [ + "default", + "__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO +] } + +env_logger = { version = "0.10", default-features = false, features = [ + "auto-color", + "humantime", +] } + +winit = { workspace = true } diff --git a/examples/external_eventloop/README.md b/examples/external_eventloop/README.md new file mode 100644 index 000000000..11b06389b --- /dev/null +++ b/examples/external_eventloop/README.md @@ -0,0 +1,7 @@ +Example running an eframe application on an external eventloop. + +This allows you to run your eframe application alongside other windows and/or toolkits on the same event loop. + +```sh +cargo run -p external_eventloop +``` diff --git a/examples/external_eventloop/src/main.rs b/examples/external_eventloop/src/main.rs new file mode 100644 index 000000000..178c2865f --- /dev/null +++ b/examples/external_eventloop/src/main.rs @@ -0,0 +1,89 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +#![allow(rustdoc::missing_crate_level_docs)] // it's an example + +use eframe::{egui, UserEvent}; +use std::{cell::Cell, rc::Rc}; +use winit::event_loop::{ControlFlow, EventLoop}; + +fn main() -> eframe::Result { + env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). + let options = eframe::NativeOptions { + viewport: egui::ViewportBuilder::default().with_inner_size([320.0, 240.0]), + ..Default::default() + }; + + let eventloop = EventLoop::::with_user_event().build().unwrap(); + eventloop.set_control_flow(ControlFlow::Poll); + + let mut winit_app = eframe::create_native( + "External Eventloop Application", + options, + Box::new(|_| Ok(Box::::default())), + &eventloop, + ); + + eventloop.run_app(&mut winit_app)?; + + Ok(()) +} + +struct MyApp { + value: Rc>, + spin: bool, + blinky: bool, +} + +impl Default for MyApp { + fn default() -> Self { + Self { + value: Rc::new(Cell::new(42)), + spin: false, + blinky: false, + } + } +} + +impl eframe::App for MyApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::CentralPanel::default().show(ctx, |ui| { + ui.heading("My External Eventloop Application"); + + ui.horizontal(|ui| { + if ui.button("Increment Now").clicked() { + self.value.set(self.value.get() + 1); + } + }); + ui.label(format!("Value: {}", self.value.get())); + + if ui.button("Toggle Spinner").clicked() { + self.spin = !self.spin; + } + + if ui.button("Toggle Blinky").clicked() { + self.blinky = !self.blinky; + } + + if self.spin { + ui.spinner(); + } + + if self.blinky { + let now = ui.ctx().input(|i| i.time); + let blink = now % 1.0 < 0.5; + egui::Frame::new() + .inner_margin(3) + .corner_radius(5) + .fill(if blink { + egui::Color32::RED + } else { + egui::Color32::TRANSPARENT + }) + .show(ui, |ui| { + ui.label("Blinky!"); + }); + + ctx.request_repaint_after_secs((0.5 - (now % 0.5)) as f32); + } + }); + } +} diff --git a/examples/external_eventloop_async/Cargo.toml b/examples/external_eventloop_async/Cargo.toml new file mode 100644 index 000000000..399ff7c39 --- /dev/null +++ b/examples/external_eventloop_async/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "external_eventloop_async" +version = "0.1.0" +authors = ["Will Brown "] +license = "MIT OR Apache-2.0" +edition = "2021" +rust-version = "1.84" +publish = false + +[lints] +workspace = true + +[features] +linux-example = [] + +[[bin]] +name = "external_eventloop_async" +required-features = ["linux-example"] + +[dependencies] +eframe = { workspace = true, features = [ + "default", + "__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO +] } + +env_logger = { version = "0.10", default-features = false, features = [ + "auto-color", + "humantime", +] } + +log = { workspace = true } + +winit = { workspace = true } + +tokio = { version = "1", features = ["rt", "time", "net"] } diff --git a/examples/external_eventloop_async/README.md b/examples/external_eventloop_async/README.md new file mode 100644 index 000000000..37755e08d --- /dev/null +++ b/examples/external_eventloop_async/README.md @@ -0,0 +1,10 @@ +Example running an eframe application on an external eventloop on top of a tokio executor on Linux. + +By running the event loop, eframe, and tokio in the same thread, one can leverage local async tasks. +These tasks can share data with the UI without the need for locks or message passing. + +In tokio CPU-bound async tasks can be run with `spawn_blocking` to avoid impacting the UI frame rate. + +```sh +cargo run -p external_eventloop_async --features linux-example +``` diff --git a/examples/external_eventloop_async/src/app.rs b/examples/external_eventloop_async/src/app.rs new file mode 100644 index 000000000..de3326b19 --- /dev/null +++ b/examples/external_eventloop_async/src/app.rs @@ -0,0 +1,130 @@ +use eframe::{egui, EframePumpStatus, UserEvent}; +use std::{cell::Cell, io, os::fd::AsRawFd as _, rc::Rc, time::Duration}; +use tokio::task::LocalSet; +use winit::event_loop::{ControlFlow, EventLoop}; + +pub fn run() -> io::Result<()> { + env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). + let options = eframe::NativeOptions { + viewport: egui::ViewportBuilder::default().with_inner_size([320.0, 240.0]), + ..Default::default() + }; + + let mut eventloop = EventLoop::::with_user_event().build().unwrap(); + eventloop.set_control_flow(ControlFlow::Poll); + + let mut winit_app = eframe::create_native( + "External Eventloop Application", + options, + Box::new(|_| Ok(Box::::default())), + &eventloop, + ); + + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + + let local = LocalSet::new(); + local.block_on(&rt, async { + let eventloop_fd = tokio::io::unix::AsyncFd::new(eventloop.as_raw_fd())?; + let mut control_flow = ControlFlow::Poll; + + loop { + let mut guard = match control_flow { + ControlFlow::Poll => None, + ControlFlow::Wait => Some(eventloop_fd.readable().await?), + ControlFlow::WaitUntil(deadline) => { + tokio::time::timeout_at(deadline.into(), eventloop_fd.readable()) + .await + .ok() + .transpose()? + } + }; + + match winit_app.pump_eframe_app(&mut eventloop, None) { + EframePumpStatus::Continue(next) => control_flow = next, + EframePumpStatus::Exit(code) => { + log::info!("exit code: {code}"); + break; + } + } + + if let Some(mut guard) = guard.take() { + guard.clear_ready(); + } + } + + Ok::<_, io::Error>(()) + }) +} + +struct MyApp { + value: Rc>, + spin: bool, + blinky: bool, +} + +impl Default for MyApp { + fn default() -> Self { + Self { + value: Rc::new(Cell::new(42)), + spin: false, + blinky: false, + } + } +} + +impl eframe::App for MyApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::CentralPanel::default().show(ctx, |ui| { + ui.heading("My External Eventloop Application"); + + ui.horizontal(|ui| { + if ui.button("Increment Now").clicked() { + self.value.set(self.value.get() + 1); + } + if ui.button("Increment Later").clicked() { + let value = self.value.clone(); + let ctx = ctx.clone(); + tokio::task::spawn_local(async move { + tokio::time::sleep(Duration::from_secs(1)).await; + value.set(value.get() + 1); + ctx.request_repaint(); + }); + } + }); + ui.label(format!("Value: {}", self.value.get())); + + if ui.button("Toggle Spinner").clicked() { + self.spin = !self.spin; + } + + if ui.button("Toggle Blinky").clicked() { + self.blinky = !self.blinky; + } + + if self.spin { + ui.spinner(); + } + + if self.blinky { + let now = ui.ctx().input(|i| i.time); + let blink = now % 1.0 < 0.5; + egui::Frame::new() + .inner_margin(3) + .corner_radius(5) + .fill(if blink { + egui::Color32::RED + } else { + egui::Color32::TRANSPARENT + }) + .show(ui, |ui| { + ui.label("Blinky!"); + }); + + ctx.request_repaint_after_secs((0.5 - (now % 0.5)) as f32); + } + }); + } +} diff --git a/examples/external_eventloop_async/src/main.rs b/examples/external_eventloop_async/src/main.rs new file mode 100644 index 000000000..bbb52084f --- /dev/null +++ b/examples/external_eventloop_async/src/main.rs @@ -0,0 +1,15 @@ +#![allow(rustdoc::missing_crate_level_docs)] // it's an example + +#[cfg(target_os = "linux")] +mod app; + +#[cfg(target_os = "linux")] +fn main() -> std::io::Result<()> { + app::run() +} + +// Do not check `app` on unsupported platforms when check "--all-features" is used in CI. +#[cfg(not(target_os = "linux"))] +fn main() { + println!("This example only supports Linux."); +} From 1ab325900878dd41794d033bd30b49a6f14c937e Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Wed, 30 Apr 2025 10:38:41 +0200 Subject: [PATCH 071/129] Add italic button benchmark to test `RichText` performance impact (#6897) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Time on my m4 pro macbook: 302.79 ns (vs 303.83 ns for the regular button 🤷) --- crates/egui_demo_lib/benches/benchmark.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/crates/egui_demo_lib/benches/benchmark.rs b/crates/egui_demo_lib/benches/benchmark.rs index 331788c9b..2b9a2cc05 100644 --- a/crates/egui_demo_lib/benches/benchmark.rs +++ b/crates/egui_demo_lib/benches/benchmark.rs @@ -4,7 +4,7 @@ use criterion::{criterion_group, criterion_main, BatchSize, Criterion}; use egui::epaint::TextShape; use egui::load::SizedTexture; -use egui::{Button, Id, TextureId, Ui, UiBuilder, Vec2}; +use egui::{Button, Id, RichText, TextureId, Ui, UiBuilder, Vec2}; use egui_demo_lib::LOREM_IPSUM_LONG; use rand::Rng as _; @@ -121,6 +121,15 @@ pub fn criterion_benchmark(c: &mut Criterion) { BatchSize::LargeInput, ); }); + group.bench_function("4_button_italic", |b| { + b.iter_batched_ref( + || create_benchmark_ui(ctx), + |ui| { + ui.add(Button::new(RichText::new("Hello World").italics())); + }, + BatchSize::LargeInput, + ); + }); }); } From fdaac16e4a5515472f01de276a5d5fc39b5af737 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Wed, 30 Apr 2025 10:40:50 +0200 Subject: [PATCH 072/129] Fix image button panicking with tiny `available_space` (#6900) * Fixes * [x] I have followed the instructions in the PR template --- crates/egui/src/widgets/button.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 2a85a164d..a75997eff 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -249,7 +249,7 @@ impl Widget for Button<'_> { ) } else { ( - ui.available_size() - 2.0 * button_padding, + (ui.available_size() - 2.0 * button_padding).at_least(Vec2::ZERO), default_font_height(), ) }; From 2947821c60f4ef474194695bcd3e71c9def380cc Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Wed, 30 Apr 2025 12:55:57 +0200 Subject: [PATCH 073/129] Load images on the ui thread for tests (#6901) https://github.com/emilk/egui/pull/5394 made it so images would load on a background thread, which is great. But this makes snapshot tests that have images via include_image!() flakey since they might or might not load by the time the snapshot is rendered. This is no perfect solution, since the underlying problem of "waiting for something async to happen" still exists and we should add some more general solution for that. --- crates/egui_extras/src/loaders/image_loader.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/crates/egui_extras/src/loaders/image_loader.rs b/crates/egui_extras/src/loaders/image_loader.rs index bb025651d..2528c2aec 100644 --- a/crates/egui_extras/src/loaders/image_loader.rs +++ b/crates/egui_extras/src/loaders/image_loader.rs @@ -8,9 +8,6 @@ use egui::{ use image::ImageFormat; use std::{mem::size_of, path::Path, sync::Arc, task::Poll}; -#[cfg(not(target_arch = "wasm32"))] -use std::thread; - type Entry = Poll, String>>; #[derive(Default)] @@ -76,7 +73,7 @@ impl ImageLoader for ImageCrateLoader { return Err(LoadError::NotSupported); } - #[cfg(not(target_arch = "wasm32"))] + #[cfg(not(any(target_arch = "wasm32", test)))] #[expect(clippy::unnecessary_wraps)] // needed here to match other return types fn load_image( ctx: &egui::Context, @@ -88,7 +85,7 @@ impl ImageLoader for ImageCrateLoader { cache.lock().insert(uri.clone(), Poll::Pending); // Do the image parsing on a bg thread - thread::Builder::new() + std::thread::Builder::new() .name(format!("egui_extras::ImageLoader::load({uri:?})")) .spawn({ let ctx = ctx.clone(); @@ -116,7 +113,8 @@ impl ImageLoader for ImageCrateLoader { Ok(ImagePoll::Pending { size: None }) } - #[cfg(target_arch = "wasm32")] + // Load images on the current thread for tests, so they are less flaky + #[cfg(any(target_arch = "wasm32", test))] fn load_image( _ctx: &egui::Context, uri: &str, From f3611e3e5a448ba8a96cb880ea3a29245bb3a2d2 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 30 Apr 2025 14:10:59 +0200 Subject: [PATCH 074/129] Enforce that PRs are not opened from the 'master' branch of a fork (#6899) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Fail all PRs that are opened from the master/main branch of the fork. ## Why PR:s opened from the `master` branch cannot be collaborated on. That is, we maintainers cannot push our own commits to it (e.g. to fix smaller problems with it before merging). ## How Untested code straight from Claude 3.7 šŸ˜… --- .github/workflows/enforce_branch_name.yml | 34 +++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/enforce_branch_name.yml diff --git a/.github/workflows/enforce_branch_name.yml b/.github/workflows/enforce_branch_name.yml new file mode 100644 index 000000000..7868426cb --- /dev/null +++ b/.github/workflows/enforce_branch_name.yml @@ -0,0 +1,34 @@ +name: PR Branch Name Check + +on: + pull_request_target: + types: [opened, reopened, synchronize] + +jobs: + check-source-branch: + runs-on: ubuntu-latest + steps: + - name: Leave comment if PR is from master/main branch of fork + if: ${{ github.event.pull_request.head.repo.fork == 'true' && (github.event.pull_request.head.ref == 'master' || github.event.pull_request.head.ref == 'main') }} + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: 'āš ļø **ERROR:** Pull requests from the `master`/`main` branch of forks are not allowed, because it prevents maintainers from contributing to your PR. Please create a feature branch in your fork and submit the PR from that branch instead.' + }) + + - name: Check PR source branch + run: | + # Check if PR is from a fork + if [[ "${{ github.event.pull_request.head.repo.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 + 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 + fi + fi From 6c922f72a819e6083ffc4b6a452c2493c9170e63 Mon Sep 17 00:00:00 2001 From: Alexander Nadeau Date: Wed, 30 Apr 2025 08:12:08 -0400 Subject: [PATCH 075/129] Fix text distortion on mobile devices/browsers with `glow` backend (#6893) Did not test on platforms other than my phone, but I can't imagine it causing problems. AFAIK if highp isn't supported then `precision highp float;` needs to still not cause the program to fail to link/compile or anything; it should just silently use some other precision. * Fixes https://github.com/emilk/egui/issues/4268 for me but I only tested it on a native Android app and I don't know whether backends other than glow are affected. * [x] I have followed the instructions in the PR template (but the change is trivial so I'm just doing it from the master branch) Before: ![image](https://github.com/user-attachments/assets/9f449749-5a48-4e9c-aef0-7a8ac3912eb6) After: ![image](https://github.com/user-attachments/assets/544e5977-13e0-411a-bccf-b15a15289e28) --- crates/egui_glow/src/shader/fragment.glsl | 8 +++++++- crates/egui_glow/src/shader/vertex.glsl | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/crates/egui_glow/src/shader/fragment.glsl b/crates/egui_glow/src/shader/fragment.glsl index 30da2809e..f2792ed04 100644 --- a/crates/egui_glow/src/shader/fragment.glsl +++ b/crates/egui_glow/src/shader/fragment.glsl @@ -1,5 +1,11 @@ #ifdef GL_ES - precision mediump float; + // To avoid weird distortion issues when rendering text etc, we want highp if possible. + // But apparently some devices don't support it, so we have to check first. + #if defined(GL_FRAGMENT_PRECISION_HIGH) && GL_FRAGMENT_PRECISION_HIGH == 1 + precision highp float; + #else + precision mediump float; + #endif #endif uniform sampler2D u_sampler; diff --git a/crates/egui_glow/src/shader/vertex.glsl b/crates/egui_glow/src/shader/vertex.glsl index fff31463c..0c6e9d231 100644 --- a/crates/egui_glow/src/shader/vertex.glsl +++ b/crates/egui_glow/src/shader/vertex.glsl @@ -9,7 +9,13 @@ #endif #ifdef GL_ES - precision mediump float; + // To avoid weird distortion issues when rendering text etc, we want highp if possible. + // But apparently some devices don't support it, so we have to check first. + #if defined(GL_FRAGMENT_PRECISION_HIGH) && GL_FRAGMENT_PRECISION_HIGH == 1 + precision highp float; + #else + precision mediump float; + #endif #endif uniform vec2 u_screen_size; From ba70106399645cdf66a36736137dc135a843f830 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 6 May 2025 10:25:02 +0200 Subject: [PATCH 076/129] Fix enforce_branch_name.yml --- .github/workflows/enforce_branch_name.yml | 26 +++++++++++------------ 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/enforce_branch_name.yml b/.github/workflows/enforce_branch_name.yml index 7868426cb..85035330e 100644 --- a/.github/workflows/enforce_branch_name.yml +++ b/.github/workflows/enforce_branch_name.yml @@ -8,19 +8,6 @@ jobs: check-source-branch: runs-on: ubuntu-latest steps: - - name: Leave comment if PR is from master/main branch of fork - if: ${{ github.event.pull_request.head.repo.fork == 'true' && (github.event.pull_request.head.ref == 'master' || github.event.pull_request.head.ref == 'main') }} - uses: actions/github-script@v6 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: 'āš ļø **ERROR:** Pull requests from the `master`/`main` branch of forks are not allowed, because it prevents maintainers from contributing to your PR. Please create a feature branch in your fork and submit the PR from that branch instead.' - }) - - name: Check PR source branch run: | # Check if PR is from a fork @@ -32,3 +19,16 @@ jobs: exit 1 fi fi + + - name: Leave comment if PR is from master/main branch of fork d + if: ${{ failure() }} + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: 'āš ļø **ERROR:** Pull requests from the `master`/`main` branch of forks are not allowed, because it prevents maintainers from contributing to your PR. Please create a feature branch in your fork and submit the PR from that branch instead.' + }) From 71e0b0859cd3dd9cf362f39345ed6a66c3889032 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 6 May 2025 17:35:56 +0200 Subject: [PATCH 077/129] Make `WidgetText` smaller and faster (#6903) * In preparation of #5830, this should reduce the performance impact of that PR --------- Co-authored-by: Emil Ernerfeldt --- crates/egui/src/widget_text.rs | 202 ++++++++++++-------- crates/egui/src/widgets/label.rs | 14 +- crates/egui_demo_app/src/wrap_app.rs | 2 +- crates/epaint/src/text/text_layout_types.rs | 15 ++ 4 files changed, 150 insertions(+), 83 deletions(-) diff --git a/crates/egui/src/widget_text.rs b/crates/egui/src/widget_text.rs index e66cb1bc8..d9f98859b 100644 --- a/crates/egui/src/widget_text.rs +++ b/crates/egui/src/widget_text.rs @@ -1,6 +1,7 @@ use std::{borrow::Cow, sync::Arc}; use emath::GuiRounding as _; +use epaint::text::TextFormat; use crate::{ text::{LayoutJob, TextWrapping}, @@ -488,7 +489,16 @@ impl RichText { /// which will be replaced with a color chosen by the widget that paints the text. #[derive(Clone)] pub enum WidgetText { - RichText(RichText), + /// Plain unstyled text. + /// + /// We have this as a special case, as it is the common-case, + /// and it uses less memory than [`Self::RichText`]. + Text(String), + + /// Text and optional style choices for it. + /// + /// Prefer [`Self::Text`] if there is no styling, as it will be faster. + RichText(Arc), /// Use this [`LayoutJob`] when laying out the text. /// @@ -502,7 +512,7 @@ pub enum WidgetText { /// /// You can color the text however you want, or use [`Color32::PLACEHOLDER`] /// which will be replaced with a color chosen by the widget that paints the text. - LayoutJob(LayoutJob), + LayoutJob(Arc), /// Use exactly this galley when painting the text. /// @@ -513,7 +523,7 @@ pub enum WidgetText { impl Default for WidgetText { fn default() -> Self { - Self::RichText(RichText::default()) + Self::Text(String::new()) } } @@ -521,6 +531,7 @@ impl WidgetText { #[inline] pub fn is_empty(&self) -> bool { match self { + Self::Text(text) => text.is_empty(), Self::RichText(text) => text.is_empty(), Self::LayoutJob(job) => job.is_empty(), Self::Galley(galley) => galley.is_empty(), @@ -530,21 +541,36 @@ impl WidgetText { #[inline] pub fn text(&self) -> &str { match self { + Self::Text(text) => text, Self::RichText(text) => text.text(), Self::LayoutJob(job) => &job.text, Self::Galley(galley) => galley.text(), } } + /// Map the contents based on the provided closure. + /// + /// - [`Self::Text`] => convert to [`RichText`] and call f + /// - [`Self::RichText`] => call f + /// - else do nothing + #[must_use] + fn map_rich_text(self, f: F) -> Self + where + F: FnOnce(RichText) -> RichText, + { + match self { + Self::Text(text) => Self::RichText(Arc::new(f(RichText::new(text)))), + Self::RichText(text) => Self::RichText(Arc::new(f(Arc::unwrap_or_clone(text)))), + other => other, + } + } + /// Override the [`TextStyle`] if, and only if, this is a [`RichText`]. /// /// Prefer using [`RichText`] directly! #[inline] pub fn text_style(self, text_style: TextStyle) -> Self { - match self { - Self::RichText(text) => Self::RichText(text.text_style(text_style)), - Self::LayoutJob(_) | Self::Galley(_) => self, - } + self.map_rich_text(|text| text.text_style(text_style)) } /// Set the [`TextStyle`] unless it has already been set @@ -552,10 +578,7 @@ impl WidgetText { /// Prefer using [`RichText`] directly! #[inline] pub fn fallback_text_style(self, text_style: TextStyle) -> Self { - match self { - Self::RichText(text) => Self::RichText(text.fallback_text_style(text_style)), - Self::LayoutJob(_) | Self::Galley(_) => self, - } + self.map_rich_text(|text| text.fallback_text_style(text_style)) } /// Override text color if, and only if, this is a [`RichText`]. @@ -563,111 +586,85 @@ impl WidgetText { /// Prefer using [`RichText`] directly! #[inline] pub fn color(self, color: impl Into) -> Self { - match self { - Self::RichText(text) => Self::RichText(text.color(color)), - Self::LayoutJob(_) | Self::Galley(_) => self, - } + self.map_rich_text(|text| text.color(color)) } /// Prefer using [`RichText`] directly! + #[inline] pub fn heading(self) -> Self { - match self { - Self::RichText(text) => Self::RichText(text.heading()), - Self::LayoutJob(_) | Self::Galley(_) => self, - } + self.map_rich_text(|text| text.heading()) } /// Prefer using [`RichText`] directly! + #[inline] pub fn monospace(self) -> Self { - match self { - Self::RichText(text) => Self::RichText(text.monospace()), - Self::LayoutJob(_) | Self::Galley(_) => self, - } + self.map_rich_text(|text| text.monospace()) } /// Prefer using [`RichText`] directly! + #[inline] pub fn code(self) -> Self { - match self { - Self::RichText(text) => Self::RichText(text.code()), - Self::LayoutJob(_) | Self::Galley(_) => self, - } + self.map_rich_text(|text| text.code()) } /// Prefer using [`RichText`] directly! + #[inline] pub fn strong(self) -> Self { - match self { - Self::RichText(text) => Self::RichText(text.strong()), - Self::LayoutJob(_) | Self::Galley(_) => self, - } + self.map_rich_text(|text| text.strong()) } /// Prefer using [`RichText`] directly! + #[inline] pub fn weak(self) -> Self { - match self { - Self::RichText(text) => Self::RichText(text.weak()), - Self::LayoutJob(_) | Self::Galley(_) => self, - } + self.map_rich_text(|text| text.weak()) } /// Prefer using [`RichText`] directly! + #[inline] pub fn underline(self) -> Self { - match self { - Self::RichText(text) => Self::RichText(text.underline()), - Self::LayoutJob(_) | Self::Galley(_) => self, - } + self.map_rich_text(|text| text.underline()) } /// Prefer using [`RichText`] directly! + #[inline] pub fn strikethrough(self) -> Self { - match self { - Self::RichText(text) => Self::RichText(text.strikethrough()), - Self::LayoutJob(_) | Self::Galley(_) => self, - } + self.map_rich_text(|text| text.strikethrough()) } /// Prefer using [`RichText`] directly! + #[inline] pub fn italics(self) -> Self { - match self { - Self::RichText(text) => Self::RichText(text.italics()), - Self::LayoutJob(_) | Self::Galley(_) => self, - } + self.map_rich_text(|text| text.italics()) } /// Prefer using [`RichText`] directly! + #[inline] pub fn small(self) -> Self { - match self { - Self::RichText(text) => Self::RichText(text.small()), - Self::LayoutJob(_) | Self::Galley(_) => self, - } + self.map_rich_text(|text| text.small()) } /// Prefer using [`RichText`] directly! + #[inline] pub fn small_raised(self) -> Self { - match self { - Self::RichText(text) => Self::RichText(text.small_raised()), - Self::LayoutJob(_) | Self::Galley(_) => self, - } + self.map_rich_text(|text| text.small_raised()) } /// Prefer using [`RichText`] directly! + #[inline] pub fn raised(self) -> Self { - match self { - Self::RichText(text) => Self::RichText(text.raised()), - Self::LayoutJob(_) | Self::Galley(_) => self, - } + self.map_rich_text(|text| text.raised()) } /// Prefer using [`RichText`] directly! + #[inline] pub fn background_color(self, background_color: impl Into) -> Self { - match self { - Self::RichText(text) => Self::RichText(text.background_color(background_color)), - Self::LayoutJob(_) | Self::Galley(_) => self, - } + self.map_rich_text(|text| text.background_color(background_color)) } /// Returns a value rounded to [`emath::GUI_ROUNDING`]. pub(crate) fn font_height(&self, fonts: &epaint::Fonts, style: &Style) -> f32 { match self { + Self::Text(_) => fonts.row_height(&FontSelection::Default.resolve(style)), Self::RichText(text) => text.font_height(fonts, style), Self::LayoutJob(job) => job.font_height(fonts), Self::Galley(galley) => { @@ -685,11 +682,24 @@ impl WidgetText { style: &Style, fallback_font: FontSelection, default_valign: Align, - ) -> LayoutJob { + ) -> Arc { match self { - Self::RichText(text) => text.into_layout_job(style, fallback_font, default_valign), + Self::Text(text) => Arc::new(LayoutJob::simple_format( + text, + TextFormat { + font_id: FontSelection::Default.resolve(style), + color: crate::Color32::PLACEHOLDER, + valign: default_valign, + ..Default::default() + }, + )), + Self::RichText(text) => Arc::new(Arc::unwrap_or_clone(text).into_layout_job( + style, + fallback_font, + default_valign, + )), Self::LayoutJob(job) => job, - Self::Galley(galley) => (*galley.job).clone(), + Self::Galley(galley) => galley.job.clone(), } } @@ -721,12 +731,30 @@ impl WidgetText { default_valign: Align, ) -> Arc { match self { - Self::RichText(text) => { - let mut layout_job = text.into_layout_job(style, fallback_font, default_valign); + Self::Text(text) => { + let mut layout_job = LayoutJob::simple_format( + text, + TextFormat { + font_id: FontSelection::Default.resolve(style), + color: crate::Color32::PLACEHOLDER, + valign: default_valign, + ..Default::default() + }, + ); layout_job.wrap = text_wrapping; ctx.fonts(|f| f.layout_job(layout_job)) } - Self::LayoutJob(mut job) => { + Self::RichText(text) => { + let mut layout_job = Arc::unwrap_or_clone(text).into_layout_job( + style, + fallback_font, + default_valign, + ); + layout_job.wrap = text_wrapping; + ctx.fonts(|f| f.layout_job(layout_job)) + } + Self::LayoutJob(job) => { + let mut job = Arc::unwrap_or_clone(job); job.wrap = text_wrapping; ctx.fonts(|f| f.layout_job(job)) } @@ -738,48 +766,55 @@ impl WidgetText { impl From<&str> for WidgetText { #[inline] fn from(text: &str) -> Self { - Self::RichText(RichText::new(text)) + Self::Text(text.to_owned()) } } impl From<&String> for WidgetText { #[inline] fn from(text: &String) -> Self { - Self::RichText(RichText::new(text)) + Self::Text(text.clone()) } } impl From for WidgetText { #[inline] fn from(text: String) -> Self { - Self::RichText(RichText::new(text)) + Self::Text(text) } } impl From<&Box> for WidgetText { #[inline] fn from(text: &Box) -> Self { - Self::RichText(RichText::new(text.clone())) + Self::Text(text.to_string()) } } impl From> for WidgetText { #[inline] fn from(text: Box) -> Self { - Self::RichText(RichText::new(text)) + Self::Text(text.into()) } } impl From> for WidgetText { #[inline] fn from(text: Cow<'_, str>) -> Self { - Self::RichText(RichText::new(text)) + Self::Text(text.into_owned()) } } impl From for WidgetText { #[inline] fn from(rich_text: RichText) -> Self { + Self::RichText(Arc::new(rich_text)) + } +} + +impl From> for WidgetText { + #[inline] + fn from(rich_text: Arc) -> Self { Self::RichText(rich_text) } } @@ -787,6 +822,13 @@ impl From for WidgetText { impl From for WidgetText { #[inline] fn from(layout_job: LayoutJob) -> Self { + Self::LayoutJob(Arc::new(layout_job)) + } +} + +impl From> for WidgetText { + #[inline] + fn from(layout_job: Arc) -> Self { Self::LayoutJob(layout_job) } } @@ -797,3 +839,13 @@ impl From> for WidgetText { Self::Galley(galley) } } + +#[cfg(test)] +mod tests { + use crate::WidgetText; + + #[test] + fn ensure_small_widget_text() { + assert_eq!(size_of::(), size_of::()); + } +} diff --git a/crates/egui/src/widgets/label.rs b/crates/egui/src/widgets/label.rs index 3656af92b..d90bdf963 100644 --- a/crates/egui/src/widgets/label.rs +++ b/crates/egui/src/widgets/label.rs @@ -1,12 +1,10 @@ use std::sync::Arc; use crate::{ - epaint, pos2, text_selection, Align, Direction, FontSelection, Galley, Pos2, Response, Sense, - Stroke, TextWrapMode, Ui, Widget, WidgetInfo, WidgetText, WidgetType, + epaint, pos2, text_selection::LabelSelectionState, Align, Direction, FontSelection, Galley, + Pos2, Response, Sense, Stroke, TextWrapMode, Ui, Widget, WidgetInfo, WidgetText, WidgetType, }; -use self::text_selection::LabelSelectionState; - /// Static text. /// /// Usually it is more convenient to use [`Ui::label`]. @@ -182,9 +180,11 @@ impl Label { } let valign = ui.text_valign(); - let mut layout_job = self - .text - .into_layout_job(ui.style(), FontSelection::Default, valign); + let mut layout_job = Arc::unwrap_or_clone(self.text.into_layout_job( + ui.style(), + FontSelection::Default, + valign, + )); let available_width = ui.available_width(); diff --git a/crates/egui_demo_app/src/wrap_app.rs b/crates/egui_demo_app/src/wrap_app.rs index ea5fbfaba..3775d93d2 100644 --- a/crates/egui_demo_app/src/wrap_app.rs +++ b/crates/egui_demo_app/src/wrap_app.rs @@ -134,7 +134,7 @@ impl std::fmt::Display for Anchor { impl From for egui::WidgetText { fn from(value: Anchor) -> Self { - Self::RichText(egui::RichText::new(value.to_string())) + Self::from(value.to_string()) } } diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 49ec29087..795b9c9f4 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -118,6 +118,21 @@ impl LayoutJob { } } + /// Break on `\n` + #[inline] + pub fn simple_format(text: String, format: TextFormat) -> Self { + Self { + sections: vec![LayoutSection { + leading_space: 0.0, + byte_range: 0..text.len(), + format, + }], + text, + break_on_newline: true, + ..Default::default() + } + } + /// Does not break on `\n`, but shows the replacement character instead. #[inline] pub fn simple_singleline(text: String, font_id: FontId, color: Color32) -> Self { From 5bb20f511e1e3bbaf7d0d16761fe8b6fa9c419a2 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 6 May 2025 17:40:18 +0200 Subject: [PATCH 078/129] Fix links and text selection in horizontal_wrapped layout (#6905) * Closes * [x] I have followed the instructions in the PR template This was broken in https://github.com/emilk/egui/pull/5411. Not sure if this is the best fix or if `PlacedRow::rect` should be updated, but I think it makes sense that PlacedRow::rect ignores leading space. --- crates/egui/src/text_selection/accesskit_text.rs | 2 +- crates/egui/src/widgets/label.rs | 4 +++- crates/epaint/src/text/text_layout_types.rs | 10 +++++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/crates/egui/src/text_selection/accesskit_text.rs b/crates/egui/src/text_selection/accesskit_text.rs index de193e3b0..e04a54d18 100644 --- a/crates/egui/src/text_selection/accesskit_text.rs +++ b/crates/egui/src/text_selection/accesskit_text.rs @@ -45,7 +45,7 @@ pub fn update_accesskit_for_text_widget( let row_id = parent_id.with(row_index); ctx.accesskit_node_builder(row_id, |builder| { builder.set_role(accesskit::Role::TextRun); - let rect = global_from_galley * row.rect(); + 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(), diff --git a/crates/egui/src/widgets/label.rs b/crates/egui/src/widgets/label.rs index d90bdf963..9f3606d12 100644 --- a/crates/egui/src/widgets/label.rs +++ b/crates/egui/src/widgets/label.rs @@ -216,7 +216,9 @@ impl Label { let pos = pos2(ui.max_rect().left(), ui.cursor().top()); assert!(!galley.rows.is_empty(), "Galleys are never empty"); // collect a response from many rows: - let rect = galley.rows[0].rect().translate(pos.to_vec2()); + let rect = galley.rows[0] + .rect_without_leading_space() + .translate(pos.to_vec2()); let mut response = ui.allocate_rect(rect, sense); for placed_row in galley.rows.iter().skip(1) { let rect = placed_row.rect().translate(pos.to_vec2()); diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 795b9c9f4..6b7863426 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -576,10 +576,18 @@ pub struct PlacedRow { impl PlacedRow { /// Logical bounding rectangle on font heights etc. - /// Use this when drawing a selection or similar! + /// + /// This ignores / includes the `LayoutSection::leading_space`. pub fn rect(&self) -> Rect { Rect::from_min_size(self.pos, self.row.size) } + + /// Same as [`Self::rect`] but excluding the `LayoutSection::leading_space`. + pub fn rect_without_leading_space(&self) -> Rect { + let x = self.glyphs.first().map_or(self.pos.x, |g| g.pos.x); + let size_x = self.size.x - x; + Rect::from_min_size(Pos2::new(x, self.pos.y), Vec2::new(size_x, self.size.y)) + } } impl std::ops::Deref for PlacedRow { From 7216d0e38633e3c30f6ed90b2577a86c56512765 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 6 May 2025 17:54:06 +0200 Subject: [PATCH 079/129] Use mimalloc for benchmarks (#7029) `mimalloc` is a _much_ faster allocator, especially important when doing a lot of small allocations (which egui does). We use `mimalloc` in Rerun, and I recommend everyone to use it. ## The difference it makes ![image](https://github.com/user-attachments/assets/b22e0025-bc5e-4b3c-94e0-74ce46e86f85) --- Cargo.lock | 22 ++++++++++++++++++++++ Cargo.toml | 1 + crates/egui/src/lib.rs | 3 +++ crates/egui_demo_app/Cargo.toml | 1 + crates/egui_demo_app/src/main.rs | 3 +++ crates/egui_demo_lib/Cargo.toml | 3 ++- crates/egui_demo_lib/benches/benchmark.rs | 3 +++ crates/epaint/Cargo.toml | 1 + crates/epaint/benches/benchmark.rs | 3 +++ 9 files changed, 39 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 35ff4c9af..8354ad599 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1336,6 +1336,7 @@ dependencies = [ "env_logger", "image", "log", + "mimalloc", "poll-promise", "profiling", "puffin", @@ -1358,6 +1359,7 @@ dependencies = [ "egui", "egui_extras", "egui_kittest", + "mimalloc", "rand", "serde", "unicode_names2", @@ -1559,6 +1561,7 @@ dependencies = [ "emath", "epaint_default_fonts", "log", + "mimalloc", "nohash-hasher", "parking_lot", "profiling", @@ -2486,6 +2489,16 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +[[package]] +name = "libmimalloc-sys" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec9d6fac27761dabcd4ee73571cdb06b7022dc99089acbe5435691edffaac0f4" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "libredox" version = "0.1.3" @@ -2591,6 +2604,15 @@ dependencies = [ "paste", ] +[[package]] +name = "mimalloc" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "995942f432bbb4822a7e9c3faa87a695185b0d09273ba85f097b54f4e458f2af" +dependencies = [ + "libmimalloc-sys", +] + [[package]] name = "mime" version = "0.3.17" diff --git a/Cargo.toml b/Cargo.toml index 3b306176b..d3adbd90a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,6 +87,7 @@ home = "0.5.9" image = { version = "0.25", default-features = false } kittest = { version = "0.1.0", git = "https://github.com/rerun-io/kittest", branch = "main" } log = { version = "0.4", features = ["std"] } +mimalloc = "0.1.46" nohash-hasher = "0.2" parking_lot = "0.12" pollster = "0.4" diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index f8842a1cd..1025e7763 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -400,6 +400,9 @@ //! profile-with-puffin = ["profiling/profile-with-puffin"] //! ``` //! +//! ## Custom allocator +//! egui apps can run significantly (~20%) faster by using a custom allocator, like [mimalloc](https://crates.io/crates/mimalloc) or [talc](https://crates.io/crates/talc). +//! #![allow(clippy::float_cmp)] #![allow(clippy::manual_range_contains)] diff --git a/crates/egui_demo_app/Cargo.toml b/crates/egui_demo_app/Cargo.toml index b3eeb565d..5868ed481 100644 --- a/crates/egui_demo_app/Cargo.toml +++ b/crates/egui_demo_app/Cargo.toml @@ -86,6 +86,7 @@ env_logger = { version = "0.10", default-features = false, features = [ "auto-color", "humantime", ] } +mimalloc.workspace = true rfd = { version = "0.15.3", optional = true } # web: diff --git a/crates/egui_demo_app/src/main.rs b/crates/egui_demo_app/src/main.rs index 476ffdacd..cf391ee99 100644 --- a/crates/egui_demo_app/src/main.rs +++ b/crates/egui_demo_app/src/main.rs @@ -4,6 +4,9 @@ #![allow(rustdoc::missing_crate_level_docs)] // it's an example #![allow(clippy::never_loop)] // False positive +#[global_allocator] +static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; // Much faster allocator, can give 20% speedups: https://github.com/emilk/egui/pull/7029 + // When compiling natively: fn main() -> eframe::Result { for arg in std::env::args().skip(1) { diff --git a/crates/egui_demo_lib/Cargo.toml b/crates/egui_demo_lib/Cargo.toml index 77b8fdcb3..61b35f2e6 100644 --- a/crates/egui_demo_lib/Cargo.toml +++ b/crates/egui_demo_lib/Cargo.toml @@ -56,8 +56,9 @@ serde = { workspace = true, optional = true } [dev-dependencies] criterion.workspace = true -egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] } egui = { workspace = true, features = ["default_fonts"] } +egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] } +mimalloc.workspace = true # for benchmarks rand = "0.9" [[bench]] diff --git a/crates/egui_demo_lib/benches/benchmark.rs b/crates/egui_demo_lib/benches/benchmark.rs index 2b9a2cc05..b511f0de8 100644 --- a/crates/egui_demo_lib/benches/benchmark.rs +++ b/crates/egui_demo_lib/benches/benchmark.rs @@ -8,6 +8,9 @@ use egui::{Button, Id, RichText, TextureId, Ui, UiBuilder, Vec2}; use egui_demo_lib::LOREM_IPSUM_LONG; use rand::Rng as _; +#[global_allocator] +static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; // Much faster allocator + /// Each iteration should be called in their own `Ui` with an intentional id clash, /// to prevent the Context from building a massive map of `WidgetRects` (which would slow the test, /// causing unreliable results). diff --git a/crates/epaint/Cargo.toml b/crates/epaint/Cargo.toml index b8b006d44..0dccd2256 100644 --- a/crates/epaint/Cargo.toml +++ b/crates/epaint/Cargo.toml @@ -101,6 +101,7 @@ backtrace = { workspace = true, optional = true } [dev-dependencies] criterion.workspace = true +mimalloc.workspace = true similar-asserts.workspace = true diff --git a/crates/epaint/benches/benchmark.rs b/crates/epaint/benches/benchmark.rs index 444f81a11..14f4d2fa7 100644 --- a/crates/epaint/benches/benchmark.rs +++ b/crates/epaint/benches/benchmark.rs @@ -5,6 +5,9 @@ use epaint::{ TessellationOptions, Tessellator, TextureAtlas, Vec2, }; +#[global_allocator] +static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; // Much faster allocator + fn single_dashed_lines(c: &mut Criterion) { c.bench_function("single_dashed_lines", move |b| { b.iter(|| { From d0876a1a60fa79883a7fd047fed7ddb9ad2791c7 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 8 May 2025 09:15:42 +0200 Subject: [PATCH 080/129] Rename `master` branch to `main` (#7034) For consistency with other repositories, i.e. so I can write `git checkout main` without worrying which repo I'm browsing. --- .github/ISSUE_TEMPLATE/bug_report.md | 6 +-- .github/pull_request_template.md | 2 +- .github/workflows/deploy_web_demo.yml | 4 +- .github/workflows/png_only_on_lfs.yml | 2 +- CHANGELOG.md | 4 +- README.md | 42 +++++++++---------- RELEASES.md | 6 +-- crates/eframe/Cargo.toml | 8 ++-- crates/eframe/README.md | 8 ++-- crates/eframe/src/epi.rs | 2 +- crates/eframe/src/lib.rs | 2 +- crates/egui-wgpu/Cargo.toml | 4 +- crates/egui-wgpu/src/renderer.rs | 2 +- crates/egui-winit/Cargo.toml | 4 +- crates/egui/examples/README.md | 4 +- crates/egui/src/context.rs | 2 +- crates/egui/src/drag_and_drop.rs | 2 +- crates/egui/src/lib.rs | 8 ++-- crates/egui/src/viewport.rs | 2 +- crates/egui_demo_app/README.md | 4 +- crates/egui_demo_app/src/apps/http_app.rs | 4 +- crates/egui_demo_app/src/backend_panel.rs | 2 +- crates/egui_demo_lib/Cargo.toml | 4 +- crates/egui_demo_lib/src/demo/password.rs | 2 +- .../egui_demo_lib/src/demo/toggle_switch.rs | 2 +- .../src/easy_mark/easy_mark_editor.rs | 2 +- crates/egui_demo_lib/src/lib.rs | 4 +- crates/egui_glow/Cargo.toml | 4 +- crates/egui_glow/README.md | 4 +- crates/egui_glow/src/painter.rs | 2 +- crates/egui_kittest/src/snapshot.rs | 2 +- crates/egui_web/README.md | 2 +- crates/emath/Cargo.toml | 4 +- crates/epaint/Cargo.toml | 4 +- crates/epaint_default_fonts/Cargo.toml | 4 +- examples/README.md | 4 +- web_demo/README.md | 2 +- 37 files changed, 85 insertions(+), 85 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 66c17862f..e7c1d6bc3 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -10,13 +10,13 @@ assignees: '' **Describe the bug** diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 58fcd32ad..fb9926e3a 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,5 +1,5 @@ --- crates/eframe/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/eframe/src/lib.rs b/crates/eframe/src/lib.rs index 78208eb53..f0f392256 100644 --- a/crates/eframe/src/lib.rs +++ b/crates/eframe/src/lib.rs @@ -209,7 +209,7 @@ pub mod icon_data; /// This is how you start a native (desktop) app. /// -/// The first argument is name of your app, which is a an identifier +/// The first argument is name of your app, which is an identifier /// used for the save location of persistence (see [`App::save`]). /// It is also used as the application id on wayland. /// If you set no title on the viewport, the app id will be used From 417fdb1a43c7a1118126527f33825dc8162f0ed6 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 3 Jun 2025 07:59:02 -0700 Subject: [PATCH 103/129] Fix typo in changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aeab0c72a..09faecc52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -326,7 +326,7 @@ There also has been several small improvements to the look of egui: * The `extra_asserts` and `extra_debug_asserts` feature flags have been removed ([#4478](https://github.com/emilk/egui/pull/4478)) * Remove `Event::Scroll` and handle it in egui. Use `Event::MouseWheel` instead ([#4524](https://github.com/emilk/egui/pull/4524)) * `Event::Zoom` is no longer emitted on ctrl+scroll. Use `InputState::smooth_scroll_delta` instead ([#4524](https://github.com/emilk/egui/pull/4524)) -* `ui.set_enabled` and `set_visbile` have been deprecated ([#4614](https://github.com/emilk/egui/pull/4614)) +* `ui.set_enabled` and `set_visible` have been deprecated ([#4614](https://github.com/emilk/egui/pull/4614)) * `DragValue::clamp_range` renamed to `range` (([#4728](https://github.com/emilk/egui/pull/4728)) ### ⭐ Added From 6d04140736ddfbd1e406195de6804f57f1406321 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Wed, 4 Jun 2025 10:10:47 +0200 Subject: [PATCH 104/129] Fix update from ci script on linux (#7113) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apparently MacOS is case insensitive 😬 --- scripts/update_snapshots_from_ci.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/update_snapshots_from_ci.sh b/scripts/update_snapshots_from_ci.sh index 755399d1f..c42ffb0fd 100755 --- a/scripts/update_snapshots_from_ci.sh +++ b/scripts/update_snapshots_from_ci.sh @@ -2,6 +2,7 @@ # This script searches for the last CI run with your branch name, downloads the test_results artefact # and replaces your existing snapshots with the new ones. # Make sure you have the gh cli installed and authenticated before running this script. +# If prompted to select a default repo, choose the emilk/egui one set -eu @@ -9,7 +10,7 @@ BRANCH=$(git rev-parse --abbrev-ref HEAD) RUN_ID=$(gh run list --branch "$BRANCH" --workflow "Rust" --json databaseId -q '.[0].databaseId') -ECHO "Downloading test results from run $RUN_ID from branch $BRANCH" +echo "Downloading test results from run $RUN_ID from branch $BRANCH" # remove any existing .new.png that might have been left behind find . -type d -path "*/tests/snapshots*" | while read dir; do From 96816449364b052ae8aac82c7d74739efa32c5f7 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sat, 7 Jun 2025 18:25:19 +0200 Subject: [PATCH 105/129] Move all input-related options into `InputOptions` (#7121) --- crates/egui/src/context.rs | 2 +- crates/egui/src/input_state/mod.rs | 106 +++++++++++++++++++---------- crates/egui/src/memory/mod.rs | 37 ---------- 3 files changed, 72 insertions(+), 73 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index b567493ac..4ec5e140a 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -491,7 +491,7 @@ impl ContextImpl { new_raw_input, viewport.repaint.requested_immediate_repaint_prev_pass(), pixels_per_point, - &self.memory.options, + self.memory.options.input_options, ); let screen_rect = viewport.input.screen_rect; diff --git a/crates/egui/src/input_state/mod.rs b/crates/egui/src/input_state/mod.rs index a5d6951ce..6b3760159 100644 --- a/crates/egui/src/input_state/mod.rs +++ b/crates/egui/src/input_state/mod.rs @@ -18,9 +18,15 @@ pub use touch_state::MultiTouchInfo; use touch_state::TouchState; /// Options for input state handling. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct InputOptions { + /// Multiplier for the scroll speed when reported in [`crate::MouseWheelUnit::Line`]s. + pub line_scroll_speed: f32, + + /// Controls the speed at which we zoom in when doing ctrl/cmd + scroll. + pub scroll_zoom_speed: f32, + /// After a pointer-down event, if the pointer moves more than this, it won't become a click. pub max_click_dist: f32, @@ -39,7 +45,17 @@ pub struct InputOptions { impl Default for InputOptions { fn default() -> Self { + // TODO(emilk): figure out why these constants need to be different on web and on native (winit). + let is_web = cfg!(target_arch = "wasm32"); + let line_scroll_speed = if is_web { + 8.0 + } else { + 40.0 // Scroll speed decided by consensus: https://github.com/emilk/egui/issues/461 + }; + Self { + line_scroll_speed, + scroll_zoom_speed: 1.0 / 200.0, max_click_dist: 6.0, max_click_duration: 0.8, max_double_click_delay: 0.3, @@ -51,39 +67,59 @@ impl InputOptions { /// Show the options in the ui. pub fn ui(&mut self, ui: &mut crate::Ui) { let Self { + line_scroll_speed, + scroll_zoom_speed, max_click_dist, max_click_duration, max_double_click_delay, } = self; - crate::containers::CollapsingHeader::new("InputOptions") - .default_open(false) + crate::Grid::new("InputOptions") + .num_columns(2) + .striped(true) .show(ui, |ui| { - ui.horizontal(|ui| { - ui.label("Max click distance"); - ui.add( - crate::DragValue::new(max_click_dist) - .range(0.0..=f32::INFINITY) + ui.label("Line scroll speed"); + ui.add(crate::DragValue::new(line_scroll_speed).range(0.0..=f32::INFINITY)) + .on_hover_text( + "How many lines to scroll with each tick of the mouse wheel", + ); + ui.end_row(); + + ui.label("Scroll zoom speed"); + ui.add( + crate::DragValue::new(scroll_zoom_speed) + .range(0.0..=f32::INFINITY) + .speed(0.001), + ) + .on_hover_text("How fast to zoom with ctrl/cmd + scroll"); + ui.end_row(); + + ui.label("Max click distance"); + ui.add(crate::DragValue::new(max_click_dist).range(0.0..=f32::INFINITY)) + .on_hover_text( + "If the pointer moves more than this, it won't become a click", + ); + ui.end_row(); + + + ui.label("Max click duration"); + ui.add( + crate::DragValue::new(max_click_duration) + .range(0.1..=f64::INFINITY) + .speed(0.1), ) - .on_hover_text("If the pointer moves more than this, it won't become a click"); - }); - ui.horizontal(|ui| { - ui.label("Max click duration"); - ui.add( - crate::DragValue::new(max_click_duration) - .range(0.1..=f64::INFINITY) - .speed(0.1), - ) - .on_hover_text("If the pointer is down for longer than this it will no longer register as a click"); - }); - ui.horizontal(|ui| { - ui.label("Max double click delay"); - ui.add( - crate::DragValue::new(max_double_click_delay) - .range(0.01..=f64::INFINITY) - .speed(0.1), - ) - .on_hover_text("Max time interval for double click to count"); - }); + .on_hover_text( + "If the pointer is down for longer than this it will no longer register as a click", + ); + ui.end_row(); + + ui.label("Max double click delay"); + ui.add( + crate::DragValue::new(max_double_click_delay) + .range(0.01..=f64::INFINITY) + .speed(0.1), + ) + .on_hover_text("Max time interval for double click to count"); + ui.end_row(); }); } } @@ -267,7 +303,7 @@ impl InputState { mut new: RawInput, requested_immediate_repaint_prev_frame: bool, pixels_per_point: f32, - options: &crate::Options, + input_options: InputOptions, ) -> Self { profiling::function_scope!(); @@ -287,7 +323,7 @@ impl InputState { for touch_state in self.touch_states.values_mut() { touch_state.begin_pass(time, &new, self.pointer.interact_pos); } - let pointer = self.pointer.begin_pass(time, &new, options); + let pointer = self.pointer.begin_pass(time, &new, input_options); let mut keys_down = self.keys_down; let mut zoom_factor_delta = 1.0; // TODO(emilk): smoothing for zoom factor @@ -320,7 +356,7 @@ impl InputState { } => { let mut delta = match unit { MouseWheelUnit::Point => *delta, - MouseWheelUnit::Line => options.line_scroll_speed * *delta, + MouseWheelUnit::Line => input_options.line_scroll_speed * *delta, MouseWheelUnit::Page => screen_rect.height() * *delta, }; @@ -403,7 +439,7 @@ impl InputState { } zoom_factor_delta *= - (options.scroll_zoom_speed * smooth_scroll_delta_for_zoom).exp(); + (input_options.scroll_zoom_speed * smooth_scroll_delta_for_zoom).exp(); } } @@ -437,7 +473,7 @@ impl InputState { keys_down, events: new.events.clone(), // TODO(emilk): remove clone() and use raw.events raw: new, - input_options: options.input_options.clone(), + input_options, } } @@ -912,12 +948,12 @@ impl PointerState { mut self, time: f64, new: &RawInput, - options: &crate::Options, + input_options: InputOptions, ) -> Self { let was_decidedly_dragging = self.is_decidedly_dragging(); self.time = time; - self.input_options = options.input_options.clone(); + self.input_options = input_options; self.pointer_events.clear(); diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index 5bf1a1d8f..e9f7d9f86 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -284,14 +284,6 @@ pub struct Options { /// By default this is `true` in debug builds. pub warn_on_id_clash: bool, - // ------------------------------ - // Input: - /// Multiplier for the scroll speed when reported in [`crate::MouseWheelUnit::Line`]s. - pub line_scroll_speed: f32, - - /// Controls the speed at which we zoom in when doing ctrl/cmd + scroll. - pub scroll_zoom_speed: f32, - /// Options related to input state handling. pub input_options: crate::input_state::InputOptions, @@ -311,14 +303,6 @@ pub struct Options { impl Default for Options { fn default() -> Self { - // TODO(emilk): figure out why these constants need to be different on web and on native (winit). - let is_web = cfg!(target_arch = "wasm32"); - let line_scroll_speed = if is_web { - 8.0 - } else { - 40.0 // Scroll speed decided by consensus: https://github.com/emilk/egui/issues/461 - }; - Self { dark_style: std::sync::Arc::new(Theme::Dark.default_style()), light_style: std::sync::Arc::new(Theme::Light.default_style()), @@ -335,8 +319,6 @@ impl Default for Options { warn_on_id_clash: cfg!(debug_assertions), // Input: - line_scroll_speed, - scroll_zoom_speed: 1.0 / 200.0, input_options: Default::default(), reduce_texture_memory: false, } @@ -391,9 +373,6 @@ impl Options { screen_reader: _, // needs to come from the integration preload_font_glyphs: _, warn_on_id_clash, - - line_scroll_speed, - scroll_zoom_speed, input_options, reduce_texture_memory, } = self; @@ -448,22 +427,6 @@ impl Options { CollapsingHeader::new("šŸ–± Input") .default_open(false) .show(ui, |ui| { - ui.horizontal(|ui| { - ui.label("Line scroll speed"); - ui.add(crate::DragValue::new(line_scroll_speed).range(0.0..=f32::INFINITY)) - .on_hover_text( - "How many lines to scroll with each tick of the mouse wheel", - ); - }); - ui.horizontal(|ui| { - ui.label("Scroll zoom speed"); - ui.add( - crate::DragValue::new(scroll_zoom_speed) - .range(0.0..=f32::INFINITY) - .speed(0.001), - ) - .on_hover_text("How fast to zoom with ctrl/cmd + scroll"); - }); input_options.ui(ui); }); From cbd9c603997c46230cf3918699af389b7709b3cb Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sat, 7 Jun 2025 18:36:16 +0200 Subject: [PATCH 106/129] Add `Modifiers::matches_any` (#7123) * Part of https://github.com/emilk/egui/issues/7120 --- crates/egui/src/data/input.rs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/crates/egui/src/data/input.rs b/crates/egui/src/data/input.rs index f84bc78c2..00950addb 100644 --- a/crates/egui/src/data/input.rs +++ b/crates/egui/src/data/input.rs @@ -842,6 +842,37 @@ impl Modifiers { self.cmd_ctrl_matches(pattern) } + /// Check if any of the modifiers match exactly. + /// + /// Returns true if the same modifier is pressed in `self` as in `pattern`, + /// for at least one modifier. + /// + /// ## Behavior: + /// ``` + /// # use egui::Modifiers; + /// assert!(Modifiers::CTRL.matches_any(Modifiers::CTRL)); + /// assert!(Modifiers::CTRL.matches_any(Modifiers::CTRL | Modifiers::SHIFT)); + /// assert!((Modifiers::CTRL | Modifiers::SHIFT).matches_any(Modifiers::CTRL)); + /// ``` + pub fn matches_any(&self, pattern: Self) -> bool { + if self.alt && pattern.alt { + return true; + } + if self.shift && pattern.shift { + return true; + } + if self.ctrl && pattern.ctrl { + return true; + } + if self.mac_cmd && pattern.mac_cmd { + return true; + } + if (self.mac_cmd || self.command || self.ctrl) && pattern.command { + return true; + } + false + } + /// Checks only cmd/ctrl, not alt/shift. /// /// `self` here are the currently pressed modifiers, From 1d5b011793f5d5662006de7fd9a21bfe8c0a200a Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sat, 7 Jun 2025 18:36:23 +0200 Subject: [PATCH 107/129] Add `OperatingSystem::is_mac` (#7122) * Part of https://github.com/emilk/egui/issues/7120 --- crates/egui/src/os.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/egui/src/os.rs b/crates/egui/src/os.rs index e7497160b..283e33863 100644 --- a/crates/egui/src/os.rs +++ b/crates/egui/src/os.rs @@ -76,4 +76,9 @@ impl OperatingSystem { Self::Unknown } } + + /// Are we either macOS or iOS? + pub fn is_mac(&self) -> bool { + matches!(self, Self::Mac | Self::IOS) + } } From 53098fad7b71778dc51c6ce95a27ad222cc33e2b Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sat, 7 Jun 2025 19:18:13 +0200 Subject: [PATCH 108/129] Support vertical-only scrolling by holding down Alt (#7124) * Closes https://github.com/emilk/egui/issues/7120 You can now zoom only the X axis by holding down shift, and zoom only the Y axis by holding down ALT. In summary * `Shift`: horizontal * `Alt`: vertical * `Ctrl`: zoom (`Cmd` on Mac) Thus follows: * `scroll`: pan both axis (at least for trackpads and mice with two-axis scroll) * `Shift + scroll`: pan only horizontal axis * `Alt + scroll`: pan only vertical axis * `Ctrl + scroll`: zoom all axes * `Ctrl + Shift + scroll`: zoom only horizontal axis * `Ctrl + Alt + scroll`: zoom only vertical axis This is provided the application uses `zoom_delta_2d` for its zooming needs. The modifiers are exposed in `InputOptions`, but it is strongly recommended that you do not change them. ## Testing Unfortunately we have no nice way of testing this in egui. But I've tested it in `egui_plot`. --- crates/egui/src/data/input.rs | 6 + crates/egui/src/input_state/mod.rs | 135 +++++++++++++++------ crates/egui/src/input_state/touch_state.rs | 4 +- crates/egui/src/lib.rs | 2 +- 4 files changed, 106 insertions(+), 41 deletions(-) diff --git a/crates/egui/src/data/input.rs b/crates/egui/src/data/input.rs index 00950addb..2e35e34e7 100644 --- a/crates/egui/src/data/input.rs +++ b/crates/egui/src/data/input.rs @@ -986,6 +986,12 @@ impl std::ops::BitOrAssign for Modifiers { } } +impl Modifiers { + pub fn ui(&self, ui: &mut crate::Ui) { + ui.label(ModifierNames::NAMES.format(self, ui.ctx().os().is_mac())); + } +} + // ---------------------------------------------------------------------------- /// Names of different modifier keys. diff --git a/crates/egui/src/input_state/mod.rs b/crates/egui/src/input_state/mod.rs index 6b3760159..c871a8452 100644 --- a/crates/egui/src/input_state/mod.rs +++ b/crates/egui/src/input_state/mod.rs @@ -41,6 +41,23 @@ pub struct InputOptions { /// The new pointer press must come within this many seconds from previous pointer release /// for double click (or when this value is doubled, triple click) to count. pub max_double_click_delay: f64, + + /// When this modifier is down, all scroll events are treated as zoom events. + /// + /// The default is CTRL/CMD, and it is STRONGLY recommended to NOT change this. + pub zoom_modifier: Modifiers, + + /// When this modifier is down, all scroll events are treated as horizontal scrolls, + /// and when combined with [`Self::zoom_modifier`] it will result in zooming + /// on only the horizontal axis. + /// + /// The default is SHIFT, and it is STRONGLY recommended to NOT change this. + pub horizontal_scroll_modifier: Modifiers, + + /// When this modifier is down, all scroll events are treated as vertical scrolls, + /// and when combined with [`Self::zoom_modifier`] it will result in zooming + /// on only the vertical axis. + pub vertical_scroll_modifier: Modifiers, } impl Default for InputOptions { @@ -59,6 +76,9 @@ impl Default for InputOptions { max_click_dist: 6.0, max_click_duration: 0.8, max_double_click_delay: 0.3, + zoom_modifier: Modifiers::COMMAND, + horizontal_scroll_modifier: Modifiers::SHIFT, + vertical_scroll_modifier: Modifiers::ALT, } } } @@ -72,6 +92,9 @@ impl InputOptions { max_click_dist, max_click_duration, max_double_click_delay, + zoom_modifier, + horizontal_scroll_modifier, + vertical_scroll_modifier, } = self; crate::Grid::new("InputOptions") .num_columns(2) @@ -100,7 +123,6 @@ impl InputOptions { ); ui.end_row(); - ui.label("Max click duration"); ui.add( crate::DragValue::new(max_click_duration) @@ -120,6 +142,19 @@ impl InputOptions { ) .on_hover_text("Max time interval for double click to count"); ui.end_row(); + + ui.label("zoom_modifier"); + zoom_modifier.ui(ui); + ui.end_row(); + + ui.label("horizontal_scroll_modifier"); + horizontal_scroll_modifier.ui(ui); + ui.end_row(); + + ui.label("vertical_scroll_modifier"); + vertical_scroll_modifier.ui(ui); + ui.end_row(); + }); } } @@ -263,7 +298,7 @@ pub struct InputState { /// Input state management configuration. /// /// This gets copied from `egui::Options` at the start of each frame for convenience. - input_options: InputOptions, + options: InputOptions, } impl Default for InputState { @@ -291,7 +326,7 @@ impl Default for InputState { modifiers: Default::default(), keys_down: Default::default(), events: Default::default(), - input_options: Default::default(), + options: Default::default(), } } } @@ -303,7 +338,7 @@ impl InputState { mut new: RawInput, requested_immediate_repaint_prev_frame: bool, pixels_per_point: f32, - input_options: InputOptions, + options: InputOptions, ) -> Self { profiling::function_scope!(); @@ -323,7 +358,7 @@ impl InputState { for touch_state in self.touch_states.values_mut() { touch_state.begin_pass(time, &new, self.pointer.interact_pos); } - let pointer = self.pointer.begin_pass(time, &new, input_options); + let pointer = self.pointer.begin_pass(time, &new, options); let mut keys_down = self.keys_down; let mut zoom_factor_delta = 1.0; // TODO(emilk): smoothing for zoom factor @@ -356,15 +391,22 @@ impl InputState { } => { let mut delta = match unit { MouseWheelUnit::Point => *delta, - MouseWheelUnit::Line => input_options.line_scroll_speed * *delta, + MouseWheelUnit::Line => options.line_scroll_speed * *delta, MouseWheelUnit::Page => screen_rect.height() * *delta, }; - if modifiers.shift { - // Treat as horizontal scrolling. + let is_horizontal = modifiers.matches_any(options.horizontal_scroll_modifier); + let is_vertical = modifiers.matches_any(options.vertical_scroll_modifier); + + if is_horizontal && !is_vertical { + // Treat all scrolling as horizontal scrolling. // Note: one Mac we already get horizontal scroll events when shift is down. delta = vec2(delta.x + delta.y, 0.0); } + if !is_horizontal && is_vertical { + // Treat all scrolling as vertical scrolling. + delta = vec2(0.0, delta.x + delta.y); + } raw_scroll_delta += delta; @@ -378,14 +420,14 @@ impl InputState { MouseWheelUnit::Line | MouseWheelUnit::Page => false, }; - let is_zoom = modifiers.ctrl || modifiers.mac_cmd || modifiers.command; + let is_zoom = modifiers.matches_any(options.zoom_modifier); #[expect(clippy::collapsible_else_if)] if is_zoom { if is_smooth { - smooth_scroll_delta_for_zoom += delta.y; + smooth_scroll_delta_for_zoom += delta.x + delta.y; } else { - unprocessed_scroll_delta_for_zoom += delta.y; + unprocessed_scroll_delta_for_zoom += delta.x + delta.y; } } else { if is_smooth { @@ -439,7 +481,7 @@ impl InputState { } zoom_factor_delta *= - (input_options.scroll_zoom_speed * smooth_scroll_delta_for_zoom).exp(); + (options.scroll_zoom_speed * smooth_scroll_delta_for_zoom).exp(); } } @@ -473,7 +515,7 @@ impl InputState { keys_down, events: new.events.clone(), // TODO(emilk): remove clone() and use raw.events raw: new, - input_options, + options, } } @@ -488,10 +530,13 @@ impl InputState { self.screen_rect } - /// Zoom scale factor this frame (e.g. from ctrl-scroll or pinch gesture). + /// Uniform zoom scale factor this frame (e.g. from ctrl-scroll or pinch gesture). /// * `zoom = 1`: no change /// * `zoom < 1`: pinch together /// * `zoom > 1`: pinch spread + /// + /// If your application supports non-proportional zooming, + /// then you probably want to use [`Self::zoom_delta_2d`] instead. #[inline(always)] pub fn zoom_delta(&self) -> f32 { // If a multi touch gesture is detected, it measures the exact and linear proportions of @@ -521,10 +566,29 @@ impl InputState { // the distances of the finger tips. It is therefore potentially more accurate than // `zoom_factor_delta` which is based on the `ctrl-scroll` event which, in turn, may be // synthesized from an original touch gesture. - self.multi_touch().map_or_else( - || Vec2::splat(self.zoom_factor_delta), - |touch| touch.zoom_delta_2d, - ) + if let Some(multi_touch) = self.multi_touch() { + multi_touch.zoom_delta_2d + } else { + let mut zoom = Vec2::splat(self.zoom_factor_delta); + + let is_horizontal = self + .modifiers + .matches_any(self.options.horizontal_scroll_modifier); + let is_vertical = self + .modifiers + .matches_any(self.options.vertical_scroll_modifier); + + if is_horizontal && !is_vertical { + // Horizontal-only zooming. + zoom.y = 1.0; + } + if !is_horizontal && is_vertical { + // Vertical-only zooming. + zoom.x = 1.0; + } + + zoom + } } /// How long has it been (in seconds) since the use last scrolled? @@ -550,10 +614,10 @@ impl InputState { // We need to wake up and check for press-and-hold for the context menu. if let Some(press_start_time) = self.pointer.press_start_time { let press_duration = self.time - press_start_time; - if self.input_options.max_click_duration.is_finite() - && press_duration < self.input_options.max_click_duration + if self.options.max_click_duration.is_finite() + && press_duration < self.options.max_click_duration { - let secs_until_menu = self.input_options.max_click_duration - press_duration; + let secs_until_menu = self.options.max_click_duration - press_duration; return Some(Duration::from_secs_f64(secs_until_menu)); } } @@ -914,7 +978,7 @@ pub struct PointerState { /// Input state management configuration. /// /// This gets copied from `egui::Options` at the start of each frame for convenience. - input_options: InputOptions, + options: InputOptions, } impl Default for PointerState { @@ -937,23 +1001,18 @@ impl Default for PointerState { last_last_click_time: f64::NEG_INFINITY, last_move_time: f64::NEG_INFINITY, pointer_events: vec![], - input_options: Default::default(), + options: Default::default(), } } } impl PointerState { #[must_use] - pub(crate) fn begin_pass( - mut self, - time: f64, - new: &RawInput, - input_options: InputOptions, - ) -> Self { + pub(crate) fn begin_pass(mut self, time: f64, new: &RawInput, options: InputOptions) -> Self { let was_decidedly_dragging = self.is_decidedly_dragging(); self.time = time; - self.input_options = input_options; + self.options = options; self.pointer_events.clear(); @@ -974,7 +1033,7 @@ impl PointerState { if let Some(press_origin) = self.press_origin { self.has_moved_too_much_for_a_click |= - press_origin.distance(pos) > self.input_options.max_click_dist; + press_origin.distance(pos) > self.options.max_click_dist; } self.last_move_time = time; @@ -1013,10 +1072,10 @@ impl PointerState { let clicked = self.could_any_button_be_click(); let click = if clicked { - let double_click = (time - self.last_click_time) - < self.input_options.max_double_click_delay; + let double_click = + (time - self.last_click_time) < self.options.max_double_click_delay; let triple_click = (time - self.last_last_click_time) - < (self.input_options.max_double_click_delay * 2.0); + < (self.options.max_double_click_delay * 2.0); let count = if triple_click { 3 } else if double_click { @@ -1320,7 +1379,7 @@ impl PointerState { } if let Some(press_start_time) = self.press_start_time { - if self.time - press_start_time > self.input_options.max_click_duration { + if self.time - press_start_time > self.options.max_click_duration { return false; } } @@ -1356,7 +1415,7 @@ impl PointerState { && !self.has_moved_too_much_for_a_click && self.button_down(PointerButton::Primary) && self.press_start_time.is_some_and(|press_start_time| { - self.time - press_start_time > self.input_options.max_click_duration + self.time - press_start_time > self.options.max_click_duration }) } @@ -1416,7 +1475,7 @@ impl InputState { modifiers, keys_down, events, - input_options: _, + options: _, } = self; ui.style_mut() @@ -1502,7 +1561,7 @@ impl PointerState { last_last_click_time, pointer_events, last_move_time, - input_options: _, + options: _, } = self; ui.label(format!("latest_pos: {latest_pos:?}")); diff --git a/crates/egui/src/input_state/touch_state.rs b/crates/egui/src/input_state/touch_state.rs index 1ff2dc388..b4c789a8e 100644 --- a/crates/egui/src/input_state/touch_state.rs +++ b/crates/egui/src/input_state/touch_state.rs @@ -194,7 +194,7 @@ impl TouchState { let zoom_delta = state.current.avg_distance / state_previous.avg_distance; - let zoom_delta2 = match state.pinch_type { + let zoom_delta_2d = match state.pinch_type { PinchType::Horizontal => Vec2::new( state.current.avg_abs_distance2.x / state_previous.avg_abs_distance2.x, 1.0, @@ -213,7 +213,7 @@ impl TouchState { start_pos: state.start_pointer_pos, num_touches: self.active_touches.len(), zoom_delta, - zoom_delta_2d: zoom_delta2, + zoom_delta_2d, rotation_delta: normalized_angle(state.current.heading - state_previous.heading), translation_delta: state.current.avg_pos - state_previous.avg_pos, force: state.current.avg_force, diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 424654abd..6d42a230b 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -496,7 +496,7 @@ pub use self::{ epaint::text::TextWrapMode, grid::Grid, id::{Id, IdMap}, - input_state::{InputState, MultiTouchInfo, PointerState}, + input_state::{InputOptions, InputState, MultiTouchInfo, PointerState}, layers::{LayerId, Order}, layout::*, load::SizeHint, From 6e34152fa006c7fb7d6e5aa273ac46373dd33d97 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sat, 7 Jun 2025 19:22:16 +0200 Subject: [PATCH 109/129] Add `Context::format_modifiers` (#7125) Convenience --- crates/egui/src/context.rs | 68 ++++++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 28 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 4ec5e140a..751604d43 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -21,8 +21,7 @@ use crate::{ input_state::{InputState, MultiTouchInfo, PointerEvent}, interaction, layers::GraphicLayers, - load, - load::{Bytes, Loaders, SizedTexture}, + load::{self, Bytes, Loaders, SizedTexture}, memory::{Options, Theme}, os::OperatingSystem, output::FullOutput, @@ -32,10 +31,10 @@ use crate::{ viewport::ViewportClass, Align2, CursorIcon, DeferredViewportUiCallback, FontDefinitions, Grid, Id, ImmediateViewport, ImmediateViewportRendererCallback, Key, KeyboardShortcut, Label, LayerId, Memory, - ModifierNames, NumExt as _, Order, Painter, RawInput, Response, RichText, ScrollArea, Sense, - Style, TextStyle, TextureHandle, TextureOptions, Ui, ViewportBuilder, ViewportCommand, - ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportOutput, Widget as _, - WidgetRect, WidgetText, + ModifierNames, Modifiers, NumExt as _, Order, Painter, RawInput, Response, RichText, + ScrollArea, Sense, Style, TextStyle, TextureHandle, TextureOptions, Ui, ViewportBuilder, + ViewportCommand, ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportOutput, + Widget as _, WidgetRect, WidgetText, }; #[cfg(feature = "accesskit")] @@ -1484,35 +1483,48 @@ impl Context { self.send_cmd(crate::OutputCommand::CopyImage(image)); } + fn can_show_modifier_symbols(&self) -> bool { + let ModifierNames { + alt, + ctrl, + shift, + mac_cmd, + .. + } = ModifierNames::SYMBOLS; + + let font_id = TextStyle::Body.resolve(&self.style()); + self.fonts(|f| { + let mut lock = f.lock(); + let font = lock.fonts.font(&font_id); + font.has_glyphs(alt) + && font.has_glyphs(ctrl) + && font.has_glyphs(shift) + && font.has_glyphs(mac_cmd) + }) + } + + /// Format the given modifiers in a human-readable way (e.g. `Ctrl+Shift+X`). + pub fn format_modifiers(&self, modifiers: Modifiers) -> String { + let os = self.os(); + + let is_mac = os.is_mac(); + + if is_mac && self.can_show_modifier_symbols() { + ModifierNames::SYMBOLS.format(&modifiers, is_mac) + } else { + ModifierNames::NAMES.format(&modifiers, is_mac) + } + } + /// Format the given shortcut in a human-readable way (e.g. `Ctrl+Shift+X`). /// /// Can be used to get the text for [`crate::Button::shortcut_text`]. pub fn format_shortcut(&self, shortcut: &KeyboardShortcut) -> String { let os = self.os(); - let is_mac = matches!(os, OperatingSystem::Mac | OperatingSystem::IOS); + let is_mac = os.is_mac(); - let can_show_symbols = || { - let ModifierNames { - alt, - ctrl, - shift, - mac_cmd, - .. - } = ModifierNames::SYMBOLS; - - let font_id = TextStyle::Body.resolve(&self.style()); - self.fonts(|f| { - let mut lock = f.lock(); - let font = lock.fonts.font(&font_id); - font.has_glyphs(alt) - && font.has_glyphs(ctrl) - && font.has_glyphs(shift) - && font.has_glyphs(mac_cmd) - }) - }; - - if is_mac && can_show_symbols() { + if is_mac && self.can_show_modifier_symbols() { shortcut.format(&ModifierNames::SYMBOLS, is_mac) } else { shortcut.format(&ModifierNames::NAMES, is_mac) From 209e818bd8867f31354c6df0de1f1c98d9be6037 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sat, 7 Jun 2025 10:24:28 -0700 Subject: [PATCH 110/129] Improve deprecation message for old `egui::menu` --- crates/egui/src/menu.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/menu.rs b/crates/egui/src/menu.rs index 8de1fe951..9863f3941 100644 --- a/crates/egui/src/menu.rs +++ b/crates/egui/src/menu.rs @@ -1,5 +1,5 @@ #![allow(deprecated)] -//! Menu bar functionality (very basic so far). +//! Deprecated menu API - Use [`crate::containers::menu`] instead. //! //! Usage: //! ``` @@ -88,6 +88,7 @@ fn set_menu_style(style: &mut Style) { /// The menu bar goes well in a [`crate::TopBottomPanel::top`], /// but can also be placed in a [`crate::Window`]. /// In the latter case you may want to wrap it in [`Frame`]. +#[deprecated = "Use `crate::containers::menu::Bar` instead"] pub fn bar(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse { ui.horizontal(|ui| { set_menu_style(ui.style_mut()); From b8dfb138b692a3aa0371578eb368edaffae15618 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sat, 7 Jun 2025 10:24:41 -0700 Subject: [PATCH 111/129] Remove outdated link in README --- examples/serial_windows/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/serial_windows/README.md b/examples/serial_windows/README.md index 7543f95ca..9cf60b01f 100644 --- a/examples/serial_windows/README.md +++ b/examples/serial_windows/README.md @@ -7,8 +7,7 @@ Expected order of execution: - Similarly, when the second window is closed after a delay a third will be shown. - Once the third is closed the program will stop. -NOTE: this doesn't work on Mac due to . -See also . +NOTE: this doesn't work on Mac. See also . ```sh cargo run -p serial_windows From bdbe6558527899331440dd11024c07bcebb3c876 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sat, 7 Jun 2025 17:19:12 -0700 Subject: [PATCH 112/129] Mark HarnessBuilder build functions with #[must_use] --- crates/egui_kittest/src/builder.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/egui_kittest/src/builder.rs b/crates/egui_kittest/src/builder.rs index 63f19c3c6..3b10ba37a 100644 --- a/crates/egui_kittest/src/builder.rs +++ b/crates/egui_kittest/src/builder.rs @@ -205,6 +205,7 @@ impl HarnessBuilder { /// }); /// }); /// ``` + #[must_use] pub fn build<'a>(self, app: impl FnMut(&egui::Context) + 'a) -> Harness<'a> { Harness::from_builder(self, AppKind::Context(Box::new(app)), (), None) } @@ -224,6 +225,7 @@ impl HarnessBuilder { /// ui.label("Hello, world!"); /// }); /// ``` + #[must_use] pub fn build_ui<'a>(self, app: impl FnMut(&mut egui::Ui) + 'a) -> Harness<'a> { Harness::from_builder(self, AppKind::Ui(Box::new(app)), (), None) } From cfb10a04f5141fa10bbecce5dc9046207c3c13f7 Mon Sep 17 00:00:00 2001 From: Rinde van Lon Date: Wed, 11 Jun 2025 11:01:34 +0100 Subject: [PATCH 113/129] Improve `ComboBox` doc example (#7116) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Improves the `ComboBox` example with some code that shows how to handle changes in the `ComboBox`’s selection. The approach is based on the advice given in https://github.com/emilk/egui/discussions/923 . I hope this saves future me (and hopefully others) a web search for how to do this. * [x] I have followed the instructions in the PR template --- crates/egui/src/containers/combo_box.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/containers/combo_box.rs b/crates/egui/src/containers/combo_box.rs index ab89c4351..3ae8344ba 100644 --- a/crates/egui/src/containers/combo_box.rs +++ b/crates/egui/src/containers/combo_box.rs @@ -16,9 +16,10 @@ pub type IconPainter = Box; /// /// ``` /// # egui::__run_test_ui(|ui| { -/// # #[derive(Debug, PartialEq)] +/// # #[derive(Debug, PartialEq, Copy, Clone)] /// # enum Enum { First, Second, Third } /// # let mut selected = Enum::First; +/// let before = selected; /// egui::ComboBox::from_label("Select one!") /// .selected_text(format!("{:?}", selected)) /// .show_ui(ui, |ui| { @@ -27,6 +28,10 @@ pub type IconPainter = Box; /// ui.selectable_value(&mut selected, Enum::Third, "Third"); /// } /// ); +/// +/// if selected != before { +/// // Handle selection change +/// } /// # }); /// ``` #[must_use = "You should call .show*"] From 9f9153805d9d9a9863feef0b4e731c1495790c78 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Wed, 11 Jun 2025 17:38:06 +0200 Subject: [PATCH 114/129] lint: fix lints appearing in rust stable currently (#7118) * [x] I have followed the instructions in the PR template --- crates/eframe/src/native/file_storage.rs | 2 +- crates/egui-winit/src/lib.rs | 12 ++++++------ crates/egui/src/context.rs | 12 ++++++------ crates/egui/src/data/input.rs | 4 +--- crates/egui/src/data/output.rs | 2 +- crates/egui/src/memory/mod.rs | 8 ++++---- crates/egui/src/widgets/text_edit/builder.rs | 8 +++++--- crates/egui_demo_lib/src/demo/screenshot.rs | 2 +- crates/egui_demo_lib/src/demo/tests/grid_test.rs | 2 +- 9 files changed, 26 insertions(+), 26 deletions(-) diff --git a/crates/eframe/src/native/file_storage.rs b/crates/eframe/src/native/file_storage.rs index e1fdc9b84..5a3b1af32 100644 --- a/crates/eframe/src/native/file_storage.rs +++ b/crates/eframe/src/native/file_storage.rs @@ -45,7 +45,7 @@ pub fn storage_dir(app_id: &str) -> Option { #[expect(unsafe_code)] fn roaming_appdata() -> Option { use std::ffi::OsString; - use std::os::windows::ffi::OsStringExt; + use std::os::windows::ffi::OsStringExt as _; use std::ptr; use std::slice; diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 03ec3e831..c196acaa9 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -1848,8 +1848,8 @@ pub fn short_device_event_description(event: &winit::event::DeviceEvent) -> &'st use winit::event::DeviceEvent; match event { - DeviceEvent::Added { .. } => "DeviceEvent::Added", - DeviceEvent::Removed { .. } => "DeviceEvent::Removed", + DeviceEvent::Added => "DeviceEvent::Added", + DeviceEvent::Removed => "DeviceEvent::Removed", DeviceEvent::MouseMotion { .. } => "DeviceEvent::MouseMotion", DeviceEvent::MouseWheel { .. } => "DeviceEvent::MouseWheel", DeviceEvent::Motion { .. } => "DeviceEvent::Motion", @@ -1867,11 +1867,11 @@ pub fn short_window_event_description(event: &winit::event::WindowEvent) -> &'st WindowEvent::ActivationTokenDone { .. } => "WindowEvent::ActivationTokenDone", WindowEvent::Resized { .. } => "WindowEvent::Resized", WindowEvent::Moved { .. } => "WindowEvent::Moved", - WindowEvent::CloseRequested { .. } => "WindowEvent::CloseRequested", - WindowEvent::Destroyed { .. } => "WindowEvent::Destroyed", + WindowEvent::CloseRequested => "WindowEvent::CloseRequested", + WindowEvent::Destroyed => "WindowEvent::Destroyed", WindowEvent::DroppedFile { .. } => "WindowEvent::DroppedFile", WindowEvent::HoveredFile { .. } => "WindowEvent::HoveredFile", - WindowEvent::HoveredFileCancelled { .. } => "WindowEvent::HoveredFileCancelled", + WindowEvent::HoveredFileCancelled => "WindowEvent::HoveredFileCancelled", WindowEvent::Focused { .. } => "WindowEvent::Focused", WindowEvent::KeyboardInput { .. } => "WindowEvent::KeyboardInput", WindowEvent::ModifiersChanged { .. } => "WindowEvent::ModifiersChanged", @@ -1882,7 +1882,7 @@ pub fn short_window_event_description(event: &winit::event::WindowEvent) -> &'st WindowEvent::MouseWheel { .. } => "WindowEvent::MouseWheel", WindowEvent::MouseInput { .. } => "WindowEvent::MouseInput", WindowEvent::PinchGesture { .. } => "WindowEvent::PinchGesture", - WindowEvent::RedrawRequested { .. } => "WindowEvent::RedrawRequested", + WindowEvent::RedrawRequested => "WindowEvent::RedrawRequested", WindowEvent::DoubleTapGesture { .. } => "WindowEvent::DoubleTapGesture", WindowEvent::RotationGesture { .. } => "WindowEvent::RotationGesture", WindowEvent::TouchpadPressure { .. } => "WindowEvent::TouchpadPressure", diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 751604d43..3204927b8 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -3511,9 +3511,10 @@ impl Context { // Try most recently added loaders first (hence `.rev()`) for loader in bytes_loaders.iter().rev() { - match loader.load(self, uri) { - Err(load::LoadError::NotSupported) => continue, - result => return result, + let result = loader.load(self, uri); + match result { + Err(load::LoadError::NotSupported) => {} + _ => return result, } } @@ -3554,10 +3555,9 @@ impl Context { // Try most recently added loaders first (hence `.rev()`) for loader in image_loaders.iter().rev() { match loader.load(self, uri, size_hint) { - Err(load::LoadError::NotSupported) => continue, + Err(load::LoadError::NotSupported) => {} Err(load::LoadError::FormatNotSupported { detected_format }) => { format = format.or(detected_format); - continue; } result => return result, } @@ -3600,7 +3600,7 @@ impl Context { // Try most recently added loaders first (hence `.rev()`) for loader in texture_loaders.iter().rev() { match loader.load(self, uri, texture_options, size_hint) { - Err(load::LoadError::NotSupported) => continue, + Err(load::LoadError::NotSupported) => {} result => return result, } } diff --git a/crates/egui/src/data/input.rs b/crates/egui/src/data/input.rs index 2e35e34e7..1e2580678 100644 --- a/crates/egui/src/data/input.rs +++ b/crates/egui/src/data/input.rs @@ -256,9 +256,7 @@ impl ViewportInfo { /// If this is not the root viewport, /// it is up to the user to hide this viewport the next frame. pub fn close_requested(&self) -> bool { - self.events - .iter() - .any(|&event| event == ViewportEvent::Close) + self.events.contains(&ViewportEvent::Close) } /// Helper: move [`Self::events`], clone the other fields. diff --git a/crates/egui/src/data/output.rs b/crates/egui/src/data/output.rs index ab9044cb6..233f75a42 100644 --- a/crates/egui/src/data/output.rs +++ b/crates/egui/src/data/output.rs @@ -739,7 +739,7 @@ impl WidgetInfo { if text_value.is_empty() { "blank".into() } else { - text_value.to_string() + text_value.clone() } } else { "blank".into() diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index e9f7d9f86..9bf482b83 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -1235,7 +1235,7 @@ impl Areas { self.visible_areas_current_frame.insert(layer_id); self.wants_to_be_on_top.insert(layer_id); - if !self.order.iter().any(|x| *x == layer_id) { + if !self.order.contains(&layer_id) { self.order.push(layer_id); } } @@ -1256,10 +1256,10 @@ impl Areas { self.sublayers.entry(parent).or_default().insert(child); // Make sure the layers are in the order list: - if !self.order.iter().any(|x| *x == parent) { + if !self.order.contains(&parent) { self.order.push(parent); } - if !self.order.iter().any(|x| *x == child) { + if !self.order.contains(&child) { self.order.push(child); } } @@ -1268,7 +1268,7 @@ impl Areas { self.order .iter() .filter(|layer| layer.order == order && !self.is_sublayer(layer)) - .last() + .next_back() .copied() } diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index ca201edda..29f4b2cbb 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -863,9 +863,11 @@ impl TextEdit<'_> { fn mask_if_password(is_password: bool, text: &str) -> String { fn mask_password(text: &str) -> String { - std::iter::repeat(epaint::text::PASSWORD_REPLACEMENT_CHAR) - .take(text.chars().count()) - .collect::() + std::iter::repeat_n( + epaint::text::PASSWORD_REPLACEMENT_CHAR, + text.chars().count(), + ) + .collect::() } if is_password { diff --git a/crates/egui_demo_lib/src/demo/screenshot.rs b/crates/egui_demo_lib/src/demo/screenshot.rs index c2367914b..64370ec07 100644 --- a/crates/egui_demo_lib/src/demo/screenshot.rs +++ b/crates/egui_demo_lib/src/demo/screenshot.rs @@ -58,7 +58,7 @@ impl crate::View for Screenshot { None } }) - .last() + .next_back() }); if let Some(image) = image { diff --git a/crates/egui_demo_lib/src/demo/tests/grid_test.rs b/crates/egui_demo_lib/src/demo/tests/grid_test.rs index 511aa428f..0b7bfa485 100644 --- a/crates/egui_demo_lib/src/demo/tests/grid_test.rs +++ b/crates/egui_demo_lib/src/demo/tests/grid_test.rs @@ -110,7 +110,7 @@ impl crate::View for GridTest { ui.end_row(); let mut dyn_text = String::from("O"); - dyn_text.extend(std::iter::repeat('h').take(self.text_length)); + dyn_text.extend(std::iter::repeat_n('h', self.text_length)); ui.label(dyn_text); ui.label("Fifth row, second column"); ui.end_row(); From f0abce9bb8591466ce01f741eced5b271d3a4dc1 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 11 Jun 2025 23:00:59 +0200 Subject: [PATCH 115/129] `Button` inherits the `alt_text` of the `Image` in it, if any (#7136) If a `Button` has an `Image` in it (and no text), then the `Image::alt_text` will be used as the accessibility label for the button. --- crates/egui/src/widgets/button.rs | 10 +++++++--- crates/egui/src/widgets/image.rs | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index a75997eff..9ae573aef 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -314,11 +314,15 @@ impl Widget for Button<'_> { let (rect, mut response) = ui.allocate_at_least(desired_size, sense); response.widget_info(|| { + let mut widget_info = WidgetInfo::new(WidgetType::Button); + widget_info.enabled = ui.is_enabled(); + if let Some(galley) = &galley { - WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), galley.text()) - } else { - WidgetInfo::new(WidgetType::Button) + widget_info.label = Some(galley.text().to_owned()); + } else if let Some(image) = &image { + widget_info.label = image.alt_text.clone(); } + widget_info }); if ui.is_rect_visible(rect) { diff --git a/crates/egui/src/widgets/image.rs b/crates/egui/src/widgets/image.rs index e0ff7d36a..07b08e53c 100644 --- a/crates/egui/src/widgets/image.rs +++ b/crates/egui/src/widgets/image.rs @@ -54,7 +54,7 @@ pub struct Image<'a> { sense: Sense, size: ImageSize, pub(crate) show_loading_spinner: Option, - alt_text: Option, + pub(crate) alt_text: Option, } impl<'a> Image<'a> { From 6eb7bb6e0814c609701240e38e668f1a0f6516a7 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Fri, 13 Jun 2025 09:39:52 +0200 Subject: [PATCH 116/129] Add `AtomLayout`, abstracing layouting within widgets (#5830) Today each widget does its own custom layout, which has some drawbacks: - not very flexible - you can add an `Image` to `Button` but it will always be shown on the left side - you can't add a `Image` to a e.g. a `SelectableLabel` - a lot of duplicated code This PR introduces `Atoms` and `AtomLayout` which abstracts over "widget content" and layout within widgets, so it'd be possible to add images / text / custom rendering (for e.g. the checkbox) to any widget. A simple custom button implementation is now as easy as this: ```rs pub struct ALButton<'a> { al: AtomicLayout<'a>, } impl<'a> ALButton<'a> { pub fn new(content: impl IntoAtomics) -> Self { Self { al: content.into_atomics() } } } impl<'a> Widget for ALButton<'a> { fn ui(mut self, ui: &mut Ui) -> Response { let response = ui.ctx().read_response(ui.next_auto_id()); let visuals = response.map_or(&ui.style().visuals.widgets.inactive, |response| { ui.style().interact(&response) }); self.al.frame = self .al .frame .inner_margin(ui.style().spacing.button_padding) .fill(visuals.bg_fill) .stroke(visuals.bg_stroke) .corner_radius(visuals.corner_radius); self.al.show(ui) } } ``` The initial implementation only does very basic layout, just enough to be able to implement most current egui widgets, so: - only horizontal layout - everything is centered - a single item may grow/shrink based on the available space - everything can be contained in a Frame There is a trait `IntoAtoms` that conveniently allows you to construct `Atoms` from a tuple ``` ui.button((Image::new("image.png"), "Click me!")) ``` to get a button with image and text. This PR reimplements three egui widgets based on the new AtomLayout: - Button - matches the old button pixel-by-pixel - Button with image is now [properly aligned](https://github.com/emilk/egui/pull/5830/files#diff-962ce2c68ab50724b01c6b64c683c4067edd9b79fcdcb39a6071021e33ebe772) in justified layouts - selected button style now matches SelecatbleLabel look - For some reason the DragValue text seems shifted by a pixel almost everywhere, but I think it's more centered now, yay? - Checkbox - basically pixel-perfect but apparently the check mesh is very slightly different so I had to update the snapshot - somehow needs a bit more space in some snapshot tests? - RadioButton - pixel-perfect - somehow needs a bit more space in some snapshot tests? I plan on updating TextEdit based on AtomLayout in a separate PR (so you could use it to add a icon within the textedit frame). --- Cargo.lock | 1 + Cargo.toml | 1 + crates/egui/Cargo.toml | 1 + crates/egui/src/atomics/atom.rs | 109 ++++ crates/egui/src/atomics/atom_ext.rs | 107 ++++ crates/egui/src/atomics/atom_kind.rs | 120 +++++ crates/egui/src/atomics/atom_layout.rs | 493 ++++++++++++++++++ crates/egui/src/atomics/atoms.rs | 220 ++++++++ crates/egui/src/atomics/mod.rs | 15 + crates/egui/src/atomics/sized_atom.rs | 26 + crates/egui/src/atomics/sized_atom_kind.rs | 25 + crates/egui/src/containers/menu.rs | 12 +- crates/egui/src/lib.rs | 2 + crates/egui/src/ui.rs | 34 +- crates/egui/src/widget_text.rs | 16 +- crates/egui/src/widgets/button.rs | 356 ++++--------- crates/egui/src/widgets/checkbox.rs | 120 +++-- crates/egui/src/widgets/radio_button.rs | 97 ++-- .../tests/snapshots/imageviewer.png | 4 +- .../egui_demo_lib/src/demo/widget_gallery.rs | 5 +- .../tests/snapshots/demos/Grid Test.png | 2 +- .../tests/snapshots/demos/Layout Test.png | 4 +- .../tests/snapshots/demos/Scene.png | 4 +- .../tests/snapshots/demos/Scrolling.png | 2 +- .../tests/snapshots/demos/Sliders.png | 4 +- .../tests/snapshots/demos/Table.png | 4 +- .../tests/snapshots/widget_gallery.png | 4 +- crates/egui_extras/src/syntax_highlighting.rs | 2 +- crates/egui_kittest/tests/menu.rs | 12 +- tests/egui_tests/tests/snapshots/grow_all.png | 3 + .../tests/snapshots/layout/atoms_image.png | 3 + .../tests/snapshots/layout/atoms_minimal.png | 3 + .../snapshots/layout/atoms_multi_grow.png | 3 + .../tests/snapshots/layout/button_image.png | 4 +- .../snapshots/layout/checkbox_checked.png | 4 +- .../egui_tests/tests/snapshots/max_width.png | 3 + .../tests/snapshots/max_width_and_grow.png | 3 + .../tests/snapshots/shrink_first_text.png | 3 + .../tests/snapshots/shrink_last_text.png | 3 + .../tests/snapshots/size_max_size.png | 3 + .../button_image_shortcut_selected.png | 4 +- tests/egui_tests/tests/test_atoms.rs | 71 +++ tests/egui_tests/tests/test_widgets.rs | 25 +- 43 files changed, 1528 insertions(+), 409 deletions(-) create mode 100644 crates/egui/src/atomics/atom.rs create mode 100644 crates/egui/src/atomics/atom_ext.rs create mode 100644 crates/egui/src/atomics/atom_kind.rs create mode 100644 crates/egui/src/atomics/atom_layout.rs create mode 100644 crates/egui/src/atomics/atoms.rs create mode 100644 crates/egui/src/atomics/mod.rs create mode 100644 crates/egui/src/atomics/sized_atom.rs create mode 100644 crates/egui/src/atomics/sized_atom_kind.rs create mode 100644 tests/egui_tests/tests/snapshots/grow_all.png create mode 100644 tests/egui_tests/tests/snapshots/layout/atoms_image.png create mode 100644 tests/egui_tests/tests/snapshots/layout/atoms_minimal.png create mode 100644 tests/egui_tests/tests/snapshots/layout/atoms_multi_grow.png create mode 100644 tests/egui_tests/tests/snapshots/max_width.png create mode 100644 tests/egui_tests/tests/snapshots/max_width_and_grow.png create mode 100644 tests/egui_tests/tests/snapshots/shrink_first_text.png create mode 100644 tests/egui_tests/tests/snapshots/shrink_last_text.png create mode 100644 tests/egui_tests/tests/snapshots/size_max_size.png create mode 100644 tests/egui_tests/tests/test_atoms.rs diff --git a/Cargo.lock b/Cargo.lock index 9d7c11a19..9a441cb30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1263,6 +1263,7 @@ dependencies = [ "profiling", "ron", "serde", + "smallvec", "unicode-segmentation", ] diff --git a/Cargo.toml b/Cargo.toml index d1efd4aee..bf5b27d30 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -98,6 +98,7 @@ raw-window-handle = "0.6.0" ron = "0.10.1" serde = { version = "1", features = ["derive"] } similar-asserts = "1.4.2" +smallvec = "1" thiserror = "1.0.37" type-map = "0.5.0" unicode-segmentation = "1.12.0" diff --git a/crates/egui/Cargo.toml b/crates/egui/Cargo.toml index e7641ef31..238a8d8ea 100644 --- a/crates/egui/Cargo.toml +++ b/crates/egui/Cargo.toml @@ -87,6 +87,7 @@ ahash.workspace = true bitflags.workspace = true nohash-hasher.workspace = true profiling.workspace = true +smallvec.workspace = true unicode-segmentation.workspace = true #! ### Optional dependencies diff --git a/crates/egui/src/atomics/atom.rs b/crates/egui/src/atomics/atom.rs new file mode 100644 index 000000000..4f4b5b750 --- /dev/null +++ b/crates/egui/src/atomics/atom.rs @@ -0,0 +1,109 @@ +use crate::{AtomKind, Id, SizedAtom, Ui}; +use emath::{NumExt as _, Vec2}; +use epaint::text::TextWrapMode; + +/// A low-level ui building block. +/// +/// Implements [`From`] for [`String`], [`str`], [`crate::Image`] and much more for convenience. +/// You can directly call the `atom_*` methods on anything that implements `Into`. +/// ``` +/// # use egui::{Image, emath::Vec2}; +/// use egui::AtomExt as _; +/// let string_atom = "Hello".atom_grow(true); +/// let image_atom = Image::new("some_image_url").atom_size(Vec2::splat(20.0)); +/// ``` +#[derive(Clone, Debug)] +pub struct Atom<'a> { + /// See [`crate::AtomExt::atom_size`] + pub size: Option, + + /// See [`crate::AtomExt::atom_max_size`] + pub max_size: Vec2, + + /// See [`crate::AtomExt::atom_grow`] + pub grow: bool, + + /// See [`crate::AtomExt::atom_shrink`] + pub shrink: bool, + + /// The atom type + pub kind: AtomKind<'a>, +} + +impl Default for Atom<'_> { + fn default() -> Self { + Atom { + size: None, + max_size: Vec2::INFINITY, + grow: false, + shrink: false, + kind: AtomKind::Empty, + } + } +} + +impl<'a> Atom<'a> { + /// Create an empty [`Atom`] marked as `grow`. + /// + /// This will expand in size, allowing all preceding atoms to be left-aligned, + /// and all following atoms to be right-aligned + pub fn grow() -> Self { + Atom { + grow: true, + ..Default::default() + } + } + + /// Create a [`AtomKind::Custom`] with a specific size. + pub fn custom(id: Id, size: impl Into) -> Self { + Atom { + size: Some(size.into()), + kind: AtomKind::Custom(id), + ..Default::default() + } + } + + /// Turn this into a [`SizedAtom`]. + pub fn into_sized( + self, + ui: &Ui, + mut available_size: Vec2, + mut wrap_mode: Option, + ) -> SizedAtom<'a> { + if !self.shrink && self.max_size.x.is_infinite() { + wrap_mode = Some(TextWrapMode::Extend); + } + available_size = available_size.at_most(self.max_size); + if let Some(size) = self.size { + available_size = available_size.at_most(size); + } + if self.max_size.x.is_finite() { + wrap_mode = Some(TextWrapMode::Truncate); + } + + let (preferred, kind) = self.kind.into_sized(ui, available_size, wrap_mode); + + let size = self + .size + .map_or_else(|| kind.size(), |s| s.at_most(self.max_size)); + + SizedAtom { + size, + preferred_size: preferred, + grow: self.grow, + kind, + } + } +} + +impl<'a, T> From for Atom<'a> +where + T: Into>, +{ + fn from(value: T) -> Self { + Atom { + kind: value.into(), + ..Default::default() + } + } +} diff --git a/crates/egui/src/atomics/atom_ext.rs b/crates/egui/src/atomics/atom_ext.rs new file mode 100644 index 000000000..0c34544d8 --- /dev/null +++ b/crates/egui/src/atomics/atom_ext.rs @@ -0,0 +1,107 @@ +use crate::{Atom, FontSelection, 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 atom to a fixed size. + /// + /// If [`Atom::grow`] is `true`, this will be the minimum width. + /// If [`Atom::shrink`] is `true`, this will be the maximum width. + /// If both are true, the width will have no effect. + /// + /// [`Self::atom_max_size`] will limit size. + /// + /// See [`crate::AtomKind`] docs to see how the size affects the different types. + fn atom_size(self, size: Vec2) -> Atom<'a>; + + /// Grow this atom to the available space. + /// + /// This will affect the size of the [`Atom`] in the main direction. Since + /// [`crate::AtomLayout`] today only supports horizontal layout, it will affect the width. + /// + /// You can also combine this with [`Self::atom_shrink`] to make it always take exactly the + /// remaining space. + fn atom_grow(self, grow: bool) -> Atom<'a>; + + /// Shrink this atom if there isn't enough space. + /// + /// This will affect the size of the [`Atom`] in the main direction. Since + /// [`crate::AtomLayout`] today only supports horizontal layout, it will affect the width. + /// + /// NOTE: Only a single [`Atom`] may shrink for each widget. + /// + /// If no atom was set to shrink and `wrap_mode != TextWrapMode::Extend`, the first + /// `AtomKind::Text` is set to shrink. + fn atom_shrink(self, shrink: bool) -> Atom<'a>; + + /// Set the maximum size of this atom. + /// + /// Will not affect the space taken by `grow` (All atoms marked as grow will always grow + /// equally to fill the available space). + fn atom_max_size(self, max_size: Vec2) -> Atom<'a>; + + /// Set the maximum width of this atom. + /// + /// Will not affect the space taken by `grow` (All atoms marked as grow will always grow + /// equally to fill the available space). + fn atom_max_width(self, max_width: f32) -> Atom<'a>; + + /// Set the maximum height of this atom. + fn atom_max_height(self, max_height: f32) -> Atom<'a>; + + /// Set the max height of this atom to match the font size. + /// + /// This is useful for e.g. limiting the height of icons in buttons. + fn atom_max_height_font_size(self, ui: &Ui) -> Atom<'a> + where + Self: Sized, + { + let font_selection = FontSelection::default(); + let font_id = font_selection.resolve(ui.style()); + let height = ui.fonts(|f| f.row_height(&font_id)); + self.atom_max_height(height) + } +} + +impl<'a, T> AtomExt<'a> for T +where + T: Into> + Sized, +{ + fn atom_size(self, size: Vec2) -> Atom<'a> { + let mut atom = self.into(); + atom.size = Some(size); + atom + } + + fn atom_grow(self, grow: bool) -> Atom<'a> { + let mut atom = self.into(); + atom.grow = grow; + atom + } + + fn atom_shrink(self, shrink: bool) -> Atom<'a> { + let mut atom = self.into(); + atom.shrink = shrink; + atom + } + + fn atom_max_size(self, max_size: Vec2) -> Atom<'a> { + let mut atom = self.into(); + atom.max_size = max_size; + atom + } + + fn atom_max_width(self, max_width: f32) -> Atom<'a> { + let mut atom = self.into(); + atom.max_size.x = max_width; + atom + } + + fn atom_max_height(self, max_height: f32) -> Atom<'a> { + let mut atom = self.into(); + atom.max_size.y = max_height; + atom + } +} diff --git a/crates/egui/src/atomics/atom_kind.rs b/crates/egui/src/atomics/atom_kind.rs new file mode 100644 index 000000000..2672e646b --- /dev/null +++ b/crates/egui/src/atomics/atom_kind.rs @@ -0,0 +1,120 @@ +use crate::{Id, Image, ImageSource, SizedAtomKind, TextStyle, Ui, WidgetText}; +use emath::Vec2; +use epaint::text::TextWrapMode; + +/// The different kinds of [`crate::Atom`]s. +#[derive(Clone, Default, Debug)] +pub enum AtomKind<'a> { + /// Empty, that can be used with [`crate::AtomExt::atom_grow`] to reserve space. + #[default] + Empty, + + /// Text atom. + /// + /// Truncation within [`crate::AtomLayout`] works like this: + /// - + /// - if `wrap_mode` is not Extend + /// - if no atom is `shrink` + /// - the first text atom is selected and will be marked as `shrink` + /// - the atom marked as `shrink` will shrink / wrap based on the selected wrap mode + /// - any other text atoms will have `wrap_mode` extend + /// - if `wrap_mode` is extend, Text will extend as expected. + /// + /// Unless [`crate::AtomExt::atom_max_width`] is set, `wrap_mode` should only be set via [`crate::Style`] or + /// [`crate::AtomLayout::wrap_mode`], as setting a wrap mode on a [`WidgetText`] atom + /// that is not `shrink` will have unexpected results. + /// + /// The size is determined by converting the [`WidgetText`] into a galley and using the galleys + /// size. You can use [`crate::AtomExt::atom_size`] to override this, and [`crate::AtomExt::atom_max_width`] + /// to limit the width (Causing the text to wrap or truncate, depending on the `wrap_mode`. + /// [`crate::AtomExt::atom_max_height`] has no effect on text. + Text(WidgetText), + + /// Image atom. + /// + /// By default the size is determined via [`Image::calc_size`]. + /// You can use [`crate::AtomExt::atom_max_size`] or [`crate::AtomExt::atom_size`] to customize the size. + /// There is also a helper [`crate::AtomExt::atom_max_height_font_size`] to set the max height to the + /// default font height, which is convenient for icons. + Image(Image<'a>), + + /// For custom rendering. + /// + /// You can get the [`crate::Rect`] with the [`Id`] from [`crate::AtomLayoutResponse`] and use a + /// [`crate::Painter`] or [`Ui::put`] to add/draw some custom content. + /// + /// 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.put(rect, Button::new("āµ")); + /// } + /// # }); + /// ``` + Custom(Id), +} + +impl<'a> AtomKind<'a> { + pub fn text(text: impl Into) -> Self { + AtomKind::Text(text.into()) + } + + pub fn image(image: impl Into>) -> Self { + AtomKind::Image(image.into()) + } + + /// Turn this [`AtomKind`] into a [`SizedAtomKind`]. + /// + /// This converts [`WidgetText`] into [`crate::Galley`] and tries to load and size [`Image`]. + /// The first returned argument is the preferred size. + pub fn into_sized( + self, + ui: &Ui, + available_size: Vec2, + wrap_mode: Option, + ) -> (Vec2, SizedAtomKind<'a>) { + match self { + AtomKind::Text(text) => { + let galley = text.into_galley(ui, wrap_mode, available_size.x, TextStyle::Button); + ( + galley.size(), // TODO(#5762): calculate the preferred size + 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)) + } + AtomKind::Custom(id) => (Vec2::ZERO, SizedAtomKind::Custom(id)), + AtomKind::Empty => (Vec2::ZERO, SizedAtomKind::Empty), + } + } +} + +impl<'a> From> for AtomKind<'a> { + fn from(value: ImageSource<'a>) -> Self { + AtomKind::Image(value.into()) + } +} + +impl<'a> From> for AtomKind<'a> { + fn from(value: Image<'a>) -> Self { + AtomKind::Image(value) + } +} + +impl From for AtomKind<'_> +where + T: Into, +{ + fn from(value: T) -> Self { + AtomKind::Text(value.into()) + } +} diff --git a/crates/egui/src/atomics/atom_layout.rs b/crates/egui/src/atomics/atom_layout.rs new file mode 100644 index 000000000..a25a4b7c6 --- /dev/null +++ b/crates/egui/src/atomics/atom_layout.rs @@ -0,0 +1,493 @@ +use crate::atomics::ATOMS_SMALL_VEC_SIZE; +use crate::{ + AtomKind, Atoms, Frame, Id, Image, IntoAtoms, Response, Sense, SizedAtom, SizedAtomKind, Ui, + Widget, +}; +use emath::{Align2, GuiRounding as _, NumExt as _, Rect, Vec2}; +use epaint::text::TextWrapMode; +use epaint::{Color32, Galley}; +use smallvec::SmallVec; +use std::ops::{Deref, DerefMut}; +use std::sync::Arc; + +/// Intra-widget layout utility. +/// +/// Used to lay out and paint [`crate::Atom`]s. +/// This is used internally by widgets like [`crate::Button`] and [`crate::Checkbox`]. +/// You can use it to make your own widgets. +/// +/// Painting the atoms can be split in two phases: +/// - [`AtomLayout::allocate`] +/// - calculates sizes +/// - converts texts to [`Galley`]s +/// - allocates a [`Response`] +/// - returns a [`AllocatedAtomLayout`] +/// - [`AllocatedAtomLayout::paint`] +/// - paints the [`Frame`] +/// - calculates individual [`crate::Atom`] positions +/// - paints each single atom +/// +/// You can use this to first allocate a response and then modify, e.g., the [`Frame`] on the +/// [`AllocatedAtomLayout`] for interaction styling. +pub struct AtomLayout<'a> { + id: Option, + pub atoms: Atoms<'a>, + gap: Option, + pub(crate) frame: Frame, + pub(crate) sense: Sense, + fallback_text_color: Option, + min_size: Vec2, + wrap_mode: Option, + align2: Option, +} + +impl Default for AtomLayout<'_> { + fn default() -> Self { + Self::new(()) + } +} + +impl<'a> AtomLayout<'a> { + pub fn new(atoms: impl IntoAtoms<'a>) -> Self { + Self { + id: None, + atoms: atoms.into_atoms(), + gap: None, + frame: Frame::default(), + sense: Sense::hover(), + fallback_text_color: None, + min_size: Vec2::ZERO, + wrap_mode: None, + align2: None, + } + } + + /// Set the gap between atoms. + /// + /// Default: `Spacing::icon_spacing` + #[inline] + pub fn gap(mut self, gap: f32) -> Self { + self.gap = Some(gap); + self + } + + /// Set the [`Frame`]. + #[inline] + pub fn frame(mut self, frame: Frame) -> Self { + self.frame = frame; + self + } + + /// Set the [`Sense`] used when allocating the [`Response`]. + #[inline] + pub fn sense(mut self, sense: Sense) -> Self { + self.sense = sense; + self + } + + /// Set the fallback (default) text color. + /// + /// Default: [`crate::Visuals::text_color`] + #[inline] + pub fn fallback_text_color(mut self, color: Color32) -> Self { + self.fallback_text_color = Some(color); + self + } + + /// Set the minimum size of the Widget. + /// + /// This will find and expand atoms with `grow: true`. + /// If there are no growable atoms then everything will be left-aligned. + #[inline] + pub fn min_size(mut self, size: Vec2) -> Self { + self.min_size = size; + self + } + + /// Set the [`Id`] used to allocate a [`Response`]. + #[inline] + pub fn id(mut self, id: Id) -> Self { + self.id = Some(id); + self + } + + /// Set the [`TextWrapMode`] for the [`crate::Atom`] marked as `shrink`. + /// + /// Only a single [`crate::Atom`] may shrink. If this (or `ui.wrap_mode()`) is not + /// [`TextWrapMode::Extend`] and no item is set to shrink, the first (left-most) + /// [`AtomKind::Text`] will be set to shrink. + #[inline] + pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self { + self.wrap_mode = Some(wrap_mode); + self + } + + /// Set the [`Align2`]. + /// + /// This will align the [`crate::Atom`]s within the [`Rect`] returned by [`Ui::allocate_space`]. + /// + /// The default is chosen based on the [`Ui`]s [`crate::Layout`]. See + /// [this snapshot](https://github.com/emilk/egui/blob/master/tests/egui_tests/tests/snapshots/layout/button.png) + /// for info on how the [`crate::Layout`] affects the alignment. + #[inline] + pub fn align2(mut self, align2: Align2) -> Self { + self.align2 = Some(align2); + self + } + + /// [`AtomLayout::allocate`] and [`AllocatedAtomLayout::paint`] in one go. + pub fn show(self, ui: &mut Ui) -> AtomLayoutResponse { + self.allocate(ui).paint(ui) + } + + /// Calculate sizes, create [`Galley`]s and allocate a [`Response`]. + /// + /// Use the returned [`AllocatedAtomLayout`] for painting. + pub fn allocate(self, ui: &mut Ui) -> AllocatedAtomLayout<'a> { + let Self { + id, + mut atoms, + gap, + frame, + sense, + fallback_text_color, + min_size, + wrap_mode, + align2, + } = self; + + let wrap_mode = wrap_mode.unwrap_or(ui.wrap_mode()); + + // 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); + if !any_shrink { + let first_text = atoms + .iter_mut() + .find(|a| matches!(a.kind, AtomKind::Text(..))); + if let Some(atom) = first_text { + atom.shrink = true; // Will make the text truncate or shrink depending on wrap_mode + } + } + } + + let id = id.unwrap_or_else(|| ui.next_auto_id()); + + let fallback_text_color = + fallback_text_color.unwrap_or_else(|| ui.style().visuals.text_color()); + let gap = gap.unwrap_or(ui.spacing().icon_spacing); + + // The size available for the content + let available_inner_size = ui.available_size() - frame.total_margin().sum(); + + let mut desired_width = 0.0; + + // Preferred width / height is the ideal size of the widget, e.g. the size where the + // text is not wrapped. Used to set Response::intrinsic_size. + let mut preferred_width = 0.0; + let mut preferred_height = 0.0; + + let mut height: f32 = 0.0; + + let mut sized_items = SmallVec::new(); + + let mut grow_count = 0; + + let mut shrink_item = None; + + let align2 = align2.unwrap_or_else(|| { + Align2([ui.layout().horizontal_align(), ui.layout().vertical_align()]) + }); + + if atoms.len() > 1 { + let gap_space = gap * (atoms.len() as f32 - 1.0); + desired_width += gap_space; + preferred_width += gap_space; + } + + for (idx, item) in atoms.into_iter().enumerate() { + if item.grow { + grow_count += 1; + } + if item.shrink { + debug_assert!( + shrink_item.is_none(), + "Only one atomic may be marked as shrink. {item:?}" + ); + if shrink_item.is_none() { + shrink_item = Some((idx, item)); + continue; + } + } + let sized = item.into_sized(ui, available_inner_size, Some(wrap_mode)); + let size = sized.size; + + desired_width += size.x; + preferred_width += sized.preferred_size.x; + + height = height.at_least(size.y); + preferred_height = preferred_height.at_least(sized.preferred_size.y); + + sized_items.push(sized); + } + + if let Some((index, item)) = shrink_item { + // The `shrink` item gets the remaining space + let available_size_for_shrink_item = Vec2::new( + available_inner_size.x - desired_width, + available_inner_size.y, + ); + + let sized = item.into_sized(ui, available_size_for_shrink_item, Some(wrap_mode)); + let size = sized.size; + + desired_width += size.x; + preferred_width += sized.preferred_size.x; + + height = height.at_least(size.y); + preferred_height = preferred_height.at_least(sized.preferred_size.y); + + sized_items.insert(index, sized); + } + + let margin = frame.total_margin(); + let desired_size = Vec2::new(desired_width, height); + let frame_size = (desired_size + margin.sum()).at_least(min_size); + + let (_, rect) = ui.allocate_space(frame_size); + let mut response = ui.interact(rect, id, sense); + + response.intrinsic_size = + Some((Vec2::new(preferred_width, preferred_height) + margin.sum()).at_least(min_size)); + + AllocatedAtomLayout { + sized_atoms: sized_items, + frame, + fallback_text_color, + response, + grow_count, + desired_size, + align2, + gap, + } + } +} + +/// Instructions for painting an [`AtomLayout`]. +#[derive(Clone, Debug)] +pub struct AllocatedAtomLayout<'a> { + pub sized_atoms: SmallVec<[SizedAtom<'a>; ATOMS_SMALL_VEC_SIZE]>, + pub frame: Frame, + pub fallback_text_color: Color32, + pub response: Response, + grow_count: usize, + // The size of the inner content, before any growing. + desired_size: Vec2, + align2: Align2, + gap: f32, +} + +impl<'atom> AllocatedAtomLayout<'atom> { + pub fn iter_kinds(&self) -> impl Iterator> { + self.sized_atoms.iter().map(|atom| &atom.kind) + } + + pub fn iter_kinds_mut(&mut self) -> impl Iterator> { + self.sized_atoms.iter_mut().map(|atom| &mut atom.kind) + } + + pub fn iter_images(&self) -> impl Iterator> { + self.iter_kinds().filter_map(|kind| { + if let SizedAtomKind::Image(image, _) = kind { + Some(image) + } else { + None + } + }) + } + + pub fn iter_images_mut(&mut self) -> impl Iterator> { + self.iter_kinds_mut().filter_map(|kind| { + if let SizedAtomKind::Image(image, _) = kind { + Some(image) + } else { + None + } + }) + } + + pub fn iter_texts(&self) -> impl Iterator> + use<'atom, '_> { + self.iter_kinds().filter_map(|kind| { + if let SizedAtomKind::Text(text) = kind { + Some(text) + } else { + None + } + }) + } + + pub fn iter_texts_mut(&mut self) -> impl Iterator> + use<'atom, '_> { + self.iter_kinds_mut().filter_map(|kind| { + if let SizedAtomKind::Text(text) = kind { + Some(text) + } else { + None + } + }) + } + + pub fn map_kind(&mut self, mut f: F) + where + F: FnMut(SizedAtomKind<'atom>) -> SizedAtomKind<'atom>, + { + for kind in self.iter_kinds_mut() { + *kind = f(std::mem::take(kind)); + } + } + + pub fn map_images(&mut self, mut f: F) + where + F: FnMut(Image<'atom>) -> Image<'atom>, + { + self.map_kind(|kind| { + if let SizedAtomKind::Image(image, size) = kind { + SizedAtomKind::Image(f(image), size) + } else { + kind + } + }); + } + + /// Paint the [`Frame`] and individual [`crate::Atom`]s. + pub fn paint(self, ui: &Ui) -> AtomLayoutResponse { + let Self { + sized_atoms, + frame, + fallback_text_color, + response, + grow_count, + desired_size, + align2, + gap, + } = self; + + let inner_rect = response.rect - self.frame.total_margin(); + + ui.painter().add(frame.paint(inner_rect)); + + let width_to_fill = inner_rect.width(); + let extra_space = f32::max(width_to_fill - desired_size.x, 0.0); + let grow_width = f32::max(extra_space / grow_count as f32, 0.0).floor_ui(); + + let aligned_rect = if grow_count > 0 { + align2.align_size_within_rect(Vec2::new(width_to_fill, desired_size.y), inner_rect) + } else { + align2.align_size_within_rect(desired_size, inner_rect) + }; + + let mut cursor = aligned_rect.left(); + + let mut response = AtomLayoutResponse::empty(response); + + for sized in sized_atoms { + let size = sized.size; + // TODO(lucasmerlin): This is not ideal, since this might lead to accumulated rounding errors + // https://github.com/emilk/egui/pull/5830#discussion_r2079627864 + let growth = if sized.is_grow() { grow_width } else { 0.0 }; + + let frame = aligned_rect + .with_min_x(cursor) + .with_max_x(cursor + size.x + growth); + cursor = frame.right() + gap; + + let align = Align2::CENTER_CENTER; + let rect = align.align_size_within_rect(size, frame); + + match sized.kind { + SizedAtomKind::Text(galley) => { + ui.painter().galley(rect.min, galley, fallback_text_color); + } + SizedAtomKind::Image(image, _) => { + 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 => {} + } + } + + response + } +} + +/// Response from a [`AtomLayout::show`] or [`AllocatedAtomLayout::paint`]. +/// +/// Use [`AtomLayoutResponse::rect`] to get the response rects from [`AtomKind::Custom`]. +#[derive(Clone, Debug)] +pub struct AtomLayoutResponse { + pub response: Response, + // There should rarely be more than one custom rect. + custom_rects: SmallVec<[(Id, Rect); 1]>, +} + +impl AtomLayoutResponse { + pub fn empty(response: Response) -> Self { + Self { + response, + custom_rects: Default::default(), + } + } + + pub fn custom_rects(&self) -> impl Iterator + '_ { + self.custom_rects.iter().copied() + } + + /// Use this together with [`AtomKind::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 { + self.custom_rects + .iter() + .find_map(|(i, r)| if *i == id { Some(*r) } else { None }) + } +} + +impl Widget for AtomLayout<'_> { + fn ui(self, ui: &mut Ui) -> Response { + self.show(ui).response + } +} + +impl<'a> Deref for AtomLayout<'a> { + type Target = Atoms<'a>; + + fn deref(&self) -> &Self::Target { + &self.atoms + } +} + +impl DerefMut for AtomLayout<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.atoms + } +} + +impl<'a> Deref for AllocatedAtomLayout<'a> { + type Target = [SizedAtom<'a>]; + + fn deref(&self) -> &Self::Target { + &self.sized_atoms + } +} + +impl DerefMut for AllocatedAtomLayout<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.sized_atoms + } +} diff --git a/crates/egui/src/atomics/atoms.rs b/crates/egui/src/atomics/atoms.rs new file mode 100644 index 000000000..3752ace70 --- /dev/null +++ b/crates/egui/src/atomics/atoms.rs @@ -0,0 +1,220 @@ +use crate::{Atom, AtomKind, Image, WidgetText}; +use smallvec::SmallVec; +use std::borrow::Cow; +use std::ops::{Deref, DerefMut}; + +// Rarely there should be more than 2 atoms in one Widget. +// I guess it could happen in a menu button with Image and right text... +pub(crate) const ATOMS_SMALL_VEC_SIZE: usize = 2; + +/// A list of [`Atom`]s. +#[derive(Clone, Debug, Default)] +pub struct Atoms<'a>(SmallVec<[Atom<'a>; ATOMS_SMALL_VEC_SIZE]>); + +impl<'a> Atoms<'a> { + pub fn new(atoms: impl IntoAtoms<'a>) -> Self { + atoms.into_atoms() + } + + /// Insert a new [`Atom`] at the end of the list (right side). + pub fn push_right(&mut self, atom: impl Into>) { + self.0.push(atom.into()); + } + + /// 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()); + } + + /// 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. + pub fn text(&self) -> Option> { + let mut string: Option> = None; + for atom in &self.0 { + if let AtomKind::Text(text) = &atom.kind { + if let Some(string) = &mut string { + let string = string.to_mut(); + string.push(' '); + string.push_str(text.text()); + } else { + string = Some(Cow::Borrowed(text.text())); + } + } + } + string + } + + pub fn iter_kinds(&'a self) -> impl Iterator> { + self.0.iter().map(|atom| &atom.kind) + } + + pub fn iter_kinds_mut(&'a mut self) -> impl Iterator> { + self.0.iter_mut().map(|atom| &mut atom.kind) + } + + pub fn iter_images(&'a self) -> impl Iterator> { + self.iter_kinds().filter_map(|kind| { + if let AtomKind::Image(image) = kind { + Some(image) + } else { + None + } + }) + } + + pub fn iter_images_mut(&'a mut self) -> impl Iterator> { + self.iter_kinds_mut().filter_map(|kind| { + if let AtomKind::Image(image) = kind { + Some(image) + } else { + None + } + }) + } + + pub fn iter_texts(&'a self) -> impl Iterator { + self.iter_kinds().filter_map(|kind| { + if let AtomKind::Text(text) = kind { + Some(text) + } else { + None + } + }) + } + + pub fn iter_texts_mut(&'a mut self) -> impl Iterator { + self.iter_kinds_mut().filter_map(|kind| { + if let AtomKind::Text(text) = kind { + Some(text) + } else { + None + } + }) + } + + pub fn map_atoms(&mut self, mut f: impl FnMut(Atom<'a>) -> Atom<'a>) { + self.iter_mut() + .for_each(|atom| *atom = f(std::mem::take(atom))); + } + + pub fn map_kind(&'a mut self, mut f: F) + where + F: FnMut(AtomKind<'a>) -> AtomKind<'a>, + { + for kind in self.iter_kinds_mut() { + *kind = f(std::mem::take(kind)); + } + } + + pub fn map_images(&'a mut self, mut f: F) + where + F: FnMut(Image<'a>) -> Image<'a>, + { + self.map_kind(|kind| { + if let AtomKind::Image(image) = kind { + AtomKind::Image(f(image)) + } else { + kind + } + }); + } + + pub fn map_texts(&'a mut self, mut f: F) + where + F: FnMut(WidgetText) -> WidgetText, + { + self.map_kind(|kind| { + if let AtomKind::Text(text) = kind { + AtomKind::Text(f(text)) + } else { + kind + } + }); + } +} + +impl<'a> IntoIterator for Atoms<'a> { + type Item = Atom<'a>; + type IntoIter = smallvec::IntoIter<[Atom<'a>; ATOMS_SMALL_VEC_SIZE]>; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +/// Helper trait to convert a tuple of atoms into [`Atoms`]. +/// +/// ``` +/// use egui::{Atoms, Image, IntoAtoms, RichText}; +/// let atoms: Atoms = ( +/// "Some text", +/// RichText::new("Some RichText"), +/// Image::new("some_image_url"), +/// ).into_atoms(); +/// ``` +impl<'a, T> IntoAtoms<'a> for T +where + T: Into>, +{ + fn collect(self, atoms: &mut Atoms<'a>) { + atoms.push_right(self); + } +} + +/// Trait for turning a tuple of [`Atom`]s into [`Atoms`]. +pub trait IntoAtoms<'a> { + fn collect(self, atoms: &mut Atoms<'a>); + + fn into_atoms(self) -> Atoms<'a> + where + Self: Sized, + { + let mut atoms = Atoms::default(); + self.collect(&mut atoms); + atoms + } +} + +impl<'a> IntoAtoms<'a> for Atoms<'a> { + fn collect(self, atoms: &mut Self) { + atoms.0.extend(self.0); + } +} + +macro_rules! all_the_atoms { + ($($T:ident),*) => { + impl<'a, $($T),*> IntoAtoms<'a> for ($($T),*) + where + $($T: IntoAtoms<'a>),* + { + fn collect(self, _atoms: &mut Atoms<'a>) { + #[allow(clippy::allow_attributes)] + #[allow(non_snake_case)] + let ($($T),*) = self; + $($T.collect(_atoms);)* + } + } + }; +} + +all_the_atoms!(); +all_the_atoms!(T0, T1); +all_the_atoms!(T0, T1, T2); +all_the_atoms!(T0, T1, T2, T3); +all_the_atoms!(T0, T1, T2, T3, T4); +all_the_atoms!(T0, T1, T2, T3, T4, T5); + +impl<'a> Deref for Atoms<'a> { + type Target = [Atom<'a>]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Atoms<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} diff --git a/crates/egui/src/atomics/mod.rs b/crates/egui/src/atomics/mod.rs new file mode 100644 index 000000000..7c8922c97 --- /dev/null +++ b/crates/egui/src/atomics/mod.rs @@ -0,0 +1,15 @@ +mod atom; +mod atom_ext; +mod atom_kind; +mod atom_layout; +mod atoms; +mod sized_atom; +mod sized_atom_kind; + +pub use atom::*; +pub use atom_ext::*; +pub use atom_kind::*; +pub use atom_layout::*; +pub use atoms::*; +pub use sized_atom::*; +pub use sized_atom_kind::*; diff --git a/crates/egui/src/atomics/sized_atom.rs b/crates/egui/src/atomics/sized_atom.rs new file mode 100644 index 000000000..50fa443a9 --- /dev/null +++ b/crates/egui/src/atomics/sized_atom.rs @@ -0,0 +1,26 @@ +use crate::SizedAtomKind; +use emath::Vec2; + +/// A [`crate::Atom`] which has been sized. +#[derive(Clone, Debug)] +pub struct SizedAtom<'a> { + pub(crate) grow: bool, + + /// The size of the atom. + /// + /// Used for placing this atom in [`crate::AtomLayout`], the cursor will advance by + /// size.x + gap. + pub size: Vec2, + + /// Preferred size of the atom. This is used to calculate `Response::intrinsic_size`. + pub preferred_size: Vec2, + + pub kind: SizedAtomKind<'a>, +} + +impl SizedAtom<'_> { + /// Was this [`crate::Atom`] marked as `grow`? + pub fn is_grow(&self) -> bool { + self.grow + } +} diff --git a/crates/egui/src/atomics/sized_atom_kind.rs b/crates/egui/src/atomics/sized_atom_kind.rs new file mode 100644 index 000000000..ff8da1631 --- /dev/null +++ b/crates/egui/src/atomics/sized_atom_kind.rs @@ -0,0 +1,25 @@ +use crate::{Id, Image}; +use emath::Vec2; +use epaint::Galley; +use std::sync::Arc; + +/// A sized [`crate::AtomKind`]. +#[derive(Clone, Default, Debug)] +pub enum SizedAtomKind<'a> { + #[default] + Empty, + Text(Arc), + Image(Image<'a>, Vec2), + Custom(Id), +} + +impl SizedAtomKind<'_> { + /// Get the calculated size. + pub fn size(&self) -> Vec2 { + match self { + SizedAtomKind::Text(galley) => galley.size(), + SizedAtomKind::Image(_, size) => *size, + SizedAtomKind::Empty | SizedAtomKind::Custom(_) => Vec2::ZERO, + } + } +} diff --git a/crates/egui/src/containers/menu.rs b/crates/egui/src/containers/menu.rs index 228bbd86d..4fe06477d 100644 --- a/crates/egui/src/containers/menu.rs +++ b/crates/egui/src/containers/menu.rs @@ -1,7 +1,7 @@ use crate::style::StyleModifier; use crate::{ - Button, Color32, Context, Frame, Id, InnerResponse, Layout, Popup, PopupCloseBehavior, - Response, Style, Ui, UiBuilder, UiKind, UiStack, UiStackInfo, Widget as _, WidgetText, + Button, Color32, Context, Frame, Id, InnerResponse, IntoAtoms, Layout, Popup, + PopupCloseBehavior, Response, Style, Ui, UiBuilder, UiKind, UiStack, UiStackInfo, Widget as _, }; use emath::{vec2, Align, RectAlign, Vec2}; use epaint::Stroke; @@ -243,8 +243,8 @@ pub struct MenuButton<'a> { } impl<'a> MenuButton<'a> { - pub fn new(text: impl Into) -> Self { - Self::from_button(Button::new(text)) + pub fn new(atoms: impl IntoAtoms<'a>) -> Self { + Self::from_button(Button::new(atoms.into_atoms())) } /// Set the config for the menu. @@ -293,8 +293,8 @@ impl<'a> SubMenuButton<'a> { /// The default right arrow symbol: `"āµ"` pub const RIGHT_ARROW: &'static str = "āµ"; - pub fn new(text: impl Into) -> Self { - Self::from_button(Button::new(text).right_text("āµ")) + pub fn new(atoms: impl IntoAtoms<'a>) -> Self { + Self::from_button(Button::new(atoms.into_atoms()).right_text("āµ")) } /// Create a new submenu button from a [`Button`]. diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 6d42a230b..4ff7db230 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -444,6 +444,7 @@ mod widget_rect; pub mod widget_text; pub mod widgets; +mod atomics; #[cfg(feature = "callstack")] #[cfg(debug_assertions)] mod callstack; @@ -482,6 +483,7 @@ pub mod text { } pub use self::{ + atomics::*, containers::*, context::{Context, RepaintCause, RequestRepaintInfo}, data::{ diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 4174ca961..bc2ed860d 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -25,10 +25,10 @@ use crate::{ color_picker, Button, Checkbox, DragValue, Hyperlink, Image, ImageSource, Label, Link, RadioButton, SelectableLabel, Separator, Spinner, TextEdit, Widget, }, - Align, Color32, Context, CursorIcon, DragAndDrop, Id, InnerResponse, InputState, LayerId, - Memory, Order, Painter, PlatformOutput, Pos2, Rangef, Rect, Response, Rgba, RichText, Sense, - Style, TextStyle, TextWrapMode, UiBuilder, UiKind, UiStack, UiStackInfo, Vec2, WidgetRect, - WidgetText, + Align, Color32, Context, CursorIcon, DragAndDrop, Id, InnerResponse, InputState, IntoAtoms, + LayerId, Memory, Order, Painter, PlatformOutput, Pos2, Rangef, Rect, Response, Rgba, RichText, + Sense, Style, TextStyle, TextWrapMode, UiBuilder, UiKind, UiStack, UiStackInfo, Vec2, + WidgetRect, WidgetText, }; // ---------------------------------------------------------------------------- @@ -2055,8 +2055,8 @@ impl Ui { /// ``` #[must_use = "You should check if the user clicked this with `if ui.button(…).clicked() { … } "] #[inline] - pub fn button(&mut self, text: impl Into) -> Response { - Button::new(text).ui(self) + pub fn button<'a>(&mut self, atoms: impl IntoAtoms<'a>) -> Response { + Button::new(atoms).ui(self) } /// A button as small as normal body text. @@ -2073,8 +2073,8 @@ impl Ui { /// /// See also [`Self::toggle_value`]. #[inline] - pub fn checkbox(&mut self, checked: &mut bool, text: impl Into) -> Response { - Checkbox::new(checked, text).ui(self) + pub fn checkbox<'a>(&mut self, checked: &'a mut bool, atoms: impl IntoAtoms<'a>) -> Response { + Checkbox::new(checked, atoms).ui(self) } /// Acts like a checkbox, but looks like a [`SelectableLabel`]. @@ -2095,8 +2095,8 @@ impl Ui { /// Often you want to use [`Self::radio_value`] instead. #[must_use = "You should check if the user clicked this with `if ui.radio(…).clicked() { … } "] #[inline] - pub fn radio(&mut self, selected: bool, text: impl Into) -> Response { - RadioButton::new(selected, text).ui(self) + pub fn radio<'a>(&mut self, selected: bool, atoms: impl IntoAtoms<'a>) -> Response { + RadioButton::new(selected, atoms).ui(self) } /// Show a [`RadioButton`]. It is selected if `*current_value == selected_value`. @@ -2118,13 +2118,13 @@ impl Ui { /// } /// # }); /// ``` - pub fn radio_value( + pub fn radio_value<'a, Value: PartialEq>( &mut self, current_value: &mut Value, alternative: Value, - text: impl Into, + atoms: impl IntoAtoms<'a>, ) -> Response { - let mut response = self.radio(*current_value == alternative, text); + let mut response = self.radio(*current_value == alternative, atoms); if response.clicked() && *current_value != alternative { *current_value = alternative; response.mark_changed(); @@ -3041,15 +3041,15 @@ impl Ui { /// ``` /// /// See also: [`Self::close`] and [`Response::context_menu`]. - pub fn menu_button( + pub fn menu_button<'a, R>( &mut self, - title: impl Into, + atoms: impl IntoAtoms<'a>, add_contents: impl FnOnce(&mut Ui) -> R, ) -> InnerResponse> { let (response, inner) = if menu::is_in_menu(self) { - menu::SubMenuButton::new(title).ui(self, add_contents) + menu::SubMenuButton::new(atoms).ui(self, add_contents) } else { - menu::MenuButton::new(title).ui(self, add_contents) + menu::MenuButton::new(atoms).ui(self, add_contents) }; InnerResponse::new(inner.map(|i| i.inner), response) } diff --git a/crates/egui/src/widget_text.rs b/crates/egui/src/widget_text.rs index d9f98859b..d0edb6a26 100644 --- a/crates/egui/src/widget_text.rs +++ b/crates/egui/src/widget_text.rs @@ -1,7 +1,7 @@ -use std::{borrow::Cow, sync::Arc}; - use emath::GuiRounding as _; use epaint::text::TextFormat; +use std::fmt::Formatter; +use std::{borrow::Cow, sync::Arc}; use crate::{ text::{LayoutJob, TextWrapping}, @@ -521,6 +521,18 @@ pub enum WidgetText { Galley(Arc), } +impl std::fmt::Debug for WidgetText { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let text = self.text(); + match self { + Self::Text(_) => write!(f, "Text({text:?})"), + Self::RichText(_) => write!(f, "RichText({text:?})"), + Self::LayoutJob(_) => write!(f, "LayoutJob({text:?})"), + Self::Galley(_) => write!(f, "Galley({text:?})"), + } + } +} + impl Default for WidgetText { fn default() -> Self { Self::Text(String::new()) diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 9ae573aef..aa75eabd8 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -1,6 +1,7 @@ use crate::{ - widgets, Align, Color32, CornerRadius, FontSelection, Image, NumExt as _, Rect, Response, - Sense, Stroke, TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetType, + Atom, AtomExt as _, AtomKind, AtomLayout, AtomLayoutResponse, Color32, CornerRadius, Frame, + Image, IntoAtoms, NumExt as _, Response, Sense, Stroke, TextWrapMode, Ui, Vec2, Widget, + WidgetInfo, WidgetText, WidgetType, }; /// Clickable button with text. @@ -23,56 +24,66 @@ use crate::{ /// ``` #[must_use = "You should put this widget in a ui with `ui.add(widget);`"] pub struct Button<'a> { - image: Option>, - text: Option, - right_text: WidgetText, - wrap_mode: Option, - - /// None means default for interact + layout: AtomLayout<'a>, fill: Option, stroke: Option, - sense: Sense, small: bool, frame: Option, min_size: Vec2, corner_radius: Option, selected: bool, image_tint_follows_text_color: bool, + limit_image_size: bool, } impl<'a> Button<'a> { - pub fn new(text: impl Into) -> Self { - Self::opt_image_and_text(None, Some(text.into())) - } - - /// Creates a button with an image. The size of the image as displayed is defined by the provided size. - pub fn image(image: impl Into>) -> Self { - Self::opt_image_and_text(Some(image.into()), None) - } - - /// Creates a button with an image to the left of the text. The size of the image as displayed is defined by the provided size. - pub fn image_and_text(image: impl Into>, text: impl Into) -> Self { - Self::opt_image_and_text(Some(image.into()), Some(text.into())) - } - - pub fn opt_image_and_text(image: Option>, text: Option) -> Self { + pub fn new(atoms: impl IntoAtoms<'a>) -> Self { Self { - text, - image, - right_text: Default::default(), - wrap_mode: None, + layout: AtomLayout::new(atoms.into_atoms()).sense(Sense::click()), fill: None, stroke: None, - sense: Sense::click(), small: false, frame: None, min_size: Vec2::ZERO, corner_radius: None, selected: false, image_tint_follows_text_color: false, + limit_image_size: false, } } + /// Creates a button with an image. The size of the image as displayed is defined by the provided size. + /// + /// Note: In contrast to [`Button::new`], this limits the image size to the default font height + /// (using [`crate::AtomExt::atom_max_height_font_size`]). + pub fn image(image: impl Into>) -> Self { + Self::opt_image_and_text(Some(image.into()), None) + } + + /// Creates a button with an image to the left of the text. + /// + /// Note: In contrast to [`Button::new`], this limits the image size to the default font height + /// (using [`crate::AtomExt::atom_max_height_font_size`]). + pub fn image_and_text(image: impl Into>, text: impl Into) -> Self { + Self::opt_image_and_text(Some(image.into()), Some(text.into())) + } + + /// Create a button with an optional image and optional text. + /// + /// Note: In contrast to [`Button::new`], this limits the image size to the default font height + /// (using [`crate::AtomExt::atom_max_height_font_size`]). + pub fn opt_image_and_text(image: Option>, text: Option) -> Self { + let mut button = Self::new(()); + if let Some(image) = image { + button.layout.push_right(image); + } + if let Some(text) = text { + button.layout.push_right(text); + } + button.limit_image_size = true; + button + } + /// Set the wrap mode for the text. /// /// By default, [`crate::Ui::wrap_mode`] will be used, which can be overridden with [`crate::Style::wrap_mode`]. @@ -80,23 +91,20 @@ impl<'a> Button<'a> { /// Note that any `\n` in the text will always produce a new line. #[inline] pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self { - self.wrap_mode = Some(wrap_mode); + self.layout = self.layout.wrap_mode(wrap_mode); self } /// Set [`Self::wrap_mode`] to [`TextWrapMode::Wrap`]. #[inline] - pub fn wrap(mut self) -> Self { - self.wrap_mode = Some(TextWrapMode::Wrap); - - self + pub fn wrap(self) -> Self { + self.wrap_mode(TextWrapMode::Wrap) } /// Set [`Self::wrap_mode`] to [`TextWrapMode::Truncate`]. #[inline] - pub fn truncate(mut self) -> Self { - self.wrap_mode = Some(TextWrapMode::Truncate); - self + pub fn truncate(self) -> Self { + self.wrap_mode(TextWrapMode::Truncate) } /// Override background fill color. Note that this will override any on-hover effects. @@ -104,7 +112,6 @@ impl<'a> Button<'a> { #[inline] pub fn fill(mut self, fill: impl Into) -> Self { self.fill = Some(fill.into()); - self.frame = Some(true); self } @@ -120,9 +127,6 @@ impl<'a> Button<'a> { /// Make this a small button, suitable for embedding into text. #[inline] pub fn small(mut self) -> Self { - if let Some(text) = self.text { - self.text = Some(text.text_style(TextStyle::Body)); - } self.small = true; self } @@ -138,7 +142,7 @@ impl<'a> Button<'a> { /// Change this to a drag-button with `Sense::drag()`. #[inline] pub fn sense(mut self, sense: Sense) -> Self { - self.sense = sense; + self.layout = self.layout.sense(sense); self } @@ -182,15 +186,22 @@ impl<'a> Button<'a> { /// /// See also [`Self::right_text`]. #[inline] - pub fn shortcut_text(mut self, shortcut_text: impl Into) -> Self { - self.right_text = shortcut_text.into().weak(); + pub fn shortcut_text(mut self, shortcut_text: impl Into>) -> Self { + let mut atom = shortcut_text.into(); + atom.kind = match atom.kind { + AtomKind::Text(text) => AtomKind::Text(text.weak()), + other => other, + }; + self.layout.push_right(Atom::grow()); + self.layout.push_right(atom); self } /// Show some text on the right side of the button. #[inline] - pub fn right_text(mut self, right_text: impl Into) -> Self { - self.right_text = right_text.into(); + pub fn right_text(mut self, right_text: impl Into>) -> Self { + self.layout.push_right(Atom::grow()); + self.layout.push_right(right_text.into()); self } @@ -200,39 +211,41 @@ impl<'a> Button<'a> { self.selected = selected; self } -} -impl Widget for Button<'_> { - fn ui(self, ui: &mut Ui) -> Response { + /// Show the button and return a [`AtomLayoutResponse`] for painting custom contents. + pub fn atom_ui(self, ui: &mut Ui) -> AtomLayoutResponse { let Button { - text, - image, - right_text, - wrap_mode, + mut layout, fill, stroke, - sense, small, frame, - min_size, + mut min_size, corner_radius, selected, image_tint_follows_text_color, + limit_image_size, } = self; - let frame = frame.unwrap_or_else(|| ui.visuals().button_frame); + if !small { + min_size.y = min_size.y.at_least(ui.spacing().interact_size.y); + } - let default_font_height = || { - let font_selection = FontSelection::default(); - let font_id = font_selection.resolve(ui.style()); - ui.fonts(|f| f.row_height(&font_id)) - }; + if limit_image_size { + layout.map_atoms(|atom| { + if matches!(&atom.kind, AtomKind::Image(_)) { + atom.atom_max_height_font_size(ui) + } else { + atom + } + }); + } - let text_font_height = ui - .fonts(|fonts| text.as_ref().map(|wt| wt.font_height(fonts, ui.style()))) - .unwrap_or_else(default_font_height); + let text = layout.text().map(String::from); - let mut button_padding = if frame { + let has_frame = frame.unwrap_or_else(|| ui.visuals().button_frame); + + let mut button_padding = if has_frame { ui.spacing().button_padding } else { Vec2::ZERO @@ -241,192 +254,53 @@ impl Widget for Button<'_> { button_padding.y = 0.0; } - let (space_available_for_image, right_text_font_height) = if let Some(text) = &text { - let font_height = ui.fonts(|fonts| text.font_height(fonts, ui.style())); - ( - Vec2::splat(font_height), // Reasonable? - font_height, - ) - } else { - ( - (ui.available_size() - 2.0 * button_padding).at_least(Vec2::ZERO), - default_font_height(), - ) - }; + let mut prepared = layout + .frame(Frame::new().inner_margin(button_padding)) + .min_size(min_size) + .allocate(ui); - let image_size = if let Some(image) = &image { - image - .load_and_calc_size(ui, space_available_for_image) - .unwrap_or(space_available_for_image) - } else { - Vec2::ZERO - }; + let response = if ui.is_rect_visible(prepared.response.rect) { + let visuals = ui.style().interact_selectable(&prepared.response, selected); - let gap_before_right_text = ui.spacing().item_spacing.x; - - let mut text_wrap_width = ui.available_width() - 2.0 * button_padding.x; - if image.is_some() { - text_wrap_width -= image_size.x + ui.spacing().icon_spacing; - } - - // Note: we don't wrap the right text - let right_galley = (!right_text.is_empty()).then(|| { - right_text.into_galley( - ui, - Some(TextWrapMode::Extend), - f32::INFINITY, - TextStyle::Button, - ) - }); - - if let Some(right_galley) = &right_galley { - // Leave space for the right text: - text_wrap_width -= gap_before_right_text + right_galley.size().x; - } - - let galley = - text.map(|text| text.into_galley(ui, wrap_mode, text_wrap_width, TextStyle::Button)); - - let mut desired_size = Vec2::ZERO; - if image.is_some() { - desired_size.x += image_size.x; - desired_size.y = desired_size.y.max(image_size.y); - } - if image.is_some() && galley.is_some() { - desired_size.x += ui.spacing().icon_spacing; - } - if let Some(galley) = &galley { - desired_size.x += galley.size().x; - desired_size.y = desired_size.y.max(galley.size().y).max(text_font_height); - } - if let Some(right_galley) = &right_galley { - desired_size.x += gap_before_right_text + right_galley.size().x; - desired_size.y = desired_size - .y - .max(right_galley.size().y) - .max(right_text_font_height); - } - desired_size += 2.0 * button_padding; - if !small { - desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y); - } - desired_size = desired_size.at_least(min_size); - - let (rect, mut response) = ui.allocate_at_least(desired_size, sense); - response.widget_info(|| { - let mut widget_info = WidgetInfo::new(WidgetType::Button); - widget_info.enabled = ui.is_enabled(); - - if let Some(galley) = &galley { - widget_info.label = Some(galley.text().to_owned()); - } else if let Some(image) = &image { - widget_info.label = image.alt_text.clone(); + if image_tint_follows_text_color { + prepared.map_images(|image| image.tint(visuals.text_color())); } - widget_info - }); - if ui.is_rect_visible(rect) { - let visuals = ui.style().interact(&response); + prepared.fallback_text_color = visuals.text_color(); - let (frame_expansion, frame_cr, frame_fill, frame_stroke) = if selected { - let selection = ui.visuals().selection; - ( - Vec2::ZERO, - CornerRadius::ZERO, - selection.bg_fill, - selection.stroke, - ) - } else if frame { - let expansion = Vec2::splat(visuals.expansion); - ( - expansion, - visuals.corner_radius, - visuals.weak_bg_fill, - visuals.bg_stroke, - ) - } else { - Default::default() + if has_frame { + let stroke = stroke.unwrap_or(visuals.bg_stroke); + let fill = fill.unwrap_or(visuals.weak_bg_fill); + prepared.frame = prepared + .frame + .inner_margin( + button_padding + Vec2::splat(visuals.expansion) - Vec2::splat(stroke.width), + ) + .outer_margin(-Vec2::splat(visuals.expansion)) + .fill(fill) + .stroke(stroke) + .corner_radius(corner_radius.unwrap_or(visuals.corner_radius)); }; - let frame_cr = corner_radius.unwrap_or(frame_cr); - let frame_fill = fill.unwrap_or(frame_fill); - let frame_stroke = stroke.unwrap_or(frame_stroke); - ui.painter().rect( - rect.expand2(frame_expansion), - frame_cr, - frame_fill, - frame_stroke, - epaint::StrokeKind::Inside, - ); - let mut cursor_x = rect.min.x + button_padding.x; + prepared.paint(ui) + } else { + AtomLayoutResponse::empty(prepared.response) + }; - if let Some(image) = &image { - let mut image_pos = ui - .layout() - .align_size_within_rect(image_size, rect.shrink2(button_padding)) - .min; - if galley.is_some() || right_galley.is_some() { - image_pos.x = cursor_x; - } - let image_rect = Rect::from_min_size(image_pos, image_size); - cursor_x += image_size.x; - let tlr = image.load_for_size(ui.ctx(), image_size); - let mut image_options = image.image_options().clone(); - if image_tint_follows_text_color { - image_options.tint = image_options.tint * visuals.text_color(); - } - widgets::image::paint_texture_load_result( - ui, - &tlr, - image_rect, - image.show_loading_spinner, - &image_options, - None, - ); - response = widgets::image::texture_load_result_response( - &image.source(ui.ctx()), - &tlr, - response, - ); + response.response.widget_info(|| { + if let Some(text) = &text { + WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), text) + } else { + WidgetInfo::new(WidgetType::Button) } - - if image.is_some() && galley.is_some() { - cursor_x += ui.spacing().icon_spacing; - } - - if let Some(galley) = galley { - let mut text_pos = ui - .layout() - .align_size_within_rect(galley.size(), rect.shrink2(button_padding)) - .min; - if image.is_some() || right_galley.is_some() { - text_pos.x = cursor_x; - } - ui.painter().galley(text_pos, galley, visuals.text_color()); - } - - if let Some(right_galley) = right_galley { - // Always align to the right - let layout = if ui.layout().is_horizontal() { - ui.layout().with_main_align(Align::Max) - } else { - ui.layout().with_cross_align(Align::Max) - }; - let right_text_pos = layout - .align_size_within_rect(right_galley.size(), rect.shrink2(button_padding)) - .min; - - ui.painter() - .galley(right_text_pos, right_galley, visuals.text_color()); - } - } - - if let Some(cursor) = ui.visuals().interact_cursor { - if response.hovered() { - ui.ctx().set_cursor_icon(cursor); - } - } + }); response } } + +impl Widget for Button<'_> { + fn ui(self, ui: &mut Ui) -> Response { + self.atom_ui(ui).response + } +} diff --git a/crates/egui/src/widgets/checkbox.rs b/crates/egui/src/widgets/checkbox.rs index 97bd97b88..f7498de5a 100644 --- a/crates/egui/src/widgets/checkbox.rs +++ b/crates/egui/src/widgets/checkbox.rs @@ -1,6 +1,6 @@ use crate::{ - epaint, pos2, vec2, NumExt as _, Response, Sense, Shape, TextStyle, Ui, Vec2, Widget, - WidgetInfo, WidgetText, WidgetType, + epaint, pos2, Atom, AtomLayout, Atoms, Id, IntoAtoms, NumExt as _, Response, Sense, Shape, Ui, + Vec2, Widget, WidgetInfo, WidgetType, }; // TODO(emilk): allow checkbox without a text label @@ -19,21 +19,21 @@ use crate::{ #[must_use = "You should put this widget in a ui with `ui.add(widget);`"] pub struct Checkbox<'a> { checked: &'a mut bool, - text: WidgetText, + atoms: Atoms<'a>, indeterminate: bool, } impl<'a> Checkbox<'a> { - pub fn new(checked: &'a mut bool, text: impl Into) -> Self { + pub fn new(checked: &'a mut bool, atoms: impl IntoAtoms<'a>) -> Self { Checkbox { checked, - text: text.into(), + atoms: atoms.into_atoms(), indeterminate: false, } } pub fn without_text(checked: &'a mut bool) -> Self { - Self::new(checked, WidgetText::default()) + Self::new(checked, ()) } /// Display an indeterminate state (neither checked nor unchecked) @@ -51,92 +51,88 @@ impl Widget for Checkbox<'_> { fn ui(self, ui: &mut Ui) -> Response { let Checkbox { checked, - text, + mut atoms, indeterminate, } = self; let spacing = &ui.spacing(); let icon_width = spacing.icon_width; - let icon_spacing = spacing.icon_spacing; - let (galley, mut desired_size) = if text.is_empty() { - (None, vec2(icon_width, 0.0)) - } else { - let total_extra = vec2(icon_width + icon_spacing, 0.0); + let mut min_size = Vec2::splat(spacing.interact_size.y); + min_size.y = min_size.y.at_least(icon_width); - let wrap_width = ui.available_width() - total_extra.x; - let galley = text.into_galley(ui, None, wrap_width, TextStyle::Button); + // In order to center the checkbox based on min_size we set the icon height to at least min_size.y + let mut icon_size = Vec2::splat(icon_width); + icon_size.y = icon_size.y.at_least(min_size.y); + let rect_id = Id::new("egui::checkbox"); + atoms.push_left(Atom::custom(rect_id, icon_size)); - let mut desired_size = total_extra + galley.size(); - desired_size = desired_size.at_least(spacing.interact_size); + let text = atoms.text().map(String::from); - (Some(galley), desired_size) - }; + let mut prepared = AtomLayout::new(atoms) + .sense(Sense::click()) + .min_size(min_size) + .allocate(ui); - desired_size = desired_size.at_least(Vec2::splat(spacing.interact_size.y)); - desired_size.y = desired_size.y.max(icon_width); - let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click()); - - if response.clicked() { + if prepared.response.clicked() { *checked = !*checked; - response.mark_changed(); + prepared.response.mark_changed(); } - response.widget_info(|| { + prepared.response.widget_info(|| { if indeterminate { WidgetInfo::labeled( WidgetType::Checkbox, ui.is_enabled(), - galley.as_ref().map_or("", |x| x.text()), + text.as_deref().unwrap_or(""), ) } else { WidgetInfo::selected( WidgetType::Checkbox, ui.is_enabled(), *checked, - galley.as_ref().map_or("", |x| x.text()), + text.as_deref().unwrap_or(""), ) } }); - if ui.is_rect_visible(rect) { + if ui.is_rect_visible(prepared.response.rect) { // let visuals = ui.style().interact_selectable(&response, *checked); // too colorful - let visuals = ui.style().interact(&response); - let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect); - ui.painter().add(epaint::RectShape::new( - big_icon_rect.expand(visuals.expansion), - visuals.corner_radius, - visuals.bg_fill, - visuals.bg_stroke, - epaint::StrokeKind::Inside, - )); + let visuals = *ui.style().interact(&prepared.response); + prepared.fallback_text_color = visuals.text_color(); + let response = prepared.paint(ui); - if indeterminate { - // Horizontal line: - ui.painter().add(Shape::hline( - small_icon_rect.x_range(), - small_icon_rect.center().y, - visuals.fg_stroke, - )); - } else if *checked { - // Check mark: - ui.painter().add(Shape::line( - vec![ - pos2(small_icon_rect.left(), small_icon_rect.center().y), - pos2(small_icon_rect.center().x, small_icon_rect.bottom()), - pos2(small_icon_rect.right(), small_icon_rect.top()), - ], - visuals.fg_stroke, + if let Some(rect) = response.rect(rect_id) { + let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect); + ui.painter().add(epaint::RectShape::new( + big_icon_rect.expand(visuals.expansion), + visuals.corner_radius, + visuals.bg_fill, + visuals.bg_stroke, + epaint::StrokeKind::Inside, )); + + if indeterminate { + // Horizontal line: + ui.painter().add(Shape::hline( + small_icon_rect.x_range(), + small_icon_rect.center().y, + visuals.fg_stroke, + )); + } else if *checked { + // Check mark: + ui.painter().add(Shape::line( + vec![ + pos2(small_icon_rect.left(), small_icon_rect.center().y), + pos2(small_icon_rect.center().x, small_icon_rect.bottom()), + pos2(small_icon_rect.right(), small_icon_rect.top()), + ], + visuals.fg_stroke, + )); + } } - if let Some(galley) = galley { - let text_pos = pos2( - rect.min.x + icon_width + icon_spacing, - rect.center().y - 0.5 * galley.size().y, - ); - ui.painter().galley(text_pos, galley, visuals.text_color()); - } + response.response + } else { + prepared.response } - - response } } diff --git a/crates/egui/src/widgets/radio_button.rs b/crates/egui/src/widgets/radio_button.rs index 7c178840d..53dda399f 100644 --- a/crates/egui/src/widgets/radio_button.rs +++ b/crates/egui/src/widgets/radio_button.rs @@ -1,6 +1,6 @@ use crate::{ - epaint, pos2, vec2, NumExt as _, Response, Sense, TextStyle, Ui, Vec2, Widget, WidgetInfo, - WidgetText, WidgetType, + epaint, Atom, AtomLayout, Atoms, Id, IntoAtoms, NumExt as _, Response, Sense, Ui, Vec2, Widget, + WidgetInfo, WidgetType, }; /// One out of several alternatives, either selected or not. @@ -23,89 +23,84 @@ use crate::{ /// # }); /// ``` #[must_use = "You should put this widget in a ui with `ui.add(widget);`"] -pub struct RadioButton { +pub struct RadioButton<'a> { checked: bool, - text: WidgetText, + atoms: Atoms<'a>, } -impl RadioButton { - pub fn new(checked: bool, text: impl Into) -> Self { +impl<'a> RadioButton<'a> { + pub fn new(checked: bool, atoms: impl IntoAtoms<'a>) -> Self { Self { checked, - text: text.into(), + atoms: atoms.into_atoms(), } } } -impl Widget for RadioButton { +impl Widget for RadioButton<'_> { fn ui(self, ui: &mut Ui) -> Response { - let Self { checked, text } = self; + let Self { checked, mut atoms } = self; let spacing = &ui.spacing(); let icon_width = spacing.icon_width; - let icon_spacing = spacing.icon_spacing; - let (galley, mut desired_size) = if text.is_empty() { - (None, vec2(icon_width, 0.0)) - } else { - let total_extra = vec2(icon_width + icon_spacing, 0.0); + let mut min_size = Vec2::splat(spacing.interact_size.y); + min_size.y = min_size.y.at_least(icon_width); - let wrap_width = ui.available_width() - total_extra.x; - let text = text.into_galley(ui, None, wrap_width, TextStyle::Button); + // In order to center the checkbox based on min_size we set the icon height to at least min_size.y + let mut icon_size = Vec2::splat(icon_width); + icon_size.y = icon_size.y.at_least(min_size.y); + let rect_id = Id::new("egui::radio_button"); + atoms.push_left(Atom::custom(rect_id, icon_size)); - let mut desired_size = total_extra + text.size(); - desired_size = desired_size.at_least(spacing.interact_size); + let text = atoms.text().map(String::from); - (Some(text), desired_size) - }; + let mut prepared = AtomLayout::new(atoms) + .sense(Sense::click()) + .min_size(min_size) + .allocate(ui); - desired_size = desired_size.at_least(Vec2::splat(spacing.interact_size.y)); - desired_size.y = desired_size.y.max(icon_width); - let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click()); - - response.widget_info(|| { + prepared.response.widget_info(|| { WidgetInfo::selected( WidgetType::RadioButton, ui.is_enabled(), checked, - galley.as_ref().map_or("", |x| x.text()), + text.as_deref().unwrap_or(""), ) }); - if ui.is_rect_visible(rect) { + if ui.is_rect_visible(prepared.response.rect) { // let visuals = ui.style().interact_selectable(&response, checked); // too colorful - let visuals = ui.style().interact(&response); + let visuals = *ui.style().interact(&prepared.response); - let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect); + prepared.fallback_text_color = visuals.text_color(); + let response = prepared.paint(ui); - let painter = ui.painter(); + if let Some(rect) = response.rect(rect_id) { + let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect); - painter.add(epaint::CircleShape { - center: big_icon_rect.center(), - radius: big_icon_rect.width() / 2.0 + visuals.expansion, - fill: visuals.bg_fill, - stroke: visuals.bg_stroke, - }); + let painter = ui.painter(); - if checked { painter.add(epaint::CircleShape { - center: small_icon_rect.center(), - radius: small_icon_rect.width() / 3.0, - fill: visuals.fg_stroke.color, // Intentional to use stroke and not fill - // fill: ui.visuals().selection.stroke.color, // too much color - stroke: Default::default(), + center: big_icon_rect.center(), + radius: big_icon_rect.width() / 2.0 + visuals.expansion, + fill: visuals.bg_fill, + stroke: visuals.bg_stroke, }); - } - if let Some(galley) = galley { - let text_pos = pos2( - rect.min.x + icon_width + icon_spacing, - rect.center().y - 0.5 * galley.size().y, - ); - ui.painter().galley(text_pos, galley, visuals.text_color()); + if checked { + painter.add(epaint::CircleShape { + center: small_icon_rect.center(), + radius: small_icon_rect.width() / 3.0, + fill: visuals.fg_stroke.color, // Intentional to use stroke and not fill + // fill: ui.visuals().selection.stroke.color, // too much color + stroke: Default::default(), + }); + } } + response.response + } else { + prepared.response } - - response } } diff --git a/crates/egui_demo_app/tests/snapshots/imageviewer.png b/crates/egui_demo_app/tests/snapshots/imageviewer.png index 57c88b50a..d5bde1f98 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:f7572ec2dad9038c24beb9949e4c05155cd0f5479153de6647c38911ec5c67a0 -size 100779 +oid sha256:e2fae780123389ca0affa762a5c031b84abcdd31c7a830d485c907c8c370b006 +size 100780 diff --git a/crates/egui_demo_lib/src/demo/widget_gallery.rs b/crates/egui_demo_lib/src/demo/widget_gallery.rs index c7b6df28a..31f5d279a 100644 --- a/crates/egui_demo_lib/src/demo/widget_gallery.rs +++ b/crates/egui_demo_lib/src/demo/widget_gallery.rs @@ -322,7 +322,10 @@ mod tests { let mut harness = Harness::builder() .with_pixels_per_point(2.0) .with_size(Vec2::new(380.0, 550.0)) - .build_ui(|ui| demo.ui(ui)); + .build_ui(|ui| { + egui_extras::install_image_loaders(ui.ctx()); + demo.ui(ui); + }); harness.fit_contents(); 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 526dc7ace..394bea644 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:c69c211061663cd17756eb0ad5a7720ed883047dbcedb39c493c544cfc644ed3 +oid sha256:0c975c8b646425878b704f32198286010730746caf5d463ca8cbcfe539922816 size 99087 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 e2899160b..7800f5f5f 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:18fe761145335a60b1eeb1f7f2072224df86f0e2006caa09d1f3cc4bd263d90c -size 46560 +oid sha256:c47a19d1f56fcc4c30c7e88aada2a50e038d66c1b591b4646b86c11bffb3c66f +size 46563 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scene.png b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png index 212a7ccd4..ea8f9c857 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:0fcfee082fe1dcbb7515ca6e3d5457e71fecf91a3efc4f76906a32fdb588adb4 -size 35096 +oid sha256:cdff6256488f3a40c65a3d73c0635377bf661c57927bce4c853b2a5f3b33274e +size 35121 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png b/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png index 17e76840e..49b223e7d 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:b236fe02f6cd52041359cf4b1a00e9812b95560353ce5df4fa6cb20fdbb45307 +oid sha256:4a347875ef98ebbd606774e03baffdb317cb0246882db116fee1aa7685efbb88 size 179653 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png b/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png index 09f8eaa6c..92e94b78f 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:1579351658875af48ad9aafeb08d928d83f1bda42bf092fdcceecd0aa6730e26 -size 115313 +oid sha256:f0e3eeca8abb4fba632cef4621d478fb66af1a0f13e099dda9a79420cc2b6301 +size 115320 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Table.png b/crates/egui_demo_lib/tests/snapshots/demos/Table.png index c11788f40..8a269fd4e 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:9446da28768cae0b489e0f6243410a8b3acf0ca2a0b70690d65d2a6221bc25b9 -size 30517 +oid sha256:9d27ed8292a2612b337f663bff73cd009a82f806c61f0863bf70a53fd4c281ff +size 75074 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery.png index b05ebda12..bcb09fe26 100644 --- a/crates/egui_demo_lib/tests/snapshots/widget_gallery.png +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af75f773e9e4ad2615893babce5b99e7fd127c76dd0976ac8dc95307f38a59dc -size 152854 +oid sha256:ee129f0542f21e12f5aa3c2f9746e7cadd73441a04d580f57c12c1cdd40d8b07 +size 153136 diff --git a/crates/egui_extras/src/syntax_highlighting.rs b/crates/egui_extras/src/syntax_highlighting.rs index 6bb51184f..8d688ae8e 100644 --- a/crates/egui_extras/src/syntax_highlighting.rs +++ b/crates/egui_extras/src/syntax_highlighting.rs @@ -511,7 +511,7 @@ struct Highlighter {} #[cfg(not(feature = "syntect"))] impl Highlighter { - #[expect(clippy::unused_self, clippy::unnecessary_wraps)] + #[expect(clippy::unused_self)] fn highlight_impl( &self, theme: &CodeTheme, diff --git a/crates/egui_kittest/tests/menu.rs b/crates/egui_kittest/tests/menu.rs index a01e39dca..fc19e8040 100644 --- a/crates/egui_kittest/tests/menu.rs +++ b/crates/egui_kittest/tests/menu.rs @@ -99,7 +99,7 @@ fn menu_close_on_click_outside() { harness.run(); harness - .get_by_label("Submenu C (CloseOnClickOutside)") + .get_by_label_contains("Submenu C (CloseOnClickOutside)") .hover(); harness.run(); @@ -133,7 +133,7 @@ fn menu_close_on_click() { harness.get_by_label("Menu A").simulate_click(); harness.run(); - harness.get_by_label("Submenu B with icon").hover(); + harness.get_by_label_contains("Submenu B with icon").hover(); harness.run(); // Clicking the button should close the menu (even if ui.close() is not called by the button) @@ -154,7 +154,9 @@ fn clicking_submenu_button_should_never_close_menu() { harness.run(); // Clicking the submenu button should not close the menu - harness.get_by_label("Submenu B with icon").simulate_click(); + harness + .get_by_label_contains("Submenu B with icon") + .simulate_click(); harness.run(); harness.get_by_label("Button in Submenu B").simulate_click(); @@ -177,12 +179,12 @@ fn menu_snapshots() { results.add(harness.try_snapshot("menu/opened")); harness - .get_by_label("Submenu C (CloseOnClickOutside)") + .get_by_label_contains("Submenu C (CloseOnClickOutside)") .hover(); harness.run(); results.add(harness.try_snapshot("menu/submenu")); - harness.get_by_label("Submenu D").hover(); + harness.get_by_label_contains("Submenu D").hover(); harness.run(); results.add(harness.try_snapshot("menu/subsubmenu")); } diff --git a/tests/egui_tests/tests/snapshots/grow_all.png b/tests/egui_tests/tests/snapshots/grow_all.png new file mode 100644 index 000000000..7ef69ea16 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/grow_all.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34f0c49cef96c7c3d08dbe835efd9366a4ced6ad2c6aa7facb0de08fd1a44648 +size 14011 diff --git a/tests/egui_tests/tests/snapshots/layout/atoms_image.png b/tests/egui_tests/tests/snapshots/layout/atoms_image.png new file mode 100644 index 000000000..3d9efa8a1 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/layout/atoms_image.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39a13fdac498d6f851a28ea3ca19d523235d5e0ab8e765ea980cf8fb2f64ba35 +size 387619 diff --git a/tests/egui_tests/tests/snapshots/layout/atoms_minimal.png b/tests/egui_tests/tests/snapshots/layout/atoms_minimal.png new file mode 100644 index 000000000..1eb0a8348 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/layout/atoms_minimal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a09e926d25e2b6f63dc6df00ab5e5b76745aae1f288231f1a602421b2bbb53b +size 384721 diff --git a/tests/egui_tests/tests/snapshots/layout/atoms_multi_grow.png b/tests/egui_tests/tests/snapshots/layout/atoms_multi_grow.png new file mode 100644 index 000000000..87211765f --- /dev/null +++ b/tests/egui_tests/tests/snapshots/layout/atoms_multi_grow.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14a1dc826aeced98cab1413f915dcbbe904b5b1eadfc4d811232bc8ccbe7f550 +size 299556 diff --git a/tests/egui_tests/tests/snapshots/layout/button_image.png b/tests/egui_tests/tests/snapshots/layout/button_image.png index 737f0670c..fb6ff3b34 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:975c279d6da2a2cb000df72bf5d9f3bdd200bb20adc00e29e8fd9ed4d2c6f6b1 -size 340923 +oid sha256:01309596ac9eb90b2dfc00074cfd39d26e3f6d1f83299f227cb4bbea9ccd3b66 +size 339917 diff --git a/tests/egui_tests/tests/snapshots/layout/checkbox_checked.png b/tests/egui_tests/tests/snapshots/layout/checkbox_checked.png index 66ae8115f..9c6fb4c07 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:aaf9b032037d0708894e568cc8e256b32be9cfb586eaffdc6167143b85562b37 -size 415016 +oid sha256:1d842f88b6a94f19aa59bdae9dbbf42f4662aaead1b8f73ac0194f183112e1b8 +size 415066 diff --git a/tests/egui_tests/tests/snapshots/max_width.png b/tests/egui_tests/tests/snapshots/max_width.png new file mode 100644 index 000000000..bab2f3876 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/max_width.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90cfa6e9be28ef538491ad94615e162ecc107df6a320084ec30840a75660ac35 +size 8759 diff --git a/tests/egui_tests/tests/snapshots/max_width_and_grow.png b/tests/egui_tests/tests/snapshots/max_width_and_grow.png new file mode 100644 index 000000000..077bccbd8 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/max_width_and_grow.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:effb4a69a7a6af12614be59a0afb0be2d2ebad402da3d7ee99fa25ae350bf4a0 +size 8761 diff --git a/tests/egui_tests/tests/snapshots/shrink_first_text.png b/tests/egui_tests/tests/snapshots/shrink_first_text.png new file mode 100644 index 000000000..c9196ea24 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/shrink_first_text.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf5032b2a08f993ae023934715222fe8d35a3a2e5cc09026d9e7ea3c296a9dc7 +size 11609 diff --git a/tests/egui_tests/tests/snapshots/shrink_last_text.png b/tests/egui_tests/tests/snapshots/shrink_last_text.png new file mode 100644 index 000000000..038b70a2f --- /dev/null +++ b/tests/egui_tests/tests/snapshots/shrink_last_text.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:84d0c37a198fb56d8608a201dbe7ad19e7de7802bd5110316b36228e14b5f330 +size 12140 diff --git a/tests/egui_tests/tests/snapshots/size_max_size.png b/tests/egui_tests/tests/snapshots/size_max_size.png new file mode 100644 index 000000000..3ea8feab0 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/size_max_size.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c6a7555290f6121d6e48657e3ae810976b540ee9328909aca2d6c078b3d76ab4 +size 8735 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 3ff34c6be..114baa35d 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:09c5904877c8895d3ad41b7082019ef87db40c6a91ad47401bb9b8ac79a62bdc -size 12914 +oid sha256:f9151f1c9d8a769ac2143a684cabf5d9ed1e453141fff555da245092003f1df1 +size 13563 diff --git a/tests/egui_tests/tests/test_atoms.rs b/tests/egui_tests/tests/test_atoms.rs new file mode 100644 index 000000000..abc9f2d05 --- /dev/null +++ b/tests/egui_tests/tests/test_atoms.rs @@ -0,0 +1,71 @@ +use egui::{Align, AtomExt as _, Button, Layout, TextWrapMode, Ui, Vec2}; +use egui_kittest::{HarnessBuilder, SnapshotResult, SnapshotResults}; + +#[test] +fn test_atoms() { + let mut results = SnapshotResults::new(); + + results.add(single_test("max_width", |ui| { + ui.add(Button::new(( + "max width not grow".atom_max_width(30.0), + "other text", + ))); + })); + results.add(single_test("max_width_and_grow", |ui| { + ui.add(Button::new(( + "max width and grow".atom_max_width(30.0).atom_grow(true), + "other text", + ))); + })); + results.add(single_test("shrink_first_text", |ui| { + ui.style_mut().wrap_mode = Some(TextWrapMode::Truncate); + ui.add(Button::new(("this should shrink", "this shouldn't"))); + })); + results.add(single_test("shrink_last_text", |ui| { + ui.style_mut().wrap_mode = Some(TextWrapMode::Truncate); + ui.add(Button::new(( + "this shouldn't shrink", + "this should".atom_shrink(true), + ))); + })); + results.add(single_test("grow_all", |ui| { + ui.style_mut().wrap_mode = Some(TextWrapMode::Truncate); + ui.add(Button::new(( + "I grow".atom_grow(true), + "I also grow".atom_grow(true), + "I grow as well".atom_grow(true), + ))); + })); + results.add(single_test("size_max_size", |ui| { + ui.style_mut().wrap_mode = Some(TextWrapMode::Truncate); + ui.add(Button::new(( + "size and max size" + .atom_size(Vec2::new(80.0, 80.0)) + .atom_max_size(Vec2::new(20.0, 20.0)), + "other text".atom_grow(true), + ))); + })); +} + +fn single_test(name: &str, mut f: impl FnMut(&mut Ui)) -> SnapshotResult { + let mut harness = HarnessBuilder::default() + .with_size(Vec2::new(400.0, 200.0)) + .build_ui(move |ui| { + ui.label("Normal"); + let normal_width = ui.horizontal(&mut f).response.rect.width(); + + ui.label("Justified"); + ui.with_layout( + Layout::left_to_right(Align::Min).with_main_justify(true), + &mut f, + ); + + ui.label("Shrunk"); + ui.scope(|ui| { + ui.set_max_width(normal_width / 2.0); + f(ui); + }); + }); + + harness.try_snapshot(name) +} diff --git a/tests/egui_tests/tests/test_widgets.rs b/tests/egui_tests/tests/test_widgets.rs index 96015cbd9..110eff810 100644 --- a/tests/egui_tests/tests/test_widgets.rs +++ b/tests/egui_tests/tests/test_widgets.rs @@ -1,8 +1,8 @@ use egui::load::SizedTexture; use egui::{ - include_image, Align, Button, Color32, ColorImage, Direction, DragValue, Event, Grid, Layout, - PointerButton, Pos2, Response, Slider, Stroke, StrokeKind, TextWrapMode, TextureHandle, - TextureOptions, Ui, UiBuilder, Vec2, Widget as _, + include_image, Align, AtomExt as _, AtomLayout, Button, Color32, ColorImage, Direction, + DragValue, Event, Grid, IntoAtoms as _, Layout, PointerButton, Pos2, Response, Slider, Stroke, + StrokeKind, TextWrapMode, TextureHandle, TextureOptions, Ui, UiBuilder, Vec2, Widget as _, }; use egui_kittest::kittest::{by, Node, Queryable as _}; use egui_kittest::{Harness, SnapshotResult, SnapshotResults}; @@ -92,6 +92,25 @@ fn widget_tests() { }, &mut results, ); + + let source = include_image!("../../../crates/eframe/data/icon.png"); + let interesting_atoms = vec![ + ("minimal", ("Hello World!").into_atoms()), + ( + "image", + (source.clone().atom_max_height(12.0), "With Image").into_atoms(), + ), + ( + "multi_grow", + ("g".atom_grow(true), "2", "g".atom_grow(true), "4").into_atoms(), + ), + ]; + + for atoms in interesting_atoms { + results.add(test_widget_layout(&format!("atoms_{}", atoms.0), |ui| { + AtomLayout::new(atoms.1.clone()).ui(ui) + })); + } } fn test_widget(name: &str, mut w: impl FnMut(&mut Ui) -> Response, results: &mut SnapshotResults) { From 5bc19f3ca3b5725f0f8219c7d45fddbaca3dca6f Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Fri, 13 Jun 2025 13:54:07 +0200 Subject: [PATCH 117/129] Report image alt text as text if widget contains no other text (#7142) - Same as https://github.com/emilk/egui/pull/7136 but now for atomics --- crates/egui/src/atomics/atoms.rs | 9 +++++++++ tests/egui_tests/tests/regression_tests.rs | 14 ++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 tests/egui_tests/tests/regression_tests.rs diff --git a/crates/egui/src/atomics/atoms.rs b/crates/egui/src/atomics/atoms.rs index 3752ace70..635b2a132 100644 --- a/crates/egui/src/atomics/atoms.rs +++ b/crates/egui/src/atomics/atoms.rs @@ -42,6 +42,15 @@ impl<'a> Atoms<'a> { } } } + + // If there is no text, try to find an image with alt text. + if string.is_none() { + string = self.iter().find_map(|a| match &a.kind { + AtomKind::Image(image) => image.alt_text.as_deref().map(Cow::Borrowed), + _ => None, + }); + } + string } diff --git a/tests/egui_tests/tests/regression_tests.rs b/tests/egui_tests/tests/regression_tests.rs new file mode 100644 index 000000000..d67759427 --- /dev/null +++ b/tests/egui_tests/tests/regression_tests.rs @@ -0,0 +1,14 @@ +use egui::{include_image, Image}; +use egui_kittest::kittest::Queryable as _; +use egui_kittest::Harness; + +#[test] +fn image_button_should_have_alt_text() { + let harness = Harness::new_ui(|ui| { + _ = ui.button( + Image::new(include_image!("../../../crates/eframe/data/icon.png")).alt_text("Egui"), + ); + }); + + harness.get_by_label("Egui"); +} From 4c04996a72e6dbff67db5cca6bee3682952dfc3e Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Fri, 13 Jun 2025 14:06:50 +0200 Subject: [PATCH 118/129] Fix missing repaint after `consume_key` (#7134) Usually input events automatically trigger a repaint. But since consume_key would remove the event egui would think there were no events and not trigger a repaint. This fixes it by setting a flag on InputState on consume_key. * related: https://github.com/rerun-io/rerun/issues/10165 --- crates/egui/src/context.rs | 8 +++++--- crates/egui/src/input_state/mod.rs | 8 ++++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 3204927b8..27a3fcd9b 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -492,6 +492,7 @@ impl ContextImpl { pixels_per_point, self.memory.options.input_options, ); + let repaint_after = viewport.input.wants_repaint_after(); let screen_rect = viewport.input.screen_rect; @@ -553,6 +554,10 @@ impl ContextImpl { } self.update_fonts_mut(); + + if let Some(delay) = repaint_after { + self.request_repaint_after(delay, viewport_id, RepaintCause::new()); + } } /// Load fonts unless already loaded. @@ -2398,10 +2403,7 @@ impl ContextImpl { if repaint_needed { self.request_repaint(ended_viewport_id, RepaintCause::new()); - } else if let Some(delay) = viewport.input.wants_repaint_after() { - self.request_repaint_after(delay, ended_viewport_id, RepaintCause::new()); } - // ------------------- let all_viewport_ids = self.all_viewport_ids(); diff --git a/crates/egui/src/input_state/mod.rs b/crates/egui/src/input_state/mod.rs index c871a8452..7fd073167 100644 --- a/crates/egui/src/input_state/mod.rs +++ b/crates/egui/src/input_state/mod.rs @@ -597,10 +597,14 @@ impl InputState { (self.time - self.last_scroll_time) as f32 } - /// The [`crate::Context`] will call this at the end of each frame to see if we need a repaint. + /// The [`crate::Context`] will call this at the beginning of each frame to see if we need a repaint. /// /// Returns how long to wait for a repaint. - pub fn wants_repaint_after(&self) -> Option { + /// + /// NOTE: It's important to call this immediately after [`Self::begin_pass`] since calls to + /// [`Self::consume_key`] will remove events from the vec, meaning those key presses wouldn't + /// cause a repaint. + pub(crate) fn wants_repaint_after(&self) -> Option { if self.pointer.wants_repaint() || self.unprocessed_scroll_delta.abs().max_elem() > 0.2 || self.unprocessed_scroll_delta_for_zoom.abs() > 0.2 From a126be4dc15f30d692a8fd176bc42972009376a9 Mon Sep 17 00:00:00 2001 From: Gerhard de Clercq <11624490+Gerharddc@users.noreply.github.com> Date: Mon, 16 Jun 2025 01:28:04 +0200 Subject: [PATCH 119/129] Mention VTK 3D integration example (#7086) This commit adds a reference to an additional 3D integration example (using the VTK C++ library) to the README. ![Demo](https://github.com/user-attachments/assets/99cbe4e6-0aaf-41c2-9b18-179d58047284) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4f1e62c4e..0bcff697e 100644 --- a/README.md +++ b/README.md @@ -312,6 +312,7 @@ You can also render your 3D scene to a texture and display it using [`ui.image( Examples: * Using [`egui-miniquad`]( https://github.com/not-fl3/egui-miniquad): https://github.com/not-fl3/egui-miniquad/blob/master/examples/render_to_egui_image.rs +* Using [`eframe`](https://github.com/emilk/egui/tree/main/crates/eframe) + [`VTK (C++)`](https://vtk.org/): https://github.com/Gerharddc/vtk-egui-demo ## Other From 742da95bd7a6f18c1aacc49fb1ce93c537e38576 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Mon, 16 Jun 2025 02:28:27 +0300 Subject: [PATCH 120/129] Support for Back button Key on Android (#7073) When your press a Back button on Android (for example at `native-activity`), [Winit translates this key](https://github.com/rust-windowing/winit/blob/47b938dbe78702d521c2c7a43b6f741a3bb8cb0b/src/platform_impl/android/keycodes.rs#L237C42-L237C53) as `NamedKey::BrowserBack`. Added convertion to `Key::Escape` at `egui-winit` module. --------- Co-authored-by: Advocat --- crates/egui-winit/src/lib.rs | 2 ++ crates/egui/src/data/key.rs | 13 ++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index c196acaa9..f205c3108 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -1144,6 +1144,8 @@ fn key_from_named_key(named_key: winit::keyboard::NamedKey) -> Option NamedKey::F33 => Key::F33, NamedKey::F34 => Key::F34, NamedKey::F35 => Key::F35, + + NamedKey::BrowserBack => Key::BrowserBack, _ => { log::trace!("Unknown key: {named_key:?}"); return None; diff --git a/crates/egui/src/data/key.rs b/crates/egui/src/data/key.rs index 602ec5bda..2a0f33fc3 100644 --- a/crates/egui/src/data/key.rs +++ b/crates/egui/src/data/key.rs @@ -183,6 +183,11 @@ pub enum Key { F33, F34, F35, + + /// Back navigation key from multimedia keyboard. + /// Android sends this key on Back button press. + /// Does not work on Web. + BrowserBack, // When adding keys, remember to also update: // * crates/egui-winit/src/lib.rs // * Key::ALL @@ -307,6 +312,8 @@ impl Key { Self::F33, Self::F34, Self::F35, + // Navigation keys: + Self::BrowserBack, ]; /// Converts `"A"` to `Key::A`, `Space` to `Key::Space`, etc. @@ -435,6 +442,8 @@ impl Key { "F34" => Self::F34, "F35" => Self::F35, + "BrowserBack" => Self::BrowserBack, + _ => return None, }) } @@ -588,6 +597,8 @@ impl Key { Self::F33 => "F33", Self::F34 => "F34", Self::F35 => "F35", + + Self::BrowserBack => "BrowserBack", } } } @@ -596,7 +607,7 @@ impl Key { fn test_key_from_name() { assert_eq!( Key::ALL.len(), - Key::F35 as usize + 1, + Key::BrowserBack as usize + 1, "Some keys are missing in Key::ALL" ); From f33ff2c83d5fe38592ed3f93182b67cdb6cf77ef Mon Sep 17 00:00:00 2001 From: Nicolas Date: Mon, 16 Jun 2025 01:40:42 +0200 Subject: [PATCH 121/129] Make `HSVA` derive serde (#7132) This pull request introduces a change to the `Hsva` struct in the `crates/ecolor/src/hsva.rs` file to enable serialization and deserialization when the `serde` feature is enabled. * Closes * [x] I have followed the instructions in the PR template --- crates/ecolor/src/hsva.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/ecolor/src/hsva.rs b/crates/ecolor/src/hsva.rs index 02ffd67d6..8388e4139 100644 --- a/crates/ecolor/src/hsva.rs +++ b/crates/ecolor/src/hsva.rs @@ -4,6 +4,7 @@ use crate::{ /// Hue, saturation, value, alpha. All in the range [0, 1]. /// No premultiplied alpha. +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[derive(Clone, Copy, Debug, Default, PartialEq)] pub struct Hsva { /// hue 0-1 From df2c16ef0a895dac6d269f36f419f6e495e16c15 Mon Sep 17 00:00:00 2001 From: Patrick Marks Date: Mon, 16 Jun 2025 01:42:01 +0200 Subject: [PATCH 122/129] Add anchored text rotation method, and clarify related docs (#7130) Add a helper method to perform rotation about a specified anchor. * Closes #7051 --- .../src/demo/misc_demo_window.rs | 102 +++++++++++++++++- .../tests/snapshots/demos/Misc Demos.png | 4 +- crates/epaint/src/shapes/text_shape.rs | 15 ++- 3 files changed, 116 insertions(+), 5 deletions(-) diff --git a/crates/egui_demo_lib/src/demo/misc_demo_window.rs b/crates/egui_demo_lib/src/demo/misc_demo_window.rs index edb19c3ea..99d85aeeb 100644 --- a/crates/egui_demo_lib/src/demo/misc_demo_window.rs +++ b/crates/egui_demo_lib/src/demo/misc_demo_window.rs @@ -1,8 +1,8 @@ use super::{Demo, View}; use egui::{ - vec2, Align, Checkbox, CollapsingHeader, Color32, Context, FontId, Resize, RichText, Sense, - Slider, Stroke, TextFormat, TextStyle, Ui, Vec2, Window, + vec2, Align, Align2, Checkbox, CollapsingHeader, Color32, ComboBox, Context, FontId, Resize, + RichText, Sense, Slider, Stroke, TextFormat, TextStyle, Ui, Vec2, Window, }; /// Showcase some ui code @@ -16,6 +16,7 @@ pub struct MiscDemoWindow { custom_collapsing_header: CustomCollapsingHeader, tree: Tree, box_painting: BoxPainting, + text_rotation: TextRotation, dummy_bool: bool, dummy_usize: usize, @@ -32,6 +33,7 @@ impl Default for MiscDemoWindow { custom_collapsing_header: Default::default(), tree: Tree::demo(), box_painting: Default::default(), + text_rotation: Default::default(), dummy_bool: false, dummy_usize: 0, @@ -79,6 +81,10 @@ impl View for MiscDemoWindow { }); }); + CollapsingHeader::new("Text rotation") + .default_open(false) + .show(ui, |ui| self.text_rotation.ui(ui)); + CollapsingHeader::new("Colors") .default_open(false) .show(ui, |ui| { @@ -729,3 +735,95 @@ fn text_layout_demo(ui: &mut Ui) { ui.label(job); } + +// ---------------------------------------------------------------------------- + +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "serde", serde(default))] +struct TextRotation { + size: Vec2, + angle: f32, + align: egui::Align2, +} + +impl Default for TextRotation { + fn default() -> Self { + Self { + size: vec2(200.0, 200.0), + angle: 0.0, + align: egui::Align2::LEFT_TOP, + } + } +} + +impl TextRotation { + pub fn ui(&mut self, ui: &mut Ui) { + ui.add(Slider::new(&mut self.angle, 0.0..=2.0 * std::f32::consts::PI).text("angle")); + + let default_color = if ui.visuals().dark_mode { + Color32::LIGHT_GRAY + } else { + Color32::DARK_GRAY + }; + + let aligns = [ + (Align2::LEFT_TOP, "LEFT_TOP"), + (Align2::LEFT_CENTER, "LEFT_CENTER"), + (Align2::LEFT_BOTTOM, "LEFT_BOTTOM"), + (Align2::CENTER_TOP, "CENTER_TOP"), + (Align2::CENTER_CENTER, "CENTER_CENTER"), + (Align2::CENTER_BOTTOM, "CENTER_BOTTOM"), + (Align2::RIGHT_TOP, "RIGHT_TOP"), + (Align2::RIGHT_CENTER, "RIGHT_CENTER"), + (Align2::RIGHT_BOTTOM, "RIGHT_BOTTOM"), + ]; + + ComboBox::new("anchor", "Anchor") + .selected_text(aligns.iter().find(|(a, _)| *a == self.align).unwrap().1) + .show_ui(ui, |ui| { + for (align2, name) in &aligns { + ui.selectable_value(&mut self.align, *align2, *name); + } + }); + + ui.horizontal_wrapped(|ui| { + let (response, painter) = ui.allocate_painter(self.size, Sense::empty()); + let rect = response.rect; + + let start_pos = self.size / 2.0; + + let s = ui.ctx().fonts(|f| { + let mut t = egui::Shape::text( + f, + rect.min + start_pos, + egui::Align2::LEFT_TOP, + "sample_text", + egui::FontId::new(12.0, egui::FontFamily::Proportional), + default_color, + ); + + if let egui::epaint::Shape::Text(ts) = &mut t { + let new = ts.clone().with_angle_and_anchor(self.angle, self.align); + *ts = new; + }; + + t + }); + + if let egui::epaint::Shape::Text(ts) = &s { + let align_pt = + rect.min + start_pos + self.align.pos_in_rect(&ts.galley.rect).to_vec2(); + painter.circle(align_pt, 2.0, Color32::RED, (0.0, Color32::RED)); + }; + + painter.rect( + rect, + 0.0, + default_color.gamma_multiply(0.3), + (0.0, Color32::BLACK), + egui::StrokeKind::Middle, + ); + painter.add(s); + }); + } +} 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 267fa6be7..f11c5d8a7 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:880344367ed65f83898ceca4843b1b6259d1690242ced0d29ac8dc48100a8faa -size 62956 +oid sha256:116a53258be27d9c7c56538e5f83202ea731f19887fabadc0449d24fde4d80d9 +size 64494 diff --git a/crates/epaint/src/shapes/text_shape.rs b/crates/epaint/src/shapes/text_shape.rs index 4ea0ac352..bf9db964b 100644 --- a/crates/epaint/src/shapes/text_shape.rs +++ b/crates/epaint/src/shapes/text_shape.rs @@ -1,5 +1,7 @@ use std::sync::Arc; +use emath::{Align2, Rot2}; + use crate::*; /// How to paint some text on screen. @@ -78,7 +80,7 @@ impl TextShape { self } - /// Rotate text by this many radians clockwise. + /// Set text rotation to `angle` radians clockwise. /// The pivot is `pos` (the upper left corner of the text). #[inline] pub fn with_angle(mut self, angle: f32) -> Self { @@ -86,6 +88,17 @@ impl TextShape { self } + /// Set the text rotation to the `angle` radians clockwise. + /// The pivot is determined by the given `anchor` point on the text bounding box. + #[inline] + pub fn with_angle_and_anchor(mut self, angle: f32, anchor: Align2) -> Self { + self.angle = angle; + let a0 = anchor.pos_in_rect(&self.galley.rect).to_vec2(); + let a1 = Rot2::from_angle(angle) * a0; + self.pos += a0 - a1; + self + } + /// Render text with this opacity in gamma space #[inline] pub fn with_opacity_factor(mut self, opacity_factor: f32) -> Self { From 54fded362dfc82f0ea188a110fc950d9adbb2f13 Mon Sep 17 00:00:00 2001 From: valadaptive <79560998+valadaptive@users.noreply.github.com> Date: Sun, 15 Jun 2025 19:53:00 -0400 Subject: [PATCH 123/129] Clamp text cursor positions in the same places where we used to (#7081) Closes #7077. This fixes the problem shown in #7077 where clearing a `TextEdit` wouldn't reset its cursor position. I've fixed that by adding back the `TextCursorState::range` method, which clamps the selection range to that of the passed `Galley`, and calling it in the same places where it was called before #5785. (/cc @juancampa) * [x] I have followed the instructions in the PR template --- .../src/text_selection/label_text_selection.rs | 8 ++++---- .../egui/src/text_selection/text_cursor_state.rs | 14 ++++++++++++-- crates/egui/src/widgets/text_edit/builder.rs | 8 ++++---- crates/epaint/src/text/text_layout_types.rs | 4 ++++ 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/crates/egui/src/text_selection/label_text_selection.rs b/crates/egui/src/text_selection/label_text_selection.rs index 9ce8fbd5b..a315c2354 100644 --- a/crates/egui/src/text_selection/label_text_selection.rs +++ b/crates/egui/src/text_selection/label_text_selection.rs @@ -530,7 +530,7 @@ impl LabelSelectionState { let mut cursor_state = self.cursor_for(ui, response, global_from_galley, galley); - let old_range = cursor_state.char_range(); + let old_range = cursor_state.range(galley); if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() { if response.contains_pointer() { @@ -544,7 +544,7 @@ impl LabelSelectionState { } } - if let Some(mut cursor_range) = cursor_state.char_range() { + if let Some(mut cursor_range) = cursor_state.range(galley) { let galley_rect = global_from_galley * Rect::from_min_size(Pos2::ZERO, galley.size()); self.selection_bbox_this_frame = self.selection_bbox_this_frame.union(galley_rect); @@ -562,7 +562,7 @@ impl LabelSelectionState { } // Look for changes due to keyboard and/or mouse interaction: - let new_range = cursor_state.char_range(); + let new_range = cursor_state.range(galley); let selection_changed = old_range != new_range; if let (true, Some(range)) = (selection_changed, new_range) { @@ -632,7 +632,7 @@ impl LabelSelectionState { } } - let cursor_range = cursor_state.char_range(); + let cursor_range = cursor_state.range(galley); let mut new_vertex_indices = vec![]; diff --git a/crates/egui/src/text_selection/text_cursor_state.rs b/crates/egui/src/text_selection/text_cursor_state.rs index 298d8abfb..d2158c6bd 100644 --- a/crates/egui/src/text_selection/text_cursor_state.rs +++ b/crates/egui/src/text_selection/text_cursor_state.rs @@ -35,6 +35,16 @@ impl TextCursorState { self.ccursor_range } + /// The currently selected range of characters, clamped within the character + /// range of the given [`Galley`]. + pub fn range(&self, galley: &Galley) -> Option { + self.ccursor_range.map(|mut range| { + range.primary = galley.clamp_cursor(&range.primary); + range.secondary = galley.clamp_cursor(&range.secondary); + range + }) + } + /// Sets the currently selected range of characters. pub fn set_char_range(&mut self, ccursor_range: Option) { self.ccursor_range = ccursor_range; @@ -69,7 +79,7 @@ impl TextCursorState { if response.hovered() && ui.input(|i| i.pointer.any_pressed()) { // The start of a drag (or a click). if ui.input(|i| i.modifiers.shift) { - if let Some(mut cursor_range) = self.char_range() { + if let Some(mut cursor_range) = self.range(galley) { cursor_range.primary = cursor_at_pointer; self.set_char_range(Some(cursor_range)); } else { @@ -81,7 +91,7 @@ impl TextCursorState { true } else if is_being_dragged { // Drag to select text: - if let Some(mut cursor_range) = self.char_range() { + if let Some(mut cursor_range) = self.range(galley) { cursor_range.primary = cursor_at_pointer; self.set_char_range(Some(cursor_range)); } diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 29f4b2cbb..e21c512a9 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -617,7 +617,7 @@ impl TextEdit<'_> { } let mut cursor_range = None; - let prev_cursor_range = state.cursor.char_range(); + 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)); @@ -720,7 +720,7 @@ impl TextEdit<'_> { let has_focus = ui.memory(|mem| mem.has_focus(id)); if has_focus { - if let Some(cursor_range) = state.cursor.char_range() { + if let Some(cursor_range) = state.cursor.range(&galley) { // Add text selection rectangles to the galley: paint_text_selection(&mut galley, ui.visuals(), &cursor_range, None); } @@ -742,7 +742,7 @@ impl TextEdit<'_> { painter.galley(galley_pos, galley.clone(), text_color); if has_focus { - if let Some(cursor_range) = state.cursor.char_range() { + if let Some(cursor_range) = state.cursor.range(&galley) { let primary_cursor_rect = cursor_rect(&galley, &cursor_range.primary, row_height) .translate(galley_pos.to_vec2()); @@ -898,7 +898,7 @@ fn events( ) -> (bool, CCursorRange) { let os = ui.ctx().os(); - let mut cursor_range = state.cursor.char_range().unwrap_or(default_cursor_range); + let mut cursor_range = state.cursor.range(galley).unwrap_or(default_cursor_range); // We feed state to the undoer both before and after handling input // so that the undoer creates automatic saves even when there are no events for a while. diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 6b7863426..f7e11911b 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -1104,6 +1104,10 @@ impl Galley { } } + pub fn clamp_cursor(&self, cursor: &CCursor) -> CCursor { + self.cursor_from_layout(self.layout_from_cursor(*cursor)) + } + pub fn cursor_up_one_row( &self, cursor: &CCursor, From 96c34139fdf59b522b7816609f85f4a3ffe8e5e6 Mon Sep 17 00:00:00 2001 From: Azkellas Date: Mon, 16 Jun 2025 02:11:26 +0200 Subject: [PATCH 124/129] Select all text in DragValue when gaining focus via keyboard (#7107) Previously, the `DragValue` widget selected all text when focus was gained via a mouse click, but didn't when focus was gained via keyboard. https://github.com/user-attachments/assets/5e82ca2c-0214-4201-ad2d-056dabc05e92 This PR makes both gained focus behaving the same way by selecting the text on focus gained via keyboard. https://github.com/user-attachments/assets/f246c779-3368-428c-a6b2-cec20dbc20a6 - [x] I have followed the instructions in the PR template Co-authored-by: Emil Ernerfeldt --- crates/egui/src/widgets/drag_value.rs | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/crates/egui/src/widgets/drag_value.rs b/crates/egui/src/widgets/drag_value.rs index 864222ae1..a9d971916 100644 --- a/crates/egui/src/widgets/drag_value.rs +++ b/crates/egui/src/widgets/drag_value.rs @@ -3,7 +3,7 @@ use std::{cmp::Ordering, ops::RangeInclusive}; use crate::{ - emath, text, Button, CursorIcon, Key, Modifiers, NumExt as _, Response, RichText, Sense, + emath, text, Button, CursorIcon, Id, Key, Modifiers, NumExt as _, Response, RichText, Sense, TextEdit, TextWrapMode, Ui, Widget, WidgetInfo, MINUS_CHAR_STR, }; @@ -569,6 +569,11 @@ impl Widget for DragValue<'_> { .font(text_style), ); + // Select all text when the edit gains focus. + if ui.memory_mut(|mem| mem.gained_focus(id)) { + select_all_text(ui, id, response.id, &value_text); + } + let update = if update_while_editing { // Update when the edit content has changed. response.changed() @@ -623,12 +628,7 @@ impl Widget for DragValue<'_> { if response.clicked() { ui.data_mut(|data| data.remove::(id)); ui.memory_mut(|mem| mem.request_focus(id)); - let mut state = TextEdit::load_state(ui.ctx(), id).unwrap_or_default(); - state.cursor.set_char_range(Some(text::CCursorRange::two( - text::CCursor::default(), - text::CCursor::new(value_text.chars().count()), - ))); - state.store(ui.ctx(), response.id); + select_all_text(ui, id, response.id, &value_text); } else if response.dragged() { ui.ctx().set_cursor_icon(cursor_icon); @@ -759,6 +759,16 @@ pub(crate) fn clamp_value_to_range(x: f64, range: RangeInclusive) -> f64 { } } +/// Select all text in the `DragValue` text edit widget. +fn select_all_text(ui: &Ui, widget_id: Id, response_id: Id, value_text: &str) { + let mut state = TextEdit::load_state(ui.ctx(), widget_id).unwrap_or_default(); + state.cursor.set_char_range(Some(text::CCursorRange::two( + text::CCursor::default(), + text::CCursor::new(value_text.chars().count()), + ))); + state.store(ui.ctx(), response_id); +} + #[cfg(test)] mod tests { use super::clamp_value_to_range; From 699be07978b80f439d20c16232096cce110c8538 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 15 Jun 2025 18:01:48 -0700 Subject: [PATCH 125/129] Add Vec2::ONE --- crates/emath/src/vec2.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/emath/src/vec2.rs b/crates/emath/src/vec2.rs index 9a173348b..4771343e8 100644 --- a/crates/emath/src/vec2.rs +++ b/crates/emath/src/vec2.rs @@ -140,6 +140,7 @@ impl Vec2 { pub const DOWN: Self = Self { x: 0.0, y: 1.0 }; pub const ZERO: Self = Self { x: 0.0, y: 0.0 }; + pub const ONE: Self = Self { x: 1.0, y: 1.0 }; pub const INFINITY: Self = Self::splat(f32::INFINITY); pub const NAN: Self = Self::splat(f32::NAN); From 06760e1b0869459ef78840c2a380f594e7a9d1e3 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 16 Jun 2025 08:30:46 +0200 Subject: [PATCH 126/129] Change API of `Tooltip` slightly (#7151) We try to be consistent with our parameter order to reduce surprise for users. I also renamed a few things to clarify what is what --- CONTRIBUTING.md | 3 +- crates/egui/src/containers/old_popup.rs | 6 ++-- crates/egui/src/containers/tooltip.rs | 39 ++++++++++++++----------- 3 files changed, 27 insertions(+), 21 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 613de20c5..7ddedc378 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,7 +35,7 @@ You can test your code locally by running `./scripts/check.sh`. There are snapshots test that might need to be updated. Run the tests with `UPDATE_SNAPSHOTS=true cargo test --workspace --all-features` to update all of them. If CI keeps complaining about snapshots (which could happen if you don't use macOS, snapshots in CI are currently -rendered with macOS), you can instead run `./scripts/update_snapshots_from_ci.sh` to update your local snapshots from +rendered with macOS), you can instead run `./scripts/update_snapshots_from_ci.sh` to update your local snapshots from the last CI run of your PR (which will download the `test_results` artefact). For more info about the tests see [egui_kittest](./crates/egui_kittest/README.md). Snapshots and other big files are stored with git lfs. See [Working with git lfs](#working-with-git-lfs) for more info. @@ -125,6 +125,7 @@ While using an immediate mode gui is simple, implementing one is a lot more tric * Flip `if !condition {} else {}` * Sets of things should be lexicographically sorted (e.g. crate dependencies in `Cargo.toml`) * Put each type in their own file, unless they are trivial (e.g. a `struct` with no `impl`) +* Put most generic arguments first (e.g. `Context`), and most specific last * Break the above rules when it makes sense diff --git a/crates/egui/src/containers/old_popup.rs b/crates/egui/src/containers/old_popup.rs index 3e5de650f..cc75494e1 100644 --- a/crates/egui/src/containers/old_popup.rs +++ b/crates/egui/src/containers/old_popup.rs @@ -61,7 +61,7 @@ pub fn show_tooltip_at_pointer( widget_id: Id, add_contents: impl FnOnce(&mut Ui) -> R, ) -> Option { - Tooltip::new(widget_id, ctx.clone(), PopupAnchor::Pointer, parent_layer) + Tooltip::new(ctx.clone(), parent_layer, widget_id, PopupAnchor::Pointer) .gap(12.0) .show(add_contents) .map(|response| response.inner) @@ -78,7 +78,7 @@ pub fn show_tooltip_for( widget_rect: &Rect, add_contents: impl FnOnce(&mut Ui) -> R, ) -> Option { - Tooltip::new(widget_id, ctx.clone(), *widget_rect, parent_layer) + Tooltip::new(ctx.clone(), parent_layer, widget_id, *widget_rect) .show(add_contents) .map(|response| response.inner) } @@ -94,7 +94,7 @@ pub fn show_tooltip_at( suggested_position: Pos2, add_contents: impl FnOnce(&mut Ui) -> R, ) -> Option { - Tooltip::new(widget_id, ctx.clone(), suggested_position, parent_layer) + Tooltip::new(ctx.clone(), parent_layer, widget_id, suggested_position) .show(add_contents) .map(|response| response.inner) } diff --git a/crates/egui/src/containers/tooltip.rs b/crates/egui/src/containers/tooltip.rs index c85739d75..a6cb3199e 100644 --- a/crates/egui/src/containers/tooltip.rs +++ b/crates/egui/src/containers/tooltip.rs @@ -7,26 +7,31 @@ use emath::Vec2; pub struct Tooltip<'a> { pub popup: Popup<'a>, - layer_id: LayerId, - widget_id: Id, + + /// The layer of the parent widget. + parent_layer: LayerId, + + /// The id of the widget that owns this tooltip. + parent_widget: Id, } impl Tooltip<'_> { - /// Show a tooltip that is always open + /// Show a tooltip that is always open. pub fn new( - widget_id: Id, ctx: Context, + parent_layer: LayerId, + parent_widget: Id, anchor: impl Into, - layer_id: LayerId, ) -> Self { + let width = ctx.style().spacing.tooltip_width; Self { - // TODO(lucasmerlin): Set width somehow (we're missing context here) - popup: Popup::new(widget_id, ctx, anchor.into(), layer_id) + popup: Popup::new(parent_widget, ctx, anchor.into(), parent_layer) .kind(PopupKind::Tooltip) .gap(4.0) + .width(width) .sense(Sense::hover()), - layer_id, - widget_id, + parent_layer, + parent_widget, } } @@ -39,8 +44,8 @@ impl Tooltip<'_> { .sense(Sense::hover()); Self { popup, - layer_id: response.layer_id, - widget_id: response.id, + parent_layer: response.layer_id, + parent_widget: response.id, } } @@ -96,8 +101,8 @@ impl Tooltip<'_> { pub fn show(self, content: impl FnOnce(&mut crate::Ui) -> R) -> Option> { let Self { mut popup, - layer_id: parent_layer, - widget_id, + parent_layer, + parent_widget, } = self; if !popup.is_open() { @@ -111,11 +116,11 @@ impl Tooltip<'_> { fs.layers .entry(parent_layer) .or_default() - .widget_with_tooltip = Some(widget_id); + .widget_with_tooltip = Some(parent_widget); fs.tooltips .widget_tooltips - .get(&widget_id) + .get(&parent_widget) .copied() .unwrap_or(PerWidgetTooltipState { bounding_rect: rect, @@ -123,7 +128,7 @@ impl Tooltip<'_> { }) }); - let tooltip_area_id = Self::tooltip_id(widget_id, state.tooltip_count); + let tooltip_area_id = Self::tooltip_id(parent_widget, state.tooltip_count); popup = popup.anchor(state.bounding_rect).id(tooltip_area_id); let response = popup.show(|ui| { @@ -144,7 +149,7 @@ impl Tooltip<'_> { response .response .ctx - .pass_state_mut(|fs| fs.tooltips.widget_tooltips.insert(widget_id, state)); + .pass_state_mut(|fs| fs.tooltips.widget_tooltips.insert(parent_widget, state)); Self::remember_that_tooltip_was_shown(&response.response.ctx); } From 5194c0df3ef162ec6e2df9bb29bbe944d6d68aa9 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Mon, 16 Jun 2025 08:42:17 +0200 Subject: [PATCH 127/129] Minor atoms improvements (#7145) Improve some lifetime bounds and add some convenience constructors --- crates/egui/src/atomics/atoms.rs | 48 ++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/crates/egui/src/atomics/atoms.rs b/crates/egui/src/atomics/atoms.rs index 635b2a132..4b19c9e26 100644 --- a/crates/egui/src/atomics/atoms.rs +++ b/crates/egui/src/atomics/atoms.rs @@ -54,15 +54,15 @@ impl<'a> Atoms<'a> { string } - pub fn iter_kinds(&'a self) -> impl Iterator> { + pub fn iter_kinds(&self) -> impl Iterator> { self.0.iter().map(|atom| &atom.kind) } - pub fn iter_kinds_mut(&'a mut self) -> impl Iterator> { + pub fn iter_kinds_mut(&mut self) -> impl Iterator> { self.0.iter_mut().map(|atom| &mut atom.kind) } - pub fn iter_images(&'a self) -> impl Iterator> { + pub fn iter_images(&self) -> impl Iterator> { self.iter_kinds().filter_map(|kind| { if let AtomKind::Image(image) = kind { Some(image) @@ -72,7 +72,7 @@ impl<'a> Atoms<'a> { }) } - pub fn iter_images_mut(&'a mut self) -> impl Iterator> { + pub fn iter_images_mut(&mut self) -> impl Iterator> { self.iter_kinds_mut().filter_map(|kind| { if let AtomKind::Image(image) = kind { Some(image) @@ -82,7 +82,7 @@ impl<'a> Atoms<'a> { }) } - pub fn iter_texts(&'a self) -> impl Iterator { + pub fn iter_texts(&self) -> impl Iterator + use<'_, 'a> { self.iter_kinds().filter_map(|kind| { if let AtomKind::Text(text) = kind { Some(text) @@ -92,7 +92,7 @@ impl<'a> Atoms<'a> { }) } - pub fn iter_texts_mut(&'a mut self) -> impl Iterator { + pub fn iter_texts_mut(&mut self) -> impl Iterator + use<'a, '_> { self.iter_kinds_mut().filter_map(|kind| { if let AtomKind::Text(text) = kind { Some(text) @@ -107,7 +107,7 @@ impl<'a> Atoms<'a> { .for_each(|atom| *atom = f(std::mem::take(atom))); } - pub fn map_kind(&'a mut self, mut f: F) + pub fn map_kind(&mut self, mut f: F) where F: FnMut(AtomKind<'a>) -> AtomKind<'a>, { @@ -116,7 +116,7 @@ impl<'a> Atoms<'a> { } } - pub fn map_images(&'a mut self, mut f: F) + pub fn map_images(&mut self, mut f: F) where F: FnMut(Image<'a>) -> Image<'a>, { @@ -129,7 +129,7 @@ impl<'a> Atoms<'a> { }); } - pub fn map_texts(&'a mut self, mut f: F) + pub fn map_texts(&mut self, mut f: F) where F: FnMut(WidgetText) -> WidgetText, { @@ -227,3 +227,33 @@ impl DerefMut for Atoms<'_> { &mut self.0 } } + +impl<'a, T: Into>> From> for Atoms<'a> { + fn from(vec: Vec) -> Self { + Atoms(vec.into_iter().map(Into::into).collect()) + } +} + +impl<'a, T: Into> + Clone> From<&[T]> for Atoms<'a> { + fn from(slice: &[T]) -> Self { + Atoms(slice.iter().cloned().map(Into::into).collect()) + } +} + +impl<'a, Item: Into>> FromIterator for Atoms<'a> { + fn from_iter>(iter: T) -> Self { + Atoms(iter.into_iter().map(Into::into).collect()) + } +} + +#[cfg(test)] +mod tests { + use crate::Atoms; + + #[test] + fn collect_atoms() { + let _: Atoms<'_> = ["Hello", "World"].into_iter().collect(); + let _ = Atoms::from(vec!["Hi"]); + let _ = Atoms::from(["Hi"].as_slice()); + } +} From 3e07862b7255883db131627aad16eaaebed57af4 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 17 Apr 2025 12:06:03 +0200 Subject: [PATCH 128/129] Correctly calculate preferred/intrinsic size in AtomLayout --- crates/egui/src/atomics/atom_kind.rs | 16 +++++++++++-- crates/epaint/src/text/text_layout_types.rs | 25 +++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/crates/egui/src/atomics/atom_kind.rs b/crates/egui/src/atomics/atom_kind.rs index 2672e646b..75b03c04a 100644 --- a/crates/egui/src/atomics/atom_kind.rs +++ b/crates/egui/src/atomics/atom_kind.rs @@ -81,9 +81,21 @@ impl<'a> AtomKind<'a> { ) -> (Vec2, SizedAtomKind<'a>) { match self { AtomKind::Text(text) => { - let galley = text.into_galley(ui, wrap_mode, available_size.x, TextStyle::Button); + let wrap_mode = wrap_mode.unwrap_or(ui.wrap_mode()); + let desired_size = matches!(wrap_mode, TextWrapMode::Truncate).then(|| { + text.clone() + .into_galley( + ui, + Some(TextWrapMode::Extend), + available_size.x, + TextStyle::Button, + ) + .desired_size() + }); + let galley = + text.into_galley(ui, Some(wrap_mode), available_size.x, TextStyle::Button); ( - galley.size(), // TODO(#5762): calculate the preferred size + desired_size.unwrap_or_else(|| galley.desired_size()), SizedAtomKind::Text(galley), ) } diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index f7e11911b..f6a182dcb 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -795,6 +795,31 @@ impl Galley { self.rect.size() } + // TODO: Instead return Option? + pub fn desired_size(&self) -> Vec2 { + let mut current_width: f32 = 0.0; + let mut widest_width: f32 = 0.0; + let mut height = self.rows.first().map_or(0.0, |row| row.height()); + for row in &self.rows { + if current_width != 0.0 { + let space = row.glyphs.last(); + if let Some(space) = space { + if space.chr.is_whitespace() { + // TODO: Needed or not? Doesn't seem like it's needed + // current_width += space.advance_width; + } + } + } + current_width += row.rect().width(); + widest_width = widest_width.max(current_width); + if row.ends_with_newline { + height += row.height(); + current_width = 0.0; + } + } + vec2(widest_width, height) + } + pub(crate) fn round_output_to_gui(&mut self) { for placed_row in &mut self.rows { // Optimization: only call `make_mut` if necessary (can cause a deep clone) From cd3ac65c0994ffd82e2b8f3b2fe3e8a860ac9ba1 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Sat, 14 Jun 2025 13:04:36 +0200 Subject: [PATCH 129/129] At least size --- crates/egui/src/atomics/atom.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui/src/atomics/atom.rs b/crates/egui/src/atomics/atom.rs index 4f4b5b750..b3336b263 100644 --- a/crates/egui/src/atomics/atom.rs +++ b/crates/egui/src/atomics/atom.rs @@ -89,7 +89,7 @@ impl<'a> Atom<'a> { SizedAtom { size, - preferred_size: preferred, + preferred_size: preferred.at_least(size), grow: self.grow, kind, }