diff --git a/theatre/package.json b/theatre/package.json index 4deaade..aada9e9 100644 --- a/theatre/package.json +++ b/theatre/package.json @@ -74,6 +74,7 @@ "react-error-boundary": "^3.1.3", "react-icons": "^4.2.0", "react-is": "^17.0.2", + "react-merge-refs": "^1.1.0", "react-shadow": "^19.0.2", "react-use": "^17.2.4", "react-use-gesture": "^9.1.3", diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/LengthIndicator/LengthEditorPopover.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/LengthIndicator/LengthEditorPopover.tsx new file mode 100644 index 0000000..f06ff99 --- /dev/null +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/LengthIndicator/LengthEditorPopover.tsx @@ -0,0 +1,96 @@ +import type {Pointer} from '@theatre/dataverse' +import React, {useLayoutEffect, useMemo, useRef} from 'react' +import styled from 'styled-components' +import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout' +import {usePrism, useVal} from '@theatre/dataverse-react' +import getStudio from '@theatre/studio/getStudio' +import BasicNumberInput from '@theatre/studio/uiComponents/form/BasicNumberInput' +import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore' +import {propNameText} from '@theatre/studio/panels/ObjectEditorPanel/propEditors/utils/SingleRowPropEditor' + +const greaterThanZero = (v: number) => isFinite(v) && v > 0 + +const Container = styled.div` + display: flex; + gap: 8px; + padding: 4px 8px; + height: 28px; + align-items: center; +` + +const Label = styled.div` + ${propNameText}; + white-space: nowrap; +` + +const LengthEditorPopover: React.FC<{ + layoutP: Pointer + /** + * Called when user hits enter/escape + */ + onRequestClose: () => void +}> = ({layoutP, onRequestClose}) => { + const sheet = useVal(layoutP.sheet) + + const fns = useMemo(() => { + let tempTransaction: CommitOrDiscard | undefined + + return { + temporarilySetValue(newLength: number): void { + if (tempTransaction) { + tempTransaction.discard() + tempTransaction = undefined + } + tempTransaction = getStudio()!.tempTransaction(({stateEditors}) => { + stateEditors.coreByProject.historic.sheetsById.sequence.setLength({ + ...sheet.address, + length: newLength, + }) + }) + }, + discardTemporaryValue(): void { + if (tempTransaction) { + tempTransaction.discard() + tempTransaction = undefined + } + }, + permenantlySetValue(newLength: number): void { + if (tempTransaction) { + tempTransaction.discard() + tempTransaction = undefined + } + getStudio()!.transaction(({stateEditors}) => { + stateEditors.coreByProject.historic.sheetsById.sequence.setLength({ + ...sheet.address, + length: newLength, + }) + }) + }, + } + }, [layoutP, sheet]) + + const inputRef = useRef(null) + useLayoutEffect(() => { + inputRef.current!.focus() + }, []) + + return usePrism(() => { + const sequence = sheet.getSequence() + const sequenceLength = sequence.length + + return ( + + + + + ) + }, [sheet, fns, inputRef]) +} + +export default LengthEditorPopover diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/LengthIndicator.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/LengthIndicator/LengthIndicator.tsx similarity index 96% rename from theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/LengthIndicator.tsx rename to theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/LengthIndicator/LengthIndicator.tsx index 0a334b4..869d51f 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/LengthIndicator.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/LengthIndicator/LengthIndicator.tsx @@ -17,6 +17,7 @@ import { useLockFrameStampPosition, } from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider' import {GoChevronLeft, GoChevronRight} from 'react-icons/all' +import LengthEditorPopover from './LengthEditorPopover' const coverWidth = 1000 @@ -130,9 +131,13 @@ type IProps = { const LengthIndicator: React.FC = ({layoutP}) => { const [nodeRef, node] = useRefAndState(null) const [isDraggingD] = useDragBulge(node, {layoutP}) - const [popoverNode, openPopover, _, isPopoverOpen] = usePopover(() => { - return
poppio
- }) + const [popoverNode, openPopover, closePopover, isPopoverOpen] = usePopover( + () => { + return ( + + ) + }, + ) return usePrism(() => { const sheet = val(layoutP.sheet) diff --git a/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/RightOverlay.tsx b/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/RightOverlay.tsx index 72248a7..eafc0cb 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/RightOverlay.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/RightOverlay.tsx @@ -5,7 +5,7 @@ import type {Pointer} from '@theatre/dataverse' import {val} from '@theatre/dataverse' import React from 'react' import styled from 'styled-components' -import LengthIndicator from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/LengthIndicator' +import LengthIndicator from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/LengthIndicator/LengthIndicator' import FrameStamp from './FrameStamp' import HorizontalScrollbar from './HorizontalScrollbar' import Playhead from './Playhead' diff --git a/theatre/studio/src/store/stateEditors.ts b/theatre/studio/src/store/stateEditors.ts index 51909a4..9807157 100644 --- a/theatre/studio/src/store/stateEditors.ts +++ b/theatre/studio/src/store/stateEditors.ts @@ -392,7 +392,7 @@ namespace stateEditors { export function setLength( p: WithoutSheetInstance & {length: number}, ) { - _ensure(p).length = p.length + _ensure(p).length = parseFloat(p.length.toFixed(2)) } function _ensureTracksOfObject( diff --git a/theatre/studio/src/uiComponents/Popover/Popover.tsx b/theatre/studio/src/uiComponents/Popover/Popover.tsx index b6e6e51..afa34c9 100644 --- a/theatre/studio/src/uiComponents/Popover/Popover.tsx +++ b/theatre/studio/src/uiComponents/Popover/Popover.tsx @@ -6,8 +6,6 @@ import {createPortal} from 'react-dom' import useWindowSize from 'react-use/esm/useWindowSize' import styled from 'styled-components' -const minWidth = 190 - /** * How far from the menu should the pointer travel to auto close the menu */ @@ -15,14 +13,11 @@ const defaultPointerDistanceThreshold = 200 const Container = styled.ul` position: absolute; - min-width: ${minWidth}px; z-index: 10000; background: ${transparentize(0.2, '#111')}; color: white; - list-style-type: none; - padding: 2px 0; + padding: 0; margin: 0; - border-radius: 1px; cursor: default; pointer-events: all; border-radius: 3px; diff --git a/theatre/studio/src/uiComponents/form/BasicNumberInput.tsx b/theatre/studio/src/uiComponents/form/BasicNumberInput.tsx index 2cf4b4a..d9a902c 100644 --- a/theatre/studio/src/uiComponents/form/BasicNumberInput.tsx +++ b/theatre/studio/src/uiComponents/form/BasicNumberInput.tsx @@ -1,9 +1,11 @@ import {theme} from '@theatre/studio/css' import {clamp, isInteger, round} from 'lodash-es' import {darken, lighten} from 'polished' +import type {MutableRefObject} from 'react' import React, {useMemo, useRef, useState} from 'react' import styled from 'styled-components' import DraggableArea from '@theatre/studio/uiComponents/DraggableArea' +import mergeRefs from 'react-merge-refs' type IMode = IState['mode'] @@ -75,9 +77,8 @@ const FillIndicator = styled.div` } ` -function isValueAcceptable(s: string) { - const v = parseFloat(s) - return !isNaN(v) +function isFiniteFloat(s: string) { + return isFinite(parseFloat(s)) } type IState_NoFocus = { @@ -98,6 +99,8 @@ type IState_Dragging = { type IState = IState_NoFocus | IState_EditingViaKeyboard | IState_Dragging +const alwaysValid = (v: number) => true + const BasicNumberInput: React.FC<{ value: number temporarilySetValue: (v: number) => void @@ -105,13 +108,22 @@ const BasicNumberInput: React.FC<{ permenantlySetValue: (v: number) => void className?: string range?: [min: number, max: number] + isValid?: (v: number) => boolean + inputRef?: MutableRefObject + /** + * Called when the user hits Enter. One of the *SetValue() callbacks will be called + * before this, so use this for UI purposes such as closing a popover. + */ + onBlur?: () => void }> = (propsA) => { const [stateA, setState] = useState({mode: 'noFocus'}) + const isValid = propsA.isValid ?? alwaysValid const refs = useRef({state: stateA, props: propsA}) refs.current = {state: stateA, props: propsA} const inputRef = useRef(null) + const bodyCursorBeforeDrag = useRef(null) const callbacks = useMemo(() => { @@ -122,9 +134,9 @@ const BasicNumberInput: React.FC<{ setState({...curState, currentEditedValueInString: value}) - if (!isValueAcceptable(value)) return - const valInFloat = parseFloat(value) + if (!isFinite(valInFloat) || !isValid(valInFloat)) return + refs.current.props.temporarilySetValue(valInFloat) } @@ -132,16 +144,17 @@ const BasicNumberInput: React.FC<{ if (refs.current.state.mode === 'editingViaKeyboard') { commitKeyboardInput() setState({mode: 'noFocus'}) - } else { } + if (propsA.onBlur) propsA.onBlur() } const commitKeyboardInput = () => { const curState = refs.current.state as IState_EditingViaKeyboard - if (!isValueAcceptable(curState.currentEditedValueInString)) { + const value = parseFloat(curState.currentEditedValueInString) + + if (!isFinite(value) || !isValid(value)) { refs.current.props.discardTemporaryValue() } else { - const value = parseFloat(curState.currentEditedValueInString) if (curState.valueBeforeEditing === value) { refs.current.props.discardTemporaryValue() } else { @@ -256,6 +269,9 @@ const BasicNumberInput: React.FC<{ value = 'NaN' } + const _refs = [inputRef] + if (propsA.inputRef) _refs.push(propsA.inputRef) + const theInput = ( { e.stopPropagation() }} diff --git a/yarn.lock b/yarn.lock index 2025759..b5f7a76 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16436,6 +16436,7 @@ fsevents@^1.2.7: react-error-boundary: ^3.1.3 react-icons: ^4.2.0 react-is: ^17.0.2 + react-merge-refs: ^1.1.0 react-shadow: ^19.0.2 react-use: ^17.2.4 react-use-gesture: ^9.1.3