-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
ContentView drain: command palette (stack E) #6029
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
Changes from 4 commits
Commits
Show all changes
39 commits
Select commit
Hold shift + click to select a range
a8543f1
ContentView drain: lift command-palette domain into CmuxCommandPalett…
azooz2003-bit 7bb426d
ContentView drain: complete command-palette test cutover (stack E)
azooz2003-bit d017279
ContentView drain: remove leftover duplicate overlay-promotion policy…
azooz2003-bit 521da34
ContentView drain: lift AppKit-support primitives into CmuxAppKitSupp…
azooz2003-bit b9d9911
ContentView drain: lift NSColor.hexString into CmuxFoundation (stack …
azooz2003-bit cd036a7
ContentView drain: lift String.nilIfEmpty into CmuxFoundation (stack …
azooz2003-bit cffc1f3
ContentView drain: import CmuxFoundation in cmuxTests callers of NSCo…
azooz2003-bit 6f0aadd
ContentView drain: lift sidebar scroll-view resolver cluster into Cmu…
azooz2003-bit ff864cf
ContentView drain: retarget SidebarScrollViewConfiguratorTests to Cmu…
azooz2003-bit fdc6420
ContentView drain: lift feedback composer domain into CmuxFeedback (s…
azooz2003-bit 06ff565
ContentView drain: lift feedback composer message-editor views into C…
azooz2003-bit c9ac520
ContentView drain: link CmuxAppKitSupportUI into cmuxTests target (st…
azooz2003-bit 656773c
ContentView drain: retarget app-host palette tests to CmuxCommandPale…
azooz2003-bit 48f9614
ContentView drain: make SidebarScrollViewResolverView.resolveScrollVi…
azooz2003-bit 8373ccc
ContentView drain: link CmuxCommandPalette into cmuxTests target (sta…
azooz2003-bit fc6d171
ContentView drain: fix CmuxAppKitSupportUI strict-concurrency errors …
azooz2003-bit b561670
ContentView drain: refresh file-length budget for palette-test import…
azooz2003-bit 48bcea5
ContentView drain: import CmuxCommandPalette in CommandPaletteNucleoF…
azooz2003-bit 393fe30
ContentView drain: retarget palette fingerprint test calls to package…
azooz2003-bit 5cdf4bd
ContentView drain: lift sidebar drag auto-scroll domain to CmuxAppKit…
azooz2003-bit b0c1ca6
ContentView drain: lift sidebar drop planner to CmuxFoundation (stack E)
azooz2003-bit 6720428
ContentView drain: lift sidebar tab drop-indicator predicate to CmuxF…
azooz2003-bit ec97502
ContentView drain: lift browser-stack drop planner to CmuxSidebarProv…
azooz2003-bit 307fcfa
ContentView drain: lift sidebar drag-lifecycle policies to CmuxFounda…
azooz2003-bit cec2995
ContentView drain: wire CmuxSidebarProviderKit into cmuxTests target …
azooz2003-bit f36cdfb
Merge remote-tracking branch 'origin/main' into feat-contentview-drain
azooz2003-bit 57df083
ContentView drain: import CmuxFoundation in two lifted-type test cons…
azooz2003-bit 1675a29
ContentView drain: lift SidebarFeedbackComposerSheet to CmuxFeedbackUI
azooz2003-bit 547fd6a
ContentView drain: lift shortcut-hint + dev-banner + markdown leaves …
azooz2003-bit 2196d23
ContentView drain: lift SidebarWorkspaceSelectionSyncPolicy to CmuxFo…
azooz2003-bit 68a2620
ContentView drain: lift SidebarDragLifecycleNotification to CmuxFound…
azooz2003-bit 743c2ae
Merge remote-tracking branch 'origin/main' into feat-contentview-drain
azooz2003-bit de016c5
Merge origin/main into feat-contentview-drain (re-sync after Wave-3 m…
azooz2003-bit b0f340a
ContentView drain: mark lifted policy/value namespace enums lint:allow
azooz2003-bit 7978cb2
Merge origin/main into feat-contentview-drain (re-sync #2 after #6067)
azooz2003-bit e878eb0
Merge remote-tracking branch 'origin/main' into feat-contentview-drain
azooz2003-bit 3e88c09
Merge remote-tracking branch 'origin/main' into feat-contentview-drain
azooz2003-bit a271bd9
Merge remote-tracking branch 'origin/main' into feat-contentview-drain
azooz2003-bit f2bd672
Merge remote-tracking branch 'origin/main' into feat-contentview-drain
azooz2003-bit File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| // swift-tools-version: 6.0 | ||
|
|
||
| import PackageDescription | ||
|
|
||
| let package = Package( | ||
| name: "CmuxAppKitSupportUI", | ||
| platforms: [ | ||
| .macOS(.v14), | ||
| ], | ||
| products: [ | ||
| .library( | ||
| name: "CmuxAppKitSupportUI", | ||
| targets: ["CmuxAppKitSupportUI"] | ||
| ), | ||
| ], | ||
| targets: [ | ||
| .target( | ||
| name: "CmuxAppKitSupportUI", | ||
| swiftSettings: [ | ||
| .swiftLanguageMode(.v6), | ||
| .enableUpcomingFeature("ExistentialAny"), | ||
| .enableUpcomingFeature("InternalImportsByDefault"), | ||
| ] | ||
| ), | ||
| .testTarget( | ||
| name: "CmuxAppKitSupportUITests", | ||
| dependencies: ["CmuxAppKitSupportUI"], | ||
| swiftSettings: [ | ||
| .swiftLanguageMode(.v6), | ||
| .enableUpcomingFeature("ExistentialAny"), | ||
| .enableUpcomingFeature("InternalImportsByDefault"), | ||
| ] | ||
| ), | ||
| ] | ||
| ) |
24 changes: 24 additions & 0 deletions
24
Packages/CmuxAppKitSupportUI/Sources/CmuxAppKitSupportUI/Mouse/MiddleClickCapture.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import AppKit | ||
| public import SwiftUI | ||
|
|
||
| /// A transparent overlay that intercepts only middle-mouse clicks, letting left-click | ||
| /// selection and right-click context menus hit-test through to the underlying view tree. | ||
| public struct MiddleClickCapture: NSViewRepresentable { | ||
| public let onMiddleClick: () -> Void | ||
|
|
||
| /// Creates a middle-click capture overlay. | ||
| /// - Parameter onMiddleClick: Invoked when a middle (button 2) click lands on the overlay. | ||
| public init(onMiddleClick: @escaping () -> Void) { | ||
| self.onMiddleClick = onMiddleClick | ||
| } | ||
|
|
||
| public func makeNSView(context: Context) -> MiddleClickCaptureView { | ||
| let view = MiddleClickCaptureView() | ||
| view.onMiddleClick = onMiddleClick | ||
| return view | ||
| } | ||
|
|
||
| public func updateNSView(_ nsView: MiddleClickCaptureView, context: Context) { | ||
| nsView.onMiddleClick = onMiddleClick | ||
| } | ||
| } |
26 changes: 26 additions & 0 deletions
26
Packages/CmuxAppKitSupportUI/Sources/CmuxAppKitSupportUI/Mouse/MiddleClickCaptureView.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| public import AppKit | ||
|
|
||
| /// Backing `NSView` for ``MiddleClickCapture`` that hit-tests only middle-clicks. | ||
| public final class MiddleClickCaptureView: NSView { | ||
| /// Invoked when a middle (button 2) mouse-down lands on this view. | ||
| public var onMiddleClick: (() -> Void)? | ||
|
|
||
| public override func hitTest(_ point: NSPoint) -> NSView? { | ||
| // Only intercept middle-click so left-click selection and right-click context menus | ||
| // continue to hit-test through to SwiftUI/AppKit normally. | ||
| guard let event = NSApp.currentEvent, | ||
| event.type == .otherMouseDown, | ||
| event.buttonNumber == 2 else { | ||
| return nil | ||
| } | ||
| return self | ||
| } | ||
|
|
||
| public override func otherMouseDown(with event: NSEvent) { | ||
| guard event.buttonNumber == 2 else { | ||
| super.otherMouseDown(with: event) | ||
| return | ||
| } | ||
| onMiddleClick?() | ||
| } | ||
| } |
174 changes: 174 additions & 0 deletions
174
...ages/CmuxAppKitSupportUI/Sources/CmuxAppKitSupportUI/Popover/ArrowlessPopoverAnchor.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,174 @@ | ||
| public import AppKit | ||
| public import SwiftUI | ||
|
|
||
| /// An `NSViewRepresentable` that presents SwiftUI content in an `NSPopover` with the | ||
| /// popover arrow hidden, anchored to an invisible SwiftUI-backed view. | ||
| /// | ||
| /// The popover is positioned relative to a synthetic rect inset toward the anchor so the | ||
| /// detached content sits a fixed gap from the anchoring edge while the arrow stays hidden. | ||
| public struct ArrowlessPopoverAnchor<PopoverContent: View>: NSViewRepresentable { | ||
| @Binding public var isPresented: Bool | ||
| public let preferredEdge: NSRectEdge | ||
| public let detachedGap: CGFloat | ||
| @ViewBuilder public let content: () -> PopoverContent | ||
|
|
||
| /// Creates an arrowless popover anchor. | ||
| /// - Parameters: | ||
| /// - isPresented: Binding driving popover presentation. | ||
| /// - preferredEdge: The edge of the anchor the popover prefers to appear from. | ||
| /// - detachedGap: The gap, in points, between the anchor edge and the popover. | ||
| /// - content: The SwiftUI content rendered inside the popover. | ||
| public init( | ||
| isPresented: Binding<Bool>, | ||
| preferredEdge: NSRectEdge, | ||
| detachedGap: CGFloat, | ||
| @ViewBuilder content: @escaping () -> PopoverContent | ||
| ) { | ||
| self._isPresented = isPresented | ||
| self.preferredEdge = preferredEdge | ||
| self.detachedGap = detachedGap | ||
| self.content = content | ||
| } | ||
|
|
||
| public func makeNSView(context: Context) -> NSView { | ||
| let view = NSView() | ||
| context.coordinator.anchorView = view | ||
| return view | ||
| } | ||
|
|
||
| public func updateNSView(_ nsView: NSView, context: Context) { | ||
| context.coordinator.anchorView = nsView | ||
| context.coordinator.updateRootView(AnyView(content())) | ||
|
|
||
| if isPresented { | ||
| context.coordinator.present( | ||
| preferredEdge: preferredEdge, | ||
| detachedGap: detachedGap | ||
| ) | ||
| } else { | ||
| context.coordinator.dismiss() | ||
| } | ||
| } | ||
|
|
||
| public func makeCoordinator() -> Coordinator { | ||
| Coordinator(isPresented: $isPresented) | ||
| } | ||
|
|
||
| /// Bridges popover lifecycle between AppKit's `NSPopover` and the SwiftUI binding. | ||
| @MainActor | ||
| public final class Coordinator: NSObject, NSPopoverDelegate { | ||
| @Binding var isPresented: Bool | ||
|
|
||
| weak var anchorView: NSView? | ||
| private let hostingController = NSHostingController(rootView: AnyView(EmptyView())) | ||
| private var popover: NSPopover? | ||
|
|
||
| init(isPresented: Binding<Bool>) { | ||
| _isPresented = isPresented | ||
| } | ||
|
|
||
| func updateRootView(_ rootView: AnyView) { | ||
| hostingController.rootView = AnyView(rootView.fixedSize()) | ||
| hostingController.view.invalidateIntrinsicContentSize() | ||
| hostingController.view.layoutSubtreeIfNeeded() | ||
| } | ||
|
|
||
| func present(preferredEdge: NSRectEdge, detachedGap: CGFloat) { | ||
| guard let anchorView else { | ||
| isPresented = false | ||
| dismiss() | ||
| return | ||
| } | ||
|
|
||
| let popover = popover ?? makePopover() | ||
| if popover.isShown { | ||
| return | ||
| } | ||
|
|
||
| hostingController.view.invalidateIntrinsicContentSize() | ||
| hostingController.view.layoutSubtreeIfNeeded() | ||
| let fittingSize = hostingController.view.fittingSize | ||
| if fittingSize.width > 0, fittingSize.height > 0 { | ||
| popover.contentSize = NSSize( | ||
| width: ceil(fittingSize.width), | ||
| height: ceil(fittingSize.height) | ||
| ) | ||
| } | ||
|
|
||
| popover.show( | ||
| relativeTo: positioningRect( | ||
| for: anchorView.bounds, | ||
| preferredEdge: preferredEdge, | ||
| detachedGap: detachedGap | ||
| ), | ||
| of: anchorView, | ||
| preferredEdge: preferredEdge | ||
| ) | ||
| } | ||
|
|
||
| func dismiss() { | ||
| popover?.performClose(nil) | ||
| popover = nil | ||
| } | ||
|
|
||
| public func popoverDidClose(_ notification: Notification) { | ||
| popover = nil | ||
| if isPresented { | ||
| isPresented = false | ||
| } | ||
| } | ||
|
|
||
| private func makePopover() -> NSPopover { | ||
| let popover = NSPopover() | ||
| popover.behavior = .semitransient | ||
| popover.animates = true | ||
| popover.setValue(true, forKeyPath: "shouldHideAnchor") | ||
| popover.contentViewController = hostingController | ||
| popover.delegate = self | ||
| self.popover = popover | ||
| return popover | ||
| } | ||
|
|
||
| private func positioningRect( | ||
| for bounds: CGRect, | ||
| preferredEdge: NSRectEdge, | ||
| detachedGap: CGFloat | ||
| ) -> CGRect { | ||
| let hiddenArrowInset: CGFloat = 13 | ||
| let compensation = max(hiddenArrowInset - detachedGap, 0) | ||
|
|
||
| switch preferredEdge { | ||
| case .maxY: | ||
| return NSRect( | ||
| x: bounds.minX, | ||
| y: bounds.maxY - compensation, | ||
| width: bounds.width, | ||
| height: compensation | ||
| ) | ||
| case .minY: | ||
| return NSRect( | ||
| x: bounds.minX, | ||
| y: bounds.minY, | ||
| width: bounds.width, | ||
| height: compensation | ||
| ) | ||
| case .maxX: | ||
| return NSRect( | ||
| x: bounds.maxX - compensation, | ||
| y: bounds.minY, | ||
| width: compensation, | ||
| height: bounds.height | ||
| ) | ||
| case .minX: | ||
| return NSRect( | ||
| x: bounds.minX, | ||
| y: bounds.minY, | ||
| width: compensation, | ||
| height: bounds.height | ||
| ) | ||
| @unknown default: | ||
| return bounds | ||
| } | ||
| } | ||
| } | ||
| } | ||
20 changes: 20 additions & 0 deletions
20
Packages/CmuxAppKitSupportUI/Sources/CmuxAppKitSupportUI/Scroll/ClearScrollBackground.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import AppKit | ||
| public import SwiftUI | ||
|
|
||
| /// A `ViewModifier` that makes the enclosing `NSScrollView` fully transparent, hiding the | ||
| /// SwiftUI scroll content background and clearing the AppKit scroll-view layer chain. | ||
| public struct ClearScrollBackground: ViewModifier { | ||
| /// Creates the clear-scroll-background modifier. | ||
| public init() {} | ||
|
|
||
| public func body(content: Content) -> some View { | ||
| if #available(macOS 13.0, *) { | ||
| content | ||
| .scrollContentBackground(.hidden) | ||
| .background(ScrollBackgroundClearer()) | ||
| } else { | ||
| content | ||
| .background(ScrollBackgroundClearer()) | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
IsshouldHideAnchora documented public API/property forNSPopoverin AppKit (macOS 14 through latest)? If not, what stable/public alternative exists to hide a popover arrow?💡 Result:
The property shouldHideAnchor is not a documented public API for NSPopover in AppKit [1][2]. While it appears in some runtime headers [3][4] and has been discussed in developer forums as a potential private or internal flag that can be accessed via Key-Value Coding (e.g., popover.setValue(true, forKeyPath: "shouldHideAnchor")) [5], using private APIs is discouraged and may lead to app rejection or unexpected behavior in future macOS versions. There is no native, stable, or public public API to remove or hide the popover arrow in AppKit [6][7]. The most reliable, non-private "stable" workaround used by developers is to exploit the system's built-in behavior: the popover automatically hides its arrow when the positioning view is moved outside the visible rect of its window [8][9]. You can achieve this by: 1. Creating a secondary, transparent positioning view or using a placeholder view. 2. Presenting the popover relative to this view [8]. 3. Immediately moving the positioning view outside the visible bounds of the screen or window (e.g., by adjusting its frame to a negative coordinate), which triggers the system to hide the anchor arrow [8][9]. Alternatively, for more robust or custom control over the popover appearance and behavior, many developers choose to implement a custom popover using an NSWindow or utilize established, well-maintained third-party libraries [6].
Citations:
Avoid KVC against
NSPopoverprivate key (shouldHideAnchor).ArrowlessPopoverAnchor.swiftline 125 usespopover.setValue(true, forKeyPath: "shouldHideAnchor"), butshouldHideAnchoris not a documented publicNSPopoverAPI; relying on this private KVC contract risks runtime failures or broken popover presentation if AppKit changes.🤖 Prompt for AI Agents