feat(theme): themes as files (theme crate: toml subset + ints, contrast check, wire); whimsy dark + light; display reads themes/, serves THEME/THEME_NAME, init SET_THEME -> THEME_CHANGED to all windows, bar themed; k-ui follows; settings appearance; init keeps choice in sealed users/<id>/settings/, reapplies at sign-in + display restart

This commit is contained in:
2026-10-28 13:36:57 +01:00
parent 29becf297c
commit 6827e1455d
43 changed files with 1140 additions and 179 deletions

View File

@@ -61,7 +61,7 @@ impl Host for FakeHost {
type Srv = DisplayServer<GridFramebuffer<W, H>, FakeHost>;
fn server() -> Srv {
DisplayServer::new(GridFramebuffer::new(), FakeHost::default())
DisplayServer::new(GridFramebuffer::new(), FakeHost::default(), crate::themes::Themes::new())
}
/// Opens a window through launcher `opener`: its badge.
@@ -187,3 +187,48 @@ fn a_present_outside_the_window_is_clipped() {
assert_eq!(s.framebuffer().pixel(31, H - 1), Some(RED));
assert_ne!(s.framebuffer().pixel(29, H - 1), Some(RED), "left of the rectangle is untouched");
}
#[test]
fn init_switches_the_theme_and_every_window_hears_of_it() {
let light = include_str!("../../../../../disk/themes/whimsy-light.toml");
let mut themes = crate::themes::Themes::new();
themes.add(light).unwrap();
let mut s = DisplayServer::new(GridFramebuffer::new(), FakeHost::default(), themes);
let t = open(&mut s, 1, b"Terminal");
let snake = open(&mut s, 2, b"Snake");
let (n1, n2) = (CapSlot::new(9).unwrap(), CapSlot::new(10).unwrap());
s.handle_call(t, [w::SUBSCRIBE, 0, 0, 0], Some(n1));
s.handle_call(snake, [w::SUBSCRIBE, 0, 0, 0], Some(n2));
let names: Vec<_> = (0..)
.map_while(|i| match s.handle_call(t, [w::THEME_NAME, i, 0, 0], None).0 {
[w::OK, a, b, c] => {
let (n, len) = w::unpack_name([a, b, c]);
Some(n[..len].to_vec())
}
_ => None,
})
.collect();
assert_eq!(names, [b"Whimsy Dark".to_vec(), b"Whimsy Light".to_vec()]);
let read = |s: &mut Srv| -> (usize, theme::Theme) {
let mut words = [0; w::THEME_WORDS];
let mut head = 0;
for part in 0..w::THEME_WORDS / 2 {
let r = s.handle_call(snake, [w::THEME, part, 0, 0], None).0;
assert_eq!(r[0], w::OK);
head = r[1];
words[part * 2..part * 2 + 2].copy_from_slice(&r[2..]);
}
(head >> 32, theme::wire::decode(words))
};
assert_eq!(read(&mut s).0, 0);
let n = w::pack_name(b"Whimsy Light");
assert_eq!(s.handle_call(5, [w::SET_THEME, n[0], n[1], n[2]], None).0[0], MALFORMED, "init's alone");
assert_eq!(s.handle_call(0, [w::SET_THEME, n[0], n[1], n[2]], None).0[0], w::OK);
let (index, t2) = read(&mut s);
assert_eq!((index, t2.background), (1, theme::parse(light).unwrap().background));
assert!(s.host().signalled.contains(&n1) && s.host().signalled.contains(&n2), "behind or in front");
assert_eq!(s.handle_call(t, [w::POLL, 0, 0, 0], None).0[0], w::THEME_CHANGED);
let bogus = w::pack_name(b"Nope");
assert_eq!(s.handle_call(0, [w::SET_THEME, bogus[0], bogus[1], bogus[2]], None).0[0], MALFORMED);
}