1use egui::{
6 vec2, Color32, CornerRadius, CursorIcon, Pos2, Rect, Sense, Stroke, StrokeKind, Ui, Vec2,
7};
8
9use crate::{sizing, theme};
10
11#[derive(Clone, Copy)]
13pub struct TailwindColor {
14 pub name: &'static str,
16 pub shades: [Color32; 11],
18}
19
20impl TailwindColor {
21 pub const fn new(name: &'static str, shades: [(u8, u8, u8); 11]) -> Self {
23 Self {
24 name,
25 shades: [
26 Color32::from_rgb(shades[0].0, shades[0].1, shades[0].2),
27 Color32::from_rgb(shades[1].0, shades[1].1, shades[1].2),
28 Color32::from_rgb(shades[2].0, shades[2].1, shades[2].2),
29 Color32::from_rgb(shades[3].0, shades[3].1, shades[3].2),
30 Color32::from_rgb(shades[4].0, shades[4].1, shades[4].2),
31 Color32::from_rgb(shades[5].0, shades[5].1, shades[5].2),
32 Color32::from_rgb(shades[6].0, shades[6].1, shades[6].2),
33 Color32::from_rgb(shades[7].0, shades[7].1, shades[7].2),
34 Color32::from_rgb(shades[8].0, shades[8].1, shades[8].2),
35 Color32::from_rgb(shades[9].0, shades[9].1, shades[9].2),
36 Color32::from_rgb(shades[10].0, shades[10].1, shades[10].2),
37 ],
38 }
39 }
40
41 pub const fn primary(&self) -> Color32 {
43 self.shades[5]
44 }
45
46 pub const fn shade(&self, index: usize) -> Color32 {
48 self.shades[index]
49 }
50}
51
52pub const SHADE_LABELS: [&str; 11] = [
54 "50", "100", "200", "300", "400", "500", "600", "700", "800", "900", "950",
55];
56
57pub struct TailwindPalette;
59
60impl TailwindPalette {
61 pub fn all() -> &'static [TailwindColor] {
63 TAILWIND_COLORS
64 }
65
66 pub fn by_name(name: &str) -> Option<&'static TailwindColor> {
68 TAILWIND_COLORS.iter().find(|c| c.name == name)
69 }
70
71 pub fn quick_colors() -> &'static [usize] {
74 &[10, 2, 4, 11, 13, 16, 17]
76 }
77}
78
79pub const TAILWIND_COLORS: &[TailwindColor] = &[
81 TailwindColor::new("Red", [
82 (254, 242, 242), (254, 226, 226), (254, 202, 202), (252, 165, 165),
83 (248, 113, 113), (239, 68, 68), (220, 38, 38), (185, 28, 28),
84 (153, 27, 27), (127, 29, 29), (69, 10, 10),
85 ]),
86 TailwindColor::new("Orange", [
87 (255, 247, 237), (255, 237, 213), (254, 215, 170), (253, 186, 116),
88 (251, 146, 60), (249, 115, 22), (234, 88, 12), (194, 65, 12),
89 (154, 52, 18), (124, 45, 18), (67, 20, 7),
90 ]),
91 TailwindColor::new("Amber", [
92 (255, 251, 235), (254, 243, 199), (253, 230, 138), (252, 211, 77),
93 (251, 191, 36), (245, 158, 11), (217, 119, 6), (180, 83, 9),
94 (146, 64, 14), (120, 53, 15), (69, 26, 3),
95 ]),
96 TailwindColor::new("Yellow", [
97 (254, 252, 232), (254, 249, 195), (254, 240, 138), (253, 224, 71),
98 (250, 204, 21), (234, 179, 8), (202, 138, 4), (161, 98, 7),
99 (133, 77, 14), (113, 63, 18), (66, 32, 6),
100 ]),
101 TailwindColor::new("Lime", [
102 (247, 254, 231), (236, 252, 203), (217, 249, 157), (190, 242, 100),
103 (163, 230, 53), (132, 204, 22), (101, 163, 13), (77, 124, 15),
104 (63, 98, 18), (54, 83, 20), (26, 46, 5),
105 ]),
106 TailwindColor::new("Green", [
107 (240, 253, 244), (220, 252, 231), (187, 247, 208), (134, 239, 172),
108 (74, 222, 128), (34, 197, 94), (22, 163, 74), (21, 128, 61),
109 (22, 101, 52), (20, 83, 45), (5, 46, 22),
110 ]),
111 TailwindColor::new("Emerald", [
112 (236, 253, 245), (209, 250, 229), (167, 243, 208), (110, 231, 183),
113 (52, 211, 153), (16, 185, 129), (5, 150, 105), (4, 120, 87),
114 (6, 95, 70), (6, 78, 59), (2, 44, 34),
115 ]),
116 TailwindColor::new("Teal", [
117 (240, 253, 250), (204, 251, 241), (153, 246, 228), (94, 234, 212),
118 (45, 212, 191), (20, 184, 166), (13, 148, 136), (15, 118, 110),
119 (17, 94, 89), (19, 78, 74), (4, 47, 46),
120 ]),
121 TailwindColor::new("Cyan", [
122 (236, 254, 255), (207, 250, 254), (165, 243, 252), (103, 232, 249),
123 (34, 211, 238), (6, 182, 212), (8, 145, 178), (14, 116, 144),
124 (21, 94, 117), (22, 78, 99), (8, 51, 68),
125 ]),
126 TailwindColor::new("Sky", [
127 (240, 249, 255), (224, 242, 254), (186, 230, 253), (125, 211, 252),
128 (56, 189, 248), (14, 165, 233), (2, 132, 199), (3, 105, 161),
129 (7, 89, 133), (12, 74, 110), (8, 47, 73),
130 ]),
131 TailwindColor::new("Blue", [
132 (239, 246, 255), (219, 234, 254), (191, 219, 254), (147, 197, 253),
133 (96, 165, 250), (59, 130, 246), (37, 99, 235), (29, 78, 216),
134 (30, 64, 175), (30, 58, 138), (23, 37, 84),
135 ]),
136 TailwindColor::new("Indigo", [
137 (238, 242, 255), (224, 231, 255), (199, 210, 254), (165, 180, 252),
138 (129, 140, 248), (99, 102, 241), (79, 70, 229), (67, 56, 202),
139 (55, 48, 163), (49, 46, 129), (30, 27, 75),
140 ]),
141 TailwindColor::new("Violet", [
142 (245, 243, 255), (237, 233, 254), (221, 214, 254), (196, 181, 253),
143 (167, 139, 250), (139, 92, 246), (124, 58, 237), (109, 40, 217),
144 (91, 33, 182), (76, 29, 149), (46, 16, 101),
145 ]),
146 TailwindColor::new("Purple", [
147 (250, 245, 255), (243, 232, 255), (233, 213, 255), (216, 180, 254),
148 (192, 132, 252), (168, 85, 247), (147, 51, 234), (126, 34, 206),
149 (107, 33, 168), (88, 28, 135), (59, 7, 100),
150 ]),
151 TailwindColor::new("Fuchsia", [
152 (253, 244, 255), (250, 232, 255), (245, 208, 254), (240, 171, 252),
153 (232, 121, 249), (217, 70, 239), (192, 38, 211), (162, 28, 175),
154 (134, 25, 143), (112, 26, 117), (74, 4, 78),
155 ]),
156 TailwindColor::new("Pink", [
157 (253, 242, 248), (252, 231, 243), (251, 207, 232), (249, 168, 212),
158 (244, 114, 182), (236, 72, 153), (219, 39, 119), (190, 24, 93),
159 (157, 23, 77), (131, 24, 67), (80, 7, 36),
160 ]),
161 TailwindColor::new("Rose", [
162 (255, 241, 242), (255, 228, 230), (254, 205, 211), (253, 164, 175),
163 (251, 113, 133), (244, 63, 94), (225, 29, 72), (190, 18, 60),
164 (159, 18, 57), (136, 19, 55), (76, 5, 25),
165 ]),
166 TailwindColor::new("Slate", [
167 (248, 250, 252), (241, 245, 249), (226, 232, 240), (203, 213, 225),
168 (148, 163, 184), (100, 116, 139), (71, 85, 105), (51, 65, 85),
169 (30, 41, 59), (15, 23, 42), (2, 6, 23),
170 ]),
171 TailwindColor::new("Gray", [
172 (249, 250, 251), (243, 244, 246), (229, 231, 235), (209, 213, 219),
173 (156, 163, 175), (107, 114, 128), (75, 85, 99), (55, 65, 81),
174 (31, 41, 55), (17, 24, 39), (3, 7, 18),
175 ]),
176 TailwindColor::new("Zinc", [
177 (250, 250, 250), (244, 244, 245), (228, 228, 231), (212, 212, 216),
178 (161, 161, 170), (113, 113, 122), (82, 82, 91), (63, 63, 70),
179 (39, 39, 42), (24, 24, 27), (9, 9, 11),
180 ]),
181 TailwindColor::new("Neutral", [
182 (250, 250, 250), (245, 245, 245), (229, 229, 229), (212, 212, 212),
183 (163, 163, 163), (115, 115, 115), (82, 82, 82), (64, 64, 64),
184 (38, 38, 38), (23, 23, 23), (10, 10, 10),
185 ]),
186 TailwindColor::new("Stone", [
187 (250, 250, 249), (245, 245, 244), (231, 229, 228), (214, 211, 209),
188 (168, 162, 158), (120, 113, 108), (87, 83, 78), (68, 64, 60),
189 (41, 37, 36), (28, 25, 23), (12, 10, 9),
190 ]),
191];
192
193#[derive(Clone)]
195pub struct ColorSwatchStyle {
196 pub size: Vec2,
198 pub circular: bool,
200 pub selection_style: SelectionStyle,
202}
203
204#[derive(Clone, Copy, PartialEq)]
206pub enum SelectionStyle {
207 None,
209 InnerRing,
211 OuterBorder,
213}
214
215impl Default for ColorSwatchStyle {
216 fn default() -> Self {
217 Self {
218 size: vec2(sizing::SMALL, sizing::SMALL),
219 circular: true,
220 selection_style: SelectionStyle::InnerRing,
221 }
222 }
223}
224
225impl ColorSwatchStyle {
226 pub fn small() -> Self {
228 Self::default()
229 }
230
231 pub fn grid() -> Self {
233 Self {
234 size: vec2(16.0, 16.0),
235 circular: true,
236 selection_style: SelectionStyle::InnerRing,
237 }
238 }
239
240 pub fn large() -> Self {
242 Self {
243 size: vec2(28.0, 28.0),
244 circular: true,
245 selection_style: SelectionStyle::InnerRing,
246 }
247 }
248}
249
250pub struct ColorSwatch<'a> {
252 color: Color32,
253 tooltip: &'a str,
254 selected: bool,
255 style: ColorSwatchStyle,
256}
257
258impl<'a> ColorSwatch<'a> {
259 pub fn new(color: Color32, tooltip: &'a str) -> Self {
261 Self {
262 color,
263 tooltip,
264 selected: false,
265 style: ColorSwatchStyle::default(),
266 }
267 }
268
269 pub fn selected(mut self, selected: bool) -> Self {
271 self.selected = selected;
272 self
273 }
274
275 pub fn style(mut self, style: ColorSwatchStyle) -> Self {
277 self.style = style;
278 self
279 }
280
281 pub fn grid(mut self) -> Self {
283 self.style = ColorSwatchStyle::grid();
284 self
285 }
286
287 pub fn show(self, ui: &mut Ui) -> (bool, Rect) {
289 let (rect, response) = ui.allocate_exact_size(self.style.size, Sense::click());
290
291 if ui.is_rect_visible(rect) {
292 let center = rect.center();
293 let radius = rect.width().min(rect.height()) / 2.0;
294
295 if self.style.circular {
296 ui.painter().circle_filled(center, radius, self.color);
298
299 if self.selected && self.style.selection_style == SelectionStyle::InnerRing {
300 ui.painter().circle_stroke(
302 center,
303 radius - 3.0,
304 Stroke::new(2.0, Color32::from_gray(30)),
305 );
306 } else if self.selected && self.style.selection_style == SelectionStyle::OuterBorder
307 {
308 ui.painter()
309 .circle_stroke(center, radius, Stroke::new(2.0, theme::ACCENT));
310 }
311 } else {
312 ui.painter().rect_filled(
314 rect,
315 CornerRadius::same(sizing::CORNER_RADIUS),
316 self.color,
317 );
318
319 if self.selected {
320 ui.painter().rect_stroke(
321 rect,
322 CornerRadius::same(sizing::CORNER_RADIUS),
323 Stroke::new(2.0, Color32::from_gray(30)),
324 StrokeKind::Inside,
325 );
326 }
327 }
328 }
329
330 let clicked = response.clicked();
331 response.on_hover_text(self.tooltip).on_hover_cursor(CursorIcon::PointingHand);
332 (clicked, rect)
333 }
334}
335
336pub struct ColorSwatchWithWheel<'a> {
338 color: Color32,
339 tooltip: &'a str,
340 size: Vec2,
341}
342
343impl<'a> ColorSwatchWithWheel<'a> {
344 pub fn new(color: Color32, tooltip: &'a str) -> Self {
346 Self {
347 color,
348 tooltip,
349 size: vec2(sizing::SMALL, sizing::SMALL),
350 }
351 }
352
353 pub fn size(mut self, size: Vec2) -> Self {
355 self.size = size;
356 self
357 }
358
359 pub fn show(self, ui: &mut Ui) -> (bool, Rect) {
361 let (rect, response) = ui.allocate_exact_size(self.size, Sense::click());
362
363 if ui.is_rect_visible(rect) {
364 let center = rect.center();
365 let outer_radius = rect.width().min(rect.height()) / 2.0;
366 let ring_width = 3.0;
367 let inner_radius = outer_radius - ring_width;
368
369 let num_segments = 32;
371 for i in 0..num_segments {
372 let angle1 = (i as f32 / num_segments as f32) * std::f32::consts::TAU;
373 let angle2 = ((i + 1) as f32 / num_segments as f32) * std::f32::consts::TAU;
374
375 let hue = i as f32 / num_segments as f32;
376 let hue_color = hue_to_rgb(hue);
377
378 let p1 = Pos2::new(
379 center.x + outer_radius * angle1.cos(),
380 center.y + outer_radius * angle1.sin(),
381 );
382 let p2 = Pos2::new(
383 center.x + outer_radius * angle2.cos(),
384 center.y + outer_radius * angle2.sin(),
385 );
386 let p3 = Pos2::new(
387 center.x + inner_radius * angle2.cos(),
388 center.y + inner_radius * angle2.sin(),
389 );
390 let p4 = Pos2::new(
391 center.x + inner_radius * angle1.cos(),
392 center.y + inner_radius * angle1.sin(),
393 );
394
395 ui.painter().add(egui::Shape::convex_polygon(
396 vec![p1, p2, p3, p4],
397 hue_color,
398 Stroke::NONE,
399 ));
400 }
401
402 ui.painter()
404 .circle_filled(center, inner_radius, Color32::from_gray(30));
405
406 ui.painter()
408 .circle_filled(center, inner_radius - 2.0, self.color);
409 }
410
411 let clicked = response.clicked();
412 response.on_hover_text(self.tooltip).on_hover_cursor(CursorIcon::PointingHand);
413 (clicked, rect)
414 }
415}
416
417pub struct NoColorSwatch<'a> {
419 tooltip: &'a str,
420 selected: bool,
421 size: Vec2,
422}
423
424impl<'a> NoColorSwatch<'a> {
425 pub fn new(tooltip: &'a str) -> Self {
427 Self {
428 tooltip,
429 selected: false,
430 size: vec2(sizing::SMALL, sizing::SMALL),
431 }
432 }
433
434 pub fn selected(mut self, selected: bool) -> Self {
436 self.selected = selected;
437 self
438 }
439
440 pub fn grid(mut self) -> Self {
442 self.size = vec2(16.0, 16.0);
443 self
444 }
445
446 pub fn show(self, ui: &mut Ui) -> bool {
448 let (rect, response) = ui.allocate_exact_size(self.size, Sense::click());
449
450 if ui.is_rect_visible(rect) {
451 let center = rect.center();
452 let radius = rect.width().min(rect.height()) / 2.0;
453
454 ui.painter().circle_filled(center, radius, Color32::WHITE);
456 ui.painter()
457 .circle_stroke(center, radius, Stroke::new(1.0, Color32::from_gray(200)));
458
459 let offset = radius * 0.6;
461 ui.painter().line_segment(
462 [
463 Pos2::new(center.x - offset, center.y + offset),
464 Pos2::new(center.x + offset, center.y - offset),
465 ],
466 Stroke::new(2.0, Color32::from_rgb(239, 68, 68)),
467 );
468
469 if self.selected {
470 ui.painter().circle_stroke(
471 center,
472 radius - 3.0,
473 Stroke::new(2.0, Color32::from_gray(30)),
474 );
475 }
476 }
477
478 let clicked = response.clicked();
479 response.on_hover_text(self.tooltip).on_hover_cursor(CursorIcon::PointingHand);
480 clicked
481 }
482}
483
484pub struct ColorGrid<'a> {
486 current_color: Color32,
487 title: &'a str,
488 shade_indices: &'a [usize],
490 position: ColorGridPosition,
491}
492
493#[derive(Clone, Copy, Default)]
495pub enum ColorGridPosition {
496 #[default]
498 Below,
499 Above,
501}
502
503impl<'a> ColorGrid<'a> {
504 pub fn new(current_color: Color32, title: &'a str) -> Self {
506 Self {
507 current_color,
508 title,
509 shade_indices: &[1, 2, 3, 4, 5, 6, 7, 8, 9], position: ColorGridPosition::Below,
511 }
512 }
513
514 pub fn shades(mut self, indices: &'a [usize]) -> Self {
516 self.shade_indices = indices;
517 self
518 }
519
520 pub fn above(mut self) -> Self {
522 self.position = ColorGridPosition::Above;
523 self
524 }
525
526 pub fn below(mut self) -> Self {
528 self.position = ColorGridPosition::Below;
529 self
530 }
531
532 pub fn show(self, ctx: &egui::Context, anchor_rect: Rect) -> Option<Color32> {
535 let mut selected = None;
536
537 let grid_height = 220.0;
539 let pos = match self.position {
540 ColorGridPosition::Below => {
541 Pos2::new(anchor_rect.left() - 100.0, anchor_rect.bottom() + 8.0)
542 }
543 ColorGridPosition::Above => {
544 Pos2::new(anchor_rect.left() - 50.0, anchor_rect.top() - grid_height - 8.0)
545 }
546 };
547
548 egui::Area::new(egui::Id::new("color_grid"))
549 .fixed_pos(pos)
550 .order(egui::Order::Foreground)
551 .show(ctx, |ui| {
552 crate::menu::panel_frame().show(ui, |ui| {
553 ui.vertical(|ui| {
554 ui.spacing_mut().item_spacing = vec2(0.0, 4.0);
555
556 ui.label(
558 egui::RichText::new(self.title)
559 .size(12.0)
560 .color(theme::TEXT_MUTED),
561 );
562
563 ui.add_space(4.0);
564
565 #[derive(Clone, Copy)]
569 enum QuickColor {
570 None,
571 Black,
572 White,
573 Tailwind(usize), }
575 let quick_colors: [QuickColor; 9] = [
576 QuickColor::None,
577 QuickColor::Black,
578 QuickColor::White,
579 QuickColor::Tailwind(10), QuickColor::Tailwind(0), QuickColor::Tailwind(6), QuickColor::Tailwind(2), QuickColor::Tailwind(13), QuickColor::Tailwind(17), ];
586 let quick_tooltips = ["None", "Black", "White", "Blue", "Red", "Emerald", "Amber", "Purple", "Slate"];
587
588 for (row_idx, &shade_idx) in self.shade_indices.iter().enumerate() {
589 ui.horizontal(|ui| {
590 ui.spacing_mut().item_spacing = vec2(2.0, 0.0);
591
592 for tw_color in TAILWIND_COLORS.iter() {
594 let color = tw_color.shades[shade_idx];
595 let is_selected = colors_match(self.current_color, color);
596 let tooltip =
597 format!("{} {}", tw_color.name, SHADE_LABELS[shade_idx]);
598 let (clicked, _) = ColorSwatch::new(color, &tooltip)
599 .selected(is_selected)
600 .grid()
601 .show(ui);
602 if clicked {
603 selected = Some(color);
604 }
605 }
606
607 if row_idx < quick_colors.len() {
609 ui.add_space(4.0);
610
611 match quick_colors[row_idx] {
613 QuickColor::None => {
614 let is_selected = self.current_color.a() == 0;
616 if NoColorSwatch::new(quick_tooltips[row_idx])
617 .selected(is_selected)
618 .grid()
619 .show(ui)
620 {
621 selected = Some(Color32::TRANSPARENT);
622 }
623 }
624 QuickColor::Black => {
625 let is_selected = colors_match(self.current_color, Color32::BLACK);
626 let (clicked, _) = ColorSwatch::new(Color32::BLACK, quick_tooltips[row_idx])
627 .selected(is_selected)
628 .grid()
629 .show(ui);
630 if clicked {
631 selected = Some(Color32::BLACK);
632 }
633 }
634 QuickColor::White => {
635 let size = vec2(16.0, 16.0);
637 let (rect, response) = ui.allocate_exact_size(size, Sense::click());
638 if ui.is_rect_visible(rect) {
639 let center = rect.center();
640 let radius = 8.0;
641 ui.painter().circle_filled(center, radius, Color32::WHITE);
642 ui.painter().circle_stroke(center, radius, Stroke::new(1.0, Color32::from_gray(180)));
643 let is_selected = colors_match(self.current_color, Color32::WHITE);
644 if is_selected {
645 ui.painter().circle_stroke(center, radius - 3.0, Stroke::new(2.0, Color32::from_gray(30)));
646 }
647 }
648 let clicked = response.clicked();
649 response.on_hover_text(quick_tooltips[row_idx]).on_hover_cursor(CursorIcon::PointingHand);
650 if clicked {
651 selected = Some(Color32::WHITE);
652 }
653 }
654 QuickColor::Tailwind(idx) => {
655 let color = TAILWIND_COLORS[idx].shades[5]; let is_selected = colors_match(self.current_color, color);
657 let (clicked, _) = ColorSwatch::new(color, quick_tooltips[row_idx])
658 .selected(is_selected)
659 .grid()
660 .show(ui);
661 if clicked {
662 selected = Some(color);
663 }
664 }
665 }
666 }
667 });
668 }
669 });
670 });
671 });
672
673 selected
674 }
675}
676
677pub fn colors_match(a: Color32, b: Color32) -> bool {
679 a.r() == b.r() && a.g() == b.g() && a.b() == b.b()
680}
681
682pub fn hue_to_rgb(hue: f32) -> Color32 {
684 let h = hue * 6.0;
685 let c = 1.0_f32;
686 let x = c * (1.0 - (h % 2.0 - 1.0).abs());
687
688 let (r, g, b) = match h as i32 {
689 0 => (c, x, 0.0),
690 1 => (x, c, 0.0),
691 2 => (0.0, c, x),
692 3 => (0.0, x, c),
693 4 => (x, 0.0, c),
694 _ => (c, 0.0, x),
695 };
696
697 Color32::from_rgb((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8)
698}
699
700pub fn parse_css_color(color: &str) -> Color32 {
702 if color.starts_with('#') && color.len() == 7 {
703 let r = u8::from_str_radix(&color[1..3], 16).unwrap_or(128);
704 let g = u8::from_str_radix(&color[3..5], 16).unwrap_or(128);
705 let b = u8::from_str_radix(&color[5..7], 16).unwrap_or(128);
706 Color32::from_rgb(r, g, b)
707 } else {
708 Color32::from_rgb(128, 128, 128)
709 }
710}