Add respecting OS setting for natural scroll direction

This commit is contained in:
Jonathan Tran
2024-09-20 20:19:18 -04:00
parent 4da6298e2a
commit 24c2fe996f
6 changed files with 65 additions and 6 deletions

1
interface.d.ts vendored
View File

@ -69,6 +69,7 @@ export interface IElectronAPI {
kittycad: (access: string, args: any) => any
listMachines: () => Promise<MachinesListing>
getMachineApiIp: () => Promise<string | null>
readNaturalScrollDirection: () => Promise<boolean>
onUpdateDownloaded: (
callback: (value: string) => void
) => Electron.IpcRenderer

View File

@ -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)

View File

@ -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<CameraSystem, MouseGuard> = {
!e.ctrlKey &&
!e.altKey &&
!e.metaKey,
scrollReverseY: true,
scrollAllowInvertY: true,
},
rotate: {
description: `${ALT} + Scroll`,

View File

@ -565,3 +565,7 @@ export const getUser = async (
}
return Promise.reject(new Error('unreachable'))
}
export async function readNaturalScrollDirection() {
return window.electron.readNaturalScrollDirection()
}

View File

@ -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)
}

View File

@ -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<MachinesListing> => {
const getMachineApiIp = async (): Promise<String | null> =>
ipcRenderer.invoke('find_machine_api')
async function readNaturalScrollDirection(): Promise<boolean> {
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,
})