QOL improvements to the FocusRange and SequenceEdito (#125)

This commit is contained in:
Aria 2022-05-03 12:38:08 +02:00 committed by GitHub
parent d85e3053af
commit 3ecc3dd012
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 571 additions and 380 deletions

View file

@ -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;
` `

View file

@ -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 (
<> <>

View file

@ -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);

View file

@ -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

View file

@ -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

View file

@ -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} />

View file

@ -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)

View file

@ -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 {

View file

@ -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 {

View file

@ -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

View file

@ -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 ref={hitZoneRef as $IntentionalAny}
? focusRangeStripTheme.disabled.backgroundColor data-pos={position.toFixed(3)}
: focusRangeStripTheme.enabled.backgroundColor {...{
[attributeNameThatLocksFramestamp]: position.toFixed(3),
const startHandlerOffset = focusRangeStripTheme.hitZoneWidth / 2 }}
const endHandlerOffset = className={`${isDragging && 'dragging'} ${enabled && 'enabled'}`}
startHandlerOffset - focusRangeStripTheme.thumbWidth enabled={enabled}
type={thumbType}
return existingRange !== undefined ? ( style={{
<> transform: `translate3d(${posInClippedSpace}px, 0, 0)`,
<HitZone }}
ref={hitZoneRef as $IntentionalAny} >
data-pos={position.toFixed(3)} <ColoredMargin type={thumbType} enabled={enabled} />
style={{ <OuterColoredMargin type={thumbType} />
transform: `translate3d(${posInClippedSpace}px, 0, 0)`, <svg viewBox="0 0 9 18" xmlns="http://www.w3.org/2000/svg">
cursor: thumbType === 'start' ? 'w-resize' : 'e-resize', <line x1="4" y1="6" x2="4" y2="12" />
pointerEvents, <line x1="6" y1="6" x2="6" y2="12" />
}} </svg>
> </TheDiv>
<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>
</>
) : (
<></>
) )
}, [layoutP, hitZoneRef, existingRangeD, focusRangeEnabled]) }, [layoutP, hitZoneRef, existingRangeD, isDragging])
} }
export default FocusRangeThumb export default FocusRangeThumb

View file

@ -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() }, [!isShiftDown && isPointerHovering])
}
unlock = panelStuffRef.current.addBoundsHighlightLock()
}
},
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])
} }

View file

@ -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),
)} )}

View file

@ -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}

View file

@ -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>

View file

@ -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
}