From ffa74d641c2ccc8feceee527432bde802a85a5e3 Mon Sep 17 00:00:00 2001 From: cmux Date: Fri, 6 Mar 2026 13:59:04 -0800 Subject: [PATCH] Hide help menu popover arrow Replace the SwiftUI .popover with an NSPopover presented via NSViewRepresentable that uses the shouldHideAnchor KVC trick to hide the arrow. The positioning rect is shifted toward the preferred edge to compensate for the hidden arrow's space. Co-Authored-By: Claude Opus 4.6 --- Sources/ContentView.swift | 76 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 588eba67..1fb28ebe 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -7492,9 +7492,12 @@ private struct SidebarHelpMenuButton: View { } .buttonStyle(SidebarFooterIconButtonStyle()) .frame(width: buttonSize, height: buttonSize, alignment: .center) - .popover(isPresented: $isPopoverPresented, arrowEdge: .bottom) { + .background(ArrowlessPopoverAnchor( + isPresented: $isPopoverPresented, + preferredEdge: .maxY + ) { helpPopover - } + }) .accessibilityElement(children: .ignore) .help(helpTitle) .accessibilityLabel(helpTitle) @@ -7655,6 +7658,75 @@ private struct SidebarHelpMenuButton: View { } } +/// Presents an NSPopover without an arrow using the shouldHideAnchor KVC trick. +private struct ArrowlessPopoverAnchor: NSViewRepresentable { + @Binding var isPresented: Bool + let preferredEdge: NSRectEdge + @ViewBuilder let content: () -> PopoverContent + + func makeNSView(context: Context) -> NSView { + NSView() + } + + func updateNSView(_ nsView: NSView, context: Context) { + if isPresented { + guard context.coordinator.popover == nil else { return } + + let popover = NSPopover() + popover.behavior = .semitransient + popover.animates = true + popover.setValue(true, forKeyPath: "shouldHideAnchor") + popover.contentViewController = NSHostingController(rootView: content()) + popover.delegate = context.coordinator + context.coordinator.popover = popover + + // Show relative to a rect shifted toward the preferred edge to close + // the gap left by the hidden arrow (~13pt arrow height). + let arrowCompensation: CGFloat = 13 + var rect = nsView.bounds + switch preferredEdge { + case .maxY: + rect = NSRect(x: rect.minX, y: rect.maxY - arrowCompensation, width: rect.width, height: arrowCompensation) + case .minY: + rect = NSRect(x: rect.minX, y: rect.minY, width: rect.width, height: arrowCompensation) + case .maxX: + rect = NSRect(x: rect.maxX - arrowCompensation, y: rect.minY, width: arrowCompensation, height: rect.height) + case .minX: + rect = NSRect(x: rect.minX, y: rect.minY, width: arrowCompensation, height: rect.height) + default: break + } + popover.show(relativeTo: rect, of: nsView, preferredEdge: preferredEdge) + } else { + context.coordinator.dismiss() + } + } + + func makeCoordinator() -> Coordinator { + Coordinator(isPresented: $isPresented) + } + + final class Coordinator: NSObject, NSPopoverDelegate { + @Binding var isPresented: Bool + var popover: NSPopover? + + init(isPresented: Binding) { + _isPresented = isPresented + } + + func dismiss() { + popover?.performClose(nil) + popover = nil + } + + func popoverDidClose(_ notification: Notification) { + popover = nil + if isPresented { + isPresented = false + } + } + } +} + private struct SidebarFooterIconButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { SidebarFooterIconButtonStyleBody(configuration: configuration)