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

@@ -99,5 +99,5 @@ run-gui: image
-drive file=boot.img,if=none,format=raw,id=hd0 \
-device virtio-blk-device,drive=hd0 $(QEMU_KEYS) \
-device virtio-gpu-device \
-device virtio-keyboard-device \
-device virtio-keyboard-device -device virtio-tablet-device \
-serial stdio

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

View File

@@ -219,6 +219,18 @@ app cannot pick its own); the theme that window reads carries it, its
lightness moved until it passes contrast (`theme::with_accent`), or the
theme's own accent if it cannot, is black or white, or looks destructive.
The pointer (QEMU's `virtio-tablet-device`, the second virtio input
device, which init hands the input server after the keyboard) is the
display's as well. The input server reports positions as fractions of
the screen (`types::input::event`, `POINTER_*`, `x | y << 16` in 0 to
65535); the display draws the cursor itself over everything (a sprite,
redrawing what it hid as it moves), brings a window forward when its
name on the bar is pressed, and passes presses and releases below the
bar to the window in front, in that window's pixels. Moves are not
passed on. K-UI hit-tests them against what it last drew: a press
focuses the thing under it, letting go over the same thing presses it
(a field is only focused).
Input reaches apps through the display server, the input server's only
client. It subscribes to the input server with a notification, drains it
when signalled, keeps F1, and queues the rest for the window in front,

View File

@@ -33,7 +33,7 @@ boot() {
-kernel "${KERNEL}" \
-drive "file=${IMAGE},if=none,format=raw,id=hd0" -device virtio-blk-device,drive=hd0 \
-drive "file=${KEYS},if=none,format=raw,id=ks" -device virtio-blk-device,drive=ks \
-device virtio-rng-device -device virtio-gpu-device -device virtio-keyboard-device \
-device virtio-rng-device -device virtio-gpu-device -device virtio-keyboard-device -device virtio-tablet-device \
-display none -serial "file:${LOG}" -monitor "unix:${MON},server,nowait"
local s=$?
wait