1use 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
21const N: u32 = 6 * 6;
25
26fn background_checkers(painter: &Painter, rect: Rect) {
27 let rect = rect.shrink(0.5); 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
55pub 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
68pub 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 background_checkers(painter, rect);
75
76 if color == Color32::TRANSPARENT {
77 } 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); ui.painter().rect_stroke(
105 rect,
106 corner_radius,
107 (stroke_width, visuals.bg_fill), 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); {
131 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); {
151 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), pos2(x + r, rect.bottom()), pos2(x - r, rect.bottom()), ],
161 picked_color,
162 Stroke::new(visuals.fg_stroke.width, contrast_color(picked_color)),
163 ));
164 }
165 }
166
167 response
168}
169
170fn 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)); ui.painter()
218 .rect_stroke(rect, 0.0, visuals.bg_stroke, StrokeKind::Inside); 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
235fn is_additive_alpha(a: f32) -> bool {
239 a < 0.0
240}
241
242#[derive(Clone, Copy, PartialEq, Eq)]
244pub enum Alpha {
245 Opaque,
247
248 OnlyBlend,
250
251 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 } 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 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 hsvag.a = alpha;
280 } else {
281 *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 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 hsvag.a = alpha;
302 } else {
303 *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; }
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
388fn 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
420fn 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
463pub 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
480pub 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 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
531pub 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
541pub 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
553pub 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
563pub 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
574fn 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
580fn 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
586fn 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}