Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions desktop/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
177 changes: 123 additions & 54 deletions desktop/src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,51 +4,73 @@ 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 {
pub(crate) fn new() -> Self {
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,
}
}

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_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 {
Expand All @@ -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 ui_capture = if self.pointer_keys.is_empty() {
self.pointer_locked() || !self.in_viewport(*position)
} else {
self.ui_capture
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,
},
};
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;
Expand All @@ -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,
Expand All @@ -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),
Expand All @@ -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
},
Expand All @@ -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),
Expand All @@ -163,20 +208,20 @@ 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;
}

// 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,
});
}
Expand All @@ -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 route(&self, position: PhysicalPosition<f64>) -> Route {
if self.ui_captures(position) { Route::Ui } else { Route::Editor }
}

fn pointer_state(&self) -> PointerState {
PointerState {
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),
Expand All @@ -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,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: UI would look a bit nicer.

Editor,
}

struct ViewportInfo {
x: f64,
y: f64,
Expand Down
3 changes: 3 additions & 0 deletions desktop/wrapper/src/intercept_frontend_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
5 changes: 4 additions & 1 deletion desktop/wrapper/src/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -40,6 +40,9 @@ pub enum DesktopFrontendMessage {
UpdateUIScale {
scale: f64,
},
WindowUpdateDirectInput {
enabled: bool,
},
UpdateOverlays(vello::Scene),
PersistenceWriteDocument {
id: DocumentId,
Expand Down
1 change: 1 addition & 0 deletions editor/src/messages/app_window/app_window_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::messages::prelude::*;
pub enum AppWindowMessage {
PointerLock,
PointerLockMove { x: f64, y: f64 },
DirectInput { enabled: bool },
Restart,
Close,
Minimize,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ impl MessageHandler<AppWindowMessage, ()> for AppWindowMessageHandler {
AppWindowMessage::PointerLockMove { x, y } => {
responses.add(FrontendMessage::WindowPointerLockMove { position: (x, y) });
}
AppWindowMessage::DirectInput { enabled } => {
#[cfg(not(target_family = "wasm"))]
responses.add(FrontendMessage::WindowUpdateDirectInput { enabled });
#[cfg(target_family = "wasm")]
let _ = enabled;
}
AppWindowMessage::Close => {
#[cfg(not(target_family = "wasm"))]
responses.add(FrontendMessage::WindowClose);
Expand Down
4 changes: 4 additions & 0 deletions editor/src/messages/frontend/frontend_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,10 @@ pub enum FrontendMessage {
position: (f64, f64),
},
#[cfg(not(target_family = "wasm"))]
WindowUpdateDirectInput {
enabled: bool,
},
#[cfg(not(target_family = "wasm"))]
WindowClose,
#[cfg(not(target_family = "wasm"))]
WindowMinimize,
Expand Down
Loading
Loading