diff --git a/editor/src/components/inspector/sections/layout-section/self-layout-subsection/frame-updating-layout-section.spec.browser2.tsx b/editor/src/components/inspector/sections/layout-section/self-layout-subsection/frame-updating-layout-section.spec.browser2.tsx index 26a18ed7c5bc..eaf49c0d9058 100644 --- a/editor/src/components/inspector/sections/layout-section/self-layout-subsection/frame-updating-layout-section.spec.browser2.tsx +++ b/editor/src/components/inspector/sections/layout-section/self-layout-subsection/frame-updating-layout-section.spec.browser2.tsx @@ -18,7 +18,13 @@ import * as EP from '../../../../../core/shared/element-path' import { selectComponentsForTest } from '../../../../../utils/utils.test-utils' import { RegisteredCanvasStrategies } from '../../../../canvas/canvas-strategies/canvas-strategies' import { act, fireEvent } from '@testing-library/react' -import { mouseClickAtPoint } from '../../../../canvas/event-helpers.test-utils' +import { + mouseClickAtPoint, + mouseDownAtPoint, + mouseDragFromPointToPoint, + mouseMoveToPoint, + pressKey, +} from '../../../../canvas/event-helpers.test-utils' import { getDomRectCenter } from '../../../../../core/shared/dom-utils' import { getFixedHugDropdownId } from '../../../fill-hug-fixed-control' @@ -144,6 +150,77 @@ describe('Frame updating layout section', () => { } describe('Left control', () => { + it( + 'scrubbing the left control label', + makeTestCase({ + baseProject: `
+ +
`, + actionChange: async (renderResult) => { + // Select the rectangle. + await selectComponentsForTest(renderResult, [ + EP.fromString( + `${BakedInStoryboardUID}/${TestSceneUID}/${TestAppUID}:root-div/rectangle-1`, + ), + ]) + + const scrubLabel = await renderResult.renderedDOM.findByTestId( + `frame-left-number-input-label-div`, + ) + const scrubLabelBounds = scrubLabel.getBoundingClientRect() + const scrubLabelCenter = getDomRectCenter(scrubLabelBounds) + const scrubLabelEndPoint = { x: scrubLabelCenter.x + 100, y: scrubLabelCenter.y } + await mouseDragFromPointToPoint(scrubLabel, scrubLabelCenter, scrubLabelEndPoint) + }, + expectedFrames: { + [`${BakedInStoryboardUID}/${TestSceneUID}/${TestAppUID}:root-div/rectangle-1`]: + localRectangle({ + x: 140, + y: 100, + width: 200, + height: 300, + }), + }, + expectedProject: `
+ +
`, + expectedFixedHugDropdownWidthValue: 'Fixed', + expectedFixedHugDropdownHeightValue: 'Fixed', + }), + ) + it( 'with a single element selected when setting value directly', makeTestCase({ diff --git a/editor/src/core/performance/__snapshots__/performance-regression-tests.spec.tsx.snap b/editor/src/core/performance/__snapshots__/performance-regression-tests.spec.tsx.snap index 88978fea576f..7a36c09939f1 100644 --- a/editor/src/core/performance/__snapshots__/performance-regression-tests.spec.tsx.snap +++ b/editor/src/core/performance/__snapshots__/performance-regression-tests.spec.tsx.snap @@ -411,6 +411,8 @@ Array [ "//div//Symbol(react.memo)(NumberInput)", "//div//Symbol(react.forward_ref)(Styled(div))", "/div//Symbol(react.forward_ref)(Styled(div))/div", + "/Symbol(react.forward_ref)(Styled(div))/div/NumberInput/img", + "/Symbol(react.forward_ref)(Styled(div))/div/NumberInput/div", "/Symbol(react.forward_ref)(Styled(div))/div/NumberInput/div:data-testid='radius-one-mouse-down-handler'", "/Symbol(react.forward_ref)(Styled(div))/div/NumberInput/Symbol(react.forward_ref)(EmotionCssPropInternal)", "/Symbol(react.forward_ref)(Styled(div))/div/NumberInput/Symbol(react.forward_ref)(EmotionCssPropInternal)", @@ -438,6 +440,8 @@ Array [ "//div//Symbol(react.memo)(NumberInput)", "//div//Symbol(react.forward_ref)(Styled(div))", "/div//Symbol(react.forward_ref)(Styled(div))/div", + "/Symbol(react.forward_ref)(Styled(div))/div/NumberInput/img", + "/Symbol(react.forward_ref)(Styled(div))/div/NumberInput/div", "/Symbol(react.forward_ref)(Styled(div))/div/NumberInput/div:data-testid='padding-V-mouse-down-handler'", "/Symbol(react.forward_ref)(Styled(div))/div/NumberInput/Symbol(react.forward_ref)(EmotionCssPropInternal)", "/Symbol(react.forward_ref)(Styled(div))/div/NumberInput/Symbol(react.forward_ref)(EmotionCssPropInternal)", @@ -446,6 +450,8 @@ Array [ "/Symbol(react.forward_ref)(EmotionCssPropInternal)/div/Symbol(react.forward_ref)(EmotionCssPropInternal)/Symbol(react.memo)(Symbol(react.forward_ref)())", "/Symbol(react.forward_ref)(EmotionCssPropInternal)/Symbol(react.memo)(Symbol(react.forward_ref)())/Symbol(react.forward_ref)()/Symbol(react.forward_ref)(Styled(input)):data-testid='padding-V'", "/Symbol(react.memo)(Symbol(react.forward_ref)())/Symbol(react.forward_ref)()/Symbol(react.forward_ref)(Styled(input))/input:data-testid='padding-V'", + "/Symbol(react.forward_ref)(Styled(div))/div/NumberInput/img", + "/Symbol(react.forward_ref)(Styled(div))/div/NumberInput/div", "/Symbol(react.forward_ref)(Styled(div))/div/NumberInput/div:data-testid='padding-H-mouse-down-handler'", "/Symbol(react.forward_ref)(Styled(div))/div/NumberInput/Symbol(react.forward_ref)(EmotionCssPropInternal)", "/Symbol(react.forward_ref)(Styled(div))/div/NumberInput/Symbol(react.forward_ref)(EmotionCssPropInternal)", @@ -551,6 +557,8 @@ Array [ "/div/FrameUpdatingLayoutControl/UtopiaSpiedFunctionComponent(InspectorContextMenuWrapper)/Symbol(react.forward_ref)(EmotionCssPropInternal)", "/FrameUpdatingLayoutControl/UtopiaSpiedFunctionComponent(InspectorContextMenuWrapper)/Symbol(react.forward_ref)(EmotionCssPropInternal)/div", "/Symbol(react.forward_ref)(EmotionCssPropInternal)/div/UtopiaSpiedFunctionComponent(MenuProvider)/div", + "/UtopiaSpiedFunctionComponent(MenuProvider)/div/NumberInput/img", + "/UtopiaSpiedFunctionComponent(MenuProvider)/div/NumberInput/div", "/UtopiaSpiedFunctionComponent(MenuProvider)/div/NumberInput/div:data-testid='frame-left-number-input-mouse-down-handler'", "/UtopiaSpiedFunctionComponent(MenuProvider)/div/NumberInput/Symbol(react.forward_ref)(EmotionCssPropInternal)", "/UtopiaSpiedFunctionComponent(MenuProvider)/div/NumberInput/Symbol(react.forward_ref)(EmotionCssPropInternal)", @@ -572,6 +580,8 @@ Array [ "/div/FrameUpdatingLayoutControl/UtopiaSpiedFunctionComponent(InspectorContextMenuWrapper)/Symbol(react.forward_ref)(EmotionCssPropInternal)", "/FrameUpdatingLayoutControl/UtopiaSpiedFunctionComponent(InspectorContextMenuWrapper)/Symbol(react.forward_ref)(EmotionCssPropInternal)/div", "/Symbol(react.forward_ref)(EmotionCssPropInternal)/div/UtopiaSpiedFunctionComponent(MenuProvider)/div", + "/UtopiaSpiedFunctionComponent(MenuProvider)/div/NumberInput/img", + "/UtopiaSpiedFunctionComponent(MenuProvider)/div/NumberInput/div", "/UtopiaSpiedFunctionComponent(MenuProvider)/div/NumberInput/div:data-testid='frame-top-number-input-mouse-down-handler'", "/UtopiaSpiedFunctionComponent(MenuProvider)/div/NumberInput/Symbol(react.forward_ref)(EmotionCssPropInternal)", "/UtopiaSpiedFunctionComponent(MenuProvider)/div/NumberInput/Symbol(react.forward_ref)(EmotionCssPropInternal)", @@ -1033,6 +1043,8 @@ Array [ "//div//Symbol(react.memo)(NumberInput)", "//div//Symbol(react.forward_ref)(Styled(div))", "/div//Symbol(react.forward_ref)(Styled(div))/div", + "/Symbol(react.forward_ref)(Styled(div))/div/NumberInput/img", + "/Symbol(react.forward_ref)(Styled(div))/div/NumberInput/div", "/Symbol(react.forward_ref)(Styled(div))/div/NumberInput/div:data-testid='radius-one-mouse-down-handler'", "/Symbol(react.forward_ref)(Styled(div))/div/NumberInput/Symbol(react.forward_ref)(EmotionCssPropInternal)", "/Symbol(react.forward_ref)(Styled(div))/div/NumberInput/Symbol(react.forward_ref)(EmotionCssPropInternal)", @@ -1060,6 +1072,8 @@ Array [ "//div//Symbol(react.memo)(NumberInput)", "//div//Symbol(react.forward_ref)(Styled(div))", "/div//Symbol(react.forward_ref)(Styled(div))/div", + "/Symbol(react.forward_ref)(Styled(div))/div/NumberInput/img", + "/Symbol(react.forward_ref)(Styled(div))/div/NumberInput/div", "/Symbol(react.forward_ref)(Styled(div))/div/NumberInput/div:data-testid='padding-V-mouse-down-handler'", "/Symbol(react.forward_ref)(Styled(div))/div/NumberInput/Symbol(react.forward_ref)(EmotionCssPropInternal)", "/Symbol(react.forward_ref)(Styled(div))/div/NumberInput/Symbol(react.forward_ref)(EmotionCssPropInternal)", @@ -1068,6 +1082,8 @@ Array [ "/Symbol(react.forward_ref)(EmotionCssPropInternal)/div/Symbol(react.forward_ref)(EmotionCssPropInternal)/Symbol(react.memo)(Symbol(react.forward_ref)())", "/Symbol(react.forward_ref)(EmotionCssPropInternal)/Symbol(react.memo)(Symbol(react.forward_ref)())/Symbol(react.forward_ref)()/Symbol(react.forward_ref)(Styled(input)):data-testid='padding-V'", "/Symbol(react.memo)(Symbol(react.forward_ref)())/Symbol(react.forward_ref)()/Symbol(react.forward_ref)(Styled(input))/input:data-testid='padding-V'", + "/Symbol(react.forward_ref)(Styled(div))/div/NumberInput/img", + "/Symbol(react.forward_ref)(Styled(div))/div/NumberInput/div", "/Symbol(react.forward_ref)(Styled(div))/div/NumberInput/div:data-testid='padding-H-mouse-down-handler'", "/Symbol(react.forward_ref)(Styled(div))/div/NumberInput/Symbol(react.forward_ref)(EmotionCssPropInternal)", "/Symbol(react.forward_ref)(Styled(div))/div/NumberInput/Symbol(react.forward_ref)(EmotionCssPropInternal)", @@ -1091,6 +1107,8 @@ Array [ "/div/FrameUpdatingLayoutControl/UtopiaSpiedFunctionComponent(InspectorContextMenuWrapper)/Symbol(react.forward_ref)(EmotionCssPropInternal)", "/FrameUpdatingLayoutControl/UtopiaSpiedFunctionComponent(InspectorContextMenuWrapper)/Symbol(react.forward_ref)(EmotionCssPropInternal)/div", "/Symbol(react.forward_ref)(EmotionCssPropInternal)/div/UtopiaSpiedFunctionComponent(MenuProvider)/div", + "/UtopiaSpiedFunctionComponent(MenuProvider)/div/NumberInput/img", + "/UtopiaSpiedFunctionComponent(MenuProvider)/div/NumberInput/div", "/UtopiaSpiedFunctionComponent(MenuProvider)/div/NumberInput/div:data-testid='frame-left-number-input-mouse-down-handler'", "/UtopiaSpiedFunctionComponent(MenuProvider)/div/NumberInput/Symbol(react.forward_ref)(EmotionCssPropInternal)", "/UtopiaSpiedFunctionComponent(MenuProvider)/div/NumberInput/Symbol(react.forward_ref)(EmotionCssPropInternal)", @@ -1112,6 +1130,8 @@ Array [ "/div/FrameUpdatingLayoutControl/UtopiaSpiedFunctionComponent(InspectorContextMenuWrapper)/Symbol(react.forward_ref)(EmotionCssPropInternal)", "/FrameUpdatingLayoutControl/UtopiaSpiedFunctionComponent(InspectorContextMenuWrapper)/Symbol(react.forward_ref)(EmotionCssPropInternal)/div", "/Symbol(react.forward_ref)(EmotionCssPropInternal)/div/UtopiaSpiedFunctionComponent(MenuProvider)/div", + "/UtopiaSpiedFunctionComponent(MenuProvider)/div/NumberInput/img", + "/UtopiaSpiedFunctionComponent(MenuProvider)/div/NumberInput/div", "/UtopiaSpiedFunctionComponent(MenuProvider)/div/NumberInput/div:data-testid='frame-top-number-input-mouse-down-handler'", "/UtopiaSpiedFunctionComponent(MenuProvider)/div/NumberInput/Symbol(react.forward_ref)(EmotionCssPropInternal)", "/UtopiaSpiedFunctionComponent(MenuProvider)/div/NumberInput/Symbol(react.forward_ref)(EmotionCssPropInternal)", @@ -1411,6 +1431,8 @@ Array [ "/OpacityRow/UtopiaSpiedFunctionComponent(InspectorContextMenuWrapper)/Symbol(react.forward_ref)(EmotionCssPropInternal)/div", "/Symbol(react.forward_ref)(EmotionCssPropInternal)/div/UtopiaSpiedFunctionComponent(MenuProvider)/div", "/UtopiaSpiedFunctionComponent(MenuProvider)/div//Symbol(react.memo)(NumberInput)", + "/div//NumberInput/img", + "/div//NumberInput/div", "/div//NumberInput/div:data-testid='opacity-mouse-down-handler'", "/div//NumberInput/Symbol(react.forward_ref)(EmotionCssPropInternal)", "/div//NumberInput/Symbol(react.forward_ref)(EmotionCssPropInternal)", @@ -1982,6 +2004,8 @@ Array [ "/OpacityRow/UtopiaSpiedFunctionComponent(InspectorContextMenuWrapper)/Symbol(react.forward_ref)(EmotionCssPropInternal)/div", "/Symbol(react.forward_ref)(EmotionCssPropInternal)/div/UtopiaSpiedFunctionComponent(MenuProvider)/div", "/UtopiaSpiedFunctionComponent(MenuProvider)/div//Symbol(react.memo)(NumberInput)", + "/div//NumberInput/img", + "/div//NumberInput/div", "/div//NumberInput/div:data-testid='opacity-mouse-down-handler'", "/div//NumberInput/Symbol(react.forward_ref)(EmotionCssPropInternal)", "/div//NumberInput/Symbol(react.forward_ref)(EmotionCssPropInternal)", @@ -2505,6 +2529,8 @@ Array [ "/OpacityRow/UtopiaSpiedFunctionComponent(InspectorContextMenuWrapper)/Symbol(react.forward_ref)(EmotionCssPropInternal)/div", "/Symbol(react.forward_ref)(EmotionCssPropInternal)/div/UtopiaSpiedFunctionComponent(MenuProvider)/div", "/UtopiaSpiedFunctionComponent(MenuProvider)/div//Symbol(react.memo)(NumberInput)", + "/div//NumberInput/img", + "/div//NumberInput/div", "/div//NumberInput/div:data-testid='opacity-mouse-down-handler'", "/div//NumberInput/Symbol(react.forward_ref)(EmotionCssPropInternal)", "/div//NumberInput/Symbol(react.forward_ref)(EmotionCssPropInternal)", @@ -2906,6 +2932,8 @@ Array [ "/OpacityRow/UtopiaSpiedFunctionComponent(InspectorContextMenuWrapper)/Symbol(react.forward_ref)(EmotionCssPropInternal)/div", "/Symbol(react.forward_ref)(EmotionCssPropInternal)/div/UtopiaSpiedFunctionComponent(MenuProvider)/div", "/UtopiaSpiedFunctionComponent(MenuProvider)/div//Symbol(react.memo)(NumberInput)", + "/div//NumberInput/img", + "/div//NumberInput/div", "/div//NumberInput/div:data-testid='opacity-mouse-down-handler'", "/div//NumberInput/Symbol(react.forward_ref)(EmotionCssPropInternal)", "/div//NumberInput/Symbol(react.forward_ref)(EmotionCssPropInternal)", diff --git a/editor/src/core/performance/performance-regression-tests.spec.tsx b/editor/src/core/performance/performance-regression-tests.spec.tsx index ef7f9349c484..a2bd8d40f165 100644 --- a/editor/src/core/performance/performance-regression-tests.spec.tsx +++ b/editor/src/core/performance/performance-regression-tests.spec.tsx @@ -65,7 +65,7 @@ describe('React Render Count Tests -', () => { const renderCountAfter = renderResult.getNumberOfRenders() // if this breaks, GREAT NEWS but update the test please :) - expect(renderCountAfter - renderCountBefore).toMatchInlineSnapshot(`749`) + expect(renderCountAfter - renderCountBefore).toMatchInlineSnapshot(`753`) expect(renderResult.getRenderInfo()).toMatchSnapshot() }) @@ -127,7 +127,7 @@ describe('React Render Count Tests -', () => { const renderCountAfter = renderResult.getNumberOfRenders() // if this breaks, GREAT NEWS but update the test please :) - expect(renderCountAfter - renderCountBefore).toMatchInlineSnapshot(`1097`) + expect(renderCountAfter - renderCountBefore).toMatchInlineSnapshot(`1101`) expect(renderResult.getRenderInfo()).toMatchSnapshot() }) @@ -183,7 +183,7 @@ describe('React Render Count Tests -', () => { const renderCountAfter = renderResult.getNumberOfRenders() // if this breaks, GREAT NEWS but update the test please :) - expect(renderCountAfter - renderCountBefore).toMatchInlineSnapshot(`529`) + expect(renderCountAfter - renderCountBefore).toMatchInlineSnapshot(`539`) expect(renderResult.getRenderInfo()).toMatchSnapshot() }) @@ -249,7 +249,7 @@ describe('React Render Count Tests -', () => { const renderCountAfter = renderResult.getNumberOfRenders() // if this breaks, GREAT NEWS but update the test please :) - expect(renderCountAfter - renderCountBefore).toMatchInlineSnapshot(`702`) + expect(renderCountAfter - renderCountBefore).toMatchInlineSnapshot(`712`) expect(renderResult.getRenderInfo()).toMatchSnapshot() }) }) diff --git a/editor/src/uuiui/inputs/number-input.tsx b/editor/src/uuiui/inputs/number-input.tsx index 82d079451b53..597f801eecd0 100644 --- a/editor/src/uuiui/inputs/number-input.tsx +++ b/editor/src/uuiui/inputs/number-input.tsx @@ -35,7 +35,7 @@ import type { } from '../../components/inspector/controls/control' import type { Either } from '../../core/shared/either' import { isLeft, mapEither } from '../../core/shared/either' -import { clampValue } from '../../core/shared/math-utils' +import { clampValue, point } from '../../core/shared/math-utils' import { memoize } from '../../core/shared/memoize' import type { ControlStyles } from '../../uuiui-deps' import { getControlStyles, CSSCursor } from '../../uuiui-deps' @@ -51,8 +51,7 @@ import { } from './base-input' import { usePropControlledStateV2 } from '../../components/inspector/common/inspector-utils' import { useControlsDisabledInSubtree } from '../utilities/disable-subtree' - -export type LabelDragDirection = 'horizontal' | 'vertical' +import { getPossiblyHashedURL } from '../../utils/hashed-assets' function getDisplayValueNotMemoized( value: CSSNumber | null, @@ -88,36 +87,20 @@ function parseDisplayValueNotMemoized( const parseDisplayValue = memoize(parseDisplayValueNotMemoized, { maxSize: 1000 }) -const dragDeltaSign = (start: number, end: number, directionAdjustment: 1 | -1): 1 | -1 => { - const raw = (start - end) * directionAdjustment - return raw >= 0 ? 1 : -1 +function dragDeltaSign(delta: number): 1 | -1 { + return delta >= 0 ? 1 : -1 } -const calculateDragDirectionDelta = ( - start: number, - end: number, - scalingFactor: number, - directionAdjustment: 1 | -1, -): number => { - const sign = dragDeltaSign(start, end, directionAdjustment) - const rawAbsDelta = Math.abs(start - end) +function calculateDragDirectionDelta(delta: number, scalingFactor: number): number { + const sign = dragDeltaSign(delta) + const rawAbsDelta = Math.abs(delta) + // Floor the value and then restore its sign so that it is rounded towards zero. const scaledAbsDelta = Math.floor(rawAbsDelta / scalingFactor) return sign * scaledAbsDelta } -const calculateDragDelta = ( - dragOriginX: number, - dragOriginY: number, - dragScreenX: number, - dragScreenY: number, - labelDragDirection: LabelDragDirection, - scalingFactor: number = 2, -) => { - if (labelDragDirection === 'horizontal') { - return calculateDragDirectionDelta(dragOriginX, dragScreenX, scalingFactor, 1) - } else { - return calculateDragDirectionDelta(dragOriginY, dragScreenY, scalingFactor, -1) - } +function calculateDragDelta(delta: number, scalingFactor: number = 2): number { + return calculateDragDirectionDelta(delta, scalingFactor) } let incrementTimeout: number | undefined = undefined @@ -226,29 +209,15 @@ export const NumberInput = React.memo( const [isFauxcused, setIsFauxcused] = React.useState(false) const isFocused = isActuallyFocused || isFauxcused - const [labelDragDirection, setLabelDragDirection] = - React.useState('horizontal') - const [, setValueAtDragOriginState] = React.useState(0) - const valueAtDragOrigin = React.useRef(0) + const valueAtDragOrigin = React.useRef(null) const setValueAtDragOrigin = (n: number) => { valueAtDragOrigin.current = n setValueAtDragOriginState(n) } - const [, setDragOriginXState] = React.useState(-Infinity) - const dragOriginX = React.useRef(-Infinity) - const setDragOriginX = (n: number) => { - dragOriginX.current = n - setDragOriginXState(n) - } - - const [, setDragOriginYState] = React.useState(-Infinity) - const dragOriginY = React.useRef(-Infinity) - const setDragOriginY = (n: number) => { - dragOriginY.current = n - setDragOriginYState(n) - } + const [dragOriginX, setDragOriginX] = React.useState(null) + const [dragOriginY, setDragOriginY] = React.useState(null) const [, setScrubThresholdPassedState] = React.useState(false) const scrubThresholdPassed = React.useRef(false) @@ -257,6 +226,15 @@ export const NumberInput = React.memo( setScrubThresholdPassedState(b) } + const simulatedPointerRef = React.useRef(null) + const pointerOriginRef = React.useRef(null) + + const accumulatedMouseDeltaX = React.useRef(0) + // This is here to alleviate a circular reference issue that I stumbled into with the callbacks, + // it means that the cleanup callback isn't dependent on the event listeners, which result in + // a break in the circle. + const scrubbingCleanupCallbacks = React.useRef void>>([]) + const [valueChangedSinceFocus, setValueChangedSinceFocus] = React.useState(false) const scaleFactor = valueUnit === '%' ? 100 : 1 @@ -327,53 +305,41 @@ export const NumberInput = React.memo( ) const setScrubValue = React.useCallback( - ( - unit: CSSNumberUnit | null, - screenX: number, - screenY: number, - scrubDragOriginX: number, - scrubDragOriginY: number, - transient: boolean, - ) => { - const primaryAxisDelta = calculateDragDelta( - scrubDragOriginX, - scrubDragOriginY, - screenX, - screenY, - labelDragDirection, - ) - const numericValue = clampValue( - valueAtDragOrigin.current - stepSize * primaryAxisDelta, - minimum, - maximum, - ) - const newValue = cssNumber(numericValue, unit) - - if (transient) { - if (onTransientSubmitValue != null) { - onTransientSubmitValue(newValue) - } else if (onSubmitValue != null) { - onSubmitValue(newValue) - } - } else { - if (onForcedSubmitValue != null) { - onForcedSubmitValue(newValue) - } else if (onSubmitValue != null) { - onSubmitValue(newValue) + (transient: boolean) => { + const dragDelta = calculateDragDelta(accumulatedMouseDeltaX.current) + if (valueAtDragOrigin.current != null) { + const numericValue = clampValue( + valueAtDragOrigin.current + stepSize * dragDelta, + minimum, + maximum, + ) + const newValue = cssNumber(numericValue, valueUnit) + + if (transient) { + if (onTransientSubmitValue != null) { + onTransientSubmitValue(newValue) + } else if (onSubmitValue != null) { + onSubmitValue(newValue) + } + } else { + if (onForcedSubmitValue != null) { + onForcedSubmitValue(newValue) + } else if (onSubmitValue != null) { + onSubmitValue(newValue) + } } + updateValue(newValue) } - updateValue(newValue) - return newValue }, [ - labelDragDirection, - maximum, - minimum, stepSize, - onForcedSubmitValue, - onSubmitValue, - onTransientSubmitValue, + minimum, + maximum, + valueUnit, updateValue, + onTransientSubmitValue, + onSubmitValue, + onForcedSubmitValue, ], ) @@ -385,53 +351,79 @@ export const NumberInput = React.memo( const onThresholdPassed = (e: MouseEvent, fn: () => void) => { const thresholdPassed = - scrubThresholdPassed.current || Math.abs(e.screenX - dragOriginX.current) >= ScrubThreshold + scrubThresholdPassed.current || Math.abs(accumulatedMouseDeltaX.current) >= ScrubThreshold if (thresholdPassed) { fn() } } - const scrubOnMouseMove = React.useCallback( + const cancelPointerLock = React.useCallback( + (revertChanges: 'revert-nothing' | 'revert-changes') => { + if (document.pointerLockElement === pointerOriginRef.current) { + document.exitPointerLock() + } + if ( + revertChanges === 'revert-changes' && + onSubmitValue != null && + valueAtDragOrigin.current != null + ) { + const oldValue = cssNumber(valueAtDragOrigin.current, valueUnit) + onSubmitValue(oldValue, false) + } + + setIsFauxcused(false) + ref.current?.focus() + + setScrubThresholdPassed(false) + setGlobalCursor?.(null) + }, + [onSubmitValue, setGlobalCursor, valueUnit], + ) + + const checkPointerLockChange = React.useCallback(() => { + if (document.pointerLockElement !== pointerOriginRef.current) { + cancelPointerLock('revert-changes') + scrubbingCleanupCallbacks.current.forEach((fn) => fn()) + } + }, [cancelPointerLock]) + + const scrubOnMouseUp = React.useCallback( (e: MouseEvent) => { + scrubbingCleanupCallbacks.current.forEach((fn) => fn()) onThresholdPassed(e, () => { - if (!scrubThresholdPassed.current) { - setScrubThresholdPassed(true) - } - setScrubValue( - valueUnit, - e.screenX, - e.screenY, - dragOriginX.current, - dragOriginY.current, - true, - ) + setScrubValue(false) }) + cancelPointerLock('revert-nothing') }, - [setScrubValue, valueUnit], + [cancelPointerLock, setScrubValue], ) - const scrubOnMouseUp = React.useCallback( + const scrubOnMouseMove = React.useCallback( (e: MouseEvent) => { - window.removeEventListener('mouseup', scrubOnMouseUp) - window.removeEventListener('mousemove', scrubOnMouseMove) - - setIsFauxcused(false) - ref.current?.focus() + // Apply the movement to the accumulated delta, as the movement is + // relative to the last event. + accumulatedMouseDeltaX.current += e.movementX onThresholdPassed(e, () => { - setScrubValue( - valueUnit, - e.screenX, - e.screenY, - dragOriginX.current, - dragOriginY.current, - false, - ) + if (!scrubThresholdPassed.current) { + setScrubThresholdPassed(true) + if (pointerOriginRef.current != null) { + pointerOriginRef.current.requestPointerLock() + scrubbingCleanupCallbacks.current.push(() => { + window.removeEventListener('mouseup', scrubOnMouseUp) + }) + scrubbingCleanupCallbacks.current.push(() => { + window.removeEventListener('mousemove', scrubOnMouseMove) + }) + scrubbingCleanupCallbacks.current.push(() => { + document.removeEventListener('pointerlockchange', checkPointerLockChange, true) + }) + } + } + setScrubValue(true) }) - setScrubThresholdPassed(false) - setGlobalCursor?.(null) }, - [scrubOnMouseMove, setScrubValue, valueUnit, ref, setGlobalCursor], + [checkPointerLockChange, scrubOnMouseUp, setScrubValue], ) const rc = roundCorners == null ? 'all' : roundCorners @@ -461,7 +453,7 @@ export const NumberInput = React.memo( ref.current?.blur() } }, - [incrementBy, stepSize, ref, updateValue], + [updateValue, incrementBy, stepSize], ) const onKeyUp = React.useCallback( @@ -634,14 +626,23 @@ export const NumberInput = React.memo( setIsFauxcused(true) window.addEventListener('mousemove', scrubOnMouseMove) window.addEventListener('mouseup', scrubOnMouseUp) - setLabelDragDirection('horizontal') + document.addEventListener('pointerlockchange', checkPointerLockChange, true) setValueAtDragOrigin(value?.value ?? 0) - setDragOriginX(e.screenX) - setDragOriginY(e.screenY) + setDragOriginX(e.pageX) + setDragOriginY(e.pageY) setGlobalCursor?.(CSSCursor.ResizeEW) + accumulatedMouseDeltaX.current = 0 } }, - [scrubOnMouseMove, scrubOnMouseUp, setGlobalCursor, value, disabled, disableScrubbing], + [ + disabled, + disableScrubbing, + scrubOnMouseMove, + scrubOnMouseUp, + checkPointerLockChange, + value?.value, + setGlobalCursor, + ], ) const placeholder = getControlStylesAwarePlaceholder(controlStyles) @@ -663,8 +664,47 @@ export const NumberInput = React.memo( } : undefined + let simulatedPointerTransformX: number | undefined = undefined + if (pointerOriginRef.current != null && scrubThresholdPassed.current && dragOriginX != null) { + const pointerOriginRect = pointerOriginRef.current.getBoundingClientRect() + const intendedPointerX = + (pointerOriginRect.left + accumulatedMouseDeltaX.current) % window.screen.width + simulatedPointerTransformX = intendedPointerX - pointerOriginRect.left + } + return ( -
+
+
+ +