From c881445b41ff0a2c314092a6bb8f86904f9eac77 Mon Sep 17 00:00:00 2001 From: Elliot Boschwitz Date: Sun, 27 Oct 2024 21:27:41 -0700 Subject: [PATCH] Refactored sidebar for improved perf and reusability (#490) --- Stitch.xcodeproj/project.pbxproj | 74 +-- .../xcshareddata/swiftpm/Package.resolved | 15 +- Stitch/App/Logging/LogUtils.swift | 26 +- .../App/Shortcut/ProjectsHomeCommands.swift | 34 +- .../GestureHostingController.swift | 2 +- .../LayerInspectorActions.swift | 2 +- .../LayerInspector/LayerInspectorView.swift | 8 +- .../LayerInspector/LayerMultiselect.swift | 2 +- Stitch/Graph/Model/SchemaVersions.swift | 6 + .../Node/Layer/Type/GroupLayerNode.swift | 60 +- .../Graph/Node/Layer/Util/LayerGroups.swift | 9 - .../Layer/Util/LayerNodeEntityUtils.swift | 6 +- .../Graph/Node/Layer/Util/LayerNodeId.swift | 2 +- .../Layer/ViewModel/LayerNodeViewModel.swift | 12 +- Stitch/Graph/Node/Model/GraphCopyable.swift | 3 +- .../Model/Field/FocusedUserEditField.swift | 2 +- .../LayerNamesDropDownChoiceView.swift | 6 +- .../NodeRowObserverExtensions.swift | 2 +- .../Node/Title/View/CanvasItemTitleView.swift | 2 +- .../Graph/Node/Util/NodeCreatedAction.swift | 36 +- .../Graph/Node/Util/NodeDeletedAction.swift | 9 +- .../Graph/Node/Util/NodeViewModelUtils.swift | 3 +- .../View/NodeTag/NodeTagMenuButtonsView.swift | 2 +- .../Layer/Util/LayerNodesSorting.swift | 13 +- .../Gesture/SidebarListActiveGesture.swift | 6 +- .../Model/Gesture/SidebarSelectionState.swift | 81 +-- .../Model/LayerNodesForSidebarDict.swift | 19 - .../Sidebar/Model/ProjectSidebarTab.swift | 37 ++ Stitch/Graph/Sidebar/Model/SidebarDeps.swift | 21 - .../Sidebar/Model/SidebarGroupsDict.swift | 61 -- .../SidebarItem/SidebarDragDestination.swift | 35 + .../Model/SidebarItem/SidebarItem.swift | 44 -- .../Model/SidebarItem/SidebarListItem.swift | 64 +- .../SidebarListItemSelectionStatus.swift | 16 +- .../Sidebar/Model/SidebarListState.swift | 134 ---- .../Sidebar/Util/DeriveSidebarList.swift | 18 - .../Util/Gesture/LegacySidebarActions.swift | 566 ++++++---------- .../Gesture/SidebarListActionHelpers.swift | 238 +------ .../Util/Gesture/SidebarListItemActions.swift | 34 +- .../Util/Gesture/SidebarListItemHelpers.swift | 222 +------ .../Util/Gesture/SidebarListUIActions.swift | 572 ---------------- .../LayerGrouping/SidebarListItemUtils.swift | 388 ----------- .../Sidebar/Util/LegacySidebarData.swift | 130 ---- .../SidebarGroupCreated.swift | 51 +- .../SidebarGroupUncreated.swift | 55 +- .../SidebarItemSelectionActions.swift | 244 +++---- .../SidebarListItemSelectionActions.swift | 257 ++++---- .../SidebarListItemSelectionHelpers.swift | 327 +++------ .../SidebarSelectedItemsDeleted.swift | 40 +- .../SidebarSelectedItemsDuplicated.swift | 9 +- Stitch/Graph/Sidebar/Util/SidebarIcons.swift | 49 +- .../Util/SidebarListItemGroupActions.swift | 199 +----- .../Util/SidebarListItemToggleHelpers.swift | 353 ---------- .../Util/UpdateStateAfterListChange.swift | 168 ----- .../Sidebar/View/SidebarEditButtonView.swift | 22 +- .../Sidebar/View/SidebarFooterView.swift | 61 +- .../SidebarListItemChevronView.swift | 32 +- .../SidebarListItemSelectionCircleView.swift | 31 +- .../SidebarItem/SidebarListItemView.swift | 59 +- .../SidebarItem/SidebarListLabelView.swift | 140 ++-- ...SidebarListItemGestureRecognizerView.swift | 151 +++-- .../SidebarListItemSwipeButton.swift | 12 +- .../SidebarListItemSwipeInnerView.swift | 165 +---- .../SwipeView/SidebarListItemSwipeMenu.swift | 28 +- .../SwipeView/SidebarListItemSwipeView.swift | 136 ++-- .../Graph/Sidebar/View/SidebarListView.swift | 172 ++--- .../Sidebar/View/iPadProjectSidebarView.swift | 21 +- .../ViewModel/LayersSidebarViewModel.swift | 23 + .../ViewModel/ProjectSidebarObservable.swift | 123 ++++ .../SidebarItemGestureViewModel.swift | 359 +++++----- .../ViewModel/SidebarItemSwipable.swift | 623 ++++++++++++++++++ Stitch/Graph/Util/GraphActions.swift | 8 - .../NodeDuplicationActions.swift | 8 +- .../NodeSelection/NodeSelectionUtil.swift | 4 +- .../Graph/View/Gesture/GraphGestureView.swift | 2 +- Stitch/Graph/ViewModel/GraphDelegate.swift | 4 +- Stitch/Graph/ViewModel/GraphState.swift | 51 +- Stitch/Graph/ViewModel/GraphUI.swift | 2 +- Stitch/Stitch.entitlements | 4 +- 79 files changed, 2325 insertions(+), 4690 deletions(-) delete mode 100644 Stitch/Graph/Sidebar/Model/LayerNodesForSidebarDict.swift create mode 100644 Stitch/Graph/Sidebar/Model/ProjectSidebarTab.swift delete mode 100644 Stitch/Graph/Sidebar/Model/SidebarDeps.swift delete mode 100644 Stitch/Graph/Sidebar/Model/SidebarGroupsDict.swift create mode 100644 Stitch/Graph/Sidebar/Model/SidebarItem/SidebarDragDestination.swift delete mode 100644 Stitch/Graph/Sidebar/Model/SidebarItem/SidebarItem.swift delete mode 100644 Stitch/Graph/Sidebar/Model/SidebarListState.swift delete mode 100644 Stitch/Graph/Sidebar/Util/DeriveSidebarList.swift delete mode 100644 Stitch/Graph/Sidebar/Util/Gesture/SidebarListUIActions.swift delete mode 100644 Stitch/Graph/Sidebar/Util/LayerGrouping/SidebarListItemUtils.swift delete mode 100644 Stitch/Graph/Sidebar/Util/LegacySidebarData.swift delete mode 100644 Stitch/Graph/Sidebar/Util/SidebarListItemToggleHelpers.swift delete mode 100644 Stitch/Graph/Sidebar/Util/UpdateStateAfterListChange.swift create mode 100644 Stitch/Graph/Sidebar/ViewModel/LayersSidebarViewModel.swift create mode 100644 Stitch/Graph/Sidebar/ViewModel/ProjectSidebarObservable.swift create mode 100644 Stitch/Graph/Sidebar/ViewModel/SidebarItemSwipable.swift diff --git a/Stitch.xcodeproj/project.pbxproj b/Stitch.xcodeproj/project.pbxproj index 2f9a5ae07..4854cd383 100644 --- a/Stitch.xcodeproj/project.pbxproj +++ b/Stitch.xcodeproj/project.pbxproj @@ -122,7 +122,6 @@ B55500EB2C1BAD0E0081C3F1 /* ColorInvertModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55500EA2C1BAD0E0081C3F1 /* ColorInvertModifier.swift */; }; B55500ED2C1BAD610081C3F1 /* PreviewAbsoluteShapeLayerModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55500EC2C1BAD610081C3F1 /* PreviewAbsoluteShapeLayerModifier.swift */; }; B55500EF2C1BADB20081C3F1 /* PreviewCommonMisc.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55500EE2C1BADB20081C3F1 /* PreviewCommonMisc.swift */; }; - B55500F42C1BC2320081C3F1 /* SidebarListItemUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55500F32C1BC2320081C3F1 /* SidebarListItemUtils.swift */; }; B55500F62C1BC2660081C3F1 /* SidebarListItemSwipeInnerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55500F52C1BC2660081C3F1 /* SidebarListItemSwipeInnerView.swift */; }; B55500F92C1BC2D20081C3F1 /* SidebarListItemSwipeMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55500F82C1BC2D20081C3F1 /* SidebarListItemSwipeMenu.swift */; }; B55500FB2C1BC2FF0081C3F1 /* SidebarListItemSwipeButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55500FA2C1BC2FF0081C3F1 /* SidebarListItemSwipeButton.swift */; }; @@ -217,6 +216,9 @@ B5C603A7275D9E5C00EA2999 /* CameraOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C603A6275D9E5C00EA2999 /* CameraOrientation.swift */; }; B5CA9D2E2A77503B00B4E431 /* NumberFormatterObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CA9D2D2A77503B00B4E431 /* NumberFormatterObserver.swift */; }; B5CAFF922831D07100C2EC38 /* ProjectsListItemBlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CAFF912831D07100C2EC38 /* ProjectsListItemBlurView.swift */; }; + B5CB68322CC99FD800DFC660 /* ProjectSidebarTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CB68312CC99FD400DFC660 /* ProjectSidebarTab.swift */; }; + B5CB68342CC9A06500DFC660 /* ProjectSidebarObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CB68332CC9A06100DFC660 /* ProjectSidebarObservable.swift */; }; + B5CB68362CC9A0D700DFC660 /* LayersSidebarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CB68352CC9A0D300DFC660 /* LayersSidebarViewModel.swift */; }; B5CB82F4283D987F00FE29A6 /* StitchStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CB82F3283D987F00FE29A6 /* StitchStore.swift */; }; B5CEC1A428F8D95700BE083E /* StitchAVCaptureSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CEC1A328F8D95700BE083E /* StitchAVCaptureSession.swift */; }; B5CF6448294B8AF700EF83DB /* StitchButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CF6447294B8AF700EF83DB /* StitchButton.swift */; }; @@ -243,6 +245,8 @@ B5EC61F92A5E2174002A639F /* NodeViewType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5EC61F82A5E2174002A639F /* NodeViewType.swift */; }; B5EC61FB2A5F5713002A639F /* NodeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5EC61FA2A5F5713002A639F /* NodeViewModel.swift */; }; B5EC874F28256DD0004C2AB3 /* AlertsViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5EC874E28256DD0004C2AB3 /* AlertsViewModifier.swift */; }; + B5ECCB0C2CC94ED400585F1D /* SidebarDragDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5ECCB0B2CC94ECE00585F1D /* SidebarDragDestination.swift */; }; + B5ECCB0E2CC94F9600585F1D /* SidebarItemSwipable.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5ECCB0D2CC94F9000585F1D /* SidebarItemSwipable.swift */; }; B5EFD61A2C1134EC006F5E7F /* MediaLayerViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5EFD6192C1134EC006F5E7F /* MediaLayerViewModifier.swift */; }; B5F0FC162B4F2FB5009104A8 /* GraphReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F0FC152B4F2FB5009104A8 /* GraphReducer.swift */; }; B5F5FD432B37564500BBD9A2 /* WhatToTest.en-US.txt in Resources */ = {isa = PBXBuildFile; fileRef = B5F5FD422B37564500BBD9A2 /* WhatToTest.en-US.txt */; }; @@ -348,7 +352,6 @@ EC165EDC29C8B9330024ECDF /* JSONShapeCommandParsing.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC165EDB29C8B9330024ECDF /* JSONShapeCommandParsing.swift */; }; EC165EDE29C8BA890024ECDF /* JSONCustomShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC165EDD29C8BA890024ECDF /* JSONCustomShape.swift */; }; EC165EE029C8E1FE0024ECDF /* CodablePoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC165EDF29C8E1FE0024ECDF /* CodablePoint.swift */; }; - EC16EF6A27E1683B00B732EE /* SidebarListState.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC16EF6927E1683B00B732EE /* SidebarListState.swift */; }; EC17549328B9753800968D65 /* UIDeviceUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC17549228B9753800968D65 /* UIDeviceUtils.swift */; }; EC17ACC226B4BC5900137EA5 /* StitchAudio.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC17ACBF26B4BC5900137EA5 /* StitchAudio.swift */; }; EC17ACC426B4BC5900137EA5 /* StitchVideoMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC17ACC126B4BC5900137EA5 /* StitchVideoMetadata.swift */; }; @@ -403,11 +406,7 @@ EC28D55B26B2142D00E1118E /* LayerGroups.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC28D55A26B2142D00E1118E /* LayerGroups.swift */; }; EC28E4B526F92AA0006D20CB /* ProjectsHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC28E4B426F92AA0006D20CB /* ProjectsHomeView.swift */; }; EC28E4B726F92B36006D20CB /* ProjectsListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC28E4B626F92B36006D20CB /* ProjectsListItemView.swift */; }; - EC2942312BA1364600D14CD1 /* UpdateStateAfterListChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC2942302BA1364600D14CD1 /* UpdateStateAfterListChange.swift */; }; - EC2942332BA136D500D14CD1 /* LayerNodesForSidebarDict.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC2942322BA136D500D14CD1 /* LayerNodesForSidebarDict.swift */; }; - EC2942352BA136F500D14CD1 /* SidebarDeps.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC2942342BA136F500D14CD1 /* SidebarDeps.swift */; }; EC2942372BA1377B00D14CD1 /* SidebarListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC2942362BA1377B00D14CD1 /* SidebarListItem.swift */; }; - EC2942392BA13C8100D14CD1 /* SidebarListUIActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC2942382BA13C8100D14CD1 /* SidebarListUIActions.swift */; }; EC29423D2BA13DB400D14CD1 /* JumpToCanvasItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC29423C2BA13DB400D14CD1 /* JumpToCanvasItem.swift */; }; EC29423F2BA13DDC00D14CD1 /* SidebarGroupUncreated.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC29423E2BA13DDC00D14CD1 /* SidebarGroupUncreated.swift */; }; EC2942412BA13E0800D14CD1 /* SidebarSelectedItemsDeleted.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC2942402BA13E0800D14CD1 /* SidebarSelectedItemsDeleted.swift */; }; @@ -426,7 +425,6 @@ EC2F62972A0D718600D86A50 /* ShapeCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC2F62962A0D718600D86A50 /* ShapeCommand.swift */; }; EC2F62992A0DAB2600D86A50 /* PackNodeHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC2F62982A0DAB2600D86A50 /* PackNodeHelpers.swift */; }; EC3127F62BA3B6470087049A /* NodeWirelessBroadcastSubmenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3127F52BA3B6470087049A /* NodeWirelessBroadcastSubmenuView.swift */; }; - EC3127F82BA3DE120087049A /* DeriveSidebarList.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3127F72BA3DE120087049A /* DeriveSidebarList.swift */; }; EC31438728189FDF0023DB26 /* ProjectCreatedActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC31438628189FDF0023DB26 /* ProjectCreatedActions.swift */; }; EC3420D82AF9BA1200EBD896 /* CommentBoxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3420D72AF9BA1200EBD896 /* CommentBoxView.swift */; }; EC3420DE2AF9C2BE00EBD896 /* CommentBoxDataUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3420DD2AF9C2BE00EBD896 /* CommentBoxDataUtils.swift */; }; @@ -478,7 +476,6 @@ EC4A720F274C09A40026B233 /* WirelessReceiverNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC4A720E274C09A40026B233 /* WirelessReceiverNode.swift */; }; EC4A7211274C260C0026B233 /* WirelessPortView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC4A7210274C260C0026B233 /* WirelessPortView.swift */; }; EC4A7213274C5A460026B233 /* WirelessReceiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC4A7212274C5A460026B233 /* WirelessReceiver.swift */; }; - EC4BD8942BA1343400B8F38A /* SidebarGroupsDict.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC4BD8932BA1343400B8F38A /* SidebarGroupsDict.swift */; }; EC4BD89A2BA1350300B8F38A /* SidebarLayerData.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC4BD8992BA1350300B8F38A /* SidebarLayerData.swift */; }; EC4CFB6526FE480700D085F3 /* StitchSoundFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC4CFB6426FE480700D085F3 /* StitchSoundFile.swift */; }; EC4CFB6726FE483300D085F3 /* StitchMicrophone.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC4CFB6626FE483300D085F3 /* StitchMicrophone.swift */; }; @@ -561,7 +558,6 @@ EC6EDC2D27FF907300843E62 /* SidebarListItemSelectionActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC6EDC2C27FF907300843E62 /* SidebarListItemSelectionActions.swift */; }; EC6EDC3227FF93CF00843E62 /* SidebarSelectionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC6EDC3127FF93CF00843E62 /* SidebarSelectionState.swift */; }; EC6EDC3427FF943700843E62 /* SidebarVisibilityState.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC6EDC3327FF943700843E62 /* SidebarVisibilityState.swift */; }; - EC6EDC3827FF94BC00843E62 /* SidebarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC6EDC3727FF94BC00843E62 /* SidebarItem.swift */; }; EC731CB92A37F05100241CB2 /* PatchDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC731CB82A37F05000241CB2 /* PatchDescription.swift */; }; EC733DC02639CB2100DD4EC1 /* Interactions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC733DBF2639CB2100DD4EC1 /* Interactions.swift */; }; EC733DC22639E22E00DD4EC1 /* PatchNodeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC733DC12639E22E00DD4EC1 /* PatchNodeViewModel.swift */; }; @@ -733,7 +729,6 @@ ECBEE1662CA705F800A10B37 /* SidebarListItemHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECBEE1652CA705F800A10B37 /* SidebarListItemHelpers.swift */; }; ECC175872A43D6A30017815D /* StitchNodeNames.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECC175862A43D6A30017815D /* StitchNodeNames.swift */; }; ECC1758A2A43D9440017815D /* LayerDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECC175892A43D9440017815D /* LayerDescription.swift */; }; - ECC2BCC62B9AD1E900D5F942 /* LegacySidebarData.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECC2BCC52B9AD1E900D5F942 /* LegacySidebarData.swift */; }; ECC2BCCB2B9B72A700D5F942 /* SidebarListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECC2BCCA2B9B72A700D5F942 /* SidebarListView.swift */; }; ECC2BCCD2B9B72B700D5F942 /* SidebarFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECC2BCCC2B9B72B700D5F942 /* SidebarFooterView.swift */; }; ECC2BCCF2B9B72E900D5F942 /* SidebarListItemSwipeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECC2BCCE2B9B72E900D5F942 /* SidebarListItemSwipeView.swift */; }; @@ -745,7 +740,6 @@ ECC2BCDD2B9B74D200D5F942 /* SidebarListItemChevronView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECC2BCDC2B9B74D200D5F942 /* SidebarListItemChevronView.swift */; }; ECC2BCDF2B9B751500D5F942 /* SidebarListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECC2BCDE2B9B751500D5F942 /* SidebarListItemView.swift */; }; ECC2BCE52B9B7E8000D5F942 /* SidebarListItemSelectionCircleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECC2BCE42B9B7E8000D5F942 /* SidebarListItemSelectionCircleView.swift */; }; - ECC2BCE92B9B865300D5F942 /* SidebarListItemToggleHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECC2BCE82B9B865300D5F942 /* SidebarListItemToggleHelpers.swift */; }; ECC2BCEB2B9B867E00D5F942 /* SidebarListActionHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECC2BCEA2B9B867E00D5F942 /* SidebarListActionHelpers.swift */; }; ECC2BCED2B9CD43100D5F942 /* SidebarListItemGroupActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECC2BCEC2B9CD43100D5F942 /* SidebarListItemGroupActions.swift */; }; ECC2BCEF2B9E3B9200D5F942 /* LayerGroupFit.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECC2BCEE2B9E3B9200D5F942 /* LayerGroupFit.swift */; }; @@ -1056,7 +1050,6 @@ B55500EA2C1BAD0E0081C3F1 /* ColorInvertModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorInvertModifier.swift; sourceTree = ""; }; B55500EC2C1BAD610081C3F1 /* PreviewAbsoluteShapeLayerModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewAbsoluteShapeLayerModifier.swift; sourceTree = ""; }; B55500EE2C1BADB20081C3F1 /* PreviewCommonMisc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewCommonMisc.swift; sourceTree = ""; }; - B55500F32C1BC2320081C3F1 /* SidebarListItemUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarListItemUtils.swift; sourceTree = ""; }; B55500F52C1BC2660081C3F1 /* SidebarListItemSwipeInnerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarListItemSwipeInnerView.swift; sourceTree = ""; }; B55500F82C1BC2D20081C3F1 /* SidebarListItemSwipeMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarListItemSwipeMenu.swift; sourceTree = ""; }; B55500FA2C1BC2FF0081C3F1 /* SidebarListItemSwipeButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarListItemSwipeButton.swift; sourceTree = ""; }; @@ -1147,6 +1140,10 @@ B5C603A6275D9E5C00EA2999 /* CameraOrientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraOrientation.swift; sourceTree = ""; }; B5CA9D2D2A77503B00B4E431 /* NumberFormatterObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberFormatterObserver.swift; sourceTree = ""; }; B5CAFF912831D07100C2EC38 /* ProjectsListItemBlurView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectsListItemBlurView.swift; sourceTree = ""; }; + B5CB68312CC99FD400DFC660 /* ProjectSidebarTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectSidebarTab.swift; sourceTree = ""; }; + B5CB68332CC9A06100DFC660 /* ProjectSidebarObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectSidebarObservable.swift; sourceTree = ""; }; + B5CB68352CC9A0D300DFC660 /* LayersSidebarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayersSidebarViewModel.swift; sourceTree = ""; }; + B5CB68372CC9AC8D00DFC660 /* StitchViewKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = StitchViewKit; path = ../StitchViewKit; sourceTree = SOURCE_ROOT; }; B5CB82F3283D987F00FE29A6 /* StitchStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StitchStore.swift; sourceTree = ""; }; B5CEC1A328F8D95700BE083E /* StitchAVCaptureSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StitchAVCaptureSession.swift; sourceTree = ""; }; B5CF6447294B8AF700EF83DB /* StitchButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StitchButton.swift; sourceTree = ""; }; @@ -1171,6 +1168,8 @@ B5EC61F82A5E2174002A639F /* NodeViewType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeViewType.swift; sourceTree = ""; }; B5EC61FA2A5F5713002A639F /* NodeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeViewModel.swift; sourceTree = ""; }; B5EC874E28256DD0004C2AB3 /* AlertsViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertsViewModifier.swift; sourceTree = ""; }; + B5ECCB0B2CC94ECE00585F1D /* SidebarDragDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarDragDestination.swift; sourceTree = ""; }; + B5ECCB0D2CC94F9000585F1D /* SidebarItemSwipable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarItemSwipable.swift; sourceTree = ""; }; B5EFD6192C1134EC006F5E7F /* MediaLayerViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaLayerViewModifier.swift; sourceTree = ""; }; B5F0FC152B4F2FB5009104A8 /* GraphReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphReducer.swift; sourceTree = ""; }; B5F5FD422B37564500BBD9A2 /* WhatToTest.en-US.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = "WhatToTest.en-US.txt"; sourceTree = ""; }; @@ -1292,7 +1291,6 @@ EC165EDB29C8B9330024ECDF /* JSONShapeCommandParsing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONShapeCommandParsing.swift; sourceTree = ""; }; EC165EDD29C8BA890024ECDF /* JSONCustomShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONCustomShape.swift; sourceTree = ""; }; EC165EDF29C8E1FE0024ECDF /* CodablePoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodablePoint.swift; sourceTree = ""; }; - EC16EF6927E1683B00B732EE /* SidebarListState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarListState.swift; sourceTree = ""; }; EC17549228B9753800968D65 /* UIDeviceUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIDeviceUtils.swift; sourceTree = ""; }; EC17ACBF26B4BC5900137EA5 /* StitchAudio.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StitchAudio.swift; sourceTree = ""; }; EC17ACC126B4BC5900137EA5 /* StitchVideoMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StitchVideoMetadata.swift; sourceTree = ""; }; @@ -1345,11 +1343,7 @@ EC28D55A26B2142D00E1118E /* LayerGroups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayerGroups.swift; sourceTree = ""; }; EC28E4B426F92AA0006D20CB /* ProjectsHomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectsHomeView.swift; sourceTree = ""; }; EC28E4B626F92B36006D20CB /* ProjectsListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectsListItemView.swift; sourceTree = ""; }; - EC2942302BA1364600D14CD1 /* UpdateStateAfterListChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateStateAfterListChange.swift; sourceTree = ""; }; - EC2942322BA136D500D14CD1 /* LayerNodesForSidebarDict.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayerNodesForSidebarDict.swift; sourceTree = ""; }; - EC2942342BA136F500D14CD1 /* SidebarDeps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarDeps.swift; sourceTree = ""; }; EC2942362BA1377B00D14CD1 /* SidebarListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarListItem.swift; sourceTree = ""; }; - EC2942382BA13C8100D14CD1 /* SidebarListUIActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarListUIActions.swift; sourceTree = ""; }; EC29423C2BA13DB400D14CD1 /* JumpToCanvasItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JumpToCanvasItem.swift; sourceTree = ""; }; EC29423E2BA13DDC00D14CD1 /* SidebarGroupUncreated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarGroupUncreated.swift; sourceTree = ""; }; EC2942402BA13E0800D14CD1 /* SidebarSelectedItemsDeleted.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarSelectedItemsDeleted.swift; sourceTree = ""; }; @@ -1368,7 +1362,6 @@ EC2F62962A0D718600D86A50 /* ShapeCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShapeCommand.swift; sourceTree = ""; }; EC2F62982A0DAB2600D86A50 /* PackNodeHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackNodeHelpers.swift; sourceTree = ""; }; EC3127F52BA3B6470087049A /* NodeWirelessBroadcastSubmenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeWirelessBroadcastSubmenuView.swift; sourceTree = ""; }; - EC3127F72BA3DE120087049A /* DeriveSidebarList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeriveSidebarList.swift; sourceTree = ""; }; EC31438628189FDF0023DB26 /* ProjectCreatedActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectCreatedActions.swift; sourceTree = ""; }; EC3420D72AF9BA1200EBD896 /* CommentBoxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentBoxView.swift; sourceTree = ""; }; EC3420DD2AF9C2BE00EBD896 /* CommentBoxDataUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentBoxDataUtils.swift; sourceTree = ""; }; @@ -1419,7 +1412,6 @@ EC4A720E274C09A40026B233 /* WirelessReceiverNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WirelessReceiverNode.swift; sourceTree = ""; }; EC4A7210274C260C0026B233 /* WirelessPortView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WirelessPortView.swift; sourceTree = ""; }; EC4A7212274C5A460026B233 /* WirelessReceiver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WirelessReceiver.swift; sourceTree = ""; }; - EC4BD8932BA1343400B8F38A /* SidebarGroupsDict.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarGroupsDict.swift; sourceTree = ""; }; EC4BD8992BA1350300B8F38A /* SidebarLayerData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarLayerData.swift; sourceTree = ""; }; EC4CFB6426FE480700D085F3 /* StitchSoundFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StitchSoundFile.swift; sourceTree = ""; }; EC4CFB6626FE483300D085F3 /* StitchMicrophone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StitchMicrophone.swift; sourceTree = ""; }; @@ -1501,7 +1493,6 @@ EC6EDC2C27FF907300843E62 /* SidebarListItemSelectionActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarListItemSelectionActions.swift; sourceTree = ""; }; EC6EDC3127FF93CF00843E62 /* SidebarSelectionState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarSelectionState.swift; sourceTree = ""; }; EC6EDC3327FF943700843E62 /* SidebarVisibilityState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarVisibilityState.swift; sourceTree = ""; }; - EC6EDC3727FF94BC00843E62 /* SidebarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarItem.swift; sourceTree = ""; }; EC731CB82A37F05000241CB2 /* PatchDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatchDescription.swift; sourceTree = ""; }; EC733DBF2639CB2100DD4EC1 /* Interactions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Interactions.swift; sourceTree = ""; }; EC733DC12639E22E00DD4EC1 /* PatchNodeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatchNodeViewModel.swift; sourceTree = ""; }; @@ -1671,7 +1662,6 @@ ECBEE1652CA705F800A10B37 /* SidebarListItemHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarListItemHelpers.swift; sourceTree = ""; }; ECC175862A43D6A30017815D /* StitchNodeNames.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StitchNodeNames.swift; sourceTree = ""; }; ECC175892A43D9440017815D /* LayerDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayerDescription.swift; sourceTree = ""; }; - ECC2BCC52B9AD1E900D5F942 /* LegacySidebarData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacySidebarData.swift; sourceTree = ""; }; ECC2BCCA2B9B72A700D5F942 /* SidebarListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarListView.swift; sourceTree = ""; }; ECC2BCCC2B9B72B700D5F942 /* SidebarFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarFooterView.swift; sourceTree = ""; }; ECC2BCCE2B9B72E900D5F942 /* SidebarListItemSwipeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarListItemSwipeView.swift; sourceTree = ""; }; @@ -1683,7 +1673,6 @@ ECC2BCDC2B9B74D200D5F942 /* SidebarListItemChevronView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarListItemChevronView.swift; sourceTree = ""; }; ECC2BCDE2B9B751500D5F942 /* SidebarListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarListItemView.swift; sourceTree = ""; }; ECC2BCE42B9B7E8000D5F942 /* SidebarListItemSelectionCircleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarListItemSelectionCircleView.swift; sourceTree = ""; }; - ECC2BCE82B9B865300D5F942 /* SidebarListItemToggleHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarListItemToggleHelpers.swift; sourceTree = ""; }; ECC2BCEA2B9B867E00D5F942 /* SidebarListActionHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarListActionHelpers.swift; sourceTree = ""; }; ECC2BCEC2B9CD43100D5F942 /* SidebarListItemGroupActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarListItemGroupActions.swift; sourceTree = ""; }; ECC2BCEE2B9E3B9200D5F942 /* LayerGroupFit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayerGroupFit.swift; sourceTree = ""; }; @@ -1921,6 +1910,7 @@ 200BD859254F66EB00692903 /* Stitch */ = { isa = PBXGroup; children = ( + B5CB68372CC9AC8D00DFC660 /* StitchViewKit */, EC15E425260271C6006F2E08 /* Stitch.entitlements */, 200BD863254F66EE00692903 /* Info.plist */, B5617A7B2C175C3C00B53DAF /* App */, @@ -2397,12 +2387,9 @@ B55500F12C1BC1AC0081C3F1 /* Model */ = { isa = PBXGroup; children = ( + B5CB68312CC99FD400DFC660 /* ProjectSidebarTab.swift */, B555010E2C1BD1810081C3F1 /* SidebarItem */, B55501082C1BCBF20081C3F1 /* Gesture */, - EC4BD8932BA1343400B8F38A /* SidebarGroupsDict.swift */, - EC2942322BA136D500D14CD1 /* LayerNodesForSidebarDict.swift */, - EC16EF6927E1683B00B732EE /* SidebarListState.swift */, - EC2942342BA136F500D14CD1 /* SidebarDeps.swift */, ); path = Model; sourceTree = ""; @@ -2414,11 +2401,7 @@ B55501092C1BCEDD0081C3F1 /* LayerGrouping */, B555010A2C1BCEE70081C3F1 /* Gesture */, B55501012C1BC48C0081C3F1 /* SidebarIcons.swift */, - ECC2BCC52B9AD1E900D5F942 /* LegacySidebarData.swift */, - EC2942302BA1364600D14CD1 /* UpdateStateAfterListChange.swift */, - ECC2BCE82B9B865300D5F942 /* SidebarListItemToggleHelpers.swift */, ECC2BCEC2B9CD43100D5F942 /* SidebarListItemGroupActions.swift */, - EC3127F72BA3DE120087049A /* DeriveSidebarList.swift */, EC29423C2BA13DB400D14CD1 /* JumpToCanvasItem.swift */, B555010C2C1BD0E10081C3F1 /* SidebarVisibilityUtils.swift */, ); @@ -2452,6 +2435,9 @@ B55501032C1BC86E0081C3F1 /* ViewModel */ = { isa = PBXGroup; children = ( + B5CB68352CC9A0D300DFC660 /* LayersSidebarViewModel.swift */, + B5CB68332CC9A06100DFC660 /* ProjectSidebarObservable.swift */, + B5ECCB0D2CC94F9000585F1D /* SidebarItemSwipable.swift */, ECC2BCD22B9B734F00D5F942 /* SidebarItemGestureViewModel.swift */, ); path = ViewModel; @@ -2470,7 +2456,6 @@ B55501092C1BCEDD0081C3F1 /* LayerGrouping */ = { isa = PBXGroup; children = ( - B55500F32C1BC2320081C3F1 /* SidebarListItemUtils.swift */, EC4BD8992BA1350300B8F38A /* SidebarLayerData.swift */, ECC2BCEE2B9E3B9200D5F942 /* LayerGroupFit.swift */, ); @@ -2482,7 +2467,6 @@ children = ( B55501062C1BCAD50081C3F1 /* SidebarListItemActions.swift */, ECC2BCD82B9B73FA00D5F942 /* LegacySidebarActions.swift */, - EC2942382BA13C8100D14CD1 /* SidebarListUIActions.swift */, ECC2BCEA2B9B867E00D5F942 /* SidebarListActionHelpers.swift */, ECBEE1652CA705F800A10B37 /* SidebarListItemHelpers.swift */, ); @@ -2506,7 +2490,7 @@ B555010E2C1BD1810081C3F1 /* SidebarItem */ = { isa = PBXGroup; children = ( - EC6EDC3727FF94BC00843E62 /* SidebarItem.swift */, + B5ECCB0B2CC94ECE00585F1D /* SidebarDragDestination.swift */, EC2942362BA1377B00D14CD1 /* SidebarListItem.swift */, EC6EDC3327FF943700843E62 /* SidebarVisibilityState.swift */, ECA2E2B827F6210D00D31ECF /* SidebarListItemSelectionStatus.swift */, @@ -4645,7 +4629,6 @@ B5EC874F28256DD0004C2AB3 /* AlertsViewModifier.swift in Sources */, E41A66B32BD2BB8E00385C9F /* LinearGradientNode.swift in Sources */, EC17ECB229970C300097870C /* ExtendedAttributesUtils.swift in Sources */, - EC3127F82BA3DE120087049A /* DeriveSidebarList.swift in Sources */, EC2F62992A0DAB2600D86A50 /* PackNodeHelpers.swift in Sources */, ECB9A8692AEB4922006E65EE /* HueSliderView.swift in Sources */, EC8044982944321A00DCFCF4 /* LayerLoopNodeCoordinate.swift in Sources */, @@ -4654,7 +4637,6 @@ B59447212B97FA140039603D /* CommentBoxViewModel.swift in Sources */, E4AB307C29B4F42F000A66B0 /* YOLOv3Tiny.mlmodel in Sources */, ECD7FA8D29D73E2D0073ACAC /* ComponentActions.swift in Sources */, - EC2942392BA13C8100D14CD1 /* SidebarListUIActions.swift in Sources */, EC2BBF042947BBC2005BCCDB /* ColorFillLayerNode.swift in Sources */, B550C22F28D0404300F40B80 /* MiddlewareService.swift in Sources */, EC83D89A2926E42400255311 /* GraphUICursorSelectionActions.swift in Sources */, @@ -4721,6 +4703,7 @@ ECFC975D29679930000414C4 /* BouncyConverterNode.swift in Sources */, EC560E0D293A773D00350046 /* LoopDedupeNode.swift in Sources */, B5EFD61A2C1134EC006F5E7F /* MediaLayerViewModifier.swift in Sources */, + B5CB68322CC99FD800DFC660 /* ProjectSidebarTab.swift in Sources */, ECDCE3562821EA5800D83EC3 /* PortValueDisplay.swift in Sources */, EC1A357A2C25CE8100E5A6D8 /* LayerInspectorActions.swift in Sources */, ECF387C9280B5C4C0075F0E5 /* NodeInputOutputView.swift in Sources */, @@ -4750,7 +4733,6 @@ ECEA12FC2C45B67D0058CD6A /* GenericFlyoutView.swift in Sources */, EC0B8A812953C5FE00AA01B6 /* OvalPatchNode.swift in Sources */, ECE7443F2AB4E924006B3EF4 /* BackwardEdgeHelpers.swift in Sources */, - EC2942312BA1364600D14CD1 /* UpdateStateAfterListChange.swift in Sources */, EC6EDC3427FF943700843E62 /* SidebarVisibilityState.swift in Sources */, B55500BB2C1B99810081C3F1 /* FieldViewModelType.swift in Sources */, 205E6DDA25AFCB16001C5C27 /* NodeTesting.swift in Sources */, @@ -4760,7 +4742,6 @@ B548533729371E3900C43828 /* RealityView.swift in Sources */, EC16587D2B6AF03F00FE4AE6 /* EdgeEditingState.swift in Sources */, ECFCBE57284055710087585C /* GraphMovementObserver.swift in Sources */, - EC6EDC3827FF94BC00843E62 /* SidebarItem.swift in Sources */, EC00D80526FAC4E50024AAF6 /* GraphActions.swift in Sources */, EC5734312B75BA0E00D93A78 /* ReduxHelpers.swift in Sources */, B5410608286D66F3000688CA /* NodeUtils.swift in Sources */, @@ -4829,6 +4810,7 @@ EC68A700288B31A000689F92 /* GroupNodeDestructionActions.swift in Sources */, ECE639222C489C6900FB8D5F /* FlyoutUtils.swift in Sources */, B50AD534292C9E3F003CC61E /* InsertNodeMenuSearchResults.swift in Sources */, + B5CB68362CC9A0D700DFC660 /* LayersSidebarViewModel.swift in Sources */, ECFAB3DB2BB75F8D007B569A /* SpringAnimationNumberOp.swift in Sources */, B5DDF700293BC69B00D72220 /* NetworkRequestUtils.swift in Sources */, EC808C022CC6CF4400F30C70 /* NumberCoercers.swift in Sources */, @@ -4837,7 +4819,6 @@ EC82939426F96C0700307D91 /* MediaConstants.swift in Sources */, EC35A511297622030014D14A /* JSONShapeCommand.swift in Sources */, E4F7AC262974B6E700B7F55A /* PreviewRealityLayer.swift in Sources */, - ECC2BCE92B9B865300D5F942 /* SidebarListItemToggleHelpers.swift in Sources */, EC5E6C892B71839600B4CACC /* AnimatableForwardCircuitLine.swift in Sources */, EC28CC9C287E348A00987792 /* PatchDefaultNodeExtensions.swift in Sources */, ECB1B7B226FB9D7A006D9C95 /* FileUtils.swift in Sources */, @@ -4862,6 +4843,7 @@ B5B43FCC2B4F565300C6A847 /* NodeDefinition.swift in Sources */, EC9DB346280F1F710061EC3C /* EdgeDrawingObserver.swift in Sources */, ECB9812626A73BDD002ED9A5 /* ClipNode.swift in Sources */, + B5ECCB0C2CC94ED400585F1D /* SidebarDragDestination.swift in Sources */, B59F03B02CB6E67200A9422C /* EncoderDirectoryLocation.swift in Sources */, ECC558A3294A941B0025BB42 /* LayerDragEndedHelpers.swift in Sources */, ECD0E1A228D3699800133045 /* ImageToBase64StringNode.swift in Sources */, @@ -4911,12 +4893,10 @@ EC1B41AE26BC4BD40016C2C2 /* LayerDraggedActions.swift in Sources */, EC49149529358BC100D74344 /* HexNode.swift in Sources */, B50AD530292C9DDF003CC61E /* InsertNodeMenuNodeDescriptionView.swift in Sources */, - EC2942352BA136F500D14CD1 /* SidebarDeps.swift in Sources */, B5FEE4B428AC443500B50BCD /* StitchVideoDelegate.swift in Sources */, EC28CC9E287E351100987792 /* PatchNodeTypeExtensions.swift in Sources */, B590041A28501E6700EDDDFD /* MediaPickerValueEntry.swift in Sources */, EC609DD229357B8B00BF60A5 /* LessThanNode.swift in Sources */, - EC4BD8942BA1343400B8F38A /* SidebarGroupsDict.swift in Sources */, B530D5072C18ABEB0005C6A9 /* DirectoryObserverActions.swift in Sources */, EC8C5C152616415F0099C374 /* SoundKitNode.swift in Sources */, ECF236802CC1B00E002BC84A /* LayerGroupOrientationDropDownChoiceView.swift in Sources */, @@ -4947,6 +4927,7 @@ ECECF3DD2C095A8F004A4D10 /* TabKeyPressActions.swift in Sources */, ECB5A8162657230600D46D3E /* SampleHoldNode.swift in Sources */, B53A92292A7D80CC00192E86 /* GraphMovementViewModifier.swift in Sources */, + B5ECCB0E2CC94F9600585F1D /* SidebarItemSwipable.swift in Sources */, EC28CC9A287E347A00987792 /* PatchEvaluateExtensions.swift in Sources */, ECCEB26A2963928D007E35D1 /* UnionNode.swift in Sources */, ECD039272641F3C9003926B4 /* PreviewGroup.swift in Sources */, @@ -4987,6 +4968,7 @@ EC8605BA269E1A2B007B8ED3 /* ArrayAppendNode.swift in Sources */, EC40812D2C014EF70047989F /* StitchDocumentURLUtils.swift in Sources */, B50F41712C1F52C00082262F /* LayerInspectorView.swift in Sources */, + B5CB68342CC9A06500DFC660 /* ProjectSidebarObservable.swift in Sources */, ECA92A20260A6A1400281B52 /* CounterNode.swift in Sources */, EC650BD926864DF200C4CCC9 /* SoundImportNode.swift in Sources */, ECE08E432649D14D00996EE2 /* ScrollInteractionNode.swift in Sources */, @@ -5020,7 +5002,6 @@ EC8C5C112616334B0099C374 /* OptionSwitchNode.swift in Sources */, EC0F334E2BED9415008093BC /* StoreDelegate.swift in Sources */, B55500F92C1BC2D20081C3F1 /* SidebarListItemSwipeMenu.swift in Sources */, - EC2942332BA136D500D14CD1 /* LayerNodesForSidebarDict.swift in Sources */, EC0F334A2BED93D9008093BC /* NodeDelegate.swift in Sources */, EC5E3A1828188DF5001E156F /* ProjectDestructiveActions.swift in Sources */, B50192092C6D100E00A048ED /* LayerInputObserver.swift in Sources */, @@ -5218,7 +5199,6 @@ 20308FA425AD1721008CE3BE /* GraphEvaluationUtil.swift in Sources */, EC519E372BACC0ED005C6131 /* MathExpressionSubmenuButtonView.swift in Sources */, B5EC48BC2A0ECC0000C6AE72 /* LayerNodesSorting.swift in Sources */, - ECC2BCC62B9AD1E900D5F942 /* LegacySidebarData.swift in Sources */, B525BB482C18AFC300A98EB4 /* FileDropHandling.swift in Sources */, B5D689BB2758203B0086C297 /* PreviewWindowElementUIKitGestures.swift in Sources */, B55500D32C1BA6810081C3F1 /* PreviewWindowSizing.swift in Sources */, @@ -5253,7 +5233,6 @@ ECF0B0F42877E45A001D7CFC /* ColorOrbValueButtonView.swift in Sources */, EC4CFB6526FE480700D085F3 /* StitchSoundFile.swift in Sources */, ECEEA5022AFD9EC800C7CE86 /* TagMenuHelperViews.swift in Sources */, - B55500F42C1BC2320081C3F1 /* SidebarListItemUtils.swift in Sources */, ECB33C6226F15A1C0069885B /* ProjectsUtils.swift in Sources */, EC609DB429352FB800BF60A5 /* NumberExtensionUtils.swift in Sources */, ECA92A00260A694C00281B52 /* PressInteractionPatchNode.swift in Sources */, @@ -5295,7 +5274,6 @@ E40B2F532CB86E48005F5479 /* StitchAIConstants.swift in Sources */, EC609DC02935337E00BF60A5 /* Point3D.swift in Sources */, EC1B41B626BCB1380016C2C2 /* NodeDeletedAction.swift in Sources */, - EC16EF6A27E1683B00B732EE /* SidebarListState.swift in Sources */, EC49149329358A6E00D74344 /* ColorToRGBANode.swift in Sources */, ECF2ED112A732B09003EB106 /* GraphMultigesture.swift in Sources */, EC8DD69C29A594930061FB67 /* PortValuesUtils.swift in Sources */, @@ -6357,8 +6335,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StitchDesign/StitchViewKit.git"; requirement = { - kind = exactVersion; - version = 1.0.3; + kind = revision; + revision = f933687a9a77f73852c0e46cb1d4cc8e279baa3b; }; }; B557805F2CAF5D9500907BA8 /* XCRemoteSwiftPackageReference "StitchEngine" */ = { @@ -6381,8 +6359,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StitchDesign/StitchSchemaKit"; requirement = { - kind = exactVersion; - version = 26.0.0; + kind = revision; + revision = 6e0c9363337ddaba01afbc51fb27d143b18f6cca; }; }; EC1137362719F9B400ADCA72 /* XCRemoteSwiftPackageReference "swift-collections" */ = { diff --git a/Stitch.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Stitch.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 876992914..64a211c87 100644 --- a/Stitch.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Stitch.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,4 @@ { - "originHash" : "87be221b3fbded1c8027b1ce26671216264e4726379ed9409d52282eb1aab675", "pins" : [ { "identity" : "audiokit", @@ -67,17 +66,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StitchDesign/StitchSchemaKit", "state" : { - "revision" : "0e6458824efb1400aa1e158f165bb9035c703d06", - "version" : "26.0.0" - } - }, - { - "identity" : "stitchviewkit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/StitchDesign/StitchViewKit.git", - "state" : { - "revision" : "d8eaaf3e7522038e16f963a332c505a105ecbde5", - "version" : "1.0.3" + "revision" : "6e0c9363337ddaba01afbc51fb27d143b18f6cca" } }, { @@ -144,5 +133,5 @@ } } ], - "version" : 3 + "version" : 2 } diff --git a/Stitch/App/Logging/LogUtils.swift b/Stitch/App/Logging/LogUtils.swift index 676dd2ab3..1389ff275 100644 --- a/Stitch/App/Logging/LogUtils.swift +++ b/Stitch/App/Logging/LogUtils.swift @@ -18,17 +18,17 @@ enum LoggingAction: Equatable { case none, logToServer, fatal } -struct LogToServer: AppEvent { - let message: String - - // TODO: write state + message + device info to server (if online) - func handle(state: AppState) -> AppResponse { - .noChange - } -} +//struct LogToServer: AppEvent { +// let message: String +// +// // TODO: write state + message + device info to server (if online) +// func handle(state: AppState) -> AppResponse { +// .noChange +// } +//} // For debug printing from within SwiftUI views -func log(_ message: String, _ loggingAction: LoggingAction = .none) { +func log(_ message: Any, _ loggingAction: LoggingAction = .none) { #if DEBUG || DEV_DEBUG print("** \(message)") @@ -37,14 +37,14 @@ func log(_ message: String, _ loggingAction: LoggingAction = .none) { return case .fatal: #if DEV_DEBUG - fatalError("FATAL:" + message) + fatalError("FATAL: \(message)") #endif case .logToServer: print("HAD MAJOR ERROR: \(message)") - DispatchQueue.main.async { - dispatch(LogToServer(message: message)) - } +// DispatchQueue.main.async { +// dispatch(LogToServer(message: message)) +// } } #endif } diff --git a/Stitch/App/Shortcut/ProjectsHomeCommands.swift b/Stitch/App/Shortcut/ProjectsHomeCommands.swift index ba10d5ce4..822fd43f3 100644 --- a/Stitch/App/Shortcut/ProjectsHomeCommands.swift +++ b/Stitch/App/Shortcut/ProjectsHomeCommands.swift @@ -23,45 +23,19 @@ struct ProjectsHomeCommands: Commands { var layersActivelySelected: Bool { self.graph?.hasActivelySelectedLayers ?? false } - - var selections: SidebarSelectionState? { - self.graph?.sidebarSelectionState - } var graph: GraphState? { store.currentDocument?.visibleGraph } - var groups: SidebarGroupsDict? { - graph?.getSidebarGroupsDict() - } - - var layerNodes: LayerNodesForSidebarDict? { - if let graph = graph { - return LayerNodesForSidebarDict.fromLayerNodesDict( - nodes: graph.layerNodes, - orderedSidebarItems: graph.orderedSidebarLayers) - } - return nil - } - var ungroupButtonEnabled: Bool { - if let selections = selections, - let layerNodes = layerNodes { - return canUngroup(selections.primary, nodes: layerNodes) - } - return false + self.graph?.layersSidebarViewModel.canUngroup() ?? false } var groupButtonEnabled: Bool { - if let selections = selections, - let groups = groups { - return selections.nonEmptyPrimary.map { canBeGrouped($0, groups: groups) } ?? false - } - return false + self.graph?.layersSidebarViewModel.canBeGrouped() ?? false } - var textFieldFocused: Bool { let k = activeReduxFocusedField.isDefined || focusedField.isDefined // log("ProjectsHomeCommands: activeReduxFocusedField: \(activeReduxFocusedField)") @@ -315,7 +289,7 @@ struct ProjectsHomeCommands: Commands { // Disabled if no layers are actively selected disabled: !layersActivelySelected || !groupButtonEnabled) { // deletes both selected nodes and selected comments - dispatch(SidebarGroupCreated()) + self.graph?.layersSidebarViewModel.sidebarGroupCreated() } SwiftUIShortcutView(title: "Ungroup Layers", @@ -325,7 +299,7 @@ struct ProjectsHomeCommands: Commands { disabled: !layersActivelySelected || !ungroupButtonEnabled) { // disabled: !layersActivelySelected) { // deletes both selected nodes and selected comments - dispatch(SidebarGroupUncreated()) + self.graph?.layersSidebarViewModel.sidebarGroupUncreated() } } // replacing: .pasteboard diff --git a/Stitch/Graph/Gesture/View/ViewController/GestureHostingController.swift b/Stitch/Graph/Gesture/View/ViewController/GestureHostingController.swift index 66e5b676e..f9452351a 100644 --- a/Stitch/Graph/Gesture/View/ViewController/GestureHostingController.swift +++ b/Stitch/Graph/Gesture/View/ViewController/GestureHostingController.swift @@ -10,6 +10,6 @@ import StitchSchemaKit /// View controller abstraction for better handling of gestures. Fixes problem where a gesture /// may not work across many different views. -class GestureHostingController: StitchHostingController { +final class GestureHostingController: StitchHostingController { weak var delegate: UIGestureRecognizerDelegate? } diff --git a/Stitch/Graph/LayerInspector/LayerInspectorActions.swift b/Stitch/Graph/LayerInspector/LayerInspectorActions.swift index 96889af98..6de817ad8 100644 --- a/Stitch/Graph/LayerInspector/LayerInspectorActions.swift +++ b/Stitch/Graph/LayerInspector/LayerInspectorActions.swift @@ -13,7 +13,7 @@ import StitchSchemaKit extension GraphDelegate { // TODO: cache these for perf var nonEditModeSelectedLayerInLayerSidebar: NodeId? { - self.sidebarSelectionState.inspectorFocusedLayers.focused.first?.id + self.sidebarSelectionState.inspectorFocusedLayers.focused.first } // TODO: cache these for perf diff --git a/Stitch/Graph/LayerInspector/LayerInspectorView.swift b/Stitch/Graph/LayerInspector/LayerInspectorView.swift index e0fa5839d..03a436882 100644 --- a/Stitch/Graph/LayerInspector/LayerInspectorView.swift +++ b/Stitch/Graph/LayerInspector/LayerInspectorView.swift @@ -315,13 +315,13 @@ extension GraphState { return nil } - var selectedLayers = self.sidebarSelectionState.inspectorFocusedLayers.focused + var selectedLayers = self.layersSidebarViewModel.inspectorFocusedLayers.focused #if DEV_DEBUG // For debug if selectedLayers.isEmpty, let layer = self.layerNodes.keys.first { - selectedLayers = .init([.init(layer)]) + selectedLayers = .init([layer]) } #endif @@ -338,7 +338,7 @@ extension GraphState { return (header: "Multiselect", // node: nil, // TODO: is this bad? grabbing - node: firstLayer.asNodeId, + node: firstLayer, inputs: inputs, outputs: []) // TODO: multiselect for outputs @@ -346,7 +346,7 @@ extension GraphState { // else had 0 or 1 layers selected: else { - guard let inspectedLayerId = self.sidebarSelectionState.inspectorFocusedLayers.focused.first?.id, + guard let inspectedLayerId = self.layersSidebarViewModel.inspectorFocusedLayers.focused.first, let node = self.getNodeViewModel(inspectedLayerId), let layerNode = node.layerNode else { // log("LayerInspectorView: No inspector-focused layers?: \(self.sidebarSelectionState.inspectorFocusedLayers)") diff --git a/Stitch/Graph/LayerInspector/LayerMultiselect.swift b/Stitch/Graph/LayerInspector/LayerMultiselect.swift index a1cd0bb39..3a277ea38 100644 --- a/Stitch/Graph/LayerInspector/LayerMultiselect.swift +++ b/Stitch/Graph/LayerInspector/LayerMultiselect.swift @@ -82,7 +82,7 @@ extension LayerInputPort { let selectedLayers = graph.sidebarSelectionState.inspectorFocusedLayers let observers: [LayerInputObserver] = selectedLayers.focused.compactMap { - if let layerNode = graph.getNodeViewModel($0.id)?.layerNode { + if let layerNode = graph.getNodeViewModel($0)?.layerNode { let observer: LayerInputObserver = layerNode[keyPath: self.layerNodeKeyPath] return observer } diff --git a/Stitch/Graph/Model/SchemaVersions.swift b/Stitch/Graph/Model/SchemaVersions.swift index 664a10aa9..25d4926fc 100644 --- a/Stitch/Graph/Model/SchemaVersions.swift +++ b/Stitch/Graph/Model/SchemaVersions.swift @@ -196,6 +196,8 @@ extension StitchDocumentVersion { return StitchDocument_V25.StitchDocument.self case ._V26: return StitchDocument_V26.StitchDocument.self + case ._V27: + return StitchDocument_V27.StitchDocument.self } } } @@ -210,6 +212,8 @@ extension StitchSystemVersion { return StitchSystem_V25.StitchSystem.self case ._V26: return StitchSystem_V26.StitchSystem.self + case ._V27: + return StitchSystem_V27.StitchSystem.self } } } @@ -224,6 +228,8 @@ extension StitchComonentVersion { return StitchComponent_V25.StitchComponent.self case ._V26: return StitchComponent_V26.StitchComponent.self + case ._V27: + return StitchComponent_V27.StitchComponent.self } } } diff --git a/Stitch/Graph/Node/Layer/Type/GroupLayerNode.swift b/Stitch/Graph/Node/Layer/Type/GroupLayerNode.swift index 73db21167..861377236 100644 --- a/Stitch/Graph/Node/Layer/Type/GroupLayerNode.swift +++ b/Stitch/Graph/Node/Layer/Type/GroupLayerNode.swift @@ -134,62 +134,4 @@ struct GroupLayerNode: LayerNodeDefinition { extension LayerSize { static let defaultLayerGroupSize = LayerSize(width: .fill, height: .fill) -} - -extension GraphState { - // Creates just the LayerNode itself; - // does not add to SidebarGroups state etc. - - // When we create a GroupLayerNode, we must: - // (1) determine its position and size - // based on its children's sizes and positions; and - // (2) update the children's positions - // ASSUMES: "Fit to Selection" mode. - @MainActor - func createGroupLayerNode(groupLayerData: SidebarLayerData, - // position of layer node on graph - position: CGPoint, - // z-height of layer node on graph - zIndex: ZIndex) -> NodeViewModel? { - guard let children = groupLayerData.children else { - fatalErrorIfDebug() - return nil - } - - let selectedNodes = children - .flatMap { $0.allElementIds } - .toSet - - let parentSize: CGSize = self.getParentSizeForSelectedNodes(selectedNodes: selectedNodes) - - // determine sise and position of group layer node, - // plus how much to adjust the position of any children inside. - let layerGroupFit = self.getLayerGroupFit( - selectedNodes, - parentSize: parentSize) - - self.adjustGroupChildrenToLayerFit( - layerGroupFit, - selectedNodes) - - let newNode = Layer.group.graphNode.createViewModel(id: groupLayerData.id, - position: position, - zIndex: zIndex, - graphDelegate: self) - newNode.layerNode?.sizePort.updatePortValues([.size(layerGroupFit.size)]) - newNode.layerNode?.positionPort.updatePortValues([.position(layerGroupFit.position)]) - - newNode.graphDelegate = self - - // Update selected nodes to report to new group node - selectedNodes.forEach { nodeId in - guard let layerNode = self.getNodeViewModel(nodeId)?.layerNode else { - log("createGroupLayerNode: no node found") - return - } - layerNode.layerGroupId = newNode.id - } - - return newNode - } -} +} \ No newline at end of file diff --git a/Stitch/Graph/Node/Layer/Util/LayerGroups.swift b/Stitch/Graph/Node/Layer/Util/LayerGroups.swift index 3259dee35..945a7cabb 100644 --- a/Stitch/Graph/Node/Layer/Util/LayerGroups.swift +++ b/Stitch/Graph/Node/Layer/Util/LayerGroups.swift @@ -9,15 +9,6 @@ import Foundation import SwiftUI import StitchSchemaKit -// Find the parent, if any, for this layer node. -func findGroupLayerParentForLayerNode(_ nodeId: LayerNodeId, - _ groups: SidebarGroupsDict) -> LayerNodeId? { - - groups.first { (_: LayerNodeId, value: LayerIdList) in - value.contains(nodeId) - }?.key -} - extension GraphState { // Assumes: // - all selected nodes have either same parent or no parent ('top level') diff --git a/Stitch/Graph/Node/Layer/Util/LayerNodeEntityUtils.swift b/Stitch/Graph/Node/Layer/Util/LayerNodeEntityUtils.swift index 2a9275a7f..3290b0a65 100644 --- a/Stitch/Graph/Node/Layer/Util/LayerNodeEntityUtils.swift +++ b/Stitch/Graph/Node/Layer/Util/LayerNodeEntityUtils.swift @@ -117,8 +117,7 @@ extension LayerNodeEntity { materialThicknessPort: LayerInputEntity = .empty, deviceAppearancePort: LayerInputEntity = .empty, hasSidebarVisibility: Bool, - layerGroupId: NodeId?, - isExpandedInSidebar: Bool?) { + layerGroupId: NodeId?) { let outputsCount = layer.layerGraphNode.rowDefinitions(for: nil).outputs.count @@ -240,7 +239,6 @@ extension LayerNodeEntity { materialThicknessPort: materialThicknessPort, hasSidebarVisibility: hasSidebarVisibility, - layerGroupId: layerGroupId, - isExpandedInSidebar: isExpandedInSidebar) + layerGroupId: layerGroupId) } } diff --git a/Stitch/Graph/Node/Layer/Util/LayerNodeId.swift b/Stitch/Graph/Node/Layer/Util/LayerNodeId.swift index 80e70e5c0..6ceecd2c4 100644 --- a/Stitch/Graph/Node/Layer/Util/LayerNodeId.swift +++ b/Stitch/Graph/Node/Layer/Util/LayerNodeId.swift @@ -15,7 +15,7 @@ extension LayerNodeId { } var asItemId: SidebarListItemId { - SidebarListItemId(id) + self.asNodeId } var asNodeId: NodeId { diff --git a/Stitch/Graph/Node/Layer/ViewModel/LayerNodeViewModel.swift b/Stitch/Graph/Node/Layer/ViewModel/LayerNodeViewModel.swift index 2a740cdc1..6320c8ff1 100644 --- a/Stitch/Graph/Node/Layer/ViewModel/LayerNodeViewModel.swift +++ b/Stitch/Graph/Node/Layer/ViewModel/LayerNodeViewModel.swift @@ -165,8 +165,6 @@ final class LayerNodeViewModel { } } } - - var isExpandedInSidebar: Bool? init(from schema: LayerNodeEntity) { @@ -180,7 +178,6 @@ final class LayerNodeViewModel { self.layer = schema.layer self.hasSidebarVisibility = schema.hasSidebarVisibility self.layerGroupId = schema.layerGroupId - self.isExpandedInSidebar = schema.isExpandedInSidebar self.outputPorts = rowDefinitions .createOutputLayerPorts(schema: schema, @@ -412,8 +409,7 @@ extension LayerNodeViewModel: SchemaObserver { var schema = LayerNodeEntity(nodeId: self.id, layer: layer, hasSidebarVisibility: hasSidebarVisibility, - layerGroupId: layerGroupId, - isExpandedInSidebar: self.isExpandedInSidebar) + layerGroupId: layerGroupId) // Only encode keypaths used by this layer self.layer.layerGraphNode.inputDefinitions.forEach { inputType in @@ -561,12 +557,6 @@ extension LayerNodeViewModel { changedPortId: changedPortId) } } - - var visibilityStatusIcon: String { - self.hasSidebarVisibility - ? SIDEBAR_VISIBILITY_STATUS_VISIBLE_ICON - : SIDEBAR_VISIBILITY_STATUS_HIDDEN_ICON - } } extension Layer { diff --git a/Stitch/Graph/Node/Model/GraphCopyable.swift b/Stitch/Graph/Node/Model/GraphCopyable.swift index cd613cd2d..192d2f483 100644 --- a/Stitch/Graph/Node/Model/GraphCopyable.swift +++ b/Stitch/Graph/Node/Model/GraphCopyable.swift @@ -191,8 +191,7 @@ extension LayerNodeEntity: GraphCopyable { var newSchema = LayerNodeEntity(nodeId: newId, layer: self.layer, hasSidebarVisibility: self.hasSidebarVisibility, - layerGroupId: mappableData.get(self.layerGroupId), - isExpandedInSidebar: self.isExpandedInSidebar) + layerGroupId: mappableData.get(self.layerGroupId)) // Iterate through layer inputs self.layer.layerGraphNode.inputDefinitions.forEach { diff --git a/Stitch/Graph/Node/Port/Model/Field/FocusedUserEditField.swift b/Stitch/Graph/Node/Port/Model/Field/FocusedUserEditField.swift index 9c1fa7cb2..27ee33ab6 100644 --- a/Stitch/Graph/Node/Port/Model/Field/FocusedUserEditField.swift +++ b/Stitch/Graph/Node/Port/Model/Field/FocusedUserEditField.swift @@ -33,7 +33,7 @@ enum FocusedUserEditField: Equatable, Hashable { any, // default option llmRecordingModal, stitchAIPromptModal, - sidebarLayerTitle(LayerNodeId) + sidebarLayerTitle(String) var getTextFieldLayerInputEdit: PreviewCoordinate? { switch self { diff --git a/Stitch/Graph/Node/Port/View/Field/InputView/LayerNamesDropDownChoiceView.swift b/Stitch/Graph/Node/Port/View/Field/InputView/LayerNamesDropDownChoiceView.swift index df10a777d..033b521c3 100644 --- a/Stitch/Graph/Node/Port/View/Field/InputView/LayerNamesDropDownChoiceView.swift +++ b/Stitch/Graph/Node/Port/View/Field/InputView/LayerNamesDropDownChoiceView.swift @@ -91,9 +91,9 @@ extension GraphState { } @MainActor func getDescendants(for layer: LayerNodeId) -> LayerIdSet { - getDescendantsIds(id: layer, - groups: self.getSidebarGroupsDict(), - acc: .init()) + self.layersSidebarViewModel.getDescendantsIds(id: layer.asItemId) + .map { $0.asLayerNodeId } + .toSet } } diff --git a/Stitch/Graph/Node/Port/ViewModel/NodeRowObserver/NodeRowObserverExtensions.swift b/Stitch/Graph/Node/Port/ViewModel/NodeRowObserver/NodeRowObserverExtensions.swift index 303f0fed2..428848109 100644 --- a/Stitch/Graph/Node/Port/ViewModel/NodeRowObserver/NodeRowObserverExtensions.swift +++ b/Stitch/Graph/Node/Port/ViewModel/NodeRowObserver/NodeRowObserverExtensions.swift @@ -76,7 +76,7 @@ extension NodeRowObserver { // A row for a layer inspector is visible just if layer inspector is open case .layerInspector: - let layerFocused = graph.sidebarSelectionState.inspectorFocusedLayers.focused.contains(rowViewModel.id.nodeId.asLayerNodeId) + let layerFocused = graph.sidebarSelectionState.inspectorFocusedLayers.focused.contains(rowViewModel.id.nodeId) // TODO: why can't we the proper condition here? Why must we always return `true`? For perf, we only want to update inspector UI-fields if that inspector is open and this row observer's layer is actually focused; otherwise it's same as if we're updating an off-screen node // return showsLayerInspector && layerFocused diff --git a/Stitch/Graph/Node/Title/View/CanvasItemTitleView.swift b/Stitch/Graph/Node/Title/View/CanvasItemTitleView.swift index 4608ee21d..62ddab722 100644 --- a/Stitch/Graph/Node/Title/View/CanvasItemTitleView.swift +++ b/Stitch/Graph/Node/Title/View/CanvasItemTitleView.swift @@ -8,7 +8,7 @@ import SwiftUI import StitchSchemaKit -extension UUID { +extension CustomStringConvertible { var debugFriendlyId: String { String(self.description.dropLast(30)) } diff --git a/Stitch/Graph/Node/Util/NodeCreatedAction.swift b/Stitch/Graph/Node/Util/NodeCreatedAction.swift index 1a3099e42..fd183f19f 100644 --- a/Stitch/Graph/Node/Util/NodeCreatedAction.swift +++ b/Stitch/Graph/Node/Util/NodeCreatedAction.swift @@ -81,28 +81,6 @@ extension StitchDocumentViewModel { } self.visibleGraph.visibleNodesViewModel.nodes.updateValue(node, forKey: node.id) - if node.kind.isLayer { - log("had layer") - // Note: do not update sidebar-list-state until after the layer node has actually been added to GraphState - - // If we created a layer group, it will start out expanded - if case .layer(.group) = choice { - log("had layer group, will add to expanded") - node.layerNode?.isExpandedInSidebar = true - } - - self.visibleGraph.sidebarListState = getMasterListFrom( - layerNodes: self.visibleGraph.visibleNodesViewModel.layerNodes, - expanded: self.visibleGraph.getSidebarExpandedItems(), - orderedSidebarItems: self.visibleGraph.orderedSidebarLayers) - - // TODO: why is this necessary? - _updateStateAfterListChange( - updatedList: self.visibleGraph.sidebarListState, - expanded: self.visibleGraph.getSidebarExpandedItems(), - graphState: self.visibleGraph) - } - node.initializeDelegate(graph: self.visibleGraph, document: self) @@ -151,9 +129,7 @@ extension StitchDocumentViewModel { case .group: log("createNode: unexpectedly had Group node for NodeKind choice; exiting early") - #if DEBUG - fatalError() - #endif + fatalErrorIfDebug() return nil // TODO: break this logic up into smaller, separate functions, @@ -165,14 +141,14 @@ extension StitchDocumentViewModel { position: center.toCGSize, zIndex: highestZIndex + 1, graphDelegate: self.visibleGraph) else { - #if DEBUG - fatalError() - #endif + fatalErrorIfDebug() return nil } - + let sidebarLayerData = SidebarLayerData(id: layerNode.id) - self.visibleGraph.orderedSidebarLayers.insert(sidebarLayerData, at: 0) + var newSidebarData = self.visibleGraph.layersSidebarViewModel.createdOrderedEncodedData() + newSidebarData.insert(sidebarLayerData, at: 0) + self.visibleGraph.layersSidebarViewModel.update(from: newSidebarData) return layerNode diff --git a/Stitch/Graph/Node/Util/NodeDeletedAction.swift b/Stitch/Graph/Node/Util/NodeDeletedAction.swift index 13c7ecb14..f0332c674 100644 --- a/Stitch/Graph/Node/Util/NodeDeletedAction.swift +++ b/Stitch/Graph/Node/Util/NodeDeletedAction.swift @@ -22,7 +22,7 @@ struct DeleteShortcutKeyPressed: GraphEventWithResponse { // Check which we have focused: layers or canvas items if state.hasActivelySelectedLayers { - state.sidebarSelectedItemsDeletingViaEditMode() + state.layersSidebarViewModel.deleteSelectedItems() state.updateInspectorFocusedLayers() } @@ -137,6 +137,8 @@ extension GraphState { return } + let isLayer = node.kind.isLayer + // Find nodes to recursively delete switch node.kind { case .layer(let layer) where layer == .group: @@ -194,6 +196,11 @@ extension GraphState { node.inputs.findImportedMediaKeys().forEach { mediaKey in self.checkToDeleteMedia(mediaKey, from: node.id) } + + // Update sidebar + if isLayer { + self.layersSidebarViewModel.deleteItems(from: Set([id])) + } } /// Checks if some media is used elsewhere before proceeding to delete. diff --git a/Stitch/Graph/Node/Util/NodeViewModelUtils.swift b/Stitch/Graph/Node/Util/NodeViewModelUtils.swift index ddb721409..1d36f2b22 100644 --- a/Stitch/Graph/Node/Util/NodeViewModelUtils.swift +++ b/Stitch/Graph/Node/Util/NodeViewModelUtils.swift @@ -69,8 +69,7 @@ extension NodeViewModel { let layerNode = LayerNodeEntity(nodeId: id, layer: layerNode.layer, hasSidebarVisibility: true, - layerGroupId: nil, - isExpandedInSidebar: nil) + layerGroupId: nil) let nodeEntity = NodeEntity(id: id, nodeTypeEntity: .layer(layerNode), title: graphNode.defaultTitle) diff --git a/Stitch/Graph/Node/View/NodeTag/NodeTagMenuButtonsView.swift b/Stitch/Graph/Node/View/NodeTag/NodeTagMenuButtonsView.swift index d009aaa26..c61d6ba64 100644 --- a/Stitch/Graph/Node/View/NodeTag/NodeTagMenuButtonsView.swift +++ b/Stitch/Graph/Node/View/NodeTag/NodeTagMenuButtonsView.swift @@ -208,7 +208,7 @@ struct NodeTagMenuButtonsView: View { var hideLayerButton: some View { if let layerNode = node.layerNode { Button { - dispatch(SidebarItemHiddenStatusToggled(clickedId: layerNode.id.asLayerNodeId)) + dispatch(SidebarItemHiddenStatusToggled(clickedId: layerNode.id)) } label: { Text(layerNode.hasSidebarVisibility ? "Hide Layer" : "Unhide Layer") } diff --git a/Stitch/Graph/PrototypePreview/Layer/Util/LayerNodesSorting.swift b/Stitch/Graph/PrototypePreview/Layer/Util/LayerNodesSorting.swift index 2c0b4c1d8..9f51eaf70 100644 --- a/Stitch/Graph/PrototypePreview/Layer/Util/LayerNodesSorting.swift +++ b/Stitch/Graph/PrototypePreview/Layer/Util/LayerNodesSorting.swift @@ -428,19 +428,20 @@ func getLayerTypesForPinnedViews(pinnedData: LayerPinData, // views pinned to th return layerTypesAtThisLevel } -extension SidebarLayerList { +import StitchViewKit +extension Array where Element: StitchNestedListElement & Equatable { // TODO: remove after StitchViewModelKit's `StitchNestedList.get` method is fixed - func getSidebarLayerData(_ layerId: NodeId) -> SidebarLayerData? { - let layer: SidebarLayerData? = nil + func getSidebarLayerData(_ itemId: Element.ID) -> Element? { + let layer: Element? = nil for sidebarLayerData in self { - if sidebarLayerData.id == layerId { + if sidebarLayerData.id == itemId { return sidebarLayerData } - else if let layerFoundInChildren = sidebarLayerData.children?.getSidebarLayerData(layerId) { + else if let layerFoundInChildren = sidebarLayerData.children?.getSidebarLayerData(itemId) { return layerFoundInChildren } } // self.forEach @@ -448,7 +449,7 @@ extension SidebarLayerList { return layer } - func getSidebarLayerDataIndex(_ layerId: NodeId) -> Int? { + func getSidebarLayerDataIndex(_ layerId: Element.ID) -> Int? { let index: Int? = nil for sidebarLayerData in self { diff --git a/Stitch/Graph/Sidebar/Model/Gesture/SidebarListActiveGesture.swift b/Stitch/Graph/Sidebar/Model/Gesture/SidebarListActiveGesture.swift index 7a10f4ecc..46c5c5432 100644 --- a/Stitch/Graph/Sidebar/Model/Gesture/SidebarListActiveGesture.swift +++ b/Stitch/Graph/Sidebar/Model/Gesture/SidebarListActiveGesture.swift @@ -14,9 +14,9 @@ typealias LongPressAndDragGestureType = SequenceGesture<_EndedGesture> -enum SidebarListActiveGesture: Equatable { +enum SidebarListActiveGesture: Equatable where SidebarID: Equatable { case scrolling, // scrolling the entire list - dragging(SidebarListItemId), // drag or (long press + drag); on a single item + dragging(SidebarID), // drag or (long press + drag); on a single item swiping, // swiping single item none @@ -38,7 +38,7 @@ enum SidebarListActiveGesture: Equatable { } } - var dragId: SidebarListItemId? { + var dragId: SidebarID? { switch self { case .dragging(let x): return x diff --git a/Stitch/Graph/Sidebar/Model/Gesture/SidebarSelectionState.swift b/Stitch/Graph/Sidebar/Model/Gesture/SidebarSelectionState.swift index ff95058ac..d3c1d5401 100644 --- a/Stitch/Graph/Sidebar/Model/Gesture/SidebarSelectionState.swift +++ b/Stitch/Graph/Sidebar/Model/Gesture/SidebarSelectionState.swift @@ -7,35 +7,24 @@ import Foundation import StitchSchemaKit -import OrderedCollections -typealias OrderedLayerNodeIdSet = OrderedSet -typealias SidebarSelections = LayerIdSet -typealias NonEmptySidebarSelections = NonEmptyLayerIdSet - -extension LayerIdSet { - var asSidebarListItemIdSet: SidebarListItemIdSet { - self.map(\.asItemId).toSet - } -} - -struct InspectorFocusedLayers: Codable, Equatable, Hashable { +struct InspectorFocusedData { // Focused = what we see focused in the inspector - var focused = LayerIdSet() + var focused = Set() // Actively Selected = what we see focused in inspector + what user has recently tapped on - var activelySelected = LayerIdSet() + var activelySelected = Set() // Updated by regular or command click, but not shick click (with some exceptions) - var lastFocusedLayer: LayerNodeId? = nil + var lastFocusedLayer: ItemID? = nil // Inserts into both focused and activelySelected layer id sets - func insert(_ layer: LayerNodeId) -> Self { + func insert(_ layer: ItemID) -> Self { self.insert(.init([layer])) } - func insert(_ layers: LayerIdSet) -> Self { + func insert(_ layers: Set) -> Self { var data = self data.focused = data.focused.union(layers) data.activelySelected = data.activelySelected.union(layers) @@ -43,59 +32,49 @@ struct InspectorFocusedLayers: Codable, Equatable, Hashable { } } -extension SidebarSelections { - var nonEmptyPrimary: NonEmptySidebarSelections? { - if self.isEmpty { - return nil - } else { - return NES(self)! - } - } -} - -// if a group is selected, -struct SidebarSelectionState: Codable, Equatable, Hashable { - - var isEditMode: Bool = false - - // avoid this? - var madeStack: Bool = false +@Observable +final class SidebarSelectionObserver { + typealias SidebarSelections = Set var haveDuplicated: Bool = false - var optionDragInProgress: Bool = false + var optionDragInProgress: Bool = false // non-empty only during active layer drag (multi-drag only?) - var implicitlyDragged = SidebarListItemIdSet() + // var implicitlyDragged = SidebarListItemIdSet() // Layers focused in the inspector - var inspectorFocusedLayers = InspectorFocusedLayers() //LayerIdSet() + var inspectorFocusedLayers = InspectorFocusedData() //LayerIdSet() // items selected because directly clicked var primary = SidebarSelections() - + // items selected because eg their parent was selected var secondary = SidebarSelections() +} + +extension SidebarSelectionObserver { + func getSelectionStatus(_ id: ItemID) -> SidebarListItemSelectionStatus { + + if self.primary.contains(id) { + return .primary + } else if self.secondary.contains(id) { + return .secondary + } else { + return .none + } + + } var all: SidebarSelections { primary.union(secondary) } - - var nonEmptyPrimary: NonEmptySidebarSelections? { - self.primary.nonEmptyPrimary - } - - func isSelected(_ id: LayerNodeId) -> Bool { + + func isSelected(_ id: ItemID) -> Bool { all.contains(id) } - mutating func resetEditModeSelections() { + func resetEditModeSelections() { self.primary = SidebarSelections() self.secondary = SidebarSelections() } - - // better - mutating func combine(other: SidebarSelectionState) { - self.primary = self.primary.union(other.primary) - self.secondary = self.secondary.union(other.secondary) - } } diff --git a/Stitch/Graph/Sidebar/Model/LayerNodesForSidebarDict.swift b/Stitch/Graph/Sidebar/Model/LayerNodesForSidebarDict.swift deleted file mode 100644 index 193be5cad..000000000 --- a/Stitch/Graph/Sidebar/Model/LayerNodesForSidebarDict.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// LayerNodesForSidebarDict.swift -// Stitch -// -// Created by Christian J Clampitt on 3/12/24. -// - -import SwiftUI -import StitchSchemaKit -import OrderedCollections - -typealias LayerNodesForSidebarDict = OrderedDictionary - -// All the data the sidebar needs about a layer -struct LayerNodeForSidebar: Equatable, Codable, Hashable { - let id: LayerNodeId - let layer: Layer - let displayTitle: String -} diff --git a/Stitch/Graph/Sidebar/Model/ProjectSidebarTab.swift b/Stitch/Graph/Sidebar/Model/ProjectSidebarTab.swift new file mode 100644 index 000000000..70402d973 --- /dev/null +++ b/Stitch/Graph/Sidebar/Model/ProjectSidebarTab.swift @@ -0,0 +1,37 @@ +// +// ProjectSidebarTab.swift +// Stitch +// +// Created by Elliot Boschwitz on 10/23/24. +// + +import SwiftUI + +enum ProjectSidebarTab: String, Identifiable, CaseIterable { + case layers = "Layers" + case assets = "Assets" +} + +extension ProjectSidebarTab { + var id: String { + self.rawValue + } + + var iconName: String { + switch self { + case .layers: + return "square.3.layers.3d.down.left" + case .assets: + return "folder" + } + } + + var viewModelType: any ProjectSidebarObservable.Type { + switch self { + case .layers: + return LayersSidebarViewModel.self + default: + fatalError() + } + } +} diff --git a/Stitch/Graph/Sidebar/Model/SidebarDeps.swift b/Stitch/Graph/Sidebar/Model/SidebarDeps.swift deleted file mode 100644 index 31fd82d73..000000000 --- a/Stitch/Graph/Sidebar/Model/SidebarDeps.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// SidebarDeps.swift -// Stitch -// -// Created by Christian J Clampitt on 3/12/24. -// - -import Foundation - -import SwiftUI -import StitchSchemaKit -import OrderedCollections - -struct SidebarDeps: Equatable { - - // replace with LayerNodesForSidebarDict - // var layerNodes: LayerNodesDict - var layerNodes: LayerNodesForSidebarDict - var groups = SidebarGroupsDict() - var expandedItems = LayerIdSet() -} diff --git a/Stitch/Graph/Sidebar/Model/SidebarGroupsDict.swift b/Stitch/Graph/Sidebar/Model/SidebarGroupsDict.swift deleted file mode 100644 index 0461a0697..000000000 --- a/Stitch/Graph/Sidebar/Model/SidebarGroupsDict.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// SidebarGroupsDict.swift -// Stitch -// -// Created by Christian J Clampitt on 3/12/24. -// - -import Foundation -import StitchSchemaKit -import OrderedCollections - -typealias SidebarGroupsDict = OrderedDictionary - -extension SidebarGroupsDict { - - static func fromOrderedSidebarItems(_ orderedSidebarItems: OrderedSidebarLayers) -> SidebarGroupsDict { - - var partialResult = SidebarGroupsDict() - - // just assume top level for now; non-nested etc. - orderedSidebarItems.forEach { (orderedSidebarItem : SidebarLayerData) in - - partialResult = addToSidebarGroupsDict( - orderedSidebarItem: orderedSidebarItem, - partialResult: partialResult) - } - - return partialResult - } -} - -func addToSidebarGroupsDict(orderedSidebarItem: SidebarLayerData, - partialResult: SidebarGroupsDict) -> SidebarGroupsDict { - - var partialResult = partialResult - - let isGroupLayer = orderedSidebarItem.children.isDefined - - // careful: need to know whether the ordered sidebar item is for a group or not; can't just rely on children-list being present or not - if isGroupLayer, - let children = orderedSidebarItem.children { - - // Add a result for this OSI itself - partialResult[orderedSidebarItem.id.asLayerNodeId] = children.map(\.id.asLayerNodeId) - - // Then handle its children - children.forEach { childOSI in - partialResult = addToSidebarGroupsDict( - orderedSidebarItem: childOSI, - partialResult: partialResult) - } - } - - return partialResult -} - -extension GraphState { - func getSidebarGroupsDict() -> SidebarGroupsDict { - .fromOrderedSidebarItems(self.orderedSidebarLayers) - } -} diff --git a/Stitch/Graph/Sidebar/Model/SidebarItem/SidebarDragDestination.swift b/Stitch/Graph/Sidebar/Model/SidebarItem/SidebarDragDestination.swift new file mode 100644 index 000000000..e11fc7a8b --- /dev/null +++ b/Stitch/Graph/Sidebar/Model/SidebarItem/SidebarDragDestination.swift @@ -0,0 +1,35 @@ +// +// SidebarDragDestination.swift +// Stitch +// +// Created by Elliot Boschwitz on 10/23/24. +// + +import SwiftUI + +/// Helps us determine if we place items after a certain element or at the top of some group. +enum SidebarDragDestination { + case afterElement(Element) + case topOfGroup(Element?) // root if nil +} + +extension SidebarDragDestination { + var element: Element? { + switch self { + case .afterElement(let element): return element + case .topOfGroup(let element): return element + } + } + + var id: Element.ID? { + switch self { + case .afterElement(let element): return element.id + case .topOfGroup(let element): return element?.id + } + } + + var isAfter: Bool { + if case .afterElement = self { return true } + return false + } +} diff --git a/Stitch/Graph/Sidebar/Model/SidebarItem/SidebarItem.swift b/Stitch/Graph/Sidebar/Model/SidebarItem/SidebarItem.swift deleted file mode 100644 index abd944355..000000000 --- a/Stitch/Graph/Sidebar/Model/SidebarItem/SidebarItem.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// SidebarItemsData.swift -// Stitch -// -// Created by Christian J Clampitt on 4/7/22. -// - -import Foundation -import SwiftUI -import StitchSchemaKit - -typealias SidebarItems = [SidebarItem] - -struct SidebarItem: Equatable, Identifiable, Hashable, Codable { - - let layerName: LayerNodeTitle - let layerNodeId: LayerNodeId - - var id: LayerNodeId { - layerNodeId - } - var groupInfo: GroupInfo? - - var childItems: [SidebarItem] { - groupInfo?.elements ?? [] - } - - var children: SidebarItems { - childItems - } -} - -struct LayerNodeTitle: Equatable, Hashable, Codable { - let value: String - - init(_ s: String) { - value = s - } -} - -struct GroupInfo: Equatable, Hashable, Codable { - let groupdId: LayerNodeId - var elements: [SidebarItem] -} diff --git a/Stitch/Graph/Sidebar/Model/SidebarItem/SidebarListItem.swift b/Stitch/Graph/Sidebar/Model/SidebarItem/SidebarListItem.swift index 1d3ff6f16..de8e61905 100644 --- a/Stitch/Graph/Sidebar/Model/SidebarItem/SidebarListItem.swift +++ b/Stitch/Graph/Sidebar/Model/SidebarItem/SidebarListItem.swift @@ -8,61 +8,23 @@ import Foundation import StitchSchemaKit -struct SidebarListItem: Equatable, Codable, Hashable, Identifiable { - let id: SidebarListItemId - let layer: LayerNodeTitle - var location: CGPoint - var previousLocation: CGPoint - - var zIndex: ZIndex = 1 - var parentId: SidebarListItemId? // has a parent? - - let isGroup: Bool // is a parent for others? - - init(id: SidebarListItemId, - layer: LayerNodeTitle, - location: CGPoint, - parentId: SidebarListItemId? = nil, - isGroup: Bool) { - - self.id = id - self.layer = layer - self.location = location - self.previousLocation = location - self.parentId = parentId - self.isGroup = isGroup - } - +extension Identifiable { // this item's index - func itemIndex(_ items: SidebarListItems) -> Int { - items.firstIndex { $0.id == self.id }! - } - - // use previousLocation, which is not changed during drag, - // to know the item's indentation before being dragged. - var indentationLevel: IndentationLevel { - IndentationLevel.fromXLocation(x: self.previousLocation.x) + func itemIndex(_ items: [Self]) -> Int { + guard let index = items.firstIndex(where: { $0.id == self.id }) else { + fatalErrorIfDebug() + return -1 + } + + return index } } -typealias SidebarListItemIds = [SidebarListItemId] +typealias SidebarListItemId = NodeId -struct SidebarListItemId: Identifiable, Equatable, Hashable, Codable { - let value: UUID - - init(_ value: UUID) { - self.value = value - } - - var id: UUID { - value - } - - var asNodeId: NodeId { - value - } +typealias SidebarListItemIds = [SidebarListItemId] - var asLayerNodeId: LayerNodeId { - LayerNodeId(value) - } +struct SidebarIndex: Equatable { + let groupIndex: Int // horizontal + let rowIndex: Int // vertical } diff --git a/Stitch/Graph/Sidebar/Model/SidebarItem/SidebarListItemSelectionStatus.swift b/Stitch/Graph/Sidebar/Model/SidebarItem/SidebarListItemSelectionStatus.swift index 18dc7efd2..8f14c443c 100644 --- a/Stitch/Graph/Sidebar/Model/SidebarItem/SidebarListItemSelectionStatus.swift +++ b/Stitch/Graph/Sidebar/Model/SidebarItem/SidebarListItemSelectionStatus.swift @@ -9,7 +9,7 @@ import Foundation import SwiftUI import StitchSchemaKit -enum SidebarListItemSelectionStatus: Codable, Equatable { +enum SidebarListItemSelectionStatus { case primary, secondary, none // ie not selected // both primary and secondary count as 'being selected' @@ -35,17 +35,3 @@ enum SidebarListItemSelectionStatus: Codable, Equatable { } } } - -// what about when a group is collapsed? -func getSelectionStatus(_ id: LayerNodeId, - _ selections: SidebarSelectionState) -> SidebarListItemSelectionStatus { - - if selections.primary.contains(id) { - return .primary - } else if selections.secondary.contains(id) { - return .secondary - } else { - return .none - } - -} diff --git a/Stitch/Graph/Sidebar/Model/SidebarListState.swift b/Stitch/Graph/Sidebar/Model/SidebarListState.swift deleted file mode 100644 index dac2125d7..000000000 --- a/Stitch/Graph/Sidebar/Model/SidebarListState.swift +++ /dev/null @@ -1,134 +0,0 @@ -// -// CustomListData.swift -// prototype -// -// Created by Christian J Clampitt on 3/15/22. -// - -import Foundation -import SwiftUI -import StitchSchemaKit - -// // MARK: DATA - -struct SidebarListState: Codable, Equatable, Hashable { - var masterList: SidebarListItemsCoordinator - var current: SidebarDraggedItem? - var proposedGroup: ProposedGroup? - var cursorDrag: SidebarCursorHorizontalDrag? - - // empty initialization - init(_ masterList: MasterList = MasterList([]), - _ current: SidebarDraggedItem? = nil, - _ proposedGroup: ProposedGroup? = nil, - _ cursorDrag: SidebarCursorHorizontalDrag? = nil) { - self.masterList = masterList - self.current = current - self.proposedGroup = proposedGroup - self.cursorDrag = cursorDrag - } -} - -// if nil, then the 'proposed group' is top level -// and xIdentation = 0 -struct ProposedGroup: Equatable, Codable, Hashable { - - let parentId: SidebarListItemId - let xIndentation: CGFloat - - var indentationLevel: IndentationLevel { - IndentationLevel.fromXLocation(x: xIndentation) - } -} - -struct IndentationLevel: Equatable { - let value: Int - - init(_ value: Int) { - self.value = value - } - - func inc() -> IndentationLevel { - IndentationLevel(self.value + 1) - } - - func dec() -> IndentationLevel { - IndentationLevel(self.value - 1) - } - - static func fromXLocation(x: CGFloat) -> IndentationLevel { - IndentationLevel(Int(x / CGFloat(CUSTOM_LIST_ITEM_INDENTATION_LEVEL))) - } - - var toXLocation: CGFloat { - CGFloat(self.value * CUSTOM_LIST_ITEM_INDENTATION_LEVEL) - } -} - -struct SidebarDraggedItem: Equatable, Codable, Hashable { - // directly dragged - var current: SidebarListItemId - - // layers dragged along as part of children or which were otherwise explcitly-selected etc. - var draggedAlong: SidebarListItemIdSet -} - -typealias SidebarListItems = [SidebarListItem] - -// parentId: [children in order] -typealias ExcludedGroups = [SidebarListItemId: SidebarListItems] -typealias SidebarListItemIdSet = Set -typealias CollapsedGroups = SidebarListItemIdSet - -// TODO: better name or abstraction here? -struct SidebarListItemsCoordinator: Codable, Equatable, Hashable { - var items: SidebarListItems - // the [parentId: child-ids] that are not currently shown - var excludedGroups: ExcludedGroups - - // groups currently opened or closed; - // an item's id is added when its group closed, - // removed when its group opened; - // NOTE: a supergroup parent closing/opening does NOT affect a subgroup's closed/open status - var collapsedGroups: SidebarListItemIdSet - - init(_ items: SidebarListItems, - _ excludedGroups: ExcludedGroups = ExcludedGroups(), - _ collapsedGroups: SidebarListItemIdSet = SidebarListItemIdSet()) { - self.items = items - self.excludedGroups = excludedGroups - self.collapsedGroups = collapsedGroups - } -} - -extension SidebarListItemsCoordinator { - @MainActor - func appendToExcludedGroup(for key: SidebarListItemId, - _ newItem: SidebarListItem) -> SidebarListItemsCoordinator { - var masterList = self - - masterList.excludedGroups = Stitch.appendToExcludedGroup( - for: key, - [newItem], - masterList.excludedGroups) - - return masterList - } -} - -// `SidebarCursorDrag` represents the current position -// of user's cursor during a sidebar-list-item drag operation. -// We must keep track of the gesture's x-translation -// without changing the x-position of the being-dragged-item -// (which is controlled by `snapDescendants`). -struct SidebarCursorHorizontalDrag: Codable, Equatable, Hashable { - var x: CGFloat - var previousX: CGFloat - - // called at start of a drag gesture - @MainActor - static func fromItem(_ item: SidebarListItem) -> SidebarCursorHorizontalDrag { - SidebarCursorHorizontalDrag(x: item.location.x, - previousX: item.previousLocation.x) - } -} diff --git a/Stitch/Graph/Sidebar/Util/DeriveSidebarList.swift b/Stitch/Graph/Sidebar/Util/DeriveSidebarList.swift deleted file mode 100644 index a7c8e4981..000000000 --- a/Stitch/Graph/Sidebar/Util/DeriveSidebarList.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// DeriveSidebarList.swift -// Stitch -// -// Created by Christian J Clampitt on 3/14/24. -// - -import Foundation - -// TODO: this is a 'cover-all' for cases where we reactively update sidebar-ui-state after e.g. a node count change. -// Alternatively we could find all the actions that cause such a change, or locate this logic in the redux middleware itself. -// What we're really struggling with is handling derived data -struct DeriveSidebarList: GraphEvent { - - func handle(state: GraphState) { - state.updateSidebarListStateAfterStateChange() - } -} diff --git a/Stitch/Graph/Sidebar/Util/Gesture/LegacySidebarActions.swift b/Stitch/Graph/Sidebar/Util/Gesture/LegacySidebarActions.swift index 8200c54ed..0fc6c29fa 100644 --- a/Stitch/Graph/Sidebar/Util/Gesture/LegacySidebarActions.swift +++ b/Stitch/Graph/Sidebar/Util/Gesture/LegacySidebarActions.swift @@ -9,161 +9,104 @@ import Foundation import SwiftUI import StitchSchemaKit -struct SidebarListItemLongPressed: GraphEvent { - - let id: SidebarListItemId - - func handle(state: GraphState) { - - // log("SidebarListItemLongPressed called: id: \(id)") +let SIDEBAR_ITEM_MAX_Z_INDEX: ZIndex = 999 - state.sidebarListState.current = SidebarDraggedItem( - current: id, - // can be empty just because - // we're first starting the drag - draggedAlong: SidebarListItemIdSet()) +extension ProjectSidebarObservable { + @MainActor + func sidebarListItemLongPressed(itemId: Self.ItemID) { + self.currentItemDragged = itemId } -} - -import Foundation - -// Function to find the set item whose index in the list is the smallest -func findSetItemWithSmallestIndex(from set: LayerIdSet, - in list: [ListItem]) -> LayerNodeId? { - var smallestIndex: Int? = nil - var smallestItem: LayerNodeId? = nil - // Iterate through each item in the set - for item in set { - if let index = list.firstIndex(where: { $0.id == item.id }) { - // If it's the first item or if its index is smaller than the current smallest, update it - if smallestIndex == nil || index < smallestIndex! { - smallestIndex = index - smallestItem = item + // Function to find the set item whose index in the list is the smallest + func findSetItemWithSmallestIndex(from set: Set) -> Self.ItemID? { + var smallestIndex: Int? = nil + var smallestItem: Self.ItemID? = nil + + // Iterate through each item in the set + for item in set { + if let index = self.items.flattenedItems.firstIndex(where: { $0.id == item }) { + // If it's the first item or if its index is smaller than the current smallest, update it + if smallestIndex == nil || index < smallestIndex! { + smallestIndex = index + smallestItem = item + } } } - } - - // Return the item with the smallest index, or nil if no items from the set were found in the list - return smallestItem -} - -extension GraphState { - - // All the focused layers minus the actively dragged item - func getOtherSelections(draggedItem: SidebarListItemId) -> SidebarListItemIdSet { - var otherDragged = self.sidebarSelectionState - .inspectorFocusedLayers - .focused.map(\.asItemId) - .toSet - otherDragged.remove(draggedItem) - - return otherDragged - } -} - -func getDraggedAlongHelper(item: SidebarListItemId, - allItems: SidebarListItems, // for retrieving children - acc: SidebarListItemIdSet) -> SidebarListItemIdSet { - var acc = acc - acc.insert(item) - - let children = allItems.filter { $0.parentId == item } - children.forEach { child in - let updatedAcc = getDraggedAlongHelper(item: child.id, - allItems: allItems, - acc: acc) - acc = acc.union(updatedAcc) + // Return the item with the smallest index, or nil if no items from the set were found in the list + return smallestItem } - return acc -} - -func getDraggedAlong(_ draggedItem: SidebarListItem, - allItems: SidebarListItems, - acc: SidebarListItemIdSet, - selections: SidebarListItemIdSet) -> SidebarListItemIdSet { - - log("getDraggedAlong: draggedItem: \(draggedItem.layer) \(draggedItem.id)") - var acc = acc - - let explicitlyDraggedItems: SidebarListItemIdSet = selections.union([draggedItem.id]) - - explicitlyDraggedItems.forEach { explicitlyDraggedItem in - let updatedAcc = getDraggedAlongHelper(item: explicitlyDraggedItem, allItems: allItems, acc: acc) - acc = acc.union(updatedAcc) + func getDraggedAlong(_ draggedItem: Self.ItemViewModel, + selections: Set) -> Set { + let children = draggedItem.children?.map { $0.id } ?? [] + + return ([draggedItem.id] + children) + .toSet + .union(selections) } - return acc -} - - - -// Note: call this AFTER we've dragged and have a big list of all the 'dragged along' items -func getImplicitlyDragged(items: SidebarListItems, - draggedAlong: SidebarListItemIdSet, - selections: SidebarListItemIdSet) -> SidebarListItemIdSet { - - items.reduce(into: SidebarListItemIdSet()) { partialResult, item in - // if the item was NOT selected, yet was dragged along, - // then it is "implicitly" selected - if !selections.contains(item.id), - draggedAlong.contains(item.id) { - partialResult.insert(item.id) + // Note: call this AFTER we've dragged and have a big list of all the 'dragged along' items + func getImplicitlyDragged(draggedAlong: Set, + selections: Set) -> Set { + + self.items.flattenedItems.reduce(into: Set()) { partialResult, item in + // if the item was NOT selected, yet was dragged along, + // then it is "implicitly" selected + if !selections.contains(item.id), + draggedAlong.contains(item.id) { + partialResult.insert(item.id) + } } } -} - -struct SidebarListItemDragged: GraphEvent { - - let itemId: SidebarListItemId - let translation: CGSize - - func handle(state: GraphState) { + + @MainActor + func sidebarListItemDragged(item: Self.ItemViewModel, + translation: CGSize) { // log("SidebarListItemDragged called: item \(itemId) ") + guard let graph = self.graphDelegate else { + fatalErrorIfDebug() + return + } -// var list = state.sidebarListState - - log("SidebarListItemDragged: state.keypressState.isOptionPressed: \(state.keypressState.isOptionPressed)") - - var itemId = itemId + let state = self + // The tracked dragged item may change if option + click + var draggedItem = item // if state.keypressState.isOptionPressed && state.sidebarSelectionState.haveDuplicated { // if state.keypressState.isOptionPressed && state.sidebarSelectionState.optionDragInProgress { - if state.sidebarSelectionState.optionDragInProgress { + if state.selectionState.optionDragInProgress { // If we're currently doing an option+drag, then item needs to just be the top log("SidebarListItemDragged: had option drag and have already duplicated the layers") - if let selectedItemWithSmallestIndex = findSetItemWithSmallestIndex( - from: state.sidebarSelectionState.inspectorFocusedLayers.focused, - in: state.orderedSidebarLayers.getFlattenedList()) { + if let selectedItemIdWithSmallestIndex = self.findSetItemWithSmallestIndex( + from: state.inspectorFocusedLayers.focused), + let selectedItemWithSmallestIndex = self.items.get(selectedItemIdWithSmallestIndex) { log("SidebarListItemDragged: had option drag, will use selectedItemWithSmallestIndex \(selectedItemWithSmallestIndex) as itemId") - itemId = selectedItemWithSmallestIndex.asItemId + draggedItem = selectedItemWithSmallestIndex } } - let focusedLayers = state.sidebarSelectionState.inspectorFocusedLayers.focused + let focusedLayers = state.inspectorFocusedLayers.focused // Dragging a layer not already selected = dragging just that layer and deselecting all the others - if !focusedLayers.contains(itemId.asLayerNodeId) { - state.sidebarSelectionState.resetEditModeSelections() - let layerNodeId = itemId.asLayerNodeId - state.sidebarSelectionState.inspectorFocusedLayers.focused = .init([layerNodeId]) - state.sidebarSelectionState.inspectorFocusedLayers.activelySelected = .init([layerNodeId]) - state.sidebarItemSelectedViaEditMode(layerNodeId, + if !focusedLayers.contains(draggedItem.id) { + state.selectionState.resetEditModeSelections() + state.selectionState.inspectorFocusedLayers.focused = .init([draggedItem.id]) + state.selectionState.inspectorFocusedLayers.activelySelected = .init([draggedItem.id]) + state.sidebarItemSelectedViaEditMode(draggedItem.id, isSidebarItemTapped: true) - state.sidebarSelectionState.inspectorFocusedLayers.lastFocusedLayer = layerNodeId + state.selectionState.inspectorFocusedLayers.lastFocusedLayer = draggedItem.id } // if state.keypressState.isOptionPressed && !state.sidebarSelectionState.haveDuplicated { - if state.keypressState.isOptionPressed - && !state.sidebarSelectionState.haveDuplicated - && !state.sidebarSelectionState.optionDragInProgress { + if graph.keypressState.isOptionPressed + && !state.selectionState.haveDuplicated + && !state.selectionState.optionDragInProgress { log("SidebarListItemDragged: option held during drag; will duplicate layers") // duplicate the items @@ -171,10 +114,9 @@ struct SidebarListItemDragged: GraphEvent { // also, it aready updates the selected and focused sidebar layers etc. // But will the user's cursor still be on / under the original layer ? - state.sidebarSelectedItemsDuplicatedViaEditMode() - state.sidebarListState = state.sidebarListState - state.sidebarSelectionState.haveDuplicated = true - state.sidebarSelectionState.optionDragInProgress = true + state.graphDelegate?.sidebarSelectedItemsDuplicated() + state.selectionState.haveDuplicated = true + state.selectionState.optionDragInProgress = true log("") // return ? @@ -191,260 +133,184 @@ struct SidebarListItemDragged: GraphEvent { // do we need this `else if` ? // else if focusedLayers.count > 1 { if focusedLayers.count > 1 { - // log("SidebarListItemDragged: multiple selections; dragging an existing one") - // Turn the master list into a "master list with a stack" first, - if !state.sidebarSelectionState.madeStack, - let item = state.sidebarListState.masterList.items.first(where: { $0.id == itemId }), - - let masterListWithStack = getStack( - item, - items: state.sidebarListState.masterList.items, - selections: state.sidebarSelectionState.inspectorFocusedLayers.focused.asSidebarListItemIdSet) { - - // log("SidebarListItemDragged: masterListWithStack \(masterListWithStack.map(\.layer))") - - state.sidebarListState.masterList.items = masterListWithStack - state.sidebarSelectionState.madeStack = true - } - - if let selectedItemWithSmallestIndex = findSetItemWithSmallestIndex( - from: state.sidebarSelectionState.inspectorFocusedLayers.focused, - in: state.orderedSidebarLayers.getFlattenedList()), - itemId != selectedItemWithSmallestIndex.asItemId { + if let selectedItemIdWithSmallestIndex = self.findSetItemWithSmallestIndex( + from: state.selectionState.inspectorFocusedLayers.focused), + let selectedItemWithSmallestIndex = self.items.get(selectedItemIdWithSmallestIndex), + draggedItem.id != selectedItemIdWithSmallestIndex { // If we had mutiple layers focused, the "dragged item" should be the top item // (Note: we'll also move all the potentially-disparate/island'd layers into a single stack; so we may want to do this AFTER the items are all stacked? or we're just concerned about the dragged-item, not its index per se?) - itemId = selectedItemWithSmallestIndex.asItemId + draggedItem = selectedItemWithSmallestIndex // log("SidebarListItemDragged item is now \(selectedItemWithSmallestIndex) ") } } - - guard let item = state.sidebarListState.masterList.items.first(where: { $0.id == itemId }) else { - // if we couldn't find the item, it's been deleted - log("SidebarListItemDragged: item \(itemId) was already deleted") - return - } - let otherSelections = state.getOtherSelections(draggedItem: itemId) // log("SidebarListItemDragged: otherDragged \(otherSelections) ") - let (result, draggedAlong) = onSidebarListItemDragged( - item, // this dragged item - translation, // drag data - // ALL items - state.sidebarListState.masterList, - otherSelections: otherSelections) - - state.sidebarListState.current = result.beingDragged - state.sidebarListState.masterList = result.masterList - state.sidebarListState.proposedGroup = result.proposed - state.sidebarListState.cursorDrag = result.cursorDrag - - - // JUST USED FOR UI PURPOSES, color changes etc. - let implicitlyDragged = getImplicitlyDragged( - items: state.sidebarListState.masterList.items, - draggedAlong: draggedAlong, - selections: state.sidebarSelectionState.inspectorFocusedLayers.focused.asSidebarListItemIdSet) - state.sidebarSelectionState.implicitlyDragged = implicitlyDragged - - // Need to update the preview window then - _updateStateAfterListChange( - updatedList: state.sidebarListState, - expanded: state.getSidebarExpandedItems(), - graphState: state) - - // Recalculate the ordered-preview-layers - state.updateOrderedPreviewLayers() + self.onSidebarListItemDragged( + translation) } -} - -let SIDEBAR_ITEM_MAX_Z_INDEX: ZIndex = 999 - - -@MainActor -func onSidebarListItemDragged(_ item: SidebarListItem, // assumes we've already - _ translation: CGSize, - _ masterList: MasterList, - otherSelections: SidebarListItemIdSet) -> (SidebarListItemDraggedResult, SidebarListItemIdSet) { - - // log("onSidebarListItemDragged called: item.id: \(item.id)") - - var item = item - var masterList = masterList - var cursorDrag = SidebarCursorHorizontalDrag.fromItem(item) - let originalItemIndex = masterList.items.firstIndex { $0.id == item.id }! - - var alreadyDragged = SidebarListItemIdSet() - var draggedAlong = SidebarListItemIdSet() - - // log("onSidebarListItemDragged: otherSelections: \(otherSelections)") - // log("onSidebarListItemDragged: draggedAlong: \(draggedAlong)") - - // TODO: remove this property, and use an `isBeingDragged` check in the UI instead? - item.zIndex = SIDEBAR_ITEM_MAX_Z_INDEX - - // First time this is called, we pass in ALL items - let (newItems, - newIndices, - updatedAlreadyDragged, - updatedDraggedAlong) = updatePositionsHelper( - item, - masterList.items, - [], - translation, - otherSelections: otherSelections, - alreadyDragged: alreadyDragged, - draggedAlong: draggedAlong) - - // limit this from going negative? - cursorDrag.x = cursorDrag.previousX + translation.width - - masterList.items = newItems - item = masterList.items[originalItemIndex] // update the `item` too! - alreadyDragged = alreadyDragged.union(updatedAlreadyDragged) - draggedAlong = draggedAlong.union(updatedDraggedAlong) - - let calculatedIndex = calculateNewIndexOnDrag( - item: item, - items: masterList.items, - otherSelections: otherSelections, - draggedAlong: draggedAlong, - movingDown: translation.height > 0, - originalItemIndex: originalItemIndex, - movedIndices: newIndices) - - masterList.items = maybeMoveIndices( - originalItemId: item.id, - masterList.items, - indicesMoved: newIndices, - to: calculatedIndex, - originalIndex: originalItemIndex) + @MainActor + func onSidebarListItemDragged(_ translation: CGSize) { + let visualList = self.getVisualFlattenedList() + + // Track old count before selections are made below + // In-place removals mean we need to save this now + let oldCount = visualList.count - // i.e. get the index of this dragged-item, given the updated masterList's items - let updatedOriginalIndex = item.itemIndex(masterList.items) - // update `item` again! - item = masterList.items[updatedOriginalIndex] - - // should skip this for now? - let result = setItemsInGroupOrTopLevel( - item: item, - masterList: masterList, - otherSelections: otherSelections, - draggedAlong: draggedAlong, - cursorDrag: cursorDrag) - - return (result, draggedAlong) -} - -struct SidebarListItemDraggedResult { - let masterList: MasterList - let proposed: ProposedGroup? - let beingDragged: SidebarDraggedItem - let cursorDrag: SidebarCursorHorizontalDrag -} - -struct SidebarListItemDragEnded: GraphEventWithResponse { - - let itemId: SidebarListItemId - - func handle(state: GraphState) -> GraphResponse { - - log("SidebarListItemDragEnded called: itemId: \(itemId)") + let allSelections = self.selectionState + .inspectorFocusedLayers + .focused + + // Important to keep items in sorted order + let allDraggedItems = visualList.filter { item in + allSelections.contains(item.id) + } + + // Includes visible children of dragged nodes (aka implicitly dragged) + let allDraggedItemsPlusChildren = allDraggedItems.flattenedVisibleItems + + guard let firstDraggedItem = allDraggedItems.first else { + fatalErrorIfDebug() + return + } - var itemId = itemId + // Remove dragged items from data structure used for identifying drag location + let filteredVisualList = visualList.filter { item in + !allDraggedItemsPlusChildren.contains(where: { $0.id == item.id }) + } -// if state.keypressState.isOptionPressed && state.sidebarSelectionState.haveDuplicated { - if state.sidebarSelectionState.optionDragInProgress { - // If we're currently doing an option+drag, then item needs to just be the top - log("SidebarListItemDragged: had option drag and have already duplicated the layers") + let originalItemIndex = firstDraggedItem.sidebarIndex + + // Set state for a new drag + guard let firstDragPosition = firstDraggedItem.dragPosition else { + self.currentItemDragged = firstDraggedItem.id - if let selectedItemWithSmallestIndex = findSetItemWithSmallestIndex( - from: state.sidebarSelectionState.inspectorFocusedLayers.focused, - in: state.orderedSidebarLayers.getFlattenedList()) { - log("SidebarListItemDragged: had option drag, will use selectedItemWithSmallestIndex \(selectedItemWithSmallestIndex) as itemId") - itemId = selectedItemWithSmallestIndex.asItemId + // Remove elements from groups if there are selections inside other selected groups + Self.removeSelectionsFromGroups(selections: allDraggedItems) + + // Set up previous drag position, which we'll increment off of + allDraggedItemsPlusChildren.enumerated().forEach { index, item in + let initialPosition = CGPoint(x: item.location.x, + y: firstDraggedItem.location.y + (SIDEBAR_LIST_ITEM_ROW_COLORED_AREA_HEIGHT * CGFloat(index))) + + translation.toCGPoint + item.prevDragPosition = initialPosition + item.dragPosition = item.prevDragPosition } + + return } + // Dragging down = indices increase +// let isDraggingDown = (item.dragPosition?.y ?? .zero) > oldDragPosition.y - let item = state.sidebarListState.masterList.items.first { $0.id == itemId } - guard let item = item else { - // if we couldn't find the item, it's been deleted - log("SidebarListItemDragEnded: item \(itemId) was already deleted") - return .noChange + // Update drag positions + allDraggedItemsPlusChildren.forEach { draggedItem in + draggedItem.dragPosition = (draggedItem.prevDragPosition ?? .zero) + translation.toCGPoint } - - // if no `current`, then we were just swiping? - if let current = state.sidebarListState.current { - state.sidebarListState.masterList.items = onSidebarListItemDragEnded( - item, - state.sidebarListState.masterList.items, - otherSelections: state.getOtherSelections(draggedItem: itemId), - // MUST have a `current` - // NO! ... this can be nil now eg when we call our onDragEnded logic via swipe - draggedAlong: current.draggedAlong, - proposed: state.sidebarListState.proposedGroup) - } else { - log("SidebarListItemDragEnded: had no current, so will not do the full onDragEnded call") + + guard let calculatedIndex = Self.getMovedtoIndex( + firstItemLocation: firstDragPosition, + movingDown: translation.height > 0, + flattenedItems: filteredVisualList, + maxRowIndex: visualList.count - 1) else { + log("onSidebarListItemDragged: no index found") + return } + + if originalItemIndex != calculatedIndex { + self.movedDraggedItems(draggedElement: firstDraggedItem, + draggedItems: allDraggedItems, + visualList: filteredVisualList, + to: calculatedIndex, + draggedItemsPlusChildrenCount: allDraggedItemsPlusChildren.count, + oldCount: oldCount) + } + } + + /// Filters out collapsed groups. + /// List mut be flattened for drag gestures. + func getVisualFlattenedList() -> [Self.ItemViewModel] { + self.items.getVisualFlattenedList() + } + + @MainActor + func movedDraggedItems(draggedElement: Self.ItemViewModel, + draggedItems: [Self.ItemViewModel], + visualList: [Self.ItemViewModel], + to index: SidebarIndex, + draggedItemsPlusChildrenCount: Int, + oldCount: Int) { + + let visualList = visualList + let draggedItemIdSet = draggedItems.map(\.id).toSet + + let draggedToElementResult = visualList.findClosestElement(draggedElement: draggedElement, + to: index, + numItemsDragged: draggedItemsPlusChildrenCount) + + // We should have removed dragged elements from the visual list + assertInDebug(!draggedItems.contains(where: { $0.id == draggedToElementResult.id })) - // also reset: the potentially highlighted group, - state.sidebarListState.proposedGroup = nil - // the current dragging item, - state.sidebarListState.current = nil - // and the current x-drag tracking - state.sidebarListState.cursorDrag = nil - - state.sidebarSelectionState.madeStack = false - state.sidebarSelectionState.haveDuplicated = false - state.sidebarSelectionState.optionDragInProgress = false - state.sidebarSelectionState.implicitlyDragged = .init() + // Remove items from dragged set--these will be added later + var reducedItemsList = self.items + reducedItemsList.remove(draggedItemIdSet) + + guard !draggedItems.isEmpty else { return } + + let newItemsList = reducedItemsList.movedDraggedItems(draggedItems, + at: draggedToElementResult, + dragPositionIndex: index) + + // Don't use assert test after movedDraggedItems because of references to self list + assertInDebug(newItemsList.getVisualFlattenedList().count == oldCount) + + self.items = newItemsList + self.items.updateSidebarIndices() + + // TODO: should only be for layers sidebar + self.graphDelegate?.updateOrderedPreviewLayers() + } - return .persistenceResponse + /// Removes selected elements from other selected groups. + static func removeSelectionsFromGroups(selections: [Self.ItemViewModel]) { + var queue = selections + + // Traverse backwards by exploring parent delegate + while let element = queue.popLast() { + guard let parent = element.parentDelegate else { continue } + + if selections.contains(where: { $0.id == parent.id }) { + parent.children?.remove(element.id) + } + + queue.append(parent) + } } } +extension ProjectSidebarObservable { + @MainActor + func sidebarListItemDragEnded() { + +// log("sidebarListItemDragEnded called") -@MainActor -func onSidebarListItemDragEnded(_ item: SidebarListItem, - _ items: SidebarListItems, - otherSelections: SidebarListItemIdSet, - draggedAlong: SidebarListItemIdSet, - proposed: ProposedGroup?) -> SidebarListItems { - - log("onSidebarListItemDragEnded called") - - var items = items - var item = item - - item.zIndex = 0 // is this even used still? - let index = item.itemIndex(items) - items[index] = item + let state = self + + self.items.flattenedItems.forEach { + $0.dragPosition = nil + $0.prevDragPosition = nil + } - // finalizes items' positions by index; - // also updates items' previousPositions. - items = setYPositionByIndices( - originalItemId: item.id, - items, - isDragEnded: true) + self.currentItemDragged = nil - let allDragged: SidebarListItemIds = [item.id] + Array(draggedAlong) + otherSelections + // reset the current dragging item + state.currentItemDragged = nil - // update both the X and Y in the previousLocation of the items that were moved; - // ie `item` AND every id in `draggedAlong` - for draggedId in allDragged { - guard var draggedItem = retrieveItem(draggedId, items) else { - fatalErrorIfDebug("Could not retrieve item") - continue - } - draggedItem.previousLocation = draggedItem.location - items = updateSidebarListItem(draggedItem, items) + state.selectionState.haveDuplicated = false + state.selectionState.optionDragInProgress = false + + state.graphDelegate?.encodeProjectInBackground() } - - // reset the z-indices - items = updateZIndices(items, zIndex: 0) - - return items } diff --git a/Stitch/Graph/Sidebar/Util/Gesture/SidebarListActionHelpers.swift b/Stitch/Graph/Sidebar/Util/Gesture/SidebarListActionHelpers.swift index e97cd94df..bb515b07b 100644 --- a/Stitch/Graph/Sidebar/Util/Gesture/SidebarListActionHelpers.swift +++ b/Stitch/Graph/Sidebar/Util/Gesture/SidebarListActionHelpers.swift @@ -8,217 +8,31 @@ import Foundation import SwiftUI - -// When dragging: set actively-dragged and dragged-along items' z-indices to be high -// When drag ended: set all items z-indices = 0 -@MainActor -func updateZIndices(_ items: SidebarListItems, - zIndex: ZIndex) -> SidebarListItems { - items.map { - var item = $0 - item.zIndex = zIndex - return item - } -} - -let SIDEBAR_LIST_ITEM_MAX_Z_INDEX: ZIndex = 9999 -let SIDEBAR_LIST_ITEM_MIN_Z_INDEX: ZIndex = 0 - -@MainActor -func updateAllZIndices(items: SidebarListItems, - itemId: SidebarListItemId, - draggedAlong: SidebarListItemIdSet) -> SidebarListItems { - - var items = items - - let updatedItems = updateZIndices( - items.filter { - ($0.id == itemId) || draggedAlong.contains($0.id) - }, - zIndex: SIDEBAR_LIST_ITEM_MAX_Z_INDEX) - - for updatedItem in updatedItems { - items = updateSidebarListItem(updatedItem, items) - } - - return items -} - -@MainActor -func setItemsInGroupOrTopLevel(item: SidebarListItem, - masterList: MasterList, - otherSelections: SidebarListItemIdSet, - draggedAlong: SidebarListItemIdSet, - cursorDrag: SidebarCursorHorizontalDrag) -> SidebarListItemDraggedResult { - - var masterList = masterList - - // set all dragged items' z-indices to max - masterList.items = updateAllZIndices( - items: masterList.items, itemId: - item.id, draggedAlong: - draggedAlong) - - // Propose a group based on the dragged item (in Stack case, will be Stack's top item) - let proposed = proposeGroup( - item, - masterList, - draggedAlong.count, - cursorDrag: cursorDrag) - - let beingDragged = SidebarDraggedItem(current: item.id, - draggedAlong: draggedAlong) - - log("setItemsInGroupOrTopLevel: beingDragged: \(beingDragged)") - - if let proposed = proposed { - log("setItemsInGroupOrTopLevel: had proposed: \(proposed)") - masterList.items = moveSidebarListItemIntoGroup(item, - masterList.items, - otherSelections: otherSelections, - draggedAlong: draggedAlong, - proposed) - } - - // if no proposed group, then we moved item to top level: - // 1. reset done-dragging item's x to `0` - // 2. set item's parent to nil - else { - log("setItemsInGroupOrTopLevel: no proposed group; will snap to top level") - masterList.items = moveSidebarListItemToTopLevel(item, - masterList.items, - otherSelections: otherSelections, - draggedAlong: draggedAlong) - } - - return SidebarListItemDraggedResult(masterList: masterList, - proposed: proposed, - beingDragged: beingDragged, - cursorDrag: cursorDrag) -} - -// We've moved the item up or down (along with its children); -// did we move it enough to have a new index placement for it? -@MainActor -func calculateNewIndexOnDrag(item: SidebarListItem, - items: SidebarListItems, - otherSelections: SidebarListItemIdSet, - draggedAlong: SidebarListItemIdSet, - movingDown: Bool, - originalItemIndex: Int, - movedIndices: [Int]) -> Int { - - let maxMovedToIndex = getMaxMovedToIndex( - item: item, - items: items, - otherSelections: otherSelections, - draggedAlong: draggedAlong) - - var calculatedIndex = getMovedtoIndex( - item: item, - items: items, - otherSelections: otherSelections, - draggedAlong: draggedAlong, - maxIndex: maxMovedToIndex, - movingDown: movingDown) - - // log("calculateNewIndexOnDrag: originalItemIndex: \(originalItemIndex)") - // log("calculateNewIndexOnDrag: calculatedIndex was: \(calculatedIndex)") - - // Is this really correct? - // i.e. shouldn't this be the `maxMovedToIndex` ? - // er, this is like "absolute max index", looking at ALL items in the list - let maxIndex = items.count - 1 - - // Can't this be combined with something else? - calculatedIndex = adjustMoveToIndex( - calculatedIndex: calculatedIndex, - originalItemIndex: originalItemIndex, - movedIndices: movedIndices, - maxIndex: maxIndex) - - // log("calculateNewIndexOnDrag: calculatedIndex is now: \(calculatedIndex)") - - return calculatedIndex -} - -// the highest index we can have moved an item to; -// based on item count but with special considerations -// for whether we're dragging a group. -func getMaxMovedToIndex(item: SidebarListItem, - items: SidebarListItems, - otherSelections: SidebarListItemIdSet, - draggedAlong: SidebarListItemIdSet) -> Int { - - var maxIndex = items.count - 1 - - // log("getMaxMovedToIndex: maxIndex was \(maxIndex)") - - // Presumably we don't actually need to trck whether the `dragged item` is a group or not; `draggedAlong` already represents the children that will be dragged along - let itemsWithoutDraggedAlongOrOtherSelections = items.filter { x in !draggedAlong.contains(x.id) && !otherSelections.contains(x.id) } - - // log("getMaxMovedToIndex: itemsWithoutDraggedAlongOrOtherSelections \(itemsWithoutDraggedAlongOrOtherSelections.map(\.id))") - - maxIndex = itemsWithoutDraggedAlongOrOtherSelections.count - 1 - // log("getMaxMovedToIndex: maxIndex is now \(maxIndex)") - - return maxIndex -} - -func getMovedtoIndex(item: SidebarListItem, - items: SidebarListItems, - otherSelections: SidebarListItemIdSet, - draggedAlong: SidebarListItemIdSet, - maxIndex: Int, - movingDown: Bool) -> Int { - - let maxY = maxIndex * CUSTOM_LIST_ITEM_VIEW_HEIGHT - - var range = (0...maxY) - .filter { $0.isMultiple(of: CUSTOM_LIST_ITEM_VIEW_HEIGHT / 2) } - - range.append(range.last! + CUSTOM_LIST_ITEM_VIEW_HEIGHT/2 ) - - if movingDown { - range = range.reversed() - } - - // try to find the highest threshold we (our item's location.y) satisfy - for threshold in range { - - // for moving up, want to find the first threshold we UNDERSHOOT - // where range is (0, 50, 150, ..., 250) - - // for moving down, want to find the first treshold we OVERSHOOT - // where range is (250, ..., 150, 50, 0) - - let foundThreshold = movingDown - ? item.location.y > CGFloat(threshold) - : item.location.y < CGFloat(threshold) - - if foundThreshold { - var k = (CGFloat(threshold)/CGFloat(CUSTOM_LIST_ITEM_VIEW_HEIGHT)) - // if we're moving the item down, - // then we'll want to round up the threshold - if movingDown { - k.round(.up) - } else { - k.round(.down) - } - // NEVER RETURN AN INDEX HIGHER THAN MAX-INDEX - let ki = Int(k) - if ki > maxIndex { - print("getMovedtoIndex: maxIndex: \(maxIndex)") - return maxIndex - } else { - print("getMovedtoIndex: ki: \(ki)") - return ki - } - } +extension ProjectSidebarObservable { + @MainActor + static func getMovedtoIndex(firstItemLocation: CGPoint, + movingDown: Bool, + flattenedItems: [Self.ItemViewModel], + // captures max index before dragged elements were removed from list + maxRowIndex: Int) -> SidebarIndex? { + + let dragAdjustment = Double(CUSTOM_LIST_ITEM_VIEW_HEIGHT) / 2 + let maxGroupIndex = (flattenedItems.max { $0.sidebarIndex.groupIndex < $1.sidebarIndex.groupIndex }?.sidebarIndex.groupIndex ?? 0) + 1 + let dragX = max(firstItemLocation.x, 0) + let rawFloatX = Int(floor(dragX / Double(CUSTOM_LIST_ITEM_INDENTATION_LEVEL))) + + // Note: previous usage of rounding function used inaccurate "movingDown" logic that doesn't apppear any better + // than a slight dragAdjustment offset +// let fnRoundingY = movingDown ? ceil : floor + let dragY = max(firstItemLocation.y, 0) + let rawFloatY = (dragY + dragAdjustment) / Double(CUSTOM_LIST_ITEM_VIEW_HEIGHT) + + let groupIndex = min(Int(rawFloatX), maxGroupIndex) + let rowIndex = min(Int(rawFloatY), maxRowIndex) + + let sidebarIndex = SidebarIndex(groupIndex: groupIndex, rowIndex: rowIndex) +// log("row index: \(rowIndex)\trawFloatY: \(rawFloatY)") + + return sidebarIndex } - - // if didn't find anything, return the original index? - let k = items.firstIndex { $0.id == item.id }! - // log("getMovedtoIndex: k: \(k)") - return k } diff --git a/Stitch/Graph/Sidebar/Util/Gesture/SidebarListItemActions.swift b/Stitch/Graph/Sidebar/Util/Gesture/SidebarListItemActions.swift index a1d468b49..37df331ff 100644 --- a/Stitch/Graph/Sidebar/Util/Gesture/SidebarListItemActions.swift +++ b/Stitch/Graph/Sidebar/Util/Gesture/SidebarListItemActions.swift @@ -8,23 +8,16 @@ import Foundation import StitchSchemaKit -struct SidebarLayerHovered: GraphUIEvent { - let layer: LayerNodeId - - func handle(state: GraphUIState) { - state.highlightedSidebarLayers.insert(layer) +extension GraphUIState { + func sidebarLayerHovered(layerId: LayerNodeId) { + self.highlightedSidebarLayers.insert(layerId) } -} -struct SidebarLayerHoverEnded: GraphUIEvent { - let layer: LayerNodeId - - func handle(state: GraphUIState) { - state.highlightedSidebarLayers.remove(layer) + func sidebarLayerHoverEnded(layerId: LayerNodeId) { + self.highlightedSidebarLayers.remove(layerId) } } - /* "Sidebar item's hide-icon clicked" @@ -39,7 +32,7 @@ struct SidebarLayerHoverEnded: GraphUIEvent { */ struct SidebarItemHiddenStatusToggled: GraphEventWithResponse { - let clickedId: LayerNodeId + let clickedId: NodeId @MainActor func handle(state: GraphState) -> GraphResponse { @@ -50,7 +43,7 @@ struct SidebarItemHiddenStatusToggled: GraphEventWithResponse { struct SelectedLayersVisiblityUpdated: GraphEventWithResponse { - let selectedLayers: LayerIdSet + let selectedLayers: NodeIdSet let newVisibilityStatus: Bool @MainActor @@ -65,22 +58,17 @@ struct SelectedLayersVisiblityUpdated: GraphEventWithResponse { extension GraphState { @MainActor - func layerHiddenStatusToggled(_ clickedId: LayerNodeId, + func layerHiddenStatusToggled(_ clickedId: NodeId, // If provided, then we are explicitly setting true/false (for multiple layers) as opposed to just toggling an individual layer newVisibilityStatus: Bool? = nil) { - - guard let layerNode = self.getLayerNode(id: clickedId.id)?.layerNode else { + + guard let layerNode = self.getLayerNode(id: clickedId)?.layerNode else { log("SidebarItemHiddenStatusToggled: could not find layer node for clickedId \(clickedId.id)") fatalErrorIfDebug() // Is this bad? return } - let sidebarGroups = self.getSidebarGroupsDict() - - let descendants: LayerIdSet = getDescendantsIds( - id: clickedId, - groups: sidebarGroups, - acc: LayerIdSet()) + let descendants = self.getDescendants(for: clickedId.asLayerNodeId) if let newVisibilityStatus = newVisibilityStatus { layerNode.hasSidebarVisibility = newVisibilityStatus diff --git a/Stitch/Graph/Sidebar/Util/Gesture/SidebarListItemHelpers.swift b/Stitch/Graph/Sidebar/Util/Gesture/SidebarListItemHelpers.swift index 8b3e7f159..5dbd9e073 100644 --- a/Stitch/Graph/Sidebar/Util/Gesture/SidebarListItemHelpers.swift +++ b/Stitch/Graph/Sidebar/Util/Gesture/SidebarListItemHelpers.swift @@ -8,228 +8,12 @@ import Foundation import SwiftUI - - -extension SidebarListItem { - - //extension SidebarLayerData { - - func isSelected(_ selections: SidebarListItemIdSet) -> Bool { +extension SidebarItemSwipable { + func isSelected(_ selections: Set) -> Bool { selections.contains(self.id) } - func implicitlyDragged(_ implicitlyDraggedItems: SidebarListItemIdSet) -> Bool { + func implicitlyDragged(_ implicitlyDraggedItems: Set) -> Bool { implicitlyDraggedItems.contains(self.id) } - - func wipeIndentationLevel() -> Self { - var item = self - item.previousLocation.x = .zero - item.location.x = .zero - item.parentId = nil - return item - } - - func setIndentToOneLevel() -> Self { - var item = self - item.previousLocation.x = CGFloat(CUSTOM_LIST_ITEM_INDENTATION_LEVEL) - item.location.x = CGFloat(CUSTOM_LIST_ITEM_INDENTATION_LEVEL) - return item - } -} - -func getStack(_ draggedItem: SidebarListItem, - items: [SidebarListItem], - // all selections - selections: SidebarListItemIdSet) -> [SidebarListItem]? { - - guard let draggedItemIndex = items.firstIndex(where: { $0.id == draggedItem.id }) else { - print("getStack: no dragged item index") - return nil - } - - // All items that were dragged along, whether explicitly or implicitly selected - let draggedAlong = getDraggedAlong(draggedItem, - allItems: items, - acc: .init(), - selections: selections) - - // Items that were dragged along but not explicitly selected - let implicitlyDraggedItems: SidebarListItemIdSet = getImplicitlyDragged( - items: items, - draggedAlong: draggedAlong, - selections: selections) - - let nonDraggedItemsAbove = items.enumerated().compactMap { itemAndIndex in - itemAndIndex.offset < draggedItemIndex ? itemAndIndex.element : nil - }.filter { !$0.isSelected(selections) && !$0.implicitlyDragged(implicitlyDraggedItems)} - - let nonDraggedItemsBelow = items.enumerated().compactMap { itemAndIndex in - itemAndIndex.offset > draggedItemIndex ? itemAndIndex.element : nil - }.filter { !$0.isSelected(selections) && !$0.implicitlyDragged(implicitlyDraggedItems)} - - print("getStack: nonDraggedItemsAbove: \(nonDraggedItemsAbove.map(\.id))") - print("getStack: nonDraggedItemsBelow: \(nonDraggedItemsBelow.map(\.id))") - - // All items either explicitly-dragged (because selected) or implicitly-dragged (because a child of a selected parent) - let allDraggedItems = items.filter { $0.isSelected(selections) || $0.implicitlyDragged(implicitlyDraggedItems) } - - var draggedResult = [SidebarListItem]() - var itemsHandledBySomeChunk = SidebarListItemIdSet() - for draggedItem in allDraggedItems { - print("getStack: on draggedItem \(draggedItem.id)") - - if itemsHandledBySomeChunk.contains(draggedItem.id) { - print("getStack: draggedItem \(draggedItem.id) was already handled by some chunk") - continue - } - - let draggedItemIsSelected = draggedItem.isSelected(selections) - - // An explicitly-dragged parent kicks off a "chunk" - if draggedItem.isGroup, - draggedItemIsSelected { - print("getStack: draggedItem \(draggedItem.id) starts a chunk") - // wipe the draggedItem's - let chunk = rearrangeChunk( - selectedParentItem: draggedItem, - selections: selections, - implicitlyDragged: implicitlyDraggedItems, - flatMasterList: items) - - itemsHandledBySomeChunk = itemsHandledBySomeChunk.union(SidebarListItemIdSet.init(chunk.map(\.id))) - draggedResult += chunk - } - - // Explicitly selected items get their indents wiped - else if draggedItemIsSelected { - print("getStack: draggedItem \(draggedItem.id) is explicitly selected") - var draggedItem = draggedItem - draggedItem = draggedItem.wipeIndentationLevel() - draggedResult.append(draggedItem) - } - - else { - print("getStack: draggedItem \(draggedItem.id) is only implicitly-selected") - draggedResult.append(draggedItem) - } - } - - let rearrangedMasterList = nonDraggedItemsAbove + draggedResult + nonDraggedItemsBelow - - // Use the newly-reordered masterList's indices to update each master list item's y position - let _rearrangedMasterList = setYPositionByIndices( - originalItemId: draggedItem.id, - rearrangedMasterList, - // treat as drag ended so that we update previousLocation etc. - isDragEnded: true) - - return _rearrangedMasterList -} - -func rearrangeChunk(selectedParentItem: SidebarListItem, - selections: SidebarListItemIdSet, - implicitlyDragged: SidebarListItemIdSet, - flatMasterList: [SidebarListItem]) -> [SidebarListItem] { - - print("rearrangeChunk: on chunk begun by \(selectedParentItem.layer) \(selectedParentItem.id)") - - guard let selectedParentItemIndex: Int = flatMasterList.firstIndex(where: { $0.id == selectedParentItem.id }) else { - print("rearrangeChunk: no selected parent item index for \(selectedParentItem.id)") - return [] - } - - guard let chunkEnderIndex: Int = getChunkEnderIndex( - selectedParentItem: selectedParentItem, - selectedParentItemIndex: selectedParentItemIndex, - selections: selections, - flatMasterList: flatMasterList) else { - - print("rearrangeChunk: no chunkEnderIndex for \(selectedParentItem.id)") - return [] - } - log("chunkEnderIndex: \(chunkEnderIndex)") - - // let chunk = flatMasterList[selectedParentItemIndex...chunkEnderIndex] - - // exclude the parent itself? - // let chunk = flatMasterList[(selectedParentItemIndex + 1)...chunkEnderIndex] - - // excluded chunkEnder? - let chunk = flatMasterList[(selectedParentItemIndex + 1)...(chunkEnderIndex - 1)] - - print("rearrangeChunk: chunk: \(chunk.map(\.layer)) \(chunk.map(\.id))") - - let explicitlyDragged = chunk.filter { $0.isSelected(selections) } - let implicitlyDragged = chunk.filter { $0.implicitlyDragged(implicitlyDragged) } - - print("rearrangeChunk: explicitlyDragged: \(explicitlyDragged.map(\.layer)) \(explicitlyDragged.map(\.id))") - print("rearrangeChunk: implicitlyDragged: \(implicitlyDragged.map(\.layer)) \(implicitlyDragged.map(\.id))") - - let wipedExplicitlyDragged = wipeIndentationLevelsOfSelectedItems( - items: explicitlyDragged, - selections: selections) - - // Must also wipe the indentation level of the selectedParentItem - var selectedParentItem = selectedParentItem - selectedParentItem = selectedParentItem.wipeIndentationLevel() - - // Also, the implicitly-dragged children can at most have +1 indentation level, - // since their selected parent was made top level (i.e. identation level 0). - let oneIndentLevelImplicitlyDragged = implicitlyDragged.map { item in - var item = item -// item.indentationLevel = 1 - print("rearrangeChunk: item \(item.layer) indent was: \(item.indentationLevel)") - item = item.setIndentToOneLevel() - print("rearrangeChunk: item \(item.layer) indent is now: \(item.indentationLevel)") - return item - } - - return [selectedParentItem] + oneIndentLevelImplicitlyDragged + wipedExplicitlyDragged -} - - -// THE INDEX OF THE DOWN-THE-LIST TOP LEVEL ITEM that ends the chunk -func getChunkEnderIndex(selectedParentItem: SidebarListItem, - selectedParentItemIndex: Int, - selections: SidebarListItemIdSet, - flatMasterList: [SidebarListItem]) -> Int? { - - let itemsAndIndices = flatMasterList.enumerated() - - for itemAndIndex in itemsAndIndices { - let index = itemAndIndex.offset - let item = itemAndIndex.element - - if index > selectedParentItemIndex - && item.indentationLevel.value == 0 - && item.isSelected(selections) { - print("getChunkEnderIndex: found selected chunk ender index: \(index), item \(item)") - return index - } - } - - // It can happen that there is no top level item below us that is selected. - // In that case, we just grab the index of the first top level item below us. - for itemAndIndex in flatMasterList.enumerated() { - let index = itemAndIndex.offset - let item = itemAndIndex.element - - if index > selectedParentItemIndex - && item.indentationLevel.value == 0 { - print("getChunkEnderIndex: found chunk ender index: \(index), item \(item)") - return index - } - } - - // TODO: what happens if there's no item AT ALL below us? - // just return the last item in the chunk + 1 ? - if let maxIndex = itemsAndIndices.map(\.offset).max() { - print("getChunkEnderIndex: no layers below at all; will use max index \(maxIndex) chunk ender index") - // +1, so that we think we're going to some "imaginary" layer below us - return maxIndex + 1 - } - - print("getChunkEnderIndex: no chunk ender index") - return nil } diff --git a/Stitch/Graph/Sidebar/Util/Gesture/SidebarListUIActions.swift b/Stitch/Graph/Sidebar/Util/Gesture/SidebarListUIActions.swift deleted file mode 100644 index f4a009d28..000000000 --- a/Stitch/Graph/Sidebar/Util/Gesture/SidebarListUIActions.swift +++ /dev/null @@ -1,572 +0,0 @@ -// -// SidebarListUIActions.swift -// Stitch -// -// Created by Christian J Clampitt on 3/12/24. -// - -import Foundation -import SwiftUI - -// Actions related to modifying ui-state within the sidebar - -// functions just for onDragged and onDragEnded - -// you're just updating a single item -// but need to update all the descendants as well? -@MainActor -func moveSidebarListItemIntoGroup(_ item: SidebarListItem, - _ items: SidebarListItems, - otherSelections: SidebarListItemIdSet, - draggedAlong: SidebarListItemIdSet, - _ proposedGroup: ProposedGroup) -> SidebarListItems { - - let newParent = proposedGroup.parentId - - var items = items - - // Every explicitly dragged item gets the new parent - for otherSelection in ([item.id] + otherSelections) { - guard var otherItem = retrieveItem(otherSelection, items) else { - fatalErrorIfDebug("Could not retrieve item") - continue - } - otherItem.parentId = proposedGroup.parentId - otherItem.location.x = proposedGroup.indentationLevel.toXLocation - items = updateSidebarListItem(otherItem, items) - } - - guard let updatedItem = retrieveItem(item.id, items) else { - fatalErrorIfDebug("Could not retrieve item") - return items - } - - return maybeSnapDescendants(updatedItem, - items, - draggedAlong: draggedAlong, - startingIndentationLevel: proposedGroup.indentationLevel) -} - -@MainActor -func moveSidebarListItemToTopLevel(_ item: SidebarListItem, - _ items: SidebarListItems, - otherSelections: SidebarListItemIdSet, - draggedAlong: SidebarListItemIdSet) -> SidebarListItems { - - var items = items - - // Every explicitly dragged item gets its parent and indentation-level wiped - for otherSelection in ([item.id] + otherSelections) { - guard var otherItem = retrieveItem(otherSelection, items) else { - fatalErrorIfDebug("Could not retrieve item") - continue - } - otherItem.parentId = nil - otherItem.location.x = 0 - items = updateSidebarListItem(otherItem, items) - } - - guard let updatedItem = retrieveItem(item.id, items) else { - fatalErrorIfDebug("Could not retrieve item") - return items - } - - return maybeSnapDescendants(updatedItem, - items, - draggedAlong: draggedAlong, - startingIndentationLevel: IndentationLevel(0)) - -} - -@MainActor -func maybeSnapDescendants(_ item: SidebarListItem, - _ items: SidebarListItems, - draggedAlong: SidebarListItemIdSet, - // the indentation level from the proposed group - // (if top level then = 0) - startingIndentationLevel: IndentationLevel) -> SidebarListItems { - - log("maybeSnapDescendants: item at start: \(item)") - - let descendants = items.filter { draggedAlong.contains($0.id) } - - if descendants.isEmpty { - log("maybeSnapDescendants: no children for this now-top-level item \(item.id); exiting early") - return items - } - - let indentDiff: Int = startingIndentationLevel.value - item.indentationLevel.value - - var items = items - - for child in descendants { - var child = child - let childExistingIndent = child.indentationLevel.value - let newIndent = childExistingIndent + indentDiff - let finalChildIndent = IndentationLevel(newIndent) - child = setXLocationByIndentation(child, finalChildIndent) - items = updateSidebarListItem(child, items) - } - - return items -} - -func setXLocationByIndentation(_ item: SidebarListItem, - _ indentationLevel: IndentationLevel) -> SidebarListItem { - var item = item - item.location.x = indentationLevel.toXLocation - return item -} - -// accepts `parentIndentation` -// eg a child of a top level item will receive `parentIndentation = 50` -// and so child's x location must always be 50 greater than its parent -func updateYPosition(translation: CGSize, - location: CGPoint) -> CGPoint { - CGPoint(x: location.x, // NEVER adjust x - y: translation.height + location.y) -} - -// ie We've just REORDERED `items`, -// and now want to set their heights according to the REORDERED items. -func setYPositionByIndices(originalItemId: SidebarListItemId, - _ items: SidebarListItems, - isDragEnded: Bool = false) -> SidebarListItems { - - items.enumerated().map { (offset, item) in - var item = item - let newY = CGFloat(offset * CUSTOM_LIST_ITEM_VIEW_HEIGHT) - - if !isDragEnded && item.id == originalItemId { - print("setYPositionByIndices: will not change originalItemId \(originalItemId)'s y-position until drag-is-ended") - return item - } else { - item.location.y = newY - if isDragEnded { - print("setYPositionByIndices: drag ended, so resetting previous position") - item.previousLocation.y = newY - } - return item - } - } -} - -func wipeIndentationLevelsOfSelectedItems(items: SidebarListItems, - selections: SidebarListItemIdSet) -> SidebarListItems { - items.map { (item: SidebarListItem) in - if item.isSelected(selections) { - var item = item - item.location.x = 0 - item.previousLocation.x = 0 - item.parentId = nil // Also removes parent, if item is now top level - return item - } else { - return item - } - } -} - -func removeSelectedItemsFromParents(items: SidebarListItems, - selections: LayerIdSet) -> SidebarListItems { - // Must also iterate through selected items and set their parentId = nil - items.map { (item: SidebarListItem) in - if selections.contains(item.id.asLayerNodeId) { - var item = item - item.parentId = nil - return item - } else { - return item - } - } -} - -// Grab the item immediately below; -// if it has a parent (which should be above us), -// use that parent as the proposed group. -@MainActor -func groupFromChildBelow(_ item: SidebarListItem, - _ items: SidebarListItems, - movedItemChildrenCount: Int, - excludedGroups: ExcludedGroups) -> ProposedGroup? { - - log("groupFromChildBelow: item: \(item)") - // let debugItems = items.enumerated().map { ($0.offset, $0.element.layer) } - // log("groupFromChildBelow: items: \(debugItems)") - - let movedItemIndex = item.itemIndex(items) - - let entireIndex = movedItemIndex + movedItemChildrenCount - // log("groupFromChildBelow: entireIndex: \(entireIndex)") - - // must look at the index of the first item BELOW THE ENTIRE BEING-MOVED-ITEM-LIST - let indexBelow: Int = entireIndex + 1 - - // log("groupFromChildBelow: indexBelow: \(indexBelow)") - - guard let itemBelow = items[safeIndex: indexBelow] else { - log("groupFromChildBelow: no itemBelow") - return nil - } - - log("groupFromChildBelow: itemBelow: \(itemBelow)") - - guard let parentOfItemBelow = itemBelow.parentId else { - log("groupFromChildBelow: no parent on itemBelow") - return nil - } - - let itemsAbove = getItemsAbove(item, items) - - guard let parentItemAbove = itemsAbove.first(where: { $0.id == parentOfItemBelow }), - // added: - parentItemAbove.isGroup else { - log("groupFromChildBelow: could not find parent above") - return nil - } - - log("groupFromChildBelow: parentItemAbove: \(parentItemAbove)") - - let proposedParent = parentItemAbove.id - let proposedIndentation = parentItemAbove.indentationLevel.inc().toXLocation - - // we'll use the indentation level of the parent + 1 - return ProposedGroup(parentId: proposedParent, - xIndentation: proposedIndentation) -} - -@MainActor -func getItemsBelow(_ item: SidebarListItem, _ items: SidebarListItems) -> SidebarListItems { - let movedItemIndex = item.itemIndex(items) - // eg if movedItem's index is 5, - // then items below have indices 6, 7, 8, ... - return items.filter { $0.itemIndex(items) > movedItemIndex } -} - -@MainActor -func getItemsAbove(_ item: SidebarListItem, _ items: SidebarListItems) -> SidebarListItems { - let movedItemIndex = item.itemIndex(items) - // eg if movedItem's index is 5, - // then items above have indices 4, 3, 2, ... - return items.filter { $0.itemIndex(items) < movedItemIndex } -} - -@MainActor -func findDeepestParent(_ item: SidebarListItem, // the moved-item - _ masterList: SidebarListItemsCoordinator, - cursorDrag: SidebarCursorHorizontalDrag) -> ProposedGroup? { - - var proposed: ProposedGroup? - - log("findDeepestParent: item.id: \(item.id)") - log("findDeepestParent: item.location.x: \(item.location.x)") - log("findDeepestParent: cursorDrag: \(cursorDrag)") - - let items = masterList.items - let excludedGroups = masterList.excludedGroups - - let itemLocationX = cursorDrag.x - - for itemAbove in getItemsAbove(item, items) { - log("findDeepestParent: itemAbove.id: \(itemAbove.id)") - log("findDeepestParent: itemAbove.location.x: \(itemAbove.location.x)") - - // Is this dragged item east of the above item? - // Must be >, not >=, since = is for certain top level cases. - if itemLocationX > itemAbove.location.x { - let itemAboveHasChildren = hasChildren(itemAbove.id, masterList) - - // if the itemAbove us itself a parent, - // then we want to put our being-dragged-item into that itemAbove's child list; - // and NOT use that itemAbove's own parent as our group - if itemAboveHasChildren, - !excludedGroups[itemAbove.id].isDefined, - itemAbove.isGroup { - log("found itemAbove that has children; will make being-dragged-item") - - // make sure it's not a closed group that we're proposing! - - proposed = ProposedGroup(parentId: itemAbove.id, - xIndentation: itemAbove.indentationLevel.inc().toXLocation) - } - - // this can't quite be right -- - // eg we can find an item above us that has its own parent, - // we'd wrongly put the being-dragged-item into - - else if let itemAboveParentId = itemAbove.parentId, - !excludedGroups[itemAboveParentId].isDefined { - log("found itemAbove that is part of a group whose parent id is: \(itemAbove.parentId)") - proposed = ProposedGroup( - parentId: itemAboveParentId, - xIndentation: itemAbove.location.x) - } - - // if the item above is NOT itself part of a group, - // we'll just use the item above now as its parent - else if !excludedGroups[itemAbove.id].isDefined, - item.isGroup { - log("found itemAbove without parent") - proposed = ProposedGroup( - parentId: itemAbove.id, - xIndentation: IndentationLevel(1).toXLocation) - // ^^^ if item has no parent ie is top level, - // then need this indentation to be at least one level - } - log("findDeepestParent: found proposed: \(proposed)") - log("findDeepestParent: ... for itemAbove: \(itemAbove.id)") - } else { - log("\(item.id) was not at/east of itemAbove \(itemAbove.id)") - } - } - log("findDeepestParent: final proposed: \(String(describing: proposed))") - return proposed -} - -// if we're blocked by a top level item, -// then we ourselves must become a top level item -@MainActor -func blockedByTopLevelItemImmediatelyAbove(_ item: SidebarListItem, - _ items: SidebarListItems) -> Bool { - - let index = item.itemIndex(items) - if let immediatelyAbove = items[safeIndex: index - 1], - // `parentId: nil` = item is top level - // !immediatelyAbove.parentId.isDefined { - - // ie the item above us is not part of a group - !immediatelyAbove.parentId.isDefined, - - // ... and not itself a group. - // (if the item immediately above is a group, - // then we should allow it to be proposed) - !immediatelyAbove.isGroup { - - // log("blocked by child-less top-level item immediately above") - return true - } - return false -} - -@MainActor -func proposeGroup(_ item: SidebarListItem, // the moved-item - _ masterList: SidebarListItemsCoordinator, - _ draggedAlongCount: Int, // all dragged items, whether implicitly or explicitly selelected - cursorDrag: SidebarCursorHorizontalDrag) -> ProposedGroup? { - - // Note: we do not need to filter out otherSelectons etc., which are inc - let items = masterList.items - - // log("proposeGroup: will try to propose group for item: \(item.id)") - - // GENERAL RULE: - var proposed = findDeepestParent(item, - masterList, - cursorDrag: cursorDrag) - - // Exceptions: - - // Does the item have a non-parent top-level it immediately above it? - // if so, that blocks group proposal - if blockedByTopLevelItemImmediatelyAbove(item, items) { - log("proposeGroup: blocked by non-parent top-level item above") - proposed = nil - } - - if let groupDueToChildBelow = groupFromChildBelow( - item, - items, - movedItemChildrenCount: draggedAlongCount, - excludedGroups: masterList.excludedGroups) { - - log("proposeGroup: found group \(groupDueToChildBelow.parentId) from child below") - - // if our drag is east of the proposed-from-below's indentation level, - // and we already found a proposed group from 'deepest parent', - // then don't use proposed-from-below. - let keepProposed = (groupDueToChildBelow.indentationLevel.toXLocation < cursorDrag.x) && proposed.isDefined - - if !keepProposed { - log("proposeGroup: will use group from child below") - proposed = groupDueToChildBelow - } - } - - log("proposeGroup: returning: \(String(describing: proposed))") - - if let proposedParentId = proposed?.parentId, - let proposedParentItem = retrieveItem(proposedParentId, items), - !proposedParentItem.isGroup { - fatalErrorIfDebug() // Can never propose a parent that is not actually a group - return nil - } - - return proposed -} - -@MainActor -func updateSidebarListItem(_ item: SidebarListItem, - _ items: SidebarListItems) -> SidebarListItems { - let index = item.itemIndex(items) - var items = items - items[index] = item - return items -} - -// used only during on drag; -@MainActor -func updatePositionsHelper(_ item: SidebarListItem, - _ items: SidebarListItems, - _ indicesToMove: [Int], - _ translation: CGSize, - - // doesn't change during drag gesture itself - otherSelections: SidebarListItemIdSet, - alreadyDragged: SidebarListItemIdSet, - // changes during drag gesture? - draggedAlong: SidebarListItemIdSet) -> (SidebarListItems, - [Int], - SidebarListItemIdSet, - SidebarListItemIdSet) { - - // log("updatePositionsHelper for item \(item.id)") - // log("updatePositionsHelper: alreadyDragged at start of helper: \(alreadyDragged)") - - var item = item - - // When called from top level, this is the ENTIRE `masterList.items` - // ... and we never filter it, so we end up always passed - var items = items // When - - var indicesToMove = indicesToMove - var draggedAlong = draggedAlong - - // always update the item's position first: - item.location = updateYPosition( - translation: translation, - location: item.previousLocation) - - let index: Int = items.firstIndex { $0.id == item.id }! - items[index] = item - indicesToMove.append(index) - - - // Tricky: this recursively looks at every item in the last and checks whether we dragged its parent; if so, we adjust it - - var alreadyDragged = alreadyDragged // SidebarListItemIdSet() - - items.forEach { childItem in - - let isNotDraggedItem = childItem.id != item.id - // This is the meat of this function -- is this child item the child of the parent we're dragging ? - let isChildOfDraggedParent = childItem.parentId.map { $0 == item.id } ?? false - - let isOtherDragged = otherSelections.contains(childItem.id) - - let isNotAlreadyDragged = !alreadyDragged.contains(childItem.id) - // log("updatePositionsHelper: childItem: \(childItem.id)") - // log("updatePositionsHelper: isNotAlreadyDragged: \(isNotAlreadyDragged)") - - - if isNotDraggedItem && isNotAlreadyDragged && - (isChildOfDraggedParent || isOtherDragged) { - - draggedAlong.insert(childItem.id) - // log("updatePositionsHelper: alreadyDragged was: \(alreadyDragged)") - alreadyDragged.insert(childItem.id) - - let (newItems, - newIndices, - updatedAlreadyDragged, - updatedDraggedAlong) = updatePositionsHelper( - - childItem, - items, - indicesToMove, - translation, - otherSelections: otherSelections, - alreadyDragged: alreadyDragged, - draggedAlong: draggedAlong) - - // And we update the items - for newItem in newItems { - let i = items.firstIndex { $0.id == newItem.id }! - items[i] = newItem - } - - indicesToMove = newIndices - alreadyDragged = alreadyDragged.union(updatedAlreadyDragged) - draggedAlong = draggedAlong.union(updatedDraggedAlong) - } // if ... - - } // items.forEach - - return (items, indicesToMove, alreadyDragged, draggedAlong) -} - -func adjustMoveToIndex(calculatedIndex: Int, - originalItemIndex: Int, - movedIndices: [Int], - maxIndex: Int) -> Int { - - var calculatedIndex = calculatedIndex - - // Suppose we have [blue, black, green], - // blue is black's parent, - // and blue and green are both top level. - // If we move blue down, `getMovedtoIndex` will give us a new index of 1 instead of 0. - // But index 1 is the position of blue's child! - // So we add the diff. - if calculatedIndex > originalItemIndex { - let diff = calculatedIndex - originalItemIndex - print("adjustMoveToIndex: diff: \(diff)") - - // movedIndices is never going to be empty! - // it always has at least a single item - if movedIndices.isEmpty { - // calculatedIndex = calculatedIndex + diff - calculatedIndex += diff - print("adjustMoveToIndex: empty movedIndices: calculatedIndex is now: \(calculatedIndex)") - } else { - let maxMovedIndex = movedIndices.max()! - print("adjustMoveToIndex: maxMovedIndex: \(maxMovedIndex)") - calculatedIndex = maxMovedIndex + diff - print("adjustMoveToIndex: nonEmpty movedIndices: calculatedIndex is now: \(calculatedIndex)") - } - - if calculatedIndex > maxIndex { - print("adjustMoveToIndex: calculatedIndex was too large, will use max index instead") - calculatedIndex = maxIndex - } - return calculatedIndex - - } else { - print("adjustMoveToIndex: Will NOT adjust moveTo index") - return calculatedIndex - } -} - -func maybeMoveIndices(originalItemId: SidebarListItemId, - _ items: SidebarListItems, - indicesMoved: [Int], - to: Int, - originalIndex: Int) -> SidebarListItems { - - var items = items - - if to != originalIndex { - - let finalOffset = to > originalIndex ? to + 1 : to - - items.move(fromOffsets: IndexSet(indicesMoved), - toOffset: finalOffset) - - items = setYPositionByIndices( - originalItemId: originalItemId, - items, - isDragEnded: false) - - return items - } else { - return items - } -} diff --git a/Stitch/Graph/Sidebar/Util/LayerGrouping/SidebarListItemUtils.swift b/Stitch/Graph/Sidebar/Util/LayerGrouping/SidebarListItemUtils.swift deleted file mode 100644 index 7b122c99b..000000000 --- a/Stitch/Graph/Sidebar/Util/LayerGrouping/SidebarListItemUtils.swift +++ /dev/null @@ -1,388 +0,0 @@ -// -// SidebarListItemUtils.swift -// Stitch -// -// Created by Elliot Boschwitz on 6/13/24. -// - -import Foundation -import SwiftUI -import StitchSchemaKit -import OrderedCollections - -// the total height of the view, ie including padding etc. -//let CUSTOM_LIST_ITEM_VIEW_HEIGHT: Int = 44 - -// 28 is colored background; but need 4 padding? -//let CUSTOM_LIST_ITEM_VIEW_HEIGHT: Int = 28 -//let CUSTOM_LIST_ITEM_VIEW_HEIGHT: Int = 32 -let CUSTOM_LIST_ITEM_VIEW_HEIGHT: Int = Int(SIDEBAR_LIST_ITEM_ROW_COLORED_AREA_HEIGHT) - -// Per Figma, 12 pixels to east -// (During DragTest dev was `VIEW_HEIGHT / 2`) -//let CUSTOM_LIST_ITEM_INDENTATION_LEVEL: Int = 12 -//let CUSTOM_LIST_ITEM_INDENTATION_LEVEL: Int = 20 -let CUSTOM_LIST_ITEM_INDENTATION_LEVEL: Int = 24 - -extension SidebarListItem { - static var fakeSidebarListItem: Self { - SidebarListItem.init( - id: .init(NodeId.fakeNodeId), - layer: .init("Fake title"), - location: .zero, - isGroup: false) - } -} - -func asGroupIdSet(groups: SidebarGroupsDict) -> LayerIdSet { - groups.reduce(LayerIdSet()) { (acc: LayerIdSet, kv: (key: LayerNodeId, value: LayerIdList)) in - acc.union([kv.key] + kv.value) - } -} - -func asSidebarItems(groups: SidebarGroupsDict, - layerNodes: LayerNodesForSidebarDict) -> SidebarItems { - -// log("asSidebarItems: groups: \(groups)") - // DEBUG: this fn recevies - if layerNodes.isEmpty { - log("DEBUG: asSidebarItems: layer nodes in renderState were likely empty due to creation of custom debug appState.") - return [] - } - - // the "top level" ie non-nested sidebar items; - // but note that any of these top level sidebars could themselves contain - // sidebar items of their own. - var topLevelSidebarItems = SidebarItems() - // better?: get all ids associated with groups, whether as key or child, as a SET - // and if a layer node's id appears in that set, then it's not a 'simple case' - - // any layer node that is itself a group or is a child of a group - let groupIds: LayerIdSet = asGroupIdSet(groups: groups) - // log("asSidebarItems: groupIds: \(groupIds)") - - var handledNodes = LayerIdSet() - - // let maxAttempts = 500 - let maxAttempts = 200 - var attempts = 0 - - while handledNodes.count != layerNodes.count { - - // TO STOP US FROM FREEZING - attempts += 1 - if attempts >= maxAttempts { - // log("asSidebarItems: failed to generate new sidebar items") - #if DEV_DEBUG - log("asSidebarItems: handledNodes: \(handledNodes)") - log("asSidebarItems: layerNodes: \(layerNodes)") - // fatalError() - return [] - #endif - return [] - } - - layerNodes.forEach { (id: LayerNodeId, node: LayerNodeForSidebar) in - // log("asSidebarItems: on layer node: \(node.displayTitle) ... \(id)") - // let id = LayerNodeId(id) // update layerNodes dict to contain - - // if node already handled, then skip - if handledNodes.contains(id) { - // log("asSidebarItems: already handled id: \(id)") - } - - // simple case: layer node is - // - not a group layer node and - // - not in any group's children - else if !groupIds.contains(id) { - // - // log("asSidebarItems: simple case: id: \(id)") - topLevelSidebarItems.append( - SidebarItem(layerName: node.displayTitle.toLayerNodeTitle, - layerNodeId: id)) - - handledNodes.insert(id) - } - - // more complex case: layer is group itself or is a child in a group - else { - // log("asSidebarItems: complex case: id: \(id)") - let xs = asSidebarItemsHelper( - node, - groups, - layerNodes, - handledNodes: handledNodes - ) - topLevelSidebarItems += xs.0 - // add the handled nodes to current handled nodes - handledNodes = handledNodes.union(xs.1) - } - } - } - -// log("asSidebarItems: topLevelSidebarItems: \(topLevelSidebarItems)") -// log("asSidebarItems: topLevelSidebarItems.count: \(topLevelSidebarItems.count)") - return topLevelSidebarItems -} - -func asSidebarItemsHelper(_ node: LayerNodeForSidebar, // layer node whose .layer == .group - _ groups: SidebarGroupsDict, - _ layerNodes: LayerNodesForSidebarDict, // ALL layer nodes - handledNodes: LayerIdSet) -> (SidebarItems, LayerIdSet) { - - var items = SidebarItems() - var handledNodes: LayerIdSet = handledNodes - // log("asSidebarItemsHelper: groups: \(groups)") - // log("asSidebarItemsHelper: node id: \(node.id)") - // log("asSidebarItemsHelper: handledNodes: \(handledNodes)") - - // if the passed-in node is a child in a group node, - // rather than a group node itself, - // that's okay; we just return empty list for now; - // we'll eventually handle the passed-in node in some group - - // is this also supposed to be: "and node.layerNodeId" is not a group? - guard let children: LayerIdList = groups[node.id] - else { - // log("asSidebarItemsHelper: node.id \(node.id) is not itself a group node") - // ie don't yet the node.id to handled-nodes, since it's not handled yet - return (items, handledNodes) - } - - // if this current node.id is group and a child in some other group, - // AND the other group has NOT YET been handled already, - // we want to return an empty list; - if groups.contains(where: { (key: LayerNodeId, value: LayerIdList) in - // this node.id is a child of some other group - value.contains(node.id) - // and this other group has not yet been handled - && !handledNodes.contains(key) - }) { - // log("asSidebarItemsHelper: node.id \(node.id) is part of some other group that has not yet been handled") - return (items, handledNodes) - } - // ^^ MAYBE WE'RE HITTING THE ISSUE HERE? - // ^^ BUT THIS - - for childId in children { - guard let childNode: LayerNodeForSidebar = layerNodes[childId] else { - // Only hit once, on sidebar which may have bad data - log("asSidebarItemsHelper: no layer-node-for-sidebar for childId \(childId)") - continue - } - - // if child node is not a group... - if childNode.layer != .group { - // log("asSidebarItemsHelper: regular: childId: \(childId)") - items += [ - SidebarItem(layerName: childNode.displayTitle.toLayerNodeTitle, - layerNodeId: childId) - ] - // we've now officially handled this childid - handledNodes.insert(childId) - } - - // if child node IS a group layer node - else { - // log("asSidebarItemsHelper: group: childId: \(childId)") - handledNodes.insert(node.id) - - let ys = asSidebarItemsHelper(childNode, - groups, - layerNodes, - handledNodes: handledNodes) - - // log("asSidebarItemsHelper ys.0.count: \(ys.0.count)") - // log("asSidebarItemsHelper ys.1: \(ys.1)") - items += ys.0 - // if we turned a group layer child node into another list of items, - // then we've 'handled' that child node - handledNodes.insert(childId) - - // whatever other nodes we 'handled' during that process, - // we also add to our handled nodes - handledNodes = handledNodes.union(ys.1) - } - } - - // add a sidebar item for the group itself - let result = [SidebarItem( - layerName: node.displayTitle.toLayerNodeTitle, - layerNodeId: node.id, - groupInfo: GroupInfo(groupdId: node.id, - elements: items)) - ] - - // add the node id itself to handled nodes, if we successfully handled it - handledNodes.insert(node.id) - - // log("asSidebarItemsHelper: END: handledNodes: \(handledNodes)") - - return (result, handledNodes) -} - -func sidebarListItemsFromSidebarItems(_ sidebarItems: SidebarItems, - // which items are currently open - expanded: LayerIdSet) -> SidebarListItemsCoordinator { - - var currentHighestIndex = -1 - var items = SidebarListItems() - - // this isn't correct? - var collapsed = CollapsedGroups() - var excluded = ExcludedGroups() - - sidebarItems.forEach { sidebarItem in - // log("sidebarListItemsFromSidebarItems: on sidebarItem: \(sidebarItem)") - - let (newIndex, newItems, _, newExcluded, newCollapsed) = sidebarListItemsFromSidebarItemsHelper( - sidebarItem, - currentHighestIndex, - parentId: nil, // nil when at top level - nestingLevel: 0, // 0 when at top - expanded: expanded, - excluded: excluded, - collapsed: collapsed) - - currentHighestIndex = newIndex - items += newItems - - excluded.merge(newExcluded) { (items1: SidebarListItems, items2: SidebarListItems) in - mergeDictChildren(items1, items2) - } - collapsed = collapsed.union(newCollapsed) - } - - return SidebarListItemsCoordinator(items, excluded, collapsed) -} - -func sidebarListItemsFromSidebarItemsHelper(_ sidebarItem: SidebarItem, - _ currentHighestIndex: Int, - parentId: SidebarListItemId?, - nestingLevel: Int, - expanded: LayerIdSet, - excluded: ExcludedGroups, - collapsed: CollapsedGroups) -> (Int, - SidebarListItems, - Int, - ExcludedGroups, - CollapsedGroups) { - - var currentHighestIndex = currentHighestIndex - var items = SidebarListItems() - var nestingLevel = nestingLevel - var excluded = excluded - var collapsed = collapsed - - currentHighestIndex += 1 - - let hasChildren = sidebarItem.groupInfo.isDefined - - var item = SidebarListItem( - id: SidebarListItemId(sidebarItem.id.id), - layer: sidebarItem.layerName, - // location: CGPoint(x: (CUSTOM_LIST_ITEM_VIEW_HEIGHT / 2) * nestingLevel, - location: CGPoint(x: CUSTOM_LIST_ITEM_INDENTATION_LEVEL * nestingLevel, - y: CUSTOM_LIST_ITEM_VIEW_HEIGHT * currentHighestIndex), - parentId: parentId, - isGroup: hasChildren) - - // If item is a collapsed parent, then add it to excluded groups but not `items`. - - // TODO: find a better way to handle this such that we don't need `expanded` anymore... - if item.isGroup && expanded.doesNotContain(item.id.asLayerNodeId) { - // log("itemsFromSidebarHelper: excluding a non-expanded parent: \(item.id)") - collapsed.insert(item.id) - excluded = appendToExcludedGroup(for: item.id, [], excluded) - } - - // If this is a child whose parent is collapsed, - // then add this child to excluded groups, - // and remove from it `items` list. - if let parentId = item.parentId, - collapsed.contains(parentId) { - // log("itemsFromSidebarHelper: excluding a child of a non-expanded parent: \(item.id)") - // decrement currentHighestIndex because we won't actually show this item - currentHighestIndex -= 1 - // item.location = CGPoint(x: (CUSTOM_LIST_ITEM_VIEW_HEIGHT / 2) * nestingLevel, - item.location = CGPoint(x: CUSTOM_LIST_ITEM_INDENTATION_LEVEL * nestingLevel, - y: CUSTOM_LIST_ITEM_VIEW_HEIGHT * currentHighestIndex) - excluded = appendToExcludedGroup(for: parentId, [item], excluded) - } else { - // log("itemsFromSidebarHelper: adding item id: \(item.id)") - items.append(item) - } - - if !hasChildren { - // log("No children, so returning") - return (currentHighestIndex, items, nestingLevel, excluded, collapsed) - } - - // if we're about to go down another level, - // increment the nesting - if hasChildren { - nestingLevel += 1 - } - - // log("itemsFromSidebarHelper: had children: sidebarItem.children: \(sidebarItem.children)") - - sidebarItem.children.forEach { sidebarItemChild in - // log("itemsFromSidebarHelper: on child: sidebarItemChild: \(sidebarItemChild)") - let (newIndex, - newItems, - newLevel, - newExcluded, - newCollapsed) = sidebarListItemsFromSidebarItemsHelper(sidebarItemChild, - currentHighestIndex, - parentId: item.id, - nestingLevel: nestingLevel, - expanded: expanded, - excluded: excluded, - collapsed: collapsed) - - currentHighestIndex = newIndex - items += newItems - nestingLevel = newLevel - excluded.merge(newExcluded) { (items1: SidebarListItems, items2: SidebarListItems) in - mergeDictChildren(items1, items2) - } - - collapsed = collapsed.union(newCollapsed) - - } - // While looking through the children, - // we had an increased nesting level, - // but when we're done, we move back out, - // and so must decrement the nesting level. - nestingLevel -= 1 - - return (currentHighestIndex, items, nestingLevel, excluded, collapsed) -} - -// used for reduce-like operations on ExcludedGroups -func mergeDictChildren(_ items1: SidebarListItems, - _ items2: SidebarListItems) -> SidebarListItems { - OrderedSet(items1 + items2).elements -} - -extension String { - var toLayerNodeTitle: LayerNodeTitle { - LayerNodeTitle(self) - } -} - -extension SidebarItem { - func toSidebarLayerData() -> SidebarLayerData { - let item = self - -// SidebarLayerData(id: item.id.asNodeId, -// children: item.groupInfo?.elements?.map({ $0.toSidebarLayerData() })) - - if let children = item.groupInfo?.elements { - return SidebarLayerData(id: item.id.asNodeId, - children: children.map { $0.toSidebarLayerData() }) - } else { - return SidebarLayerData(id: item.id.asNodeId) - } - } -} diff --git a/Stitch/Graph/Sidebar/Util/LegacySidebarData.swift b/Stitch/Graph/Sidebar/Util/LegacySidebarData.swift deleted file mode 100644 index d81b5b820..000000000 --- a/Stitch/Graph/Sidebar/Util/LegacySidebarData.swift +++ /dev/null @@ -1,130 +0,0 @@ -// -// LegacySidebarData.swift -// Stitch -// -// Created by Christian J Clampitt on 3/7/24. -// - -import Foundation -import StitchSchemaKit - -func flattenExcludedGroups(_ excluded: ExcludedGroups) -> SidebarListItems { - excluded.flatMap { (_: SidebarListItemId, value: SidebarListItems) in - value - } -} - - -// Any group that is not collapsed is considered 'expanded' -func expandedItemsFromCollapsedGroups(_ layerNodes: LayerNodesForSidebarDict, - _ collapsedGroups: SidebarListItemIdSet) -> LayerIdSet { - var acc = LayerIdSet() - - layerNodes.values.forEach { node in - let id = SidebarListItemId(node.id.id) - if node.layer == .group && !collapsedGroups.contains(id) { - // log("expandedItemsFromCollapsedGroups: will add node.layerNodeId to current expanded") - acc.insert(node.id) - } - } - - return acc -} - - -/* - Simplifying assumptions: - - all groups are open (i.e. none are collapsed; so can leave - */ -// e.g. we just added a layer node, so need to produce a new -//func getMasterListFrom(existingMasterList: MasterList, // for existing collapsed groups etc. -// layerNodes: LayerNodesDict, -// orderedSidebarItems: SidebarLayerList) -> MasterList { - -//func getMasterListFrom(layerNodes: LayerNodesDict, -// orderedSidebarItems: SidebarLayerList) -> MasterList { - -// Really, we return not just the master list but the SidebarListState which contains `current` etc. -func getMasterListFrom(layerNodes: NodesViewModelDict, - expanded: LayerIdSet, - orderedSidebarItems: SidebarLayerList) -> SidebarListState { - - let masterList = _updateListAfterStateChange( - orderedSidebarLayers: orderedSidebarItems, - expanded: expanded, - layerNodes: layerNodes) - - var state = SidebarListState() - state.masterList = masterList - return state -} - -extension LayerNodesForSidebarDict { - // assumes `nodes` is layer nodes only - static func fromLayerNodesDict(nodes: NodesViewModelDict, - orderedSidebarItems: OrderedSidebarLayers) -> LayerNodesForSidebarDict { - - orderedSidebarItems.reduce(into: LayerNodesForSidebarDict()) { partialResult, osi in - partialResult = addLayerNodeForSidebarToDict( - nodes: nodes, - orderedSidebarItem: osi, - partialResult: partialResult) - } - } -} - -func addLayerNodeForSidebarToDict(nodes: NodesViewModelDict, - orderedSidebarItem: SidebarLayerData, - partialResult: LayerNodesForSidebarDict) -> LayerNodesForSidebarDict { - - // log("addLayerNodeForSidebarToDict called for orderedSidebarItem: \(orderedSidebarItem)") - - var partialResult = partialResult - - if let node = nodes.get(orderedSidebarItem.id), - let layerNode = node.layerNode { - - partialResult[node.id.asLayerNodeId] = LayerNodeForSidebar( - id: node.id.asLayerNodeId, - layer: layerNode.layer, - displayTitle: node.getDisplayTitle()) - - orderedSidebarItem.children?.forEach({ childOSI in - partialResult = addLayerNodeForSidebarToDict( - nodes: nodes, - orderedSidebarItem: childOSI, - partialResult: partialResult) - }) - - - } else { - // e.g. the layer node was deleted - log("LayerNodesForSidebarDict: fromLayerNodesDict: did not have node for OrderedSidebarItem \(orderedSidebarItem.id)") - } - - return partialResult -} - -func _updateListAfterStateChange(orderedSidebarLayers: SidebarLayerList, - expanded: LayerIdSet, - layerNodes: NodesViewModelDict) -> MasterList { - - - let layerNodesForSidebar: LayerNodesForSidebarDict = .fromLayerNodesDict( - nodes: layerNodes, - orderedSidebarItems: orderedSidebarLayers) - - let groups: SidebarGroupsDict = .fromOrderedSidebarItems( - orderedSidebarLayers) - - let items: SidebarItems = asSidebarItems( - groups: groups, - layerNodes: layerNodesForSidebar) - - let masterList = sidebarListItemsFromSidebarItems( - items, - // i.e. which groups are open - expanded: expanded) - - return masterList -} diff --git a/Stitch/Graph/Sidebar/Util/ListModification/SidebarGroupCreated.swift b/Stitch/Graph/Sidebar/Util/ListModification/SidebarGroupCreated.swift index 659e70a12..9791c9d65 100644 --- a/Stitch/Graph/Sidebar/Util/ListModification/SidebarGroupCreated.swift +++ b/Stitch/Graph/Sidebar/Util/ListModification/SidebarGroupCreated.swift @@ -11,25 +11,35 @@ import SwiftUI // when a sidebar group is created from a selection of sidebar items, // we should insert the group at the location of the -struct SidebarGroupCreated: StitchDocumentEvent { - - func handle(state: StitchDocumentViewModel) { +extension LayersSidebarViewModel { + @MainActor + func sidebarGroupCreated() { log("SidebarGroupCreated called") + guard let graph = self.graphDelegate, + let state = graph.documentDelegate else { + return + } + // Create node view model for the new Layer Group let newNode = Layer.group.layerGraphNode.createViewModel( position: state.newNodeCenterLocation, - zIndex: state.visibleGraph.highestZIndex + 1, - graphDelegate: state.visibleGraph) + zIndex: graph.highestZIndex + 1, + graphDelegate: graph) - let primarilySelectedLayers = state.visibleGraph.sidebarSelectionState.primary.map { $0.asNodeId }.toSet + let primarilySelectedLayers: Set = self.selectionState.primary // Are any of these selections already part of a group? // If so, the newly created LayerGroup will have that group as its own parent (layerGroupId). - let existingParentForSelections = state.visibleGraph.layerGroupForSelections(primarilySelectedLayers) - guard let newGroupData = state.visibleGraph.orderedSidebarLayers + var existingParentForSelections: SidebarListItemId? + if let _existingParentForSelections = graph.layerGroupForSelections(primarilySelectedLayers) { + existingParentForSelections = .init(_existingParentForSelections) + } + + let encodedData = self.createdOrderedEncodedData() + guard let newGroupData = encodedData .createGroup(newGroupId: newNode.id, parentLayerGroupId: existingParentForSelections, selections: primarilySelectedLayers) else { @@ -38,35 +48,32 @@ struct SidebarGroupCreated: StitchDocumentEvent { } // newNode.adjustPosition(center: state.newNodeCenterLocation) - newNode.graphDelegate = state.visibleGraph // redundant? + newNode.graphDelegate = graph // redundant? // Add to state state.nodeCreated(node: newNode) // Update sidebar state - state.visibleGraph.orderedSidebarLayers.insertGroup(group: newGroupData, - selections: primarilySelectedLayers) + var newEncodableData = encodedData + newEncodableData.insertGroup(group: newGroupData, + selections: primarilySelectedLayers) newNode.layerNode?.layerGroupId = existingParentForSelections - // Newly created groups start out expanded: - newNode.layerNode?.isExpandedInSidebar = true - // Iterate through primarly selected layers, // assigning new LG as their layerGoupId. primarilySelectedLayers.forEach { layerId in - if let layerNode = state.visibleGraph.getLayerNode(id: layerId)?.layerNode { + if let layerNode = graph.getLayerNode(id: layerId)?.layerNode { layerNode.layerGroupId = newNode.id } } - // Update legacy state - state.visibleGraph.updateSidebarListStateAfterStateChange() + self.update(from: newEncodableData) // Only reset edit mode selections if we're explicitly in edit mode (i.e. on iPad) - if state.graph.sidebarSelectionState.isEditMode { + if self.isEditing { // Reset selections - state.visibleGraph.sidebarSelectionState.resetEditModeSelections() + self.selectionState.resetEditModeSelections() } @@ -74,9 +81,9 @@ struct SidebarGroupCreated: StitchDocumentEvent { // TODO: adjust position of children // TODO: determine real size of just-created LayerGroup - let groupFit: LayerGroupFit = state.visibleGraph.getLayerGroupFit( + let groupFit: LayerGroupFit = graph.getLayerGroupFit( primarilySelectedLayers, - parentSize: state.visibleGraph.getParentSizeForSelectedNodes(selectedNodes: primarilySelectedLayers)) + parentSize: graph.getParentSizeForSelectedNodes(selectedNodes: primarilySelectedLayers)) // TODO: any reason to not use .auto x .auto for a nearly created group? ... perhaps for .background, which can become too big in a group whose children use .position modifiers? // TODO: how important is the LayerGroupFit.adjustment/offset etc. ? @@ -88,7 +95,7 @@ struct SidebarGroupCreated: StitchDocumentEvent { // Update layer group's size input newNode.layerNode?.sizePort.updatePortValues([.size(assumedLayerGroupSize)]) - state.visibleGraph.persistNewNode(newNode) + graph.persistNewNode(newNode) } } diff --git a/Stitch/Graph/Sidebar/Util/ListModification/SidebarGroupUncreated.swift b/Stitch/Graph/Sidebar/Util/ListModification/SidebarGroupUncreated.swift index 1d9cdd29e..6443b65bf 100644 --- a/Stitch/Graph/Sidebar/Util/ListModification/SidebarGroupUncreated.swift +++ b/Stitch/Graph/Sidebar/Util/ListModification/SidebarGroupUncreated.swift @@ -12,49 +12,58 @@ import SwiftUI // eg we had only groups selected, and pressed 'ungroup' // note: see `_SidebarGroupUncreated` for new stuff to do e.g. `syncSidebarDataWithNodes` -struct SidebarGroupUncreated: GraphEventWithResponse { - func handle(state: GraphState) -> GraphResponse { - state.sidebarGroupUncreatedViaEditMode() - return .init(willPersist: true) +extension ProjectSidebarObservable { + @MainActor + func sidebarGroupUncreated() { + let primarilySelectedGroups = self.selectionState.primary + let encodedData = self.createdOrderedEncodedData() + + guard let group = primarilySelectedGroups.first, + let item = self.items.get(group) else { + // Expected group here + fatalErrorIfDebug() + return + } + + let children = item.children ?? [] + + // Update sidebar self + let newEncodedData = encodedData.ungroup(selectedGroupId: group) + + // reset selection-state + self.selectionState.resetEditModeSelections() + + self.sidebarGroupUncreatedViaEditMode(groupId: group, + children: children.map(\.id)) + + self.persistSidebarChanges(encodedData: newEncodedData) } } -extension GraphState { +extension LayersSidebarViewModel { @MainActor - func sidebarGroupUncreatedViaEditMode() { + func sidebarGroupUncreatedViaEditMode(groupId: NodeId, children: [NodeId]) { log("_SidebarGroupUncreated called") - let primarilySelectedGroups = self.sidebarSelectionState.primary - - guard let group = primarilySelectedGroups.first else { - // Expected group here + guard let graph = self.graphDelegate else { fatalErrorIfDebug() return } - // let children = self.orderedSidebarLayers.get(group.id)?.children ?? [] - let children = self.orderedSidebarLayers.getSidebarLayerData(group.id)?.children ?? [] - - let newParentId = self.getNodeViewModel(group.asNodeId)?.layerNode?.layerGroupId - - // Update sidebar self - self.orderedSidebarLayers = self.orderedSidebarLayers.ungroup(selectedGroupId: group.asNodeId) + let newParentId = graph.getNodeViewModel(groupId)?.layerNode?.layerGroupId // find each child of the group, set its layer group id to the parent of the selected group children.forEach { child in - if let layerNode = self.getNodeViewModel(child.id) { + if let layerNode = graph.getNodeViewModel(child) { layerNode.layerNode?.layerGroupId = newParentId } } // finally, delete layer group node itself (but not its children) - self.deleteNode(id: group.id, willDeleteLayerGroupChildren: false) + graph.deleteNode(id: groupId, willDeleteLayerGroupChildren: false) // update legacy sidebar data - self.updateGraphData() - - // reset selection-state - self.sidebarSelectionState = .init() + graph.updateGraphData() } } diff --git a/Stitch/Graph/Sidebar/Util/ListModification/SidebarItemSelectionActions.swift b/Stitch/Graph/Sidebar/Util/ListModification/SidebarItemSelectionActions.swift index d6e6fb364..8a469fc26 100644 --- a/Stitch/Graph/Sidebar/Util/ListModification/SidebarItemSelectionActions.swift +++ b/Stitch/Graph/Sidebar/Util/ListModification/SidebarItemSelectionActions.swift @@ -18,44 +18,38 @@ struct SidebarItemTapped: GraphEvent { let commandHeld: Bool func handle(state: GraphState) { - state.sidebarItemTapped(id: id, - shiftHeld: shiftHeld, - commandHeld: commandHeld) + state.layersSidebarViewModel + .sidebarItemTapped(id: id.asItemId, + shiftHeld: shiftHeld, + commandHeld: commandHeld) } } -extension GraphState { - +extension ProjectSidebarObservable { @MainActor - func sidebarItemTapped(id: LayerNodeId, + func sidebarItemTapped(id: Self.ItemID, shiftHeld: Bool, commandHeld: Bool) { log("sidebarItemTapped: id: \(id)") - - #if DEV_DEBUG - let nodeTitle = self.getNode(id.asNodeId)!.getDisplayTitle() - log("sidebarItemTapped: layer: \(nodeTitle)") - #endif - log("sidebarItemTapped: shiftHeld: \(shiftHeld)") - - let originalSelections = self.sidebarSelectionState.inspectorFocusedLayers.focused + + let originalSelections = self.selectionState.inspectorFocusedLayers.focused log("sidebarItemTapped: originalSelections: \(originalSelections)") if shiftHeld, originalSelections.isEmpty { // Special case: if no current selections, shift-click just selects from the top to the clicked item; and the shift-clicked item counts as the 'last selected item' - let flatList = self.orderedSidebarLayers.getFlattenedList() - if let indexOfTappedItem = flatList.firstIndex(where: { $0.id == id.asNodeId }) { + let flatList = self.items.flattenedItems + if let indexOfTappedItem = flatList.firstIndex(where: { $0.id == id }) { let selectionsFromTop = flatList[0...indexOfTappedItem].map(\.id) - self.sidebarSelectionState.inspectorFocusedLayers.focused = .init(selectionsFromTop.map(\.asLayerNodeId)) - self.sidebarSelectionState.inspectorFocusedLayers.activelySelected = .init(selectionsFromTop.map(\.asLayerNodeId)) + self.selectionState.inspectorFocusedLayers.focused = selectionsFromTop.toSet + self.selectionState.inspectorFocusedLayers.activelySelected = selectionsFromTop.toSet - self.sidebarSelectionState.inspectorFocusedLayers.lastFocusedLayer = id + self.selectionState.inspectorFocusedLayers.lastFocusedLayer = id - self.editModeSelectTappedItems(tappedItems: self.sidebarSelectionState.inspectorFocusedLayers.focused) + self.editModeSelectTappedItems(tappedItems: self.selectionState.inspectorFocusedLayers.focused) } else { log("sidebarItemTapped: could not retrieve index of tapped item when") fatalErrorIfDebug() @@ -64,143 +58,136 @@ extension GraphState { } else if shiftHeld, - // We must have at least one layer already selected / focused - !originalSelections.isEmpty, - let lastClickedItemId = self.sidebarSelectionState.inspectorFocusedLayers.lastFocusedLayer { + // We must have at least one layer already selected / focused + !originalSelections.isEmpty, + let lastClickedItemId = self.selectionState.inspectorFocusedLayers.lastFocusedLayer { log("sidebarItemTapped: shift select") - guard let clickedItem: SidebarLayerData = self.orderedSidebarLayers.getSidebarLayerData(id.id), - let lastClickedItem: SidebarLayerData = self.orderedSidebarLayers.getSidebarLayerData(lastClickedItemId.id) else { + guard let clickedItem = self.retrieveItem(id), + let lastClickedItem = self.retrieveItem(lastClickedItemId) else { log("sidebarItemTapped: could not get clicked data") fatalErrorIfDebug() return } - log("sidebarItemTapped: lastClickedItemId: \(lastClickedItemId)") + log("sidebarItemTapped: lastClickedItemId: \(lastClickedItemId)") - let flatList = self.orderedSidebarLayers.getFlattenedList() + let flatList = self.items.flattenedItems - let originalIsland = getIsland(in: flatList, - startItem: lastClickedItem, - selections: originalSelections) + let originalIsland = flatList.getIsland(startItem: lastClickedItem, + selections: originalSelections) // log("sidebarItemTapped: originalIsland around last clicked item \(originalIsland.map(\.id))") - - if let itemsBetween = itemsBetweenClosestSelectedStart( + + if let itemsBetween = self.itemsBetweenClosestSelectedStart( in: flatList, clickedItem: clickedItem, lastClickedItem: lastClickedItem, // Look at focused layers selections: originalSelections) { - - log("sidebarItemTapped: itemsBetween: \(itemsBetween.map(\.id))") - let itemsBetweenSet: LayerIdSet = itemsBetween.map(\.id.asLayerNodeId).toSet + let idItemsBetween = itemsBetween.map(\.id).toSet // ORIGINAL - self.sidebarSelectionState.inspectorFocusedLayers.focused = - self.sidebarSelectionState.inspectorFocusedLayers.focused.union(itemsBetweenSet) + self.selectionState.inspectorFocusedLayers.focused = + self.selectionState.inspectorFocusedLayers.focused.union(idItemsBetween) + + self.selectionState.inspectorFocusedLayers.activelySelected = self.selectionState.inspectorFocusedLayers.focused.union(idItemsBetween) - self.sidebarSelectionState.inspectorFocusedLayers.activelySelected = self.sidebarSelectionState.inspectorFocusedLayers.focused.union(itemsBetweenSet) - - self.shrinkExpansions(flatList: flatList, - itemsBetween: itemsBetween, - originalIsland: originalIsland, - lastClickedItem: lastClickedItem, - justClickedItem: clickedItem) - // Shift click does NOT change the `lastFocusedLayer` // self.sidebarSelectionState.inspectorFocusedLayers.lastFocusedLayer = id // If we ended up selecting the exact same as the original, // then we actually DE-SELECTED the range. - let newSelections = self.sidebarSelectionState.inspectorFocusedLayers.focused + let newSelections = self.selectionState.inspectorFocusedLayers.focused log("sidebarItemTapped: selected range: newSelections: \(newSelections)") if newSelections == originalSelections { log("sidebarItemTapped: selected range; will wipe inspectorFocusedLayers") - - itemsBetweenSet.forEach { itemBetween in + + itemsBetween.forEach { itemBetween in log("sidebarItemTapped: will remove item Between \(itemBetween)") - self.sidebarSelectionState.inspectorFocusedLayers.focused.remove(itemBetween.id.asLayerNodeId) - self.sidebarSelectionState.inspectorFocusedLayers.activelySelected.remove(itemBetween.id.asLayerNodeId) + self.selectionState.inspectorFocusedLayers.focused.remove(itemBetween.id) + self.selectionState.inspectorFocusedLayers.activelySelected.remove(itemBetween.id) } } - self.editModeSelectTappedItems(tappedItems: self.sidebarSelectionState.inspectorFocusedLayers.focused) + self.editModeSelectTappedItems(tappedItems: self.selectionState.inspectorFocusedLayers.focused) - self.deselectAllCanvasItems() + self.graphDelegate?.deselectAllCanvasItems() } else { log("sidebarItemTapped: did not have itemsBetween") // TODO: this can happen when just-clicked == last-clicked, but some apps do not any deselection etc. // If we shift click the last-clicked item, then remove everything in the island? - if clickedItem == lastClickedItem { + if clickedItem.id == lastClickedItem.id { log("clicked the same item as the last clicked; will deselect original island and select only last selected") originalIsland.forEach { - self.sidebarSelectionState.inspectorFocusedLayers.focused.remove($0.id.asLayerNodeId) - self.sidebarSelectionState.inspectorFocusedLayers.activelySelected.remove($0.id.asLayerNodeId) + self.selectionState.inspectorFocusedLayers.focused.remove($0.id) + self.selectionState.inspectorFocusedLayers.activelySelected.remove($0.id) } - self.sidebarSelectionState.inspectorFocusedLayers.focused.insert(clickedItem.id.asLayerNodeId) - self.sidebarSelectionState.inspectorFocusedLayers.activelySelected.insert(clickedItem.id.asLayerNodeId) + self.selectionState.inspectorFocusedLayers.focused.insert(clickedItem.id) + self.selectionState.inspectorFocusedLayers.activelySelected.insert(clickedItem.id) - self.editModeSelectTappedItems(tappedItems: self.sidebarSelectionState.inspectorFocusedLayers.focused) + self.editModeSelectTappedItems(tappedItems: self.selectionState.inspectorFocusedLayers.focused) - self.deselectAllCanvasItems() + self.graphDelegate?.deselectAllCanvasItems() } } - } -// else { -// log("sidebarItemTapped: either shift not held or focused layers were empty") -// } - + } + // else { + // log("sidebarItemTapped: either shift not held or focused layers were empty") + // } + else if commandHeld { log("sidebarItemTapped: command select") - let alreadySelected = self.sidebarSelectionState.inspectorFocusedLayers.activelySelected.contains(id) + let alreadySelected = self.selectionState.inspectorFocusedLayers.activelySelected.contains(id) // Note: Cmd + Click will select a currently-unselected layer or deselect an already-selected layer if alreadySelected { - self.sidebarSelectionState.inspectorFocusedLayers.focused.remove(id) - self.sidebarSelectionState.inspectorFocusedLayers.activelySelected.remove(id) + self.selectionState.inspectorFocusedLayers.focused.remove(id) + self.selectionState.inspectorFocusedLayers.activelySelected.remove(id) self.sidebarItemDeselectedViaEditMode(id) // Don't set nil, but rather use `orderedSet.dropLast.last` ? - self.sidebarSelectionState.inspectorFocusedLayers.lastFocusedLayer = nil + self.selectionState.inspectorFocusedLayers.lastFocusedLayer = nil } else { - self.sidebarSelectionState.inspectorFocusedLayers.focused.insert(id) - self.sidebarSelectionState.inspectorFocusedLayers.activelySelected.insert(id) + self.selectionState.inspectorFocusedLayers.focused.insert(id) + self.selectionState.inspectorFocusedLayers.activelySelected.insert(id) self.sidebarItemSelectedViaEditMode(id, isSidebarItemTapped: true) - self.sidebarSelectionState.inspectorFocusedLayers.lastFocusedLayer = id - self.deselectAllCanvasItems() + self.selectionState.inspectorFocusedLayers.lastFocusedLayer = id + self.graphDelegate?.deselectAllCanvasItems() } } else { log("sidebarItemTapped: normal select") - self.sidebarSelectionState.resetEditModeSelections() + self.selectionState.resetEditModeSelections() // Note: Click will not deselect an already-selected layer - self.sidebarSelectionState.inspectorFocusedLayers.focused = .init([id]) - self.sidebarSelectionState.inspectorFocusedLayers.activelySelected = .init([id]) + self.selectionState.inspectorFocusedLayers.focused = .init([id]) + self.selectionState.inspectorFocusedLayers.activelySelected = .init([id]) self.sidebarItemSelectedViaEditMode(id, isSidebarItemTapped: true) - self.sidebarSelectionState.inspectorFocusedLayers.lastFocusedLayer = id - self.deselectAllCanvasItems() + self.selectionState.inspectorFocusedLayers.lastFocusedLayer = id + self.graphDelegate?.deselectAllCanvasItems() } - self.updateInspectorFocusedLayers() - + self.graphDelegate?.updateInspectorFocusedLayers() + // Reset selected row in property sidebar when focused-layers changes - self.graphUI.propertySidebar.selectedProperty = nil + self.graphDelegate?.documentDelegate?.graphUI.propertySidebar.selectedProperty = nil } - +} + +extension GraphState { @MainActor func updateInspectorFocusedLayers() { #if !targetEnvironment(macCatalyst) // If left sidebar is in edit-mode, "primary selections" become inspector-focused - if self.sidebarSelectionState.isEditMode { + if self.layersSidebarViewModel.isEditing { self.sidebarSelectionState.inspectorFocusedLayers.focused = self.sidebarSelectionState.primary self.sidebarSelectionState.inspectorFocusedLayers.activelySelected = self.sidebarSelectionState.primary } @@ -223,7 +210,7 @@ extension GraphState { let selectedSidebarLayers = self.sidebarSelectionState.inspectorFocusedLayers let selectedNodes: [NodeViewModel] = selectedSidebarLayers.focused.compactMap { - self.getNode($0.asNodeId) + self.getNode($0) } guard selectedNodes.count == selectedSidebarLayers.focused.count else { @@ -233,7 +220,7 @@ extension GraphState { } guard let firstSelectedLayer = selectedSidebarLayers.focused.first, - let firstSelectedNode: NodeViewModel = self.getNode(firstSelectedLayer.asNodeId), + let firstSelectedNode: NodeViewModel = self.getNode(firstSelectedLayer), let firstSelectedLayerNode: LayerNodeViewModel = firstSelectedNode.layerNode else { log("multipleSidebarLayersSelected: did not have any selected sidebar layers?") return nil @@ -273,107 +260,86 @@ struct SidebarItemSelected: GraphEvent { let id: LayerNodeId func handle(state: GraphState) { - state.sidebarItemSelectedViaEditMode(id, isSidebarItemTapped: false) + state.layersSidebarViewModel + .sidebarItemSelectedViaEditMode(id.asItemId, + isSidebarItemTapped: false) } } -extension GraphState { +extension ProjectSidebarObservable { @MainActor - func sidebarItemSelectedViaEditMode(_ id: LayerNodeId, + func sidebarItemSelectedViaEditMode(_ id: Self.ItemID, isSidebarItemTapped: Bool) { - let sidebarGroups = self.getSidebarGroupsDict() - // if we actively-selected (non-edit-mode-selected) an item that is already secondarily-selected, we don't need to change the if isSidebarItemTapped, - self.sidebarSelectionState.secondary.contains(id) { + self.selectionState.secondary.contains(id) { log("sidebarItemSelectedViaEditMode: \(id) was already secondarily selected") return } - // we selected a group -- so 100% select the group // and 80% all the children further down in the street - if self.getNodeViewModel(id.id)?.kind.getLayer == .group { - - self.sidebarSelectionState = addExclusivelyToPrimary( - id, self.sidebarSelectionState) + guard let item = self.retrieveItem(id) else { + return + } + + if item.isGroup { + self.addExclusivelyToPrimary(id) - sidebarGroups[id]?.forEach({ (childId: LayerNodeId) in - self.sidebarSelectionState = secondarilySelectAllChildren( - id: childId, - groups: sidebarGroups, - acc: self.sidebarSelectionState) - }) + item.children?.forEach{ child in + child.secondarilySelectAllChildren() + } } // If we selected a child of a group, // then deselect that parent and all other children, // and primarily select the child. // ie deselect everything(?), and only select the child. - else if let parent = findGroupLayerParentForLayerNode(id, sidebarGroups) { + else if let parent = item.parentDelegate { // if the parent is currently selected, // then deselect the parent and all other children - if self.sidebarSelectionState.isSelected(parent) { - self.sidebarSelectionState.resetEditModeSelections() - self.sidebarSelectionState = addExclusivelyToPrimary(id, self.sidebarSelectionState) + if self.selectionState.isSelected(parent.id) { + self.selectionState.resetEditModeSelections() + self.addExclusivelyToPrimary(id) } // ... otherwise, just primarily select the child else { - self.sidebarSelectionState = addExclusivelyToPrimary( - id, - self.sidebarSelectionState) + self.addExclusivelyToPrimary(id) } } // else: simple case?: else { - self.sidebarSelectionState = addExclusivelyToPrimary(id, self.sidebarSelectionState) + self.addExclusivelyToPrimary(id) } - self.updateInspectorFocusedLayers() + self.graphDelegate?.updateInspectorFocusedLayers() } } struct SidebarItemDeselected: GraphEvent { - let id: LayerNodeId + let id: SidebarListItemId func handle(state: GraphState) { - state.sidebarItemDeselectedViaEditMode(id) + state.layersSidebarViewModel.sidebarItemDeselectedViaEditMode(id) } } -extension GraphState { +extension ProjectSidebarObservable { + // If we deselected a group, + // then we should also deselect all its children. @MainActor - func sidebarItemDeselectedViaEditMode(_ id: LayerNodeId) { - // If we deselected a group, - // then we should also deselect all its children. - let groups = self.getSidebarGroupsDict() - - var idsToDeselect = LayerIdSet([id]) - - groups[id]?.forEach({ (childId: LayerNodeId) in - // ids all ids to remove - let ids = getDescendantsIds( - id: childId, - groups: groups, - acc: idsToDeselect) - idsToDeselect = idsToDeselect.union(ids) - - }) + func sidebarItemDeselectedViaEditMode(_ id: Self.ItemID) { + guard let item = self.items.get(id) else { + fatalErrorIfDebug() + return + } - // log("SidebarItemDeselected: idsToDeselect: \(idsToDeselect)") + item.removeFromSelections() - // now that we've gathered all the ids (ie directly de-selected item + its descendants), - // we can remove them - idsToDeselect.forEach { idToRemove in - self.sidebarSelectionState = removeFromSelections( - idToRemove, - self.sidebarSelectionState) - } - - self.updateInspectorFocusedLayers() + self.graphDelegate?.updateInspectorFocusedLayers() } } diff --git a/Stitch/Graph/Sidebar/Util/ListModification/SidebarListItemSelectionActions.swift b/Stitch/Graph/Sidebar/Util/ListModification/SidebarListItemSelectionActions.swift index 566b5d02e..46b361bbe 100644 --- a/Stitch/Graph/Sidebar/Util/ListModification/SidebarListItemSelectionActions.swift +++ b/Stitch/Graph/Sidebar/Util/ListModification/SidebarListItemSelectionActions.swift @@ -25,163 +25,136 @@ extension GraphState { // ASSUMES NON-EMPTY } -func secondarilySelectAllChildren(id: LayerNodeId, - groups: SidebarGroupsDict, - acc: SidebarSelectionState) -> SidebarSelectionState { - - var acc = acc - - // add to acc - acc = addExclusivelyToSecondary(id, acc) - - // recur on children - if let children = groups[id] { - children.forEach { (child: LayerNodeId) in - acc.combine(other: secondarilySelectAllChildren( - id: child, - groups: groups, - acc: acc)) +extension SidebarItemSwipable { + /// Recursively "secondarily" selects children. + func secondarilySelectAllChildren() { + guard let sidebar = self.sidebarDelegate else { + fatalErrorIfDebug() + return + } + + // add to selection state + sidebar.addExclusivelyToSecondary(self.id) + + // recur on children + self.children?.forEach { child in + child.secondarilySelectAllChildren() } } - - return acc -} - -// children to deselect -func getDescendantsIds(id: LayerNodeId, - groups: SidebarGroupsDict, - acc: LayerIdSet) -> LayerIdSet { - - var acc = acc - acc.insert(id) - - // recur on children - if let children = groups[id] { - children.forEach { (child: LayerNodeId) in - acc = acc.union(getDescendantsIds(id: child, - groups: groups, - acc: acc)) + + /// Recursively removes self + children from selection state. + func removeFromSelections() { + guard let sidebar = self.sidebarDelegate else { + fatalErrorIfDebug() + return + } + + sidebar.selectionState.primary.remove(self.id) + sidebar.selectionState.secondary.remove(self.id) + + self.children?.forEach { child in + child.removeFromSelections() } } - - return acc -} - -func removeFromSelections(_ id: LayerNodeId, - _ selection: SidebarSelectionState) -> SidebarSelectionState { - - var selection = selection - - selection.primary.remove(id) - selection.secondary.remove(id) - - return selection } -func addExclusivelyToPrimary(_ id: LayerNodeId, - _ selection: SidebarSelectionState) -> SidebarSelectionState { - - var selection = selection - - // add to primary - selection.primary.insert(id) - - // ... and remove from secondary (migt not be present?): - selection.secondary.remove(id) - - return selection -} - -func addExclusivelyToSecondary(_ id: LayerNodeId, - _ selection: SidebarSelectionState) -> SidebarSelectionState { - - var selection = selection - - selection.secondary.insert(id) - selection.primary.remove(id) - - return selection -} - - -func allShareSameParent(_ selections: NonEmptySidebarSelections, - groups: SidebarGroupsDict) -> Bool { - - if let parent = findGroupLayerParentForLayerNode(selections.first, groups) { - return selections.allSatisfy { (id: LayerNodeId) in - // does `id` have a parent, and is that parent the same as the random parent? - findGroupLayerParentForLayerNode(id, groups).map { $0 == parent } ?? false +extension ProjectSidebarObservable { + + // children to deselect + @MainActor + func getDescendantsIds(id: Self.ItemID) -> Set { + guard let children = self.createdOrderedEncodedData().get(id)?.children else { return .init() } + return children.flatMap { $0.allElementIds } + .toSet + } + + func addExclusivelyToPrimary(_ id: Self.ItemID) { + // add to primary + self.selectionState.primary.insert(id) + + // ... and remove from secondary (migt not be present?): + self.selectionState.secondary.remove(id) + } + + func addExclusivelyToSecondary(_ id: Self.ItemID) { + let selection = self.selectionState + selection.secondary.insert(id) + selection.primary.remove(id) + } + + + func allShareSameParent(_ selections: Self.SidebarSelectionState.SidebarSelections) -> Bool { + + if let firstSelection = selections.first, + let firstSelectionItem = self.items.get(firstSelection), + let parent = firstSelectionItem.parentDelegate?.id { + return selections.allSatisfy { id in + // does `id` have a parent, and is that parent the same as the random parent? + let item = self.items.get(id) + return item?.parentDelegate?.id == parent + } + } else { + return false // ie no parent } - } else { - return false // ie no parent } -} - -// selections can only be grouped if they ALL belong to EXACT SAME PARENT (or top level) -// ASSUMES NON-EMPTY -func canBeGrouped(_ selections: NonEmptySidebarSelections, - groups: SidebarGroupsDict) -> Bool { - - // items are on same level if they are all top level - let allTopLevel = selections.allSatisfy { - !findGroupLayerParentForLayerNode($0, groups).isDefined + + // selections can only be grouped if they ALL belong to EXACT SAME PARENT (or top level) + // ASSUMES NON-EMPTY + @MainActor + func canBeGrouped() -> Bool { + let selections = self.selectionState.primary + + // items are on same level if they are all top level + let allTopLevel = selections.allSatisfy { selectionId in + self.items.get(selectionId)?.parentDelegate == nil + } + + // ... or if they all have same parent + let allSameParent = self.allShareSameParent(selections) + + return allTopLevel || allSameParent } - - // ... or if they all have same parent - let allSameParent = allShareSameParent(selections, groups: groups) - - return allTopLevel || allSameParent -} - -// Can ungroup selections just if: -// 1. at least one group is 100% selected, and -// 2. no non-group items are 100% selected -func canUngroup(_ primarySelections: SidebarSelections, - nodes: LayerNodesForSidebarDict) -> Bool { - - !groupPrimarySelections(primarySelections, - nodes: nodes).isEmpty - - && nonGroupPrimarySelections(primarySelections, - nodes: nodes).isEmpty -} - -// 100% selected items that ARE groups -func groupPrimarySelections(_ primarySelections: SidebarSelections, - nodes: LayerNodesForSidebarDict) -> LayerIdList { - - primarySelections.filter { (selected: LayerNodeId) in - if let node = nodes[selected] { - return node.layer == .group + + // Can ungroup selections just if: + // 1. at least one group is 100% selected, and + // 2. no non-group items are 100% selected + @MainActor func canUngroup() -> Bool { + !groupPrimarySelections().isEmpty && + nonGroupPrimarySelections().isEmpty + } + + // 100% selected items that ARE groups + @MainActor func groupPrimarySelections() -> [Self.ItemID] { + self.selectionState.primary.filter { selected in + if let item = self.items.get(selected) { + return item.isGroup + } + return false } - return false } -} - -// 100% selected items that are NOT groups -func nonGroupPrimarySelections(_ primarySelections: SidebarSelections, - nodes: LayerNodesForSidebarDict) -> LayerIdList { - - primarySelections.filter { (selected: LayerNodeId) in - if let node = nodes[selected] { - return node.layer != .group + + // 100% selected items that are NOT groups + @MainActor func nonGroupPrimarySelections() -> Set { + self.selectionState.primary.filter { selected in + if let item = self.items.get(selected) { + return !item.isGroup + } + return false } - return false + } + + func canDuplicate() -> Bool { + !self.selectionState.primary.isEmpty } } -func canDuplicate(_ primarySelections: SidebarSelections) -> Bool { - !primarySelections.isEmpty -} - -// When an individual sidebar item is deleted via the swipe menu -struct SidebarItemDeleted: GraphEvent { - let itemId: SidebarListItemId - - func handle(state: GraphState) { - state.deleteNode(id: itemId.asNodeId) +extension GraphState { + // When an individual sidebar item is deleted via the swipe menu + @MainActor + func sidebarItemDeleted(itemId: SidebarListItemId) { + self.deleteNode(id: itemId) - state.updateGraphData() - state.encodeProjectInBackground() + self.updateGraphData() + self.encodeProjectInBackground() } } diff --git a/Stitch/Graph/Sidebar/Util/ListModification/SidebarListItemSelectionHelpers.swift b/Stitch/Graph/Sidebar/Util/ListModification/SidebarListItemSelectionHelpers.swift index 205f426ee..5c3cf95bb 100644 --- a/Stitch/Graph/Sidebar/Util/ListModification/SidebarListItemSelectionHelpers.swift +++ b/Stitch/Graph/Sidebar/Util/ListModification/SidebarListItemSelectionHelpers.swift @@ -8,185 +8,42 @@ import StitchSchemaKit import Foundation import SwiftUI +import StitchViewKit -typealias ListItem = SidebarLayerData - -// Function to find the closest selected item (start point) relative to an end item, excluding the end itself -func findClosestSelectedStart(in flatList: [ListItem], - to clickedItem: ListItem, - selections: LayerIdSet) -> ListItem? { - - // Find the index of the end item - guard let clickedItemIndex = flatList.firstIndex(of: clickedItem) else { - log("findClosestSelectedStart: could not find clickedItemIndex") - return nil // Return nil if the end item is not found - } - - // Initialize a variable to store the closest selected item - var closestSelected: ListItem? = nil - var closestDistance = Int.max - - // Search for the closest selected item (before and after the end index) - for i in 0..) -> [Self.ItemViewModel]? { + + // log("itemsBetweenClosestSelectedStart: flatList map ids: \(flatList.map(\.id))") + + let start = lastClickedItem + + guard let startIndex = flatList.firstIndex(where: { $0.id == start.id }), + let clickedItemIndex = flatList.firstIndex(where: { $0.id == clickedItem.id }) else { + // log("itemsBetweenClosestSelectedStart: could not get index of start item and/or clicked item") + return nil } - } - - log("findClosestSelectedStart: returning closestSelected \(closestSelected)") - return closestSelected -} - -// TODO: finalize this logic; it's not as simple as "the range between last-clicked and just-clicked" nor is it "the range between just-clicked and least-distant-currently-selected" -//func itemsBetweenClosestSelectedStart(in nestedList: [ListItem], -func itemsBetweenClosestSelectedStart(in flatList: [ListItem], - clickedItem: ListItem, - lastClickedItem: ListItem, - selections: LayerIdSet) -> [ListItem]? { - - // log("itemsBetweenClosestSelectedStart: flatList map ids: \(flatList.map(\.id))") - - let start = lastClickedItem - - guard let startIndex = flatList.firstIndex(of: start), - let clickedItemIndex = flatList.firstIndex(of: clickedItem) else { - // log("itemsBetweenClosestSelectedStart: could not get index of start item and/or clicked item") - return nil - } - - // Ensure that start and end are not the same - guard start != clickedItem else { - // log("itemsBetweenClosestSelectedStart: start same as clicked item") - return nil // If start and end are the same, return nil or handle as needed - } - - let startEarlierThanClickedItem = startIndex < clickedItemIndex - - // Determine the range and ensure it includes items regardless of their order - let range = startEarlierThanClickedItem ? startIndex...clickedItemIndex : clickedItemIndex...startIndex -// let range = startIndex < clickedItemIndex ? startIndex...clickedItemIndex : clickedItemIndex...startIndex - - // Return the items between start and end (inclusive) - return Array(flatList[range]) -} - -enum SidebarSelectionExpansionDirection: Equatable { - case upward, downward, none -} - -extension SidebarSelectionExpansionDirection { - - static func getExpansionDirection(islandTopIndex: Int, - islandBottomIndex: Int, - lastClickedIndex: Int) -> Self { - // If island top is above last clicked, - // then we expanded upward - if islandTopIndex < lastClickedIndex { - return .upward + // Ensure that start and end are not the same + guard start.id != clickedItem.id else { + // log("itemsBetweenClosestSelectedStart: start same as clicked item") + return nil // If start and end are the same, return nil or handle as needed } - // If island bottom is below last clicked, - // then we expanded downward - if islandBottomIndex > lastClickedIndex { - return .downward - } + let startEarlierThanClickedItem = startIndex < clickedItemIndex - return .none + // Determine the range and ensure it includes items regardless of their order + let range = startEarlierThanClickedItem ? startIndex...clickedItemIndex : clickedItemIndex...startIndex + // let range = startIndex < clickedItemIndex ? startIndex...clickedItemIndex : clickedItemIndex...startIndex + // Return the items between start and end (inclusive) + return Array(flatList[range]) } -} -extension GraphState { - - // TODO: combine this with our logic for adding to the current selections - func shrinkExpansions(flatList: [ListItem], // ALL items with nesting flattened; used for finding indices - itemsBetween: [ListItem], // the 'range' we clicked; items between last-clicked and just-clicked - originalIsland: [ListItem], // the original contiguous selection range - lastClickedItem: ListItem, // the last-non-shift-clicked item - // the just shift-clicked item - justClickedItem: ListItem) { - - let newIsland: [ListItem] = itemsBetween - - guard - let lastClickedIndex = flatList.firstIndex(of: lastClickedItem), - let justClickedIndex = flatList.firstIndex(of: justClickedItem), - let originalIslandTop = originalIsland.first, - let originalIslandBottom = originalIsland.last, - let originalIslandTopIndex = flatList.firstIndex(of: originalIslandTop), - let originalIslandBottomIndex = flatList.firstIndex(of: originalIslandBottom) else { - log("Could not retrieve requires indices") - return - } - - let originalExpansionDirection = SidebarSelectionExpansionDirection.getExpansionDirection( - islandTopIndex: originalIslandTopIndex, - islandBottomIndex: originalIslandBottomIndex, - lastClickedIndex: lastClickedIndex) - - var shrunk = false - - // it's not even as simple as 'expansion directions' - - - // Assuming the case where the island "expands downward", i.e. original island's bottom is below the last clicked, - // then - - // Given that - // it's more about "is the new clicked below or above - - if originalExpansionDirection == .downward { - - // If the original island was expanded downward, - // but the new click range does not extend as far down, - // then we shrunk: - if justClickedIndex < originalIslandBottomIndex { - log("expandOrShrinkExpansions: had expanded downward but new range does not goes as far down") - shrunk = true - } - } - - if originalExpansionDirection == .upward { - // If the original island was expanded upward - // but the new click range does not extend as far up, - // then we shrunk: - if justClickedIndex > originalIslandTopIndex { - log("expandOrShrinkExpansions: had expanded upward but new range does not goes as far up") - shrunk = true - } - } - - log("expandOrShrinkExpansions: shrunk \(shrunk)") - - if shrunk { - // If we shrunk, remove the items that are in the original island but not the new island - flatList.forEach { item in - let itemIsInNewIsland = newIsland.contains(item) - let itemIsInOldIsland = originalIsland.contains(item) - - if itemIsInOldIsland - && !itemIsInNewIsland - && item != lastClickedItem { - - log("expandOrShrinkExpansions: will remove item \(item)") - self.sidebarSelectionState.inspectorFocusedLayers.focused.remove(item.id.asLayerNodeId) - self.sidebarSelectionState.inspectorFocusedLayers.activelySelected.remove(item.id.asLayerNodeId) - } - } - } - } - /* Given an unordered set of tapped items, Start at top level of the ordered sidebar layers. @@ -196,21 +53,20 @@ extension GraphState { Iterating through the ordered sidebar layers provides the order and guarantees you don’t hit a child before its parent. */ @MainActor - func editModeSelectTappedItems(tappedItems: LayerIdSet) { + func editModeSelectTappedItems(tappedItems: Set) { // Wipe existing edit mode selections - self.sidebarSelectionState.resetEditModeSelections() + self.selectionState.resetEditModeSelections() - self.orderedSidebarLayers.getFlattenedList().forEach { (sidebarLayer: SidebarLayerData) in - - let layerId = sidebarLayer.id.asLayerNodeId - let wasTapped = tappedItems.contains(layerId) + self.items.recursiveForEach { sidebarLayer in + let itemId = sidebarLayer.id + let wasTapped = tappedItems.contains(itemId) // Only interested in items that were tapped if wasTapped { log("editModeSelectTappedItems: sidebarLayer.id \(sidebarLayer.id) was tapped") - self.sidebarItemSelectedViaEditMode(layerId, + self.sidebarItemSelectedViaEditMode(itemId, isSidebarItemTapped: true) } // if wasTapped else { @@ -218,76 +74,63 @@ extension GraphState { } } // forEach } -} - - -extension SidebarLayerList { - func getFlattenedList() -> [ListItem] { - flattenListItems(self, acc: .init()) - } -} - -func flattenListItems(_ items: [ListItem], - acc: [ListItem]) -> [ListItem] { - var acc = acc - items.forEach { item in - acc.append(item) - let accFromChildren = flattenListItems(item.children ?? [], acc: .init()) - acc += accFromChildren + + func retrieveItem(_ id: Self.ItemID) -> Self.ItemViewModel? { + self.items.get(id) } - return acc } - -// Function to find all items between the smallest and largest consecutive selected items (inclusive) -// `findItemsBetweenSmallestAndLargestSelected` -func getIsland(in list: [ListItem], - startItem: ListItem, - selections: LayerIdSet) -> [ListItem] { - - // Ensure the starting index is within bounds - guard let startIndex = list.firstIndex(where: { $0.id == startItem.id }), - startIndex >= 0 && startIndex < list.count else { - return [] - } - - // Check if the starting item is selected - - guard let startItem = list[safe: startIndex], - selections.contains(startItem.id.asLayerNodeId) else { - log("findItemsBetweenSmallestAndLargestSelected: starting index's item was not atually selected") - return [] - } - - // Initialize variables to store the smallest and largest selected items - var smallestIndex = startIndex - var largestIndex = startIndex - - // Move backward to find the smallest consecutive selected item - for i in stride(from: startIndex - 1, through: 0, by: -1) { +extension Array where Element: Identifiable { + // Function to find all items between the smallest and largest consecutive selected items (inclusive) + // `findItemsBetweenSmallestAndLargestSelected` + func getIsland(startItem: Element, + selections: Set) -> [Element] { + let list = self + + // Ensure the starting index is within bounds + guard let startIndex = list.firstIndex(where: { $0.id == startItem.id }), + startIndex >= 0 && startIndex < list.count else { + return [] + } + + // Check if the starting item is selected - if let _i = list[safe: i], - selections.contains(_i.id.asLayerNodeId) { - smallestIndex = i - } else { - break + guard let startItem = list[safe: startIndex], + selections.contains(startItem.id) else { + log("findItemsBetweenSmallestAndLargestSelected: starting index's item was not atually selected") + return [] } - } - - // Move forward to find the largest consecutive selected item - for i in (startIndex + 1).. GraphResponse { - state.sidebarSelectedItemsDeletingViaEditMode() + state.layersSidebarViewModel.deleteSelectedItems() return .shouldPersist } } -extension GraphState { - func sidebarSelectedItemsDeletingViaEditMode() { - let deletedIds = self.sidebarSelectionState.all.map(\.id) - +extension ProjectSidebarObservable { + func deleteSelectedItems() { + self.deleteItems(from: self.selectionState.all) + } + + func deleteItems(from deletedIds: Set) { deletedIds.forEach { - self.visibleNodesViewModel.nodes.removeValue(forKey: $0) + self.items.remove($0) } - - self.updateSidebarListStateAfterStateChange() - // TODO: why is this necessary? - _updateStateAfterListChange( - updatedList: self.sidebarListState, - expanded: self.getSidebarExpandedItems(), - graphState: self) + self.items.updateSidebarIndices() + + self.didItemsDelete(ids: deletedIds) + } +} + +extension LayersSidebarViewModel { + @MainActor + func didItemsDelete(ids: Set) { + self.graphDelegate?.didItemsDelete(ids: ids) + } +} + +extension GraphState { + @MainActor + func didItemsDelete(ids: Set) { + ids.forEach { + self.deleteNode(id: $0) + } } } diff --git a/Stitch/Graph/Sidebar/Util/ListModification/SidebarSelectedItemsDuplicated.swift b/Stitch/Graph/Sidebar/Util/ListModification/SidebarSelectedItemsDuplicated.swift index d79fec123..4f5c828eb 100644 --- a/Stitch/Graph/Sidebar/Util/ListModification/SidebarSelectedItemsDuplicated.swift +++ b/Stitch/Graph/Sidebar/Util/ListModification/SidebarSelectedItemsDuplicated.swift @@ -11,14 +11,17 @@ import Foundation struct SidebarSelectedItemsDuplicated: GraphEventWithResponse { func handle(state: GraphState) -> GraphResponse { - state.sidebarSelectedItemsDuplicatedViaEditMode() + state.sidebarSelectedItemsDuplicated() return .persistenceResponse } } extension GraphState { @MainActor - func sidebarSelectedItemsDuplicatedViaEditMode() { - self.copyAndPasteSelectedNodes(selectedNodeIds: self.sidebarSelectionState.all.map(\.asNodeId).toSet) + func sidebarSelectedItemsDuplicated() { + let nodeIds = self.layersSidebarViewModel.selectionState.all + self.copyAndPasteSelectedNodes(selectedNodeIds: nodeIds) + + // Move nodes } } diff --git a/Stitch/Graph/Sidebar/Util/SidebarIcons.swift b/Stitch/Graph/Sidebar/Util/SidebarIcons.swift index 6eabb2bac..daed37fac 100644 --- a/Stitch/Graph/Sidebar/Util/SidebarIcons.swift +++ b/Stitch/Graph/Sidebar/Util/SidebarIcons.swift @@ -11,12 +11,40 @@ import StitchSchemaKit let MASKS_LAYER_ABOVE_ICON_NAME = "arrow.turn.left.up" //extension Layer { -extension GraphState { - @MainActor - func sidebarLeftSideIcon(layer: Layer, - layerId: NodeId, - activeIndex: ActiveIndex) -> String { - switch layer { +extension SidebarItemGestureViewModel { + @MainActor var isMasking: Bool { + + // TODO: why is this not animated? and why does it jitter? +// // index of this layer +// guard let index = graph.sidebarListState.masterList.items +// .firstIndex(where: { $0.id.asLayerNodeId == nodeId }) else { +// return withAnimation { false } +// } +// +// // hasSidebarLayerImmediatelyAbove +// guard graph.sidebarListState.masterList.items[safe: index - 1].isDefined else { +// return withAnimation { false } +// } +// + let atleastOneIndexMasks = self.graphDelegate? + .getLayerNode(id: self.id)? + .layerNode?.masksPort.allLoopedValues + .contains(where: { $0.getBool ?? false }) + ?? false + +// return withAnimation { + return atleastOneIndexMasks +// } + } + + @MainActor var sidebarLeftSideIcon: String { + guard let layerNode = self.graphDelegate?.getNodeViewModel(id)?.layerNode, + let activeIndex = self.graphDelegate?.activeIndex else { +// fatalErrorIfDebug() + return "oval" + } + + switch layerNode.layer { case .group: return "folder" case .image: @@ -57,12 +85,9 @@ extension GraphState { return "line.3.crossed.swirl.circle.fill" case .sfSymbol: let defaultSymbol = "star" - if let sfSymbolInputValues = self.getNode(layerId)?.layerNode?.sfSymbolPort.allLoopedValues { - let adjustedActiveIndex = activeIndex.adjustedIndex(sfSymbolInputValues.count) - return sfSymbolInputValues[safe: adjustedActiveIndex]?.getString?.string ?? defaultSymbol - } else { - return defaultSymbol - } + let sfSymbolInputValues = layerNode.sfSymbolPort.allLoopedValues + let adjustedActiveIndex = activeIndex.adjustedIndex(sfSymbolInputValues.count) + return sfSymbolInputValues[safe: adjustedActiveIndex]?.getString?.string ?? defaultSymbol case .videoStreaming: return "video.bubble.left" case .material: diff --git a/Stitch/Graph/Sidebar/Util/SidebarListItemGroupActions.swift b/Stitch/Graph/Sidebar/Util/SidebarListItemGroupActions.swift index a03a37200..4e7c67f65 100644 --- a/Stitch/Graph/Sidebar/Util/SidebarListItemGroupActions.swift +++ b/Stitch/Graph/Sidebar/Util/SidebarListItemGroupActions.swift @@ -8,199 +8,46 @@ import Foundation import StitchSchemaKit -extension GraphState { - - func getSidebarExpandedItems() -> LayerIdSet { - self.layerNodes.values.filter { - $0.layerNode?.isExpandedInSidebar ?? false - } - .map(\.id.asLayerNodeId) - .toSet - } - - func applySidebarExpandedItems(_ expanded: LayerIdSet) { - self.layerNodes.values.forEach { - if $0.isGroupLayer { - $0.layerNode?.isExpandedInSidebar = expanded.contains($0.layerNodeId) - } else { - $0.layerNode?.isExpandedInSidebar = nil - } - } - } - +extension ProjectSidebarObservable { // for non-edit-mode selections @MainActor - func deselectDescendantsOfClosedGroup(_ closedParentId: LayerNodeId) { + func deselectDescendantsOfClosedGroup(_ closedParent: Self.ItemViewModel) { // Remove any non-edit-mode selected children; we don't want the 'selected sidebar layer' to be hidden - guard let closedParent = retrieveItem(closedParentId.asItemId, - self.sidebarListState.masterList.items) else { - fatalErrorIfDebug("Could not retrieve item") - return - } - - let descendants = Stitch.getDescendants(closedParent, - self.sidebarListState.masterList.items) + let descendants = closedParent.children?.flattenedItems ?? [] for childen in descendants { - self.sidebarSelectionState.inspectorFocusedLayers.focused.remove(childen.id.asLayerNodeId) - self.sidebarSelectionState.inspectorFocusedLayers.activelySelected.remove(childen.id.asLayerNodeId) + self.selectionState.inspectorFocusedLayers.focused.remove(childen.id) + self.selectionState.inspectorFocusedLayers.activelySelected.remove(childen.id) } } -} - -struct SidebarListItemGroupClosed: GraphEventWithResponse { - - let closedParentId: LayerNodeId - func handle(state: GraphState) -> GraphResponse { + @MainActor + func sidebarListItemGroupClosed(closedParent: Self.ItemViewModel) { - var expanded = state.getSidebarExpandedItems() + closedParent.isExpandedInSidebar = false // Remove any non-edit-mode selected children; we don't want the 'selected sidebar layer' to be hidden - state.deselectDescendantsOfClosedGroup(closedParentId) - - state.sidebarListState.masterList = onSidebarListItemGroupClosed( - closedId: closedParentId.asItemId, - state.sidebarListState.masterList) + self.deselectDescendantsOfClosedGroup(closedParent) - // // also need to remove id from sidebar's expandedSet - // expanded.remove(closedParent) + self.items.updateSidebarIndices() - // NOTEL Excluded-groups contains ALL collapsed groups; `masterList.collapsedGroups` only contains top-level collapsed groups? - state.sidebarListState.masterList.excludedGroups.keys.forEach { - expanded.remove($0.asLayerNodeId) - } - - state.applySidebarExpandedItems(expanded) - - _updateStateAfterListChange( - updatedList: state.sidebarListState, - expanded: state.getSidebarExpandedItems(), - graphState: state) - - return .shouldPersist + self.persistSidebarChanges() } -} - -struct SidebarListItemGroupOpened: GraphEventWithResponse { - - let openedParent: LayerNodeId - - func handle(state: GraphState) -> GraphResponse { - - state.sidebarListState.masterList = onSidebarListItemGroupOpened( - openedId: openedParent.asItemId, - state.sidebarListState.masterList) -// state.sidebarExpandedItems.insert(openedParent) - state.getNodeViewModel(openedParent.asNodeId)?.layerNode?.isExpandedInSidebar = true + // When group opened: + // - move parent's children from ExcludedGroups to Items + // - wipe parent's entry in ExcludedGroups + // - move down (+y) any items below the now-open parent + @MainActor + func sidebarListItemGroupOpened(parentItem: Self.ItemViewModel) { - _updateStateAfterListChange( - updatedList: state.sidebarListState, - expanded: state.getSidebarExpandedItems(), - graphState: state) + log("onSidebarListItemGroupOpened called") - return .shouldPersist - } -} - - -// When group opened: -// - move parent's children from ExcludedGroups to Items -// - wipe parent's entry in ExcludedGroups -// - move down (+y) any items below the now-open parent -func onSidebarListItemGroupOpened(openedId: SidebarListItemId, - _ masterList: MasterList) -> MasterList { - - log("onSidebarListItemGroupOpened called") - - var masterList = masterList - - // important: remove this item from collapsedGroups, - // so that we can unfurl its own children - masterList.collapsedGroups.remove(openedId) - - guard let parentItem = retrieveItem(openedId, masterList.items) else { - fatalErrorIfDebug("Could not retrieve item") - return masterList - } - let parentIndex = parentItem.itemIndex(masterList.items) - - let originalCount = masterList.items.count - - let (updatedMaster, lastIndex) = unhideChildren( - openedParent: openedId, - parentIndex: parentIndex, - parentY: parentItem.location.y, - masterList) - - masterList = updatedMaster - - // count after adding hidden descendants back to `items` - let updatedCount = masterList.items.count - - // how many items total we added by unhiding the parent's children - let addedCount = updatedCount - originalCount - - let moveDownBy = addedCount * CUSTOM_LIST_ITEM_VIEW_HEIGHT - - // and move any items below this parent DOWN - // ... but skip any children, since their positions' have already been updated - masterList.items = adjustNonDescendantsBelow( - lastIndex, - adjustment: CGFloat(moveDownBy), - masterList.items) - - return masterList -} - -// When group closed: -// - remove parent's children from `items` -// - add removed children to ExcludedGroups dict -// - move up the position of items below the now-closed parent -@MainActor -func onSidebarListItemGroupClosed(closedId: SidebarListItemId, - _ masterList: MasterList) -> MasterList { - - print("onSidebarListItemGroupClosed called") - - guard let closedParent = retrieveItem(closedId, masterList.items) else { - fatalErrorIfDebug("Could not retrieve item") - return masterList - } - - var masterList = masterList - - if !hasOpenChildren(closedParent, masterList.items) { - masterList.collapsedGroups.insert(closedId) - masterList.excludedGroups.updateValue([], forKey: closedId) - return masterList + // Trigger inherited class + parentItem.isExpandedInSidebar = true + self.items.updateSidebarIndices() + + self.persistSidebarChanges() } - - let descendantsCount = getDescendants( - closedParent, - masterList.items).count - - let moveUpBy = descendantsCount * CUSTOM_LIST_ITEM_VIEW_HEIGHT - - // hide the children: - // - populates ExcludedGroups - // - removes now-hidden descendants from `items` - masterList = hideChildren(closedParentId: closedId, - masterList) - - // and move any items below this parent upward - masterList.items = adjustItemsBelow( - // parent's own index should not have changed if we only - // removed or changed items AFTER its index. - closedParent.id, - closedParent.itemIndex(masterList.items), - adjustment: -CGFloat(moveUpBy), - masterList.items) - - // add parent to collapsed group - masterList.collapsedGroups.insert(closedId) - - return masterList } diff --git a/Stitch/Graph/Sidebar/Util/SidebarListItemToggleHelpers.swift b/Stitch/Graph/Sidebar/Util/SidebarListItemToggleHelpers.swift deleted file mode 100644 index 3ef6772ec..000000000 --- a/Stitch/Graph/Sidebar/Util/SidebarListItemToggleHelpers.swift +++ /dev/null @@ -1,353 +0,0 @@ -// -// _SidebarListItemToggleHelpers.swift -// Stitch -// -// Created by Christian J Clampitt on 3/8/24. -// - -// -// CustomListToggleHelpers.swift -// Stitch -// -// Created by Christian J Clampitt on 3/15/22. -// - -import Foundation -import SwiftUI - -// functions for opening and closing groups - -// ONLY USEFUL FOR NON-DRAGGING CASES -// ie when closing or opening a group -@MainActor -func getDescendants(_ parentItem: SidebarListItem, - _ items: SidebarListItems) -> SidebarListItems { - - var descendants = SidebarListItems() - - for item in getItemsBelow(parentItem, items) { - // log("itemBelow: \(item.id), \(item.location.x)") - // if you encounter an item at or west of the parentXLocation, - // then you've finished the parent's nested groups - if item.location.x <= parentItem.location.x { - // log("getDescendants: exiting early") - // log("getDescendants: early exit: descendants: \(descendants)") - return descendants - } else { - descendants.append(item) - } - } - // log("getDescendants: returning: descendants: \(descendants)") - return descendants -} - -// if "parent" does not have an iimte -// Better?: `!getDescendents.isEmpty` -func hasOpenChildren(_ item: SidebarListItem, _ items: SidebarListItems) -> Bool { - - let parentIndex = item.itemIndex(items) - let nextChildIndex = parentIndex + 1 - - if let child = items[safeIndex: nextChildIndex], - let childParent = child.parentId, - childParent == item.id { - return true - } - return false -} - -// only called if parent has children -@MainActor -func hideChildren(closedParentId: SidebarListItemId, - _ masterList: MasterList) -> MasterList { - - var masterList = masterList - - guard let closedParent = retrieveItem(closedParentId, masterList.items) else { - fatalErrorIfDebug("Could not retrieve item") - return masterList - } - - // if there are no descendants, then we're basically done - - // all the items below this parent, with indentation > parent's - let descendants = getDescendants(closedParent, masterList.items) - - // starting: immediate parent will have closed parent's id - var currentParent: SidebarListItemId = closedParentId - - // starting: immediate child of parent will have parent's indentation level + 1 - var currentDeepestIndentation = closedParent.indentationLevel.inc() - - for descendant in descendants { - // log("on descendant: \(descendant)") - - // if we ever have a descendant at, or west of, the closedParent, - // then we made a mistake! - if descendant.indentationLevel.value <= closedParent.indentationLevel.value { - fatalError() - } - - if descendant.indentationLevel == currentDeepestIndentation { - masterList = masterList.appendToExcludedGroup( - for: currentParent, - descendant) - } - // we either increased or decreased in indentation - else { - // if we changed indentation levels (whether east or west), - // we should have a new parent - currentParent = descendant.parentId! - - // ie we went deeper (farther east) - if descendant.indentationLevel.value > currentDeepestIndentation.value { - // log("went east") - currentDeepestIndentation = currentDeepestIndentation.inc() - } - // ie. we backed up (went one level west) - // ie. descendant.indentationLevel.value < currentDeepestIndentation.value - else { - // log("went west") - currentDeepestIndentation = currentDeepestIndentation.dec() - } - - // set the descendant AFTER we've updated the parent - masterList = masterList.appendToExcludedGroup( - for: currentParent, - descendant) - } - } - - // finally, remove descendants from items list - let descendentsIdSet: Set = Set(descendants.map(\.id)) - masterList.items.removeAll { descendentsIdSet.contains($0.id) } - - return masterList -} - -func appendToExcludedGroup(for key: SidebarListItemId, - _ newItems: SidebarListItems, - _ excludedGroups: ExcludedGroups) -> ExcludedGroups { - // log("appendToExcludedGroup called") - - var existing: SidebarListItems = excludedGroups[key] ?? [] - existing.append(contentsOf: newItems) - - var excludedGroups = excludedGroups - excludedGroups.updateValue(existing, forKey: key) - - return excludedGroups -} - -// retrieve children -// nil = parentId had no -// non-nil = returning children, plus removing the parentId entry from ExcludedGroups -func popExcludedChildren(parentId: SidebarListItemId, - _ masterList: MasterList) -> (SidebarListItems, ExcludedGroups)? { - - if let excludedChildren = masterList.excludedGroups[parentId] { - - // prevents us from opening any subgroups that weren't already opend - if masterList.collapsedGroups.contains(parentId) { - log("this subgroup was closed when it was put away, so will skip it") - return nil - } - - var groups = masterList.excludedGroups - groups.removeValue(forKey: parentId) - return (excludedChildren, groups) - } - return nil -} - -func setOpenedChildHeight(_ item: SidebarListItem, - _ height: CGFloat) -> SidebarListItem { - var item = item - // set height only; preserve indentation - item.location = CGPoint(x: item.location.x, y: height) - item.previousLocation = item.location - return item -} - -func unhideChildrenHelper(item: SidebarListItem, // item that could be a parent or not - currentHighestIndex: Int, // starts: opened parent's index - currentHighestHeight: CGFloat, // starts: opened parent's height - _ masterList: MasterList, - isRoot: Bool) -> (MasterList, Int, CGFloat) { - - var masterList = masterList - var currentHighestIndex = currentHighestIndex - var currentHighestHeight = currentHighestHeight - - // insert item - if !isRoot { - let (updatedMaster, - updatedHighestIndex, - updatedHighestHeight) = insertUnhiddenItem(item: item, - currentHighestIndex: currentHighestIndex, - currentHighestHeight: currentHighestHeight, - masterList) - - masterList = updatedMaster - currentHighestIndex = updatedHighestIndex - currentHighestHeight = updatedHighestHeight - } - // else { - // log("unhideChildrenHelper: had root item \(item.id), so will not add root item again") - // } - - // does this `item` have children of its own? - // if so, recur - if let (excludedChildren, updatedGroups) = popExcludedChildren( - parentId: item.id, masterList) { - - // log("unhideChildrenHelper: had children") - - masterList.excludedGroups = updatedGroups - - // excluded children must be handled in IN ORDER - for child in excludedChildren { - // log("unhideChildrenHelper: on child \(child.id) of item \(item.id)") - let (updatedMaster, - updatedHighestIndex, - updatedHighestHeight) = unhideChildrenHelper( - item: child, - currentHighestIndex: currentHighestIndex, - currentHighestHeight: currentHighestHeight, - masterList, - isRoot: false) - - masterList = updatedMaster - currentHighestIndex = updatedHighestIndex - currentHighestHeight = updatedHighestHeight - } - } - // else { - // log("unhideChildrenHelper: did not have children") - // } - - return (masterList, currentHighestIndex, currentHighestHeight) -} - -func insertUnhiddenItem(item: SidebarListItem, // item that could be a parent or not - currentHighestIndex: Int, // starts: opened parent's index - currentHighestHeight: CGFloat, // starts: opened parent's height - _ masterList: MasterList) -> (MasterList, Int, CGFloat) { - - var item = item - var currentHighestIndex = currentHighestIndex - var currentHighestHeight = currentHighestHeight - var masterList = masterList - - // + 1 so inserted AFTER previous currentHighestIndex - currentHighestIndex += 1 - currentHighestHeight += CGFloat(CUSTOM_LIST_ITEM_VIEW_HEIGHT) - - item = setOpenedChildHeight(item, currentHighestHeight) - masterList.items.insert(item, at: currentHighestIndex) - - return (masterList, currentHighestIndex, currentHighestHeight) -} - -func unhideChildren(openedParent: SidebarListItemId, - parentIndex: Int, - parentY: CGFloat, - _ masterList: MasterList) -> (MasterList, Int) { - - // this can actually happen - guard masterList.excludedGroups[openedParent].isDefined else { - #if DEV || DEV_DEBUG - log("Attempted to open a parent that did not have excluded children") - fatalError() - #endif - return (masterList, parentIndex) // - } - - // log("unhideChildren: parentIndex: \(parentIndex)") - - guard let parent = retrieveItem(openedParent, masterList.items) else { - fatalErrorIfDebug("Could not retrieve item") - return (masterList, parentIndex) - } - - // if you start with the parent, you double add it - let (updatedMaster, lastIndex, _) = unhideChildrenHelper( - item: parent, - currentHighestIndex: parent.itemIndex(masterList.items), - currentHighestHeight: parent.location.y, - masterList, - isRoot: true) - - return (updatedMaster, lastIndex) -} - -// all children, closed or open -func childrenForParent(parentId: SidebarListItemId, - _ items: SidebarListItems) -> SidebarListItems { - items.filter { $0.parentId == parentId } -} - -func adjustItemsBelow(_ parentId: SidebarListItemId, - _ parentIndex: Int, // parent that was opened or closed - adjustment: CGFloat, // down = +y; up = -y - _ items: SidebarListItems) -> SidebarListItems { - - return items.map { item in - // only adjust items below the parent - if item.itemIndex(items) > parentIndex, - // ... but don't adjust children of the parent, - // since their position was already set in `unhideGroups`; - // and when hiding a group, there are no children to adjust. - item.parentId != parentId { - var item = item - // adjust both location and previousLocation - item.location = CGPoint(x: item.location.x, - y: item.location.y + adjustment) - item.previousLocation = item.location - return item - } else { - // print("Will not adjust item \(item.id)") - return item - } - } -} - -func adjustNonDescendantsBelow(_ lastIndex: Int, // the last item - adjustment: CGFloat, // down = +y; up = -y - _ items: SidebarListItems) -> SidebarListItems { - - return items.map { item in - if item.itemIndex(items) > lastIndex { - var item = item - item.location = CGPoint(x: item.location.x, - y: item.location.y + adjustment) - item.previousLocation = item.location - return item - } else { - return item - } - } -} - -func retrieveItem(_ id: SidebarListItemId, - _ items: SidebarListItems) -> SidebarListItem? { - items.first { $0.id == id } -} - -func hasChildren(_ parentId: SidebarListItemId, _ masterList: MasterList) -> Bool { - - if let x = masterList.items.first(where: { $0.id == parentId }), - x.isGroup { - // log("hasChildren: true because isGroup") - return true - } else if masterList.excludedGroups[parentId].isDefined { - // log("hasChildren: true because has entry in excludedGroups") - return true - } else if !childrenForParent(parentId: parentId, masterList.items).isEmpty { - // log("hasChildren: true because has non-empty children in on-screen items") - return true - } else { - // log("hasChildren: false....") - return false - } -} diff --git a/Stitch/Graph/Sidebar/Util/UpdateStateAfterListChange.swift b/Stitch/Graph/Sidebar/Util/UpdateStateAfterListChange.swift deleted file mode 100644 index 6aaa78200..000000000 --- a/Stitch/Graph/Sidebar/Util/UpdateStateAfterListChange.swift +++ /dev/null @@ -1,168 +0,0 @@ -// -// UpdateStateAfterListChange.swift -// Stitch -// -// Created by Christian J Clampitt on 3/12/24. -// - -import Foundation -import SwiftUI -import StitchSchemaKit - -// MasterList -typealias MasterList = SidebarListItemsCoordinator - -// e.g. we dragged a sidebar item, and we need to now update the view models (ordered-sidebar-items and layer nodes dict) based on the new SidebarListState -// should make this function smaller; not take all of graphState but return specific values etc. -func _updateStateAfterListChange(updatedList: SidebarListState, - expanded: LayerIdSet, - graphState: GraphState) { - - let orderedSidebarLayers: SidebarLayerList = graphState.orderedSidebarLayers - let layerNodes: NodesViewModelDict = graphState.layerNodes - - let layerNodeSidebarState: LayerNodesForSidebarDict = .fromLayerNodesDict( - nodes: layerNodes, - orderedSidebarItems: orderedSidebarLayers) - - // put the update sidebar-list-state in graph state - graphState.sidebarListState = updatedList - - let groups = graphState.getSidebarGroupsDict() - - // we have an updated sidebarListState; - // we need to turn that into an update of the view models: (ordered-sidebar-items, layer nodes dict) - - let oldDeps = SidebarDeps( - layerNodes: layerNodeSidebarState, - groups: groups, - expandedItems: expanded) - - // ie list state -> redux state - let updatedDeps: SidebarDeps = sidebarListItemsToSidebarDeps( - oldDeps, - updatedList.masterList) - - let newGroups: SidebarGroupsDict = updatedDeps.groups - let newLayerNodesForSidebar: LayerNodesForSidebarDict = updatedDeps.layerNodes - - - // two steps: - // 1. update layer nodes' layerGroupId; some layer node may now be part of a different group, or not in a group at alll anymore - // 2. update ordered-sidebar-items - - // step 1: - graphState.layerNodes.values.forEach { node in -// log("_updateStateAfterListChange: updated layer group parent id for node.id: \(node.id)") -// - // TODO: Can skip this check and just assume layerNodes has a `LayerNodeViewModel` ? - if node.layerNode.isDefined { - - // find parent for this layer node, based on new sidebar-groups-dict - let groupLayerParent = findGroupLayerParentForLayerNode(node.id.asLayerNodeId, - newGroups) - - - node.layerNode?.layerGroupId = groupLayerParent?.id - } -// else { -// log("_updateStateAfterListChange: wasn't a layer node?") -// } - } - - // better?: use logic from `GraphSchema.getOrderedLayers` to create SidebarItems from layerNodesForSidebarDict and sidebarGroups, and then turn those SidebarItems into OSIs - let newSidebarItems = asSidebarItems(groups: newGroups, - layerNodes: newLayerNodesForSidebar) - - let newOrderedSidebarLayers: OrderedSidebarLayers = newSidebarItems.map { $0.toSidebarLayerData() - } - - graphState.orderedSidebarLayers = newOrderedSidebarLayers -} - -// creates new expandedItem -// creates new (LayerNodesForSidebarDict, SidebarGroups, ExpandedItems) -func sidebarListItemsToSidebarDeps(_ sidebar: SidebarDeps, - _ masterList: SidebarListItemsCoordinator) -> SidebarDeps { - - let expanded = expandedItemsFromCollapsedGroups( - sidebar.layerNodes, - masterList.collapsedGroups) - - // log("sidebarListItemsToSidebarDeps: expanded: \(expanded)") - - var updatedNodes = LayerNodesForSidebarDict() // LayerNodesDict() - var updatedGroups = SidebarGroupsDict() - - // `items` will be in order; and so will children in excluded groups - let allItems = masterList.items + flattenExcludedGroups(masterList.excludedGroups) - - // Rebuild the ordered-dict of layer-nodes in same order of sidebarListItems. - allItems.forEach { (item: SidebarListItem) in - - // log("sidebarListItemsToSidebarDeps: item.id: \(item.id)") - - guard let node: LayerNodeForSidebar = sidebar.layerNodes[item.id.asLayerNodeId] else { - // we had a rect item that had no corresponding layer node! - fatalError() - } - - // Always add the layer node to the rebuilt layer-nodes-dict - - // Additionally: if the item is a child of the group, - // add it to the sidebar groups dict - if let parentId = item.parentId { - // log("sidebarListItemsToSidebarDeps: item \(item.id) had parent id: \(parentId)") - // log("sidebarListItemsToSidebarDeps: updatedGroups was: \(updatedGroups)") - - updatedGroups = appendToSidebarGroup( - for: parentId.asLayerNodeId, - [item.id.asLayerNodeId], - updatedGroups) - - // log("sidebarListItemsToSidebarDeps: updatedGroups is now: \(updatedGroups)") - } - - // Always add [] when we encounter the group itself; - // ensures we create a `sidebarGroups` entry for every group, - // even if group is empty. - if item.isGroup { - // log("sidebarListItemsToSidebarDeps: item \(item.id) was a parent") - // log("sidebarListItemsToSidebarDeps: updatedGroups was: \(updatedGroups)") - updatedGroups = appendToSidebarGroup( - for: item.id.asLayerNodeId, - [], - updatedGroups) - // log("sidebarListItemsToSidebarDeps: updatedGroups is now: \(updatedGroups)") - } - - // presumably appends to end? - // ie `insertingAt: currentIndex + 1` - updatedNodes.updateValue(node, forKey: node.id) - } - - var sidebar = sidebar - sidebar.layerNodes = updatedNodes - sidebar.groups = updatedGroups - sidebar.expandedItems = expanded - - return sidebar -} - -func appendToSidebarGroup(for key: LayerNodeId, - _ newChildren: [LayerNodeId], - _ groups: SidebarGroupsDict) -> SidebarGroupsDict { - - var groups = groups - var existing: LayerIdList = groups[key] ?? [] - - // for child in newChildren { - // if existing.contains(child) { - // log("appendToSidebarGroup: child \(child) is already in existing: \(existing)") - // } - // } - - existing.append(contentsOf: newChildren) - groups.updateValue(existing, forKey: key) - return groups -} diff --git a/Stitch/Graph/Sidebar/View/SidebarEditButtonView.swift b/Stitch/Graph/Sidebar/View/SidebarEditButtonView.swift index 810bb02ee..727b696d8 100644 --- a/Stitch/Graph/Sidebar/View/SidebarEditButtonView.swift +++ b/Stitch/Graph/Sidebar/View/SidebarEditButtonView.swift @@ -18,24 +18,22 @@ extension EditMode { } } -struct SidebarEditButtonView: View { - static let EDIT_BUTTON_COLOR: Color = Color(.editButton) - - @Binding var isEditing: Bool +struct SidebarEditButtonView: View where SidebarViewModel: ProjectSidebarObservable { + @Bindable var sidebarViewModel: SidebarViewModel var body: some View { StitchButton { - isEditing.toggle() + sidebarViewModel.isEditing.toggle() } label: { - Text(isEditing ? "Done" : "Edit") + Text(sidebarViewModel.isEditing ? "Done" : "Edit") } .font(SwiftUI.Font.system(size: 18)) - .foregroundColor(Self.EDIT_BUTTON_COLOR) + .foregroundColor(Color(.editButton)) } } -struct SidebarEditButtonView_Previews: PreviewProvider { - static var previews: some View { - SidebarEditButtonView(isEditing: .constant(false)) - } -} +//struct SidebarEditButtonView_Previews: PreviewProvider { +// static var previews: some View { +// SidebarEditButtonView(isEditing: .constant(false)) +// } +//} diff --git a/Stitch/Graph/Sidebar/View/SidebarFooterView.swift b/Stitch/Graph/Sidebar/View/SidebarFooterView.swift index 25b1f7d52..4f65f829b 100644 --- a/Stitch/Graph/Sidebar/View/SidebarFooterView.swift +++ b/Stitch/Graph/Sidebar/View/SidebarFooterView.swift @@ -9,17 +9,22 @@ import SwiftUI import StitchSchemaKit import StitchViewKit -struct SidebarFooterView: View { +struct SidebarFooterView: View { - static let SIDEBAR_FOOTER_HEIGHT: CGFloat = 64 - static let SIDEBAR_FOOTER_COLOR: Color = Color(.sideBarFooter) + private let SIDEBAR_FOOTER_HEIGHT: CGFloat = 64 + private let SIDEBAR_FOOTER_COLOR: Color = Color(.sideBarFooter) - let groups: SidebarGroupsDict - let selections: SidebarSelectionState - let isBeingEdited: Bool + @Bindable var sidebarViewModel: SidebarViewModel let syncStatus: iCloudSyncStatus - let layerNodes: LayerNodesForSidebarDict + var isBeingEdited: Bool { + self.sidebarViewModel.isEditing + } + + var selections: SidebarViewModel.SidebarSelectionState { + self.sidebarViewModel.selectionState + } + var showEditModeFooter: Bool { #if targetEnvironment(macCatalyst) // on Catalyst, show edit mode footer if we're in edit-mode or have at least one edit-mode-selection @@ -42,12 +47,11 @@ struct SidebarFooterView: View { } .padding() .animation(.default, value: showEditModeFooter) - .animation(.default, value: selections) - .animation(.default, value: groups) - .animation(.default, value: layerNodes) + .animation(.default, value: selections.primary) + .animation(.default, value: selections.secondary) .frame(maxWidth: .infinity) - .height(Self.SIDEBAR_FOOTER_HEIGHT) - .background(Self.SIDEBAR_FOOTER_COLOR.ignoresSafeArea()) + .height(self.SIDEBAR_FOOTER_HEIGHT) + .background(self.SIDEBAR_FOOTER_COLOR.ignoresSafeArea()) } var normalFooter: some View { @@ -61,10 +65,8 @@ struct SidebarFooterView: View { var editModeFooter: some View { HStack(spacing: 10) { Spacer() - SidebarFooterButtonsView(groups: groups, - selections: selections, - isBeingEdited: isBeingEdited, - layerNodes: layerNodes) + SidebarFooterButtonsView(sidebarViewModel: sidebarViewModel, + isBeingEdited: isBeingEdited) } } // editModeFooter } @@ -79,37 +81,32 @@ struct DisabledButtonModifier: ViewModifier { } } -struct SidebarFooterButtonsView: View { - - let groups: SidebarGroupsDict - let selections: SidebarSelectionState +struct SidebarFooterButtonsView: View where SidebarViewModel: ProjectSidebarObservable { + @Bindable var sidebarViewModel: SidebarViewModel let isBeingEdited: Bool - let layerNodes: LayerNodesForSidebarDict + + var selections: SidebarViewModel.SidebarSelectionState { + self.sidebarViewModel.selectionState + } var body: some View { let allButtonsDisabled = selections.all.isEmpty - - let ungroupButtonEnabled = canUngroup(selections.primary, - nodes: layerNodes) - - let groupButtonEnabled = selections - .nonEmptyPrimary - .map { canBeGrouped($0, groups: groups) } ?? false - - let duplicateButtonEnabled = canDuplicate(selections.primary) + let ungroupButtonEnabled = sidebarViewModel.canUngroup() + let groupButtonEnabled = sidebarViewModel.canBeGrouped() + let duplicateButtonEnabled = sidebarViewModel.canDuplicate() // return HStack(spacing: 10) { return Group { // Spacer() StitchButton { - dispatch(SidebarGroupUncreated()) + self.sidebarViewModel.sidebarGroupUncreated() } label: { Text("Ungroup") .modifier(DisabledButtonModifier(buttonEnabled: ungroupButtonEnabled)) }.disabled(!ungroupButtonEnabled) StitchButton { - dispatch(SidebarGroupCreated()) + sidebarViewModel.sidebarGroupCreated() } label: { Text("Group") .modifier(DisabledButtonModifier(buttonEnabled: groupButtonEnabled)) diff --git a/Stitch/Graph/Sidebar/View/SidebarItem/SidebarListItemChevronView.swift b/Stitch/Graph/Sidebar/View/SidebarItem/SidebarListItemChevronView.swift index 9320ed5f0..f28d4cb84 100644 --- a/Stitch/Graph/Sidebar/View/SidebarItem/SidebarListItemChevronView.swift +++ b/Stitch/Graph/Sidebar/View/SidebarItem/SidebarListItemChevronView.swift @@ -12,15 +12,18 @@ import StitchSchemaKit // group closed; rotated 90 degrees to be 'group open' let CHEVRON_GROUP_TOGGLE_ICON = "chevron.right" -struct SidebarListItemChevronView: View { - - let isClosed: Bool - let parentId: LayerNodeId +struct SidebarListItemChevronView: View where SidebarViewModel: ProjectSidebarObservable { + let sidebarViewModel: SidebarViewModel + let item: SidebarViewModel.ItemViewModel // white when layer is non-edit-mode selected; else determined by primary vs secondary selection status - let fontColor: Color + var fontColor: Color { + item.fontColor + } - let isHidden: Bool + var isClosed: Bool { + item.isCollapsedGroup + } var rotationZ: CGFloat { isClosed ? 0 : 90 @@ -48,13 +51,16 @@ struct SidebarListItemChevronView: View { axis: (x: 0, y: 0, z: rotationZ)) .contentShape(Rectangle()) - .onTapGesture { - if isClosed { - dispatch(SidebarListItemGroupOpened(openedParent: parentId)) - } else { - dispatch(SidebarListItemGroupClosed(closedParentId: parentId)) - } - } + .simultaneousGesture( + TapGesture() + .onEnded { + if isClosed { + sidebarViewModel.sidebarListItemGroupOpened(parentItem: item) + } else { + sidebarViewModel.sidebarListItemGroupClosed(closedParent: item) + } + } + ) .animation(.linear, value: rotationZ) } } diff --git a/Stitch/Graph/Sidebar/View/SidebarItem/SidebarListItemSelectionCircleView.swift b/Stitch/Graph/Sidebar/View/SidebarItem/SidebarListItemSelectionCircleView.swift index 3048d4fac..a220f6f45 100644 --- a/Stitch/Graph/Sidebar/View/SidebarItem/SidebarListItemSelectionCircleView.swift +++ b/Stitch/Graph/Sidebar/View/SidebarItem/SidebarListItemSelectionCircleView.swift @@ -8,24 +8,26 @@ import SwiftUI import StitchSchemaKit -struct SidebarListItemSelectionCircleView: View { +struct SidebarListItemSelectionCircleView: View where Item: SidebarItemSwipable { - static let SELECTION_CIRCLE_SELECTED = "circle.inset.filled" - static let SELECTION_CIRCLE = "circle" + private let SELECTION_CIRCLE_SELECTED = "circle.inset.filled" + private let SELECTION_CIRCLE = "circle" - let id: LayerNodeId + @Bindable var item: Item + @Bindable var selectionState: SidebarSelectionObserver // white when layer is non-edit-mode selected; else determined by primary vs secondary selection status let fontColor: Color - - let selection: SidebarListItemSelectionStatus - let isHidden: Bool let isBeingEdited: Bool var iconName: String { selection.isSelected - ? Self.SELECTION_CIRCLE_SELECTED - : Self.SELECTION_CIRCLE + ? self.SELECTION_CIRCLE_SELECTED + : self.SELECTION_CIRCLE + } + + var selection: SidebarListItemSelectionStatus { + selectionState.getSelectionStatus(item.id) } var body: some View { @@ -46,17 +48,20 @@ struct SidebarListItemSelectionCircleView: View { height: SIDEBAR_ITEM_ICON_LENGTH) .padding(4) .contentShape(Rectangle()) - .onTapGesture { + + // simultaneous needed to fix issues where SidebarListGestureRecognizer's + // tap gesturecancels touches + .simultaneousGesture(TapGesture().onEnded { log("SidebarListItemSelectionCircleView: tapCallback") // ie What kind of selection did we have? // - if item was already 100% selected, then deselect // - if was 80% or 0% selected, then 100% select switch selection { case .primary: - dispatch(SidebarItemDeselected(id: id)) + item.didUnselectOnEditMode() case .secondary, .none: - dispatch(SidebarItemSelected(id: id)) + item.didSelectOnEditMode() } - } + }) } } diff --git a/Stitch/Graph/Sidebar/View/SidebarItem/SidebarListItemView.swift b/Stitch/Graph/Sidebar/View/SidebarItem/SidebarListItemView.swift index b6335246f..bf9ceb873 100644 --- a/Stitch/Graph/Sidebar/View/SidebarItem/SidebarListItemView.swift +++ b/Stitch/Graph/Sidebar/View/SidebarItem/SidebarListItemView.swift @@ -9,49 +9,35 @@ import SwiftUI import StitchSchemaKit import GameController -struct SidebarListItemView: View { +struct SidebarListItemView: View where SidebarViewModel: ProjectSidebarObservable { + typealias ItemID = SidebarViewModel.ItemID - @Environment(\.appTheme) var theme + @Environment(\.appTheme) private var theme + @EnvironmentObject private var keyboardObserver: KeyboardObserver @Bindable var graph: GraphState - - @EnvironmentObject var keyboardObserver: KeyboardObserver - - var item: SidebarListItem - let name: String - let layer: Layer - var current: SidebarDraggedItem? - var proposedGroup: ProposedGroup? - var isClosed: Bool - - // white when layer is non-edit-mode selected; else determined by primary vs secondary selection status - let fontColor: Color - - let selection: SidebarListItemSelectionStatus - let isBeingEdited: Bool - let isHidden: Bool - + @Bindable var sidebarViewModel: SidebarViewModel + @Bindable var item: SidebarViewModel.ItemViewModel let swipeOffset: CGFloat - - // TODO: should be for *all* selected-layers during a drag - var isBeingDragged: Bool { - current.map { $0.current == item.id } ?? false + + var isBeingEdited: Bool { + self.sidebarViewModel.isEditing } - var isProposedGroup: Bool { - proposedGroup?.parentId == item.id + var proposedGroup: SidebarViewModel.ItemViewModel? { + self.sidebarViewModel.proposedGroup } - var layerNodeId: LayerNodeId { - item.id.asLayerNodeId + var isProposedGroup: Bool { + proposedGroup?.id == item.id } var isNonEditModeFocused: Bool { - graph.sidebarSelectionState.inspectorFocusedLayers.focused.contains(layerNodeId) + sidebarViewModel.inspectorFocusedLayers.focused.contains(item.id) } var isNonEditModeActivelySelected: Bool { - graph.sidebarSelectionState.inspectorFocusedLayers.activelySelected.contains(layerNodeId) + sidebarViewModel.inspectorFocusedLayers.activelySelected.contains(item.id) } var isNonEditModeSelected: Bool { @@ -63,20 +49,12 @@ struct SidebarListItemView: View { HStack(spacing: 0) { SidebarListItemLeftLabelView( graph: graph, - name: name, - layer: layer, - nodeId: layerNodeId, - fontColor: fontColor, - selection: selection, - isHidden: isHidden, - isBeingEdited: isBeingEdited, - isGroup: item.isGroup, - isClosed: isClosed) + sidebarViewModel: sidebarViewModel, + itemViewModel: item) // .padding(.leading) .offset(x: -swipeOffset) Spacer() - } .contentShape(Rectangle()) // for hit area @@ -115,9 +93,6 @@ struct SidebarListItemView: View { // Preferably animate the smallest view possible; when this .animation was applied outside the .overlay, we undesiredly animated text color changes .animation(.default, value: isProposedGroup) } - - // TODO: needs to be for all actively-dragged selected layers -// .animation(.default, value: isBeingDragged) } } diff --git a/Stitch/Graph/Sidebar/View/SidebarItem/SidebarListLabelView.swift b/Stitch/Graph/Sidebar/View/SidebarItem/SidebarListLabelView.swift index 27f2718cf..e4bc0731b 100644 --- a/Stitch/Graph/Sidebar/View/SidebarItem/SidebarListLabelView.swift +++ b/Stitch/Graph/Sidebar/View/SidebarItem/SidebarListLabelView.swift @@ -8,80 +8,37 @@ import SwiftUI import StitchSchemaKit -struct SidebarListItemLeftLabelView: View { - - @Bindable var graph: GraphState - - let name: String - let layer: Layer - let nodeId: LayerNodeId // debug +struct SidebarListItemLeftLabelView: View where SidebarViewModel: ProjectSidebarObservable { + @State private var isBeingEditedAnimated = false - // white when layer is non-edit-mode selected; else determined by primary vs secondary selection status - let fontColor: Color + @Bindable var graph: GraphState + @Bindable var sidebarViewModel: SidebarViewModel + @Bindable var itemViewModel: SidebarViewModel.ItemViewModel - let selection: SidebarListItemSelectionStatus - let isHidden: Bool - let isBeingEdited: Bool - let isGroup: Bool - let isClosed: Bool - - @State private var isBeingEditedAnimated = false - - // TODO: perf: will this GraphState-reading computed variable cause SidebarListItemLeftLabelView to render too often? + var isBeingEdited: Bool { + self.sidebarViewModel.isEditing + } - // TODO: should we only show the arrow icon when we have a sidebar layer immediately above? @MainActor var masks: Bool { - - // TODO: why is this not animated? and why does it jitter? -// // index of this layer -// guard let index = graph.sidebarListState.masterList.items -// .firstIndex(where: { $0.id.asLayerNodeId == nodeId }) else { -// return withAnimation { false } -// } -// -// // hasSidebarLayerImmediatelyAbove -// guard graph.sidebarListState.masterList.items[safe: index - 1].isDefined else { -// return withAnimation { false } -// } -// - let atleastOneIndexMasks = graph - .getLayerNode(id: nodeId.id)? - .layerNode?.masksPort.allLoopedValues - .contains(where: { $0.getBool ?? false }) - ?? false - - return withAnimation { - atleastOneIndexMasks - } + self.itemViewModel.isMasking } - - var _name: String { -// return name - -#if DEV_DEBUG - name + " \(nodeId.id.debugFriendlyId)" -#else - name -#endif + var fontColor: Color { + self.itemViewModel.fontColor } var body: some View { HStack(spacing: 4) { -// if isGroup { - SidebarListItemChevronView(isClosed: isClosed, - parentId: nodeId, - fontColor: fontColor, - isHidden: isHidden) - .opacity(isGroup ? 1 : 0) + SidebarListItemChevronView(sidebarViewModel: sidebarViewModel, + item: itemViewModel) +// isHidden: isHidden) + .opacity(itemViewModel.isGroup ? 1 : 0) // .border(.green) // } - Image(systemName: graph.sidebarLeftSideIcon(layer: layer, - layerId: nodeId.asNodeId, - activeIndex: graph.activeIndex)) + Image(systemName: itemViewModel.sidebarLeftSideIcon) .scaledToFit() .frame(width: SIDEBAR_LIST_ITEM_ICON_AND_TEXT_AREA_HEIGHT, height: SIDEBAR_LIST_ITEM_ICON_AND_TEXT_AREA_HEIGHT) @@ -116,8 +73,7 @@ struct SidebarListItemLeftLabelView: View { var label: some View { Group { if isBeingEdited { - SidebarListLabelEditView(id: nodeId, - name: _name, + SidebarListLabelEditView(item: self.itemViewModel, fontColor: fontColor, graph: graph) .truncationMode(.tail) @@ -127,8 +83,7 @@ struct SidebarListItemLeftLabelView: View { .padding(.trailing, 60) #endif } else { - SidebarListLabelEditView(id: nodeId, - name: _name, + SidebarListLabelEditView(item: self.itemViewModel, fontColor: fontColor, graph: graph) } @@ -137,24 +92,32 @@ struct SidebarListItemLeftLabelView: View { } } -struct SidebarListLabelEditView: View { +struct SidebarListLabelEditView: View where ItemViewModel: SidebarItemSwipable { // Do we need to add another focused field type here? // If this is focused, you don't want - let id: LayerNodeId - let name: String + @Bindable var item: ItemViewModel let fontColor: Color @Bindable var graph: GraphState @State var edit: String = "" + var name: String { + let name = self.item.name +#if DEV_DEBUG + return name + " \(self.item.id.debugFriendlyId)" +#else + return name +#endif + } + @MainActor var isFocused: Bool { switch graph.graphUI.reduxFocusedField { - case .sidebarLayerTitle(let layerNodeId): - let k = layerNodeId == id + case .sidebarLayerTitle(let idString): + let k = item.id.description == idString // log("SidebarListLabelEditView: isFocused: \(k) for \(id)") return k default: @@ -170,14 +133,12 @@ struct SidebarListLabelEditView: View { if isFocused { // logInView("SidebarListLabelEditView: editable field") StitchTextEditingBindingField(currentEdit: self.$edit, - fieldType: .sidebarLayerTitle(id), + fieldType: .sidebarLayerTitle(self.item.id.description), font: SIDEBAR_LIST_ITEM_FONT, fontColor: fontColor, fieldEditCallback: { (newEdit: String, isCommitting: Bool) in - // Treat this is as a "layer inspector edit" ? - dispatch(NodeTitleEdited(titleEditType: .layerInspector(id.asNodeId), - edit: newEdit, - isCommitting: isCommitting)) + self.item.didLabelEdit(to: newEdit, + isCommitting: isCommitting) }) } else { // logInView("SidebarListLabelEditView: read only") @@ -191,45 +152,35 @@ struct SidebarListLabelEditView: View { self.edit = name } .onTapGesture(count: 2) { - log("SidebarListLabelEditView: double tap \(id)") - dispatch(ReduxFieldFocused(focusedField: .sidebarLayerTitle(id))) + dispatch(ReduxFieldFocused(focusedField: .sidebarLayerTitle(self.item.id.description))) } } } -struct SidebarListItemRightLabelView: View { +struct SidebarListItemRightLabelView: View where ItemViewModel: SidebarItemSwipable { + @State private var isBeingEditedAnimated = false - let item: SidebarListItem - let isGroup: Bool - let isClosed: Bool - - // white when layer is non-edit-mode selected; else determined by primary vs secondary selection status - let fontColor: Color - - let selection: SidebarListItemSelectionStatus + let item: ItemViewModel + let selectionState: SidebarSelectionObserver let isBeingEdited: Bool // is sidebar being edited? - let isHidden: Bool - @State private var isBeingEditedAnimated = false - var body: some View { - let id = item.id.asLayerNodeId +// let id = item.id.asLayerNodeId HStack(spacing: .zero) { if isBeingEditedAnimated { HStack(spacing: .zero) { - SidebarListItemSelectionCircleView(id: id, - fontColor: fontColor, - selection: selection, - isHidden: isHidden, + SidebarListItemSelectionCircleView(item: item, + selectionState: selectionState, + fontColor: item.fontColor, isBeingEdited: isBeingEdited) .padding(.trailing, 4) - SidebarListDragIconView(item: item) + SidebarListDragIconView() .padding(.trailing, 4) } .transition(.slideInAndOut) @@ -249,9 +200,6 @@ let EDIT_MODE_HAMBURGER_DRAG_ICON_COLOR: Color = .gray // Always gray, whether l // TODO: on iPad, dragging the hamburger icon should immediately drag the sidebar-item without need for long press first struct SidebarListDragIconView: View { - - let item: SidebarListItem - var body: some View { Image(systemName: EDIT_MODE_HAMBURGER_DRAG_ICON) // TODO: Should use white if this sidebar layer is selected? diff --git a/Stitch/Graph/Sidebar/View/SidebarItem/SwipeView/SidebarListItemGestureRecognizerView.swift b/Stitch/Graph/Sidebar/View/SidebarItem/SwipeView/SidebarListItemGestureRecognizerView.swift index 62348a8e4..fcdc5645f 100644 --- a/Stitch/Graph/Sidebar/View/SidebarItem/SwipeView/SidebarListItemGestureRecognizerView.swift +++ b/Stitch/Graph/Sidebar/View/SidebarItem/SwipeView/SidebarListItemGestureRecognizerView.swift @@ -24,16 +24,19 @@ typealias OnItemDragChangedHandler = (CGSize) -> Void // or trackpad click + drag (for immediate item dragging) // a gesture recognizer for the item in the custom list itself -struct SidebarListItemGestureRecognizerView: UIViewControllerRepresentable { - let view: T - @ObservedObject var gestureViewModel: SidebarItemGestureViewModel +struct SidebarListItemGestureRecognizerView: UIViewControllerRepresentable { + @EnvironmentObject private var keyboardObserver: KeyboardObserver - @EnvironmentObject var keyboardObserver: KeyboardObserver + let view: T + @Bindable var sidebarViewModel: SidebarViewModel + @Bindable var gestureViewModel: SidebarViewModel.ItemViewModel var instantDrag: Bool = false - var graph: GraphState - var layerNodeId: LayerNodeId + var itemId: SidebarViewModel.ItemID { + gestureViewModel.id + } func makeUIViewController(context: Context) -> GestureHostingController { let vc = GestureHostingController( @@ -87,22 +90,23 @@ struct SidebarListItemGestureRecognizerView: UIViewControllerRepresenta uiViewController.rootView = view delegate.instantDrag = instantDrag - - delegate.graph = graph - delegate.layerNodeId = layerNodeId + delegate.gestureViewModel = gestureViewModel + delegate.sidebarViewModel = sidebarViewModel + delegate.keyboardObserver = keyboardObserver + delegate.itemId = itemId } - func makeCoordinator() -> SidebarListGestureRecognizer { - SidebarListGestureRecognizer( + func makeCoordinator() -> SidebarListGestureRecognizer { + SidebarListGestureRecognizer( gestureViewModel: gestureViewModel, + sidebarViewModel: sidebarViewModel, keyboardObserver: keyboardObserver, instantDrag: instantDrag, - graph: graph, - layerNodeId: layerNodeId) + itemId: itemId) } } -final class SidebarListGestureRecognizer: NSObject, UIGestureRecognizerDelegate { +final class SidebarListGestureRecognizer: NSObject, UIGestureRecognizerDelegate, UIContextMenuInteractionDelegate { // Handles: // - one finger on screen item-swiping // - two fingers on trackpad item-swiping @@ -112,29 +116,31 @@ final class SidebarListGestureRecognizer: NSObject, UIGestureRecognizerDelegate // - one finger long-press-drag item-dragging: see `SwiftUI .simultaneousGesture` // - two fingers on trackpad list scrolling - let gestureViewModel: SidebarItemGestureViewModel - var keyboardObserver: KeyboardObserver - var instantDrag: Bool - var graph: GraphState - var layerNodeId: LayerNodeId + var itemId: SidebarViewModel.ItemID var shiftHeldDown = false var commandHeldDown = false - init(gestureViewModel: SidebarItemGestureViewModel, + weak var sidebarViewModel: SidebarViewModel? + weak var gestureViewModel: SidebarViewModel.ItemViewModel? + weak var keyboardObserver: KeyboardObserver? + + init(gestureViewModel: SidebarViewModel.ItemViewModel, + sidebarViewModel: SidebarViewModel, keyboardObserver: KeyboardObserver, instantDrag: Bool, - graph: GraphState, - layerNodeId: LayerNodeId) { - + itemId: SidebarViewModel.ItemID) { + self.sidebarViewModel = sidebarViewModel self.gestureViewModel = gestureViewModel self.keyboardObserver = keyboardObserver self.instantDrag = instantDrag - - self.graph = graph - self.layerNodeId = layerNodeId + self.itemId = itemId + } + + var graph: GraphState? { + self.sidebarViewModel?.graphDelegate } func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, @@ -160,14 +166,16 @@ final class SidebarListGestureRecognizer: NSObject, UIGestureRecognizerDelegate } @objc func tapInView(_ gestureRecognizer: UITapGestureRecognizer) { - if graph.sidebarSelectionState.isEditMode || gestureViewModel.swipeSetting == .open { + guard let sidebarViewModel = self.sidebarViewModel, + let gestureViewModel = self.gestureViewModel else { return } + + if sidebarViewModel.isEditing || gestureViewModel.swipeSetting == .open { return } - dispatch(SidebarItemTapped(id: layerNodeId, - shiftHeld: self.shiftHeldDown, - commandHeld: self.commandHeldDown)) - + self.sidebarViewModel?.sidebarItemTapped(id: self.itemId, + shiftHeld: self.shiftHeldDown, + commandHeld: self.commandHeldDown) } // finger on screen @@ -188,10 +196,10 @@ final class SidebarListGestureRecognizer: NSObject, UIGestureRecognizerDelegate case .changed: if instantDrag { - gestureViewModel.onItemDragChanged(translation.toCGSize) + gestureViewModel?.onItemDragChanged(translation.toCGSize) } // let velocity = gestureRecognizer.velocity(in: gestureRecognizer.view) - gestureViewModel.onItemSwipeChanged(translation.x) + gestureViewModel?.onItemSwipeChanged(translation.x) default: break // do nothing } @@ -203,9 +211,9 @@ final class SidebarListGestureRecognizer: NSObject, UIGestureRecognizerDelegate switch gestureRecognizer.state { case .ended, .cancelled: if instantDrag { - gestureViewModel.onItemDragEnded() + gestureViewModel?.onItemDragEnded() } - gestureViewModel.onItemSwipeEnded() + gestureViewModel?.onItemSwipeEnded() default: break } @@ -230,11 +238,11 @@ final class SidebarListGestureRecognizer: NSObject, UIGestureRecognizerDelegate switch gestureRecognizer.state { case .changed: - gestureViewModel.onItemSwipeChanged(translation.x) + gestureViewModel?.onItemSwipeChanged(translation.x) // let velocity = gestureRecognizer.velocity(in: gestureRecognizer.view) case .ended, .cancelled: - gestureViewModel.onItemSwipeEnded() - gestureViewModel.onItemDragEnded() + gestureViewModel?.onItemSwipeEnded() + gestureViewModel?.onItemDragEnded() default: // log("CustomListItemGestureRecognizerVC: touches 0: trackpadGestureHandler: default") break @@ -245,74 +253,79 @@ final class SidebarListGestureRecognizer: NSObject, UIGestureRecognizerDelegate else if gestureRecognizer.numberOfTouches == 1 { switch gestureRecognizer.state { case .changed: - gestureViewModel.onItemDragChanged(translation.toCGSize) + gestureViewModel?.onItemDragChanged(translation.toCGSize) default: // log("CustomListItemGestureRecognizerVC: trackpadGestureHandler: default") break } } } // trackpadGestureHandler -} - -extension SidebarListGestureRecognizer: UIContextMenuInteractionDelegate { - + // // NOTE: Not needed, since the required `contextMenuInteraction` delegate method is called every time the menu appears? // func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willDisplayMenuFor configuration: UIContextMenuConfiguration, animator: (any UIContextMenuInteractionAnimating)?) { // log("UIContextMenuInteractionDelegate: contextMenuInteraction: WILL DISPLAY MENU") // } - + func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? { - + if let keyboardObserver = self.keyboardObserver, + let graph = self.graph { + return self.gestureViewModel?.contextMenuInteraction(itemId: self.itemId, + graph: graph, + keyboardObserver: keyboardObserver) + } + return nil + } +} + +extension SidebarItemGestureViewModel { + @MainActor + func contextMenuInteraction(itemId: SidebarListItemId, + graph: GraphState, + keyboardObserver: KeyboardObserver) -> UIContextMenuConfiguration? { // log("UIContextMenuInteractionDelegate: contextMenuInteraction") + + guard let sidebarViewModel = self.sidebarDelegate else { return nil } + let selections = sidebarViewModel.selectionState // Only select the layer if not already actively-selected; otherwise just open the menu - if !self.graph.sidebarSelectionState.inspectorFocusedLayers.activelySelected.contains(self.layerNodeId) { + if !selections.inspectorFocusedLayers.activelySelected.contains(itemId) { let isShiftDown = keyboardObserver.keyboard?.keyboardInput?.isShiftPressed ?? false // Note: we do the selection logic in here so that - self.graph.sidebarItemTapped( - id: self.layerNodeId, + self.sidebarDelegate?.sidebarItemTapped( + id: itemId, shiftHeld: isShiftDown, - commandHeld: self.graph.keypressState.isCommandPressed) + commandHeld: graph.keypressState.isCommandPressed) } - - let selections = self.graph.sidebarSelectionState - let groups = self.graph.getSidebarGroupsDict() - let sidebarDeps = SidebarDeps(layerNodes: .fromLayerNodesDict( nodes: self.graph.layerNodes, orderedSidebarItems: self.graph.orderedSidebarLayers), - groups: groups, - expandedItems: self.graph.getSidebarExpandedItems()) - let layerNodes = sidebarDeps.layerNodes - - let primary = selections.primary - - return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { _ in + return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { [weak graph] _ in + guard let graph = graph else { return UIMenu(title: "", children: []) } + var buttons: [UIMenuElement] = [] - if canUngroup(primary, nodes: layerNodes) { + if sidebarViewModel.canUngroup() { buttons.append(UIAction(title: "Ungroup", image: nil) { action in // Handle action here - dispatch(SidebarGroupUncreated()) + self.sidebarDelegate?.sidebarGroupUncreated() }) } - let canGroup = primary.nonEmptyPrimary.map { canBeGrouped($0, groups: groups) } ?? false - if canGroup { + if sidebarViewModel.canBeGrouped() { buttons.append(UIAction(title: "Group", image: nil) { action in - dispatch(SidebarGroupCreated()) + sidebarViewModel.sidebarGroupCreated() }) } - if canDuplicate(primary) { + if sidebarViewModel.canDuplicate() { let groupButton = UIAction(title: "Duplicate", image: nil) { action in dispatch(SidebarSelectedItemsDuplicated()) } buttons.append(groupButton) } - let activeSelections = self.graph.sidebarSelectionState.inspectorFocusedLayers.activelySelected + let activeSelections = graph.sidebarSelectionState.inspectorFocusedLayers.activelySelected let atLeastOneSelected = !activeSelections.isEmpty @@ -326,7 +339,7 @@ extension SidebarListGestureRecognizer: UIContextMenuInteractionDelegate { if onlyOneSelected, let layerNodeId = selections.primary.first, - let isVisible = self.graph.getLayerNode(id: layerNodeId.asNodeId)?.layerNode?.hasSidebarVisibility { + let isVisible = graph.getLayerNode(id: layerNodeId)?.layerNode?.hasSidebarVisibility { buttons.append(UIAction(title: isVisible ? "Hide Layer" : "Unhide Layer", image: nil) { action in dispatch(SidebarItemHiddenStatusToggled(clickedId: layerNodeId)) @@ -336,7 +349,7 @@ extension SidebarListGestureRecognizer: UIContextMenuInteractionDelegate { if activeSelections.count > 1 { buttons.append(UIAction(title: "Hide Layers", image: nil) { action in dispatch(SelectedLayersVisiblityUpdated(selectedLayers: selections.primary, - newVisibilityStatus: false)) + newVisibilityStatus: false)) }) buttons.append(UIAction(title: "Unhide Layers", image: nil) { action in diff --git a/Stitch/Graph/Sidebar/View/SidebarItem/SwipeView/SidebarListItemSwipeButton.swift b/Stitch/Graph/Sidebar/View/SidebarItem/SwipeView/SidebarListItemSwipeButton.swift index 0e87d785e..6556f7440 100644 --- a/Stitch/Graph/Sidebar/View/SidebarItem/SwipeView/SidebarListItemSwipeButton.swift +++ b/Stitch/Graph/Sidebar/View/SidebarItem/SwipeView/SidebarListItemSwipeButton.swift @@ -9,19 +9,19 @@ import Foundation import SwiftUI import StitchSchemaKit -struct SidebarListItemSwipeButton: View { - var action: Action? +struct SidebarListItemSwipeButton: View { let sfImageName: String let backgroundColor: Color var willLeftAlign: Bool = false - @ObservedObject var gestureViewModel: SidebarItemGestureViewModel + @Bindable var gestureViewModel: Item + + let action: @MainActor () -> Void var body: some View { UIKitTappableWrapper(tapCallback: { - if let action = action { - dispatch(action) - } + action() + withAnimation { gestureViewModel.resetSwipePosition() } diff --git a/Stitch/Graph/Sidebar/View/SidebarItem/SwipeView/SidebarListItemSwipeInnerView.swift b/Stitch/Graph/Sidebar/View/SidebarItem/SwipeView/SidebarListItemSwipeInnerView.swift index 0bf7ce527..d8185d16f 100644 --- a/Stitch/Graph/Sidebar/View/SidebarItem/SwipeView/SidebarListItemSwipeInnerView.swift +++ b/Stitch/Graph/Sidebar/View/SidebarItem/SwipeView/SidebarListItemSwipeInnerView.swift @@ -9,153 +9,35 @@ import Foundation import SwiftUI import StitchSchemaKit -struct SidebarListItemSwipeInnerView: View { - - @Environment(\.appTheme) var theme +struct SidebarListItemSwipeInnerView: View where SidebarViewModel: ProjectSidebarObservable { + // The actual rendered distance for the swipe distance + @State private var swipeX: Double = 0 + @State private var sidebarWidth: Double = .zero @Bindable var graph: GraphState - - var item: SidebarListItem - let name: String - let layer: Layer - var current: SidebarDraggedItem? - var proposedGroup: ProposedGroup? - var isClosed: Bool - let selection: SidebarListItemSelectionStatus - let isBeingEdited: Bool - let swipeSetting: SidebarSwipeSetting - let sidebarWidth: CGFloat - let isHovered: Bool - - // The actual rendered distance for the swipe distance - @State var swipeX: CGFloat = 0 - @ObservedObject var gestureViewModel: SidebarItemGestureViewModel + @Bindable var sidebarViewModel: SidebarViewModel + @Bindable var itemViewModel: SidebarViewModel.ItemViewModel var showMainItem: Bool { swipeX < DEFAULT_ACTION_THRESHOLD } - var itemIndent: CGFloat { item.location.x } - - @MainActor - var isHidden: Bool { - graph.getVisibilityStatus(for: item.id.asNodeId) != .visible - } - - var fontColor: Color { - - #if DEV_DEBUG - if isHidden { - return .purple - } - #endif - - // Any 'focused' (doesn't have to be 'actively selected') layer uses white text - if isNonEditModeSelected { -#if DEV_DEBUG - return .red -#else - return .white -#endif - } - -#if DEV_DEBUG - // Easier to see secondary selections for debug - // return selection.color(isHidden) - - switch selection { - case .primary: - return .brown - case .secondary: - return .green - case .none: - return .blue - } - -#endif - - if isBeingEdited || isHidden { - return selection.color(isHidden) - } else { - // i.e. if we are not in edit mode, do NOT show secondarily-selected layers (i.e. children of a primarily-selected parent) as gray - return SIDE_BAR_OPTIONS_TITLE_FONT_COLOR - } - - } - - var layerNodeId: LayerNodeId { - item.id.asLayerNodeId - } - - var isBeingDragged: Bool { - current.map { $0.current == item.id } ?? false - } - - var isNonEditModeFocused: Bool { - graph.sidebarSelectionState.inspectorFocusedLayers.focused.contains(layerNodeId) - } - - var isNonEditModeActivelySelected: Bool { - graph.sidebarSelectionState.inspectorFocusedLayers.activelySelected.contains(layerNodeId) - } - - var isNonEditModeSelected: Bool { - isNonEditModeFocused || isNonEditModeActivelySelected - } - - var isImplicitlyDragged: Bool { - graph.sidebarSelectionState.implicitlyDragged.contains(item.id) - } - - var useHalfOpacityBackground: Bool { - isImplicitlyDragged || (isNonEditModeFocused && !isNonEditModeActivelySelected) - } - - var backgroundOpacity: CGFloat { - if isImplicitlyDragged { - return 0.5 - } else if (isNonEditModeFocused || isBeingDragged) { - return (isNonEditModeFocused && !isNonEditModeActivelySelected) ? 0.5 : 1 - } else { - return 0 - } - } - var body: some View { HStack(spacing: .zero) { - // Main row hides if swipe menu exceeds threshold if showMainItem { SidebarListItemView(graph: graph, - item: item, - name: name, - layer: layer, - current: current, - proposedGroup: proposedGroup, - isClosed: isClosed, - fontColor: fontColor, - selection: selection, - isBeingEdited: isBeingEdited, - isHidden: isHidden, + sidebarViewModel: sidebarViewModel, + item: itemViewModel, swipeOffset: swipeX) - .padding(.leading, itemIndent + 5) - .background { - theme.fontColor - .opacity(self.backgroundOpacity) - } // right-side label overlay comes AFTER x-placement of item, // so as not to be affected by x-placement. .overlay(alignment: .trailing) { #if !targetEnvironment(macCatalyst) SidebarListItemRightLabelView( - item: item, - isGroup: item.isGroup, - isClosed: isClosed, - fontColor: fontColor, - selection: selection, - isBeingEdited: isBeingEdited, - isHidden: isHidden) + item: itemViewModel, + selectionState: sidebarViewModel.selectionState, + isBeingEdited: sidebarViewModel.isEditing) .frame(height: SIDEBAR_LIST_ITEM_ICON_AND_TEXT_AREA_HEIGHT) - #endif // // TODO: revisit this; currently still broken on Catalyst and the UIKitTappableWrapper becomes unresponsive as soon as we apply a SwiftUI .frame or .offset; `Spacer()`s also do not seem to work @@ -176,22 +58,34 @@ struct SidebarListItemSwipeInnerView: View { } .padding(.trailing, 2) - } #if !targetEnvironment(macCatalyst) SidebarListItemSwipeMenu( - item: item, - swipeOffset: swipeX, - visStatusIconName: graph.getLayerNode(id: item.id.id)?.layerNode?.visibilityStatusIcon ?? SIDEBAR_VISIBILITY_STATUS_VISIBLE_ICON, - gestureViewModel: self.gestureViewModel) + gestureViewModel: itemViewModel, + swipeOffset: swipeX) #endif } +#if !targetEnvironment(macCatalyst) + .background { + GeometryReader { geometry in + Color.clear + .onAppear { + self.sidebarWidth = geometry.size.width + } + .onChange(of: geometry.size.width) { _, newWidth in + if newWidth != self.sidebarWidth { + self.sidebarWidth = newWidth + } + } + } + } +#endif // Animates swipe distance if it gets pinned to its open or closed position. // Does NOT animate for normal swiping. #if !targetEnvironment(macCatalyst) - .onChange(of: swipeSetting) { newSwipeSetting in + .onChange(of: self.itemViewModel.swipeSetting) { _, newSwipeSetting in switch newSwipeSetting { case .closed, .open: @@ -211,6 +105,5 @@ struct SidebarListItemSwipeInnerView: View { } #endif .animation(.stitchAnimation(duration: 0.25), value: showMainItem) - .animation(.stitchAnimation(duration: 0.25), value: itemIndent) } } diff --git a/Stitch/Graph/Sidebar/View/SidebarItem/SwipeView/SidebarListItemSwipeMenu.swift b/Stitch/Graph/Sidebar/View/SidebarItem/SwipeView/SidebarListItemSwipeMenu.swift index 889b5ea5c..b449b1146 100644 --- a/Stitch/Graph/Sidebar/View/SidebarItem/SwipeView/SidebarListItemSwipeMenu.swift +++ b/Stitch/Graph/Sidebar/View/SidebarItem/SwipeView/SidebarListItemSwipeMenu.swift @@ -9,34 +9,38 @@ import Foundation import SwiftUI import StitchSchemaKit -struct SidebarListItemSwipeMenu: View { - let item: SidebarListItem +struct SidebarListItemSwipeMenu: View where Item: SidebarItemSwipable { + @Bindable var gestureViewModel: Item let swipeOffset: CGFloat - let visStatusIconName: String - - @ObservedObject var gestureViewModel: SidebarItemGestureViewModel var showNonDefaultOptions: Bool { swipeOffset < DEFAULT_ACTION_THRESHOLD } + @MainActor + var visStatusIconName: String { + gestureViewModel.isVisible ? SIDEBAR_VISIBILITY_STATUS_VISIBLE_ICON : SIDEBAR_VISIBILITY_STATUS_HIDDEN_ICON + } + var body: some View { HStack(spacing: 2) { // Hide other options after sufficient swipe if showNonDefaultOptions { SidebarListItemSwipeButton(sfImageName: "ellipsis.circle", backgroundColor: GREY_SWIPE_MENU_OPTION_COLOR, - gestureViewModel: gestureViewModel) + gestureViewModel: gestureViewModel) { } - SidebarListItemSwipeButton(action: SidebarItemHiddenStatusToggled(clickedId: item.id.asLayerNodeId), - sfImageName: visStatusIconName, + SidebarListItemSwipeButton(sfImageName: visStatusIconName, backgroundColor: STITCH_PURPLE, - gestureViewModel: gestureViewModel) + gestureViewModel: gestureViewModel) { + gestureViewModel.didToggleVisibility() + } } - SidebarListItemSwipeButton(action: SidebarItemDeleted(itemId: item.id), - sfImageName: "trash", + SidebarListItemSwipeButton(sfImageName: "trash", backgroundColor: Color(.stitchRed), willLeftAlign: !showNonDefaultOptions, - gestureViewModel: gestureViewModel) + gestureViewModel: gestureViewModel) { + gestureViewModel.didDeleteItem() + } } .animation(.stitchAnimation(duration: 0.25), value: showNonDefaultOptions) .disabled(swipeOffset == 0) diff --git a/Stitch/Graph/Sidebar/View/SidebarItem/SwipeView/SidebarListItemSwipeView.swift b/Stitch/Graph/Sidebar/View/SidebarItem/SwipeView/SidebarListItemSwipeView.swift index 815b9613b..922e83b2d 100644 --- a/Stitch/Graph/Sidebar/View/SidebarItem/SwipeView/SidebarListItemSwipeView.swift +++ b/Stitch/Graph/Sidebar/View/SidebarItem/SwipeView/SidebarListItemSwipeView.swift @@ -8,96 +8,84 @@ import SwiftUI import StitchSchemaKit -struct SidebarListItemSwipeView: View { - @Bindable var graph: GraphState +struct SidebarListItemSwipeView: View where SidebarViewModel: ProjectSidebarObservable { + typealias ItemViewModel = SidebarViewModel.ItemViewModel - @StateObject var gestureViewModel: SidebarItemGestureViewModel - - var item: SidebarListItem - let name: String - let layer: Layer - var current: SidebarDraggedItem? - var proposedGroup: ProposedGroup? - var isClosed: Bool - let selection: SidebarListItemSelectionStatus - let isBeingEdited: Bool - - @Binding var activeGesture: SidebarListActiveGesture - - @Binding var activeSwipeId: SidebarListItemId? - - @State var isHovered = false + @Environment(\.appTheme) private var theme + @State private var isHovered = false + + @Bindable var graph: GraphState + @Bindable var sidebarViewModel: SidebarViewModel + @Bindable var gestureViewModel: ItemViewModel - init(graph: Bindable, - item: SidebarListItem, - name: String, - layer: Layer, - current: SidebarDraggedItem? = nil, - proposedGroup: ProposedGroup? = nil, - isClosed: Bool, - selection: SidebarListItemSelectionStatus, - isBeingEdited: Bool, - activeGesture: Binding, - activeSwipeId: Binding = .constant(nil)) { + var yOffset: CGFloat { + guard let dragPosition = gestureViewModel.dragPosition else { + return gestureViewModel.location.y + } - self._graph = graph - - self.item = item - self.name = name - self.layer = layer - self.current = current - self.proposedGroup = proposedGroup - self.isClosed = isClosed - self.selection = selection - self.isBeingEdited = isBeingEdited - self._activeGesture = activeGesture - self._activeSwipeId = activeSwipeId - - self._gestureViewModel = StateObject(wrappedValue: SidebarItemGestureViewModel(item: item, - activeGesture: activeGesture, - activeSwipeId: activeSwipeId)) + return dragPosition.y + } + + var indentationPadding: Int { + CUSTOM_LIST_ITEM_INDENTATION_LEVEL * gestureViewModel.sidebarIndex.groupIndex + } + + // Controls animation for non-dragged elements + var animationDuration: Double { + gestureViewModel.isBeingDragged ? 0 : 0.25 } var body: some View { // TODO: why does drag gesture on Catalyst break if we remove this? SidebarListItemGestureRecognizerView( view: customSwipeItem, - gestureViewModel: gestureViewModel, - graph: graph, - layerNodeId: item.id.asLayerNodeId) + sidebarViewModel: sidebarViewModel, + gestureViewModel: gestureViewModel) + .transition(.move(edge: .top).combined(with: .opacity)) + + // MARK: indent padding animation needs to be before y animation + .animation(.stitchAnimation(duration: 0.25), value: indentationPadding) + .animation(.stitchAnimation(duration: animationDuration), value: gestureViewModel.location.y) + + .zIndex(gestureViewModel.zIndex) .height(CGFloat(CUSTOM_LIST_ITEM_VIEW_HEIGHT)) .padding(.horizontal, 4) - - // More accurate: needs to come before the `.offset(y:)` modifier + .padding(.leading, CGFloat(indentationPadding)) + .background { + theme.fontColor + .opacity(gestureViewModel.backgroundOpacity) + } .onHover { hovering in - // log("hovering: sidebar item \(item.id.id)") + // log("hovering: sidebar item \(gestureViewModel.id)") // log("hovering: \(hovering)") self.isHovered = hovering if hovering { - dispatch(SidebarLayerHovered(layer: item.id.asLayerNodeId)) + self.gestureViewModel.sidebarLayerHovered(itemId: gestureViewModel.id) } else { - dispatch(SidebarLayerHoverEnded(layer: item.id.asLayerNodeId)) + self.gestureViewModel.sidebarLayerHoverEnded(itemId: gestureViewModel.id) } } - .offset(y: item.location.y) - #if targetEnvironment(macCatalyst) + // MARK: - offset must come after hover and before gestures for dragging to work! + .offset(y: yOffset) + +#if targetEnvironment(macCatalyst) // SwiftUI gesture handlers must come AFTER `.offset` .simultaneousGesture(gestureViewModel.macDragGesture) - #else +#else // SwiftUI gesture handlers must come AFTER `.offset` .onTapGesture { } // fixes long press + drag on iPad screen-touch // could also be a `.simultaneousGesture`? .gesture(gestureViewModel.longPressDragGesture) - #endif - .onChange(of: activeSwipeId) { _ in +#endif + + .onChange(of: sidebarViewModel.activeSwipeId) { gestureViewModel.resetSwipePosition() } - .onChange(of: isBeingEdited) { newValue in - gestureViewModel.editOn = newValue + .onChange(of: sidebarViewModel.isEditing) { gestureViewModel.resetSwipePosition() } - .onChange(of: activeGesture) { newValue in + .onChange(of: sidebarViewModel.activeGesture) { _, newValue in switch newValue { // scrolling or dragging resets swipe-menu case .scrolling, .dragging: @@ -107,27 +95,15 @@ struct SidebarListItemSwipeView: View { } } } - + // TODO: retrieve sidebar-width via a GeometryReader on whole sidebar rather than each individual item var customSwipeItem: some View { - GeometryReader { geometry in - SidebarListItemSwipeInnerView( - graph: graph, - item: item, - name: name, - layer: layer, - current: current, - proposedGroup: proposedGroup, - isClosed: isClosed, - selection: selection, - isBeingEdited: isBeingEdited, - swipeSetting: gestureViewModel.swipeSetting, - sidebarWidth: geometry.size.width, - isHovered: isHovered, - gestureViewModel: gestureViewModel) - .padding(1) // ensures .clipped doesn't cut off proposed-group border - .clipped() // ensures edit buttons don't animate outside sidebar - } + SidebarListItemSwipeInnerView( + graph: graph, + sidebarViewModel: sidebarViewModel, + itemViewModel: gestureViewModel) + .padding(1) // ensures .clipped doesn't cut off proposed-group border + .clipped() // ensures edit buttons don't animate outside sidebar } } diff --git a/Stitch/Graph/Sidebar/View/SidebarListView.swift b/Stitch/Graph/Sidebar/View/SidebarListView.swift index f6c4e7c49..337b75d82 100644 --- a/Stitch/Graph/Sidebar/View/SidebarListView.swift +++ b/Stitch/Graph/Sidebar/View/SidebarListView.swift @@ -22,111 +22,84 @@ let SIDEBAR_LIST_ITEM_ROW_COLORED_AREA_HEIGHT: CGFloat = 32.0 let SIDEBAR_LIST_ITEM_FONT: Font = stitchFont(18) #endif - struct SidebarListView: View { - + static let tabs = ["Layers", "Assets"] + @State private var currentTab = ProjectSidebarTab.layers.rawValue + @Bindable var graph: GraphState - - let isBeingEdited: Bool let syncStatus: iCloudSyncStatus - - @State var activeSwipeId: SidebarListItemId? - @State var activeGesture: SidebarListActiveGesture = .none - - // Animated state - @State var isBeingEditedAnimated = false - - var selections: SidebarSelectionState { - graph.sidebarSelectionState - } - var sidebarListState: SidebarListState { - graph.sidebarListState + var body: some View { + VStack { + // TODO: re-enable tabs for asset manager +// Picker("Sidebar Tabs", selection: self.$currentTab) { +// ForEach(Self.tabs, id: \.self) { tab in +//// HStack { +// // Image(systemName: tab.iconName) +// Text(tab) +// .width(200) +//// } +// } +// } +// .pickerStyle(.segmented) + + switch ProjectSidebarTab(rawValue: self.currentTab) { + case .none: + FatalErrorIfDebugView() + case .some(let tab): + switch tab { + case .layers: + SidebarListScrollView(graph: graph, + sidebarViewModel: graph.layersSidebarViewModel, + tab: tab, + syncStatus: syncStatus) + case .assets: + FatalErrorIfDebugView() + } + } + } } +} + +struct SidebarListScrollView: View where SidebarObservable: ProjectSidebarObservable { + @State private var isBeingEditedAnimated = false - var groups: SidebarGroupsDict { - graph.getSidebarGroupsDict() - } + @Bindable var graph: GraphState + @Bindable var sidebarViewModel: SidebarObservable + let tab: ProjectSidebarTab + let syncStatus: iCloudSyncStatus - var sidebarDeps: SidebarDeps { - SidebarDeps( - layerNodes: .fromLayerNodesDict( - nodes: graph.layerNodes, - orderedSidebarItems: graph.orderedSidebarLayers), - groups: groups, - expandedItems: graph.getSidebarExpandedItems()) - } - - var layerNodesForSidebarDict: LayerNodesForSidebarDict { - sidebarDeps.layerNodes - } - - var masterList: SidebarListItemsCoordinator { - sidebarListState.masterList + var isBeingEdited: Bool { + self.sidebarViewModel.isEditing } var body: some View { VStack(spacing: 0) { listView Spacer() - // Note: previously was in an `.overlay(footer, alignment: .bottom)` which now seems unnecessary - SidebarFooterView(groups: sidebarDeps.groups, - selections: selections, - isBeingEdited: isBeingEditedAnimated, - syncStatus: syncStatus, - layerNodes: layerNodesForSidebarDict) - } - // NOTE: only listen for changes to expandedItems or sidebar-groups, - // not the layerNodes, since layerNodes change constantly - // when eg a Time Node is attached to a Text Layer. - .onChange(of: sidebarDeps.expandedItems, perform: { _ in - activeSwipeId = nil - }) - .onChange(of: sidebarDeps.groups, perform: { _ in - activeSwipeId = nil - }) - // TODO: see note in `DeriveSidebarList` - .onChange(of: graph.nodes.keys.count) { _, _ in - dispatch(DeriveSidebarList()) + SidebarFooterView(sidebarViewModel: sidebarViewModel, + syncStatus: syncStatus) } } - + // Note: sidebar-list-items is a flat list; // indentation is handled by calculated indentations. @MainActor var listView: some View { - - let current: SidebarDraggedItem? = sidebarListState.current - + let allFlattenedItems = self.sidebarViewModel.getVisualFlattenedList() + return ScrollView(.vertical) { - // use .topLeading ? - ZStack(alignment: .leading) { - + ZStack(alignment: .topLeading) { // HACK - if masterList.items.isEmpty { - fakeSidebarListItem + if allFlattenedItems.isEmpty { + Color.clear } - ForEach(masterList.items, id: \.id.value) { (item: SidebarListItem) in - - let selection = getSelectionStatus( - item.id.asLayerNodeId, - selections) - + ForEach(allFlattenedItems) { item in SidebarListItemSwipeView( - graph: $graph, - item: item, - name: graph.getNodeViewModel(item.id.asNodeId)?.getDisplayTitle() ?? item.layer.value, - layer: layerNodesForSidebarDict[item.id.asLayerNodeId]?.layer ?? .rectangle, - current: current, - proposedGroup: sidebarListState.proposedGroup, - isClosed: masterList.collapsedGroups.contains(item.id), - selection: selection, - isBeingEdited: isBeingEditedAnimated, - activeGesture: $activeGesture, - activeSwipeId: $activeSwipeId) - .zIndex(item.zIndex) // TODO: replace wi - .transition(.move(edge: .top).combined(with: .opacity)) + graph: graph, + sidebarViewModel: sidebarViewModel, + gestureViewModel: item) } // ForEach } // ZStack @@ -134,7 +107,7 @@ struct SidebarListView: View { // Need to specify the amount space (height) the sidebar items all-together need, // so that scroll view doesn't interfere with e.g. tap gestures on views deeper inside // (e.g. the tap gesture on the circle in edit-mode) - .frame(height: Double(CUSTOM_LIST_ITEM_VIEW_HEIGHT * masterList.items.count), + .frame(height: Double(CUSTOM_LIST_ITEM_VIEW_HEIGHT * allFlattenedItems.count), alignment: .top) // #if DEV_DEBUG @@ -154,40 +127,15 @@ struct SidebarListView: View { #if !targetEnvironment(macCatalyst) - .animation(.spring(), value: selections) + .toolbar { + SidebarEditButtonView(sidebarViewModel: self.sidebarViewModel) + } #endif // TODO: remove some of these animations ? - .animation(.spring(), value: isBeingEdited) - .animation(.spring(), value: sidebarListState.proposedGroup) - .animation(.spring(), value: sidebarDeps) - .animation(.easeIn, value: sidebarListState.masterList.items) - - .onChange(of: isBeingEdited) { newValue in + .animation(.spring(), value: isBeingEdited) + .onChange(of: isBeingEdited) { _, newValue in // This handler enables all animations isBeingEditedAnimated = newValue } - - } - - // HACK for proper width even when sidebar is empty - // TODO: revisit and re-organize UI to avoid this hack - @ViewBuilder @MainActor - var fakeSidebarListItem: some View { - - let item = SidebarListItem.fakeSidebarListItem - - SidebarListItemSwipeView( - graph: $graph, - item: item, - name: item.layer.value, - layer: .rectangle, - current: .none, - proposedGroup: .none, - isClosed: true, - selection: .none, - isBeingEdited: false, - activeGesture: $activeGesture, - activeSwipeId: $activeSwipeId) - .opacity(0) } } diff --git a/Stitch/Graph/Sidebar/View/iPadProjectSidebarView.swift b/Stitch/Graph/Sidebar/View/iPadProjectSidebarView.swift index be8bfe179..5717ebb84 100644 --- a/Stitch/Graph/Sidebar/View/iPadProjectSidebarView.swift +++ b/Stitch/Graph/Sidebar/View/iPadProjectSidebarView.swift @@ -28,15 +28,12 @@ struct StitchSidebarView: View { } struct ProjectSidebarView: View { - @State private var isEditing = false @Bindable var graph: GraphState let syncStatus: iCloudSyncStatus var body: some View { VStack(alignment: .leading, spacing: .zero) { - SidebarListView(graph: graph, - isBeingEdited: isEditing, syncStatus: syncStatus) //#if !targetEnvironment(macCatalyst) // .padding(.top) @@ -51,9 +48,6 @@ struct ProjectSidebarView: View { // iPad only #if !targetEnvironment(macCatalyst) .navigationTitle("Stitch") - .toolbar { - SidebarEditButtonView(isEditing: $isEditing) - } // Allows scrolled up content to be visible underneath other nav-stack icons; not ideal. // .toolbarBackground(.hidden, for: .automatic) @@ -62,26 +56,19 @@ struct ProjectSidebarView: View { .toolbarBackground(.visible, for: .automatic) .toolbarBackground(Color.WHITE_IN_LIGHT_MODE_BLACK_IN_DARK_MODE, for: .automatic) #endif - - .onChange(of: self.isEditing, initial: true) { _, newValue in - dispatch(SidebarEditModeToggled(isEditing: newValue)) - } } } -struct SidebarEditModeToggled: GraphEvent { - let isEditing: Bool - - func handle(state: GraphState) { +extension LayersSidebarViewModel { + func editModeToggled(to isEditing: Bool) { // Reset selection-state, but preserve inspector's focused layers - let inspectorFocusedLayers = state.sidebarSelectionState.inspectorFocusedLayers // Don't actually reset these? // state.sidebarSelectionState.resetEditModeSelections() - state.sidebarSelectionState.inspectorFocusedLayers = inspectorFocusedLayers +// self.inspectorFocusedLayers = inspectorFocusedLayers // Do not set until the end; otherwise selection-state resets loses the change. - state.sidebarSelectionState.isEditMode = isEditing +// self.selectionState.isEditMode = isEditing } } diff --git a/Stitch/Graph/Sidebar/ViewModel/LayersSidebarViewModel.swift b/Stitch/Graph/Sidebar/ViewModel/LayersSidebarViewModel.swift new file mode 100644 index 000000000..f8818f46e --- /dev/null +++ b/Stitch/Graph/Sidebar/ViewModel/LayersSidebarViewModel.swift @@ -0,0 +1,23 @@ +// +// LayersSidebarViewModel.swift +// Stitch +// +// Created by Elliot Boschwitz on 10/23/24. +// + +import SwiftUI + +@Observable +final class LayersSidebarViewModel: ProjectSidebarObservable { + typealias EncodedItemData = SidebarLayerData + + var isEditing = false + var items: [SidebarItemGestureViewModel] = [] + var selectionState = SidebarSelectionObserver() + var activeSwipeId: NodeId? + var activeGesture: SidebarListActiveGesture = .none + var implicitlyDragged = NodeIdSet() + var currentItemDragged: NodeId? + + weak var graphDelegate: GraphState? +} diff --git a/Stitch/Graph/Sidebar/ViewModel/ProjectSidebarObservable.swift b/Stitch/Graph/Sidebar/ViewModel/ProjectSidebarObservable.swift new file mode 100644 index 000000000..13bc3272e --- /dev/null +++ b/Stitch/Graph/Sidebar/ViewModel/ProjectSidebarObservable.swift @@ -0,0 +1,123 @@ +// +// ProjectSidebarObservable.swift +// Stitch +// +// Created by Elliot Boschwitz on 10/23/24. +// + +import SwiftUI +import StitchViewKit + +protocol ProjectSidebarObservable: AnyObject, Observable where ItemViewModel.ID == EncodedItemData.ID, + Self.ItemViewModel.SidebarViewModel == Self { + associatedtype ItemViewModel: SidebarItemSwipable + associatedtype EncodedItemData: StitchNestedListElement + + typealias ItemID = ItemViewModel.ID + typealias SidebarSelectionState = SidebarSelectionObserver + typealias ExcludedGroups = [ItemID: [ItemViewModel]] + + var isEditing: Bool { get set } + + var items: [ItemViewModel] { get set } + + var selectionState: SidebarSelectionState { get set } + + var activeSwipeId: ItemID? { get set } + + var activeGesture: SidebarListActiveGesture { get set } + + + var currentItemDragged: Self.ItemID? { get set } + + var graphDelegate: GraphState? { get set } + + func canBeGrouped() -> Bool + + func canUngroup() -> Bool + + func sidebarGroupCreated() + + @MainActor + func sidebarGroupUncreatedViaEditMode(groupId: Self.ItemID, children: [Self.ItemID]) + + func didItemsDelete(ids: Set) +} + +extension ProjectSidebarObservable { + var proposedGroup: Self.ItemViewModel? { + guard let currentItemDragged = self.currentItemDragged else { return nil } + return self.items.get(currentItemDragged)?.parentDelegate + } + + var inspectorFocusedLayers: InspectorFocusedData { + get { + self.selectionState.inspectorFocusedLayers + } + set(newValue) { + self.selectionState.inspectorFocusedLayers = newValue + } + } + + func initializeDelegate(graph: GraphState) { + self.graphDelegate = graph + + self.items.recursiveForEach { + $0.sidebarDelegate = self + } + } + + @MainActor func persistSidebarChanges(encodedData: [Self.EncodedItemData]? = nil) { + // Create new encodable data + let encodedData: [Self.EncodedItemData] = encodedData ?? self.createdOrderedEncodedData() + + // Refreshes view + self.update(from: encodedData) + + self.graphDelegate?.encodeProjectInBackground() + } + + @MainActor func createdOrderedEncodedData() -> [Self.EncodedItemData] { + self.items.map { item in + item.createSchema() + } + } + + func update(from encodedData: [Self.EncodedItemData]) { + self.sync(from: encodedData) + } + + func sync(from encodedData: [Self.EncodedItemData]) { + let existingViewModels = self.items.reduce(into: [Self.ItemID : Self.ItemViewModel]()) { result, viewModel in + result.updateValue(viewModel, forKey: viewModel.id) + } + + self.items = self.recursiveSync(elements: encodedData, + existingViewModels: existingViewModels) + self.items.updateSidebarIndices() + } + + func recursiveSync(elements: [Self.EncodedItemData], + existingViewModels: [Self.ItemID : Self.ItemViewModel], + parent: Self.ItemViewModel? = nil) -> [Self.ItemViewModel] { + elements.map { element in + let viewModel = existingViewModels[element.id] ?? .init(data: element, + parentDelegate: parent, + sidebarViewModel: self) + + viewModel.update(from: element) + + guard let children = element.children else { + viewModel.children = nil + viewModel.isExpandedInSidebar = nil + return viewModel + } + + let childrenViewModels = self.recursiveSync(elements: children, + existingViewModels: existingViewModels, + parent: viewModel) + viewModel.children = childrenViewModels + return viewModel + } + } +} diff --git a/Stitch/Graph/Sidebar/ViewModel/SidebarItemGestureViewModel.swift b/Stitch/Graph/Sidebar/ViewModel/SidebarItemGestureViewModel.swift index 5d09fa047..3ed420264 100644 --- a/Stitch/Graph/Sidebar/ViewModel/SidebarItemGestureViewModel.swift +++ b/Stitch/Graph/Sidebar/ViewModel/SidebarItemGestureViewModel.swift @@ -6,6 +6,7 @@ // import SwiftUI +import StitchViewKit // MARK: SIDEBAR ITEM SWIPE CONSTANTS @@ -23,190 +24,228 @@ let DEFAULT_ACTION_THRESHOLD: CGFloat = SIDEBAR_WIDTH * 0.75 let GREY_SWIPE_MENU_OPTION_COLOR: Color = Color(.greySwipMenuOption) -final class SidebarItemGestureViewModel: ObservableObject { - let item: SidebarListItem +let CUSTOM_LIST_ITEM_VIEW_HEIGHT: Int = Int(SIDEBAR_LIST_ITEM_ROW_COLORED_AREA_HEIGHT) +let CUSTOM_LIST_ITEM_INDENTATION_LEVEL: Int = 24 +@Observable +final class SidebarItemGestureViewModel: SidebarItemSwipable { + var sidebarIndex: SidebarIndex = .init(groupIndex: .zero, rowIndex: .zero) + var id: NodeId + var children: [SidebarItemGestureViewModel]? + + var isExpandedInSidebar: Bool? + + var dragPosition: CGPoint? + var prevDragPosition: CGPoint? + // published property to be read in view - @Published var swipeSetting: SidebarSwipeSetting = .closed + var swipeSetting: SidebarSwipeSetting = .closed - private var previousSwipeX: CGFloat = 0 - @Binding var activeGesture: SidebarListActiveGesture { - didSet { - switch activeGesture { - // scrolling or dragging resets swipe-menu - case .scrolling, .dragging: - resetSwipePosition() - default: - return - } + internal var previousSwipeX: CGFloat = 0 + + weak var sidebarDelegate: LayersSidebarViewModel? + weak var parentDelegate: SidebarItemGestureViewModel? + + init(data: SidebarLayerData, + parentDelegate: SidebarItemGestureViewModel?, + sidebarViewModel: LayersSidebarViewModel) { + self.id = data.id + self.isExpandedInSidebar = data.isExpandedInSidebar + self.parentDelegate = parentDelegate + self.sidebarDelegate = sidebarViewModel + + self.children = data.children?.map { + SidebarItemGestureViewModel(data: $0, + parentDelegate: self, + sidebarViewModel: sidebarViewModel) } } - - // Tracks if the edit menu is open - var editOn: Bool = false - @Binding var activeSwipeId: SidebarListItemId? - - init(item: SidebarListItem, - activeGesture: Binding, - activeSwipeId: Binding) { - self.item = item - self._activeGesture = activeGesture - self._activeSwipeId = activeSwipeId + + init(id: NodeViewModel.ID, + children: [SidebarItemGestureViewModel]?, + isExpandedInSidebar: Bool?) { + self.id = id + self.children = children + self.isExpandedInSidebar = isExpandedInSidebar } +} - // MARK: GESTURE HANDLERS - - @MainActor - var onItemDragChanged: OnItemDragChangedHandler { - return { (translation: CGSize) in - // print("SidebarItemGestureViewModel: itemDragChangedGesture called") - self.activeGesture = .dragging(self.item.id) - dispatch(SidebarListItemDragged( - itemId: self.item.id, - translation: translation)) +extension SidebarItemGestureViewModel { + static func createId() -> NodeViewModel.ID { + .init() + } + + func createSchema() -> SidebarLayerData { + .init(id: self.id, + children: self.children?.map { $0.createSchema() }, + isExpandedInSidebar: self.isExpandedInSidebar) + } + + func update(from schema: EncodedItemData) { + self.id = schema.id + self.isExpandedInSidebar = isExpandedInSidebar + } + + @MainActor var name: String { + guard let node = self.graphDelegate?.getNodeViewModel(self.id) else { +// fatalErrorIfDebug() + return "" } + + return node.getDisplayTitle() } - - @MainActor - var onItemDragEnded: OnDragEndedHandler { - return { - // print("SidebarItemGestureViewModel: itemDragEndedGesture called") - if self.activeGesture == .none { - // print("SidebarItemGestureViewModel: onItemDragEnded: no active gesture, so will do nothing") - self.activeGesture = .none - } else { - self.activeGesture = .none - dispatch(SidebarListItemDragEnded(itemId: self.item.id)) - } + + @MainActor var isVisible: Bool { + guard let node = self.graphDelegate?.getLayerNode(id: self.id)?.layerNode else { +// fatalErrorIfDebug() + return true } + + return node.hasSidebarVisibility } - + @MainActor - var macDragGesture: DragGestureTypeSignature { - - // print("SidebarItemGestureViewModel: macDragGesture: called") - -// let itemDrag = DragGesture(minimumDistance: 0) - // Use a tiny min-distance so that we can distinguish between a tap vs a drag - let itemDrag = DragGesture(minimumDistance: 5) - .onChanged { value in - // print("SidebarItemGestureViewModel: macDragGesture: itemDrag onChanged") - self.onItemDragChanged(value.translation) - }.onEnded { _ in - // print("SidebarItemGestureViewModel: macDragGesture: itemDrag onEnded") - self.onItemDragEnded() - } - - return itemDrag + func didLabelEdit(to newString: String, + isCommitting: Bool) { + // Treat this is as a "layer inspector edit" ? + dispatch(NodeTitleEdited(titleEditType: .layerInspector(self.id), + edit: newString, + isCommitting: isCommitting)) + } + + func sidebarLayerHovered(itemId: SidebarListItemId) { + self.graphDelegate?.graphUI.sidebarLayerHovered(layerId: itemId.asLayerNodeId) + } + + func sidebarLayerHoverEnded(itemId: SidebarListItemId) { + self.graphDelegate?.graphUI.sidebarLayerHoverEnded(layerId: itemId.asLayerNodeId) } @MainActor - var longPressDragGesture: LongPressAndDragGestureType { - - let longPress = LongPressGesture(minimumDuration: 0.5).onEnded { _ in - print("SidebarItemGestureViewModel: longPressDragGesture: longPress onChanged") - self.activeGesture = .dragging(self.item.id) - dispatch(SidebarListItemLongPressed(id: self.item.id)) + func didDeleteItem() { + self.graphDelegate?.sidebarItemDeleted(itemId: self.id) + } + + @MainActor + func didToggleVisibility() { + dispatch(SidebarItemHiddenStatusToggled(clickedId: self.id)) + } + + @MainActor + func didSelectOnEditMode() { + dispatch(SidebarItemSelected(id: self.id.asLayerNodeId)) + } + + @MainActor + func didUnselectOnEditMode() { + dispatch(SidebarItemDeselected(id: self.id)) + } + + var isNonEditModeFocused: Bool { + guard let sidebar = self.sidebarDelegate else { return false } + return sidebar.inspectorFocusedLayers.focused.contains(self.id) + } + + var isNonEditModeActivelySelected: Bool { + guard let sidebar = self.sidebarDelegate else { return false } + return sidebar.inspectorFocusedLayers.activelySelected.contains(self.id) + } + + var isNonEditModeSelected: Bool { + isNonEditModeFocused || isNonEditModeActivelySelected + } + + var backgroundOpacity: CGFloat { + if isImplicitlyDragged { + return 0.5 + } else if (isNonEditModeFocused || isBeingDragged) { + return (isNonEditModeFocused && !isNonEditModeActivelySelected) ? 0.5 : 1 + } else { + return 0 } - - // TODO: Does `minimumDistance` matter? -// let itemDrag = DragGesture(minimumDistance: 0) - let itemDrag = DragGesture(minimumDistance: 5) - .onChanged { value in - print("SidebarItemGestureViewModel: longPressDragGesture: itemDrag onChanged") - self.onItemDragChanged(value.translation) - }.onEnded { _ in - print("SidebarItemGestureViewModel: longPressDragGesture: itemDrag onEnded") - self.onItemDragEnded() - } - - return longPress.sequenced(before: itemDrag) } - - var onItemSwipeChanged: OnDragChangedHandler { + + var useHalfOpacityBackground: Bool { + isImplicitlyDragged || (isNonEditModeFocused && !isNonEditModeActivelySelected) + } + + @MainActor + var isHidden: Bool { + self.graphDelegate?.getVisibilityStatus(for: self.id) != .visible + } + + @MainActor + var fontColor: Color { + guard let selection = self.sidebarDelegate?.selectionState.getSelectionStatus(self.id) else { return .white } - let onSwipeChanged: OnDragChangedHandler = { (translationWidth: CGFloat) in - if self.editOn { - // print("SidebarItemGestureViewModel: itemSwipeChangedGesture: currently in edit mode, so cannot swipe") - return - } - -#if targetEnvironment(macCatalyst) - return +#if DEV_DEBUG + if isHidden { + return .purple + } +#endif + + // Any 'focused' (doesn't have to be 'actively selected') layer uses white text + if isNonEditModeSelected { +#if DEV_DEBUG + return .red +#else + return .white #endif - - // if we have no active gesture, - // and we met the swipe threshold, - // then we can begin swiping - if self.activeGesture.isNone - && translationWidth.magnitude > SIDEBAR_ACTIVE_GESTURE_SWIPE_THRESHOLD { - // print("SidebarItemGestureViewModel: itemSwipeChangedGesture: setting us to swipe") - self.activeGesture = .swiping - } - if self.activeGesture.isSwipe { - // print("SidebarItemGestureViewModel: itemSwipeChangedGesture: updating per swipe") - // never let us drag the list eastward beyond its frame - let newSwipeX = max(self.previousSwipeX - translationWidth, 0) - self.swipeSetting = .swiping(newSwipeX) - - self.activeSwipeId = self.item.id - } } - - return onSwipeChanged - } - - // not redefined when a passed in redux value changes? - // unless we make a function? - @MainActor - var onItemSwipeEnded: OnDragEndedHandler { - let onSwipeEnded: OnDragEndedHandler = { - // print("SidebarItemGestureViewModel: itemSwipeEndedGesture called") - - if self.editOn { - // print("SidebarItemGestureViewModel: itemSwipeEndedGesture: currently in edit mode, so cannot swipe") - return - } - -#if targetEnvironment(macCatalyst) - return +#if DEV_DEBUG + // Easier to see secondary selections for debug + // return selection.color(isHidden) + + switch selection { + case .primary: + return .brown + case .secondary: + return .green + case .none: + return .blue + } + #endif - - // if we had been swiping, then we reset activeGesture - if self.activeGesture.isSwipe { - // print("SidebarItemGestureViewModel: itemSwipeEndedGesture onEnded: resetting swipe") - self.activeGesture = .none - if self.atDefaultActionThreshold { - // Don't need to change x position here, - // since redOption's offset handles that. - dispatch(SidebarItemDeleted(itemId: self.item.id)) - } else if self.hasCrossedRestingThreshold { - self.swipeSetting = .open - } - // we didn't pull it out far enough -- set x = 0 - else { - self.swipeSetting = .closed - } - self.previousSwipeX = self.swipeSetting.distance - self.activeSwipeId = self.item.id - } // if active... + + if isBeingEdited || isHidden { + return selection.color(isHidden) + } else { + // i.e. if we are not in edit mode, do NOT show secondarily-selected layers (i.e. children of a primarily-selected parent) as gray + return SIDE_BAR_OPTIONS_TITLE_FONT_COLOR } - return onSwipeEnded - } - - // MARK: SWIPE LOGIC - - func resetSwipePosition() { - swipeSetting = .closed - previousSwipeX = 0 } - - var atDefaultActionThreshold: Bool { - swipeSetting.distance >= DEFAULT_ACTION_THRESHOLD + + // TODO: should we only show the arrow icon when we have a sidebar layer immediately above? + @MainActor + var masks: Bool { + guard let graph = self.graphDelegate else { return false } + + // TODO: why is this not animated? and why does it jitter? +// // index of this layer +// guard let index = graph.sidebarListState.masterList.items +// .firstIndex(where: { $0.id.asLayerNodeId == nodeId }) else { +// return withAnimation { false } +// } +// +// // hasSidebarLayerImmediatelyAbove +// guard graph.sidebarListState.masterList.items[safe: index - 1].isDefined else { +// return withAnimation { false } +// } +// + let atleastOneIndexMasks = graph + .getLayerNode(id: self.id)? + .layerNode?.masksPort.allLoopedValues + .contains(where: { $0.getBool ?? false }) + ?? false + + return withAnimation { + atleastOneIndexMasks + } } - - var hasCrossedRestingThreshold: Bool { - swipeSetting.distance >= RESTING_THRESHOLD + + @MainActor + func sidebarItemDeleted(itemId: SidebarListItemId) { + self.graphDelegate?.sidebarItemDeleted(itemId: itemId) } } diff --git a/Stitch/Graph/Sidebar/ViewModel/SidebarItemSwipable.swift b/Stitch/Graph/Sidebar/ViewModel/SidebarItemSwipable.swift new file mode 100644 index 000000000..72824ad35 --- /dev/null +++ b/Stitch/Graph/Sidebar/ViewModel/SidebarItemSwipable.swift @@ -0,0 +1,623 @@ +// +// SidebarItemSwipable.swift +// Stitch +// +// Created by Elliot Boschwitz on 10/23/24. +// + +import SwiftUI +import StitchViewKit + +protocol SidebarItemSwipable: AnyObject, Observable, Identifiable, StitchNestedListElement where Self.ID: Equatable & CustomStringConvertible, + SidebarViewModel.ItemViewModel == Self { + associatedtype SidebarViewModel: ProjectSidebarObservable + typealias ActiveGesture = SidebarListActiveGesture + typealias EncodedItemData = SidebarViewModel.EncodedItemData + + var children: [Self]? { get set } + + var parentDelegate: Self? { get set } + + @MainActor var name: String { get } + + var swipeSetting: SidebarSwipeSetting { get set } + + var previousSwipeX: CGFloat { get set } + + @MainActor var isVisible: Bool { get } + + var sidebarIndex: SidebarIndex { get set } + + var dragPosition: CGPoint? { get set } + + var prevDragPosition: CGPoint? { get set } + + var isExpandedInSidebar: Bool? { get set } + + var sidebarDelegate: SidebarViewModel? { get set } + + @MainActor var fontColor: Color { get } + + var backgroundOpacity: CGFloat { get } + + @MainActor var sidebarLeftSideIcon: String { get } + + @MainActor var isMasking: Bool { get } + + init(data: Self.EncodedItemData, + parentDelegate: Self?, + sidebarViewModel: Self.SidebarViewModel) + + @MainActor + func sidebarItemDeleted(itemId: Self.ID) + + @MainActor + func contextMenuInteraction(itemId: Self.ID, + graph: GraphState, + keyboardObserver: KeyboardObserver) -> UIContextMenuConfiguration? + + @MainActor + func sidebarLayerHovered(itemId: Self.ID) + + @MainActor + func sidebarLayerHoverEnded(itemId: Self.ID) + + @MainActor + func didSelectOnEditMode() + + @MainActor + func didUnselectOnEditMode() + + @MainActor + func didDeleteItem() + + @MainActor + func didToggleVisibility() + + @MainActor + func didLabelEdit(to newString: String, isCommitting: Bool) + + @MainActor + func createSchema() -> SidebarViewModel.EncodedItemData + + func update(from schema: Self.EncodedItemData) +} + +extension SidebarItemSwipable { + var zIndex: Double { + if self.isBeingDragged { + return SIDEBAR_ITEM_MAX_Z_INDEX + } + + return 0 + } + + @MainActor + var isGroup: Bool { + self.children.isDefined + } + + @MainActor + func supportedGroupRangeOnDrag(beforeElement: Self?, + afterElement: Self?) -> Range { + guard let beforeElement = beforeElement else { + return 0..<1 + } + + let beforeGroupIndex = beforeElement.sidebarIndex.groupIndex + guard let afterElement = afterElement else { + // Allow any nesting at end of list + return 0.. { + get { + self.sidebarDelegate?.activeGesture ?? .none + } + set(newValue) { + self.sidebarDelegate?.activeGesture = newValue + } + } + + var activeSwipeId: Self.ID? { + get { + self.sidebarDelegate?.activeSwipeId ?? nil + } + set(newValue) { + self.sidebarDelegate?.activeSwipeId = newValue + } + } + + var isBeingEdited: Bool { + self.sidebarDelegate?.isEditing ?? false + } + + @MainActor + var location: CGPoint { + let index = self.sidebarIndex + return .init(x: CUSTOM_LIST_ITEM_INDENTATION_LEVEL * index.groupIndex, + y: Self.inferLocationY(from: index.rowIndex)) + } + + static func inferLocationY(from rowIndex: Int) -> Int { + CUSTOM_LIST_ITEM_VIEW_HEIGHT * rowIndex + } + + var isImplicitlyDragged: Bool { + var visitedItem: Self? = self + + while let parent = visitedItem?.parentDelegate { + if parent.isBeingDragged { return true } + visitedItem = parent + } + + return false + } + + var isBeingDragged: Bool { + self.dragPosition != nil + } + + var isCollapsedGroup: Bool { + !(self.isExpandedInSidebar ?? true) + } + + // MARK: GESTURE HANDLERS + + @MainActor + var onItemDragChanged: OnItemDragChangedHandler { + return { (translation: CGSize) in + + if self.activeGesture != .dragging(self.id) { +// log("SidebarItemGestureViewModel: itemDragChangedGesture called on \(self.id.description)") + self.activeGesture = .dragging(self.id) + } + + // Needs to be dispatched due to simultaneous access issues with view + Task { @MainActor [weak self] in + guard let item = self else { return } + + item.sidebarDelegate?.sidebarListItemDragged( + item: item, + translation: translation) + } + } + } + + @MainActor + var onItemDragEnded: OnDragEndedHandler { + return { + guard self.activeGesture != .none else { return } + + if self.activeGesture != .none { + self.activeGesture = .none + } + + // Task here resolves a race condition with onItemDragChanged which also uses a Task + Task { @MainActor [weak self] in + self?.sidebarDelegate?.sidebarListItemDragEnded() + } + } + } + + @MainActor + var macDragGesture: DragGestureTypeSignature { + + // print("SidebarItemGestureViewModel: macDragGesture: called") + +// let itemDrag = DragGesture(minimumDistance: 0) + // Use a tiny min-distance so that we can distinguish between a tap vs a drag + // 15 pixels is enough to prevent a slight stutter that can exist + let itemDrag = DragGesture(minimumDistance: 15) + .onChanged { value in + // print("SidebarItemGestureViewModel: macDragGesture: itemDrag onChanged") + self.onItemDragChanged(value.translation) + }.onEnded { _ in + // print("SidebarItemGestureViewModel: macDragGesture: itemDrag onEnded") + self.onItemDragEnded() + } + + return itemDrag + } + + @MainActor + var longPressDragGesture: LongPressAndDragGestureType { + + let longPress = LongPressGesture(minimumDuration: 0.5).onEnded { _ in + if self.activeGesture != .dragging(self.id) { + log("SidebarItemGestureViewModel: longPressDragGesture: longPress onChanged") + self.activeGesture = .dragging(self.id) + } + + self.sidebarDelegate?.sidebarListItemLongPressed(itemId: self.id) + } + + // TODO: Does `minimumDistance` matter? +// let itemDrag = DragGesture(minimumDistance: 0) + let itemDrag = DragGesture(minimumDistance: 5) + .onChanged { value in +// print("SidebarItemGestureViewModel: longPressDragGesture: itemDrag onChanged") + self.onItemDragChanged(value.translation) + }.onEnded { _ in +// print("SidebarItemGestureViewModel: longPressDragGesture: itemDrag onEnded") + self.onItemDragEnded() + } + + return longPress.sequenced(before: itemDrag) + } + + var onItemSwipeChanged: OnDragChangedHandler { + let onSwipeChanged: OnDragChangedHandler = { (translationWidth: CGFloat) in + if self.isBeingEdited { + // print("SidebarItemGestureViewModel: itemSwipeChangedGesture: currently in edit mode, so cannot swipe") + return + } + +#if targetEnvironment(macCatalyst) + return +#endif + + // if we have no active gesture, + // and we met the swipe threshold, + // then we can begin swiping + if self.activeGesture.isNone + && translationWidth.magnitude > SIDEBAR_ACTIVE_GESTURE_SWIPE_THRESHOLD { + // print("SidebarItemGestureViewModel: itemSwipeChangedGesture: setting us to swipe") + if self.activeGesture != .swiping { + self.activeGesture = .swiping + } + } + if self.activeGesture.isSwipe { + // print("SidebarItemGestureViewModel: itemSwipeChangedGesture: updating per swipe") + // never let us drag the list eastward beyond its frame + let newSwipeX = max(self.previousSwipeX - translationWidth, 0) + self.swipeSetting = .swiping(newSwipeX) + + if self.activeSwipeId != self.id { + self.activeSwipeId = self.id + } + } + } + + return onSwipeChanged + } + + // not redefined when a passed in redux value changes? + // unless we make a function? + @MainActor + var onItemSwipeEnded: OnDragEndedHandler { + let onSwipeEnded: OnDragEndedHandler = { + // print("SidebarItemGestureViewModel: itemSwipeEndedGesture called") + + if self.isBeingEdited { + // print("SidebarItemGestureViewModel: itemSwipeEndedGesture: currently in edit mode, so cannot swipe") + return + } + +#if targetEnvironment(macCatalyst) + return +#endif + + // if we had been swiping, then we reset activeGesture + if self.activeGesture.isSwipe { + // print("SidebarItemGestureViewModel: itemSwipeEndedGesture onEnded: resetting swipe") + + if self.activeGesture != .none { + self.activeGesture = .none + } + + if self.atDefaultActionThreshold { + // Don't need to change x position here, + // since redOption's offset handles that. + self.sidebarItemDeleted(itemId: self.id) + } else if self.hasCrossedRestingThreshold { + self.swipeSetting = .open + } + // we didn't pull it out far enough -- set x = 0 + else { + self.swipeSetting = .closed + } + self.previousSwipeX = self.swipeSetting.distance + self.activeSwipeId = self.id + } // if active... + } + return onSwipeEnded + } + + // MARK: SWIPE LOGIC + + func resetSwipePosition() { + swipeSetting = .closed + previousSwipeX = 0 + } + + var atDefaultActionThreshold: Bool { + swipeSetting.distance >= DEFAULT_ACTION_THRESHOLD + } + + var hasCrossedRestingThreshold: Bool { + swipeSetting.distance >= RESTING_THRESHOLD + } + + var graphDelegate: GraphState? { + self.sidebarDelegate?.graphDelegate + } + + var parentId: Self.ID? { + self.parentDelegate?.id + } + + var rowIndex: Int { + guard let sidebar = self.sidebarDelegate else { + fatalErrorIfDebug() + return -1 + } + + let flattenedItems = sidebar.items.flattenedItems + guard let index = flattenedItems.enumerated().first(where: { $0.1.id == self.id })?.0 else { + fatalErrorIfDebug() + return -1 + } + + return index + } +} + +extension Array where Element: SidebarItemSwipable { + var flattenedItems: [Element] { + self.flatMap { item in + var items = [item] + items += item.children?.flattenedItems ?? [] + return items + } + } + + /// Same operation as `flattenedItems` but filters out collapsed groups. + var flattenedVisibleItems: [Element] { + self.flatMap { item in + guard item.isExpandedInSidebar ?? false else { return [item] } + + var items = [item] + items += item.children?.flattenedItems ?? [] + return items + } + } + + func updateSidebarIndices() { + var currentRowIndex = 0 + return self.updateSidebarIndices(currentGroupIndex: 0, + currentRowIndex: ¤tRowIndex) + } + + private func updateSidebarIndices(currentGroupIndex: Int, + currentRowIndex: inout Int, + parent: Element? = nil) { + for item in self { + let newIndex = SidebarIndex(groupIndex: currentGroupIndex, + rowIndex: currentRowIndex) + + // Saves render cycles + if newIndex != item.sidebarIndex { + item.sidebarIndex = newIndex + } + + if item.parentDelegate?.id != parent?.id { + item.parentDelegate = parent + } + + currentRowIndex += 1 + + if let children = item.children, + item.isExpandedInSidebar ?? false { + children + .updateSidebarIndices(currentGroupIndex: currentGroupIndex + 1, + currentRowIndex: ¤tRowIndex, + parent: item) + } + } + } + + /// Helper that recursively travels nested data structure. + func recursiveForEach(_ callback: @escaping (Element) -> ()) { + self.forEach { item in + callback(item) + + item.children?.recursiveForEach(callback) + } + } + + /// Helper that recursively travels nested data structure in DFS traversal (aka children first). + func recursiveCompactMap(_ callback: @escaping (Element) -> Element?) -> [Element] { + self.compactMap { item in + item.children = item.children?.recursiveCompactMap(callback) + + return callback(item) + } + } + + /// Filters out collapsed groups. + /// List mut be flattened for drag gestures. + func getVisualFlattenedList() -> [Element] { + self.flatMap { item in + if let children = item.children, + item.isExpandedInSidebar ?? false { + return [item] + children.getVisualFlattenedList() + } + + return [item] + } + } + +// /// Helper that recursively travels nested data structure. +// func recursiveMap(_ callback: @escaping (Element) -> T) -> [T] { +// self.map { item in +// let newItem = callback(item) +// item.children = item.children?.map(callback) +// return newItem +// } +// } + + @MainActor + mutating private func insertDraggedElements(_ elements: [Element], + at index: Int, + shouldPlaceAfter: Bool = true) { + let insertOffset = shouldPlaceAfter ? 1 : 0 + + // Logic we want is to insert after the desired element, hence + 1 + self.insert(contentsOf: elements, at: index + insertOffset) + } + + /// Recursive function that traverses nested array until index == 0. + @MainActor + func movedDraggedItems(_ draggedItems: [Element], + at dragResult: SidebarDragDestination, + dragPositionIndex: SidebarIndex) -> [Element] { + guard let element = dragResult.element else { + var newList = self + newList.insertDraggedElements(draggedItems, + at: 0, + shouldPlaceAfter: false) + return newList + } + + guard let indexAtHierarchy = self.firstIndex(where: { $0.id == element.id }) else { + // Recurse children until element found + return self.map { item in + item.children = item.children?.movedDraggedItems(draggedItems, + at: dragResult, + dragPositionIndex: dragPositionIndex) + return item + } + } + + var newList = self + + switch dragResult { + case .afterElement: + newList.insertDraggedElements(draggedItems, + at: indexAtHierarchy, + shouldPlaceAfter: true) + return newList + + case .topOfGroup: + assertInDebug(element.isGroup) + guard var children = element.children else { + fatalErrorIfDebug() + return self + } + + children.insertDraggedElements(draggedItems, + at: 0, + shouldPlaceAfter: false) + element.children = children + newList[indexAtHierarchy] = element + + return newList + } + } + + /// Given some made-up location, finds the closest element in a nested sidebar list. Used for item dragging. + /// Rules: + /// * Must match the group index + /// * Must ponit to group layer if otherwise top of list + /// * Recommended element cannot reside "below" the requested row index. + /// Note: the enum result type determines if an element is placed either after some other element or into the list of a group. + /// The enum is needed because there's no way to insert the element at the top of a list when the default rule is placing an element after. + @MainActor + func findClosestElement(draggedElement: Element, + to indexOfDraggedLocation: SidebarIndex, + numItemsDragged: Int) -> SidebarDragDestination { + let beforeElement = self[safe: indexOfDraggedLocation.rowIndex - 1] + let afterElement = self[safe: indexOfDraggedLocation.rowIndex] + + let supportedGroupRanges = draggedElement + .supportedGroupRangeOnDrag(beforeElement: beforeElement, + afterElement: afterElement) + + // Filters for: + // 1. Row indices smaller than index--we want all because we could append after a group which is higher up the stack. + // 2. Rows with allowed groups--which are constrained by the index's above and below element. + let flattenedItems = self[0..= indexOfDraggedLocation.rowIndex ? numItemsDragged - 1 : 0 + rhsRowIndex -= rhsRowIndex >= indexOfDraggedLocation.rowIndex ? numItemsDragged - 1 : 0 + + let lhsGroupIndexDiff = abs(indexOfDraggedLocation.groupIndex - lhs.sidebarIndex.groupIndex) + let lhsRowIndexDiff = abs(indexOfDraggedLocation.rowIndex - lhsRowIndex) + + let rhsGroupIndexDiff = abs(indexOfDraggedLocation.groupIndex - rhs.sidebarIndex.groupIndex) + let rhsRowIndexDiff = abs(indexOfDraggedLocation.rowIndex - rhsRowIndex) + + // Equal groups + if lhsGroupIndexDiff == rhsGroupIndexDiff { + return lhsRowIndexDiff < rhsRowIndexDiff + } + + return lhsGroupIndexDiff < rhsGroupIndexDiff + } + +#if DEV_DEBUG + log("before: \(beforeElement?.id.debugFriendlyId ?? "none")\tafter: \(afterElement?.id.debugFriendlyId ?? "none")") + log("supported group ranges: \(supportedGroupRanges)") + log("recommendation test for \(indexOfDraggedLocation):") + rankedItems.forEach { print("\($0.id.debugFriendlyId), \($0.sidebarIndex), diff: \(abs(indexOfDraggedLocation.rowIndex - $0.sidebarIndex.rowIndex))") } +#endif + + // Covers top of list and many top of group scenarios + guard let recommendedItem = rankedItems.first else { + return .topOfGroup(beforeElement) + } + + // Check if element is dragged more right-ward for placement into group + if let beforeElement = beforeElement { + // Horizontal drag is east of parent group + let isDraggedIntoChildHierarchy = indexOfDraggedLocation.groupIndex > recommendedItem.sidebarIndex.groupIndex + + // Horizontal drag permits west-ward movement to move to parent context + let allowsDraggingToParentHierarchy = supportedGroupRanges.contains(beforeElement.sidebarIndex.groupIndex) + + // User dragged into child list and that was allowed given below items + let wasValidDragIntoChildren = isDraggedIntoChildHierarchy || !allowsDraggingToParentHierarchy + + // Ensures above element is ane epxanded group + let isExpandedGroup = beforeElement.isGroup && !beforeElement.isCollapsedGroup + + let didMoveToTopOfGroup = isExpandedGroup && wasValidDragIntoChildren + if didMoveToTopOfGroup { + log("findClosestElement: dragged to top of group") + return .topOfGroup(beforeElement) + } + } + + // Default scenarios result in placing after some other element + return .afterElement(recommendedItem) + } +} diff --git a/Stitch/Graph/Util/GraphActions.swift b/Stitch/Graph/Util/GraphActions.swift index 0ec046224..0cb940025 100644 --- a/Stitch/Graph/Util/GraphActions.swift +++ b/Stitch/Graph/Util/GraphActions.swift @@ -71,14 +71,6 @@ extension GraphState: DocumentEncodableDelegate { result.updateValue(url, forKey: url.mediaKey) } } - - func updateSidebarListStateAfterStateChange() { - self.sidebarListState = getMasterListFrom( - layerNodes: self.visibleNodesViewModel.layerNodes, - // TODO: use real, persisted expanded sidebar items - expanded: self.getSidebarExpandedItems(), - orderedSidebarItems: self.orderedSidebarLayers) - } } struct PreviewWindowDimensionsSwapped: StitchDocumentEvent { diff --git a/Stitch/Graph/Util/NodeSelection/NodeDuplicationActions.swift b/Stitch/Graph/Util/NodeSelection/NodeDuplicationActions.swift index 1650e7b54..0666bf32d 100644 --- a/Stitch/Graph/Util/NodeSelection/NodeDuplicationActions.swift +++ b/Stitch/Graph/Util/NodeSelection/NodeDuplicationActions.swift @@ -34,7 +34,7 @@ extension StitchDocumentViewModel { let activelySelectedLayers = state.visibleGraph.sidebarSelectionState.inspectorFocusedLayers.activelySelected if !activelySelectedLayers.isEmpty { - state.visibleGraph.sidebarSelectedItemsDuplicatedViaEditMode() + state.visibleGraph.sidebarSelectedItemsDuplicated() } else { let copiedComponentResult = state.visibleGraph.createCopiedComponent( groupNodeFocused: state.graphUI.groupNodeFocused, @@ -185,11 +185,11 @@ extension GraphState { case .layer(let layerNode): // Actively-select the new layer node - let id = nodeEntity.id.asLayerNodeId + let id = nodeEntity.id self.sidebarSelectionState.inspectorFocusedLayers.focused.insert(id) self.sidebarSelectionState.inspectorFocusedLayers.activelySelected.insert(id) - self.sidebarItemSelectedViaEditMode( + self.layersSidebarViewModel.sidebarItemSelectedViaEditMode( id, // Can treat as always true? isSidebarItemTapped: true) @@ -228,7 +228,7 @@ extension GraphState { } // Also wipe sidebar selection state - self.sidebarSelectionState = .init() + self.sidebarSelectionState.resetEditModeSelections() } // Duplicate ONLY the selected comment boxes diff --git a/Stitch/Graph/Util/NodeSelection/NodeSelectionUtil.swift b/Stitch/Graph/Util/NodeSelection/NodeSelectionUtil.swift index a8cb6e459..e42b1aa75 100644 --- a/Stitch/Graph/Util/NodeSelection/NodeSelectionUtil.swift +++ b/Stitch/Graph/Util/NodeSelection/NodeSelectionUtil.swift @@ -96,10 +96,10 @@ struct SelectAllShortcutKeyPressed: GraphEvent { // Wipe the 'last selected item' state.sidebarSelectionState.inspectorFocusedLayers = .init() - let allLayers: LayerIdSet = state.orderedSidebarLayers.getFlattenedList().map(\.id.asLayerNodeId).toSet + let allLayers = state.orderedSidebarLayers.flattenedItems.map(\.id).toSet state.sidebarSelectionState.inspectorFocusedLayers = state.sidebarSelectionState.inspectorFocusedLayers.insert(allLayers) - state.editModeSelectTappedItems(tappedItems: state.sidebarSelectionState.inspectorFocusedLayers.focused) + state.layersSidebarViewModel.editModeSelectTappedItems(tappedItems: state.sidebarSelectionState.inspectorFocusedLayers.focused) } else { selectAllNodesAtTraversalLevel(state) diff --git a/Stitch/Graph/View/Gesture/GraphGestureView.swift b/Stitch/Graph/View/Gesture/GraphGestureView.swift index 03a9195c8..49002a007 100644 --- a/Stitch/Graph/View/Gesture/GraphGestureView.swift +++ b/Stitch/Graph/View/Gesture/GraphGestureView.swift @@ -66,7 +66,7 @@ struct GraphGestureView: UIViewControllerRepresentable { } } -class GraphGestureDelegate: NSObject, UIGestureRecognizerDelegate { +final class GraphGestureDelegate: NSObject, UIGestureRecognizerDelegate { static let zoomScrollRate = 0.04 weak var document: StitchDocumentViewModel? diff --git a/Stitch/Graph/ViewModel/GraphDelegate.swift b/Stitch/Graph/ViewModel/GraphDelegate.swift index 8a89381a2..9d1b467da 100644 --- a/Stitch/Graph/ViewModel/GraphDelegate.swift +++ b/Stitch/Graph/ViewModel/GraphDelegate.swift @@ -44,6 +44,8 @@ protocol GraphDelegate: AnyObject, Sendable { var scrollInteractionNodes: [LayerNodeId: NodeIdSet] { get set } var enabledCameraNodeIds: NodeIdSet { get set } + + var sidebarSelectionState: LayersSidebarViewModel.SidebarSelectionState { get } @MainActor var connections: GraphState.TopologicalData.Connections { get } @@ -69,7 +71,7 @@ protocol GraphDelegate: AnyObject, Sendable { @MainActor var multiselectInputs: LayerInputTypeSet? { get } - var sidebarSelectionState: SidebarSelectionState { get set } + var layersSidebarViewModel: LayersSidebarViewModel { get } var orderedSidebarLayers: OrderedSidebarLayers { get } diff --git a/Stitch/Graph/ViewModel/GraphState.swift b/Stitch/Graph/ViewModel/GraphState.swift index a4aa7c844..73f853916 100644 --- a/Stitch/Graph/ViewModel/GraphState.swift +++ b/Stitch/Graph/ViewModel/GraphState.swift @@ -22,20 +22,16 @@ final class GraphState: Sendable { let saveLocation: [UUID] - // TODO: wrap in a new data structure like `SidebarUIState` - var sidebarListState: SidebarListState = .init() - var sidebarSelectionState = SidebarSelectionState() - var id = UUID() var name: String = STITCH_PROJECT_DEFAULT_NAME var commentBoxesDict = CommentBoxesDict() - + let visibleNodesViewModel = VisibleNodesViewModel() let edgeDrawingObserver = EdgeDrawingObserver() - + var selectedEdges = Set() - + // Hackiness for handling edge case in our UI where somehow // UIKit node drag and SwiftUI port drag can happen at sometime. var nodeIsMoving = false @@ -45,9 +41,9 @@ final class GraphState: Sendable { var dragInteractionNodes = [LayerNodeId: NodeIdSet]() var pressInteractionNodes = [LayerNodeId: NodeIdSet]() var scrollInteractionNodes = [LayerNodeId: NodeIdSet]() - + // Ordered list of layers in sidebar - var orderedSidebarLayers: SidebarLayerList = [] + let layersSidebarViewModel: LayersSidebarViewModel // Cache of ordered list of preview layer view models; // updated in various scenarious, e.g. sidebar list item dragged @@ -64,10 +60,10 @@ final class GraphState: Sendable { // Tracks all created and imported components var components: [UUID: StitchMasterComponent] = [:] - + // Maps a MediaKey to some URL var mediaLibrary: MediaLibrary = [:] - + // Tracks nodes with camera enabled var enabledCameraNodeIds = NodeIdSet() @@ -78,7 +74,7 @@ final class GraphState: Sendable { var lastEncodedDocument: GraphEntity weak var documentDelegate: StitchDocumentViewModel? weak var documentEncoderDelegate: (any DocumentEncodable)? - + init(from schema: GraphEntity, nodes: NodesViewModelDict, components: MasterComponentsDict, @@ -88,12 +84,20 @@ final class GraphState: Sendable { self.saveLocation = saveLocation self.id = schema.id self.name = schema.name + self.layersSidebarViewModel = .init() self.commentBoxesDict.sync(from: schema.commentBoxes) self.components = components - self.orderedSidebarLayers = schema.orderedSidebarLayers self.visibleNodesViewModel.nodes = nodes self.syncMediaFiles(mediaFiles) + self.layersSidebarViewModel.sync(from: schema.orderedSidebarLayers) + } +} + +extension GraphState { + @MainActor + var orderedSidebarLayers: SidebarLayerList { + self.layersSidebarViewModel.createdOrderedEncodedData() } convenience init(from schema: GraphEntity, @@ -128,6 +132,8 @@ final class GraphState: Sendable { self.documentDelegate = document self.documentEncoderDelegate = documentEncoderDelegate + self.layersSidebarViewModel.initializeDelegate(graph: self) + self.nodes.values.forEach { $0.initializeDelegate(graph: self, document: document) } @@ -136,16 +142,8 @@ final class GraphState: Sendable { $0.initializeDelegate(parentGraph: self) } - self.updateSidebarListStateAfterStateChange() - - // TODO: why is this necessary? - _updateStateAfterListChange( - updatedList: self.sidebarListState, - expanded: self.getSidebarExpandedItems(), - graphState: self) - self.updateTopologicalData() - + self.visibleNodesViewModel .updateNodesPagingDict(components: self.components, parentGraphPath: self.saveLocation) @@ -251,7 +249,11 @@ extension GraphState: GraphDelegate { } extension GraphState { - @MainActor func createSchema() -> GraphEntity { + var sidebarSelectionState: LayersSidebarViewModel.SidebarSelectionState { + self.layersSidebarViewModel.selectionState + } + + @MainActor func createSchema() -> GraphEntity { let nodes = self.visibleNodesViewModel.nodes.values .map { $0.createSchema() } let commentBoxes = self.commentBoxesDict.values.map { $0.createSchema() } @@ -296,10 +298,11 @@ extension GraphState { self.visibleNodesViewModel.nodes = newDictionary } + @MainActor private func updateSynchronousProperties(from schema: GraphEntity) { self.id = schema.id self.name = schema.name - self.orderedSidebarLayers = schema.orderedSidebarLayers + self.layersSidebarViewModel.update(from: schema.orderedSidebarLayers) } @MainActor func update(from schema: GraphEntity) async { diff --git a/Stitch/Graph/ViewModel/GraphUI.swift b/Stitch/Graph/ViewModel/GraphUI.swift index 8f77eb2f7..05b5160e2 100644 --- a/Stitch/Graph/ViewModel/GraphUI.swift +++ b/Stitch/Graph/ViewModel/GraphUI.swift @@ -415,7 +415,7 @@ extension CanvasItemViewModel { // Anytime we select a canvas item, // we "de-actively-select" any sidebar layers, // but do not touch the "focused" layers. - self.graphDelegate?.sidebarSelectionState.inspectorFocusedLayers.activelySelected = .init() + self.graphDelegate?.layersSidebarViewModel.inspectorFocusedLayers.activelySelected = .init() } @MainActor diff --git a/Stitch/Stitch.entitlements b/Stitch/Stitch.entitlements index 898ab4607..e5baaee79 100644 --- a/Stitch/Stitch.entitlements +++ b/Stitch/Stitch.entitlements @@ -6,7 +6,7 @@ development com.apple.developer.icloud-container-identifiers - iCloud.app.stitchdesign.stitch + iCloud.app.dev.stitchdesign.stitch com.apple.developer.icloud-services @@ -15,7 +15,7 @@ com.apple.developer.ubiquity-container-identifiers - iCloud.app.stitchdesign.stitch + iCloud.app.dev.stitchdesign.stitch com.apple.security.app-sandbox