Files
Graphite/desktop/src/input.rs
Timon d2bf8f99d2 Desktop: Mirror direct viewport input to the UI (#4502)
Desktop: Mirror direct-routed input to the UI
2026-09-06 01:29:19 +00:00

385 lines
13 KiB
Rust

use std::time::Instant;
use winit::dpi::PhysicalPosition;
use winit::event::{ButtonSource, ElementState, MouseButton, MouseScrollDelta, PointerSource, TabletToolData, TabletToolKind, WindowEvent};
use winit::keyboard::ModifiersState;
use crate::ui::{InputEvent, MULTICLICK_ALLOWED_TRAVEL, MULTICLICK_TIMEOUT, PINCH_ZOOM_SPEED, SCROLL_LINE_HEIGHT, SCROLL_LINE_WIDTH, SCROLL_SPEED_X, SCROLL_SPEED_Y};
use crate::wrapper::messages::{EditorPointerState, InputMessage, ModifierKeys, MouseKeys, ScrollDelta};
// Marks input event as observe-only, meaning it should not result in any input messages being send by the frontend.
// And should instead be ignored by frontend input processing and only be used to update the UI state (hover, cursor).
// TODO(Timon): Remove and find a less hacky solution
trait ObserveOnlyExt {
fn observe_only(self) -> Self;
}
impl ObserveOnlyExt for crate::ui::InputEventBuilder {
fn observe_only(self) -> Self {
self.num_lock(true)
}
}
pub(crate) struct InputState {
start: Instant,
viewport_info: Option<ViewportInfo>,
modifiers: ModifiersState,
pointer_position: PhysicalPosition<f64>,
pointer_state: PointerState,
click_tracker: ClickTracker,
direct_input: bool,
}
impl InputState {
pub(crate) fn new() -> Self {
Self {
start: Instant::now(),
viewport_info: None,
modifiers: ModifiersState::default(),
pointer_position: PhysicalPosition::default(),
pointer_state: PointerState::Hover { route: Route::Ui },
click_tracker: ClickTracker::default(),
direct_input: false,
}
}
pub(crate) fn set_viewport_info(&mut self, x: f64, y: f64, width: f64, height: f64, scale: f64) {
self.viewport_info = Some(ViewportInfo { x, y, width, height, scale });
}
pub(crate) fn set_direct_input(&mut self, enabled: bool) {
self.direct_input = enabled;
}
pub(crate) fn lock_pointer(&mut self) {
self.pointer_state = match self.pointer_state {
PointerState::Hover { route } => PointerState::Locked {
route,
keys: MouseKeys::empty(),
position: self.pointer_position,
},
PointerState::Stroke { route, keys } | PointerState::Locked { route, keys, .. } => PointerState::Locked {
route,
keys,
position: self.pointer_position,
},
};
}
pub(crate) fn unlock_pointer(&mut self) -> Option<PhysicalPosition<f64>> {
let PointerState::Locked {
route: resume,
keys,
position: restore,
} = self.pointer_state
else {
return None;
};
self.pointer_position = restore;
self.pointer_state = match keys.is_empty() {
true => PointerState::Hover { route: Route::Ui },
false => PointerState::Stroke { route: resume, keys },
};
Some(restore)
}
pub(crate) fn pointer_locked(&self) -> bool {
matches!(self.pointer_state, PointerState::Locked { .. })
}
pub(crate) fn modifiers(&self) -> ModifiersState {
self.modifiers
}
pub(crate) fn process(&mut self, event: &WindowEvent, mut editor_callback: impl FnMut(InputMessage), mut ui_callback: impl FnMut(InputEvent)) {
match event {
WindowEvent::PointerMoved { position, source, .. } => {
self.pointer_position = *position;
let route = match self.pointer_state {
PointerState::Hover { .. } => {
let next = self.route(*position);
self.pointer_state = PointerState::Hover { route: next };
next
}
PointerState::Stroke { route, .. } => route,
PointerState::Locked { keys, route: resume, .. } => match keys.is_empty() {
true => Route::Ui,
false => resume,
},
};
match route {
Route::Ui => ui_callback(InputEvent::pointer().position(*position).moved().modifiers(self.modifiers).build()),
Route::Editor => {
if !self.pointer_locked() {
ui_callback(InputEvent::pointer().position(*position).moved().modifiers(self.modifiers).observe_only().build());
}
editor_callback(InputMessage::PointerMove {
editor_mouse_state: match source {
PointerSource::TabletTool { kind, data } => self.tablet_pointer_state(kind, data),
_ => self.pointer_state(),
},
modifier_keys: self.modifier_keys(),
});
}
}
}
WindowEvent::PointerEntered { position, .. } => {
self.pointer_position = *position;
ui_callback(InputEvent::pointer().position(*position).entered().modifiers(self.modifiers).build())
}
WindowEvent::PointerLeft { position: Some(position), .. } => {
self.pointer_position = *position;
ui_callback(InputEvent::pointer().position(*position).exited().modifiers(self.modifiers).build())
}
WindowEvent::PointerLeft { position: None, .. } => ui_callback(InputEvent::pointer().exited().modifiers(self.modifiers).build()),
WindowEvent::PointerButton { state, button, position, .. } => {
self.pointer_position = *position;
let mouse_button = button.clone().mouse_button();
let keys = match mouse_button {
Some(MouseButton::Left) => MouseKeys::LEFT,
Some(MouseButton::Right) => MouseKeys::RIGHT,
Some(MouseButton::Middle) => MouseKeys::MIDDLE,
Some(MouseButton::Back) => MouseKeys::BACK,
Some(MouseButton::Forward) => MouseKeys::FORWARD,
_ => MouseKeys::NONE,
};
let (pointer, route) = match self.pointer_state {
PointerState::Hover { route } => match (state.is_pressed(), keys.is_empty()) {
(true, false) => {
let route = self.route(*position);
(PointerState::Stroke { route, keys }, route)
}
(true, true) => (PointerState::Hover { route }, self.route(*position)),
(false, _) => (PointerState::Hover { route }, route),
},
PointerState::Stroke { route, keys: mut held } => {
match state.is_pressed() {
true => held.insert(keys),
false => held.remove(keys),
}
match held.is_empty() {
true => (PointerState::Hover { route }, route),
false => (PointerState::Stroke { route, keys: held }, route),
}
}
PointerState::Locked { route, keys: mut held, position } => {
let resume = if state.is_pressed() && held.is_empty() { Route::Ui } else { route };
match state.is_pressed() {
true => held.insert(keys),
false => held.remove(keys),
}
(PointerState::Locked { route: resume, keys: held, position }, Route::Ui)
}
};
self.pointer_state = pointer;
let count = mouse_button.map_or(1, |button| self.click_tracker.input(*position, button, *state));
let back_or_forward = matches!(mouse_button, Some(MouseButton::Back | MouseButton::Forward));
let pointer = InputEvent::pointer().position(*position);
let input = match state {
ElementState::Pressed => pointer.pressed(button.clone(), count),
ElementState::Released => pointer.released(button.clone(), count),
}
.modifiers(self.modifiers);
if self.pointer_locked() || keys.is_empty() || !(back_or_forward || route == Route::Editor) {
ui_callback(input.build());
return;
}
if route == Route::Editor {
ui_callback(input.observe_only().build());
}
let editor_mouse_state = match button {
ButtonSource::TabletTool { kind, data, .. } => self.tablet_pointer_state(kind, data),
_ => self.pointer_state(),
};
let modifier_keys = self.modifier_keys();
match state {
ElementState::Pressed => editor_callback(InputMessage::PointerDown { editor_mouse_state, modifier_keys }),
ElementState::Released if count % 2 == 0 => {
editor_callback(InputMessage::PointerUp { editor_mouse_state, modifier_keys });
editor_callback(InputMessage::DoubleClick {
editor_mouse_state: EditorPointerState {
mouse_keys: keys,
..editor_mouse_state
},
modifier_keys,
});
}
ElementState::Released => editor_callback(InputMessage::PointerUp { editor_mouse_state, modifier_keys }),
}
}
WindowEvent::MouseWheel { delta, .. } => {
if self.pointer_locked() || self.ui_captures(self.pointer_position) {
let input = match delta {
MouseScrollDelta::LineDelta(x, y) => InputEvent::pointer().scrolled_lines(f64::from(*x), f64::from(*y)),
MouseScrollDelta::PixelDelta(position) => InputEvent::pointer().scrolled_pixels(position.x, position.y),
};
ui_callback(input.modifiers(self.modifiers).build());
return;
}
let (x, y) = match delta {
MouseScrollDelta::LineDelta(x, y) => (f64::from(*x) * SCROLL_LINE_WIDTH, f64::from(*y) * SCROLL_LINE_HEIGHT),
MouseScrollDelta::PixelDelta(position) => (position.x, position.y),
};
let scroll_delta = ScrollDelta::new(-x * SCROLL_SPEED_X, -y * SCROLL_SPEED_Y, 0.);
editor_callback(InputMessage::WheelScroll {
editor_mouse_state: EditorPointerState { scroll_delta, ..self.pointer_state() },
modifier_keys: self.modifier_keys(),
});
}
WindowEvent::PinchGesture { delta, .. } => {
if self.pointer_locked() || self.ui_captures(self.pointer_position) || !delta.is_normal() {
ui_callback(InputEvent::pointer().zoomed(*delta).modifiers(self.modifiers).build());
return;
}
// TODO: This is a temporary solution to handle pinch gestures, we should handle pinch gestures editor-side instead.
let scroll_delta = ScrollDelta::new(0., -delta * PINCH_ZOOM_SPEED, 0.);
editor_callback(InputMessage::WheelScroll {
editor_mouse_state: EditorPointerState { scroll_delta, ..self.pointer_state() },
modifier_keys: self.modifier_keys() | ModifierKeys::CONTROL,
});
}
WindowEvent::ModifiersChanged(modifiers) => {
self.modifiers = modifiers.state();
}
WindowEvent::KeyboardInput { event, .. } => ui_callback(InputEvent::key(event).modifiers(self.modifiers).build()),
_ => {}
}
}
fn scale(&self) -> f64 {
self.viewport_info.as_ref().map_or(1., |info| info.scale)
}
fn ui_captures(&self, position: PhysicalPosition<f64>) -> bool {
!self.direct_input || !self.viewport_info.as_ref().is_some_and(|info| info.contains(position))
}
fn route(&self, position: PhysicalPosition<f64>) -> Route {
if self.ui_captures(position) { Route::Ui } else { Route::Editor }
}
fn pointer_keys(&self) -> MouseKeys {
match self.pointer_state {
PointerState::Hover { .. } => MouseKeys::empty(),
PointerState::Stroke { keys, .. } | PointerState::Locked { keys, .. } => keys,
}
}
fn pointer_state(&self) -> EditorPointerState {
EditorPointerState {
editor_position: (self.pointer_position.x / self.scale(), self.pointer_position.y / self.scale()).into(),
mouse_keys: self.pointer_keys(),
time: Some(self.start.elapsed().as_secs_f64() * 1000.),
..Default::default()
}
}
fn tablet_pointer_state(&self, kind: &TabletToolKind, data: &TabletToolData) -> EditorPointerState {
EditorPointerState {
pressure: data.force.map(|force| force.normalized(None)),
tilt: data.clone().tilt().map(|tilt| (f64::from(tilt.x), f64::from(tilt.y)).into()),
twist: data.twist.map(f64::from),
wheel: data.tangential_force.map(f64::from),
eraser: matches!(kind, TabletToolKind::Eraser),
..self.pointer_state()
}
}
fn modifier_keys(&self) -> ModifierKeys {
let mut keys = ModifierKeys::empty();
keys.set(ModifierKeys::SHIFT, self.modifiers.shift_key());
keys.set(ModifierKeys::CONTROL, self.modifiers.control_key());
keys.set(ModifierKeys::ALT, self.modifiers.alt_key());
keys.set(ModifierKeys::META_OR_COMMAND, self.modifiers.meta_key());
keys
}
}
#[derive(Clone, Copy)]
enum PointerState {
Hover { route: Route },
Stroke { route: Route, keys: MouseKeys },
Locked { route: Route, keys: MouseKeys, position: PhysicalPosition<f64> },
}
#[derive(Clone, Copy, PartialEq)]
enum Route {
Ui,
Editor,
}
struct ViewportInfo {
x: f64,
y: f64,
width: f64,
height: f64,
scale: f64,
}
impl ViewportInfo {
fn contains(&self, position: PhysicalPosition<f64>) -> bool {
position.x >= self.x && position.y >= self.y && position.x <= self.x + self.width && position.y <= self.y + self.height
}
}
#[derive(Default)]
struct ClickTracker {
left: ClickChains,
right: ClickChains,
middle: ClickChains,
back: ClickChains,
forward: ClickChains,
}
impl ClickTracker {
fn input(&mut self, position: PhysicalPosition<f64>, button: MouseButton, state: ElementState) -> u32 {
let position = (position.x as i32, position.y as i32);
let clicks = match button {
MouseButton::Left => &mut self.left,
MouseButton::Right => &mut self.right,
MouseButton::Middle => &mut self.middle,
MouseButton::Back => &mut self.back,
MouseButton::Forward => &mut self.forward,
_ => return 1,
};
let chain = match state {
ElementState::Pressed => &mut clicks.down,
ElementState::Released => &mut clicks.up,
};
let now = Instant::now();
let count = match chain {
Some(previous) => {
let within_time = now.saturating_duration_since(previous.time) <= MULTICLICK_TIMEOUT;
let dx = position.0.abs_diff(previous.position.0) as usize;
let dy = position.1.abs_diff(previous.position.1) as usize;
let within_distance = dx <= MULTICLICK_ALLOWED_TRAVEL && dy <= MULTICLICK_ALLOWED_TRAVEL;
if within_time && within_distance { previous.count.saturating_add(1) } else { 1 }
}
None => 1,
};
*chain = Some(Click { time: now, position, count });
count
}
}
#[derive(Default)]
struct ClickChains {
down: Option<Click>,
up: Option<Click>,
}
struct Click {
time: Instant,
position: (i32, i32),
count: u32,
}