feat(pointer): virtio tablet as 2nd input dev (driver per-instance VAs, abs/btn decode), input server serves 2 devices on one notif; display software cursor, bar click switches window, presses/releases to front window in its px; k-ui hit-tests (press focuses, release on same presses, field only focuses)

This commit is contained in:
2026-10-28 16:50:20 +01:00
parent 0de13f124d
commit 1401af3d82
22 changed files with 545 additions and 46 deletions

View File

@@ -0,0 +1,60 @@
//! The pointer's cursor: an arrow the display draws over everything, a
//! dark outline round a light fill so it shows on any theme. Pure: what
//! to draw where.
use port_framebuffer::Framebuffer;
use types::display::{Color, ScreenCoord};
/// The arrow, a row a string: `#` outline, `.` fill, anything else clear.
const ARROW: [&[u8]; 16] = [
b"#",
b"##",
b"#.#",
b"#..#",
b"#...#",
b"#....#",
b"#.....#",
b"#......#",
b"#.......#",
b"#........#",
b"#.....#####",
b"#..#..#",
b"#.# #..#",
b"## #..#",
b"# #..#",
b" ####",
];
pub const WIDTH: u16 = 11;
pub const HEIGHT: u16 = 16;
const OUTLINE: Color = Color::rgb(16, 16, 24);
const FILL: Color = Color::rgb(250, 250, 250);
/// A pointer position from the input server (0 to 65535 of the screen)
/// in pixels of a side `size` long.
pub fn scale(v: u16, size: u16) -> u16 {
((v as u32 * size as u32) >> 16) as u16
}
/// Whether the cursor at (`cx`, `cy`) touches the rectangle from (`x0`,
/// `y0`) up to (`x1`, `y1`).
pub fn touches(cx: u16, cy: u16, x0: u16, y0: u16, x1: u16, y1: u16) -> bool {
cx < x1 && x0 < cx.saturating_add(WIDTH) && cy < y1 && y0 < cy.saturating_add(HEIGHT)
}
/// Draws the arrow with its tip at (`x`, `y`), clipped to the screen.
pub fn draw(fb: &mut impl Framebuffer, x: u16, y: u16) {
let (w, h) = (fb.width().raw(), fb.height().raw());
for (dy, row) in ARROW.iter().enumerate() {
for (dx, c) in row.iter().enumerate() {
let colour = match c {
b'#' => OUTLINE,
b'.' => FILL,
_ => continue,
};
let (px, py) = (x.saturating_add(dx as u16), y.saturating_add(dy as u16));
if px < w && py < h {
fb.write_pixel(ScreenCoord::new(px), ScreenCoord::new(py), colour);
}
}
}
}

View File

@@ -1,6 +1,7 @@
#![no_std]
#![forbid(unsafe_code)]
pub mod cursor;
pub mod host;
pub mod server;
pub mod themes;

View File

@@ -5,7 +5,10 @@
//! 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`. The bar wears the theme in use, which
//! the display also hands every window (see `themes`).
//! the display also hands every window (see `themes`). The pointer is the
//! display's too: it draws the cursor over everything, a press on the bar
//! brings that window to the front, and presses and releases below it go
//! to the window in front, in its own pixels.
use libkos::syscall::Give;
use port_framebuffer::Framebuffer;
@@ -15,6 +18,7 @@ use types::bounded::CapSlot;
use types::display::window::{self as w, NAME_BYTES};
use types::display::{Color, Dimension, ScreenCoord};
use crate::cursor;
use crate::themes::Themes;
use crate::windows::{MAX_WINDOWS, Windows};
@@ -68,6 +72,8 @@ pub struct DisplayServer<F: Framebuffer, H: Host> {
user: [u8; NAME_BYTES],
user_len: usize,
themes: Themes,
/// Where the pointer is on the screen, once it has moved.
cursor: Option<(u16, u16)>,
}
fn colour(c: theme::Rgba) -> Color {
@@ -76,7 +82,7 @@ fn colour(c: theme::Rgba) -> Color {
impl<F: Framebuffer, H: Host> DisplayServer<F, H> {
pub fn new(fb: F, host: H, themes: Themes) -> Self {
let mut s = Self { fb, host, windows: Windows::new(), caps: [None; MAX_WINDOWS], user: [0; NAME_BYTES], user_len: 0, themes };
let mut s = Self { fb, host, windows: Windows::new(), caps: [None; MAX_WINDOWS], user: [0; NAME_BYTES], user_len: 0, themes, cursor: None };
s.repaint();
s
}
@@ -155,6 +161,84 @@ impl<F: Framebuffer, H: Host> DisplayServer<F, H> {
let bytes = self.host.pixels(index, row as usize * ww as usize + x0 as usize, usize::from(x1 - x0));
self.fb.write_rgba_row(ScreenCoord::new(x0), ScreenCoord::new(row + BAR_HEIGHT), bytes);
}
// What was just copied may have covered the cursor.
self.cursor_over(x0, y0 + BAR_HEIGHT, x1, y1 + BAR_HEIGHT);
self.fb.flush();
}
/// Draws the cursor again if it touches what was just drawn (screen
/// pixels from (`x0`, `y0`) up to (`x1`, `y1`)).
fn cursor_over(&mut self, x0: u16, y0: u16, x1: u16, y1: u16) {
if let Some((cx, cy)) = self.cursor.filter(|(cx, cy)| cursor::touches(*cx, *cy, x0, y0, x1, y1)) {
cursor::draw(&mut self.fb, cx, cy);
}
}
/// The bar's labels, left to right: where each starts and ends, and
/// the window it names.
fn bar_labels(&self) -> impl Iterator<Item = (u16, u16, usize)> + '_ {
self.windows.list().scan(0u16, |x, (slot, name, _)| {
let start = *x;
// A space either side, and a gap after.
let end = start.saturating_add((name.len() as u16 + 2) * font::WIDTH);
*x = end.saturating_add(font::WIDTH);
Some((start, end, slot))
})
}
/// The pointer moved, pressed or let go: `event` as the input server
/// words it.
fn pointer(&mut self, event: [usize; 4]) {
use types::input::event::{POINTER_DOWN, unpack_point};
let (px, py) = unpack_point(event[2]);
let (x, y) = (cursor::scale(px, self.fb.width().raw()), cursor::scale(py, self.fb.height().raw()));
if self.cursor != Some((x, y)) {
self.move_cursor(x, y);
}
match event[0] {
types::input::event::POINTER_MOVE => {}
// On the bar: the window named there comes to the front.
_ if y < BAR_HEIGHT => {
if event[0] == POINTER_DOWN {
let hit = self.bar_labels().find(|(x0, x1, _)| (*x0..*x1).contains(&x)).map(|(_, _, slot)| slot);
if hit.is_some_and(|slot| self.windows.raise(slot)) {
self.repaint();
}
}
}
// Below it: the window in front's, in its own pixels.
tag => {
let local = types::input::event::pack_point(x, y - BAR_HEIGHT);
if let Some(notify) = self.windows.push([tag, event[1], local, 0]) {
self.host.signal(notify);
}
}
}
}
/// Takes the cursor from where it was (drawing back what it hid) to
/// (`x`, `y`).
fn move_cursor(&mut self, x: u16, y: u16) {
let old = self.cursor.replace((x, y));
if let Some((ox, oy)) = old {
if oy < BAR_HEIGHT {
self.draw_bar();
}
let top = oy.max(BAR_HEIGHT);
let bottom = oy.saturating_add(cursor::HEIGHT);
match self.windows.front() {
Some(index) if bottom > top => {
let (lx, ly) = (ScreenCoord::new(ox), ScreenCoord::new(top - BAR_HEIGHT));
self.present(index, lx, ly, Dimension::new(cursor::WIDTH), Dimension::new(bottom - top));
}
None if bottom > top => {
let bg = colour(self.themes.active().background);
self.fb.fill_rect(ScreenCoord::new(ox), ScreenCoord::new(top), Dimension::new(cursor::WIDTH), Dimension::new(bottom - top), bg);
}
_ => {}
}
}
cursor::draw(&mut self.fb, x, y);
self.fb.flush();
}
@@ -167,6 +251,7 @@ impl<F: Framebuffer, H: Host> DisplayServer<F, H> {
None => {
let bg = colour(self.themes.active().background);
self.fb.fill_rect(ScreenCoord::new(0), ScreenCoord::new(BAR_HEIGHT), width, height, bg);
self.cursor_over(0, BAR_HEIGHT, width.raw(), BAR_HEIGHT + height.raw());
self.fb.flush();
}
}
@@ -201,6 +286,7 @@ impl<F: Framebuffer, H: Host> DisplayServer<F, H> {
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);
self.cursor_over(0, 0, width.raw(), BAR_HEIGHT);
}
/// What a window key does.
@@ -346,6 +432,10 @@ impl<F: Framebuffer, H: Host> Server for DisplayServer<F, H> {
fn signal(&mut self, _bits: usize) {
let mut told = None;
while let Some(event) = self.host.input() {
if matches!(event[0], types::input::event::POINTER_DOWN | types::input::event::POINTER_MOVE | types::input::event::POINTER_UP) {
self.pointer(event);
continue;
}
if let Some(key) = display_key(event) {
if event[0] == types::input::event::KEY_DOWN {
self.window_key(key);

View File

@@ -298,3 +298,36 @@ fn window_keys_raise_home_and_close_only_apps() {
key(&mut s, w::CLOSE_KEY);
assert!(s.slot_for_test(terminal).is_some());
}
#[test]
fn the_pointer_switches_on_the_bar_and_presses_go_to_the_front_window_in_its_pixels() {
use types::input::event::{POINTER_DOWN, POINTER_MOVE, POINTER_UP, pack_point, unpack_point};
let mut s = server();
let t = open(&mut s, 1, b"T");
let app = open(&mut s, 2, b"A");
let notify = CapSlot::new(9).unwrap();
s.handle_call(app, [w::SUBSCRIBE, 0, 0, 0], Some(notify));
s.handle_call(t, [w::SUBSCRIBE, 0, 0, 0], Some(CapSlot::new(10).unwrap()));
// The middle of pixel (x, y), as the input server words a position.
let at = |x: usize, y: usize| pack_point(((2 * x + 1) * 32768 / W) as u16, ((2 * y + 1) * 32768 / H) as u16);
// Moves are the display's: nothing reaches the window.
s.host_mut_for_test().events = vec![[POINTER_MOVE, 0, at(12, 20), 0]];
s.signal(0);
assert_eq!(s.handle_call(app, [w::POLL, 0, 0, 0], None).0[0], 0);
assert_ne!(shown_at(&s, 12, 20), Some(Color::from_raw(0)), "the cursor is drawn where the pointer is");
// A press and release below the bar: the window in front's, in its pixels.
s.host_mut_for_test().events = vec![[POINTER_DOWN, 0, at(12, 20), 0], [POINTER_UP, 0, at(12, 20), 0]];
s.signal(0);
let down = s.handle_call(app, [w::POLL, 0, 0, 0], None).0;
assert_eq!(down[0], POINTER_DOWN);
assert_eq!(unpack_point(down[2]), (12, 20 - BAR_HEIGHT));
assert_eq!(s.handle_call(app, [w::POLL, 0, 0, 0], None).0[0], POINTER_UP);
// A press on the bar, on the first name: that window comes forward.
s.host_mut_for_test().events = vec![[POINTER_DOWN, 0, at(2, 2), 0]];
s.signal(0);
assert_eq!(s.windows_front_for_test(), s.slot_for_test(t));
}
fn shown_at(s: &Srv, x: usize, y: usize) -> Option<Color> {
s.framebuffer().pixel(x, y)
}