1use 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
22const N: u32 = 6 * 6;
26
27fn background_checkers(painter: &Painter, rect: Rect) {
28 let rect = rect.shrink(0.5); 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
56pub 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
69pub 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 background_checkers(painter, rect);
76
77 if color == Color32::TRANSPARENT {
78 } 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
88fn color_button(ui: &mut Ui, color: Color32, open: bool) -> Response {
89 let size = ui.spacing().interact_size;
90 let (rect, response) = ui.allocate_exact_size(size, Sense::click());
91 response.widget_info(|| WidgetInfo::new(WidgetType::ColorButton));
92
93 if ui.is_rect_visible(rect) {
94 let visuals = if open {
95 &ui.visuals().widgets.open
96 } else {
97 ui.style().interact(&response)
98 };
99 let rect = rect.expand(visuals.expansion);
100
101 let stroke_width = 1.0;
102 show_color_at(ui.painter(), color, rect.shrink(stroke_width));
103
104 let corner_radius = visuals.corner_radius.at_most(2); ui.painter().rect_stroke(
106 rect,
107 corner_radius,
108 (stroke_width, visuals.bg_fill), StrokeKind::Inside,
110 );
111 }
112
113 response
114}
115
116fn color_slider_1d(ui: &mut Ui, value: &mut f32, color_at: impl Fn(f32) -> Color32) -> Response {
117 #![allow(clippy::identity_op)]
118
119 let desired_size = vec2(ui.spacing().slider_width, ui.spacing().interact_size.y);
120 let (rect, response) = ui.allocate_at_least(desired_size, Sense::click_and_drag());
121
122 if let Some(mpos) = response.interact_pointer_pos() {
123 *value = remap_clamp(mpos.x, rect.left()..=rect.right(), 0.0..=1.0);
124 }
125
126 if ui.is_rect_visible(rect) {
127 let visuals = ui.style().interact(&response);
128
129 background_checkers(ui.painter(), rect); {
132 let mut mesh = Mesh::default();
134 for i in 0..=N {
135 let t = i as f32 / (N as f32);
136 let color = color_at(t);
137 let x = lerp(rect.left()..=rect.right(), t);
138 mesh.colored_vertex(pos2(x, rect.top()), color);
139 mesh.colored_vertex(pos2(x, rect.bottom()), color);
140 if i < N {
141 mesh.add_triangle(2 * i + 0, 2 * i + 1, 2 * i + 2);
142 mesh.add_triangle(2 * i + 1, 2 * i + 2, 2 * i + 3);
143 }
144 }
145 ui.painter().add(Shape::mesh(mesh));
146 }
147
148 ui.painter()
149 .rect_stroke(rect, 0.0, visuals.bg_stroke, StrokeKind::Inside); {
152 let x = lerp(rect.left()..=rect.right(), *value);
154 let r = rect.height() / 4.0;
155 let picked_color = color_at(*value);
156 ui.painter().add(Shape::convex_polygon(
157 vec![
158 pos2(x, rect.center().y), pos2(x + r, rect.bottom()), pos2(x - r, rect.bottom()), ],
162 picked_color,
163 Stroke::new(visuals.fg_stroke.width, contrast_color(picked_color)),
164 ));
165 }
166 }
167
168 response
169}
170
171fn color_slider_2d(
181 ui: &mut Ui,
182 x_value: &mut f32,
183 y_value: &mut f32,
184 color_at: impl Fn(f32, f32) -> Color32,
185) -> Response {
186 let desired_size = Vec2::splat(ui.spacing().slider_width);
187 let (rect, response) = ui.allocate_at_least(desired_size, Sense::click_and_drag());
188
189 if let Some(mpos) = response.interact_pointer_pos() {
190 *x_value = remap_clamp(mpos.x, rect.left()..=rect.right(), 0.0..=1.0);
191 *y_value = remap_clamp(mpos.y, rect.bottom()..=rect.top(), 0.0..=1.0);
192 }
193
194 if ui.is_rect_visible(rect) {
195 let visuals = ui.style().interact(&response);
196 let mut mesh = Mesh::default();
197
198 for xi in 0..=N {
199 for yi in 0..=N {
200 let xt = xi as f32 / (N as f32);
201 let yt = yi as f32 / (N as f32);
202 let color = color_at(xt, yt);
203 let x = lerp(rect.left()..=rect.right(), xt);
204 let y = lerp(rect.bottom()..=rect.top(), yt);
205 mesh.colored_vertex(pos2(x, y), color);
206
207 if xi < N && yi < N {
208 let x_offset = 1;
209 let y_offset = N + 1;
210 let tl = yi * y_offset + xi;
211 mesh.add_triangle(tl, tl + x_offset, tl + y_offset);
212 mesh.add_triangle(tl + x_offset, tl + y_offset, tl + y_offset + x_offset);
213 }
214 }
215 }
216 ui.painter().add(Shape::mesh(mesh)); ui.painter()
219 .rect_stroke(rect, 0.0, visuals.bg_stroke, StrokeKind::Inside); let x = lerp(rect.left()..=rect.right(), *x_value);
223 let y = lerp(rect.bottom()..=rect.top(), *y_value);
224 let picked_color = color_at(*x_value, *y_value);
225 ui.painter().add(epaint::CircleShape {
226 center: pos2(x, y),
227 radius: rect.width() / 12.0,
228 fill: picked_color,
229 stroke: Stroke::new(visuals.fg_stroke.width, contrast_color(picked_color)),
230 });
231 }
232
233 response
234}
235
236fn is_additive_alpha(a: f32) -> bool {
240 a < 0.0
241}
242
243#[derive(Clone, Copy, PartialEq, Eq)]
245pub enum Alpha {
246 Opaque,
248
249 OnlyBlend,
251
252 BlendOrAdditive,
254}
255
256fn color_picker_hsvag_2d(ui: &mut Ui, hsvag: &mut HsvaGamma, alpha: Alpha) {
257 use crate::style::NumericColorSpace;
258
259 let alpha_control = if is_additive_alpha(hsvag.a) {
260 Alpha::Opaque } else {
262 alpha
263 };
264
265 match ui.style().visuals.numeric_color_space {
266 NumericColorSpace::GammaByte => {
267 let mut srgba_unmultiplied = Hsva::from(*hsvag).to_srgba_unmultiplied();
268 if srgba_edit_ui(ui, &mut srgba_unmultiplied, alpha_control) {
270 if is_additive_alpha(hsvag.a) {
271 let alpha = hsvag.a;
272
273 *hsvag = HsvaGamma::from(Hsva::from_additive_srgb([
274 srgba_unmultiplied[0],
275 srgba_unmultiplied[1],
276 srgba_unmultiplied[2],
277 ]));
278
279 hsvag.a = alpha;
281 } else {
282 *hsvag = HsvaGamma::from(Hsva::from_srgba_unmultiplied(srgba_unmultiplied));
284 }
285 }
286 }
287
288 NumericColorSpace::Linear => {
289 let mut rgba_unmultiplied = Hsva::from(*hsvag).to_rgba_unmultiplied();
290 if rgba_edit_ui(ui, &mut rgba_unmultiplied, alpha_control) {
292 if is_additive_alpha(hsvag.a) {
293 let alpha = hsvag.a;
294
295 *hsvag = HsvaGamma::from(Hsva::from_rgb([
296 rgba_unmultiplied[0],
297 rgba_unmultiplied[1],
298 rgba_unmultiplied[2],
299 ]));
300
301 hsvag.a = alpha;
303 } else {
304 *hsvag = HsvaGamma::from(Hsva::from_rgba_unmultiplied(
306 rgba_unmultiplied[0],
307 rgba_unmultiplied[1],
308 rgba_unmultiplied[2],
309 rgba_unmultiplied[3],
310 ));
311 }
312 }
313 }
314 }
315
316 let current_color_size = vec2(ui.spacing().slider_width, ui.spacing().interact_size.y);
317 show_color(ui, *hsvag, current_color_size).on_hover_text("Selected color");
318
319 if alpha == Alpha::BlendOrAdditive {
320 let a = &mut hsvag.a;
321 let mut additive = is_additive_alpha(*a);
322 ui.horizontal(|ui| {
323 ui.label("Blending:");
324 ui.radio_value(&mut additive, false, "Normal");
325 ui.radio_value(&mut additive, true, "Additive");
326
327 if additive {
328 *a = -a.abs();
329 }
330
331 if !additive {
332 *a = a.abs();
333 }
334 });
335 }
336
337 let opaque = HsvaGamma { a: 1.0, ..*hsvag };
338
339 let HsvaGamma { h, s, v, a: _ } = hsvag;
340
341 if false {
342 color_slider_1d(ui, s, |s| HsvaGamma { s, ..opaque }.into()).on_hover_text("Saturation");
343 }
344
345 if false {
346 color_slider_1d(ui, v, |v| HsvaGamma { v, ..opaque }.into()).on_hover_text("Value");
347 }
348
349 color_slider_2d(ui, s, v, |s, v| HsvaGamma { s, v, ..opaque }.into());
350
351 color_slider_1d(ui, h, |h| {
352 HsvaGamma {
353 h,
354 s: 1.0,
355 v: 1.0,
356 a: 1.0,
357 }
358 .into()
359 })
360 .on_hover_text("Hue");
361
362 let additive = is_additive_alpha(hsvag.a);
363
364 if alpha == Alpha::Opaque {
365 hsvag.a = 1.0;
366 } else {
367 let a = &mut hsvag.a;
368
369 if alpha == Alpha::OnlyBlend {
370 if is_additive_alpha(*a) {
371 *a = 0.5; }
373 color_slider_1d(ui, a, |a| HsvaGamma { a, ..opaque }.into()).on_hover_text("Alpha");
374 } else if !additive {
375 color_slider_1d(ui, a, |a| HsvaGamma { a, ..opaque }.into()).on_hover_text("Alpha");
376 }
377 }
378}
379
380fn input_type_button_ui(ui: &mut Ui) {
381 let mut input_type = ui.ctx().style().visuals.numeric_color_space;
382 if input_type.toggle_button_ui(ui).changed() {
383 ui.ctx().all_styles_mut(|s| {
384 s.visuals.numeric_color_space = input_type;
385 });
386 }
387}
388
389fn srgba_edit_ui(ui: &mut Ui, [r, g, b, a]: &mut [u8; 4], alpha: Alpha) -> bool {
394 let mut edited = false;
395
396 ui.horizontal(|ui| {
397 input_type_button_ui(ui);
398
399 if ui
400 .button("📋")
401 .on_hover_text("Click to copy color values")
402 .clicked()
403 {
404 if alpha == Alpha::Opaque {
405 ui.ctx().copy_text(format!("{r}, {g}, {b}"));
406 } else {
407 ui.ctx().copy_text(format!("{r}, {g}, {b}, {a}"));
408 }
409 }
410 edited |= DragValue::new(r).speed(0.5).prefix("R ").ui(ui).changed();
411 edited |= DragValue::new(g).speed(0.5).prefix("G ").ui(ui).changed();
412 edited |= DragValue::new(b).speed(0.5).prefix("B ").ui(ui).changed();
413 if alpha != Alpha::Opaque {
414 edited |= DragValue::new(a).speed(0.5).prefix("A ").ui(ui).changed();
415 }
416 });
417
418 edited
419}
420
421fn rgba_edit_ui(ui: &mut Ui, [r, g, b, a]: &mut [f32; 4], alpha: Alpha) -> bool {
426 fn drag_value(ui: &mut Ui, prefix: &str, value: &mut f32) -> Response {
427 DragValue::new(value)
428 .speed(0.003)
429 .prefix(prefix)
430 .range(0.0..=1.0)
431 .custom_formatter(|n, _| format!("{n:.03}"))
432 .ui(ui)
433 }
434
435 let mut edited = false;
436
437 ui.horizontal(|ui| {
438 input_type_button_ui(ui);
439
440 if ui
441 .button("📋")
442 .on_hover_text("Click to copy color values")
443 .clicked()
444 {
445 if alpha == Alpha::Opaque {
446 ui.ctx().copy_text(format!("{r:.03}, {g:.03}, {b:.03}"));
447 } else {
448 ui.ctx()
449 .copy_text(format!("{r:.03}, {g:.03}, {b:.03}, {a:.03}"));
450 }
451 }
452
453 edited |= drag_value(ui, "R ", r).changed();
454 edited |= drag_value(ui, "G ", g).changed();
455 edited |= drag_value(ui, "B ", b).changed();
456 if alpha != Alpha::Opaque {
457 edited |= drag_value(ui, "A ", a).changed();
458 }
459 });
460
461 edited
462}
463
464pub fn color_picker_hsva_2d(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> bool {
468 let mut hsvag = HsvaGamma::from(*hsva);
469 ui.vertical(|ui| {
470 color_picker_hsvag_2d(ui, &mut hsvag, alpha);
471 });
472 let new_hasva = Hsva::from(hsvag);
473 if *hsva == new_hasva {
474 false
475 } else {
476 *hsva = new_hasva;
477 true
478 }
479}
480
481pub fn color_picker_color32(ui: &mut Ui, srgba: &mut Color32, alpha: Alpha) -> bool {
485 let mut hsva = color_cache_get(ui.ctx(), *srgba);
486 let changed = color_picker_hsva_2d(ui, &mut hsva, alpha);
487 *srgba = Color32::from(hsva);
488 color_cache_set(ui.ctx(), *srgba, hsva);
489 changed
490}
491
492pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> Response {
493 let popup_id = ui.auto_id_with("popup");
494 let open = Popup::is_id_open(ui.ctx(), popup_id);
495 let mut button_response = color_button(ui, (*hsva).into(), open);
496 if ui.style().explanation_tooltips {
497 button_response = button_response.on_hover_text("Click to edit color");
498 }
499
500 const COLOR_SLIDER_WIDTH: f32 = 275.0;
501
502 Popup::menu(&button_response)
503 .id(popup_id)
504 .close_behavior(PopupCloseBehavior::CloseOnClickOutside)
505 .show(|ui| {
506 ui.spacing_mut().slider_width = COLOR_SLIDER_WIDTH;
507 if color_picker_hsva_2d(ui, hsva, alpha) {
508 button_response.mark_changed();
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}