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

@@ -145,7 +145,7 @@ fn run_program(sh: &mut Shell, args: &[&str]) {
// A Ctrl+C typed since the last program is not for this one.
let _ = syscall::poll(sh.done);
match sh.apps.launch(args, Some(TERMINAL), folder) {
Ok((process, granted)) => {
Ok((process, _granted)) => {
if syscall::watch_death(process, sh.done).is_ok() {
while syscall::wait(sh.done).is_ok_and(|bits| {
if bits & INTERRUPTED != 0 {
@@ -157,10 +157,6 @@ fn run_program(sh: &mut Shell, args: &[&str]) {
let _ = syscall::kill(process);
}
syscall::drop_cap(process);
// It drew over the terminal and took the keyboard.
if Capabilities::from_bits(granted).contains(Capability::Display) {
libkos::terminal::redraw(TERMINAL);
}
}
Err(r) => refused(args[0], r),
}

View File

@@ -12,5 +12,4 @@ bench = false
types = { workspace = true }
libkos = { workspace = true }
display-server = { workspace = true }
server-framework = { workspace = true }
port-framebuffer = { workspace = true }

View File

@@ -4,57 +4,40 @@
extern crate libkos;
use libkos::display::Canvas;
use libkos::syscall;
use port_framebuffer::Framebuffer;
use snake::game::{Direction, GameState, Snake};
use snake::renderer::{self, RenderCommand};
use types::bounded::CapSlot;
use types::display::{Color, Dimension, ScreenCoord};
use types::time::Ticks;
use libkos::syscall;
use server_framework::Encode;
use display_server::protocol::DisplayRequest;
/// Steps a second; QEMU virt's timebase is 10 MHz.
const STEPS_PER_SECOND: u64 = 8;
const TICK_DELAY: u64 = 10_000_000 / STEPS_PER_SECOND;
const MAX_RENDER_CMDS: usize = 140;
/// The display server, which the shell hands over because snake's manifest
/// asks for it. Input comes through it too, while snake has focus.
fn display_ep() -> CapSlot {
libkos::start::display().unwrap_or(CapSlot::const_new(0))
}
fn display_send(req: &DisplayRequest) {
let _ = syscall::request(display_ep(), req.encode());
}
fn draw_rect(x: u16, y: u16, w: u16, h: u16, color: u32) {
display_send(&DisplayRequest::DrawRect {
x: ScreenCoord::new(x),
y: ScreenCoord::new(y),
w: Dimension::new(w),
h: Dimension::new(h),
color: Color::from_raw(color),
});
}
fn flush() {
display_send(&DisplayRequest::Flush);
fn draw_rect(canvas: &mut Canvas, x: u16, y: u16, w: u16, h: u16, color: u32) {
canvas.fill_rect(ScreenCoord::new(x), ScreenCoord::new(y), Dimension::new(w), Dimension::new(h), Color::from_raw(color));
}
libkos::entry!(main);
fn main() {
if libkos::start::display().is_none() {
libkos::println!("snake: needs the display (see /bin/snake.manifest)");
// Its window, which the permission manager opened because snake's
// manifest asks for the display. Input comes through it too.
let Some(mut canvas) = libkos::start::display().and_then(Canvas::attach) else {
libkos::println!("snake: needs the display (see its manifest)");
return;
}
};
let Ok(alarm) = syscall::create_notification() else {
libkos::println!("snake: no timer");
return;
};
// Takes focus; keys are polled every tick, the signal only wakes it.
let _ = libkos::display::subscribe(display_ep(), alarm, types::badge::Badge::new(1 << 1));
let window = canvas.window();
let _ = libkos::display::subscribe(window, alarm, types::badge::Badge::new(1 << 1));
let mut clock = Clock::new(alarm);
let mut snake = Snake::new();
let mut commands = [RenderCommand { x: 0, y: 0, w: 0, h: 0, color: 0 }; MAX_RENDER_CMDS];
@@ -62,24 +45,24 @@ fn main() {
loop {
let count = renderer::render(&snake, &mut commands);
for cmd in &commands[..count] {
draw_rect(cmd.x, cmd.y, cmd.w, cmd.h, cmd.color);
draw_rect(&mut canvas, cmd.x, cmd.y, cmd.w, cmd.h, cmd.color);
}
flush();
canvas.flush();
if snake.state() == GameState::GameOver {
let gw = renderer::grid_pixel_width();
let gh = renderer::grid_pixel_height();
draw_rect(gw / 2 - 60, gh / 2 - 12, 120, 24, 0xFFCC0000);
flush();
draw_rect(&mut canvas, gw / 2 - 60, gh / 2 - 12, 120, 24, 0xFFCC0000);
canvas.flush();
// Any key leaves.
while next_input() == Input::None {
while next_input(window) == Input::None {
clock.wait_tick();
}
return;
}
clock.wait_tick();
if handle_input(&mut snake) == Input::Quit {
if handle_input(window, &mut snake) == Input::Quit {
return;
}
snake.tick();
@@ -95,8 +78,8 @@ enum Input {
}
/// The next key, if one is waiting: q or Escape quits.
fn next_input() -> Input {
let Ok([tag, kind, ch, _]) = syscall::request(display_ep(), [types::display::input::POLL, 0, 0, 0]) else {
fn next_input(window: CapSlot) -> Input {
let Ok([tag, kind, ch, _]) = syscall::request(window, [types::display::window::POLL, 0, 0, 0]) else {
return Input::None;
};
if tag == 0 {
@@ -117,9 +100,9 @@ fn next_input() -> Input {
}
/// Drains the pending keys, applying each turn; reports a quit.
fn handle_input(snake: &mut Snake) -> Input {
fn handle_input(window: CapSlot, snake: &mut Snake) -> Input {
loop {
match next_input() {
match next_input(window) {
Input::None => return Input::None,
Input::Quit => return Input::Quit,
Input::Turn(d) => snake.set_direction(d),

View File

@@ -37,18 +37,12 @@ fn subscribe_to_input() -> Option<CapSlot> {
sent.ok().map(|_| INPUT)
}
fn server<F: port_framebuffer::Framebuffer>(fb: F, input: Option<CapSlot>) -> display_server::server::DisplayServer<F> {
match input {
Some(input) => display_server::server::DisplayServer::with_input(fb, input),
None => display_server::server::DisplayServer::new(fb),
}
fn server<F: port_framebuffer::Framebuffer>(fb: F, input: Option<CapSlot>) -> display_server::server::DisplayServer<F, display_server::host::KernelHost> {
display_server::server::DisplayServer::new(fb, display_server::host::KernelHost::new(EP, input))
}
#[inline(never)]
fn run_virtio(mut fb: driver_framebuffer_virtio::VirtioFramebuffer, input: Option<CapSlot>) -> ! {
use port_framebuffer::Framebuffer;
fb.clear(types::display::Color::rgb(30, 30, 60));
fb.flush();
fn run_virtio(fb: driver_framebuffer_virtio::VirtioFramebuffer, input: Option<CapSlot>) -> ! {
let mut srv = server(fb, input);
server_framework::serve(&mut srv, EP)
}

View File

@@ -74,3 +74,67 @@ impl Framebuffer for MockFramebuffer {
self.scrolls += 1;
}
}
/// A screen of `W` by `H` pixels that keeps what is drawn, for tests that
/// look at the picture rather than the calls.
pub struct GridFramebuffer<const W: usize, const H: usize> {
pixels: [[Color; W]; H],
pub flushes: usize,
}
impl<const W: usize, const H: usize> GridFramebuffer<W, H> {
pub fn new() -> Self {
Self { pixels: [[Color::from_raw(0); W]; H], flushes: 0 }
}
/// The pixel at (x, y), `None` off the screen.
pub fn pixel(&self, x: usize, y: usize) -> Option<Color> {
self.pixels.get(y).and_then(|row| row.get(x)).copied()
}
}
impl<const W: usize, const H: usize> Default for GridFramebuffer<W, H> {
fn default() -> Self {
Self::new()
}
}
impl<const W: usize, const H: usize> Framebuffer for GridFramebuffer<W, H> {
fn width(&self) -> Dimension {
Dimension::new(W as u16)
}
fn height(&self) -> Dimension {
Dimension::new(H as u16)
}
fn write_pixel(&mut self, x: ScreenCoord, y: ScreenCoord, color: Color) {
if let Some(px) = self.pixels.get_mut(y.raw() as usize).and_then(|r| r.get_mut(x.raw() as usize)) {
*px = color;
}
}
fn fill_rect(&mut self, x: ScreenCoord, y: ScreenCoord, w: Dimension, h: Dimension, color: Color) {
for row in y.raw()..y.raw().saturating_add(h.raw()) {
for col in x.raw()..x.raw().saturating_add(w.raw()) {
self.write_pixel(ScreenCoord::new(col), ScreenCoord::new(row), color);
}
}
}
fn clear(&mut self, color: Color) {
self.pixels = [[color; W]; H];
}
fn flush(&mut self) {
self.flushes += 1;
}
fn scroll_up(&mut self, top: ScreenCoord, height: Dimension, by: Dimension, fill: Color) {
let (top, height, by) = (top.raw() as usize, height.raw() as usize, by.raw() as usize);
let end = (top + height).min(H);
for y in top..end {
self.pixels[y] = if y + by < end { self.pixels[y + by] } else { [fill; W] };
}
}
}

View File

@@ -37,6 +37,8 @@ pub fn key(code: u16, shift: bool) -> Option<Key> {
105 => Some(Key::Left),
106 => Some(Key::Right),
43 => Some(Key::Char(if shift { b'|' } else { b'\\' })),
59..=68 => Some(Key::Function((code - 58) as u8)),
87 | 88 => Some(Key::Function((code - 76) as u8)),
_ => None,
};
named.or_else(|| {
@@ -52,6 +54,13 @@ pub fn key(code: u16, shift: bool) -> Option<Key> {
mod tests {
use super::*;
#[test]
fn function_keys_are_numbered() {
assert_eq!(key(59, false), Some(Key::Function(1)));
assert_eq!(key(68, false), Some(Key::Function(10)));
assert_eq!(key(88, true), Some(Key::Function(12)));
}
#[test]
fn ctrl_letters_are_control_characters() {
assert_eq!(with_ctrl(Key::Char(b'c')), Key::Char(3));

View File

@@ -63,6 +63,9 @@ pub(crate) struct Launched {
/// to take back when it dies.
pub(crate) grants: [Option<CapSlot>; 2],
pub(crate) spawn: Option<Badge>,
/// The badge on its copy of the display, if it has one: the windows it
/// opened close when it goes.
pub(crate) display_badge: Option<usize>,
}
impl<'a> Launcher<'a> {
@@ -101,7 +104,7 @@ impl<'a> Launcher<'a> {
(self.storage, self.scratch)
}
fn endpoint(&self, name: &str) -> Option<CapSlot> {
pub(crate) fn endpoint(&self, name: &str) -> Option<CapSlot> {
self.served.iter().flatten().find(|(n, _)| *n == name).map(|(_, ep)| *ep)
}
@@ -117,8 +120,10 @@ impl<'a> Launcher<'a> {
Some(ep) => ep,
None => syscall::create_endpoint().ok_or("no endpoint")?,
};
// Read to receive on it; init keeps the original.
let server = perms(&[Permission::Read, Permission::Execute]);
// Read to receive on it; Delegate to mint badged copies of
// its own (storage's grants, the display's windows); init
// keeps the original.
let server = perms(&[Permission::Read, Permission::Execute, Permission::Delegate]);
syscall::delegate(ep, child, server).ok_or("cannot give its endpoint")?;
Some(ep)
}
@@ -127,10 +132,14 @@ impl<'a> Launcher<'a> {
// A client calls (Execute) and may send capabilities through it and
// pass it on (Delegate); never Read, so it cannot pose as the server
// and take other clients' requests.
let mut display_badge = None;
for name in p.uses {
let ep = self.endpoint(name).ok_or("needs a server that is not running")?;
let badge = Badge::new(self.next_badge);
self.next_badge += 1;
if *name == "display" {
display_badge = Some(badge.raw());
}
let copy = syscall::mint(ep, badge, perms(&[Permission::Execute, Permission::Delegate]))
.map_err(|_| "cannot mint an endpoint")?;
let given = syscall::delegate(copy, child, perms(&[Permission::Execute, Permission::Delegate]));
@@ -192,7 +201,7 @@ impl<'a> Launcher<'a> {
note = ", console handed over";
}
}
Ok(Launched { note, process: child, ep, grants, spawn })
Ok(Launched { note, process: child, ep, grants, spawn, display_badge })
}
/// The path on disk `scope` names: inside the user's folder if it is a

View File

@@ -103,12 +103,16 @@ pub const SERVERS: &[Program] = &[
uses: &["input"],
// Its clients subscribe and take focus.
restarts_clients: true,
// The framebuffer alone is ~1000 pages at 1280x800.
..server("display", b"bin/display", Limits { pages: 1280, regions: 4, ..SMALL })
// Its windows' state (input queues among it) is KiBs.
stack_pages: 8,
// The framebuffer is ~1000 pages at 1280x800, and so is each of
// the (at most 4) windows' pixel buffers, which it owns.
..server("display", b"bin/display", Limits { pages: 5200, regions: 8, ..SMALL })
},
// Text on the display, lines from the keyboard (it takes input focus).
// Its clients wait on lines and Ctrl+C.
Program { uses: &["display"], restarts_clients: true, ..server("terminal", b"bin/terminal", SMALL) },
// Its clients wait on lines and Ctrl+C. It draws its own text now,
// beside a screenful of grid.
Program { uses: &["display"], restarts_clients: true, stack_pages: 8, ..server("terminal", b"bin/terminal", SMALL) },
server("session", b"bin/session", SMALL),
];

View File

@@ -2,7 +2,8 @@
//! server, so a blocking call to it could wait on itself. Text goes into a
//! ring; a thread of init's own takes it to the log and waits there when
//! the log is down, instead of init. Until that thread runs (early boot),
//! init prints to the console directly.
//! init prints to the console directly. The same thread carries the few
//! messages init sends other servers it restarts (`post`).
use core::fmt;
use core::sync::atomic::{AtomicU8, AtomicUsize, Ordering};
@@ -20,6 +21,31 @@ static LOST: AtomicUsize = AtomicUsize::new(0);
/// The printer's notification, once it runs.
static WAKE: AtomicUsize = AtomicUsize::new(usize::MAX);
/// Messages waiting: an endpoint slot and four words each.
const POSTS: usize = 16;
static POSTED: [[AtomicUsize; 5]; POSTS] = [const { [const { AtomicUsize::new(0) }; 5] }; POSTS];
static POST_HEAD: AtomicUsize = AtomicUsize::new(0);
static POST_TAIL: AtomicUsize = AtomicUsize::new(0);
/// Has `words` called to `ep` by the printer thread, so init never waits
/// on it; dropped (and said so) if too many are waiting.
pub(crate) fn post(ep: CapSlot, words: [usize; 4]) {
let tail = POST_TAIL.load(Ordering::Relaxed);
if tail - POST_HEAD.load(Ordering::Acquire) == POSTS || wake().is_none() {
crate::outln!("init: a message to a server was dropped");
return;
}
let slot = &POSTED[tail % POSTS];
slot[0].store(ep.value() as usize, Ordering::Relaxed);
for (w, v) in slot[1..].iter().zip(words) {
w.store(v, Ordering::Relaxed);
}
POST_TAIL.store(tail + 1, Ordering::Release);
if let Some(wake) = wake() {
let _ = syscall::signal(wake);
}
}
fn wake() -> Option<CapSlot> {
u8::try_from(WAKE.load(Ordering::Acquire)).ok().and_then(CapSlot::new)
}
@@ -87,6 +113,17 @@ fn printer(_: &'static AtomicUsize) -> ! {
if lost > 0 {
libkos::println!("init: {} bytes of output lost while the log was down", lost);
}
while POST_HEAD.load(Ordering::Relaxed) != POST_TAIL.load(Ordering::Acquire) {
let head = POST_HEAD.load(Ordering::Relaxed);
let slot = &POSTED[head % POSTS];
let words = core::array::from_fn(|i| slot[i + 1].load(Ordering::Relaxed));
let ep = u8::try_from(slot[0].load(Ordering::Relaxed)).ok().and_then(CapSlot::new);
POST_HEAD.store(head + 1, Ordering::Release);
if let Some(ep) = ep {
// Waits while that server is down; init goes on.
let _ = syscall::request(ep, words);
}
}
}
}

View File

@@ -108,6 +108,9 @@ pub(crate) fn sign_in(l: &mut Launcher) -> Result<(), &'static str> {
crate::factory::prepare(fs, folder, scratch)?;
let name = core::str::from_utf8(&h.name[..h.name_len]).unwrap_or("someone");
crate::outln!("init: {} is in", name);
if let Some(display) = l.endpoint("display") {
crate::output::post(display, libkos::display::set_user_words(name.as_bytes()));
}
l.user = Some(User { folder, guest });
start_session(l);
Ok(())
@@ -129,6 +132,9 @@ fn start_session(l: &mut Launcher) {
pub(crate) fn sign_out(l: &mut Launcher) {
l.spawner.clear();
l.stop_session();
if let Some(display) = l.endpoint("display") {
crate::output::post(display, libkos::display::set_user_words(b""));
}
let user = l.user.take();
let (fs, _) = l.storage();
let locked = fs.lock().is_ok();

View File

@@ -83,14 +83,23 @@ pub(crate) struct Supervised {
ep: Option<CapSlot>,
/// Killed for hanging: the watchdog's ping is queued on its endpoint.
hung: bool,
/// The badge on its copy of the display: its windows close with it.
display_badge: Option<usize>,
}
/// What a program held through init, to take back when it goes.
struct Held {
grants: [Option<CapSlot>; 2],
spawn: Option<Badge>,
display_badge: Option<usize>,
}
impl Supervised {
/// What this run held through init, the first time only: by the time
/// its death is heard, a slot it freed may hold someone else's
/// capability.
fn take_held(&mut self) -> Option<([Option<CapSlot>; 2], Option<Badge>)> {
let held = (self.grants, self.spawn);
fn take_held(&mut self) -> Option<Held> {
let held = Held { grants: self.grants, spawn: self.spawn, display_badge: self.display_badge };
self.grants = [None; 2];
self.spawn = None;
core::mem::take(&mut self.holding).then_some(held)
@@ -128,6 +137,7 @@ impl Launcher<'_> {
holding: true,
ep,
hung: false,
display_badge: started.display_badge,
});
self.ping(index);
Ok(note)
@@ -168,8 +178,12 @@ impl Launcher<'_> {
/// Takes back what supervised program `index` held through init.
fn let_go(&mut self, index: usize) {
// Never a call to a server: it may be the one that is down.
let Some((grants, spawn)) = self.supervised[index].as_mut().and_then(Supervised::take_held) else { return };
self.release(grants, spawn);
let Some(held) = self.supervised[index].as_mut().and_then(Supervised::take_held) else { return };
self.release(held.grants, held.spawn);
// The windows it opened (the terminal's, its apps') close.
if let (Some(badge), Some(display)) = (held.display_badge, self.endpoint("display")) {
crate::output::post(display, libkos::display::forget_words(badge));
}
}
/// Has the watchdog ping program `index`, if it is to be pinged.
@@ -306,6 +320,7 @@ impl Launcher<'_> {
spawn: started.spawn,
holding: true,
ep: started.ep,
display_badge: started.display_badge,
..s
});
self.ping(index);

View File

@@ -5,3 +5,4 @@ edition = "2024"
[dependencies]
types = { workspace = true }
port-framebuffer = { workspace = true }

View File

@@ -1,86 +1,188 @@
//! The window protocol's client (`types::display::window`). A launcher
//! opens a window for an app and passes it on; the app maps the window's
//! pixel buffer as a `Canvas`, draws into it (`raster` draws text) and
//! presents what changed; the display shows it while the window is in
//! front.
use crate::syscall;
use crate::window::Window;
use types::address::{ByteOffset, VirtualAddress};
use types::bounded::CapSlot;
use types::display::window::{self as w, PIXEL_BYTES};
use types::display::{Color, Dimension, ScreenCoord};
use types::mapping::PagePerms;
/// A failed call reads as an error reply (tag `usize::MAX`).
fn ipc(ep: CapSlot, w0: usize, w1: usize, w2: usize, w3: usize) -> (usize, usize, usize, usize) {
let r = syscall::request(ep, [w0, w1, w2, w3]).unwrap_or([usize::MAX, 0, 0, 0]);
(r[0], r[1], r[2], r[3])
/// Where an app's canvas is mapped.
const CANVAS_VA: VirtualAddress = VirtualAddress::from_raw(0x0100_0000);
/// A window just opened: its capability, to pass on, and its size.
pub struct Opened {
pub window: CapSlot,
pub width: Dimension,
pub height: Dimension,
}
pub fn draw_rect(
ep: CapSlot,
x: ScreenCoord,
y: ScreenCoord,
w: Dimension,
h: Dimension,
color: Color,
) {
let xy = (x.raw() as usize) | ((y.raw() as usize) << 16);
let wh = (w.raw() as usize) | ((h.raw() as usize) << 16);
ipc(ep, 0, xy, wh, color.raw() as usize);
}
pub fn clear(ep: CapSlot, color: Color) {
ipc(ep, 1, color.raw() as usize, 0, 0);
}
pub fn flush(ep: CapSlot) {
ipc(ep, 2, 0, 0, 0);
}
pub fn get_dimensions(ep: CapSlot) -> (Dimension, Dimension) {
let (tag, w, h, _) = ipc(ep, 3, 0, 0, 0);
if tag == 1 {
(Dimension::new(w as u16), Dimension::new(h as u16))
} else {
(Dimension::new(0), Dimension::new(0))
/// A launcher opens a window named `name` (what the OS bar shows).
pub fn open(display: CapSlot, name: &[u8]) -> Option<Opened> {
let n = w::pack_name(name);
match syscall::call(display, [w::OPEN, n[0], n[1], n[2]], None).ok()? {
syscall::Received::Message { words: [w::OK, width, height, _], cap: Some(window), .. } => {
Some(Opened { window, width: Dimension::new(width as u16), height: Dimension::new(height as u16) })
}
_ => None,
}
}
/// Draws `s` (up to `TEXT_BYTES` per call, more in several) from `(x, y)`.
pub fn draw_text(ep: CapSlot, x: ScreenCoord, y: ScreenCoord, fg: Color, bg: Color, s: &[u8]) {
use types::display::text::{TEXT_BYTES, Text};
for (i, chunk) in s.chunks(TEXT_BYTES).enumerate() {
let x = ScreenCoord::new(x.raw().saturating_add((i * TEXT_BYTES * CELL_WIDTH) as u16));
let _ = syscall::request(ep, Text::new(x, y, fg, bg, chunk).words());
}
/// Closes `window`: every copy of it, and its buffer, stop working.
pub fn close(window: CapSlot) {
let _ = syscall::request(window, [w::CLOSE, 0, 0, 0]);
}
/// Moves the rows from `top`, `height` tall, up by `by`, filling the rows
/// left at the bottom with `fill`.
pub fn scroll(ep: CapSlot, top: ScreenCoord, height: Dimension, by: Dimension, fill: Color) {
let band = (top.raw() as usize) | ((height.raw() as usize) << 16);
let _ = syscall::request(ep, [types::display::SCROLL, band, by.raw() as usize, fill.raw() as usize]);
}
/// The display server's character cell, in pixels.
pub const CELL_WIDTH: usize = 8;
pub const CELL_HEIGHT: usize = 16;
/// Takes input focus: returns a notification signalled whenever events
/// arrive; read them with `poll_input`.
pub fn subscribe_input(ep: CapSlot) -> Option<CapSlot> {
use types::badge::Badge;
let notify = syscall::create_notification().ok()?;
subscribe(ep, notify, Badge::NONE).then_some(notify)
}
/// Takes input focus, to be told through `notify` with `badge`'s bits.
pub fn subscribe(ep: CapSlot, notify: CapSlot, badge: types::badge::Badge) -> bool {
/// Takes input for `window`: `notify` is signalled with `badge`'s bits
/// when some arrives while it is in front; read it with `poll_input`.
pub fn subscribe(window: CapSlot, notify: CapSlot, badge: types::badge::Badge) -> bool {
use types::permission::{Permission, PermissionSet};
// Signal only (Delegate just to send it; the transfer strips it).
let signal = PermissionSet::empty().with(Permission::Write).with(Permission::Delegate);
let Ok(copy) = syscall::mint(notify, badge, signal) else {
return false;
};
let sent = syscall::call(ep, [types::display::input::SUBSCRIBE, 0, 0, 0], Some(copy.into()));
let sent = syscall::call(window, [w::SUBSCRIBE, 0, 0, 0], Some(copy.into()));
syscall::drop_cap(copy);
sent.is_ok_and(|r| r.words()[0] == 0)
sent.is_ok_and(|r| r.words()[0] == w::OK)
}
/// The next input event, if this process has focus and one is waiting.
pub fn poll_input(ep: CapSlot) -> Option<crate::input::InputEvent> {
let words = syscall::request(ep, [types::display::input::POLL, 0, 0, 0]).ok()?;
/// `window`'s next input event, if one is waiting.
pub fn poll_input(window: CapSlot) -> Option<crate::input::InputEvent> {
let words = syscall::request(window, [w::POLL, 0, 0, 0]).ok()?;
crate::input::decode(words)
}
/// A changed area, as corners.
#[derive(Clone, Copy)]
struct Damage {
x0: u16,
y0: u16,
x1: u16,
y1: u16,
}
/// A window's pixels, mapped here. Draw with the `Framebuffer` methods (and
/// `raster`); `flush` presents everything drawn since the last one.
pub struct Canvas {
window: CapSlot,
pixels: Window,
width: Dimension,
height: Dimension,
damage: Option<Damage>,
}
impl Canvas {
/// Maps `window`'s buffer.
pub fn attach(window: CapSlot) -> Option<Self> {
let (words, buffer) = match syscall::call(window, [w::BUFFER, 0, 0, 0], None).ok()? {
syscall::Received::Message { words, cap: Some(buffer), .. } if words[0] == w::OK => (words, buffer),
_ => return None,
};
let pixels = crate::window::map(buffer, CANVAS_VA, PagePerms::ReadWrite).ok()?;
let (width, height) = (Dimension::new(words[1] as u16), Dimension::new(words[2] as u16));
// The display says how big; the mapping says how much is there.
(width.raw() as usize * height.raw() as usize * PIXEL_BYTES <= pixels.len())
.then_some(Self { window, pixels, width, height, damage: None })
}
pub fn window(&self) -> CapSlot {
self.window
}
fn offset(&self, x: u16, y: u16) -> ByteOffset {
ByteOffset::new((y as usize * self.width.raw() as usize + x as usize) * PIXEL_BYTES)
}
/// Marks `x..x+w`, `y..y+h` (already clipped) as changed.
fn touch(&mut self, x: u16, y: u16, w: u16, h: u16) {
let (x1, y1) = (x + w, y + h);
self.damage = Some(match self.damage {
None => Damage { x0: x, y0: y, x1, y1 },
Some(d) => Damage { x0: d.x0.min(x), y0: d.y0.min(y), x1: d.x1.max(x1), y1: d.y1.max(y1) },
});
}
/// The part of a rectangle inside the canvas, if any.
fn clip(&self, x: ScreenCoord, y: ScreenCoord, w: Dimension, h: Dimension) -> Option<(u16, u16, u16, u16)> {
let (cw, ch) = (self.width.raw(), self.height.raw());
let (x, y) = (x.raw(), y.raw());
(x < cw && y < ch).then(|| (x, y, w.raw().min(cw - x), h.raw().min(ch - y)))
}
}
impl port_framebuffer::Framebuffer for Canvas {
fn width(&self) -> Dimension {
self.width
}
fn height(&self) -> Dimension {
self.height
}
fn write_pixel(&mut self, x: ScreenCoord, y: ScreenCoord, color: Color) {
if let Some((x, y, _, _)) = self.clip(x, y, Dimension::new(1), Dimension::new(1)) {
self.pixels.write(self.offset(x, y), color.raw());
self.touch(x, y, 1, 1);
}
}
fn write_row(&mut self, x: ScreenCoord, y: ScreenCoord, row: &[Color]) {
let Some((x, y, w, _)) = self.clip(x, y, Dimension::new(row.len().min(u16::MAX as usize) as u16), Dimension::new(1)) else { return };
let mut run = [0u32; 64];
for (i, chunk) in row[..w as usize].chunks(run.len()).enumerate() {
for (slot, c) in run.iter_mut().zip(chunk) {
*slot = c.raw();
}
self.pixels.write_u32s(self.offset(x + (i * 64) as u16, y), &run[..chunk.len()]);
}
self.touch(x, y, w, 1);
}
fn fill_rect(&mut self, x: ScreenCoord, y: ScreenCoord, w: Dimension, h: Dimension, color: Color) {
let Some((x, y, w, h)) = self.clip(x, y, w, h) else { return };
for row in y..y + h {
self.pixels.fill_u32(self.offset(x, row), w as usize, color.raw());
}
self.touch(x, y, w, h);
}
fn clear(&mut self, color: Color) {
let (w, h) = (self.width, self.height);
self.fill_rect(ScreenCoord::new(0), ScreenCoord::new(0), w, h, color);
}
/// Presents what changed since the last flush.
fn flush(&mut self) {
let Some(d) = self.damage.take() else { return };
let rect = w::pack_rect(ScreenCoord::new(d.x0), ScreenCoord::new(d.y0), Dimension::new(d.x1 - d.x0), Dimension::new(d.y1 - d.y0));
let _ = syscall::request(self.window, [w::PRESENT, rect[0], rect[1], 0]);
}
fn scroll_up(&mut self, top: ScreenCoord, height: Dimension, by: Dimension, fill: Color) {
let Some((_, top, _, height)) = self.clip(ScreenCoord::new(0), top, self.width, height) else { return };
let by = by.raw().min(height);
let row = self.width.raw() as usize * PIXEL_BYTES;
let kept = (height - by) as usize * row;
self.pixels.copy_within(self.offset(0, top + by), self.offset(0, top), kept);
let width = self.width;
self.fill_rect(ScreenCoord::new(0), ScreenCoord::new(top + height - by), width, Dimension::new(by), fill);
self.touch(0, top, width.raw(), height);
}
}
/// From init only: who is signed in, shown in the OS bar.
pub fn set_user_words(name: &[u8]) -> [usize; 4] {
let n = w::pack_name(name);
[w::SET_USER, n[0], n[1], n[2]]
}
/// From init only: the launcher behind `badge` is gone, and its windows.
pub fn forget_words(badge: usize) -> [usize; 4] {
[w::FORGET, badge, 0, 0]
}

View File

@@ -13,6 +13,8 @@ pub enum Key {
Backspace,
Tab,
Char(u8),
/// F1 is 1.
Function(u8),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -33,6 +35,7 @@ fn decode_key(kind: usize, extra: usize) -> Option<Key> {
7 => Some(Key::Backspace),
8 => Some(Key::Tab),
9 => Some(Key::Char(extra as u8)),
types::input::event::FUNCTION_KEY => Some(Key::Function(extra as u8)),
_ => None,
}
}

View File

@@ -37,12 +37,6 @@ pub fn clear(terminal: CapSlot) {
let _ = syscall::request(terminal, [types::terminal::CLEAR, 0, 0, 0]);
}
/// Repaints the terminal and gives it input focus again, after a program
/// that drew on the screen.
pub fn redraw(terminal: CapSlot) {
let _ = syscall::request(terminal, [types::terminal::REDRAW, 0, 0, 0]);
}
/// Has the terminal signal `notify` with `badge`'s bits when Ctrl+C is
/// typed while no line is being read. Only the first caller is heard.
pub fn on_interrupt(terminal: CapSlot, notify: CapSlot, badge: types::badge::Badge) -> bool {

View File

@@ -13,6 +13,8 @@ pub enum Key {
Backspace,
Tab,
Char(u8),
/// F1 is 1.
Function(u8),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]

11
crates/raster/Cargo.toml Normal file
View File

@@ -0,0 +1,11 @@
[package]
name = "raster"
version = "0.1.0"
edition = "2024"
[dependencies]
types = { workspace = true }
port-framebuffer = { workspace = true }
[dev-dependencies]
driver-framebuffer-mock = { workspace = true }

View File

@@ -1,4 +1,4 @@
//! The display server's one font: Spleen 8x16 (BSD-2-Clause, Frederic
//! The 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.

70
crates/raster/src/lib.rs Normal file
View File

@@ -0,0 +1,70 @@
//! Drawing on anything pixel-addressed (a `Framebuffer`): the screen, for
//! the display server, or a window's pixel buffer, for an app.
#![no_std]
#![forbid(unsafe_code)]
pub mod font;
use port_framebuffer::Framebuffer;
use types::display::{Color, ScreenCoord};
/// Characters drawn per row of pixels at a time: a small buffer, so a
/// caller's stack need not hold a whole line.
const CHUNK: usize = 32;
/// `text` from (x, y), each character cell whole, `fg` on `bg`, so text
/// overwrites what was there; cells past the right or bottom edge are left
/// out.
pub fn draw_text(fb: &mut impl Framebuffer, x: ScreenCoord, y: ScreenCoord, fg: Color, bg: Color, text: &[u8]) {
let (fw, fh) = (fb.width().raw(), fb.height().raw());
let (x0, y0) = (x.raw(), y.raw());
if y0.saturating_add(font::HEIGHT) > fh {
return;
}
let fit = (fw.saturating_sub(x0) / font::WIDTH) as usize;
let chars = &text[..text.len().min(fit)];
// One run per pixel row across a chunk of cells, not a call per pixel.
let mut line = [bg; CHUNK * font::WIDTH as usize];
for (i, chunk) in chars.chunks(CHUNK).enumerate() {
let at = ScreenCoord::new(x0 + (i * CHUNK) as u16 * font::WIDTH);
let line = &mut line[..chunk.len() * font::WIDTH as usize];
for row in 0..font::HEIGHT {
for (cell, c) in line.chunks_mut(font::WIDTH as usize).zip(chunk) {
let bits = font::glyph(*c)[row as usize];
for (col, px) in cell.iter_mut().enumerate() {
*px = if bits & (0x80 >> col) != 0 { fg } else { bg };
}
}
fb.write_row(at, ScreenCoord::new(y0 + row), line);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use driver_framebuffer_mock::GridFramebuffer;
#[test]
fn text_is_drawn_cell_by_cell_and_clipped_at_the_edge() {
let mut fb = GridFramebuffer::<20, 20>::new();
let (fg, bg) = (Color::rgb(255, 255, 255), Color::rgb(1, 1, 1));
draw_text(&mut fb, ScreenCoord::new(0), ScreenCoord::new(0), fg, bg, b"!!!");
// '!' has its stem in columns 3 and 4 from row 2.
assert_eq!(fb.pixel(3, 2), Some(fg));
assert_eq!(fb.pixel(0, 2), Some(bg));
assert_eq!(fb.pixel(11, 2), Some(fg), "the second cell");
assert_eq!(fb.pixel(19, 2), Some(Color::from_raw(0)), "a third cell would not fit whole");
}
#[test]
fn a_long_line_is_drawn_in_chunks_without_a_gap() {
let mut fb = GridFramebuffer::<{ 8 * 70 }, 16>::new();
let (fg, bg) = (Color::rgb(255, 255, 255), Color::rgb(1, 1, 1));
draw_text(&mut fb, ScreenCoord::new(0), ScreenCoord::new(0), fg, bg, &[b'!'; 70]);
// The stem of the 33rd and the 70th '!'.
assert_eq!(fb.pixel(32 * 8 + 3, 2), Some(fg));
assert_eq!(fb.pixel(69 * 8 + 3, 2), Some(fg));
}
}

View File

@@ -8,6 +8,7 @@ types = { workspace = true }
server-framework = { workspace = true }
libkos = { workspace = true }
port-framebuffer = { workspace = true }
raster = { workspace = true }
[dev-dependencies]
driver-framebuffer-mock = { workspace = true }

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

@@ -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()
}
}

View File

@@ -53,6 +53,7 @@ fn encode_key(key: Key) -> (usize, usize) {
Key::Backspace => (7, 0),
Key::Tab => (8, 0),
Key::Char(c) => (9, c as usize),
Key::Function(n) => (types::input::event::FUNCTION_KEY, n as usize),
}
}
@@ -68,6 +69,7 @@ fn decode_key(kind: usize, data: usize) -> Option<Key> {
7 => Some(Key::Backspace),
8 => Some(Key::Tab),
9 => Some(Key::Char(data as u8)),
types::input::event::FUNCTION_KEY => Some(Key::Function(data as u8)),
_ => None,
}
}

View File

@@ -43,6 +43,8 @@ struct Running {
granted: Capabilities,
/// Its data folder grant, taken back when it dies.
data: Option<CapSlot>,
/// Its window, closed when it dies.
window: Option<CapSlot>,
}
pub struct PermissionServer {
@@ -177,13 +179,19 @@ impl PermissionServer {
true => self.store.data_grant(&plan.app.id),
false => None,
};
// Opened here, named from its manifest: the app cannot name its own.
let window = match plan.granted.contains(Capability::Display) {
true => self.display.and_then(|d| libkos::display::open(d, plan.app.name.as_str().as_bytes())).map(|o| o.window),
false => None,
};
let staged = (badge == self.staged.badge).then_some(&self.staged);
let arg = StartArg {
log: staged.and_then(|s| s.output).and_then(|o| child.give(o, call)).or(child.log()),
storage: staged.and_then(|s| s.folder).and_then(|f| child.give(f, call)),
data: data.and_then(|d| child.give(d, call)),
// It subscribes to input by sending the display a notification.
display: self.display.filter(|_| plan.granted.contains(Capability::Display)).and_then(|d| child.give_sending(d, call)),
// Delegate, to send its notification when it subscribes.
display: window.and_then(|w| child.give_sending(w, call)),
args: child.set_args(args),
console: None,
};
@@ -193,10 +201,11 @@ impl PermissionServer {
if let Some(d) = data {
let _ = libkos::fs::release(d);
}
close_window(window);
syscall::drop_cap(child.process);
return Err(code(reply::FAILED));
}
self.running[index] = Some(Running { id: plan.app.id, process: child.process, granted: plan.granted, data });
self.running[index] = Some(Running { id: plan.app.id, process: child.process, granted: plan.granted, data, window });
Ok((child.process, plan.granted))
}
@@ -271,6 +280,7 @@ impl PermissionServer {
if let Some(d) = r.data {
let _ = libkos::fs::release(d);
}
close_window(r.window);
syscall::drop_cap(r.process);
}
}
@@ -331,3 +341,10 @@ impl Server for PermissionServer {
self.reap(bits);
}
}
fn close_window(window: Option<CapSlot>) {
if let Some(w) = window {
libkos::display::close(w);
syscall::drop_cap(w);
}
}

View File

@@ -12,3 +12,5 @@ bench = false
types = { workspace = true }
libkos = { workspace = true }
server-framework = { workspace = true }
port-framebuffer = { workspace = true }
raster = { workspace = true }

View File

@@ -11,8 +11,9 @@ pub mod server;
#[path = "tests/mod.rs"]
mod tests;
use libkos::display::{self, CELL_HEIGHT, CELL_WIDTH};
use libkos::display::{self, Canvas};
use libkos::input::{InputEvent, Key};
use port_framebuffer::Framebuffer;
use libkos::syscall;
use types::badge::Badge;
use types::bounded::CapSlot;
@@ -23,6 +24,9 @@ use types::terminal::{KEYS_BIT, TIMER_BIT};
const EP: CapSlot = CapSlot::const_new(0);
const DISPLAY: CapSlot = CapSlot::const_new(1);
const CELL_WIDTH: usize = raster::font::WIDTH as usize;
const CELL_HEIGHT: usize = raster::font::HEIGHT as usize;
const FG: Color = Color::rgb(220, 220, 220);
const BG: Color = Color::rgb(16, 16, 24);
/// Output is shown at most this long after it is written. The timebase is
@@ -30,12 +34,9 @@ const BG: Color = Color::rgb(16, 16, 24);
const FLUSH_DELAY: u64 = 20 * 10_000;
struct Screen {
/// Bound to the serving thread: the display signals it with
/// `KEYS_BIT`, the flush timer with `TIMER_BIT`.
notify: CapSlot,
/// The terminal's window, all of which scrolls.
canvas: Canvas,
timer: CapSlot,
/// The screen's height, all of which scrolls.
height: Dimension,
}
/// A key as the line editor wants it, if it is one.
@@ -56,21 +57,22 @@ fn at(col: usize, row: usize) -> (ScreenCoord, ScreenCoord) {
impl server::Console for Screen {
fn draw_row(&mut self, row: usize, text: &[u8]) {
let (x, y) = at(0, row);
display::draw_text(DISPLAY, x, y, FG, BG, text);
raster::draw_text(&mut self.canvas, x, y, FG, BG, text);
}
fn draw_cursor(&mut self, col: usize, row: usize, under: u8) {
let (x, y) = at(col, row);
display::draw_text(DISPLAY, x, y, BG, FG, &[under]);
raster::draw_text(&mut self.canvas, x, y, BG, FG, &[under]);
}
fn flush(&mut self) {
display::flush(DISPLAY);
self.canvas.flush();
}
fn scroll(&mut self, rows: usize) {
let by = Dimension::new(u16::try_from(rows * CELL_HEIGHT).unwrap_or(u16::MAX));
display::scroll(DISPLAY, ScreenCoord::new(0), self.height, by, BG);
let height = self.canvas.height();
self.canvas.scroll_up(ScreenCoord::new(0), height, by, BG);
}
fn flush_later(&mut self) {
@@ -79,7 +81,7 @@ impl server::Console for Screen {
}
fn poll_key(&mut self) -> Option<u8> {
while let Some(event) = display::poll_input(DISPLAY) {
while let Some(event) = display::poll_input(self.canvas.window()) {
if let Some(key) = byte(event) {
return Some(key);
}
@@ -95,11 +97,6 @@ impl server::Console for Screen {
let _ = syscall::reply_saved(reply, words, None);
}
fn take_focus(&mut self) {
display::subscribe(DISPLAY, self.notify, Badge::new(KEYS_BIT));
display::clear(DISPLAY, BG);
}
fn interrupt(&mut self, notification: CapSlot) {
let _ = syscall::signal(notification);
}
@@ -119,14 +116,17 @@ pub fn run() -> ! {
libkos::println!("terminal: no timer, exiting");
syscall::exit();
};
if !display::subscribe(DISPLAY, notify, Badge::new(KEYS_BIT)) {
libkos::println!("terminal: no input focus, exiting");
let Some(mut canvas) = display::open(DISPLAY, b"Terminal").and_then(|w| Canvas::attach(w.window)) else {
libkos::println!("terminal: no window, exiting");
syscall::exit();
};
if !display::subscribe(canvas.window(), notify, Badge::new(KEYS_BIT)) {
libkos::println!("terminal: no input, exiting");
syscall::exit();
}
let (w, h) = display::get_dimensions(DISPLAY);
display::clear(DISPLAY, BG);
let cols = w.raw() as usize / CELL_WIDTH;
let rows = h.raw() as usize / CELL_HEIGHT;
let mut srv = server::TerminalServer::new(Screen { notify, timer, height: h }, cols, rows);
canvas.clear(BG);
let cols = canvas.width().raw() as usize / CELL_WIDTH;
let rows = canvas.height().raw() as usize / CELL_HEIGHT;
let mut srv = server::TerminalServer::new(Screen { canvas, timer }, cols, rows);
server_framework::serve(&mut srv, EP)
}

View File

@@ -24,8 +24,6 @@ pub trait Console {
/// Keeps the caller of the request being handled, to answer later.
fn save_reply(&mut self) -> Option<SavedReply>;
fn answer(&mut self, reply: SavedReply, words: [usize; 4]);
/// Takes input focus back from whoever took it, and clears the screen.
fn take_focus(&mut self);
fn interrupt(&mut self, notification: CapSlot);
/// Lets go of a notification it was given.
fn forget(&mut self, notification: CapSlot);
@@ -259,15 +257,6 @@ impl<C: Console> Server for TerminalServer<C> {
// Kept for later: what goes back now is not sent.
types::terminal::READ => self.read(false).unwrap_or([0; 4]),
types::terminal::READ_SECRET => self.read(true).unwrap_or([0; 4]),
types::terminal::REDRAW => {
// Taking focus back clears the screen.
self.console.take_focus();
self.drawn = [0; crate::grid::MAX_ROWS];
self.grid.touch_all();
self.redraw();
self.console.flush();
[0; 4]
}
types::terminal::ON_INTERRUPT => match cap {
Some(n) if badge & types::terminal::CONTROL != 0 => {
if let Some(old) = self.on_interrupt.replace(n) {

View File

@@ -15,7 +15,6 @@ struct Fake {
typed: Vec<u8>,
flushes: usize,
timers: usize,
focus_taken: usize,
scrolled: usize,
/// Every row drawn, with how many cells.
rows_drawn: Vec<(usize, usize)>,
@@ -52,9 +51,6 @@ impl Console for Fake {
fn answer(&mut self, reply: SavedReply, words: [usize; 4]) {
self.answered.push((reply, words));
}
fn take_focus(&mut self) {
self.focus_taken += 1;
}
fn interrupt(&mut self, notification: CapSlot) {
self.interrupted.push(notification);
}
@@ -149,15 +145,6 @@ fn ctrl_c_drops_the_line_being_typed() {
assert_eq!(line_of(&mut t), b"ls\n");
}
#[test]
fn redraw_takes_focus_back_and_paints_every_row() {
let mut t = term(&[]);
t.handle_call(1, text_words(b"hi"), None);
t.handle_call(1, [types::terminal::REDRAW, 0, 0, 0], None);
assert_eq!(t.console().focus_taken, 1);
assert_eq!(&t.grid().row(0)[..2], b"hi");
}
#[test]
fn a_full_screen_scrolls_its_pixels_and_draws_one_row() {
let mut t = term(&[]);

View File

@@ -49,78 +49,87 @@ impl Dimension {
}
}
/// `DrawText`, shared by the display server and `libkos::display`: up to
/// `TEXT_BYTES` characters at a pixel position, each cell `fg` on `bg`.
/// Words: `[TEXT | len << 8 | fg << 16, x | y << 16 | bg << 32, bytes 0..8, 8..16]`.
/// Request tag: move a band of rows up (the display server's `Scroll`).
pub const SCROLL: usize = 7;
/// The window protocol, shared by the display server and
/// `libkos::display` (see docs/ipc.md). The display owns the screen and
/// draws the OS bar; every app draws only into its own window's pixel
/// buffer, which the display shows while the window is in front.
pub mod window {
use super::{Dimension, ScreenCoord};
pub mod text {
use super::{Color, ScreenCoord};
/// Through a launcher's copy of the display endpoint (one init gave a
/// system program): opens a window for the app named in words 1..4
/// (`pack_name`). Reply `[OK, width, height, 0]` carrying the window
/// capability, which the launcher passes on; the new window is in front.
pub const OPEN: usize = 1;
/// Through a window: reply `[OK, width, height, 0]` carrying its pixel buffer, a
/// region the holder maps read-write: `width * height` pixels, rows
/// one after another, each pixel a `Color` as a little-endian u32.
pub const BUFFER: usize = 2;
/// Through a window: the rectangle (`pack_rect`) that changed.
pub const PRESENT: usize = 3;
/// Through a window: carries the notification signalled when input
/// for it arrives (only while it is in front).
pub const SUBSCRIBE: usize = 4;
/// Through a window: its next input event, as the input server words
/// it (tag 0: none).
pub const POLL: usize = 5;
/// Through a window: closes it; every copy of it stops working.
pub const CLOSE: usize = 6;
/// From init (unbadged) only: who is signed in, for the OS bar (words
/// 1..4, `pack_name`; empty when nobody is).
pub const SET_USER: usize = 7;
pub const TEXT: usize = 4;
pub const TEXT_BYTES: usize = 16;
/// From init (unbadged) only: word 1 a launcher's badge, which has
/// gone; every window it opened closes.
pub const FORGET: usize = 8;
/// The function key that brings the next window to the front; the
/// display keeps it to itself.
pub const SWITCH_KEY: usize = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Text {
pub x: ScreenCoord,
pub y: ScreenCoord,
pub fg: Color,
pub bg: Color,
pub bytes: [u8; TEXT_BYTES],
pub len: usize,
/// On the badge of every window capability; launchers' badges are
/// small, so the two never meet.
pub const BADGE: usize = 1 << 61;
pub const OK: usize = 0;
pub const PIXEL_BYTES: usize = 4;
/// Longest name the OS bar shows.
pub const NAME_BYTES: usize = 24;
/// A name in three words, NUL padded, cut at `NAME_BYTES`.
pub fn pack_name(name: &[u8]) -> [usize; 3] {
let mut bytes = [0u8; NAME_BYTES];
let n = name.len().min(NAME_BYTES);
bytes[..n].copy_from_slice(&name[..n]);
core::array::from_fn(|i| {
u64::from_le_bytes(bytes[i * 8..i * 8 + 8].try_into().unwrap_or([0; 8])) as usize
})
}
impl Text {
/// `s` past `TEXT_BYTES` is cut off.
pub fn new(x: ScreenCoord, y: ScreenCoord, fg: Color, bg: Color, s: &[u8]) -> Self {
let len = s.len().min(TEXT_BYTES);
let mut bytes = [0u8; TEXT_BYTES];
bytes[..len].copy_from_slice(&s[..len]);
Self {
x,
y,
fg,
bg,
bytes,
len,
}
/// The name in `words`, and how long it is.
pub fn unpack_name(words: [usize; 3]) -> ([u8; NAME_BYTES], usize) {
let mut bytes = [0u8; NAME_BYTES];
for (chunk, w) in bytes.chunks_mut(8).zip(words) {
chunk.copy_from_slice(&(w as u64).to_le_bytes());
}
let len = bytes.iter().position(|b| *b == 0).unwrap_or(NAME_BYTES);
(bytes, len)
}
pub fn chars(&self) -> &[u8] {
&self.bytes[..self.len.min(TEXT_BYTES)]
}
/// A rectangle in two words: `x | y << 16` and `w | h << 16`.
pub fn pack_rect(x: ScreenCoord, y: ScreenCoord, w: Dimension, h: Dimension) -> [usize; 2] {
[
x.raw() as usize | (y.raw() as usize) << 16,
w.raw() as usize | (h.raw() as usize) << 16,
]
}
pub fn words(&self) -> [usize; 4] {
let (lo, hi) = self.bytes.as_chunks::<8>().0.split_at(1);
[
TEXT | self.len << 8 | (self.fg.raw() as usize) << 16,
self.x.raw() as usize
| (self.y.raw() as usize) << 16
| (self.bg.raw() as usize) << 32,
u64::from_le_bytes(lo[0]) as usize,
u64::from_le_bytes(hi[0]) as usize,
]
}
pub fn from_words(w: [usize; 4]) -> Option<Self> {
let len = (w[0] >> 8) & 0xFF;
if w[0] & 0xFF != TEXT || len > TEXT_BYTES {
return None;
}
let mut bytes = [0u8; TEXT_BYTES];
for (b, word) in bytes.as_chunks_mut::<8>().0.iter_mut().zip(&w[2..]) {
*b = (*word as u64).to_le_bytes();
}
Some(Self {
x: ScreenCoord::new((w[1] & 0xFFFF) as u16),
y: ScreenCoord::new(((w[1] >> 16) & 0xFFFF) as u16),
fg: Color::from_raw((w[0] >> 16) as u32),
bg: Color::from_raw((w[1] >> 32) as u32),
bytes,
len,
})
}
pub fn unpack_rect([xy, wh]: [usize; 2]) -> (ScreenCoord, ScreenCoord, Dimension, Dimension) {
let half = |w: usize, shift: u32| ((w >> shift) & 0xFFFF) as u16;
(
ScreenCoord::new(half(xy, 0)),
ScreenCoord::new(half(xy, 16)),
Dimension::new(half(wh, 0)),
Dimension::new(half(wh, 16)),
)
}
#[cfg(test)]
@@ -128,26 +137,18 @@ pub mod text {
use super::*;
#[test]
fn text_round_trips() {
let t = Text::new(
ScreenCoord::new(1272),
ScreenCoord::new(784),
Color::rgb(1, 2, 3),
Color::rgb(4, 5, 6),
b"$ ls -la /home/x",
fn names_and_rects_round_trip() {
let (bytes, len) = unpack_name(pack_name(b"Snake"));
assert_eq!(&bytes[..len], b"Snake");
let (bytes, len) = unpack_name(pack_name(&[b'a'; 40]));
assert_eq!(&bytes[..len], &[b'a'; NAME_BYTES]);
let r = (
ScreenCoord::new(1279),
ScreenCoord::new(783),
Dimension::new(1),
Dimension::new(784),
);
assert_eq!(Text::from_words(t.words()), Some(t));
assert_eq!(Text::new(t.x, t.y, t.fg, t.bg, &[b'a'; 40]).len, TEXT_BYTES);
assert_eq!(unpack_rect(pack_rect(r.0, r.1, r.2, r.3)), r);
}
}
}
/// Input through the display server: an app subscribes with a notification
/// and is then the one keys go to (focus); when it is signalled it polls.
/// A poll's reply is an input event exactly as the input server sends it
/// (tag 0: no event).
pub mod input {
/// Carries the notification to signal; the subscriber takes focus.
pub const SUBSCRIBE: usize = 5;
pub const POLL: usize = 6;
}

View File

@@ -3,3 +3,11 @@
pub const POLL_EVENT: usize = 0;
/// Carries a notification the server signals whenever new events arrive.
pub const SUBSCRIBE: usize = 2;
/// An event as the input server words it: `[tag, kind, extra, 0]`.
pub mod event {
pub const KEY_DOWN: usize = 1;
pub const KEY_UP: usize = 2;
/// A function key: `extra` is its number (F1 is 1).
pub const FUNCTION_KEY: usize = 10;
}

View File

@@ -8,9 +8,6 @@ pub const CLEAR: usize = 11;
/// As `READ`, but what is typed shows as `*` and is forgotten once read:
/// for passcodes.
pub const READ_SECRET: usize = 14;
/// Repaints the whole screen and takes input focus back, after a program
/// that drew on the screen itself.
pub const REDRAW: usize = 12;
/// Carries a notification the terminal signals on Ctrl+C while no line is
/// being read. Only through a copy whose badge has `CONTROL`; the latest
/// one counts, so a restarted shell simply registers again.