Skip to content

Commit 66035bf

Browse files
committed
Addded Bezel Notification to editor and firing a bezel notification when looping find results or submitting when there is no result. Commented emphasize logic back in.
1 parent 8a028a3 commit 66035bf

File tree

5 files changed

+348
-62
lines changed

5 files changed

+348
-62
lines changed

Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/CodeEditSourceEditorExampleApp.swift

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,6 @@ struct CodeEditSourceEditorExampleApp: App {
1212
var body: some Scene {
1313
DocumentGroup(newDocument: CodeEditSourceEditorExampleDocument()) { file in
1414
ContentView(document: file.$document, fileURL: file.fileURL)
15-
.toolbar {
16-
Button("Toolbar Item") {
17-
print("Toolbar Item Pressed")
18-
}
19-
}
2015
}
2116
.windowToolbarStyle(.unifiedCompact)
2217
}

Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Views/ContentView.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,10 @@ struct ContentView: View {
4444
cursorPositions: $cursorPositions,
4545
useThemeBackground: true,
4646
highlightProviders: [treeSitterClient],
47-
contentInsets: NSEdgeInsets(top: proxy.safeAreaInsets.top, left: 0, bottom: 0, right: 0),
47+
contentInsets: NSEdgeInsets(top: proxy.safeAreaInsets.top, left: 0, bottom: 28.0, right: 0),
4848
useSystemCursor: useSystemCursor
4949
)
50-
.safeAreaInset(edge: .bottom, spacing: 0) {
50+
.overlay(alignment: .bottom) {
5151
HStack {
5252
Toggle("Wrap Lines", isOn: $wrapLines)
5353
.toggleStyle(.button)
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
//
2+
// BezelNotification.swift
3+
// CodeEditSourceEditor
4+
//
5+
// Created by Austin Condiff on 3/17/25.
6+
//
7+
8+
import AppKit
9+
import SwiftUI
10+
11+
/// A utility class for showing temporary bezel notifications with SF Symbols
12+
final class BezelNotification {
13+
private static var shared = BezelNotification()
14+
private var window: NSWindow?
15+
private var hostingView: NSHostingView<BezelView>?
16+
private var frameObserver: NSObjectProtocol?
17+
private var targetView: NSView?
18+
private var hideTimer: DispatchWorkItem?
19+
20+
private init() {}
21+
22+
deinit {
23+
if let observer = frameObserver {
24+
NotificationCenter.default.removeObserver(observer)
25+
}
26+
}
27+
28+
/// Shows a bezel notification with the given SF Symbol name
29+
/// - Parameters:
30+
/// - symbolName: The name of the SF Symbol to display
31+
/// - over: The view to center the bezel over
32+
/// - duration: How long to show the bezel for (defaults to 0.75 seconds)
33+
static func show(symbolName: String, over view: NSView, duration: TimeInterval = 0.75) {
34+
shared.showBezel(symbolName: symbolName, over: view, duration: duration)
35+
}
36+
37+
private func showBezel(symbolName: String, over view: NSView, duration: TimeInterval) {
38+
// Cancel any existing hide timer
39+
hideTimer?.cancel()
40+
hideTimer = nil
41+
42+
// Close existing window if any
43+
cleanup()
44+
45+
self.targetView = view
46+
47+
// Create the window and view
48+
let bezelContent = BezelView(symbolName: symbolName)
49+
let hostingView = NSHostingView(rootView: bezelContent)
50+
self.hostingView = hostingView
51+
52+
let window = NSPanel(
53+
contentRect: .zero,
54+
styleMask: [.borderless, .nonactivatingPanel, .hudWindow],
55+
backing: .buffered,
56+
defer: true
57+
)
58+
window.backgroundColor = .clear
59+
window.isOpaque = false
60+
window.hasShadow = false
61+
window.level = .floating
62+
window.contentView = hostingView
63+
window.isMovable = false
64+
window.isReleasedWhenClosed = false
65+
66+
// Make it a child window that moves with the parent
67+
if let parentWindow = view.window {
68+
parentWindow.addChildWindow(window, ordered: .above)
69+
}
70+
71+
self.window = window
72+
73+
// Size and position the window
74+
let size = NSSize(width: 110, height: 110)
75+
hostingView.frame.size = size
76+
77+
// Initial position
78+
updateBezelPosition()
79+
80+
// Observe frame changes
81+
frameObserver = NotificationCenter.default.addObserver(
82+
forName: NSView.frameDidChangeNotification,
83+
object: view,
84+
queue: .main
85+
) { [weak self] _ in
86+
self?.updateBezelPosition()
87+
}
88+
89+
// Show immediately without fade
90+
window.alphaValue = 1
91+
window.orderFront(nil)
92+
93+
// Schedule hide
94+
let timer = DispatchWorkItem { [weak self] in
95+
self?.dismiss()
96+
}
97+
self.hideTimer = timer
98+
DispatchQueue.main.asyncAfter(deadline: .now() + duration, execute: timer)
99+
}
100+
101+
private func updateBezelPosition() {
102+
guard let window = window,
103+
let view = targetView else { return }
104+
105+
let size = NSSize(width: 110, height: 110)
106+
107+
// Position relative to the view's content area
108+
let visibleRect: NSRect
109+
if let scrollView = view.enclosingScrollView {
110+
// Get the visible rect in the scroll view's coordinate space
111+
visibleRect = scrollView.contentView.visibleRect
112+
} else {
113+
visibleRect = view.bounds
114+
}
115+
116+
// Convert visible rect to window coordinates
117+
let viewFrameInWindow = view.enclosingScrollView?.contentView.convert(visibleRect, to: nil)
118+
?? view.convert(visibleRect, to: nil)
119+
guard let screenFrame = view.window?.convertToScreen(viewFrameInWindow) else { return }
120+
121+
// Calculate center position relative to the visible content area
122+
let xPos = screenFrame.midX - (size.width / 2)
123+
let yPos = screenFrame.midY - (size.height / 2)
124+
125+
// Update frame
126+
let bezelFrame = NSRect(origin: NSPoint(x: xPos, y: yPos), size: size)
127+
window.setFrame(bezelFrame, display: true)
128+
}
129+
130+
private func cleanup() {
131+
// Cancel any existing hide timer
132+
hideTimer?.cancel()
133+
hideTimer = nil
134+
135+
// Remove frame observer
136+
if let observer = frameObserver {
137+
NotificationCenter.default.removeObserver(observer)
138+
frameObserver = nil
139+
}
140+
141+
// Remove child window relationship
142+
if let window = window, let parentWindow = window.parent {
143+
parentWindow.removeChildWindow(window)
144+
}
145+
146+
// Close and clean up window
147+
window?.orderOut(nil) // Ensure window is removed from screen
148+
window?.close()
149+
window = nil
150+
151+
// Clean up hosting view
152+
hostingView?.removeFromSuperview()
153+
hostingView = nil
154+
155+
// Clear target view reference
156+
targetView = nil
157+
}
158+
159+
private func dismiss() {
160+
guard let window = window else { return }
161+
162+
NSAnimationContext.runAnimationGroup({ context in
163+
context.duration = 0.15
164+
window.animator().alphaValue = 0
165+
}, completionHandler: { [weak self] in
166+
self?.cleanup()
167+
})
168+
}
169+
}
170+
171+
/// The SwiftUI view for the bezel content
172+
private struct BezelView: View {
173+
let symbolName: String
174+
175+
var body: some View {
176+
Image(systemName: symbolName)
177+
.imageScale(.large)
178+
.font(.system(size: 56, weight: .thin))
179+
.foregroundStyle(.secondary)
180+
.frame(width: 110, height: 110)
181+
.background(.ultraThinMaterial)
182+
.clipShape(RoundedRectangle(cornerSize: CGSize(width: 18.0, height: 18.0)))
183+
}
184+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
//
2+
// EffectView.swift
3+
// CodeEditModules/CodeEditUI
4+
//
5+
// Created by Rehatbir Singh on 15/03/2022.
6+
//
7+
8+
import SwiftUI
9+
10+
/// A SwiftUI Wrapper for `NSVisualEffectView`
11+
///
12+
/// ## Usage
13+
/// ```swift
14+
/// EffectView(material: .headerView, blendingMode: .withinWindow)
15+
/// ```
16+
struct EffectView: NSViewRepresentable {
17+
private let material: NSVisualEffectView.Material
18+
private let blendingMode: NSVisualEffectView.BlendingMode
19+
private let emphasized: Bool
20+
21+
/// Initializes the
22+
/// [`NSVisualEffectView`](https://developer.apple.com/documentation/appkit/nsvisualeffectview)
23+
/// with a
24+
/// [`Material`](https://developer.apple.com/documentation/appkit/nsvisualeffectview/material) and
25+
/// [`BlendingMode`](https://developer.apple.com/documentation/appkit/nsvisualeffectview/blendingmode)
26+
///
27+
/// By setting the
28+
/// [`emphasized`](https://developer.apple.com/documentation/appkit/nsvisualeffectview/1644721-isemphasized)
29+
/// flag, the emphasized state of the material will be used if available.
30+
///
31+
/// - Parameters:
32+
/// - material: The material to use. Defaults to `.headerView`.
33+
/// - blendingMode: The blending mode to use. Defaults to `.withinWindow`.
34+
/// - emphasized:A Boolean value indicating whether to emphasize the look of the material. Defaults to `false`.
35+
init(
36+
_ material: NSVisualEffectView.Material = .headerView,
37+
blendingMode: NSVisualEffectView.BlendingMode = .withinWindow,
38+
emphasized: Bool = false
39+
) {
40+
self.material = material
41+
self.blendingMode = blendingMode
42+
self.emphasized = emphasized
43+
}
44+
45+
func makeNSView(context: Context) -> NSVisualEffectView {
46+
let view = NSVisualEffectView()
47+
view.material = material
48+
view.blendingMode = blendingMode
49+
view.isEmphasized = emphasized
50+
view.state = .followsWindowActiveState
51+
return view
52+
}
53+
54+
func updateNSView(_ nsView: NSVisualEffectView, context: Context) {
55+
nsView.material = material
56+
nsView.blendingMode = blendingMode
57+
}
58+
59+
/// Returns the system selection style as an ``EffectView`` if the `condition` is met.
60+
/// Otherwise it returns `Color.clear`
61+
///
62+
/// - Parameter condition: The condition of when to apply the background. Defaults to `true`.
63+
/// - Returns: A View
64+
@ViewBuilder
65+
static func selectionBackground(_ condition: Bool = true) -> some View {
66+
if condition {
67+
EffectView(.selection, blendingMode: .withinWindow, emphasized: true)
68+
} else {
69+
Color.clear
70+
}
71+
}
72+
}

0 commit comments

Comments
 (0)