Skip to main content

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