180 lines
6.1 KiB
Rust
180 lines
6.1 KiB
Rust
//! 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<Color32> {
|
|
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);
|
|
}
|
|
}
|