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/.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 diff --git a/CHANGELOG.md b/CHANGELOG.md index b6733405c..b61c6398d 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. + +![scene](https://github.com/user-attachments/assets/7dc5e395-a3cb-4bf3-83a3-51a76a48c409) + +#### 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 a96b35be0..a8e629a03 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` @@ -51,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` @@ -60,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 @@ -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` 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/ecolor/src/color32.rs b/crates/ecolor/src/color32.rs index c38a8d6b9..87f4915cf 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); @@ -69,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); @@ -230,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. @@ -268,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 { @@ -284,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/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/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() { 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/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 abe1b8afe..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. /// @@ -115,8 +115,11 @@ pub struct Frame { #[doc(alias = "border")] pub stroke: Stroke, - /// The rounding of the corners of [`Self::stroke`] and [`Self::fill`]. - pub rounding: Rounding, + /// 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 corner_radius: CornerRadius, /// Margin outside the painted frame. /// @@ -158,11 +161,14 @@ 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, }; + /// No colors, no margins, no border. + /// + /// Same as [`Frame::NONE`]. pub const fn new() -> Self { Self::NONE } @@ -176,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) } @@ -193,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()) @@ -202,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()) @@ -211,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()) @@ -224,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()) } @@ -266,13 +272,26 @@ 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(); + 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. @@ -415,26 +434,25 @@ impl Frame { inner_margin: _, fill, stroke, - rounding, + corner_radius, outer_margin: _, 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, - rounding, + widget_rect, + corner_radius, fill, stroke, - epaint::StrokeKind::Outside, + epaint::StrokeKind::Inside, )); 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 215f6322d..6075377ff 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 @@ -607,11 +607,10 @@ 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 = window_frame.rounding; + let mut round = + window_frame.corner_radius - window_frame.stroke.width.round() as u8; if !is_collapsed { round.se = 0; @@ -666,28 +665,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; } @@ -1053,7 +1052,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); @@ -1071,56 +1070,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)); @@ -1230,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/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/src/lib.rs b/crates/egui/src/lib.rs index dcd2fb276..ff3f1cdf8 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 f6a330788..7a04087eb 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..22a0a0a9d 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,20 @@ 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 +471,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 e6418ec82..e7aaaa915 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 57784af1d..dc8b7f653 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_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_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_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 6ab99e59b..e3aa314b5 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(), ]), } @@ -365,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 @@ -405,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/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 5f1022ffc..edb19c3ea 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)); @@ -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/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/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/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..031f3e2ce --- /dev/null +++ b/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs @@ -0,0 +1,379 @@ +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), + ), + ( + "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 { + 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(1.0..=32.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); + } + } + + 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 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, + corner_radius, + 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("Corner radius"); + ui.add(corner_radius); + 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/src/demo/widget_gallery.rs b/crates/egui_demo_lib/src/demo/widget_gallery.rs index cfe746899..6005d6aec 100644 --- a/crates/egui_demo_lib/src/demo/widget_gallery.rs +++ b/crates/egui_demo_lib/src/demo/widget_gallery.rs @@ -22,6 +22,9 @@ pub struct WidgetGallery { #[cfg(feature = "chrono")] #[cfg_attr(feature = "serde", serde(skip))] date: Option, + + #[cfg(feature = "chrono")] + with_date_button: bool, } impl Default for WidgetGallery { @@ -38,10 +41,24 @@ impl Default for WidgetGallery { animate_progress_bar: false, #[cfg(feature = "chrono")] date: None, + #[cfg(feature = "chrono")] + with_date_button: true, } } } +impl WidgetGallery { + #[allow(unused_mut)] // if not chrono + #[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 +141,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 +245,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/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_demo_lib/tests/snapshots/demos/Bézier Curve.png b/crates/egui_demo_lib/tests/snapshots/demos/Bézier Curve.png index 09dc7549a..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:d4cfd5191dc7046a782ef2350dc8e0547d2702182badcb15b6b928ce077b76c1 -size 32154 +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 6ab8e1327..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:4631f841b9e23833505af39eb1c45908013d3b1e1278d477bcedf6a460c71802 -size 27163 +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 3e05ffd4f..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:96e750ebfcc6ec2c130674407388c30cce844977cde19adfebf351fd08698a4f -size 81726 +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 15123bc3b..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:b03613e597631da3b922ef3d696c4ce74cec41f86a2779fc5b084a316fc9e8e8 -size 11764 +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 feb573aa0..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:76d77e2df39af248d004a023b238bb61ed598ab2bea3e0c6f2885f9537ec1103 -size 25988 +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 34315f422..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:003d905893b80ffc132a783155971ad3054229e9d6c046e2760c912559a66b3d -size 20869 +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 05d87fa8b..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:be0f96c700b7662aab5098f8412dae3676116eeed65e70f6b295dd3375b329d0 -size 10968 +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 b5e5c4bec..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:99c94a09c0f6984909481189f9a1415ea90bd7c45e42b4b3763ee36f3d831a65 -size 133231 +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 689bb2153..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:e4fef5fa8661f207bae2c58381e729cdaf77aecc8b3f178caf262dc310e3a490 -size 24206 +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 b974f7489..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:7b38828e423195628dcea09b0cbdd66aa4c077334ab7082efd358c7a3297009d -size 17827 +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 6853e8564..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:c53403996451da14f7991ea61bd20b96dbc67eb67dd2041975dd6ce5015a6980 -size 22485 +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 baf613180..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:63673b070951246b98ca07613fa81496dbfdd10627bac3c9c4356ebff1a36b20 -size 64319 +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 4764035ca..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:374b4095a3c7b827b80d6ab01b945458ae0128a2974c2af8aaf6b7e9fef6b459 -size 32554 +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 7e6254b74..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:5e756c90069803bb4d2821fff723877bffffd44b26613f5b06c8a042d6901ca4 -size 36578 +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 2aee66f72..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:6efc59cb9908533baa1a7346b359e9e21c5faf0e373dac6fa7db5476e644233d -size 17678 +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 fa400718f..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:eb631bd2608aec6c83f8815b9db0b28627bf82692fd2b1cb793119083b4f8ad1 -size 264496 +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 d9f483ec1..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:d076f5365bfa87b7e61d3808b8b9b367157ea6e55ccf665720cbd2237d53793d -size 35563 +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 023aaa104..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:e0870e9e1c9dc718690124a4d490f1061414f15fa40571e571d9319c3b65e74e -size 23709 +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 7efa04ccc..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:532dbcbec94bb9c9fb8cc0665646790a459088e98077118b5fbb2112898e1a43 -size 183854 +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 b03b3dcc7..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:3fd584643b68084ec4b65369e08d229e2d389551bbefa59c84ad4b58621593f7 -size 117754 +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 972368971..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:7e2b854d99c9b17d15ef92188cdac521d7c0635aef9ba457cd3801884a784009 -size 26159 +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 9c5e33304..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:f0b7dc029de8e669d76f3be5e0800e973c354edcf7cefa239faed07c2cd5e0d5 -size 70452 +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 b73935d7e..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:64ba40810a6480e511e8d198b0dfb39e8b220eb2f5592877e27b17ee8d47b9c3 -size 66387 +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 07e9177b7..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:0a8151f5bd01b2cb5183c532d11f57bbb7e8cc1e77a3c4b890537d157922c961 -size 21261 +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 e53122482..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:96ce36fcc334793801ab724c711f592faf086a9c98c812684e6b76010e9d1974 -size 59714 +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 028346098..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:8155d93b78692ced67ddee4011526ef627838ede78351871df5feef8aa512857 -size 13141 +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 70cf2378e..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:bf6da022ab97f9d4b862cc8e91bdfd7f9785a3ab0540aa1c2bd2646bd30a3450 -size 35115 +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 2ce31fe45..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:035b35ed053cabd5022d9a97f7d4d5a79d194c9f14594e7602ecd9ef94c24ae5 -size 48053 +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 5ea3c5b3e..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:80a2968e211c83639b60e84b805f1327fb37b87144cada672a403c7e92ace8a8 -size 48066 +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 5bb1fe23e..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:86df5dc4b4ddd6f44226242b6d9b5e9f2aacd45193ae9f784fb5084a7a509e0b -size 43987 +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 2c868b440..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:1138bbc3b7e73cccd555f0fd58c27a5bda4d84484fdc1bd5223fc9802d0c5328 -size 44089 +oid sha256:9c1bc8e22aa1050a4e7d1b2abe407251e22d338c38a7e41c045a384c9139b4de +size 43895 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..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:3562bb87b155b2c406a72a651ffb1991305aa1e15273ce9f32cedc5766318537 -size 554922 +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 d5d26d2bb..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:ba6c0bd127ab02375f2ae5fbc3eeef33a1bdf074cbb180de2733c700b81df3e5 -size 771069 +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 98f73cee9..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:d85ab6d04059009fd2c3ad8001332b27e710c46c9300f2f1f409b882c49211dc -size 918967 +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 6de95b07d..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:a64f1bdec565357fe4dee3acb46b12eeb0492b522fb3bb9697d39dadce2e8c21 -size 1039455 +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 1d373b92b..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:5ca008dca03372bb334564e55fa2d1d25a36751a43df6001a1c1cf3e4db9bcd4 -size 1130930 +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 b8b2a7658..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:7f695127e7fe6cb3b752a0acd71db85d51d2de68e45051a7afe91f4d960acf27 -size 1311641 +oid sha256:9345de28f09e2891fd01db20bb0b94176ec3c89d8c2f344a6640d33e97ab5400 +size 1311417 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/Additive rectangle.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png new file mode 100644 index 000000000..979991d13 --- /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: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 new file mode 100644 index 000000000..249b5db4d --- /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: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 new file mode 100644 index 000000000..c81deb054 --- /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: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 new file mode 100644 index 000000000..2a29cd732 --- /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: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 new file mode 100644 index 000000000..b7d315e47 --- /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: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 new file mode 100644 index 000000000..2fe78dd62 --- /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: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 new file mode 100644 index 000000000..603442ac0 --- /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: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 new file mode 100644 index 000000000..266c77826 --- /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:f9cf9d7f1921bfc0d61a2ae31e69a98d28280e4699823de5e732cdb102aee5ac +size 37253 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_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_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/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/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..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. @@ -155,8 +157,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. @@ -170,7 +179,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` @@ -182,13 +191,18 @@ pub fn try_image_snapshot_options( new: &image::RgbaImage, name: &str, options: &SnapshotOptions, -) -> Result<(), SnapshotError> { +) -> SnapshotResult { let SnapshotOptions { threshold, 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")); @@ -203,23 +217,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 +247,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 +281,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(()) } @@ -281,12 +303,12 @@ 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 /// 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()) } @@ -301,7 +323,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 @@ -319,7 +341,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 @@ -336,7 +358,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 @@ -349,7 +371,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 @@ -358,29 +380,29 @@ impl Harness<'_, State> { &mut self, name: &str, options: &SnapshotOptions, - ) -> Result<(), SnapshotError> { + ) -> SnapshotResult { let image = self .render() .map_err(|err| SnapshotError::RenderError { err })?; 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 /// 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 })?; 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 @@ -393,7 +415,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 @@ -408,10 +430,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 @@ -440,7 +462,7 @@ impl Harness<'_, State> { &mut self, name: &str, options: &SnapshotOptions, - ) -> Result<(), SnapshotError> { + ) -> SnapshotResult { self.try_snapshot_options(name, options) } @@ -448,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) } @@ -468,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/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/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/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")); } 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/src/rounding.rs b/crates/epaint/src/corner_radius.rs similarity index 70% rename from crates/epaint/src/rounding.rs rename to crates/epaint/src/corner_radius.rs index 12695f387..07bd56c9e 100644 --- a/crates/epaint/src/rounding.rs +++ b/crates/epaint/src/corner_radius.rs @@ -1,12 +1,16 @@ /// 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]`. /// -/// 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, @@ -20,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, @@ -95,32 +99,45 @@ impl Rounding { } } -impl std::ops::Add for Rounding { +impl std::ops::Add for CornerRadius { type Output = Self; #[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::AddAssign for Rounding { +impl std::ops::Add for CornerRadius { + 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), + } + } +} + +impl std::ops::AddAssign for CornerRadius { #[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), }; } } -impl std::ops::AddAssign for Rounding { +impl std::ops::AddAssign for CornerRadius { #[inline] fn add_assign(&mut self, rhs: u8) { *self = Self { @@ -132,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 { @@ -145,7 +162,20 @@ impl std::ops::Sub for Rounding { } } -impl std::ops::SubAssign for Rounding { +impl std::ops::Sub for CornerRadius { + 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 CornerRadius { #[inline] fn sub_assign(&mut self, rhs: Self) { *self = Self { @@ -157,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 { @@ -169,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 { @@ -182,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 { @@ -194,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 { @@ -207,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/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/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 ca7ab01f1..ead5b7af2 100644 --- a/crates/epaint/src/shapes/rect_shape.rs +++ b/crates/epaint/src/shapes/rect_shape.rs @@ -8,8 +8,17 @@ use crate::*; pub struct RectShape { pub rect: Rect, - /// How rounded the corners are. Use `Rounding::ZERO` for no rounding. - pub rounding: Rounding, + /// How rounded the corners of the rectangle are. + /// + /// 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, + /// 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 corner_radius: CornerRadius, /// How to fill the rectangle. pub fill: Color32, @@ -21,13 +30,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, @@ -64,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, @@ -84,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 @@ -99,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. @@ -117,8 +126,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/shapes/shape.rs b/crates/epaint/src/shapes/shape.rs index 2e43fc585..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,8 +451,9 @@ impl Shape { } Self::Rect(rect_shape) => { rect_shape.rect = transform * rect_shape.rect; + rect_shape.corner_radius *= transform.scaling; rect_shape.stroke.width *= transform.scaling; - rect_shape.rounding *= 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 fa85a9588..5d82c1963 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,11 +115,17 @@ impl PathStroke { Self { width: width.into(), color: ColorMode::UV(Arc::new(callback)), - kind: StrokeKind::default(), + kind: StrokeKind::Middle, } } + #[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, @@ -127,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, @@ -135,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, @@ -168,7 +177,7 @@ impl From for PathStroke { Self { width: value.width, color: ColorMode::Solid(value.color), - kind: StrokeKind::default(), + kind: StrokeKind::Middle, } } } diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index f05d9b47a..e8f8ad52c 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, CornerRadiusF32, CubicBezierShape, EllipseShape, Mesh, + PathShape, Primitive, QuadraticBezierShape, RectShape, Shape, Stroke, StrokeKind, TextShape, + TextureId, Vertex, WHITE_UV, }; -use self::color::ColorMode; -use self::stroke::PathStroke; - // ---------------------------------------------------------------------------- #[allow(clippy::approx_constant)] @@ -477,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); @@ -500,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. @@ -525,19 +534,19 @@ impl Path { pub mod path { //! Helpers for constructing paths - use crate::Rounding; + use crate::CornerRadiusF32; 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, 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 == Rounding::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 @@ -548,29 +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(); - let r = crate::Roundingf::from(r); + add_circle_quadrant(path, pos2(max.x - cr.se, max.y - cr.se), cr.se, 0.0); // south east - 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 { + 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 } } @@ -626,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_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.floor() as _).at_least(0) + cr.at_most(max_cr).at_least(0.0) } } @@ -755,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(); @@ -811,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; @@ -825,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); @@ -858,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(); @@ -916,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 { - stroke::StrokeKind::Middle => { /* Nothing to do */ } - stroke::StrokeKind::Outside => { - p.pos += p.normal * stroke.width * 0.5; - } - stroke::StrokeKind::Inside => { - p.pos -= p.normal * stroke.width * 0.5; - } - } -} - /// Tessellate the given path as a stroke with thickness. fn stroke_path( feathering: f32, @@ -937,55 +910,128 @@ 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 != stroke::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; + // 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 { - /* - 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; } } + // TODO(emilk): add line caps (if this is an open line). + + 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); @@ -996,11 +1042,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); @@ -1009,15 +1052,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 @@ -1040,13 +1092,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); @@ -1059,6 +1111,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 @@ -1075,6 +1135,10 @@ fn stroke_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); @@ -1086,11 +1150,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); @@ -1106,11 +1170,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); @@ -1135,11 +1199,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); @@ -1185,32 +1249,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), @@ -1221,6 +1274,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); + } } } @@ -1469,9 +1534,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`]. @@ -1538,9 +1601,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`]. @@ -1586,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 @@ -1593,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 @@ -1600,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); + } } } @@ -1644,27 +1733,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`]. @@ -1672,53 +1758,30 @@ 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, - stroke_kind, + corner_radius, + mut fill, + mut stroke, + 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 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; - // 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 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); - } + if stroke.width == 0.0 { + stroke.color = Color32::TRANSPARENT; } // It is common to (sometimes accidentally) create an infinitely sized rectangle. @@ -1726,6 +1789,91 @@ impl Tessellator { 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. + 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 <= 0.0 { + rect = rect.round_to_pixels(self.pixels_per_point); + } else if stroke.width <= pixel_size + || is_nearest_integer_odd(self.pixels_per_point * stroke.width) + { + rect = rect.round_to_pixel_center(self.pixels_per_point); + } else { + rect = rect.round_to_pixels(self.pixels_per_point); + } + } + 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); + } + } + } + let old_feathering = self.feathering; if self.feathering < blur_width { @@ -1733,55 +1881,95 @@ 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); + corner_radius += 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: - 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: - 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 let Some(brush) = brush { + let original_cr = corner_radius; + + match stroke_kind { + StrokeKind::Inside => {} + StrokeKind::Middle => { + rect = rect.expand(stroke.width / 2.0); + corner_radius += stroke.width / 2.0; + } + StrokeKind::Outside => { + rect = rect.expand(stroke.width); + corner_radius += stroke.width; + } + } + + stroke_kind = StrokeKind::Inside; + + // 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 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_cr = 0.1; // Large enough to avoid numerical issues + let min_outside_cr = stroke.width + min_inside_cr; + + let extra_cr_tweak = 0.4; // Otherwise is doesn't _feels_ enough. + + 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 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 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 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); + } + } + + let path = &mut self.scratchpad_path; + path.clear(); + 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); + + 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; - // Textured let uv_from_pos = |p: Pos2| { pos2( remap(p.x, rect.x_range(), uv.x_range()), @@ -1789,12 +1977,14 @@ impl Tessellator { ) }; 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); + 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 @@ -1995,24 +2185,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); + } } } 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 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/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 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)