Skip to content

(WIP) - Terminal Groups - Issue #379 #2076

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 25 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// UtilityAreaTerminalGroup.swift
// CodeEdit
//
// Created by Gustavo Soré on 30/06/25.
//

import Foundation

struct UtilityAreaTerminalGroup: Identifiable, Hashable {
var id = UUID()
var name: String = "Grupo"
var terminals: [UtilityAreaTerminal] = []
var isCollapsed: Bool = false
var userName: Bool = false

static func == (lhs: UtilityAreaTerminalGroup, rhs: UtilityAreaTerminalGroup) -> Bool {
lhs.id == rhs.id
}

func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//
// NewGroupDropDelegate.swift
// CodeEdit
//
// Created by Gustavo Soré on 30/06/25.
//

import SwiftUI
import UniformTypeIdentifiers

/// Drop delegate responsible for handling the case when a terminal is dropped
/// outside of any existing group — i.e., it should create a new group with the dropped terminal.
struct NewGroupDropDelegate: DropDelegate {
/// The view model that manages terminal groups and selection state.
let viewModel: UtilityAreaViewModel

/// Validates whether the drop operation includes terminal data that this delegate can handle.
///
/// - Parameter info: The drop information provided by the system.
/// - Returns: `true` if the drop contains a valid terminal item type.
func validateDrop(info: DropInfo) -> Bool {
info.hasItemsConforming(to: [UTType.terminal.identifier])
}

/// Performs the drop by creating a new group and moving the terminal into it.
///
/// - Parameter info: The drop information containing the dragged item.
/// - Returns: `true` if the drop was successfully handled.
func performDrop(info: DropInfo) -> Bool {
// Extract the first item provider that conforms to the terminal type.
guard let item = info.itemProviders(for: [UTType.terminal.identifier]).first else {
return false
}

// Load and decode the terminal drag information.
item.loadDataRepresentation(forTypeIdentifier: UTType.terminal.identifier) { data, _ in
guard let data = data,
let dragInfo = try? JSONDecoder().decode(TerminalDragInfo.self, from: data),
let terminal = viewModel.terminalGroups
.flatMap({ $0.terminals })
.first(where: { $0.id == dragInfo.terminalID }) else {
return
}

// Perform the group creation and terminal movement on the main thread.
DispatchQueue.main.async {
withAnimation(.easeInOut(duration: 0.2)) {
// Optional logic to clean up old location (if needed).
viewModel.finalizeMoveTerminal(terminal, toGroup: UUID(), before: nil)

// Create a new group containing the dropped terminal.
viewModel.createGroup(with: [terminal])

// Reset drag-related state.
viewModel.dragOverTerminalID = nil
viewModel.draggedTerminalID = nil
}
}
}

return true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
//
// TerminalDropDelegate.swift
// CodeEdit
//
// Created by Gustavo Soré on 30/06/25.
//

import SwiftUI
import UniformTypeIdentifiers

/// Handles drop interactions for a terminal inside a specific group,
/// allowing for reordering or moving between groups.
struct TerminalDropDelegate: DropDelegate {
/// The ID of the group where the drop target resides.
let groupID: UUID

/// The shared view model managing terminal groups and selection state.
let viewModel: UtilityAreaViewModel

/// The ID of the terminal that is the drop destination, or `nil` if dropping at the end.
let destinationTerminalID: UUID?

/// Validates if the drop contains terminal data.
///
/// - Parameter info: The current drop context.
/// - Returns: `true` if the item conforms to the terminal type.
func validateDrop(info: DropInfo) -> Bool {
info.hasItemsConforming(to: [UTType.terminal.identifier])
}

/// Called when the drop enters a new target.
/// Sets the drag state in the view model for UI feedback.
///
/// - Parameter info: The drop context.
func dropEntered(info: DropInfo) {
guard let item = info.itemProviders(for: [UTType.terminal.identifier]).first else { return }

item.loadDataRepresentation(forTypeIdentifier: UTType.terminal.identifier) { data, _ in
guard let data = data,
let dragInfo = try? JSONDecoder().decode(TerminalDragInfo.self, from: data) else { return }

DispatchQueue.main.async {
withAnimation {
viewModel.draggedTerminalID = dragInfo.terminalID
viewModel.dragOverTerminalID = destinationTerminalID
}
}
}
}

/// Called continuously as the drop is updated over the view.
/// Updates drag-over visual feedback.
///
/// - Parameter info: The drop context.
/// - Returns: A drop proposal that defines the type of drop operation (e.g., move).
func dropUpdated(info: DropInfo) -> DropProposal? {
DispatchQueue.main.async {
withAnimation(.easeInOut(duration: 0.2)) {
viewModel.dragOverTerminalID = destinationTerminalID
}
}

return DropProposal(operation: .move)
}

/// Called when the drop is performed.
/// Decodes the dragged terminal and triggers its relocation in the model.
///
/// - Parameter info: The drop context with the drag payload.
/// - Returns: `true` if the drop was handled successfully.
func performDrop(info: DropInfo) -> Bool {
guard let item = info.itemProviders(for: [UTType.terminal.identifier]).first else { return false }

item.loadDataRepresentation(forTypeIdentifier: UTType.terminal.identifier) { data, _ in
guard let data = data,
let dragInfo = try? JSONDecoder().decode(TerminalDragInfo.self, from: data),
let terminal = viewModel.terminalGroups
.flatMap({ $0.terminals })
.first(where: { $0.id == dragInfo.terminalID }) else { return }

DispatchQueue.main.async {
withAnimation(.easeInOut(duration: 0.2)) {
viewModel.finalizeMoveTerminal(
terminal,
toGroup: groupID,
before: destinationTerminalID
)
viewModel.dragOverTerminalID = nil
viewModel.draggedTerminalID = nil
}
}
}

return true
}
}
Loading