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:
Aria 2023-01-04 13:43:49 +02:00 committed by GitHub
parent f6361e7905
commit feb3ad34b8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 168 additions and 34 deletions

View file

@ -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,

View file

@ -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>>()

View 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

View file

@ -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'