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

@@ -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 {