mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
Desktop: Make input routing aware of floating menus (#4498)
* Desktop: Make input routing aware of floating menus * Desktop: Route mouse input like pen input * Desktop: Model pointer input routing as a state machine
This commit is contained in:
@@ -280,6 +280,9 @@ impl App {
|
||||
render_state.set_viewport_scale([viewport_scale_x as f32, viewport_scale_y as f32]);
|
||||
}
|
||||
}
|
||||
DesktopFrontendMessage::WindowUpdateDirectInput { enabled } => {
|
||||
self.input_state.set_direct_input(enabled);
|
||||
}
|
||||
DesktopFrontendMessage::UpdateUIScale { scale } => {
|
||||
self.ui_scale = scale;
|
||||
self.resize();
|
||||
|
||||
@@ -4,17 +4,16 @@ use winit::event::{ButtonSource, ElementState, MouseButton, MouseScrollDelta, Po
|
||||
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::{InputMessage, ModifierKeys, MouseKeys, PointerState, ScrollDelta};
|
||||
use crate::wrapper::messages::{EditorPointerState, InputMessage, ModifierKeys, MouseKeys, ScrollDelta};
|
||||
|
||||
pub(crate) struct InputState {
|
||||
start: Instant,
|
||||
viewport_info: Option<ViewportInfo>,
|
||||
pointer_lock_position: Option<PhysicalPosition<f64>>,
|
||||
modifiers: ModifiersState,
|
||||
pointer_position: PhysicalPosition<f64>,
|
||||
pointer_keys: MouseKeys,
|
||||
ui_capture: bool,
|
||||
pointer_state: PointerState,
|
||||
click_tracker: ClickTracker,
|
||||
direct_input: bool,
|
||||
}
|
||||
|
||||
impl InputState {
|
||||
@@ -22,12 +21,11 @@ impl InputState {
|
||||
Self {
|
||||
start: Instant::now(),
|
||||
viewport_info: None,
|
||||
pointer_lock_position: None,
|
||||
modifiers: ModifiersState::default(),
|
||||
pointer_position: PhysicalPosition::default(),
|
||||
pointer_keys: MouseKeys::empty(),
|
||||
ui_capture: true,
|
||||
pointer_state: PointerState::Hover { route: Route::Ui },
|
||||
click_tracker: ClickTracker::default(),
|
||||
direct_input: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,20 +33,44 @@ impl InputState {
|
||||
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_lock_position = Some(self.pointer_position);
|
||||
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 position = self.pointer_lock_position.take();
|
||||
if let Some(position) = position {
|
||||
self.pointer_position = position;
|
||||
}
|
||||
position
|
||||
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 {
|
||||
self.pointer_lock_position.is_some()
|
||||
matches!(self.pointer_state, PointerState::Locked { .. })
|
||||
}
|
||||
|
||||
pub(crate) fn modifiers(&self) -> ModifiersState {
|
||||
@@ -60,24 +82,28 @@ impl InputState {
|
||||
WindowEvent::PointerMoved { position, source, .. } => {
|
||||
self.pointer_position = *position;
|
||||
|
||||
let PointerSource::TabletTool { kind, data } = source else {
|
||||
ui_callback(InputEvent::pointer().position(*position).moved().modifiers(self.modifiers).build());
|
||||
return;
|
||||
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,
|
||||
},
|
||||
};
|
||||
let ui_capture = if self.pointer_keys.is_empty() {
|
||||
self.pointer_locked() || !self.in_viewport(*position)
|
||||
} else {
|
||||
self.ui_capture
|
||||
};
|
||||
if ui_capture {
|
||||
ui_callback(InputEvent::pointer().position(*position).moved().modifiers(self.modifiers).build());
|
||||
return;
|
||||
match route {
|
||||
Route::Ui => ui_callback(InputEvent::pointer().position(*position).moved().modifiers(self.modifiers).build()),
|
||||
Route::Editor => 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(),
|
||||
}),
|
||||
}
|
||||
|
||||
editor_callback(InputMessage::PointerMove {
|
||||
editor_mouse_state: self.tablet_pointer_state(kind, data),
|
||||
modifier_keys: self.modifier_keys(),
|
||||
});
|
||||
}
|
||||
WindowEvent::PointerEntered { position, .. } => {
|
||||
self.pointer_position = *position;
|
||||
@@ -91,13 +117,6 @@ impl InputState {
|
||||
WindowEvent::PointerButton { state, button, position, .. } => {
|
||||
self.pointer_position = *position;
|
||||
|
||||
let tablet = matches!(button, ButtonSource::TabletTool { .. });
|
||||
|
||||
// Stroke keeps capture decided from first button press until all buttons are released.
|
||||
if state.is_pressed() && self.pointer_keys.is_empty() {
|
||||
self.ui_capture = self.pointer_locked() || !tablet || !self.in_viewport(*position);
|
||||
}
|
||||
|
||||
let mouse_button = button.clone().mouse_button();
|
||||
let keys = match mouse_button {
|
||||
Some(MouseButton::Left) => MouseKeys::LEFT,
|
||||
@@ -107,15 +126,41 @@ impl InputState {
|
||||
Some(MouseButton::Forward) => MouseKeys::FORWARD,
|
||||
_ => MouseKeys::NONE,
|
||||
};
|
||||
match state {
|
||||
ElementState::Pressed => self.pointer_keys.insert(keys),
|
||||
ElementState::Released => self.pointer_keys.remove(keys),
|
||||
}
|
||||
|
||||
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));
|
||||
if self.pointer_locked() || !(back_or_forward || (tablet && !self.ui_capture)) {
|
||||
if self.pointer_locked() || keys.is_empty() || !(back_or_forward || route == Route::Editor) {
|
||||
let pointer = InputEvent::pointer().position(*position);
|
||||
let input = match state {
|
||||
ElementState::Pressed => pointer.pressed(button.clone(), count),
|
||||
@@ -135,7 +180,7 @@ impl InputState {
|
||||
ElementState::Released if count % 2 == 0 => {
|
||||
editor_callback(InputMessage::PointerUp { editor_mouse_state, modifier_keys });
|
||||
editor_callback(InputMessage::DoubleClick {
|
||||
editor_mouse_state: PointerState {
|
||||
editor_mouse_state: EditorPointerState {
|
||||
mouse_keys: keys,
|
||||
..editor_mouse_state
|
||||
},
|
||||
@@ -146,7 +191,7 @@ impl InputState {
|
||||
}
|
||||
}
|
||||
WindowEvent::MouseWheel { delta, .. } => {
|
||||
if self.pointer_locked() || !self.in_viewport(self.pointer_position) {
|
||||
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),
|
||||
@@ -163,12 +208,12 @@ impl InputState {
|
||||
let scroll_delta = ScrollDelta::new(-x * SCROLL_SPEED_X, -y * SCROLL_SPEED_Y, 0.);
|
||||
|
||||
editor_callback(InputMessage::WheelScroll {
|
||||
editor_mouse_state: PointerState { scroll_delta, ..self.pointer_state() },
|
||||
editor_mouse_state: EditorPointerState { scroll_delta, ..self.pointer_state() },
|
||||
modifier_keys: self.modifier_keys(),
|
||||
});
|
||||
}
|
||||
WindowEvent::PinchGesture { delta, .. } => {
|
||||
if self.pointer_locked() || !self.in_viewport(self.pointer_position) || !delta.is_normal() {
|
||||
if self.pointer_locked() || self.ui_captures(self.pointer_position) || !delta.is_normal() {
|
||||
ui_callback(InputEvent::pointer().zoomed(*delta).modifiers(self.modifiers).build());
|
||||
return;
|
||||
}
|
||||
@@ -176,7 +221,7 @@ impl InputState {
|
||||
// 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: PointerState { scroll_delta, ..self.pointer_state() },
|
||||
editor_mouse_state: EditorPointerState { scroll_delta, ..self.pointer_state() },
|
||||
modifier_keys: self.modifier_keys() | ModifierKeys::CONTROL,
|
||||
});
|
||||
}
|
||||
@@ -192,21 +237,32 @@ impl InputState {
|
||||
self.viewport_info.as_ref().map_or(1., |info| info.scale)
|
||||
}
|
||||
|
||||
fn in_viewport(&self, position: PhysicalPosition<f64>) -> bool {
|
||||
self.viewport_info.as_ref().is_some_and(|info| info.contains(position))
|
||||
fn ui_captures(&self, position: PhysicalPosition<f64>) -> bool {
|
||||
!self.direct_input || !self.viewport_info.as_ref().is_some_and(|info| info.contains(position))
|
||||
}
|
||||
|
||||
fn pointer_state(&self) -> PointerState {
|
||||
PointerState {
|
||||
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,
|
||||
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) -> PointerState {
|
||||
PointerState {
|
||||
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),
|
||||
@@ -226,6 +282,19 @@ impl InputState {
|
||||
}
|
||||
}
|
||||
|
||||
#[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,
|
||||
|
||||
@@ -122,6 +122,9 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
|
||||
FrontendMessage::WindowPointerLock => {
|
||||
dispatcher.respond(DesktopFrontendMessage::PointerLock);
|
||||
}
|
||||
FrontendMessage::WindowUpdateDirectInput { enabled } => {
|
||||
dispatcher.respond(DesktopFrontendMessage::WindowUpdateDirectInput { enabled });
|
||||
}
|
||||
FrontendMessage::WindowClose => {
|
||||
dispatcher.respond(DesktopFrontendMessage::WindowClose);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ pub(crate) use graphite_editor::messages::prelude::Message as EditorMessage;
|
||||
|
||||
pub use graphite_editor::messages::frontend::utility_types::{DocumentInfo, FileFilter, PersistedState};
|
||||
pub use graphite_editor::messages::input_mapper::utility_types::keyboard::{Key, ModifierKeys};
|
||||
pub use graphite_editor::messages::input_mapper::utility_types::pointer::{EditorPointerState as PointerState, EditorPosition as Position, MouseKeys, ScrollDelta};
|
||||
pub use graphite_editor::messages::input_mapper::utility_types::pointer::{EditorPointerState, MouseKeys, ScrollDelta};
|
||||
pub use graphite_editor::messages::prelude::DocumentId;
|
||||
pub use graphite_editor::messages::prelude::InputPreprocessorMessage as InputMessage;
|
||||
pub use graphite_editor::messages::prelude::PreferencesMessageHandler as Preferences;
|
||||
@@ -40,6 +40,9 @@ pub enum DesktopFrontendMessage {
|
||||
UpdateUIScale {
|
||||
scale: f64,
|
||||
},
|
||||
WindowUpdateDirectInput {
|
||||
enabled: bool,
|
||||
},
|
||||
UpdateOverlays(vello::Scene),
|
||||
PersistenceWriteDocument {
|
||||
id: DocumentId,
|
||||
|
||||
Reference in New Issue
Block a user