From 24c2fe996f5afda85ddee0dda8b42a1b19b87cbb Mon Sep 17 00:00:00 2001 From: Jonathan Tran Date: Fri, 20 Sep 2024 20:19:18 -0400 Subject: [PATCH] Add respecting OS setting for natural scroll direction --- interface.d.ts | 1 + src/clientSideScene/CameraControls.ts | 27 +++++++++++++++++++++++---- src/lib/cameraControls.ts | 4 ++-- src/lib/desktop.ts | 4 ++++ src/lib/utils.ts | 14 ++++++++++++++ src/preload.ts | 21 +++++++++++++++++++++ 6 files changed, 65 insertions(+), 6 deletions(-) diff --git a/interface.d.ts b/interface.d.ts index 7319c69c1..e4051d2fb 100644 --- a/interface.d.ts +++ b/interface.d.ts @@ -69,6 +69,7 @@ export interface IElectronAPI { kittycad: (access: string, args: any) => any listMachines: () => Promise getMachineApiIp: () => Promise + readNaturalScrollDirection: () => Promise onUpdateDownloaded: ( callback: (value: string) => void ) => Electron.IpcRenderer diff --git a/src/clientSideScene/CameraControls.ts b/src/clientSideScene/CameraControls.ts index b162755f5..6c0f39bca 100644 --- a/src/clientSideScene/CameraControls.ts +++ b/src/clientSideScene/CameraControls.ts @@ -22,7 +22,12 @@ import { UnreliableSubscription, } from 'lang/std/engineConnection' import { EngineCommand } from 'lang/std/artifactGraph' -import { toSync, uuidv4 } from 'lib/utils' +import { + cachedNaturalScrollDirection, + refreshNaturalScrollDirection, + toSync, + uuidv4, +} from 'lib/utils' import { deg2Rad } from 'lib/utils2d' import { isReducedMotion, roundOff, throttle } from 'lib/utils' import * as TWEEN from '@tweenjs/tween.js' @@ -34,6 +39,9 @@ const ORTHOGRAPHIC_CAMERA_SIZE = 20 const FRAMES_TO_ANIMATE_IN = 30 const ORTHOGRAPHIC_MAGIC_FOV = 4 +// Load the setting from the OS. +refreshNaturalScrollDirection().catch(reportRejection) + const tempQuaternion = new Quaternion() // just used for maths type interactionType = 'pan' | 'rotate' | 'zoom' @@ -522,15 +530,25 @@ export class CameraControls { } } + zoomDirection = (event: WheelEvent): 1 | -1 => { + if (!this.interactionGuards.zoom.scrollAllowInvertY) return 1 + // Safari provides the updated user setting on every event, so it's more + // accurate than our cached value. + if ('webkitDirectionInvertedFromDevice' in event) { + return event.webkitDirectionInvertedFromDevice ? -1 : 1 + } + return cachedNaturalScrollDirection ? -1 : 1 + } + onMouseWheel = (event: WheelEvent) => { const interaction = this.getInteractionType(event) if (interaction === 'none') return event.preventDefault() - const zoomDirection = this.interactionGuards.zoom.scrollReverseY ? -1 : 1 if (this.syncDirection === 'engineToClient') { if (interaction === 'zoom') { - this.zoomDataFromLastFrame = event.deltaY * zoomDirection + const zoomDir = this.zoomDirection(event) + this.zoomDataFromLastFrame = event.deltaY * zoomDir } else { this.moveDataFromLastFrame = [ 'wheel', @@ -551,8 +569,9 @@ export class CameraControls { this.handleStart() if (interaction === 'zoom') { + const zoomDir = this.zoomDirection(event) this.pendingZoom = - 1 + (event.deltaY / window.devicePixelRatio) * 0.001 * zoomDirection + 1 + (event.deltaY / window.devicePixelRatio) * 0.001 * zoomDir } else { this.isDragging = true this.mouseDownPosition.set(event.clientX, event.clientY) diff --git a/src/lib/cameraControls.ts b/src/lib/cameraControls.ts index 1d7540570..a4475bec9 100644 --- a/src/lib/cameraControls.ts +++ b/src/lib/cameraControls.ts @@ -66,7 +66,7 @@ interface MouseGuardZoomHandler { description: string dragCallback: (e: MouseEvent) => boolean scrollCallback: (e: WheelEvent) => boolean - scrollReverseY?: boolean + scrollAllowInvertY?: boolean lenientDragStartButton?: number } @@ -157,7 +157,7 @@ export const cameraMouseDragGuards: Record = { !e.ctrlKey && !e.altKey && !e.metaKey, - scrollReverseY: true, + scrollAllowInvertY: true, }, rotate: { description: `${ALT} + Scroll`, diff --git a/src/lib/desktop.ts b/src/lib/desktop.ts index 327733ebb..b44ccee3c 100644 --- a/src/lib/desktop.ts +++ b/src/lib/desktop.ts @@ -565,3 +565,7 @@ export const getUser = async ( } return Promise.reject(new Error('unreachable')) } + +export async function readNaturalScrollDirection() { + return window.electron.readNaturalScrollDirection() +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index a738232ae..05e7d3d2e 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -4,6 +4,7 @@ import { v4 } from 'uuid' import { isDesktop } from './isDesktop' import { AnyMachineSnapshot } from 'xstate' import { AsyncFn } from './types' +import { readNaturalScrollDirection } from './desktop' export const uuidv4 = v4 @@ -262,6 +263,19 @@ export function isReducedMotion(): boolean { ) } +/** + * True if Apple Trackpad scroll should move the content. I.e. if this is true, + * and the user scrolls down, the viewport moves up relative to the content. + */ +export let cachedNaturalScrollDirection = platform() === 'macos' + +export async function refreshNaturalScrollDirection() { + if (!isDesktop()) return cachedNaturalScrollDirection + const isNatural = await readNaturalScrollDirection() + cachedNaturalScrollDirection = isNatural + return isNatural +} + export function XOR(bool1: boolean, bool2: boolean): boolean { return (bool1 || bool2) && !(bool1 && bool2) } diff --git a/src/preload.ts b/src/preload.ts index 1f458a424..3b7cfd719 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -5,6 +5,7 @@ import os from 'node:os' import fsSync from 'node:fs' import packageJson from '../package.json' import { MachinesListing } from 'lib/machineManager' +import { exec } from 'child_process' import chokidar from 'chokidar' const open = (args: any) => ipcRenderer.invoke('dialog.showOpenDialog', args) @@ -81,6 +82,25 @@ const listMachines = async (): Promise => { const getMachineApiIp = async (): Promise => ipcRenderer.invoke('find_machine_api') +async function readNaturalScrollDirection(): Promise { + if (os.platform() !== 'darwin') { + // TODO: Detect this on other OS's. + return false + } + return new Promise((resolve, reject) => { + exec( + 'defaults read -globalDomain com.apple.swipescrolldirection', + (err, stdout) => { + if (err) { + reject(err) + } else { + resolve(stdout.trim() === '1') + } + } + ) + }) +} + contextBridge.exposeInMainWorld('electron', { startDeviceFlow, loginWithDeviceFlow, @@ -144,6 +164,7 @@ contextBridge.exposeInMainWorld('electron', { kittycad, listMachines, getMachineApiIp, + readNaturalScrollDirection, onUpdateDownloaded, appRestart, })