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,56 +0,0 @@
//! Who gets input: the display server is the input server's only client
//! and passes events on to the app in focus, the one that subscribed last.
//! Apps out of focus get nothing.
/// Events received and not yet polled by the app in focus.
const QUEUED: usize = 32;
pub struct Focus<N: Copy> {
/// The app in focus: its badge and the notification it gave to be told.
holder: Option<(usize, N)>,
events: [[usize; 4]; QUEUED],
head: usize,
len: usize,
}
impl<N: Copy> Focus<N> {
pub const fn new() -> Self {
Self { holder: None, events: [[0; 4]; QUEUED], head: 0, len: 0 }
}
/// `badge` takes focus; what the last holder had not read is dropped.
pub fn subscribe(&mut self, badge: usize, notify: N) {
self.holder = Some((badge, notify));
self.len = 0;
}
/// Queues an event (as the input server sends it) for the holder;
/// returns whom to tell. With no holder the event goes nowhere.
pub fn push(&mut self, event: [usize; 4]) -> Option<N> {
let (_, notify) = self.holder?;
if self.len == QUEUED {
self.head = (self.head + 1) % QUEUED;
self.len -= 1;
}
self.events[(self.head + self.len) % QUEUED] = event;
self.len += 1;
Some(notify)
}
/// The next event for `badge`, if it is in focus and one is queued.
pub fn poll(&mut self, badge: usize) -> Option<[usize; 4]> {
if self.holder.is_none_or(|(b, _)| b != badge) || self.len == 0 {
return None;
}
let event = self.events[self.head];
self.head = (self.head + 1) % QUEUED;
self.len -= 1;
Some(event)
}
}
impl<N: Copy> Default for Focus<N> {
fn default() -> Self {
Self::new()
}
}

View File

@@ -1,24 +0,0 @@
Copyright (c) 2018-2024, Frederic Cambus
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

View File

@@ -1,114 +0,0 @@
//! The display server's one font: Spleen 8x16 (BSD-2-Clause, Frederic
//! Cambus; see LICENSE-spleen), printable ASCII only. Generated from
//! spleen-8x16.bdf; each glyph is 16 rows, bit 7 the leftmost pixel.
pub const WIDTH: u16 = 8;
pub const HEIGHT: u16 = 16;
const FIRST: u8 = b' ';
const LAST: u8 = b'~';
/// The glyph for `c`; anything unprintable shows as `?`.
pub fn glyph(c: u8) -> &'static [u8; 16] {
let c = if (FIRST..=LAST).contains(&c) { c } else { b'?' };
&GLYPHS[usize::from(c - FIRST)]
}
#[rustfmt::skip]
static GLYPHS: [[u8; 16]; 95] = [
[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // ' '
[0x00, 0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00], // '!'
[0x00, 0x66, 0x66, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // '"'
[0x00, 0x00, 0x6c, 0x6c, 0xfe, 0x6c, 0x6c, 0x6c, 0x6c, 0xfe, 0x6c, 0x6c, 0x00, 0x00, 0x00, 0x00], // '#'
[0x00, 0x10, 0x7e, 0xd0, 0xd0, 0xd0, 0x7c, 0x16, 0x16, 0x16, 0x16, 0xfc, 0x10, 0x00, 0x00, 0x00], // '$'
[0x00, 0x00, 0x06, 0x66, 0x6c, 0x0c, 0x18, 0x18, 0x30, 0x36, 0x66, 0x60, 0x00, 0x00, 0x00, 0x00], // '%'
[0x00, 0x00, 0x38, 0x6c, 0x6c, 0x6c, 0x38, 0x70, 0xda, 0xcc, 0xcc, 0x7a, 0x00, 0x00, 0x00, 0x00], // '&'
[0x00, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // "'"
[0x00, 0x0e, 0x18, 0x30, 0x30, 0x60, 0x60, 0x60, 0x60, 0x30, 0x30, 0x18, 0x0e, 0x00, 0x00, 0x00], // '('
[0x00, 0x70, 0x18, 0x0c, 0x0c, 0x06, 0x06, 0x06, 0x06, 0x0c, 0x0c, 0x18, 0x70, 0x00, 0x00, 0x00], // ')'
[0x00, 0x00, 0x00, 0x00, 0x66, 0x3c, 0x18, 0xff, 0x18, 0x3c, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00], // '*'
[0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x7e, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // '+'
[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x30, 0x00, 0x00, 0x00], // ','
[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // '-'
[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00], // '.'
[0x00, 0x06, 0x06, 0x0c, 0x0c, 0x18, 0x18, 0x30, 0x30, 0x60, 0x60, 0xc0, 0xc0, 0x00, 0x00, 0x00], // '/'
[0x00, 0x00, 0x7c, 0xc6, 0xc6, 0xce, 0xde, 0xf6, 0xe6, 0xc6, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00], // '0'
[0x00, 0x00, 0x18, 0x38, 0x78, 0x58, 0x18, 0x18, 0x18, 0x18, 0x18, 0x7e, 0x00, 0x00, 0x00, 0x00], // '1'
[0x00, 0x00, 0x7c, 0xc6, 0x06, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xc6, 0xfe, 0x00, 0x00, 0x00, 0x00], // '2'
[0x00, 0x00, 0x7c, 0xc6, 0x06, 0x06, 0x3c, 0x06, 0x06, 0x06, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00], // '3'
[0x00, 0x00, 0xc0, 0xc0, 0xcc, 0xcc, 0xcc, 0xcc, 0xfe, 0x0c, 0x0c, 0x0c, 0x00, 0x00, 0x00, 0x00], // '4'
[0x00, 0x00, 0xfe, 0xc6, 0xc0, 0xc0, 0xfc, 0x06, 0x06, 0x06, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00], // '5'
[0x00, 0x00, 0x7c, 0xc6, 0xc0, 0xc0, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00], // '6'
[0x00, 0x00, 0xfe, 0xc6, 0x06, 0x06, 0x0c, 0x18, 0x30, 0x30, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00], // '7'
[0x00, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00], // '8'
[0x00, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x06, 0x06, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00], // '9'
[0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00], // ':'
[0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x18, 0x18, 0x30, 0x00, 0x00, 0x00], // ';'
[0x00, 0x00, 0x06, 0x0c, 0x18, 0x30, 0x60, 0x60, 0x30, 0x18, 0x0c, 0x06, 0x00, 0x00, 0x00, 0x00], // '<'
[0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // '='
[0x00, 0x00, 0x60, 0x30, 0x18, 0x0c, 0x06, 0x06, 0x0c, 0x18, 0x30, 0x60, 0x00, 0x00, 0x00, 0x00], // '>'
[0x00, 0x00, 0x7c, 0xc6, 0x06, 0x0c, 0x18, 0x30, 0x30, 0x00, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00], // '?'
[0x00, 0x00, 0x00, 0x7c, 0xc2, 0xda, 0xda, 0xda, 0xda, 0xde, 0xc0, 0x7c, 0x00, 0x00, 0x00, 0x00], // '@'
[0x00, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xfe, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // 'A'
[0x00, 0x00, 0xfc, 0xc6, 0xc6, 0xc6, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0xfc, 0x00, 0x00, 0x00, 0x00], // 'B'
[0x00, 0x00, 0x7e, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0x7e, 0x00, 0x00, 0x00, 0x00], // 'C'
[0x00, 0x00, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xfc, 0x00, 0x00, 0x00, 0x00], // 'D'
[0x00, 0x00, 0x7e, 0xc0, 0xc0, 0xc0, 0xf8, 0xc0, 0xc0, 0xc0, 0xc0, 0x7e, 0x00, 0x00, 0x00, 0x00], // 'E'
[0x00, 0x00, 0x7e, 0xc0, 0xc0, 0xc0, 0xf8, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0x00, 0x00, 0x00, 0x00], // 'F'
[0x00, 0x00, 0x7e, 0xc0, 0xc0, 0xc0, 0xde, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x00, 0x00, 0x00, 0x00], // 'G'
[0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xfe, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // 'H'
[0x00, 0x00, 0x7e, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x7e, 0x00, 0x00, 0x00, 0x00], // 'I'
[0x00, 0x00, 0x7e, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xf0, 0x00, 0x00, 0x00, 0x00], // 'J'
[0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xcc, 0xf8, 0xcc, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // 'K'
[0x00, 0x00, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0x7e, 0x00, 0x00, 0x00, 0x00], // 'L'
[0x00, 0x00, 0xc6, 0xee, 0xfe, 0xd6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // 'M'
[0x00, 0x00, 0xc6, 0xc6, 0xe6, 0xe6, 0xd6, 0xd6, 0xce, 0xce, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // 'N'
[0x00, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00], // 'O'
[0x00, 0x00, 0xfc, 0xc6, 0xc6, 0xc6, 0xfc, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0x00, 0x00, 0x00, 0x00], // 'P'
[0x00, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xd6, 0xd6, 0x7c, 0x18, 0x0c, 0x00, 0x00], // 'Q'
[0x00, 0x00, 0xfc, 0xc6, 0xc6, 0xc6, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // 'R'
[0x00, 0x00, 0x7e, 0xc0, 0xc0, 0xc0, 0x7c, 0x06, 0x06, 0x06, 0x06, 0xfc, 0x00, 0x00, 0x00, 0x00], // 'S'
[0x00, 0x00, 0xff, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00], // 'T'
[0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x00, 0x00, 0x00, 0x00], // 'U'
[0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x6c, 0x38, 0x10, 0x00, 0x00, 0x00, 0x00], // 'V'
[0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xd6, 0xfe, 0xee, 0xc6, 0x00, 0x00, 0x00, 0x00], // 'W'
[0x00, 0x00, 0xc6, 0xc6, 0xc6, 0x6c, 0x38, 0x6c, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // 'X'
[0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x06, 0x06, 0x06, 0x06, 0xfc, 0x00, 0x00, 0x00, 0x00], // 'Y'
[0x00, 0x00, 0xfe, 0x06, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xc0, 0xc0, 0xfe, 0x00, 0x00, 0x00, 0x00], // 'Z'
[0x00, 0x3e, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x3e, 0x00, 0x00, 0x00], // '['
[0x00, 0xc0, 0xc0, 0x60, 0x60, 0x30, 0x30, 0x18, 0x18, 0x0c, 0x0c, 0x06, 0x06, 0x00, 0x00, 0x00], // '\\'
[0x00, 0x7c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x7c, 0x00, 0x00, 0x00], // ']'
[0x00, 0x10, 0x38, 0x6c, 0xc6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // '^'
[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0x00], // '_'
[0x00, 0x30, 0x18, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // '`'
[0x00, 0x00, 0x00, 0x00, 0x00, 0x7c, 0x06, 0x7e, 0xc6, 0xc6, 0xc6, 0x7e, 0x00, 0x00, 0x00, 0x00], // 'a'
[0x00, 0x00, 0xc0, 0xc0, 0xc0, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xfc, 0x00, 0x00, 0x00, 0x00], // 'b'
[0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0x7e, 0x00, 0x00, 0x00, 0x00], // 'c'
[0x00, 0x00, 0x06, 0x06, 0x06, 0x7e, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x00, 0x00, 0x00, 0x00], // 'd'
[0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0xc6, 0xc6, 0xfe, 0xc0, 0xc0, 0x7e, 0x00, 0x00, 0x00, 0x00], // 'e'
[0x00, 0x00, 0x1e, 0x30, 0x30, 0x30, 0x7c, 0x30, 0x30, 0x30, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00], // 'f'
[0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x06, 0x06, 0xfc, 0x00], // 'g'
[0x00, 0x00, 0xc0, 0xc0, 0xc0, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // 'h'
[0x00, 0x00, 0x18, 0x18, 0x00, 0x38, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1c, 0x00, 0x00, 0x00, 0x00], // 'i'
[0x00, 0x00, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x70, 0x00], // 'j'
[0x00, 0x00, 0xc0, 0xc0, 0xc0, 0xcc, 0xd8, 0xf0, 0xf0, 0xd8, 0xcc, 0xc6, 0x00, 0x00, 0x00, 0x00], // 'k'
[0x00, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x1c, 0x00, 0x00, 0x00, 0x00], // 'l'
[0x00, 0x00, 0x00, 0x00, 0x00, 0xec, 0xd6, 0xd6, 0xd6, 0xd6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // 'm'
[0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // 'n'
[0x00, 0x00, 0x00, 0x00, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00], // 'o'
[0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xfc, 0xc0, 0xc0, 0xc0, 0x00], // 'p'
[0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x06, 0x06, 0x06, 0x00], // 'q'
[0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0xc6, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0x00, 0x00, 0x00, 0x00], // 'r'
[0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0xc0, 0xc0, 0x7c, 0x06, 0x06, 0xfc, 0x00, 0x00, 0x00, 0x00], // 's'
[0x00, 0x00, 0x30, 0x30, 0x30, 0x7c, 0x30, 0x30, 0x30, 0x30, 0x30, 0x1e, 0x00, 0x00, 0x00, 0x00], // 't'
[0x00, 0x00, 0x00, 0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x00, 0x00, 0x00, 0x00], // 'u'
[0x00, 0x00, 0x00, 0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0x6c, 0x38, 0x10, 0x00, 0x00, 0x00, 0x00], // 'v'
[0x00, 0x00, 0x00, 0x00, 0x00, 0xc6, 0xc6, 0xd6, 0xd6, 0xd6, 0xd6, 0x6e, 0x00, 0x00, 0x00, 0x00], // 'w'
[0x00, 0x00, 0x00, 0x00, 0x00, 0xc6, 0x6c, 0x38, 0x38, 0x6c, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // 'x'
[0x00, 0x00, 0x00, 0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x06, 0x06, 0xfc, 0x00], // 'y'
[0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xfe, 0x00, 0x00, 0x00, 0x00], // 'z'
[0x00, 0x0e, 0x18, 0x18, 0x18, 0x18, 0x70, 0x70, 0x18, 0x18, 0x18, 0x18, 0x0e, 0x00, 0x00, 0x00], // '{'
[0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00], // '|'
[0x00, 0x70, 0x18, 0x18, 0x18, 0x18, 0x0e, 0x0e, 0x18, 0x18, 0x18, 0x18, 0x70, 0x00, 0x00, 0x00], // '}'
[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0x7e, 0x4c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // '~'
];

View File

@@ -0,0 +1,99 @@
//! `Host` on the kernel: window buffers are regions of the display's own,
//! made once per slot and reused (a region lives as long as its owner), so
//! the display's memory has a fixed ceiling whatever apps do.
use libkos::syscall;
use libkos::window::Window;
use types::address::{ByteOffset, VirtualAddress};
use types::badge::Badge;
use types::bounded::{CapSlot, PageCount};
use types::display::Color;
use types::display::window::PIXEL_BYTES;
use types::mapping::PagePerms;
use types::permission::{Permission, PermissionSet};
use crate::server::Host;
use crate::windows::MAX_WINDOWS;
/// Where slot `i`'s buffer is mapped in the display: 4 MiB apart, room for
/// a 1280x800 window.
const BUFFERS_VA: usize = 0x0080_0000;
const BUFFER_SPAN: usize = 0x0040_0000;
const PAGE: usize = 4096;
pub struct KernelHost {
/// The display's own endpoint, which window capabilities are minted
/// from.
ep: CapSlot,
input: Option<CapSlot>,
buffers: [Option<(CapSlot, Window)>; MAX_WINDOWS],
}
impl KernelHost {
pub fn new(ep: CapSlot, input: Option<CapSlot>) -> Self {
Self { ep, input, buffers: [None; MAX_WINDOWS] }
}
}
impl Host for KernelHost {
fn buffer(&mut self, index: usize, pixels: usize) -> Option<CapSlot> {
let slot = self.buffers.get_mut(index)?;
if slot.is_none() {
let bytes = (pixels * PIXEL_BYTES).min(BUFFER_SPAN);
let pages = PageCount::new(u16::try_from(bytes.div_ceil(PAGE)).ok()?)?;
let region = syscall::create_region(pages).ok()?;
let va = VirtualAddress::from_raw(BUFFERS_VA + index * BUFFER_SPAN);
match libkos::window::map(region, va, PagePerms::ReadWrite) {
Ok(window) => *slot = Some((region, window)),
Err(_) => {
syscall::drop_cap(region);
return None;
}
}
}
slot.as_ref().map(|(region, _)| *region)
}
fn blank(&mut self, index: usize) {
if let Some((_, window)) = self.buffers.get(index).and_then(Option::as_ref) {
window.fill_u32(ByteOffset::ZERO, window.len() / PIXEL_BYTES, 0);
}
}
fn read(&self, index: usize, from: usize, out: &mut [Color]) {
let Some((_, window)) = self.buffers.get(index).and_then(Option::as_ref) else { return };
for (i, px) in out.iter_mut().enumerate() {
let at = (from + i) * PIXEL_BYTES;
if at + PIXEL_BYTES > window.len() {
return;
}
*px = Color::from_raw(window.read::<u32>(ByteOffset::new(at)));
}
}
fn reclaim(&mut self, index: usize) {
if let Some((region, _)) = self.buffers.get(index).and_then(Option::as_ref) {
// Every copy handed out goes, and with it every mapping of one.
let _ = syscall::revoke(*region);
}
}
fn mint(&mut self, badge: usize) -> Option<CapSlot> {
let perms = PermissionSet::empty().with(Permission::Execute).with(Permission::Delegate);
syscall::mint(self.ep, Badge::new(badge), perms).ok()
}
fn revoke(&mut self, cap: CapSlot) {
let _ = syscall::revoke(cap);
syscall::drop_cap(cap);
}
fn signal(&mut self, notify: CapSlot) {
let _ = syscall::signal(notify);
}
fn input(&mut self) -> Option<[usize; 4]> {
let event = syscall::request(self.input?, [types::input::POLL_EVENT, 0, 0, 0]).ok()?;
(event[0] != 0).then_some(event)
}
}

View File

@@ -1,10 +1,9 @@
#![no_std]
#![forbid(unsafe_code)]
pub mod focus;
pub mod font;
pub mod protocol;
pub mod host;
pub mod server;
pub mod windows;
#[cfg(test)]
#[path = "tests/mod.rs"]

View File

@@ -1,93 +0,0 @@
use server_framework::{Decode, Encode};
use types::display::text::Text;
use types::display::{Color, Dimension, ScreenCoord};
#[derive(Debug, PartialEq)]
pub enum DisplayRequest {
DrawRect { x: ScreenCoord, y: ScreenCoord, w: Dimension, h: Dimension, color: Color },
Clear { color: Color },
Flush,
GetDimensions,
DrawText(Text),
/// Moves the rows from `top`, `height` tall, up by `by`; the rows left
/// at the bottom are filled with `fill`.
Scroll { top: ScreenCoord, height: Dimension, by: Dimension, fill: Color },
}
#[derive(Debug, PartialEq)]
pub enum DisplayResponse {
Ok,
Dimensions { width: Dimension, height: Dimension },
Error,
}
impl Encode for DisplayRequest {
fn encode(&self) -> [usize; 4] {
match self {
DisplayRequest::DrawRect { x, y, w, h, color } => {
let xy = (x.raw() as usize) | ((y.raw() as usize) << 16);
let wh = (w.raw() as usize) | ((h.raw() as usize) << 16);
[0, xy, wh, color.raw() as usize]
}
DisplayRequest::Clear { color } => [1, color.raw() as usize, 0, 0],
DisplayRequest::Flush => [2, 0, 0, 0],
DisplayRequest::GetDimensions => [3, 0, 0, 0],
DisplayRequest::DrawText(t) => t.words(),
DisplayRequest::Scroll { top, height, by, fill } => {
let band = (top.raw() as usize) | ((height.raw() as usize) << 16);
[types::display::SCROLL, band, by.raw() as usize, fill.raw() as usize]
}
}
}
}
impl Decode for DisplayRequest {
fn decode(words: [usize; 4]) -> Option<Self> {
match words[0] {
0 => {
let x = ScreenCoord::new((words[1] & 0xFFFF) as u16);
let y = ScreenCoord::new(((words[1] >> 16) & 0xFFFF) as u16);
let w = Dimension::new((words[2] & 0xFFFF) as u16);
let h = Dimension::new(((words[2] >> 16) & 0xFFFF) as u16);
let color = Color::from_raw(words[3] as u32);
Some(DisplayRequest::DrawRect { x, y, w, h, color })
}
1 => Some(DisplayRequest::Clear { color: Color::from_raw(words[1] as u32) }),
2 => Some(DisplayRequest::Flush),
3 => Some(DisplayRequest::GetDimensions),
types::display::SCROLL => Some(DisplayRequest::Scroll {
top: ScreenCoord::new((words[1] & 0xFFFF) as u16),
height: Dimension::new(((words[1] >> 16) & 0xFFFF) as u16),
by: Dimension::new((words[2] & 0xFFFF) as u16),
fill: Color::from_raw(words[3] as u32),
}),
_ => Text::from_words(words).map(DisplayRequest::DrawText),
}
}
}
impl Encode for DisplayResponse {
fn encode(&self) -> [usize; 4] {
match self {
DisplayResponse::Ok => [0, 0, 0, 0],
DisplayResponse::Dimensions { width, height } => {
[1, width.raw() as usize, height.raw() as usize, 0]
}
DisplayResponse::Error => [2, 0, 0, 0],
}
}
}
impl Decode for DisplayResponse {
fn decode(words: [usize; 4]) -> Option<Self> {
match words[0] {
0 => Some(DisplayResponse::Ok),
1 => Some(DisplayResponse::Dimensions {
width: Dimension::new(words[1] as u16),
height: Dimension::new(words[2] as u16),
}),
2 => Some(DisplayResponse::Error),
_ => None,
}
}
}

View File

@@ -1,138 +1,285 @@
//! The display server (see the vision's Display System note): it alone
//! holds the screen. It draws the OS bar along the top (the windows, which
//! one is in front, who is signed in) and shows the window in front below
//! it, copied from that window's pixel buffer as its app presents changes.
//! Apps draw only into their own buffer; the bar is the display's, so no
//! app can pose as another. The kernel side (buffers, capabilities, the
//! input server) is behind `Host`.
use libkos::syscall::Give;
use port_framebuffer::Framebuffer;
use raster::font;
use server_framework::{Decode, Encode, MALFORMED, Server};
use types::bounded::CapSlot;
use types::display::window::{self as w, NAME_BYTES};
use types::display::{Color, Dimension, ScreenCoord};
use crate::protocol::{DisplayRequest, DisplayResponse};
use port_framebuffer::Framebuffer;
use types::display::text::{TEXT_BYTES, Text};
use types::display::{Dimension, ScreenCoord};
use crate::windows::{MAX_WINDOWS, Windows};
use crate::focus::Focus;
use crate::font;
/// The OS bar: one row of text.
pub const BAR_HEIGHT: u16 = font::HEIGHT;
const BAR_BG: Color = Color::rgb(24, 24, 40);
const BAR_FG: Color = Color::rgb(170, 170, 190);
const FRONT_BG: Color = Color::rgb(90, 90, 150);
const FRONT_FG: Color = Color::rgb(255, 255, 255);
/// Pixels copied per row at a time.
const RUN: usize = 256;
pub struct DisplayServer<F: Framebuffer> {
fb: F,
/// The input server, which signals this server's bound notification.
input: Option<CapSlot>,
focus: Focus<CapSlot>,
/// What the display needs from the kernel.
pub trait Host {
/// Window slot `index`'s pixel buffer of `pixels` pixels, made the
/// first time; the capability its window's holder maps.
fn buffer(&mut self, index: usize, pixels: usize) -> Option<CapSlot>;
/// Blanks slot `index`'s buffer, so a new window shows nothing of the
/// last one's.
fn blank(&mut self, index: usize);
/// Pixels of slot `index`'s buffer from pixel `from` on, into `out`.
fn read(&self, index: usize, from: usize, out: &mut [Color]);
/// Takes back every copy of slot `index`'s buffer handed out.
fn reclaim(&mut self, index: usize);
/// A copy of the display's endpoint carrying `badge`.
fn mint(&mut self, badge: usize) -> Option<CapSlot>;
/// Deletes `cap` and every copy made from it.
fn revoke(&mut self, cap: CapSlot);
fn signal(&mut self, notify: CapSlot);
/// The input server's next event, if one is waiting.
fn input(&mut self) -> Option<[usize; 4]>;
}
impl<F: Framebuffer> DisplayServer<F> {
pub fn new(fb: F) -> Self {
Self { fb, input: None, focus: Focus::new() }
}
/// Nothing goes through `handle`: every request needs its badge.
pub struct Never;
/// A server that passes `input`'s events on to the app in focus.
pub fn with_input(fb: F, input: CapSlot) -> Self {
Self { input: Some(input), ..Self::new(fb) }
impl Decode for Never {
fn decode(_: [usize; 4]) -> Option<Self> {
None
}
}
impl Encode for Never {
fn encode(&self) -> [usize; 4] {
[MALFORMED, 0, 0, 0]
}
}
pub struct DisplayServer<F: Framebuffer, H: Host> {
fb: F,
host: H,
windows: Windows<CapSlot>,
/// Each open window's capability, as minted.
caps: [Option<CapSlot>; MAX_WINDOWS],
user: [u8; NAME_BYTES],
user_len: usize,
}
impl<F: Framebuffer, H: Host> DisplayServer<F, H> {
pub fn new(fb: F, host: H) -> Self {
let mut s = Self { fb, host, windows: Windows::new(), caps: [None; MAX_WINDOWS], user: [0; NAME_BYTES], user_len: 0 };
s.repaint();
s
}
pub fn framebuffer(&self) -> &F {
&self.fb
}
/// Draws each cell whole, background too, so text overwrites what was
/// there; cells past the screen edge are skipped.
fn draw_text(&mut self, t: &Text) {
let (fw, fh) = (self.fb.width().raw(), self.fb.height().raw());
let (x0, y0) = (t.x.raw(), t.y.raw());
if y0.saturating_add(font::HEIGHT) > fh {
pub fn host(&self) -> &H {
&self.host
}
#[cfg(test)]
pub fn host_mut_for_test(&mut self) -> &mut H {
&mut self.host
}
/// A window's size: the screen below the bar.
fn window_size(&self) -> (Dimension, Dimension) {
(self.fb.width(), Dimension::new(self.fb.height().raw().saturating_sub(BAR_HEIGHT)))
}
fn pixels(&self) -> usize {
let (width, height) = self.window_size();
width.raw() as usize * height.raw() as usize
}
fn open(&mut self, opener: usize, name: &[u8]) -> ([usize; 4], Option<Give>) {
let Some((index, badge)) = self.windows.open(opener, name) else {
return ([MALFORMED, 0, 0, 0], None);
};
let pixels = self.pixels();
let made = self.host.buffer(index, pixels).is_some();
let cap = made.then(|| self.host.mint(badge)).flatten();
let Some(cap) = cap else {
self.windows.close(index);
return ([MALFORMED, 0, 0, 0], None);
};
self.host.blank(index);
self.caps[index] = Some(cap);
self.repaint();
let (width, height) = self.window_size();
// The launcher passes it on to the app.
([w::OK, width.raw() as usize, height.raw() as usize, 0], Some(Give::keeping_delegate(cap)))
}
fn close(&mut self, index: usize) {
if let Some(cap) = self.caps.get_mut(index).and_then(Option::take) {
self.host.revoke(cap);
}
self.host.reclaim(index);
self.windows.close(index);
self.repaint();
}
/// Copies a rectangle of window `index` to the screen, if it is in
/// front; clipped to the window.
fn present(&mut self, index: usize, x: ScreenCoord, y: ScreenCoord, width: Dimension, height: Dimension) {
if self.windows.front() != Some(index) {
return;
}
// Whole cells only.
let fit = (fw.saturating_sub(x0) / font::WIDTH) as usize;
let chars = &t.chars()[..t.chars().len().min(fit)];
// One run per pixel row across every cell, not a call per pixel.
let mut line = [t.bg; TEXT_BYTES * font::WIDTH as usize];
let line = &mut line[..chars.len() * font::WIDTH as usize];
for row in 0..font::HEIGHT {
for (cell, c) in line.chunks_mut(font::WIDTH as usize).zip(chars) {
let bits = font::glyph(*c)[row as usize];
for (col, px) in cell.iter_mut().enumerate() {
*px = if bits & (0x80 >> col) != 0 { t.fg } else { t.bg };
}
let (ww, wh) = (self.window_size().0.raw(), self.window_size().1.raw());
let (x0, y0) = (x.raw().min(ww), y.raw().min(wh));
let (x1, y1) = (x0.saturating_add(width.raw()).min(ww), y0.saturating_add(height.raw()).min(wh));
let mut run = [Color::from_raw(0); RUN];
for row in y0..y1 {
let mut col = x0;
while col < x1 {
let n = usize::from(x1 - col).min(RUN);
self.host.read(index, row as usize * ww as usize + col as usize, &mut run[..n]);
self.fb.write_row(ScreenCoord::new(col), ScreenCoord::new(row + BAR_HEIGHT), &run[..n]);
col += n as u16;
}
}
self.fb.flush();
}
/// The bar, and the window in front (or nothing) below it.
fn repaint(&mut self) {
self.draw_bar();
let (width, height) = self.window_size();
match self.windows.front() {
Some(index) => self.present(index, ScreenCoord::new(0), ScreenCoord::new(0), width, height),
None => {
self.fb.fill_rect(ScreenCoord::new(0), ScreenCoord::new(BAR_HEIGHT), width, height, Color::rgb(30, 30, 60));
self.fb.flush();
}
self.fb.write_row(t.x, ScreenCoord::new(y0 + row), line);
}
}
}
impl<F: Framebuffer> Server for DisplayServer<F> {
type Request = DisplayRequest;
type Response = DisplayResponse;
fn handle_call(&mut self, badge: usize, words: [usize; 4], cap: Option<CapSlot>) -> ([usize; 4], Option<libkos::syscall::Give>) {
use types::display::input::{POLL, SUBSCRIBE};
match (words[0], cap) {
(SUBSCRIBE, Some(notify)) => {
self.focus.subscribe(badge, notify);
(DisplayResponse::Ok.encode(), None)
/// Each window's name, the front one highlighted; who is signed in at
/// the right.
fn draw_bar(&mut self) {
let width = self.fb.width();
self.fb.fill_rect(ScreenCoord::new(0), ScreenCoord::new(0), width, Dimension::new(BAR_HEIGHT), BAR_BG);
let mut x = 0u16;
let mut label = [b' '; NAME_BYTES + 2];
let entries: [Option<([u8; NAME_BYTES], usize, bool)>; MAX_WINDOWS] = {
let mut out = [None; MAX_WINDOWS];
for (slot, (_, name, front)) in out.iter_mut().zip(self.windows.list()) {
let mut n = [0u8; NAME_BYTES];
n[..name.len()].copy_from_slice(name);
*slot = Some((n, name.len(), front));
}
(SUBSCRIBE, None) => (DisplayResponse::Error.encode(), None),
(POLL, _) => (self.focus.poll(badge).unwrap_or([0; 4]), None),
_ => match DisplayRequest::decode(words) {
Some(request) => (self.handle(request).encode(), None),
out
};
for (name, len, front) in entries.into_iter().flatten() {
label[1..=len].copy_from_slice(&name[..len]);
label[len + 1] = b' ';
let text = &label[..len + 2];
let (fg, bg) = if front { (FRONT_FG, FRONT_BG) } else { (BAR_FG, BAR_BG) };
raster::draw_text(&mut self.fb, ScreenCoord::new(x), ScreenCoord::new(0), fg, bg, text);
x = x.saturating_add(text.len() as u16 * font::WIDTH + font::WIDTH);
}
let user = &self.user[..self.user_len];
let at = width.raw().saturating_sub((user.len() as u16 + 1) * font::WIDTH);
raster::draw_text(&mut self.fb, ScreenCoord::new(at), ScreenCoord::new(0), BAR_FG, BAR_BG, user);
}
/// 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)))
}
None => ([MALFORMED, 0, 0, 0], None),
},
}
}
/// The input server has events: take them all, tell the app in focus.
fn signal(&mut self, _bits: usize) {
let Some(input) = self.input else {
return;
};
let mut told = None;
while let Ok(event) = libkos::syscall::request(input, [types::input::POLL_EVENT, 0, 0, 0]) {
if event[0] == 0 {
break;
(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)
}
told = self.focus.push(event).or(told);
}
if let Some(notify) = told {
let _ = libkos::syscall::signal(notify);
}
}
fn handle(&mut self, request: DisplayRequest) -> DisplayResponse {
match request {
DisplayRequest::DrawRect { x, y, w, h, color } => {
// Clients choose every coordinate; clip to the screen so a
// huge or off-screen rectangle costs nothing and coordinates
// never overflow in the driver.
let (fw, fh) = (self.fb.width().raw(), self.fb.height().raw());
if x.raw() < fw && y.raw() < fh {
let w = Dimension::new(w.raw().min(fw - x.raw()));
let h = Dimension::new(h.raw().min(fh - y.raw()));
self.fb.fill_rect(x, y, w, h, color);
}
DisplayResponse::Ok
(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);
([w::OK, 0, 0, 0], None)
}
DisplayRequest::Clear { color } => {
self.fb.clear(color);
DisplayResponse::Ok
}
DisplayRequest::Flush => {
self.fb.flush();
DisplayResponse::Ok
}
DisplayRequest::Scroll { top, height, by, fill } => {
// Clipped like a rectangle: only rows on the screen move.
let fh = self.fb.height().raw();
if top.raw() < fh {
let height = Dimension::new(height.raw().min(fh - top.raw()));
let by = Dimension::new(by.raw().min(height.raw()));
self.fb.scroll_up(top, height, by, fill);
}
DisplayResponse::Ok
}
DisplayRequest::DrawText(t) => {
self.draw_text(&t);
DisplayResponse::Ok
}
DisplayRequest::GetDimensions => DisplayResponse::Dimensions {
width: self.fb.width(),
height: self.fb.height(),
},
_ => ([MALFORMED, 0, 0, 0], None),
}
}
}
fn is_switch_key(event: [usize; 4]) -> bool {
use types::input::event::{FUNCTION_KEY, KEY_DOWN, KEY_UP};
matches!(event[0], KEY_DOWN | KEY_UP) && event[1] == FUNCTION_KEY && event[2] == w::SWITCH_KEY
}
impl<F: Framebuffer, H: Host> Server for DisplayServer<F, H> {
type Request = Never;
type Response = Never;
fn handle(&mut self, request: Never) -> Never {
request
}
fn handle_call(&mut self, badge: usize, words: [usize; 4], cap: Option<CapSlot>) -> ([usize; 4], Option<Give>) {
if badge & w::BADGE != 0 {
return match self.windows.slot(badge) {
Some(index) => self.window_call(index, badge, words, cap),
// Closed: its capability is on its way out.
None => ([MALFORMED, 0, 0, 0], None),
};
}
match words[0] {
w::OPEN => {
let (name, len) = w::unpack_name([words[1], words[2], words[3]]);
self.open(badge, &name[..len])
}
w::SET_USER if badge == 0 => {
let (name, len) = w::unpack_name([words[1], words[2], words[3]]);
(self.user, self.user_len) = (name, len);
self.repaint();
([w::OK, 0, 0, 0], None)
}
w::FORGET if badge == 0 => {
let mut gone = [None; MAX_WINDOWS];
for (slot, index) in gone.iter_mut().zip(self.windows.opened_by(words[1])) {
*slot = Some(index);
}
for index in gone.into_iter().flatten() {
self.close(index);
}
([w::OK, 0, 0, 0], None)
}
_ => ([MALFORMED, 0, 0, 0], None),
}
}
/// The input server has events: the switch key is the display's, the
/// rest go to the window in front.
fn signal(&mut self, _bits: usize) {
let mut told = None;
while let Some(event) = self.host.input() {
if is_switch_key(event) {
if event[0] == types::input::event::KEY_DOWN && self.windows.cycle().is_some() {
self.repaint();
}
continue;
}
told = self.windows.push(event).or(told);
}
if let Some(notify) = told {
self.host.signal(notify);
}
}
}

View File

@@ -1,36 +0,0 @@
use crate::focus::Focus;
const KEY: [usize; 4] = [1, 9, b'a' as usize, 0];
#[test]
fn only_the_app_in_focus_gets_events() {
let mut f: Focus<u8> = Focus::new();
assert_eq!(f.push(KEY), None, "nobody to give it to");
f.subscribe(1, 10);
assert_eq!(f.push(KEY), Some(10));
assert_eq!(f.poll(2), None);
assert_eq!(f.poll(1), Some(KEY));
assert_eq!(f.poll(1), None);
}
#[test]
fn a_new_subscriber_takes_focus_and_the_old_queue_is_dropped() {
let mut f: Focus<u8> = Focus::new();
f.subscribe(1, 10);
f.push(KEY);
f.subscribe(2, 20);
assert_eq!(f.poll(1), None);
assert_eq!(f.poll(2), None);
assert_eq!(f.push(KEY), Some(20));
assert_eq!(f.poll(2), Some(KEY));
}
#[test]
fn a_full_queue_drops_the_oldest() {
let mut f: Focus<u8> = Focus::new();
f.subscribe(1, 10);
for i in 0..40 {
f.push([1, 9, i, 0]);
}
assert_eq!(f.poll(1), Some([1, 9, 8, 0]));
}

View File

@@ -1,3 +1,2 @@
mod focus;
mod protocol;
mod server;
mod windows;

View File

@@ -1,66 +0,0 @@
use crate::protocol::{DisplayRequest, DisplayResponse};
use server_framework::{Decode, Encode};
use types::display::{Color, Dimension, ScreenCoord};
#[test]
fn draw_rect_roundtrip() {
let req = DisplayRequest::DrawRect {
x: ScreenCoord::new(10),
y: ScreenCoord::new(20),
w: Dimension::new(100),
h: Dimension::new(50),
color: Color::from_raw(0xFF0000),
};
let words = req.encode();
let decoded = DisplayRequest::decode(words).unwrap();
assert_eq!(decoded, req);
}
#[test]
fn clear_roundtrip() {
let req = DisplayRequest::Clear { color: Color::from_raw(0x00FF00) };
let words = req.encode();
let decoded = DisplayRequest::decode(words).unwrap();
assert_eq!(decoded, req);
}
#[test]
fn flush_roundtrip() {
let words = DisplayRequest::Flush.encode();
let decoded = DisplayRequest::decode(words).unwrap();
assert_eq!(decoded, DisplayRequest::Flush);
}
#[test]
fn get_dimensions_roundtrip() {
let words = DisplayRequest::GetDimensions.encode();
let decoded = DisplayRequest::decode(words).unwrap();
assert_eq!(decoded, DisplayRequest::GetDimensions);
}
#[test]
fn dimensions_response_roundtrip() {
let resp = DisplayResponse::Dimensions {
width: Dimension::new(320),
height: Dimension::new(240),
};
let words = resp.encode();
let decoded = DisplayResponse::decode(words).unwrap();
assert_eq!(decoded, resp);
}
#[test]
fn invalid_request_returns_none() {
assert!(DisplayRequest::decode([99, 0, 0, 0]).is_none());
}
#[test]
fn scroll_roundtrip() {
let req = DisplayRequest::Scroll {
top: ScreenCoord::new(0),
height: Dimension::new(800),
by: Dimension::new(16),
fill: Color::from_raw(0x101018),
};
assert_eq!(DisplayRequest::decode(req.encode()), Some(req));
}

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

View File

@@ -0,0 +1,69 @@
extern crate std;
use crate::windows::{MAX_WINDOWS, Windows};
use std::string::String;
use std::vec::Vec;
fn names(w: &Windows<u8>) -> Vec<(String, bool)> {
w.list().map(|(_, n, front)| (String::from_utf8_lossy(n).into_owned(), front)).collect()
}
#[test]
fn a_new_window_comes_to_the_front_and_the_key_cycles() {
let mut w = Windows::<u8>::new();
let (t, _) = w.open(1, b"Terminal").unwrap();
let (s, _) = w.open(2, b"Snake").unwrap();
assert_eq!(w.front(), Some(s));
assert_eq!(names(&w), [("Terminal".into(), false), ("Snake".into(), true)]);
assert_eq!(w.cycle(), Some(t));
assert_eq!(w.cycle(), Some(s));
}
#[test]
fn closing_the_front_window_shows_the_next_one() {
let mut w = Windows::<u8>::new();
let (t, _) = w.open(1, b"Terminal").unwrap();
let (s, _) = w.open(2, b"Snake").unwrap();
assert!(w.close(s), "the front changed");
assert_eq!(w.front(), Some(t));
assert!(w.close(t));
assert_eq!(w.front(), None);
assert_eq!(w.cycle(), None, "nothing to cycle to");
}
#[test]
fn only_the_front_window_gets_input_and_only_its_own() {
let mut w = Windows::<u8>::new();
let (_, tb) = w.open(1, b"Terminal").unwrap();
assert!(w.subscribe(tb, 7));
let (_, sb) = w.open(2, b"Snake").unwrap();
assert!(w.subscribe(sb, 9));
assert_eq!(w.push([1, 9, 0, 0]), Some(9), "snake is in front");
assert_eq!(w.poll(tb), None, "the terminal sees none of it");
assert_eq!(w.poll(sb), Some([1, 9, 0, 0]));
w.cycle();
assert_eq!(w.push([1, 4, 0, 0]), Some(7));
}
#[test]
fn badges_are_never_reused_and_room_runs_out() {
let mut w = Windows::<u8>::new();
let (i, first) = w.open(1, b"a").unwrap();
w.close(i);
let (_, second) = w.open(1, b"a").unwrap();
assert_ne!(first, second);
assert_eq!(w.slot(first), None, "a closed window's badge reaches nothing");
for _ in 1..MAX_WINDOWS {
assert!(w.open(1, b"x").is_some());
}
assert!(w.open(1, b"x").is_none());
}
#[test]
fn a_launchers_windows_are_found_by_its_badge() {
let mut w = Windows::<u8>::new();
let (a, _) = w.open(5, b"one").unwrap();
w.open(6, b"other").unwrap();
let (b, _) = w.open(5, b"two").unwrap();
assert_eq!(w.opened_by(5).collect::<Vec<_>>(), [a, b]);
}

View File

@@ -0,0 +1,156 @@
//! Which windows there are and which is in front (see `types::display::
//! window`). Every window fills the screen below the OS bar; one is in
//! front at a time, and only it is shown and gets input. A new window comes
//! to the front; a key cycles through them. Pure: the server keeps the
//! pixels.
use types::display::window::{BADGE, NAME_BYTES};
pub const MAX_WINDOWS: usize = 4;
/// Events received and not yet polled by the window in front.
const QUEUED: usize = 32;
struct Queue {
events: [[usize; 4]; QUEUED],
head: usize,
len: usize,
}
impl Queue {
const fn new() -> Self {
Self { events: [[0; 4]; QUEUED], head: 0, len: 0 }
}
/// The oldest is dropped when it is full.
fn push(&mut self, event: [usize; 4]) {
if self.len == QUEUED {
self.head = (self.head + 1) % QUEUED;
self.len -= 1;
}
self.events[(self.head + self.len) % QUEUED] = event;
self.len += 1;
}
fn pop(&mut self) -> Option<[usize; 4]> {
if self.len == 0 {
return None;
}
let event = self.events[self.head];
self.head = (self.head + 1) % QUEUED;
self.len -= 1;
Some(event)
}
}
struct Window<N> {
badge: usize,
/// The launcher's badge: its windows go when it does.
opener: usize,
name: [u8; NAME_BYTES],
name_len: usize,
notify: Option<N>,
queue: Queue,
}
pub struct Windows<N: Copy> {
slots: [Option<Window<N>>; MAX_WINDOWS],
front: Option<usize>,
/// Makes each window's badge new, so a closed window's never returns.
opened: usize,
}
impl<N: Copy> Windows<N> {
pub const fn new() -> Self {
Self { slots: [const { None }; MAX_WINDOWS], front: None, opened: 0 }
}
/// A window for `name`, opened through `opener`'s capability, in
/// front: its slot and its badge. `None` if there is no room.
pub fn open(&mut self, opener: usize, name: &[u8]) -> Option<(usize, usize)> {
let index = self.slots.iter().position(Option::is_none)?;
self.opened += 1;
let badge = BADGE | self.opened << 8 | index;
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.front = Some(index);
Some((index, badge))
}
/// The slot of the window behind `badge`, if it is open.
pub fn slot(&self, badge: usize) -> Option<usize> {
self.slots.iter().position(|w| w.as_ref().is_some_and(|w| w.badge == badge))
}
/// Closes the window in `index`; whether the front changed.
pub fn close(&mut self, index: usize) -> bool {
if self.slots.get_mut(index).and_then(Option::take).is_none() {
return false;
}
if self.front != Some(index) {
return false;
}
self.front = None;
self.front = self.after(index);
true
}
/// The slots of every window `opener` opened.
pub fn opened_by(&self, opener: usize) -> impl Iterator<Item = usize> + '_ {
self.slots.iter().enumerate().filter_map(move |(i, w)| w.as_ref().filter(|w| w.opener == opener).map(|_| i))
}
/// The next open window after `index`, round, if any.
fn after(&self, index: usize) -> Option<usize> {
(1..=MAX_WINDOWS).map(|step| (index + step) % MAX_WINDOWS).find(|i| self.slots[*i].is_some())
}
/// Brings the next window to the front; the new front if it changed.
pub fn cycle(&mut self) -> Option<usize> {
let next = self.after(self.front?)?;
(Some(next) != self.front).then(|| {
self.front = Some(next);
next
})
}
pub fn front(&self) -> Option<usize> {
self.front
}
/// 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 };
w.notify = Some(notify);
true
}
/// Queues an event for the window in front; whom to tell, if anyone.
pub fn push(&mut self, event: [usize; 4]) -> Option<N> {
let w = self.front.and_then(|i| self.slots[i].as_mut())?;
w.queue.push(event);
w.notify
}
/// The window `badge`'s next event.
pub fn poll(&mut self, badge: usize) -> Option<[usize; 4]> {
let i = self.slot(badge)?;
self.slots[i].as_mut()?.queue.pop()
}
/// Every open window, for the bar: its slot, its name, and whether it
/// is in front.
pub fn list(&self) -> impl Iterator<Item = (usize, &[u8], bool)> + '_ {
self.slots
.iter()
.enumerate()
.filter_map(move |(i, w)| w.as_ref().map(|w| (i, &w.name[..w.name_len], self.front == Some(i))))
}
}
impl<N: Copy> Default for Windows<N> {
fn default() -> Self {
Self::new()
}
}