Skip to content

Commit fb0c050

Browse files
committed
Add a widget window to show/hide the suggestion panel
1 parent 52ec0a7 commit fb0c050

File tree

2 files changed

+133
-72
lines changed

2 files changed

+133
-72
lines changed

Core/Sources/Service/GUI/SuggestionPanelController.swift

Lines changed: 119 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import SwiftUI
66

77
@MainActor
88
final class SuggestionPanelController {
9-
private lazy var window = {
9+
private lazy var widgetWindow = {
1010
let it = NSWindow(
1111
contentRect: .zero,
1212
styleMask: .borderless,
@@ -18,21 +18,41 @@ final class SuggestionPanelController {
1818
it.backgroundColor = .clear
1919
it.level = .statusBar
2020
it.contentView = NSHostingView(
21-
rootView: SuggestionPanelView(viewModel: viewModel)
22-
.allowsHitTesting(false)
23-
.frame(width: 400, height: 250)
21+
rootView: WidgetView(
22+
viewModel: widgetViewModel,
23+
panelViewModel: suggestionPanelViewModel
24+
)
25+
)
26+
it.setIsVisible(true)
27+
return it
28+
}()
29+
30+
private lazy var panelWindow = {
31+
let it = NSWindow(
32+
contentRect: .zero,
33+
styleMask: .borderless,
34+
backing: .buffered,
35+
defer: false
36+
)
37+
it.isReleasedWhenClosed = false
38+
it.isOpaque = false
39+
it.backgroundColor = .clear
40+
it.level = .statusBar
41+
it.contentView = NSHostingView(
42+
rootView: SuggestionPanelView(viewModel: suggestionPanelViewModel)
2443
)
2544
it.setIsVisible(true)
2645
return it
2746
}()
2847

29-
let viewModel = SuggestionPanelViewModel()
48+
let widgetViewModel = WidgetViewModel()
49+
let suggestionPanelViewModel = SuggestionPanelViewModel()
3050

3151
private var windowChangeObservationTask: Task<Void, Error>?
3252
private var activeApplicationMonitorTask: Task<Void, Error>?
3353
private var xcode: NSRunningApplication?
3454
private var suggestionForFiles: [URL: Suggestion] = [:]
35-
55+
3656
enum Suggestion {
3757
case code([String], startLineIndex: Int)
3858
}
@@ -58,11 +78,22 @@ final class SuggestionPanelController {
5878
}
5979
}
6080
}
61-
81+
6282
func suggestCode(_ code: String, startLineIndex: Int, fileURL: URL) {
63-
viewModel.suggestion = code.split(separator: "\n").map(String.init)
64-
viewModel.startLineIndex = startLineIndex
65-
suggestionForFiles[fileURL] = .code(viewModel.suggestion, startLineIndex: startLineIndex)
83+
suggestionPanelViewModel.suggestion = code.split(separator: "\n").map(String.init)
84+
suggestionPanelViewModel.startLineIndex = startLineIndex
85+
suggestionPanelViewModel.isPanelDisplayed = true
86+
suggestionForFiles[fileURL] = .code(
87+
suggestionPanelViewModel.suggestion,
88+
startLineIndex: startLineIndex
89+
)
90+
}
91+
92+
func discardSuggestion(fileURL: URL) {
93+
suggestionForFiles[fileURL] = nil
94+
suggestionPanelViewModel.suggestion = []
95+
suggestionPanelViewModel.startLineIndex = 0
96+
suggestionPanelViewModel.isPanelDisplayed = false
6697
}
6798

6899
private func observeXcodeWindowChangeIfNeeded(_ app: NSRunningApplication) {
@@ -82,18 +113,19 @@ final class SuggestionPanelController {
82113
guard let self else { return }
83114
try Task.checkCancellation()
84115
self.updateWindowLocation()
85-
116+
86117
if notification.name == kAXFocusedUIElementChangedNotification {
87118
guard let fileURL = try? await Environment.fetchCurrentFileURL(),
88119
let suggestion = suggestionForFiles[fileURL]
89120
else {
90-
viewModel.suggestion = []
121+
suggestionPanelViewModel.suggestion = []
91122
continue
92123
}
93124
switch suggestion {
94125
case let .code(code, startLineIndex):
95-
viewModel.suggestion = code
96-
viewModel.startLineIndex = startLineIndex
126+
return
127+
suggestionPanelViewModel.suggestion = code
128+
suggestionPanelViewModel.startLineIndex = startLineIndex
97129
}
98130
}
99131
}
@@ -123,68 +155,104 @@ final class SuggestionPanelController {
123155
var size: CGSize = .zero
124156
let foundSize = AXValueGetValue(sizeValue, .cgSize, &size)
125157
let screen = NSScreen.screens.first
126-
var frame = CGRect(origin: position, size: size)
158+
let frame = CGRect(origin: position, size: size)
127159
if foundSize, foundPosition, let screen {
128-
frame.origin = .init(
129-
x: frame.maxX + 2,
130-
y: screen.frame.height - frame.minY - 250
160+
let anchorFrame = CGRect(
161+
x: frame.maxX - 4 - 30,
162+
y: screen.frame.height - frame.minY - 30,
163+
width: 30,
164+
height: 30
165+
)
166+
widgetWindow.setFrame(anchorFrame, display: false)
167+
168+
let panelFrame = CGRect(
169+
x: anchorFrame.maxX + 8,
170+
y: anchorFrame.minY - 300 + 30,
171+
width: 400,
172+
height: 300
131173
)
132-
frame.size = .init(width: 400, height: 300)
133-
window.alphaValue = 1
134-
window.setFrame(frame, display: false)
174+
panelWindow.alphaValue = 1
175+
panelWindow.setFrame(panelFrame, display: false)
135176
return
136177
}
137178
}
138179
}
139180

140-
window.alphaValue = 0
181+
panelWindow.alphaValue = 0
141182
}
142183
}
143184

144-
#warning("MUSTDO: Update when editing file is changed.")
145-
146185
@MainActor
147186
final class SuggestionPanelViewModel: ObservableObject {
148187
@Published var startLineIndex: Int = 0
149-
@Published var suggestion: [String] = ["Hello", "World"] {
150-
didSet {
151-
isPanelDisplayed = !suggestion.isEmpty
152-
}
153-
}
154-
188+
@Published var suggestion: [String] = ["Hello", "World"]
155189
@Published var isPanelDisplayed = true
156190
}
157191

158192
struct SuggestionPanelView: View {
159193
@ObservedObject var viewModel: SuggestionPanelViewModel
194+
@State var isHovered: Bool = false
160195

161196
var body: some View {
162-
if viewModel.isPanelDisplayed {
163-
if !viewModel.suggestion.isEmpty {
164-
ZStack(alignment: .topLeading) {
165-
Color(red: 31 / 255, green: 31 / 255, blue: 36 / 255)
166-
ScrollView {
167-
VStack(alignment: .leading) {
168-
ForEach(0..<viewModel.suggestion.count, id: \.self) { index in
169-
HStack(alignment: .firstTextBaseline) {
170-
Text("\(index)")
171-
Text(viewModel.suggestion[index])
172-
}
197+
if !viewModel.suggestion.isEmpty {
198+
ZStack(alignment: .topLeading) {
199+
RoundedRectangle(cornerRadius: 8, style: .continuous)
200+
.fill(Color(red: 31 / 255, green: 31 / 255, blue: 36 / 255))
201+
ScrollView {
202+
VStack(alignment: .leading) {
203+
ForEach(0..<viewModel.suggestion.count, id: \.self) { index in
204+
HStack(alignment: .firstTextBaseline) {
205+
Text("\(index)")
206+
Text(viewModel.suggestion[index])
173207
}
174-
Spacer()
175208
}
176-
.foregroundColor(.white)
177-
.font(.system(size: 12, design: .monospaced))
178-
.multilineTextAlignment(.leading)
179-
.padding()
209+
Spacer()
180210
}
211+
.foregroundColor(.white)
212+
.font(.system(size: 12, design: .monospaced))
213+
.multilineTextAlignment(.leading)
214+
.padding()
215+
}
216+
}
217+
.opacity({
218+
guard viewModel.isPanelDisplayed else { return 0 }
219+
return isHovered ? 0.3 : 1
220+
}())
221+
.frame(maxWidth: .infinity, maxHeight: .infinity)
222+
.onHover { yes in
223+
withAnimation(.easeInOut(duration: 0.2)) {
224+
isHovered = yes
181225
}
182-
.frame(maxWidth: .infinity, maxHeight: .infinity)
183-
} else {
184-
Color(red: 31 / 255, green: 31 / 255, blue: 36 / 255)
185226
}
186-
} else {
187-
EmptyView()
227+
.allowsHitTesting(viewModel.isPanelDisplayed)
188228
}
189229
}
190230
}
231+
232+
@MainActor
233+
final class WidgetViewModel: ObservableObject {
234+
enum Position {
235+
case topLeft
236+
case topRight
237+
case bottomLeft
238+
case bottomRight
239+
}
240+
241+
var position: Position = .topRight
242+
243+
init() {}
244+
}
245+
246+
struct WidgetView: View {
247+
@ObservedObject var viewModel: WidgetViewModel
248+
var panelViewModel: SuggestionPanelViewModel
249+
250+
var body: some View {
251+
Circle().fill(.blue)
252+
.onTapGesture {
253+
withAnimation(.easeInOut(duration: 0.2)) {
254+
panelViewModel.isPanelDisplayed.toggle()
255+
}
256+
}
257+
}
258+
}

Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,7 @@ struct WindowBaseCommandHandler: SuggestionCommandHanlder {
4242
if let suggestion = filespace.presentingSuggestion {
4343
presentSuggestion(suggestion, lines: editor.lines, fileURL: fileURL)
4444
} else {
45-
Task { @MainActor in
46-
GraphicalUserInterfaceController.shared.suggestionPanelController.viewModel
47-
.suggestion = []
48-
}
45+
discardSuggestion(fileURL: fileURL)
4946
}
5047
}
5148

@@ -69,10 +66,7 @@ struct WindowBaseCommandHandler: SuggestionCommandHanlder {
6966
if let suggestion = filespace.presentingSuggestion {
7067
presentSuggestion(suggestion, lines: editor.lines, fileURL: fileURL)
7168
} else {
72-
Task { @MainActor in
73-
GraphicalUserInterfaceController.shared.suggestionPanelController.viewModel
74-
.suggestion = []
75-
}
69+
discardSuggestion(fileURL: fileURL)
7670
}
7771
}
7872

@@ -96,10 +90,7 @@ struct WindowBaseCommandHandler: SuggestionCommandHanlder {
9690
if let suggestion = filespace.presentingSuggestion {
9791
presentSuggestion(suggestion, lines: editor.lines, fileURL: fileURL)
9892
} else {
99-
Task { @MainActor in
100-
GraphicalUserInterfaceController.shared.suggestionPanelController.viewModel
101-
.suggestion = []
102-
}
93+
discardSuggestion(fileURL: fileURL)
10394
}
10495
}
10596

@@ -115,18 +106,13 @@ struct WindowBaseCommandHandler: SuggestionCommandHanlder {
115106
let (workspace, _) = try await Workspace.fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
116107
workspace.rejectSuggestion(forFileAt: fileURL)
117108

118-
// hide it
119-
120-
Task { @MainActor in
121-
GraphicalUserInterfaceController.shared.suggestionPanelController.viewModel
122-
.suggestion = []
123-
}
109+
discardSuggestion(fileURL: fileURL)
124110
}
125111

126112
func acceptSuggestion(editor: EditorContent) async throws -> UpdatedContent? {
127-
Task { @MainActor in
128-
GraphicalUserInterfaceController.shared.suggestionPanelController.viewModel
129-
.suggestion = []
113+
Task {
114+
let fileURL = try await Environment.fetchCurrentFileURL()
115+
discardSuggestion(fileURL: fileURL)
130116
}
131117
return try await CommentBaseCommandHandler().acceptSuggestion(editor: editor)
132118
}
@@ -150,4 +136,11 @@ struct WindowBaseCommandHandler: SuggestionCommandHanlder {
150136
)
151137
}
152138
}
139+
140+
func discardSuggestion(fileURL: URL) {
141+
Task { @MainActor in
142+
let controller = GraphicalUserInterfaceController.shared.suggestionPanelController
143+
controller.discardSuggestion(fileURL: fileURL)
144+
}
145+
}
153146
}

0 commit comments

Comments
 (0)