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:
60
crates/servers/display-server/src/cursor.rs
Normal file
60
crates/servers/display-server/src/cursor.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
#![no_std]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
pub mod cursor;
|
||||
pub mod host;
|
||||
pub mod server;
|
||||
pub mod themes;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -34,9 +34,9 @@ impl Decode for InputRequest {
|
||||
|
||||
const EV_KEY_DOWN: usize = 1;
|
||||
const EV_KEY_UP: usize = 2;
|
||||
const EV_PTR_DOWN: usize = 3;
|
||||
const EV_PTR_MOVE: usize = 4;
|
||||
const EV_PTR_UP: usize = 5;
|
||||
const EV_PTR_DOWN: usize = types::input::event::POINTER_DOWN;
|
||||
const EV_PTR_MOVE: usize = types::input::event::POINTER_MOVE;
|
||||
const EV_PTR_UP: usize = types::input::event::POINTER_UP;
|
||||
const EV_SCROLL: usize = 6;
|
||||
const EV_GAMEPAD_BTN: usize = 7;
|
||||
const EV_GAMEPAD_AXIS: usize = 8;
|
||||
|
||||
@@ -4,13 +4,39 @@ use port_input_device::{InputDevice, InputEvent};
|
||||
use types::bounded::CapSlot;
|
||||
use crate::protocol::{InputRequest, InputResponse};
|
||||
|
||||
/// Events captured on interrupts and not yet asked for.
|
||||
const BUFFERED: usize = 32;
|
||||
/// Events captured on interrupts and not yet asked for (a tablet sends
|
||||
/// many).
|
||||
pub const BUFFERED: usize = 64;
|
||||
/// Devices served at once (a keyboard and a tablet).
|
||||
pub const MAX_DEVICES: usize = 2;
|
||||
|
||||
/// Two devices as one: events from either, the first's first.
|
||||
pub struct Pair<A, B> {
|
||||
pub first: A,
|
||||
pub second: Option<B>,
|
||||
}
|
||||
|
||||
impl<A: InputDevice, B: InputDevice> InputDevice for Pair<A, B> {
|
||||
fn poll(&mut self) -> Option<InputEvent> {
|
||||
self.first.poll().or_else(|| self.second.as_mut()?.poll())
|
||||
}
|
||||
|
||||
fn has_event(&self) -> bool {
|
||||
self.first.has_event() || self.second.as_ref().is_some_and(|d| d.has_event())
|
||||
}
|
||||
|
||||
fn acknowledge(&mut self) {
|
||||
self.first.acknowledge();
|
||||
if let Some(d) = self.second.as_mut() {
|
||||
d.acknowledge();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InputServer<D: InputDevice> {
|
||||
device: D,
|
||||
/// The IRQ to re-arm after each interrupt, if the device has one.
|
||||
irq: Option<CapSlot>,
|
||||
/// The IRQs to re-arm after each interrupt, those the devices have.
|
||||
irqs: [Option<CapSlot>; MAX_DEVICES],
|
||||
events: [Option<InputEvent>; BUFFERED],
|
||||
head: usize,
|
||||
len: usize,
|
||||
@@ -21,14 +47,14 @@ pub struct InputServer<D: InputDevice> {
|
||||
impl<D: InputDevice> InputServer<D> {
|
||||
/// A server that reads the device only when a client asks.
|
||||
pub fn new(device: D) -> Self {
|
||||
Self { device, irq: None, events: [None; BUFFERED], head: 0, len: 0, subscriber: None }
|
||||
Self { device, irqs: [None; MAX_DEVICES], events: [None; BUFFERED], head: 0, len: 0, subscriber: None }
|
||||
}
|
||||
|
||||
/// A server woken by `irq` (routed and bound by the caller): it drains
|
||||
/// the device into a buffer on every interrupt, so events that arrive
|
||||
/// between requests are kept.
|
||||
pub fn with_irq(device: D, irq: CapSlot) -> Self {
|
||||
Self { irq: Some(irq), ..Self::new(device) }
|
||||
/// A server woken by `irqs` (routed and bound by the caller): it
|
||||
/// drains the devices into a buffer on every interrupt, so events that
|
||||
/// arrive between requests are kept.
|
||||
pub fn with_irqs(device: D, irqs: [Option<CapSlot>; MAX_DEVICES]) -> Self {
|
||||
Self { irqs, ..Self::new(device) }
|
||||
}
|
||||
|
||||
fn push(&mut self, event: InputEvent) {
|
||||
@@ -109,7 +135,7 @@ impl<D: InputDevice> Server for InputServer<D> {
|
||||
let _ = libkos::syscall::signal(subscriber);
|
||||
}
|
||||
self.device.acknowledge();
|
||||
if let Some(irq) = self.irq {
|
||||
for irq in self.irqs.into_iter().flatten() {
|
||||
let _ = libkos::syscall::ack_interrupt(irq);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ fn an_interrupt_buffers_events_until_asked() {
|
||||
let mut device = MockInputDevice::new();
|
||||
device.push_event(InputEvent::KeyDown(Key::Up));
|
||||
device.push_event(InputEvent::KeyDown(Key::Down));
|
||||
let mut server = InputServer::with_irq(device, types::bounded::CapSlot::new(1).unwrap());
|
||||
let mut server = InputServer::with_irqs(device, [types::bounded::CapSlot::new(1), None]);
|
||||
server.signal(1);
|
||||
// The device itself is now drained; the events wait in the server.
|
||||
assert!(!port_input_device::InputDevice::has_event(server.device()));
|
||||
@@ -72,9 +72,10 @@ fn an_interrupt_buffers_events_until_asked() {
|
||||
|
||||
#[test]
|
||||
fn a_full_buffer_drops_the_oldest_event() {
|
||||
let mut server = InputServer::with_irq(MockInputDevice::new(), types::bounded::CapSlot::new(1).unwrap());
|
||||
// 40 interrupts, one key each; the buffer keeps the last 32.
|
||||
for i in 0..40u8 {
|
||||
let mut server = InputServer::with_irqs(MockInputDevice::new(), [types::bounded::CapSlot::new(1), None]);
|
||||
// More interrupts than the buffer holds, one key each: it keeps the last.
|
||||
let extra = 8;
|
||||
for i in 0..(crate::server::BUFFERED + extra) as u8 {
|
||||
let mut device = MockInputDevice::new();
|
||||
device.push_event(InputEvent::KeyDown(Key::Char(i)));
|
||||
server.replace_device_for_test(device);
|
||||
@@ -82,7 +83,7 @@ fn a_full_buffer_drops_the_oldest_event() {
|
||||
}
|
||||
assert_eq!(
|
||||
server.handle(InputRequest::PollEvent),
|
||||
InputResponse::Event(InputEvent::KeyDown(Key::Char(8)))
|
||||
InputResponse::Event(InputEvent::KeyDown(Key::Char(extra as u8)))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user