From 64ec5df379ee0fb011ba7e07cd0da1aa217ccd49 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Tue, 27 Oct 2026 16:33:26 +0100 Subject: [PATCH] feat(display): DrawText with built-in Spleen 8x16 font (BSD-2) --- crates/libkos/src/display.rs | 13 ++ .../display-server/src/font/LICENSE-spleen | 24 ++++ crates/servers/display-server/src/font/mod.rs | 114 ++++++++++++++++++ crates/servers/display-server/src/lib.rs | 1 + crates/servers/display-server/src/protocol.rs | 5 +- crates/servers/display-server/src/server.rs | 31 ++++- .../display-server/src/tests/server.rs | 29 +++++ crates/types/src/display.rs | 75 ++++++++++++ 8 files changed, 290 insertions(+), 2 deletions(-) create mode 100644 crates/servers/display-server/src/font/LICENSE-spleen create mode 100644 crates/servers/display-server/src/font/mod.rs diff --git a/crates/libkos/src/display.rs b/crates/libkos/src/display.rs index 555029f..464e600 100644 --- a/crates/libkos/src/display.rs +++ b/crates/libkos/src/display.rs @@ -37,3 +37,16 @@ pub fn get_dimensions(ep: CapSlot) -> (Dimension, Dimension) { (Dimension::new(0), Dimension::new(0)) } } + +/// 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()); + } +} + +/// The display server's character cell, in pixels. +pub const CELL_WIDTH: usize = 8; +pub const CELL_HEIGHT: usize = 16; diff --git a/crates/servers/display-server/src/font/LICENSE-spleen b/crates/servers/display-server/src/font/LICENSE-spleen new file mode 100644 index 0000000..4387948 --- /dev/null +++ b/crates/servers/display-server/src/font/LICENSE-spleen @@ -0,0 +1,24 @@ +Copyright (c) 2018-2024, Frederic Cambus +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/crates/servers/display-server/src/font/mod.rs b/crates/servers/display-server/src/font/mod.rs new file mode 100644 index 0000000..82ad163 --- /dev/null +++ b/crates/servers/display-server/src/font/mod.rs @@ -0,0 +1,114 @@ +//! The display server's one font: Spleen 8x16 (BSD-2-Clause, Frederic +//! Cambus; see LICENSE-spleen), printable ASCII only. Generated from +//! spleen-8x16.bdf; each glyph is 16 rows, bit 7 the leftmost pixel. + +pub const WIDTH: u16 = 8; +pub const HEIGHT: u16 = 16; + +const FIRST: u8 = b' '; +const LAST: u8 = b'~'; + +/// The glyph for `c`; anything unprintable shows as `?`. +pub fn glyph(c: u8) -> &'static [u8; 16] { + let c = if (FIRST..=LAST).contains(&c) { c } else { b'?' }; + &GLYPHS[usize::from(c - FIRST)] +} + +#[rustfmt::skip] +static GLYPHS: [[u8; 16]; 95] = [ + [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // ' ' + [0x00, 0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00], // '!' + [0x00, 0x66, 0x66, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // '"' + [0x00, 0x00, 0x6c, 0x6c, 0xfe, 0x6c, 0x6c, 0x6c, 0x6c, 0xfe, 0x6c, 0x6c, 0x00, 0x00, 0x00, 0x00], // '#' + [0x00, 0x10, 0x7e, 0xd0, 0xd0, 0xd0, 0x7c, 0x16, 0x16, 0x16, 0x16, 0xfc, 0x10, 0x00, 0x00, 0x00], // '$' + [0x00, 0x00, 0x06, 0x66, 0x6c, 0x0c, 0x18, 0x18, 0x30, 0x36, 0x66, 0x60, 0x00, 0x00, 0x00, 0x00], // '%' + [0x00, 0x00, 0x38, 0x6c, 0x6c, 0x6c, 0x38, 0x70, 0xda, 0xcc, 0xcc, 0x7a, 0x00, 0x00, 0x00, 0x00], // '&' + [0x00, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // "'" + [0x00, 0x0e, 0x18, 0x30, 0x30, 0x60, 0x60, 0x60, 0x60, 0x30, 0x30, 0x18, 0x0e, 0x00, 0x00, 0x00], // '(' + [0x00, 0x70, 0x18, 0x0c, 0x0c, 0x06, 0x06, 0x06, 0x06, 0x0c, 0x0c, 0x18, 0x70, 0x00, 0x00, 0x00], // ')' + [0x00, 0x00, 0x00, 0x00, 0x66, 0x3c, 0x18, 0xff, 0x18, 0x3c, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00], // '*' + [0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x7e, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // '+' + [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x30, 0x00, 0x00, 0x00], // ',' + [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // '-' + [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00], // '.' + [0x00, 0x06, 0x06, 0x0c, 0x0c, 0x18, 0x18, 0x30, 0x30, 0x60, 0x60, 0xc0, 0xc0, 0x00, 0x00, 0x00], // '/' + [0x00, 0x00, 0x7c, 0xc6, 0xc6, 0xce, 0xde, 0xf6, 0xe6, 0xc6, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00], // '0' + [0x00, 0x00, 0x18, 0x38, 0x78, 0x58, 0x18, 0x18, 0x18, 0x18, 0x18, 0x7e, 0x00, 0x00, 0x00, 0x00], // '1' + [0x00, 0x00, 0x7c, 0xc6, 0x06, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xc6, 0xfe, 0x00, 0x00, 0x00, 0x00], // '2' + [0x00, 0x00, 0x7c, 0xc6, 0x06, 0x06, 0x3c, 0x06, 0x06, 0x06, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00], // '3' + [0x00, 0x00, 0xc0, 0xc0, 0xcc, 0xcc, 0xcc, 0xcc, 0xfe, 0x0c, 0x0c, 0x0c, 0x00, 0x00, 0x00, 0x00], // '4' + [0x00, 0x00, 0xfe, 0xc6, 0xc0, 0xc0, 0xfc, 0x06, 0x06, 0x06, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00], // '5' + [0x00, 0x00, 0x7c, 0xc6, 0xc0, 0xc0, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00], // '6' + [0x00, 0x00, 0xfe, 0xc6, 0x06, 0x06, 0x0c, 0x18, 0x30, 0x30, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00], // '7' + [0x00, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00], // '8' + [0x00, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x06, 0x06, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00], // '9' + [0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00], // ':' + [0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x18, 0x18, 0x30, 0x00, 0x00, 0x00], // ';' + [0x00, 0x00, 0x06, 0x0c, 0x18, 0x30, 0x60, 0x60, 0x30, 0x18, 0x0c, 0x06, 0x00, 0x00, 0x00, 0x00], // '<' + [0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // '=' + [0x00, 0x00, 0x60, 0x30, 0x18, 0x0c, 0x06, 0x06, 0x0c, 0x18, 0x30, 0x60, 0x00, 0x00, 0x00, 0x00], // '>' + [0x00, 0x00, 0x7c, 0xc6, 0x06, 0x0c, 0x18, 0x30, 0x30, 0x00, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00], // '?' + [0x00, 0x00, 0x00, 0x7c, 0xc2, 0xda, 0xda, 0xda, 0xda, 0xde, 0xc0, 0x7c, 0x00, 0x00, 0x00, 0x00], // '@' + [0x00, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xfe, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // 'A' + [0x00, 0x00, 0xfc, 0xc6, 0xc6, 0xc6, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0xfc, 0x00, 0x00, 0x00, 0x00], // 'B' + [0x00, 0x00, 0x7e, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0x7e, 0x00, 0x00, 0x00, 0x00], // 'C' + [0x00, 0x00, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xfc, 0x00, 0x00, 0x00, 0x00], // 'D' + [0x00, 0x00, 0x7e, 0xc0, 0xc0, 0xc0, 0xf8, 0xc0, 0xc0, 0xc0, 0xc0, 0x7e, 0x00, 0x00, 0x00, 0x00], // 'E' + [0x00, 0x00, 0x7e, 0xc0, 0xc0, 0xc0, 0xf8, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0x00, 0x00, 0x00, 0x00], // 'F' + [0x00, 0x00, 0x7e, 0xc0, 0xc0, 0xc0, 0xde, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x00, 0x00, 0x00, 0x00], // 'G' + [0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xfe, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // 'H' + [0x00, 0x00, 0x7e, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x7e, 0x00, 0x00, 0x00, 0x00], // 'I' + [0x00, 0x00, 0x7e, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xf0, 0x00, 0x00, 0x00, 0x00], // 'J' + [0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xcc, 0xf8, 0xcc, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // 'K' + [0x00, 0x00, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0x7e, 0x00, 0x00, 0x00, 0x00], // 'L' + [0x00, 0x00, 0xc6, 0xee, 0xfe, 0xd6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // 'M' + [0x00, 0x00, 0xc6, 0xc6, 0xe6, 0xe6, 0xd6, 0xd6, 0xce, 0xce, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // 'N' + [0x00, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00], // 'O' + [0x00, 0x00, 0xfc, 0xc6, 0xc6, 0xc6, 0xfc, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0x00, 0x00, 0x00, 0x00], // 'P' + [0x00, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xd6, 0xd6, 0x7c, 0x18, 0x0c, 0x00, 0x00], // 'Q' + [0x00, 0x00, 0xfc, 0xc6, 0xc6, 0xc6, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // 'R' + [0x00, 0x00, 0x7e, 0xc0, 0xc0, 0xc0, 0x7c, 0x06, 0x06, 0x06, 0x06, 0xfc, 0x00, 0x00, 0x00, 0x00], // 'S' + [0x00, 0x00, 0xff, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00], // 'T' + [0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x00, 0x00, 0x00, 0x00], // 'U' + [0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x6c, 0x38, 0x10, 0x00, 0x00, 0x00, 0x00], // 'V' + [0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xd6, 0xfe, 0xee, 0xc6, 0x00, 0x00, 0x00, 0x00], // 'W' + [0x00, 0x00, 0xc6, 0xc6, 0xc6, 0x6c, 0x38, 0x6c, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // 'X' + [0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x06, 0x06, 0x06, 0x06, 0xfc, 0x00, 0x00, 0x00, 0x00], // 'Y' + [0x00, 0x00, 0xfe, 0x06, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xc0, 0xc0, 0xfe, 0x00, 0x00, 0x00, 0x00], // 'Z' + [0x00, 0x3e, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x3e, 0x00, 0x00, 0x00], // '[' + [0x00, 0xc0, 0xc0, 0x60, 0x60, 0x30, 0x30, 0x18, 0x18, 0x0c, 0x0c, 0x06, 0x06, 0x00, 0x00, 0x00], // '\\' + [0x00, 0x7c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x7c, 0x00, 0x00, 0x00], // ']' + [0x00, 0x10, 0x38, 0x6c, 0xc6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // '^' + [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0x00], // '_' + [0x00, 0x30, 0x18, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // '`' + [0x00, 0x00, 0x00, 0x00, 0x00, 0x7c, 0x06, 0x7e, 0xc6, 0xc6, 0xc6, 0x7e, 0x00, 0x00, 0x00, 0x00], // 'a' + [0x00, 0x00, 0xc0, 0xc0, 0xc0, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xfc, 0x00, 0x00, 0x00, 0x00], // 'b' + [0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0x7e, 0x00, 0x00, 0x00, 0x00], // 'c' + [0x00, 0x00, 0x06, 0x06, 0x06, 0x7e, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x00, 0x00, 0x00, 0x00], // 'd' + [0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0xc6, 0xc6, 0xfe, 0xc0, 0xc0, 0x7e, 0x00, 0x00, 0x00, 0x00], // 'e' + [0x00, 0x00, 0x1e, 0x30, 0x30, 0x30, 0x7c, 0x30, 0x30, 0x30, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00], // 'f' + [0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x06, 0x06, 0xfc, 0x00], // 'g' + [0x00, 0x00, 0xc0, 0xc0, 0xc0, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // 'h' + [0x00, 0x00, 0x18, 0x18, 0x00, 0x38, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1c, 0x00, 0x00, 0x00, 0x00], // 'i' + [0x00, 0x00, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x70, 0x00], // 'j' + [0x00, 0x00, 0xc0, 0xc0, 0xc0, 0xcc, 0xd8, 0xf0, 0xf0, 0xd8, 0xcc, 0xc6, 0x00, 0x00, 0x00, 0x00], // 'k' + [0x00, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x1c, 0x00, 0x00, 0x00, 0x00], // 'l' + [0x00, 0x00, 0x00, 0x00, 0x00, 0xec, 0xd6, 0xd6, 0xd6, 0xd6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // 'm' + [0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // 'n' + [0x00, 0x00, 0x00, 0x00, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00], // 'o' + [0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xfc, 0xc0, 0xc0, 0xc0, 0x00], // 'p' + [0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x06, 0x06, 0x06, 0x00], // 'q' + [0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0xc6, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0x00, 0x00, 0x00, 0x00], // 'r' + [0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0xc0, 0xc0, 0x7c, 0x06, 0x06, 0xfc, 0x00, 0x00, 0x00, 0x00], // 's' + [0x00, 0x00, 0x30, 0x30, 0x30, 0x7c, 0x30, 0x30, 0x30, 0x30, 0x30, 0x1e, 0x00, 0x00, 0x00, 0x00], // 't' + [0x00, 0x00, 0x00, 0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x00, 0x00, 0x00, 0x00], // 'u' + [0x00, 0x00, 0x00, 0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0x6c, 0x38, 0x10, 0x00, 0x00, 0x00, 0x00], // 'v' + [0x00, 0x00, 0x00, 0x00, 0x00, 0xc6, 0xc6, 0xd6, 0xd6, 0xd6, 0xd6, 0x6e, 0x00, 0x00, 0x00, 0x00], // 'w' + [0x00, 0x00, 0x00, 0x00, 0x00, 0xc6, 0x6c, 0x38, 0x38, 0x6c, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00], // 'x' + [0x00, 0x00, 0x00, 0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x06, 0x06, 0xfc, 0x00], // 'y' + [0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xfe, 0x00, 0x00, 0x00, 0x00], // 'z' + [0x00, 0x0e, 0x18, 0x18, 0x18, 0x18, 0x70, 0x70, 0x18, 0x18, 0x18, 0x18, 0x0e, 0x00, 0x00, 0x00], // '{' + [0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00], // '|' + [0x00, 0x70, 0x18, 0x18, 0x18, 0x18, 0x0e, 0x0e, 0x18, 0x18, 0x18, 0x18, 0x70, 0x00, 0x00, 0x00], // '}' + [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0x7e, 0x4c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // '~' +]; diff --git a/crates/servers/display-server/src/lib.rs b/crates/servers/display-server/src/lib.rs index 08873ca..51c2760 100644 --- a/crates/servers/display-server/src/lib.rs +++ b/crates/servers/display-server/src/lib.rs @@ -1,5 +1,6 @@ #![no_std] +pub mod font; pub mod protocol; pub mod server; diff --git a/crates/servers/display-server/src/protocol.rs b/crates/servers/display-server/src/protocol.rs index 700715d..84ea462 100644 --- a/crates/servers/display-server/src/protocol.rs +++ b/crates/servers/display-server/src/protocol.rs @@ -1,4 +1,5 @@ use server_framework::{Decode, Encode}; +use types::display::text::Text; use types::display::{Color, Dimension, ScreenCoord}; #[derive(Debug, PartialEq)] @@ -7,6 +8,7 @@ pub enum DisplayRequest { Clear { color: Color }, Flush, GetDimensions, + DrawText(Text), } #[derive(Debug, PartialEq)] @@ -27,6 +29,7 @@ impl Encode for DisplayRequest { 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(), } } } @@ -45,7 +48,7 @@ impl Decode for DisplayRequest { 1 => Some(DisplayRequest::Clear { color: Color::from_raw(words[1] as u32) }), 2 => Some(DisplayRequest::Flush), 3 => Some(DisplayRequest::GetDimensions), - _ => None, + _ => Text::from_words(words).map(DisplayRequest::DrawText), } } } diff --git a/crates/servers/display-server/src/server.rs b/crates/servers/display-server/src/server.rs index 72deecb..595afe4 100644 --- a/crates/servers/display-server/src/server.rs +++ b/crates/servers/display-server/src/server.rs @@ -2,7 +2,10 @@ use server_framework::Server; use crate::protocol::{DisplayRequest, DisplayResponse}; use port_framebuffer::Framebuffer; -use types::display::Dimension; +use types::display::text::Text; +use types::display::{Dimension, ScreenCoord}; + +use crate::font; pub struct DisplayServer { fb: F, @@ -16,6 +19,28 @@ impl DisplayServer { 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 { + return; + } + for (i, c) in t.chars().iter().enumerate() { + let Some(cx) = x0.checked_add(i as u16 * font::WIDTH).filter(|cx| cx + font::WIDTH <= fw) else { + return; + }; + for (row, bits) in font::glyph(*c).iter().enumerate() { + for col in 0..font::WIDTH { + let on = bits & (0x80 >> col) != 0; + let (x, y) = (ScreenCoord::new(cx + col), ScreenCoord::new(y0 + row as u16)); + self.fb.write_pixel(x, y, if on { t.fg } else { t.bg }); + } + } + } + } } impl Server for DisplayServer { @@ -44,6 +69,10 @@ impl Server for DisplayServer { self.fb.flush(); DisplayResponse::Ok } + DisplayRequest::DrawText(t) => { + self.draw_text(&t); + DisplayResponse::Ok + } DisplayRequest::GetDimensions => DisplayResponse::Dimensions { width: self.fb.width(), height: self.fb.height(), diff --git a/crates/servers/display-server/src/tests/server.rs b/crates/servers/display-server/src/tests/server.rs index 6e6e4de..06189ee 100644 --- a/crates/servers/display-server/src/tests/server.rs +++ b/crates/servers/display-server/src/tests/server.rs @@ -138,3 +138,32 @@ fn an_off_screen_rect_draws_nothing() { } assert_eq!(server.framebuffer().pixel_count(), 0); } + +#[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)); +} + +#[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); +} diff --git a/crates/types/src/display.rs b/crates/types/src/display.rs index f135d25..f3f5171 100644 --- a/crates/types/src/display.rs +++ b/crates/types/src/display.rs @@ -48,3 +48,78 @@ impl Dimension { self.0 } } + +/// `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]`. +pub mod text { + use super::{Color, ScreenCoord}; + + pub const TEXT: usize = 4; + pub const TEXT_BYTES: usize = 16; + + #[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, + } + + 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 } + } + + pub fn chars(&self) -> &[u8] { + &self.bytes[..self.len.min(TEXT_BYTES)] + } + + 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 { + 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, + }) + } + } + + #[cfg(test)] + mod tests { + 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"); + 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); + } + } +}