//! Colours: the six `canvasColor` presets plus the light and dark palettes the //! editor paints with. use eframe::egui::Color32; use crate::model::Color; /// The six preset colours, in spec order (`"1"` … `"6"`). pub const PRESETS: [Color32; 6] = [ Color32::from_rgb(0xfb, 0x46, 0x4c), // 1 red Color32::from_rgb(0xe9, 0x97, 0x3f), // 2 orange Color32::from_rgb(0xe0, 0xde, 0x71), // 3 yellow Color32::from_rgb(0x44, 0xcf, 0x6e), // 4 green Color32::from_rgb(0x53, 0xdf, 0xdd), // 5 cyan Color32::from_rgb(0xa8, 0x82, 0xff), // 6 purple ]; /// Every colour the canvas is painted with, for one of the two themes. #[derive(Clone, Copy)] pub struct Palette { pub dark: bool, pub canvas_bg: Color32, pub grid: Color32, pub grid_strong: Color32, pub surface: Color32, pub surface_muted: Color32, pub border: Color32, pub text: Color32, pub text_muted: Color32, pub accent: Color32, pub edge: Color32, pub danger: Color32, } impl Palette { pub fn dark() -> Self { Self { dark: true, canvas_bg: Color32::from_rgb(0x1a, 0x1a, 0x1e), grid: Color32::from_rgb(0x26, 0x26, 0x2c), grid_strong: Color32::from_rgb(0x30, 0x30, 0x38), surface: Color32::from_rgb(0x24, 0x24, 0x2a), surface_muted: Color32::from_rgb(0x2c, 0x2c, 0x33), border: Color32::from_rgb(0x44, 0x44, 0x4e), text: Color32::from_rgb(0xe4, 0xe4, 0xe8), text_muted: Color32::from_rgb(0x95, 0x95, 0xa0), accent: Color32::from_rgb(0x70, 0x9c, 0xff), edge: Color32::from_rgb(0x8a, 0x8a, 0x98), danger: Color32::from_rgb(0xe5, 0x53, 0x53), } } pub fn light() -> Self { Self { dark: false, canvas_bg: Color32::from_rgb(0xf7, 0xf7, 0xf9), grid: Color32::from_rgb(0xe6, 0xe6, 0xea), grid_strong: Color32::from_rgb(0xd6, 0xd6, 0xdc), surface: Color32::from_rgb(0xff, 0xff, 0xff), surface_muted: Color32::from_rgb(0xf0, 0xf0, 0xf4), border: Color32::from_rgb(0xcd, 0xcd, 0xd6), text: Color32::from_rgb(0x1e, 0x1e, 0x24), text_muted: Color32::from_rgb(0x6b, 0x6b, 0x78), accent: Color32::from_rgb(0x2f, 0x6f, 0xed), edge: Color32::from_rgb(0x76, 0x76, 0x84), danger: Color32::from_rgb(0xc4, 0x2b, 0x2b), } } /// Turns a `canvasColor` into a paintable colour. pub fn resolve(&self, color: &Color) -> Color32 { match color { Color::Preset(n) => PRESETS[(*n as usize - 1).min(5)], Color::Hex(hex) => parse_hex(hex).unwrap_or(self.accent), } } /// Border and fill for a node with the given `canvasColor`. pub fn node_colors(&self, color: Option<&Color>) -> (Color32, Color32) { match color { None => (self.surface, self.border), Some(color) => { let accent = self.resolve(color); let strength = if self.dark { 0.16 } else { 0.10 }; (blend(accent, self.surface, strength), accent) } } } /// Fill and border for a group box. pub fn group_colors(&self, color: Option<&Color>) -> (Color32, Color32) { let border = color.map_or(self.border, |c| self.resolve(c)); let strength = if self.dark { 0.10 } else { 0.07 }; (blend(border, self.canvas_bg, strength), border) } /// Colour of an edge, honouring its optional `canvasColor`. pub fn edge_color(&self, color: Option<&Color>) -> Color32 { color.map_or(self.edge, |c| self.resolve(c)) } } /// Parses `#rgb` / `#rrggbb`. pub fn parse_hex(hex: &str) -> Option { let hex = hex.trim().strip_prefix('#')?; let expand = |c: u8| c * 17; match hex.len() { 3 => { let mut it = hex.chars().map(|c| c.to_digit(16).map(|d| expand(d as u8))); Some(Color32::from_rgb(it.next()??, it.next()??, it.next()??)) } 6 => { let byte = |i: usize| u8::from_str_radix(&hex[i..i + 2], 16).ok(); Some(Color32::from_rgb(byte(0)?, byte(2)?, byte(4)?)) } _ => None, } } /// Mixes `fg` into `bg` with the given strength in `0..=1`. pub fn blend(fg: Color32, bg: Color32, strength: f32) -> Color32 { let mix = |a: u8, b: u8| (a as f32 * strength + b as f32 * (1.0 - strength)).round() as u8; Color32::from_rgb( mix(fg.r(), bg.r()), mix(fg.g(), bg.g()), mix(fg.b(), bg.b()), ) } /// Black or white, whichever stays readable on `background`. pub fn contrasting_text(background: Color32) -> Color32 { let luma = 0.299 * background.r() as f32 + 0.587 * background.g() as f32 + 0.114 * background.b() as f32; if luma > 150.0 { Color32::from_rgb(0x1e, 0x1e, 0x24) } else { Color32::from_rgb(0xf2, 0xf2, 0xf5) } } #[cfg(test)] mod tests { use super::*; #[test] fn parses_both_hex_lengths() { assert_eq!(parse_hex("#fff"), Some(Color32::WHITE)); assert_eq!(parse_hex("#ff0000"), Some(Color32::from_rgb(255, 0, 0))); assert_eq!(parse_hex("nope"), None); assert_eq!(parse_hex("#12345"), None); } #[test] fn presets_resolve_by_number() { let palette = Palette::dark(); assert_eq!(palette.resolve(&Color::Preset(1)), PRESETS[0]); assert_eq!(palette.resolve(&Color::Preset(6)), PRESETS[5]); assert_eq!( palette.resolve(&Color::Hex("#ff00ff".into())), Color32::from_rgb(255, 0, 255) ); } #[test] fn a_coloured_node_keeps_a_readable_fill() { let palette = Palette::dark(); let (fill, border) = palette.node_colors(Some(&Color::Preset(1))); assert_eq!(border, PRESETS[0]); // The fill is only tinted, it stays close to the surface colour. assert!(fill.r() > palette.surface.r() && fill.r() < PRESETS[0].r()); } #[test] fn text_contrast_flips_with_the_background() { assert_eq!(contrasting_text(Color32::WHITE).r(), 0x1e); assert_eq!(contrasting_text(Color32::BLACK).r(), 0xf2); } }