Skip to main content

egui_charts/theme/
presets.rs

1//! The dark, light and high-contrast themes.
2
3use egui::Color32;
4
5use super::{ChartColors, Radii, Spacing, TextSizes, Theme, UiColors};
6
7const fn rgb(r: u8, g: u8, b: u8) -> Color32 {
8    Color32::from_rgb(r, g, b)
9}
10
11/// Every preset with its name: "dark", "light" and "high-contrast".
12pub fn presets() -> [(&'static str, Theme); 3] {
13    [
14        ("dark", Theme::dark()),
15        ("light", Theme::light()),
16        ("high-contrast", Theme::high_contrast()),
17    ]
18}
19
20impl Theme {
21    /// Dark UI and chart. This is the default theme.
22    pub fn dark() -> Self {
23        let accent = rgb(41, 98, 255);
24        Self {
25            dark: true,
26            ui: UiColors {
27                panel_bg: Color32::BLACK,
28                panel_bg_secondary: rgb(55, 59, 70),
29                panel_bg_floating: rgb(42, 46, 57),
30                modal_overlay: Color32::from_black_alpha(180),
31                text: rgb(219, 219, 219),
32                text_secondary: rgb(184, 184, 184),
33                text_muted: rgb(165, 165, 165),
34                text_disabled: rgb(87, 87, 87),
35                border: rgb(46, 46, 46),
36                border_control: rgb(144, 144, 144),
37                btn_bg_hover: rgb(32, 32, 32),
38                btn_bg_active: rgb(48, 48, 48),
39                selection_bg: accent,
40                selection_text: Color32::WHITE,
41                focus: Color32::WHITE,
42                accent,
43                accent_hover: rgb(35, 88, 230),
44                accent_active: rgb(31, 78, 205),
45                accent_text: Color32::WHITE,
46                danger_bg: rgb(211, 47, 47),
47                danger_bg_hover: rgb(198, 40, 40),
48                danger_bg_active: rgb(183, 28, 28),
49                danger_text: Color32::WHITE,
50                success: rgb(88, 187, 92),
51                take_profit: rgb(0, 166, 147),
52                stop_loss: rgb(255, 166, 0),
53                warning: rgb(255, 152, 0),
54                error: rgb(255, 130, 125),
55                info: rgb(100, 181, 246),
56            },
57            chart: ChartColors {
58                bg: Color32::BLACK,
59                bg_axis: Color32::BLACK,
60                bg_tooltip: rgb(55, 59, 70),
61                grid_line: rgb(34, 34, 34),
62                grid_line_major: Color32::from_rgba_unmultiplied(255, 255, 255, 18),
63                axis_text: Color32::WHITE,
64                axis_text_secondary: rgb(184, 184, 184),
65                crosshair_line: rgb(119, 119, 119),
66                crosshair_label_bg: rgb(55, 59, 70),
67                crosshair_label_text: rgb(209, 212, 220),
68                candle_up: rgb(8, 153, 129),
69                candle_down: rgb(242, 54, 69),
70                order_notable: rgb(255, 193, 7),
71                order_large: rgb(179, 136, 255),
72                volume_alpha: 128,
73            },
74            text: TextSizes::default(),
75            spacing: Spacing::default(),
76            radius: Radii::default(),
77        }
78    }
79
80    /// Light UI and chart.
81    pub fn light() -> Self {
82        let dark = Self::dark();
83        Self {
84            dark: false,
85            ui: UiColors {
86                panel_bg: Color32::WHITE,
87                panel_bg_secondary: rgb(235, 235, 235),
88                panel_bg_floating: Color32::WHITE,
89                modal_overlay: Color32::from_black_alpha(96),
90                text: Color32::BLACK,
91                text_secondary: Color32::BLACK,
92                text_muted: rgb(105, 105, 105),
93                text_disabled: rgb(126, 126, 126),
94                border: rgb(235, 235, 235),
95                border_control: rgb(130, 130, 130),
96                btn_bg_hover: rgb(234, 234, 234),
97                btn_bg_active: rgb(214, 214, 214),
98                accent: rgb(41, 98, 255),
99                selection_bg: rgb(219, 229, 255),
100                selection_text: dark.ui.accent_active,
101                focus: dark.ui.accent_active,
102                success: rgb(40, 115, 45),
103                take_profit: rgb(0, 166, 147),
104                stop_loss: rgb(255, 166, 0),
105                warning: rgb(160, 75, 8),
106                error: rgb(198, 40, 40),
107                info: rgb(2, 119, 189),
108                ..dark.ui
109            },
110            chart: ChartColors {
111                bg: Color32::WHITE,
112                bg_axis: Color32::WHITE,
113                bg_tooltip: Color32::WHITE,
114                grid_line: rgb(229, 229, 229),
115                grid_line_major: rgb(229, 229, 229),
116                axis_text: Color32::BLACK,
117                axis_text_secondary: Color32::BLACK,
118                crosshair_line: rgb(116, 116, 116),
119                crosshair_label_bg: Color32::BLACK,
120                crosshair_label_text: rgb(219, 219, 219),
121                candle_up: rgb(8, 153, 129),
122                candle_down: rgb(242, 54, 69),
123                order_notable: rgb(180, 130, 0),
124                order_large: rgb(146, 74, 250),
125                volume_alpha: 128,
126            },
127            ..dark
128        }
129    }
130
131    /// Black surfaces with white text, for low vision and bright surroundings.
132    ///
133    /// Every text colour except disabled text reads at 7:1 or better on panels and
134    /// windows, and outlines and the focus ring at 4.5:1 or better.
135    pub fn high_contrast() -> Self {
136        let dark = Self::dark();
137        Self {
138            ui: UiColors {
139                panel_bg: Color32::BLACK,
140                panel_bg_secondary: rgb(18, 18, 18),
141                panel_bg_floating: rgb(10, 10, 10),
142                modal_overlay: Color32::TRANSPARENT,
143                text: Color32::WHITE,
144                text_secondary: rgb(235, 235, 235),
145                text_muted: rgb(200, 200, 200),
146                text_disabled: rgb(140, 140, 140),
147                border: rgb(150, 150, 150),
148                border_control: rgb(200, 200, 200),
149                btn_bg_hover: rgb(45, 45, 45),
150                btn_bg_active: rgb(70, 70, 70),
151                // A dark selection keeps the selection text, which egui also uses to outline a
152                // focused text input, light enough to read on the black panels.
153                selection_bg: rgb(41, 71, 102),
154                selection_text: Color32::WHITE,
155                focus: rgb(255, 214, 0),
156                // The shades get lighter, away from the black text on them.
157                accent: rgb(102, 178, 255),
158                accent_hover: rgb(140, 198, 255),
159                accent_active: rgb(179, 218, 255),
160                accent_text: Color32::BLACK,
161                danger_bg: rgb(255, 120, 120),
162                danger_bg_hover: rgb(255, 154, 154),
163                danger_bg_active: rgb(255, 188, 188),
164                danger_text: Color32::BLACK,
165                success: rgb(90, 220, 120),
166                take_profit: rgb(0, 166, 147),
167                stop_loss: rgb(255, 166, 0),
168                warning: rgb(255, 200, 60),
169                error: rgb(255, 170, 170),
170                info: rgb(120, 200, 255),
171            },
172            chart: ChartColors {
173                bg: Color32::BLACK,
174                bg_axis: Color32::BLACK,
175                bg_tooltip: rgb(10, 10, 10),
176                grid_line: Color32::from_white_alpha(40),
177                grid_line_major: Color32::from_white_alpha(64),
178                axis_text: Color32::WHITE,
179                axis_text_secondary: rgb(200, 200, 200),
180                crosshair_line: Color32::WHITE,
181                crosshair_label_bg: rgb(45, 45, 45),
182                crosshair_label_text: Color32::WHITE,
183                candle_up: rgb(0, 220, 130),
184                candle_down: rgb(255, 95, 95),
185                order_notable: dark.chart.order_notable,
186                order_large: dark.chart.order_large,
187                volume_alpha: 128,
188            },
189            ..dark
190        }
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197    use crate::theme::contrast;
198
199    /// Guards the order size tier colours of every preset: each reads at 3:1 or more on the chart
200    /// and the panel background, and stays at least 100 sRGB units from the candle colours and
201    /// from the other tier, so a marked order is neither faint nor taken for a candle.
202    #[test]
203    fn order_tiers_read_on_every_preset_and_differ_from_the_candles() {
204        let distance = |a: Color32, b: Color32| {
205            [(a.r(), b.r()), (a.g(), b.g()), (a.b(), b.b())]
206                .map(|(a, b)| (i32::from(a) - i32::from(b)).pow(2))
207                .iter()
208                .sum::<i32>()
209        };
210        for (name, theme) in presets() {
211            let chart = theme.chart;
212            for (tier, color, other) in [
213                ("notable", chart.order_notable, chart.order_large),
214                ("large", chart.order_large, chart.order_notable),
215            ] {
216                for (surface, bg) in [("chart", chart.bg), ("panel", theme.ui.panel_bg)] {
217                    let ratio = contrast(color, bg);
218                    assert!(
219                        ratio >= 3.0,
220                        "{name} {tier} {color:?} on the {surface} background: {ratio:.2}:1"
221                    );
222                }
223                for (role, near) in [
224                    ("up candle", chart.candle_up),
225                    ("down candle", chart.candle_down),
226                    ("other tier", other),
227                ] {
228                    let squared = distance(color, near);
229                    assert!(
230                        squared >= 100 * 100,
231                        "{name} {tier} {color:?} is {:.1} from the {role} {near:?}",
232                        f64::from(squared).sqrt()
233                    );
234                }
235            }
236        }
237    }
238}