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:
2026-10-28 02:48:39 +01:00
parent d04dcfffa1
commit 864f039bc7
45 changed files with 1382 additions and 824 deletions

View File

@@ -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");
}