feat(pointer): virtio tablet as 2nd input dev (driver per-instance VAs, abs/btn decode), input server serves 2 devices on one notif; display software cursor, bar click switches window, presses/releases to front window in its px; k-ui hit-tests (press focuses, release on same presses, field only focuses)

This commit is contained in:
2026-10-28 16:50:20 +01:00
parent 0de13f124d
commit 1401af3d82
22 changed files with 545 additions and 46 deletions

View File

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

View File

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

View File

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

View File

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