Skip to main content

paiagram_core/
colors.rs

1//! The color definitions.
2
3use bevy::color::Srgba;
4use bevy::color::palettes::tailwind::*;
5use bevy::prelude::*;
6use egui::Color32;
7use egui::color_picker::{Alpha, color_picker_color32, show_color_at};
8use egui_i18n::tr;
9use serde::{Deserialize, Serialize};
10
11/// A color displayed in the application. This is used for stations, intervals, and trip classes.
12#[derive(Reflect, Debug, Clone, Copy, Serialize, Deserialize)]
13#[reflect(opaque, Serialize, Deserialize)]
14pub enum DisplayedColor {
15    /// A predefined colour
16    Predefined(PredefinedColor),
17    /// A custom colour defined using egui's [`egui::Color32`]
18    Custom(Color32),
19}
20
21impl DisplayedColor {
22    /// Generate a displayed color from a seed. The process is not randomized. The seed could be
23    /// anything that can be converted to [u8], e.g., a string.
24    pub fn from_seed(data: impl AsRef<[u8]>) -> Self {
25        let bytes = data.as_ref();
26        let mut sum = 0u8;
27        for byte in bytes.iter().copied() {
28            sum = sum.wrapping_add(byte);
29        }
30        Self::Predefined(PredefinedColor::from_index(sum as usize))
31    }
32}
33
34impl Default for DisplayedColor {
35    fn default() -> Self {
36        Self::Predefined(PredefinedColor::Neutral)
37    }
38}
39
40// this is copied from egui
41fn color_button(ui: &mut egui::Ui, color: Color32, open: bool) -> egui::Response {
42    let size = ui.spacing().interact_size;
43    let (rect, response) = ui.allocate_exact_size(size, egui::Sense::click());
44    response.widget_info(|| egui::WidgetInfo::new(egui::WidgetType::ColorButton));
45
46    if ui.is_rect_visible(rect) {
47        let visuals = if open {
48            &ui.visuals().widgets.open
49        } else {
50            ui.style().interact(&response)
51        };
52        let rect = rect.expand(visuals.expansion);
53
54        let stroke_width = 1.0;
55        show_color_at(ui.painter(), color, rect.shrink(stroke_width));
56
57        let corner_radius = visuals.corner_radius.at_most(2); // Can't do more rounding because the background grid doesn't do any rounding
58        ui.painter().rect_stroke(
59            rect,
60            corner_radius,
61            (stroke_width, visuals.bg_fill), /* Using fill for stroke is intentional, because
62                                              * default style has no
63                                              * border */
64            egui::StrokeKind::Inside,
65        );
66    }
67
68    response
69}
70
71impl egui::Widget for &mut DisplayedColor {
72    fn ui(self, ui: &mut egui::Ui) -> egui::Response {
73        let is_dark = ui.visuals().dark_mode;
74        let button_res = color_button(ui, self.get(is_dark), false);
75
76        let current_predefined = match *self {
77            DisplayedColor::Predefined(p) => Some(p),
78            DisplayedColor::Custom(_) => None,
79        };
80
81        egui::Popup::menu(&button_res)
82            .close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside)
83            .show(|ui| {
84                ui.horizontal(|ui| {
85                    ui.vertical(|ui| {
86                        ui.label("Predefined");
87                        ui.set_max_width(200.0);
88                        ui.horizontal_wrapped(|ui| {
89                            ui.style_mut().spacing.item_spacing = egui::Vec2::splat(4.0);
90                            for predefined in PredefinedColor::ALL {
91                                let color = predefined.get(is_dark);
92                                let is_selected = current_predefined == Some(predefined);
93                                let button = egui::Button::new("")
94                                    .fill(color)
95                                    .min_size(egui::vec2(24.0, 24.0))
96                                    .stroke(if is_selected {
97                                        ui.visuals().selection.stroke
98                                    } else {
99                                        ui.visuals().widgets.inactive.bg_stroke
100                                    });
101
102                                if ui.add(button).clicked() {
103                                    *self = DisplayedColor::Predefined(predefined);
104                                }
105                            }
106                        });
107                    });
108                    ui.separator();
109                    ui.vertical(|ui| {
110                        ui.label("Custom");
111                        let mut custom_color = match *self {
112                            DisplayedColor::Custom(c) => c,
113                            DisplayedColor::Predefined(p) => p.get(is_dark),
114                        };
115                        if color_picker_color32(ui, &mut custom_color, Alpha::Opaque) {
116                            *self = DisplayedColor::Custom(custom_color);
117                        }
118                    });
119                })
120            });
121        button_res
122    }
123}
124
125impl DisplayedColor {
126    /// get the color as [`egui::Color32`]
127    pub fn get(self, is_dark: bool) -> Color32 {
128        match self {
129            Self::Predefined(p) => p.get(is_dark),
130            Self::Custom(c) => c,
131        }
132    }
133}
134
135/// Tailwind CSS predefined colors used in the program.
136#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
137pub enum PredefinedColor {
138    Red,
139    Orange,
140    Amber,
141    Yellow,
142    Lime,
143    Green,
144    Emerald,
145    Teal,
146    Cyan,
147    Sky,
148    Blue,
149    Indigo,
150    Violet,
151    Purple,
152    Fuchsia,
153    Pink,
154    Rose,
155    Slate,
156    Gray,
157    Zinc,
158    Neutral,
159    Stone,
160}
161
162impl PredefinedColor {
163    pub const ALL: [Self; 22] = [
164        Self::Red,
165        Self::Orange,
166        Self::Amber,
167        Self::Yellow,
168        Self::Lime,
169        Self::Green,
170        Self::Emerald,
171        Self::Teal,
172        Self::Cyan,
173        Self::Sky,
174        Self::Blue,
175        Self::Indigo,
176        Self::Violet,
177        Self::Purple,
178        Self::Fuchsia,
179        Self::Pink,
180        Self::Rose,
181        Self::Slate,
182        Self::Gray,
183        Self::Zinc,
184        Self::Neutral,
185        Self::Stone,
186    ];
187
188    /// Select a color given the index. The index could be any number.
189    pub fn from_index(i: usize) -> Self {
190        Self::ALL[i % Self::ALL.len()]
191    }
192
193    /// The (translated) name of the colour
194    #[rustfmt::skip]
195    pub fn name(self) -> impl AsRef<str> {
196        match self {
197            Self::Red       => tr!("colour-red"),
198            Self::Orange    => tr!("colour-orange"),
199            Self::Amber     => tr!("colour-amber"),
200            Self::Yellow    => tr!("colour-yellow"),
201            Self::Lime      => tr!("colour-lime"),
202            Self::Green     => tr!("colour-green"),
203            Self::Emerald   => tr!("colour-emerald"),
204            Self::Teal      => tr!("colour-teal"),
205            Self::Cyan      => tr!("colour-cyan"),
206            Self::Sky       => tr!("colour-sky"),
207            Self::Blue      => tr!("colour-blue"),
208            Self::Indigo    => tr!("colour-indigo"),
209            Self::Violet    => tr!("colour-violet"),
210            Self::Purple    => tr!("colour-purple"),
211            Self::Fuchsia   => tr!("colour-fuchsia"),
212            Self::Pink      => tr!("colour-pink"),
213            Self::Rose      => tr!("colour-rose"),
214            Self::Slate     => tr!("colour-slate"),
215            Self::Gray      => tr!("colour-gray"),
216            Self::Zinc      => tr!("colour-zinc"),
217            Self::Neutral   => tr!("colour-neutral"),
218            Self::Stone     => tr!("colour-stone"),
219        }
220    }
221
222    /// Get the color given the current UI theme. Returns the lighter 400 varation if the theme is
223    /// dark, and returns the (usually) darker 700 variation if the theme is light.
224    pub const fn get(self, is_dark: bool) -> Color32 {
225        #[rustfmt::skip]
226        let c = match (self, is_dark) {
227            (Self::Red, true)       => RED_400,
228            (Self::Red, false)      => RED_700,
229            (Self::Orange, true)    => ORANGE_400,
230            (Self::Orange, false)   => ORANGE_700,
231            (Self::Amber, true)     => AMBER_400,
232            (Self::Amber, false)    => AMBER_700,
233            (Self::Yellow, true)    => YELLOW_400,
234            (Self::Yellow, false)   => YELLOW_700,
235            (Self::Lime, true)      => LIME_400,
236            (Self::Lime, false)     => LIME_700,
237            (Self::Green, true)     => GREEN_400,
238            (Self::Green, false)    => GREEN_700,
239            (Self::Emerald, true)   => EMERALD_400,
240            (Self::Emerald, false)  => EMERALD_700,
241            (Self::Teal, true)      => TEAL_400,
242            (Self::Teal, false)     => TEAL_700,
243            (Self::Cyan, true)      => CYAN_400,
244            (Self::Cyan, false)     => CYAN_700,
245            (Self::Sky, true)       => SKY_400,
246            (Self::Sky, false)      => SKY_700,
247            (Self::Blue, true)      => BLUE_400,
248            (Self::Blue, false)     => BLUE_700,
249            (Self::Indigo, true)    => INDIGO_400,
250            (Self::Indigo, false)   => INDIGO_700,
251            (Self::Violet, true)    => VIOLET_400,
252            (Self::Violet, false)   => VIOLET_700,
253            (Self::Purple, true)    => PURPLE_400,
254            (Self::Purple, false)   => PURPLE_700,
255            (Self::Fuchsia, true)   => FUCHSIA_400,
256            (Self::Fuchsia, false)  => FUCHSIA_700,
257            (Self::Pink, true)      => PINK_400,
258            (Self::Pink, false)     => PINK_700,
259            (Self::Rose, true)      => ROSE_400,
260            (Self::Rose, false)     => ROSE_700,
261            (Self::Slate, true)     => SLATE_400,
262            (Self::Slate, false)    => SLATE_700,
263            (Self::Gray, true)      => GRAY_400,
264            (Self::Gray, false)     => GRAY_700,
265            (Self::Zinc, true)      => ZINC_400,
266            (Self::Zinc, false)     => ZINC_700,
267            (Self::Neutral, true)   => NEUTRAL_400,
268            (Self::Neutral, false)  => NEUTRAL_700,
269            (Self::Stone, true)     => STONE_400,
270            (Self::Stone, false)    => STONE_700,
271        };
272        translate_srgba_to_color32(c)
273    }
274}
275
276/// Translate a bevy [`Srgba`] to egui [`Color32`]
277pub const fn translate_srgba_to_color32(c: Srgba) -> Color32 {
278    Color32::from_rgba_unmultiplied_const(
279        (c.red * 256.0) as u8,
280        (c.green * 256.0) as u8,
281        (c.blue * 256.0) as u8,
282        (c.alpha * 256.0) as u8,
283    )
284}