use std::collections::HashMap;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use teksilo_canvas::Point;
use teksilo_core::event::{ButtonMask, Key, Modifiers, PointerButton, ScrollDelta, WidgetEvent};
use teksilo_core::gesture::{GestureEvent, TapEvent};
use teksilo_core::pointer::{
BackendDeviceKey, EventTime, PointerId, PointerIdAllocator, PointerInfo, PointerPhase,
PointerSample, ScrollPhase, ScrollSample, ScrollSource,
};
use teksilo_core::trace_input;
use teksilo_tokens::{InputTokens, PenKind, PointerKind};
use crate::pen::{PenBatching, PenButtons, PenPacket, PenSource, back_date_into};
use crate::pointer_backend::{
BackendCaps, BackendEvent, InputSample, PlatformKind, PointerBackend,
};
use crate::window_system::WindowSystem;
const PHANTOM_SLOP: f32 = 1.0;
const PHANTOM_LIFT_WINDOW: Duration = Duration::from_millis(150);
const PROMOTED_CLICK_WINDOW: Duration = Duration::from_millis(500);
const MOMENTUM_HANDOFF_WINDOW: Duration = Duration::from_millis(100);
const PEN_DEVICE: BackendDeviceKey = BackendDeviceKey::new(0x7065_6E5F_0000_0001);
static NEXT_PEN_SESSION: AtomicU64 = AtomicU64::new(1);
#[derive(Copy, Clone, Debug)]
struct Contact {
id: PointerId,
position: Point,
primary: bool,
}
#[derive(Copy, Clone, Debug)]
struct PenContact {
id: PointerId,
session: u64,
tool: PenKind,
position: Point,
down: bool,
buttons: PenButtons,
primary: bool,
}
#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
enum ScrollStreamState {
#[default]
Idle,
InGesture,
InMomentum,
}
#[derive(Debug)]
pub struct TranslationState {
scale_factor: f64,
cursor_position: Option<Point>,
current_modifiers: Modifiers,
window_system: WindowSystem,
input: InputTokens,
now: EventTime,
contacts: HashMap<(BackendDeviceKey, u64), Contact>,
last_lift: Option<(Point, EventTime)>,
mouse_buttons: Vec<PointerButton>,
scroll_state: ScrollStreamState,
scroll_ended_at: Option<EventTime>,
warned_press_without_cursor: bool,
pen: Option<Box<dyn PenSource>>,
pen_contact: Option<PenContact>,
pen_scratch: Vec<PenPacket>,
pen_times: Vec<EventTime>,
pen_device_times: Vec<Option<u32>>,
pen_batching: PenBatching,
}
#[deny(missing_docs, clippy::missing_docs_in_private_items)]
impl TranslationState {
pub fn new() -> Self {
Self {
scale_factor: 1.0,
cursor_position: None,
current_modifiers: Modifiers::NONE,
window_system: WindowSystem::Unknown,
input: InputTokens::default(),
now: EventTime::ZERO,
contacts: HashMap::new(),
last_lift: None,
mouse_buttons: Vec::new(),
scroll_state: ScrollStreamState::default(),
scroll_ended_at: None,
warned_press_without_cursor: false,
pen: None,
pen_contact: None,
pen_scratch: Vec::new(),
pen_times: Vec::new(),
pen_device_times: Vec::new(),
pen_batching: PenBatching::default(),
}
}
pub fn set_scale_factor(&mut self, factor: f64) {
self.scale_factor = factor;
}
pub fn scale_factor(&self) -> f64 {
self.scale_factor
}
pub fn cursor_position(&self) -> Option<Point> {
self.cursor_position
}
pub fn set_modifiers(&mut self, modifiers: Modifiers) {
self.current_modifiers = modifiers;
}
pub fn modifiers(&self) -> Modifiers {
self.current_modifiers
}
pub fn set_window_system(&mut self, window_system: WindowSystem) {
self.window_system = window_system;
}
pub fn window_system(&self) -> WindowSystem {
self.window_system
}
pub fn set_input_tokens(&mut self, input: InputTokens) {
self.input = input;
}
pub fn input_tokens(&self) -> &InputTokens {
&self.input
}
pub fn set_now(&mut self, now: EventTime) {
if now > self.now {
self.now = now;
}
}
pub fn now(&self) -> EventTime {
self.now
}
pub fn live_contact_count(&self) -> usize {
self.contacts.len()
}
fn platform(&self) -> PlatformKind {
match self.window_system {
WindowSystem::X11 | WindowSystem::Wayland => PlatformKind::Unix,
WindowSystem::Unknown => PlatformKind::HOST,
}
}
fn promotes_touch_to_mouse(&self) -> bool {
BackendCaps::for_platform(self.platform(), self.window_system).synthesises_mouse_from_touch
}
fn is_phantom_motion(&self, position: Point) -> bool {
if !self.promotes_touch_to_mouse() {
return false;
}
if !self.contacts.is_empty() {
return true;
}
match self.last_lift {
Some((lift, at)) if self.now.saturating_since(at) <= PHANTOM_LIFT_WINDOW => {
near(lift, position, PHANTOM_SLOP)
}
_ => false,
}
}
fn is_promoted_click(&self) -> bool {
if !self.promotes_touch_to_mouse() {
return false;
}
if !self.contacts.is_empty() {
return true;
}
matches!(
self.last_lift,
Some((_, at)) if self.now.saturating_since(at) <= PROMOTED_CLICK_WINDOW
)
}
fn mouse_pointer(&self) -> PointerInfo {
let mut info = PointerInfo::mouse(self.now);
info.buttons = self
.mouse_buttons
.iter()
.fold(ButtonMask::NONE, |mask, b| mask.union((*b).into()));
info
}
fn translate_touch(&mut self, touch: &winit::event::Touch) -> Option<PointerSample> {
if !self.input.touch_enabled {
trace_input!(
Samples,
"touch dropped: touch_enabled=false (os id {})",
touch.id
);
return None;
}
let device = device_key(touch.device_id);
let key = (device, touch.id);
let position = Point::new(
(touch.location.x / self.scale_factor) as f32,
(touch.location.y / self.scale_factor) as f32,
);
let (phase, id, primary) = match touch.phase {
winit::event::TouchPhase::Started => {
let id = PointerIdAllocator::global().begin(device, touch.id);
let primary = self.contacts.is_empty();
self.contacts.insert(
key,
Contact {
id,
position,
primary,
},
);
(PointerPhase::Down, id, primary)
}
winit::event::TouchPhase::Moved => {
let contact = self.contacts.get_mut(&key)?;
contact.position = position;
(PointerPhase::Move, contact.id, contact.primary)
}
winit::event::TouchPhase::Ended | winit::event::TouchPhase::Cancelled => {
let contact = self.contacts.remove(&key)?;
PointerIdAllocator::global().end(device, touch.id);
self.last_lift = Some((position, self.now));
let phase = if matches!(touch.phase, winit::event::TouchPhase::Ended) {
PointerPhase::Up
} else {
PointerPhase::Cancel
};
(phase, contact.id, contact.primary)
}
};
let mut pointer = PointerInfo::touch(id, self.now);
pointer.primary = primary;
pointer.buttons = match phase {
PointerPhase::Down | PointerPhase::Move => ButtonMask::PRIMARY,
PointerPhase::Up | PointerPhase::Cancel => ButtonMask::NONE,
};
pointer.axes.pressure = touch.force.and_then(pressure_from_force);
pointer.axes.contact = self
.pen
.as_ref()
.and_then(|source| source.touch_contact(touch.id));
let button = match phase {
PointerPhase::Down | PointerPhase::Up => Some(PointerButton::Primary),
_ => None,
};
trace_input!(
Samples,
"touch {:?} {:?} os_id={} at {:?}",
phase,
id,
touch.id,
position
);
Some(PointerSample {
pointer,
phase,
position,
button,
modifiers: self.current_modifiers,
coalesced: Vec::new(),
})
}
pub fn set_pen_source(&mut self, source: Box<dyn PenSource>) {
self.pen = Some(source);
}
pub fn take_pen_source(&mut self) -> Option<Box<dyn PenSource>> {
self.pen.take()
}
pub fn has_pen_source(&self) -> bool {
self.pen.is_some()
}
pub fn pen_polls_off_thread(&self) -> bool {
self.pen.as_ref().is_some_and(|p| p.polls_off_thread())
}
pub fn pen_in_proximity(&self) -> bool {
self.pen_contact.is_some()
}
pub fn pen_batching(&self) -> PenBatching {
self.pen_batching
}
pub fn set_pen_batching(&mut self, mode: PenBatching) {
self.pen_batching = mode;
}
pub fn poll_pen(&mut self, now: EventTime) -> Vec<InputSample> {
let floor = self.now;
self.set_now(now);
let Some(mut source) = self.pen.take() else {
return Vec::new();
};
let mut packets = std::mem::take(&mut self.pen_scratch);
packets.clear();
source.poll(&mut packets);
self.pen = Some(source);
let mut device_times = std::mem::take(&mut self.pen_device_times);
device_times.clear();
device_times.extend(packets.iter().map(|packet| packet.device_time_ms));
let mut times = std::mem::take(&mut self.pen_times);
back_date_into(self.now, &device_times, &mut times);
let mut samples = Vec::new();
let mut pinned: Vec<usize> = Vec::new();
for (packet, time) in packets.iter().zip(times.iter().copied()) {
let id_before = self.pen_contact.map(|contact| contact.id);
let start = samples.len();
samples.append(&mut self.translate_pen_packet(packet, time.max(floor)));
if let Some(fresh) = self.pen_contact.map(|contact| contact.id)
&& Some(fresh) != id_before
&& let Some(offset) = samples[start..].iter().position(
|sample| matches!(sample, InputSample::Pointer(p) if p.pointer.id == fresh),
)
{
pinned.push(start + offset);
}
}
if self.pen_batching == PenBatching::Coalesce {
coalesce_pen_moves(&mut samples, &pinned);
}
packets.clear();
self.pen_scratch = packets;
device_times.clear();
self.pen_device_times = device_times;
times.clear();
self.pen_times = times;
samples
}
pub fn translate_pen_packet(
&mut self,
packet: &PenPacket,
time: EventTime,
) -> Vec<InputSample> {
let mut samples = Vec::new();
if let Some(contact) = self.pen_contact
&& (!packet.in_proximity || contact.tool != packet.tool)
{
samples.push(self.end_pen_session(contact, packet.position, time));
}
if !packet.in_proximity {
return samples;
}
let (mut state, just_entered) = match self.pen_contact {
Some(contact) => (contact, false),
None => {
let contact = self.begin_pen_session(packet);
samples.push(self.pen_sample(&contact, PointerPhase::Move, None, packet, time));
(contact, true)
}
};
let mut transitions: Vec<(PointerPhase, PointerButton)> = Vec::new();
if state.down != packet.down {
transitions.push((
if packet.down {
PointerPhase::Down
} else {
PointerPhase::Up
},
PointerButton::Primary,
));
}
for (bit, button) in [
(PenButtons::BARREL, PointerButton::Secondary),
(PenButtons::SECONDARY_BARREL, PointerButton::Middle),
] {
let was = state.buttons.contains(bit);
let held = packet.buttons.contains(bit);
if was != held {
transitions.push((
if held {
PointerPhase::Down
} else {
PointerPhase::Up
},
button,
));
}
}
let moved = state.position != packet.position;
state.position = packet.position;
if !just_entered && (moved || transitions.is_empty()) {
samples.push(self.pen_sample(&state, PointerPhase::Move, None, packet, time));
}
for (phase, button) in transitions {
let pressed = phase == PointerPhase::Down;
match button {
PointerButton::Primary => state.down = pressed,
PointerButton::Secondary => {
state.buttons = state.buttons.with(PenButtons::BARREL, pressed);
}
_ => {
state.buttons = state.buttons.with(PenButtons::SECONDARY_BARREL, pressed);
}
}
samples.push(self.pen_sample(&state, phase, Some(button), packet, time));
}
state.buttons = packet.buttons;
state.down = packet.down;
self.pen_contact = Some(state);
samples
}
fn begin_pen_session(&mut self, packet: &PenPacket) -> PenContact {
let session = NEXT_PEN_SESSION.fetch_add(1, Ordering::Relaxed);
let id = PointerIdAllocator::global().begin(PEN_DEVICE, session);
let contact = PenContact {
id,
session,
tool: packet.tool,
position: packet.position,
down: false,
buttons: PenButtons::NONE,
primary: self.contacts.is_empty(),
};
trace_input!(
Samples,
"pen {:?} in proximity ({:?}) at {:?}",
id,
packet.tool,
packet.position
);
self.pen_contact = Some(contact);
contact
}
fn end_pen_session(
&mut self,
contact: PenContact,
position: Point,
time: EventTime,
) -> InputSample {
PointerIdAllocator::global().end(PEN_DEVICE, contact.session);
self.pen_contact = None;
trace_input!(Samples, "pen {:?} left proximity", contact.id);
let mut pointer = PointerInfo::touch(contact.id, time);
pointer.kind = PointerKind::Pen(contact.tool);
pointer.primary = contact.primary;
pointer.buttons = ButtonMask::NONE;
InputSample::Pointer(PointerSample {
pointer,
phase: PointerPhase::Cancel,
position,
button: None,
modifiers: self.current_modifiers,
coalesced: Vec::new(),
})
}
fn pen_sample(
&self,
contact: &PenContact,
phase: PointerPhase,
button: Option<PointerButton>,
packet: &PenPacket,
time: EventTime,
) -> InputSample {
let mut pointer = PointerInfo::touch(contact.id, time);
pointer.kind = PointerKind::Pen(contact.tool);
pointer.primary = contact.primary;
pointer.buttons = pen_button_mask(contact.down, contact.buttons);
pointer.axes.pressure = Some(packet.pressure.clamp(0.0, 1.0));
pointer.axes.tilt = packet.tilt;
pointer.axes.twist = packet.twist;
InputSample::Pointer(PointerSample {
pointer,
phase,
position: contact.position,
button,
modifiers: self.current_modifiers,
coalesced: Vec::new(),
})
}
fn translate_scroll(
&mut self,
delta: winit::event::MouseScrollDelta,
winit_phase: winit::event::TouchPhase,
) -> ScrollSample {
use winit::event::TouchPhase;
let in_handoff = matches!(
self.scroll_ended_at,
Some(at) if self.now.saturating_since(at) <= MOMENTUM_HANDOFF_WINDOW
);
let phase = match (winit_phase, self.scroll_state) {
(TouchPhase::Started, _) if in_handoff => {
self.scroll_state = ScrollStreamState::InMomentum;
ScrollPhase::Momentum
}
(TouchPhase::Started, _) => {
self.scroll_state = ScrollStreamState::InGesture;
self.scroll_ended_at = None;
ScrollPhase::Began
}
(TouchPhase::Moved, ScrollStreamState::InGesture) => ScrollPhase::Changed,
(TouchPhase::Moved, ScrollStreamState::InMomentum) => ScrollPhase::Momentum,
(TouchPhase::Moved, ScrollStreamState::Idle) => ScrollPhase::Discrete,
(TouchPhase::Ended, ScrollStreamState::InGesture) => {
self.scroll_state = ScrollStreamState::Idle;
self.scroll_ended_at = Some(self.now);
ScrollPhase::Ended
}
(TouchPhase::Ended, ScrollStreamState::InMomentum) => {
self.scroll_state = ScrollStreamState::Idle;
self.scroll_ended_at = None;
ScrollPhase::MomentumEnded
}
(TouchPhase::Ended, ScrollStreamState::Idle) => ScrollPhase::Discrete,
(TouchPhase::Cancelled, _) => {
self.scroll_state = ScrollStreamState::Idle;
self.scroll_ended_at = None;
ScrollPhase::Cancelled
}
};
let (scroll_delta, source) = self.scroll_delta(delta);
trace_input!(
Samples,
"scroll {:?} {:?}/{:?}",
scroll_delta,
phase,
source
);
ScrollSample {
delta: scroll_delta,
position: None,
phase,
source,
pointer: self.mouse_pointer(),
modifiers: self.current_modifiers,
}
}
fn scroll_delta(&self, delta: winit::event::MouseScrollDelta) -> (ScrollDelta, ScrollSource) {
match delta {
winit::event::MouseScrollDelta::LineDelta(x, y) => (
ScrollDelta::Lines {
x: -x * self.input.lines_per_notch,
y: -y * self.input.lines_per_notch,
},
ScrollSource::Wheel,
),
winit::event::MouseScrollDelta::PixelDelta(pos) => (
ScrollDelta::Pixels {
x: -(pos.x / self.scale_factor) as f32,
y: -(pos.y / self.scale_factor) as f32,
},
ScrollSource::Trackpad,
),
}
}
}
impl Default for TranslationState {
fn default() -> Self {
Self::new()
}
}
impl PointerBackend for TranslationState {
fn translate(&mut self, event: &BackendEvent<'_>, now: EventTime) -> Vec<InputSample> {
self.set_now(now);
let BackendEvent::Winit(event) = *event;
use winit::event::WindowEvent as WE;
match event {
WE::CursorMoved { position, .. } => {
let logical = Point::new(
(position.x / self.scale_factor) as f32,
(position.y / self.scale_factor) as f32,
);
if self.is_phantom_motion(logical) {
trace_input!(
Samples,
"cursor move suppressed (emulated) at {:?}",
logical
);
return Vec::new();
}
self.cursor_position = Some(logical);
let mut sample = PointerSample::mouse(PointerPhase::Move, logical, self.now);
sample.pointer = self.mouse_pointer();
sample.modifiers = self.current_modifiers;
vec![InputSample::Pointer(sample)]
}
WE::MouseInput { state, button, .. } => {
let Some(button) = translate_mouse_button(*button) else {
return Vec::new();
};
let Some(position) = self.cursor_position else {
self.note_press_without_cursor();
return Vec::new();
};
if self.is_promoted_click() {
trace_input!(
Samples,
"mouse {:?} suppressed (promoted from touch)",
button
);
return Vec::new();
}
let phase = match state {
winit::event::ElementState::Pressed => {
if !self.mouse_buttons.contains(&button) {
self.mouse_buttons.push(button);
}
PointerPhase::Down
}
winit::event::ElementState::Released => {
self.mouse_buttons.retain(|b| *b != button);
PointerPhase::Up
}
};
let mut sample = PointerSample::mouse(phase, position, self.now);
sample.pointer = self.mouse_pointer();
sample.button = Some(button);
sample.modifiers = self.current_modifiers;
vec![InputSample::Pointer(sample)]
}
WE::MouseWheel { delta, phase, .. } => {
vec![InputSample::Scroll(self.translate_scroll(*delta, *phase))]
}
WE::Touch(touch) => self
.translate_touch(touch)
.map(InputSample::Pointer)
.into_iter()
.collect(),
WE::PinchGesture { delta, phase, .. } => {
vec![InputSample::Gesture(pinch_gesture(
*delta,
*phase,
self.cursor_position.unwrap_or(Point::ZERO),
))]
}
WE::RotationGesture { delta, phase, .. } => {
vec![InputSample::Gesture(rotation_gesture(
*delta,
*phase,
self.cursor_position.unwrap_or(Point::ZERO),
))]
}
WE::DoubleTapGesture { .. } => vec![InputSample::Gesture(double_tap_gesture(
self.cursor_position.unwrap_or(Point::ZERO),
self.current_modifiers,
))],
_ => Vec::new(),
}
}
fn capabilities(&self) -> BackendCaps {
let mut caps = BackendCaps::for_platform(self.platform(), self.window_system);
if let Some(pen) = &self.pen {
pen.capabilities().apply_to(&mut caps);
}
caps
}
fn cancel_all(&mut self, now: EventTime) -> Vec<InputSample> {
self.set_now(now);
let mut contacts: Vec<((BackendDeviceKey, u64), Contact)> = self.contacts.drain().collect();
contacts.sort_by_key(|(_, contact)| contact.id);
let mut samples: Vec<InputSample> = Vec::with_capacity(contacts.len() + 1);
for ((device, os_id), contact) in contacts {
PointerIdAllocator::global().end(device, os_id);
let mut pointer = PointerInfo::touch(contact.id, self.now);
pointer.primary = contact.primary;
pointer.buttons = ButtonMask::NONE;
trace_input!(Samples, "cancel_all {:?}", contact.id);
samples.push(InputSample::Pointer(PointerSample {
pointer,
phase: PointerPhase::Cancel,
position: contact.position,
button: None,
modifiers: self.current_modifiers,
coalesced: Vec::new(),
}));
}
if let Some(contact) = self.pen_contact {
let position = contact.position;
samples.push(self.end_pen_session(contact, position, self.now));
}
if !self.mouse_buttons.is_empty()
&& let Some(position) = self.cursor_position
{
self.mouse_buttons.clear();
let mut sample = PointerSample::mouse(PointerPhase::Cancel, position, self.now);
sample.modifiers = self.current_modifiers;
samples.push(InputSample::Pointer(sample));
}
samples
}
}
impl TranslationState {
fn note_press_without_cursor(&mut self) {
if !self.warned_press_without_cursor {
self.warned_press_without_cursor = true;
trace_input!(
Samples,
"mouse button dropped: no cursor position yet (was dispatched at the \
window origin before P15)"
);
}
}
}
fn device_key(device_id: winit::event::DeviceId) -> BackendDeviceKey {
let mut hasher = DefaultHasher::new();
device_id.hash(&mut hasher);
BackendDeviceKey::new(hasher.finish())
}
fn pen_button_mask(down: bool, buttons: PenButtons) -> ButtonMask {
let mut mask = ButtonMask::NONE;
if down {
mask = mask.union(PointerButton::Primary.into());
}
if buttons.contains(PenButtons::BARREL) {
mask = mask.union(PointerButton::Secondary.into());
}
if buttons.contains(PenButtons::SECONDARY_BARREL) {
mask = mask.union(PointerButton::Middle.into());
}
mask
}
fn near(a: Point, b: Point, slop: f32) -> bool {
(a.x - b.x).abs() <= slop && (a.y - b.y).abs() <= slop
}
fn pressure_from_force(force: winit::event::Force) -> Option<f32> {
let normalized = match force {
winit::event::Force::Normalized(value) => value,
winit::event::Force::Calibrated {
force,
max_possible_force,
altitude_angle,
} => {
if max_possible_force <= 0.0 {
return None;
}
let perpendicular = match altitude_angle {
Some(angle) => {
let sin = angle.sin();
if sin <= f64::EPSILON {
return None;
}
force / sin
}
None => force,
};
perpendicular / max_possible_force
}
};
if !normalized.is_finite() {
return None;
}
Some((normalized as f32).clamp(0.0, 1.0))
}
pub fn translate_cursor_moved(
physical_x: f64,
physical_y: f64,
state: &mut TranslationState,
) -> Option<WidgetEvent> {
let logical_x = (physical_x / state.scale_factor) as f32;
let logical_y = (physical_y / state.scale_factor) as f32;
let position = Point::new(logical_x, logical_y);
if state.is_phantom_motion(position) {
return None;
}
state.cursor_position = Some(position);
let pointer = state.mouse_pointer();
Some(WidgetEvent::PointerMove {
position,
modifiers: state.current_modifiers,
pointer,
})
}
pub fn translate_ime(ime: winit::event::Ime) -> Option<WidgetEvent> {
match ime {
winit::event::Ime::Preedit(text, cursor) => Some(WidgetEvent::ImeComposition {
text,
cursor: cursor.map(|(begin, end)| begin..end),
}),
winit::event::Ime::Commit(text) => Some(WidgetEvent::ImeCommit { text }),
winit::event::Ime::Enabled | winit::event::Ime::Disabled => None,
}
}
pub fn translate_mouse_button(button: winit::event::MouseButton) -> Option<PointerButton> {
match button {
winit::event::MouseButton::Left => Some(PointerButton::Primary),
winit::event::MouseButton::Right => Some(PointerButton::Secondary),
winit::event::MouseButton::Middle => Some(PointerButton::Middle),
winit::event::MouseButton::Back => Some(PointerButton::Back),
winit::event::MouseButton::Forward => Some(PointerButton::Forward),
_ => None,
}
}
pub fn translate_mouse_input(
button_state: winit::event::ElementState,
button: winit::event::MouseButton,
state: &TranslationState,
) -> Option<WidgetEvent> {
let pointer_button = translate_mouse_button(button)?;
let position = state.cursor_position?;
if state.is_promoted_click() {
return None;
}
let pointer = state.mouse_pointer();
match button_state {
winit::event::ElementState::Pressed => Some(WidgetEvent::PointerDown {
position,
button: pointer_button,
modifiers: state.current_modifiers,
pointer,
}),
winit::event::ElementState::Released => Some(WidgetEvent::PointerUp {
position,
button: pointer_button,
modifiers: state.current_modifiers,
pointer,
}),
}
}
pub fn translate_modifiers(mods: winit::keyboard::ModifiersState) -> Modifiers {
let mut result = Modifiers::NONE;
if mods.control_key() {
result = result | Modifiers::CTRL;
}
if mods.shift_key() {
result = result | Modifiers::SHIFT;
}
if mods.alt_key() {
result = result | Modifiers::ALT;
}
if mods.super_key() {
result = result | Modifiers::SUPER;
}
result
}
pub fn translate_key(key: &winit::keyboard::Key) -> Option<Key> {
match key {
winit::keyboard::Key::Named(named) => translate_named_key(*named),
winit::keyboard::Key::Character(c) => {
let ch = c.chars().next()?;
match ch.to_ascii_uppercase() {
'A' => Some(Key::A),
'B' => Some(Key::B),
'C' => Some(Key::C),
'D' => Some(Key::D),
'E' => Some(Key::E),
'F' => Some(Key::F),
'G' => Some(Key::G),
'H' => Some(Key::H),
'I' => Some(Key::I),
'J' => Some(Key::J),
'K' => Some(Key::K),
'L' => Some(Key::L),
'M' => Some(Key::M),
'N' => Some(Key::N),
'O' => Some(Key::O),
'P' => Some(Key::P),
'Q' => Some(Key::Q),
'R' => Some(Key::R),
'S' => Some(Key::S),
'T' => Some(Key::T),
'U' => Some(Key::U),
'V' => Some(Key::V),
'W' => Some(Key::W),
'X' => Some(Key::X),
'Y' => Some(Key::Y),
'Z' => Some(Key::Z),
_ => Some(Key::Character(ch)),
}
}
_ => None,
}
}
fn translate_named_key(key: winit::keyboard::NamedKey) -> Option<Key> {
use winit::keyboard::NamedKey;
match key {
NamedKey::Space => Some(Key::Space),
NamedKey::Enter => Some(Key::Enter),
NamedKey::Escape => Some(Key::Escape),
NamedKey::Tab => Some(Key::Tab),
NamedKey::Backspace => Some(Key::Backspace),
NamedKey::Delete => Some(Key::Delete),
NamedKey::Insert => Some(Key::Insert),
NamedKey::ArrowUp => Some(Key::ArrowUp),
NamedKey::ArrowDown => Some(Key::ArrowDown),
NamedKey::ArrowLeft => Some(Key::ArrowLeft),
NamedKey::ArrowRight => Some(Key::ArrowRight),
NamedKey::Home => Some(Key::Home),
NamedKey::End => Some(Key::End),
NamedKey::PageUp => Some(Key::PageUp),
NamedKey::PageDown => Some(Key::PageDown),
NamedKey::F1 => Some(Key::F1),
NamedKey::F2 => Some(Key::F2),
NamedKey::F3 => Some(Key::F3),
NamedKey::F4 => Some(Key::F4),
NamedKey::F5 => Some(Key::F5),
NamedKey::F6 => Some(Key::F6),
NamedKey::F7 => Some(Key::F7),
NamedKey::F8 => Some(Key::F8),
NamedKey::F9 => Some(Key::F9),
NamedKey::F10 => Some(Key::F10),
NamedKey::F11 => Some(Key::F11),
NamedKey::F12 => Some(Key::F12),
NamedKey::F13 => Some(Key::F13),
NamedKey::F14 => Some(Key::F14),
NamedKey::F15 => Some(Key::F15),
NamedKey::F16 => Some(Key::F16),
NamedKey::F17 => Some(Key::F17),
NamedKey::F18 => Some(Key::F18),
NamedKey::F19 => Some(Key::F19),
NamedKey::F20 => Some(Key::F20),
NamedKey::F21 => Some(Key::F21),
NamedKey::F22 => Some(Key::F22),
NamedKey::F23 => Some(Key::F23),
NamedKey::F24 => Some(Key::F24),
NamedKey::CapsLock => Some(Key::CapsLock),
NamedKey::ContextMenu => Some(Key::ContextMenu),
_ => None,
}
}
pub fn translate_mouse_wheel(
delta: winit::event::MouseScrollDelta,
_phase: winit::event::TouchPhase,
state: &TranslationState,
) -> Option<WidgetEvent> {
let (scroll_delta, _) = state.scroll_delta(delta);
Some(WidgetEvent::scroll(scroll_delta, state.current_modifiers))
}
fn pinch_gesture(delta: f64, phase: winit::event::TouchPhase, center: Point) -> GestureEvent {
match phase {
winit::event::TouchPhase::Started => GestureEvent::PinchStarted { center },
winit::event::TouchPhase::Moved => GestureEvent::PinchChanged {
center,
scale: 1.0 + delta as f32,
rotation: 0.0,
},
winit::event::TouchPhase::Ended | winit::event::TouchPhase::Cancelled => {
GestureEvent::PinchEnded
}
}
}
fn rotation_gesture(
delta_degrees: f32,
phase: winit::event::TouchPhase,
center: Point,
) -> GestureEvent {
match phase {
winit::event::TouchPhase::Started => GestureEvent::PinchStarted { center },
winit::event::TouchPhase::Moved => GestureEvent::PinchChanged {
center,
scale: 1.0,
rotation: delta_degrees.to_radians(),
},
winit::event::TouchPhase::Ended | winit::event::TouchPhase::Cancelled => {
GestureEvent::PinchEnded
}
}
}
fn double_tap_gesture(position: Point, modifiers: Modifiers) -> GestureEvent {
GestureEvent::DoubleTap(TapEvent::new(position, PointerButton::Primary, modifiers))
}
pub fn translate_pinch_gesture(
delta: f64,
phase: winit::event::TouchPhase,
state: &TranslationState,
) -> Option<WidgetEvent> {
let center = state.cursor_position.unwrap_or(Point::ZERO);
Some(WidgetEvent::Gesture {
gesture: pinch_gesture(delta, phase, center),
})
}
pub fn translate_rotation_gesture(
delta_degrees: f32,
phase: winit::event::TouchPhase,
state: &TranslationState,
) -> Option<WidgetEvent> {
let center = state.cursor_position.unwrap_or(Point::ZERO);
Some(WidgetEvent::Gesture {
gesture: rotation_gesture(delta_degrees, phase, center),
})
}
pub fn translate_double_tap_gesture(state: &TranslationState) -> Option<WidgetEvent> {
let position = state.cursor_position.unwrap_or(Point::ZERO);
Some(WidgetEvent::Gesture {
gesture: double_tap_gesture(position, state.current_modifiers),
})
}
fn coalesce_pen_moves(samples: &mut Vec<InputSample>, pinned: &[usize]) {
let foldable = |index: usize, sample: &InputSample| {
matches!(
sample,
InputSample::Pointer(p) if p.phase == PointerPhase::Move && p.button.is_none()
) && !pinned.contains(&index)
};
if !samples
.windows(2)
.enumerate()
.any(|(i, w)| foldable(i, &w[0]) && foldable(i + 1, &w[1]))
{
return;
}
let mut out: Vec<(usize, InputSample)> = Vec::with_capacity(samples.len());
for (index, sample) in samples.drain(..).enumerate() {
if !foldable(index, &sample) {
out.push((index, sample));
continue;
}
let InputSample::Pointer(mut current) = sample else {
unreachable!("`foldable` matched a pointer sample")
};
if out.last().is_some_and(|(i, s)| foldable(*i, s)) {
let Some((_, InputSample::Pointer(previous))) = out.pop() else {
unreachable!("just matched")
};
let mut merged = previous.coalesced;
merged.push(
teksilo_core::CoalescedSample::new(previous.pointer.time, previous.position)
.with_axes(previous.pointer.axes),
);
merged.append(&mut current.coalesced);
current.coalesced = merged;
}
out.push((index, InputSample::Pointer(current)));
}
*samples = out.into_iter().map(|(_, sample)| sample).collect();
}
#[cfg(test)]
mod tests;