From 7519ec70996bdfb36babbbea3b958f9b2596bee5 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 3 Jan 2025 14:54:57 +0100 Subject: [PATCH 001/132] Fix: round animating collapsing header height to GUI --- crates/egui/src/containers/collapsing_header.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/containers/collapsing_header.rs b/crates/egui/src/containers/collapsing_header.rs index d6a6c7fba..121daeac4 100644 --- a/crates/egui/src/containers/collapsing_header.rs +++ b/crates/egui/src/containers/collapsing_header.rs @@ -4,6 +4,7 @@ use crate::{ emath, epaint, pos2, remap, remap_clamp, vec2, Context, Id, InnerResponse, NumExt, Rect, Response, Sense, Stroke, TextStyle, TextWrapMode, Ui, Vec2, WidgetInfo, WidgetText, WidgetType, }; +use emath::GuiRounding as _; use epaint::Shape; #[derive(Clone, Copy, Debug)] @@ -214,7 +215,7 @@ impl CollapsingState { 10.0 } else { let full_height = self.state.open_height.unwrap_or_default(); - remap_clamp(openness, 0.0..=1.0, 0.0..=full_height) + remap_clamp(openness, 0.0..=1.0, 0.0..=full_height).round_ui() }; let mut clip_rect = child_ui.clip_rect(); From 5cbf337f1823028ce9614cd1d09c73ffb29cce8b Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 3 Jan 2025 14:55:51 +0100 Subject: [PATCH 002/132] check.sh: enable all features when running `cargo test` --- scripts/check.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/check.sh b/scripts/check.sh index 7db1a6aa3..8c0f0af97 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -30,7 +30,7 @@ cargo check --quiet --all-targets --all-features cargo check --quiet -p egui_demo_app --lib --target wasm32-unknown-unknown cargo check --quiet -p egui_demo_app --lib --target wasm32-unknown-unknown --all-features # TODO(#5297) re-enable --all-features once the tests work with the unity feature -cargo test --quiet --all-targets +cargo test --quiet --all-targets --all-features cargo test --quiet --doc # slow - checks all doc-tests cargo check --quiet -p eframe --no-default-features --features "glow" From 938d8b0d2e2a551ac354d76d6208188e5942b21e Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 3 Jan 2025 16:23:31 +0100 Subject: [PATCH 003/132] egui_kittest: write `.old.png` files when updating images (#5578) After running `UPDATE_SNAPSHOTS=1 cargo test --all-features` I want to compare before/after images before committing them. Now I can! --- .gitignore | 1 + crates/egui_kittest/src/snapshot.rs | 96 ++++++++++++++++------------- 2 files changed, 55 insertions(+), 42 deletions(-) diff --git a/.gitignore b/.gitignore index 887de9da3..588826e7d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ **/target_wasm **/tests/snapshots/**/*.diff.png **/tests/snapshots/**/*.new.png +**/tests/snapshots/**/*.old.png /.*.json /.vscode /media/* diff --git a/crates/egui_kittest/src/snapshot.rs b/crates/egui_kittest/src/snapshot.rs index 956329127..be4f68a06 100644 --- a/crates/egui_kittest/src/snapshot.rs +++ b/crates/egui_kittest/src/snapshot.rs @@ -2,7 +2,7 @@ use crate::Harness; use image::ImageError; use std::fmt::Display; use std::io::ErrorKind; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; #[non_exhaustive] pub struct SnapshotOptions { @@ -159,22 +159,6 @@ fn should_update_snapshots() -> bool { std::env::var("UPDATE_SNAPSHOTS").is_ok() } -fn maybe_update_snapshot( - snapshot_path: &Path, - current: &image::RgbaImage, -) -> Result<(), SnapshotError> { - if should_update_snapshots() { - current - .save(snapshot_path) - .map_err(|err| SnapshotError::WriteSnapshot { - err, - path: snapshot_path.into(), - })?; - println!("Updated snapshot: {snapshot_path:?}"); - } - Ok(()) -} - /// Image snapshot test with custom options. /// /// If you want to change the default options for your whole project, it's recommended to create a @@ -188,11 +172,14 @@ fn maybe_update_snapshot( /// The new image from the most recent test run will be saved under `{output_path}/{name}.new.png`. /// If new image didn't match the snapshot, a diff image will be saved under `{output_path}/{name}.diff.png`. /// +/// If the env-var `UPDATE_SNAPSHOTS` is set, then the old image will backed up under `{output_path}/{name}.old.png`. +/// and then new image will be written to `{output_path}/{name}.png` +/// /// # Errors /// Returns a [`SnapshotError`] if the image does not match the snapshot or if there was an error /// reading or writing the snapshot. pub fn try_image_snapshot_options( - current: &image::RgbaImage, + new: &image::RgbaImage, name: &str, options: &SnapshotOptions, ) -> Result<(), SnapshotError> { @@ -201,45 +188,72 @@ pub fn try_image_snapshot_options( output_path, } = options; - let path = output_path.join(format!("{name}.png")); - std::fs::create_dir_all(path.parent().expect("Could not get snapshot folder")).ok(); + std::fs::create_dir_all(output_path).ok(); + // The one that is checked in to git + let snapshot_path = output_path.join(format!("{name}.png")); + + // These should be in .gitignore: let diff_path = output_path.join(format!("{name}.diff.png")); - let current_path = output_path.join(format!("{name}.new.png")); + let old_backup_path = output_path.join(format!("{name}.old.png")); + let new_path = output_path.join(format!("{name}.new.png")); - current - .save(¤t_path) + // Delete old temporary files if they exist: + std::fs::remove_file(&diff_path).ok(); + std::fs::remove_file(&old_backup_path).ok(); + std::fs::remove_file(&new_path).ok(); + + let maybe_update_snapshot = || { + if should_update_snapshots() { + // Keep the old version so the user can compare it: + std::fs::rename(&snapshot_path, &old_backup_path).ok(); + + // Write the new file to the checked in path: + new.save(&snapshot_path) + .map_err(|err| SnapshotError::WriteSnapshot { + err, + path: snapshot_path.clone(), + })?; + + // No need for an explicit `.new` file: + std::fs::remove_file(&new_path).ok(); + + println!("Updated snapshot: {snapshot_path:?}"); + } + Ok(()) + }; + + // Always write a `.new` file so the user can compare: + new.save(&new_path) .map_err(|err| SnapshotError::WriteSnapshot { err, - path: current_path, + path: new_path.clone(), })?; - let previous = match image::open(&path) { + let previous = match image::open(&snapshot_path) { Ok(image) => image.to_rgba8(), Err(err) => { - maybe_update_snapshot(&path, current)?; - return Err(SnapshotError::OpenSnapshot { path, err }); + // No previous snapshot - probablye a new test. + maybe_update_snapshot()?; + return Err(SnapshotError::OpenSnapshot { + path: snapshot_path.clone(), + err, + }); } }; - if previous.dimensions() != current.dimensions() { - maybe_update_snapshot(&path, current)?; + if previous.dimensions() != new.dimensions() { + maybe_update_snapshot()?; return Err(SnapshotError::SizeMismatch { name: name.to_owned(), expected: previous.dimensions(), - actual: current.dimensions(), + actual: new.dimensions(), }); } - let result = dify::diff::get_results( - previous, - current.clone(), - *threshold, - true, - None, - &None, - &None, - ); + // Compare existing image to the new one: + let result = + dify::diff::get_results(previous, new.clone(), *threshold, true, None, &None, &None); if let Some((diff, result_image)) = result { result_image @@ -248,15 +262,13 @@ pub fn try_image_snapshot_options( path: diff_path.clone(), err, })?; - maybe_update_snapshot(&path, current)?; + maybe_update_snapshot()?; Err(SnapshotError::Diff { name: name.to_owned(), diff, diff_path, }) } else { - // Delete old diff if it exists - std::fs::remove_file(diff_path).ok(); Ok(()) } } From 6607cd95f94d76f12623cd9ca43614cb02d9fe6e Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sat, 4 Jan 2025 10:29:22 +0100 Subject: [PATCH 004/132] =?UTF-8?q?=E2=9A=A0=EF=B8=8F=20`Frame`=20now=20in?= =?UTF-8?q?cludes=20stroke=20width=20as=20part=20of=20padding=20(#5575)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Part of https://github.com/emilk/egui/issues/4019 `Frame` now includes the width of the stroke as part of its size. From the new docs: ### `Frame` docs The total (outer) size of a frame is `content_size + inner_margin + 2*stroke.width + outer_margin`. Everything within the stroke is filled with the fill color (if any). ```text +-----------------^-------------------------------------- -+ | | outer_margin | | +------------v----^------------------------------+ | | | | stroke width | | | | +------------v---^---------------------+ | | | | | | inner_margin | | | | | | +-----------v----------------+ | | | | | | | ^ | | | | | | | | | | | | | | | | |<------ content_size ------>| | | | | | | | | | | | | | | | | v | | | | | | | +------- content_rect -------+ | | | | | | | | | | | +-------------fill_rect ---------------+ | | | | | | | +----------------- widget_rect ------------------+ | | | +---------------------- outer_rect ------------------------+ ``` The four rectangles, from inside to outside, are: * `content_rect`: the rectangle that is made available to the inner [`Ui`] or widget. * `fill_rect`: the rectangle that is filled with the fill color (inside the stroke, if any). * `widget_rect`: is the interactive part of the widget (what sense clicks etc). * `outer_rect`: what is allocated in the outer [`Ui`], and is what is returned by [`Response::rect`]. ### Notes This required rewriting a lot of the layout code for `egui::Window`, which was a massive pain. But now the window margin and stroke width is properly accounted for everywhere. --- crates/egui/src/containers/frame.rs | 322 +++++++++++----- crates/egui/src/containers/window.rs | 357 ++++++++++-------- crates/egui/src/ui.rs | 6 +- crates/egui_demo_app/src/wrap_app.rs | 8 +- .../egui_demo_app/tests/snapshots/clock.png | 4 +- .../tests/snapshots/custom3d.png | 4 +- crates/egui_demo_lib/src/demo/frame_demo.rs | 17 +- .../src/demo/misc_demo_window.rs | 90 +++-- .../tests/snapshots/demos/Bézier Curve.png | 4 +- .../tests/snapshots/demos/Code Editor.png | 4 +- .../tests/snapshots/demos/Code Example.png | 4 +- .../tests/snapshots/demos/Context Menus.png | 4 +- .../tests/snapshots/demos/Dancing Strings.png | 4 +- .../tests/snapshots/demos/Drag and Drop.png | 4 +- .../tests/snapshots/demos/Extra Viewport.png | 4 +- .../tests/snapshots/demos/Font Book.png | 4 +- .../tests/snapshots/demos/Frame.png | 4 +- .../tests/snapshots/demos/Highlighting.png | 4 +- .../snapshots/demos/Interactive Container.png | 4 +- .../tests/snapshots/demos/Misc Demos.png | 4 +- .../tests/snapshots/demos/Modals.png | 4 +- .../tests/snapshots/demos/Multi Touch.png | 4 +- .../tests/snapshots/demos/Painting.png | 4 +- .../tests/snapshots/demos/Pan Zoom.png | 4 +- .../tests/snapshots/demos/Panels.png | 4 +- .../tests/snapshots/demos/Screenshot.png | 4 +- .../tests/snapshots/demos/Scrolling.png | 4 +- .../tests/snapshots/demos/Sliders.png | 4 +- .../tests/snapshots/demos/Strip.png | 4 +- .../tests/snapshots/demos/Table.png | 4 +- .../tests/snapshots/demos/Text Layout.png | 4 +- .../tests/snapshots/demos/TextEdit.png | 4 +- .../tests/snapshots/demos/Tooltips.png | 4 +- .../tests/snapshots/demos/Undo Redo.png | 4 +- .../tests/snapshots/demos/Window Options.png | 4 +- .../tests/snapshots/modals_1.png | 4 +- .../tests/snapshots/modals_2.png | 4 +- .../tests/snapshots/modals_3.png | 4 +- ...rop_should_prevent_focusing_lower_area.png | 4 +- crates/egui_kittest/src/app_kind.rs | 2 +- examples/custom_window_frame/src/main.rs | 12 +- tests/test_ui_stack/src/main.rs | 242 ++++++------ 42 files changed, 680 insertions(+), 508 deletions(-) diff --git a/crates/egui/src/containers/frame.rs b/crates/egui/src/containers/frame.rs index 62c5d0b58..64b38582b 100644 --- a/crates/egui/src/containers/frame.rs +++ b/crates/egui/src/containers/frame.rs @@ -6,7 +6,43 @@ use crate::{ }; use epaint::{Color32, Margin, Marginf, Rect, Rounding, Shadow, Shape, Stroke}; -/// Add a background, frame and/or margin to a rectangular background of a [`Ui`]. +/// A frame around some content, including margin, colors, etc. +/// +/// ## Definitions +/// The total (outer) size of a frame is +/// `content_size + inner_margin + 2 * stroke.width + outer_margin`. +/// +/// Everything within the stroke is filled with the fill color (if any). +/// +/// ```text +/// +-----------------^-------------------------------------- -+ +/// | | outer_margin | +/// | +------------v----^------------------------------+ | +/// | | | stroke width | | +/// | | +------------v---^---------------------+ | | +/// | | | | inner_margin | | | +/// | | | +-----------v----------------+ | | | +/// | | | | ^ | | | | +/// | | | | | | | | | +/// | | | |<------ content_size ------>| | | | +/// | | | | | | | | | +/// | | | | v | | | | +/// | | | +------- content_rect -------+ | | | +/// | | | | | | +/// | | +-------------fill_rect ---------------+ | | +/// | | | | +/// | +----------------- widget_rect ------------------+ | +/// | | +/// +---------------------- outer_rect ------------------------+ +/// ``` +/// +/// The four rectangles, from inside to outside, are: +/// * `content_rect`: the rectangle that is made available to the inner [`Ui`] or widget. +/// * `fill_rect`: the rectangle that is filled with the fill color (inside the stroke, if any). +/// * `widget_rect`: is the interactive part of the widget (what sense clicks etc). +/// * `outer_rect`: what is allocated in the outer [`Ui`], and is what is returned by [`Response::rect`]. +/// +/// ## Usage /// /// ``` /// # egui::__run_test_ui(|ui| { @@ -58,19 +94,47 @@ use epaint::{Color32, Margin, Marginf, Rect, Rounding, Shadow, Shape, Stroke}; #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[must_use = "You should call .show()"] pub struct Frame { + // Fields are ordered inside-out. + // TODO(emilk): add `min_content_size: Vec2` + // /// Margin within the painted frame. + /// + /// Known as `padding` in CSS. + #[doc(alias = "padding")] pub inner_margin: Margin, - /// Margin outside the painted frame. - pub outer_margin: Margin, - - pub rounding: Rounding, - - pub shadow: Shadow, - + /// The background fill color of the frame, within the [`Self::stroke`]. + /// + /// Known as `background` in CSS. + #[doc(alias = "background")] pub fill: Color32, + /// The width and color of the outline around the frame. + /// + /// The width of the stroke is part of the total margin/padding of the frame. + #[doc(alias = "border")] pub stroke: Stroke, + + /// The rounding of the corners of [`Self::stroke`] and [`Self::fill`]. + pub rounding: Rounding, + + /// Margin outside the painted frame. + /// + /// Similar to what is called `margin` in CSS. + /// However, egui does NOT do "Margin Collapse" like in CSS, + /// i.e. when placing two frames next to each other, + /// the distance between their borders is the SUM + /// of their other margins. + /// In CSS the distance would be the MAX of their outer margins. + /// Supporting margin collapse is difficult, and would + /// requires complicating the already complicated egui layout code. + /// + /// Consider using [`crate::Spacing::item_spacing`] + /// for adding space between widgets. + pub outer_margin: Margin, + + /// Optional drop-shadow behind the frame. + pub shadow: Shadow, } #[test] @@ -85,68 +149,72 @@ fn frame_size() { ); } +/// ## Constructors impl Frame { - pub fn none() -> Self { - Self::default() + /// No colors, no margins, no border. + /// + /// This is also the default. + pub const NONE: Self = Self { + inner_margin: Margin::ZERO, + stroke: Stroke::NONE, + fill: Color32::TRANSPARENT, + rounding: Rounding::ZERO, + outer_margin: Margin::ZERO, + shadow: Shadow::NONE, + }; + + pub const fn new() -> Self { + Self::NONE + } + + #[deprecated = "Use `Frame::NONE` or `Frame::new()` instead."] + pub const fn none() -> Self { + Self::NONE } /// For when you want to group a few widgets together within a frame. pub fn group(style: &Style) -> Self { - Self { - inner_margin: Margin::same(6), // same and symmetric looks best in corners when nesting groups - rounding: style.visuals.widgets.noninteractive.rounding, - stroke: style.visuals.widgets.noninteractive.bg_stroke, - ..Default::default() - } + Self::new() + .inner_margin(6) + .rounding(style.visuals.widgets.noninteractive.rounding) + .stroke(style.visuals.widgets.noninteractive.bg_stroke) } pub fn side_top_panel(style: &Style) -> Self { - Self { - inner_margin: Margin::symmetric(8, 2), - fill: style.visuals.panel_fill, - ..Default::default() - } + Self::new() + .inner_margin(Margin::symmetric(8, 2)) + .fill(style.visuals.panel_fill) } pub fn central_panel(style: &Style) -> Self { - Self { - inner_margin: Margin::same(8), - fill: style.visuals.panel_fill, - ..Default::default() - } + Self::new().inner_margin(8).fill(style.visuals.panel_fill) } pub fn window(style: &Style) -> Self { - Self { - inner_margin: style.spacing.window_margin, - rounding: style.visuals.window_rounding, - shadow: style.visuals.window_shadow, - fill: style.visuals.window_fill(), - stroke: style.visuals.window_stroke(), - ..Default::default() - } + Self::new() + .inner_margin(style.spacing.window_margin) + .rounding(style.visuals.window_rounding) + .shadow(style.visuals.window_shadow) + .fill(style.visuals.window_fill()) + .stroke(style.visuals.window_stroke()) } pub fn menu(style: &Style) -> Self { - Self { - inner_margin: style.spacing.menu_margin, - rounding: style.visuals.menu_rounding, - shadow: style.visuals.popup_shadow, - fill: style.visuals.window_fill(), - stroke: style.visuals.window_stroke(), - ..Default::default() - } + Self::new() + .inner_margin(style.spacing.menu_margin) + .rounding(style.visuals.menu_rounding) + .shadow(style.visuals.popup_shadow) + .fill(style.visuals.window_fill()) + .stroke(style.visuals.window_stroke()) } pub fn popup(style: &Style) -> Self { - Self { - inner_margin: style.spacing.menu_margin, - rounding: style.visuals.menu_rounding, - shadow: style.visuals.popup_shadow, - fill: style.visuals.window_fill(), - stroke: style.visuals.window_stroke(), - ..Default::default() - } + Self::new() + .inner_margin(style.spacing.menu_margin) + .rounding(style.visuals.menu_rounding) + .shadow(style.visuals.popup_shadow) + .fill(style.visuals.window_fill()) + .stroke(style.visuals.window_stroke()) } /// A canvas to draw on. @@ -154,57 +222,77 @@ impl Frame { /// In bright mode this will be very bright, /// and in dark mode this will be very dark. pub fn canvas(style: &Style) -> Self { - Self { - inner_margin: Margin::same(2), - rounding: style.visuals.widgets.noninteractive.rounding, - fill: style.visuals.extreme_bg_color, - stroke: style.visuals.window_stroke(), - ..Default::default() - } + Self::new() + .inner_margin(2) + .rounding(style.visuals.widgets.noninteractive.rounding) + .fill(style.visuals.extreme_bg_color) + .stroke(style.visuals.window_stroke()) } /// A dark canvas to draw on. pub fn dark_canvas(style: &Style) -> Self { - Self { - fill: Color32::from_black_alpha(250), - ..Self::canvas(style) - } + Self::canvas(style).fill(Color32::from_black_alpha(250)) } } +/// ## Builders impl Frame { - #[inline] - pub fn fill(mut self, fill: Color32) -> Self { - self.fill = fill; - self - } - - #[inline] - pub fn stroke(mut self, stroke: impl Into) -> Self { - self.stroke = stroke.into(); - self - } - - #[inline] - pub fn rounding(mut self, rounding: impl Into) -> Self { - self.rounding = rounding.into(); - self - } - /// Margin within the painted frame. + /// + /// Known as `padding` in CSS. + #[doc(alias = "padding")] #[inline] pub fn inner_margin(mut self, inner_margin: impl Into) -> Self { self.inner_margin = inner_margin.into(); self } + /// The background fill color of the frame, within the [`Self::stroke`]. + /// + /// Known as `background` in CSS. + #[doc(alias = "background")] + #[inline] + pub fn fill(mut self, fill: Color32) -> Self { + self.fill = fill; + self + } + + /// The width and color of the outline around the frame. + /// + /// The width of the stroke is part of the total margin/padding of the frame. + #[inline] + pub fn stroke(mut self, stroke: impl Into) -> Self { + self.stroke = stroke.into(); + self + } + + /// The rounding of the corners of [`Self::stroke`] and [`Self::fill`]. + #[inline] + pub fn rounding(mut self, rounding: impl Into) -> Self { + self.rounding = rounding.into(); + self + } + /// Margin outside the painted frame. + /// + /// Similar to what is called `margin` in CSS. + /// However, egui does NOT do "Margin Collapse" like in CSS, + /// i.e. when placing two frames next to each other, + /// the distance between their borders is the SUM + /// of their other margins. + /// In CSS the distance would be the MAX of their outer margins. + /// Supporting margin collapse is difficult, and would + /// requires complicating the already complicated egui layout code. + /// + /// Consider using [`crate::Spacing::item_spacing`] + /// for adding space between widgets. #[inline] pub fn outer_margin(mut self, outer_margin: impl Into) -> Self { self.outer_margin = outer_margin.into(); self } + /// Optional drop-shadow behind the frame. #[inline] pub fn shadow(mut self, shadow: Shadow) -> Self { self.shadow = shadow; @@ -224,11 +312,37 @@ impl Frame { } } +/// ## Inspectors impl Frame { - /// Inner margin plus outer margin. + /// How much extra space the frame uses up compared to the content. + /// + /// [`Self::inner_margin`] + [`Self.stroke`]`.width` + [`Self::outer_margin`]. #[inline] pub fn total_margin(&self) -> Marginf { - Marginf::from(self.inner_margin) + Marginf::from(self.outer_margin) + Marginf::from(self.inner_margin) + + Marginf::from(self.stroke.width) + + Marginf::from(self.outer_margin) + } + + /// Calculate the `fill_rect` from the `content_rect`. + /// + /// This is the rectangle that is filled with the fill color (inside the stroke, if any). + pub fn fill_rect(&self, content_rect: Rect) -> Rect { + content_rect + self.inner_margin + } + + /// Calculate the `widget_rect` from the `content_rect`. + /// + /// This is the visible and interactive rectangle. + pub fn widget_rect(&self, content_rect: Rect) -> Rect { + content_rect + self.inner_margin + Marginf::from(self.stroke.width) + } + + /// Calculate the `outer_rect` from the `content_rect`. + /// + /// This is what is allocated in the outer [`Ui`], and is what is returned by [`Response::rect`]. + pub fn outer_rect(&self, content_rect: Rect) -> Rect { + content_rect + self.inner_margin + Marginf::from(self.stroke.width) + self.outer_margin } } @@ -259,20 +373,18 @@ impl Frame { let where_to_put_background = ui.painter().add(Shape::Noop); let outer_rect_bounds = ui.available_rect_before_wrap(); - let mut inner_rect = outer_rect_bounds - self.outer_margin - self.inner_margin; + let mut max_content_rect = outer_rect_bounds - self.total_margin(); // Make sure we don't shrink to the negative: - inner_rect.max.x = inner_rect.max.x.max(inner_rect.min.x); - inner_rect.max.y = inner_rect.max.y.max(inner_rect.min.y); + max_content_rect.max.x = max_content_rect.max.x.max(max_content_rect.min.x); + max_content_rect.max.y = max_content_rect.max.y.max(max_content_rect.min.y); let content_ui = ui.new_child( UiBuilder::new() .ui_stack_info(UiStackInfo::new(UiKind::Frame).with_frame(self)) - .max_rect(inner_rect), + .max_rect(max_content_rect), ); - // content_ui.set_clip_rect(outer_rect_bounds.shrink(self.stroke.width * 0.5)); // Can't do this since we don't know final size yet - Prepared { frame: self, where_to_put_background, @@ -298,32 +410,37 @@ impl Frame { } /// Paint this frame as a shape. - /// - /// The margin is ignored. - pub fn paint(&self, outer_rect: Rect) -> Shape { + pub fn paint(&self, content_rect: Rect) -> Shape { let Self { inner_margin: _, - outer_margin: _, - rounding, - shadow, fill, stroke, + rounding, + outer_margin: _, + shadow, } = *self; - let frame_shape = Shape::Rect(epaint::RectShape::new(outer_rect, rounding, fill, stroke)); + let fill_rect = self.fill_rect(content_rect); + let widget_rect = self.widget_rect(content_rect); + + let frame_shape = Shape::Rect(epaint::RectShape::new(fill_rect, rounding, fill, stroke)); if shadow == Default::default() { frame_shape } else { - let shadow = shadow.as_shape(outer_rect, rounding); + let shadow = shadow.as_shape(widget_rect, rounding); Shape::Vec(vec![Shape::from(shadow), frame_shape]) } } } impl Prepared { - fn content_with_margin(&self) -> Rect { - self.content_ui.min_rect() + self.frame.inner_margin + self.frame.outer_margin + fn outer_rect(&self) -> Rect { + let content_rect = self.content_ui.min_rect(); + content_rect + + self.frame.inner_margin + + Marginf::from(self.frame.stroke.width) + + self.frame.outer_margin } /// Allocate the space that was used by [`Self::content_ui`]. @@ -332,22 +449,25 @@ impl Prepared { /// /// This can be called before or after [`Self::paint`]. pub fn allocate_space(&self, ui: &mut Ui) -> Response { - ui.allocate_rect(self.content_with_margin(), Sense::hover()) + ui.allocate_rect(self.outer_rect(), Sense::hover()) } /// Paint the frame. /// /// This can be called before or after [`Self::allocate_space`]. pub fn paint(&self, ui: &Ui) { - let paint_rect = self.content_ui.min_rect() + self.frame.inner_margin; + let content_rect = self.content_ui.min_rect(); + let widget_rect = self.frame.widget_rect(content_rect); - if ui.is_rect_visible(paint_rect) { - let shape = self.frame.paint(paint_rect); + if ui.is_rect_visible(widget_rect) { + let shape = self.frame.paint(content_rect); ui.painter().set(self.where_to_put_background, shape); } } /// Convenience for calling [`Self::allocate_space`] and [`Self::paint`]. + /// + /// Returns the outer rect, i.e. including the outer margin. pub fn end(self, ui: &mut Ui) -> Response { self.paint(ui); self.allocate_space(ui) diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index d487a27dd..f1890feed 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -2,15 +2,11 @@ use std::sync::Arc; -use crate::collapsing_header::CollapsingState; -use crate::{ - Align, Align2, Context, CursorIcon, Id, InnerResponse, LayerId, NumExt, Order, Response, Sense, - TextStyle, Ui, UiKind, Vec2b, WidgetInfo, WidgetRect, WidgetText, WidgetType, -}; use emath::GuiRounding as _; -use epaint::{ - emath, pos2, vec2, Galley, Pos2, Rect, RectShape, Rounding, Roundingf, Shape, Stroke, Vec2, -}; +use epaint::{RectShape, Roundingf}; + +use crate::collapsing_header::CollapsingState; +use crate::*; use super::scroll_area::ScrollBarVisibility; use super::{area, resize, Area, Frame, Resize, ScrollArea}; @@ -452,8 +448,6 @@ impl<'open> Window<'open> { let header_color = frame.map_or_else(|| ctx.style().visuals.widgets.open.weak_bg_fill, |f| f.fill); let mut window_frame = frame.unwrap_or_else(|| Frame::window(&ctx.style())); - // Keep the original inner margin for later use - let window_margin = window_frame.inner_margin; let is_explicitly_closed = matches!(open, Some(false)); let is_open = !is_explicitly_closed || ctx.memory(|mem| mem.everything_is_visible()); @@ -483,15 +477,23 @@ impl<'open> Window<'open> { area.with_widget_info(|| WidgetInfo::labeled(WidgetType::Window, true, title.text())); - // Calculate roughly how much larger the window size is compared to the inner rect - let (title_bar_height, title_content_spacing) = if with_title_bar { + // Calculate roughly how much larger the full window inner size is compared to the content rect + let (title_bar_height_with_margin, title_content_spacing) = if with_title_bar { let style = ctx.style(); - let spacing = window_margin.sum().y; - let height = ctx.fonts(|f| title.font_height(f, &style)) + spacing; - let half_height = (height / 2.0).round() as _; + let title_bar_inner_height = ctx + .fonts(|fonts| title.font_height(fonts, &style)) + .at_least(style.spacing.interact_size.y); + let title_bar_inner_height = title_bar_inner_height + window_frame.inner_margin.sum().y; + let half_height = (title_bar_inner_height / 2.0).round() as _; window_frame.rounding.ne = window_frame.rounding.ne.clamp(0, half_height); window_frame.rounding.nw = window_frame.rounding.nw.clamp(0, half_height); - (height, spacing) + + let title_content_spacing = if is_collapsed { + 0.0 + } else { + window_frame.stroke.width + }; + (title_bar_inner_height, title_content_spacing) } else { (0.0, 0.0) }; @@ -500,7 +502,8 @@ impl<'open> Window<'open> { // Prevent window from becoming larger than the constrain rect. let constrain_rect = area.constrain_rect(); let max_width = constrain_rect.width(); - let max_height = constrain_rect.height() - title_bar_height; + let max_height = + constrain_rect.height() - title_bar_height_with_margin - title_content_spacing; resize.max_size.x = resize.max_size.x.min(max_width); resize.max_size.y = resize.max_size.y.min(max_height); } @@ -508,21 +511,28 @@ impl<'open> Window<'open> { // First check for resize to avoid frame delay: let last_frame_outer_rect = area.state().rect(); let resize_interaction = ctx.with_accessibility_parent(area.id(), || { - resize_interaction(ctx, possible, area_layer_id, last_frame_outer_rect) + resize_interaction( + ctx, + possible, + area_layer_id, + last_frame_outer_rect, + window_frame, + ) }); - let margins = window_frame.outer_margin.sum() - + window_frame.inner_margin.sum() - + vec2(0.0, title_bar_height); + { + let margins = window_frame.total_margin().sum() + + vec2(0.0, title_bar_height_with_margin + title_content_spacing); - resize_response( - resize_interaction, - ctx, - margins, - area_layer_id, - &mut area, - resize_id, - ); + resize_response( + resize_interaction, + ctx, + margins, + area_layer_id, + &mut area, + resize_id, + ); + } let mut area_content_ui = area.content_ui(ctx); if is_open { @@ -535,40 +545,43 @@ impl<'open> Window<'open> { let content_inner = { ctx.with_accessibility_parent(area.id(), || { // BEGIN FRAME -------------------------------- - let frame_stroke = window_frame.stroke; let mut frame = window_frame.begin(&mut area_content_ui); let show_close_button = open.is_some(); let where_to_put_header_background = &area_content_ui.painter().add(Shape::Noop); - // Backup item spacing before the title bar - let item_spacing = frame.content_ui.spacing().item_spacing; - // Use title bar spacing as the item spacing before the content - frame.content_ui.spacing_mut().item_spacing.y = title_content_spacing; - let title_bar = if with_title_bar { let title_bar = TitleBar::new( - &mut frame.content_ui, + &frame.content_ui, title, show_close_button, - &mut collapsing, collapsible, + window_frame, + title_bar_height_with_margin, ); - resize.min_size.x = resize.min_size.x.at_least(title_bar.rect.width()); // Prevent making window smaller than title bar width + resize.min_size.x = resize.min_size.x.at_least(title_bar.inner_rect.width()); // Prevent making window smaller than title bar width + + frame.content_ui.set_min_size(title_bar.inner_rect.size()); + + // Skip the title bar (and separator): + if is_collapsed { + frame.content_ui.add_space(title_bar.inner_rect.height()); + } else { + frame.content_ui.add_space( + title_bar.inner_rect.height() + + title_content_spacing + + window_frame.inner_margin.sum().y, + ); + } + Some(title_bar) } else { None }; - // Remove item spacing after the title bar - frame.content_ui.spacing_mut().item_spacing.y = 0.0; - - let (content_inner, mut content_response) = collapsing + let (content_inner, content_response) = collapsing .show_body_unindented(&mut frame.content_ui, |ui| { - // Restore item spacing for the content - ui.spacing_mut().item_spacing.y = item_spacing.y; - resize.show(ui, |ui| { if scroll.is_any_scroll_enabled() { scroll.show(ui, add_contents).inner @@ -584,23 +597,18 @@ impl<'open> Window<'open> { &area_content_ui, &possible, outer_rect, - frame_stroke, - window_frame.rounding, + &window_frame, resize_interaction, ); // END FRAME -------------------------------- - if let Some(title_bar) = title_bar { - let mut title_rect = Rect::from_min_size( - outer_rect.min, - Vec2 { - x: outer_rect.size().x, - y: title_bar_height, - }, - ); - - title_rect = title_rect.round_to_pixels(area_content_ui.pixels_per_point()); + if let Some(mut title_bar) = title_bar { + title_bar.inner_rect = outer_rect.shrink(window_frame.stroke.width); + title_bar.inner_rect.max.y = + title_bar.inner_rect.min.y + title_bar_height_with_margin; + title_bar.inner_rect = + title_bar.inner_rect.round_to_pixels(ctx.pixels_per_point()); if on_top && area_content_ui.visuals().window_highlight_topmost { let mut round = window_frame.rounding; @@ -612,18 +620,20 @@ impl<'open> Window<'open> { area_content_ui.painter().set( *where_to_put_header_background, - RectShape::filled(title_rect, round, header_color), + RectShape::filled(title_bar.inner_rect, round, header_color), ); }; - // Fix title bar separator line position - if let Some(response) = &mut content_response { - response.rect.min.y = outer_rect.min.y + title_bar_height; + if false { + ctx.debug_painter().debug_rect( + title_bar.inner_rect, + Color32::LIGHT_BLUE, + "title_bar.rect", + ); } title_bar.ui( &mut area_content_ui, - title_rect, &content_response, open, &mut collapsing, @@ -653,12 +663,11 @@ fn paint_resize_corner( ui: &Ui, possible: &PossibleInteractions, outer_rect: Rect, - stroke: impl Into, - rounding: impl Into, + window_frame: &Frame, i: ResizeInteraction, ) { - let inactive_stroke = stroke.into(); - let rounding = rounding.into(); + let rounding = window_frame.rounding; + let (corner, radius, corner_response) = if possible.resize_right && possible.resize_bottom { (Align2::RIGHT_BOTTOM, rounding.se, i.right & i.bottom) } else if possible.resize_left && possible.resize_bottom { @@ -694,11 +703,12 @@ fn paint_resize_corner( } else if corner_response.hover { ui.visuals().widgets.hovered.fg_stroke } else { - inactive_stroke + window_frame.stroke }; + let fill_rect = outer_rect.shrink(window_frame.stroke.width); let corner_size = Vec2::splat(ui.visuals().resize_corner_size); - let corner_rect = corner.align_size_within_rect(corner_size, outer_rect); + let corner_rect = corner.align_size_within_rect(corner_size, fill_rect); let corner_rect = corner_rect.translate(-offset * corner.to_sign()); // move away from corner crate::resize::paint_resize_corner_with_style(ui, &corner_rect, stroke.color, corner); } @@ -738,7 +748,11 @@ impl PossibleInteractions { /// Resizing the window edges. #[derive(Clone, Copy, Debug)] struct ResizeInteraction { - start_rect: Rect, + /// Outer rect (outside the stroke) + outer_rect: Rect, + + window_frame: Frame, + left: SideResponse, right: SideResponse, top: SideResponse, @@ -835,13 +849,17 @@ fn resize_response( ctx.memory_mut(|mem| mem.areas_mut().move_to_top(area_layer_id)); } +/// Acts on outer rect (outside the stroke) fn move_and_resize_window(ctx: &Context, interaction: &ResizeInteraction) -> Option { if !interaction.any_dragged() { return None; } let pointer_pos = ctx.input(|i| i.pointer.interact_pos())?; - let mut rect = interaction.start_rect; // prevent drift + let mut rect = interaction.outer_rect; // prevent drift + + // Put the rect in the center of the stroke: + rect = rect.shrink(interaction.window_frame.stroke.width / 2.0); if interaction.left.drag { rect.min.x = pointer_pos.x; @@ -855,6 +873,9 @@ fn move_and_resize_window(ctx: &Context, interaction: &ResizeInteraction) -> Opt rect.max.y = pointer_pos.y; } + // Return to having the rect outside the stroke: + rect = rect.expand(interaction.window_frame.stroke.width / 2.0); + Some(rect.round_ui()) } @@ -862,11 +883,13 @@ fn resize_interaction( ctx: &Context, possible: PossibleInteractions, layer_id: LayerId, - rect: Rect, + outer_rect: Rect, + window_frame: Frame, ) -> ResizeInteraction { if !possible.resizable() { return ResizeInteraction { - start_rect: rect, + outer_rect, + window_frame, left: Default::default(), right: Default::default(), top: Default::default(), @@ -874,6 +897,9 @@ fn resize_interaction( }; } + // The rect that is in the middle of the stroke: + let rect = outer_rect.shrink(window_frame.stroke.width / 2.0); + let side_response = |rect, id| { let response = ctx.create_widget( WidgetRect { @@ -990,7 +1016,8 @@ fn resize_interaction( } let interaction = ResizeInteraction { - start_rect: rect, + outer_rect, + window_frame, left, right, top, @@ -1027,6 +1054,18 @@ fn paint_frame_interaction(ui: &Ui, rect: Rect, interaction: ResizeInteraction) } let rounding = Roundingf::from(ui.visuals().window_rounding); + + // Put the rect in the center of the fixed window stroke: + let rect = rect.shrink(interaction.window_frame.stroke.width / 2.0); + + // Make sure the inner part of the stroke is at a pixel boundary: + let stroke = visuals.bg_stroke; + let half_stroke = stroke.width / 2.0; + let rect = rect + .shrink(half_stroke) + .round_to_pixels(ui.pixels_per_point()) + .expand(half_stroke); + let Rect { min, max } = rect; let mut points = Vec::new(); @@ -1083,80 +1122,74 @@ fn paint_frame_interaction(ui: &Ui, rect: Rect, interaction: ResizeInteraction) points.push(pos2(max.x, min.y + rounding.ne)); points.push(pos2(max.x, max.y - rounding.se)); } - ui.painter().add(Shape::line(points, visuals.bg_stroke)); + + ui.painter().add(Shape::line(points, stroke)); } // ---------------------------------------------------------------------------- struct TitleBar { - /// A title Id used for dragging windows - id: Id, + window_frame: Frame, /// Prepared text in the title title_galley: Arc, - /// Size of the title bar in a collapsed state (if window is collapsible), - /// which includes all necessary space for showing the expand button, the - /// title and the close button. - min_rect: Rect, - /// Size of the title bar in an expanded state. This size become known only - /// after expanding window and painting its content - rect: Rect, + /// after expanding window and painting its content. + /// + /// Does not include the stroke, nor the separator line between the title bar and the window contents. + inner_rect: Rect, } impl TitleBar { fn new( - ui: &mut Ui, + ui: &Ui, title: WidgetText, show_close_button: bool, - collapsing: &mut CollapsingState, collapsible: bool, + window_frame: Frame, + title_bar_height_with_margin: f32, ) -> Self { - let inner_response = ui.horizontal(|ui| { - let height = ui - .fonts(|fonts| title.font_height(fonts, ui.style())) - .max(ui.spacing().interact_size.y); - ui.set_min_height(height); + if false { + ui.ctx() + .debug_painter() + .debug_rect(ui.min_rect(), Color32::GREEN, "outer_min_rect"); + } - let item_spacing = ui.spacing().item_spacing; - let button_size = Vec2::splat(ui.spacing().icon_width); + let inner_height = title_bar_height_with_margin - window_frame.inner_margin.sum().y; - let pad = ((height - button_size.y) / 2.0).round_ui(); // calculated so that the icon is on the diagonal (if window padding is symmetrical) + let item_spacing = ui.spacing().item_spacing; + let button_size = Vec2::splat(ui.spacing().icon_width.at_most(inner_height)); - if collapsible { - ui.add_space(pad); - collapsing.show_default_button_with_size(ui, button_size); - } + let left_pad = ((inner_height - button_size.y) / 2.0).round_ui(); // calculated so that the icon is on the diagonal (if window padding is symmetrical) - let title_galley = title.into_galley( - ui, - Some(crate::TextWrapMode::Extend), - f32::INFINITY, - TextStyle::Heading, - ); + let title_galley = title.into_galley( + ui, + Some(crate::TextWrapMode::Extend), + f32::INFINITY, + TextStyle::Heading, + ); - let minimum_width = if collapsible || show_close_button { - // If at least one button is shown we make room for both buttons (since title is centered): - 2.0 * (pad + button_size.x + item_spacing.x) + title_galley.size().x - } else { - pad + title_galley.size().x + pad - }; - let min_rect = Rect::from_min_size(ui.min_rect().min, vec2(minimum_width, height)); - let id = ui.advance_cursor_after_rect(min_rect); + let minimum_width = if collapsible || show_close_button { + // If at least one button is shown we make room for both buttons (since title should be centered): + 2.0 * (left_pad + button_size.x + item_spacing.x) + title_galley.size().x + } else { + left_pad + title_galley.size().x + left_pad + }; + let min_inner_size = vec2(minimum_width, inner_height); + let min_rect = Rect::from_min_size(ui.min_rect().min, min_inner_size); - Self { - id, - title_galley, - min_rect, - rect: Rect::NAN, // Will be filled in later - } - }); + if false { + ui.ctx() + .debug_painter() + .debug_rect(min_rect, Color32::LIGHT_BLUE, "min_rect"); + } - let title_bar = inner_response.inner; - let rect = inner_response.response.rect; - - Self { rect, ..title_bar } + Self { + window_frame, + title_galley, + inner_rect: min_rect, // First estimate - will be refined later + } } /// Finishes painting of the title bar when the window content size already known. @@ -1174,17 +1207,34 @@ impl TitleBar { /// - `collapsible`: if `true`, double click on the title bar will be handled for a change /// of `collapsing` state fn ui( - mut self, + self, ui: &mut Ui, - outer_rect: Rect, content_response: &Option, open: Option<&mut bool>, collapsing: &mut CollapsingState, collapsible: bool, ) { - if let Some(content_response) = &content_response { - // Now we know how large we got to be: - self.rect.max.x = self.rect.max.x.max(content_response.rect.max.x); + let window_frame = self.window_frame; + let title_inner_rect = self.inner_rect; + + if false { + ui.ctx() + .debug_painter() + .debug_rect(self.inner_rect, Color32::RED, "TitleBar"); + } + + if collapsible { + // Show collapse-button: + let button_center = Align2::LEFT_CENTER + .align_size_within_rect(Vec2::splat(self.inner_rect.height()), self.inner_rect) + .center(); + let button_size = Vec2::splat(ui.spacing().icon_width); + let button_rect = Rect::from_center_size(button_center, button_size); + let button_rect = button_rect.round_to_pixels(ui.pixels_per_point()); + + ui.allocate_new_ui(UiBuilder::new().max_rect(button_rect), |ui| { + collapsing.show_default_button_with_size(ui, button_size); + }); } if let Some(open) = open { @@ -1194,9 +1244,9 @@ impl TitleBar { } } - let full_top_rect = Rect::from_x_y_ranges(self.rect.x_range(), self.min_rect.y_range()); let text_pos = - emath::align::center_size_in_rect(self.title_galley.size(), full_top_rect).left_top(); + emath::align::center_size_in_rect(self.title_galley.size(), title_inner_rect) + .left_top(); let text_pos = text_pos - self.title_galley.rect.min.to_vec2(); ui.painter().galley( text_pos, @@ -1205,22 +1255,35 @@ impl TitleBar { ); if let Some(content_response) = &content_response { - // paint separator between title and content: - let y = content_response.rect.top(); - // let y = lerp(self.rect.bottom()..=content_response.rect.top(), 0.5); - let stroke = ui.visuals().widgets.noninteractive.bg_stroke; - // Workaround: To prevent border infringement, - // the 0.1 value should ideally be calculated using TessellationOptions::feathering_size_in_pixels - // or we could support selectively disabling feathering on line caps - let x_range = outer_rect.x_range().shrink(0.1); - ui.painter().hline(x_range, y, stroke); + // Paint separator between title and content: + let content_rect = content_response.rect; + if false { + ui.ctx() + .debug_painter() + .debug_rect(content_rect, Color32::RED, "content_rect"); + } + let y = title_inner_rect.bottom() + window_frame.stroke.width / 2.0; + + // To verify the sanity of this, use a very wide window stroke + ui.painter() + .hline(title_inner_rect.x_range(), y, window_frame.stroke); } // Don't cover the close- and collapse buttons: - let double_click_rect = self.rect.shrink2(vec2(32.0, 0.0)); + let double_click_rect = title_inner_rect.shrink2(vec2(32.0, 0.0)); + + if false { + ui.ctx().debug_painter().debug_rect( + double_click_rect, + Color32::GREEN, + "double_click_rect", + ); + } + + let id = ui.unique_id().with("__window_title_bar"); if ui - .interact(double_click_rect, self.id, Sense::click()) + .interact(double_click_rect, id, Sense::click()) .double_clicked() && collapsible { @@ -1234,16 +1297,12 @@ impl TitleBar { /// The button is square and its size is determined by the /// [`crate::style::Spacing::icon_width`] setting. fn close_button_ui(&self, ui: &mut Ui) -> Response { + let button_center = Align2::RIGHT_CENTER + .align_size_within_rect(Vec2::splat(self.inner_rect.height()), self.inner_rect) + .center(); let button_size = Vec2::splat(ui.spacing().icon_width); - let pad = (self.rect.height() - button_size.y) / 2.0; // calculated so that the icon is on the diagonal (if window padding is symmetrical) - let button_rect = Rect::from_min_size( - pos2( - self.rect.right() - pad - button_size.x, - self.rect.center().y - 0.5 * button_size.y, - ), - button_size, - ); - + let button_rect = Rect::from_center_size(button_center, button_size); + let button_rect = button_rect.round_to_pixels(ui.pixels_per_point()); close_button(ui, button_rect) } } diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 19fda5d0f..acc08ca37 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -1771,12 +1771,14 @@ impl Ui { /// Add extra space before the next widget. /// /// The direction is dependent on the layout. - /// This will be in addition to the [`crate::style::Spacing::item_spacing`]. + /// + /// This will be in addition to the [`crate::style::Spacing::item_spacing`] + /// that is always added, but `item_spacing` won't be added _again_ by `add_space`. /// /// [`Self::min_rect`] will expand to contain the space. #[inline] pub fn add_space(&mut self, amount: f32) { - self.placer.advance_cursor(amount); + self.placer.advance_cursor(amount.round_ui()); } /// Show some text. diff --git a/crates/egui_demo_app/src/wrap_app.rs b/crates/egui_demo_app/src/wrap_app.rs index 117824e01..6409eb902 100644 --- a/crates/egui_demo_app/src/wrap_app.rs +++ b/crates/egui_demo_app/src/wrap_app.rs @@ -44,7 +44,11 @@ pub struct FractalClockApp { impl eframe::App for FractalClockApp { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { egui::CentralPanel::default() - .frame(egui::Frame::dark_canvas(&ctx.style())) + .frame( + egui::Frame::dark_canvas(&ctx.style()) + .stroke(egui::Stroke::NONE) + .rounding(0), + ) .show(ctx, |ui| { self.fractal_clock .ui(ui, self.mock_time.or(Some(crate::seconds_since_midnight()))); @@ -293,7 +297,7 @@ impl eframe::App for WrapApp { let mut cmd = Command::Nothing; egui::TopBottomPanel::top("wrap_app_top_bar") - .frame(egui::Frame::none().inner_margin(4.0)) + .frame(egui::Frame::new().inner_margin(4)) .show(ctx, |ui| { ui.horizontal_wrapped(|ui| { ui.visuals_mut().button_frame = false; diff --git a/crates/egui_demo_app/tests/snapshots/clock.png b/crates/egui_demo_app/tests/snapshots/clock.png index 51d271ddd..d97230494 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:7c05cc3d48242e46a391af34cb56f72de7933bf2cead009b6cd477c21867a84e -size 327802 +oid sha256:4aeab31841dd95b5e0f4bd0af0c0ba49a862d50836dbafdf2172fbbab950c105 +size 327741 diff --git a/crates/egui_demo_app/tests/snapshots/custom3d.png b/crates/egui_demo_app/tests/snapshots/custom3d.png index 1e51e9f6a..b138e53b9 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:61212e30fe1fecf5891ddad6ac795df510bfad76b21a7a8a13aa024fdad6d05e -size 93118 +oid sha256:0e4a90792a9876da549f3d1da9b057a078400ad15db2cc6e35f4324851137d4e +size 93115 diff --git a/crates/egui_demo_lib/src/demo/frame_demo.rs b/crates/egui_demo_lib/src/demo/frame_demo.rs index b772eb750..71c5e9ab2 100644 --- a/crates/egui_demo_lib/src/demo/frame_demo.rs +++ b/crates/egui_demo_lib/src/demo/frame_demo.rs @@ -7,19 +7,18 @@ pub struct FrameDemo { impl Default for FrameDemo { fn default() -> Self { Self { - frame: egui::Frame { - inner_margin: 12.0.into(), - outer_margin: 24.0.into(), - rounding: 14.0.into(), - shadow: egui::Shadow { + frame: egui::Frame::new() + .inner_margin(12) + .outer_margin(24) + .rounding(14) + .shadow(egui::Shadow { offset: [8, 12], blur: 16, spread: 0, color: egui::Color32::from_black_alpha(180), - }, - fill: egui::Color32::from_rgba_unmultiplied(97, 0, 255, 128), - stroke: egui::Stroke::new(1.0, egui::Color32::GRAY), - }, + }) + .fill(egui::Color32::from_rgba_unmultiplied(97, 0, 255, 128)) + .stroke(egui::Stroke::new(1.0, egui::Color32::GRAY)), } } } 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 0fd6b7fb6..f3edf3955 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, Frame, Resize, RichText, - Sense, Slider, Stroke, TextFormat, TextStyle, Ui, Vec2, Window, + vec2, Align, Checkbox, CollapsingHeader, Color32, Context, FontId, Resize, RichText, Sense, + Slider, Stroke, TextFormat, TextStyle, Ui, Vec2, Window, }; /// Showcase some ui code @@ -512,54 +512,52 @@ fn ui_stack_demo(ui: &mut Ui) { ); }); let stack = ui.stack().clone(); - Frame { - inner_margin: ui.spacing().menu_margin, - stroke: ui.visuals().widgets.noninteractive.bg_stroke, - ..Default::default() - } - .show(ui, |ui| { - egui_extras::TableBuilder::new(ui) - .column(egui_extras::Column::auto()) - .column(egui_extras::Column::auto()) - .header(18.0, |mut header| { - header.col(|ui| { - ui.strong("id"); - }); - header.col(|ui| { - ui.strong("kind"); - }); - }) - .body(|mut body| { - for node in stack.iter() { - body.row(18.0, |mut row| { - row.col(|ui| { - let response = ui.label(format!("{:?}", node.id)); + egui::Frame::new() + .inner_margin(ui.spacing().menu_margin) + .stroke(ui.visuals().widgets.noninteractive.bg_stroke) + .show(ui, |ui| { + egui_extras::TableBuilder::new(ui) + .column(egui_extras::Column::auto()) + .column(egui_extras::Column::auto()) + .header(18.0, |mut header| { + header.col(|ui| { + ui.strong("id"); + }); + header.col(|ui| { + ui.strong("kind"); + }); + }) + .body(|mut body| { + for node in stack.iter() { + body.row(18.0, |mut row| { + row.col(|ui| { + let response = ui.label(format!("{:?}", node.id)); - if response.hovered() { - ui.ctx().debug_painter().debug_rect( - node.max_rect, - Color32::GREEN, - "max_rect", - ); - ui.ctx().debug_painter().circle_filled( - node.min_rect.min, - 2.0, - Color32::RED, - ); - } - }); + if response.hovered() { + ui.ctx().debug_painter().debug_rect( + node.max_rect, + Color32::GREEN, + "max_rect", + ); + ui.ctx().debug_painter().circle_filled( + node.min_rect.min, + 2.0, + Color32::RED, + ); + } + }); - row.col(|ui| { - ui.label(if let Some(kind) = node.kind() { - format!("{kind:?}") - } else { - "-".to_owned() + row.col(|ui| { + ui.label(if let Some(kind) = node.kind() { + format!("{kind:?}") + } else { + "-".to_owned() + }); }); }); - }); - } - }); - }); + } + }); + }); ui.small("Hover on UI's ids to display their origin and max rect."); } diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Bézier Curve.png b/crates/egui_demo_lib/tests/snapshots/demos/Bézier Curve.png index c4cd0bd10..09dc7549a 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Bézier Curve.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Bézier Curve.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf83bead834ec8f88d74b32ae6331715e8c6df183e007e2a16004c019534a30f -size 31810 +oid sha256:d4cfd5191dc7046a782ef2350dc8e0547d2702182badcb15b6b928ce077b76c1 +size 32154 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png b/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png index 703c9ef19..242171125 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0a1099b85a1aaf20f3f1e091bc68259f811737feaefdfcc12acd067eca8f9117 -size 27083 +oid sha256:e89c730b462c2b60b90f2ac15fe9576e878a4906c223317c51344a0ec2b6d993 +size 27564 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png b/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png index 0f417f0b4..99885a8aa 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6969c6da67ea6cc7ebbbd7a2cc1cb13d4720befe28126367cbf2b2679d037674 -size 82363 +oid sha256:ea2c944af8bc1be42ec7c00be58dfaa23c92bca8957eda94f2ff10f5b4242562 +size 83358 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Context Menus.png b/crates/egui_demo_lib/tests/snapshots/demos/Context Menus.png index caa3c3a5d..d32a46ce7 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Context Menus.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Context Menus.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:332c2af36873d8ccccb36c08fd2e475dc1f18454a3090a851c0889395d4f364f -size 11518 +oid sha256:c401ff91fff4051042528d398d2b2270a4ae924570e6332cf8f2c6774c845160 +size 11826 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png b/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png index e86d10772..4f44756de 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d0d0b1b4d2c4b624904250bc8d6870559f0179e3f7f2d6dc4a4ff256df356237 -size 20626 +oid sha256:7efc1ff3e4e5bfd4216394f94ee7486c272a9ca1c980789f4ad143f89b0a7103 +size 21073 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png b/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png index 38199566d..8a831b65d 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d5fe6166bb8cd5fae0899957e968312b9229a79e423a5d154eda483a149b264d -size 20831 +oid sha256:d9c48cf928a17dd0980ba086aa004bde3a0040dcb82752d138c1df34f1ef3d2f +size 21167 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Extra Viewport.png b/crates/egui_demo_lib/tests/snapshots/demos/Extra Viewport.png index 96adb6942..05d87fa8b 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Extra Viewport.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Extra Viewport.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b71e1d109f90e017644dd20b9d84d81e3a6d5195afbd01ba86c85fa248c8b5c5 -size 10703 +oid sha256:be0f96c700b7662aab5098f8412dae3676116eeed65e70f6b295dd3375b329d0 +size 10968 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png b/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png index 10f5d0b40..f85436ef7 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e3c9ba9064f44a4a14405f53316c1c602184caf16cb584d7c1f1912fe59f85ab -size 135712 +oid sha256:dc69c76eaa121e9e7782cfbbb68b5a23004d79862bae4af2e3ca3a29eff04bea +size 136467 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Frame.png b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png index e896f366e..cec956325 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Frame.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:097bd512dd71c17370f6e374312c19e7ab6845f151b3c3565f2a0790b61ee7ba -size 24413 +oid sha256:23187a9fb12a3ab7df4e2321aa25b493559923d61e82802f843ee29dcd932f7b +size 24985 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png b/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png index ea3173224..12d396cb4 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2bdf54573a6b0d2fedd90314f91dd7de22dd13709e8dd699b37ef8935b6adda5 -size 17785 +oid sha256:7f433f3e8bff38a0aafd7e6cba5c5efe1abf484550a6f9e90008f8f5ea891497 +size 18113 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png b/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png index 68d03d9fd..576b7b328 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d6328c86b70f3e24aaf87db200d13dfd0baa787dd8431e47621457937f8f5021 -size 22552 +oid sha256:e5105ecf77852412c0dd904b96f0fec752f22e416df9932df4499d6d5a776f46 +size 22865 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 dcd12b097..db0322f86 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:afb57dc0bb8ff839138e36b8780136e0c8da55ff380832538fae0394143807c0 -size 65321 +oid sha256:ebf0403bd599e5c00c2831f9c4032e8d20420212c9cd7fa875f1ae1cbbc8d3a7 +size 65902 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Modals.png b/crates/egui_demo_lib/tests/snapshots/demos/Modals.png index 3341eebff..20ed40f4b 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Modals.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Modals.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:403fc1d08d79168bc9f7a08ac7f363f2df916547f08311838edfda8a499a9b2d -size 32879 +oid sha256:4e690dc73873ab75c030d3c0238e9d5b840f976dd8f4882dc1e930024d217838 +size 33323 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png b/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png index 882279e03..68470555b 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cbd490a15377bdd4fd3272f3cd126cbc4fb9ed2f6f84edcbba03dd30dc3f3d99 -size 36780 +oid sha256:e760210371dbf2a197f96a78d01b7480f0ae05d46bbb4e642276b2eb30847ec2 +size 37075 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Painting.png b/crates/egui_demo_lib/tests/snapshots/demos/Painting.png index 40cb73fe0..a2f6c14ad 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Painting.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Painting.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c31b3998817e345b12b037d7f8bec7641f77d0c7eab7da9a746b7b51c9efc8fb -size 17531 +oid sha256:fd02b208d0e4e306bbc9a54f25f5a3d20875a12182cef3224e6daa309b6cf453 +size 17898 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Pan Zoom.png b/crates/egui_demo_lib/tests/snapshots/demos/Pan Zoom.png index cecfb8432..c8f448fac 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Pan Zoom.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Pan Zoom.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4e8963c3ecd0e74fe9641d87101742a0d45c82a582d70e30eb36bc835f5aac06 -size 25330 +oid sha256:341958da648a7db3374c4337cf057ae8e81c08c4a6de7e4f1cbe9c5b049f2e62 +size 25727 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Panels.png b/crates/egui_demo_lib/tests/snapshots/demos/Panels.png index 6576f9ad5..9ce35877b 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Panels.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Panels.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:88b3a50b481942630b5804c60227f23b719dc7e3eb6dbe432c2448cb69332def -size 262141 +oid sha256:8934cff7203d19b38df9d91729091ff5d1ad6c8d445fd9c1cb62b6df1bb8cb80 +size 263547 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png b/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png index 4c97bde20..d9f7bc130 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4d5d628b54b28eccac0d9ef21bbdabace0cdf507898707956943e2259df846ca -size 23741 +oid sha256:5d61f58138798d701bb8dda2c3240eef69eb350df3168fb3aa4148e4fef3f77a +size 24077 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png b/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png index 98b10f30d..bb0b7a007 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:8763a8f8b4242cbe589cd3282dc5f4b32a84b4e0416fb8072dfefb7879a5d4f6 -size 187982 +oid sha256:e4006e93663d02fe0f4485d2c163ab2b6feded787bee87ea15616fc0b36136d0 +size 188875 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png b/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png index 19b7cf0ff..56d6d37ce 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:3d61088bf1f992467e8396ac89407fa158d6f44e2d2d196778247f3ff18cb893 -size 119759 +oid sha256:70b170ba7b8e51d9d9f766d7ce25068fa4265c4127e729af4f1adaacbb745d19 +size 120947 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Strip.png b/crates/egui_demo_lib/tests/snapshots/demos/Strip.png index 16a54ae0f..78a671578 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Strip.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Strip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:79ebaf9cccd19da2923507b5a553a23edc67382ef59e4b71f01d3bd6cc913539 -size 25829 +oid sha256:157353a8c9bcb638a8be485489e4a232f348eae3cc4ceefe227d7970c7d1f8b3 +size 26256 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Table.png b/crates/egui_demo_lib/tests/snapshots/demos/Table.png index 232a5ec32..89e92317b 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:06cbf13841e6ac5fbc57bdae6f5ad9d19718193c344420dedcc0e9d7ed2b8ba9 -size 71590 +oid sha256:cf0ddd39a45519dcf9027f691e856921c736d18e2eeafd16f0e086720121b6a7 +size 72286 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png b/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png index cc2da3a55..90259a56d 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:35e66f211c0b30a684371b520c46dbe4f9d5b6835e053a4eb65f492dd66a9e6c -size 67288 +oid sha256:914d37e326087f770994bcf3867a27d88050c57887a2b42c68414d311fa96003 +size 67698 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png b/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png index 4704ff75d..4bbe0f59b 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fa9ee8631bfe433ee6fad1fb1651fd6b63e2fb3fbc5f321a5410f7266dc16d09 -size 21296 +oid sha256:b0fdf8ce329883450e071e4733c3904577999d18ac61c922c7caacbec09dfda7 +size 21661 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png b/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png index 3e8a3ba3e..4f0a7bc17 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6475702b1bf2c65fb5196928a8934ade647a6053d6902a836e3d69cb7920091e -size 59874 +oid sha256:375b71a8ac5b0e48f3c67a089ef0e8a4fd17f8eb21fa535422099c29c2747e27 +size 59991 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png b/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png index c39aa9f85..f96b4ebee 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9d8daaec0c58709298a4594b7b2aa935aa2de327e6b71bd7417c2ba3a6eb060c -size 13020 +oid sha256:aa96b1e3733e4af94a6cb6ec765c3f3581df2175e75831eb00bd42df2e7a2708 +size 13285 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png b/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png index 5670cef58..102faa02a 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:29d8d859a8cb11e4b390d45c658ed8ff191c2e542909e12f2385c0cba62baa2d -size 35109 +oid sha256:18880dfaf5d198876c4db97ebd6313d59755a3e8298567f2b2fa91dcc21699c5 +size 35607 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_1.png b/crates/egui_demo_lib/tests/snapshots/modals_1.png index 0bd63cd50..46c12fa2d 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_1.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:17217e600d8a85ec00ecb11f0e5fe69496b30bbf715cc86785cec6e18b8c2fa1 -size 48158 +oid sha256:d626b310439bff13487548bbba8b516886c13049824a7f5dd902f6dffb3c5ba4 +size 48234 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_2.png b/crates/egui_demo_lib/tests/snapshots/modals_2.png index 8c496d81d..5ea3c5b3e 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_2.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fac50d2327a9e5e99989dd81d5644db86031b91b9c5c68fc17b5ef53ae655048 -size 47970 +oid sha256:80a2968e211c83639b60e84b805f1327fb37b87144cada672a403c7e92ace8a8 +size 48066 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_3.png b/crates/egui_demo_lib/tests/snapshots/modals_3.png index 1cc989ddc..5bb1fe23e 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_3.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e5e829257b742194742429be0efd326be17ff7f5b9b16f9df58df21e899320bd -size 43963 +oid sha256:86df5dc4b4ddd6f44226242b6d9b5e9f2aacd45193ae9f784fb5084a7a509e0b +size 43987 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png b/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png index 0246090d4..2c868b440 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5da064332c669a860a92a34b101f23e28026d4f07948f7c3e9a40e611f5e284f -size 43986 +oid sha256:1138bbc3b7e73cccd555f0fd58c27a5bda4d84484fdc1bd5223fc9802d0c5328 +size 44089 diff --git a/crates/egui_kittest/src/app_kind.rs b/crates/egui_kittest/src/app_kind.rs index 3b5cf9e73..081490e3d 100644 --- a/crates/egui_kittest/src/app_kind.rs +++ b/crates/egui_kittest/src/app_kind.rs @@ -56,7 +56,7 @@ impl<'a, State> AppKind<'a, State> { sizing_pass: bool, ) -> egui::Response { egui::CentralPanel::default() - .frame(Frame::none()) + .frame(Frame::NONE) .show(ctx, |ui| { let mut builder = egui::UiBuilder::new(); if sizing_pass { diff --git a/examples/custom_window_frame/src/main.rs b/examples/custom_window_frame/src/main.rs index 86e82759f..aefcdc677 100644 --- a/examples/custom_window_frame/src/main.rs +++ b/examples/custom_window_frame/src/main.rs @@ -45,13 +45,11 @@ impl eframe::App for MyApp { fn custom_window_frame(ctx: &egui::Context, title: &str, add_contents: impl FnOnce(&mut egui::Ui)) { use egui::{CentralPanel, UiBuilder}; - let panel_frame = egui::Frame { - fill: ctx.style().visuals.window_fill(), - rounding: 10.0.into(), - stroke: ctx.style().visuals.widgets.noninteractive.fg_stroke, - outer_margin: 1.0.into(), // so the stroke is within the bounds - ..Default::default() - }; + let panel_frame = egui::Frame::new() + .fill(ctx.style().visuals.window_fill()) + .rounding(10) + .stroke(ctx.style().visuals.widgets.noninteractive.fg_stroke) + .outer_margin(1); // so the stroke is within the bounds CentralPanel::default().frame(panel_frame).show(ctx, |ui| { let app_rect = ui.max_rect(); diff --git a/tests/test_ui_stack/src/main.rs b/tests/test_ui_stack/src/main.rs index a2f09af5c..47b4ee5ca 100644 --- a/tests/test_ui_stack/src/main.rs +++ b/tests/test_ui_stack/src/main.rs @@ -62,27 +62,23 @@ impl eframe::App for MyApp { // nested frames test ui.add_space(20.0); - egui::Frame { - stroke: ui.visuals().noninteractive().bg_stroke, - inner_margin: egui::Margin::same(4), - outer_margin: egui::Margin::same(4), - ..Default::default() - } - .show(ui, |ui| { - full_span_widget(ui, false); - stack_ui(ui); - - egui::Frame { - stroke: ui.visuals().noninteractive().bg_stroke, - inner_margin: egui::Margin::same(8), - outer_margin: egui::Margin::same(6), - ..Default::default() - } + egui::Frame::new() + .stroke(ui.visuals().noninteractive().bg_stroke) + .inner_margin(4) + .outer_margin(4) .show(ui, |ui| { full_span_widget(ui, false); stack_ui(ui); + + egui::Frame::new() + .stroke(ui.visuals().noninteractive().bg_stroke) + .inner_margin(8) + .outer_margin(6) + .show(ui, |ui| { + full_span_widget(ui, false); + stack_ui(ui); + }); }); - }); }); }); @@ -126,18 +122,16 @@ impl eframe::App for MyApp { // Ui nesting test ui.add_space(20.0); ui.label("UI nesting test:"); - egui::Frame { - stroke: ui.visuals().noninteractive().bg_stroke, - inner_margin: egui::Margin::same(4), - ..Default::default() - } - .show(ui, |ui| { - ui.horizontal(|ui| { - ui.vertical(|ui| { - ui.scope(stack_ui); + egui::Frame::new() + .stroke(ui.visuals().noninteractive().bg_stroke) + .inner_margin(4) + .show(ui, |ui| { + ui.horizontal(|ui| { + ui.vertical(|ui| { + ui.scope(stack_ui); + }); }); }); - }); // table test let mut cell_stack = None; @@ -265,106 +259,104 @@ fn stack_ui(ui: &mut egui::Ui) { } fn stack_ui_impl(ui: &mut egui::Ui, stack: &egui::UiStack) { - egui::Frame { - stroke: ui.style().noninteractive().fg_stroke, - inner_margin: egui::Margin::same(4), - ..Default::default() - } - .show(ui, |ui| { - ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); + egui::Frame::new() + .stroke(ui.style().noninteractive().fg_stroke) + .inner_margin(4) + .show(ui, |ui| { + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); - egui_extras::TableBuilder::new(ui) - .column(Column::auto()) - .column(Column::auto()) - .column(Column::auto()) - .column(Column::auto()) - .column(Column::auto()) - .column(Column::auto()) - .header(20.0, |mut header| { - header.col(|ui| { - ui.strong("id"); - }); - header.col(|ui| { - ui.strong("kind"); - }); - header.col(|ui| { - ui.strong("stroke"); - }); - header.col(|ui| { - ui.strong("inner"); - }); - header.col(|ui| { - ui.strong("outer"); - }); - header.col(|ui| { - ui.strong("direction"); - }); - }) - .body(|mut body| { - for node in stack.iter() { - body.row(20.0, |mut row| { - row.col(|ui| { - if ui.label(format!("{:?}", node.id)).hovered() { - ui.ctx().debug_painter().debug_rect( - node.max_rect, - egui::Color32::GREEN, - "max", - ); - ui.ctx().debug_painter().circle_filled( - node.min_rect.min, - 2.0, - egui::Color32::RED, - ); - } - }); - row.col(|ui| { - let s = if let Some(kind) = node.kind() { - format!("{kind:?}") - } else { - "-".to_owned() - }; - - ui.label(s); - }); - row.col(|ui| { - let frame = node.frame(); - if frame.stroke == egui::Stroke::NONE { - ui.label("-"); - } else { - let mut layout_job = egui::text::LayoutJob::default(); - layout_job.append( - "⬛ ", - 0.0, - egui::TextFormat::simple( - egui::TextStyle::Body.resolve(ui.style()), - frame.stroke.color, - ), - ); - layout_job.append( - format!("{}px", frame.stroke.width).as_str(), - 0.0, - egui::TextFormat::simple( - egui::TextStyle::Body.resolve(ui.style()), - ui.style().visuals.text_color(), - ), - ); - ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); - ui.label(layout_job); - } - }); - row.col(|ui| { - ui.label(print_margin(&node.frame().inner_margin)); - }); - row.col(|ui| { - ui.label(print_margin(&node.frame().outer_margin)); - }); - row.col(|ui| { - ui.label(format!("{:?}", node.layout_direction)); - }); + egui_extras::TableBuilder::new(ui) + .column(Column::auto()) + .column(Column::auto()) + .column(Column::auto()) + .column(Column::auto()) + .column(Column::auto()) + .column(Column::auto()) + .header(20.0, |mut header| { + header.col(|ui| { + ui.strong("id"); }); - } - }); - }); + header.col(|ui| { + ui.strong("kind"); + }); + header.col(|ui| { + ui.strong("stroke"); + }); + header.col(|ui| { + ui.strong("inner"); + }); + header.col(|ui| { + ui.strong("outer"); + }); + header.col(|ui| { + ui.strong("direction"); + }); + }) + .body(|mut body| { + for node in stack.iter() { + body.row(20.0, |mut row| { + row.col(|ui| { + if ui.label(format!("{:?}", node.id)).hovered() { + ui.ctx().debug_painter().debug_rect( + node.max_rect, + egui::Color32::GREEN, + "max", + ); + ui.ctx().debug_painter().circle_filled( + node.min_rect.min, + 2.0, + egui::Color32::RED, + ); + } + }); + row.col(|ui| { + let s = if let Some(kind) = node.kind() { + format!("{kind:?}") + } else { + "-".to_owned() + }; + + ui.label(s); + }); + row.col(|ui| { + let frame = node.frame(); + if frame.stroke == egui::Stroke::NONE { + ui.label("-"); + } else { + let mut layout_job = egui::text::LayoutJob::default(); + layout_job.append( + "⬛ ", + 0.0, + egui::TextFormat::simple( + egui::TextStyle::Body.resolve(ui.style()), + frame.stroke.color, + ), + ); + layout_job.append( + format!("{}px", frame.stroke.width).as_str(), + 0.0, + egui::TextFormat::simple( + egui::TextStyle::Body.resolve(ui.style()), + ui.style().visuals.text_color(), + ), + ); + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); + ui.label(layout_job); + } + }); + row.col(|ui| { + ui.label(print_margin(&node.frame().inner_margin)); + }); + row.col(|ui| { + ui.label(print_margin(&node.frame().outer_margin)); + }); + row.col(|ui| { + ui.label(format!("{:?}", node.layout_direction)); + }); + }); + } + }); + }); } fn print_margin(margin: &egui::Margin) -> String { From 9073516e3001a2c517c60de0095defec3e5628ea Mon Sep 17 00:00:00 2001 From: Lander Brandt Date: Mon, 6 Jan 2025 00:19:17 -0800 Subject: [PATCH 005/132] Serialize window maximized state in `WindowSettings` (#5554) A user of my Windows application reported a papercut where the application restores its size on next load, but does not restore its maximized state. This PR fixes that. To test, I patched https://github.com/emilk/eframe_template to use my local code since I knew that template saves/restores window data. Testing methodology was to simply `cargo run`, maximize the application, then close the application. `cargo run` again and the application should start maximized. Closes #1517. * [x] I have followed the instructions in the PR template * * This is mostly true, I had difficulties running `./scripts/check.sh` for some reason. Possibly a bad Python version? --- crates/egui-winit/src/lib.rs | 3 +++ crates/egui-winit/src/window_settings.rs | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index f3612f501..147ab7186 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -1824,6 +1824,9 @@ pub fn apply_viewport_builder_to_window( let pos = PhysicalPosition::new(pixels_per_point * pos.x, pixels_per_point * pos.y); window.set_outer_position(pos); } + if let Some(maximized) = builder.maximized { + window.set_maximized(maximized); + } } } diff --git a/crates/egui-winit/src/window_settings.rs b/crates/egui-winit/src/window_settings.rs index 168a086c7..d15712d4c 100644 --- a/crates/egui-winit/src/window_settings.rs +++ b/crates/egui-winit/src/window_settings.rs @@ -13,6 +13,8 @@ pub struct WindowSettings { fullscreen: bool, + maximized: bool, + /// Inner size of window in logical pixels inner_size_points: Option, } @@ -38,6 +40,7 @@ impl WindowSettings { outer_position_pixels, fullscreen: window.fullscreen().is_some(), + maximized: window.is_maximized(), inner_size_points: Some(egui::vec2( inner_size_points.width, @@ -80,7 +83,8 @@ impl WindowSettings { if let Some(inner_size_points) = self.inner_size_points { viewport_builder = viewport_builder .with_inner_size(inner_size_points) - .with_fullscreen(self.fullscreen); + .with_fullscreen(self.fullscreen) + .with_maximized(self.maximized); } viewport_builder From 0fac8eadfc65882e4bc23964a6096f22d57f6a8d Mon Sep 17 00:00:00 2001 From: Markus Ineichen Date: Mon, 6 Jan 2025 10:03:33 +0100 Subject: [PATCH 006/132] Avoid allocations for loader cache lookup (#5584) [ x ] I have ~~followed~~ _read_ the instructions in the PR template Unfortunately i had several issues: - Some snapshot-tests didn't run successfully on osx. diff shows errors around fonts or missing menu items) - cargo clippy doesn't run successfully (egui_kittest cannot find `wgpu` and `image`) - ./scripts/check.sh had other issues on my system (env: python: No such file or directory), even if python3 can be called via python in my shell Is there a system independent, standard way to run these tools (e.g. via Docker?) I submit the pr anyway, because there changes are very simple and shouldn't cause issues. --- crates/egui/src/load/texture_loader.rs | 8 +++++--- crates/egui_extras/src/loaders/svg_loader.rs | 12 +++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/egui/src/load/texture_loader.rs b/crates/egui/src/load/texture_loader.rs index 6845e86ff..a1170257c 100644 --- a/crates/egui/src/load/texture_loader.rs +++ b/crates/egui/src/load/texture_loader.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + use super::{ BytesLoader, Context, HashMap, ImagePoll, Mutex, SizeHint, SizedTexture, TextureHandle, TextureLoadResult, TextureLoader, TextureOptions, TexturePoll, @@ -5,7 +7,7 @@ use super::{ #[derive(Default)] pub struct DefaultTextureLoader { - cache: Mutex>, + cache: Mutex, TextureOptions), TextureHandle>>, } impl TextureLoader for DefaultTextureLoader { @@ -21,7 +23,7 @@ impl TextureLoader for DefaultTextureLoader { size_hint: SizeHint, ) -> TextureLoadResult { let mut cache = self.cache.lock(); - if let Some(handle) = cache.get(&(uri.into(), texture_options)) { + if let Some(handle) = cache.get(&(Cow::Borrowed(uri), texture_options)) { let texture = SizedTexture::from_handle(handle); Ok(TexturePoll::Ready { texture }) } else { @@ -30,7 +32,7 @@ impl TextureLoader for DefaultTextureLoader { ImagePoll::Ready { image } => { let handle = ctx.load_texture(uri, image, texture_options); let texture = SizedTexture::from_handle(&handle); - cache.insert((uri.into(), texture_options), handle); + cache.insert((Cow::Owned(uri.to_owned()), texture_options), handle); let reduce_texture_memory = ctx.options(|o| o.reduce_texture_memory); if reduce_texture_memory { let loaders = ctx.loaders(); diff --git a/crates/egui_extras/src/loaders/svg_loader.rs b/crates/egui_extras/src/loaders/svg_loader.rs index 67b59540c..2a3f15569 100644 --- a/crates/egui_extras/src/loaders/svg_loader.rs +++ b/crates/egui_extras/src/loaders/svg_loader.rs @@ -1,4 +1,4 @@ -use std::{mem::size_of, path::Path, sync::Arc}; +use std::{borrow::Cow, mem::size_of, path::Path, sync::Arc}; use ahash::HashMap; @@ -12,7 +12,7 @@ type Entry = Result, String>; #[derive(Default)] pub struct SvgLoader { - cache: Mutex>, + cache: Mutex, SizeHint), Entry>>, } impl SvgLoader { @@ -37,23 +37,21 @@ impl ImageLoader for SvgLoader { return Err(LoadError::NotSupported); } - let uri = uri.to_owned(); - let mut cache = self.cache.lock(); // We can't avoid the `uri` clone here without unsafe code. - if let Some(entry) = cache.get(&(uri.clone(), size_hint)).cloned() { + if let Some(entry) = cache.get(&(Cow::Borrowed(uri), size_hint)).cloned() { match entry { Ok(image) => Ok(ImagePoll::Ready { image }), Err(err) => Err(LoadError::Loading(err)), } } else { - match ctx.try_load_bytes(&uri) { + match ctx.try_load_bytes(uri) { Ok(BytesPoll::Ready { bytes, .. }) => { log::trace!("started loading {uri:?}"); let result = crate::image::load_svg_bytes_with_size(&bytes, Some(size_hint)) .map(Arc::new); log::trace!("finished loading {uri:?}"); - cache.insert((uri, size_hint), result.clone()); + cache.insert((Cow::Owned(uri.to_owned()), size_hint), result.clone()); match result { Ok(image) => Ok(ImagePoll::Ready { image }), Err(err) => Err(LoadError::Loading(err)), From 35860418ac3e187da2234c8baf0f326563b675e9 Mon Sep 17 00:00:00 2001 From: Pol Welter Date: Mon, 6 Jan 2025 19:29:53 +0100 Subject: [PATCH 007/132] Use bitfield instead of bools in `Response` and `Sense` (#5556) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes . Factoring the `bool` members of `Response` into a bitfield, the size of `Response` is now 96 bytes (down from 104). I gave `Sense` the same treatment, however this has no effects on `Response` due to padding. I've decided not to pursue `PointerState`, as it is quite large (_many_ members that are sized and aligned to multiples of 8 bytes), so I don't expect any noticeable benefit from making handful of `bool`s slightly leaner. In any case, the changes to `Sense` are already quite a bit more intrusive than those to `Response`. The previous implementation overloaded the names of the attributes `click` and `drag` with similarly named methods that _construct_ `Sense` with the corresponding flag set. Now, that the attributes can no longer be accessed directly, I had to introduce methods with new names (`senses_click()`, `senses_drag()` and `is_focusable()`). I don't think this is the cleanest solution: the old methods are essentially redundant now that the named constants like `Sense::CLICK` exist. I did however not want to needlessly break backwards compatibility. I am happy to revert it (or go further 🙂) if there are concerns. --- Cargo.lock | 1 + Cargo.toml | 1 + README.md | 1 + crates/egui/Cargo.toml | 1 + crates/egui/src/containers/modal.rs | 17 +- crates/egui/src/context.rs | 105 ++++---- crates/egui/src/hit_test.rs | 51 ++-- crates/egui/src/interaction.rs | 6 +- crates/egui/src/response.rs | 229 +++++++++--------- crates/egui/src/sense.rs | 101 +++----- .../text_selection/label_text_selection.rs | 2 +- .../src/text_selection/text_cursor_state.rs | 2 +- crates/egui/src/ui.rs | 4 +- crates/egui/src/widgets/button.rs | 2 +- crates/egui/src/widgets/drag_value.rs | 4 +- crates/egui/src/widgets/label.rs | 2 +- crates/egui/src/widgets/slider.rs | 4 +- crates/egui/src/widgets/text_edit/builder.rs | 12 +- 18 files changed, 266 insertions(+), 279 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 009aeb731..2f3085cb3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1260,6 +1260,7 @@ dependencies = [ "accesskit", "ahash", "backtrace", + "bitflags 2.6.0", "document-features", "emath", "epaint", diff --git a/Cargo.toml b/Cargo.toml index 1dcae33b6..716551151 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,6 +73,7 @@ ahash = { version = "0.8.11", default-features = false, features = [ "std", ] } backtrace = "0.3" +bitflags = "2.6" bytemuck = "1.7.2" criterion = { version = "0.5.1", default-features = false } dify = { version = "0.7", default-features = false } diff --git a/README.md b/README.md index 247ad32d9..b28cc995d 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,7 @@ Light Theme: * [`ab_glyph`](https://crates.io/crates/ab_glyph) * [`ahash`](https://crates.io/crates/ahash) +* [`bitflags`](https://crates.io/crates/bitflags) * [`nohash-hasher`](https://crates.io/crates/nohash-hasher) * [`parking_lot`](https://crates.io/crates/parking_lot) diff --git a/crates/egui/Cargo.toml b/crates/egui/Cargo.toml index a0f11fdfa..d0407b1c3 100644 --- a/crates/egui/Cargo.toml +++ b/crates/egui/Cargo.toml @@ -84,6 +84,7 @@ emath = { workspace = true, default-features = false } epaint = { workspace = true, default-features = false } ahash.workspace = true +bitflags.workspace = true nohash-hasher.workspace = true profiling.workspace = true diff --git a/crates/egui/src/containers/modal.rs b/crates/egui/src/containers/modal.rs index 521b9dedb..8bf596675 100644 --- a/crates/egui/src/containers/modal.rs +++ b/crates/egui/src/containers/modal.rs @@ -86,11 +86,7 @@ impl Modal { response, } = area.show(ctx, |ui| { let bg_rect = ui.ctx().screen_rect(); - let bg_sense = Sense { - click: true, - drag: true, - focusable: false, - }; + let bg_sense = Sense::CLICK | Sense::DRAG; let mut backdrop = ui.new_child(UiBuilder::new().sense(bg_sense).max_rect(bg_rect)); backdrop.set_min_size(bg_rect.size()); ui.painter().rect_filled(bg_rect, 0.0, backdrop_color); @@ -101,14 +97,9 @@ impl Modal { // We need the extra scope with the sense since frame can't have a sense and since we // need to prevent the clicks from passing through to the backdrop. let inner = ui - .scope_builder( - UiBuilder::new().sense(Sense { - click: true, - drag: true, - focusable: false, - }), - |ui| frame.show(ui, content).inner, - ) + .scope_builder(UiBuilder::new().sense(Sense::CLICK | Sense::DRAG), |ui| { + frame.show(ui, content).inner + }) .inner; (inner, backdrop_response) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 3ecf42ca4..b696d338c 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -30,7 +30,7 @@ use crate::{ os::OperatingSystem, output::FullOutput, pass_state::PassState, - resize, scroll_area, + resize, response, scroll_area, util::IdTypeMap, viewport::ViewportClass, Align2, CursorIcon, DeferredViewportUiCallback, FontDefinitions, Grid, Id, ImmediateViewport, @@ -1151,8 +1151,9 @@ impl Context { /// 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.focusable && self.memory(|mem| mem.allows_interaction(w.layer_id)); + let interested_in_focus = w.enabled + && w.sense.is_focusable() + && self.memory(|mem| mem.allows_interaction(w.layer_id)); // Remember this widget self.write(|ctx| { @@ -1173,7 +1174,7 @@ impl Context { self.memory_mut(|mem| mem.surrender_focus(w.id)); } - if w.sense.interactive() || w.sense.focusable { + if w.sense.interactive() || w.sense.is_focusable() { self.check_for_id_clash(w.id, w.rect, "widget"); } @@ -1181,7 +1182,7 @@ impl Context { let res = self.get_response(w); #[cfg(feature = "accesskit")] - if allow_focus && w.sense.focusable { + if allow_focus && w.sense.is_focusable() { // Make sure anything that can receive focus has an AccessKit node. // TODO(mwcampbell): For nodes that are filled from widget info, // some information is written to the node twice. @@ -1213,11 +1214,13 @@ impl Context { #[deprecated = "Use Response.contains_pointer or Context::read_response instead"] pub fn widget_contains_pointer(&self, id: Id) -> bool { self.read_response(id) - .map_or(false, |response| response.contains_pointer) + .map_or(false, |response| response.contains_pointer()) } /// Do all interaction for an existing widget, without (re-)registering it. pub(crate) fn get_response(&self, widget_rect: WidgetRect) -> Response { + use response::Flags; + let WidgetRect { id, layer_id, @@ -1237,61 +1240,72 @@ impl Context { rect, interact_rect, sense, - enabled, - contains_pointer: false, - hovered: false, - highlighted, - clicked: false, - fake_primary_click: false, - long_touched: false, - drag_started: false, - dragged: false, - drag_stopped: false, - is_pointer_button_down_on: false, + flags: Flags::empty(), interact_pointer_pos: None, - changed: false, intrinsic_size: None, }; + res.flags.set(Flags::ENABLED, enabled); + res.flags.set(Flags::HIGHLIGHTED, highlighted); + self.write(|ctx| { let viewport = ctx.viewports.entry(ctx.viewport_id()).or_default(); - res.contains_pointer = viewport.interact_widgets.contains_pointer.contains(&id); + res.flags.set( + Flags::CONTAINS_POINTER, + viewport.interact_widgets.contains_pointer.contains(&id), + ); let input = &viewport.input; let memory = &mut ctx.memory; if enabled - && sense.click + && sense.senses_click() && memory.has_focus(id) && (input.key_pressed(Key::Space) || input.key_pressed(Key::Enter)) { // Space/enter works like a primary click for e.g. selected buttons - res.fake_primary_click = true; + res.flags.set(Flags::FAKE_PRIMARY_CLICKED, true); } #[cfg(feature = "accesskit")] if enabled - && sense.click + && sense.senses_click() && input.has_accesskit_action_request(id, accesskit::Action::Click) { - res.fake_primary_click = true; + res.flags.set(Flags::FAKE_PRIMARY_CLICKED, true); } - if enabled && sense.click && Some(id) == viewport.interact_widgets.long_touched { - res.long_touched = true; + if enabled && sense.senses_click() && Some(id) == viewport.interact_widgets.long_touched + { + res.flags.set(Flags::LONG_TOUCHED, true); } let interaction = memory.interaction(); - res.is_pointer_button_down_on = interaction.potential_click_id == Some(id) - || interaction.potential_drag_id == Some(id); + res.flags.set( + Flags::IS_POINTER_BUTTON_DOWN_ON, + interaction.potential_click_id == Some(id) + || interaction.potential_drag_id == Some(id), + ); - if res.enabled { - res.hovered = viewport.interact_widgets.hovered.contains(&id); - res.dragged = Some(id) == viewport.interact_widgets.dragged; - res.drag_started = Some(id) == viewport.interact_widgets.drag_started; - res.drag_stopped = Some(id) == viewport.interact_widgets.drag_stopped; + if res.enabled() { + res.flags.set( + Flags::HOVERED, + viewport.interact_widgets.hovered.contains(&id), + ); + res.flags.set( + Flags::DRAGGED, + Some(id) == viewport.interact_widgets.dragged, + ); + res.flags.set( + Flags::DRAG_STARTED, + Some(id) == viewport.interact_widgets.drag_started, + ); + res.flags.set( + Flags::DRAG_STOPPED, + Some(id) == viewport.interact_widgets.drag_stopped, + ); } let clicked = Some(id) == viewport.interact_widgets.clicked; @@ -1304,20 +1318,22 @@ impl Context { any_press = true; } PointerEvent::Released { click, .. } => { - if enabled && sense.click && clicked && click.is_some() { - res.clicked = true; + if enabled && sense.senses_click() && clicked && click.is_some() { + res.flags.set(Flags::CLICKED, true); } - res.is_pointer_button_down_on = false; - res.dragged = false; + res.flags.set(Flags::IS_POINTER_BUTTON_DOWN_ON, false); + res.flags.set(Flags::DRAGGED, false); } } } // is_pointer_button_down_on is false when released, but we want interact_pointer_pos // to still work. - let is_interacted_with = - res.is_pointer_button_down_on || res.long_touched || clicked || res.drag_stopped; + let is_interacted_with = res.is_pointer_button_down_on() + || res.long_touched() + || clicked + || res.drag_stopped(); if is_interacted_with { res.interact_pointer_pos = input.pointer.interact_pos(); if let (Some(to_global), Some(pos)) = ( @@ -1330,10 +1346,10 @@ impl Context { if input.pointer.any_down() && !is_interacted_with { // We don't hover widgets while interacting with *other* widgets: - res.hovered = false; + res.flags.set(Flags::HOVERED, false); } - let pointer_pressed_elsewhere = any_press && !res.hovered; + let pointer_pressed_elsewhere = any_press && !res.hovered(); if pointer_pressed_elsewhere && memory.has_focus(id) { memory.surrender_focus(id); } @@ -2152,11 +2168,12 @@ impl Context { let painter = Painter::new(self.clone(), *layer_id, Rect::EVERYTHING); for rect in rects { if rect.sense.interactive() { - let (color, text) = if rect.sense.click && rect.sense.drag { + let (color, text) = if rect.sense.senses_click() && rect.sense.senses_drag() + { (Color32::from_rgb(0x88, 0, 0x88), "click+drag") - } else if rect.sense.click { + } else if rect.sense.senses_click() { (Color32::from_rgb(0x88, 0, 0), "click") - } else if rect.sense.drag { + } else if rect.sense.senses_drag() { (Color32::from_rgb(0, 0, 0x88), "drag") } else { // unreachable since we only show interactive @@ -3131,7 +3148,7 @@ impl Context { // TODO(emilk): `Sense::hover_highlight()` let response = ui.add(Label::new(RichText::new(text).monospace()).sense(Sense::click())); - if response.hovered && is_visible { + if response.hovered() && is_visible { ui.ctx() .debug_painter() .debug_rect(area.rect(), Color32::RED, ""); diff --git a/crates/egui/src/hit_test.rs b/crates/egui/src/hit_test.rs index 8741f5f8b..81c8abbb8 100644 --- a/crates/egui/src/hit_test.rs +++ b/crates/egui/src/hit_test.rs @@ -2,7 +2,7 @@ use ahash::HashMap; use emath::TSTransform; -use crate::{ahash, emath, LayerId, Pos2, Rect, WidgetRect, WidgetRects}; +use crate::{ahash, emath, LayerId, Pos2, Rect, Sense, WidgetRect, WidgetRects}; /// Result of a hit-test against [`WidgetRects`]. /// @@ -128,8 +128,8 @@ pub fn hit_test( // the `enabled` flag everywhere: for w in &mut close { if !w.enabled { - w.sense.click = false; - w.sense.drag = false; + w.sense -= Sense::CLICK; + w.sense -= Sense::DRAG; } } @@ -158,11 +158,11 @@ pub fn hit_test( restore_widget_rect(wr); } if let Some(wr) = &mut hits.drag { - debug_assert!(wr.sense.drag); + debug_assert!(wr.sense.senses_drag()); restore_widget_rect(wr); } if let Some(wr) = &mut hits.click { - debug_assert!(wr.sense.click); + debug_assert!(wr.sense.senses_click()); restore_widget_rect(wr); } } @@ -179,8 +179,16 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits { #![allow(clippy::collapsible_else_if)] // First find the best direct hits: - let hit_click = find_closest_within(close.iter().copied().filter(|w| w.sense.click), pos, 0.0); - let hit_drag = find_closest_within(close.iter().copied().filter(|w| w.sense.drag), pos, 0.0); + let hit_click = find_closest_within( + close.iter().copied().filter(|w| w.sense.senses_click()), + pos, + 0.0, + ); + let hit_drag = find_closest_within( + close.iter().copied().filter(|w| w.sense.senses_drag()), + pos, + 0.0, + ); match (hit_click, hit_drag) { (None, None) => { @@ -190,14 +198,14 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits { close .iter() .copied() - .filter(|w| w.sense.click || w.sense.drag), + .filter(|w| w.sense.senses_click() || w.sense.senses_drag()), pos, ); if let Some(closest) = closest { WidgetHits { - click: closest.sense.click.then_some(closest), - drag: closest.sense.drag.then_some(closest), + click: closest.sense.senses_click().then_some(closest), + drag: closest.sense.senses_drag().then_some(closest), ..Default::default() } } else { @@ -218,9 +226,12 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits { // or a moveable window. // It could also be something small, like a slider, or panel resize handle. - let closest_click = find_closest(close.iter().copied().filter(|w| w.sense.click), pos); + let closest_click = find_closest( + close.iter().copied().filter(|w| w.sense.senses_click()), + pos, + ); if let Some(closest_click) = closest_click { - if closest_click.sense.drag { + if closest_click.sense.senses_drag() { // We have something close that sense both clicks and drag. // Should we use it over the direct drag-hit? if hit_drag @@ -244,7 +255,7 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits { } } } else { - // These is a close pure-click widget. + // This is a close pure-click widget. // However, we should be careful to only return two different widgets // when it is absolutely not going to confuse the user. if hit_drag @@ -277,7 +288,7 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits { close .iter() .copied() - .filter(|w| w.sense.drag && w.id != hit_drag.id), + .filter(|w| w.sense.senses_drag() && w.id != hit_drag.id), pos, ); @@ -331,7 +342,7 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits { let click_is_on_top_of_drag = drag_idx < click_idx; if click_is_on_top_of_drag { - if hit_click.sense.drag { + if hit_click.sense.senses_drag() { // The top thing senses both clicks and drags. WidgetHits { click: Some(hit_click), @@ -349,7 +360,7 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits { } } } else { - if hit_drag.sense.click { + if hit_drag.sense.senses_click() { // The top thing senses both clicks and drags. WidgetHits { click: Some(hit_drag), @@ -393,7 +404,7 @@ fn find_closest_within( if dist_sq == closest_dist_sq { // It's a tie! Pick the thin candidate over the thick one. // This makes it easier to hit a thin resize-handle, for instance: - if should_prioritizie_hits_on_back(closest.interact_rect, widget.interact_rect) { + if should_prioritize_hits_on_back(closest.interact_rect, widget.interact_rect) { continue; } } @@ -409,12 +420,12 @@ fn find_closest_within( closest } -/// Should we prioritizie hits on `back` over those on `front`? +/// Should we prioritize hits on `back` over those on `front`? /// /// `back` should be behind the `front` widget. /// /// Returns true if `back` is a small hit-target and `front` is not. -fn should_prioritizie_hits_on_back(back: Rect, front: Rect) -> bool { +fn should_prioritize_hits_on_back(back: Rect, front: Rect) -> bool { if front.contains_rect(back) { return false; // back widget is fully occluded; no way to hit it } @@ -484,7 +495,7 @@ mod tests { assert_eq!(hits.click.unwrap().id, Id::new("click-and-drag")); assert_eq!(hits.drag.unwrap().id, Id::new("click-and-drag")); - // Close hit - should still ignore the drag-background so as not to confuse the userr: + // Close hit - should still ignore the drag-background so as not to confuse the user: let hits = hit_test_on_close(&widgets, pos2(105.0, 5.0)); assert_eq!(hits.click.unwrap().id, Id::new("click-and-drag")); assert_eq!(hits.drag.unwrap().id, Id::new("click-and-drag")); diff --git a/crates/egui/src/interaction.rs b/crates/egui/src/interaction.rs index 22ec8ebc5..b6b63e19d 100644 --- a/crates/egui/src/interaction.rs +++ b/crates/egui/src/interaction.rs @@ -192,14 +192,14 @@ pub(crate) fn interact( // Check if we started dragging something new: if let Some(widget) = interaction.potential_drag_id.and_then(|id| widgets.get(id)) { if widget.enabled { - let is_dragged = if widget.sense.click && widget.sense.drag { + let is_dragged = if widget.sense.senses_click() && widget.sense.senses_drag() { // This widget is sensitive to both clicks and drags. // When the mouse first is pressed, it could be either, // so we postpone the decision until we know. input.pointer.is_decidedly_dragging() } else { // This widget is just sensitive to drags, so we can mark it as dragged right away: - widget.sense.drag + widget.sense.senses_drag() }; if is_dragged { @@ -271,7 +271,7 @@ pub(crate) fn interact( let mut hovered: IdSet = hits.click.iter().chain(&hits.drag).map(|w| w.id).collect(); for w in &hits.contains_pointer { - let is_interactive = w.sense.click || w.sense.drag; + let is_interactive = w.sense.senses_click() || w.sense.senses_drag(); if is_interactive { // The only interactive widgets we mark as hovered are the ones // in `hits.click` and `hits.drag`! diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index c65d9ca8c..73e775168 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -9,14 +9,14 @@ use crate::{ /// The result of adding a widget to a [`Ui`]. /// -/// A [`Response`] lets you know whether or not a widget is being hovered, clicked or dragged. +/// A [`Response`] lets you know whether a widget is being hovered, clicked or dragged. /// It also lets you easily show a tooltip on hover. /// /// Whenever something gets added to a [`Ui`], a [`Response`] object is returned. /// [`ui.add`] returns a [`Response`], as does [`ui.button`], and all similar shortcuts. /// /// ⚠️ The `Response` contains a clone of [`Context`], and many methods lock the `Context`. -/// It can therefor be a deadlock to use `Context` from within a context-locking closures, +/// It can therefore be a deadlock to use `Context` from within a context-locking closures, /// such as [`Context::input`]. #[derive(Clone, Debug)] pub struct Response { @@ -50,78 +50,12 @@ pub struct Response { /// (that is handled by the `Painter` directly). pub sense: Sense, - /// Was the widget enabled? - /// If `false`, there was no interaction attempted (not even hover). - #[doc(hidden)] - pub enabled: bool, - // OUT: - /// The pointer is above this widget with no other blocking it. - #[doc(hidden)] - pub contains_pointer: bool, - - /// The pointer is hovering above this widget or the widget was clicked/tapped this frame. - #[doc(hidden)] - pub hovered: bool, - - /// The widget is highlighted via a call to [`Self::highlight`] or [`Context::highlight_widget`]. - #[doc(hidden)] - pub highlighted: bool, - - /// This widget was clicked this frame. - /// - /// Which pointer and how many times we don't know, - /// and ask [`crate::InputState`] about at runtime. - /// - /// This is only set to true if the widget was clicked - /// by an actual mouse. - #[doc(hidden)] - pub clicked: bool, - - /// This widget should act as if clicked due - /// to something else than a click. - /// - /// This is set to true if the widget has keyboard focus and - /// the user hit the Space or Enter key. - #[doc(hidden)] - pub fake_primary_click: bool, - - /// This widget was long-pressed on a touch screen to simulate a secondary click. - #[doc(hidden)] - pub long_touched: bool, - - /// The widget started being dragged this frame. - #[doc(hidden)] - pub drag_started: bool, - - /// The widget is being dragged. - #[doc(hidden)] - pub dragged: bool, - - /// The widget was being dragged, but now it has been released. - #[doc(hidden)] - pub drag_stopped: bool, - - /// Is the pointer button currently down on this widget? - /// This is true if the pointer is pressing down or dragging a widget - #[doc(hidden)] - pub is_pointer_button_down_on: bool, - - /// Where the pointer (mouse/touch) were when when this widget was clicked or dragged. + /// Where the pointer (mouse/touch) were when this widget was clicked or dragged. /// `None` if the widget is not being interacted with. #[doc(hidden)] pub interact_pointer_pos: Option, - /// Was the underlying data changed? - /// - /// e.g. the slider was dragged, text was entered in a [`TextEdit`](crate::TextEdit) etc. - /// Always `false` for something like a [`Button`](crate::Button). - /// - /// Note that this can be `true` even if the user did not interact with the widget, - /// for instance if an existing slider value was clamped to the given range. - #[doc(hidden)] - pub changed: bool, - /// The intrinsic / desired size of the widget. /// /// For a button, this will be the size of the label + the frames padding, @@ -133,6 +67,73 @@ pub struct Response { /// for improved layouting. /// See for instance [`egui_flex`](https://github.com/lucasmerlin/hello_egui/tree/main/crates/egui_flex). pub intrinsic_size: Option, + + #[doc(hidden)] + pub flags: Flags, +} + +/// A bit set for various boolean properties of `Response`. +#[doc(hidden)] +#[derive(Copy, Clone, Debug)] +pub struct Flags(u16); + +bitflags::bitflags! { + impl Flags: u16 { + /// Was the widget enabled? + /// If `false`, there was no interaction attempted (not even hover). + const ENABLED = 1<<0; + + /// The pointer is above this widget with no other blocking it. + const CONTAINS_POINTER = 1<<1; + + /// The pointer is hovering above this widget or the widget was clicked/tapped this frame. + const HOVERED = 1<<2; + + /// The widget is highlighted via a call to [`Response::highlight`] or + /// [`Context::highlight_widget`]. + const HIGHLIGHTED = 1<<3; + + /// This widget was clicked this frame. + /// + /// Which pointer and how many times we don't know, + /// and ask [`crate::InputState`] about at runtime. + /// + /// This is only set to true if the widget was clicked + /// by an actual mouse. + const CLICKED = 1<<4; + + /// This widget should act as if clicked due + /// to something else than a click. + /// + /// This is set to true if the widget has keyboard focus and + /// the user hit the Space or Enter key. + const FAKE_PRIMARY_CLICKED = 1<<5; + + /// This widget was long-pressed on a touch screen to simulate a secondary click. + const LONG_TOUCHED = 1<<6; + + /// The widget started being dragged this frame. + const DRAG_STARTED = 1<<7; + + /// The widget is being dragged. + const DRAGGED = 1<<8; + + /// The widget was being dragged, but now it has been released. + const DRAG_STOPPED = 1<<9; + + /// Is the pointer button currently down on this widget? + /// This is true if the pointer is pressing down or dragging a widget + const IS_POINTER_BUTTON_DOWN_ON = 1<<10; + + /// Was the underlying data changed? + /// + /// e.g. the slider was dragged, text was entered in a [`TextEdit`](crate::TextEdit) etc. + /// Always `false` for something like a [`Button`](crate::Button). + /// + /// Note that this can be `true` even if the user did not interact with the widget, + /// for instance if an existing slider value was clamped to the given range. + const CHANGED = 1<<11; + } } impl Response { @@ -150,7 +151,7 @@ impl Response { /// You can use [`Self::interact`] to sense more things *after* adding a widget. #[inline(always)] pub fn clicked(&self) -> bool { - self.fake_primary_click || self.clicked_by(PointerButton::Primary) + self.flags.contains(Flags::FAKE_PRIMARY_CLICKED) || self.clicked_by(PointerButton::Primary) } /// Returns true if this widget was clicked this frame by the given mouse button. @@ -163,7 +164,7 @@ impl Response { /// Use [`Self::secondary_clicked`] instead to also detect that. #[inline] pub fn clicked_by(&self, button: PointerButton) -> bool { - self.clicked && self.ctx.input(|i| i.pointer.button_clicked(button)) + self.flags.contains(Flags::CLICKED) && self.ctx.input(|i| i.pointer.button_clicked(button)) } /// Returns true if this widget was clicked this frame by the secondary mouse button (e.g. the right mouse button). @@ -171,7 +172,7 @@ impl Response { /// This also returns true if the widget was pressed-and-held on a touch screen. #[inline] pub fn secondary_clicked(&self) -> bool { - self.long_touched || self.clicked_by(PointerButton::Secondary) + self.flags.contains(Flags::LONG_TOUCHED) || self.clicked_by(PointerButton::Secondary) } /// Was this long-pressed on a touch screen? @@ -179,7 +180,7 @@ impl Response { /// Usually you want to check [`Self::secondary_clicked`] instead. #[inline] pub fn long_touched(&self) -> bool { - self.long_touched + self.flags.contains(Flags::LONG_TOUCHED) } /// Returns true if this widget was clicked this frame by the middle mouse button. @@ -203,13 +204,15 @@ impl Response { /// Returns true if this widget was double-clicked this frame by the given button. #[inline] pub fn double_clicked_by(&self, button: PointerButton) -> bool { - self.clicked && self.ctx.input(|i| i.pointer.button_double_clicked(button)) + self.flags.contains(Flags::CLICKED) + && self.ctx.input(|i| i.pointer.button_double_clicked(button)) } /// Returns true if this widget was triple-clicked this frame by the given button. #[inline] pub fn triple_clicked_by(&self, button: PointerButton) -> bool { - self.clicked && self.ctx.input(|i| i.pointer.button_triple_clicked(button)) + self.flags.contains(Flags::CLICKED) + && self.ctx.input(|i| i.pointer.button_triple_clicked(button)) } /// `true` if there was a click *outside* the rect of this widget. @@ -224,7 +227,7 @@ impl Response { let pointer = &i.pointer; if pointer.any_click() { - if self.contains_pointer || self.hovered { + if self.contains_pointer() || self.hovered() { false } else if let Some(pos) = pointer.interact_pos() { !self.interact_rect.contains(pos) @@ -242,7 +245,7 @@ impl Response { /// and the widget should be drawn in a gray disabled look. #[inline(always)] pub fn enabled(&self) -> bool { - self.enabled + self.flags.contains(Flags::ENABLED) } /// The pointer is hovering above this widget or the widget was clicked/tapped this frame. @@ -251,7 +254,7 @@ impl Response { /// `hovered` is always `false` for disabled widgets. #[inline(always)] pub fn hovered(&self) -> bool { - self.hovered + self.flags.contains(Flags::HOVERED) } /// Returns true if the pointer is contained by the response rect, and no other widget is covering it. @@ -264,14 +267,14 @@ impl Response { /// [`Self::contains_pointer`] also checks that no other widget is covering this response rectangle. #[inline(always)] pub fn contains_pointer(&self) -> bool { - self.contains_pointer + self.flags.contains(Flags::CONTAINS_POINTER) } /// The widget is highlighted via a call to [`Self::highlight`] or [`Context::highlight_widget`]. #[doc(hidden)] #[inline(always)] pub fn highlighted(&self) -> bool { - self.highlighted + self.flags.contains(Flags::HIGHLIGHTED) } /// This widget has the keyboard focus (i.e. is receiving key presses). @@ -316,7 +319,7 @@ impl Response { self.ctx.memory_mut(|mem| mem.surrender_focus(self.id)); } - /// Did a drag on this widgets begin this frame? + /// Did a drag on this widget begin this frame? /// /// This is only true if the widget sense drags. /// If the widget also senses clicks, this will only become true if the pointer has moved a bit. @@ -324,10 +327,10 @@ impl Response { /// This will only be true for a single frame. #[inline] pub fn drag_started(&self) -> bool { - self.drag_started + self.flags.contains(Flags::DRAG_STARTED) } - /// Did a drag on this widgets by the button begin this frame? + /// Did a drag on this widget by the button begin this frame? /// /// This is only true if the widget sense drags. /// If the widget also senses clicks, this will only become true if the pointer has moved a bit. @@ -354,7 +357,7 @@ impl Response { /// You can use [`Self::interact`] to sense more things *after* adding a widget. #[inline(always)] pub fn dragged(&self) -> bool { - self.dragged + self.flags.contains(Flags::DRAGGED) } /// See [`Self::dragged`]. @@ -366,7 +369,7 @@ impl Response { /// The widget was being dragged, but now it has been released. #[inline] pub fn drag_stopped(&self) -> bool { - self.drag_stopped + self.flags.contains(Flags::DRAG_STOPPED) } /// The widget was being dragged by the button, but now it has been released. @@ -378,7 +381,7 @@ impl Response { #[inline] #[deprecated = "Renamed 'drag_stopped'"] pub fn drag_released(&self) -> bool { - self.drag_stopped + self.drag_stopped() } /// The widget was being dragged by the button, but now it has been released. @@ -422,7 +425,7 @@ impl Response { crate::DragAndDrop::set_payload(&self.ctx, payload); } - if self.hovered() && !self.sense.click { + if self.hovered() && !self.sense.senses_click() { // Things that can be drag-dropped should use the Grab cursor icon, // but if the thing is _also_ clickable, that can be annoying. self.ctx.set_cursor_icon(CursorIcon::Grab); @@ -460,7 +463,7 @@ impl Response { } } - /// Where the pointer (mouse/touch) were when when this widget was clicked or dragged. + /// Where the pointer (mouse/touch) were when this widget was clicked or dragged. /// /// `None` if the widget is not being interacted with. #[inline] @@ -492,7 +495,7 @@ impl Response { /// This could also be thought of as "is this widget being interacted with?". #[inline(always)] pub fn is_pointer_button_down_on(&self) -> bool { - self.is_pointer_button_down_on + self.flags.contains(Flags::IS_POINTER_BUTTON_DOWN_ON) } /// Was the underlying data changed? @@ -510,7 +513,7 @@ impl Response { /// for instance if an existing slider value was clamped to the given range. #[inline(always)] pub fn changed(&self) -> bool { - self.changed + self.flags.contains(Flags::CHANGED) } /// Report the data shown by this widget changed. @@ -519,10 +522,10 @@ impl Response { /// e.g. checkboxes, sliders etc. /// /// This should be called when the *content* changes, but not when the view does. - /// So we call this when the text of a [`crate::TextEdit`], but not when the cursors changes. + /// So we call this when the text of a [`crate::TextEdit`], but not when the cursor changes. #[inline(always)] pub fn mark_changed(&mut self) { - self.changed = true; + self.flags.set(Flags::CHANGED, true); } /// Show this UI if the widget was hovered (i.e. a tooltip). @@ -547,7 +550,7 @@ impl Response { /// ``` #[doc(alias = "tooltip")] pub fn on_hover_ui(self, add_contents: impl FnOnce(&mut Ui)) -> Self { - if self.enabled && self.should_show_hover_ui() { + if self.flags.contains(Flags::ENABLED) && self.should_show_hover_ui() { self.show_tooltip_ui(add_contents); } self @@ -555,7 +558,7 @@ impl Response { /// Show this UI when hovering if the widget is disabled. pub fn on_disabled_hover_ui(self, add_contents: impl FnOnce(&mut Ui)) -> Self { - if !self.enabled && self.should_show_hover_ui() { + if !self.enabled() && self.should_show_hover_ui() { crate::containers::show_tooltip_for( &self.ctx, self.layer_id, @@ -569,7 +572,7 @@ impl Response { /// Like `on_hover_ui`, but show the ui next to cursor. pub fn on_hover_ui_at_pointer(self, add_contents: impl FnOnce(&mut Ui)) -> Self { - if self.enabled && self.should_show_hover_ui() { + if self.enabled() && self.should_show_hover_ui() { crate::containers::show_tooltip_at_pointer( &self.ctx, self.layer_id, @@ -725,8 +728,8 @@ impl Response { } // Fast early-outs: - if self.enabled { - if !self.hovered || !self.ctx.input(|i| i.pointer.has_pointer()) { + if self.enabled() { + if !self.hovered() || !self.ctx.input(|i| i.pointer.has_pointer()) { return false; } } else if !self.ctx.rect_contains_pointer(self.layer_id, self.rect) { @@ -734,7 +737,7 @@ impl Response { } // There is a tooltip_delay before showing the first tooltip, - // but once one tooltips is show, moving the mouse cursor to + // but once one tooltip is show, moving the mouse cursor to // another widget should show the tooltip for that widget right away. // Let the user quickly move over some dead space to hover the next thing @@ -817,7 +820,7 @@ impl Response { #[inline] pub fn highlight(mut self) -> Self { self.ctx.highlight_widget(self.id); - self.highlighted = true; + self.flags.set(Flags::HIGHLIGHTED, true); self } @@ -888,7 +891,7 @@ impl Response { rect: self.rect, interact_rect: self.interact_rect, sense: self.sense | sense, - enabled: self.enabled, + enabled: self.enabled(), }, true, ) @@ -951,7 +954,7 @@ impl Response { Some(OutputEvent::TripleClicked(make_info())) } else if self.gained_focus() { Some(OutputEvent::FocusGained(make_info())) - } else if self.changed { + } else if self.changed() { Some(OutputEvent::ValueChanged(make_info())) } else { None @@ -983,7 +986,7 @@ impl Response { #[cfg(feature = "accesskit")] pub(crate) fn fill_accesskit_node_common(&self, builder: &mut accesskit::Node) { - if !self.enabled { + if !self.enabled() { builder.set_disabled(); } builder.set_bounds(accesskit::Rect { @@ -992,10 +995,10 @@ impl Response { x1: self.rect.max.x.into(), y1: self.rect.max.y.into(), }); - if self.sense.focusable { + if self.sense.is_focusable() { builder.add_action(accesskit::Action::Focus); } - if self.sense.click { + if self.sense.senses_click() { builder.add_action(accesskit::Action::Click); } } @@ -1125,9 +1128,9 @@ impl Response { pub fn paint_debug_info(&self) { self.ctx.debug_painter().debug_rect( self.rect, - if self.hovered { + if self.hovered() { crate::Color32::DARK_GREEN - } else if self.enabled { + } else if self.enabled() { crate::Color32::BLUE } else { crate::Color32::RED @@ -1157,20 +1160,8 @@ impl Response { rect: self.rect.union(other.rect), interact_rect: self.interact_rect.union(other.interact_rect), sense: self.sense.union(other.sense), - enabled: self.enabled || other.enabled, - contains_pointer: self.contains_pointer || other.contains_pointer, - hovered: self.hovered || other.hovered, - highlighted: self.highlighted || other.highlighted, - clicked: self.clicked || other.clicked, - fake_primary_click: self.fake_primary_click || other.fake_primary_click, - long_touched: self.long_touched || other.long_touched, - drag_started: self.drag_started || other.drag_started, - dragged: self.dragged || other.dragged, - drag_stopped: self.drag_stopped || other.drag_stopped, - is_pointer_button_down_on: self.is_pointer_button_down_on - || other.is_pointer_button_down_on, + flags: self.flags | other.flags, interact_pointer_pos: self.interact_pointer_pos.or(other.interact_pointer_pos), - changed: self.changed || other.changed, intrinsic_size: None, } } diff --git a/crates/egui/src/sense.rs b/crates/egui/src/sense.rs index 1b6394b2c..464db311f 100644 --- a/crates/egui/src/sense.rs +++ b/crates/egui/src/sense.rs @@ -1,36 +1,37 @@ /// What sort of interaction is a widget sensitive to? #[derive(Clone, Copy, Eq, PartialEq)] // #[cfg_attr(feature = "serde", derive(serde::Serialize))] -pub struct Sense { - /// Buttons, sliders, windows, … - pub click: bool, +pub struct Sense(u8); - /// Sliders, windows, scroll bars, scroll areas, … - pub drag: bool, +bitflags::bitflags! { + impl Sense: u8 { - /// This widget wants focus. - /// - /// Anything interactive + labels that can be focused - /// for the benefit of screen readers. - pub focusable: bool, + const HOVER = 0; + + /// Buttons, sliders, windows, … + const CLICK = 1<<0; + + /// Sliders, windows, scroll bars, scroll areas, … + const DRAG = 1<<1; + + /// This widget wants focus. + /// + /// Anything interactive + labels that can be focused + /// for the benefit of screen readers. + const FOCUSABLE = 1<<2; + } } impl std::fmt::Debug for Sense { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let Self { - click, - drag, - focusable, - } = self; - write!(f, "Sense {{")?; - if *click { + if self.senses_click() { write!(f, " click")?; } - if *drag { + if self.senses_drag() { write!(f, " drag")?; } - if *focusable { + if self.is_focusable() { write!(f, " focusable")?; } write!(f, " }}") @@ -42,42 +43,26 @@ impl Sense { #[doc(alias = "none")] #[inline] pub fn hover() -> Self { - Self { - click: false, - drag: false, - focusable: false, - } + Self::empty() } /// Senses no clicks or drags, but can be focused with the keyboard. /// Used for labels that can be focused for the benefit of screen readers. #[inline] pub fn focusable_noninteractive() -> Self { - Self { - click: false, - drag: false, - focusable: true, - } + Self::FOCUSABLE } /// Sense clicks and hover, but not drags. #[inline] pub fn click() -> Self { - Self { - click: true, - drag: false, - focusable: true, - } + Self::CLICK | Self::FOCUSABLE } /// Sense drags and hover, but not clicks. #[inline] pub fn drag() -> Self { - Self { - click: false, - drag: true, - focusable: true, - } + Self::DRAG | Self::FOCUSABLE } /// Sense both clicks, drags and hover (e.g. a slider or window). @@ -90,43 +75,27 @@ impl Sense { /// See [`crate::PointerState::is_decidedly_dragging`] for details. #[inline] pub fn click_and_drag() -> Self { - Self { - click: true, - drag: true, - focusable: true, - } - } - - /// The logical "or" of two [`Sense`]s. - #[must_use] - #[inline] - pub fn union(self, other: Self) -> Self { - Self { - click: self.click | other.click, - drag: self.drag | other.drag, - focusable: self.focusable | other.focusable, - } + Self::CLICK | Self::FOCUSABLE | Self::DRAG } /// Returns true if we sense either clicks or drags. #[inline] pub fn interactive(&self) -> bool { - self.click || self.drag + self.intersects(Self::CLICK | Self::DRAG) } -} - -impl std::ops::BitOr for Sense { - type Output = Self; #[inline] - fn bitor(self, rhs: Self) -> Self { - self.union(rhs) + pub fn senses_click(&self) -> bool { + self.contains(Self::CLICK) } -} -impl std::ops::BitOrAssign for Sense { #[inline] - fn bitor_assign(&mut self, rhs: Self) { - *self = self.union(rhs); + pub fn senses_drag(&self) -> bool { + self.contains(Self::DRAG) + } + + #[inline] + pub fn is_focusable(&self) -> bool { + self.contains(Self::FOCUSABLE) } } diff --git a/crates/egui/src/text_selection/label_text_selection.rs b/crates/egui/src/text_selection/label_text_selection.rs index fe5eac00e..e19a40e49 100644 --- a/crates/egui/src/text_selection/label_text_selection.rs +++ b/crates/egui/src/text_selection/label_text_selection.rs @@ -484,7 +484,7 @@ impl LabelSelectionState { ) -> Vec { let widget_id = response.id; - if response.hovered { + if response.hovered() { ui.ctx().set_cursor_icon(CursorIcon::Text); } diff --git a/crates/egui/src/text_selection/text_cursor_state.rs b/crates/egui/src/text_selection/text_cursor_state.rs index dc75c7dd8..61407353a 100644 --- a/crates/egui/src/text_selection/text_cursor_state.rs +++ b/crates/egui/src/text_selection/text_cursor_state.rs @@ -122,7 +122,7 @@ impl TextCursorState { secondary: galley.from_ccursor(ccursor_range.secondary), })); true - } else if response.sense.drag { + } else if response.sense.senses_drag() { if response.hovered() && ui.input(|i| i.pointer.any_pressed()) { // The start of a drag (or a click). if ui.input(|i| i.modifiers.shift) { diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index acc08ca37..879177139 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -2079,7 +2079,7 @@ impl Ui { // only touch `*radians` if we actually changed the degree value if degrees != radians.to_degrees() { *radians = degrees.to_radians(); - response.changed = true; + response.mark_changed(); } response @@ -2102,7 +2102,7 @@ impl Ui { // only touch `*radians` if we actually changed the value if taus != *radians / TAU { *radians = taus * TAU; - response.changed = true; + response.mark_changed(); } response diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 088800e45..701e89b1c 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -387,7 +387,7 @@ impl Widget for Button<'_> { } if let Some(cursor) = ui.visuals().interact_cursor { - if response.hovered { + if response.hovered() { ui.ctx().set_cursor_icon(cursor); } } diff --git a/crates/egui/src/widgets/drag_value.rs b/crates/egui/src/widgets/drag_value.rs index a5b8c25b2..175fdcc5a 100644 --- a/crates/egui/src/widgets/drag_value.rs +++ b/crates/egui/src/widgets/drag_value.rs @@ -660,7 +660,9 @@ impl<'a> Widget for DragValue<'a> { response }; - response.changed = get(&mut get_set_value) != old_value; + if get(&mut get_set_value) != old_value { + response.mark_changed(); + } response.widget_info(|| WidgetInfo::drag_value(ui.is_enabled(), value)); diff --git a/crates/egui/src/widgets/label.rs b/crates/egui/src/widgets/label.rs index b6ade45ae..eb3e3840e 100644 --- a/crates/egui/src/widgets/label.rs +++ b/crates/egui/src/widgets/label.rs @@ -146,7 +146,7 @@ impl Label { } else { Sense::click() }; - select_sense.focusable = false; // Don't move focus to labels with TAB key. + select_sense -= Sense::FOCUSABLE; // Don't move focus to labels with TAB key. sense = sense.union(select_sense); } diff --git a/crates/egui/src/widgets/slider.rs b/crates/egui/src/widgets/slider.rs index 71f4c8499..acf0359a8 100644 --- a/crates/egui/src/widgets/slider.rs +++ b/crates/egui/src/widgets/slider.rs @@ -946,7 +946,9 @@ impl<'a> Slider<'a> { self.slider_ui(ui, &response); let value = self.get_value(); - response.changed = value != old_value; + if value != old_value { + response.mark_changed(); + } response.widget_info(|| WidgetInfo::slider(ui.is_enabled(), value, self.text.text())); #[cfg(feature = "accesskit")] diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index be96c00fa..5c3b080f1 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -7,7 +7,7 @@ use crate::{ epaint, os::OperatingSystem, output::OutputEvent, - text_selection, + response, text_selection, text_selection::{ text_cursor_state::cursor_rect, visuals::paint_text_selection, CCursorRange, CursorRange, }, @@ -565,8 +565,8 @@ impl<'t> TextEdit<'t> { let mut response = ui.interact(outer_rect, id, sense); response.intrinsic_size = Some(Vec2::new(desired_width, desired_outer_size.y)); - response.fake_primary_click = false; // Don't sent `OutputEvent::Clicked` when a user presses the space bar - + // Don't sent `OutputEvent::Clicked` when a user presses the space bar + response.flags -= response::Flags::FAKE_PRIMARY_CLICKED; let text_clip_rect = rect; let painter = ui.painter_at(text_clip_rect.expand(1.0)); // expand to avoid clipping cursor @@ -740,14 +740,14 @@ impl<'t> TextEdit<'t> { let primary_cursor_rect = cursor_rect(galley_pos, &galley, &cursor_range.primary, row_height); - if response.changed || selection_changed { + if response.changed() || selection_changed { // Scroll to keep primary cursor in view: ui.scroll_to_rect(primary_cursor_rect + margin, None); } if text.is_mutable() && interactive { let now = ui.ctx().input(|i| i.time); - if response.changed || selection_changed { + if response.changed() || selection_changed { state.last_interaction_time = now; } @@ -794,7 +794,7 @@ impl<'t> TextEdit<'t> { state.clone().store(ui.ctx(), id); - if response.changed { + if response.changed() { response.widget_info(|| { WidgetInfo::text_edit( ui.is_enabled(), From 52060c0c41ee6b0a4677924b40aa951732c27a6c Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Tue, 7 Jan 2025 08:33:44 +0100 Subject: [PATCH 008/132] Change `Harness::run` to run until no more repaints are requested (#5580) Previously, `Harness::run` just called `Harness::step` 3 times. If that wasn't enough, tests would often call run multiple times so all animations would finish properly. Also, I introduced `HarnessBuilder::with_step_dt` to customize with how big of a dt each frame is called. I set the default to 1.0 / 6.0 (~6fps) so we don't waste cpu in tests waiting on animations. `HarnessBuilder::max_steps` allows us to control how many steps `Harness::run` should run before panicing. The default is 6, so we run for up to 1.0 logical seconds (six frames at 6 fps), which should be enough to finish most animations. Turns out a lot of snapshots where rendered before fully shown and had a light opacity, those are now fixed. * [x] I have followed the instructions in the PR template --- crates/egui/src/context.rs | 2 +- crates/egui_demo_app/tests/test_demo_app.rs | 5 +- .../src/demo/demo_app_windows.rs | 2 +- crates/egui_demo_lib/src/demo/modals.rs | 22 +-- .../tests/snapshots/demos/Code Editor.png | 4 +- .../tests/snapshots/demos/Code Example.png | 4 +- .../tests/snapshots/demos/Context Menus.png | 4 +- .../tests/snapshots/demos/Dancing Strings.png | 4 +- .../tests/snapshots/demos/Drag and Drop.png | 4 +- .../tests/snapshots/demos/Font Book.png | 4 +- .../tests/snapshots/demos/Frame.png | 4 +- .../tests/snapshots/demos/Highlighting.png | 4 +- .../snapshots/demos/Interactive Container.png | 4 +- .../tests/snapshots/demos/Misc Demos.png | 4 +- .../tests/snapshots/demos/Modals.png | 4 +- .../tests/snapshots/demos/Multi Touch.png | 4 +- .../tests/snapshots/demos/Painting.png | 4 +- .../tests/snapshots/demos/Pan Zoom.png | 4 +- .../tests/snapshots/demos/Panels.png | 4 +- .../tests/snapshots/demos/Screenshot.png | 4 +- .../tests/snapshots/demos/Scrolling.png | 4 +- .../tests/snapshots/demos/Sliders.png | 4 +- .../tests/snapshots/demos/Strip.png | 4 +- .../tests/snapshots/demos/Table.png | 4 +- .../tests/snapshots/demos/Text Layout.png | 4 +- .../tests/snapshots/demos/TextEdit.png | 4 +- .../tests/snapshots/demos/Tooltips.png | 4 +- .../tests/snapshots/demos/Undo Redo.png | 4 +- .../tests/snapshots/demos/Window Options.png | 4 +- .../tests/snapshots/modals_1.png | 4 +- .../snapshots/rendering_test/dpi_1.00.png | 4 +- .../snapshots/rendering_test/dpi_1.25.png | 4 +- .../snapshots/rendering_test/dpi_1.50.png | 4 +- .../snapshots/rendering_test/dpi_1.67.png | 4 +- .../snapshots/rendering_test/dpi_1.75.png | 4 +- .../snapshots/rendering_test/dpi_2.00.png | 4 +- crates/egui_kittest/src/builder.rs | 24 +++ crates/egui_kittest/src/lib.rs | 144 ++++++++++++++++-- 38 files changed, 230 insertions(+), 97 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index b696d338c..95ae19656 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -775,7 +775,7 @@ impl Context { writer(&mut self.0.write()) } - /// Run the ui code for one 1. + /// Run the ui code for one frame. /// /// At most [`Options::max_passes`] calls will be issued to `run_ui`, /// and only on the rare occasion that [`Context::request_discard`] is called. diff --git a/crates/egui_demo_app/tests/test_demo_app.rs b/crates/egui_demo_app/tests/test_demo_app.rs index b242dad43..fc4940fb9 100644 --- a/crates/egui_demo_app/tests/test_demo_app.rs +++ b/crates/egui_demo_app/tests/test_demo_app.rs @@ -49,7 +49,7 @@ fn test_demo_app() { // Load a local image where we know it exists and loads quickly #[cfg(feature = "image_viewer")] Anchor::ImageViewer => { - harness.run(); + harness.step(); harness .get_by_role_and_label(Role::TextInput, "URI:") @@ -65,7 +65,8 @@ fn test_demo_app() { _ => {} } - harness.run(); + // Can't use Harness::run because fractal clock keeps requesting repaints + harness.run_steps(2); if let Err(e) = harness.try_snapshot(&anchor.to_string()) { results.push(e); 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 9cf01e9ea..64d4d7809 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -397,7 +397,7 @@ mod tests { harness.set_size(Vec2::new(size.width as f32, size.height as f32)); // Run the app for some more frames... - harness.run(); + harness.run_ok(); let mut options = SnapshotOptions::default(); // The Bézier Curve demo needs a threshold of 2.1 to pass on linux diff --git a/crates/egui_demo_lib/src/demo/modals.rs b/crates/egui_demo_lib/src/demo/modals.rs index 8d22c0cc0..833e07a28 100644 --- a/crates/egui_demo_lib/src/demo/modals.rs +++ b/crates/egui_demo_lib/src/demo/modals.rs @@ -183,12 +183,13 @@ mod tests { harness.get_by_role(Role::ComboBox).click(); - harness.run(); + // Harness::run would fail because we keep requesting repaints to simulate progress. + harness.run_ok(); assert!(harness.ctx.memory(|mem| mem.any_popup_open())); assert!(harness.state().user_modal_open); harness.press_key(Key::Escape); - harness.run(); + harness.run_ok(); assert!(!harness.ctx.memory(|mem| mem.any_popup_open())); assert!(harness.state().user_modal_open); } @@ -238,17 +239,11 @@ mod tests { results.push(harness.try_snapshot("modals_1")); harness.get_by_label("Save").click(); - // TODO(lucasmerlin): Remove these extra runs once run checks for repaint requests - harness.run(); - harness.run(); - harness.run(); + harness.run_ok(); results.push(harness.try_snapshot("modals_2")); harness.get_by_label("Yes Please").click(); - // TODO(lucasmerlin): Remove these extra runs once run checks for repaint requests - harness.run(); - harness.run(); - harness.run(); + harness.run_ok(); results.push(harness.try_snapshot("modals_3")); for result in results { @@ -272,14 +267,11 @@ mod tests { initial_state, ); - // TODO(lucasmerlin): Remove these extra runs once run checks for repaint requests - harness.run(); - harness.run(); - harness.run(); + harness.run_ok(); harness.get_by_label("Yes Please").simulate_click(); - harness.run(); + harness.run_ok(); // This snapshots should show the progress bar modal on top of the save modal. harness.snapshot("modals_backdrop_should_prevent_focusing_lower_area"); diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png b/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png index 242171125..6ab8e1327 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e89c730b462c2b60b90f2ac15fe9576e878a4906c223317c51344a0ec2b6d993 -size 27564 +oid sha256:4631f841b9e23833505af39eb1c45908013d3b1e1278d477bcedf6a460c71802 +size 27163 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png b/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png index 99885a8aa..3e05ffd4f 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea2c944af8bc1be42ec7c00be58dfaa23c92bca8957eda94f2ff10f5b4242562 -size 83358 +oid sha256:96e750ebfcc6ec2c130674407388c30cce844977cde19adfebf351fd08698a4f +size 81726 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Context Menus.png b/crates/egui_demo_lib/tests/snapshots/demos/Context Menus.png index d32a46ce7..15123bc3b 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Context Menus.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Context Menus.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c401ff91fff4051042528d398d2b2270a4ae924570e6332cf8f2c6774c845160 -size 11826 +oid sha256:b03613e597631da3b922ef3d696c4ce74cec41f86a2779fc5b084a316fc9e8e8 +size 11764 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png b/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png index 4f44756de..feb573aa0 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7efc1ff3e4e5bfd4216394f94ee7486c272a9ca1c980789f4ad143f89b0a7103 -size 21073 +oid sha256:76d77e2df39af248d004a023b238bb61ed598ab2bea3e0c6f2885f9537ec1103 +size 25988 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png b/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png index 8a831b65d..34315f422 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d9c48cf928a17dd0980ba086aa004bde3a0040dcb82752d138c1df34f1ef3d2f -size 21167 +oid sha256:003d905893b80ffc132a783155971ad3054229e9d6c046e2760c912559a66b3d +size 20869 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png b/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png index f85436ef7..b5e5c4bec 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc69c76eaa121e9e7782cfbbb68b5a23004d79862bae4af2e3ca3a29eff04bea -size 136467 +oid sha256:99c94a09c0f6984909481189f9a1415ea90bd7c45e42b4b3763ee36f3d831a65 +size 133231 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Frame.png b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png index cec956325..8d9779b29 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Frame.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:23187a9fb12a3ab7df4e2321aa25b493559923d61e82802f843ee29dcd932f7b -size 24985 +oid sha256:7d8135b745cb95a7e7c7a26e73e160742f88ec177a2fa262215c4886d98ff172 +size 24403 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png b/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png index 12d396cb4..b974f7489 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f433f3e8bff38a0aafd7e6cba5c5efe1abf484550a6f9e90008f8f5ea891497 -size 18113 +oid sha256:7b38828e423195628dcea09b0cbdd66aa4c077334ab7082efd358c7a3297009d +size 17827 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png b/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png index 576b7b328..6853e8564 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e5105ecf77852412c0dd904b96f0fec752f22e416df9932df4499d6d5a776f46 -size 22865 +oid sha256:c53403996451da14f7991ea61bd20b96dbc67eb67dd2041975dd6ce5015a6980 +size 22485 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 db0322f86..baf613180 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:ebf0403bd599e5c00c2831f9c4032e8d20420212c9cd7fa875f1ae1cbbc8d3a7 -size 65902 +oid sha256:63673b070951246b98ca07613fa81496dbfdd10627bac3c9c4356ebff1a36b20 +size 64319 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Modals.png b/crates/egui_demo_lib/tests/snapshots/demos/Modals.png index 20ed40f4b..4764035ca 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Modals.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Modals.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4e690dc73873ab75c030d3c0238e9d5b840f976dd8f4882dc1e930024d217838 -size 33323 +oid sha256:374b4095a3c7b827b80d6ab01b945458ae0128a2974c2af8aaf6b7e9fef6b459 +size 32554 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png b/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png index 68470555b..7e6254b74 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e760210371dbf2a197f96a78d01b7480f0ae05d46bbb4e642276b2eb30847ec2 -size 37075 +oid sha256:5e756c90069803bb4d2821fff723877bffffd44b26613f5b06c8a042d6901ca4 +size 36578 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Painting.png b/crates/egui_demo_lib/tests/snapshots/demos/Painting.png index a2f6c14ad..2638bf08d 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Painting.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Painting.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fd02b208d0e4e306bbc9a54f25f5a3d20875a12182cef3224e6daa309b6cf453 -size 17898 +oid sha256:64fe3ef34aaf3104931954f4a39760b99944f42da13f866622ca0222b750f6be +size 17731 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Pan Zoom.png b/crates/egui_demo_lib/tests/snapshots/demos/Pan Zoom.png index c8f448fac..3a8b19322 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Pan Zoom.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Pan Zoom.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:341958da648a7db3374c4337cf057ae8e81c08c4a6de7e4f1cbe9c5b049f2e62 -size 25727 +oid sha256:dfa05d7f8c36b51c054253fbe8483c38637a12dde4a9d6051b226680820db319 +size 25097 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Panels.png b/crates/egui_demo_lib/tests/snapshots/demos/Panels.png index 9ce35877b..fa400718f 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Panels.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Panels.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8934cff7203d19b38df9d91729091ff5d1ad6c8d445fd9c1cb62b6df1bb8cb80 -size 263547 +oid sha256:eb631bd2608aec6c83f8815b9db0b28627bf82692fd2b1cb793119083b4f8ad1 +size 264496 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png b/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png index d9f7bc130..023aaa104 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5d61f58138798d701bb8dda2c3240eef69eb350df3168fb3aa4148e4fef3f77a -size 24077 +oid sha256:e0870e9e1c9dc718690124a4d490f1061414f15fa40571e571d9319c3b65e74e +size 23709 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png b/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png index bb0b7a007..7efa04ccc 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:e4006e93663d02fe0f4485d2c163ab2b6feded787bee87ea15616fc0b36136d0 -size 188875 +oid sha256:532dbcbec94bb9c9fb8cc0665646790a459088e98077118b5fbb2112898e1a43 +size 183854 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png b/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png index 56d6d37ce..b03b3dcc7 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:70b170ba7b8e51d9d9f766d7ce25068fa4265c4127e729af4f1adaacbb745d19 -size 120947 +oid sha256:3fd584643b68084ec4b65369e08d229e2d389551bbefa59c84ad4b58621593f7 +size 117754 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Strip.png b/crates/egui_demo_lib/tests/snapshots/demos/Strip.png index 78a671578..972368971 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Strip.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Strip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:157353a8c9bcb638a8be485489e4a232f348eae3cc4ceefe227d7970c7d1f8b3 -size 26256 +oid sha256:7e2b854d99c9b17d15ef92188cdac521d7c0635aef9ba457cd3801884a784009 +size 26159 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Table.png b/crates/egui_demo_lib/tests/snapshots/demos/Table.png index 89e92317b..9c5e33304 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:cf0ddd39a45519dcf9027f691e856921c736d18e2eeafd16f0e086720121b6a7 -size 72286 +oid sha256:f0b7dc029de8e669d76f3be5e0800e973c354edcf7cefa239faed07c2cd5e0d5 +size 70452 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png b/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png index 90259a56d..b73935d7e 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:914d37e326087f770994bcf3867a27d88050c57887a2b42c68414d311fa96003 -size 67698 +oid sha256:64ba40810a6480e511e8d198b0dfb39e8b220eb2f5592877e27b17ee8d47b9c3 +size 66387 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png b/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png index 4bbe0f59b..07e9177b7 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b0fdf8ce329883450e071e4733c3904577999d18ac61c922c7caacbec09dfda7 -size 21661 +oid sha256:0a8151f5bd01b2cb5183c532d11f57bbb7e8cc1e77a3c4b890537d157922c961 +size 21261 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png b/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png index 4f0a7bc17..e53122482 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:375b71a8ac5b0e48f3c67a089ef0e8a4fd17f8eb21fa535422099c29c2747e27 -size 59991 +oid sha256:96ce36fcc334793801ab724c711f592faf086a9c98c812684e6b76010e9d1974 +size 59714 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png b/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png index f96b4ebee..028346098 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aa96b1e3733e4af94a6cb6ec765c3f3581df2175e75831eb00bd42df2e7a2708 -size 13285 +oid sha256:8155d93b78692ced67ddee4011526ef627838ede78351871df5feef8aa512857 +size 13141 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png b/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png index 102faa02a..70cf2378e 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:18880dfaf5d198876c4db97ebd6313d59755a3e8298567f2b2fa91dcc21699c5 -size 35607 +oid sha256:bf6da022ab97f9d4b862cc8e91bdfd7f9785a3ab0540aa1c2bd2646bd30a3450 +size 35115 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_1.png b/crates/egui_demo_lib/tests/snapshots/modals_1.png index 46c12fa2d..2ce31fe45 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_1.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d626b310439bff13487548bbba8b516886c13049824a7f5dd902f6dffb3c5ba4 -size 48234 +oid sha256:035b35ed053cabd5022d9a97f7d4d5a79d194c9f14594e7602ecd9ef94c24ae5 +size 48053 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png index 4bb0582ae..3e3ec6991 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ed2a356452d792e32bea57f044da9d86da27fd8504826dd6b87618a53519ea6a -size 522556 +oid sha256:3562bb87b155b2c406a72a651ffb1991305aa1e15273ce9f32cedc5766318537 +size 554922 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png index 1abf12ef1..d5d26d2bb 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0d04a5854528c6141f7def6f9229c51c6d2d4c87e2f656be4d149e7b2b852976 -size 729056 +oid sha256:ba6c0bd127ab02375f2ae5fbc3eeef33a1bdf074cbb180de2733c700b81df3e5 +size 771069 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png index c3bd68ac2..98f73cee9 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e6e5c1a745e357faa7b98f7a2cd1ca139c4a14be154b9d21feb8030933acfdb7 -size 867552 +oid sha256:d85ab6d04059009fd2c3ad8001332b27e710c46c9300f2f1f409b882c49211dc +size 918967 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png index 4564e5871..6de95b07d 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d9f4a37541fd1a0754c1cb1f3a2d4a76f03d67ca4e5596c8e6982d691d29dea -size 980286 +oid sha256:a64f1bdec565357fe4dee3acb46b12eeb0492b522fb3bb9697d39dadce2e8c21 +size 1039455 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png index 93931ef38..1d373b92b 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4d1c99867202e16500146b7146a32fd83d70d60f5ac94aae4ca405a6377e4625 -size 1066559 +oid sha256:5ca008dca03372bb334564e55fa2d1d25a36751a43df6001a1c1cf3e4db9bcd4 +size 1130930 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png index 4d7663946..b8b2a7658 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:08fc1c89fd2d04aa12c75a1829dacfff090e322c65ad969648799833e1b072eb -size 1235574 +oid sha256:7f695127e7fe6cb3b752a0acd71db85d51d2de68e45051a7afe91f4d960acf27 +size 1311641 diff --git a/crates/egui_kittest/src/builder.rs b/crates/egui_kittest/src/builder.rs index d0c219bb1..d21558113 100644 --- a/crates/egui_kittest/src/builder.rs +++ b/crates/egui_kittest/src/builder.rs @@ -8,6 +8,8 @@ use std::marker::PhantomData; pub struct HarnessBuilder { pub(crate) screen_rect: Rect, pub(crate) pixels_per_point: f32, + pub(crate) max_steps: u64, + pub(crate) step_dt: f32, pub(crate) state: PhantomData, pub(crate) renderer: Box, } @@ -19,6 +21,8 @@ impl Default for HarnessBuilder { pixels_per_point: 1.0, state: PhantomData, renderer: Box::new(LazyRenderer::default()), + max_steps: 4, + step_dt: 1.0 / 4.0, } } } @@ -40,6 +44,26 @@ impl HarnessBuilder { self } + /// Set the maximum number of steps to run when calling [`Harness::run`]. + /// + /// Default is 4. + /// With the default `step_dt`, this means 1 second of simulation. + #[inline] + pub fn with_max_steps(mut self, max_steps: u64) -> Self { + self.max_steps = max_steps; + self + } + + /// Set the time delta for a single [`Harness::step`]. + /// + /// Default is 1.0 / 4.0 (4fps). + /// The default is low so we don't waste cpu waiting for animations. + #[inline] + pub fn with_step_dt(mut self, step_dt: f32) -> Self { + self.step_dt = step_dt; + self + } + /// Set the [`TestRenderer`] to use for rendering. /// /// By default, a [`LazyRenderer`] is used. diff --git a/crates/egui_kittest/src/lib.rs b/crates/egui_kittest/src/lib.rs index 83c6f548a..dfaad2c97 100644 --- a/crates/egui_kittest/src/lib.rs +++ b/crates/egui_kittest/src/lib.rs @@ -10,7 +10,9 @@ mod snapshot; #[cfg(feature = "snapshot")] pub use snapshot::*; -use std::fmt::{Debug, Formatter}; +use std::fmt::{Debug, Display, Formatter}; +use std::time::Duration; + mod app_kind; mod renderer; #[cfg(feature = "wgpu")] @@ -26,9 +28,26 @@ use crate::event::EventState; pub use builder::*; pub use renderer::*; -use egui::{Modifiers, Pos2, Rect, Vec2, ViewportId}; +use egui::{Modifiers, Pos2, Rect, RepaintCause, Vec2, ViewportId}; use kittest::{Node, Queryable}; +pub struct ExceededMaxStepsError { + pub max_steps: u64, + pub repaint_causes: Vec, +} + +impl Display for ExceededMaxStepsError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Harness::run exceeded max_steps ({}). If your expect your ui to keep repainting \ + (e.g. when showing a spinner) call Harness::step or Harness::run_steps instead.\ + \nRepaint causes: {:#?}", + self.max_steps, self.repaint_causes, + ) + } +} + /// The test Harness. This contains everything needed to run the test. /// Create a new Harness using [`Harness::new`] or [`Harness::builder`]. /// @@ -45,6 +64,8 @@ pub struct Harness<'a, State = ()> { response: Option, state: State, renderer: Box, + max_steps: u64, + step_dt: f32, } impl<'a, State> Debug for Harness<'a, State> { @@ -60,14 +81,24 @@ impl<'a, State> Harness<'a, State> { mut state: State, ctx: Option, ) -> Self { + let HarnessBuilder { + screen_rect, + pixels_per_point, + max_steps, + step_dt, + state: _, + mut renderer, + } = builder; let ctx = ctx.unwrap_or_default(); ctx.enable_accesskit(); + // Disable cursor blinking so it doesn't interfere with snapshots + ctx.all_styles_mut(|style| style.visuals.text_cursor.blink = false); let mut input = egui::RawInput { - screen_rect: Some(builder.screen_rect), + screen_rect: Some(screen_rect), ..Default::default() }; let viewport = input.viewports.get_mut(&ViewportId::ROOT).unwrap(); - viewport.native_pixels_per_point = Some(builder.pixels_per_point); + viewport.native_pixels_per_point = Some(pixels_per_point); let mut response = None; @@ -77,7 +108,6 @@ impl<'a, State> Harness<'a, State> { response = app.run(ctx, &mut state, false); }); - let mut renderer = builder.renderer; renderer.handle_delta(&output.textures_delta); let mut harness = Self { @@ -96,9 +126,11 @@ impl<'a, State> Harness<'a, State> { event_state: EventState::default(), state, renderer, + max_steps, + step_dt, }; // Run the harness until it is stable, ensuring that all Areas are shown and animations are done - harness.run(); + harness.run_ok(); harness } @@ -188,7 +220,8 @@ impl<'a, State> Harness<'a, State> { } /// Run a frame. - /// This will call the app closure with the current context and update the Harness. + /// This will call the app closure with the queued events and current context and + /// update the Harness. pub fn step(&mut self) { self._step(false); } @@ -200,6 +233,8 @@ impl<'a, State> Harness<'a, State> { } } + self.input.predicted_dt = self.step_dt; + let mut output = self.ctx.run(self.input.take(), |ctx| { self.response = self.app.run(ctx, &mut self.state, sizing_pass); }); @@ -215,22 +250,95 @@ impl<'a, State> Harness<'a, State> { } /// Resize the test harness to fit the contents. This only works when creating the Harness via - /// [`Harness::new_ui`] or [`HarnessBuilder::build_ui`]. + /// [`Harness::new_ui`] / [`Harness::new_ui_state`] or + /// [`HarnessBuilder::build_ui`] / [`HarnessBuilder::build_ui_state`]. pub fn fit_contents(&mut self) { self._step(true); if let Some(response) = &self.response { self.set_size(response.rect.size()); } - self.run(); + self.run_ok(); } - /// Run a few frames. - /// This will soon be changed to run the app until it is "stable", meaning + /// Run until /// - all animations are done /// - no more repaints are requested - pub fn run(&mut self) { - const STEPS: usize = 2; - for _ in 0..STEPS { + /// + /// Returns the number of frames that were run. + /// + /// # Panics + /// Panics if the number of steps exceeds the maximum number of steps set + /// in [`HarnessBuilder::with_max_steps`]. + /// + /// See also: + /// - [`Harness::try_run`]. + /// - [`Harness::run_ok`]. + /// - [`Harness::step`]. + /// - [`Harness::run_steps`]. + #[track_caller] + pub fn run(&mut self) -> u64 { + match self.try_run() { + Ok(steps) => steps, + Err(err) => { + panic!("{err}"); + } + } + } + + /// Run 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`]. + 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) + } + + /// Run 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, or None if the maximum number of steps was exceeded. + /// + /// See also: + /// - [`Harness::run`]. + /// - [`Harness::try_run`]. + /// - [`Harness::step`]. + /// - [`Harness::run_steps`]. + pub fn run_ok(&mut self) -> Option { + self.try_run().ok() + } + + /// Run a number of steps. + /// Equivalent to calling [`Harness::step`] x times. + pub fn run_steps(&mut self, steps: usize) { + for _ in 0..steps { self.step(); } } @@ -297,6 +405,14 @@ impl<'a, State> Harness<'a, State> { pub fn render(&mut self) -> Result { self.renderer.render(&self.ctx, &self.output) } + + /// Get the root viewport output + fn root_viewport_output(&self) -> &egui::ViewportOutput { + self.output + .viewport_output + .get(&ViewportId::ROOT) + .expect("Missing root viewport") + } } /// Utilities for stateless harnesses. From 7cb8187ac8daa84b7aa08e27a85c4463cdc259e0 Mon Sep 17 00:00:00 2001 From: Aely <29923178+Aely0@users.noreply.github.com> Date: Tue, 7 Jan 2025 09:35:58 +0200 Subject: [PATCH 009/132] Support RGB WebP images (#5586) Current WebP loader assumes all WebP images to be RGBA, which is the case if the image is animated (that's what `image` crate assumes at least). Static images can instead choose to exclude its alpha channel, though it seems to be more of a default choice to include it, even if it's not being utilized. Currently, loading a static RGB WebP image will cause a panic when `ColorImage::from_rgba_unmultiplied` gets called in the loader ``` thread 'main' panicked at /home/aely/.cargo/git/checkouts/egui-226fc7cdd51201c1/f87219d/crates/epaint/src/image.rs:97:9: assertion `left == right` failed left: 29184 right: 21888 ``` --- crates/egui_extras/src/loaders/webp_loader.rs | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/crates/egui_extras/src/loaders/webp_loader.rs b/crates/egui_extras/src/loaders/webp_loader.rs index bb042093b..528c449a7 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 _, ImageDecoder, Rgba}; +use image::{codecs::webp::WebPDecoder, AnimationDecoder as _, ColorType, ImageDecoder, Rgba}; use std::{io::Cursor, mem::size_of, sync::Arc, time::Duration}; #[derive(Clone)] @@ -48,6 +48,17 @@ impl WebP { frame_durations: FrameDurations::new(durations), })) } else { + // color_type() of WebPDecoder only returns Rgb8/Rgba8 variants of ColorType + let create_image = match decoder.color_type() { + ColorType::Rgb8 => ColorImage::from_rgb, + ColorType::Rgba8 => ColorImage::from_rgba_unmultiplied, + unreachable => { + return Err(format!( + "Unreachable WebP color type, expected Rgb8/Rgba8, got {unreachable:?}" + )) + } + }; + let (width, height) = decoder.dimensions(); let size = decoder.total_bytes() as usize; @@ -56,10 +67,10 @@ impl WebP { .read_image(&mut data) .map_err(|error| format!("WebP image read failure ({error})"))?; - let image = - ColorImage::from_rgba_unmultiplied([width as usize, height as usize], &data); - - Ok(Self::Static(Arc::new(image))) + Ok(Self::Static(Arc::new(create_image( + [width as usize, height as usize], + &data, + )))) } } From 329c8f2fc14ebfe773f2283694087ecd691cb9f3 Mon Sep 17 00:00:00 2001 From: Andrew Farkas <6060305+HactarCE@users.noreply.github.com> Date: Tue, 7 Jan 2025 02:37:23 -0500 Subject: [PATCH 010/132] Fix panic due to non-total ordering in `Area::compare_order()` (#5569) [Area::compare_order()](https://github.com/emilk/egui/blob/ee4ab08c8a208f4044a8c571326c9414e7a1c8a6/crates/egui/src/memory/mod.rs#L1174-L1183) is not a total ordering. If three layers A, B, and C have the same `order` field but only A and B are present in `order_map`, then `A==C` and `B==C` but `A!=C`. This can cause a panic in the stdlib sort function, and does in [my app](https://github.com/HactarCE/Hyperspeedcube/tree/v2.0) although it's very difficult to reproduce. * [x] I have self-reviewed this PR and run `./scripts/check.sh` * [x] I have followed the instructions in the PR template --- crates/egui/src/memory/mod.rs | 56 +++++++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index 976ad2d95..2c669f0ff 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -1175,17 +1175,19 @@ impl Areas { /// /// May return [`std::cmp::Ordering::Equal`] if the layers are not in the order list. pub(crate) fn compare_order(&self, a: LayerId, b: LayerId) -> std::cmp::Ordering { - if let (Some(a), Some(b)) = (self.order_map.get(&a), self.order_map.get(&b)) { - a.cmp(b) - } else { - a.order.cmp(&b.order) + // Sort by layer `order` first and use `order_map` to resolve disputes. + // If `order_map` only contains one layer ID, then the other one will be + // lower because `None < Some(x)`. + match a.order.cmp(&b.order) { + std::cmp::Ordering::Equal => self.order_map.get(&a).cmp(&self.order_map.get(&b)), + cmp => cmp, } } pub(crate) fn set_state(&mut self, layer_id: LayerId, state: area::AreaState) { self.visible_areas_current_frame.insert(layer_id); self.areas.insert(layer_id.id, state); - if !self.order.iter().any(|x| *x == layer_id) { + if !self.order.contains(&layer_id) { self.order.push(layer_id); } } @@ -1351,3 +1353,47 @@ fn memory_impl_send_sync() { fn assert_send_sync() {} assert_send_sync::(); } + +#[test] +fn order_map_total_ordering() { + let mut layers = [ + LayerId::new(Order::Tooltip, Id::new("a")), + LayerId::new(Order::Background, Id::new("b")), + LayerId::new(Order::Background, Id::new("c")), + LayerId::new(Order::Tooltip, Id::new("d")), + LayerId::new(Order::Background, Id::new("e")), + LayerId::new(Order::Background, Id::new("f")), + LayerId::new(Order::Tooltip, Id::new("g")), + ]; + let mut areas = Areas::default(); + + // skip some of the layers + for &layer in &layers[3..] { + areas.set_state(layer, crate::AreaState::default()); + } + areas.end_pass(); // sort layers + + // Sort layers + layers.sort_by(|&a, &b| areas.compare_order(a, b)); + + // Assert that `areas.compare_order()` forms a total ordering + let mut equivalence_classes = vec![0]; + let mut i = 0; + for l in layers.windows(2) { + assert!(l[0].order <= l[1].order, "does not follow LayerId.order"); + if areas.compare_order(l[0], l[1]) != std::cmp::Ordering::Equal { + i += 1; + } + equivalence_classes.push(i); + } + assert_eq!(layers.len(), equivalence_classes.len()); + for (&l1, c1) in std::iter::zip(&layers, &equivalence_classes) { + for (&l2, c2) in std::iter::zip(&layers, &equivalence_classes) { + assert_eq!( + c1.cmp(c2), + areas.compare_order(l1, l2), + "not a total ordering", + ); + } + } +} From 7186f72cbeda14bc2465cc05eec2ed97f19f170e Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Tue, 7 Jan 2025 13:26:57 +0100 Subject: [PATCH 011/132] Add a test for comboboxes (#5574) * [x] I have followed the instructions in the PR template --- crates/egui_kittest/tests/regression_tests.rs | 53 ++++++++++++++++++- .../tests/snapshots/combobox_closed.png | 3 ++ .../tests/snapshots/combobox_opened.png | 3 ++ 3 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 crates/egui_kittest/tests/snapshots/combobox_closed.png create mode 100644 crates/egui_kittest/tests/snapshots/combobox_opened.png diff --git a/crates/egui_kittest/tests/regression_tests.rs b/crates/egui_kittest/tests/regression_tests.rs index 32b412e6e..8567e10ea 100644 --- a/crates/egui_kittest/tests/regression_tests.rs +++ b/crates/egui_kittest/tests/regression_tests.rs @@ -1,4 +1,5 @@ -use egui::{Button, Image, Vec2, Widget}; +use egui::accesskit::Role; +use egui::{Button, ComboBox, Image, Vec2, Widget}; use egui_kittest::{kittest::Queryable, Harness}; #[test] @@ -43,3 +44,53 @@ fn image_failed() { #[cfg(all(feature = "wgpu", feature = "snapshot"))] harness.snapshot("image_snapshots"); } + +#[test] +fn test_combobox() { + let items = ["Item 1", "Item 2", "Item 3"]; + let mut harness = Harness::builder() + .with_size(Vec2::new(300.0, 200.0)) + .build_ui_state( + |ui, selected| { + ComboBox::new("combobox", "Select Something").show_index( + ui, + selected, + items.len(), + |idx| *items.get(idx).expect("Invalid index"), + ); + }, + 0, + ); + + harness.run(); + + let mut results = vec![]; + + #[cfg(all(feature = "wgpu", feature = "snapshot"))] + results.push(harness.try_snapshot("combobox_closed")); + + let combobox = harness.get_by_role_and_label(Role::ComboBox, "Select Something"); + combobox.click(); + + harness.run(); + + #[cfg(all(feature = "wgpu", feature = "snapshot"))] + results.push(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(); + + harness.run(); + + assert_eq!(harness.state(), &1); + + // Popup should be closed now + assert!(harness.query_by_label("Item 2").is_none()); + + for result in results { + if let Err(err) = result { + panic!("{}", err); + } + } +} diff --git a/crates/egui_kittest/tests/snapshots/combobox_closed.png b/crates/egui_kittest/tests/snapshots/combobox_closed.png new file mode 100644 index 000000000..18a9d0bc9 --- /dev/null +++ b/crates/egui_kittest/tests/snapshots/combobox_closed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:202091634d4483949cb1c5c4c5ec02faa23f4d19e7e833aba135887b77e3188d +size 4485 diff --git a/crates/egui_kittest/tests/snapshots/combobox_opened.png b/crates/egui_kittest/tests/snapshots/combobox_opened.png new file mode 100644 index 000000000..201fc999e --- /dev/null +++ b/crates/egui_kittest/tests/snapshots/combobox_opened.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a61ecf294d930ebbee9837611d7a75381e690348f448b1c0c8264b27f44ceb3 +size 7535 From 443df84a22881e78f003688ce7ece802b2b050c3 Mon Sep 17 00:00:00 2001 From: Andreas Reich Date: Wed, 8 Jan 2025 14:24:58 +0100 Subject: [PATCH 012/132] Extend `WgpuSetup`, `egui_kittest` now prefers software rasterizers for testing (#5506) --- Cargo.lock | 29 +- crates/eframe/src/native/wgpu_integration.rs | 4 +- crates/eframe/src/web/web_painter_wgpu.rs | 46 +-- crates/egui-wgpu/src/lib.rs | 315 +++++++++---------- crates/egui-wgpu/src/setup.rs | 237 ++++++++++++++ crates/egui-wgpu/src/winit.rs | 13 +- crates/egui_demo_lib/Cargo.toml | 1 - crates/egui_kittest/Cargo.toml | 11 +- crates/egui_kittest/src/wgpu.rs | 75 +++-- 9 files changed, 478 insertions(+), 253 deletions(-) create mode 100644 crates/egui-wgpu/src/setup.rs diff --git a/Cargo.lock b/Cargo.lock index 2f3085cb3..930cf3326 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1349,7 +1349,6 @@ dependencies = [ "egui_kittest", "serde", "unicode_names2", - "wgpu", ] [[package]] @@ -1927,6 +1926,18 @@ dependencies = [ "bitflags 2.6.0", ] +[[package]] +name = "gpu-allocator" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd" +dependencies = [ + "log", + "presser", + "thiserror", + "windows", +] + [[package]] name = "gpu-descriptor" version = "0.3.0" @@ -3110,6 +3121,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + [[package]] name = "proc-macro-crate" version = "3.2.0" @@ -3263,6 +3280,12 @@ dependencies = [ "getrandom", ] +[[package]] +name = "range-alloc" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8a99fddc9f0ba0a85884b8d14e3592853e787d581ca1816c91349b10e4eeab" + [[package]] name = "raw-window-handle" version = "0.5.2" @@ -4513,6 +4536,7 @@ dependencies = [ "android_system_properties", "arrayvec", "ash", + "bit-set 0.8.0", "bitflags 2.6.0", "block", "bytemuck", @@ -4521,6 +4545,7 @@ dependencies = [ "glow 0.14.2", "glutin_wgl_sys", "gpu-alloc", + "gpu-allocator", "gpu-descriptor", "js-sys", "khronos-egl", @@ -4534,6 +4559,7 @@ dependencies = [ "once_cell", "parking_lot", "profiling", + "range-alloc", "raw-window-handle 0.6.2", "renderdoc-sys", "rustc-hash", @@ -4543,6 +4569,7 @@ dependencies = [ "web-sys", "wgpu-types", "windows", + "windows-core 0.58.0", ] [[package]] diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index 93643c223..dc12c52a9 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -183,7 +183,7 @@ impl<'app> WgpuWinitApp<'app> { ) -> crate::Result<&mut WgpuWinitRunning<'app>> { profiling::function_scope!(); #[allow(unsafe_code, unused_mut, unused_unsafe)] - let mut painter = egui_wgpu::winit::Painter::new( + let mut painter = pollster::block_on(egui_wgpu::winit::Painter::new( egui_ctx.clone(), self.native_options.wgpu_options.clone(), self.native_options.multisampling.max(1) as _, @@ -193,7 +193,7 @@ impl<'app> WgpuWinitApp<'app> { ), self.native_options.viewport.transparent.unwrap_or(false), self.native_options.dithering, - ); + )); let window = Arc::new(window); diff --git a/crates/eframe/src/web/web_painter_wgpu.rs b/crates/eframe/src/web/web_painter_wgpu.rs index 5e217180c..f018c7316 100644 --- a/crates/eframe/src/web/web_painter_wgpu.rs +++ b/crates/eframe/src/web/web_painter_wgpu.rs @@ -4,7 +4,7 @@ use super::web_painter::WebPainter; use crate::WebOptions; use egui::{Event, UserData, ViewportId}; use egui_wgpu::capture::{capture_channel, CaptureReceiver, CaptureSender, CaptureState}; -use egui_wgpu::{RenderState, SurfaceErrorAction, WgpuSetup}; +use egui_wgpu::{RenderState, SurfaceErrorAction}; use wasm_bindgen::JsValue; use web_sys::HtmlCanvasElement; @@ -63,49 +63,7 @@ impl WebPainterWgpu { ) -> Result { log::debug!("Creating wgpu painter"); - let instance = match &options.wgpu_options.wgpu_setup { - WgpuSetup::CreateNew { - supported_backends: backends, - power_preference, - .. - } => { - let mut backends = *backends; - - // Don't try WebGPU if we're not in a secure context. - if backends.contains(wgpu::Backends::BROWSER_WEBGPU) { - let is_secure_context = - web_sys::window().map_or(false, |w| w.is_secure_context()); - if !is_secure_context { - log::info!( - "WebGPU is only available in secure contexts, i.e. on HTTPS and on localhost." - ); - - // Don't try WebGPU since we established now that it will fail. - backends.remove(wgpu::Backends::BROWSER_WEBGPU); - - if backends.is_empty() { - return Err("No available supported graphics backends.".to_owned()); - } - } - } - - log::debug!("Creating wgpu instance with backends {:?}", backends); - - let instance = - wgpu::util::new_instance_with_webgpu_detection(wgpu::InstanceDescriptor { - backends, - ..Default::default() - }) - .await; - - // 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)] - Arc::new(instance) - } - WgpuSetup::Existing { instance, .. } => instance.clone(), - }; - + let instance = options.wgpu_options.wgpu_setup.new_instance().await; let surface = instance .create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone())) .map_err(|err| format!("failed to create wgpu surface: {err}"))?; diff --git a/crates/egui-wgpu/src/lib.rs b/crates/egui-wgpu/src/lib.rs index b90e52001..0d795783a 100644 --- a/crates/egui-wgpu/src/lib.rs +++ b/crates/egui-wgpu/src/lib.rs @@ -23,8 +23,10 @@ pub use wgpu; /// Low-level painting of [`egui`](https://github.com/emilk/egui) on [`wgpu`]. mod renderer; +mod setup; + pub use renderer::*; -use wgpu::{Adapter, Device, Instance, Queue, TextureFormat}; +pub use setup::{NativeAdapterSelectorMethod, WgpuSetup, WgpuSetupCreateNew, WgpuSetupExisting}; /// Helpers for capturing screenshots of the UI. pub mod capture; @@ -40,8 +42,8 @@ use epaint::mutex::RwLock; /// An error produced by egui-wgpu. #[derive(thiserror::Error, Debug)] pub enum WgpuError { - #[error("Failed to create wgpu adapter, no suitable adapter found.")] - NoSuitableAdapterFound, + #[error("Failed to create wgpu adapter, no suitable adapter found: {0}")] + NoSuitableAdapterFound(String), #[error("There was no valid format for the surface at all.")] NoSurfaceFormatsAvailable, @@ -67,8 +69,9 @@ pub struct RenderState { /// /// This is not available on web. /// On web, we always select WebGPU is available, then fall back to WebGL if not. + // TODO(gfx-rs/wgpu#6665): Remove layer of `Arc` here once we update to wgpu 24 #[cfg(not(target_arch = "wasm32"))] - pub available_adapters: Arc<[wgpu::Adapter]>, + pub available_adapters: Arc<[Arc]>, /// Wgpu device used for rendering, created from the adapter. pub device: Arc, @@ -83,6 +86,75 @@ pub struct RenderState { pub renderer: Arc>, } +async fn request_adapter( + instance: &wgpu::Instance, + power_preference: wgpu::PowerPreference, + compatible_surface: Option<&wgpu::Surface<'_>>, + _available_adapters: &[Arc], +) -> Result, WgpuError> { + profiling::function_scope!(); + + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference, + compatible_surface, + // We don't expose this as an option right now since it's fairly rarely useful: + // * only has an effect on native + // * fails if there's no software rasterizer available + // * can achieve the same with `native_adapter_selector` + force_fallback_adapter: false, + }) + .await + .ok_or_else(|| { + #[cfg(not(target_arch = "wasm32"))] + if _available_adapters.is_empty() { + log::info!("No wgpu adapters found"); + } else if _available_adapters.len() == 1 { + log::info!( + "The only available wgpu adapter was not suitable: {}", + adapter_info_summary(&_available_adapters[0].get_info()) + ); + } else { + log::info!( + "No suitable wgpu adapter found out of the {} available ones: {}", + _available_adapters.len(), + describe_adapters(_available_adapters) + ); + } + + WgpuError::NoSuitableAdapterFound("`request_adapters` returned `None`".to_owned()) + })?; + + #[cfg(target_arch = "wasm32")] + log::debug!( + "Picked wgpu adapter: {}", + adapter_info_summary(&adapter.get_info()) + ); + + #[cfg(not(target_arch = "wasm32"))] + if _available_adapters.len() == 1 { + log::debug!( + "Picked the only available wgpu adapter: {}", + adapter_info_summary(&adapter.get_info()) + ); + } else { + log::info!( + "There were {} available wgpu adapters: {}", + _available_adapters.len(), + describe_adapters(_available_adapters) + ); + log::debug!( + "Picked wgpu adapter: {}", + adapter_info_summary(&adapter.get_info()) + ); + } + + // 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)] + Ok(Arc::new(adapter)) +} + impl RenderState { /// Creates a new [`RenderState`], containing everything needed for drawing egui with wgpu. /// @@ -100,96 +172,73 @@ impl RenderState { // This is always an empty list on web. #[cfg(not(target_arch = "wasm32"))] - let available_adapters = instance.enumerate_adapters(wgpu::Backends::all()); + let available_adapters = { + let backends = if let WgpuSetup::CreateNew(create_new) = &config.wgpu_setup { + create_new.instance_descriptor.backends + } else { + wgpu::Backends::all() + }; + + instance + .enumerate_adapters(backends) + // TODO(gfx-rs/wgpu#6665): Remove layer of `Arc` here once we update to wgpu 24. + .into_iter() + .map(Arc::new) + .collect::>() + }; let (adapter, device, queue) = match config.wgpu_setup.clone() { - WgpuSetup::CreateNew { - supported_backends: _, + WgpuSetup::CreateNew(WgpuSetupCreateNew { + instance_descriptor: _, power_preference, + native_adapter_selector: _native_adapter_selector, device_descriptor, - } => { + trace_path, + }) => { let adapter = { - profiling::scope!("request_adapter"); - instance - .request_adapter(&wgpu::RequestAdapterOptions { + #[cfg(target_arch = "wasm32")] + { + request_adapter(instance, power_preference, compatible_surface, &[]).await + } + #[cfg(not(target_arch = "wasm32"))] + if let Some(native_adapter_selector) = _native_adapter_selector { + native_adapter_selector(&available_adapters, compatible_surface) + .map_err(WgpuError::NoSuitableAdapterFound) + } else { + request_adapter( + instance, power_preference, compatible_surface, - force_fallback_adapter: false, - }) + &available_adapters, + ) .await - .ok_or_else(|| { - #[cfg(not(target_arch = "wasm32"))] - if available_adapters.is_empty() { - log::info!("No wgpu adapters found"); - } else if available_adapters.len() == 1 { - log::info!( - "The only available wgpu adapter was not suitable: {}", - adapter_info_summary(&available_adapters[0].get_info()) - ); - } else { - log::info!( - "No suitable wgpu adapter found out of the {} available ones: {}", - available_adapters.len(), - describe_adapters(&available_adapters) - ); - } + } + }?; - WgpuError::NoSuitableAdapterFound - })? - }; - - #[cfg(target_arch = "wasm32")] - log::debug!( - "Picked wgpu adapter: {}", - adapter_info_summary(&adapter.get_info()) - ); - - #[cfg(not(target_arch = "wasm32"))] - if available_adapters.len() == 1 { - log::debug!( - "Picked the only available wgpu adapter: {}", - adapter_info_summary(&adapter.get_info()) - ); - } else { - log::info!( - "There were {} available wgpu adapters: {}", - available_adapters.len(), - describe_adapters(&available_adapters) - ); - log::debug!( - "Picked wgpu adapter: {}", - adapter_info_summary(&adapter.get_info()) - ); - } - - let trace_path = std::env::var("WGPU_TRACE"); let (device, queue) = { profiling::scope!("request_device"); adapter - .request_device( - &(*device_descriptor)(&adapter), - trace_path.ok().as_ref().map(std::path::Path::new), - ) + .request_device(&(*device_descriptor)(&adapter), trace_path.as_deref()) .await? }; // 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)] - (Arc::new(adapter), Arc::new(device), Arc::new(queue)) + (adapter, Arc::new(device), Arc::new(queue)) } - WgpuSetup::Existing { + WgpuSetup::Existing(WgpuSetupExisting { instance: _, adapter, device, queue, - } => (adapter, device, queue), + }) => (adapter, device, queue), }; let surface_formats = { profiling::scope!("get_capabilities"); compatible_surface.map_or_else( - || vec![TextureFormat::Rgba8Unorm], + || vec![wgpu::TextureFormat::Rgba8Unorm], |s| s.get_capabilities(&adapter).formats, ) }; @@ -219,20 +268,17 @@ impl RenderState { } #[cfg(not(target_arch = "wasm32"))] -fn describe_adapters(adapters: &[wgpu::Adapter]) -> String { +fn describe_adapters(adapters: &[Arc]) -> String { if adapters.is_empty() { "(none)".to_owned() } else if adapters.len() == 1 { adapter_info_summary(&adapters[0].get_info()) } else { - let mut list_string = String::new(); - for adapter in adapters { - if !list_string.is_empty() { - list_string += ", "; - } - list_string += &format!("{{{}}}", adapter_info_summary(&adapter.get_info())); - } - list_string + adapters + .iter() + .map(|a| format!("{{{}}}", adapter_info_summary(&a.get_info()))) + .collect::>() + .join(", ") } } @@ -245,62 +291,6 @@ pub enum SurfaceErrorAction { RecreateSurface, } -#[derive(Clone)] -pub enum WgpuSetup { - /// Construct a wgpu setup using some predefined settings & heuristics. - /// This is the default option. You can customize most behaviours overriding the - /// supported backends, power preferences, and device description. - /// - /// This can also be configured with the environment variables: - /// * `WGPU_BACKEND`: `vulkan`, `dx11`, `dx12`, `metal`, `opengl`, `webgpu` - /// * `WGPU_POWER_PREF`: `low`, `high` or `none` - CreateNew { - /// Backends that should be supported (wgpu will pick one of these). - /// - /// For instance, if you only want to support WebGL (and not WebGPU), - /// you can set this to [`wgpu::Backends::GL`]. - /// - /// By default on web, WebGPU will be used if available. - /// WebGL will only be used as a fallback, - /// and only if you have enabled the `webgl` feature of crate `wgpu`. - supported_backends: wgpu::Backends, - - /// Power preference for the adapter. - power_preference: wgpu::PowerPreference, - - /// Configuration passed on device request, given an adapter - device_descriptor: - Arc wgpu::DeviceDescriptor<'static> + Send + Sync>, - }, - - /// Run on an existing wgpu setup. - Existing { - instance: Arc, - adapter: Arc, - device: Arc, - queue: Arc, - }, -} - -impl std::fmt::Debug for WgpuSetup { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::CreateNew { - supported_backends, - power_preference, - device_descriptor: _, - } => f - .debug_struct("AdapterSelection::Standard") - .field("supported_backends", &supported_backends) - .field("power_preference", &power_preference) - .finish(), - Self::Existing { .. } => f - .debug_struct("AdapterSelection::Existing") - .finish_non_exhaustive(), - } - } -} - /// Configuration for using wgpu with eframe or the egui-wgpu winit feature. #[derive(Clone)] pub struct WgpuConfiguration { @@ -352,42 +342,8 @@ impl Default for WgpuConfiguration { fn default() -> Self { Self { present_mode: wgpu::PresentMode::AutoVsync, - desired_maximum_frame_latency: None, - - // By default, create a new wgpu setup. This will create a new instance, adapter, device and queue. - // This will create an instance for the supported backends (which can be configured by - // `WGPU_BACKEND`), and will pick an adapter by iterating adapters based on their power preference. The power - // preference can also be configured by `WGPU_POWER_PREF`. - wgpu_setup: WgpuSetup::CreateNew { - // Add GL backend, primarily because WebGPU is not stable enough yet. - // (note however, that the GL backend needs to be opted-in via the wgpu feature flag "webgl") - supported_backends: wgpu::util::backend_bits_from_env() - .unwrap_or(wgpu::Backends::PRIMARY | wgpu::Backends::GL), - - power_preference: wgpu::util::power_preference_from_env() - .unwrap_or(wgpu::PowerPreference::HighPerformance), - device_descriptor: Arc::new(|adapter| { - let base_limits = if adapter.get_info().backend == wgpu::Backend::Gl { - wgpu::Limits::downlevel_webgl2_defaults() - } else { - wgpu::Limits::default() - }; - - wgpu::DeviceDescriptor { - label: Some("egui wgpu device"), - required_features: wgpu::Features::default(), - required_limits: wgpu::Limits { - // When using a depth buffer, we have to be able to create a texture - // large enough for the entire surface, and we want to support 4k+ displays. - max_texture_dimension_2d: 8192, - ..base_limits - }, - memory_hints: wgpu::MemoryHints::default(), - } - }), - }, - + wgpu_setup: Default::default(), on_surface_error: Arc::new(|err| { if err == wgpu::SurfaceError::Outdated { // This error occurs when the app is minimized on Windows. @@ -468,8 +424,14 @@ pub fn adapter_info_summary(info: &wgpu::AdapterInfo) -> String { summary += &format!(", driver_info: {driver_info:?}"); } if *vendor != 0 { - // TODO(emilk): decode using https://github.com/gfx-rs/wgpu/blob/767ac03245ee937d3dc552edc13fe7ab0a860eec/wgpu-hal/src/auxil/mod.rs#L7 - summary += &format!(", vendor: 0x{vendor:04X}"); + #[cfg(not(target_arch = "wasm32"))] + { + summary += &format!(", vendor: {} (0x{vendor:04X})", parse_vendor_id(*vendor)); + } + #[cfg(target_arch = "wasm32")] + { + summary += &format!(", vendor: 0x{vendor:04X}"); + } } if *device != 0 { summary += &format!(", device: 0x{device:02X}"); @@ -477,3 +439,20 @@ pub fn adapter_info_summary(info: &wgpu::AdapterInfo) -> String { summary } + +/// Tries to parse the adapter's vendor ID to a human-readable string. +#[cfg(not(target_arch = "wasm32"))] +pub fn parse_vendor_id(vendor_id: u32) -> &'static str { + match vendor_id { + wgpu::hal::auxil::db::amd::VENDOR => "AMD", + wgpu::hal::auxil::db::apple::VENDOR => "Apple", + wgpu::hal::auxil::db::arm::VENDOR => "ARM", + wgpu::hal::auxil::db::broadcom::VENDOR => "Broadcom", + wgpu::hal::auxil::db::imgtec::VENDOR => "Imagination Technologies", + wgpu::hal::auxil::db::intel::VENDOR => "Intel", + wgpu::hal::auxil::db::mesa::VENDOR => "Mesa", + wgpu::hal::auxil::db::nvidia::VENDOR => "NVIDIA", + wgpu::hal::auxil::db::qualcomm::VENDOR => "Qualcomm", + _ => "Unknown", + } +} diff --git a/crates/egui-wgpu/src/setup.rs b/crates/egui-wgpu/src/setup.rs new file mode 100644 index 000000000..567c0d75f --- /dev/null +++ b/crates/egui-wgpu/src/setup.rs @@ -0,0 +1,237 @@ +use std::sync::Arc; + +#[derive(Clone)] +pub enum WgpuSetup { + /// Construct a wgpu setup using some predefined settings & heuristics. + /// This is the default option. You can customize most behaviours overriding the + /// supported backends, power preferences, and device description. + /// + /// By default can also be configured with various environment variables: + /// * `WGPU_BACKEND`: `vulkan`, `dx12`, `metal`, `opengl`, `webgpu` + /// * `WGPU_POWER_PREF`: `low`, `high` or `none` + /// * `WGPU_TRACE`: Path to a file to output a wgpu trace file. + /// + /// Each instance flag also comes with an environment variable (for details see [`wgpu::InstanceFlags`]): + /// * `WGPU_VALIDATION`: Enables validation (enabled by default in debug builds). + /// * `WGPU_DEBUG`: Generate debug information in shaders and objects (enabled by default in debug builds). + /// * `WGPU_ALLOW_UNDERLYING_NONCOMPLIANT_ADAPTER`: Whether wgpu should expose adapters that run on top of non-compliant adapters. + /// * `WGPU_GPU_BASED_VALIDATION`: Enable GPU-based validation. + CreateNew(WgpuSetupCreateNew), + + /// Run on an existing wgpu setup. + Existing(WgpuSetupExisting), +} + +impl Default for WgpuSetup { + fn default() -> Self { + Self::CreateNew(WgpuSetupCreateNew::default()) + } +} + +impl std::fmt::Debug for WgpuSetup { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::CreateNew(create_new) => f + .debug_tuple("WgpuSetup::CreateNew") + .field(create_new) + .finish(), + Self::Existing { .. } => f.debug_tuple("WgpuSetup::Existing").finish(), + } + } +} + +impl WgpuSetup { + /// Creates a new [`wgpu::Instance`] or clones the existing one. + /// + /// Does *not* store the wgpu instance, so calling this repeatedly may + /// create a new instance every time! + pub async fn new_instance(&self) -> Arc { + match self { + Self::CreateNew(create_new) => { + #[allow(unused_mut)] + let mut backends = create_new.instance_descriptor.backends; + + // Don't try WebGPU if we're not in a secure context. + #[cfg(target_arch = "wasm32")] + if backends.contains(wgpu::Backends::BROWSER_WEBGPU) { + let is_secure_context = + wgpu::web_sys::window().map_or(false, |w| w.is_secure_context()); + if !is_secure_context { + log::info!( + "WebGPU is only available in secure contexts, i.e. on HTTPS and on localhost." + ); + backends.remove(wgpu::Backends::BROWSER_WEBGPU); + } + } + + log::debug!("Creating wgpu instance with backends {:?}", backends); + + #[allow(clippy::arc_with_non_send_sync)] + Arc::new( + wgpu::util::new_instance_with_webgpu_detection(wgpu::InstanceDescriptor { + backends: create_new.instance_descriptor.backends, + flags: create_new.instance_descriptor.flags, + dx12_shader_compiler: create_new + .instance_descriptor + .dx12_shader_compiler + .clone(), + gles_minor_version: create_new.instance_descriptor.gles_minor_version, + }) + .await, + ) + } + Self::Existing(existing) => existing.instance.clone(), + } + } +} + +impl From for WgpuSetup { + fn from(create_new: WgpuSetupCreateNew) -> Self { + Self::CreateNew(create_new) + } +} + +impl From for WgpuSetup { + fn from(existing: WgpuSetupExisting) -> Self { + Self::Existing(existing) + } +} + +/// Method for selecting an adapter on native. +/// +/// This can be used for fully custom adapter selection. +/// If available, `wgpu::Surface` is passed to allow checking for surface compatibility. +// TODO(gfx-rs/wgpu#6665): Remove layer of `Arc` here. +pub type NativeAdapterSelectorMethod = Arc< + dyn Fn(&[Arc], Option<&wgpu::Surface<'_>>) -> Result, String> + + Send + + Sync, +>; + +/// Configuration for creating a new wgpu setup. +/// +/// Used for [`WgpuSetup::CreateNew`]. +pub struct WgpuSetupCreateNew { + /// Instance descriptor for creating a wgpu instance. + /// + /// The most important field is [`wgpu::InstanceDescriptor::backends`], which + /// controls which backends are supported (wgpu will pick one of these). + /// If you only want to support WebGL (and not WebGPU), + /// you can set this to [`wgpu::Backends::GL`]. + /// By default on web, WebGPU will be used if available. + /// WebGL will only be used as a fallback, + /// and only if you have enabled the `webgl` feature of crate `wgpu`. + pub instance_descriptor: wgpu::InstanceDescriptor, + + /// Power preference for the adapter if [`Self::native_adapter_selector`] is not set or targeting web. + pub power_preference: wgpu::PowerPreference, + + /// Optional selector for native adapters. + /// + /// This field has no effect when targeting web! + /// Otherwise, if set [`Self::power_preference`] is ignored and the adapter is instead selected by this method. + /// Note that [`Self::instance_descriptor`]'s [`wgpu::InstanceDescriptor::backends`] + /// are still used to filter the adapter enumeration in the first place. + /// + /// Defaults to `None`. + pub native_adapter_selector: Option, + + /// Configuration passed on device request, given an adapter + pub device_descriptor: + Arc wgpu::DeviceDescriptor<'static> + Send + Sync>, + + /// Option path to output a wgpu trace file. + /// + /// This only works if this feature is enabled in `wgpu-core`. + /// Does not work when running with WebGPU. + /// Defaults to the path set in the `WGPU_TRACE` environment variable. + pub trace_path: Option, +} + +impl Clone for WgpuSetupCreateNew { + fn clone(&self) -> Self { + Self { + // TODO(gfx-rs/wgpu/#6849): use .clone() + instance_descriptor: wgpu::InstanceDescriptor { + backends: self.instance_descriptor.backends, + flags: self.instance_descriptor.flags, + dx12_shader_compiler: self.instance_descriptor.dx12_shader_compiler.clone(), + gles_minor_version: self.instance_descriptor.gles_minor_version, + }, + power_preference: self.power_preference, + native_adapter_selector: self.native_adapter_selector.clone(), + device_descriptor: self.device_descriptor.clone(), + trace_path: self.trace_path.clone(), + } + } +} + +impl std::fmt::Debug for WgpuSetupCreateNew { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WgpuSetupCreateNew") + .field("instance_descriptor", &self.instance_descriptor) + .field("power_preference", &self.power_preference) + .field( + "native_adapter_selector", + &self.native_adapter_selector.is_some(), + ) + .field("trace_path", &self.trace_path) + .finish() + } +} + +impl Default for WgpuSetupCreateNew { + fn default() -> Self { + Self { + instance_descriptor: wgpu::InstanceDescriptor { + // Add GL backend, primarily because WebGPU is not stable enough yet. + // (note however, that the GL backend needs to be opted-in via the wgpu feature flag "webgl") + backends: wgpu::util::backend_bits_from_env() + .unwrap_or(wgpu::Backends::PRIMARY | wgpu::Backends::GL), + flags: wgpu::InstanceFlags::from_build_config().with_env(), + dx12_shader_compiler: wgpu::Dx12Compiler::default(), + gles_minor_version: wgpu::Gles3MinorVersion::Automatic, + }, + + power_preference: wgpu::util::power_preference_from_env() + .unwrap_or(wgpu::PowerPreference::HighPerformance), + + native_adapter_selector: None, + + device_descriptor: Arc::new(|adapter| { + let base_limits = if adapter.get_info().backend == wgpu::Backend::Gl { + wgpu::Limits::downlevel_webgl2_defaults() + } else { + wgpu::Limits::default() + }; + + wgpu::DeviceDescriptor { + label: Some("egui wgpu device"), + required_features: wgpu::Features::default(), + required_limits: wgpu::Limits { + // When using a depth buffer, we have to be able to create a texture + // large enough for the entire surface, and we want to support 4k+ displays. + max_texture_dimension_2d: 8192, + ..base_limits + }, + memory_hints: wgpu::MemoryHints::default(), + } + }), + + trace_path: std::env::var("WGPU_TRACE") + .ok() + .map(std::path::PathBuf::from), + } + } +} + +/// Configuration for using an existing wgpu setup. +/// +/// Used for [`WgpuSetup::Existing`]. +#[derive(Clone)] +pub struct WgpuSetupExisting { + pub instance: Arc, + pub adapter: Arc, + pub device: Arc, + pub queue: Arc, +} diff --git a/crates/egui-wgpu/src/winit.rs b/crates/egui-wgpu/src/winit.rs index 19be186f2..86f7bd84d 100644 --- a/crates/egui-wgpu/src/winit.rs +++ b/crates/egui-wgpu/src/winit.rs @@ -51,7 +51,7 @@ impl Painter { /// [`set_window()`](Self::set_window) once you have /// a [`winit::window::Window`] with a valid `.raw_window_handle()` /// associated. - pub fn new( + pub async fn new( context: Context, configuration: WgpuConfiguration, msaa_samples: u32, @@ -59,17 +59,8 @@ impl Painter { support_transparent_backbuffer: bool, dithering: bool, ) -> Self { - let instance = match &configuration.wgpu_setup { - crate::WgpuSetup::CreateNew { - supported_backends, .. - } => Arc::new(wgpu::Instance::new(wgpu::InstanceDescriptor { - backends: *supported_backends, - ..Default::default() - })), - crate::WgpuSetup::Existing { instance, .. } => instance.clone(), - }; - let (capture_tx, capture_rx) = capture_channel(); + let instance = configuration.wgpu_setup.new_instance().await; Self { context, diff --git a/crates/egui_demo_lib/Cargo.toml b/crates/egui_demo_lib/Cargo.toml index b494e18a9..047ed3059 100644 --- a/crates/egui_demo_lib/Cargo.toml +++ b/crates/egui_demo_lib/Cargo.toml @@ -57,7 +57,6 @@ serde = { workspace = true, optional = true } [dev-dependencies] criterion.workspace = true egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] } -wgpu = { workspace = true, features = ["metal"] } egui = { workspace = true, features = ["default_fonts"] } [[bench]] diff --git a/crates/egui_kittest/Cargo.toml b/crates/egui_kittest/Cargo.toml index 4f52d4aef..1e4af19e6 100644 --- a/crates/egui_kittest/Cargo.toml +++ b/crates/egui_kittest/Cargo.toml @@ -20,7 +20,13 @@ include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"] [features] # Adds a wgpu-based test renderer. -wgpu = ["dep:egui-wgpu", "dep:pollster", "dep:image", "eframe?/wgpu"] +wgpu = [ + "dep:egui-wgpu", + "dep:pollster", + "dep:image", + "dep:wgpu", + "eframe?/wgpu", +] # Adds a dify-based image snapshot utility. snapshot = ["dep:dify", "dep:image", "image/png"] @@ -38,6 +44,8 @@ eframe = { workspace = true, optional = true } egui-wgpu = { workspace = true, optional = true } pollster = { workspace = true, optional = true } image = { workspace = true, optional = true } +# Enable DX12 because it always comes with a software rasterizer. +wgpu = { workspace = true, features = ["metal", "dx12"], optional = true } # snapshot dependencies dify = { workspace = true, optional = true } @@ -48,7 +56,6 @@ document-features = { workspace = true, optional = true } [dev-dependencies] egui = { workspace = true, features = ["default_fonts"] } image = { workspace = true, features = ["png"] } -wgpu = { workspace = true, features = ["metal"] } [lints] workspace = true diff --git a/crates/egui_kittest/src/wgpu.rs b/crates/egui_kittest/src/wgpu.rs index 3f229763f..e7734c958 100644 --- a/crates/egui_kittest/src/wgpu.rs +++ b/crates/egui_kittest/src/wgpu.rs @@ -1,27 +1,56 @@ -use crate::texture_to_image::texture_to_image; -use eframe::epaint::TextureId; -use egui::TexturesDelta; -use egui_wgpu::wgpu::{Backends, StoreOp, TextureFormat}; -use egui_wgpu::{wgpu, RenderState, ScreenDescriptor, WgpuSetup}; -use image::RgbaImage; use std::iter::once; use std::sync::Arc; -use wgpu::Maintain; -// TODO(#5506): Replace this with the setup from https://github.com/emilk/egui/pull/5506 +use egui::TexturesDelta; +use egui_wgpu::{wgpu, RenderState, ScreenDescriptor, WgpuSetup}; +use image::RgbaImage; + +use crate::texture_to_image::texture_to_image; + +/// Default wgpu setup used for the wgpu renderer. pub fn default_wgpu_setup() -> egui_wgpu::WgpuSetup { - egui_wgpu::WgpuSetup::CreateNew { - supported_backends: Backends::all(), - device_descriptor: Arc::new(|_| wgpu::DeviceDescriptor::default()), - power_preference: wgpu::PowerPreference::default(), - } + let mut setup = egui_wgpu::WgpuSetupCreateNew::default(); + + // WebGPU not supported yet since we rely on blocking screenshots. + setup + .instance_descriptor + .backends + .remove(wgpu::Backends::BROWSER_WEBGPU); + + // Prefer software rasterizers. + setup.native_adapter_selector = Some(Arc::new(|adapters, _surface| { + let mut adapters = adapters.iter().collect::>(); + + // Adapters are already sorted by preferred backend by wgpu, but let's be explicit. + adapters.sort_by_key(|a| match a.get_info().backend { + wgpu::Backend::Metal => 0, + wgpu::Backend::Vulkan => 1, + wgpu::Backend::Dx12 => 2, + wgpu::Backend::Gl => 4, + wgpu::Backend::BrowserWebGpu => 6, + wgpu::Backend::Empty => 7, + }); + + // Prefer CPU adapters, otherwise if we can't, prefer discrete GPU over integrated GPU. + adapters.sort_by_key(|a| match a.get_info().device_type { + wgpu::DeviceType::Cpu => 0, // CPU is the best for our purposes! + wgpu::DeviceType::DiscreteGpu => 1, + wgpu::DeviceType::Other + | wgpu::DeviceType::IntegratedGpu + | wgpu::DeviceType::VirtualGpu => 2, + }); + + adapters + .first() + .map(|a| (*a).clone()) + .ok_or("No adapter found".to_owned()) + })); + + egui_wgpu::WgpuSetup::CreateNew(setup) } pub fn create_render_state(setup: WgpuSetup) -> egui_wgpu::RenderState { - let instance = match &setup { - WgpuSetup::Existing { instance, .. } => instance.clone(), - WgpuSetup::CreateNew { .. } => Default::default(), - }; + let instance = pollster::block_on(setup.new_instance()); pollster::block_on(egui_wgpu::RenderState::create( &egui_wgpu::WgpuConfiguration { @@ -72,7 +101,7 @@ impl WgpuTestRenderer { render_state .renderer .read() - .texture(&TextureId::Managed(0)) + .texture(&egui::epaint::TextureId::Managed(0)) .is_none(), "The RenderState passed in has been used before, pass in a fresh RenderState instead." ); @@ -143,7 +172,7 @@ impl crate::TestRenderer for WgpuTestRenderer { mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, - format: TextureFormat::Rgba8Unorm, + format: self.render_state.target_format, usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, view_formats: &[], }); @@ -159,12 +188,10 @@ impl crate::TestRenderer for WgpuTestRenderer { resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), - store: StoreOp::Store, + store: wgpu::StoreOp::Store, }, })], - depth_stencil_attachment: None, - occlusion_query_set: None, - timestamp_writes: None, + ..Default::default() }) .forget_lifetime(); @@ -175,7 +202,7 @@ impl crate::TestRenderer for WgpuTestRenderer { .queue .submit(user_buffers.into_iter().chain(once(encoder.finish()))); - self.render_state.device.poll(Maintain::Wait); + self.render_state.device.poll(wgpu::Maintain::Wait); Ok(texture_to_image( &self.render_state.device, From f0d7c74e838b8e8920a22e7515990fbe057ec218 Mon Sep 17 00:00:00 2001 From: Andreas Reich Date: Wed, 8 Jan 2025 15:45:46 +0100 Subject: [PATCH 013/132] `response` module is now public, allowing access to `egui::response::Flags` (#5592) --- crates/egui/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index fa3cb8585..85e12cb8a 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -427,7 +427,7 @@ pub mod os; mod painter; mod pass_state; pub(crate) mod placer; -mod response; +pub mod response; mod sense; pub mod style; pub mod text_selection; From 1339639706228dfbf8955f9b171b82af699688d1 Mon Sep 17 00:00:00 2001 From: Andreas Reich Date: Sat, 11 Jan 2025 18:05:57 +0100 Subject: [PATCH 014/132] Fix Windows clippy issues (#5593) These have been a lil bit annoying when running `cargo clippy --all-features --all-targets` on windows --- crates/eframe/src/native/file_storage.rs | 43 ++++++++++++++---------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/crates/eframe/src/native/file_storage.rs b/crates/eframe/src/native/file_storage.rs index 346c46b42..3cb338517 100644 --- a/crates/eframe/src/native/file_storage.rs +++ b/crates/eframe/src/native/file_storage.rs @@ -58,27 +58,34 @@ fn roaming_appdata() -> Option { extern "C" { fn wcslen(buf: *const u16) -> usize; } - unsafe { - let mut path = ptr::null_mut(); - match SHGetKnownFolderPath( + let mut path_raw = ptr::null_mut(); + + // SAFETY: SHGetKnownFolderPath allocates for us, we don't pass any pointers to it. + // See https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shgetknownfolderpath + let result = unsafe { + SHGetKnownFolderPath( &FOLDERID_RoamingAppData, KF_FLAG_DONT_VERIFY as u32, std::ptr::null_mut(), - &mut path, - ) { - S_OK => { - let path_slice = slice::from_raw_parts(path, wcslen(path)); - let s = OsString::from_wide(&path_slice); - CoTaskMemFree(path.cast()); - Some(PathBuf::from(s)) - } - _ => { - // Free any allocated memory even on failure. A null ptr is a no-op for `CoTaskMemFree`. - CoTaskMemFree(path.cast()); - None - } - } - } + &mut path_raw, + ) + }; + + let path = if result == S_OK { + // SAFETY: SHGetKnownFolderPath indicated success and is supposed to allocate a nullterminated string for us. + let path_slice = unsafe { slice::from_raw_parts(path_raw, wcslen(path_raw)) }; + Some(PathBuf::from(OsString::from_wide(path_slice))) + } else { + None + }; + + // SAFETY: + // This memory got allocated by SHGetKnownFolderPath, we didn't touch anything in the process. + // A null ptr is a no-op for `CoTaskMemFree`, so in case this failed we're still good. + // https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-cotaskmemfree + unsafe { CoTaskMemFree(path_raw.cast()) }; + + path } #[cfg(any(not(windows), target_vendor = "uwp"))] From 164f56f5545de3b20c18de392f34c607e9698cd6 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 13 Jan 2025 08:29:13 +0100 Subject: [PATCH 015/132] Fix some clippy issues found by 1.84.0 (#5603) --- crates/eframe/src/epi.rs | 14 -------------- crates/eframe/src/native/glow_integration.rs | 4 ++-- crates/eframe/src/native/wgpu_integration.rs | 4 ++-- crates/egui-wgpu/src/setup.rs | 2 +- crates/egui/src/containers/collapsing_header.rs | 2 +- crates/egui/src/containers/scroll_area.rs | 2 +- crates/egui/src/containers/window.rs | 2 +- crates/egui/src/context.rs | 12 ++++++------ crates/egui/src/data/input.rs | 2 +- crates/egui/src/data/user_data.rs | 2 +- crates/egui/src/drag_and_drop.rs | 2 +- crates/egui/src/input_state/mod.rs | 2 +- crates/egui/src/menu.rs | 2 +- crates/egui/src/response.rs | 2 +- .../src/text_selection/label_text_selection.rs | 2 +- crates/egui/src/ui_stack.rs | 6 +++--- crates/egui/src/widgets/checkbox.rs | 2 +- crates/egui/src/widgets/drag_value.rs | 2 +- crates/egui/src/widgets/image.rs | 6 +++--- crates/egui/src/widgets/image_button.rs | 2 +- crates/egui/src/widgets/label.rs | 2 +- crates/egui/src/widgets/slider.rs | 4 ++-- crates/egui/src/widgets/text_edit/builder.rs | 8 ++++---- crates/egui/src/widgets/text_edit/text_buffer.rs | 4 ++-- crates/egui_demo_lib/src/demo/text_edit.rs | 4 +--- crates/egui_extras/src/datepicker/button.rs | 2 +- crates/egui_extras/src/datepicker/popup.rs | 2 +- crates/egui_extras/src/strip.rs | 4 ++-- crates/egui_extras/src/table.rs | 14 +++++++------- crates/egui_kittest/src/app_kind.rs | 2 +- crates/egui_kittest/src/lib.rs | 4 ++-- crates/epaint/src/mutex.rs | 10 +++++----- crates/epaint/src/text/text_layout.rs | 12 ++++++------ 33 files changed, 65 insertions(+), 81 deletions(-) diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index 7a7e92244..fb589fe2e 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -878,20 +878,6 @@ pub trait Storage { fn flush(&mut self); } -/// Stores nothing. -#[derive(Clone, Default)] -pub(crate) struct DummyStorage {} - -impl Storage for DummyStorage { - fn get_string(&self, _key: &str) -> Option { - None - } - - fn set_string(&mut self, _key: &str, _value: String) {} - - fn flush(&mut self) {} -} - /// Get and deserialize the [RON](https://github.com/ron-rs/ron) stored at the given key. #[cfg(feature = "ron")] pub fn get_value(storage: &dyn Storage, key: &str) -> Option { diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index f17c6ad50..8e9323670 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -344,7 +344,7 @@ impl<'app> GlowWinitApp<'app> { } } -impl<'app> WinitApp for GlowWinitApp<'app> { +impl WinitApp for GlowWinitApp<'_> { fn egui_ctx(&self) -> Option<&egui::Context> { self.running.as_ref().map(|r| &r.integration.egui_ctx) } @@ -479,7 +479,7 @@ impl<'app> WinitApp for GlowWinitApp<'app> { } } -impl<'app> GlowWinitRunning<'app> { +impl GlowWinitRunning<'_> { fn run_ui_and_paint( &mut self, event_loop: &ActiveEventLoop, diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index dc12c52a9..f93386f84 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -323,7 +323,7 @@ impl<'app> WgpuWinitApp<'app> { } } -impl<'app> WinitApp for WgpuWinitApp<'app> { +impl WinitApp for WgpuWinitApp<'_> { fn egui_ctx(&self) -> Option<&egui::Context> { self.running.as_ref().map(|r| &r.integration.egui_ctx) } @@ -487,7 +487,7 @@ impl<'app> WinitApp for WgpuWinitApp<'app> { } } -impl<'app> WgpuWinitRunning<'app> { +impl WgpuWinitRunning<'_> { fn save_and_destroy(&mut self) { profiling::function_scope!(); diff --git a/crates/egui-wgpu/src/setup.rs b/crates/egui-wgpu/src/setup.rs index 567c0d75f..876b0c8e3 100644 --- a/crates/egui-wgpu/src/setup.rs +++ b/crates/egui-wgpu/src/setup.rs @@ -55,7 +55,7 @@ impl WgpuSetup { #[cfg(target_arch = "wasm32")] if backends.contains(wgpu::Backends::BROWSER_WEBGPU) { let is_secure_context = - wgpu::web_sys::window().map_or(false, |w| w.is_secure_context()); + wgpu::web_sys::window().is_some_and(|w| w.is_secure_context()); if !is_secure_context { log::info!( "WebGPU is only available in secure contexts, i.e. on HTTPS and on localhost." diff --git a/crates/egui/src/containers/collapsing_header.rs b/crates/egui/src/containers/collapsing_header.rs index 121daeac4..c5fa812d8 100644 --- a/crates/egui/src/containers/collapsing_header.rs +++ b/crates/egui/src/containers/collapsing_header.rs @@ -283,7 +283,7 @@ pub struct HeaderResponse<'ui, HeaderRet> { header_response: InnerResponse, } -impl<'ui, HeaderRet> HeaderResponse<'ui, HeaderRet> { +impl HeaderResponse<'_, HeaderRet> { pub fn is_open(&self) -> bool { self.state.is_open() } diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index 3c14a02e5..12702fb2f 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -1184,7 +1184,7 @@ impl Prepared { && ui.input(|i| { i.pointer .latest_pos() - .map_or(false, |p| handle_rect.contains(p)) + .is_some_and(|p| handle_rect.contains(p)) }); let visuals = ui.visuals(); if response.is_pointer_button_down_on() { diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index f1890feed..215f6322d 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -415,7 +415,7 @@ impl<'open> Window<'open> { } } -impl<'open> Window<'open> { +impl Window<'_> { /// Returns `None` if the window is not open (if [`Window::open`] was called with `&mut false`). /// Returns `Some(InnerResponse { inner: None })` if the window is collapsed. #[inline] diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 95ae19656..44c807b06 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -211,14 +211,14 @@ impl ContextImpl { fn requested_immediate_repaint_prev_pass(&self, viewport_id: &ViewportId) -> bool { self.viewports .get(viewport_id) - .map_or(false, |v| v.repaint.requested_immediate_repaint_prev_pass()) + .is_some_and(|v| v.repaint.requested_immediate_repaint_prev_pass()) } #[must_use] fn has_requested_repaint(&self, viewport_id: &ViewportId) -> bool { - self.viewports.get(viewport_id).map_or(false, |v| { - 0 < v.repaint.outstanding || v.repaint.repaint_delay < Duration::MAX - }) + self.viewports + .get(viewport_id) + .is_some_and(|v| 0 < v.repaint.outstanding || v.repaint.repaint_delay < Duration::MAX) } } @@ -1214,7 +1214,7 @@ impl Context { #[deprecated = "Use Response.contains_pointer or Context::read_response instead"] pub fn widget_contains_pointer(&self, id: Id) -> bool { self.read_response(id) - .map_or(false, |response| response.contains_pointer()) + .is_some_and(|response| response.contains_pointer()) } /// Do all interaction for an existing widget, without (re-)registering it. @@ -2632,7 +2632,7 @@ impl Context { pub fn is_context_menu_open(&self) -> bool { self.data(|d| { d.get_temp::(menu::CONTEXT_MENU_ID_STR.into()) - .map_or(false, |state| state.has_root()) + .is_some_and(|state| state.has_root()) }) } } diff --git a/crates/egui/src/data/input.rs b/crates/egui/src/data/input.rs index 7987ea612..0bced4270 100644 --- a/crates/egui/src/data/input.rs +++ b/crates/egui/src/data/input.rs @@ -986,7 +986,7 @@ impl ModifierNames<'static> { }; } -impl<'a> ModifierNames<'a> { +impl ModifierNames<'_> { pub fn format(&self, modifiers: &Modifiers, is_mac: bool) -> String { let mut s = String::new(); diff --git a/crates/egui/src/data/user_data.rs b/crates/egui/src/data/user_data.rs index 20bf5e1a1..12d90adf7 100644 --- a/crates/egui/src/data/user_data.rs +++ b/crates/egui/src/data/user_data.rs @@ -54,7 +54,7 @@ impl<'de> serde::Deserialize<'de> for UserData { { struct UserDataVisitor; - impl<'de> serde::de::Visitor<'de> for UserDataVisitor { + impl serde::de::Visitor<'_> for UserDataVisitor { type Value = UserData; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { diff --git a/crates/egui/src/drag_and_drop.rs b/crates/egui/src/drag_and_drop.rs index fc9f29f5e..7f0c01b71 100644 --- a/crates/egui/src/drag_and_drop.rs +++ b/crates/egui/src/drag_and_drop.rs @@ -141,7 +141,7 @@ impl DragAndDrop { pub fn has_any_payload(ctx: &Context) -> bool { ctx.data(|data| { let state = data.get_temp::(Id::NULL); - state.map_or(false, |state| state.payload.is_some()) + state.is_some_and(|state| state.payload.is_some()) }) } } diff --git a/crates/egui/src/input_state/mod.rs b/crates/egui/src/input_state/mod.rs index 048e880e3..751d635f1 100644 --- a/crates/egui/src/input_state/mod.rs +++ b/crates/egui/src/input_state/mod.rs @@ -1303,7 +1303,7 @@ impl PointerState { self.started_decidedly_dragging && !self.has_moved_too_much_for_a_click && self.button_down(PointerButton::Primary) - && self.press_start_time.map_or(false, |press_start_time| { + && self.press_start_time.is_some_and(|press_start_time| { self.time - press_start_time > self.input_options.max_click_duration }) } diff --git a/crates/egui/src/menu.rs b/crates/egui/src/menu.rs index 166e18da0..301c69dd3 100644 --- a/crates/egui/src/menu.rs +++ b/crates/egui/src/menu.rs @@ -679,7 +679,7 @@ impl MenuState { || self .sub_menu .as_ref() - .map_or(false, |(_, sub)| sub.read().area_contains(pos)) + .is_some_and(|(_, sub)| sub.read().area_contains(pos)) } fn next_entry_index(&mut self) -> usize { diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 73e775168..f5861f4f1 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -618,7 +618,7 @@ impl Response { let any_open_popups = self.ctx.prev_pass_state(|fs| { fs.layers .get(&self.layer_id) - .map_or(false, |layer| !layer.open_popups.is_empty()) + .is_some_and(|layer| !layer.open_popups.is_empty()) }); if any_open_popups { // Hide tooltips if the user opens a popup (menu, combo-box, etc) in the same layer. diff --git a/crates/egui/src/text_selection/label_text_selection.rs b/crates/egui/src/text_selection/label_text_selection.rs index e19a40e49..ea5f3c9c6 100644 --- a/crates/egui/src/text_selection/label_text_selection.rs +++ b/crates/egui/src/text_selection/label_text_selection.rs @@ -263,7 +263,7 @@ impl LabelSelectionState { let new_text_starts_with_space_or_punctuation = new_text .chars() .next() - .map_or(false, |c| c.is_whitespace() || c.is_ascii_punctuation()); + .is_some_and(|c| c.is_whitespace() || c.is_ascii_punctuation()); if existing_ends_with_space == Some(false) && !new_text_starts_with_space_or_punctuation { diff --git a/crates/egui/src/ui_stack.rs b/crates/egui/src/ui_stack.rs index 7aa131bec..550f6b182 100644 --- a/crates/egui/src/ui_stack.rs +++ b/crates/egui/src/ui_stack.rs @@ -229,13 +229,13 @@ impl UiStack { /// Is this [`crate::Ui`] a panel? #[inline] pub fn is_panel_ui(&self) -> bool { - self.kind().map_or(false, |kind| kind.is_panel()) + self.kind().is_some_and(|kind| kind.is_panel()) } /// Is this [`crate::Ui`] an [`crate::Area`]? #[inline] pub fn is_area_ui(&self) -> bool { - self.kind().map_or(false, |kind| kind.is_area()) + self.kind().is_some_and(|kind| kind.is_area()) } /// Is this a root [`crate::Ui`], i.e. created with [`crate::Ui::new()`]? @@ -285,4 +285,4 @@ impl<'a> Iterator for UiStackIterator<'a> { } } -impl<'a> FusedIterator for UiStackIterator<'a> {} +impl FusedIterator for UiStackIterator<'_> {} diff --git a/crates/egui/src/widgets/checkbox.rs b/crates/egui/src/widgets/checkbox.rs index f8b31430e..96cc3d22b 100644 --- a/crates/egui/src/widgets/checkbox.rs +++ b/crates/egui/src/widgets/checkbox.rs @@ -47,7 +47,7 @@ impl<'a> Checkbox<'a> { } } -impl<'a> Widget for Checkbox<'a> { +impl Widget for Checkbox<'_> { fn ui(self, ui: &mut Ui) -> Response { let Checkbox { checked, diff --git a/crates/egui/src/widgets/drag_value.rs b/crates/egui/src/widgets/drag_value.rs index 175fdcc5a..846ab72f0 100644 --- a/crates/egui/src/widgets/drag_value.rs +++ b/crates/egui/src/widgets/drag_value.rs @@ -426,7 +426,7 @@ impl<'a> DragValue<'a> { } } -impl<'a> Widget for DragValue<'a> { +impl Widget for DragValue<'_> { fn ui(self, ui: &mut Ui) -> Response { let Self { mut get_set_value, diff --git a/crates/egui/src/widgets/image.rs b/crates/egui/src/widgets/image.rs index d1976a12c..2290eabbd 100644 --- a/crates/egui/src/widgets/image.rs +++ b/crates/egui/src/widgets/image.rs @@ -372,7 +372,7 @@ impl<'a> Image<'a> { } } -impl<'a> Widget for Image<'a> { +impl Widget for Image<'_> { fn ui(self, ui: &mut Ui) -> Response { let tlr = self.load_for_size(ui.ctx(), ui.available_size()); let original_image_size = tlr.as_ref().ok().and_then(|t| t.size()); @@ -568,7 +568,7 @@ pub enum ImageSource<'a> { }, } -impl<'a> std::fmt::Debug for ImageSource<'a> { +impl std::fmt::Debug for ImageSource<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ImageSource::Bytes { uri, .. } | ImageSource::Uri(uri) => uri.as_ref().fmt(f), @@ -577,7 +577,7 @@ impl<'a> std::fmt::Debug for ImageSource<'a> { } } -impl<'a> ImageSource<'a> { +impl ImageSource<'_> { /// Size of the texture, if known. #[inline] pub fn texture_size(&self) -> Option { diff --git a/crates/egui/src/widgets/image_button.rs b/crates/egui/src/widgets/image_button.rs index fdcae898a..3a03ab295 100644 --- a/crates/egui/src/widgets/image_button.rs +++ b/crates/egui/src/widgets/image_button.rs @@ -71,7 +71,7 @@ impl<'a> ImageButton<'a> { } } -impl<'a> Widget for ImageButton<'a> { +impl Widget for ImageButton<'_> { fn ui(self, ui: &mut Ui) -> Response { let padding = if self.frame { // so we can see that it is a button: diff --git a/crates/egui/src/widgets/label.rs b/crates/egui/src/widgets/label.rs index eb3e3840e..67dc196bc 100644 --- a/crates/egui/src/widgets/label.rs +++ b/crates/egui/src/widgets/label.rs @@ -244,7 +244,7 @@ impl Widget for Label { // Interactive = the uses asked to sense interaction. // We DON'T want to have the color respond just because the text is selectable; // the cursor is enough to communicate that. - let interactive = self.sense.map_or(false, |sense| sense != Sense::hover()); + let interactive = self.sense.is_some_and(|sense| sense != Sense::hover()); let selectable = self.selectable; diff --git a/crates/egui/src/widgets/slider.rs b/crates/egui/src/widgets/slider.rs index acf0359a8..d535a5c75 100644 --- a/crates/egui/src/widgets/slider.rs +++ b/crates/egui/src/widgets/slider.rs @@ -643,7 +643,7 @@ impl<'a> Slider<'a> { } } -impl<'a> Slider<'a> { +impl Slider<'_> { /// Just the slider, no text fn allocate_slider_space(&self, ui: &mut Ui, thickness: f32) -> Response { let desired_size = match self.orientation { @@ -1015,7 +1015,7 @@ impl<'a> Slider<'a> { } } -impl<'a> Widget for Slider<'a> { +impl Widget for Slider<'_> { fn ui(mut self, ui: &mut Ui) -> Response { let inner_response = match self.orientation { SliderOrientation::Horizontal => ui.horizontal(|ui| self.add_contents(ui)), diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 5c3b080f1..2619035a4 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -88,11 +88,11 @@ pub struct TextEdit<'t> { background_color: Option, } -impl<'t> WidgetWithState for TextEdit<'t> { +impl WidgetWithState for TextEdit<'_> { type State = TextEditState; } -impl<'t> TextEdit<'t> { +impl TextEdit<'_> { pub fn load_state(ctx: &Context, id: Id) -> Option { TextEditState::load(ctx, id) } @@ -394,13 +394,13 @@ impl<'t> TextEdit<'t> { // ---------------------------------------------------------------------------- -impl<'t> Widget for TextEdit<'t> { +impl Widget for TextEdit<'_> { fn ui(self, ui: &mut Ui) -> Response { self.show(ui).response } } -impl<'t> TextEdit<'t> { +impl TextEdit<'_> { /// Show the [`TextEdit`], returning a rich [`TextEditOutput`]. /// /// ``` diff --git a/crates/egui/src/widgets/text_edit/text_buffer.rs b/crates/egui/src/widgets/text_edit/text_buffer.rs index ea3992c57..31b746324 100644 --- a/crates/egui/src/widgets/text_edit/text_buffer.rs +++ b/crates/egui/src/widgets/text_edit/text_buffer.rs @@ -228,7 +228,7 @@ impl TextBuffer for String { } } -impl<'a> TextBuffer for Cow<'a, str> { +impl TextBuffer for Cow<'_, str> { fn is_mutable(&self) -> bool { true } @@ -259,7 +259,7 @@ impl<'a> TextBuffer for Cow<'a, str> { } /// Immutable view of a `&str`! -impl<'a> TextBuffer for &'a str { +impl TextBuffer for &str { fn is_mutable(&self) -> bool { false } diff --git a/crates/egui_demo_lib/src/demo/text_edit.rs b/crates/egui_demo_lib/src/demo/text_edit.rs index 5ce5c93be..06911957f 100644 --- a/crates/egui_demo_lib/src/demo/text_edit.rs +++ b/crates/egui_demo_lib/src/demo/text_edit.rs @@ -58,9 +58,7 @@ impl crate::View for TextEditDemo { } }); - let anything_selected = output - .cursor_range - .map_or(false, |cursor| !cursor.is_empty()); + let anything_selected = output.cursor_range.is_some_and(|cursor| !cursor.is_empty()); ui.add_enabled( anything_selected, diff --git a/crates/egui_extras/src/datepicker/button.rs b/crates/egui_extras/src/datepicker/button.rs index 81a97c12a..601c16f67 100644 --- a/crates/egui_extras/src/datepicker/button.rs +++ b/crates/egui_extras/src/datepicker/button.rs @@ -103,7 +103,7 @@ impl<'a> DatePickerButton<'a> { } } -impl<'a> Widget for DatePickerButton<'a> { +impl Widget for DatePickerButton<'_> { fn ui(self, ui: &mut Ui) -> egui::Response { let id = ui.make_persistent_id(self.id_salt); let mut button_state = ui diff --git a/crates/egui_extras/src/datepicker/popup.rs b/crates/egui_extras/src/datepicker/popup.rs index 230b6e0b3..8bf2f1a12 100644 --- a/crates/egui_extras/src/datepicker/popup.rs +++ b/crates/egui_extras/src/datepicker/popup.rs @@ -37,7 +37,7 @@ pub(crate) struct DatePickerPopup<'a> { pub highlight_weekends: bool, } -impl<'a> DatePickerPopup<'a> { +impl DatePickerPopup<'_> { /// Returns `true` if user pressed `Save` button. pub fn draw(&mut self, ui: &mut Ui) -> bool { let id = ui.make_persistent_id("date_picker"); diff --git a/crates/egui_extras/src/strip.rs b/crates/egui_extras/src/strip.rs index 9087f673b..00fc65774 100644 --- a/crates/egui_extras/src/strip.rs +++ b/crates/egui_extras/src/strip.rs @@ -166,7 +166,7 @@ pub struct Strip<'a, 'b> { size_index: usize, } -impl<'a, 'b> Strip<'a, 'b> { +impl Strip<'_, '_> { #[cfg_attr(debug_assertions, track_caller)] fn next_cell_size(&mut self) -> (CellSize, CellSize) { let size = if let Some(size) = self.sizes.get(self.size_index) { @@ -219,7 +219,7 @@ impl<'a, 'b> Strip<'a, 'b> { } } -impl<'a, 'b> Drop for Strip<'a, 'b> { +impl Drop for Strip<'_, '_> { fn drop(&mut self) { while self.size_index < self.sizes.len() { self.empty(); diff --git a/crates/egui_extras/src/table.rs b/crates/egui_extras/src/table.rs index ec8899030..0a6fe690e 100644 --- a/crates/egui_extras/src/table.rs +++ b/crates/egui_extras/src/table.rs @@ -698,7 +698,7 @@ pub struct Table<'a> { sense: egui::Sense, } -impl<'a> Table<'a> { +impl Table<'_> { /// Access the contained [`egui::Ui`]. /// /// You can use this to e.g. modify the [`egui::Style`] with [`egui::Ui::style_mut`]. @@ -1228,7 +1228,7 @@ impl<'a> TableBody<'a> { // Capture the hover information for the just created row. This is used in the next render // to ensure that the entire row is highlighted. fn capture_hover_state(&self, response: &Option, row_index: usize) { - let is_row_hovered = response.as_ref().map_or(false, |r| r.hovered()); + let is_row_hovered = response.as_ref().is_some_and(|r| r.hovered()); if is_row_hovered { self.layout .ui @@ -1237,7 +1237,7 @@ impl<'a> TableBody<'a> { } } -impl<'a> Drop for TableBody<'a> { +impl Drop for TableBody<'_> { fn drop(&mut self) { self.layout.allocate_rect(); } @@ -1264,7 +1264,7 @@ pub struct TableRow<'a, 'b> { response: &'b mut Option, } -impl<'a, 'b> TableRow<'a, 'b> { +impl TableRow<'_, '_> { /// Add the contents of a column on this row (i.e. a cell). /// /// Returns the used space (`min_rect`) plus the [`Response`] of the whole cell. @@ -1272,11 +1272,11 @@ impl<'a, 'b> TableRow<'a, 'b> { pub fn col(&mut self, add_cell_contents: impl FnOnce(&mut Ui)) -> (Rect, Response) { let col_index = self.col_index; - let clip = self.columns.get(col_index).map_or(false, |c| c.clip); + let clip = self.columns.get(col_index).is_some_and(|c| c.clip); let auto_size_this_frame = self .columns .get(col_index) - .map_or(false, |c| c.auto_size_this_frame); + .is_some_and(|c| c.auto_size_this_frame); let width = if let Some(width) = self.widths.get(col_index) { self.col_index += 1; @@ -1355,7 +1355,7 @@ impl<'a, 'b> TableRow<'a, 'b> { } } -impl<'a, 'b> Drop for TableRow<'a, 'b> { +impl Drop for TableRow<'_, '_> { #[inline] fn drop(&mut self) { self.layout.end_line(); diff --git a/crates/egui_kittest/src/app_kind.rs b/crates/egui_kittest/src/app_kind.rs index 081490e3d..56b942323 100644 --- a/crates/egui_kittest/src/app_kind.rs +++ b/crates/egui_kittest/src/app_kind.rs @@ -21,7 +21,7 @@ pub(crate) enum AppKind<'a, State> { Eframe(AppKindEframe<'a, State>), } -impl<'a, State> AppKind<'a, State> { +impl AppKind<'_, State> { pub fn run( &mut self, ctx: &egui::Context, diff --git a/crates/egui_kittest/src/lib.rs b/crates/egui_kittest/src/lib.rs index dfaad2c97..fdb31372c 100644 --- a/crates/egui_kittest/src/lib.rs +++ b/crates/egui_kittest/src/lib.rs @@ -68,7 +68,7 @@ pub struct Harness<'a, State = ()> { step_dt: f32, } -impl<'a, State> Debug for Harness<'a, State> { +impl Debug for Harness<'_, State> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { self.kittest.fmt(f) } @@ -461,7 +461,7 @@ impl<'a> Harness<'a> { } } -impl<'t, 'n, 'h, State> Queryable<'t, 'n> for Harness<'h, State> +impl<'t, 'n, State> Queryable<'t, 'n> for Harness<'_, State> where 'n: 't, { diff --git a/crates/epaint/src/mutex.rs b/crates/epaint/src/mutex.rs index bd984a1d0..465722c17 100644 --- a/crates/epaint/src/mutex.rs +++ b/crates/epaint/src/mutex.rs @@ -190,7 +190,7 @@ mod rw_lock_impl { } } - impl<'a, T> Deref for RwLockReadGuard<'a, T> { + impl Deref for RwLockReadGuard<'_, T> { type Target = T; fn deref(&self) -> &Self::Target { @@ -198,7 +198,7 @@ mod rw_lock_impl { } } - impl<'a, T> Drop for RwLockReadGuard<'a, T> { + impl Drop for RwLockReadGuard<'_, T> { fn drop(&mut self) { let tid = std::thread::current().id(); self.holders.lock().remove(&tid); @@ -229,7 +229,7 @@ mod rw_lock_impl { } } - impl<'a, T> Deref for RwLockWriteGuard<'a, T> { + impl Deref for RwLockWriteGuard<'_, T> { type Target = T; fn deref(&self) -> &Self::Target { @@ -237,13 +237,13 @@ mod rw_lock_impl { } } - impl<'a, T> DerefMut for RwLockWriteGuard<'a, T> { + impl DerefMut for RwLockWriteGuard<'_, T> { fn deref_mut(&mut self) -> &mut Self::Target { self.guard.as_mut().unwrap() } } - impl<'a, T> Drop for RwLockWriteGuard<'a, T> { + impl Drop for RwLockWriteGuard<'_, T> { fn drop(&mut self) { let tid = std::thread::current().id(); self.holders.lock().remove(&tid); diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index 2f19e538f..4eb5965c6 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -993,22 +993,22 @@ impl RowBreakCandidates { punctuation, any, } = self; - if space.map_or(false, |s| s < index) { + if space.is_some_and(|s| s < index) { *space = None; } - if cjk.map_or(false, |s| s < index) { + if cjk.is_some_and(|s| s < index) { *cjk = None; } - if pre_cjk.map_or(false, |s| s < index) { + if pre_cjk.is_some_and(|s| s < index) { *pre_cjk = None; } - if dash.map_or(false, |s| s < index) { + if dash.is_some_and(|s| s < index) { *dash = None; } - if punctuation.map_or(false, |s| s < index) { + if punctuation.is_some_and(|s| s < index) { *punctuation = None; } - if any.map_or(false, |s| s < index) { + if any.is_some_and(|s| s < index) { *any = None; } } From 366900c55059e121093a61ed6184e764a68d67f3 Mon Sep 17 00:00:00 2001 From: Alix Bott Date: Tue, 14 Jan 2025 08:44:39 +0100 Subject: [PATCH 016/132] implement Debug for RichText (#5596) * Derive Debug for RichText * [x] I have followed the instructions in the PR template --- crates/egui/src/widget_text.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui/src/widget_text.rs b/crates/egui/src/widget_text.rs index d5cd16f09..1132aa304 100644 --- a/crates/egui/src/widget_text.rs +++ b/crates/egui/src/widget_text.rs @@ -22,7 +22,7 @@ use crate::{ /// RichText::new("colored").color(Color32::RED); /// RichText::new("Large and underlined").size(20.0).underline(); /// ``` -#[derive(Clone, Default, PartialEq)] +#[derive(Debug, Clone, Default, PartialEq)] pub struct RichText { text: String, size: Option, From a5d7cf5bd76b54531fc3a97a2ce7f37d16ae0069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=96R=C3=96K=20Attila?= Date: Thu, 16 Jan 2025 17:00:29 +0100 Subject: [PATCH 017/132] Upgrade to wgpu 24 (#5610) --- .github/workflows/deploy_web_demo.yml | 2 +- .github/workflows/rust.yml | 16 +- Cargo.lock | 278 +++++++++++------- Cargo.toml | 4 +- clippy.toml | 2 +- crates/eframe/src/web/panic_handler.rs | 4 +- crates/eframe/src/web/text_agent.rs | 5 +- crates/egui-wgpu/src/capture.rs | 4 +- crates/egui-wgpu/src/renderer.rs | 4 +- crates/egui-wgpu/src/setup.rs | 27 +- crates/egui/src/lib.rs | 2 +- crates/egui_demo_app/Cargo.toml | 2 +- crates/egui_kittest/src/texture_to_image.rs | 4 +- deny.toml | 4 +- 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 +- scripts/setup_web.sh | 4 +- 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 +- 42 files changed, 226 insertions(+), 190 deletions(-) diff --git a/.github/workflows/deploy_web_demo.yml b/.github/workflows/deploy_web_demo.yml index 6eb546961..6c9d80962 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.80.0 + toolchain: 1.81.0 override: true - uses: Swatinem/rust-cache@v2 diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index b1f5a5a37..f1514cf2f 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -18,7 +18,7 @@ jobs: - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.80.0 + toolchain: 1.81.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.80.0 + toolchain: 1.81.0 targets: wasm32-unknown-unknown - run: sudo apt-get update && sudo apt-get install libgtk-3-dev libatk1.0-dev @@ -103,7 +103,7 @@ jobs: - name: wasm-bindgen uses: jetli/wasm-bindgen-action@v0.1.0 with: - version: "0.2.95" + version: "0.2.97" - run: ./scripts/wasm_bindgen_check.sh --skip-setup @@ -155,7 +155,7 @@ jobs: - uses: actions/checkout@v4 - uses: EmbarkStudios/cargo-deny-action@v1 with: - rust-version: "1.80.0" + rust-version: "1.81.0" log-level: error command: check arguments: --target ${{ matrix.target }} @@ -170,7 +170,7 @@ jobs: - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.80.0 + toolchain: 1.81.0 targets: aarch64-linux-android - name: Set up cargo cache @@ -189,7 +189,7 @@ jobs: - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.80.0 + toolchain: 1.81.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.80.0 + toolchain: 1.81.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.80.0 + toolchain: 1.81.0 - name: Set up cargo cache uses: Swatinem/rust-cache@v2 diff --git a/Cargo.lock b/Cargo.lock index 930cf3326..709b7b77a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,7 +38,7 @@ dependencies = [ "accesskit_consumer", "atspi-common", "serde", - "thiserror", + "thiserror 1.0.66", "zvariant 4.2.0", ] @@ -163,7 +163,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" dependencies = [ "android-properties", - "bitflags 2.6.0", + "bitflags 2.8.0", "cc", "cesu8", "jni", @@ -174,7 +174,7 @@ dependencies = [ "ndk-context", "ndk-sys 0.6.0+11769913", "num_enum", - "thiserror", + "thiserror 1.0.66", ] [[package]] @@ -591,9 +591,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" dependencies = [ "serde", ] @@ -643,9 +643,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" -version = "1.19.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" dependencies = [ "bytemuck_derive", ] @@ -685,12 +685,12 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "log", "polling", "rustix", "slab", - "thiserror", + "thiserror 1.0.66", ] [[package]] @@ -734,12 +734,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "cfg_aliases" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" - [[package]] name = "cfg_aliases" version = "0.2.1" @@ -1225,7 +1219,7 @@ dependencies = [ "egui-wgpu", "egui-winit", "egui_glow", - "glow 0.16.0", + "glow", "glutin", "glutin-winit", "home", @@ -1260,7 +1254,7 @@ dependencies = [ "accesskit", "ahash", "backtrace", - "bitflags 2.6.0", + "bitflags 2.8.0", "document-features", "emath", "epaint", @@ -1282,7 +1276,7 @@ dependencies = [ "epaint", "log", "profiling", - "thiserror", + "thiserror 1.0.66", "type-map", "web-time", "wgpu", @@ -1379,7 +1373,7 @@ dependencies = [ "document-features", "egui", "egui-winit", - "glow 0.16.0", + "glow", "glutin", "glutin-winit", "log", @@ -1817,18 +1811,6 @@ dependencies = [ "xml-rs", ] -[[package]] -name = "glow" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d51fa363f025f5c111e03f13eda21162faeacb6911fe8caa0c0349f9cf0c4483" -dependencies = [ - "js-sys", - "slotmap", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "glow" version = "0.16.0" @@ -1847,8 +1829,8 @@ version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec69412a0bf07ea7607e638b415447857a808846c2b685a43c8aa18bc6d5e499" dependencies = [ - "bitflags 2.6.0", - "cfg_aliases 0.2.1", + "bitflags 2.8.0", + "cfg_aliases", "cgl", "core-foundation", "dispatch", @@ -1872,7 +1854,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85edca7075f8fc728f28cb8fbb111a96c3b89e930574369e3e9c27eb75d3788f" dependencies = [ - "cfg_aliases 0.2.1", + "cfg_aliases", "glutin", "raw-window-handle 0.6.2", "winit", @@ -1913,7 +1895,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "gpu-alloc-types", ] @@ -1923,7 +1905,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", ] [[package]] @@ -1934,7 +1916,7 @@ checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd" dependencies = [ "log", "presser", - "thiserror", + "thiserror 1.0.66", "windows", ] @@ -1944,7 +1926,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c08c1f623a8d0b722b8b99f821eb0ba672a1618f0d3b16ddbee1cedd2dd8557" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "gpu-descriptor-types", "hashbrown", ] @@ -1955,7 +1937,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", ] [[package]] @@ -1978,6 +1960,12 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hello_android" version = "0.1.0" @@ -2310,7 +2298,7 @@ dependencies = [ "combine", "jni-sys", "log", - "thiserror", + "thiserror 1.0.66", "walkdir", "windows-sys 0.45.0", ] @@ -2338,10 +2326,11 @@ checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" [[package]] name = "js-sys" -version = "0.3.72" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +checksum = "a865e038f7f6ed956f788f0d7d60c541fff74c7bd74272c5d4cf15c63743e705" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -2418,7 +2407,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "libc", "redox_syscall 0.5.7", ] @@ -2504,11 +2493,11 @@ dependencies = [ [[package]] name = "metal" -version = "0.29.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" +checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "block", "core-graphics-types", "foreign-types", @@ -2559,22 +2548,23 @@ dependencies = [ [[package]] name = "naga" -version = "23.0.0" +version = "24.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d5941e45a15b53aad4375eedf02033adb7a28931eedc31117faffa52e6a857e" +checksum = "e380993072e52eef724eddfcde0ed013b0c023c3f0417336ed041aa9f076994e" dependencies = [ "arrayvec", "bit-set 0.8.0", - "bitflags 2.6.0", - "cfg_aliases 0.1.1", + "bitflags 2.8.0", + "cfg_aliases", "codespan-reporting", "hexf-parse", "indexmap", "log", "rustc-hash", "spirv", + "strum", "termcolor", - "thiserror", + "thiserror 2.0.11", "unicode-xid", ] @@ -2584,13 +2574,13 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "jni-sys", "log", "ndk-sys 0.6.0+11769913", "num_enum", "raw-window-handle 0.6.2", - "thiserror", + "thiserror 1.0.66", ] [[package]] @@ -2623,9 +2613,9 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "cfg-if", - "cfg_aliases 0.2.1", + "cfg_aliases", "libc", "memoffset", ] @@ -2703,7 +2693,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "block2", "libc", "objc2", @@ -2719,7 +2709,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "block2", "objc2", "objc2-core-location", @@ -2743,7 +2733,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "block2", "objc2", "objc2-foundation", @@ -2785,7 +2775,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "block2", "dispatch", "libc", @@ -2810,7 +2800,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "block2", "objc2", "objc2-foundation", @@ -2822,7 +2812,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "block2", "objc2", "objc2-foundation", @@ -2845,7 +2835,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "block2", "objc2", "objc2-cloud-kit", @@ -2877,7 +2867,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "block2", "objc2", "objc2-core-location", @@ -2920,6 +2910,15 @@ dependencies = [ "libredox", ] +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + [[package]] name = "ordered-stream" version = "0.2.0" @@ -3138,9 +3137,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.89" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] @@ -3339,7 +3338,7 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", ] [[package]] @@ -3350,7 +3349,7 @@ checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom", "libredox", - "thiserror", + "thiserror 1.0.66", ] [[package]] @@ -3455,7 +3454,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ "base64 0.21.7", - "bitflags 2.6.0", + "bitflags 2.8.0", "serde", "serde_derive", ] @@ -3484,7 +3483,7 @@ version = "0.38.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "errno", "libc", "linux-raw-sys", @@ -3523,6 +3522,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + [[package]] name = "ryu" version = "1.0.18" @@ -3701,7 +3706,7 @@ version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "calloop", "calloop-wayland-source", "cursor-icon", @@ -3709,7 +3714,7 @@ dependencies = [ "log", "memmap2", "rustix", - "thiserror", + "thiserror 1.0.66", "wayland-backend", "wayland-client", "wayland-csd-frame", @@ -3752,7 +3757,7 @@ version = "0.3.0+sdk-1.3.268.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", ] [[package]] @@ -3776,6 +3781,28 @@ dependencies = [ "float-cmp", ] +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "subtle" version = "2.6.1" @@ -3794,9 +3821,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.86" +version = "2.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89275301d38033efb81a6e60e3497e734dfcc62571f2854bf4b16690398824c" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" dependencies = [ "proc-macro2", "quote", @@ -3831,7 +3858,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "thiserror", + "thiserror 1.0.66", "walkdir", "yaml-rust", ] @@ -3905,7 +3932,16 @@ version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d171f59dbaa811dbbb1aee1e73db92ec2b122911a48e1390dfe327a821ddede" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.66", +] + +[[package]] +name = "thiserror" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +dependencies = [ + "thiserror-impl 2.0.11", ] [[package]] @@ -3919,6 +3955,17 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror-impl" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tiff" version = "0.9.1" @@ -4251,9 +4298,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.95" +version = "0.2.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +checksum = "d15e63b4482863c109d70a7b8706c1e364eb6ea449b201a76c5b89cedcec2d5c" dependencies = [ "cfg-if", "once_cell", @@ -4262,9 +4309,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.95" +version = "0.2.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +checksum = "8d36ef12e3aaca16ddd3f67922bc63e48e953f126de60bd33ccc0101ef9998cd" dependencies = [ "bumpalo", "log", @@ -4289,9 +4336,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.95" +version = "0.2.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +checksum = "705440e08b42d3e4b36de7d66c944be628d579796b8090bfa3471478a2260051" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4299,9 +4346,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.95" +version = "0.2.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +checksum = "98c9ae5a76e46f4deecd0f0255cc223cfa18dc9b261213b8aa0c7b36f61b3f1d" dependencies = [ "proc-macro2", "quote", @@ -4312,9 +4359,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.95" +version = "0.2.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" +checksum = "6ee99da9c5ba11bd675621338ef6fa52296b76b83305e9b6e5c77d4c286d6d49" [[package]] name = "wayland-backend" @@ -4336,7 +4383,7 @@ version = "0.31.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b66249d3fc69f76fd74c82cc319300faa554e9d865dab1f7cd66cc20db10b280" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "rustix", "wayland-backend", "wayland-scanner", @@ -4348,7 +4395,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "cursor-icon", "wayland-backend", ] @@ -4370,7 +4417,7 @@ version = "0.32.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd0ade57c4e6e9a8952741325c30bf82f4246885dca8bf561898b86d0c1f58e" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -4382,7 +4429,7 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b31cab548ee68c7eb155517f2212049dc151f7cd7910c2b66abfd31c3ee12bd" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -4395,7 +4442,7 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "782e12f6cd923c3c316130d56205ebab53f55d6666b7faddfad36cecaeeb4022" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -4427,9 +4474,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.72" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +checksum = "a98bc3c33f0fe7e59ad7cd041b89034fa82a7c2d4365ca538dda6cdaf513863c" dependencies = [ "js-sys", "wasm-bindgen", @@ -4479,12 +4526,13 @@ checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" [[package]] name = "wgpu" -version = "23.0.1" +version = "24.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80f70000db37c469ea9d67defdc13024ddf9a5f1b89cb2941b812ad7cde1735a" +checksum = "e41253fc7b660735e2a2d9a58c563f2a047d3cc3445293d8f4095538c9e8afbe" dependencies = [ "arrayvec", - "cfg_aliases 0.1.1", + "bitflags 2.8.0", + "cfg_aliases", "document-features", "js-sys", "log", @@ -4504,14 +4552,14 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "23.0.1" +version = "24.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d63c3c478de8e7e01786479919c8769f62a22eec16788d8c2ac77ce2c132778a" +checksum = "82a39b8842dc9ffcbe34346e3ab6d496b32a47f6497e119d762c97fcaae3cb37" dependencies = [ "arrayvec", "bit-vec 0.8.0", - "bitflags 2.6.0", - "cfg_aliases 0.1.1", + "bitflags 2.8.0", + "cfg_aliases", "document-features", "indexmap", "log", @@ -4522,27 +4570,27 @@ dependencies = [ "raw-window-handle 0.6.2", "rustc-hash", "smallvec", - "thiserror", + "thiserror 2.0.11", "wgpu-hal", "wgpu-types", ] [[package]] name = "wgpu-hal" -version = "23.0.1" +version = "24.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89364b8a0b211adc7b16aeaf1bd5ad4a919c1154b44c9ce27838213ba05fd821" +checksum = "5a782e5056b060b0b4010881d1decddd059e44f2ecd01e2db2971b48ad3627e5" dependencies = [ "android_system_properties", "arrayvec", "ash", "bit-set 0.8.0", - "bitflags 2.6.0", + "bitflags 2.8.0", "block", "bytemuck", - "cfg_aliases 0.1.1", + "cfg_aliases", "core-graphics-types", - "glow 0.14.2", + "glow", "glutin_wgl_sys", "gpu-alloc", "gpu-allocator", @@ -4557,6 +4605,7 @@ dependencies = [ "ndk-sys 0.5.0+25.2.9519653", "objc", "once_cell", + "ordered-float", "parking_lot", "profiling", "range-alloc", @@ -4564,7 +4613,7 @@ dependencies = [ "renderdoc-sys", "rustc-hash", "smallvec", - "thiserror", + "thiserror 2.0.11", "wasm-bindgen", "web-sys", "wgpu-types", @@ -4574,12 +4623,13 @@ dependencies = [ [[package]] name = "wgpu-types" -version = "23.0.0" +version = "24.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "610f6ff27778148c31093f3b03abc4840f9636d58d597ca2f5977433acfe0068" +checksum = "50ac044c0e76c03a0378e7786ac505d010a873665e2d51383dcff8dd227dc69c" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "js-sys", + "log", "web-sys", ] @@ -4605,7 +4655,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -4910,11 +4960,11 @@ dependencies = [ "ahash", "android-activity", "atomic-waker", - "bitflags 2.6.0", + "bitflags 2.8.0", "block2", "bytemuck", "calloop", - "cfg_aliases 0.2.1", + "cfg_aliases", "concurrent-queue", "core-foundation", "core-graphics", @@ -5028,7 +5078,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "dlib", "log", "once_cell", diff --git a/Cargo.toml b/Cargo.toml index 716551151..3ce59e668 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.80" +rust-version = "1.81" version = "0.30.0" @@ -100,7 +100,7 @@ wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" web-sys = "0.3.70" web-time = "1.1.0" # Timekeeping for native and web -wgpu = { version = "23.0.0", default-features = false } +wgpu = { version = "24.0.0", default-features = false } windows-sys = "0.59" winit = { version = "0.30.7", default-features = false } diff --git a/clippy.toml b/clippy.toml index 9e5fdd1e5..f349943a9 100644 --- a/clippy.toml +++ b/clippy.toml @@ -3,7 +3,7 @@ # ----------------------------------------------------------------------------- # Section identical to scripts/clippy_wasm/clippy.toml: -msrv = "1.80" +msrv = "1.81" allow-unwrap-in-tests = true diff --git a/crates/eframe/src/web/panic_handler.rs b/crates/eframe/src/web/panic_handler.rs index b379f775d..ce9ba844c 100644 --- a/crates/eframe/src/web/panic_handler.rs +++ b/crates/eframe/src/web/panic_handler.rs @@ -56,7 +56,7 @@ struct PanicHandlerInner { /// Contains a summary about a panics. /// -/// This is basically a human-readable version of [`std::panic::PanicInfo`] +/// This is basically a human-readable version of [`std::panic::PanicHookInfo`] /// with an added callstack. #[derive(Clone, Debug)] pub struct PanicSummary { @@ -66,7 +66,7 @@ pub struct PanicSummary { impl PanicSummary { /// Construct a summary from a panic. - pub fn new(info: &std::panic::PanicInfo<'_>) -> Self { + pub fn new(info: &std::panic::PanicHookInfo<'_>) -> Self { let message = info.to_string(); let callstack = Error::new().stack(); Self { message, callstack } diff --git a/crates/eframe/src/web/text_agent.rs b/crates/eframe/src/web/text_agent.rs index f0eb67d60..db47df2ae 100644 --- a/crates/eframe/src/web/text_agent.rs +++ b/crates/eframe/src/web/text_agent.rs @@ -20,9 +20,10 @@ impl TextAgent { // create an `` element let input = document .create_element("input")? - .dyn_into::()?; + .dyn_into::()?; + input.set_autofocus(true)?; + let input = input.dyn_into::()?; input.set_type("text"); - input.set_autofocus(true); input.set_attribute("autocapitalize", "off")?; // append it to `` and hide it outside of the viewport diff --git a/crates/egui-wgpu/src/capture.rs b/crates/egui-wgpu/src/capture.rs index 1ce780d05..40cf5484f 100644 --- a/crates/egui-wgpu/src/capture.rs +++ b/crates/egui-wgpu/src/capture.rs @@ -139,9 +139,9 @@ impl CaptureState { encoder.copy_texture_to_buffer( tex.as_image_copy(), - wgpu::ImageCopyBuffer { + wgpu::TexelCopyBufferInfo { buffer: &buffer, - layout: wgpu::ImageDataLayout { + layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(padding.padded_bytes_per_row), rows_per_image: None, diff --git a/crates/egui-wgpu/src/renderer.rs b/crates/egui-wgpu/src/renderer.rs index 2c1fa0428..5a5c953bb 100644 --- a/crates/egui-wgpu/src/renderer.rs +++ b/crates/egui-wgpu/src/renderer.rs @@ -579,14 +579,14 @@ impl Renderer { let queue_write_data_to_texture = |texture, origin| { profiling::scope!("write_texture"); queue.write_texture( - wgpu::ImageCopyTexture { + wgpu::TexelCopyTextureInfo { texture, mip_level: 0, origin, aspect: wgpu::TextureAspect::All, }, data_bytes, - wgpu::ImageDataLayout { + wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(4 * width), rows_per_image: Some(height), diff --git a/crates/egui-wgpu/src/setup.rs b/crates/egui-wgpu/src/setup.rs index 876b0c8e3..45e15ea22 100644 --- a/crates/egui-wgpu/src/setup.rs +++ b/crates/egui-wgpu/src/setup.rs @@ -68,16 +68,8 @@ impl WgpuSetup { #[allow(clippy::arc_with_non_send_sync)] Arc::new( - wgpu::util::new_instance_with_webgpu_detection(wgpu::InstanceDescriptor { - backends: create_new.instance_descriptor.backends, - flags: create_new.instance_descriptor.flags, - dx12_shader_compiler: create_new - .instance_descriptor - .dx12_shader_compiler - .clone(), - gles_minor_version: create_new.instance_descriptor.gles_minor_version, - }) - .await, + wgpu::util::new_instance_with_webgpu_detection(&create_new.instance_descriptor) + .await, ) } Self::Existing(existing) => existing.instance.clone(), @@ -151,13 +143,7 @@ pub struct WgpuSetupCreateNew { impl Clone for WgpuSetupCreateNew { fn clone(&self) -> Self { Self { - // TODO(gfx-rs/wgpu/#6849): use .clone() - instance_descriptor: wgpu::InstanceDescriptor { - backends: self.instance_descriptor.backends, - flags: self.instance_descriptor.flags, - dx12_shader_compiler: self.instance_descriptor.dx12_shader_compiler.clone(), - gles_minor_version: self.instance_descriptor.gles_minor_version, - }, + instance_descriptor: self.instance_descriptor.clone(), power_preference: self.power_preference, native_adapter_selector: self.native_adapter_selector.clone(), device_descriptor: self.device_descriptor.clone(), @@ -186,14 +172,13 @@ impl Default for WgpuSetupCreateNew { instance_descriptor: wgpu::InstanceDescriptor { // Add GL backend, primarily because WebGPU is not stable enough yet. // (note however, that the GL backend needs to be opted-in via the wgpu feature flag "webgl") - backends: wgpu::util::backend_bits_from_env() + backends: wgpu::Backends::from_env() .unwrap_or(wgpu::Backends::PRIMARY | wgpu::Backends::GL), flags: wgpu::InstanceFlags::from_build_config().with_env(), - dx12_shader_compiler: wgpu::Dx12Compiler::default(), - gles_minor_version: wgpu::Gles3MinorVersion::Automatic, + backend_options: wgpu::BackendOptions::from_env_or_default(), }, - power_preference: wgpu::util::power_preference_from_env() + power_preference: wgpu::PowerPreference::from_env() .unwrap_or(wgpu::PowerPreference::HighPerformance), native_adapter_selector: None, diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 85e12cb8a..8d0858425 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.80.0 or later to use `egui`. +//! You need to have rust 1.81.0 or later to use `egui`. //! //! To quickly get started with egui, you can take a look at [`eframe_template`](https://github.com/emilk/eframe_template) //! which uses [`eframe`](https://docs.rs/eframe). diff --git a/crates/egui_demo_app/Cargo.toml b/crates/egui_demo_app/Cargo.toml index 3115c845e..6ed74efab 100644 --- a/crates/egui_demo_app/Cargo.toml +++ b/crates/egui_demo_app/Cargo.toml @@ -90,7 +90,7 @@ rfd = { version = "0.15", optional = true } # web: [target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = "=0.2.95" +wasm-bindgen = "=0.2.97" wasm-bindgen-futures.workspace = true web-sys.workspace = true diff --git a/crates/egui_kittest/src/texture_to_image.rs b/crates/egui_kittest/src/texture_to_image.rs index 98803ac8a..8f7cb72a5 100644 --- a/crates/egui_kittest/src/texture_to_image.rs +++ b/crates/egui_kittest/src/texture_to_image.rs @@ -23,9 +23,9 @@ pub(crate) fn texture_to_image(device: &Device, queue: &Queue, texture: &Texture // Copy the data from the texture to the buffer encoder.copy_texture_to_buffer( texture.as_image_copy(), - wgpu::ImageCopyBuffer { + wgpu::TexelCopyBufferInfo { buffer: &output_buffer, - layout: wgpu::ImageDataLayout { + layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(buffer_dimensions.padded_bytes_per_row as u32), rows_per_image: None, diff --git a/deny.toml b/deny.toml index ede37dea4..4db5246a7 100644 --- a/deny.toml +++ b/deny.toml @@ -48,14 +48,14 @@ 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 = "cfg_aliases" }, # old version via wgpu { name = "event-listener" }, # TODO(emilk): rustls pulls in two versions of this 😭 { name = "futures-lite" }, # old version via accesskit_unix and zbus - { name = "glow" }, # old version via wgpu { 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 unmaintianed 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) diff --git a/examples/confirm_exit/Cargo.toml b/examples/confirm_exit/Cargo.toml index 62e8bc29c..c3050c32d 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.80" +rust-version = "1.81" publish = false [lints] diff --git a/examples/custom_3d_glow/Cargo.toml b/examples/custom_3d_glow/Cargo.toml index 1ddec02f5..3ed54df78 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.80" +rust-version = "1.81" publish = false [lints] diff --git a/examples/custom_font/Cargo.toml b/examples/custom_font/Cargo.toml index 5214bdc1f..afaeb4117 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.80" +rust-version = "1.81" publish = false [lints] diff --git a/examples/custom_font_style/Cargo.toml b/examples/custom_font_style/Cargo.toml index 241b89340..f64a48803 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.80" +rust-version = "1.81" publish = false [lints] diff --git a/examples/custom_keypad/Cargo.toml b/examples/custom_keypad/Cargo.toml index 73c6b0e7a..7e765e4ec 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.80" +rust-version = "1.81" publish = false [lints] diff --git a/examples/custom_style/Cargo.toml b/examples/custom_style/Cargo.toml index f87ce0bf8..423179aaa 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.80" +rust-version = "1.81" publish = false [lints] diff --git a/examples/custom_window_frame/Cargo.toml b/examples/custom_window_frame/Cargo.toml index 4a53ee487..b0e89c8a6 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.80" +rust-version = "1.81" publish = false [lints] diff --git a/examples/file_dialog/Cargo.toml b/examples/file_dialog/Cargo.toml index 1a9f86c40..cf61cd45f 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.80" +rust-version = "1.81" publish = false [lints] diff --git a/examples/hello_android/Cargo.toml b/examples/hello_android/Cargo.toml index a7d272773..b3bf4407e 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.80" +rust-version = "1.81" 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 8816b4255..1e4b553db 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.80" +rust-version = "1.81" publish = false [lints] diff --git a/examples/hello_world_par/Cargo.toml b/examples/hello_world_par/Cargo.toml index b3e00b208..541b220f7 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.80" +rust-version = "1.81" publish = false [lints] diff --git a/examples/hello_world_simple/Cargo.toml b/examples/hello_world_simple/Cargo.toml index 7197c6033..db1d7906d 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.80" +rust-version = "1.81" publish = false [lints] diff --git a/examples/images/Cargo.toml b/examples/images/Cargo.toml index f1b4f97dd..c013e7b43 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.80" +rust-version = "1.81" publish = false [lints] diff --git a/examples/keyboard_events/Cargo.toml b/examples/keyboard_events/Cargo.toml index 1af098493..d57f17193 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.80" +rust-version = "1.81" publish = false [lints] diff --git a/examples/multiple_viewports/Cargo.toml b/examples/multiple_viewports/Cargo.toml index 1644d6a72..d5efc23df 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.80" +rust-version = "1.81" publish = false [lints] diff --git a/examples/puffin_profiler/Cargo.toml b/examples/puffin_profiler/Cargo.toml index d0e9e485a..d26d8a6cb 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.80" +rust-version = "1.81" publish = false [package.metadata.cargo-machete] diff --git a/examples/screenshot/Cargo.toml b/examples/screenshot/Cargo.toml index 5963ea3ad..b5a22ce7b 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.80" +rust-version = "1.81" publish = false [lints] diff --git a/examples/serial_windows/Cargo.toml b/examples/serial_windows/Cargo.toml index 1f6d5ea43..c18a6029d 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.80" +rust-version = "1.81" publish = false [lints] diff --git a/examples/user_attention/Cargo.toml b/examples/user_attention/Cargo.toml index 25aa47348..3ae28d693 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.80" +rust-version = "1.81" publish = false [lints] diff --git a/rust-toolchain b/rust-toolchain index 38e5e90f3..0eefd31bc 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.80.0" +channel = "1.81.0" components = ["rustfmt", "clippy"] targets = ["wasm32-unknown-unknown"] diff --git a/scripts/check.sh b/scripts/check.sh index 8c0f0af97..0d835617b 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.80.0 install --quiet typos-cli +cargo +1.81.0 install --quiet typos-cli export RUSTFLAGS="-D warnings" export RUSTDOCFLAGS="-D warnings" # https://github.com/emilk/egui/pull/1454 diff --git a/scripts/clippy_wasm/clippy.toml b/scripts/clippy_wasm/clippy.toml index f06033c71..2d34d64fc 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.80" +msrv = "1.81" allow-unwrap-in-tests = true diff --git a/scripts/setup_web.sh b/scripts/setup_web.sh index d4166a3af..a49f82059 100755 --- a/scripts/setup_web.sh +++ b/scripts/setup_web.sh @@ -9,6 +9,6 @@ set -x rustup target add wasm32-unknown-unknown # For generating JS bindings: -if ! cargo install --list | grep -q 'wasm-bindgen-cli v0.2.95'; then - cargo install --force --quiet wasm-bindgen-cli --version 0.2.95 +if ! cargo install --list | grep -q 'wasm-bindgen-cli v0.2.97'; then + cargo install --force --quiet wasm-bindgen-cli --version 0.2.97 fi diff --git a/tests/test_egui_extras_compilation/Cargo.toml b/tests/test_egui_extras_compilation/Cargo.toml index f1d67f4bb..9f1aeca52 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.80" +rust-version = "1.81" publish = false [lints] diff --git a/tests/test_inline_glow_paint/Cargo.toml b/tests/test_inline_glow_paint/Cargo.toml index bcda5b3dd..15cb21640 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.80" +rust-version = "1.81" publish = false [lints] diff --git a/tests/test_size_pass/Cargo.toml b/tests/test_size_pass/Cargo.toml index e3819a107..a44f2cc0b 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.80" +rust-version = "1.81" publish = false [lints] diff --git a/tests/test_ui_stack/Cargo.toml b/tests/test_ui_stack/Cargo.toml index 12ba9961b..dd8bdb5de 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.80" +rust-version = "1.81" publish = false [lints] diff --git a/tests/test_viewports/Cargo.toml b/tests/test_viewports/Cargo.toml index cb877a710..2636b5177 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.80" +rust-version = "1.81" publish = false [lints] From cf965aaa30987a5b6fa2380f37c0ce8cb869347d Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 16 Jan 2025 21:12:19 +0100 Subject: [PATCH 018/132] Update awalsh128/cache-apt-pkgs-action to 1.4.3 to fix CI --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index f1514cf2f..18bcbc659 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -22,7 +22,7 @@ jobs: - name: Install packages (Linux) if: runner.os == 'Linux' - uses: awalsh128/cache-apt-pkgs-action@v1.4.2 + uses: awalsh128/cache-apt-pkgs-action@v1.4.3 with: packages: libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev libgtk-3-dev # libgtk-3-dev is used by rfd version: 1.0 From 30e66e457575096bd60e95800e7dd9fd755c0046 Mon Sep 17 00:00:00 2001 From: Andreas Reich Date: Mon, 20 Jan 2025 18:06:35 +0100 Subject: [PATCH 019/132] Wgpu resources are no longer wrapped in `Arc` (since they are now `Clone`) (#5612) Co-authored-by: Nicolas --- crates/egui-wgpu/src/lib.rs | 34 ++++++++++------------------- crates/egui-wgpu/src/setup.rs | 21 +++++++----------- crates/egui-wgpu/src/winit.rs | 2 +- crates/egui_kittest/src/builder.rs | 7 +++--- crates/egui_kittest/src/lib.rs | 1 + crates/egui_kittest/src/renderer.rs | 17 +++++++++++---- 6 files changed, 37 insertions(+), 45 deletions(-) diff --git a/crates/egui-wgpu/src/lib.rs b/crates/egui-wgpu/src/lib.rs index 0d795783a..e19c26574 100644 --- a/crates/egui-wgpu/src/lib.rs +++ b/crates/egui-wgpu/src/lib.rs @@ -63,21 +63,20 @@ pub enum WgpuError { #[derive(Clone)] pub struct RenderState { /// Wgpu adapter used for rendering. - pub adapter: Arc, + pub adapter: wgpu::Adapter, /// All the available adapters. /// /// This is not available on web. /// On web, we always select WebGPU is available, then fall back to WebGL if not. - // TODO(gfx-rs/wgpu#6665): Remove layer of `Arc` here once we update to wgpu 24 #[cfg(not(target_arch = "wasm32"))] - pub available_adapters: Arc<[Arc]>, + pub available_adapters: Vec, /// Wgpu device used for rendering, created from the adapter. - pub device: Arc, + pub device: wgpu::Device, /// Wgpu queue used for rendering, created from the adapter. - pub queue: Arc, + pub queue: wgpu::Queue, /// The target texture format used for presenting to the window. pub target_format: wgpu::TextureFormat, @@ -90,8 +89,8 @@ async fn request_adapter( instance: &wgpu::Instance, power_preference: wgpu::PowerPreference, compatible_surface: Option<&wgpu::Surface<'_>>, - _available_adapters: &[Arc], -) -> Result, WgpuError> { + _available_adapters: &[wgpu::Adapter], +) -> Result { profiling::function_scope!(); let adapter = instance @@ -149,10 +148,7 @@ async fn request_adapter( ); } - // 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)] - Ok(Arc::new(adapter)) + Ok(adapter) } impl RenderState { @@ -179,12 +175,7 @@ impl RenderState { wgpu::Backends::all() }; - instance - .enumerate_adapters(backends) - // TODO(gfx-rs/wgpu#6665): Remove layer of `Arc` here once we update to wgpu 24. - .into_iter() - .map(Arc::new) - .collect::>() + instance.enumerate_adapters(backends) }; let (adapter, device, queue) = match config.wgpu_setup.clone() { @@ -222,10 +213,7 @@ impl RenderState { .await? }; - // 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)] - (adapter, Arc::new(device), Arc::new(queue)) + (adapter, device, queue) } WgpuSetup::Existing(WgpuSetupExisting { instance: _, @@ -258,7 +246,7 @@ impl RenderState { Ok(Self { adapter, #[cfg(not(target_arch = "wasm32"))] - available_adapters: available_adapters.into(), + available_adapters, device, queue, target_format, @@ -268,7 +256,7 @@ impl RenderState { } #[cfg(not(target_arch = "wasm32"))] -fn describe_adapters(adapters: &[Arc]) -> String { +fn describe_adapters(adapters: &[wgpu::Adapter]) -> String { if adapters.is_empty() { "(none)".to_owned() } else if adapters.len() == 1 { diff --git a/crates/egui-wgpu/src/setup.rs b/crates/egui-wgpu/src/setup.rs index 45e15ea22..1f70b6bb7 100644 --- a/crates/egui-wgpu/src/setup.rs +++ b/crates/egui-wgpu/src/setup.rs @@ -45,7 +45,7 @@ impl WgpuSetup { /// /// Does *not* store the wgpu instance, so calling this repeatedly may /// create a new instance every time! - pub async fn new_instance(&self) -> Arc { + pub async fn new_instance(&self) -> wgpu::Instance { match self { Self::CreateNew(create_new) => { #[allow(unused_mut)] @@ -65,12 +65,8 @@ impl WgpuSetup { } log::debug!("Creating wgpu instance with backends {:?}", backends); - - #[allow(clippy::arc_with_non_send_sync)] - Arc::new( - wgpu::util::new_instance_with_webgpu_detection(&create_new.instance_descriptor) - .await, - ) + wgpu::util::new_instance_with_webgpu_detection(&create_new.instance_descriptor) + .await } Self::Existing(existing) => existing.instance.clone(), } @@ -93,9 +89,8 @@ impl From for WgpuSetup { /// /// This can be used for fully custom adapter selection. /// If available, `wgpu::Surface` is passed to allow checking for surface compatibility. -// TODO(gfx-rs/wgpu#6665): Remove layer of `Arc` here. pub type NativeAdapterSelectorMethod = Arc< - dyn Fn(&[Arc], Option<&wgpu::Surface<'_>>) -> Result, String> + dyn Fn(&[wgpu::Adapter], Option<&wgpu::Surface<'_>>) -> Result + Send + Sync, >; @@ -215,8 +210,8 @@ impl Default for WgpuSetupCreateNew { /// Used for [`WgpuSetup::Existing`]. #[derive(Clone)] pub struct WgpuSetupExisting { - pub instance: Arc, - pub adapter: Arc, - pub device: Arc, - pub queue: Arc, + pub instance: wgpu::Instance, + pub adapter: wgpu::Adapter, + pub device: wgpu::Device, + pub queue: wgpu::Queue, } diff --git a/crates/egui-wgpu/src/winit.rs b/crates/egui-wgpu/src/winit.rs index 86f7bd84d..cc3a7db2c 100644 --- a/crates/egui-wgpu/src/winit.rs +++ b/crates/egui-wgpu/src/winit.rs @@ -27,7 +27,7 @@ pub struct Painter { depth_format: Option, screen_capture_state: Option, - instance: Arc, + instance: wgpu::Instance, render_state: Option, // Per viewport/window: diff --git a/crates/egui_kittest/src/builder.rs b/crates/egui_kittest/src/builder.rs index d21558113..4010b4584 100644 --- a/crates/egui_kittest/src/builder.rs +++ b/crates/egui_kittest/src/builder.rs @@ -1,5 +1,4 @@ use crate::app_kind::AppKind; -use crate::wgpu::WgpuTestRenderer; use crate::{Harness, LazyRenderer, TestRenderer}; use egui::{Pos2, Rect, Vec2}; use std::marker::PhantomData; @@ -75,16 +74,16 @@ impl HarnessBuilder { /// Enable wgpu rendering with a default setup suitable for testing. /// - /// This sets up a [`WgpuTestRenderer`] with the default setup. + /// This sets up a [`crate::wgpu::WgpuTestRenderer`] with the default setup. #[cfg(feature = "wgpu")] pub fn wgpu(self) -> Self { - self.renderer(WgpuTestRenderer::default()) + self.renderer(crate::wgpu::WgpuTestRenderer::default()) } /// Enable wgpu rendering with the given setup. #[cfg(feature = "wgpu")] pub fn wgpu_setup(self, setup: egui_wgpu::WgpuSetup) -> Self { - self.renderer(WgpuTestRenderer::from_setup(setup)) + self.renderer(crate::wgpu::WgpuTestRenderer::from_setup(setup)) } /// Create a new Harness with the given app closure and a state. diff --git a/crates/egui_kittest/src/lib.rs b/crates/egui_kittest/src/lib.rs index fdb31372c..661cb92c3 100644 --- a/crates/egui_kittest/src/lib.rs +++ b/crates/egui_kittest/src/lib.rs @@ -402,6 +402,7 @@ impl<'a, State> Harness<'a, State> { /// /// # Errors /// Returns an error if the rendering fails. + #[cfg(any(feature = "wgpu", feature = "snapshot"))] pub fn render(&mut self) -> Result { self.renderer.render(&self.ctx, &self.output) } diff --git a/crates/egui_kittest/src/renderer.rs b/crates/egui_kittest/src/renderer.rs index cbd789a0c..0806c4ead 100644 --- a/crates/egui_kittest/src/renderer.rs +++ b/crates/egui_kittest/src/renderer.rs @@ -1,5 +1,4 @@ -use egui::{Context, FullOutput, TexturesDelta}; -use image::RgbaImage; +use egui::TexturesDelta; pub trait TestRenderer { /// We use this to pass the glow / wgpu render state to [`eframe::Frame`]. @@ -13,7 +12,12 @@ pub trait TestRenderer { /// /// # Errors /// Returns an error if the rendering fails. - fn render(&mut self, ctx: &Context, output: &FullOutput) -> Result; + #[cfg(any(feature = "wgpu", feature = "snapshot"))] + fn render( + &mut self, + ctx: &egui::Context, + output: &egui::FullOutput, + ) -> Result; } /// A lazy renderer that initializes the renderer on the first render call. @@ -58,7 +62,12 @@ impl TestRenderer for LazyRenderer { } } - fn render(&mut self, ctx: &Context, output: &FullOutput) -> Result { + #[cfg(any(feature = "wgpu", feature = "snapshot"))] + fn render( + &mut self, + ctx: &egui::Context, + output: &egui::FullOutput, + ) -> Result { match self { Self::Uninitialized { texture_ops, From 5b740f97acc3e0c24023609948ceeac415ee1c10 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 21 Jan 2025 11:56:45 +0100 Subject: [PATCH 020/132] Remove `egui::special_emojis::TWITTER` (#5622) Twitter is gone, as is its icon. Also, fuck Elon Musk Find me on https://bsky.app/profile/ernerfeldt.bsky.social --- Cargo.toml | 2 +- RELEASES.md | 4 ++-- crates/egui/src/lib.rs | 3 --- crates/egui_demo_lib/src/demo/about.rs | 6 +++--- crates/egui_demo_lib/src/demo/demo_app_windows.rs | 12 ++++++------ crates/egui_demo_lib/src/demo/font_book.rs | 1 - 6 files changed, 12 insertions(+), 16 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3ce59e668..b8a58df8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -114,6 +114,7 @@ rust_2018_idioms = { level = "warn", priority = -1 } rust_2021_prelude_collisions = "warn" semicolon_in_expressions_from_macros = "warn" trivial_numeric_casts = "warn" +unexpected_cfgs = "warn" unsafe_op_in_unsafe_fn = "warn" # `unsafe_op_in_unsafe_fn` may become the default in future Rust versions: https://github.com/rust-lang/rust/issues/71668 unused_extern_crates = "warn" unused_import_braces = "warn" @@ -203,7 +204,6 @@ match_same_arms = "warn" match_wild_err_arm = "warn" match_wildcard_for_single_variants = "warn" mem_forget = "warn" -mismatched_target_os = "warn" mismatching_type_param_order = "warn" missing_enforced_import_renames = "warn" missing_errors_doc = "warn" diff --git a/RELEASES.md b/RELEASES.md index 79bf23bc6..a96b35be0 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -52,7 +52,7 @@ We don't update the MSRV in a patch release, unless we really, really need to. ## Preparation * [ ] run `scripts/generate_example_screenshots.sh` if needed * [ ] write a short release note that fits in a tweet -* [ ] record gif for `CHANGELOG.md` release note (and later twitter post) +* [ ] record gif for `CHANGELOG.md` release note (and later bluesky post) * [ ] update changelogs using `scripts/generate_changelog.py --version 0.x.0 --write` * [ ] bump version numbers in workspace `Cargo.toml` @@ -87,7 +87,7 @@ I usually do this all on the `master` branch, but doing it in a release branch i ``` ## Announcements -* [ ] [twitter](https://x.com/ernerfeldt/status/1772665412225823105) +* [ ] [Bluesky](https://bsky.app/profile/ernerfeldt.bsky.social) * [ ] egui discord * [ ] [r/rust](https://www.reddit.com/r/rust/comments/1bocr5s/announcing_egui_027_with_improved_menus_and/) * [ ] [r/programming](https://www.reddit.com/r/programming/comments/1bocsf6/announcing_egui_027_an_easytouse_crossplatform/) diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 8d0858425..ccd48e860 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -625,9 +625,6 @@ pub mod special_emojis { /// The Github logo. pub const GITHUB: char = ''; - /// The Twitter bird. - pub const TWITTER: char = ''; - /// The word `git`. pub const GIT: char = ''; diff --git a/crates/egui_demo_lib/src/demo/about.rs b/crates/egui_demo_lib/src/demo/about.rs index 22fa70635..7c980e214 100644 --- a/crates/egui_demo_lib/src/demo/about.rs +++ b/crates/egui_demo_lib/src/demo/about.rs @@ -98,14 +98,14 @@ fn about_immediate_mode(ui: &mut egui::Ui) { } fn links(ui: &mut egui::Ui) { - use egui::special_emojis::{GITHUB, TWITTER}; + use egui::special_emojis::GITHUB; ui.hyperlink_to( format!("{GITHUB} github.com/emilk/egui"), "https://github.com/emilk/egui", ); ui.hyperlink_to( - format!("{TWITTER} @ernerfeldt"), - "https://twitter.com/ernerfeldt", + "@ernerfeldt.bsky.social", + "https://bsky.app/profile/ernerfeldt.bsky.social", ); ui.hyperlink_to("📓 egui documentation", "https://docs.rs/egui/"); } 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 64d4d7809..b74eb7db3 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -237,10 +237,10 @@ impl DemoWindows { }); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - use egui::special_emojis::{GITHUB, TWITTER}; + use egui::special_emojis::GITHUB; ui.hyperlink_to( - egui::RichText::new(TWITTER).size(font_size), - "https://twitter.com/ernerfeldt", + egui::RichText::new("🦋").size(font_size), + "https://bsky.app/profile/ernerfeldt.bsky.social", ); ui.hyperlink_to( egui::RichText::new(GITHUB).size(font_size), @@ -264,14 +264,14 @@ impl DemoWindows { ui.separator(); - use egui::special_emojis::{GITHUB, TWITTER}; + use egui::special_emojis::GITHUB; ui.hyperlink_to( format!("{GITHUB} egui on GitHub"), "https://github.com/emilk/egui", ); ui.hyperlink_to( - format!("{TWITTER} @ernerfeldt"), - "https://twitter.com/ernerfeldt", + "@ernerfeldt.bsky.social", + "https://bsky.app/profile/ernerfeldt.bsky.social", ); ui.separator(); diff --git a/crates/egui_demo_lib/src/demo/font_book.rs b/crates/egui_demo_lib/src/demo/font_book.rs index 352bc0273..605b7ed7f 100644 --- a/crates/egui_demo_lib/src/demo/font_book.rs +++ b/crates/egui_demo_lib/src/demo/font_book.rs @@ -200,7 +200,6 @@ fn special_char_name(chr: char) -> Option<&'static str> { '\u{E600}' => Some("web-dribbble"), '\u{E601}' => Some("web-stackoverflow"), '\u{E602}' => Some("web-vimeo"), - '\u{E603}' => Some("web-twitter"), '\u{E604}' => Some("web-facebook"), '\u{E605}' => Some("web-googleplus"), '\u{E606}' => Some("web-pinterest"), From 493d5d0982765b99f5729780569421ac72f8b2f1 Mon Sep 17 00:00:00 2001 From: Matthias Kronberg Date: Wed, 22 Jan 2025 13:17:51 +0100 Subject: [PATCH 021/132] Fix hovering through custom menu button (#5555) This change discards widgets which are fully covered by another widget in a higher layer from the hit test algorithm. * Closes * [x] I have followed the instructions in the PR template --------- Co-authored-by: Emil Ernerfeldt --- crates/egui/src/hit_test.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/hit_test.rs b/crates/egui/src/hit_test.rs index 81c8abbb8..910e558b9 100644 --- a/crates/egui/src/hit_test.rs +++ b/crates/egui/src/hit_test.rs @@ -2,7 +2,7 @@ use ahash::HashMap; use emath::TSTransform; -use crate::{ahash, emath, LayerId, Pos2, Rect, Sense, WidgetRect, WidgetRects}; +use crate::{ahash, emath, id::IdSet, LayerId, Pos2, Rect, Sense, WidgetRect, WidgetRects}; /// Result of a hit-test against [`WidgetRects`]. /// @@ -133,6 +133,23 @@ pub fn hit_test( } } + // Find widgets which are hidden behind another widget and discard them. + // This is the case when a widget fully contains another widget and is on a different layer. + // It prevents "hovering through" widgets when there is a clickable widget behind. + + let mut hidden = IdSet::default(); + for (i, current) in close.iter().enumerate().rev() { + for next in &close[i + 1..] { + if next.interact_rect.contains_rect(current.interact_rect) + && current.layer_id != next.layer_id + { + hidden.insert(current.id); + } + } + } + + close.retain(|c| !hidden.contains(&c.id)); + let mut hits = hit_test_on_close(&close, pos); hits.contains_pointer = close From 71f7bdc9194b8993cf0e88788eb80a4d9badd198 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 22 Jan 2025 13:18:02 +0100 Subject: [PATCH 022/132] Implement `nohash_hasher::IsEnabled` for `Id` (#5628) `egui::id::IdSet` and `egui::id::IdMap` were already optimized to not do additional hashing (because the `Id` already is a hash), but now they are just type aliases for `nohash_hasher::IntSet/IntMap`. See https://crates.io/crates/nohash-hasher for more --- crates/egui/src/id.rs | 75 ++--------------------------------- crates/egui/src/pass_state.rs | 4 +- 2 files changed, 6 insertions(+), 73 deletions(-) diff --git a/crates/egui/src/id.rs b/crates/egui/src/id.rs index c5047c266..ddc1a59b7 100644 --- a/crates/egui/src/id.rs +++ b/crates/egui/src/id.rs @@ -33,6 +33,8 @@ use std::num::NonZeroU64; #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct Id(NonZeroU64); +impl nohash_hasher::IsEnabled for Id {} + impl Id { /// A special [`Id`], in particular as a key to [`crate::Memory::data`] /// for when there is no particular widget to attach the data. @@ -112,77 +114,8 @@ fn id_size() { // ---------------------------------------------------------------------------- -// Idea taken from the `nohash_hasher` crate. -#[derive(Default)] -pub struct IdHasher(u64); - -impl std::hash::Hasher for IdHasher { - fn write(&mut self, _: &[u8]) { - unreachable!("Invalid use of IdHasher"); - } - - fn write_u8(&mut self, _n: u8) { - unreachable!("Invalid use of IdHasher"); - } - - fn write_u16(&mut self, _n: u16) { - unreachable!("Invalid use of IdHasher"); - } - - fn write_u32(&mut self, _n: u32) { - unreachable!("Invalid use of IdHasher"); - } - - #[inline(always)] - fn write_u64(&mut self, n: u64) { - self.0 = n; - } - - fn write_usize(&mut self, _n: usize) { - unreachable!("Invalid use of IdHasher"); - } - - fn write_i8(&mut self, _n: i8) { - unreachable!("Invalid use of IdHasher"); - } - - fn write_i16(&mut self, _n: i16) { - unreachable!("Invalid use of IdHasher"); - } - - fn write_i32(&mut self, _n: i32) { - unreachable!("Invalid use of IdHasher"); - } - - fn write_i64(&mut self, _n: i64) { - unreachable!("Invalid use of IdHasher"); - } - - fn write_isize(&mut self, _n: isize) { - unreachable!("Invalid use of IdHasher"); - } - - #[inline(always)] - fn finish(&self) -> u64 { - self.0 - } -} - -#[derive(Copy, Clone, Debug, Default)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct BuildIdHasher {} - -impl std::hash::BuildHasher for BuildIdHasher { - type Hasher = IdHasher; - - #[inline(always)] - fn build_hasher(&self) -> IdHasher { - IdHasher::default() - } -} - /// `IdSet` is a `HashSet` optimized by knowing that [`Id`] has good entropy, and doesn't need more hashing. -pub type IdSet = std::collections::HashSet; +pub type IdSet = nohash_hasher::IntSet; /// `IdMap` is a `HashMap` optimized by knowing that [`Id`] has good entropy, and doesn't need more hashing. -pub type IdMap = std::collections::HashMap; +pub type IdMap = nohash_hasher::IntMap; diff --git a/crates/egui/src/pass_state.rs b/crates/egui/src/pass_state.rs index 5501220f7..1cca5becc 100644 --- a/crates/egui/src/pass_state.rs +++ b/crates/egui/src/pass_state.rs @@ -1,4 +1,4 @@ -use ahash::{HashMap, HashSet}; +use ahash::HashMap; use crate::{id::IdSet, style, Align, Id, IdMap, LayerId, Rangef, Rect, Vec2, WidgetRects}; @@ -34,7 +34,7 @@ pub struct PerLayerState { /// Is there any open popup (menus, combo-boxes, etc)? /// /// Does NOT include tooltips. - pub open_popups: HashSet, + pub open_popups: IdSet, /// Which widget is showing a tooltip (if any)? /// From a04e25af63dac2cdd40185a9e4ab44bae144033d Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Wed, 22 Jan 2025 15:13:34 +0100 Subject: [PATCH 023/132] Prepare for `objc2` frameworks v0.3 (#5624) The next version of the `objc2` framework crates will have a bunch of default features enabled, see https://github.com/madsmtm/objc2/issues/627, so this PR pre-emptively disables them, so that your compile times down blow up once you upgrade to the next version (which is yet to be released, but will be soon). * [x] I have followed the instructions in the PR template --- crates/eframe/Cargo.toml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/eframe/Cargo.toml b/crates/eframe/Cargo.toml index f2384b878..21f81d9e3 100644 --- a/crates/eframe/Cargo.toml +++ b/crates/eframe/Cargo.toml @@ -177,12 +177,14 @@ wgpu = { workspace = true, optional = true, features = [ # mac: [target.'cfg(any(target_os = "macos"))'.dependencies] objc2 = "0.5.1" -objc2-foundation = { version = "0.2.0", features = [ +objc2-foundation = { version = "0.2.0", default-features = false, features = [ + "std", "block2", "NSData", "NSString", ] } -objc2-app-kit = { version = "0.2.0", features = [ +objc2-app-kit = { version = "0.2.0", default-features = false, features = [ + "std", "NSApplication", "NSImage", "NSMenu", From e53bb53795f082cfe955622bf6e15c1526598c5c Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Wed, 22 Jan 2025 15:16:50 +0100 Subject: [PATCH 024/132] Update `egui_default_fonts` license (#5361) Ubuntu-font-1.0 is now in SPDX Reference: https://spdx.org/licenses/Ubuntu-font-1.0.html Reference: https://github.com/aboutcode-org/scancode-toolkit/blob/824163fa6fc3fff25e455eaae110019e683fa442/src/licensedcode/data/licenses/ubuntu-font-1.0.LICENSE#L11 Signed-off-by: Philippe Ombredanne --- crates/epaint_default_fonts/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/epaint_default_fonts/Cargo.toml b/crates/epaint_default_fonts/Cargo.toml index 8d31507d3..9e71d390c 100644 --- a/crates/epaint_default_fonts/Cargo.toml +++ b/crates/epaint_default_fonts/Cargo.toml @@ -6,7 +6,7 @@ description = "Default fonts for use in epaint / egui" edition.workspace = true rust-version.workspace = true homepage = "https://github.com/emilk/egui/tree/master/crates/epaint_default_fonts" -license = "(MIT OR Apache-2.0) AND OFL-1.1 AND LicenseRef-UFL-1.0" # OFL and UFL are from the font files themselves. +license = "(MIT OR Apache-2.0) AND OFL-1.1 AND Ubuntu-font-1.0" # OFL and UFL are from the font files themselves. readme = "README.md" repository = "https://github.com/emilk/egui/tree/master/crates/epaint_default_fonts" categories = ["graphics", "gui"] From bdf7bfddd730d9178458a7014206e95d832788ce Mon Sep 17 00:00:00 2001 From: n4n5 <56606507+Its-Just-Nans@users.noreply.github.com> Date: Wed, 22 Jan 2025 15:20:06 +0100 Subject: [PATCH 025/132] Add keys for `!`, `{`, `}` (#5548) * Help on https://github.com/emilk/egui/issues/3653 * [x] I have followed the instructions in the PR template --- crates/egui/src/data/key.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/crates/egui/src/data/key.rs b/crates/egui/src/data/key.rs index a075b025a..602ec5bda 100644 --- a/crates/egui/src/data/key.rs +++ b/crates/egui/src/data/key.rs @@ -49,12 +49,21 @@ pub enum Key { /// `?` Questionmark, + // '!' + Exclamationmark, + // `[` OpenBracket, // `]` CloseBracket, + // `{` + OpenCurlyBracket, + + // `}` + CloseCurlyBracket, + /// Also known as "backquote" or "grave" Backtick, @@ -215,11 +224,14 @@ impl Key { Self::Semicolon, Self::OpenBracket, Self::CloseBracket, + Self::OpenCurlyBracket, + Self::CloseCurlyBracket, Self::Backtick, Self::Backslash, Self::Slash, Self::Pipe, Self::Questionmark, + Self::Exclamationmark, Self::Quote, // Digits: Self::Num0, @@ -341,8 +353,11 @@ impl Key { "/" | "Slash" => Self::Slash, "|" | "Pipe" => Self::Pipe, "?" | "Questionmark" => Self::Questionmark, + "!" | "Exclamationmark" => Self::Exclamationmark, "[" | "OpenBracket" => Self::OpenBracket, "]" | "CloseBracket" => Self::CloseBracket, + "{" | "OpenCurlyBracket" => Self::OpenCurlyBracket, + "}" | "CloseCurlyBracket" => Self::CloseCurlyBracket, "`" | "Backtick" | "Backquote" | "Grave" => Self::Backtick, "'" | "Quote" => Self::Quote, @@ -446,8 +461,11 @@ impl Key { Self::Slash => "/", Self::Pipe => "|", Self::Questionmark => "?", + Self::Exclamationmark => "!", Self::OpenBracket => "[", Self::CloseBracket => "]", + Self::OpenCurlyBracket => "{", + Self::CloseCurlyBracket => "}", Self::Backtick => "`", _ => self.name(), @@ -490,8 +508,11 @@ impl Key { Self::Slash => "Slash", Self::Pipe => "Pipe", Self::Questionmark => "Questionmark", + Self::Exclamationmark => "Exclamationmark", Self::OpenBracket => "OpenBracket", Self::CloseBracket => "CloseBracket", + Self::OpenCurlyBracket => "OpenCurlyBracket", + Self::CloseCurlyBracket => "CloseCurlyBracket", Self::Backtick => "Backtick", Self::Quote => "Quote", From 97bdb2851cc2fc5341e069a5076c0822da4b7b4b Mon Sep 17 00:00:00 2001 From: Joshua Holmes <91363480+joshua-holmes@users.noreply.github.com> Date: Wed, 22 Jan 2025 06:28:23 -0800 Subject: [PATCH 026/132] Remove references to glium (#5626) * Remove references to `glium` backend, because it is deprecated since egui v0.18.0 * [x] I have followed the instructions in the PR template --- crates/eframe/CHANGELOG.md | 2 +- crates/eframe/src/native/file_storage.rs | 2 +- crates/egui_demo_lib/Cargo.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/eframe/CHANGELOG.md b/crates/eframe/CHANGELOG.md index cbb6cf411..0ddfa4b0e 100644 --- a/crates/eframe/CHANGELOG.md +++ b/crates/eframe/CHANGELOG.md @@ -1,7 +1,7 @@ # Changelog for eframe All notable changes to the `eframe` crate. -NOTE: [`egui-winit`](../egui-winit/CHANGELOG.md), [`egui_glium`](../egui_glium/CHANGELOG.md), [`egui_glow`](../egui_glow/CHANGELOG.md),and [`egui-wgpu`](../egui-wgpu/CHANGELOG.md) have their own changelogs! +NOTE: [`egui-winit`](../egui-winit/CHANGELOG.md), [`egui_glow`](../egui_glow/CHANGELOG.md),and [`egui-wgpu`](../egui-wgpu/CHANGELOG.md) have their own changelogs! This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. diff --git a/crates/eframe/src/native/file_storage.rs b/crates/eframe/src/native/file_storage.rs index 3cb338517..7fde2d288 100644 --- a/crates/eframe/src/native/file_storage.rs +++ b/crates/eframe/src/native/file_storage.rs @@ -96,7 +96,7 @@ fn roaming_appdata() -> Option { // ---------------------------------------------------------------------------- /// A key-value store backed by a [RON](https://github.com/ron-rs/ron) file on disk. -/// Used to restore egui state, glium window position/size and app state. +/// Used to restore egui state, glow window position/size and app state. pub struct FileStorage { ron_filepath: PathBuf, kv: HashMap, diff --git a/crates/egui_demo_lib/Cargo.toml b/crates/egui_demo_lib/Cargo.toml index 047ed3059..0e0299f11 100644 --- a/crates/egui_demo_lib/Cargo.toml +++ b/crates/egui_demo_lib/Cargo.toml @@ -10,7 +10,7 @@ license.workspace = true readme = "README.md" repository = "https://github.com/emilk/egui/tree/master/crates/egui_demo_lib" categories = ["gui", "graphics"] -keywords = ["glium", "egui", "gui", "gamedev"] +keywords = ["glow", "egui", "gui", "gamedev"] include = [ "../LICENSE-APACHE", "../LICENSE-MIT", From 712941828956598ebf9738bcdd27b6e4bafa9332 Mon Sep 17 00:00:00 2001 From: Yerkebulan Tulibergenov Date: Wed, 22 Jan 2025 06:28:38 -0800 Subject: [PATCH 027/132] Use Python 3 in `scripts/lint.py` (#5617) * [x] I have followed the instructions in the PR template ## Changes - Use Python 3 in `scripts/lint.py`. ## Why Some modern OS and distributions do not provide Python 2 by default. This includes recent macOS versions. I see that `scripts/generate_changelog.py` already uses Python 3, so I don't think this change should be controversial. Without this change, I am unable to run `./scripts/check.sh` on macOS 15.2 without installing Python 2 or adding alias for `python` executable. --- scripts/lint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/lint.py b/scripts/lint.py index 221d980d0..4939d735f 100755 --- a/scripts/lint.py +++ b/scripts/lint.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 """ Runs custom linting on Rust code. """ From 2815ff5a0684724a67e612a97805501dbc32fdf3 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 23 Jan 2025 10:57:40 +0100 Subject: [PATCH 028/132] Allow `Ubuntu-font-1.0` license --- deny.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/deny.toml b/deny.toml index 4db5246a7..a2b861fdf 100644 --- a/deny.toml +++ b/deny.toml @@ -85,6 +85,7 @@ allow = [ "MPL-2.0", # https://www.mozilla.org/en-US/MPL/2.0/FAQ/ - see Q11. Used by webpki-roots on Linux. "OFL-1.1", # https://spdx.org/licenses/OFL-1.1.html "OpenSSL", # https://www.openssl.org/source/license.html - used on Linux + "Ubuntu-font-1.0", # https://ubuntu.com/legal/font-licence "Unicode-3.0", # https://www.unicode.org/license.txt "Unicode-DFS-2016", # https://spdx.org/licenses/Unicode-DFS-2016.html "Zlib", # https://tldrlegal.com/license/zlib-libpng-license-(zlib) From 304c6518e396be5d58973707740536b80c189aad Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 23 Jan 2025 11:07:51 +0100 Subject: [PATCH 029/132] Update cargo-deny-action to v2 (#5632) --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 18bcbc659..f0e3b3a72 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -153,7 +153,7 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - - uses: EmbarkStudios/cargo-deny-action@v1 + - uses: EmbarkStudios/cargo-deny-action@v2 with: rust-version: "1.81.0" log-level: error From 6680e9c079b85c27bbd16873c8b92495de8de320 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 23 Jan 2025 12:11:29 +0100 Subject: [PATCH 030/132] Web: Fix incorrect scale when moving to screen with new DPI (#5631) * Closes https://github.com/emilk/egui/issues/5246 Tested on * [x] Chromium * [x] Firefox * [x] Safari On Chromium and Firefox we get one annoying frame with the wrong size, which can mess up the layout of egui apps, but this PR is still a huge improvement, and I don't want to spend more time on this right now. --- crates/eframe/Cargo.toml | 1 + crates/eframe/src/web/app_runner.rs | 14 ++- crates/eframe/src/web/events.rs | 172 +++++++++++++++++++++------- crates/eframe/src/web/mod.rs | 13 ++- crates/eframe/src/web/web_runner.rs | 101 +++++++++------- 5 files changed, 212 insertions(+), 89 deletions(-) diff --git a/crates/eframe/Cargo.toml b/crates/eframe/Cargo.toml index 21f81d9e3..883ec7d59 100644 --- a/crates/eframe/Cargo.toml +++ b/crates/eframe/Cargo.toml @@ -211,6 +211,7 @@ percent-encoding = "2.1" wasm-bindgen.workspace = true wasm-bindgen-futures.workspace = true web-sys = { workspace = true, features = [ + "AddEventListenerOptions", "BinaryType", "Blob", "BlobPropertyBag", diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index 6d11069f8..76ae1761f 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -59,7 +59,7 @@ impl AppRunner { egui_ctx.options_mut(|o| { // On web by default egui follows the zoom factor of the browser, - // and lets the browser handle the zoom shortscuts. + // and lets the browser handle the zoom shortcuts. // A user can still zoom egui separately by calling [`egui::Context::set_zoom_factor`]. o.zoom_with_keyboard = false; o.zoom_factor = 1.0; @@ -216,6 +216,18 @@ impl AppRunner { let canvas_size = super::canvas_size_in_points(self.canvas(), self.egui_ctx()); let mut raw_input = self.input.new_frame(canvas_size); + if super::DEBUG_RESIZE { + log::info!( + "egui running at canvas size: {}x{}, DPR: {}, zoom_factor: {}. egui size: {}x{} points", + self.canvas().width(), + self.canvas().height(), + super::native_pixels_per_point(), + self.egui_ctx.zoom_factor(), + canvas_size.x, + canvas_size.y, + ); + } + self.app.raw_input_hook(&self.egui_ctx, &mut raw_input); let full_output = self.egui_ctx.run(raw_input, |egui_ctx| { diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index 414e5be23..733e2e561 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -1,10 +1,14 @@ +use web_sys::EventTarget; + +use crate::web::string_from_js_value; + use super::{ button_from_mouse_event, location_hash, modifiers_from_kb_event, modifiers_from_mouse_event, - modifiers_from_wheel_event, 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, + 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, + DEBUG_RESIZE, }; -use web_sys::EventTarget; // TODO(emilk): there are more calls to `prevent_default` and `stop_propagation` // than what is probably needed. @@ -363,10 +367,17 @@ fn install_window_events(runner_ref: &WebRunner, window: &EventTarget) -> Result runner.save(); })?; - // NOTE: resize is handled by `ResizeObserver` below + // We want to handle the case of dragging the browser from one monitor to another, + // which can cause the DPR to change without any resize event (e.g. Safari). + install_dpr_change_event(runner_ref)?; + + // No need to subscribe to "resize": we already subscribe to the canvas + // size using a ResizeObserver, and we also subscribe to DPR changes of the monitor. for event_name in &["load", "pagehide", "pageshow"] { runner_ref.add_event_listener(window, event_name, move |_: web_sys::Event, runner| { - // log::debug!("{event_name:?}"); + if DEBUG_RESIZE { + log::debug!("{event_name:?}"); + } runner.needs_repaint.repaint_asap(); })?; } @@ -380,6 +391,48 @@ fn install_window_events(runner_ref: &WebRunner, window: &EventTarget) -> Result Ok(()) } +fn install_dpr_change_event(web_runner: &WebRunner) -> Result<(), JsValue> { + let original_dpr = native_pixels_per_point(); + + let window = web_sys::window().unwrap(); + let Some(media_query_list) = + window.match_media(&format!("(resolution: {original_dpr}dppx)"))? + else { + log::error!( + "Failed to create MediaQueryList: eframe won't be able to detect changes in DPR" + ); + return Ok(()); + }; + + let closure = move |_: web_sys::Event, app_runner: &mut AppRunner, web_runner: &WebRunner| { + let new_dpr = native_pixels_per_point(); + log::debug!("Device Pixel Ratio changed from {original_dpr} to {new_dpr}"); + + if true { + // Explicitly resize canvas to match the new DPR. + // This is a bit ugly, but I haven't found a better way to do it. + let canvas = app_runner.canvas(); + canvas.set_width((canvas.width() as f32 * new_dpr / original_dpr).round() as _); + canvas.set_height((canvas.height() as f32 * new_dpr / original_dpr).round() as _); + log::debug!("Resized canvas to {}x{}", canvas.width(), canvas.height()); + } + + // It may be tempting to call `resize_observer.observe(&canvas)` here, + // but unfortunately this has no effect. + + if let Err(err) = install_dpr_change_event(web_runner) { + log::error!( + "Failed to install DPR change event: {}", + string_from_js_value(&err) + ); + } + }; + + let options = web_sys::AddEventListenerOptions::default(); + options.set_once(true); + web_runner.add_event_listener_ex(&media_query_list, "change", &options, closure) +} + fn install_color_scheme_change_event( runner_ref: &WebRunner, window: &web_sys::Window, @@ -813,53 +866,79 @@ fn install_drag_and_drop(runner_ref: &WebRunner, target: &EventTarget) -> Result Ok(()) } -/// Install a `ResizeObserver` to observe changes to the size of the canvas. -/// -/// This is the only way to ensure a canvas size change without an associated window `resize` event -/// actually results in a resize of the canvas. +/// A `ResizeObserver` is used to observe changes to the size of the canvas. /// /// The resize observer is called the by the browser at `observe` time, instead of just on the first actual resize. /// We use that to trigger the first `request_animation_frame` _after_ updating the size of the canvas to the correct dimensions, /// to avoid [#4622](https://github.com/emilk/egui/issues/4622). -pub(crate) fn install_resize_observer(runner_ref: &WebRunner) -> Result<(), JsValue> { - let closure = Closure::wrap(Box::new({ - let runner_ref = runner_ref.clone(); - move |entries: js_sys::Array| { - // Only call the wrapped closure if the egui code has not panicked - if let Some(mut runner_lock) = runner_ref.try_lock() { - let canvas = runner_lock.canvas(); - let (width, height) = match get_display_size(&entries) { - Ok(v) => v, - Err(err) => { - log::error!("{}", super::string_from_js_value(&err)); - return; +pub struct ResizeObserverContext { + observer: web_sys::ResizeObserver, + + // Kept so it is not dropped until we are done with it. + _closure: Closure, +} + +impl Drop for ResizeObserverContext { + fn drop(&mut self) { + self.observer.disconnect(); + } +} + +impl ResizeObserverContext { + pub fn new(runner_ref: &WebRunner) -> Result { + let closure = Closure::wrap(Box::new({ + let runner_ref = runner_ref.clone(); + move |entries: js_sys::Array| { + if DEBUG_RESIZE { + // log::info!("ResizeObserverContext callback"); + } + // Only call the wrapped closure if the egui code has not panicked + if let Some(mut runner_lock) = runner_ref.try_lock() { + let canvas = runner_lock.canvas(); + let (width, height) = match get_display_size(&entries) { + Ok(v) => v, + Err(err) => { + log::error!("{}", super::string_from_js_value(&err)); + return; + } + }; + if DEBUG_RESIZE { + log::info!( + "ResizeObserver: new canvas size: {width}x{height}, DPR: {}", + web_sys::window().unwrap().device_pixel_ratio() + ); } - }; - canvas.set_width(width); - canvas.set_height(height); + canvas.set_width(width); + canvas.set_height(height); - // force an immediate repaint - runner_lock.needs_repaint.repaint_asap(); - paint_if_needed(&mut runner_lock); - drop(runner_lock); - // we rely on the resize observer to trigger the first `request_animation_frame`: - if let Err(err) = runner_ref.request_animation_frame() { - log::error!("{}", super::string_from_js_value(&err)); - }; + // force an immediate repaint + runner_lock.needs_repaint.repaint_asap(); + paint_if_needed(&mut runner_lock); + drop(runner_lock); + // we rely on the resize observer to trigger the first `request_animation_frame`: + if let Err(err) = runner_ref.request_animation_frame() { + log::error!("{}", super::string_from_js_value(&err)); + }; + } } - } - }) as Box); + }) as Box); - let observer = web_sys::ResizeObserver::new(closure.as_ref().unchecked_ref())?; - let options = web_sys::ResizeObserverOptions::new(); - options.set_box(web_sys::ResizeObserverBoxOptions::ContentBox); - if let Some(runner_lock) = runner_ref.try_lock() { - observer.observe_with_options(runner_lock.canvas(), &options); - drop(runner_lock); - runner_ref.set_resize_observer(observer, closure); + let observer = web_sys::ResizeObserver::new(closure.as_ref().unchecked_ref())?; + + Ok(Self { + observer, + _closure: closure, + }) } - Ok(()) + pub fn observe(&self, canvas: &web_sys::HtmlCanvasElement) { + if DEBUG_RESIZE { + log::info!("Calling observe on canvas…"); + } + let options = web_sys::ResizeObserverOptions::new(); + options.set_box(web_sys::ResizeObserverBoxOptions::ContentBox); + self.observer.observe_with_options(canvas, &options); + } } // Code ported to Rust from: @@ -878,6 +957,10 @@ fn get_display_size(resize_observer_entries: &js_sys::Array) -> Result<(u32, u32 width = size.inline_size(); height = size.block_size(); dpr = 1.0; // no need to apply + + if DEBUG_RESIZE { + // log::info!("devicePixelContentBoxSize {width}x{height}"); + } } else if JsValue::from_str("contentBoxSize").js_in(entry.as_ref()) { let content_box_size = entry.content_box_size(); let idx0 = content_box_size.at(0); @@ -892,6 +975,9 @@ fn get_display_size(resize_observer_entries: &js_sys::Array) -> Result<(u32, u32 width = size.inline_size(); height = size.block_size(); } + if DEBUG_RESIZE { + log::info!("contentBoxSize {width}x{height}"); + } } else { // legacy let content_rect = entry.content_rect(); diff --git a/crates/eframe/src/web/mod.rs b/crates/eframe/src/web/mod.rs index 911c453f2..3dc7d7f8b 100644 --- a/crates/eframe/src/web/mod.rs +++ b/crates/eframe/src/web/mod.rs @@ -51,6 +51,9 @@ use input::{ // ---------------------------------------------------------------------------- +/// Debug browser resizing? +const DEBUG_RESIZE: bool = false; + pub(crate) fn string_from_js_value(value: &JsValue) -> String { value.as_string().unwrap_or_else(|| format!("{value:#?}")) } @@ -152,7 +155,10 @@ fn canvas_content_rect(canvas: &web_sys::HtmlCanvasElement) -> egui::Rect { } fn canvas_size_in_points(canvas: &web_sys::HtmlCanvasElement, ctx: &egui::Context) -> egui::Vec2 { - let pixels_per_point = ctx.pixels_per_point(); + // ctx.pixels_per_point can be outdated + + let pixels_per_point = ctx.zoom_factor() * native_pixels_per_point(); + egui::vec2( canvas.width() as f32 / pixels_per_point, canvas.height() as f32 / pixels_per_point, @@ -352,3 +358,8 @@ pub fn percent_decode(s: &str) -> String { .decode_utf8_lossy() .to_string() } + +/// Are we running inside the Safari browser? +pub fn is_safari_browser() -> bool { + web_sys::window().is_some_and(|window| window.has_own_property(&JsValue::from("safari"))) +} diff --git a/crates/eframe/src/web/web_runner.rs b/crates/eframe/src/web/web_runner.rs index 6cbc371f3..80c9d021d 100644 --- a/crates/eframe/src/web/web_runner.rs +++ b/crates/eframe/src/web/web_runner.rs @@ -4,7 +4,11 @@ use wasm_bindgen::prelude::*; use crate::{epi, App}; -use super::{events, text_agent::TextAgent, AppRunner, PanicHandler}; +use super::{ + events::{self, ResizeObserverContext}, + text_agent::TextAgent, + AppRunner, PanicHandler, +}; /// This is how `eframe` runs your web application /// @@ -18,7 +22,7 @@ pub struct WebRunner { /// If we ever panic during running, this `RefCell` is poisoned. /// So before we use it, we need to check [`Self::panic_handler`]. - runner: Rc>>, + app_runner: Rc>>, /// In case of a panic, unsubscribe these. /// They have to be in a separate `Rc` so that we don't need to pass them to @@ -39,7 +43,7 @@ impl WebRunner { Self { panic_handler, - runner: Rc::new(RefCell::new(None)), + app_runner: Rc::new(RefCell::new(None)), events_to_unsubscribe: Rc::new(RefCell::new(Default::default())), frame: Default::default(), resize_observer: Default::default(), @@ -58,28 +62,33 @@ impl WebRunner { ) -> Result<(), JsValue> { self.destroy(); - let text_agent = TextAgent::attach(self)?; - - let runner = AppRunner::new(canvas, web_options, app_creator, text_agent).await?; - { // Make sure the canvas can be given focus. // https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex - runner.canvas().set_tab_index(0); + canvas.set_tab_index(0); // Don't outline the canvas when it has focus: - runner.canvas().style().set_property("outline", "none")?; + canvas.style().set_property("outline", "none")?; } - self.runner.replace(Some(runner)); + let text_agent = TextAgent::attach(self)?; { - events::install_event_handlers(self)?; + let resize_observer = events::ResizeObserverContext::new(self)?; - // The resize observer handles calling `request_animation_frame` to start the render loop. - events::install_resize_observer(self)?; + // This will (eventually) result in a `request_animation_frame` to start the render loop. + resize_observer.observe(&canvas); + + self.resize_observer.replace(Some(resize_observer)); } + { + let app_runner = AppRunner::new(canvas, web_options, app_creator, text_agent).await?; + self.app_runner.replace(Some(app_runner)); + } + + events::install_event_handlers(self)?; + Ok(()) } @@ -109,10 +118,7 @@ impl WebRunner { } } - if let Some(context) = self.resize_observer.take() { - context.resize_observer.disconnect(); - drop(context.closure); - } + self.resize_observer.replace(None); } /// Shut down eframe and clean up resources. @@ -124,7 +130,7 @@ impl WebRunner { window.cancel_animation_frame(frame.id).ok(); } - if let Some(runner) = self.runner.replace(None) { + if let Some(runner) = self.app_runner.replace(None) { runner.destroy(); } } @@ -138,7 +144,7 @@ impl WebRunner { self.unsubscribe_from_all_events(); None } else { - let lock = self.runner.try_borrow_mut().ok()?; + let lock = self.app_runner.try_borrow_mut().ok()?; std::cell::RefMut::filter_map(lock, |lock| -> Option<&mut AppRunner> { lock.as_mut() }) .ok() } @@ -166,20 +172,45 @@ impl WebRunner { event_name: &'static str, mut closure: impl FnMut(E, &mut AppRunner) + 'static, ) -> Result<(), wasm_bindgen::JsValue> { - let runner_ref = self.clone(); + let options = web_sys::AddEventListenerOptions::default(); + self.add_event_listener_ex( + target, + event_name, + &options, + move |event, app_runner, _web_runner| closure(event, app_runner), + ) + } + + /// Convenience function to reduce boilerplate and ensure that all event handlers + /// are dealt with in the same way. + /// + /// All events added with this method will automatically be unsubscribed on panic, + /// or when [`Self::destroy`] is called. + pub fn add_event_listener_ex( + &self, + target: &web_sys::EventTarget, + event_name: &'static str, + options: &web_sys::AddEventListenerOptions, + mut closure: impl FnMut(E, &mut AppRunner, &Self) + 'static, + ) -> Result<(), wasm_bindgen::JsValue> { + let web_runner = self.clone(); // Create a JS closure based on the FnMut provided let closure = Closure::wrap(Box::new(move |event: web_sys::Event| { // Only call the wrapped closure if the egui code has not panicked - if let Some(mut runner_lock) = runner_ref.try_lock() { + if let Some(mut runner_lock) = web_runner.try_lock() { // Cast the event to the expected event type let event = event.unchecked_into::(); - closure(event, &mut runner_lock); + closure(event, &mut runner_lock, &web_runner); } }) as Box); // Add the event listener to the target - target.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?; + target.add_event_listener_with_callback_and_add_event_listener_options( + event_name, + closure.as_ref().unchecked_ref(), + options, + )?; let handle = TargetEvent { target: target.clone(), @@ -208,13 +239,13 @@ impl WebRunner { let window = web_sys::window().unwrap(); let closure = Closure::once({ - let runner_ref = self.clone(); + let web_runner = self.clone(); move || { // We can paint now, so clear the animation frame. // This drops the `closure` and allows another // animation frame to be scheduled - let _ = runner_ref.frame.take(); - events::paint_and_schedule(&runner_ref) + let _ = web_runner.frame.take(); + events::paint_and_schedule(&web_runner) } }); @@ -226,19 +257,6 @@ impl WebRunner { Ok(()) } - - pub(crate) fn set_resize_observer( - &self, - resize_observer: web_sys::ResizeObserver, - closure: Closure, - ) { - self.resize_observer - .borrow_mut() - .replace(ResizeObserverContext { - resize_observer, - closure, - }); - } } // ---------------------------------------------------------------------------- @@ -253,11 +271,6 @@ struct AnimationFrameRequest { _closure: Closure Result<(), JsValue>>, } -struct ResizeObserverContext { - resize_observer: web_sys::ResizeObserver, - closure: Closure, -} - struct TargetEvent { target: web_sys::EventTarget, event_name: String, From edbf4e8998c218754e5d4fe2e42a890863981bbe Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 23 Jan 2025 12:17:20 +0100 Subject: [PATCH 031/132] clean up deny.toml --- deny.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/deny.toml b/deny.toml index a2b861fdf..674f14d16 100644 --- a/deny.toml +++ b/deny.toml @@ -79,7 +79,6 @@ allow = [ "BSL-1.0", # https://tldrlegal.com/license/boost-software-license-1.0-explained "CC0-1.0", # https://creativecommons.org/publicdomain/zero/1.0/ "ISC", # https://www.tldrlegal.com/license/isc-license - "LicenseRef-UFL-1.0", # no official SPDX, see https://github.com/emilk/egui/issues/2321 "MIT-0", # https://choosealicense.com/licenses/mit-0/ "MIT", # https://tldrlegal.com/license/mit-license "MPL-2.0", # https://www.mozilla.org/en-US/MPL/2.0/FAQ/ - see Q11. Used by webpki-roots on Linux. From bc5f908b80cbcb20a728b36d20df860d90a29263 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 23 Jan 2025 13:47:52 +0100 Subject: [PATCH 032/132] Fix initial paint of web app (#5633) * Broke in https://github.com/emilk/egui/pull/5631 (for _some_ apps) --- crates/eframe/src/web/events.rs | 4 +++- crates/eframe/src/web/web_runner.rs | 17 ++++++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index 733e2e561..7e2d07a1b 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -890,7 +890,7 @@ impl ResizeObserverContext { let runner_ref = runner_ref.clone(); move |entries: js_sys::Array| { if DEBUG_RESIZE { - // log::info!("ResizeObserverContext callback"); + log::info!("ResizeObserverContext callback"); } // Only call the wrapped closure if the egui code has not panicked if let Some(mut runner_lock) = runner_ref.try_lock() { @@ -919,6 +919,8 @@ impl ResizeObserverContext { if let Err(err) = runner_ref.request_animation_frame() { log::error!("{}", super::string_from_js_value(&err)); }; + } else { + log::warn!("ResizeObserverContext callback: failed to lock runner"); } } }) as Box); diff --git a/crates/eframe/src/web/web_runner.rs b/crates/eframe/src/web/web_runner.rs index 80c9d021d..9fea500bc 100644 --- a/crates/eframe/src/web/web_runner.rs +++ b/crates/eframe/src/web/web_runner.rs @@ -71,24 +71,27 @@ impl WebRunner { canvas.style().set_property("outline", "none")?; } - let text_agent = TextAgent::attach(self)?; + { + // First set up the app runner: + let text_agent = TextAgent::attach(self)?; + let app_runner = + AppRunner::new(canvas.clone(), web_options, app_creator, text_agent).await?; + self.app_runner.replace(Some(app_runner)); + } { let resize_observer = events::ResizeObserverContext::new(self)?; - // This will (eventually) result in a `request_animation_frame` to start the render loop. + // Properly size the canvas. Will also call `self.request_animation_frame()` (eventually) resize_observer.observe(&canvas); self.resize_observer.replace(Some(resize_observer)); } - { - let app_runner = AppRunner::new(canvas, web_options, app_creator, text_agent).await?; - self.app_runner.replace(Some(app_runner)); - } - events::install_event_handlers(self)?; + log::info!("event handlers installed."); + Ok(()) } From 93d214429491dfd628cd7331c9cef96d11a8738d Mon Sep 17 00:00:00 2001 From: Pandicon <70060103+Pandicon@users.noreply.github.com> Date: Mon, 27 Jan 2025 08:14:49 +0100 Subject: [PATCH 033/132] Save state on suspend on Android and iOS (#5601) This pull request fixes a subset of #5492 by saving the application state when the `suspended` event is received on Android. This way, even if the user exits the app and closes it manually right after changing some state, it will be saved since `suspended` gets fired when the app is exited. It does not fix the `on_exit` function not being fired - this seems to be a winit bug (the `exiting` function in the winit application handler trait is not called on exit). Once it gets fixed, it may be possible to remove logic introduced by this PR (however, I am not sure how it would handle the app being killed by the system when in the background, that would have to be tested). I've tested the logic by: * Leaving from the app to the home screen, then killing it from the "recent apps" menu * Leaving from the app to the "recent apps" menu and killing it * Restarting the device while the app was running In all of these instances, the state was saved (the last one being a pleasant surprise). It was tested on the repository mentioned in #5492 with my forked repository as the source for eframe (I unfortunately am not able to test it in a larger project of mine due to dependence on "3rd party" egui libraries (like egui_notify) which do not compile along with the master branch of eframe (different versions of egui), but I believe it should work in the same manner in all scenarios). Tests were conducted on a Galaxy Tab S8 running Android 14, One UI 6.1.1. CI passed on my fork. * [x] I have followed the instructions in the PR template --- crates/eframe/src/native/glow_integration.rs | 24 +++++++++++++--- crates/eframe/src/native/run.rs | 10 +++++++ crates/eframe/src/native/wgpu_integration.rs | 28 +++++++++++++++---- crates/eframe/src/native/winit_integration.rs | 5 ++++ 4 files changed, 58 insertions(+), 9 deletions(-) diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index 8e9323670..877245e22 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -366,6 +366,20 @@ impl WinitApp for GlowWinitApp<'_> { .and_then(|r| r.glutin.borrow().window_from_viewport.get(&id).copied()) } + fn save(&mut self) { + log::debug!("WinitApp::save called"); + if let Some(running) = self.running.as_mut() { + profiling::function_scope!(); + + // This is used because of the "save on suspend" logic on Android. Once the application is suspended, there is no window associated to it, which was causing panics when `.window().expect()` was used. + let window_opt = running.glutin.borrow().window_opt(ViewportId::ROOT); + + running + .integration + .save(running.app.as_mut(), window_opt.as_deref()); + } + } + fn save_and_destroy(&mut self) { if let Some(mut running) = self.running.take() { profiling::function_scope!(); @@ -413,7 +427,7 @@ impl WinitApp for GlowWinitApp<'_> { if let Some(running) = &mut self.running { running.glutin.borrow_mut().on_suspend()?; } - Ok(EventResult::Wait) + Ok(EventResult::Save) } fn device_event( @@ -1214,10 +1228,12 @@ impl GlutinWindowContext { .expect("viewport doesn't exist") } + fn window_opt(&self, viewport_id: ViewportId) -> Option> { + self.viewport(viewport_id).window.clone() + } + fn window(&self, viewport_id: ViewportId) -> Arc { - self.viewport(viewport_id) - .window - .clone() + self.window_opt(viewport_id) .expect("winit window doesn't exist") } diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs index e328877a4..fb02ac439 100644 --- a/crates/eframe/src/native/run.rs +++ b/crates/eframe/src/native/run.rs @@ -89,6 +89,7 @@ impl WinitAppWrapper { event_result: Result, ) { let mut exit = false; + let mut save = false; log::trace!("event_result: {event_result:?}"); @@ -126,6 +127,10 @@ impl WinitAppWrapper { ); Ok(event_result) } + EventResult::Save => { + save = true; + Ok(event_result) + } EventResult::Exit => { exit = true; Ok(event_result) @@ -139,6 +144,11 @@ impl WinitAppWrapper { self.return_result = Err(err); }; + if save { + log::debug!("Received an EventResult::Save - saving app state"); + self.winit_app.save(); + } + if exit { if self.run_and_return { log::debug!("Asking to exit event loop…"); diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index f93386f84..0e0fcbf5d 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -355,6 +355,13 @@ impl WinitApp for WgpuWinitApp<'_> { ) } + fn save(&mut self) { + log::debug!("WinitApp::save called"); + if let Some(running) = self.running.as_mut() { + running.save(); + } + } + fn save_and_destroy(&mut self) { if let Some(mut running) = self.running.take() { running.save_and_destroy(); @@ -415,7 +422,7 @@ impl WinitApp for WgpuWinitApp<'_> { fn suspended(&mut self, _: &ActiveEventLoop) -> crate::Result { #[cfg(target_os = "android")] self.drop_window()?; - Ok(EventResult::Wait) + Ok(EventResult::Save) } fn device_event( @@ -488,13 +495,23 @@ impl WinitApp for WgpuWinitApp<'_> { } impl WgpuWinitRunning<'_> { + /// Saves the application state + fn save(&mut self) { + let shared = self.shared.borrow(); + // This is done because of the "save on suspend" logic on Android. Once the application is suspended, there is no window associated to it. + let window = if let Some(Viewport { window, .. }) = shared.viewports.get(&ViewportId::ROOT) + { + window.as_deref() + } else { + None + }; + self.integration.save(self.app.as_mut(), window); + } + fn save_and_destroy(&mut self) { profiling::function_scope!(); - let mut shared = self.shared.borrow_mut(); - if let Some(Viewport { window, .. }) = shared.viewports.get(&ViewportId::ROOT) { - self.integration.save(self.app.as_mut(), window.as_deref()); - } + self.save(); #[cfg(feature = "glow")] self.app.on_exit(None); @@ -502,6 +519,7 @@ impl WgpuWinitRunning<'_> { #[cfg(not(feature = "glow"))] self.app.on_exit(); + let mut shared = self.shared.borrow_mut(); shared.painter.destroy(); } diff --git a/crates/eframe/src/native/winit_integration.rs b/crates/eframe/src/native/winit_integration.rs index 2b6c54a67..d85443f37 100644 --- a/crates/eframe/src/native/winit_integration.rs +++ b/crates/eframe/src/native/winit_integration.rs @@ -70,6 +70,8 @@ pub trait WinitApp { fn window_id_from_viewport_id(&self, id: ViewportId) -> Option; + fn save(&mut self); + fn save_and_destroy(&mut self); fn run_ui_and_paint( @@ -119,6 +121,9 @@ pub enum EventResult { RepaintAt(WindowId, Instant), + /// Causes a save of the client state when the persistence feature is enabled. + Save, + Exit, } From 37c564be2c903975a3739ad6dae5c3462ac5d402 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 28 Jan 2025 19:50:05 +0100 Subject: [PATCH 034/132] Remove unnecessary profiling scope --- crates/egui/src/context.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 44c807b06..e0459485d 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -3505,7 +3505,6 @@ impl Context { /// The loaders of bytes, images, and textures. pub fn loaders(&self) -> Arc { - profiling::function_scope!(); self.read(|this| this.loaders.clone()) } } From e8f351b729931db02e2076d81e8454063a2f9911 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jochen=20G=C3=B6rtler?= Date: Tue, 28 Jan 2025 20:06:10 +0100 Subject: [PATCH 035/132] Add `egui::Scene` for panning/zooming a `Ui` (#5505) This is similar to `ScrollArea`, but: * Supports zooming * Has no scroll bars * Has no limits on the scrolling ## TODO * [x] Automatic sizing of `Scene`s outer bounds * [x] Fix text selection in scenes * [x] Implement `fit_rect` * [x] Document / improve API --------- Co-authored-by: Emil Ernerfeldt --- crates/egui/src/containers/mod.rs | 2 + crates/egui/src/containers/scene.rs | 219 ++++++++++++++++++ crates/egui/src/containers/scroll_area.rs | 3 + .../egui/src/text_selection/accesskit_text.rs | 8 +- .../text_selection/label_text_selection.rs | 78 +++++-- .../src/text_selection/text_cursor_state.rs | 18 +- crates/egui/src/widgets/text_edit/builder.rs | 11 +- .../src/demo/demo_app_windows.rs | 2 +- crates/egui_demo_lib/src/demo/mod.rs | 2 +- crates/egui_demo_lib/src/demo/pan_zoom.rs | 145 ------------ crates/egui_demo_lib/src/demo/scene.rs | 82 +++++++ .../tests/snapshots/demos/Scene.png | 3 + crates/emath/src/ts_transform.rs | 7 +- 13 files changed, 391 insertions(+), 189 deletions(-) create mode 100644 crates/egui/src/containers/scene.rs delete mode 100644 crates/egui_demo_lib/src/demo/pan_zoom.rs create mode 100644 crates/egui_demo_lib/src/demo/scene.rs create mode 100644 crates/egui_demo_lib/tests/snapshots/demos/Scene.png diff --git a/crates/egui/src/containers/mod.rs b/crates/egui/src/containers/mod.rs index e68e0def1..abb444598 100644 --- a/crates/egui/src/containers/mod.rs +++ b/crates/egui/src/containers/mod.rs @@ -10,6 +10,7 @@ pub mod modal; pub mod panel; pub mod popup; pub(crate) mod resize; +mod scene; pub mod scroll_area; mod sides; pub(crate) mod window; @@ -23,6 +24,7 @@ pub use { panel::{CentralPanel, SidePanel, TopBottomPanel}, popup::*, resize::Resize, + scene::Scene, scroll_area::ScrollArea, sides::Sides, window::Window, diff --git a/crates/egui/src/containers/scene.rs b/crates/egui/src/containers/scene.rs new file mode 100644 index 000000000..5deeb01b6 --- /dev/null +++ b/crates/egui/src/containers/scene.rs @@ -0,0 +1,219 @@ +use core::f32; + +use emath::{GuiRounding, Pos2}; + +use crate::{ + emath::TSTransform, InnerResponse, LayerId, Rangef, Rect, Response, Sense, Ui, UiBuilder, Vec2, +}; + +/// Creates a transformation that fits a given scene rectangle into the available screen size. +/// +/// The resulting visual scene bounds can be larger, due to letterboxing. +/// +/// Returns the transformation from `scene` to `global` coordinates. +fn fit_to_rect_in_scene( + rect_in_global: Rect, + rect_in_scene: Rect, + zoom_range: Rangef, +) -> TSTransform { + // Compute the scale factor to fit the bounding rectangle into the available screen size: + let scale = rect_in_global.size() / rect_in_scene.size(); + + // Use the smaller of the two scales to ensure the whole rectangle fits on the screen: + let scale = scale.min_elem(); + + // Clamp scale to what is allowed + let scale = zoom_range.clamp(scale); + + // Compute the translation to center the bounding rect in the screen: + let center_in_global = rect_in_global.center().to_vec2(); + let center_scene = rect_in_scene.center().to_vec2(); + + // Set the transformation to scale and then translate to center. + TSTransform::from_translation(center_in_global - scale * center_scene) + * TSTransform::from_scaling(scale) +} + +/// A container that allows you to zoom and pan. +/// +/// This is similar to [`crate::ScrollArea`] but: +/// * Supports zooming +/// * Has no scroll bars +/// * Has no limits on the scrolling +#[derive(Clone, Debug)] +#[must_use = "You should call .show()"] +pub struct Scene { + zoom_range: Rangef, + max_inner_size: Vec2, +} + +impl Default for Scene { + fn default() -> Self { + Self { + zoom_range: Rangef::new(f32::EPSILON, 1.0), + max_inner_size: Vec2::splat(1000.0), + } + } +} + +impl Scene { + #[inline] + pub fn new() -> Self { + Default::default() + } + + /// Set the allowed zoom range. + /// + /// The default zoom range is `0.0..=1.0`, + /// which mean you zan make things arbitrarily small, but you cannot zoom in past a `1:1` ratio. + /// + /// If you want to allow zooming in, you can set the zoom range to `0.0..=f32::INFINITY`. + /// Note that text rendering becomes blurry when you zoom in: . + #[inline] + pub fn zoom_range(mut self, zoom_range: impl Into) -> Self { + self.zoom_range = zoom_range.into(); + self + } + + /// Set the maximum size of the inner [`Ui`] that will be created. + #[inline] + pub fn max_inner_size(mut self, max_inner_size: impl Into) -> Self { + self.max_inner_size = max_inner_size.into(); + self + } + + /// `scene_rect` contains the view bounds of the inner [`Ui`]. + /// + /// `scene_rect` will be mutated by any panning/zooming done by the user. + /// If `scene_rect` is somehow invalid (e.g. `Rect::ZERO`), + /// then it will be reset to the inner rect of the inner ui. + /// + /// You need to store the `scene_rect` in your state between frames. + pub fn show( + &self, + parent_ui: &mut Ui, + scene_rect: &mut Rect, + add_contents: impl FnOnce(&mut Ui) -> R, + ) -> InnerResponse { + let (outer_rect, _outer_response) = + parent_ui.allocate_exact_size(parent_ui.available_size_before_wrap(), Sense::hover()); + + let mut to_global = fit_to_rect_in_scene(outer_rect, *scene_rect, self.zoom_range); + + let scene_rect_was_good = + to_global.is_valid() && scene_rect.is_finite() && scene_rect.size() != Vec2::ZERO; + + let mut inner_rect = *scene_rect; + + let ret = self.show_global_transform(parent_ui, outer_rect, &mut to_global, |ui| { + let r = add_contents(ui); + inner_rect = ui.min_rect(); + r + }); + + if ret.response.changed() { + // Only update if changed, both to avoid numeric drift, + // and to avoid expanding the scene rect unnecessarily. + *scene_rect = to_global.inverse() * outer_rect; + } + + if !scene_rect_was_good { + // Auto-reset if the trsnsformation goes bad somehow (or started bad). + *scene_rect = inner_rect; + } + + ret + } + + fn show_global_transform( + &self, + parent_ui: &mut Ui, + outer_rect: Rect, + to_global: &mut TSTransform, + add_contents: impl FnOnce(&mut Ui) -> R, + ) -> InnerResponse { + // Create a new egui paint layer, where we can draw our contents: + let scene_layer_id = LayerId::new( + parent_ui.layer_id().order, + parent_ui.id().with("scene_area"), + ); + + // Put the layer directly on-top of the main layer of the ui: + parent_ui + .ctx() + .set_sublayer(parent_ui.layer_id(), scene_layer_id); + + let mut local_ui = parent_ui.new_child( + UiBuilder::new() + .layer_id(scene_layer_id) + .max_rect(Rect::from_min_size(Pos2::ZERO, self.max_inner_size)) + .sense(Sense::click_and_drag()), + ); + + let mut pan_response = local_ui.response(); + + // Update the `to_global` transform based on use interaction: + self.register_pan_and_zoom(&local_ui, &mut pan_response, to_global); + + // Set a correct global clip rect: + local_ui.set_clip_rect(to_global.inverse() * outer_rect); + + // Add the actual contents to the area: + let ret = add_contents(&mut local_ui); + + // This ensures we catch clicks/drags/pans anywhere on the background. + local_ui.force_set_min_rect((to_global.inverse() * outer_rect).round_ui()); + + // Tell egui to apply the transform on the layer: + local_ui + .ctx() + .set_transform_layer(scene_layer_id, *to_global); + + InnerResponse { + response: pan_response, + inner: ret, + } + } + + /// 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() { + to_global.translation += to_global.scaling * resp.drag_delta(); + resp.mark_changed(); + } + + if let Some(mouse_pos) = ui.input(|i| i.pointer.latest_pos()) { + if resp.contains_pointer() { + let pointer_in_scene = to_global.inverse() * mouse_pos; + let zoom_delta = ui.ctx().input(|i| i.zoom_delta()); + let pan_delta = ui.ctx().input(|i| i.smooth_scroll_delta); + + // Most of the time we can return early. This is also important to + // avoid `ui_from_scene` to change slightly due to floating point errors. + if zoom_delta == 1.0 && pan_delta == Vec2::ZERO { + return; + } + + if zoom_delta != 1.0 { + // Zoom in on pointer, but only if we are not zoomed in or out too far. + let zoom_delta = zoom_delta.clamp( + self.zoom_range.min / to_global.scaling, + self.zoom_range.max / to_global.scaling, + ); + + *to_global = *to_global + * TSTransform::from_translation(pointer_in_scene.to_vec2()) + * TSTransform::from_scaling(zoom_delta) + * TSTransform::from_translation(-pointer_in_scene.to_vec2()); + + // Clamp to exact zoom range. + to_global.scaling = self.zoom_range.clamp(to_global.scaling); + } + + // Pan: + *to_global = TSTransform::from_translation(pan_delta) * *to_global; + resp.mark_changed(); + } + } + } +} diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index 12702fb2f..3f6804212 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -161,6 +161,9 @@ impl ScrollBarVisibility { /// ``` /// /// You can scroll to an element using [`crate::Response::scroll_to_me`], [`Ui::scroll_to_cursor`] and [`Ui::scroll_to_rect`]. +/// +/// ## See also +/// If you want to allow zooming, use [`crate::Scene`]. #[derive(Clone, Debug)] #[must_use = "You should call .show()"] pub struct ScrollArea { diff --git a/crates/egui/src/text_selection/accesskit_text.rs b/crates/egui/src/text_selection/accesskit_text.rs index d0c386903..dedbc79dc 100644 --- a/crates/egui/src/text_selection/accesskit_text.rs +++ b/crates/egui/src/text_selection/accesskit_text.rs @@ -1,4 +1,6 @@ -use crate::{Context, Galley, Id, Pos2}; +use emath::TSTransform; + +use crate::{Context, Galley, Id}; use super::{text_cursor_state::is_word_char, CursorRange}; @@ -8,7 +10,7 @@ pub fn update_accesskit_for_text_widget( widget_id: Id, cursor_range: Option, role: accesskit::Role, - galley_pos: Pos2, + global_from_galley: TSTransform, galley: &Galley, ) { let parent_id = ctx.accesskit_node_builder(widget_id, |builder| { @@ -43,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 = row.rect.translate(galley_pos.to_vec2()); + let rect = global_from_galley * row.rect; builder.set_bounds(accesskit::Rect { x0: rect.min.x.into(), y0: rect.min.y.into(), diff --git a/crates/egui/src/text_selection/label_text_selection.rs b/crates/egui/src/text_selection/label_text_selection.rs index ea5f3c9c6..aa9f0986a 100644 --- a/crates/egui/src/text_selection/label_text_selection.rs +++ b/crates/egui/src/text_selection/label_text_selection.rs @@ -1,5 +1,7 @@ use std::sync::Arc; +use emath::TSTransform; + use crate::{ layers::ShapeIdx, text::CCursor, text_selection::CCursorRange, Context, CursorIcon, Event, Galley, Id, LayerId, Pos2, Rect, Response, Ui, @@ -25,9 +27,14 @@ struct WidgetTextCursor { } impl WidgetTextCursor { - fn new(widget_id: Id, cursor: impl Into, galley_pos: Pos2, galley: &Galley) -> Self { + fn new( + widget_id: Id, + cursor: impl Into, + global_from_galley: TSTransform, + galley: &Galley, + ) -> Self { let ccursor = cursor.into(); - let pos = pos_in_galley(galley_pos, galley, ccursor); + let pos = global_from_galley * pos_in_galley(galley, ccursor); Self { widget_id, ccursor, @@ -36,8 +43,8 @@ impl WidgetTextCursor { } } -fn pos_in_galley(galley_pos: Pos2, galley: &Galley, ccursor: CCursor) -> Pos2 { - galley_pos + galley.pos_from_ccursor(ccursor).center().to_vec2() +fn pos_in_galley(galley: &Galley, ccursor: CCursor) -> Pos2 { + galley.pos_from_ccursor(ccursor).center() } impl std::fmt::Debug for WidgetTextCursor { @@ -228,8 +235,7 @@ impl LabelSelectionState { self.selection = None; } - fn copy_text(&mut self, galley_pos: Pos2, galley: &Galley, cursor_range: &CursorRange) { - let new_galley_rect = Rect::from_min_size(galley_pos, galley.size()); + fn copy_text(&mut self, new_galley_rect: Rect, galley: &Galley, cursor_range: &CursorRange) { let new_text = selected_text(galley, cursor_range); if new_text.is_empty() { return; @@ -308,7 +314,7 @@ impl LabelSelectionState { &mut self, ui: &Ui, response: &Response, - galley_pos: Pos2, + global_from_galley: TSTransform, galley: &Galley, ) -> TextCursorState { let Some(selection) = &mut self.selection else { @@ -321,6 +327,8 @@ impl LabelSelectionState { return TextCursorState::default(); } + let galley_from_global = global_from_galley.inverse(); + let multi_widget_text_select = ui.style().interaction.multi_widget_text_select; let may_select_widget = @@ -328,7 +336,8 @@ impl LabelSelectionState { if self.is_dragging && may_select_widget { if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() { - let galley_rect = Rect::from_min_size(galley_pos, galley.size()); + let galley_rect = + global_from_galley * Rect::from_min_size(Pos2::ZERO, galley.size()); let galley_rect = galley_rect.intersect(ui.clip_rect()); let is_in_same_column = galley_rect @@ -342,7 +351,7 @@ impl LabelSelectionState { let new_primary = if response.contains_pointer() { // Dragging into this widget - easy case: - Some(galley.cursor_from_pos(pointer_pos - galley_pos)) + Some(galley.cursor_from_pos((galley_from_global * pointer_pos).to_vec2())) } else if is_in_same_column && !self.has_reached_primary && selection.primary.pos.y <= selection.secondary.pos.y @@ -376,7 +385,7 @@ impl LabelSelectionState { if let Some(new_primary) = new_primary { selection.primary = - WidgetTextCursor::new(response.id, new_primary, galley_pos, galley); + WidgetTextCursor::new(response.id, new_primary, global_from_galley, galley); // We don't want the latency of `drag_started`. let drag_started = ui.input(|i| i.pointer.any_pressed()); @@ -402,11 +411,12 @@ impl LabelSelectionState { let has_secondary = response.id == selection.secondary.widget_id; if has_primary { - selection.primary.pos = pos_in_galley(galley_pos, galley, selection.primary.ccursor); + selection.primary.pos = + global_from_galley * pos_in_galley(galley, selection.primary.ccursor); } if has_secondary { selection.secondary.pos = - pos_in_galley(galley_pos, galley, selection.secondary.ccursor); + global_from_galley * pos_in_galley(galley, selection.secondary.ccursor); } self.has_reached_primary |= has_primary; @@ -479,11 +489,21 @@ impl LabelSelectionState { &mut self, ui: &Ui, response: &Response, - galley_pos: Pos2, + galley_pos_in_layer: Pos2, galley: &mut Arc, ) -> Vec { let widget_id = response.id; + let global_from_layer = ui + .ctx() + .layer_transform_to_global(ui.layer_id()) + .unwrap_or_default(); + let layer_from_galley = TSTransform::from_translation(galley_pos_in_layer.to_vec2()); + let galley_from_layer = layer_from_galley.inverse(); + let layer_from_global = global_from_layer.inverse(); + let galley_from_global = galley_from_layer * layer_from_global; + let global_from_galley = global_from_layer * layer_from_galley; + if response.hovered() { ui.ctx().set_cursor_icon(CursorIcon::Text); } @@ -493,13 +513,14 @@ impl LabelSelectionState { let old_selection = self.selection; - let mut cursor_state = self.cursor_for(ui, response, galley_pos, galley); + let mut cursor_state = self.cursor_for(ui, response, global_from_galley, galley); let old_range = cursor_state.range(galley); if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() { if response.contains_pointer() { - let cursor_at_pointer = galley.cursor_from_pos(pointer_pos - galley_pos); + let cursor_at_pointer = + galley.cursor_from_pos((galley_from_global * pointer_pos).to_vec2()); // This is where we handle start-of-drag and double-click-to-select. // Actual drag-to-select happens elsewhere. @@ -509,7 +530,7 @@ impl LabelSelectionState { } if let Some(mut cursor_range) = cursor_state.range(galley) { - let galley_rect = Rect::from_min_size(galley_pos, galley.size()); + 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); if let Some(selection) = &self.selection { @@ -519,7 +540,7 @@ impl LabelSelectionState { } if got_copy_event(ui.ctx()) { - self.copy_text(galley_pos, galley, &cursor_range); + self.copy_text(galley_rect, galley, &cursor_range); } cursor_state.set_range(Some(cursor_range)); @@ -541,23 +562,32 @@ impl LabelSelectionState { if primary_changed || !ui.style().interaction.multi_widget_text_select { selection.primary = - WidgetTextCursor::new(widget_id, range.primary, galley_pos, galley); + WidgetTextCursor::new(widget_id, range.primary, global_from_galley, galley); self.has_reached_primary = true; } if secondary_changed || !ui.style().interaction.multi_widget_text_select { - selection.secondary = - WidgetTextCursor::new(widget_id, range.secondary, galley_pos, galley); + selection.secondary = WidgetTextCursor::new( + widget_id, + range.secondary, + global_from_galley, + galley, + ); self.has_reached_secondary = true; } } else { // Start of a new selection self.selection = Some(CurrentSelection { layer_id: response.layer_id, - primary: WidgetTextCursor::new(widget_id, range.primary, galley_pos, galley), + primary: WidgetTextCursor::new( + widget_id, + range.primary, + global_from_galley, + galley, + ), secondary: WidgetTextCursor::new( widget_id, range.secondary, - galley_pos, + global_from_galley, galley, ), }); @@ -580,7 +610,7 @@ impl LabelSelectionState { // Scroll to keep primary cursor in view: let row_height = estimate_row_height(galley); let primary_cursor_rect = - cursor_rect(galley_pos, galley, &range.primary, row_height); + global_from_galley * cursor_rect(galley, &range.primary, row_height); ui.scroll_to_rect(primary_cursor_rect, None); } } @@ -606,7 +636,7 @@ impl LabelSelectionState { response.id, cursor_range, accesskit::Role::Label, - galley_pos, + global_from_galley, galley, ); diff --git a/crates/egui/src/text_selection/text_cursor_state.rs b/crates/egui/src/text_selection/text_cursor_state.rs index 61407353a..ebc618b2c 100644 --- a/crates/egui/src/text_selection/text_cursor_state.rs +++ b/crates/egui/src/text_selection/text_cursor_state.rs @@ -5,7 +5,7 @@ use epaint::text::{ Galley, }; -use crate::{epaint, NumExt, Pos2, Rect, Response, Ui}; +use crate::{epaint, NumExt, Rect, Response, Ui}; use super::{CCursorRange, CursorRange}; @@ -335,14 +335,14 @@ pub fn slice_char_range(s: &str, char_range: std::ops::Range) -> &str { &s[start_byte..end_byte] } -/// The thin rectangle of one end of the selection, e.g. the primary cursor. -pub fn cursor_rect(galley_pos: Pos2, galley: &Galley, cursor: &Cursor, row_height: f32) -> Rect { - let mut cursor_pos = galley - .pos_from_cursor(cursor) - .translate(galley_pos.to_vec2()); - cursor_pos.max.y = cursor_pos.max.y.at_least(cursor_pos.min.y + row_height); +/// The thin rectangle of one end of the selection, e.g. the primary cursor, in local galley coordinates. +pub fn cursor_rect(galley: &Galley, cursor: &Cursor, row_height: f32) -> Rect { + let mut cursor_pos = galley.pos_from_cursor(cursor); + // Handle completely empty galleys - cursor_pos = cursor_pos.expand(1.5); - // slightly above/below row + cursor_pos.max.y = cursor_pos.max.y.at_least(cursor_pos.min.y + row_height); + + cursor_pos = cursor_pos.expand(1.5); // slightly above/below row + cursor_pos } diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 2619035a4..81dd6a448 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use emath::Rect; +use emath::{Rect, TSTransform}; use epaint::text::{cursor::CCursor, Galley, LayoutJob}; use crate::{ @@ -587,8 +587,8 @@ impl TextEdit<'_> { && ui.input(|i| i.pointer.is_moving()) { // text cursor preview: - let cursor_rect = - cursor_rect(rect.min, &galley, &cursor_at_pointer, row_height); + let cursor_rect = TSTransform::from_translation(rect.min.to_vec2()) + * cursor_rect(&galley, &cursor_at_pointer, row_height); text_selection::visuals::paint_cursor_end(&painter, ui.visuals(), cursor_rect); } @@ -738,7 +738,8 @@ impl TextEdit<'_> { if has_focus { if let Some(cursor_range) = state.cursor.range(&galley) { let primary_cursor_rect = - cursor_rect(galley_pos, &galley, &cursor_range.primary, row_height); + cursor_rect(&galley, &cursor_range.primary, row_height) + .translate(galley_pos.to_vec2()); if response.changed() || selection_changed { // Scroll to keep primary cursor in view: @@ -837,7 +838,7 @@ impl TextEdit<'_> { id, cursor_range, role, - galley_pos, + TSTransform::from_translation(galley_pos.to_vec2()), &galley, ); } 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 b74eb7db3..c8b142cac 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -77,8 +77,8 @@ impl Default for DemoGroups { Box::::default(), Box::::default(), Box::::default(), - Box::::default(), Box::::default(), + Box::::default(), Box::::default(), Box::::default(), Box::::default(), diff --git a/crates/egui_demo_lib/src/demo/mod.rs b/crates/egui_demo_lib/src/demo/mod.rs index c00725fbd..cb68a46fb 100644 --- a/crates/egui_demo_lib/src/demo/mod.rs +++ b/crates/egui_demo_lib/src/demo/mod.rs @@ -21,9 +21,9 @@ pub mod modals; pub mod multi_touch; pub mod paint_bezier; pub mod painting; -pub mod pan_zoom; pub mod panels; pub mod password; +pub mod scene; pub mod screenshot; pub mod scrolling; pub mod sliders; diff --git a/crates/egui_demo_lib/src/demo/pan_zoom.rs b/crates/egui_demo_lib/src/demo/pan_zoom.rs deleted file mode 100644 index e51b5b9d7..000000000 --- a/crates/egui_demo_lib/src/demo/pan_zoom.rs +++ /dev/null @@ -1,145 +0,0 @@ -use egui::emath::TSTransform; -use egui::TextWrapMode; - -#[derive(Clone, Default, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct PanZoom { - transform: TSTransform, - drag_value: f32, -} - -impl Eq for PanZoom {} - -impl crate::Demo for PanZoom { - fn name(&self) -> &'static str { - "🔍 Pan Zoom" - } - - fn show(&mut self, ctx: &egui::Context, open: &mut bool) { - use crate::View as _; - let window = egui::Window::new("Pan Zoom") - .default_width(300.0) - .default_height(300.0) - .vscroll(false) - .open(open); - window.show(ctx, |ui| self.ui(ui)); - } -} - -impl crate::View for PanZoom { - fn ui(&mut self, ui: &mut egui::Ui) { - ui.label( - "Pan, zoom in, and zoom out with scrolling (see the plot demo for more instructions). \ - Double click on the background to reset.", - ); - ui.vertical_centered(|ui| { - ui.add(crate::egui_github_link_file!()); - }); - ui.separator(); - - let (id, rect) = ui.allocate_space(ui.available_size()); - let response = ui.interact(rect, id, egui::Sense::click_and_drag()); - // Allow dragging the background as well. - if response.dragged() { - self.transform.translation += response.drag_delta(); - } - - // Plot-like reset - if response.double_clicked() { - self.transform = TSTransform::default(); - } - - let transform = - TSTransform::from_translation(ui.min_rect().left_top().to_vec2()) * self.transform; - - if let Some(pointer) = ui.ctx().input(|i| i.pointer.hover_pos()) { - // Note: doesn't catch zooming / panning if a button in this PanZoom container is hovered. - if response.hovered() { - let pointer_in_layer = transform.inverse() * pointer; - let zoom_delta = ui.ctx().input(|i| i.zoom_delta()); - let pan_delta = ui.ctx().input(|i| i.smooth_scroll_delta); - - // Zoom in on pointer: - self.transform = self.transform - * TSTransform::from_translation(pointer_in_layer.to_vec2()) - * TSTransform::from_scaling(zoom_delta) - * TSTransform::from_translation(-pointer_in_layer.to_vec2()); - - // Pan: - self.transform = TSTransform::from_translation(pan_delta) * self.transform; - } - } - - for (i, (pos, callback)) in [ - ( - egui::Pos2::new(0.0, 0.0), - Box::new(|ui: &mut egui::Ui, _: &mut Self| { - ui.button("top left").on_hover_text("Normal tooltip") - }) as Box egui::Response>, - ), - ( - egui::Pos2::new(0.0, 120.0), - Box::new(|ui: &mut egui::Ui, _| { - ui.button("bottom left").on_hover_text("Normal tooltip") - }), - ), - ( - egui::Pos2::new(120.0, 120.0), - Box::new(|ui: &mut egui::Ui, _| { - ui.button("right bottom") - .on_hover_text_at_pointer("Tooltip at pointer") - }), - ), - ( - egui::Pos2::new(120.0, 0.0), - Box::new(|ui: &mut egui::Ui, _| { - ui.button("right top") - .on_hover_text_at_pointer("Tooltip at pointer") - }), - ), - ( - egui::Pos2::new(60.0, 60.0), - Box::new(|ui, state| { - use egui::epaint::{pos2, CircleShape, Color32, QuadraticBezierShape, Stroke}; - // Smiley face. - let painter = ui.painter(); - painter.add(CircleShape::filled(pos2(0.0, -10.0), 1.0, Color32::YELLOW)); - painter.add(CircleShape::filled(pos2(10.0, -10.0), 1.0, Color32::YELLOW)); - painter.add(QuadraticBezierShape::from_points_stroke( - [pos2(0.0, 0.0), pos2(5.0, 3.0), pos2(10.0, 0.0)], - false, - Color32::TRANSPARENT, - Stroke::new(1.0, Color32::YELLOW), - )); - - ui.add(egui::Slider::new(&mut state.drag_value, 0.0..=100.0).text("My value")) - }), - ), - ] - .into_iter() - .enumerate() - { - let window_layer = ui.layer_id(); - let id = egui::Area::new(id.with(("subarea", i))) - .default_pos(pos) - .order(egui::Order::Middle) - .constrain(false) - .show(ui.ctx(), |ui| { - ui.set_clip_rect(transform.inverse() * rect); - egui::Frame::default() - .rounding(egui::Rounding::same(4)) - .inner_margin(egui::Margin::same(8)) - .stroke(ui.ctx().style().visuals.window_stroke) - .fill(ui.style().visuals.panel_fill) - .show(ui, |ui| { - ui.style_mut().wrap_mode = Some(TextWrapMode::Extend); - callback(ui, self) - }); - }) - .response - .layer_id; - ui.ctx().set_transform_layer(id, transform); - ui.ctx().set_sublayer(window_layer, id); - } - } -} diff --git a/crates/egui_demo_lib/src/demo/scene.rs b/crates/egui_demo_lib/src/demo/scene.rs new file mode 100644 index 000000000..a7d268e1f --- /dev/null +++ b/crates/egui_demo_lib/src/demo/scene.rs @@ -0,0 +1,82 @@ +use egui::{Pos2, Rect, Scene, Vec2}; + +use super::widget_gallery; + +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct SceneDemo { + widget_gallery: widget_gallery::WidgetGallery, + scene_rect: Rect, +} + +impl Default for SceneDemo { + fn default() -> Self { + Self { + widget_gallery: Default::default(), + scene_rect: Rect::ZERO, // `egui::Scene` will initialize this to something valid + } + } +} + +impl crate::Demo for SceneDemo { + fn name(&self) -> &'static str { + "🔍 Scene" + } + + fn show(&mut self, ctx: &egui::Context, open: &mut bool) { + use crate::View as _; + let window = egui::Window::new("Scene") + .default_width(300.0) + .default_height(300.0) + .scroll(false) + .open(open); + window.show(ctx, |ui| self.ui(ui)); + } +} + +impl crate::View for SceneDemo { + fn ui(&mut self, ui: &mut egui::Ui) { + ui.label( + "You can pan by scrolling, and zoom using cmd-scroll. \ + Double click on the background to reset view.", + ); + ui.vertical_centered(|ui| { + ui.add(crate::egui_github_link_file!()); + }); + ui.separator(); + + ui.label(format!("Scene rect: {:#?}", &mut self.scene_rect)); + + ui.separator(); + + egui::Frame::group(ui.style()) + .inner_margin(0.0) + .show(ui, |ui| { + let scene = Scene::new() + .max_inner_size([350.0, 1000.0]) + .zoom_range(0.1..=2.0); + + let mut reset_view = false; + let mut inner_rect = Rect::NAN; + let response = scene + .show(ui, &mut self.scene_rect, |ui| { + reset_view = ui.button("Reset view").clicked(); + + ui.add_space(16.0); + + self.widget_gallery.ui(ui); + + ui.put( + Rect::from_min_size(Pos2::new(0.0, -64.0), Vec2::new(200.0, 16.0)), + egui::Label::new("You can put a widget anywhere").selectable(false), + ); + + inner_rect = ui.min_rect(); + }) + .response; + + if reset_view || response.double_clicked() { + self.scene_rect = inner_rect; + } + }); + } +} diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scene.png b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png new file mode 100644 index 000000000..6f1e00fe1 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4eed8890c6d8fa6b97639197f5d1be79a72724a70470c5e5ae4b55e3447b9b88 +size 35561 diff --git a/crates/emath/src/ts_transform.rs b/crates/emath/src/ts_transform.rs index 4a761191b..46c7e2a8e 100644 --- a/crates/emath/src/ts_transform.rs +++ b/crates/emath/src/ts_transform.rs @@ -33,7 +33,7 @@ impl TSTransform { #[inline] /// Creates a new translation that first scales points around - /// `(0, 0)`, then translates them. + /// `(0, 0)`, then translates them. pub fn new(translation: Vec2, scaling: f32) -> Self { Self { translation, @@ -51,6 +51,11 @@ impl TSTransform { Self::new(Vec2::ZERO, scaling) } + /// Is this a valid, invertible transform? + pub fn is_valid(&self) -> bool { + self.scaling.is_finite() && self.translation.x.is_finite() && self.scaling != 0.0 + } + /// Inverts the transform. /// /// ``` From 7d87acb5faf5969a316895a42bfe0be67c575eca Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 29 Jan 2025 08:43:18 +0100 Subject: [PATCH 036/132] Remove dead code from CI --- .github/workflows/spelling_and_links.yml | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/.github/workflows/spelling_and_links.yml b/.github/workflows/spelling_and_links.yml index 2b4c8de14..d7b32b007 100644 --- a/.github/workflows/spelling_and_links.yml +++ b/.github/workflows/spelling_and_links.yml @@ -4,7 +4,7 @@ on: [pull_request] jobs: typos: # https://github.com/crate-ci/typos - # Add exceptions to _typos.toml + # Add exceptions to .typos.toml # install and run locally: cargo install typos-cli && typos name: typos runs-on: ubuntu-latest @@ -14,15 +14,7 @@ jobs: - name: Check spelling of entire workspace uses: crate-ci/typos@master - # Disabled: too many names of crates and user-names etc - # spellcheck: - # name: Spellcheck - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v4 - # - uses: streetsidesoftware/cspell-action@v2 - # with: - # files: "**/*.md" + linkinator: name: linkinator runs-on: ubuntu-latest From f86f62bb3ddd37db3462184b3a3a514b5e4a7d2b Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 29 Jan 2025 08:44:13 +0100 Subject: [PATCH 037/132] Improve deprecation text for `open_url/copied_text` --- crates/egui/src/data/output.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/egui/src/data/output.rs b/crates/egui/src/data/output.rs index 64a30d361..e1f9086a2 100644 --- a/crates/egui/src/data/output.rs +++ b/crates/egui/src/data/output.rs @@ -112,7 +112,7 @@ pub struct PlatformOutput { pub cursor_icon: CursorIcon, /// If set, open this url. - #[deprecated = "Use `Context::open_url` instead"] + #[deprecated = "Use `Context::open_url` or `PlatformOutput::commands` instead"] pub open_url: Option, /// If set, put this text in the system clipboard. Ignore if empty. @@ -126,7 +126,7 @@ pub struct PlatformOutput { /// } /// # }); /// ``` - #[deprecated = "Use `Context::copy_text` instead"] + #[deprecated = "Use `Context::copy_text` or `PlatformOutput::commands` instead"] pub copied_text: String, /// Events that may be useful to e.g. a screen reader. From 10523ec22dae13e5136d3c3b6d8ebb3114ea3f2f Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 29 Jan 2025 08:44:29 +0100 Subject: [PATCH 038/132] Improve README.md slightly --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b28cc995d..fe2a7dbc1 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,7 @@ Still, egui can be used to create professional looking applications, like [the R * Label text selection * And more! -Check out the [3rd party egui crates wiki](https://github.com/emilk/egui/wiki/3rd-party-egui-crates) for even more +Check out the [3rd party egui crates wiki](https://github.com/emilk/egui/wiki/3rd-party-egui-crates) for even more widgets and features, maintained by the community. @@ -153,7 +153,7 @@ Light Theme: * [`parking_lot`](https://crates.io/crates/parking_lot) Heavier dependencies are kept out of `egui`, even as opt-in. -No code that isn't fully Wasm-friendly is part of `egui`. +All code in `egui` is Wasm-friendly (even outside a browser). To load images into `egui` you can use the official [`egui_extras`](https://github.com/emilk/egui/tree/master/crates/egui_extras) crate. @@ -191,7 +191,7 @@ These are the official egui integrations: ### 3rd party integrations -Check the wiki to find [3rd party integrations](https://github.com/emilk/egui/wiki/3rd-party-integrations) +Check the wiki to find [3rd party integrations](https://github.com/emilk/egui/wiki/3rd-party-integrations) and [egui crates](https://github.com/emilk/egui/wiki/3rd-party-egui-crates). ### Writing your own egui integration From e343125f7024d32562de081da1cbd2bab8707df5 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 29 Jan 2025 08:44:47 +0100 Subject: [PATCH 039/132] deny.toml: point users to `cargo tree --duplicates` --- deny.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deny.toml b/deny.toml index 674f14d16..3e8a63371 100644 --- a/deny.toml +++ b/deny.toml @@ -35,7 +35,7 @@ ignore = [ ] [bans] -multiple-versions = "deny" +multiple-versions = "deny" # Use `cargo tree --duplicates` to easily find duplicates wildcards = "deny" deny = [ { name = "cmake", reason = "It has hurt me too much" }, From 83649f2e295a4c27ea08023b6e7f591d3e60ea7f Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 29 Jan 2025 08:44:56 +0100 Subject: [PATCH 040/132] Improve changelog generator --- scripts/generate_changelog.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/generate_changelog.py b/scripts/generate_changelog.py index 1621fd297..8b1aee15f 100755 --- a/scripts/generate_changelog.py +++ b/scripts/generate_changelog.py @@ -337,7 +337,9 @@ def main() -> None: if crate in crate_sections: prs = crate_sections[crate] print_section(crate, changelog_from_prs(prs, crate)) + print() print_section("Unsorted PRs", "\n".join([f"* {item}" for item in unsorted_prs])) + print() print_section( "Unsorted commits", "\n".join([f"* {item}" for item in unsorted_commits]) ) From 6be17136f27ad6c450f3ccbaf3ceb8ba0305d5ef Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 29 Jan 2025 12:46:12 +0100 Subject: [PATCH 041/132] `RectShape`: add control over where the stoke goes (#5647) Adds `RectShape::stroke_kind` so you can select if the stroke goes inside, outside, or is centered on the rectangle. Also adds `RectShape::round_to_pixels` so you can override `TessellationOptions::round_rects_to_pixels`. --- crates/egui/src/containers/frame.rs | 5 ++- crates/epaint/src/shape_transform.rs | 2 ++ crates/epaint/src/shapes/rect_shape.rs | 34 ++++++++++++++++++ crates/epaint/src/stroke.rs | 8 ++--- crates/epaint/src/tessellator.rs | 48 +++++++++++++++++--------- 5 files changed, 76 insertions(+), 21 deletions(-) diff --git a/crates/egui/src/containers/frame.rs b/crates/egui/src/containers/frame.rs index 64b38582b..b1bf56119 100644 --- a/crates/egui/src/containers/frame.rs +++ b/crates/egui/src/containers/frame.rs @@ -423,7 +423,10 @@ impl Frame { let fill_rect = self.fill_rect(content_rect); let widget_rect = self.widget_rect(content_rect); - let frame_shape = Shape::Rect(epaint::RectShape::new(fill_rect, rounding, fill, stroke)); + let frame_shape = Shape::Rect( + epaint::RectShape::new(fill_rect, rounding, fill, stroke) + .with_stroke_kind(epaint::StrokeKind::Outside), + ); if shadow == Default::default() { frame_shape diff --git a/crates/epaint/src/shape_transform.rs b/crates/epaint/src/shape_transform.rs index 25ff0d469..45805a276 100644 --- a/crates/epaint/src/shape_transform.rs +++ b/crates/epaint/src/shape_transform.rs @@ -63,6 +63,8 @@ pub fn adjust_colors( rounding: _, fill, stroke, + stroke_kind: _, + round_to_pixels: _, blur_width: _, brush: _, }) => { diff --git a/crates/epaint/src/shapes/rect_shape.rs b/crates/epaint/src/shapes/rect_shape.rs index b0750aa89..30ab07605 100644 --- a/crates/epaint/src/shapes/rect_shape.rs +++ b/crates/epaint/src/shapes/rect_shape.rs @@ -22,6 +22,18 @@ pub struct RectShape { /// This means the [`Self::visual_bounding_rect`] is `rect.size() + 2.0 * stroke.width`. pub stroke: Stroke, + /// Is the stroke on the inside, outside, or centered on the rectangle? + pub stroke_kind: StrokeKind, + + /// Snap the rectangle to pixels? + /// + /// Rounding produces sharper rectangles. + /// It is the outside of the fill (=inside of the stroke) + /// that will be rounded to the physical pixel grid. + /// + /// If `None`, [`crate::TessellationOptions::round_rects_to_pixels`] will be used. + pub round_to_pixels: Option, + /// If larger than zero, the edges of the rectangle /// (for both fill and stroke) will be blurred. /// @@ -63,6 +75,8 @@ impl RectShape { rounding: rounding.into(), fill: fill_color.into(), stroke: stroke.into(), + stroke_kind: StrokeKind::Outside, + round_to_pixels: None, blur_width: 0.0, brush: Default::default(), } @@ -84,6 +98,26 @@ impl RectShape { Self::new(rect, rounding, fill, stroke) } + /// Set if the stroke is on the inside, outside, or centered on the rectangle. + #[inline] + pub fn with_stroke_kind(mut self, stroke_kind: StrokeKind) -> Self { + self.stroke_kind = stroke_kind; + self + } + + /// Snap the rectangle to pixels? + /// + /// Rounding produces sharper rectangles. + /// It is the outside of the fill (=inside of the stroke) + /// that will be rounded to the physical pixel grid. + /// + /// If `None`, [`crate::TessellationOptions::round_rects_to_pixels`] will be used. + #[inline] + pub fn with_round_to_pixels(mut self, round_to_pixels: bool) -> Self { + self.round_to_pixels = Some(round_to_pixels); + self + } + /// If larger than zero, the edges of the rectangle /// (for both fill and stroke) will be blurred. /// diff --git a/crates/epaint/src/stroke.rs b/crates/epaint/src/stroke.rs index 63412cdc0..fa85a9588 100644 --- a/crates/epaint/src/stroke.rs +++ b/crates/epaint/src/stroke.rs @@ -56,17 +56,17 @@ impl std::hash::Hash for Stroke { } /// Describes how the stroke of a shape should be painted. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub enum StrokeKind { - /// The stroke should be painted entirely outside of the shape - Outside, - /// The stroke should be painted entirely inside of the shape Inside, /// The stroke should be painted right on the edge of the shape, half inside and half outside. Middle, + + /// The stroke should be painted entirely outside of the shape + Outside, } impl Default for StrokeKind { diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index b3e018a91..f4562e5ca 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -5,14 +5,14 @@ #![allow(clippy::identity_op)] -use crate::texture_atlas::PreparedDisc; -use crate::{ - color, emath, stroke, CircleShape, ClippedPrimitive, ClippedShape, Color32, CubicBezierShape, - EllipseShape, Mesh, PathShape, Primitive, QuadraticBezierShape, RectShape, Rounding, Shape, - Stroke, TextShape, TextureId, Vertex, WHITE_UV, -}; use emath::{pos2, remap, vec2, GuiRounding as _, NumExt, Pos2, Rect, Rot2, Vec2}; +use crate::{ + color, emath, stroke, texture_atlas::PreparedDisc, CircleShape, ClippedPrimitive, ClippedShape, + Color32, CubicBezierShape, EllipseShape, Mesh, PathShape, Primitive, QuadraticBezierShape, + RectShape, Rounding, Shape, Stroke, StrokeKind, TextShape, TextureId, Vertex, WHITE_UV, +}; + use self::color::ColorMode; use self::stroke::PathStroke; @@ -686,6 +686,8 @@ pub struct TessellationOptions { /// /// This makes the rectangle strokes more crisp, /// and makes filled rectangles tile perfectly (without feathering). + /// + /// You can override this with [`crate::RectShape::round_to_pixels`]. pub round_rects_to_pixels: bool, /// Output the clip rectangles to be painted. @@ -1669,27 +1671,41 @@ impl Tessellator { /// /// * `rect`: the rectangle to tessellate. /// * `out`: triangles are appended to this. - pub fn tessellate_rect(&mut self, rect: &RectShape, out: &mut Mesh) { - let brush = rect.brush.as_ref(); + pub fn tessellate_rect(&mut self, rect_shape: &RectShape, out: &mut Mesh) { + let brush = rect_shape.brush.as_ref(); let RectShape { mut rect, mut rounding, fill, stroke, + stroke_kind, + round_to_pixels, mut blur_width, - .. - } = *rect; + brush: _, // brush is extracted on its own, because it is not Copy + } = *rect_shape; + + let round_to_pixels = round_to_pixels.unwrap_or(self.options.round_rects_to_pixels); + + // Modify `rect` so that it represents the filled region, with the stroke on the outside: + match stroke_kind { + StrokeKind::Inside => { + rect = rect.shrink(stroke.width); + } + StrokeKind::Middle => { + rect = rect.shrink(stroke.width / 2.0); + } + StrokeKind::Outside => { + // Already good + } + } if self.options.coarse_tessellation_culling && !rect.expand(stroke.width).intersects(self.clip_rect) { return; } - if rect.is_negative() { - return; - } - if self.options.round_rects_to_pixels { + if round_to_pixels { // Since the stroke extends outside of the rectangle, // we can round the rectangle sides to the physical pixel edges, // and the filled rect will appear crisp, as will the inside of the stroke. @@ -1736,7 +1752,7 @@ impl Tessellator { if rect.width() < 0.5 * self.feathering { // Very thin - approximate by a vertical line-segment: let line = [rect.center_top(), rect.center_bottom()]; - if fill != Color32::TRANSPARENT { + if 0.0 < rect.width() && fill != Color32::TRANSPARENT { self.tessellate_line_segment(line, Stroke::new(rect.width(), fill), out); } if !stroke.is_empty() { @@ -1746,7 +1762,7 @@ impl Tessellator { } else if rect.height() < 0.5 * self.feathering { // Very thin - approximate by a horizontal line-segment: let line = [rect.left_center(), rect.right_center()]; - if fill != Color32::TRANSPARENT { + if 0.0 < rect.height() && fill != Color32::TRANSPARENT { self.tessellate_line_segment(line, Stroke::new(rect.height(), fill), out); } if !stroke.is_empty() { From 8d2c8c203c55d0aa0a3efa7326d4b8ec7bebf475 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 29 Jan 2025 13:07:05 +0100 Subject: [PATCH 042/132] generate_changelog.py: add sections for "Performance" and "Removed" --- scripts/generate_changelog.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/generate_changelog.py b/scripts/generate_changelog.py index 8b1aee15f..a3ee04a4a 100755 --- a/scripts/generate_changelog.py +++ b/scripts/generate_changelog.py @@ -154,6 +154,8 @@ def changelog_from_prs(pr_infos: List[PrInfo], crate_name: str) -> str: fixed = [] added = [] + performance = [] + removed = [] rest = [] for pr in pr_infos: summary = pr_summary(pr, crate_name) @@ -161,6 +163,10 @@ def changelog_from_prs(pr_infos: List[PrInfo], crate_name: str) -> str: fixed.append(pr) elif summary.startswith("Add") or "feature" in pr.labels: added.append(pr) + elif "performance" in pr.labels: + performance.append(pr) + elif summary.startswith("Remove"): + removed.append(pr) else: rest.append(pr) @@ -168,7 +174,9 @@ def changelog_from_prs(pr_infos: List[PrInfo], crate_name: str) -> str: result += pr_info_section(added, crate_name=crate_name, heading="⭐ Added") result += pr_info_section(rest, crate_name=crate_name, heading="🔧 Changed") + result += pr_info_section(removed, crate_name=crate_name, heading="🔥 Removed") result += pr_info_section(fixed, crate_name=crate_name, heading="🐛 Fixed") + result += pr_info_section(performance, crate_name=crate_name, heading="🚀 Performance") return result.rstrip() From 525d435a8475d56a6ea0ed35f41a363b622f2fce Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 29 Jan 2025 15:52:49 +0100 Subject: [PATCH 043/132] Require a `StrokeKind` when painting rectangles with strokes (#5648) This is a breaking change, requiring users to think about wether the stroke is inside/centered/outside the rect. When in doubt, add `egui::StrokeKind::Inside` to the function call. --- .../egui/src/containers/collapsing_header.rs | 12 ++++-- crates/egui/src/containers/combo_box.rs | 1 + crates/egui/src/containers/frame.rs | 11 ++++-- crates/egui/src/containers/resize.rs | 1 + crates/egui/src/context.rs | 4 +- crates/egui/src/grid.rs | 7 +++- crates/egui/src/lib.rs | 2 +- crates/egui/src/painter.rs | 17 +++++++-- crates/egui/src/pass_state.rs | 16 +++++++- crates/egui/src/placer.rs | 2 +- crates/egui/src/ui.rs | 10 +++-- crates/egui/src/widgets/button.rs | 1 + crates/egui/src/widgets/checkbox.rs | 1 + crates/egui/src/widgets/color_picker.rs | 19 +++++++--- crates/egui/src/widgets/image_button.rs | 8 +++- crates/egui/src/widgets/progress_bar.rs | 5 +-- crates/egui/src/widgets/selected_label.rs | 1 + crates/egui/src/widgets/slider.rs | 9 ++++- crates/egui/src/widgets/text_edit/builder.rs | 8 +++- crates/egui_demo_app/src/frame_history.rs | 1 + .../tests/snapshots/imageviewer.png | 4 +- crates/egui_demo_lib/benches/benchmark.rs | 8 +++- .../src/demo/misc_demo_window.rs | 1 + crates/egui_demo_lib/src/demo/paint_bezier.rs | 7 +++- .../egui_demo_lib/src/demo/toggle_switch.rs | 18 +++++++-- crates/egui_demo_lib/src/rendering_test.rs | 7 +++- .../tests/snapshots/demos/Frame.png | 4 +- .../tests/snapshots/demos/Painting.png | 4 +- .../tests/snapshots/demos/Scene.png | 4 +- .../tests/snapshots/widget_gallery.png | 4 +- .../tests/snapshots/combobox_opened.png | 4 +- crates/epaint/src/shapes/rect_shape.rs | 37 +++++++++++++------ crates/epaint/src/shapes/shape.rs | 8 ++-- crates/epaint/src/tessellator.rs | 9 ++++- tests/test_viewports/src/main.rs | 2 +- 35 files changed, 184 insertions(+), 73 deletions(-) diff --git a/crates/egui/src/containers/collapsing_header.rs b/crates/egui/src/containers/collapsing_header.rs index c5fa812d8..4ac57d36d 100644 --- a/crates/egui/src/containers/collapsing_header.rs +++ b/crates/egui/src/containers/collapsing_header.rs @@ -5,7 +5,7 @@ use crate::{ Response, Sense, Stroke, TextStyle, TextWrapMode, Ui, Vec2, WidgetInfo, WidgetText, WidgetType, }; use emath::GuiRounding as _; -use epaint::Shape; +use epaint::{Shape, StrokeKind}; #[derive(Clone, Copy, Debug)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -576,6 +576,7 @@ impl CollapsingHeader { visuals.rounding, visuals.weak_bg_fill, visuals.bg_stroke, + StrokeKind::Inside, )); } @@ -583,8 +584,13 @@ impl CollapsingHeader { { let rect = rect.expand(visuals.expansion); - ui.painter() - .rect(rect, visuals.rounding, visuals.bg_fill, visuals.bg_stroke); + ui.painter().rect( + rect, + visuals.rounding, + visuals.bg_fill, + visuals.bg_stroke, + StrokeKind::Inside, + ); } { diff --git a/crates/egui/src/containers/combo_box.rs b/crates/egui/src/containers/combo_box.rs index 55157294b..d6a71c2e4 100644 --- a/crates/egui/src/containers/combo_box.rs +++ b/crates/egui/src/containers/combo_box.rs @@ -474,6 +474,7 @@ fn button_frame( visuals.rounding, visuals.weak_bg_fill, visuals.bg_stroke, + epaint::StrokeKind::Inside, ), ); } diff --git a/crates/egui/src/containers/frame.rs b/crates/egui/src/containers/frame.rs index b1bf56119..abe1b8afe 100644 --- a/crates/egui/src/containers/frame.rs +++ b/crates/egui/src/containers/frame.rs @@ -423,10 +423,13 @@ impl Frame { let fill_rect = self.fill_rect(content_rect); let widget_rect = self.widget_rect(content_rect); - let frame_shape = Shape::Rect( - epaint::RectShape::new(fill_rect, rounding, fill, stroke) - .with_stroke_kind(epaint::StrokeKind::Outside), - ); + let frame_shape = Shape::Rect(epaint::RectShape::new( + fill_rect, + rounding, + fill, + stroke, + epaint::StrokeKind::Outside, + )); if shadow == Default::default() { frame_shape diff --git a/crates/egui/src/containers/resize.rs b/crates/egui/src/containers/resize.rs index bbb86dfbf..0df9a1136 100644 --- a/crates/egui/src/containers/resize.rs +++ b/crates/egui/src/containers/resize.rs @@ -356,6 +356,7 @@ impl Resize { rect, 3.0, ui.visuals().widgets.noninteractive.bg_stroke, + epaint::StrokeKind::Inside, )); } diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index e0459485d..ea36040cb 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -11,7 +11,7 @@ use epaint::{ tessellator, text::{FontInsert, FontPriority, Fonts}, util::OrderedFloat, - vec2, ClippedPrimitive, ClippedShape, Color32, ImageData, ImageDelta, Pos2, Rect, + vec2, ClippedPrimitive, ClippedShape, Color32, ImageData, ImageDelta, Pos2, Rect, StrokeKind, TessellationOptions, TextureAtlas, TextureId, Vec2, }; @@ -1087,7 +1087,7 @@ impl Context { let text = format!("🔥 {text}"); let color = self.style().visuals.error_fg_color; let painter = self.debug_painter(); - painter.rect_stroke(widget_rect, 0.0, (1.0, color)); + painter.rect_stroke(widget_rect, 0.0, (1.0, color), StrokeKind::Outside); let below = widget_rect.bottom() + 32.0 < screen_rect.bottom(); diff --git a/crates/egui/src/grid.rs b/crates/egui/src/grid.rs index 102027dd8..1e341432c 100644 --- a/crates/egui/src/grid.rs +++ b/crates/egui/src/grid.rs @@ -208,7 +208,12 @@ impl GridLayout { if (debug_expand_width && too_wide) || (debug_expand_height && too_high) { let painter = self.ctx.debug_painter(); - painter.rect_stroke(rect, 0.0, (1.0, Color32::LIGHT_BLUE)); + painter.rect_stroke( + rect, + 0.0, + (1.0, Color32::LIGHT_BLUE), + crate::StrokeKind::Inside, + ); let stroke = Stroke::new(2.5, Color32::from_rgb(200, 0, 0)); let paint_line_seg = |a, b| painter.line_segment([a, b], stroke); diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index ccd48e860..15f4faf3c 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -465,7 +465,7 @@ pub use epaint::{ text::{FontData, FontDefinitions, FontFamily, FontId, FontTweak}, textures::{TextureFilter, TextureOptions, TextureWrapMode, TexturesDelta}, ClippedPrimitive, ColorImage, FontImage, ImageData, Margin, Mesh, PaintCallback, - PaintCallbackInfo, Rounding, Shadow, Shape, Stroke, TextureHandle, TextureId, + PaintCallbackInfo, Rounding, Shadow, Shape, Stroke, StrokeKind, TextureHandle, TextureId, }; pub mod text { diff --git a/crates/egui/src/painter.rs b/crates/egui/src/painter.rs index e30a148d9..4ed74cddb 100644 --- a/crates/egui/src/painter.rs +++ b/crates/egui/src/painter.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use emath::GuiRounding as _; use epaint::{ text::{Fonts, Galley, LayoutJob}, - CircleShape, ClippedShape, PathStroke, RectShape, Rounding, Shape, Stroke, + CircleShape, ClippedShape, PathStroke, RectShape, Rounding, Shape, Stroke, StrokeKind, }; use crate::{ @@ -301,6 +301,7 @@ impl Painter { 0.0, color.additive().linear_multiply(0.015), (1.0, color), + StrokeKind::Outside, ); self.text( rect.min, @@ -407,15 +408,22 @@ impl Painter { }) } - /// The stroke extends _outside_ the [`Rect`]. + /// See also [`Self::rect_filled`] and [`Self::rect_stroke`]. pub fn rect( &self, rect: Rect, rounding: impl Into, fill_color: impl Into, stroke: impl Into, + stroke_kind: StrokeKind, ) -> ShapeIdx { - self.add(RectShape::new(rect, rounding, fill_color, stroke)) + self.add(RectShape::new( + rect, + rounding, + fill_color, + stroke, + stroke_kind, + )) } pub fn rect_filled( @@ -433,8 +441,9 @@ impl Painter { rect: Rect, rounding: impl Into, stroke: impl Into, + stroke_kind: StrokeKind, ) -> ShapeIdx { - self.add(RectShape::stroke(rect, rounding, stroke)) + self.add(RectShape::stroke(rect, rounding, stroke, stroke_kind)) } /// Show an arrow starting at `origin` and going in the direction of `vec`, with the length `vec.length()`. diff --git a/crates/egui/src/pass_state.rs b/crates/egui/src/pass_state.rs index 1cca5becc..942001c73 100644 --- a/crates/egui/src/pass_state.rs +++ b/crates/egui/src/pass_state.rs @@ -121,7 +121,13 @@ impl DebugRect { Color32::LIGHT_BLUE }; let rect_bg_color = Color32::BLUE.gamma_multiply(0.5); - painter.rect(rect, 0.0, rect_bg_color, (1.0, rect_fg_color)); + painter.rect( + rect, + 0.0, + rect_bg_color, + (1.0, rect_fg_color), + crate::StrokeKind::Outside, + ); } if !callstack.is_empty() { @@ -157,7 +163,13 @@ impl DebugRect { text_bg_color }; let text_rect = Rect::from_min_size(text_pos, galley.size()); - painter.rect(text_rect, 0.0, text_bg_color, (1.0, text_rect_stroke_color)); + painter.rect( + text_rect, + 0.0, + text_bg_color, + (1.0, text_rect_stroke_color), + crate::StrokeKind::Middle, + ); painter.galley(text_pos, galley, text_color); if is_clicking { diff --git a/crates/egui/src/placer.rs b/crates/egui/src/placer.rs index 3490eef10..2de822c03 100644 --- a/crates/egui/src/placer.rs +++ b/crates/egui/src/placer.rs @@ -281,7 +281,7 @@ impl Placer { if let Some(grid) = &self.grid { let rect = grid.next_cell(self.cursor(), Vec2::splat(0.0)); - painter.rect_stroke(rect, 1.0, stroke); + painter.rect_stroke(rect, 1.0, stroke, epaint::StrokeKind::Inside); let align = Align2::CENTER_CENTER; painter.debug_text(align.pos_in_rect(&rect), align, stroke.color, text); } else { diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 879177139..1fdfccdc6 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -1186,7 +1186,7 @@ impl Ui { /// # egui::__run_test_ui(|ui| { /// let response = ui.allocate_response(egui::vec2(100.0, 200.0), egui::Sense::click()); /// if response.clicked() { /* … */ } - /// ui.painter().rect_stroke(response.rect, 0.0, (1.0, egui::Color32::WHITE)); + /// ui.painter().rect_stroke(response.rect, 0.0, (1.0, egui::Color32::WHITE), egui::StrokeKind::Inside); /// # }); /// ``` pub fn allocate_response(&mut self, desired_size: Vec2, sense: Sense) -> Response { @@ -1253,8 +1253,12 @@ impl Ui { let debug_expand_height = self.style().debug.show_expand_height; if (debug_expand_width && too_wide) || (debug_expand_height && too_high) { - self.painter - .rect_stroke(rect, 0.0, (1.0, Color32::LIGHT_BLUE)); + self.painter.rect_stroke( + rect, + 0.0, + (1.0, Color32::LIGHT_BLUE), + crate::StrokeKind::Inside, + ); let stroke = Stroke::new(2.5, Color32::from_rgb(200, 0, 0)); let paint_line_seg = |a, b| self.painter().line_segment([a, b], stroke); diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 701e89b1c..9e44e940e 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -319,6 +319,7 @@ impl Widget for Button<'_> { frame_rounding, frame_fill, frame_stroke, + epaint::StrokeKind::Inside, ); let mut cursor_x = rect.min.x + button_padding.x; diff --git a/crates/egui/src/widgets/checkbox.rs b/crates/egui/src/widgets/checkbox.rs index 96cc3d22b..f54478984 100644 --- a/crates/egui/src/widgets/checkbox.rs +++ b/crates/egui/src/widgets/checkbox.rs @@ -107,6 +107,7 @@ impl Widget for Checkbox<'_> { visuals.rounding, visuals.bg_fill, visuals.bg_stroke, + epaint::StrokeKind::Inside, )); if indeterminate { diff --git a/crates/egui/src/widgets/color_picker.rs b/crates/egui/src/widgets/color_picker.rs index ea1b53aef..b56f3c4e9 100644 --- a/crates/egui/src/widgets/color_picker.rs +++ b/crates/egui/src/widgets/color_picker.rs @@ -7,7 +7,7 @@ use crate::{ }; use epaint::{ ecolor::{Color32, Hsva, HsvaGamma, Rgba}, - pos2, vec2, Mesh, Rect, Shape, Stroke, Vec2, + pos2, vec2, Mesh, Rect, Shape, Stroke, StrokeKind, Vec2, }; fn contrast_color(color: impl Into) -> Color32 { @@ -97,11 +97,16 @@ fn color_button(ui: &mut Ui, color: Color32, open: bool) -> Response { }; let rect = rect.expand(visuals.expansion); - show_color_at(ui.painter(), color, rect); + let stroke_width = 1.0; + show_color_at(ui.painter(), color, rect.shrink(stroke_width)); let rounding = visuals.rounding.at_most(2); // Can't do more rounding because the background grid doesn't do any rounding - ui.painter() - .rect_stroke(rect, rounding, (2.0, visuals.bg_fill)); // fill is intentional, because default style has no border + ui.painter().rect_stroke( + rect, + rounding, + (stroke_width, visuals.bg_fill), // Using fill for stroke is intentional, because default style has no border + StrokeKind::Inside, + ); } response @@ -139,7 +144,8 @@ fn color_slider_1d(ui: &mut Ui, value: &mut f32, color_at: impl Fn(f32) -> Color ui.painter().add(Shape::mesh(mesh)); } - ui.painter().rect_stroke(rect, 0.0, visuals.bg_stroke); // outline + ui.painter() + .rect_stroke(rect, 0.0, visuals.bg_stroke, StrokeKind::Inside); // outline { // Show where the slider is at: @@ -208,7 +214,8 @@ fn color_slider_2d( } ui.painter().add(Shape::mesh(mesh)); // fill - ui.painter().rect_stroke(rect, 0.0, visuals.bg_stroke); // outline + ui.painter() + .rect_stroke(rect, 0.0, visuals.bg_stroke, StrokeKind::Inside); // outline // Show where the slider is at: let x = lerp(rect.left()..=rect.right(), *x_value); diff --git a/crates/egui/src/widgets/image_button.rs b/crates/egui/src/widgets/image_button.rs index 3a03ab295..7bf55cb60 100644 --- a/crates/egui/src/widgets/image_button.rs +++ b/crates/egui/src/widgets/image_button.rs @@ -137,8 +137,12 @@ impl Widget for ImageButton<'_> { ); // Draw frame outline: - ui.painter() - .rect_stroke(rect.expand2(expansion), rounding, stroke); + ui.painter().rect_stroke( + rect.expand2(expansion), + rounding, + stroke, + epaint::StrokeKind::Inside, + ); } widgets::image::texture_load_result_response(&self.image.source(ui.ctx()), &tlr, response) diff --git a/crates/egui/src/widgets/progress_bar.rs b/crates/egui/src/widgets/progress_bar.rs index 7f926d02b..a8c27f5c3 100644 --- a/crates/egui/src/widgets/progress_bar.rs +++ b/crates/egui/src/widgets/progress_bar.rs @@ -137,7 +137,7 @@ impl Widget for ProgressBar { let corner_radius = outer_rect.height() / 2.0; let rounding = rounding.unwrap_or_else(|| corner_radius.into()); ui.painter() - .rect(outer_rect, rounding, visuals.extreme_bg_color, Stroke::NONE); + .rect_filled(outer_rect, rounding, visuals.extreme_bg_color); let min_width = 2.0 * f32::max(rounding.sw as _, rounding.nw as _).at_most(corner_radius); let filled_width = (outer_rect.width() * progress).at_least(min_width); @@ -152,13 +152,12 @@ impl Widget for ProgressBar { bright }; - ui.painter().rect( + ui.painter().rect_filled( inner_rect, rounding, Color32::from( Rgba::from(fill.unwrap_or(visuals.selection.bg_fill)) * color_factor as f32, ), - Stroke::NONE, ); if animate && !is_custom_rounding { diff --git a/crates/egui/src/widgets/selected_label.rs b/crates/egui/src/widgets/selected_label.rs index 193fee74d..16978794b 100644 --- a/crates/egui/src/widgets/selected_label.rs +++ b/crates/egui/src/widgets/selected_label.rs @@ -74,6 +74,7 @@ impl Widget for SelectableLabel { visuals.rounding, visuals.weak_bg_fill, visuals.bg_stroke, + epaint::StrokeKind::Inside, ); } diff --git a/crates/egui/src/widgets/slider.rs b/crates/egui/src/widgets/slider.rs index d535a5c75..f721ce101 100644 --- a/crates/egui/src/widgets/slider.rs +++ b/crates/egui/src/widgets/slider.rs @@ -815,8 +815,13 @@ impl Slider<'_> { }; let v = v + Vec2::splat(visuals.expansion); let rect = Rect::from_center_size(center, 2.0 * v); - ui.painter() - .rect(rect, visuals.rounding, visuals.bg_fill, visuals.fg_stroke); + ui.painter().rect( + rect, + visuals.rounding, + visuals.bg_fill, + visuals.fg_stroke, + epaint::StrokeKind::Inside, + ); } } } diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 81dd6a448..05a8470c1 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -1,7 +1,10 @@ use std::sync::Arc; use emath::{Rect, TSTransform}; -use epaint::text::{cursor::CCursor, Galley, LayoutJob}; +use epaint::{ + text::{cursor::CCursor, Galley, LayoutJob}, + StrokeKind, +}; use crate::{ epaint, @@ -442,6 +445,7 @@ impl TextEdit<'_> { visuals.rounding, background_color, ui.visuals().selection.stroke, + StrokeKind::Inside, ) } else { epaint::RectShape::new( @@ -449,6 +453,7 @@ impl TextEdit<'_> { visuals.rounding, background_color, visuals.bg_stroke, // TODO(emilk): we want to show something here, or a text-edit field doesn't "pop". + StrokeKind::Inside, ) } } else { @@ -457,6 +462,7 @@ impl TextEdit<'_> { frame_rect, visuals.rounding, visuals.bg_stroke, // TODO(emilk): we want to show something here, or a text-edit field doesn't "pop". + StrokeKind::Inside, ) }; diff --git a/crates/egui_demo_app/src/frame_history.rs b/crates/egui_demo_app/src/frame_history.rs index a6a3fbeeb..b1091faba 100644 --- a/crates/egui_demo_app/src/frame_history.rs +++ b/crates/egui_demo_app/src/frame_history.rs @@ -75,6 +75,7 @@ impl FrameHistory { style.rounding, ui.visuals().extreme_bg_color, ui.style().noninteractive().bg_stroke, + egui::StrokeKind::Inside, ))); let rect = rect.shrink(4.0); diff --git a/crates/egui_demo_app/tests/snapshots/imageviewer.png b/crates/egui_demo_app/tests/snapshots/imageviewer.png index 1176b2d33..750fa3577 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:e6cc6ff64eb73ddac89ecdacd07c2176f3ab952c0db4593fccf6d11f155ec392 -size 103100 +oid sha256:2292f0f80bfd3c80055a72eb983549ac2875d36acb333732bd0a67e51b24ae4f +size 102983 diff --git a/crates/egui_demo_lib/benches/benchmark.rs b/crates/egui_demo_lib/benches/benchmark.rs index d3820603d..cbcc4d88f 100644 --- a/crates/egui_demo_lib/benches/benchmark.rs +++ b/crates/egui_demo_lib/benches/benchmark.rs @@ -76,7 +76,13 @@ pub fn criterion_benchmark(c: &mut Criterion) { let painter = ui.painter(); let rect = ui.max_rect(); b.iter(|| { - painter.rect(rect, 2.0, egui::Color32::RED, (1.0, egui::Color32::WHITE)); + painter.rect( + rect, + 2.0, + egui::Color32::RED, + (1.0, egui::Color32::WHITE), + egui::StrokeKind::Inside, + ); }); }); }); 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 f3edf3955..5f1022ffc 100644 --- a/crates/egui_demo_lib/src/demo/misc_demo_window.rs +++ b/crates/egui_demo_lib/src/demo/misc_demo_window.rs @@ -390,6 +390,7 @@ impl BoxPainting { self.rounding, ui.visuals().text_color().gamma_multiply(0.5), Stroke::new(self.stroke_width, Color32::WHITE), + egui::StrokeKind::Inside, ); } }); diff --git a/crates/egui_demo_lib/src/demo/paint_bezier.rs b/crates/egui_demo_lib/src/demo/paint_bezier.rs index 61dc3b7e6..08e29a9ff 100644 --- a/crates/egui_demo_lib/src/demo/paint_bezier.rs +++ b/crates/egui_demo_lib/src/demo/paint_bezier.rs @@ -1,6 +1,7 @@ -use egui::epaint::{CubicBezierShape, PathShape, QuadraticBezierShape}; use egui::{ - emath, epaint, pos2, Color32, Context, Frame, Grid, Pos2, Rect, Sense, Shape, Stroke, Ui, Vec2, + emath, + epaint::{self, CubicBezierShape, PathShape, QuadraticBezierShape}, + pos2, Color32, Context, Frame, Grid, Pos2, Rect, Sense, Shape, Stroke, StrokeKind, Ui, Vec2, Widget, Window, }; @@ -132,6 +133,7 @@ impl PaintBezier { shape.visual_bounding_rect(), 0.0, self.bounding_box_stroke, + StrokeKind::Outside, )); painter.add(shape); } @@ -143,6 +145,7 @@ impl PaintBezier { shape.visual_bounding_rect(), 0.0, self.bounding_box_stroke, + StrokeKind::Outside, )); painter.add(shape); } diff --git a/crates/egui_demo_lib/src/demo/toggle_switch.rs b/crates/egui_demo_lib/src/demo/toggle_switch.rs index ca32987ef..9619bd2c0 100644 --- a/crates/egui_demo_lib/src/demo/toggle_switch.rs +++ b/crates/egui_demo_lib/src/demo/toggle_switch.rs @@ -56,8 +56,13 @@ pub fn toggle_ui(ui: &mut egui::Ui, on: &mut bool) -> egui::Response { // All coordinates are in absolute screen coordinates so we use `rect` to place the elements. let rect = rect.expand(visuals.expansion); let radius = 0.5 * rect.height(); - ui.painter() - .rect(rect, radius, visuals.bg_fill, visuals.bg_stroke); + ui.painter().rect( + rect, + radius, + visuals.bg_fill, + visuals.bg_stroke, + egui::StrokeKind::Inside, + ); // Paint the circle, animating it from left to right with `how_on`: let circle_x = egui::lerp((rect.left() + radius)..=(rect.right() - radius), how_on); let center = egui::pos2(circle_x, rect.center().y); @@ -88,8 +93,13 @@ fn toggle_ui_compact(ui: &mut egui::Ui, on: &mut bool) -> egui::Response { let visuals = ui.style().interact_selectable(&response, *on); let rect = rect.expand(visuals.expansion); let radius = 0.5 * rect.height(); - ui.painter() - .rect(rect, radius, visuals.bg_fill, visuals.bg_stroke); + ui.painter().rect( + rect, + radius, + visuals.bg_fill, + visuals.bg_stroke, + egui::StrokeKind::Inside, + ); let circle_x = egui::lerp((rect.left() + radius)..=(rect.right() - radius), how_on); let center = egui::pos2(circle_x, rect.center().y); ui.painter() diff --git a/crates/egui_demo_lib/src/rendering_test.rs b/crates/egui_demo_lib/src/rendering_test.rs index 9d4c7985f..d43116e0b 100644 --- a/crates/egui_demo_lib/src/rendering_test.rs +++ b/crates/egui_demo_lib/src/rendering_test.rs @@ -452,7 +452,12 @@ fn pixel_test_strokes(ui: &mut Ui) { Pos2::new(cursor_pixel.x, cursor_pixel.y), Vec2::splat(size as f32), ); - painter.rect_stroke(rect_points / pixels_per_point, 0.0, stroke); + painter.rect_stroke( + rect_points / pixels_per_point, + 0.0, + stroke, + egui::StrokeKind::Outside, + ); cursor_pixel.x += (1 + size) as f32 + thickness_pixels * 2.0; } } diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Frame.png b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png index 8d9779b29..689bb2153 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Frame.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d8135b745cb95a7e7c7a26e73e160742f88ec177a2fa262215c4886d98ff172 -size 24403 +oid sha256:e4fef5fa8661f207bae2c58381e729cdaf77aecc8b3f178caf262dc310e3a490 +size 24206 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Painting.png b/crates/egui_demo_lib/tests/snapshots/demos/Painting.png index 2638bf08d..2aee66f72 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Painting.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Painting.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:64fe3ef34aaf3104931954f4a39760b99944f42da13f866622ca0222b750f6be -size 17731 +oid sha256:6efc59cb9908533baa1a7346b359e9e21c5faf0e373dac6fa7db5476e644233d +size 17678 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scene.png b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png index 6f1e00fe1..d9f483ec1 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:4eed8890c6d8fa6b97639197f5d1be79a72724a70470c5e5ae4b55e3447b9b88 -size 35561 +oid sha256:d076f5365bfa87b7e61d3808b8b9b367157ea6e55ccf665720cbd2237d53793d +size 35563 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery.png index 6f06e7727..84e731171 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:b3dc1bf9a59007a6ad0fb66a345d6cf272bd8bdcd26b10dbf411c1280e62b6fc -size 158285 +oid sha256:a291f3a5724aefc59ba7881f48752ccc826ca5e480741c221d195061f562ccc9 +size 158220 diff --git a/crates/egui_kittest/tests/snapshots/combobox_opened.png b/crates/egui_kittest/tests/snapshots/combobox_opened.png index 201fc999e..9020c95c2 100644 --- a/crates/egui_kittest/tests/snapshots/combobox_opened.png +++ b/crates/egui_kittest/tests/snapshots/combobox_opened.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0a61ecf294d930ebbee9837611d7a75381e690348f448b1c0c8264b27f44ceb3 -size 7535 +oid sha256:64fd46da67cab2afae0ea8997a88fb43fd207e24cc3943086d978a8de717320f +size 7542 diff --git a/crates/epaint/src/shapes/rect_shape.rs b/crates/epaint/src/shapes/rect_shape.rs index 30ab07605..ca7ab01f1 100644 --- a/crates/epaint/src/shapes/rect_shape.rs +++ b/crates/epaint/src/shapes/rect_shape.rs @@ -16,10 +16,8 @@ pub struct RectShape { /// The thickness and color of the outline. /// - /// The stroke extends _outside_ the edge of [`Self::rect`], - /// i.e. using [`crate::StrokeKind::Outside`]. - /// - /// This means the [`Self::visual_bounding_rect`] is `rect.size() + 2.0 * stroke.width`. + /// Whether or not the stroke is inside or outside the edge of [`Self::rect`], + /// is controlled by [`Self::stroke_kind`]. pub stroke: Stroke, /// Is the stroke on the inside, outside, or centered on the rectangle? @@ -62,20 +60,21 @@ fn rect_shape_size() { } impl RectShape { - /// The stroke extends _outside_ the [`Rect`]. + /// See also [`Self::filled`] and [`Self::stroke`]. #[inline] pub fn new( rect: Rect, rounding: impl Into, fill_color: impl Into, stroke: impl Into, + stroke_kind: StrokeKind, ) -> Self { Self { rect, rounding: rounding.into(), fill: fill_color.into(), stroke: stroke.into(), - stroke_kind: StrokeKind::Outside, + stroke_kind, round_to_pixels: None, blur_width: 0.0, brush: Default::default(), @@ -88,14 +87,24 @@ impl RectShape { rounding: impl Into, fill_color: impl Into, ) -> Self { - Self::new(rect, rounding, fill_color, Stroke::NONE) + Self::new( + rect, + rounding, + fill_color, + Stroke::NONE, + StrokeKind::Outside, // doesn't matter + ) } - /// The stroke extends _outside_ the [`Rect`]. #[inline] - pub fn stroke(rect: Rect, rounding: impl Into, stroke: impl Into) -> Self { + pub fn stroke( + rect: Rect, + rounding: impl Into, + stroke: impl Into, + stroke_kind: StrokeKind, + ) -> Self { let fill = Color32::TRANSPARENT; - Self::new(rect, rounding, fill, stroke) + Self::new(rect, rounding, fill, stroke, stroke_kind) } /// Set if the stroke is on the inside, outside, or centered on the rectangle. @@ -146,8 +155,12 @@ impl RectShape { if self.fill == Color32::TRANSPARENT && self.stroke.is_empty() { Rect::NOTHING } else { - let Stroke { width, .. } = self.stroke; // Make sure we remember to update this if we change `stroke` to `PathStroke` - self.rect.expand(width + self.blur_width / 2.0) + let expand = match self.stroke_kind { + StrokeKind::Inside => 0.0, + StrokeKind::Middle => self.stroke.width / 2.0, + StrokeKind::Outside => self.stroke.width, + }; + self.rect.expand(expand + self.blur_width / 2.0) } } diff --git a/crates/epaint/src/shapes/shape.rs b/crates/epaint/src/shapes/shape.rs index 62af1b581..2e43fc585 100644 --- a/crates/epaint/src/shapes/shape.rs +++ b/crates/epaint/src/shapes/shape.rs @@ -7,7 +7,7 @@ use emath::{pos2, Align2, Pos2, Rangef, Rect, TSTransform, Vec2}; use crate::{ stroke::PathStroke, text::{FontId, Fonts, Galley}, - Color32, Mesh, Rounding, Stroke, TextureId, + Color32, Mesh, Rounding, Stroke, StrokeKind, TextureId, }; use super::{ @@ -275,6 +275,7 @@ impl Shape { Self::Ellipse(EllipseShape::stroke(center, radius, stroke)) } + /// See also [`Self::rect_stroke`]. #[inline] pub fn rect_filled( rect: Rect, @@ -284,14 +285,15 @@ impl Shape { Self::Rect(RectShape::filled(rect, rounding, fill_color)) } - /// The stroke extends _outside_ the [`Rect`]. + /// See also [`Self::rect_filled`]. #[inline] pub fn rect_stroke( rect: Rect, rounding: impl Into, stroke: impl Into, + stroke_kind: StrokeKind, ) -> Self { - Self::Rect(RectShape::stroke(rect, rounding, stroke)) + Self::Rect(RectShape::stroke(rect, rounding, stroke, stroke_kind)) } #[allow(clippy::needless_pass_by_value)] diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index f4562e5ca..f05d9b47a 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1400,7 +1400,7 @@ impl Tessellator { if self.options.debug_paint_text_rects { let rect = text_shape.galley.rect.translate(text_shape.pos.to_vec2()); self.tessellate_rect( - &RectShape::stroke(rect.expand(0.5), 2.0, (0.5, Color32::GREEN)), + &RectShape::stroke(rect, 2.0, (0.5, Color32::GREEN), StrokeKind::Outside), out, ); } @@ -2189,7 +2189,12 @@ impl Tessellator { .flat_map(|clipped_primitive| { let mut clip_rect_mesh = Mesh::default(); self.tessellate_shape( - Shape::rect_stroke(clipped_primitive.clip_rect, 0.0, stroke), + Shape::rect_stroke( + clipped_primitive.clip_rect, + 0.0, + stroke, + StrokeKind::Outside, + ), &mut clip_rect_mesh, ); diff --git a/tests/test_viewports/src/main.rs b/tests/test_viewports/src/main.rs index 9d06db431..9cdb0b967 100644 --- a/tests/test_viewports/src/main.rs +++ b/tests/test_viewports/src/main.rs @@ -478,7 +478,7 @@ fn drop_target( ui.painter().set( background_id, - egui::epaint::RectShape::new(rect, style.rounding, fill, stroke), + egui::epaint::RectShape::new(rect, style.rounding, fill, stroke, egui::StrokeKind::Inside), ); egui::InnerResponse::new(ret, response) From 8eda32ec64c7b48c7b8f9195e3abc8c10efa8f00 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 30 Jan 2025 09:34:22 +0100 Subject: [PATCH 044/132] egui_kittest: succeed and keep going when `UPDATE_SNAPSHOTS` is set (#5649) It used to be that `UPDATE_SNAPSHOTS=true cargo test --all-features` would stop on the first crate with a failure, requiring you to run it multiple times, which is annoying, and a waste of time. --- crates/egui_kittest/README.md | 13 ++--- crates/egui_kittest/src/snapshot.rs | 79 +++++++++++++++++------------ 2 files changed, 54 insertions(+), 38 deletions(-) diff --git a/crates/egui_kittest/README.md b/crates/egui_kittest/README.md index 99fe9be65..a9c1286bf 100644 --- a/crates/egui_kittest/README.md +++ b/crates/egui_kittest/README.md @@ -1,4 +1,4 @@ -# egui_kittest +# egui_kittest Ui testing library for egui, based on [kittest](https://github.com/rerun-io/kittest) (an [AccessKit](https://github.com/AccessKit/accesskit) based testing library). @@ -14,16 +14,16 @@ fn main() { }; let mut harness = Harness::new_ui(app); - + let checkbox = harness.get_by_label("Check me!"); assert_eq!(checkbox.toggled(), Some(Toggled::False)); checkbox.click(); - + harness.run(); let checkbox = harness.get_by_label("Check me!"); assert_eq!(checkbox.toggled(), Some(Toggled::True)); - + // Shrink the window size to the smallest size possible harness.fit_contents(); @@ -38,9 +38,10 @@ There is a snapshot testing feature. To create snapshot tests, enable the `snaps Once enabled, you can call `Harness::snapshot` to render the ui and save the image to the `tests/snapshots` directory. To update the snapshots, run your tests with `UPDATE_SNAPSHOTS=true`, so e.g. `UPDATE_SNAPSHOTS=true cargo test`. -Running with `UPDATE_SNAPSHOTS=true` will still cause the tests to fail, but on the next run, the tests should pass. +Running with `UPDATE_SNAPSHOTS=true` will cause the tests to succeed. +This is so that you can set `UPDATE_SNAPSHOTS=true` and update _all_ tests, without `cargo test` failing on the first failing crate. -If you want to have multiple snapshots in the same test, it makes sense to collect the results in a `Vec` +If you want to have multiple snapshots in the same test, it makes sense to collect the results in a `Vec` ([look here](https://github.com/emilk/egui/blob/70a01138b77f9c5724a35a6ef750b9ae1ab9f2dc/crates/egui_demo_lib/src/demo/demo_app_windows.rs#L388-L427) for an example). This way they can all be updated at the same time. diff --git a/crates/egui_kittest/src/snapshot.rs b/crates/egui_kittest/src/snapshot.rs index be4f68a06..409c287c8 100644 --- a/crates/egui_kittest/src/snapshot.rs +++ b/crates/egui_kittest/src/snapshot.rs @@ -155,8 +155,15 @@ impl Display for SnapshotError { } } +/// If this is set, we update the snapshots (if different), +/// and _succeed_ the test. +/// This is so that you can set `UPDATE_SNAPSHOTS=true` and update _all_ tests, +/// without `cargo test` failing on the first failing crate. fn should_update_snapshots() -> bool { - std::env::var("UPDATE_SNAPSHOTS").is_ok() + match std::env::var("UPDATE_SNAPSHOTS") { + Ok(value) => !matches!(value.as_str(), "false" | "0" | "no" | "off"), + Err(_) => false, + } } /// Image snapshot test with custom options. @@ -203,23 +210,22 @@ pub fn try_image_snapshot_options( std::fs::remove_file(&old_backup_path).ok(); std::fs::remove_file(&new_path).ok(); - let maybe_update_snapshot = || { - if should_update_snapshots() { - // Keep the old version so the user can compare it: - std::fs::rename(&snapshot_path, &old_backup_path).ok(); + let update_snapshot = || { + // Keep the old version so the user can compare it: + std::fs::rename(&snapshot_path, &old_backup_path).ok(); - // Write the new file to the checked in path: - new.save(&snapshot_path) - .map_err(|err| SnapshotError::WriteSnapshot { - err, - path: snapshot_path.clone(), - })?; + // Write the new file to the checked in path: + new.save(&snapshot_path) + .map_err(|err| SnapshotError::WriteSnapshot { + err, + path: snapshot_path.clone(), + })?; - // No need for an explicit `.new` file: - std::fs::remove_file(&new_path).ok(); + // No need for an explicit `.new` file: + std::fs::remove_file(&new_path).ok(); + + println!("Updated snapshot: {snapshot_path:?}"); - println!("Updated snapshot: {snapshot_path:?}"); - } Ok(()) }; @@ -234,21 +240,27 @@ pub fn try_image_snapshot_options( Ok(image) => image.to_rgba8(), Err(err) => { // No previous snapshot - probablye a new test. - maybe_update_snapshot()?; - return Err(SnapshotError::OpenSnapshot { - path: snapshot_path.clone(), - err, - }); + if should_update_snapshots() { + return update_snapshot(); + } else { + return Err(SnapshotError::OpenSnapshot { + path: snapshot_path.clone(), + err, + }); + } } }; if previous.dimensions() != new.dimensions() { - maybe_update_snapshot()?; - return Err(SnapshotError::SizeMismatch { - name: name.to_owned(), - expected: previous.dimensions(), - actual: new.dimensions(), - }); + if should_update_snapshots() { + return update_snapshot(); + } else { + return Err(SnapshotError::SizeMismatch { + name: name.to_owned(), + expected: previous.dimensions(), + actual: new.dimensions(), + }); + } } // Compare existing image to the new one: @@ -262,12 +274,15 @@ pub fn try_image_snapshot_options( path: diff_path.clone(), err, })?; - maybe_update_snapshot()?; - Err(SnapshotError::Diff { - name: name.to_owned(), - diff, - diff_path, - }) + if should_update_snapshots() { + update_snapshot() + } else { + Err(SnapshotError::Diff { + name: name.to_owned(), + diff, + diff_path, + }) + } } else { Ok(()) } From ee5f0d6d52ea5311d6bd6acc13105faf3f0cf6a8 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 30 Jan 2025 20:37:29 +0100 Subject: [PATCH 045/132] eframe web: forward cmd-S/O to egui app (stop default browser action) (#5655) This allow eframe apps to capture cmd-S and cmd-O to trigger their own save and open actions, instead of having the browser do something annoying. --- crates/eframe/src/web/events.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index 7e2d07a1b..762f202fa 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -219,9 +219,16 @@ fn should_prevent_default_for_key( // * cmd-shift-C (debug tools) // * cmd/ctrl-c/v/x (lest we prevent copy/paste/cut events) - // Prevent ctrl-P from opening the print dialog. Users may want to use it for a command palette. - if egui_key == egui::Key::P && (modifiers.ctrl || modifiers.command || modifiers.mac_cmd) { - return true; + // Prevent cmd/ctrl plus these keys from triggering the default browser action: + let keys = [ + egui::Key::O, // open + egui::Key::P, // print (cmd-P is common for command palette) + egui::Key::S, // save + ]; + for key in keys { + if egui_key == key && (modifiers.ctrl || modifiers.command || modifiers.mac_cmd) { + return true; + } } if egui_key == egui::Key::Space && !runner.text_agent.has_focus() { From 04fca9c3245883018709f4a50572fd3d71c1be60 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 30 Jan 2025 20:51:12 +0100 Subject: [PATCH 046/132] Remove date button from Scene demo, so as not to fail tests each day (#5657) --- crates/egui_demo_lib/src/demo/scene.rs | 2 +- .../egui_demo_lib/src/demo/widget_gallery.rs | 19 ++++++++++++++++++- .../tests/snapshots/demos/Scene.png | 4 ++-- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/crates/egui_demo_lib/src/demo/scene.rs b/crates/egui_demo_lib/src/demo/scene.rs index a7d268e1f..ba0dc5974 100644 --- a/crates/egui_demo_lib/src/demo/scene.rs +++ b/crates/egui_demo_lib/src/demo/scene.rs @@ -11,7 +11,7 @@ pub struct SceneDemo { impl Default for SceneDemo { fn default() -> Self { Self { - widget_gallery: Default::default(), + widget_gallery: widget_gallery::WidgetGallery::default().with_date_button(false), // disable date button so that we don't fail the snapshot test scene_rect: Rect::ZERO, // `egui::Scene` will initialize this to something valid } } diff --git a/crates/egui_demo_lib/src/demo/widget_gallery.rs b/crates/egui_demo_lib/src/demo/widget_gallery.rs index cfe746899..c51a9efd8 100644 --- a/crates/egui_demo_lib/src/demo/widget_gallery.rs +++ b/crates/egui_demo_lib/src/demo/widget_gallery.rs @@ -22,6 +22,8 @@ pub struct WidgetGallery { #[cfg(feature = "chrono")] #[cfg_attr(feature = "serde", serde(skip))] date: Option, + + with_date_button: bool, } impl Default for WidgetGallery { @@ -38,10 +40,23 @@ impl Default for WidgetGallery { animate_progress_bar: false, #[cfg(feature = "chrono")] date: None, + #[cfg(feature = "chrono")] + with_date_button: true, } } } +impl WidgetGallery { + #[inline] + pub fn with_date_button(mut self, _with_date_button: bool) -> Self { + #[cfg(feature = "chrono")] + { + self.with_date_button = _with_date_button; + } + self + } +} + impl crate::Demo for WidgetGallery { fn name(&self) -> &'static str { "🗄 Widget Gallery" @@ -124,6 +139,8 @@ impl WidgetGallery { animate_progress_bar, #[cfg(feature = "chrono")] date, + #[cfg(feature = "chrono")] + with_date_button, } = self; ui.add(doc_link_label("Label", "label")); @@ -226,7 +243,7 @@ impl WidgetGallery { ui.end_row(); #[cfg(feature = "chrono")] - { + if *with_date_button { let date = date.get_or_insert_with(|| chrono::offset::Utc::now().date_naive()); ui.add(doc_link_label_with_crate( "egui_extras", diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scene.png b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png index d9f483ec1..ae1d6acf7 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:d076f5365bfa87b7e61d3808b8b9b367157ea6e55ccf665720cbd2237d53793d -size 35563 +oid sha256:93f2bf32b0607b56a9eebefdc07b55d66988251eeb0ccf7799c2836281d5d5fb +size 35573 From 4b9da5f6504b693bb621f3952f967337d0421c10 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 30 Jan 2025 21:02:50 +0100 Subject: [PATCH 047/132] Remove `StrokeKind::default` (#5658) Since there is no natural default for `RectShape`. --- crates/epaint/src/stroke.rs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/crates/epaint/src/stroke.rs b/crates/epaint/src/stroke.rs index fa85a9588..ce7d274dd 100644 --- a/crates/epaint/src/stroke.rs +++ b/crates/epaint/src/stroke.rs @@ -69,16 +69,10 @@ pub enum StrokeKind { Outside, } -impl Default for StrokeKind { - fn default() -> Self { - Self::Middle - } -} - /// Describes the width and color of paths. The color can either be solid or provided by a callback. For more information, see [`ColorMode`] /// /// The default stroke is the same as [`Stroke::NONE`]. -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct PathStroke { pub width: f32, @@ -86,6 +80,13 @@ pub struct PathStroke { pub kind: StrokeKind, } +impl Default for PathStroke { + #[inline] + fn default() -> Self { + Self::NONE + } +} + impl PathStroke { /// Same as [`PathStroke::default`]. pub const NONE: Self = Self { @@ -99,7 +100,7 @@ impl PathStroke { Self { width: width.into(), color: ColorMode::Solid(color.into()), - kind: StrokeKind::default(), + kind: StrokeKind::Middle, } } @@ -114,7 +115,7 @@ impl PathStroke { Self { width: width.into(), color: ColorMode::UV(Arc::new(callback)), - kind: StrokeKind::default(), + kind: StrokeKind::Middle, } } @@ -168,7 +169,7 @@ impl From for PathStroke { Self { width: value.width, color: ColorMode::Solid(value.color), - kind: StrokeKind::default(), + kind: StrokeKind::Middle, } } } From 50294b5d9f51d4c599c243669c464f826ac51728 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 30 Jan 2025 21:04:36 +0100 Subject: [PATCH 048/132] Be smarter when rounding rectangles to the pixel grid (#5656) --- .../tests/snapshots/demos/Scene.png | 4 +- crates/epaint/src/shapes/rect_shape.rs | 6 +- crates/epaint/src/tessellator.rs | 138 +++++++++++------- 3 files changed, 90 insertions(+), 58 deletions(-) diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scene.png b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png index ae1d6acf7..78f697490 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:93f2bf32b0607b56a9eebefdc07b55d66988251eeb0ccf7799c2836281d5d5fb -size 35573 +oid sha256:9fee66bab841602862c301a7df3add365ea93fde0ac9b71ca7f9b74a709a68d2 +size 35576 diff --git a/crates/epaint/src/shapes/rect_shape.rs b/crates/epaint/src/shapes/rect_shape.rs index ca7ab01f1..f691234b1 100644 --- a/crates/epaint/src/shapes/rect_shape.rs +++ b/crates/epaint/src/shapes/rect_shape.rs @@ -21,13 +21,13 @@ pub struct RectShape { pub stroke: Stroke, /// Is the stroke on the inside, outside, or centered on the rectangle? + /// + /// If you want to perfectly tile rectangles, use [`StrokeKind::Inside`]. pub stroke_kind: StrokeKind, /// Snap the rectangle to pixels? /// /// Rounding produces sharper rectangles. - /// It is the outside of the fill (=inside of the stroke) - /// that will be rounded to the physical pixel grid. /// /// If `None`, [`crate::TessellationOptions::round_rects_to_pixels`] will be used. pub round_to_pixels: Option, @@ -117,8 +117,6 @@ impl RectShape { /// Snap the rectangle to pixels? /// /// Rounding produces sharper rectangles. - /// It is the outside of the fill (=inside of the stroke) - /// that will be rounded to the physical pixel grid. /// /// If `None`, [`crate::TessellationOptions::round_rects_to_pixels`] will be used. #[inline] diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index f05d9b47a..45919759a 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -8,14 +8,12 @@ use emath::{pos2, remap, vec2, GuiRounding as _, NumExt, Pos2, Rect, Rot2, Vec2}; use crate::{ - color, emath, stroke, texture_atlas::PreparedDisc, CircleShape, ClippedPrimitive, ClippedShape, - Color32, CubicBezierShape, EllipseShape, Mesh, PathShape, Primitive, QuadraticBezierShape, - RectShape, Rounding, Shape, Stroke, StrokeKind, TextShape, TextureId, Vertex, WHITE_UV, + color::ColorMode, emath, stroke::PathStroke, texture_atlas::PreparedDisc, CircleShape, + ClippedPrimitive, ClippedShape, Color32, CubicBezierShape, EllipseShape, Mesh, PathShape, + Primitive, QuadraticBezierShape, RectShape, Rounding, Shape, Stroke, StrokeKind, TextShape, + TextureId, Vertex, WHITE_UV, }; -use self::color::ColorMode; -use self::stroke::PathStroke; - // ---------------------------------------------------------------------------- #[allow(clippy::approx_constant)] @@ -920,13 +918,13 @@ fn fill_closed_path_with_uv( #[inline(always)] fn translate_stroke_point(p: &mut PathPoint, stroke: &PathStroke) { match stroke.kind { - stroke::StrokeKind::Middle => { /* Nothing to do */ } - stroke::StrokeKind::Outside => { - p.pos += p.normal * stroke.width * 0.5; - } - stroke::StrokeKind::Inside => { + StrokeKind::Inside => { p.pos -= p.normal * stroke.width * 0.5; } + StrokeKind::Middle => { /* Nothing to do */ } + StrokeKind::Outside => { + p.pos += p.normal * stroke.width * 0.5; + } } } @@ -947,7 +945,7 @@ fn stroke_path( let idx = out.vertices.len() as u32; // Translate the points along their normals if the stroke is outside or inside - if stroke.kind != stroke::StrokeKind::Middle { + if stroke.kind != StrokeKind::Middle { path.iter_mut() .for_each(|p| translate_stroke_point(p, stroke)); } @@ -1672,12 +1670,18 @@ impl Tessellator { /// * `rect`: the rectangle to tessellate. /// * `out`: triangles are appended to this. pub fn tessellate_rect(&mut self, rect_shape: &RectShape, out: &mut Mesh) { + if self.options.coarse_tessellation_culling + && !rect_shape.visual_bounding_rect().intersects(self.clip_rect) + { + return; + } + let brush = rect_shape.brush.as_ref(); let RectShape { mut rect, mut rounding, fill, - stroke, + mut stroke, stroke_kind, round_to_pixels, mut blur_width, @@ -1686,9 +1690,56 @@ impl Tessellator { let round_to_pixels = round_to_pixels.unwrap_or(self.options.round_rects_to_pixels); - // Modify `rect` so that it represents the filled region, with the stroke on the outside: + // Important: round to pixels BEFORE applying stroke_kind + if round_to_pixels { + // The rounding is aware of the stroke kind. + // It is designed to be clever in trying to divine the intentions of the user. + match stroke_kind { + StrokeKind::Inside => { + // The stroke is inside the rect, so the rect defines the _outside_ of the stroke. + // We round the outside of the stroke on a pixel boundary. + // This will make the outside of the stroke crisp. + // + // Will make each stroke asymmetric if not an even multiple of physical pixels, + // but the left stroke will always be the mirror image of the right stroke, + // and the top stroke will always be the mirror image of the bottom stroke. + // + // This is so that a user can tile rectangles with `StrokeKind::Inside`, + // and get no pixel overlap between them. + rect = rect.round_to_pixels(self.pixels_per_point); + } + StrokeKind::Middle => { + // On this path we optimize for crisp and symmetric strokes. + // We put odd-width strokes in the center of pixels. + // To understand why, see `fn round_line_segment`. + if stroke.width <= self.feathering + || is_nearest_integer_odd(self.pixels_per_point * stroke.width) + { + rect = rect.round_to_pixel_center(self.pixels_per_point); + } else { + rect = rect.round_to_pixels(self.pixels_per_point); + } + } + StrokeKind::Outside => { + // Put the inside of the stroke on a pixel boundary. + // Makes the inside of the stroke and the filled rect crisp, + // but the outside of the stroke may become feathered (blurry). + // + // Will make each stroke asymmetric if not an even multiple of physical pixels, + // but the left stroke will always be the mirror image of the right stroke, + // and the top stroke will always be the mirror image of the bottom stroke. + rect = rect.round_to_pixels(self.pixels_per_point); + } + } + } + + // Modify `rect` so that it represents the filled region, with the stroke on the outside. + // Important: do this AFTER rounding to pixels match stroke_kind { StrokeKind::Inside => { + // Shrink the stroke so it fits inside the rect: + stroke.width = stroke.width.at_most(rect.size().min_elem() / 2.0); + rect = rect.shrink(stroke.width); } StrokeKind::Middle => { @@ -1699,28 +1750,6 @@ impl Tessellator { } } - if self.options.coarse_tessellation_culling - && !rect.expand(stroke.width).intersects(self.clip_rect) - { - return; - } - - if round_to_pixels { - // Since the stroke extends outside of the rectangle, - // we can round the rectangle sides to the physical pixel edges, - // and the filled rect will appear crisp, as will the inside of the stroke. - let Stroke { width, .. } = stroke; // Make sure we remember to update this if we change `stroke` to `PathStroke` - if width <= self.feathering && !stroke.is_empty() { - // If the stroke is thin, make sure its center is in the center of the pixel: - rect = rect - .expand(width / 2.0) - .round_to_pixel_center(self.pixels_per_point) - .shrink(width / 2.0); - } else { - rect = rect.round_to_pixels(self.pixels_per_point); - } - } - // It is common to (sometimes accidentally) create an infinitely sized rectangle. // Make sure we can handle that: rect.min = rect.min.at_least(pos2(-1e7, -1e7)); @@ -1751,6 +1780,7 @@ impl Tessellator { if rect.width() < 0.5 * self.feathering { // Very thin - approximate by a vertical line-segment: + // There is room for improvement here, but it is not critical. let line = [rect.center_top(), rect.center_bottom()]; if 0.0 < rect.width() && fill != Color32::TRANSPARENT { self.tessellate_line_segment(line, Stroke::new(rect.width(), fill), out); @@ -1761,6 +1791,7 @@ impl Tessellator { } } else if rect.height() < 0.5 * self.feathering { // Very thin - approximate by a horizontal line-segment: + // There is room for improvement here, but it is not critical. let line = [rect.left_center(), rect.right_center()]; if 0.0 < rect.height() && fill != Color32::TRANSPARENT { self.tessellate_line_segment(line, Stroke::new(rect.height(), fill), out); @@ -1776,22 +1807,25 @@ impl Tessellator { path.add_line_loop(&self.scratchpad_points); let path_stroke = PathStroke::from(stroke).outside(); - if let Some(brush) = brush { - let crate::Brush { - fill_texture_id, - uv, - } = **brush; - // Textured - let uv_from_pos = |p: Pos2| { - pos2( - remap(p.x, rect.x_range(), uv.x_range()), - remap(p.y, rect.y_range(), uv.y_range()), - ) - }; - path.fill_with_uv(self.feathering, fill, fill_texture_id, uv_from_pos, out); - } else { - // Untextured - path.fill(self.feathering, fill, &path_stroke, out); + if rect.is_positive() { + // Fill + if let Some(brush) = brush { + // Textured + let crate::Brush { + fill_texture_id, + uv, + } = **brush; + let uv_from_pos = |p: Pos2| { + pos2( + remap(p.x, rect.x_range(), uv.x_range()), + remap(p.y, rect.y_range(), uv.y_range()), + ) + }; + path.fill_with_uv(self.feathering, fill, fill_texture_id, uv_from_pos, out); + } else { + // Untextured + path.fill(self.feathering, fill, &path_stroke, out); + } } path.stroke_closed(self.feathering, &path_stroke, out); From 99369666eea587f98dc458347e0b9daa6ac99c99 Mon Sep 17 00:00:00 2001 From: Michael Grupp Date: Mon, 3 Feb 2025 14:26:02 +0100 Subject: [PATCH 049/132] Fix typo in kittest docs (#5667) * ~Closes ~ (just a quick typo fix) * [x] I have followed the instructions in the PR template --- crates/egui_kittest/src/snapshot.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/egui_kittest/src/snapshot.rs b/crates/egui_kittest/src/snapshot.rs index 409c287c8..0b68677b0 100644 --- a/crates/egui_kittest/src/snapshot.rs +++ b/crates/egui_kittest/src/snapshot.rs @@ -177,7 +177,7 @@ fn should_update_snapshots() -> bool { /// The snapshot files will be saved under [`SnapshotOptions::output_path`]. /// The snapshot will be saved under `{output_path}/{name}.png`. /// The new image from the most recent test run will be saved under `{output_path}/{name}.new.png`. -/// If new image didn't match the snapshot, a diff image will be saved under `{output_path}/{name}.diff.png`. +/// If the new image didn't match the snapshot, a diff image will be saved under `{output_path}/{name}.diff.png`. /// /// If the env-var `UPDATE_SNAPSHOTS` is set, then the old image will backed up under `{output_path}/{name}.old.png`. /// and then new image will be written to `{output_path}/{name}.png` @@ -296,7 +296,7 @@ pub fn try_image_snapshot_options( /// The snapshot files will be saved under [`SnapshotOptions::output_path`]. /// The snapshot will be saved under `{output_path}/{name}.png`. /// The new image from the most recent test run will be saved under `{output_path}/{name}.new.png`. -/// If new image didn't match the snapshot, a diff image will be saved under `{output_path}/{name}.diff.png`. +/// If the new image didn't match the snapshot, a diff image will be saved under `{output_path}/{name}.diff.png`. /// /// # Errors /// Returns a [`SnapshotError`] if the image does not match the snapshot or if there was an error @@ -316,7 +316,7 @@ pub fn try_image_snapshot(current: &image::RgbaImage, name: &str) -> Result<(), /// The snapshot files will be saved under [`SnapshotOptions::output_path`]. /// The snapshot will be saved under `{output_path}/{name}.png`. /// The new image from the most recent test run will be saved under `{output_path}/{name}.new.png`. -/// If new image didn't match the snapshot, a diff image will be saved under `{output_path}/{name}.diff.png`. +/// If the new image didn't match the snapshot, a diff image will be saved under `{output_path}/{name}.diff.png`. /// /// # Panics /// Panics if the image does not match the snapshot or if there was an error reading or writing the @@ -334,7 +334,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 new image didn't match the snapshot, a diff image will be saved under `tests/snapshots/{name}.diff.png`. +/// If the new image didn't match the snapshot, a diff image will be saved under `tests/snapshots/{name}.diff.png`. /// /// # Panics /// Panics if the image does not match the snapshot or if there was an error reading or writing the @@ -351,7 +351,7 @@ pub fn image_snapshot(current: &image::RgbaImage, name: &str) { #[cfg(feature = "wgpu")] impl Harness<'_, State> { - /// Render a image using the setup [`crate::TestRenderer`] and compare it to the snapshot + /// Render an image using the setup [`crate::TestRenderer`] and compare it to the snapshot /// with custom options. /// /// If you want to change the default options for your whole project, you could create an @@ -364,7 +364,7 @@ impl Harness<'_, State> { /// The snapshot files will be saved under [`SnapshotOptions::output_path`]. /// The snapshot will be saved under `{output_path}/{name}.png`. /// The new image from the most recent test run will be saved under `{output_path}/{name}.new.png`. - /// If new image didn't match the snapshot, a diff image will be saved under `{output_path}/{name}.diff.png`. + /// If the new image didn't match the snapshot, a diff image will be saved under `{output_path}/{name}.diff.png`. /// /// # Errors /// Returns a [`SnapshotError`] if the image does not match the snapshot, if there was an @@ -380,10 +380,10 @@ impl Harness<'_, State> { try_image_snapshot_options(&image, name, options) } - /// Render a image using the setup [`crate::TestRenderer`] and compare it to the snapshot. + /// Render an image using the setup [`crate::TestRenderer`] and compare it to the snapshot. /// 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 new image didn't match the snapshot, a diff image will be saved under `tests/snapshots/{name}.diff.png`. + /// If the new image didn't match the snapshot, a diff image will be saved under `tests/snapshots/{name}.diff.png`. /// /// # Errors /// Returns a [`SnapshotError`] if the image does not match the snapshot, if there was an @@ -395,7 +395,7 @@ impl Harness<'_, State> { try_image_snapshot(&image, name) } - /// Render a image using the setup [`crate::TestRenderer`] and compare it to the snapshot + /// Render an image using the setup [`crate::TestRenderer`] and compare it to the snapshot /// with custom options. /// /// If you want to change the default options for your whole project, you could create an @@ -408,7 +408,7 @@ impl Harness<'_, State> { /// The snapshot files will be saved under [`SnapshotOptions::output_path`]. /// The snapshot will be saved under `{output_path}/{name}.png`. /// The new image from the most recent test run will be saved under `{output_path}/{name}.new.png`. - /// If new image didn't match the snapshot, a diff image will be saved under `{output_path}/{name}.diff.png`. + /// If the new image didn't match the snapshot, a diff image will be saved under `{output_path}/{name}.diff.png`. /// /// # Panics /// Panics if the image does not match the snapshot, if there was an error reading or writing the @@ -423,10 +423,10 @@ impl Harness<'_, State> { } } - /// Render a image using the setup [`crate::TestRenderer`] and compare it to the snapshot. + /// Render an image using the setup [`crate::TestRenderer`] and compare it to the snapshot. /// 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 new image didn't match the snapshot, a diff image will be saved under `tests/snapshots/{name}.diff.png`. + /// If the new image didn't match the snapshot, a diff image will be saved under `tests/snapshots/{name}.diff.png`. /// /// # Panics /// Panics if the image does not match the snapshot, if there was an error reading or writing the From b6aa897b9dbaee34bb623217679f03ece4473adb Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Tue, 4 Feb 2025 10:16:51 +0100 Subject: [PATCH 050/132] Fix the format + check + test workflow (#5671) * [x] I have followed the instructions in the PR template This broke because upload-artefact v3 was deprecated: https://github.com/emilk/egui/actions/runs/13112546294/job/36579426301?pr=5670 From bac8ea09ac696ff66432111c9b487b192c8e40bc Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Tue, 4 Feb 2025 10:17:15 +0100 Subject: [PATCH 051/132] Add docs to `Frame::new` (#5670) * [x] I have followed the instructions in the PR template --- crates/egui/src/containers/frame.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/egui/src/containers/frame.rs b/crates/egui/src/containers/frame.rs index abe1b8afe..4386f8f86 100644 --- a/crates/egui/src/containers/frame.rs +++ b/crates/egui/src/containers/frame.rs @@ -163,6 +163,9 @@ impl Frame { shadow: Shadow::NONE, }; + /// No colors, no margins, no border. + /// + /// Same as [`Frame::NONE`]. pub const fn new() -> Self { Self::NONE } From 9e1117019ae5ae61edf15c7148134bd5931bd985 Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Tue, 4 Feb 2025 04:29:26 -0500 Subject: [PATCH 052/132] Add `Color32::CYAN` and `Color32::MAGENTA` (#5663) I often find myself reaching out for these two colors so I figured I'd just add the constants next to `YELLOW` to have CMYK --- crates/ecolor/src/color32.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/ecolor/src/color32.rs b/crates/ecolor/src/color32.rs index c38a8d6b9..52e322667 100644 --- a/crates/ecolor/src/color32.rs +++ b/crates/ecolor/src/color32.rs @@ -56,7 +56,10 @@ impl Color32 { pub const RED: Self = Self::from_rgb(255, 0, 0); pub const LIGHT_RED: Self = Self::from_rgb(255, 128, 128); + pub const CYAN: Self = Self::from_rgb(0, 255, 255); + pub const MAGENTA: Self = Self::from_rgb(255, 0, 255); pub const YELLOW: Self = Self::from_rgb(255, 255, 0); + pub const ORANGE: Self = Self::from_rgb(255, 165, 0); pub const LIGHT_YELLOW: Self = Self::from_rgb(255, 255, 0xE0); pub const KHAKI: Self = Self::from_rgb(240, 230, 140); From 3c07e01d089f336ed30689179f38b6e752952201 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 4 Feb 2025 11:30:12 +0100 Subject: [PATCH 053/132] Improve tessellation quality (#5669) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Defining what `Rounding` is This PR defines what `Rounding` means: it is the corner radius of underlying `RectShape` rectangle. If you use `StrokeKind::Inside`, this means the rounding is of the outer part of the stroke. Conversely, if you use `StrokeKind::Outside`, the stroke is outside the rounded rectangle, so the stroke has an inner radius or `rounding`, and an outer radius that is larger by `stroke.width`. This definitions is the same as Figma uses. ## Improving general shape rendering The rendering of filled shapes (rectangles, circles, paths, bezier) has been rewritten. Instead of first painting the fill with the stroke on top, we now paint them as one single mesh with shared vertices at the border. This has several benefits: * Less work (faster and with fewer vertices produced) * No overdraw (nicer rendering of translucent shapes) * Correct blending of stroke and fill The logic for rendering thin strokes has also been improved, so that the width of a stroke of `StrokeKind::Outside` never affects the filled area (this used to be wrong for thin strokes). ## Improving of rectangle rendering Rectangles also has specific improvements in how thin rectangles are painted. The handling of "Blur width" is also a lot better, and now works for rectangles with strokes. There also used to be bugs with specific combinations of corner radius and stroke width, that are now fixed. ## But why? With the new `egui::Scene` we end up with a lot of zoomed out shapes, with sub-pixel strokes. These need to look good! One thing led to another, and then I became obsessive 😅 ## Tessellation Test In order to investigate the rendering, I created a Tessellation Test in the `egui_demo_lib`. [Try it here](https://egui-pr-preview.github.io/pr/5669-emilkimprove-tessellator) ![Screenshot 2025-02-04 at 08 45 50](https://github.com/user-attachments/assets/20b47a30-de6a-4ff5-885b-2e2fd6d88321) ![image](https://github.com/user-attachments/assets/e17c50eb-5ae7-48d4-bb0d-4f2165075897) --- crates/ecolor/src/color32.rs | 38 ++ crates/egui/src/containers/frame.rs | 15 +- crates/egui/src/containers/window.rs | 3 +- crates/egui/src/introspection.rs | 2 +- .../tests/snapshots/imageviewer.png | 4 +- .../src/demo/demo_app_windows.rs | 1 + .../src/demo/misc_demo_window.rs | 6 +- crates/egui_demo_lib/src/demo/tests/mod.rs | 2 + .../src/demo/tests/tessellation_test.rs | 364 +++++++++++ .../tests/snapshots/demos/Bézier Curve.png | 4 +- .../tests/snapshots/demos/Code Editor.png | 4 +- .../tests/snapshots/demos/Code Example.png | 4 +- .../tests/snapshots/demos/Context Menus.png | 4 +- .../tests/snapshots/demos/Dancing Strings.png | 4 +- .../tests/snapshots/demos/Drag and Drop.png | 4 +- .../tests/snapshots/demos/Extra Viewport.png | 4 +- .../tests/snapshots/demos/Font Book.png | 4 +- .../tests/snapshots/demos/Frame.png | 4 +- .../tests/snapshots/demos/Highlighting.png | 4 +- .../snapshots/demos/Interactive Container.png | 4 +- .../tests/snapshots/demos/Misc Demos.png | 4 +- .../tests/snapshots/demos/Modals.png | 4 +- .../tests/snapshots/demos/Multi Touch.png | 4 +- .../tests/snapshots/demos/Painting.png | 4 +- .../tests/snapshots/demos/Panels.png | 4 +- .../tests/snapshots/demos/Scene.png | 4 +- .../tests/snapshots/demos/Screenshot.png | 4 +- .../tests/snapshots/demos/Scrolling.png | 4 +- .../tests/snapshots/demos/Sliders.png | 4 +- .../tests/snapshots/demos/Strip.png | 4 +- .../tests/snapshots/demos/Table.png | 4 +- .../tests/snapshots/demos/Text Layout.png | 4 +- .../tests/snapshots/demos/TextEdit.png | 4 +- .../tests/snapshots/demos/Tooltips.png | 4 +- .../tests/snapshots/demos/Undo Redo.png | 4 +- .../tests/snapshots/demos/Window Options.png | 4 +- .../tests/snapshots/modals_1.png | 4 +- .../tests/snapshots/modals_2.png | 4 +- .../tests/snapshots/modals_3.png | 4 +- ...rop_should_prevent_focusing_lower_area.png | 4 +- .../snapshots/rendering_test/dpi_1.00.png | 4 +- .../snapshots/rendering_test/dpi_1.25.png | 4 +- .../snapshots/rendering_test/dpi_1.50.png | 4 +- .../snapshots/rendering_test/dpi_1.67.png | 4 +- .../snapshots/rendering_test/dpi_1.75.png | 4 +- .../snapshots/rendering_test/dpi_2.00.png | 4 +- .../tests/snapshots/tessellation_test.png | 3 + .../tessellation_test/Blurred stroke.png | 3 + .../snapshots/tessellation_test/Blurred.png | 3 + .../tessellation_test/Minimal rounding.png | 3 + .../snapshots/tessellation_test/Normal.png | 3 + .../Thick stroke, minimal rounding.png | 3 + .../tessellation_test/Thin filled.png | 3 + .../tessellation_test/Thin stroked.png | 3 + .../tests/snapshots/widget_gallery.png | 4 +- crates/egui_kittest/src/snapshot.rs | 7 +- .../tests/snapshots/combobox_opened.png | 4 +- crates/epaint/src/mesh.rs | 7 + crates/epaint/src/rounding.rs | 46 +- crates/epaint/src/shapes/rect_shape.rs | 11 +- crates/epaint/src/shapes/shape.rs | 29 +- crates/epaint/src/stroke.rs | 8 + crates/epaint/src/tessellator.rs | 569 +++++++++++------- 63 files changed, 947 insertions(+), 345 deletions(-) create mode 100644 crates/egui_demo_lib/src/demo/tests/tessellation_test.rs create mode 100644 crates/egui_demo_lib/tests/snapshots/tessellation_test.png create mode 100644 crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png create mode 100644 crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png create mode 100644 crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png create mode 100644 crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png create mode 100644 crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png create mode 100644 crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png create mode 100644 crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png diff --git a/crates/ecolor/src/color32.rs b/crates/ecolor/src/color32.rs index 52e322667..87f4915cf 100644 --- a/crates/ecolor/src/color32.rs +++ b/crates/ecolor/src/color32.rs @@ -72,6 +72,8 @@ impl Color32 { pub const BLUE: Self = Self::from_rgb(0, 0, 255); pub const LIGHT_BLUE: Self = Self::from_rgb(0xAD, 0xD8, 0xE6); + pub const PURPLE: Self = Self::from_rgb(0x80, 0, 0x80); + pub const GOLD: Self = Self::from_rgb(255, 215, 0); pub const DEBUG_COLOR: Self = Self::from_rgba_premultiplied(0, 200, 0, 128); @@ -233,6 +235,23 @@ impl Color32 { ]) } + /// Multiply with 127 to make color half as opaque, perceptually. + /// + /// Fast multiplication in gamma-space. + /// + /// This is perceptually even, and faster that [`Self::linear_multiply`]. + #[inline] + pub fn gamma_multiply_u8(self, factor: u8) -> Self { + let Self([r, g, b, a]) = self; + let factor = factor as u32; + Self([ + ((r as u32 * factor + 127) / 255) as u8, + ((g as u32 * factor + 127) / 255) as u8, + ((b as u32 * factor + 127) / 255) as u8, + ((a as u32 * factor + 127) / 255) as u8, + ]) + } + /// Multiply with 0.5 to make color half as opaque in linear space. /// /// This is using linear space, which is not perceptually even. @@ -271,6 +290,11 @@ impl Color32 { fast_round(lerp((self[3] as f32)..=(other[3] as f32), t)), ) } + + /// Blend two colors, so that `self` is behind the argument. + pub fn blend(self, on_top: Self) -> Self { + self.gamma_multiply_u8(255 - on_top.a()) + on_top + } } impl std::ops::Mul for Color32 { @@ -287,3 +311,17 @@ impl std::ops::Mul for Color32 { ]) } } + +impl std::ops::Add for Color32 { + type Output = Self; + + #[inline] + fn add(self, other: Self) -> Self { + Self([ + self[0].saturating_add(other[0]), + self[1].saturating_add(other[1]), + self[2].saturating_add(other[2]), + self[3].saturating_add(other[3]), + ]) + } +} diff --git a/crates/egui/src/containers/frame.rs b/crates/egui/src/containers/frame.rs index 4386f8f86..07ab8e28a 100644 --- a/crates/egui/src/containers/frame.rs +++ b/crates/egui/src/containers/frame.rs @@ -115,7 +115,10 @@ pub struct Frame { #[doc(alias = "border")] pub stroke: Stroke, - /// The rounding of the corners of [`Self::stroke`] and [`Self::fill`]. + /// The rounding of the _outer_ corner of the [`Self::stroke`] + /// (or, if there is no stroke, the outer corner of [`Self::fill`]). + /// + /// In other words, this is the corner radius of the _widget rect_. pub rounding: Rounding, /// Margin outside the painted frame. @@ -269,7 +272,10 @@ impl Frame { self } - /// The rounding of the corners of [`Self::stroke`] and [`Self::fill`]. + /// The rounding of the _outer_ corner of the [`Self::stroke`] + /// (or, if there is no stroke, the outer corner of [`Self::fill`]). + /// + /// In other words, this is the corner radius of the _widget rect_. #[inline] pub fn rounding(mut self, rounding: impl Into) -> Self { self.rounding = rounding.into(); @@ -423,15 +429,14 @@ impl Frame { shadow, } = *self; - let fill_rect = self.fill_rect(content_rect); let widget_rect = self.widget_rect(content_rect); let frame_shape = Shape::Rect(epaint::RectShape::new( - fill_rect, + widget_rect, rounding, fill, stroke, - epaint::StrokeKind::Outside, + epaint::StrokeKind::Inside, )); if shadow == Default::default() { diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index 215f6322d..75f4ac8e0 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -611,7 +611,8 @@ impl Window<'_> { title_bar.inner_rect.round_to_pixels(ctx.pixels_per_point()); if on_top && area_content_ui.visuals().window_highlight_topmost { - let mut round = window_frame.rounding; + let mut round = + window_frame.rounding - window_frame.stroke.width.round() as u8; if !is_collapsed { round.se = 0; diff --git a/crates/egui/src/introspection.rs b/crates/egui/src/introspection.rs index 85e1ee6c6..e21ddb3a5 100644 --- a/crates/egui/src/introspection.rs +++ b/crates/egui/src/introspection.rs @@ -160,7 +160,7 @@ impl Widget for &mut epaint::TessellationOptions { .on_hover_text("Apply feathering to smooth out the edges of shapes. Turn off for small performance gain."); if *feathering { - ui.add(crate::DragValue::new(feathering_size_in_pixels).range(0.0..=10.0).speed(0.1).suffix(" px")); + ui.add(crate::DragValue::new(feathering_size_in_pixels).range(0.0..=10.0).speed(0.025).suffix(" px")); } }); diff --git a/crates/egui_demo_app/tests/snapshots/imageviewer.png b/crates/egui_demo_app/tests/snapshots/imageviewer.png index 750fa3577..62624506d 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:2292f0f80bfd3c80055a72eb983549ac2875d36acb333732bd0a67e51b24ae4f -size 102983 +oid sha256:57274cec5ee7e5522073249b931ea65ead22752aea1de40666543e765c1b6b85 +size 102929 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 c8b142cac..27f862ad0 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -100,6 +100,7 @@ impl Default for DemoGroups { Box::::default(), Box::::default(), Box::::default(), + Box::::default(), Box::::default(), ]), } 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 5f1022ffc..ce154ab4a 100644 --- a/crates/egui_demo_lib/src/demo/misc_demo_window.rs +++ b/crates/egui_demo_lib/src/demo/misc_demo_window.rs @@ -124,9 +124,9 @@ impl View for MiscDemoWindow { ) .changed() { - self.checklist - .iter_mut() - .for_each(|checked| *checked = all_checked); + for check in &mut self.checklist { + *check = all_checked; + } } for (i, checked) in self.checklist.iter_mut().enumerate() { ui.checkbox(checked, format!("Item {}", i + 1)); diff --git a/crates/egui_demo_lib/src/demo/tests/mod.rs b/crates/egui_demo_lib/src/demo/tests/mod.rs index 13332fea7..d9fad5382 100644 --- a/crates/egui_demo_lib/src/demo/tests/mod.rs +++ b/crates/egui_demo_lib/src/demo/tests/mod.rs @@ -6,6 +6,7 @@ mod input_event_history; mod input_test; mod layout_test; mod manual_layout_test; +mod tessellation_test; mod window_resize_test; pub use clipboard_test::ClipboardTest; @@ -16,4 +17,5 @@ pub use input_event_history::InputEventHistory; pub use input_test::InputTest; pub use layout_test::LayoutTest; pub use manual_layout_test::ManualLayoutTest; +pub use tessellation_test::TessellationTest; pub use window_resize_test::WindowResizeTest; diff --git a/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs b/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs new file mode 100644 index 000000000..033861016 --- /dev/null +++ b/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs @@ -0,0 +1,364 @@ +use egui::{ + emath::{GuiRounding, TSTransform}, + epaint::{self, RectShape}, + vec2, Color32, Pos2, Rect, Sense, StrokeKind, Vec2, +}; + +#[derive(Clone, Debug, PartialEq)] +pub struct TessellationTest { + shape: RectShape, + + magnification_pixel_size: f32, + tessellation_options: epaint::TessellationOptions, + paint_edges: bool, +} + +impl Default for TessellationTest { + fn default() -> Self { + let shape = Self::interesting_shapes()[0].1.clone(); + Self { + shape, + magnification_pixel_size: 12.0, + tessellation_options: Default::default(), + paint_edges: false, + } + } +} + +impl TessellationTest { + fn interesting_shapes() -> Vec<(&'static str, RectShape)> { + fn sized(size: impl Into) -> Rect { + Rect::from_center_size(Pos2::ZERO, size.into()) + } + + let baby_blue = Color32::from_rgb(0, 181, 255); + + let mut shapes = vec![ + ( + "Normal", + RectShape::new( + sized([20.0, 16.0]), + 2.0, + baby_blue, + (1.0, Color32::WHITE), + StrokeKind::Inside, + ), + ), + ( + "Minimal rounding", + RectShape::new( + sized([20.0, 16.0]), + 1.0, + baby_blue, + (1.0, Color32::WHITE), + StrokeKind::Inside, + ), + ), + ( + "Thin filled", + RectShape::filled(sized([20.0, 0.5]), 2.0, baby_blue), + ), + ( + "Thin stroked", + RectShape::new( + sized([20.0, 0.5]), + 2.0, + baby_blue, + (0.5, Color32::WHITE), + StrokeKind::Inside, + ), + ), + ( + "Blurred", + RectShape::filled(sized([20.0, 16.0]), 2.0, baby_blue).with_blur_width(50.0), + ), + ( + "Thick stroke, minimal rounding", + RectShape::new( + sized([20.0, 16.0]), + 1.0, + baby_blue, + (3.0, Color32::WHITE), + StrokeKind::Inside, + ), + ), + ( + "Blurred stroke", + RectShape::new( + sized([20.0, 16.0]), + 0.0, + baby_blue, + (5.0, Color32::WHITE), + StrokeKind::Inside, + ) + .with_blur_width(5.0), + ), + ]; + + for (_name, shape) in &mut shapes { + shape.round_to_pixels = Some(true); + } + + shapes + } +} + +impl crate::Demo for TessellationTest { + fn name(&self) -> &'static str { + "Tessellation Test" + } + + fn show(&mut self, ctx: &egui::Context, open: &mut bool) { + egui::Window::new(self.name()) + .resizable(false) + .open(open) + .show(ctx, |ui| { + use crate::View as _; + self.ui(ui); + }); + } +} + +impl crate::View for TessellationTest { + fn ui(&mut self, ui: &mut egui::Ui) { + ui.add(crate::egui_github_link_file!()); + egui::reset_button(ui, self, "Reset"); + + ui.horizontal(|ui| { + ui.group(|ui| { + ui.vertical(|ui| { + rect_shape_ui(ui, &mut self.shape); + }); + }); + + ui.group(|ui| { + ui.vertical(|ui| { + ui.heading("Real size"); + egui::Frame::dark_canvas(ui.style()).show(ui, |ui| { + let (resp, painter) = + ui.allocate_painter(Vec2::splat(128.0), Sense::hover()); + let canvas = resp.rect; + + let pixels_per_point = ui.pixels_per_point(); + let pixel_size = 1.0 / pixels_per_point; + let mut shape = self.shape.clone(); + shape.rect = Rect::from_center_size(canvas.center(), shape.rect.size()) + .round_to_pixel_center(pixels_per_point) + .translate(Vec2::new(pixel_size / 3.0, pixel_size / 5.0)); // Intentionally offset to test the effect of rounding + painter.add(shape); + }); + }); + }); + }); + + ui.group(|ui| { + ui.heading("Zoomed in"); + let magnification_pixel_size = &mut self.magnification_pixel_size; + let tessellation_options = &mut self.tessellation_options; + + egui::Grid::new("TessellationOptions") + .num_columns(2) + .spacing([12.0, 8.0]) + .striped(true) + .show(ui, |ui| { + ui.label("Magnification"); + ui.add( + egui::DragValue::new(magnification_pixel_size) + .speed(0.5) + .range(0.0..=64.0), + ); + ui.end_row(); + + ui.label("Feathering width"); + ui.horizontal(|ui| { + ui.checkbox(&mut tessellation_options.feathering, ""); + ui.add_enabled( + tessellation_options.feathering, + egui::DragValue::new( + &mut tessellation_options.feathering_size_in_pixels, + ) + .speed(0.1) + .range(0.0..=4.0) + .suffix(" px"), + ); + }); + ui.end_row(); + + ui.label("Paint edges"); + ui.checkbox(&mut self.paint_edges, ""); + ui.end_row(); + }); + + let magnification_pixel_size = *magnification_pixel_size; + + egui::Frame::dark_canvas(ui.style()).show(ui, |ui| { + let (resp, painter) = ui.allocate_painter( + magnification_pixel_size * (self.shape.rect.size() + Vec2::splat(8.0)), + Sense::hover(), + ); + let canvas = resp.rect; + + let mut shape = self.shape.clone(); + shape.rect = shape.rect.translate(Vec2::new(1.0 / 3.0, 1.0 / 5.0)); // Intentionally offset to test the effect of rounding + + let mut mesh = epaint::Mesh::default(); + let mut tessellator = epaint::Tessellator::new( + 1.0, + *tessellation_options, + ui.fonts(|f| f.font_image_size()), + vec![], + ); + tessellator.tessellate_rect(&shape, &mut mesh); + + // Scale and position the mesh: + mesh.transform( + TSTransform::from_translation(canvas.center().to_vec2()) + * TSTransform::from_scaling(magnification_pixel_size), + ); + let mesh = std::sync::Arc::new(mesh); + painter.add(epaint::Shape::mesh(mesh.clone())); + + if self.paint_edges { + let stroke = epaint::Stroke::new(0.5, Color32::MAGENTA); + for triangle in mesh.triangles() { + let a = mesh.vertices[triangle[0] as usize]; + let b = mesh.vertices[triangle[1] as usize]; + let c = mesh.vertices[triangle[2] as usize]; + + painter.line_segment([a.pos, b.pos], stroke); + painter.line_segment([b.pos, c.pos], stroke); + painter.line_segment([c.pos, a.pos], stroke); + } + } + + // Draw pixel centers: + let pixel_radius = 0.75; + let pixel_color = Color32::GRAY; + for yi in 0.. { + let y = (yi as f32 + 0.5) * magnification_pixel_size; + if y > canvas.height() / 2.0 { + break; + } + for xi in 0.. { + let x = (xi as f32 + 0.5) * magnification_pixel_size; + if x > canvas.width() / 2.0 { + break; + } + for offset in [vec2(x, y), vec2(x, -y), vec2(-x, y), vec2(-x, -y)] { + painter.circle_filled( + canvas.center() + offset, + pixel_radius, + pixel_color, + ); + } + } + } + }); + }); + } +} + +fn rect_shape_ui(ui: &mut egui::Ui, shape: &mut RectShape) { + egui::ComboBox::from_id_salt("prefabs") + .selected_text("Prefabs") + .show_ui(ui, |ui| { + for (name, prefab) in TessellationTest::interesting_shapes() { + ui.selectable_value(shape, prefab, name); + } + }); + + ui.add_space(4.0); + + let RectShape { + rect, + rounding, + fill, + stroke, + stroke_kind, + blur_width, + round_to_pixels, + brush: _, + } = shape; + + let round_to_pixels = round_to_pixels.get_or_insert(true); + + egui::Grid::new("RectShape") + .num_columns(2) + .spacing([12.0, 8.0]) + .striped(true) + .show(ui, |ui| { + ui.label("Size"); + ui.horizontal(|ui| { + let mut size = rect.size(); + ui.add( + egui::DragValue::new(&mut size.x) + .speed(0.2) + .range(0.0..=64.0), + ); + ui.add( + egui::DragValue::new(&mut size.y) + .speed(0.2) + .range(0.0..=64.0), + ); + *rect = Rect::from_center_size(Pos2::ZERO, size); + }); + ui.end_row(); + + ui.label("Rounding"); + ui.add(rounding); + ui.end_row(); + + ui.label("Fill"); + ui.color_edit_button_srgba(fill); + ui.end_row(); + + ui.label("Stroke"); + ui.add(stroke); + ui.end_row(); + + ui.label("Stroke kind"); + ui.horizontal(|ui| { + ui.selectable_value(stroke_kind, StrokeKind::Inside, "Inside"); + ui.selectable_value(stroke_kind, StrokeKind::Middle, "Middle"); + ui.selectable_value(stroke_kind, StrokeKind::Outside, "Outside"); + }); + ui.end_row(); + + ui.label("Blur width"); + ui.add( + egui::DragValue::new(blur_width) + .speed(0.5) + .range(0.0..=20.0), + ); + ui.end_row(); + + ui.label("Round to pixels"); + ui.checkbox(round_to_pixels, ""); + ui.end_row(); + }); +} + +#[cfg(test)] +mod tests { + use crate::View as _; + + use super::*; + + #[test] + fn snapshot_tessellation_test() { + for (name, shape) in TessellationTest::interesting_shapes() { + let mut test = TessellationTest { + shape, + ..Default::default() + }; + let mut harness = egui_kittest::Harness::new_ui(|ui| { + test.ui(ui); + }); + + harness.fit_contents(); + harness.run(); + + harness.snapshot(&format!("tessellation_test/{name}")); + } + } +} diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Bézier Curve.png b/crates/egui_demo_lib/tests/snapshots/demos/Bézier Curve.png index 09dc7549a..16adbcdcf 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Bézier Curve.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Bézier Curve.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d4cfd5191dc7046a782ef2350dc8e0547d2702182badcb15b6b928ce077b76c1 -size 32154 +oid sha256:23f19871720b67659a7b56cee8a78edc941c4bac86f55efc6fa549f10c4712fb +size 31754 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png b/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png index 6ab8e1327..10593d4e0 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4631f841b9e23833505af39eb1c45908013d3b1e1278d477bcedf6a460c71802 -size 27163 +oid sha256:e4d8fee9fd8e69ecd60ebd0dd41c29b61cc13e7013b1d20ad93d40fc4ed1cc03 +size 27091 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png b/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png index 3e05ffd4f..88cd2ffa3 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96e750ebfcc6ec2c130674407388c30cce844977cde19adfebf351fd08698a4f -size 81726 +oid sha256:b291e7efd895ab095590285b841903f05dc7d4dadbab7d9001b04a92953c1694 +size 81677 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Context Menus.png b/crates/egui_demo_lib/tests/snapshots/demos/Context Menus.png index 15123bc3b..2b46eaf44 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Context Menus.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Context Menus.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b03613e597631da3b922ef3d696c4ce74cec41f86a2779fc5b084a316fc9e8e8 -size 11764 +oid sha256:5d75230689807ae7fb692bac5c9b33ee04c02b9e54963e2d6ada05860157daf6 +size 11705 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png b/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png index feb573aa0..ba02ae257 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:76d77e2df39af248d004a023b238bb61ed598ab2bea3e0c6f2885f9537ec1103 -size 25988 +oid sha256:f512159108c7681834ae2a52c5e1d4eb4dbe678a509f3dab3384e35d6c8dbee2 +size 25865 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png b/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png index 34315f422..7b47f16ff 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:003d905893b80ffc132a783155971ad3054229e9d6c046e2760c912559a66b3d -size 20869 +oid sha256:f2a4f5e67ff6615877794304e8be5a5db647d48e20e4248259179cddefbf4088 +size 20806 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Extra Viewport.png b/crates/egui_demo_lib/tests/snapshots/demos/Extra Viewport.png index 05d87fa8b..055f1651f 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Extra Viewport.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Extra Viewport.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be0f96c700b7662aab5098f8412dae3676116eeed65e70f6b295dd3375b329d0 -size 10968 +oid sha256:0bc474a37d6d3a7a08dd41963bf9009c05315de91bb515cc2b19443b79480bff +size 10723 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png b/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png index b5e5c4bec..647e3824f 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:99c94a09c0f6984909481189f9a1415ea90bd7c45e42b4b3763ee36f3d831a65 -size 133231 +oid sha256:0bff769b3f9e46c5e38170885b2c8301294cbcc1c8ee22c17672205edd509924 +size 133170 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Frame.png b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png index 689bb2153..3791d77a6 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Frame.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e4fef5fa8661f207bae2c58381e729cdaf77aecc8b3f178caf262dc310e3a490 -size 24206 +oid sha256:f90f94a842a1d0f1386c3cdd28e60e6ad6efe968f510a11bde418b5bc70a81d2 +size 23897 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png b/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png index b974f7489..3c1cf6de8 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b38828e423195628dcea09b0cbdd66aa4c077334ab7082efd358c7a3297009d -size 17827 +oid sha256:fdbdf1159f0ab4579bf9ceefbd8e40ac7af468368ab11ffec8578214b57d867c +size 17758 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png b/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png index 6853e8564..a3ef616d9 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c53403996451da14f7991ea61bd20b96dbc67eb67dd2041975dd6ce5015a6980 -size 22485 +oid sha256:ce647fd9626f126e7f19b4494bb98a2086071f9c45f7dd6ba4708632e7433a7b +size 22418 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 baf613180..882914ff1 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:63673b070951246b98ca07613fa81496dbfdd10627bac3c9c4356ebff1a36b20 -size 64319 +oid sha256:a843e6772809a1c904968fac19b49973675e726a3b7727ca1b898ad3b9072b0c +size 64257 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Modals.png b/crates/egui_demo_lib/tests/snapshots/demos/Modals.png index 4764035ca..bf3a487df 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Modals.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Modals.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:374b4095a3c7b827b80d6ab01b945458ae0128a2974c2af8aaf6b7e9fef6b459 -size 32554 +oid sha256:b437f27c46ddf82e4268d9bb86d33f43c382d1ab3ed45297bc136600cdc9960c +size 32493 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png b/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png index 7e6254b74..7ef97c87b 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5e756c90069803bb4d2821fff723877bffffd44b26613f5b06c8a042d6901ca4 -size 36578 +oid sha256:2cd0bda8b4d3d7f833273097c8bb52cfd8ea63a405d6798b9aeb7002b143ac74 +size 36459 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Painting.png b/crates/egui_demo_lib/tests/snapshots/demos/Painting.png index 2aee66f72..5e5d369a1 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Painting.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Painting.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6efc59cb9908533baa1a7346b359e9e21c5faf0e373dac6fa7db5476e644233d -size 17678 +oid sha256:a57c3bf373a283b79188080e2ddf7407c2974655fc5ad59222442e14faae055c +size 17508 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Panels.png b/crates/egui_demo_lib/tests/snapshots/demos/Panels.png index fa400718f..3e751d554 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Panels.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Panels.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb631bd2608aec6c83f8815b9db0b28627bf82692fd2b1cb793119083b4f8ad1 -size 264496 +oid sha256:161b5853f528206f2531587753369f2f6f3c52203af668eba0d81a41c2a915e0 +size 264432 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scene.png b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png index 78f697490..d51bbd358 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:9fee66bab841602862c301a7df3add365ea93fde0ac9b71ca7f9b74a709a68d2 -size 35576 +oid sha256:118413a5ec56c6589914c5cb59bda2884a4d6acec3b34bbc367bc893c3de8127 +size 35409 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png b/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png index 023aaa104..8cf0ed424 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0870e9e1c9dc718690124a4d490f1061414f15fa40571e571d9319c3b65e74e -size 23709 +oid sha256:361791f30739f841c46433f5d16d281ba9d0c52027b4cf156c3ac293aa795f46 +size 23592 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png b/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png index 7efa04ccc..16b5868cc 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:532dbcbec94bb9c9fb8cc0665646790a459088e98077118b5fbb2112898e1a43 -size 183854 +oid sha256:81338d7b0412989590bc0808419d93788e972e7ab6f70f481f86bd23263a5395 +size 183821 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png b/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png index b03b3dcc7..38fe97fed 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:3fd584643b68084ec4b65369e08d229e2d389551bbefa59c84ad4b58621593f7 -size 117754 +oid sha256:41c51350c09360738ca284b05c944a11164e4a614beb95ce4cc962222af33d87 +size 117764 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Strip.png b/crates/egui_demo_lib/tests/snapshots/demos/Strip.png index 972368971..3d586971f 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Strip.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Strip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e2b854d99c9b17d15ef92188cdac521d7c0635aef9ba457cd3801884a784009 -size 26159 +oid sha256:794475478cfe2fc954731742165b1f0419a0e64616e72b3766c1e031dbba7ca1 +size 26092 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Table.png b/crates/egui_demo_lib/tests/snapshots/demos/Table.png index 9c5e33304..2658a2535 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:f0b7dc029de8e669d76f3be5e0800e973c354edcf7cefa239faed07c2cd5e0d5 -size 70452 +oid sha256:7073aa30f3e7dfd116d9a4f02f6c5a075ce068d2e6f43eaede3c4dd6c56f925e +size 70439 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png b/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png index b73935d7e..48cad20b9 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:64ba40810a6480e511e8d198b0dfb39e8b220eb2f5592877e27b17ee8d47b9c3 -size 66387 +oid sha256:4bb3cd05f253c0e109b83a0556b975af7a96d57678e57de3b9fa130cc8a8de1c +size 66318 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png b/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png index 07e9177b7..6a5551c45 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0a8151f5bd01b2cb5183c532d11f57bbb7e8cc1e77a3c4b890537d157922c961 -size 21261 +oid sha256:5b550769d5ec5d834f89fba56375d10e5f9b3ba703599d90a5b6607aff1c2b06 +size 21194 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png b/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png index e53122482..ee474166b 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96ce36fcc334793801ab724c711f592faf086a9c98c812684e6b76010e9d1974 -size 59714 +oid sha256:45f343b55be98976de32bd3c5438212a46b787123ea3096cf8fc6bec99493184 +size 59699 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png b/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png index 028346098..5e1cb1b35 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8155d93b78692ced67ddee4011526ef627838ede78351871df5feef8aa512857 -size 13141 +oid sha256:8d25f774320ca844fa4dfba5215ed66f067d0f30c6d7c8ad7ec29d97ee7732e0 +size 13073 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png b/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png index 70cf2378e..0cd7c200a 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf6da022ab97f9d4b862cc8e91bdfd7f9785a3ab0540aa1c2bd2646bd30a3450 -size 35115 +oid sha256:540a05c5b1de7e362bcb48a04611323fbc65c8787b8ae852ada8aa145803753d +size 34968 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_1.png b/crates/egui_demo_lib/tests/snapshots/modals_1.png index 2ce31fe45..1c0bdc0d2 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_1.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:035b35ed053cabd5022d9a97f7d4d5a79d194c9f14594e7602ecd9ef94c24ae5 -size 48053 +oid sha256:2276b8221389da4f644cad43ce446cd7d28f41820cea36fa3ea860808710053d +size 47878 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_2.png b/crates/egui_demo_lib/tests/snapshots/modals_2.png index 5ea3c5b3e..943b4a38a 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_2.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:80a2968e211c83639b60e84b805f1327fb37b87144cada672a403c7e92ace8a8 -size 48066 +oid sha256:054d606681e08bf61762e5c7d7596c4f483e66ac889795e4be4956103328e0bb +size 47862 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_3.png b/crates/egui_demo_lib/tests/snapshots/modals_3.png index 5bb1fe23e..bc6071dbb 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_3.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:86df5dc4b4ddd6f44226242b6d9b5e9f2aacd45193ae9f784fb5084a7a509e0b -size 43987 +oid sha256:f7fb29285e53b619f333757052d05f86d973680ac1ce6d1b25d28b7824bbce51 +size 43725 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png b/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png index 2c868b440..9a52f0498 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1138bbc3b7e73cccd555f0fd58c27a5bda4d84484fdc1bd5223fc9802d0c5328 -size 44089 +oid sha256:be50a396838d4fe29bcfd0807377ad0cda538fea8569acf709da0b13505bf09b +size 43871 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png index 3e3ec6991..b0f087acd 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3562bb87b155b2c406a72a651ffb1991305aa1e15273ce9f32cedc5766318537 -size 554922 +oid sha256:9c6ce16a8a8c34d882485da6bbe08039fb55f90636da8136f68b1bb9baf0effb +size 557610 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png index d5d26d2bb..efa1c8d5b 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ba6c0bd127ab02375f2ae5fbc3eeef33a1bdf074cbb180de2733c700b81df3e5 -size 771069 +oid sha256:a49c052578a46adb41bc02c6d7fdc264ed0ab8ae636cc8a11ac729fe1e48091b +size 791802 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png index 98f73cee9..18b0233d3 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d85ab6d04059009fd2c3ad8001332b27e710c46c9300f2f1f409b882c49211dc -size 918967 +oid sha256:989c55a83b8bc7cce4f459d8b835962377927a98f2bce085e92cba8438438ecd +size 943736 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png index 6de95b07d..b3a865457 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a64f1bdec565357fe4dee3acb46b12eeb0492b522fb3bb9697d39dadce2e8c21 -size 1039455 +oid sha256:01ac61dc5bcecf6bf0d13c8399460b1afae652efe6fecc1d0e4b2f27d9f1c5a4 +size 1046906 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png index 1d373b92b..1b44fad93 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5ca008dca03372bb334564e55fa2d1d25a36751a43df6001a1c1cf3e4db9bcd4 -size 1130930 +oid sha256:3348923ecbad34e385e3ed52ab9b7c88b5d4fc07de00620302d5b191d90a453f +size 1140236 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png index b8b2a7658..f1f12eb1f 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f695127e7fe6cb3b752a0acd71db85d51d2de68e45051a7afe91f4d960acf27 -size 1311641 +oid sha256:179c17b0405c6e87bd3cafaa7272e3e6d6eefd462b4406cca2a7abfe8af6f2bd +size 1317569 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test.png new file mode 100644 index 000000000..7c65aef2c --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa7d25b097911f6b18308bab56d302e3dae9f8f9916f563d5703632a26eda260 +size 72501 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png new file mode 100644 index 000000000..3140fbc94 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c39868f184364555ae90fbfc035aa668f61189be7aeee6bec4e45a8de438ad8e +size 87661 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png new file mode 100644 index 000000000..51b0bd901 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dd029fdc49e6d4078337472c39b9d58bf69073c1b7750c6dd1b7ccd450d52395 +size 119869 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png new file mode 100644 index 000000000..290216b1b --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e698ba12efd129099877248f9630ba983d683e1b495b2523ed3569989341e905 +size 51735 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png new file mode 100644 index 000000000..ff42489e8 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:18b81b5cd88372b65b1ecc62e9a5e894960279310b05a1bd5c8df5bffa244ad0 +size 54922 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png new file mode 100644 index 000000000..8fa9370ff --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bf5df173431d330e4b6045a72227c2bb7613ec98c63f013ea899a3a57cd6617a +size 55522 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png new file mode 100644 index 000000000..882691e82 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:987c162842a08271e833c41a55573d9f30cf045bf7ca3cb03e81d0cc13d5a16e +size 36763 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png new file mode 100644 index 000000000..6741df537 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c5c3055cd190823a4204aa6f23362a88bc5ab5ed5453d9be1b6077dded6cd54 +size 36809 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery.png index 84e731171..87f13e8e5 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:a291f3a5724aefc59ba7881f48752ccc826ca5e480741c221d195061f562ccc9 -size 158220 +oid sha256:946bf96ae558ee7373b50bf11959e82b1f4d91866ec61b04b0336ae170b6f7b2 +size 158553 diff --git a/crates/egui_kittest/src/snapshot.rs b/crates/egui_kittest/src/snapshot.rs index 0b68677b0..dc49caec3 100644 --- a/crates/egui_kittest/src/snapshot.rs +++ b/crates/egui_kittest/src/snapshot.rs @@ -195,7 +195,12 @@ pub fn try_image_snapshot_options( output_path, } = options; - std::fs::create_dir_all(output_path).ok(); + let parent_path = if let Some(parent) = PathBuf::from(name).parent() { + output_path.join(parent) + } else { + output_path.clone() + }; + std::fs::create_dir_all(parent_path).ok(); // The one that is checked in to git let snapshot_path = output_path.join(format!("{name}.png")); diff --git a/crates/egui_kittest/tests/snapshots/combobox_opened.png b/crates/egui_kittest/tests/snapshots/combobox_opened.png index 9020c95c2..ef84c8a77 100644 --- a/crates/egui_kittest/tests/snapshots/combobox_opened.png +++ b/crates/egui_kittest/tests/snapshots/combobox_opened.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:64fd46da67cab2afae0ea8997a88fb43fd207e24cc3943086d978a8de717320f -size 7542 +oid sha256:f65efbf60e190d83d187ec51f3f7811eb55135ef4feb9586e931e8498bc05d64 +size 7430 diff --git a/crates/epaint/src/mesh.rs b/crates/epaint/src/mesh.rs index 495759d04..930cb7716 100644 --- a/crates/epaint/src/mesh.rs +++ b/crates/epaint/src/mesh.rs @@ -98,6 +98,13 @@ impl Mesh { self.indices.is_empty() && self.vertices.is_empty() } + /// Iterate over the triangles of this mesh, returning vertex indices. + pub fn triangles(&self) -> impl Iterator + '_ { + self.indices + .chunks_exact(3) + .map(|chunk| [chunk[0], chunk[1], chunk[2]]) + } + /// Calculate a bounding rectangle. pub fn calc_bounds(&self) -> Rect { let mut bounds = Rect::NOTHING; diff --git a/crates/epaint/src/rounding.rs b/crates/epaint/src/rounding.rs index 12695f387..e0d79b14c 100644 --- a/crates/epaint/src/rounding.rs +++ b/crates/epaint/src/rounding.rs @@ -1,5 +1,9 @@ /// How rounded the corners of things should be. /// +/// This specific the _corner radius_ of the underlying geometric shape (e.g. rectangle). +/// If there is a stroke, then the stroke will have an inner and outer corner radius +/// which will depends on its width and [`crate::StrokeKind`]. +/// /// The rounding uses `u8` to save space, /// so the amount of rounding is limited to integers in the range `[0, 255]`. /// @@ -100,10 +104,23 @@ impl std::ops::Add for Rounding { #[inline] fn add(self, rhs: Self) -> Self { Self { - nw: self.nw + rhs.nw, - ne: self.ne + rhs.ne, - sw: self.sw + rhs.sw, - se: self.se + rhs.se, + nw: self.nw.saturating_add(rhs.nw), + ne: self.ne.saturating_add(rhs.ne), + sw: self.sw.saturating_add(rhs.sw), + se: self.se.saturating_add(rhs.se), + } + } +} + +impl std::ops::Add for Rounding { + type Output = Self; + #[inline] + fn add(self, rhs: u8) -> Self { + Self { + nw: self.nw.saturating_add(rhs), + ne: self.ne.saturating_add(rhs), + sw: self.sw.saturating_add(rhs), + se: self.se.saturating_add(rhs), } } } @@ -112,10 +129,10 @@ impl std::ops::AddAssign for Rounding { #[inline] fn add_assign(&mut self, rhs: Self) { *self = Self { - nw: self.nw + rhs.nw, - ne: self.ne + rhs.ne, - sw: self.sw + rhs.sw, - se: self.se + rhs.se, + nw: self.nw.saturating_add(rhs.nw), + ne: self.ne.saturating_add(rhs.ne), + sw: self.sw.saturating_add(rhs.sw), + se: self.se.saturating_add(rhs.se), }; } } @@ -145,6 +162,19 @@ impl std::ops::Sub for Rounding { } } +impl std::ops::Sub for Rounding { + type Output = Self; + #[inline] + fn sub(self, rhs: u8) -> Self { + Self { + nw: self.nw.saturating_sub(rhs), + ne: self.ne.saturating_sub(rhs), + sw: self.sw.saturating_sub(rhs), + se: self.se.saturating_sub(rhs), + } + } +} + impl std::ops::SubAssign for Rounding { #[inline] fn sub_assign(&mut self, rhs: Self) { diff --git a/crates/epaint/src/shapes/rect_shape.rs b/crates/epaint/src/shapes/rect_shape.rs index f691234b1..cd54dc8e9 100644 --- a/crates/epaint/src/shapes/rect_shape.rs +++ b/crates/epaint/src/shapes/rect_shape.rs @@ -8,7 +8,16 @@ use crate::*; pub struct RectShape { pub rect: Rect, - /// How rounded the corners are. Use `Rounding::ZERO` for no rounding. + /// How rounded the corners of the rectangle are. + /// + /// Use `Rounding::ZERO` for for sharp corners. + /// + /// This is the corner radii of the rectangle. + /// If there is a stroke, then the stroke will have an inner and outer corner radius, + /// and those will depend on [`StrokeKind`] and the stroke width. + /// + /// For [`StrokeKind::Inside`], the outside of the stroke coincides with the rectangle, + /// so the rounding will in this case specify the outer corner radius. pub rounding: Rounding, /// How to fill the rectangle. diff --git a/crates/epaint/src/shapes/shape.rs b/crates/epaint/src/shapes/shape.rs index 2e43fc585..ddbaacfd3 100644 --- a/crates/epaint/src/shapes/shape.rs +++ b/crates/epaint/src/shapes/shape.rs @@ -451,8 +451,9 @@ impl Shape { } Self::Rect(rect_shape) => { rect_shape.rect = transform * rect_shape.rect; - rect_shape.stroke.width *= transform.scaling; rect_shape.rounding *= transform.scaling; + rect_shape.stroke.width *= transform.scaling; + rect_shape.blur_width *= transform.scaling; } Self::Text(text_shape) => { text_shape.pos = transform * text_shape.pos; @@ -472,17 +473,17 @@ impl Shape { Self::Mesh(mesh) => { Arc::make_mut(mesh).transform(transform); } - Self::QuadraticBezier(bezier_shape) => { - bezier_shape.points[0] = transform * bezier_shape.points[0]; - bezier_shape.points[1] = transform * bezier_shape.points[1]; - bezier_shape.points[2] = transform * bezier_shape.points[2]; - bezier_shape.stroke.width *= transform.scaling; - } - Self::CubicBezier(cubic_curve) => { - for p in &mut cubic_curve.points { + Self::QuadraticBezier(bezier) => { + for p in &mut bezier.points { *p = transform * *p; } - cubic_curve.stroke.width *= transform.scaling; + bezier.stroke.width *= transform.scaling; + } + Self::CubicBezier(bezier) => { + for p in &mut bezier.points { + *p = transform * *p; + } + bezier.stroke.width *= transform.scaling; } Self::Callback(shape) => { shape.rect = transform * shape.rect; @@ -502,7 +503,7 @@ fn points_from_line( shapes: &mut Vec, ) { let mut position_on_segment = 0.0; - path.windows(2).for_each(|window| { + for window in path.windows(2) { let (start, end) = (window[0], window[1]); let vector = end - start; let segment_length = vector.length(); @@ -512,7 +513,7 @@ fn points_from_line( position_on_segment += spacing; } position_on_segment -= segment_length; - }); + } } /// Creates dashes from a line. @@ -529,7 +530,7 @@ fn dashes_from_line( let mut drawing_dash = false; let mut step = 0; let steps = dash_lengths.len(); - path.windows(2).for_each(|window| { + for window in path.windows(2) { let (start, end) = (window[0], window[1]); let vector = end - start; let segment_length = vector.length(); @@ -560,5 +561,5 @@ fn dashes_from_line( } position_on_segment -= segment_length; - }); + } } diff --git a/crates/epaint/src/stroke.rs b/crates/epaint/src/stroke.rs index ce7d274dd..5d82c1963 100644 --- a/crates/epaint/src/stroke.rs +++ b/crates/epaint/src/stroke.rs @@ -119,7 +119,13 @@ impl PathStroke { } } + #[inline] + pub fn with_kind(self, kind: StrokeKind) -> Self { + Self { kind, ..self } + } + /// Set the stroke to be painted right on the edge of the shape, half inside and half outside. + #[inline] pub fn middle(self) -> Self { Self { kind: StrokeKind::Middle, @@ -128,6 +134,7 @@ impl PathStroke { } /// Set the stroke to be painted entirely outside of the shape + #[inline] pub fn outside(self) -> Self { Self { kind: StrokeKind::Outside, @@ -136,6 +143,7 @@ impl PathStroke { } /// Set the stroke to be painted entirely inside of the shape + #[inline] pub fn inside(self) -> Self { Self { kind: StrokeKind::Inside, diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 45919759a..b16d22f52 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -10,7 +10,7 @@ use emath::{pos2, remap, vec2, GuiRounding as _, NumExt, Pos2, Rect, Rot2, Vec2} use crate::{ color::ColorMode, emath, stroke::PathStroke, texture_atlas::PreparedDisc, CircleShape, ClippedPrimitive, ClippedShape, Color32, CubicBezierShape, EllipseShape, Mesh, PathShape, - Primitive, QuadraticBezierShape, RectShape, Rounding, Shape, Stroke, StrokeKind, TextShape, + Primitive, QuadraticBezierShape, RectShape, Roundingf, Shape, Stroke, StrokeKind, TextShape, TextureId, Vertex, WHITE_UV, }; @@ -475,6 +475,20 @@ impl Path { } } + /// The path is taken to be closed (i.e. returning to the start again). + /// + /// Calling this may reverse the vertices in the path if they are wrong winding order. + /// The preferred winding order is clockwise. + pub fn fill_and_stroke( + &mut self, + feathering: f32, + fill: Color32, + stroke: &PathStroke, + out: &mut Mesh, + ) { + stroke_and_fill_path(feathering, &mut self.0, PathType::Closed, stroke, fill, out); + } + /// Open-ended. pub fn stroke_open(&mut self, feathering: f32, stroke: &PathStroke, out: &mut Mesh) { stroke_path(feathering, &mut self.0, PathType::Open, stroke, out); @@ -498,12 +512,9 @@ impl Path { /// The path is taken to be closed (i.e. returning to the start again). /// /// Calling this may reverse the vertices in the path if they are wrong winding order. - /// /// The preferred winding order is clockwise. - /// - /// The stroke colors is used for color-correct feathering. - pub fn fill(&mut self, feathering: f32, color: Color32, stroke: &PathStroke, out: &mut Mesh) { - fill_closed_path(feathering, &mut self.0, color, stroke, out); + pub fn fill(&mut self, feathering: f32, color: Color32, out: &mut Mesh) { + fill_closed_path(feathering, &mut self.0, color, out); } /// Like [`Self::fill`] but with texturing. @@ -523,11 +534,11 @@ impl Path { pub mod path { //! Helpers for constructing paths - use crate::Rounding; + use crate::Roundingf; use emath::{pos2, Pos2, Rect}; /// overwrites existing points - pub fn rounded_rectangle(path: &mut Vec, rect: Rect, rounding: Rounding) { + pub fn rounded_rectangle(path: &mut Vec, rect: Rect, rounding: Roundingf) { path.clear(); let min = rect.min; @@ -535,7 +546,7 @@ pub mod path { let r = clamp_rounding(rounding, rect); - if r == Rounding::ZERO { + if r == Roundingf::ZERO { path.reserve(4); path.push(pos2(min.x, min.y)); // left top path.push(pos2(max.x, min.y)); // right top @@ -546,8 +557,6 @@ pub mod path { // Duplicated vertices can happen when one side is all rounding, with no straight edge between. let eps = f32::EPSILON * rect.size().max_elem(); - let r = crate::Roundingf::from(r); - add_circle_quadrant(path, pos2(max.x - r.se, max.y - r.se), r.se, 0.0); // south east if rect.width() <= r.se + r.sw + eps { @@ -624,11 +633,11 @@ pub mod path { } // Ensures the radius of each corner is within a valid range - fn clamp_rounding(rounding: Rounding, rect: Rect) -> Rounding { + fn clamp_rounding(rounding: Roundingf, rect: Rect) -> Roundingf { let half_width = rect.width() * 0.5; let half_height = rect.height() * 0.5; let max_cr = half_width.min(half_height); - rounding.at_most(max_cr.floor() as _).at_least(0) + rounding.at_most(max_cr).at_least(0.0) } } @@ -753,36 +762,17 @@ fn cw_signed_area(path: &[PathPoint]) -> f64 { /// Calling this may reverse the vertices in the path if they are wrong winding order. /// /// The preferred winding order is clockwise. -/// -/// A stroke is required so that the fill's feathering can fade to the right color. You can pass `&PathStroke::NONE` if -/// this path won't be stroked. -fn fill_closed_path( - feathering: f32, - path: &mut [PathPoint], - color: Color32, - stroke: &PathStroke, - out: &mut Mesh, -) { - if color == Color32::TRANSPARENT { +fn fill_closed_path(feathering: f32, path: &mut [PathPoint], fill_color: Color32, out: &mut Mesh) { + if fill_color == Color32::TRANSPARENT { return; } - // TODO(juancampa): This bounding box is computed twice per shape: once here and another when tessellating the - // stroke, consider hoisting that logic to the tessellator/scratchpad. - let bbox = if matches!(stroke.color, ColorMode::UV(_)) { - Rect::from_points(&path.iter().map(|p| p.pos).collect::>()).expand(feathering) - } else { - Rect::NAN - }; - - let stroke_color = &stroke.color; - let get_stroke_color: Box Color32> = match stroke_color { - ColorMode::Solid(col) => Box::new(|_pos: Pos2| *col), - ColorMode::UV(fun) => Box::new(|pos: Pos2| fun(bbox, pos)), - }; - let n = path.len() as u32; - if feathering > 0.0 { + if n < 3 { + return; + } + + if 0.0 < feathering { if cw_signed_area(path) < 0.0 { // Wrong winding order - fix: path.reverse(); @@ -809,10 +799,9 @@ fn fill_closed_path( let pos_inner = p1.pos - dm; let pos_outer = p1.pos + dm; - let color_outer = get_stroke_color(pos_outer); - out.colored_vertex(pos_inner, color); - out.colored_vertex(pos_outer, color_outer); + out.colored_vertex(pos_inner, fill_color); + out.colored_vertex(pos_outer, Color32::TRANSPARENT); out.add_triangle(idx_inner + i1 * 2, idx_inner + i0 * 2, idx_outer + 2 * i0); out.add_triangle(idx_outer + i0 * 2, idx_outer + i1 * 2, idx_inner + 2 * i1); i0 = i1; @@ -823,7 +812,7 @@ fn fill_closed_path( out.vertices.extend(path.iter().map(|p| Vertex { pos: p.pos, uv: WHITE_UV, - color, + color: fill_color, })); for i in 2..n { out.add_triangle(idx, idx + i - 1, idx + i); @@ -856,7 +845,7 @@ fn fill_closed_path_with_uv( } let n = path.len() as u32; - if feathering > 0.0 { + if 0.0 < feathering { if cw_signed_area(path) < 0.0 { // Wrong winding order - fix: path.reverse(); @@ -914,20 +903,6 @@ fn fill_closed_path_with_uv( } } -/// Translate a point along their normals according to the stroke kind. -#[inline(always)] -fn translate_stroke_point(p: &mut PathPoint, stroke: &PathStroke) { - match stroke.kind { - StrokeKind::Inside => { - p.pos -= p.normal * stroke.width * 0.5; - } - StrokeKind::Middle => { /* Nothing to do */ } - StrokeKind::Outside => { - p.pos += p.normal * stroke.width * 0.5; - } - } -} - /// Tessellate the given path as a stroke with thickness. fn stroke_path( feathering: f32, @@ -935,55 +910,122 @@ fn stroke_path( path_type: PathType, stroke: &PathStroke, out: &mut Mesh, +) { + let fill = Color32::TRANSPARENT; + stroke_and_fill_path(feathering, path, path_type, stroke, fill, out); +} + +/// Tessellate the given path as a stroke with thickness, with optional fill color. +/// +/// Calling this may reverse the vertices in the path if they are wrong winding order. +/// +/// The preferred winding order is clockwise. +fn stroke_and_fill_path( + feathering: f32, + path: &mut [PathPoint], + path_type: PathType, + stroke: &PathStroke, + color_fill: Color32, + out: &mut Mesh, ) { let n = path.len() as u32; - if stroke.is_empty() || n < 2 { + if n < 2 { return; } + if stroke.width == 0.0 { + // Skip the stroke, just fill. + return fill_closed_path(feathering, path, color_fill, out); + } + + if color_fill != Color32::TRANSPARENT && cw_signed_area(path) < 0.0 { + // Wrong winding order - fix: + path.reverse(); + for point in &mut *path { + point.normal = -point.normal; + } + } + + if stroke.color == ColorMode::TRANSPARENT { + // Skip the stroke, just fill. But subtract the width from the path: + match stroke.kind { + StrokeKind::Inside => { + for point in &mut *path { + point.pos -= stroke.width * point.normal; + } + } + StrokeKind::Middle => { + for point in &mut *path { + point.pos -= 0.5 * stroke.width * point.normal; + } + } + StrokeKind::Outside => {} + } + + // Skip the stroke, just fill. + return fill_closed_path(feathering, path, color_fill, out); + } + let idx = out.vertices.len() as u32; - // Translate the points along their normals if the stroke is outside or inside - if stroke.kind != StrokeKind::Middle { - path.iter_mut() - .for_each(|p| translate_stroke_point(p, stroke)); + // Move the points so that the stroke is on middle of the path. + match stroke.kind { + StrokeKind::Inside => { + for point in &mut *path { + point.pos -= 0.5 * stroke.width * point.normal; + } + } + StrokeKind::Middle => { + // correct + } + StrokeKind::Outside => { + for point in &mut *path { + point.pos += 0.5 * stroke.width * point.normal; + } + } } // Expand the bounding box to include the thickness of the path - let bbox = if matches!(stroke.color, ColorMode::UV(_)) { + let uv_bbox = if matches!(stroke.color, ColorMode::UV(_)) { Rect::from_points(&path.iter().map(|p| p.pos).collect::>()) .expand((stroke.width / 2.0) + feathering) } else { Rect::NAN }; - let get_color = |col: &ColorMode, pos: Pos2| match col { ColorMode::Solid(col) => *col, - ColorMode::UV(fun) => fun(bbox, pos), + ColorMode::UV(fun) => fun(uv_bbox, pos), }; - if feathering > 0.0 { - let color_inner = &stroke.color; + if 0.0 < feathering { let color_outer = Color32::TRANSPARENT; + let color_middle = &stroke.color; let thin_line = stroke.width <= feathering; if thin_line { - /* - We paint the line using three edges: outer, inner, outer. - - . o i o outer, inner, outer - . |---| feathering (pixel width) - */ - - // Fade out as it gets thinner: - if let ColorMode::Solid(col) = color_inner { - let color_inner = mul_color(*col, stroke.width / feathering); - if color_inner == Color32::TRANSPARENT { - return; + // If the stroke is painted smaller than the pixel width (=feathering width), + // then we risk severe aliasing. + // Instead, we paint the stroke as a triangular ridge, two feather-widths wide, + // and lessen the opacity of the middle part instead of making it thinner. + if color_fill != Color32::TRANSPARENT && stroke.width < feathering { + // If this is filled shape, then we need to also compensate so that the + // filled area remains the same as it would have been without the + // artificially wide line. + for point in &mut *path { + point.pos += 0.5 * (feathering - stroke.width) * point.normal; } } + let opacity = stroke.width / feathering; + + /* + We paint the line using three edges: outer, middle, fill. + + . o m i outer, middle, fill + . |---| feathering (pixel width) + */ + out.reserve_triangles(4 * n as usize); out.reserve_vertices(3 * n as usize); @@ -994,11 +1036,8 @@ fn stroke_path( let p = p1.pos; let n = p1.normal; out.colored_vertex(p + n * feathering, color_outer); - out.colored_vertex( - p, - mul_color(get_color(color_inner, p), stroke.width / feathering), - ); - out.colored_vertex(p - n * feathering, color_outer); + out.colored_vertex(p, mul_color(get_color(color_middle, p), opacity)); + out.colored_vertex(p - n * feathering, color_fill); if connect_with_previous { out.add_triangle(idx + 3 * i0 + 0, idx + 3 * i0 + 1, idx + 3 * i1 + 0); @@ -1007,15 +1046,24 @@ fn stroke_path( out.add_triangle(idx + 3 * i0 + 1, idx + 3 * i0 + 2, idx + 3 * i1 + 1); out.add_triangle(idx + 3 * i0 + 2, idx + 3 * i1 + 1, idx + 3 * i1 + 2); } + i0 = i1; } + + if color_fill != Color32::TRANSPARENT { + out.reserve_triangles(n as usize - 2); + let idx_fill = idx + 2; + for i in 2..n { + out.add_triangle(idx_fill + 3 * (i - 1), idx_fill, idx_fill + 3 * i); + } + } } else { // thick anti-aliased line /* - We paint the line using four edges: outer, inner, inner, outer + We paint the line using four edges: outer, middle, middle, fill - . o i p i o outer, inner, point, inner, outer + . o m p m f outer, middle, point, middle, fill . |---| feathering (pixel width) . |--------------| width . |---------| outer_rad @@ -1038,13 +1086,13 @@ fn stroke_path( out.colored_vertex(p + n * outer_rad, color_outer); out.colored_vertex( p + n * inner_rad, - get_color(color_inner, p + n * inner_rad), + get_color(color_middle, p + n * inner_rad), ); out.colored_vertex( p - n * inner_rad, - get_color(color_inner, p - n * inner_rad), + get_color(color_middle, p - n * inner_rad), ); - out.colored_vertex(p - n * outer_rad, color_outer); + out.colored_vertex(p - n * outer_rad, color_fill); out.add_triangle(idx + 4 * i0 + 0, idx + 4 * i0 + 1, idx + 4 * i1 + 0); out.add_triangle(idx + 4 * i0 + 1, idx + 4 * i1 + 0, idx + 4 * i1 + 1); @@ -1057,6 +1105,14 @@ fn stroke_path( i0 = i1; } + + if color_fill != Color32::TRANSPARENT { + out.reserve_triangles(n as usize - 2); + let idx_fill = idx + 3; + for i in 2..n { + out.add_triangle(idx_fill + 4 * (i - 1), idx_fill, idx_fill + 4 * i); + } + } } PathType::Open => { // Anti-alias the ends by extruding the outer edge and adding @@ -1084,11 +1140,11 @@ fn stroke_path( out.colored_vertex(p + n * outer_rad + back_extrude, color_outer); out.colored_vertex( p + n * inner_rad, - get_color(color_inner, p + n * inner_rad), + get_color(color_middle, p + n * inner_rad), ); out.colored_vertex( p - n * inner_rad, - get_color(color_inner, p - n * inner_rad), + get_color(color_middle, p - n * inner_rad), ); out.colored_vertex(p - n * outer_rad + back_extrude, color_outer); @@ -1104,11 +1160,11 @@ fn stroke_path( out.colored_vertex(p + n * outer_rad, color_outer); out.colored_vertex( p + n * inner_rad, - get_color(color_inner, p + n * inner_rad), + get_color(color_middle, p + n * inner_rad), ); out.colored_vertex( p - n * inner_rad, - get_color(color_inner, p - n * inner_rad), + get_color(color_middle, p - n * inner_rad), ); out.colored_vertex(p - n * outer_rad, color_outer); @@ -1133,11 +1189,11 @@ fn stroke_path( out.colored_vertex(p + n * outer_rad + back_extrude, color_outer); out.colored_vertex( p + n * inner_rad, - get_color(color_inner, p + n * inner_rad), + get_color(color_middle, p + n * inner_rad), ); out.colored_vertex( p - n * inner_rad, - get_color(color_inner, p - n * inner_rad), + get_color(color_middle, p - n * inner_rad), ); out.colored_vertex(p - n * outer_rad + back_extrude, color_outer); @@ -1183,32 +1239,21 @@ fn stroke_path( let thin_line = stroke.width <= feathering; if thin_line { // Fade out thin lines rather than making them thinner + let opacity = stroke.width / feathering; let radius = feathering / 2.0; - if let ColorMode::Solid(color) = stroke.color { - let color = mul_color(color, stroke.width / feathering); - if color == Color32::TRANSPARENT { - return; - } - } - for p in path { + for p in path.iter_mut() { out.colored_vertex( p.pos + radius * p.normal, - mul_color( - get_color(&stroke.color, p.pos + radius * p.normal), - stroke.width / feathering, - ), + mul_color(get_color(&stroke.color, p.pos + radius * p.normal), opacity), ); out.colored_vertex( p.pos - radius * p.normal, - mul_color( - get_color(&stroke.color, p.pos - radius * p.normal), - stroke.width / feathering, - ), + mul_color(get_color(&stroke.color, p.pos - radius * p.normal), opacity), ); } } else { let radius = stroke.width / 2.0; - for p in path { + for p in path.iter_mut() { out.colored_vertex( p.pos + radius * p.normal, get_color(&stroke.color, p.pos + radius * p.normal), @@ -1219,6 +1264,18 @@ fn stroke_path( ); } } + + if color_fill != Color32::TRANSPARENT { + // We Need to create new vertices, because the ones we used for the stroke + // has the wrong color. + + // Shrink to ignore the stroke… + for point in &mut *path { + point.pos -= 0.5 * stroke.width * point.normal; + } + // …then fill: + fill_closed_path(feathering, path, color_fill, out); + } } } @@ -1467,9 +1524,7 @@ impl Tessellator { self.scratchpad_path.clear(); self.scratchpad_path.add_circle(center, radius); self.scratchpad_path - .fill(self.feathering, fill, &path_stroke, out); - self.scratchpad_path - .stroke_closed(self.feathering, &path_stroke, out); + .fill_and_stroke(self.feathering, fill, &path_stroke, out); } /// Tessellate a single [`EllipseShape`] into a [`Mesh`]. @@ -1536,9 +1591,7 @@ impl Tessellator { self.scratchpad_path.clear(); self.scratchpad_path.add_line_loop(&points); self.scratchpad_path - .fill(self.feathering, fill, &path_stroke, out); - self.scratchpad_path - .stroke_closed(self.feathering, &path_stroke, out); + .fill_and_stroke(self.feathering, fill, &path_stroke, out); } /// Tessellate a single [`Mesh`] into a [`Mesh`]. @@ -1642,27 +1695,24 @@ impl Tessellator { } = path_shape; self.scratchpad_path.clear(); + if *closed { self.scratchpad_path.add_line_loop(points); - } else { - self.scratchpad_path.add_open_points(points); - } - if *fill != Color32::TRANSPARENT { - debug_assert!( - closed, + self.scratchpad_path + .fill_and_stroke(self.feathering, *fill, stroke, out); + } else { + debug_assert_eq!( + *fill, + Color32::TRANSPARENT, "You asked to fill a path that is not closed. That makes no sense." ); + + self.scratchpad_path.add_open_points(points); + self.scratchpad_path - .fill(self.feathering, *fill, stroke, out); + .stroke(self.feathering, PathType::Open, stroke, out); } - let typ = if *closed { - PathType::Closed - } else { - PathType::Open - }; - self.scratchpad_path - .stroke(self.feathering, typ, stroke, out); } /// Tessellate a single [`Rect`] into a [`Mesh`]. @@ -1679,18 +1729,69 @@ impl Tessellator { let brush = rect_shape.brush.as_ref(); let RectShape { mut rect, - mut rounding, - fill, + rounding, + mut fill, mut stroke, - stroke_kind, + mut stroke_kind, round_to_pixels, mut blur_width, brush: _, // brush is extracted on its own, because it is not Copy } = *rect_shape; + let mut rounding = Roundingf::from(rounding); let round_to_pixels = round_to_pixels.unwrap_or(self.options.round_rects_to_pixels); + let pixel_size = 1.0 / self.pixels_per_point; - // Important: round to pixels BEFORE applying stroke_kind + if stroke.width == 0.0 { + stroke.color = Color32::TRANSPARENT; + } + + // It is common to (sometimes accidentally) create an infinitely sized rectangle. + // Make sure we can handle that: + rect.min = rect.min.at_least(pos2(-1e7, -1e7)); + rect.max = rect.max.at_most(pos2(1e7, 1e7)); + + if !stroke.is_empty() { + // Check if the stroke covers the whole rectangle + let rect_with_stroke = match stroke_kind { + StrokeKind::Inside => rect, + StrokeKind::Middle => rect.expand(stroke.width / 2.0), + StrokeKind::Outside => rect.expand(stroke.width), + }; + + if rect_with_stroke.size().min_elem() <= 2.0 * stroke.width + 0.5 * self.feathering { + // The stroke covers the fill. + // Change this to be a fill-only shape, using the stroke color as the new fill color. + rect = rect_with_stroke; + + // We blend so that if the stroke is semi-transparent, + // the fill still shines through. + fill = stroke.color; + + stroke = Stroke::NONE; + } + } + + if stroke.is_empty() { + // Approximate thin rectangles with line segments. + // This is important so that thin rectangles look good. + if rect.width() <= 2.0 * self.feathering { + return self.tessellate_line_segment( + [rect.center_top(), rect.center_bottom()], + (rect.width(), fill), + out, + ); + } + if rect.height() <= 2.0 * self.feathering { + return self.tessellate_line_segment( + [rect.left_center(), rect.right_center()], + (rect.height(), fill), + out, + ); + } + } + + // Important: round to pixels BEFORE modifying/applying stroke_kind if round_to_pixels { // The rounding is aware of the stroke kind. // It is designed to be clever in trying to divine the intentions of the user. @@ -1712,7 +1813,9 @@ impl Tessellator { // On this path we optimize for crisp and symmetric strokes. // We put odd-width strokes in the center of pixels. // To understand why, see `fn round_line_segment`. - if stroke.width <= self.feathering + if stroke.width <= 0.0 { + rect = rect.round_to_pixels(self.pixels_per_point); + } else if stroke.width <= pixel_size || is_nearest_integer_odd(self.pixels_per_point * stroke.width) { rect = rect.round_to_pixel_center(self.pixels_per_point); @@ -1733,28 +1836,6 @@ impl Tessellator { } } - // Modify `rect` so that it represents the filled region, with the stroke on the outside. - // Important: do this AFTER rounding to pixels - match stroke_kind { - StrokeKind::Inside => { - // Shrink the stroke so it fits inside the rect: - stroke.width = stroke.width.at_most(rect.size().min_elem() / 2.0); - - rect = rect.shrink(stroke.width); - } - StrokeKind::Middle => { - rect = rect.shrink(stroke.width / 2.0); - } - StrokeKind::Outside => { - // Already good - } - } - - // It is common to (sometimes accidentally) create an infinitely sized rectangle. - // Make sure we can handle that: - rect.min = rect.min.at_least(pos2(-1e7, -1e7)); - rect.max = rect.max.at_most(pos2(1e7, 1e7)); - let old_feathering = self.feathering; if self.feathering < blur_width { @@ -1762,73 +1843,102 @@ impl Tessellator { // Feathering is usually used to make the edges of a shape softer for anti-aliasing. // The tessellator can't handle blurring/feathering larger than the smallest side of the rect. - // Thats because the tessellator approximate very thin rectangles as line segments, - // and these line segments don't have rounded corners. - // When the feathering is small (the size of a pixel), this is usually fine, - // but here we have a huge feathering to simulate blur, - // so we need to avoid this optimization in the tessellator, - // which is also why we add this rather big epsilon: - let eps = 0.1; + let eps = 0.1; // avoid numerical problems blur_width = blur_width - .at_most(rect.size().min_elem() - eps) + .at_most(rect.size().min_elem() - eps - 2.0 * stroke.width) .at_least(0.0); - rounding += Rounding::from(0.5 * blur_width); + rounding += 0.5 * blur_width; self.feathering = self.feathering.max(blur_width); } - if rect.width() < 0.5 * self.feathering { - // Very thin - approximate by a vertical line-segment: - // There is room for improvement here, but it is not critical. - let line = [rect.center_top(), rect.center_bottom()]; - if 0.0 < rect.width() && fill != Color32::TRANSPARENT { - self.tessellate_line_segment(line, Stroke::new(rect.width(), fill), out); - } - if !stroke.is_empty() { - self.tessellate_line_segment(line, stroke, out); // back… - self.tessellate_line_segment(line, stroke, out); // …and forth - } - } else if rect.height() < 0.5 * self.feathering { - // Very thin - approximate by a horizontal line-segment: - // There is room for improvement here, but it is not critical. - let line = [rect.left_center(), rect.right_center()]; - if 0.0 < rect.height() && fill != Color32::TRANSPARENT { - self.tessellate_line_segment(line, Stroke::new(rect.height(), fill), out); - } - if !stroke.is_empty() { - self.tessellate_line_segment(line, stroke, out); // back… - self.tessellate_line_segment(line, stroke, out); // …and forth - } - } else { - let path = &mut self.scratchpad_path; - path.clear(); - path::rounded_rectangle(&mut self.scratchpad_points, rect, rounding); - path.add_line_loop(&self.scratchpad_points); - let path_stroke = PathStroke::from(stroke).outside(); + { + // Modify `rect` so that it represents the OUTER border + // We do this because `path::rounded_rectangle` uses the + // corner radius to pick the fidelity/resolution of the corner. - if rect.is_positive() { - // Fill - if let Some(brush) = brush { - // Textured - let crate::Brush { - fill_texture_id, - uv, - } = **brush; - let uv_from_pos = |p: Pos2| { - pos2( - remap(p.x, rect.x_range(), uv.x_range()), - remap(p.y, rect.y_range(), uv.y_range()), - ) - }; - path.fill_with_uv(self.feathering, fill, fill_texture_id, uv_from_pos, out); - } else { - // Untextured - path.fill(self.feathering, fill, &path_stroke, out); + let original_rounding = rounding; + + match stroke_kind { + StrokeKind::Inside => {} + StrokeKind::Middle => { + rect = rect.expand(stroke.width / 2.0); + rounding += stroke.width / 2.0; + } + StrokeKind::Outside => { + rect = rect.expand(stroke.width); + rounding += stroke.width; } } - path.stroke_closed(self.feathering, &path_stroke, out); + stroke_kind = StrokeKind::Inside; + + // A small rounding is incompatible with a wide stroke, + // because the small bend will be extruded inwards and cross itself. + // There are two ways to solve this (wile maintaining constant stroke width): + // either we increase the rounding, or we set it to zero. + // We choose the former: if the user asks for _any_ rounding, they should get it. + + let min_inside_rounding = 0.1; // Large enough to avoid numerical issues + let min_outside_rounding = stroke.width + min_inside_rounding; + + let extra_rounding_tweak = 0.4; // Otherwise is doesn't _feels_ enough. + + if 0.0 < original_rounding.nw { + rounding.nw += extra_rounding_tweak; + rounding.nw = rounding.nw.at_least(min_outside_rounding); + } + if 0.0 < original_rounding.ne { + rounding.ne += extra_rounding_tweak; + rounding.ne = rounding.ne.at_least(min_outside_rounding); + } + if 0.0 < original_rounding.sw { + rounding.sw += extra_rounding_tweak; + rounding.sw = rounding.sw.at_least(min_outside_rounding); + } + if 0.0 < original_rounding.se { + rounding.se += extra_rounding_tweak; + rounding.se = rounding.se.at_least(min_outside_rounding); + } + } + + let path = &mut self.scratchpad_path; + path.clear(); + path::rounded_rectangle(&mut self.scratchpad_points, rect, rounding); + path.add_line_loop(&self.scratchpad_points); + + let path_stroke = PathStroke::from(stroke).with_kind(stroke_kind); + + if let Some(brush) = brush { + // Textured fill + + let fill_rect = match stroke_kind { + StrokeKind::Inside => rect.shrink(stroke.width), + StrokeKind::Middle => rect.shrink(stroke.width / 2.0), + StrokeKind::Outside => rect, + }; + + if fill_rect.is_positive() { + let crate::Brush { + fill_texture_id, + uv, + } = **brush; + let uv_from_pos = |p: Pos2| { + pos2( + remap(p.x, rect.x_range(), uv.x_range()), + remap(p.y, rect.y_range(), uv.y_range()), + ) + }; + path.fill_with_uv(self.feathering, fill, fill_texture_id, uv_from_pos, out); + } + + if !stroke.is_empty() { + path.stroke_closed(self.feathering, &path_stroke, out); + } + } else { + // Stroke and maybe fill + path.fill_and_stroke(self.feathering, fill, &path_stroke, out); } self.feathering = old_feathering; // restore @@ -2029,24 +2139,21 @@ impl Tessellator { self.scratchpad_path.clear(); if closed { self.scratchpad_path.add_line_loop(points); - } else { - self.scratchpad_path.add_open_points(points); - } - if fill != Color32::TRANSPARENT { - debug_assert!( - closed, - "You asked to fill a path that is not closed. That makes no sense." - ); + self.scratchpad_path - .fill(self.feathering, fill, stroke, out); - } - let typ = if closed { - PathType::Closed + .fill_and_stroke(self.feathering, fill, stroke, out); } else { - PathType::Open - }; - self.scratchpad_path - .stroke(self.feathering, typ, stroke, out); + debug_assert_eq!( + fill, + Color32::TRANSPARENT, + "You asked to fill a bezier path that is not closed. That makes no sense." + ); + + self.scratchpad_path.add_open_points(points); + + self.scratchpad_path + .stroke(self.feathering, PathType::Open, stroke, out); + } } } From 23ed49334e77342da5bf5c7c67b05e78eb510c1b Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 4 Feb 2025 12:53:18 +0100 Subject: [PATCH 054/132] =?UTF-8?q?=E2=9A=A0=EF=B8=8F=20Rename=20`Rounding?= =?UTF-8?q?`=20to=20`CornerRadius`=20(#5673)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Breaking change! * `Rounding` -> `CornerRadius` * `rounding` -> `corner_radius` This is to: * Clarify * Conform to other systems (e.g. Figma) * Avoid confusion with `GuiRounding` --- .../egui/src/containers/collapsing_header.rs | 4 +- crates/egui/src/containers/combo_box.rs | 2 +- crates/egui/src/containers/frame.rs | 36 ++++--- crates/egui/src/containers/scroll_area.rs | 4 +- crates/egui/src/containers/window.rs | 80 ++++++--------- crates/egui/src/lib.rs | 9 +- crates/egui/src/menu.rs | 2 +- crates/egui/src/painter.rs | 16 +-- crates/egui/src/style.rs | 97 ++++++++++--------- crates/egui/src/ui.rs | 2 +- crates/egui/src/widgets/button.rs | 28 +++--- crates/egui/src/widgets/checkbox.rs | 2 +- crates/egui/src/widgets/color_picker.rs | 4 +- crates/egui/src/widgets/image.rs | 46 ++++++--- crates/egui/src/widgets/image_button.rs | 21 +++- crates/egui/src/widgets/progress_bar.rs | 40 ++++---- crates/egui/src/widgets/selected_label.rs | 2 +- crates/egui/src/widgets/slider.rs | 12 +-- crates/egui/src/widgets/text_edit/builder.rs | 6 +- crates/egui_demo_app/src/frame_history.rs | 2 +- crates/egui_demo_app/src/wrap_app.rs | 2 +- crates/egui_demo_lib/src/demo/frame_demo.rs | 4 +- .../src/demo/misc_demo_window.rs | 8 +- .../src/demo/tests/tessellation_test.rs | 6 +- .../tests/snapshots/demos/Frame.png | 4 +- .../tessellation_test/Blurred stroke.png | 4 +- .../snapshots/tessellation_test/Blurred.png | 4 +- .../tessellation_test/Minimal rounding.png | 4 +- .../snapshots/tessellation_test/Normal.png | 4 +- .../Thick stroke, minimal rounding.png | 4 +- .../tessellation_test/Thin filled.png | 4 +- .../tessellation_test/Thin stroked.png | 4 +- crates/egui_extras/src/layout.rs | 6 +- .../src/{rounding.rs => corner_radius.rs} | 36 +++---- .../{roundingf.rs => corner_radius_f32.rs} | 56 +++++------ crates/epaint/src/lib.rs | 11 ++- crates/epaint/src/shadow.rs | 8 +- crates/epaint/src/shape_transform.rs | 2 +- crates/epaint/src/shapes/rect_shape.rs | 16 +-- crates/epaint/src/shapes/shape.rs | 12 +-- crates/epaint/src/tessellator.rs | 82 ++++++++-------- examples/custom_window_frame/src/main.rs | 2 +- examples/images/src/main.rs | 2 +- tests/test_viewports/src/main.rs | 8 +- 44 files changed, 378 insertions(+), 330 deletions(-) rename crates/epaint/src/{rounding.rs => corner_radius.rs} (88%) rename crates/epaint/src/{roundingf.rs => corner_radius_f32.rs} (79%) diff --git a/crates/egui/src/containers/collapsing_header.rs b/crates/egui/src/containers/collapsing_header.rs index 4ac57d36d..1ed2a27fb 100644 --- a/crates/egui/src/containers/collapsing_header.rs +++ b/crates/egui/src/containers/collapsing_header.rs @@ -573,7 +573,7 @@ impl CollapsingHeader { if ui.visuals().collapsing_header_frame || show_background { ui.painter().add(epaint::RectShape::new( header_response.rect.expand(visuals.expansion), - visuals.rounding, + visuals.corner_radius, visuals.weak_bg_fill, visuals.bg_stroke, StrokeKind::Inside, @@ -586,7 +586,7 @@ impl CollapsingHeader { ui.painter().rect( rect, - visuals.rounding, + visuals.corner_radius, visuals.bg_fill, visuals.bg_stroke, StrokeKind::Inside, diff --git a/crates/egui/src/containers/combo_box.rs b/crates/egui/src/containers/combo_box.rs index d6a71c2e4..98cf0182e 100644 --- a/crates/egui/src/containers/combo_box.rs +++ b/crates/egui/src/containers/combo_box.rs @@ -471,7 +471,7 @@ fn button_frame( where_to_put_background, epaint::RectShape::new( outer_rect.expand(visuals.expansion), - visuals.rounding, + visuals.corner_radius, visuals.weak_bg_fill, visuals.bg_stroke, epaint::StrokeKind::Inside, diff --git a/crates/egui/src/containers/frame.rs b/crates/egui/src/containers/frame.rs index 07ab8e28a..343897dcc 100644 --- a/crates/egui/src/containers/frame.rs +++ b/crates/egui/src/containers/frame.rs @@ -4,7 +4,7 @@ use crate::{ epaint, layers::ShapeIdx, InnerResponse, Response, Sense, Style, Ui, UiBuilder, UiKind, UiStackInfo, }; -use epaint::{Color32, Margin, Marginf, Rect, Rounding, Shadow, Shape, Stroke}; +use epaint::{Color32, CornerRadius, Margin, Marginf, Rect, Shadow, Shape, Stroke}; /// A frame around some content, including margin, colors, etc. /// @@ -119,7 +119,7 @@ pub struct Frame { /// (or, if there is no stroke, the outer corner of [`Self::fill`]). /// /// In other words, this is the corner radius of the _widget rect_. - pub rounding: Rounding, + pub corner_radius: CornerRadius, /// Margin outside the painted frame. /// @@ -161,7 +161,7 @@ impl Frame { inner_margin: Margin::ZERO, stroke: Stroke::NONE, fill: Color32::TRANSPARENT, - rounding: Rounding::ZERO, + corner_radius: CornerRadius::ZERO, outer_margin: Margin::ZERO, shadow: Shadow::NONE, }; @@ -182,7 +182,7 @@ impl Frame { pub fn group(style: &Style) -> Self { Self::new() .inner_margin(6) - .rounding(style.visuals.widgets.noninteractive.rounding) + .corner_radius(style.visuals.widgets.noninteractive.corner_radius) .stroke(style.visuals.widgets.noninteractive.bg_stroke) } @@ -199,7 +199,7 @@ impl Frame { pub fn window(style: &Style) -> Self { Self::new() .inner_margin(style.spacing.window_margin) - .rounding(style.visuals.window_rounding) + .corner_radius(style.visuals.window_corner_radius) .shadow(style.visuals.window_shadow) .fill(style.visuals.window_fill()) .stroke(style.visuals.window_stroke()) @@ -208,7 +208,7 @@ impl Frame { pub fn menu(style: &Style) -> Self { Self::new() .inner_margin(style.spacing.menu_margin) - .rounding(style.visuals.menu_rounding) + .corner_radius(style.visuals.menu_corner_radius) .shadow(style.visuals.popup_shadow) .fill(style.visuals.window_fill()) .stroke(style.visuals.window_stroke()) @@ -217,7 +217,7 @@ impl Frame { pub fn popup(style: &Style) -> Self { Self::new() .inner_margin(style.spacing.menu_margin) - .rounding(style.visuals.menu_rounding) + .corner_radius(style.visuals.menu_corner_radius) .shadow(style.visuals.popup_shadow) .fill(style.visuals.window_fill()) .stroke(style.visuals.window_stroke()) @@ -230,7 +230,7 @@ impl Frame { pub fn canvas(style: &Style) -> Self { Self::new() .inner_margin(2) - .rounding(style.visuals.widgets.noninteractive.rounding) + .corner_radius(style.visuals.widgets.noninteractive.corner_radius) .fill(style.visuals.extreme_bg_color) .stroke(style.visuals.window_stroke()) } @@ -277,11 +277,21 @@ impl Frame { /// /// In other words, this is the corner radius of the _widget rect_. #[inline] - pub fn rounding(mut self, rounding: impl Into) -> Self { - self.rounding = rounding.into(); + pub fn corner_radius(mut self, corner_radius: impl Into) -> Self { + self.corner_radius = corner_radius.into(); self } + /// The rounding of the _outer_ corner of the [`Self::stroke`] + /// (or, if there is no stroke, the outer corner of [`Self::fill`]). + /// + /// In other words, this is the corner radius of the _widget rect_. + #[inline] + #[deprecated = "Renamed to `corner_radius`"] + pub fn rounding(self, corner_radius: impl Into) -> Self { + self.corner_radius(corner_radius) + } + /// Margin outside the painted frame. /// /// Similar to what is called `margin` in CSS. @@ -424,7 +434,7 @@ impl Frame { inner_margin: _, fill, stroke, - rounding, + corner_radius, outer_margin: _, shadow, } = *self; @@ -433,7 +443,7 @@ impl Frame { let frame_shape = Shape::Rect(epaint::RectShape::new( widget_rect, - rounding, + corner_radius, fill, stroke, epaint::StrokeKind::Inside, @@ -442,7 +452,7 @@ impl Frame { if shadow == Default::default() { frame_shape } else { - let shadow = shadow.as_shape(widget_rect, rounding); + let shadow = shadow.as_shape(widget_rect, corner_radius); Shape::Vec(vec![Shape::from(shadow), frame_shape]) } } diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index 3f6804212..727a9da7b 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -1240,7 +1240,7 @@ impl Prepared { // Background: ui.painter().add(epaint::Shape::rect_filled( outer_scroll_bar_rect, - visuals.rounding, + visuals.corner_radius, ui.visuals() .extreme_bg_color .gamma_multiply(background_opacity), @@ -1249,7 +1249,7 @@ impl Prepared { // Handle: ui.painter().add(epaint::Shape::rect_filled( handle_rect, - visuals.rounding, + visuals.corner_radius, handle_color.gamma_multiply(handle_opacity), )); } diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index 75f4ac8e0..7582bd701 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use emath::GuiRounding as _; -use epaint::{RectShape, Roundingf}; +use epaint::{CornerRadiusF32, RectShape}; use crate::collapsing_header::CollapsingState; use crate::*; @@ -485,8 +485,8 @@ impl Window<'_> { .at_least(style.spacing.interact_size.y); let title_bar_inner_height = title_bar_inner_height + window_frame.inner_margin.sum().y; let half_height = (title_bar_inner_height / 2.0).round() as _; - window_frame.rounding.ne = window_frame.rounding.ne.clamp(0, half_height); - window_frame.rounding.nw = window_frame.rounding.nw.clamp(0, half_height); + window_frame.corner_radius.ne = window_frame.corner_radius.ne.clamp(0, half_height); + window_frame.corner_radius.nw = window_frame.corner_radius.nw.clamp(0, half_height); let title_content_spacing = if is_collapsed { 0.0 @@ -612,7 +612,7 @@ impl Window<'_> { if on_top && area_content_ui.visuals().window_highlight_topmost { let mut round = - window_frame.rounding - window_frame.stroke.width.round() as u8; + window_frame.corner_radius - window_frame.stroke.width.round() as u8; if !is_collapsed { round.se = 0; @@ -667,28 +667,28 @@ fn paint_resize_corner( window_frame: &Frame, i: ResizeInteraction, ) { - let rounding = window_frame.rounding; + let cr = window_frame.corner_radius; let (corner, radius, corner_response) = if possible.resize_right && possible.resize_bottom { - (Align2::RIGHT_BOTTOM, rounding.se, i.right & i.bottom) + (Align2::RIGHT_BOTTOM, cr.se, i.right & i.bottom) } else if possible.resize_left && possible.resize_bottom { - (Align2::LEFT_BOTTOM, rounding.sw, i.left & i.bottom) + (Align2::LEFT_BOTTOM, cr.sw, i.left & i.bottom) } else if possible.resize_left && possible.resize_top { - (Align2::LEFT_TOP, rounding.nw, i.left & i.top) + (Align2::LEFT_TOP, cr.nw, i.left & i.top) } else if possible.resize_right && possible.resize_top { - (Align2::RIGHT_TOP, rounding.ne, i.right & i.top) + (Align2::RIGHT_TOP, cr.ne, i.right & i.top) } else { // We're not in two directions, but it is still nice to tell the user // we're resizable by painting the resize corner in the expected place // (i.e. for windows only resizable in one direction): if possible.resize_right || possible.resize_bottom { - (Align2::RIGHT_BOTTOM, rounding.se, i.right & i.bottom) + (Align2::RIGHT_BOTTOM, cr.se, i.right & i.bottom) } else if possible.resize_left || possible.resize_bottom { - (Align2::LEFT_BOTTOM, rounding.sw, i.left & i.bottom) + (Align2::LEFT_BOTTOM, cr.sw, i.left & i.bottom) } else if possible.resize_left || possible.resize_top { - (Align2::LEFT_TOP, rounding.nw, i.left & i.top) + (Align2::LEFT_TOP, cr.nw, i.left & i.top) } else if possible.resize_right || possible.resize_top { - (Align2::RIGHT_TOP, rounding.ne, i.right & i.top) + (Align2::RIGHT_TOP, cr.ne, i.right & i.top) } else { return; } @@ -1054,7 +1054,7 @@ fn paint_frame_interaction(ui: &Ui, rect: Rect, interaction: ResizeInteraction) bottom = interaction.bottom.hover; } - let rounding = Roundingf::from(ui.visuals().window_rounding); + let cr = CornerRadiusF32::from(ui.visuals().window_corner_radius); // Put the rect in the center of the fixed window stroke: let rect = rect.shrink(interaction.window_frame.stroke.width / 2.0); @@ -1072,56 +1072,36 @@ fn paint_frame_interaction(ui: &Ui, rect: Rect, interaction: ResizeInteraction) let mut points = Vec::new(); if right && !bottom && !top { - points.push(pos2(max.x, min.y + rounding.ne)); - points.push(pos2(max.x, max.y - rounding.se)); + points.push(pos2(max.x, min.y + cr.ne)); + points.push(pos2(max.x, max.y - cr.se)); } if right && bottom { - points.push(pos2(max.x, min.y + rounding.ne)); - points.push(pos2(max.x, max.y - rounding.se)); - add_circle_quadrant( - &mut points, - pos2(max.x - rounding.se, max.y - rounding.se), - rounding.se, - 0.0, - ); + points.push(pos2(max.x, min.y + cr.ne)); + points.push(pos2(max.x, max.y - cr.se)); + add_circle_quadrant(&mut points, pos2(max.x - cr.se, max.y - cr.se), cr.se, 0.0); } if bottom { - points.push(pos2(max.x - rounding.se, max.y)); - points.push(pos2(min.x + rounding.sw, max.y)); + points.push(pos2(max.x - cr.se, max.y)); + points.push(pos2(min.x + cr.sw, max.y)); } if left && bottom { - add_circle_quadrant( - &mut points, - pos2(min.x + rounding.sw, max.y - rounding.sw), - rounding.sw, - 1.0, - ); + add_circle_quadrant(&mut points, pos2(min.x + cr.sw, max.y - cr.sw), cr.sw, 1.0); } if left { - points.push(pos2(min.x, max.y - rounding.sw)); - points.push(pos2(min.x, min.y + rounding.nw)); + points.push(pos2(min.x, max.y - cr.sw)); + points.push(pos2(min.x, min.y + cr.nw)); } if left && top { - add_circle_quadrant( - &mut points, - pos2(min.x + rounding.nw, min.y + rounding.nw), - rounding.nw, - 2.0, - ); + add_circle_quadrant(&mut points, pos2(min.x + cr.nw, min.y + cr.nw), cr.nw, 2.0); } if top { - points.push(pos2(min.x + rounding.nw, min.y)); - points.push(pos2(max.x - rounding.ne, min.y)); + points.push(pos2(min.x + cr.nw, min.y)); + points.push(pos2(max.x - cr.ne, min.y)); } if right && top { - add_circle_quadrant( - &mut points, - pos2(max.x - rounding.ne, min.y + rounding.ne), - rounding.ne, - 3.0, - ); - points.push(pos2(max.x, min.y + rounding.ne)); - points.push(pos2(max.x, max.y - rounding.se)); + add_circle_quadrant(&mut points, pos2(max.x - cr.ne, min.y + cr.ne), cr.ne, 3.0); + points.push(pos2(max.x, min.y + cr.ne)); + points.push(pos2(max.x, max.y - cr.se)); } ui.painter().add(Shape::line(points, stroke)); diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 15f4faf3c..759f7e41a 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -464,8 +464,8 @@ pub use epaint::{ mutex, text::{FontData, FontDefinitions, FontFamily, FontId, FontTweak}, textures::{TextureFilter, TextureOptions, TextureWrapMode, TexturesDelta}, - ClippedPrimitive, ColorImage, FontImage, ImageData, Margin, Mesh, PaintCallback, - PaintCallbackInfo, Rounding, Shadow, Shape, Stroke, StrokeKind, TextureHandle, TextureId, + ClippedPrimitive, ColorImage, CornerRadius, FontImage, ImageData, Margin, Mesh, PaintCallback, + PaintCallbackInfo, Shadow, Shape, Stroke, StrokeKind, TextureHandle, TextureId, }; pub mod text { @@ -510,6 +510,9 @@ pub use self::{ widgets::*, }; +#[deprecated = "Renamed to CornerRadius"] +pub type Rounding = CornerRadius; + // ---------------------------------------------------------------------------- /// Helper function that adds a label when compiling with debug assertions enabled. @@ -538,7 +541,7 @@ pub fn warn_if_debug_build(ui: &mut crate::Ui) { /// ui.add( /// egui::Image::new(egui::include_image!("../assets/ferris.png")) /// .max_width(200.0) -/// .rounding(10.0), +/// .corner_radius(10), /// ); /// /// let image_source: egui::ImageSource = egui::include_image!("../assets/ferris.png"); diff --git a/crates/egui/src/menu.rs b/crates/egui/src/menu.rs index 301c69dd3..96aeeac61 100644 --- a/crates/egui/src/menu.rs +++ b/crates/egui/src/menu.rs @@ -580,7 +580,7 @@ impl SubMenuButton { if ui.visuals().button_frame { ui.painter().rect_filled( rect.expand(visuals.expansion), - visuals.rounding, + visuals.corner_radius, visuals.weak_bg_fill, ); } diff --git a/crates/egui/src/painter.rs b/crates/egui/src/painter.rs index 4ed74cddb..aef97cccf 100644 --- a/crates/egui/src/painter.rs +++ b/crates/egui/src/painter.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use emath::GuiRounding as _; use epaint::{ text::{Fonts, Galley, LayoutJob}, - CircleShape, ClippedShape, PathStroke, RectShape, Rounding, Shape, Stroke, StrokeKind, + CircleShape, ClippedShape, CornerRadius, PathStroke, RectShape, Shape, Stroke, StrokeKind, }; use crate::{ @@ -412,14 +412,14 @@ impl Painter { pub fn rect( &self, rect: Rect, - rounding: impl Into, + corner_radius: impl Into, fill_color: impl Into, stroke: impl Into, stroke_kind: StrokeKind, ) -> ShapeIdx { self.add(RectShape::new( rect, - rounding, + corner_radius, fill_color, stroke, stroke_kind, @@ -429,21 +429,21 @@ impl Painter { pub fn rect_filled( &self, rect: Rect, - rounding: impl Into, + corner_radius: impl Into, fill_color: impl Into, ) -> ShapeIdx { - self.add(RectShape::filled(rect, rounding, fill_color)) + self.add(RectShape::filled(rect, corner_radius, fill_color)) } /// The stroke extends _outside_ the [`Rect`]. pub fn rect_stroke( &self, rect: Rect, - rounding: impl Into, + corner_radius: impl Into, stroke: impl Into, stroke_kind: StrokeKind, ) -> ShapeIdx { - self.add(RectShape::stroke(rect, rounding, stroke, stroke_kind)) + self.add(RectShape::stroke(rect, corner_radius, stroke, stroke_kind)) } /// Show an arrow starting at `origin` and going in the direction of `vec`, with the length `vec.length()`. @@ -472,7 +472,7 @@ impl Painter { /// # egui::__run_test_ui(|ui| { /// # let rect = egui::Rect::from_min_size(Default::default(), egui::Vec2::splat(100.0)); /// egui::Image::new(egui::include_image!("../assets/ferris.png")) - /// .rounding(5.0) + /// .corner_radius(5) /// .tint(egui::Color32::LIGHT_BLUE) /// .paint_at(ui, rect); /// # }); diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 1d5a1b25c..c84f2dc8a 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -5,7 +5,7 @@ use std::{collections::BTreeMap, ops::RangeInclusive, sync::Arc}; use emath::Align; -use epaint::{text::FontTweak, Rounding, Shadow, Stroke}; +use epaint::{text::FontTweak, CornerRadius, Shadow, Stroke}; use crate::{ ecolor::Color32, @@ -915,7 +915,7 @@ pub struct Visuals { /// A good color for error text (e.g. red). pub error_fg_color: Color32, - pub window_rounding: Rounding, + pub window_corner_radius: CornerRadius, pub window_shadow: Shadow, pub window_fill: Color32, pub window_stroke: Stroke, @@ -923,7 +923,7 @@ pub struct Visuals { /// Highlight the topmost window. pub window_highlight_topmost: bool, - pub menu_rounding: Rounding, + pub menu_corner_radius: CornerRadius, /// Panel background color pub panel_fill: Color32, @@ -1107,7 +1107,7 @@ pub struct WidgetVisuals { pub bg_stroke: Stroke, /// Button frames etc. - pub rounding: Rounding, + pub corner_radius: CornerRadius, /// Stroke and text color of the interactive part of a component (button text, slider grab, check-mark, …). pub fg_stroke: Stroke, @@ -1121,6 +1121,11 @@ impl WidgetVisuals { pub fn text_color(&self) -> Color32 { self.fg_stroke.color } + + #[deprecated = "Renamed to corner_radius"] + pub fn rounding(&self) -> CornerRadius { + self.corner_radius + } } /// Options for help debug egui by adding extra visualization @@ -1291,7 +1296,7 @@ impl Visuals { warn_fg_color: Color32::from_rgb(255, 143, 0), // orange error_fg_color: Color32::from_rgb(255, 0, 0), // red - window_rounding: Rounding::same(6), + window_corner_radius: CornerRadius::same(6), window_shadow: Shadow { offset: [10, 20], blur: 15, @@ -1302,7 +1307,7 @@ impl Visuals { window_stroke: Stroke::new(1.0, Color32::from_gray(60)), window_highlight_topmost: true, - menu_rounding: Rounding::same(6), + menu_corner_radius: CornerRadius::same(6), panel_fill: Color32::from_gray(27), @@ -1412,7 +1417,7 @@ impl Widgets { bg_fill: Color32::from_gray(27), bg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // separators, indentation lines fg_stroke: Stroke::new(1.0, Color32::from_gray(140)), // normal text color - rounding: Rounding::same(2), + corner_radius: CornerRadius::same(2), expansion: 0.0, }, inactive: WidgetVisuals { @@ -1420,7 +1425,7 @@ impl Widgets { bg_fill: Color32::from_gray(60), // checkbox background bg_stroke: Default::default(), fg_stroke: Stroke::new(1.0, Color32::from_gray(180)), // button text - rounding: Rounding::same(2), + corner_radius: CornerRadius::same(2), expansion: 0.0, }, hovered: WidgetVisuals { @@ -1428,7 +1433,7 @@ impl Widgets { bg_fill: Color32::from_gray(70), bg_stroke: Stroke::new(1.0, Color32::from_gray(150)), // e.g. hover over window edge or button fg_stroke: Stroke::new(1.5, Color32::from_gray(240)), - rounding: Rounding::same(3), + corner_radius: CornerRadius::same(3), expansion: 1.0, }, active: WidgetVisuals { @@ -1436,7 +1441,7 @@ impl Widgets { bg_fill: Color32::from_gray(55), bg_stroke: Stroke::new(1.0, Color32::WHITE), fg_stroke: Stroke::new(2.0, Color32::WHITE), - rounding: Rounding::same(2), + corner_radius: CornerRadius::same(2), expansion: 1.0, }, open: WidgetVisuals { @@ -1444,7 +1449,7 @@ impl Widgets { bg_fill: Color32::from_gray(27), bg_stroke: Stroke::new(1.0, Color32::from_gray(60)), fg_stroke: Stroke::new(1.0, Color32::from_gray(210)), - rounding: Rounding::same(2), + corner_radius: CornerRadius::same(2), expansion: 0.0, }, } @@ -1457,7 +1462,7 @@ impl Widgets { bg_fill: Color32::from_gray(248), bg_stroke: Stroke::new(1.0, Color32::from_gray(190)), // separators, indentation lines fg_stroke: Stroke::new(1.0, Color32::from_gray(80)), // normal text color - rounding: Rounding::same(2), + corner_radius: CornerRadius::same(2), expansion: 0.0, }, inactive: WidgetVisuals { @@ -1465,7 +1470,7 @@ impl Widgets { bg_fill: Color32::from_gray(230), // checkbox background bg_stroke: Default::default(), fg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // button text - rounding: Rounding::same(2), + corner_radius: CornerRadius::same(2), expansion: 0.0, }, hovered: WidgetVisuals { @@ -1473,7 +1478,7 @@ impl Widgets { bg_fill: Color32::from_gray(220), bg_stroke: Stroke::new(1.0, Color32::from_gray(105)), // e.g. hover over window edge or button fg_stroke: Stroke::new(1.5, Color32::BLACK), - rounding: Rounding::same(3), + corner_radius: CornerRadius::same(3), expansion: 1.0, }, active: WidgetVisuals { @@ -1481,7 +1486,7 @@ impl Widgets { bg_fill: Color32::from_gray(165), bg_stroke: Stroke::new(1.0, Color32::BLACK), fg_stroke: Stroke::new(2.0, Color32::BLACK), - rounding: Rounding::same(2), + corner_radius: CornerRadius::same(2), expansion: 1.0, }, open: WidgetVisuals { @@ -1489,7 +1494,7 @@ impl Widgets { bg_fill: Color32::from_gray(220), bg_stroke: Stroke::new(1.0, Color32::from_gray(160)), fg_stroke: Stroke::new(1.0, Color32::BLACK), - rounding: Rounding::same(2), + corner_radius: CornerRadius::same(2), expansion: 0.0, }, } @@ -1924,7 +1929,7 @@ impl WidgetVisuals { weak_bg_fill, bg_fill: mandatory_bg_fill, bg_stroke, - rounding, + corner_radius, fg_stroke, expansion, } = self; @@ -1948,8 +1953,8 @@ impl WidgetVisuals { ui.add(bg_stroke); ui.end_row(); - ui.label("Rounding"); - ui.add(rounding); + ui.label("Corner radius"); + ui.add(corner_radius); ui.end_row(); ui.label("Foreground stroke (text)"); @@ -1978,13 +1983,13 @@ impl Visuals { warn_fg_color, error_fg_color, - window_rounding, + window_corner_radius, window_shadow, window_fill, window_stroke, window_highlight_topmost, - menu_rounding, + menu_corner_radius, panel_fill, @@ -2066,8 +2071,8 @@ impl Visuals { ui.add(window_stroke); ui.end_row(); - ui.label("Rounding"); - ui.add(window_rounding); + ui.label("Corner radius"); + ui.add(window_corner_radius); ui.end_row(); ui.label("Shadow"); @@ -2084,8 +2089,8 @@ impl Visuals { .spacing([12.0, 8.0]) .striped(true) .show(ui, |ui| { - ui.label("Rounding"); - ui.add(menu_rounding); + ui.label("Corner radius"); + ui.add(menu_corner_radius); ui.end_row(); ui.label("Shadow"); @@ -2388,7 +2393,7 @@ impl Widget for &mut Margin { } } -impl Widget for &mut Rounding { +impl Widget for &mut CornerRadius { fn ui(self, ui: &mut Ui) -> Response { let mut same = self.is_same(); @@ -2398,37 +2403,39 @@ impl Widget for &mut Rounding { let mut cr = self.nw; ui.add(DragValue::new(&mut cr).range(0.0..=f32::INFINITY)); - *self = Rounding::same(cr); + *self = CornerRadius::same(cr); }) .response } else { ui.vertical(|ui| { ui.checkbox(&mut same, "same"); - crate::Grid::new("rounding").num_columns(2).show(ui, |ui| { - ui.label("NW"); - ui.add(DragValue::new(&mut self.nw).range(0.0..=f32::INFINITY)); - ui.end_row(); + crate::Grid::new("Corner radius") + .num_columns(2) + .show(ui, |ui| { + ui.label("NW"); + ui.add(DragValue::new(&mut self.nw).range(0.0..=f32::INFINITY)); + ui.end_row(); - ui.label("NE"); - ui.add(DragValue::new(&mut self.ne).range(0.0..=f32::INFINITY)); - ui.end_row(); + ui.label("NE"); + ui.add(DragValue::new(&mut self.ne).range(0.0..=f32::INFINITY)); + ui.end_row(); - ui.label("SW"); - ui.add(DragValue::new(&mut self.sw).range(0.0..=f32::INFINITY)); - ui.end_row(); + ui.label("SW"); + ui.add(DragValue::new(&mut self.sw).range(0.0..=f32::INFINITY)); + ui.end_row(); - ui.label("SE"); - ui.add(DragValue::new(&mut self.se).range(0.0..=f32::INFINITY)); - ui.end_row(); - }); + ui.label("SE"); + ui.add(DragValue::new(&mut self.se).range(0.0..=f32::INFINITY)); + ui.end_row(); + }); }) .response }; // Apply the checkbox: if same { - *self = Rounding::from(self.average()); + *self = CornerRadius::from(self.average()); } else { // Make sure we aren't same: if self.is_same() { @@ -2513,7 +2520,7 @@ impl Widget for &mut crate::Frame { let crate::Frame { inner_margin, outer_margin, - rounding, + corner_radius, shadow, fill, stroke, @@ -2533,8 +2540,8 @@ impl Widget for &mut crate::Frame { ui.push_id("outer", |ui| ui.add(outer_margin)); ui.end_row(); - ui.label("Rounding"); - ui.add(rounding); + ui.label("Corner radius"); + ui.add(corner_radius); ui.end_row(); ui.label("Shadow"); diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 1fdfccdc6..8422c3ce8 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -2127,7 +2127,7 @@ impl Ui { /// ui.add( /// egui::Image::new(egui::include_image!("../assets/ferris.png")) /// .max_width(200.0) - /// .rounding(10.0), + /// .corner_radius(10), /// ); /// # }); /// ``` diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 9e44e940e..578aedcd6 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -1,5 +1,5 @@ use crate::{ - widgets, Align, Color32, Image, NumExt, Rect, Response, Rounding, Sense, Stroke, TextStyle, + widgets, Align, Color32, CornerRadius, Image, NumExt, Rect, Response, Sense, Stroke, TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetType, }; @@ -35,7 +35,7 @@ pub struct Button<'a> { small: bool, frame: Option, min_size: Vec2, - rounding: Option, + corner_radius: Option, selected: bool, image_tint_follows_text_color: bool, } @@ -69,7 +69,7 @@ impl<'a> Button<'a> { small: false, frame: None, min_size: Vec2::ZERO, - rounding: None, + corner_radius: None, selected: false, image_tint_follows_text_color: false, } @@ -153,11 +153,17 @@ impl<'a> Button<'a> { /// Set the rounding of the button. #[inline] - pub fn rounding(mut self, rounding: impl Into) -> Self { - self.rounding = Some(rounding.into()); + pub fn corner_radius(mut self, corner_radius: impl Into) -> Self { + self.corner_radius = Some(corner_radius.into()); self } + #[inline] + #[deprecated = "Renamed to `corner_radius`"] + pub fn rounding(self, corner_radius: impl Into) -> Self { + self.corner_radius(corner_radius) + } + /// If true, the tint of the image is multiplied by the widget text color. /// /// This makes sense for images that are white, that should have the same color as the text color. @@ -202,7 +208,7 @@ impl Widget for Button<'_> { small, frame, min_size, - rounding, + corner_radius, selected, image_tint_follows_text_color, } = self; @@ -292,11 +298,11 @@ impl Widget for Button<'_> { if ui.is_rect_visible(rect) { let visuals = ui.style().interact(&response); - let (frame_expansion, frame_rounding, frame_fill, frame_stroke) = if selected { + let (frame_expansion, frame_cr, frame_fill, frame_stroke) = if selected { let selection = ui.visuals().selection; ( Vec2::ZERO, - Rounding::ZERO, + CornerRadius::ZERO, selection.bg_fill, selection.stroke, ) @@ -304,19 +310,19 @@ impl Widget for Button<'_> { let expansion = Vec2::splat(visuals.expansion); ( expansion, - visuals.rounding, + visuals.corner_radius, visuals.weak_bg_fill, visuals.bg_stroke, ) } else { Default::default() }; - let frame_rounding = rounding.unwrap_or(frame_rounding); + 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_rounding, + frame_cr, frame_fill, frame_stroke, epaint::StrokeKind::Inside, diff --git a/crates/egui/src/widgets/checkbox.rs b/crates/egui/src/widgets/checkbox.rs index f54478984..7bdb6c86f 100644 --- a/crates/egui/src/widgets/checkbox.rs +++ b/crates/egui/src/widgets/checkbox.rs @@ -104,7 +104,7 @@ impl Widget for Checkbox<'_> { 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.rounding, + visuals.corner_radius, visuals.bg_fill, visuals.bg_stroke, epaint::StrokeKind::Inside, diff --git a/crates/egui/src/widgets/color_picker.rs b/crates/egui/src/widgets/color_picker.rs index b56f3c4e9..a9906cef1 100644 --- a/crates/egui/src/widgets/color_picker.rs +++ b/crates/egui/src/widgets/color_picker.rs @@ -100,10 +100,10 @@ fn color_button(ui: &mut Ui, color: Color32, open: bool) -> Response { let stroke_width = 1.0; show_color_at(ui.painter(), color, rect.shrink(stroke_width)); - let rounding = visuals.rounding.at_most(2); // Can't do more rounding because the background grid doesn't do any rounding + let corner_radius = visuals.corner_radius.at_most(2); // Can't do more rounding because the background grid doesn't do any rounding ui.painter().rect_stroke( rect, - rounding, + corner_radius, (stroke_width, visuals.bg_fill), // Using fill for stroke is intentional, because default style has no border StrokeKind::Inside, ); diff --git a/crates/egui/src/widgets/image.rs b/crates/egui/src/widgets/image.rs index 2290eabbd..89974dcaf 100644 --- a/crates/egui/src/widgets/image.rs +++ b/crates/egui/src/widgets/image.rs @@ -8,7 +8,7 @@ use epaint::{ use crate::{ load::{Bytes, SizeHint, SizedTexture, TextureLoadResult, TexturePoll}, - pos2, Color32, Context, Id, Mesh, Painter, Rect, Response, Rounding, Sense, Shape, Spinner, + pos2, Color32, Context, CornerRadius, Id, Mesh, Painter, Rect, Response, Sense, Shape, Spinner, TextStyle, TextureOptions, Ui, Vec2, Widget, WidgetInfo, WidgetType, }; @@ -29,7 +29,7 @@ use crate::{ /// # egui::__run_test_ui(|ui| { /// ui.add( /// egui::Image::new(egui::include_image!("../../assets/ferris.png")) -/// .rounding(5.0) +/// .corner_radius(5) /// ); /// # }); /// ``` @@ -39,7 +39,7 @@ use crate::{ /// # egui::__run_test_ui(|ui| { /// # let rect = egui::Rect::from_min_size(Default::default(), egui::Vec2::splat(100.0)); /// egui::Image::new(egui::include_image!("../../assets/ferris.png")) -/// .rounding(5.0) +/// .corner_radius(5) /// .tint(egui::Color32::LIGHT_BLUE) /// .paint_at(ui, rect); /// # }); @@ -233,25 +233,37 @@ impl<'a> Image<'a> { #[inline] pub fn rotate(mut self, angle: f32, origin: Vec2) -> Self { self.image_options.rotation = Some((Rot2::from_angle(angle), origin)); - self.image_options.rounding = Rounding::ZERO; // incompatible with rotation + self.image_options.corner_radius = CornerRadius::ZERO; // incompatible with rotation self } /// Round the corners of the image. /// - /// The default is no rounding ([`Rounding::ZERO`]). + /// The default is no rounding ([`CornerRadius::ZERO`]). /// /// Due to limitations in the current implementation, /// this will turn off any rotation of the image. #[inline] - pub fn rounding(mut self, rounding: impl Into) -> Self { - self.image_options.rounding = rounding.into(); - if self.image_options.rounding != Rounding::ZERO { + pub fn corner_radius(mut self, corner_radius: impl Into) -> Self { + self.image_options.corner_radius = corner_radius.into(); + if self.image_options.corner_radius != CornerRadius::ZERO { self.image_options.rotation = None; // incompatible with rounding } self } + /// Round the corners of the image. + /// + /// The default is no rounding ([`CornerRadius::ZERO`]). + /// + /// Due to limitations in the current implementation, + /// this will turn off any rotation of the image. + #[inline] + #[deprecated = "Renamed to `corner_radius`"] + pub fn rounding(self, corner_radius: impl Into) -> Self { + self.corner_radius(corner_radius) + } + /// Show a spinner when the image is loading. /// /// By default this uses the value of [`crate::Visuals::image_loading_spinners`]. @@ -354,7 +366,7 @@ impl<'a> Image<'a> { /// # egui::__run_test_ui(|ui| { /// # let rect = egui::Rect::from_min_size(Default::default(), egui::Vec2::splat(100.0)); /// egui::Image::new(egui::include_image!("../../assets/ferris.png")) - /// .rounding(5.0) + /// .corner_radius(5) /// .tint(egui::Color32::LIGHT_BLUE) /// .paint_at(ui, rect); /// # }); @@ -778,11 +790,11 @@ pub struct ImageOptions { /// Round the corners of the image. /// - /// The default is no rounding ([`Rounding::ZERO`]). + /// The default is no rounding ([`CornerRadius::ZERO`]). /// /// Due to limitations in the current implementation, /// this will turn off any rotation of the image. - pub rounding: Rounding, + pub corner_radius: CornerRadius, } impl Default for ImageOptions { @@ -792,7 +804,7 @@ impl Default for ImageOptions { bg_fill: Default::default(), tint: Color32::WHITE, rotation: None, - rounding: Rounding::ZERO, + corner_radius: CornerRadius::ZERO, } } } @@ -804,7 +816,11 @@ pub fn paint_texture_at( texture: &SizedTexture, ) { if options.bg_fill != Default::default() { - painter.add(RectShape::filled(rect, options.rounding, options.bg_fill)); + painter.add(RectShape::filled( + rect, + options.corner_radius, + options.bg_fill, + )); } match options.rotation { @@ -812,7 +828,7 @@ pub fn paint_texture_at( // TODO(emilk): implement this using `PathShape` (add texture support to it). // This will also give us anti-aliasing of rotated images. debug_assert!( - options.rounding == Rounding::ZERO, + options.corner_radius == CornerRadius::ZERO, "Image had both rounding and rotation. Please pick only one" ); @@ -823,7 +839,7 @@ pub fn paint_texture_at( } None => { painter.add( - RectShape::filled(rect, options.rounding, options.tint) + RectShape::filled(rect, options.corner_radius, options.tint) .with_texture(texture.id, options.uv), ); } diff --git a/crates/egui/src/widgets/image_button.rs b/crates/egui/src/widgets/image_button.rs index 7bf55cb60..b1dddbf7c 100644 --- a/crates/egui/src/widgets/image_button.rs +++ b/crates/egui/src/widgets/image_button.rs @@ -1,5 +1,5 @@ use crate::{ - widgets, Color32, Image, Rect, Response, Rounding, Sense, Ui, Vec2, Widget, WidgetInfo, + widgets, Color32, CornerRadius, Image, Rect, Response, Sense, Ui, Vec2, Widget, WidgetInfo, WidgetType, }; @@ -62,13 +62,24 @@ impl<'a> ImageButton<'a> { } /// Set rounding for the `ImageButton`. + /// /// If the underlying image already has rounding, this /// will override that value. #[inline] - pub fn rounding(mut self, rounding: impl Into) -> Self { - self.image = self.image.rounding(rounding.into()); + pub fn corner_radius(mut self, corner_radius: impl Into) -> Self { + self.image = self.image.corner_radius(corner_radius.into()); self } + + /// Set rounding for the `ImageButton`. + /// + /// If the underlying image already has rounding, this + /// will override that value. + #[inline] + #[deprecated = "Renamed to `corner_radius`"] + pub fn rounding(self, corner_radius: impl Into) -> Self { + self.corner_radius(corner_radius) + } } impl Widget for ImageButton<'_> { @@ -100,7 +111,7 @@ impl Widget for ImageButton<'_> { let selection = ui.visuals().selection; ( Vec2::ZERO, - self.image.image_options().rounding, + self.image.image_options().corner_radius, selection.bg_fill, selection.stroke, ) @@ -109,7 +120,7 @@ impl Widget for ImageButton<'_> { let expansion = Vec2::splat(visuals.expansion); ( expansion, - self.image.image_options().rounding, + self.image.image_options().corner_radius, visuals.weak_bg_fill, visuals.bg_stroke, ) diff --git a/crates/egui/src/widgets/progress_bar.rs b/crates/egui/src/widgets/progress_bar.rs index a8c27f5c3..44c9b8971 100644 --- a/crates/egui/src/widgets/progress_bar.rs +++ b/crates/egui/src/widgets/progress_bar.rs @@ -1,5 +1,5 @@ use crate::{ - lerp, vec2, Color32, NumExt, Pos2, Rect, Response, Rgba, Rounding, Sense, Shape, Stroke, + lerp, vec2, Color32, CornerRadius, NumExt, Pos2, Rect, Response, Rgba, Sense, Shape, Stroke, TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetType, }; @@ -19,7 +19,7 @@ pub struct ProgressBar { text: Option, fill: Option, animate: bool, - rounding: Option, + corner_radius: Option, } impl ProgressBar { @@ -32,7 +32,7 @@ impl ProgressBar { text: None, fill: None, animate: false, - rounding: None, + corner_radius: None, } } @@ -75,7 +75,7 @@ impl ProgressBar { /// Note that this will cause the UI to be redrawn. /// Defaults to `false`. /// - /// If [`Self::rounding`] and [`Self::animate`] are used simultaneously, the animation is not + /// If [`Self::corner_radius`] and [`Self::animate`] are used simultaneously, the animation is not /// rendered, since it requires a perfect circle to render correctly. However, the UI is still /// redrawn. #[inline] @@ -86,14 +86,20 @@ impl ProgressBar { /// Set the rounding of the progress bar. /// - /// If [`Self::rounding`] and [`Self::animate`] are used simultaneously, the animation is not + /// If [`Self::corner_radius`] and [`Self::animate`] are used simultaneously, the animation is not /// rendered, since it requires a perfect circle to render correctly. However, the UI is still /// redrawn. #[inline] - pub fn rounding(mut self, rounding: impl Into) -> Self { - self.rounding = Some(rounding.into()); + pub fn corner_radius(mut self, corner_radius: impl Into) -> Self { + self.corner_radius = Some(corner_radius.into()); self } + + #[inline] + #[deprecated = "Renamed to `corner_radius`"] + pub fn rounding(self, corner_radius: impl Into) -> Self { + self.corner_radius(corner_radius) + } } impl Widget for ProgressBar { @@ -105,7 +111,7 @@ impl Widget for ProgressBar { text, fill, animate, - rounding, + corner_radius, } = self; let animate = animate && progress < 1.0; @@ -133,13 +139,13 @@ impl Widget for ProgressBar { } let visuals = ui.style().visuals.clone(); - let is_custom_rounding = rounding.is_some(); - let corner_radius = outer_rect.height() / 2.0; - let rounding = rounding.unwrap_or_else(|| corner_radius.into()); + let has_custom_cr = corner_radius.is_some(); + let half_height = outer_rect.height() / 2.0; + let corner_radius = corner_radius.unwrap_or_else(|| half_height.into()); ui.painter() - .rect_filled(outer_rect, rounding, visuals.extreme_bg_color); + .rect_filled(outer_rect, corner_radius, visuals.extreme_bg_color); let min_width = - 2.0 * f32::max(rounding.sw as _, rounding.nw as _).at_most(corner_radius); + 2.0 * f32::max(corner_radius.sw as _, corner_radius.nw as _).at_most(half_height); let filled_width = (outer_rect.width() * progress).at_least(min_width); let inner_rect = Rect::from_min_size(outer_rect.min, vec2(filled_width, outer_rect.height())); @@ -154,25 +160,25 @@ impl Widget for ProgressBar { ui.painter().rect_filled( inner_rect, - rounding, + corner_radius, Color32::from( Rgba::from(fill.unwrap_or(visuals.selection.bg_fill)) * color_factor as f32, ), ); - if animate && !is_custom_rounding { + if animate && !has_custom_cr { let n_points = 20; let time = ui.input(|i| i.time); let start_angle = time * std::f64::consts::TAU; let end_angle = start_angle + 240f64.to_radians() * time.sin(); - let circle_radius = corner_radius - 2.0; + let circle_radius = half_height - 2.0; let points: Vec = (0..n_points) .map(|i| { let angle = lerp(start_angle..=end_angle, i as f64 / n_points as f64); let (sin, cos) = angle.sin_cos(); inner_rect.right_center() + circle_radius * vec2(cos as f32, sin as f32) - + vec2(-corner_radius, 0.0) + + vec2(-half_height, 0.0) }) .collect(); ui.painter() diff --git a/crates/egui/src/widgets/selected_label.rs b/crates/egui/src/widgets/selected_label.rs index 16978794b..dfed4d2ba 100644 --- a/crates/egui/src/widgets/selected_label.rs +++ b/crates/egui/src/widgets/selected_label.rs @@ -71,7 +71,7 @@ impl Widget for SelectableLabel { ui.painter().rect( rect, - visuals.rounding, + visuals.corner_radius, visuals.weak_bg_fill, visuals.bg_stroke, epaint::StrokeKind::Inside, diff --git a/crates/egui/src/widgets/slider.rs b/crates/egui/src/widgets/slider.rs index f721ce101..590282074 100644 --- a/crates/egui/src/widgets/slider.rs +++ b/crates/egui/src/widgets/slider.rs @@ -760,10 +760,10 @@ impl Slider<'_> { let rail_radius = (spacing.slider_rail_height / 2.0).at_least(0.0); let rail_rect = self.rail_rect(rect, rail_radius); - let rounding = widget_visuals.inactive.rounding; + let corner_radius = widget_visuals.inactive.corner_radius; ui.painter() - .rect_filled(rail_rect, rounding, widget_visuals.inactive.bg_fill); + .rect_filled(rail_rect, corner_radius, widget_visuals.inactive.bg_fill); let position_1d = self.position_from_value(value, position_range); let center = self.marker_center(position_1d, &rail_rect); @@ -780,16 +780,16 @@ impl Slider<'_> { // The trailing rect has to be drawn differently depending on the orientation. match self.orientation { SliderOrientation::Horizontal => { - trailing_rail_rect.max.x = center.x + rounding.nw as f32; + trailing_rail_rect.max.x = center.x + corner_radius.nw as f32; } SliderOrientation::Vertical => { - trailing_rail_rect.min.y = center.y - rounding.se as f32; + trailing_rail_rect.min.y = center.y - corner_radius.se as f32; } }; ui.painter().rect_filled( trailing_rail_rect, - rounding, + corner_radius, ui.visuals().selection.bg_fill, ); } @@ -817,7 +817,7 @@ impl Slider<'_> { let rect = Rect::from_center_size(center, 2.0 * v); ui.painter().rect( rect, - visuals.rounding, + visuals.corner_radius, visuals.bg_fill, visuals.fg_stroke, epaint::StrokeKind::Inside, diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 05a8470c1..2f685bbc1 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -442,7 +442,7 @@ impl TextEdit<'_> { if output.response.has_focus() { epaint::RectShape::new( frame_rect, - visuals.rounding, + visuals.corner_radius, background_color, ui.visuals().selection.stroke, StrokeKind::Inside, @@ -450,7 +450,7 @@ impl TextEdit<'_> { } else { epaint::RectShape::new( frame_rect, - visuals.rounding, + visuals.corner_radius, background_color, visuals.bg_stroke, // TODO(emilk): we want to show something here, or a text-edit field doesn't "pop". StrokeKind::Inside, @@ -460,7 +460,7 @@ impl TextEdit<'_> { let visuals = &ui.style().visuals.widgets.inactive; epaint::RectShape::stroke( frame_rect, - visuals.rounding, + visuals.corner_radius, visuals.bg_stroke, // TODO(emilk): we want to show something here, or a text-edit field doesn't "pop". StrokeKind::Inside, ) diff --git a/crates/egui_demo_app/src/frame_history.rs b/crates/egui_demo_app/src/frame_history.rs index b1091faba..231ba4a4f 100644 --- a/crates/egui_demo_app/src/frame_history.rs +++ b/crates/egui_demo_app/src/frame_history.rs @@ -72,7 +72,7 @@ impl FrameHistory { let mut shapes = Vec::with_capacity(3 + 2 * history.len()); shapes.push(Shape::Rect(epaint::RectShape::new( rect, - style.rounding, + style.corner_radius, ui.visuals().extreme_bg_color, ui.style().noninteractive().bg_stroke, egui::StrokeKind::Inside, diff --git a/crates/egui_demo_app/src/wrap_app.rs b/crates/egui_demo_app/src/wrap_app.rs index 6409eb902..eb26a5d7c 100644 --- a/crates/egui_demo_app/src/wrap_app.rs +++ b/crates/egui_demo_app/src/wrap_app.rs @@ -47,7 +47,7 @@ impl eframe::App for FractalClockApp { .frame( egui::Frame::dark_canvas(&ctx.style()) .stroke(egui::Stroke::NONE) - .rounding(0), + .corner_radius(0), ) .show(ctx, |ui| { self.fractal_clock diff --git a/crates/egui_demo_lib/src/demo/frame_demo.rs b/crates/egui_demo_lib/src/demo/frame_demo.rs index 71c5e9ab2..8c4fc984f 100644 --- a/crates/egui_demo_lib/src/demo/frame_demo.rs +++ b/crates/egui_demo_lib/src/demo/frame_demo.rs @@ -10,7 +10,7 @@ impl Default for FrameDemo { frame: egui::Frame::new() .inner_margin(12) .outer_margin(24) - .rounding(14) + .corner_radius(14) .shadow(egui::Shadow { offset: [8, 12], blur: 16, @@ -56,7 +56,7 @@ impl crate::View for FrameDemo { // We want to paint a background around the outer margin of the demonstration frame, so we use another frame around it: egui::Frame::default() .stroke(ui.visuals().widgets.noninteractive.bg_stroke) - .rounding(ui.visuals().widgets.noninteractive.rounding) + .corner_radius(ui.visuals().widgets.noninteractive.corner_radius) .show(ui, |ui| { self.frame.show(ui, |ui| { ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); 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 ce154ab4a..edb19c3ea 100644 --- a/crates/egui_demo_lib/src/demo/misc_demo_window.rs +++ b/crates/egui_demo_lib/src/demo/misc_demo_window.rs @@ -358,7 +358,7 @@ impl ColorWidgets { #[cfg_attr(feature = "serde", serde(default))] struct BoxPainting { size: Vec2, - rounding: f32, + corner_radius: f32, stroke_width: f32, num_boxes: usize, } @@ -367,7 +367,7 @@ impl Default for BoxPainting { fn default() -> Self { Self { size: vec2(64.0, 32.0), - rounding: 5.0, + corner_radius: 5.0, stroke_width: 2.0, num_boxes: 1, } @@ -378,7 +378,7 @@ impl BoxPainting { pub fn ui(&mut self, ui: &mut Ui) { ui.add(Slider::new(&mut self.size.x, 0.0..=500.0).text("width")); ui.add(Slider::new(&mut self.size.y, 0.0..=500.0).text("height")); - ui.add(Slider::new(&mut self.rounding, 0.0..=50.0).text("rounding")); + ui.add(Slider::new(&mut self.corner_radius, 0.0..=50.0).text("corner_radius")); ui.add(Slider::new(&mut self.stroke_width, 0.0..=10.0).text("stroke_width")); ui.add(Slider::new(&mut self.num_boxes, 0..=8).text("num_boxes")); @@ -387,7 +387,7 @@ impl BoxPainting { let (rect, _response) = ui.allocate_at_least(self.size, Sense::hover()); ui.painter().rect( rect, - self.rounding, + self.corner_radius, ui.visuals().text_color().gamma_multiply(0.5), Stroke::new(self.stroke_width, Color32::WHITE), egui::StrokeKind::Inside, 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 033861016..8c5a1adc3 100644 --- a/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs +++ b/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs @@ -271,7 +271,7 @@ fn rect_shape_ui(ui: &mut egui::Ui, shape: &mut RectShape) { let RectShape { rect, - rounding, + corner_radius, fill, stroke, stroke_kind, @@ -304,8 +304,8 @@ fn rect_shape_ui(ui: &mut egui::Ui, shape: &mut RectShape) { }); ui.end_row(); - ui.label("Rounding"); - ui.add(rounding); + ui.label("Corner radius"); + ui.add(corner_radius); ui.end_row(); ui.label("Fill"); diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Frame.png b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png index 3791d77a6..547c65bee 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Frame.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f90f94a842a1d0f1386c3cdd28e60e6ad6efe968f510a11bde418b5bc70a81d2 -size 23897 +oid sha256:244d539111e994a4ed2aa95a2b4d0ff12b948e21843c8a1dddcf54cbb388f1aa +size 24280 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png index 3140fbc94..8e97cb8e3 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c39868f184364555ae90fbfc035aa668f61189be7aeee6bec4e45a8de438ad8e -size 87661 +oid sha256:daa5ec4ddd2f983c4b9f2b0a73c973f58abcb186fbd0d68a9fd0ce7173e5d4e7 +size 88031 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png index 51b0bd901..da1f47816 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dd029fdc49e6d4078337472c39b9d58bf69073c1b7750c6dd1b7ccd450d52395 -size 119869 +oid sha256:c09af9c7f4297e2d5b2305366ed57b3203807ca2426314acdf836e25f154d8eb +size 120244 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png index 290216b1b..7704e4ddf 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e698ba12efd129099877248f9630ba983d683e1b495b2523ed3569989341e905 -size 51735 +oid sha256:f621bcf7c8fd18156bef2428ab3b9994e6a4d475ae589eb734d50f9a4f3383bd +size 52101 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png index ff42489e8..0fcb1aeb4 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:18b81b5cd88372b65b1ecc62e9a5e894960279310b05a1bd5c8df5bffa244ad0 -size 54922 +oid sha256:f1476e105a3e9c1ff7b2f4a82481462795e4708e3fcf6d495a042faae537184e +size 55298 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png index 8fa9370ff..4df96a3ec 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf5df173431d330e4b6045a72227c2bb7613ec98c63f013ea899a3a57cd6617a -size 55522 +oid sha256:695a731d9e302db2c5b7f4a0ef44794cf55eb0be093c070b8ffaeb28121569bc +size 55888 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png index 882691e82..c45845c66 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:987c162842a08271e833c41a55573d9f30cf045bf7ca3cb03e81d0cc13d5a16e -size 36763 +oid sha256:854c12c69b31c0c82a9596d167772e00d7a051600e4151535e2cec04491e57a6 +size 37139 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png index 6741df537..9cf019ea1 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c5c3055cd190823a4204aa6f23362a88bc5ab5ed5453d9be1b6077dded6cd54 -size 36809 +oid sha256:1aacb27847c2e942d56d60e4b72797d93f7265a6effe4e47437e1314e6477e6f +size 37184 diff --git a/crates/egui_extras/src/layout.rs b/crates/egui_extras/src/layout.rs index 0121f83e0..fb84169f3 100644 --- a/crates/egui_extras/src/layout.rs +++ b/crates/egui_extras/src/layout.rs @@ -128,7 +128,7 @@ impl<'l> StripLayout<'l> { if flags.striped { self.ui.painter().rect_filled( gapless_rect, - egui::Rounding::ZERO, + egui::CornerRadius::ZERO, self.ui.visuals().faint_bg_color, ); } @@ -136,7 +136,7 @@ impl<'l> StripLayout<'l> { if flags.selected { self.ui.painter().rect_filled( gapless_rect, - egui::Rounding::ZERO, + egui::CornerRadius::ZERO, self.ui.visuals().selection.bg_fill, ); } @@ -144,7 +144,7 @@ impl<'l> StripLayout<'l> { if flags.hovered && !flags.selected && self.sense.interactive() { self.ui.painter().rect_filled( gapless_rect, - egui::Rounding::ZERO, + egui::CornerRadius::ZERO, self.ui.visuals().widgets.hovered.bg_fill, ); } diff --git a/crates/epaint/src/rounding.rs b/crates/epaint/src/corner_radius.rs similarity index 88% rename from crates/epaint/src/rounding.rs rename to crates/epaint/src/corner_radius.rs index e0d79b14c..07bd56c9e 100644 --- a/crates/epaint/src/rounding.rs +++ b/crates/epaint/src/corner_radius.rs @@ -7,10 +7,10 @@ /// The rounding uses `u8` to save space, /// so the amount of rounding is limited to integers in the range `[0, 255]`. /// -/// For calculations, you may want to use [`crate::Roundingf`] instead, which uses `f32`. +/// For calculations, you may want to use [`crate::CornerRadiusF32`] instead, which uses `f32`. #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct Rounding { +pub struct CornerRadius { /// Radius of the rounding of the North-West (left top) corner. pub nw: u8, @@ -24,28 +24,28 @@ pub struct Rounding { pub se: u8, } -impl Default for Rounding { +impl Default for CornerRadius { #[inline] fn default() -> Self { Self::ZERO } } -impl From for Rounding { +impl From for CornerRadius { #[inline] fn from(radius: u8) -> Self { Self::same(radius) } } -impl From for Rounding { +impl From for CornerRadius { #[inline] fn from(radius: f32) -> Self { Self::same(radius.round() as u8) } } -impl Rounding { +impl CornerRadius { /// No rounding on any corner. pub const ZERO: Self = Self { nw: 0, @@ -99,7 +99,7 @@ impl Rounding { } } -impl std::ops::Add for Rounding { +impl std::ops::Add for CornerRadius { type Output = Self; #[inline] fn add(self, rhs: Self) -> Self { @@ -112,7 +112,7 @@ impl std::ops::Add for Rounding { } } -impl std::ops::Add for Rounding { +impl std::ops::Add for CornerRadius { type Output = Self; #[inline] fn add(self, rhs: u8) -> Self { @@ -125,7 +125,7 @@ impl std::ops::Add for Rounding { } } -impl std::ops::AddAssign for Rounding { +impl std::ops::AddAssign for CornerRadius { #[inline] fn add_assign(&mut self, rhs: Self) { *self = Self { @@ -137,7 +137,7 @@ impl std::ops::AddAssign for Rounding { } } -impl std::ops::AddAssign for Rounding { +impl std::ops::AddAssign for CornerRadius { #[inline] fn add_assign(&mut self, rhs: u8) { *self = Self { @@ -149,7 +149,7 @@ impl std::ops::AddAssign for Rounding { } } -impl std::ops::Sub for Rounding { +impl std::ops::Sub for CornerRadius { type Output = Self; #[inline] fn sub(self, rhs: Self) -> Self { @@ -162,7 +162,7 @@ impl std::ops::Sub for Rounding { } } -impl std::ops::Sub for Rounding { +impl std::ops::Sub for CornerRadius { type Output = Self; #[inline] fn sub(self, rhs: u8) -> Self { @@ -175,7 +175,7 @@ impl std::ops::Sub for Rounding { } } -impl std::ops::SubAssign for Rounding { +impl std::ops::SubAssign for CornerRadius { #[inline] fn sub_assign(&mut self, rhs: Self) { *self = Self { @@ -187,7 +187,7 @@ impl std::ops::SubAssign for Rounding { } } -impl std::ops::SubAssign for Rounding { +impl std::ops::SubAssign for CornerRadius { #[inline] fn sub_assign(&mut self, rhs: u8) { *self = Self { @@ -199,7 +199,7 @@ impl std::ops::SubAssign for Rounding { } } -impl std::ops::Div for Rounding { +impl std::ops::Div for CornerRadius { type Output = Self; #[inline] fn div(self, rhs: f32) -> Self { @@ -212,7 +212,7 @@ impl std::ops::Div for Rounding { } } -impl std::ops::DivAssign for Rounding { +impl std::ops::DivAssign for CornerRadius { #[inline] fn div_assign(&mut self, rhs: f32) { *self = Self { @@ -224,7 +224,7 @@ impl std::ops::DivAssign for Rounding { } } -impl std::ops::Mul for Rounding { +impl std::ops::Mul for CornerRadius { type Output = Self; #[inline] fn mul(self, rhs: f32) -> Self { @@ -237,7 +237,7 @@ impl std::ops::Mul for Rounding { } } -impl std::ops::MulAssign for Rounding { +impl std::ops::MulAssign for CornerRadius { #[inline] fn mul_assign(&mut self, rhs: f32) { *self = Self { diff --git a/crates/epaint/src/roundingf.rs b/crates/epaint/src/corner_radius_f32.rs similarity index 79% rename from crates/epaint/src/roundingf.rs rename to crates/epaint/src/corner_radius_f32.rs index b49cbc77f..0a88aaac7 100644 --- a/crates/epaint/src/roundingf.rs +++ b/crates/epaint/src/corner_radius_f32.rs @@ -1,11 +1,11 @@ -use crate::Rounding; +use crate::CornerRadius; /// How rounded the corners of things should be, in `f32`. /// -/// This is used for calculations, but storage is usually done with the more compact [`Rounding`]. +/// This is used for calculations, but storage is usually done with the more compact [`CornerRadius`]. #[derive(Copy, Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct Roundingf { +pub struct CornerRadiusF32 { /// Radius of the rounding of the North-West (left top) corner. pub nw: f32, @@ -19,38 +19,38 @@ pub struct Roundingf { pub se: f32, } -impl From for Roundingf { +impl From for CornerRadiusF32 { #[inline] - fn from(rounding: Rounding) -> Self { + fn from(cr: CornerRadius) -> Self { Self { - nw: rounding.nw as f32, - ne: rounding.ne as f32, - sw: rounding.sw as f32, - se: rounding.se as f32, + nw: cr.nw as f32, + ne: cr.ne as f32, + sw: cr.sw as f32, + se: cr.se as f32, } } } -impl From for Rounding { +impl From for CornerRadius { #[inline] - fn from(rounding: Roundingf) -> Self { + fn from(cr: CornerRadiusF32) -> Self { Self { - nw: rounding.nw.round() as u8, - ne: rounding.ne.round() as u8, - sw: rounding.sw.round() as u8, - se: rounding.se.round() as u8, + nw: cr.nw.round() as u8, + ne: cr.ne.round() as u8, + sw: cr.sw.round() as u8, + se: cr.se.round() as u8, } } } -impl Default for Roundingf { +impl Default for CornerRadiusF32 { #[inline] fn default() -> Self { Self::ZERO } } -impl From for Roundingf { +impl From for CornerRadiusF32 { #[inline] fn from(radius: f32) -> Self { Self { @@ -62,7 +62,7 @@ impl From for Roundingf { } } -impl Roundingf { +impl CornerRadiusF32 { /// No rounding on any corner. pub const ZERO: Self = Self { nw: 0.0, @@ -111,7 +111,7 @@ impl Roundingf { } } -impl std::ops::Add for Roundingf { +impl std::ops::Add for CornerRadiusF32 { type Output = Self; #[inline] fn add(self, rhs: Self) -> Self { @@ -124,7 +124,7 @@ impl std::ops::Add for Roundingf { } } -impl std::ops::AddAssign for Roundingf { +impl std::ops::AddAssign for CornerRadiusF32 { #[inline] fn add_assign(&mut self, rhs: Self) { *self = Self { @@ -136,7 +136,7 @@ impl std::ops::AddAssign for Roundingf { } } -impl std::ops::AddAssign for Roundingf { +impl std::ops::AddAssign for CornerRadiusF32 { #[inline] fn add_assign(&mut self, rhs: f32) { *self = Self { @@ -148,7 +148,7 @@ impl std::ops::AddAssign for Roundingf { } } -impl std::ops::Sub for Roundingf { +impl std::ops::Sub for CornerRadiusF32 { type Output = Self; #[inline] fn sub(self, rhs: Self) -> Self { @@ -161,7 +161,7 @@ impl std::ops::Sub for Roundingf { } } -impl std::ops::SubAssign for Roundingf { +impl std::ops::SubAssign for CornerRadiusF32 { #[inline] fn sub_assign(&mut self, rhs: Self) { *self = Self { @@ -173,7 +173,7 @@ impl std::ops::SubAssign for Roundingf { } } -impl std::ops::SubAssign for Roundingf { +impl std::ops::SubAssign for CornerRadiusF32 { #[inline] fn sub_assign(&mut self, rhs: f32) { *self = Self { @@ -185,7 +185,7 @@ impl std::ops::SubAssign for Roundingf { } } -impl std::ops::Div for Roundingf { +impl std::ops::Div for CornerRadiusF32 { type Output = Self; #[inline] fn div(self, rhs: f32) -> Self { @@ -198,7 +198,7 @@ impl std::ops::Div for Roundingf { } } -impl std::ops::DivAssign for Roundingf { +impl std::ops::DivAssign for CornerRadiusF32 { #[inline] fn div_assign(&mut self, rhs: f32) { *self = Self { @@ -210,7 +210,7 @@ impl std::ops::DivAssign for Roundingf { } } -impl std::ops::Mul for Roundingf { +impl std::ops::Mul for CornerRadiusF32 { type Output = Self; #[inline] fn mul(self, rhs: f32) -> Self { @@ -223,7 +223,7 @@ impl std::ops::Mul for Roundingf { } } -impl std::ops::MulAssign for Roundingf { +impl std::ops::MulAssign for CornerRadiusF32 { #[inline] fn mul_assign(&mut self, rhs: f32) { *self = Self { diff --git a/crates/epaint/src/lib.rs b/crates/epaint/src/lib.rs index b1d0045ee..ac0a90c60 100644 --- a/crates/epaint/src/lib.rs +++ b/crates/epaint/src/lib.rs @@ -25,13 +25,13 @@ mod brush; pub mod color; +mod corner_radius; +mod corner_radius_f32; pub mod image; mod margin; mod marginf; mod mesh; pub mod mutex; -mod rounding; -mod roundingf; mod shadow; pub mod shape_transform; mod shapes; @@ -48,12 +48,12 @@ mod viewport; pub use self::{ brush::Brush, color::ColorMode, + corner_radius::CornerRadius, + corner_radius_f32::CornerRadiusF32, image::{ColorImage, FontImage, ImageData, ImageDelta}, margin::Margin, marginf::Marginf, mesh::{Mesh, Mesh16, Vertex}, - rounding::Rounding, - roundingf::Roundingf, shadow::Shadow, shapes::{ CircleShape, CubicBezierShape, EllipseShape, PaintCallback, PaintCallbackInfo, PathShape, @@ -69,6 +69,9 @@ pub use self::{ viewport::ViewportInPixels, }; +#[deprecated = "Renamed to CornerRadius"] +pub type Rounding = CornerRadius; + #[allow(deprecated)] pub use tessellator::tessellate_shapes; diff --git a/crates/epaint/src/shadow.rs b/crates/epaint/src/shadow.rs index 959049ace..e05cbdbe4 100644 --- a/crates/epaint/src/shadow.rs +++ b/crates/epaint/src/shadow.rs @@ -1,4 +1,4 @@ -use crate::{Color32, Marginf, Rect, RectShape, Rounding, Vec2}; +use crate::{Color32, CornerRadius, Marginf, Rect, RectShape, Vec2}; /// The color and fuzziness of a fuzzy shape. /// @@ -44,7 +44,7 @@ impl Shadow { }; /// The argument is the rectangle of the shadow caster. - pub fn as_shape(&self, rect: Rect, rounding: impl Into) -> RectShape { + pub fn as_shape(&self, rect: Rect, corner_radius: impl Into) -> RectShape { // tessellator.clip_rect = clip_rect; // TODO(emilk): culling let Self { @@ -58,9 +58,9 @@ impl Shadow { let rect = rect .translate(Vec2::new(offset_x as _, offset_y as _)) .expand(spread as _); - let rounding = rounding.into() + Rounding::from(spread); + let corner_radius = corner_radius.into() + CornerRadius::from(spread); - RectShape::filled(rect, rounding, color).with_blur_width(blur as _) + RectShape::filled(rect, corner_radius, color).with_blur_width(blur as _) } /// How much larger than the parent rect are we in each direction? diff --git a/crates/epaint/src/shape_transform.rs b/crates/epaint/src/shape_transform.rs index 45805a276..469f2e521 100644 --- a/crates/epaint/src/shape_transform.rs +++ b/crates/epaint/src/shape_transform.rs @@ -60,7 +60,7 @@ pub fn adjust_colors( }) | Shape::Rect(RectShape { rect: _, - rounding: _, + corner_radius: _, fill, stroke, stroke_kind: _, diff --git a/crates/epaint/src/shapes/rect_shape.rs b/crates/epaint/src/shapes/rect_shape.rs index cd54dc8e9..ead5b7af2 100644 --- a/crates/epaint/src/shapes/rect_shape.rs +++ b/crates/epaint/src/shapes/rect_shape.rs @@ -10,7 +10,7 @@ pub struct RectShape { /// How rounded the corners of the rectangle are. /// - /// Use `Rounding::ZERO` for for sharp corners. + /// Use [`CornerRadius::ZERO`] for for sharp corners. /// /// This is the corner radii of the rectangle. /// If there is a stroke, then the stroke will have an inner and outer corner radius, @@ -18,7 +18,7 @@ pub struct RectShape { /// /// For [`StrokeKind::Inside`], the outside of the stroke coincides with the rectangle, /// so the rounding will in this case specify the outer corner radius. - pub rounding: Rounding, + pub corner_radius: CornerRadius, /// How to fill the rectangle. pub fill: Color32, @@ -73,14 +73,14 @@ impl RectShape { #[inline] pub fn new( rect: Rect, - rounding: impl Into, + corner_radius: impl Into, fill_color: impl Into, stroke: impl Into, stroke_kind: StrokeKind, ) -> Self { Self { rect, - rounding: rounding.into(), + corner_radius: corner_radius.into(), fill: fill_color.into(), stroke: stroke.into(), stroke_kind, @@ -93,12 +93,12 @@ impl RectShape { #[inline] pub fn filled( rect: Rect, - rounding: impl Into, + corner_radius: impl Into, fill_color: impl Into, ) -> Self { Self::new( rect, - rounding, + corner_radius, fill_color, Stroke::NONE, StrokeKind::Outside, // doesn't matter @@ -108,12 +108,12 @@ impl RectShape { #[inline] pub fn stroke( rect: Rect, - rounding: impl Into, + corner_radius: impl Into, stroke: impl Into, stroke_kind: StrokeKind, ) -> Self { let fill = Color32::TRANSPARENT; - Self::new(rect, rounding, fill, stroke, stroke_kind) + Self::new(rect, corner_radius, fill, stroke, stroke_kind) } /// Set if the stroke is on the inside, outside, or centered on the rectangle. diff --git a/crates/epaint/src/shapes/shape.rs b/crates/epaint/src/shapes/shape.rs index ddbaacfd3..6c24881de 100644 --- a/crates/epaint/src/shapes/shape.rs +++ b/crates/epaint/src/shapes/shape.rs @@ -7,7 +7,7 @@ use emath::{pos2, Align2, Pos2, Rangef, Rect, TSTransform, Vec2}; use crate::{ stroke::PathStroke, text::{FontId, Fonts, Galley}, - Color32, Mesh, Rounding, Stroke, StrokeKind, TextureId, + Color32, CornerRadius, Mesh, Stroke, StrokeKind, TextureId, }; use super::{ @@ -279,21 +279,21 @@ impl Shape { #[inline] pub fn rect_filled( rect: Rect, - rounding: impl Into, + corner_radius: impl Into, fill_color: impl Into, ) -> Self { - Self::Rect(RectShape::filled(rect, rounding, fill_color)) + Self::Rect(RectShape::filled(rect, corner_radius, fill_color)) } /// See also [`Self::rect_filled`]. #[inline] pub fn rect_stroke( rect: Rect, - rounding: impl Into, + corner_radius: impl Into, stroke: impl Into, stroke_kind: StrokeKind, ) -> Self { - Self::Rect(RectShape::stroke(rect, rounding, stroke, stroke_kind)) + Self::Rect(RectShape::stroke(rect, corner_radius, stroke, stroke_kind)) } #[allow(clippy::needless_pass_by_value)] @@ -451,7 +451,7 @@ impl Shape { } Self::Rect(rect_shape) => { rect_shape.rect = transform * rect_shape.rect; - rect_shape.rounding *= transform.scaling; + rect_shape.corner_radius *= transform.scaling; rect_shape.stroke.width *= transform.scaling; rect_shape.blur_width *= transform.scaling; } diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index b16d22f52..608317dc6 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -9,8 +9,8 @@ use emath::{pos2, remap, vec2, GuiRounding as _, NumExt, Pos2, Rect, Rot2, Vec2} use crate::{ color::ColorMode, emath, stroke::PathStroke, texture_atlas::PreparedDisc, CircleShape, - ClippedPrimitive, ClippedShape, Color32, CubicBezierShape, EllipseShape, Mesh, PathShape, - Primitive, QuadraticBezierShape, RectShape, Roundingf, Shape, Stroke, StrokeKind, TextShape, + ClippedPrimitive, ClippedShape, Color32, CornerRadiusF32, CubicBezierShape, EllipseShape, Mesh, + PathShape, Primitive, QuadraticBezierShape, RectShape, Shape, Stroke, StrokeKind, TextShape, TextureId, Vertex, WHITE_UV, }; @@ -534,19 +534,19 @@ impl Path { pub mod path { //! Helpers for constructing paths - use crate::Roundingf; + use crate::CornerRadiusF32; use emath::{pos2, Pos2, Rect}; /// overwrites existing points - pub fn rounded_rectangle(path: &mut Vec, rect: Rect, rounding: Roundingf) { + pub fn rounded_rectangle(path: &mut Vec, rect: Rect, cr: CornerRadiusF32) { path.clear(); let min = rect.min; let max = rect.max; - let r = clamp_rounding(rounding, rect); + let cr = clamp_corner_radius(cr, rect); - if r == Roundingf::ZERO { + if cr == CornerRadiusF32::ZERO { path.reserve(4); path.push(pos2(min.x, min.y)); // left top path.push(pos2(max.x, min.y)); // right top @@ -557,27 +557,27 @@ pub mod path { // Duplicated vertices can happen when one side is all rounding, with no straight edge between. let eps = f32::EPSILON * rect.size().max_elem(); - add_circle_quadrant(path, pos2(max.x - r.se, max.y - r.se), r.se, 0.0); // south east + add_circle_quadrant(path, pos2(max.x - cr.se, max.y - cr.se), cr.se, 0.0); // south east - if rect.width() <= r.se + r.sw + eps { + if rect.width() <= cr.se + cr.sw + eps { path.pop(); // avoid duplicated vertex } - add_circle_quadrant(path, pos2(min.x + r.sw, max.y - r.sw), r.sw, 1.0); // south west + add_circle_quadrant(path, pos2(min.x + cr.sw, max.y - cr.sw), cr.sw, 1.0); // south west - if rect.height() <= r.sw + r.nw + eps { + if rect.height() <= cr.sw + cr.nw + eps { path.pop(); // avoid duplicated vertex } - add_circle_quadrant(path, pos2(min.x + r.nw, min.y + r.nw), r.nw, 2.0); // north west + add_circle_quadrant(path, pos2(min.x + cr.nw, min.y + cr.nw), cr.nw, 2.0); // north west - if rect.width() <= r.nw + r.ne + eps { + if rect.width() <= cr.nw + cr.ne + eps { path.pop(); // avoid duplicated vertex } - add_circle_quadrant(path, pos2(max.x - r.ne, min.y + r.ne), r.ne, 3.0); // north east + add_circle_quadrant(path, pos2(max.x - cr.ne, min.y + cr.ne), cr.ne, 3.0); // north east - if rect.height() <= r.ne + r.se + eps { + if rect.height() <= cr.ne + cr.se + eps { path.pop(); // avoid duplicated vertex } } @@ -633,11 +633,11 @@ pub mod path { } // Ensures the radius of each corner is within a valid range - fn clamp_rounding(rounding: Roundingf, rect: Rect) -> Roundingf { + fn clamp_corner_radius(cr: CornerRadiusF32, rect: Rect) -> CornerRadiusF32 { let half_width = rect.width() * 0.5; let half_height = rect.height() * 0.5; let max_cr = half_width.min(half_height); - rounding.at_most(max_cr).at_least(0.0) + cr.at_most(max_cr).at_least(0.0) } } @@ -1729,7 +1729,7 @@ impl Tessellator { let brush = rect_shape.brush.as_ref(); let RectShape { mut rect, - rounding, + corner_radius, mut fill, mut stroke, mut stroke_kind, @@ -1738,7 +1738,7 @@ impl Tessellator { brush: _, // brush is extracted on its own, because it is not Copy } = *rect_shape; - let mut rounding = Roundingf::from(rounding); + let mut corner_radius = CornerRadiusF32::from(corner_radius); let round_to_pixels = round_to_pixels.unwrap_or(self.options.round_rects_to_pixels); let pixel_size = 1.0 / self.pixels_per_point; @@ -1848,7 +1848,7 @@ impl Tessellator { .at_most(rect.size().min_elem() - eps - 2.0 * stroke.width) .at_least(0.0); - rounding += 0.5 * blur_width; + corner_radius += 0.5 * blur_width; self.feathering = self.feathering.max(blur_width); } @@ -1858,54 +1858,54 @@ impl Tessellator { // We do this because `path::rounded_rectangle` uses the // corner radius to pick the fidelity/resolution of the corner. - let original_rounding = rounding; + let original_cr = corner_radius; match stroke_kind { StrokeKind::Inside => {} StrokeKind::Middle => { rect = rect.expand(stroke.width / 2.0); - rounding += stroke.width / 2.0; + corner_radius += stroke.width / 2.0; } StrokeKind::Outside => { rect = rect.expand(stroke.width); - rounding += stroke.width; + corner_radius += stroke.width; } } stroke_kind = StrokeKind::Inside; - // A small rounding is incompatible with a wide stroke, + // A small corner_radius is incompatible with a wide stroke, // because the small bend will be extruded inwards and cross itself. // There are two ways to solve this (wile maintaining constant stroke width): - // either we increase the rounding, or we set it to zero. - // We choose the former: if the user asks for _any_ rounding, they should get it. + // either we increase the corner_radius, or we set it to zero. + // We choose the former: if the user asks for _any_ corner_radius, they should get it. - let min_inside_rounding = 0.1; // Large enough to avoid numerical issues - let min_outside_rounding = stroke.width + min_inside_rounding; + let min_inside_cr = 0.1; // Large enough to avoid numerical issues + let min_outside_cr = stroke.width + min_inside_cr; - let extra_rounding_tweak = 0.4; // Otherwise is doesn't _feels_ enough. + let extra_cr_tweak = 0.4; // Otherwise is doesn't _feels_ enough. - if 0.0 < original_rounding.nw { - rounding.nw += extra_rounding_tweak; - rounding.nw = rounding.nw.at_least(min_outside_rounding); + if 0.0 < original_cr.nw { + corner_radius.nw += extra_cr_tweak; + corner_radius.nw = corner_radius.nw.at_least(min_outside_cr); } - if 0.0 < original_rounding.ne { - rounding.ne += extra_rounding_tweak; - rounding.ne = rounding.ne.at_least(min_outside_rounding); + if 0.0 < original_cr.ne { + corner_radius.ne += extra_cr_tweak; + corner_radius.ne = corner_radius.ne.at_least(min_outside_cr); } - if 0.0 < original_rounding.sw { - rounding.sw += extra_rounding_tweak; - rounding.sw = rounding.sw.at_least(min_outside_rounding); + if 0.0 < original_cr.sw { + corner_radius.sw += extra_cr_tweak; + corner_radius.sw = corner_radius.sw.at_least(min_outside_cr); } - if 0.0 < original_rounding.se { - rounding.se += extra_rounding_tweak; - rounding.se = rounding.se.at_least(min_outside_rounding); + if 0.0 < original_cr.se { + corner_radius.se += extra_cr_tweak; + corner_radius.se = corner_radius.se.at_least(min_outside_cr); } } let path = &mut self.scratchpad_path; path.clear(); - path::rounded_rectangle(&mut self.scratchpad_points, rect, rounding); + path::rounded_rectangle(&mut self.scratchpad_points, rect, corner_radius); path.add_line_loop(&self.scratchpad_points); let path_stroke = PathStroke::from(stroke).with_kind(stroke_kind); diff --git a/examples/custom_window_frame/src/main.rs b/examples/custom_window_frame/src/main.rs index aefcdc677..eef0db002 100644 --- a/examples/custom_window_frame/src/main.rs +++ b/examples/custom_window_frame/src/main.rs @@ -47,7 +47,7 @@ fn custom_window_frame(ctx: &egui::Context, title: &str, add_contents: impl FnOn let panel_frame = egui::Frame::new() .fill(ctx.style().visuals.window_fill()) - .rounding(10) + .corner_radius(10) .stroke(ctx.style().visuals.widgets.noninteractive.fg_stroke) .outer_margin(1); // so the stroke is within the bounds diff --git a/examples/images/src/main.rs b/examples/images/src/main.rs index a8373774a..d7ccfd7b4 100644 --- a/examples/images/src/main.rs +++ b/examples/images/src/main.rs @@ -35,7 +35,7 @@ impl eframe::App for MyApp { .on_hover_text_at_pointer("Svg"); let url = "https://picsum.photos/seed/1.759706314/1024"; - ui.add(egui::Image::new(url).rounding(10.0)) + ui.add(egui::Image::new(url).corner_radius(10)) .on_hover_text_at_pointer(url); }); }); diff --git a/tests/test_viewports/src/main.rs b/tests/test_viewports/src/main.rs index 9cdb0b967..3f9964c94 100644 --- a/tests/test_viewports/src/main.rs +++ b/tests/test_viewports/src/main.rs @@ -478,7 +478,13 @@ fn drop_target( ui.painter().set( background_id, - egui::epaint::RectShape::new(rect, style.rounding, fill, stroke, egui::StrokeKind::Inside), + egui::epaint::RectShape::new( + rect, + style.corner_radius, + fill, + stroke, + egui::StrokeKind::Inside, + ), ); egui::InnerResponse::new(ret, response) From b8051cc3014f06c02f826f68db966b4975b67a06 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Tue, 4 Feb 2025 14:01:32 +0100 Subject: [PATCH 055/132] Add `SnapshotResults` struct to egui_kittest (#5672) I got annoyed by all the slightly different variations of "collect snapshot results and unwrap them at the end of test" I've written, so I added a struct to make this nice and simple. One controversial thing: It panics when dropped. I wanted to ensure people cannot forget to unwrap the results at the end, and this was the best thing I could come up with. I don't think this is possible via clippy lint or something like that. * [x] I have followed the instructions in the PR template --- .github/workflows/rust.yml | 4 +- crates/egui/src/painter.rs | 1 - crates/egui_demo_app/tests/test_demo_app.rs | 11 +- .../src/demo/demo_app_windows.rs | 11 +- crates/egui_demo_lib/src/demo/modals.rs | 14 +-- .../egui_demo_lib/src/demo/widget_gallery.rs | 1 + crates/egui_demo_lib/src/rendering_test.rs | 10 +- crates/egui_kittest/src/snapshot.rs | 116 +++++++++++++++++- crates/egui_kittest/tests/regression_tests.rs | 14 +-- crates/egui_kittest/tests/tests.rs | 6 +- 10 files changed, 135 insertions(+), 53 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index f0e3b3a72..eadce83be 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -9,7 +9,7 @@ env: jobs: fmt-crank-check-test: - name: Format + check + test + name: Format + check runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 @@ -223,7 +223,7 @@ jobs: tests: name: Run tests - # We run the tests on macOS because it will run with a actual GPU + # We run the tests on macOS because it will run with an actual GPU runs-on: macos-latest steps: diff --git a/crates/egui/src/painter.rs b/crates/egui/src/painter.rs index aef97cccf..22a0a0a9d 100644 --- a/crates/egui/src/painter.rs +++ b/crates/egui/src/painter.rs @@ -435,7 +435,6 @@ impl Painter { self.add(RectShape::filled(rect, corner_radius, fill_color)) } - /// The stroke extends _outside_ the [`Rect`]. pub fn rect_stroke( &self, rect: Rect, diff --git a/crates/egui_demo_app/tests/test_demo_app.rs b/crates/egui_demo_app/tests/test_demo_app.rs index fc4940fb9..0247b9fc2 100644 --- a/crates/egui_demo_app/tests/test_demo_app.rs +++ b/crates/egui_demo_app/tests/test_demo_app.rs @@ -2,6 +2,7 @@ use egui::accesskit::Role; use egui::Vec2; use egui_demo_app::{Anchor, WrapApp}; use egui_kittest::kittest::Queryable; +use egui_kittest::SnapshotResults; #[test] fn test_demo_app() { @@ -27,7 +28,7 @@ fn test_demo_app() { "Expected to find the Custom3d app.", ); - let mut results = vec![]; + let mut results = SnapshotResults::new(); for (name, anchor) in apps { harness.get_by_role_and_label(Role::Button, name).click(); @@ -68,12 +69,6 @@ fn test_demo_app() { // Can't use Harness::run because fractal clock keeps requesting repaints harness.run_steps(2); - if let Err(e) = harness.try_snapshot(&anchor.to_string()) { - results.push(e); - } - } - - if let Some(error) = results.first() { - panic!("{error}"); + results.add(harness.try_snapshot(&anchor.to_string())); } } 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 27f862ad0..6f753c3a4 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -366,13 +366,13 @@ mod tests { use crate::{demo::demo_app_windows::DemoGroups, Demo}; use egui::Vec2; use egui_kittest::kittest::Queryable; - use egui_kittest::{Harness, SnapshotOptions}; + use egui_kittest::{Harness, SnapshotOptions, SnapshotResults}; #[test] fn demos_should_match_snapshot() { let demos = DemoGroups::default().demos; - let mut errors = Vec::new(); + let mut results = SnapshotResults::new(); for mut demo in demos.demos { // Widget Gallery needs to be customized (to set a specific date) and has its own test @@ -406,12 +406,7 @@ mod tests { options.threshold = 2.1; } - let result = harness.try_snapshot_options(&format!("demos/{name}"), &options); - if let Err(err) = result { - errors.push(err.to_string()); - } + results.add(harness.try_snapshot_options(&format!("demos/{name}"), &options)); } - - assert!(errors.is_empty(), "Errors: {errors:#?}"); } } diff --git a/crates/egui_demo_lib/src/demo/modals.rs b/crates/egui_demo_lib/src/demo/modals.rs index 833e07a28..d344d99c0 100644 --- a/crates/egui_demo_lib/src/demo/modals.rs +++ b/crates/egui_demo_lib/src/demo/modals.rs @@ -165,7 +165,7 @@ mod tests { use egui::accesskit::Role; use egui::Key; use egui_kittest::kittest::Queryable; - use egui_kittest::Harness; + use egui_kittest::{Harness, SnapshotResults}; #[test] fn clicking_escape_when_popup_open_should_not_close_modal() { @@ -233,22 +233,18 @@ mod tests { initial_state, ); - let mut results = Vec::new(); + let mut results = SnapshotResults::new(); harness.run(); - results.push(harness.try_snapshot("modals_1")); + results.add(harness.try_snapshot("modals_1")); harness.get_by_label("Save").click(); harness.run_ok(); - results.push(harness.try_snapshot("modals_2")); + results.add(harness.try_snapshot("modals_2")); harness.get_by_label("Yes Please").click(); harness.run_ok(); - results.push(harness.try_snapshot("modals_3")); - - for result in results { - result.unwrap(); - } + results.add(harness.try_snapshot("modals_3")); } // This tests whether the backdrop actually prevents interaction with lower layers. diff --git a/crates/egui_demo_lib/src/demo/widget_gallery.rs b/crates/egui_demo_lib/src/demo/widget_gallery.rs index c51a9efd8..eae07ccd5 100644 --- a/crates/egui_demo_lib/src/demo/widget_gallery.rs +++ b/crates/egui_demo_lib/src/demo/widget_gallery.rs @@ -23,6 +23,7 @@ pub struct WidgetGallery { #[cfg_attr(feature = "serde", serde(skip))] date: Option, + #[cfg(feature = "chrono")] with_date_button: bool, } diff --git a/crates/egui_demo_lib/src/rendering_test.rs b/crates/egui_demo_lib/src/rendering_test.rs index d43116e0b..e83e643bb 100644 --- a/crates/egui_demo_lib/src/rendering_test.rs +++ b/crates/egui_demo_lib/src/rendering_test.rs @@ -688,10 +688,11 @@ fn mul_color_gamma(left: Color32, right: Color32) -> Color32 { mod tests { use crate::ColorTest; use egui_kittest::kittest::Queryable as _; + use egui_kittest::SnapshotResults; #[test] pub fn rendering_test() { - let mut errors = vec![]; + let mut results = SnapshotResults::new(); for dpi in [1.0, 1.25, 1.5, 1.75, 1.6666667, 2.0] { let mut color_test = ColorTest::default(); let mut harness = egui_kittest::Harness::builder() @@ -708,12 +709,7 @@ mod tests { harness.fit_contents(); - let result = harness.try_snapshot(&format!("rendering_test/dpi_{dpi:.2}")); - if let Err(err) = result { - errors.push(err); - } + results.add(harness.try_snapshot(&format!("rendering_test/dpi_{dpi:.2}"))); } - - assert!(errors.is_empty(), "Errors: {errors:#?}"); } } diff --git a/crates/egui_kittest/src/snapshot.rs b/crates/egui_kittest/src/snapshot.rs index dc49caec3..86ed053d2 100644 --- a/crates/egui_kittest/src/snapshot.rs +++ b/crates/egui_kittest/src/snapshot.rs @@ -4,6 +4,8 @@ use std::fmt::Display; use std::io::ErrorKind; use std::path::PathBuf; +pub type SnapshotResult = Result<(), SnapshotError>; + #[non_exhaustive] pub struct SnapshotOptions { /// The threshold for the image comparison. @@ -189,7 +191,7 @@ pub fn try_image_snapshot_options( new: &image::RgbaImage, name: &str, options: &SnapshotOptions, -) -> Result<(), SnapshotError> { +) -> SnapshotResult { let SnapshotOptions { threshold, output_path, @@ -306,7 +308,7 @@ pub fn try_image_snapshot_options( /// # Errors /// Returns a [`SnapshotError`] if the image does not match the snapshot or if there was an error /// reading or writing the snapshot. -pub fn try_image_snapshot(current: &image::RgbaImage, name: &str) -> Result<(), SnapshotError> { +pub fn try_image_snapshot(current: &image::RgbaImage, name: &str) -> SnapshotResult { try_image_snapshot_options(current, name, &SnapshotOptions::default()) } @@ -378,7 +380,7 @@ impl Harness<'_, State> { &mut self, name: &str, options: &SnapshotOptions, - ) -> Result<(), SnapshotError> { + ) -> SnapshotResult { let image = self .render() .map_err(|err| SnapshotError::RenderError { err })?; @@ -393,7 +395,7 @@ impl Harness<'_, State> { /// # Errors /// Returns a [`SnapshotError`] if the image does not match the snapshot, if there was an /// error reading or writing the snapshot, if the rendering fails or if no default renderer is available. - pub fn try_snapshot(&mut self, name: &str) -> Result<(), SnapshotError> { + pub fn try_snapshot(&mut self, name: &str) -> SnapshotResult { let image = self .render() .map_err(|err| SnapshotError::RenderError { err })?; @@ -460,7 +462,7 @@ impl Harness<'_, State> { &mut self, name: &str, options: &SnapshotOptions, - ) -> Result<(), SnapshotError> { + ) -> SnapshotResult { self.try_snapshot_options(name, options) } @@ -468,7 +470,7 @@ impl Harness<'_, State> { since = "0.31.0", note = "Use `try_snapshot` instead. This function will be removed in 0.32" )] - pub fn try_wgpu_snapshot(&mut self, name: &str) -> Result<(), SnapshotError> { + pub fn try_wgpu_snapshot(&mut self, name: &str) -> SnapshotResult { self.try_snapshot(name) } @@ -488,3 +490,105 @@ impl Harness<'_, State> { self.snapshot(name); } } + +/// Utility to collect snapshot errors and display them at the end of the test. +/// +/// # Example +/// ``` +/// # let harness = MockHarness; +/// # struct MockHarness; +/// # impl MockHarness { +/// # fn try_snapshot(&self, _: &str) -> Result<(), egui_kittest::SnapshotError> { Ok(()) } +/// # } +/// +/// // [...] Construct a Harness +/// +/// let mut results = egui_kittest::SnapshotResults::new(); +/// +/// // Call add for each snapshot in your test +/// results.add(harness.try_snapshot("my_test")); +/// +/// // If there are any errors, SnapshotResults will panic once dropped. +/// ``` +/// +/// # Panics +/// Panics if there are any errors when dropped (this way it is impossible to forget to call `unwrap`). +/// If you don't want to panic, you can use [`SnapshotResults::into_result`] or [`SnapshotResults::into_inner`]. +/// If you want to panic early, you can use [`SnapshotResults::unwrap`]. +#[derive(Debug, Default)] +pub struct SnapshotResults { + errors: Vec, +} + +impl Display for SnapshotResults { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.errors.is_empty() { + write!(f, "All snapshots passed") + } else { + writeln!(f, "Snapshot errors:")?; + for error in &self.errors { + writeln!(f, " {error}")?; + } + Ok(()) + } + } +} + +impl SnapshotResults { + pub fn new() -> Self { + Default::default() + } + + /// Check if the result is an error and add it to the list of errors. + pub fn add(&mut self, result: SnapshotResult) { + if let Err(err) = result { + self.errors.push(err); + } + } + + /// Check if there are any errors. + pub fn has_errors(&self) -> bool { + !self.errors.is_empty() + } + + /// Convert this into a `Result<(), Self>`. + #[allow(clippy::missing_errors_doc)] + pub fn into_result(self) -> Result<(), Self> { + if self.has_errors() { + Err(self) + } else { + Ok(()) + } + } + + pub fn into_inner(mut self) -> Vec { + std::mem::take(&mut self.errors) + } + + /// Panics if there are any errors, displaying each. + #[allow(clippy::unused_self)] + #[track_caller] + pub fn unwrap(self) { + // Panic is handled in drop + } +} + +impl From for Vec { + fn from(results: SnapshotResults) -> Self { + results.into_inner() + } +} + +impl Drop for SnapshotResults { + #[track_caller] + fn drop(&mut self) { + // Don't panic if we are already panicking (the test probably failed for another reason) + if std::thread::panicking() { + return; + } + #[allow(clippy::manual_assert)] + if self.has_errors() { + panic!("{}", self); + } + } +} diff --git a/crates/egui_kittest/tests/regression_tests.rs b/crates/egui_kittest/tests/regression_tests.rs index 8567e10ea..e7186dcac 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}; +use egui_kittest::{kittest::Queryable, Harness, SnapshotResults}; #[test] pub fn focus_should_skip_over_disabled_buttons() { @@ -64,10 +64,10 @@ fn test_combobox() { harness.run(); - let mut results = vec![]; + let mut results = SnapshotResults::new(); #[cfg(all(feature = "wgpu", feature = "snapshot"))] - results.push(harness.try_snapshot("combobox_closed")); + results.add(harness.try_snapshot("combobox_closed")); let combobox = harness.get_by_role_and_label(Role::ComboBox, "Select Something"); combobox.click(); @@ -75,7 +75,7 @@ fn test_combobox() { harness.run(); #[cfg(all(feature = "wgpu", feature = "snapshot"))] - results.push(harness.try_snapshot("combobox_opened")); + 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 @@ -87,10 +87,4 @@ fn test_combobox() { // Popup should be closed now assert!(harness.query_by_label("Item 2").is_none()); - - for result in results { - if let Err(err) = result { - panic!("{}", err); - } - } } diff --git a/crates/egui_kittest/tests/tests.rs b/crates/egui_kittest/tests/tests.rs index bf7d0bdb2..29b4c7b11 100644 --- a/crates/egui_kittest/tests/tests.rs +++ b/crates/egui_kittest/tests/tests.rs @@ -1,4 +1,4 @@ -use egui_kittest::Harness; +use egui_kittest::{Harness, SnapshotResults}; #[test] fn test_shrink() { @@ -10,6 +10,8 @@ fn test_shrink() { harness.fit_contents(); + let mut results = SnapshotResults::new(); + #[cfg(all(feature = "snapshot", feature = "wgpu"))] - harness.snapshot("test_shrink"); + results.add(harness.try_snapshot("test_shrink")); } From c90b97f4ef822d555c8574b1c58aeaa273c0afd2 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 4 Feb 2025 14:36:29 +0100 Subject: [PATCH 056/132] Fix sharp corners for rectangles with StrokeKind != Inside (#5675) Oops! --- .../src/demo/tests/tessellation_test.rs | 13 +++++++++++++ .../tests/snapshots/rendering_test/dpi_1.00.png | 4 ++-- .../tests/snapshots/rendering_test/dpi_1.25.png | 4 ++-- .../tests/snapshots/rendering_test/dpi_1.50.png | 4 ++-- .../tests/snapshots/rendering_test/dpi_1.67.png | 4 ++-- .../tests/snapshots/rendering_test/dpi_1.75.png | 4 ++-- .../tests/snapshots/rendering_test/dpi_2.00.png | 4 ++-- .../tessellation_test/Additive rectangle.png | 3 +++ crates/epaint/src/tessellator.rs | 16 ++++++++++++---- 9 files changed, 40 insertions(+), 16 deletions(-) create mode 100644 crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png 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 8c5a1adc3..58f53560d 100644 --- a/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs +++ b/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs @@ -93,6 +93,19 @@ impl TessellationTest { ) .with_blur_width(5.0), ), + ( + "Additive rectangle", + RectShape::new( + sized([24.0, 12.0]), + 0.0, + egui::Color32::LIGHT_RED.additive().linear_multiply(0.025), + ( + 1.0, + egui::Color32::LIGHT_BLUE.additive().linear_multiply(0.1), + ), + StrokeKind::Outside, + ), + ), ]; for (_name, shape) in &mut shapes { diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png index b0f087acd..c6d381558 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9c6ce16a8a8c34d882485da6bbe08039fb55f90636da8136f68b1bb9baf0effb -size 557610 +oid sha256:c03f90098fb2b2ff846586cc608126580050303b48e7c918b135efcdc6d52686 +size 554947 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png index efa1c8d5b..a6f969040 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a49c052578a46adb41bc02c6d7fdc264ed0ab8ae636cc8a11ac729fe1e48091b -size 791802 +oid sha256:6ee96b64140d91e1b98cbf5dfef9ac5654050c48b3f9d36889f83a62c9ec985a +size 788322 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png index 18b0233d3..a9055f50c 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:989c55a83b8bc7cce4f459d8b835962377927a98f2bce085e92cba8438438ecd -size 943736 +oid sha256:c82611e7b3ec5c7b82e552421aeef6ad5562d649339708ab6283e0c5af3589c6 +size 939339 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png index b3a865457..0cd4d8d88 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:01ac61dc5bcecf6bf0d13c8399460b1afae652efe6fecc1d0e4b2f27d9f1c5a4 -size 1046906 +oid sha256:3b315e64464b060c4ef2b508bcb223663e0560871b82c9513fe232f92bd33609 +size 1041890 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png index 1b44fad93..5562fed0b 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3348923ecbad34e385e3ed52ab9b7c88b5d4fc07de00620302d5b191d90a453f -size 1140236 +oid sha256:1dc4d7c51eb301ab3e0679b0247c96997ffa30b763c7b203a91fff706264d929 +size 1134943 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png index f1f12eb1f..0e531d131 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:179c17b0405c6e87bd3cafaa7272e3e6d6eefd462b4406cca2a7abfe8af6f2bd -size 1317569 +oid sha256:92c0615b09916c9a70fc898fbbde2921363044a3bcfcf82410e1d3f6e466bb8a +size 1311678 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png new file mode 100644 index 000000000..dbd8f31d3 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70fd682bf6ba07b235cf2db643e9d050522aeb3e6ef28426c2b3d23c09920d22 +size 46196 diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 608317dc6..0e2dd4c8e 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1885,19 +1885,27 @@ impl Tessellator { let extra_cr_tweak = 0.4; // Otherwise is doesn't _feels_ enough. - if 0.0 < original_cr.nw { + if original_cr.nw == 0.0 { + corner_radius.nw = 0.0; + } else { corner_radius.nw += extra_cr_tweak; corner_radius.nw = corner_radius.nw.at_least(min_outside_cr); } - if 0.0 < original_cr.ne { + if original_cr.ne == 0.0 { + corner_radius.ne = 0.0; + } else { corner_radius.ne += extra_cr_tweak; corner_radius.ne = corner_radius.ne.at_least(min_outside_cr); } - if 0.0 < original_cr.sw { + if original_cr.sw == 0.0 { + corner_radius.sw = 0.0; + } else { corner_radius.sw += extra_cr_tweak; corner_radius.sw = corner_radius.sw.at_least(min_outside_cr); } - if 0.0 < original_cr.se { + if original_cr.se == 0.0 { + corner_radius.se = 0.0; + } else { corner_radius.se += extra_cr_tweak; corner_radius.se = corner_radius.se.at_least(min_outside_cr); } From c6bda9a38c08c8e3e21e2062a2ede5b59d1c6a20 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 4 Feb 2025 15:31:51 +0100 Subject: [PATCH 057/132] Make the ends of vline/hline sharper (#5676) TL;DR: line caps are annoying in two ways: A) we only add them for lines wider than a pixel B) they always make the line longer (if added) --- .../egui_demo_app/tests/snapshots/clock.png | 4 +- .../tests/snapshots/easymarkeditor.png | 4 +- .../tests/snapshots/demos/Bézier Curve.png | 4 +- .../tests/snapshots/demos/Code Editor.png | 4 +- .../tests/snapshots/demos/Code Example.png | 4 +- .../tests/snapshots/demos/Context Menus.png | 4 +- .../tests/snapshots/demos/Dancing Strings.png | 4 +- .../tests/snapshots/demos/Drag and Drop.png | 4 +- .../tests/snapshots/demos/Extra Viewport.png | 4 +- .../tests/snapshots/demos/Font Book.png | 4 +- .../tests/snapshots/demos/Frame.png | 4 +- .../tests/snapshots/demos/Highlighting.png | 4 +- .../snapshots/demos/Interactive Container.png | 4 +- .../tests/snapshots/demos/Misc Demos.png | 4 +- .../tests/snapshots/demos/Modals.png | 4 +- .../tests/snapshots/demos/Multi Touch.png | 4 +- .../tests/snapshots/demos/Painting.png | 4 +- .../tests/snapshots/demos/Panels.png | 4 +- .../tests/snapshots/demos/Scene.png | 4 +- .../tests/snapshots/demos/Screenshot.png | 4 +- .../tests/snapshots/demos/Scrolling.png | 4 +- .../tests/snapshots/demos/Sliders.png | 4 +- .../tests/snapshots/demos/Strip.png | 4 +- .../tests/snapshots/demos/Table.png | 4 +- .../tests/snapshots/demos/Text Layout.png | 4 +- .../tests/snapshots/demos/TextEdit.png | 4 +- .../tests/snapshots/demos/Tooltips.png | 4 +- .../tests/snapshots/demos/Undo Redo.png | 4 +- .../tests/snapshots/demos/Window Options.png | 4 +- .../tests/snapshots/modals_1.png | 4 +- .../tests/snapshots/modals_2.png | 4 +- .../tests/snapshots/modals_3.png | 4 +- ...rop_should_prevent_focusing_lower_area.png | 4 +- .../snapshots/rendering_test/dpi_1.00.png | 4 +- .../snapshots/rendering_test/dpi_1.25.png | 4 +- .../snapshots/rendering_test/dpi_1.50.png | 4 +- .../snapshots/rendering_test/dpi_1.67.png | 4 +- .../snapshots/rendering_test/dpi_1.75.png | 4 +- .../snapshots/rendering_test/dpi_2.00.png | 4 +- .../tessellation_test/Additive rectangle.png | 4 +- .../tessellation_test/Blurred stroke.png | 4 +- .../snapshots/tessellation_test/Blurred.png | 4 +- .../tessellation_test/Minimal rounding.png | 4 +- .../snapshots/tessellation_test/Normal.png | 4 +- .../Thick stroke, minimal rounding.png | 4 +- .../tessellation_test/Thin filled.png | 4 +- .../tessellation_test/Thin stroked.png | 4 +- .../tests/snapshots/test_shrink.png | 4 +- crates/epaint/src/tessellator.rs | 40 ++++++++++++++++++- 49 files changed, 135 insertions(+), 97 deletions(-) diff --git a/crates/egui_demo_app/tests/snapshots/clock.png b/crates/egui_demo_app/tests/snapshots/clock.png index d97230494..80fef31cc 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:4aeab31841dd95b5e0f4bd0af0c0ba49a862d50836dbafdf2172fbbab950c105 -size 327741 +oid sha256:2c15a74a1b1ed3b52a53966a3df2901ca520b92fbfbd10503e32ddb8431e1467 +size 335399 diff --git a/crates/egui_demo_app/tests/snapshots/easymarkeditor.png b/crates/egui_demo_app/tests/snapshots/easymarkeditor.png index 23e1c59b0..014264330 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:7bcf6e2977bed682d7bdaa0b6a6786e528662dd0791d2e6f83cf1b4852035838 -size 182833 +oid sha256:d8f1046ee5d50d73a17009fd1f11f056b5828fedc62908d00730a6aa77125473 +size 182900 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Bézier Curve.png b/crates/egui_demo_lib/tests/snapshots/demos/Bézier Curve.png index 16adbcdcf..63b57f0df 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Bézier Curve.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Bézier Curve.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:23f19871720b67659a7b56cee8a78edc941c4bac86f55efc6fa549f10c4712fb -size 31754 +oid sha256:73b24bec3383450b627683370467b68f0195bd43ce205a44dd49b60cca187b1b +size 31773 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png b/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png index 10593d4e0..bf53d1b11 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e4d8fee9fd8e69ecd60ebd0dd41c29b61cc13e7013b1d20ad93d40fc4ed1cc03 -size 27091 +oid sha256:062b6e6b6bd7167df24545308afbc3e38ce328fff0b76ca9921180010766f827 +size 27153 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png b/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png index 88cd2ffa3..4131e24f8 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b291e7efd895ab095590285b841903f05dc7d4dadbab7d9001b04a92953c1694 -size 81677 +oid sha256:d6287aa1fd30baee49bd5fdf3fb307f49c2ecdcb5e6a83c309ec4c051d7506a4 +size 81718 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Context Menus.png b/crates/egui_demo_lib/tests/snapshots/demos/Context Menus.png index 2b46eaf44..877ce849a 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Context Menus.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Context Menus.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5d75230689807ae7fb692bac5c9b33ee04c02b9e54963e2d6ada05860157daf6 -size 11705 +oid sha256:5dfdfe8ed7edf0074b9a4f4dd0f9c27f4c9d7411398c6dba590f032a342ad0e4 +size 11716 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png b/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png index ba02ae257..7938ab998 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f512159108c7681834ae2a52c5e1d4eb4dbe678a509f3dab3384e35d6c8dbee2 -size 25865 +oid sha256:16fdf761b370962678b938735d41221b70658d5b003794c71f73327bfd7d2017 +size 25907 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png b/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png index 7b47f16ff..d8922e3bf 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f2a4f5e67ff6615877794304e8be5a5db647d48e20e4248259179cddefbf4088 -size 20806 +oid sha256:f789cc43f83f01b014b07e93e33ed85ecc0ef323fec404f602ca44ba6fc0ce9e +size 20819 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Extra Viewport.png b/crates/egui_demo_lib/tests/snapshots/demos/Extra Viewport.png index 055f1651f..5eccebf88 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Extra Viewport.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Extra Viewport.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0bc474a37d6d3a7a08dd41963bf9009c05315de91bb515cc2b19443b79480bff -size 10723 +oid sha256:baca02791fa90a063dff7f365ce7a0a748f7b892a920cddebd9d2593a8231786 +size 10769 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png b/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png index 647e3824f..59efbb6c1 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0bff769b3f9e46c5e38170885b2c8301294cbcc1c8ee22c17672205edd509924 -size 133170 +oid sha256:23ddfd103996b7a6d71ada175b714a3d1813ef6f521fea307b5d9cb2908cb638 +size 133212 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Frame.png b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png index 547c65bee..ac6deb6b8 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Frame.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:244d539111e994a4ed2aa95a2b4d0ff12b948e21843c8a1dddcf54cbb388f1aa -size 24280 +oid sha256:0be2bf06ea69579fe72d03abd0c5cc17fefcac4f403f5e6c6343b8b275080f29 +size 24324 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png b/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png index 3c1cf6de8..6bf98330c 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fdbdf1159f0ab4579bf9ceefbd8e40ac7af468368ab11ffec8578214b57d867c -size 17758 +oid sha256:094db03eeea90a1e901c581b19c6ab733f50a396eb9927d82dd558c719404524 +size 17805 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png b/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png index a3ef616d9..bb337964b 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce647fd9626f126e7f19b4494bb98a2086071f9c45f7dd6ba4708632e7433a7b -size 22418 +oid sha256:234ae929c7f8b3f623b7e09690f8069d98ba72052c08fc58c85fd9a3dfee04a1 +size 22430 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 882914ff1..b39b7110a 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:a843e6772809a1c904968fac19b49973675e726a3b7727ca1b898ad3b9072b0c -size 64257 +oid sha256:e8251378ba4c74fd29a6aff82517ec6b4c322c5589f85398c6d5e757986a3d83 +size 64300 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Modals.png b/crates/egui_demo_lib/tests/snapshots/demos/Modals.png index bf3a487df..2fccd9242 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Modals.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Modals.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b437f27c46ddf82e4268d9bb86d33f43c382d1ab3ed45297bc136600cdc9960c -size 32493 +oid sha256:8679ac7ec6e90cfe9164b66a02ce3c9db403df0f20f45211c73b1d9a950d2718 +size 32522 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png b/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png index 7ef97c87b..6a4f65e64 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2cd0bda8b4d3d7f833273097c8bb52cfd8ea63a405d6798b9aeb7002b143ac74 -size 36459 +oid sha256:05987e9dfd291b0db57d7611c1feb6742198c14546b779975366643d64ef9ea5 +size 36539 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Painting.png b/crates/egui_demo_lib/tests/snapshots/demos/Painting.png index 5e5d369a1..cab418b5b 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Painting.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Painting.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a57c3bf373a283b79188080e2ddf7407c2974655fc5ad59222442e14faae055c -size 17508 +oid sha256:1107f4ab1411e054ecefe19e0de6a7276ec2ab81c2c9221056860a3d62263ebf +size 17596 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Panels.png b/crates/egui_demo_lib/tests/snapshots/demos/Panels.png index 3e751d554..72bb68bbf 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Panels.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Panels.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:161b5853f528206f2531587753369f2f6f3c52203af668eba0d81a41c2a915e0 -size 264432 +oid sha256:8ca983bab555739e9a2eefb518a8009e1de9638c70efb50edb407cc26973d1eb +size 264478 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scene.png b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png index d51bbd358..42e3f7625 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:118413a5ec56c6589914c5cb59bda2884a4d6acec3b34bbc367bc893c3de8127 -size 35409 +oid sha256:0728689de055812e85ccb15e5f4175dd7f9096e610d4ac04256367afa4e036a5 +size 35477 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png b/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png index 8cf0ed424..523014112 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:361791f30739f841c46433f5d16d281ba9d0c52027b4cf156c3ac293aa795f46 -size 23592 +oid sha256:ffc6e6011ab774613f1a937dace27129bf9c5e5ee10a3a3cfa4973d82b8e5429 +size 23604 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png b/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png index 16b5868cc..2d9d0a5ba 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:81338d7b0412989590bc0808419d93788e972e7ab6f70f481f86bd23263a5395 -size 183821 +oid sha256:d84c161d7fd0b1d5670b79e360d6737a3c004998c94835592180a12c055959de +size 183870 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png b/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png index 38fe97fed..eec4c34fe 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:41c51350c09360738ca284b05c944a11164e4a614beb95ce4cc962222af33d87 -size 117764 +oid sha256:32a9eba39d88023484abf29ab0bd7a29bfc2ba6b1f7ca42496f87a854616f424 +size 117783 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Strip.png b/crates/egui_demo_lib/tests/snapshots/demos/Strip.png index 3d586971f..01e6abdd6 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Strip.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Strip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:794475478cfe2fc954731742165b1f0419a0e64616e72b3766c1e031dbba7ca1 -size 26092 +oid sha256:e91609711f26a067de0e9bfd5f6f1680339e8137ab1a8325290ce399857d2797 +size 26135 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Table.png b/crates/egui_demo_lib/tests/snapshots/demos/Table.png index 2658a2535..eecf9aa0f 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:7073aa30f3e7dfd116d9a4f02f6c5a075ce068d2e6f43eaede3c4dd6c56f925e -size 70439 +oid sha256:b5171572d2bfee9f44cb8fc6aeba2ff3035823035d27ec42480e4bd49e6f5e63 +size 70524 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png b/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png index 48cad20b9..8eae9dc33 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4bb3cd05f253c0e109b83a0556b975af7a96d57678e57de3b9fa130cc8a8de1c -size 66318 +oid sha256:f7b21826be83c1a16b9fa7b19130f40261a9735c3eb450eaf9f8d5a3bbfccb7a +size 66365 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png b/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png index 6a5551c45..181223e48 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b550769d5ec5d834f89fba56375d10e5f9b3ba703599d90a5b6607aff1c2b06 -size 21194 +oid sha256:7f7986da3f4a3b94e701bf94066896aedb41458f12f6cdaf93a4fd371b1705da +size 21206 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png b/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png index ee474166b..af9ee2651 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:45f343b55be98976de32bd3c5438212a46b787123ea3096cf8fc6bec99493184 -size 59699 +oid sha256:8fb28c16dc2e4359ada002a5fb98e955db606f44db4bdd1a400ab2e4c43c850e +size 59701 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png b/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png index 5e1cb1b35..9f2d64d17 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8d25f774320ca844fa4dfba5215ed66f067d0f30c6d7c8ad7ec29d97ee7732e0 -size 13073 +oid sha256:2a8a3e5953a5ee41d95f2a23b580bb9c82e869b7bad0f2611ed866c55bc12b07 +size 13102 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png b/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png index 0cd7c200a..2128b9d05 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:540a05c5b1de7e362bcb48a04611323fbc65c8787b8ae852ada8aa145803753d -size 34968 +oid sha256:12fdbe6805dbce0d993236f30ac14d9c03411b6918399ec5bfa7aa39104d0640 +size 35182 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_1.png b/crates/egui_demo_lib/tests/snapshots/modals_1.png index 1c0bdc0d2..177e90cc3 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_1.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2276b8221389da4f644cad43ce446cd7d28f41820cea36fa3ea860808710053d -size 47878 +oid sha256:3d270118b982413bd0c05e9f20a3ee53b88a1681e273f73a81d1733b05c7a006 +size 47889 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_2.png b/crates/egui_demo_lib/tests/snapshots/modals_2.png index 943b4a38a..7e5d7ff82 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_2.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:054d606681e08bf61762e5c7d7596c4f483e66ac889795e4be4956103328e0bb -size 47862 +oid sha256:9f4ad4ac06f2113533943fc00ed9747c237c9b7b6f0890ccf9857e606290c7ec +size 47871 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_3.png b/crates/egui_demo_lib/tests/snapshots/modals_3.png index bc6071dbb..12c338393 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_3.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7fb29285e53b619f333757052d05f86d973680ac1ce6d1b25d28b7824bbce51 -size 43725 +oid sha256:5fee7a7f4ebcda3fd55d1d31e21b46246c4d5f166fb1f0de04265e7db424d9c8 +size 43733 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png b/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png index 9a52f0498..5d0da39d8 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be50a396838d4fe29bcfd0807377ad0cda538fea8569acf709da0b13505bf09b -size 43871 +oid sha256:510a3b5d8b10013eed234977bf46575a39cc951611a073154652ce6bf2bc3ecc +size 43880 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png index c6d381558..e1b600fe5 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c03f90098fb2b2ff846586cc608126580050303b48e7c918b135efcdc6d52686 -size 554947 +oid sha256:03ee62427611101758958adf2650a4a0eea4e023f07c9ec4ebc63425233e8a04 +size 554949 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png index a6f969040..ab9c8b818 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6ee96b64140d91e1b98cbf5dfef9ac5654050c48b3f9d36889f83a62c9ec985a -size 788322 +oid sha256:82ef265f0e22649c7fcdb9556879c1a30df582bd4e97c647258b3e5acc03d112 +size 771298 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png index a9055f50c..b2d8c3113 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c82611e7b3ec5c7b82e552421aeef6ad5562d649339708ab6283e0c5af3589c6 -size 939339 +oid sha256:cad71b486a479eb9c5339a93f4acc3df2d0b6b188ad023b9b044be7311b0ab72 +size 918775 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png index 0cd4d8d88..2056c3fa0 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3b315e64464b060c4ef2b508bcb223663e0560871b82c9513fe232f92bd33609 -size 1041890 +oid sha256:dc9ed4d29f4227b9d38b477ee8f546ea8597acda56a6909ba4826891ebdbea01 +size 1039263 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png index 5562fed0b..586916e56 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1dc4d7c51eb301ab3e0679b0247c96997ffa30b763c7b203a91fff706264d929 -size 1134943 +oid sha256:e9bf826bee811d8af345ec1281266fc9bef6d7c3782279516984a6c75130a929 +size 1130895 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png index 0e531d131..b764e7bee 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:92c0615b09916c9a70fc898fbbde2921363044a3bcfcf82410e1d3f6e466bb8a -size 1311678 +oid sha256:9345de28f09e2891fd01db20bb0b94176ec3c89d8c2f344a6640d33e97ab5400 +size 1311417 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png index dbd8f31d3..979991d13 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:70fd682bf6ba07b235cf2db643e9d050522aeb3e6ef28426c2b3d23c09920d22 -size 46196 +oid sha256:5524138c3cb98aa71ef67083ad2d01813ab2394f93f9a7897f2e465ef5a1d0bc +size 46270 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png index 8e97cb8e3..249b5db4d 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:daa5ec4ddd2f983c4b9f2b0a73c973f58abcb186fbd0d68a9fd0ce7173e5d4e7 -size 88031 +oid sha256:bdf06c41b69eef1eadc8b46020e6e2a7b985a54e1cf75646ca47caaaea525b95 +size 88092 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png index da1f47816..c81deb054 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c09af9c7f4297e2d5b2305366ed57b3203807ca2426314acdf836e25f154d8eb -size 120244 +oid sha256:c5ca9c97cef8242ee6ff73d571479be12a8d4e9b3508b3eb6cdf93abda62f4e6 +size 120314 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png index 7704e4ddf..2a29cd732 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f621bcf7c8fd18156bef2428ab3b9994e6a4d475ae589eb734d50f9a4f3383bd -size 52101 +oid sha256:f0481c97c34693b32575d96b1d4bc1238cbb0eb75a934661072f1b52ffee71cf +size 52171 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png index 0fcb1aeb4..b7d315e47 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f1476e105a3e9c1ff7b2f4a82481462795e4708e3fcf6d495a042faae537184e -size 55298 +oid sha256:883fdf81e51bfe6333ddcad7998458db251f9cf513c9433179061d7d086eebe0 +size 55367 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png index 4df96a3ec..2fe78dd62 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:695a731d9e302db2c5b7f4a0ef44794cf55eb0be093c070b8ffaeb28121569bc -size 55888 +oid sha256:4294949669042e009ac6825ea599dc96e33cdde25e21174b01e3ef108ad478d5 +size 55944 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png index c45845c66..603442ac0 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:854c12c69b31c0c82a9596d167772e00d7a051600e4151535e2cec04491e57a6 -size 37139 +oid sha256:39e5d196ddcaa213b30b0655fe29881a1551c3036c2262f84af8960f66365300 +size 37207 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png index 9cf019ea1..266c77826 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1aacb27847c2e942d56d60e4b72797d93f7265a6effe4e47437e1314e6477e6f -size 37184 +oid sha256:f9cf9d7f1921bfc0d61a2ae31e69a98d28280e4699823de5e732cdb102aee5ac +size 37253 diff --git a/crates/egui_kittest/tests/snapshots/test_shrink.png b/crates/egui_kittest/tests/snapshots/test_shrink.png index 10967a3d5..a6e6b1f3a 100644 --- a/crates/egui_kittest/tests/snapshots/test_shrink.png +++ b/crates/egui_kittest/tests/snapshots/test_shrink.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7008bdb595a19782c4f724bed363e51bd93121f5211186aa0e8014c8ba1007c2 -size 3005 +oid sha256:b5aa7db1bb52481607069ee4a81209ece81b0c70801969b33bc2d1b2f7087de7 +size 2911 diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 0e2dd4c8e..e8f8ad52c 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1002,7 +1002,11 @@ fn stroke_and_fill_path( let color_outer = Color32::TRANSPARENT; let color_middle = &stroke.color; - let thin_line = stroke.width <= feathering; + // We add a bit of an epsilon here, because when we round to pixels, + // we can get rounding errors (unless pixels_per_point is an integer). + // And it's better to err on the side of the nicer rendering with line caps + // (the thin-line optimization has no line caps). + let thin_line = stroke.width <= 0.9 * feathering; if thin_line { // If the stroke is painted smaller than the pixel width (=feathering width), // then we risk severe aliasing. @@ -1017,6 +1021,8 @@ fn stroke_and_fill_path( } } + // TODO(emilk): add line caps (if this is an open line). + let opacity = stroke.width / feathering; /* @@ -1129,6 +1135,10 @@ fn stroke_and_fill_path( // (in the future it would be great with an option to add a circular end instead) + // TODO(emilk): we should probably shrink before adding the line caps, + // so that we don't add to the area of the line. + // TODO(emilk): make line caps optional. + out.reserve_triangles(6 * n as usize + 4); out.reserve_vertices(4 * n as usize); @@ -1637,6 +1647,11 @@ impl Tessellator { } if self.options.round_line_segments_to_pixels { + let feathering = self.feathering; + let pixels_per_point = self.pixels_per_point; + + let quarter_pixel = 0.25 * feathering; // Used to avoid fence post problem. + let [a, b] = &mut points; if a.x == b.x { // Vertical line @@ -1644,6 +1659,20 @@ impl Tessellator { round_line_segment(&mut x, &stroke, self.pixels_per_point); a.x = x; b.x = x; + + // Often the ends of the line are exactly on a pixel boundary, + // but we extend line segments with a cap that is a pixel wide… + // Solution: first shrink the line segment (on each end), + // then round to pixel center! + // We shrink by half-a-pixel n total (a quarter on each end), + // so that on average we avoid the fence-post-problem after rounding. + if a.y < b.y { + a.y = (a.y + quarter_pixel).round_to_pixel_center(pixels_per_point); + b.y = (b.y - quarter_pixel).round_to_pixel_center(pixels_per_point); + } else { + a.y = (a.y - quarter_pixel).round_to_pixel_center(pixels_per_point); + b.y = (b.y + quarter_pixel).round_to_pixel_center(pixels_per_point); + } } if a.y == b.y { // Horizontal line @@ -1651,6 +1680,15 @@ impl Tessellator { round_line_segment(&mut y, &stroke, self.pixels_per_point); a.y = y; b.y = y; + + // See earlier comment for vertical lines + if a.x < b.x { + a.x = (a.x + quarter_pixel).round_to_pixel_center(pixels_per_point); + b.x = (b.x - quarter_pixel).round_to_pixel_center(pixels_per_point); + } else { + a.x = (a.x - quarter_pixel).round_to_pixel_center(pixels_per_point); + b.x = (b.x + quarter_pixel).round_to_pixel_center(pixels_per_point); + } } } From 7ec1fbf467d39fbe269c08058137e5f2de0159d7 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 4 Feb 2025 16:05:36 +0100 Subject: [PATCH 058/132] Fix minor glitch in rendering of selected windows (#5678) --- crates/egui/src/containers/window.rs | 4 +--- crates/egui_demo_lib/tests/snapshots/demos/Bézier Curve.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Code Example.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Context Menus.png | 4 ++-- .../egui_demo_lib/tests/snapshots/demos/Dancing Strings.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Extra Viewport.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Font Book.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Frame.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png | 4 ++-- .../tests/snapshots/demos/Interactive Container.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Misc Demos.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Modals.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Painting.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Panels.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Scene.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Sliders.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Strip.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Table.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Window Options.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/modals_1.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/modals_2.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/modals_3.png | 4 ++-- .../modals_backdrop_should_prevent_focusing_lower_area.png | 4 ++-- 32 files changed, 63 insertions(+), 65 deletions(-) diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index 7582bd701..6075377ff 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -607,8 +607,6 @@ impl Window<'_> { title_bar.inner_rect = outer_rect.shrink(window_frame.stroke.width); title_bar.inner_rect.max.y = title_bar.inner_rect.min.y + title_bar_height_with_margin; - title_bar.inner_rect = - title_bar.inner_rect.round_to_pixels(ctx.pixels_per_point()); if on_top && area_content_ui.visuals().window_highlight_topmost { let mut round = @@ -1211,7 +1209,7 @@ impl TitleBar { .center(); let button_size = Vec2::splat(ui.spacing().icon_width); let button_rect = Rect::from_center_size(button_center, button_size); - let button_rect = button_rect.round_to_pixels(ui.pixels_per_point()); + let button_rect = button_rect.round_ui(); ui.allocate_new_ui(UiBuilder::new().max_rect(button_rect), |ui| { collapsing.show_default_button_with_size(ui, button_size); diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Bézier Curve.png b/crates/egui_demo_lib/tests/snapshots/demos/Bézier Curve.png index 63b57f0df..190f7055c 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Bézier Curve.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Bézier Curve.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:73b24bec3383450b627683370467b68f0195bd43ce205a44dd49b60cca187b1b -size 31773 +oid sha256:536fa3adb51f69fac91396b50e26b3b18e0aa8ff245e4a187087b02240839a90 +size 31780 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png b/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png index bf53d1b11..8574b6d22 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:062b6e6b6bd7167df24545308afbc3e38ce328fff0b76ca9921180010766f827 -size 27153 +oid sha256:c129436a0b1dbfae999adfe0dcc6f5c4e0683c4e9b9a1e52f4b7bbb85ce3a462 +size 27162 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png b/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png index 4131e24f8..ed731869b 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d6287aa1fd30baee49bd5fdf3fb307f49c2ecdcb5e6a83c309ec4c051d7506a4 -size 81718 +oid sha256:322a50c522ba4ac67206332e1d251e121c8c3d5538ca7961880623b20f4933e5 +size 81732 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Context Menus.png b/crates/egui_demo_lib/tests/snapshots/demos/Context Menus.png index 877ce849a..ea25033bf 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Context Menus.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Context Menus.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5dfdfe8ed7edf0074b9a4f4dd0f9c27f4c9d7411398c6dba590f032a342ad0e4 -size 11716 +oid sha256:d4cc8e0919fed5bd1ef981658626dba728435ab95da8ee96ced1fb4838d535ff +size 11741 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png b/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png index 7938ab998..d216ff1fe 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:16fdf761b370962678b938735d41221b70658d5b003794c71f73327bfd7d2017 -size 25907 +oid sha256:d6ba28dacacf5b6f67746fb5187b601e222fd6baf190af2248fdc98909fc17fd +size 25921 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png b/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png index d8922e3bf..097415b60 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f789cc43f83f01b014b07e93e33ed85ecc0ef323fec404f602ca44ba6fc0ce9e -size 20819 +oid sha256:8196e08717f16c5ad17d0f84a4e57e63bb5a51c8f2b171071bf983af18ec161d +size 20834 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Extra Viewport.png b/crates/egui_demo_lib/tests/snapshots/demos/Extra Viewport.png index 5eccebf88..cc8c50e54 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Extra Viewport.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Extra Viewport.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:baca02791fa90a063dff7f365ce7a0a748f7b892a920cddebd9d2593a8231786 -size 10769 +oid sha256:df029c69651ee452cc4b265828280e47ffbcafb2958d71d67a5fe38f5211afe7 +size 10788 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png b/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png index 59efbb6c1..230c7151e 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:23ddfd103996b7a6d71ada175b714a3d1813ef6f521fea307b5d9cb2908cb638 -size 133212 +oid sha256:0a62d309912501be8a5de7af4f1039a2a5731b1ed76fad17527f5783a5375f42 +size 133230 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Frame.png b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png index ac6deb6b8..bfba77f18 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Frame.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0be2bf06ea69579fe72d03abd0c5cc17fefcac4f403f5e6c6343b8b275080f29 -size 24324 +oid sha256:9870334dd6091fa684b78f487ad9a1bb39e6e8d97f987eb74a55de2d7b764f70 +size 24345 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png b/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png index 6bf98330c..7b14e8870 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:094db03eeea90a1e901c581b19c6ab733f50a396eb9927d82dd558c719404524 -size 17805 +oid sha256:bae5f410ed30ef4dba6f3b529ae20e34a26f6c15c4cafd197899cf876271f5f1 +size 17828 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png b/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png index bb337964b..d118733ba 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:234ae929c7f8b3f623b7e09690f8069d98ba72052c08fc58c85fd9a3dfee04a1 -size 22430 +oid sha256:68ded8dccceb3da2764243f2a554c2b4cf825fca09008d60dd520c7fbb2c5d3e +size 22445 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 b39b7110a..8aff2977f 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:e8251378ba4c74fd29a6aff82517ec6b4c322c5589f85398c6d5e757986a3d83 -size 64300 +oid sha256:d55baa6e3d4af44a35ec847639c35f968b05ad907352c45b3eb09cce6cd24280 +size 64357 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Modals.png b/crates/egui_demo_lib/tests/snapshots/demos/Modals.png index 2fccd9242..7c20eab75 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Modals.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Modals.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8679ac7ec6e90cfe9164b66a02ce3c9db403df0f20f45211c73b1d9a950d2718 -size 32522 +oid sha256:98f7210fa72bdb00364e3576aefca126a6f31eff52870d116ba74c167354b13b +size 32533 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png b/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png index 6a4f65e64..bd25e38a8 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:05987e9dfd291b0db57d7611c1feb6742198c14546b779975366643d64ef9ea5 -size 36539 +oid sha256:7868d662bd61d490dce9c049fca6c6e6b978255664fa709e959891bb40a7d434 +size 36577 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Painting.png b/crates/egui_demo_lib/tests/snapshots/demos/Painting.png index cab418b5b..749ecd471 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Painting.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Painting.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1107f4ab1411e054ecefe19e0de6a7276ec2ab81c2c9221056860a3d62263ebf -size 17596 +oid sha256:fb3d031b8f658a90cf98e7a7bc5e0d7a3b601d742e2a9469cd115e7466e06524 +size 17628 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Panels.png b/crates/egui_demo_lib/tests/snapshots/demos/Panels.png index 72bb68bbf..a4527a73b 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Panels.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Panels.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ca983bab555739e9a2eefb518a8009e1de9638c70efb50edb407cc26973d1eb -size 264478 +oid sha256:7ace9a6626446f8e29ec4c3f688e60cbeb86e79ad962044858aabe33a9c3d0e9 +size 264538 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scene.png b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png index 42e3f7625..e79968364 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:0728689de055812e85ccb15e5f4175dd7f9096e610d4ac04256367afa4e036a5 -size 35477 +oid sha256:7d412700c156c641f0184a239198f33bd2427a1ea998a3ee07160cf0f837df94 +size 35451 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png b/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png index 523014112..803a39f99 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ffc6e6011ab774613f1a937dace27129bf9c5e5ee10a3a3cfa4973d82b8e5429 -size 23604 +oid sha256:89efd018caac097a5f9be37dcae15fc60b1475c72fc913ec9940540344e0b09f +size 23622 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png b/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png index 2d9d0a5ba..2e2af10c9 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:d84c161d7fd0b1d5670b79e360d6737a3c004998c94835592180a12c055959de -size 183870 +oid sha256:22c89f7b9b84563d6ee7db0d9a66f6b95c9034261fdffca53ae9737d70d2b376 +size 183881 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png b/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png index eec4c34fe..e47f11082 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:32a9eba39d88023484abf29ab0bd7a29bfc2ba6b1f7ca42496f87a854616f424 -size 117783 +oid sha256:316c172a936f215afdcc45e7f5b32400e6acd759551adb2cc741f7121b9d83eb +size 117790 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Strip.png b/crates/egui_demo_lib/tests/snapshots/demos/Strip.png index 01e6abdd6..ea9a21881 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Strip.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Strip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e91609711f26a067de0e9bfd5f6f1680339e8137ab1a8325290ce399857d2797 -size 26135 +oid sha256:88913690a2b225ca634e38406a6a852250019a19d9bb33a4242e77c10fe88422 +size 26142 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Table.png b/crates/egui_demo_lib/tests/snapshots/demos/Table.png index eecf9aa0f..3012f2f36 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:b5171572d2bfee9f44cb8fc6aeba2ff3035823035d27ec42480e4bd49e6f5e63 -size 70524 +oid sha256:43b8dae4a936bf56b92368fcef64ff2ce2518aabc534a77fa578730493034f0f +size 70536 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png b/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png index 8eae9dc33..84f7cc7bd 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7b21826be83c1a16b9fa7b19130f40261a9735c3eb450eaf9f8d5a3bbfccb7a -size 66365 +oid sha256:a307ac48abc79548c16468b3606a5df283ab2a5ac28345bd801bcc3887063414 +size 66384 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png b/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png index 181223e48..93789ff45 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f7986da3f4a3b94e701bf94066896aedb41458f12f6cdaf93a4fd371b1705da -size 21206 +oid sha256:6311be2b850b5e41ac6dadf639b00584438b56f651a3c8d75ac8f5e06c9ad6fa +size 21224 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png b/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png index af9ee2651..6d9aced18 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8fb28c16dc2e4359ada002a5fb98e955db606f44db4bdd1a400ab2e4c43c850e -size 59701 +oid sha256:c80158ac9c823f94d2830d1423236ad441dc7da31e748b6815c69663fa2a03d0 +size 59662 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png b/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png index 9f2d64d17..5f77c2db3 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2a8a3e5953a5ee41d95f2a23b580bb9c82e869b7bad0f2611ed866c55bc12b07 -size 13102 +oid sha256:3584f16229bae50cc04b31df6bf5ccf43288fd05b447b34b29f118eb7435a090 +size 13103 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png b/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png index 2128b9d05..21b08252a 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:12fdbe6805dbce0d993236f30ac14d9c03411b6918399ec5bfa7aa39104d0640 -size 35182 +oid sha256:3411a4a8939b7e731c9c1a6331921b0ac905f4e3e86a51af70bdb38d9446f5e1 +size 35193 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_1.png b/crates/egui_demo_lib/tests/snapshots/modals_1.png index 177e90cc3..2036be3c9 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_1.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d270118b982413bd0c05e9f20a3ee53b88a1681e273f73a81d1733b05c7a006 -size 47889 +oid sha256:1ac48ec9f7bde9869f1b3097e9f897b5e8df96cd6159a6ded542582dc69ab32c +size 47913 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_2.png b/crates/egui_demo_lib/tests/snapshots/modals_2.png index 7e5d7ff82..60e6ddef5 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_2.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9f4ad4ac06f2113533943fc00ed9747c237c9b7b6f0890ccf9857e606290c7ec -size 47871 +oid sha256:795e16389b31ad719050247eb9e736782380a83fa71b5b35b50e17812c8d9bdd +size 47886 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_3.png b/crates/egui_demo_lib/tests/snapshots/modals_3.png index 12c338393..1ae5a3314 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_3.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5fee7a7f4ebcda3fd55d1d31e21b46246c4d5f166fb1f0de04265e7db424d9c8 -size 43733 +oid sha256:a62a286e29aa0e0f949088ddefe01137535877408ba88778f61cbfe8d50c2261 +size 43750 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png b/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png index 5d0da39d8..e059b556f 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:510a3b5d8b10013eed234977bf46575a39cc951611a073154652ce6bf2bc3ecc -size 43880 +oid sha256:9c1bc8e22aa1050a4e7d1b2abe407251e22d338c38a7e41c045a384c9139b4de +size 43895 From 95ab92ca4a1919d5371c26d5cf993396479e99d0 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 4 Feb 2025 16:28:27 +0100 Subject: [PATCH 059/132] Protect against mistyping "tessellator" --- .typos.toml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.typos.toml b/.typos.toml index de51a691c..db5bf48b4 100644 --- a/.typos.toml +++ b/.typos.toml @@ -7,5 +7,15 @@ ime = "ime" # Input Method Editor nknown = "nknown" # part of @55nknown username ro = "ro" # read-only, also part of the username @Phen-Ro +# I mistype these so often +tesalator = "tessellator" +teselator = "tessellator" +tessalator = "tessellator" +tesselator = "tessellator" +tesalation = "tessellation" +teselation = "tessellation" +tessalation = "tessellation" +tesselation = "tessellation" + [files] extend-exclude = ["web_demo/egui_demo_app.js"] # auto-generated From f162c3bcbfdf3f3420a2112960d19e306ced9c8d Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 4 Feb 2025 16:28:52 +0100 Subject: [PATCH 060/132] Explain how to set up generate_changelog.py --- scripts/generate_changelog.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/generate_changelog.py b/scripts/generate_changelog.py index a3ee04a4a..7e3ae4284 100755 --- a/scripts/generate_changelog.py +++ b/scripts/generate_changelog.py @@ -5,6 +5,8 @@ Summarizes recent PRs based on their GitHub labels. The result can be copy-pasted into CHANGELOG.md, though it often needs some manual editing too. + +Setup: pip install GitPython requests tqdm """ import argparse From d97cd8233858be557025d86ec5b2d59bc62ba669 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 4 Feb 2025 16:28:36 +0100 Subject: [PATCH 061/132] Update RELEASES.md --- RELEASES.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index a96b35be0..5850713db 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -36,14 +36,9 @@ We don't update the MSRV in a patch release, unless we really, really need to. ## Release testing * [ ] `cargo r -p egui_demo_app` and click around for while -* [ ] `./scripts/build_demo_web.sh --release -g` - - check frame-rate and wasm size - - test on mobile - - test on chromium - - check the in-browser profiler -* [ ] check the color test * [ ] update `eframe_template` and test * [ ] update `egui_plot` and test +* [ ] update `egui_table` and test * [ ] update `egui_tiles` and test * [ ] test with Rerun * [ ] `./scripts/check.sh` @@ -98,3 +93,5 @@ I usually do this all on the `master` branch, but doing it in a release branch i * [ ] publish new `egui_plot` * [ ] publish new `egui_table` * [ ] publish new `egui_tiles` +* [ ] make a PR to `egui_commonmark` +* [ ] make a PR to `rerun` From 5c372a7b367a7aa7ed6e82ada9f57a8a11e8d596 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 4 Feb 2025 16:47:56 +0100 Subject: [PATCH 062/132] Release 0.31.0 - Scene container, improved rendering quality --- CHANGELOG.md | 82 ++++++++++++++++++++++++ Cargo.lock | 30 ++++----- Cargo.toml | 26 ++++---- RELEASES.md | 6 +- crates/ecolor/CHANGELOG.md | 4 ++ crates/eframe/CHANGELOG.md | 8 +++ crates/egui-wgpu/CHANGELOG.md | 6 ++ crates/egui-winit/CHANGELOG.md | 5 ++ crates/egui_extras/CHANGELOG.md | 6 ++ crates/egui_glow/CHANGELOG.md | 4 ++ crates/egui_kittest/CHANGELOG.md | 12 ++++ crates/epaint/CHANGELOG.md | 21 ++++++ crates/epaint_default_fonts/CHANGELOG.md | 4 ++ 13 files changed, 183 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6733405c..8877c9905 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,88 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.31.0 - 2025-02-04 - Scene container, improved rendering quality + +### Highlights ✨ + +#### Scene container +This release adds the `Scene` container to egui. It is a pannable, zoomable canvas that can contain `Widget`s and child `Ui`s. +This will make it easier to e.g. implement a graph editor. + +![image](https://github.com/user-attachments/assets/b37d56ef-f6ef-46be-bb99-6eb25b6efca9) + +#### Clearer, pixel perfect rendering +The tessellator has been updated for improved rendering quality and better performance. It will produce fewer vertices +and shapes will have less overdraw. We've also defined what `CornerRadius` (previously `Rounding`) means. + +We've also added a tessellator test to the [demo app](https://www.egui.rs/), where you can play around with different +values to see what's produced: + + +https://github.com/user-attachments/assets/adf55e3b-fb48-4df0-aaa2-150ee3163684 + + +Check the [PR](https://github.com/emilk/egui/pull/5669) for more details. + +#### `CornerRadius`, `Margin`, `Shadow` size reduction +In order to pave the path for more complex and customizable styling solutions, we've reduced the size of +`CornerRadius`, `Margin` and `Shadow` values to `i8` and `u8`. + + + +### Migration guide +- Add a `StrokeKind` to all your `Painter::rect` calls [#5648](https://github.com/emilk/egui/pull/5648) +- `StrokeKind::default` was removed, since the 'normal' value depends on the context [#5658](https://github.com/emilk/egui/pull/5658) + - You probably want to use `StrokeKind::Inside` when drawing rectangles + - You probably want to use `StrokeKind::Middle` when drawing open paths +- Rename `Rounding` to `CornerRadius` [#5673](https://github.com/emilk/egui/pull/5673) +- `CornerRadius`, `Margin` and `Shadow` have been updated to use `i8` and `u8` [#5563](https://github.com/emilk/egui/pull/5563), [#5567](https://github.com/emilk/egui/pull/5567), [#5568](https://github.com/emilk/egui/pull/5568) + - Remove the .0 from your values + - Cast dynamic values with `as i8` / `as u8` or `as _` if you want Rust to infer the type + - Rust will do a 'saturating' cast, so if your `f32` value is bigger than `127` it will be clamped to `127` +- `RectShape` parameters changed [#5565](https://github.com/emilk/egui/pull/5565) + - Prefer to use the builder methods to create it instead of initializing it directly +- `Frame` now takes the `Stroke` width into account for its sizing, so check all views of your app to make sure they still look right. + Read the [PR](https://github.com/emilk/egui/pull/5575) for more info. + +### ⭐ Added +* Add `egui::Scene` for panning/zooming a `Ui` [#5505](https://github.com/emilk/egui/pull/5505) by [@grtlr](https://github.com/grtlr) +* Animated WebP support [#5470](https://github.com/emilk/egui/pull/5470) by [@Aely0](https://github.com/Aely0) +* Improve tessellation quality [#5669](https://github.com/emilk/egui/pull/5669) by [@emilk](https://github.com/emilk) +* Add `OutputCommand` for copying text and opening URL:s [#5532](https://github.com/emilk/egui/pull/5532) by [@emilk](https://github.com/emilk) +* Add `Context::copy_image` [#5533](https://github.com/emilk/egui/pull/5533) by [@emilk](https://github.com/emilk) +* Add `WidgetType::Image` and `Image::alt_text` [#5534](https://github.com/emilk/egui/pull/5534) by [@lucasmerlin](https://github.com/lucasmerlin) +* Add `epaint::Brush` for controlling `RectShape` texturing [#5565](https://github.com/emilk/egui/pull/5565) by [@emilk](https://github.com/emilk) +* Implement `nohash_hasher::IsEnabled` for `Id` [#5628](https://github.com/emilk/egui/pull/5628) by [@emilk](https://github.com/emilk) +* Add keys for `!`, `{`, `}` [#5548](https://github.com/emilk/egui/pull/5548) by [@Its-Just-Nans](https://github.com/Its-Just-Nans) +* Add `RectShape::stroke_kind ` to control if stroke is inside/outside/centered [#5647](https://github.com/emilk/egui/pull/5647) by [@emilk](https://github.com/emilk) + +### 🔧 Changed +* ⚠️ `Frame` now includes stroke width as part of padding [#5575](https://github.com/emilk/egui/pull/5575) by [@emilk](https://github.com/emilk) +* Rename `Rounding` to `CornerRadius` [#5673](https://github.com/emilk/egui/pull/5673) by [@emilk](https://github.com/emilk) +* Require a `StrokeKind` when painting rectangles with strokes [#5648](https://github.com/emilk/egui/pull/5648) by [@emilk](https://github.com/emilk) +* Round widget coordinates to even multiple of 1/32 [#5517](https://github.com/emilk/egui/pull/5517) by [@emilk](https://github.com/emilk) +* Make all lines and rectangles crisp [#5518](https://github.com/emilk/egui/pull/5518) by [@emilk](https://github.com/emilk) +* Tweak window resize handles [#5524](https://github.com/emilk/egui/pull/5524) by [@emilk](https://github.com/emilk) + +### 🔥 Removed +* Remove `egui::special_emojis::TWITTER` [#5622](https://github.com/emilk/egui/pull/5622) by [@emilk](https://github.com/emilk) +* Remove `StrokeKind::default` [#5658](https://github.com/emilk/egui/pull/5658) by [@emilk](https://github.com/emilk) + +### 🐛 Fixed +* Use correct minimum version of `profiling` crate [#5494](https://github.com/emilk/egui/pull/5494) by [@lucasmerlin](https://github.com/lucasmerlin) +* Fix interactive widgets sometimes being incorrectly marked as hovered [#5523](https://github.com/emilk/egui/pull/5523) by [@emilk](https://github.com/emilk) +* Fix panic due to non-total ordering in `Area::compare_order()` [#5569](https://github.com/emilk/egui/pull/5569) by [@HactarCE](https://github.com/HactarCE) +* Fix hovering through custom menu button [#5555](https://github.com/emilk/egui/pull/5555) by [@M4tthewDE](https://github.com/M4tthewDE) + +### 🚀 Performance +* Use `u8` in `CornerRadius`, and introduce `CornerRadiusF32` [#5563](https://github.com/emilk/egui/pull/5563) by [@emilk](https://github.com/emilk) +* Store `Margin` using `i8` to reduce its size [#5567](https://github.com/emilk/egui/pull/5567) by [@emilk](https://github.com/emilk) +* Shrink size of `Shadow` by using `i8/u8` instead of `f32` [#5568](https://github.com/emilk/egui/pull/5568) by [@emilk](https://github.com/emilk) +* Avoid allocations for loader cache lookup [#5584](https://github.com/emilk/egui/pull/5584) by [@mineichen](https://github.com/mineichen) +* Use bitfield instead of bools in `Response` and `Sense` [#5556](https://github.com/emilk/egui/pull/5556) by [@polwel](https://github.com/polwel) + + ## 0.30.0 - 2024-12-16 - Modals and better layer support ### ✨ Highlights diff --git a/Cargo.lock b/Cargo.lock index 709b7b77a..4956e1436 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1197,7 +1197,7 @@ checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" [[package]] name = "ecolor" -version = "0.30.0" +version = "0.31.0" dependencies = [ "bytemuck", "cint", @@ -1209,7 +1209,7 @@ dependencies = [ [[package]] name = "eframe" -version = "0.30.0" +version = "0.31.0" dependencies = [ "ahash", "bytemuck", @@ -1249,7 +1249,7 @@ dependencies = [ [[package]] name = "egui" -version = "0.30.0" +version = "0.31.0" dependencies = [ "accesskit", "ahash", @@ -1267,7 +1267,7 @@ dependencies = [ [[package]] name = "egui-wgpu" -version = "0.30.0" +version = "0.31.0" dependencies = [ "ahash", "bytemuck", @@ -1285,7 +1285,7 @@ dependencies = [ [[package]] name = "egui-winit" -version = "0.30.0" +version = "0.31.0" dependencies = [ "accesskit_winit", "ahash", @@ -1306,7 +1306,7 @@ dependencies = [ [[package]] name = "egui_demo_app" -version = "0.30.0" +version = "0.31.0" dependencies = [ "bytemuck", "chrono", @@ -1333,7 +1333,7 @@ dependencies = [ [[package]] name = "egui_demo_lib" -version = "0.30.0" +version = "0.31.0" dependencies = [ "chrono", "criterion", @@ -1347,7 +1347,7 @@ dependencies = [ [[package]] name = "egui_extras" -version = "0.30.0" +version = "0.31.0" dependencies = [ "ahash", "chrono", @@ -1366,7 +1366,7 @@ dependencies = [ [[package]] name = "egui_glow" -version = "0.30.0" +version = "0.31.0" dependencies = [ "ahash", "bytemuck", @@ -1386,7 +1386,7 @@ dependencies = [ [[package]] name = "egui_kittest" -version = "0.30.0" +version = "0.31.0" dependencies = [ "dify", "document-features", @@ -1421,7 +1421,7 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "emath" -version = "0.30.0" +version = "0.31.0" dependencies = [ "bytemuck", "document-features", @@ -1512,7 +1512,7 @@ dependencies = [ [[package]] name = "epaint" -version = "0.30.0" +version = "0.31.0" dependencies = [ "ab_glyph", "ahash", @@ -1533,7 +1533,7 @@ dependencies = [ [[package]] name = "epaint_default_fonts" -version = "0.30.0" +version = "0.31.0" [[package]] name = "equivalent" @@ -3099,7 +3099,7 @@ checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" [[package]] name = "popups" -version = "0.30.0" +version = "0.31.0" dependencies = [ "eframe", "env_logger", @@ -5105,7 +5105,7 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" [[package]] name = "xtask" -version = "0.30.0" +version = "0.31.0" [[package]] name = "yaml-rust" diff --git a/Cargo.toml b/Cargo.toml index b8a58df8b..3934c44b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ members = [ edition = "2021" license = "MIT OR Apache-2.0" rust-version = "1.81" -version = "0.30.0" +version = "0.31.0" [profile.release] @@ -55,18 +55,18 @@ opt-level = 2 [workspace.dependencies] -emath = { version = "0.30.0", path = "crates/emath", default-features = false } -ecolor = { version = "0.30.0", path = "crates/ecolor", default-features = false } -epaint = { version = "0.30.0", path = "crates/epaint", default-features = false } -epaint_default_fonts = { version = "0.30.0", path = "crates/epaint_default_fonts" } -egui = { version = "0.30.0", path = "crates/egui", default-features = false } -egui-winit = { version = "0.30.0", path = "crates/egui-winit", default-features = false } -egui_extras = { version = "0.30.0", path = "crates/egui_extras", default-features = false } -egui-wgpu = { version = "0.30.0", path = "crates/egui-wgpu", default-features = false } -egui_demo_lib = { version = "0.30.0", path = "crates/egui_demo_lib", default-features = false } -egui_glow = { version = "0.30.0", path = "crates/egui_glow", default-features = false } -egui_kittest = { version = "0.30.0", path = "crates/egui_kittest", default-features = false } -eframe = { version = "0.30.0", path = "crates/eframe", default-features = false } +emath = { version = "0.31.0", path = "crates/emath", default-features = false } +ecolor = { version = "0.31.0", path = "crates/ecolor", default-features = false } +epaint = { version = "0.31.0", path = "crates/epaint", default-features = false } +epaint_default_fonts = { version = "0.31.0", path = "crates/epaint_default_fonts" } +egui = { version = "0.31.0", path = "crates/egui", default-features = false } +egui-winit = { version = "0.31.0", path = "crates/egui-winit", default-features = false } +egui_extras = { version = "0.31.0", path = "crates/egui_extras", default-features = false } +egui-wgpu = { version = "0.31.0", path = "crates/egui-wgpu", default-features = false } +egui_demo_lib = { version = "0.31.0", path = "crates/egui_demo_lib", default-features = false } +egui_glow = { version = "0.31.0", path = "crates/egui_glow", default-features = false } +egui_kittest = { version = "0.31.0", path = "crates/egui_kittest", default-features = false } +eframe = { version = "0.31.0", path = "crates/eframe", default-features = false } ahash = { version = "0.8.11", default-features = false, features = [ "no-rng", # we don't need DOS-protection, so we let users opt-in to it instead diff --git a/RELEASES.md b/RELEASES.md index 5850713db..a8e629a03 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -46,7 +46,7 @@ We don't update the MSRV in a patch release, unless we really, really need to. ## Preparation * [ ] run `scripts/generate_example_screenshots.sh` if needed -* [ ] write a short release note that fits in a tweet +* [ ] write a short release note that fits in a bluesky post * [ ] record gif for `CHANGELOG.md` release note (and later bluesky post) * [ ] update changelogs using `scripts/generate_changelog.py --version 0.x.0 --write` * [ ] bump version numbers in workspace `Cargo.toml` @@ -55,9 +55,9 @@ We don't update the MSRV in a patch release, unless we really, really need to. I usually do this all on the `master` branch, but doing it in a release branch is also fine, as long as you remember to merge it into `master` later. * [ ] Run `typos` -* [ ] `git commit -m 'Release 0.x.0 - summary'` +* [ ] `git commit -m 'Release 0.x.0 - '` * [ ] `cargo publish` (see below) -* [ ] `git tag -a 0.x.0 -m 'Release 0.x.0 - summary'` +* [ ] `git tag -a 0.x.0 -m 'Release 0.x.0 - '` * [ ] `git pull --tags ; git tag -d latest && git tag -a latest -m 'Latest release' && git push --tags origin latest --force ; git push --tags` * [ ] merge release PR or push to `master` * [ ] check that CI is green diff --git a/crates/ecolor/CHANGELOG.md b/crates/ecolor/CHANGELOG.md index 37b5555b6..61cfce610 100644 --- a/crates/ecolor/CHANGELOG.md +++ b/crates/ecolor/CHANGELOG.md @@ -6,6 +6,10 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.31.0 - 2025-02-04 +* Add `Color32::CYAN` and `Color32::MAGENTA` [#5663](https://github.com/emilk/egui/pull/5663) by [@juancampa](https://github.com/juancampa) + + ## 0.30.0 - 2024-12-16 * Use boxed slice for lookup table to avoid stack overflow [#5212](https://github.com/emilk/egui/pull/5212) by [@YgorSouza](https://github.com/YgorSouza) * Add `Color32::mul` [#5437](https://github.com/emilk/egui/pull/5437) by [@emilk](https://github.com/emilk) diff --git a/crates/eframe/CHANGELOG.md b/crates/eframe/CHANGELOG.md index 0ddfa4b0e..967b69e3b 100644 --- a/crates/eframe/CHANGELOG.md +++ b/crates/eframe/CHANGELOG.md @@ -7,6 +7,14 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.31.0 - 2025-02-04 +* Web: Fix incorrect scale when moving to screen with new DPI [#5631](https://github.com/emilk/egui/pull/5631) by [@emilk](https://github.com/emilk) +* Re-enable IME support on Linux [#5198](https://github.com/emilk/egui/pull/5198) by [@YgorSouza](https://github.com/YgorSouza) +* Serialize window maximized state in `WindowSettings` [#5554](https://github.com/emilk/egui/pull/5554) by [@landaire](https://github.com/landaire) +* Save state on suspend on Android and iOS [#5601](https://github.com/emilk/egui/pull/5601) by [@Pandicon](https://github.com/Pandicon) +* Eframe web: forward cmd-S/O to egui app (stop default browser action) [#5655](https://github.com/emilk/egui/pull/5655) by [@emilk](https://github.com/emilk) + + ## 0.30.0 - 2024-12-16 - Android support NOTE: you now need to enable the `wayland` or `x11` features to get Linux support, including getting it to work on most CI systems. diff --git a/crates/egui-wgpu/CHANGELOG.md b/crates/egui-wgpu/CHANGELOG.md index d92e867a7..a20cafa14 100644 --- a/crates/egui-wgpu/CHANGELOG.md +++ b/crates/egui-wgpu/CHANGELOG.md @@ -6,6 +6,12 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.31.0 - 2025-02-04 +* Upgrade to wgpu 24 [#5610](https://github.com/emilk/egui/pull/5610) by [@torokati44](https://github.com/torokati44) +* Extend `WgpuSetup`, `egui_kittest` now prefers software rasterizers for testing [#5506](https://github.com/emilk/egui/pull/5506) by [@Wumpf](https://github.com/Wumpf) +* Wgpu resources are no longer wrapped in `Arc` (since they are now `Clone`) [#5612](https://github.com/emilk/egui/pull/5612) by [@Wumpf](https://github.com/Wumpf) + + ## 0.30.0 - 2024-12-16 * Fix docs.rs build [#5204](https://github.com/emilk/egui/pull/5204) by [@lucasmerlin](https://github.com/lucasmerlin) * Free textures after submitting queue instead of before with wgpu renderer [#5226](https://github.com/emilk/egui/pull/5226) by [@Rusty-Cube](https://github.com/Rusty-Cube) diff --git a/crates/egui-winit/CHANGELOG.md b/crates/egui-winit/CHANGELOG.md index d16446420..7cc8667c5 100644 --- a/crates/egui-winit/CHANGELOG.md +++ b/crates/egui-winit/CHANGELOG.md @@ -5,6 +5,11 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.31.0 - 2025-02-04 +* Re-enable IME support on Linux [#5198](https://github.com/emilk/egui/pull/5198) by [@YgorSouza](https://github.com/YgorSouza) +* Update to winit 0.30.7 [#5516](https://github.com/emilk/egui/pull/5516) by [@emilk](https://github.com/emilk) + + ## 0.30.0 - 2024-12-16 * iOS: Support putting UI next to the dynamic island [#5211](https://github.com/emilk/egui/pull/5211) by [@frederik-uni](https://github.com/frederik-uni) * Remove implicit `accesskit_winit` feature [#5316](https://github.com/emilk/egui/pull/5316) by [@waywardmonkeys](https://github.com/waywardmonkeys) diff --git a/crates/egui_extras/CHANGELOG.md b/crates/egui_extras/CHANGELOG.md index 7617b1863..d6cc2e9c8 100644 --- a/crates/egui_extras/CHANGELOG.md +++ b/crates/egui_extras/CHANGELOG.md @@ -5,6 +5,12 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.31.0 - 2025-02-04 +* Animated WebP support [#5470](https://github.com/emilk/egui/pull/5470), [#5586](https://github.com/emilk/egui/pull/5586) by [@Aely0](https://github.com/Aely0) +* Make image extension check case-insensitive [#5501](https://github.com/emilk/egui/pull/5501) by [@RyanBluth](https://github.com/RyanBluth) +* Avoid allocations for loader cache lookup [#5584](https://github.com/emilk/egui/pull/5584) by [@mineichen](https://github.com/mineichen) + + ## 0.30.0 - 2024-12-16 * Use `Table::id_salt` on `ScrollArea` [#5282](https://github.com/emilk/egui/pull/5282) by [@jwhear](https://github.com/jwhear) * Use proper `image` crate URI and MIME support detection [#5324](https://github.com/emilk/egui/pull/5324) by [@xangelix](https://github.com/xangelix) diff --git a/crates/egui_glow/CHANGELOG.md b/crates/egui_glow/CHANGELOG.md index dc3d6dfda..abdfa240e 100644 --- a/crates/egui_glow/CHANGELOG.md +++ b/crates/egui_glow/CHANGELOG.md @@ -6,6 +6,10 @@ Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.31.0 - 2025-02-04 +### ⭐ Added +* Add `Harness::new_eframe` and `TestRenderer` trait [#5539](https://github.com/emilk/egui/pull/5539) by [@lucasmerlin](https://github.com/lucasmerlin) +* Change `Harness::run` to run until no more repaints are requested [#5580](https://github.com/emilk/egui/pull/5580) by [@lucasmerlin](https://github.com/lucasmerlin) +* Add `SnapshotResults` struct to `egui_kittest` [#5672](https://github.com/emilk/egui/pull/5672) by [@lucasmerlin](https://github.com/lucasmerlin) + +### 🔧 Changed +* Extend `WgpuSetup`, `egui_kittest` now prefers software rasterizers for testing [#5506](https://github.com/emilk/egui/pull/5506) by [@Wumpf](https://github.com/Wumpf) +* Write `.old.png` files when updating images [#5578](https://github.com/emilk/egui/pull/5578) by [@emilk](https://github.com/emilk) +* Succeed and keep going when `UPDATE_SNAPSHOTS` is set [#5649](https://github.com/emilk/egui/pull/5649) by [@emilk](https://github.com/emilk) + + ## 0.30.0 - 2024-12-16 - Initial relrease * Support for egui 0.30.0 * Automate clicks and text input diff --git a/crates/epaint/CHANGELOG.md b/crates/epaint/CHANGELOG.md index 742cf0acd..cee374ce6 100644 --- a/crates/epaint/CHANGELOG.md +++ b/crates/epaint/CHANGELOG.md @@ -5,6 +5,27 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.31.0 - 2025-02-04 +### ⭐ Added +* Improve tessellation quality [#5669](https://github.com/emilk/egui/pull/5669) by [@emilk](https://github.com/emilk) +* Add `epaint::Brush` for controlling `RectShape` texturing [#5565](https://github.com/emilk/egui/pull/5565) by [@emilk](https://github.com/emilk) +* Add `RectShape::stroke_kind ` to control if stroke is inside/outside/centered [#5647](https://github.com/emilk/egui/pull/5647) by [@emilk](https://github.com/emilk) + +### 🔧 Changed +* Rename `Rounding` to `CornerRadius` [#5673](https://github.com/emilk/egui/pull/5673) by [@emilk](https://github.com/emilk) +* Make all lines and rectangles crisp [#5518](https://github.com/emilk/egui/pull/5518) by [@emilk](https://github.com/emilk) +* Better rounding of rectangles with thin outlines [#5571](https://github.com/emilk/egui/pull/5571) by [@emilk](https://github.com/emilk) +* Require a `StrokeKind` when painting rectangles with strokes [#5648](https://github.com/emilk/egui/pull/5648) by [@emilk](https://github.com/emilk) + +### 🔥 Removed +* Remove `StrokeKind::default` [#5658](https://github.com/emilk/egui/pull/5658) by [@emilk](https://github.com/emilk) + +### 🚀 Performance +* Use `u8` in `Rounding`, and introduce `Roundingf` [#5563](https://github.com/emilk/egui/pull/5563) by [@emilk](https://github.com/emilk) +* Store `Margin` using `i8` to reduce its size [#5567](https://github.com/emilk/egui/pull/5567) by [@emilk](https://github.com/emilk) +* Shrink size of `Shadow` by using `i8/u8` instead of `f32` [#5568](https://github.com/emilk/egui/pull/5568) by [@emilk](https://github.com/emilk) + + ## 0.30.0 - 2024-12-16 * Expand max font atlas size from 8k to 16k [#5257](https://github.com/emilk/egui/pull/5257) by [@rustbasic](https://github.com/rustbasic) * Put font data into `Arc` to reduce memory consumption [#5276](https://github.com/emilk/egui/pull/5276) by [@StarStarJ](https://github.com/StarStarJ) diff --git a/crates/epaint_default_fonts/CHANGELOG.md b/crates/epaint_default_fonts/CHANGELOG.md index 42cd89ba5..8d9eff7cd 100644 --- a/crates/epaint_default_fonts/CHANGELOG.md +++ b/crates/epaint_default_fonts/CHANGELOG.md @@ -5,6 +5,10 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.31.0 - 2025-02-04 +* Update `egui_default_fonts` license [#5361](https://github.com/emilk/egui/pull/5361) by [@pombredanne](https://github.com/pombredanne) + + ## 0.30.0 - 2024-12-16 Nothing new From d7453e42d3be72b1d5e22bd623c4db8817c99314 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 4 Feb 2025 17:03:38 +0100 Subject: [PATCH 063/132] Fix gif in CHANGELOG.md --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8877c9905..b61c6398d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,13 +22,13 @@ Changes since the last release can be found at Date: Tue, 4 Feb 2025 17:04:29 +0100 Subject: [PATCH 064/132] Fix warning --- crates/egui_demo_lib/src/demo/widget_gallery.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/egui_demo_lib/src/demo/widget_gallery.rs b/crates/egui_demo_lib/src/demo/widget_gallery.rs index eae07ccd5..6005d6aec 100644 --- a/crates/egui_demo_lib/src/demo/widget_gallery.rs +++ b/crates/egui_demo_lib/src/demo/widget_gallery.rs @@ -48,6 +48,7 @@ impl Default for WidgetGallery { } impl WidgetGallery { + #[allow(unused_mut)] // if not chrono #[inline] pub fn with_date_button(mut self, _with_date_button: bool) -> Self { #[cfg(feature = "chrono")] From 0db56dc9f1a8459b5b9376159fab7d7048b19b65 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 4 Feb 2025 17:35:55 +0100 Subject: [PATCH 065/132] Fix the magnification range of the tessellation test --- .../src/demo/tests/tessellation_test.rs | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) 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 58f53560d..031f3e2ce 100644 --- a/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs +++ b/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs @@ -178,7 +178,7 @@ impl crate::View for TessellationTest { ui.add( egui::DragValue::new(magnification_pixel_size) .speed(0.5) - .range(0.0..=64.0), + .range(1.0..=32.0), ); ui.end_row(); @@ -244,25 +244,27 @@ impl crate::View for TessellationTest { } } - // Draw pixel centers: - let pixel_radius = 0.75; - let pixel_color = Color32::GRAY; - for yi in 0.. { - let y = (yi as f32 + 0.5) * magnification_pixel_size; - if y > canvas.height() / 2.0 { - break; - } - for xi in 0.. { - let x = (xi as f32 + 0.5) * magnification_pixel_size; - if x > canvas.width() / 2.0 { + if 3.0 < magnification_pixel_size { + // Draw pixel centers: + let pixel_radius = 0.75; + let pixel_color = Color32::GRAY; + for yi in 0.. { + let y = (yi as f32 + 0.5) * magnification_pixel_size; + if y > canvas.height() / 2.0 { break; } - for offset in [vec2(x, y), vec2(x, -y), vec2(-x, y), vec2(-x, -y)] { - painter.circle_filled( - canvas.center() + offset, - pixel_radius, - pixel_color, - ); + for xi in 0.. { + let x = (xi as f32 + 0.5) * magnification_pixel_size; + if x > canvas.width() / 2.0 { + break; + } + for offset in [vec2(x, y), vec2(x, -y), vec2(-x, y), vec2(-x, -y)] { + painter.circle_filled( + canvas.center() + offset, + pixel_radius, + pixel_color, + ); + } } } } From 54d00d7d6990dab35b4ec8083d08b42d247c952a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=93teris=20Pakalns?= Date: Thu, 6 Feb 2025 22:37:32 +0200 Subject: [PATCH 066/132] Fix panic when rendering thin textured rectangles (#5692) * Closes https://github.com/emilk/egui/issues/5664 * [x] I have followed the instructions in the PR template --- crates/epaint/src/tessellator.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index e8f8ad52c..6d953f257 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1810,7 +1810,7 @@ impl Tessellator { } } - if stroke.is_empty() { + if stroke.is_empty() && out.texture_id == TextureId::default() { // Approximate thin rectangles with line segments. // This is important so that thin rectangles look good. if rect.width() <= 2.0 * self.feathering { From 81806c4b86677c691b74dc04ef9af9f20555682d Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 7 Feb 2025 09:22:10 +0100 Subject: [PATCH 067/132] Add badges to kittest README.md --- crates/egui_kittest/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/egui_kittest/README.md b/crates/egui_kittest/README.md index a9c1286bf..1cef1b0d6 100644 --- a/crates/egui_kittest/README.md +++ b/crates/egui_kittest/README.md @@ -1,5 +1,10 @@ # egui_kittest +[![Latest version](https://img.shields.io/crates/v/egui_kittest.svg)](https://crates.io/crates/egui_kittest) +[![Documentation](https://docs.rs/egui_kittest/badge.svg)](https://docs.rs/egui_kittest) +![MIT](https://img.shields.io/badge/license-MIT-blue.svg) +![Apache](https://img.shields.io/badge/license-Apache-blue.svg) + Ui testing library for egui, based on [kittest](https://github.com/rerun-io/kittest) (an [AccessKit](https://github.com/AccessKit/accesskit) based testing library). ## Example usage From 1c6e7b1bd0546ec6a7f354077e3e2e5c0d8b145a Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Mon, 10 Feb 2025 09:33:36 +0100 Subject: [PATCH 068/132] Fix modifiers not working in kittest (#5693) * Closes * [x] I have followed the instructions in the PR template It still isn't ideal, since you have to remember to call key_up on a separate frame. --------- Co-authored-by: Emil Ernerfeldt --- RELEASES.md | 4 ++- crates/egui/src/data/input.rs | 7 +++++ crates/egui_kittest/src/event.rs | 34 ++++++++++++++------- crates/egui_kittest/src/lib.rs | 34 ++++++++++++++++----- crates/egui_kittest/tests/tests.rs | 47 +++++++++++++++++++++++++++--- 5 files changed, 104 insertions(+), 22 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index a8e629a03..34ef11463 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -74,13 +74,15 @@ I usually do this all on the `master` branch, but doing it in a release branch i (cd crates/egui && cargo publish --quiet) && echo "✅ egui" (cd crates/egui-winit && cargo publish --quiet) && echo "✅ egui-winit" (cd crates/egui-wgpu && cargo publish --quiet) && echo "✅ egui-wgpu" +(cd crates/eframe && cargo publish --quiet) && echo "✅ eframe" (cd crates/egui_kittest && cargo publish --quiet) && echo "✅ egui_kittest" (cd crates/egui_extras && cargo publish --quiet) && echo "✅ egui_extras" (cd crates/egui_demo_lib && cargo publish --quiet) && echo "✅ egui_demo_lib" (cd crates/egui_glow && cargo publish --quiet) && echo "✅ egui_glow" -(cd crates/eframe && cargo publish --quiet) && echo "✅ eframe" ``` +\ + ## Announcements * [ ] [Bluesky](https://bsky.app/profile/ernerfeldt.bsky.social) * [ ] egui discord diff --git a/crates/egui/src/data/input.rs b/crates/egui/src/data/input.rs index 0bced4270..d781d243e 100644 --- a/crates/egui/src/data/input.rs +++ b/crates/egui/src/data/input.rs @@ -943,6 +943,13 @@ impl std::ops::BitOr for Modifiers { } } +impl std::ops::BitOrAssign for Modifiers { + #[inline] + fn bitor_assign(&mut self, rhs: Self) { + *self = *self | rhs; + } +} + // ---------------------------------------------------------------------------- /// Names of different modifier keys. diff --git a/crates/egui_kittest/src/event.rs b/crates/egui_kittest/src/event.rs index 5ac07488d..c3045d623 100644 --- a/crates/egui_kittest/src/event.rs +++ b/crates/egui_kittest/src/event.rs @@ -4,12 +4,26 @@ use kittest::{ElementState, MouseButton, SimulatedEvent}; #[derive(Default)] pub(crate) struct EventState { - modifiers: Modifiers, last_mouse_pos: Pos2, } impl EventState { - pub fn kittest_event_to_egui(&mut self, event: kittest::Event) -> Option { + /// Map the kittest events to egui events, add them 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, events: Vec, input: &mut egui::RawInput) { + for event in events { + 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 { @@ -23,7 +37,7 @@ impl EventState { SimulatedEvent::MouseInput { state, button } => { pointer_button_to_egui(button).map(|button| PointerButton { button, - modifiers: self.modifiers, + modifiers: *modifiers, pos: self.last_mouse_pos, pressed: matches!(state, ElementState::Pressed), }) @@ -32,22 +46,22 @@ impl EventState { SimulatedEvent::KeyInput { state, key } => { match key { kittest::Key::Alt => { - self.modifiers.alt = matches!(state, ElementState::Pressed); + modifiers.alt = matches!(state, ElementState::Pressed); } kittest::Key::Command => { - self.modifiers.command = matches!(state, ElementState::Pressed); + modifiers.command = matches!(state, ElementState::Pressed); } kittest::Key::Control => { - self.modifiers.ctrl = matches!(state, ElementState::Pressed); + modifiers.ctrl = matches!(state, ElementState::Pressed); } kittest::Key::Shift => { - self.modifiers.shift = matches!(state, ElementState::Pressed); + modifiers.shift = matches!(state, ElementState::Pressed); } _ => {} } kittest_key_to_egui(key).map(|key| Event::Key { key, - modifiers: self.modifiers, + modifiers: *modifiers, pressed: matches!(state, ElementState::Pressed), repeat: false, physical_key: None, @@ -58,7 +72,7 @@ impl EventState { } } -pub fn kittest_key_to_egui(value: kittest::Key) -> Option { +fn kittest_key_to_egui(value: kittest::Key) -> Option { use egui::Key as EKey; use kittest::Key; match value { @@ -170,7 +184,7 @@ pub fn kittest_key_to_egui(value: kittest::Key) -> Option { } } -pub fn pointer_button_to_egui(value: MouseButton) -> Option { +fn pointer_button_to_egui(value: MouseButton) -> Option { match value { MouseButton::Left => Some(egui::PointerButton::Primary), MouseButton::Right => Some(egui::PointerButton::Secondary), diff --git a/crates/egui_kittest/src/lib.rs b/crates/egui_kittest/src/lib.rs index 661cb92c3..9bccd263e 100644 --- a/crates/egui_kittest/src/lib.rs +++ b/crates/egui_kittest/src/lib.rs @@ -227,11 +227,8 @@ impl<'a, State> Harness<'a, State> { } fn _step(&mut self, sizing_pass: bool) { - for event in self.kittest.take_events() { - if let Some(event) = self.event_state.kittest_event_to_egui(event) { - self.input.events.push(event); - } - } + self.event_state + .update(self.kittest.take_events(), &mut self.input); self.input.predicted_dt = self.step_dt; @@ -376,12 +373,32 @@ impl<'a, State> Harness<'a, 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.press_key_modifiers(Modifiers::default(), key); + self.input.events.push(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, + repeat: false, + physical_key: None, + }); } /// Press a key with modifiers. - /// This will create a key down event and a key up event. + /// 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, @@ -389,6 +406,7 @@ impl<'a, State> Harness<'a, State> { repeat: false, physical_key: None, }); + self.step(); self.input.events.push(egui::Event::Key { key, pressed: false, @@ -396,6 +414,8 @@ impl<'a, State> Harness<'a, State> { repeat: false, physical_key: None, }); + + self.input.modifiers = previous_modifiers; } /// Render the last output to an image. diff --git a/crates/egui_kittest/tests/tests.rs b/crates/egui_kittest/tests/tests.rs index 29b4c7b11..4fa1239a7 100644 --- a/crates/egui_kittest/tests/tests.rs +++ b/crates/egui_kittest/tests/tests.rs @@ -1,4 +1,6 @@ -use egui_kittest::{Harness, SnapshotResults}; +use egui::Modifiers; +use egui_kittest::Harness; +use kittest::{Key, Queryable}; #[test] fn test_shrink() { @@ -10,8 +12,45 @@ fn test_shrink() { harness.fit_contents(); - let mut results = SnapshotResults::new(); - #[cfg(all(feature = "snapshot", feature = "wgpu"))] - results.add(harness.try_snapshot("test_shrink")); + harness.snapshot("test_shrink"); +} + +#[test] +fn test_modifiers() { + #[derive(Default)] + struct State { + cmd_clicked: bool, + cmd_z_pressed: bool, + } + let mut harness = Harness::new_ui_state( + |ui, state| { + if ui.button("Click me").clicked() && ui.input(|i| i.modifiers.command) { + state.cmd_clicked = true; + } + if ui.input(|i| i.modifiers.command && i.key_pressed(egui::Key::Z)) { + state.cmd_z_pressed = true; + } + }, + 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(); + // TODO(lucasmerlin): Right now the key_up needs to happen on a separate frame or it won't register. + // This should be more intuitive + harness.run(); + harness.get_by_label("Click me").key_up(Key::Command); + + harness.run(); + + harness.press_key_modifiers(Modifiers::COMMAND, egui::Key::Z); + // TODO(lucasmerlin): This should also work (Same problem as above) + // harness.node().key_combination(&[Key::Command, Key::Z]); + + let state = harness.state(); + assert!(state.cmd_clicked, "The button wasn't command-clicked"); + assert!(state.cmd_z_pressed, "Cmd+Z wasn't pressed"); } From 510b3cdf489500e68d5027e45934a0f68ed3b1fa Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Tue, 11 Feb 2025 11:23:59 +0100 Subject: [PATCH 069/132] Rename `Marginf` to `MarginF32` for consistency with `CornerRadiusF32` (#5677) * [x] I have followed the instructions in the PR template --- RELEASES.md | 1 + crates/egui/src/containers/frame.rs | 16 ++-- crates/epaint/src/lib.rs | 4 +- crates/epaint/src/margin.rs | 2 +- .../epaint/src/{marginf.rs => margin_f32.rs} | 79 ++++++++++--------- crates/epaint/src/shadow.rs | 6 +- 6 files changed, 56 insertions(+), 52 deletions(-) rename crates/epaint/src/{marginf.rs => margin_f32.rs} (79%) diff --git a/RELEASES.md b/RELEASES.md index 34ef11463..9d87988b7 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -45,6 +45,7 @@ We don't update the MSRV in a patch release, unless we really, really need to. * [ ] check that CI is green ## Preparation +* [ ] make sure there are no important unmerged PRs * [ ] run `scripts/generate_example_screenshots.sh` if needed * [ ] write a short release note that fits in a bluesky post * [ ] record gif for `CHANGELOG.md` release note (and later bluesky post) diff --git a/crates/egui/src/containers/frame.rs b/crates/egui/src/containers/frame.rs index 343897dcc..1fdbbca92 100644 --- a/crates/egui/src/containers/frame.rs +++ b/crates/egui/src/containers/frame.rs @@ -4,7 +4,7 @@ use crate::{ epaint, layers::ShapeIdx, InnerResponse, Response, Sense, Style, Ui, UiBuilder, UiKind, UiStackInfo, }; -use epaint::{Color32, CornerRadius, Margin, Marginf, Rect, Shadow, Shape, Stroke}; +use epaint::{Color32, CornerRadius, Margin, MarginF32, Rect, Shadow, Shape, Stroke}; /// A frame around some content, including margin, colors, etc. /// @@ -337,10 +337,10 @@ impl Frame { /// /// [`Self::inner_margin`] + [`Self.stroke`]`.width` + [`Self::outer_margin`]. #[inline] - pub fn total_margin(&self) -> Marginf { - Marginf::from(self.inner_margin) - + Marginf::from(self.stroke.width) - + Marginf::from(self.outer_margin) + pub fn total_margin(&self) -> MarginF32 { + MarginF32::from(self.inner_margin) + + MarginF32::from(self.stroke.width) + + MarginF32::from(self.outer_margin) } /// Calculate the `fill_rect` from the `content_rect`. @@ -354,14 +354,14 @@ impl Frame { /// /// This is the visible and interactive rectangle. pub fn widget_rect(&self, content_rect: Rect) -> Rect { - content_rect + self.inner_margin + Marginf::from(self.stroke.width) + content_rect + self.inner_margin + MarginF32::from(self.stroke.width) } /// Calculate the `outer_rect` from the `content_rect`. /// /// This is what is allocated in the outer [`Ui`], and is what is returned by [`Response::rect`]. pub fn outer_rect(&self, content_rect: Rect) -> Rect { - content_rect + self.inner_margin + Marginf::from(self.stroke.width) + self.outer_margin + content_rect + self.inner_margin + MarginF32::from(self.stroke.width) + self.outer_margin } } @@ -463,7 +463,7 @@ impl Prepared { let content_rect = self.content_ui.min_rect(); content_rect + self.frame.inner_margin - + Marginf::from(self.frame.stroke.width) + + MarginF32::from(self.frame.stroke.width) + self.frame.outer_margin } diff --git a/crates/epaint/src/lib.rs b/crates/epaint/src/lib.rs index ac0a90c60..f84a8caff 100644 --- a/crates/epaint/src/lib.rs +++ b/crates/epaint/src/lib.rs @@ -29,7 +29,7 @@ mod corner_radius; mod corner_radius_f32; pub mod image; mod margin; -mod marginf; +mod margin_f32; mod mesh; pub mod mutex; mod shadow; @@ -52,7 +52,7 @@ pub use self::{ corner_radius_f32::CornerRadiusF32, image::{ColorImage, FontImage, ImageData, ImageDelta}, margin::Margin, - marginf::Marginf, + margin_f32::*, mesh::{Mesh, Mesh16, Vertex}, shadow::Shadow, shapes::{ diff --git a/crates/epaint/src/margin.rs b/crates/epaint/src/margin.rs index e8fc530aa..2e87b7b30 100644 --- a/crates/epaint/src/margin.rs +++ b/crates/epaint/src/margin.rs @@ -9,7 +9,7 @@ use emath::{vec2, Rect, Vec2}; /// Use with care. /// /// All values are stored as [`i8`] to keep the size of [`Margin`] small. -/// If you want floats, use [`crate::Marginf`] instead. +/// If you want floats, use [`crate::MarginF32`] instead. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct Margin { diff --git a/crates/epaint/src/marginf.rs b/crates/epaint/src/margin_f32.rs similarity index 79% rename from crates/epaint/src/marginf.rs rename to crates/epaint/src/margin_f32.rs index eb3a1fbc9..fd88611d0 100644 --- a/crates/epaint/src/marginf.rs +++ b/crates/epaint/src/margin_f32.rs @@ -10,14 +10,17 @@ use crate::Margin; /// For storage, use [`crate::Margin`] instead. #[derive(Clone, Copy, Debug, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct Marginf { +pub struct MarginF32 { pub left: f32, pub right: f32, pub top: f32, pub bottom: f32, } -impl From for Marginf { +#[deprecated = "Renamed to MarginF32"] +pub type Marginf = MarginF32; + +impl From for MarginF32 { #[inline] fn from(margin: Margin) -> Self { Self { @@ -29,9 +32,9 @@ impl From for Marginf { } } -impl From for Margin { +impl From for Margin { #[inline] - fn from(marginf: Marginf) -> Self { + fn from(marginf: MarginF32) -> Self { Self { left: marginf.left as _, right: marginf.right as _, @@ -41,7 +44,7 @@ impl From for Margin { } } -impl Marginf { +impl MarginF32 { pub const ZERO: Self = Self { left: 0.0, right: 0.0, @@ -108,22 +111,22 @@ impl Marginf { } } -impl From for Marginf { +impl From for MarginF32 { #[inline] fn from(v: f32) -> Self { Self::same(v) } } -impl From for Marginf { +impl From for MarginF32 { #[inline] fn from(v: Vec2) -> Self { Self::symmetric(v.x, v.y) } } -/// `Marginf + Marginf` -impl std::ops::Add for Marginf { +/// `MarginF32 + MarginF32` +impl std::ops::Add for MarginF32 { type Output = Self; #[inline] @@ -137,8 +140,8 @@ impl std::ops::Add for Marginf { } } -/// `Marginf + f32` -impl std::ops::Add for Marginf { +/// `MarginF32 + f32` +impl std::ops::Add for MarginF32 { type Output = Self; #[inline] @@ -153,7 +156,7 @@ impl std::ops::Add for Marginf { } /// `Margind += f32` -impl std::ops::AddAssign for Marginf { +impl std::ops::AddAssign for MarginF32 { #[inline] fn add_assign(&mut self, v: f32) { self.left += v; @@ -163,8 +166,8 @@ impl std::ops::AddAssign for Marginf { } } -/// `Marginf * f32` -impl std::ops::Mul for Marginf { +/// `MarginF32 * f32` +impl std::ops::Mul for MarginF32 { type Output = Self; #[inline] @@ -178,8 +181,8 @@ impl std::ops::Mul for Marginf { } } -/// `Marginf *= f32` -impl std::ops::MulAssign for Marginf { +/// `MarginF32 *= f32` +impl std::ops::MulAssign for MarginF32 { #[inline] fn mul_assign(&mut self, v: f32) { self.left *= v; @@ -189,8 +192,8 @@ impl std::ops::MulAssign for Marginf { } } -/// `Marginf / f32` -impl std::ops::Div for Marginf { +/// `MarginF32 / f32` +impl std::ops::Div for MarginF32 { type Output = Self; #[inline] @@ -204,8 +207,8 @@ impl std::ops::Div for Marginf { } } -/// `Marginf /= f32` -impl std::ops::DivAssign for Marginf { +/// `MarginF32 /= f32` +impl std::ops::DivAssign for MarginF32 { #[inline] fn div_assign(&mut self, v: f32) { self.left /= v; @@ -215,8 +218,8 @@ impl std::ops::DivAssign for Marginf { } } -/// `Marginf - Marginf` -impl std::ops::Sub for Marginf { +/// `MarginF32 - MarginF32` +impl std::ops::Sub for MarginF32 { type Output = Self; #[inline] @@ -230,8 +233,8 @@ impl std::ops::Sub for Marginf { } } -/// `Marginf - f32` -impl std::ops::Sub for Marginf { +/// `MarginF32 - f32` +impl std::ops::Sub for MarginF32 { type Output = Self; #[inline] @@ -245,8 +248,8 @@ impl std::ops::Sub for Marginf { } } -/// `Marginf -= f32` -impl std::ops::SubAssign for Marginf { +/// `MarginF32 -= f32` +impl std::ops::SubAssign for MarginF32 { #[inline] fn sub_assign(&mut self, v: f32) { self.left -= v; @@ -256,12 +259,12 @@ impl std::ops::SubAssign for Marginf { } } -/// `Rect + Marginf` -impl std::ops::Add for Rect { +/// `Rect + MarginF32` +impl std::ops::Add for Rect { type Output = Self; #[inline] - fn add(self, margin: Marginf) -> Self { + fn add(self, margin: MarginF32) -> Self { Self::from_min_max( self.min - margin.left_top(), self.max + margin.right_bottom(), @@ -269,20 +272,20 @@ impl std::ops::Add for Rect { } } -/// `Rect += Marginf` -impl std::ops::AddAssign for Rect { +/// `Rect += MarginF32` +impl std::ops::AddAssign for Rect { #[inline] - fn add_assign(&mut self, margin: Marginf) { + fn add_assign(&mut self, margin: MarginF32) { *self = *self + margin; } } -/// `Rect - Marginf` -impl std::ops::Sub for Rect { +/// `Rect - MarginF32` +impl std::ops::Sub for Rect { type Output = Self; #[inline] - fn sub(self, margin: Marginf) -> Self { + fn sub(self, margin: MarginF32) -> Self { Self::from_min_max( self.min + margin.left_top(), self.max - margin.right_bottom(), @@ -290,10 +293,10 @@ impl std::ops::Sub for Rect { } } -/// `Rect -= Marginf` -impl std::ops::SubAssign for Rect { +/// `Rect -= MarginF32` +impl std::ops::SubAssign for Rect { #[inline] - fn sub_assign(&mut self, margin: Marginf) { + fn sub_assign(&mut self, margin: MarginF32) { *self = *self - margin; } } diff --git a/crates/epaint/src/shadow.rs b/crates/epaint/src/shadow.rs index e05cbdbe4..ee010ee99 100644 --- a/crates/epaint/src/shadow.rs +++ b/crates/epaint/src/shadow.rs @@ -1,4 +1,4 @@ -use crate::{Color32, CornerRadius, Marginf, Rect, RectShape, Vec2}; +use crate::{Color32, CornerRadius, MarginF32, Rect, RectShape, Vec2}; /// The color and fuzziness of a fuzzy shape. /// @@ -64,7 +64,7 @@ impl Shadow { } /// How much larger than the parent rect are we in each direction? - pub fn margin(&self) -> Marginf { + pub fn margin(&self) -> MarginF32 { let Self { offset, blur, @@ -74,7 +74,7 @@ impl Shadow { let spread = spread as f32; let blur = blur as f32; let [offset_x, offset_y] = offset; - Marginf { + MarginF32 { left: spread + 0.5 * blur - offset_x as f32, right: spread + 0.5 * blur + offset_x as f32, top: spread + 0.5 * blur - offset_y as f32, From 982b2580f40bd300b7570b7ec8b5cc2889c4b25c Mon Sep 17 00:00:00 2001 From: YgorSouza <43298013+YgorSouza@users.noreply.github.com> Date: Wed, 12 Feb 2025 14:20:15 +0100 Subject: [PATCH 070/132] Enable all features for egui_kittest docs (#5711) - Enable all-features when generating docs - Add x11 feature so it builds on Linux - Add double hashes to the feature comments so document-features includes them in the docs * Closes * [x] I have followed the instructions in the PR template --- crates/egui_kittest/Cargo.toml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/crates/egui_kittest/Cargo.toml b/crates/egui_kittest/Cargo.toml index 1e4af19e6..ef38ebd3b 100644 --- a/crates/egui_kittest/Cargo.toml +++ b/crates/egui_kittest/Cargo.toml @@ -18,8 +18,12 @@ include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--generate-link-to-definition"] + [features] -# Adds a wgpu-based test renderer. +## Adds a wgpu-based test renderer. wgpu = [ "dep:egui-wgpu", "dep:pollster", @@ -28,12 +32,15 @@ wgpu = [ "eframe?/wgpu", ] -# Adds a dify-based image snapshot utility. +## Adds a dify-based image snapshot utility. snapshot = ["dep:dify", "dep:image", "image/png"] -# Allows testing eframe::App +## Allows testing eframe::App eframe = ["dep:eframe", "eframe/accesskit"] +# This is just so it compiles with `--all-features` on Linux +x11 = ["eframe?/x11"] + [dependencies] kittest.workspace = true @@ -50,7 +57,7 @@ wgpu = { workspace = true, features = ["metal", "dx12"], optional = true } # snapshot dependencies dify = { workspace = true, optional = true } -## Enable this when generating docs. +# Enable this when generating docs. document-features = { workspace = true, optional = true } [dev-dependencies] From 08c5a641a17580fb6cfac947aaf95634018abeb7 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 12 Feb 2025 14:20:50 +0100 Subject: [PATCH 071/132] Run a frame per queued event in egui_kittest (#5704) This should fix the remaining problems with the modifiers * [x] I have followed the instructions in the PR template --- crates/egui_kittest/src/event.rs | 10 ++++------ crates/egui_kittest/src/lib.rs | 17 +++++++++++------ crates/egui_kittest/tests/tests.rs | 15 +++++++++------ 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/crates/egui_kittest/src/event.rs b/crates/egui_kittest/src/event.rs index c3045d623..e756d4dc9 100644 --- a/crates/egui_kittest/src/event.rs +++ b/crates/egui_kittest/src/event.rs @@ -8,14 +8,12 @@ pub(crate) struct EventState { } impl EventState { - /// Map the kittest events to egui events, add them to the input and update the modifiers. + /// 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, events: Vec, input: &mut egui::RawInput) { - for event in events { - if let Some(event) = self.kittest_event_to_egui(&mut input.modifiers, event) { - input.events.push(event); - } + 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); } } diff --git a/crates/egui_kittest/src/lib.rs b/crates/egui_kittest/src/lib.rs index 9bccd263e..59bd5c056 100644 --- a/crates/egui_kittest/src/lib.rs +++ b/crates/egui_kittest/src/lib.rs @@ -219,17 +219,22 @@ impl<'a, State> Harness<'a, State> { self } - /// Run a frame. - /// This will call the app closure with the queued events and current context and + /// Run a frame for each queued event (or a single frame if there are no events). + /// This will call the app closure with each queued event and /// update the Harness. pub fn step(&mut self) { - self._step(false); + let events = self.kittest.take_events(); + if events.is_empty() { + self._step(false); + } + for event in events { + self.event_state.update(event, &mut self.input); + self._step(false); + } } + /// Run a single step. This will not process any events. fn _step(&mut self, sizing_pass: bool) { - self.event_state - .update(self.kittest.take_events(), &mut self.input); - self.input.predicted_dt = self.step_dt; let mut output = self.ctx.run(self.input.take(), |ctx| { diff --git a/crates/egui_kittest/tests/tests.rs b/crates/egui_kittest/tests/tests.rs index 4fa1239a7..52f455c7b 100644 --- a/crates/egui_kittest/tests/tests.rs +++ b/crates/egui_kittest/tests/tests.rs @@ -22,6 +22,7 @@ fn test_modifiers() { struct State { cmd_clicked: bool, cmd_z_pressed: bool, + cmd_y_pressed: bool, } let mut harness = Harness::new_ui_state( |ui, state| { @@ -31,6 +32,9 @@ fn test_modifiers() { if ui.input(|i| i.modifiers.command && i.key_pressed(egui::Key::Z)) { state.cmd_z_pressed = true; } + if ui.input(|i| i.modifiers.command && i.key_pressed(egui::Key::Y)) { + state.cmd_y_pressed = true; + } }, State::default(), ); @@ -39,18 +43,17 @@ fn test_modifiers() { // 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(); - // TODO(lucasmerlin): Right now the key_up needs to happen on a separate frame or it won't register. - // This should be more intuitive - harness.run(); harness.get_by_label("Click me").key_up(Key::Command); - harness.run(); harness.press_key_modifiers(Modifiers::COMMAND, egui::Key::Z); - // TODO(lucasmerlin): This should also work (Same problem as above) - // harness.node().key_combination(&[Key::Command, Key::Z]); + harness.run(); + + harness.node().key_combination(&[Key::Command, Key::Y]); + harness.run(); let state = harness.state(); assert!(state.cmd_clicked, "The button wasn't command-clicked"); assert!(state.cmd_z_pressed, "Cmd+Z wasn't pressed"); + assert!(state.cmd_y_pressed, "Cmd+Y wasn't pressed"); } From 40f002fe3ff86061a92e8d1a96f3a7a6d4e8747c Mon Sep 17 00:00:00 2001 From: Andreas Reich Date: Tue, 18 Feb 2025 09:52:24 +0100 Subject: [PATCH 072/132] Add guidelines for image comparison tests (#5714) Guidelines & why images may differ Based on (but slightly altered): * https://github.com/rerun-io/rerun/pull/8989 --- crates/egui_kittest/README.md | 59 +++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/crates/egui_kittest/README.md b/crates/egui_kittest/README.md index 1cef1b0d6..ff071c9f4 100644 --- a/crates/egui_kittest/README.md +++ b/crates/egui_kittest/README.md @@ -55,3 +55,62 @@ You should add the following to your `.gitignore`: **/tests/snapshots/**/*.diff.png **/tests/snapshots/**/*.new.png ``` + +### Guidelines for writing snapshot tests + +* Whenever **possible** prefer regular Rust tests or `insta` snapshot tests over image comparison tests because… + * …compared to regular Rust tests, they can be relatively slow to run + * …they are brittle since unrelated side effects (like a change in color) can cause the test to fail + * …images take up repo space +* images should… + * …be checked in or otherwise be available (egui use [git LFS](https://git-lfs.com/) files for this purpose) + * …depict exactly what's tested and nothing else + * …have a low resolution to avoid growth in repo size + * …have a low comparison threshold to avoid the test passing despite unwanted differences (the default threshold should be fine for most usecases!) + +### What do do when CI / another computer produces a different image? + +The default tolerance settings should be fine for almost all gui comparison tests. +However, especially when you're using custom rendering, you may observe images difference with different setups leading to unexpected test failures. + +First check whether the difference is due to a change in enabled rendering features, potentially due to difference in hardware (/software renderer) capabilitites. +Generally you should carefully enforcing the same set of features for all test runs, but this may happen nonetheless. + +Once you validated that the differences are miniscule and hard to avoid, you can try to _carefully_ adjust the comparison tolerance setting (`SnapshotOptions::threshold`, TODO([#5683](https://github.com/emilk/egui/issues/5683)): as well as number of pixels allowed to differ) for the specific test. + +⚠️ **WARNING** ⚠️ +Picking too high tolerances may mean that you are missing actual test failures. +It is recommended to manually verify that the tests still break under the right circumstances as expected after adjusting the tolerances. + +--- + +In order to avoid image differences, it can be useful to form an understanding of how they occur in the first place. + +Discrepancies can be caused by a variety of implementation details that depend on the concrete GPU, OS, rendering backend (Metal/Vulkan/DX12 etc.) or graphics driver (even between different versions of the same driver). + +Common issues include: +* multi-sample anti-aliasing + * sample placement and sample resolve steps are implementation defined + * alpha-to-coverage algorithm/pattern can wary wildly between implementations +* texture filtering + * different implementations may apply different optimizations *even* for simple linear texture filtering +* out of bounds texture access (via `textureLoad`) + * implementations are free to return indeterminate values instead of clamping +* floating point evaluation, for details see [WGSL spec § 15.7. Floating Point Evaluation](https://www.w3.org/TR/WGSL/#floating-point-evaluation). Notably: + * rounding mode may be inconsistent + * floating point math "optimizations" may occur + * depending on output shading language, different arithmetic optimizations may be performed upon floating point operations even if they change the result + * floating point denormal flush + * even on modern implementations, denormal float values may be flushed to zero + * `NaN`/`Inf` handling + * whenever the result of a function should yield `NaN`/`Inf`, implementations may free to yield an indeterminate value instead + * builtin-function function precision & error handling (trigonometric functions and others) +* [partial derivatives (dpdx/dpdx)](https://www.w3.org/TR/WGSL/#dpdx-builtin) + * implementations are free to use either `dpdxFine` or `dpdxCoarse` +* [...] + +From this follow a few simple recommendations (these may or may not apply as they may impose unwanted restrictions on your rendering setup): +* avoid enabling mult-sample anti-aliasing whenever it's not explicitly tested or needed +* do not rely on NaN, Inf and denormal float values +* consider dedicated test paths for texture sampling +* prefer explicit partial derivative functions From 66c73b9cbfbd4d44489fc6f6a840d7d82bc34389 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Tue, 18 Feb 2025 12:01:06 +0100 Subject: [PATCH 073/132] Set hint_text in WidgetInfo (#5724) The placeholder in kittest is currently not set for TextEdit Fields. This resolves it * [x] I have followed the instructions in the PR template --- crates/egui/src/data/output.rs | 12 ++++++++++++ crates/egui/src/response.rs | 3 +++ crates/egui/src/widgets/text_edit/builder.rs | 3 +++ 3 files changed, 18 insertions(+) diff --git a/crates/egui/src/data/output.rs b/crates/egui/src/data/output.rs index e1f9086a2..2fdaec1e1 100644 --- a/crates/egui/src/data/output.rs +++ b/crates/egui/src/data/output.rs @@ -539,6 +539,9 @@ pub struct WidgetInfo { /// Selected range of characters in [`Self::current_text_value`]. pub text_selection: Option>, + + /// The hint text for text edit fields. + pub hint_text: Option, } impl std::fmt::Debug for WidgetInfo { @@ -552,6 +555,7 @@ impl std::fmt::Debug for WidgetInfo { selected, value, text_selection, + hint_text, } = self; let mut s = f.debug_struct("WidgetInfo"); @@ -580,6 +584,9 @@ impl std::fmt::Debug for WidgetInfo { if let Some(text_selection) = text_selection { s.field("text_selection", text_selection); } + if let Some(hint_text) = hint_text { + s.field("hint_text", hint_text); + } s.finish() } @@ -596,6 +603,7 @@ impl WidgetInfo { selected: None, value: None, text_selection: None, + hint_text: None, } } @@ -643,9 +651,11 @@ impl WidgetInfo { enabled: bool, prev_text_value: impl ToString, text_value: impl ToString, + hint_text: impl ToString, ) -> Self { let text_value = text_value.to_string(); let prev_text_value = prev_text_value.to_string(); + let hint_text = hint_text.to_string(); let prev_text_value = if text_value == prev_text_value { None } else { @@ -655,6 +665,7 @@ impl WidgetInfo { enabled, current_text_value: Some(text_value), prev_text_value, + hint_text: Some(hint_text), ..Self::new(WidgetType::TextEdit) } } @@ -684,6 +695,7 @@ impl WidgetInfo { selected, value, text_selection: _, + hint_text: _, } = self; // TODO(emilk): localization diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index f5861f4f1..131a420da 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -1059,6 +1059,9 @@ impl Response { // Indeterminate state builder.set_toggled(Toggled::Mixed); } + if let Some(hint_text) = info.hint_text { + builder.set_placeholder(hint_text); + } } /// Associate a label with a control for accessibility. diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 2f685bbc1..465f5568c 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -505,6 +505,7 @@ impl TextEdit<'_> { .unwrap_or_else(|| ui.visuals().widgets.inactive.text_color()); let prev_text = text.as_str().to_owned(); + let hint_text_str = hint_text.text().to_owned(); let font_id = font_selection.resolve(ui.style()); let row_height = ui.fonts(|f| f.row_height(&font_id)); @@ -807,6 +808,7 @@ impl TextEdit<'_> { ui.is_enabled(), mask_if_password(password, prev_text.as_str()), mask_if_password(password, text.as_str()), + hint_text_str.as_str(), ) }); } else if selection_changed { @@ -825,6 +827,7 @@ impl TextEdit<'_> { ui.is_enabled(), mask_if_password(password, prev_text.as_str()), mask_if_password(password, text.as_str()), + hint_text_str.as_str(), ) }); } From a8e98d3f9bb5e773d2e8a1c59b2448b2e32242a8 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Tue, 18 Feb 2025 15:53:07 +0100 Subject: [PATCH 074/132] Add `Popup` and `Tooltip`, unifying the previous behaviours (#5713) This introduces new `Tooltip` and `Popup` structs that unify and extend the old popups and tooltips. `Popup` handles the positioning and optionally stores state on whether the popup is open (for click based popups like `ComboBox`, menus, context menus). `Tooltip` is based on `Popup` and handles state of whether the tooltip should be shown (which turns out to be quite complex to handles all the edge cases). Both `Popup` and `Tooltip` can easily be constructed from a `Response` and then customized via builder methods. This also introduces `PositionAlign`, for aligning something outside of a `Rect` (in contrast to `Align2` for aligning inside a `Rect`). But I don't like the name, any suggestions? Inspired by [mui's tooltip positioning](https://mui.com/material-ui/react-tooltip/#positioned-tooltips). * Part of #4607 * [x] I have followed the instructions in the PR template TODOs: - [x] Automatic tooltip positioning based on available space - [x] Review / fix / remove all code TODOs - [x] ~Update the helper fns on `Response` to be consistent in naming and parameters (Some use tooltip, some hover_ui, some take &self, some take self)~ actually, I think the naming and parameter make sense on second thought - [x] Make sure all old code is marked deprecated For discussion during review: - the following check in `show_tooltip_for` still necessary?: ```rust let is_touch_screen = ctx.input(|i| i.any_touches()); let allow_placing_below = !is_touch_screen; // There is a finger below. TODO: Needed? ``` --- crates/egui/src/containers/area.rs | 19 +- crates/egui/src/containers/combo_box.rs | 98 +- crates/egui/src/containers/mod.rs | 6 +- crates/egui/src/containers/old_popup.rs | 211 ++++ crates/egui/src/containers/popup.rs | 931 ++++++++++-------- crates/egui/src/containers/tooltip.rs | 376 +++++++ crates/egui/src/lib.rs | 3 +- crates/egui/src/memory/mod.rs | 21 +- crates/egui/src/response.rs | 215 +--- crates/egui_demo_lib/src/demo/context_menu.rs | 17 + .../src/demo/demo_app_windows.rs | 1 + crates/egui_demo_lib/src/demo/mod.rs | 1 + crates/egui_demo_lib/src/demo/popups.rs | 181 ++++ crates/egui_demo_lib/src/demo/tooltips.rs | 3 + .../tests/snapshots/demos/Context Menus.png | 4 +- .../tests/snapshots/demos/Popups.png | 3 + .../tests/snapshots/demos/Tooltips.png | 4 +- crates/egui_kittest/tests/popup.rs | 31 + crates/emath/src/align.rs | 28 + crates/emath/src/lib.rs | 2 + crates/emath/src/rect_align.rs | 279 ++++++ examples/popups/src/main.rs | 19 +- 22 files changed, 1738 insertions(+), 715 deletions(-) create mode 100644 crates/egui/src/containers/old_popup.rs create mode 100644 crates/egui/src/containers/tooltip.rs create mode 100644 crates/egui_demo_lib/src/demo/popups.rs create mode 100644 crates/egui_demo_lib/tests/snapshots/demos/Popups.png create mode 100644 crates/egui_kittest/tests/popup.rs create mode 100644 crates/emath/src/rect_align.rs diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index 6af762561..147e4426f 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, NumExt, Order, Pos2, Rect, Response, - Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetRect, WidgetWithState, + emath, pos2, Align2, Context, Id, InnerResponse, LayerId, Layout, NumExt, Order, Pos2, Rect, + Response, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetRect, WidgetWithState, }; /// State of an [`Area`] that is persisted between frames. @@ -120,6 +120,7 @@ pub struct Area { anchor: Option<(Align2, Vec2)>, new_pos: Option, fade_in: bool, + layout: Layout, } impl WidgetWithState for Area { @@ -145,6 +146,7 @@ impl Area { pivot: Align2::LEFT_TOP, anchor: None, fade_in: true, + layout: Layout::default(), } } @@ -339,6 +341,13 @@ impl Area { self.fade_in = fade_in; self } + + /// Set the layout for the child Ui. + #[inline] + pub fn layout(mut self, layout: Layout) -> Self { + self.layout = layout; + self + } } pub(crate) struct Prepared { @@ -358,6 +367,7 @@ pub(crate) struct Prepared { sizing_pass: bool, fade_in: bool, + layout: Layout, } impl Area { @@ -390,6 +400,7 @@ impl Area { constrain, constrain_rect, fade_in, + layout, } = self; let constrain_rect = constrain_rect.unwrap_or_else(|| ctx.screen_rect()); @@ -516,6 +527,7 @@ impl Area { constrain_rect, sizing_pass, fade_in, + layout, } } } @@ -543,7 +555,8 @@ impl Prepared { let mut ui_builder = UiBuilder::new() .ui_stack_info(UiStackInfo::new(self.kind)) .layer_id(self.layer_id) - .max_rect(max_rect); + .max_rect(max_rect) + .layout(self.layout); if !self.enabled { ui_builder = ui_builder.disabled(); diff --git a/crates/egui/src/containers/combo_box.rs b/crates/egui/src/containers/combo_box.rs index 98cf0182e..884a9c36d 100644 --- a/crates/egui/src/containers/combo_box.rs +++ b/crates/egui/src/containers/combo_box.rs @@ -1,7 +1,7 @@ use epaint::Shape; use crate::{ - epaint, style::WidgetVisuals, vec2, Align2, Context, Id, InnerResponse, NumExt, Painter, + epaint, style::WidgetVisuals, vec2, Align2, Context, Id, InnerResponse, NumExt, Painter, Popup, PopupCloseBehavior, Rect, Response, ScrollArea, Sense, Stroke, TextStyle, TextWrapMode, Ui, UiBuilder, Vec2, WidgetInfo, WidgetText, WidgetType, }; @@ -9,15 +9,8 @@ use crate::{ #[allow(unused_imports)] // Documentation use crate::style::Spacing; -/// Indicate whether a popup will be shown above or below the box. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum AboveOrBelow { - Above, - Below, -} - /// A function that paints the [`ComboBox`] icon -pub type IconPainter = Box; +pub type IconPainter = Box; /// A drop-down selection menu with a descriptive label. /// @@ -135,7 +128,6 @@ impl ComboBox { /// rect: egui::Rect, /// visuals: &egui::style::WidgetVisuals, /// _is_open: bool, - /// _above_or_below: egui::AboveOrBelow, /// ) { /// let rect = egui::Rect::from_center_size( /// rect.center(), @@ -154,10 +146,8 @@ impl ComboBox { /// .show_ui(ui, |_ui| {}); /// # }); /// ``` - pub fn icon( - mut self, - icon_fn: impl FnOnce(&Ui, Rect, &WidgetVisuals, bool, AboveOrBelow) + 'static, - ) -> Self { + #[inline] + pub fn icon(mut self, icon_fn: impl FnOnce(&Ui, Rect, &WidgetVisuals, bool) + 'static) -> Self { self.icon = Some(Box::new(icon_fn)); self } @@ -322,22 +312,6 @@ fn combo_box_dyn<'c, R>( let is_popup_open = ui.memory(|m| m.is_popup_open(popup_id)); - let popup_height = ui.memory(|m| { - m.areas() - .get(popup_id) - .and_then(|state| state.size) - .map_or(100.0, |size| size.y) - }); - - let above_or_below = - if ui.next_widget_position().y + ui.spacing().interact_size.y + popup_height - < ui.ctx().screen_rect().bottom() - { - AboveOrBelow::Below - } else { - AboveOrBelow::Above - }; - let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode()); let close_behavior = close_behavior.unwrap_or(PopupCloseBehavior::CloseOnClick); @@ -385,15 +359,9 @@ fn combo_box_dyn<'c, R>( icon_rect.expand(visuals.expansion), visuals, is_popup_open, - above_or_below, ); } else { - paint_default_icon( - ui.painter(), - icon_rect.expand(visuals.expansion), - visuals, - above_or_below, - ); + paint_default_icon(ui.painter(), icon_rect.expand(visuals.expansion), visuals); } let text_rect = Align2::LEFT_CENTER.align_size_within_rect(galley.size(), rect); @@ -402,19 +370,15 @@ fn combo_box_dyn<'c, R>( } }); - if button_response.clicked() { - ui.memory_mut(|mem| mem.toggle_popup(popup_id)); - } - let height = height.unwrap_or_else(|| ui.spacing().combo_height); - let inner = crate::popup::popup_above_or_below_widget( - ui, - popup_id, - &button_response, - above_or_below, - close_behavior, - |ui| { + let inner = Popup::menu(&button_response) + .id(popup_id) + .width(button_response.rect.width()) + .close_behavior(close_behavior) + .show(|ui| { + ui.set_min_width(ui.available_width()); + ScrollArea::vertical() .max_height(height) .show(ui, |ui| { @@ -427,8 +391,8 @@ fn combo_box_dyn<'c, R>( menu_contents(ui) }) .inner - }, - ); + }) + .map(|r| r.inner); InnerResponse { inner, @@ -484,33 +448,19 @@ fn button_frame( response } -fn paint_default_icon( - painter: &Painter, - rect: Rect, - visuals: &WidgetVisuals, - above_or_below: AboveOrBelow, -) { +fn paint_default_icon(painter: &Painter, rect: Rect, visuals: &WidgetVisuals) { let rect = Rect::from_center_size( rect.center(), vec2(rect.width() * 0.7, rect.height() * 0.45), ); - match above_or_below { - AboveOrBelow::Above => { - // Upward pointing triangle - painter.add(Shape::convex_polygon( - vec![rect.left_bottom(), rect.right_bottom(), rect.center_top()], - visuals.fg_stroke.color, - Stroke::NONE, - )); - } - AboveOrBelow::Below => { - // Downward pointing triangle - painter.add(Shape::convex_polygon( - vec![rect.left_top(), rect.right_top(), rect.center_bottom()], - visuals.fg_stroke.color, - Stroke::NONE, - )); - } - } + // Downward pointing triangle + // Previously, we would show an up arrow when we expected the popup to open upwards + // (due to lack of space below the button), but this could look weird in edge cases, so this + // feature was removed. (See https://github.com/emilk/egui/pull/5713#issuecomment-2654420245) + painter.add(Shape::convex_polygon( + vec![rect.left_top(), rect.right_top(), rect.center_bottom()], + visuals.fg_stroke.color, + Stroke::NONE, + )); } diff --git a/crates/egui/src/containers/mod.rs b/crates/egui/src/containers/mod.rs index abb444598..0d9587e62 100644 --- a/crates/egui/src/containers/mod.rs +++ b/crates/egui/src/containers/mod.rs @@ -7,12 +7,14 @@ pub mod collapsing_header; mod combo_box; pub mod frame; pub mod modal; +pub mod old_popup; pub mod panel; -pub mod popup; +mod popup; pub(crate) mod resize; mod scene; pub mod scroll_area; mod sides; +mod tooltip; pub(crate) mod window; pub use { @@ -21,11 +23,13 @@ pub use { combo_box::*, frame::Frame, modal::{Modal, ModalResponse}, + old_popup::*, panel::{CentralPanel, SidePanel, TopBottomPanel}, popup::*, resize::Resize, scene::Scene, scroll_area::ScrollArea, sides::Sides, + tooltip::*, window::Window, }; diff --git a/crates/egui/src/containers/old_popup.rs b/crates/egui/src/containers/old_popup.rs new file mode 100644 index 000000000..c803ecf42 --- /dev/null +++ b/crates/egui/src/containers/old_popup.rs @@ -0,0 +1,211 @@ +//! Old and deprecated API for popups. Use [`Popup`] instead. +#![allow(deprecated)] + +use crate::containers::tooltip::Tooltip; +use crate::{ + Align, Context, Id, LayerId, Layout, Popup, PopupAnchor, PopupCloseBehavior, Pos2, Rect, + Response, Ui, Widget, WidgetText, +}; +use emath::RectAlign; +// ---------------------------------------------------------------------------- + +/// Show a tooltip at the current pointer position (if any). +/// +/// Most of the time it is easier to use [`Response::on_hover_ui`]. +/// +/// See also [`show_tooltip_text`]. +/// +/// Returns `None` if the tooltip could not be placed. +/// +/// ``` +/// # egui::__run_test_ui(|ui| { +/// # #[allow(deprecated)] +/// if ui.ui_contains_pointer() { +/// egui::show_tooltip(ui.ctx(), ui.layer_id(), egui::Id::new("my_tooltip"), |ui| { +/// ui.label("Helpful text"); +/// }); +/// } +/// # }); +/// ``` +#[deprecated = "Use `egui::Tooltip` instead"] +pub fn show_tooltip( + ctx: &Context, + parent_layer: LayerId, + widget_id: Id, + add_contents: impl FnOnce(&mut Ui) -> R, +) -> Option { + show_tooltip_at_pointer(ctx, parent_layer, widget_id, add_contents) +} + +/// Show a tooltip at the current pointer position (if any). +/// +/// Most of the time it is easier to use [`Response::on_hover_ui`]. +/// +/// See also [`show_tooltip_text`]. +/// +/// Returns `None` if the tooltip could not be placed. +/// +/// ``` +/// # egui::__run_test_ui(|ui| { +/// if ui.ui_contains_pointer() { +/// egui::show_tooltip_at_pointer(ui.ctx(), ui.layer_id(), egui::Id::new("my_tooltip"), |ui| { +/// ui.label("Helpful text"); +/// }); +/// } +/// # }); +/// ``` +#[deprecated = "Use `egui::Tooltip` instead"] +pub fn show_tooltip_at_pointer( + ctx: &Context, + parent_layer: LayerId, + widget_id: Id, + add_contents: impl FnOnce(&mut Ui) -> R, +) -> Option { + Tooltip::new(widget_id, ctx.clone(), PopupAnchor::Pointer, parent_layer) + .gap(12.0) + .show(add_contents) + .map(|response| response.inner) +} + +/// Show a tooltip under the given area. +/// +/// If the tooltip does not fit under the area, it tries to place it above it instead. +#[deprecated = "Use `egui::Tooltip` instead"] +pub fn show_tooltip_for( + ctx: &Context, + parent_layer: LayerId, + widget_id: Id, + widget_rect: &Rect, + add_contents: impl FnOnce(&mut Ui) -> R, +) -> Option { + Tooltip::new(widget_id, ctx.clone(), *widget_rect, parent_layer) + .show(add_contents) + .map(|response| response.inner) +} + +/// Show a tooltip at the given position. +/// +/// Returns `None` if the tooltip could not be placed. +#[deprecated = "Use `egui::Tooltip` instead"] +pub fn show_tooltip_at( + ctx: &Context, + parent_layer: LayerId, + widget_id: Id, + suggested_position: Pos2, + add_contents: impl FnOnce(&mut Ui) -> R, +) -> Option { + Tooltip::new(widget_id, ctx.clone(), suggested_position, parent_layer) + .show(add_contents) + .map(|response| response.inner) +} + +/// Show some text at the current pointer position (if any). +/// +/// Most of the time it is easier to use [`Response::on_hover_text`]. +/// +/// See also [`show_tooltip`]. +/// +/// Returns `None` if the tooltip could not be placed. +/// +/// ``` +/// # egui::__run_test_ui(|ui| { +/// if ui.ui_contains_pointer() { +/// egui::show_tooltip_text(ui.ctx(), ui.layer_id(), egui::Id::new("my_tooltip"), "Helpful text"); +/// } +/// # }); +/// ``` +#[deprecated = "Use `egui::Tooltip` instead"] +pub fn show_tooltip_text( + ctx: &Context, + parent_layer: LayerId, + widget_id: Id, + text: impl Into, +) -> Option<()> { + show_tooltip(ctx, parent_layer, widget_id, |ui| { + crate::widgets::Label::new(text).ui(ui); + }) +} + +/// Was this tooltip visible last frame? +#[deprecated = "Use `Tooltip::was_tooltip_open_last_frame` instead"] +pub fn was_tooltip_open_last_frame(ctx: &Context, widget_id: Id) -> bool { + Tooltip::was_tooltip_open_last_frame(ctx, widget_id) +} + +/// Indicate whether a popup will be shown above or below the box. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum AboveOrBelow { + Above, + Below, +} + +/// Helper for [`popup_above_or_below_widget`]. +#[deprecated = "Use `egui::Popup` instead"] +pub fn popup_below_widget( + ui: &Ui, + popup_id: Id, + widget_response: &Response, + close_behavior: PopupCloseBehavior, + add_contents: impl FnOnce(&mut Ui) -> R, +) -> Option { + popup_above_or_below_widget( + ui, + popup_id, + widget_response, + AboveOrBelow::Below, + close_behavior, + add_contents, + ) +} + +/// Shows a popup above or below another widget. +/// +/// Useful for drop-down menus (combo boxes) or suggestion menus under text fields. +/// +/// The opened popup will have a minimum width matching its parent. +/// +/// You must open the popup with [`crate::Memory::open_popup`] or [`crate::Memory::toggle_popup`]. +/// +/// Returns `None` if the popup is not open. +/// +/// ``` +/// # egui::__run_test_ui(|ui| { +/// let response = ui.button("Open popup"); +/// let popup_id = ui.make_persistent_id("my_unique_id"); +/// if response.clicked() { +/// ui.memory_mut(|mem| mem.toggle_popup(popup_id)); +/// } +/// let below = egui::AboveOrBelow::Below; +/// let close_on_click_outside = egui::PopupCloseBehavior::CloseOnClickOutside; +/// # #[allow(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:"); +/// ui.label("…"); +/// }); +/// # }); +/// ``` +#[deprecated = "Use `egui::Popup` instead"] +pub fn popup_above_or_below_widget( + _parent_ui: &Ui, + popup_id: Id, + widget_response: &Response, + above_or_below: AboveOrBelow, + close_behavior: PopupCloseBehavior, + add_contents: impl FnOnce(&mut Ui) -> R, +) -> Option { + let response = Popup::from_response(widget_response) + .layout(Layout::top_down_justified(Align::LEFT)) + .open_memory(None, close_behavior) + .id(popup_id) + .align(match above_or_below { + AboveOrBelow::Above => RectAlign::TOP_START, + AboveOrBelow::Below => RectAlign::BOTTOM_START, + }) + .width(widget_response.rect.width()) + .show(|ui| { + ui.set_min_width(ui.available_width()); + add_contents(ui) + })?; + Some(response.inner) +} diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index 81bf84a2f..877ee4202 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -1,334 +1,75 @@ -//! Show popup windows, tooltips, context menus etc. - -use pass_state::PerWidgetTooltipState; - use crate::{ - pass_state, vec2, AboveOrBelow, Align, Align2, Area, AreaState, Context, Frame, Id, - InnerResponse, Key, LayerId, Layout, Order, Pos2, Rect, Response, Sense, Ui, UiKind, Vec2, - Widget, WidgetText, + Area, AreaState, Context, Frame, Id, InnerResponse, Key, LayerId, Layout, Order, Response, + Sense, Ui, UiKind, }; +use emath::{vec2, Align, Pos2, Rect, RectAlign, Vec2}; +use std::iter::once; -// ---------------------------------------------------------------------------- +/// What should we anchor the popup to? +/// The final position for the popup will be calculated based on [`RectAlign`] +/// and can be customized with [`Popup::align`] and [`Popup::align_alternatives`]. +/// [`PopupAnchor`] is the parent rect of [`RectAlign`]. +/// +/// For [`PopupAnchor::Pointer`], [`PopupAnchor::PointerFixed`] and [`PopupAnchor::Position`], +/// the rect will be derived via [`Rect::from_pos`] (so a zero-sized rect at the given position). +/// +/// The rect should be in global coordinates. `PopupAnchor::from(&response)` will automatically +/// do this conversion. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PopupAnchor { + /// Show the popup relative to some parent [`Rect`]. + ParentRect(Rect), -fn when_was_a_toolip_last_shown_id() -> Id { - Id::new("when_was_a_toolip_last_shown") + /// Show the popup relative to the mouse pointer. + Pointer, + + /// Remember the mouse position and show the popup relative to that (like a context menu). + PointerFixed, + + /// Show the popup relative to a specific position. + Position(Pos2), } -pub fn seconds_since_last_tooltip(ctx: &Context) -> f32 { - let when_was_a_toolip_last_shown = - ctx.data(|d| d.get_temp::(when_was_a_toolip_last_shown_id())); - - if let Some(when_was_a_toolip_last_shown) = when_was_a_toolip_last_shown { - let now = ctx.input(|i| i.time); - (now - when_was_a_toolip_last_shown) as f32 - } else { - f32::INFINITY +impl From for PopupAnchor { + fn from(rect: Rect) -> Self { + Self::ParentRect(rect) } } -fn remember_that_tooltip_was_shown(ctx: &Context) { - let now = ctx.input(|i| i.time); - ctx.data_mut(|data| data.insert_temp::(when_was_a_toolip_last_shown_id(), now)); +impl From for PopupAnchor { + fn from(pos: Pos2) -> Self { + Self::Position(pos) + } } -// ---------------------------------------------------------------------------- - -/// Show a tooltip at the current pointer position (if any). -/// -/// Most of the time it is easier to use [`Response::on_hover_ui`]. -/// -/// See also [`show_tooltip_text`]. -/// -/// Returns `None` if the tooltip could not be placed. -/// -/// ``` -/// # egui::__run_test_ui(|ui| { -/// if ui.ui_contains_pointer() { -/// egui::show_tooltip(ui.ctx(), ui.layer_id(), egui::Id::new("my_tooltip"), |ui| { -/// ui.label("Helpful text"); -/// }); -/// } -/// # }); -/// ``` -pub fn show_tooltip( - ctx: &Context, - parent_layer: LayerId, - widget_id: Id, - add_contents: impl FnOnce(&mut Ui) -> R, -) -> Option { - show_tooltip_at_pointer(ctx, parent_layer, widget_id, add_contents) -} - -/// Show a tooltip at the current pointer position (if any). -/// -/// Most of the time it is easier to use [`Response::on_hover_ui`]. -/// -/// See also [`show_tooltip_text`]. -/// -/// Returns `None` if the tooltip could not be placed. -/// -/// ``` -/// # egui::__run_test_ui(|ui| { -/// if ui.ui_contains_pointer() { -/// egui::show_tooltip_at_pointer(ui.ctx(), ui.layer_id(), egui::Id::new("my_tooltip"), |ui| { -/// ui.label("Helpful text"); -/// }); -/// } -/// # }); -/// ``` -pub fn show_tooltip_at_pointer( - ctx: &Context, - parent_layer: LayerId, - widget_id: Id, - add_contents: impl FnOnce(&mut Ui) -> R, -) -> Option { - ctx.input(|i| i.pointer.hover_pos()).map(|pointer_pos| { - let allow_placing_below = true; - - // Add a small exclusion zone around the pointer to avoid tooltips - // covering what we're hovering over. - let mut pointer_rect = Rect::from_center_size(pointer_pos, Vec2::splat(24.0)); - - // Keep the left edge of the tooltip in line with the cursor: - pointer_rect.min.x = pointer_pos.x; - - // Transform global coords to layer coords: - if let Some(from_global) = ctx.layer_transform_from_global(parent_layer) { - pointer_rect = from_global * pointer_rect; +impl From<&Response> for PopupAnchor { + fn from(response: &Response) -> Self { + let mut widget_rect = response.rect; + if let Some(to_global) = response.ctx.layer_transform_to_global(response.layer_id) { + widget_rect = to_global * widget_rect; } - - show_tooltip_at_dyn( - ctx, - parent_layer, - widget_id, - allow_placing_below, - &pointer_rect, - Box::new(add_contents), - ) - }) -} - -/// Show a tooltip under the given area. -/// -/// If the tooltip does not fit under the area, it tries to place it above it instead. -pub fn show_tooltip_for( - ctx: &Context, - parent_layer: LayerId, - widget_id: Id, - widget_rect: &Rect, - add_contents: impl FnOnce(&mut Ui) -> R, -) -> R { - let is_touch_screen = ctx.input(|i| i.any_touches()); - let allow_placing_below = !is_touch_screen; // There is a finger below. - show_tooltip_at_dyn( - ctx, - parent_layer, - widget_id, - allow_placing_below, - widget_rect, - Box::new(add_contents), - ) -} - -/// Show a tooltip at the given position. -/// -/// Returns `None` if the tooltip could not be placed. -pub fn show_tooltip_at( - ctx: &Context, - parent_layer: LayerId, - widget_id: Id, - suggested_position: Pos2, - add_contents: impl FnOnce(&mut Ui) -> R, -) -> R { - let allow_placing_below = true; - let rect = Rect::from_center_size(suggested_position, Vec2::ZERO); - show_tooltip_at_dyn( - ctx, - parent_layer, - widget_id, - allow_placing_below, - &rect, - Box::new(add_contents), - ) -} - -fn show_tooltip_at_dyn<'c, R>( - ctx: &Context, - parent_layer: LayerId, - widget_id: Id, - allow_placing_below: bool, - widget_rect: &Rect, - add_contents: Box R + 'c>, -) -> R { - // Transform layer coords to global coords: - let mut widget_rect = *widget_rect; - if let Some(to_global) = ctx.layer_transform_to_global(parent_layer) { - widget_rect = to_global * widget_rect; + Self::ParentRect(widget_rect) } - - remember_that_tooltip_was_shown(ctx); - - let mut state = ctx.pass_state_mut(|fs| { - // Remember that this is the widget showing the tooltip: - fs.layers - .entry(parent_layer) - .or_default() - .widget_with_tooltip = Some(widget_id); - - fs.tooltips - .widget_tooltips - .get(&widget_id) - .copied() - .unwrap_or(PerWidgetTooltipState { - bounding_rect: widget_rect, - tooltip_count: 0, - }) - }); - - let tooltip_area_id = tooltip_id(widget_id, state.tooltip_count); - let expected_tooltip_size = AreaState::load(ctx, tooltip_area_id) - .and_then(|area| area.size) - .unwrap_or(vec2(64.0, 32.0)); - - let screen_rect = ctx.screen_rect(); - - let (pivot, anchor) = find_tooltip_position( - screen_rect, - state.bounding_rect, - allow_placing_below, - expected_tooltip_size, - ); - - let InnerResponse { inner, response } = Area::new(tooltip_area_id) - .kind(UiKind::Popup) - .order(Order::Tooltip) - .pivot(pivot) - .fixed_pos(anchor) - .default_width(ctx.style().spacing.tooltip_width) - .sense(Sense::hover()) // don't click to bring to front - .show(ctx, |ui| { - // By default the text in tooltips aren't selectable. - // This means that most tooltips aren't interactable, - // which also mean they won't stick around so you can click them. - // Only tooltips that have actual interactive stuff (buttons, links, …) - // will stick around when you try to click them. - ui.style_mut().interaction.selectable_labels = false; - - Frame::popup(&ctx.style()).show_dyn(ui, add_contents).inner - }); - - state.tooltip_count += 1; - state.bounding_rect = state.bounding_rect.union(response.rect); - ctx.pass_state_mut(|fs| fs.tooltips.widget_tooltips.insert(widget_id, state)); - - inner } -/// What is the id of the next tooltip for this widget? -pub fn next_tooltip_id(ctx: &Context, widget_id: Id) -> Id { - let tooltip_count = ctx.pass_state(|fs| { - fs.tooltips - .widget_tooltips - .get(&widget_id) - .map_or(0, |state| state.tooltip_count) - }); - tooltip_id(widget_id, tooltip_count) -} - -pub fn tooltip_id(widget_id: Id, tooltip_count: usize) -> Id { - widget_id.with(tooltip_count) -} - -/// Returns `(PIVOT, POS)` to mean: put the `PIVOT` corner of the tooltip at `POS`. -/// -/// Note: the position might need to be constrained to the screen, -/// (e.g. moved sideways if shown under the widget) -/// but the `Area` will take care of that. -fn find_tooltip_position( - screen_rect: Rect, - widget_rect: Rect, - allow_placing_below: bool, - tooltip_size: Vec2, -) -> (Align2, Pos2) { - let spacing = 4.0; - - // Does it fit below? - if allow_placing_below - && widget_rect.bottom() + spacing + tooltip_size.y <= screen_rect.bottom() - { - return ( - Align2::LEFT_TOP, - widget_rect.left_bottom() + spacing * Vec2::DOWN, - ); +impl PopupAnchor { + /// Get the rect the popup should be shown relative to. + /// Returns `Rect::from_pos` for [`PopupAnchor::Pointer`], [`PopupAnchor::PointerFixed`] + /// and [`PopupAnchor::Position`] (so the rect will be zero-sized). + pub fn rect(self, popup_id: Id, ctx: &Context) -> Option { + match self { + Self::ParentRect(rect) => Some(rect), + Self::Pointer => ctx.pointer_hover_pos().map(Rect::from_pos), + Self::PointerFixed => ctx + .memory(|mem| mem.popup_position(popup_id)) + .map(Rect::from_pos), + Self::Position(pos) => Some(Rect::from_pos(pos)), + } } - - // Does it fit above? - if screen_rect.top() + tooltip_size.y + spacing <= widget_rect.top() { - return ( - Align2::LEFT_BOTTOM, - widget_rect.left_top() + spacing * Vec2::UP, - ); - } - - // Does it fit to the right? - if widget_rect.right() + spacing + tooltip_size.x <= screen_rect.right() { - return ( - Align2::LEFT_TOP, - widget_rect.right_top() + spacing * Vec2::RIGHT, - ); - } - - // Does it fit to the left? - if screen_rect.left() + tooltip_size.x + spacing <= widget_rect.left() { - return ( - Align2::RIGHT_TOP, - widget_rect.left_top() + spacing * Vec2::LEFT, - ); - } - - // It doesn't fit anywhere :( - - // Just show it anyway: - (Align2::LEFT_TOP, screen_rect.left_top()) -} - -/// Show some text at the current pointer position (if any). -/// -/// Most of the time it is easier to use [`Response::on_hover_text`]. -/// -/// See also [`show_tooltip`]. -/// -/// Returns `None` if the tooltip could not be placed. -/// -/// ``` -/// # egui::__run_test_ui(|ui| { -/// if ui.ui_contains_pointer() { -/// egui::show_tooltip_text(ui.ctx(), ui.layer_id(), egui::Id::new("my_tooltip"), "Helpful text"); -/// } -/// # }); -/// ``` -pub fn show_tooltip_text( - ctx: &Context, - parent_layer: LayerId, - widget_id: Id, - text: impl Into, -) -> Option<()> { - show_tooltip(ctx, parent_layer, widget_id, |ui| { - crate::widgets::Label::new(text).ui(ui); - }) -} - -/// Was this popup visible last frame? -pub fn was_tooltip_open_last_frame(ctx: &Context, widget_id: Id) -> bool { - let primary_tooltip_area_id = tooltip_id(widget_id, 0); - ctx.memory(|mem| { - mem.areas() - .visible_last_frame(&LayerId::new(Order::Tooltip, primary_tooltip_area_id)) - }) } /// Determines popup's close behavior -#[derive(Clone, Copy)] +#[derive(Clone, Copy, PartialEq, Eq)] pub enum PopupCloseBehavior { /// Popup will be closed on click anywhere, inside or outside the popup. /// @@ -344,114 +85,480 @@ pub enum PopupCloseBehavior { IgnoreClicks, } -/// Helper for [`popup_above_or_below_widget`]. -pub fn popup_below_widget( - ui: &Ui, - popup_id: Id, - widget_response: &Response, - close_behavior: PopupCloseBehavior, - add_contents: impl FnOnce(&mut Ui) -> R, -) -> Option { - popup_above_or_below_widget( - ui, - popup_id, - widget_response, - AboveOrBelow::Below, - close_behavior, - add_contents, - ) +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SetOpenCommand { + /// Set the open state to the given value + Bool(bool), + + /// Toggle the open state + Toggle, } -/// Shows a popup above or below another widget. -/// -/// Useful for drop-down menus (combo boxes) or suggestion menus under text fields. -/// -/// The opened popup will have a minimum width matching its parent. -/// -/// You must open the popup with [`crate::Memory::open_popup`] or [`crate::Memory::toggle_popup`]. -/// -/// Returns `None` if the popup is not open. -/// -/// ``` -/// # egui::__run_test_ui(|ui| { -/// let response = ui.button("Open popup"); -/// let popup_id = ui.make_persistent_id("my_unique_id"); -/// if response.clicked() { -/// ui.memory_mut(|mem| mem.toggle_popup(popup_id)); -/// } -/// let below = egui::AboveOrBelow::Below; -/// let close_on_click_outside = egui::popup::PopupCloseBehavior::CloseOnClickOutside; -/// egui::popup::popup_above_or_below_widget(ui, popup_id, &response, below, close_on_click_outside, |ui| { -/// ui.set_min_width(200.0); // if you want to control the size -/// ui.label("Some more info, or things you can select:"); -/// ui.label("…"); -/// }); -/// # }); -/// ``` -pub fn popup_above_or_below_widget( - parent_ui: &Ui, - popup_id: Id, - widget_response: &Response, - above_or_below: AboveOrBelow, - close_behavior: PopupCloseBehavior, - add_contents: impl FnOnce(&mut Ui) -> R, -) -> Option { - if !parent_ui.memory(|mem| mem.is_popup_open(popup_id)) { - return None; +impl From for SetOpenCommand { + fn from(b: bool) -> Self { + Self::Bool(b) } +} - let (mut pos, pivot) = match above_or_below { - AboveOrBelow::Above => (widget_response.rect.left_top(), Align2::LEFT_BOTTOM), - AboveOrBelow::Below => (widget_response.rect.left_bottom(), Align2::LEFT_TOP), - }; +/// How do we determine if the popup should be open or closed +enum OpenKind<'a> { + /// Always open + Open, - if let Some(to_global) = parent_ui - .ctx() - .layer_transform_to_global(parent_ui.layer_id()) - { - pos = to_global * pos; - } + /// Always closed + Closed, - let frame = Frame::popup(parent_ui.style()); - let frame_margin = frame.total_margin(); - let inner_width = (widget_response.rect.width() - frame_margin.sum().x).max(0.0); + /// Open if the bool is true + Bool(&'a mut bool, PopupCloseBehavior), - parent_ui.ctx().pass_state_mut(|fs| { - fs.layers - .entry(parent_ui.layer_id()) - .or_default() - .open_popups - .insert(popup_id) - }); + /// Store the open state via [`crate::Memory`] + Memory { + set: Option, + close_behavior: PopupCloseBehavior, + }, +} - let response = Area::new(popup_id) - .kind(UiKind::Popup) - .order(Order::Foreground) - .fixed_pos(pos) - .default_width(inner_width) - .pivot(pivot) - .show(parent_ui.ctx(), |ui| { - frame - .show(ui, |ui| { - ui.with_layout(Layout::top_down_justified(Align::LEFT), |ui| { - ui.set_min_width(inner_width); - add_contents(ui) - }) - .inner - }) - .inner - }); - - let should_close = match close_behavior { - PopupCloseBehavior::CloseOnClick => widget_response.clicked_elsewhere(), - PopupCloseBehavior::CloseOnClickOutside => { - widget_response.clicked_elsewhere() && response.response.clicked_elsewhere() +impl<'a> OpenKind<'a> { + /// Returns `true` if the popup should be open + fn is_open(&self, id: Id, ctx: &Context) -> bool { + match self { + OpenKind::Open => true, + OpenKind::Closed => false, + OpenKind::Bool(open, _) => **open, + OpenKind::Memory { .. } => ctx.memory(|mem| mem.is_popup_open(id)), } - PopupCloseBehavior::IgnoreClicks => false, - }; - - if parent_ui.input(|i| i.key_pressed(Key::Escape)) || should_close { - parent_ui.memory_mut(|mem| mem.close_popup()); } - Some(response.inner) +} + +/// Is the popup a popup, tooltip or menu? +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PopupKind { + Popup, + Tooltip, + Menu, +} + +pub struct Popup<'a> { + id: Id, + ctx: Context, + anchor: PopupAnchor, + rect_align: RectAlign, + alternative_aligns: Option<&'a [RectAlign]>, + layer_id: LayerId, + open_kind: OpenKind<'a>, + kind: PopupKind, + + /// Gap between the anchor and the popup + gap: f32, + + /// Used later depending on close behavior + widget_clicked_elsewhere: bool, + + /// Default width passed to the Area + width: Option, + sense: Sense, + layout: Layout, + frame: Option, +} + +impl<'a> Popup<'a> { + /// Create a new popup + pub fn new(id: Id, ctx: Context, anchor: impl Into, layer_id: LayerId) -> Self { + Self { + id, + ctx, + anchor: anchor.into(), + open_kind: OpenKind::Open, + kind: PopupKind::Popup, + layer_id, + rect_align: RectAlign::BOTTOM_START, + alternative_aligns: None, + gap: 0.0, + widget_clicked_elsewhere: false, + width: None, + sense: Sense::click(), + layout: Layout::default(), + frame: None, + } + } + + /// Set the kind of the popup. Used for [`Area::kind`] and [`Area::order`]. + #[inline] + pub fn kind(mut self, kind: PopupKind) -> Self { + self.kind = kind; + self + } + + /// Set the [`RectAlign`] of the popup relative to the [`PopupAnchor`]. + /// This is the default position, and will be used if it fits. + /// See [`Self::align_alternatives`] for more on this. + #[inline] + pub fn align(mut self, position_align: RectAlign) -> Self { + self.rect_align = position_align; + self + } + + /// Set alternative positions to try if the default one doesn't fit. Set to an empty slice to + /// always use the position you set with [`Self::align`]. + /// By default, this will try [`RectAlign::symmetries`] and then [`RectAlign::MENU_ALIGNS`]. + #[inline] + pub fn align_alternatives(mut self, alternatives: &'a [RectAlign]) -> Self { + self.alternative_aligns = Some(alternatives); + self + } + + /// Show a popup relative to some widget. + /// The popup will be always open. + /// + /// See [`Self::menu`] and [`Self::context_menu`] for common use cases. + pub fn from_response(response: &Response) -> Self { + let mut popup = Self::new( + response.id.with("popup"), + response.ctx.clone(), + response, + response.layer_id, + ); + popup.widget_clicked_elsewhere = response.clicked_elsewhere(); + popup + } + + /// Show a popup when the widget was clicked. + /// Sets the layout to `Layout::top_down_justified(Align::Min)`. + pub fn menu(response: &Response) -> Self { + Self::from_response(response) + .open_memory( + if response.clicked() { + Some(SetOpenCommand::Toggle) + } else { + None + }, + PopupCloseBehavior::CloseOnClick, + ) + .layout(Layout::top_down_justified(Align::Min)) + } + + /// Show a context menu when the widget was secondary clicked. + /// Sets the layout to `Layout::top_down_justified(Align::Min)`. + /// In contrast to [`Self::menu`], this will open at the pointer position. + pub fn context_menu(response: &Response) -> Self { + Self::from_response(response) + .open_memory( + response + .secondary_clicked() + .then_some(SetOpenCommand::Bool(true)), + PopupCloseBehavior::CloseOnClick, + ) + .layout(Layout::top_down_justified(Align::Min)) + .at_pointer_fixed() + } + + /// Force the popup to be open or closed. + #[inline] + pub fn open(mut self, open: bool) -> Self { + self.open_kind = if open { + OpenKind::Open + } else { + OpenKind::Closed + }; + self + } + + /// Store the open state via [`crate::Memory`]. + /// You can set the state via the first [`SetOpenCommand`] param. + #[inline] + pub fn open_memory( + mut self, + set_state: impl Into>, + close_behavior: PopupCloseBehavior, + ) -> Self { + self.open_kind = OpenKind::Memory { + set: set_state.into(), + close_behavior, + }; + self + } + + /// Store the open state via a mutable bool. + #[inline] + pub fn open_bool(mut self, open: &'a mut bool, close_behavior: PopupCloseBehavior) -> Self { + self.open_kind = OpenKind::Bool(open, close_behavior); + self + } + + /// Set the close behavior of the popup. + /// + /// This will do nothing if [`Popup::open`] was called. + #[inline] + pub fn close_behavior(mut self, close_behavior: PopupCloseBehavior) -> Self { + match &mut self.open_kind { + OpenKind::Memory { + close_behavior: behavior, + .. + } + | OpenKind::Bool(_, behavior) => { + *behavior = close_behavior; + } + _ => {} + } + self + } + + /// Show the popup relative to the pointer. + #[inline] + pub fn at_pointer(mut self) -> Self { + self.anchor = PopupAnchor::Pointer; + self + } + + /// Remember the pointer position at the time of opening the popup, and show the popup + /// relative to that. + #[inline] + pub fn at_pointer_fixed(mut self) -> Self { + self.anchor = PopupAnchor::PointerFixed; + self + } + + /// Show the popup relative to a specific position. + #[inline] + pub fn at_position(mut self, position: Pos2) -> Self { + self.anchor = PopupAnchor::Position(position); + self + } + + /// Show the popup relative to the given [`PopupAnchor`]. + #[inline] + pub fn anchor(mut self, anchor: impl Into) -> Self { + self.anchor = anchor.into(); + self + } + + /// Set the gap between the anchor and the popup. + #[inline] + pub fn gap(mut self, gap: f32) -> Self { + self.gap = gap; + self + } + + /// Set the sense of the popup. + #[inline] + pub fn sense(mut self, sense: Sense) -> Self { + self.sense = sense; + self + } + + /// Set the layout of the popup. + #[inline] + pub fn layout(mut self, layout: Layout) -> Self { + self.layout = layout; + self + } + + /// The width that will be passed to [`Area::default_width`]. + #[inline] + pub fn width(mut self, width: f32) -> Self { + self.width = Some(width); + self + } + + /// Set the id of the Area. + #[inline] + pub fn id(mut self, id: Id) -> Self { + self.id = id; + self + } + + /// Get the [`Context`] + pub fn ctx(&self) -> &Context { + &self.ctx + } + + /// Return the [`PopupAnchor`] of the popup. + pub fn get_anchor(&self) -> PopupAnchor { + self.anchor + } + + /// Return the anchor rect of the popup. + /// + /// Returns `None` if the anchor is [`PopupAnchor::Pointer`] and there is no pointer. + pub fn get_anchor_rect(&self) -> Option { + self.anchor.rect(self.id, &self.ctx) + } + + /// Get the expected rect the popup will be shown in. + /// + /// Returns `None` if the popup wasn't shown before or anchor is `PopupAnchor::Pointer` and + /// there is no pointer. + pub fn get_popup_rect(&self) -> Option { + let size = self.get_expected_size(); + if let Some(size) = size { + self.get_anchor_rect() + .map(|anchor| self.get_best_align().align_rect(&anchor, size, self.gap)) + } else { + None + } + } + + /// Get the id of the popup. + pub fn get_id(&self) -> Id { + self.id + } + + /// Is the popup open? + pub fn is_open(&self) -> bool { + match &self.open_kind { + OpenKind::Open => true, + OpenKind::Closed => false, + OpenKind::Bool(open, _) => **open, + OpenKind::Memory { .. } => self.ctx.memory(|mem| mem.is_popup_open(self.id)), + } + } + + pub fn get_expected_size(&self) -> Option { + AreaState::load(&self.ctx, self.id).and_then(|area| area.size) + } + + /// Calculate the best alignment for the popup, based on the last size and screen rect. + pub fn get_best_align(&self) -> RectAlign { + let expected_popup_size = self + .get_expected_size() + .unwrap_or(vec2(self.width.unwrap_or(0.0), 0.0)); + + let Some(anchor_rect) = self.anchor.rect(self.id, &self.ctx) else { + return self.rect_align; + }; + + RectAlign::find_best_align( + #[allow(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 + .map(|a| a.iter().copied().chain([].iter().copied())) + .unwrap_or( + self.rect_align + .symmetries() + .iter() + .copied() + .chain(RectAlign::MENU_ALIGNS.iter().copied()), + ), + ), + self.ctx.screen_rect(), + anchor_rect, + self.gap, + expected_popup_size, + ) + } + + /// Show the popup. + /// Returns `None` if the popup is not open or anchor is `PopupAnchor::Pointer` and there is + /// no pointer. + pub fn show(self, content: impl FnOnce(&mut Ui) -> R) -> Option> { + let best_align = self.get_best_align(); + + let Popup { + id, + ctx, + anchor, + open_kind, + kind, + layer_id, + rect_align: _, + alternative_aligns: _, + gap, + widget_clicked_elsewhere, + width, + sense, + layout, + frame, + } = self; + + let hover_pos = ctx.pointer_hover_pos(); + if let OpenKind::Memory { set, .. } = open_kind { + ctx.memory_mut(|mem| match set { + Some(SetOpenCommand::Bool(open)) => { + if open { + match self.anchor { + PopupAnchor::PointerFixed => { + mem.open_popup_at(id, hover_pos); + } + _ => mem.open_popup(id), + } + } else { + mem.close_popup(); + } + } + Some(SetOpenCommand::Toggle) => { + mem.toggle_popup(id); + } + None => {} + }); + } + + if !open_kind.is_open(id, &ctx) { + return None; + } + + let (ui_kind, order) = match kind { + PopupKind::Popup => (UiKind::Popup, Order::Foreground), + PopupKind::Tooltip => (UiKind::Tooltip, Order::Tooltip), + PopupKind::Menu => (UiKind::Menu, Order::Foreground), + }; + + if kind == PopupKind::Popup { + ctx.pass_state_mut(|fs| { + fs.layers + .entry(layer_id) + .or_default() + .open_popups + .insert(id) + }); + } + + let anchor_rect = anchor.rect(id, &ctx)?; + + let (pivot, anchor) = best_align.pivot_pos(&anchor_rect, gap); + + let mut area = Area::new(id) + .order(order) + .kind(ui_kind) + .pivot(pivot) + .fixed_pos(anchor) + .sense(sense) + .layout(layout); + + if let Some(width) = width { + area = area.default_width(width); + } + + let frame = frame.unwrap_or_else(|| Frame::popup(&ctx.style())); + + let response = area.show(&ctx, |ui| frame.show(ui, content).inner); + + let should_close = |close_behavior| { + let should_close = match close_behavior { + PopupCloseBehavior::CloseOnClick => widget_clicked_elsewhere, + PopupCloseBehavior::CloseOnClickOutside => { + widget_clicked_elsewhere && response.response.clicked_elsewhere() + } + PopupCloseBehavior::IgnoreClicks => false, + }; + + should_close || ctx.input(|i| i.key_pressed(Key::Escape)) + }; + + match open_kind { + OpenKind::Open | OpenKind::Closed => {} + OpenKind::Bool(open, close_behavior) => { + if should_close(close_behavior) { + *open = false; + } + } + OpenKind::Memory { close_behavior, .. } => { + if should_close(close_behavior) { + ctx.memory_mut(|mem| mem.close_popup()); + } + } + } + + Some(response) + } } diff --git a/crates/egui/src/containers/tooltip.rs b/crates/egui/src/containers/tooltip.rs new file mode 100644 index 000000000..1cfc2a9c9 --- /dev/null +++ b/crates/egui/src/containers/tooltip.rs @@ -0,0 +1,376 @@ +use crate::pass_state::PerWidgetTooltipState; +use crate::{ + AreaState, Context, Id, InnerResponse, LayerId, Layout, Order, Popup, PopupAnchor, PopupKind, + Response, Sense, +}; +use emath::Vec2; + +pub struct Tooltip<'a> { + pub popup: Popup<'a>, + layer_id: LayerId, + widget_id: Id, +} + +impl<'a> Tooltip<'a> { + /// Show a tooltip that is always open + pub fn new( + widget_id: Id, + ctx: Context, + anchor: impl Into, + layer_id: LayerId, + ) -> Self { + Self { + // TODO(lucasmerlin): Set width somehow (we're missing context here) + popup: Popup::new(widget_id, ctx, anchor.into(), layer_id) + .kind(PopupKind::Tooltip) + .gap(4.0) + .sense(Sense::hover()), + layer_id, + widget_id, + } + } + + /// Show a tooltip for a widget. Always open (as long as this function is called). + pub fn for_widget(response: &Response) -> Self { + let popup = Popup::from_response(response) + .kind(PopupKind::Tooltip) + .gap(4.0) + .width(response.ctx.style().spacing.tooltip_width) + .sense(Sense::hover()); + Self { + popup, + layer_id: response.layer_id, + widget_id: response.id, + } + } + + /// Show a tooltip when hovering an enabled widget. + pub fn for_enabled(response: &Response) -> Self { + let mut tooltip = Self::for_widget(response); + tooltip.popup = tooltip + .popup + .open(response.enabled() && Self::should_show_tooltip(response)); + tooltip + } + + /// Show a tooltip when hovering a disabled widget. + pub fn for_disabled(response: &Response) -> Self { + let mut tooltip = Self::for_widget(response); + tooltip.popup = tooltip + .popup + .open(!response.enabled() && Self::should_show_tooltip(response)); + tooltip + } + + /// Show the tooltip at the pointer position. + #[inline] + pub fn at_pointer(mut self) -> Self { + self.popup = self.popup.at_pointer(); + self + } + + /// Set the gap between the tooltip and the anchor + /// + /// Default: 5.0 + #[inline] + pub fn gap(mut self, gap: f32) -> Self { + self.popup = self.popup.gap(gap); + self + } + + /// Set the layout of the tooltip + #[inline] + pub fn layout(mut self, layout: Layout) -> Self { + self.popup = self.popup.layout(layout); + self + } + + /// Set the width of the tooltip + #[inline] + pub fn width(mut self, width: f32) -> Self { + self.popup = self.popup.width(width); + self + } + + /// Show the tooltip + pub fn show(self, content: impl FnOnce(&mut crate::Ui) -> R) -> Option> { + let Self { + mut popup, + layer_id: parent_layer, + widget_id, + } = self; + + if !popup.is_open() { + return None; + } + + let rect = popup.get_anchor_rect()?; + + let mut state = popup.ctx().pass_state_mut(|fs| { + // Remember that this is the widget showing the tooltip: + fs.layers + .entry(parent_layer) + .or_default() + .widget_with_tooltip = Some(widget_id); + + fs.tooltips + .widget_tooltips + .get(&widget_id) + .copied() + .unwrap_or(PerWidgetTooltipState { + bounding_rect: rect, + tooltip_count: 0, + }) + }); + + let tooltip_area_id = Self::tooltip_id(widget_id, state.tooltip_count); + popup = popup.anchor(state.bounding_rect).id(tooltip_area_id); + + let response = popup.show(|ui| { + // By default, the text in tooltips aren't selectable. + // This means that most tooltips aren't interactable, + // which also mean they won't stick around so you can click them. + // Only tooltips that have actual interactive stuff (buttons, links, …) + // will stick around when you try to click them. + ui.style_mut().interaction.selectable_labels = false; + + content(ui) + }); + + // The popup might not be shown on at_pointer if there is no pointer. + if let Some(response) = &response { + state.tooltip_count += 1; + state.bounding_rect = state.bounding_rect.union(response.response.rect); + response + .response + .ctx + .pass_state_mut(|fs| fs.tooltips.widget_tooltips.insert(widget_id, state)); + Self::remember_that_tooltip_was_shown(&response.response.ctx); + } + + response + } + + fn when_was_a_toolip_last_shown_id() -> Id { + Id::new("when_was_a_toolip_last_shown") + } + + pub fn seconds_since_last_tooltip(ctx: &Context) -> f32 { + let when_was_a_toolip_last_shown = + ctx.data(|d| d.get_temp::(Self::when_was_a_toolip_last_shown_id())); + + if let Some(when_was_a_toolip_last_shown) = when_was_a_toolip_last_shown { + let now = ctx.input(|i| i.time); + (now - when_was_a_toolip_last_shown) as f32 + } else { + f32::INFINITY + } + } + + fn remember_that_tooltip_was_shown(ctx: &Context) { + let now = ctx.input(|i| i.time); + ctx.data_mut(|data| data.insert_temp::(Self::when_was_a_toolip_last_shown_id(), now)); + } + + /// What is the id of the next tooltip for this widget? + pub fn next_tooltip_id(ctx: &Context, widget_id: Id) -> Id { + let tooltip_count = ctx.pass_state(|fs| { + fs.tooltips + .widget_tooltips + .get(&widget_id) + .map_or(0, |state| state.tooltip_count) + }); + Self::tooltip_id(widget_id, tooltip_count) + } + + pub fn tooltip_id(widget_id: Id, tooltip_count: usize) -> Id { + widget_id.with(tooltip_count) + } + + /// Should we show a tooltip for this response? + pub fn should_show_tooltip(response: &Response) -> bool { + if response.ctx.memory(|mem| mem.everything_is_visible()) { + return true; + } + + let any_open_popups = response.ctx.prev_pass_state(|fs| { + fs.layers + .get(&response.layer_id) + .is_some_and(|layer| !layer.open_popups.is_empty()) + }); + if any_open_popups { + // Hide tooltips if the user opens a popup (menu, combo-box, etc.) in the same layer. + return false; + } + + let style = response.ctx.style(); + + let tooltip_delay = style.interaction.tooltip_delay; + let tooltip_grace_time = style.interaction.tooltip_grace_time; + + let ( + time_since_last_scroll, + time_since_last_click, + time_since_last_pointer_movement, + pointer_pos, + pointer_dir, + ) = response.ctx.input(|i| { + ( + i.time_since_last_scroll(), + i.pointer.time_since_last_click(), + i.pointer.time_since_last_movement(), + i.pointer.hover_pos(), + i.pointer.direction(), + ) + }); + + if time_since_last_scroll < tooltip_delay { + // See https://github.com/emilk/egui/issues/4781 + // Note that this means we cannot have `ScrollArea`s in a tooltip. + response + .ctx + .request_repaint_after_secs(tooltip_delay - time_since_last_scroll); + return false; + } + + let is_our_tooltip_open = response.is_tooltip_open(); + + if is_our_tooltip_open { + // Check if we should automatically stay open: + + let tooltip_id = Self::next_tooltip_id(&response.ctx, response.id); + let tooltip_layer_id = LayerId::new(Order::Tooltip, tooltip_id); + + let tooltip_has_interactive_widget = response.ctx.viewport(|vp| { + vp.prev_pass + .widgets + .get_layer(tooltip_layer_id) + .any(|w| w.enabled && w.sense.interactive()) + }); + + if tooltip_has_interactive_widget { + // We keep the tooltip open if hovered, + // or if the pointer is on its way to it, + // so that the user can interact with the tooltip + // (i.e. click links that are in it). + if let Some(area) = AreaState::load(&response.ctx, tooltip_id) { + let rect = area.rect(); + + if let Some(pos) = pointer_pos { + if rect.contains(pos) { + return true; // hovering interactive tooltip + } + if pointer_dir != Vec2::ZERO + && rect.intersects_ray(pos, pointer_dir.normalized()) + { + return true; // on the way to interactive tooltip + } + } + } + } + } + + let clicked_more_recently_than_moved = + time_since_last_click < time_since_last_pointer_movement + 0.1; + if clicked_more_recently_than_moved { + // It is common to click a widget and then rest the mouse there. + // It would be annoying to then see a tooltip for it immediately. + // Similarly, clicking should hide the existing tooltip. + // Only hovering should lead to a tooltip, not clicking. + // The offset is only to allow small movement just right after the click. + return false; + } + + if is_our_tooltip_open { + // Check if we should automatically stay open: + + if pointer_pos.is_some_and(|pointer_pos| response.rect.contains(pointer_pos)) { + // Handle the case of a big tooltip that covers the widget: + return true; + } + } + + let is_other_tooltip_open = response.ctx.prev_pass_state(|fs| { + if let Some(already_open_tooltip) = fs + .layers + .get(&response.layer_id) + .and_then(|layer| layer.widget_with_tooltip) + { + already_open_tooltip != response.id + } else { + false + } + }); + if is_other_tooltip_open { + // We only allow one tooltip per layer. First one wins. It is up to that tooltip to close itself. + return false; + } + + // Fast early-outs: + if response.enabled() { + if !response.hovered() || !response.ctx.input(|i| i.pointer.has_pointer()) { + return false; + } + } else if !response + .ctx + .rect_contains_pointer(response.layer_id, response.rect) + { + return false; + } + + // There is a tooltip_delay before showing the first tooltip, + // but once one tooltip is show, moving the mouse cursor to + // another widget should show the tooltip for that widget right away. + + // Let the user quickly move over some dead space to hover the next thing + let tooltip_was_recently_shown = + Self::seconds_since_last_tooltip(&response.ctx) < tooltip_grace_time; + + if !tooltip_was_recently_shown && !is_our_tooltip_open { + if style.interaction.show_tooltips_only_when_still { + // We only show the tooltip when the mouse pointer is still. + if !response + .ctx + .input(|i| i.pointer.is_still() && i.smooth_scroll_delta == Vec2::ZERO) + { + // wait for mouse to stop + response.ctx.request_repaint(); + return false; + } + } + + let time_since_last_interaction = time_since_last_scroll + .min(time_since_last_pointer_movement) + .min(time_since_last_click); + let time_til_tooltip = tooltip_delay - time_since_last_interaction; + + if 0.0 < time_til_tooltip { + // Wait until the mouse has been still for a while + response.ctx.request_repaint_after_secs(time_til_tooltip); + return false; + } + } + + // We don't want tooltips of things while we are dragging them, + // but we do want tooltips while holding down on an item on a touch screen. + if response + .ctx + .input(|i| i.pointer.any_down() && i.pointer.has_moved_too_much_for_a_click) + { + return false; + } + + // All checks passed: show the tooltip! + + true + } + + /// Was this tooltip visible last frame? + pub fn was_tooltip_open_last_frame(ctx: &Context, widget_id: Id) -> bool { + let primary_tooltip_area_id = Self::tooltip_id(widget_id, 0); + ctx.memory(|mem| { + mem.areas() + .visible_last_frame(&LayerId::new(Order::Tooltip, primary_tooltip_area_id)) + }) + } +} diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 759f7e41a..82b37c50e 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -458,7 +458,8 @@ pub use epaint::emath; pub use ecolor::hex_color; pub use ecolor::{Color32, Rgba}; pub use emath::{ - lerp, pos2, remap, remap_clamp, vec2, Align, Align2, NumExt, Pos2, Rangef, Rect, Vec2, Vec2b, + lerp, pos2, remap, remap_clamp, vec2, Align, Align2, NumExt, Pos2, Rangef, Rect, RectAlign, + Vec2, Vec2b, }; pub use epaint::{ mutex, diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index 2c669f0ff..3a6053f73 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -89,8 +89,12 @@ pub struct Memory { /// 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, + popup: Option<(Id, Option)>, #[cfg_attr(feature = "persistence", serde(skip))] everything_is_visible: bool, @@ -1070,7 +1074,7 @@ impl Memory { impl Memory { /// Is the given popup open? pub fn is_popup_open(&self, popup_id: Id) -> bool { - self.popup == Some(popup_id) || self.everything_is_visible() + self.popup.is_some_and(|(id, _)| id == popup_id) || self.everything_is_visible() } /// Is any popup open? @@ -1080,7 +1084,18 @@ impl Memory { /// Open the given popup and close all others. pub fn open_popup(&mut self, popup_id: Id) { - self.popup = Some(popup_id); + self.popup = Some((popup_id, None)); + } + + /// Open the popup and remember its position. + pub fn open_popup_at(&mut self, popup_id: Id, pos: impl Into>) { + self.popup = Some((popup_id, pos.into())); + } + + /// Get the position for this popup. + pub fn popup_position(&self, id: Id) -> Option { + self.popup + .and_then(|(popup_id, pos)| if popup_id == id { pos } else { None }) } /// Close the open popup, if any. diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 131a420da..a5a702cfc 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -2,8 +2,8 @@ use std::{any::Any, sync::Arc}; use crate::{ emath::{Align, Pos2, Rect, Vec2}, - menu, pass_state, AreaState, Context, CursorIcon, Id, LayerId, Order, PointerButton, Sense, Ui, - WidgetRect, WidgetText, + menu, pass_state, Context, CursorIcon, Id, LayerId, PointerButton, Popup, PopupKind, Sense, + Tooltip, Ui, WidgetRect, WidgetText, }; // ---------------------------------------------------------------------------- @@ -550,36 +550,22 @@ impl Response { /// ``` #[doc(alias = "tooltip")] pub fn on_hover_ui(self, add_contents: impl FnOnce(&mut Ui)) -> Self { - if self.flags.contains(Flags::ENABLED) && self.should_show_hover_ui() { - self.show_tooltip_ui(add_contents); - } + Tooltip::for_enabled(&self).show(add_contents); self } /// Show this UI when hovering if the widget is disabled. pub fn on_disabled_hover_ui(self, add_contents: impl FnOnce(&mut Ui)) -> Self { - if !self.enabled() && self.should_show_hover_ui() { - crate::containers::show_tooltip_for( - &self.ctx, - self.layer_id, - self.id, - &self.rect, - add_contents, - ); - } + Tooltip::for_disabled(&self).show(add_contents); self } /// Like `on_hover_ui`, but show the ui next to cursor. pub fn on_hover_ui_at_pointer(self, add_contents: impl FnOnce(&mut Ui)) -> Self { - if self.enabled() && self.should_show_hover_ui() { - crate::containers::show_tooltip_at_pointer( - &self.ctx, - self.layer_id, - self.id, - add_contents, - ); - } + Tooltip::for_enabled(&self) + .at_pointer() + .gap(12.0) + .show(add_contents); self } @@ -587,13 +573,9 @@ impl Response { /// /// This can be used to give attention to a widget during a tutorial. pub fn show_tooltip_ui(&self, add_contents: impl FnOnce(&mut Ui)) { - crate::containers::show_tooltip_for( - &self.ctx, - self.layer_id, - self.id, - &self.rect, - add_contents, - ); + Popup::from_response(self) + .kind(PopupKind::Tooltip) + .show(add_contents); } /// Always show this tooltip, even if disabled and the user isn't hovering it. @@ -607,180 +589,7 @@ impl Response { /// Was the tooltip open last frame? pub fn is_tooltip_open(&self) -> bool { - crate::popup::was_tooltip_open_last_frame(&self.ctx, self.id) - } - - fn should_show_hover_ui(&self) -> bool { - if self.ctx.memory(|mem| mem.everything_is_visible()) { - return true; - } - - let any_open_popups = self.ctx.prev_pass_state(|fs| { - fs.layers - .get(&self.layer_id) - .is_some_and(|layer| !layer.open_popups.is_empty()) - }); - if any_open_popups { - // Hide tooltips if the user opens a popup (menu, combo-box, etc) in the same layer. - return false; - } - - let style = self.ctx.style(); - - let tooltip_delay = style.interaction.tooltip_delay; - let tooltip_grace_time = style.interaction.tooltip_grace_time; - - let ( - time_since_last_scroll, - time_since_last_click, - time_since_last_pointer_movement, - pointer_pos, - pointer_dir, - ) = self.ctx.input(|i| { - ( - i.time_since_last_scroll(), - i.pointer.time_since_last_click(), - i.pointer.time_since_last_movement(), - i.pointer.hover_pos(), - i.pointer.direction(), - ) - }); - - if time_since_last_scroll < tooltip_delay { - // See https://github.com/emilk/egui/issues/4781 - // Note that this means we cannot have `ScrollArea`s in a tooltip. - self.ctx - .request_repaint_after_secs(tooltip_delay - time_since_last_scroll); - return false; - } - - let is_our_tooltip_open = self.is_tooltip_open(); - - if is_our_tooltip_open { - // Check if we should automatically stay open: - - let tooltip_id = crate::next_tooltip_id(&self.ctx, self.id); - let tooltip_layer_id = LayerId::new(Order::Tooltip, tooltip_id); - - let tooltip_has_interactive_widget = self.ctx.viewport(|vp| { - vp.prev_pass - .widgets - .get_layer(tooltip_layer_id) - .any(|w| w.enabled && w.sense.interactive()) - }); - - if tooltip_has_interactive_widget { - // We keep the tooltip open if hovered, - // or if the pointer is on its way to it, - // so that the user can interact with the tooltip - // (i.e. click links that are in it). - if let Some(area) = AreaState::load(&self.ctx, tooltip_id) { - let rect = area.rect(); - - if let Some(pos) = pointer_pos { - if rect.contains(pos) { - return true; // hovering interactive tooltip - } - if pointer_dir != Vec2::ZERO - && rect.intersects_ray(pos, pointer_dir.normalized()) - { - return true; // on the way to interactive tooltip - } - } - } - } - } - - let clicked_more_recently_than_moved = - time_since_last_click < time_since_last_pointer_movement + 0.1; - if clicked_more_recently_than_moved { - // It is common to click a widget and then rest the mouse there. - // It would be annoying to then see a tooltip for it immediately. - // Similarly, clicking should hide the existing tooltip. - // Only hovering should lead to a tooltip, not clicking. - // The offset is only to allow small movement just right after the click. - return false; - } - - if is_our_tooltip_open { - // Check if we should automatically stay open: - - if pointer_pos.is_some_and(|pointer_pos| self.rect.contains(pointer_pos)) { - // Handle the case of a big tooltip that covers the widget: - return true; - } - } - - let is_other_tooltip_open = self.ctx.prev_pass_state(|fs| { - if let Some(already_open_tooltip) = fs - .layers - .get(&self.layer_id) - .and_then(|layer| layer.widget_with_tooltip) - { - already_open_tooltip != self.id - } else { - false - } - }); - if is_other_tooltip_open { - // We only allow one tooltip per layer. First one wins. It is up to that tooltip to close itself. - return false; - } - - // Fast early-outs: - if self.enabled() { - if !self.hovered() || !self.ctx.input(|i| i.pointer.has_pointer()) { - return false; - } - } else if !self.ctx.rect_contains_pointer(self.layer_id, self.rect) { - return false; - } - - // There is a tooltip_delay before showing the first tooltip, - // but once one tooltip is show, moving the mouse cursor to - // another widget should show the tooltip for that widget right away. - - // Let the user quickly move over some dead space to hover the next thing - let tooltip_was_recently_shown = - crate::popup::seconds_since_last_tooltip(&self.ctx) < tooltip_grace_time; - - if !tooltip_was_recently_shown && !is_our_tooltip_open { - if style.interaction.show_tooltips_only_when_still { - // We only show the tooltip when the mouse pointer is still. - if !self - .ctx - .input(|i| i.pointer.is_still() && i.smooth_scroll_delta == Vec2::ZERO) - { - // wait for mouse to stop - self.ctx.request_repaint(); - return false; - } - } - - let time_since_last_interaction = time_since_last_scroll - .min(time_since_last_pointer_movement) - .min(time_since_last_click); - let time_til_tooltip = tooltip_delay - time_since_last_interaction; - - if 0.0 < time_til_tooltip { - // Wait until the mouse has been still for a while - self.ctx.request_repaint_after_secs(time_til_tooltip); - return false; - } - } - - // We don't want tooltips of things while we are dragging them, - // but we do want tooltips while holding down on an item on a touch screen. - if self - .ctx - .input(|i| i.pointer.any_down() && i.pointer.has_moved_too_much_for_a_click) - { - return false; - } - - // All checks passed: show the tooltip! - - true + Tooltip::was_tooltip_open_last_frame(&self.ctx, self.id) } /// Like `on_hover_text`, but show the text next to cursor. diff --git a/crates/egui_demo_lib/src/demo/context_menu.rs b/crates/egui_demo_lib/src/demo/context_menu.rs index 2dce3e763..35abc0800 100644 --- a/crates/egui_demo_lib/src/demo/context_menu.rs +++ b/crates/egui_demo_lib/src/demo/context_menu.rs @@ -1,3 +1,5 @@ +use egui::{ComboBox, Popup}; + #[derive(Clone, Default, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct ContextMenus {} @@ -32,6 +34,20 @@ impl crate::View for ContextMenus { } }); + ui.horizontal(|ui| { + let response = ui.button("New menu"); + Popup::menu(&response).show(Self::nested_menus); + + let response = ui.button("New context menu"); + Popup::context_menu(&response).show(Self::nested_menus); + + ComboBox::new("Hi", "Hi").show_ui(ui, |ui| { + _ = ui.selectable_label(false, "I have some long text that should be wrapped"); + _ = ui.selectable_label(false, "Short"); + _ = ui.selectable_label(false, "Medium length"); + }); + }); + ui.vertical_centered(|ui| { ui.add(crate::egui_github_link_file!()); }); @@ -51,6 +67,7 @@ impl ContextMenus { ui.close_menu(); } let _ = ui.button("Item"); + ui.menu_button("Recursive", Self::nested_menus) }); ui.menu_button("SubMenu", |ui| { if ui.button("Open…").clicked() { 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 6f753c3a4..80d32e697 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -78,6 +78,7 @@ impl Default for DemoGroups { Box::::default(), Box::::default(), Box::::default(), + Box::::default(), Box::::default(), Box::::default(), Box::::default(), diff --git a/crates/egui_demo_lib/src/demo/mod.rs b/crates/egui_demo_lib/src/demo/mod.rs index cb68a46fb..8042f1fe6 100644 --- a/crates/egui_demo_lib/src/demo/mod.rs +++ b/crates/egui_demo_lib/src/demo/mod.rs @@ -23,6 +23,7 @@ pub mod paint_bezier; pub mod painting; pub mod panels; pub mod password; +mod popups; pub mod scene; pub mod screenshot; pub mod scrolling; diff --git a/crates/egui_demo_lib/src/demo/popups.rs b/crates/egui_demo_lib/src/demo/popups.rs new file mode 100644 index 000000000..0eeb76272 --- /dev/null +++ b/crates/egui_demo_lib/src/demo/popups.rs @@ -0,0 +1,181 @@ +use egui::{vec2, Align2, ComboBox, Frame, Id, Popup, PopupCloseBehavior, RectAlign, Tooltip, Ui}; + +/// Showcase [`Popup`]. +#[derive(Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "serde", serde(default))] +pub struct PopupsDemo { + align4: RectAlign, + gap: f32, + #[cfg_attr(feature = "serde", serde(skip))] + close_behavior: PopupCloseBehavior, + popup_open: bool, +} + +impl PopupsDemo { + fn apply_options<'a>(&self, popup: Popup<'a>) -> Popup<'a> { + popup + .align(self.align4) + .gap(self.gap) + .close_behavior(self.close_behavior) + } +} + +impl Default for PopupsDemo { + fn default() -> Self { + Self { + align4: RectAlign::default(), + gap: 4.0, + close_behavior: PopupCloseBehavior::CloseOnClick, + popup_open: false, + } + } +} + +impl crate::Demo for PopupsDemo { + fn name(&self) -> &'static str { + "\u{2755} Popups" + } + + fn show(&mut self, ctx: &egui::Context, open: &mut bool) { + egui::Window::new(self.name()) + .open(open) + .resizable(false) + .default_width(250.0) + .constrain(false) + .show(ctx, |ui| { + use crate::View as _; + self.ui(ui); + }); + } +} + +impl crate::View for PopupsDemo { + fn ui(&mut self, ui: &mut egui::Ui) { + ui.horizontal(|ui| { + ui.style_mut().spacing.item_spacing.x = 0.0; + let align_combobox = |ui: &mut Ui, label: &str, align: &mut Align2| { + 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"), + ]; + + ui.label(label); + ComboBox::new(label, "") + .selected_text(aligns.iter().find(|(a, _)| a == align).unwrap().1) + .show_ui(ui, |ui| { + for (align2, name) in &aligns { + ui.selectable_value(align, *align2, *name); + } + }); + }; + + ui.label("Align4("); + align_combobox(ui, "parent: ", &mut self.align4.parent); + ui.label(", "); + align_combobox(ui, "child: ", &mut self.align4.child); + ui.label(") "); + + let presets = [ + (RectAlign::TOP_START, "Top start"), + (RectAlign::TOP, "Top"), + (RectAlign::TOP_END, "Top end"), + (RectAlign::RIGHT_START, "Right start"), + (RectAlign::RIGHT, "Right Center"), + (RectAlign::RIGHT_END, "Right end"), + (RectAlign::BOTTOM_START, "Bottom start"), + (RectAlign::BOTTOM, "Bottom"), + (RectAlign::BOTTOM_END, "Bottom end"), + (RectAlign::LEFT_START, "Left start"), + (RectAlign::LEFT, "Left"), + (RectAlign::LEFT_END, "Left end"), + ]; + + ui.label(" Presets: "); + + ComboBox::new("Preset", "") + .selected_text( + presets + .iter() + .find(|(a, _)| a == &self.align4) + .map_or("Select", |(_, name)| *name), + ) + .show_ui(ui, |ui| { + for (align4, name) in &presets { + ui.selectable_value(&mut self.align4, *align4, *name); + } + }); + }); + ui.horizontal(|ui| { + ui.label("Gap:"); + ui.add(egui::DragValue::new(&mut self.gap)); + }); + ui.horizontal(|ui| { + ui.label("Close behavior:"); + ui.selectable_value( + &mut self.close_behavior, + PopupCloseBehavior::CloseOnClick, + "Close on click", + ) + .on_hover_text("Closes when the user clicks anywhere (inside or outside)"); + ui.selectable_value( + &mut self.close_behavior, + PopupCloseBehavior::CloseOnClickOutside, + "Close on click outside", + ) + .on_hover_text("Closes when the user clicks outside the popup"); + ui.selectable_value( + &mut self.close_behavior, + PopupCloseBehavior::IgnoreClicks, + "Ignore clicks", + ) + .on_hover_text("Close only when the button is clicked again"); + }); + + ui.checkbox(&mut self.popup_open, "Show popup"); + + let response = Frame::group(ui.style()) + .inner_margin(vec2(0.0, 25.0)) + .show(ui, |ui| { + ui.vertical_centered(|ui| ui.button("Click, right-click and hover me!")) + .inner + }) + .inner; + + self.apply_options(Popup::menu(&response).id(Id::new("menu"))) + .show(|ui| { + _ = ui.button("Menu item 1"); + _ = ui.button("Menu item 2"); + }); + + self.apply_options(Popup::context_menu(&response).id(Id::new("context_menu"))) + .show(|ui| { + _ = ui.button("Context menu item 1"); + _ = ui.button("Context menu item 2"); + }); + + if self.popup_open { + self.apply_options(Popup::from_response(&response).id(Id::new("popup"))) + .show(|ui| { + ui.label("Popup contents"); + }); + } + + let mut tooltip = Tooltip::for_enabled(&response); + tooltip.popup = self.apply_options(tooltip.popup); + tooltip.show(|ui| { + ui.label("Tooltips are popups, too!"); + }); + + ui.vertical_centered(|ui| { + ui.add(crate::egui_github_link_file!()); + }); + } +} diff --git a/crates/egui_demo_lib/src/demo/tooltips.rs b/crates/egui_demo_lib/src/demo/tooltips.rs index ede4bb9fb..0e391c553 100644 --- a/crates/egui_demo_lib/src/demo/tooltips.rs +++ b/crates/egui_demo_lib/src/demo/tooltips.rs @@ -83,6 +83,9 @@ impl Tooltips { ui.label("You can select this text."); }); + ui.label("This tooltip shows at the mouse cursor.") + .on_hover_text_at_pointer("Move me around!!"); + ui.separator(); // --------------------------------------------------------- let tooltip_ui = |ui: &mut egui::Ui| { diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Context Menus.png b/crates/egui_demo_lib/tests/snapshots/demos/Context Menus.png index ea25033bf..ac3c736a0 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Context Menus.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Context Menus.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d4cc8e0919fed5bd1ef981658626dba728435ab95da8ee96ced1fb4838d535ff -size 11741 +oid sha256:eb2bc4a38f20ed0f5fced36e8e56936bee328b24a0a45127d5d3739d40331cb7 +size 15514 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Popups.png b/crates/egui_demo_lib/tests/snapshots/demos/Popups.png new file mode 100644 index 000000000..1575cbe08 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/demos/Popups.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4806984f9c801a054cea80b89664293680abaa57cf0a95cf9682f111e3794fc1 +size 25080 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png b/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png index 6d9aced18..f8bb020e2 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c80158ac9c823f94d2830d1423236ad441dc7da31e748b6815c69663fa2a03d0 -size 59662 +oid sha256:92b70683a685869274749d057de174896e18dae5cb67e70221c3efdb7106cdda +size 63684 diff --git a/crates/egui_kittest/tests/popup.rs b/crates/egui_kittest/tests/popup.rs new file mode 100644 index 000000000..f55bf6388 --- /dev/null +++ b/crates/egui_kittest/tests/popup.rs @@ -0,0 +1,31 @@ +use kittest::Queryable; + +#[test] +fn test_interactive_tooltip() { + struct State { + link_clicked: bool, + } + + let mut harness = egui_kittest::Harness::new_ui_state( + |ui, state| { + ui.label("I have a tooltip").on_hover_ui(|ui| { + if ui.link("link").clicked() { + state.link_clicked = true; + } + }); + }, + State { + link_clicked: false, + }, + ); + + harness.get_by_label_contains("tooltip").hover(); + harness.run(); + harness.get_by_label("link").hover(); + harness.run(); + harness.get_by_label("link").simulate_click(); + + harness.run(); + + assert!(harness.state().link_clicked); +} diff --git a/crates/emath/src/align.rs b/crates/emath/src/align.rs index 71f172441..b1b56755e 100644 --- a/crates/emath/src/align.rs +++ b/crates/emath/src/align.rs @@ -50,6 +50,16 @@ impl Align { } } + /// Returns the inverse alignment. + /// `Min` becomes `Max`, `Center` stays the same, `Max` becomes `Min`. + pub fn flip(self) -> Self { + match self { + Self::Min => Self::Max, + Self::Center => Self::Center, + Self::Max => Self::Min, + } + } + /// Returns a range of given size within a specified range. /// /// If the requested `size` is bigger than the size of `range`, then the returned @@ -170,6 +180,24 @@ impl Align2 { vec2(self.x().to_sign(), self.y().to_sign()) } + /// Flip on the x-axis + /// e.g. `TOP_LEFT` -> `TOP_RIGHT` + pub fn flip_x(self) -> Self { + Self([self.x().flip(), self.y()]) + } + + /// Flip on the y-axis + /// e.g. `TOP_LEFT` -> `BOTTOM_LEFT` + pub fn flip_y(self) -> Self { + Self([self.x(), self.y().flip()]) + } + + /// Flip on both axes + /// e.g. `TOP_LEFT` -> `BOTTOM_RIGHT` + pub fn flip(self) -> Self { + Self([self.x().flip(), self.y().flip()]) + } + /// Used e.g. to anchor a piece of text to a part of the rectangle. /// Give a position within the rect, specified by the aligns pub fn anchor_rect(self, rect: Rect) -> Rect { diff --git a/crates/emath/src/lib.rs b/crates/emath/src/lib.rs index 05210fbb2..ae04f9ecf 100644 --- a/crates/emath/src/lib.rs +++ b/crates/emath/src/lib.rs @@ -34,6 +34,7 @@ mod ordered_float; mod pos2; mod range; mod rect; +mod rect_align; mod rect_transform; mod rot2; pub mod smart_aim; @@ -50,6 +51,7 @@ pub use self::{ pos2::*, range::Rangef, rect::*, + rect_align::RectAlign, rect_transform::*, rot2::*, ts_transform::*, diff --git a/crates/emath/src/rect_align.rs b/crates/emath/src/rect_align.rs new file mode 100644 index 000000000..5a8102ad1 --- /dev/null +++ b/crates/emath/src/rect_align.rs @@ -0,0 +1,279 @@ +use crate::{Align2, Pos2, Rect, Vec2}; + +/// Position a child [`Rect`] relative to a parent [`Rect`]. +/// +/// The corner from [`RectAlign::child`] on the new rect will be aligned to +/// the corner from [`RectAlign::parent`] on the original rect. +/// +/// There are helper constants for the 12 common menu positions: +/// ```text +/// ┌───────────┐ ┌────────┐ ┌─────────┐ +/// │ TOP_START │ │ TOP │ │ TOP_END │ +/// └───────────┘ └────────┘ └─────────┘ +/// ┌──────────┐ ┌────────────────────────────────────┐ ┌───────────┐ +/// │LEFT_START│ │ │ │RIGHT_START│ +/// └──────────┘ │ │ └───────────┘ +/// ┌──────────┐ │ │ ┌───────────┐ +/// │ LEFT │ │ some_rect │ │ RIGHT │ +/// └──────────┘ │ │ └───────────┘ +/// ┌──────────┐ │ │ ┌───────────┐ +/// │ LEFT_END │ │ │ │ RIGHT_END │ +/// └──────────┘ └────────────────────────────────────┘ └───────────┘ +/// ┌────────────┐ ┌──────┐ ┌──────────┐ +/// │BOTTOM_START│ │BOTTOM│ │BOTTOM_END│ +/// └────────────┘ └──────┘ └──────────┘ +/// ``` +// There is no `new` function on purpose, since writing out `parent` and `child` is more +// reasonable. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct RectAlign { + /// The alignment in the parent (original) rect. + pub parent: Align2, + + /// The alignment in the child (new) rect. + pub child: Align2, +} + +impl Default for RectAlign { + fn default() -> Self { + Self::BOTTOM_START + } +} + +impl RectAlign { + /// Along the top edge, leftmost. + pub const TOP_START: Self = Self { + parent: Align2::LEFT_TOP, + child: Align2::LEFT_BOTTOM, + }; + + /// Along the top edge, centered. + pub const TOP: Self = Self { + parent: Align2::CENTER_TOP, + child: Align2::CENTER_BOTTOM, + }; + + /// Along the top edge, rightmost. + pub const TOP_END: Self = Self { + parent: Align2::RIGHT_TOP, + child: Align2::RIGHT_BOTTOM, + }; + + /// Along the right edge, topmost. + pub const RIGHT_START: Self = Self { + parent: Align2::RIGHT_TOP, + child: Align2::LEFT_TOP, + }; + + /// Along the right edge, centered. + pub const RIGHT: Self = Self { + parent: Align2::RIGHT_CENTER, + child: Align2::LEFT_CENTER, + }; + + /// Along the right edge, bottommost. + pub const RIGHT_END: Self = Self { + parent: Align2::RIGHT_BOTTOM, + child: Align2::LEFT_BOTTOM, + }; + + /// Along the bottom edge, rightmost. + pub const BOTTOM_END: Self = Self { + parent: Align2::RIGHT_BOTTOM, + child: Align2::RIGHT_TOP, + }; + + /// Along the bottom edge, centered. + pub const BOTTOM: Self = Self { + parent: Align2::CENTER_BOTTOM, + child: Align2::CENTER_TOP, + }; + + /// Along the bottom edge, leftmost. + pub const BOTTOM_START: Self = Self { + parent: Align2::LEFT_BOTTOM, + child: Align2::LEFT_TOP, + }; + + /// Along the left edge, bottommost. + pub const LEFT_END: Self = Self { + parent: Align2::LEFT_BOTTOM, + child: Align2::RIGHT_BOTTOM, + }; + + /// Along the left edge, centered. + pub const LEFT: Self = Self { + parent: Align2::LEFT_CENTER, + child: Align2::RIGHT_CENTER, + }; + + /// Along the left edge, topmost. + pub const LEFT_START: Self = Self { + parent: Align2::LEFT_TOP, + child: Align2::RIGHT_TOP, + }; + + /// The 12 most common menu positions as an array, for use with [`RectAlign::find_best_align`]. + pub const MENU_ALIGNS: [Self; 12] = [ + Self::BOTTOM_START, + Self::BOTTOM_END, + Self::TOP_START, + Self::TOP_END, + Self::RIGHT_END, + Self::RIGHT_START, + Self::LEFT_END, + Self::LEFT_START, + // These come last on purpose, we prefer the corner ones + Self::TOP, + Self::RIGHT, + Self::BOTTOM, + Self::LEFT, + ]; + + /// Align in the parent rect. + pub fn parent(&self) -> Align2 { + self.parent + } + + /// Align in the child rect. + pub fn child(&self) -> Align2 { + self.child + } + + /// Convert an [`Align2`] to an [`RectAlign`], positioning the child rect inside the parent. + pub fn from_align2(align: Align2) -> Self { + Self { + parent: align, + child: align, + } + } + + /// The center of the child rect will be aligned to a corner of the parent rect. + pub fn over_corner(align: Align2) -> Self { + Self { + parent: align, + child: Align2::CENTER_CENTER, + } + } + + /// Position the child rect outside the parent rect. + pub fn outside(align: Align2) -> Self { + Self { + parent: align, + child: align.flip(), + } + } + + /// Calculate the child rect based on a size and some optional gap. + pub fn align_rect(&self, parent_rect: &Rect, size: Vec2, gap: f32) -> Rect { + let (pivot, anchor) = self.pivot_pos(parent_rect, gap); + pivot.anchor_size(anchor, size) + } + + /// Returns a [`Align2`] and a [`Pos2`] that you can e.g. use with `Area::fixed_pos` + /// and `Area::pivot` to align an `Area` to some rect. + pub fn pivot_pos(&self, parent_rect: &Rect, gap: f32) -> (Align2, Pos2) { + (self.child(), self.anchor(parent_rect, gap)) + } + + /// Returns a sign vector (-1, 0 or 1 in each direction) that can be used as an offset to the + /// child rect, creating a gap between the rects while keeping the edges aligned. + pub fn gap_vector(&self) -> Vec2 { + let mut gap = -self.child.to_sign(); + + // Align the edges in these cases + match *self { + Self::TOP_START | Self::TOP_END | Self::BOTTOM_START | Self::BOTTOM_END => { + gap.x = 0.0; + } + Self::LEFT_START | Self::LEFT_END | Self::RIGHT_START | Self::RIGHT_END => { + gap.y = 0.0; + } + _ => {} + } + + gap + } + + /// Calculator the anchor point for the child rect, based on the parent rect and an optional gap. + pub fn anchor(&self, parent_rect: &Rect, gap: f32) -> Pos2 { + let pos = self.parent.pos_in_rect(parent_rect); + + let offset = self.gap_vector() * gap; + + pos + offset + } + + /// Flip the alignment on the x-axis. + pub fn flip_x(self) -> Self { + Self { + parent: self.parent.flip_x(), + child: self.child.flip_x(), + } + } + + /// Flip the alignment on the y-axis. + pub fn flip_y(self) -> Self { + Self { + parent: self.parent.flip_y(), + child: self.child.flip_y(), + } + } + + /// Flip the alignment on both axes. + pub fn flip(self) -> Self { + Self { + parent: self.parent.flip(), + child: self.child.flip(), + } + } + + /// Returns the 3 alternative [`RectAlign`]s that are flipped in various ways, for use + /// with [`RectAlign::find_best_align`]. + pub fn symmetries(self) -> [Self; 3] { + [self.flip_x(), self.flip_y(), self.flip()] + } + + /// Look for the [`RectAlign`] that fits best in the available space. + /// + /// See also: + /// - [`RectAlign::symmetries`] to calculate alternatives + /// - [`RectAlign::MENU_ALIGNS`] for the 12 common menu positions + pub fn find_best_align( + mut values_to_try: impl Iterator, + available_space: Rect, + parent_rect: Rect, + gap: f32, + size: Vec2, + ) -> Self { + let area = size.x * size.y; + + let blocked_area = |pos: Self| { + let rect = pos.align_rect(&parent_rect, size, gap); + area - available_space.intersect(rect).area() + }; + + let first = values_to_try.next().unwrap_or_default(); + + if blocked_area(first) == 0.0 { + return first; + } + + let mut best_area = blocked_area(first); + let mut best = first; + + for align in values_to_try { + let blocked = blocked_area(align); + if blocked == 0.0 { + return align; + } + if blocked < best_area { + best = align; + best_area = blocked; + } + } + + best + } +} diff --git a/examples/popups/src/main.rs b/examples/popups/src/main.rs index baed12c2a..3da475832 100644 --- a/examples/popups/src/main.rs +++ b/examples/popups/src/main.rs @@ -1,7 +1,7 @@ #![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::{popup_below_widget, CentralPanel, ComboBox, Id, PopupCloseBehavior}; +use eframe::egui::{CentralPanel, ComboBox, Popup, PopupCloseBehavior}; fn main() -> Result<(), eframe::Error> { env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). @@ -42,23 +42,14 @@ impl eframe::App for MyApp { ui.label("PopupCloseBehavior::IgnoreClicks popup"); let response = ui.button("Open"); - let popup_id = Id::new("popup_id"); - if response.clicked() { - ui.memory_mut(|mem| mem.toggle_popup(popup_id)); - } - - popup_below_widget( - ui, - popup_id, - &response, - PopupCloseBehavior::IgnoreClicks, - |ui| { + Popup::menu(&response) + .close_behavior(PopupCloseBehavior::IgnoreClicks) + .show(|ui| { ui.set_min_width(310.0); ui.label("This popup will be open until you press the button again"); ui.checkbox(&mut self.checkbox, "Checkbox"); - }, - ); + }); }); } } From 770c976ed719eaf99a40e45cb3d194a2c99430af Mon Sep 17 00:00:00 2001 From: Braden Steffaniak Date: Tue, 18 Feb 2025 11:30:50 -0500 Subject: [PATCH 075/132] Fix image_loader for animated image types (#5688) Hi, after upgrading to 0.31.0 all of my beautiful static webp images started failing to load. I use the image_loader to load those via the `image` crate. I noticed that with 0.31.0 there are additions to how animated image types are handled with frames and such. And with those changes the frame index is attached to the uri at the end. This was problematic for the image_loader, because it wasn't updated to handle that frame tag at the end of the uri, so when looking up the bytes, it would fail to match the uri in the bytes cache (the bytes were being saved without the frame index, but attempting to be fetched _with_ the frame index). This fixes the image_loader for me with webp & gif. They don't load the animations, but I think that is because I don't have the custom image_loader set up so I'm not worried about that for myself. I'm not sure if that part is problematic in general, or if its just the way I have my features set up. You can recreate the issue on master by swapping out the dependency features in the `images` example like this: ``` # egui_extras = { workspace = true, features = ["default", "all_loaders"] } # env_logger = { version = "0.10", default-features = false, features = [ # "auto-color", # "humantime", # ] } # image = { workspace = true, features = ["jpeg", "png"] } egui_extras = { workspace = true, features = ["image", "svg"] } env_logger = { version = "0.10", default-features = false, features = [ "auto-color", "humantime", ] } image = { workspace = true, features = ["jpeg", "png", "webp", "gif"] } ``` * [x] I have followed the instructions in the PR template --------- Co-authored-by: lucasmerlin --- crates/egui_extras/src/loaders/image_loader.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/egui_extras/src/loaders/image_loader.rs b/crates/egui_extras/src/loaders/image_loader.rs index 171e56170..a2a6fb1df 100644 --- a/crates/egui_extras/src/loaders/image_loader.rs +++ b/crates/egui_extras/src/loaders/image_loader.rs @@ -1,5 +1,6 @@ use ahash::HashMap; use egui::{ + decode_animated_image_uri, load::{BytesPoll, ImageLoadResult, ImageLoader, ImagePoll, LoadError, SizeHint}, mutex::Mutex, ColorImage, @@ -58,6 +59,11 @@ impl ImageLoader for ImageCrateLoader { // 2. Mime from `BytesPoll::Ready` // 3. image::guess_format (used internally by image::load_from_memory) + // TODO(lucasmerlin): Egui currently changes all URIs for webp and gif files to include + // the frame index (#0), which breaks if the animated image loader is disabled. + // We work around this by removing the frame index from the URI here + let uri = decode_animated_image_uri(uri).map_or(uri, |(uri, _frame_index)| uri); + // (1) if uri.starts_with("file://") && !is_supported_uri(uri) { return Err(LoadError::NotSupported); From 071e090e2b2601e5ed4726a63a753188503dfaf2 Mon Sep 17 00:00:00 2001 From: Bryce Berger Date: Tue, 18 Feb 2025 11:33:27 -0500 Subject: [PATCH 076/132] add Label::show_tooltip_when_elided (#5710) fixes #5708 Allows the user to disable the automatic tooltip when a Label is elided * Closes * [x] I have followed the instructions in the PR template --- crates/egui/src/widgets/label.rs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/widgets/label.rs b/crates/egui/src/widgets/label.rs index 67dc196bc..34b684df6 100644 --- a/crates/egui/src/widgets/label.rs +++ b/crates/egui/src/widgets/label.rs @@ -30,6 +30,7 @@ pub struct Label { sense: Option, selectable: Option, halign: Option, + show_tooltip_when_elided: bool, } impl Label { @@ -40,6 +41,7 @@ impl Label { sense: None, selectable: None, halign: None, + show_tooltip_when_elided: true, } } @@ -116,6 +118,23 @@ impl Label { self.sense = Some(sense); self } + + /// Show the full text when hovered, if the text was elided. + /// + /// By default, this is true. + /// + /// ``` + /// # use egui::{Label, Sense}; + /// # egui::__run_test_ui(|ui| { + /// ui.add(Label::new("some text").show_tooltip_when_elided(false)) + /// .on_hover_text("completely different text"); + /// # }); + /// ``` + #[inline] + pub fn show_tooltip_when_elided(mut self, show: bool) -> Self { + self.show_tooltip_when_elided = show; + self + } } impl Label { @@ -247,13 +266,14 @@ impl Widget for Label { let interactive = self.sense.is_some_and(|sense| sense != Sense::hover()); let selectable = self.selectable; + let show_tooltip_when_elided = self.show_tooltip_when_elided; let (galley_pos, galley, mut response) = self.layout_in_ui(ui); response .widget_info(|| WidgetInfo::labeled(WidgetType::Label, ui.is_enabled(), galley.text())); if ui.is_rect_visible(response.rect) { - if galley.elided { + if show_tooltip_when_elided && galley.elided { // Show the full (non-elided) text on hover: response = response.on_hover_text(galley.text()); } From 43261a53965aa57dd12e6e9a2a91137b582b7450 Mon Sep 17 00:00:00 2001 From: Aiden Date: Wed, 19 Feb 2025 01:01:07 +0800 Subject: [PATCH 077/132] Add pointer events and focus handling for apps run in a Shadow DOM (#5627) * [x] I have followed the instructions in the PR template This PR handles pointer events and focus which did following changes: - `element_from_point` and focus is now acquired from root node object by using `get_root_node` from document or a shadow root. - `TextAgent` is appended individually in each shadow root. These changes handles pointer events and focus well in a web app that are running in a shadow dom, or else the hover pointer actions and keyboard input events are not triggered in a shadow dom. Helpful for building embeddable/multi-view web-apps. --- crates/eframe/Cargo.toml | 1 + crates/eframe/src/web/events.rs | 19 +++++++++++++------ crates/eframe/src/web/mod.rs | 20 ++++++++++++-------- crates/eframe/src/web/text_agent.rs | 15 +++++++++++++-- crates/eframe/src/web/web_runner.rs | 2 +- 5 files changed, 40 insertions(+), 17 deletions(-) diff --git a/crates/eframe/Cargo.toml b/crates/eframe/Cargo.toml index 883ec7d59..774bcbbe9 100644 --- a/crates/eframe/Cargo.toml +++ b/crates/eframe/Cargo.toml @@ -253,6 +253,7 @@ web-sys = { workspace = true, features = [ "ResizeObserverEntry", "ResizeObserverOptions", "ResizeObserverSize", + "ShadowRoot", "Storage", "Touch", "TouchEvent", diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index 762f202fa..6a1b7b6db 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -1,5 +1,3 @@ -use web_sys::EventTarget; - use crate::web::string_from_js_value; use super::{ @@ -10,6 +8,8 @@ use super::{ DEBUG_RESIZE, }; +use web_sys::{Document, EventTarget, ShadowRoot}; + // TODO(emilk): there are more calls to `prevent_default` and `stop_propagation` // than what is probably needed. @@ -570,10 +570,17 @@ fn install_pointerup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), /// Returns true if the cursor is above the canvas, or if we're dragging something. /// Pass in the position in browser viewport coordinates (usually event.clientX/Y). fn is_interested_in_pointer_event(runner: &AppRunner, pos: egui::Pos2) -> bool { - let document = web_sys::window().unwrap().document().unwrap(); - let is_hovering_canvas = document - .element_from_point(pos.x, pos.y) - .is_some_and(|element| element.eq(runner.canvas())); + let root_node = runner.canvas().get_root_node(); + + let element_at_point = if let Some(document) = root_node.dyn_ref::() { + document.element_from_point(pos.x, pos.y) + } else if let Some(shadow) = root_node.dyn_ref::() { + shadow.element_from_point(pos.x, pos.y) + } else { + None + }; + + let is_hovering_canvas = element_at_point.is_some_and(|element| element.eq(runner.canvas())); let is_pointer_down = runner .egui_ctx() .input(|i| i.pointer.any_down() || i.any_touches()); diff --git a/crates/eframe/src/web/mod.rs b/crates/eframe/src/web/mod.rs index 3dc7d7f8b..c67fa69e6 100644 --- a/crates/eframe/src/web/mod.rs +++ b/crates/eframe/src/web/mod.rs @@ -41,7 +41,7 @@ pub(crate) type ActiveWebPainter = web_painter_wgpu::WebPainterWgpu; pub use backend::*; use wasm_bindgen::prelude::*; -use web_sys::MediaQueryList; +use web_sys::{Document, MediaQueryList, Node}; use input::{ button_from_mouse_event, modifiers_from_kb_event, modifiers_from_mouse_event, @@ -64,18 +64,22 @@ pub(crate) fn string_from_js_value(value: &JsValue) -> String { /// - ``/`` with an `href` attribute /// - ``/`