Skip to content

Commit c641ed0

Browse files
committed
Support toast in widgets
1 parent f2e7888 commit c641ed0

9 files changed

Lines changed: 142 additions & 100 deletions

File tree

Core/Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,7 @@ let package = Package(
273273
dependencies: [
274274
"PromptToCodeService",
275275
"ChatGPTChatTab",
276+
.product(name: "Toast", package: "Tool"),
276277
.product(name: "UserDefaultsObserver", package: "Tool"),
277278
.product(name: "SharedUIComponents", package: "Tool"),
278279
.product(name: "AppMonitoring", package: "Tool"),

Core/Sources/Service/Service.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Dependencies
22
import Foundation
33
import SuggestionService
4+
import Toast
45
import Workspace
56
import WorkspaceSuggestionService
67
import XcodeInspector
@@ -30,6 +31,8 @@ public final class Service {
3031
let proService: ProService
3132
#endif
3233

34+
@Dependency(\.toast) var toast
35+
3336
private init() {
3437
@Dependency(\.workspacePool) var workspacePool
3538

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import ComposableArchitecture
2+
import Environment
3+
import Preferences
4+
import SwiftUI
5+
import Toast
6+
7+
public struct ToastPanel: ReducerProtocol {
8+
public struct State: Equatable {
9+
var toast: Toast.State = .init()
10+
var colorScheme: ColorScheme = .light
11+
var alignTopToAnchor = false
12+
}
13+
14+
public enum Action: Equatable {
15+
case start
16+
case toast(Toast.Action)
17+
}
18+
19+
public var body: some ReducerProtocol<State, Action> {
20+
Scope(state: \.toast, action: /Action.toast) {
21+
Toast()
22+
}
23+
24+
Reduce { state, action in
25+
switch action {
26+
case .start:
27+
return .run { send in
28+
await send(.toast(.start))
29+
}
30+
case .toast:
31+
return .none
32+
}
33+
}
34+
}
35+
}

Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,6 @@ public struct WidgetFeature: ReducerProtocol {
1616
var frame: CGRect = .zero
1717
}
1818

19-
public struct Windows: Equatable {
20-
public var widgetWindowState = WindowState()
21-
public var chatWindowState = WindowState()
22-
public var suggestionPanelWindowState = WindowState()
23-
public var sharedPanelWindowState = WindowState()
24-
public var tabWindowState = WindowState()
25-
}
26-
2719
public enum WindowCanBecomeKey: Equatable {
2820
case sharedPanel
2921
case chatPanel
@@ -33,8 +25,8 @@ public struct WidgetFeature: ReducerProtocol {
3325
var focusingDocumentURL: URL?
3426
public var colorScheme: ColorScheme = .light
3527

36-
public var toast = Toast.State()
37-
28+
var toastPanel = ToastPanel.State()
29+
3830
// MARK: Panels
3931

4032
public var panelState = PanelFeature.State()
@@ -123,7 +115,7 @@ public struct WidgetFeature: ReducerProtocol {
123115
case updateWindowOpacityFinished
124116
case updateKeyWindow(WindowCanBecomeKey)
125117

126-
case toast(Toast.Action)
118+
case toastPanel(ToastPanel.Action)
127119
case panel(PanelFeature.Action)
128120
case chatPanel(ChatPanelFeature.Action)
129121
case circularWidget(CircularWidgetFeature.Action)
@@ -147,10 +139,10 @@ public struct WidgetFeature: ReducerProtocol {
147139
public init() {}
148140

149141
public var body: some ReducerProtocol<State, Action> {
150-
Scope(state: \.toast, action: /Action.toast) {
151-
Toast()
142+
Scope(state: \.toastPanel, action: /Action.toastPanel) {
143+
ToastPanel()
152144
}
153-
145+
154146
Scope(state: \._circularWidgetState, action: /Action.circularWidget) {
155147
CircularWidgetFeature()
156148
}
@@ -236,7 +228,7 @@ public struct WidgetFeature: ReducerProtocol {
236228
case .startup:
237229
return .merge(
238230
.run { send in
239-
await send(.toast(.start))
231+
await send(.toastPanel(.start))
240232
await send(.observeActiveApplicationChange)
241233
await send(.observeCompletionPanelChange)
242234
await send(.observeFullscreenChange)
@@ -490,6 +482,7 @@ public struct WidgetFeature: ReducerProtocol {
490482
}()
491483

492484
state.colorScheme = scheme
485+
state.toastPanel.colorScheme = scheme
493486
state.panelState.sharedPanelState.colorScheme = scheme
494487
state.panelState.suggestionPanelState.colorScheme = scheme
495488
state.chatPanelState.colorScheme = scheme
@@ -514,6 +507,10 @@ public struct WidgetFeature: ReducerProtocol {
514507
state.panelState.suggestionPanelState.isPanelOutOfFrame = true
515508
}
516509

510+
state.toastPanel.alignTopToAnchor = widgetLocation
511+
.defaultPanelLocation
512+
.alignPanelTop
513+
517514
let isChatPanelDetached = state.chatPanelState.chatPanelInASeparateWindow
518515

519516
return .run { _ in
@@ -523,8 +520,8 @@ public struct WidgetFeature: ReducerProtocol {
523520
display: false,
524521
animate: animated
525522
)
526-
windows.tabWindow.setFrame(
527-
widgetLocation.tabFrame,
523+
windows.toastWindow.setFrame(
524+
widgetLocation.defaultPanelLocation.frame,
528525
display: false,
529526
animate: animated
530527
)
@@ -582,7 +579,7 @@ public struct WidgetFeature: ReducerProtocol {
582579
windows.sharedPanelWindow.alphaValue = noFocus ? 0 : 1
583580
windows.suggestionPanelWindow.alphaValue = noFocus ? 0 : 1
584581
windows.widgetWindow.alphaValue = noFocus ? 0 : 1
585-
windows.tabWindow.alphaValue = 0
582+
windows.toastWindow.alphaValue = noFocus ? 0 : 1
586583

587584
if isChatPanelDetached {
588585
windows.chatPanelWindow.alphaValue = hasChat ? 1 : 0
@@ -604,7 +601,7 @@ public struct WidgetFeature: ReducerProtocol {
604601
windows.sharedPanelWindow.alphaValue = noFocus ? 0 : 1
605602
windows.suggestionPanelWindow.alphaValue = noFocus ? 0 : 1
606603
windows.widgetWindow.alphaValue = noFocus ? 0 : 1
607-
windows.tabWindow.alphaValue = 0
604+
windows.toastWindow.alphaValue = noFocus ? 0 : 1
608605
if isChatPanelDetached {
609606
windows.chatPanelWindow.alphaValue = hasChat ? 1 : 0
610607
} else {
@@ -615,7 +612,7 @@ public struct WidgetFeature: ReducerProtocol {
615612
windows.sharedPanelWindow.alphaValue = 0
616613
windows.suggestionPanelWindow.alphaValue = 0
617614
windows.widgetWindow.alphaValue = 0
618-
windows.tabWindow.alphaValue = 0
615+
windows.toastWindow.alphaValue = 0
619616
if !isChatPanelDetached {
620617
windows.chatPanelWindow.alphaValue = 0
621618
}
@@ -639,8 +636,8 @@ public struct WidgetFeature: ReducerProtocol {
639636
await windows.sharedPanelWindow.makeKeyAndOrderFront(nil)
640637
}
641638
}
642-
643-
case .toast:
639+
640+
case .toastPanel:
644641
return .none
645642

646643
case .circularWidget:

Core/Sources/SuggestionWidget/ModuleDependency.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,17 @@ public final class SuggestionWidgetControllerDependency {
2222
public final class WidgetWindows {
2323
var fullscreenDetector: NSWindow!
2424
var widgetWindow: NSWindow!
25-
var tabWindow: NSWindow!
2625
var sharedPanelWindow: NSWindow!
2726
var suggestionPanelWindow: NSWindow!
2827
var chatPanelWindow: NSWindow!
28+
var toastWindow: NSWindow!
2929

3030
nonisolated
3131
init() {}
3232

3333
func orderFront() {
3434
widgetWindow?.orderFrontRegardless()
35-
tabWindow?.orderFrontRegardless()
35+
toastWindow?.orderFrontRegardless()
3636
sharedPanelWindow?.orderFrontRegardless()
3737
suggestionPanelWindow?.orderFrontRegardless()
3838
chatPanelWindow?.orderFrontRegardless()
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import ComposableArchitecture
2+
import Dependencies
3+
import Foundation
4+
import SwiftUI
5+
import Toast
6+
7+
struct ToastPanelView: View {
8+
let store: StoreOf<ToastPanel>
9+
10+
struct ViewState: Equatable {
11+
let colorScheme: ColorScheme
12+
let alignTopToAnchor: Bool
13+
}
14+
15+
var body: some View {
16+
WithViewStore(store, observe: {
17+
ViewState(
18+
colorScheme: $0.colorScheme,
19+
alignTopToAnchor: $0.alignTopToAnchor
20+
)
21+
}) { viewStore in
22+
VStack(spacing: 4) {
23+
if !viewStore.alignTopToAnchor {
24+
Spacer()
25+
}
26+
27+
WithViewStore(store, observe: \.toast.messages) { viewStore in
28+
ForEach(viewStore.state) { message in
29+
message.content
30+
.foregroundColor(.white)
31+
.padding(8)
32+
.frame(maxWidth: .infinity)
33+
.background({
34+
switch message.type {
35+
case .info: return Color(nsColor: .systemIndigo)
36+
case .error: return Color(nsColor: .systemRed)
37+
case .warning: return Color(nsColor: .systemOrange)
38+
}
39+
}() as Color, in: RoundedRectangle(cornerRadius: 8))
40+
.shadow(color: Color.black.opacity(0.2), radius: 4)
41+
}
42+
}
43+
44+
if viewStore.alignTopToAnchor {
45+
Spacer()
46+
}
47+
}
48+
.colorScheme(viewStore.colorScheme)
49+
}
50+
.frame(maxWidth: .infinity, maxHeight: .infinity)
51+
.allowsHitTesting(false)
52+
}
53+
}
54+

Core/Sources/SuggestionWidget/SuggestionWidgetController.swift

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -58,30 +58,6 @@ public final class SuggestionWidgetController: NSObject {
5858
return it
5959
}()
6060

61-
private lazy var tabWindow = {
62-
let it = CanBecomeKeyWindow(
63-
contentRect: .zero,
64-
styleMask: .borderless,
65-
backing: .buffered,
66-
defer: false
67-
)
68-
it.isReleasedWhenClosed = false
69-
it.isOpaque = false
70-
it.backgroundColor = .clear
71-
it.level = .floating
72-
it.collectionBehavior = [.fullScreenAuxiliary, .transient]
73-
it.hasShadow = true
74-
it.contentView = NSHostingView(
75-
rootView: TabView(store: store.scope(
76-
state: \.chatPanelState,
77-
action: WidgetFeature.Action.chatPanel
78-
))
79-
)
80-
it.setIsVisible(true)
81-
it.canBecomeKeyChecker = { false }
82-
return it
83-
}()
84-
8561
private lazy var sharedPanelWindow = {
8662
let it = CanBecomeKeyWindow(
8763
contentRect: .init(x: 0, y: 0, width: Style.panelWidth, height: Style.panelHeight),
@@ -171,6 +147,31 @@ public final class SuggestionWidgetController: NSObject {
171147
return it
172148
}()
173149

150+
private lazy var toastWindow = {
151+
let it = CanBecomeKeyWindow(
152+
contentRect: .zero,
153+
styleMask: [.borderless],
154+
backing: .buffered,
155+
defer: false
156+
)
157+
it.isReleasedWhenClosed = false
158+
it.isOpaque = true
159+
it.backgroundColor = .clear
160+
it.level = .floating
161+
it.collectionBehavior = [.fullScreenAuxiliary, .transient]
162+
it.hasShadow = false
163+
it.contentView = NSHostingView(
164+
rootView: ToastPanelView(store: store.scope(
165+
state: \.toastPanel,
166+
action: WidgetFeature.Action.toastPanel
167+
))
168+
)
169+
it.setIsVisible(true)
170+
it.ignoresMouseEvents = true
171+
it.canBecomeKeyChecker = { false }
172+
return it
173+
}()
174+
174175
let store: StoreOf<WidgetFeature>
175176
let viewStore: ViewStoreOf<WidgetFeature>
176177
let chatTabPool: ChatTabPool
@@ -193,7 +194,7 @@ public final class SuggestionWidgetController: NSObject {
193194
if ProcessInfo.processInfo.environment["IS_UNIT_TEST"] == "YES" { return }
194195

195196
dependency.windows.chatPanelWindow = chatPanelWindow
196-
dependency.windows.tabWindow = tabWindow
197+
dependency.windows.toastWindow = toastWindow
197198
dependency.windows.sharedPanelWindow = sharedPanelWindow
198199
dependency.windows.suggestionPanelWindow = suggestionPanelWindow
199200
dependency.windows.fullscreenDetector = fullscreenDetector
@@ -223,7 +224,7 @@ public extension SuggestionWidgetController {
223224
}
224225

225226
func presentError(_ errorDescription: String) {
226-
store.send(.panel(.presentError(errorDescription)))
227+
store.send(.toastPanel(.toast(.toast(errorDescription, .error))))
227228
}
228229

229230
func presentChatRoom() {

Core/Sources/SuggestionWidget/TabView.swift

Lines changed: 0 additions & 49 deletions
This file was deleted.

0 commit comments

Comments
 (0)