diff --git a/crates/apps/settings/manifest.toml b/crates/apps/settings/manifest.toml index 19b8a8d..82390e8 100644 --- a/crates/apps/settings/manifest.toml +++ b/crates/apps/settings/manifest.toml @@ -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"] diff --git a/crates/apps/tally/manifest.toml b/crates/apps/tally/manifest.toml index 2a78887..f2e5afd 100644 --- a/crates/apps/tally/manifest.toml +++ b/crates/apps/tally/manifest.toml @@ -4,7 +4,7 @@ name = "Tally" version = "1.0.0" binary = "tally" author = "K-OS" -memory = "6 MiB" +memory = "3 MiB" [capabilities] required = ["display"] diff --git a/crates/k-ui/src/glass.rs b/crates/k-ui/src/glass.rs deleted file mode 100644 index 90e2733..0000000 --- a/crates/k-ui/src/glass.rs +++ /dev/null @@ -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, t: &Theme, w: u32, h: u32) -> Option { - 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 = 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"); - } -} diff --git a/crates/k-ui/src/lib.rs b/crates/k-ui/src/lib.rs index d309555..eeb7d0e 100644 --- a/crates/k-ui/src/lib.rs +++ b/crates/k-ui/src/lib.rs @@ -9,7 +9,6 @@ extern crate alloc; pub mod element; -pub mod glass; pub mod layout; mod pixels; pub mod motion; diff --git a/crates/k-ui/src/pixels.rs b/crates/k-ui/src/pixels.rs index fb6a045..ba3a35f 100644 --- a/crates/k-ui/src/pixels.rs +++ b/crates/k-ui/src/pixels.rs @@ -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, core::ops::Range) { +fn span(r: Rect, width: usize, height: usize) -> (core::ops::Range, core::ops::Range) { 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) -> core::ops::Range { - 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, 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> 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; } diff --git a/crates/k-ui/src/render.rs b/crates/k-ui/src/render.rs index bd36916..9b5f7b0 100644 --- a/crates/k-ui/src/render.rs +++ b/crates/k-ui/src/render.rs @@ -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(pm: &mut PixmapMut, root: &Element, 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(pm: &mut PixmapMut, root: &Element, rects: &[Rect], fonts: &Fonts, theme: &Theme, frame: Frame) { let bg = theme.background; + let width = pm.width() as usize; 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 }; + 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); } } } diff --git a/crates/k-ui/src/ui.rs b/crates/k-ui/src/ui.rs index 8213985..92f547e 100644 --- a/crates/k-ui/src/ui.rs +++ b/crates/k-ui/src/ui.rs @@ -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 { whole: bool, /// Where the ring was last drawn. last_ring: Option, - /// The wallpaper and its blur, for this theme and window size. - backdrop: Option, } impl Ui { 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 Ui { pub fn draw(&mut self, pm: &mut PixmapMut, fonts: &Fonts, now: u64) -> (bool, Option) { 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 Ui { .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. diff --git a/crates/theme/src/contrast.rs b/crates/theme/src/contrast.rs index 2d13e0c..efa63dd 100644 --- a/crates/theme/src/contrast.rs +++ b/crates/theme/src/contrast.rs @@ -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 { - 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 }); diff --git a/crates/theme/src/lib.rs b/crates/theme/src/lib.rs index e83c987..d33c950 100644 --- a/crates/theme/src/lib.rs +++ b/crates/theme/src/lib.rs @@ -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] { diff --git a/crates/theme/src/parse.rs b/crates/theme/src/parse.rs index a5e1055..ddeb4a8 100644 --- a/crates/theme/src/parse.rs +++ b/crates/theme/src/parse.rs @@ -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 { @@ -57,8 +54,7 @@ fn once(slot: &mut Option, value: Option, line: usize) -> Result<(), To pub fn parse(text: &str) -> Result { let mut name: Option<([u8; NAME_BYTES], usize)> = None; let mut colours: [Option; 10] = [None; 10]; - let mut orbs: Option<[Rgba; 3]> = None; - let mut numbers: [Option; 9] = [None; 9]; + let mut numbers: [Option; 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 { 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 { 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 { 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 { 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, }) } diff --git a/crates/theme/src/tests.rs b/crates/theme/src/tests.rs index 2965e8e..66720d8 100644 --- a/crates/theme/src/tests.rs +++ b/crates/theme/src/tests.rs @@ -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] diff --git a/crates/theme/src/wire.rs b/crates/theme/src/wire.rs index 3dbd14c..46203bf 100644 --- a/crates/theme/src/wire.rs +++ b/crates/theme/src/wire.rs @@ -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, } } diff --git a/disk/themes/whimsy-dark.toml b/disk/themes/whimsy-dark.toml index be8475f..cbcdeaa 100644 --- a/disk/themes/whimsy-dark.toml +++ b/disk/themes/whimsy-dark.toml @@ -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 diff --git a/disk/themes/whimsy-light.toml b/disk/themes/whimsy-light.toml index e63bca1..2d73cea 100644 --- a/disk/themes/whimsy-light.toml +++ b/disk/themes/whimsy-light.toml @@ -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 diff --git a/docs/apps.md b/docs/apps.md index 0d7c2a0..6de14ed 100644 --- a/docs/apps.md +++ b/docs/apps.md @@ -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]