feat(display): windows: full-screen, F1 cycles, OS bar (names, front, user); canvas-only (display-owned buffers, 4 max, reused); launchers OPEN (pm per app, terminal own), window caps minted by display; FORGET on launcher death via init courier; raster crate (font, chunked text); F-keys in input; terminal+snake on canvas; REDRAW gone
This commit is contained in:
@@ -1,184 +1,189 @@
|
||||
use crate::protocol::{DisplayRequest, DisplayResponse};
|
||||
use crate::server::DisplayServer;
|
||||
use driver_framebuffer_mock::MockFramebuffer;
|
||||
use server_framework::Server;
|
||||
use types::display::{Color, Dimension, ScreenCoord};
|
||||
extern crate std;
|
||||
|
||||
fn test_server() -> DisplayServer<MockFramebuffer> {
|
||||
DisplayServer::new(MockFramebuffer::new(
|
||||
Dimension::new(320),
|
||||
Dimension::new(240),
|
||||
))
|
||||
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 {
|
||||
buffers: Vec<Option<Vec<Color>>>,
|
||||
minted: Vec<usize>,
|
||||
revoked: Vec<CapSlot>,
|
||||
reclaimed: Vec<usize>,
|
||||
signalled: Vec<CapSlot>,
|
||||
events: Vec<[usize; 4]>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draw_rect_writes_pixels() {
|
||||
let mut server = test_server();
|
||||
let resp = server.handle(DisplayRequest::DrawRect {
|
||||
x: ScreenCoord::new(0),
|
||||
y: ScreenCoord::new(0),
|
||||
w: Dimension::new(4),
|
||||
h: Dimension::new(3),
|
||||
color: Color::from_raw(0xFF),
|
||||
});
|
||||
assert_eq!(resp, DisplayResponse::Ok);
|
||||
assert_eq!(server.framebuffer().pixel_count(), 12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_resets_pixels() {
|
||||
let mut server = test_server();
|
||||
server.handle(DisplayRequest::DrawRect {
|
||||
x: ScreenCoord::new(0),
|
||||
y: ScreenCoord::new(0),
|
||||
w: Dimension::new(10),
|
||||
h: Dimension::new(10),
|
||||
color: Color::from_raw(0xFF),
|
||||
});
|
||||
server.handle(DisplayRequest::Clear {
|
||||
color: Color::from_raw(0),
|
||||
});
|
||||
assert_eq!(server.framebuffer().pixel_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flush_sets_flag() {
|
||||
let mut server = test_server();
|
||||
assert!(!server.framebuffer().was_flushed());
|
||||
server.handle(DisplayRequest::Flush);
|
||||
assert!(server.framebuffer().was_flushed());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_dimensions_returns_size() {
|
||||
let mut server = test_server();
|
||||
let resp = server.handle(DisplayRequest::GetDimensions);
|
||||
assert_eq!(
|
||||
resp,
|
||||
DisplayResponse::Dimensions {
|
||||
width: Dimension::new(320),
|
||||
height: Dimension::new(240),
|
||||
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);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draw_rect_correct_pixel_positions() {
|
||||
let mut server = test_server();
|
||||
server.handle(DisplayRequest::DrawRect {
|
||||
x: ScreenCoord::new(5),
|
||||
y: ScreenCoord::new(10),
|
||||
w: Dimension::new(2),
|
||||
h: Dimension::new(2),
|
||||
color: Color::from_raw(0xAB),
|
||||
});
|
||||
let fb = server.framebuffer();
|
||||
assert_eq!(fb.pixel_count(), 4);
|
||||
assert_eq!(
|
||||
fb.pixel_at(0),
|
||||
(
|
||||
ScreenCoord::new(5),
|
||||
ScreenCoord::new(10),
|
||||
Color::from_raw(0xAB)
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
fb.pixel_at(1),
|
||||
(
|
||||
ScreenCoord::new(6),
|
||||
ScreenCoord::new(10),
|
||||
Color::from_raw(0xAB)
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
fb.pixel_at(2),
|
||||
(
|
||||
ScreenCoord::new(5),
|
||||
ScreenCoord::new(11),
|
||||
Color::from_raw(0xAB)
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
fb.pixel_at(3),
|
||||
(
|
||||
ScreenCoord::new(6),
|
||||
ScreenCoord::new(11),
|
||||
Color::from_raw(0xAB)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_huge_rect_is_clipped_to_the_screen() {
|
||||
let mut server = test_server();
|
||||
let resp = server.handle(DisplayRequest::DrawRect {
|
||||
x: ScreenCoord::new(310),
|
||||
y: ScreenCoord::new(230),
|
||||
w: Dimension::new(u16::MAX),
|
||||
h: Dimension::new(u16::MAX),
|
||||
color: Color::from_raw(0xFF),
|
||||
});
|
||||
assert_eq!(resp, DisplayResponse::Ok);
|
||||
assert_eq!(server.framebuffer().pixel_count(), 10 * 10);
|
||||
let (x, y, _) = server.framebuffer().pixel_at(99);
|
||||
assert_eq!((x.raw(), y.raw()), (319, 239));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_off_screen_rect_draws_nothing() {
|
||||
let mut server = test_server();
|
||||
for (x, y) in [(320, 0), (0, 240), (u16::MAX, u16::MAX)] {
|
||||
server.handle(DisplayRequest::DrawRect {
|
||||
x: ScreenCoord::new(x),
|
||||
y: ScreenCoord::new(y),
|
||||
w: Dimension::new(u16::MAX),
|
||||
h: Dimension::new(u16::MAX),
|
||||
color: Color::from_raw(0xFF),
|
||||
});
|
||||
self.buffers[index].get_or_insert_with(|| vec![Color::from_raw(0); pixels]);
|
||||
CapSlot::new(40 + index as u8)
|
||||
}
|
||||
assert_eq!(server.framebuffer().pixel_count(), 0);
|
||||
fn blank(&mut self, index: usize) {
|
||||
if let Some(b) = self.buffers[index].as_mut() {
|
||||
b.fill(Color::from_raw(0));
|
||||
}
|
||||
}
|
||||
fn read(&self, index: usize, from: usize, out: &mut [Color]) {
|
||||
let b = self.buffers[index].as_ref().unwrap();
|
||||
out.copy_from_slice(&b[from..from + out.len()]);
|
||||
}
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
type Srv = DisplayServer<GridFramebuffer<W, H>, FakeHost>;
|
||||
|
||||
fn server() -> Srv {
|
||||
DisplayServer::new(GridFramebuffer::new(), FakeHost::default())
|
||||
}
|
||||
|
||||
/// 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.
|
||||
s.host_mut_for_test().buffers[slot].as_mut().unwrap().fill(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 text_draws_whole_glyph_cells() {
|
||||
use types::display::text::Text;
|
||||
let mut server = test_server();
|
||||
let (fg, bg) = (Color::rgb(255, 255, 255), Color::rgb(0, 0, 0));
|
||||
let t = Text::new(ScreenCoord::new(8), ScreenCoord::new(16), fg, bg, b"A");
|
||||
assert_eq!(server.handle(DisplayRequest::DrawText(t)), DisplayResponse::Ok);
|
||||
// One 8x16 cell, written row by row.
|
||||
assert_eq!(server.framebuffer().pixel_count(), 128);
|
||||
// 'A' row 2 is 0x7c: column 0 off, column 1 on.
|
||||
let at = |i: usize| server.framebuffer().pixel_at(i);
|
||||
assert_eq!(at(16), (ScreenCoord::new(8), ScreenCoord::new(18), bg));
|
||||
assert_eq!(at(17), (ScreenCoord::new(9), ScreenCoord::new(18), fg));
|
||||
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 text_past_the_edge_is_cut_not_wrapped() {
|
||||
use types::display::text::Text;
|
||||
let mut server = test_server();
|
||||
let c = Color::rgb(1, 1, 1);
|
||||
// 320 wide: from x=304 only two cells fit.
|
||||
let t = Text::new(ScreenCoord::new(304), ScreenCoord::new(0), c, c, b"abc");
|
||||
server.handle(DisplayRequest::DrawText(t));
|
||||
assert_eq!(server.framebuffer().pixel_count(), 256);
|
||||
let low = Text::new(ScreenCoord::new(0), ScreenCoord::new(230), c, c, b"a");
|
||||
server.handle(DisplayRequest::DrawText(low));
|
||||
assert_eq!(server.framebuffer().pixel_count(), 256);
|
||||
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 a_scroll_off_the_screen_moves_nothing() {
|
||||
let mut s = test_server();
|
||||
let scroll = |top| DisplayRequest::Scroll {
|
||||
top: ScreenCoord::new(top),
|
||||
height: Dimension::new(u16::MAX),
|
||||
by: Dimension::new(u16::MAX),
|
||||
fill: Color::from_raw(0),
|
||||
};
|
||||
s.handle(scroll(u16::MAX));
|
||||
assert_eq!(s.framebuffer().scrolls(), 0);
|
||||
s.handle(scroll(0));
|
||||
assert_eq!(s.framebuffer().scrolls(), 1, "clipped, then done");
|
||||
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.
|
||||
s.host_mut_for_test().buffers[1].as_mut().unwrap().fill(RED);
|
||||
open(&mut s, 2, b"Snake");
|
||||
assert!(s.host().buffers[1].as_ref().unwrap().iter().all(|c| *c == Color::from_raw(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");
|
||||
s.host_mut_for_test().buffers[0].as_mut().unwrap().fill(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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user