Krolyxon
Commit 8756c54

Branches

Tags

Refs →

bench51cratesbench51-guisrctheme.rs

293 lines · 9.8 KB · Rust

1//! Nothing design tokens mapped onto egui: palettes for both themes, the
2//! bundled fonts, and text helpers that keep the type budget in one place.
3
4use std::sync::Arc;
5
6use bench51_core::syntax::Tok;
7use eframe::egui::{
8 self, Color32, Context, CornerRadius, FontData, FontDefinitions, FontFamily, FontId, RichText,
9 Shadow, Stroke, Style, TextStyle, Theme, Visuals,
10 epaint::text::{FontTweak, VariationCoords},
11};
12
13/// The colour tokens of one theme (`references/tokens.md` §2).
14pub struct Palette {
15 /// Page background: OLED black / warm off-white paper.
16 pub black: Color32,
17 pub surface: Color32,
18 pub surface_raised: Color32,
19 pub border: Color32,
20 pub border_visible: Color32,
21 pub text_disabled: Color32,
22 pub text_secondary: Color32,
23 pub text_primary: Color32,
24 pub text_display: Color32,
25 pub interactive: Color32,
26}
27
28pub const DARK: Palette = Palette {
29 black: Color32::from_rgb(0x00, 0x00, 0x00),
30 surface: Color32::from_rgb(0x11, 0x11, 0x11),
31 surface_raised: Color32::from_rgb(0x1A, 0x1A, 0x1A),
32 border: Color32::from_rgb(0x22, 0x22, 0x22),
33 border_visible: Color32::from_rgb(0x33, 0x33, 0x33),
34 text_disabled: Color32::from_rgb(0x66, 0x66, 0x66),
35 text_secondary: Color32::from_rgb(0x99, 0x99, 0x99),
36 text_primary: Color32::from_rgb(0xE8, 0xE8, 0xE8),
37 text_display: Color32::from_rgb(0xFF, 0xFF, 0xFF),
38 interactive: Color32::from_rgb(0x5B, 0x9B, 0xF6),
39};
40
41pub const LIGHT: Palette = Palette {
42 black: Color32::from_rgb(0xF5, 0xF5, 0xF5),
43 surface: Color32::from_rgb(0xFF, 0xFF, 0xFF),
44 surface_raised: Color32::from_rgb(0xF0, 0xF0, 0xF0),
45 border: Color32::from_rgb(0xE8, 0xE8, 0xE8),
46 border_visible: Color32::from_rgb(0xCC, 0xCC, 0xCC),
47 text_disabled: Color32::from_rgb(0x99, 0x99, 0x99),
48 text_secondary: Color32::from_rgb(0x66, 0x66, 0x66),
49 text_primary: Color32::from_rgb(0x1A, 0x1A, 0x1A),
50 text_display: Color32::from_rgb(0x00, 0x00, 0x00),
51 interactive: Color32::from_rgb(0x00, 0x7A, 0xFF),
52};
53
54/// Signal red: breakpoints, errors, the PC bar. Never decorative.
55pub const ACCENT: Color32 = Color32::from_rgb(0xD7, 0x19, 0x21);
56/// Values that changed since the last step.
57pub const WARNING: Color32 = Color32::from_rgb(0xD4, 0xA8, 0x43);
58
59/// The three type sizes: labels, body/data, and the one hero per screen.
60pub const LABEL: f32 = 11.0;
61pub const BODY: f32 = 14.0;
62pub const HERO: f32 = 44.0;
63/// Doto at welcome-screen scale.
64pub const HERO_XL: f32 = 72.0;
65/// Tracking for ALL CAPS labels (0.08em at 11px).
66pub const LABEL_TRACKING: f32 = 0.9;
67
68pub fn palette(ctx: &Context) -> &'static Palette {
69 match ctx.theme() {
70 Theme::Dark => &DARK,
71 Theme::Light => &LIGHT,
72 }
73}
74
75pub fn mono_bold() -> FontFamily {
76 FontFamily::Name("mono-bold".into())
77}
78
79pub fn grotesk_medium() -> FontFamily {
80 FontFamily::Name("grotesk-medium".into())
81}
82
83pub fn doto() -> FontFamily {
84 FontFamily::Name("doto".into())
85}
86
87pub fn mono(size: f32) -> FontId {
88 FontId::monospace(size)
89}
90
91impl Palette {
92 /// Instrument-panel label: Space Mono, ALL CAPS, tracked, secondary grey.
93 pub fn label(&self, text: impl AsRef<str>) -> RichText {
94 RichText::new(text.as_ref().to_uppercase())
95 .font(mono(LABEL))
96 .extra_letter_spacing(LABEL_TRACKING)
97 .color(self.text_secondary)
98 }
99
100 /// Data value in Space Mono at body size.
101 pub fn data(&self, text: impl Into<String>) -> RichText {
102 RichText::new(text)
103 .font(mono(BODY))
104 .color(self.text_primary)
105 }
106
107 /// Dot-matrix hero number. One per screen.
108 pub fn hero(&self, text: impl Into<String>, size: f32) -> RichText {
109 RichText::new(text)
110 .font(FontId::new(size, doto()))
111 .color(self.text_display)
112 }
113
114 /// Syntax colours: greyscale first, hierarchy by brightness and weight.
115 pub fn syntax(&self, tok: Tok) -> (Color32, bool) {
116 match tok {
117 Tok::Label => (self.text_display, true),
118 Tok::Mnemonic => (self.text_display, false),
119 Tok::Register | Tok::Sfr | Tok::Plain => (self.text_primary, false),
120 Tok::Number | Tok::Str | Tok::Directive => (self.text_secondary, false),
121 Tok::Comment => (self.text_disabled, false),
122 }
123 }
124}
125
126fn fonts() -> FontDefinitions {
127 let mut defs = FontDefinitions::default();
128 let mut add = |name: &str, bytes: &'static [u8], tweak: FontTweak| {
129 defs.font_data.insert(
130 name.to_owned(),
131 Arc::new(FontData::from_static(bytes).tweak(tweak)),
132 );
133 };
134 add(
135 "space-grotesk",
136 include_bytes!("../assets/fonts/SpaceGrotesk-Regular.ttf"),
137 FontTweak::default(),
138 );
139 add(
140 "space-grotesk-medium",
141 include_bytes!("../assets/fonts/SpaceGrotesk-Medium.ttf"),
142 FontTweak::default(),
143 );
144 add(
145 "space-mono",
146 include_bytes!("../assets/fonts/SpaceMono-Regular.ttf"),
147 FontTweak::default(),
148 );
149 add(
150 "space-mono-bold",
151 include_bytes!("../assets/fonts/SpaceMono-Bold.ttf"),
152 FontTweak::default(),
153 );
154 // Round dots, bold enough to read as a display face.
155 add(
156 "doto",
157 include_bytes!("../assets/fonts/Doto[ROND,wght].ttf"),
158 FontTweak {
159 coords: VariationCoords::new([(b"ROND", 100.0), (b"wght", 800.0)]),
160 ..FontTweak::default()
161 },
162 );
163
164 // egui's own fonts stay behind ours as fallbacks for symbols.
165 let prop_fallback = defs.families[&FontFamily::Proportional].clone();
166 let mono_fallback = defs.families[&FontFamily::Monospace].clone();
167 let with = |first: &[&str], rest: &[String]| -> Vec<String> {
168 first
169 .iter()
170 .map(|s| s.to_string())
171 .chain(rest.iter().cloned())
172 .collect()
173 };
174 defs.families.insert(
175 FontFamily::Proportional,
176 with(&["space-grotesk"], &prop_fallback),
177 );
178 defs.families
179 .insert(FontFamily::Monospace, with(&["space-mono"], &mono_fallback));
180 defs.families
181 .insert(mono_bold(), with(&["space-mono-bold"], &mono_fallback));
182 defs.families.insert(
183 grotesk_medium(),
184 with(&["space-grotesk-medium"], &prop_fallback),
185 );
186 defs.families
187 .insert(doto(), with(&["doto", "space-mono"], &mono_fallback));
188 defs
189}
190
191fn visuals(p: &Palette, dark: bool) -> Visuals {
192 let mut v = if dark {
193 Visuals::dark()
194 } else {
195 Visuals::light()
196 };
197 let technical = CornerRadius::same(4);
198 v.panel_fill = p.black;
199 v.window_fill = p.surface;
200 v.window_stroke = Stroke::new(1.0, p.border_visible);
201 v.window_shadow = Shadow::NONE;
202 v.popup_shadow = Shadow::NONE;
203 v.window_corner_radius = CornerRadius::same(16);
204 v.menu_corner_radius = CornerRadius::same(8);
205 v.window_highlight_topmost = false;
206 v.extreme_bg_color = p.surface;
207 v.text_edit_bg_color = Some(p.surface);
208 v.faint_bg_color = p.surface_raised;
209 v.code_bg_color = p.surface_raised;
210 v.hyperlink_color = p.interactive;
211 v.warn_fg_color = WARNING;
212 v.error_fg_color = ACCENT;
213 v.selection.bg_fill = if dark {
214 Color32::from_white_alpha(46)
215 } else {
216 Color32::from_black_alpha(36)
217 };
218 v.selection.stroke = Stroke::new(1.0, p.text_display);
219 v.text_cursor.stroke = Stroke::new(2.0, p.text_display);
220 v.striped = false;
221 v.indent_has_left_vline = false;
222 v.collapsing_header_frame = false;
223 v.slider_trailing_fill = true;
224 v.resize_corner_size = 8.0;
225
226 let w = &mut v.widgets;
227 w.noninteractive.bg_fill = p.surface;
228 w.noninteractive.weak_bg_fill = p.surface;
229 w.noninteractive.bg_stroke = Stroke::new(1.0, p.border);
230 w.noninteractive.fg_stroke = Stroke::new(1.0, p.text_primary);
231 w.noninteractive.corner_radius = technical;
232
233 w.inactive.bg_fill = p.surface_raised;
234 w.inactive.weak_bg_fill = Color32::TRANSPARENT;
235 w.inactive.bg_stroke = Stroke::new(1.0, p.border_visible);
236 w.inactive.fg_stroke = Stroke::new(1.0, p.text_primary);
237 w.inactive.corner_radius = technical;
238 w.inactive.expansion = 0.0;
239
240 // Hover: the border and text brighten. No scale, no glow.
241 w.hovered.bg_fill = p.surface_raised;
242 w.hovered.weak_bg_fill = p.surface_raised;
243 w.hovered.bg_stroke = Stroke::new(1.0, p.text_secondary);
244 w.hovered.fg_stroke = Stroke::new(1.5, p.text_display);
245 w.hovered.corner_radius = technical;
246 w.hovered.expansion = 0.0;
247
248 w.active.bg_fill = p.surface_raised;
249 w.active.weak_bg_fill = p.surface_raised;
250 w.active.bg_stroke = Stroke::new(1.0, p.text_primary);
251 w.active.fg_stroke = Stroke::new(2.0, p.text_display);
252 w.active.corner_radius = technical;
253 w.active.expansion = 0.0;
254
255 w.open = w.hovered;
256 v
257}
258
259fn style(theme: Theme) -> Style {
260 let (p, dark) = match theme {
261 Theme::Dark => (&DARK, true),
262 Theme::Light => (&LIGHT, false),
263 };
264 let mut s = Style {
265 visuals: visuals(p, dark),
266 ..Style::default()
267 };
268 s.text_styles = [
269 (TextStyle::Small, FontId::monospace(LABEL)),
270 (TextStyle::Body, FontId::proportional(BODY)),
271 (TextStyle::Button, FontId::monospace(12.0)),
272 (TextStyle::Heading, FontId::new(18.0, grotesk_medium())),
273 (TextStyle::Monospace, FontId::monospace(13.0)),
274 ]
275 .into();
276 s.spacing.item_spacing = egui::vec2(8.0, 4.0);
277 s.spacing.button_padding = egui::vec2(10.0, 4.0);
278 s.spacing.interact_size.y = 22.0;
279 s.spacing.menu_margin = egui::Margin::same(8);
280 s.spacing.window_margin = egui::Margin::same(16);
281 s.spacing.menu_spacing = 2.0;
282 s.animation_time = 0.15;
283 s.interaction.selectable_labels = false;
284 s.interaction.tooltip_delay = 0.4;
285 s
286}
287
288/// Installs the fonts and both themes on `ctx`.
289pub fn install(ctx: &Context) {
290 ctx.set_fonts(fonts());
291 ctx.set_style_of(Theme::Dark, style(Theme::Dark));
292 ctx.set_style_of(Theme::Light, style(Theme::Light));
293}