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.
*/
export const getPointerMeta = (
p: Pointer<$IntentionalAny> | Pointer<{}> | Pointer<unknown>,
): PointerMeta => {
export const getPointerMeta = <_>(p: PointerType<_>): PointerMeta => {
// @ts-ignore @todo
const meta: PointerMeta = p[
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.
*/
export const getPointerParts = (
p: Pointer<$IntentionalAny> | Pointer<{}> | Pointer<unknown>,
export const getPointerParts = <_>(
p: Pointer<_>,
): {root: {}; path: PathToProp} => {
const {root, path} = getPointerMeta(p)
return {root, path}

View file

@ -37,6 +37,8 @@ export type SerializableMap<
*
* However this wouldn't protect against other unserializable stuff, or nested
* 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 =
| string

View file

@ -14,9 +14,7 @@ type State_Captured = {
}
type State =
| {
type: 'Ready'
}
| {type: 'Ready'}
| State_Captured
| {type: 'Committed'}
| {type: 'Discarded'}
@ -24,12 +22,20 @@ type State =
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 {
/**
* 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 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
/**
* 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.
*/
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)) {
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}) => {
sets.forEach((pointer) => {
const {root, path} = getPointerParts(pointer as Pointer<$FixMe>)
const {root, path} = getPointerParts(pointer)
if (!isSheetObject(root)) {
return
}

View file

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

View file

@ -10,7 +10,7 @@ import {
} from '@theatre/studio/panels/BasePanel/common'
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
import ObjectDetails from './ObjectDetails'
import ProjectDetails from './ProjectDetails/ProjectDetails'
import ProjectDetails from './ProjectDetails'
const Container = styled.div`
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 {isPropConfigComposite} from '@theatre/shared/propTypes/utils'
import type {$IntentionalAny} from '@theatre/shared/utils/types'
import {getPointerParts} from '@theatre/dataverse'
import type {Pointer} from '@theatre/dataverse'
import last from 'lodash-es/last'
import {darken, transparentize} from 'polished'
import React from 'react'
import styled from 'styled-components'
import DeterminePropEditor from './DeterminePropEditor'
import {
indentationFormula,
propNameText,
rowBg,
} from './utils/SingleRowPropEditor'
import DefaultOrStaticValueIndicator from './utils/DefaultValueIndicator'
} from '@theatre/studio/panels/DetailPanel/DeterminePropEditorForDetail/SingleRowPropEditor'
import {propNameTextCSS} from '@theatre/studio/propEditors/utils/propNameTextCSS'
import DefaultOrStaticValueIndicator from '@theatre/studio/propEditors/DefaultValueIndicator'
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
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`
--step: 8px;
@ -49,7 +50,7 @@ const PropName = styled.div`
/* color: white; */
}
${() => propNameText};
${() => propNameTextCSS};
`
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; */
`
const CompoundPropEditor: IPropEditorFC<
PropTypeConfig_Compound<$IntentionalAny>
> = ({pointerToProp, obj, propConfig, visualIndentation: depth}) => {
export type ICompoundPropDetailEditorProps<
TPropTypeConfig extends PropTypeConfig_Compound<any>,
> = {
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 allSubs = Object.entries(propConfig.props)
@ -75,64 +90,15 @@ const CompoundPropEditor: IPropEditorFC<
const [propNameContainerRef, propNameContainer] =
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
// previous versions of the DetailCompoundPropEditor had a context menu item for "Reset values".
return (
<Container>
{/* {contextMenu} */}
<Header
// @ts-ignore
style={{'--depth': depth - 1}}
style={{'--depth': visualIndentation - 1}}
>
<Padding>
<DefaultOrStaticValueIndicator hasStaticOverride={false} />
@ -142,19 +108,19 @@ const CompoundPropEditor: IPropEditorFC<
<SubProps
// @ts-ignore
style={{'--depth': depth}}
depth={depth}
style={{'--depth': visualIndentation}}
depth={visualIndentation}
lastSubIsComposite={lastSubPropIsComposite}
>
{[...nonCompositeSubs, ...compositeSubs].map(
([subPropKey, subPropConfig]) => {
return (
<DeterminePropEditor
<DeterminePropEditorForDetail
key={'prop-' + subPropKey}
propConfig={subPropConfig}
pointerToProp={pointerToProp[subPropKey]}
pointerToProp={pointerToProp[subPropKey] as Pointer<$FixMe>}
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 {last} from 'lodash-es'
import React from 'react'
import type {useEditingToolsForPrimitiveProp} from '@theatre/studio/panels/DetailPanel/propEditors/utils/useEditingToolsForPrimitiveProp'
import {shadeToColor} from '@theatre/studio/panels/DetailPanel/propEditors/utils/useEditingToolsForPrimitiveProp'
import type {useEditingToolsForSimplePropInDetailsPanel} from '@theatre/studio/propEditors/useEditingToolsForSimpleProp'
import styled, {css} from 'styled-components'
import {transparentize} from 'polished'
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))`
@ -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`
display: flex;
height: 30px;
@ -83,7 +76,7 @@ const PropNameContainer = styled.div`
user-select: none;
cursor: default;
${propNameText};
${propNameTextCSS};
&:hover {
color: white;
}
@ -111,13 +104,13 @@ const InputContainer = styled.div`
type ISingleRowPropEditorProps<T> = {
propConfig: propTypes.PropTypeConfig
pointerToProp: Pointer<T>
stuff: ReturnType<typeof useEditingToolsForPrimitiveProp>
editingTools: ReturnType<typeof useEditingToolsForSimplePropInDetailsPanel>
}
export function SingleRowPropEditor<T>({
propConfig,
pointerToProp,
stuff,
editingTools,
children,
}: React.PropsWithChildren<ISingleRowPropEditorProps<T>>): React.ReactElement<
any,
@ -129,16 +122,14 @@ export function SingleRowPropEditor<T>({
useRefAndState<HTMLDivElement | null>(null)
const [contextMenu] = useContextMenu(propNameContainer, {
menuItems: stuff.contextMenuItems,
menuItems: editingTools.contextMenuItems,
})
const color = shadeToColor[stuff.shade]
return (
<Row>
{contextMenu}
<Left>
<ControlsContainer>{stuff.controlIndicators}</ControlsContainer>
<ControlsContainer>{editingTools.controlIndicators}</ControlsContainer>
<PropNameContainer
ref={propNameContainerRef}

View file

@ -1,18 +1,18 @@
import React, {useMemo} from 'react'
import DeterminePropEditor from './propEditors/DeterminePropEditor'
import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
import type {Pointer} from '@theatre/dataverse'
import type {$FixMe} from '@theatre/shared/utils/types'
import DeterminePropEditorForDetail from './DeterminePropEditorForDetail'
const ObjectDetails: React.FC<{
objects: SheetObject[]
/** TODO: add support for multiple objects (it would show their common props) */
objects: [SheetObject]
}> = ({objects}) => {
// @todo add support for multiple objects (it would show their common props)
const obj = objects[0]
const key = useMemo(() => JSON.stringify(obj.address), [obj])
return (
<DeterminePropEditor
<DeterminePropEditorForDetail
key={key}
obj={obj}
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 React, {useCallback, useState} from 'react'
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 {rowBgColor} from './DeterminePropEditorForDetail/SingleRowPropEditor'
import StateConflictRow from './ProjectDetails/StateConflictRow'
const Container = styled.div`
background-color: ${rowBgColor};
@ -59,7 +59,7 @@ const ProjectDetails: React.FC<{
}, [])
const [tooltip, openExportTooltip] = usePopover(
{pointerDistanceThreshold: 50},
{debugName: 'ProjectDetails', pointerDistanceThreshold: 50},
() => (
<ExportTooltip>
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 {HiOutlineChevronRight} from 'react-icons/all'
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}>`
--depth: ${(props) => props.depth};
@ -33,7 +33,7 @@ const Header = styled(BaseHeader)<{
`
const Head_Label = styled.span`
${propNameText};
${propNameTextCSS};
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;

View file

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

View file

@ -94,6 +94,7 @@ const Connector: React.FC<IProps> = (props) => {
const rightDims = val(props.layoutP.rightDims)
const [popoverNode, openPopover, closePopover, isPopoverOpen] = usePopover(
{
debugName: 'Connector',
closeWhenPointerIsDistant: !isPointerBeingCaptured(),
constraints: {
minX: rightDims.screenX + POPOVER_MARGIN,
@ -130,9 +131,10 @@ const Connector: React.FC<IProps> = (props) => {
{...themeValues}
ref={nodeRef}
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
}), 1, 1)`,
}))`,
}}
onClick={(e) => {
if (node) openPopover(e, node)

View file

@ -126,7 +126,7 @@ type IProps = {
/**
* Called when user hits enter/escape
*/
onRequestClose: () => void
onRequestClose: (reason: string) => void
} & Parameters<typeof KeyframeEditor>[0]
const CurveEditorPopover: React.FC<IProps> = (props) => {
@ -188,9 +188,9 @@ const CurveEditorPopover: React.FC<IProps> = (props) => {
optionsRef.current[displayedPresets[0].label]?.current?.focus()
} else if (e.key === 'Escape') {
discardTempValue(tempTransaction)
props.onRequestClose()
props.onRequestClose('key Escape')
} 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>) => {
if (e.key === 'Escape') {
discardTempValue(tempTransaction)
props.onRequestClose()
props.onRequestClose('key Escape')
e.stopPropagation()
} else if (e.key === 'Enter') {
props.onRequestClose()
props.onRequestClose('key Enter')
e.stopPropagation()
}
}
@ -267,7 +267,7 @@ const CurveEditorPopover: React.FC<IProps> = (props) => {
const onEasingOptionMouseOut = () => setPreview(null)
const onSelectEasingOption = (item: {label: string; value: string}) => {
setTempValue(tempTransaction, props, cur, next, item.value)
props.onRequestClose()
props.onRequestClose('selected easing option')
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 type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
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 useRefAndState from '@theatre/studio/utils/useRefAndState'
import {val} from '@theatre/dataverse'
import {lighten} from 'polished'
import React, {useMemo, useRef} from 'react'
import styled from 'styled-components'
import {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
import {includeLockFrameStampAttrs} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
import {
includeLockFrameStampAttrs,
useLockFrameStampPosition,
} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
import {
lockedCursorCssVarName,
useCssCursorLock,
@ -19,6 +23,11 @@ import SnapCursor from './SnapCursor.svg'
import selectedKeyframeIdsIfInSingleTrack from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/selectedKeyframeIdsIfInSingleTrack'
import type {IKeyframeEditorProps} from './KeyframeEditor'
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
const HIT_ZONE_SIZE_PX = 12
@ -96,8 +105,14 @@ type IKeyframeDotProps = IKeyframeEditorProps
const KeyframeDot: React.VFC<IKeyframeDotProps> = (props) => {
const [ref, node] = useRefAndState<HTMLDivElement | null>(null)
const [isDragging] = useDragKeyframe(node, props)
const [contextMenu] = useKeyframeContextMenu(node, props)
const [inlineEditorPopover, openEditor] =
useKeyframeInlineEditorPopover(props)
const [isDragging] = useDragForKeyframeDot(node, props, {
onClickFromDrag(dragStartEvent) {
openEditor(dragStartEvent, ref.current!)
},
})
return (
<>
@ -108,6 +123,7 @@ const KeyframeDot: React.VFC<IKeyframeDotProps> = (props) => {
className={isDragging ? 'beingDragged' : ''}
/>
<Diamond isSelected={!!props.selection} />
{inlineEditorPopover}
{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,
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] {
const propsRef = useRef(props)
propsRef.current = props
@ -146,7 +200,6 @@ function useDragKeyframe(
const useDragOpts = useMemo<UseDragOpts>(() => {
return {
debugName: 'KeyframeDot/useDragKeyframe',
onDragStart(event) {
const props = propsRef.current
if (props.selection) {
@ -203,8 +256,12 @@ function useDragKeyframe(
})
},
onDragEnd(dragHappened) {
if (dragHappened) tempTransaction?.commit()
else tempTransaction?.discard()
if (dragHappened) {
tempTransaction?.commit()
} else {
tempTransaction?.discard()
options.onClickFromDrag(event)
}
},
}
},
@ -247,7 +304,7 @@ function copyKeyFrameContextMenuItem(
keyframeIds: string[],
): IContextMenuItem {
return {
label: keyframeIds.length > 1 ? 'Copy selection' : 'Copy keyframe',
label: keyframeIds.length > 1 ? 'Copy Selection' : 'Copy Keyframe',
callback: () => {
const keyframes = keyframeIds.map(
(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 BasicNumberInput from '@theatre/studio/uiComponents/form/BasicNumberInput'
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
@ -20,7 +20,7 @@ const Container = styled.div`
`
const Label = styled.div`
${propNameText};
${propNameTextCSS};
white-space: nowrap;
`
@ -31,7 +31,7 @@ const LengthEditorPopover: React.FC<{
/**
* Called when user hits enter/escape
*/
onRequestClose: () => void
onRequestClose: (reason: string) => void
}> = ({layoutP, onRequestClose}) => {
const sheet = useVal(layoutP.sheet)
@ -89,7 +89,7 @@ const LengthEditorPopover: React.FC<{
{...fns}
isValid={greaterThanZero}
inputRef={inputRef}
onBlur={onRequestClose}
onBlur={onRequestClose.bind(null, 'length editor number input blur')}
nudge={nudge}
/>
</Container>

View file

@ -129,6 +129,8 @@ type IProps = {
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.
* 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 [isDragging] = useDragBulge(node, {layoutP})
const [popoverNode, openPopover, closePopover, isPopoverOpen] = usePopover(
{},
{debugName: 'LengthIndicator'},
() => {
return (
<BasicPopover>
@ -178,7 +180,9 @@ const LengthIndicator: React.FC<IProps> = ({layoutP}) => {
<Strip
style={{
height: height + 'px',
transform: `translateX(${translateX === 0 ? -1000 : translateX}px)`,
transform: `translateX(${
translateX === 0 ? RENDER_OUT_OF_VIEW_X : translateX
}px)`,
}}
className={isDragging ? 'dragging' : ''}
>

View file

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

View file

@ -3,7 +3,7 @@ import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEdi
import {usePrism} from '@theatre/react'
import type {BasicNumberInputNudgeFn} 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 React from 'react'
import {val} from '@theatre/dataverse'
@ -21,7 +21,7 @@ const Container = styled.div`
`
const Label = styled.div`
${propNameText};
${propNameTextCSS};
white-space: nowrap;
`
@ -32,7 +32,7 @@ const PlayheadPositionPopover: React.FC<{
/**
* Called when user hits enter/escape
*/
onRequestClose: () => void
onRequestClose: (reason: string) => void
}> = ({layoutP, onRequestClose}) => {
const sheet = val(layoutP.sheet)
const sequence = sheet.getSequence()
@ -80,7 +80,7 @@ const PlayheadPositionPopover: React.FC<{
{...fns}
isValid={greaterThanZero}
inputRef={inputRef}
onBlur={onRequestClose}
onBlur={onRequestClose.bind(null, 'number input blur')}
nudge={nudge}
/>
</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,
} from '@theatre/shared/utils/color'
import React, {useCallback, useRef} from 'react'
import {useEditingToolsForPrimitiveProp} from './utils/useEditingToolsForPrimitiveProp'
import {SingleRowPropEditor} from './utils/SingleRowPropEditor'
import {RgbaColorPicker} from '@theatre/studio/uiComponents/colorPicker'
import styled from 'styled-components'
import usePopover from '@theatre/studio/uiComponents/Popover/usePopover'
import BasicStringInput from '@theatre/studio/uiComponents/form/BasicStringInput'
import {popoverBackgroundColor} from '@theatre/studio/uiComponents/Popover/BasicPopover'
import type {IPropEditorFC} from './utils/IPropEditorFC'
import type {ISimplePropEditorReactProps} from './ISimplePropEditorReactProps'
const RowContainer = styled.div`
display: flex;
@ -22,16 +20,18 @@ const RowContainer = styled.div`
gap: 4px;
`
interface PuckProps {
background: Rgba
interface ColorPreviewPuckProps {
rgbaColor: Rgba
}
const Puck = styled.div.attrs<PuckProps>((props) => ({
const ColorPreviewPuck = styled.div.attrs<ColorPreviewPuckProps>((props) => ({
style: {
background: props.background,
// weirdly, rgba2hex is needed to ensure initial render was correct background?
// huge head scratcher.
background: rgba2hex(props.rgbaColor),
},
}))<PuckProps>`
height: calc(100% - 12px);
}))<ColorPreviewPuckProps>`
height: 18px;
aspect-ratio: 1;
border-radius: 2px;
`
@ -42,7 +42,7 @@ const HexInput = styled(BasicStringInput)`
const noop = () => {}
const Popover = styled.div`
const RgbaPopover = styled.div`
position: absolute;
background-color: ${popoverBackgroundColor};
color: white;
@ -60,60 +60,58 @@ const Popover = styled.div`
box-shadow: none;
`
const RgbaPropEditor: IPropEditorFC<PropTypeConfig_Rgba> = ({
propConfig,
pointerToProp,
obj,
}) => {
function RgbaPropEditor({
editingTools,
value,
}: ISimplePropEditorReactProps<PropTypeConfig_Rgba>) {
const containerRef = useRef<HTMLDivElement>(null!)
const stuff = useEditingToolsForPrimitiveProp(pointerToProp, obj, propConfig)
const onChange = useCallback(
(color: string) => {
const rgba = decorateRgba(parseRgbaFromHex(color))
stuff.permanentlySetValue(rgba)
editingTools.permanentlySetValue(rgba)
},
[stuff],
[editingTools],
)
const [popoverNode, openPopover] = usePopover({}, () => {
return (
<Popover>
const [popoverNode, openPopover] = usePopover(
{debugName: 'RgbaPropEditor'},
() => (
<RgbaPopover>
<RgbaColorPicker
color={{
r: stuff.value.r,
g: stuff.value.g,
b: stuff.value.b,
a: stuff.value.a,
r: value.r,
g: value.g,
b: value.b,
a: value.a,
}}
temporarilySetValue={(color) => {
const rgba = decorateRgba(color)
stuff.temporarilySetValue(rgba)
editingTools.temporarilySetValue(rgba)
}}
permanentlySetValue={(color) => {
// console.log('perm')
const rgba = decorateRgba(color)
stuff.permanentlySetValue(rgba)
editingTools.permanentlySetValue(rgba)
}}
discardTemporaryValue={stuff.discardTemporaryValue}
discardTemporaryValue={editingTools.discardTemporaryValue}
/>
</Popover>
</RgbaPopover>
),
)
})
return (
<SingleRowPropEditor {...{stuff, propConfig, pointerToProp}}>
<>
<RowContainer>
<Puck
background={stuff.value}
<ColorPreviewPuck
rgbaColor={value}
ref={containerRef}
onClick={(e) => {
openPopover(e, containerRef.current)
}}
/>
<HexInput
value={rgba2hex(stuff.value)}
value={rgba2hex(value)}
temporarilySetValue={noop}
discardTemporaryValue={noop}
permanentlySetValue={onChange}
@ -121,7 +119,7 @@ const RgbaPropEditor: IPropEditorFC<PropTypeConfig_Rgba> = ({
/>
</RowContainer>
{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 SheetObject from '@theatre/core/sheetObjects/SheetObject'
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 getDeep from '@theatre/shared/utils/getDeep'
import {usePrism} from '@theatre/react'
import type {SerializablePrimitive} from '@theatre/shared/utils/types'
import {getPointerParts, prism, val} from '@theatre/dataverse'
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 type {SerializablePrimitive as SerializablePrimitive} from '@theatre/shared/utils/types'
import type {PropTypeConfig_AllSimples} from '@theatre/core/propTypes'
import {isPropConfSequencable} from '@theatre/shared/propTypes/utils'
import type {SequenceTrackId} from '@theatre/shared/utils/ids'
interface CommonStuff<T> {
import DefaultOrStaticValueIndicator from './DefaultValueIndicator'
import NextPrevKeyframeCursors from './NextPrevKeyframeCursors'
interface EditingToolsCommon<T> {
value: T
beingScrubbed: boolean
contextMenuItems: Array<IContextMenuItem>
/** e.g. `< • >` or `< >` for {@link EditingToolsSequenced} */
controlIndicators: React.ReactElement
temporarilySetValue(v: T): void
@ -28,38 +31,50 @@ interface CommonStuff<T> {
permanentlySetValue(v: T): void
}
interface Default<T> extends CommonStuff<T> {
interface EditingToolsDefault<T> extends EditingToolsCommon<T> {
type: 'Default'
shade: Shade
}
interface Static<T> extends CommonStuff<T> {
interface EditingToolsStatic<T> extends EditingToolsCommon<T> {
type: 'Static'
shade: Shade
}
interface Sequenced<T> extends CommonStuff<T> {
interface EditingToolsSequenced<T> extends EditingToolsCommon<T> {
type: 'Sequenced'
shade: Shade
/** based on the position of the playhead */
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,
>(
pointerToProp: Pointer<T>,
obj: SheetObject,
propConfig: PropTypeConfig,
): Stuff<T> {
propConfig: PropTypeConfig_AllSimples,
): EditingTools<T> {
return usePrism(() => {
const pathToProp = getPointerParts(pointerToProp).path
const final = obj.getValueByPointer(pointerToProp) as T
const callbacks = prism.memo(
'callbacks',
const editPropValue = prism.memo(
'editPropValue',
() => {
let currentScrub: Scrub | null = null
@ -96,10 +111,6 @@ export function useEditingToolsForPrimitiveProp<
[],
)
// const validSequenceTracks = val(
// obj.template.getMapOfValidSequenceTracks_forStudio(),
// )
const beingScrubbed =
val(
get(
@ -114,8 +125,8 @@ export function useEditingToolsForPrimitiveProp<
const contextMenuItems: IContextMenuItem[] = []
const common: CommonStuff<T> = {
...callbacks,
const common: EditingToolsCommon<T> = {
...editPropValue,
value: final,
beingScrubbed,
contextMenuItems,
@ -224,7 +235,7 @@ export function useEditingToolsForPrimitiveProp<
/>
)
const ret: Sequenced<T> = {
const ret: EditingToolsSequenced<T> = {
...common,
type: 'Sequenced',
shade,
@ -239,7 +250,7 @@ export function useEditingToolsForPrimitiveProp<
contextMenuItems.push({
label: 'Reset to default',
callback: () => {
getStudio()!.transaction(({unset}) => {
getStudio()!.transaction(({unset: unset}) => {
unset(pointerToProp)
})
},
@ -264,7 +275,7 @@ export function useEditingToolsForPrimitiveProp<
const statics = val(obj.template.getStaticValues())
if (typeof getDeep(statics, pathToProp) !== 'undefined') {
const ret: Static<T> = {
const ret: EditingToolsStatic<T> = {
...common,
type: 'Static',
shade: common.beingScrubbed ? 'Static_BeingScrubbed' : 'Static',
@ -275,7 +286,7 @@ export function useEditingToolsForPrimitiveProp<
return ret
}
const ret: Default<T> = {
const ret: EditingToolsDefault<T> = {
...common,
type: '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(
p: WithoutSheetInstance<SheetObjectAddress> & {
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 {PortalContext} from 'reakit'
import type {AbsolutePlacementBoxConstraints} from './TooltipWrapper';
import type {AbsolutePlacementBoxConstraints} from './TooltipWrapper'
import TooltipWrapper from './TooltipWrapper'
export type OpenFn = (e: React.MouseEvent, target: HTMLElement) => void
type CloseFn = () => void
export type OpenFn = (
e: React.MouseEvent | MouseEvent | {clientX: number; clientY: number},
target: HTMLElement,
) => void
type CloseFn = (reason: string) => void
type State =
| {isOpen: false}
| {
@ -17,8 +27,19 @@ type State =
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(
opts: {
debugName: string
closeWhenPointerIsDistant?: boolean
pointerDistanceThreshold?: number
closeOnClickOutside?: boolean
@ -26,6 +47,8 @@ export default function usePopover(
},
render: () => React.ReactElement,
): [node: React.ReactNode, open: OpenFn, close: CloseFn, isOpen: boolean] {
const _debug = (...args: any) => {} // console.debug.bind(console, opts.debugName)
const [state, setState] = useState<State>({
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})
}, [])
/**
* 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(() => {
if (lock.childHasFocusRef.current) return
if (opts.closeOnClickOutside !== false) {
close()
close('clicked outside popover')
}
}, [opts.closeOnClickOutside])
@ -53,19 +88,24 @@ export default function usePopover(
if (opts.closeWhenPointerIsDistant === false) return undefined
return {
threshold: opts.pointerDistanceThreshold ?? 100,
callback: close,
callback: () => {
if (lock.childHasFocusRef.current) return
close('pointer outside')
},
}
}, [opts.closeWhenPointerIsDistant])
const node = state.isOpen ? (
createPortal(
<PopoverAutoCloseLock.Provider value={lock.childPopoverLock}>
<TooltipWrapper
children={render}
target={state.target}
onClickOutside={onClickOutside}
onPointerOutside={onPointerOutside}
constraints={opts.constraints}
/>,
/>
</PopoverAutoCloseLock.Provider>,
portalLayer!,
)
) : (
@ -74,3 +114,48 @@ export default function usePopover(
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<{
value: string
onChange: (val: string) => void
options: Record<string, string>
function BasicSelect<TLiteralOptions extends string>({
value,
onChange,
options,
className,
}: {
value: TLiteralOptions
onChange: (val: TLiteralOptions) => void
options: Record<TLiteralOptions, string>
className?: string
}> = ({value, onChange, options, className}) => {
}) {
const _onChange = useCallback(
(el: React.ChangeEvent<HTMLSelectElement>) => {
onChange(String(el.target.value))
onChange(String(el.target.value) as TLiteralOptions)
},
[onChange],
)
return (
<Container>
<Select className={className} value={value} onChange={_onChange}>

View file

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