Version 1
This commit is contained in:
+178
@@ -0,0 +1,178 @@
|
||||
//! 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user