feat(k-ui): glass look: orb wallpaper + blurred, tinted glass panels (half-res backdrop per theme+size, integer code, frames only copy), soft analytic shadow, top gloss; contrast checked over wallpaper/glass per orb (light accent -> #0a5cb0); partial frames drawn in place; Rect geometry, tree lookups split out; settings/tally 6 MiB
This commit is contained in:
@@ -4,7 +4,7 @@ name = "Settings"
|
|||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
binary = "settings"
|
binary = "settings"
|
||||||
author = "K-OS"
|
author = "K-OS"
|
||||||
memory = "3 MiB"
|
memory = "6 MiB"
|
||||||
|
|
||||||
[capabilities]
|
[capabilities]
|
||||||
required = ["display", "permissions", "session"]
|
required = ["display", "permissions", "session"]
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ name = "Tally"
|
|||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
binary = "tally"
|
binary = "tally"
|
||||||
author = "K-OS"
|
author = "K-OS"
|
||||||
memory = "3 MiB"
|
memory = "6 MiB"
|
||||||
|
|
||||||
[capabilities]
|
[capabilities]
|
||||||
required = ["display"]
|
required = ["display"]
|
||||||
|
|||||||
314
crates/k-ui/src/glass.rs
Normal file
314
crates/k-ui/src/glass.rs
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
//! 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::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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut glass = Image { w: sw, h: sh, px: wall.px.clone() };
|
||||||
|
blur(&mut glass, (t.blur_radius as usize).div_ceil(SCALE));
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The pixels of `r` (whole ones) inside `pm`: columns and rows.
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How much of pixel (`x`, `y`) is inside `r` with its corners rounded by
|
||||||
|
/// `radius`, in 256ths: worked out only in the corners.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
fn blend(d: &mut [u8], s: [u8; 4], k: u32) {
|
||||||
|
for (d, s) in d.iter_mut().zip(s) {
|
||||||
|
*d = ((s as u32 * k + *d as u32 * (256 - k)) >> 8) as u8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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, pm.len() / 4 / width.max(1));
|
||||||
|
let (x0, x1) = (cols.start, cols.end);
|
||||||
|
let mut copied: Option<usize> = None;
|
||||||
|
for y in rows {
|
||||||
|
let corner = radius > 0.0 && ((y as f32) < r.y + radius || (y as f32 + 1.0) > r.y + r.h - radius);
|
||||||
|
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 row = &mut pm[at + x0 * 4..at + x1 * 4];
|
||||||
|
if corner {
|
||||||
|
for (i, d) in row.as_chunks_mut::<4>().0.iter_mut().enumerate() {
|
||||||
|
let s = src[((x0 + i) / SCALE).min(img.w - 1)];
|
||||||
|
match cover(r, radius, x0 + i, y) {
|
||||||
|
256 => d.copy_from_slice(&s),
|
||||||
|
0 => {}
|
||||||
|
k => blend(d, s, k),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
copied = None;
|
||||||
|
} else {
|
||||||
|
for (i, d) in row.as_chunks_mut::<4>().0.iter_mut().enumerate() {
|
||||||
|
d.copy_from_slice(&src[((x0 + i) / SCALE).min(img.w - 1)]);
|
||||||
|
}
|
||||||
|
copied = Some(y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 top = Rect { h: depth.min(r.h), ..r };
|
||||||
|
let (cols, rows) = span(top, width, pm.len() / 4 / width.max(1));
|
||||||
|
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 row = &mut pm[y * width * 4..(y + 1) * width * 4];
|
||||||
|
for x in cols.clone() {
|
||||||
|
let k = (cover(r, radius, x, y) * a) >> 8;
|
||||||
|
if k > 0 {
|
||||||
|
blend(&mut row[x * 4..x * 4 + 4], white, k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How far (`x`, `y`) is outside `r` with its corners rounded by `radius`
|
||||||
|
/// (negative inside).
|
||||||
|
fn outside(r: Rect, radius: f32, x: f32, y: f32) -> f32 {
|
||||||
|
let (hw, hh) = (r.w / 2.0, r.h / 2.0);
|
||||||
|
let qx = libm::fabsf(x - (r.x + hw)) - (hw - radius);
|
||||||
|
let qy = libm::fabsf(y - (r.y + hh)) - (hh - radius);
|
||||||
|
let (ox, oy) = (qx.max(0.0), qy.max(0.0));
|
||||||
|
let out = if ox > 0.0 && oy > 0.0 { libm::sqrtf(ox * ox + oy * oy) } else { ox.max(oy) };
|
||||||
|
out + qx.max(qy).min(0.0) - radius
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A soft shadow under `r` (corners rounded by `radius`): cast `down`
|
||||||
|
/// pixels below it, `dark` (percent) at its edge and gone `spread` out.
|
||||||
|
/// Only where it shows is touched: what `r` covers 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 caster = 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, pm.len() / 4 / width.max(1));
|
||||||
|
// Well inside `r`: covered by it.
|
||||||
|
let (in_x, in_y) = (r.x + radius..r.x + r.w - radius, r.y..r.y + r.h);
|
||||||
|
for y in rows {
|
||||||
|
let fy = y as f32 + 0.5;
|
||||||
|
let row = &mut pm[y * width * 4..(y + 1) * width * 4];
|
||||||
|
for x in cols.clone() {
|
||||||
|
let fx = x as f32 + 0.5;
|
||||||
|
if in_x.contains(&fx) && in_y.contains(&fy) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let d = outside(caster, radius, fx, fy).max(0.0);
|
||||||
|
let fade = (1.0 - d / spread).max(0.0);
|
||||||
|
let k = (fade * fade * dark as f32 * 256.0 / 100.0) as u32;
|
||||||
|
if k > 0 {
|
||||||
|
blend(&mut row[x * 4..x * 4 + 4], [0, 0, 0, 255], k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A box blur run three times over (close to a Gaussian), `r` pixels each
|
||||||
|
/// way, rows then columns.
|
||||||
|
fn blur(img: &mut Image, r: usize) {
|
||||||
|
if r == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let (w, h) = (img.w, img.h);
|
||||||
|
let mut line = vec![[0u8; 4]; w.max(h)];
|
||||||
|
for _ in 0..3 {
|
||||||
|
for y in 0..h {
|
||||||
|
pass(&mut img.px, y * w, 1, w, r, &mut line);
|
||||||
|
}
|
||||||
|
for x in 0..w {
|
||||||
|
pass(&mut img.px, x, w, h, r, &mut line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One box pass along `n` pixels starting at `start`, `step` apart; the
|
||||||
|
/// edges are held.
|
||||||
|
fn pass(px: &mut [[u8; 4]], start: usize, step: usize, n: usize, r: usize, line: &mut [[u8; 4]]) {
|
||||||
|
let at = |i: usize| start + i.min(n - 1) * step;
|
||||||
|
let width = (2 * r + 1) as u32;
|
||||||
|
for c in 0..4 {
|
||||||
|
// The window centred on pixel 0, the edge held.
|
||||||
|
let mut sum = px[at(0)][c] as u32 * (r as u32 + 1) + (1..=r).map(|i| px[at(i)][c] as u32).sum::<u32>();
|
||||||
|
for (i, out) in line.iter_mut().take(n).enumerate() {
|
||||||
|
out[c] = (sum / width) as u8;
|
||||||
|
sum = sum + px[at(i + r + 1)][c] as u32 - px[at(i.saturating_sub(r))][c] as u32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (i, p) in line.iter().take(n).enumerate() {
|
||||||
|
px[start + i * step] = *p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,23 @@ pub struct Rect {
|
|||||||
pub h: f32,
|
pub h: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Rect {
|
||||||
|
pub fn overlaps(self, o: Rect) -> bool {
|
||||||
|
self.x < o.x + o.w && o.x < self.x + self.w && self.y < o.y + o.h && o.y < self.y + self.h
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bigger by `by` on every side.
|
||||||
|
pub fn grow(self, by: f32) -> Rect {
|
||||||
|
Rect { x: self.x - by, y: self.y - by, w: self.w + 2.0 * by, h: self.h + 2.0 * by }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The smallest rectangle holding both.
|
||||||
|
pub fn union(self, o: Rect) -> Rect {
|
||||||
|
let (x0, y0) = (self.x.min(o.x), self.y.min(o.y));
|
||||||
|
Rect { x: x0, y: y0, w: (self.x + self.w).max(o.x + o.w) - x0, h: (self.y + self.h).max(o.y + o.h) - y0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// How big text is, from whoever has the fonts.
|
/// How big text is, from whoever has the fonts.
|
||||||
pub trait Measure {
|
pub trait Measure {
|
||||||
fn text(&self, s: &str, style: TextStyle) -> (f32, f32);
|
fn text(&self, s: &str, style: TextStyle) -> (f32, f32);
|
||||||
|
|||||||
@@ -9,12 +9,14 @@
|
|||||||
extern crate alloc;
|
extern crate alloc;
|
||||||
|
|
||||||
pub mod element;
|
pub mod element;
|
||||||
|
pub mod glass;
|
||||||
pub mod layout;
|
pub mod layout;
|
||||||
pub mod motion;
|
pub mod motion;
|
||||||
pub mod render;
|
pub mod render;
|
||||||
pub mod run;
|
pub mod run;
|
||||||
pub mod text;
|
pub mod text;
|
||||||
pub mod theme;
|
pub mod theme;
|
||||||
|
mod tree;
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
|
|
||||||
pub use element::{Element, Size, button, column, list_item, row, text, text_input, toggle};
|
pub use element::{Element, Size, button, column, list_item, row, text, text_input, toggle};
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
//! Drawing a laid-out tree into a window's pixels (RGBA, premultiplied):
|
//! Drawing a laid-out tree into a window's pixels (RGBA, premultiplied):
|
||||||
//! the theme decides every colour and shape.
|
//! 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.
|
||||||
|
|
||||||
use alloc::string::String;
|
use alloc::string::String;
|
||||||
use tiny_skia::{Color, FillRule, Paint, PathBuilder, PixmapMut, Stroke, Transform};
|
use tiny_skia::{Color, FillRule, Paint, PathBuilder, PixmapMut, Stroke, Transform};
|
||||||
|
|
||||||
use crate::element::{Element, Kind};
|
use crate::element::{Element, Kind};
|
||||||
|
use crate::glass::Backdrop;
|
||||||
use crate::layout::Rect;
|
use crate::layout::Rect;
|
||||||
use crate::text::Fonts;
|
use crate::text::Fonts;
|
||||||
use crate::theme::{Rgba, TextStyle, Theme, Weight};
|
use crate::theme::{Rgba, TextStyle, Theme, Weight};
|
||||||
@@ -12,8 +15,14 @@ use crate::theme::{Rgba, TextStyle, Theme, Weight};
|
|||||||
/// The focus ring's distance outside the button, and its width.
|
/// The focus ring's distance outside the button, and its width.
|
||||||
const RING_GAP: f32 = 3.0;
|
const RING_GAP: f32 = 3.0;
|
||||||
const RING_WIDTH: f32 = 2.0;
|
const RING_WIDTH: f32 = 2.0;
|
||||||
/// How far outside an element its ring reaches.
|
/// How far outside an element its ring or its shadow reaches.
|
||||||
pub const RING_REACH: f32 = RING_GAP + RING_WIDTH + 1.0;
|
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;
|
||||||
/// A field's outline, and its caret, across.
|
/// A field's outline, and its caret, across.
|
||||||
const OUTLINE: f32 = 1.5;
|
const OUTLINE: f32 = 1.5;
|
||||||
const CARET: f32 = 2.0;
|
const CARET: f32 = 2.0;
|
||||||
@@ -56,6 +65,9 @@ pub struct Frame {
|
|||||||
pub flip: Option<(usize, f32)>,
|
pub flip: Option<(usize, f32)>,
|
||||||
/// How far in a new screen has come: 1 is all the way.
|
/// How far in a new screen has come: 1 is all the way.
|
||||||
pub enter: f32,
|
pub enter: f32,
|
||||||
|
/// Only this part is drawn (what moved), if not all: it must hold
|
||||||
|
/// whole elements (see `Ui::draw`).
|
||||||
|
pub clip: Option<Rect>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What drawing needs at hand: the pixels, the fonts, the theme, and the
|
/// What drawing needs at hand: the pixels, the fonts, the theme, and the
|
||||||
@@ -65,6 +77,7 @@ struct Painter<'p, 'a> {
|
|||||||
fonts: &'p Fonts,
|
fonts: &'p Fonts,
|
||||||
theme: &'p Theme,
|
theme: &'p Theme,
|
||||||
frame: Frame,
|
frame: Frame,
|
||||||
|
backdrop: Option<&'p Backdrop>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mix(a: Rgba, b: Rgba, t: f32) -> Rgba {
|
fn mix(a: Rgba, b: Rgba, t: f32) -> Rgba {
|
||||||
@@ -123,10 +136,10 @@ impl Painter<'_, '_> {
|
|||||||
*index += 1;
|
*index += 1;
|
||||||
let Some(&r) = rects.get(me) else { return };
|
let Some(&r) = rects.get(me) else { return };
|
||||||
let theme = self.theme;
|
let theme = self.theme;
|
||||||
// Off the pixels (drawing only part of the window): nothing to do.
|
// Off the window, or not in the part being drawn: nothing to do.
|
||||||
let (pw, ph) = (self.pm.width() as f32, self.pm.height() as f32);
|
let (pw, ph) = (self.pm.width() as f32, self.pm.height() as f32);
|
||||||
let outside = r.x >= pw || r.y >= ph || r.x + r.w <= 0.0 || r.y + r.h <= 0.0;
|
let all = Rect { x: 0.0, y: 0.0, w: pw, h: ph };
|
||||||
if outside && !matches!(e.kind, Kind::Flex(_)) {
|
if !matches!(e.kind, Kind::Flex(_)) && !(r.overlaps(all) && self.frame.clip.is_none_or(|c| r.overlaps(c))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
match &e.kind {
|
match &e.kind {
|
||||||
@@ -139,12 +152,14 @@ impl Painter<'_, '_> {
|
|||||||
Kind::Button(b) => {
|
Kind::Button(b) => {
|
||||||
let (fill, label) = theme.button(b.variant);
|
let (fill, label) = theme.button(b.variant);
|
||||||
if let Some(fill) = fill {
|
if let Some(fill) = fill {
|
||||||
|
self.shadow(r);
|
||||||
self.fill(r, theme.corner_radius, fill);
|
self.fill(r, theme.corner_radius, fill);
|
||||||
|
self.gloss(r);
|
||||||
}
|
}
|
||||||
self.text_in(&b.label, TextStyle::Label, r, true, label);
|
self.text_in(&b.label, TextStyle::Label, r, true, label);
|
||||||
}
|
}
|
||||||
Kind::Item(i) => {
|
Kind::Item(i) => {
|
||||||
self.fill(r, theme.corner_radius, theme.surface);
|
self.panel(r);
|
||||||
let inner = self.inset(r);
|
let inner = self.inset(r);
|
||||||
match &i.subtitle {
|
match &i.subtitle {
|
||||||
Some(sub) => {
|
Some(sub) => {
|
||||||
@@ -159,7 +174,7 @@ impl Painter<'_, '_> {
|
|||||||
self.text_in(">", TextStyle::Label, chevron, true, theme.text_secondary);
|
self.text_in(">", TextStyle::Label, chevron, true, theme.text_secondary);
|
||||||
}
|
}
|
||||||
Kind::Toggle(g) => {
|
Kind::Toggle(g) => {
|
||||||
self.fill(r, theme.corner_radius, theme.surface);
|
self.panel(r);
|
||||||
let inner = self.inset(r);
|
let inner = self.inset(r);
|
||||||
self.text_in(&g.label, TextStyle::Body, inner, false, theme.text_primary);
|
self.text_in(&g.label, TextStyle::Body, inner, false, theme.text_primary);
|
||||||
let (tw, th) = crate::layout::SWITCH;
|
let (tw, th) = crate::layout::SWITCH;
|
||||||
@@ -190,7 +205,7 @@ impl Painter<'_, '_> {
|
|||||||
/// placeholder dimmed, and the caret while it has focus.
|
/// placeholder dimmed, and the caret while it has focus.
|
||||||
fn field<E>(&mut self, i: &crate::element::Input<E>, r: Rect, focused: bool) {
|
fn field<E>(&mut self, i: &crate::element::Input<E>, r: Rect, focused: bool) {
|
||||||
let theme = self.theme;
|
let theme = self.theme;
|
||||||
self.fill(r, theme.corner_radius, theme.surface);
|
self.panel(r);
|
||||||
if let Some(path) = rounded(r, theme.corner_radius) {
|
if let Some(path) = rounded(r, theme.corner_radius) {
|
||||||
let stroke = Stroke { width: OUTLINE, ..Stroke::default() };
|
let stroke = Stroke { width: OUTLINE, ..Stroke::default() };
|
||||||
self.pm.stroke_path(&path, &paint(theme.surface_raised), &stroke, Transform::identity(), None);
|
self.pm.stroke_path(&path, &paint(theme.surface_raised), &stroke, Transform::identity(), None);
|
||||||
@@ -226,6 +241,34 @@ impl Painter<'_, '_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A glass panel: its shadow, the wallpaper blurred through it, the
|
||||||
|
/// surface over that, and its highlight.
|
||||||
|
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::glass::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::glass::gloss(self.pm.data_mut(), width, r, self.theme.corner_radius, self.theme.highlight, GLOSS);
|
||||||
|
}
|
||||||
|
|
||||||
/// `r` filled with `c`, its corners rounded by `radius`.
|
/// `r` filled with `c`, its corners rounded by `radius`.
|
||||||
fn fill(&mut self, r: Rect, radius: f32, c: Rgba) {
|
fn fill(&mut self, r: Rect, radius: f32, c: Rgba) {
|
||||||
if let Some(path) = rounded(r, radius) {
|
if let Some(path) = rounded(r, radius) {
|
||||||
@@ -240,12 +283,26 @@ impl Painter<'_, '_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draws `root`, laid out as `rects`, as `frame` says; a screen still
|
/// Draws `root`, laid out as `rects`, as `frame` says (all of it, or its
|
||||||
/// coming in is faded toward the background.
|
/// `clip`), over the wallpaper (`backdrop`, or the plain background). A
|
||||||
pub fn render<E>(pm: &mut PixmapMut, root: &Element<E>, rects: &[Rect], fonts: &Fonts, theme: &Theme, frame: Frame) {
|
/// 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>) {
|
||||||
let bg = theme.background;
|
let bg = theme.background;
|
||||||
pm.fill(Color::from_rgba8(bg.0, bg.1, bg.2, bg.3));
|
let all = Rect { x: 0.0, y: 0.0, w: pm.width() as f32, h: pm.height() as f32 };
|
||||||
let mut p = Painter { pm, fonts, theme, frame };
|
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 };
|
||||||
p.draw(root, rects, &mut 0);
|
p.draw(root, rects, &mut 0);
|
||||||
if let Some(r) = frame.ring {
|
if let Some(r) = frame.ring {
|
||||||
p.ring(r);
|
p.ring(r);
|
||||||
@@ -256,24 +313,3 @@ pub fn render<E>(pm: &mut PixmapMut, root: &Element<E>, rects: &[Rect], fonts: &
|
|||||||
p.fill(all, 0.0, veil);
|
p.fill(all, 0.0, veil);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draws only `part` of the window (what moved), into `pm` there: the
|
|
||||||
/// rest of `pm` is left as it was.
|
|
||||||
pub fn render_part<E>(pm: &mut PixmapMut, root: &Element<E>, rects: &[Rect], fonts: &Fonts, theme: &Theme, frame: Frame, part: Rect) {
|
|
||||||
let (x0, y0) = (libm::floorf(part.x.max(0.0)), libm::floorf(part.y.max(0.0)));
|
|
||||||
let x1 = libm::ceilf(part.x + part.w).min(pm.width() as f32);
|
|
||||||
let y1 = libm::ceilf(part.y + part.h).min(pm.height() as f32);
|
|
||||||
let (w, h) = ((x1 - x0) as u32, (y1 - y0) as u32);
|
|
||||||
let Some(mut piece) = (x1 > x0 && y1 > y0).then(|| tiny_skia::Pixmap::new(w, h)).flatten() else { return };
|
|
||||||
let shift = |r: Rect| Rect { x: r.x - x0, y: r.y - y0, ..r };
|
|
||||||
let moved: alloc::vec::Vec<Rect> = rects.iter().map(|r| shift(*r)).collect();
|
|
||||||
let frame = Frame { ring: frame.ring.map(shift), ..frame };
|
|
||||||
render(&mut piece.as_mut(), root, &moved, fonts, theme, frame);
|
|
||||||
let stride = pm.width() as usize * 4;
|
|
||||||
let row = w as usize * 4;
|
|
||||||
let data = pm.data_mut();
|
|
||||||
for (i, line) in piece.data().chunks_exact(row).enumerate() {
|
|
||||||
let at = (y0 as usize + i) * stride + x0 as usize * 4;
|
|
||||||
data[at..at + row].copy_from_slice(line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
80
crates/k-ui/src/tree.rs
Normal file
80
crates/k-ui/src/tree.rs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
//! Finding things in a view by their place in pre-order (an element, then
|
||||||
|
//! its children), the order layout, drawing and focus all walk it in.
|
||||||
|
|
||||||
|
use alloc::vec::Vec;
|
||||||
|
|
||||||
|
use crate::element::{Element, Input, Kind};
|
||||||
|
use crate::layout::Rect;
|
||||||
|
use crate::render::REACH;
|
||||||
|
|
||||||
|
/// The pre-order indices of every element focus can rest on in `e`.
|
||||||
|
pub fn pressables<E>(e: &Element<E>, index: &mut usize, out: &mut Vec<usize>) {
|
||||||
|
if e.kind.focusable() {
|
||||||
|
out.push(*index);
|
||||||
|
}
|
||||||
|
*index += 1;
|
||||||
|
if let Kind::Flex(f) = &e.kind {
|
||||||
|
for c in &f.children {
|
||||||
|
pressables(c, index, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The event of the pre-order `target`th element, if it is a button.
|
||||||
|
pub fn event_at<E: Clone>(e: &Element<E>, index: &mut usize, target: usize) -> Option<E> {
|
||||||
|
let me = *index;
|
||||||
|
*index += 1;
|
||||||
|
match &e.kind {
|
||||||
|
Kind::Flex(f) => f.children.iter().find_map(|c| event_at(c, index, target)),
|
||||||
|
k if me == target => k.event().cloned(),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The pre-order `target`th element, if it is a field.
|
||||||
|
pub fn input_at<'e, E>(e: &'e Element<E>, index: &mut usize, target: usize) -> Option<&'e Input<E>> {
|
||||||
|
let me = *index;
|
||||||
|
*index += 1;
|
||||||
|
match &e.kind {
|
||||||
|
Kind::Flex(f) => f.children.iter().find_map(|c| input_at(c, index, target)),
|
||||||
|
Kind::Input(i) if me == target => Some(i),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the pre-order `target`th element is a toggle.
|
||||||
|
pub fn toggle_at<E>(e: &Element<E>, index: &mut usize, target: usize) -> bool {
|
||||||
|
let me = *index;
|
||||||
|
*index += 1;
|
||||||
|
match &e.kind {
|
||||||
|
Kind::Flex(f) => f.children.iter().any(|c| toggle_at(c, index, target)),
|
||||||
|
Kind::Toggle(_) => me == target,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `part`, grown to take in every element it touches: drawn in pieces, a
|
||||||
|
/// shape's anti-aliased edge would come out a little different where a
|
||||||
|
/// piece cuts it.
|
||||||
|
pub fn whole_elements<E>(view: &Element<E>, rects: &[Rect], part: Rect) -> Rect {
|
||||||
|
let mut leaves = Vec::new();
|
||||||
|
leaf_rects(view, rects, &mut 0, &mut leaves);
|
||||||
|
let mut part = part;
|
||||||
|
// Taking one in may reach another: until nothing more is touched.
|
||||||
|
loop {
|
||||||
|
let grown = leaves.iter().map(|r| r.grow(REACH)).filter(|r| r.overlaps(part)).fold(part, Rect::union);
|
||||||
|
if grown == part {
|
||||||
|
return grown;
|
||||||
|
}
|
||||||
|
part = grown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn leaf_rects<E>(e: &Element<E>, rects: &[Rect], index: &mut usize, out: &mut Vec<Rect>) {
|
||||||
|
let me = *index;
|
||||||
|
*index += 1;
|
||||||
|
match &e.kind {
|
||||||
|
Kind::Flex(f) => f.children.iter().for_each(|c| leaf_rects(c, rects, index, out)),
|
||||||
|
_ => out.extend(rects.get(me)),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,10 +10,12 @@
|
|||||||
use alloc::vec::Vec;
|
use alloc::vec::Vec;
|
||||||
use tiny_skia::PixmapMut;
|
use tiny_skia::PixmapMut;
|
||||||
|
|
||||||
use crate::element::{Element, Input, Kind};
|
use crate::element::{Element, Input};
|
||||||
|
use crate::glass::Backdrop;
|
||||||
use crate::layout::{self, Rect};
|
use crate::layout::{self, Rect};
|
||||||
use crate::motion::{Ring, Tween};
|
use crate::motion::{Ring, Tween};
|
||||||
use crate::render::{self, Frame};
|
use crate::render::{self, Frame};
|
||||||
|
use crate::tree::{event_at, input_at, pressables, toggle_at, whole_elements};
|
||||||
use crate::text::{Fonts, Measurer};
|
use crate::text::{Fonts, Measurer};
|
||||||
use crate::theme::Theme;
|
use crate::theme::Theme;
|
||||||
|
|
||||||
@@ -89,46 +91,13 @@ pub struct Ui<A: App> {
|
|||||||
whole: bool,
|
whole: bool,
|
||||||
/// Where the ring was last drawn.
|
/// Where the ring was last drawn.
|
||||||
last_ring: Option<Rect>,
|
last_ring: Option<Rect>,
|
||||||
}
|
/// The wallpaper and its blur, for this theme and window size.
|
||||||
|
backdrop: Option<Backdrop>,
|
||||||
/// The pre-order indices of every element focus can rest on in `e`.
|
|
||||||
fn pressables<E>(e: &Element<E>, index: &mut usize, out: &mut Vec<usize>) {
|
|
||||||
if e.kind.focusable() {
|
|
||||||
out.push(*index);
|
|
||||||
}
|
|
||||||
*index += 1;
|
|
||||||
if let Kind::Flex(f) = &e.kind {
|
|
||||||
for c in &f.children {
|
|
||||||
pressables(c, index, out);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The event of the pre-order `target`th element, if it is a button.
|
|
||||||
fn event_at<E: Clone>(e: &Element<E>, index: &mut usize, target: usize) -> Option<E> {
|
|
||||||
let me = *index;
|
|
||||||
*index += 1;
|
|
||||||
match &e.kind {
|
|
||||||
Kind::Flex(f) => f.children.iter().find_map(|c| event_at(c, index, target)),
|
|
||||||
k if me == target => k.event().cloned(),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The pre-order `target`th element, if it is a field.
|
|
||||||
fn input_at<'e, E>(e: &'e Element<E>, index: &mut usize, target: usize) -> Option<&'e Input<E>> {
|
|
||||||
let me = *index;
|
|
||||||
*index += 1;
|
|
||||||
match &e.kind {
|
|
||||||
Kind::Flex(f) => f.children.iter().find_map(|c| input_at(c, index, target)),
|
|
||||||
Kind::Input(i) if me == target => Some(i),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<A: App> Ui<A> {
|
impl<A: App> Ui<A> {
|
||||||
pub fn new(theme: Theme) -> Self {
|
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 }
|
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 }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A new screen: from its top, coming in, the ring where it lands.
|
/// A new screen: from its top, coming in, the ring where it lands.
|
||||||
@@ -260,6 +229,7 @@ impl<A: App> Ui<A> {
|
|||||||
pub fn draw(&mut self, pm: &mut PixmapMut, fonts: &Fonts, now: u64) -> (bool, Option<Rect>) {
|
pub fn draw(&mut self, pm: &mut PixmapMut, fonts: &Fonts, now: u64) -> (bool, Option<Rect>) {
|
||||||
let view = self.app.view();
|
let view = self.app.view();
|
||||||
let (w, h) = (pm.width() as f32, pm.height() as f32);
|
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 };
|
let measurer = Measurer { fonts, theme: &self.theme };
|
||||||
// As tall as it needs: what does not fit is scrolled to.
|
// As tall as it needs: what does not fit is scrolled to.
|
||||||
let tall = layout::natural(&view, &measurer, &self.theme).1.max(h);
|
let tall = layout::natural(&view, &measurer, &self.theme).1.max(h);
|
||||||
@@ -297,21 +267,21 @@ impl<A: App> Ui<A> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let flip = self.flip.as_mut().map(|(i, t)| (*i, t.progress(now)));
|
let flip = self.flip.as_mut().map(|(i, t)| (*i, t.progress(now)));
|
||||||
let frame = Frame { focused, caret: self.caret, ring, flip, enter };
|
let frame = Frame { focused, caret: self.caret, ring, flip, enter, clip: None };
|
||||||
let full = self.whole || self.enter.is_some();
|
let full = self.whole || self.enter.is_some();
|
||||||
let part = [self.last_ring, ring, flip.and_then(|(i, _)| rects.get(i).copied())]
|
let part = [self.last_ring, ring, flip.and_then(|(i, _)| rects.get(i).copied())]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flatten()
|
.flatten()
|
||||||
.map(|r| grow(r, render::RING_REACH))
|
.map(|r| r.grow(render::REACH))
|
||||||
.reduce(union)
|
.reduce(Rect::union)
|
||||||
.map(|p| whole_elements(&view, &rects, p));
|
.map(|p| whole_elements(&view, &rects, p));
|
||||||
let drew = match (full, part) {
|
let drew = match (full, part) {
|
||||||
(true, _) => {
|
(true, _) => {
|
||||||
render::render(pm, &view, &rects, fonts, &self.theme, frame);
|
render::render(pm, &view, &rects, fonts, &self.theme, frame, self.backdrop.as_ref());
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
(false, Some(p)) => {
|
(false, Some(p)) => {
|
||||||
render::render_part(pm, &view, &rects, fonts, &self.theme, frame, p);
|
render::render(pm, &view, &rects, fonts, &self.theme, Frame { clip: Some(p), ..frame }, self.backdrop.as_ref());
|
||||||
Some(p)
|
Some(p)
|
||||||
}
|
}
|
||||||
// Nothing moved and nothing changed.
|
// Nothing moved and nothing changed.
|
||||||
@@ -330,51 +300,9 @@ impl<A: App> Ui<A> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `part`, grown to take in every element it touches: drawn in pieces, a
|
|
||||||
/// shape's anti-aliased edge would come out a little different where a
|
|
||||||
/// piece cuts it.
|
|
||||||
fn whole_elements<E>(view: &Element<E>, rects: &[Rect], part: Rect) -> Rect {
|
|
||||||
let mut leaves = Vec::new();
|
|
||||||
leaf_rects(view, rects, &mut 0, &mut leaves);
|
|
||||||
leaves.into_iter().map(|r| grow(r, 1.0)).filter(|r| overlap(*r, part)).fold(part, union)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn leaf_rects<E>(e: &Element<E>, rects: &[Rect], index: &mut usize, out: &mut Vec<Rect>) {
|
|
||||||
let me = *index;
|
|
||||||
*index += 1;
|
|
||||||
match &e.kind {
|
|
||||||
Kind::Flex(f) => f.children.iter().for_each(|c| leaf_rects(c, rects, index, out)),
|
|
||||||
_ => out.extend(rects.get(me)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn overlap(a: Rect, b: Rect) -> bool {
|
|
||||||
a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h
|
|
||||||
}
|
|
||||||
|
|
||||||
fn grow(r: Rect, by: f32) -> Rect {
|
|
||||||
Rect { x: r.x - by, y: r.y - by, w: r.w + 2.0 * by, h: r.h + 2.0 * by }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn union(a: Rect, b: Rect) -> Rect {
|
|
||||||
let (x0, y0) = (a.x.min(b.x), a.y.min(b.y));
|
|
||||||
Rect { x: x0, y: y0, w: (a.x + a.w).max(b.x + b.w) - x0, h: (a.y + a.h).max(b.y + b.h) - y0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// How far right a new screen starts, in pixels.
|
/// How far right a new screen starts, in pixels.
|
||||||
const SLIDE: f32 = 32.0;
|
const SLIDE: f32 = 32.0;
|
||||||
|
|
||||||
/// Whether the pre-order `target`th element is a toggle.
|
|
||||||
fn toggle_at<E>(e: &Element<E>, index: &mut usize, target: usize) -> bool {
|
|
||||||
let me = *index;
|
|
||||||
*index += 1;
|
|
||||||
match &e.kind {
|
|
||||||
Kind::Flex(f) => f.children.iter().any(|c| toggle_at(c, index, target)),
|
|
||||||
Kind::Toggle(_) => me == target,
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The scroll that shows `top..top + height` within a window `window` high
|
/// The scroll that shows `top..top + height` within a window `window` high
|
||||||
/// over a view `view` high, moving as little as possible from `scroll`.
|
/// over a view `view` high, moving as little as possible from `scroll`.
|
||||||
fn keep_in_sight(scroll: f32, top: f32, height: f32, window: f32, view: f32) -> f32 {
|
fn keep_in_sight(scroll: f32, top: f32, height: f32, window: f32, view: f32) -> f32 {
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
//! Contrast as WCAG 2 measures it: the ratio of two colours' relative
|
//! 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,
|
//! 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.
|
//! 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::parse::Error;
|
||||||
use crate::{Rgba, Theme};
|
use crate::{Rgba, Theme};
|
||||||
@@ -22,25 +25,48 @@ pub fn ratio(a: Rgba, b: Rgba) -> f32 {
|
|||||||
const TEXT: f32 = 4.5;
|
const TEXT: f32 = 4.5;
|
||||||
const SHAPE: f32 = 3.0;
|
const SHAPE: f32 = 3.0;
|
||||||
|
|
||||||
/// Every pairing K-UI draws, each with what it must reach.
|
/// Pairings drawn on solid colours, each with what it must reach.
|
||||||
fn pairings(t: &Theme) -> [(&'static str, Rgba, Rgba, f32); 10] {
|
fn solid(t: &Theme) -> [(&'static str, Rgba, Rgba, f32); 4] {
|
||||||
[
|
[
|
||||||
("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_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_accent on accent", t.on_accent, t.accent, TEXT),
|
||||||
("on_destructive on destructive", t.on_destructive, t.destructive, TEXT),
|
("on_destructive on destructive", t.on_destructive, t.destructive, TEXT),
|
||||||
("accent on surface", t.accent, t.surface, SHAPE),
|
("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.
|
/// The theme, if everything drawn with it can be read.
|
||||||
pub fn check(t: Theme) -> Result<Theme, Error> {
|
pub fn check(t: Theme) -> Result<Theme, Error> {
|
||||||
for (pair, fg, bg, min) in pairings(&t) {
|
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 {
|
||||||
let r = ratio(fg, bg);
|
let r = ratio(fg, bg);
|
||||||
if r < min {
|
if r < min {
|
||||||
return Err(Error::Contrast { pair, ratio_x100: (r * 100.0) as u32 });
|
return Err(Error::Contrast { pair, ratio_x100: (r * 100.0) as u32 });
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ pub mod wire;
|
|||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
pub use accent::with_accent;
|
pub use accent::with_accent;
|
||||||
pub use contrast::{check, ratio};
|
pub use contrast::{check, glass, ratio, wallpaper};
|
||||||
pub use parse::{Error, parse};
|
pub use parse::{Error, parse};
|
||||||
|
|
||||||
/// A colour as the theme names it: straight (not premultiplied) RGBA.
|
/// A colour as the theme names it: straight (not premultiplied) RGBA.
|
||||||
@@ -83,6 +83,9 @@ pub enum Weight {
|
|||||||
SemiBold,
|
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.
|
/// A theme's name: what Settings lists and a user's choice records.
|
||||||
pub const NAME_BYTES: usize = types::name::BYTES;
|
pub const NAME_BYTES: usize = types::name::BYTES;
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,10 @@ fn a_theme_survives_the_wire_but_its_name() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn unreadable_pairings_are_refused() {
|
fn unreadable_pairings_are_refused() {
|
||||||
let grey = DARK.replace("text_secondary = \"#9e9eb8\"", "text_secondary = \"#3a3a4a\"");
|
let grey = DARK.replace("text_secondary = \"#9e9eb8\"", "text_secondary = \"#3a3a4a\"");
|
||||||
assert!(matches!(check(parse(&grey).unwrap()), Err(Error::Contrast { pair: "text_secondary on background", .. })));
|
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!((ratio(Rgba::rgb(0), Rgba::rgb(0xffffff)) - 21.0).abs() < 0.01);
|
assert!((ratio(Rgba::rgb(0), Rgba::rgb(0xffffff)) - 21.0).abs() < 0.01);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ name = "Whimsy Light"
|
|||||||
background = "#f3f2f8"
|
background = "#f3f2f8"
|
||||||
surface = "#ffffff"
|
surface = "#ffffff"
|
||||||
surface_raised = "#e6e4f0"
|
surface_raised = "#e6e4f0"
|
||||||
accent = "#0b6bcb"
|
accent = "#0a5cb0"
|
||||||
on_accent = "#ffffff"
|
on_accent = "#ffffff"
|
||||||
text_primary = "#1a1a2e"
|
text_primary = "#1a1a2e"
|
||||||
text_secondary = "#55556e"
|
text_secondary = "#55556e"
|
||||||
|
|||||||
@@ -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)
|
command = "snake" # optional: what the shell runs it as (default: binary)
|
||||||
author = "K-OS" # optional
|
author = "K-OS" # optional
|
||||||
icon = "icon.png" # optional: a file in the app's folder
|
icon = "icon.png" # optional: a file in the app's folder
|
||||||
memory = "3 MiB" # optional: how much it may use, KiB or MiB,
|
memory = "6 MiB" # optional: how much it may use, KiB or MiB,
|
||||||
# 64 KiB to 16 MiB (default 512 KiB)
|
# 64 KiB to 16 MiB (default 512 KiB)
|
||||||
|
|
||||||
[capabilities]
|
[capabilities]
|
||||||
|
|||||||
Reference in New Issue
Block a user