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:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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(®s, 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;
|
||||
|
||||
79
crates/drivers/input/virtio/src/pointer.rs
Normal file
79
crates/drivers/input/virtio/src/pointer.rs
Normal 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 }));
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
41
crates/k-ui/src/pointer.rs
Normal file
41
crates/k-ui/src/pointer.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
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)))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user