summaryrefslogtreecommitdiffstats
path: root/gui/src/theme.rs
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-01 19:58:59 +0100
committersalmonik <salmonik@tuta.io>2026-11-01 19:58:59 +0100
commit10ff558853f549d3f3c7c8432488bfad9da6973a (patch)
tree271b05fead384e2d91e5c8332fea9d00b540f0cc /gui/src/theme.rs
parentv4.0.7: finish the Phase 1 boundary with a command shell (diff)
downloadzagan-bet-10ff558853f549d3f3c7c8432488bfad9da6973a.tar.gz
zagan-bet-10ff558853f549d3f3c7c8432488bfad9da6973a.zip
v4.1.0: add the egui workspace member and the roulette style spike
Phase 3 of the GUI plan. gui/ is a separate package that depends on the core with default features off, so egui never enters the TUI or server build graph. It carries the theme tokens and two embedded fonts (Open Sans OFL, Hack MIT), paints the felt, chips, cursor and wheel from RouletteView, and composes the persistent header with the betting sidebar. The reference screen runs on demo state: arrow keys move the shared Cursor, clicks place chips, and SPIN animates a 2.4 s wheel that settles with the winning pocket under the pointer. No network until Phase 4. Verified x86_64-pc-windows-gnu cross-compile. Measured on Linux at 1080p with llvmpipe: 6.0 MB binary (4.7 MB on Windows), 52 ms to window, 137 MB idle RSS, 0% idle CPU.
Diffstat (limited to '')
-rw-r--r--gui/src/theme.rs211
1 files changed, 211 insertions, 0 deletions
diff --git a/gui/src/theme.rs b/gui/src/theme.rs
new file mode 100644
index 0000000..9270aa6
--- /dev/null
+++ b/gui/src/theme.rs
@@ -0,0 +1,211 @@
1//! Look and feel for the desktop frontend.
2//!
3//! The palette is derived from the terminal client's ANSI scheme so the two
4//! frontends read as the same product: near-black ground, low-contrast panels,
5//! cyan for focus, and the felt green reserved for the roulette table.
6//!
7//! egui's defaults are a debug tool's palette. Every colour, size and corner
8//! used by the app comes from this module; widgets never pick their own.
9
10use std::sync::Arc;
11
12use eframe::egui::style::WidgetVisuals;
13use eframe::egui::{
14 self, Color32, Context, CornerRadius, FontData, FontDefinitions, FontFamily, FontId, Margin,
15 RichText, Stroke, TextStyle, Theme, Visuals,
16};
17
18// ---------------------------------------------------------------------------
19// Colour tokens
20// ---------------------------------------------------------------------------
21
22/// Window ground. Everything else sits above this.
23pub const BG: Color32 = Color32::from_rgb(0x0a, 0x0e, 0x12);
24/// Cards, header, sidebar.
25pub const SURFACE: Color32 = Color32::from_rgb(0x11, 0x17, 0x1d);
26/// Inputs, pills, raised rows.
27pub const SURFACE_RAISED: Color32 = Color32::from_rgb(0x17, 0x1f, 0x27);
28/// Hover state.
29pub const SURFACE_HOVER: Color32 = Color32::from_rgb(0x1d, 0x27, 0x31);
30/// Hairlines and table borders.
31pub const LINE: Color32 = Color32::from_rgb(0x24, 0x2e, 0x38);
32/// Primary text.
33pub const TEXT: Color32 = Color32::from_rgb(0xd9, 0xe2, 0xea);
34/// Labels and secondary text.
35pub const TEXT_DIM: Color32 = Color32::from_rgb(0x8a, 0x98, 0xa6);
36/// Disabled text, captions.
37pub const TEXT_FAINT: Color32 = Color32::from_rgb(0x5b, 0x67, 0x73);
38/// Focus, cursor, selection.
39pub const ACCENT: Color32 = Color32::from_rgb(0x4f, 0xb8, 0xc9);
40/// Wins, "online", the spin button.
41pub const WIN: Color32 = Color32::from_rgb(0x4e, 0xc2, 0x7b);
42/// Losses and errors.
43pub const LOSS: Color32 = Color32::from_rgb(0xe0, 0x5a, 0x5a);
44/// Chips and pending bets.
45pub const GOLD: Color32 = Color32::from_rgb(0xe0, 0xb6, 0x4a);
46
47/// Roulette felt. Never used outside the table.
48pub const FELT: Color32 = Color32::from_rgb(0x0d, 0x5c, 0x39);
49pub const FELT_DARK: Color32 = Color32::from_rgb(0x09, 0x47, 0x2c);
50pub const FELT_LINE: Color32 = Color32::from_rgb(0x1f, 0x7d, 0x52);
51pub const FELT_TEXT: Color32 = Color32::from_rgb(0xec, 0xf7, 0xf0);
52pub const ROULETTE_RED: Color32 = Color32::from_rgb(0xb0, 0x31, 0x3a);
53pub const ROULETTE_BLACK: Color32 = Color32::from_rgb(0x15, 0x1b, 0x21);
54pub const ROULETTE_ZERO: Color32 = Color32::from_rgb(0x10, 0x6b, 0x45);
55
56// ---------------------------------------------------------------------------
57// Spacing, radius, type scale
58// ---------------------------------------------------------------------------
59
60pub const SPACE_1: f32 = 4.0;
61pub const SPACE_2: f32 = 8.0;
62pub const SPACE_3: f32 = 12.0;
63pub const SPACE_4: f32 = 16.0;
64pub const SPACE_5: f32 = 24.0;
65
66pub const RADIUS_SM: CornerRadius = CornerRadius::same(4);
67pub const RADIUS_MD: CornerRadius = CornerRadius::same(8);
68pub const RADIUS_LG: CornerRadius = CornerRadius::same(12);
69
70pub const FONT_BODY: f32 = 14.0;
71pub const FONT_SMALL: f32 = 12.0;
72pub const FONT_LABEL: f32 = 11.0;
73pub const FONT_TITLE: f32 = 24.0;
74pub const FONT_MONO: f32 = 16.0;
75
76// ---------------------------------------------------------------------------
77// Fonts
78// ---------------------------------------------------------------------------
79
80/// Open Sans, SIL OFL 1.1. See `assets/fonts/OFL-OpenSans.txt`.
81pub const FONT_SANS_BYTES: &[u8] = include_bytes!("../assets/fonts/OpenSans-Regular.ttf");
82/// Hack, MIT. See `assets/fonts/LICENSE-Hack.md`.
83pub const FONT_MONO_BYTES: &[u8] = include_bytes!("../assets/fonts/Hack-Regular.ttf");
84
85pub fn sans(size: f32) -> FontId {
86 FontId::new(size, FontFamily::Proportional)
87}
88
89pub fn mono(size: f32) -> FontId {
90 FontId::new(size, FontFamily::Monospace)
91}
92
93/// A small uppercase label, the terminal's dim section headers.
94pub fn label(text: &str) -> RichText {
95 RichText::new(text.to_uppercase())
96 .font(mono(FONT_LABEL))
97 .color(TEXT_DIM)
98}
99
100/// Group an amount in threes: 1234567 -> "1 234 567".
101pub fn group_digits(amount: i64) -> String {
102 let negative = amount < 0;
103 let digits = amount.unsigned_abs().to_string();
104 let mut grouped = String::with_capacity(digits.len() + digits.len() / 3);
105 for (index, character) in digits.chars().enumerate() {
106 if index > 0 && (digits.len() - index) % 3 == 0 {
107 grouped.push(' ');
108 }
109 grouped.push(character);
110 }
111 if negative {
112 format!("-{grouped}")
113 } else {
114 grouped
115 }
116}
117
118/// `1000` -> `"1 000 Kr"`.
119pub fn money(amount: i64) -> String {
120 format!("{} Kr", group_digits(amount))
121}
122
123// ---------------------------------------------------------------------------
124// Installation
125// ---------------------------------------------------------------------------
126
127/// Install the embedded fonts and the dark style. Call once, from the app
128/// constructor, before the first frame.
129pub fn install(ctx: &Context) {
130 let mut fonts = FontDefinitions::default();
131 fonts.font_data.insert(
132 "zagan-sans".to_owned(),
133 Arc::new(FontData::from_static(FONT_SANS_BYTES)),
134 );
135 fonts.font_data.insert(
136 "zagan-mono".to_owned(),
137 Arc::new(FontData::from_static(FONT_MONO_BYTES)),
138 );
139 fonts
140 .families
141 .get_mut(&FontFamily::Proportional)
142 .expect("proportional family exists")
143 .insert(0, "zagan-sans".to_owned());
144 fonts
145 .families
146 .get_mut(&FontFamily::Monospace)
147 .expect("monospace family exists")
148 .insert(0, "zagan-mono".to_owned());
149 ctx.set_fonts(fonts);
150
151 ctx.set_theme(Theme::Dark);
152 let mut style = (*ctx.global_style()).clone();
153 style.text_styles = [
154 (TextStyle::Heading, sans(FONT_TITLE)),
155 (TextStyle::Body, sans(FONT_BODY)),
156 (TextStyle::Button, sans(FONT_BODY)),
157 (TextStyle::Small, sans(FONT_SMALL)),
158 (TextStyle::Monospace, mono(FONT_MONO)),
159 ]
160 .into();
161 style.spacing.item_spacing = egui::vec2(SPACE_2, SPACE_2);
162 style.spacing.button_padding = egui::vec2(SPACE_3, SPACE_2);
163 style.spacing.interact_size = egui::vec2(36.0, 32.0);
164 style.spacing.window_margin = Margin::same(SPACE_3 as i8);
165 style.visuals = visuals();
166 ctx.set_global_style(style);
167}
168
169fn widget(
170 bg_fill: Color32,
171 bg_stroke: Color32,
172 fg: Color32,
173 corner_radius: CornerRadius,
174) -> WidgetVisuals {
175 WidgetVisuals {
176 bg_fill,
177 weak_bg_fill: bg_fill,
178 bg_stroke: Stroke::new(1.0, bg_stroke),
179 corner_radius,
180 fg_stroke: Stroke::new(1.0, fg),
181 expansion: 0.0,
182 }
183}
184
185fn visuals() -> Visuals {
186 let mut visuals = Visuals::dark();
187 visuals.override_text_color = Some(TEXT);
188 visuals.panel_fill = BG;
189 visuals.window_fill = SURFACE;
190 visuals.window_stroke = Stroke::new(1.0, LINE);
191 visuals.window_corner_radius = RADIUS_LG;
192 visuals.extreme_bg_color = BG;
193 visuals.faint_bg_color = SURFACE_RAISED;
194 visuals.code_bg_color = SURFACE_RAISED;
195 visuals.hyperlink_color = ACCENT;
196 visuals.warn_fg_color = GOLD;
197 visuals.error_fg_color = LOSS;
198 visuals.selection.bg_fill = ACCENT.gamma_multiply(0.30);
199 visuals.selection.stroke = Stroke::new(1.0, ACCENT);
200 visuals.widgets.noninteractive = widget(SURFACE, LINE, TEXT, RADIUS_MD);
201 visuals.widgets.inactive = widget(SURFACE_RAISED, LINE, TEXT, RADIUS_MD);
202 visuals.widgets.hovered = widget(SURFACE_HOVER, ACCENT.gamma_multiply(0.75), TEXT, RADIUS_MD);
203 visuals.widgets.active = widget(
204 ACCENT.gamma_multiply(0.25),
205 ACCENT,
206 Color32::WHITE,
207 RADIUS_MD,
208 );
209 visuals.widgets.open = visuals.widgets.active;
210 visuals
211}