diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 85057aa449..b89d1a5a96 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -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(); diff --git a/desktop/src/input.rs b/desktop/src/input.rs index 40b307b822..83668a0a8d 100644 --- a/desktop/src/input.rs +++ b/desktop/src/input.rs @@ -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, - pointer_lock_position: Option>, modifiers: ModifiersState, pointer_position: PhysicalPosition, - 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> { - 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 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; @@ -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) -> bool { - self.viewport_info.as_ref().is_some_and(|info| info.contains(position)) + fn ui_captures(&self, position: PhysicalPosition) -> bool { + !self.direct_input || !self.viewport_info.as_ref().is_some_and(|info| info.contains(position)) + } + + fn route(&self, position: PhysicalPosition) -> 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), @@ -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 }, +} + +#[derive(Clone, Copy, PartialEq)] +enum Route { + Ui, + Editor, +} + struct ViewportInfo { x: f64, y: f64, diff --git a/desktop/wrapper/src/intercept_frontend_message.rs b/desktop/wrapper/src/intercept_frontend_message.rs index b188e39041..6d13e4bc4c 100644 --- a/desktop/wrapper/src/intercept_frontend_message.rs +++ b/desktop/wrapper/src/intercept_frontend_message.rs @@ -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); } diff --git a/desktop/wrapper/src/messages.rs b/desktop/wrapper/src/messages.rs index 296653f191..0559e02f36 100644 --- a/desktop/wrapper/src/messages.rs +++ b/desktop/wrapper/src/messages.rs @@ -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, diff --git a/editor/src/messages/app_window/app_window_message.rs b/editor/src/messages/app_window/app_window_message.rs index 1a68fdc938..b6e9641a16 100644 --- a/editor/src/messages/app_window/app_window_message.rs +++ b/editor/src/messages/app_window/app_window_message.rs @@ -5,6 +5,7 @@ use crate::messages::prelude::*; pub enum AppWindowMessage { PointerLock, PointerLockMove { x: f64, y: f64 }, + DirectInput { enabled: bool }, Restart, Close, Minimize, diff --git a/editor/src/messages/app_window/app_window_message_handler.rs b/editor/src/messages/app_window/app_window_message_handler.rs index 0477bed6e7..e09ad3c19c 100644 --- a/editor/src/messages/app_window/app_window_message_handler.rs +++ b/editor/src/messages/app_window/app_window_message_handler.rs @@ -17,6 +17,12 @@ impl MessageHandler 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); diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index f9d08799f4..29daaa269b 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -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, diff --git a/frontend/src/components/layout/FloatingMenu.svelte b/frontend/src/components/layout/FloatingMenu.svelte index 58654511ae..770ecce2f6 100644 --- a/frontend/src/components/layout/FloatingMenu.svelte +++ b/frontend/src/components/layout/FloatingMenu.svelte @@ -17,10 +17,11 @@