native macOS codings agent orchestrator
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

Fix sidebar drop overlays and hover state

onevcat 7400ec0f 304a2f8c

+169 -132
+1
supacode/Features/Repositories/Views/RepositorySectionView.swift
··· 182 182 } 183 183 } 184 184 .frame(maxWidth: .infinity, minHeight: headerCellHeight, maxHeight: .infinity, alignment: .center) 185 + .padding(.horizontal, 12) 185 186 .padding(.top, hasTopSpacing ? 4 : 0) 186 187 .padding(.bottom, hasTopSpacing && !repository.capabilities.supportsWorktrees ? 4 : 0) 187 188 .contentShape(.interaction, .rect)
+86 -18
supacode/Features/Repositories/Views/SidebarDragSupport.swift
··· 26 26 } 27 27 28 28 struct SidebarRepositoryDropDelegate: DropDelegate { 29 - let destination: Int 29 + let destination: (DropInfo) -> Int 30 30 let repositoryOrderIDs: [Repository.ID] 31 31 @Binding var targetedDestination: Int? 32 32 let onDrop: (IndexSet, Int) -> Void 33 33 let onDragEnded: () -> Void 34 34 35 35 func dropEntered(info: DropInfo) { 36 - targetedDestination = destination 36 + targetedDestination = destination(info) 37 37 } 38 38 39 39 func dropExited(info: DropInfo) { 40 - if targetedDestination == destination { 41 - targetedDestination = nil 42 - } 40 + targetedDestination = nil 43 41 } 44 42 45 43 func dropUpdated(info: DropInfo) -> DropProposal? { 46 - DropProposal(operation: .move) 44 + targetedDestination = destination(info) 45 + return DropProposal(operation: .move) 47 46 } 48 47 49 48 func performDrop(info: DropInfo) -> Bool { 49 + let dropDestination = destination(info) 50 50 targetedDestination = nil 51 51 guard let provider = info.itemProviders(for: [.prowlSidebarRepositoryID]).first else { 52 52 onDragEnded() ··· 56 56 guard let data, 57 57 let repositoryID = String(data: data, encoding: .utf8), 58 58 let source = repositoryOrderIDs.firstIndex(of: repositoryID), 59 - source != destination, 60 - source + 1 != destination 59 + source != dropDestination, 60 + source + 1 != dropDestination 61 61 else { 62 62 Task { @MainActor in onDragEnded() } 63 63 return 64 64 } 65 65 Task { @MainActor in 66 - onDrop(IndexSet(integer: source), destination) 66 + onDrop(IndexSet(integer: source), dropDestination) 67 67 onDragEnded() 68 68 } 69 69 } ··· 72 72 } 73 73 74 74 struct SidebarWorktreeDropDelegate: DropDelegate { 75 - let destination: Int 75 + let destination: (DropInfo) -> Int 76 76 let sectionIDs: [Worktree.ID] 77 77 @Binding var targetedDestination: Int? 78 78 let onDrop: (IndexSet, Int) -> Void 79 79 let onDragEnded: () -> Void 80 80 81 81 func dropEntered(info: DropInfo) { 82 - targetedDestination = destination 82 + targetedDestination = destination(info) 83 83 } 84 84 85 85 func dropExited(info: DropInfo) { 86 - if targetedDestination == destination { 87 - targetedDestination = nil 88 - } 86 + targetedDestination = nil 89 87 } 90 88 91 89 func dropUpdated(info: DropInfo) -> DropProposal? { 92 - DropProposal(operation: .move) 90 + targetedDestination = destination(info) 91 + return DropProposal(operation: .move) 93 92 } 94 93 95 94 func performDrop(info: DropInfo) -> Bool { 95 + let dropDestination = destination(info) 96 96 targetedDestination = nil 97 97 guard let provider = info.itemProviders(for: [.prowlSidebarWorktreeID]).first else { 98 98 onDragEnded() ··· 102 102 guard let data, 103 103 let worktreeID = String(data: data, encoding: .utf8), 104 104 let source = sectionIDs.firstIndex(of: worktreeID), 105 - source != destination, 106 - source + 1 != destination 105 + source != dropDestination, 106 + source + 1 != dropDestination 107 107 else { 108 108 Task { @MainActor in onDragEnded() } 109 109 return 110 110 } 111 111 Task { @MainActor in 112 - onDrop(IndexSet(integer: source), destination) 112 + onDrop(IndexSet(integer: source), dropDestination) 113 113 onDragEnded() 114 114 } 115 115 } ··· 138 138 } 139 139 140 140 extension View { 141 + func repositoryDropTarget( 142 + index: Int, 143 + repositoryOrderIDs: [Repository.ID], 144 + targetedDestination: Binding<Int?>, 145 + onDrop: @escaping (IndexSet, Int) -> Void, 146 + onDragEnded: @escaping () -> Void 147 + ) -> some View { 148 + self 149 + .overlay(alignment: .top) { 150 + SidebarDropIndicator(isVisible: targetedDestination.wrappedValue == index) 151 + } 152 + .overlay(alignment: .bottom) { 153 + SidebarDropIndicator(isVisible: targetedDestination.wrappedValue == index + 1) 154 + } 155 + .background { 156 + GeometryReader { proxy in 157 + Color.clear 158 + .onDrop( 159 + of: [.prowlSidebarRepositoryID], 160 + delegate: SidebarRepositoryDropDelegate( 161 + destination: { info in 162 + info.location.y < proxy.size.height / 2 ? index : index + 1 163 + }, 164 + repositoryOrderIDs: repositoryOrderIDs, 165 + targetedDestination: targetedDestination, 166 + onDrop: onDrop, 167 + onDragEnded: onDragEnded 168 + ) 169 + ) 170 + .accessibilityHidden(true) 171 + } 172 + } 173 + } 174 + 175 + func worktreeDropTarget( 176 + index: Int, 177 + rowIDs: [Worktree.ID], 178 + targetedDestination: Binding<Int?>, 179 + onDrop: @escaping (IndexSet, Int) -> Void, 180 + onDragEnded: @escaping () -> Void 181 + ) -> some View { 182 + self 183 + .overlay(alignment: .top) { 184 + SidebarDropIndicator(isVisible: targetedDestination.wrappedValue == index, horizontalPadding: 28) 185 + } 186 + .overlay(alignment: .bottom) { 187 + SidebarDropIndicator(isVisible: targetedDestination.wrappedValue == index + 1, horizontalPadding: 28) 188 + } 189 + .background { 190 + GeometryReader { proxy in 191 + Color.clear 192 + .onDrop( 193 + of: [.prowlSidebarWorktreeID], 194 + delegate: SidebarWorktreeDropDelegate( 195 + destination: { info in 196 + info.location.y < proxy.size.height / 2 ? index : index + 1 197 + }, 198 + sectionIDs: rowIDs, 199 + targetedDestination: targetedDestination, 200 + onDrop: onDrop, 201 + onDragEnded: onDragEnded 202 + ) 203 + ) 204 + .accessibilityHidden(true) 205 + } 206 + } 207 + } 208 + 141 209 @ViewBuilder 142 210 func draggableRepository( 143 211 id: Repository.ID,
+59 -75
supacode/Features/Repositories/Views/SidebarListView.swift
··· 67 67 } 68 68 69 69 ForEach(Array(repositoryItems.enumerated()), id: \.element.id) { index, item in 70 - repositoryDropZone( 71 - destination: index, 72 - repositoryOrderIDs: presentation.repositoryOrderIDs 73 - ) 74 70 repositoryItemView( 75 71 item, 76 72 index: index, 73 + repositoryOrderIDs: presentation.repositoryOrderIDs, 77 74 hotkeyRows: hotkeyRows, 78 75 selectedWorktreeIDs: selectedWorktreeIDs 79 76 ) 80 77 } 81 - repositoryDropZone( 82 - destination: repositoryItems.count, 83 - repositoryOrderIDs: presentation.repositoryOrderIDs 84 - ) 85 78 } 86 79 .padding(.vertical, 2) 87 80 } ··· 95 88 } 96 89 if case .dataTransferCompleted = session.phase { 97 90 endSidebarDrag() 98 - return 99 91 } 100 - beginSidebarDrag() 101 92 } 102 93 .safeAreaInset(edge: .top) { 103 94 HStack(spacing: 4) { ··· 186 177 private func repositoryItemView( 187 178 _ item: SidebarItem, 188 179 index: Int, 180 + repositoryOrderIDs: [Repository.ID], 189 181 hotkeyRows: [WorktreeRowModel], 190 182 selectedWorktreeIDs: Set<Worktree.ID> 191 183 ) -> some View { 192 - switch item { 193 - case .repository(let model): 194 - if let repository = store.state.repositories[id: model.repositoryID] { 195 - RepositorySectionView( 196 - repository: repository, 197 - hasTopSpacing: index > 0, 198 - isDragActive: isDragActive, 199 - hotkeyRows: hotkeyRows, 200 - selectedWorktreeIDs: selectedWorktreeIDs, 201 - expandedRepoIDs: $expandedRepoIDs, 202 - store: store, 203 - terminalManager: terminalManager, 204 - onRepositorySelected: { 205 - selectRepository(repository) 184 + Group { 185 + switch item { 186 + case .repository(let model): 187 + if let repository = store.state.repositories[id: model.repositoryID] { 188 + RepositorySectionView( 189 + repository: repository, 190 + hasTopSpacing: index > 0, 191 + isDragActive: isDragActive, 192 + hotkeyRows: hotkeyRows, 193 + selectedWorktreeIDs: selectedWorktreeIDs, 194 + expandedRepoIDs: $expandedRepoIDs, 195 + store: store, 196 + terminalManager: terminalManager, 197 + onRepositorySelected: { 198 + selectRepository(repository) 199 + } 200 + ) 201 + .draggableRepository( 202 + id: model.repositoryID, 203 + isEnabled: !model.isRemoving, 204 + beginDrag: beginSidebarDrag 205 + ) 206 + } 207 + 208 + case .failedRepository(let model): 209 + FailedRepositoryRow( 210 + name: model.name, 211 + path: model.path, 212 + showFailure: { 213 + let message = "\(model.path)\n\n\(model.failureMessage)" 214 + store.send(.presentAlert(title: "Unable to load \(model.name)", message: message)) 215 + }, 216 + removeRepository: { 217 + store.send(.repositoryManagement(.removeFailedRepository(model.id))) 206 218 } 207 219 ) 220 + .padding(.horizontal, 12) 221 + .overlay(alignment: .top) { 222 + if index > 0 { 223 + Rectangle() 224 + .fill(.secondary) 225 + .frame(height: 1) 226 + .frame(maxWidth: .infinity) 227 + .accessibilityHidden(true) 228 + } 229 + } 208 230 .draggableRepository( 209 - id: model.repositoryID, 210 - isEnabled: !model.isRemoving, 231 + id: model.id, 232 + isEnabled: model.isReorderable, 211 233 beginDrag: beginSidebarDrag 212 234 ) 213 - } 214 235 215 - case .failedRepository(let model): 216 - FailedRepositoryRow( 217 - name: model.name, 218 - path: model.path, 219 - showFailure: { 220 - let message = "\(model.path)\n\n\(model.failureMessage)" 221 - store.send(.presentAlert(title: "Unable to load \(model.name)", message: message)) 222 - }, 223 - removeRepository: { 224 - store.send(.repositoryManagement(.removeFailedRepository(model.id))) 225 - } 226 - ) 227 - .padding(.horizontal, 12) 228 - .overlay(alignment: .top) { 229 - if index > 0 { 230 - Rectangle() 231 - .fill(.secondary) 232 - .frame(height: 1) 233 - .frame(maxWidth: .infinity) 234 - .accessibilityHidden(true) 235 - } 236 + case .listHeader, .archivedWorktrees: 237 + EmptyView() 236 238 } 237 - .draggableRepository( 238 - id: model.id, 239 - isEnabled: model.isReorderable, 240 - beginDrag: beginSidebarDrag 241 - ) 242 - 243 - case .listHeader, .archivedWorktrees: 244 - EmptyView() 245 239 } 246 - } 247 - 248 - private func repositoryDropZone( 249 - destination: Int, 250 - repositoryOrderIDs: [Repository.ID] 251 - ) -> some View { 252 - SidebarDropIndicator(isVisible: targetedRepositoryDropDestination == destination) 253 - .onDrop( 254 - of: [.prowlSidebarRepositoryID], 255 - delegate: SidebarRepositoryDropDelegate( 256 - destination: destination, 257 - repositoryOrderIDs: repositoryOrderIDs, 258 - targetedDestination: $targetedRepositoryDropDestination, 259 - onDrop: { offsets, destination in 260 - store.send(.worktreeOrdering(.repositoriesMoved(offsets, destination))) 261 - }, 262 - onDragEnded: endSidebarDrag 263 - ) 264 - ) 240 + .repositoryDropTarget( 241 + index: index, 242 + repositoryOrderIDs: repositoryOrderIDs, 243 + targetedDestination: $targetedRepositoryDropDestination, 244 + onDrop: { offsets, destination in 245 + store.send(.worktreeOrdering(.repositoriesMoved(offsets, destination))) 246 + }, 247 + onDragEnded: endSidebarDrag 248 + ) 265 249 } 266 250 267 251 private func beginSidebarDrag() {
+23 -39
supacode/Features/Repositories/Views/WorktreeRowsView.swift
··· 90 90 ) -> some View { 91 91 let rowIDs = rows.map(\.id) 92 92 ForEach(Array(rows.enumerated()), id: \.element.id) { index, row in 93 - worktreeDropZone( 94 - destination: index, 95 - rowIDs: rowIDs, 96 - targetedDestination: targetedDestination, 97 - section: section 98 - ) 99 93 rowView( 100 94 row, 101 95 isRepositoryRemoving: isRepositoryRemoving, 102 96 moveDisabled: isRepositoryRemoving || row.isDeleting || row.isArchiving, 103 97 shortcutHint: worktreeShortcutHint(for: shortcutIndexByID[row.id]) 104 98 ) 99 + .worktreeDropTarget( 100 + index: index, 101 + rowIDs: rowIDs, 102 + targetedDestination: targetedDestination, 103 + onDrop: { offsets, destination in 104 + moveWorktrees(section: section, offsets: offsets, destination: destination) 105 + }, 106 + onDragEnded: endWorktreeDrag 107 + ) 105 108 } 106 - worktreeDropZone( 107 - destination: rows.count, 108 - rowIDs: rowIDs, 109 - targetedDestination: targetedDestination, 110 - section: section 111 - ) 112 109 } 113 110 114 111 @ViewBuilder ··· 224 221 didEnd: Bool 225 222 ) { 226 223 if didEnd { 227 - draggingWorktreeIDs = [] 224 + endWorktreeDrag() 228 225 return 229 226 } 230 227 if draggedIDs != draggingWorktreeIDs { 231 228 draggingWorktreeIDs = draggedIDs 232 229 } 233 - } 234 - 235 - private func worktreeDropZone( 236 - destination: Int, 237 - rowIDs: [Worktree.ID], 238 - targetedDestination: Binding<Int?>, 239 - section: SidebarWorktreeSection 240 - ) -> some View { 241 - SidebarDropIndicator(isVisible: targetedDestination.wrappedValue == destination, horizontalPadding: 28) 242 - .onDrop( 243 - of: [.prowlSidebarWorktreeID], 244 - delegate: SidebarWorktreeDropDelegate( 245 - destination: destination, 246 - sectionIDs: rowIDs, 247 - targetedDestination: targetedDestination, 248 - onDrop: { offsets, destination in 249 - switch section { 250 - case .pinned: 251 - store.send(.worktreeOrdering(.pinnedWorktreesMoved(repositoryID: repository.id, offsets, destination))) 252 - case .unpinned: 253 - store.send(.worktreeOrdering(.unpinnedWorktreesMoved(repositoryID: repository.id, offsets, destination))) 254 - } 255 - }, 256 - onDragEnded: endWorktreeDrag 257 - ) 258 - ) 259 230 } 260 231 261 232 private func selectWorktreeRow(_ worktreeID: Worktree.ID) { ··· 299 270 targetedPinnedDropDestination = nil 300 271 targetedUnpinnedDropDestination = nil 301 272 store.send(.worktreeOrdering(.setSidebarDragActive(false))) 273 + } 274 + 275 + private func moveWorktrees( 276 + section: SidebarWorktreeSection, 277 + offsets: IndexSet, 278 + destination: Int 279 + ) { 280 + switch section { 281 + case .pinned: 282 + store.send(.worktreeOrdering(.pinnedWorktreesMoved(repositoryID: repository.id, offsets, destination))) 283 + case .unpinned: 284 + store.send(.worktreeOrdering(.unpinnedWorktreesMoved(repositoryID: repository.id, offsets, destination))) 285 + } 302 286 } 303 287 304 288 private struct WorktreeRowViewConfig {