From b9a0723d88dee87ba0965470b554a6efdab1d0bb Mon Sep 17 00:00:00 2001
From: Recoordinate <296084221+latent-9@users.noreply.github.com>
Date: Wed, 19 Aug 2026 00:52:07 +1200
Subject: [PATCH 01/16] Fix documentation typos (#8404)
Two small documentation typos noticed while reading the docs:
- `crates/egui_extras/README.md`: "adds some features on top top of" ->
"adds some features on top of"
- `README.md`: "check out the [the egui web demo]" -> "check out [the
egui web demo]" (removes the duplicated "the"; link text and target
unchanged)
Documentation only; no code changes.
---
README.md | 2 +-
crates/egui_extras/README.md | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 45bd23f6d..4f105a0ef 100644
--- a/README.md
+++ b/README.md
@@ -66,7 +66,7 @@ ui.image(egui::include_image!("ferris.png"));
## Quick start
-There are simple examples in [the `examples/` folder](https://github.com/emilk/egui/blob/main/examples/). If you want to write a web app, then go to and follow the instructions. The official docs are at . For inspiration and more examples, check out the [the egui web demo](https://www.egui.rs/#demo) and follow the links in it to its source code.
+There are simple examples in [the `examples/` folder](https://github.com/emilk/egui/blob/main/examples/). If you want to write a web app, then go to and follow the instructions. The official docs are at . For inspiration and more examples, check out [the egui web demo](https://www.egui.rs/#demo) and follow the links in it to its source code.
If you want to integrate egui into an existing engine, go to the [Integrations](#integrations) section.
diff --git a/crates/egui_extras/README.md b/crates/egui_extras/README.md
index 4e5e96dce..41c9ece8a 100644
--- a/crates/egui_extras/README.md
+++ b/crates/egui_extras/README.md
@@ -6,7 +6,7 @@


-This is a crate that adds some features on top top of [`egui`](https://github.com/emilk/egui). This crate is for experimental features, and features that require big dependencies that do not belong in `egui`.
+This is a crate that adds some features on top of [`egui`](https://github.com/emilk/egui). This crate is for experimental features, and features that require big dependencies that do not belong in `egui`.
## Images
One thing `egui_extras` is commonly used for is to install image loaders for `egui`:
From fd54387eac03f57ca772a8fb590ceaadf780f31c Mon Sep 17 00:00:00 2001
From: Emil Ernerfeldt
Date: Thu, 20 Aug 2026 01:42:12 -0700
Subject: [PATCH 02/16] Clean up clippy lint config (#8437)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* [x] I have followed the instructions in the PR template
Remove clippy lint groups already covered by `all` (`complexity`,
`perf`, `suspicious`, and the misspelled `correctnesss`), and two
individual lints covered by the `cargo` group (`negative_feature_names`,
`wildcard_dependencies`).
Add `publish = false` to the two internal crates that were missing it
(`popups`, `egui_tests`), which silences the
`clippy::cargo_common_metadata` warnings.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5
---
Cargo.toml | 9 ++++++---
examples/popups/Cargo.toml | 1 +
tests/egui_tests/Cargo.toml | 1 +
3 files changed, 8 insertions(+), 3 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index b6534d2d1..3aae7fa74 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -179,7 +179,11 @@ all = "warn"
# See also clippy.toml
[workspace.lints.clippy]
+# `all` = `correctness` + `suspicious` + `style` + `complexity` + `perf`.
+# The remaining groups are `nursery` and `restriction`,
+# which are not meant to be enabled wholesale - we cherry-pick from them below.
all = { level = "warn", priority = -1 }
+cargo = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }
allow_attributes = "warn"
@@ -216,7 +220,6 @@ missing_assert_message = "warn"
mutex_integer = "warn"
needless_pass_by_ref_mut = "warn"
needless_type_cast = "warn"
-negative_feature_names = "warn"
non_zero_suggestions = "warn"
nonstandard_macro_braces = "warn"
or_fun_call = "warn"
@@ -258,7 +261,6 @@ unwrap_used = "warn"
use_self = "warn"
useless_let_if_seq = "warn"
verbose_file_reads = "warn"
-wildcard_dependencies = "warn"
# Pedantic lints we opt out of, with the number of hits at the time we enabled `pedantic`:
@@ -286,10 +288,11 @@ trivially_copy_pass_by_ref = "allow" # 119 hits
unreadable_literal = "allow" # 513 hits
used_underscore_binding = "allow" # 25 hits
-# These are meh:
+# Other:
assigning_clones = "allow" # No please
manual_range_contains = "allow" # this one is just worse imho
map_unwrap_or = "allow" # so is this one
+multiple_crate_versions = "allow" # we handle this with `cargo deny`
wildcard_imports = "allow" # `use crate::*` is useful to avoid merge conflicts when adding/removing imports
# NOTE: these are in `restriction`/`nursery`, so the `allow` is a no-op today.
diff --git a/examples/popups/Cargo.toml b/examples/popups/Cargo.toml
index d1f199918..c0d4384c7 100644
--- a/examples/popups/Cargo.toml
+++ b/examples/popups/Cargo.toml
@@ -2,6 +2,7 @@
name = "popups"
edition.workspace = true
license.workspace = true
+publish = false
rust-version.workspace = true
version.workspace = true
diff --git a/tests/egui_tests/Cargo.toml b/tests/egui_tests/Cargo.toml
index 44a7b9c8f..89d6a9fda 100644
--- a/tests/egui_tests/Cargo.toml
+++ b/tests/egui_tests/Cargo.toml
@@ -2,6 +2,7 @@
name = "egui_tests"
edition.workspace = true
license.workspace = true
+publish = false
rust-version.workspace = true
version.workspace = true
From 38c4ab7b3d030cd34960296f620cfe4d066de032 Mon Sep 17 00:00:00 2001
From: Keith
Date: Thu, 20 Aug 2026 13:36:39 -0700
Subject: [PATCH 03/16] Simplify and optimize `Color32::from_rgba_unmultiplied`
(#8427)
So I first noticed that this function was using a large lookup table
behind a OnceLock. I initially thought about just making it a const, but
when looking at things further. I realized it could be made much
simpler.
If we just treated the numbers as fixed point we can get rid of any of
the floating point calculations and especially divisions. You can see
how efficiently this can compile down here:
https://llvm.godbolt.org/z/K83jEjvdq
You can see all three versions here: https://godbolt.org/z/nWc1as1nq
* First one is basically the original essentially being called by:
`ColorImage::from_rgba_unmultiplied()`
* Second is the const Lookup table instead of the OnceLock and runtime
generation.
* Third is the fixed point implementation.
At least looking at the bytes reported compiler explorer the OnceLock
and the const Table results are in similar size, and the const table is
surprisingly smaller when I compile to a binary object in compiler
explorer. However though the oncelock is producing a lot SIMD
instructions for initialization so I guess not too surprised. The fixed
point math is much smaller than both.
The const table is probably faster, but does bloat the binary images,
and again when it's this fast to compute:
https://llvm.godbolt.org/z/K83jEjvdq I am not sure the extra bytes are
worth it.
Next, what I did was merge `from_rgba_unmultiplied` and
`from_rgba_unmultiplied_const`. Moreover with the fixed point math the
`from_rgba_unmultiplied_const` is probably not necessary anymore, but
it's part of the public API so I left it. Lastly, I just added a sanity
test to make sure the math checks out which it does. You can even sweep
the 2^16 inputs to be sure.
---
crates/ecolor/src/color32.rs | 47 ++++++++++++------------------------
crates/ecolor/src/lib.rs | 11 +++++++++
2 files changed, 27 insertions(+), 31 deletions(-)
diff --git a/crates/ecolor/src/color32.rs b/crates/ecolor/src/color32.rs
index 68a8fc3d6..7a9cf72b7 100644
--- a/crates/ecolor/src/color32.rs
+++ b/crates/ecolor/src/color32.rs
@@ -1,4 +1,4 @@
-use crate::{Rgba, fast_round, linear_f32_from_linear_u8};
+use crate::{Rgba, fast_round, mul_frac_round};
/// This format is used for space-efficient color representation (32 bits).
///
@@ -131,35 +131,10 @@ impl Color32 {
/// but for transparent colors what you get back might be slightly different (rounding errors).
#[inline]
pub fn from_rgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
- use std::sync::OnceLock;
- match a {
- // common-case optimization:
- 0 => Self::TRANSPARENT,
-
- // common-case optimization:
- 255 => Self::from_rgb(r, g, b),
-
- a => {
- static LOOKUP_TABLE: OnceLock> = OnceLock::new();
- let lut = LOOKUP_TABLE.get_or_init(|| {
- (0..=u16::MAX)
- .map(|i| {
- let [value, alpha] = i.to_ne_bytes();
- fast_round(value as f32 * linear_f32_from_linear_u8(alpha))
- })
- .collect()
- });
-
- let [r, g, b] =
- [r, g, b].map(|value| lut[usize::from(u16::from_ne_bytes([value, a]))]);
- Self::from_rgba_premultiplied(r, g, b, a)
- }
- }
+ Self::from_rgba_unmultiplied_const(r, g, b, a)
}
- /// Same as [`Self::from_rgba_unmultiplied`], but can be used in a const context.
- ///
- /// It is slightly slower when operating on non-const data.
+ /// This is the same as [`Self::from_rgba_unmultiplied`], but for const contexts.
#[inline]
pub const fn from_rgba_unmultiplied_const(r: u8, g: u8, b: u8, a: u8) -> Self {
match a {
@@ -170,9 +145,9 @@ impl Color32 {
255 => Self::from_rgb(r, g, b),
a => {
- let r = fast_round(r as f32 * linear_f32_from_linear_u8(a));
- let g = fast_round(g as f32 * linear_f32_from_linear_u8(a));
- let b = fast_round(b as f32 * linear_f32_from_linear_u8(a));
+ let r = mul_frac_round(r, a);
+ let g = mul_frac_round(g, a);
+ let b = mul_frac_round(b, a);
Self::from_rgba_premultiplied(r, g, b, a)
}
}
@@ -535,4 +510,14 @@ mod test {
Color32::from_rgba_unmultiplied(255, 0, 0, 128)
);
}
+
+ #[test]
+ fn mul_frac_round_vs_old() {
+ for x in (0..=255u8).step_by(4) {
+ for a in (1..=255u8).step_by(4) {
+ let old = fast_round(x as f32 * crate::linear_f32_from_linear_u8(a));
+ assert_eq!(old, mul_frac_round(x, a));
+ }
+ }
+ }
}
diff --git a/crates/ecolor/src/lib.rs b/crates/ecolor/src/lib.rs
index ea7cff6f7..02ea96c51 100644
--- a/crates/ecolor/src/lib.rs
+++ b/crates/ecolor/src/lib.rs
@@ -134,6 +134,17 @@ const fn fast_round(r: f32) -> u8 {
(r + 0.5) as _ // rust does a saturating cast since 1.45
}
+/// Compute val * (frac/255) with no floating point or divisions.
+#[inline]
+const fn mul_frac_round(val: u8, frac: u8) -> u8 {
+ // Treat this as a simple fixed point calculation
+ let p = (val as u16) * (frac as u16) + 128;
+ ((p + (p >> 8)) >> 8) as u8
+ // Logic split out a bit more.
+ //let p = (val as u16) * (frac as u16) + 127; // + 127 to round or remove to truncate.
+ //return ((p + 1 + (p >> 8)) >> 8) as u8;
+}
+
#[test]
pub fn test_srgba_conversion() {
for b in 0..=255 {
From 00dc6e814b251bf34c77063ed7efcd1024f3646f Mon Sep 17 00:00:00 2001
From: Emil Ernerfeldt
Date: Fri, 21 Aug 2026 01:33:29 -0700
Subject: [PATCH 04/16] Make kittest's predictable texture filtering honor
`TextureOptions::NEAREST` (#8441)
---
crates/egui-wgpu/src/egui.wgsl | 15 ++++++-
crates/egui-wgpu/src/renderer.rs | 44 +++++++++++++++++++
crates/egui_demo_lib/tests/misc.rs | 35 +++++++++++++++
.../snapshots/nearest_texture_filtering.png | 3 ++
4 files changed, 95 insertions(+), 2 deletions(-)
create mode 100644 crates/egui_demo_lib/tests/snapshots/nearest_texture_filtering.png
diff --git a/crates/egui-wgpu/src/egui.wgsl b/crates/egui-wgpu/src/egui.wgsl
index 39210841b..5b4b8240f 100644
--- a/crates/egui-wgpu/src/egui.wgsl
+++ b/crates/egui-wgpu/src/egui.wgsl
@@ -98,20 +98,31 @@ fn vs_main(
@group(1) @binding(0) var r_tex_color: texture_2d;
@group(1) @binding(1) var r_tex_sampler: sampler;
+/// 1 if the texture sampler uses nearest filtering, 0 if linear.
+/// Only read when `predictable_texture_filtering` is on.
+@group(1) @binding(2) var r_tex_nearest_filtering: u32;
+
fn sample_texture(in: VertexOutput) -> vec4 {
if r_locals.predictable_texture_filtering == 0 {
// Hardware filtering: fast, but varies across GPUs and drivers.
return textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
} else {
- // Manual bilinear filtering with four taps at pixel centers using textureLoad
let texture_size = vec2(textureDimensions(r_tex_color, 0));
let texture_size_f = vec2(texture_size);
+ let max_coord = texture_size - vec2(1, 1);
+
+ if r_tex_nearest_filtering == 1 {
+ // Nearest filtering: load the texel under the sample position.
+ let texel = clamp(vec2(in.tex_coord * texture_size_f), vec2(0, 0), max_coord);
+ return textureLoad(r_tex_color, texel, 0);
+ }
+
+ // Manual bilinear filtering with four taps at pixel centers using textureLoad
let pixel_coord = in.tex_coord * texture_size_f - 0.5;
let pixel_fract = fract(pixel_coord);
let pixel_floor = vec2(floor(pixel_coord));
// Manual texture clamping
- let max_coord = texture_size - vec2(1, 1);
let p00 = clamp(pixel_floor + vec2(0, 0), vec2(0, 0), max_coord);
let p10 = clamp(pixel_floor + vec2(1, 0), vec2(0, 0), max_coord);
let p01 = clamp(pixel_floor + vec2(0, 1), vec2(0, 0), max_coord);
diff --git a/crates/egui-wgpu/src/renderer.rs b/crates/egui-wgpu/src/renderer.rs
index de8808de3..267363591 100644
--- a/crates/egui-wgpu/src/renderer.rs
+++ b/crates/egui-wgpu/src/renderer.rs
@@ -245,6 +245,12 @@ pub struct Renderer {
uniform_bind_group: wgpu::BindGroup,
texture_bind_group_layout: wgpu::BindGroupLayout,
+ /// Uniform buffers each holding a single `u32`:
+ /// 1 if the texture sampler uses nearest filtering, 0 otherwise.
+ /// Indexed by that flag value.
+ /// Read by the shader when `predictable_texture_filtering` is on.
+ nearest_filtering_flag_buffers: [wgpu::Buffer; 2],
+
/// Map of egui texture IDs to textures and their associated bindgroups (texture view +
/// sampler). The texture may be None if the `TextureId` is just a handle to a user-provided
/// sampler.
@@ -347,10 +353,28 @@ impl Renderer {
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
+ wgpu::BindGroupLayoutEntry {
+ binding: 2,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Buffer {
+ has_dynamic_offset: false,
+ min_binding_size: NonZeroU64::new(core::mem::size_of::() as _),
+ ty: wgpu::BufferBindingType::Uniform,
+ },
+ count: None,
+ },
],
})
};
+ let nearest_filtering_flag_buffers = [0_u32, 1_u32].map(|flag| {
+ device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
+ label: Some(&format!("egui_nearest_filtering_flag_{flag}")),
+ contents: bytemuck::bytes_of(&flag),
+ usage: wgpu::BufferUsages::UNIFORM,
+ })
+ });
+
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("egui_pipeline_layout"),
bind_group_layouts: &[
@@ -458,6 +482,7 @@ impl Renderer {
previous_uniform_buffer_content: UniformBuffer::zeroed(),
uniform_bind_group,
texture_bind_group_layout,
+ nearest_filtering_flag_buffers,
textures: HashMap::default(),
next_user_texture_id: 0,
samplers: HashMap::default(),
@@ -709,6 +734,8 @@ impl Renderer {
};
let bind_group = bind_group.unwrap_or_else(|| {
+ let nearest =
+ image_delta.options.magnification == epaint::textures::TextureFilter::Nearest;
let sampler = self
.samplers
.entry(image_delta.options)
@@ -727,6 +754,11 @@ impl Renderer {
binding: 1,
resource: wgpu::BindingResource::Sampler(sampler),
},
+ wgpu::BindGroupEntry {
+ binding: 2,
+ resource: self.nearest_filtering_flag_buffers[usize::from(nearest)]
+ .as_entire_binding(),
+ },
],
})
});
@@ -829,6 +861,7 @@ impl Renderer {
) -> epaint::TextureId {
profiling::function_scope!();
+ let nearest = sampler_descriptor.mag_filter == wgpu::FilterMode::Nearest;
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
compare: None,
..sampler_descriptor
@@ -846,6 +879,11 @@ impl Renderer {
binding: 1,
resource: wgpu::BindingResource::Sampler(&sampler),
},
+ wgpu::BindGroupEntry {
+ binding: 2,
+ resource: self.nearest_filtering_flag_buffers[usize::from(nearest)]
+ .as_entire_binding(),
+ },
],
});
@@ -885,6 +923,7 @@ impl Renderer {
.get_mut(&id)
.expect("Tried to update a texture that has not been allocated yet.");
+ let nearest = sampler_descriptor.mag_filter == wgpu::FilterMode::Nearest;
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
compare: None,
..sampler_descriptor
@@ -902,6 +941,11 @@ impl Renderer {
binding: 1,
resource: wgpu::BindingResource::Sampler(&sampler),
},
+ wgpu::BindGroupEntry {
+ binding: 2,
+ resource: self.nearest_filtering_flag_buffers[usize::from(nearest)]
+ .as_entire_binding(),
+ },
],
});
diff --git a/crates/egui_demo_lib/tests/misc.rs b/crates/egui_demo_lib/tests/misc.rs
index 427710629..07b61215a 100644
--- a/crates/egui_demo_lib/tests/misc.rs
+++ b/crates/egui_demo_lib/tests/misc.rs
@@ -1,6 +1,41 @@
use egui::{Color32, accesskit::Role};
use egui_kittest::{Harness, kittest::Queryable as _};
+/// Textures with [`egui::TextureOptions::NEAREST`] should render crisp,
+/// also with kittest's predictable texture filtering.
+#[test]
+fn test_nearest_texture_filtering() {
+ let mut texture: Option = None;
+ let mut harness = Harness::builder()
+ .with_size(egui::Vec2::new(80.0, 48.0))
+ .build_ui(move |ui| {
+ let texture = texture.get_or_insert_with(|| {
+ let pixels = [
+ Color32::BLACK,
+ Color32::WHITE,
+ Color32::BLACK,
+ Color32::WHITE,
+ Color32::WHITE,
+ Color32::BLACK,
+ Color32::WHITE,
+ Color32::BLACK,
+ ];
+ let image = egui::ColorImage::new([4, 2], pixels.to_vec());
+ ui.ctx()
+ .load_texture("checkerboard", image, egui::TextureOptions::NEAREST)
+ });
+
+ let rect = egui::Rect::from_min_size(egui::pos2(8.0, 8.0), egui::vec2(64.0, 32.0));
+ let uv = egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0));
+ ui.painter().add(
+ egui::epaint::RectShape::filled(rect, 0, Color32::WHITE)
+ .with_texture(texture.id(), uv),
+ );
+ });
+ harness.run();
+ harness.snapshot("nearest_texture_filtering");
+}
+
#[test]
fn test_kerning() {
let mut results = egui_kittest::SnapshotResults::new();
diff --git a/crates/egui_demo_lib/tests/snapshots/nearest_texture_filtering.png b/crates/egui_demo_lib/tests/snapshots/nearest_texture_filtering.png
new file mode 100644
index 000000000..fedbc8f13
--- /dev/null
+++ b/crates/egui_demo_lib/tests/snapshots/nearest_texture_filtering.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:72e133c853ab933d37711665ce1278f8f23ce35b8a74a46559f269d0dfd0bf40
+size 353
From f5c9373e263b09ccb74e78170b767c69120d8029 Mon Sep 17 00:00:00 2001
From: Emil Ernerfeldt
Date: Fri, 21 Aug 2026 01:33:54 -0700
Subject: [PATCH 05/16] Add `epaint::RoundedRect` primitive (#8440)
---
crates/epaint/src/lib.rs | 2 +
crates/epaint/src/rounded_rect.rs | 152 ++++++++++++++++++++++++++++++
crates/epaint/src/shadow.rs | 12 ++-
crates/epaint/src/tessellator.rs | 28 +++---
4 files changed, 173 insertions(+), 21 deletions(-)
create mode 100644 crates/epaint/src/rounded_rect.rs
diff --git a/crates/epaint/src/lib.rs b/crates/epaint/src/lib.rs
index bff5c79a3..c3e7a5b74 100644
--- a/crates/epaint/src/lib.rs
+++ b/crates/epaint/src/lib.rs
@@ -33,6 +33,7 @@ mod margin;
mod margin_f32;
mod mesh;
pub mod mutex;
+mod rounded_rect;
mod shadow;
pub mod shape_transform;
mod shapes;
@@ -56,6 +57,7 @@ pub use self::{
margin::Margin,
margin_f32::*,
mesh::{Mesh, Mesh16, Vertex},
+ rounded_rect::RoundedRect,
shadow::Shadow,
shapes::{
CircleShape, CubicBezierShape, EllipseShape, PaintCallback, PaintCallbackInfo, PathShape,
diff --git a/crates/epaint/src/rounded_rect.rs b/crates/epaint/src/rounded_rect.rs
new file mode 100644
index 000000000..e5023dcd0
--- /dev/null
+++ b/crates/epaint/src/rounded_rect.rs
@@ -0,0 +1,152 @@
+use emath::{Pos2, Rect, Vec2, vec2};
+
+use crate::CornerRadiusF32;
+
+/// A rectangle geometry with rounded corners.
+///
+/// Not a painting primitive. For that, see [`crate::RectShape`].
+#[derive(Copy, Clone, Debug, PartialEq)]
+#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
+pub struct RoundedRect {
+ rect: Rect,
+ corner_radius: CornerRadiusF32,
+}
+
+impl RoundedRect {
+ /// The corner radius is clamped to half the size of the rectangle.
+ #[inline]
+ pub fn new(rect: Rect, corner_radius: impl Into) -> Self {
+ let max_radius = 0.5 * rect.size().min_elem();
+ Self {
+ rect,
+ corner_radius: corner_radius.into().at_most(max_radius).at_least(0.0),
+ }
+ }
+
+ #[inline]
+ pub fn rect(&self) -> Rect {
+ self.rect
+ }
+
+ #[inline]
+ pub fn corner_radius(&self) -> CornerRadiusF32 {
+ self.corner_radius
+ }
+
+ /// Split into the rectangle and the corner radius.
+ #[inline]
+ pub fn into_parts(self) -> (Rect, CornerRadiusF32) {
+ let Self {
+ rect,
+ corner_radius,
+ } = self;
+ (rect, corner_radius)
+ }
+
+ /// Expand the rectangle and the corner radii by the given amount.
+ #[inline]
+ #[must_use]
+ pub fn expand(self, amount: f32) -> Self {
+ Self::new(
+ self.rect.expand(amount),
+ self.corner_radius + CornerRadiusF32::same(amount),
+ )
+ }
+
+ /// Clamp the given position to lie within this rounded rectangle.
+ ///
+ /// Positions in the corner regions are projected onto the corner arcs.
+ pub fn clamp_pos(&self, pos: Pos2) -> Pos2 {
+ let Self {
+ rect,
+ corner_radius,
+ } = *self;
+ let pos = rect.clamp(pos);
+ let corners = [
+ (corner_radius.nw, vec2(-1.0, -1.0)),
+ (corner_radius.ne, vec2(1.0, -1.0)),
+ (corner_radius.sw, vec2(-1.0, 1.0)),
+ (corner_radius.se, vec2(1.0, 1.0)),
+ ];
+ for (radius, dir) in corners {
+ let arc_center = rect.center() + dir * (rect.size() / 2.0 - Vec2::splat(radius));
+ let offset = pos - arc_center;
+ if 0.0 < offset.x * dir.x && 0.0 < offset.y * dir.y && radius < offset.length() {
+ return arc_center + (radius / offset.length()) * offset;
+ }
+ }
+ pos
+ }
+}
+
+impl From for RoundedRect {
+ #[inline]
+ fn from(rect: Rect) -> Self {
+ Self {
+ rect,
+ corner_radius: CornerRadiusF32::ZERO,
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use emath::pos2;
+
+ use super::*;
+
+ #[test]
+ fn clamp_pos() {
+ let rect = Rect::from_min_max(pos2(0.0, 0.0), pos2(100.0, 100.0));
+ let rounded = RoundedRect::new(
+ rect,
+ CornerRadiusF32 {
+ nw: 10.0,
+ ne: 0.0,
+ sw: 0.0,
+ se: 20.0,
+ },
+ );
+
+ // Interior point is untouched:
+ assert_eq!(rounded.clamp_pos(pos2(50.0, 50.0)), pos2(50.0, 50.0));
+
+ // Sharp corner is untouched:
+ assert_eq!(rounded.clamp_pos(pos2(100.0, 0.0)), pos2(100.0, 0.0));
+
+ // Outside the rect is clamped to the edge:
+ assert_eq!(rounded.clamp_pos(pos2(-10.0, 50.0)), pos2(0.0, 50.0));
+
+ // Rounded corner is projected onto the arc:
+ let clamped = rounded.clamp_pos(pos2(0.0, 0.0));
+ let arc_center = pos2(10.0, 10.0);
+ assert!((clamped - arc_center).length() - 10.0 < 0.001);
+ let expected = 10.0 - 10.0 / core::f32::consts::SQRT_2;
+ assert!((clamped - pos2(expected, expected)).length() < 0.001);
+
+ // Point on the arc stays put:
+ assert_eq!(rounded.clamp_pos(pos2(10.0, 0.0)), pos2(10.0, 0.0));
+ }
+
+ #[test]
+ fn expand() {
+ let rect = Rect::from_min_max(pos2(10.0, 10.0), pos2(90.0, 90.0));
+ let expanded = RoundedRect::new(rect, 20.0).expand(10.0);
+ assert_eq!(
+ expanded.rect(),
+ Rect::from_min_max(pos2(0.0, 0.0), pos2(100.0, 100.0))
+ );
+ assert_eq!(expanded.corner_radius(), CornerRadiusF32::same(30.0));
+ }
+
+ #[test]
+ fn oversized_radius_is_clamped() {
+ // A radius larger than half the rect is clamped, like in the tessellator:
+ let rect = Rect::from_min_max(pos2(0.0, 0.0), pos2(100.0, 100.0));
+ assert_eq!(RoundedRect::new(rect, 200.0), RoundedRect::new(rect, 50.0));
+ assert_eq!(
+ RoundedRect::new(rect, 200.0).corner_radius(),
+ CornerRadiusF32::same(50.0)
+ );
+ }
+}
diff --git a/crates/epaint/src/shadow.rs b/crates/epaint/src/shadow.rs
index 251b57b7a..11dd3f1d1 100644
--- a/crates/epaint/src/shadow.rs
+++ b/crates/epaint/src/shadow.rs
@@ -1,4 +1,4 @@
-use crate::{Color32, CornerRadius, MarginF32, Rect, RectShape, Vec2};
+use crate::{Color32, CornerRadius, MarginF32, Rect, RectShape, RoundedRect, Vec2};
/// The color and fuzziness of a fuzzy shape.
///
@@ -56,10 +56,12 @@ impl Shadow {
} = *self;
let [offset_x, offset_y] = offset;
- let rect = rect
- .translate(Vec2::new(offset_x as _, offset_y as _))
- .expand(spread as _);
- let corner_radius = corner_radius.into() + CornerRadius::from(spread);
+ let (rect, corner_radius) = RoundedRect::new(
+ rect.translate(Vec2::new(offset_x as _, offset_y as _)),
+ corner_radius.into(),
+ )
+ .expand(f32::from(spread))
+ .into_parts();
RectShape::filled(rect, corner_radius, color).with_blur_width(blur as _)
}
diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs
index a9784f48b..6f3a3698b 100644
--- a/crates/epaint/src/tessellator.rs
+++ b/crates/epaint/src/tessellator.rs
@@ -11,8 +11,8 @@ use emath::{
use crate::{
CircleShape, ClippedPrimitive, ClippedShape, Color32, CornerRadiusF32, CubicBezierShape,
- EllipseShape, Mesh, PathShape, Primitive, QuadraticBezierShape, RectShape, Shape, Stroke,
- StrokeKind, TextShape, TextureId, Vertex, color::ColorMode, emath, stroke::PathStroke,
+ EllipseShape, Mesh, PathShape, Primitive, QuadraticBezierShape, RectShape, RoundedRect, Shape,
+ Stroke, StrokeKind, TextShape, TextureId, Vertex, color::ColorMode, emath, stroke::PathStroke,
texture_atlas::PreparedDisc,
};
@@ -536,18 +536,19 @@ impl Path {
pub mod path {
//! Helpers for constructing paths
- use crate::CornerRadiusF32;
- use emath::{Pos2, Rect, pos2};
+ use crate::{CornerRadiusF32, RoundedRect};
+ use emath::{Pos2, pos2};
/// overwrites existing points
- pub fn rounded_rectangle(path: &mut Vec, rect: Rect, cr: CornerRadiusF32) {
+ pub fn rounded_rectangle(path: &mut Vec, rounded_rect: RoundedRect) {
path.clear();
+ // The corner radius is already clamped to half the rect size by `RoundedRect`:
+ let (rect, cr) = rounded_rect.into_parts();
+
let min = rect.min;
let max = rect.max;
- let cr = clamp_corner_radius(cr, rect);
-
if cr == CornerRadiusF32::ZERO {
path.reserve(4);
path.push(pos2(min.x, min.y)); // left top
@@ -633,14 +634,6 @@ pub mod path {
path.extend(quadrant_vertices.iter().map(|&n| center + radius * n));
}
}
-
- // Ensures the radius of each corner is within a valid range
- 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);
- cr.at_most(max_cr).at_least(0.0)
- }
}
// ----------------------------------------------------------------------------
@@ -1938,7 +1931,10 @@ impl Tessellator {
let path = &mut self.scratchpad_path;
path.clear();
- path::rounded_rectangle(&mut self.scratchpad_points, rect, corner_radius);
+ path::rounded_rectangle(
+ &mut self.scratchpad_points,
+ RoundedRect::new(rect, corner_radius),
+ );
// Apply rotation if angle is non-zero
if angle != 0.0 {
From 97603fc082aa4eecfeb7feccbcb6c2507dffaf28 Mon Sep 17 00:00:00 2001
From: YouStones <100306716+AdrienZianne@users.noreply.github.com>
Date: Fri, 21 Aug 2026 15:05:45 +0200
Subject: [PATCH 06/16] Theme plugin system (experimental) (#8153)
# What it does
Addition of a new system of theme plugins which allow the user to use
different rules engine to compute the style for the available
specialised widget style.
# How to use
Create a engine implementing the trait `ThemePlugin` and `ThemeStyle` and implement the necessary methods, then register this
way (example for `ButtonStyle`):
```ui.add_theme::(&mycustomengine);```
Now all button will call the `ThemeStyle` method to compute the correct style and later use the cached value to avoid the costly computation.
If no valid `ThemeStyle` or engine is available then it fallback to the default style.
* Closes part of
* [x] I have followed the instructions in the PR template
---------
Co-authored-by: adrien <221212@umons.ac.be>
Co-authored-by: Emil Ernerfeldt
Co-authored-by: Claude Opus 5 (1M context)
Co-authored-by: Lucas Meurer
---
Cargo.lock | 8 +
crates/eframe/Cargo.toml | 6 +
crates/egui/Cargo.toml | 6 +
crates/egui/src/context.rs | 47 ++-
crates/egui/src/lib.rs | 7 +
crates/egui/src/theme/default_style.rs | 156 +++++++++
crates/egui/src/theme/mod.rs | 48 +++
crates/egui/src/theme/style_provider.rs | 17 +
crates/egui/src/theme/themes.rs | 96 ++++++
crates/egui/src/widget_style.rs | 318 ------------------
crates/egui/src/widget_style/classes.rs | 109 ++++++
crates/egui/src/widget_style/mod.rs | 149 ++++++++
crates/egui/src/widgets/button.rs | 2 +-
crates/egui/src/widgets/checkbox.rs | 5 +-
crates/egui/src/widgets/separator.rs | 4 +-
.../tests/snapshots/demos/Dancing Strings.png | 4 +-
examples/styling_engine/Cargo.toml | 20 ++
examples/styling_engine/README.md | 10 +
examples/styling_engine/src/main.rs | 162 +++++++++
tests/egui_tests/Cargo.toml | 2 +-
tests/egui_tests/tests/test_atoms.rs | 17 +-
21 files changed, 857 insertions(+), 336 deletions(-)
create mode 100644 crates/egui/src/theme/default_style.rs
create mode 100644 crates/egui/src/theme/mod.rs
create mode 100644 crates/egui/src/theme/style_provider.rs
create mode 100644 crates/egui/src/theme/themes.rs
delete mode 100644 crates/egui/src/widget_style.rs
create mode 100644 crates/egui/src/widget_style/classes.rs
create mode 100644 crates/egui/src/widget_style/mod.rs
create mode 100644 examples/styling_engine/Cargo.toml
create mode 100644 examples/styling_engine/README.md
create mode 100644 examples/styling_engine/src/main.rs
diff --git a/Cargo.lock b/Cargo.lock
index 71ed40f62..88207d5a9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4479,6 +4479,14 @@ dependencies = [
"float-cmp",
]
+[[package]]
+name = "styling_engine"
+version = "0.1.0"
+dependencies = [
+ "eframe",
+ "env_logger",
+]
+
[[package]]
name = "subtle"
version = "2.6.1"
diff --git a/crates/eframe/Cargo.toml b/crates/eframe/Cargo.toml
index 4b30ad18d..a9475365e 100644
--- a/crates/eframe/Cargo.toml
+++ b/crates/eframe/Cargo.toml
@@ -53,6 +53,12 @@ android-native-activity = ["egui-winit/android-native-activity"]
## If you plan on specifying your own fonts you may disable this feature.
default_fonts = ["egui/default_fonts"]
+## Enable experimental egui features that might have massive breaking changes or be removed entirely in future updates.
+## Enabling this won't break semver, it's just future compatibility risk.
+##
+## Currently, this enables the theme plugin.
+experimental = ["egui/experimental"]
+
## Enable [`glow`](https://github.com/grovesNL/glow) for painting, via [`egui_glow`](https://github.com/emilk/egui/tree/main/crates/egui_glow).
##
## There is generally no need to enable both the `wgpu` and `glow` features,
diff --git a/crates/egui/Cargo.toml b/crates/egui/Cargo.toml
index 4aee66384..0bbd8494e 100644
--- a/crates/egui/Cargo.toml
+++ b/crates/egui/Cargo.toml
@@ -45,6 +45,12 @@ color-hex = ["epaint/color-hex"]
## If you plan on specifying your own fonts you may disable this feature.
default_fonts = ["epaint/default_fonts"]
+## Enable experimental egui features that might have massive breaking changes or be removed entirely in future updates.
+## Enabling this won't break semver, it's just future compatibility risk.
+##
+## Currently, this enables the theme plugin.
+experimental = []
+
## [`mint`](https://docs.rs/mint) enables interoperability with other math libraries such as [`glam`](https://docs.rs/glam) and [`nalgebra`](https://docs.rs/nalgebra).
mint = ["epaint/mint"]
diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs
index 25b384c95..48c63f436 100644
--- a/crates/egui/src/context.rs
+++ b/crates/egui/src/context.rs
@@ -36,9 +36,10 @@ use crate::{
output::{FullOutput, LogicOutput},
pass_state::PassState,
plugin::{self, TypedPluginHandle},
- resize, response, scroll_area,
+ resize, response, scroll_area, theme,
util::IdTypeMap,
viewport::ViewportClass,
+ widget_style::{StyleArgs, WidgetStyle},
};
use crate::IdMap;
@@ -412,6 +413,8 @@ struct ContextImpl {
is_accesskit_enabled: bool,
loaders: Arc,
+
+ themes: theme::Themes,
}
impl ContextImpl {
@@ -2093,6 +2096,48 @@ impl Context {
}
}
+/// Experimental theming, gated behind the `experimental_theme` feature.
+impl Context {
+ /// Register a [`StyleProvider`](crate::theme::StyleProvider) for the specified widget type.
+ ///
+ /// A theme can only be added once for a specified widget.
+ /// If a theme is already registered for this widget, this is a no-op (useful for `eframe::run_simple_native`).
+ ///
+ /// If you want to add the theme anyway, use [`Self::replace_widget_theme`] instead.
+ #[cfg(feature = "experimental")]
+ pub fn add_widget_theme(
+ &self,
+ theme: impl theme::StyleProvider + Send + Sync + 'static,
+ ) {
+ self.write(|ctx| ctx.themes.register::(theme, false));
+ }
+
+ /// Register a [`StyleProvider`](crate::theme::StyleProvider) for the specified widget.
+ ///
+ /// Overwrite any theme already registered for the specified widget [`WidgetStyle`].
+ /// This allow to live edit a theme.
+ #[cfg(feature = "experimental")]
+ pub fn replace_widget_theme(
+ &self,
+ theme: impl theme::StyleProvider + Send + Sync + 'static,
+ ) {
+ self.write(|ctx| ctx.themes.register::(theme, true));
+ }
+
+ /// Compute the `WidgetStyle` using the registered theme.
+ ///
+ /// The types you need to call this (e.g. `StyleArgs`) are only public
+ /// with the `experimental_theme` feature.
+ #[cfg_attr(not(feature = "experimental"), doc(hidden))]
+ pub fn get_widget_style(
+ &self,
+ modifiers: &StyleArgs<'_>,
+ ) -> S {
+ let theme = self.read(move |ctx| ctx.themes.get::());
+ theme.lock().style(modifiers)
+ }
+}
+
impl Context {
/// Tell `egui` which fonts to use.
///
diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs
index 27ab7035c..2d4099d92 100644
--- a/crates/egui/src/lib.rs
+++ b/crates/egui/src/lib.rs
@@ -417,13 +417,20 @@ pub mod response;
mod sense;
pub mod style;
pub mod text_selection;
+#[cfg(feature = "experimental")]
+pub mod theme;
+#[cfg(not(feature = "experimental"))]
+mod theme;
mod ui;
mod ui_builder;
mod ui_stack;
pub mod util;
pub mod viewport;
mod widget_rect;
+#[cfg(feature = "experimental")]
pub mod widget_style;
+#[cfg(not(feature = "experimental"))]
+mod widget_style;
pub mod widget_text;
pub mod widgets;
diff --git a/crates/egui/src/theme/default_style.rs b/crates/egui/src/theme/default_style.rs
new file mode 100644
index 000000000..a23ef8fcd
--- /dev/null
+++ b/crates/egui/src/theme/default_style.rs
@@ -0,0 +1,156 @@
+use emath::Vec2;
+use epaint::{Shadow, Stroke, text::TextWrapMode};
+
+use crate::{
+ Frame, TextStyle,
+ theme::StyleProvider,
+ widget_style::{
+ BaseStyle, ButtonStyle, CheckboxStyle, HasClasses as _, LabelStyle, SELECTED_CLASS,
+ SeparatorStyle, StyleArgs, TextVisuals, WidgetState,
+ },
+};
+
+/// The default [`StyleProvider`], implementing the default egui look based on
+/// [`crate::style::WidgetVisuals`].
+#[derive(Debug, Clone)]
+pub struct DefaultStyle;
+
+impl StyleProvider for DefaultStyle {
+ fn style(&mut self, modifiers: &StyleArgs<'_>) -> BaseStyle {
+ let StyleArgs { style, state, .. } = modifiers;
+ let spacing = &style.spacing;
+ let widget_visuals = match state {
+ WidgetState::Noninteractive => style.visuals.widgets.noninteractive,
+ WidgetState::Inactive => style.visuals.widgets.inactive,
+ WidgetState::Hovered => style.visuals.widgets.hovered,
+ WidgetState::Active => style.visuals.widgets.active,
+ };
+
+ BaseStyle {
+ frame: Frame {
+ fill: widget_visuals.bg_fill,
+ stroke: widget_visuals.bg_stroke,
+ corner_radius: widget_visuals.corner_radius,
+ inner_margin: spacing.button_padding.into(),
+ ..Default::default()
+ },
+ stroke: widget_visuals.fg_stroke,
+ text: TextVisuals {
+ color: widget_visuals.text_color(),
+ font_id: modifiers
+ .style
+ .override_font_id
+ .clone()
+ .unwrap_or_else(|| TextStyle::Body.resolve(style)),
+ strikethrough: Stroke::NONE,
+ underline: Stroke::NONE,
+ },
+ }
+ }
+}
+
+impl StyleProvider for DefaultStyle {
+ fn style(&mut self, modifiers: &StyleArgs<'_>) -> ButtonStyle {
+ let StyleArgs {
+ ctx,
+ classes,
+ style,
+ state,
+ ..
+ } = modifiers;
+ let spacing = &style.spacing;
+ let mut widget_visuals = match state {
+ WidgetState::Noninteractive => style.visuals.widgets.noninteractive,
+ WidgetState::Inactive => style.visuals.widgets.inactive,
+ WidgetState::Hovered => style.visuals.widgets.hovered,
+ WidgetState::Active => style.visuals.widgets.active,
+ };
+
+ let mut ws: BaseStyle = ctx.get_widget_style(modifiers);
+
+ if classes.has(SELECTED_CLASS) {
+ let visuals = &style.visuals;
+ widget_visuals.weak_bg_fill = visuals.selection.bg_fill;
+ widget_visuals.bg_fill = visuals.selection.bg_fill;
+ widget_visuals.fg_stroke = visuals.selection.stroke;
+ ws.text.color = visuals.selection.stroke.color;
+ }
+
+ ButtonStyle {
+ frame: Frame {
+ fill: widget_visuals.weak_bg_fill,
+ stroke: widget_visuals.bg_stroke,
+ corner_radius: widget_visuals.corner_radius,
+ outer_margin: (-Vec2::splat(widget_visuals.expansion)).into(),
+ inner_margin: (spacing.button_padding + Vec2::splat(widget_visuals.expansion)
+ - Vec2::splat(widget_visuals.bg_stroke.width))
+ .into(),
+ ..Default::default()
+ },
+ text_style: ws.text,
+ }
+ }
+}
+
+impl StyleProvider for DefaultStyle {
+ fn style(&mut self, modifiers: &StyleArgs<'_>) -> CheckboxStyle {
+ let StyleArgs {
+ ctx, style, state, ..
+ } = modifiers;
+ let spacing = &style.spacing;
+ let widget_visuals = match state {
+ WidgetState::Noninteractive => style.visuals.widgets.noninteractive,
+ WidgetState::Inactive => style.visuals.widgets.inactive,
+ WidgetState::Hovered => style.visuals.widgets.hovered,
+ WidgetState::Active => style.visuals.widgets.active,
+ };
+
+ let ws: BaseStyle = ctx.get_widget_style(modifiers);
+
+ CheckboxStyle {
+ frame: Frame::new(),
+ checkbox_size: spacing.icon_width,
+ check_size: spacing.icon_width_inner,
+ checkbox_frame: Frame {
+ fill: widget_visuals.bg_fill,
+ corner_radius: widget_visuals.corner_radius,
+ stroke: widget_visuals.bg_stroke,
+ ..Default::default()
+ },
+ text_style: ws.text,
+ check_stroke: ws.stroke,
+ }
+ }
+}
+
+impl StyleProvider for DefaultStyle {
+ fn style(&mut self, modifiers: &StyleArgs<'_>) -> LabelStyle {
+ let StyleArgs { ctx, .. } = modifiers;
+ let ws: BaseStyle = ctx.get_widget_style(modifiers);
+
+ LabelStyle {
+ frame: Frame {
+ fill: ws.frame.fill,
+ inner_margin: 0.0.into(),
+ outer_margin: 0.0.into(),
+ stroke: Stroke::NONE,
+ shadow: Shadow::NONE,
+ corner_radius: 0.into(),
+ },
+ text: ws.text,
+ wrap_mode: TextWrapMode::Wrap,
+ }
+ }
+}
+
+impl StyleProvider for DefaultStyle {
+ fn style(&mut self, modifiers: &StyleArgs<'_>) -> SeparatorStyle {
+ let StyleArgs { style, .. } = modifiers;
+
+ SeparatorStyle {
+ spacing: 6.0,
+ // A separator is never interactive, so its stroke doesn't depend on the widget state:
+ stroke: style.visuals.widgets.noninteractive.bg_stroke,
+ }
+ }
+}
diff --git a/crates/egui/src/theme/mod.rs b/crates/egui/src/theme/mod.rs
new file mode 100644
index 000000000..088d7f778
--- /dev/null
+++ b/crates/egui/src/theme/mod.rs
@@ -0,0 +1,48 @@
+//! Theming: pluggable [`StyleProvider`]s that compute the style of each widget.
+
+// This module is only public with the `experimental_theme` feature,
+// so without it a lot of it looks unused:
+#![cfg_attr(not(feature = "experimental"), allow(dead_code, unused_imports))]
+
+mod default_style;
+mod style_provider;
+mod themes;
+
+pub use self::{default_style::DefaultStyle, style_provider::StyleProvider, themes::Themes};
+
+use crate::{
+ Ui,
+ widget_style::{Classes, StyleArgs, WidgetState, WidgetStyle},
+};
+
+impl Ui {
+ /// The style of the widget with the given [`crate::Id`] and `Classes`,
+ /// as computed by the registered theme.
+ ///
+ /// The types you need to call this are only public with the
+ /// `experimental_theme` feature.
+ #[cfg_attr(not(feature = "experimental"), doc(hidden))]
+ pub fn widget_style(
+ &self,
+ id: crate::Id,
+ classes: &Classes,
+ ) -> S {
+ // Fetch the state of the widget, as it was in the previous pass
+ let state = if let Some(response) = self.read_response(id) {
+ response.widget_state()
+ } else {
+ // We don't know the state of the widget yet, so we would style it wrong.
+ // It will be styled correctly on next frame.
+ self.ctx().request_repaint();
+ WidgetState::default()
+ };
+
+ self.get_widget_style::(&StyleArgs {
+ classes,
+ state,
+ style: self.style(),
+ stack: self.stack(),
+ ctx: self,
+ })
+ }
+}
diff --git a/crates/egui/src/theme/style_provider.rs b/crates/egui/src/theme/style_provider.rs
new file mode 100644
index 000000000..5f9205a85
--- /dev/null
+++ b/crates/egui/src/theme/style_provider.rs
@@ -0,0 +1,17 @@
+use core::any::TypeId;
+
+use crate::widget_style::StyleArgs;
+
+/// A Theme plugin that implement a style computation for a defined `WidgetStyle`
+pub trait StyleProvider {
+ /// The style according to the classes and state of the widget
+ fn style(&mut self, modifiers: &StyleArgs<'_>) -> S;
+
+ /// Used to tell different themes apart
+ fn type_id(&self) -> TypeId
+ where
+ Self: 'static,
+ {
+ TypeId::of::()
+ }
+}
diff --git a/crates/egui/src/theme/themes.rs b/crates/egui/src/theme/themes.rs
new file mode 100644
index 000000000..4294f9460
--- /dev/null
+++ b/crates/egui/src/theme/themes.rs
@@ -0,0 +1,96 @@
+use std::sync::Arc;
+
+use epaint::mutex::Mutex;
+
+use crate::{
+ Id,
+ theme::{StyleProvider, default_style::DefaultStyle},
+ util::IdTypeMap,
+ widget_style::{
+ BaseStyle, ButtonStyle, CheckboxStyle, LabelStyle, SeparatorStyle, WidgetStyle,
+ },
+};
+
+/// The registry of [`StyleProvider`]s, one per [`WidgetStyle`] type.
+///
+/// Each widget asks this registry for the provider of its style type
+/// (e.g. [`ButtonStyle`]), and that provider computes the final style from the
+/// widget's classes and state.
+///
+/// A default provider is registered for every built-in style; register your
+/// own with [`Context::add_widget_theme`](crate::Context::add_widget_theme) or
+/// [`Context::replace_widget_theme`](crate::Context::replace_widget_theme).
+pub struct Themes {
+ themes: IdTypeMap,
+}
+
+type ThemeWrap = Arc + Send + Sync>>>;
+
+impl Default for Themes {
+ /// Register the default egui theme
+ fn default() -> Self {
+ let mut themes = IdTypeMap::default();
+
+ themes.insert_temp::>(
+ Id::NULL,
+ Arc::new(Mutex::new(Box::new(DefaultStyle))),
+ );
+
+ themes.insert_temp::>(
+ Id::NULL,
+ Arc::new(Mutex::new(Box::new(DefaultStyle))),
+ );
+
+ themes.insert_temp::>(
+ Id::NULL,
+ Arc::new(Mutex::new(Box::new(DefaultStyle))),
+ );
+
+ themes.insert_temp::>(
+ Id::NULL,
+ Arc::new(Mutex::new(Box::new(DefaultStyle))),
+ );
+
+ themes.insert_temp::>(
+ Id::NULL,
+ Arc::new(Mutex::new(Box::new(DefaultStyle))),
+ );
+
+ Self { themes }
+ }
+}
+
+impl Themes {
+ /// Register a [`StyleProvider`] for the specified widget [`WidgetStyle`] `S`
+ ///
+ /// Existing themes are overwritten if `force` is `true` or the new theme differs.
+ pub(crate) fn register(
+ &mut self,
+ theme: impl StyleProvider + Send + Sync + 'static,
+ force: bool,
+ ) {
+ if !force
+ && self
+ .themes
+ .get_temp::>(Id::NULL)
+ .is_some_and(|t| t.lock().type_id() == theme.type_id())
+ {
+ return;
+ }
+
+ self.themes
+ .insert_temp::>(Id::NULL, Arc::new(Mutex::new(Box::new(theme))));
+ }
+
+ /// Fetch the style of the current theme
+ pub fn get(&self) -> ThemeWrap {
+ let v = self.themes.get_temp::>(Id::NULL);
+
+ v.unwrap_or_else(|| {
+ panic!(
+ "A style should be set for {:?}",
+ core::any::type_name::()
+ )
+ })
+ }
+}
diff --git a/crates/egui/src/widget_style.rs b/crates/egui/src/widget_style.rs
deleted file mode 100644
index f6239cae9..000000000
--- a/crates/egui/src/widget_style.rs
+++ /dev/null
@@ -1,318 +0,0 @@
-use std::{borrow::Cow, fmt};
-
-use emath::Vec2;
-use epaint::{Color32, FontId, Shadow, Stroke, text::TextWrapMode};
-use smallvec::SmallVec;
-
-use crate::{
- Frame, Response, Style, TextBuffer as _, TextStyle,
- style::{WidgetVisuals, Widgets},
-};
-
-/// General text style
-pub struct TextVisuals {
- /// Font used
- pub font_id: FontId,
-
- /// Font color
- pub color: Color32,
-
- /// Text decoration
- pub underline: Stroke,
- pub strikethrough: Stroke,
-}
-
-/// General widget style
-pub struct WidgetStyle {
- pub frame: Frame,
-
- pub text: TextVisuals,
-
- pub stroke: Stroke,
-}
-
-/// Dedicated button style
-pub struct ButtonStyle {
- pub frame: Frame,
- pub text_style: TextVisuals,
-}
-
-/// Dedicated checkbox style
-pub struct CheckboxStyle {
- /// Frame around
- pub frame: Frame,
-
- /// Text next to it
- pub text_style: TextVisuals,
-
- /// Checkbox size
- pub checkbox_size: f32,
-
- /// Checkmark size
- pub check_size: f32,
-
- /// Frame of the checkbox itself
- pub checkbox_frame: Frame,
-
- /// Checkmark stroke
- pub check_stroke: Stroke,
-}
-
-/// Dedicated label style
-pub struct LabelStyle {
- /// Frame around
- pub frame: Frame,
-
- /// Text style
- pub text: TextVisuals,
-
- /// Wrap mode used
- pub wrap_mode: TextWrapMode,
-}
-
-/// Dedicated separator style
-pub struct SeparatorStyle {
- /// How much space is allocated in the layout direction
- pub spacing: f32,
-
- /// How to paint it
- pub stroke: Stroke,
-}
-
-/// The different state of a widget can be
-#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
-pub enum WidgetState {
- Noninteractive,
- #[default]
- Inactive,
- Hovered,
- Active,
-}
-
-impl Widgets {
- /// The widget visuals according to the state
- pub fn state(&self, state: WidgetState) -> &WidgetVisuals {
- match state {
- WidgetState::Noninteractive => &self.noninteractive,
- WidgetState::Inactive => &self.inactive,
- WidgetState::Hovered => &self.hovered,
- WidgetState::Active => &self.active,
- }
- }
-}
-
-impl Response {
- pub fn widget_state(&self) -> WidgetState {
- if !self.sense.interactive() {
- WidgetState::Noninteractive
- } else if self.is_pointer_button_down_on() || self.has_focus() || self.clicked() {
- WidgetState::Active
- } else if self.hovered() || self.highlighted() {
- WidgetState::Hovered
- } else {
- WidgetState::Inactive
- }
- }
-}
-
-impl Style {
- /// The general widget style. The style is computed according to the classes and state of the widget.
- pub fn widget_style(&self, _classes: &Classes, state: WidgetState) -> WidgetStyle {
- let visuals = self.visuals.widgets.state(state);
- let font_id = self.override_font_id.clone();
- WidgetStyle {
- frame: Frame {
- fill: visuals.bg_fill,
- stroke: visuals.bg_stroke,
- corner_radius: visuals.corner_radius,
- inner_margin: self.spacing.button_padding.into(),
- ..Default::default()
- },
- stroke: visuals.fg_stroke,
- text: TextVisuals {
- color: self
- .visuals
- .override_text_color
- .unwrap_or_else(|| visuals.text_color()),
- font_id: font_id.unwrap_or_else(|| TextStyle::Body.resolve(self)),
- strikethrough: Stroke::NONE,
- underline: Stroke::NONE,
- },
- }
- }
-
- /// The dedicated button style. The style is computed according to the classes and state of the widget.
- /// It depend on the general widget style.
- pub fn button_style(&self, classes: &Classes, state: WidgetState) -> ButtonStyle {
- let mut visuals = *self.visuals.widgets.state(state);
- let mut ws = self.widget_style(classes, state);
-
- if classes.has(SELECTED_CLASS) {
- visuals.weak_bg_fill = self.visuals.selection.bg_fill;
- visuals.bg_fill = self.visuals.selection.bg_fill;
- visuals.fg_stroke = self.visuals.selection.stroke;
- ws.text.color = self.visuals.selection.stroke.color;
- }
-
- ButtonStyle {
- frame: Frame {
- fill: visuals.weak_bg_fill,
- stroke: visuals.bg_stroke,
- corner_radius: visuals.corner_radius,
- outer_margin: (-Vec2::splat(visuals.expansion)).into(),
- inner_margin: (self.spacing.button_padding + Vec2::splat(visuals.expansion)
- - Vec2::splat(visuals.bg_stroke.width))
- .into(),
- ..Default::default()
- },
- text_style: ws.text,
- }
- }
-
- /// The dedicated checkbox style. The style is computed according to the classes and state of the widget.
- /// It depend on the general widget style.
- pub fn checkbox_style(&self, classes: &Classes, state: WidgetState) -> CheckboxStyle {
- let visuals = self.visuals.widgets.state(state);
- let ws = self.widget_style(classes, state);
- CheckboxStyle {
- frame: Frame::new(),
- checkbox_size: self.spacing.icon_width,
- check_size: self.spacing.icon_width_inner,
- checkbox_frame: Frame {
- fill: visuals.bg_fill,
- corner_radius: visuals.corner_radius,
- stroke: visuals.bg_stroke,
- ..Default::default()
- },
- text_style: ws.text,
- check_stroke: ws.stroke,
- }
- }
-
- /// The dedicated label style. The style is computed according to the classes and state of the widget.
- /// It depend on the general widget style.
- pub fn label_style(&self, classes: &Classes, state: WidgetState) -> LabelStyle {
- let ws = self.widget_style(classes, state);
- LabelStyle {
- frame: Frame {
- fill: ws.frame.fill,
- inner_margin: 0.0.into(),
- outer_margin: 0.0.into(),
- stroke: Stroke::NONE,
- shadow: Shadow::NONE,
- corner_radius: 0.into(),
- },
- text: ws.text,
- wrap_mode: TextWrapMode::Wrap,
- }
- }
-
- /// The dedicated separator style. The style is computed according to the classes and state of the widget.
- /// It depend on the general widget style.
- pub fn separator_style(&self, _classes: &Classes, _state: WidgetState) -> SeparatorStyle {
- let visuals = self.visuals.noninteractive();
- SeparatorStyle {
- spacing: 6.0,
- stroke: visuals.bg_stroke,
- }
- }
-}
-
-/// The root class is a special class present on every top-level [`crate::Ui`].
-pub const ROOT_CLASS: &str = "root";
-
-/// The selected class is a special class present on selected [`crate::Button`].
-pub const SELECTED_CLASS: &str = "selected";
-
-/// A class is a static string identifier.
-pub type ClassName = Cow<'static, str>;
-
-/// Classes are string identifier that can be set on widget/Ui.
-///
-/// This can be used by styling engine to compute a different style
-/// based on the set of classes present on the widget/Ui.
-#[derive(Debug, Default, Clone)]
-pub struct Classes {
- classes: SmallVec<[ClassName; 5]>,
-}
-
-impl Classes {
- /// Add a class to the list if the condition is true
- #[inline]
- fn add_if(&mut self, class: impl Into, condition: bool) {
- if condition {
- self.classes.push(class.into());
- }
- }
-}
-
-impl HasClasses for Classes {
- fn classes(&self) -> &Classes {
- self
- }
-
- fn classes_mut(&mut self) -> &mut Classes {
- self
- }
-}
-
-impl core::fmt::Display for Classes {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- self.classes.iter().for_each(|class| {
- let _ = f.write_str(class.as_str());
- });
- f.write_str("")
- }
-}
-
-/// Any widgets supporting [`Classes`] must implement this trait
-pub trait HasClasses {
- fn classes(&self) -> &Classes;
-
- fn classes_mut(&mut self) -> &mut Classes;
-
- /// Add the given class by consuming [`self`]
- #[inline]
- fn with_class(mut self, class: impl Into) -> Self
- where
- Self: Sized,
- {
- self.classes_mut().add_if(class.into(), true);
- self
- }
-
- /// Add the given class by consuming [`self`] if the condition is true
- #[inline]
- fn with_class_if(mut self, class: impl Into, condition: bool) -> Self
- where
- Self: Sized,
- {
- self.classes_mut().add_if(class.into(), condition);
- self
- }
-
- /// Add the given class in-place
- #[inline]
- fn add_class(&mut self, class: impl Into) -> &mut Self
- where
- Self: Sized,
- {
- self.classes_mut().add_if(class.into(), true);
- self
- }
-
- /// Add the given class in-place if the condition is true
- #[inline]
- fn add_class_if(&mut self, class: impl Into, condition: bool) -> &mut Self
- where
- Self: Sized,
- {
- self.classes_mut().add_if(class.into(), condition);
- self
- }
-
- /// True if the class is present
- fn has(&self, class: impl Into) -> bool {
- self.classes().classes.contains(&class.into())
- }
-}
diff --git a/crates/egui/src/widget_style/classes.rs b/crates/egui/src/widget_style/classes.rs
new file mode 100644
index 000000000..95ef43aae
--- /dev/null
+++ b/crates/egui/src/widget_style/classes.rs
@@ -0,0 +1,109 @@
+use std::{borrow::Cow, fmt};
+
+use smallvec::SmallVec;
+
+use crate::TextBuffer as _;
+
+/// The root class is a special class present on every top-level [`crate::Ui`].
+pub const ROOT_CLASS: &str = "root";
+
+/// The selected class is a special class present on selected [`crate::Button`].
+pub const SELECTED_CLASS: &str = "selected";
+
+/// A class is a static string identifier.
+pub type ClassName = Cow<'static, str>;
+
+/// Classes are string identifier that can be set on widget/Ui.
+///
+/// This can be used by styling engine to compute a different style
+/// based on the set of classes present on the widget/Ui.
+#[derive(Debug, Default, Clone, Hash)]
+pub struct Classes {
+ classes: SmallVec<[ClassName; 5]>,
+}
+
+impl Classes {
+ /// Add a class to the list if the condition is true
+ #[inline]
+ fn add_if(&mut self, class: impl Into, condition: bool) {
+ if condition {
+ self.classes.push(class.into());
+ }
+ }
+}
+
+impl HasClasses for Classes {
+ fn classes(&self) -> &Classes {
+ self
+ }
+
+ fn classes_mut(&mut self) -> &mut Classes {
+ self
+ }
+}
+
+impl core::fmt::Display for Classes {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ self.classes.iter().for_each(|class| {
+ let _ = f.write_str(class.as_str());
+ });
+ f.write_str("")
+ }
+}
+
+/// Any widgets supporting [`Classes`] must implement this trait
+pub trait HasClasses {
+ fn classes(&self) -> &Classes;
+
+ fn classes_mut(&mut self) -> &mut Classes;
+
+ /// Add the given class by consuming `self`
+ #[inline]
+ fn with_class(mut self, class: impl Into) -> Self
+ where
+ Self: Sized,
+ {
+ self.classes_mut().add_if(class.into(), true);
+ self
+ }
+
+ /// Add the given class by consuming `self` if the condition is true
+ #[inline]
+ fn with_class_if(mut self, class: impl Into, condition: bool) -> Self
+ where
+ Self: Sized,
+ {
+ self.classes_mut().add_if(class.into(), condition);
+ self
+ }
+
+ /// Add the given class in-place
+ #[inline]
+ fn add_class(&mut self, class: impl Into) -> &mut Self
+ where
+ Self: Sized,
+ {
+ self.classes_mut().add_if(class.into(), true);
+ self
+ }
+
+ /// Add the given class in-place if the condition is true
+ #[inline]
+ fn add_class_if(&mut self, class: impl Into, condition: bool) -> &mut Self
+ where
+ Self: Sized,
+ {
+ self.classes_mut().add_if(class.into(), condition);
+ self
+ }
+
+ /// True if the class is present
+ fn has(&self, class: impl Into) -> bool {
+ self.classes().classes.contains(&class.into())
+ }
+
+ /// The list of class
+ fn as_slice(&self) -> &[ClassName] {
+ &self.classes().classes
+ }
+}
diff --git a/crates/egui/src/widget_style/mod.rs b/crates/egui/src/widget_style/mod.rs
new file mode 100644
index 000000000..1a2c19959
--- /dev/null
+++ b/crates/egui/src/widget_style/mod.rs
@@ -0,0 +1,149 @@
+// This module is only public with the `experimental_theme` feature,
+// so without it a lot of it looks unused:
+#![cfg_attr(not(feature = "experimental"), allow(dead_code, unused_imports))]
+
+mod classes;
+
+pub use self::classes::{ClassName, Classes, HasClasses, ROOT_CLASS, SELECTED_CLASS};
+
+use core::fmt::Debug;
+
+use epaint::{Color32, FontId, Stroke, text::TextWrapMode};
+
+use crate::{
+ Context, Frame, Response, Style, UiStack,
+ style::{WidgetVisuals, Widgets},
+};
+
+/// Each dedicated style must implement this trait to be used in the theme plugin system
+pub trait WidgetStyle: Debug + Clone + Send + Sync + core::any::Any + 'static {}
+
+/// General text style
+#[derive(Debug, Clone)]
+pub struct TextVisuals {
+ /// Font used
+ pub font_id: FontId,
+
+ /// Font color
+ pub color: Color32,
+
+ /// Text decoration
+ pub underline: Stroke,
+ pub strikethrough: Stroke,
+}
+
+/// General widget style
+#[derive(Debug, Clone)]
+pub struct BaseStyle {
+ pub frame: Frame,
+
+ pub text: TextVisuals,
+
+ pub stroke: Stroke,
+}
+
+impl WidgetStyle for BaseStyle {}
+
+/// Dedicated button style
+#[derive(Debug, Clone)]
+pub struct ButtonStyle {
+ pub frame: Frame,
+ pub text_style: TextVisuals,
+}
+
+impl WidgetStyle for ButtonStyle {}
+
+/// Dedicated checkbox style
+#[derive(Debug, Clone)]
+pub struct CheckboxStyle {
+ /// Frame around
+ pub frame: Frame,
+
+ /// Text next to it
+ pub text_style: TextVisuals,
+
+ /// Checkbox size
+ pub checkbox_size: f32,
+
+ /// Checkmark size
+ pub check_size: f32,
+
+ /// Frame of the checkbox itself
+ pub checkbox_frame: Frame,
+
+ /// Checkmark stroke
+ pub check_stroke: Stroke,
+}
+
+impl WidgetStyle for CheckboxStyle {}
+
+/// Dedicated label style
+#[derive(Debug, Clone)]
+pub struct LabelStyle {
+ /// Frame around
+ pub frame: Frame,
+
+ /// Text style
+ pub text: TextVisuals,
+
+ /// Wrap mode used
+ pub wrap_mode: TextWrapMode,
+}
+
+impl WidgetStyle for LabelStyle {}
+
+/// Dedicated separator style
+#[derive(Debug, Clone)]
+pub struct SeparatorStyle {
+ /// How much space is allocated in the layout direction
+ pub spacing: f32,
+
+ /// How to paint it
+ pub stroke: Stroke,
+}
+
+impl WidgetStyle for SeparatorStyle {}
+
+/// The different state of a widget can be
+#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Hash)]
+pub enum WidgetState {
+ Noninteractive,
+ #[default]
+ Inactive,
+ Hovered,
+ Active,
+}
+
+impl Widgets {
+ /// The widget visuals according to the state
+ pub fn state(&self, state: WidgetState) -> &WidgetVisuals {
+ match state {
+ WidgetState::Noninteractive => &self.noninteractive,
+ WidgetState::Inactive => &self.inactive,
+ WidgetState::Hovered => &self.hovered,
+ WidgetState::Active => &self.active,
+ }
+ }
+}
+
+impl Response {
+ pub fn widget_state(&self) -> WidgetState {
+ if !self.sense.interactive() {
+ WidgetState::Noninteractive
+ } else if self.is_pointer_button_down_on() || self.has_focus() || self.clicked() {
+ WidgetState::Active
+ } else if self.hovered() || self.highlighted() {
+ WidgetState::Hovered
+ } else {
+ WidgetState::Inactive
+ }
+ }
+}
+
+pub struct StyleArgs<'a> {
+ pub classes: &'a Classes,
+ pub state: WidgetState,
+ pub stack: &'a UiStack,
+ pub style: &'a Style,
+ pub ctx: &'a Context,
+}
diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs
index a1d2f84ed..d7f6c8a0c 100644
--- a/crates/egui/src/widgets/button.rs
+++ b/crates/egui/src/widgets/button.rs
@@ -328,7 +328,7 @@ impl<'a> Button<'a> {
classes.add_class_if(SELECTED_CLASS, selected.unwrap_or(false));
- let ButtonStyle { frame, text_style } = ui.style().button_style(&classes, state);
+ let ButtonStyle { frame, text_style } = ui.widget_style(id, &classes);
let mut button_padding = if has_frame_margin {
frame.inner_margin
diff --git a/crates/egui/src/widgets/checkbox.rs b/crates/egui/src/widgets/checkbox.rs
index 0aaedec76..9310c64d5 100644
--- a/crates/egui/src/widgets/checkbox.rs
+++ b/crates/egui/src/widgets/checkbox.rs
@@ -70,9 +70,6 @@ impl Widget for Checkbox<'_> {
// Get the widget style by reading the response from the previous pass
let id = ui.next_auto_id();
- let response: Option = ui.ctx().read_response(id);
- let state = response.map(|r| r.widget_state()).unwrap_or_default();
-
let CheckboxStyle {
check_size,
checkbox_frame,
@@ -80,7 +77,7 @@ impl Widget for Checkbox<'_> {
frame,
check_stroke,
text_style,
- } = ui.style().checkbox_style(&classes, state);
+ } = ui.widget_style(id, &classes);
let mut min_size = Vec2::splat(ui.spacing().interact_size.y);
min_size.y = min_size.y.at_least(checkbox_size);
diff --git a/crates/egui/src/widgets/separator.rs b/crates/egui/src/widgets/separator.rs
index 48d02fe8e..e188d4127 100644
--- a/crates/egui/src/widgets/separator.rs
+++ b/crates/egui/src/widgets/separator.rs
@@ -101,12 +101,10 @@ impl Widget for Separator {
// Get the widget style by reading the response from the previous pass
let id = ui.next_auto_id();
- let response: Option = ui.ctx().read_response(id);
- let state = response.map(|r| r.widget_state()).unwrap_or_default();
let SeparatorStyle {
spacing: spacing_style,
stroke,
- } = ui.style().separator_style(&classes, state);
+ } = ui.widget_style(id, &classes);
// override the spacing if not set
let spacing = spacing.unwrap_or(spacing_style);
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 ec34487f1..cd27c9038 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:05527c073b1ee2f6a15052a9097d1ad515331ff32d572cf510086f9ebb7a7bbc
-size 27253
+oid sha256:ad431823e85de056c95e6e97d1a9f5905880a1ecc34f24da5bc0b611e42fe99b
+size 27260
diff --git a/examples/styling_engine/Cargo.toml b/examples/styling_engine/Cargo.toml
new file mode 100644
index 000000000..038b59fd6
--- /dev/null
+++ b/examples/styling_engine/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "styling_engine"
+version = "0.1.0"
+authors = ["Emil Ernerfeldt "]
+license = "MIT OR Apache-2.0"
+edition = "2024"
+rust-version = "1.92"
+publish = false
+
+[lints]
+workspace = true
+
+
+[dependencies]
+eframe = { workspace = true, features = [
+ "default",
+ "__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO
+ "experimental", # this example is all about the experimental theming API
+] }
+env_logger = { workspace = true, features = ["auto-color", "humantime"] }
diff --git a/examples/styling_engine/README.md b/examples/styling_engine/README.md
new file mode 100644
index 000000000..43f54f5bb
--- /dev/null
+++ b/examples/styling_engine/README.md
@@ -0,0 +1,10 @@
+Example showing how the style engine work.
+
+A custom `StyleProvider` styles every button, using _classes_ to tell them apart.
+The theme can be edited live in the left panel.
+
+```sh
+cargo run -p styling_engine
+```
+
+
diff --git a/examples/styling_engine/src/main.rs b/examples/styling_engine/src/main.rs
new file mode 100644
index 000000000..54763696a
--- /dev/null
+++ b/examples/styling_engine/src/main.rs
@@ -0,0 +1,162 @@
+#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
+
+//! A small styling engine: a custom [`StyleProvider`] that styles every [`egui::Button`]
+//! based on the _classes_ set on it, and that can be edited live.
+
+use eframe::egui::{
+ self, CentralPanel, Color32, Frame, Panel,
+ theme::StyleProvider,
+ widget_style::{BaseStyle, ButtonStyle, HasClasses as _, StyleArgs, WidgetState},
+};
+
+/// Buttons with this class are styled as a destructive action.
+const DANGER: &str = "danger";
+
+/// Our styling engine: it decides what every button looks like.
+#[derive(Clone, Copy, PartialEq)]
+struct MyTheme {
+ normal: Color32,
+ danger: Color32,
+ corner_radius: u8,
+}
+
+impl MyTheme {
+ fn preset(preset: Preset) -> Self {
+ match preset {
+ Preset::Ocean => Self {
+ normal: Color32::from_rgb(0x1E, 0x5A, 0x8A),
+ danger: Color32::from_rgb(0x9B, 0x2C, 0x2C),
+ corner_radius: 4,
+ },
+ Preset::Candy => Self {
+ normal: Color32::from_rgb(0xB8, 0x3B, 0x9E),
+ danger: Color32::from_rgb(0xD9, 0x6A, 0x1F),
+ corner_radius: 16,
+ },
+ }
+ }
+}
+
+impl StyleProvider for MyTheme {
+ fn style(&mut self, args: &StyleArgs<'_>) -> ButtonStyle {
+ let StyleArgs {
+ classes,
+ state,
+ ctx,
+ ..
+ } = args;
+
+ // Start from the style egui computed for a generic widget, so we inherit e.g. the font:
+ let base: BaseStyle = ctx.get_widget_style(args);
+
+ let fill = if classes.has(DANGER) {
+ self.danger
+ } else {
+ self.normal
+ };
+
+ // React to what the user is doing with the button:
+ let fill = match state {
+ WidgetState::Hovered => fill.gamma_multiply(1.4),
+ WidgetState::Active => fill.gamma_multiply(0.7),
+ _ => fill,
+ };
+
+ ButtonStyle {
+ frame: Frame::new()
+ .fill(fill)
+ .corner_radius(self.corner_radius)
+ .inner_margin(8),
+ text_style: base.text,
+ }
+ }
+}
+
+#[derive(Clone, Copy, PartialEq)]
+enum Preset {
+ Ocean,
+ Candy,
+}
+
+fn main() -> eframe::Result {
+ env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
+
+ let options = eframe::NativeOptions {
+ viewport: egui::ViewportBuilder::default().with_inner_size([600.0, 340.0]),
+ ..Default::default()
+ };
+
+ let mut preset = Preset::Ocean;
+ let mut theme = MyTheme::preset(preset);
+ let mut last_click = "nothing";
+
+ eframe::run_ui_native("Styling engine", options, move |ui, _frame| {
+ // Register our theme for all buttons. This is a no-op after the first frame.
+ ui.add_widget_theme::(theme);
+
+ Panel::left("controls").default_size(260.0).show(ui, |ui| {
+ // The custom theme inherits the text color from egui's light/dark theme:
+ egui::global_theme_preference_buttons(ui);
+
+ ui.separator();
+
+ ui.heading("Button theme");
+
+ let mut changed = false;
+
+ egui::ComboBox::from_label("Preset")
+ .selected_text(match preset {
+ Preset::Ocean => "Ocean",
+ Preset::Candy => "Candy",
+ })
+ .show_ui(ui, |ui| {
+ changed |= ui
+ .selectable_value(&mut preset, Preset::Ocean, "Ocean")
+ .changed();
+ changed |= ui
+ .selectable_value(&mut preset, Preset::Candy, "Candy")
+ .changed();
+ });
+ if changed {
+ theme = MyTheme::preset(preset);
+ }
+
+ ui.horizontal(|ui| {
+ changed |= ui.color_edit_button_srgba(&mut theme.normal).changed();
+ ui.label("Normal");
+ });
+ ui.horizontal(|ui| {
+ changed |= ui.color_edit_button_srgba(&mut theme.danger).changed();
+ ui.label("Danger");
+ });
+ changed |= ui
+ .add(egui::Slider::new(&mut theme.corner_radius, 0..=24).text("Corner radius"))
+ .changed();
+
+ if changed {
+ // Overwrite the registered theme with the edited one:
+ ui.replace_widget_theme::(theme);
+ }
+ });
+
+ CentralPanel::default().show(ui, |ui| {
+ ui.heading("Buttons");
+ ui.label("All buttons below are styled by the custom theme.");
+ ui.add_space(8.0);
+
+ if ui.button("Save").clicked() {
+ last_click = "Save";
+ }
+ ui.add_space(4.0);
+ if ui
+ .add(egui::Button::new("Delete everything").with_class(DANGER))
+ .clicked()
+ {
+ last_click = "Delete everything";
+ }
+
+ ui.add_space(8.0);
+ ui.label(format!("Last clicked: {last_click}"));
+ });
+ })
+}
diff --git a/tests/egui_tests/Cargo.toml b/tests/egui_tests/Cargo.toml
index 89d6a9fda..7e894a2c0 100644
--- a/tests/egui_tests/Cargo.toml
+++ b/tests/egui_tests/Cargo.toml
@@ -10,7 +10,7 @@ version.workspace = true
ignored = ["image"] # We need the png feature
[dev-dependencies]
-egui = { workspace = true, default-features = true }
+egui = { workspace = true, default-features = true, features = ["experimental"] }
egui_kittest = { workspace = true, features = ["snapshot", "wgpu"] }
egui_extras = { workspace = true, features = ["image"] }
image = { workspace = true, features = ["png"] }
diff --git a/tests/egui_tests/tests/test_atoms.rs b/tests/egui_tests/tests/test_atoms.rs
index 6368de3a8..b6cd6cda8 100644
--- a/tests/egui_tests/tests/test_atoms.rs
+++ b/tests/egui_tests/tests/test_atoms.rs
@@ -1,5 +1,7 @@
use egui::{
- Align, Atom, AtomExt as _, AtomLayout, Button, Direction, Frame, Layout, TextWrapMode, Ui, Vec2,
+ Align, Atom, AtomExt as _, AtomLayout, Button, Direction, Frame, Layout, TextWrapMode, Ui,
+ Vec2,
+ widget_style::{ButtonStyle, StyleArgs},
};
use egui_kittest::{HarnessBuilder, SnapshotResult, SnapshotResults};
@@ -129,11 +131,14 @@ fn test_atom_layout_nesting_and_direction() {
let style = ui.style();
let canvas_frame = Frame::canvas(style);
- let button_frame = style
- .button_style(
- &egui::widget_style::Classes::default(),
- egui::widget_style::WidgetState::Inactive,
- )
+ let button_frame = ui
+ .get_widget_style::(&StyleArgs {
+ classes: &egui::widget_style::Classes::default(),
+ state: egui::widget_style::WidgetState::Inactive,
+ ctx: ui,
+ stack: ui.stack(),
+ style,
+ })
.frame;
let row = |direction: Direction| {
From a15a7e6611830aa5d18ccffed34ca177fdc0cd17 Mon Sep 17 00:00:00 2001
From: Emil Ernerfeldt
Date: Mon, 24 Aug 2026 02:07:06 -0700
Subject: [PATCH 07/16] Increase widget corner radius to 4 (#8445)
A little more round and friendly!
* See also https://github.com/emilk/egui/pull/8439
---
crates/egui/src/style.rs | 20 +++++++++----------
.../egui_demo_app/tests/snapshots/clock.png | 4 ++--
.../tests/snapshots/custom3d.png | 4 ++--
.../tests/snapshots/easymarkeditor.png | 4 ++--
.../tests/snapshots/imageviewer.png | 4 ++--
.../tests/snapshots/demos/Bézier Curve.png | 4 ++--
.../tests/snapshots/demos/Code Editor.png | 4 ++--
.../tests/snapshots/demos/Dancing Strings.png | 4 ++--
.../tests/snapshots/demos/Font Book.png | 4 ++--
.../tests/snapshots/demos/Frame.png | 4 ++--
.../tests/snapshots/demos/Grid Test.png | 4 ++--
.../tests/snapshots/demos/Input Test.png | 4 ++--
.../snapshots/demos/Manual Layout Test.png | 4 ++--
.../tests/snapshots/demos/Multi Touch.png | 4 ++--
.../tests/snapshots/demos/Painting.png | 4 ++--
.../tests/snapshots/demos/Popups.png | 4 ++--
.../tests/snapshots/demos/Scene.png | 4 ++--
.../tests/snapshots/demos/Screenshot.png | 4 ++--
.../tests/snapshots/demos/Scrolling.png | 4 ++--
.../tests/snapshots/demos/Table.png | 4 ++--
.../snapshots/demos/Tessellation Test.png | 4 ++--
.../tests/snapshots/demos/Window Options.png | 4 ++--
.../snapshots/demos/Window Resize Test.png | 4 ++--
.../tests/snapshots/modals_2.png | 4 ++--
.../tests/snapshots/modals_3.png | 4 ++--
...rop_should_prevent_focusing_lower_area.png | 4 ++--
.../snapshots/rendering_test/dpi_1.00.png | 4 ++--
.../snapshots/rendering_test/dpi_1.25.png | 4 ++--
.../snapshots/rendering_test/dpi_1.50.png | 4 ++--
.../snapshots/rendering_test/dpi_1.75.png | 4 ++--
.../snapshots/rendering_test/dpi_2.00.png | 4 ++--
.../tessellation_test/Additive rectangle.png | 4 ++--
.../tessellation_test/Blurred stroke.png | 4 ++--
.../snapshots/tessellation_test/Blurred.png | 4 ++--
.../tessellation_test/Minimal rounding.png | 4 ++--
.../snapshots/tessellation_test/Normal.png | 4 ++--
.../Thick stroke, minimal rounding.png | 4 ++--
.../tessellation_test/Thin filled.png | 4 ++--
.../tessellation_test/Thin stroked.png | 4 ++--
.../snapshots/widget_gallery_dark_x1.png | 4 ++--
.../snapshots/widget_gallery_dark_x2.png | 4 ++--
.../snapshots/widget_gallery_light_x1.png | 4 ++--
.../snapshots/widget_gallery_light_x2.png | 4 ++--
.../tests/snapshots/combobox_opened.png | 4 ++--
.../tests/snapshots/readme_example.png | 4 ++--
.../tests/snapshots/atom_layout_nesting.png | 4 ++--
.../tests/snapshots/layout/button.png | 4 ++--
.../tests/snapshots/layout/button_image.png | 4 ++--
.../layout/button_image_shortcut.png | 4 ++--
.../snapshots/layout/checkbox_checked.png | 4 ++--
.../tests/snapshots/layout/drag_value.png | 4 ++--
.../layout/selectable_value_selected.png | 4 ++--
.../tests/snapshots/layout/slider.png | 4 ++--
.../snapshots/layout/text_edit_no_clip.png | 4 ++--
.../separator_off_hovered.png | 4 ++--
.../separator_off_idle.png | 4 ++--
.../separator_on_hovered.png | 4 ++--
.../separator_on_idle.png | 4 ++--
.../test_tooltip_hover_regression.png | 4 ++--
.../text_edit_delay_1_h_invisible.png | 4 ++--
.../tests/snapshots/text_edit_halign.png | 4 ++--
.../tests/snapshots/text_edit_rtl_0.png | 4 ++--
.../tests/snapshots/text_edit_rtl_1.png | 4 ++--
.../tests/snapshots/text_edit_rtl_2.png | 4 ++--
.../tests/snapshots/text_edit_scroll_1_5.png | 4 ++--
.../tests/snapshots/visuals/button.png | 4 ++--
.../tests/snapshots/visuals/button_image.png | 4 ++--
.../visuals/button_image_shortcut.png | 4 ++--
.../button_image_shortcut_selected.png | 4 ++--
.../tests/snapshots/visuals/checkbox.png | 4 ++--
.../snapshots/visuals/checkbox_checked.png | 4 ++--
.../tests/snapshots/visuals/drag_value.png | 4 ++--
.../snapshots/visuals/selectable_value.png | 4 ++--
.../visuals/selectable_value_selected.png | 4 ++--
.../tests/snapshots/visuals/slider.png | 4 ++--
.../tests/snapshots/visuals/text_edit.png | 4 ++--
.../snapshots/visuals/text_edit_clip.png | 4 ++--
.../snapshots/visuals/text_edit_no_clip.png | 4 ++--
.../visuals/text_edit_placeholder_clip.png | 4 ++--
.../visuals/text_edit_prefix_suffix.png | 4 ++--
80 files changed, 168 insertions(+), 168 deletions(-)
diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs
index f6df21f09..ae467656c 100644
--- a/crates/egui/src/style.rs
+++ b/crates/egui/src/style.rs
@@ -1685,7 +1685,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
- corner_radius: CornerRadius::same(2),
+ corner_radius: CornerRadius::same(4),
expansion: 0.0,
},
inactive: WidgetVisuals {
@@ -1693,7 +1693,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
- corner_radius: CornerRadius::same(2),
+ corner_radius: CornerRadius::same(4),
expansion: 0.0,
},
hovered: WidgetVisuals {
@@ -1701,7 +1701,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)),
- corner_radius: CornerRadius::same(3),
+ corner_radius: CornerRadius::same(4),
expansion: 0.0,
},
active: WidgetVisuals {
@@ -1709,7 +1709,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),
- corner_radius: CornerRadius::same(2),
+ corner_radius: CornerRadius::same(4),
expansion: 0.0,
},
open: WidgetVisuals {
@@ -1717,7 +1717,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)),
- corner_radius: CornerRadius::same(2),
+ corner_radius: CornerRadius::same(4),
expansion: 0.0,
},
}
@@ -1730,7 +1730,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
- corner_radius: CornerRadius::same(2),
+ corner_radius: CornerRadius::same(4),
expansion: 0.0,
},
inactive: WidgetVisuals {
@@ -1738,7 +1738,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
- corner_radius: CornerRadius::same(2),
+ corner_radius: CornerRadius::same(4),
expansion: 0.0,
},
hovered: WidgetVisuals {
@@ -1746,7 +1746,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),
- corner_radius: CornerRadius::same(3),
+ corner_radius: CornerRadius::same(4),
expansion: 0.0,
},
active: WidgetVisuals {
@@ -1754,7 +1754,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),
- corner_radius: CornerRadius::same(2),
+ corner_radius: CornerRadius::same(4),
expansion: 0.0,
},
open: WidgetVisuals {
@@ -1762,7 +1762,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),
- corner_radius: CornerRadius::same(2),
+ corner_radius: CornerRadius::same(4),
expansion: 0.0,
},
}
diff --git a/crates/egui_demo_app/tests/snapshots/clock.png b/crates/egui_demo_app/tests/snapshots/clock.png
index 87c4b5374..812163491 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:dda3ad81551d0001bb9d3614a266010e44c8c18700964d8734c3e825672d9383
-size 334759
+oid sha256:f5290f645746e693fa7c9c7cb2985b0ec4a2522d52629e2cf583e683a3d67b58
+size 334814
diff --git a/crates/egui_demo_app/tests/snapshots/custom3d.png b/crates/egui_demo_app/tests/snapshots/custom3d.png
index fc393aa9e..3e8e97e79 100644
--- a/crates/egui_demo_app/tests/snapshots/custom3d.png
+++ b/crates/egui_demo_app/tests/snapshots/custom3d.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9035687ecd3aae0d8639000f091592d69acec0e73e7ba17e04c9794c5321cb1e
-size 92771
+oid sha256:5ac1089b1502496408e932025892e9f587ae62861e2d3cf2620bf5c5a09aa265
+size 92929
diff --git a/crates/egui_demo_app/tests/snapshots/easymarkeditor.png b/crates/egui_demo_app/tests/snapshots/easymarkeditor.png
index 7f908cbea..afcc360bc 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:418f9bc7eb13dd32ce2bba8e4b8003b095dd00aa8c30f1640ea0f94f87a085eb
-size 168430
+oid sha256:dde8e504560d658fbb3eaee0dc12bbbd5c930841bc5637bc60274773d8a50e2d
+size 168735
diff --git a/crates/egui_demo_app/tests/snapshots/imageviewer.png b/crates/egui_demo_app/tests/snapshots/imageviewer.png
index 7381f2737..e87b6a8a7 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:8d278d0d879e8cb44c4c2e65bdee4441616abcf3507adc37959ccf042991b31c
-size 98780
+oid sha256:8f93b6872fb1df5841afcc61a840514bd391f436274136e14725364e33f1e4b2
+size 101079
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 aa52b8bda..92726ea05 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:7c777cd6b36219b92c88ebf5987f9239a5750d25be4a87a58841bcf6db259599
-size 32422
+oid sha256:1bb3195aa5eb1fa7da6f6bb52eec745704e10eb82efb68d5f328f9bfa09fc2d4
+size 32538
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 12b0c24d3..30c75f122 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:2d941c979def6f30fd227c144d20c7f138f9c06c3338d563ac075c5e17e9b795
-size 26911
+oid sha256:b6351ddb148941f73a182e676a3be4ac5d7165c618324459d0061dbf93f213d9
+size 27023
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 cd27c9038..18c633650 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:ad431823e85de056c95e6e97d1a9f5905880a1ecc34f24da5bc0b611e42fe99b
-size 27260
+oid sha256:3a4ac42a9b5e44074caa2ec450bd54d834c14182ad8929a5f0567b53bbe5359a
+size 27426
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 bd3cd328c..09067c974 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:73429ec71d1ae05518352f1a99304f55647dac362092c9cd5618f709b9ba0d11
-size 99950
+oid sha256:611b18575d2768d40b89f28427bd987d90393e886da2a2c4e09ea6503f85acdd
+size 100303
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Frame.png b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png
index bacae8846..7bd61f73e 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:79ee28a9c0d8ef80d53560584312a33569f668e0a49f6ad5d277ffef507f2818
-size 25116
+oid sha256:3b6f03016682b8a9970d2f425cfd417833b8e532c69bbcfbf8290ccaa8335f3b
+size 25938
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Grid Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Grid Test.png
index d7a4c8e1e..c1ccf772a 100644
--- a/crates/egui_demo_lib/tests/snapshots/demos/Grid Test.png
+++ b/crates/egui_demo_lib/tests/snapshots/demos/Grid Test.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ff02fa99b92059d7b4f8286b8a2fd2e27a7f8f005328da97f0b9786aac9bbb17
-size 99316
+oid sha256:f7d190cbc76e61133a6a1d1c800c24fa5de7dba09e3ba89dff9dc5b71ab8e788
+size 100543
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Input Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Input Test.png
index c1627ebba..524e55d17 100644
--- a/crates/egui_demo_lib/tests/snapshots/demos/Input Test.png
+++ b/crates/egui_demo_lib/tests/snapshots/demos/Input Test.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3e2f29678c41e45bedc916dbe845f05c343bfad5528a4143c32cac6c3dee41e7
-size 50512
+oid sha256:a2b67e1e43d55c26ec2dfcf420ef7c5f009e3e5c501780ac59b3f1848728b833
+size 50940
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Manual Layout Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Manual Layout Test.png
index 89e4a647b..6955d9f92 100644
--- a/crates/egui_demo_lib/tests/snapshots/demos/Manual Layout Test.png
+++ b/crates/egui_demo_lib/tests/snapshots/demos/Manual Layout Test.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:345ab9ac3586c3cb9d3b02d46c590a81c7bc31778a3b631874520e57b6694076
-size 22994
+oid sha256:7eacac965912ded40f68e8b3c6d91218e6688c12b383e64fabf0d92ea9c73d9e
+size 24039
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 b3c10c6a2..2172cd528 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:353d92d99e9c2350267a43bff7bb9100d3109c82f8d54f9f7a9e3a312ea0c4e6
-size 35378
+oid sha256:eba72ac8696d81552f5106b02437f0b524eb77e61ff1d8014cebce6483eaa784
+size 35474
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Painting.png b/crates/egui_demo_lib/tests/snapshots/demos/Painting.png
index 9a5859bf5..a4b4ccdf6 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:218cce13bd96c97aca6a529ba49baaeda5a126cf657e6af1788646cb845725d0
-size 17686
+oid sha256:e9d4349ba73e1d1191191ebd1918ed80e373036e13dee837400194f53b0b9abd
+size 17899
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Popups.png b/crates/egui_demo_lib/tests/snapshots/demos/Popups.png
index c452f78da..4b1488773 100644
--- a/crates/egui_demo_lib/tests/snapshots/demos/Popups.png
+++ b/crates/egui_demo_lib/tests/snapshots/demos/Popups.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3edbe6debf700364949ef481ba1e8b2319db624ddc316570b6180636dcd88935
-size 57262
+oid sha256:294a43fb1f3117450cde67cb38c4684b90a7b0f69d9c6e7002c0f6d06c70d02c
+size 58044
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scene.png b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png
index 16b02f86d..721c8a64c 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:141271330d4cef517c4ec2c4c2c41306f8b5de386738381a0e4446fa89df7cdf
-size 30450
+oid sha256:65d1c8766fa8f854806a408a0289a79e942fdf6642be7cf6f4e8924412d31c25
+size 30855
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png b/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png
index 603404a9b..1bf13363f 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:be289c58296a7656c8550d25ff92b8cd0b03c5c9c527b9ca4a38acda8bbe3ac1
-size 23807
+oid sha256:80091cab95735142ca35100cc74f784aa92812eeaf2e6ef991389de3651b4cfa
+size 24045
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png b/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png
index 004257863..e55a44cff 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:fc28830c402d3aa861a220cfc600c3ce2d7b8ca77307691b3f844a34f27afd1f
-size 151376
+oid sha256:e7777f85030bad9c28237cdcb77d0835eafd3a1c69603545860cd980c1a2cbbd
+size 151843
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Table.png b/crates/egui_demo_lib/tests/snapshots/demos/Table.png
index 1316fe84e..acdce7292 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:ca08bb7cd0be0b5a9d10833e5c9b65e889f78a5a8056df69d6b0006c3a228227
-size 66050
+oid sha256:17c16934b22f0439d4a13a78d91a85aec7ff740aeff9f8feb9dda3b059c4d83b
+size 67107
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Tessellation Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Tessellation Test.png
index ad87167a3..e97fe67a2 100644
--- a/crates/egui_demo_lib/tests/snapshots/demos/Tessellation Test.png
+++ b/crates/egui_demo_lib/tests/snapshots/demos/Tessellation Test.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:97452108d7809775756f1a229644076f5b4c75a6a6d8b8edc082c2448ae35e94
-size 60701
+oid sha256:ccac475bb38e76dc044d34f0a48e73b61e9d4734d02105c5635e02bcdaa19c63
+size 62104
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 83750aba9..c5c4ead81 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:d27392f625445e82285c8eaeb532e11dcb02030f24c7a769356cb4c23d7c2067
-size 41516
+oid sha256:7bbfacbbc740729ce8729f4507c6cc155a0dcda51dd4442d3db332cf2f6fd9f1
+size 42790
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png
index 3a448e1dd..25227f44a 100644
--- a/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png
+++ b/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:90c4ea93f234aded298ddeeb98e2d6a0566f07efcdaf5c990e8a47a6533db516
-size 446483
+oid sha256:9ac3830efd9c5eb051ed6bf22bd90dd556883b29505096c3901f28f8673b6eba
+size 446518
diff --git a/crates/egui_demo_lib/tests/snapshots/modals_2.png b/crates/egui_demo_lib/tests/snapshots/modals_2.png
index 0995b1814..70d6b8139 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:4b069ad508fd3a518df3a39e1740d1786c4af4c3c4a479db04c00f701980efa8
-size 48277
+oid sha256:2718c333123388c8751f461382867b0640ce7a02c220db311c06ff634e6178c1
+size 48620
diff --git a/crates/egui_demo_lib/tests/snapshots/modals_3.png b/crates/egui_demo_lib/tests/snapshots/modals_3.png
index 182b7336d..cd460f8a4 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:e92e72c651e0ad2c0fc38008d24d9ddf28a70e3b35b26898af78e74028e2c251
-size 44357
+oid sha256:a41a61cce2cda5fb9372324f915ffe2b47f68ead5e26b471ea29dfb5208695a7
+size 44600
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 7d1519d01..44c5f4d1e 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:1a5f7bc46c7baf62b559458b3596e386d66bf2281b7c35e5fdb6c5c4a64569f2
-size 44347
+oid sha256:bff8f0531d5cf92e0afe857a04705599d30e77d526091b03296e381b45f02170
+size 44488
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 ea32cd32f..8cae2d56f 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:ce6e44aeb60b6cc2179e14c467edc2d570ea1adbce08511650fc733e39f7757c
-size 590679
+oid sha256:6402e1da2627f143d07e01b1bcbcbfafa174fcf76fd4c1f8b217b5227528a067
+size 590792
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 1bff62c8d..6ba4fe11b 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:2edd6679d434df3407048472aaa95e410158f28e9a13c9bd88e3f8be95b08d9d
-size 745771
+oid sha256:eddc77778c52b56763e1906fe9fdc4e575d4c99f3a438dc9f74260f75f13154f
+size 745930
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 39c2a6b36..029708c0d 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:20cb796b59113853345c98e5238021a9c5c977c25689ee9201fdec1e4d98123d
-size 973781
+oid sha256:bfc46024827dd504be4765a3eab02bb1c27cb870d128285580fa1eb0cff8390c
+size 973977
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 736c3dd67..2e7cf7674 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:b5d5941c8fa0456f8a92fce4888e7a2888945f5df7cfed867ef073c439c7ad9f
-size 1131030
+oid sha256:e9918502356af39490dc8056509117dee7b7a290c1cac06767d3478c38bb27d3
+size 1131255
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 b4bf73cb7..48f651b79 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:78ef6dba3dcf5e26a9289ce226165224c712c22f9b5dfe699b599c7d2531133b
-size 1363589
+oid sha256:95cd6e583873e36399df82133b8eca8b3be34ea8d5c6d36cb653288c8cf3b53f
+size 1363872
diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png
index f2494709e..507e73c88 100644
--- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png
+++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3daf9b7cbfb48f6d083126e58c605cb19f462509acc03843f0bca5c645945e23
-size 45044
+oid sha256:a7028aa93b6212054dc41d3daf88454d53e6b05d670b987f2d918f6aa0b38324
+size 46455
diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png
index 7254be665..b442e6d3b 100644
--- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png
+++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:de7db1296bdd127dfd827c6d1cc1a5a840391c3abb558be07faeb7e6612ed6e8
-size 86830
+oid sha256:d9139bb87beac6bdb91f0e48f5cabba61ca7ba5344a8fcefd094d2efb98e7056
+size 88246
diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png
index e88d67b6b..256e413b3 100644
--- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png
+++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:4ef3a17791ca4a3e0209eec5d191c00c66e08fc4dd07fb4845490288c5bfbdb6
-size 118889
+oid sha256:603afb5653a960161a6884647bc51e8547160ffab186d7a9a2b4839ed77e7d0b
+size 120323
diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png
index a3c23b831..c81ef8fa0 100644
--- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png
+++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:465a3aa235ea69bce6650cf6e100ffc719bba67baa0cb13d7015a0fc54d53d9a
-size 51376
+oid sha256:a596437f8274a87525ce107f94d6c8394166c3aa26d06737f1abdc2ae894f77b
+size 52790
diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png
index ec1182d15..9b305c524 100644
--- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png
+++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:aac429243686d35096760eb1b7c6a2bcad1c1d00a3341a858a6c71aaff2cf128
-size 54582
+oid sha256:001c89ba338b51d48f1e471e17306e84e74b0f952a856b65532ffeacbf52f2cb
+size 55982
diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png
index fa5ea442d..48d0dcd74 100644
--- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png
+++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8afb610d9e86b324db22849457eb23419c4f3c240e5ee951a13ce8901003bca8
-size 55053
+oid sha256:bf299c3c52250b1a17411e3e1aef57a4293787c9a9777da6fda01bb6a0e387c1
+size 56466
diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png
index 44c159f4f..68520ec06 100644
--- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png
+++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:75ff881add9f2968d19d8bb1d39239de5d99105027e91af209dfa5b45e644d2c
-size 35943
+oid sha256:26c857faec0666f207182166d34bc9980f1cf3165813a9128cd24d1429156933
+size 37357
diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png
index b725ec08f..ecb524b23 100644
--- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png
+++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1887d632d0994efec3dd7cecc41e3ad460c109baa1012d69e005cdc916e3cd24
-size 35925
+oid sha256:db4795c68fffb20f74d46664be9b7c4bfcaf941a8b3e27a7315f9525dd90d5c4
+size 37340
diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png
index 5ea006f1f..3ae797d56 100644
--- a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png
+++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8c0054dd1717833ae6f19726b58bffbf70bd4dc784b5f2774b194800a3adffed
-size 64973
+oid sha256:46af578e5e90d0591644707b9ad305615ee098182ffa14c6ccf29bdbbc2a0bef
+size 65919
diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png
index c37f49163..234926bbb 100644
--- a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png
+++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9be7321d8184c3d06cfeb54410c1f839892c4f836767e9b1950e70306cde6c54
-size 151090
+oid sha256:71433ceac92b327d2c5109302c479d398b64283a75327bba6d584654a0c53f3e
+size 153251
diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png
index 54f2d37fd..d767c6179 100644
--- a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png
+++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:f07c25c053e9c4d5f7416cc00e101e13b7007b65c46ba13d0853d95ca43fddc4
-size 59950
+oid sha256:196d957732cc5a0d0b8b30875b5ade59236c288293e2cc8a53f9ed6c89edad79
+size 60722
diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png
index 318fb89e4..e4544e5d2 100644
--- a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png
+++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b6a9bc30b4d6ed100555ee553554848d20d93f25643e72eeef1826483ed307d9
-size 145940
+oid sha256:8d26c20506fffcbca07c3eb0a986357129590684da83f8cdd1a56c65cf3e6452
+size 147925
diff --git a/crates/egui_kittest/tests/snapshots/combobox_opened.png b/crates/egui_kittest/tests/snapshots/combobox_opened.png
index 20dd3e232..1269fb4f6 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:84e857521c20ea2458d9f9d724578e0495c06e75571c25104bf64a0be0be617d
-size 8028
+oid sha256:c32712afee568ce9dca8eeb6bd1dc0f072f9e9b9189ba0bbcd32f11317d73b98
+size 8160
diff --git a/crates/egui_kittest/tests/snapshots/readme_example.png b/crates/egui_kittest/tests/snapshots/readme_example.png
index 55a13d3e4..89a3733b4 100644
--- a/crates/egui_kittest/tests/snapshots/readme_example.png
+++ b/crates/egui_kittest/tests/snapshots/readme_example.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:57211d5ac8021223ed288b9fd6f651ed901ec91599ed67d54ac540f3a2037937
-size 2205
+oid sha256:1933058e8eb0568b6a42be5bcb4aae28a478daa9b62ccb0e72d33623f9161cb1
+size 2246
diff --git a/tests/egui_tests/tests/snapshots/atom_layout_nesting.png b/tests/egui_tests/tests/snapshots/atom_layout_nesting.png
index bbff7f706..8042de636 100644
--- a/tests/egui_tests/tests/snapshots/atom_layout_nesting.png
+++ b/tests/egui_tests/tests/snapshots/atom_layout_nesting.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e43b410ab0a06b40da5ad9c7fd105d11201d44a3afd63fd213429cc39013c0d4
-size 6964
+oid sha256:da19989c02e0dc6c558575c7ab0f5e51b2747b2d65d69f6883636d039ed54d2a
+size 7335
diff --git a/tests/egui_tests/tests/snapshots/layout/button.png b/tests/egui_tests/tests/snapshots/layout/button.png
index 464daa5de..de7da072b 100644
--- a/tests/egui_tests/tests/snapshots/layout/button.png
+++ b/tests/egui_tests/tests/snapshots/layout/button.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1bafb447c177f2bd94980c1d146a2826ecf62070fb6ee18e63e6a925e2741210
-size 314478
+oid sha256:c9df24e02feeac296cf03f42b7a5343f943ee1dd00508c5b6da2563928ec228b
+size 324343
diff --git a/tests/egui_tests/tests/snapshots/layout/button_image.png b/tests/egui_tests/tests/snapshots/layout/button_image.png
index ce2844f8f..1a4d273b0 100644
--- a/tests/egui_tests/tests/snapshots/layout/button_image.png
+++ b/tests/egui_tests/tests/snapshots/layout/button_image.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:eadf74cc7f7e10be804df0e7df65c232ecaed7d77c2c89197548ab7206706843
-size 351669
+oid sha256:106d9e06bbcae5db4e0a5813df48899f16c3ac131c9a44e45b45253bede781d1
+size 361653
diff --git a/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png b/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png
index 3a8d09f95..807420053 100644
--- a/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png
+++ b/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:60739a18972a5e866610ae2c8f5af719a1bbaeffcdd8c6319e783c37e28d3107
-size 435108
+oid sha256:83f6000e0ff5705df6012036da6337d589baeb6652d1b554213ab5948c505040
+size 445361
diff --git a/tests/egui_tests/tests/snapshots/layout/checkbox_checked.png b/tests/egui_tests/tests/snapshots/layout/checkbox_checked.png
index 3a2676aba..175e97d95 100644
--- a/tests/egui_tests/tests/snapshots/layout/checkbox_checked.png
+++ b/tests/egui_tests/tests/snapshots/layout/checkbox_checked.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2fb6db99b4dfa1b0c6f97799ffd72c264c02eefd803eee5737075231760c80bd
-size 417925
+oid sha256:4bcbf331a0d18b1233689132d149e4766e8db2827bafce50696d73195d83fc02
+size 426870
diff --git a/tests/egui_tests/tests/snapshots/layout/drag_value.png b/tests/egui_tests/tests/snapshots/layout/drag_value.png
index 105cd3453..9044959b1 100644
--- a/tests/egui_tests/tests/snapshots/layout/drag_value.png
+++ b/tests/egui_tests/tests/snapshots/layout/drag_value.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:f54f444c25fbd1def8d0f11d03e1fd8bde6745a2593d0029e346cbaafa584ed3
-size 264007
+oid sha256:4372664983d9ae7e9889610349f86f025e0c9fccccaaa20dd4735663baede051
+size 274014
diff --git a/tests/egui_tests/tests/snapshots/layout/selectable_value_selected.png b/tests/egui_tests/tests/snapshots/layout/selectable_value_selected.png
index a52c4de9a..96369bc4c 100644
--- a/tests/egui_tests/tests/snapshots/layout/selectable_value_selected.png
+++ b/tests/egui_tests/tests/snapshots/layout/selectable_value_selected.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:97c2f56dad8d6aa45385901f9b23a13c49c21fc5aa811ab50dcf7c8451b177dd
-size 401596
+oid sha256:45f1b8f5b31f925b7ceb0552c67bcf2df6e43dfba1980381d3ae677a4b713b5c
+size 412451
diff --git a/tests/egui_tests/tests/snapshots/layout/slider.png b/tests/egui_tests/tests/snapshots/layout/slider.png
index 306d2a97f..bee3b9c16 100644
--- a/tests/egui_tests/tests/snapshots/layout/slider.png
+++ b/tests/egui_tests/tests/snapshots/layout/slider.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:619139eb166d383cc9b35653c8c4a97c55cd21b171a24e253d923558cc0d35e1
-size 275512
+oid sha256:d9395e6534bd5d1ddcdd2be8c446bf85b7c3b65dc7a0df7c018db68d2a563a6f
+size 312206
diff --git a/tests/egui_tests/tests/snapshots/layout/text_edit_no_clip.png b/tests/egui_tests/tests/snapshots/layout/text_edit_no_clip.png
index 8adc7676b..9a0048ea3 100644
--- a/tests/egui_tests/tests/snapshots/layout/text_edit_no_clip.png
+++ b/tests/egui_tests/tests/snapshots/layout/text_edit_no_clip.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:01795b279efc5c680d77817f7d0e6e9f18dc3969ec964d8d3419d0175b0f4d87
-size 512262
+oid sha256:82a7349d6e7ac45656b38c5e96a49f7d1c84b14ea14b1f548420040d618c1c3e
+size 515130
diff --git a/tests/egui_tests/tests/snapshots/panel_separator_line/separator_off_hovered.png b/tests/egui_tests/tests/snapshots/panel_separator_line/separator_off_hovered.png
index bb3745c59..5957cf916 100644
--- a/tests/egui_tests/tests/snapshots/panel_separator_line/separator_off_hovered.png
+++ b/tests/egui_tests/tests/snapshots/panel_separator_line/separator_off_hovered.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:bbe8eda2e6162220ff8baa6e84156ea18f0bb15ffc46ebf9d60b6d3eeb8861d4
-size 9016
+oid sha256:38d8a73f05854a07e233e0b5b95baf591e82d7941133443cbba14fd6e78aeb8b
+size 9170
diff --git a/tests/egui_tests/tests/snapshots/panel_separator_line/separator_off_idle.png b/tests/egui_tests/tests/snapshots/panel_separator_line/separator_off_idle.png
index 8a690c1a5..322b14bf2 100644
--- a/tests/egui_tests/tests/snapshots/panel_separator_line/separator_off_idle.png
+++ b/tests/egui_tests/tests/snapshots/panel_separator_line/separator_off_idle.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:547316973b563bd92841f428838848323d656688cd03e8babcbc24626a389419
-size 7195
+oid sha256:776b759db9ca940c3a9c716c35e64a8e6d12ad8bdd2eadaf8527346cd2edc354
+size 7349
diff --git a/tests/egui_tests/tests/snapshots/panel_separator_line/separator_on_hovered.png b/tests/egui_tests/tests/snapshots/panel_separator_line/separator_on_hovered.png
index 43e51aa02..2b014293a 100644
--- a/tests/egui_tests/tests/snapshots/panel_separator_line/separator_on_hovered.png
+++ b/tests/egui_tests/tests/snapshots/panel_separator_line/separator_on_hovered.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b18774a40c1c1065645be7b59c90e65bb6fb40d4931ab37ccf40126e2767b709
-size 9019
+oid sha256:cb1950b37981750c1a391d6faa54954e6255608ec9ec30db6c76ec9561a20614
+size 9173
diff --git a/tests/egui_tests/tests/snapshots/panel_separator_line/separator_on_idle.png b/tests/egui_tests/tests/snapshots/panel_separator_line/separator_on_idle.png
index 3c92145ab..2f88ac5b2 100644
--- a/tests/egui_tests/tests/snapshots/panel_separator_line/separator_on_idle.png
+++ b/tests/egui_tests/tests/snapshots/panel_separator_line/separator_on_idle.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0e9ffcb9f14f86155a4a8279d694ae3e43c6bb8df59fa681673199c01ff68f96
-size 7206
+oid sha256:119252d84d2315e069a11a7ce40b4c699f86f44fdeec5e5fab2c4e8bfe1d8b20
+size 7359
diff --git a/tests/egui_tests/tests/snapshots/test_tooltip_hover_regression.png b/tests/egui_tests/tests/snapshots/test_tooltip_hover_regression.png
index 9f489aa81..e8d9d654b 100644
--- a/tests/egui_tests/tests/snapshots/test_tooltip_hover_regression.png
+++ b/tests/egui_tests/tests/snapshots/test_tooltip_hover_regression.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:59bef0e3593896985988c03171c710d8ed31ed8bb89c7a3f63c060c573e4fb74
-size 6510
+oid sha256:ad01abdc39b9432aea6f46fca7517e78ad45d95165a2cfb4159a198e3de40667
+size 6566
diff --git a/tests/egui_tests/tests/snapshots/text_edit_delay_1_h_invisible.png b/tests/egui_tests/tests/snapshots/text_edit_delay_1_h_invisible.png
index 66ad3f4a4..89de59f1d 100644
--- a/tests/egui_tests/tests/snapshots/text_edit_delay_1_h_invisible.png
+++ b/tests/egui_tests/tests/snapshots/text_edit_delay_1_h_invisible.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e74e32fb12decf36c8685274226c29c4e748b2bf276bf6b47ba19ac9a3def66a
-size 2290
+oid sha256:806f3f69228e62d9d0838fb27a02f46cae29efffee54d8182ecbfd2c12f405ba
+size 2413
diff --git a/tests/egui_tests/tests/snapshots/text_edit_halign.png b/tests/egui_tests/tests/snapshots/text_edit_halign.png
index 2f81509e6..31c19aede 100644
--- a/tests/egui_tests/tests/snapshots/text_edit_halign.png
+++ b/tests/egui_tests/tests/snapshots/text_edit_halign.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:48114ad6d116fb9288ce9fe3b173017bda317f69753e7ac03a090b8d02d6cb4d
-size 13258
+oid sha256:b66afca19d0a0a6fac4737816443fbd481635897807689dbd75321360ffa079d
+size 13803
diff --git a/tests/egui_tests/tests/snapshots/text_edit_rtl_0.png b/tests/egui_tests/tests/snapshots/text_edit_rtl_0.png
index 796a1e3b5..b4338fa85 100644
--- a/tests/egui_tests/tests/snapshots/text_edit_rtl_0.png
+++ b/tests/egui_tests/tests/snapshots/text_edit_rtl_0.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ed3665dfb232b8f0b1483802bfafb4605e8361d7eb977de5a58862e52ab724fa
-size 2296
+oid sha256:20afbde3e1edc7a599936f83188fb1a2c35d2d1344ce7ad8e115b937266481fc
+size 2547
diff --git a/tests/egui_tests/tests/snapshots/text_edit_rtl_1.png b/tests/egui_tests/tests/snapshots/text_edit_rtl_1.png
index cd6d5a621..d270bf1ca 100644
--- a/tests/egui_tests/tests/snapshots/text_edit_rtl_1.png
+++ b/tests/egui_tests/tests/snapshots/text_edit_rtl_1.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5cfdd6255aba92f0253571bde4f22afc8e0fb5fe3cb882946459d623c0a5d89c
-size 2982
+oid sha256:60f3988b6b0758fa7c2e97ab2bba282b45eee7b6092e4dafc68f692865aeafa7
+size 3211
diff --git a/tests/egui_tests/tests/snapshots/text_edit_rtl_2.png b/tests/egui_tests/tests/snapshots/text_edit_rtl_2.png
index cd6d5a621..d270bf1ca 100644
--- a/tests/egui_tests/tests/snapshots/text_edit_rtl_2.png
+++ b/tests/egui_tests/tests/snapshots/text_edit_rtl_2.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5cfdd6255aba92f0253571bde4f22afc8e0fb5fe3cb882946459d623c0a5d89c
-size 2982
+oid sha256:60f3988b6b0758fa7c2e97ab2bba282b45eee7b6092e4dafc68f692865aeafa7
+size 3211
diff --git a/tests/egui_tests/tests/snapshots/text_edit_scroll_1_5.png b/tests/egui_tests/tests/snapshots/text_edit_scroll_1_5.png
index b9e0dea1a..a2a4b086b 100644
--- a/tests/egui_tests/tests/snapshots/text_edit_scroll_1_5.png
+++ b/tests/egui_tests/tests/snapshots/text_edit_scroll_1_5.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7db4a0f33da131ca69771fef974689680c802187cdd8e835131d70457aca775c
-size 1639
+oid sha256:b464510b3423f92637c9ce502187c55017288ed7a6265e2a577e20cbd40ee448
+size 1703
diff --git a/tests/egui_tests/tests/snapshots/visuals/button.png b/tests/egui_tests/tests/snapshots/visuals/button.png
index 4978b5a3b..095667ea3 100644
--- a/tests/egui_tests/tests/snapshots/visuals/button.png
+++ b/tests/egui_tests/tests/snapshots/visuals/button.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2eb21d7b99171d249127377698225f0b2e103733864f6809785dfba169e133bf
-size 11213
+oid sha256:f8ed8c3114cc34e8415ed5ebed12d54d8790a0b1dfd38cbe8d8d52cef9cf817a
+size 11582
diff --git a/tests/egui_tests/tests/snapshots/visuals/button_image.png b/tests/egui_tests/tests/snapshots/visuals/button_image.png
index 9d3669597..4bfa690b0 100644
--- a/tests/egui_tests/tests/snapshots/visuals/button_image.png
+++ b/tests/egui_tests/tests/snapshots/visuals/button_image.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3322b616a292f685f6051d2bf9be0df28ca7472e8fc3c982cd094ca44dc47ff8
-size 12117
+oid sha256:e918acf970083110d9bf2e5fe5fa8570c1d17861117375b3c78ff32988c4e5df
+size 12600
diff --git a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png
index 41d88992d..b4b1a8586 100644
--- a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png
+++ b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2021446525a7b1090aaa6b3f12fdd9295c67cde00de61c7f0d36838b6dfc2593
-size 15148
+oid sha256:dac827ce7a3c829acb789d6b87006ec58929ce72efbf552b595266b220ca5960
+size 15524
diff --git a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png
index 3bb097aeb..f3ad0c0d1 100644
--- a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png
+++ b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e341f6d3fb3d9d09bd573584f68e4ada4e9af0dcb27cf1bc164797f62865c398
-size 14734
+oid sha256:9b7961331cf4842ea955e1e62b3ab74de0e589441d0830c45d0285080ea67d03
+size 15169
diff --git a/tests/egui_tests/tests/snapshots/visuals/checkbox.png b/tests/egui_tests/tests/snapshots/visuals/checkbox.png
index e45ea9fb3..b26d2e611 100644
--- a/tests/egui_tests/tests/snapshots/visuals/checkbox.png
+++ b/tests/egui_tests/tests/snapshots/visuals/checkbox.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9fd43ec4633a59798d2077d9fd3ad8292d5da66678a2d75d33c957a062d7848a
-size 13218
+oid sha256:0885a39324b015b92761d09fc1a6694d11d4b1fa9dfb23e05273479ed82a7b09
+size 13624
diff --git a/tests/egui_tests/tests/snapshots/visuals/checkbox_checked.png b/tests/egui_tests/tests/snapshots/visuals/checkbox_checked.png
index 31f84dbeb..c5fe38c6c 100644
--- a/tests/egui_tests/tests/snapshots/visuals/checkbox_checked.png
+++ b/tests/egui_tests/tests/snapshots/visuals/checkbox_checked.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3e2252dbe6b26cc6b484d09c57a47f4ecd0d75df88f2279a7dd95ca43b597e9d
-size 14282
+oid sha256:6572c4c44e12f49152cc2dfcae39140ffe69a0a1172d6aca540f10d5ba9b609e
+size 14633
diff --git a/tests/egui_tests/tests/snapshots/visuals/drag_value.png b/tests/egui_tests/tests/snapshots/visuals/drag_value.png
index b327692cb..90be73b8d 100644
--- a/tests/egui_tests/tests/snapshots/visuals/drag_value.png
+++ b/tests/egui_tests/tests/snapshots/visuals/drag_value.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:75e5610bd64f6beda7abb005344eff1f5ee23220272077d3b6ee5345baa616aa
-size 9095
+oid sha256:76be7b08da637ecbf898efd8322615d5e2f1cb1757665e5486db86ccb7fb3438
+size 9462
diff --git a/tests/egui_tests/tests/snapshots/visuals/selectable_value.png b/tests/egui_tests/tests/snapshots/visuals/selectable_value.png
index bb2b2a32b..90c0e8010 100644
--- a/tests/egui_tests/tests/snapshots/visuals/selectable_value.png
+++ b/tests/egui_tests/tests/snapshots/visuals/selectable_value.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b42c8d774d1ecb50f5bde1aa358a37cbb669b59d934786fb1cc4c6f0c3400701
-size 13711
+oid sha256:a05d5b941f2681f3466f0caa19dd6a71099fc897ba5eb7ac760e6ed6ba92d4be
+size 13971
diff --git a/tests/egui_tests/tests/snapshots/visuals/selectable_value_selected.png b/tests/egui_tests/tests/snapshots/visuals/selectable_value_selected.png
index 87163dd91..cc73e5f92 100644
--- a/tests/egui_tests/tests/snapshots/visuals/selectable_value_selected.png
+++ b/tests/egui_tests/tests/snapshots/visuals/selectable_value_selected.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7037f694fef1211f2ea9d8e7e60836d8eb97c9242c7b396efeb8390eaf0852f1
-size 13730
+oid sha256:568e1c2c0d28bcf5507d97562bedf12d32e99f78f75b3ac70f937115ea97a3ed
+size 14141
diff --git a/tests/egui_tests/tests/snapshots/visuals/slider.png b/tests/egui_tests/tests/snapshots/visuals/slider.png
index 8b758e108..2cf499527 100644
--- a/tests/egui_tests/tests/snapshots/visuals/slider.png
+++ b/tests/egui_tests/tests/snapshots/visuals/slider.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2d09706e0dbbbf6d07e39470285dc8bcd1b3c6d09bedd91cd20d86c96dba1131
-size 9911
+oid sha256:3e4b3d8ad1d40ca0d4c8f6a547cc470239b65ffd7de59fdb413fcf39934d9bd9
+size 11078
diff --git a/tests/egui_tests/tests/snapshots/visuals/text_edit.png b/tests/egui_tests/tests/snapshots/visuals/text_edit.png
index 2cd2c737a..c099f7ee3 100644
--- a/tests/egui_tests/tests/snapshots/visuals/text_edit.png
+++ b/tests/egui_tests/tests/snapshots/visuals/text_edit.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:bf723b58802e9cf5aefb0a06689ed2bfaa45afff9f0fc48d37e742244da097e6
-size 8149
+oid sha256:01afe50f7bc1177c7beaff73d10b63fbf6ff73ad621e76d9da05ecc3f5c362c7
+size 8614
diff --git a/tests/egui_tests/tests/snapshots/visuals/text_edit_clip.png b/tests/egui_tests/tests/snapshots/visuals/text_edit_clip.png
index 666364d68..be087fdb5 100644
--- a/tests/egui_tests/tests/snapshots/visuals/text_edit_clip.png
+++ b/tests/egui_tests/tests/snapshots/visuals/text_edit_clip.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e900a4004a1fc808bf74bef7b1ece7c98e16582c40155e222c04f759d229fc70
-size 10125
+oid sha256:5b4e19e088cacfc0524aad0e4cba32f701e57f3d9fd27bfc2b285949315c07a9
+size 10577
diff --git a/tests/egui_tests/tests/snapshots/visuals/text_edit_no_clip.png b/tests/egui_tests/tests/snapshots/visuals/text_edit_no_clip.png
index fff21ac13..b06736cc1 100644
--- a/tests/egui_tests/tests/snapshots/visuals/text_edit_no_clip.png
+++ b/tests/egui_tests/tests/snapshots/visuals/text_edit_no_clip.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2119fd3aa9aefe8198e7b79968f7fafc4aa93294ba4c2577dc55929302067996
-size 21827
+oid sha256:5be608caad5e27606dada8f99a2e95bced62b865d8274463b76224299c8abc7e
+size 22257
diff --git a/tests/egui_tests/tests/snapshots/visuals/text_edit_placeholder_clip.png b/tests/egui_tests/tests/snapshots/visuals/text_edit_placeholder_clip.png
index 0fb7a4708..b3cd53d18 100644
--- a/tests/egui_tests/tests/snapshots/visuals/text_edit_placeholder_clip.png
+++ b/tests/egui_tests/tests/snapshots/visuals/text_edit_placeholder_clip.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:02a333af5e58c53a89d436c4540579d3ea0c1e99c1cad4c019db1da34ff9de10
-size 8840
+oid sha256:0e6eb347e0622502e2fab2e06aee376d1b41338d504c136b9dfeae6de0465ddf
+size 9234
diff --git a/tests/egui_tests/tests/snapshots/visuals/text_edit_prefix_suffix.png b/tests/egui_tests/tests/snapshots/visuals/text_edit_prefix_suffix.png
index e00868b1f..f67caa61b 100644
--- a/tests/egui_tests/tests/snapshots/visuals/text_edit_prefix_suffix.png
+++ b/tests/egui_tests/tests/snapshots/visuals/text_edit_prefix_suffix.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a37ed30425967301ffa7dda3fdc8f316dfd7f4665c731b17778e3e5942783e81
-size 10534
+oid sha256:51949a5a32c81c03dcb93d7ea02780abce5acde7a627681562470ee4f3f932fb
+size 10993
From 726b995608ccacc61ebd90ab760f9d396b1db2f8 Mon Sep 17 00:00:00 2001
From: Emil Ernerfeldt
Date: Mon, 24 Aug 2026 02:28:14 -0700
Subject: [PATCH 08/16] Round the corners of the color picker (#8439)
The color picker's gradients and background checkers now follow the
style's corner radius, giving it a softer look.
This required changing the painting from being Mesh-based to being
texture-based
it's subtle with the default settings:
But you can [increase it](https://github.com/emilk/egui/pull/8445):
### Before
for reference
---------
Co-authored-by: Claude Fable 5
---
crates/egui-wgpu/src/egui.wgsl | 49 +++--
crates/egui-wgpu/src/renderer.rs | 66 ++++++-
crates/egui/src/context.rs | 59 ++++++
crates/egui/src/widgets/color_picker.rs | 179 +++++++++++-------
.../tests/snapshots/imageviewer.png | 4 +-
crates/egui_demo_lib/tests/color_picker.rs | 17 ++
.../tests/snapshots/color_picker.png | 3 +
.../tests/snapshots/demos/Frame.png | 4 +-
.../tests/snapshots/demos/Scene.png | 4 +-
.../snapshots/rendering_test/dpi_1.00.png | 4 +-
.../snapshots/rendering_test/dpi_1.25.png | 4 +-
.../snapshots/rendering_test/dpi_1.50.png | 4 +-
.../snapshots/rendering_test/dpi_1.67.png | 4 +-
.../snapshots/rendering_test/dpi_1.75.png | 4 +-
.../snapshots/rendering_test/dpi_2.00.png | 4 +-
.../tessellation_test/Additive rectangle.png | 4 +-
.../snapshots/tessellation_test/Blurred.png | 4 +-
.../tessellation_test/Thin filled.png | 4 +-
.../snapshots/widget_gallery_dark_x1.png | 4 +-
.../snapshots/widget_gallery_dark_x2.png | 4 +-
.../snapshots/widget_gallery_light_x1.png | 4 +-
.../snapshots/widget_gallery_light_x2.png | 4 +-
crates/epaint/src/corner_radius_f32.rs | 122 ++++++++----
crates/epaint/src/rounded_rect.rs | 7 +
24 files changed, 411 insertions(+), 155 deletions(-)
create mode 100644 crates/egui_demo_lib/tests/color_picker.rs
create mode 100644 crates/egui_demo_lib/tests/snapshots/color_picker.png
diff --git a/crates/egui-wgpu/src/egui.wgsl b/crates/egui-wgpu/src/egui.wgsl
index 5b4b8240f..bd3b5313e 100644
--- a/crates/egui-wgpu/src/egui.wgsl
+++ b/crates/egui-wgpu/src/egui.wgsl
@@ -98,9 +98,38 @@ fn vs_main(
@group(1) @binding(0) var r_tex_color: texture_2d;
@group(1) @binding(1) var r_tex_sampler: sampler;
-/// 1 if the texture sampler uses nearest filtering, 0 if linear.
-/// Only read when `predictable_texture_filtering` is on.
-@group(1) @binding(2) var r_tex_nearest_filtering: u32;
+/// Set in bit 0 of `r_tex_flags` if the sampler uses nearest filtering.
+///
+/// Must match `TEX_FLAG_NEAREST` in `renderer.rs`.
+const TEX_FLAG_NEAREST: u32 = 1u;
+
+/// Wrap modes, stored in bits 1+ of `r_tex_flags`.
+///
+/// Must match the `WRAP_MODE_*` constants in `renderer.rs`.
+const WRAP_MODE_CLAMP_TO_EDGE: u32 = 0u;
+const WRAP_MODE_REPEAT: u32 = 1u;
+const WRAP_MODE_MIRRORED_REPEAT: u32 = 2u;
+
+/// Texture flags, only read when `predictable_texture_filtering` is on.
+///
+/// Bit 0: `TEX_FLAG_NEAREST`.
+/// Bits 1+: one of the `WRAP_MODE_*` constants.
+@group(1) @binding(2) var r_tex_flags: u32;
+
+/// Map a texel coordinate to a valid texel according to the texture's wrap mode.
+fn wrap_texel_coord(coord: vec2, texture_size: vec2) -> vec2 {
+ let wrap_mode = r_tex_flags >> 1u;
+ if wrap_mode == WRAP_MODE_REPEAT {
+ return ((coord % texture_size) + texture_size) % texture_size;
+ } else if wrap_mode == WRAP_MODE_MIRRORED_REPEAT {
+ let period = 2 * texture_size;
+ let phase = ((coord % period) + period) % period;
+ return min(phase, period - vec2(1, 1) - phase);
+ } else {
+ // WRAP_MODE_CLAMP_TO_EDGE
+ return clamp(coord, vec2(0, 0), texture_size - vec2(1, 1));
+ }
+}
fn sample_texture(in: VertexOutput) -> vec4 {
if r_locals.predictable_texture_filtering == 0 {
@@ -109,11 +138,10 @@ fn sample_texture(in: VertexOutput) -> vec4 {
} else {
let texture_size = vec2(textureDimensions(r_tex_color, 0));
let texture_size_f = vec2(texture_size);
- let max_coord = texture_size - vec2(1, 1);
- if r_tex_nearest_filtering == 1 {
+ if (r_tex_flags & TEX_FLAG_NEAREST) != 0u {
// Nearest filtering: load the texel under the sample position.
- let texel = clamp(vec2(in.tex_coord * texture_size_f), vec2(0, 0), max_coord);
+ let texel = wrap_texel_coord(vec2(floor(in.tex_coord * texture_size_f)), texture_size);
return textureLoad(r_tex_color, texel, 0);
}
@@ -122,11 +150,10 @@ fn sample_texture(in: VertexOutput) -> vec4 {
let pixel_fract = fract(pixel_coord);
let pixel_floor = vec2(floor(pixel_coord));
- // Manual texture clamping
- let p00 = clamp(pixel_floor + vec2(0, 0), vec2(0, 0), max_coord);
- let p10 = clamp(pixel_floor + vec2(1, 0), vec2(0, 0), max_coord);
- let p01 = clamp(pixel_floor + vec2(0, 1), vec2(0, 0), max_coord);
- let p11 = clamp(pixel_floor + vec2(1, 1), vec2(0, 0), max_coord);
+ let p00 = wrap_texel_coord(pixel_floor + vec2(0, 0), texture_size);
+ let p10 = wrap_texel_coord(pixel_floor + vec2(1, 0), texture_size);
+ let p01 = wrap_texel_coord(pixel_floor + vec2(0, 1), texture_size);
+ let p11 = wrap_texel_coord(pixel_floor + vec2(1, 1), texture_size);
// Load at pixel centers
let tl = textureLoad(r_tex_color, p00, 0);
diff --git a/crates/egui-wgpu/src/renderer.rs b/crates/egui-wgpu/src/renderer.rs
index 267363591..cb9f40f18 100644
--- a/crates/egui-wgpu/src/renderer.rs
+++ b/crates/egui-wgpu/src/renderer.rs
@@ -245,11 +245,10 @@ pub struct Renderer {
uniform_bind_group: wgpu::BindGroup,
texture_bind_group_layout: wgpu::BindGroupLayout,
- /// Uniform buffers each holding a single `u32`:
- /// 1 if the texture sampler uses nearest filtering, 0 otherwise.
- /// Indexed by that flag value.
+ /// Uniform buffers each holding a single `u32` of texture flags
+ /// (see [`texture_flags`]), indexed by that flag value.
/// Read by the shader when `predictable_texture_filtering` is on.
- nearest_filtering_flag_buffers: [wgpu::Buffer; 2],
+ texture_flag_buffers: [wgpu::Buffer; NUM_TEXTURE_FLAGS],
/// Map of egui texture IDs to textures and their associated bindgroups (texture view +
/// sampler). The texture may be None if the `TextureId` is just a handle to a user-provided
@@ -367,9 +366,10 @@ impl Renderer {
})
};
- let nearest_filtering_flag_buffers = [0_u32, 1_u32].map(|flag| {
+ let texture_flag_buffers = core::array::from_fn::<_, NUM_TEXTURE_FLAGS, _>(|flag| {
+ let flag = flag as u32;
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
- label: Some(&format!("egui_nearest_filtering_flag_{flag}")),
+ label: Some(&format!("egui_texture_flags_{flag}")),
contents: bytemuck::bytes_of(&flag),
usage: wgpu::BufferUsages::UNIFORM,
})
@@ -482,7 +482,7 @@ impl Renderer {
previous_uniform_buffer_content: UniformBuffer::zeroed(),
uniform_bind_group,
texture_bind_group_layout,
- nearest_filtering_flag_buffers,
+ texture_flag_buffers,
textures: HashMap::default(),
next_user_texture_id: 0,
samplers: HashMap::default(),
@@ -736,6 +736,7 @@ impl Renderer {
let bind_group = bind_group.unwrap_or_else(|| {
let nearest =
image_delta.options.magnification == epaint::textures::TextureFilter::Nearest;
+ let wrap_mode = wrap_mode_flag(image_delta.options.wrap_mode);
let sampler = self
.samplers
.entry(image_delta.options)
@@ -756,7 +757,7 @@ impl Renderer {
},
wgpu::BindGroupEntry {
binding: 2,
- resource: self.nearest_filtering_flag_buffers[usize::from(nearest)]
+ resource: self.texture_flag_buffers[texture_flags(nearest, wrap_mode)]
.as_entire_binding(),
},
],
@@ -862,6 +863,7 @@ impl Renderer {
profiling::function_scope!();
let nearest = sampler_descriptor.mag_filter == wgpu::FilterMode::Nearest;
+ let wrap_mode = address_mode_wrap_flag(sampler_descriptor.address_mode_u);
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
compare: None,
..sampler_descriptor
@@ -881,7 +883,7 @@ impl Renderer {
},
wgpu::BindGroupEntry {
binding: 2,
- resource: self.nearest_filtering_flag_buffers[usize::from(nearest)]
+ resource: self.texture_flag_buffers[texture_flags(nearest, wrap_mode)]
.as_entire_binding(),
},
],
@@ -924,6 +926,7 @@ impl Renderer {
.expect("Tried to update a texture that has not been allocated yet.");
let nearest = sampler_descriptor.mag_filter == wgpu::FilterMode::Nearest;
+ let wrap_mode = address_mode_wrap_flag(sampler_descriptor.address_mode_u);
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
compare: None,
..sampler_descriptor
@@ -943,7 +946,7 @@ impl Renderer {
},
wgpu::BindGroupEntry {
binding: 2,
- resource: self.nearest_filtering_flag_buffers[usize::from(nearest)]
+ resource: self.texture_flag_buffers[texture_flags(nearest, wrap_mode)]
.as_entire_binding(),
},
],
@@ -1124,6 +1127,49 @@ impl Renderer {
}
}
+/// Set in bit 0 of the texture flags if the sampler uses nearest filtering.
+///
+/// Must match `TEX_FLAG_NEAREST` in `egui.wgsl`.
+const TEX_FLAG_NEAREST: u32 = 1;
+
+/// Wrap modes, stored in bits 1+ of the texture flags.
+///
+/// Must match the `WRAP_MODE_*` constants in `egui.wgsl`.
+const WRAP_MODE_CLAMP_TO_EDGE: u32 = 0;
+const WRAP_MODE_REPEAT: u32 = 1;
+const WRAP_MODE_MIRRORED_REPEAT: u32 = 2;
+
+/// Number of distinct values [`texture_flags`] can return.
+const NUM_TEXTURE_FLAGS: usize = 6;
+
+fn wrap_mode_flag(wrap_mode: epaint::textures::TextureWrapMode) -> u32 {
+ match wrap_mode {
+ epaint::textures::TextureWrapMode::ClampToEdge => WRAP_MODE_CLAMP_TO_EDGE,
+ epaint::textures::TextureWrapMode::Repeat => WRAP_MODE_REPEAT,
+ epaint::textures::TextureWrapMode::MirroredRepeat => WRAP_MODE_MIRRORED_REPEAT,
+ }
+}
+
+fn address_mode_wrap_flag(address_mode: wgpu::AddressMode) -> u32 {
+ match address_mode {
+ wgpu::AddressMode::Repeat => WRAP_MODE_REPEAT,
+ wgpu::AddressMode::MirrorRepeat => WRAP_MODE_MIRRORED_REPEAT,
+ wgpu::AddressMode::ClampToEdge | wgpu::AddressMode::ClampToBorder => {
+ WRAP_MODE_CLAMP_TO_EDGE
+ }
+ }
+}
+
+/// Index into [`Renderer::texture_flag_buffers`]: the texture flags
+/// read by the shader when `predictable_texture_filtering` is on.
+///
+/// Bit 0: [`TEX_FLAG_NEAREST`].
+/// Bits 1+: one of the `WRAP_MODE_*` constants.
+fn texture_flags(nearest: bool, wrap_mode: u32) -> usize {
+ let nearest = if nearest { TEX_FLAG_NEAREST } else { 0 };
+ (nearest | (wrap_mode << 1)) as usize
+}
+
fn create_sampler(
options: epaint::textures::TextureOptions,
device: &wgpu::Device,
diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs
index 48c63f436..a48b10570 100644
--- a/crates/egui/src/context.rs
+++ b/crates/egui/src/context.rs
@@ -2452,6 +2452,65 @@ impl Context {
TextureHandle::new(tex_mngr, tex_id)
}
+ /// Load a texture, or update a previously cached one if the image changed.
+ ///
+ /// This is like [`Self::load_texture`], but caches the texture by `id`,
+ /// and only re-uploads the image when it changes.
+ /// This makes it safe to call every frame,
+ /// which is convenient for small, procedurally generated images.
+ ///
+ /// The `id` must be globally unique for each cached image
+ /// (e.g. derived from a widget [`Id`]),
+ /// or the callers will fight over the same texture, re-uploading it every frame.
+ ///
+ /// If this is not called for a full frame, the cache entry is evicted,
+ /// dropping both the cached [`ImageData`] (the CPU-side pixels)
+ /// and the cached [`TextureHandle`].
+ /// Dropping the handle also frees the texture itself,
+ /// unless you keep a clone of the handle alive.
+ pub fn load_texture_cached(
+ &self,
+ name: impl Into,
+ id: Id,
+ image: impl Into,
+ options: TextureOptions,
+ ) -> TextureHandle {
+ profiling::function_scope!();
+
+ use crate::cache::FramePublisher;
+
+ type TextureCache = FramePublisher;
+
+ let image = image.into();
+ let cached: Option<(ImageData, TextureOptions, TextureHandle)> =
+ self.memory_mut(|mem| mem.caches.cache::().get(&id).cloned());
+
+ let (image, handle) = match cached {
+ Some((cached_image, cached_options, handle))
+ if cached_image == image && cached_options == options =>
+ {
+ (cached_image, handle)
+ }
+ Some((_, _, mut handle)) => {
+ handle.set(image.clone(), options);
+ (image, handle)
+ }
+ None => {
+ let handle = self.load_texture(name, image.clone(), options);
+ (image, handle)
+ }
+ };
+
+ // (Re-)publish to keep the entry from being evicted:
+ self.memory_mut(|mem| {
+ mem.caches
+ .cache::()
+ .set(id, (image, options, handle.clone()));
+ });
+
+ handle
+ }
+
/// Low-level texture manager.
///
/// In general it is easier to use [`Self::load_texture`] and [`TextureHandle`].
diff --git a/crates/egui/src/widgets/color_picker.rs b/crates/egui/src/widgets/color_picker.rs
index a85c2a7b4..90a548660 100644
--- a/crates/egui/src/widgets/color_picker.rs
+++ b/crates/egui/src/widgets/color_picker.rs
@@ -6,9 +6,11 @@ use crate::{
WidgetInfo, WidgetType, epaint, lerp, remap_clamp,
};
use epaint::{
- Mesh, Rect, Shape, Stroke, StrokeKind, Vec2,
+ ColorImage, Rect, RectShape, RoundedRect, Shape, Stroke, StrokeKind, Vec2,
ecolor::{Color32, Hsva, HsvaGamma, Rgba},
- pos2, vec2,
+ pos2,
+ textures::TextureOptions,
+ vec2,
};
fn contrast_color(color: impl Into) -> Color32 {
@@ -19,38 +21,63 @@ fn contrast_color(color: impl Into) -> Color32 {
}
}
-/// Number of vertices per dimension in the color sliders.
+/// Resolution of the color slider gradients: the textures have `N + 1` texels per dimension.
/// We need at least 6 for hues, and more for smooth 2D areas.
/// Should always be a multiple of 6 to hit the peak hues in HSV/HSL (every 60°).
const N: u32 = 6 * 6;
-fn background_checkers(painter: &Painter, rect: Rect) {
- let rect = rect.shrink(0.5); // Small hack to avoid the checkers from peeking through the sides
- if !rect.is_positive() {
+fn background_checkers(painter: &Painter, bounds: RoundedRect) {
+ // Shrink slightly, so the dark checkers don't peek through
+ // the antialiased edge of the color painted on top:
+ let pixel_width = 1.0 / painter.ctx().pixels_per_point();
+ let (rect, corner_radius) = bounds.shrink(pixel_width).into_parts();
+
+ if !rect.is_positive() || !rect.is_finite() {
return;
}
let dark_color = Color32::from_gray(32);
let bright_color = Color32::from_gray(128);
- let checker_size = Vec2::splat(rect.height() / 2.0);
- let n = (rect.width() / checker_size.x).round() as u32;
+ // A single repeating 2x2 checker tile, stretched so that
+ // each checker is half the rect height, like before:
+ let image = ColorImage::new(
+ [2, 2],
+ vec![bright_color, dark_color, dark_color, bright_color],
+ );
+ let texture = painter.ctx().load_texture_cached(
+ "color_picker_checkers",
+ Id::new("color_picker_checkers"),
+ image,
+ TextureOptions::NEAREST_REPEAT,
+ );
- let mut mesh = Mesh::default();
- mesh.add_colored_rect(rect, dark_color);
+ // One tile (one uv unit) covers 2x2 checkers, i.e. the full rect height:
+ let uv = Rect::from_min_max(pos2(0.0, 0.0), pos2(rect.width() / rect.height(), 1.0));
+ painter
+ .add(RectShape::filled(rect, corner_radius, Color32::WHITE).with_texture(texture.id(), uv));
+}
- let mut top = true;
- for i in 0..n {
- let x = lerp(rect.left()..=rect.right(), i as f32 / (n as f32));
- let small_rect = if top {
- Rect::from_min_size(pos2(x, rect.top()), checker_size)
- } else {
- Rect::from_min_size(pos2(x, rect.center().y), checker_size)
- };
- mesh.add_colored_rect(small_rect, bright_color);
- top = !top;
- }
- painter.add(Shape::mesh(mesh));
+/// Paint a gradient image over the given rounded rect, with an outline.
+fn paint_gradient(ui: &Ui, id: Id, bounds: RoundedRect, outline: Stroke, image: ColorImage) {
+ let (rect, corner_radius) = bounds.into_parts();
+ let [width, height] = image.size;
+ let texture = ui
+ .ctx()
+ .load_texture_cached("color_gradient", id, image, TextureOptions::LINEAR);
+
+ // Inset the uv by half a texel, so the edges sample the exact edge colors:
+ let inset = vec2(0.5 / width as f32, 0.5 / height as f32);
+ let uv = Rect::from_min_max(inset.to_pos2(), pos2(1.0 - inset.x, 1.0 - inset.y));
+
+ ui.painter()
+ .add(RectShape::filled(rect, corner_radius, Color32::WHITE).with_texture(texture.id(), uv));
+
+ // The outline must be a separate shape:
+ // a textured `RectShape` cannot have a stroke,
+ // since the stroke vertices would sample the same texture.
+ ui.painter()
+ .rect_stroke(rect, corner_radius, outline, StrokeKind::Inside);
}
/// Show a color with background checkers to demonstrate transparency (if any).
@@ -72,7 +99,7 @@ pub fn show_color_at(painter: &Painter, color: Color32, rect: Rect) {
painter.rect_filled(rect, 0.0, color);
} else {
// Transparent: how both the transparent and opaque versions of the color
- background_checkers(painter, rect);
+ background_checkers(painter, rect.into());
if color == Color32::TRANSPARENT {
// There is no opaque version, so just show the background checkers
@@ -89,21 +116,35 @@ pub fn show_color_at(painter: &Painter, color: Color32, rect: Rect) {
fn show_srgba_unmultiplied(ui: &mut Ui, srgba: [u8; 4], desired_size: Vec2) -> Response {
let (rect, response) = ui.allocate_at_least(desired_size, Sense::hover());
if ui.is_rect_visible(rect) {
- show_srgba_unmultiplied_at(ui.painter(), srgba, rect);
+ let corner_radius = ui.visuals().widgets.noninteractive.corner_radius;
+ show_srgba_unmultiplied_at(ui.painter(), srgba, RoundedRect::new(rect, corner_radius));
}
response
}
/// Show a color with background checkers to demonstrate transparency (if any).
-fn show_srgba_unmultiplied_at(painter: &Painter, [r, g, b, a]: [u8; 4], rect: Rect) {
+fn show_srgba_unmultiplied_at(painter: &Painter, [r, g, b, a]: [u8; 4], bounds: RoundedRect) {
+ let (rect, corner_radius) = bounds.into_parts();
if a == 255 {
- painter.rect_filled(rect, 0.0, Color32::from_rgb(r, g, b));
+ painter.rect_filled(rect, corner_radius, Color32::from_rgb(r, g, b));
} else {
- background_checkers(painter, rect);
let left = Rect::from_min_max(rect.left_top(), rect.center_bottom());
let right = Rect::from_min_max(rect.center_top(), rect.right_bottom());
- painter.rect_filled(left, 0.0, Color32::from_rgba_unmultiplied(r, g, b, a));
- painter.rect_filled(right, 0.0, Color32::from_rgb(r, g, b));
+
+ // Clamp to what the half-width rects can express,
+ // so the checkers and both halves all round their corners the same:
+ let corner_radius = corner_radius.at_most(0.5 * left.size().min_elem());
+
+ background_checkers(painter, RoundedRect::new(rect, corner_radius));
+
+ let left_corner_radius = corner_radius.with_east(0.0);
+ let right_corner_radius = corner_radius.with_west(0.0);
+ painter.rect_filled(
+ left,
+ left_corner_radius,
+ Color32::from_rgba_unmultiplied(r, g, b, a),
+ );
+ painter.rect_filled(right, right_corner_radius, Color32::from_rgb(r, g, b));
}
}
@@ -121,9 +162,15 @@ fn color_button(ui: &mut Ui, srgba: [u8; 4], open: bool) -> Response {
let rect = rect.expand(visuals.expansion);
let stroke_width = 1.0;
- show_srgba_unmultiplied_at(ui.painter(), srgba, rect.shrink(stroke_width));
+ let corner_radius = visuals.corner_radius;
+ show_srgba_unmultiplied_at(
+ ui.painter(),
+ srgba,
+ // Shrink both the rect and the corner radius,
+ // so the fill arcs stay concentric with the inside stroke:
+ RoundedRect::new(rect, corner_radius).shrink(stroke_width),
+ );
- 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,
corner_radius,
@@ -136,8 +183,6 @@ fn color_button(ui: &mut Ui, srgba: [u8; 4], open: bool) -> Response {
}
fn color_slider_1d(ui: &mut Ui, value: &mut f32, color_at: impl Fn(f32) -> Color32) -> Response {
- #![expect(clippy::identity_op)]
-
let desired_size = vec2(ui.spacing().slider_width, ui.spacing().interact_size.y);
let (rect, response) = ui.allocate_at_least(desired_size, Sense::click_and_drag());
@@ -147,29 +192,25 @@ fn color_slider_1d(ui: &mut Ui, value: &mut f32, color_at: impl Fn(f32) -> Color
if ui.is_rect_visible(rect) {
let visuals = ui.style().interact(&response);
+ let corner_radius = visuals.corner_radius;
+ let bounds = RoundedRect::new(rect, corner_radius);
- background_checkers(ui.painter(), rect); // for alpha:
+ background_checkers(ui.painter(), bounds); // for alpha:
{
- // fill color:
- let mut mesh = Mesh::default();
- for i in 0..=N {
- let t = i as f32 / (N as f32);
- let color = color_at(t);
- let x = lerp(rect.left()..=rect.right(), t);
- mesh.colored_vertex(pos2(x, rect.top()), color);
- mesh.colored_vertex(pos2(x, rect.bottom()), color);
- if i < N {
- mesh.add_triangle(2 * i + 0, 2 * i + 1, 2 * i + 2);
- mesh.add_triangle(2 * i + 1, 2 * i + 2, 2 * i + 3);
- }
- }
- ui.painter().add(Shape::mesh(mesh));
+ // fill color gradient:
+ let width = N as usize + 1;
+ let pixels = (0..width).map(|i| color_at(i as f32 / N as f32)).collect();
+ let image = ColorImage::new([width, 1], pixels);
+ paint_gradient(
+ ui,
+ response.id.with("gradient"),
+ bounds,
+ visuals.bg_stroke,
+ image,
+ );
}
- ui.painter()
- .rect_stroke(rect, 0.0, visuals.bg_stroke, StrokeKind::Inside); // outline
-
{
// Show where the slider is at:
let x = lerp(rect.left()..=rect.right(), *value);
@@ -215,30 +256,28 @@ fn color_slider_2d(
if ui.is_rect_visible(rect) {
let visuals = ui.style().interact(&response);
- let mut mesh = Mesh::default();
+ let corner_radius = visuals.corner_radius;
- for xi in 0..=N {
+ {
+ // fill color gradient:
+ let width = N as usize + 1;
+ let mut pixels = Vec::with_capacity(width * width);
for yi in 0..=N {
- let xt = xi as f32 / (N as f32);
- let yt = yi as f32 / (N as f32);
- let color = color_at(xt, yt);
- let x = lerp(rect.left()..=rect.right(), xt);
- let y = lerp(rect.bottom()..=rect.top(), yt);
- mesh.colored_vertex(pos2(x, y), color);
-
- if xi < N && yi < N {
- let x_offset = 1;
- let y_offset = N + 1;
- let tl = yi * y_offset + xi;
- mesh.add_triangle(tl, tl + x_offset, tl + y_offset);
- mesh.add_triangle(tl + x_offset, tl + y_offset, tl + y_offset + x_offset);
+ let yt = 1.0 - yi as f32 / (N as f32); // texel rows go from top to bottom
+ for xi in 0..=N {
+ let xt = xi as f32 / (N as f32);
+ pixels.push(color_at(xt, yt));
}
}
+ let image = ColorImage::new([width, width], pixels);
+ paint_gradient(
+ ui,
+ response.id.with("gradient"),
+ RoundedRect::new(rect, corner_radius),
+ visuals.bg_stroke,
+ image,
+ );
}
- ui.painter().add(Shape::mesh(mesh)); // fill
-
- ui.painter()
- .rect_stroke(rect, 0.0, visuals.bg_stroke, StrokeKind::Inside); // outline
// Show where the slider is at:
let x = lerp(rect.left()..=rect.right(), *x_value);
diff --git a/crates/egui_demo_app/tests/snapshots/imageviewer.png b/crates/egui_demo_app/tests/snapshots/imageviewer.png
index e87b6a8a7..c08ab1aac 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:8f93b6872fb1df5841afcc61a840514bd391f436274136e14725364e33f1e4b2
-size 101079
+oid sha256:24fc466f6470761a5064a590929fa3a0d95741ed691fc257415eee349bc528e9
+size 101394
diff --git a/crates/egui_demo_lib/tests/color_picker.rs b/crates/egui_demo_lib/tests/color_picker.rs
new file mode 100644
index 000000000..e151e22e8
--- /dev/null
+++ b/crates/egui_demo_lib/tests/color_picker.rs
@@ -0,0 +1,17 @@
+use egui::Color32;
+use egui::color_picker::{Alpha, color_picker_color32};
+use egui_kittest::Harness;
+
+#[test]
+fn color_picker() {
+ let mut color = Color32::from_rgba_unmultiplied(130, 130, 130, 45);
+ let mut harness = Harness::builder()
+ .with_pixels_per_point(2.0)
+ .build_ui(move |ui| {
+ ui.spacing_mut().slider_width = 275.0;
+ color_picker_color32(ui, &mut color, Alpha::OnlyBlend);
+ });
+ harness.run();
+ harness.fit_contents();
+ harness.snapshot("color_picker");
+}
diff --git a/crates/egui_demo_lib/tests/snapshots/color_picker.png b/crates/egui_demo_lib/tests/snapshots/color_picker.png
new file mode 100644
index 000000000..f39bcb23a
--- /dev/null
+++ b/crates/egui_demo_lib/tests/snapshots/color_picker.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ed0f9f3ec94ac3d75266343d11cae5842d72fbea3c78f7995b1b0ad5aa3cdcad
+size 141933
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Frame.png b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png
index 7bd61f73e..3464b0467 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:3b6f03016682b8a9970d2f425cfd417833b8e532c69bbcfbf8290ccaa8335f3b
-size 25938
+oid sha256:68351659df756ce331a4eeb147be8d1b4fed39dbbb54a0d6f685997b54dc07a7
+size 26327
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scene.png b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png
index 721c8a64c..0fe8d6285 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:65d1c8766fa8f854806a408a0289a79e942fdf6642be7cf6f4e8924412d31c25
-size 30855
+oid sha256:65d1d15d78c871eb87224c44d2fe889cc0d7a33eb15aeb4852c10bdb02aa730d
+size 30877
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 8cae2d56f..be9c23ad9 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:6402e1da2627f143d07e01b1bcbcbfafa174fcf76fd4c1f8b217b5227528a067
-size 590792
+oid sha256:10d58fa20fe3cc06a98dc79adf6237652b87871d2dc394476ea524ac62f2750b
+size 590735
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 6ba4fe11b..41e2ecba3 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:eddc77778c52b56763e1906fe9fdc4e575d4c99f3a438dc9f74260f75f13154f
-size 745930
+oid sha256:1de8583bef3fb9e3530b4a09f498360d37d21c099dd44cd45ee99ccc332d7c1c
+size 745855
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 029708c0d..c17b4e694 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:bfc46024827dd504be4765a3eab02bb1c27cb870d128285580fa1eb0cff8390c
-size 973977
+oid sha256:43c1f297b0d3e62483b05efe3eb7089a61944b2e348bc791f0503ccbf6c5ab4a
+size 973927
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 fb0da4a80..061cf2451 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:9658cc6b4dd2dd33356facc6b48cf69f9a86f976ac2dd4f16e362a2fd3f73827
-size 1081864
+oid sha256:17790adf9f5ba91aa00a22c44303422ed02e71a1ab7b5c1484e13e2a132bd639
+size 1081792
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 2e7cf7674..4b5cbb4d4 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:e9918502356af39490dc8056509117dee7b7a290c1cac06767d3478c38bb27d3
-size 1131255
+oid sha256:b434961f2ac616e2563aa2ff9e706158190dfcc2713de6fd7740290e603c8a1b
+size 1131176
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 48f651b79..06fd5cd91 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:95cd6e583873e36399df82133b8eca8b3be34ea8d5c6d36cb653288c8cf3b53f
-size 1363872
+oid sha256:de79f0a3dacdf5387faaeeb64f8939ff5912e362d8d649d6e25fd3dbda940325
+size 1363775
diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png
index 507e73c88..a3bfe564d 100644
--- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png
+++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a7028aa93b6212054dc41d3daf88454d53e6b05d670b987f2d918f6aa0b38324
-size 46455
+oid sha256:a62ed81b1693cd7eea2099c0840d82318d27e382f88ca1dd15ba9e2a59e3cf4b
+size 46677
diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png
index 256e413b3..33acf005a 100644
--- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png
+++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:603afb5653a960161a6884647bc51e8547160ffab186d7a9a2b4839ed77e7d0b
-size 120323
+oid sha256:5ad40e8b41882e3456569c8cb2e831d33fafa910989674e05b20568798528154
+size 120579
diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png
index 68520ec06..c04b50283 100644
--- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png
+++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:26c857faec0666f207182166d34bc9980f1cf3165813a9128cd24d1429156933
-size 37357
+oid sha256:ea03f6b0e0071910cf2b4279549fb0347cd6a9f8d0334830ce4f34f5824a1ef9
+size 37595
diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png
index 3ae797d56..6ccb7780e 100644
--- a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png
+++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:46af578e5e90d0591644707b9ad305615ee098182ffa14c6ccf29bdbbc2a0bef
-size 65919
+oid sha256:952df0869c400d8b1f5dc0a33cb7a8be1fa30d91929e57b7c33af0cfbc9cead4
+size 66043
diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png
index 234926bbb..025a284fd 100644
--- a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png
+++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:71433ceac92b327d2c5109302c479d398b64283a75327bba6d584654a0c53f3e
-size 153251
+oid sha256:066067e34d47aa8d2631e64b2ea37d94bae77bdfb1af9337f34f724e1ff14cb6
+size 153589
diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png
index d767c6179..d5aa7b0d0 100644
--- a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png
+++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:196d957732cc5a0d0b8b30875b5ade59236c288293e2cc8a53f9ed6c89edad79
-size 60722
+oid sha256:0d79861c18cbaf2f2ab738d0a5633f354c11a52b8a8e87c623e624ecdfcc6965
+size 60813
diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png
index e4544e5d2..005503f34 100644
--- a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png
+++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8d26c20506fffcbca07c3eb0a986357129590684da83f8cdd1a56c65cf3e6452
-size 147925
+oid sha256:cde27ba5614f4bd5553d609b23e6dd78aea113c88443de42fa47aa85929c9455
+size 148235
diff --git a/crates/epaint/src/corner_radius_f32.rs b/crates/epaint/src/corner_radius_f32.rs
index ef6e597f5..75210b744 100644
--- a/crates/epaint/src/corner_radius_f32.rs
+++ b/crates/epaint/src/corner_radius_f32.rs
@@ -73,6 +73,7 @@ impl CornerRadiusF32 {
/// Same rounding on all four corners.
#[inline]
+ #[must_use]
pub const fn same(radius: f32) -> Self {
Self {
nw: radius,
@@ -90,6 +91,7 @@ impl CornerRadiusF32 {
/// Make sure each corner has a rounding of at least this.
#[inline]
+ #[must_use]
pub fn at_least(&self, min: f32) -> Self {
Self {
nw: self.nw.max(min),
@@ -101,6 +103,7 @@ impl CornerRadiusF32 {
/// Make sure each corner has a rounding of at most this.
#[inline]
+ #[must_use]
pub fn at_most(&self, max: f32) -> Self {
Self {
nw: self.nw.min(max),
@@ -109,79 +112,111 @@ impl CornerRadiusF32 {
se: self.se.min(max),
}
}
+
+ /// Set the rounding of the two east (right) corners.
+ #[inline]
+ #[must_use]
+ pub fn with_east(self, radius: f32) -> Self {
+ Self {
+ ne: radius,
+ se: radius,
+ ..self
+ }
+ }
+
+ /// Set the rounding of the two north (top) corners.
+ #[inline]
+ #[must_use]
+ pub fn with_north(self, radius: f32) -> Self {
+ Self {
+ nw: radius,
+ ne: radius,
+ ..self
+ }
+ }
+
+ /// Set the rounding of the two south (bottom) corners.
+ #[inline]
+ #[must_use]
+ pub fn with_south(self, radius: f32) -> Self {
+ Self {
+ sw: radius,
+ se: radius,
+ ..self
+ }
+ }
+
+ /// Set the rounding of the two west (left) corners.
+ #[inline]
+ #[must_use]
+ pub fn with_west(self, radius: f32) -> Self {
+ Self {
+ nw: radius,
+ sw: radius,
+ ..self
+ }
+ }
}
impl core::ops::Add for CornerRadiusF32 {
type Output = Self;
+
+ /// Saturates at zero: no corner radius will go negative.
#[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 + rhs.nw).max(0.0),
+ ne: (self.ne + rhs.ne).max(0.0),
+ sw: (self.sw + rhs.sw).max(0.0),
+ se: (self.se + rhs.se).max(0.0),
}
}
}
impl core::ops::AddAssign for CornerRadiusF32 {
+ /// Saturates at zero: no corner radius will go negative.
#[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,
- };
+ *self = *self + rhs;
}
}
impl core::ops::AddAssign for CornerRadiusF32 {
+ /// Saturates at zero: no corner radius will go negative.
#[inline]
fn add_assign(&mut self, rhs: f32) {
- *self = Self {
- nw: self.nw + rhs,
- ne: self.ne + rhs,
- sw: self.sw + rhs,
- se: self.se + rhs,
- };
+ *self = *self + Self::same(rhs);
}
}
impl core::ops::Sub for CornerRadiusF32 {
type Output = Self;
+
+ /// Saturates at zero: no corner radius will go negative.
#[inline]
fn sub(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 - rhs.nw).max(0.0),
+ ne: (self.ne - rhs.ne).max(0.0),
+ sw: (self.sw - rhs.sw).max(0.0),
+ se: (self.se - rhs.se).max(0.0),
}
}
}
impl core::ops::SubAssign for CornerRadiusF32 {
+ /// Saturates at zero: no corner radius will go negative.
#[inline]
fn sub_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,
- };
+ *self = *self - rhs;
}
}
impl core::ops::SubAssign for CornerRadiusF32 {
+ /// Saturates at zero: no corner radius will go negative.
#[inline]
fn sub_assign(&mut self, rhs: f32) {
- *self = Self {
- nw: self.nw - rhs,
- ne: self.ne - rhs,
- sw: self.sw - rhs,
- se: self.se - rhs,
- };
+ *self = *self - Self::same(rhs);
}
}
@@ -234,3 +269,26 @@ impl core::ops::MulAssign for CornerRadiusF32 {
};
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::CornerRadiusF32;
+
+ #[test]
+ fn add_and_sub_saturate_at_zero() {
+ assert_eq!(
+ CornerRadiusF32::same(2.0) + CornerRadiusF32::same(-5.0),
+ CornerRadiusF32::ZERO
+ );
+ assert_eq!(
+ CornerRadiusF32::same(2.0) - CornerRadiusF32::same(5.0),
+ CornerRadiusF32::ZERO
+ );
+
+ let mut cr = CornerRadiusF32::same(1.0);
+ cr += -3.0;
+ assert_eq!(cr, CornerRadiusF32::ZERO);
+ cr -= -2.0;
+ assert_eq!(cr, CornerRadiusF32::same(2.0));
+ }
+}
diff --git a/crates/epaint/src/rounded_rect.rs b/crates/epaint/src/rounded_rect.rs
index e5023dcd0..07de5f1cb 100644
--- a/crates/epaint/src/rounded_rect.rs
+++ b/crates/epaint/src/rounded_rect.rs
@@ -53,6 +53,13 @@ impl RoundedRect {
)
}
+ /// Shrink the rectangle and the corner radii by the given amount.
+ #[inline]
+ #[must_use]
+ pub fn shrink(self, amount: f32) -> Self {
+ self.expand(-amount)
+ }
+
/// Clamp the given position to lie within this rounded rectangle.
///
/// Positions in the corner regions are projected onto the corner arcs.
From 7aa7f848581e57c74cccff5fcb48bd3bd48b8f28 Mon Sep 17 00:00:00 2001
From: rustbasic <127506429+rustbasic@users.noreply.github.com>
Date: Mon, 24 Aug 2026 19:49:33 +0900
Subject: [PATCH 09/16] Fix transparent child viewports on `Windows` with
`glow` (#8423)
Fix transparent child viewports on `Windows` with `glow`
* Closes #3632
* Related #4451
* Related #5072
* Closes #7543
* Related #8116
Transparent native child viewports could become opaque on Windows when
the
selected GL config reports that it does not support transparency.
`glutin_winit::finalize_window` clears the native transparent window
attribute
in that case. However, on affected Windows GL paths, transparent native
windows
and their GL surfaces still composite correctly.
This change preserves the transparent window attribute for explicitly
transparent non-root viewports on Windows by creating those windows
directly.
Tested manually on Windows with glow:
- root transparent viewport
- deferred native child viewport
- immediate native child viewport
---
crates/eframe/src/native/glow_integration.rs | 31 ++++++++++++++++++--
1 file changed, 29 insertions(+), 2 deletions(-)
diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs
index cca1d22c6..28a1ee681 100644
--- a/crates/eframe/src/native/glow_integration.rs
+++ b/crates/eframe/src/native/glow_integration.rs
@@ -1268,11 +1268,30 @@ impl GlutinWindowContext {
);
if window_attributes.transparent()
&& self.gl_config.supports_transparency() == Some(false)
+ && !cfg!(target_os = "windows")
{
log::error!("Cannot create transparent window: the GL config does not support it");
}
- let window =
- glutin_winit::finalize_window(event_loop, window_attributes, &self.gl_config)?;
+
+ let window = cfg_select! {
+ target_os = "windows" => {
+ if viewport_id != ViewportId::ROOT && window_attributes.transparent() {
+ // Preserve explicitly requested transparent child viewports on Windows.
+ // Some GL paths report no transparency support although composition works.
+ event_loop.create_window(window_attributes)?
+ } else {
+ glutin_winit::finalize_window(
+ event_loop,
+ window_attributes,
+ &self.gl_config,
+ )?
+ }
+ }
+ _ => {
+ // Keep the normal platform-specific finalization path elsewhere.
+ glutin_winit::finalize_window(event_loop, window_attributes, &self.gl_config)?
+ }
+ };
egui_winit::apply_viewport_builder_to_window(
&self.egui_ctx,
&window,
@@ -1497,9 +1516,17 @@ fn initialize_or_update_viewport(
.and_then(|vp| vp.builder.icon.clone());
}
+ let root_transparent = viewports
+ .get(&ViewportId::ROOT)
+ .and_then(|viewport| viewport.builder.transparent);
+
match viewports.entry(ids.this) {
Entry::Vacant(entry) => {
// New viewport:
+ if ids.this != ViewportId::ROOT && builder.transparent.is_none() {
+ // Child viewports inherit the root setting unless they explicitly override it.
+ builder.transparent = root_transparent;
+ }
log::debug!("Creating new viewport {:?} ({:?})", ids.this, builder.title);
entry.insert(Viewport {
ids,
From 144b1a1f833c1d049cc1e8d82ee9bc9872c486ee Mon Sep 17 00:00:00 2001
From: Sylvain <67423638+Le-Syl21@users.noreply.github.com>
Date: Tue, 25 Aug 2026 14:07:33 +0200
Subject: [PATCH 10/16] eframe: apply ViewportBuilder::with_monitor on the glow
backend (#8302)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Problem
`ViewportBuilder::with_monitor(idx)` is a **no-op on the glow backend**.
A window
built with `.with_monitor(...)` opens on the default monitor, not
fullscreen —
while the exact same code works on wgpu.
The monitor → `Fullscreen::Borderless(Some(monitor))` resolution only
lives in
`egui_winit::create_window`, which the **wgpu** backend uses. The
**glow**
backend builds its windows straight from
`egui_winit::create_winit_window_attributes`
at two sites in `glow_integration.rs` — the initial window and
per-viewport
deferred windows — so the monitor is never applied there.
## Repro
A multi-viewport app on the glow renderer with any viewport built via
`ViewportBuilder::default().with_monitor(1)` opens that window on
monitor 0,
un-fullscreen. Switching to the wgpu renderer places it correctly. Seen
on a
3-monitor GNOME/Wayland setup where the main window and secondary
viewports all
need to land borderless-fullscreen on specific outputs.
## Fix
Extract the resolution into a public
`apply_monitor_to_window_attributes` in
`egui-winit` and call it from `create_window` (unchanged behaviour) plus
both
glow window-creation sites, so `with_monitor` behaves the same on glow
and wgpu.
No public API change beyond the new helper; no behaviour change on wgpu.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---
crates/eframe/src/native/glow_integration.rs | 17 +++++----
crates/egui-winit/src/lib.rs | 36 +++++++++++++++-----
2 files changed, 38 insertions(+), 15 deletions(-)
diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs
index 28a1ee681..82a150aad 100644
--- a/crates/eframe/src/native/glow_integration.rs
+++ b/crates/eframe/src/native/glow_integration.rs
@@ -1094,9 +1094,10 @@ impl GlutinWindowContext {
//
// The justification for FallbackEgl over PreferEgl is at https://github.com/emilk/egui/pull/2526#issuecomment-1400229576 .
.with_preference(glutin_winit::ApiPreference::FallbackEgl)
- .with_window_attributes(Some(egui_winit::create_winit_window_attributes(
- egui_ctx,
- viewport_builder.clone(),
+ .with_window_attributes(Some(egui_winit::apply_monitor_to_window_attributes(
+ egui_winit::create_winit_window_attributes(egui_ctx, viewport_builder.clone()),
+ &viewport_builder,
+ event_loop,
)));
let (window, gl_config) = {
@@ -1262,9 +1263,13 @@ impl GlutinWindowContext {
window
} else {
log::debug!("Creating a window for viewport {viewport_id:?}");
- let window_attributes = egui_winit::create_winit_window_attributes(
- &self.egui_ctx,
- viewport.builder.clone(),
+ let window_attributes = egui_winit::apply_monitor_to_window_attributes(
+ egui_winit::create_winit_window_attributes(
+ &self.egui_ctx,
+ viewport.builder.clone(),
+ ),
+ &viewport.builder,
+ event_loop,
);
if window_attributes.transparent()
&& self.gl_config.supports_transparency() == Some(false)
diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs
index 0029a47b7..70d1fda78 100644
--- a/crates/egui-winit/src/lib.rs
+++ b/crates/egui-winit/src/lib.rs
@@ -1971,12 +1971,33 @@ pub fn create_window(
) -> Result {
profiling::function_scope!();
- let mut window_attributes = create_winit_window_attributes(egui_ctx, viewport_builder.clone());
+ let window_attributes = apply_monitor_to_window_attributes(
+ create_winit_window_attributes(egui_ctx, viewport_builder.clone()),
+ viewport_builder,
+ event_loop,
+ );
- // Resolve target monitor index → MonitorHandle, so the window is created
- // directly in borderless fullscreen on the requested output. This is the
- // only reliable way to target a specific monitor under Wayland, and also
- // avoids the Mutter race where OuterPosition is ignored pre-mapping.
+ let window = event_loop.create_window(window_attributes)?;
+ apply_viewport_builder_to_window(egui_ctx, &window, viewport_builder);
+ Ok(window)
+}
+
+/// Apply [`ViewportBuilder::with_monitor`] to freshly-built [`winit::window::WindowAttributes`].
+///
+/// Resolve the target monitor index → `MonitorHandle` and request borderless
+/// fullscreen on that output, so the window is created directly on the right
+/// monitor. This is the only reliable way to target a specific monitor under
+/// Wayland, and also avoids the Mutter race where `OuterPosition` is ignored
+/// pre-mapping.
+///
+/// Must be called by every backend that builds its own window from
+/// [`create_winit_window_attributes`] (the glow backend and per-viewport window
+/// creation do this) — otherwise `with_monitor` silently does nothing there.
+pub fn apply_monitor_to_window_attributes(
+ mut window_attributes: winit::window::WindowAttributes,
+ viewport_builder: &ViewportBuilder,
+ event_loop: &ActiveEventLoop,
+) -> winit::window::WindowAttributes {
if let Some(idx) = viewport_builder.monitor {
if let Some(monitor) = event_loop.available_monitors().nth(idx) {
window_attributes = window_attributes
@@ -1988,10 +2009,7 @@ pub fn create_window(
);
}
}
-
- let window = event_loop.create_window(window_attributes)?;
- apply_viewport_builder_to_window(egui_ctx, &window, viewport_builder);
- Ok(window)
+ window_attributes
}
pub fn create_winit_window_attributes(
From 2fca9406d78b08f5c6ddaba9f9f57ee1c78ab52e Mon Sep 17 00:00:00 2001
From: taro <34352544+tarolling@users.noreply.github.com>
Date: Tue, 25 Aug 2026 07:15:18 -0500
Subject: [PATCH 11/16] Hide drop shadow window decoration when in fullscreen
mode (#8449)
* Closes
* [x] I have followed the instructions in the PR template
---
crates/egui-winit/src/lib.rs | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs
index 70d1fda78..5ca800108 100644
--- a/crates/egui-winit/src/lib.rs
+++ b/crates/egui-winit/src/lib.rs
@@ -1867,7 +1867,9 @@ fn process_viewport_command(
#[cfg(target_os = "windows")]
{
use winit::platform::windows::WindowExtWindows as _;
- window.set_undecorated_shadow(!v);
+
+ // don't request the undecorated-window drop shadow in fullscreen (#8399)
+ window.set_undecorated_shadow(!v && window.fullscreen().is_none());
}
}
ViewportCommand::WindowLevel(l) => window.set_window_level(match l {
@@ -2189,7 +2191,10 @@ pub fn create_winit_window_attributes(
if let Some(show) = _taskbar {
window_attributes = window_attributes.with_skip_taskbar(!show);
}
- window_attributes = window_attributes.with_undecorated_shadow(!decorations.unwrap_or(true));
+
+ // don't request the undecorated-window drop shadow in fullscreen (#8399)
+ let want_undecorated_shadow = !decorations.unwrap_or(true) && !fullscreen.unwrap_or(false);
+ window_attributes = window_attributes.with_undecorated_shadow(want_undecorated_shadow);
}
#[cfg(target_os = "macos")]
From f228ed2232d3a6b3c223583fd2ef5dabb6dc8171 Mon Sep 17 00:00:00 2001
From: Zachary Harvie
Date: Tue, 25 Aug 2026 05:16:33 -0700
Subject: [PATCH 12/16] Fix `egui-wgpu` build on `wasm32-unknown-emscripten`
(#8454)
Fix a build issue in the `egui-wgpu` crate when building for the
`wasm32-unknown-emscripten` target. WGPU does not support the WebGPU
backend on that target.
---
crates/egui-wgpu/src/setup.rs | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/crates/egui-wgpu/src/setup.rs b/crates/egui-wgpu/src/setup.rs
index 3909a9b53..3c9ceae5d 100644
--- a/crates/egui-wgpu/src/setup.rs
+++ b/crates/egui-wgpu/src/setup.rs
@@ -101,7 +101,10 @@ impl WgpuSetup {
let mut backends = create_new.instance_descriptor.backends;
// Don't try WebGPU if we're not in a secure context.
- #[cfg(target_arch = "wasm32")]
+ // Emscripten is excluded: wgpu gates both its `webgpu` backend and
+ // its `web_sys` re-export on `not(Emscripten)` (see the cfg aliases
+ // in `wgpu/build.rs`).
+ #[cfg(all(target_arch = "wasm32", not(target_os = "emscripten")))]
if backends.contains(wgpu::Backends::BROWSER_WEBGPU) {
let is_secure_context =
wgpu::web_sys::window().is_some_and(|w| w.is_secure_context());
From 35b069585dd26bf5f0840111aeb03474bc71066a Mon Sep 17 00:00:00 2001
From: Emil Ernerfeldt
Date: Tue, 25 Aug 2026 06:41:02 -0700
Subject: [PATCH 13/16] Add an LLM policy (#8458)
https://www.smbc-comics.com/comic/blame
---------
Co-authored-by: Lucas Meurer
---
.github/pull_request_template.md | 3 ++-
CONTRIBUTING.md | 12 ++++++++++++
2 files changed, 14 insertions(+), 1 deletion(-)
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index fb9926e3a..559430c7f 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -3,11 +3,12 @@ Please read the "Making a PR" section of [`CONTRIBUTING.md`](https://github.com/
* Keep your PR:s small and focused.
* The PR title is what ends up in the changelog, so make it descriptive!
+* Keep PR description short and to the point! No long LLM slop.
* If applicable, add a screenshot or gif.
* If it is a non-trivial addition, consider adding a demo for it to `egui_demo_lib`, or a new example.
* Do NOT open PR:s from your `master` branch, as that makes it hard for maintainers to test and add commits to your PR.
* Remember to run `cargo fmt` and `cargo clippy`.
-* Open the PR as a draft until you have self-reviewed it and run `./scripts/check.sh`.
+* Open the PR as a draft until a human has reviewed it
* When you have addressed a PR comment, mark it as resolved.
Please be patient! I will review your PR, but my time is limited!
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d3140d942..a2ce09dcf 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -48,6 +48,18 @@ Don't worry about having many small commits in the PR - they will be squashed to
Please keep pull requests small and focused. The smaller it is, the more likely it is to get merged.
+### Agents & LLM policy
+We care about maintaining high code quality, no matter who or what wrote the code.
+
+You (human!) are always responsible for the quality of code, issues, and PR descriptions.
+
+A human should always review the code before undrafting the PR.
+
+Make sure the PR description is short and to the point.
+
+#### Undue burden
+We reserve the right to close any PRs that cause an undue burden on the maintainership. This could include, but is not limited to, massive PRs, LLM slop, or contributions not in good faith.
+
## Working with git lfs
We use [git-lfs](https://git-lfs.com/) to store big files in the repository.
From caea233a47870d5477383949057cc9fdc0f7fbca Mon Sep 17 00:00:00 2001
From: Juanperias <136520331+Juanperias@users.noreply.github.com>
Date: Wed, 26 Aug 2026 02:09:32 -0400
Subject: [PATCH 14/16] Fix wgpu padding (#8448)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The Egui web example was throwing an error in WGPU. In this case, the
error was
```
panicked at /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/wgpu-30.0.0/src/backend/wgpu_core.rs:1414:26: wgpu error: Validation Error Caused by: In Device::create_render_pipeline, label = ‘egui_pipeline’ In the provided shader, the type given for group 1 binding 2 has a size of 4. Since the device does not support DownlevelFlags::BUFFER_BINDINGS_NOT_16_BYTE_ALIGNED`, the type must have a size that is a multiple of 16 bytes
```
One reference I used was the following Pull Request:
The biggest change I made was in `egui-wgpu` to
`r_tex_nearest_filtering`
Shader (`egui.wgsl`)
```wgsl
// Old
@group(1) @binding(2) var r_tex_nearest_filtering: u32;
// New
@group(1) @binding(2) var r_tex_nearest_filtering: vec4;
```
And with the corresponding changes in `renderer.rs`, I ran it locally
and it worked perfectly in Firefox 149 and Chromium 151 (Both fail when
attempting to access https://www.egui.rs/#demo, returning the error
described above)
* Closes
* Fixes
* [X] I have followed the instructions in the PR template
---------
Co-authored-by: Emil Ernerfeldt
Co-authored-by: Claude Fable 5
---
crates/egui-wgpu/src/egui.wgsl | 8 +++++---
crates/egui-wgpu/src/renderer.rs | 6 ++++--
2 files changed, 9 insertions(+), 5 deletions(-)
diff --git a/crates/egui-wgpu/src/egui.wgsl b/crates/egui-wgpu/src/egui.wgsl
index bd3b5313e..9567b0d3e 100644
--- a/crates/egui-wgpu/src/egui.wgsl
+++ b/crates/egui-wgpu/src/egui.wgsl
@@ -98,6 +98,7 @@ fn vs_main(
@group(1) @binding(0) var r_tex_color: texture_2d;
@group(1) @binding(1) var r_tex_sampler: sampler;
+
/// Set in bit 0 of `r_tex_flags` if the sampler uses nearest filtering.
///
/// Must match `TEX_FLAG_NEAREST` in `renderer.rs`.
@@ -114,11 +115,11 @@ const WRAP_MODE_MIRRORED_REPEAT: u32 = 2u;
///
/// Bit 0: `TEX_FLAG_NEAREST`.
/// Bits 1+: one of the `WRAP_MODE_*` constants.
-@group(1) @binding(2) var r_tex_flags: u32;
+@group(1) @binding(2) var r_tex_flags: vec4;
/// Map a texel coordinate to a valid texel according to the texture's wrap mode.
fn wrap_texel_coord(coord: vec2, texture_size: vec2) -> vec2 {
- let wrap_mode = r_tex_flags >> 1u;
+ let wrap_mode = r_tex_flags[0] >> 1u;
if wrap_mode == WRAP_MODE_REPEAT {
return ((coord % texture_size) + texture_size) % texture_size;
} else if wrap_mode == WRAP_MODE_MIRRORED_REPEAT {
@@ -131,6 +132,7 @@ fn wrap_texel_coord(coord: vec2, texture_size: vec2) -> vec2 {
}
}
+
fn sample_texture(in: VertexOutput) -> vec4 {
if r_locals.predictable_texture_filtering == 0 {
// Hardware filtering: fast, but varies across GPUs and drivers.
@@ -139,7 +141,7 @@ fn sample_texture(in: VertexOutput) -> vec4 {
let texture_size = vec2(textureDimensions(r_tex_color, 0));
let texture_size_f = vec2(texture_size);
- if (r_tex_flags & TEX_FLAG_NEAREST) != 0u {
+ if (r_tex_flags[0] & TEX_FLAG_NEAREST) != 0u {
// Nearest filtering: load the texel under the sample position.
let texel = wrap_texel_coord(vec2(floor(in.tex_coord * texture_size_f)), texture_size);
return textureLoad(r_tex_color, texel, 0);
diff --git a/crates/egui-wgpu/src/renderer.rs b/crates/egui-wgpu/src/renderer.rs
index cb9f40f18..d5130d5c0 100644
--- a/crates/egui-wgpu/src/renderer.rs
+++ b/crates/egui-wgpu/src/renderer.rs
@@ -357,7 +357,9 @@ impl Renderer {
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
has_dynamic_offset: false,
- min_binding_size: NonZeroU64::new(core::mem::size_of::() as _),
+ min_binding_size: NonZeroU64::new(
+ (core::mem::size_of::() * 4) as _,
+ ),
ty: wgpu::BufferBindingType::Uniform,
},
count: None,
@@ -370,7 +372,7 @@ impl Renderer {
let flag = flag as u32;
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(&format!("egui_texture_flags_{flag}")),
- contents: bytemuck::bytes_of(&flag),
+ contents: bytemuck::bytes_of(&[flag, 0, 0, 0]),
usage: wgpu::BufferUsages::UNIFORM,
})
});
From 5d3e958ecfd3468a460c57094ebaeca6e3c4f325 Mon Sep 17 00:00:00 2001
From: limo520
Date: Thu, 27 Aug 2026 09:14:01 +0800
Subject: [PATCH 15/16] Fix the constrain option not working in the window
options demo (#8433)
`constrain_to` overrides `constrain`, so I adjusted it to call
`constrain_to` before `constrain`.
* [x] I have followed the instructions in the PR template
Signed-off-by: limo520 <247564335@qq.com>
---
crates/egui_demo_lib/src/demo/window_options.rs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/crates/egui_demo_lib/src/demo/window_options.rs b/crates/egui_demo_lib/src/demo/window_options.rs
index d95faa546..6a45778e9 100644
--- a/crates/egui_demo_lib/src/demo/window_options.rs
+++ b/crates/egui_demo_lib/src/demo/window_options.rs
@@ -71,13 +71,13 @@ impl crate::Demo for WindowOptions {
let mut window = egui::Window::new(title)
.id(egui::Id::new("demo_window_options")) // required since we change the title
.resizable(resizable)
+ .constrain_to(ui.available_rect_before_wrap())
.constrain(constrain)
.collapsible(collapsible)
.movable(movable)
.title_bar(title_bar)
.drag_area(drag_area)
.scroll(scroll2)
- .constrain_to(ui.available_rect_before_wrap())
.enabled(enabled);
if closable {
window = window.open(open);
From 7b34fc0e03669946315cc90a73ad7c870e713697 Mon Sep 17 00:00:00 2001
From: Emil Ernerfeldt
Date: Mon, 31 Aug 2026 11:01:10 +0200
Subject: [PATCH 16/16] Point Rust Analyzer to `target/rust-analyzer` (#8463)
---
.gitignore | 2 ++
.vscode/settings.json | 4 ++--
scripts/lint.py | 11 ++++++-----
3 files changed, 10 insertions(+), 7 deletions(-)
diff --git a/.gitignore b/.gitignore
index 588826e7d..86507503b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,6 @@
.DS_Store
+# `target_ra` and `target_wasm` are legacy locations; they now live inside `target/`.
+# Keep them on their own lines: a trailing `#` is not a comment in .gitignore.
**/target
**/target_ra
**/target_wasm
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 21d4bb4b0..113ad5092 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -8,7 +8,7 @@
}
},
"files.exclude": {
- "target_ra/**": true,
+ "target_ra/**": true, // legacy Rust Analyzer target dir
"target_wasm/**": true,
"target/**": true,
},
@@ -18,7 +18,7 @@
"rust-analyzer.cargo.allTargets": true,
"rust-analyzer.cargo.features": "all",
// Use a separate target directory for Rust Analyzer so it doesn't prevent cargo/clippy from doing things.
- "rust-analyzer.cargo.targetDir": "target_ra",
+ "rust-analyzer.cargo.targetDir": true,
"rust-analyzer.showUnlinkedFileNotification": false,
// Uncomment the following options and restart rust-analyzer to get it to check code behind `cfg(target_arch=wasm32)`.
// Don't forget to put it in a comment again before committing.
diff --git a/scripts/lint.py b/scripts/lint.py
index 0c1319e8a..1d26e5ca1 100755
--- a/scripts/lint.py
+++ b/scripts/lint.py
@@ -66,11 +66,8 @@ def lint_lines(filepath, lines_in):
)
lines_out.append("#[inline]")
-
if re.search(r"TODO[^(]", line):
- errors.append(
- f"{filepath}:{line_nr}: write 'TODO(username):' instead"
- )
+ errors.append(f"{filepath}:{line_nr}: write 'TODO(username):' instead")
if re.search(r"\.zip\(", line):
errors.append(
@@ -177,7 +174,11 @@ def main():
root_dirpath = os.path.abspath(f"{script_dirpath}/..")
os.chdir(root_dirpath)
- exclude = set(["target", "target_ra", "target_wasm"])
+ exclude = {
+ "target",
+ "target_ra", # legacy Rust Analyzer target dir
+ "target_wasm",
+ }
for root, dirs, files in os.walk(".", topdown=True):
dirs[:] = [d for d in dirs if d not in exclude]
for filename in files: