-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Expand file tree
/
Copy pathChatDropdownView.swift
More file actions
129 lines (120 loc) · 4.35 KB
/
ChatDropdownView.swift
File metadata and controls
129 lines (120 loc) · 4.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import ConversationServiceProvider
import AppKit
import SwiftUI
import ComposableArchitecture
protocol DropDownItem: Equatable {
var id: String { get }
var displayName: String { get }
var displayDescription: String { get }
}
extension ChatTemplate: DropDownItem {
var displayName: String { id }
var displayDescription: String { description }
}
extension ChatAgent: DropDownItem {
var id: String { slug }
var displayName: String { slug }
var displayDescription: String { description }
}
struct ChatDropdownView<T: DropDownItem>: View {
@Binding var items: [T]
let prefixSymbol: String
let onSelect: (T) -> Void
@State private var selectedIndex = 0
@State private var frameHeight: CGFloat = 0
@State private var localMonitor: Any? = nil
public var body: some View {
WithPerceptionTracking {
VStack(alignment: .leading, spacing: 0) {
ForEach(Array(items.enumerated()), id: \.element.id) { index, item in
HStack {
Text(prefixSymbol + item.displayName)
.hoverPrimaryForeground(isHovered: selectedIndex == index)
Spacer()
Text(item.displayDescription)
.hoverSecondaryForeground(isHovered: selectedIndex == index)
}
.padding(.horizontal, 8)
.padding(.vertical, 6)
.contentShape(Rectangle())
.onTapGesture {
onSelect(item)
}
.hoverBackground(isHovered: selectedIndex == index)
.onHover { isHovered in
if isHovered {
selectedIndex = index
}
}
}
}
.background(
GeometryReader { geometry in
Color.clear
.onAppear { frameHeight = geometry.size.height }
.onChange(of: geometry.size.height) { newHeight in
frameHeight = newHeight
}
}
)
.background(.ultraThickMaterial)
.cornerRadius(6)
.shadow(radius: 2)
.overlay(
RoundedRectangle(cornerRadius: 6)
.stroke(Color(nsColor: .separatorColor), lineWidth: 1)
)
.frame(maxWidth: .infinity)
.offset(y: -1 * frameHeight)
.onChange(of: items) { _ in
selectedIndex = 0
}
.onAppear {
selectedIndex = 0
localMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
switch event.keyCode {
case 126: // Up arrow
moveSelection(up: true)
return nil
case 125: // Down arrow
moveSelection(up: false)
return nil
case 36: // Return key
handleEnter()
return nil
case 48: // Tab key
handleTab()
return nil // not forwarding the Tab Event which will replace the typed message to "\t"
default:
break
}
return event
}
}
.onDisappear {
if let monitor = localMonitor {
NSEvent.removeMonitor(monitor)
localMonitor = nil
}
}
}
}
private func moveSelection(up: Bool) {
guard !items.isEmpty else { return }
let lowerBound = 0
let upperBound = items.count - 1
let newIndex = selectedIndex + (up ? -1 : 1)
selectedIndex = newIndex < lowerBound ? upperBound : (newIndex > upperBound ? lowerBound : newIndex)
}
private func handleEnter() {
handleTemplateSelection()
}
private func handleTab() {
handleTemplateSelection()
}
private func handleTemplateSelection() {
if items.count > 0 && selectedIndex < items.count {
onSelect(items[selectedIndex])
}
}
}