QOL improvements to the FocusRange and SequenceEdito (#125)
This commit is contained in:
parent
d85e3053af
commit
3ecc3dd012
16 changed files with 571 additions and 380 deletions
|
@ -11,14 +11,10 @@ import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||||
|
|
||||||
const Base = styled.div`
|
const Base = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 10;
|
|
||||||
${pointerEventsAutoInNormalMode};
|
${pointerEventsAutoInNormalMode};
|
||||||
&:after {
|
&:after {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -2px;
|
inset: -5px;
|
||||||
right: -2px;
|
|
||||||
bottom: -2px;
|
|
||||||
left: -2px;
|
|
||||||
display: block;
|
display: block;
|
||||||
content: ' ';
|
content: ' ';
|
||||||
}
|
}
|
||||||
|
@ -40,55 +36,70 @@ const Base = styled.div`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const Horizontal = styled(Base)`
|
const Side = styled(Base)`
|
||||||
left: 0;
|
/**
|
||||||
right: 0;
|
The horizintal/vertical resize handles have z-index:-1 and are offset 1px outside of the panel
|
||||||
|
to make sure they don't occlude any element that pops out of the panel (like the Playhead in SequenceEditorPanel).
|
||||||
|
|
||||||
|
This means that panels will always need an extra 1px margin for their resize handles to be visible, but that's not a problem
|
||||||
|
that we have to deal with right now (if it is at all a problem).
|
||||||
|
|
||||||
|
*/
|
||||||
|
z-index: -1;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Horizontal = styled(Side)`
|
||||||
|
left: 0px;
|
||||||
|
right: 0px;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const Top = styled(Horizontal)`
|
const Top = styled(Horizontal)`
|
||||||
top: 0;
|
top: -1px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const Bottom = styled(Horizontal)`
|
const Bottom = styled(Horizontal)`
|
||||||
bottom: 0;
|
bottom: -1px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const Vertical = styled(Base)`
|
const Vertical = styled(Side)`
|
||||||
top: 0;
|
z-index: -1;
|
||||||
bottom: 0;
|
top: -1px;
|
||||||
|
bottom: -1px;
|
||||||
width: 1px;
|
width: 1px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const Left = styled(Vertical)`
|
const Left = styled(Vertical)`
|
||||||
left: 0;
|
left: -1px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const Right = styled(Vertical)`
|
const Right = styled(Vertical)`
|
||||||
right: 0;
|
right: -1px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const Square = styled(Base)`
|
const Angle = styled(Base)`
|
||||||
|
// The angles have z-index: 10 to make sure they _do_ occlude other elements in the panel.
|
||||||
|
z-index: 10;
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const TopLeft = styled(Square)`
|
const TopLeft = styled(Angle)`
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
`
|
`
|
||||||
|
|
||||||
const TopRight = styled(Square)`
|
const TopRight = styled(Angle)`
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
`
|
`
|
||||||
|
|
||||||
const BottomLeft = styled(Square)`
|
const BottomLeft = styled(Angle)`
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
`
|
`
|
||||||
|
|
||||||
const BottomRight = styled(Square)`
|
const BottomRight = styled(Angle)`
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
`
|
`
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import styled from 'styled-components'
|
|
||||||
import PanelResizeHandle from './PanelResizeHandle'
|
import PanelResizeHandle from './PanelResizeHandle'
|
||||||
|
|
||||||
const Container = styled.div``
|
|
||||||
|
|
||||||
const PanelResizers: React.FC<{}> = (props) => {
|
const PanelResizers: React.FC<{}> = (props) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -14,7 +14,10 @@ import styled from 'styled-components'
|
||||||
import type KeyframeEditor from './KeyframeEditor'
|
import type KeyframeEditor from './KeyframeEditor'
|
||||||
import {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
import {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||||
import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||||
import {useCssCursorLock} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
import {
|
||||||
|
lockedCursorCssPropName,
|
||||||
|
useCssCursorLock,
|
||||||
|
} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
||||||
import SnapCursor from './SnapCursor.svg'
|
import SnapCursor from './SnapCursor.svg'
|
||||||
import selectedKeyframeIdsIfInSingleTrack from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/selectedKeyframeIdsIfInSingleTrack'
|
import selectedKeyframeIdsIfInSingleTrack from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/selectedKeyframeIdsIfInSingleTrack'
|
||||||
|
|
||||||
|
@ -57,6 +60,8 @@ const HitZone = styled.div`
|
||||||
|
|
||||||
#pointer-root.draggingPositionInSequenceEditor & {
|
#pointer-root.draggingPositionInSequenceEditor & {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
|
cursor: var(${lockedCursorCssPropName});
|
||||||
|
|
||||||
&:hover:after {
|
&:hover:after {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(50% - ${snapCursorSize / 2}px);
|
top: calc(50% - ${snapCursorSize / 2}px);
|
||||||
|
|
|
@ -1,86 +0,0 @@
|
||||||
import type {Pointer} from '@theatre/dataverse'
|
|
||||||
import {prism, val} from '@theatre/dataverse'
|
|
||||||
import {usePrism} from '@theatre/react'
|
|
||||||
import getStudio from '@theatre/studio/getStudio'
|
|
||||||
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
|
||||||
import {topStripHeight} from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/TopStrip'
|
|
||||||
import React, {useMemo} from 'react'
|
|
||||||
import styled from 'styled-components'
|
|
||||||
|
|
||||||
const focusRangeAreaTheme = {
|
|
||||||
enabled: {
|
|
||||||
backgroundColor: '#646568',
|
|
||||||
opacity: 0.05,
|
|
||||||
},
|
|
||||||
disabled: {
|
|
||||||
backgroundColor: '#646568',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const Container = styled.div`
|
|
||||||
position: absolute;
|
|
||||||
opacity: ${focusRangeAreaTheme.enabled.opacity};
|
|
||||||
background: transparent;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
`
|
|
||||||
const FocusRangeArea: React.FC<{
|
|
||||||
layoutP: Pointer<SequenceEditorPanelLayout>
|
|
||||||
}> = ({layoutP}) => {
|
|
||||||
const existingRangeD = useMemo(
|
|
||||||
() =>
|
|
||||||
prism(() => {
|
|
||||||
const {projectId, sheetId} = val(layoutP.sheet).address
|
|
||||||
const existingRange = val(
|
|
||||||
getStudio().atomP.ahistoric.projects.stateByProjectId[projectId]
|
|
||||||
.stateBySheetId[sheetId].sequence.focusRange,
|
|
||||||
)
|
|
||||||
return existingRange
|
|
||||||
}),
|
|
||||||
[layoutP],
|
|
||||||
)
|
|
||||||
|
|
||||||
return usePrism(() => {
|
|
||||||
const existingRange = existingRangeD.getValue()
|
|
||||||
|
|
||||||
const range = existingRange?.range || {start: 0, end: 0}
|
|
||||||
|
|
||||||
const height = val(layoutP.rightDims.height) + topStripHeight
|
|
||||||
|
|
||||||
let startPosInClippedSpace: number,
|
|
||||||
endPosInClippedSpace: number,
|
|
||||||
conditionalStyleProps:
|
|
||||||
| {
|
|
||||||
width: number
|
|
||||||
transform: string
|
|
||||||
background?: string
|
|
||||||
}
|
|
||||||
| undefined
|
|
||||||
|
|
||||||
if (existingRange !== undefined) {
|
|
||||||
startPosInClippedSpace = val(layoutP.clippedSpace.fromUnitSpace)(
|
|
||||||
range.start,
|
|
||||||
)
|
|
||||||
|
|
||||||
endPosInClippedSpace = val(layoutP.clippedSpace.fromUnitSpace)(range.end)
|
|
||||||
|
|
||||||
conditionalStyleProps = {
|
|
||||||
width: endPosInClippedSpace - startPosInClippedSpace,
|
|
||||||
transform: `translate3d(${
|
|
||||||
startPosInClippedSpace - val(layoutP.clippedSpace.fromUnitSpace)(0)
|
|
||||||
}px, 0, 0)`,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingRange.enabled === true) {
|
|
||||||
conditionalStyleProps.background =
|
|
||||||
focusRangeAreaTheme.enabled.backgroundColor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container style={{...conditionalStyleProps, height: `${height}px`}} />
|
|
||||||
)
|
|
||||||
}, [layoutP, existingRangeD])
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FocusRangeArea
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
import type {Pointer} from '@theatre/dataverse'
|
||||||
|
import {prism, val} from '@theatre/dataverse'
|
||||||
|
import {usePrism} from '@theatre/react'
|
||||||
|
import getStudio from '@theatre/studio/getStudio'
|
||||||
|
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
||||||
|
import {topStripHeight} from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/TopStrip'
|
||||||
|
import React, {useMemo} from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
const divWidth = 1000
|
||||||
|
|
||||||
|
const Curtain = styled.div<{enabled: boolean}>`
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
opacity: 0.15;
|
||||||
|
top: ${topStripHeight}px;
|
||||||
|
width: ${divWidth}px;
|
||||||
|
transform-origin: top left;
|
||||||
|
z-index: 20;
|
||||||
|
pointer-events: none;
|
||||||
|
background-color: ${(props) => (props.enabled ? '#000000' : 'transparent')};
|
||||||
|
`
|
||||||
|
|
||||||
|
const FocusRangeCurtains: React.FC<{
|
||||||
|
layoutP: Pointer<SequenceEditorPanelLayout>
|
||||||
|
}> = ({layoutP}) => {
|
||||||
|
const existingRangeD = useMemo(
|
||||||
|
() =>
|
||||||
|
prism(() => {
|
||||||
|
const {projectId, sheetId} = val(layoutP.sheet).address
|
||||||
|
const existingRange = val(
|
||||||
|
getStudio().atomP.ahistoric.projects.stateByProjectId[projectId]
|
||||||
|
.stateBySheetId[sheetId].sequence.focusRange,
|
||||||
|
)
|
||||||
|
return existingRange
|
||||||
|
}),
|
||||||
|
[layoutP],
|
||||||
|
)
|
||||||
|
|
||||||
|
return usePrism(() => {
|
||||||
|
const existingRange = existingRangeD.getValue()
|
||||||
|
|
||||||
|
if (!existingRange || !existingRange.enabled) return null
|
||||||
|
|
||||||
|
const {range} = existingRange
|
||||||
|
|
||||||
|
const height = val(layoutP.rightDims.height)
|
||||||
|
|
||||||
|
const unitSpaceToClippedSpace = val(layoutP.clippedSpace.fromUnitSpace)
|
||||||
|
|
||||||
|
const els = [
|
||||||
|
[-1000, range.start],
|
||||||
|
[range.end, val(layoutP.clippedSpace.range.end)],
|
||||||
|
].map(([start, end], i) => {
|
||||||
|
const startPosInClippedSpace = unitSpaceToClippedSpace(start)
|
||||||
|
|
||||||
|
const endPosInClippedSpace = unitSpaceToClippedSpace(end)
|
||||||
|
const desiredWidth = endPosInClippedSpace - startPosInClippedSpace
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Curtain
|
||||||
|
key={`curtain-${i}`}
|
||||||
|
enabled={true}
|
||||||
|
style={{
|
||||||
|
height: `${height}px`,
|
||||||
|
transform: `translateX(${
|
||||||
|
val(layoutP.scaledSpace.leftPadding) +
|
||||||
|
startPosInClippedSpace -
|
||||||
|
unitSpaceToClippedSpace(0)
|
||||||
|
}px) scaleX(${desiredWidth / divWidth})`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return <>{els}</>
|
||||||
|
}, [layoutP, existingRangeD])
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FocusRangeCurtains
|
|
@ -10,7 +10,7 @@ import {zIndexes} from '@theatre/studio/panels/SequenceEditorPanel/SequenceEdito
|
||||||
import DopeSheetSelectionView from './DopeSheetSelectionView'
|
import DopeSheetSelectionView from './DopeSheetSelectionView'
|
||||||
import HorizontallyScrollableArea from './HorizontallyScrollableArea'
|
import HorizontallyScrollableArea from './HorizontallyScrollableArea'
|
||||||
import SheetRow from './SheetRow'
|
import SheetRow from './SheetRow'
|
||||||
import FocusRangeArea from './FocusRangeArea'
|
import FocusRangeCurtains from './FocusRangeCurtains'
|
||||||
|
|
||||||
export const contentWidth = 1000000
|
export const contentWidth = 1000000
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ const Background = styled.div<{width: number}>`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
width: ${(props) => props.width};
|
width: ${(props) => props.width}px;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
z-index: ${() => zIndexes.rightBackground};
|
z-index: ${() => zIndexes.rightBackground};
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -49,7 +49,7 @@ const Right: React.FC<{
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<HorizontallyScrollableArea layoutP={layoutP} height={height}>
|
<HorizontallyScrollableArea layoutP={layoutP} height={height}>
|
||||||
<FocusRangeArea layoutP={layoutP} />
|
<FocusRangeCurtains layoutP={layoutP} />
|
||||||
<DopeSheetSelectionView layoutP={layoutP}>
|
<DopeSheetSelectionView layoutP={layoutP}>
|
||||||
<ListContainer style={{top: tree.top + 'px'}}>
|
<ListContainer style={{top: tree.top + 'px'}}>
|
||||||
<SheetRow leaf={tree} layoutP={layoutP} />
|
<SheetRow leaf={tree} layoutP={layoutP} />
|
||||||
|
|
|
@ -179,7 +179,11 @@ const pointerPositionInUnitSpace = (
|
||||||
|
|
||||||
if (
|
if (
|
||||||
inRange(clientX, x, x + rightWidth) &&
|
inRange(clientX, x, x + rightWidth) &&
|
||||||
inRange(clientY, y, y + height)
|
inRange(
|
||||||
|
clientY,
|
||||||
|
y + 16 /* leaving a bit of space for the top stip here */,
|
||||||
|
y + height,
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
const posInRightDims = clientX - x
|
const posInRightDims = clientX - x
|
||||||
const posInUnitSpace = clippedSpaceToUnitSpace(posInRightDims)
|
const posInUnitSpace = clippedSpaceToUnitSpace(posInRightDims)
|
||||||
|
|
|
@ -13,7 +13,10 @@ import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Histo
|
||||||
import {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
import {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||||
import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||||
import {useCssCursorLock} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
import {
|
||||||
|
lockedCursorCssPropName,
|
||||||
|
useCssCursorLock,
|
||||||
|
} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
||||||
|
|
||||||
export const dotSize = 6
|
export const dotSize = 6
|
||||||
|
|
||||||
|
@ -42,6 +45,7 @@ const HitZone = styled.circle`
|
||||||
|
|
||||||
#pointer-root.draggingPositionInSequenceEditor & {
|
#pointer-root.draggingPositionInSequenceEditor & {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
|
cursor: var(${lockedCursorCssPropName});
|
||||||
}
|
}
|
||||||
|
|
||||||
&.beingDragged {
|
&.beingDragged {
|
||||||
|
|
|
@ -13,7 +13,10 @@ import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Histo
|
||||||
import {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
import {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||||
import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||||
import {useCssCursorLock} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
import {
|
||||||
|
lockedCursorCssPropName,
|
||||||
|
useCssCursorLock,
|
||||||
|
} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
||||||
|
|
||||||
export const dotSize = 6
|
export const dotSize = 6
|
||||||
|
|
||||||
|
@ -42,6 +45,7 @@ const HitZone = styled.circle`
|
||||||
|
|
||||||
#pointer-root.draggingPositionInSequenceEditor & {
|
#pointer-root.draggingPositionInSequenceEditor & {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
|
cursor: var(${lockedCursorCssPropName});
|
||||||
}
|
}
|
||||||
|
|
||||||
&.beingDragged {
|
&.beingDragged {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type {Pointer} from '@theatre/dataverse'
|
import type {Pointer} from '@theatre/dataverse'
|
||||||
import {prism, val} from '@theatre/dataverse'
|
import {prism, val} from '@theatre/dataverse'
|
||||||
import {usePrism} from '@theatre/react'
|
import {usePrism, useVal} from '@theatre/react'
|
||||||
import type {$IntentionalAny, IRange} from '@theatre/shared/utils/types'
|
import type {$IntentionalAny, IRange} from '@theatre/shared/utils/types'
|
||||||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||||
import getStudio from '@theatre/studio/getStudio'
|
import getStudio from '@theatre/studio/getStudio'
|
||||||
|
@ -19,17 +19,16 @@ export const focusRangeStripTheme = {
|
||||||
stroke: '#646568',
|
stroke: '#646568',
|
||||||
},
|
},
|
||||||
disabled: {
|
disabled: {
|
||||||
backgroundColor: '#282A2C',
|
backgroundColor: '#282a2cc5',
|
||||||
|
stroke: '#595a5d',
|
||||||
},
|
},
|
||||||
playing: {
|
hover: {
|
||||||
backgroundColor: 'red',
|
|
||||||
},
|
|
||||||
highlight: {
|
|
||||||
backgroundColor: '#34373D',
|
backgroundColor: '#34373D',
|
||||||
stroke: '#C8CAC0',
|
stroke: '#C8CAC0',
|
||||||
},
|
},
|
||||||
dragging: {
|
dragging: {
|
||||||
backgroundColor: '#3F444A',
|
backgroundColor: '#3F444A',
|
||||||
|
stroke: '#C8CAC0',
|
||||||
},
|
},
|
||||||
thumbWidth: 9,
|
thumbWidth: 9,
|
||||||
hitZoneWidth: 26,
|
hitZoneWidth: 26,
|
||||||
|
@ -38,22 +37,42 @@ export const focusRangeStripTheme = {
|
||||||
|
|
||||||
const stripWidth = 1000
|
const stripWidth = 1000
|
||||||
|
|
||||||
const RangeStrip = styled.div`
|
export const RangeStrip = styled.div<{enabled: boolean}>`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: ${() => topStripHeight};
|
height: ${() => topStripHeight - 1}px;
|
||||||
background-color: ${focusRangeStripTheme.enabled.backgroundColor};
|
background-color: ${(props) =>
|
||||||
|
props.enabled
|
||||||
|
? focusRangeStripTheme.enabled.backgroundColor
|
||||||
|
: focusRangeStripTheme.disabled.backgroundColor};
|
||||||
|
cursor: grab;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: ${stripWidth}px;
|
width: ${stripWidth}px;
|
||||||
transform-origin: left top;
|
transform-origin: left top;
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: ${focusRangeStripTheme.highlight.backgroundColor};
|
background-color: ${focusRangeStripTheme.hover.backgroundColor};
|
||||||
}
|
}
|
||||||
&.dragging {
|
&.dragging {
|
||||||
background-color: ${focusRangeStripTheme.dragging.backgroundColor};
|
background-color: ${focusRangeStripTheme.dragging.backgroundColor};
|
||||||
cursor: grabbing !important;
|
cursor: grabbing !important;
|
||||||
}
|
}
|
||||||
${pointerEventsAutoInNormalMode};
|
${pointerEventsAutoInNormalMode};
|
||||||
|
|
||||||
|
/* covers the one pixel space between the focus range strip and the top strip
|
||||||
|
of the sequence editor panel, which would have caused that one pixel to act
|
||||||
|
like a panel drag zone */
|
||||||
|
&:after {
|
||||||
|
display: block;
|
||||||
|
content: ' ';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -1px;
|
||||||
|
height: 1px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: transparent;
|
||||||
|
pointer-events: normal;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -108,14 +127,14 @@ const FocusRangeStrip: React.FC<{
|
||||||
[layoutP],
|
[layoutP],
|
||||||
)
|
)
|
||||||
|
|
||||||
const sheet = val(layoutP.sheet)
|
|
||||||
|
|
||||||
const [rangeStripRef, rangeStripNode] = useRefAndState<HTMLElement | null>(
|
const [rangeStripRef, rangeStripNode] = useRefAndState<HTMLElement | null>(
|
||||||
null,
|
null,
|
||||||
)
|
)
|
||||||
|
|
||||||
const [contextMenu] = useContextMenu(rangeStripNode, {
|
const [contextMenu] = useContextMenu(rangeStripNode, {
|
||||||
items: () => {
|
items: () => {
|
||||||
|
const sheet = val(layoutP.sheet)
|
||||||
|
|
||||||
const existingRange = existingRangeD.getValue()
|
const existingRange = existingRangeD.getValue()
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
@ -156,7 +175,9 @@ const FocusRangeStrip: React.FC<{
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const scaledSpaceToUnitSpace = val(layoutP.scaledSpace.toUnitSpace)
|
const scaledSpaceToUnitSpace = useVal(layoutP.scaledSpace.toUnitSpace)
|
||||||
|
const [isDraggingRef, isDragging] = useRefAndState(false)
|
||||||
|
const sheet = useVal(layoutP.sheet)
|
||||||
|
|
||||||
const gestureHandlers = useMemo((): Parameters<typeof useDrag>[1] => {
|
const gestureHandlers = useMemo((): Parameters<typeof useDrag>[1] => {
|
||||||
let sequence = sheet.getSequence()
|
let sequence = sheet.getSequence()
|
||||||
|
@ -172,18 +193,20 @@ const FocusRangeStrip: React.FC<{
|
||||||
onDragStart(event) {
|
onDragStart(event) {
|
||||||
existingRange = existingRangeD.getValue()
|
existingRange = existingRangeD.getValue()
|
||||||
|
|
||||||
if (existingRange?.enabled === true) {
|
if (existingRange) {
|
||||||
startPosBeforeDrag = existingRange.range.start
|
startPosBeforeDrag = existingRange.range.start
|
||||||
endPosBeforeDrag = existingRange.range.end
|
endPosBeforeDrag = existingRange.range.end
|
||||||
dragHappened = false
|
dragHappened = false
|
||||||
sequence = val(layoutP.sheet).getSequence()
|
sequence = val(layoutP.sheet).getSequence()
|
||||||
target = event.target as HTMLDivElement
|
target = event.target as HTMLDivElement
|
||||||
target.classList.add('dragging')
|
isDraggingRef.current = true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDrag(dx) {
|
onDrag(dx) {
|
||||||
existingRange = existingRangeD.getValue()
|
existingRange = existingRangeD.getValue()
|
||||||
if (existingRange?.enabled) {
|
if (existingRange) {
|
||||||
dragHappened = true
|
dragHappened = true
|
||||||
const deltaPos = scaledSpaceToUnitSpace(dx)
|
const deltaPos = scaledSpaceToUnitSpace(dx)
|
||||||
|
|
||||||
|
@ -211,14 +234,15 @@ const FocusRangeStrip: React.FC<{
|
||||||
start: newStartPosition,
|
start: newStartPosition,
|
||||||
end: newEndPosition,
|
end: newEndPosition,
|
||||||
},
|
},
|
||||||
enabled: existingRange?.enabled || true,
|
enabled: existingRange?.enabled ?? true,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDragEnd() {
|
onDragEnd() {
|
||||||
if (existingRange?.enabled) {
|
isDraggingRef.current = false
|
||||||
|
if (existingRange) {
|
||||||
if (dragHappened && tempTransaction !== undefined) {
|
if (dragHappened && tempTransaction !== undefined) {
|
||||||
tempTransaction.commit()
|
tempTransaction.commit()
|
||||||
} else if (tempTransaction) {
|
} else if (tempTransaction) {
|
||||||
|
@ -227,7 +251,6 @@ const FocusRangeStrip: React.FC<{
|
||||||
tempTransaction = undefined
|
tempTransaction = undefined
|
||||||
}
|
}
|
||||||
if (target !== undefined) {
|
if (target !== undefined) {
|
||||||
target.classList.remove('dragging')
|
|
||||||
target = undefined
|
target = undefined
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -261,37 +284,25 @@ const FocusRangeStrip: React.FC<{
|
||||||
scaleX = (endX - startX) / stripWidth
|
scaleX = (endX - startX) / stripWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
let conditionalStyleProps: {
|
if (!existingRange) return <></>
|
||||||
background?: string
|
|
||||||
cursor?: string
|
|
||||||
} = {}
|
|
||||||
|
|
||||||
if (existingRange !== undefined) {
|
return (
|
||||||
if (existingRange.enabled === false) {
|
|
||||||
conditionalStyleProps.background =
|
|
||||||
focusRangeStripTheme.disabled.backgroundColor
|
|
||||||
conditionalStyleProps.cursor = 'default'
|
|
||||||
} else {
|
|
||||||
conditionalStyleProps.cursor = 'grab'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return existingRange === undefined ? (
|
|
||||||
<></>
|
|
||||||
) : (
|
|
||||||
<>
|
<>
|
||||||
{contextMenu}
|
{contextMenu}
|
||||||
<RangeStrip
|
<RangeStrip
|
||||||
id="range-strip"
|
id="range-strip"
|
||||||
|
enabled={existingRange.enabled}
|
||||||
|
className={`${isDragging ? 'dragging' : ''} ${
|
||||||
|
existingRange.enabled ? 'enabled' : ''
|
||||||
|
}`}
|
||||||
ref={rangeStripRef as $IntentionalAny}
|
ref={rangeStripRef as $IntentionalAny}
|
||||||
style={{
|
style={{
|
||||||
transform: `translateX(${translateX}px) scale(${scaleX}, 1)`,
|
transform: `translateX(${translateX}px) scale(${scaleX}, 1)`,
|
||||||
...conditionalStyleProps,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}, [layoutP, rangeStripRef, existingRangeD, contextMenu])
|
}, [layoutP, rangeStripRef, existingRangeD, contextMenu, isDragging])
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FocusRangeStrip
|
export default FocusRangeStrip
|
||||||
|
|
|
@ -1,72 +1,161 @@
|
||||||
import type {Pointer} from '@theatre/dataverse'
|
import type {Pointer} from '@theatre/dataverse'
|
||||||
import {prism, val} from '@theatre/dataverse'
|
import {prism, val} from '@theatre/dataverse'
|
||||||
import {usePrism} from '@theatre/react'
|
import {usePrism, useVal} from '@theatre/react'
|
||||||
import type {$IntentionalAny, IRange} from '@theatre/shared/utils/types'
|
import type {$IntentionalAny, IRange} from '@theatre/shared/utils/types'
|
||||||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
|
||||||
import getStudio from '@theatre/studio/getStudio'
|
import getStudio from '@theatre/studio/getStudio'
|
||||||
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
||||||
import {topStripHeight} from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/TopStrip'
|
import {
|
||||||
|
topStripHeight,
|
||||||
|
topStripTheme,
|
||||||
|
} from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/TopStrip'
|
||||||
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
|
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
|
||||||
import {useCssCursorLock} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
import {
|
||||||
|
lockedCursorCssPropName,
|
||||||
|
useCssCursorLock,
|
||||||
|
} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
||||||
import useDrag from '@theatre/studio/uiComponents/useDrag'
|
import useDrag from '@theatre/studio/uiComponents/useDrag'
|
||||||
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
||||||
import React, {useMemo, useRef, useState} from 'react'
|
import React, {useMemo} from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import {focusRangeStripTheme} from './FocusRangeStrip'
|
import {
|
||||||
|
attributeNameThatLocksFramestamp,
|
||||||
|
useLockFrameStampPosition,
|
||||||
|
} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||||
|
import {focusRangeStripTheme, RangeStrip} from './FocusRangeStrip'
|
||||||
|
import type Sheet from '@theatre/core/sheets/Sheet'
|
||||||
|
|
||||||
const Handler = styled.div`
|
const TheDiv = styled.div<{enabled: boolean; type: 'start' | 'end'}>`
|
||||||
content: ' ';
|
|
||||||
width: ${focusRangeStripTheme.thumbWidth};
|
|
||||||
height: ${() => topStripHeight};
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
${pointerEventsAutoInNormalMode};
|
top: 0;
|
||||||
|
// the right handle has to be pulled back by its width since its right side indicates its position, not its left side
|
||||||
|
left: ${(props) =>
|
||||||
|
props.type === 'start' ? 0 : -focusRangeStripTheme.thumbWidth}px;
|
||||||
|
transform-origin: left top;
|
||||||
|
width: ${focusRangeStripTheme.thumbWidth}px;
|
||||||
|
height: ${() => topStripHeight - 1}px;
|
||||||
|
z-index: 3;
|
||||||
|
|
||||||
|
background-color: ${({enabled}) =>
|
||||||
|
enabled
|
||||||
|
? focusRangeStripTheme.enabled.backgroundColor
|
||||||
|
: focusRangeStripTheme.disabled.backgroundColor};
|
||||||
|
|
||||||
stroke: ${focusRangeStripTheme.enabled.stroke};
|
stroke: ${focusRangeStripTheme.enabled.stroke};
|
||||||
user-select: none;
|
user-select: none;
|
||||||
&:hover {
|
|
||||||
background: ${focusRangeStripTheme.highlight.backgroundColor} !important;
|
cursor: ${(props) => (props.type === 'start' ? 'w-resize' : 'e-resize')};
|
||||||
|
|
||||||
|
// no pointer events unless pointer-root is in normal mode _and_ the
|
||||||
|
// focus range is enabled
|
||||||
|
#pointer-root & {
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
`
|
|
||||||
|
|
||||||
const dims = (size: number) => `
|
#pointer-root.normal & {
|
||||||
left: ${-size / 2}px;
|
pointer-events: auto;
|
||||||
width: ${size}px;
|
}
|
||||||
height: ${size}px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const HitZone = styled.div`
|
#pointer-root.draggingPositionInSequenceEditor & {
|
||||||
top: 0;
|
pointer-events: auto;
|
||||||
left: 0;
|
cursor: var(${lockedCursorCssPropName});
|
||||||
transform-origin: left top;
|
}
|
||||||
position: absolute;
|
|
||||||
z-index: 3;
|
|
||||||
${dims(focusRangeStripTheme.hitZoneWidth)}
|
|
||||||
`
|
|
||||||
|
|
||||||
const Tooltip = styled.div`
|
&.dragging {
|
||||||
font-size: 10px;
|
pointer-events: none !important;
|
||||||
white-space: nowrap;
|
}
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 2px;
|
// highlight the handle when it's being dragged or the whole strip is being dragged
|
||||||
${pointerEventsAutoInNormalMode};
|
&.dragging,
|
||||||
background-color: #0000004d;
|
${() => RangeStrip}.dragging ~ & {
|
||||||
display: none;
|
background: ${focusRangeStripTheme.dragging.backgroundColor};
|
||||||
position: absolute;
|
stroke: ${focusRangeStripTheme.dragging.stroke};
|
||||||
top: -${() => topStripHeight + 2};
|
}
|
||||||
transform: translateX(-50%);
|
|
||||||
${HitZone}:hover &, ${Handler}.dragging & {
|
#pointer-root.draggingPositionInSequenceEditor &:hover {
|
||||||
|
background: ${focusRangeStripTheme.dragging.backgroundColor};
|
||||||
|
stroke: #40aaa4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// highlight the handle if it's hovered, or the whole strip is hovverd
|
||||||
|
${() => RangeStrip}:hover ~ &, &:hover {
|
||||||
|
background: ${focusRangeStripTheme.hover.backgroundColor};
|
||||||
|
stroke: ${focusRangeStripTheme.hover.stroke};
|
||||||
|
}
|
||||||
|
|
||||||
|
// a larger hit zone
|
||||||
|
&:before {
|
||||||
display: block;
|
display: block;
|
||||||
color: white;
|
content: ' ';
|
||||||
background-color: '#000000';
|
position: absolute;
|
||||||
|
inset: -8px;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This acts as a bit of a horizontal shadow that covers the frame numbers that show up
|
||||||
|
* right next to the thumb, making the appearance of the focus range more tidy.
|
||||||
|
*/
|
||||||
|
const ColoredMargin = styled.div<{type: 'start' | 'end'; enabled: boolean}>`
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
${() => RangeStrip}.dragging ~ ${TheDiv} > & {
|
||||||
|
--bg: ${focusRangeStripTheme.dragging.backgroundColor};
|
||||||
|
}
|
||||||
|
|
||||||
|
--bg: ${({enabled}) =>
|
||||||
|
enabled
|
||||||
|
? focusRangeStripTheme.enabled.backgroundColor
|
||||||
|
: focusRangeStripTheme.disabled.backgroundColor};
|
||||||
|
|
||||||
|
// highlight the handle if it's hovered, or the whole strip is hovverd
|
||||||
|
${() => RangeStrip}:hover ~ ${TheDiv} > & {
|
||||||
|
--bg: ${focusRangeStripTheme.hover.backgroundColor};
|
||||||
|
}
|
||||||
|
|
||||||
|
background: linear-gradient(
|
||||||
|
${(props) => (props.type === 'start' ? 90 : -90)}deg,
|
||||||
|
var(--bg) 0%,
|
||||||
|
#ffffff00 100%
|
||||||
|
);
|
||||||
|
|
||||||
|
width: 12px;
|
||||||
|
left: ${(props) =>
|
||||||
|
props.type === 'start'
|
||||||
|
? focusRangeStripTheme.thumbWidth
|
||||||
|
: // pushing the right-side thumb's margin 1px to the right to make sure there is no space
|
||||||
|
// between it and the thumb
|
||||||
|
-focusRangeStripTheme.thumbWidth + 1}px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const OuterColoredMargin = styled.div<{
|
||||||
|
type: 'start' | 'end'
|
||||||
|
}>`
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
--bg: ${() => topStripTheme.backgroundColor};
|
||||||
|
|
||||||
|
background: linear-gradient(
|
||||||
|
${(props) => (props.type === 'start' ? -90 : 90)}deg,
|
||||||
|
var(--bg) 0%,
|
||||||
|
#ffffff00 100%
|
||||||
|
);
|
||||||
|
|
||||||
|
width: 12px;
|
||||||
|
left: ${(props) =>
|
||||||
|
props.type === 'start' ? -12 : focusRangeStripTheme.thumbWidth}px;
|
||||||
|
`
|
||||||
|
|
||||||
const FocusRangeThumb: React.FC<{
|
const FocusRangeThumb: React.FC<{
|
||||||
layoutP: Pointer<SequenceEditorPanelLayout>
|
layoutP: Pointer<SequenceEditorPanelLayout>
|
||||||
thumbType: keyof IRange
|
thumbType: keyof IRange
|
||||||
}> = ({layoutP, thumbType}) => {
|
}> = ({layoutP, thumbType}) => {
|
||||||
const [hitZoneRef, hitZoneNode] = useRefAndState<HTMLElement | null>(null)
|
const [hitZoneRef, hitZoneNode] = useRefAndState<HTMLElement | null>(null)
|
||||||
const handlerRef = useRef<HTMLElement | null>(null)
|
|
||||||
const [isDragging, setIsDragging] = useState<boolean>(false)
|
|
||||||
|
|
||||||
const existingRangeD = useMemo(
|
const existingRangeD = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
@ -81,51 +170,34 @@ const FocusRangeThumb: React.FC<{
|
||||||
[layoutP],
|
[layoutP],
|
||||||
)
|
)
|
||||||
|
|
||||||
const sheet = val(layoutP.sheet)
|
|
||||||
const scaledSpaceToUnitSpace = val(layoutP.scaledSpace.toUnitSpace)
|
|
||||||
let sequence = sheet.getSequence()
|
|
||||||
|
|
||||||
const focusRangeEnabled = existingRangeD.getValue()?.enabled || false
|
|
||||||
|
|
||||||
const gestureHandlers = useMemo((): Parameters<typeof useDrag>[1] => {
|
const gestureHandlers = useMemo((): Parameters<typeof useDrag>[1] => {
|
||||||
const defaultRange = {start: 0, end: sequence.length}
|
let defaultRange: IRange
|
||||||
let range = existingRangeD.getValue()?.range || defaultRange
|
let range: IRange
|
||||||
let focusRangeEnabled: boolean
|
let focusRangeEnabled: boolean
|
||||||
let posBeforeDrag = range[thumbType]
|
let posBeforeDrag: number
|
||||||
let tempTransaction: CommitOrDiscard | undefined
|
let tempTransaction: CommitOrDiscard | undefined
|
||||||
let dragHappened = false
|
|
||||||
let originalBackground: string
|
|
||||||
let originalStroke: string
|
|
||||||
let minFocusRangeStripWidth: number
|
let minFocusRangeStripWidth: number
|
||||||
|
let sheet: Sheet
|
||||||
|
let scaledSpaceToUnitSpace: (s: number) => number
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onDragStart() {
|
onDragStart() {
|
||||||
|
sheet = val(layoutP.sheet)
|
||||||
|
const sequence = sheet.getSequence()
|
||||||
|
defaultRange = {start: 0, end: sequence.length}
|
||||||
let existingRange = existingRangeD.getValue() || {
|
let existingRange = existingRangeD.getValue() || {
|
||||||
range: defaultRange,
|
range: defaultRange,
|
||||||
enabled: false,
|
enabled: false,
|
||||||
}
|
}
|
||||||
focusRangeEnabled = existingRange.enabled
|
focusRangeEnabled = existingRange.enabled
|
||||||
dragHappened = false
|
|
||||||
sequence = val(layoutP.sheet).getSequence()
|
|
||||||
posBeforeDrag = existingRange.range[thumbType]
|
posBeforeDrag = existingRange.range[thumbType]
|
||||||
|
scaledSpaceToUnitSpace = val(layoutP.scaledSpace.toUnitSpace)
|
||||||
minFocusRangeStripWidth = scaledSpaceToUnitSpace(
|
minFocusRangeStripWidth = scaledSpaceToUnitSpace(
|
||||||
focusRangeStripTheme.rangeStripMinWidth,
|
focusRangeStripTheme.rangeStripMinWidth,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (handlerRef.current) {
|
|
||||||
originalBackground = handlerRef.current.style.background
|
|
||||||
originalStroke = handlerRef.current.style.stroke
|
|
||||||
handlerRef.current.style.background =
|
|
||||||
focusRangeStripTheme.highlight.backgroundColor
|
|
||||||
handlerRef.current.style.stroke =
|
|
||||||
focusRangeStripTheme.highlight.stroke
|
|
||||||
handlerRef.current.style
|
|
||||||
handlerRef.current.classList.add('dragging')
|
|
||||||
setIsDragging(true)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onDrag(dx, _, event) {
|
onDrag(dx, _, event) {
|
||||||
dragHappened = true
|
|
||||||
range = existingRangeD.getValue()?.range || defaultRange
|
range = existingRangeD.getValue()?.range || defaultRange
|
||||||
|
|
||||||
const deltaPos = scaledSpaceToUnitSpace(dx)
|
const deltaPos = scaledSpaceToUnitSpace(dx)
|
||||||
|
@ -149,7 +221,7 @@ const FocusRangeThumb: React.FC<{
|
||||||
oldPosPlusDeltaPos,
|
oldPosPlusDeltaPos,
|
||||||
range['start'] + minFocusRangeStripWidth,
|
range['start'] + minFocusRangeStripWidth,
|
||||||
),
|
),
|
||||||
sequence.length,
|
sheet.getSequence().length,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,7 +243,9 @@ const FocusRangeThumb: React.FC<{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const newPositionInFrame = sequence.closestGridPosition(newPosition)
|
const newPositionInFrame = sheet
|
||||||
|
.getSequence()
|
||||||
|
.closestGridPosition(newPosition)
|
||||||
|
|
||||||
if (tempTransaction !== undefined) {
|
if (tempTransaction !== undefined) {
|
||||||
tempTransaction.discard()
|
tempTransaction.discard()
|
||||||
|
@ -187,40 +261,34 @@ const FocusRangeThumb: React.FC<{
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onDragEnd() {
|
onDragEnd(dragHappened) {
|
||||||
if (handlerRef.current) {
|
|
||||||
handlerRef.current.classList.remove('dragging')
|
|
||||||
setIsDragging(false)
|
|
||||||
|
|
||||||
if (originalBackground) {
|
|
||||||
handlerRef.current.style.background = originalBackground
|
|
||||||
}
|
|
||||||
if (originalBackground) {
|
|
||||||
handlerRef.current.style.stroke = originalStroke
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (dragHappened && tempTransaction !== undefined) {
|
if (dragHappened && tempTransaction !== undefined) {
|
||||||
tempTransaction.commit()
|
tempTransaction.commit()
|
||||||
} else if (tempTransaction) {
|
} else if (tempTransaction) {
|
||||||
tempTransaction.discard()
|
tempTransaction.discard()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
lockCursorTo: thumbType === 'start' ? 'w-resize' : 'e-resize',
|
|
||||||
}
|
}
|
||||||
}, [sheet, scaledSpaceToUnitSpace])
|
}, [layoutP])
|
||||||
|
|
||||||
useDrag(hitZoneNode, gestureHandlers)
|
const [isDragging] = useDrag(hitZoneNode, gestureHandlers)
|
||||||
|
|
||||||
useCssCursorLock(isDragging, 'draggingPositionInSequenceEditor', 'ew-resize')
|
useCssCursorLock(
|
||||||
|
isDragging,
|
||||||
|
'draggingPositionInSequenceEditor',
|
||||||
|
thumbType === 'start' ? 'w-resize' : 'e-resize',
|
||||||
|
)
|
||||||
|
|
||||||
|
const existingRange = useVal(existingRangeD)
|
||||||
|
|
||||||
|
useLockFrameStampPosition(isDragging, existingRange?.range[thumbType] ?? 0)
|
||||||
|
|
||||||
return usePrism(() => {
|
return usePrism(() => {
|
||||||
const existingRange = existingRangeD.getValue()
|
const existingRange = existingRangeD.getValue()
|
||||||
const defaultRange = {
|
if (!existingRange) return null
|
||||||
range: {start: 0, end: sequence.length},
|
const {enabled} = existingRange
|
||||||
enabled: false,
|
|
||||||
}
|
const position = existingRange.range[thumbType]
|
||||||
const position =
|
|
||||||
existingRange?.range[thumbType] || defaultRange.range[thumbType]
|
|
||||||
|
|
||||||
let posInClippedSpace: number = val(layoutP.clippedSpace.fromUnitSpace)(
|
let posInClippedSpace: number = val(layoutP.clippedSpace.fromUnitSpace)(
|
||||||
position,
|
position,
|
||||||
|
@ -230,54 +298,32 @@ const FocusRangeThumb: React.FC<{
|
||||||
posInClippedSpace < 0 ||
|
posInClippedSpace < 0 ||
|
||||||
val(layoutP.clippedSpace.width) < posInClippedSpace
|
val(layoutP.clippedSpace.width) < posInClippedSpace
|
||||||
) {
|
) {
|
||||||
posInClippedSpace = -1000
|
posInClippedSpace = -10000
|
||||||
}
|
}
|
||||||
|
|
||||||
const pointerEvents = focusRangeEnabled ? 'auto' : 'none'
|
return (
|
||||||
|
<TheDiv
|
||||||
const background = focusRangeEnabled
|
|
||||||
? focusRangeStripTheme.disabled.backgroundColor
|
|
||||||
: focusRangeStripTheme.enabled.backgroundColor
|
|
||||||
|
|
||||||
const startHandlerOffset = focusRangeStripTheme.hitZoneWidth / 2
|
|
||||||
const endHandlerOffset =
|
|
||||||
startHandlerOffset - focusRangeStripTheme.thumbWidth
|
|
||||||
|
|
||||||
return existingRange !== undefined ? (
|
|
||||||
<>
|
|
||||||
<HitZone
|
|
||||||
ref={hitZoneRef as $IntentionalAny}
|
ref={hitZoneRef as $IntentionalAny}
|
||||||
data-pos={position.toFixed(3)}
|
data-pos={position.toFixed(3)}
|
||||||
|
{...{
|
||||||
|
[attributeNameThatLocksFramestamp]: position.toFixed(3),
|
||||||
|
}}
|
||||||
|
className={`${isDragging && 'dragging'} ${enabled && 'enabled'}`}
|
||||||
|
enabled={enabled}
|
||||||
|
type={thumbType}
|
||||||
style={{
|
style={{
|
||||||
transform: `translate3d(${posInClippedSpace}px, 0, 0)`,
|
transform: `translate3d(${posInClippedSpace}px, 0, 0)`,
|
||||||
cursor: thumbType === 'start' ? 'w-resize' : 'e-resize',
|
|
||||||
pointerEvents,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Handler
|
|
||||||
ref={handlerRef as $IntentionalAny}
|
|
||||||
style={{
|
|
||||||
background,
|
|
||||||
left: `${
|
|
||||||
thumbType === 'start' ? startHandlerOffset : endHandlerOffset
|
|
||||||
}px`,
|
|
||||||
pointerEvents,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<ColoredMargin type={thumbType} enabled={enabled} />
|
||||||
|
<OuterColoredMargin type={thumbType} />
|
||||||
<svg viewBox="0 0 9 18" xmlns="http://www.w3.org/2000/svg">
|
<svg viewBox="0 0 9 18" xmlns="http://www.w3.org/2000/svg">
|
||||||
<line x1="4" y1="6" x2="4" y2="12" />
|
<line x1="4" y1="6" x2="4" y2="12" />
|
||||||
<line x1="6" y1="6" x2="6" y2="12" />
|
<line x1="6" y1="6" x2="6" y2="12" />
|
||||||
</svg>
|
</svg>
|
||||||
<Tooltip>
|
</TheDiv>
|
||||||
{sequence.positionFormatter.formatBasic(sequence.length)}
|
|
||||||
</Tooltip>
|
|
||||||
</Handler>
|
|
||||||
</HitZone>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)
|
)
|
||||||
}, [layoutP, hitZoneRef, existingRangeD, focusRangeEnabled])
|
}, [layoutP, hitZoneRef, existingRangeD, isDragging])
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FocusRangeThumb
|
export default FocusRangeThumb
|
||||||
|
|
|
@ -12,20 +12,25 @@ import {
|
||||||
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
||||||
import {topStripHeight} from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/TopStrip'
|
import {topStripHeight} from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/TopStrip'
|
||||||
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
|
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
|
||||||
|
import {useCssCursorLock} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
||||||
import useDrag from '@theatre/studio/uiComponents/useDrag'
|
import useDrag from '@theatre/studio/uiComponents/useDrag'
|
||||||
|
import useHoverWithoutDescendants from '@theatre/studio/uiComponents/useHoverWithoutDescendants'
|
||||||
|
import useKeyDown from '@theatre/studio/uiComponents/useKeyDown'
|
||||||
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
||||||
import {clamp} from 'lodash-es'
|
import {clamp} from 'lodash-es'
|
||||||
import React, {useMemo, useRef} from 'react'
|
import React, {useEffect, useMemo, useRef, useState} from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import FocusRangeStrip, {focusRangeStripTheme} from './FocusRangeStrip'
|
import FocusRangeStrip, {focusRangeStripTheme} from './FocusRangeStrip'
|
||||||
import FocusRangeThumb from './FocusRangeThumb'
|
import FocusRangeThumb from './FocusRangeThumb'
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div<{isShiftDown: boolean}>`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: ${() => topStripHeight}px;
|
height: ${() => topStripHeight}px;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
/* Use the "grab" cursor if the shift key is up, which is the one used on the top strip of the sequence editor */
|
||||||
|
cursor: ${(props) => (props.isShiftDown ? 'ew-resize' : 'move')};
|
||||||
`
|
`
|
||||||
|
|
||||||
const FocusRangeZone: React.FC<{
|
const FocusRangeZone: React.FC<{
|
||||||
|
@ -55,44 +60,28 @@ const FocusRangeZone: React.FC<{
|
||||||
usePanelDragZoneGestureHandlers(layoutP, panelStuffRef),
|
usePanelDragZoneGestureHandlers(layoutP, panelStuffRef),
|
||||||
)
|
)
|
||||||
|
|
||||||
const [onMouseEnter, onMouseLeave] = useMemo(() => {
|
const isShiftDown = useKeyDown('Shift')
|
||||||
let unlock: VoidFn | undefined
|
const isPointerHovering = useHoverWithoutDescendants(containerNode)
|
||||||
return [
|
|
||||||
function onMouseEnter(event: React.MouseEvent) {
|
useEffect(() => {
|
||||||
if (event.shiftKey === false) {
|
if (!isShiftDown && isPointerHovering) {
|
||||||
if (unlock) {
|
const unlock = panelStuffRef.current.addBoundsHighlightLock()
|
||||||
const u = unlock
|
return unlock
|
||||||
unlock = undefined
|
|
||||||
u()
|
|
||||||
}
|
}
|
||||||
unlock = panelStuffRef.current.addBoundsHighlightLock()
|
}, [!isShiftDown && isPointerHovering])
|
||||||
}
|
|
||||||
},
|
|
||||||
function onMouseLeave(event: React.MouseEvent) {
|
|
||||||
if (event.shiftKey === false) {
|
|
||||||
if (unlock) {
|
|
||||||
const u = unlock
|
|
||||||
unlock = undefined
|
|
||||||
u()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return usePrism(() => {
|
return usePrism(() => {
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
ref={containerRef as $IntentionalAny}
|
ref={containerRef as $IntentionalAny}
|
||||||
onMouseEnter={onMouseEnter}
|
isShiftDown={isShiftDown}
|
||||||
onMouseLeave={onMouseLeave}
|
|
||||||
>
|
>
|
||||||
<FocusRangeStrip layoutP={layoutP} />
|
<FocusRangeStrip layoutP={layoutP} />
|
||||||
<FocusRangeThumb thumbType="start" layoutP={layoutP} />
|
<FocusRangeThumb thumbType="start" layoutP={layoutP} />
|
||||||
<FocusRangeThumb thumbType="end" layoutP={layoutP} />
|
<FocusRangeThumb thumbType="end" layoutP={layoutP} />
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}, [layoutP, existingRangeD])
|
}, [layoutP, existingRangeD, isShiftDown])
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FocusRangeZone
|
export default FocusRangeZone
|
||||||
|
@ -101,6 +90,14 @@ function usePanelDragZoneGestureHandlers(
|
||||||
layoutP: Pointer<SequenceEditorPanelLayout>,
|
layoutP: Pointer<SequenceEditorPanelLayout>,
|
||||||
panelStuffRef: React.MutableRefObject<ReturnType<typeof usePanel>>,
|
panelStuffRef: React.MutableRefObject<ReturnType<typeof usePanel>>,
|
||||||
) {
|
) {
|
||||||
|
const [mode, setMode] = useState<'none' | 'creating' | 'moving-panel'>('none')
|
||||||
|
|
||||||
|
useCssCursorLock(
|
||||||
|
mode !== 'none',
|
||||||
|
'dragging',
|
||||||
|
mode === 'creating' ? 'ew-resize' : 'move',
|
||||||
|
)
|
||||||
|
|
||||||
return useMemo((): Parameters<typeof useDrag>[1] => {
|
return useMemo((): Parameters<typeof useDrag>[1] => {
|
||||||
const focusRangeCreationGestureHandlers = (): Parameters<
|
const focusRangeCreationGestureHandlers = (): Parameters<
|
||||||
typeof useDrag
|
typeof useDrag
|
||||||
|
@ -175,7 +172,7 @@ function usePanelDragZoneGestureHandlers(
|
||||||
}
|
}
|
||||||
tempTransaction = undefined
|
tempTransaction = undefined
|
||||||
},
|
},
|
||||||
lockCursorTo: 'grabbing',
|
lockCursorTo: 'ew-resize',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -234,22 +231,21 @@ function usePanelDragZoneGestureHandlers(
|
||||||
return {
|
return {
|
||||||
onDragStart(event) {
|
onDragStart(event) {
|
||||||
if (event.shiftKey) {
|
if (event.shiftKey) {
|
||||||
|
setMode('creating')
|
||||||
currentGestureHandlers = focusRangeCreationGestureHandlers()
|
currentGestureHandlers = focusRangeCreationGestureHandlers()
|
||||||
} else {
|
} else {
|
||||||
|
setMode('moving-panel')
|
||||||
currentGestureHandlers = panelMoveGestureHandlers()
|
currentGestureHandlers = panelMoveGestureHandlers()
|
||||||
}
|
}
|
||||||
currentGestureHandlers.onDragStart!(event)
|
currentGestureHandlers.onDragStart!(event)
|
||||||
},
|
},
|
||||||
onDrag(dx, dy, event) {
|
onDrag(dx, dy, event) {
|
||||||
if (!currentGestureHandlers) {
|
|
||||||
console.error('oh no')
|
|
||||||
}
|
|
||||||
currentGestureHandlers!.onDrag(dx, dy, event)
|
currentGestureHandlers!.onDrag(dx, dy, event)
|
||||||
},
|
},
|
||||||
onDragEnd(dragHappened) {
|
onDragEnd(dragHappened) {
|
||||||
|
setMode('none')
|
||||||
currentGestureHandlers!.onDragEnd!(dragHappened)
|
currentGestureHandlers!.onDragEnd!(dragHappened)
|
||||||
},
|
},
|
||||||
lockCursorTo: 'grabbing',
|
|
||||||
}
|
}
|
||||||
}, [layoutP, panelStuffRef])
|
}, [layoutP, panelStuffRef])
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,10 @@ import usePopover from '@theatre/studio/uiComponents/Popover/usePopover'
|
||||||
import BasicPopover from '@theatre/studio/uiComponents/Popover/BasicPopover'
|
import BasicPopover from '@theatre/studio/uiComponents/Popover/BasicPopover'
|
||||||
import PlayheadPositionPopover from './PlayheadPositionPopover'
|
import PlayheadPositionPopover from './PlayheadPositionPopover'
|
||||||
import {getIsPlayheadAttachedToFocusRange} from '@theatre/studio/UIRoot/useKeyboardShortcuts'
|
import {getIsPlayheadAttachedToFocusRange} from '@theatre/studio/UIRoot/useKeyboardShortcuts'
|
||||||
|
import {
|
||||||
|
lockedCursorCssPropName,
|
||||||
|
useCssCursorLock,
|
||||||
|
} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
||||||
|
|
||||||
const Container = styled.div<{isVisible: boolean}>`
|
const Container = styled.div<{isVisible: boolean}>`
|
||||||
--thumbColor: #00e0ff;
|
--thumbColor: #00e0ff;
|
||||||
|
@ -41,9 +45,11 @@ const Rod = styled.div`
|
||||||
height: calc(100% - 8px);
|
height: calc(100% - 8px);
|
||||||
border-left: 1px solid #27e0fd;
|
border-left: 1px solid #27e0fd;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
#pointer-root.draggingPositionInSequenceEditor &:not(.seeking) {
|
#pointer-root.draggingPositionInSequenceEditor &:not(.seeking) {
|
||||||
pointer-events: auto;
|
/* pointer-events: auto; */
|
||||||
|
/* cursor: var(${lockedCursorCssPropName}); */
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -55,6 +61,7 @@ const Rod = styled.div`
|
||||||
`
|
`
|
||||||
|
|
||||||
const Thumb = styled.div`
|
const Thumb = styled.div`
|
||||||
|
background-color: var(--thumbColor);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 5px;
|
width: 5px;
|
||||||
height: 13px;
|
height: 13px;
|
||||||
|
@ -62,16 +69,104 @@ const Thumb = styled.div`
|
||||||
left: -2px;
|
left: -2px;
|
||||||
z-index: 11;
|
z-index: 11;
|
||||||
cursor: ew-resize;
|
cursor: ew-resize;
|
||||||
|
--sunblock-color: #1f2b2b;
|
||||||
|
|
||||||
${pointerEventsAutoInNormalMode};
|
${pointerEventsAutoInNormalMode};
|
||||||
|
|
||||||
|
&.seeking {
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
#pointer-root.draggingPositionInSequenceEditor &:not(.seeking) {
|
#pointer-root.draggingPositionInSequenceEditor &:not(.seeking) {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
|
cursor: var(${lockedCursorCssPropName});
|
||||||
|
}
|
||||||
|
|
||||||
|
${Container}.playheadattachedtofocusrange > & {
|
||||||
|
top: -8px;
|
||||||
|
--sunblock-color: #005662;
|
||||||
|
&:before,
|
||||||
|
&:after {
|
||||||
|
border-bottom-width: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
content: ' ';
|
||||||
|
left: -2px;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-bottom: 4px solid var(--sunblock-color);
|
||||||
|
border-left: 2px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
content: ' ';
|
||||||
|
right: -2px;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-bottom: 4px solid var(--sunblock-color);
|
||||||
|
border-right: 2px solid transparent;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const Squinch = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
left: 1px;
|
||||||
|
right: 1px;
|
||||||
|
top: 13px;
|
||||||
|
border-top: 3px solid var(--thumbColor);
|
||||||
|
border-right: 1px solid transparent;
|
||||||
|
border-left: 1px solid transparent;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
/* ${Container}.playheadattachedtofocusrange & {
|
||||||
|
top: 10px;
|
||||||
|
&:before,
|
||||||
|
&:after {
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
} */
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
content: ' ';
|
||||||
|
top: -4px;
|
||||||
|
left: -2px;
|
||||||
|
height: 8px;
|
||||||
|
width: 2px;
|
||||||
|
background: none;
|
||||||
|
border-radius: 0 100% 0 0;
|
||||||
|
border-top: 1px solid var(--thumbColor);
|
||||||
|
border-right: 1px solid var(--thumbColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
content: ' ';
|
||||||
|
top: -4px;
|
||||||
|
right: -2px;
|
||||||
|
height: 8px;
|
||||||
|
width: 2px;
|
||||||
|
background: none;
|
||||||
|
border-radius: 100% 0 0 0;
|
||||||
|
border-top: 1px solid var(--thumbColor);
|
||||||
|
border-left: 1px solid var(--thumbColor);
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const Tooltip = styled.div`
|
const Tooltip = styled.div`
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
top: -20px;
|
||||||
|
left: 4px;
|
||||||
|
padding: 0 2px;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
background: #1a1a1a;
|
background: #1a1a1a;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
@ -84,34 +179,6 @@ const Tooltip = styled.div`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const RegularThumbSvg: React.FC = () => (
|
|
||||||
<svg
|
|
||||||
width="7"
|
|
||||||
height="26"
|
|
||||||
viewBox="0 0 7 26"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
style={{fill: '#00e0ff', marginLeft: '-1px'}}
|
|
||||||
>
|
|
||||||
<path d="M 0,0 L 7,0 L 7,13 C 4,15 4,26 4,26 L 3,26 C 3,26 3,15 0,13 L 0,0 Z" />
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
|
|
||||||
const LargeThumbSvg: React.FC = () => (
|
|
||||||
<svg
|
|
||||||
width="9"
|
|
||||||
height="37"
|
|
||||||
viewBox="0 0 9 37"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
style={{
|
|
||||||
fill: '#00e0ff',
|
|
||||||
marginLeft: '-2px',
|
|
||||||
marginTop: '-4px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<path d="M 0,0 L 9,0 L 9,18 C 5,20 5,37 5,37 L 4,37 C 4,37 4,20 0,18 L 0,0 Z" />
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
|
|
||||||
const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
|
const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
|
||||||
layoutP,
|
layoutP,
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -131,20 +198,18 @@ const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const scaledSpaceToUnitSpace = val(layoutP.scaledSpace.toUnitSpace)
|
|
||||||
|
|
||||||
// This may not currently snap correctly like it does when grabbing the "Rod".
|
|
||||||
// See https://www.notion.so/theatrejs/dragging-from-playhead-does-not-snap-dadac4fa755149cebbcb70a655c3a0d5
|
|
||||||
const gestureHandlers = useMemo((): Parameters<typeof useDrag>[1] => {
|
const gestureHandlers = useMemo((): Parameters<typeof useDrag>[1] => {
|
||||||
const setIsSeeking = val(layoutP.seeker.setIsSeeking)
|
const setIsSeeking = val(layoutP.seeker.setIsSeeking)
|
||||||
|
|
||||||
let posBeforeSeek = 0
|
let posBeforeSeek = 0
|
||||||
let sequence: Sequence
|
let sequence: Sequence
|
||||||
|
let scaledSpaceToUnitSpace: typeof layoutP.scaledSpace.toUnitSpace.$$__pointer_type
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onDragStart() {
|
onDragStart() {
|
||||||
sequence = val(layoutP.sheet).getSequence()
|
sequence = val(layoutP.sheet).getSequence()
|
||||||
posBeforeSeek = sequence.position
|
posBeforeSeek = sequence.position
|
||||||
|
scaledSpaceToUnitSpace = val(layoutP.scaledSpace.toUnitSpace)
|
||||||
setIsSeeking(true)
|
setIsSeeking(true)
|
||||||
},
|
},
|
||||||
onDrag(dx, _, event) {
|
onDrag(dx, _, event) {
|
||||||
|
@ -174,20 +239,25 @@ const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
|
||||||
onDragEnd() {
|
onDragEnd() {
|
||||||
setIsSeeking(false)
|
setIsSeeking(false)
|
||||||
},
|
},
|
||||||
lockCursorTo: 'ew-resize',
|
|
||||||
}
|
}
|
||||||
}, [scaledSpaceToUnitSpace])
|
}, [])
|
||||||
|
|
||||||
useDrag(thumbNode, gestureHandlers)
|
const [isDragging] = useDrag(thumbNode, gestureHandlers)
|
||||||
|
|
||||||
|
useCssCursorLock(isDragging, 'draggingPositionInSequenceEditor', 'ew-resize')
|
||||||
|
|
||||||
// hide the frame stamp when seeking
|
// hide the frame stamp when seeking
|
||||||
useLockFrameStampPosition(useVal(layoutP.seeker.isSeeking), -1)
|
useLockFrameStampPosition(useVal(layoutP.seeker.isSeeking) || isDragging, -1)
|
||||||
|
|
||||||
return usePrism(() => {
|
return usePrism(() => {
|
||||||
const isSeeking = val(layoutP.seeker.isSeeking)
|
const isSeeking = val(layoutP.seeker.isSeeking)
|
||||||
|
|
||||||
const sequence = val(layoutP.sheet).getSequence()
|
const sequence = val(layoutP.sheet).getSequence()
|
||||||
|
|
||||||
|
const isPlayheadAttachedToFocusRange = val(
|
||||||
|
getIsPlayheadAttachedToFocusRange(sequence),
|
||||||
|
)
|
||||||
|
|
||||||
const posInUnitSpace = sequence.positionDerivation.getValue()
|
const posInUnitSpace = sequence.positionDerivation.getValue()
|
||||||
|
|
||||||
const posInClippedSpace = val(layoutP.clippedSpace.fromUnitSpace)(
|
const posInClippedSpace = val(layoutP.clippedSpace.fromUnitSpace)(
|
||||||
|
@ -197,32 +267,27 @@ const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
|
||||||
posInClippedSpace >= 0 &&
|
posInClippedSpace >= 0 &&
|
||||||
posInClippedSpace <= val(layoutP.clippedSpace.width)
|
posInClippedSpace <= val(layoutP.clippedSpace.width)
|
||||||
|
|
||||||
const isPlayheadAttachedToFocusRange = val(
|
|
||||||
getIsPlayheadAttachedToFocusRange(sequence),
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{popoverNode}
|
{popoverNode}
|
||||||
<Container
|
<Container
|
||||||
isVisible={isVisible}
|
isVisible={isVisible}
|
||||||
style={{transform: `translate3d(${posInClippedSpace}px, 0, 0)`}}
|
style={{transform: `translate3d(${posInClippedSpace}px, 0, 0)`}}
|
||||||
className={isSeeking ? 'seeking' : ''}
|
className={`${isSeeking && 'seeking'} ${
|
||||||
|
isPlayheadAttachedToFocusRange && 'playheadattachedtofocusrange'
|
||||||
|
}`}
|
||||||
{...{[attributeNameThatLocksFramestamp]: 'hide'}}
|
{...{[attributeNameThatLocksFramestamp]: 'hide'}}
|
||||||
>
|
>
|
||||||
<Thumb
|
<Thumb
|
||||||
ref={thumbRef as $IntentionalAny}
|
ref={thumbRef as $IntentionalAny}
|
||||||
data-pos={posInUnitSpace.toFixed(3)}
|
data-pos={posInUnitSpace.toFixed(3)}
|
||||||
|
onClick={(e) => {
|
||||||
|
openPopover(e, thumbNode!)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<RoomToClick room={8} />
|
<RoomToClick room={8} />
|
||||||
{isPlayheadAttachedToFocusRange ? (
|
<Squinch />
|
||||||
<LargeThumbSvg />
|
<Tooltip>
|
||||||
) : (
|
|
||||||
<RegularThumbSvg />
|
|
||||||
)}
|
|
||||||
<Tooltip
|
|
||||||
style={{top: isPlayheadAttachedToFocusRange ? '-23px' : '-18px'}}
|
|
||||||
>
|
|
||||||
{sequence.positionFormatter.formatForPlayhead(
|
{sequence.positionFormatter.formatForPlayhead(
|
||||||
sequence.closestGridPosition(posInUnitSpace),
|
sequence.closestGridPosition(posInUnitSpace),
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -74,9 +74,9 @@ const PlayheadPositionPopover: React.FC<{
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Label>Playhead position</Label>
|
<Label>Sequence position</Label>
|
||||||
<BasicNumberInput
|
<BasicNumberInput
|
||||||
value={sequence.position}
|
value={Number(sequence.position.toFixed(3))}
|
||||||
{...fns}
|
{...fns}
|
||||||
isValid={greaterThanZero}
|
isValid={greaterThanZero}
|
||||||
inputRef={inputRef}
|
inputRef={inputRef}
|
||||||
|
|
|
@ -11,6 +11,14 @@ import styled from 'styled-components'
|
||||||
// using an ID to make CSS selectors faster
|
// using an ID to make CSS selectors faster
|
||||||
const elementId = 'pointer-root'
|
const elementId = 'pointer-root'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the cursor is locked, this css prop is added to #pointer-root
|
||||||
|
* whose value will be the locked cursor (e.g. ew-resize).
|
||||||
|
*
|
||||||
|
* Look up references of this constant for examples of how it is used.
|
||||||
|
*/
|
||||||
|
export const lockedCursorCssPropName = '--lockedCursor'
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
&.normal {
|
&.normal {
|
||||||
|
@ -54,13 +62,20 @@ const PointerEventsHandler: React.FC<{
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const lockedCursor = locks[0]?.cursor ?? ''
|
||||||
return (
|
return (
|
||||||
<context.Provider value={contextValue}>
|
<context.Provider value={contextValue}>
|
||||||
<Container
|
<Container
|
||||||
id={elementId}
|
id={elementId}
|
||||||
className={(locks[0]?.className ?? 'normal') + ' ' + props.className}
|
className={(locks[0]?.className ?? 'normal') + ' ' + props.className}
|
||||||
>
|
>
|
||||||
<CursorOverride style={{cursor: locks[0]?.cursor ?? ''}}>
|
<CursorOverride
|
||||||
|
style={{
|
||||||
|
cursor: lockedCursor,
|
||||||
|
// @ts-ignore
|
||||||
|
[lockedCursorCssPropName]: lockedCursor,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</CursorOverride>
|
</CursorOverride>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
import {useEffect, useState} from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A react hook that returns true if the pointer is hovering over the target element and not its descendants
|
||||||
|
*/
|
||||||
|
export default function useHoverWithoutDescendants(
|
||||||
|
target: HTMLElement | null | undefined,
|
||||||
|
): boolean {
|
||||||
|
const [isHovered, setIsHovered] = useState<boolean>(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsHovered(false)
|
||||||
|
if (!target) return
|
||||||
|
|
||||||
|
const onMouseEnterOrMove = (e: MouseEvent) => {
|
||||||
|
if (e.target === target) {
|
||||||
|
setIsHovered(true)
|
||||||
|
} else {
|
||||||
|
setIsHovered(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const onMouseLeave = () => {
|
||||||
|
setIsHovered(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
target.addEventListener('mouseenter', onMouseEnterOrMove)
|
||||||
|
target.addEventListener('mousemove', onMouseEnterOrMove)
|
||||||
|
target.addEventListener('mouseleave', onMouseLeave)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setIsHovered(false)
|
||||||
|
target.removeEventListener('mouseenter', onMouseEnterOrMove)
|
||||||
|
target.removeEventListener('mousemove', onMouseEnterOrMove)
|
||||||
|
target.removeEventListener('mouseleave', onMouseLeave)
|
||||||
|
}
|
||||||
|
}, [target])
|
||||||
|
|
||||||
|
return isHovered
|
||||||
|
}
|
Loading…
Reference in a new issue