use teksilo_canvas::Point;
use teksilo_tokens::PointerKind;
use super::{PointerId, PointerInfo, PointerPhase, PointerSample};
use crate::widget_id::WidgetId;
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq)]
pub struct PointerEntry {
pub info: PointerInfo,
pub position: Point,
pub down_position: Point,
pub hovered: Option<WidgetId>,
pub captured_by: Option<WidgetId>,
pub hover_within: Vec<WidgetId>,
pub sequence: Option<crate::gesture::PointerSequence>,
pub last_accepted: Option<WidgetId>,
}
impl PointerEntry {
fn new(info: PointerInfo, position: Point) -> Self {
Self {
info,
position,
down_position: position,
hovered: None,
captured_by: None,
hover_within: Vec::new(),
sequence: None,
last_accepted: None,
}
}
pub fn hovers(&self) -> bool {
self.info.kind.hovers()
}
pub fn is_contacting(&self) -> bool {
!self.hovers() || !self.info.buttons.is_empty()
}
}
#[derive(Clone, Debug)]
pub struct PointerTable {
entries: Vec<PointerEntry>,
primary: Option<PointerId>,
hover_owner: Option<PointerId>,
cap: usize,
}
impl Default for PointerTable {
fn default() -> Self {
Self::new()
}
}
impl PointerTable {
pub const DEFAULT_CAP: usize = 10;
pub fn new() -> Self {
Self {
entries: Vec::new(),
primary: None,
hover_owner: None,
cap: Self::DEFAULT_CAP,
}
}
pub fn with_cap(cap: usize) -> Self {
Self {
cap: cap.max(1),
..Self::new()
}
}
pub fn cap(&self) -> usize {
self.cap
}
pub fn get(&self, id: PointerId) -> Option<&PointerEntry> {
self.entries.iter().find(|e| e.info.id == id)
}
pub fn get_mut(&mut self, id: PointerId) -> Option<&mut PointerEntry> {
self.entries.iter_mut().find(|e| e.info.id == id)
}
pub fn begin(&mut self, sample: &PointerSample) -> Option<PointerId> {
if !self.would_admit(&sample.pointer) {
return None;
}
self.admit(
sample.pointer,
sample.position,
sample.phase == PointerPhase::Down,
)
}
pub fn would_admit(&self, info: &PointerInfo) -> bool {
if self.contains(info.id) {
return true;
}
if info.palm {
crate::trace_input!(
Samples,
"dropping {:?}: the backend classified it as a palm",
info.id
);
return false;
}
if self.entries.len() >= self.cap {
crate::trace_input!(
Samples,
"dropping {:?}: the pointer table already holds {} pointers",
info.id,
self.cap
);
return false;
}
true
}
pub fn admit(
&mut self,
info: PointerInfo,
position: Point,
is_down: bool,
) -> Option<PointerId> {
let id = info.id;
if let Some(entry) = self.get_mut(id) {
entry.info = info;
entry.position = position;
if is_down {
entry.down_position = position;
}
self.elect();
return Some(id);
}
if !self.would_admit(&info) {
return None;
}
let mut entry = PointerEntry::new(info, position);
if is_down {
entry.down_position = position;
}
self.entries.push(entry);
self.elect();
Some(id)
}
pub fn end(&mut self, id: PointerId) -> Option<PointerEntry> {
let index = self.entries.iter().position(|e| e.info.id == id)?;
let entry = self.entries.remove(index);
self.elect();
Some(entry)
}
pub fn primary(&self) -> Option<&PointerEntry> {
self.primary.and_then(|id| self.get(id))
}
pub fn hover_owner(&self) -> Option<&PointerEntry> {
self.hover_owner.and_then(|id| self.get(id))
}
pub fn hover_owner_mut(&mut self) -> Option<&mut PointerEntry> {
let id = self.hover_owner?;
self.get_mut(id)
}
pub fn hover_owner_id(&self) -> Option<PointerId> {
self.hover_owner
}
pub fn primary_id(&self) -> Option<PointerId> {
self.primary
}
pub fn iter(&self) -> impl Iterator<Item = &PointerEntry> + '_ {
self.entries.iter()
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut PointerEntry> + '_ {
self.entries.iter_mut()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn contact_count(&self) -> usize {
self.entries.iter().filter(|e| e.is_contacting()).count()
}
pub fn contains(&self, id: PointerId) -> bool {
self.get(id).is_some()
}
pub fn retain_active(&mut self, arena: &crate::arena::WidgetArena) {
for entry in &mut self.entries {
if entry.hovered.is_some_and(|id| !arena.is_active(id)) {
entry.hovered = None;
}
if entry.captured_by.is_some_and(|id| !arena.is_active(id)) {
entry.captured_by = None;
}
entry.hover_within.retain(|id| arena.is_active(*id));
if entry.last_accepted.is_some_and(|id| !arena.is_active(id)) {
entry.last_accepted = None;
}
}
}
pub fn release_captures_of(&mut self, widget: WidgetId) {
for entry in &mut self.entries {
if entry.captured_by == Some(widget) {
entry.captured_by = None;
}
}
}
fn elect(&mut self) {
self.primary = self
.entries
.iter()
.find(|e| e.info.kind == PointerKind::Mouse)
.or_else(|| self.entries.iter().min_by_key(|e| e.info.id))
.map(|e| e.info.id);
let owner_still_valid = self
.hover_owner
.and_then(|id| self.get(id))
.is_some_and(|e| e.hovers());
if !owner_still_valid {
self.hover_owner = self
.entries
.iter()
.filter(|e| e.hovers())
.max_by_key(|e| e.info.id)
.map(|e| e.info.id);
}
}
pub fn claim_hover_owner(&mut self, id: PointerId) -> Option<PointerId> {
if !self.get(id).is_some_and(|e| e.hovers()) {
return None;
}
let previous = self.hover_owner;
if previous == Some(id) {
return None;
}
self.hover_owner = Some(id);
previous.filter(|prev| self.contains(*prev))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pointer::{EventTime, PointerAxes};
use teksilo_tokens::PenKind;
fn id(raw: u64) -> PointerId {
let alloc = crate::pointer::PointerIdAllocator::global();
let device = crate::pointer::BackendDeviceKey::new(0x7AB1E ^ raw);
let out = alloc.begin(device, raw);
alloc.end(device, raw);
out
}
fn touch(pid: PointerId) -> PointerInfo {
PointerInfo::touch(pid, EventTime::ZERO)
}
fn mouse() -> PointerInfo {
PointerInfo::mouse(EventTime::ZERO)
}
fn pen(pid: PointerId) -> PointerInfo {
let mut info = PointerInfo::touch(pid, EventTime::ZERO);
info.kind = PointerKind::Pen(PenKind::Pen);
info
}
fn sample(info: PointerInfo, phase: PointerPhase, at: Point) -> PointerSample {
PointerSample {
pointer: info,
phase,
position: at,
button: None,
modifiers: crate::event::Modifiers::NONE,
coalesced: Vec::new(),
}
}
#[test]
fn a_mouse_wins_the_primary_role_over_an_older_contact() {
let mut table = PointerTable::new();
let finger = id(1);
table.admit(touch(finger), Point::ZERO, true);
assert_eq!(table.primary_id(), Some(finger), "the only live pointer");
table.admit(mouse(), Point::new(5.0, 5.0), false);
assert_eq!(
table.primary_id(),
Some(PointerId::MOUSE),
"a mouse always wins the primary role"
);
}
#[test]
fn a_contact_is_never_the_hover_owner() {
let mut table = PointerTable::new();
let finger = id(2);
table.admit(touch(finger), Point::ZERO, true);
assert_eq!(table.hover_owner_id(), None, "a finger cannot hover");
assert_eq!(table.claim_hover_owner(finger), None);
assert_eq!(table.hover_owner_id(), None);
}
#[test]
fn a_pen_can_own_hover_and_displaces_the_mouse() {
let mut table = PointerTable::new();
table.admit(mouse(), Point::ZERO, false);
assert_eq!(table.hover_owner_id(), Some(PointerId::MOUSE));
let stylus = id(3);
table.admit(pen(stylus), Point::new(2.0, 2.0), false);
assert_eq!(table.hover_owner_id(), Some(PointerId::MOUSE));
assert_eq!(
table.claim_hover_owner(stylus),
Some(PointerId::MOUSE),
"the displaced owner is reported so it can be sent a leave"
);
assert_eq!(table.hover_owner_id(), Some(stylus));
}
#[test]
fn ending_the_hover_owner_falls_back_to_another_hovering_pointer() {
let mut table = PointerTable::new();
table.admit(mouse(), Point::ZERO, false);
let stylus = id(4);
table.admit(pen(stylus), Point::ZERO, false);
table.claim_hover_owner(stylus);
table.end(stylus);
assert_eq!(table.hover_owner_id(), Some(PointerId::MOUSE));
}
#[test]
fn captures_are_independent_per_pointer() {
let mut table = PointerTable::new();
let a = id(5);
let b = id(6);
table.admit(touch(a), Point::ZERO, true);
table.admit(touch(b), Point::new(50.0, 0.0), true);
let mut arena = crate::arena::WidgetArena::new();
let w1 = arena.insert(Box::new(crate::test_widgets::FillWidget::new()));
let w2 = arena.insert(Box::new(crate::test_widgets::FillWidget::new()));
table.get_mut(a).expect("a is live").captured_by = Some(w1);
table.get_mut(b).expect("b is live").captured_by = Some(w2);
table.end(a);
assert_eq!(
table.get(b).and_then(|e| e.captured_by),
Some(w2),
"ending one contact must leave the other's capture alone"
);
}
#[test]
fn the_eleventh_contact_is_refused() {
let mut table = PointerTable::new();
for n in 0..PointerTable::DEFAULT_CAP {
let pid = id(100 + n as u64);
assert_eq!(
table.begin(&sample(touch(pid), PointerPhase::Down, Point::ZERO)),
Some(pid),
"contact {} is within the cap",
n + 1
);
}
assert_eq!(table.len(), PointerTable::DEFAULT_CAP);
let overflow = id(200);
assert_eq!(
table.begin(&sample(touch(overflow), PointerPhase::Down, Point::ZERO)),
None,
"the eleventh contact must be refused, not evicted onto another"
);
assert_eq!(table.len(), PointerTable::DEFAULT_CAP);
}
#[test]
fn a_live_pointer_is_refreshed_even_at_the_cap() {
let mut table = PointerTable::with_cap(1);
let finger = id(7);
table.admit(touch(finger), Point::ZERO, true);
assert_eq!(
table.admit(touch(finger), Point::new(9.0, 9.0), false),
Some(finger),
"the cap bounds pointers, not samples"
);
assert_eq!(
table.get(finger).map(|e| e.position),
Some(Point::new(9.0, 9.0))
);
assert_eq!(
table.get(finger).map(|e| e.down_position),
Some(Point::ZERO),
"a move must not move the press origin"
);
}
#[test]
fn a_palm_is_refused() {
let mut table = PointerTable::new();
let mut info = touch(id(8));
info.palm = true;
assert_eq!(
table.begin(&sample(info, PointerPhase::Down, Point::ZERO)),
None
);
assert!(table.is_empty());
}
#[test]
fn contact_count_ignores_a_resting_mouse() {
let mut table = PointerTable::new();
table.admit(mouse(), Point::ZERO, false);
assert_eq!(
table.contact_count(),
0,
"a hovering mouse is not a contact"
);
let mut pressed = mouse();
pressed.buttons = crate::event::ButtonMask::PRIMARY;
table.admit(pressed, Point::ZERO, true);
assert_eq!(table.contact_count(), 1, "a held button is a contact");
table.admit(touch(id(9)), Point::ZERO, true);
assert_eq!(table.contact_count(), 2);
}
#[test]
fn retain_active_scrubs_dead_widget_references() {
let mut arena = crate::arena::WidgetArena::new();
let live = arena.insert(Box::new(crate::test_widgets::FillWidget::new()));
let dead = WidgetId::default();
let mut table = PointerTable::new();
let finger = id(10);
table.admit(touch(finger), Point::ZERO, true);
{
let entry = table.get_mut(finger).expect("finger is live");
entry.hovered = Some(dead);
entry.captured_by = Some(dead);
entry.hover_within = vec![live, dead];
}
table.retain_active(&arena);
let entry = table.get(finger).expect("finger is still live");
assert_eq!(entry.hovered, None);
assert_eq!(entry.captured_by, None);
assert_eq!(entry.hover_within, vec![live]);
}
#[test]
fn the_entry_keeps_the_samples_own_info() {
let mut table = PointerTable::new();
let stylus = id(11);
let mut info = pen(stylus);
info.axes = PointerAxes {
pressure: Some(0.4),
..PointerAxes::default()
};
table.admit(info, Point::new(1.0, 2.0), true);
assert_eq!(
table.get(stylus).map(|e| e.info.axes.pressure),
Some(Some(0.4))
);
}
}