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:
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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`].
|
||||
|
||||
@@ -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);
|
||||
// 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,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);
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8f93b6872fb1df5841afcc61a840514bd391f436274136e14725364e33f1e4b2
|
||||
size 101079
|
||||
oid sha256:24fc466f6470761a5064a590929fa3a0d95741ed691fc257415eee349bc528e9
|
||||
size 101394
|
||||
|
||||
17
crates/egui_demo_lib/tests/color_picker.rs
Normal file
17
crates/egui_demo_lib/tests/color_picker.rs
Normal 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");
|
||||
}
|
||||
3
crates/egui_demo_lib/tests/snapshots/color_picker.png
Normal file
3
crates/egui_demo_lib/tests/snapshots/color_picker.png
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ed0f9f3ec94ac3d75266343d11cae5842d72fbea3c78f7995b1b0ad5aa3cdcad
|
||||
size 141933
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3b6f03016682b8a9970d2f425cfd417833b8e532c69bbcfbf8290ccaa8335f3b
|
||||
size 25938
|
||||
oid sha256:68351659df756ce331a4eeb147be8d1b4fed39dbbb54a0d6f685997b54dc07a7
|
||||
size 26327
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:65d1c8766fa8f854806a408a0289a79e942fdf6642be7cf6f4e8924412d31c25
|
||||
size 30855
|
||||
oid sha256:65d1d15d78c871eb87224c44d2fe889cc0d7a33eb15aeb4852c10bdb02aa730d
|
||||
size 30877
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6402e1da2627f143d07e01b1bcbcbfafa174fcf76fd4c1f8b217b5227528a067
|
||||
size 590792
|
||||
oid sha256:10d58fa20fe3cc06a98dc79adf6237652b87871d2dc394476ea524ac62f2750b
|
||||
size 590735
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:eddc77778c52b56763e1906fe9fdc4e575d4c99f3a438dc9f74260f75f13154f
|
||||
size 745930
|
||||
oid sha256:1de8583bef3fb9e3530b4a09f498360d37d21c099dd44cd45ee99ccc332d7c1c
|
||||
size 745855
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bfc46024827dd504be4765a3eab02bb1c27cb870d128285580fa1eb0cff8390c
|
||||
size 973977
|
||||
oid sha256:43c1f297b0d3e62483b05efe3eb7089a61944b2e348bc791f0503ccbf6c5ab4a
|
||||
size 973927
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9658cc6b4dd2dd33356facc6b48cf69f9a86f976ac2dd4f16e362a2fd3f73827
|
||||
size 1081864
|
||||
oid sha256:17790adf9f5ba91aa00a22c44303422ed02e71a1ab7b5c1484e13e2a132bd639
|
||||
size 1081792
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e9918502356af39490dc8056509117dee7b7a290c1cac06767d3478c38bb27d3
|
||||
size 1131255
|
||||
oid sha256:b434961f2ac616e2563aa2ff9e706158190dfcc2713de6fd7740290e603c8a1b
|
||||
size 1131176
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:95cd6e583873e36399df82133b8eca8b3be34ea8d5c6d36cb653288c8cf3b53f
|
||||
size 1363872
|
||||
oid sha256:de79f0a3dacdf5387faaeeb64f8939ff5912e362d8d649d6e25fd3dbda940325
|
||||
size 1363775
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a7028aa93b6212054dc41d3daf88454d53e6b05d670b987f2d918f6aa0b38324
|
||||
size 46455
|
||||
oid sha256:a62ed81b1693cd7eea2099c0840d82318d27e382f88ca1dd15ba9e2a59e3cf4b
|
||||
size 46677
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:603afb5653a960161a6884647bc51e8547160ffab186d7a9a2b4839ed77e7d0b
|
||||
size 120323
|
||||
oid sha256:5ad40e8b41882e3456569c8cb2e831d33fafa910989674e05b20568798528154
|
||||
size 120579
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:26c857faec0666f207182166d34bc9980f1cf3165813a9128cd24d1429156933
|
||||
size 37357
|
||||
oid sha256:ea03f6b0e0071910cf2b4279549fb0347cd6a9f8d0334830ce4f34f5824a1ef9
|
||||
size 37595
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:46af578e5e90d0591644707b9ad305615ee098182ffa14c6ccf29bdbbc2a0bef
|
||||
size 65919
|
||||
oid sha256:952df0869c400d8b1f5dc0a33cb7a8be1fa30d91929e57b7c33af0cfbc9cead4
|
||||
size 66043
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:71433ceac92b327d2c5109302c479d398b64283a75327bba6d584654a0c53f3e
|
||||
size 153251
|
||||
oid sha256:066067e34d47aa8d2631e64b2ea37d94bae77bdfb1af9337f34f724e1ff14cb6
|
||||
size 153589
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:196d957732cc5a0d0b8b30875b5ade59236c288293e2cc8a53f9ed6c89edad79
|
||||
size 60722
|
||||
oid sha256:0d79861c18cbaf2f2ab738d0a5633f354c11a52b8a8e87c623e624ecdfcc6965
|
||||
size 60813
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8d26c20506fffcbca07c3eb0a986357129590684da83f8cdd1a56c65cf3e6452
|
||||
size 147925
|
||||
oid sha256:cde27ba5614f4bd5553d609b23e6dd78aea113c88443de42fa47aa85929c9455
|
||||
size 148235
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user