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>>, minted: Vec, revoked: Vec, reclaimed: Vec, signalled: Vec, events: Vec<[usize; 4]>, } impl Host for FakeHost { fn buffer(&mut self, index: usize, pixels: usize) -> Option { 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 { 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, 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 { 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()); }