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.