diff --git a/packages/dataverse/src/pointer.ts b/packages/dataverse/src/pointer.ts index 62aaed1..65d0645 100644 --- a/packages/dataverse/src/pointer.ts +++ b/packages/dataverse/src/pointer.ts @@ -113,9 +113,7 @@ const proxyHandler = { * * @param p - The pointer. */ -export const getPointerMeta = ( - p: Pointer<$IntentionalAny> | Pointer<{}> | Pointer, -): 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, +export const getPointerParts = <_>( + p: Pointer<_>, ): {root: {}; path: PathToProp} => { const {root, path} = getPointerMeta(p) return {root, path} diff --git a/theatre/shared/src/utils/types.ts b/theatre/shared/src/utils/types.ts index 863a5ce..927def3 100644 --- a/theatre/shared/src/utils/types.ts +++ b/theatre/shared/src/utils/types.ts @@ -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 diff --git a/theatre/studio/src/Scrub.ts b/theatre/studio/src/Scrub.ts index 071344a..fa9cae7 100644 --- a/theatre/studio/src/Scrub.ts +++ b/theatre/studio/src/Scrub.ts @@ -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 } diff --git a/theatre/studio/src/StudioStore/createTransactionPrivateApi.ts b/theatre/studio/src/StudioStore/createTransactionPrivateApi.ts index 00816b8..778ad44 100644 --- a/theatre/studio/src/StudioStore/createTransactionPrivateApi.ts +++ b/theatre/studio/src/StudioStore/createTransactionPrivateApi.ts @@ -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) diff --git a/theatre/studio/src/panels/DetailPanel/DetailPanel.tsx b/theatre/studio/src/panels/DetailPanel/DetailPanel.tsx index 50b9cc2..07dc03b 100644 --- a/theatre/studio/src/panels/DetailPanel/DetailPanel.tsx +++ b/theatre/studio/src/panels/DetailPanel/DetailPanel.tsx @@ -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; diff --git a/theatre/studio/src/panels/DetailPanel/DeterminePropEditorForDetail.tsx b/theatre/studio/src/panels/DetailPanel/DeterminePropEditorForDetail.tsx new file mode 100644 index 0000000..2fe3f77 --- /dev/null +++ b/theatre/studio/src/panels/DetailPanel/DeterminePropEditorForDetail.tsx @@ -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 +> = (p) => { + const propConfig = + p.propConfig ?? getPropTypeByPointer(p.pointerToProp, p.obj) + + if (propConfig.type === 'compound') { + return ( + + ) + } else if (propConfig.type === 'enum') { + // notice: enums are not implemented, yet. + return <> + } else { + const PropEditor = simplePropEditorByPropType[propConfig.type] + + return ( + + > + } + obj={p.obj} + visualIndentation={p.visualIndentation} + pointerToProp={p.pointerToProp} + propConfig={propConfig} + /> + ) + } +} + +export default DeterminePropEditorForDetail +type IDeterminePropEditorForDetailProps = + IDetailEditablePropertyProps & { + visualIndentation: number + } +type IDetailEditablePropertyProps = { + obj: SheetObject + pointerToProp: Pointer['valueType']> + propConfig: PropConfigForType +} diff --git a/theatre/studio/src/panels/DetailPanel/propEditors/CompoundPropEditor.tsx b/theatre/studio/src/panels/DetailPanel/DeterminePropEditorForDetail/DetailCompoundPropEditor.tsx similarity index 51% rename from theatre/studio/src/panels/DetailPanel/propEditors/CompoundPropEditor.tsx rename to theatre/studio/src/panels/DetailPanel/DeterminePropEditorForDetail/DetailCompoundPropEditor.tsx index c2e6eec..f02c8e1 100644 --- a/theatre/studio/src/panels/DetailPanel/propEditors/CompoundPropEditor.tsx +++ b/theatre/studio/src/panels/DetailPanel/DeterminePropEditorForDetail/DetailCompoundPropEditor.tsx @@ -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, +> = { + propConfig: TPropTypeConfig + pointerToProp: Pointer + obj: SheetObject + visualIndentation: number +} + +function DetailCompoundPropEditor< + TPropTypeConfig extends PropTypeConfig_Compound, +>({ + pointerToProp, + obj, + propConfig, + visualIndentation, +}: ICompoundPropDetailEditorProps) { 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(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 ( - {/* {contextMenu} */}
@@ -142,19 +108,19 @@ const CompoundPropEditor: IPropEditorFC< {[...nonCompositeSubs, ...compositeSubs].map( ([subPropKey, subPropConfig]) => { return ( - } obj={obj} - visualIndentation={depth + 1} + visualIndentation={visualIndentation + 1} /> ) }, @@ -164,4 +130,4 @@ const CompoundPropEditor: IPropEditorFC< ) } -export default CompoundPropEditor +export default DetailCompoundPropEditor diff --git a/theatre/studio/src/panels/DetailPanel/DeterminePropEditorForDetail/DetailSimplePropEditor.tsx b/theatre/studio/src/panels/DetailPanel/DeterminePropEditorForDetail/DetailSimplePropEditor.tsx new file mode 100644 index 0000000..a82431a --- /dev/null +++ b/theatre/studio/src/panels/DetailPanel/DeterminePropEditorForDetail/DetailSimplePropEditor.tsx @@ -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, +> = { + propConfig: TPropTypeConfig + pointerToProp: Pointer + obj: SheetObject + visualIndentation: number + SimpleEditorComponent: React.VFC> +} + +/** + * 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) { + const editingTools = useEditingToolsForSimplePropInDetailsPanel( + pointerToProp, + obj, + propConfig, + ) + + return ( + + + + ) +} + +export default DetailSimplePropEditor diff --git a/theatre/studio/src/panels/DetailPanel/propEditors/utils/SingleRowPropEditor.tsx b/theatre/studio/src/panels/DetailPanel/DeterminePropEditorForDetail/SingleRowPropEditor.tsx similarity index 83% rename from theatre/studio/src/panels/DetailPanel/propEditors/utils/SingleRowPropEditor.tsx rename to theatre/studio/src/panels/DetailPanel/DeterminePropEditorForDetail/SingleRowPropEditor.tsx index 18ae40a..7aca78a 100644 --- a/theatre/studio/src/panels/DetailPanel/propEditors/utils/SingleRowPropEditor.tsx +++ b/theatre/studio/src/panels/DetailPanel/DeterminePropEditorForDetail/SingleRowPropEditor.tsx @@ -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 = { propConfig: propTypes.PropTypeConfig pointerToProp: Pointer - stuff: ReturnType + editingTools: ReturnType } export function SingleRowPropEditor({ propConfig, pointerToProp, - stuff, + editingTools, children, }: React.PropsWithChildren>): React.ReactElement< any, @@ -129,16 +122,14 @@ export function SingleRowPropEditor({ useRefAndState(null) const [contextMenu] = useContextMenu(propNameContainer, { - menuItems: stuff.contextMenuItems, + menuItems: editingTools.contextMenuItems, }) - const color = shadeToColor[stuff.shade] - return ( {contextMenu} - {stuff.controlIndicators} + {editingTools.controlIndicators} = ({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 ( - } diff --git a/theatre/studio/src/panels/DetailPanel/ProjectDetails/ProjectDetails.tsx b/theatre/studio/src/panels/DetailPanel/ProjectDetails.tsx similarity index 92% rename from theatre/studio/src/panels/DetailPanel/ProjectDetails/ProjectDetails.tsx rename to theatre/studio/src/panels/DetailPanel/ProjectDetails.tsx index 751c1a5..ee86871 100644 --- a/theatre/studio/src/panels/DetailPanel/ProjectDetails/ProjectDetails.tsx +++ b/theatre/studio/src/panels/DetailPanel/ProjectDetails.tsx @@ -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}, () => ( This will create a JSON file with the state of your project. You can diff --git a/theatre/studio/src/panels/DetailPanel/propEditors/BooleanPropEditor.tsx b/theatre/studio/src/panels/DetailPanel/propEditors/BooleanPropEditor.tsx deleted file mode 100644 index 9174111..0000000 --- a/theatre/studio/src/panels/DetailPanel/propEditors/BooleanPropEditor.tsx +++ /dev/null @@ -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 = ({ - propConfig, - pointerToProp, - obj, -}) => { - const stuff = useEditingToolsForPrimitiveProp( - pointerToProp, - obj, - propConfig, - ) - - const onChange = useCallback( - (el: React.ChangeEvent) => { - stuff.permanentlySetValue(Boolean(el.target.checked)) - }, - [propConfig, pointerToProp, obj], - ) - - return ( - - - - ) -} - -export default BooleanPropEditor diff --git a/theatre/studio/src/panels/DetailPanel/propEditors/DeterminePropEditor.tsx b/theatre/studio/src/panels/DetailPanel/propEditors/DeterminePropEditor.tsx deleted file mode 100644 index f9019e1..0000000 --- a/theatre/studio/src/panels/DetailPanel/propEditors/DeterminePropEditor.tsx +++ /dev/null @@ -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 = Extract< - PropTypeConfig, - {type: K} -> - -type IPropEditorByPropType = { - [K in PropTypeConfig['type']]: React.FC<{ - obj: SheetObject - pointerToProp: Pointer['valueType']> - propConfig: PropConfigByType - visualIndentation: number - }> -} - -const propEditorByPropType: IPropEditorByPropType = { - compound: CompoundPropEditor, - number: NumberPropEditor, - string: StringPropEditor, - enum: () => <>, - boolean: BooleanPropEditor, - stringLiteral: StringLiteralPropEditor, - rgba: RgbaPropEditor, -} - -export type IEditablePropertyProps = { - obj: SheetObject - pointerToProp: Pointer['valueType']> - propConfig: PropConfigByType -} - -type IDeterminePropEditorProps = - IEditablePropertyProps & { - visualIndentation: number - } - -const DeterminePropEditor: React.FC< - IDeterminePropEditorProps -> = (p) => { - const propConfig = - p.propConfig ?? getPropTypeByPointer(p.pointerToProp, p.obj) - - const PropEditor = propEditorByPropType[propConfig.type] - - return ( - - ) -} - -export default DeterminePropEditor diff --git a/theatre/studio/src/panels/DetailPanel/propEditors/NumberPropEditor.tsx b/theatre/studio/src/panels/DetailPanel/propEditors/NumberPropEditor.tsx deleted file mode 100644 index 865fb9b..0000000 --- a/theatre/studio/src/panels/DetailPanel/propEditors/NumberPropEditor.tsx +++ /dev/null @@ -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 = ({ - propConfig, - pointerToProp, - obj, -}) => { - const stuff = useEditingToolsForPrimitiveProp( - pointerToProp, - obj, - propConfig, - ) - - const nudge = useCallback( - (params: {deltaX: number; deltaFraction: number; magnitude: number}) => { - return propConfig.nudgeFn({...params, config: propConfig}) - }, - [propConfig], - ) - - return ( - - - - ) -} - -export default NumberPropEditor diff --git a/theatre/studio/src/panels/DetailPanel/propEditors/StringLiteralPropEditor.tsx b/theatre/studio/src/panels/DetailPanel/propEditors/StringLiteralPropEditor.tsx deleted file mode 100644 index ed70a2f..0000000 --- a/theatre/studio/src/panels/DetailPanel/propEditors/StringLiteralPropEditor.tsx +++ /dev/null @@ -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( - pointerToProp, - obj, - propConfig, - ) - - const onChange = useCallback( - (val: string) => { - stuff.permanentlySetValue(val) - }, - [propConfig, pointerToProp, obj], - ) - - return ( - - {propConfig.as === 'menu' ? ( - - ) : ( - - )} - - ) -} - -export default StringLiteralPropEditor diff --git a/theatre/studio/src/panels/DetailPanel/propEditors/StringPropEditor.tsx b/theatre/studio/src/panels/DetailPanel/propEditors/StringPropEditor.tsx deleted file mode 100644 index 0f6fed3..0000000 --- a/theatre/studio/src/panels/DetailPanel/propEditors/StringPropEditor.tsx +++ /dev/null @@ -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 = ({ - propConfig, - pointerToProp, - obj, -}) => { - const stuff = useEditingToolsForPrimitiveProp( - pointerToProp, - obj, - propConfig, - ) - - return ( - - - - ) -} - -export default StringPropEditor diff --git a/theatre/studio/src/panels/DetailPanel/propEditors/utils/IPropEditorFC.ts b/theatre/studio/src/panels/DetailPanel/propEditors/utils/IPropEditorFC.ts deleted file mode 100644 index 0ec3cae..0000000 --- a/theatre/studio/src/panels/DetailPanel/propEditors/utils/IPropEditorFC.ts +++ /dev/null @@ -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> = - React.FC<{ - propConfig: TPropTypeConfig - pointerToProp: Pointer - obj: SheetObject - visualIndentation: number - }> diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/AnyCompositeRow.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/AnyCompositeRow.tsx index e5bf72b..33ed8be 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/AnyCompositeRow.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/AnyCompositeRow.tsx @@ -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; diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/PrimitivePropRow.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/PrimitivePropRow.tsx index 3e7eff5..c3c71ea 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/PrimitivePropRow.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/PrimitivePropRow.tsx @@ -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, diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/Connector.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/Connector.tsx index d2a61df..3f5d1d0 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/Connector.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/Connector.tsx @@ -94,6 +94,7 @@ const Connector: React.FC = (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 = (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) diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/CurveEditorPopover/CurveEditorPopover.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/CurveEditorPopover/CurveEditorPopover.tsx index 14e52a1..8f17179 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/CurveEditorPopover/CurveEditorPopover.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/CurveEditorPopover/CurveEditorPopover.tsx @@ -126,7 +126,7 @@ type IProps = { /** * Called when user hits enter/escape */ - onRequestClose: () => void + onRequestClose: (reason: string) => void } & Parameters[0] const CurveEditorPopover: React.FC = (props) => { @@ -188,9 +188,9 @@ const CurveEditorPopover: React.FC = (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 = (props) => { const onEasingOptionKeydown = (e: KeyboardEvent) => { 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 = (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 } diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/DeterminePropEditorForKeyframe.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/DeterminePropEditorForKeyframe.tsx new file mode 100644 index 0000000..a1d11cf --- /dev/null +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/DeterminePropEditorForKeyframe.tsx @@ -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 = { + editingTools: IEditingTools['valueType']> + propConfig: PropConfigForType + keyframeValue: PropConfigForType['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, +) { + 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 ( + + {p.displayLabel} + + > + } + propConfig={propConfig} + editingTools={p.editingTools} + keyframeValue={p.keyframeValue} + /> + + ) + } +} diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/DeterminePropEditorForKeyframe/KeyframeSimplePropEditor.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/DeterminePropEditorForKeyframe/KeyframeSimplePropEditor.tsx new file mode 100644 index 0000000..a2b3f4b --- /dev/null +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/DeterminePropEditorForKeyframe/KeyframeSimplePropEditor.tsx @@ -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 + keyframeValue: TPropTypeConfig['valueType'] + SimpleEditorComponent: React.VFC> +} + +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) { + return ( + + + + ) +} + +export default KeyframeSimplePropEditor diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/KeyframeDot.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/KeyframeDot.tsx index 4bb99b4..35a6922 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/KeyframeDot.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/KeyframeDot.tsx @@ -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 = (props) => { const [ref, node] = useRefAndState(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 = (props) => { className={isDragging ? 'beingDragged' : ''} /> + {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'}, () => ( + + + + )) +} + +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(() => { 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) => diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/useTempTransactionEditingTools.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/useTempTransactionEditingTools.tsx new file mode 100644 index 0000000..71820d1 --- /dev/null +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/useTempTransactionEditingTools.tsx @@ -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( + writeTx: (api: ITransactionPrivateApi, value: T) => void, +): IEditingTools { + return useMemo(() => createTempTransactionEditingTools(writeTx), []) +} + +function createTempTransactionEditingTools( + 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() + }, + } +} diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/LengthIndicator/LengthEditorPopover.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/LengthIndicator/LengthEditorPopover.tsx index 4586834..006f5de 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/LengthIndicator/LengthEditorPopover.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/LengthIndicator/LengthEditorPopover.tsx @@ -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} /> diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/LengthIndicator/LengthIndicator.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/LengthIndicator/LengthIndicator.tsx index b92d296..cd3b4ca 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/LengthIndicator/LengthIndicator.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/LengthIndicator/LengthIndicator.tsx @@ -129,6 +129,8 @@ type IProps = { layoutP: Pointer } +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 = ({layoutP}) => { const [nodeRef, node] = useRefAndState(null) const [isDragging] = useDragBulge(node, {layoutP}) const [popoverNode, openPopover, closePopover, isPopoverOpen] = usePopover( - {}, + {debugName: 'LengthIndicator'}, () => { return ( @@ -178,7 +180,9 @@ const LengthIndicator: React.FC = ({layoutP}) => { diff --git a/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/Playhead.tsx b/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/Playhead.tsx index 9f898b5..8a2b10b 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/Playhead.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/Playhead.tsx @@ -188,7 +188,7 @@ const Playhead: React.FC<{layoutP: Pointer}> = ({ const [thumbRef, thumbNode] = useRefAndState(null) const [popoverNode, openPopover, closePopover, isPopoverOpen] = usePopover( - {}, + {debugName: 'Playhead'}, () => { return ( diff --git a/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/PlayheadPositionPopover.tsx b/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/PlayheadPositionPopover.tsx index 9c759a3..492d673 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/PlayheadPositionPopover.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/PlayheadPositionPopover.tsx @@ -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} /> diff --git a/theatre/studio/src/panels/DetailPanel/propEditors/utils/DefaultValueIndicator.tsx b/theatre/studio/src/propEditors/DefaultValueIndicator.tsx similarity index 100% rename from theatre/studio/src/panels/DetailPanel/propEditors/utils/DefaultValueIndicator.tsx rename to theatre/studio/src/propEditors/DefaultValueIndicator.tsx diff --git a/theatre/studio/src/panels/DetailPanel/propEditors/utils/NextPrevKeyframeCursors.tsx b/theatre/studio/src/propEditors/NextPrevKeyframeCursors.tsx similarity index 100% rename from theatre/studio/src/panels/DetailPanel/propEditors/utils/NextPrevKeyframeCursors.tsx rename to theatre/studio/src/propEditors/NextPrevKeyframeCursors.tsx diff --git a/theatre/studio/src/propEditors/simpleEditors/BooleanPropEditor.tsx b/theatre/studio/src/propEditors/simpleEditors/BooleanPropEditor.tsx new file mode 100644 index 0000000..787cd44 --- /dev/null +++ b/theatre/studio/src/propEditors/simpleEditors/BooleanPropEditor.tsx @@ -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) { + const onChange = useCallback( + (el: React.ChangeEvent) => { + editingTools.permanentlySetValue(Boolean(el.target.checked)) + }, + [propConfig, editingTools], + ) + + return +} + +export default BooleanPropEditor diff --git a/theatre/studio/src/propEditors/simpleEditors/ISimplePropEditorReactProps.ts b/theatre/studio/src/propEditors/simpleEditors/ISimplePropEditorReactProps.ts new file mode 100644 index 0000000..c49aa12 --- /dev/null +++ b/theatre/studio/src/propEditors/simpleEditors/ISimplePropEditorReactProps.ts @@ -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, +> = { + propConfig: TPropTypeConfig + editingTools: IEditingTools + value: TPropTypeConfig['valueType'] +} diff --git a/theatre/studio/src/propEditors/simpleEditors/NumberPropEditor.tsx b/theatre/studio/src/propEditors/simpleEditors/NumberPropEditor.tsx new file mode 100644 index 0000000..eff8b46 --- /dev/null +++ b/theatre/studio/src/propEditors/simpleEditors/NumberPropEditor.tsx @@ -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) { + const nudge = useCallback( + (params: {deltaX: number; deltaFraction: number; magnitude: number}) => { + return propConfig.nudgeFn({...params, config: propConfig}) + }, + [propConfig], + ) + + return ( + + ) +} + +export default NumberPropEditor diff --git a/theatre/studio/src/panels/DetailPanel/propEditors/RgbaPropEditor.tsx b/theatre/studio/src/propEditors/simpleEditors/RgbaPropEditor.tsx similarity index 63% rename from theatre/studio/src/panels/DetailPanel/propEditors/RgbaPropEditor.tsx rename to theatre/studio/src/propEditors/simpleEditors/RgbaPropEditor.tsx index 4a5f5cb..e9e43e5 100644 --- a/theatre/studio/src/panels/DetailPanel/propEditors/RgbaPropEditor.tsx +++ b/theatre/studio/src/propEditors/simpleEditors/RgbaPropEditor.tsx @@ -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((props) => ({ +const ColorPreviewPuck = styled.div.attrs((props) => ({ style: { - background: props.background, + // weirdly, rgba2hex is needed to ensure initial render was correct background? + // huge head scratcher. + background: rgba2hex(props.rgbaColor), }, -}))` - height: calc(100% - 12px); +}))` + 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 = ({ - propConfig, - pointerToProp, - obj, -}) => { +function RgbaPropEditor({ + editingTools, + value, +}: ISimplePropEditorReactProps) { const containerRef = useRef(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 ( - + const [popoverNode, openPopover] = usePopover( + {debugName: 'RgbaPropEditor'}, + () => ( + { 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} /> - - ) - }) + + ), + ) return ( - + <> - { openPopover(e, containerRef.current) }} /> = ({ /> {popoverNode} - + ) } diff --git a/theatre/studio/src/propEditors/simpleEditors/StringLiteralPropEditor.tsx b/theatre/studio/src/propEditors/simpleEditors/StringLiteralPropEditor.tsx new file mode 100644 index 0000000..4648a26 --- /dev/null +++ b/theatre/studio/src/propEditors/simpleEditors/StringLiteralPropEditor.tsx @@ -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({ + propConfig, + editingTools, + value, +}: ISimplePropEditorReactProps>) { + const onChange = useCallback( + (val: TLiteralOptions) => { + editingTools.permanentlySetValue(val) + }, + [propConfig, editingTools], + ) + + return propConfig.as === 'menu' ? ( + + ) : ( + + ) +} + +export default StringLiteralPropEditor diff --git a/theatre/studio/src/propEditors/simpleEditors/StringPropEditor.tsx b/theatre/studio/src/propEditors/simpleEditors/StringPropEditor.tsx new file mode 100644 index 0000000..f01d854 --- /dev/null +++ b/theatre/studio/src/propEditors/simpleEditors/StringPropEditor.tsx @@ -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) { + return ( + + ) +} + +export default StringPropEditor diff --git a/theatre/studio/src/propEditors/simpleEditors/simplePropEditorByPropType.ts b/theatre/studio/src/propEditors/simpleEditors/simplePropEditorByPropType.ts new file mode 100644 index 0000000..ca2ca48 --- /dev/null +++ b/theatre/studio/src/propEditors/simpleEditors/simplePropEditorByPropType.ts @@ -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> + > +} diff --git a/theatre/studio/src/panels/DetailPanel/propEditors/utils/useEditingToolsForPrimitiveProp.tsx b/theatre/studio/src/propEditors/useEditingToolsForSimpleProp.tsx similarity index 85% rename from theatre/studio/src/panels/DetailPanel/propEditors/utils/useEditingToolsForPrimitiveProp.tsx rename to theatre/studio/src/propEditors/useEditingToolsForSimpleProp.tsx index f94b067..79cb0d6 100644 --- a/theatre/studio/src/panels/DetailPanel/propEditors/utils/useEditingToolsForPrimitiveProp.tsx +++ b/theatre/studio/src/propEditors/useEditingToolsForSimpleProp.tsx @@ -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 { +import DefaultOrStaticValueIndicator from './DefaultValueIndicator' +import NextPrevKeyframeCursors from './NextPrevKeyframeCursors' + +interface EditingToolsCommon { value: T beingScrubbed: boolean contextMenuItems: Array + /** e.g. `< • >` or `< >` for {@link EditingToolsSequenced} */ controlIndicators: React.ReactElement temporarilySetValue(v: T): void @@ -28,38 +31,50 @@ interface CommonStuff { permanentlySetValue(v: T): void } -interface Default extends CommonStuff { +interface EditingToolsDefault extends EditingToolsCommon { type: 'Default' shade: Shade } -interface Static extends CommonStuff { +interface EditingToolsStatic extends EditingToolsCommon { type: 'Static' shade: Shade } -interface Sequenced extends CommonStuff { +interface EditingToolsSequenced extends EditingToolsCommon { type: 'Sequenced' shade: Shade + /** based on the position of the playhead */ nearbyKeyframes: NearbyKeyframes } -type Stuff = Default | Static | Sequenced +type EditingTools = + | EditingToolsDefault + | EditingToolsStatic + | EditingToolsSequenced -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, obj: SheetObject, - propConfig: PropTypeConfig, -): Stuff { + propConfig: PropTypeConfig_AllSimples, +): EditingTools { 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 = { - ...callbacks, + const common: EditingToolsCommon = { + ...editPropValue, value: final, beingScrubbed, contextMenuItems, @@ -224,7 +235,7 @@ export function useEditingToolsForPrimitiveProp< /> ) - const ret: Sequenced = { + const ret: EditingToolsSequenced = { ...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 = { + const ret: EditingToolsStatic = { ...common, type: 'Static', shade: common.beingScrubbed ? 'Static_BeingScrubbed' : 'Static', @@ -275,7 +286,7 @@ export function useEditingToolsForPrimitiveProp< return ret } - const ret: Default = { + const ret: EditingToolsDefault = { ...common, type: 'Default', shade: 'Default', diff --git a/theatre/studio/src/propEditors/utils/IEditingTools.tsx b/theatre/studio/src/propEditors/utils/IEditingTools.tsx new file mode 100644 index 0000000..4e68ab0 --- /dev/null +++ b/theatre/studio/src/propEditors/utils/IEditingTools.tsx @@ -0,0 +1,5 @@ +export interface IEditingTools { + temporarilySetValue(v: T): void + discardTemporaryValue(): void + permanentlySetValue(v: T): void +} diff --git a/theatre/studio/src/propEditors/utils/PropConfigForType.ts b/theatre/studio/src/propEditors/utils/PropConfigForType.ts new file mode 100644 index 0000000..7d21c6a --- /dev/null +++ b/theatre/studio/src/propEditors/utils/PropConfigForType.ts @@ -0,0 +1,6 @@ +import type {PropTypeConfig} from '@theatre/core/propTypes' + +export type PropConfigForType = Extract< + PropTypeConfig, + {type: K} +> diff --git a/theatre/studio/src/propEditors/utils/getPropTypeByPointer.tsx b/theatre/studio/src/propEditors/utils/getPropTypeByPointer.tsx new file mode 100644 index 0000000..d9c49c6 --- /dev/null +++ b/theatre/studio/src/propEditors/utils/getPropTypeByPointer.tsx @@ -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 +} diff --git a/theatre/studio/src/propEditors/utils/propNameTextCSS.tsx b/theatre/studio/src/propEditors/utils/propNameTextCSS.tsx new file mode 100644 index 0000000..6ce482a --- /dev/null +++ b/theatre/studio/src/propEditors/utils/propNameTextCSS.tsx @@ -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); +` diff --git a/theatre/studio/src/store/stateEditors.ts b/theatre/studio/src/store/stateEditors.ts index 3ea39c4..03843da 100644 --- a/theatre/studio/src/store/stateEditors.ts +++ b/theatre/studio/src/store/stateEditors.ts @@ -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 & { trackId: SequenceTrackId diff --git a/theatre/studio/src/uiComponents/Popover/usePopover.tsx b/theatre/studio/src/uiComponents/Popover/usePopover.tsx index 59a67d9..dd52635 100644 --- a/theatre/studio/src/uiComponents/Popover/usePopover.tsx +++ b/theatre/studio/src/uiComponents/Popover/usePopover.tsx @@ -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({ isOpen: false, }) @@ -38,13 +61,25 @@ export default function usePopover( }) }, []) - const close = useCallback(() => { + const close = useCallback((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( - , + + + , 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 + }, + } + }, + }, + } +} diff --git a/theatre/studio/src/uiComponents/form/BasicSelect.tsx b/theatre/studio/src/uiComponents/form/BasicSelect.tsx index dec6c5e..dde9280 100644 --- a/theatre/studio/src/uiComponents/form/BasicSelect.tsx +++ b/theatre/studio/src/uiComponents/form/BasicSelect.tsx @@ -50,18 +50,24 @@ const Select = styled.select` } ` -const BasicSelect: React.FC<{ - value: string - onChange: (val: string) => void - options: Record +function BasicSelect({ + value, + onChange, + options, + className, +}: { + value: TLiteralOptions + onChange: (val: TLiteralOptions) => void + options: Record className?: string -}> = ({value, onChange, options, className}) => { +}) { const _onChange = useCallback( (el: React.ChangeEvent) => { - onChange(String(el.target.value)) + onChange(String(el.target.value) as TLiteralOptions) }, [onChange], ) + return (