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

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