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:
41
crates/k-ui/src/pointer.rs
Normal file
41
crates/k-ui/src/pointer.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
//! The pointer, hit-tested here, in the app (the display only says where
|
||||
//! in the window a button went down and up): a press focuses what is under
|
||||
//! it, and letting go over the same thing presses it, as Enter would. A
|
||||
//! field is only focused: clicking one does not submit it.
|
||||
|
||||
use super::{App, Key, Outcome, Ui, input_at};
|
||||
|
||||
impl<A: App> Ui<A> {
|
||||
/// What focus can rest on under (`x`, `y`), as last drawn: the last
|
||||
/// in order is the one on top.
|
||||
fn hit(&self, x: f32, y: f32) -> Option<usize> {
|
||||
self.hits.iter().rposition(|r| x >= r.x && x < r.x + r.w && y >= r.y && y < r.y + r.h)
|
||||
}
|
||||
|
||||
/// A button went down at (`x`, `y`) in the window.
|
||||
pub fn pointer_down(&mut self, x: u16, y: u16) -> Outcome {
|
||||
self.held = self.hit(x as f32, y as f32);
|
||||
match self.held {
|
||||
Some(i) if i != self.focus => {
|
||||
self.focus = i;
|
||||
self.caret = usize::MAX;
|
||||
Outcome::Changed
|
||||
}
|
||||
_ => Outcome::Nothing,
|
||||
}
|
||||
}
|
||||
|
||||
/// The button went up at (`x`, `y`).
|
||||
pub fn pointer_up(&mut self, x: u16, y: u16) -> Outcome {
|
||||
let held = self.held.take();
|
||||
if held.is_none() || held != self.hit(x as f32, y as f32) {
|
||||
return Outcome::Nothing;
|
||||
}
|
||||
let view = self.app.view();
|
||||
let target = self.pressables(&view).get(self.focus).copied();
|
||||
if target.is_some_and(|t| input_at(&view, &mut 0, t).is_some()) {
|
||||
return Outcome::Nothing;
|
||||
}
|
||||
self.key(Key::Press)
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,18 @@ pub fn run<A: App>() -> ! {
|
||||
continue;
|
||||
}
|
||||
Event::Closed => syscall::exit(),
|
||||
Event::Input(InputEvent::PointerDown { x, y, .. }) => {
|
||||
dirty |= ui.pointer_down(x, y) == Outcome::Changed;
|
||||
continue;
|
||||
}
|
||||
Event::Input(InputEvent::PointerUp { x, y, .. }) => {
|
||||
match ui.pointer_up(x, y) {
|
||||
Outcome::Leave => syscall::exit(),
|
||||
Outcome::Changed => dirty = true,
|
||||
Outcome::Nothing => {}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
Event::Input(InputEvent::KeyDown(k)) => k,
|
||||
Event::Input(_) => continue,
|
||||
};
|
||||
|
||||
@@ -315,3 +315,55 @@ fn drawing_only_what_moved_ends_where_drawing_it_all_does() {
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// The middle of `r`, as the display gives a point.
|
||||
fn middle(r: Rect) -> (u16, u16) {
|
||||
((r.x + r.w / 2.0) as u16, (r.y + r.h / 2.0) as u16)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_click_focuses_and_presses_what_is_under_it_and_only_that() {
|
||||
let fonts = Fonts::load().unwrap();
|
||||
let mut ui = Ui::<Counter>::new(Theme::WHIMSY_DARK);
|
||||
ui.key(Key::Press);
|
||||
let mut pm = tiny_skia::Pixmap::new(320, 200).unwrap();
|
||||
ui.draw(&mut pm.as_mut(), &fonts, 0);
|
||||
let view = ui.app.view();
|
||||
let m = crate::text::Measurer { fonts: &fonts, theme: T };
|
||||
let rects = layout(&view, Rect { x: 0.0, y: 0.0, w: 320.0, h: 200.0 }, &m, T);
|
||||
let (reset, add) = (middle(rects[3]), middle(rects[2]));
|
||||
// Down on Reset, up elsewhere: focused, not pressed.
|
||||
assert_eq!(ui.pointer_down(reset.0, reset.1), Outcome::Changed);
|
||||
assert_eq!(ui.pointer_up(300, 190), Outcome::Nothing);
|
||||
assert_eq!(ui.app.0, 1);
|
||||
// Down and up on it: pressed.
|
||||
ui.pointer_down(reset.0, reset.1);
|
||||
assert_eq!(ui.pointer_up(reset.0, reset.1), Outcome::Changed);
|
||||
assert_eq!(ui.app.0, 0);
|
||||
// Nothing there: nothing happens.
|
||||
ui.draw(&mut pm.as_mut(), &fonts, 16);
|
||||
assert_eq!(ui.pointer_down(310, 5), Outcome::Nothing);
|
||||
assert_eq!(ui.pointer_up(310, 5), Outcome::Nothing);
|
||||
ui.pointer_down(add.0, add.1);
|
||||
ui.pointer_up(add.0, add.1);
|
||||
assert_eq!(ui.app.0, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_click_on_a_field_focuses_it_without_submitting() {
|
||||
let fonts = Fonts::load().unwrap();
|
||||
let mut ui = Ui::<Form>::new(Theme::WHIMSY_DARK);
|
||||
ui.key(Key::Next);
|
||||
let mut pm = tiny_skia::Pixmap::new(320, 200).unwrap();
|
||||
ui.draw(&mut pm.as_mut(), &fonts, 0);
|
||||
let view = ui.app.view();
|
||||
let m = crate::text::Measurer { fonts: &fonts, theme: T };
|
||||
let rects = layout(&view, Rect { x: 0.0, y: 0.0, w: 320.0, h: 200.0 }, &m, T);
|
||||
// The form, the field's column, its label, the field, the button.
|
||||
let (x, y) = middle(rects[3]);
|
||||
assert_eq!(ui.pointer_down(x, y), Outcome::Changed, "focus moves to the field");
|
||||
assert_eq!(ui.pointer_up(x, y), Outcome::Nothing);
|
||||
assert_eq!(ui.app.sent, None, "not submitted");
|
||||
ui.key(Key::Char('z'));
|
||||
assert_eq!(ui.app.name, "abz", "typing goes to it");
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ use crate::layout::{self, Rect};
|
||||
use crate::motion::{Ring, Tween};
|
||||
use crate::render::{self, Frame};
|
||||
use crate::tree::{event_at, input_at, pressables, toggle_at, whole_elements};
|
||||
|
||||
#[path = "pointer.rs"]
|
||||
mod pointer;
|
||||
use crate::text::{Fonts, Measurer};
|
||||
use crate::theme::Theme;
|
||||
|
||||
@@ -90,11 +93,15 @@ pub struct Ui<A: App> {
|
||||
whole: bool,
|
||||
/// Where the ring was last drawn.
|
||||
last_ring: Option<Rect>,
|
||||
/// Where each thing focus can rest on was last drawn, in order.
|
||||
hits: Vec<Rect>,
|
||||
/// What a pointer press went down on, until it is let go.
|
||||
held: Option<usize>,
|
||||
}
|
||||
|
||||
impl<A: App> Ui<A> {
|
||||
pub fn new(theme: Theme) -> Self {
|
||||
Self { app: A::init(), focus: 0, scroll: 0.0, caret: usize::MAX, theme, ring: None, flip: None, enter: None, drawn: None, whole: true, last_ring: None }
|
||||
Self { app: A::init(), focus: 0, scroll: 0.0, caret: usize::MAX, theme, ring: None, flip: None, enter: None, drawn: None, whole: true, last_ring: None, hits: Vec::new(), held: None }
|
||||
}
|
||||
|
||||
/// A new screen: from its top, coming in, the ring where it lands.
|
||||
@@ -247,6 +254,7 @@ impl<A: App> Ui<A> {
|
||||
(r.x, r.y) = (libm::roundf(r.x), libm::roundf(r.y));
|
||||
(r.w, r.h) = (x1 - r.x, y1 - r.y);
|
||||
}
|
||||
self.hits = self.pressables(&view).iter().filter_map(|i| rects.get(*i).copied()).collect();
|
||||
let dt = self.drawn.map_or(0.0, |d| now.saturating_sub(d).min(100) as f32 / 1000.0);
|
||||
self.drawn = Some(now);
|
||||
let target = focused.and_then(|i| rects.get(i)).copied();
|
||||
|
||||
Reference in New Issue
Block a user