334 lines
14 KiB
Rust
334 lines
14 KiB
Rust
extern crate std;
|
|
|
|
use crate::server::{BAR_HEIGHT, DisplayServer, Host};
|
|
use driver_framebuffer_mock::GridFramebuffer;
|
|
use server_framework::{MALFORMED, Server};
|
|
use std::vec;
|
|
use std::vec::Vec;
|
|
use types::bounded::CapSlot;
|
|
use types::display::Color;
|
|
use types::display::window::{self as w, BADGE};
|
|
|
|
const W: usize = 32;
|
|
const H: usize = 16 + 8;
|
|
|
|
/// Buffers in memory; a capability is its slot number plus 40.
|
|
#[derive(Default)]
|
|
struct FakeHost {
|
|
/// Each slot's pixels, the bytes R, G, B, A.
|
|
buffers: Vec<Option<Vec<u8>>>,
|
|
minted: Vec<usize>,
|
|
revoked: Vec<CapSlot>,
|
|
reclaimed: Vec<usize>,
|
|
signalled: Vec<CapSlot>,
|
|
events: Vec<[usize; 4]>,
|
|
}
|
|
|
|
impl Host for FakeHost {
|
|
fn buffer(&mut self, index: usize, pixels: usize) -> Option<CapSlot> {
|
|
if self.buffers.len() <= index {
|
|
self.buffers.resize(index + 1, None);
|
|
}
|
|
self.buffers[index].get_or_insert_with(|| vec![0; pixels * 4]);
|
|
CapSlot::new(40 + index as u8)
|
|
}
|
|
fn blank(&mut self, index: usize) {
|
|
if let Some(b) = self.buffers[index].as_mut() {
|
|
b.fill(0);
|
|
}
|
|
}
|
|
fn pixels(&self, index: usize, from: usize, count: usize) -> &[u8] {
|
|
&self.buffers[index].as_ref().unwrap()[from * 4..(from + count) * 4]
|
|
}
|
|
fn reclaim(&mut self, index: usize) {
|
|
self.reclaimed.push(index);
|
|
}
|
|
fn mint(&mut self, badge: usize) -> Option<CapSlot> {
|
|
self.minted.push(badge);
|
|
CapSlot::new(20 + self.minted.len() as u8)
|
|
}
|
|
fn revoke(&mut self, cap: CapSlot) {
|
|
self.revoked.push(cap);
|
|
}
|
|
fn signal(&mut self, notify: CapSlot) {
|
|
self.signalled.push(notify);
|
|
}
|
|
fn input(&mut self) -> Option<[usize; 4]> {
|
|
(!self.events.is_empty()).then(|| self.events.remove(0))
|
|
}
|
|
}
|
|
|
|
/// A window buffer all one colour, as an app would draw it.
|
|
fn fill(buf: &mut [u8], c: Color) {
|
|
for px in buf.chunks_exact_mut(4) {
|
|
px.copy_from_slice(&c.to_rgba());
|
|
}
|
|
}
|
|
|
|
type Srv = DisplayServer<GridFramebuffer<W, H>, FakeHost>;
|
|
|
|
fn server() -> Srv {
|
|
DisplayServer::new(GridFramebuffer::new(), FakeHost::default(), crate::themes::Themes::new())
|
|
}
|
|
|
|
/// Opens a window through launcher `opener`: its badge.
|
|
fn open(s: &mut Srv, opener: usize, name: &[u8]) -> usize {
|
|
let n = w::pack_name(name);
|
|
let (reply, cap) = s.handle_call(opener, [w::OPEN, n[0], n[1], n[2]], None);
|
|
assert_eq!(reply, [w::OK, W, H - BAR_HEIGHT as usize, 0]);
|
|
assert!(cap.is_some_and(|g| g.keep_delegate), "the launcher may pass it on");
|
|
*s.host().minted.last().unwrap()
|
|
}
|
|
|
|
/// Paints window `slot`'s buffer one colour and presents all of it.
|
|
fn paint(s: &mut Srv, badge: usize, slot: usize, color: Color) -> [usize; 4] {
|
|
// The app writes its buffer directly; here, through the fake.
|
|
fill(s.host_mut_for_test().buffers[slot].as_mut().unwrap(), color);
|
|
let rect = w::pack_rect(
|
|
types::display::ScreenCoord::new(0),
|
|
types::display::ScreenCoord::new(0),
|
|
types::display::Dimension::new(W as u16),
|
|
types::display::Dimension::new(H as u16),
|
|
);
|
|
s.handle_call(badge, [w::PRESENT, rect[0], rect[1], 0], None).0
|
|
}
|
|
|
|
fn shown(s: &Srv) -> Option<Color> {
|
|
s.framebuffer().pixel(W / 2, H - 1)
|
|
}
|
|
|
|
const RED: Color = Color::rgb(200, 0, 0);
|
|
const BLUE: Color = Color::rgb(0, 0, 200);
|
|
|
|
#[test]
|
|
fn the_front_window_is_shown_below_the_bar() {
|
|
let mut s = server();
|
|
let t = open(&mut s, 1, b"Terminal");
|
|
assert_eq!(paint(&mut s, t, 0, RED)[0], w::OK);
|
|
assert_eq!(shown(&s), Some(RED));
|
|
assert_ne!(s.framebuffer().pixel(W - 1, 0), Some(RED), "the bar is the display's");
|
|
assert_eq!(s.framebuffer().pixel(0, BAR_HEIGHT as usize), Some(RED), "the window starts under it");
|
|
}
|
|
|
|
#[test]
|
|
fn a_window_behind_draws_nothing_until_it_is_in_front() {
|
|
let mut s = server();
|
|
let t = open(&mut s, 1, b"Terminal");
|
|
paint(&mut s, t, 0, RED);
|
|
let snake = open(&mut s, 2, b"Snake");
|
|
assert_eq!(shown(&s), Some(Color::from_raw(0)), "a new window starts blank");
|
|
paint(&mut s, t, 0, BLUE);
|
|
assert_eq!(shown(&s), Some(Color::from_raw(0)), "the terminal is behind");
|
|
paint(&mut s, snake, 1, RED);
|
|
assert_eq!(shown(&s), Some(RED));
|
|
// The switch key brings the terminal back, as it last drew.
|
|
s.host_mut_for_test().events = vec![[1, 10, 1, 0], [2, 10, 1, 0]];
|
|
s.signal(0);
|
|
assert_eq!(shown(&s), Some(BLUE));
|
|
}
|
|
|
|
#[test]
|
|
fn input_goes_to_the_front_window_and_the_switch_key_to_nobody() {
|
|
let mut s = server();
|
|
let t = open(&mut s, 1, b"Terminal");
|
|
let notify = CapSlot::new(9).unwrap();
|
|
assert_eq!(s.handle_call(t, [w::SUBSCRIBE, 0, 0, 0], Some(notify)).0[0], w::OK);
|
|
s.host_mut_for_test().events = vec![[1, 9, b'a' as usize, 0], [1, 10, 1, 0]];
|
|
s.signal(0);
|
|
assert_eq!(s.host().signalled, [notify]);
|
|
assert_eq!(s.handle_call(t, [w::POLL, 0, 0, 0], None).0, [1, 9, b'a' as usize, 0]);
|
|
assert_eq!(s.handle_call(t, [w::POLL, 0, 0, 0], None).0, [0; 4], "F1 was the display's");
|
|
}
|
|
|
|
#[test]
|
|
fn only_launchers_open_windows_and_only_init_sets_the_user() {
|
|
let mut s = server();
|
|
let t = open(&mut s, 1, b"Terminal");
|
|
let n = w::pack_name(b"Fake");
|
|
assert_eq!(s.handle_call(t, [w::OPEN, n[0], n[1], n[2]], None).0[0], MALFORMED, "not through a window");
|
|
assert_eq!(s.handle_call(1, [w::SET_USER, n[0], n[1], n[2]], None).0[0], MALFORMED);
|
|
assert_eq!(s.handle_call(0, [w::SET_USER, n[0], n[1], n[2]], None).0[0], w::OK);
|
|
assert_eq!(s.handle_call(BADGE | 12345, [w::POLL, 0, 0, 0], None).0[0], MALFORMED, "a made-up window");
|
|
}
|
|
|
|
#[test]
|
|
fn closing_takes_the_window_and_its_buffer_back() {
|
|
let mut s = server();
|
|
let t = open(&mut s, 1, b"Terminal");
|
|
let snake = open(&mut s, 2, b"Snake");
|
|
paint(&mut s, t, 0, BLUE);
|
|
assert_eq!(s.handle_call(snake, [w::CLOSE, 0, 0, 0], None).0[0], w::OK);
|
|
assert_eq!(s.host().revoked, [CapSlot::new(22).unwrap()]);
|
|
assert_eq!(s.host().reclaimed, [1]);
|
|
assert_eq!(shown(&s), Some(BLUE), "the terminal is in front again");
|
|
assert_eq!(s.handle_call(snake, [w::POLL, 0, 0, 0], None).0[0], MALFORMED, "closed");
|
|
// Its buffer is reused, blank, for the next window.
|
|
fill(s.host_mut_for_test().buffers[1].as_mut().unwrap(), RED);
|
|
open(&mut s, 2, b"Snake");
|
|
assert!(s.host().buffers[1].as_ref().unwrap().iter().all(|b| *b == 0));
|
|
}
|
|
|
|
#[test]
|
|
fn a_launcher_gone_takes_its_windows_with_it() {
|
|
let mut s = server();
|
|
open(&mut s, 1, b"Terminal");
|
|
open(&mut s, 2, b"Snake");
|
|
open(&mut s, 2, b"Counter");
|
|
assert_eq!(s.handle_call(3, [w::FORGET, 2, 0, 0], None).0[0], MALFORMED, "only init says so");
|
|
assert_eq!(s.handle_call(0, [w::FORGET, 2, 0, 0], None).0[0], w::OK);
|
|
assert_eq!(s.host().reclaimed, [1, 2]);
|
|
}
|
|
|
|
#[test]
|
|
fn a_present_outside_the_window_is_clipped() {
|
|
let mut s = server();
|
|
let t = open(&mut s, 1, b"Terminal");
|
|
fill(s.host_mut_for_test().buffers[0].as_mut().unwrap(), RED);
|
|
let rect = w::pack_rect(
|
|
types::display::ScreenCoord::new(30),
|
|
types::display::ScreenCoord::new(5),
|
|
types::display::Dimension::new(60000),
|
|
types::display::Dimension::new(60000),
|
|
);
|
|
assert_eq!(s.handle_call(t, [w::PRESENT, rect[0], rect[1], 0], None).0[0], w::OK);
|
|
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);
|
|
}
|
|
|
|
#[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);
|
|
}
|
|
|
|
#[test]
|
|
fn window_keys_raise_home_and_close_only_apps() {
|
|
let key = |s: &mut Srv, k: usize| {
|
|
s.host_mut_for_test().events = vec![[1, 10, k, 0], [2, 10, k, 0]];
|
|
s.signal(0);
|
|
};
|
|
let mut s = server();
|
|
let terminal = open(&mut s, 1, b"Terminal");
|
|
let home = open(&mut s, 2, b"Home");
|
|
let app = open(&mut s, 2, b"Tally");
|
|
assert_eq!(s.handle_call(home, [w::ROLE, w::role::HOME, 0, 0], None).0[0], w::OK);
|
|
assert_eq!(s.handle_call(app, [w::ROLE, w::role::APP, 0, 0], None).0[0], w::OK);
|
|
assert_eq!(s.handle_call(app, [w::ROLE, w::role::HOME, 0, 0], None).0[0], MALFORMED, "once");
|
|
s.handle_call(terminal, [w::BUFFER, 0, 0, 0], None);
|
|
assert_eq!(s.handle_call(terminal, [w::ROLE, w::role::APP, 0, 0], None).0[0], MALFORMED, "not once it runs");
|
|
let notify = CapSlot::new(9).unwrap();
|
|
s.handle_call(app, [w::SUBSCRIBE, 0, 0, 0], Some(notify));
|
|
|
|
key(&mut s, w::HOME_KEY);
|
|
assert_eq!(s.windows_front_for_test(), s.slot_for_test(home));
|
|
key(&mut s, w::CLOSE_KEY);
|
|
assert!(s.slot_for_test(home).is_some(), "Home is not closed");
|
|
// The app in front: closed, and told.
|
|
s.handle_call(app, [w::POLL, 0, 0, 0], None);
|
|
key(&mut s, w::SWITCH_KEY);
|
|
assert_eq!(s.windows_front_for_test(), s.slot_for_test(app));
|
|
key(&mut s, w::CLOSE_KEY);
|
|
assert_eq!(s.slot_for_test(app), None);
|
|
assert!(s.host().signalled.contains(¬ify));
|
|
assert_eq!(s.handle_call(app, [w::POLL, 0, 0, 0], None).0[0], MALFORMED, "its window is gone");
|
|
// The terminal in front: kept.
|
|
key(&mut s, w::SWITCH_KEY);
|
|
while s.windows_front_for_test() != s.slot_for_test(terminal) {
|
|
key(&mut s, w::SWITCH_KEY);
|
|
}
|
|
key(&mut s, w::CLOSE_KEY);
|
|
assert!(s.slot_for_test(terminal).is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn the_pointer_switches_on_the_bar_and_presses_go_to_the_front_window_in_its_pixels() {
|
|
use types::input::event::{POINTER_DOWN, POINTER_MOVE, POINTER_UP, pack_point, unpack_point};
|
|
let mut s = server();
|
|
let t = open(&mut s, 1, b"T");
|
|
let app = open(&mut s, 2, b"A");
|
|
let notify = CapSlot::new(9).unwrap();
|
|
s.handle_call(app, [w::SUBSCRIBE, 0, 0, 0], Some(notify));
|
|
s.handle_call(t, [w::SUBSCRIBE, 0, 0, 0], Some(CapSlot::new(10).unwrap()));
|
|
// The middle of pixel (x, y), as the input server words a position.
|
|
let at = |x: usize, y: usize| pack_point(((2 * x + 1) * 32768 / W) as u16, ((2 * y + 1) * 32768 / H) as u16);
|
|
// Moves are the display's: nothing reaches the window.
|
|
s.host_mut_for_test().events = vec![[POINTER_MOVE, 0, at(12, 20), 0]];
|
|
s.signal(0);
|
|
assert_eq!(s.handle_call(app, [w::POLL, 0, 0, 0], None).0[0], 0);
|
|
assert_ne!(shown_at(&s, 12, 20), Some(Color::from_raw(0)), "the cursor is drawn where the pointer is");
|
|
// A press and release below the bar: the window in front's, in its pixels.
|
|
s.host_mut_for_test().events = vec![[POINTER_DOWN, 0, at(12, 20), 0], [POINTER_UP, 0, at(12, 20), 0]];
|
|
s.signal(0);
|
|
let down = s.handle_call(app, [w::POLL, 0, 0, 0], None).0;
|
|
assert_eq!(down[0], POINTER_DOWN);
|
|
assert_eq!(unpack_point(down[2]), (12, 20 - BAR_HEIGHT));
|
|
assert_eq!(s.handle_call(app, [w::POLL, 0, 0, 0], None).0[0], POINTER_UP);
|
|
// A press on the bar, on the first name: that window comes forward.
|
|
s.host_mut_for_test().events = vec![[POINTER_DOWN, 0, at(2, 2), 0]];
|
|
s.signal(0);
|
|
assert_eq!(s.windows_front_for_test(), s.slot_for_test(t));
|
|
}
|
|
|
|
fn shown_at(s: &Srv, x: usize, y: usize) -> Option<Color> {
|
|
s.framebuffer().pixel(x, y)
|
|
}
|