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:
parent
e140bb6fc4
commit
2324218453
47 changed files with 895 additions and 516 deletions
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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}
|
|
@ -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>}
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
}>
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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) =>
|
||||
|
|
|
@ -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()
|
||||
},
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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' : ''}
|
||||
>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
|
@ -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']
|
||||
}
|
|
@ -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
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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
|
|
@ -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>>
|
||||
>
|
||||
}
|
|
@ -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',
|
5
theatre/studio/src/propEditors/utils/IEditingTools.tsx
Normal file
5
theatre/studio/src/propEditors/utils/IEditingTools.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
export interface IEditingTools<T> {
|
||||
temporarilySetValue(v: T): void
|
||||
discardTemporaryValue(): void
|
||||
permanentlySetValue(v: T): void
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import type {PropTypeConfig} from '@theatre/core/propTypes'
|
||||
|
||||
export type PropConfigForType<K extends PropTypeConfig['type']> = Extract<
|
||||
PropTypeConfig,
|
||||
{type: K}
|
||||
>
|
|
@ -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
|
||||
}
|
8
theatre/studio/src/propEditors/utils/propNameTextCSS.tsx
Normal file
8
theatre/studio/src/propEditors/utils/propNameTextCSS.tsx
Normal 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);
|
||||
`
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
<TooltipWrapper
|
||||
children={render}
|
||||
target={state.target}
|
||||
onClickOutside={onClickOutside}
|
||||
onPointerOutside={onPointerOutside}
|
||||
constraints={opts.constraints}
|
||||
/>,
|
||||
<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
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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],
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue