summaryrefslogtreecommitdiffstats
path: root/gui/src/theme.rs
blob: 9270aa661485a9781eefdfb4a2ff73bd25e26e3b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
//! Look and feel for the desktop frontend.
//!
//! The palette is derived from the terminal client's ANSI scheme so the two
//! frontends read as the same product: near-black ground, low-contrast panels,
//! cyan for focus, and the felt green reserved for the roulette table.
//!
//! egui's defaults are a debug tool's palette. Every colour, size and corner
//! used by the app comes from this module; widgets never pick their own.

use std::sync::Arc;

use eframe::egui::style::WidgetVisuals;
use eframe::egui::{
    self, Color32, Context, CornerRadius, FontData, FontDefinitions, FontFamily, FontId, Margin,
    RichText, Stroke, TextStyle, Theme, Visuals,
};

// ---------------------------------------------------------------------------
// Colour tokens
// ---------------------------------------------------------------------------

/// Window ground. Everything else sits above this.
pub const BG: Color32 = Color32::from_rgb(0x0a, 0x0e, 0x12);
/// Cards, header, sidebar.
pub const SURFACE: Color32 = Color32::from_rgb(0x11, 0x17, 0x1d);
/// Inputs, pills, raised rows.
pub const SURFACE_RAISED: Color32 = Color32::from_rgb(0x17, 0x1f, 0x27);
/// Hover state.
pub const SURFACE_HOVER: Color32 = Color32::from_rgb(0x1d, 0x27, 0x31);
/// Hairlines and table borders.
pub const LINE: Color32 = Color32::from_rgb(0x24, 0x2e, 0x38);
/// Primary text.
pub const TEXT: Color32 = Color32::from_rgb(0xd9, 0xe2, 0xea);
/// Labels and secondary text.
pub const TEXT_DIM: Color32 = Color32::from_rgb(0x8a, 0x98, 0xa6);
/// Disabled text, captions.
pub const TEXT_FAINT: Color32 = Color32::from_rgb(0x5b, 0x67, 0x73);
/// Focus, cursor, selection.
pub const ACCENT: Color32 = Color32::from_rgb(0x4f, 0xb8, 0xc9);
/// Wins, "online", the spin button.
pub const WIN: Color32 = Color32::from_rgb(0x4e, 0xc2, 0x7b);
/// Losses and errors.
pub const LOSS: Color32 = Color32::from_rgb(0xe0, 0x5a, 0x5a);
/// Chips and pending bets.
pub const GOLD: Color32 = Color32::from_rgb(0xe0, 0xb6, 0x4a);

/// Roulette felt. Never used outside the table.
pub const FELT: Color32 = Color32::from_rgb(0x0d, 0x5c, 0x39);
pub const FELT_DARK: Color32 = Color32::from_rgb(0x09, 0x47, 0x2c);
pub const FELT_LINE: Color32 = Color32::from_rgb(0x1f, 0x7d, 0x52);
pub const FELT_TEXT: Color32 = Color32::from_rgb(0xec, 0xf7, 0xf0);
pub const ROULETTE_RED: Color32 = Color32::from_rgb(0xb0, 0x31, 0x3a);
pub const ROULETTE_BLACK: Color32 = Color32::from_rgb(0x15, 0x1b, 0x21);
pub const ROULETTE_ZERO: Color32 = Color32::from_rgb(0x10, 0x6b, 0x45);

// ---------------------------------------------------------------------------
// Spacing, radius, type scale
// ---------------------------------------------------------------------------

pub const SPACE_1: f32 = 4.0;
pub const SPACE_2: f32 = 8.0;
pub const SPACE_3: f32 = 12.0;
pub const SPACE_4: f32 = 16.0;
pub const SPACE_5: f32 = 24.0;

pub const RADIUS_SM: CornerRadius = CornerRadius::same(4);
pub const RADIUS_MD: CornerRadius = CornerRadius::same(8);
pub const RADIUS_LG: CornerRadius = CornerRadius::same(12);

pub const FONT_BODY: f32 = 14.0;
pub const FONT_SMALL: f32 = 12.0;
pub const FONT_LABEL: f32 = 11.0;
pub const FONT_TITLE: f32 = 24.0;
pub const FONT_MONO: f32 = 16.0;

// ---------------------------------------------------------------------------
// Fonts
// ---------------------------------------------------------------------------

/// Open Sans, SIL OFL 1.1. See `assets/fonts/OFL-OpenSans.txt`.
pub const FONT_SANS_BYTES: &[u8] = include_bytes!("../assets/fonts/OpenSans-Regular.ttf");
/// Hack, MIT. See `assets/fonts/LICENSE-Hack.md`.
pub const FONT_MONO_BYTES: &[u8] = include_bytes!("../assets/fonts/Hack-Regular.ttf");

pub fn sans(size: f32) -> FontId {
    FontId::new(size, FontFamily::Proportional)
}

pub fn mono(size: f32) -> FontId {
    FontId::new(size, FontFamily::Monospace)
}

/// A small uppercase label, the terminal's dim section headers.
pub fn label(text: &str) -> RichText {
    RichText::new(text.to_uppercase())
        .font(mono(FONT_LABEL))
        .color(TEXT_DIM)
}

/// Group an amount in threes: 1234567 -> "1 234 567".
pub fn group_digits(amount: i64) -> String {
    let negative = amount < 0;
    let digits = amount.unsigned_abs().to_string();
    let mut grouped = String::with_capacity(digits.len() + digits.len() / 3);
    for (index, character) in digits.chars().enumerate() {
        if index > 0 && (digits.len() - index) % 3 == 0 {
            grouped.push(' ');
        }
        grouped.push(character);
    }
    if negative {
        format!("-{grouped}")
    } else {
        grouped
    }
}

/// `1000` -> `"1 000 Kr"`.
pub fn money(amount: i64) -> String {
    format!("{} Kr", group_digits(amount))
}

// ---------------------------------------------------------------------------
// Installation
// ---------------------------------------------------------------------------

/// Install the embedded fonts and the dark style. Call once, from the app
/// constructor, before the first frame.
pub fn install(ctx: &Context) {
    let mut fonts = FontDefinitions::default();
    fonts.font_data.insert(
        "zagan-sans".to_owned(),
        Arc::new(FontData::from_static(FONT_SANS_BYTES)),
    );
    fonts.font_data.insert(
        "zagan-mono".to_owned(),
        Arc::new(FontData::from_static(FONT_MONO_BYTES)),
    );
    fonts
        .families
        .get_mut(&FontFamily::Proportional)
        .expect("proportional family exists")
        .insert(0, "zagan-sans".to_owned());
    fonts
        .families
        .get_mut(&FontFamily::Monospace)
        .expect("monospace family exists")
        .insert(0, "zagan-mono".to_owned());
    ctx.set_fonts(fonts);

    ctx.set_theme(Theme::Dark);
    let mut style = (*ctx.global_style()).clone();
    style.text_styles = [
        (TextStyle::Heading, sans(FONT_TITLE)),
        (TextStyle::Body, sans(FONT_BODY)),
        (TextStyle::Button, sans(FONT_BODY)),
        (TextStyle::Small, sans(FONT_SMALL)),
        (TextStyle::Monospace, mono(FONT_MONO)),
    ]
    .into();
    style.spacing.item_spacing = egui::vec2(SPACE_2, SPACE_2);
    style.spacing.button_padding = egui::vec2(SPACE_3, SPACE_2);
    style.spacing.interact_size = egui::vec2(36.0, 32.0);
    style.spacing.window_margin = Margin::same(SPACE_3 as i8);
    style.visuals = visuals();
    ctx.set_global_style(style);
}

fn widget(
    bg_fill: Color32,
    bg_stroke: Color32,
    fg: Color32,
    corner_radius: CornerRadius,
) -> WidgetVisuals {
    WidgetVisuals {
        bg_fill,
        weak_bg_fill: bg_fill,
        bg_stroke: Stroke::new(1.0, bg_stroke),
        corner_radius,
        fg_stroke: Stroke::new(1.0, fg),
        expansion: 0.0,
    }
}

fn visuals() -> Visuals {
    let mut visuals = Visuals::dark();
    visuals.override_text_color = Some(TEXT);
    visuals.panel_fill = BG;
    visuals.window_fill = SURFACE;
    visuals.window_stroke = Stroke::new(1.0, LINE);
    visuals.window_corner_radius = RADIUS_LG;
    visuals.extreme_bg_color = BG;
    visuals.faint_bg_color = SURFACE_RAISED;
    visuals.code_bg_color = SURFACE_RAISED;
    visuals.hyperlink_color = ACCENT;
    visuals.warn_fg_color = GOLD;
    visuals.error_fg_color = LOSS;
    visuals.selection.bg_fill = ACCENT.gamma_multiply(0.30);
    visuals.selection.stroke = Stroke::new(1.0, ACCENT);
    visuals.widgets.noninteractive = widget(SURFACE, LINE, TEXT, RADIUS_MD);
    visuals.widgets.inactive = widget(SURFACE_RAISED, LINE, TEXT, RADIUS_MD);
    visuals.widgets.hovered = widget(SURFACE_HOVER, ACCENT.gamma_multiply(0.75), TEXT, RADIUS_MD);
    visuals.widgets.active = widget(
        ACCENT.gamma_multiply(0.25),
        ACCENT,
        Color32::WHITE,
        RADIUS_MD,
    );
    visuals.widgets.open = visuals.widgets.active;
    visuals
}