egui/widgets/
color_picker.rs

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