egui/widgets/
color_picker.rs

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