diff --git a/Makefile b/Makefile index e3544fc..9c93c27 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/crates/bins/input-qemu/src/main.rs b/crates/bins/input-qemu/src/main.rs index 45e68e0..85c7837 100644 --- a/crates/bins/input-qemu/src/main.rs +++ b/crates/bins/input-qemu/src/main.rs @@ -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(device: D, irq: Option, 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(device: D, irqs: [Option; 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) } diff --git a/crates/drivers/input/virtio/src/lib.rs b/crates/drivers/input/virtio/src/lib.rs index 1e9e7a8..bf3f946 100644 --- a/crates/drivers/input/virtio/src/lib.rs +++ b/crates/drivers/input/virtio/src/lib.rs @@ -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 { - 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 { + 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; diff --git a/crates/drivers/input/virtio/src/pointer.rs b/crates/drivers/input/virtio/src/pointer.rs new file mode 100644 index 0000000..8315f5a --- /dev/null +++ b/crates/drivers/input/virtio/src/pointer.rs @@ -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 { + 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 })); + } +} diff --git a/crates/init/src/devices.rs b/crates/init/src/devices.rs index 39d4fa5..06b5bbf 100644 --- a/crates/init/src/devices.rs +++ b/crates/init/src/devices.rs @@ -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, diff --git a/crates/init/src/launch.rs b/crates/init/src/launch.rs index 385a785..745eb31 100644 --- a/crates/init/src/launch.rs +++ b/crates/init/src/launch.rs @@ -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, } diff --git a/crates/k-ui/src/pointer.rs b/crates/k-ui/src/pointer.rs new file mode 100644 index 0000000..6891272 --- /dev/null +++ b/crates/k-ui/src/pointer.rs @@ -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 Ui { + /// 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 { + 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) + } +} diff --git a/crates/k-ui/src/run.rs b/crates/k-ui/src/run.rs index a6d5122..9f9c1d8 100644 --- a/crates/k-ui/src/run.rs +++ b/crates/k-ui/src/run.rs @@ -72,6 +72,18 @@ pub fn run() -> ! { 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, }; diff --git a/crates/k-ui/src/tests.rs b/crates/k-ui/src/tests.rs index e9ecb0a..30c5d74 100644 --- a/crates/k-ui/src/tests.rs +++ b/crates/k-ui/src/tests.rs @@ -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::::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::
::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"); +} diff --git a/crates/k-ui/src/ui.rs b/crates/k-ui/src/ui.rs index 92f547e..e67a43b 100644 --- a/crates/k-ui/src/ui.rs +++ b/crates/k-ui/src/ui.rs @@ -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 { whole: bool, /// Where the ring was last drawn. last_ring: Option, + /// Where each thing focus can rest on was last drawn, in order. + hits: Vec, + /// What a pointer press went down on, until it is let go. + held: Option, } impl Ui { 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 Ui { (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(); diff --git a/crates/libkos/src/device.rs b/crates/libkos/src/device.rs index 7fb1a00..1e4703f 100644 --- a/crates/libkos/src/device.rs +++ b/crates/libkos/src/device.rs @@ -90,6 +90,20 @@ pub fn dma(pages: PageCount, va: VirtualAddress) -> Result { /// 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)?; diff --git a/crates/libkos/src/input.rs b/crates/libkos/src/input.rs index b08f2ae..f38c898 100644 --- a/crates/libkos/src/input.rs +++ b/crates/libkos/src/input.rs @@ -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 { @@ -50,6 +54,14 @@ pub fn decode([tag, kind, extra, _]: [usize; 4]) -> Option { 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, } } diff --git a/crates/servers/display-server/src/cursor.rs b/crates/servers/display-server/src/cursor.rs new file mode 100644 index 0000000..1cb8ed3 --- /dev/null +++ b/crates/servers/display-server/src/cursor.rs @@ -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); + } + } + } +} diff --git a/crates/servers/display-server/src/lib.rs b/crates/servers/display-server/src/lib.rs index d71b4be..84e7ab4 100644 --- a/crates/servers/display-server/src/lib.rs +++ b/crates/servers/display-server/src/lib.rs @@ -1,6 +1,7 @@ #![no_std] #![forbid(unsafe_code)] +pub mod cursor; pub mod host; pub mod server; pub mod themes; diff --git a/crates/servers/display-server/src/server.rs b/crates/servers/display-server/src/server.rs index b227414..689238f 100644 --- a/crates/servers/display-server/src/server.rs +++ b/crates/servers/display-server/src/server.rs @@ -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 { 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 DisplayServer { 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 DisplayServer { 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 + '_ { + 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 DisplayServer { 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 DisplayServer { 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 Server for DisplayServer { 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); diff --git a/crates/servers/display-server/src/tests/server.rs b/crates/servers/display-server/src/tests/server.rs index eb6f1e0..33b0a67 100644 --- a/crates/servers/display-server/src/tests/server.rs +++ b/crates/servers/display-server/src/tests/server.rs @@ -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 { + s.framebuffer().pixel(x, y) +} diff --git a/crates/servers/input-server/src/protocol.rs b/crates/servers/input-server/src/protocol.rs index 7861d4b..7018084 100644 --- a/crates/servers/input-server/src/protocol.rs +++ b/crates/servers/input-server/src/protocol.rs @@ -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; diff --git a/crates/servers/input-server/src/server.rs b/crates/servers/input-server/src/server.rs index 38b1575..60fb487 100644 --- a/crates/servers/input-server/src/server.rs +++ b/crates/servers/input-server/src/server.rs @@ -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 { + pub first: A, + pub second: Option, +} + +impl InputDevice for Pair { + fn poll(&mut self) -> Option { + 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 { device: D, - /// The IRQ to re-arm after each interrupt, if the device has one. - irq: Option, + /// The IRQs to re-arm after each interrupt, those the devices have. + irqs: [Option; MAX_DEVICES], events: [Option; BUFFERED], head: usize, len: usize, @@ -21,14 +47,14 @@ pub struct InputServer { impl InputServer { /// 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; MAX_DEVICES]) -> Self { + Self { irqs, ..Self::new(device) } } fn push(&mut self, event: InputEvent) { @@ -109,7 +135,7 @@ impl Server for InputServer { 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); } } diff --git a/crates/servers/input-server/src/tests/server.rs b/crates/servers/input-server/src/tests/server.rs index e19af49..494aa5a 100644 --- a/crates/servers/input-server/src/tests/server.rs +++ b/crates/servers/input-server/src/tests/server.rs @@ -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))) ); } diff --git a/crates/types/src/input.rs b/crates/types/src/input.rs index 26ab49a..551e496 100644 --- a/crates/types/src/input.rs +++ b/crates/types/src/input.rs @@ -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; } diff --git a/docs/ipc.md b/docs/ipc.md index 49742a2..9f38f5b 100644 --- a/docs/ipc.md +++ b/docs/ipc.md @@ -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, diff --git a/scripts/test-qemu.sh b/scripts/test-qemu.sh index 56fc106..fadcab6 100755 --- a/scripts/test-qemu.sh +++ b/scripts/test-qemu.sh @@ -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