Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 23 additions & 23 deletions Sources/GhosttyTerminalView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7066,17 +7066,17 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
private var lastScrollEventTime: CFTimeInterval = 0
private var visibleInUI: Bool = true
private var pendingSurfaceSize: CGSize?
private var deferredSurfaceSizeRetryQueued = false
private var deferredSurfaceSizeRetryQueued = false, needsSurfaceSizeRetryAfterMetalLayerRealizes = false
private var deferredSurfaceSizeNonMetalRetryCount = 0
private var lastDrawableSize: CGSize = .zero
private var isFindEscapeSuppressionArmed = false
private var hasPendingLeftMouseRelease = false
#if DEBUG
private var lastSizeSkipSignature: String?
#endif
private static let maxDeferredSurfaceSizeNonMetalRetryCount = 8

private var hasUsableFocusGeometry: Bool {
bounds.width > 1 && bounds.height > 1
}
private var hasUsableFocusGeometry: Bool { bounds.width > 1 && bounds.height > 1 }

static func shouldRequestFirstResponderForMouseFocus(
focusFollowsMouseEnabled: Bool,
Expand Down Expand Up @@ -7120,6 +7120,7 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
metalLayer.setRenderDemand(GhosttyApp.renderedFrameNotificationDemand)
metalLayer.pixelFormat = .bgra8Unorm
metalLayer.isOpaque = false
Task { @MainActor [weak self] in self?.reconcileSurfaceSizeAfterMetalLayerAttachIfNeeded() }
// framebufferOnly=false lets the macOS compositor read the drawable
// when blending translucent or blurred window layers. This matches
// standalone Ghostty's SurfaceView and is required for background-opacity
Expand Down Expand Up @@ -7408,18 +7409,15 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
size.height > 0 {
return size
}

let currentBounds = bounds.size
if currentBounds.width > 0, currentBounds.height > 0 {
return currentBounds
}

if let pending = pendingSurfaceSize,
pending.width > 0,
pending.height > 0 {
return pending
}

return currentBounds
}

Expand Down Expand Up @@ -7451,23 +7449,19 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
}

private func activeSurfaceResizeDeferralReason() -> String? {
if inLiveResize || window?.inLiveResize == true {
return nil
}
if inLiveResize || window?.inLiveResize == true { return nil }
return Self.shouldDeferSurfaceResizeForActiveDrag() ? "tabDrag" : nil
}

private func scheduleDeferredSurfaceSizeRetryIfNeeded() {
guard window != nil else { return }
guard !deferredSurfaceSizeRetryQueued else { return }
@discardableResult private func scheduleDeferredSurfaceSizeRetryIfNeeded() -> Bool {
guard window != nil, !deferredSurfaceSizeRetryQueued else { return false }
deferredSurfaceSizeRetryQueued = true
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.deferredSurfaceSizeRetryQueued = false
_ = self.updateSurfaceSize()
}
Task { @MainActor [weak self] in guard let self else { return }; self.deferredSurfaceSizeRetryQueued = false; _ = self.updateSurfaceSize() }
return true
}

@MainActor fileprivate func reconcileSurfaceSizeAfterMetalLayerAttachIfNeeded() { guard needsSurfaceSizeRetryAfterMetalLayerRealizes else { return }; deferredSurfaceSizeNonMetalRetryCount = 0; _ = updateSurfaceSize() }

@discardableResult
private func updateSurfaceSize(size: CGSize? = nil) -> Bool {
guard let terminalSurface = terminalSurface else { return false }
Expand All @@ -7486,6 +7480,7 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
#endif
return false
}
if pendingSurfaceSize != size { deferredSurfaceSizeNonMetalRetryCount = 0 }
pendingSurfaceSize = size
if let deferralReason = activeSurfaceResizeDeferralReason() {
scheduleDeferredSurfaceSizeRetryIfNeeded()
Expand Down Expand Up @@ -7561,15 +7556,19 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
layer?.contentsScale = layerScale
layer?.masksToBounds = true
if let metalLayer = layer as? CAMetalLayer {
deferredSurfaceSizeNonMetalRetryCount = 0
needsSurfaceSizeRetryAfterMetalLayerRealizes = false
if drawablePixelSize != lastDrawableSize || metalLayer.drawableSize != drawablePixelSize {
if metalLayer.drawableSize != drawablePixelSize {
didChange = true
}
if metalLayer.drawableSize != drawablePixelSize {
metalLayer.drawableSize = drawablePixelSize
}
lastDrawableSize = drawablePixelSize
}
} else if deferredSurfaceSizeNonMetalRetryCount < Self.maxDeferredSurfaceSizeNonMetalRetryCount,
scheduleDeferredSurfaceSizeRetryIfNeeded() {
needsSurfaceSizeRetryAfterMetalLayerRealizes = true
deferredSurfaceSizeNonMetalRetryCount += 1
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
CATransaction.commit()

Expand All @@ -7590,9 +7589,10 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
}

#if DEBUG
fileprivate func debugPendingSurfaceSize() -> CGSize? {
pendingSurfaceSize
}
fileprivate func debugPendingSurfaceSize() -> CGSize? { pendingSurfaceSize }
func debugLastDrawableSizeForTesting() -> CGSize { lastDrawableSize }
func debugDeferredSurfaceSizeRetryQueuedForTesting() -> Bool { deferredSurfaceSizeRetryQueued }
@discardableResult func debugUpdateSurfaceSizeForTesting(_ size: CGSize) -> Bool { updateSurfaceSize(size: size) }
#endif

/// Force a full size reconciliation for the current bounds.
Expand Down
4 changes: 4 additions & 0 deletions cmux.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@
C3873001C3873001C3873001 /* GhosttyCrashBreadcrumb.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3873002C3873002C3873002 /* GhosttyCrashBreadcrumb.swift */; };
C3873003C3873003C3873003 /* GhosttyCrashReportMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3873004C3873004C3873004 /* GhosttyCrashReportMetadata.swift */; };
D5935001A1B2C3D4E5F60718 /* GhosttyDECCKMArrowKeyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5935002A1B2C3D4E5F60718 /* GhosttyDECCKMArrowKeyTests.swift */; };
604100010000000000000001 /* GhosttyDrawableSizeRetryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 604100010000000000000002 /* GhosttyDrawableSizeRetryTests.swift */; };
F9000000A1B2C3D4E5F60718 /* GhosttyEnsureFocusWindowActivationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9000001A1B2C3D4E5F60718 /* GhosttyEnsureFocusWindowActivationTests.swift */; };
048A66B6400AB53182FA4304 /* GhosttyKeyModifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77F5AB36771F1414A95E3330 /* GhosttyKeyModifiers.swift */; };
A5001006 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5001016 /* GhosttyKit.xcframework */; };
Expand Down Expand Up @@ -1231,6 +1232,7 @@
C3873002C3873002C3873002 /* GhosttyCrashBreadcrumb.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyCrashBreadcrumb.swift; sourceTree = "<group>"; };
C3873004C3873004C3873004 /* GhosttyCrashReportMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyCrashReportMetadata.swift; sourceTree = "<group>"; };
D5935002A1B2C3D4E5F60718 /* GhosttyDECCKMArrowKeyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyDECCKMArrowKeyTests.swift; sourceTree = "<group>"; };
604100010000000000000002 /* GhosttyDrawableSizeRetryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyDrawableSizeRetryTests.swift; sourceTree = "<group>"; };
F9000001A1B2C3D4E5F60718 /* GhosttyEnsureFocusWindowActivationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyEnsureFocusWindowActivationTests.swift; sourceTree = "<group>"; };
77F5AB36771F1414A95E3330 /* GhosttyKeyModifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyKeyModifiers.swift; sourceTree = "<group>"; };
A5001016 /* GhosttyKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GhosttyKit.xcframework; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2404,6 +2406,7 @@
C0F16A000000000000000002 /* ShellStartupMatrixTests.swift */,
C0F16C000000000000000002 /* ShellStartupMissingBundleTests.swift */,
F4000001A1B2C3D4E5F60718 /* GhosttyConfigTests.swift */,
604100010000000000000002 /* GhosttyDrawableSizeRetryTests.swift */,
BCBC0A0E0000000000000D02 /* BrowserChromeMetricsTests.swift */,
A11EB0000000000000000001 /* GhosttyConfigPathResolverTests.swift */,
C13519000000000000000004 /* GhosttyTerminalStartupEnvironmentTests.swift */,
Expand Down Expand Up @@ -3695,6 +3698,7 @@
A11EB0000000000000000000 /* GhosttyConfigPathResolverTests.swift in Sources */,
F4000000A1B2C3D4E5F60718 /* GhosttyConfigTests.swift in Sources */,
D5935001A1B2C3D4E5F60718 /* GhosttyDECCKMArrowKeyTests.swift in Sources */,
604100010000000000000001 /* GhosttyDrawableSizeRetryTests.swift in Sources */,
F9000000A1B2C3D4E5F60718 /* GhosttyEnsureFocusWindowActivationTests.swift in Sources */,
A11EAF000000000000000000 /* GhosttyNotificationDispatcherTests.swift in Sources */,
BDFCA25C0CE77A299C915E26 /* GhosttyOptionAsAltModsTests.swift in Sources */,
Expand Down
113 changes: 113 additions & 0 deletions cmuxTests/GhosttyDrawableSizeRetryTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import AppKit
import QuartzCore
import Testing

#if canImport(cmux_DEV)
@testable import cmux_DEV
#elseif canImport(cmux)
@testable import cmux
#endif

@MainActor
@Suite
struct GhosttyDrawableSizeRetryTests {
@Test func reconcilesDrawableAfterFullSizeUpdateRunsBeforeMetalLayerRealizes() throws {
_ = NSApplication.shared

let initialSize = CGSize(width: 800, height: 600)
let targetSize = CGSize(width: 1296, height: 893)
let initialFrame = NSRect(origin: .zero, size: initialSize)
let terminalSurface = TerminalSurface(
tabId: UUID(),
context: GHOSTTY_SURFACE_CONTEXT_SPLIT,
configTemplate: nil,
workingDirectory: nil
)
let hostedView = terminalSurface.hostedView
let window = NSWindow(
contentRect: initialFrame,
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
)
window.isReleasedWhenClosed = false
defer {
window.orderOut(nil)
window.close()
}

let contentView = try #require(window.contentView)
hostedView.frame = initialFrame
hostedView.autoresizingMask = [.width, .height]
contentView.addSubview(hostedView)

window.makeKeyAndOrderFront(nil)
window.displayIfNeeded()
contentView.layoutSubtreeIfNeeded()
hostedView.layoutSubtreeIfNeeded()
_ = hostedView.reconcileGeometryNow()

let surfaceView = try #require(findGhosttyNSView(in: hostedView))
let initialDrawableSize = surfaceView.convertToBacking(initialFrame).size
_ = surfaceView.forceRefreshSurface()
#expect(surfaceView.layer is CAMetalLayer)
#expect(surfaceView.debugLastDrawableSizeForTesting() == initialDrawableSize)
drainDeferredSurfaceSizeRetry(on: surfaceView)
#expect(!surfaceView.debugDeferredSurfaceSizeRetryQueuedForTesting())

let nonMetalLayer = CALayer()
nonMetalLayer.contentsScale = window.backingScaleFactor
surfaceView.layer = nonMetalLayer
#expect(!surfaceView.debugDeferredSurfaceSizeRetryQueuedForTesting())

let targetFrame = NSRect(origin: .zero, size: targetSize)
window.setFrame(targetFrame, display: false)
hostedView.frame = targetFrame
surfaceView.frame = targetFrame
#expect(surfaceView.bounds.size == targetSize)

let expectedDrawableSize = surfaceView.convertToBacking(targetFrame).size
#expect(expectedDrawableSize.width > 0)
#expect(expectedDrawableSize.height > 0)
#expect(expectedDrawableSize != initialDrawableSize)

_ = surfaceView.debugUpdateSurfaceSizeForTesting(targetSize)

#expect(surfaceView.debugLastDrawableSizeForTesting() == initialDrawableSize)
#expect(surfaceView.debugDeferredSurfaceSizeRetryQueuedForTesting())

let realizedLayer = try #require(surfaceView.makeBackingLayer() as? CAMetalLayer)
realizedLayer.contentsScale = window.backingScaleFactor
realizedLayer.masksToBounds = true
realizedLayer.drawableSize = initialDrawableSize
surfaceView.layer = realizedLayer

let deadline = Date().addingTimeInterval(0.5)
while realizedLayer.drawableSize != expectedDrawableSize && Date() < deadline {
RunLoop.current.run(until: Date().addingTimeInterval(0.01))
}

#expect(realizedLayer.drawableSize == expectedDrawableSize)
}

private func findGhosttyNSView(in view: NSView) -> GhosttyNSView? {
if let view = view as? GhosttyNSView {
return view
}

for subview in view.subviews {
if let match = findGhosttyNSView(in: subview) {
return match
}
}

return nil
}

private func drainDeferredSurfaceSizeRetry(on surfaceView: GhosttyNSView) {
let deadline = Date().addingTimeInterval(0.5)
while surfaceView.debugDeferredSurfaceSizeRetryQueuedForTesting() && Date() < deadline {
RunLoop.current.run(until: Date().addingTimeInterval(0.01))
}
}
}
Loading