Keyframe snapping

* Implemented useCursorLock() to lock the cursor icon during a gesture
* Keyframes now snap to other keyframes
* Keyframes snap to the playhead
* The playhead also snaps to keyframes
This commit is contained in:
Aria Minaei 2021-08-03 23:56:04 +02:00
parent e6bfd999a2
commit 197d59e4c4
27 changed files with 340 additions and 81 deletions

View file

@ -9,7 +9,7 @@ export default class UI {
_showing = false
private _renderTimeout: NodeJS.Timer | undefined = undefined
private _documentBodyUIIsRenderedIn: HTMLElement | undefined = undefined
readonly containerShadow: HTMLElement
readonly containerShadow: ShadowRoot & HTMLElement
constructor(readonly studio: Studio) {
// @todo we can't bootstrap theatre (as in, to design theatre using theatre), if we rely on IDed elements
@ -29,7 +29,7 @@ export default class UI {
// To see why I had to cast this value to HTMLElement, take a look at its
// references. There are a few functions that actually work with a ShadowRoot
// but are typed to accept HTMLElement
}) as $IntentionalAny as HTMLElement
}) as $IntentionalAny as ShadowRoot & HTMLElement
}
show() {

View file

@ -11,6 +11,7 @@ import useRefAndState from '@theatre/studio/utils/useRefAndState'
import {PortalContext} from 'reakit'
import type {$IntentionalAny} from '@theatre/shared/utils/types'
import useKeyboardShortcuts from './useKeyboardShortcuts'
import PointerEventsHandler from '@theatre/studio/uiComponents/PointerEventsHandler'
const GlobalStyle = createGlobalStyle`
:host {
@ -31,14 +32,13 @@ const GlobalStyle = createGlobalStyle`
}
`
const Container = styled.div`
const Container = styled(PointerEventsHandler)`
z-index: 50;
position: fixed;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
pointer-events: none;
`
const PortalLayer = styled.div`

View file

@ -1,4 +1,12 @@
import {lighten} from 'polished'
import {css} from 'styled-components'
export const pointerEventsAutoInNormalMode = css`
pointer-events: none;
#pointer-root.normal & {
pointer-events: auto;
}
`
export const theme = {
panel: {

View file

@ -77,8 +77,28 @@ const ClosePanelButton = styled.button`
}
`
/**
* The &:after part blocks pointer events from reaching the content of the
* pane when a drag gesture is active in theatre's UI. It's a hack and its downside
* is that pane content cannot interact with the rest of theatre's UI while a drag
* gesture is active.
* @todo find a less hacky way?
*/
const F2 = styled(F2Impl)`
position: relative;
&:after {
z-index: 10;
position: absolute;
inset: 0;
display: block;
content: ' ';
pointer-events: none;
#pointer-root:not(.normal) & {
pointer-events: auto;
}
}
`
const ErrorContainer = styled.div`

View file

@ -7,11 +7,12 @@ import {lighten} from 'polished'
import React, {useMemo, useRef, useState} from 'react'
import styled from 'styled-components'
import {panelDimsToPanelPosition, usePanel} from './BasePanel'
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
const Base = styled.div`
position: absolute;
z-index: 10;
pointer-events: auto;
${pointerEventsAutoInNormalMode};
&:after {
position: absolute;
top: -2px;

View file

@ -1,3 +1,4 @@
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
import React from 'react'
import styled from 'styled-components'
import {usePanel} from './BasePanel'
@ -7,7 +8,7 @@ const Container = styled.div`
position: absolute;
user-select: none;
box-sizing: border-box;
pointer-events: auto;
${pointerEventsAutoInNormalMode};
/* box-shadow: 1px 2px 10px -5px black; */
z-index: 1000;

View file

@ -11,6 +11,7 @@ import {
TitleBar_Piece,
TitleBar_Punctuation,
} from '@theatre/studio/panels/BasePanel/common'
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
const Container = styled.div`
background-color: transparent;
@ -30,7 +31,7 @@ const Container = styled.div`
bottom: 0;
right: 0;
width: 20px;
pointer-events: auto;
${pointerEventsAutoInNormalMode};
}
`
@ -54,7 +55,7 @@ const Title = styled.div`
font-weight: 500;
font-size: 10px;
user-select: none;
pointer-events: auto;
${pointerEventsAutoInNormalMode};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@ -84,7 +85,7 @@ const Header = styled.div`
`
const Body = styled.div`
pointer-events: auto;
${pointerEventsAutoInNormalMode};
position: absolute;
top: ${headerHeight};
left: 0;

View file

@ -15,11 +15,12 @@ import {
rowBg,
} from './utils/SingleRowPropEditor'
import DefaultOrStaticValueIndicator from './utils/DefaultValueIndicator'
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
const Container = styled.div`
--step: 8px;
--left-pad: 0px;
pointer-events: auto;
${pointerEventsAutoInNormalMode};
`
const Header = styled.div`

View file

@ -1,4 +1,5 @@
import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic'
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
import {transparentize} from 'polished'
import React from 'react'
import styled from 'styled-components'
@ -75,7 +76,8 @@ const PrevOrNextButton = styled(Button)<{available: boolean}>`
props.available
? nextPrevCursorsTheme.onColor
: nextPrevCursorsTheme.offColor};
pointer-events: ${(props) => (props.available ? 'auto' : 'none')};
${(props) => (props.available ? pointerEventsAutoInNormalMode : '')};
`
const Prev = styled(PrevOrNextButton)<{available: boolean}>`

View file

@ -9,6 +9,7 @@ import type {useEditingToolsForPrimitiveProp} from '@theatre/studio/panels/Objec
import {shadeToColor} from '@theatre/studio/panels/ObjectEditorPanel/propEditors/utils/useEditingToolsForPrimitiveProp'
import styled, {css} from 'styled-components'
import {transparentize} from 'polished'
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
export const indentationFormula = `calc(var(--left-pad) + var(--depth) * var(--step))`
@ -39,7 +40,7 @@ const Row = styled.div`
align-items: stretch;
--right-width: 60%;
position: relative;
pointer-events: auto;
${pointerEventsAutoInNormalMode};
${rowBg};
`

View file

@ -5,6 +5,7 @@ import styled, {css} from 'styled-components'
import noop from '@theatre/shared/utils/noop'
import {transparentize, darken, opacify, lighten} from 'polished'
import {rowBgColor} from '@theatre/studio/panels/ObjectEditorPanel/propEditors/utils/SingleRowPropEditor'
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
export const Container = styled.li<{depth: number}>`
--depth: ${(props) => props.depth};
@ -70,7 +71,7 @@ const Head_Label = styled.span`
${outlineItemFont};
padding: 2px 8px;
pointer-events: auto;
${pointerEventsAutoInNormalMode};
position: relative;
display: block;
height: 13px;
@ -96,7 +97,7 @@ const Head_Label = styled.span`
display: block;
content: ' ';
z-index: 0;
pointer-events: auto;
${pointerEventsAutoInNormalMode};
}
`

View file

@ -2,6 +2,7 @@ import React from 'react'
import styled from 'styled-components'
import {panelZIndexes} from '@theatre/studio/panels/BasePanel/common'
import ProjectsList from './ProjectsList/ProjectsList'
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
const Container = styled.div`
background-color: transparent;
@ -21,7 +22,7 @@ const Container = styled.div`
bottom: 0;
left: 0;
width: 20px;
pointer-events: auto;
${pointerEventsAutoInNormalMode};
}
&:hover:before {
@ -58,7 +59,7 @@ const Header = styled.div`
top: 0;
left: 0;
width: 180px;
pointer-events: auto;
${pointerEventsAutoInNormalMode};
&:after {
position: absolute;
@ -78,14 +79,14 @@ const Title = styled.div`
font-weight: 500;
font-size: 10px;
user-select: none;
pointer-events: auto;
${pointerEventsAutoInNormalMode};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`
const Body = styled.div`
pointer-events: auto;
${pointerEventsAutoInNormalMode};
position: absolute;
top: ${headerHeight};
left: 0;

View file

@ -14,6 +14,7 @@ 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 {useCursorLock} from '@theatre/studio/uiComponents/PointerEventsHandler'
export const dotSize = 6
const hitZoneSize = 12
@ -46,10 +47,20 @@ const Square = styled.div<{isSelected: boolean}>`
const HitZone = styled.div`
position: absolute;
${dims(hitZoneSize)};
z-index: 1;
cursor: ew-resize;
&:hover + ${Square} {
#pointer-root.draggingpositioninsequenceeditor & {
pointer-events: auto;
}
&.beingDragged {
pointer-events: none !important;
}
&:hover + ${Square}, &.beingDragged + ${Square} {
${dims(dotSize + 5)}
}
`
@ -60,7 +71,7 @@ const Dot: React.FC<IProps> = (props) => {
const [ref, node] = useRefAndState<HTMLDivElement | null>(null)
const [contextMenu] = useKeyframeContextMenu(node, props)
useDragKeyframe(node, props)
const [isDragging] = useDragKeyframe(node, props)
return (
<>
@ -71,6 +82,7 @@ const Dot: React.FC<IProps> = (props) => {
[attributeNameThatLocksFramestamp]:
props.keyframe.position.toFixed(3),
}}
className={isDragging ? 'beingDragged' : ''}
/>
<Square isSelected={!!props.selection} />
{contextMenu}
@ -107,7 +119,10 @@ function useKeyframeContextMenu(node: HTMLDivElement | null, props: IProps) {
})
}
function useDragKeyframe(node: HTMLDivElement | null, props: IProps) {
function useDragKeyframe(
node: HTMLDivElement | null,
props: IProps,
): [isDragging: boolean] {
const [isDragging, setIsDragging] = useState(false)
useLockFrameStampPosition(isDragging, props.keyframe.position)
@ -125,7 +140,6 @@ function useDragKeyframe(node: HTMLDivElement | null, props: IProps) {
| undefined
return {
lockCursorTo: 'ew-resize',
onDragStart(event) {
setIsDragging(true)
if (propsRef.current.selection) {
@ -149,11 +163,30 @@ function useDragKeyframe(node: HTMLDivElement | null, props: IProps) {
selectionDragHandlers.onDrag(dx, dy, event)
return
}
const original =
propsAtStartOfDrag.trackData.keyframes[propsAtStartOfDrag.index]
const deltaPos = toUnitSpace(dx)
const newPosBeforeSnapping = Math.max(original.position + deltaPos, 0)
let newPosition = newPosBeforeSnapping
const snapTarget = event
.composedPath()
.find(
(el): el is Element =>
el instanceof Element &&
el !== node &&
el.hasAttribute('data-pos'),
)
if (snapTarget) {
const snapPos = parseFloat(snapTarget.getAttribute('data-pos')!)
if (isFinite(snapPos)) {
newPosition = snapPos
}
}
if (tempTransaction) {
tempTransaction.discard()
tempTransaction = undefined
@ -163,7 +196,7 @@ function useDragKeyframe(node: HTMLDivElement | null, props: IProps) {
{
...propsAtStartOfDrag.leaf.sheetObject.address,
trackId: propsAtStartOfDrag.leaf.trackId,
keyframes: [{...original, position: newPosBeforeSnapping}],
keyframes: [{...original, position: newPosition}],
},
)
})
@ -191,4 +224,8 @@ function useDragKeyframe(node: HTMLDivElement | null, props: IProps) {
}, [])
useDrag(node, gestureHandlers)
useCursorLock(isDragging, 'draggingPositionInSequenceEditor', 'ew-resize')
return [isDragging]
}

View file

@ -9,6 +9,8 @@ import {clamp, mapValues} from 'lodash-es'
import React, {useLayoutEffect, useMemo} from 'react'
import styled from 'styled-components'
import {useReceiveVerticalWheelEvent} from '@theatre/studio/panels/SequenceEditorPanel/VerticalScrollContainer'
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
import {useCursorLock} from '@theatre/studio/uiComponents/PointerEventsHandler'
const Container = styled.div`
position: absolute;
@ -16,7 +18,7 @@ const Container = styled.div`
right: 0;
overflow-x: scroll;
overflow-y: hidden;
pointer-events: all;
${pointerEventsAutoInNormalMode};
// hide the scrollbar on Gecko
scrollbar-width: none;
@ -87,16 +89,42 @@ function useDragHandlers(
const setIsSeeking = val(layoutP.seeker.setIsSeeking)
return {
onDrag(dx: number) {
onDrag(dx: number, _, event) {
const deltaPos = scaledSpaceToUnitSpace(dx)
const newPos = clamp(posBeforeSeek + deltaPos, 0, sequence.length)
sequence.position = newPos
const unsnappedPos = clamp(posBeforeSeek + deltaPos, 0, sequence.length)
let newPosition = unsnappedPos
const snapTarget = event.composedPath().find(
(el): el is Element =>
el instanceof Element &&
// el !== thumbNode &&
el.hasAttribute('data-pos'),
)
if (snapTarget) {
const snapPos = parseFloat(snapTarget.getAttribute('data-pos')!)
if (isFinite(snapPos)) {
newPosition = snapPos
}
}
sequence.position = newPosition
},
onDragStart(event) {
if (event.target instanceof HTMLInputElement) return false
if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) {
return false
}
if (
event
.composedPath()
.some((el) => el instanceof HTMLElement && el.draggable === true)
) {
return false
}
const initialPositionInClippedSpace =
event.clientX - containerEl!.getBoundingClientRect().left
@ -114,11 +142,12 @@ function useDragHandlers(
onDragEnd() {
setIsSeeking(false)
},
lockCursorTo: 'ew-resize',
}
}, [layoutP, containerEl])
useDrag(containerEl, handlers)
const [isDragigng] = useDrag(containerEl, handlers)
useCursorLock(isDragigng, 'draggingPositionInSequenceEditor', 'ew-resize')
}
function useHandlePanAndZoom(
@ -201,13 +230,17 @@ function useUpdateScrollFromClippedSpaceRange(
return rangeStartInScaledSpace
})
const untap = d.changesWithoutValues().tap(() => {
const update = () => {
const rangeStartInScaledSpace = d.getValue()
node.scrollLeft = rangeStartInScaledSpace
})
}
const untap = d.changesWithoutValues().tap(update)
update()
const timeout = setTimeout(update, 100)
return () => {
clearTimeout(timeout)
untap()
}
}, [layoutP, node])

View file

@ -18,6 +18,7 @@ import {
} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
import {GoChevronLeft, GoChevronRight} from 'react-icons/all'
import LengthEditorPopover from './LengthEditorPopover'
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
const coverWidth = 1000
@ -71,7 +72,7 @@ const Tooltip = styled.div`
white-space: nowrap;
padding: 1px 8px;
border-radius: 0 2px 2px 0;
pointer-events: auto;
${pointerEventsAutoInNormalMode};
cursor: ew-resize;
color: #464646;
background-color: #0000004d;
@ -89,7 +90,7 @@ const Tumb = styled.div`
white-space: nowrap;
padding: 1px 2px;
border-radius: 2px;
pointer-events: auto;
${pointerEventsAutoInNormalMode};
justify-content: center;
align-items: center;
cursor: ew-resize;

View file

@ -11,6 +11,7 @@ import React, {useMemo, useRef} from 'react'
import styled from 'styled-components'
import {transformBox} from './Curve'
import type KeyframeEditor from './KeyframeEditor'
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
export const dotSize = 6
@ -28,7 +29,7 @@ const HitZone = styled.circle`
r: 6px;
fill: transparent;
cursor: move;
pointer-events: all;
${pointerEventsAutoInNormalMode};
&:hover {
}
&:hover + ${Circle} {

View file

@ -12,6 +12,8 @@ import type KeyframeEditor from './KeyframeEditor'
import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic'
import {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
import {useCursorLock} from '@theatre/studio/uiComponents/PointerEventsHandler'
export const dotSize = 6
@ -28,13 +30,23 @@ const HitZone = styled.circle`
vector-effect: non-scaling-stroke;
r: 6px;
fill: transparent;
cursor: move;
pointer-events: all;
&:hover {
}
${pointerEventsAutoInNormalMode};
&:hover + ${Circle} {
r: 6px;
}
#pointer-root.normal & {
cursor: move;
}
#pointer-root.draggingpositioninsequenceeditor & {
pointer-events: auto;
}
&.beingDragged {
pointer-events: none !important;
}
`
type IProps = Parameters<typeof KeyframeEditor>[0]
@ -47,7 +59,7 @@ const Dot: React.FC<IProps> = (props) => {
const next = trackData.keyframes[index + 1]
const [contextMenu] = useKeyframeContextMenu(node, props)
useDragKeyframe(node, props)
const isDragging = useDragKeyframe(node, props)
const cyInExtremumSpace = props.extremumSpace.fromValueSpace(cur.value)
@ -63,6 +75,8 @@ const Dot: React.FC<IProps> = (props) => {
{...{
[attributeNameThatLocksFramestamp]: cur.position.toFixed(3),
}}
data-pos={cur.position.toFixed(3)}
className={isDragging ? 'beingDragged' : ''}
/>
<Circle
style={{
@ -78,7 +92,10 @@ const Dot: React.FC<IProps> = (props) => {
export default Dot
function useDragKeyframe(node: SVGCircleElement | null, _props: IProps): void {
function useDragKeyframe(
node: SVGCircleElement | null,
_props: IProps,
): boolean {
const [isDragging, setIsDragging] = useState(false)
useLockFrameStampPosition(isDragging, _props.keyframe.position)
const propsRef = useRef(_props)
@ -203,6 +220,8 @@ function useDragKeyframe(node: SVGCircleElement | null, _props: IProps): void {
}, [])
useDrag(node, gestureHandlers)
useCursorLock(isDragging, 'draggingPositionInSequenceEditor', 'ew-resize')
return isDragging
}
function useKeyframeContextMenu(node: SVGCircleElement | null, props: IProps) {

View file

@ -10,6 +10,7 @@ import React, {useCallback, useMemo, useState} from 'react'
import styled from 'styled-components'
import {zIndexes} from '@theatre/studio/panels/SequenceEditorPanel/SequenceEditorPanel'
import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
const Container = styled.div`
--threadHeight: 6px;
@ -20,7 +21,7 @@ const Container = styled.div`
width: 100%;
left: 12px;
/* bottom: 8px; */
pointer-events: auto;
${pointerEventsAutoInNormalMode};
z-index: ${() => zIndexes.horizontalScrollbar};
`

View file

@ -15,6 +15,7 @@ import {
attributeNameThatLocksFramestamp,
useLockFrameStampPosition,
} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
const Container = styled.div<{isVisible: boolean}>`
--thumbColor: #00e0ff;
@ -36,6 +37,17 @@ const Rod = styled.div`
height: calc(100% - 8px);
border-left: 1px solid #27e0fd;
z-index: 10;
#pointer-root.draggingpositioninsequenceeditor &:not(.seeking) {
pointer-events: auto;
&:after {
position: absolute;
inset: -2px;
display: block;
content: ' ';
}
}
`
const Thumb = styled.div`
@ -47,7 +59,11 @@ const Thumb = styled.div`
left: -2px;
z-index: 11;
cursor: ew-resize;
${pointerEventsAutoInNormalMode};
#pointer-root.draggingpositioninsequenceeditor &:not(.seeking) {
pointer-events: auto;
}
&:before {
position: absolute;
@ -148,10 +164,29 @@ const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
scaledSpaceToUnitSpace = val(layoutP.scaledSpace.toUnitSpace)
setIsSeeking(true)
},
onDrag(dx) {
onDrag(dx, _, event) {
const deltaPos = scaledSpaceToUnitSpace(dx)
const newPos = clamp(posBeforeSeek + deltaPos, 0, sequence.length)
sequence.position = newPos
const unsnappedPos = clamp(posBeforeSeek + deltaPos, 0, sequence.length)
let newPosition = unsnappedPos
const snapTarget = event
.composedPath()
.find(
(el): el is Element =>
el instanceof Element &&
el !== thumbNode &&
el.hasAttribute('data-pos'),
)
if (snapTarget) {
const snapPos = parseFloat(snapTarget.getAttribute('data-pos')!)
if (isFinite(snapPos)) {
newPosition = snapPos
}
}
sequence.position = newPosition
},
onDragEnd() {
setIsSeeking(false)
@ -186,7 +221,10 @@ const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
className={isSeeking ? 'seeking' : ''}
{...{[attributeNameThatLocksFramestamp]: 'hide'}}
>
<Thumb ref={thumbRef as $IntentionalAny}>
<Thumb
ref={thumbRef as $IntentionalAny}
data-pos={posInUnitSpace.toFixed(3)}
>
<RoomToClick room={8} />
<Squinch />
<Tooltip>
@ -196,7 +234,10 @@ const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
</Tooltip>
</Thumb>
<Rod />
<Rod
data-pos={posInUnitSpace.toFixed(3)}
className={isSeeking ? 'seeking' : ''}
/>
</Container>
)
}, [layoutP])

View file

@ -6,6 +6,7 @@ import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEdi
import StampsGrid from '@theatre/studio/panels/SequenceEditorPanel/FrameGrid/StampsGrid'
import PanelDragZone from '@theatre/studio/panels/BasePanel/PanelDragZone'
import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
export const topStripHeight = 20
@ -23,7 +24,7 @@ const Container = styled(PanelDragZone)`
box-sizing: border-box;
background: ${topStripTheme.backgroundColor};
border-bottom: 1px solid ${topStripTheme.borderColor};
pointer-events: auto;
${pointerEventsAutoInNormalMode};
`
const TopStrip: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({

View file

@ -0,0 +1,2 @@
// export const
export {}

View file

@ -0,0 +1,81 @@
import type {$IntentionalAny} from '@theatre/shared/utils/types'
import React, {
createContext,
useContext,
useLayoutEffect,
useMemo,
useState,
} from 'react'
import styled from 'styled-components'
// using an ID to make CSS selectors faster
const elementId = 'pointer-root'
const Container = styled.div`
pointer-events: auto;
&.normal {
pointer-events: none;
}
`
const CursorOverride = styled.div<{cursor: null | string}>`
position: absolute;
inset: 0;
pointer-events: none;
#pointer-root:not(.normal) > & {
cursor: ${(props) => props.cursor ?? 'default'};
pointer-events: auto;
}
`
type Context = {
getLock: (className: string, cursor: string) => () => void
}
type Lock = {className: string; cursor: string}
const context = createContext<Context>({} as $IntentionalAny)
const PointerEventsHandler: React.FC<{
className?: string
}> = (props) => {
const [locks, setLocks] = useState<Lock[]>([])
const contextValue = useMemo<Context>(() => {
const getLock = (className: string, cursor: string) => {
const lock = {className, cursor}
setLocks((s) => [...s, lock])
const unlock = () => {
setLocks((s) => s.filter((l) => l !== lock))
}
return unlock
}
return {
getLock,
}
}, [])
return (
<context.Provider value={contextValue}>
<Container id={elementId} className={locks[0]?.className ?? 'normal'}>
<CursorOverride cursor={locks[0]?.cursor}>
{props.children}
</CursorOverride>
</Container>
</context.Provider>
)
}
export const useCursorLock = (
enabled: boolean,
className: string,
cursor: string,
) => {
const ctx = useContext(context)
useLayoutEffect(() => {
if (!enabled) return
return ctx.getLock(className, cursor)
}, [enabled, className, cursor])
}
export default PointerEventsHandler

View file

@ -1,3 +1,4 @@
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
import getStudio from '@theatre/studio/getStudio'
import useBoundingClientRect from '@theatre/studio/uiComponents/useBoundingClientRect'
import transparentize from 'polished/lib/color/transparentize'
@ -23,7 +24,7 @@ const Container = styled.ul`
padding: 0;
margin: 0;
cursor: default;
pointer-events: all;
${pointerEventsAutoInNormalMode};
border-radius: 3px;
`

View file

@ -1,18 +1,19 @@
export function createCursorLock(cursor: string) {
const el = document.createElement('div')
el.style.cssText = `
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 9999999;`
// import getStudio from '@theatre/studio/getStudio'
el.style.cursor = cursor
document.body.appendChild(el)
const relinquish = () => {
document.body.removeChild(el)
}
// export function createCursorLock(cursor: string) {
// const el = getStudio()!.ui.containerShadow.getElementById(
// 'pointer-events-root',
// )! as HTMLDivElement
return relinquish
}
// el.style.cursor = cursor
// el.classList.remove('pointer-events-mode-normal')
// el.classList.add('pointer-events-mode-locked-for-drag')
// const relinquish = () => {
// el.style.cursor = ''
// el.classList.add('pointer-events-mode-normal')
// el.classList.remove('pointer-events-mode-locked-for-drag')
// }
// return relinquish
// }
export {}

View file

@ -1,3 +1,4 @@
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
import getStudio from '@theatre/studio/getStudio'
import useBoundingClientRect from '@theatre/studio/uiComponents/useBoundingClientRect'
import transparentize from 'polished/lib/color/transparentize'
@ -26,7 +27,7 @@ const Container = styled.ul`
margin: 0;
border-radius: 1px;
cursor: default;
pointer-events: all;
${pointerEventsAutoInNormalMode};
border-radius: 3px;
`

View file

@ -4,11 +4,12 @@ import styled from 'styled-components'
import type {ButtonProps} from 'reakit'
import {outlinePanelTheme} from '@theatre/studio/panels/OutlinePanel/BaseItem'
import {darken, opacify} from 'polished'
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
const {baseBg, baseBorderColor, baseFontColor} = outlinePanelTheme
export const TheButton = styled.button`
pointer-events: auto;
${pointerEventsAutoInNormalMode};
position: relative;
display: flex;
align-items: center;

View file

@ -1,7 +1,7 @@
import {voidFn} from '@theatre/shared/utils'
import type {$FixMe} from '@theatre/shared/utils/types'
import {useLayoutEffect, useRef} from 'react'
import {createCursorLock} from './createCursorLock'
import useRefAndState from '@theatre/studio/utils/useRefAndState'
import {useCursorLock} from './PointerEventsHandler'
export type UseDragOpts = {
disabled?: boolean
@ -15,12 +15,19 @@ export type UseDragOpts = {
export default function useDrag(
target: HTMLElement | SVGElement | undefined | null,
opts: UseDragOpts,
) {
): [isDragging: boolean] {
const optsRef = useRef<typeof opts>(opts)
optsRef.current = opts
const modeRef =
useRef<'dragStartCalled' | 'dragging' | 'notDragging'>('notDragging')
const [modeRef, mode] = useRefAndState<
'dragStartCalled' | 'dragging' | 'notDragging'
>('notDragging')
useCursorLock(
mode === 'dragging' && typeof opts.lockCursorTo === 'string',
'dragging',
opts.lockCursorTo!,
)
const stateRef = useRef<{
dragHappened: boolean
@ -38,12 +45,7 @@ export default function useDrag(
return [event.screenX - startPos.x, event.screenY - startPos.y]
}
let relinquishCursorLock = voidFn
const dragHandler = (event: MouseEvent) => {
if (!stateRef.current.dragHappened && optsRef.current.lockCursorTo) {
relinquishCursorLock = createCursorLock(optsRef.current.lockCursorTo)
}
if (!stateRef.current.dragHappened) stateRef.current.dragHappened = true
modeRef.current = 'dragging'
@ -57,8 +59,6 @@ export default function useDrag(
optsRef.current.onDragEnd &&
optsRef.current.onDragEnd(stateRef.current.dragHappened)
relinquishCursorLock()
relinquishCursorLock = voidFn
}
const addDragListeners = () => {
@ -119,7 +119,6 @@ export default function useDrag(
removeDragListeners()
target.removeEventListener('mousedown', onMouseDown as $FixMe)
target.removeEventListener('click', preventUnwantedClick as $FixMe)
relinquishCursorLock()
if (modeRef.current !== 'notDragging') {
optsRef.current.onDragEnd &&
@ -128,4 +127,6 @@ export default function useDrag(
modeRef.current = 'notDragging'
}
}, [target])
return [mode === 'dragging']
}