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: '$'}),
|
amount: types.number(10, {range: [0, 1000], label: '$'}),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
pos: {
|
||||||
x: types.number(200),
|
x: types.number(200),
|
||||||
y: types.number(200),
|
y: types.number(200),
|
||||||
|
},
|
||||||
color: types.rgba({r: 1, g: 0, b: 0, a: 1}),
|
color: types.rgba({r: 1, g: 0, b: 0, a: 1}),
|
||||||
}
|
}
|
||||||
|
|
||||||
// this can also be inferred with
|
// this can also be inferred with
|
||||||
type _State = ShorthandCompoundPropsToInitialValue<typeof boxObjectConfig>
|
type _State = ShorthandCompoundPropsToInitialValue<typeof boxObjectConfig>
|
||||||
type State = {
|
type State = {
|
||||||
|
pos: {
|
||||||
x: number
|
x: number
|
||||||
y: number
|
y: number
|
||||||
|
}
|
||||||
test: string
|
test: string
|
||||||
testLiteral: string
|
testLiteral: string
|
||||||
bool: boolean
|
bool: boolean
|
||||||
|
@ -78,8 +82,10 @@ const Box: React.FC<{
|
||||||
() =>
|
() =>
|
||||||
Object.assign({}, boxObjectConfig, {
|
Object.assign({}, boxObjectConfig, {
|
||||||
// give the box initial values offset from each other
|
// give the box initial values offset from each other
|
||||||
|
pos: {
|
||||||
x: ((id.codePointAt(0) ?? 0) % 15) * 100,
|
x: ((id.codePointAt(0) ?? 0) % 15) * 100,
|
||||||
y: ((id.codePointAt(0) ?? 0) % 15) * 100,
|
y: ((id.codePointAt(0) ?? 0) % 15) * 100,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
[id],
|
[id],
|
||||||
)
|
)
|
||||||
|
@ -95,7 +101,7 @@ const Box: React.FC<{
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const unsubscribeFromChanges = onChange(obj.props, (newValues) => {
|
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)
|
preRef.current.innerText = JSON.stringify(newValues, null, 2)
|
||||||
colorRef.current.style.background = newValues.color.toString()
|
colorRef.current.style.background = newValues.color.toString()
|
||||||
})
|
})
|
||||||
|
@ -104,12 +110,12 @@ const Box: React.FC<{
|
||||||
|
|
||||||
const dragOpts = useMemo((): UseDragOpts => {
|
const dragOpts = useMemo((): UseDragOpts => {
|
||||||
let scrub: IScrub | undefined
|
let scrub: IScrub | undefined
|
||||||
let initial: typeof obj.value
|
let initial: typeof obj.value.pos
|
||||||
let firstOnDragCalled = false
|
let firstOnDragCalled = false
|
||||||
return {
|
return {
|
||||||
onDragStart() {
|
onDragStart() {
|
||||||
scrub = studio.scrub()
|
scrub = studio.scrub()
|
||||||
initial = obj.value
|
initial = obj.value.pos
|
||||||
firstOnDragCalled = false
|
firstOnDragCalled = false
|
||||||
},
|
},
|
||||||
onDrag(x, y) {
|
onDrag(x, y) {
|
||||||
|
@ -118,7 +124,7 @@ const Box: React.FC<{
|
||||||
firstOnDragCalled = true
|
firstOnDragCalled = true
|
||||||
}
|
}
|
||||||
scrub!.capture(({set}) => {
|
scrub!.capture(({set}) => {
|
||||||
set(obj.props, {
|
set(obj.props.pos, {
|
||||||
...initial,
|
...initial,
|
||||||
x: x + initial.x,
|
x: x + initial.x,
|
||||||
y: y + initial.y,
|
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 {isPropConfigComposite} from '@theatre/shared/propTypes/utils'
|
||||||
import type {$FixMe} from '@theatre/shared/utils/types'
|
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 type {Pointer} from '@theatre/dataverse'
|
||||||
import last from 'lodash-es/last'
|
import last from 'lodash-es/last'
|
||||||
import {darken, transparentize} from 'polished'
|
import {darken, transparentize} from 'polished'
|
||||||
import React, {useMemo} from 'react'
|
import React, {useLayoutEffect, useMemo} from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import {rowIndentationFormulaCSS} from '@theatre/studio/panels/DetailPanel/DeterminePropEditorForDetail/rowIndentationFormulaCSS'
|
import {rowIndentationFormulaCSS} from '@theatre/studio/panels/DetailPanel/DeterminePropEditorForDetail/rowIndentationFormulaCSS'
|
||||||
import {propNameTextCSS} from '@theatre/studio/propEditors/utils/propNameTextCSS'
|
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 {whatPropIsHighlighted} from '@theatre/studio/panels/SequenceEditorPanel/whatPropIsHighlighted'
|
||||||
import {deriver} from '@theatre/studio/utils/derive-utils'
|
import {deriver} from '@theatre/studio/utils/derive-utils'
|
||||||
import {getDetailRowHighlightBackground} from './getDetailRowHighlightBackground'
|
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`
|
const Container = styled.div`
|
||||||
--step: 15px;
|
--step: 15px;
|
||||||
--left-pad: 15px;
|
--left-pad: 15px;
|
||||||
${pointerEventsAutoInNormalMode};
|
${pointerEventsAutoInNormalMode};
|
||||||
|
--right-width: 60%;
|
||||||
`
|
`
|
||||||
|
|
||||||
const Header = deriver(styled.div<{isHighlighted: PropHighlighted}>`
|
const Header = deriver(styled.div<{isHighlighted: PropHighlighted}>`
|
||||||
|
@ -40,6 +50,7 @@ const Padding = styled.div`
|
||||||
padding-left: ${rowIndentationFormulaCSS};
|
padding-left: ${rowIndentationFormulaCSS};
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
width: calc(100% - var(--right-width));
|
||||||
`
|
`
|
||||||
|
|
||||||
const PropName = deriver(styled.div<{isHighlighted: PropHighlighted}>`
|
const PropName = deriver(styled.div<{isHighlighted: PropHighlighted}>`
|
||||||
|
@ -48,6 +59,7 @@ const PropName = deriver(styled.div<{isHighlighted: PropHighlighted}>`
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
&:hover {
|
&:hover {
|
||||||
color: white;
|
color: white;
|
||||||
|
@ -63,6 +75,51 @@ const SubProps = styled.div<{depth: number; lastSubIsComposite: boolean}>`
|
||||||
/* padding: ${(props) => (props.lastSubIsComposite ? 0 : '4px')} 0; */
|
/* 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<
|
export type ICompoundPropDetailEditorProps<
|
||||||
TPropTypeConfig extends PropTypeConfig_Compound<any>,
|
TPropTypeConfig extends PropTypeConfig_Compound<any>,
|
||||||
> = {
|
> = {
|
||||||
|
@ -114,6 +171,24 @@ function DetailCompoundPropEditor<
|
||||||
[pointerToProp],
|
[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 (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
{contextMenu}
|
{contextMenu}
|
||||||
|
@ -127,12 +202,40 @@ function DetailCompoundPropEditor<
|
||||||
<PropName
|
<PropName
|
||||||
isHighlighted={isPropHighlightedD}
|
isHighlighted={isPropHighlightedD}
|
||||||
ref={propNameContainerRef}
|
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>
|
</PropName>
|
||||||
</Padding>
|
</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>
|
</Header>
|
||||||
|
|
||||||
|
{!isCollapsed && (
|
||||||
<SubProps
|
<SubProps
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
style={{'--depth': visualIndentation}}
|
style={{'--depth': visualIndentation}}
|
||||||
|
@ -153,8 +256,11 @@ function DetailCompoundPropEditor<
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
</SubProps>
|
</SubProps>
|
||||||
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default React.memo(DetailCompoundPropEditor)
|
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 Bell} from './Bell'
|
||||||
export {default as Trash} from './Trash'
|
export {default as Trash} from './Trash'
|
||||||
export {default as AddImage} from './AddImage'
|
export {default as AddImage} from './AddImage'
|
||||||
|
export {default as EllipsisFill} from './EllipsisFill'
|
||||||
|
|
Loading…
Reference in a new issue