Skip to content

Instant Popovers #1922

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CodeEdit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,7 @@
B6CFD8112C20A8EE00E63F1A /* NSFont+WithWeight.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6CFD8102C20A8EE00E63F1A /* NSFont+WithWeight.swift */; };
B6D7EA592971078500301FAC /* InspectorSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6D7EA582971078500301FAC /* InspectorSection.swift */; };
B6D7EA5C297107DD00301FAC /* InspectorField.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6D7EA5B297107DD00301FAC /* InspectorField.swift */; };
B6DCDAC62CCDE2B90099FBF9 /* InstantPopoverModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6DCDAC52CCDE2B90099FBF9 /* InstantPopoverModifier.swift */; };
B6E41C7029DD157F0088F9F4 /* AccountsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E41C6F29DD157F0088F9F4 /* AccountsSettingsView.swift */; };
B6E41C7429DD40010088F9F4 /* View+HideSidebarToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E41C7329DD40010088F9F4 /* View+HideSidebarToggle.swift */; };
B6E41C7929DE02800088F9F4 /* AccountSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E41C7829DE02800088F9F4 /* AccountSelectionView.swift */; };
Expand Down Expand Up @@ -1250,6 +1251,7 @@
B6CFD8102C20A8EE00E63F1A /* NSFont+WithWeight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSFont+WithWeight.swift"; sourceTree = "<group>"; };
B6D7EA582971078500301FAC /* InspectorSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorSection.swift; sourceTree = "<group>"; };
B6D7EA5B297107DD00301FAC /* InspectorField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorField.swift; sourceTree = "<group>"; };
B6DCDAC52CCDE2B90099FBF9 /* InstantPopoverModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPopoverModifier.swift; sourceTree = "<group>"; };
B6E41C6F29DD157F0088F9F4 /* AccountsSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsSettingsView.swift; sourceTree = "<group>"; };
B6E41C7329DD40010088F9F4 /* View+HideSidebarToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+HideSidebarToggle.swift"; sourceTree = "<group>"; };
B6E41C7829DE02800088F9F4 /* AccountSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSelectionView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2068,6 +2070,7 @@
6CBA0D502A1BF524002C6FAA /* SegmentedControlImproved.swift */,
587B9D8C29300ABD00AC7927 /* SettingsTextEditor.swift */,
587B9D8F29300ABD00AC7927 /* ToolbarBranchPicker.swift */,
B6DCDAC52CCDE2B90099FBF9 /* InstantPopoverModifier.swift */,
2897E1C62979A29200741E32 /* TrackableScrollView.swift */,
B60718302B15A9A3009CDAB4 /* CEOutlineGroup.swift */,
);
Expand Down Expand Up @@ -4032,6 +4035,7 @@
6CFF967429BEBCC300182D6F /* FindCommands.swift in Sources */,
587B9E6529301D8F00AC7927 /* GitLabGroupAccess.swift in Sources */,
6C91D57229B176FF0059A90D /* EditorManager.swift in Sources */,
B6DCDAC62CCDE2B90099FBF9 /* InstantPopoverModifier.swift in Sources */,
6C82D6BC29C00CD900495C54 /* FirstResponderPropertyWrapper.swift in Sources */,
58D01C9B293167DC00C5B6B4 /* CodeEditKeychainConstants.swift in Sources */,
B640A99E29E2184700715F20 /* SettingsForm.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ struct SchemeDropDownView: View {
.onHover(perform: { hovering in
self.isHoveringScheme = hovering
})
.popover(isPresented: $isSchemePopOverPresented, arrowEdge: .bottom) {
.instantPopover(isPresented: $isSchemePopOverPresented, arrowEdge: .bottom) {
VStack(alignment: .leading, spacing: 0) {
WorkspaceMenuItemView(
workspaceFileManager: workspaceFileManager,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ struct TaskDropDownView: View {
.onHover { hovering in
self.isHoveringTasks = hovering
}
.popover(isPresented: $isTaskPopOverPresented, arrowEdge: .bottom) {
.instantPopover(isPresented: $isTaskPopOverPresented, arrowEdge: .bottom) {
taskPopoverContent
}
.onTapGesture {
Expand Down
132 changes: 132 additions & 0 deletions CodeEdit/Features/CodeEditUI/Views/InstantPopoverModifier.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
//
// InstantPopoverModifier.swift
// CodeEdit
//
// Created by Kihron on 10/26/24.
//

import SwiftUI

struct InstantPopoverModifier<PopoverContent: View>: ViewModifier {
@Binding var isPresented: Bool
let arrowEdge: Edge
let popoverContent: PopoverContent

func body(content: Content) -> some View {
content
.background(
PopoverPresenter(
isPresented: $isPresented,
arrowEdge: arrowEdge,
contentView: popoverContent
)
)
}
}

struct PopoverPresenter<ContentView: View>: NSViewRepresentable {
@Binding var isPresented: Bool
let arrowEdge: Edge
let contentView: ContentView

func makeNSView(context: Context) -> NSView { NSView() }

func updateNSView(_ nsView: NSView, context: Context) {
if isPresented, context.coordinator.popover == nil {
let popover = NSPopover()
popover.animates = false
let hostingController = NSHostingController(rootView: contentView)

hostingController.view.layoutSubtreeIfNeeded()
let contentSize = hostingController.view.fittingSize
popover.contentSize = contentSize

popover.contentViewController = hostingController
popover.delegate = context.coordinator
popover.behavior = .semitransient

let nsRectEdge = edgeToNSRectEdge(arrowEdge)
popover.show(relativeTo: nsView.bounds, of: nsView, preferredEdge: nsRectEdge)
context.coordinator.popover = popover

if let parentWindow = nsView.window {
context.coordinator.startObservingWindow(parentWindow)
}
} else if !isPresented, let popover = context.coordinator.popover {
popover.close()
context.coordinator.popover = nil
}
}

func makeCoordinator() -> Coordinator {
Coordinator(isPresented: $isPresented)
}

class Coordinator: NSObject, NSPopoverDelegate {
@Binding var isPresented: Bool
var popover: NSPopover?

init(isPresented: Binding<Bool>) {
_isPresented = isPresented
super.init()
}

func startObservingWindow(_ window: NSWindow) {
/// Observe when the window loses focus
NotificationCenter.default.addObserver(
forName: NSWindow.didResignKeyNotification,
object: window,
queue: .main
) { [weak self] _ in
guard let self = self else { return }
/// The parent window is no longer focused, close the popover
DispatchQueue.main.async {
self.isPresented = false
self.popover?.close()
}
}
}

func popoverWillClose(_ notification: Notification) {
DispatchQueue.main.async {
self.isPresented = false
}
}

func popoverDidClose(_ notification: Notification) {
popover = nil
}
}

private func edgeToNSRectEdge(_ edge: Edge) -> NSRectEdge {
switch edge {
case .top: return .minY
case .leading: return .minX
case .bottom: return .maxY
case .trailing: return .maxX
}
}
}

extension View {

/// A custom view modifier that presents a popover attached to the view with no animation.
/// - Parameters:
/// - isPresented: A binding to whether the popover is presented.
/// - arrowEdge: The edge of the view that the popover points to. Defaults to `.bottom`.
/// - content: A closure returning the content of the popover.
/// - Returns: A view that presents a popover when `isPresented` is `true`.
func instantPopover<Content: View>(
isPresented: Binding<Bool>,
arrowEdge: Edge = .bottom,
@ViewBuilder content: () -> Content
) -> some View {
self.modifier(
InstantPopoverModifier(
isPresented: isPresented,
arrowEdge: arrowEdge,
popoverContent: content()
)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ struct HistoryInspectorItemView: View {
} set: { newValue in
if newValue {
selection = commit
} else {
} else if selection == commit {
selection = nil
}
}
}

var body: some View {
CommitListItemView(commit: commit, showRef: false)
.popover(isPresented: showPopup, arrowEdge: .leading) {
.instantPopover(isPresented: showPopup, arrowEdge: .leading) {
HistoryPopoverView(commit: commit)
}
}
Expand Down
Loading