From 501905b60df88ff84fe6ea9a0d51543b6cc4638f Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Wed, 16 Apr 2025 18:58:58 +0200 Subject: [PATCH 01/78] 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 02/78] 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 03/78] 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 04/78] 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 05/78] 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 06/78] 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 07/78] 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 08/78] 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 09/78] 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 10/78] 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 11/78] 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 12/78] `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 13/78] 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 14/78] 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 15/78] 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 16/78] 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 17/78] 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 18/78] 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 19/78] 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 20/78] 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 21/78] 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 22/78] 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 23/78] 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 24/78] 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 25/78] 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 26/78] 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 27/78] 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 28/78] 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 51/78] 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 52/78] 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 53/78] 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 54/78] 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 55/78] 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 56/78] 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 57/78] 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 58/78] 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 59/78] 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 60/78] 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 61/78] 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 62/78] 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 63/78] `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 64/78] 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 65/78] 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 66/78] 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 67/78] 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 68/78] 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 69/78] 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 70/78] 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 71/78] 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 72/78] 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 73/78] 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 74/78] 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 75/78] 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 011e0d261a36c42d3988010dbca2921c1f2bc051 Mon Sep 17 00:00:00 2001 From: Zach Bateman <39414345+zachbateman@users.noreply.github.com> Date: Mon, 16 Jun 2025 11:27:26 -0500 Subject: [PATCH 76/78] egui_extras: Enable setting DatePickerButton start and end year explicitly (#7061) Add the ability to set the `DatePickerButton`'s start and end years via new `start_year` and `end_year` methods. Continue to use the existing today - 100 years and today + 10 years behavior if a year is not specified. * This more fully closes and expands on . * [x] I have followed the instructions in the PR template --- crates/egui_extras/src/datepicker/button.rs | 15 +++++++++++++++ crates/egui_extras/src/datepicker/popup.rs | 7 ++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/crates/egui_extras/src/datepicker/button.rs b/crates/egui_extras/src/datepicker/button.rs index 601c16f67..0ec0680f8 100644 --- a/crates/egui_extras/src/datepicker/button.rs +++ b/crates/egui_extras/src/datepicker/button.rs @@ -1,6 +1,7 @@ use super::popup::DatePickerPopup; use chrono::NaiveDate; use egui::{Area, Button, Frame, InnerResponse, Key, Order, RichText, Ui, Widget}; +use std::ops::RangeInclusive; #[derive(Default, Clone)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -19,6 +20,7 @@ pub struct DatePickerButton<'a> { show_icon: bool, format: String, highlight_weekends: bool, + start_end_years: Option>, } impl<'a> DatePickerButton<'a> { @@ -33,6 +35,7 @@ impl<'a> DatePickerButton<'a> { show_icon: true, format: "%Y-%m-%d".to_owned(), highlight_weekends: true, + start_end_years: None, } } @@ -101,6 +104,17 @@ impl<'a> DatePickerButton<'a> { self.highlight_weekends = highlight_weekends; self } + + /// Set the start and end years for the date picker. (Default: today's year - 100 to today's year + 10) + /// This will limit the years you can choose from in the dropdown to the specified range. + /// + /// For example, if you want to provide the range of years from 2000 to 2035, you can use: + /// `start_end_years(2000..=2035)`. + #[inline] + pub fn start_end_years(mut self, start_end_years: RangeInclusive) -> Self { + self.start_end_years = Some(start_end_years); + self + } } impl Widget for DatePickerButton<'_> { @@ -167,6 +181,7 @@ impl Widget for DatePickerButton<'_> { calendar: self.calendar, calendar_week: self.calendar_week, highlight_weekends: self.highlight_weekends, + start_end_years: self.start_end_years, } .draw(ui) }) diff --git a/crates/egui_extras/src/datepicker/popup.rs b/crates/egui_extras/src/datepicker/popup.rs index 79f3d37f6..c63de3a91 100644 --- a/crates/egui_extras/src/datepicker/popup.rs +++ b/crates/egui_extras/src/datepicker/popup.rs @@ -35,6 +35,7 @@ pub(crate) struct DatePickerPopup<'a> { pub calendar: bool, pub calendar_week: bool, pub highlight_weekends: bool, + pub start_end_years: Option>, } impl DatePickerPopup<'_> { @@ -84,7 +85,11 @@ impl DatePickerPopup<'_> { ComboBox::from_id_salt("date_picker_year") .selected_text(popup_state.year.to_string()) .show_ui(ui, |ui| { - for year in today.year() - 100..today.year() + 10 { + let (start_year, end_year) = match &self.start_end_years { + Some(range) => (*range.start(), *range.end()), + None => (today.year() - 100, today.year() + 10), + }; + for year in start_year..=end_year { if ui .selectable_value( &mut popup_state.year, From 8c2df4802c36e3937c8af8ba90862c09c87f30af Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 16 Jun 2025 19:36:19 +0200 Subject: [PATCH 77/78] Add back old `Tooltip::new` (#7156) I was a bit too hasty in https://github.com/emilk/egui/pull/7151 and changed a public API in a breaking way, for no good reason --- crates/egui/src/containers/old_popup.rs | 6 +++--- crates/egui/src/containers/tooltip.rs | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/crates/egui/src/containers/old_popup.rs b/crates/egui/src/containers/old_popup.rs index cc75494e1..3ddf77bf6 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(ctx.clone(), parent_layer, widget_id, PopupAnchor::Pointer) + Tooltip::always_open(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(ctx.clone(), parent_layer, widget_id, *widget_rect) + Tooltip::always_open(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(ctx.clone(), parent_layer, widget_id, suggested_position) + Tooltip::always_open(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 a6cb3199e..2060c61cf 100644 --- a/crates/egui/src/containers/tooltip.rs +++ b/crates/egui/src/containers/tooltip.rs @@ -17,7 +17,25 @@ pub struct Tooltip<'a> { impl Tooltip<'_> { /// Show a tooltip that is always open. + #[deprecated = "Use `Tooltip::always_open` instead."] pub fn new( + parent_widget: Id, + ctx: Context, + anchor: impl Into, + parent_layer: LayerId, + ) -> Self { + Self { + popup: Popup::new(parent_widget, ctx, anchor.into(), parent_layer) + .kind(PopupKind::Tooltip) + .gap(4.0) + .sense(Sense::hover()), + parent_layer, + parent_widget, + } + } + + /// Show a tooltip that is always open. + pub fn always_open( ctx: Context, parent_layer: LayerId, parent_widget: Id, From 0152a875192490d482aed3890f682ec406968ea3 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 17 Jun 2025 12:17:38 +0200 Subject: [PATCH 78/78] Create custom `egui_kittest::Node` (#7138) This adds a custom Node struct with proper support for egui types (`Key`, `Modifiers`, `egui::Event`, `Rect`) instead of needing to use the kittest / accesskit types. I also changed the `click` function to do a proper mouse move / mouse down instead of the accesskit click. Also added `accesskit_click` to trigger the accesskit event. This resulted in some changed snapshots, since the elements are now hovered. Also renamed `press_key` to `key_press` for consistency with `key_down/key_up`. Also removed the Deref to the AccessKit Node, to make it clearer when to expect egui and when to expect accesskit types. * Closes #5705 * [x] I have followed the instructions in the PR template --- Cargo.lock | 2 +- .../egui_demo_app/tests/snapshots/clock.png | 4 +- .../tests/snapshots/custom3d.png | 4 +- .../tests/snapshots/easymarkeditor.png | 4 +- .../tests/snapshots/imageviewer.png | 4 +- crates/egui_demo_app/tests/test_demo_app.rs | 2 +- .../src/demo/demo_app_windows.rs | 10 +- crates/egui_demo_lib/src/demo/modals.rs | 6 +- crates/egui_demo_lib/src/demo/text_edit.rs | 7 +- crates/egui_demo_lib/src/rendering_test.rs | 5 +- crates/egui_kittest/README.md | 6 +- crates/egui_kittest/src/event.rs | 194 ------------------ crates/egui_kittest/src/lib.rs | 183 +++++++++++++---- crates/egui_kittest/src/node.rs | 162 +++++++++++++++ crates/egui_kittest/tests/menu.rs | 24 +-- crates/egui_kittest/tests/popup.rs | 2 +- crates/egui_kittest/tests/regression_tests.rs | 13 +- .../tests/snapshots/readme_example.png | 4 +- crates/egui_kittest/tests/tests.rs | 14 +- tests/egui_tests/tests/test_widgets.rs | 14 +- 20 files changed, 359 insertions(+), 305 deletions(-) delete mode 100644 crates/egui_kittest/src/event.rs create mode 100644 crates/egui_kittest/src/node.rs diff --git a/Cargo.lock b/Cargo.lock index 9a441cb30..abd6aca03 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2422,7 +2422,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "kittest" version = "0.1.0" -source = "git+https://github.com/rerun-io/kittest?branch=main#679f9ade828021295c5f86f38275d9271d001004" +source = "git+https://github.com/rerun-io/kittest?branch=main#91bf0fd98b5afe04427bb3aea4c68c6e0034b4bd" dependencies = [ "accesskit", "accesskit_consumer", diff --git a/crates/egui_demo_app/tests/snapshots/clock.png b/crates/egui_demo_app/tests/snapshots/clock.png index 2b0bfc6c1..31bb331ef 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:61db3807f755ac832ba069e1adaf8aeb550c88737b4907748667a271ae29863d -size 334792 +oid sha256:0bd688ff74f9a096edab545fbcbf61b61a464183da066ae4a120ce1e2abf3e7b +size 334969 diff --git a/crates/egui_demo_app/tests/snapshots/custom3d.png b/crates/egui_demo_app/tests/snapshots/custom3d.png index ce19412f7..2458cd8ba 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:21e0a6cdf175606a513ddf410ae1b873a9817305ecad403116fad3c6ff795fa3 -size 92185 +oid sha256:c80c4ae4c2bfbc5c91e9cd94213a4f87646fe910b4a7c747531a1efcf23def47 +size 92364 diff --git a/crates/egui_demo_app/tests/snapshots/easymarkeditor.png b/crates/egui_demo_app/tests/snapshots/easymarkeditor.png index 7666b658e..08f5fc98f 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:3e6a383dca7e91d07df4bf501e2de13d046f04546a08d026efe3f82fc96b6e29 -size 178887 +oid sha256:8cf6d0b20f127f22d49daefed27fc2d0ca43d645fe1486cf7f6fcbb676bdec82 +size 179065 diff --git a/crates/egui_demo_app/tests/snapshots/imageviewer.png b/crates/egui_demo_app/tests/snapshots/imageviewer.png index d5bde1f98..a13af2e71 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:e2fae780123389ca0affa762a5c031b84abcdd31c7a830d485c907c8c370b006 -size 100780 +oid sha256:0e37b3ce49c9ccc1a64beb58b176e23ab6c1fa2d897f676b0de85e510e6bfa85 +size 100845 diff --git a/crates/egui_demo_app/tests/test_demo_app.rs b/crates/egui_demo_app/tests/test_demo_app.rs index 8b0b272fa..65afff10f 100644 --- a/crates/egui_demo_app/tests/test_demo_app.rs +++ b/crates/egui_demo_app/tests/test_demo_app.rs @@ -55,7 +55,7 @@ fn test_demo_app() { harness .get_by_role_and_label(Role::TextInput, "URI:") .focus(); - harness.press_key_modifiers(egui::Modifiers::COMMAND, egui::Key::A); + harness.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::A); harness .get_by_role_and_label(Role::TextInput, "URI:") 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 cc43645c6..6e9a92ef4 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -371,8 +371,8 @@ fn file_menu_button(ui: &mut Ui) { #[cfg(test)] mod tests { use crate::{demo::demo_app_windows::DemoGroups, Demo as _}; - use egui::Vec2; - use egui_kittest::kittest::Queryable as _; + + use egui_kittest::kittest::{NodeT as _, Queryable as _}; use egui_kittest::{Harness, SnapshotOptions, SnapshotResults}; #[test] @@ -399,12 +399,12 @@ mod tests { demo.show(ctx, &mut true); }); - let window = harness.node().children().next().unwrap(); + let window = harness.queryable_node().children().next().unwrap(); // TODO(lucasmerlin): Windows should probably have a label? //let window = harness.get_by_label(name); - let size = window.raw_bounds().expect("window bounds").size(); - harness.set_size(Vec2::new(size.width as f32, size.height as f32)); + let size = window.rect().size(); + harness.set_size(size); // Run the app for some more frames... harness.run_ok(); diff --git a/crates/egui_demo_lib/src/demo/modals.rs b/crates/egui_demo_lib/src/demo/modals.rs index fcb33f0bb..5fb1548e3 100644 --- a/crates/egui_demo_lib/src/demo/modals.rs +++ b/crates/egui_demo_lib/src/demo/modals.rs @@ -190,7 +190,7 @@ mod tests { assert!(harness.ctx.memory(|mem| mem.any_popup_open())); assert!(harness.state().user_modal_open); - harness.press_key(Key::Escape); + harness.key_press(Key::Escape); harness.run_ok(); assert!(!harness.ctx.memory(|mem| mem.any_popup_open())); assert!(harness.state().user_modal_open); @@ -214,7 +214,7 @@ mod tests { assert!(harness.state().user_modal_open); assert!(harness.state().save_modal_open); - harness.press_key(Key::Escape); + harness.key_press(Key::Escape); harness.run(); assert!(harness.state().user_modal_open); @@ -267,7 +267,7 @@ mod tests { harness.run_ok(); - harness.get_by_label("Yes Please").simulate_click(); + harness.get_by_label("Yes Please").click(); harness.run_ok(); diff --git a/crates/egui_demo_lib/src/demo/text_edit.rs b/crates/egui_demo_lib/src/demo/text_edit.rs index 685a9c38f..4ef34a51e 100644 --- a/crates/egui_demo_lib/src/demo/text_edit.rs +++ b/crates/egui_demo_lib/src/demo/text_edit.rs @@ -113,8 +113,8 @@ impl crate::View for TextEditDemo { #[cfg(test)] mod tests { - use egui::{accesskit, CentralPanel}; - use egui_kittest::kittest::{Key, Queryable as _}; + use egui::{accesskit, CentralPanel, Key, Modifiers}; + use egui_kittest::kittest::Queryable as _; use egui_kittest::Harness; #[test] @@ -133,8 +133,9 @@ mod tests { let text_edit = harness.get_by_role(accesskit::Role::TextInput); assert_eq!(text_edit.value().as_deref(), Some("Hello, world!")); + text_edit.focus(); - text_edit.key_combination(&[Key::Command, Key::A]); + harness.key_press_modifiers(Modifiers::COMMAND, Key::A); text_edit.type_text("Hi "); harness.run(); diff --git a/crates/egui_demo_lib/src/rendering_test.rs b/crates/egui_demo_lib/src/rendering_test.rs index 5f0e91bc5..32e51a82a 100644 --- a/crates/egui_demo_lib/src/rendering_test.rs +++ b/crates/egui_demo_lib/src/rendering_test.rs @@ -737,8 +737,9 @@ mod tests { }); { - // Expand color-test collapsing header - harness.get_by_label("Color test").click(); + // Expand color-test collapsing header. We accesskit-click since collapsing header + // might not be on screen at this point. + harness.get_by_label("Color test").click_accesskit(); harness.run(); } diff --git a/crates/egui_kittest/README.md b/crates/egui_kittest/README.md index ff071c9f4..31a55f618 100644 --- a/crates/egui_kittest/README.md +++ b/crates/egui_kittest/README.md @@ -10,7 +10,7 @@ Ui testing library for egui, based on [kittest](https://github.com/rerun-io/kitt ## Example usage ```rust use egui::accesskit::Toggled; -use egui_kittest::{Harness, kittest::Queryable}; +use egui_kittest::{Harness, kittest::{Queryable, NodeT}}; fn main() { let mut checked = false; @@ -21,13 +21,13 @@ fn main() { let mut harness = Harness::new_ui(app); let checkbox = harness.get_by_label("Check me!"); - assert_eq!(checkbox.toggled(), Some(Toggled::False)); + assert_eq!(checkbox.accesskit_node().toggled(), Some(Toggled::False)); checkbox.click(); harness.run(); let checkbox = harness.get_by_label("Check me!"); - assert_eq!(checkbox.toggled(), Some(Toggled::True)); + assert_eq!(checkbox.accesskit_node().toggled(), Some(Toggled::True)); // Shrink the window size to the smallest size possible harness.fit_contents(); diff --git a/crates/egui_kittest/src/event.rs b/crates/egui_kittest/src/event.rs deleted file mode 100644 index e756d4dc9..000000000 --- a/crates/egui_kittest/src/event.rs +++ /dev/null @@ -1,194 +0,0 @@ -use egui::Event::PointerButton; -use egui::{Event, Modifiers, Pos2}; -use kittest::{ElementState, MouseButton, SimulatedEvent}; - -#[derive(Default)] -pub(crate) struct EventState { - last_mouse_pos: Pos2, -} - -impl EventState { - /// Map the kittest event to an egui event, add it to the input and update the modifiers. - /// This function accesses `egui::RawInput::modifiers`. Make sure it is not reset after each - /// frame (Since we use [`egui::RawInput::take`], this should be fine). - pub fn update(&mut self, event: kittest::Event, input: &mut egui::RawInput) { - if let Some(event) = self.kittest_event_to_egui(&mut input.modifiers, event) { - input.events.push(event); - } - } - - fn kittest_event_to_egui( - &mut self, - modifiers: &mut Modifiers, - event: kittest::Event, - ) -> Option { - match event { - kittest::Event::ActionRequest(e) => Some(Event::AccessKitActionRequest(e)), - kittest::Event::Simulated(e) => match e { - SimulatedEvent::CursorMoved { position } => { - self.last_mouse_pos = Pos2::new(position.x as f32, position.y as f32); - Some(Event::PointerMoved(Pos2::new( - position.x as f32, - position.y as f32, - ))) - } - SimulatedEvent::MouseInput { state, button } => { - pointer_button_to_egui(button).map(|button| PointerButton { - button, - modifiers: *modifiers, - pos: self.last_mouse_pos, - pressed: matches!(state, ElementState::Pressed), - }) - } - SimulatedEvent::Ime(text) => Some(Event::Text(text)), - SimulatedEvent::KeyInput { state, key } => { - match key { - kittest::Key::Alt => { - modifiers.alt = matches!(state, ElementState::Pressed); - } - kittest::Key::Command => { - modifiers.command = matches!(state, ElementState::Pressed); - } - kittest::Key::Control => { - modifiers.ctrl = matches!(state, ElementState::Pressed); - } - kittest::Key::Shift => { - modifiers.shift = matches!(state, ElementState::Pressed); - } - _ => {} - } - kittest_key_to_egui(key).map(|key| Event::Key { - key, - modifiers: *modifiers, - pressed: matches!(state, ElementState::Pressed), - repeat: false, - physical_key: None, - }) - } - }, - } - } -} - -fn kittest_key_to_egui(value: kittest::Key) -> Option { - use egui::Key as EKey; - use kittest::Key; - match value { - Key::ArrowDown => Some(EKey::ArrowDown), - Key::ArrowLeft => Some(EKey::ArrowLeft), - Key::ArrowRight => Some(EKey::ArrowRight), - Key::ArrowUp => Some(EKey::ArrowUp), - Key::Escape => Some(EKey::Escape), - Key::Tab => Some(EKey::Tab), - Key::Backspace => Some(EKey::Backspace), - Key::Enter => Some(EKey::Enter), - Key::Space => Some(EKey::Space), - Key::Insert => Some(EKey::Insert), - Key::Delete => Some(EKey::Delete), - Key::Home => Some(EKey::Home), - Key::End => Some(EKey::End), - Key::PageUp => Some(EKey::PageUp), - Key::PageDown => Some(EKey::PageDown), - Key::Copy => Some(EKey::Copy), - Key::Cut => Some(EKey::Cut), - Key::Paste => Some(EKey::Paste), - Key::Colon => Some(EKey::Colon), - Key::Comma => Some(EKey::Comma), - Key::Backslash => Some(EKey::Backslash), - Key::Slash => Some(EKey::Slash), - Key::Pipe => Some(EKey::Pipe), - Key::Questionmark => Some(EKey::Questionmark), - Key::OpenBracket => Some(EKey::OpenBracket), - Key::CloseBracket => Some(EKey::CloseBracket), - Key::Backtick => Some(EKey::Backtick), - Key::Minus => Some(EKey::Minus), - Key::Period => Some(EKey::Period), - Key::Plus => Some(EKey::Plus), - Key::Equals => Some(EKey::Equals), - Key::Semicolon => Some(EKey::Semicolon), - Key::Quote => Some(EKey::Quote), - Key::Num0 => Some(EKey::Num0), - Key::Num1 => Some(EKey::Num1), - Key::Num2 => Some(EKey::Num2), - Key::Num3 => Some(EKey::Num3), - Key::Num4 => Some(EKey::Num4), - Key::Num5 => Some(EKey::Num5), - Key::Num6 => Some(EKey::Num6), - Key::Num7 => Some(EKey::Num7), - Key::Num8 => Some(EKey::Num8), - Key::Num9 => Some(EKey::Num9), - Key::A => Some(EKey::A), - Key::B => Some(EKey::B), - Key::C => Some(EKey::C), - Key::D => Some(EKey::D), - Key::E => Some(EKey::E), - Key::F => Some(EKey::F), - Key::G => Some(EKey::G), - Key::H => Some(EKey::H), - Key::I => Some(EKey::I), - Key::J => Some(EKey::J), - Key::K => Some(EKey::K), - Key::L => Some(EKey::L), - Key::M => Some(EKey::M), - Key::N => Some(EKey::N), - Key::O => Some(EKey::O), - Key::P => Some(EKey::P), - Key::Q => Some(EKey::Q), - Key::R => Some(EKey::R), - Key::S => Some(EKey::S), - Key::T => Some(EKey::T), - Key::U => Some(EKey::U), - Key::V => Some(EKey::V), - Key::W => Some(EKey::W), - Key::X => Some(EKey::X), - Key::Y => Some(EKey::Y), - Key::Z => Some(EKey::Z), - Key::F1 => Some(EKey::F1), - Key::F2 => Some(EKey::F2), - Key::F3 => Some(EKey::F3), - Key::F4 => Some(EKey::F4), - Key::F5 => Some(EKey::F5), - Key::F6 => Some(EKey::F6), - Key::F7 => Some(EKey::F7), - Key::F8 => Some(EKey::F8), - Key::F9 => Some(EKey::F9), - Key::F10 => Some(EKey::F10), - Key::F11 => Some(EKey::F11), - Key::F12 => Some(EKey::F12), - Key::F13 => Some(EKey::F13), - Key::F14 => Some(EKey::F14), - Key::F15 => Some(EKey::F15), - Key::F16 => Some(EKey::F16), - Key::F17 => Some(EKey::F17), - Key::F18 => Some(EKey::F18), - Key::F19 => Some(EKey::F19), - Key::F20 => Some(EKey::F20), - Key::F21 => Some(EKey::F21), - Key::F22 => Some(EKey::F22), - Key::F23 => Some(EKey::F23), - Key::F24 => Some(EKey::F24), - Key::F25 => Some(EKey::F25), - Key::F26 => Some(EKey::F26), - Key::F27 => Some(EKey::F27), - Key::F28 => Some(EKey::F28), - Key::F29 => Some(EKey::F29), - Key::F30 => Some(EKey::F30), - Key::F31 => Some(EKey::F31), - Key::F32 => Some(EKey::F32), - Key::F33 => Some(EKey::F33), - Key::F34 => Some(EKey::F34), - Key::F35 => Some(EKey::F35), - _ => None, - } -} - -fn pointer_button_to_egui(value: MouseButton) -> Option { - match value { - MouseButton::Left => Some(egui::PointerButton::Primary), - MouseButton::Right => Some(egui::PointerButton::Secondary), - MouseButton::Middle => Some(egui::PointerButton::Middle), - MouseButton::Back => Some(egui::PointerButton::Extra1), - MouseButton::Forward => Some(egui::PointerButton::Extra2), - MouseButton::Other(_) => None, - } -} diff --git a/crates/egui_kittest/src/lib.rs b/crates/egui_kittest/src/lib.rs index 0963a9491..ac67c8dad 100644 --- a/crates/egui_kittest/src/lib.rs +++ b/crates/egui_kittest/src/lib.rs @@ -4,7 +4,6 @@ #![cfg_attr(feature = "document-features", doc = document_features::document_features!())] mod builder; -mod event; #[cfg(feature = "snapshot")] mod snapshot; @@ -14,6 +13,7 @@ use std::fmt::{Debug, Display, Formatter}; use std::time::Duration; mod app_kind; +mod node; mod renderer; #[cfg(feature = "wgpu")] mod texture_to_image; @@ -23,13 +23,13 @@ pub mod wgpu; pub use kittest; use crate::app_kind::AppKind; -use crate::event::EventState; pub use builder::*; +pub use node::*; pub use renderer::*; -use egui::{Modifiers, Pos2, Rect, RepaintCause, Vec2, ViewportId}; -use kittest::{Node, Queryable}; +use egui::{Key, Modifiers, Pos2, Rect, RepaintCause, Vec2, ViewportId}; +use kittest::Queryable; #[derive(Debug, Clone)] pub struct ExceededMaxStepsError { @@ -61,13 +61,13 @@ pub struct Harness<'a, State = ()> { kittest: kittest::State, output: egui::FullOutput, app: AppKind<'a, State>, - event_state: EventState, response: Option, state: State, renderer: Box, max_steps: u64, step_dt: f32, wait_for_pending_images: bool, + queued_events: EventQueue, } impl Debug for Harness<'_, State> { @@ -126,12 +126,12 @@ impl<'a, State> Harness<'a, State> { ), output, response, - event_state: EventState::default(), state, renderer, max_steps, step_dt, wait_for_pending_images, + queued_events: Default::default(), }; // Run the harness until it is stable, ensuring that all Areas are shown and animations are done harness.run_ok(); @@ -227,12 +227,19 @@ impl<'a, State> Harness<'a, State> { /// This will call the app closure with each queued event and /// update the Harness. pub fn step(&mut self) { - let events = self.kittest.take_events(); + let events = std::mem::take(&mut *self.queued_events.lock()); if events.is_empty() { self._step(false); } for event in events { - self.event_state.update(event, &mut self.input); + match event { + EventType::Event(event) => { + self.input.events.push(event); + } + EventType::Modifiers(modifiers) => { + self.input.modifiers = modifiers; + } + } self._step(false); } } @@ -414,52 +421,128 @@ impl<'a, State> Harness<'a, State> { &mut self.state } - /// Press a key. - /// This will create a key down event and a key up event. - pub fn press_key(&mut self, key: egui::Key) { - self.input.events.push(egui::Event::Key { + fn event(&self, event: egui::Event) { + self.queued_events.lock().push(EventType::Event(event)); + } + + fn event_modifiers(&self, event: egui::Event, modifiers: Modifiers) { + let mut queue = self.queued_events.lock(); + queue.push(EventType::Modifiers(modifiers)); + queue.push(EventType::Event(event)); + queue.push(EventType::Modifiers(Modifiers::default())); + } + + fn modifiers(&self, modifiers: Modifiers) { + self.queued_events + .lock() + .push(EventType::Modifiers(modifiers)); + } + + pub fn key_down(&self, key: egui::Key) { + self.event(egui::Event::Key { key, pressed: true, - modifiers: self.input.modifiers, - repeat: false, - physical_key: None, - }); - self.input.events.push(egui::Event::Key { - key, - pressed: false, - modifiers: self.input.modifiers, + modifiers: Modifiers::default(), repeat: false, physical_key: None, }); } - /// Press a key with modifiers. - /// This will create a key-down event, a key-up event, and update the modifiers. - /// - /// NOTE: In contrast to the event fns on [`Node`], this will call [`Harness::step`], in - /// order to properly update modifiers. - pub fn press_key_modifiers(&mut self, modifiers: Modifiers, key: egui::Key) { - // Combine the modifiers with the current modifiers - let previous_modifiers = self.input.modifiers; - self.input.modifiers |= modifiers; - - self.input.events.push(egui::Event::Key { - key, - pressed: true, + pub fn key_down_modifiers(&self, modifiers: Modifiers, key: egui::Key) { + self.event_modifiers( + egui::Event::Key { + key, + pressed: true, + modifiers, + repeat: false, + physical_key: None, + }, modifiers, - repeat: false, - physical_key: None, - }); - self.step(); - self.input.events.push(egui::Event::Key { + ); + } + + pub fn key_up(&self, key: egui::Key) { + self.event(egui::Event::Key { key, pressed: false, - modifiers, + modifiers: Modifiers::default(), repeat: false, physical_key: None, }); + } - self.input.modifiers = previous_modifiers; + pub fn key_up_modifiers(&self, modifiers: Modifiers, key: egui::Key) { + self.event_modifiers( + egui::Event::Key { + key, + pressed: false, + modifiers, + repeat: false, + physical_key: None, + }, + modifiers, + ); + } + + /// Press the given keys in combination. + /// + /// For e.g. [`Key::A`] + [`Key::B`] this would generate: + /// - Press [`Key::A`] + /// - Press [`Key::B`] + /// - Release [`Key::B`] + /// - Release [`Key::A`] + pub fn key_combination(&self, keys: &[Key]) { + for key in keys { + self.key_down(*key); + } + for key in keys.iter().rev() { + self.key_up(*key); + } + } + + /// Press the given keys in combination, with modifiers. + /// + /// For e.g. [`Modifiers::COMMAND`] + [`Key::A`] + [`Key::B`] this would generate: + /// - Press [`Modifiers::COMMAND`] + /// - Press [`Key::A`] + /// - Press [`Key::B`] + /// - Release [`Key::B`] + /// - Release [`Key::A`] + /// - Release [`Modifiers::COMMAND`] + pub fn key_combination_modifiers(&self, modifiers: Modifiers, keys: &[Key]) { + self.modifiers(modifiers); + + for pressed in [true, false] { + for key in keys { + self.event(egui::Event::Key { + key: *key, + pressed, + modifiers, + repeat: false, + physical_key: None, + }); + } + } + + self.modifiers(Modifiers::default()); + } + + /// Press a key. + /// + /// This will create a key down event and a key up event. + pub fn key_press(&self, key: egui::Key) { + self.key_combination(&[key]); + } + + /// Press a key with modifiers. + /// + /// This will + /// - set the modifiers + /// - create a key down event + /// - create a key up event + /// - reset the modifiers + pub fn key_press_modifiers(&self, modifiers: Modifiers, key: egui::Key) { + self.key_combination_modifiers(modifiers, &[key]); } /// Render the last output to an image. @@ -478,6 +561,18 @@ impl<'a, State> Harness<'a, State> { .get(&ViewportId::ROOT) .expect("Missing root viewport") } + + fn root(&self) -> Node<'_> { + Node { + accesskit_node: self.kittest.root(), + queue: &self.queued_events, + } + } + + #[deprecated = "Use `Harness::root` instead."] + pub fn node(&self) -> Node<'_> { + self.root() + } } /// Utilities for stateless harnesses. @@ -526,11 +621,11 @@ impl<'a> Harness<'a> { } } -impl<'t, 'n, State> Queryable<'t, 'n> for Harness<'_, State> +impl<'tree, 'node, State> Queryable<'tree, 'node, Node<'tree>> for Harness<'_, State> where - 'n: 't, + 'node: 'tree, { - fn node(&'n self) -> Node<'t> { - self.kittest_state().node() + fn queryable_node(&'node self) -> Node<'tree> { + self.root() } } diff --git a/crates/egui_kittest/src/node.rs b/crates/egui_kittest/src/node.rs new file mode 100644 index 000000000..384a5dbd4 --- /dev/null +++ b/crates/egui_kittest/src/node.rs @@ -0,0 +1,162 @@ +use egui::accesskit::ActionRequest; +use egui::mutex::Mutex; +use egui::{accesskit, Modifiers, PointerButton, Pos2}; +use kittest::{debug_fmt_node, AccessKitNode, NodeT}; +use std::fmt::{Debug, Formatter}; + +pub(crate) enum EventType { + Event(egui::Event), + Modifiers(Modifiers), +} + +pub(crate) type EventQueue = Mutex>; + +#[derive(Clone, Copy)] +pub struct Node<'tree> { + pub(crate) accesskit_node: AccessKitNode<'tree>, + pub(crate) queue: &'tree EventQueue, +} + +impl Debug for Node<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + debug_fmt_node(self, f) + } +} + +impl<'tree> NodeT<'tree> for Node<'tree> { + fn accesskit_node(&self) -> AccessKitNode<'tree> { + self.accesskit_node + } + + fn new_related(&self, child_node: AccessKitNode<'tree>) -> Self { + Self { + queue: self.queue, + accesskit_node: child_node, + } + } +} + +impl Node<'_> { + fn event(&self, event: egui::Event) { + self.queue.lock().push(EventType::Event(event)); + } + + fn modifiers(&self, modifiers: Modifiers) { + self.queue.lock().push(EventType::Modifiers(modifiers)); + } + + pub fn hover(&self) { + self.event(egui::Event::PointerMoved(self.rect().center())); + } + + /// Click at the node center with the primary button. + pub fn click(&self) { + self.click_button(PointerButton::Primary); + } + + #[deprecated = "Use `click()` instead."] + pub fn simulate_click(&self) { + self.click(); + } + + pub fn click_secondary(&self) { + self.click_button(PointerButton::Secondary); + } + + pub fn click_button(&self, button: PointerButton) { + self.hover(); + for pressed in [true, false] { + self.event(egui::Event::PointerButton { + pos: self.rect().center(), + button, + pressed, + modifiers: Modifiers::default(), + }); + } + } + + pub fn click_modifiers(&self, modifiers: Modifiers) { + self.click_button_modifiers(PointerButton::Primary, modifiers); + } + + pub fn click_button_modifiers(&self, button: PointerButton, modifiers: Modifiers) { + self.hover(); + self.modifiers(modifiers); + for pressed in [true, false] { + self.event(egui::Event::PointerButton { + pos: self.rect().center(), + button, + pressed, + modifiers, + }); + } + self.modifiers(Modifiers::default()); + } + + /// Click the node via accesskit. + /// + /// This will trigger a [`accesskit::Action::Click`] action. + /// In contrast to `click()`, this can also click widgets that are not currently visible. + pub fn click_accesskit(&self) { + self.event(egui::Event::AccessKitActionRequest( + accesskit::ActionRequest { + target: self.accesskit_node.id(), + action: accesskit::Action::Click, + data: None, + }, + )); + } + + pub fn rect(&self) -> egui::Rect { + let rect = self + .accesskit_node + .bounding_box() + .expect("Every egui node should have a rect"); + egui::Rect { + min: Pos2::new(rect.x0 as f32, rect.y0 as f32), + max: Pos2::new(rect.x1 as f32, rect.y1 as f32), + } + } + + pub fn focus(&self) { + self.event(egui::Event::AccessKitActionRequest(ActionRequest { + action: accesskit::Action::Focus, + target: self.accesskit_node.id(), + data: None, + })); + } + + #[deprecated = "Use `Harness::key_down` instead."] + pub fn key_down(&self, key: egui::Key) { + self.event(egui::Event::Key { + key, + pressed: true, + modifiers: Modifiers::default(), + repeat: false, + physical_key: None, + }); + } + + #[deprecated = "Use `Harness::key_up` instead."] + pub fn key_up(&self, key: egui::Key) { + self.event(egui::Event::Key { + key, + pressed: false, + modifiers: Modifiers::default(), + repeat: false, + physical_key: None, + }); + } + + pub fn type_text(&self, text: &str) { + self.event(egui::Event::Text(text.to_owned())); + } + + pub fn value(&self) -> Option { + self.accesskit_node.value() + } + + pub fn is_focused(&self) -> bool { + self.accesskit_node.is_focused() + } +} diff --git a/crates/egui_kittest/tests/menu.rs b/crates/egui_kittest/tests/menu.rs index fc19e8040..48f348a21 100644 --- a/crates/egui_kittest/tests/menu.rs +++ b/crates/egui_kittest/tests/menu.rs @@ -95,7 +95,7 @@ fn menu_close_on_click_outside() { TestMenu::new(MenuConfig::new().close_behavior(PopupCloseBehavior::CloseOnClick)) .into_harness(); - harness.get_by_label("Menu A").simulate_click(); + harness.get_by_label("Menu A").click(); harness.run(); harness @@ -106,9 +106,7 @@ fn menu_close_on_click_outside() { // We should be able to check the checkbox without closing the menu // Click a couple of times, just in case for expect_checked in [true, false, true, false] { - harness - .get_by_label("Checkbox in Submenu C") - .simulate_click(); + harness.get_by_label("Checkbox in Submenu C").click(); harness.run(); assert_eq!(expect_checked, harness.state().checked); } @@ -119,7 +117,7 @@ fn menu_close_on_click_outside() { assert!(harness.query_by_label("Checkbox in Submenu C").is_some()); // Clicking outside should close the menu - harness.get_by_label("Some other label").simulate_click(); + harness.get_by_label("Some other label").click(); harness.run(); assert!(harness.query_by_label("Checkbox in Submenu C").is_none()); } @@ -130,14 +128,14 @@ fn menu_close_on_click() { TestMenu::new(MenuConfig::new().close_behavior(PopupCloseBehavior::CloseOnClick)) .into_harness(); - harness.get_by_label("Menu A").simulate_click(); + harness.get_by_label("Menu A").click(); harness.run(); 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) - harness.get_by_label("Button in Submenu B").simulate_click(); + harness.get_by_label("Button in Submenu B").click(); harness.run(); assert!(harness.query_by_label("Button in Submenu B").is_none()); } @@ -145,21 +143,19 @@ fn menu_close_on_click() { #[test] fn clicking_submenu_button_should_never_close_menu() { // We test for this since otherwise the menu wouldn't work on touch devices - // The other tests use .hover to open submenus, but this test explicitly uses .simulate_click + // The other tests use .hover to open submenus, but this test explicitly uses .click let mut harness = TestMenu::new(MenuConfig::new().close_behavior(PopupCloseBehavior::CloseOnClick)) .into_harness(); - harness.get_by_label("Menu A").simulate_click(); + harness.get_by_label("Menu A").click(); harness.run(); // Clicking the submenu button should not close the menu - harness - .get_by_label_contains("Submenu B with icon") - .simulate_click(); + harness.get_by_label_contains("Submenu B with icon").click(); harness.run(); - harness.get_by_label("Button in Submenu B").simulate_click(); + harness.get_by_label("Button in Submenu B").click(); harness.run(); assert!(harness.query_by_label("Button in Submenu B").is_none()); } @@ -174,7 +170,7 @@ fn menu_snapshots() { harness.run(); results.add(harness.try_snapshot("menu/closed_hovered")); - harness.get_by_label("Menu A").simulate_click(); + harness.get_by_label("Menu A").click(); harness.run(); results.add(harness.try_snapshot("menu/opened")); diff --git a/crates/egui_kittest/tests/popup.rs b/crates/egui_kittest/tests/popup.rs index 368e8de7e..a8d3bcc9d 100644 --- a/crates/egui_kittest/tests/popup.rs +++ b/crates/egui_kittest/tests/popup.rs @@ -23,7 +23,7 @@ fn test_interactive_tooltip() { harness.run(); harness.get_by_label("link").hover(); harness.run(); - harness.get_by_label("link").simulate_click(); + harness.get_by_label("link").click(); harness.run(); diff --git a/crates/egui_kittest/tests/regression_tests.rs b/crates/egui_kittest/tests/regression_tests.rs index 50ed1095a..5107799f5 100644 --- a/crates/egui_kittest/tests/regression_tests.rs +++ b/crates/egui_kittest/tests/regression_tests.rs @@ -10,19 +10,19 @@ pub fn focus_should_skip_over_disabled_buttons() { ui.add(Button::new("Button 3")); }); - harness.press_key(egui::Key::Tab); + harness.key_press(egui::Key::Tab); harness.run(); let button_1 = harness.get_by_label("Button 1"); assert!(button_1.is_focused()); - harness.press_key(egui::Key::Tab); + harness.key_press(egui::Key::Tab); harness.run(); let button_3 = harness.get_by_label("Button 3"); assert!(button_3.is_focused()); - harness.press_key(egui::Key::Tab); + harness.key_press(egui::Key::Tab); harness.run(); let button_1 = harness.get_by_label("Button 1"); @@ -41,13 +41,13 @@ pub fn focus_should_skip_over_disabled_drag_values() { ui.add(egui::DragValue::new(&mut value_3)); }); - harness.press_key(egui::Key::Tab); + harness.key_press(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.key_press(egui::Key::Tab); harness.run(); let drag_value_3 = harness.get_by(|node| node.numeric_value() == Some(3.0)); @@ -103,8 +103,7 @@ fn test_combobox() { results.add(harness.try_snapshot("combobox_opened")); let item_2 = harness.get_by_role_and_label(Role::Button, "Item 2"); - // Node::click doesn't close the popup, so we use simulate_click - item_2.simulate_click(); + item_2.click(); harness.run(); diff --git a/crates/egui_kittest/tests/snapshots/readme_example.png b/crates/egui_kittest/tests/snapshots/readme_example.png index 4b8288326..a3d4ca79a 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:b1d172484712e3e12038f8ff427db8c0073aba124aa1b6be17edcc7dccb12f74 -size 1656 +oid sha256:341658df1dfe665e79180d4540965a986a21de09c9cbc1a8744bdcff1a7c2086 +size 1892 diff --git a/crates/egui_kittest/tests/tests.rs b/crates/egui_kittest/tests/tests.rs index 2b223f457..8c92f4314 100644 --- a/crates/egui_kittest/tests/tests.rs +++ b/crates/egui_kittest/tests/tests.rs @@ -1,6 +1,6 @@ use egui::{include_image, Modifiers, Vec2}; use egui_kittest::Harness; -use kittest::{Key, Queryable as _}; +use kittest::Queryable as _; #[test] fn test_shrink() { @@ -39,17 +39,15 @@ fn test_modifiers() { State::default(), ); - harness.get_by_label("Click me").key_down(Key::Command); - // This run isn't necessary, but allows us to test whether modifiers are remembered between frames - harness.run(); - harness.get_by_label("Click me").click(); - harness.get_by_label("Click me").key_up(Key::Command); + harness + .get_by_label("Click me") + .click_modifiers(Modifiers::COMMAND); harness.run(); - harness.press_key_modifiers(Modifiers::COMMAND, egui::Key::Z); + harness.key_press_modifiers(Modifiers::COMMAND, egui::Key::Z); harness.run(); - harness.node().key_combination(&[Key::Command, Key::Y]); + harness.key_combination_modifiers(Modifiers::COMMAND, &[egui::Key::Y]); harness.run(); let state = harness.state(); diff --git a/tests/egui_tests/tests/test_widgets.rs b/tests/egui_tests/tests/test_widgets.rs index 110eff810..e7082d472 100644 --- a/tests/egui_tests/tests/test_widgets.rs +++ b/tests/egui_tests/tests/test_widgets.rs @@ -1,11 +1,11 @@ use egui::load::SizedTexture; use egui::{ include_image, Align, AtomExt as _, AtomLayout, Button, Color32, ColorImage, Direction, - DragValue, Event, Grid, IntoAtoms as _, Layout, PointerButton, Pos2, Response, Slider, Stroke, + DragValue, Event, Grid, IntoAtoms as _, Layout, PointerButton, 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}; +use egui_kittest::kittest::{by, Queryable as _}; +use egui_kittest::{Harness, Node, SnapshotResult, SnapshotResults}; #[test] fn widget_tests() { @@ -278,14 +278,10 @@ impl<'a> VisualTests<'a> { }); 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, - ); + let rect = harness.get_next().rect(); harness.input_mut().events.push(Event::PointerButton { button: PointerButton::Primary, - pos, + pos: rect.center(), pressed: true, modifiers: Default::default(), });