Skip to content

Commit ffb0f8b

Browse files
authored
Instant Popovers (#1922)
- Added a custom view modifier to display popovers without animations - Updated existing popovers to use the new instant popover modifier
1 parent 9d743ec commit ffb0f8b

File tree

5 files changed

+140
-4
lines changed

5 files changed

+140
-4
lines changed

CodeEdit.xcodeproj/project.pbxproj

+4
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,7 @@
586586
B6CFD8112C20A8EE00E63F1A /* NSFont+WithWeight.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6CFD8102C20A8EE00E63F1A /* NSFont+WithWeight.swift */; };
587587
B6D7EA592971078500301FAC /* InspectorSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6D7EA582971078500301FAC /* InspectorSection.swift */; };
588588
B6D7EA5C297107DD00301FAC /* InspectorField.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6D7EA5B297107DD00301FAC /* InspectorField.swift */; };
589+
B6DCDAC62CCDE2B90099FBF9 /* InstantPopoverModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6DCDAC52CCDE2B90099FBF9 /* InstantPopoverModifier.swift */; };
589590
B6E41C7029DD157F0088F9F4 /* AccountsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E41C6F29DD157F0088F9F4 /* AccountsSettingsView.swift */; };
590591
B6E41C7429DD40010088F9F4 /* View+HideSidebarToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E41C7329DD40010088F9F4 /* View+HideSidebarToggle.swift */; };
591592
B6E41C7929DE02800088F9F4 /* AccountSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E41C7829DE02800088F9F4 /* AccountSelectionView.swift */; };
@@ -1250,6 +1251,7 @@
12501251
B6CFD8102C20A8EE00E63F1A /* NSFont+WithWeight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSFont+WithWeight.swift"; sourceTree = "<group>"; };
12511252
B6D7EA582971078500301FAC /* InspectorSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorSection.swift; sourceTree = "<group>"; };
12521253
B6D7EA5B297107DD00301FAC /* InspectorField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorField.swift; sourceTree = "<group>"; };
1254+
B6DCDAC52CCDE2B90099FBF9 /* InstantPopoverModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPopoverModifier.swift; sourceTree = "<group>"; };
12531255
B6E41C6F29DD157F0088F9F4 /* AccountsSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsSettingsView.swift; sourceTree = "<group>"; };
12541256
B6E41C7329DD40010088F9F4 /* View+HideSidebarToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+HideSidebarToggle.swift"; sourceTree = "<group>"; };
12551257
B6E41C7829DE02800088F9F4 /* AccountSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSelectionView.swift; sourceTree = "<group>"; };
@@ -2068,6 +2070,7 @@
20682070
6CBA0D502A1BF524002C6FAA /* SegmentedControlImproved.swift */,
20692071
587B9D8C29300ABD00AC7927 /* SettingsTextEditor.swift */,
20702072
587B9D8F29300ABD00AC7927 /* ToolbarBranchPicker.swift */,
2073+
B6DCDAC52CCDE2B90099FBF9 /* InstantPopoverModifier.swift */,
20712074
2897E1C62979A29200741E32 /* TrackableScrollView.swift */,
20722075
B60718302B15A9A3009CDAB4 /* CEOutlineGroup.swift */,
20732076
);
@@ -4032,6 +4035,7 @@
40324035
6CFF967429BEBCC300182D6F /* FindCommands.swift in Sources */,
40334036
587B9E6529301D8F00AC7927 /* GitLabGroupAccess.swift in Sources */,
40344037
6C91D57229B176FF0059A90D /* EditorManager.swift in Sources */,
4038+
B6DCDAC62CCDE2B90099FBF9 /* InstantPopoverModifier.swift in Sources */,
40354039
6C82D6BC29C00CD900495C54 /* FirstResponderPropertyWrapper.swift in Sources */,
40364040
58D01C9B293167DC00C5B6B4 /* CodeEditKeychainConstants.swift in Sources */,
40374041
B640A99E29E2184700715F20 /* SettingsForm.swift in Sources */,

CodeEdit/Features/ActivityViewer/Tasks/SchemeDropDownView.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ struct SchemeDropDownView: View {
5353
.onHover(perform: { hovering in
5454
self.isHoveringScheme = hovering
5555
})
56-
.popover(isPresented: $isSchemePopOverPresented, arrowEdge: .bottom) {
56+
.instantPopover(isPresented: $isSchemePopOverPresented, arrowEdge: .bottom) {
5757
VStack(alignment: .leading, spacing: 0) {
5858
WorkspaceMenuItemView(
5959
workspaceFileManager: workspaceFileManager,

CodeEdit/Features/ActivityViewer/Tasks/TaskDropDownView.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ struct TaskDropDownView: View {
3838
.onHover { hovering in
3939
self.isHoveringTasks = hovering
4040
}
41-
.popover(isPresented: $isTaskPopOverPresented, arrowEdge: .bottom) {
41+
.instantPopover(isPresented: $isTaskPopOverPresented, arrowEdge: .bottom) {
4242
taskPopoverContent
4343
}
4444
.onTapGesture {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
//
2+
// InstantPopoverModifier.swift
3+
// CodeEdit
4+
//
5+
// Created by Kihron on 10/26/24.
6+
//
7+
8+
import SwiftUI
9+
10+
struct InstantPopoverModifier<PopoverContent: View>: ViewModifier {
11+
@Binding var isPresented: Bool
12+
let arrowEdge: Edge
13+
let popoverContent: PopoverContent
14+
15+
func body(content: Content) -> some View {
16+
content
17+
.background(
18+
PopoverPresenter(
19+
isPresented: $isPresented,
20+
arrowEdge: arrowEdge,
21+
contentView: popoverContent
22+
)
23+
)
24+
}
25+
}
26+
27+
struct PopoverPresenter<ContentView: View>: NSViewRepresentable {
28+
@Binding var isPresented: Bool
29+
let arrowEdge: Edge
30+
let contentView: ContentView
31+
32+
func makeNSView(context: Context) -> NSView { NSView() }
33+
34+
func updateNSView(_ nsView: NSView, context: Context) {
35+
if isPresented, context.coordinator.popover == nil {
36+
let popover = NSPopover()
37+
popover.animates = false
38+
let hostingController = NSHostingController(rootView: contentView)
39+
40+
hostingController.view.layoutSubtreeIfNeeded()
41+
let contentSize = hostingController.view.fittingSize
42+
popover.contentSize = contentSize
43+
44+
popover.contentViewController = hostingController
45+
popover.delegate = context.coordinator
46+
popover.behavior = .semitransient
47+
48+
let nsRectEdge = edgeToNSRectEdge(arrowEdge)
49+
popover.show(relativeTo: nsView.bounds, of: nsView, preferredEdge: nsRectEdge)
50+
context.coordinator.popover = popover
51+
52+
if let parentWindow = nsView.window {
53+
context.coordinator.startObservingWindow(parentWindow)
54+
}
55+
} else if !isPresented, let popover = context.coordinator.popover {
56+
popover.close()
57+
context.coordinator.popover = nil
58+
}
59+
}
60+
61+
func makeCoordinator() -> Coordinator {
62+
Coordinator(isPresented: $isPresented)
63+
}
64+
65+
class Coordinator: NSObject, NSPopoverDelegate {
66+
@Binding var isPresented: Bool
67+
var popover: NSPopover?
68+
69+
init(isPresented: Binding<Bool>) {
70+
_isPresented = isPresented
71+
super.init()
72+
}
73+
74+
func startObservingWindow(_ window: NSWindow) {
75+
/// Observe when the window loses focus
76+
NotificationCenter.default.addObserver(
77+
forName: NSWindow.didResignKeyNotification,
78+
object: window,
79+
queue: .main
80+
) { [weak self] _ in
81+
guard let self = self else { return }
82+
/// The parent window is no longer focused, close the popover
83+
DispatchQueue.main.async {
84+
self.isPresented = false
85+
self.popover?.close()
86+
}
87+
}
88+
}
89+
90+
func popoverWillClose(_ notification: Notification) {
91+
DispatchQueue.main.async {
92+
self.isPresented = false
93+
}
94+
}
95+
96+
func popoverDidClose(_ notification: Notification) {
97+
popover = nil
98+
}
99+
}
100+
101+
private func edgeToNSRectEdge(_ edge: Edge) -> NSRectEdge {
102+
switch edge {
103+
case .top: return .minY
104+
case .leading: return .minX
105+
case .bottom: return .maxY
106+
case .trailing: return .maxX
107+
}
108+
}
109+
}
110+
111+
extension View {
112+
113+
/// A custom view modifier that presents a popover attached to the view with no animation.
114+
/// - Parameters:
115+
/// - isPresented: A binding to whether the popover is presented.
116+
/// - arrowEdge: The edge of the view that the popover points to. Defaults to `.bottom`.
117+
/// - content: A closure returning the content of the popover.
118+
/// - Returns: A view that presents a popover when `isPresented` is `true`.
119+
func instantPopover<Content: View>(
120+
isPresented: Binding<Bool>,
121+
arrowEdge: Edge = .bottom,
122+
@ViewBuilder content: () -> Content
123+
) -> some View {
124+
self.modifier(
125+
InstantPopoverModifier(
126+
isPresented: isPresented,
127+
arrowEdge: arrowEdge,
128+
popoverContent: content()
129+
)
130+
)
131+
}
132+
}

CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorItemView.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@ struct HistoryInspectorItemView: View {
1818
} set: { newValue in
1919
if newValue {
2020
selection = commit
21-
} else {
21+
} else if selection == commit {
2222
selection = nil
2323
}
2424
}
2525
}
2626

2727
var body: some View {
2828
CommitListItemView(commit: commit, showRef: false)
29-
.popover(isPresented: showPopup, arrowEdge: .leading) {
29+
.instantPopover(isPresented: showPopup, arrowEdge: .leading) {
3030
HistoryPopoverView(commit: commit)
3131
}
3232
}

0 commit comments

Comments
 (0)