feat(theme): per-app accent: pm gives window manifest accent before app runs (display ACCENT, refused after BUFFER); theme::with_accent keeps hue, shifts lightness until contrast passes, else theme accent; tally pink

This commit is contained in:
2026-10-28 13:40:38 +01:00
parent 6827e1455d
commit 2b82f052ae
14 changed files with 158 additions and 16 deletions

View File

@@ -215,20 +215,24 @@ impl<F: Framebuffer, H: Host> DisplayServer<F, H> {
/// A request through window `index`'s capability.
fn window_call(&mut self, index: usize, badge: usize, words: [usize; 4], cap: Option<CapSlot>) -> ([usize; 4], Option<Give>) {
match (words[0], cap) {
(w::BUFFER, _) => match self.host.buffer(index, self.pixels()) {
Some(buffer) => {
let (width, height) = self.window_size();
([w::OK, width.raw() as usize, height.raw() as usize, 0], Some(Give::from(buffer)))
(w::BUFFER, _) => {
// The app runs: its accent is settled.
self.windows.start(badge);
match self.host.buffer(index, self.pixels()) {
Some(buffer) => {
let (width, height) = self.window_size();
([w::OK, width.raw() as usize, height.raw() as usize, 0], Some(Give::from(buffer)))
}
None => ([MALFORMED, 0, 0, 0], None),
}
None => ([MALFORMED, 0, 0, 0], None),
},
}
(w::ACCENT, _) if words[1] <= 0xff_ffff && self.windows.set_accent(badge, words[1] as u32) => ([w::OK, 0, 0, 0], None),
(w::PRESENT, _) => {
let (x, y, width, height) = w::unpack_rect([words[1], words[2]]);
self.present(index, x, y, width, height);
([w::OK, 0, 0, 0], None)
}
(w::SUBSCRIBE, Some(notify)) if self.windows.subscribe(badge, notify) => ([w::OK, 0, 0, 0], None),
(w::THEME, _) => match self.themes.part(words[1]) {
(w::THEME, _) => match self.themes.part(words[1], self.windows.accent(badge)) {
Some([a, b, c]) => ([w::OK, a, b, c], None),
None => ([MALFORMED, 0, 0, 0], None),
},
@@ -239,6 +243,7 @@ impl<F: Framebuffer, H: Host> DisplayServer<F, H> {
}
None => ([MALFORMED, 0, 0, 0], None),
},
(w::SUBSCRIBE, Some(notify)) if self.windows.subscribe(badge, notify) => ([w::OK, 0, 0, 0], None),
(w::POLL, _) => (self.windows.poll(badge).unwrap_or([0; 4]), None),
(w::CLOSE, _) => {
self.close(index);

View File

@@ -232,3 +232,23 @@ fn init_switches_the_theme_and_every_window_hears_of_it() {
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);
}
#[test]
fn only_the_launcher_sets_an_accent_and_it_is_in_the_theme() {
let mut s = server();
let app = open(&mut s, 2, b"Tally");
let other = open(&mut s, 3, b"Snake");
let pink = 0xe91e63;
assert_eq!(s.handle_call(app, [w::ACCENT, pink, 0, 0], None).0[0], w::OK);
assert_eq!(s.handle_call(app, [w::ACCENT, 0x00ff00, 0, 0], None).0[0], MALFORMED, "once");
// The app starts: too late for it to choose one itself.
s.handle_call(other, [w::BUFFER, 0, 0, 0], None);
assert_eq!(s.handle_call(other, [w::ACCENT, pink, 0, 0], None).0[0], MALFORMED);
let accent = |s: &mut Srv, badge| {
let r = s.handle_call(badge, [w::THEME, 0, 0, 0], None).0;
// Colour 3, the top of the theme's second word.
theme::Rgba::rgb((r[3] >> 32) as u32 & 0xff_ffff)
};
assert_eq!(accent(&mut s, other), theme::Theme::WHIMSY_DARK.accent);
assert_eq!(accent(&mut s, app), theme::with_accent(theme::Theme::WHIMSY_DARK, theme::Rgba::rgb(pink as u32)).accent);
}

View File

@@ -56,10 +56,14 @@ impl Themes {
Some(changed)
}
/// Part `part` of the theme in use, for a window: `changes | index <<
/// 32`, and two of its words.
pub fn part(&self, part: usize) -> Option<[usize; 3]> {
let words = theme::wire::encode(self.active());
/// Part `part` of the theme in use, for a window (with its app's
/// accent, if any): `changes | index << 32`, and two of its words.
pub fn part(&self, part: usize, accent: Option<u32>) -> Option<[usize; 3]> {
let t = match accent {
Some(rgb) => theme::with_accent(*self.active(), theme::Rgba::rgb(rgb)),
None => *self.active(),
};
let words = theme::wire::encode(&t);
let pair = words.get(part * 2..part * 2 + 2)?;
Some([self.changes & 0xffff_ffff | self.active << 32, pair[0], pair[1]])
}

View File

@@ -50,6 +50,10 @@ struct Window<N> {
name_len: usize,
notify: Option<N>,
queue: Queue,
/// Its app's accent, `0xrrggbb`.
accent: Option<u32>,
/// Its buffer was asked for: the app runs, so the accent is settled.
started: bool,
}
pub struct Windows<N: Copy> {
@@ -73,7 +77,7 @@ impl<N: Copy> Windows<N> {
let mut n = [0u8; NAME_BYTES];
let name_len = name.len().min(NAME_BYTES);
n[..name_len].copy_from_slice(&name[..name_len]);
self.slots[index] = Some(Window { badge, opener, name: n, name_len, notify: None, queue: Queue::new() });
self.slots[index] = Some(Window { badge, opener, name: n, name_len, notify: None, queue: Queue::new(), accent: None, started: false });
self.front = Some(index);
Some((index, badge))
}
@@ -119,6 +123,27 @@ impl<N: Copy> Windows<N> {
self.front
}
/// Gives the window `badge` its app's accent: once, before it starts.
pub fn set_accent(&mut self, badge: usize, rgb: u32) -> bool {
let Some(w) = self.slot(badge).and_then(|i| self.slots[i].as_mut()) else { return false };
let ok = !w.started && w.accent.is_none();
if ok {
w.accent = Some(rgb);
}
ok
}
/// The window `badge`'s app is running (its buffer was asked for).
pub fn start(&mut self, badge: usize) {
if let Some(w) = self.slot(badge).and_then(|i| self.slots[i].as_mut()) {
w.started = true;
}
}
pub fn accent(&self, badge: usize) -> Option<u32> {
self.slots[self.slot(badge)?].as_ref()?.accent
}
/// Tells `notify` when input for the window `badge` arrives.
pub fn subscribe(&mut self, badge: usize, notify: N) -> bool {
let Some(w) = self.slot(badge).and_then(|i| self.slots[i].as_mut()) else { return false };

View File

@@ -201,6 +201,10 @@ impl PermissionServer {
true => self.display.and_then(|d| libkos::display::open(d, plan.app.name.as_str().as_bytes())).map(|o| o.window),
false => None,
};
// Its brand colour too, which the display adapts to the theme.
if let (Some(w), Some(accent)) = (window, plan.app.accent) {
libkos::display::set_accent(w, accent.0);
}
// Managing others through a copy of this endpoint that can do no
// more (see `MANAGE_BADGE`).
let manage = match plan.granted.contains(Capability::Permissions) {