refactor(k-ui): retire glass look (wallpaper, blur, shadow, gloss): flat surfaces; theme drops [wallpaper]/[glass] tokens + orb contrast checks; light accent back to #0b6bcb; settings/tally 3 MiB
This commit is contained in:
@@ -1,9 +1,6 @@
|
||||
//! Contrast as WCAG 2 measures it: the ratio of two colours' relative
|
||||
//! luminance, from 1 (the same) to 21 (black on white). Text needs 4.5,
|
||||
//! other things the eye must find (the focus ring, a toggle's track) 3.
|
||||
//! What text sits on is not one colour: the wallpaper glows with the orbs,
|
||||
//! and glass panels let it through, so each is checked over the plain
|
||||
//! background and over every orb.
|
||||
|
||||
use crate::parse::Error;
|
||||
use crate::{Rgba, Theme};
|
||||
@@ -25,48 +22,25 @@ pub fn ratio(a: Rgba, b: Rgba) -> f32 {
|
||||
const TEXT: f32 = 4.5;
|
||||
const SHAPE: f32 = 3.0;
|
||||
|
||||
/// Pairings drawn on solid colours, each with what it must reach.
|
||||
fn solid(t: &Theme) -> [(&'static str, Rgba, Rgba, f32); 4] {
|
||||
/// Every pairing K-UI draws, each with what it must reach.
|
||||
fn pairings(t: &Theme) -> [(&'static str, Rgba, Rgba, f32); 10] {
|
||||
[
|
||||
("text_primary on background", t.text_primary, t.background, TEXT),
|
||||
("text_primary on surface", t.text_primary, t.surface, TEXT),
|
||||
("text_primary on surface_raised", t.text_primary, t.surface_raised, TEXT),
|
||||
("text_secondary on background", t.text_secondary, t.background, TEXT),
|
||||
("text_secondary on surface", t.text_secondary, t.surface, TEXT),
|
||||
("accent on background", t.accent, t.background, TEXT),
|
||||
("on_accent on accent", t.on_accent, t.accent, TEXT),
|
||||
("on_destructive on destructive", t.on_destructive, t.destructive, TEXT),
|
||||
("accent on surface", t.accent, t.surface, SHAPE),
|
||||
("focus on background", t.focus, t.background, SHAPE),
|
||||
]
|
||||
}
|
||||
|
||||
/// Pairings drawn on the wallpaper (`w`) and on glass over it (`g`).
|
||||
fn over(t: &Theme, w: Rgba, g: Rgba) -> [(&'static str, Rgba, Rgba, f32); 6] {
|
||||
[
|
||||
("text_primary on the wallpaper", t.text_primary, w, TEXT),
|
||||
("text_secondary on the wallpaper", t.text_secondary, w, TEXT),
|
||||
("accent on the wallpaper", t.accent, w, TEXT),
|
||||
("focus on the wallpaper", t.focus, w, SHAPE),
|
||||
("text_primary on glass", t.text_primary, g, TEXT),
|
||||
("text_secondary on glass", t.text_secondary, g, TEXT),
|
||||
]
|
||||
}
|
||||
|
||||
fn mix(a: Rgba, b: Rgba, t: f32) -> Rgba {
|
||||
let m = |x: u8, y: u8| (x as f32 + (y as f32 - x as f32) * t) as u8;
|
||||
Rgba(m(a.0, b.0), m(a.1, b.1), m(a.2, b.2), 255)
|
||||
}
|
||||
|
||||
/// The wallpaper where an orb is strongest.
|
||||
pub fn wallpaper(t: &Theme, orb: Rgba) -> Rgba {
|
||||
mix(t.background, orb, crate::ORB_STRENGTH)
|
||||
}
|
||||
|
||||
/// A glass panel over `under`.
|
||||
pub fn glass(t: &Theme, under: Rgba) -> Rgba {
|
||||
mix(under, t.surface, t.panel_opacity as f32 / 100.0)
|
||||
}
|
||||
|
||||
/// The theme, if everything drawn with it can be read.
|
||||
pub fn check(t: Theme) -> Result<Theme, Error> {
|
||||
let walls = [t.background].into_iter().chain(t.orbs.iter().map(|o| wallpaper(&t, *o)));
|
||||
let pairs = walls.flat_map(|w| over(&t, w, glass(&t, w))).chain(solid(&t));
|
||||
for (pair, fg, bg, min) in pairs {
|
||||
for (pair, fg, bg, min) in pairings(&t) {
|
||||
let r = ratio(fg, bg);
|
||||
if r < min {
|
||||
return Err(Error::Contrast { pair, ratio_x100: (r * 100.0) as u32 });
|
||||
|
||||
@@ -17,7 +17,7 @@ pub mod wire;
|
||||
mod tests;
|
||||
|
||||
pub use accent::with_accent;
|
||||
pub use contrast::{check, glass, ratio, wallpaper};
|
||||
pub use contrast::{check, ratio};
|
||||
pub use parse::{Error, parse};
|
||||
|
||||
/// A colour as the theme names it: straight (not premultiplied) RGBA.
|
||||
@@ -83,9 +83,6 @@ pub enum Weight {
|
||||
SemiBold,
|
||||
}
|
||||
|
||||
/// How strongly the orbs show through the background at their hearts.
|
||||
pub const ORB_STRENGTH: f32 = 0.5;
|
||||
|
||||
/// A theme's name: what Settings lists and a user's choice records.
|
||||
pub const NAME_BYTES: usize = types::name::BYTES;
|
||||
|
||||
@@ -103,8 +100,6 @@ pub struct Theme {
|
||||
pub destructive: Rgba,
|
||||
pub on_destructive: Rgba,
|
||||
pub focus: Rgba,
|
||||
/// Soft colours the wallpaper glows with, behind the glass.
|
||||
pub orbs: [Rgba; 3],
|
||||
pub corner_radius: f32,
|
||||
/// The spacing scale's unit, in pixels.
|
||||
pub spacing_base: f32,
|
||||
@@ -113,11 +108,6 @@ pub struct Theme {
|
||||
pub duration_normal: u16,
|
||||
pub spring_stiffness: u16,
|
||||
pub spring_damping: u16,
|
||||
/// Pixels.
|
||||
pub blur_radius: u8,
|
||||
/// Percent.
|
||||
pub panel_opacity: u8,
|
||||
pub highlight: u8,
|
||||
}
|
||||
|
||||
pub(crate) const fn name_bytes(s: &[u8]) -> ([u8; NAME_BYTES], usize) {
|
||||
@@ -147,16 +137,12 @@ impl Theme {
|
||||
destructive: Rgba::rgb(0xef5350),
|
||||
on_destructive: Rgba::rgb(0x1a0a0a),
|
||||
focus: Rgba::rgb(0xffffff),
|
||||
orbs: [Rgba::rgb(0x3a1c71), Rgba::rgb(0x0f4c5c), Rgba::rgb(0x5b1a4a)],
|
||||
corner_radius: 16.0,
|
||||
spacing_base: 4.0,
|
||||
duration_fast: 150,
|
||||
duration_normal: 280,
|
||||
spring_stiffness: 300,
|
||||
spring_damping: 30,
|
||||
blur_radius: 12,
|
||||
panel_opacity: 72,
|
||||
highlight: 18,
|
||||
};
|
||||
|
||||
pub fn name(&self) -> &[u8] {
|
||||
|
||||
@@ -29,16 +29,13 @@ const COLOURS: [&str; 10] = [
|
||||
];
|
||||
|
||||
/// The whole numbers: table, key, and the range allowed.
|
||||
const NUMBERS: [(&str, &str, u32, u32); 9] = [
|
||||
const NUMBERS: [(&str, &str, u32, u32); 6] = [
|
||||
("shape", "corner_radius", 0, 32),
|
||||
("spacing", "base", 2, 8),
|
||||
("motion", "duration_fast", 0, 2000),
|
||||
("motion", "duration_normal", 0, 2000),
|
||||
("motion", "spring_stiffness", 1, 2000),
|
||||
("motion", "spring_damping", 1, 200),
|
||||
("glass", "blur_radius", 0, 32),
|
||||
("glass", "panel_opacity", 0, 100),
|
||||
("glass", "highlight", 0, 100),
|
||||
];
|
||||
|
||||
fn colour(s: &str) -> Option<Rgba> {
|
||||
@@ -57,8 +54,7 @@ fn once<T>(slot: &mut Option<T>, value: Option<T>, line: usize) -> Result<(), To
|
||||
pub fn parse(text: &str) -> Result<Theme, Error> {
|
||||
let mut name: Option<([u8; NAME_BYTES], usize)> = None;
|
||||
let mut colours: [Option<Rgba>; 10] = [None; 10];
|
||||
let mut orbs: Option<[Rgba; 3]> = None;
|
||||
let mut numbers: [Option<u32>; 9] = [None; 9];
|
||||
let mut numbers: [Option<u32>; 6] = [None; 6];
|
||||
toml::parse(text, |e| {
|
||||
let line = e.line;
|
||||
match (e.table, e.key, e.value) {
|
||||
@@ -70,21 +66,12 @@ pub fn parse(text: &str) -> Result<Theme, Error> {
|
||||
let i = COLOURS.iter().position(|k| *k == key).ok_or(Toml::Unknown { line })?;
|
||||
once(&mut colours[i], colour(s), line)
|
||||
}
|
||||
("wallpaper", "orbs", Value::List(list)) => {
|
||||
let mut out = [Rgba::rgb(0); 3];
|
||||
let mut n = 0;
|
||||
for s in list.iter() {
|
||||
*out.get_mut(n).ok_or(Toml::Invalid { line })? = colour(s).ok_or(Toml::Invalid { line })?;
|
||||
n += 1;
|
||||
}
|
||||
once(&mut orbs, (n == 3).then_some(out), line)
|
||||
}
|
||||
(table, key, Value::Int(v)) => {
|
||||
let i = NUMBERS.iter().position(|(t, k, ..)| *t == table && *k == key).ok_or(Toml::Unknown { line })?;
|
||||
let (_, _, lo, hi) = NUMBERS[i];
|
||||
once(&mut numbers[i], (lo..=hi).contains(&v).then_some(v), line)
|
||||
}
|
||||
("meta" | "colour" | "wallpaper" | "shape" | "spacing" | "motion" | "glass", _, _) => Err(Toml::Invalid { line }),
|
||||
("meta" | "colour" | "shape" | "spacing" | "motion", _, _) => Err(Toml::Invalid { line }),
|
||||
_ => Err(Toml::Unknown { line }),
|
||||
}
|
||||
})
|
||||
@@ -94,7 +81,7 @@ pub fn parse(text: &str) -> Result<Theme, Error> {
|
||||
for (i, slot) in colours.iter().enumerate() {
|
||||
c[i] = slot.ok_or(Error::Toml(Toml::Missing(COLOURS[i])))?;
|
||||
}
|
||||
let mut n = [0u32; 9];
|
||||
let mut n = [0u32; 6];
|
||||
for (i, slot) in numbers.iter().enumerate() {
|
||||
n[i] = slot.ok_or(Error::Toml(Toml::Missing(NUMBERS[i].1)))?;
|
||||
}
|
||||
@@ -111,7 +98,6 @@ pub fn parse(text: &str) -> Result<Theme, Error> {
|
||||
destructive: c[7],
|
||||
on_destructive: c[8],
|
||||
focus: c[9],
|
||||
orbs: orbs.ok_or(Error::Toml(Toml::Missing("orbs")))?,
|
||||
corner_radius: n[0] as f32,
|
||||
spacing_base: n[1] as f32,
|
||||
// In range, so these fit.
|
||||
@@ -119,8 +105,5 @@ pub fn parse(text: &str) -> Result<Theme, Error> {
|
||||
duration_normal: n[3] as u16,
|
||||
spring_stiffness: n[4] as u16,
|
||||
spring_damping: n[5] as u16,
|
||||
blur_radius: n[6] as u8,
|
||||
panel_opacity: n[7] as u8,
|
||||
highlight: n[8] as u8,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -31,10 +31,7 @@ fn a_theme_survives_the_wire_but_its_name() {
|
||||
#[test]
|
||||
fn unreadable_pairings_are_refused() {
|
||||
let grey = DARK.replace("text_secondary = \"#9e9eb8\"", "text_secondary = \"#3a3a4a\"");
|
||||
assert!(matches!(check(parse(&grey).unwrap()), Err(Error::Contrast { pair: "text_secondary on the wallpaper", .. })));
|
||||
// An orb too bright for the text over it.
|
||||
let glare = DARK.replace("\"#0f4c5c\"", "\"#f0f0f0\"");
|
||||
assert!(matches!(check(parse(&glare).unwrap()), Err(Error::Contrast { .. })));
|
||||
assert!(matches!(check(parse(&grey).unwrap()), Err(Error::Contrast { pair: "text_secondary on background", .. })));
|
||||
assert!((ratio(Rgba::rgb(0), Rgba::rgb(0xffffff)) - 21.0).abs() < 0.01);
|
||||
}
|
||||
|
||||
@@ -47,12 +44,11 @@ fn every_token_is_needed_and_checked() {
|
||||
("corner_radius = 16", "corner_radius = 99"),
|
||||
("base = 4", "base = \"4\""),
|
||||
("\"#ffffff\"", "\"white\""),
|
||||
("orbs = [\"#3a1c71\", \"#0f4c5c\", \"#5b1a4a\"]", "orbs = [\"#3a1c71\"]"),
|
||||
] {
|
||||
assert!(matches!(parse(&DARK.replace(from, to)), Err(Error::Toml(T::Invalid { .. }))), "{to}");
|
||||
}
|
||||
assert!(matches!(parse(&DARK.replace("highlight = 18", "glow = 18")), Err(Error::Toml(T::Unknown { .. }))));
|
||||
assert!(matches!(parse(&std::format!("{DARK}\n[glass]\n")), Err(Error::Toml(T::Duplicate { .. }))));
|
||||
assert!(matches!(parse(&DARK.replace("spring_damping = 30", "bounce = 30")), Err(Error::Toml(T::Unknown { .. }))));
|
||||
assert!(matches!(parse(&std::format!("{DARK}\n[motion]\n")), Err(Error::Toml(T::Duplicate { .. }))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::{Rgba, Theme};
|
||||
|
||||
pub const WORDS: usize = types::display::window::THEME_WORDS;
|
||||
|
||||
fn colours(t: &Theme) -> [Rgba; 12] {
|
||||
fn colours(t: &Theme) -> [Rgba; 10] {
|
||||
[
|
||||
t.background,
|
||||
t.surface,
|
||||
@@ -17,34 +17,27 @@ fn colours(t: &Theme) -> [Rgba; 12] {
|
||||
t.destructive,
|
||||
t.on_destructive,
|
||||
t.focus,
|
||||
t.orbs[0],
|
||||
t.orbs[1],
|
||||
]
|
||||
}
|
||||
|
||||
pub fn encode(t: &Theme) -> [usize; WORDS] {
|
||||
let c = colours(t);
|
||||
let mut w = [0usize; WORDS];
|
||||
for i in 0..6 {
|
||||
for i in 0..5 {
|
||||
w[i] = c[2 * i].to_rgb() as usize | (c[2 * i + 1].to_rgb() as usize) << 32;
|
||||
}
|
||||
w[6] = t.corner_radius as usize
|
||||
// In range (see `parse`): radius and base a byte each, damping too.
|
||||
w[5] = t.corner_radius as usize
|
||||
| (t.spacing_base as usize) << 8
|
||||
| (t.blur_radius as usize) << 16
|
||||
| (t.panel_opacity as usize) << 24
|
||||
| (t.highlight as usize) << 32
|
||||
| (t.duration_fast as usize) << 40;
|
||||
w[7] = t.duration_normal as usize
|
||||
| (t.spring_stiffness as usize) << 16
|
||||
| (t.spring_damping as usize & 0xff) << 32
|
||||
| (t.orbs[2].to_rgb() as usize & 0xff_ffff) << 40;
|
||||
| (t.spring_damping as usize & 0xff) << 16
|
||||
| (t.duration_fast as usize) << 24
|
||||
| (t.duration_normal as usize) << 40;
|
||||
w[6] = t.spring_stiffness as usize;
|
||||
w
|
||||
}
|
||||
|
||||
pub fn decode(w: [usize; WORDS]) -> Theme {
|
||||
let c = |i: usize| Rgba::rgb((w[i / 2] >> (32 * (i % 2))) as u32 & 0xff_ffff);
|
||||
let byte = |word: usize, at: u32| (w[word] >> at) as u8;
|
||||
let half = |word: usize, at: u32| (w[word] >> at) as u16;
|
||||
Theme {
|
||||
name: [0; crate::NAME_BYTES],
|
||||
name_len: 0,
|
||||
@@ -58,16 +51,11 @@ pub fn decode(w: [usize; WORDS]) -> Theme {
|
||||
destructive: c(7),
|
||||
on_destructive: c(8),
|
||||
focus: c(9),
|
||||
orbs: [c(10), c(11), Rgba::rgb((w[7] >> 40) as u32 & 0xff_ffff)],
|
||||
corner_radius: byte(6, 0) as f32,
|
||||
spacing_base: byte(6, 8) as f32,
|
||||
blur_radius: byte(6, 16),
|
||||
panel_opacity: byte(6, 24),
|
||||
highlight: byte(6, 32),
|
||||
duration_fast: half(6, 40),
|
||||
duration_normal: half(7, 0),
|
||||
spring_stiffness: half(7, 16),
|
||||
// At most 200 (see `parse`): a byte.
|
||||
spring_damping: byte(7, 32) as u16,
|
||||
corner_radius: (w[5] & 0xff) as f32,
|
||||
spacing_base: ((w[5] >> 8) & 0xff) as f32,
|
||||
spring_damping: ((w[5] >> 16) & 0xff) as u16,
|
||||
duration_fast: (w[5] >> 24) as u16,
|
||||
duration_normal: (w[5] >> 40) as u16,
|
||||
spring_stiffness: w[6] as u16,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user