1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-29 04:40:03 -04:00

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:

<img width="286" height="397" alt="Screenshot 2026-08-21 at 15 08 18"
src="https://github.com/user-attachments/assets/3a9d5289-71fb-4f34-8a20-1fe96891ba36"
/>


But you can [increase it](https://github.com/emilk/egui/pull/8445):


<img width="285" height="391" alt="Screenshot 2026-08-21 at 15 08 41"
src="https://github.com/user-attachments/assets/b0dfa7da-5b96-40e0-a80d-1da385d57a1a"
/>


### Before
for reference
<img width="286" height="392" alt="Screenshot 2026-08-21 at 19 08 27"
src="https://github.com/user-attachments/assets/7c754f36-795c-4381-9f9f-1fc8dd9a0264"
/>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Emil Ernerfeldt
2026-08-24 02:28:14 -07:00
committed by GitHub
parent a15a7e6611
commit 726b995608
24 changed files with 411 additions and 155 deletions

View File

@@ -98,9 +98,38 @@ fn vs_main(
@group(1) @binding(0) var r_tex_color: texture_2d<f32>;
@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<uniform> 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<uniform> 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<i32>, texture_size: vec2<i32>) -> vec2<i32> {
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<i32>(1, 1) - phase);
} else {
// WRAP_MODE_CLAMP_TO_EDGE
return clamp(coord, vec2<i32>(0, 0), texture_size - vec2<i32>(1, 1));
}
}
fn sample_texture(in: VertexOutput) -> vec4<f32> {
if r_locals.predictable_texture_filtering == 0 {
@@ -109,11 +138,10 @@ fn sample_texture(in: VertexOutput) -> vec4<f32> {
} else {
let texture_size = vec2<i32>(textureDimensions(r_tex_color, 0));
let texture_size_f = vec2<f32>(texture_size);
let max_coord = texture_size - vec2<i32>(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<i32>(in.tex_coord * texture_size_f), vec2<i32>(0, 0), max_coord);
let texel = wrap_texel_coord(vec2<i32>(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<f32> {
let pixel_fract = fract(pixel_coord);
let pixel_floor = vec2<i32>(floor(pixel_coord));
// Manual texture clamping
let p00 = clamp(pixel_floor + vec2<i32>(0, 0), vec2<i32>(0, 0), max_coord);
let p10 = clamp(pixel_floor + vec2<i32>(1, 0), vec2<i32>(0, 0), max_coord);
let p01 = clamp(pixel_floor + vec2<i32>(0, 1), vec2<i32>(0, 0), max_coord);
let p11 = clamp(pixel_floor + vec2<i32>(1, 1), vec2<i32>(0, 0), max_coord);
let p00 = wrap_texel_coord(pixel_floor + vec2<i32>(0, 0), texture_size);
let p10 = wrap_texel_coord(pixel_floor + vec2<i32>(1, 0), texture_size);
let p01 = wrap_texel_coord(pixel_floor + vec2<i32>(0, 1), texture_size);
let p11 = wrap_texel_coord(pixel_floor + vec2<i32>(1, 1), texture_size);
// Load at pixel centers
let tl = textureLoad(r_tex_color, p00, 0);

View File

@@ -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,

View File

@@ -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<String>,
id: Id,
image: impl Into<ImageData>,
options: TextureOptions,
) -> TextureHandle {
profiling::function_scope!();
use crate::cache::FramePublisher;
type TextureCache = FramePublisher<Id, (ImageData, TextureOptions, TextureHandle)>;
let image = image.into();
let cached: Option<(ImageData, TextureOptions, TextureHandle)> =
self.memory_mut(|mem| mem.caches.cache::<TextureCache>().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::<TextureCache>()
.set(id, (image, options, handle.clone()));
});
handle
}
/// Low-level texture manager.
///
/// In general it is easier to use [`Self::load_texture`] and [`TextureHandle`].

View File

@@ -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<Rgba>) -> Color32 {
@@ -19,38 +21,63 @@ fn contrast_color(color: impl Into<Rgba>) -> 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);
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;
// 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));
}
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,28 +192,24 @@ 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);
// 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().add(Shape::mesh(mesh));
}
ui.painter()
.rect_stroke(rect, 0.0, visuals.bg_stroke, StrokeKind::Inside); // outline
{
// Show where the slider is at:
@@ -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 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);
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);
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);

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8f93b6872fb1df5841afcc61a840514bd391f436274136e14725364e33f1e4b2
size 101079
oid sha256:24fc466f6470761a5064a590929fa3a0d95741ed691fc257415eee349bc528e9
size 101394

View File

@@ -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");
}

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ed0f9f3ec94ac3d75266343d11cae5842d72fbea3c78f7995b1b0ad5aa3cdcad
size 141933

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3b6f03016682b8a9970d2f425cfd417833b8e532c69bbcfbf8290ccaa8335f3b
size 25938
oid sha256:68351659df756ce331a4eeb147be8d1b4fed39dbbb54a0d6f685997b54dc07a7
size 26327

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:65d1c8766fa8f854806a408a0289a79e942fdf6642be7cf6f4e8924412d31c25
size 30855
oid sha256:65d1d15d78c871eb87224c44d2fe889cc0d7a33eb15aeb4852c10bdb02aa730d
size 30877

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6402e1da2627f143d07e01b1bcbcbfafa174fcf76fd4c1f8b217b5227528a067
size 590792
oid sha256:10d58fa20fe3cc06a98dc79adf6237652b87871d2dc394476ea524ac62f2750b
size 590735

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:eddc77778c52b56763e1906fe9fdc4e575d4c99f3a438dc9f74260f75f13154f
size 745930
oid sha256:1de8583bef3fb9e3530b4a09f498360d37d21c099dd44cd45ee99ccc332d7c1c
size 745855

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bfc46024827dd504be4765a3eab02bb1c27cb870d128285580fa1eb0cff8390c
size 973977
oid sha256:43c1f297b0d3e62483b05efe3eb7089a61944b2e348bc791f0503ccbf6c5ab4a
size 973927

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9658cc6b4dd2dd33356facc6b48cf69f9a86f976ac2dd4f16e362a2fd3f73827
size 1081864
oid sha256:17790adf9f5ba91aa00a22c44303422ed02e71a1ab7b5c1484e13e2a132bd639
size 1081792

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e9918502356af39490dc8056509117dee7b7a290c1cac06767d3478c38bb27d3
size 1131255
oid sha256:b434961f2ac616e2563aa2ff9e706158190dfcc2713de6fd7740290e603c8a1b
size 1131176

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:95cd6e583873e36399df82133b8eca8b3be34ea8d5c6d36cb653288c8cf3b53f
size 1363872
oid sha256:de79f0a3dacdf5387faaeeb64f8939ff5912e362d8d649d6e25fd3dbda940325
size 1363775

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a7028aa93b6212054dc41d3daf88454d53e6b05d670b987f2d918f6aa0b38324
size 46455
oid sha256:a62ed81b1693cd7eea2099c0840d82318d27e382f88ca1dd15ba9e2a59e3cf4b
size 46677

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:603afb5653a960161a6884647bc51e8547160ffab186d7a9a2b4839ed77e7d0b
size 120323
oid sha256:5ad40e8b41882e3456569c8cb2e831d33fafa910989674e05b20568798528154
size 120579

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:26c857faec0666f207182166d34bc9980f1cf3165813a9128cd24d1429156933
size 37357
oid sha256:ea03f6b0e0071910cf2b4279549fb0347cd6a9f8d0334830ce4f34f5824a1ef9
size 37595

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:46af578e5e90d0591644707b9ad305615ee098182ffa14c6ccf29bdbbc2a0bef
size 65919
oid sha256:952df0869c400d8b1f5dc0a33cb7a8be1fa30d91929e57b7c33af0cfbc9cead4
size 66043

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:71433ceac92b327d2c5109302c479d398b64283a75327bba6d584654a0c53f3e
size 153251
oid sha256:066067e34d47aa8d2631e64b2ea37d94bae77bdfb1af9337f34f724e1ff14cb6
size 153589

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:196d957732cc5a0d0b8b30875b5ade59236c288293e2cc8a53f9ed6c89edad79
size 60722
oid sha256:0d79861c18cbaf2f2ab738d0a5633f354c11a52b8a8e87c623e624ecdfcc6965
size 60813

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8d26c20506fffcbca07c3eb0a986357129590684da83f8cdd1a56c65cf3e6452
size 147925
oid sha256:cde27ba5614f4bd5553d609b23e6dd78aea113c88443de42fa47aa85929c9455
size 148235

View File

@@ -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<f32> 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<f32> 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<f32> 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));
}
}

View File

@@ -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.