From 6558181dbb36a008058701ecf66a342492970647 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Roche?= Date: Mon, 2 Jan 2023 19:43:19 +0100 Subject: [PATCH] Make items in the outline menu collapsable (#367) Co-authored-by: Clement Roche Co-authored-by: Cole Lawrence Co-authored-by: Aria --- .../src/panels/OutlinePanel/BaseItem.tsx | 44 +++-- .../ObjectsList/NamespacedObjects.tsx | 49 ----- .../OutlinePanel/ObjectsList/ObjectsList.tsx | 170 ++++++++++++++---- .../ObjectsList/getObjectNamespacePath.tsx | 19 -- .../ProjectsList/ProjectListItem.tsx | 7 +- .../SheetsList/SheetInstanceItem.tsx | 7 +- .../panels/OutlinePanel/outlinePanelUtils.ts | 45 +++++ theatre/studio/src/store/stateEditors.ts | 28 +++ theatre/studio/src/store/types/ahistoric.ts | 1 + 9 files changed, 252 insertions(+), 118 deletions(-) delete mode 100644 theatre/studio/src/panels/OutlinePanel/ObjectsList/NamespacedObjects.tsx delete mode 100644 theatre/studio/src/panels/OutlinePanel/ObjectsList/getObjectNamespacePath.tsx create mode 100644 theatre/studio/src/panels/OutlinePanel/outlinePanelUtils.ts diff --git a/theatre/studio/src/panels/OutlinePanel/BaseItem.tsx b/theatre/studio/src/panels/OutlinePanel/BaseItem.tsx index 4c5e133..8ad2013 100644 --- a/theatre/studio/src/panels/OutlinePanel/BaseItem.tsx +++ b/theatre/studio/src/panels/OutlinePanel/BaseItem.tsx @@ -1,6 +1,7 @@ import type {VoidFn} from '@theatre/shared/utils/types' import React from 'react' import styled, {css} from 'styled-components' +import noop from '@theatre/shared/utils/noop' import {pointerEventsAutoInNormalMode} from '@theatre/studio/css' import {ChevronDown, Package} from '@theatre/studio/uiComponents/icons' @@ -62,15 +63,6 @@ const Header = styled(BaseHeader)` border-bottom: 1px solid rgba(255, 255, 255, 0.08); } - // Hit zone - &:before { - position: absolute; - inset: -1px 0; - display: block; - content: ' '; - z-index: 5; - } - @supports not (backdrop-filter: blur()) { background: rgba(40, 43, 47, 0.95); } @@ -105,16 +97,24 @@ const Head_IconContainer = styled.div` opacity: 0.99; ` -const Head_Icon_WithDescendants = styled.span<{isOpen: boolean}>` +const Head_Icon_WithDescendants = styled.span` font-size: 9px; position: relative; display: block; + + ${Container}.collapsed & { + transform: rotate(-90deg); + } ` const ChildrenContainer = styled.ul` margin: 0; padding: 0; list-style: none; + + ${Container}.collapsed & { + display: none; + } ` type SelectionStatus = @@ -129,7 +129,18 @@ const BaseItem: React.FC<{ depth: number selectionStatus: SelectionStatus labelDecoration?: React.ReactNode -}> = ({label, children, depth, select, selectionStatus, labelDecoration}) => { + collapsed?: boolean + setIsCollapsed?: (v: boolean) => void +}> = ({ + label, + children, + depth, + select, + selectionStatus, + labelDecoration, + collapsed = false, + setIsCollapsed, +}) => { const canContainChildren = children !== undefined return ( @@ -138,11 +149,18 @@ const BaseItem: React.FC<{ /* @ts-ignore */ {'--depth': depth} } + className={collapsed ? 'collapsed' : ''} > -
+
{canContainChildren ? ( - + { + evt.stopPropagation() + setIsCollapsed?.(!collapsed) + select?.() + }} + > ) : ( diff --git a/theatre/studio/src/panels/OutlinePanel/ObjectsList/NamespacedObjects.tsx b/theatre/studio/src/panels/OutlinePanel/ObjectsList/NamespacedObjects.tsx deleted file mode 100644 index 4cf1780..0000000 --- a/theatre/studio/src/panels/OutlinePanel/ObjectsList/NamespacedObjects.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import type SheetObject from '@theatre/core/sheetObjects/SheetObject' -import {getObjectNamespacePath} from './getObjectNamespacePath' - -/** See {@link addToNamespace} for adding to the namespace, easily. */ -export type NamespacedObjects = Map< - string, - { - object?: SheetObject - nested?: NamespacedObjects - } -> - -export function addToNamespace( - mutObjects: NamespacedObjects, - object: SheetObject, -) { - _addToNamespace(mutObjects, getObjectNamespacePath(object), object) -} -function _addToNamespace( - mutObjects: NamespacedObjects, - path: string[], - object: SheetObject, -) { - console.assert(path.length > 0, 'expecting path to not be empty') - const [next, ...rest] = path - let existing = mutObjects.get(next) - if (!existing) { - existing = { - nested: undefined, - object: undefined, - } - mutObjects.set(next, existing) - } - - if (rest.length === 0) { - console.assert( - !existing.object, - 'expect not to have existing object with same name', - {existing, object}, - ) - existing.object = object - } else { - if (!existing.nested) { - existing.nested = new Map() - } - - _addToNamespace(existing.nested, rest, object) - } -} diff --git a/theatre/studio/src/panels/OutlinePanel/ObjectsList/ObjectsList.tsx b/theatre/studio/src/panels/OutlinePanel/ObjectsList/ObjectsList.tsx index 5946130..aa97250 100644 --- a/theatre/studio/src/panels/OutlinePanel/ObjectsList/ObjectsList.tsx +++ b/theatre/studio/src/panels/OutlinePanel/ObjectsList/ObjectsList.tsx @@ -6,8 +6,7 @@ import styled from 'styled-components' import {ObjectItem} from './ObjectItem' import type SheetObject from '@theatre/core/sheetObjects/SheetObject' import BaseItem from '@theatre/studio/panels/OutlinePanel/BaseItem' -import type {NamespacedObjects} from './NamespacedObjects' -import {addToNamespace} from './NamespacedObjects' +import {useCollapseStateInOutlinePanel} from '@theatre/studio/panels/OutlinePanel/outlinePanelUtils' export const Li = styled.li<{isSelected: boolean}>` color: ${(props) => (props.isSelected ? 'white' : 'hsl(1, 1%, 80%)')}; @@ -28,54 +27,155 @@ const ObjectsList: React.FC<{ addToNamespace(rootObject, object) }) - return + return ( + + ) }, [sheet, depth]) } function NamespaceTree(props: { namespace: NamespacedObjects visualIndentation: number + path: string[] + sheet: Sheet }) { return ( <> {[...props.namespace.entries()].map(([label, {object, nested}]) => { - const nestedChildrenElt = nested && ( - - ) - const sameNameElt = object && ( - - ) - return ( - - {sameNameElt} - {nestedChildrenElt && ( - - )} - + ) })} ) } +function Namespace(props: { + nested?: NamespacedObjects + label: string + object?: SheetObject + visualIndentation: number + path: string[] + sheet: Sheet +}) { + const {nested, label, object, sheet} = props + const {collapsed, setCollapsed} = useCollapseStateInOutlinePanel({ + type: 'namespace', + sheet, + path: props.path, + }) + + const nestedChildrenElt = nested && ( + + ) + const sameNameElt = object && ( + + ) + + return ( + + {sameNameElt} + {nestedChildrenElt && ( + + )} + + ) +} + export default ObjectsList + +/** See {@link addToNamespace} for adding to the namespace, easily. */ +type NamespacedObjects = Map< + string, + { + object?: SheetObject + nested?: NamespacedObjects + path: string[] + } +> + +function addToNamespace( + mutObjects: NamespacedObjects, + object: SheetObject, + path = getObjectNamespacePath(object), +) { + const [next, ...rest] = path + let existing = mutObjects.get(next) + if (!existing) { + existing = { + nested: undefined, + object: undefined, + path: [...path], + } + mutObjects.set(next, existing) + } + + if (rest.length === 0) { + console.assert( + !existing.object, + 'expect not to have existing object with same name', + {existing, object}, + ) + existing.object = object + } else { + if (!existing.nested) { + existing.nested = new Map() + } + + addToNamespace(existing.nested, object, rest) + } +} + +function getObjectNamespacePath(object: SheetObject): string[] { + let existing = OBJECT_SPLITS_MEMO.get(object) + if (!existing) { + existing = object.address.objectKey.split( + RE_SPLIT_BY_SLASH_WITHOUT_WHITESPACE, + ) + console.assert(existing.length > 0, 'expected not empty') + OBJECT_SPLITS_MEMO.set(object, existing) + } + return existing +} +/** + * Relying on the fact we try to "sanitize paths" earlier. + * Go look for `sanifySlashedPath` in a `utils/slashedPaths.ts`. + */ +const RE_SPLIT_BY_SLASH_WITHOUT_WHITESPACE = /\s*\/\s*/g +const OBJECT_SPLITS_MEMO = new WeakMap() diff --git a/theatre/studio/src/panels/OutlinePanel/ObjectsList/getObjectNamespacePath.tsx b/theatre/studio/src/panels/OutlinePanel/ObjectsList/getObjectNamespacePath.tsx deleted file mode 100644 index a823af3..0000000 --- a/theatre/studio/src/panels/OutlinePanel/ObjectsList/getObjectNamespacePath.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import type SheetObject from '@theatre/core/sheetObjects/SheetObject' - -export function getObjectNamespacePath(object: SheetObject): string[] { - let existing = OBJECT_SPLITS_MEMO.get(object) - if (!existing) { - existing = object.address.objectKey.split( - RE_SPLIT_BY_SLASH_WITHOUT_WHITESPACE, - ) - console.assert(existing.length > 0, 'expected not empty') - OBJECT_SPLITS_MEMO.set(object, existing) - } - return existing -} -/** - * Relying on the fact we try to "sanitize paths" earlier. - * Go look for `sanifySlashedPath` in a `utils/slashedPaths.ts`. - */ -const RE_SPLIT_BY_SLASH_WITHOUT_WHITESPACE = /\s*\/\s*/g -const OBJECT_SPLITS_MEMO = new WeakMap() diff --git a/theatre/studio/src/panels/OutlinePanel/ProjectsList/ProjectListItem.tsx b/theatre/studio/src/panels/OutlinePanel/ProjectsList/ProjectListItem.tsx index 747197f..4d159ff 100644 --- a/theatre/studio/src/panels/OutlinePanel/ProjectsList/ProjectListItem.tsx +++ b/theatre/studio/src/panels/OutlinePanel/ProjectsList/ProjectListItem.tsx @@ -7,6 +7,7 @@ import {usePrism} from '@theatre/react' import {getOutlineSelection} from '@theatre/studio/selectors' import {val} from '@theatre/dataverse' import styled from 'styled-components' +import {useCollapseStateInOutlinePanel} from '@theatre/studio/panels/OutlinePanel/outlinePanelUtils' const ConflictNotice = styled.div` color: #ff6363; @@ -38,10 +39,14 @@ const ProjectListItem: React.FC<{ }) }, [project]) + const {collapsed, setCollapsed} = useCollapseStateInOutlinePanel(project) + return ( Has Conflicts : null } @@ -56,7 +61,7 @@ const ProjectListItem: React.FC<{ : 'not-selected' } select={select} - > + /> ) } diff --git a/theatre/studio/src/panels/OutlinePanel/SheetsList/SheetInstanceItem.tsx b/theatre/studio/src/panels/OutlinePanel/SheetsList/SheetInstanceItem.tsx index f5ecc96..1b86801 100644 --- a/theatre/studio/src/panels/OutlinePanel/SheetsList/SheetInstanceItem.tsx +++ b/theatre/studio/src/panels/OutlinePanel/SheetsList/SheetInstanceItem.tsx @@ -6,6 +6,7 @@ import styled from 'styled-components' import ObjectsList from '@theatre/studio/panels/OutlinePanel/ObjectsList/ObjectsList' import BaseItem from '@theatre/studio/panels/OutlinePanel/BaseItem' import type Sheet from '@theatre/core/sheets/Sheet' +import {useCollapseStateInOutlinePanel} from '@theatre/studio/panels/OutlinePanel/outlinePanelUtils' const Head = styled.div` display: flex; @@ -21,6 +22,8 @@ export const SheetInstanceItem: React.FC<{ depth: number sheet: Sheet }> = ({sheet, depth}) => { + const {collapsed, setCollapsed} = useCollapseStateInOutlinePanel(sheet) + const setSelectedSheet = useCallback(() => { getStudio()!.transaction(({stateEditors}) => { stateEditors.studio.historic.panels.outline.selection.set([sheet]) @@ -34,6 +37,8 @@ export const SheetInstanceItem: React.FC<{ s === sheet) ? 'selected' @@ -58,5 +63,5 @@ export const SheetInstanceItem: React.FC<{ ) - }, [depth]) + }, [depth, collapsed]) } diff --git a/theatre/studio/src/panels/OutlinePanel/outlinePanelUtils.ts b/theatre/studio/src/panels/OutlinePanel/outlinePanelUtils.ts new file mode 100644 index 0000000..5002537 --- /dev/null +++ b/theatre/studio/src/panels/OutlinePanel/outlinePanelUtils.ts @@ -0,0 +1,45 @@ +import type Project from '@theatre/core/projects/Project' +import {useCallback} from 'react' +import getStudio from '@theatre/studio/getStudio' +import {useVal} from '@theatre/react' +import type Sheet from '@theatre/core/sheets/Sheet' + +export function useCollapseStateInOutlinePanel( + item: Project | Sheet | {type: 'namespace'; sheet: Sheet; path: string[]}, +): { + collapsed: boolean + setCollapsed: (collapsed: boolean) => void +} { + const itemKey = + item.type === 'namespace' + ? `namespace:${item.sheet.address.sheetId}:${item.path.join('/')}` + : item.type === 'Theatre_Project' + ? 'project' + : item.type === 'Theatre_Sheet' + ? `sheetInstance:${item.address.sheetId}:${item.address.sheetInstanceId}` + : 'unknown' + + const projectId = + item.type === 'namespace' + ? item.sheet.address.projectId + : item.address.projectId + + const isCollapsed = + useVal( + getStudio().atomP.ahistoric.projects.stateByProjectId[projectId] + .collapsedItemsInOutline[itemKey], + ) ?? false + + const setCollapsed = useCallback( + (isCollapsed: boolean) => { + getStudio().transaction(({stateEditors}) => { + stateEditors.studio.ahistoric.projects.stateByProjectId.collapsedItemsInOutline.set( + {projectId, isCollapsed, itemKey: itemKey}, + ) + }) + }, + [itemKey], + ) + + return {collapsed: isCollapsed, setCollapsed} +} diff --git a/theatre/studio/src/store/stateEditors.ts b/theatre/studio/src/store/stateEditors.ts index 06176d3..afa3a4f 100644 --- a/theatre/studio/src/store/stateEditors.ts +++ b/theatre/studio/src/store/stateEditors.ts @@ -455,6 +455,7 @@ namespace stateEditors { } } } + export namespace projects { export namespace stateByProjectId { export function _ensure(p: ProjectAddress) { @@ -468,6 +469,33 @@ namespace stateEditors { return s.projects.stateByProjectId[p.projectId]! } + export namespace collapsedItemsInOutline { + export function _ensure(p: ProjectAddress) { + const projectState = + stateEditors.studio.ahistoric.projects.stateByProjectId._ensure( + p, + ) + if (!projectState.collapsedItemsInOutline) { + projectState.collapsedItemsInOutline = {} + } + return projectState.collapsedItemsInOutline! + } + export function set( + p: ProjectAddress & {isCollapsed: boolean; itemKey: string}, + ) { + const collapsedItemsInOutline = + stateEditors.studio.ahistoric.projects.stateByProjectId.collapsedItemsInOutline._ensure( + p, + ) + + if (p.isCollapsed) { + collapsedItemsInOutline[p.itemKey] = true + } else { + delete collapsedItemsInOutline[p.itemKey] + } + } + } + export namespace stateBySheetId { export function _ensure(p: WithoutSheetInstance) { const projectState = diff --git a/theatre/studio/src/store/types/ahistoric.ts b/theatre/studio/src/store/types/ahistoric.ts index a21ee37..a7e3776 100644 --- a/theatre/studio/src/store/types/ahistoric.ts +++ b/theatre/studio/src/store/types/ahistoric.ts @@ -45,6 +45,7 @@ export type StudioAhistoricState = { stateByProjectId: StrictRecord< ProjectId, { + collapsedItemsInOutline?: StrictRecord stateBySheetId: StrictRecord< SheetId, {