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`
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
${pointerEventsAutoInNormalMode};
|
||||
&:after {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
right: -2px;
|
||||
bottom: -2px;
|
||||
left: -2px;
|
||||
inset: -5px;
|
||||
display: block;
|
||||
content: ' ';
|
||||
}
|
||||
|
@ -40,55 +36,70 @@ const Base = styled.div`
|
|||
}
|
||||
`
|
||||
|
||||
const Horizontal = styled(Base)`
|
||||
left: 0;
|
||||
right: 0;
|
||||
const Side = styled(Base)`
|
||||
/**
|
||||
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;
|
||||
`
|
||||
|
||||
const Top = styled(Horizontal)`
|
||||
top: 0;
|
||||
top: -1px;
|
||||
`
|
||||
|
||||
const Bottom = styled(Horizontal)`
|
||||
bottom: 0;
|
||||
bottom: -1px;
|
||||
`
|
||||
|
||||
const Vertical = styled(Base)`
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
const Vertical = styled(Side)`
|
||||
z-index: -1;
|
||||
top: -1px;
|
||||
bottom: -1px;
|
||||
width: 1px;
|
||||
`
|
||||
|
||||
const Left = styled(Vertical)`
|
||||
left: 0;
|
||||
left: -1px;
|
||||
`
|
||||
|
||||
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;
|
||||
height: 8px;
|
||||
`
|
||||
|
||||
const TopLeft = styled(Square)`
|
||||
const TopLeft = styled(Angle)`
|
||||
top: 0;
|
||||
left: 0;
|
||||
`
|
||||
|
||||
const TopRight = styled(Square)`
|
||||
const TopRight = styled(Angle)`
|
||||
top: 0;
|
||||
right: 0;
|
||||
`
|
||||
|
||||
const BottomLeft = styled(Square)`
|
||||
const BottomLeft = styled(Angle)`
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
`
|
||||
|
||||
const BottomRight = styled(Square)`
|
||||
const BottomRight = styled(Angle)`
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
`
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import PanelResizeHandle from './PanelResizeHandle'
|
||||
|
||||
const Container = styled.div``
|
||||
|
||||
const PanelResizers: React.FC<{}> = (props) => {
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -14,7 +14,10 @@ import styled from 'styled-components'
|
|||
import type KeyframeEditor from './KeyframeEditor'
|
||||
import {useLockFrameStampPosition} 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 selectedKeyframeIdsIfInSingleTrack from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/selectedKeyframeIdsIfInSingleTrack'
|
||||
|
||||
|
@ -57,6 +60,8 @@ const HitZone = styled.div`
|
|||
|
||||
#pointer-root.draggingPositionInSequenceEditor & {
|
||||
pointer-events: auto;
|
||||
cursor: var(${lockedCursorCssPropName});
|
||||
|
||||
&:hover:after {
|
||||
position: absolute;
|
||||
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 HorizontallyScrollableArea from './HorizontallyScrollableArea'
|
||||
import SheetRow from './SheetRow'
|
||||
import FocusRangeArea from './FocusRangeArea'
|
||||
import FocusRangeCurtains from './FocusRangeCurtains'
|
||||
|
||||
export const contentWidth = 1000000
|
||||
|
||||
|
@ -27,7 +27,7 @@ const Background = styled.div<{width: number}>`
|
|||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: ${(props) => props.width};
|
||||
width: ${(props) => props.width}px;
|
||||
bottom: 0;
|
||||
z-index: ${() => zIndexes.rightBackground};
|
||||
overflow: hidden;
|
||||
|
@ -49,7 +49,7 @@ const Right: React.FC<{
|
|||
return (
|
||||
<>
|
||||
<HorizontallyScrollableArea layoutP={layoutP} height={height}>
|
||||
<FocusRangeArea layoutP={layoutP} />
|
||||
<FocusRangeCurtains layoutP={layoutP} />
|
||||
<DopeSheetSelectionView layoutP={layoutP}>
|
||||
<ListContainer style={{top: tree.top + 'px'}}>
|
||||
<SheetRow leaf={tree} layoutP={layoutP} />
|
||||
|
|
|
@ -179,7 +179,11 @@ const pointerPositionInUnitSpace = (
|
|||
|
||||
if (
|
||||
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 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 {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||
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
|
||||
|
||||
|
@ -42,6 +45,7 @@ const HitZone = styled.circle`
|
|||
|
||||
#pointer-root.draggingPositionInSequenceEditor & {
|
||||
pointer-events: auto;
|
||||
cursor: var(${lockedCursorCssPropName});
|
||||
}
|
||||
|
||||
&.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 {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||
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
|
||||
|
||||
|
@ -42,6 +45,7 @@ const HitZone = styled.circle`
|
|||
|
||||
#pointer-root.draggingPositionInSequenceEditor & {
|
||||
pointer-events: auto;
|
||||
cursor: var(${lockedCursorCssPropName});
|
||||
}
|
||||
|
||||
&.beingDragged {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type {Pointer} 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 {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||
import getStudio from '@theatre/studio/getStudio'
|
||||
|
@ -19,17 +19,16 @@ export const focusRangeStripTheme = {
|
|||
stroke: '#646568',
|
||||
},
|
||||
disabled: {
|
||||
backgroundColor: '#282A2C',
|
||||
backgroundColor: '#282a2cc5',
|
||||
stroke: '#595a5d',
|
||||
},
|
||||
playing: {
|
||||
backgroundColor: 'red',
|
||||
},
|
||||
highlight: {
|
||||
hover: {
|
||||
backgroundColor: '#34373D',
|
||||
stroke: '#C8CAC0',
|
||||
},
|
||||
dragging: {
|
||||
backgroundColor: '#3F444A',
|
||||
stroke: '#C8CAC0',
|
||||
},
|
||||
thumbWidth: 9,
|
||||
hitZoneWidth: 26,
|
||||
|
@ -38,22 +37,42 @@ export const focusRangeStripTheme = {
|
|||
|
||||
const stripWidth = 1000
|
||||
|
||||
const RangeStrip = styled.div`
|
||||
export const RangeStrip = styled.div<{enabled: boolean}>`
|
||||
position: absolute;
|
||||
height: ${() => topStripHeight};
|
||||
background-color: ${focusRangeStripTheme.enabled.backgroundColor};
|
||||
height: ${() => topStripHeight - 1}px;
|
||||
background-color: ${(props) =>
|
||||
props.enabled
|
||||
? focusRangeStripTheme.enabled.backgroundColor
|
||||
: focusRangeStripTheme.disabled.backgroundColor};
|
||||
cursor: grab;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: ${stripWidth}px;
|
||||
transform-origin: left top;
|
||||
&:hover {
|
||||
background-color: ${focusRangeStripTheme.highlight.backgroundColor};
|
||||
background-color: ${focusRangeStripTheme.hover.backgroundColor};
|
||||
}
|
||||
&.dragging {
|
||||
background-color: ${focusRangeStripTheme.dragging.backgroundColor};
|
||||
cursor: grabbing !important;
|
||||
}
|
||||
${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],
|
||||
)
|
||||
|
||||
const sheet = val(layoutP.sheet)
|
||||
|
||||
const [rangeStripRef, rangeStripNode] = useRefAndState<HTMLElement | null>(
|
||||
null,
|
||||
)
|
||||
|
||||
const [contextMenu] = useContextMenu(rangeStripNode, {
|
||||
items: () => {
|
||||
const sheet = val(layoutP.sheet)
|
||||
|
||||
const existingRange = existingRangeD.getValue()
|
||||
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] => {
|
||||
let sequence = sheet.getSequence()
|
||||
|
@ -172,18 +193,20 @@ const FocusRangeStrip: React.FC<{
|
|||
onDragStart(event) {
|
||||
existingRange = existingRangeD.getValue()
|
||||
|
||||
if (existingRange?.enabled === true) {
|
||||
if (existingRange) {
|
||||
startPosBeforeDrag = existingRange.range.start
|
||||
endPosBeforeDrag = existingRange.range.end
|
||||
dragHappened = false
|
||||
sequence = val(layoutP.sheet).getSequence()
|
||||
target = event.target as HTMLDivElement
|
||||
target.classList.add('dragging')
|
||||
isDraggingRef.current = true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
},
|
||||
onDrag(dx) {
|
||||
existingRange = existingRangeD.getValue()
|
||||
if (existingRange?.enabled) {
|
||||
if (existingRange) {
|
||||
dragHappened = true
|
||||
const deltaPos = scaledSpaceToUnitSpace(dx)
|
||||
|
||||
|
@ -211,14 +234,15 @@ const FocusRangeStrip: React.FC<{
|
|||
start: newStartPosition,
|
||||
end: newEndPosition,
|
||||
},
|
||||
enabled: existingRange?.enabled || true,
|
||||
enabled: existingRange?.enabled ?? true,
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
},
|
||||
onDragEnd() {
|
||||
if (existingRange?.enabled) {
|
||||
isDraggingRef.current = false
|
||||
if (existingRange) {
|
||||
if (dragHappened && tempTransaction !== undefined) {
|
||||
tempTransaction.commit()
|
||||
} else if (tempTransaction) {
|
||||
|
@ -227,7 +251,6 @@ const FocusRangeStrip: React.FC<{
|
|||
tempTransaction = undefined
|
||||
}
|
||||
if (target !== undefined) {
|
||||
target.classList.remove('dragging')
|
||||
target = undefined
|
||||
}
|
||||
},
|
||||
|
@ -261,37 +284,25 @@ const FocusRangeStrip: React.FC<{
|
|||
scaleX = (endX - startX) / stripWidth
|
||||
}
|
||||
|
||||
let conditionalStyleProps: {
|
||||
background?: string
|
||||
cursor?: string
|
||||
} = {}
|
||||
if (!existingRange) return <></>
|
||||
|
||||
if (existingRange !== undefined) {
|
||||
if (existingRange.enabled === false) {
|
||||
conditionalStyleProps.background =
|
||||
focusRangeStripTheme.disabled.backgroundColor
|
||||
conditionalStyleProps.cursor = 'default'
|
||||
} else {
|
||||
conditionalStyleProps.cursor = 'grab'
|
||||
}
|
||||
}
|
||||
|
||||
return existingRange === undefined ? (
|
||||
<></>
|
||||
) : (
|
||||
return (
|
||||
<>
|
||||
{contextMenu}
|
||||
<RangeStrip
|
||||
id="range-strip"
|
||||
enabled={existingRange.enabled}
|
||||
className={`${isDragging ? 'dragging' : ''} ${
|
||||
existingRange.enabled ? 'enabled' : ''
|
||||
}`}
|
||||
ref={rangeStripRef as $IntentionalAny}
|
||||
style={{
|
||||
transform: `translateX(${translateX}px) scale(${scaleX}, 1)`,
|
||||
...conditionalStyleProps,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}, [layoutP, rangeStripRef, existingRangeD, contextMenu])
|
||||
}, [layoutP, rangeStripRef, existingRangeD, contextMenu, isDragging])
|
||||
}
|
||||
|
||||
export default FocusRangeStrip
|
||||
|
|
|
@ -1,72 +1,161 @@
|
|||
import type {Pointer} 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 {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||
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 {
|
||||
topStripHeight,
|
||||
topStripTheme,
|
||||
} from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/TopStrip'
|
||||
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 useRefAndState from '@theatre/studio/utils/useRefAndState'
|
||||
import React, {useMemo, useRef, useState} from 'react'
|
||||
import React, {useMemo} from 'react'
|
||||
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`
|
||||
content: ' ';
|
||||
width: ${focusRangeStripTheme.thumbWidth};
|
||||
height: ${() => topStripHeight};
|
||||
const TheDiv = styled.div<{enabled: boolean; type: 'start' | 'end'}>`
|
||||
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};
|
||||
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) => `
|
||||
left: ${-size / 2}px;
|
||||
width: ${size}px;
|
||||
height: ${size}px;
|
||||
`
|
||||
#pointer-root.normal & {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
const HitZone = styled.div`
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform-origin: left top;
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
${dims(focusRangeStripTheme.hitZoneWidth)}
|
||||
`
|
||||
#pointer-root.draggingPositionInSequenceEditor & {
|
||||
pointer-events: auto;
|
||||
cursor: var(${lockedCursorCssPropName});
|
||||
}
|
||||
|
||||
const Tooltip = styled.div`
|
||||
font-size: 10px;
|
||||
white-space: nowrap;
|
||||
padding: 2px 8px;
|
||||
border-radius: 2px;
|
||||
${pointerEventsAutoInNormalMode};
|
||||
background-color: #0000004d;
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: -${() => topStripHeight + 2};
|
||||
transform: translateX(-50%);
|
||||
${HitZone}:hover &, ${Handler}.dragging & {
|
||||
&.dragging {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
// highlight the handle when it's being dragged or the whole strip is being dragged
|
||||
&.dragging,
|
||||
${() => RangeStrip}.dragging ~ & {
|
||||
background: ${focusRangeStripTheme.dragging.backgroundColor};
|
||||
stroke: ${focusRangeStripTheme.dragging.stroke};
|
||||
}
|
||||
|
||||
#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;
|
||||
color: white;
|
||||
background-color: '#000000';
|
||||
content: ' ';
|
||||
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<{
|
||||
layoutP: Pointer<SequenceEditorPanelLayout>
|
||||
thumbType: keyof IRange
|
||||
}> = ({layoutP, thumbType}) => {
|
||||
const [hitZoneRef, hitZoneNode] = useRefAndState<HTMLElement | null>(null)
|
||||
const handlerRef = useRef<HTMLElement | null>(null)
|
||||
const [isDragging, setIsDragging] = useState<boolean>(false)
|
||||
|
||||
const existingRangeD = useMemo(
|
||||
() =>
|
||||
|
@ -81,51 +170,34 @@ const FocusRangeThumb: React.FC<{
|
|||
[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 defaultRange = {start: 0, end: sequence.length}
|
||||
let range = existingRangeD.getValue()?.range || defaultRange
|
||||
let defaultRange: IRange
|
||||
let range: IRange
|
||||
let focusRangeEnabled: boolean
|
||||
let posBeforeDrag = range[thumbType]
|
||||
let posBeforeDrag: number
|
||||
let tempTransaction: CommitOrDiscard | undefined
|
||||
let dragHappened = false
|
||||
let originalBackground: string
|
||||
let originalStroke: string
|
||||
let minFocusRangeStripWidth: number
|
||||
let sheet: Sheet
|
||||
let scaledSpaceToUnitSpace: (s: number) => number
|
||||
|
||||
return {
|
||||
onDragStart() {
|
||||
sheet = val(layoutP.sheet)
|
||||
const sequence = sheet.getSequence()
|
||||
defaultRange = {start: 0, end: sequence.length}
|
||||
let existingRange = existingRangeD.getValue() || {
|
||||
range: defaultRange,
|
||||
enabled: false,
|
||||
}
|
||||
focusRangeEnabled = existingRange.enabled
|
||||
dragHappened = false
|
||||
sequence = val(layoutP.sheet).getSequence()
|
||||
|
||||
posBeforeDrag = existingRange.range[thumbType]
|
||||
scaledSpaceToUnitSpace = val(layoutP.scaledSpace.toUnitSpace)
|
||||
minFocusRangeStripWidth = scaledSpaceToUnitSpace(
|
||||
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) {
|
||||
dragHappened = true
|
||||
range = existingRangeD.getValue()?.range || defaultRange
|
||||
|
||||
const deltaPos = scaledSpaceToUnitSpace(dx)
|
||||
|
@ -149,7 +221,7 @@ const FocusRangeThumb: React.FC<{
|
|||
oldPosPlusDeltaPos,
|
||||
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) {
|
||||
tempTransaction.discard()
|
||||
|
@ -187,40 +261,34 @@ const FocusRangeThumb: React.FC<{
|
|||
)
|
||||
})
|
||||
},
|
||||
onDragEnd() {
|
||||
if (handlerRef.current) {
|
||||
handlerRef.current.classList.remove('dragging')
|
||||
setIsDragging(false)
|
||||
|
||||
if (originalBackground) {
|
||||
handlerRef.current.style.background = originalBackground
|
||||
}
|
||||
if (originalBackground) {
|
||||
handlerRef.current.style.stroke = originalStroke
|
||||
}
|
||||
}
|
||||
onDragEnd(dragHappened) {
|
||||
if (dragHappened && tempTransaction !== undefined) {
|
||||
tempTransaction.commit()
|
||||
} else if (tempTransaction) {
|
||||
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(() => {
|
||||
const existingRange = existingRangeD.getValue()
|
||||
const defaultRange = {
|
||||
range: {start: 0, end: sequence.length},
|
||||
enabled: false,
|
||||
}
|
||||
const position =
|
||||
existingRange?.range[thumbType] || defaultRange.range[thumbType]
|
||||
if (!existingRange) return null
|
||||
const {enabled} = existingRange
|
||||
|
||||
const position = existingRange.range[thumbType]
|
||||
|
||||
let posInClippedSpace: number = val(layoutP.clippedSpace.fromUnitSpace)(
|
||||
position,
|
||||
|
@ -230,54 +298,32 @@ const FocusRangeThumb: React.FC<{
|
|||
posInClippedSpace < 0 ||
|
||||
val(layoutP.clippedSpace.width) < posInClippedSpace
|
||||
) {
|
||||
posInClippedSpace = -1000
|
||||
posInClippedSpace = -10000
|
||||
}
|
||||
|
||||
const pointerEvents = focusRangeEnabled ? 'auto' : 'none'
|
||||
|
||||
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}
|
||||
data-pos={position.toFixed(3)}
|
||||
style={{
|
||||
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,
|
||||
}}
|
||||
>
|
||||
<svg viewBox="0 0 9 18" xmlns="http://www.w3.org/2000/svg">
|
||||
<line x1="4" y1="6" x2="4" y2="12" />
|
||||
<line x1="6" y1="6" x2="6" y2="12" />
|
||||
</svg>
|
||||
<Tooltip>
|
||||
{sequence.positionFormatter.formatBasic(sequence.length)}
|
||||
</Tooltip>
|
||||
</Handler>
|
||||
</HitZone>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
return (
|
||||
<TheDiv
|
||||
ref={hitZoneRef as $IntentionalAny}
|
||||
data-pos={position.toFixed(3)}
|
||||
{...{
|
||||
[attributeNameThatLocksFramestamp]: position.toFixed(3),
|
||||
}}
|
||||
className={`${isDragging && 'dragging'} ${enabled && 'enabled'}`}
|
||||
enabled={enabled}
|
||||
type={thumbType}
|
||||
style={{
|
||||
transform: `translate3d(${posInClippedSpace}px, 0, 0)`,
|
||||
}}
|
||||
>
|
||||
<ColoredMargin type={thumbType} enabled={enabled} />
|
||||
<OuterColoredMargin type={thumbType} />
|
||||
<svg viewBox="0 0 9 18" xmlns="http://www.w3.org/2000/svg">
|
||||
<line x1="4" y1="6" x2="4" y2="12" />
|
||||
<line x1="6" y1="6" x2="6" y2="12" />
|
||||
</svg>
|
||||
</TheDiv>
|
||||
)
|
||||
}, [layoutP, hitZoneRef, existingRangeD, focusRangeEnabled])
|
||||
}, [layoutP, hitZoneRef, existingRangeD, isDragging])
|
||||
}
|
||||
|
||||
export default FocusRangeThumb
|
||||
|
|
|
@ -12,20 +12,25 @@ import {
|
|||
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
||||
import {topStripHeight} from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/TopStrip'
|
||||
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
|
||||
import {useCssCursorLock} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
||||
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 {clamp} from 'lodash-es'
|
||||
import React, {useMemo, useRef} from 'react'
|
||||
import React, {useEffect, useMemo, useRef, useState} from 'react'
|
||||
import styled from 'styled-components'
|
||||
import FocusRangeStrip, {focusRangeStripTheme} from './FocusRangeStrip'
|
||||
import FocusRangeThumb from './FocusRangeThumb'
|
||||
|
||||
const Container = styled.div`
|
||||
const Container = styled.div<{isShiftDown: boolean}>`
|
||||
position: absolute;
|
||||
height: ${() => topStripHeight}px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
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<{
|
||||
|
@ -55,44 +60,28 @@ const FocusRangeZone: React.FC<{
|
|||
usePanelDragZoneGestureHandlers(layoutP, panelStuffRef),
|
||||
)
|
||||
|
||||
const [onMouseEnter, onMouseLeave] = useMemo(() => {
|
||||
let unlock: VoidFn | undefined
|
||||
return [
|
||||
function onMouseEnter(event: React.MouseEvent) {
|
||||
if (event.shiftKey === false) {
|
||||
if (unlock) {
|
||||
const u = unlock
|
||||
unlock = undefined
|
||||
u()
|
||||
}
|
||||
unlock = panelStuffRef.current.addBoundsHighlightLock()
|
||||
}
|
||||
},
|
||||
function onMouseLeave(event: React.MouseEvent) {
|
||||
if (event.shiftKey === false) {
|
||||
if (unlock) {
|
||||
const u = unlock
|
||||
unlock = undefined
|
||||
u()
|
||||
}
|
||||
}
|
||||
},
|
||||
]
|
||||
}, [])
|
||||
const isShiftDown = useKeyDown('Shift')
|
||||
const isPointerHovering = useHoverWithoutDescendants(containerNode)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isShiftDown && isPointerHovering) {
|
||||
const unlock = panelStuffRef.current.addBoundsHighlightLock()
|
||||
return unlock
|
||||
}
|
||||
}, [!isShiftDown && isPointerHovering])
|
||||
|
||||
return usePrism(() => {
|
||||
return (
|
||||
<Container
|
||||
ref={containerRef as $IntentionalAny}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
isShiftDown={isShiftDown}
|
||||
>
|
||||
<FocusRangeStrip layoutP={layoutP} />
|
||||
<FocusRangeThumb thumbType="start" layoutP={layoutP} />
|
||||
<FocusRangeThumb thumbType="end" layoutP={layoutP} />
|
||||
</Container>
|
||||
)
|
||||
}, [layoutP, existingRangeD])
|
||||
}, [layoutP, existingRangeD, isShiftDown])
|
||||
}
|
||||
|
||||
export default FocusRangeZone
|
||||
|
@ -101,6 +90,14 @@ function usePanelDragZoneGestureHandlers(
|
|||
layoutP: Pointer<SequenceEditorPanelLayout>,
|
||||
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] => {
|
||||
const focusRangeCreationGestureHandlers = (): Parameters<
|
||||
typeof useDrag
|
||||
|
@ -175,7 +172,7 @@ function usePanelDragZoneGestureHandlers(
|
|||
}
|
||||
tempTransaction = undefined
|
||||
},
|
||||
lockCursorTo: 'grabbing',
|
||||
lockCursorTo: 'ew-resize',
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -234,22 +231,21 @@ function usePanelDragZoneGestureHandlers(
|
|||
return {
|
||||
onDragStart(event) {
|
||||
if (event.shiftKey) {
|
||||
setMode('creating')
|
||||
currentGestureHandlers = focusRangeCreationGestureHandlers()
|
||||
} else {
|
||||
setMode('moving-panel')
|
||||
currentGestureHandlers = panelMoveGestureHandlers()
|
||||
}
|
||||
currentGestureHandlers.onDragStart!(event)
|
||||
},
|
||||
onDrag(dx, dy, event) {
|
||||
if (!currentGestureHandlers) {
|
||||
console.error('oh no')
|
||||
}
|
||||
currentGestureHandlers!.onDrag(dx, dy, event)
|
||||
},
|
||||
onDragEnd(dragHappened) {
|
||||
setMode('none')
|
||||
currentGestureHandlers!.onDragEnd!(dragHappened)
|
||||
},
|
||||
lockCursorTo: 'grabbing',
|
||||
}
|
||||
}, [layoutP, panelStuffRef])
|
||||
}
|
||||
|
|
|
@ -20,6 +20,10 @@ import usePopover from '@theatre/studio/uiComponents/Popover/usePopover'
|
|||
import BasicPopover from '@theatre/studio/uiComponents/Popover/BasicPopover'
|
||||
import PlayheadPositionPopover from './PlayheadPositionPopover'
|
||||
import {getIsPlayheadAttachedToFocusRange} from '@theatre/studio/UIRoot/useKeyboardShortcuts'
|
||||
import {
|
||||
lockedCursorCssPropName,
|
||||
useCssCursorLock,
|
||||
} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
||||
|
||||
const Container = styled.div<{isVisible: boolean}>`
|
||||
--thumbColor: #00e0ff;
|
||||
|
@ -41,9 +45,11 @@ const Rod = styled.div`
|
|||
height: calc(100% - 8px);
|
||||
border-left: 1px solid #27e0fd;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
|
||||
#pointer-root.draggingPositionInSequenceEditor &:not(.seeking) {
|
||||
pointer-events: auto;
|
||||
/* pointer-events: auto; */
|
||||
/* cursor: var(${lockedCursorCssPropName}); */
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
|
@ -55,6 +61,7 @@ const Rod = styled.div`
|
|||
`
|
||||
|
||||
const Thumb = styled.div`
|
||||
background-color: var(--thumbColor);
|
||||
position: absolute;
|
||||
width: 5px;
|
||||
height: 13px;
|
||||
|
@ -62,16 +69,104 @@ const Thumb = styled.div`
|
|||
left: -2px;
|
||||
z-index: 11;
|
||||
cursor: ew-resize;
|
||||
--sunblock-color: #1f2b2b;
|
||||
|
||||
${pointerEventsAutoInNormalMode};
|
||||
|
||||
&.seeking {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
#pointer-root.draggingPositionInSequenceEditor &:not(.seeking) {
|
||||
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`
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
left: 4px;
|
||||
padding: 0 2px;
|
||||
transform: translateX(-50%);
|
||||
background: #1a1a1a;
|
||||
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>}> = ({
|
||||
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 setIsSeeking = val(layoutP.seeker.setIsSeeking)
|
||||
|
||||
let posBeforeSeek = 0
|
||||
let sequence: Sequence
|
||||
let scaledSpaceToUnitSpace: typeof layoutP.scaledSpace.toUnitSpace.$$__pointer_type
|
||||
|
||||
return {
|
||||
onDragStart() {
|
||||
sequence = val(layoutP.sheet).getSequence()
|
||||
posBeforeSeek = sequence.position
|
||||
scaledSpaceToUnitSpace = val(layoutP.scaledSpace.toUnitSpace)
|
||||
setIsSeeking(true)
|
||||
},
|
||||
onDrag(dx, _, event) {
|
||||
|
@ -174,20 +239,25 @@ const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
|
|||
onDragEnd() {
|
||||
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
|
||||
useLockFrameStampPosition(useVal(layoutP.seeker.isSeeking), -1)
|
||||
useLockFrameStampPosition(useVal(layoutP.seeker.isSeeking) || isDragging, -1)
|
||||
|
||||
return usePrism(() => {
|
||||
const isSeeking = val(layoutP.seeker.isSeeking)
|
||||
|
||||
const sequence = val(layoutP.sheet).getSequence()
|
||||
|
||||
const isPlayheadAttachedToFocusRange = val(
|
||||
getIsPlayheadAttachedToFocusRange(sequence),
|
||||
)
|
||||
|
||||
const posInUnitSpace = sequence.positionDerivation.getValue()
|
||||
|
||||
const posInClippedSpace = val(layoutP.clippedSpace.fromUnitSpace)(
|
||||
|
@ -197,32 +267,27 @@ const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
|
|||
posInClippedSpace >= 0 &&
|
||||
posInClippedSpace <= val(layoutP.clippedSpace.width)
|
||||
|
||||
const isPlayheadAttachedToFocusRange = val(
|
||||
getIsPlayheadAttachedToFocusRange(sequence),
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
{popoverNode}
|
||||
<Container
|
||||
isVisible={isVisible}
|
||||
style={{transform: `translate3d(${posInClippedSpace}px, 0, 0)`}}
|
||||
className={isSeeking ? 'seeking' : ''}
|
||||
className={`${isSeeking && 'seeking'} ${
|
||||
isPlayheadAttachedToFocusRange && 'playheadattachedtofocusrange'
|
||||
}`}
|
||||
{...{[attributeNameThatLocksFramestamp]: 'hide'}}
|
||||
>
|
||||
<Thumb
|
||||
ref={thumbRef as $IntentionalAny}
|
||||
data-pos={posInUnitSpace.toFixed(3)}
|
||||
onClick={(e) => {
|
||||
openPopover(e, thumbNode!)
|
||||
}}
|
||||
>
|
||||
<RoomToClick room={8} />
|
||||
{isPlayheadAttachedToFocusRange ? (
|
||||
<LargeThumbSvg />
|
||||
) : (
|
||||
<RegularThumbSvg />
|
||||
)}
|
||||
<Tooltip
|
||||
style={{top: isPlayheadAttachedToFocusRange ? '-23px' : '-18px'}}
|
||||
>
|
||||
<Squinch />
|
||||
<Tooltip>
|
||||
{sequence.positionFormatter.formatForPlayhead(
|
||||
sequence.closestGridPosition(posInUnitSpace),
|
||||
)}
|
||||
|
|
|
@ -74,9 +74,9 @@ const PlayheadPositionPopover: React.FC<{
|
|||
|
||||
return (
|
||||
<Container>
|
||||
<Label>Playhead position</Label>
|
||||
<Label>Sequence position</Label>
|
||||
<BasicNumberInput
|
||||
value={sequence.position}
|
||||
value={Number(sequence.position.toFixed(3))}
|
||||
{...fns}
|
||||
isValid={greaterThanZero}
|
||||
inputRef={inputRef}
|
||||
|
|
|
@ -11,6 +11,14 @@ import styled from 'styled-components'
|
|||
// using an ID to make CSS selectors faster
|
||||
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`
|
||||
pointer-events: auto;
|
||||
&.normal {
|
||||
|
@ -54,13 +62,20 @@ const PointerEventsHandler: React.FC<{
|
|||
}
|
||||
}, [])
|
||||
|
||||
const lockedCursor = locks[0]?.cursor ?? ''
|
||||
return (
|
||||
<context.Provider value={contextValue}>
|
||||
<Container
|
||||
id={elementId}
|
||||
className={(locks[0]?.className ?? 'normal') + ' ' + props.className}
|
||||
>
|
||||
<CursorOverride style={{cursor: locks[0]?.cursor ?? ''}}>
|
||||
<CursorOverride
|
||||
style={{
|
||||
cursor: lockedCursor,
|
||||
// @ts-ignore
|
||||
[lockedCursorCssPropName]: lockedCursor,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</CursorOverride>
|
||||
</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