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..9797c0b066af --- /dev/null +++ b/Sources/Panels/FilePreviewNativeBackground.swift @@ -0,0 +1,65 @@ +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 + ) + } + } + + 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 1c61f5af4cb7..7fbd4d9dc2ec 100644 --- a/Sources/Panels/FilePreviewPanel.swift +++ b/Sources/Panels/FilePreviewPanel.swift @@ -134,6 +134,15 @@ 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: 40, height: 40) + } + } } struct FileExternalOpenMenu: View { @@ -162,15 +171,16 @@ struct FileExternalOpenMenu: View { isDisabled: isDisabled ) case .chrome: - Menu { - menuContent(primaryApplication: primaryApplication, otherApplications: otherApplications) + Button { + presentMenu( + applications: applications, + currentPrimaryApplication: primaryApplication, + otherApplications: otherApplications + ) } label: { label } - .menuStyle(.borderlessButton) - .menuIndicator(.hidden) - .buttonStyle(.plain) - .foregroundColor(.secondary) + .contentShape(Rectangle()) .disabled(isDisabled) .help(helpText) .accessibilityLabel(helpText) @@ -181,32 +191,6 @@ struct FileExternalOpenMenu: View { } } - @ViewBuilder - private func menuContent( - primaryApplication: FileExternalOpenApplication?, - otherApplications: [FileExternalOpenApplication] - ) -> some View { - 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) - } - } - } - @ViewBuilder private var label: some View { switch style { @@ -215,7 +199,10 @@ struct FileExternalOpenMenu: View { case .chrome: Image(systemName: "square.and.arrow.up") .font(.system(size: 16, weight: .semibold)) - .frame(width: 42, height: 40) + .foregroundStyle(.secondary) + .frame(width: style.buttonSize.width, height: style.buttonSize.height) + .contentShape(Rectangle()) + .accessibilityHidden(true) } } @@ -244,6 +231,86 @@ 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(menuItem( + title: openInTitle(primaryApplication.displayName), + applicationURL: primaryApplication.url + )) + + 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, + 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, applicationURL: nil)) + } + + return menu + } + + private func menuItem(title: String, applicationURL: URL?) -> NSMenuItem { + let item = NSMenuItem( + title: title, + action: #selector(FileExternalOpenMenuActionTarget.open(_:)), + keyEquivalent: "" + ) + item.target = FileExternalOpenMenuActionTarget.shared + item.representedObject = FileExternalOpenMenuActionPayload( + fileURL: fileURL, + applicationURL: applicationURL + ) + return item + } } private struct FileExternalOpenHeaderMenuButton: View { @@ -269,13 +336,13 @@ private struct FileExternalOpenHeaderMenuButton: View { if let event = NSApp.currentEvent, let contentView = event.window?.contentView { let point = contentView.convert(event.locationInWindow, from: nil) - menu.popUp(positioning: nil, at: point, in: contentView) + menu.popUp(positioning: nil as NSMenuItem?, at: point, in: contentView) return } guard let contentView = NSApp.keyWindow?.contentView else { return } menu.popUp( - positioning: nil, + positioning: nil as NSMenuItem?, at: NSPoint(x: contentView.bounds.maxX - 24, y: contentView.bounds.maxY - 32), in: contentView ) @@ -1125,14 +1192,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 { @@ -1142,7 +1209,7 @@ struct FilePreviewPanelView: View { content } .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color.clear) + .background(Color(nsColor: contentBackgroundColor)) .overlay { RoundedRectangle(cornerRadius: FocusFlashPattern.ringCornerRadius) .stroke(cmuxAccentColor().opacity(focusFlashOpacity), lineWidth: 3) @@ -1196,17 +1263,38 @@ struct FilePreviewPanelView: View { FilePreviewTextEditor( panel: panel, isVisibleInUI: isVisibleInUI, - themeBackgroundColor: .clear, - 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 + ) } } } @@ -1260,10 +1348,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 @@ -1271,6 +1362,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) } @@ -1496,7 +1588,7 @@ struct FilePreviewPDFZoomChromeView: View { zoomButtons(includeDividers: false) secondaryButtons(includeDividers: false) if let fileURL { - FileExternalOpenMenu(fileURL: fileURL) + FileExternalOpenMenu(fileURL: fileURL, style: .chrome) } } label: { Label( @@ -1523,8 +1615,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)) } } } @@ -1696,6 +1788,7 @@ struct FilePreviewPDFChromeStyleModifier: ViewModifier { content .buttonStyle(.borderless) .controlSize(.regular) + .foregroundStyle(Color.secondary) } } @@ -1760,6 +1853,76 @@ 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 + .buttonStyle(.borderless) + .controlSize(.regular) + .foregroundStyle(Color.secondary) + } + } + + @ViewBuilder + private func liquidGlassChrome(content: Content) -> some View { + #if compiler(>=6.3) + if #available(macOS 26.0, *) { + content + .buttonStyle(.borderless) + .controlSize(.regular) + .foregroundStyle(Color.secondary) + .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 + .buttonStyle(.borderless) + .controlSize(.regular) + .foregroundStyle(Color.secondary) + .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 @@ -2150,7 +2313,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 } @@ -2185,11 +2348,26 @@ 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 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() @@ -2212,6 +2390,7 @@ final class FilePreviewPDFContainerView: NSView, NSSplitViewDelegate, NSOutlineV override func layout() { super.layout() + applyBackgroundAppearance() if !didSetInitialSidebarWidth, bounds.width > 0 { didSetInitialSidebarWidth = true let initialWidth = clampedSidebarWidth(lastSidebarWidth) @@ -2236,6 +2415,14 @@ final class FilePreviewPDFContainerView: NSView, NSSplitViewDelegate, NSOutlineV registerFocusEndpoint() } + func setBackgroundAppearance(backgroundColor: NSColor, drawsBackground: Bool) { + guard previewBackgroundColor != backgroundColor || drawsPreviewBackground != drawsBackground else { return } + previewBackgroundColor = backgroundColor + drawsPreviewBackground = drawsBackground + invalidatePDFScrollBackgroundAppearance() + applyBackgroundAppearance() + } + func setURL(_ url: URL) { guard currentURL != url else { applyPreferredSidebarWidthIfNeeded() @@ -2282,6 +2469,8 @@ final class FilePreviewPDFContainerView: NSView, NSSplitViewDelegate, NSOutlineV updateSidebarContent() applyPreferredSidebarWidthIfNeeded() updatePageControls(scrollThumbnailToVisible: false) + invalidatePDFScrollBackgroundAppearance() + applyBackgroundAppearance() refreshPDFSmartFitWithoutViewportRestore() } @@ -2291,11 +2480,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 @@ -2419,7 +2608,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) @@ -2433,6 +2621,57 @@ final class FilePreviewPDFContainerView: NSView, NSSplitViewDelegate, NSOutlineV ]) } + private func applyBackgroundAppearance() { + 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 + let scrollBackgroundAppearance = currentPDFScrollBackgroundAppearance( + resolvedBackgroundColor: resolvedBackgroundColor + ) + guard shouldApplyPDFScrollBackground(scrollBackgroundAppearance) else { return } + FilePreviewNativeBackground.applyScrollBackgrounds( + in: pdfView, + backgroundColor: previewBackgroundColor, + drawsBackground: drawsPreviewBackground + ) + lastAppliedPDFScrollBackgroundAppearance = scrollBackgroundAppearance + } + + private func invalidatePDFScrollBackgroundAppearance() { + 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(_ appearance: PDFScrollBackgroundAppearance) -> Bool { + guard let lastAppliedPDFScrollBackgroundAppearance else { return true } + return !lastAppliedPDFScrollBackgroundAppearance.matches(appearance) + } + private func setupFloatingChrome() { chromeHost.frame = bounds.width > 0 && bounds.height > 0 ? bounds @@ -2464,6 +2703,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), @@ -2472,7 +2714,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), @@ -3241,10 +3483,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 @@ -3252,6 +3497,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) } @@ -3331,6 +3577,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 @@ -3355,6 +3603,7 @@ private final class FilePreviewImageContainerView: NSView { override func layout() { super.layout() + applyBackgroundAppearance() if isFitMode { scale = fitScale() } @@ -3379,6 +3628,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 } @@ -3435,8 +3691,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 @@ -3476,6 +3730,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() { @@ -3954,6 +4225,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() @@ -3965,6 +4238,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 @@ -3972,10 +4250,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? @@ -3998,25 +4293,49 @@ 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 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 + ) return previewView } func updateNSView(_ nsView: NSView, context: Context) { nsView.isHidden = !isVisibleInUI - guard let previewView = nsView as? QLPreviewView else { return } + 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: previewView, + backgroundColor: backgroundColor, + drawsBackground: drawsBackground + ) } static func dismantleNSView(_ nsView: NSView, coordinator: Coordinator) { @@ -4031,6 +4350,18 @@ private struct QuickLookPreviewView: NSViewRepresentable { Coordinator() } + private static func applyBackgroundAppearance( + to view: NSView, + backgroundColor: NSColor, + drawsBackground: Bool + ) { + FilePreviewNativeBackground.applyRootLayer( + to: 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 36e66b0138dc..a2a14d699410 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 = false + 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 = false + 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,9 +90,9 @@ struct FilePreviewTextEditor: NSViewRepresentable where PanelModel: static func applyTheme( to scrollView: NSScrollView, backgroundColor: NSColor, - foregroundColor: NSColor + foregroundColor: NSColor, + drawsBackground: Bool ) { - let drawsBackground = shouldDrawBackground(for: backgroundColor) let resolvedBackgroundColor = drawsBackground ? backgroundColor : .clear scrollView.drawsBackground = drawsBackground scrollView.backgroundColor = resolvedBackgroundColor @@ -95,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 diff --git a/Sources/Panels/MarkdownPanelView.swift b/Sources/Panels/MarkdownPanelView.swift index c66d974102f1..05d0fdbfe3d3 100644 --- a/Sources/Panels/MarkdownPanelView.swift +++ b/Sources/Panels/MarkdownPanelView.swift @@ -53,7 +53,7 @@ struct MarkdownPanelView: View { } } .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color.clear) + .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: .clear, - themeForegroundColor: themeForegroundColor + themeBackgroundColor: appearance.contentBackgroundColor, + themeForegroundColor: themeForegroundColor, + drawsBackground: appearance.drawsContentBackground ) .frame(maxWidth: .infinity, maxHeight: .infinity) } @@ -183,6 +185,10 @@ struct MarkdownPanelView: View { // MARK: - Theme + private var contentBackgroundColor: Color { + Color(nsColor: appearance.contentBackgroundColor) + } + private var themeBackgroundColor: NSColor { appearance.backgroundColor } diff --git a/Sources/Panels/MarkdownWebRenderer.swift b/Sources/Panels/MarkdownWebRenderer.swift index 00bf23b32974..a437fa224f36 100644 --- a/Sources/Panels/MarkdownWebRenderer.swift +++ b/Sources/Panels/MarkdownWebRenderer.swift @@ -40,6 +40,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 @@ -65,6 +66,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 @@ -91,6 +93,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) } @@ -115,6 +118,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..aa065d0b5200 100644 --- a/cmuxTests/TerminalAndGhosttyTests.swift +++ b/cmuxTests/TerminalAndGhosttyTests.swift @@ -1578,6 +1578,62 @@ 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.backgroundColor.alphaComponent, 0.42, accuracy: 0.0001) + 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.backgroundColor.alphaComponent, 1.0, accuracy: 0.0001) + 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.backgroundColor.alphaComponent, 1.0, accuracy: 0.0001) + 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.backgroundColor.alphaComponent, 1.0, accuracy: 0.0001) + XCTAssertEqual(appearance.contentBackgroundColor.alphaComponent, 0.0, accuracy: 0.0001) + } +} + final class GhosttyResponderResolutionTests: XCTestCase { private final class FocusProbeView: NSView { diff --git a/cmuxTests/WindowAndDragTests.swift b/cmuxTests/WindowAndDragTests.swift index 1ce14e3316b0..c12418c81b54 100644 --- a/cmuxTests/WindowAndDragTests.swift +++ b/cmuxTests/WindowAndDragTests.swift @@ -1885,12 +1885,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 { @@ -2438,7 +2444,8 @@ final class FilePreviewPanelTextSavingTests: XCTestCase { FilePreviewTextEditor.applyTheme( to: scrollView, backgroundColor: .clear, - foregroundColor: .white + foregroundColor: .white, + drawsBackground: false ) XCTAssertFalse(scrollView.drawsBackground) @@ -2461,7 +2468,8 @@ final class FilePreviewPanelTextSavingTests: XCTestCase { FilePreviewTextEditor.applyTheme( to: scrollView, backgroundColor: backgroundColor, - foregroundColor: .white + foregroundColor: .white, + drawsBackground: true ) XCTAssertTrue(scrollView.drawsBackground)