Skip to content

Commit b91877e

Browse files
committed
Added a close button click state, allows clicking under scroll view.
1 parent 2471175 commit b91877e

File tree

5 files changed

+140
-140
lines changed

5 files changed

+140
-140
lines changed

CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -59,33 +59,6 @@ struct FileInspectorView: View {
5959
widthOptions
6060
wrapLinesToggle
6161
}
62-
Section("Test Notifications") {
63-
Button("Add Test Notification") {
64-
NotificationManager.shared.post(
65-
iconSymbol: "bell.badge.fill",
66-
iconColor: .red,
67-
title: "Test Notification",
68-
description: "This is a test notification",
69-
actionButtonTitle: "Action",
70-
action: {
71-
print("Test notification action triggered")
72-
}
73-
)
74-
}
75-
Button("Add Sticky Notification") {
76-
NotificationManager.shared.post(
77-
iconSymbol: "pin.fill",
78-
iconColor: .orange,
79-
title: "Sticky Notification",
80-
description: "This notification will stay until dismissed",
81-
actionButtonTitle: "Acknowledge",
82-
action: {
83-
print("Sticky notification acknowledged")
84-
},
85-
isSticky: true
86-
)
87-
}
88-
}
8962
}
9063
} else {
9164
NoSelectionInspectorView()

CodeEdit/Features/Notifications/NotificationManager.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -323,16 +323,21 @@ final class NotificationManager: NSObject, ObservableObject {
323323
timers[notification.id]?.invalidate()
324324
timers[notification.id] = nil
325325
hiddenNotificationIds.remove(notification.id)
326-
326+
327327
if let index = activeNotifications.firstIndex(where: { $0.id == notification.id }) {
328328
activeNotifications[index].isBeingDismissed = true
329329
}
330-
330+
331331
withAnimation(.easeOut(duration: 0.2)) {
332332
activeNotifications.removeAll(where: { $0.id == notification.id })
333+
334+
// If this was the last notification and they were manually shown, hide the panel
335+
if activeNotifications.isEmpty && isManuallyShown {
336+
isManuallyShown = false
337+
}
333338
}
334339
notifications.removeAll(where: { $0.id == notification.id })
335-
340+
336341
// Mark as read when dismissed
337342
markAsRead(notification)
338343
}

CodeEdit/Features/Notifications/Views/NotificationBannerView.swift

Lines changed: 50 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,31 @@
77

88
import SwiftUI
99

10+
struct CloseButtonStyle: ButtonStyle {
11+
@Environment(\.colorScheme)
12+
private var colorScheme
13+
14+
func makeBody(configuration: Configuration) -> some View {
15+
configuration.label
16+
.font(.system(size: 10))
17+
.foregroundColor(.secondary)
18+
.frame(width: 20, height: 20, alignment: .center)
19+
.background(Color.primary.opacity(configuration.isPressed ? colorScheme == .dark ? 0.10 : 0.05 : 0.00))
20+
.background(.regularMaterial)
21+
.overlay(
22+
RoundedRectangle(cornerRadius: 10)
23+
.stroke(Color(nsColor: .separatorColor), lineWidth: 2)
24+
)
25+
.cornerRadius(10)
26+
.shadow(
27+
color: Color(.black.withAlphaComponent(colorScheme == .dark ? 0.2 : 0.1)),
28+
radius: 5,
29+
x: 0,
30+
y: 2
31+
)
32+
}
33+
}
34+
1035
struct NotificationBannerView: View {
1136
@Environment(\.colorScheme)
1237
private var colorScheme
@@ -17,12 +42,8 @@ struct NotificationBannerView: View {
1742
let onDismiss: () -> Void
1843
let onAction: () -> Void
1944

20-
@State private var offset: CGFloat = 0
21-
@State private var opacity: CGFloat = 1
2245
@State private var isHovering = false
2346

24-
private let dismissThreshold: CGFloat = 100
25-
2647
let cornerRadius: CGFloat = 10
2748

2849
private var backgroundContainer: some View {
@@ -35,44 +56,6 @@ struct NotificationBannerView: View {
3556
.stroke(Color(nsColor: .separatorColor), lineWidth: 2)
3657
}
3758

38-
private var dragGesture: some Gesture {
39-
DragGesture(minimumDistance: 2)
40-
.onChanged { value in
41-
if value.translation.width > 0 {
42-
offset = value.translation.width
43-
opacity = 1 - (offset / dismissThreshold)
44-
}
45-
}
46-
.onEnded { value in
47-
let velocity = value.predictedEndLocation.x - value.location.x
48-
49-
if offset > dismissThreshold || velocity > 100 {
50-
withAnimation(.easeOut(duration: 0.2)) {
51-
offset = NSScreen.main?.frame.width ?? 1000
52-
opacity = 0
53-
}
54-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
55-
onDismiss()
56-
}
57-
} else {
58-
withAnimation(.easeOut(duration: 0.2)) {
59-
offset = 0
60-
opacity = 1
61-
}
62-
}
63-
}
64-
}
65-
66-
private var xOffset: CGFloat {
67-
if offset > 0 {
68-
return offset
69-
}
70-
if !notificationManager.isNotificationVisible(notification) && !notification.isBeingDismissed {
71-
return 350 // Width of banner + padding
72-
}
73-
return 0
74-
}
75-
7659
var body: some View {
7760
VStack(spacing: 10) {
7861
HStack(alignment: .top, spacing: 10) {
@@ -164,32 +147,27 @@ struct NotificationBannerView: View {
164147
if !notification.isSticky && isHovering {
165148
Button(action: onDismiss) {
166149
Image(systemName: "xmark")
167-
.font(.system(size: 10))
168-
.foregroundColor(.secondary)
169-
.frame(width: 20, height: 20, alignment: .center)
170-
.background(.regularMaterial)
171-
.overlay(
172-
RoundedRectangle(cornerRadius: 10)
173-
.stroke(Color(nsColor: .separatorColor), lineWidth: 2)
174-
)
175-
.cornerRadius(10)
176-
.shadow(
177-
color: Color(.black.withAlphaComponent(colorScheme == .dark ? 0.2 : 0.1)),
178-
radius: 5,
179-
x: 0,
180-
y: 2
181-
)
182150
}
183-
.buttonStyle(.borderless)
151+
.buttonStyle(CloseButtonStyle())
184152
.padding(.top, -5)
185153
.padding(.leading, -5)
186154
.transition(.opacity)
187155
}
188156
}
189157
.frame(width: 300)
190-
.offset(x: xOffset)
191-
.opacity(opacity)
192-
.simultaneousGesture(dragGesture)
158+
.transition(.asymmetric(
159+
insertion: .move(edge: .trailing),
160+
removal: .modifier(
161+
active: DismissTransition(
162+
useOpactityTransition: notification.isBeingDismissed,
163+
isIdentity: false
164+
),
165+
identity: DismissTransition(
166+
useOpactityTransition: notification.isBeingDismissed,
167+
isIdentity: true
168+
)
169+
)
170+
))
193171
.onHover { hovering in
194172
withAnimation(.easeOut(duration: 0.2)) {
195173
isHovering = hovering
@@ -203,3 +181,14 @@ struct NotificationBannerView: View {
203181
}
204182
}
205183
}
184+
185+
struct DismissTransition: ViewModifier {
186+
let useOpactityTransition: Bool
187+
let isIdentity: Bool
188+
189+
func body(content: Content) -> some View {
190+
content
191+
.opacity(useOpactityTransition && !isIdentity ? 0 : 1)
192+
.offset(x: !useOpactityTransition && !isIdentity ? 350 : 0)
193+
}
194+
}

CodeEdit/Features/Notifications/Views/NotificationOverlayView.swift

Lines changed: 79 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -19,67 +19,100 @@ struct NotificationOverlayView: View {
1919
// Fixed width for notifications
2020
private let notificationWidth: CGFloat = 320 // 300 + 10 padding on each side
2121

22+
@State private var hasOverflow: Bool = false
23+
@State private var contentHeight: CGFloat = 0.0
24+
25+
private func updateOverflow(contentHeight: CGFloat, containerHeight: CGFloat) {
26+
if !hasOverflow && contentHeight > containerHeight {
27+
hasOverflow = true
28+
} else if hasOverflow && contentHeight <= containerHeight {
29+
hasOverflow = false
30+
}
31+
}
32+
2233
var notifications: some View {
2334
VStack(spacing: 8) {
24-
ForEach(notificationManager.activeNotifications, id: \.id) { notification in
25-
if controlActiveState == .active || controlActiveState == .key {
26-
NotificationBannerView(
27-
notification: notification,
28-
onDismiss: {
29-
notificationManager.dismissNotification(notification)
30-
},
31-
onAction: {
32-
notification.action()
33-
notificationManager.dismissNotification(notification)
34-
// Only hide if manually shown
35-
if notificationManager.isManuallyShown {
36-
notificationManager.toggleNotificationsVisibility()
37-
}
35+
ForEach(
36+
notificationManager.activeNotifications.filter {
37+
notificationManager.isNotificationVisible($0)
38+
},
39+
id: \.id
40+
) { notification in
41+
NotificationBannerView(
42+
notification: notification,
43+
onDismiss: {
44+
notificationManager.dismissNotification(notification)
45+
},
46+
onAction: {
47+
notification.action()
48+
notificationManager.dismissNotification(notification)
49+
// Only hide if manually shown
50+
if notificationManager.isManuallyShown {
51+
notificationManager.toggleNotificationsVisibility()
3852
}
39-
)
40-
.transition(.asymmetric(
41-
insertion: .move(edge: .trailing),
42-
removal: .opacity
43-
))
44-
}
53+
}
54+
)
4555
}
4656
}
4757
.padding(10)
4858
}
4959

50-
var body: some View {
51-
ViewThatFits(in: .vertical) {
52-
notifications
53-
.border(.red)
54-
GeometryReader { geometry in
55-
HStack {
56-
Spacer() // Push content to trailing edge
57-
ScrollViewReader { proxy in
58-
ScrollView(.vertical, showsIndicators: false) {
59-
VStack(spacing: 0) {
60-
// Invisible anchor view at the top to scroll back to when closed
61-
Color.clear.frame(height: 0).id(topID)
62-
notifications
63-
}
64-
.padding(.bottom, 30) // Account for the status bar
60+
var notificationsWithScrollView: some View {
61+
GeometryReader { geometry in
62+
HStack {
63+
Spacer() // Push content to trailing edge
64+
ScrollViewReader { proxy in
65+
ScrollView(.vertical, showsIndicators: false) {
66+
VStack(alignment: .trailing, spacing: 0) {
67+
// Invisible anchor view at the top to scroll back to when closed
68+
Color.clear.frame(height: 0).id(topID)
69+
notifications
6570
}
66-
.frame(width: notificationWidth)
67-
.frame(maxHeight: geometry.size.height)
68-
.scrollDisabled(!notificationManager.isManuallyShown)
69-
.onChange(of: notificationManager.isManuallyShown) { isShown in
70-
if !isShown {
71-
// Delay scroll animation until after notifications are hidden
72-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
73-
withAnimation(.easeOut(duration: 0.3)) {
74-
proxy.scrollTo(topID, anchor: .top)
75-
}
71+
.background(
72+
GeometryReader { proxy in
73+
Color.clear.onChange(of: proxy.size.height) { newValue in
74+
contentHeight = newValue
75+
updateOverflow(contentHeight: newValue, containerHeight: geometry.size.height)
76+
}
77+
}
78+
)
79+
}
80+
.frame(maxWidth: notificationWidth, alignment: .trailing)
81+
.frame(height: min(geometry.size.height, contentHeight))
82+
.scrollDisabled(!hasOverflow)
83+
.onChange(of: geometry.size.height) { newValue in
84+
updateOverflow(contentHeight: contentHeight, containerHeight: newValue)
85+
}
86+
.onChange(of: notificationManager.isManuallyShown) { isShown in
87+
if !isShown {
88+
// Delay scroll animation until after notifications are hidden
89+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
90+
withAnimation(.easeOut(duration: 0.3)) {
91+
proxy.scrollTo(topID, anchor: .top)
7692
}
7793
}
7894
}
7995
}
96+
.allowsHitTesting(
97+
notificationManager.activeNotifications
98+
.contains { notificationManager.isNotificationVisible($0) }
99+
)
80100
}
81-
.animation(.easeInOut(duration: 0.3), value: notificationManager.activeNotifications)
82101
}
83102
}
84103
}
104+
105+
var body: some View {
106+
Group {
107+
if #available(macOS 14.0, *) {
108+
notificationsWithScrollView
109+
.scrollClipDisabled(true)
110+
} else {
111+
notificationsWithScrollView
112+
}
113+
}
114+
.opacity(controlActiveState == .active || controlActiveState == .key ? 1 : 0)
115+
.offset(x: controlActiveState == .active || controlActiveState == .key ? 0 : 350)
116+
.animation(.easeInOut(duration: 0.2), value: controlActiveState)
117+
}
85118
}

CodeEdit/WorkspaceView.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,6 @@ struct WorkspaceView: View {
5252
focus: $focusedEditor
5353
)
5454
.frame(maxWidth: .infinity, maxHeight: .infinity)
55-
.overlay(alignment: .topTrailing) {
56-
NotificationOverlayView()
57-
}
5855
.onChange(of: geo.size.height) { newHeight in
5956
editorsHeight = newHeight
6057
}
@@ -106,6 +103,9 @@ struct WorkspaceView: View {
106103
}
107104
.accessibilityElement(children: .contain)
108105
}
106+
.overlay(alignment: .topTrailing) {
107+
NotificationOverlayView()
108+
}
109109
.onChange(of: focusedEditor) { newValue in
110110
/// update active tab group only if the new one is not the same with it.
111111
if let newValue, editorManager.activeEditor != newValue {

0 commit comments

Comments
 (0)