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:
2
Makefile
2
Makefile
@@ -99,5 +99,5 @@ run-gui: image
|
|||||||
-drive file=boot.img,if=none,format=raw,id=hd0 \
|
-drive file=boot.img,if=none,format=raw,id=hd0 \
|
||||||
-device virtio-blk-device,drive=hd0 $(QEMU_KEYS) \
|
-device virtio-blk-device,drive=hd0 $(QEMU_KEYS) \
|
||||||
-device virtio-gpu-device \
|
-device virtio-gpu-device \
|
||||||
-device virtio-keyboard-device \
|
-device virtio-keyboard-device -device virtio-tablet-device \
|
||||||
-serial stdio
|
-serial stdio
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
extern crate libkos;
|
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 libkos::device::{self, KIND_UART_16550, KIND_VIRTIO};
|
||||||
use port_input_device::InputDevice;
|
use port_input_device::InputDevice;
|
||||||
use types::bounded::CapSlot;
|
use types::bounded::CapSlot;
|
||||||
@@ -12,17 +14,24 @@ libkos::entry!(main);
|
|||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let ep = CapSlot::new(0).unwrap();
|
let ep = CapSlot::new(0).unwrap();
|
||||||
// Init hands over the keyboard (virtio) or, failing that, the UART.
|
// Init hands over the input devices, two (a keyboard, a tablet), each
|
||||||
let dev = device::receive_device(ep);
|
// virtio or, failing a keyboard, the UART; either may be none.
|
||||||
match (dev.kind, dev.mmio) {
|
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)) => {
|
(KIND_VIRTIO, Some(mmio)) => {
|
||||||
if let Some(d) = driver_input_virtio::VirtioInputDevice::try_new(mmio) {
|
if let Some(d) = VirtioInputDevice::try_new(mmio, 0) {
|
||||||
run(d, dev.irq, ep);
|
run(Pair { first: d, second: tablet }, irqs, ep);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(KIND_UART_16550, Some(mmio)) => {
|
(KIND_UART_16550, Some(mmio)) => {
|
||||||
if let Some(d) = driver_input_uart::UartInputDevice::new(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();
|
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)]
|
#[inline(never)]
|
||||||
fn run<D: InputDevice>(device: D, irq: Option<CapSlot>, ep: CapSlot) -> ! {
|
fn run<D: InputDevice>(device: D, irqs: [Option<CapSlot>; MAX_DEVICES], ep: CapSlot) -> ! {
|
||||||
let mut srv = match irq.filter(|irq| device::bind_irq(*irq).is_ok()) {
|
let mut lines = [CapSlot::const_new(0); MAX_DEVICES];
|
||||||
Some(irq) => input_server::server::InputServer::with_irq(device, irq),
|
let mut n = 0;
|
||||||
None => input_server::server::InputServer::new(device),
|
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)
|
server_framework::serve(&mut srv, ep)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
mod keymap;
|
mod keymap;
|
||||||
|
mod pointer;
|
||||||
|
|
||||||
use core::sync::atomic::{Ordering, fence};
|
use core::sync::atomic::{Ordering, fence};
|
||||||
use hw_virtio::mmio::{MmioAccess, Registers};
|
use hw_virtio::mmio::{MmioAccess, Registers};
|
||||||
@@ -11,10 +12,16 @@ use types::address::{ByteOffset, PhysicalAddress, VirtualAddress};
|
|||||||
use types::bounded::PageCount;
|
use types::bounded::PageCount;
|
||||||
|
|
||||||
/// Drivers map their devices in the device window (0x01D0_0000 up, 64 KiB
|
/// Drivers map their devices in the device window (0x01D0_0000 up, 64 KiB
|
||||||
/// each), clear of program images and heaps.
|
/// each), clear of program images and heaps; an input device takes 16 KiB
|
||||||
const INPUT_MMIO_VA: VirtualAddress = VirtualAddress::from_raw(0x01D1_0000);
|
/// of it, the `n`th from here (a keyboard, a tablet).
|
||||||
const VQUEUE_VA: VirtualAddress = VirtualAddress::from_raw(0x01D1_1000);
|
const INPUT_VA: usize = 0x01D1_0000;
|
||||||
const EVBUF_VA: VirtualAddress = VirtualAddress::from_raw(0x01D1_3000);
|
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 QUEUE_SIZE: usize = 32;
|
||||||
const EVENT_SIZE: usize = 8;
|
const EVENT_SIZE: usize = 8;
|
||||||
@@ -37,20 +44,27 @@ pub struct VirtioInputDevice {
|
|||||||
shift: bool,
|
shift: bool,
|
||||||
ctrl: bool,
|
ctrl: bool,
|
||||||
alt: bool,
|
alt: bool,
|
||||||
|
/// Where a tablet's pointer is, between its reports.
|
||||||
|
pointer: pointer::Pointer,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VirtioInputDevice {
|
impl VirtioInputDevice {
|
||||||
/// Takes over the virtio input device behind `mmio`. `None` if it is
|
/// Takes over the virtio input device behind `mmio`, as this program's
|
||||||
/// not one or any step fails.
|
/// `n`th (each has addresses of its own). `None` if it is not one or
|
||||||
pub fn try_new(mmio: types::bounded::CapSlot) -> Option<Self> {
|
/// any step fails.
|
||||||
let regs = Registers::new(libkos::device::map_mmio(mmio, INPUT_MMIO_VA).ok()?);
|
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
|
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
|
|| regs.read(hw_virtio::mmio::REG_DEVICE_ID) != hw_virtio::mmio::DEVICE_ID_INPUT
|
||||||
{
|
{
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let rings = libkos::device::dma(PageCount::new(2)?, VQUEUE_VA).ok()?;
|
let rings = libkos::device::dma(PageCount::new(2)?, at(VQUEUE_AT)).ok()?;
|
||||||
let events = libkos::device::dma(PageCount::new(1)?, EVBUF_VA).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);
|
hw_virtio::mmio::init_legacy_device(®s, rings.physical, QUEUE_SIZE as u32);
|
||||||
|
|
||||||
let mut dev = Self {
|
let mut dev = Self {
|
||||||
@@ -63,6 +77,7 @@ impl VirtioInputDevice {
|
|||||||
shift: false,
|
shift: false,
|
||||||
ctrl: false,
|
ctrl: false,
|
||||||
alt: false,
|
alt: false,
|
||||||
|
pointer: pointer::Pointer::default(),
|
||||||
};
|
};
|
||||||
dev.fill_queue();
|
dev.fill_queue();
|
||||||
Some(dev)
|
Some(dev)
|
||||||
@@ -123,6 +138,9 @@ impl InputDevice for VirtioInputDevice {
|
|||||||
let (ev_type, ev_code, ev_value) = self.read_event(desc_idx);
|
let (ev_type, ev_code, ev_value) = self.read_event(desc_idx);
|
||||||
self.resubmit(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) {
|
if ev_type == EV_KEY && matches!(ev_code, keymap::LEFT_SHIFT | keymap::RIGHT_SHIFT) {
|
||||||
self.shift = ev_value != 0;
|
self.shift = ev_value != 0;
|
||||||
continue;
|
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 disk: Device,
|
||||||
pub gpu: Device,
|
pub gpu: Device,
|
||||||
pub input: 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.
|
/// The gatekeeper's: a small disk of its own for the key slots.
|
||||||
pub keystore: Device,
|
pub keystore: Device,
|
||||||
pub random: Device,
|
pub random: Device,
|
||||||
@@ -94,6 +96,7 @@ pub fn discover() -> Devices {
|
|||||||
disk: Device::NONE,
|
disk: Device::NONE,
|
||||||
gpu: Device::NONE,
|
gpu: Device::NONE,
|
||||||
input: Device::NONE,
|
input: Device::NONE,
|
||||||
|
pointer: Device::NONE,
|
||||||
keystore: Device::NONE,
|
keystore: Device::NONE,
|
||||||
random: 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, size) if size <= KEYSTORE_MAX_SECTORS => &mut found.keystore,
|
||||||
(DEVICE_ID_BLOCK, _) => &mut found.disk,
|
(DEVICE_ID_BLOCK, _) => &mut found.disk,
|
||||||
(DEVICE_ID_GPU, _) => &mut found.gpu,
|
(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_INPUT, _) => &mut found.input,
|
||||||
(DEVICE_ID_RNG, _) => &mut found.random,
|
(DEVICE_ID_RNG, _) => &mut found.random,
|
||||||
_ => continue,
|
_ => continue,
|
||||||
|
|||||||
@@ -244,7 +244,12 @@ impl<'a> Launcher<'a> {
|
|||||||
Device::Keystore => (self.devices.keystore, ", keystore handed over", ", no keystore"),
|
Device::Keystore => (self.devices.keystore, ", keystore handed over", ", no keystore"),
|
||||||
Device::Random => (self.devices.random, ", randomness handed over", ", no randomness"),
|
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,
|
true => given,
|
||||||
false => none,
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
Event::Closed => syscall::exit(),
|
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(InputEvent::KeyDown(k)) => k,
|
||||||
Event::Input(_) => continue,
|
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::motion::{Ring, Tween};
|
||||||
use crate::render::{self, Frame};
|
use crate::render::{self, Frame};
|
||||||
use crate::tree::{event_at, input_at, pressables, toggle_at, whole_elements};
|
use crate::tree::{event_at, input_at, pressables, toggle_at, whole_elements};
|
||||||
|
|
||||||
|
#[path = "pointer.rs"]
|
||||||
|
mod pointer;
|
||||||
use crate::text::{Fonts, Measurer};
|
use crate::text::{Fonts, Measurer};
|
||||||
use crate::theme::Theme;
|
use crate::theme::Theme;
|
||||||
|
|
||||||
@@ -90,11 +93,15 @@ pub struct Ui<A: App> {
|
|||||||
whole: bool,
|
whole: bool,
|
||||||
/// Where the ring was last drawn.
|
/// Where the ring was last drawn.
|
||||||
last_ring: Option<Rect>,
|
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> {
|
impl<A: App> Ui<A> {
|
||||||
pub fn new(theme: Theme) -> Self {
|
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.
|
/// 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.x, r.y) = (libm::roundf(r.x), libm::roundf(r.y));
|
||||||
(r.w, r.h) = (x1 - r.x, y1 - 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);
|
let dt = self.drawn.map_or(0.0, |d| now.saturating_sub(d).min(100) as f32 / 1000.0);
|
||||||
self.drawn = Some(now);
|
self.drawn = Some(now);
|
||||||
let target = focused.and_then(|i| rects.get(i)).copied();
|
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
|
/// Routes `irq` to a fresh notification bound to the calling thread, so
|
||||||
/// its receive calls also return when the interrupt fires, and arms the
|
/// its receive calls also return when the interrupt fires, and arms the
|
||||||
/// line (lines start masked; every acknowledgement re-arms).
|
/// 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<()> {
|
pub fn bind_irq(irq: CapSlot) -> Result<()> {
|
||||||
let notification = syscall::create_notification()?;
|
let notification = syscall::create_notification()?;
|
||||||
syscall::set_irq_notification(irq, notification)?;
|
syscall::set_irq_notification(irq, notification)?;
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ pub enum Key {
|
|||||||
pub enum InputEvent {
|
pub enum InputEvent {
|
||||||
KeyDown(Key),
|
KeyDown(Key),
|
||||||
KeyUp(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> {
|
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,
|
0 => None,
|
||||||
1 => decode_key(kind, extra).map(InputEvent::KeyDown),
|
1 => decode_key(kind, extra).map(InputEvent::KeyDown),
|
||||||
2 => decode_key(kind, extra).map(InputEvent::KeyUp),
|
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,
|
_ => 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]
|
#![no_std]
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
|
pub mod cursor;
|
||||||
pub mod host;
|
pub mod host;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
pub mod themes;
|
pub mod themes;
|
||||||
|
|||||||
@@ -5,7 +5,10 @@
|
|||||||
//! Apps draw only into their own buffer; the bar is the display's, so no
|
//! 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
|
//! app can pose as another. The kernel side (buffers, capabilities, the
|
||||||
//! input server) is behind `Host`. The bar wears the theme in use, which
|
//! 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 libkos::syscall::Give;
|
||||||
use port_framebuffer::Framebuffer;
|
use port_framebuffer::Framebuffer;
|
||||||
@@ -15,6 +18,7 @@ use types::bounded::CapSlot;
|
|||||||
use types::display::window::{self as w, NAME_BYTES};
|
use types::display::window::{self as w, NAME_BYTES};
|
||||||
use types::display::{Color, Dimension, ScreenCoord};
|
use types::display::{Color, Dimension, ScreenCoord};
|
||||||
|
|
||||||
|
use crate::cursor;
|
||||||
use crate::themes::Themes;
|
use crate::themes::Themes;
|
||||||
use crate::windows::{MAX_WINDOWS, Windows};
|
use crate::windows::{MAX_WINDOWS, Windows};
|
||||||
|
|
||||||
@@ -68,6 +72,8 @@ pub struct DisplayServer<F: Framebuffer, H: Host> {
|
|||||||
user: [u8; NAME_BYTES],
|
user: [u8; NAME_BYTES],
|
||||||
user_len: usize,
|
user_len: usize,
|
||||||
themes: Themes,
|
themes: Themes,
|
||||||
|
/// Where the pointer is on the screen, once it has moved.
|
||||||
|
cursor: Option<(u16, u16)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn colour(c: theme::Rgba) -> Color {
|
fn colour(c: theme::Rgba) -> Color {
|
||||||
@@ -76,7 +82,7 @@ fn colour(c: theme::Rgba) -> Color {
|
|||||||
|
|
||||||
impl<F: Framebuffer, H: Host> DisplayServer<F, H> {
|
impl<F: Framebuffer, H: Host> DisplayServer<F, H> {
|
||||||
pub fn new(fb: F, host: H, themes: Themes) -> Self {
|
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.repaint();
|
||||||
s
|
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));
|
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);
|
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();
|
self.fb.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,6 +251,7 @@ impl<F: Framebuffer, H: Host> DisplayServer<F, H> {
|
|||||||
None => {
|
None => {
|
||||||
let bg = colour(self.themes.active().background);
|
let bg = colour(self.themes.active().background);
|
||||||
self.fb.fill_rect(ScreenCoord::new(0), ScreenCoord::new(BAR_HEIGHT), width, height, bg);
|
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();
|
self.fb.flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,6 +286,7 @@ impl<F: Framebuffer, H: Host> DisplayServer<F, H> {
|
|||||||
let user = &self.user[..self.user_len];
|
let user = &self.user[..self.user_len];
|
||||||
let at = width.raw().saturating_sub((user.len() as u16 + 1) * font::WIDTH);
|
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);
|
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.
|
/// 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) {
|
fn signal(&mut self, _bits: usize) {
|
||||||
let mut told = None;
|
let mut told = None;
|
||||||
while let Some(event) = self.host.input() {
|
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 let Some(key) = display_key(event) {
|
||||||
if event[0] == types::input::event::KEY_DOWN {
|
if event[0] == types::input::event::KEY_DOWN {
|
||||||
self.window_key(key);
|
self.window_key(key);
|
||||||
|
|||||||
@@ -298,3 +298,36 @@ fn window_keys_raise_home_and_close_only_apps() {
|
|||||||
key(&mut s, w::CLOSE_KEY);
|
key(&mut s, w::CLOSE_KEY);
|
||||||
assert!(s.slot_for_test(terminal).is_some());
|
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_DOWN: usize = 1;
|
||||||
const EV_KEY_UP: usize = 2;
|
const EV_KEY_UP: usize = 2;
|
||||||
const EV_PTR_DOWN: usize = 3;
|
const EV_PTR_DOWN: usize = types::input::event::POINTER_DOWN;
|
||||||
const EV_PTR_MOVE: usize = 4;
|
const EV_PTR_MOVE: usize = types::input::event::POINTER_MOVE;
|
||||||
const EV_PTR_UP: usize = 5;
|
const EV_PTR_UP: usize = types::input::event::POINTER_UP;
|
||||||
const EV_SCROLL: usize = 6;
|
const EV_SCROLL: usize = 6;
|
||||||
const EV_GAMEPAD_BTN: usize = 7;
|
const EV_GAMEPAD_BTN: usize = 7;
|
||||||
const EV_GAMEPAD_AXIS: usize = 8;
|
const EV_GAMEPAD_AXIS: usize = 8;
|
||||||
|
|||||||
@@ -4,13 +4,39 @@ use port_input_device::{InputDevice, InputEvent};
|
|||||||
use types::bounded::CapSlot;
|
use types::bounded::CapSlot;
|
||||||
use crate::protocol::{InputRequest, InputResponse};
|
use crate::protocol::{InputRequest, InputResponse};
|
||||||
|
|
||||||
/// Events captured on interrupts and not yet asked for.
|
/// Events captured on interrupts and not yet asked for (a tablet sends
|
||||||
const BUFFERED: usize = 32;
|
/// 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> {
|
pub struct InputServer<D: InputDevice> {
|
||||||
device: D,
|
device: D,
|
||||||
/// The IRQ to re-arm after each interrupt, if the device has one.
|
/// The IRQs to re-arm after each interrupt, those the devices have.
|
||||||
irq: Option<CapSlot>,
|
irqs: [Option<CapSlot>; MAX_DEVICES],
|
||||||
events: [Option<InputEvent>; BUFFERED],
|
events: [Option<InputEvent>; BUFFERED],
|
||||||
head: usize,
|
head: usize,
|
||||||
len: usize,
|
len: usize,
|
||||||
@@ -21,14 +47,14 @@ pub struct InputServer<D: InputDevice> {
|
|||||||
impl<D: InputDevice> InputServer<D> {
|
impl<D: InputDevice> InputServer<D> {
|
||||||
/// A server that reads the device only when a client asks.
|
/// A server that reads the device only when a client asks.
|
||||||
pub fn new(device: D) -> Self {
|
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
|
/// A server woken by `irqs` (routed and bound by the caller): it
|
||||||
/// the device into a buffer on every interrupt, so events that arrive
|
/// drains the devices into a buffer on every interrupt, so events that
|
||||||
/// between requests are kept.
|
/// arrive between requests are kept.
|
||||||
pub fn with_irq(device: D, irq: CapSlot) -> Self {
|
pub fn with_irqs(device: D, irqs: [Option<CapSlot>; MAX_DEVICES]) -> Self {
|
||||||
Self { irq: Some(irq), ..Self::new(device) }
|
Self { irqs, ..Self::new(device) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push(&mut self, event: InputEvent) {
|
fn push(&mut self, event: InputEvent) {
|
||||||
@@ -109,7 +135,7 @@ impl<D: InputDevice> Server for InputServer<D> {
|
|||||||
let _ = libkos::syscall::signal(subscriber);
|
let _ = libkos::syscall::signal(subscriber);
|
||||||
}
|
}
|
||||||
self.device.acknowledge();
|
self.device.acknowledge();
|
||||||
if let Some(irq) = self.irq {
|
for irq in self.irqs.into_iter().flatten() {
|
||||||
let _ = libkos::syscall::ack_interrupt(irq);
|
let _ = libkos::syscall::ack_interrupt(irq);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ fn an_interrupt_buffers_events_until_asked() {
|
|||||||
let mut device = MockInputDevice::new();
|
let mut device = MockInputDevice::new();
|
||||||
device.push_event(InputEvent::KeyDown(Key::Up));
|
device.push_event(InputEvent::KeyDown(Key::Up));
|
||||||
device.push_event(InputEvent::KeyDown(Key::Down));
|
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);
|
server.signal(1);
|
||||||
// The device itself is now drained; the events wait in the server.
|
// The device itself is now drained; the events wait in the server.
|
||||||
assert!(!port_input_device::InputDevice::has_event(server.device()));
|
assert!(!port_input_device::InputDevice::has_event(server.device()));
|
||||||
@@ -72,9 +72,10 @@ fn an_interrupt_buffers_events_until_asked() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_full_buffer_drops_the_oldest_event() {
|
fn a_full_buffer_drops_the_oldest_event() {
|
||||||
let mut server = InputServer::with_irq(MockInputDevice::new(), types::bounded::CapSlot::new(1).unwrap());
|
let mut server = InputServer::with_irqs(MockInputDevice::new(), [types::bounded::CapSlot::new(1), None]);
|
||||||
// 40 interrupts, one key each; the buffer keeps the last 32.
|
// More interrupts than the buffer holds, one key each: it keeps the last.
|
||||||
for i in 0..40u8 {
|
let extra = 8;
|
||||||
|
for i in 0..(crate::server::BUFFERED + extra) as u8 {
|
||||||
let mut device = MockInputDevice::new();
|
let mut device = MockInputDevice::new();
|
||||||
device.push_event(InputEvent::KeyDown(Key::Char(i)));
|
device.push_event(InputEvent::KeyDown(Key::Char(i)));
|
||||||
server.replace_device_for_test(device);
|
server.replace_device_for_test(device);
|
||||||
@@ -82,7 +83,7 @@ fn a_full_buffer_drops_the_oldest_event() {
|
|||||||
}
|
}
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
server.handle(InputRequest::PollEvent),
|
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 mod event {
|
||||||
pub const KEY_DOWN: usize = 1;
|
pub const KEY_DOWN: usize = 1;
|
||||||
pub const KEY_UP: usize = 2;
|
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).
|
/// A function key: `extra` is its number (F1 is 1).
|
||||||
pub const FUNCTION_KEY: usize = 10;
|
pub const FUNCTION_KEY: usize = 10;
|
||||||
}
|
}
|
||||||
|
|||||||
12
docs/ipc.md
12
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
|
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.
|
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
|
Input reaches apps through the display server, the input server's only
|
||||||
client. It subscribes to the input server with a notification, drains it
|
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,
|
when signalled, keeps F1, and queues the rest for the window in front,
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ boot() {
|
|||||||
-kernel "${KERNEL}" \
|
-kernel "${KERNEL}" \
|
||||||
-drive "file=${IMAGE},if=none,format=raw,id=hd0" -device virtio-blk-device,drive=hd0 \
|
-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 \
|
-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"
|
-display none -serial "file:${LOG}" -monitor "unix:${MON},server,nowait"
|
||||||
local s=$?
|
local s=$?
|
||||||
wait
|
wait
|
||||||
|
|||||||
Reference in New Issue
Block a user