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 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
97fn 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); ui.painter().rect_stroke(
128 rect,
129 corner_radius,
130 (stroke_width, visuals.bg_fill), 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); {
154 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); {
174 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), pos2(x + r, rect.bottom()), pos2(x - r, rect.bottom()), ],
184 picked_color,
185 Stroke::new(visuals.fg_stroke.width, contrast_color(picked_color)),
186 ));
187 }
188 }
189
190 response
191}
192
193fn 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)); ui.painter()
241 .rect_stroke(rect, 0.0, visuals.bg_stroke, StrokeKind::Inside); 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
258fn is_additive_alpha(a: f32) -> bool {
262 a < 0.0
263}
264
265#[derive(Clone, Copy, PartialEq, Eq)]
267pub enum Alpha {
268 Opaque,
270
271 OnlyBlend,
273
274 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 } 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 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 hsvag.a = alpha;
303 } else {
304 *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 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 hsvag.a = alpha;
325 } else {
326 *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; }
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
416fn 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
448fn 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
490pub 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
507pub 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
541pub 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
551pub 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
563pub 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
573pub 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
584fn 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
590fn 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
596fn 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}