egui/widgets/
color_picker.rs

1//! Color picker widgets.
2
3use crate::util::fixed_cache::FixedCache;
4use crate::{
5    Context, DragValue, Id, Painter, Popup, PopupCloseBehavior, Response, Sense, Ui, Widget as _,
6    WidgetInfo, WidgetType, epaint, lerp, remap_clamp,
7};
8use epaint::{
9    Mesh, Rect, Shape, Stroke, StrokeKind, Vec2,
10    ecolor::{Color32, Hsva, HsvaGamma, Rgba},
11    pos2, vec2,
12};
13
14fn contrast_color(color: impl Into<Rgba>) -> Color32 {
15    if color.into().intensity() < 0.5 {
16        Color32::WHITE
17    } else {
18        Color32::BLACK
19    }
20}
21
22/// Number of vertices per dimension in the color sliders.
23/// We need at least 6 for hues, and more for smooth 2D areas.
24/// Should always be a multiple of 6 to hit the peak hues in HSV/HSL (every 60°).
25const N: u32 = 6 * 6;
26
27fn background_checkers(painter: &Painter, rect: Rect) {
28    let rect = rect.shrink(0.5); // Small hack to avoid the checkers from peeking through the sides
29    if !rect.is_positive() {
30        return;
31    }
32
33    let dark_color = Color32::from_gray(32);
34    let bright_color = Color32::from_gray(128);
35
36    let checker_size = Vec2::splat(rect.height() / 2.0);
37    let n = (rect.width() / checker_size.x).round() as u32;
38
39    let mut mesh = Mesh::default();
40    mesh.add_colored_rect(rect, dark_color);
41
42    let mut top = true;
43    for i in 0..n {
44        let x = lerp(rect.left()..=rect.right(), i as f32 / (n as f32));
45        let small_rect = if top {
46            Rect::from_min_size(pos2(x, rect.top()), checker_size)
47        } else {
48            Rect::from_min_size(pos2(x, rect.center().y), checker_size)
49        };
50        mesh.add_colored_rect(small_rect, bright_color);
51        top = !top;
52    }
53    painter.add(Shape::mesh(mesh));
54}
55
56/// Show a color with background checkers to demonstrate transparency (if any).
57pub fn show_color(ui: &mut Ui, color: impl Into<Color32>, desired_size: Vec2) -> Response {
58    show_color32(ui, color.into(), desired_size)
59}
60
61fn show_color32(ui: &mut Ui, color: Color32, desired_size: Vec2) -> Response {
62    let (rect, response) = ui.allocate_at_least(desired_size, Sense::hover());
63    if ui.is_rect_visible(rect) {
64        show_color_at(ui.painter(), color, rect);
65    }
66    response
67}
68
69/// Show a color with background checkers to demonstrate transparency (if any).
70pub fn show_color_at(painter: &Painter, color: Color32, rect: Rect) {
71    if color.is_opaque() {
72        painter.rect_filled(rect, 0.0, color);
73    } else {
74        // Transparent: how both the transparent and opaque versions of the color
75        background_checkers(painter, rect);
76
77        if color == Color32::TRANSPARENT {
78            // There is no opaque version, so just show the background checkers
79        } else {
80            let left = Rect::from_min_max(rect.left_top(), rect.center_bottom());
81            let right = Rect::from_min_max(rect.center_top(), rect.right_bottom());
82            painter.rect_filled(left, 0.0, color);
83            painter.rect_filled(right, 0.0, color.to_opaque());
84        }
85    }
86}
87
88fn color_button(ui: &mut Ui, color: Color32, open: bool) -> Response {
89    let size = ui.spacing().interact_size;
90    let (rect, response) = ui.allocate_exact_size(size, Sense::click());
91    response.widget_info(|| WidgetInfo::new(WidgetType::ColorButton));
92
93    if ui.is_rect_visible(rect) {
94        let visuals = if open {
95            &ui.visuals().widgets.open
96        } else {
97            ui.style().interact(&response)
98        };
99        let rect = rect.expand(visuals.expansion);
100
101        let stroke_width = 1.0;
102        show_color_at(ui.painter(), color, rect.shrink(stroke_width));
103
104        let corner_radius = visuals.corner_radius.at_most(2); // Can't do more rounding because the background grid doesn't do any rounding
105        ui.painter().rect_stroke(
106            rect,
107            corner_radius,
108            (stroke_width, visuals.bg_fill), // Using fill for stroke is intentional, because default style has no border
109            StrokeKind::Inside,
110        );
111    }
112
113    response
114}
115
116fn color_slider_1d(ui: &mut Ui, value: &mut f32, color_at: impl Fn(f32) -> Color32) -> Response {
117    #![allow(clippy::identity_op)]
118
119    let desired_size = vec2(ui.spacing().slider_width, ui.spacing().interact_size.y);
120    let (rect, response) = ui.allocate_at_least(desired_size, Sense::click_and_drag());
121
122    if let Some(mpos) = response.interact_pointer_pos() {
123        *value = remap_clamp(mpos.x, rect.left()..=rect.right(), 0.0..=1.0);
124    }
125
126    if ui.is_rect_visible(rect) {
127        let visuals = ui.style().interact(&response);
128
129        background_checkers(ui.painter(), rect); // for alpha:
130
131        {
132            // fill color:
133            let mut mesh = Mesh::default();
134            for i in 0..=N {
135                let t = i as f32 / (N as f32);
136                let color = color_at(t);
137                let x = lerp(rect.left()..=rect.right(), t);
138                mesh.colored_vertex(pos2(x, rect.top()), color);
139                mesh.colored_vertex(pos2(x, rect.bottom()), color);
140                if i < N {
141                    mesh.add_triangle(2 * i + 0, 2 * i + 1, 2 * i + 2);
142                    mesh.add_triangle(2 * i + 1, 2 * i + 2, 2 * i + 3);
143                }
144            }
145            ui.painter().add(Shape::mesh(mesh));
146        }
147
148        ui.painter()
149            .rect_stroke(rect, 0.0, visuals.bg_stroke, StrokeKind::Inside); // outline
150
151        {
152            // Show where the slider is at:
153            let x = lerp(rect.left()..=rect.right(), *value);
154            let r = rect.height() / 4.0;
155            let picked_color = color_at(*value);
156            ui.painter().add(Shape::convex_polygon(
157                vec![
158                    pos2(x, rect.center().y),   // tip
159                    pos2(x + r, rect.bottom()), // right bottom
160                    pos2(x - r, rect.bottom()), // left bottom
161                ],
162                picked_color,
163                Stroke::new(visuals.fg_stroke.width, contrast_color(picked_color)),
164            ));
165        }
166    }
167
168    response
169}
170
171/// # Arguments
172/// * `x_value` - X axis, either saturation or value (0.0-1.0).
173/// * `y_value` - Y axis, either saturation or value (0.0-1.0).
174/// * `color_at` - A function that dictates how the mix of saturation and value will be displayed in the 2d slider.
175///
176/// e.g.: `|x_value, y_value| HsvaGamma { h: 1.0, s: x_value, v: y_value, a: 1.0 }.into()` displays the colors as follows:
177/// * top-left: white `[s: 0.0, v: 1.0]`
178/// * top-right: fully saturated color `[s: 1.0, v: 1.0]`
179/// * bottom-right: black `[s: 0.0, v: 1.0].`
180fn color_slider_2d(
181    ui: &mut Ui,
182    x_value: &mut f32,
183    y_value: &mut f32,
184    color_at: impl Fn(f32, f32) -> Color32,
185) -> Response {
186    let desired_size = Vec2::splat(ui.spacing().slider_width);
187    let (rect, response) = ui.allocate_at_least(desired_size, Sense::click_and_drag());
188
189    if let Some(mpos) = response.interact_pointer_pos() {
190        *x_value = remap_clamp(mpos.x, rect.left()..=rect.right(), 0.0..=1.0);
191        *y_value = remap_clamp(mpos.y, rect.bottom()..=rect.top(), 0.0..=1.0);
192    }
193
194    if ui.is_rect_visible(rect) {
195        let visuals = ui.style().interact(&response);
196        let mut mesh = Mesh::default();
197
198        for xi in 0..=N {
199            for yi in 0..=N {
200                let xt = xi as f32 / (N as f32);
201                let yt = yi as f32 / (N as f32);
202                let color = color_at(xt, yt);
203                let x = lerp(rect.left()..=rect.right(), xt);
204                let y = lerp(rect.bottom()..=rect.top(), yt);
205                mesh.colored_vertex(pos2(x, y), color);
206
207                if xi < N && yi < N {
208                    let x_offset = 1;
209                    let y_offset = N + 1;
210                    let tl = yi * y_offset + xi;
211                    mesh.add_triangle(tl, tl + x_offset, tl + y_offset);
212                    mesh.add_triangle(tl + x_offset, tl + y_offset, tl + y_offset + x_offset);
213                }
214            }
215        }
216        ui.painter().add(Shape::mesh(mesh)); // fill
217
218        ui.painter()
219            .rect_stroke(rect, 0.0, visuals.bg_stroke, StrokeKind::Inside); // outline
220
221        // Show where the slider is at:
222        let x = lerp(rect.left()..=rect.right(), *x_value);
223        let y = lerp(rect.bottom()..=rect.top(), *y_value);
224        let picked_color = color_at(*x_value, *y_value);
225        ui.painter().add(epaint::CircleShape {
226            center: pos2(x, y),
227            radius: rect.width() / 12.0,
228            fill: picked_color,
229            stroke: Stroke::new(visuals.fg_stroke.width, contrast_color(picked_color)),
230        });
231    }
232
233    response
234}
235
236/// We use a negative alpha for additive colors within this file (a bit ironic).
237///
238/// We use alpha=0 to mean "transparent".
239fn is_additive_alpha(a: f32) -> bool {
240    a < 0.0
241}
242
243/// What options to show for alpha
244#[derive(Clone, Copy, PartialEq, Eq)]
245pub enum Alpha {
246    /// Set alpha to 1.0, and show no option for it.
247    Opaque,
248
249    /// Only show normal blend options for alpha.
250    OnlyBlend,
251
252    /// Show both blend and additive options.
253    BlendOrAdditive,
254}
255
256fn color_picker_hsvag_2d(ui: &mut Ui, hsvag: &mut HsvaGamma, alpha: Alpha) {
257    use crate::style::NumericColorSpace;
258
259    let alpha_control = if is_additive_alpha(hsvag.a) {
260        Alpha::Opaque // no alpha control for additive colors
261    } else {
262        alpha
263    };
264
265    match ui.style().visuals.numeric_color_space {
266        NumericColorSpace::GammaByte => {
267            let mut srgba_unmultiplied = Hsva::from(*hsvag).to_srgba_unmultiplied();
268            // Only update if changed to avoid rounding issues.
269            if srgba_edit_ui(ui, &mut srgba_unmultiplied, alpha_control) {
270                if is_additive_alpha(hsvag.a) {
271                    let alpha = hsvag.a;
272
273                    *hsvag = HsvaGamma::from(Hsva::from_additive_srgb([
274                        srgba_unmultiplied[0],
275                        srgba_unmultiplied[1],
276                        srgba_unmultiplied[2],
277                    ]));
278
279                    // Don't edit the alpha:
280                    hsvag.a = alpha;
281                } else {
282                    // Normal blending.
283                    *hsvag = HsvaGamma::from(Hsva::from_srgba_unmultiplied(srgba_unmultiplied));
284                }
285            }
286        }
287
288        NumericColorSpace::Linear => {
289            let mut rgba_unmultiplied = Hsva::from(*hsvag).to_rgba_unmultiplied();
290            // Only update if changed to avoid rounding issues.
291            if rgba_edit_ui(ui, &mut rgba_unmultiplied, alpha_control) {
292                if is_additive_alpha(hsvag.a) {
293                    let alpha = hsvag.a;
294
295                    *hsvag = HsvaGamma::from(Hsva::from_rgb([
296                        rgba_unmultiplied[0],
297                        rgba_unmultiplied[1],
298                        rgba_unmultiplied[2],
299                    ]));
300
301                    // Don't edit the alpha:
302                    hsvag.a = alpha;
303                } else {
304                    // Normal blending.
305                    *hsvag = HsvaGamma::from(Hsva::from_rgba_unmultiplied(
306                        rgba_unmultiplied[0],
307                        rgba_unmultiplied[1],
308                        rgba_unmultiplied[2],
309                        rgba_unmultiplied[3],
310                    ));
311                }
312            }
313        }
314    }
315
316    let current_color_size = vec2(ui.spacing().slider_width, ui.spacing().interact_size.y);
317    show_color(ui, *hsvag, current_color_size).on_hover_text("Selected color");
318
319    if alpha == Alpha::BlendOrAdditive {
320        let a = &mut hsvag.a;
321        let mut additive = is_additive_alpha(*a);
322        ui.horizontal(|ui| {
323            ui.label("Blending:");
324            ui.radio_value(&mut additive, false, "Normal");
325            ui.radio_value(&mut additive, true, "Additive");
326
327            if additive {
328                *a = -a.abs();
329            }
330
331            if !additive {
332                *a = a.abs();
333            }
334        });
335    }
336
337    let opaque = HsvaGamma { a: 1.0, ..*hsvag };
338
339    let HsvaGamma { h, s, v, a: _ } = hsvag;
340
341    if false {
342        color_slider_1d(ui, s, |s| HsvaGamma { s, ..opaque }.into()).on_hover_text("Saturation");
343    }
344
345    if false {
346        color_slider_1d(ui, v, |v| HsvaGamma { v, ..opaque }.into()).on_hover_text("Value");
347    }
348
349    color_slider_2d(ui, s, v, |s, v| HsvaGamma { s, v, ..opaque }.into());
350
351    color_slider_1d(ui, h, |h| {
352        HsvaGamma {
353            h,
354            s: 1.0,
355            v: 1.0,
356            a: 1.0,
357        }
358        .into()
359    })
360    .on_hover_text("Hue");
361
362    let additive = is_additive_alpha(hsvag.a);
363
364    if alpha == Alpha::Opaque {
365        hsvag.a = 1.0;
366    } else {
367        let a = &mut hsvag.a;
368
369        if alpha == Alpha::OnlyBlend {
370            if is_additive_alpha(*a) {
371                *a = 0.5; // was additive, but isn't allowed to be
372            }
373            color_slider_1d(ui, a, |a| HsvaGamma { a, ..opaque }.into()).on_hover_text("Alpha");
374        } else if !additive {
375            color_slider_1d(ui, a, |a| HsvaGamma { a, ..opaque }.into()).on_hover_text("Alpha");
376        }
377    }
378}
379
380fn input_type_button_ui(ui: &mut Ui) {
381    let mut input_type = ui.ctx().style().visuals.numeric_color_space;
382    if input_type.toggle_button_ui(ui).changed() {
383        ui.ctx().all_styles_mut(|s| {
384            s.visuals.numeric_color_space = input_type;
385        });
386    }
387}
388
389/// Shows 4 `DragValue` widgets to be used to edit the RGBA u8 values.
390/// Alpha's `DragValue` is hidden when `Alpha::Opaque`.
391///
392/// Returns `true` on change.
393fn srgba_edit_ui(ui: &mut Ui, [r, g, b, a]: &mut [u8; 4], alpha: Alpha) -> bool {
394    let mut edited = false;
395
396    ui.horizontal(|ui| {
397        input_type_button_ui(ui);
398
399        if ui
400            .button("📋")
401            .on_hover_text("Click to copy color values")
402            .clicked()
403        {
404            if alpha == Alpha::Opaque {
405                ui.ctx().copy_text(format!("{r}, {g}, {b}"));
406            } else {
407                ui.ctx().copy_text(format!("{r}, {g}, {b}, {a}"));
408            }
409        }
410        edited |= DragValue::new(r).speed(0.5).prefix("R ").ui(ui).changed();
411        edited |= DragValue::new(g).speed(0.5).prefix("G ").ui(ui).changed();
412        edited |= DragValue::new(b).speed(0.5).prefix("B ").ui(ui).changed();
413        if alpha != Alpha::Opaque {
414            edited |= DragValue::new(a).speed(0.5).prefix("A ").ui(ui).changed();
415        }
416    });
417
418    edited
419}
420
421/// Shows 4 `DragValue` widgets to be used to edit the RGBA f32 values.
422/// Alpha's `DragValue` is hidden when `Alpha::Opaque`.
423///
424/// Returns `true` on change.
425fn rgba_edit_ui(ui: &mut Ui, [r, g, b, a]: &mut [f32; 4], alpha: Alpha) -> bool {
426    fn drag_value(ui: &mut Ui, prefix: &str, value: &mut f32) -> Response {
427        DragValue::new(value)
428            .speed(0.003)
429            .prefix(prefix)
430            .range(0.0..=1.0)
431            .custom_formatter(|n, _| format!("{n:.03}"))
432            .ui(ui)
433    }
434
435    let mut edited = false;
436
437    ui.horizontal(|ui| {
438        input_type_button_ui(ui);
439
440        if ui
441            .button("📋")
442            .on_hover_text("Click to copy color values")
443            .clicked()
444        {
445            if alpha == Alpha::Opaque {
446                ui.ctx().copy_text(format!("{r:.03}, {g:.03}, {b:.03}"));
447            } else {
448                ui.ctx()
449                    .copy_text(format!("{r:.03}, {g:.03}, {b:.03}, {a:.03}"));
450            }
451        }
452
453        edited |= drag_value(ui, "R ", r).changed();
454        edited |= drag_value(ui, "G ", g).changed();
455        edited |= drag_value(ui, "B ", b).changed();
456        if alpha != Alpha::Opaque {
457            edited |= drag_value(ui, "A ", a).changed();
458        }
459    });
460
461    edited
462}
463
464/// Shows a color picker where the user can change the given [`Hsva`] color.
465///
466/// Returns `true` on change.
467pub fn color_picker_hsva_2d(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> bool {
468    let mut hsvag = HsvaGamma::from(*hsva);
469    ui.vertical(|ui| {
470        color_picker_hsvag_2d(ui, &mut hsvag, alpha);
471    });
472    let new_hasva = Hsva::from(hsvag);
473    if *hsva == new_hasva {
474        false
475    } else {
476        *hsva = new_hasva;
477        true
478    }
479}
480
481/// Shows a color picker where the user can change the given [`Color32`] color.
482///
483/// Returns `true` on change.
484pub fn color_picker_color32(ui: &mut Ui, srgba: &mut Color32, alpha: Alpha) -> bool {
485    let mut hsva = color_cache_get(ui.ctx(), *srgba);
486    let changed = color_picker_hsva_2d(ui, &mut hsva, alpha);
487    *srgba = Color32::from(hsva);
488    color_cache_set(ui.ctx(), *srgba, hsva);
489    changed
490}
491
492pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> Response {
493    let popup_id = ui.auto_id_with("popup");
494    let open = Popup::is_id_open(ui.ctx(), popup_id);
495    let mut button_response = color_button(ui, (*hsva).into(), open);
496    if ui.style().explanation_tooltips {
497        button_response = button_response.on_hover_text("Click to edit color");
498    }
499
500    const COLOR_SLIDER_WIDTH: f32 = 275.0;
501
502    Popup::menu(&button_response)
503        .id(popup_id)
504        .close_behavior(PopupCloseBehavior::CloseOnClickOutside)
505        .show(|ui| {
506            ui.spacing_mut().slider_width = COLOR_SLIDER_WIDTH;
507            if color_picker_hsva_2d(ui, hsva, alpha) {
508                button_response.mark_changed();
509            }
510        });
511
512    button_response
513}
514
515/// Shows a button with the given color.
516/// If the user clicks the button, a full color picker is shown.
517pub fn color_edit_button_srgba(ui: &mut Ui, srgba: &mut Color32, alpha: Alpha) -> Response {
518    let mut hsva = color_cache_get(ui.ctx(), *srgba);
519    let response = color_edit_button_hsva(ui, &mut hsva, alpha);
520    *srgba = Color32::from(hsva);
521    color_cache_set(ui.ctx(), *srgba, hsva);
522    response
523}
524
525/// Shows a button with the given color.
526/// If the user clicks the button, a full color picker is shown.
527/// The given color is in `sRGB` space.
528pub fn color_edit_button_srgb(ui: &mut Ui, srgb: &mut [u8; 3]) -> Response {
529    let mut srgba = Color32::from_rgb(srgb[0], srgb[1], srgb[2]);
530    let response = color_edit_button_srgba(ui, &mut srgba, Alpha::Opaque);
531    srgb[0] = srgba[0];
532    srgb[1] = srgba[1];
533    srgb[2] = srgba[2];
534    response
535}
536
537/// Shows a button with the given color.
538/// If the user clicks the button, a full color picker is shown.
539pub fn color_edit_button_rgba(ui: &mut Ui, rgba: &mut Rgba, alpha: Alpha) -> Response {
540    let mut hsva = color_cache_get(ui.ctx(), *rgba);
541    let response = color_edit_button_hsva(ui, &mut hsva, alpha);
542    *rgba = Rgba::from(hsva);
543    color_cache_set(ui.ctx(), *rgba, hsva);
544    response
545}
546
547/// Shows a button with the given color.
548/// If the user clicks the button, a full color picker is shown.
549pub fn color_edit_button_rgb(ui: &mut Ui, rgb: &mut [f32; 3]) -> Response {
550    let mut rgba = Rgba::from_rgb(rgb[0], rgb[1], rgb[2]);
551    let response = color_edit_button_rgba(ui, &mut rgba, Alpha::Opaque);
552    rgb[0] = rgba[0];
553    rgb[1] = rgba[1];
554    rgb[2] = rgba[2];
555    response
556}
557
558// To ensure we keep hue slider when `srgba` is gray we store the full [`Hsva`] in a cache:
559fn color_cache_get(ctx: &Context, rgba: impl Into<Rgba>) -> Hsva {
560    let rgba = rgba.into();
561    use_color_cache(ctx, |cc| cc.get(&rgba).copied()).unwrap_or_else(|| Hsva::from(rgba))
562}
563
564// To ensure we keep hue slider when `srgba` is gray we store the full [`Hsva`] in a cache:
565fn color_cache_set(ctx: &Context, rgba: impl Into<Rgba>, hsva: Hsva) {
566    let rgba = rgba.into();
567    use_color_cache(ctx, |cc| cc.set(rgba, hsva));
568}
569
570// To ensure we keep hue slider when `srgba` is gray we store the full [`Hsva`] in a cache:
571fn use_color_cache<R>(ctx: &Context, f: impl FnOnce(&mut FixedCache<Rgba, Hsva>) -> R) -> R {
572    ctx.data_mut(|d| f(d.get_temp_mut_or_default(Id::NULL)))
573}