Skip to content

Commit 73682b4

Browse files
Desktop: Implement pointer lock for NumberInput (#3638)
* Desktop: Implement pointer lock for NumberInput * add shift and ctrl modifiers * fixup
1 parent 6616d1b commit 73682b4

12 files changed

Lines changed: 150 additions & 26 deletions

File tree

desktop/src/app.rs

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use std::sync::mpsc::{Receiver, Sender, SyncSender};
55
use std::thread;
66
use std::time::{Duration, Instant};
77
use winit::application::ApplicationHandler;
8-
use winit::dpi::PhysicalSize;
8+
use winit::dpi::{PhysicalPosition, PhysicalSize};
99
use winit::event::{ButtonSource, ElementState, MouseButton, WindowEvent};
1010
use winit::event_loop::{ActiveEventLoop, ControlFlow};
1111
use winit::window::WindowId;
@@ -27,6 +27,8 @@ pub(crate) struct App {
2727
window_size: PhysicalSize<u32>,
2828
window_maximized: bool,
2929
window_fullscreen: bool,
30+
pointer_position: PhysicalPosition<f64>,
31+
pointer_lock_position: Option<PhysicalPosition<f64>>,
3032
ui_scale: f64,
3133
app_event_receiver: Receiver<AppEvent>,
3234
app_event_scheduler: AppEventScheduler,
@@ -84,6 +86,8 @@ impl App {
8486
window_size: PhysicalSize { width: 0, height: 0 },
8587
window_maximized: false,
8688
window_fullscreen: false,
89+
pointer_position: Default::default(),
90+
pointer_lock_position: Default::default(),
8791
ui_scale: 1.,
8892
app_event_receiver,
8993
app_event_scheduler,
@@ -329,6 +333,12 @@ impl App {
329333
window.clipboard_write(content);
330334
}
331335
}
336+
DesktopFrontendMessage::PointerLock => {
337+
self.pointer_lock_position = Some(self.pointer_position);
338+
if let Some(window) = &self.window {
339+
window.start_pointer_lock();
340+
}
341+
}
332342
DesktopFrontendMessage::WindowClose => {
333343
self.app_event_scheduler.schedule(AppEvent::CloseWindow);
334344
}
@@ -480,6 +490,26 @@ impl ApplicationHandler for App {
480490
}
481491

482492
fn window_event(&mut self, event_loop: &dyn ActiveEventLoop, _window_id: WindowId, event: WindowEvent) {
493+
// Handle pointer lock release
494+
if let Some(pointer_lock_position) = self.pointer_lock_position
495+
&& let WindowEvent::PointerButton {
496+
state: ElementState::Released,
497+
button: ButtonSource::Mouse(MouseButton::Left),
498+
..
499+
} = event
500+
{
501+
self.pointer_lock_position = None;
502+
if let Some(window) = &self.window {
503+
window.end_pointer_lock();
504+
}
505+
self.cef_context.handle_window_event(&WindowEvent::PointerMoved {
506+
device_id: None,
507+
position: pointer_lock_position,
508+
primary: true,
509+
source: winit::event::PointerSource::Mouse,
510+
});
511+
}
512+
483513
self.cef_context.handle_window_event(&event);
484514

485515
match event {
@@ -556,13 +586,29 @@ impl ApplicationHandler for App {
556586
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
557587
}
558588
}
589+
590+
WindowEvent::PointerMoved { position, .. } | WindowEvent::PointerLeft { position: Some(position), .. } | WindowEvent::PointerEntered { position, .. }
591+
if self.pointer_lock_position.is_none() =>
592+
{
593+
self.pointer_position = position;
594+
}
595+
559596
_ => {}
560597
}
561598

562599
// Notify cef of possible input events
563600
self.cef_context.work();
564601
}
565602

603+
fn device_event(&mut self, _event_loop: &dyn ActiveEventLoop, _device_id: Option<winit::event::DeviceId>, event: winit::event::DeviceEvent) {
604+
if self.pointer_lock_position.is_some()
605+
&& let winit::event::DeviceEvent::PointerMotion { delta: (x, y) } = event
606+
{
607+
let message = DesktopWrapperMessage::PointerLockMove { x, y };
608+
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
609+
}
610+
}
611+
566612
fn about_to_wait(&mut self, event_loop: &dyn ActiveEventLoop) {
567613
// Set a timeout in case we miss any cef schedule requests
568614
let timeout = Instant::now() + Duration::from_millis(10);

desktop/src/window.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,16 @@ impl Window {
159159
self.winit_window.set_cursor(cursor);
160160
}
161161

162+
pub(crate) fn start_pointer_lock(&self) {
163+
let _ = self.winit_window.set_cursor_grab(winit::window::CursorGrabMode::Locked);
164+
self.winit_window.set_cursor_visible(false);
165+
}
166+
167+
pub(crate) fn end_pointer_lock(&self) {
168+
let _ = self.winit_window.set_cursor_grab(winit::window::CursorGrabMode::None);
169+
self.winit_window.set_cursor_visible(true);
170+
}
171+
162172
pub(crate) fn update_menu(&self, entries: Vec<MenuItem>) {
163173
self.native_handle.update_menu(entries);
164174
}

desktop/wrapper/src/handle_desktop_wrapper_message.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,5 +172,9 @@ pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMess
172172
dispatcher.queue_editor_message(message);
173173
}
174174
}
175+
DesktopWrapperMessage::PointerLockMove { x, y } => {
176+
let message = AppWindowMessage::PointerLockMove { x, y };
177+
dispatcher.queue_editor_message(message);
178+
}
175179
}
176180
}

desktop/wrapper/src/intercept_frontend_message.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,9 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
136136
FrontendMessage::TriggerClipboardWrite { content } => {
137137
dispatcher.respond(DesktopFrontendMessage::ClipboardWrite { content });
138138
}
139+
FrontendMessage::WindowPointerLock => {
140+
dispatcher.respond(DesktopFrontendMessage::PointerLock);
141+
}
139142
FrontendMessage::WindowClose => {
140143
dispatcher.respond(DesktopFrontendMessage::WindowClose);
141144
}

desktop/wrapper/src/messages.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ pub enum DesktopFrontendMessage {
6565
ClipboardWrite {
6666
content: String,
6767
},
68+
PointerLock,
6869
WindowClose,
6970
WindowMinimize,
7071
WindowMaximize,
@@ -132,6 +133,10 @@ pub enum DesktopWrapperMessage {
132133
ClipboardReadResult {
133134
content: Option<String>,
134135
},
136+
PointerLockMove {
137+
x: f64,
138+
y: f64,
139+
},
135140
}
136141

137142
#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)]

editor/src/messages/app_window/app_window_message.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ use super::app_window_message_handler::AppWindowPlatform;
66
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
77
pub enum AppWindowMessage {
88
UpdatePlatform { platform: AppWindowPlatform },
9+
PointerLock,
10+
PointerLockMove { x: f64, y: f64 },
911
Close,
1012
Minimize,
1113
Maximize,

editor/src/messages/app_window/app_window_message_handler.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ impl MessageHandler<AppWindowMessage, ()> for AppWindowMessageHandler {
1515
self.platform = platform;
1616
responses.add(FrontendMessage::UpdatePlatform { platform: self.platform });
1717
}
18+
AppWindowMessage::PointerLock => {
19+
responses.add(FrontendMessage::WindowPointerLock);
20+
}
21+
AppWindowMessage::PointerLockMove { x, y } => {
22+
responses.add(FrontendMessage::WindowPointerLockMove { x, y });
23+
}
1824
AppWindowMessage::Close => {
1925
responses.add(FrontendMessage::WindowClose);
2026
}

editor/src/messages/frontend/frontend_message.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,11 @@ pub enum FrontendMessage {
364364
},
365365

366366
// Window prefix: cause the application window to do something
367+
WindowPointerLock,
368+
WindowPointerLockMove {
369+
x: f64,
370+
y: f64,
371+
},
367372
WindowClose,
368373
WindowMinimize,
369374
WindowMaximize,

frontend/src/components/widgets/inputs/NumberInput.svelte

Lines changed: 48 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
<script lang="ts">
2-
import { createEventDispatcher, onMount, onDestroy } from "svelte";
2+
import { createEventDispatcher, onMount, onDestroy, getContext } from "svelte";
33
44
import { evaluateMathExpression } from "@graphite/../wasm/pkg/graphite_wasm";
5+
import type { Editor } from "@graphite/editor";
56
import { PRESS_REPEAT_DELAY_MS, PRESS_REPEAT_INTERVAL_MS } from "@graphite/io-managers/input";
67
import type { NumberInputMode, NumberInputIncrementBehavior, ActionShortcut } from "@graphite/messages";
78
import { browserVersion, isDesktop } from "@graphite/utility-functions/platform";
@@ -16,6 +17,8 @@
1617
1718
const dispatch = createEventDispatcher<{ value: number | undefined; startHistoryTransaction: undefined }>();
1819
20+
const editor = getContext<Editor>("editor");
21+
1922
// Content
2023
/// When `value` is not provided (i.e. it's `undefined`), a dash is displayed.
2124
export let value: number | undefined = undefined;
@@ -80,6 +83,8 @@
8083
let initialValueBeforeDragging: number | undefined = undefined;
8184
// Stores the total value change during the process of dragging the slider. Set to 0 when not dragging.
8285
let cumulativeDragDelta = 0;
86+
// Track whether the Shift key is currently held down.
87+
let shiftKeyDown = false;
8388
// Track whether the Ctrl key is currently held down.
8489
let ctrlKeyDown = false;
8590
@@ -91,17 +96,20 @@
9196
...(mode === "Range" ? { "--progress-factor": Math.min(Math.max((rangeSliderValueAsRendered - rangeMin) / (rangeMax - rangeMin), 0), 1) } : {}),
9297
};
9398
94-
// Keep track of the Ctrl key being held down.
95-
const trackCtrl = (e: KeyboardEvent | MouseEvent) => (ctrlKeyDown = e.ctrlKey);
99+
// Keep track of the Shift and Ctrl key being held down.
100+
const trackShiftAndCtrl = (e: KeyboardEvent | MouseEvent) => {
101+
shiftKeyDown = e.shiftKey;
102+
ctrlKeyDown = e.ctrlKey;
103+
};
96104
onMount(() => {
97-
addEventListener("keydown", trackCtrl);
98-
addEventListener("keyup", trackCtrl);
99-
addEventListener("mousemove", trackCtrl);
105+
addEventListener("keydown", trackShiftAndCtrl);
106+
addEventListener("keyup", trackShiftAndCtrl);
107+
addEventListener("mousemove", trackShiftAndCtrl);
100108
});
101109
onDestroy(() => {
102-
removeEventListener("keydown", trackCtrl);
103-
removeEventListener("keyup", trackCtrl);
104-
removeEventListener("mousemove", trackCtrl);
110+
removeEventListener("keydown", trackShiftAndCtrl);
111+
removeEventListener("keyup", trackShiftAndCtrl);
112+
removeEventListener("mousemove", trackShiftAndCtrl);
105113
clearTimeout(repeatTimeout);
106114
});
107115
@@ -369,6 +377,9 @@
369377
370378
// Enter dragging state
371379
if (usePointerLock) target.requestPointerLock();
380+
if (isDesktop()) {
381+
editor.handle.appWindowPointerLock();
382+
}
372383
initialValueBeforeDragging = value;
373384
cumulativeDragDelta = 0;
374385
@@ -412,19 +423,16 @@
412423
413424
// Calculate and then update the dragged value offset, slowed down by 10x when Shift is held.
414425
if (ignoredFirstMovement && initialValueBeforeDragging !== undefined) {
415-
const CHANGE_PER_DRAG_PX = 0.1;
416-
const CHANGE_PER_DRAG_PX_SLOW = CHANGE_PER_DRAG_PX / 10;
417-
418-
const dragDelta = e.movementX * (e.shiftKey ? CHANGE_PER_DRAG_PX_SLOW : CHANGE_PER_DRAG_PX);
419-
cumulativeDragDelta += dragDelta;
420-
421-
const combined = initialValueBeforeDragging + cumulativeDragDelta;
422-
const combineSnapped = e.ctrlKey ? Math.round(combined) : combined;
423-
424-
const newValue = updateValue(combineSnapped);
425-
426-
// If the value was altered within the `updateValue()` call, we need to rectify the cumulative drag delta to account for the change.
427-
if (newValue !== undefined) cumulativeDragDelta -= combineSnapped - newValue;
426+
pointerLockMoveUpdate(e.movementX, e.shiftKey, e.ctrlKey, initialValueBeforeDragging);
427+
}
428+
ignoredFirstMovement = true;
429+
};
430+
// On desktop we don't get `pointermove` events while in pointer lock (cef doesn't support pointer lock).
431+
// We have to listen for our custom `pointerlockmove` events instead.
432+
const pointerLockMove = (e: Event) => {
433+
if (ignoredFirstMovement && initialValueBeforeDragging !== undefined && e instanceof CustomEvent) {
434+
const delta = (e.detail as { x: number }).x;
435+
pointerLockMoveUpdate(delta, shiftKeyDown, ctrlKeyDown, initialValueBeforeDragging);
428436
}
429437
ignoredFirstMovement = true;
430438
};
@@ -443,14 +451,32 @@
443451
// Clean up the event listeners.
444452
removeEventListener("pointerup", pointerUp);
445453
removeEventListener("pointermove", pointerMove);
454+
removeEventListener("pointerlockmove", pointerLockMove);
446455
if (usePointerLock) document.removeEventListener("pointerlockchange", pointerLockChange);
447456
};
448457
449458
addEventListener("pointerup", pointerUp);
450459
addEventListener("pointermove", pointerMove);
460+
addEventListener("pointerlockmove", pointerLockMove);
451461
if (usePointerLock) document.addEventListener("pointerlockchange", pointerLockChange);
452462
}
453463
464+
function pointerLockMoveUpdate(delta: number, slow: boolean, snapping: boolean, initialValue: number) {
465+
const CHANGE_PER_DRAG_PX = 0.1;
466+
const CHANGE_PER_DRAG_PX_SLOW = CHANGE_PER_DRAG_PX / 10;
467+
468+
const dragDelta = delta * (slow ? CHANGE_PER_DRAG_PX_SLOW : CHANGE_PER_DRAG_PX);
469+
cumulativeDragDelta += dragDelta;
470+
471+
const combined = initialValue + cumulativeDragDelta;
472+
const combineSnapped = snapping ? Math.round(combined) : combined;
473+
474+
const newValue = updateValue(combineSnapped);
475+
476+
// If the value was altered within the `updateValue()` call, we need to rectify the cumulative drag delta to account for the change.
477+
if (newValue !== undefined) cumulativeDragDelta -= combineSnapped - newValue;
478+
}
479+
454480
// ===============================
455481
// RANGE MODE: DRAGGING THE SLIDER
456482
// ===============================

frontend/src/io-managers/input.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { get } from "svelte/store";
22

33
import { type Editor } from "@graphite/editor";
4-
import { TriggerClipboardRead } from "@graphite/messages";
4+
import { TriggerClipboardRead, WindowPointerLockMove } from "@graphite/messages";
55
import { type DialogState } from "@graphite/state-providers/dialog";
66
import { type DocumentState } from "@graphite/state-providers/document";
77
import { type FullscreenState } from "@graphite/state-providers/fullscreen";
@@ -500,6 +500,12 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
500500
}
501501
});
502502

503+
// Pointer lock movement events on desktop
504+
editor.subscriptions.subscribeJsMessage(WindowPointerLockMove, (data) => {
505+
const event = new CustomEvent("pointerlockmove", { detail: data });
506+
window.dispatchEvent(event);
507+
});
508+
503509
// Helper functions
504510

505511
function potentiallyRestoreCanvasFocus(e: Event) {

0 commit comments

Comments
 (0)