@@ -6,7 +6,7 @@ import SwiftUI
66
77@MainActor
88final 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
147186final 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
158192struct 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+ }
0 commit comments