Merge remote-tracking branch 'origin/main' into pr-317-session-persistence

# Conflicts:
#	Sources/AppDelegate.swift
This commit is contained in:
Lawrence Chen 2026-02-23 19:20:56 -08:00
commit ea33e3adbd
15 changed files with 2401 additions and 344 deletions

View file

@ -984,14 +984,6 @@ private func commandPaletteWindowOverlayController(for window: NSWindow) -> Wind
return controller
}
private struct CommandPaletteRowFramePreferenceKey: PreferenceKey {
static var defaultValue: [Int: CGRect] = [:]
static func reduce(value: inout [Int: CGRect], nextValue: () -> [Int: CGRect]) {
value.merge(nextValue(), uniquingKeysWith: { _, rhs in rhs })
}
}
enum WorkspaceMountPolicy {
// Keep only the selected workspace mounted to minimize layer-tree traversal.
static let maxMountedWorkspaces = 1
@ -1127,8 +1119,8 @@ struct ContentView: View {
@State private var commandPaletteRenameDraft: String = ""
@State private var commandPaletteSelectedResultIndex: Int = 0
@State private var commandPaletteHoveredResultIndex: Int?
@State private var commandPaletteLastSelectionIndex: Int = 0
@State private var commandPaletteRowFrames: [Int: CGRect] = [:]
@State private var commandPaletteScrollTargetIndex: Int?
@State private var commandPaletteScrollTargetAnchor: UnitPoint?
@State private var commandPaletteRestoreFocusTarget: CommandPaletteRestoreFocusTarget?
@State private var commandPaletteUsageHistoryByCommandId: [String: CommandPaletteUsageEntry] = [:]
@AppStorage(CommandPaletteRenameSelectionSettings.selectAllOnFocusKey)
@ -1204,11 +1196,6 @@ struct ContentView: View {
case kind
}
enum CommandPaletteScrollAnchor: Equatable {
case top
case bottom
}
private struct CommandPaletteTrailingLabel {
let text: String
let style: CommandPaletteTrailingLabelStyle
@ -1284,6 +1271,10 @@ struct ContentView: View {
static let panelHasUnread = "panel.hasUnread"
static let updateHasAvailable = "update.hasAvailable"
static func terminalOpenTargetAvailable(_ target: TerminalDirectoryOpenTarget) -> String {
"terminal.openTarget.\(target.rawValue).available"
}
}
private struct CommandPaletteCommandContribution {
@ -1757,6 +1748,7 @@ struct ContentView: View {
WindowDragHandleView()
TitlebarLeadingInsetReader(inset: $titlebarLeadingInset)
.allowsHitTesting(false)
HStack(spacing: 8) {
if isFullScreen && !sidebarState.isVisible {
@ -1772,6 +1764,7 @@ struct ContentView: View {
.font(.system(size: 13, weight: .bold))
.foregroundColor(fakeTitlebarTextColor)
.lineLimit(1)
.allowsHitTesting(false)
Spacer()
@ -1784,9 +1777,6 @@ struct ContentView: View {
.frame(height: titlebarPadding)
.frame(maxWidth: .infinity)
.contentShape(Rectangle())
.onTapGesture(count: 2) {
NSApp.keyWindow?.zoom(nil)
}
.background(fakeTitlebarBackground)
.overlay(alignment: .bottom) {
Rectangle()
@ -2231,6 +2221,9 @@ struct ContentView: View {
// Do not make the entire background draggable; it interferes with drag gestures
// like sidebar tab reordering in multi-window mode.
window.isMovableByWindowBackground = false
// Keep the window immovable by default so titlebar controls (like the folder icon)
// cannot accidentally initiate native window drags.
window.isMovable = false
window.styleMask.insert(.fullSizeContentView)
// Track this window for fullscreen notifications
@ -2487,7 +2480,7 @@ struct ContentView: View {
private var commandPaletteCommandListView: some View {
let visibleResults = Array(commandPaletteResults)
let selectedIndex = commandPaletteSelectedIndex(resultCount: visibleResults.count)
let commandPaletteListMaxHeight: CGFloat = 216
let commandPaletteListMaxHeight: CGFloat = 450
let commandPaletteRowHeight: CGFloat = 24
let commandPaletteEmptyStateHeight: CGFloat = 44
let commandPaletteListContentHeight = visibleResults.isEmpty
@ -2531,133 +2524,85 @@ struct ContentView: View {
Divider()
ScrollViewReader { proxy in
ScrollView {
LazyVStack(spacing: 0) {
if visibleResults.isEmpty {
Text(commandPaletteEmptyStateText)
.font(.system(size: 13, weight: .regular))
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 12)
.padding(.vertical, 12)
} else {
ForEach(Array(visibleResults.enumerated()), id: \.element.id) { index, result in
let isSelected = index == selectedIndex
let isHovered = commandPaletteHoveredResultIndex == index
let rowBackground: Color = isSelected
? Color.accentColor.opacity(0.12)
: (isHovered ? Color.primary.opacity(0.08) : .clear)
ScrollView {
LazyVStack(spacing: 0) {
if visibleResults.isEmpty {
Text(commandPaletteEmptyStateText)
.font(.system(size: 13, weight: .regular))
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 12)
.padding(.vertical, 12)
} else {
ForEach(Array(visibleResults.enumerated()), id: \.element.id) { index, result in
let isSelected = index == selectedIndex
let isHovered = commandPaletteHoveredResultIndex == index
let rowBackground: Color = isSelected
? Color.accentColor.opacity(0.12)
: (isHovered ? Color.primary.opacity(0.08) : .clear)
Button {
runCommandPaletteCommand(result.command)
} label: {
HStack(spacing: 8) {
commandPaletteHighlightedTitleText(
result.command.title,
matchedIndices: result.titleMatchIndices
)
.font(.system(size: 13, weight: .regular))
.lineLimit(1)
Spacer()
if let trailingLabel = commandPaletteTrailingLabel(for: result.command) {
switch trailingLabel.style {
case .shortcut:
Text(trailingLabel.text)
.font(.system(size: 11, weight: .medium))
.foregroundStyle(.secondary)
.padding(.horizontal, 4)
.padding(.vertical, 1)
.background(Color.primary.opacity(0.08), in: RoundedRectangle(cornerRadius: 4, style: .continuous))
case .kind:
Text(trailingLabel.text)
.font(.system(size: 11, weight: .regular))
.foregroundStyle(.secondary)
.lineLimit(1)
}
}
}
.padding(.horizontal, 9)
.padding(.vertical, 2)
.frame(maxWidth: .infinity, alignment: .leading)
.background(rowBackground)
.background(
GeometryReader { geometry in
Color.clear.preference(
key: CommandPaletteRowFramePreferenceKey.self,
value: [index: geometry.frame(in: .named("commandPaletteListScroll"))]
)
}
Button {
runCommandPaletteCommand(result.command)
} label: {
HStack(spacing: 8) {
commandPaletteHighlightedTitleText(
result.command.title,
matchedIndices: result.titleMatchIndices
)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.id(index)
.onHover { hovering in
if hovering {
commandPaletteHoveredResultIndex = index
} else if commandPaletteHoveredResultIndex == index {
commandPaletteHoveredResultIndex = nil
.font(.system(size: 13, weight: .regular))
.lineLimit(1)
Spacer()
if let trailingLabel = commandPaletteTrailingLabel(for: result.command) {
switch trailingLabel.style {
case .shortcut:
Text(trailingLabel.text)
.font(.system(size: 11, weight: .medium))
.foregroundStyle(.secondary)
.padding(.horizontal, 4)
.padding(.vertical, 1)
.background(Color.primary.opacity(0.08), in: RoundedRectangle(cornerRadius: 4, style: .continuous))
case .kind:
Text(trailingLabel.text)
.font(.system(size: 11, weight: .regular))
.foregroundStyle(.secondary)
.lineLimit(1)
}
}
}
.padding(.horizontal, 9)
.padding(.vertical, 2)
.frame(maxWidth: .infinity, alignment: .leading)
.background(rowBackground)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.id(index)
.onHover { hovering in
if hovering {
commandPaletteHoveredResultIndex = index
} else if commandPaletteHoveredResultIndex == index {
commandPaletteHoveredResultIndex = nil
}
}
}
}
// Force a fresh row tree per query so rendered labels/actions stay in lockstep.
.id(commandPaletteQuery)
}
.coordinateSpace(name: "commandPaletteListScroll")
.frame(height: commandPaletteListHeight)
.onChange(of: commandPaletteSelectedResultIndex) { _ in
guard !visibleResults.isEmpty else { return }
let index = commandPaletteSelectedIndex(resultCount: visibleResults.count)
let previousIndex = commandPaletteLastSelectionIndex
defer { commandPaletteLastSelectionIndex = index }
guard let anchorDecision = Self.commandPaletteScrollAnchor(
selectedIndex: index,
previousIndex: previousIndex,
resultCount: visibleResults.count,
selectedFrame: commandPaletteRowFrames[index],
viewportHeight: commandPaletteListHeight,
contentHeight: commandPaletteListContentHeight
) else { return }
let anchor: UnitPoint
switch anchorDecision {
case .top:
anchor = .top
case .bottom:
anchor = .bottom
}
DispatchQueue.main.async {
withAnimation(.easeOut(duration: 0.1)) {
proxy.scrollTo(index, anchor: anchor)
}
}
}
.onChange(of: visibleResults.count) { _ in
commandPaletteLastSelectionIndex = commandPaletteSelectedIndex(resultCount: visibleResults.count)
}
.onPreferenceChange(CommandPaletteRowFramePreferenceKey.self) { frames in
commandPaletteRowFrames = frames
guard !visibleResults.isEmpty else { return }
let index = commandPaletteSelectedIndex(resultCount: visibleResults.count)
guard let anchorDecision = Self.commandPaletteEdgeVisibilityCorrectionAnchor(
selectedIndex: index,
resultCount: visibleResults.count,
selectedFrame: frames[index],
viewportHeight: commandPaletteListHeight,
contentHeight: commandPaletteListContentHeight
) else { return }
let anchor: UnitPoint = anchorDecision == .top ? .top : .bottom
DispatchQueue.main.async {
withAnimation(.easeOut(duration: 0.08)) {
proxy.scrollTo(index, anchor: anchor)
}
}
}
.scrollTargetLayout()
// Force a fresh row tree per query so rendered labels/actions stay in lockstep.
.id(commandPaletteQuery)
}
.frame(height: commandPaletteListHeight)
.scrollPosition(
id: Binding(
get: { commandPaletteScrollTargetIndex },
// Ignore passive readback so manual scrolling doesn't mutate selection-follow state.
set: { _ in }
),
anchor: commandPaletteScrollTargetAnchor
)
.onChange(of: commandPaletteSelectedResultIndex) { _ in
updateCommandPaletteScrollTarget(resultCount: visibleResults.count, animated: true)
}
// Keep Esc-to-close behavior without showing footer controls.
@ -2672,20 +2617,19 @@ struct ContentView: View {
}
.onAppear {
commandPaletteHoveredResultIndex = nil
commandPaletteLastSelectionIndex = commandPaletteSelectedResultIndex
commandPaletteRowFrames = [:]
updateCommandPaletteScrollTarget(resultCount: visibleResults.count, animated: false)
resetCommandPaletteSearchFocus()
}
.onChange(of: commandPaletteQuery) { _ in
commandPaletteSelectedResultIndex = 0
commandPaletteHoveredResultIndex = nil
commandPaletteLastSelectionIndex = 0
commandPaletteRowFrames = [:]
commandPaletteScrollTargetIndex = nil
commandPaletteScrollTargetAnchor = nil
syncCommandPaletteDebugStateForObservedWindow()
}
.onChange(of: visibleResults.count) { _ in
commandPaletteSelectedResultIndex = commandPaletteSelectedIndex(resultCount: visibleResults.count)
commandPaletteLastSelectionIndex = commandPaletteSelectedResultIndex
updateCommandPaletteScrollTarget(resultCount: visibleResults.count, animated: false)
if let hoveredIndex = commandPaletteHoveredResultIndex, hoveredIndex >= visibleResults.count {
commandPaletteHoveredResultIndex = nil
}
@ -3288,18 +3232,29 @@ struct ContentView: View {
if let panelContext = focusedPanelContext {
let workspace = panelContext.workspace
let panelId = panelContext.panelId
let panelIsTerminal = panelContext.panel.panelType == .terminal
snapshot.setBool(CommandPaletteContextKeys.hasFocusedPanel, true)
snapshot.setString(
CommandPaletteContextKeys.panelName,
panelDisplayName(workspace: workspace, panelId: panelId, fallback: panelContext.panel.displayTitle)
)
snapshot.setBool(CommandPaletteContextKeys.panelIsBrowser, panelContext.panel.panelType == .browser)
snapshot.setBool(CommandPaletteContextKeys.panelIsTerminal, panelContext.panel.panelType == .terminal)
snapshot.setBool(CommandPaletteContextKeys.panelIsTerminal, panelIsTerminal)
snapshot.setBool(CommandPaletteContextKeys.panelHasCustomName, workspace.panelCustomTitles[panelId] != nil)
snapshot.setBool(CommandPaletteContextKeys.panelShouldPin, !workspace.isPanelPinned(panelId))
let hasUnread = workspace.manualUnreadPanelIds.contains(panelId)
|| notificationStore.hasUnreadNotification(forTabId: workspace.id, surfaceId: panelId)
snapshot.setBool(CommandPaletteContextKeys.panelHasUnread, hasUnread)
if panelIsTerminal {
let availableTargets = TerminalDirectoryOpenTarget.cachedLiveAvailableTargets
for target in TerminalDirectoryOpenTarget.commandPaletteShortcutTargets {
snapshot.setBool(
CommandPaletteContextKeys.terminalOpenTargetAvailable(target),
availableTargets.contains(target)
)
}
}
}
if case .updateAvailable = updateViewModel.effectiveState {
@ -3710,15 +3665,20 @@ struct ContentView: View {
)
)
contributions.append(
CommandPaletteCommandContribution(
commandId: "palette.terminalOpenDirectory",
title: constant("Open Current Directory in IDE"),
subtitle: terminalPanelSubtitle,
keywords: ["terminal", "directory", "open", "ide", "code", "default app"],
when: { $0.bool(CommandPaletteContextKeys.panelIsTerminal) }
for target in TerminalDirectoryOpenTarget.commandPaletteShortcutTargets {
contributions.append(
CommandPaletteCommandContribution(
commandId: target.commandPaletteCommandId,
title: constant(target.commandPaletteTitle),
subtitle: terminalPanelSubtitle,
keywords: target.commandPaletteKeywords,
when: { context in
context.bool(CommandPaletteContextKeys.panelIsTerminal)
&& context.bool(CommandPaletteContextKeys.terminalOpenTargetAvailable(target))
}
)
)
)
}
contributions.append(
CommandPaletteCommandContribution(
commandId: "palette.terminalFind",
@ -3981,9 +3941,11 @@ struct ContentView: View {
_ = tabManager.createBrowserSplit(direction: .right, url: url)
}
registry.register(commandId: "palette.terminalOpenDirectory") {
if !openFocusedDirectoryInDefaultApp() {
NSSound.beep()
for target in TerminalDirectoryOpenTarget.commandPaletteShortcutTargets {
registry.register(commandId: target.commandPaletteCommandId) {
if !openFocusedDirectory(in: target) {
NSSound.beep()
}
}
}
registry.register(commandId: "palette.terminalFind") {
@ -4047,61 +4009,43 @@ struct ContentView: View {
return min(max(commandPaletteSelectedResultIndex, 0), resultCount - 1)
}
static func commandPaletteScrollAnchor(
static func commandPaletteScrollPositionAnchor(
selectedIndex: Int,
previousIndex: Int,
resultCount: Int,
selectedFrame: CGRect?,
viewportHeight: CGFloat,
contentHeight: CGFloat,
epsilon: CGFloat = 0.5
) -> CommandPaletteScrollAnchor? {
resultCount: Int
) -> UnitPoint? {
guard resultCount > 0 else { return nil }
guard contentHeight > viewportHeight else { return nil }
// Always pin edges exactly into view when selection reaches first/last.
if selectedIndex <= 0 {
return .top
return UnitPoint.top
}
if selectedIndex >= resultCount - 1 {
return .bottom
return UnitPoint.bottom
}
if let frame = selectedFrame,
frame.minY >= (0 - epsilon),
frame.maxY <= (viewportHeight + epsilon) {
return nil
}
return selectedIndex >= previousIndex ? .bottom : .top
return nil
}
static func commandPaletteEdgeVisibilityCorrectionAnchor(
selectedIndex: Int,
resultCount: Int,
selectedFrame: CGRect?,
viewportHeight: CGFloat,
contentHeight: CGFloat,
epsilon: CGFloat = 0.5
) -> CommandPaletteScrollAnchor? {
guard resultCount > 0 else { return nil }
guard contentHeight > viewportHeight else { return nil }
let isTop = selectedIndex <= 0
let isBottom = selectedIndex >= (resultCount - 1)
guard isTop || isBottom else { return nil }
guard let frame = selectedFrame else {
return isTop ? .top : .bottom
private func updateCommandPaletteScrollTarget(resultCount: Int, animated: Bool) {
guard resultCount > 0 else {
commandPaletteScrollTargetIndex = nil
commandPaletteScrollTargetAnchor = nil
return
}
if isTop {
let topDelta = abs(frame.minY)
return topDelta > epsilon ? .top : nil
}
let selectedIndex = commandPaletteSelectedIndex(resultCount: resultCount)
commandPaletteScrollTargetAnchor = Self.commandPaletteScrollPositionAnchor(
selectedIndex: selectedIndex,
resultCount: resultCount
)
let bottomDelta = abs(frame.maxY - viewportHeight)
return bottomDelta > epsilon ? .bottom : nil
let assignTarget = {
commandPaletteScrollTargetIndex = selectedIndex
}
if animated {
withAnimation(.easeOut(duration: 0.1)) {
assignTarget()
}
} else {
assignTarget()
}
}
private func moveCommandPaletteSelection(by delta: Int) {
@ -4295,8 +4239,8 @@ struct ContentView: View {
commandPaletteRenameDraft = ""
commandPaletteSelectedResultIndex = 0
commandPaletteHoveredResultIndex = nil
commandPaletteLastSelectionIndex = 0
commandPaletteRowFrames = [:]
commandPaletteScrollTargetIndex = nil
commandPaletteScrollTargetAnchor = nil
resetCommandPaletteSearchFocus()
syncCommandPaletteDebugStateForObservedWindow()
}
@ -4309,8 +4253,8 @@ struct ContentView: View {
commandPaletteRenameDraft = ""
commandPaletteSelectedResultIndex = 0
commandPaletteHoveredResultIndex = nil
commandPaletteLastSelectionIndex = 0
commandPaletteRowFrames = [:]
commandPaletteScrollTargetIndex = nil
commandPaletteScrollTargetAnchor = nil
isCommandPaletteSearchFocused = false
isCommandPaletteRenameFocused = false
commandPaletteRestoreFocusTarget = nil
@ -4537,9 +4481,22 @@ struct ContentView: View {
return NSWorkspace.shared.open(url)
}
private func openFocusedDirectoryInDefaultApp() -> Bool {
private func openFocusedDirectory(in target: TerminalDirectoryOpenTarget) -> Bool {
guard let directoryURL = focusedTerminalDirectoryURL() else { return false }
return NSWorkspace.shared.open(directoryURL)
return openFocusedDirectory(directoryURL, in: target)
}
private func openFocusedDirectory(_ directoryURL: URL, in target: TerminalDirectoryOpenTarget) -> Bool {
switch target {
case .finder:
NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: directoryURL.path)
return true
default:
guard let applicationURL = target.applicationURL() else { return false }
let configuration = NSWorkspace.OpenConfiguration()
NSWorkspace.shared.open([directoryURL], withApplicationAt: applicationURL, configuration: configuration)
return true
}
}
private func focusedTerminalDirectoryURL() -> URL? {
@ -7417,9 +7374,21 @@ private struct DraggableFolderIconRepresentable: NSViewRepresentable {
}
}
private final class DraggableFolderNSView: NSView, NSDraggingSource {
final class DraggableFolderNSView: NSView, NSDraggingSource {
private final class FolderIconImageView: NSImageView {
override var mouseDownCanMoveWindow: Bool { false }
}
var directory: String
private var imageView: NSImageView!
private var imageView: FolderIconImageView!
private var previousWindowMovableState: Bool?
private weak var suppressedWindow: NSWindow?
private var hasActiveDragSession = false
private var didArmWindowDragSuppression = false
private func formatPoint(_ point: NSPoint) -> String {
String(format: "(%.1f,%.1f)", point.x, point.y)
}
init(directory: String) {
self.directory = directory
@ -7435,8 +7404,10 @@ private final class DraggableFolderNSView: NSView, NSDraggingSource {
NSSize(width: 16, height: 16)
}
override var mouseDownCanMoveWindow: Bool { false }
private func setupImageView() {
imageView = NSImageView()
imageView = FolderIconImageView()
imageView.imageScaling = .scaleProportionallyDown
imageView.translatesAutoresizingMaskIntoConstraints = false
addSubview(imageView)
@ -7461,9 +7432,40 @@ private final class DraggableFolderNSView: NSView, NSDraggingSource {
return context == .outsideApplication ? [.copy, .link] : .copy
}
override func mouseDown(with event: NSEvent) {
func draggingSession(_ session: NSDraggingSession, endedAt screenPoint: NSPoint, operation: NSDragOperation) {
hasActiveDragSession = false
restoreWindowMovableStateIfNeeded()
#if DEBUG
dlog("folder.dragStart dir=\(directory)")
let nowMovable = window.map { String($0.isMovable) } ?? "nil"
let windowOrigin = window.map { formatPoint($0.frame.origin) } ?? "nil"
dlog("folder.dragEnd dir=\(directory) operation=\(operation.rawValue) screen=\(formatPoint(screenPoint)) nowMovable=\(nowMovable) windowOrigin=\(windowOrigin)")
#endif
}
override func hitTest(_ point: NSPoint) -> NSView? {
guard bounds.contains(point) else { return nil }
maybeDisableWindowDraggingEarly(trigger: "hitTest")
let hit = super.hitTest(point)
#if DEBUG
let hitDesc = hit.map { String(describing: type(of: $0)) } ?? "nil"
let imageHit = (hit === imageView)
let wasMovable = previousWindowMovableState.map(String.init) ?? "nil"
let nowMovable = window.map { String($0.isMovable) } ?? "nil"
dlog("folder.hitTest point=\(formatPoint(point)) hit=\(hitDesc) imageViewHit=\(imageHit) returning=DraggableFolderNSView wasMovable=\(wasMovable) nowMovable=\(nowMovable)")
#endif
return self
}
override func mouseDown(with event: NSEvent) {
maybeDisableWindowDraggingEarly(trigger: "mouseDown")
hasActiveDragSession = false
#if DEBUG
let localPoint = convert(event.locationInWindow, from: nil)
let responderDesc = window?.firstResponder.map { String(describing: type(of: $0)) } ?? "nil"
let wasMovable = previousWindowMovableState.map(String.init) ?? "nil"
let nowMovable = window.map { String($0.isMovable) } ?? "nil"
let windowOrigin = window.map { formatPoint($0.frame.origin) } ?? "nil"
dlog("folder.mouseDown dir=\(directory) point=\(formatPoint(localPoint)) firstResponder=\(responderDesc) wasMovable=\(wasMovable) nowMovable=\(nowMovable) windowOrigin=\(windowOrigin)")
#endif
let fileURL = URL(fileURLWithPath: directory)
let draggingItem = NSDraggingItem(pasteboardWriter: fileURL as NSURL)
@ -7472,7 +7474,19 @@ private final class DraggableFolderNSView: NSView, NSDraggingSource {
iconImage.size = NSSize(width: 32, height: 32)
draggingItem.setDraggingFrame(bounds, contents: iconImage)
beginDraggingSession(with: [draggingItem], event: event, source: self)
let session = beginDraggingSession(with: [draggingItem], event: event, source: self)
hasActiveDragSession = true
#if DEBUG
let itemCount = session.draggingPasteboard.pasteboardItems?.count ?? 0
dlog("folder.dragStart dir=\(directory) pasteboardItems=\(itemCount)")
#endif
}
override func mouseUp(with event: NSEvent) {
super.mouseUp(with: event)
if !hasActiveDragSession {
restoreWindowMovableStateIfNeeded()
}
}
override func rightMouseDown(with event: NSEvent) {
@ -7541,6 +7555,59 @@ private final class DraggableFolderNSView: NSView, NSDraggingSource {
// Open "Computer" view in Finder (shows all volumes)
NSWorkspace.shared.open(URL(fileURLWithPath: "/", isDirectory: true))
}
private func restoreWindowMovableStateIfNeeded() {
guard didArmWindowDragSuppression || previousWindowMovableState != nil else { return }
let targetWindow = suppressedWindow ?? window
let depthAfter = endWindowDragSuppression(window: targetWindow)
restoreWindowDragging(window: targetWindow, previousMovableState: previousWindowMovableState)
self.previousWindowMovableState = nil
self.suppressedWindow = nil
self.didArmWindowDragSuppression = false
#if DEBUG
let nowMovable = targetWindow.map { String($0.isMovable) } ?? "nil"
dlog("folder.dragSuppression restore depth=\(depthAfter) nowMovable=\(nowMovable)")
#endif
}
private func maybeDisableWindowDraggingEarly(trigger: String) {
guard !didArmWindowDragSuppression else { return }
guard let eventType = NSApp.currentEvent?.type,
eventType == .leftMouseDown || eventType == .leftMouseDragged else {
return
}
guard let currentWindow = window else { return }
didArmWindowDragSuppression = true
suppressedWindow = currentWindow
let suppressionDepth = beginWindowDragSuppression(window: currentWindow) ?? 0
if currentWindow.isMovable {
previousWindowMovableState = temporarilyDisableWindowDragging(window: currentWindow)
} else {
previousWindowMovableState = nil
}
#if DEBUG
let wasMovable = previousWindowMovableState.map(String.init) ?? "nil"
let nowMovable = String(currentWindow.isMovable)
dlog(
"folder.dragSuppression trigger=\(trigger) event=\(eventType) depth=\(suppressionDepth) wasMovable=\(wasMovable) nowMovable=\(nowMovable)"
)
#endif
}
}
func temporarilyDisableWindowDragging(window: NSWindow?) -> Bool? {
guard let window else { return nil }
let wasMovable = window.isMovable
if wasMovable {
window.isMovable = false
}
return wasMovable
}
func restoreWindowDragging(window: NSWindow?, previousMovableState: Bool?) {
guard let window, let previousMovableState else { return }
window.isMovable = previousMovableState
}
/// Wrapper view that tries NSGlassEffectView (macOS 26+) when available or requested
@ -7622,11 +7689,16 @@ private struct SidebarVisualEffectBackground: NSViewRepresentable {
/// Reads the leading inset required to clear traffic lights + left titlebar accessories.
final class TitlebarLeadingInsetPassthroughView: NSView {
override var mouseDownCanMoveWindow: Bool { false }
override func hitTest(_ point: NSPoint) -> NSView? { nil }
}
private struct TitlebarLeadingInsetReader: NSViewRepresentable {
@Binding var inset: CGFloat
func makeNSView(context: Context) -> NSView {
let view = NSView()
let view = TitlebarLeadingInsetPassthroughView()
view.setFrameSize(.zero)
return view
}