//! 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)) } /// Net amount with an explicit sign: `"+800 Kr"`, `"-90 Kr"`. pub fn signed_money(amount: i64) -> String { let sign = if amount < 0 { "-" } else { "+" }; format!("{sign}{} Kr", group_digits(amount.abs())) } // --------------------------------------------------------------------------- // 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 }