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:
parent
e6bfd999a2
commit
197d59e4c4
27 changed files with 340 additions and 81 deletions
|
@ -9,7 +9,7 @@ export default class UI {
|
||||||
_showing = false
|
_showing = false
|
||||||
private _renderTimeout: NodeJS.Timer | undefined = undefined
|
private _renderTimeout: NodeJS.Timer | undefined = undefined
|
||||||
private _documentBodyUIIsRenderedIn: HTMLElement | undefined = undefined
|
private _documentBodyUIIsRenderedIn: HTMLElement | undefined = undefined
|
||||||
readonly containerShadow: HTMLElement
|
readonly containerShadow: ShadowRoot & HTMLElement
|
||||||
|
|
||||||
constructor(readonly studio: Studio) {
|
constructor(readonly studio: Studio) {
|
||||||
// @todo we can't bootstrap theatre (as in, to design theatre using theatre), if we rely on IDed elements
|
// @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
|
// 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
|
// references. There are a few functions that actually work with a ShadowRoot
|
||||||
// but are typed to accept HTMLElement
|
// but are typed to accept HTMLElement
|
||||||
}) as $IntentionalAny as HTMLElement
|
}) as $IntentionalAny as ShadowRoot & HTMLElement
|
||||||
}
|
}
|
||||||
|
|
||||||
show() {
|
show() {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
||||||
import {PortalContext} from 'reakit'
|
import {PortalContext} from 'reakit'
|
||||||
import type {$IntentionalAny} from '@theatre/shared/utils/types'
|
import type {$IntentionalAny} from '@theatre/shared/utils/types'
|
||||||
import useKeyboardShortcuts from './useKeyboardShortcuts'
|
import useKeyboardShortcuts from './useKeyboardShortcuts'
|
||||||
|
import PointerEventsHandler from '@theatre/studio/uiComponents/PointerEventsHandler'
|
||||||
|
|
||||||
const GlobalStyle = createGlobalStyle`
|
const GlobalStyle = createGlobalStyle`
|
||||||
:host {
|
:host {
|
||||||
|
@ -31,14 +32,13 @@ const GlobalStyle = createGlobalStyle`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled(PointerEventsHandler)`
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
right: 0px;
|
right: 0px;
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
left: 0px;
|
left: 0px;
|
||||||
pointer-events: none;
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const PortalLayer = styled.div`
|
const PortalLayer = styled.div`
|
||||||
|
|
|
@ -1,4 +1,12 @@
|
||||||
import {lighten} from 'polished'
|
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 = {
|
export const theme = {
|
||||||
panel: {
|
panel: {
|
||||||
|
|
|
@ -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)`
|
const F2 = styled(F2Impl)`
|
||||||
position: relative;
|
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`
|
const ErrorContainer = styled.div`
|
||||||
|
|
|
@ -7,11 +7,12 @@ import {lighten} from 'polished'
|
||||||
import React, {useMemo, useRef, useState} from 'react'
|
import React, {useMemo, useRef, useState} from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import {panelDimsToPanelPosition, usePanel} from './BasePanel'
|
import {panelDimsToPanelPosition, usePanel} from './BasePanel'
|
||||||
|
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||||
|
|
||||||
const Base = styled.div`
|
const Base = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
pointer-events: auto;
|
${pointerEventsAutoInNormalMode};
|
||||||
&:after {
|
&:after {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -2px;
|
top: -2px;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import {usePanel} from './BasePanel'
|
import {usePanel} from './BasePanel'
|
||||||
|
@ -7,7 +8,7 @@ const Container = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
pointer-events: auto;
|
${pointerEventsAutoInNormalMode};
|
||||||
/* box-shadow: 1px 2px 10px -5px black; */
|
/* box-shadow: 1px 2px 10px -5px black; */
|
||||||
|
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
TitleBar_Piece,
|
TitleBar_Piece,
|
||||||
TitleBar_Punctuation,
|
TitleBar_Punctuation,
|
||||||
} from '@theatre/studio/panels/BasePanel/common'
|
} from '@theatre/studio/panels/BasePanel/common'
|
||||||
|
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
@ -30,7 +31,7 @@ const Container = styled.div`
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
pointer-events: auto;
|
${pointerEventsAutoInNormalMode};
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
@ -54,7 +55,7 @@ const Title = styled.div`
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
pointer-events: auto;
|
${pointerEventsAutoInNormalMode};
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
@ -84,7 +85,7 @@ const Header = styled.div`
|
||||||
`
|
`
|
||||||
|
|
||||||
const Body = styled.div`
|
const Body = styled.div`
|
||||||
pointer-events: auto;
|
${pointerEventsAutoInNormalMode};
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: ${headerHeight};
|
top: ${headerHeight};
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
|
@ -15,11 +15,12 @@ import {
|
||||||
rowBg,
|
rowBg,
|
||||||
} from './utils/SingleRowPropEditor'
|
} from './utils/SingleRowPropEditor'
|
||||||
import DefaultOrStaticValueIndicator from './utils/DefaultValueIndicator'
|
import DefaultOrStaticValueIndicator from './utils/DefaultValueIndicator'
|
||||||
|
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
--step: 8px;
|
--step: 8px;
|
||||||
--left-pad: 0px;
|
--left-pad: 0px;
|
||||||
pointer-events: auto;
|
${pointerEventsAutoInNormalMode};
|
||||||
`
|
`
|
||||||
|
|
||||||
const Header = styled.div`
|
const Header = styled.div`
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic'
|
import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic'
|
||||||
|
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||||
import {transparentize} from 'polished'
|
import {transparentize} from 'polished'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
@ -75,7 +76,8 @@ const PrevOrNextButton = styled(Button)<{available: boolean}>`
|
||||||
props.available
|
props.available
|
||||||
? nextPrevCursorsTheme.onColor
|
? nextPrevCursorsTheme.onColor
|
||||||
: nextPrevCursorsTheme.offColor};
|
: nextPrevCursorsTheme.offColor};
|
||||||
pointer-events: ${(props) => (props.available ? 'auto' : 'none')};
|
|
||||||
|
${(props) => (props.available ? pointerEventsAutoInNormalMode : '')};
|
||||||
`
|
`
|
||||||
|
|
||||||
const Prev = styled(PrevOrNextButton)<{available: boolean}>`
|
const Prev = styled(PrevOrNextButton)<{available: boolean}>`
|
||||||
|
|
|
@ -9,6 +9,7 @@ import type {useEditingToolsForPrimitiveProp} from '@theatre/studio/panels/Objec
|
||||||
import {shadeToColor} from '@theatre/studio/panels/ObjectEditorPanel/propEditors/utils/useEditingToolsForPrimitiveProp'
|
import {shadeToColor} from '@theatre/studio/panels/ObjectEditorPanel/propEditors/utils/useEditingToolsForPrimitiveProp'
|
||||||
import styled, {css} from 'styled-components'
|
import styled, {css} from 'styled-components'
|
||||||
import {transparentize} from 'polished'
|
import {transparentize} from 'polished'
|
||||||
|
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||||
|
|
||||||
export const indentationFormula = `calc(var(--left-pad) + var(--depth) * var(--step))`
|
export const indentationFormula = `calc(var(--left-pad) + var(--depth) * var(--step))`
|
||||||
|
|
||||||
|
@ -39,7 +40,7 @@ const Row = styled.div`
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
--right-width: 60%;
|
--right-width: 60%;
|
||||||
position: relative;
|
position: relative;
|
||||||
pointer-events: auto;
|
${pointerEventsAutoInNormalMode};
|
||||||
|
|
||||||
${rowBg};
|
${rowBg};
|
||||||
`
|
`
|
||||||
|
|
|
@ -5,6 +5,7 @@ import styled, {css} from 'styled-components'
|
||||||
import noop from '@theatre/shared/utils/noop'
|
import noop from '@theatre/shared/utils/noop'
|
||||||
import {transparentize, darken, opacify, lighten} from 'polished'
|
import {transparentize, darken, opacify, lighten} from 'polished'
|
||||||
import {rowBgColor} from '@theatre/studio/panels/ObjectEditorPanel/propEditors/utils/SingleRowPropEditor'
|
import {rowBgColor} from '@theatre/studio/panels/ObjectEditorPanel/propEditors/utils/SingleRowPropEditor'
|
||||||
|
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||||
|
|
||||||
export const Container = styled.li<{depth: number}>`
|
export const Container = styled.li<{depth: number}>`
|
||||||
--depth: ${(props) => props.depth};
|
--depth: ${(props) => props.depth};
|
||||||
|
@ -70,7 +71,7 @@ const Head_Label = styled.span`
|
||||||
${outlineItemFont};
|
${outlineItemFont};
|
||||||
|
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
pointer-events: auto;
|
${pointerEventsAutoInNormalMode};
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
height: 13px;
|
height: 13px;
|
||||||
|
@ -96,7 +97,7 @@ const Head_Label = styled.span`
|
||||||
display: block;
|
display: block;
|
||||||
content: ' ';
|
content: ' ';
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
pointer-events: auto;
|
${pointerEventsAutoInNormalMode};
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import {panelZIndexes} from '@theatre/studio/panels/BasePanel/common'
|
import {panelZIndexes} from '@theatre/studio/panels/BasePanel/common'
|
||||||
import ProjectsList from './ProjectsList/ProjectsList'
|
import ProjectsList from './ProjectsList/ProjectsList'
|
||||||
|
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
@ -21,7 +22,7 @@ const Container = styled.div`
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
pointer-events: auto;
|
${pointerEventsAutoInNormalMode};
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover:before {
|
&:hover:before {
|
||||||
|
@ -58,7 +59,7 @@ const Header = styled.div`
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 180px;
|
width: 180px;
|
||||||
pointer-events: auto;
|
${pointerEventsAutoInNormalMode};
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -78,14 +79,14 @@ const Title = styled.div`
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
pointer-events: auto;
|
${pointerEventsAutoInNormalMode};
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
`
|
`
|
||||||
|
|
||||||
const Body = styled.div`
|
const Body = styled.div`
|
||||||
pointer-events: auto;
|
${pointerEventsAutoInNormalMode};
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: ${headerHeight};
|
top: ${headerHeight};
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
|
@ -14,6 +14,7 @@ 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 {useCursorLock} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
||||||
|
|
||||||
export const dotSize = 6
|
export const dotSize = 6
|
||||||
const hitZoneSize = 12
|
const hitZoneSize = 12
|
||||||
|
@ -46,10 +47,20 @@ const Square = styled.div<{isSelected: boolean}>`
|
||||||
const HitZone = styled.div`
|
const HitZone = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
${dims(hitZoneSize)};
|
${dims(hitZoneSize)};
|
||||||
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
cursor: ew-resize;
|
cursor: ew-resize;
|
||||||
|
|
||||||
&:hover + ${Square} {
|
#pointer-root.draggingpositioninsequenceeditor & {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.beingDragged {
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover + ${Square}, &.beingDragged + ${Square} {
|
||||||
${dims(dotSize + 5)}
|
${dims(dotSize + 5)}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
@ -60,7 +71,7 @@ const Dot: React.FC<IProps> = (props) => {
|
||||||
const [ref, node] = useRefAndState<HTMLDivElement | null>(null)
|
const [ref, node] = useRefAndState<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
const [contextMenu] = useKeyframeContextMenu(node, props)
|
const [contextMenu] = useKeyframeContextMenu(node, props)
|
||||||
useDragKeyframe(node, props)
|
const [isDragging] = useDragKeyframe(node, props)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -71,6 +82,7 @@ const Dot: React.FC<IProps> = (props) => {
|
||||||
[attributeNameThatLocksFramestamp]:
|
[attributeNameThatLocksFramestamp]:
|
||||||
props.keyframe.position.toFixed(3),
|
props.keyframe.position.toFixed(3),
|
||||||
}}
|
}}
|
||||||
|
className={isDragging ? 'beingDragged' : ''}
|
||||||
/>
|
/>
|
||||||
<Square isSelected={!!props.selection} />
|
<Square isSelected={!!props.selection} />
|
||||||
{contextMenu}
|
{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)
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
useLockFrameStampPosition(isDragging, props.keyframe.position)
|
useLockFrameStampPosition(isDragging, props.keyframe.position)
|
||||||
|
|
||||||
|
@ -125,7 +140,6 @@ function useDragKeyframe(node: HTMLDivElement | null, props: IProps) {
|
||||||
| undefined
|
| undefined
|
||||||
|
|
||||||
return {
|
return {
|
||||||
lockCursorTo: 'ew-resize',
|
|
||||||
onDragStart(event) {
|
onDragStart(event) {
|
||||||
setIsDragging(true)
|
setIsDragging(true)
|
||||||
if (propsRef.current.selection) {
|
if (propsRef.current.selection) {
|
||||||
|
@ -149,11 +163,30 @@ function useDragKeyframe(node: HTMLDivElement | null, props: IProps) {
|
||||||
selectionDragHandlers.onDrag(dx, dy, event)
|
selectionDragHandlers.onDrag(dx, dy, event)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const original =
|
const original =
|
||||||
propsAtStartOfDrag.trackData.keyframes[propsAtStartOfDrag.index]
|
propsAtStartOfDrag.trackData.keyframes[propsAtStartOfDrag.index]
|
||||||
const deltaPos = toUnitSpace(dx)
|
const deltaPos = toUnitSpace(dx)
|
||||||
const newPosBeforeSnapping = Math.max(original.position + deltaPos, 0)
|
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) {
|
if (tempTransaction) {
|
||||||
tempTransaction.discard()
|
tempTransaction.discard()
|
||||||
tempTransaction = undefined
|
tempTransaction = undefined
|
||||||
|
@ -163,7 +196,7 @@ function useDragKeyframe(node: HTMLDivElement | null, props: IProps) {
|
||||||
{
|
{
|
||||||
...propsAtStartOfDrag.leaf.sheetObject.address,
|
...propsAtStartOfDrag.leaf.sheetObject.address,
|
||||||
trackId: propsAtStartOfDrag.leaf.trackId,
|
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)
|
useDrag(node, gestureHandlers)
|
||||||
|
|
||||||
|
useCursorLock(isDragging, 'draggingPositionInSequenceEditor', 'ew-resize')
|
||||||
|
|
||||||
|
return [isDragging]
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,8 @@ import {clamp, mapValues} from 'lodash-es'
|
||||||
import React, {useLayoutEffect, useMemo} from 'react'
|
import React, {useLayoutEffect, useMemo} from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import {useReceiveVerticalWheelEvent} from '@theatre/studio/panels/SequenceEditorPanel/VerticalScrollContainer'
|
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`
|
const Container = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -16,7 +18,7 @@ const Container = styled.div`
|
||||||
right: 0;
|
right: 0;
|
||||||
overflow-x: scroll;
|
overflow-x: scroll;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
pointer-events: all;
|
${pointerEventsAutoInNormalMode};
|
||||||
|
|
||||||
// hide the scrollbar on Gecko
|
// hide the scrollbar on Gecko
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
|
@ -87,16 +89,42 @@ function useDragHandlers(
|
||||||
const setIsSeeking = val(layoutP.seeker.setIsSeeking)
|
const setIsSeeking = val(layoutP.seeker.setIsSeeking)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onDrag(dx: number) {
|
onDrag(dx: number, _, event) {
|
||||||
const deltaPos = scaledSpaceToUnitSpace(dx)
|
const deltaPos = scaledSpaceToUnitSpace(dx)
|
||||||
const newPos = clamp(posBeforeSeek + deltaPos, 0, sequence.length)
|
const unsnappedPos = clamp(posBeforeSeek + deltaPos, 0, sequence.length)
|
||||||
sequence.position = newPos
|
|
||||||
|
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) {
|
onDragStart(event) {
|
||||||
if (event.target instanceof HTMLInputElement) return false
|
if (event.target instanceof HTMLInputElement) return false
|
||||||
if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) {
|
if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
event
|
||||||
|
.composedPath()
|
||||||
|
.some((el) => el instanceof HTMLElement && el.draggable === true)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
const initialPositionInClippedSpace =
|
const initialPositionInClippedSpace =
|
||||||
event.clientX - containerEl!.getBoundingClientRect().left
|
event.clientX - containerEl!.getBoundingClientRect().left
|
||||||
|
|
||||||
|
@ -114,11 +142,12 @@ function useDragHandlers(
|
||||||
onDragEnd() {
|
onDragEnd() {
|
||||||
setIsSeeking(false)
|
setIsSeeking(false)
|
||||||
},
|
},
|
||||||
lockCursorTo: 'ew-resize',
|
|
||||||
}
|
}
|
||||||
}, [layoutP, containerEl])
|
}, [layoutP, containerEl])
|
||||||
|
|
||||||
useDrag(containerEl, handlers)
|
const [isDragigng] = useDrag(containerEl, handlers)
|
||||||
|
|
||||||
|
useCursorLock(isDragigng, 'draggingPositionInSequenceEditor', 'ew-resize')
|
||||||
}
|
}
|
||||||
|
|
||||||
function useHandlePanAndZoom(
|
function useHandlePanAndZoom(
|
||||||
|
@ -201,13 +230,17 @@ function useUpdateScrollFromClippedSpaceRange(
|
||||||
return rangeStartInScaledSpace
|
return rangeStartInScaledSpace
|
||||||
})
|
})
|
||||||
|
|
||||||
const untap = d.changesWithoutValues().tap(() => {
|
const update = () => {
|
||||||
const rangeStartInScaledSpace = d.getValue()
|
const rangeStartInScaledSpace = d.getValue()
|
||||||
|
|
||||||
node.scrollLeft = rangeStartInScaledSpace
|
node.scrollLeft = rangeStartInScaledSpace
|
||||||
})
|
}
|
||||||
|
const untap = d.changesWithoutValues().tap(update)
|
||||||
|
|
||||||
|
update()
|
||||||
|
const timeout = setTimeout(update, 100)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
clearTimeout(timeout)
|
||||||
untap()
|
untap()
|
||||||
}
|
}
|
||||||
}, [layoutP, node])
|
}, [layoutP, node])
|
||||||
|
|
|
@ -18,6 +18,7 @@ import {
|
||||||
} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||||
import {GoChevronLeft, GoChevronRight} from 'react-icons/all'
|
import {GoChevronLeft, GoChevronRight} from 'react-icons/all'
|
||||||
import LengthEditorPopover from './LengthEditorPopover'
|
import LengthEditorPopover from './LengthEditorPopover'
|
||||||
|
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||||
|
|
||||||
const coverWidth = 1000
|
const coverWidth = 1000
|
||||||
|
|
||||||
|
@ -71,7 +72,7 @@ const Tooltip = styled.div`
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
padding: 1px 8px;
|
padding: 1px 8px;
|
||||||
border-radius: 0 2px 2px 0;
|
border-radius: 0 2px 2px 0;
|
||||||
pointer-events: auto;
|
${pointerEventsAutoInNormalMode};
|
||||||
cursor: ew-resize;
|
cursor: ew-resize;
|
||||||
color: #464646;
|
color: #464646;
|
||||||
background-color: #0000004d;
|
background-color: #0000004d;
|
||||||
|
@ -89,7 +90,7 @@ const Tumb = styled.div`
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
padding: 1px 2px;
|
padding: 1px 2px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
pointer-events: auto;
|
${pointerEventsAutoInNormalMode};
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: ew-resize;
|
cursor: ew-resize;
|
||||||
|
|
|
@ -11,6 +11,7 @@ import React, {useMemo, useRef} from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import {transformBox} from './Curve'
|
import {transformBox} from './Curve'
|
||||||
import type KeyframeEditor from './KeyframeEditor'
|
import type KeyframeEditor from './KeyframeEditor'
|
||||||
|
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||||
|
|
||||||
export const dotSize = 6
|
export const dotSize = 6
|
||||||
|
|
||||||
|
@ -28,7 +29,7 @@ const HitZone = styled.circle`
|
||||||
r: 6px;
|
r: 6px;
|
||||||
fill: transparent;
|
fill: transparent;
|
||||||
cursor: move;
|
cursor: move;
|
||||||
pointer-events: all;
|
${pointerEventsAutoInNormalMode};
|
||||||
&:hover {
|
&:hover {
|
||||||
}
|
}
|
||||||
&:hover + ${Circle} {
|
&:hover + ${Circle} {
|
||||||
|
|
|
@ -12,6 +12,8 @@ import type KeyframeEditor from './KeyframeEditor'
|
||||||
import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic'
|
import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic'
|
||||||
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 {useCursorLock} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
||||||
|
|
||||||
export const dotSize = 6
|
export const dotSize = 6
|
||||||
|
|
||||||
|
@ -28,13 +30,23 @@ const HitZone = styled.circle`
|
||||||
vector-effect: non-scaling-stroke;
|
vector-effect: non-scaling-stroke;
|
||||||
r: 6px;
|
r: 6px;
|
||||||
fill: transparent;
|
fill: transparent;
|
||||||
cursor: move;
|
${pointerEventsAutoInNormalMode};
|
||||||
pointer-events: all;
|
|
||||||
&:hover {
|
|
||||||
}
|
|
||||||
&:hover + ${Circle} {
|
&:hover + ${Circle} {
|
||||||
r: 6px;
|
r: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#pointer-root.normal & {
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pointer-root.draggingpositioninsequenceeditor & {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.beingDragged {
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
type IProps = Parameters<typeof KeyframeEditor>[0]
|
type IProps = Parameters<typeof KeyframeEditor>[0]
|
||||||
|
@ -47,7 +59,7 @@ const Dot: React.FC<IProps> = (props) => {
|
||||||
const next = trackData.keyframes[index + 1]
|
const next = trackData.keyframes[index + 1]
|
||||||
|
|
||||||
const [contextMenu] = useKeyframeContextMenu(node, props)
|
const [contextMenu] = useKeyframeContextMenu(node, props)
|
||||||
useDragKeyframe(node, props)
|
const isDragging = useDragKeyframe(node, props)
|
||||||
|
|
||||||
const cyInExtremumSpace = props.extremumSpace.fromValueSpace(cur.value)
|
const cyInExtremumSpace = props.extremumSpace.fromValueSpace(cur.value)
|
||||||
|
|
||||||
|
@ -63,6 +75,8 @@ const Dot: React.FC<IProps> = (props) => {
|
||||||
{...{
|
{...{
|
||||||
[attributeNameThatLocksFramestamp]: cur.position.toFixed(3),
|
[attributeNameThatLocksFramestamp]: cur.position.toFixed(3),
|
||||||
}}
|
}}
|
||||||
|
data-pos={cur.position.toFixed(3)}
|
||||||
|
className={isDragging ? 'beingDragged' : ''}
|
||||||
/>
|
/>
|
||||||
<Circle
|
<Circle
|
||||||
style={{
|
style={{
|
||||||
|
@ -78,7 +92,10 @@ const Dot: React.FC<IProps> = (props) => {
|
||||||
|
|
||||||
export default Dot
|
export default Dot
|
||||||
|
|
||||||
function useDragKeyframe(node: SVGCircleElement | null, _props: IProps): void {
|
function useDragKeyframe(
|
||||||
|
node: SVGCircleElement | null,
|
||||||
|
_props: IProps,
|
||||||
|
): boolean {
|
||||||
const [isDragging, setIsDragging] = useState(false)
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
useLockFrameStampPosition(isDragging, _props.keyframe.position)
|
useLockFrameStampPosition(isDragging, _props.keyframe.position)
|
||||||
const propsRef = useRef(_props)
|
const propsRef = useRef(_props)
|
||||||
|
@ -203,6 +220,8 @@ function useDragKeyframe(node: SVGCircleElement | null, _props: IProps): void {
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useDrag(node, gestureHandlers)
|
useDrag(node, gestureHandlers)
|
||||||
|
useCursorLock(isDragging, 'draggingPositionInSequenceEditor', 'ew-resize')
|
||||||
|
return isDragging
|
||||||
}
|
}
|
||||||
|
|
||||||
function useKeyframeContextMenu(node: SVGCircleElement | null, props: IProps) {
|
function useKeyframeContextMenu(node: SVGCircleElement | null, props: IProps) {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import React, {useCallback, useMemo, useState} from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import {zIndexes} from '@theatre/studio/panels/SequenceEditorPanel/SequenceEditorPanel'
|
import {zIndexes} from '@theatre/studio/panels/SequenceEditorPanel/SequenceEditorPanel'
|
||||||
import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||||
|
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
--threadHeight: 6px;
|
--threadHeight: 6px;
|
||||||
|
@ -20,7 +21,7 @@ const Container = styled.div`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
left: 12px;
|
left: 12px;
|
||||||
/* bottom: 8px; */
|
/* bottom: 8px; */
|
||||||
pointer-events: auto;
|
${pointerEventsAutoInNormalMode};
|
||||||
z-index: ${() => zIndexes.horizontalScrollbar};
|
z-index: ${() => zIndexes.horizontalScrollbar};
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
attributeNameThatLocksFramestamp,
|
attributeNameThatLocksFramestamp,
|
||||||
useLockFrameStampPosition,
|
useLockFrameStampPosition,
|
||||||
} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||||
|
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||||
|
|
||||||
const Container = styled.div<{isVisible: boolean}>`
|
const Container = styled.div<{isVisible: boolean}>`
|
||||||
--thumbColor: #00e0ff;
|
--thumbColor: #00e0ff;
|
||||||
|
@ -36,6 +37,17 @@ 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-root.draggingpositioninsequenceeditor &:not(.seeking) {
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
position: absolute;
|
||||||
|
inset: -2px;
|
||||||
|
display: block;
|
||||||
|
content: ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const Thumb = styled.div`
|
const Thumb = styled.div`
|
||||||
|
@ -47,7 +59,11 @@ const Thumb = styled.div`
|
||||||
left: -2px;
|
left: -2px;
|
||||||
z-index: 11;
|
z-index: 11;
|
||||||
cursor: ew-resize;
|
cursor: ew-resize;
|
||||||
pointer-events: auto;
|
${pointerEventsAutoInNormalMode};
|
||||||
|
|
||||||
|
#pointer-root.draggingpositioninsequenceeditor &:not(.seeking) {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -148,10 +164,29 @@ const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
|
||||||
scaledSpaceToUnitSpace = val(layoutP.scaledSpace.toUnitSpace)
|
scaledSpaceToUnitSpace = val(layoutP.scaledSpace.toUnitSpace)
|
||||||
setIsSeeking(true)
|
setIsSeeking(true)
|
||||||
},
|
},
|
||||||
onDrag(dx) {
|
onDrag(dx, _, event) {
|
||||||
const deltaPos = scaledSpaceToUnitSpace(dx)
|
const deltaPos = scaledSpaceToUnitSpace(dx)
|
||||||
const newPos = clamp(posBeforeSeek + deltaPos, 0, sequence.length)
|
const unsnappedPos = clamp(posBeforeSeek + deltaPos, 0, sequence.length)
|
||||||
sequence.position = newPos
|
|
||||||
|
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() {
|
onDragEnd() {
|
||||||
setIsSeeking(false)
|
setIsSeeking(false)
|
||||||
|
@ -186,7 +221,10 @@ const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
|
||||||
className={isSeeking ? 'seeking' : ''}
|
className={isSeeking ? 'seeking' : ''}
|
||||||
{...{[attributeNameThatLocksFramestamp]: 'hide'}}
|
{...{[attributeNameThatLocksFramestamp]: 'hide'}}
|
||||||
>
|
>
|
||||||
<Thumb ref={thumbRef as $IntentionalAny}>
|
<Thumb
|
||||||
|
ref={thumbRef as $IntentionalAny}
|
||||||
|
data-pos={posInUnitSpace.toFixed(3)}
|
||||||
|
>
|
||||||
<RoomToClick room={8} />
|
<RoomToClick room={8} />
|
||||||
<Squinch />
|
<Squinch />
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
|
@ -196,7 +234,10 @@ const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Thumb>
|
</Thumb>
|
||||||
|
|
||||||
<Rod />
|
<Rod
|
||||||
|
data-pos={posInUnitSpace.toFixed(3)}
|
||||||
|
className={isSeeking ? 'seeking' : ''}
|
||||||
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}, [layoutP])
|
}, [layoutP])
|
||||||
|
|
|
@ -6,6 +6,7 @@ import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEdi
|
||||||
import StampsGrid from '@theatre/studio/panels/SequenceEditorPanel/FrameGrid/StampsGrid'
|
import StampsGrid from '@theatre/studio/panels/SequenceEditorPanel/FrameGrid/StampsGrid'
|
||||||
import PanelDragZone from '@theatre/studio/panels/BasePanel/PanelDragZone'
|
import PanelDragZone from '@theatre/studio/panels/BasePanel/PanelDragZone'
|
||||||
import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||||
|
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||||
|
|
||||||
export const topStripHeight = 20
|
export const topStripHeight = 20
|
||||||
|
|
||||||
|
@ -23,7 +24,7 @@ const Container = styled(PanelDragZone)`
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: ${topStripTheme.backgroundColor};
|
background: ${topStripTheme.backgroundColor};
|
||||||
border-bottom: 1px solid ${topStripTheme.borderColor};
|
border-bottom: 1px solid ${topStripTheme.borderColor};
|
||||||
pointer-events: auto;
|
${pointerEventsAutoInNormalMode};
|
||||||
`
|
`
|
||||||
|
|
||||||
const TopStrip: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
|
const TopStrip: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
// export const
|
||||||
|
export {}
|
81
theatre/studio/src/uiComponents/PointerEventsHandler.tsx
Normal file
81
theatre/studio/src/uiComponents/PointerEventsHandler.tsx
Normal 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
|
|
@ -1,3 +1,4 @@
|
||||||
|
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||||
import getStudio from '@theatre/studio/getStudio'
|
import getStudio from '@theatre/studio/getStudio'
|
||||||
import useBoundingClientRect from '@theatre/studio/uiComponents/useBoundingClientRect'
|
import useBoundingClientRect from '@theatre/studio/uiComponents/useBoundingClientRect'
|
||||||
import transparentize from 'polished/lib/color/transparentize'
|
import transparentize from 'polished/lib/color/transparentize'
|
||||||
|
@ -23,7 +24,7 @@ const Container = styled.ul`
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
pointer-events: all;
|
${pointerEventsAutoInNormalMode};
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,19 @@
|
||||||
export function createCursorLock(cursor: string) {
|
// import getStudio from '@theatre/studio/getStudio'
|
||||||
const el = document.createElement('div')
|
|
||||||
el.style.cssText = `
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 9999999;`
|
|
||||||
|
|
||||||
el.style.cursor = cursor
|
// export function createCursorLock(cursor: string) {
|
||||||
document.body.appendChild(el)
|
// const el = getStudio()!.ui.containerShadow.getElementById(
|
||||||
const relinquish = () => {
|
// 'pointer-events-root',
|
||||||
document.body.removeChild(el)
|
// )! 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 {}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||||
import getStudio from '@theatre/studio/getStudio'
|
import getStudio from '@theatre/studio/getStudio'
|
||||||
import useBoundingClientRect from '@theatre/studio/uiComponents/useBoundingClientRect'
|
import useBoundingClientRect from '@theatre/studio/uiComponents/useBoundingClientRect'
|
||||||
import transparentize from 'polished/lib/color/transparentize'
|
import transparentize from 'polished/lib/color/transparentize'
|
||||||
|
@ -26,7 +27,7 @@ const Container = styled.ul`
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border-radius: 1px;
|
border-radius: 1px;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
pointer-events: all;
|
${pointerEventsAutoInNormalMode};
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
|
@ -4,11 +4,12 @@ import styled from 'styled-components'
|
||||||
import type {ButtonProps} from 'reakit'
|
import type {ButtonProps} from 'reakit'
|
||||||
import {outlinePanelTheme} from '@theatre/studio/panels/OutlinePanel/BaseItem'
|
import {outlinePanelTheme} from '@theatre/studio/panels/OutlinePanel/BaseItem'
|
||||||
import {darken, opacify} from 'polished'
|
import {darken, opacify} from 'polished'
|
||||||
|
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||||
|
|
||||||
const {baseBg, baseBorderColor, baseFontColor} = outlinePanelTheme
|
const {baseBg, baseBorderColor, baseFontColor} = outlinePanelTheme
|
||||||
|
|
||||||
export const TheButton = styled.button`
|
export const TheButton = styled.button`
|
||||||
pointer-events: auto;
|
${pointerEventsAutoInNormalMode};
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {voidFn} from '@theatre/shared/utils'
|
|
||||||
import type {$FixMe} from '@theatre/shared/utils/types'
|
import type {$FixMe} from '@theatre/shared/utils/types'
|
||||||
import {useLayoutEffect, useRef} from 'react'
|
import {useLayoutEffect, useRef} from 'react'
|
||||||
import {createCursorLock} from './createCursorLock'
|
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
||||||
|
import {useCursorLock} from './PointerEventsHandler'
|
||||||
|
|
||||||
export type UseDragOpts = {
|
export type UseDragOpts = {
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
@ -15,12 +15,19 @@ export type UseDragOpts = {
|
||||||
export default function useDrag(
|
export default function useDrag(
|
||||||
target: HTMLElement | SVGElement | undefined | null,
|
target: HTMLElement | SVGElement | undefined | null,
|
||||||
opts: UseDragOpts,
|
opts: UseDragOpts,
|
||||||
) {
|
): [isDragging: boolean] {
|
||||||
const optsRef = useRef<typeof opts>(opts)
|
const optsRef = useRef<typeof opts>(opts)
|
||||||
optsRef.current = opts
|
optsRef.current = opts
|
||||||
|
|
||||||
const modeRef =
|
const [modeRef, mode] = useRefAndState<
|
||||||
useRef<'dragStartCalled' | 'dragging' | 'notDragging'>('notDragging')
|
'dragStartCalled' | 'dragging' | 'notDragging'
|
||||||
|
>('notDragging')
|
||||||
|
|
||||||
|
useCursorLock(
|
||||||
|
mode === 'dragging' && typeof opts.lockCursorTo === 'string',
|
||||||
|
'dragging',
|
||||||
|
opts.lockCursorTo!,
|
||||||
|
)
|
||||||
|
|
||||||
const stateRef = useRef<{
|
const stateRef = useRef<{
|
||||||
dragHappened: boolean
|
dragHappened: boolean
|
||||||
|
@ -38,12 +45,7 @@ export default function useDrag(
|
||||||
return [event.screenX - startPos.x, event.screenY - startPos.y]
|
return [event.screenX - startPos.x, event.screenY - startPos.y]
|
||||||
}
|
}
|
||||||
|
|
||||||
let relinquishCursorLock = voidFn
|
|
||||||
|
|
||||||
const dragHandler = (event: MouseEvent) => {
|
const dragHandler = (event: MouseEvent) => {
|
||||||
if (!stateRef.current.dragHappened && optsRef.current.lockCursorTo) {
|
|
||||||
relinquishCursorLock = createCursorLock(optsRef.current.lockCursorTo)
|
|
||||||
}
|
|
||||||
if (!stateRef.current.dragHappened) stateRef.current.dragHappened = true
|
if (!stateRef.current.dragHappened) stateRef.current.dragHappened = true
|
||||||
modeRef.current = 'dragging'
|
modeRef.current = 'dragging'
|
||||||
|
|
||||||
|
@ -57,8 +59,6 @@ export default function useDrag(
|
||||||
|
|
||||||
optsRef.current.onDragEnd &&
|
optsRef.current.onDragEnd &&
|
||||||
optsRef.current.onDragEnd(stateRef.current.dragHappened)
|
optsRef.current.onDragEnd(stateRef.current.dragHappened)
|
||||||
relinquishCursorLock()
|
|
||||||
relinquishCursorLock = voidFn
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const addDragListeners = () => {
|
const addDragListeners = () => {
|
||||||
|
@ -119,7 +119,6 @@ export default function useDrag(
|
||||||
removeDragListeners()
|
removeDragListeners()
|
||||||
target.removeEventListener('mousedown', onMouseDown as $FixMe)
|
target.removeEventListener('mousedown', onMouseDown as $FixMe)
|
||||||
target.removeEventListener('click', preventUnwantedClick as $FixMe)
|
target.removeEventListener('click', preventUnwantedClick as $FixMe)
|
||||||
relinquishCursorLock()
|
|
||||||
|
|
||||||
if (modeRef.current !== 'notDragging') {
|
if (modeRef.current !== 'notDragging') {
|
||||||
optsRef.current.onDragEnd &&
|
optsRef.current.onDragEnd &&
|
||||||
|
@ -128,4 +127,6 @@ export default function useDrag(
|
||||||
modeRef.current = 'notDragging'
|
modeRef.current = 'notDragging'
|
||||||
}
|
}
|
||||||
}, [target])
|
}, [target])
|
||||||
|
|
||||||
|
return [mode === 'dragging']
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue