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

@@ -4,6 +4,8 @@
extern crate libkos;
use driver_input_virtio::VirtioInputDevice;
use input_server::server::{InputServer, MAX_DEVICES, Pair};
use libkos::device::{self, KIND_UART_16550, KIND_VIRTIO};
use port_input_device::InputDevice;
use types::bounded::CapSlot;
@@ -12,17 +14,24 @@ libkos::entry!(main);
fn main() {
let ep = CapSlot::new(0).unwrap();
// Init hands over the keyboard (virtio) or, failing that, the UART.
let dev = device::receive_device(ep);
match (dev.kind, dev.mmio) {
// Init hands over the input devices, two (a keyboard, a tablet), each
// virtio or, failing a keyboard, the UART; either may be none.
let first = device::receive_device(ep);
let second = device::receive_device(ep);
let irqs = [first.irq, second.irq];
let tablet = match (second.kind, second.mmio) {
(KIND_VIRTIO, Some(mmio)) => VirtioInputDevice::try_new(mmio, 1),
_ => None,
};
match (first.kind, first.mmio) {
(KIND_VIRTIO, Some(mmio)) => {
if let Some(d) = driver_input_virtio::VirtioInputDevice::try_new(mmio) {
run(d, dev.irq, ep);
if let Some(d) = VirtioInputDevice::try_new(mmio, 0) {
run(Pair { first: d, second: tablet }, irqs, ep);
}
}
(KIND_UART_16550, Some(mmio)) => {
if let Some(d) = driver_input_uart::UartInputDevice::new(mmio) {
run(d, dev.irq, ep);
run(Pair { first: d, second: tablet }, irqs, ep);
}
}
_ => {}
@@ -31,12 +40,18 @@ fn main() {
libkos::syscall::exit();
}
/// Serves `device`, woken by its interrupt when it has one.
/// Serves `device`, woken by its interrupts when it has them.
#[inline(never)]
fn run<D: InputDevice>(device: D, irq: Option<CapSlot>, ep: CapSlot) -> ! {
let mut srv = match irq.filter(|irq| device::bind_irq(*irq).is_ok()) {
Some(irq) => input_server::server::InputServer::with_irq(device, irq),
None => input_server::server::InputServer::new(device),
fn run<D: InputDevice>(device: D, irqs: [Option<CapSlot>; MAX_DEVICES], ep: CapSlot) -> ! {
let mut lines = [CapSlot::const_new(0); MAX_DEVICES];
let mut n = 0;
for irq in irqs.into_iter().flatten() {
lines[n] = irq;
n += 1;
}
let mut srv = match n > 0 && device::bind_irqs(&lines[..n]).is_ok() {
true => InputServer::with_irqs(device, irqs),
false => InputServer::new(device),
};
server_framework::serve(&mut srv, ep)
}

View File

@@ -2,6 +2,7 @@
#![forbid(unsafe_code)]
mod keymap;
mod pointer;
use core::sync::atomic::{Ordering, fence};
use hw_virtio::mmio::{MmioAccess, Registers};
@@ -11,10 +12,16 @@ use types::address::{ByteOffset, PhysicalAddress, VirtualAddress};
use types::bounded::PageCount;
/// Drivers map their devices in the device window (0x01D0_0000 up, 64 KiB
/// each), clear of program images and heaps.
const INPUT_MMIO_VA: VirtualAddress = VirtualAddress::from_raw(0x01D1_0000);
const VQUEUE_VA: VirtualAddress = VirtualAddress::from_raw(0x01D1_1000);
const EVBUF_VA: VirtualAddress = VirtualAddress::from_raw(0x01D1_3000);
/// each), clear of program images and heaps; an input device takes 16 KiB
/// of it, the `n`th from here (a keyboard, a tablet).
const INPUT_VA: usize = 0x01D1_0000;
const INPUT_SPAN: usize = 0x4000;
/// Its registers, its queue's rings (two pages), its event buffers.
const MMIO_AT: usize = 0;
const VQUEUE_AT: usize = 0x1000;
const EVBUF_AT: usize = 0x3000;
/// How many may be driven at once in one program.
pub const MAX_INPUTS: usize = 4;
const QUEUE_SIZE: usize = 32;
const EVENT_SIZE: usize = 8;
@@ -37,20 +44,27 @@ pub struct VirtioInputDevice {
shift: bool,
ctrl: bool,
alt: bool,
/// Where a tablet's pointer is, between its reports.
pointer: pointer::Pointer,
}
impl VirtioInputDevice {
/// Takes over the virtio input device behind `mmio`. `None` if it is
/// not one or any step fails.
pub fn try_new(mmio: types::bounded::CapSlot) -> Option<Self> {
let regs = Registers::new(libkos::device::map_mmio(mmio, INPUT_MMIO_VA).ok()?);
/// Takes over the virtio input device behind `mmio`, as this program's
/// `n`th (each has addresses of its own). `None` if it is not one or
/// any step fails.
pub fn try_new(mmio: types::bounded::CapSlot, n: usize) -> Option<Self> {
if n >= MAX_INPUTS {
return None;
}
let at = |offset: usize| VirtualAddress::from_raw(INPUT_VA + n * INPUT_SPAN + offset);
let regs = Registers::new(libkos::device::map_mmio(mmio, at(MMIO_AT)).ok()?);
if regs.read(hw_virtio::mmio::REG_MAGIC) != hw_virtio::mmio::MAGIC
|| regs.read(hw_virtio::mmio::REG_DEVICE_ID) != hw_virtio::mmio::DEVICE_ID_INPUT
{
return None;
}
let rings = libkos::device::dma(PageCount::new(2)?, VQUEUE_VA).ok()?;
let events = libkos::device::dma(PageCount::new(1)?, EVBUF_VA).ok()?;
let rings = libkos::device::dma(PageCount::new(2)?, at(VQUEUE_AT)).ok()?;
let events = libkos::device::dma(PageCount::new(1)?, at(EVBUF_AT)).ok()?;
hw_virtio::mmio::init_legacy_device(&regs, rings.physical, QUEUE_SIZE as u32);
let mut dev = Self {
@@ -63,6 +77,7 @@ impl VirtioInputDevice {
shift: false,
ctrl: false,
alt: false,
pointer: pointer::Pointer::default(),
};
dev.fill_queue();
Some(dev)
@@ -123,6 +138,9 @@ impl InputDevice for VirtioInputDevice {
let (ev_type, ev_code, ev_value) = self.read_event(desc_idx);
self.resubmit(desc_idx);
if let Some(event) = self.pointer.event(ev_type, ev_code, ev_value) {
return Some(event);
}
if ev_type == EV_KEY && matches!(ev_code, keymap::LEFT_SHIFT | keymap::RIGHT_SHIFT) {
self.shift = ev_value != 0;
continue;

View File

@@ -0,0 +1,79 @@
//! A tablet's events (Linux codes, as virtio-input reports them) to
//! pointer events: absolute axes collect until the report's sync, then a
//! move; buttons press and let go where the pointer is. Positions are
//! fractions of the screen, 0 to 65535 each way.
use port_input_device::InputEvent;
pub const EV_SYN: u16 = 0;
pub const EV_ABS: u16 = 3;
const ABS_X: u16 = 0;
const ABS_Y: u16 = 1;
const BTN_LEFT: u16 = 0x110;
const BTN_RIGHT: u16 = 0x111;
/// QEMU's tablet reports 0 to this on each axis.
const ABS_MAX: u32 = 0x7fff;
/// Button ids in pointer events.
pub const LEFT: u8 = 0;
pub const RIGHT: u8 = 1;
#[derive(Default)]
pub struct Pointer {
x: u16,
y: u16,
/// An axis changed since the last sync.
moved: bool,
}
fn scaled(v: u32) -> u16 {
(v.min(ABS_MAX) * 0xffff / ABS_MAX) as u16
}
impl Pointer {
/// What one event of `kind`, `code`, `value` means, if anything yet.
pub fn event(&mut self, kind: u16, code: u16, value: u32) -> Option<InputEvent> {
let (x, y) = (self.x, self.y);
match (kind, code) {
(EV_ABS, ABS_X) => {
self.x = scaled(value);
self.moved = true;
None
}
(EV_ABS, ABS_Y) => {
self.y = scaled(value);
self.moved = true;
None
}
(EV_SYN, _) if self.moved => {
self.moved = false;
Some(InputEvent::PointerMove { id: LEFT, x, y })
}
(super::EV_KEY, BTN_LEFT | BTN_RIGHT) => {
let id = if code == BTN_LEFT { LEFT } else { RIGHT };
match value {
0 => Some(InputEvent::PointerUp { id, x, y }),
1 => Some(InputEvent::PointerDown { id, x, y }),
_ => None,
}
}
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn axes_move_on_sync_and_buttons_press_where_it_is() {
let mut p = Pointer::default();
assert_eq!(p.event(EV_ABS, ABS_X, 0x7fff), None);
assert_eq!(p.event(EV_ABS, ABS_Y, 0x4000), None);
assert_eq!(p.event(EV_SYN, 0, 0), Some(InputEvent::PointerMove { id: LEFT, x: 0xffff, y: 0x8000 }));
assert_eq!(p.event(EV_SYN, 0, 0), None, "nothing moved since");
assert_eq!(p.event(super::super::EV_KEY, BTN_LEFT, 1), Some(InputEvent::PointerDown { id: LEFT, x: 0xffff, y: 0x8000 }));
assert_eq!(p.event(super::super::EV_KEY, BTN_RIGHT, 0), Some(InputEvent::PointerUp { id: RIGHT, x: 0xffff, y: 0x8000 }));
}
}

View File

@@ -20,6 +20,8 @@ pub struct Devices {
pub disk: Device,
pub gpu: Device,
pub input: Device,
/// A second input device: a tablet (the pointer), if the board has one.
pub pointer: Device,
/// The gatekeeper's: a small disk of its own for the key slots.
pub keystore: Device,
pub random: Device,
@@ -94,6 +96,7 @@ pub fn discover() -> Devices {
disk: Device::NONE,
gpu: Device::NONE,
input: Device::NONE,
pointer: Device::NONE,
keystore: Device::NONE,
random: Device::NONE,
};
@@ -109,6 +112,8 @@ pub fn discover() -> Devices {
(DEVICE_ID_BLOCK, size) if size <= KEYSTORE_MAX_SECTORS => &mut found.keystore,
(DEVICE_ID_BLOCK, _) => &mut found.disk,
(DEVICE_ID_GPU, _) => &mut found.gpu,
// The first is the keyboard, the next the pointer.
(DEVICE_ID_INPUT, _) if found.input.mmio.is_some() => &mut found.pointer,
(DEVICE_ID_INPUT, _) => &mut found.input,
(DEVICE_ID_RNG, _) => &mut found.random,
_ => continue,

View File

@@ -244,7 +244,12 @@ impl<'a> Launcher<'a> {
Device::Keystore => (self.devices.keystore, ", keystore handed over", ", no keystore"),
Device::Random => (self.devices.random, ", randomness handed over", ", no randomness"),
};
match libkos::device::give_device(ep, dev).is_ok() && dev.mmio.is_some() {
let handed = libkos::device::give_device(ep, dev).is_ok() && dev.mmio.is_some();
// The input server takes a pointer after the keyboard, or none.
if device == Device::Input {
let _ = libkos::device::give_device(ep, self.devices.pointer);
}
match handed {
true => given,
false => none,
}

View File

@@ -0,0 +1,41 @@
//! The pointer, hit-tested here, in the app (the display only says where
//! in the window a button went down and up): a press focuses what is under
//! it, and letting go over the same thing presses it, as Enter would. A
//! field is only focused: clicking one does not submit it.
use super::{App, Key, Outcome, Ui, input_at};
impl<A: App> Ui<A> {
/// What focus can rest on under (`x`, `y`), as last drawn: the last
/// in order is the one on top.
fn hit(&self, x: f32, y: f32) -> Option<usize> {
self.hits.iter().rposition(|r| x >= r.x && x < r.x + r.w && y >= r.y && y < r.y + r.h)
}
/// A button went down at (`x`, `y`) in the window.
pub fn pointer_down(&mut self, x: u16, y: u16) -> Outcome {
self.held = self.hit(x as f32, y as f32);
match self.held {
Some(i) if i != self.focus => {
self.focus = i;
self.caret = usize::MAX;
Outcome::Changed
}
_ => Outcome::Nothing,
}
}
/// The button went up at (`x`, `y`).
pub fn pointer_up(&mut self, x: u16, y: u16) -> Outcome {
let held = self.held.take();
if held.is_none() || held != self.hit(x as f32, y as f32) {
return Outcome::Nothing;
}
let view = self.app.view();
let target = self.pressables(&view).get(self.focus).copied();
if target.is_some_and(|t| input_at(&view, &mut 0, t).is_some()) {
return Outcome::Nothing;
}
self.key(Key::Press)
}
}

View File

@@ -72,6 +72,18 @@ pub fn run<A: App>() -> ! {
continue;
}
Event::Closed => syscall::exit(),
Event::Input(InputEvent::PointerDown { x, y, .. }) => {
dirty |= ui.pointer_down(x, y) == Outcome::Changed;
continue;
}
Event::Input(InputEvent::PointerUp { x, y, .. }) => {
match ui.pointer_up(x, y) {
Outcome::Leave => syscall::exit(),
Outcome::Changed => dirty = true,
Outcome::Nothing => {}
}
continue;
}
Event::Input(InputEvent::KeyDown(k)) => k,
Event::Input(_) => continue,
};

View File

@@ -315,3 +315,55 @@ fn drawing_only_what_moved_ends_where_drawing_it_all_does() {
}
/// The middle of `r`, as the display gives a point.
fn middle(r: Rect) -> (u16, u16) {
((r.x + r.w / 2.0) as u16, (r.y + r.h / 2.0) as u16)
}
#[test]
fn a_click_focuses_and_presses_what_is_under_it_and_only_that() {
let fonts = Fonts::load().unwrap();
let mut ui = Ui::<Counter>::new(Theme::WHIMSY_DARK);
ui.key(Key::Press);
let mut pm = tiny_skia::Pixmap::new(320, 200).unwrap();
ui.draw(&mut pm.as_mut(), &fonts, 0);
let view = ui.app.view();
let m = crate::text::Measurer { fonts: &fonts, theme: T };
let rects = layout(&view, Rect { x: 0.0, y: 0.0, w: 320.0, h: 200.0 }, &m, T);
let (reset, add) = (middle(rects[3]), middle(rects[2]));
// Down on Reset, up elsewhere: focused, not pressed.
assert_eq!(ui.pointer_down(reset.0, reset.1), Outcome::Changed);
assert_eq!(ui.pointer_up(300, 190), Outcome::Nothing);
assert_eq!(ui.app.0, 1);
// Down and up on it: pressed.
ui.pointer_down(reset.0, reset.1);
assert_eq!(ui.pointer_up(reset.0, reset.1), Outcome::Changed);
assert_eq!(ui.app.0, 0);
// Nothing there: nothing happens.
ui.draw(&mut pm.as_mut(), &fonts, 16);
assert_eq!(ui.pointer_down(310, 5), Outcome::Nothing);
assert_eq!(ui.pointer_up(310, 5), Outcome::Nothing);
ui.pointer_down(add.0, add.1);
ui.pointer_up(add.0, add.1);
assert_eq!(ui.app.0, 1);
}
#[test]
fn a_click_on_a_field_focuses_it_without_submitting() {
let fonts = Fonts::load().unwrap();
let mut ui = Ui::<Form>::new(Theme::WHIMSY_DARK);
ui.key(Key::Next);
let mut pm = tiny_skia::Pixmap::new(320, 200).unwrap();
ui.draw(&mut pm.as_mut(), &fonts, 0);
let view = ui.app.view();
let m = crate::text::Measurer { fonts: &fonts, theme: T };
let rects = layout(&view, Rect { x: 0.0, y: 0.0, w: 320.0, h: 200.0 }, &m, T);
// The form, the field's column, its label, the field, the button.
let (x, y) = middle(rects[3]);
assert_eq!(ui.pointer_down(x, y), Outcome::Changed, "focus moves to the field");
assert_eq!(ui.pointer_up(x, y), Outcome::Nothing);
assert_eq!(ui.app.sent, None, "not submitted");
ui.key(Key::Char('z'));
assert_eq!(ui.app.name, "abz", "typing goes to it");
}

View File

@@ -15,6 +15,9 @@ use crate::layout::{self, Rect};
use crate::motion::{Ring, Tween};
use crate::render::{self, Frame};
use crate::tree::{event_at, input_at, pressables, toggle_at, whole_elements};
#[path = "pointer.rs"]
mod pointer;
use crate::text::{Fonts, Measurer};
use crate::theme::Theme;
@@ -90,11 +93,15 @@ pub struct Ui<A: App> {
whole: bool,
/// Where the ring was last drawn.
last_ring: Option<Rect>,
/// Where each thing focus can rest on was last drawn, in order.
hits: Vec<Rect>,
/// What a pointer press went down on, until it is let go.
held: Option<usize>,
}
impl<A: App> Ui<A> {
pub fn new(theme: Theme) -> Self {
Self { app: A::init(), focus: 0, scroll: 0.0, caret: usize::MAX, theme, ring: None, flip: None, enter: None, drawn: None, whole: true, last_ring: None }
Self { app: A::init(), focus: 0, scroll: 0.0, caret: usize::MAX, theme, ring: None, flip: None, enter: None, drawn: None, whole: true, last_ring: None, hits: Vec::new(), held: None }
}
/// A new screen: from its top, coming in, the ring where it lands.
@@ -247,6 +254,7 @@ impl<A: App> Ui<A> {
(r.x, r.y) = (libm::roundf(r.x), libm::roundf(r.y));
(r.w, r.h) = (x1 - r.x, y1 - r.y);
}
self.hits = self.pressables(&view).iter().filter_map(|i| rects.get(*i).copied()).collect();
let dt = self.drawn.map_or(0.0, |d| now.saturating_sub(d).min(100) as f32 / 1000.0);
self.drawn = Some(now);
let target = focused.and_then(|i| rects.get(i)).copied();

View File

@@ -90,6 +90,20 @@ pub fn dma(pages: PageCount, va: VirtualAddress) -> Result<Dma> {
/// Routes `irq` to a fresh notification bound to the calling thread, so
/// its receive calls also return when the interrupt fires, and arms the
/// line (lines start masked; every acknowledgement re-arms).
/// Routes every one of `irqs` to one notification bound to this thread:
/// several devices, one wait.
pub fn bind_irqs(irqs: &[CapSlot]) -> Result<()> {
let notification = syscall::create_notification()?;
for irq in irqs {
syscall::set_irq_notification(*irq, notification)?;
}
syscall::bind_notification(notification)?;
for irq in irqs {
syscall::ack_interrupt(*irq)?;
}
Ok(())
}
pub fn bind_irq(irq: CapSlot) -> Result<()> {
let notification = syscall::create_notification()?;
syscall::set_irq_notification(irq, notification)?;

View File

@@ -21,6 +21,10 @@ pub enum Key {
pub enum InputEvent {
KeyDown(Key),
KeyUp(Key),
/// A button (0 left, 1 right) pressed or let go at (`x`, `y`): through
/// a window, its pixels.
PointerDown { button: u8, x: u16, y: u16 },
PointerUp { button: u8, x: u16, y: u16 },
}
fn decode_key(kind: usize, extra: usize) -> Option<Key> {
@@ -50,6 +54,14 @@ pub fn decode([tag, kind, extra, _]: [usize; 4]) -> Option<InputEvent> {
0 => None,
1 => decode_key(kind, extra).map(InputEvent::KeyDown),
2 => decode_key(kind, extra).map(InputEvent::KeyUp),
types::input::event::POINTER_DOWN | types::input::event::POINTER_UP => {
let (x, y) = types::input::event::unpack_point(extra);
let button = kind as u8;
Some(match tag {
types::input::event::POINTER_DOWN => InputEvent::PointerDown { button, x, y },
_ => InputEvent::PointerUp { button, x, y },
})
}
_ => None,
}
}

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)
}

View File

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

View File

@@ -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);
}
}

View File

@@ -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)))
);
}

View File

@@ -8,6 +8,21 @@ pub const SUBSCRIBE: usize = 2;
pub mod event {
pub const KEY_DOWN: usize = 1;
pub const KEY_UP: usize = 2;
/// A pointer event: word 1 the button (0 left, 1 right) or finger,
/// word 2 `x | y << 16`. From the input server, a fraction of the
/// screen (0 to 65535 each way); from the display to a window, that
/// window's pixels.
pub const POINTER_DOWN: usize = 3;
pub const POINTER_MOVE: usize = 4;
pub const POINTER_UP: usize = 5;
pub const fn pack_point(x: u16, y: u16) -> usize {
x as usize | (y as usize) << 16
}
pub const fn unpack_point(word: usize) -> (u16, u16) {
(word as u16, (word >> 16) as u16)
}
/// A function key: `extra` is its number (F1 is 1).
pub const FUNCTION_KEY: usize = 10;
}