Editor popover for SequenceLengthIndicator
This commit is contained in:
parent
3745ce02ff
commit
58e9d9ff8b
8 changed files with 134 additions and 20 deletions
|
@ -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",
|
||||
|
|
|
@ -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<SequenceEditorPanelLayout>
|
||||
/**
|
||||
* 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<HTMLInputElement>(null)
|
||||
useLayoutEffect(() => {
|
||||
inputRef.current!.focus()
|
||||
}, [])
|
||||
|
||||
return usePrism(() => {
|
||||
const sequence = sheet.getSequence()
|
||||
const sequenceLength = sequence.length
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Label>Sequence length</Label>
|
||||
<BasicNumberInput
|
||||
value={sequenceLength}
|
||||
{...fns}
|
||||
isValid={greaterThanZero}
|
||||
inputRef={inputRef}
|
||||
onBlur={onRequestClose}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}, [sheet, fns, inputRef])
|
||||
}
|
||||
|
||||
export default LengthEditorPopover
|
|
@ -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<IProps> = ({layoutP}) => {
|
||||
const [nodeRef, node] = useRefAndState<HTMLDivElement | null>(null)
|
||||
const [isDraggingD] = useDragBulge(node, {layoutP})
|
||||
const [popoverNode, openPopover, _, isPopoverOpen] = usePopover(() => {
|
||||
return <div>poppio</div>
|
||||
})
|
||||
const [popoverNode, openPopover, closePopover, isPopoverOpen] = usePopover(
|
||||
() => {
|
||||
return (
|
||||
<LengthEditorPopover layoutP={layoutP} onRequestClose={closePopover} />
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
return usePrism(() => {
|
||||
const sheet = val(layoutP.sheet)
|
|
@ -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'
|
||||
|
|
|
@ -392,7 +392,7 @@ namespace stateEditors {
|
|||
export function setLength(
|
||||
p: WithoutSheetInstance<SheetAddress> & {length: number},
|
||||
) {
|
||||
_ensure(p).length = p.length
|
||||
_ensure(p).length = parseFloat(p.length.toFixed(2))
|
||||
}
|
||||
|
||||
function _ensureTracksOfObject(
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<HTMLInputElement | null>
|
||||
/**
|
||||
* 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<IState>({mode: 'noFocus'})
|
||||
const isValid = propsA.isValid ?? alwaysValid
|
||||
|
||||
const refs = useRef({state: stateA, props: propsA})
|
||||
refs.current = {state: stateA, props: propsA}
|
||||
|
||||
const inputRef = useRef<HTMLInputElement | null>(null)
|
||||
|
||||
const bodyCursorBeforeDrag = useRef<string | null>(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 = (
|
||||
<Input
|
||||
key="input"
|
||||
|
@ -266,7 +282,7 @@ const BasicNumberInput: React.FC<{
|
|||
onKeyDown={callbacks.onInputKeyDown}
|
||||
onClick={callbacks.onClick}
|
||||
onFocus={callbacks.onFocus}
|
||||
ref={inputRef}
|
||||
ref={mergeRefs(_refs)}
|
||||
onMouseDown={(e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue