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:
@@ -4,7 +4,7 @@ name = "Settings"
|
||||
version = "1.0.0"
|
||||
binary = "settings"
|
||||
author = "K-OS"
|
||||
memory = "6 MiB"
|
||||
memory = "3 MiB"
|
||||
|
||||
[capabilities]
|
||||
required = ["display", "permissions", "session"]
|
||||
|
||||
@@ -4,7 +4,7 @@ name = "Tally"
|
||||
version = "1.0.0"
|
||||
binary = "tally"
|
||||
author = "K-OS"
|
||||
memory = "6 MiB"
|
||||
memory = "3 MiB"
|
||||
|
||||
[capabilities]
|
||||
required = ["display"]
|
||||
|
||||
@@ -1,338 +0,0 @@
|
||||
//! The Whimsy look (see the vision's K-UI note): a wallpaper glowing with
|
||||
//! the theme's orbs, and glass panels over it, the wallpaper blurred and
|
||||
//! tinted through them. Both are made once per theme and window size, at
|
||||
//! half the size each way (they are soft, and an app's memory is small),
|
||||
//! by plain integer code; a frame only copies from them, each pixel
|
||||
//! doubled, so it costs about what clearing the window does.
|
||||
|
||||
use alloc::vec;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::layout::Rect;
|
||||
use crate::pixels::{between_corners, blend, corner_row, cover, height, span};
|
||||
use crate::theme::{Rgba, Theme};
|
||||
|
||||
/// The backdrop is made at 1 / `SCALE` of the window, each way.
|
||||
const SCALE: usize = 2;
|
||||
/// Where the orbs sit, as fractions of the window, and how big they are
|
||||
/// (of its larger side).
|
||||
const ORBS: [(f32, f32, f32); 3] = [(0.18, 0.22, 0.38), (0.82, 0.38, 0.32), (0.45, 0.92, 0.42)];
|
||||
/// An orb is at full strength out to this fraction of its radius, then
|
||||
/// fades: a shape with an edge, which the blur softens behind the glass.
|
||||
const ORB_CORE: f32 = 0.6;
|
||||
|
||||
/// An image in pixels of the bytes R, G, B, A (opaque).
|
||||
struct Image {
|
||||
w: usize,
|
||||
h: usize,
|
||||
px: Vec<[u8; 4]>,
|
||||
}
|
||||
|
||||
/// The wallpaper, and the glass over it (blurred, tinted), for one theme
|
||||
/// and window size.
|
||||
pub struct Backdrop {
|
||||
key: (Rgba, Rgba, [Rgba; 3], u8, u8, usize, usize),
|
||||
wall: Image,
|
||||
glass: Image,
|
||||
}
|
||||
|
||||
/// `a` moved toward `b` by `k` 256ths.
|
||||
fn mix(a: [u8; 4], b: Rgba, k: u32) -> [u8; 4] {
|
||||
let m = |x: u8, y: u8| ((x as u32 * (256 - k) + y as u32 * k) >> 8) as u8;
|
||||
[m(a[0], b.0), m(a[1], b.1), m(a[2], b.2), 255]
|
||||
}
|
||||
|
||||
/// How strongly an orb shows (256ths) at a squared distance of `i`/`FALL_STEPS`
|
||||
/// of its radius squared.
|
||||
const FALL_STEPS: usize = 256;
|
||||
|
||||
fn falloff() -> [u32; FALL_STEPS + 1] {
|
||||
let mut t = [0u32; FALL_STEPS + 1];
|
||||
for (i, k) in t.iter_mut().enumerate() {
|
||||
let d = libm::sqrtf(i as f32 / FALL_STEPS as f32);
|
||||
*k = (((1.0 - d) / (1.0 - ORB_CORE)).clamp(0.0, 1.0) * theme::ORB_STRENGTH * 256.0) as u32;
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
impl Backdrop {
|
||||
/// The backdrop for `t` in a window `w` by `h`: `cached` if it still is.
|
||||
pub fn for_window(cached: Option<Backdrop>, t: &Theme, w: u32, h: u32) -> Option<Backdrop> {
|
||||
let (w, h) = (w as usize, h as usize);
|
||||
let key = (t.background, t.surface, t.orbs, t.blur_radius, t.panel_opacity, w, h);
|
||||
if let Some(b) = cached.filter(|b| b.key == key) {
|
||||
return Some(b);
|
||||
}
|
||||
let (sw, sh) = (w.div_ceil(SCALE).max(1), h.div_ceil(SCALE).max(1));
|
||||
let bg = [t.background.0, t.background.1, t.background.2, 255];
|
||||
let mut wall = Image { w: sw, h: sh, px: vec![bg; sw * sh] };
|
||||
let big = sw.max(sh) as f32;
|
||||
let fall = falloff();
|
||||
for (orb, (fx, fy, fr)) in t.orbs.iter().zip(ORBS) {
|
||||
let (cx, cy, r) = ((fx * sw as f32) as i64, (fy * sh as f32) as i64, ((fr * big) as i64).max(1));
|
||||
let r2 = r * r;
|
||||
let rows = (cy - r).max(0) as usize..((cy + r + 1) as usize).min(sh);
|
||||
let cols = (cx - r).max(0) as usize..((cx + r + 1) as usize).min(sw);
|
||||
for y in rows {
|
||||
let dy2 = (y as i64 - cy) * (y as i64 - cy);
|
||||
for x in cols.clone() {
|
||||
let d2 = (x as i64 - cx) * (x as i64 - cx) + dy2;
|
||||
if d2 < r2 {
|
||||
let p = &mut wall.px[y * sw + x];
|
||||
*p = mix(*p, *orb, fall[(d2 * FALL_STEPS as i64 / r2) as usize]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Blurred at half this size again (it is soft), then brought back.
|
||||
let mut small = halve(&wall);
|
||||
blur(&mut small, (t.blur_radius as usize).div_ceil(2 * SCALE));
|
||||
let mut glass = double(&small, sw, sh);
|
||||
let opacity = t.panel_opacity as u32 * 256 / 100;
|
||||
for p in &mut glass.px {
|
||||
*p = mix(*p, t.surface, opacity);
|
||||
}
|
||||
Some(Backdrop { key, wall, glass })
|
||||
}
|
||||
|
||||
/// The wallpaper over `part` of the window's pixels `pm`
|
||||
/// (premultiplied RGBA, `width` pixels a row).
|
||||
pub fn wall(&self, pm: &mut [u8], width: usize, part: Rect) {
|
||||
copy(&self.wall, pm, width, part, 0.0);
|
||||
}
|
||||
|
||||
/// `part` of `pm` moved back toward the wallpaper by `k` 256ths: what
|
||||
/// is drawn there fading in.
|
||||
pub fn fade(&self, pm: &mut [u8], width: usize, part: Rect, k: u32) {
|
||||
let (cols, rows) = span(part, width, height(pm, width));
|
||||
for y in rows {
|
||||
let src = &self.wall.px[(y / SCALE).min(self.wall.h - 1) * self.wall.w..][..self.wall.w];
|
||||
let row = pm[(y * width + cols.start) * 4..(y * width + cols.end) * 4].as_chunks_mut::<4>().0;
|
||||
for (i, d) in row.iter_mut().enumerate() {
|
||||
blend(d, src[((cols.start + i) / SCALE).min(self.wall.w - 1)], k);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A glass panel over `r`, its corners rounded by `radius`.
|
||||
pub fn glass(&self, pm: &mut [u8], width: usize, r: Rect, radius: f32) {
|
||||
copy(&self.glass, pm, width, r, radius);
|
||||
}
|
||||
}
|
||||
|
||||
/// Copies `img` (scaled up) into `r` of `pm`, the corners rounded by
|
||||
/// `radius` and anti-aliased there.
|
||||
fn copy(img: &Image, pm: &mut [u8], width: usize, r: Rect, radius: f32) {
|
||||
let radius = radius.min(r.w / 2.0).min(r.h / 2.0).max(0.0);
|
||||
let (cols, rows) = span(r, width, height(pm, width));
|
||||
let mid = between_corners(r, radius, &cols);
|
||||
let (x0, x1) = (cols.start, cols.end);
|
||||
let mut copied: Option<usize> = None;
|
||||
for y in rows {
|
||||
let corner = corner_row(r, radius, y);
|
||||
let at = y * width * 4;
|
||||
// A row made from the same source row as the one above: the same.
|
||||
if let Some(above) = copied.filter(|a| *a + 1 == y && a / SCALE == y / SCALE && !corner) {
|
||||
let from = above * width * 4;
|
||||
pm.copy_within(from + x0 * 4..from + x1 * 4, at + x0 * 4);
|
||||
copied = Some(y);
|
||||
continue;
|
||||
}
|
||||
let src = &img.px[(y / SCALE).min(img.h - 1) * img.w..][..img.w];
|
||||
let straight = if corner { mid.clone() } else { cols.clone() };
|
||||
let row = &mut pm[at + straight.start * 4..at + straight.end * 4];
|
||||
for (i, d) in row.as_chunks_mut::<4>().0.iter_mut().enumerate() {
|
||||
*d = src[((straight.start + i) / SCALE).min(img.w - 1)];
|
||||
}
|
||||
if corner {
|
||||
let row = pm[at..at + width * 4].as_chunks_mut::<4>().0;
|
||||
for x in (x0..mid.start).chain(mid.end..x1) {
|
||||
let s = src[(x / SCALE).min(img.w - 1)];
|
||||
match cover(r, radius, x, y) {
|
||||
256 => row[x] = s,
|
||||
0 => {}
|
||||
k => blend(&mut row[x], s, k),
|
||||
}
|
||||
}
|
||||
}
|
||||
copied = (!corner).then_some(y);
|
||||
}
|
||||
}
|
||||
|
||||
/// `img` at half the size each way, each pixel the average of four.
|
||||
fn halve(img: &Image) -> Image {
|
||||
let (w, h) = (img.w.div_ceil(2), img.h.div_ceil(2));
|
||||
let mut px = vec![[0u8; 4]; w * h];
|
||||
for y in 0..h {
|
||||
let (r0, r1) = (2 * y, (2 * y + 1).min(img.h - 1));
|
||||
for x in 0..w {
|
||||
let (c0, c1) = (2 * x, (2 * x + 1).min(img.w - 1));
|
||||
let mut sum = [0u32; 4];
|
||||
for p in [img.px[r0 * img.w + c0], img.px[r0 * img.w + c1], img.px[r1 * img.w + c0], img.px[r1 * img.w + c1]] {
|
||||
add(&mut sum, p);
|
||||
}
|
||||
px[y * w + x] = sum.map(|s| (s / 4) as u8);
|
||||
}
|
||||
}
|
||||
Image { w, h, px }
|
||||
}
|
||||
|
||||
/// The average of two pixels, all four bytes at once.
|
||||
fn mid(a: [u8; 4], b: [u8; 4]) -> [u8; 4] {
|
||||
let (a, b) = (u32::from_le_bytes(a), u32::from_le_bytes(b));
|
||||
((a & b) + (((a ^ b) & 0xfefe_fefe) >> 1)).to_le_bytes()
|
||||
}
|
||||
|
||||
/// `img` at twice the size (cut to `w` by `h`), the new pixels between
|
||||
/// the old ones the average of their neighbours: the even rows from the
|
||||
/// source rows, the odd ones from the rows either side.
|
||||
fn double(img: &Image, w: usize, h: usize) -> Image {
|
||||
let mut px = vec![[0u8; 4]; w * h];
|
||||
for y in (0..h).step_by(2) {
|
||||
let src = &img.px[(y / 2).min(img.h - 1) * img.w..][..img.w];
|
||||
let out = &mut px[y * w..(y + 1) * w];
|
||||
for (x, o) in out.iter_mut().enumerate() {
|
||||
let s = src[(x / 2).min(img.w - 1)];
|
||||
*o = if x % 2 == 0 { s } else { mid(s, src[(x / 2 + 1).min(img.w - 1)]) };
|
||||
}
|
||||
}
|
||||
for y in (1..h).step_by(2) {
|
||||
let below = if y + 1 < h { y + 1 } else { y - 1 };
|
||||
for x in 0..w {
|
||||
px[y * w + x] = mid(px[(y - 1) * w + x], px[below * w + x]);
|
||||
}
|
||||
}
|
||||
Image { w, h, px }
|
||||
}
|
||||
|
||||
/// A box blur run three times over (close to a Gaussian), `r` pixels each
|
||||
/// way: along each row, then down every column at once, so memory is
|
||||
/// walked in order.
|
||||
fn blur(img: &mut Image, r: usize) {
|
||||
if r == 0 {
|
||||
return;
|
||||
}
|
||||
let mut line = vec![[0u8; 4]; img.w];
|
||||
let mut sums = vec![[0u32; 4]; img.w];
|
||||
for _ in 0..3 {
|
||||
for y in 0..img.h {
|
||||
across(&mut img.px[y * img.w..(y + 1) * img.w], r, &mut line);
|
||||
}
|
||||
down(img, r, &mut sums);
|
||||
}
|
||||
}
|
||||
|
||||
/// Dividing a sum of `2r + 1` bytes by that, as a multiply and a shift:
|
||||
/// exact for such sums.
|
||||
fn reciprocal(r: usize) -> u32 {
|
||||
(65536 + 2 * r as u32) / (2 * r as u32 + 1)
|
||||
}
|
||||
|
||||
fn add(sum: &mut [u32; 4], p: [u8; 4]) {
|
||||
sum[0] += p[0] as u32;
|
||||
sum[1] += p[1] as u32;
|
||||
sum[2] += p[2] as u32;
|
||||
sum[3] += p[3] as u32;
|
||||
}
|
||||
|
||||
fn sub(sum: &mut [u32; 4], p: [u8; 4]) {
|
||||
sum[0] -= p[0] as u32;
|
||||
sum[1] -= p[1] as u32;
|
||||
sum[2] -= p[2] as u32;
|
||||
sum[3] -= p[3] as u32;
|
||||
}
|
||||
|
||||
fn average(sum: &[u32; 4], inv: u32) -> [u8; 4] {
|
||||
[((sum[0] * inv) >> 16) as u8, ((sum[1] * inv) >> 16) as u8, ((sum[2] * inv) >> 16) as u8, ((sum[3] * inv) >> 16) as u8]
|
||||
}
|
||||
|
||||
/// One pass along `row`, its ends held.
|
||||
fn across(row: &mut [[u8; 4]], r: usize, line: &mut [[u8; 4]]) {
|
||||
let n = row.len();
|
||||
let line = &mut line[..n];
|
||||
line.copy_from_slice(row);
|
||||
let inv = reciprocal(r);
|
||||
let mut sum = [0u32; 4];
|
||||
for i in 0..=2 * r {
|
||||
add(&mut sum, line[i.saturating_sub(r).min(n - 1)]);
|
||||
}
|
||||
for (i, out) in row.iter_mut().enumerate() {
|
||||
*out = average(&sum, inv);
|
||||
add(&mut sum, line[(i + r + 1).min(n - 1)]);
|
||||
sub(&mut sum, line[i.saturating_sub(r)]);
|
||||
}
|
||||
}
|
||||
|
||||
/// One pass down every column, a row at a time; the top and bottom held.
|
||||
fn down(img: &mut Image, r: usize, sums: &mut [[u32; 4]]) {
|
||||
let (w, h) = (img.w, img.h);
|
||||
let src = img.px.clone();
|
||||
let row = |i: usize| &src[i.min(h - 1) * w..][..w];
|
||||
let inv = reciprocal(r);
|
||||
sums.fill([0; 4]);
|
||||
for i in 0..=2 * r {
|
||||
for (s, p) in sums.iter_mut().zip(row(i.saturating_sub(r))) {
|
||||
add(s, *p);
|
||||
}
|
||||
}
|
||||
for y in 0..h {
|
||||
for (out, s) in img.px[y * w..(y + 1) * w].iter_mut().zip(sums.iter()) {
|
||||
*out = average(s, inv);
|
||||
}
|
||||
for ((s, a), b) in sums.iter_mut().zip(row(y + r + 1)).zip(row(y.saturating_sub(r))) {
|
||||
add(s, *a);
|
||||
sub(s, *b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn image(w: usize, h: usize, c: [u8; 4]) -> Image {
|
||||
Image { w, h, px: vec![c; w * h] }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blur_spreads_a_shape_and_keeps_flat_colour_flat() {
|
||||
let mut img = image(40, 40, [10, 20, 30, 255]);
|
||||
blur(&mut img, 3);
|
||||
assert!(img.px.iter().all(|p| *p == [10, 20, 30, 255]), "nothing to blur, nothing changes");
|
||||
for y in 15..25 {
|
||||
for x in 15..25 {
|
||||
img.px[y * 40 + x] = [250, 250, 250, 255];
|
||||
}
|
||||
}
|
||||
blur(&mut img, 3);
|
||||
let red = |x: usize, y: usize| img.px[y * 40 + x][0];
|
||||
assert!(red(20, 20) > 150, "the middle, {}", red(20, 20));
|
||||
assert!((40..200).contains(&red(13, 20)), "just outside the edge: between, {}", red(13, 20));
|
||||
assert_eq!(red(2, 2), 10, "far away: untouched");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_backdrop_is_made_once_per_theme_and_size_and_glass_is_tinted() {
|
||||
let t = Theme::WHIMSY_DARK;
|
||||
let b = Backdrop::for_window(None, &t, 100, 60).unwrap();
|
||||
assert_eq!((b.wall.w, b.wall.h), (50, 30));
|
||||
let flat = Theme { orbs: [t.background; 3], ..t };
|
||||
let f = Backdrop::for_window(Some(b), &flat, 100, 60).unwrap();
|
||||
let bg = [t.background.0, t.background.1, t.background.2, 255];
|
||||
assert!(f.wall.px.iter().all(|p| *p == bg), "no orbs, no glow");
|
||||
assert_eq!(f.glass.px[0], mix(bg, t.surface, t.panel_opacity as u32 * 256 / 100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_panel_is_copied_with_round_soft_corners() {
|
||||
let t = Theme { orbs: [Rgba::rgb(0xffffff); 3], ..Theme::WHIMSY_DARK };
|
||||
let b = Backdrop::for_window(None, &t, 40, 40).unwrap();
|
||||
let mut pm = vec![0u8; 40 * 40 * 4];
|
||||
b.glass(&mut pm, 40, Rect { x: 10.0, y: 10.0, w: 20.0, h: 20.0 }, 8.0);
|
||||
let at = |x: usize, y: usize| pm[(y * 40 + x) * 4 + 3];
|
||||
assert_eq!(at(20, 20), 255, "inside");
|
||||
assert_eq!(at(10, 10), 0, "the very corner is cut off");
|
||||
assert!((1..255).contains(&at(12, 12)), "the corner's edge is soft: {}", at(12, 12));
|
||||
assert_eq!(at(5, 20), 0, "outside");
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@
|
||||
extern crate alloc;
|
||||
|
||||
pub mod element;
|
||||
pub mod glass;
|
||||
pub mod layout;
|
||||
mod pixels;
|
||||
pub mod motion;
|
||||
|
||||
@@ -1,68 +1,40 @@
|
||||
//! K-UI's own pixel loops over a window's bytes (R, G, B, A, premultiplied,
|
||||
//! `width` pixels a row): tiny-skia's translucent fills, gradients and
|
||||
//! patterns go through its float pipeline, which is slow wherever float is
|
||||
//! (QEMU emulates every float operation). Here float is kept to the
|
||||
//! rounded corners; straight runs share one value a row (or column) and
|
||||
//! blend in integers, two channels at a time.
|
||||
//! `width` pixels a row), for what covers much of the window: tiny-skia's
|
||||
//! translucent fills go through its float pipeline, which is slow wherever
|
||||
//! float is (QEMU emulates every float operation). These are integer, and
|
||||
//! blend two channels at a time.
|
||||
|
||||
use crate::layout::Rect;
|
||||
use crate::theme::Rgba;
|
||||
|
||||
/// The pixels of `r` (whole ones) inside a `width` by `height` window:
|
||||
/// columns and rows.
|
||||
pub fn span(r: Rect, width: usize, height: usize) -> (core::ops::Range<usize>, core::ops::Range<usize>) {
|
||||
fn span(r: Rect, width: usize, height: usize) -> (core::ops::Range<usize>, core::ops::Range<usize>) {
|
||||
let (x0, y0) = (r.x.max(0.0) as usize, r.y.max(0.0) as usize);
|
||||
let (x1, y1) = (((r.x + r.w).max(0.0) as usize).min(width), ((r.y + r.h).max(0.0) as usize).min(height));
|
||||
(x0..x1.max(x0), y0..y1.max(y0))
|
||||
}
|
||||
|
||||
pub fn height(pm: &[u8], width: usize) -> usize {
|
||||
fn height(pm: &[u8], width: usize) -> usize {
|
||||
pm.len() / 4 / width.max(1)
|
||||
}
|
||||
|
||||
/// The columns of `cols` clear of `r`'s rounded corners (radius `radius`):
|
||||
/// what lies between them.
|
||||
pub fn between_corners(r: Rect, radius: f32, cols: &core::ops::Range<usize>) -> core::ops::Range<usize> {
|
||||
let lo = (libm::ceilf(r.x + radius).max(0.0) as usize).clamp(cols.start, cols.end);
|
||||
let hi = (libm::floorf(r.x + r.w - radius).max(0.0) as usize).clamp(lo, cols.end);
|
||||
lo..hi
|
||||
}
|
||||
|
||||
/// Whether row `y` passes through `r`'s rounded corners.
|
||||
pub fn corner_row(r: Rect, radius: f32, y: usize) -> bool {
|
||||
radius > 0.0 && ((y as f32) < r.y + radius || (y as f32 + 1.0) > r.y + r.h - radius)
|
||||
}
|
||||
|
||||
/// How much of pixel (`x`, `y`) is inside `r` with its corners rounded by
|
||||
/// `radius`, in 256ths.
|
||||
pub fn cover(r: Rect, radius: f32, x: usize, y: usize) -> u32 {
|
||||
let (fx, fy) = (x as f32 + 0.5, y as f32 + 0.5);
|
||||
let dx = (r.x + radius - fx).max(fx - (r.x + r.w - radius));
|
||||
let dy = (r.y + radius - fy).max(fy - (r.y + r.h - radius));
|
||||
if dx <= 0.0 || dy <= 0.0 {
|
||||
return 256;
|
||||
}
|
||||
((radius + 0.5 - libm::sqrtf(dx * dx + dy * dy)).clamp(0.0, 1.0) * 256.0) as u32
|
||||
}
|
||||
|
||||
/// The pixel `d` moved toward `s` by `k` 256ths.
|
||||
pub fn blend(d: &mut [u8; 4], s: [u8; 4], k: u32) {
|
||||
fn blend(d: &mut [u8; 4], s: [u8; 4], k: u32) {
|
||||
let (d32, s32) = (u32::from_le_bytes(*d), u32::from_le_bytes(s));
|
||||
let (keep, take) = (256 - k, k);
|
||||
// Red and blue, then green and alpha, each pair in one word.
|
||||
let rb = ((d32 & 0x00ff_00ff) * keep + (s32 & 0x00ff_00ff) * take) >> 8 & 0x00ff_00ff;
|
||||
let rb = (((d32 & 0x00ff_00ff) * keep + (s32 & 0x00ff_00ff) * take) >> 8) & 0x00ff_00ff;
|
||||
let ga = (((d32 >> 8) & 0x00ff_00ff) * keep + ((s32 >> 8) & 0x00ff_00ff) * take) & 0xff00_ff00;
|
||||
*d = (rb | ga).to_le_bytes();
|
||||
}
|
||||
|
||||
/// Pixels `cols` of row `y` moved toward `s` by `k` 256ths.
|
||||
pub fn blend_run(pm: &mut [u8], width: usize, y: usize, cols: core::ops::Range<usize>, s: [u8; 4], k: u32) {
|
||||
if k == 0 || cols.is_empty() {
|
||||
return;
|
||||
}
|
||||
let row = &mut pm[(y * width + cols.start) * 4..(y * width + cols.end) * 4];
|
||||
for px in row.as_chunks_mut::<4>().0 {
|
||||
blend(px, s, k);
|
||||
/// `part` of `pm` in `c`, opaque.
|
||||
pub fn fill(pm: &mut [u8], width: usize, part: Rect, c: Rgba) {
|
||||
let (cols, rows) = span(part, width, height(pm, width));
|
||||
for y in rows {
|
||||
let row = pm[(y * width + cols.start) * 4..(y * width + cols.end) * 4].as_chunks_mut::<4>().0;
|
||||
row.fill([c.0, c.1, c.2, 255]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,81 +42,9 @@ pub fn blend_run(pm: &mut [u8], width: usize, y: usize, cols: core::ops::Range<u
|
||||
pub fn veil(pm: &mut [u8], width: usize, part: Rect, c: Rgba, k: u32) {
|
||||
let (cols, rows) = span(part, width, height(pm, width));
|
||||
for y in rows {
|
||||
blend_run(pm, width, y, cols.clone(), [c.0, c.1, c.2, 255], k);
|
||||
}
|
||||
}
|
||||
|
||||
/// The wet highlight: white along the top of `r`, `alpha` (percent) at
|
||||
/// the edge and gone `depth` pixels down.
|
||||
pub fn gloss(pm: &mut [u8], width: usize, r: Rect, radius: f32, alpha: u8, depth: f32) {
|
||||
let radius = radius.min(r.w / 2.0).min(r.h / 2.0).max(0.0);
|
||||
let (cols, rows) = span(Rect { h: depth.min(r.h), ..r }, width, height(pm, width));
|
||||
let mid = between_corners(r, radius, &cols);
|
||||
let white = [255u8; 4];
|
||||
for y in rows {
|
||||
let fade = 1.0 - (y as f32 + 0.5 - r.y) / depth;
|
||||
let a = (fade.max(0.0) * alpha as f32 * 256.0 / 100.0) as u32;
|
||||
let corner = corner_row(r, radius, y);
|
||||
let straight = if corner { mid.clone() } else { cols.clone() };
|
||||
blend_run(pm, width, y, straight, white, a);
|
||||
if corner {
|
||||
for x in (cols.start..mid.start).chain(mid.end..cols.end) {
|
||||
let k = (cover(r, radius, x, y) * a) >> 8;
|
||||
if k > 0 {
|
||||
blend_run(pm, width, y, x..x + 1, white, k);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// How dark a shadow is (256ths) `d` pixels outside what casts it.
|
||||
fn shade(d: f32, spread: f32, dark: u8) -> u32 {
|
||||
let fade = (1.0 - d.max(0.0) / spread).max(0.0);
|
||||
(fade * fade * dark as f32 * 256.0 / 100.0) as u32
|
||||
}
|
||||
|
||||
/// A soft shadow under `r` (corners rounded by `radius`): cast `down`
|
||||
/// pixels below it, `dark` (percent) at its edge and gone `spread` out.
|
||||
/// What `r` covers is left alone: it is drawn over anyway.
|
||||
pub fn shadow(pm: &mut [u8], width: usize, r: Rect, radius: f32, down: f32, spread: f32, dark: u8) {
|
||||
let radius = radius.min(r.w / 2.0).min(r.h / 2.0).max(0.0);
|
||||
let c = Rect { y: r.y + down, ..r };
|
||||
let around = Rect { x: r.x - spread, y: r.y - spread, w: r.w + 2.0 * spread, h: r.h + down + 2.0 * spread };
|
||||
let (cols, rows) = span(around, width, height(pm, width));
|
||||
let (panel, _) = span(r, width, height(pm, width));
|
||||
// Where the caster's edges are straight, across and down.
|
||||
let mid = between_corners(c, radius, &cols);
|
||||
let (sy0, sy1) = (c.y + radius, c.y + c.h - radius);
|
||||
let black = [0, 0, 0, 255];
|
||||
for y in rows {
|
||||
let fy = y as f32 + 0.5;
|
||||
let in_panel = fy >= r.y && fy < r.y + r.h;
|
||||
// Above or below the caster, where its top and bottom are straight:
|
||||
// one darkness for the whole run.
|
||||
let k = shade((c.y - fy).max(fy - (c.y + c.h)), spread, dark);
|
||||
let segments = match in_panel {
|
||||
true => [cols.start..panel.start, panel.end..cols.end],
|
||||
false => [cols.clone(), 0..0],
|
||||
};
|
||||
for seg in segments {
|
||||
let straight = seg.start.max(mid.start)..seg.end.min(mid.end);
|
||||
if !in_panel {
|
||||
blend_run(pm, width, y, straight.clone(), black, k);
|
||||
}
|
||||
for x in seg.filter(|x| in_panel || !straight.contains(x)) {
|
||||
let fx = x as f32 + 0.5;
|
||||
let d = match fy >= sy0 && fy < sy1 {
|
||||
// Beside the caster's straight sides.
|
||||
true => (c.x - fx).max(fx - (c.x + c.w)),
|
||||
false => {
|
||||
let dx = (c.x + radius - fx).max(fx - (c.x + c.w - radius)).max(0.0);
|
||||
let dy = (sy0 - fy).max(fy - sy1).max(0.0);
|
||||
libm::sqrtf(dx * dx + dy * dy) - radius
|
||||
}
|
||||
};
|
||||
blend_run(pm, width, y, x..x + 1, black, shade(d, spread, dark));
|
||||
}
|
||||
let row = pm[(y * width + cols.start) * 4..(y * width + cols.end) * 4].as_chunks_mut::<4>().0;
|
||||
for px in row {
|
||||
blend(px, [c.0, c.1, c.2, 255], k);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,21 +64,4 @@ mod tests {
|
||||
blend(&mut d, [99, 98, 97, 96], 256);
|
||||
assert_eq!(d, [99, 98, 97, 96], "all of it");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_shadow_darkens_around_and_below_but_not_under_or_far() {
|
||||
let w = 60;
|
||||
let mut pm = vec![200u8; w * w * 4];
|
||||
let r = Rect { x: 20.0, y: 20.0, w: 20.0, h: 20.0 };
|
||||
shadow(&mut pm, w, r, 6.0, 3.0, 7.0, 30);
|
||||
let red = |x: usize, y: usize| pm[(y * w + x) * 4];
|
||||
assert_eq!(red(30, 30), 200, "under the panel: left alone");
|
||||
assert!(red(30, 41) < red(30, 18), "darker below than above");
|
||||
assert!(red(17, 30) < 200, "beside it");
|
||||
assert_eq!(red(5, 5), 200, "far off");
|
||||
assert_eq!(red(30, 55), 200, "past its spread");
|
||||
}
|
||||
|
||||
extern crate std;
|
||||
use std::vec;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
//! Drawing a laid-out tree into a window's pixels (RGBA, premultiplied):
|
||||
//! the theme decides every colour and shape. Rows, toggles and fields are
|
||||
//! glass over the wallpaper (`glass`); buttons are solid, with a shadow
|
||||
//! and a highlight.
|
||||
//! the theme decides every colour and shape.
|
||||
|
||||
use alloc::string::String;
|
||||
use tiny_skia::{Color, FillRule, Paint, PathBuilder, PixmapMut, Stroke, Transform};
|
||||
|
||||
use crate::element::{Element, Kind};
|
||||
use crate::glass::Backdrop;
|
||||
use crate::layout::Rect;
|
||||
use crate::text::Fonts;
|
||||
use crate::theme::{Rgba, TextStyle, Theme, Weight};
|
||||
@@ -15,14 +12,8 @@ use crate::theme::{Rgba, TextStyle, Theme, Weight};
|
||||
/// The focus ring's distance outside the button, and its width.
|
||||
const RING_GAP: f32 = 3.0;
|
||||
const RING_WIDTH: f32 = 2.0;
|
||||
/// How far outside an element its ring or its shadow reaches.
|
||||
pub const REACH: f32 = SHADOW.0 + SHADOW.1 + 1.0;
|
||||
/// The shadow: how far down it is cast, how far it spreads, how dark it is
|
||||
/// at the edge (percent).
|
||||
const SHADOW: (f32, f32, u8) = (3.0, 7.0, 28);
|
||||
/// How far down from a panel's top its highlight fades out: above the
|
||||
/// text, which starts a row's padding down.
|
||||
const GLOSS: f32 = 12.0;
|
||||
/// How far outside an element its ring reaches.
|
||||
pub const REACH: f32 = RING_GAP + RING_WIDTH + 1.0;
|
||||
/// A field's outline, and its caret, across.
|
||||
const OUTLINE: f32 = 1.5;
|
||||
const CARET: f32 = 2.0;
|
||||
@@ -77,7 +68,6 @@ struct Painter<'p, 'a> {
|
||||
fonts: &'p Fonts,
|
||||
theme: &'p Theme,
|
||||
frame: Frame,
|
||||
backdrop: Option<&'p Backdrop>,
|
||||
}
|
||||
|
||||
fn mix(a: Rgba, b: Rgba, t: f32) -> Rgba {
|
||||
@@ -152,9 +142,7 @@ impl Painter<'_, '_> {
|
||||
Kind::Button(b) => {
|
||||
let (fill, label) = theme.button(b.variant);
|
||||
if let Some(fill) = fill {
|
||||
self.shadow(r);
|
||||
self.fill(r, theme.corner_radius, fill);
|
||||
self.gloss(r);
|
||||
}
|
||||
self.text_in(&b.label, TextStyle::Label, r, true, label);
|
||||
}
|
||||
@@ -241,32 +229,9 @@ impl Painter<'_, '_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A glass panel: its shadow, the wallpaper blurred through it, the
|
||||
/// surface over that, and its highlight.
|
||||
/// A row's, a toggle's or a field's panel.
|
||||
fn panel(&mut self, r: Rect) {
|
||||
let theme = self.theme;
|
||||
self.shadow(r);
|
||||
match self.backdrop {
|
||||
Some(b) => {
|
||||
let width = self.pm.width() as usize;
|
||||
b.glass(self.pm.data_mut(), width, r, theme.corner_radius);
|
||||
}
|
||||
None => self.fill(r, theme.corner_radius, theme.surface),
|
||||
}
|
||||
self.gloss(r);
|
||||
}
|
||||
|
||||
/// Soft and below: it lifts what casts it off the wallpaper.
|
||||
fn shadow(&mut self, r: Rect) {
|
||||
let width = self.pm.width() as usize;
|
||||
let (down, spread, dark) = SHADOW;
|
||||
crate::pixels::shadow(self.pm.data_mut(), width, r, self.theme.corner_radius, down, spread, dark);
|
||||
}
|
||||
|
||||
/// The wet highlight along the top.
|
||||
fn gloss(&mut self, r: Rect) {
|
||||
let width = self.pm.width() as usize;
|
||||
crate::pixels::gloss(self.pm.data_mut(), width, r, self.theme.corner_radius, self.theme.highlight, GLOSS);
|
||||
self.fill(r, self.theme.corner_radius, self.theme.surface);
|
||||
}
|
||||
|
||||
/// `r` filled with `c`, its corners rounded by `radius`.
|
||||
@@ -284,39 +249,23 @@ impl Painter<'_, '_> {
|
||||
}
|
||||
|
||||
/// Draws `root`, laid out as `rects`, as `frame` says (all of it, or its
|
||||
/// `clip`), over the wallpaper (`backdrop`, or the plain background). A
|
||||
/// screen still coming in is faded toward the background.
|
||||
pub fn render<E>(pm: &mut PixmapMut, root: &Element<E>, rects: &[Rect], fonts: &Fonts, theme: &Theme, frame: Frame, backdrop: Option<&Backdrop>) {
|
||||
/// `clip`). A screen still coming in fades up from the background.
|
||||
pub fn render<E>(pm: &mut PixmapMut, root: &Element<E>, rects: &[Rect], fonts: &Fonts, theme: &Theme, frame: Frame) {
|
||||
let bg = theme.background;
|
||||
let all = Rect { x: 0.0, y: 0.0, w: pm.width() as f32, h: pm.height() as f32 };
|
||||
let under = frame.clip.unwrap_or(all);
|
||||
match backdrop {
|
||||
Some(b) => {
|
||||
let width = pm.width() as usize;
|
||||
b.wall(pm.data_mut(), width, under);
|
||||
}
|
||||
None => {
|
||||
let r = tiny_skia::Rect::from_xywh(under.x, under.y, under.w, under.h);
|
||||
if let Some(r) = r {
|
||||
pm.fill_rect(r, &paint(bg), Transform::identity(), None);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut p = Painter { pm, fonts, theme, frame, backdrop };
|
||||
let all = Rect { x: 0.0, y: 0.0, w: pm.width() as f32, h: pm.height() as f32 };
|
||||
crate::pixels::fill(pm.data_mut(), width, frame.clip.unwrap_or(all), bg);
|
||||
let mut p = Painter { pm, fonts, theme, frame };
|
||||
p.draw(root, rects, &mut 0);
|
||||
if let Some(r) = frame.ring {
|
||||
p.ring(r);
|
||||
}
|
||||
if frame.enter < 1.0 {
|
||||
// The new screen's things fade up over the wallpaper, which stays.
|
||||
// Only what is on it fades: the background is the same anyway.
|
||||
let mut leaves = alloc::vec::Vec::new();
|
||||
crate::tree::leaf_rects(root, rects, &mut 0, &mut leaves);
|
||||
if let Some(content) = leaves.into_iter().map(|r| r.grow(REACH)).reduce(Rect::union) {
|
||||
let (width, k) = (p.pm.width() as usize, ((1.0 - frame.enter) * 256.0) as u32);
|
||||
match backdrop {
|
||||
Some(b) => b.fade(p.pm.data_mut(), width, content, k),
|
||||
None => crate::pixels::veil(p.pm.data_mut(), width, content, bg, k),
|
||||
}
|
||||
crate::pixels::veil(p.pm.data_mut(), width, content, bg, ((1.0 - frame.enter) * 256.0) as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ use alloc::vec::Vec;
|
||||
use tiny_skia::PixmapMut;
|
||||
|
||||
use crate::element::{Element, Input};
|
||||
use crate::glass::Backdrop;
|
||||
use crate::layout::{self, Rect};
|
||||
use crate::motion::{Ring, Tween};
|
||||
use crate::render::{self, Frame};
|
||||
@@ -91,13 +90,11 @@ pub struct Ui<A: App> {
|
||||
whole: bool,
|
||||
/// Where the ring was last drawn.
|
||||
last_ring: Option<Rect>,
|
||||
/// The wallpaper and its blur, for this theme and window size.
|
||||
backdrop: Option<Backdrop>,
|
||||
}
|
||||
|
||||
impl<A: App> Ui<A> {
|
||||
pub fn new(theme: Theme) -> Self {
|
||||
Self { app: A::init(), focus: 0, scroll: 0.0, caret: usize::MAX, theme, ring: None, flip: None, enter: None, drawn: None, whole: true, last_ring: None, backdrop: None }
|
||||
Self { app: A::init(), focus: 0, scroll: 0.0, caret: usize::MAX, theme, ring: None, flip: None, enter: None, drawn: None, whole: true, last_ring: None }
|
||||
}
|
||||
|
||||
/// A new screen: from its top, coming in, the ring where it lands.
|
||||
@@ -229,7 +226,6 @@ impl<A: App> Ui<A> {
|
||||
pub fn draw(&mut self, pm: &mut PixmapMut, fonts: &Fonts, now: u64) -> (bool, Option<Rect>) {
|
||||
let view = self.app.view();
|
||||
let (w, h) = (pm.width() as f32, pm.height() as f32);
|
||||
self.backdrop = Backdrop::for_window(self.backdrop.take(), &self.theme, pm.width(), pm.height());
|
||||
let measurer = Measurer { fonts, theme: &self.theme };
|
||||
// As tall as it needs: what does not fit is scrolled to.
|
||||
let tall = layout::natural(&view, &measurer, &self.theme).1.max(h);
|
||||
@@ -277,11 +273,11 @@ impl<A: App> Ui<A> {
|
||||
.map(|p| whole_elements(&view, &rects, p));
|
||||
let drew = match (full, part) {
|
||||
(true, _) => {
|
||||
render::render(pm, &view, &rects, fonts, &self.theme, frame, self.backdrop.as_ref());
|
||||
render::render(pm, &view, &rects, fonts, &self.theme, frame);
|
||||
None
|
||||
}
|
||||
(false, Some(p)) => {
|
||||
render::render(pm, &view, &rects, fonts, &self.theme, Frame { clip: Some(p), ..frame }, self.backdrop.as_ref());
|
||||
render::render(pm, &view, &rects, fonts, &self.theme, Frame { clip: Some(p), ..frame });
|
||||
Some(p)
|
||||
}
|
||||
// Nothing moved and nothing changed.
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,9 +16,6 @@ destructive = "#ef5350"
|
||||
on_destructive = "#1a0a0a"
|
||||
focus = "#ffffff"
|
||||
|
||||
[wallpaper]
|
||||
orbs = ["#3a1c71", "#0f4c5c", "#5b1a4a"]
|
||||
|
||||
[shape]
|
||||
corner_radius = 16
|
||||
|
||||
@@ -30,8 +27,3 @@ duration_fast = 150 # ms; 0 turns motion off
|
||||
duration_normal = 280
|
||||
spring_stiffness = 300
|
||||
spring_damping = 30
|
||||
|
||||
[glass]
|
||||
blur_radius = 12
|
||||
panel_opacity = 72 # percent
|
||||
highlight = 18 # percent
|
||||
|
||||
@@ -5,7 +5,7 @@ name = "Whimsy Light"
|
||||
background = "#f3f2f8"
|
||||
surface = "#ffffff"
|
||||
surface_raised = "#e6e4f0"
|
||||
accent = "#0a5cb0"
|
||||
accent = "#0b6bcb"
|
||||
on_accent = "#ffffff"
|
||||
text_primary = "#1a1a2e"
|
||||
text_secondary = "#55556e"
|
||||
@@ -13,9 +13,6 @@ destructive = "#c62828"
|
||||
on_destructive = "#ffffff"
|
||||
focus = "#1a1a2e"
|
||||
|
||||
[wallpaper]
|
||||
orbs = ["#c9b8ff", "#a8e6ff", "#ffc6e8"]
|
||||
|
||||
[shape]
|
||||
corner_radius = 16
|
||||
|
||||
@@ -27,8 +24,3 @@ duration_fast = 150
|
||||
duration_normal = 280
|
||||
spring_stiffness = 300
|
||||
spring_damping = 30
|
||||
|
||||
[glass]
|
||||
blur_radius = 12
|
||||
panel_opacity = 64
|
||||
highlight = 40
|
||||
|
||||
@@ -64,7 +64,7 @@ binary = "snake" # required: a file in the app's folder
|
||||
command = "snake" # optional: what the shell runs it as (default: binary)
|
||||
author = "K-OS" # optional
|
||||
icon = "icon.png" # optional: a file in the app's folder
|
||||
memory = "6 MiB" # optional: how much it may use, KiB or MiB,
|
||||
memory = "3 MiB" # optional: how much it may use, KiB or MiB,
|
||||
# 64 KiB to 16 MiB (default 512 KiB)
|
||||
|
||||
[capabilities]
|
||||
|
||||
Reference in New Issue
Block a user