1use 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
15const N: u32 = 6 * 6;
19
20fn background_checkers(painter: &Painter, rect: Rect) {
21 let rect = rect.shrink(0.5); 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
49pub 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
62pub 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 background_checkers(painter, rect);
69
70 if color == Color32::TRANSPARENT {
71 } 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); ui.painter()
98 .rect_stroke(rect, rounding, (2.0, visuals.bg_fill)); }
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); {
120 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); {
139 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), pos2(x + r, rect.bottom()), pos2(x - r, rect.bottom()), ],
149 picked_color,
150 Stroke::new(visuals.fg_stroke.width, contrast_color(picked_color)),
151 ));
152 }
153 }
154
155 response
156}
157
158fn 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)); ui.painter().rect_stroke(rect, 0.0, visuals.bg_stroke); 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
219fn is_additive_alpha(a: f32) -> bool {
223 a < 0.0
224}
225
226#[derive(Clone, Copy, PartialEq, Eq)]
228pub enum Alpha {
229 Opaque,
231
232 OnlyBlend,
234
235 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 } 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 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 hsvag.a = alpha;
264 } else {
265 *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 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 hsvag.a = alpha;
286 } else {
287 *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; }
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
372fn 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
404fn 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
447pub 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
464pub 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 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
515pub 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
525pub 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
537pub 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
547pub 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
558fn 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
564fn 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
570fn 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}