1use std::hash::Hash;
27
28use egui::{
29 ecolor::{Hsva, HsvaGamma},
30 epaint::Mesh,
31 lerp, pos2, vec2, Color32, CornerRadius, FontSelection, Id, Pos2, Rect, Response, Sense, Shape,
32 Stroke, StrokeKind, TextEdit, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetType,
33};
34
35use crate::popover::{Popover, PopoverSide};
36use crate::theme::{with_alpha, Theme};
37
38const HEX_BUF_SUFFIX: &str = "color_picker::hex_buf";
39const HSV_CACHE_SUFFIX: &str = "color_picker::hsv_cache";
40const RECENTS_SUFFIX: &str = "color_picker::recents";
41
42#[must_use = "Add with `ui.add(...)`."]
50pub struct ColorPicker<'a> {
51 id_salt: Id,
52 color: &'a mut Color32,
53 label: Option<WidgetText>,
54 palette: Vec<Color32>,
55 palette_columns: usize,
56 show_continuous: bool,
57 show_alpha: bool,
58 show_hex_input: bool,
59 show_hex_label: bool,
60 show_recents: bool,
61 recents_max: usize,
62 side: PopoverSide,
63}
64
65impl<'a> std::fmt::Debug for ColorPicker<'a> {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 f.debug_struct("ColorPicker")
68 .field("id_salt", &self.id_salt)
69 .field("color", &*self.color)
70 .field("palette_len", &self.palette.len())
71 .field("palette_columns", &self.palette_columns)
72 .field("show_continuous", &self.show_continuous)
73 .field("show_alpha", &self.show_alpha)
74 .field("show_hex_input", &self.show_hex_input)
75 .field("show_hex_label", &self.show_hex_label)
76 .field("show_recents", &self.show_recents)
77 .field("recents_max", &self.recents_max)
78 .field("side", &self.side)
79 .finish()
80 }
81}
82
83impl<'a> ColorPicker<'a> {
84 pub fn new(id_salt: impl Hash, color: &'a mut Color32) -> Self {
88 Self {
89 id_salt: Id::new(id_salt),
90 color,
91 label: None,
92 palette: Vec::new(),
93 palette_columns: 10,
94 show_continuous: true,
95 show_alpha: true,
96 show_hex_input: true,
97 show_hex_label: true,
98 show_recents: true,
99 recents_max: 10,
100 side: PopoverSide::Bottom,
101 }
102 }
103
104 #[inline]
106 pub fn label(mut self, label: impl Into<WidgetText>) -> Self {
107 self.label = Some(label.into());
108 self
109 }
110
111 pub fn palette(mut self, palette: impl IntoIterator<Item = Color32>) -> Self {
115 self.palette = palette.into_iter().collect();
116 self
117 }
118
119 #[inline]
121 pub fn palette_columns(mut self, n: usize) -> Self {
122 self.palette_columns = n.max(1);
123 self
124 }
125
126 #[inline]
128 pub fn continuous(mut self, on: bool) -> Self {
129 self.show_continuous = on;
130 self
131 }
132
133 #[inline]
135 pub fn alpha(mut self, on: bool) -> Self {
136 self.show_alpha = on;
137 self
138 }
139
140 #[inline]
142 pub fn hex_input(mut self, on: bool) -> Self {
143 self.show_hex_input = on;
144 self
145 }
146
147 #[inline]
150 pub fn hex_label(mut self, on: bool) -> Self {
151 self.show_hex_label = on;
152 self
153 }
154
155 #[inline]
158 pub fn recents(mut self, on: bool) -> Self {
159 self.show_recents = on;
160 self
161 }
162
163 #[inline]
165 pub fn recents_max(mut self, n: usize) -> Self {
166 self.recents_max = n.max(1);
167 self
168 }
169
170 #[inline]
172 pub fn side(mut self, side: PopoverSide) -> Self {
173 self.side = side;
174 self
175 }
176
177 pub fn default_palette() -> Vec<Color32> {
180 vec![
181 Color32::from_rgb(0xe2, 0xe8, 0xf0),
183 Color32::from_rgb(0x94, 0xa3, 0xb8),
184 Color32::from_rgb(0x64, 0x74, 0x8b),
185 Color32::from_rgb(0x47, 0x55, 0x69),
186 Color32::from_rgb(0x33, 0x41, 0x55),
187 Color32::from_rgb(0x1e, 0x29, 0x3b),
188 Color32::from_rgb(0x0f, 0x17, 0x2a),
189 Color32::from_rgb(0x0b, 0x11, 0x20),
190 Color32::from_rgb(0x11, 0x1a, 0x2e),
191 Color32::from_rgb(0x18, 0x24, 0x38),
192 Color32::from_rgb(0x38, 0xbd, 0xf8),
194 Color32::from_rgb(0x0e, 0xa5, 0xe9),
195 Color32::from_rgb(0x25, 0x63, 0xeb),
196 Color32::from_rgb(0x63, 0x66, 0xf1),
197 Color32::from_rgb(0xc0, 0x84, 0xfc),
198 Color32::from_rgb(0xa8, 0x55, 0xf7),
199 Color32::from_rgb(0xf4, 0x72, 0xb6),
200 Color32::from_rgb(0xfb, 0x71, 0x85),
201 Color32::from_rgb(0xf8, 0x71, 0x71),
202 Color32::from_rgb(0xef, 0x44, 0x44),
203 Color32::from_rgb(0xf5, 0x9e, 0x0b),
205 Color32::from_rgb(0xfb, 0xbf, 0x24),
206 Color32::from_rgb(0xfa, 0xcc, 0x15),
207 Color32::from_rgb(0xd9, 0x77, 0x06),
208 Color32::from_rgb(0xa3, 0xe6, 0x35),
209 Color32::from_rgb(0x86, 0xef, 0xac),
210 Color32::from_rgb(0x4a, 0xde, 0x80),
211 Color32::from_rgb(0x22, 0xc5, 0x5e),
212 Color32::from_rgb(0x14, 0xb8, 0xa6),
213 Color32::from_rgb(0x22, 0xd3, 0xee),
214 ]
215 }
216}
217
218impl<'a> Widget for ColorPicker<'a> {
219 fn ui(self, ui: &mut Ui) -> Response {
220 let theme = Theme::current(ui.ctx());
221 let p = &theme.palette;
222 let t = &theme.typography;
223
224 let label = self.label.clone();
225 let id_salt = self.id_salt;
226 let side = self.side;
227 let show_palette = !self.palette.is_empty();
228 let show_recents = self.show_recents;
229 let show_continuous = self.show_continuous;
230 let show_alpha = self.show_alpha;
231 let show_hex_input = self.show_hex_input;
232 let palette = self.palette.clone();
233 let palette_columns = self.palette_columns;
234 let recents_max = self.recents_max;
235
236 ui.vertical(|ui| {
237 if let Some(l) = &label {
238 let rich = egui::RichText::new(l.text())
239 .color(p.text_muted)
240 .size(t.label);
241 ui.add(egui::Label::new(rich).wrap_mode(egui::TextWrapMode::Extend));
242 ui.add_space(2.0);
243 }
244
245 let mut response = paint_trigger(ui, &theme, id_salt, *self.color, self.show_hex_label);
246
247 let mut discrete_pick: Option<Color32> = None;
257 let mut continuous_pick: Option<Color32> = None;
258 let mut continuous_committed = false;
259 Popover::new(("elegance::color_picker", id_salt))
260 .side(side)
261 .arrow(false)
262 .min_width(248.0)
263 .show(&response, |ui| {
264 ui.spacing_mut().item_spacing = vec2(0.0, 8.0);
265
266 let cur = *self.color;
267 let mut hsv = current_hsv(ui.ctx(), id_salt, cur);
268
269 if show_palette {
270 ui.add(small_label(&theme, "Theme palette"));
271 if let Some(picked) =
272 paint_palette_grid(ui, &theme, cur, &palette, palette_columns)
273 {
274 discrete_pick = Some(picked);
275 }
276 }
277
278 if show_recents {
279 ui.add(small_label(&theme, "Recent"));
280 let recents: Vec<Color32> = ui
281 .ctx()
282 .data(|d| d.get_temp(recents_id(id_salt)))
283 .unwrap_or_default();
284 if let Some(picked) = paint_recents_row(
285 ui,
286 &theme,
287 cur,
288 &recents,
289 palette_columns,
290 recents_max,
291 ) {
292 discrete_pick = Some(picked);
293 }
294 }
295
296 if show_continuous {
297 let (changed, ended) = paint_sv_plane(ui, &theme, &mut hsv);
298 if changed {
299 continuous_pick = Some(Color32::from(hsv));
300 }
301 continuous_committed |= ended;
302 let (changed, ended) = paint_hue_strip(ui, &theme, &mut hsv);
303 if changed {
304 continuous_pick = Some(Color32::from(hsv));
305 }
306 continuous_committed |= ended;
307 }
308
309 if show_alpha {
310 let (changed, ended) = paint_alpha_slider(ui, &theme, &mut hsv);
311 if changed {
312 continuous_pick = Some(Color32::from(hsv));
313 }
314 continuous_committed |= ended;
315 }
316
317 if show_hex_input {
318 if let Some(picked) = paint_hex_input(ui, &theme, id_salt, cur) {
319 discrete_pick = Some(picked);
320 }
321 }
322
323 if let Some(next) = discrete_pick.or(continuous_pick) {
324 if discrete_pick.is_some() {
329 hsv = HsvaGamma::from(next);
330 }
331 set_hsv(ui.ctx(), id_salt, hsv);
332 }
333 });
334
335 let next_color = discrete_pick.or(continuous_pick);
336 if let Some(picked) = next_color {
337 if picked != *self.color {
338 *self.color = picked;
339 response.mark_changed();
340 }
341 }
342 if discrete_pick.is_some() || continuous_committed {
347 push_recent(ui.ctx(), id_salt, *self.color, recents_max);
348 }
349
350 let label_text = label
351 .as_ref()
352 .map(|l| l.text().to_string())
353 .unwrap_or_else(|| "Color".to_string());
354 response
355 .widget_info(|| WidgetInfo::labeled(WidgetType::ColorButton, true, &label_text));
356 response
357 })
358 .inner
359 }
360}
361
362fn paint_trigger(
365 ui: &mut Ui,
366 theme: &Theme,
367 id_salt: Id,
368 color: Color32,
369 show_hex_label: bool,
370) -> Response {
371 let p = &theme.palette;
372 let t = &theme.typography;
373
374 let pad_outer = vec2(5.0, 5.0);
375 let swatch_size = 22.0;
376 let inner_gap = 8.0;
377
378 let hex_text = format_hex(color);
379 let galley = if show_hex_label {
380 Some(crate::theme::placeholder_galley(
381 ui,
382 &hex_text,
383 t.small,
384 false,
385 f32::INFINITY,
386 ))
387 } else {
388 None
389 };
390
391 let hex_w = galley.as_ref().map(|g| g.size().x).unwrap_or(0.0);
392 let hex_h = galley.as_ref().map(|g| g.size().y).unwrap_or(0.0);
393 let content_w = swatch_size
394 + galley
395 .as_ref()
396 .map(|_| inner_gap + hex_w + 5.0)
397 .unwrap_or(0.0);
398 let content_h = swatch_size.max(hex_h);
399
400 let desired = vec2(content_w + 2.0 * pad_outer.x, content_h + 2.0 * pad_outer.y);
401
402 let id = id_salt.with("trigger");
403 let (rect, _) = ui.allocate_exact_size(desired, Sense::hover());
404 let response = ui.interact(rect, id, Sense::click());
405
406 if ui.is_rect_visible(rect) {
407 let painter = ui.painter();
408 let radius = CornerRadius::same(theme.control_radius as u8);
409 let stroke_color = if response.has_focus() {
410 with_alpha(p.sky, 200)
411 } else if response.hovered() {
412 p.text_muted
413 } else {
414 p.border
415 };
416 painter.rect(
417 rect,
418 radius,
419 p.input_bg,
420 Stroke::new(1.0, stroke_color),
421 StrokeKind::Inside,
422 );
423
424 let swatch_rect = Rect::from_min_size(
425 pos2(
426 rect.min.x + pad_outer.x,
427 rect.center().y - swatch_size * 0.5,
428 ),
429 Vec2::splat(swatch_size),
430 );
431 paint_swatch(painter, swatch_rect, color, 4, p.is_dark, p.input_bg);
432
433 if let Some(g) = galley {
434 let text_x = swatch_rect.max.x + inner_gap;
435 let text_y = rect.center().y - g.size().y * 0.5;
436 painter.galley(pos2(text_x, text_y), g, p.text);
437 }
438 }
439
440 response
441}
442
443fn paint_palette_grid(
446 ui: &mut Ui,
447 theme: &Theme,
448 current: Color32,
449 palette: &[Color32],
450 columns: usize,
451) -> Option<Color32> {
452 let p = &theme.palette;
453 let gap = 5.0;
454 let avail = ui.available_width();
455 let cols = columns.max(1);
456 let cell = ((avail - gap * (cols - 1) as f32) / cols as f32).max(8.0);
457 let rows = palette.len().div_ceil(cols);
458 let total_h = rows as f32 * cell + (rows.saturating_sub(1)) as f32 * gap;
459 let (rect, _) = ui.allocate_exact_size(vec2(avail, total_h), Sense::hover());
460
461 let mut picked = None;
462 for (i, color) in palette.iter().copied().enumerate() {
463 let row = i / cols;
464 let col = i % cols;
465 let x = rect.min.x + col as f32 * (cell + gap);
466 let y = rect.min.y + row as f32 * (cell + gap);
467 let cell_rect = Rect::from_min_size(pos2(x, y), Vec2::splat(cell));
468 let id = ui
469 .id()
470 .with(("color_picker_palette", i, color.r(), color.g(), color.b()));
471 let resp = ui.interact(cell_rect, id, Sense::click());
472 let selected = color == current;
473 paint_palette_swatch(ui, cell_rect, color, selected, &resp, p.is_dark, theme);
474 if resp.clicked() {
475 picked = Some(color);
476 }
477 }
478 picked
479}
480
481fn paint_recents_row(
482 ui: &mut Ui,
483 theme: &Theme,
484 current: Color32,
485 recents: &[Color32],
486 columns: usize,
487 max: usize,
488) -> Option<Color32> {
489 let p = &theme.palette;
490 let cols = columns.max(1).max(max);
491 let gap = 5.0;
492 let avail = ui.available_width();
493 let cell = ((avail - gap * (cols - 1) as f32) / cols as f32).max(8.0);
494 let total_h = cell;
495 let (rect, _) = ui.allocate_exact_size(vec2(avail, total_h), Sense::hover());
496
497 let mut picked = None;
498 for col in 0..cols {
499 let x = rect.min.x + col as f32 * (cell + gap);
500 let y = rect.min.y;
501 let cell_rect = Rect::from_min_size(pos2(x, y), Vec2::splat(cell));
502 if let Some(color) = recents.get(col).copied() {
503 let id = ui.id().with(("color_picker_recents", col));
504 let resp = ui.interact(cell_rect, id, Sense::click());
505 let selected = color == current;
506 paint_palette_swatch(ui, cell_rect, color, selected, &resp, p.is_dark, theme);
507 if resp.clicked() {
508 picked = Some(color);
509 }
510 } else {
511 paint_recents_empty(ui, cell_rect, theme);
512 }
513 }
514 picked
515}
516
517fn paint_palette_swatch(
518 ui: &Ui,
519 rect: Rect,
520 color: Color32,
521 selected: bool,
522 resp: &Response,
523 is_dark: bool,
524 theme: &Theme,
525) {
526 let painter = ui.painter();
527 let radius_n: u8 = 4;
528 let radius = CornerRadius::same(radius_n);
529 if color.is_opaque() {
530 painter.rect_filled(rect, radius, color);
531 } else {
532 paint_checkers(painter, rect, radius);
533 painter.rect_filled(rect, radius, color);
534 paint_rounded_corner_mask(painter, rect, radius_n as f32, theme.palette.card);
535 }
536 let inset_color = if is_dark {
537 Color32::from_rgba_unmultiplied(15, 23, 42, 130)
538 } else {
539 Color32::from_rgba_unmultiplied(0, 0, 0, 60)
540 };
541 painter.rect_stroke(
542 rect,
543 radius,
544 Stroke::new(1.0, inset_color),
545 StrokeKind::Inside,
546 );
547
548 if selected {
549 let outer = rect.expand(2.0);
550 let painter = ui.painter();
551 painter.rect_stroke(
552 outer,
553 CornerRadius::same(5),
554 Stroke::new(2.0, theme.palette.sky),
555 StrokeKind::Outside,
556 );
557 } else if resp.hovered() {
558 let outer = rect.expand(1.0);
559 ui.painter().rect_stroke(
560 outer,
561 CornerRadius::same(5),
562 Stroke::new(1.0, with_alpha(theme.palette.text, 110)),
563 StrokeKind::Outside,
564 );
565 }
566}
567
568fn paint_recents_empty(ui: &Ui, rect: Rect, theme: &Theme) {
569 let p = &theme.palette;
570 let painter = ui.painter();
571 let radius = CornerRadius::same(4);
572 painter.rect_filled(rect, radius, p.input_bg);
573 paint_dashed_rect(
574 painter,
575 rect,
576 radius,
577 with_alpha(p.text_faint, 160),
578 1.0,
579 3.0,
580 3.0,
581 );
582}
583
584fn paint_dashed_rect(
585 painter: &egui::Painter,
586 rect: Rect,
587 _radius: CornerRadius,
588 color: Color32,
589 width: f32,
590 dash: f32,
591 gap: f32,
592) {
593 let stroke = Stroke::new(width, color);
594 let segments = |from: Pos2, to: Pos2| -> Vec<[Pos2; 2]> {
595 let dx = to.x - from.x;
596 let dy = to.y - from.y;
597 let len = (dx * dx + dy * dy).sqrt();
598 if len <= 0.0 {
599 return Vec::new();
600 }
601 let step = dash + gap;
602 let n = (len / step).floor() as usize;
603 let mut out = Vec::with_capacity(n + 1);
604 let mut t = 0.0_f32;
605 while t < len {
606 let t_end = (t + dash).min(len);
607 let a = pos2(from.x + dx * (t / len), from.y + dy * (t / len));
608 let b = pos2(from.x + dx * (t_end / len), from.y + dy * (t_end / len));
609 out.push([a, b]);
610 t += step;
611 }
612 out
613 };
614 for seg in segments(rect.left_top(), rect.right_top()) {
615 painter.line_segment(seg, stroke);
616 }
617 for seg in segments(rect.right_top(), rect.right_bottom()) {
618 painter.line_segment(seg, stroke);
619 }
620 for seg in segments(rect.right_bottom(), rect.left_bottom()) {
621 painter.line_segment(seg, stroke);
622 }
623 for seg in segments(rect.left_bottom(), rect.left_top()) {
624 painter.line_segment(seg, stroke);
625 }
626}
627
628fn paint_sv_plane(ui: &mut Ui, theme: &Theme, hsv: &mut HsvaGamma) -> (bool, bool) {
631 let p = &theme.palette;
632 let avail = ui.available_width();
633 let height = 150.0;
634 let (rect, response) = ui.allocate_exact_size(vec2(avail, height), Sense::click_and_drag());
635 let mut changed = false;
636 let committed = response.drag_stopped() || response.clicked();
637
638 if let Some(pos) = response.interact_pointer_pos() {
639 if response.is_pointer_button_down_on() {
640 let s = ((pos.x - rect.min.x) / rect.width()).clamp(0.0, 1.0);
641 let v = 1.0 - ((pos.y - rect.min.y) / rect.height()).clamp(0.0, 1.0);
642 hsv.s = s;
643 hsv.v = v;
644 changed = true;
645 }
646 }
647
648 if ui.is_rect_visible(rect) {
649 let painter = ui.painter();
650 let radius = CornerRadius::same(6);
651
652 let n: usize = 24;
654 let mut mesh = Mesh::default();
655 for yi in 0..=n {
656 for xi in 0..=n {
657 let s = xi as f32 / n as f32;
658 let v = 1.0 - yi as f32 / n as f32;
659 let c: Color32 = HsvaGamma {
660 h: hsv.h,
661 s,
662 v,
663 a: 1.0,
664 }
665 .into();
666 let x = lerp(rect.left()..=rect.right(), s);
667 let y = lerp(rect.top()..=rect.bottom(), 1.0 - v);
668 mesh.colored_vertex(pos2(x, y), c);
669 }
670 }
671 let stride = (n + 1) as u32;
672 for yi in 0..n {
673 for xi in 0..n {
674 let i = (yi * (n + 1) + xi) as u32;
675 mesh.add_triangle(i, i + 1, i + stride);
676 mesh.add_triangle(i + 1, i + stride, i + stride + 1);
677 }
678 }
679 painter.add(Shape::mesh(mesh));
680
681 paint_rounded_corner_mask(painter, rect, 6.0, p.card);
684 painter.rect_stroke(rect, radius, Stroke::new(1.0, p.border), StrokeKind::Inside);
685
686 let cx = lerp(rect.left()..=rect.right(), hsv.s);
688 let cy = lerp(rect.top()..=rect.bottom(), 1.0 - hsv.v);
689 let center = pos2(cx, cy);
690 ui.painter().circle(
691 center,
692 6.0,
693 Color32::TRANSPARENT,
694 Stroke::new(2.0, Color32::WHITE),
695 );
696 ui.painter().circle_stroke(
697 center,
698 7.0,
699 Stroke::new(1.0, Color32::from_rgba_unmultiplied(0, 0, 0, 180)),
700 );
701 }
702
703 (changed, committed)
704}
705
706fn paint_hue_strip(ui: &mut Ui, theme: &Theme, hsv: &mut HsvaGamma) -> (bool, bool) {
707 let p = &theme.palette;
708 let avail = ui.available_width();
709 let height = 14.0;
710 let (rect, response) = ui.allocate_exact_size(vec2(avail, height), Sense::click_and_drag());
711 let mut changed = false;
712 let committed = response.drag_stopped() || response.clicked();
713
714 if let Some(pos) = response.interact_pointer_pos() {
715 if response.is_pointer_button_down_on() {
716 let h = ((pos.x - rect.min.x) / rect.width()).clamp(0.0, 1.0);
717 hsv.h = h;
718 changed = true;
719 }
720 }
721
722 if ui.is_rect_visible(rect) {
723 let painter = ui.painter();
724 let radius = CornerRadius::same((rect.height() * 0.5) as u8);
725
726 let n: usize = 36;
727 let mut mesh = Mesh::default();
728 for i in 0..=n {
729 let h = i as f32 / n as f32;
730 let c: Color32 = HsvaGamma {
731 h,
732 s: 1.0,
733 v: 1.0,
734 a: 1.0,
735 }
736 .into();
737 let x = lerp(rect.left()..=rect.right(), h);
738 mesh.colored_vertex(pos2(x, rect.top()), c);
739 mesh.colored_vertex(pos2(x, rect.bottom()), c);
740 if i < n {
741 let base = (i * 2) as u32;
742 mesh.add_triangle(base, base + 1, base + 2);
743 mesh.add_triangle(base + 1, base + 2, base + 3);
744 }
745 }
746 painter.add(Shape::mesh(mesh));
747 paint_rounded_corner_mask(painter, rect, rect.height() * 0.5, p.card);
748 painter.rect_stroke(rect, radius, Stroke::new(1.0, p.border), StrokeKind::Inside);
749
750 let thumb_x = lerp(rect.left()..=rect.right(), hsv.h);
751 let thumb_center = pos2(thumb_x, rect.center().y);
752 let thumb_color: Color32 = HsvaGamma {
753 h: hsv.h,
754 s: 1.0,
755 v: 1.0,
756 a: 1.0,
757 }
758 .into();
759 painter.circle(
760 thumb_center,
761 7.0,
762 thumb_color,
763 Stroke::new(2.0, Color32::WHITE),
764 );
765 painter.circle_stroke(
766 thumb_center,
767 8.0,
768 Stroke::new(1.0, Color32::from_rgba_unmultiplied(0, 0, 0, 100)),
769 );
770 }
771
772 (changed, committed)
773}
774
775fn paint_alpha_slider(ui: &mut Ui, theme: &Theme, hsv: &mut HsvaGamma) -> (bool, bool) {
776 let p = &theme.palette;
777 let avail = ui.available_width();
778 let height = 14.0;
779 let (rect, response) = ui.allocate_exact_size(vec2(avail, height), Sense::click_and_drag());
780 let mut changed = false;
781 let committed = response.drag_stopped() || response.clicked();
782
783 if let Some(pos) = response.interact_pointer_pos() {
784 if response.is_pointer_button_down_on() {
785 let a = ((pos.x - rect.min.x) / rect.width()).clamp(0.0, 1.0);
786 hsv.a = a;
787 changed = true;
788 }
789 }
790
791 if ui.is_rect_visible(rect) {
792 let painter = ui.painter();
793 let radius = CornerRadius::same((rect.height() * 0.5) as u8);
794
795 paint_checkers(painter, rect, radius);
797
798 let opaque: Color32 = HsvaGamma { a: 1.0, ..*hsv }.into();
800 let [r, g, b, _] = opaque.to_srgba_unmultiplied();
801 let transparent = Color32::from_rgba_unmultiplied(r, g, b, 0);
802 let mut mesh = Mesh::default();
803 let n = 12u32;
804 for i in 0..=n {
805 let t = i as f32 / n as f32;
806 let c = lerp_color(transparent, opaque, t);
807 let x = lerp(rect.left()..=rect.right(), t);
808 mesh.colored_vertex(pos2(x, rect.top()), c);
809 mesh.colored_vertex(pos2(x, rect.bottom()), c);
810 if i < n {
811 let base = i * 2;
812 mesh.add_triangle(base, base + 1, base + 2);
813 mesh.add_triangle(base + 1, base + 2, base + 3);
814 }
815 }
816 painter.add(Shape::mesh(mesh));
817 paint_rounded_corner_mask(painter, rect, rect.height() * 0.5, p.card);
818 painter.rect_stroke(rect, radius, Stroke::new(1.0, p.border), StrokeKind::Inside);
819
820 let thumb_x = lerp(rect.left()..=rect.right(), hsv.a);
821 let thumb_center = pos2(thumb_x, rect.center().y);
822 painter.circle(thumb_center, 7.0, p.text, Stroke::new(2.0, p.card));
823 }
824
825 (changed, committed)
826}
827
828fn paint_hex_input(ui: &mut Ui, theme: &Theme, id_salt: Id, current: Color32) -> Option<Color32> {
831 let p = &theme.palette;
832 let t = &theme.typography;
833
834 let buf_id = id_salt.with(HEX_BUF_SUFFIX);
835 let edit_id = id_salt.with("color_picker::hex_edit");
836
837 let has_focus = ui.memory(|m| m.has_focus(edit_id));
839 let mut buf: String = ui.ctx().data(|d| d.get_temp(buf_id)).unwrap_or_default();
840 if !has_focus {
841 buf = format_hex(current);
842 }
843
844 let mut picked = None;
845 ui.horizontal(|ui| {
846 let preview_size = Vec2::splat(28.0);
848 let (preview_rect, _) = ui.allocate_exact_size(preview_size, Sense::hover());
849 let radius_n: u8 = 5;
850 let radius = CornerRadius::same(radius_n);
851 if current.is_opaque() {
852 ui.painter().rect_filled(preview_rect, radius, current);
853 } else {
854 paint_checkers(ui.painter(), preview_rect, radius);
855 ui.painter().rect_filled(preview_rect, radius, current);
856 paint_rounded_corner_mask(ui.painter(), preview_rect, radius_n as f32, p.card);
857 }
858 ui.painter().rect_stroke(
859 preview_rect,
860 radius,
861 Stroke::new(1.0, p.border),
862 StrokeKind::Inside,
863 );
864
865 ui.add_space(8.0);
866
867 let response = crate::theme::with_themed_visuals(ui, |ui| {
869 let v = ui.visuals_mut();
870 crate::theme::themed_input_visuals(v, theme, p.input_bg);
871 v.extreme_bg_color = p.input_bg;
872 v.selection.bg_fill = with_alpha(p.sky, 90);
873 v.selection.stroke = Stroke::new(1.0, p.sky);
874
875 let edit = TextEdit::singleline(&mut buf)
876 .id(edit_id)
877 .font(FontSelection::FontId(egui::FontId::monospace(t.body)))
878 .text_color(p.text)
879 .margin(vec2(8.0, 4.0))
880 .desired_width(ui.available_width());
881 ui.add(edit)
882 });
883
884 if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
886 if let Some(c) = parse_hex(&buf) {
887 picked = Some(c);
888 buf = format_hex(c);
889 } else {
890 buf = format_hex(current);
892 }
893 } else if !response.has_focus() && response.changed() {
894 } else if response.has_focus() {
896 let _ = parse_hex(&buf);
899 }
900
901 if !response.has_focus() {
902 ui.ctx().data_mut(|d| d.remove::<String>(buf_id));
904 } else {
905 ui.ctx().data_mut(|d| d.insert_temp(buf_id, buf.clone()));
906 }
907 });
908
909 picked
910}
911
912fn small_label(theme: &Theme, text: &str) -> egui::Label {
915 let rich = egui::RichText::new(text.to_uppercase())
916 .color(theme.palette.text_faint)
917 .size(theme.typography.small - 1.0);
918 egui::Label::new(rich).wrap_mode(egui::TextWrapMode::Extend)
919}
920
921fn paint_swatch(
922 painter: &egui::Painter,
923 rect: Rect,
924 color: Color32,
925 radius: u8,
926 is_dark: bool,
927 surround: Color32,
928) {
929 let r = CornerRadius::same(radius);
930 if color.is_opaque() {
931 painter.rect_filled(rect, r, color);
932 } else {
933 paint_checkers(painter, rect, r);
934 painter.rect_filled(rect, r, color);
935 paint_rounded_corner_mask(painter, rect, radius as f32, surround);
936 }
937 let inset = if is_dark {
938 Color32::from_rgba_unmultiplied(15, 23, 42, 110)
939 } else {
940 Color32::from_rgba_unmultiplied(0, 0, 0, 50)
941 };
942 painter.rect_stroke(rect, r, Stroke::new(1.0, inset), StrokeKind::Inside);
943}
944
945fn paint_rounded_corner_mask(
950 painter: &egui::Painter,
951 rect: Rect,
952 radius: f32,
953 mask_color: Color32,
954) {
955 if radius <= 0.5 || rect.width() <= 0.0 || rect.height() <= 0.0 {
956 return;
957 }
958 let r = radius.min(rect.width() * 0.5).min(rect.height() * 0.5);
959 let n: usize = 12;
960 let half_pi = std::f32::consts::FRAC_PI_2;
961 let pi = std::f32::consts::PI;
962 let corners: [(Pos2, Pos2, f32); 4] = [
963 (rect.left_top(), pos2(rect.left() + r, rect.top() + r), pi),
965 (
967 rect.right_top(),
968 pos2(rect.right() - r, rect.top() + r),
969 1.5 * pi,
970 ),
971 (
973 rect.right_bottom(),
974 pos2(rect.right() - r, rect.bottom() - r),
975 0.0,
976 ),
977 (
979 rect.left_bottom(),
980 pos2(rect.left() + r, rect.bottom() - r),
981 half_pi,
982 ),
983 ];
984 for (corner, center, start_angle) in corners {
985 let mut mesh = Mesh::default();
986 mesh.colored_vertex(corner, mask_color);
987 for i in 0..=n {
988 let t = i as f32 / n as f32;
989 let theta = start_angle + half_pi * t;
990 let p = pos2(center.x + r * theta.cos(), center.y + r * theta.sin());
991 mesh.colored_vertex(p, mask_color);
992 }
993 for i in 0..n {
994 mesh.add_triangle(0, (1 + i) as u32, (2 + i) as u32);
995 }
996 painter.add(Shape::mesh(mesh));
997 }
998}
999
1000fn paint_checkers(painter: &egui::Painter, rect: Rect, radius: CornerRadius) {
1001 let dark = Color32::from_gray(40);
1002 let light = Color32::from_gray(96);
1003 let cell = (rect.height() * 0.5).max(2.0);
1004 painter.rect_filled(rect, radius, dark);
1005 let cols = (rect.width() / cell).ceil() as i32;
1006 let rows = (rect.height() / cell).ceil() as i32;
1007 let mut tiles: Vec<Shape> = Vec::new();
1008 for j in 0..rows {
1009 for i in 0..cols {
1010 if (i + j) % 2 == 0 {
1011 continue;
1012 }
1013 let x0 = rect.min.x + i as f32 * cell;
1014 let y0 = rect.min.y + j as f32 * cell;
1015 let x1 = (x0 + cell).min(rect.max.x);
1016 let y1 = (y0 + cell).min(rect.max.y);
1017 tiles.push(Shape::rect_filled(
1018 Rect::from_min_max(pos2(x0, y0), pos2(x1, y1)),
1019 CornerRadius::ZERO,
1020 light,
1021 ));
1022 }
1023 }
1024 painter.extend(tiles);
1028}
1029
1030fn lerp_color(a: Color32, b: Color32, t: f32) -> Color32 {
1031 let t = t.clamp(0.0, 1.0);
1032 let mix = |x: u8, y: u8| -> u8 {
1033 let xf = x as f32;
1034 let yf = y as f32;
1035 (xf + (yf - xf) * t).round().clamp(0.0, 255.0) as u8
1036 };
1037 Color32::from_rgba_unmultiplied(
1038 mix(a.r(), b.r()),
1039 mix(a.g(), b.g()),
1040 mix(a.b(), b.b()),
1041 mix(a.a(), b.a()),
1042 )
1043}
1044
1045fn current_hsv(ctx: &egui::Context, id_salt: Id, color: Color32) -> HsvaGamma {
1046 let cache_id = id_salt.with(HSV_CACHE_SUFFIX);
1047 let cached: Option<HsvaGamma> = ctx.data(|d| d.get_temp(cache_id));
1048 if let Some(c) = cached {
1049 if Color32::from(c) == color {
1050 return c;
1051 }
1052 }
1053 HsvaGamma::from(Hsva::from(color))
1054}
1055
1056fn set_hsv(ctx: &egui::Context, id_salt: Id, hsv: HsvaGamma) {
1057 let cache_id = id_salt.with(HSV_CACHE_SUFFIX);
1058 ctx.data_mut(|d| d.insert_temp(cache_id, hsv));
1059}
1060
1061fn recents_id(id_salt: Id) -> Id {
1062 id_salt.with(RECENTS_SUFFIX)
1063}
1064
1065fn push_recent(ctx: &egui::Context, id_salt: Id, color: Color32, max: usize) {
1066 let id = recents_id(id_salt);
1067 let mut list: Vec<Color32> = ctx.data(|d| d.get_temp(id)).unwrap_or_default();
1068 list.retain(|c| *c != color);
1069 list.insert(0, color);
1070 list.truncate(max);
1071 ctx.data_mut(|d| d.insert_temp(id, list));
1072}
1073
1074fn format_hex(color: Color32) -> String {
1075 let [r, g, b, a] = color.to_srgba_unmultiplied();
1076 if a == 255 {
1077 format!("#{r:02X}{g:02X}{b:02X}")
1078 } else {
1079 format!("#{r:02X}{g:02X}{b:02X}{a:02X}")
1080 }
1081}
1082
1083fn parse_hex(text: &str) -> Option<Color32> {
1084 let s = text.trim().trim_start_matches('#');
1085 let bytes: Vec<u8> = s
1086 .chars()
1087 .filter_map(|c| c.to_digit(16).map(|d| d as u8))
1088 .collect();
1089 match bytes.len() {
1090 3 => Some(Color32::from_rgb(
1091 bytes[0] * 17,
1092 bytes[1] * 17,
1093 bytes[2] * 17,
1094 )),
1095 4 => Some(Color32::from_rgba_unmultiplied(
1096 bytes[0] * 17,
1097 bytes[1] * 17,
1098 bytes[2] * 17,
1099 bytes[3] * 17,
1100 )),
1101 6 => Some(Color32::from_rgb(
1102 (bytes[0] << 4) | bytes[1],
1103 (bytes[2] << 4) | bytes[3],
1104 (bytes[4] << 4) | bytes[5],
1105 )),
1106 8 => Some(Color32::from_rgba_unmultiplied(
1107 (bytes[0] << 4) | bytes[1],
1108 (bytes[2] << 4) | bytes[3],
1109 (bytes[4] << 4) | bytes[5],
1110 (bytes[6] << 4) | bytes[7],
1111 )),
1112 _ => None,
1113 }
1114}
1115
1116#[cfg(test)]
1117mod tests {
1118 use super::*;
1119
1120 #[test]
1121 fn hex_round_trip() {
1122 let c = Color32::from_rgb(0x38, 0xbd, 0xf8);
1123 assert_eq!(format_hex(c), "#38BDF8");
1124 assert_eq!(parse_hex("#38BDF8"), Some(c));
1125 assert_eq!(parse_hex("38bdf8"), Some(c));
1126 assert_eq!(parse_hex("#38B"), Some(Color32::from_rgb(0x33, 0x88, 0xbb)));
1127 assert_eq!(parse_hex(""), None);
1128 assert_eq!(parse_hex("#zzzzzz"), None);
1129 }
1130
1131 #[test]
1132 fn hex_round_trip_alpha() {
1133 let c = Color32::from_rgba_unmultiplied(0x38, 0xbd, 0xf8, 0xc0);
1134 assert_eq!(format_hex(c), "#38BDF8C0");
1135 assert_eq!(parse_hex("#38BDF8C0"), Some(c));
1136 }
1137}