Skip to content

Instantly share code, notes, and snippets.

@krzyzanowskim
Last active July 13, 2022 12:58
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save krzyzanowskim/b78f1dce35efc513c223654ab0f9ef4e to your computer and use it in GitHub Desktop.
Save krzyzanowskim/b78f1dce35efc513c223654ab0f9ef4e to your computer and use it in GitHub Desktop.
import AppKit
import SwiftUI
/*
Text("Menu")
.popUpMenu {
NSMenuItem(title: "One", action: nil, keyEquivalent: "")
NSMenuItem(title: "Two", action: nil, keyEquivalent: "")
}
*/
struct STPopUpMenu<Label: View>: View {
@State private var isPresented: Bool = false
private var content: () -> [NSMenuItem]
private var label: () -> Label
init(@STMenuItemsBuilder content: @escaping () -> [NSMenuItem], @ViewBuilder label: @escaping () -> Label) {
self.content = content
self.label = label
}
var body: some View {
STPopUpMenuContent(
isPresented: $isPresented,
content: content,
label: {
label()
.contentShape(Rectangle())
.onTapGesture {
isPresented = true
}
}
)
}
}
private struct STPopUpMenuContent<Label: View>: NSViewRepresentable {
@Binding var isPresented: Bool
private var content: () -> [NSMenuItem]
private var label: () -> Label
init(isPresented: Binding<Bool>, @STMenuItemsBuilder content: @escaping () -> [NSMenuItem], @ViewBuilder label: @escaping () -> Label) {
self._isPresented = isPresented
self.content = content
self.label = label
}
func makeNSView(context: Context) -> NSHostingView<Label> {
NSHostingView(rootView: label())
}
func updateNSView(_ nsView: NSViewType, context: Context) {
context.coordinator.parent = self
// Update label value
nsView.rootView = label()
if isPresented, context.coordinator.currentMenu == nil {
let menu = NSMenu()
menu.items = content()
context.coordinator.currentMenu = menu
menu.delegate = context.coordinator
var location: NSPoint? = nil
if let currentEvent = NSApp.currentEvent {
location = nsView.convert(currentEvent.locationInWindow, from: nil)
}
Task { @MainActor in
context.coordinator.currentMenu?.popUp(
positioning: nil,
at: location ?? CGPoint(x: CGRectGetMidX(nsView.frame), y: CGRectGetMidY(nsView.frame)),
in: nsView
)
}
} else if !isPresented {
context.coordinator.currentMenu?.cancelTracking()
context.coordinator.currentMenu = nil
}
}
func makeCoordinator() -> MyCoordinator<Label> {
MyCoordinator(parent: self)
}
class MyCoordinator<Label: View>: NSObject, NSMenuDelegate {
var parent: STPopUpMenuContent<Label>
var currentMenu: NSMenu?
init(parent: STPopUpMenuContent<Label>) {
self.parent = parent
}
func menuDidClose(_ menu: NSMenu) {
parent.isPresented = false
}
}
}
@resultBuilder
struct STMenuItemsBuilder {
static func buildBlock() -> [NSMenuItem] { [] }
static func buildBlock(_ content: NSMenuItem...) -> [NSMenuItem] { content }
static func buildBlock(_ content: [NSMenuItem]) -> [NSMenuItem] { content }
static func buildArray(_ components: [[NSMenuItem]]) -> [NSMenuItem] { components.flatMap { $0 } }
}
extension View {
func popUpMenu(@STMenuItemsBuilder _ content: @escaping () -> [NSMenuItem]) -> some View {
STPopUpMenu(content: content) {
self
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment