Compact and collapsible compound prop editor
* Create compact vector prop editor * MAke all compound props collapsible * Add collapsed indicator for compound props * Persist collapsed state accross component rerenders * Adjust dom playground to use the new vector prop Co-authored-by: Andrew Prifer <andrew.prifer@gmail.com>
This commit is contained in:
parent
f6361e7905
commit
feb3ad34b8
4 changed files with 168 additions and 34 deletions
|
@ -40,16 +40,20 @@ const boxObjectConfig = {
|
|||
amount: types.number(10, {range: [0, 1000], label: '$'}),
|
||||
}),
|
||||
}),
|
||||
x: types.number(200),
|
||||
y: types.number(200),
|
||||
pos: {
|
||||
x: types.number(200),
|
||||
y: types.number(200),
|
||||
},
|
||||
color: types.rgba({r: 1, g: 0, b: 0, a: 1}),
|
||||
}
|
||||
|
||||
// this can also be inferred with
|
||||
type _State = ShorthandCompoundPropsToInitialValue<typeof boxObjectConfig>
|
||||
type State = {
|
||||
x: number
|
||||
y: number
|
||||
pos: {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
test: string
|
||||
testLiteral: string
|
||||
bool: boolean
|
||||
|
@ -78,8 +82,10 @@ const Box: React.FC<{
|
|||
() =>
|
||||
Object.assign({}, boxObjectConfig, {
|
||||
// give the box initial values offset from each other
|
||||
x: ((id.codePointAt(0) ?? 0) % 15) * 100,
|
||||
y: ((id.codePointAt(0) ?? 0) % 15) * 100,
|
||||
pos: {
|
||||
x: ((id.codePointAt(0) ?? 0) % 15) * 100,
|
||||
y: ((id.codePointAt(0) ?? 0) % 15) * 100,
|
||||
},
|
||||
}),
|
||||
[id],
|
||||
)
|
||||
|
@ -95,7 +101,7 @@ const Box: React.FC<{
|
|||
|
||||
useLayoutEffect(() => {
|
||||
const unsubscribeFromChanges = onChange(obj.props, (newValues) => {
|
||||
boxRef.current.style.transform = `translate(${newValues.x}px, ${newValues.y}px)`
|
||||
boxRef.current.style.transform = `translate(${newValues.pos.x}px, ${newValues.pos.y}px)`
|
||||
preRef.current.innerText = JSON.stringify(newValues, null, 2)
|
||||
colorRef.current.style.background = newValues.color.toString()
|
||||
})
|
||||
|
@ -104,12 +110,12 @@ const Box: React.FC<{
|
|||
|
||||
const dragOpts = useMemo((): UseDragOpts => {
|
||||
let scrub: IScrub | undefined
|
||||
let initial: typeof obj.value
|
||||
let initial: typeof obj.value.pos
|
||||
let firstOnDragCalled = false
|
||||
return {
|
||||
onDragStart() {
|
||||
scrub = studio.scrub()
|
||||
initial = obj.value
|
||||
initial = obj.value.pos
|
||||
firstOnDragCalled = false
|
||||
},
|
||||
onDrag(x, y) {
|
||||
|
@ -118,7 +124,7 @@ const Box: React.FC<{
|
|||
firstOnDragCalled = true
|
||||
}
|
||||
scrub!.capture(({set}) => {
|
||||
set(obj.props, {
|
||||
set(obj.props.pos, {
|
||||
...initial,
|
||||
x: x + initial.x,
|
||||
y: y + initial.y,
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import type {PropTypeConfig_Compound} from '@theatre/core/propTypes'
|
||||
import type {
|
||||
PropTypeConfig_Compound,
|
||||
PropTypeConfig_Number,
|
||||
} from '@theatre/core/propTypes'
|
||||
import {isPropConfigComposite} from '@theatre/shared/propTypes/utils'
|
||||
import type {$FixMe} from '@theatre/shared/utils/types'
|
||||
import {getPointerParts} from '@theatre/dataverse'
|
||||
import {Box, getPointerParts} from '@theatre/dataverse'
|
||||
import type {Pointer} from '@theatre/dataverse'
|
||||
import last from 'lodash-es/last'
|
||||
import {darken, transparentize} from 'polished'
|
||||
import React, {useMemo} from 'react'
|
||||
import React, {useLayoutEffect, useMemo} from 'react'
|
||||
import styled from 'styled-components'
|
||||
import {rowIndentationFormulaCSS} from '@theatre/studio/panels/DetailPanel/DeterminePropEditorForDetail/rowIndentationFormulaCSS'
|
||||
import {propNameTextCSS} from '@theatre/studio/propEditors/utils/propNameTextCSS'
|
||||
|
@ -20,11 +23,18 @@ import type {PropHighlighted} from '@theatre/studio/panels/SequenceEditorPanel/w
|
|||
import {whatPropIsHighlighted} from '@theatre/studio/panels/SequenceEditorPanel/whatPropIsHighlighted'
|
||||
import {deriver} from '@theatre/studio/utils/derive-utils'
|
||||
import {getDetailRowHighlightBackground} from './getDetailRowHighlightBackground'
|
||||
import NumberPropEditor from '@theatre/studio/propEditors/simpleEditors/NumberPropEditor'
|
||||
import type {IDetailSimplePropEditorProps} from './DetailSimplePropEditor'
|
||||
import {useEditingToolsForSimplePropInDetailsPanel} from '@theatre/studio/propEditors/useEditingToolsForSimpleProp'
|
||||
import {EllipsisFill} from '@theatre/studio/uiComponents/icons'
|
||||
import {usePrism} from '@theatre/react'
|
||||
import {val} from '@theatre/dataverse'
|
||||
|
||||
const Container = styled.div`
|
||||
--step: 15px;
|
||||
--left-pad: 15px;
|
||||
${pointerEventsAutoInNormalMode};
|
||||
--right-width: 60%;
|
||||
`
|
||||
|
||||
const Header = deriver(styled.div<{isHighlighted: PropHighlighted}>`
|
||||
|
@ -40,6 +50,7 @@ const Padding = styled.div`
|
|||
padding-left: ${rowIndentationFormulaCSS};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: calc(100% - var(--right-width));
|
||||
`
|
||||
|
||||
const PropName = deriver(styled.div<{isHighlighted: PropHighlighted}>`
|
||||
|
@ -48,6 +59,7 @@ const PropName = deriver(styled.div<{isHighlighted: PropHighlighted}>`
|
|||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
user-select: none;
|
||||
&:hover {
|
||||
color: white;
|
||||
|
@ -63,6 +75,51 @@ const SubProps = styled.div<{depth: number; lastSubIsComposite: boolean}>`
|
|||
/* padding: ${(props) => (props.lastSubIsComposite ? 0 : '4px')} 0; */
|
||||
`
|
||||
|
||||
const isVectorProp = (propConfig: PropTypeConfig_Compound<any>) => {
|
||||
const props = Object.entries(propConfig.props)
|
||||
|
||||
return (
|
||||
props.length <= 3 &&
|
||||
props.every(
|
||||
([name, conf]) =>
|
||||
conf.type === 'number' && ['x', 'y', 'z'].includes(name),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function VectorComponentEditor<TPropTypeConfig extends PropTypeConfig_Number>({
|
||||
propConfig,
|
||||
pointerToProp,
|
||||
obj,
|
||||
SimpleEditorComponent: EditorComponent,
|
||||
}: IDetailSimplePropEditorProps<TPropTypeConfig>) {
|
||||
const editingTools = useEditingToolsForSimplePropInDetailsPanel(
|
||||
pointerToProp,
|
||||
obj,
|
||||
propConfig,
|
||||
)
|
||||
|
||||
return (
|
||||
<NumberPropEditor
|
||||
editingTools={editingTools}
|
||||
propConfig={propConfig}
|
||||
value={editingTools.value}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const InputContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: stretch;
|
||||
padding: 0 8px 0 2px;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
width: var(--right-width);
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
`
|
||||
|
||||
export type ICompoundPropDetailEditorProps<
|
||||
TPropTypeConfig extends PropTypeConfig_Compound<any>,
|
||||
> = {
|
||||
|
@ -114,6 +171,24 @@ function DetailCompoundPropEditor<
|
|||
[pointerToProp],
|
||||
)
|
||||
|
||||
const globalPointerPath = `${obj.address.projectId},${obj.address.sheetId},${
|
||||
obj.address.sheetInstanceId
|
||||
},${obj.address.objectKey},${getPointerParts(pointerToProp).path.join()}`
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!collapsedMap.has(globalPointerPath)) {
|
||||
collapsedMap.set(globalPointerPath, new Box(isVectorProp(propConfig)))
|
||||
}
|
||||
}, [])
|
||||
|
||||
const box = collapsedMap.get(globalPointerPath)
|
||||
|
||||
const isCollapsed = usePrism(() => {
|
||||
const box = collapsedMap.get(globalPointerPath)
|
||||
|
||||
return box ? val(box.derivation) : isVectorProp(propConfig)
|
||||
}, [box])
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{contextMenu}
|
||||
|
@ -127,34 +202,65 @@ function DetailCompoundPropEditor<
|
|||
<PropName
|
||||
isHighlighted={isPropHighlightedD}
|
||||
ref={propNameContainerRef}
|
||||
onClick={() => {
|
||||
box?.set(!box.get())
|
||||
}}
|
||||
>
|
||||
{propName || 'Props'}
|
||||
<span>{propName || 'Props'}</span>
|
||||
{!isVectorProp(propConfig) && isCollapsed && (
|
||||
<EllipsisFill
|
||||
width={24}
|
||||
height={24}
|
||||
style={{
|
||||
transform: 'translateY(2px)',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</PropName>
|
||||
</Padding>
|
||||
{isVectorProp(propConfig) && isCollapsed && (
|
||||
<InputContainer>
|
||||
{[...allSubs].map(([subPropKey, subPropConfig]) => {
|
||||
return (
|
||||
<VectorComponentEditor
|
||||
key={'prop-' + subPropKey}
|
||||
// @ts-ignore
|
||||
propConfig={subPropConfig}
|
||||
pointerToProp={pointerToProp[subPropKey] as Pointer<$FixMe>}
|
||||
obj={obj}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</InputContainer>
|
||||
)}
|
||||
</Header>
|
||||
|
||||
<SubProps
|
||||
// @ts-ignore
|
||||
style={{'--depth': visualIndentation}}
|
||||
depth={visualIndentation}
|
||||
lastSubIsComposite={lastSubPropIsComposite}
|
||||
>
|
||||
{[...nonCompositeSubs, ...compositeSubs].map(
|
||||
([subPropKey, subPropConfig]) => {
|
||||
return (
|
||||
<DeterminePropEditorForDetail
|
||||
key={'prop-' + subPropKey}
|
||||
propConfig={subPropConfig}
|
||||
pointerToProp={pointerToProp[subPropKey] as Pointer<$FixMe>}
|
||||
obj={obj}
|
||||
visualIndentation={visualIndentation + 1}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)}
|
||||
</SubProps>
|
||||
{!isCollapsed && (
|
||||
<SubProps
|
||||
// @ts-ignore
|
||||
style={{'--depth': visualIndentation}}
|
||||
depth={visualIndentation}
|
||||
lastSubIsComposite={lastSubPropIsComposite}
|
||||
>
|
||||
{[...nonCompositeSubs, ...compositeSubs].map(
|
||||
([subPropKey, subPropConfig]) => {
|
||||
return (
|
||||
<DeterminePropEditorForDetail
|
||||
key={'prop-' + subPropKey}
|
||||
propConfig={subPropConfig}
|
||||
pointerToProp={pointerToProp[subPropKey] as Pointer<$FixMe>}
|
||||
obj={obj}
|
||||
visualIndentation={visualIndentation + 1}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)}
|
||||
</SubProps>
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(DetailCompoundPropEditor)
|
||||
|
||||
const collapsedMap = new Map<string, Box<boolean>>()
|
||||
|
|
21
theatre/studio/src/uiComponents/icons/EllipsisFill.tsx
Normal file
21
theatre/studio/src/uiComponents/icons/EllipsisFill.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import * as React from 'react'
|
||||
|
||||
function EllipsisFill(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
width={16}
|
||||
height={16}
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M10.667 8a1.333 1.333 0 112.666 0 1.333 1.333 0 01-2.666 0zm-4 0a1.333 1.333 0 112.666 0 1.333 1.333 0 01-2.666 0zm-4 0a1.333 1.333 0 112.666 0 1.333 1.333 0 01-2.666 0z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default EllipsisFill
|
|
@ -17,3 +17,4 @@ export {default as Package} from './Package'
|
|||
export {default as Bell} from './Bell'
|
||||
export {default as Trash} from './Trash'
|
||||
export {default as AddImage} from './AddImage'
|
||||
export {default as EllipsisFill} from './EllipsisFill'
|
||||
|
|
Loading…
Reference in a new issue