From 11972f04d1a752d8c87a0686ed127e13e15000d1 Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Wed, 13 May 2026 02:14:26 -0700 Subject: [PATCH 1/9] Handle right sidebar tools in global search browse hits --- Sources/Search/GlobalSearchDocuments.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Search/GlobalSearchDocuments.swift b/Sources/Search/GlobalSearchDocuments.swift index 11120bb19475..baa92275e4fc 100644 --- a/Sources/Search/GlobalSearchDocuments.swift +++ b/Sources/Search/GlobalSearchDocuments.swift @@ -34,7 +34,7 @@ enum GlobalSearchDocuments { kind = .browser case .markdown: kind = .markdown - case .terminal, .filePreview: + case .terminal, .filePreview, .rightSidebarTool: kind = .title } From ab091f9d2c998e4b54bb0c4529031acd8b52840b Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Wed, 13 May 2026 02:14:35 -0700 Subject: [PATCH 2/9] Use transparent backgrounds for file preview panels --- Sources/Panels/FilePreviewPanel.swift | 215 +++++++++++++++++++-- Sources/Panels/FilePreviewTextEditor.swift | 32 ++- Sources/Panels/MarkdownPanelView.swift | 14 +- Sources/Panels/MarkdownWebRenderer.swift | 10 + Sources/Panels/TerminalPanelView.swift | 28 ++- cmuxTests/TerminalAndGhosttyTests.swift | 52 +++++ 6 files changed, 320 insertions(+), 31 deletions(-) diff --git a/Sources/Panels/FilePreviewPanel.swift b/Sources/Panels/FilePreviewPanel.swift index 882604ff22dd..0022f158516e 100644 --- a/Sources/Panels/FilePreviewPanel.swift +++ b/Sources/Panels/FilePreviewPanel.swift @@ -26,6 +26,52 @@ enum FilePreviewInteraction { } +enum FilePreviewNativeBackground { + static func resolvedColor(backgroundColor: NSColor, drawsBackground: Bool) -> NSColor { + drawsBackground ? backgroundColor : .clear + } + + static func applyRootLayer( + to view: NSView, + backgroundColor: NSColor, + drawsBackground: Bool + ) { + let resolvedBackgroundColor = resolvedColor( + backgroundColor: backgroundColor, + drawsBackground: drawsBackground + ) + view.wantsLayer = true + view.layer?.backgroundColor = resolvedBackgroundColor.cgColor + view.layer?.isOpaque = drawsBackground && resolvedBackgroundColor.alphaComponent >= 0.999 + } + + static func applyScrollBackgrounds( + in view: NSView, + backgroundColor: NSColor, + drawsBackground: Bool + ) { + let resolvedBackgroundColor = resolvedColor( + backgroundColor: backgroundColor, + drawsBackground: drawsBackground + ) + if let scrollView = view as? NSScrollView { + scrollView.drawsBackground = drawsBackground + scrollView.backgroundColor = resolvedBackgroundColor + } + if let clipView = view as? NSClipView { + clipView.drawsBackground = drawsBackground + clipView.backgroundColor = resolvedBackgroundColor + } + for subview in view.subviews { + applyScrollBackgrounds( + in: subview, + backgroundColor: backgroundColor, + drawsBackground: drawsBackground + ) + } + } +} + struct FileExternalOpenApplication: Identifiable, Equatable, Sendable { let url: URL let displayName: String @@ -993,14 +1039,14 @@ struct FilePreviewPanelView: View { @State private var focusFlashOpacity = 0.0 @State private var focusFlashAnimationGeneration = 0 - private var themeBackgroundColor: NSColor { - appearance.backgroundColor - } - private var themeForegroundColor: NSColor { appearance.foregroundColor } + private var contentBackgroundColor: NSColor { + appearance.contentBackgroundColor + } + var body: some View { VStack(spacing: 0) { if panel.previewMode != .pdf { @@ -1010,7 +1056,7 @@ struct FilePreviewPanelView: View { content } .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color(nsColor: themeBackgroundColor)) + .background(Color(nsColor: contentBackgroundColor)) .overlay { RoundedRectangle(cornerRadius: FocusFlashPattern.ringCornerRadius) .stroke(cmuxAccentColor().opacity(focusFlashOpacity), lineWidth: 3) @@ -1032,7 +1078,7 @@ struct FilePreviewPanelView: View { PanelFilePathHeader( iconSystemName: panel.displayIcon ?? "doc.viewfinder", filePath: panel.filePath, - backgroundColor: themeBackgroundColor, + backgroundColor: contentBackgroundColor, foregroundColor: themeForegroundColor ) { FileExternalOpenMenu(fileURL: panel.fileURL, isDisabled: panel.isFileUnavailable) @@ -1066,17 +1112,38 @@ struct FilePreviewPanelView: View { FilePreviewTextEditor( panel: panel, isVisibleInUI: isVisibleInUI, - themeBackgroundColor: themeBackgroundColor, - themeForegroundColor: themeForegroundColor + themeBackgroundColor: contentBackgroundColor, + themeForegroundColor: themeForegroundColor, + drawsBackground: appearance.drawsContentBackground ) case .pdf: - FilePreviewPDFView(panel: panel, isVisibleInUI: isVisibleInUI) + FilePreviewPDFView( + panel: panel, + isVisibleInUI: isVisibleInUI, + backgroundColor: contentBackgroundColor, + drawsBackground: appearance.drawsContentBackground + ) case .image: - FilePreviewImageView(panel: panel, isVisibleInUI: isVisibleInUI) + FilePreviewImageView( + panel: panel, + isVisibleInUI: isVisibleInUI, + backgroundColor: contentBackgroundColor, + drawsBackground: appearance.drawsContentBackground + ) case .media: - FilePreviewMediaView(panel: panel, isVisibleInUI: isVisibleInUI) + FilePreviewMediaView( + panel: panel, + isVisibleInUI: isVisibleInUI, + backgroundColor: contentBackgroundColor, + drawsBackground: appearance.drawsContentBackground + ) case .quickLook: - QuickLookPreviewView(panel: panel, isVisibleInUI: isVisibleInUI) + QuickLookPreviewView( + panel: panel, + isVisibleInUI: isVisibleInUI, + backgroundColor: contentBackgroundColor, + drawsBackground: appearance.drawsContentBackground + ) } } } @@ -1130,10 +1197,13 @@ struct FilePreviewPanelView: View { private struct FilePreviewPDFView: NSViewRepresentable { let panel: FilePreviewPanel let isVisibleInUI: Bool + let backgroundColor: NSColor + let drawsBackground: Bool func makeNSView(context: Context) -> FilePreviewPDFContainerView { let view = FilePreviewPDFContainerView() view.isHidden = !isVisibleInUI + view.setBackgroundAppearance(backgroundColor: backgroundColor, drawsBackground: drawsBackground) view.setPanel(panel) view.setURL(panel.fileURL) return view @@ -1141,6 +1211,7 @@ private struct FilePreviewPDFView: NSViewRepresentable { func updateNSView(_ nsView: FilePreviewPDFContainerView, context: Context) { nsView.isHidden = !isVisibleInUI + nsView.setBackgroundAppearance(backgroundColor: backgroundColor, drawsBackground: drawsBackground) nsView.setPanel(panel) nsView.setURL(panel.fileURL) } @@ -2055,6 +2126,8 @@ final class FilePreviewPDFContainerView: NSView, NSSplitViewDelegate, NSOutlineV private var activePDFRegion: FilePreviewPanelFocusIntent? private weak var observedPDFClipView: NSClipView? private var rotationAccumulator: CGFloat = 0 + private var previewBackgroundColor = NSColor.textBackgroundColor + private var drawsPreviewBackground = true private static let documentLoadQueue = DispatchQueue( label: "com.cmux.file-preview.pdf-document-load", qos: .userInitiated @@ -2082,6 +2155,7 @@ final class FilePreviewPDFContainerView: NSView, NSSplitViewDelegate, NSOutlineV override func layout() { super.layout() + applyBackgroundAppearance() if !didSetInitialSidebarWidth, bounds.width > 0 { didSetInitialSidebarWidth = true let initialWidth = clampedSidebarWidth(lastSidebarWidth) @@ -2106,6 +2180,13 @@ final class FilePreviewPDFContainerView: NSView, NSSplitViewDelegate, NSOutlineV registerFocusEndpoint() } + func setBackgroundAppearance(backgroundColor: NSColor, drawsBackground: Bool) { + guard previewBackgroundColor != backgroundColor || drawsPreviewBackground != drawsBackground else { return } + previewBackgroundColor = backgroundColor + drawsPreviewBackground = drawsBackground + applyBackgroundAppearance() + } + func setURL(_ url: URL) { guard currentURL != url else { applyPreferredSidebarWidthIfNeeded() @@ -2161,11 +2242,11 @@ final class FilePreviewPDFContainerView: NSView, NSSplitViewDelegate, NSOutlineV setupSidebar() setupPDFView() setupFloatingChrome() + applyBackgroundAppearance() pdfView.displayMode = .singlePageContinuous pdfView.displayDirection = .vertical pdfView.displaysPageBreaks = true - pdfView.backgroundColor = .textBackgroundColor pdfView.minScaleFactor = 0.1 pdfView.maxScaleFactor = 8.0 pdfView.onMagnify = { [weak self] event in @@ -2289,7 +2370,6 @@ final class FilePreviewPDFContainerView: NSView, NSSplitViewDelegate, NSOutlineV private func setupPDFView() { contentHost.wantsLayer = true - contentHost.layer?.backgroundColor = NSColor.textBackgroundColor.cgColor pdfView.translatesAutoresizingMaskIntoConstraints = false pdfView.onFocusChanged = { [weak self] isActive in self?.setActivePDFRegion(isActive ? .pdfCanvas : nil) @@ -2303,6 +2383,22 @@ final class FilePreviewPDFContainerView: NSView, NSSplitViewDelegate, NSOutlineV ]) } + private func applyBackgroundAppearance() { + let resolvedBackgroundColor = drawsPreviewBackground ? previewBackgroundColor : .clear + wantsLayer = true + layer?.backgroundColor = resolvedBackgroundColor.cgColor + layer?.isOpaque = drawsPreviewBackground && resolvedBackgroundColor.alphaComponent >= 0.999 + contentHost.wantsLayer = true + contentHost.layer?.backgroundColor = resolvedBackgroundColor.cgColor + contentHost.layer?.isOpaque = drawsPreviewBackground && resolvedBackgroundColor.alphaComponent >= 0.999 + pdfView.backgroundColor = resolvedBackgroundColor + FilePreviewNativeBackground.applyScrollBackgrounds( + in: pdfView, + backgroundColor: resolvedBackgroundColor, + drawsBackground: drawsPreviewBackground + ) + } + private func setupFloatingChrome() { chromeHost.frame = bounds.width > 0 && bounds.height > 0 ? bounds @@ -3111,10 +3207,13 @@ final class FilePreviewPDFContainerView: NSView, NSSplitViewDelegate, NSOutlineV private struct FilePreviewImageView: NSViewRepresentable { let panel: FilePreviewPanel let isVisibleInUI: Bool + let backgroundColor: NSColor + let drawsBackground: Bool func makeNSView(context: Context) -> FilePreviewImageContainerView { let view = FilePreviewImageContainerView() view.isHidden = !isVisibleInUI + view.setBackgroundAppearance(backgroundColor: backgroundColor, drawsBackground: drawsBackground) view.setPanel(panel) view.setURL(panel.fileURL) return view @@ -3122,6 +3221,7 @@ private struct FilePreviewImageView: NSViewRepresentable { func updateNSView(_ nsView: FilePreviewImageContainerView, context: Context) { nsView.isHidden = !isVisibleInUI + nsView.setBackgroundAppearance(backgroundColor: backgroundColor, drawsBackground: drawsBackground) nsView.setPanel(panel) nsView.setURL(panel.fileURL) } @@ -3201,6 +3301,8 @@ private final class FilePreviewImageContainerView: NSView { private var isFitMode = true private var rotationDegrees = 0 private var rotationAccumulator: CGFloat = 0 + private var previewBackgroundColor = NSColor.textBackgroundColor + private var drawsPreviewBackground = true private static let imageLoadQueue = DispatchQueue( label: "com.cmux.file-preview.image-load", qos: .userInitiated @@ -3225,6 +3327,7 @@ private final class FilePreviewImageContainerView: NSView { override func layout() { super.layout() + applyBackgroundAppearance() if isFitMode { scale = fitScale() } @@ -3249,6 +3352,13 @@ private final class FilePreviewImageContainerView: NSView { registerFocusEndpoint() } + func setBackgroundAppearance(backgroundColor: NSColor, drawsBackground: Bool) { + guard previewBackgroundColor != backgroundColor || drawsPreviewBackground != drawsBackground else { return } + previewBackgroundColor = backgroundColor + drawsPreviewBackground = drawsBackground + applyBackgroundAppearance() + } + func setURL(_ url: URL) { assert(Thread.isMainThread, "AppKit image updates must run on the main thread") guard currentURL != url else { return } @@ -3305,8 +3415,6 @@ private final class FilePreviewImageContainerView: NSView { scrollView.hasHorizontalScroller = true scrollView.autohidesScrollers = true scrollView.borderType = .noBorder - scrollView.drawsBackground = true - scrollView.backgroundColor = .textBackgroundColor scrollView.documentView = documentView scrollView.onMagnify = { [weak self] event in let factor = 1.0 + event.magnification @@ -3346,6 +3454,23 @@ private final class FilePreviewImageContainerView: NSView { chromeHost.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: 10), chromeHost.heightAnchor.constraint(equalToConstant: 40), ]) + applyBackgroundAppearance() + } + + private func applyBackgroundAppearance() { + let resolvedBackgroundColor = FilePreviewNativeBackground.resolvedColor( + backgroundColor: previewBackgroundColor, + drawsBackground: drawsPreviewBackground + ) + FilePreviewNativeBackground.applyRootLayer( + to: self, + backgroundColor: previewBackgroundColor, + drawsBackground: drawsPreviewBackground + ) + scrollView.drawsBackground = drawsPreviewBackground + scrollView.backgroundColor = resolvedBackgroundColor + scrollView.contentView.drawsBackground = drawsPreviewBackground + scrollView.contentView.backgroundColor = resolvedBackgroundColor } @objc private func zoomOut() { @@ -3824,6 +3949,8 @@ private final class FilePreviewMagnifyingImageView: NSImageView { private struct FilePreviewMediaView: NSViewRepresentable { let panel: FilePreviewPanel let isVisibleInUI: Bool + let backgroundColor: NSColor + let drawsBackground: Bool func makeCoordinator() -> Coordinator { Coordinator() @@ -3835,6 +3962,11 @@ private struct FilePreviewMediaView: NSViewRepresentable { playerView.controlsStyle = .floating playerView.showsFullScreenToggleButton = true playerView.videoGravity = .resizeAspect + Self.applyBackgroundAppearance( + to: playerView, + backgroundColor: backgroundColor, + drawsBackground: drawsBackground + ) panel.attachPreviewFocus(root: playerView, primaryResponder: playerView, intent: .mediaPlayer) context.coordinator.update(playerView: playerView, url: panel.fileURL) return playerView @@ -3842,10 +3974,27 @@ private struct FilePreviewMediaView: NSViewRepresentable { func updateNSView(_ nsView: AVPlayerView, context: Context) { nsView.isHidden = !isVisibleInUI + Self.applyBackgroundAppearance( + to: nsView, + backgroundColor: backgroundColor, + drawsBackground: drawsBackground + ) panel.attachPreviewFocus(root: nsView, primaryResponder: nsView, intent: .mediaPlayer) context.coordinator.update(playerView: nsView, url: panel.fileURL) } + private static func applyBackgroundAppearance( + to playerView: AVPlayerView, + backgroundColor: NSColor, + drawsBackground: Bool + ) { + FilePreviewNativeBackground.applyRootLayer( + to: playerView, + backgroundColor: backgroundColor, + drawsBackground: drawsBackground + ) + } + final class Coordinator { private var currentURL: URL? private var player: AVPlayer? @@ -3868,15 +4017,27 @@ private struct FilePreviewMediaView: NSViewRepresentable { private struct QuickLookPreviewView: NSViewRepresentable { let panel: FilePreviewPanel let isVisibleInUI: Bool + let backgroundColor: NSColor + let drawsBackground: Bool func makeNSView(context: Context) -> NSView { guard let previewView = QLPreviewView(frame: .zero, style: .normal) else { let view = NSView() view.isHidden = !isVisibleInUI + Self.applyBackgroundAppearance( + to: view, + backgroundColor: backgroundColor, + drawsBackground: drawsBackground + ) return view } previewView.isHidden = !isVisibleInUI previewView.autostarts = true + Self.applyBackgroundAppearance( + to: previewView, + backgroundColor: backgroundColor, + drawsBackground: drawsBackground + ) panel.attachPreviewFocus(root: previewView, primaryResponder: previewView, intent: .quickLook) previewView.previewItem = context.coordinator.item(for: panel.fileURL, title: panel.displayTitle) return previewView @@ -3884,6 +4045,11 @@ private struct QuickLookPreviewView: NSViewRepresentable { func updateNSView(_ nsView: NSView, context: Context) { nsView.isHidden = !isVisibleInUI + Self.applyBackgroundAppearance( + to: nsView, + backgroundColor: backgroundColor, + drawsBackground: drawsBackground + ) guard let previewView = nsView as? QLPreviewView else { return } panel.attachPreviewFocus(root: previewView, primaryResponder: previewView, intent: .quickLook) previewView.previewItem = context.coordinator.item(for: panel.fileURL, title: panel.displayTitle) @@ -3901,6 +4067,23 @@ private struct QuickLookPreviewView: NSViewRepresentable { Coordinator() } + private static func applyBackgroundAppearance( + to view: NSView, + backgroundColor: NSColor, + drawsBackground: Bool + ) { + FilePreviewNativeBackground.applyRootLayer( + to: view, + backgroundColor: backgroundColor, + drawsBackground: drawsBackground + ) + FilePreviewNativeBackground.applyScrollBackgrounds( + in: view, + backgroundColor: backgroundColor, + drawsBackground: drawsBackground + ) + } + final class Coordinator { private var item: FilePreviewQLItem? diff --git a/Sources/Panels/FilePreviewTextEditor.swift b/Sources/Panels/FilePreviewTextEditor.swift index cdf511a00ee2..1f149fdd15ab 100644 --- a/Sources/Panels/FilePreviewTextEditor.swift +++ b/Sources/Panels/FilePreviewTextEditor.swift @@ -17,6 +17,7 @@ struct FilePreviewTextEditor: NSViewRepresentable where PanelModel: let isVisibleInUI: Bool let themeBackgroundColor: NSColor let themeForegroundColor: NSColor + let drawsBackground: Bool func makeCoordinator() -> Coordinator { Coordinator(panel: panel) @@ -29,7 +30,7 @@ struct FilePreviewTextEditor: NSViewRepresentable where PanelModel: scrollView.hasHorizontalScroller = true scrollView.autohidesScrollers = true scrollView.borderType = .noBorder - scrollView.drawsBackground = true + scrollView.drawsBackground = drawsBackground let textView = SavingTextView() textView.panel = panel @@ -42,7 +43,7 @@ struct FilePreviewTextEditor: NSViewRepresentable where PanelModel: textView.usesFindPanel = true textView.usesFontPanel = false textView.font = .monospacedSystemFont(ofSize: 13, weight: .regular) - textView.drawsBackground = true + textView.drawsBackground = drawsBackground textView.minSize = NSSize(width: 0, height: 0) textView.maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude) textView.isVerticallyResizable = true @@ -58,14 +59,24 @@ struct FilePreviewTextEditor: NSViewRepresentable where PanelModel: panel.attachTextView(textView) scrollView.documentView = textView - Self.applyTheme(to: scrollView, backgroundColor: themeBackgroundColor, foregroundColor: themeForegroundColor) + Self.applyTheme( + to: scrollView, + backgroundColor: themeBackgroundColor, + foregroundColor: themeForegroundColor, + drawsBackground: drawsBackground + ) return scrollView } func updateNSView(_ scrollView: NSScrollView, context: Context) { context.coordinator.panel = panel scrollView.isHidden = !isVisibleInUI - Self.applyTheme(to: scrollView, backgroundColor: themeBackgroundColor, foregroundColor: themeForegroundColor) + Self.applyTheme( + to: scrollView, + backgroundColor: themeBackgroundColor, + foregroundColor: themeForegroundColor, + drawsBackground: drawsBackground + ) guard let textView = scrollView.documentView as? SavingTextView else { return } textView.panel = panel textView.applyFilePreviewTextEditorInsets() @@ -79,12 +90,17 @@ struct FilePreviewTextEditor: NSViewRepresentable where PanelModel: private static func applyTheme( to scrollView: NSScrollView, backgroundColor: NSColor, - foregroundColor: NSColor + foregroundColor: NSColor, + drawsBackground: Bool ) { - scrollView.backgroundColor = backgroundColor - scrollView.contentView.backgroundColor = backgroundColor + let resolvedBackgroundColor = drawsBackground ? backgroundColor : .clear + scrollView.drawsBackground = drawsBackground + scrollView.backgroundColor = resolvedBackgroundColor + scrollView.contentView.drawsBackground = drawsBackground + scrollView.contentView.backgroundColor = resolvedBackgroundColor if let textView = scrollView.documentView as? NSTextView { - textView.backgroundColor = backgroundColor + textView.drawsBackground = drawsBackground + textView.backgroundColor = resolvedBackgroundColor textView.textColor = foregroundColor textView.insertionPointColor = foregroundColor } diff --git a/Sources/Panels/MarkdownPanelView.swift b/Sources/Panels/MarkdownPanelView.swift index b922c96c2ef1..eadaec7435c3 100644 --- a/Sources/Panels/MarkdownPanelView.swift +++ b/Sources/Panels/MarkdownPanelView.swift @@ -53,7 +53,7 @@ struct MarkdownPanelView: View { } } .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(backgroundColor) + .background(contentBackgroundColor) .overlay { RoundedRectangle(cornerRadius: FocusFlashPattern.ringCornerRadius) .stroke(cmuxAccentColor().opacity(focusFlashOpacity), lineWidth: 3) @@ -85,6 +85,7 @@ struct MarkdownPanelView: View { MarkdownWebRenderer( markdown: panel.content, theme: MarkdownWebTheme.resolve(backgroundColor: themeBackgroundColor), + backgroundColor: appearance.contentBackgroundColor, panelId: panel.id, workspaceId: panel.workspaceId, filePath: panel.filePath, @@ -100,8 +101,9 @@ struct MarkdownPanelView: View { FilePreviewTextEditor( panel: panel, isVisibleInUI: isVisibleInUI, - themeBackgroundColor: themeBackgroundColor, - themeForegroundColor: themeForegroundColor + themeBackgroundColor: appearance.contentBackgroundColor, + themeForegroundColor: themeForegroundColor, + drawsBackground: appearance.drawsContentBackground ) .frame(maxWidth: .infinity, maxHeight: .infinity) } @@ -112,7 +114,7 @@ struct MarkdownPanelView: View { PanelFilePathHeader( iconSystemName: panel.displayIcon ?? "doc.richtext", filePath: panel.filePath, - backgroundColor: themeBackgroundColor, + backgroundColor: appearance.contentBackgroundColor, foregroundColor: themeForegroundColor ) { FileExternalOpenMenu( @@ -186,8 +188,8 @@ struct MarkdownPanelView: View { // MARK: - Theme - private var backgroundColor: Color { - Color(nsColor: themeBackgroundColor) + private var contentBackgroundColor: Color { + Color(nsColor: appearance.contentBackgroundColor) } private var themeBackgroundColor: NSColor { diff --git a/Sources/Panels/MarkdownWebRenderer.swift b/Sources/Panels/MarkdownWebRenderer.swift index 19f59e7936f9..4a2a4336798e 100644 --- a/Sources/Panels/MarkdownWebRenderer.swift +++ b/Sources/Panels/MarkdownWebRenderer.swift @@ -38,6 +38,7 @@ struct MarkdownWebTheme: Equatable { struct MarkdownWebRenderer: NSViewRepresentable { let markdown: String let theme: MarkdownWebTheme + let backgroundColor: NSColor let panelId: UUID let workspaceId: UUID let filePath: String @@ -63,6 +64,7 @@ struct MarkdownWebRenderer: NSViewRepresentable { let webView = MarkdownWebView(frame: .zero, configuration: config) webView.onPointerDown = onRequestPanelFocus webView.setValue(false, forKey: "drawsBackground") + applyBackground(to: webView) webView.allowsBackForwardNavigationGestures = false webView.allowsLinkPreview = false webView.navigationDelegate = context.coordinator @@ -89,6 +91,7 @@ struct MarkdownWebRenderer: NSViewRepresentable { context.coordinator.workspaceId = workspaceId context.coordinator.filePath = filePath (nsView as? MarkdownWebView)?.onPointerDown = onRequestPanelFocus + applyBackground(to: nsView) applyAppearance(to: nsView, isDark: theme.isDark) context.coordinator.update(markdown: markdown, theme: theme) } @@ -113,6 +116,13 @@ struct MarkdownWebRenderer: NSViewRepresentable { } } + private func applyBackground(to webView: WKWebView) { + webView.underPageBackgroundColor = backgroundColor + webView.wantsLayer = true + webView.layer?.backgroundColor = backgroundColor.cgColor + webView.layer?.isOpaque = backgroundColor.alphaComponent >= 0.999 + } + @MainActor final class Coordinator: NSObject, WKNavigationDelegate, WKUIDelegate, WKScriptMessageHandler { weak var webView: WKWebView? diff --git a/Sources/Panels/TerminalPanelView.swift b/Sources/Panels/TerminalPanelView.swift index 250bbb6de459..d0b69cbd291f 100644 --- a/Sources/Panels/TerminalPanelView.swift +++ b/Sources/Panels/TerminalPanelView.swift @@ -50,8 +50,21 @@ struct PanelAppearance { let dividerColor: Color let unfocusedOverlayNSColor: NSColor let unfocusedOverlayOpacity: Double + let usesClearContentBackground: Bool + + var contentBackgroundColor: NSColor { + usesClearContentBackground ? .clear : backgroundColor + } + + var drawsContentBackground: Bool { + !usesClearContentBackground + } static func fromConfig(_ config: GhosttyConfig) -> PanelAppearance { + fromConfig(config, usesTransparentWindow: cmuxShouldUseTransparentBackgroundWindow()) + } + + static func fromConfig(_ config: GhosttyConfig, usesTransparentWindow: Bool) -> PanelAppearance { PanelAppearance( backgroundColor: GhosttyBackgroundTheme.color( backgroundColor: config.backgroundColor, @@ -60,7 +73,20 @@ struct PanelAppearance { foregroundColor: config.foregroundColor, dividerColor: Color(nsColor: config.resolvedSplitDividerColor), unfocusedOverlayNSColor: config.unfocusedSplitOverlayFill, - unfocusedOverlayOpacity: config.unfocusedSplitOverlayOpacity + unfocusedOverlayOpacity: config.unfocusedSplitOverlayOpacity, + usesClearContentBackground: shouldUseClearContentBackground( + opacity: config.backgroundOpacity, + usesGhosttyGlassStyle: config.backgroundBlur.isMacOSGlassStyle, + usesTransparentWindow: usesTransparentWindow + ) ) } + + static func shouldUseClearContentBackground( + opacity: Double, + usesGhosttyGlassStyle: Bool, + usesTransparentWindow: Bool + ) -> Bool { + usesTransparentWindow || usesGhosttyGlassStyle || opacity < 0.999 + } } diff --git a/cmuxTests/TerminalAndGhosttyTests.swift b/cmuxTests/TerminalAndGhosttyTests.swift index 5a4611ff126d..ac26a326dd4a 100644 --- a/cmuxTests/TerminalAndGhosttyTests.swift +++ b/cmuxTests/TerminalAndGhosttyTests.swift @@ -1578,6 +1578,58 @@ final class GhosttyBackgroundThemeTests: XCTestCase { } } +final class PanelAppearanceBackgroundTests: XCTestCase { + func testTransparentGhosttyOpacityUsesClearContentBackground() { + var config = GhosttyConfig() + config.backgroundColor = NSColor(srgbRed: 0.10, green: 0.20, blue: 0.30, alpha: 1.0) + config.backgroundOpacity = 0.42 + config.backgroundBlur = .disabled + + let appearance = PanelAppearance.fromConfig(config, usesTransparentWindow: false) + + XCTAssertTrue(appearance.usesClearContentBackground) + XCTAssertFalse(appearance.drawsContentBackground) + XCTAssertEqual(appearance.contentBackgroundColor.alphaComponent, 0.0, accuracy: 0.0001) + } + + func testOpaqueGhosttyBackgroundKeepsPanelFill() { + var config = GhosttyConfig() + config.backgroundColor = NSColor(srgbRed: 0.10, green: 0.20, blue: 0.30, alpha: 1.0) + config.backgroundOpacity = 1.0 + config.backgroundBlur = .disabled + + let appearance = PanelAppearance.fromConfig(config, usesTransparentWindow: false) + + XCTAssertFalse(appearance.usesClearContentBackground) + XCTAssertTrue(appearance.drawsContentBackground) + XCTAssertEqual(appearance.contentBackgroundColor.alphaComponent, 1.0, accuracy: 0.0001) + } + + func testGhosttyGlassBackgroundUsesClearContentBackground() { + var config = GhosttyConfig() + config.backgroundOpacity = 1.0 + config.backgroundBlur = .macosGlassRegular + + let appearance = PanelAppearance.fromConfig(config, usesTransparentWindow: false) + + XCTAssertTrue(appearance.usesClearContentBackground) + XCTAssertFalse(appearance.drawsContentBackground) + XCTAssertEqual(appearance.contentBackgroundColor.alphaComponent, 0.0, accuracy: 0.0001) + } + + func testTransparentWindowSettingUsesClearContentBackground() { + var config = GhosttyConfig() + config.backgroundOpacity = 1.0 + config.backgroundBlur = .disabled + + let appearance = PanelAppearance.fromConfig(config, usesTransparentWindow: true) + + XCTAssertTrue(appearance.usesClearContentBackground) + XCTAssertFalse(appearance.drawsContentBackground) + XCTAssertEqual(appearance.contentBackgroundColor.alphaComponent, 0.0, accuracy: 0.0001) + } +} + final class GhosttyResponderResolutionTests: XCTestCase { private final class FocusProbeView: NSView { From 787345c8aa9252dd1c11d3861eaf027d2725bae4 Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Wed, 13 May 2026 03:10:59 -0700 Subject: [PATCH 3/9] Cover panel theme background preservation --- cmuxTests/TerminalAndGhosttyTests.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cmuxTests/TerminalAndGhosttyTests.swift b/cmuxTests/TerminalAndGhosttyTests.swift index ac26a326dd4a..aa065d0b5200 100644 --- a/cmuxTests/TerminalAndGhosttyTests.swift +++ b/cmuxTests/TerminalAndGhosttyTests.swift @@ -1589,6 +1589,7 @@ final class PanelAppearanceBackgroundTests: XCTestCase { XCTAssertTrue(appearance.usesClearContentBackground) XCTAssertFalse(appearance.drawsContentBackground) + XCTAssertEqual(appearance.backgroundColor.alphaComponent, 0.42, accuracy: 0.0001) XCTAssertEqual(appearance.contentBackgroundColor.alphaComponent, 0.0, accuracy: 0.0001) } @@ -1602,6 +1603,7 @@ final class PanelAppearanceBackgroundTests: XCTestCase { XCTAssertFalse(appearance.usesClearContentBackground) XCTAssertTrue(appearance.drawsContentBackground) + XCTAssertEqual(appearance.backgroundColor.alphaComponent, 1.0, accuracy: 0.0001) XCTAssertEqual(appearance.contentBackgroundColor.alphaComponent, 1.0, accuracy: 0.0001) } @@ -1614,6 +1616,7 @@ final class PanelAppearanceBackgroundTests: XCTestCase { XCTAssertTrue(appearance.usesClearContentBackground) XCTAssertFalse(appearance.drawsContentBackground) + XCTAssertEqual(appearance.backgroundColor.alphaComponent, 1.0, accuracy: 0.0001) XCTAssertEqual(appearance.contentBackgroundColor.alphaComponent, 0.0, accuracy: 0.0001) } @@ -1626,6 +1629,7 @@ final class PanelAppearanceBackgroundTests: XCTestCase { XCTAssertTrue(appearance.usesClearContentBackground) XCTAssertFalse(appearance.drawsContentBackground) + XCTAssertEqual(appearance.backgroundColor.alphaComponent, 1.0, accuracy: 0.0001) XCTAssertEqual(appearance.contentBackgroundColor.alphaComponent, 0.0, accuracy: 0.0001) } } From d5099622576e93b39ce8f5b72042e2a922c16214 Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Wed, 13 May 2026 04:49:34 -0700 Subject: [PATCH 4/9] Fix PDF file preview open menu --- Sources/Panels/FilePreviewPanel.swift | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Sources/Panels/FilePreviewPanel.swift b/Sources/Panels/FilePreviewPanel.swift index 0022f158516e..042f86e1648a 100644 --- a/Sources/Panels/FilePreviewPanel.swift +++ b/Sources/Panels/FilePreviewPanel.swift @@ -235,10 +235,12 @@ struct FileExternalOpenMenu: View { case .header: Image(systemName: "square.and.arrow.up") .frame(width: 18, height: 18) + .contentShape(Rectangle()) case .chrome: Image(systemName: "square.and.arrow.up") .font(.system(size: 16, weight: .semibold)) .frame(width: 42, height: 40) + .contentShape(Rectangle()) } } @@ -1437,7 +1439,7 @@ struct FilePreviewPDFZoomChromeView: View { zoomButtons(includeDividers: false) secondaryButtons(includeDividers: false) if let fileURL { - FileExternalOpenMenu(fileURL: fileURL) + FileExternalOpenMenu(fileURL: fileURL, style: .chrome) } } label: { Label( @@ -2091,7 +2093,7 @@ final class FilePreviewPDFContainerView: NSView, NSSplitViewDelegate, NSOutlineV static let minimumSidebarWidth = FilePreviewPDFSizing.minimumSidebarWidth static let maximumSidebarWidth = FilePreviewPDFSizing.maximumSidebarWidth static let floatingChromeHeight: CGFloat = 40 - static let floatingControlsWidth: CGFloat = 318 + static let floatingControlsWidth: CGFloat = 344 static let floatingChromeCornerRadius: CGFloat = 20 } @@ -2430,6 +2432,9 @@ final class FilePreviewPDFContainerView: NSView, NSSplitViewDelegate, NSOutlineV titleStack.translatesAutoresizingMaskIntoConstraints = false chromeHost.addSubview(titleStack) + let zoomWidthConstraint = zoomChromeHost.widthAnchor.constraint(equalToConstant: Metrics.floatingControlsWidth) + zoomWidthConstraint.priority = .defaultHigh + NSLayoutConstraint.activate([ sidebarChromeHost.topAnchor.constraint(equalTo: chromeHost.topAnchor, constant: 10), sidebarChromeHost.leadingAnchor.constraint(equalTo: chromeHost.leadingAnchor, constant: 10), @@ -2438,7 +2443,7 @@ final class FilePreviewPDFContainerView: NSView, NSSplitViewDelegate, NSOutlineV zoomChromeHost.topAnchor.constraint(equalTo: chromeHost.topAnchor, constant: 10), zoomChromeHost.trailingAnchor.constraint(equalTo: chromeHost.trailingAnchor, constant: -10), - zoomChromeHost.widthAnchor.constraint(equalToConstant: Metrics.floatingControlsWidth), + zoomWidthConstraint, zoomChromeHost.heightAnchor.constraint(equalToConstant: Metrics.floatingChromeHeight), titleStack.leadingAnchor.constraint(equalTo: sidebarChromeHost.trailingAnchor, constant: 12), From 91511677c149d98175d7aeb554fe39da38dbcf54 Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Wed, 13 May 2026 05:42:06 -0700 Subject: [PATCH 5/9] Make file open menu button compact --- Sources/Panels/FilePreviewPanel.swift | 267 +++++++++++++++++++++----- 1 file changed, 223 insertions(+), 44 deletions(-) diff --git a/Sources/Panels/FilePreviewPanel.swift b/Sources/Panels/FilePreviewPanel.swift index 042f86e1648a..cfd48b41941a 100644 --- a/Sources/Panels/FilePreviewPanel.swift +++ b/Sources/Panels/FilePreviewPanel.swift @@ -180,6 +180,24 @@ enum FileExternalOpenText { enum FileExternalOpenMenuStyle { case header case chrome + + var buttonSize: CGSize { + switch self { + case .header: + return CGSize(width: 18, height: 18) + case .chrome: + return CGSize(width: 42, height: 40) + } + } + + var symbolConfiguration: NSImage.SymbolConfiguration { + switch self { + case .header: + return NSImage.SymbolConfiguration(pointSize: 14, weight: .regular) + case .chrome: + return NSImage.SymbolConfiguration(pointSize: 16, weight: .semibold) + } + } } struct FileExternalOpenMenu: View { @@ -192,58 +210,22 @@ struct FileExternalOpenMenu: View { var body: some View { let applications = resolvedApplications let primaryApplication = primaryApplication(in: applications) - let otherApplications = applications.filter { application in - application.id != primaryApplication?.id - } let helpText = helpText(for: primaryApplication) - Menu { - if let primaryApplication { - Button(openInTitle(primaryApplication.displayName)) { - FileExternalOpenAction.open(fileURL: fileURL, applicationURL: primaryApplication.url) - } - if !otherApplications.isEmpty { - Divider() - Menu(FileExternalOpenText.openWithMenu) { - ForEach(otherApplications) { application in - Button(application.displayName) { - FileExternalOpenAction.open(fileURL: fileURL, applicationURL: application.url) - } - } - } - } - } else { - Button(FileExternalOpenText.openExternally) { - FileExternalOpenAction.openDefault(fileURL: fileURL) - } - } - } label: { - label - } - .menuStyle(.borderlessButton) - .disabled(isDisabled) + FileExternalOpenMenuButton( + fileURL: fileURL, + applications: applications, + isDisabled: isDisabled, + style: style, + accessibilityLabel: helpText + ) + .frame(width: style.buttonSize.width, height: style.buttonSize.height) .help(helpText) - .accessibilityLabel(helpText) .task(id: fileURL) { await refreshApplications() } } - @ViewBuilder - private var label: some View { - switch style { - case .header: - Image(systemName: "square.and.arrow.up") - .frame(width: 18, height: 18) - .contentShape(Rectangle()) - case .chrome: - Image(systemName: "square.and.arrow.up") - .font(.system(size: 16, weight: .semibold)) - .frame(width: 42, height: 40) - .contentShape(Rectangle()) - } - } - private func primaryApplication(in applications: [FileExternalOpenApplication]) -> FileExternalOpenApplication? { applications.first { $0.isDefault } ?? applications.first } @@ -271,6 +253,203 @@ struct FileExternalOpenMenu: View { } } +private struct FileExternalOpenMenuButton: NSViewRepresentable { + let fileURL: URL + let applications: [FileExternalOpenApplication] + let isDisabled: Bool + let style: FileExternalOpenMenuStyle + let accessibilityLabel: String + + func makeCoordinator() -> Coordinator { + Coordinator( + fileURL: fileURL, + applications: applications + ) + } + + func makeNSView(context: Context) -> NSButton { + let button = FileExternalOpenNSButton() + button.bezelStyle = .regularSquare + button.imagePosition = .imageOnly + button.imageScaling = .scaleProportionallyDown + button.isBordered = false + button.setButtonType(.momentaryChange) + button.target = button + button.action = #selector(FileExternalOpenNSButton.presentMenuFromAction(_:)) + button.translatesAutoresizingMaskIntoConstraints = false + update(button, coordinator: context.coordinator) + return button + } + + func updateNSView(_ button: NSButton, context: Context) { + context.coordinator.update( + fileURL: fileURL, + applications: applications + ) + update(button, coordinator: context.coordinator) + } + + private func update(_ button: NSButton, coordinator: Coordinator) { + let image = NSImage( + systemSymbolName: "square.and.arrow.up", + accessibilityDescription: nil + )?.withSymbolConfiguration(style.symbolConfiguration) + button.image = image + button.contentTintColor = .labelColor + button.isEnabled = !isDisabled + button.toolTip = accessibilityLabel + button.setAccessibilityLabel(accessibilityLabel) + if let button = button as? FileExternalOpenNSButton { + button.menuProvider = { [coordinator] in + coordinator.makeMenu() + } + } + } + + final class Coordinator: NSObject { + private var fileURL: URL + private var applications: [FileExternalOpenApplication] + + init( + fileURL: URL, + applications: [FileExternalOpenApplication] + ) { + self.fileURL = fileURL + self.applications = applications + } + + func update( + fileURL: URL, + applications: [FileExternalOpenApplication] + ) { + self.fileURL = fileURL + self.applications = applications + } + + @objc + private func openFile(_ item: NSMenuItem) { + guard let request = item.representedObject as? FileExternalOpenMenuRequest else { return } + FileExternalOpenAction.open( + fileURL: request.fileURL, + applicationURL: request.applicationURL + ) + } + + func makeMenu() -> NSMenu { + let menu = NSMenu() + menu.autoenablesItems = false + let primaryApplication = applications.first { $0.isDefault } ?? applications.first + let otherApplications = applications.filter { application in + application.id != primaryApplication?.id + } + + if let primaryApplication { + let openItem = menuItem( + title: FileExternalOpenText.openInApplication(primaryApplication.displayName), + fileURL: fileURL, + applicationURL: primaryApplication.url + ) + menu.addItem(openItem) + + if !otherApplications.isEmpty { + menu.addItem(.separator()) + let openWithMenu = NSMenu(title: FileExternalOpenText.openWithMenu) + openWithMenu.autoenablesItems = false + for application in otherApplications { + openWithMenu.addItem(menuItem( + title: application.displayName, + fileURL: fileURL, + applicationURL: application.url + )) + } + let openWithItem = NSMenuItem( + title: FileExternalOpenText.openWithMenu, + action: nil, + keyEquivalent: "" + ) + openWithItem.submenu = openWithMenu + menu.addItem(openWithItem) + } + } else { + menu.addItem(menuItem( + title: FileExternalOpenText.openExternally, + fileURL: fileURL, + applicationURL: nil + )) + } + + return menu + } + + private func menuItem( + title: String, + fileURL: URL, + applicationURL: URL? + ) -> NSMenuItem { + let item = NSMenuItem( + title: title, + action: #selector(openFile(_:)), + keyEquivalent: "" + ) + item.target = self + item.representedObject = FileExternalOpenMenuRequest( + fileURL: fileURL, + applicationURL: applicationURL + ) + return item + } + } +} + +private final class FileExternalOpenNSButton: NSButton { + var menuProvider: (() -> NSMenu)? + private var activeMenu: NSMenu? + + override func acceptsFirstMouse(for event: NSEvent?) -> Bool { + true + } + + override func mouseDown(with event: NSEvent) { + presentMenu(event: event) + } + + override func performClick(_ sender: Any?) { + presentMenu(event: nil) + } + + @objc + func presentMenuFromAction(_ sender: Any?) { + presentMenu(event: nil) + } + + private func presentMenu(event: NSEvent?) { + guard isEnabled, let menu = menuProvider?() else { return } + activeMenu = menu + if let event { + NSMenu.popUpContextMenu(menu, with: event, for: self) + } else { + menu.popUp( + positioning: nil, + at: NSPoint(x: bounds.minX, y: bounds.minY - 4), + in: self + ) + } + } +} + +private final class FileExternalOpenMenuRequest: NSObject { + let fileURL: URL + let applicationURL: URL? + + init( + fileURL: URL, + applicationURL: URL? + ) { + self.fileURL = fileURL + self.applicationURL = applicationURL + } +} + struct FilePreviewDragEntry { let filePath: String let displayTitle: String From 4fae9b6aac4ac967850c624a437f4db2704a3ba9 Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Wed, 13 May 2026 06:33:53 -0700 Subject: [PATCH 6/9] Fix PDF open menu chrome button --- Sources/Panels/FilePreviewPanel.swift | 275 +++++++------------------- 1 file changed, 68 insertions(+), 207 deletions(-) diff --git a/Sources/Panels/FilePreviewPanel.swift b/Sources/Panels/FilePreviewPanel.swift index cfd48b41941a..2ba006efcf28 100644 --- a/Sources/Panels/FilePreviewPanel.swift +++ b/Sources/Panels/FilePreviewPanel.swift @@ -189,15 +189,6 @@ enum FileExternalOpenMenuStyle { return CGSize(width: 42, height: 40) } } - - var symbolConfiguration: NSImage.SymbolConfiguration { - switch self { - case .header: - return NSImage.SymbolConfiguration(pointSize: 14, weight: .regular) - case .chrome: - return NSImage.SymbolConfiguration(pointSize: 16, weight: .semibold) - } - } } struct FileExternalOpenMenu: View { @@ -206,26 +197,76 @@ struct FileExternalOpenMenu: View { var style: FileExternalOpenMenuStyle = .header @State private var resolvedApplications: [FileExternalOpenApplication] = [] + @State private var isChromeHovered = false var body: some View { let applications = resolvedApplications let primaryApplication = primaryApplication(in: applications) + let otherApplications = applications.filter { application in + application.id != primaryApplication?.id + } let helpText = helpText(for: primaryApplication) - FileExternalOpenMenuButton( - fileURL: fileURL, - applications: applications, - isDisabled: isDisabled, + Menu { + if let primaryApplication { + Button(openInTitle(primaryApplication.displayName)) { + FileExternalOpenAction.open(fileURL: fileURL, applicationURL: primaryApplication.url) + } + if !otherApplications.isEmpty { + Divider() + Menu(FileExternalOpenText.openWithMenu) { + ForEach(otherApplications) { application in + Button(application.displayName) { + FileExternalOpenAction.open(fileURL: fileURL, applicationURL: application.url) + } + } + } + } + } else { + Button(FileExternalOpenText.openExternally) { + FileExternalOpenAction.openDefault(fileURL: fileURL) + } + } + } label: { + label + } + .menuIndicator(.hidden) + .menuStyle(.borderlessButton) + .modifier(FileExternalOpenMenuChromeStyleModifier( style: style, - accessibilityLabel: helpText - ) - .frame(width: style.buttonSize.width, height: style.buttonSize.height) + isHovered: isChromeHovered + )) + .disabled(isDisabled) .help(helpText) + .accessibilityLabel(helpText) + .onHover { hovering in + if style == .chrome { + isChromeHovered = hovering + } + } .task(id: fileURL) { await refreshApplications() } } + @ViewBuilder + private var label: some View { + switch style { + case .header: + Image(systemName: "square.and.arrow.up") + .font(.system(size: 14, weight: .regular)) + .frame(width: style.buttonSize.width, height: style.buttonSize.height) + .contentShape(Rectangle()) + .accessibilityHidden(true) + case .chrome: + Image(systemName: "square.and.arrow.up") + .font(.system(size: 16, weight: .semibold)) + .frame(width: style.buttonSize.width, height: style.buttonSize.height) + .contentShape(Rectangle()) + .accessibilityHidden(true) + } + } + private func primaryApplication(in applications: [FileExternalOpenApplication]) -> FileExternalOpenApplication? { applications.first { $0.isDefault } ?? applications.first } @@ -253,203 +294,23 @@ struct FileExternalOpenMenu: View { } } -private struct FileExternalOpenMenuButton: NSViewRepresentable { - let fileURL: URL - let applications: [FileExternalOpenApplication] - let isDisabled: Bool +private struct FileExternalOpenMenuChromeStyleModifier: ViewModifier { let style: FileExternalOpenMenuStyle - let accessibilityLabel: String - - func makeCoordinator() -> Coordinator { - Coordinator( - fileURL: fileURL, - applications: applications - ) - } - - func makeNSView(context: Context) -> NSButton { - let button = FileExternalOpenNSButton() - button.bezelStyle = .regularSquare - button.imagePosition = .imageOnly - button.imageScaling = .scaleProportionallyDown - button.isBordered = false - button.setButtonType(.momentaryChange) - button.target = button - button.action = #selector(FileExternalOpenNSButton.presentMenuFromAction(_:)) - button.translatesAutoresizingMaskIntoConstraints = false - update(button, coordinator: context.coordinator) - return button - } - - func updateNSView(_ button: NSButton, context: Context) { - context.coordinator.update( - fileURL: fileURL, - applications: applications - ) - update(button, coordinator: context.coordinator) - } - - private func update(_ button: NSButton, coordinator: Coordinator) { - let image = NSImage( - systemSymbolName: "square.and.arrow.up", - accessibilityDescription: nil - )?.withSymbolConfiguration(style.symbolConfiguration) - button.image = image - button.contentTintColor = .labelColor - button.isEnabled = !isDisabled - button.toolTip = accessibilityLabel - button.setAccessibilityLabel(accessibilityLabel) - if let button = button as? FileExternalOpenNSButton { - button.menuProvider = { [coordinator] in - coordinator.makeMenu() - } - } - } - - final class Coordinator: NSObject { - private var fileURL: URL - private var applications: [FileExternalOpenApplication] - - init( - fileURL: URL, - applications: [FileExternalOpenApplication] - ) { - self.fileURL = fileURL - self.applications = applications - } - - func update( - fileURL: URL, - applications: [FileExternalOpenApplication] - ) { - self.fileURL = fileURL - self.applications = applications - } - - @objc - private func openFile(_ item: NSMenuItem) { - guard let request = item.representedObject as? FileExternalOpenMenuRequest else { return } - FileExternalOpenAction.open( - fileURL: request.fileURL, - applicationURL: request.applicationURL - ) - } - - func makeMenu() -> NSMenu { - let menu = NSMenu() - menu.autoenablesItems = false - let primaryApplication = applications.first { $0.isDefault } ?? applications.first - let otherApplications = applications.filter { application in - application.id != primaryApplication?.id - } - - if let primaryApplication { - let openItem = menuItem( - title: FileExternalOpenText.openInApplication(primaryApplication.displayName), - fileURL: fileURL, - applicationURL: primaryApplication.url - ) - menu.addItem(openItem) - - if !otherApplications.isEmpty { - menu.addItem(.separator()) - let openWithMenu = NSMenu(title: FileExternalOpenText.openWithMenu) - openWithMenu.autoenablesItems = false - for application in otherApplications { - openWithMenu.addItem(menuItem( - title: application.displayName, - fileURL: fileURL, - applicationURL: application.url - )) - } - let openWithItem = NSMenuItem( - title: FileExternalOpenText.openWithMenu, - action: nil, - keyEquivalent: "" - ) - openWithItem.submenu = openWithMenu - menu.addItem(openWithItem) - } - } else { - menu.addItem(menuItem( - title: FileExternalOpenText.openExternally, - fileURL: fileURL, - applicationURL: nil - )) - } - - return menu - } - - private func menuItem( - title: String, - fileURL: URL, - applicationURL: URL? - ) -> NSMenuItem { - let item = NSMenuItem( - title: title, - action: #selector(openFile(_:)), - keyEquivalent: "" - ) - item.target = self - item.representedObject = FileExternalOpenMenuRequest( - fileURL: fileURL, - applicationURL: applicationURL - ) - return item - } - } -} - -private final class FileExternalOpenNSButton: NSButton { - var menuProvider: (() -> NSMenu)? - private var activeMenu: NSMenu? - - override func acceptsFirstMouse(for event: NSEvent?) -> Bool { - true - } - - override func mouseDown(with event: NSEvent) { - presentMenu(event: event) - } - - override func performClick(_ sender: Any?) { - presentMenu(event: nil) - } - - @objc - func presentMenuFromAction(_ sender: Any?) { - presentMenu(event: nil) - } + let isHovered: Bool - private func presentMenu(event: NSEvent?) { - guard isEnabled, let menu = menuProvider?() else { return } - activeMenu = menu - if let event { - NSMenu.popUpContextMenu(menu, with: event, for: self) - } else { - menu.popUp( - positioning: nil, - at: NSPoint(x: bounds.minX, y: bounds.minY - 4), - in: self - ) + @ViewBuilder + func body(content: Content) -> some View { + switch style { + case .header: + content + case .chrome: + content + .buttonStyle(FilePreviewChromeHoverButtonStyle(isHovered: isHovered)) + .contentShape(Rectangle()) } } } -private final class FileExternalOpenMenuRequest: NSObject { - let fileURL: URL - let applicationURL: URL? - - init( - fileURL: URL, - applicationURL: URL? - ) { - self.fileURL = fileURL - self.applicationURL = applicationURL - } -} - struct FilePreviewDragEntry { let filePath: String let displayTitle: String From 1e192d55054e56fc7c50ba53162420d0c28bc5c3 Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Wed, 13 May 2026 18:34:31 -0700 Subject: [PATCH 7/9] Fix PDF open-with chrome click target --- Sources/Panels/FilePreviewPanel.swift | 189 ++++++++++++++++++++++---- cmuxTests/WindowAndDragTests.swift | 8 +- 2 files changed, 169 insertions(+), 28 deletions(-) diff --git a/Sources/Panels/FilePreviewPanel.swift b/Sources/Panels/FilePreviewPanel.swift index 2ba006efcf28..879deb6c2413 100644 --- a/Sources/Panels/FilePreviewPanel.swift +++ b/Sources/Panels/FilePreviewPanel.swift @@ -186,7 +186,7 @@ enum FileExternalOpenMenuStyle { case .header: return CGSize(width: 18, height: 18) case .chrome: - return CGSize(width: 42, height: 40) + return CGSize(width: 40, height: 40) } } } @@ -207,32 +207,16 @@ struct FileExternalOpenMenu: View { } let helpText = helpText(for: primaryApplication) - Menu { - if let primaryApplication { - Button(openInTitle(primaryApplication.displayName)) { - FileExternalOpenAction.open(fileURL: fileURL, applicationURL: primaryApplication.url) - } - if !otherApplications.isEmpty { - Divider() - Menu(FileExternalOpenText.openWithMenu) { - ForEach(otherApplications) { application in - Button(application.displayName) { - FileExternalOpenAction.open(fileURL: fileURL, applicationURL: application.url) - } - } - } - } - } else { - Button(FileExternalOpenText.openExternally) { - FileExternalOpenAction.openDefault(fileURL: fileURL) - } - } + Button { + presentMenu( + applications: applications, + currentPrimaryApplication: primaryApplication, + otherApplications: otherApplications + ) } label: { label } - .menuIndicator(.hidden) - .menuStyle(.borderlessButton) - .modifier(FileExternalOpenMenuChromeStyleModifier( + .modifier(FileExternalOpenButtonStyleModifier( style: style, isHovered: isChromeHovered )) @@ -292,9 +276,79 @@ struct FileExternalOpenMenu: View { guard !Task.isCancelled else { return } resolvedApplications = applications } + + private func presentMenu( + applications: [FileExternalOpenApplication], + currentPrimaryApplication: FileExternalOpenApplication?, + otherApplications: [FileExternalOpenApplication] + ) { + guard !isDisabled else { return } + let menuApplications: [FileExternalOpenApplication] + if applications.isEmpty { + menuApplications = FileExternalOpenApplicationResolver.live.applications(for: fileURL) + } else { + menuApplications = applications + } + let primary = primaryApplication(in: menuApplications) ?? currentPrimaryApplication + let others = menuApplications.filter { application in + application.id != primary?.id + } + otherApplications.filter { application in + application.id != primary?.id + && !menuApplications.contains(where: { $0.id == application.id }) + } + let menu = makeMenu(primaryApplication: primary, otherApplications: others) + if let event = NSApp.currentEvent, let contentView = event.window?.contentView { + let point = contentView.convert(event.locationInWindow, from: nil) + menu.popUp(positioning: nil as NSMenuItem?, at: point, in: contentView) + } else { + menu.popUp(positioning: nil as NSMenuItem?, at: NSEvent.mouseLocation, in: nil as NSView?) + } + } + + private func makeMenu( + primaryApplication: FileExternalOpenApplication?, + otherApplications: [FileExternalOpenApplication] + ) -> NSMenu { + let menu = NSMenu() + menu.autoenablesItems = false + + if let primaryApplication { + menu.addItem(FileExternalOpenMenuItem( + title: openInTitle(primaryApplication.displayName) + ) { + FileExternalOpenAction.open(fileURL: fileURL, applicationURL: primaryApplication.url) + }) + + if !otherApplications.isEmpty { + menu.addItem(.separator()) + let openWithMenu = NSMenu(title: FileExternalOpenText.openWithMenu) + openWithMenu.autoenablesItems = false + for application in otherApplications { + openWithMenu.addItem(FileExternalOpenMenuItem( + title: application.displayName + ) { + FileExternalOpenAction.open(fileURL: fileURL, applicationURL: application.url) + }) + } + let openWithItem = NSMenuItem( + title: FileExternalOpenText.openWithMenu, + action: nil, + keyEquivalent: "" + ) + openWithItem.submenu = openWithMenu + menu.addItem(openWithItem) + } + } else { + menu.addItem(FileExternalOpenMenuItem(title: FileExternalOpenText.openExternally) { + FileExternalOpenAction.openDefault(fileURL: fileURL) + }) + } + + return menu + } } -private struct FileExternalOpenMenuChromeStyleModifier: ViewModifier { +private struct FileExternalOpenButtonStyleModifier: ViewModifier { let style: FileExternalOpenMenuStyle let isHovered: Bool @@ -311,6 +365,25 @@ private struct FileExternalOpenMenuChromeStyleModifier: ViewModifier { } } +private final class FileExternalOpenMenuItem: NSMenuItem { + private let handler: () -> Void + + init(title: String, handler: @escaping () -> Void) { + self.handler = handler + super.init(title: title, action: #selector(performHandler(_:)), keyEquivalent: "") + target = self + } + + required init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc + private func performHandler(_ sender: Any?) { + handler() + } +} + struct FilePreviewDragEntry { let filePath: String let displayTitle: String @@ -1506,8 +1579,8 @@ struct FilePreviewPDFZoomChromeView: View { HStack(spacing: 0) { FileExternalOpenMenu(fileURL: fileURL, style: .chrome) } - .frame(height: chromeStyleVariant == .liquidGlass ? 40 : 36) - .modifier(FilePreviewPDFChromeStyleModifier(variant: chromeStyleVariant)) + .frame(width: 40, height: 40) + .modifier(FilePreviewPDFStandaloneChromeStyleModifier(variant: chromeStyleVariant)) } } } @@ -1743,6 +1816,68 @@ struct FilePreviewPDFChromeStyleModifier: ViewModifier { } } +struct FilePreviewPDFStandaloneChromeStyleModifier: ViewModifier { + let variant: FilePreviewPDFChromeStyleVariant + + @ViewBuilder + func body(content: Content) -> some View { + switch variant { + case .systemControlGroup: + content + .buttonStyle(.automatic) + .controlSize(.regular) + case .liquidGlass: + liquidGlassChrome(content: content) + case .materialCapsule: + materialChrome(content: content, material: .regularMaterial, strokeOpacity: 0.5) + case .borderedCapsule: + materialChrome(content: content, material: .ultraThinMaterial, strokeOpacity: 0.55) + case .thinOutline: + materialChrome(content: content, material: .thinMaterial, strokeOpacity: 0.75) + case .plainToolbar: + content + .controlSize(.regular) + } + } + + @ViewBuilder + private func liquidGlassChrome(content: Content) -> some View { + #if compiler(>=6.3) + if #available(macOS 26.0, *) { + content + .glassEffect(.regular, in: Circle()) + .overlay { + Circle() + .stroke(Color.white.opacity(0.24), lineWidth: 0.85) + } + .shadow(color: Color.black.opacity(0.18), radius: 8, y: 1) + } else { + materialChrome(content: content, material: .regularMaterial, strokeOpacity: 0.28) + } + #else + materialChrome(content: content, material: .regularMaterial, strokeOpacity: 0.28) + #endif + } + + private func materialChrome( + content: Content, + material: Material, + strokeOpacity: Double + ) -> some View { + content + .background { + Circle() + .fill(material) + Circle() + .fill(Color.white.opacity(0.04)) + } + .overlay { + Circle() + .stroke(Color(nsColor: .separatorColor).opacity(strokeOpacity), lineWidth: 0.5) + } + } +} + final class FilePreviewPDFThumbnailSidebarView: NSView, NSCollectionViewDataSource, NSCollectionViewDelegate, NSCollectionViewDelegateFlowLayout { private enum Metrics { static let thumbnailHeight = FilePreviewPDFSizing.thumbnailMaximumSize.height diff --git a/cmuxTests/WindowAndDragTests.swift b/cmuxTests/WindowAndDragTests.swift index 32c1986ae7d3..f1fb59aa52e7 100644 --- a/cmuxTests/WindowAndDragTests.swift +++ b/cmuxTests/WindowAndDragTests.swift @@ -2060,12 +2060,18 @@ final class FilePreviewPDFChromeTests: XCTestCase { NSPoint(x: zoomChromeHost.frame.midX, y: zoomChromeHost.frame.midY), to: container ) + let shareProbe = chromeHost.convert( + NSPoint(x: zoomChromeHost.frame.maxX - 20, y: zoomChromeHost.frame.midY), + to: container + ) let leftChromeHit = container.hitTest(leftProbe) let rightChromeHit = container.hitTest(rightProbe) - let debugFrames = "container=\(container.frame) content=\(String(describing: contentHost?.frame)) chromeHost=\(chromeHost.frame) left=\(sidebarChromeHost.frame) right=\(zoomChromeHost.frame) leftProbe=\(leftProbe) rightProbe=\(rightProbe) leftHit=\(String(describing: leftChromeHit)) rightHit=\(String(describing: rightChromeHit))" + let shareChromeHit = container.hitTest(shareProbe) + let debugFrames = "container=\(container.frame) content=\(String(describing: contentHost?.frame)) chromeHost=\(chromeHost.frame) left=\(sidebarChromeHost.frame) right=\(zoomChromeHost.frame) leftProbe=\(leftProbe) rightProbe=\(rightProbe) shareProbe=\(shareProbe) leftHit=\(String(describing: leftChromeHit)) rightHit=\(String(describing: rightChromeHit)) shareHit=\(String(describing: shareChromeHit))" XCTAssertTrue(isView(leftChromeHit, inside: sidebarChromeHost), debugFrames) XCTAssertTrue(isView(rightChromeHit, inside: zoomChromeHost), debugFrames) + XCTAssertTrue(isView(shareChromeHit, inside: zoomChromeHost), debugFrames) } func testThumbnailSidebarUsesFullWidthSingleColumnLayout() throws { From d1795c5d226aaad96f6114993b4a12178e729092 Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Wed, 13 May 2026 21:07:55 -0700 Subject: [PATCH 8/9] Address file preview chrome review feedback --- GhosttyTabs.xcodeproj/project.pbxproj | 4 + .../Panels/FilePreviewNativeBackground.swift | 47 ++++++ Sources/Panels/FilePreviewPanel.swift | 149 +++++++----------- 3 files changed, 110 insertions(+), 90 deletions(-) create mode 100644 Sources/Panels/FilePreviewNativeBackground.swift diff --git a/GhosttyTabs.xcodeproj/project.pbxproj b/GhosttyTabs.xcodeproj/project.pbxproj index bfb8b23e6750..fe26ab803a0d 100644 --- a/GhosttyTabs.xcodeproj/project.pbxproj +++ b/GhosttyTabs.xcodeproj/project.pbxproj @@ -83,6 +83,7 @@ D0B1001CA1B2C3D4E5F60001 /* PaneDropRoutingSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B1001DA1B2C3D4E5F60001 /* PaneDropRoutingSupport.swift */; }; D0B1001EA1B2C3D4E5F60001 /* BrowserPaneDropTargetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B1001FA1B2C3D4E5F60001 /* BrowserPaneDropTargetView.swift */; }; D0B10016A1B2C3D4E5F60001 /* FilePreviewTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B10017A1B2C3D4E5F60001 /* FilePreviewTextEditor.swift */; }; + A5001450A5001450A5001450 /* FilePreviewNativeBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001451A5001451A5001451 /* FilePreviewNativeBackground.swift */; }; D0B10018A1B2C3D4E5F60001 /* FileDropOverlayViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B10019A1B2C3D4E5F60001 /* FileDropOverlayViewTests.swift */; }; D7AB34300000000000000001 /* SidebarDropPlanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AB34300000000000000002 /* SidebarDropPlanner.swift */; }; D7AB34300000000000000003 /* SidebarBonsplitTabWorkspaceDropOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AB34300000000000000004 /* SidebarBonsplitTabWorkspaceDropOverlay.swift */; }; @@ -684,6 +685,7 @@ D0B1001DA1B2C3D4E5F60001 /* PaneDropRoutingSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaneDropRoutingSupport.swift; sourceTree = ""; }; D0B1001FA1B2C3D4E5F60001 /* BrowserPaneDropTargetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserPaneDropTargetView.swift; sourceTree = ""; }; D0B10017A1B2C3D4E5F60001 /* FilePreviewTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/FilePreviewTextEditor.swift; sourceTree = ""; }; + A5001451A5001451A5001451 /* FilePreviewNativeBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/FilePreviewNativeBackground.swift; sourceTree = ""; }; A5001016 /* GhosttyKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GhosttyKit.xcframework; sourceTree = ""; }; A5001017 /* ghostty.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ghostty.h; sourceTree = ""; }; A5001018 /* cmux-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "cmux-Bridging-Header.h"; sourceTree = ""; }; @@ -1306,6 +1308,7 @@ A50014F1 /* MarkdownViewerAssets.swift */, A5001423 /* FilePreviewPanel.swift */, D0B10017A1B2C3D4E5F60001 /* FilePreviewTextEditor.swift */, + A5001451A5001451A5001451 /* FilePreviewNativeBackground.swift */, A5001442A5001442A5001442 /* FilePreviewModeSupport.swift */, A5001444A5001444A5001444 /* FilePreviewWorkspaceOpenSupport.swift */, A5001446A5001446A5001446 /* FilePreviewMagnifyingPDFView.swift */, @@ -1985,6 +1988,7 @@ A50014F3 /* MarkdownViewerAssets.swift in Sources */, A5001422 /* FilePreviewPanel.swift in Sources */, D0B10016A1B2C3D4E5F60001 /* FilePreviewTextEditor.swift in Sources */, + A5001450A5001450A5001450 /* FilePreviewNativeBackground.swift in Sources */, A5001443A5001443A5001443 /* FilePreviewModeSupport.swift in Sources */, A5001445A5001445A5001445 /* FilePreviewWorkspaceOpenSupport.swift in Sources */, A5001447A5001447A5001447 /* FilePreviewMagnifyingPDFView.swift in Sources */, diff --git a/Sources/Panels/FilePreviewNativeBackground.swift b/Sources/Panels/FilePreviewNativeBackground.swift new file mode 100644 index 000000000000..4d5193102fea --- /dev/null +++ b/Sources/Panels/FilePreviewNativeBackground.swift @@ -0,0 +1,47 @@ +import AppKit + +enum FilePreviewNativeBackground { + static func resolvedColor(backgroundColor: NSColor, drawsBackground: Bool) -> NSColor { + drawsBackground ? backgroundColor : .clear + } + + static func applyRootLayer( + to view: NSView, + backgroundColor: NSColor, + drawsBackground: Bool + ) { + let resolvedBackgroundColor = resolvedColor( + backgroundColor: backgroundColor, + drawsBackground: drawsBackground + ) + view.wantsLayer = true + view.layer?.backgroundColor = resolvedBackgroundColor.cgColor + view.layer?.isOpaque = drawsBackground && resolvedBackgroundColor.alphaComponent >= 0.999 + } + + static func applyScrollBackgrounds( + in view: NSView, + backgroundColor: NSColor, + drawsBackground: Bool + ) { + let resolvedBackgroundColor = resolvedColor( + backgroundColor: backgroundColor, + drawsBackground: drawsBackground + ) + if let scrollView = view as? NSScrollView { + scrollView.drawsBackground = drawsBackground + scrollView.backgroundColor = resolvedBackgroundColor + } + if let clipView = view as? NSClipView { + clipView.drawsBackground = drawsBackground + clipView.backgroundColor = resolvedBackgroundColor + } + for subview in view.subviews { + applyScrollBackgrounds( + in: subview, + backgroundColor: backgroundColor, + drawsBackground: drawsBackground + ) + } + } +} diff --git a/Sources/Panels/FilePreviewPanel.swift b/Sources/Panels/FilePreviewPanel.swift index 7837c16b50b3..31d87d3e9fb7 100644 --- a/Sources/Panels/FilePreviewPanel.swift +++ b/Sources/Panels/FilePreviewPanel.swift @@ -26,52 +26,6 @@ enum FilePreviewInteraction { } -enum FilePreviewNativeBackground { - static func resolvedColor(backgroundColor: NSColor, drawsBackground: Bool) -> NSColor { - drawsBackground ? backgroundColor : .clear - } - - static func applyRootLayer( - to view: NSView, - backgroundColor: NSColor, - drawsBackground: Bool - ) { - let resolvedBackgroundColor = resolvedColor( - backgroundColor: backgroundColor, - drawsBackground: drawsBackground - ) - view.wantsLayer = true - view.layer?.backgroundColor = resolvedBackgroundColor.cgColor - view.layer?.isOpaque = drawsBackground && resolvedBackgroundColor.alphaComponent >= 0.999 - } - - static func applyScrollBackgrounds( - in view: NSView, - backgroundColor: NSColor, - drawsBackground: Bool - ) { - let resolvedBackgroundColor = resolvedColor( - backgroundColor: backgroundColor, - drawsBackground: drawsBackground - ) - if let scrollView = view as? NSScrollView { - scrollView.drawsBackground = drawsBackground - scrollView.backgroundColor = resolvedBackgroundColor - } - if let clipView = view as? NSClipView { - clipView.drawsBackground = drawsBackground - clipView.backgroundColor = resolvedBackgroundColor - } - for subview in view.subviews { - applyScrollBackgrounds( - in: subview, - backgroundColor: backgroundColor, - drawsBackground: drawsBackground - ) - } - } -} - struct FileExternalOpenApplication: Identifiable, Equatable, Sendable { let url: URL let displayName: String @@ -197,7 +151,6 @@ struct FileExternalOpenMenu: View { var style: FileExternalOpenMenuStyle = .header @State private var resolvedApplications: [FileExternalOpenApplication] = [] - @State private var isChromeHovered = false var body: some View { let applications = resolvedApplications @@ -227,16 +180,10 @@ struct FileExternalOpenMenu: View { } label: { label } - .modifier(FileExternalOpenButtonStyleModifier( - style: style, - isHovered: isChromeHovered - )) + .contentShape(Rectangle()) .disabled(isDisabled) .help(helpText) .accessibilityLabel(helpText) - .onHover { hovering in - isChromeHovered = hovering - } } } .task(id: fileURL) { @@ -252,6 +199,7 @@ struct FileExternalOpenMenu: View { case .chrome: Image(systemName: "square.and.arrow.up") .font(.system(size: 16, weight: .semibold)) + .foregroundStyle(.secondary) .frame(width: style.buttonSize.width, height: style.buttonSize.height) .contentShape(Rectangle()) .accessibilityHidden(true) @@ -365,23 +313,6 @@ struct FileExternalOpenMenu: View { } } -private struct FileExternalOpenButtonStyleModifier: ViewModifier { - let style: FileExternalOpenMenuStyle - let isHovered: Bool - - @ViewBuilder - func body(content: Content) -> some View { - switch style { - case .header: - content - case .chrome: - content - .buttonStyle(FilePreviewChromeHoverButtonStyle(isHovered: isHovered)) - .contentShape(Rectangle()) - } - } -} - private struct FileExternalOpenHeaderMenuButton: View { let fileURL: URL let primaryApplication: FileExternalOpenApplication? @@ -1857,6 +1788,7 @@ struct FilePreviewPDFChromeStyleModifier: ViewModifier { content .buttonStyle(.borderless) .controlSize(.regular) + .foregroundStyle(Color.secondary) } } @@ -1941,7 +1873,9 @@ struct FilePreviewPDFStandaloneChromeStyleModifier: ViewModifier { materialChrome(content: content, material: .thinMaterial, strokeOpacity: 0.75) case .plainToolbar: content + .buttonStyle(.borderless) .controlSize(.regular) + .foregroundStyle(Color.secondary) } } @@ -1950,6 +1884,9 @@ struct FilePreviewPDFStandaloneChromeStyleModifier: ViewModifier { #if compiler(>=6.3) if #available(macOS 26.0, *) { content + .buttonStyle(.borderless) + .controlSize(.regular) + .foregroundStyle(Color.secondary) .glassEffect(.regular, in: Circle()) .overlay { Circle() @@ -1970,6 +1907,9 @@ struct FilePreviewPDFStandaloneChromeStyleModifier: ViewModifier { strokeOpacity: Double ) -> some View { content + .buttonStyle(.borderless) + .controlSize(.regular) + .foregroundStyle(Color.secondary) .background { Circle() .fill(material) @@ -2410,6 +2350,8 @@ final class FilePreviewPDFContainerView: NSView, NSSplitViewDelegate, NSOutlineV private var rotationAccumulator: CGFloat = 0 private var previewBackgroundColor = NSColor.textBackgroundColor private var drawsPreviewBackground = true + private var lastAppliedPDFScrollBackgroundColor: NSColor? + private var lastAppliedPDFScrollDrawsBackground: Bool? private static let documentLoadQueue = DispatchQueue( label: "com.cmux.file-preview.pdf-document-load", qos: .userInitiated @@ -2466,6 +2408,7 @@ final class FilePreviewPDFContainerView: NSView, NSSplitViewDelegate, NSOutlineV guard previewBackgroundColor != backgroundColor || drawsPreviewBackground != drawsBackground else { return } previewBackgroundColor = backgroundColor drawsPreviewBackground = drawsBackground + invalidatePDFScrollBackgroundAppearance() applyBackgroundAppearance() } @@ -2515,6 +2458,8 @@ final class FilePreviewPDFContainerView: NSView, NSSplitViewDelegate, NSOutlineV updateSidebarContent() applyPreferredSidebarWidthIfNeeded() updatePageControls(scrollThumbnailToVisible: false) + invalidatePDFScrollBackgroundAppearance() + applyBackgroundAppearance() refreshPDFSmartFitWithoutViewportRestore() } @@ -2666,19 +2611,41 @@ final class FilePreviewPDFContainerView: NSView, NSSplitViewDelegate, NSOutlineV } private func applyBackgroundAppearance() { - let resolvedBackgroundColor = drawsPreviewBackground ? previewBackgroundColor : .clear - wantsLayer = true - layer?.backgroundColor = resolvedBackgroundColor.cgColor - layer?.isOpaque = drawsPreviewBackground && resolvedBackgroundColor.alphaComponent >= 0.999 - contentHost.wantsLayer = true - contentHost.layer?.backgroundColor = resolvedBackgroundColor.cgColor - contentHost.layer?.isOpaque = drawsPreviewBackground && resolvedBackgroundColor.alphaComponent >= 0.999 + FilePreviewNativeBackground.applyRootLayer( + to: self, + backgroundColor: previewBackgroundColor, + drawsBackground: drawsPreviewBackground + ) + FilePreviewNativeBackground.applyRootLayer( + to: contentHost, + backgroundColor: previewBackgroundColor, + drawsBackground: drawsPreviewBackground + ) + let resolvedBackgroundColor = FilePreviewNativeBackground.resolvedColor( + backgroundColor: previewBackgroundColor, + drawsBackground: drawsPreviewBackground + ) pdfView.backgroundColor = resolvedBackgroundColor + guard shouldApplyPDFScrollBackground(resolvedBackgroundColor: resolvedBackgroundColor) else { return } FilePreviewNativeBackground.applyScrollBackgrounds( in: pdfView, - backgroundColor: resolvedBackgroundColor, + backgroundColor: previewBackgroundColor, drawsBackground: drawsPreviewBackground ) + lastAppliedPDFScrollBackgroundColor = resolvedBackgroundColor + lastAppliedPDFScrollDrawsBackground = drawsPreviewBackground + } + + private func invalidatePDFScrollBackgroundAppearance() { + lastAppliedPDFScrollBackgroundColor = nil + lastAppliedPDFScrollDrawsBackground = nil + } + + private func shouldApplyPDFScrollBackground(resolvedBackgroundColor: NSColor) -> Bool { + guard let lastAppliedPDFScrollBackgroundColor, + let lastAppliedPDFScrollDrawsBackground else { return true } + return lastAppliedPDFScrollDrawsBackground != drawsPreviewBackground + || !lastAppliedPDFScrollBackgroundColor.isEqual(resolvedBackgroundColor) } private func setupFloatingChrome() { @@ -4318,26 +4285,33 @@ private struct QuickLookPreviewView: NSViewRepresentable { } previewView.isHidden = !isVisibleInUI previewView.autostarts = true + panel.attachPreviewFocus(root: previewView, primaryResponder: previewView, intent: .quickLook) + previewView.previewItem = context.coordinator.item(for: panel.fileURL, title: panel.displayTitle) Self.applyBackgroundAppearance( to: previewView, backgroundColor: backgroundColor, drawsBackground: drawsBackground ) - panel.attachPreviewFocus(root: previewView, primaryResponder: previewView, intent: .quickLook) - previewView.previewItem = context.coordinator.item(for: panel.fileURL, title: panel.displayTitle) return previewView } func updateNSView(_ nsView: NSView, context: Context) { nsView.isHidden = !isVisibleInUI + guard let previewView = nsView as? QLPreviewView else { + Self.applyBackgroundAppearance( + to: nsView, + backgroundColor: backgroundColor, + drawsBackground: drawsBackground + ) + return + } + panel.attachPreviewFocus(root: previewView, primaryResponder: previewView, intent: .quickLook) + previewView.previewItem = context.coordinator.item(for: panel.fileURL, title: panel.displayTitle) Self.applyBackgroundAppearance( - to: nsView, + to: previewView, backgroundColor: backgroundColor, drawsBackground: drawsBackground ) - guard let previewView = nsView as? QLPreviewView else { return } - panel.attachPreviewFocus(root: previewView, primaryResponder: previewView, intent: .quickLook) - previewView.previewItem = context.coordinator.item(for: panel.fileURL, title: panel.displayTitle) } static func dismantleNSView(_ nsView: NSView, coordinator: Coordinator) { @@ -4362,11 +4336,6 @@ private struct QuickLookPreviewView: NSViewRepresentable { backgroundColor: backgroundColor, drawsBackground: drawsBackground ) - FilePreviewNativeBackground.applyScrollBackgrounds( - in: view, - backgroundColor: backgroundColor, - drawsBackground: drawsBackground - ) } final class Coordinator { From 82d8312bbc84e7f5bea8e5fa94d7feef041d0292 Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Wed, 13 May 2026 21:32:29 -0700 Subject: [PATCH 9/9] Fix PDF background cache invalidation --- .../Panels/FilePreviewNativeBackground.swift | 18 +++++++ Sources/Panels/FilePreviewPanel.swift | 48 ++++++++++++++----- Sources/Panels/FilePreviewTextEditor.swift | 4 -- 3 files changed, 54 insertions(+), 16 deletions(-) diff --git a/Sources/Panels/FilePreviewNativeBackground.swift b/Sources/Panels/FilePreviewNativeBackground.swift index 4d5193102fea..9797c0b066af 100644 --- a/Sources/Panels/FilePreviewNativeBackground.swift +++ b/Sources/Panels/FilePreviewNativeBackground.swift @@ -44,4 +44,22 @@ enum FilePreviewNativeBackground { ) } } + + static func scrollBackgroundHostIdentifiers(in view: NSView) -> Set { + var identifiers = Set() + collectScrollBackgroundHostIdentifiers(in: view, into: &identifiers) + return identifiers + } + + private static func collectScrollBackgroundHostIdentifiers( + in view: NSView, + into identifiers: inout Set + ) { + if view is NSScrollView || view is NSClipView { + identifiers.insert(ObjectIdentifier(view)) + } + for subview in view.subviews { + collectScrollBackgroundHostIdentifiers(in: subview, into: &identifiers) + } + } } diff --git a/Sources/Panels/FilePreviewPanel.swift b/Sources/Panels/FilePreviewPanel.swift index 31d87d3e9fb7..7fbd4d9dc2ec 100644 --- a/Sources/Panels/FilePreviewPanel.swift +++ b/Sources/Panels/FilePreviewPanel.swift @@ -2350,13 +2350,24 @@ final class FilePreviewPDFContainerView: NSView, NSSplitViewDelegate, NSOutlineV private var rotationAccumulator: CGFloat = 0 private var previewBackgroundColor = NSColor.textBackgroundColor private var drawsPreviewBackground = true - private var lastAppliedPDFScrollBackgroundColor: NSColor? - private var lastAppliedPDFScrollDrawsBackground: Bool? + private var lastAppliedPDFScrollBackgroundAppearance: PDFScrollBackgroundAppearance? private static let documentLoadQueue = DispatchQueue( label: "com.cmux.file-preview.pdf-document-load", qos: .userInitiated ) + private struct PDFScrollBackgroundAppearance { + let hostIdentifiers: Set + let backgroundColor: NSColor + let drawsBackground: Bool + + func matches(_ other: PDFScrollBackgroundAppearance) -> Bool { + hostIdentifiers == other.hostIdentifiers + && drawsBackground == other.drawsBackground + && backgroundColor.isEqual(other.backgroundColor) + } + } + override init(frame frameRect: NSRect) { super.init(frame: frameRect) setupView() @@ -2626,26 +2637,39 @@ final class FilePreviewPDFContainerView: NSView, NSSplitViewDelegate, NSOutlineV drawsBackground: drawsPreviewBackground ) pdfView.backgroundColor = resolvedBackgroundColor - guard shouldApplyPDFScrollBackground(resolvedBackgroundColor: resolvedBackgroundColor) else { return } + let scrollBackgroundAppearance = currentPDFScrollBackgroundAppearance( + resolvedBackgroundColor: resolvedBackgroundColor + ) + guard shouldApplyPDFScrollBackground(scrollBackgroundAppearance) else { return } FilePreviewNativeBackground.applyScrollBackgrounds( in: pdfView, backgroundColor: previewBackgroundColor, drawsBackground: drawsPreviewBackground ) - lastAppliedPDFScrollBackgroundColor = resolvedBackgroundColor - lastAppliedPDFScrollDrawsBackground = drawsPreviewBackground + lastAppliedPDFScrollBackgroundAppearance = scrollBackgroundAppearance } private func invalidatePDFScrollBackgroundAppearance() { - lastAppliedPDFScrollBackgroundColor = nil - lastAppliedPDFScrollDrawsBackground = nil + lastAppliedPDFScrollBackgroundAppearance = nil + } + + private func currentPDFScrollBackgroundAppearance( + resolvedBackgroundColor: NSColor + ) -> PDFScrollBackgroundAppearance { + var hostIdentifiers = FilePreviewNativeBackground.scrollBackgroundHostIdentifiers(in: pdfView) + if hostIdentifiers.isEmpty { + hostIdentifiers.insert(ObjectIdentifier(pdfView)) + } + return PDFScrollBackgroundAppearance( + hostIdentifiers: hostIdentifiers, + backgroundColor: resolvedBackgroundColor, + drawsBackground: drawsPreviewBackground + ) } - private func shouldApplyPDFScrollBackground(resolvedBackgroundColor: NSColor) -> Bool { - guard let lastAppliedPDFScrollBackgroundColor, - let lastAppliedPDFScrollDrawsBackground else { return true } - return lastAppliedPDFScrollDrawsBackground != drawsPreviewBackground - || !lastAppliedPDFScrollBackgroundColor.isEqual(resolvedBackgroundColor) + private func shouldApplyPDFScrollBackground(_ appearance: PDFScrollBackgroundAppearance) -> Bool { + guard let lastAppliedPDFScrollBackgroundAppearance else { return true } + return !lastAppliedPDFScrollBackgroundAppearance.matches(appearance) } private func setupFloatingChrome() { diff --git a/Sources/Panels/FilePreviewTextEditor.swift b/Sources/Panels/FilePreviewTextEditor.swift index 7201a93551b4..a2a14d699410 100644 --- a/Sources/Panels/FilePreviewTextEditor.swift +++ b/Sources/Panels/FilePreviewTextEditor.swift @@ -106,10 +106,6 @@ struct FilePreviewTextEditor: NSViewRepresentable where PanelModel: } } - static func shouldDrawBackground(for backgroundColor: NSColor) -> Bool { - backgroundColor.alphaComponent > 0.001 - } - final class Coordinator: NSObject, NSTextViewDelegate { var panel: PanelModel var isApplyingPanelUpdate = false