Implement inline keyframe editing (#135)

* refactor: improve idents near DeterminePropEditor

* fix: Allow `MouseEvent` for `usePopover` `OpenFn`
 * Anticipate to be used with `useDrag` (which is written using `MouseEvent`s)

* refactor: rename local variable depth to visualIndentation

* fix: Hide out of bounds LengthIndicator

* pointer: Fix type errors with `getPointerParts` using generics

 * Fix param type for `getPointerMeta`

* Inline keyframe editor + popover nesting
 * Complete inline editor,
 * add reason for close popover, &
 * enable popover nesting
 * enable inline keyframe editing with splitting of DeterminePropEditor
 * usePopover has PopoverAutoCloseLock helper

Co-authored-by: Elliot <key.draw@gmail.com>
Co-authored-by: Aria <aria.minaei@gmail.com>

* prop editor: Reorganize prop-editors & improve documentation

Co-authored-by: Cole Lawrence <cole@colelawrence.com>
Co-authored-by: Elliot <key.draw@gmail.com>

Co-authored-by: Elliot <key.draw@gmail.com>
Co-authored-by: Aria <aria.minaei@gmail.com>
This commit is contained in:
Cole Lawrence 2022-05-16 08:14:47 -04:00 committed by GitHub
parent e140bb6fc4
commit 2324218453
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 895 additions and 516 deletions

View file

@ -113,9 +113,7 @@ const proxyHandler = {
* *
* @param p - The pointer. * @param p - The pointer.
*/ */
export const getPointerMeta = ( export const getPointerMeta = <_>(p: PointerType<_>): PointerMeta => {
p: Pointer<$IntentionalAny> | Pointer<{}> | Pointer<unknown>,
): PointerMeta => {
// @ts-ignore @todo // @ts-ignore @todo
const meta: PointerMeta = p[ const meta: PointerMeta = p[
pointerMetaSymbol as unknown as $IntentionalAny pointerMetaSymbol as unknown as $IntentionalAny
@ -135,8 +133,8 @@ export const getPointerMeta = (
* *
* @returns An object with two properties: `root`-the root object or the pointer, and `path`-the path of the pointer. `path` is an array of the property-chain. * @returns An object with two properties: `root`-the root object or the pointer, and `path`-the path of the pointer. `path` is an array of the property-chain.
*/ */
export const getPointerParts = ( export const getPointerParts = <_>(
p: Pointer<$IntentionalAny> | Pointer<{}> | Pointer<unknown>, p: Pointer<_>,
): {root: {}; path: PathToProp} => { ): {root: {}; path: PathToProp} => {
const {root, path} = getPointerMeta(p) const {root, path} = getPointerMeta(p)
return {root, path} return {root, path}

View file

@ -37,6 +37,8 @@ export type SerializableMap<
* *
* However this wouldn't protect against other unserializable stuff, or nested * However this wouldn't protect against other unserializable stuff, or nested
* unserializable stuff, since using mapped types seem to break it for some reason. * unserializable stuff, since using mapped types seem to break it for some reason.
*
* TODO: Consider renaming to `SerializableSimple` if this should be aligned with "simple props".
*/ */
export type SerializablePrimitive = export type SerializablePrimitive =
| string | string

View file

@ -14,9 +14,7 @@ type State_Captured = {
} }
type State = type State =
| { | {type: 'Ready'}
type: 'Ready'
}
| State_Captured | State_Captured
| {type: 'Committed'} | {type: 'Committed'}
| {type: 'Discarded'} | {type: 'Discarded'}
@ -24,12 +22,20 @@ type State =
let lastScrubIdAsNumber = 0 let lastScrubIdAsNumber = 0
/** /**
* The scrub API * The scrub API is a simple construct for changing values in Theatre in a history-compatible way.
* Primarily, it can be used to create a series of value changes using a temp transaction without
* creating multiple transactions.
*
* The name is inspired by the activity of "scrubbing" the value of an input through clicking and
* dragging left and right. But, the API is not limited to chaning a single prop's value.
*
* For now, using the {@link IScrubApi.set} will result in changing the values where the
* playhead is (the `sequence.position`).
*/ */
export interface IScrubApi { export interface IScrubApi {
/** /**
* Set the value of a prop by its pointer. If the prop is sequenced, the value * Set the value of a prop by its pointer. If the prop is sequenced, the value
* will be a keyframe at the current sequence position. * will be a keyframe at the current playhead position (`sequence.position`).
* *
* @param pointer - A Pointer, like object.props * @param pointer - A Pointer, like object.props
* @param value - The value to override the existing value. This is treated as a deep partial value. * @param value - The value to override the existing value. This is treated as a deep partial value.
@ -80,7 +86,7 @@ export interface IScrub {
capture(fn: (api: IScrubApi) => void): void capture(fn: (api: IScrubApi) => void): void
/** /**
* Clearts the ops of the scrub and destroys it. After calling this, * Clears the ops of the scrub and destroys it. After calling this,
* you won't be able to call `scrub.capture()` anymore. * you won't be able to call `scrub.capture()` anymore.
*/ */
discard(): void discard(): void
@ -178,7 +184,7 @@ export default class Scrub implements IScrub {
) )
} }
const {root, path} = getPointerParts(pointer as Pointer<$FixMe>) const {root, path} = getPointerParts(pointer)
if (!isSheetObject(root)) { if (!isSheetObject(root)) {
throw new Error(`We can only scrub props of Sheet Objects for now`) throw new Error(`We can only scrub props of Sheet Objects for now`)
@ -198,7 +204,7 @@ export default class Scrub implements IScrub {
const flagsTransaction = this._studio.tempTransaction(({stateEditors}) => { const flagsTransaction = this._studio.tempTransaction(({stateEditors}) => {
sets.forEach((pointer) => { sets.forEach((pointer) => {
const {root, path} = getPointerParts(pointer as Pointer<$FixMe>) const {root, path} = getPointerParts(pointer)
if (!isSheetObject(root)) { if (!isSheetObject(root)) {
return return
} }

View file

@ -234,7 +234,7 @@ export default function createTransactionPrivateApi(
(v, pathToProp) => { (v, pathToProp) => {
unsetStaticOrKeyframeProp(v, pathToProp) unsetStaticOrKeyframeProp(v, pathToProp)
}, },
getPointerParts(pointer as Pointer<$IntentionalAny>).path, getPointerParts(pointer).path,
) )
} else { } else {
unsetStaticOrKeyframeProp(defaultValue, path) unsetStaticOrKeyframeProp(defaultValue, path)

View file

@ -10,7 +10,7 @@ import {
} from '@theatre/studio/panels/BasePanel/common' } from '@theatre/studio/panels/BasePanel/common'
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css' import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
import ObjectDetails from './ObjectDetails' import ObjectDetails from './ObjectDetails'
import ProjectDetails from './ProjectDetails/ProjectDetails' import ProjectDetails from './ProjectDetails'
const Container = styled.div` const Container = styled.div`
background-color: transparent; background-color: transparent;

View file

@ -0,0 +1,71 @@
import React from 'react'
import type {Pointer} from '@theatre/dataverse'
import type {
PropTypeConfig,
PropTypeConfig_AllSimples,
} from '@theatre/core/propTypes'
import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
import {getPropTypeByPointer} from '@theatre/studio/propEditors/utils/getPropTypeByPointer'
import {simplePropEditorByPropType} from '@theatre/studio/propEditors/simpleEditors/simplePropEditorByPropType'
import type {PropConfigForType} from '@theatre/studio/propEditors/utils/PropConfigForType'
import type {ISimplePropEditorReactProps} from '@theatre/studio/propEditors/simpleEditors/ISimplePropEditorReactProps'
import DetailCompoundPropEditor from './DeterminePropEditorForDetail/DetailCompoundPropEditor'
import DetailSimplePropEditor from './DeterminePropEditorForDetail/DetailSimplePropEditor'
/**
* Given a propConfig, this function gives the corresponding prop editor for
* use in the details panel. {@link DeterminePropEditorForKeyframe} does the
* same thing for the dope sheet inline prop editor on a keyframe. The main difference
* between this function and {@link DeterminePropEditorForKeyframe} is that this
* one shows prop editors *with* keyframe navigation controls (that look
* like `< ・ >`).
*
* @param p - propConfig object for any type of prop.
*/
const DeterminePropEditorForDetail: React.VFC<
IDeterminePropEditorForDetailProps<PropTypeConfig['type']>
> = (p) => {
const propConfig =
p.propConfig ?? getPropTypeByPointer(p.pointerToProp, p.obj)
if (propConfig.type === 'compound') {
return (
<DetailCompoundPropEditor
obj={p.obj}
visualIndentation={p.visualIndentation}
pointerToProp={p.pointerToProp}
propConfig={propConfig}
/>
)
} else if (propConfig.type === 'enum') {
// notice: enums are not implemented, yet.
return <></>
} else {
const PropEditor = simplePropEditorByPropType[propConfig.type]
return (
<DetailSimplePropEditor
SimpleEditorComponent={
PropEditor as React.VFC<
ISimplePropEditorReactProps<PropTypeConfig_AllSimples>
>
}
obj={p.obj}
visualIndentation={p.visualIndentation}
pointerToProp={p.pointerToProp}
propConfig={propConfig}
/>
)
}
}
export default DeterminePropEditorForDetail
type IDeterminePropEditorForDetailProps<K extends PropTypeConfig['type']> =
IDetailEditablePropertyProps<K> & {
visualIndentation: number
}
type IDetailEditablePropertyProps<K extends PropTypeConfig['type']> = {
obj: SheetObject
pointerToProp: Pointer<PropConfigForType<K>['valueType']>
propConfig: PropConfigForType<K>
}

View file

@ -1,21 +1,22 @@
import type {PropTypeConfig_Compound} from '@theatre/core/propTypes' import type {PropTypeConfig_Compound} from '@theatre/core/propTypes'
import {isPropConfigComposite} from '@theatre/shared/propTypes/utils' import {isPropConfigComposite} from '@theatre/shared/propTypes/utils'
import type {$IntentionalAny} from '@theatre/shared/utils/types'
import {getPointerParts} from '@theatre/dataverse' import {getPointerParts} from '@theatre/dataverse'
import type {Pointer} from '@theatre/dataverse'
import last from 'lodash-es/last' import last from 'lodash-es/last'
import {darken, transparentize} from 'polished' import {darken, transparentize} from 'polished'
import React from 'react' import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import DeterminePropEditor from './DeterminePropEditor'
import { import {
indentationFormula, indentationFormula,
propNameText,
rowBg, rowBg,
} from './utils/SingleRowPropEditor' } from '@theatre/studio/panels/DetailPanel/DeterminePropEditorForDetail/SingleRowPropEditor'
import DefaultOrStaticValueIndicator from './utils/DefaultValueIndicator' import {propNameTextCSS} from '@theatre/studio/propEditors/utils/propNameTextCSS'
import DefaultOrStaticValueIndicator from '@theatre/studio/propEditors/DefaultValueIndicator'
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css' import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
import useRefAndState from '@theatre/studio/utils/useRefAndState' import useRefAndState from '@theatre/studio/utils/useRefAndState'
import type {IPropEditorFC} from './utils/IPropEditorFC' import DeterminePropEditorForDetail from '@theatre/studio/panels/DetailPanel/DeterminePropEditorForDetail'
import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
import type {$FixMe} from '@theatre/shared/utils/types'
const Container = styled.div` const Container = styled.div`
--step: 8px; --step: 8px;
@ -49,7 +50,7 @@ const PropName = styled.div`
/* color: white; */ /* color: white; */
} }
${() => propNameText}; ${() => propNameTextCSS};
` `
const color = transparentize(0.05, `#282b2f`) const color = transparentize(0.05, `#282b2f`)
@ -59,9 +60,23 @@ const SubProps = styled.div<{depth: number; lastSubIsComposite: boolean}>`
/* padding: ${(props) => (props.lastSubIsComposite ? 0 : '4px')} 0; */ /* padding: ${(props) => (props.lastSubIsComposite ? 0 : '4px')} 0; */
` `
const CompoundPropEditor: IPropEditorFC< export type ICompoundPropDetailEditorProps<
PropTypeConfig_Compound<$IntentionalAny> TPropTypeConfig extends PropTypeConfig_Compound<any>,
> = ({pointerToProp, obj, propConfig, visualIndentation: depth}) => { > = {
propConfig: TPropTypeConfig
pointerToProp: Pointer<TPropTypeConfig['valueType']>
obj: SheetObject
visualIndentation: number
}
function DetailCompoundPropEditor<
TPropTypeConfig extends PropTypeConfig_Compound<any>,
>({
pointerToProp,
obj,
propConfig,
visualIndentation,
}: ICompoundPropDetailEditorProps<TPropTypeConfig>) {
const propName = propConfig.label ?? last(getPointerParts(pointerToProp).path) const propName = propConfig.label ?? last(getPointerParts(pointerToProp).path)
const allSubs = Object.entries(propConfig.props) const allSubs = Object.entries(propConfig.props)
@ -75,64 +90,15 @@ const CompoundPropEditor: IPropEditorFC<
const [propNameContainerRef, propNameContainer] = const [propNameContainerRef, propNameContainer] =
useRefAndState<HTMLDivElement | null>(null) useRefAndState<HTMLDivElement | null>(null)
// const [contextMenu] = useContextMenu(propNameContainer, {
// items: () => {
// const items: IContextMenuItem[] = []
// const pathToProp = getPointerParts(pointerToProp).path
// const validSequencedTracks = val(
// obj.template.getMapOfValidSequenceTracks_forStudio(),
// )
// const possibleSequenceTrackIds = getDeep(validSequencedTracks, pathToProp)
// const hasSequencedTracks = !!(
// typeof possibleSequenceTrackIds === 'object' &&
// possibleSequenceTrackIds &&
// Object.keys(possibleSequenceTrackIds).length > 0
// )
// if (hasSequencedTracks) {
// items.push({
// label: 'Make All Static',
// enabled: hasSequencedTracks,
// callback: () => {
// getStudio()!.transaction(({stateEditors}) => {
// const propAddress = {...obj.address, pathToProp}
// stateEditors.coreByProject.historic.sheetsById.sequence.setCompoundPropAsStatic(
// {
// ...propAddress,
// value: obj.getValueByPointer(
// pointerToProp,
// ) as unknown as SerializableMap,
// },
// )
// })
// },
// })
// }
// items.push({
// label: 'Reset all',
// callback: () => {
// getStudio()!.transaction(({unset}) => {
// unset(pointerToProp)
// })
// },
// })
// return items
// },
// })
const lastSubPropIsComposite = compositeSubs.length > 0 const lastSubPropIsComposite = compositeSubs.length > 0
// previous versions of the DetailCompoundPropEditor had a context menu item for "Reset values".
return ( return (
<Container> <Container>
{/* {contextMenu} */}
<Header <Header
// @ts-ignore // @ts-ignore
style={{'--depth': depth - 1}} style={{'--depth': visualIndentation - 1}}
> >
<Padding> <Padding>
<DefaultOrStaticValueIndicator hasStaticOverride={false} /> <DefaultOrStaticValueIndicator hasStaticOverride={false} />
@ -142,19 +108,19 @@ const CompoundPropEditor: IPropEditorFC<
<SubProps <SubProps
// @ts-ignore // @ts-ignore
style={{'--depth': depth}} style={{'--depth': visualIndentation}}
depth={depth} depth={visualIndentation}
lastSubIsComposite={lastSubPropIsComposite} lastSubIsComposite={lastSubPropIsComposite}
> >
{[...nonCompositeSubs, ...compositeSubs].map( {[...nonCompositeSubs, ...compositeSubs].map(
([subPropKey, subPropConfig]) => { ([subPropKey, subPropConfig]) => {
return ( return (
<DeterminePropEditor <DeterminePropEditorForDetail
key={'prop-' + subPropKey} key={'prop-' + subPropKey}
propConfig={subPropConfig} propConfig={subPropConfig}
pointerToProp={pointerToProp[subPropKey]} pointerToProp={pointerToProp[subPropKey] as Pointer<$FixMe>}
obj={obj} obj={obj}
visualIndentation={depth + 1} visualIndentation={visualIndentation + 1}
/> />
) )
}, },
@ -164,4 +130,4 @@ const CompoundPropEditor: IPropEditorFC<
) )
} }
export default CompoundPropEditor export default DetailCompoundPropEditor

View file

@ -0,0 +1,53 @@
import type {
IBasePropType,
PropTypeConfig_AllSimples,
} from '@theatre/core/propTypes'
import React from 'react'
import {useEditingToolsForSimplePropInDetailsPanel} from '@theatre/studio/propEditors/useEditingToolsForSimpleProp'
import {SingleRowPropEditor} from '@theatre/studio/panels/DetailPanel/DeterminePropEditorForDetail/SingleRowPropEditor'
import type {Pointer} from '@theatre/dataverse'
import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
import type {ISimplePropEditorReactProps} from '@theatre/studio/propEditors/simpleEditors/ISimplePropEditorReactProps'
export type IDetailSimplePropEditorProps<
TPropTypeConfig extends IBasePropType<string, any>,
> = {
propConfig: TPropTypeConfig
pointerToProp: Pointer<TPropTypeConfig['valueType']>
obj: SheetObject
visualIndentation: number
SimpleEditorComponent: React.VFC<ISimplePropEditorReactProps<TPropTypeConfig>>
}
/**
* Shown in the Object details panel, changes to this editor are usually reflected at either
* the playhead position (the `sequence.position`) or if static, the static override value.
*/
function DetailSimplePropEditor<
TPropTypeConfig extends PropTypeConfig_AllSimples,
>({
propConfig,
pointerToProp,
obj,
SimpleEditorComponent: EditorComponent,
}: IDetailSimplePropEditorProps<TPropTypeConfig>) {
const editingTools = useEditingToolsForSimplePropInDetailsPanel(
pointerToProp,
obj,
propConfig,
)
return (
<SingleRowPropEditor
{...{editingTools: editingTools, propConfig, pointerToProp}}
>
<EditorComponent
editingTools={editingTools}
propConfig={propConfig}
value={editingTools.value}
/>
</SingleRowPropEditor>
)
}
export default DetailSimplePropEditor

View file

@ -5,11 +5,11 @@ import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useCo
import useRefAndState from '@theatre/studio/utils/useRefAndState' import useRefAndState from '@theatre/studio/utils/useRefAndState'
import {last} from 'lodash-es' import {last} from 'lodash-es'
import React from 'react' import React from 'react'
import type {useEditingToolsForPrimitiveProp} from '@theatre/studio/panels/DetailPanel/propEditors/utils/useEditingToolsForPrimitiveProp' import type {useEditingToolsForSimplePropInDetailsPanel} from '@theatre/studio/propEditors/useEditingToolsForSimpleProp'
import {shadeToColor} from '@theatre/studio/panels/DetailPanel/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' import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
import {propNameTextCSS} from '@theatre/studio/propEditors/utils/propNameTextCSS'
export const indentationFormula = `calc(var(--left-pad) + var(--depth) * var(--step))` export const indentationFormula = `calc(var(--left-pad) + var(--depth) * var(--step))`
@ -39,13 +39,6 @@ export const rowBg = css`
} }
` `
export const propNameText = css`
font-weight: 300;
font-size: 11px;
color: #9a9a9a;
text-shadow: 0.5px 0.5px 2px rgba(0, 0, 0, 0.3);
`
const Row = styled.div` const Row = styled.div`
display: flex; display: flex;
height: 30px; height: 30px;
@ -83,7 +76,7 @@ const PropNameContainer = styled.div`
user-select: none; user-select: none;
cursor: default; cursor: default;
${propNameText}; ${propNameTextCSS};
&:hover { &:hover {
color: white; color: white;
} }
@ -111,13 +104,13 @@ const InputContainer = styled.div`
type ISingleRowPropEditorProps<T> = { type ISingleRowPropEditorProps<T> = {
propConfig: propTypes.PropTypeConfig propConfig: propTypes.PropTypeConfig
pointerToProp: Pointer<T> pointerToProp: Pointer<T>
stuff: ReturnType<typeof useEditingToolsForPrimitiveProp> editingTools: ReturnType<typeof useEditingToolsForSimplePropInDetailsPanel>
} }
export function SingleRowPropEditor<T>({ export function SingleRowPropEditor<T>({
propConfig, propConfig,
pointerToProp, pointerToProp,
stuff, editingTools,
children, children,
}: React.PropsWithChildren<ISingleRowPropEditorProps<T>>): React.ReactElement< }: React.PropsWithChildren<ISingleRowPropEditorProps<T>>): React.ReactElement<
any, any,
@ -129,16 +122,14 @@ export function SingleRowPropEditor<T>({
useRefAndState<HTMLDivElement | null>(null) useRefAndState<HTMLDivElement | null>(null)
const [contextMenu] = useContextMenu(propNameContainer, { const [contextMenu] = useContextMenu(propNameContainer, {
menuItems: stuff.contextMenuItems, menuItems: editingTools.contextMenuItems,
}) })
const color = shadeToColor[stuff.shade]
return ( return (
<Row> <Row>
{contextMenu} {contextMenu}
<Left> <Left>
<ControlsContainer>{stuff.controlIndicators}</ControlsContainer> <ControlsContainer>{editingTools.controlIndicators}</ControlsContainer>
<PropNameContainer <PropNameContainer
ref={propNameContainerRef} ref={propNameContainerRef}

View file

@ -1,18 +1,18 @@
import React, {useMemo} from 'react' import React, {useMemo} from 'react'
import DeterminePropEditor from './propEditors/DeterminePropEditor'
import type SheetObject from '@theatre/core/sheetObjects/SheetObject' import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
import type {Pointer} from '@theatre/dataverse' import type {Pointer} from '@theatre/dataverse'
import type {$FixMe} from '@theatre/shared/utils/types' import type {$FixMe} from '@theatre/shared/utils/types'
import DeterminePropEditorForDetail from './DeterminePropEditorForDetail'
const ObjectDetails: React.FC<{ const ObjectDetails: React.FC<{
objects: SheetObject[] /** TODO: add support for multiple objects (it would show their common props) */
objects: [SheetObject]
}> = ({objects}) => { }> = ({objects}) => {
// @todo add support for multiple objects (it would show their common props)
const obj = objects[0] const obj = objects[0]
const key = useMemo(() => JSON.stringify(obj.address), [obj]) const key = useMemo(() => JSON.stringify(obj.address), [obj])
return ( return (
<DeterminePropEditor <DeterminePropEditorForDetail
key={key} key={key}
obj={obj} obj={obj}
pointerToProp={obj.propsP as Pointer<$FixMe>} pointerToProp={obj.propsP as Pointer<$FixMe>}

View file

@ -4,9 +4,9 @@ import BasicPopover from '@theatre/studio/uiComponents/Popover/BasicPopover'
import usePopover from '@theatre/studio/uiComponents/Popover/usePopover' import usePopover from '@theatre/studio/uiComponents/Popover/usePopover'
import React, {useCallback, useState} from 'react' import React, {useCallback, useState} from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import {rowBgColor} from '@theatre/studio/panels/DetailPanel/propEditors/utils/SingleRowPropEditor'
import StateConflictRow from './StateConflictRow'
import DetailPanelButton from '@theatre/studio/uiComponents/DetailPanelButton' import DetailPanelButton from '@theatre/studio/uiComponents/DetailPanelButton'
import {rowBgColor} from './DeterminePropEditorForDetail/SingleRowPropEditor'
import StateConflictRow from './ProjectDetails/StateConflictRow'
const Container = styled.div` const Container = styled.div`
background-color: ${rowBgColor}; background-color: ${rowBgColor};
@ -59,7 +59,7 @@ const ProjectDetails: React.FC<{
}, []) }, [])
const [tooltip, openExportTooltip] = usePopover( const [tooltip, openExportTooltip] = usePopover(
{pointerDistanceThreshold: 50}, {debugName: 'ProjectDetails', pointerDistanceThreshold: 50},
() => ( () => (
<ExportTooltip> <ExportTooltip>
This will create a JSON file with the state of your project. You can This will create a JSON file with the state of your project. You can

View file

@ -1,38 +0,0 @@
import type {PropTypeConfig_Boolean} from '@theatre/core/propTypes'
import React, {useCallback} from 'react'
import {useEditingToolsForPrimitiveProp} from './utils/useEditingToolsForPrimitiveProp'
import {SingleRowPropEditor} from './utils/SingleRowPropEditor'
import styled from 'styled-components'
import BasicCheckbox from '@theatre/studio/uiComponents/form/BasicCheckbox'
import type {IPropEditorFC} from './utils/IPropEditorFC'
const Input = styled(BasicCheckbox)`
margin-left: 6px;
`
const BooleanPropEditor: IPropEditorFC<PropTypeConfig_Boolean> = ({
propConfig,
pointerToProp,
obj,
}) => {
const stuff = useEditingToolsForPrimitiveProp<boolean>(
pointerToProp,
obj,
propConfig,
)
const onChange = useCallback(
(el: React.ChangeEvent<HTMLInputElement>) => {
stuff.permanentlySetValue(Boolean(el.target.checked))
},
[propConfig, pointerToProp, obj],
)
return (
<SingleRowPropEditor {...{stuff, propConfig, pointerToProp}}>
<Input checked={stuff.value} onChange={onChange} />
</SingleRowPropEditor>
)
}
export default BooleanPropEditor

View file

@ -1,115 +0,0 @@
import type {PropTypeConfig} from '@theatre/core/propTypes'
import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
import {getPointerParts} from '@theatre/dataverse'
import type {Pointer} from '@theatre/dataverse'
import React from 'react'
import BooleanPropEditor from './BooleanPropEditor'
import CompoundPropEditor from './CompoundPropEditor'
import NumberPropEditor from './NumberPropEditor'
import StringLiteralPropEditor from './StringLiteralPropEditor'
import StringPropEditor from './StringPropEditor'
import RgbaPropEditor from './RgbaPropEditor'
import type {UnknownShorthandCompoundProps} from '@theatre/core/propTypes/internals'
/**
* Returns the PropTypeConfig by path. Assumes `path` is a valid prop path and that
* it exists in obj.
*/
export function getPropTypeByPointer<
Props extends UnknownShorthandCompoundProps,
>(pointerToProp: SheetObject['propsP'], obj: SheetObject): PropTypeConfig {
const rootConf = obj.template.config
const p = getPointerParts(pointerToProp).path
let conf = rootConf as PropTypeConfig
while (p.length !== 0) {
const key = p.shift()
if (typeof key === 'string') {
if (conf.type === 'compound') {
conf = conf.props[key]
if (!conf) {
throw new Error(
`getPropTypeConfigByPath() is called with an invalid path.`,
)
}
} else if (conf.type === 'enum') {
conf = conf.cases[key]
if (!conf) {
throw new Error(
`getPropTypeConfigByPath() is called with an invalid path.`,
)
}
} else {
throw new Error(
`getPropTypeConfigByPath() is called with an invalid path.`,
)
}
} else if (typeof key === 'number') {
throw new Error(`Number indexes are not implemented yet. @todo`)
} else {
throw new Error(
`getPropTypeConfigByPath() is called with an invalid path.`,
)
}
}
return conf
}
type PropConfigByType<K extends PropTypeConfig['type']> = Extract<
PropTypeConfig,
{type: K}
>
type IPropEditorByPropType = {
[K in PropTypeConfig['type']]: React.FC<{
obj: SheetObject
pointerToProp: Pointer<PropConfigByType<K>['valueType']>
propConfig: PropConfigByType<K>
visualIndentation: number
}>
}
const propEditorByPropType: IPropEditorByPropType = {
compound: CompoundPropEditor,
number: NumberPropEditor,
string: StringPropEditor,
enum: () => <></>,
boolean: BooleanPropEditor,
stringLiteral: StringLiteralPropEditor,
rgba: RgbaPropEditor,
}
export type IEditablePropertyProps<K extends PropTypeConfig['type']> = {
obj: SheetObject
pointerToProp: Pointer<PropConfigByType<K>['valueType']>
propConfig: PropConfigByType<K>
}
type IDeterminePropEditorProps<K extends PropTypeConfig['type']> =
IEditablePropertyProps<K> & {
visualIndentation: number
}
const DeterminePropEditor: React.FC<
IDeterminePropEditorProps<PropTypeConfig['type']>
> = (p) => {
const propConfig =
p.propConfig ?? getPropTypeByPointer(p.pointerToProp, p.obj)
const PropEditor = propEditorByPropType[propConfig.type]
return (
<PropEditor
obj={p.obj}
visualIndentation={p.visualIndentation}
// @ts-expect-error This is fine
pointerToProp={p.pointerToProp}
// @ts-expect-error This is fine
propConfig={propConfig}
/>
)
}
export default DeterminePropEditor

View file

@ -1,40 +0,0 @@
import type {PropTypeConfig_Number} from '@theatre/core/propTypes'
import BasicNumberInput from '@theatre/studio/uiComponents/form/BasicNumberInput'
import React, {useCallback} from 'react'
import {useEditingToolsForPrimitiveProp} from './utils/useEditingToolsForPrimitiveProp'
import {SingleRowPropEditor} from './utils/SingleRowPropEditor'
import type {IPropEditorFC} from './utils/IPropEditorFC'
const NumberPropEditor: IPropEditorFC<PropTypeConfig_Number> = ({
propConfig,
pointerToProp,
obj,
}) => {
const stuff = useEditingToolsForPrimitiveProp<number>(
pointerToProp,
obj,
propConfig,
)
const nudge = useCallback(
(params: {deltaX: number; deltaFraction: number; magnitude: number}) => {
return propConfig.nudgeFn({...params, config: propConfig})
},
[propConfig],
)
return (
<SingleRowPropEditor {...{stuff, propConfig, pointerToProp}}>
<BasicNumberInput
value={stuff.value}
temporarilySetValue={stuff.temporarilySetValue}
discardTemporaryValue={stuff.discardTemporaryValue}
permanentlySetValue={stuff.permanentlySetValue}
range={propConfig.range}
nudge={nudge}
/>
</SingleRowPropEditor>
)
}
export default NumberPropEditor

View file

@ -1,45 +0,0 @@
import type {PropTypeConfig_StringLiteral} from '@theatre/core/propTypes'
import React, {useCallback} from 'react'
import {useEditingToolsForPrimitiveProp} from './utils/useEditingToolsForPrimitiveProp'
import type {$IntentionalAny} from '@theatre/shared/utils/types'
import BasicSwitch from '@theatre/studio/uiComponents/form/BasicSwitch'
import BasicSelect from '@theatre/studio/uiComponents/form/BasicSelect'
import {SingleRowPropEditor} from './utils/SingleRowPropEditor'
import type {IPropEditorFC} from './utils/IPropEditorFC'
const StringLiteralPropEditor: IPropEditorFC<
PropTypeConfig_StringLiteral<$IntentionalAny>
> = ({propConfig, pointerToProp, obj}) => {
const stuff = useEditingToolsForPrimitiveProp<string>(
pointerToProp,
obj,
propConfig,
)
const onChange = useCallback(
(val: string) => {
stuff.permanentlySetValue(val)
},
[propConfig, pointerToProp, obj],
)
return (
<SingleRowPropEditor {...{stuff, propConfig, pointerToProp}}>
{propConfig.as === 'menu' ? (
<BasicSelect
value={stuff.value}
onChange={onChange}
options={propConfig.valuesAndLabels}
/>
) : (
<BasicSwitch
value={stuff.value}
onChange={onChange}
options={propConfig.valuesAndLabels}
/>
)}
</SingleRowPropEditor>
)
}
export default StringLiteralPropEditor

View file

@ -1,31 +0,0 @@
import React from 'react'
import type {PropTypeConfig_String} from '@theatre/core/propTypes'
import {useEditingToolsForPrimitiveProp} from './utils/useEditingToolsForPrimitiveProp'
import {SingleRowPropEditor} from './utils/SingleRowPropEditor'
import BasicStringInput from '@theatre/studio/uiComponents/form/BasicStringInput'
import type {IPropEditorFC} from './utils/IPropEditorFC'
const StringPropEditor: IPropEditorFC<PropTypeConfig_String> = ({
propConfig,
pointerToProp,
obj,
}) => {
const stuff = useEditingToolsForPrimitiveProp<string>(
pointerToProp,
obj,
propConfig,
)
return (
<SingleRowPropEditor {...{stuff, propConfig, pointerToProp}}>
<BasicStringInput
value={stuff.value}
temporarilySetValue={stuff.temporarilySetValue}
discardTemporaryValue={stuff.discardTemporaryValue}
permanentlySetValue={stuff.permanentlySetValue}
/>
</SingleRowPropEditor>
)
}
export default StringPropEditor

View file

@ -1,12 +0,0 @@
import type {IBasePropType} from '@theatre/core/propTypes'
import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
import type {Pointer} from '@theatre/dataverse'
/** Helper for defining consistent prop editor components */
export type IPropEditorFC<TPropTypeConfig extends IBasePropType<string, any>> =
React.FC<{
propConfig: TPropTypeConfig
pointerToProp: Pointer<TPropTypeConfig['valueType']>
obj: SheetObject
visualIndentation: number
}>

View file

@ -4,7 +4,7 @@ import type {VoidFn} from '@theatre/shared/utils/types'
import React from 'react' import React from 'react'
import {HiOutlineChevronRight} from 'react-icons/all' import {HiOutlineChevronRight} from 'react-icons/all'
import styled from 'styled-components' import styled from 'styled-components'
import {propNameText} from '@theatre/studio/panels/DetailPanel/propEditors/utils/SingleRowPropEditor' import {propNameTextCSS} from '@theatre/studio/propEditors/utils/propNameTextCSS'
export const Container = styled.li<{depth: number}>` export const Container = styled.li<{depth: number}>`
--depth: ${(props) => props.depth}; --depth: ${(props) => props.depth};
@ -33,7 +33,7 @@ const Header = styled(BaseHeader)<{
` `
const Head_Label = styled.span` const Head_Label = styled.span`
${propNameText}; ${propNameTextCSS};
overflow-x: hidden; overflow-x: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;

View file

@ -8,11 +8,11 @@ import type {Pointer} from '@theatre/dataverse'
import {val} from '@theatre/dataverse' import {val} from '@theatre/dataverse'
import React, {useCallback, useRef} from 'react' import React, {useCallback, useRef} from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import {useEditingToolsForPrimitiveProp} from '@theatre/studio/panels/DetailPanel/propEditors/utils/useEditingToolsForPrimitiveProp' import {useEditingToolsForSimplePropInDetailsPanel} from '@theatre/studio/propEditors/useEditingToolsForSimpleProp'
import {nextPrevCursorsTheme} from '@theatre/studio/panels/DetailPanel/propEditors/utils/NextPrevKeyframeCursors' import {nextPrevCursorsTheme} from '@theatre/studio/propEditors/NextPrevKeyframeCursors'
import {graphEditorColors} from '@theatre/studio/panels/SequenceEditorPanel/GraphEditor/GraphEditor' import {graphEditorColors} from '@theatre/studio/panels/SequenceEditorPanel/GraphEditor/GraphEditor'
import {BaseHeader, Container as BaseContainer} from './AnyCompositeRow' import {BaseHeader, Container as BaseContainer} from './AnyCompositeRow'
import {propNameText} from '@theatre/studio/panels/DetailPanel/propEditors/utils/SingleRowPropEditor' import {propNameTextCSS} from '@theatre/studio/propEditors/utils/propNameTextCSS'
const theme = { const theme = {
label: { label: {
@ -75,7 +75,7 @@ const GraphIcon = () => (
const Head_Label = styled.span` const Head_Label = styled.span`
margin-right: 4px; margin-right: 4px;
${propNameText}; ${propNameTextCSS};
` `
const PrimitivePropRow: React.FC<{ const PrimitivePropRow: React.FC<{
@ -87,7 +87,7 @@ const PrimitivePropRow: React.FC<{
) as Pointer<$IntentionalAny> ) as Pointer<$IntentionalAny>
const obj = leaf.sheetObject const obj = leaf.sheetObject
const {controlIndicators} = useEditingToolsForPrimitiveProp( const {controlIndicators} = useEditingToolsForSimplePropInDetailsPanel(
pointerToProp, pointerToProp,
obj, obj,
leaf.propConf, leaf.propConf,

View file

@ -94,6 +94,7 @@ const Connector: React.FC<IProps> = (props) => {
const rightDims = val(props.layoutP.rightDims) const rightDims = val(props.layoutP.rightDims)
const [popoverNode, openPopover, closePopover, isPopoverOpen] = usePopover( const [popoverNode, openPopover, closePopover, isPopoverOpen] = usePopover(
{ {
debugName: 'Connector',
closeWhenPointerIsDistant: !isPointerBeingCaptured(), closeWhenPointerIsDistant: !isPointerBeingCaptured(),
constraints: { constraints: {
minX: rightDims.screenX + POPOVER_MARGIN, minX: rightDims.screenX + POPOVER_MARGIN,
@ -130,9 +131,10 @@ const Connector: React.FC<IProps> = (props) => {
{...themeValues} {...themeValues}
ref={nodeRef} ref={nodeRef}
style={{ style={{
transform: `scale3d(calc(var(--unitSpaceToScaledSpaceMultiplier) * ${ // Previously we used scale3d, which had weird fuzzy rendering look in both FF & Chrome
transform: `scaleX(calc(var(--unitSpaceToScaledSpaceMultiplier) * ${
connectorLengthInUnitSpace / CONNECTOR_WIDTH_UNSCALED connectorLengthInUnitSpace / CONNECTOR_WIDTH_UNSCALED
}), 1, 1)`, }))`,
}} }}
onClick={(e) => { onClick={(e) => {
if (node) openPopover(e, node) if (node) openPopover(e, node)

View file

@ -126,7 +126,7 @@ type IProps = {
/** /**
* Called when user hits enter/escape * Called when user hits enter/escape
*/ */
onRequestClose: () => void onRequestClose: (reason: string) => void
} & Parameters<typeof KeyframeEditor>[0] } & Parameters<typeof KeyframeEditor>[0]
const CurveEditorPopover: React.FC<IProps> = (props) => { const CurveEditorPopover: React.FC<IProps> = (props) => {
@ -188,9 +188,9 @@ const CurveEditorPopover: React.FC<IProps> = (props) => {
optionsRef.current[displayedPresets[0].label]?.current?.focus() optionsRef.current[displayedPresets[0].label]?.current?.focus()
} else if (e.key === 'Escape') { } else if (e.key === 'Escape') {
discardTempValue(tempTransaction) discardTempValue(tempTransaction)
props.onRequestClose() props.onRequestClose('key Escape')
} else if (e.key === 'Enter') { } else if (e.key === 'Enter') {
props.onRequestClose() props.onRequestClose('key Enter')
} }
} }
@ -255,10 +255,10 @@ const CurveEditorPopover: React.FC<IProps> = (props) => {
const onEasingOptionKeydown = (e: KeyboardEvent<HTMLInputElement>) => { const onEasingOptionKeydown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Escape') { if (e.key === 'Escape') {
discardTempValue(tempTransaction) discardTempValue(tempTransaction)
props.onRequestClose() props.onRequestClose('key Escape')
e.stopPropagation() e.stopPropagation()
} else if (e.key === 'Enter') { } else if (e.key === 'Enter') {
props.onRequestClose() props.onRequestClose('key Enter')
e.stopPropagation() e.stopPropagation()
} }
} }
@ -267,7 +267,7 @@ const CurveEditorPopover: React.FC<IProps> = (props) => {
const onEasingOptionMouseOut = () => setPreview(null) const onEasingOptionMouseOut = () => setPreview(null)
const onSelectEasingOption = (item: {label: string; value: string}) => { const onSelectEasingOption = (item: {label: string; value: string}) => {
setTempValue(tempTransaction, props, cur, next, item.value) setTempValue(tempTransaction, props, cur, next, item.value)
props.onRequestClose() props.onRequestClose('selected easing option')
return Outcome.Handled return Outcome.Handled
} }

View file

@ -0,0 +1,84 @@
import React from 'react'
import styled from 'styled-components'
import type {
PropTypeConfig,
PropTypeConfig_AllSimples,
} from '@theatre/core/propTypes'
import type {IEditingTools} from '@theatre/studio/propEditors/utils/IEditingTools'
import type {PropConfigForType} from '@theatre/studio/propEditors/utils/PropConfigForType'
import type {ISimplePropEditorReactProps} from '@theatre/studio/propEditors/simpleEditors/ISimplePropEditorReactProps'
import {simplePropEditorByPropType} from '@theatre/studio/propEditors/simpleEditors/simplePropEditorByPropType'
import KeyframeSimplePropEditor from './DeterminePropEditorForKeyframe/KeyframeSimplePropEditor'
type IDeterminePropEditorForKeyframeProps<K extends PropTypeConfig['type']> = {
editingTools: IEditingTools<PropConfigForType<K>['valueType']>
propConfig: PropConfigForType<K>
keyframeValue: PropConfigForType<K>['valueType']
displayLabel?: string
}
const KeyframePropEditorContainer = styled.div`
padding: 2px;
display: flex;
align-items: stretch;
select {
min-width: 100px;
}
`
const KeyframePropLabel = styled.span`
font-style: normal;
font-weight: 400;
font-size: 11px;
line-height: 13px;
letter-spacing: 0.01em;
margin-right: 12px;
padding: 8px;
color: #919191;
`
/**
* Given a propConfig, this function gives the corresponding prop editor for
* use in the dope sheet inline prop editor on a keyframe.
* {@link DetailDeterminePropEditor} does the same thing for the details panel. The main difference
* between this function and {@link DetailDeterminePropEditor} is that this
* one shows prop editors *without* keyframe navigation controls (that look
* like `< ・ >`).
*
* @param p - propConfig object for any type of prop.
*/
export function DeterminePropEditorForKeyframe(
p: IDeterminePropEditorForKeyframeProps<PropTypeConfig['type']>,
) {
const propConfig = p.propConfig
if (propConfig.type === 'compound') {
throw new Error(
'We do not yet support editing compound props for a keyframe',
)
} else if (propConfig.type === 'enum') {
// notice: enums are not implemented, yet.
return <></>
} else {
const PropEditor = simplePropEditorByPropType[propConfig.type]
return (
<KeyframePropEditorContainer>
<KeyframePropLabel>{p.displayLabel}</KeyframePropLabel>
<KeyframeSimplePropEditor
SimpleEditorComponent={
PropEditor as React.VFC<
ISimplePropEditorReactProps<PropTypeConfig_AllSimples>
>
}
propConfig={propConfig}
editingTools={p.editingTools}
keyframeValue={p.keyframeValue}
/>
</KeyframePropEditorContainer>
)
}
}

View file

@ -0,0 +1,45 @@
import React from 'react'
import type {ISimplePropEditorReactProps} from '@theatre/studio/propEditors/simpleEditors/ISimplePropEditorReactProps'
import styled from 'styled-components'
import type {PropTypeConfig_AllSimples} from '@theatre/core/propTypes'
import type {IEditingTools} from '@theatre/studio/propEditors/utils/IEditingTools'
export type IKeyframeSimplePropEditorProps<
TPropTypeConfig extends PropTypeConfig_AllSimples,
> = {
propConfig: TPropTypeConfig
editingTools: IEditingTools<TPropTypeConfig['valueType']>
keyframeValue: TPropTypeConfig['valueType']
SimpleEditorComponent: React.VFC<ISimplePropEditorReactProps<TPropTypeConfig>>
}
const KeyframeSimplePropEditorContainer = styled.div`
padding: 0 6px;
display: flex;
align-items: center;
`
/**
* Initially used for inline keyframe property editor, this editor is attached to the
* functionality of editing a property for a sequence keyframe.
*/
function KeyframeSimplePropEditor<
TPropTypeConfig extends PropTypeConfig_AllSimples,
>({
propConfig,
editingTools,
keyframeValue: value,
SimpleEditorComponent: EditorComponent,
}: IKeyframeSimplePropEditorProps<TPropTypeConfig>) {
return (
<KeyframeSimplePropEditorContainer>
<EditorComponent
editingTools={editingTools}
propConfig={propConfig}
value={value}
/>
</KeyframeSimplePropEditorContainer>
)
}
export default KeyframeSimplePropEditor

View file

@ -1,3 +1,8 @@
import {lighten} from 'polished'
import React, {useMemo, useRef} from 'react'
import styled from 'styled-components'
import last from 'lodash-es/last'
import getStudio from '@theatre/studio/getStudio' import getStudio from '@theatre/studio/getStudio'
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore' import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu' import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
@ -6,11 +11,10 @@ import useDrag from '@theatre/studio/uiComponents/useDrag'
import type {UseDragOpts} from '@theatre/studio/uiComponents/useDrag' import type {UseDragOpts} from '@theatre/studio/uiComponents/useDrag'
import useRefAndState from '@theatre/studio/utils/useRefAndState' import useRefAndState from '@theatre/studio/utils/useRefAndState'
import {val} from '@theatre/dataverse' import {val} from '@theatre/dataverse'
import {lighten} from 'polished' import {
import React, {useMemo, useRef} from 'react' includeLockFrameStampAttrs,
import styled from 'styled-components' useLockFrameStampPosition,
import {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider' } from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
import {includeLockFrameStampAttrs} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
import { import {
lockedCursorCssVarName, lockedCursorCssVarName,
useCssCursorLock, useCssCursorLock,
@ -19,6 +23,11 @@ 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'
import type {IKeyframeEditorProps} from './KeyframeEditor' import type {IKeyframeEditorProps} from './KeyframeEditor'
import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap' import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap'
import usePopover from '@theatre/studio/uiComponents/Popover/usePopover'
import BasicPopover from '@theatre/studio/uiComponents/Popover/BasicPopover'
import {useTempTransactionEditingTools} from './useTempTransactionEditingTools'
import {DeterminePropEditorForKeyframe} from './DeterminePropEditorForKeyframe'
export const DOT_SIZE_PX = 6 export const DOT_SIZE_PX = 6
const HIT_ZONE_SIZE_PX = 12 const HIT_ZONE_SIZE_PX = 12
@ -96,8 +105,14 @@ type IKeyframeDotProps = IKeyframeEditorProps
const KeyframeDot: React.VFC<IKeyframeDotProps> = (props) => { const KeyframeDot: React.VFC<IKeyframeDotProps> = (props) => {
const [ref, node] = useRefAndState<HTMLDivElement | null>(null) const [ref, node] = useRefAndState<HTMLDivElement | null>(null)
const [isDragging] = useDragKeyframe(node, props)
const [contextMenu] = useKeyframeContextMenu(node, props) const [contextMenu] = useKeyframeContextMenu(node, props)
const [inlineEditorPopover, openEditor] =
useKeyframeInlineEditorPopover(props)
const [isDragging] = useDragForKeyframeDot(node, props, {
onClickFromDrag(dragStartEvent) {
openEditor(dragStartEvent, ref.current!)
},
})
return ( return (
<> <>
@ -108,6 +123,7 @@ const KeyframeDot: React.VFC<IKeyframeDotProps> = (props) => {
className={isDragging ? 'beingDragged' : ''} className={isDragging ? 'beingDragged' : ''}
/> />
<Diamond isSelected={!!props.selection} /> <Diamond isSelected={!!props.selection} />
{inlineEditorPopover}
{contextMenu} {contextMenu}
</> </>
) )
@ -136,9 +152,47 @@ function useKeyframeContextMenu(
}) })
} }
function useDragKeyframe( /** The editor that pops up when directly clicking a Keyframe. */
function useKeyframeInlineEditorPopover(props: IKeyframeDotProps) {
const editingTools = useEditingToolsForKeyframeEditorPopover(props)
const label = props.leaf.propConf.label ?? last(props.leaf.pathToProp)
return usePopover({debugName: 'useKeyframeInlineEditorPopover'}, () => (
<BasicPopover showPopoverEdgeTriangle>
<DeterminePropEditorForKeyframe
propConfig={props.leaf.propConf}
editingTools={editingTools}
keyframeValue={props.keyframe.value}
displayLabel={label != null ? String(label) : undefined}
/>
</BasicPopover>
))
}
function useEditingToolsForKeyframeEditorPopover(props: IKeyframeDotProps) {
const obj = props.leaf.sheetObject
return useTempTransactionEditingTools(({stateEditors}, value) => {
const newKeyframe = {...props.keyframe, value}
stateEditors.coreByProject.historic.sheetsById.sequence.replaceKeyframes({
...obj.address,
trackId: props.leaf.trackId,
keyframes: [newKeyframe],
snappingFunction: obj.sheet.getSequence().closestGridPosition,
})
})
}
function useDragForKeyframeDot(
node: HTMLDivElement | null, node: HTMLDivElement | null,
props: IKeyframeDotProps, props: IKeyframeDotProps,
options: {
/**
* hmm: this is a hack so we can actually receive the
* {@link MouseEvent} from the drag event handler and use
* it for positioning the popup.
*/
onClickFromDrag(dragStartEvent: MouseEvent): void
},
): [isDragging: boolean] { ): [isDragging: boolean] {
const propsRef = useRef(props) const propsRef = useRef(props)
propsRef.current = props propsRef.current = props
@ -146,7 +200,6 @@ function useDragKeyframe(
const useDragOpts = useMemo<UseDragOpts>(() => { const useDragOpts = useMemo<UseDragOpts>(() => {
return { return {
debugName: 'KeyframeDot/useDragKeyframe', debugName: 'KeyframeDot/useDragKeyframe',
onDragStart(event) { onDragStart(event) {
const props = propsRef.current const props = propsRef.current
if (props.selection) { if (props.selection) {
@ -203,8 +256,12 @@ function useDragKeyframe(
}) })
}, },
onDragEnd(dragHappened) { onDragEnd(dragHappened) {
if (dragHappened) tempTransaction?.commit() if (dragHappened) {
else tempTransaction?.discard() tempTransaction?.commit()
} else {
tempTransaction?.discard()
options.onClickFromDrag(event)
}
}, },
} }
}, },
@ -247,7 +304,7 @@ function copyKeyFrameContextMenuItem(
keyframeIds: string[], keyframeIds: string[],
): IContextMenuItem { ): IContextMenuItem {
return { return {
label: keyframeIds.length > 1 ? 'Copy selection' : 'Copy keyframe', label: keyframeIds.length > 1 ? 'Copy Selection' : 'Copy Keyframe',
callback: () => { callback: () => {
const keyframes = keyframeIds.map( const keyframes = keyframeIds.map(
(keyframeId) => (keyframeId) =>

View file

@ -0,0 +1,51 @@
import getStudio from '@theatre/studio/getStudio'
import type {SerializableValue} from '@theatre/shared/utils/types'
import type {
CommitOrDiscard,
ITransactionPrivateApi,
} from '@theatre/studio/StudioStore/StudioStore'
import type {IEditingTools} from '@theatre/studio/propEditors/utils/IEditingTools'
import {useMemo} from 'react'
/**
* This function takes a function `writeTx` that sets a value in the private Studio API and
* returns a memoized editingTools object which contains three functions:
* - `temporarilySetValue` - uses `writeTx` to set a value that can be discarded
* - `discardTemporaryValue` - if `temporarilySetValue` was called, discards the value it set
* - `permanentlySetValue` - uses `writeTx` to set a value
*
* @param writeTx - a function that uses a value to perform an action using the
* private Studio API.
* @returns an editingTools object that can be passed to `DeterminePropEditorForKeyframe` or
* `DetailDeterminePropEditor` and is used by the prop editors in `simplePropEditorByPropType`.
*/
export function useTempTransactionEditingTools<T extends SerializableValue>(
writeTx: (api: ITransactionPrivateApi, value: T) => void,
): IEditingTools<T> {
return useMemo(() => createTempTransactionEditingTools<T>(writeTx), [])
}
function createTempTransactionEditingTools<T>(
writeTx: (api: ITransactionPrivateApi, value: T) => void,
) {
let currentTransaction: CommitOrDiscard | null = null
const createTempTx = (value: T) =>
getStudio().tempTransaction((api) => writeTx(api, value))
function discardTemporaryValue() {
currentTransaction?.discard()
currentTransaction = null
}
return {
temporarilySetValue(value: T): void {
discardTemporaryValue()
currentTransaction = createTempTx(value)
},
discardTemporaryValue,
permanentlySetValue(value: T): void {
discardTemporaryValue()
createTempTx(value).commit()
},
}
}

View file

@ -7,7 +7,7 @@ import getStudio from '@theatre/studio/getStudio'
import type {BasicNumberInputNudgeFn} from '@theatre/studio/uiComponents/form/BasicNumberInput' import type {BasicNumberInputNudgeFn} from '@theatre/studio/uiComponents/form/BasicNumberInput'
import BasicNumberInput from '@theatre/studio/uiComponents/form/BasicNumberInput' import BasicNumberInput from '@theatre/studio/uiComponents/form/BasicNumberInput'
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore' import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
import {propNameText} from '@theatre/studio/panels/DetailPanel/propEditors/utils/SingleRowPropEditor' import {propNameTextCSS} from '@theatre/studio/propEditors/utils/propNameTextCSS'
const greaterThanZero = (v: number) => isFinite(v) && v > 0 const greaterThanZero = (v: number) => isFinite(v) && v > 0
@ -20,7 +20,7 @@ const Container = styled.div`
` `
const Label = styled.div` const Label = styled.div`
${propNameText}; ${propNameTextCSS};
white-space: nowrap; white-space: nowrap;
` `
@ -31,7 +31,7 @@ const LengthEditorPopover: React.FC<{
/** /**
* Called when user hits enter/escape * Called when user hits enter/escape
*/ */
onRequestClose: () => void onRequestClose: (reason: string) => void
}> = ({layoutP, onRequestClose}) => { }> = ({layoutP, onRequestClose}) => {
const sheet = useVal(layoutP.sheet) const sheet = useVal(layoutP.sheet)
@ -89,7 +89,7 @@ const LengthEditorPopover: React.FC<{
{...fns} {...fns}
isValid={greaterThanZero} isValid={greaterThanZero}
inputRef={inputRef} inputRef={inputRef}
onBlur={onRequestClose} onBlur={onRequestClose.bind(null, 'length editor number input blur')}
nudge={nudge} nudge={nudge}
/> />
</Container> </Container>

View file

@ -129,6 +129,8 @@ type IProps = {
layoutP: Pointer<SequenceEditorPanelLayout> layoutP: Pointer<SequenceEditorPanelLayout>
} }
const RENDER_OUT_OF_VIEW_X = -10000
/** /**
* This appears at the end of the sequence where you can adjust the length of the sequence. * This appears at the end of the sequence where you can adjust the length of the sequence.
* Kinda looks like `< >` at the top bar at end of the sequence editor. * Kinda looks like `< >` at the top bar at end of the sequence editor.
@ -137,7 +139,7 @@ const LengthIndicator: React.FC<IProps> = ({layoutP}) => {
const [nodeRef, node] = useRefAndState<HTMLDivElement | null>(null) const [nodeRef, node] = useRefAndState<HTMLDivElement | null>(null)
const [isDragging] = useDragBulge(node, {layoutP}) const [isDragging] = useDragBulge(node, {layoutP})
const [popoverNode, openPopover, closePopover, isPopoverOpen] = usePopover( const [popoverNode, openPopover, closePopover, isPopoverOpen] = usePopover(
{}, {debugName: 'LengthIndicator'},
() => { () => {
return ( return (
<BasicPopover> <BasicPopover>
@ -178,7 +180,9 @@ const LengthIndicator: React.FC<IProps> = ({layoutP}) => {
<Strip <Strip
style={{ style={{
height: height + 'px', height: height + 'px',
transform: `translateX(${translateX === 0 ? -1000 : translateX}px)`, transform: `translateX(${
translateX === 0 ? RENDER_OUT_OF_VIEW_X : translateX
}px)`,
}} }}
className={isDragging ? 'dragging' : ''} className={isDragging ? 'dragging' : ''}
> >

View file

@ -188,7 +188,7 @@ const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
const [thumbRef, thumbNode] = useRefAndState<HTMLElement | null>(null) const [thumbRef, thumbNode] = useRefAndState<HTMLElement | null>(null)
const [popoverNode, openPopover, closePopover, isPopoverOpen] = usePopover( const [popoverNode, openPopover, closePopover, isPopoverOpen] = usePopover(
{}, {debugName: 'Playhead'},
() => { () => {
return ( return (
<BasicPopover> <BasicPopover>

View file

@ -3,7 +3,7 @@ import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEdi
import {usePrism} from '@theatre/react' import {usePrism} from '@theatre/react'
import type {BasicNumberInputNudgeFn} from '@theatre/studio/uiComponents/form/BasicNumberInput' import type {BasicNumberInputNudgeFn} from '@theatre/studio/uiComponents/form/BasicNumberInput'
import BasicNumberInput from '@theatre/studio/uiComponents/form/BasicNumberInput' import BasicNumberInput from '@theatre/studio/uiComponents/form/BasicNumberInput'
import {propNameText} from '@theatre/studio/panels/DetailPanel/propEditors/utils/SingleRowPropEditor' import {propNameTextCSS} from '@theatre/studio/propEditors/utils/propNameTextCSS'
import {useLayoutEffect, useMemo, useRef} from 'react' import {useLayoutEffect, useMemo, useRef} from 'react'
import React from 'react' import React from 'react'
import {val} from '@theatre/dataverse' import {val} from '@theatre/dataverse'
@ -21,7 +21,7 @@ const Container = styled.div`
` `
const Label = styled.div` const Label = styled.div`
${propNameText}; ${propNameTextCSS};
white-space: nowrap; white-space: nowrap;
` `
@ -32,7 +32,7 @@ const PlayheadPositionPopover: React.FC<{
/** /**
* Called when user hits enter/escape * Called when user hits enter/escape
*/ */
onRequestClose: () => void onRequestClose: (reason: string) => void
}> = ({layoutP, onRequestClose}) => { }> = ({layoutP, onRequestClose}) => {
const sheet = val(layoutP.sheet) const sheet = val(layoutP.sheet)
const sequence = sheet.getSequence() const sequence = sheet.getSequence()
@ -80,7 +80,7 @@ const PlayheadPositionPopover: React.FC<{
{...fns} {...fns}
isValid={greaterThanZero} isValid={greaterThanZero}
inputRef={inputRef} inputRef={inputRef}
onBlur={onRequestClose} onBlur={onRequestClose.bind(null, 'number input blur')}
nudge={nudge} nudge={nudge}
/> />
</Container> </Container>

View file

@ -0,0 +1,26 @@
import type {PropTypeConfig_Boolean} from '@theatre/core/propTypes'
import React, {useCallback} from 'react'
import styled from 'styled-components'
import BasicCheckbox from '@theatre/studio/uiComponents/form/BasicCheckbox'
import type {ISimplePropEditorReactProps} from './ISimplePropEditorReactProps'
const Input = styled(BasicCheckbox)`
margin-left: 6px;
`
function BooleanPropEditor({
propConfig,
editingTools,
value,
}: ISimplePropEditorReactProps<PropTypeConfig_Boolean>) {
const onChange = useCallback(
(el: React.ChangeEvent<HTMLInputElement>) => {
editingTools.permanentlySetValue(Boolean(el.target.checked))
},
[propConfig, editingTools],
)
return <Input checked={value} onChange={onChange} />
}
export default BooleanPropEditor

View file

@ -0,0 +1,11 @@
import type {IBasePropType} from '@theatre/core/propTypes'
import type {IEditingTools} from '@theatre/studio/propEditors/utils/IEditingTools'
/** Helper for defining consistent prop editor components */
export type ISimplePropEditorReactProps<
TPropTypeConfig extends IBasePropType<string, any>,
> = {
propConfig: TPropTypeConfig
editingTools: IEditingTools<TPropTypeConfig['valueType']>
value: TPropTypeConfig['valueType']
}

View file

@ -0,0 +1,30 @@
import type {PropTypeConfig_Number} from '@theatre/core/propTypes'
import BasicNumberInput from '@theatre/studio/uiComponents/form/BasicNumberInput'
import React, {useCallback} from 'react'
import type {ISimplePropEditorReactProps} from './ISimplePropEditorReactProps'
function NumberPropEditor({
propConfig,
editingTools,
value,
}: ISimplePropEditorReactProps<PropTypeConfig_Number>) {
const nudge = useCallback(
(params: {deltaX: number; deltaFraction: number; magnitude: number}) => {
return propConfig.nudgeFn({...params, config: propConfig})
},
[propConfig],
)
return (
<BasicNumberInput
value={value}
temporarilySetValue={editingTools.temporarilySetValue}
discardTemporaryValue={editingTools.discardTemporaryValue}
permanentlySetValue={editingTools.permanentlySetValue}
range={propConfig.range}
nudge={nudge}
/>
)
}
export default NumberPropEditor

View file

@ -6,14 +6,12 @@ import {
parseRgbaFromHex, parseRgbaFromHex,
} from '@theatre/shared/utils/color' } from '@theatre/shared/utils/color'
import React, {useCallback, useRef} from 'react' import React, {useCallback, useRef} from 'react'
import {useEditingToolsForPrimitiveProp} from './utils/useEditingToolsForPrimitiveProp'
import {SingleRowPropEditor} from './utils/SingleRowPropEditor'
import {RgbaColorPicker} from '@theatre/studio/uiComponents/colorPicker' import {RgbaColorPicker} from '@theatre/studio/uiComponents/colorPicker'
import styled from 'styled-components' import styled from 'styled-components'
import usePopover from '@theatre/studio/uiComponents/Popover/usePopover' import usePopover from '@theatre/studio/uiComponents/Popover/usePopover'
import BasicStringInput from '@theatre/studio/uiComponents/form/BasicStringInput' import BasicStringInput from '@theatre/studio/uiComponents/form/BasicStringInput'
import {popoverBackgroundColor} from '@theatre/studio/uiComponents/Popover/BasicPopover' import {popoverBackgroundColor} from '@theatre/studio/uiComponents/Popover/BasicPopover'
import type {IPropEditorFC} from './utils/IPropEditorFC' import type {ISimplePropEditorReactProps} from './ISimplePropEditorReactProps'
const RowContainer = styled.div` const RowContainer = styled.div`
display: flex; display: flex;
@ -22,16 +20,18 @@ const RowContainer = styled.div`
gap: 4px; gap: 4px;
` `
interface PuckProps { interface ColorPreviewPuckProps {
background: Rgba rgbaColor: Rgba
} }
const Puck = styled.div.attrs<PuckProps>((props) => ({ const ColorPreviewPuck = styled.div.attrs<ColorPreviewPuckProps>((props) => ({
style: { style: {
background: props.background, // weirdly, rgba2hex is needed to ensure initial render was correct background?
// huge head scratcher.
background: rgba2hex(props.rgbaColor),
}, },
}))<PuckProps>` }))<ColorPreviewPuckProps>`
height: calc(100% - 12px); height: 18px;
aspect-ratio: 1; aspect-ratio: 1;
border-radius: 2px; border-radius: 2px;
` `
@ -42,7 +42,7 @@ const HexInput = styled(BasicStringInput)`
const noop = () => {} const noop = () => {}
const Popover = styled.div` const RgbaPopover = styled.div`
position: absolute; position: absolute;
background-color: ${popoverBackgroundColor}; background-color: ${popoverBackgroundColor};
color: white; color: white;
@ -60,60 +60,58 @@ const Popover = styled.div`
box-shadow: none; box-shadow: none;
` `
const RgbaPropEditor: IPropEditorFC<PropTypeConfig_Rgba> = ({ function RgbaPropEditor({
propConfig, editingTools,
pointerToProp, value,
obj, }: ISimplePropEditorReactProps<PropTypeConfig_Rgba>) {
}) => {
const containerRef = useRef<HTMLDivElement>(null!) const containerRef = useRef<HTMLDivElement>(null!)
const stuff = useEditingToolsForPrimitiveProp(pointerToProp, obj, propConfig)
const onChange = useCallback( const onChange = useCallback(
(color: string) => { (color: string) => {
const rgba = decorateRgba(parseRgbaFromHex(color)) const rgba = decorateRgba(parseRgbaFromHex(color))
stuff.permanentlySetValue(rgba) editingTools.permanentlySetValue(rgba)
}, },
[stuff], [editingTools],
) )
const [popoverNode, openPopover] = usePopover({}, () => { const [popoverNode, openPopover] = usePopover(
return ( {debugName: 'RgbaPropEditor'},
<Popover> () => (
<RgbaPopover>
<RgbaColorPicker <RgbaColorPicker
color={{ color={{
r: stuff.value.r, r: value.r,
g: stuff.value.g, g: value.g,
b: stuff.value.b, b: value.b,
a: stuff.value.a, a: value.a,
}} }}
temporarilySetValue={(color) => { temporarilySetValue={(color) => {
const rgba = decorateRgba(color) const rgba = decorateRgba(color)
stuff.temporarilySetValue(rgba) editingTools.temporarilySetValue(rgba)
}} }}
permanentlySetValue={(color) => { permanentlySetValue={(color) => {
// console.log('perm') // console.log('perm')
const rgba = decorateRgba(color) const rgba = decorateRgba(color)
stuff.permanentlySetValue(rgba) editingTools.permanentlySetValue(rgba)
}} }}
discardTemporaryValue={stuff.discardTemporaryValue} discardTemporaryValue={editingTools.discardTemporaryValue}
/> />
</Popover> </RgbaPopover>
),
) )
})
return ( return (
<SingleRowPropEditor {...{stuff, propConfig, pointerToProp}}> <>
<RowContainer> <RowContainer>
<Puck <ColorPreviewPuck
background={stuff.value} rgbaColor={value}
ref={containerRef} ref={containerRef}
onClick={(e) => { onClick={(e) => {
openPopover(e, containerRef.current) openPopover(e, containerRef.current)
}} }}
/> />
<HexInput <HexInput
value={rgba2hex(stuff.value)} value={rgba2hex(value)}
temporarilySetValue={noop} temporarilySetValue={noop}
discardTemporaryValue={noop} discardTemporaryValue={noop}
permanentlySetValue={onChange} permanentlySetValue={onChange}
@ -121,7 +119,7 @@ const RgbaPropEditor: IPropEditorFC<PropTypeConfig_Rgba> = ({
/> />
</RowContainer> </RowContainer>
{popoverNode} {popoverNode}
</SingleRowPropEditor> </>
) )
} }

View file

@ -0,0 +1,34 @@
import type {PropTypeConfig_StringLiteral} from '@theatre/core/propTypes'
import React, {useCallback} from 'react'
import BasicSwitch from '@theatre/studio/uiComponents/form/BasicSwitch'
import BasicSelect from '@theatre/studio/uiComponents/form/BasicSelect'
import type {ISimplePropEditorReactProps} from './ISimplePropEditorReactProps'
function StringLiteralPropEditor<TLiteralOptions extends string>({
propConfig,
editingTools,
value,
}: ISimplePropEditorReactProps<PropTypeConfig_StringLiteral<TLiteralOptions>>) {
const onChange = useCallback(
(val: TLiteralOptions) => {
editingTools.permanentlySetValue(val)
},
[propConfig, editingTools],
)
return propConfig.as === 'menu' ? (
<BasicSelect
value={value}
onChange={onChange}
options={propConfig.valuesAndLabels}
/>
) : (
<BasicSwitch
value={value}
onChange={onChange}
options={propConfig.valuesAndLabels}
/>
)
}
export default StringLiteralPropEditor

View file

@ -0,0 +1,20 @@
import React from 'react'
import type {PropTypeConfig_String} from '@theatre/core/propTypes'
import BasicStringInput from '@theatre/studio/uiComponents/form/BasicStringInput'
import type {ISimplePropEditorReactProps} from './ISimplePropEditorReactProps'
function StringPropEditor({
editingTools,
value,
}: ISimplePropEditorReactProps<PropTypeConfig_String>) {
return (
<BasicStringInput
value={value}
temporarilySetValue={editingTools.temporarilySetValue}
discardTemporaryValue={editingTools.discardTemporaryValue}
permanentlySetValue={editingTools.permanentlySetValue}
/>
)
}
export default StringPropEditor

View file

@ -0,0 +1,23 @@
import type {PropTypeConfig_AllSimples} from '@theatre/core/propTypes'
import type React from 'react'
import BooleanPropEditor from './BooleanPropEditor'
import NumberPropEditor from './NumberPropEditor'
import StringLiteralPropEditor from './StringLiteralPropEditor'
import StringPropEditor from './StringPropEditor'
import RgbaPropEditor from './RgbaPropEditor'
import type {ISimplePropEditorReactProps} from './ISimplePropEditorReactProps'
import type {PropConfigForType} from '@theatre/studio/propEditors/utils/PropConfigForType'
export const simplePropEditorByPropType: ISimplePropEditorByPropType = {
number: NumberPropEditor,
string: StringPropEditor,
boolean: BooleanPropEditor,
stringLiteral: StringLiteralPropEditor,
rgba: RgbaPropEditor,
}
type ISimplePropEditorByPropType = {
[K in PropTypeConfig_AllSimples['type']]: React.VFC<
ISimplePropEditorReactProps<PropConfigForType<K>>
>
}

View file

@ -1,3 +1,9 @@
import get from 'lodash-es/get'
import last from 'lodash-es/last'
import React from 'react'
import type {Pointer} from '@theatre/dataverse'
import {getPointerParts, prism, val} from '@theatre/dataverse'
import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic' import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic'
import type SheetObject from '@theatre/core/sheetObjects/SheetObject' import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
import getStudio from '@theatre/studio/getStudio' import getStudio from '@theatre/studio/getStudio'
@ -5,22 +11,19 @@ import type Scrub from '@theatre/studio/Scrub'
import type {IContextMenuItem} from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu' import type {IContextMenuItem} from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
import getDeep from '@theatre/shared/utils/getDeep' import getDeep from '@theatre/shared/utils/getDeep'
import {usePrism} from '@theatre/react' import {usePrism} from '@theatre/react'
import type {SerializablePrimitive} from '@theatre/shared/utils/types' import type {SerializablePrimitive as SerializablePrimitive} from '@theatre/shared/utils/types'
import {getPointerParts, prism, val} from '@theatre/dataverse' import type {PropTypeConfig_AllSimples} from '@theatre/core/propTypes'
import type {Pointer} from '@theatre/dataverse'
import get from 'lodash-es/get'
import last from 'lodash-es/last'
import React from 'react'
import DefaultOrStaticValueIndicator from './DefaultValueIndicator'
import NextPrevKeyframeCursors from './NextPrevKeyframeCursors'
import type {PropTypeConfig} from '@theatre/core/propTypes'
import {isPropConfSequencable} from '@theatre/shared/propTypes/utils' import {isPropConfSequencable} from '@theatre/shared/propTypes/utils'
import type {SequenceTrackId} from '@theatre/shared/utils/ids' import type {SequenceTrackId} from '@theatre/shared/utils/ids'
interface CommonStuff<T> { import DefaultOrStaticValueIndicator from './DefaultValueIndicator'
import NextPrevKeyframeCursors from './NextPrevKeyframeCursors'
interface EditingToolsCommon<T> {
value: T value: T
beingScrubbed: boolean beingScrubbed: boolean
contextMenuItems: Array<IContextMenuItem> contextMenuItems: Array<IContextMenuItem>
/** e.g. `< • >` or `< >` for {@link EditingToolsSequenced} */
controlIndicators: React.ReactElement controlIndicators: React.ReactElement
temporarilySetValue(v: T): void temporarilySetValue(v: T): void
@ -28,38 +31,50 @@ interface CommonStuff<T> {
permanentlySetValue(v: T): void permanentlySetValue(v: T): void
} }
interface Default<T> extends CommonStuff<T> { interface EditingToolsDefault<T> extends EditingToolsCommon<T> {
type: 'Default' type: 'Default'
shade: Shade shade: Shade
} }
interface Static<T> extends CommonStuff<T> { interface EditingToolsStatic<T> extends EditingToolsCommon<T> {
type: 'Static' type: 'Static'
shade: Shade shade: Shade
} }
interface Sequenced<T> extends CommonStuff<T> { interface EditingToolsSequenced<T> extends EditingToolsCommon<T> {
type: 'Sequenced' type: 'Sequenced'
shade: Shade shade: Shade
/** based on the position of the playhead */
nearbyKeyframes: NearbyKeyframes nearbyKeyframes: NearbyKeyframes
} }
type Stuff<T> = Default<T> | Static<T> | Sequenced<T> type EditingTools<T> =
| EditingToolsDefault<T>
| EditingToolsStatic<T>
| EditingToolsSequenced<T>
export function useEditingToolsForPrimitiveProp< /**
* Notably, this uses the {@link Scrub} API to support
* indicating in the UI which pointers (values/props) are being
* scrubbed. See how impl of {@link Scrub} manages
* `state.flagsTransaction` to keep a list of these touched paths
* for the UI to be able to recognize. (e.g. to highlight the
* item in r3f as you change its scale).
*/
export function useEditingToolsForSimplePropInDetailsPanel<
T extends SerializablePrimitive, T extends SerializablePrimitive,
>( >(
pointerToProp: Pointer<T>, pointerToProp: Pointer<T>,
obj: SheetObject, obj: SheetObject,
propConfig: PropTypeConfig, propConfig: PropTypeConfig_AllSimples,
): Stuff<T> { ): EditingTools<T> {
return usePrism(() => { return usePrism(() => {
const pathToProp = getPointerParts(pointerToProp).path const pathToProp = getPointerParts(pointerToProp).path
const final = obj.getValueByPointer(pointerToProp) as T const final = obj.getValueByPointer(pointerToProp) as T
const callbacks = prism.memo( const editPropValue = prism.memo(
'callbacks', 'editPropValue',
() => { () => {
let currentScrub: Scrub | null = null let currentScrub: Scrub | null = null
@ -96,10 +111,6 @@ export function useEditingToolsForPrimitiveProp<
[], [],
) )
// const validSequenceTracks = val(
// obj.template.getMapOfValidSequenceTracks_forStudio(),
// )
const beingScrubbed = const beingScrubbed =
val( val(
get( get(
@ -114,8 +125,8 @@ export function useEditingToolsForPrimitiveProp<
const contextMenuItems: IContextMenuItem[] = [] const contextMenuItems: IContextMenuItem[] = []
const common: CommonStuff<T> = { const common: EditingToolsCommon<T> = {
...callbacks, ...editPropValue,
value: final, value: final,
beingScrubbed, beingScrubbed,
contextMenuItems, contextMenuItems,
@ -224,7 +235,7 @@ export function useEditingToolsForPrimitiveProp<
/> />
) )
const ret: Sequenced<T> = { const ret: EditingToolsSequenced<T> = {
...common, ...common,
type: 'Sequenced', type: 'Sequenced',
shade, shade,
@ -239,7 +250,7 @@ export function useEditingToolsForPrimitiveProp<
contextMenuItems.push({ contextMenuItems.push({
label: 'Reset to default', label: 'Reset to default',
callback: () => { callback: () => {
getStudio()!.transaction(({unset}) => { getStudio()!.transaction(({unset: unset}) => {
unset(pointerToProp) unset(pointerToProp)
}) })
}, },
@ -264,7 +275,7 @@ export function useEditingToolsForPrimitiveProp<
const statics = val(obj.template.getStaticValues()) const statics = val(obj.template.getStaticValues())
if (typeof getDeep(statics, pathToProp) !== 'undefined') { if (typeof getDeep(statics, pathToProp) !== 'undefined') {
const ret: Static<T> = { const ret: EditingToolsStatic<T> = {
...common, ...common,
type: 'Static', type: 'Static',
shade: common.beingScrubbed ? 'Static_BeingScrubbed' : 'Static', shade: common.beingScrubbed ? 'Static_BeingScrubbed' : 'Static',
@ -275,7 +286,7 @@ export function useEditingToolsForPrimitiveProp<
return ret return ret
} }
const ret: Default<T> = { const ret: EditingToolsDefault<T> = {
...common, ...common,
type: 'Default', type: 'Default',
shade: 'Default', shade: 'Default',

View file

@ -0,0 +1,5 @@
export interface IEditingTools<T> {
temporarilySetValue(v: T): void
discardTemporaryValue(): void
permanentlySetValue(v: T): void
}

View file

@ -0,0 +1,6 @@
import type {PropTypeConfig} from '@theatre/core/propTypes'
export type PropConfigForType<K extends PropTypeConfig['type']> = Extract<
PropTypeConfig,
{type: K}
>

View file

@ -0,0 +1,60 @@
import type {PropTypeConfig} from '@theatre/core/propTypes'
import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
import {getPointerParts} from '@theatre/dataverse'
/**
* Returns the PropTypeConfig by path. Assumes `path` is a valid prop path and that
* it exists in obj.
*
* Example usage:
* ```
* const propConfig = getPropTypeByPointer(propP, sheetObject)
*
* if (propConfig.type === 'number') {
* //... etc.
* }
* ```
*/
export function getPropTypeByPointer(
pointerToProp: SheetObject['propsP'],
obj: SheetObject,
): PropTypeConfig {
const rootConf = obj.template.config
const p = getPointerParts(pointerToProp).path
let conf = rootConf as PropTypeConfig
while (p.length !== 0) {
const key = p.shift()
if (typeof key === 'string') {
if (conf.type === 'compound') {
conf = conf.props[key]
if (!conf) {
throw new Error(
`getPropTypeConfigByPath() is called with an invalid path.`,
)
}
} else if (conf.type === 'enum') {
conf = conf.cases[key]
if (!conf) {
throw new Error(
`getPropTypeConfigByPath() is called with an invalid path.`,
)
}
} else {
throw new Error(
`getPropTypeConfigByPath() is called with an invalid path.`,
)
}
} else if (typeof key === 'number') {
throw new Error(`Number indexes are not implemented yet. @todo`)
} else {
throw new Error(
`getPropTypeConfigByPath() is called with an invalid path.`,
)
}
}
return conf
}

View file

@ -0,0 +1,8 @@
import {css} from 'styled-components'
export const propNameTextCSS = css`
font-weight: 300;
font-size: 11px;
color: #9a9a9a;
text-shadow: 0.5px 0.5px 2px rgba(0, 0, 0, 0.3);
`

View file

@ -739,6 +739,9 @@ namespace stateEditors {
) )
} }
// Future: consider whether a list of "partial" keyframes requiring `id` is possible to accept
// * Consider how common this pattern is, as this sort of concept would best be encountered
// a few times to start to see an opportunity for improved ergonomics / crdt.
export function replaceKeyframes( export function replaceKeyframes(
p: WithoutSheetInstance<SheetObjectAddress> & { p: WithoutSheetInstance<SheetObjectAddress> & {
trackId: SequenceTrackId trackId: SequenceTrackId

View file

@ -1,11 +1,21 @@
import React, {useCallback, useContext, useMemo, useState} from 'react' import React, {
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import {createPortal} from 'react-dom' import {createPortal} from 'react-dom'
import {PortalContext} from 'reakit' import {PortalContext} from 'reakit'
import type {AbsolutePlacementBoxConstraints} from './TooltipWrapper'; import type {AbsolutePlacementBoxConstraints} from './TooltipWrapper'
import TooltipWrapper from './TooltipWrapper' import TooltipWrapper from './TooltipWrapper'
export type OpenFn = (e: React.MouseEvent, target: HTMLElement) => void export type OpenFn = (
type CloseFn = () => void e: React.MouseEvent | MouseEvent | {clientX: number; clientY: number},
target: HTMLElement,
) => void
type CloseFn = (reason: string) => void
type State = type State =
| {isOpen: false} | {isOpen: false}
| { | {
@ -17,8 +27,19 @@ type State =
target: HTMLElement target: HTMLElement
} }
const PopoverAutoCloseLock = React.createContext({
// defaults have no effects, since there would not be a
// parent popover to worry about auto-closing.
takeFocus() {
return {
releaseFocus() {},
}
},
})
export default function usePopover( export default function usePopover(
opts: { opts: {
debugName: string
closeWhenPointerIsDistant?: boolean closeWhenPointerIsDistant?: boolean
pointerDistanceThreshold?: number pointerDistanceThreshold?: number
closeOnClickOutside?: boolean closeOnClickOutside?: boolean
@ -26,6 +47,8 @@ export default function usePopover(
}, },
render: () => React.ReactElement, render: () => React.ReactElement,
): [node: React.ReactNode, open: OpenFn, close: CloseFn, isOpen: boolean] { ): [node: React.ReactNode, open: OpenFn, close: CloseFn, isOpen: boolean] {
const _debug = (...args: any) => {} // console.debug.bind(console, opts.debugName)
const [state, setState] = useState<State>({ const [state, setState] = useState<State>({
isOpen: false, isOpen: false,
}) })
@ -38,13 +61,25 @@ export default function usePopover(
}) })
}, []) }, [])
const close = useCallback<CloseFn>(() => { const close = useCallback<CloseFn>((reason) => {
_debug(`closing due to "${reason}"`)
setState({isOpen: false}) setState({isOpen: false})
}, []) }, [])
/**
* See doc comment on {@link useAutoCloseLockState}.
* Used to ensure that moving far away from a parent popover doesn't
* close a child popover.
*/
const lock = useAutoCloseLockState({
_debug,
state,
})
const onClickOutside = useCallback(() => { const onClickOutside = useCallback(() => {
if (lock.childHasFocusRef.current) return
if (opts.closeOnClickOutside !== false) { if (opts.closeOnClickOutside !== false) {
close() close('clicked outside popover')
} }
}, [opts.closeOnClickOutside]) }, [opts.closeOnClickOutside])
@ -53,19 +88,24 @@ export default function usePopover(
if (opts.closeWhenPointerIsDistant === false) return undefined if (opts.closeWhenPointerIsDistant === false) return undefined
return { return {
threshold: opts.pointerDistanceThreshold ?? 100, threshold: opts.pointerDistanceThreshold ?? 100,
callback: close, callback: () => {
if (lock.childHasFocusRef.current) return
close('pointer outside')
},
} }
}, [opts.closeWhenPointerIsDistant]) }, [opts.closeWhenPointerIsDistant])
const node = state.isOpen ? ( const node = state.isOpen ? (
createPortal( createPortal(
<PopoverAutoCloseLock.Provider value={lock.childPopoverLock}>
<TooltipWrapper <TooltipWrapper
children={render} children={render}
target={state.target} target={state.target}
onClickOutside={onClickOutside} onClickOutside={onClickOutside}
onPointerOutside={onPointerOutside} onPointerOutside={onPointerOutside}
constraints={opts.constraints} constraints={opts.constraints}
/>, />
</PopoverAutoCloseLock.Provider>,
portalLayer!, portalLayer!,
) )
) : ( ) : (
@ -74,3 +114,48 @@ export default function usePopover(
return [node, open, close, state.isOpen] return [node, open, close, state.isOpen]
} }
/**
* Keep track of the current lock state, and provide
* a lock that can be passed down to popover children.
*
* Used to ensure that moving far away from a parent popover doesn't
* close a child popover.
* When child popovers are opened, we want to suspend all auto-closing
* behaviors for parenting popovers.
*/
function useAutoCloseLockState(options: {
state: State
_debug: (message: string, args?: object) => void
}) {
const parentLock = useContext(PopoverAutoCloseLock)
useEffect(() => {
if (options.state.isOpen) {
// when this "popover" is open, then take focus from parent
const focused = parentLock.takeFocus()
options._debug('take focus')
return () => {
// when closed / unmounted, release focus
options._debug('release focus')
focused.releaseFocus()
}
}
}, [options.state.isOpen])
// child of us
const childHasFocusRef = useRef(false)
return {
childHasFocusRef: childHasFocusRef,
childPopoverLock: {
takeFocus() {
childHasFocusRef.current = true
return {
releaseFocus() {
childHasFocusRef.current = false
},
}
},
},
}
}

View file

@ -50,18 +50,24 @@ const Select = styled.select`
} }
` `
const BasicSelect: React.FC<{ function BasicSelect<TLiteralOptions extends string>({
value: string value,
onChange: (val: string) => void onChange,
options: Record<string, string> options,
className,
}: {
value: TLiteralOptions
onChange: (val: TLiteralOptions) => void
options: Record<TLiteralOptions, string>
className?: string className?: string
}> = ({value, onChange, options, className}) => { }) {
const _onChange = useCallback( const _onChange = useCallback(
(el: React.ChangeEvent<HTMLSelectElement>) => { (el: React.ChangeEvent<HTMLSelectElement>) => {
onChange(String(el.target.value)) onChange(String(el.target.value) as TLiteralOptions)
}, },
[onChange], [onChange],
) )
return ( return (
<Container> <Container>
<Select className={className} value={value} onChange={_onChange}> <Select className={className} value={value} onChange={_onChange}>

View file

@ -52,14 +52,18 @@ const Input = styled.input`
height: 0; height: 0;
` `
const BasicSwitch: React.FC<{ function BasicSwitch<TLiteralOptions extends string>({
value: string value,
onChange: (val: string) => void onChange,
options: Record<string, string> options,
}> = ({value, onChange, options}) => { }: {
value: TLiteralOptions
onChange: (val: TLiteralOptions) => void
options: Record<TLiteralOptions, string>
}) {
const _onChange = useCallback( const _onChange = useCallback(
(el: React.ChangeEvent<HTMLInputElement>) => { (el: React.ChangeEvent<HTMLInputElement>) => {
onChange(String(el.target.value)) onChange(String(el.target.value) as TLiteralOptions)
}, },
[onChange], [onChange],
) )