pub type Rgb = [u8; 3];
pub struct Palette {
pub bg: Rgb,
pub fg: Rgb,
pub strong: Rgb,
pub muted: Rgb,
pub link: Rgb,
pub border: Rgb,
pub code_bg: Rgb,
pub inline_code_bg: Rgb,
#[cfg_attr(not(feature = "webview-backend"), allow(dead_code))]
pub sidebar_bg: Rgb,
#[cfg_attr(not(feature = "webview-backend"), allow(dead_code))]
pub sidebar_hover: Rgb,
#[cfg_attr(not(feature = "webview-backend"), allow(dead_code))]
pub sidebar_active: Rgb,
}
pub const DARK: Palette = Palette {
bg: [0x0d, 0x11, 0x17],
fg: [0xe6, 0xed, 0xf3],
strong: [0xff, 0xff, 0xff],
muted: [0x8b, 0x94, 0x9e],
link: [0x58, 0xa6, 0xff],
border: [0x30, 0x36, 0x3d],
code_bg: [0x16, 0x1b, 0x22],
inline_code_bg: [0x26, 0x2c, 0x36],
sidebar_bg: [0x01, 0x04, 0x09],
sidebar_hover: [0x16, 0x1b, 0x22],
sidebar_active: [0x07, 0x19, 0x36],
};
pub const LIGHT: Palette = Palette {
bg: [0xff, 0xff, 0xff],
fg: [0x1f, 0x23, 0x28],
strong: [0x00, 0x00, 0x00],
muted: [0x64, 0x6c, 0x75],
link: [0x09, 0x69, 0xda],
border: [0xd0, 0xd7, 0xde],
code_bg: [0xf6, 0xf8, 0xfa],
inline_code_bg: [0xef, 0xf1, 0xf3],
sidebar_bg: [0xf6, 0xf8, 0xfa],
sidebar_hover: [0xea, 0xee, 0xf2],
sidebar_active: [0xdd, 0xf4, 0xff],
};
pub const BASE_FONT_SIZE: f32 = 16.0;
pub const CODE_FONT_SCALE: f32 = 0.85;
#[cfg_attr(not(feature = "webview-backend"), allow(dead_code))]
pub const LINE_HEIGHT: f32 = 1.6;
pub fn heading_scale(level: u8) -> f32 {
match level {
0 | 1 => 2.0,
2 => 1.5,
3 => 1.25,
4 => 1.0,
5 => 0.875,
_ => 0.85,
}
}
#[cfg_attr(not(feature = "egui-backend"), allow(dead_code))]
pub fn heading_size(level: u8) -> f32 {
BASE_FONT_SIZE * heading_scale(level)
}
#[cfg_attr(not(feature = "webview-backend"), allow(dead_code))]
pub fn hex(colour: Rgb) -> String {
format!("#{:02x}{:02x}{:02x}", colour[0], colour[1], colour[2])
}
#[cfg(test)]
mod tests {
use super::*;
fn luminance(colour: Rgb) -> f64 {
fn channel(v: u8) -> f64 {
let v = f64::from(v) / 255.0;
if v <= 0.040_45 {
v / 12.92
} else {
((v + 0.055) / 1.055).powf(2.4)
}
}
0.2126 * channel(colour[0]) + 0.7152 * channel(colour[1]) + 0.0722 * channel(colour[2])
}
fn contrast(a: Rgb, b: Rgb) -> f64 {
let (la, lb) = (luminance(a), luminance(b));
let (hi, lo) = if la > lb { (la, lb) } else { (lb, la) };
(hi + 0.05) / (lo + 0.05)
}
fn check(palette: &Palette, name: &str) {
let pairs: &[(&str, Rgb, Rgb)] = &[
("body on page", palette.fg, palette.bg),
("strong on page", palette.strong, palette.bg),
("muted on page", palette.muted, palette.bg),
("link on page", palette.link, palette.bg),
("body on a code block", palette.fg, palette.code_bg),
(
"body on an inline code chip",
palette.fg,
palette.inline_code_bg,
),
("body on the sidebar", palette.fg, palette.sidebar_bg),
("body on a hovered entry", palette.fg, palette.sidebar_hover),
(
"body on the active entry",
palette.fg,
palette.sidebar_active,
),
("muted on the sidebar", palette.muted, palette.sidebar_bg),
(
"muted on a hovered entry",
palette.muted,
palette.sidebar_hover,
),
(
"a link on the active entry",
palette.link,
palette.sidebar_active,
),
(
"muted on the active entry",
palette.muted,
palette.sidebar_active,
),
];
for (what, fg, bg) in pairs {
let ratio = contrast(*fg, *bg);
assert!(
ratio >= 4.5,
"{name}: {what} is {ratio:.2}:1, below the 4.5:1 minimum"
);
}
}
#[test]
fn the_dark_palette_meets_the_contrast_minimum() {
check(&DARK, "dark");
}
#[test]
fn the_light_palette_meets_the_contrast_minimum() {
check(&LIGHT, "light");
}
#[test]
fn bold_is_distinguishable_from_body_text() {
for (palette, name) in [(&DARK, "dark"), (&LIGHT, "light")] {
assert_ne!(
palette.strong, palette.fg,
"{name}: strong text must differ from body text"
);
}
}
#[test]
fn an_inline_chip_stands_out_more_than_a_code_block() {
for (palette, name) in [(&DARK, "dark"), (&LIGHT, "light")] {
let chip = contrast(palette.inline_code_bg, palette.bg);
let block = contrast(palette.code_bg, palette.bg);
assert!(
chip > block,
"{name}: the inline chip ({chip:.2}) must stand out more than a block ({block:.2})"
);
}
}
#[test]
fn the_heading_scale_descends_and_is_bounded() {
let sizes: Vec<f32> = (1..=6).map(heading_size).collect();
for pair in sizes.windows(2) {
assert!(
pair[0] > pair[1],
"each heading level must be smaller than the one above: {sizes:?}"
);
}
assert_eq!(heading_size(1), BASE_FONT_SIZE * 2.0);
assert_eq!(heading_scale(7), heading_scale(6));
assert_eq!(heading_scale(0), heading_scale(1));
}
#[test]
fn hex_round_trips_the_palette() {
assert_eq!(hex(DARK.bg), "#0d1117");
assert_eq!(hex(LIGHT.link), "#0969da");
}
}