diff --git a/theatre/shared/src/utils/ids.ts b/theatre/shared/src/utils/ids.ts index 4c5528d..cdd09e4 100644 --- a/theatre/shared/src/utils/ids.ts +++ b/theatre/shared/src/utils/ids.ts @@ -1,3 +1,6 @@ +import type SheetObject from '@theatre/core/sheetObjects/SheetObject' +import type {PathToProp} from './addresses' +import stableValueHash from './stableJsonStringify' import {nanoid as generateNonSecure} from 'nanoid/non-secure' import type {Nominal} from './Nominal' @@ -18,6 +21,33 @@ export type PaneInstanceId = Nominal<'PaneInstanceId'> export type SequenceTrackId = Nominal<'SequenceTrackId'> export type SequenceMarkerId = Nominal<'SequenceMarkerId'> export type ObjectAddressKey = Nominal<'ObjectAddressKey'> + +/** + * Studio consistent identifier for identifying any individual item on a sheet + * including a SheetObject, a SheetObject's prop, etc. + * + * See {@link createStudioSheetItemKey}. + * + * @remarks + * This is the kind of type which should not find itself in Project state, + * due to how it is lossy in the case of additional model layers being introduced. + * e.g. When we introduce an extra layer of multiple sequences per sheet, + * all the {@link StudioSheetItemKey}s will have different generated values, + * because they'll have additional information (the "sequence id"). This means + * that all data attached to those item keys will become detached. + * + * This kind of constraint might be mitigated by a sort of migrations ability, + * but for the most part it's just going to be easier to try not using + * {@link StudioSheetItemKey} for any data that needs to stick around after + * version updates to Theatre. + * + * Alternatively, if you did want some kind of universal identifier for any item + * that can be persisted and survive project model changes, it's probably going + * to be easier to simply generate a unique id for all items you want to use in + * this way, and don't do any of this concatenating/JSON.stringify "hashing" + * stuff. + */ +export type StudioSheetItemKey = Nominal<'StudioSheetItemKey'> /** UI panels can contain a {@link PaneInstanceId} or something else. */ export type UIPanelId = Nominal<'UIPanelId'> @@ -32,3 +62,24 @@ export function asSequenceTrackId(s: string): SequenceTrackId { export function generateSequenceMarkerId(): SequenceMarkerId { return generateNonSecure(10) as SequenceMarkerId } + +/** + * This will not necessarily maintain consistent key values if any + * versioning happens where something needs to + */ +export const createStudioSheetItemKey = { + forSheetObject(obj: SheetObject): StudioSheetItemKey { + return stableValueHash({ + o: obj.address.objectKey, + }) as StudioSheetItemKey + }, + forSheetObjectProp( + obj: SheetObject, + pathToProp: PathToProp, + ): StudioSheetItemKey { + return stableValueHash({ + o: obj.address.objectKey, + p: pathToProp, + }) as StudioSheetItemKey + }, +} diff --git a/theatre/studio/src/panels/DetailPanel/DeterminePropEditorForDetail/SingleRowPropEditor.tsx b/theatre/studio/src/panels/DetailPanel/DeterminePropEditorForDetail/SingleRowPropEditor.tsx index 7aca78a..2ecbd0e 100644 --- a/theatre/studio/src/panels/DetailPanel/DeterminePropEditorForDetail/SingleRowPropEditor.tsx +++ b/theatre/studio/src/panels/DetailPanel/DeterminePropEditorForDetail/SingleRowPropEditor.tsx @@ -39,7 +39,7 @@ export const rowBg = css` } ` -const Row = styled.div` +const LeftRow = styled.div` display: flex; height: 30px; justify-content: flex-start; @@ -126,7 +126,7 @@ export function SingleRowPropEditor({ }) return ( - + {contextMenu} {editingTools.controlIndicators} @@ -142,6 +142,6 @@ export function SingleRowPropEditor({ {children} - + ) } diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/AnyCompositeRow.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/AnyCompositeRow.tsx index 33ed8be..d545121 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/AnyCompositeRow.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/AnyCompositeRow.tsx @@ -5,6 +5,8 @@ import React from 'react' import {HiOutlineChevronRight} from 'react-icons/all' import styled from 'styled-components' import {propNameTextCSS} from '@theatre/studio/propEditors/utils/propNameTextCSS' +import type {ICollapsableItem} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/useSequenceEditorCollapsable' +import {useVal} from '@theatre/react' export const Container = styled.li<{depth: number}>` --depth: ${(props) => props.depth}; @@ -21,7 +23,7 @@ const Header = styled(BaseHeader)<{ isSelectable: boolean isSelected: boolean }>` - padding-left: calc(16px + var(--depth) * 20px); + padding-left: calc(8px + var(--depth) * 20px); display: flex; align-items: stretch; @@ -42,14 +44,21 @@ const Head_Label = styled.span` flex-wrap: nowrap; ` -const Head_Icon = styled.span<{isOpen: boolean}>` +const Head_Icon = styled.span<{isCollapsed: boolean}>` width: 12px; - margin-right: 8px; + padding: 8px; font-size: 9px; display: flex; align-items: center; - transform: rotateZ(${(props) => (props.isOpen ? 90 : 0)}deg); + transition: transform 0.05s ease-out, color 0.1s ease-out; + transform: rotateZ(${(props) => (props.isCollapsed ? 0 : 90)}deg); + color: #66686a; + + &:hover { + transform: rotateZ(${(props) => (props.isCollapsed ? 15 : 75)}deg); + color: #c0c4c9; + } ` const Children = styled.ul` @@ -64,8 +73,18 @@ const AnyCompositeRow: React.FC<{ toggleSelect?: VoidFn isSelected?: boolean isSelectable?: boolean -}> = ({leaf, label, children, isSelectable, isSelected, toggleSelect}) => { + collapsable?: ICollapsableItem +}> = ({ + leaf, + label, + children, + isSelectable, + isSelected, + toggleSelect, + collapsable, +}) => { const hasChildren = Array.isArray(children) && children.length > 0 + const isCollapsed = useVal(collapsable?.isCollapsed) ?? false return ( @@ -78,12 +97,15 @@ const AnyCompositeRow: React.FC<{ onClick={toggleSelect} isEven={leaf.n % 2 === 0} > - + collapsable?.toggleCollapsed()} + > {label} - {hasChildren && {children}} + {hasChildren && !isCollapsed && {children}} ) } diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/PropWithChildrenRow.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/PropWithChildrenRow.tsx index d357f05..9869c03 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/PropWithChildrenRow.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/PropWithChildrenRow.tsx @@ -4,9 +4,10 @@ import type { } from '@theatre/studio/panels/SequenceEditorPanel/layout/tree' import {usePrism} from '@theatre/react' import React from 'react' -import styled from 'styled-components' import AnyCompositeRow from './AnyCompositeRow' import PrimitivePropRow from './PrimitivePropRow' +import {useSequenceEditorCollapsable} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/useSequenceEditorCollapsable' +import {createStudioSheetItemKey} from '@theatre/shared/utils/ids' export const decideRowByPropType = ( leaf: SequenceEditorTree_PropWithChildren | SequenceEditorTree_PrimitiveProp, @@ -23,16 +24,22 @@ export const decideRowByPropType = ( /> ) -const Container = styled.div`` - -const PropWithChildrenRow: React.FC<{ +const PropWithChildrenRow: React.VFC<{ leaf: SequenceEditorTree_PropWithChildren }> = ({leaf}) => { + const collapsable = useSequenceEditorCollapsable( + createStudioSheetItemKey.forSheetObjectProp( + leaf.sheetObject, + leaf.pathToProp, + ), + ) + return usePrism(() => { return ( {leaf.children.map((propLeaf) => decideRowByPropType(propLeaf))} diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/SheetObjectRow.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/SheetObjectRow.tsx index 50cf338..7072ca5 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/SheetObjectRow.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/SheetObjectRow.tsx @@ -1,22 +1,29 @@ import type {SequenceEditorTree_SheetObject} from '@theatre/studio/panels/SequenceEditorPanel/layout/tree' import {usePrism} from '@theatre/react' import React from 'react' -import styled from 'styled-components' -import CompoundRow from './AnyCompositeRow' +import AnyCompositeRow from './AnyCompositeRow' import {decideRowByPropType} from './PropWithChildrenRow' +import {useSequenceEditorCollapsable} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/useSequenceEditorCollapsable' +import {createStudioSheetItemKey} from '@theatre/shared/utils/ids' -const Container = styled.div`` - -const SheetObjectRow: React.FC<{ +const LeftSheetObjectRow: React.VFC<{ leaf: SequenceEditorTree_SheetObject }> = ({leaf}) => { + const collapsable = useSequenceEditorCollapsable( + createStudioSheetItemKey.forSheetObject(leaf.sheetObject), + ) + return usePrism(() => { return ( - + {leaf.children.map((leaf) => decideRowByPropType(leaf))} - + ) }, [leaf]) } -export default SheetObjectRow +export default LeftSheetObjectRow diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/SheetRow.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/SheetRow.tsx index 4845e48..49c9b91 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/SheetRow.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/SheetRow.tsx @@ -1,7 +1,7 @@ import type {SequenceEditorTree_Sheet} from '@theatre/studio/panels/SequenceEditorPanel/layout/tree' import {usePrism} from '@theatre/react' import React from 'react' -import SheetObjectRow from './SheetObjectRow' +import LeftSheetObjectRow from './SheetObjectRow' const SheetRow: React.VFC<{ leaf: SequenceEditorTree_Sheet @@ -10,7 +10,7 @@ const SheetRow: React.VFC<{ return ( <> {leaf.children.map((sheetObjectLeaf) => ( - diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/PrimitivePropRow.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/PrimitivePropRow.tsx index bd51773..ce017e1 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/PrimitivePropRow.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/PrimitivePropRow.tsx @@ -6,7 +6,7 @@ import type {Pointer} from '@theatre/dataverse' import {val} from '@theatre/dataverse' import React from 'react' import KeyframedTrack from './BasicKeyframedTrack/BasicKeyframedTrack' -import Row from './Row' +import RightRow from './Row' const PrimitivePropRow: React.FC<{ leaf: SequenceEditorTree_PrimitiveProp @@ -27,13 +27,13 @@ const PrimitivePropRow: React.FC<{ console.error( `trackData type ${trackData?.type} is not yet supported on the sequence editor`, ) - return }> + return }> } else { const node = ( ) - return + return } }, [leaf, layoutP]) } diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/PropWithChildrenRow.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/PropWithChildrenRow.tsx index 004af33..2680d33 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/PropWithChildrenRow.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/PropWithChildrenRow.tsx @@ -7,7 +7,9 @@ import {usePrism} from '@theatre/react' import type {Pointer} from '@theatre/dataverse' import React from 'react' import PrimitivePropRow from './PrimitivePropRow' -import Row from './Row' +import RightRow from './Row' +import {useSequenceEditorCollapsable} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/useSequenceEditorCollapsable' +import {createStudioSheetItemKey} from '@theatre/shared/utils/ids' export const decideRowByPropType = ( leaf: SequenceEditorTree_PropWithChildren | SequenceEditorTree_PrimitiveProp, @@ -27,21 +29,26 @@ export const decideRowByPropType = ( /> ) -const PropWithChildrenRow: React.FC<{ +const PropWithChildrenRow: React.VFC<{ leaf: SequenceEditorTree_PropWithChildren layoutP: Pointer }> = ({leaf, layoutP}) => { + const collapsable = useSequenceEditorCollapsable( + createStudioSheetItemKey.forSheetObjectProp( + leaf.sheetObject, + leaf.pathToProp, + ), + ) + return usePrism(() => { const node =
return ( - + {leaf.children.map((propLeaf) => decideRowByPropType(propLeaf, layoutP), )} - + ) }, [leaf, layoutP]) } - -export default PropWithChildrenRow diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/Row.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/Row.tsx index e8da496..12e3b9e 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/Row.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/Row.tsx @@ -1,6 +1,8 @@ import type {SequenceEditorTree_Row} from '@theatre/studio/panels/SequenceEditorPanel/layout/tree' import React from 'react' import styled from 'styled-components' +import type {ICollapsableItem} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/useSequenceEditorCollapsable' +import {useVal} from '@theatre/react' const Container = styled.li<{}>` margin: 0; @@ -35,11 +37,13 @@ const Children = styled.ul` list-style: none; ` -const Row: React.FC<{ +const RightRow: React.FC<{ leaf: SequenceEditorTree_Row node: React.ReactElement -}> = ({leaf, children, node}) => { + collapsable?: ICollapsableItem +}> = ({leaf, children, node, collapsable}) => { const hasChildren = Array.isArray(children) && children.length > 0 + const isCollapsed = useVal(collapsable?.isCollapsed) ?? false return ( @@ -49,9 +53,9 @@ const Row: React.FC<{ > {node} - {hasChildren && {children}} + {hasChildren && !isCollapsed && {children}} ) } -export default Row +export default RightRow diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/SheetObjectRow.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/SheetObjectRow.tsx index 2baa343..0772276 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/SheetObjectRow.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/SheetObjectRow.tsx @@ -3,24 +3,26 @@ import type {SequenceEditorTree_SheetObject} from '@theatre/studio/panels/Sequen import {usePrism} from '@theatre/react' import type {Pointer} from '@theatre/dataverse' import React from 'react' -import styled from 'styled-components' import {decideRowByPropType} from './PropWithChildrenRow' -import Row from './Row' +import RightRow from './Row' +import {useSequenceEditorCollapsable} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/useSequenceEditorCollapsable' +import {createStudioSheetItemKey} from '@theatre/shared/utils/ids' -const Container = styled.div`` - -const SheetObjectRow: React.FC<{ +const RightSheetObjectRow: React.VFC<{ leaf: SequenceEditorTree_SheetObject layoutP: Pointer }> = ({leaf, layoutP}) => { + const collapsable = useSequenceEditorCollapsable( + createStudioSheetItemKey.forSheetObject(leaf.sheetObject), + ) return usePrism(() => { const node =
return ( - + {leaf.children.map((leaf) => decideRowByPropType(leaf, layoutP))} - + ) }, [leaf, layoutP]) } -export default SheetObjectRow +export default RightSheetObjectRow diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/SheetRow.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/SheetRow.tsx index ba13d95..ae1f95d 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/SheetRow.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/SheetRow.tsx @@ -3,7 +3,7 @@ import type {SequenceEditorTree_Sheet} from '@theatre/studio/panels/SequenceEdit import {usePrism} from '@theatre/react' import type {Pointer} from '@theatre/dataverse' import React from 'react' -import SheetObjectRow from './SheetObjectRow' +import RightSheetObjectRow from './SheetObjectRow' const SheetRow: React.FC<{ leaf: SequenceEditorTree_Sheet @@ -13,7 +13,7 @@ const SheetRow: React.FC<{ return ( <> {leaf.children.map((sheetObjectLeaf) => ( - collapsableContext.getCollapsable(sheetItemKey), + [sheetItemKey, collapsableContext], + ) +} + +/** + * Get this via {@link useSequenceEditorCollapsable} + */ +export type ICollapsableItem = { + isCollapsed: IDerivation + toggleCollapsed(): void +} +type ICollapsableContext = { + getCollapsable(sheetItemKey: StudioSheetItemKey): ICollapsableItem +} +const CollapsableContext = React.createContext(null!) +const ProviderChildrenMemo: React.FC<{}> = React.memo(({children}) => ( + <>{children} +)) + +/** + * Provide a context for managing collapsable items + * which are useable from {@link useSequenceEditorCollapsable}. + */ +export function ProvideCollapsable( + props: React.PropsWithChildren<{ + sheetId: SheetId + layoutP: Pointer + }>, +) { + const contextValue = usePrism((): ICollapsableContext => { + const studio = getStudio() + const sheetAddress = val(props.layoutP.sheet.address) + const collapsableItemsSetP = + getStudio().atomP.ahistoric.projects.stateByProjectId[ + sheetAddress.projectId + ].stateBySheetId[sheetAddress.sheetId].sequence + .sequenceEditorCollapsableItems + const setIsCollapsed = prism.memo( + 'setIsCollapsed', + () => { + return function setIsCollapsed( + studioSheetItemKey: StudioSheetItemKey, + isCollapsed: boolean, + ): void { + studio.transaction(({stateEditors}) => { + stateEditors.studio.ahistoric.projects.stateByProjectId.stateBySheetId.sequence.sequenceEditorCollapsableItems.set( + {...sheetAddress, studioSheetItemKey, isCollapsed}, + ) + }) + } + }, + [sheetAddress], + ) + return { + getCollapsable(itemId) { + const isCollapsedD = valueDerivation( + collapsableItemsSetP.byId[itemId].isCollapsed, + ).map((value) => value ?? false) + + return { + isCollapsed: isCollapsedD, + toggleCollapsed() { + setIsCollapsed(itemId, !isCollapsedD.getValue()) + }, + } + }, + } + }, [props.sheetId]) + return ( + + + + ) +} diff --git a/theatre/studio/src/panels/SequenceEditorPanel/SequenceEditorPanel.tsx b/theatre/studio/src/panels/SequenceEditorPanel/SequenceEditorPanel.tsx index 439d072..ad80780 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/SequenceEditorPanel.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/SequenceEditorPanel.tsx @@ -28,6 +28,7 @@ import { TitleBar_Punctuation, } from '@theatre/studio/panels/BasePanel/common' import type {UIPanelId} from '@theatre/shared/utils/ids' +import {ProvideCollapsable} from './DopeSheet/useSequenceEditorCollapsable' const Container = styled(PanelWrapper)` z-index: ${panelZIndexes.sequenceEditorPanel}; @@ -163,15 +164,17 @@ const Content: React.VFC<{}> = () => { return ( - -
- - {graphEditorOpen && ( - - )} - {graphEditorAvailable && } - - + + +
+ + {graphEditorOpen && ( + + )} + {graphEditorAvailable && } + + + ) }, [dims]) diff --git a/theatre/studio/src/store/stateEditors.ts b/theatre/studio/src/store/stateEditors.ts index 03843da..0c80419 100644 --- a/theatre/studio/src/store/stateEditors.ts +++ b/theatre/studio/src/store/stateEditors.ts @@ -13,6 +13,7 @@ import type { } from '@theatre/shared/utils/addresses' import {encodePathToProp} from '@theatre/shared/utils/addresses' import type { + StudioSheetItemKey, KeyframeId, SequenceMarkerId, SequenceTrackId, @@ -482,6 +483,35 @@ namespace stateEditors { ).clippedSpaceRange = {...p.range} } } + + export namespace sequenceEditorCollapsableItems { + function _ensure(p: WithoutSheetInstance) { + const seq = + stateEditors.studio.ahistoric.projects.stateByProjectId.stateBySheetId.sequence._ensure( + p, + ) + let existing = seq.sequenceEditorCollapsableItems + if (!existing) { + existing = seq.sequenceEditorCollapsableItems = + pointableSetUtil.create() + } + return existing + } + export function set( + p: WithoutSheetInstance & { + studioSheetItemKey: StudioSheetItemKey + isCollapsed: boolean + }, + ) { + const collapsableSet = _ensure(p) + Object.assign( + collapsableSet, + pointableSetUtil.add(collapsableSet, p.studioSheetItemKey, { + isCollapsed: p.isCollapsed, + }), + ) + } + } } } } diff --git a/theatre/studio/src/store/types/ahistoric.ts b/theatre/studio/src/store/types/ahistoric.ts index a38e9e8..bc0119e 100644 --- a/theatre/studio/src/store/types/ahistoric.ts +++ b/theatre/studio/src/store/types/ahistoric.ts @@ -1,10 +1,9 @@ import type {ProjectState} from '@theatre/core/projects/store/storeTypes' import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic' -import type { - ProjectId, - SheetId, -} from '@theatre/shared/utils/ids' +import type {ProjectId, SheetId} from '@theatre/shared/utils/ids' import type {IRange, StrictRecord} from '@theatre/shared/utils/types' +import type {PointableSet} from '@theatre/shared/utils/PointableSet' +import type {StudioSheetItemKey} from '@theatre/shared/utils/ids' export type StudioAhistoricState = { visibilityState: 'everythingIsHidden' | 'everythingIsVisible' @@ -48,6 +47,13 @@ export type StudioAhistoricState = { enabled: boolean range: IRange } + + sequenceEditorCollapsableItems?: PointableSet< + StudioSheetItemKey, + { + isCollapsed: boolean + } + > } } >