makeover 3.3.2

Shared theme loading for the make-family apps: TOML theme files parsed into intent-based color tokens, with perceptual derivations and WCAG contrast.
Documentation
//! Candidate page-to-raised ramps for one theme, in both units the ramp
//! decisions are argued in.
//!
//! `cargo run --example ramp_candidates -- makenotwork`
//!
//! Two units, and mixing them is how a ramp gets misread: `raised_is_distinct
//! _from_page` asserts in **oklab L on 0 to 1** (0.05), while the theme files
//! and the rulings quote **CIE L\* on 0 to 100** (6.7). They are not a factor
//! of a hundred apart. Both are printed for every row.
//!
//! Two directions, because a ramp has two ends. Lifting `raised` runs it toward
//! white on a light theme, and `bevel-light` is derived from `raised` by
//! lightening -- so past a point the lit edge stops gaining while the shadowed
//! one keeps going, and the bevel gets less symmetric rather than more legible.
//! Dropping `page` buys the same separation with the headroom left alone.
//!
//! Prints; changes nothing. The edit it argues for is a hand edit to
//! `[surface]`, and the look call is not this program's.

#![allow(clippy::many_single_char_names)]

use makeover::{Oklab, Rgb, SemanticTokens, ThemeColors};

/// CIE L\*, the unit the theme files quote.
fn cie_l(c: Rgb) -> f32 {
    let lin = |v: u8| {
        let v = f32::from(v) / 255.0;
        if v <= 0.04045 {
            v / 12.92
        } else {
            ((v + 0.055) / 1.055).powf(2.4)
        }
    };
    let (r, g, b) = c.tuple();
    let y = 0.212_672_9 * lin(r) + 0.715_152_2 * lin(g) + 0.072_175_0 * lin(b);
    let f = if y > 216.0 / 24389.0 {
        y.cbrt()
    } else {
        ((24389.0 / 27.0) * y + 16.0) / 116.0
    };
    116.0 * f - 16.0
}

/// The same color at a new oklab L, hue and chroma untouched.
fn at_l(c: Rgb, l: f32) -> Rgb {
    Rgb::from_oklab(Oklab { l, ..c.to_oklab() })
}

fn with_surfaces(theme: &ThemeColors, page: Rgb, raised: Rgb) -> SemanticTokens {
    let mut colors = theme.colors.clone();
    colors.insert("surface.page".into(), page.to_hex());
    colors.insert("surface.raised".into(), raised.to_hex());
    // Equal to raised in every house theme: an overlay is a raised surface that
    // happens to float.
    if colors.contains_key("surface.overlay") {
        colors.insert("surface.overlay".into(), raised.to_hex());
    }
    makeover::resolve(&ThemeColors {
        meta: theme.meta.clone(),
        colors,
    })
}

fn row(label: &str, page: Rgb, raised: Rgb, tokens: &SemanticTokens) {
    let delta = raised.to_oklab().l - page.to_oklab().l;
    // A well is cut into the page, so a page that has dropped past `sunken`
    // leaves the theme claiming a recess that reads as a rise. The direction
    // that drops the page runs into this before it runs out of gamut.
    let inverted = tokens
        .hex("surface-sunken")
        .and_then(Rgb::from_hex)
        .is_some_and(|sunken| sunken.to_oklab().l >= page.to_oklab().l);
    println!(
        "{label:<22} page {}  raised {}  delta {:.3} oklab / {:.1} L*  bevel {} .. {}{}",
        page.to_hex(),
        raised.to_hex(),
        delta,
        cie_l(raised) - cie_l(page),
        tokens.hex("bevel-light").unwrap_or("-"),
        tokens.hex("bevel-dark").unwrap_or("-"),
        if inverted {
            "  SUNKEN IS NO LONGER BELOW THE PAGE"
        } else {
            ""
        },
    );
}

fn main() {
    let id = std::env::args()
        .nth(1)
        .unwrap_or_else(|| "makenotwork".into());
    let dirs = vec![(
        makeover::bundled_themes_dir().expect("run me from a checkout"),
        false,
    )];
    let theme = makeover::load_theme(&dirs, &id).expect("no such theme");
    let hex = |key: &str| Rgb::from_hex(&theme.colors[key]).expect("hex");
    let (page, raised) = (hex("surface.page"), hex("surface.raised"));

    println!("{id}, as shipped:");
    row(
        "  current",
        page,
        raised,
        &with_surfaces(&theme, page, raised),
    );
    println!(
        "\nthe assertion's floor is 0.05 oklab; goingson sits at {:.3}, audiofiles at {:.3}\n",
        0.119, 0.065
    );

    println!("lifting raised, page fixed:");
    for delta in [0.070_f32, 0.085, 0.100, 0.119] {
        let candidate = at_l(raised, page.to_oklab().l + delta);
        let tokens = with_surfaces(&theme, page, candidate);
        row(&format!("  delta {delta:.3}"), page, candidate, &tokens);
    }

    println!("\ndropping page, raised fixed:");
    for delta in [0.070_f32, 0.085, 0.100, 0.119] {
        let candidate = at_l(page, raised.to_oklab().l - delta);
        let tokens = with_surfaces(&theme, candidate, raised);
        row(&format!("  delta {delta:.3}"), candidate, raised, &tokens);
    }
}