Fix tab drag blank state and preserve non-custom titles across window drops
This commit is contained in:
parent
6c6f363e4b
commit
6505f0c504
6 changed files with 252 additions and 57 deletions
|
|
@ -74,13 +74,14 @@
|
||||||
D0E0F0B0A1B2C3D4E5F60718 /* BrowserPaneNavigationKeybindUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E0F0B1A1B2C3D4E5F60718 /* BrowserPaneNavigationKeybindUITests.swift */; };
|
D0E0F0B0A1B2C3D4E5F60718 /* BrowserPaneNavigationKeybindUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E0F0B1A1B2C3D4E5F60718 /* BrowserPaneNavigationKeybindUITests.swift */; };
|
||||||
D0E0F0B2A1B2C3D4E5F60718 /* BrowserOmnibarSuggestionsUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E0F0B3A1B2C3D4E5F60718 /* BrowserOmnibarSuggestionsUITests.swift */; };
|
D0E0F0B2A1B2C3D4E5F60718 /* BrowserOmnibarSuggestionsUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E0F0B3A1B2C3D4E5F60718 /* BrowserOmnibarSuggestionsUITests.swift */; };
|
||||||
E1000000A1B2C3D4E5F60718 /* MenuKeyEquivalentRoutingUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1000001A1B2C3D4E5F60718 /* MenuKeyEquivalentRoutingUITests.swift */; };
|
E1000000A1B2C3D4E5F60718 /* MenuKeyEquivalentRoutingUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1000001A1B2C3D4E5F60718 /* MenuKeyEquivalentRoutingUITests.swift */; };
|
||||||
F1000000A1B2C3D4E5F60718 /* CmuxWebViewKeyEquivalentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1000001A1B2C3D4E5F60718 /* CmuxWebViewKeyEquivalentTests.swift */; };
|
F1000000A1B2C3D4E5F60718 /* CmuxWebViewKeyEquivalentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1000001A1B2C3D4E5F60718 /* CmuxWebViewKeyEquivalentTests.swift */; };
|
||||||
F2000000A1B2C3D4E5F60718 /* UpdatePillReleaseVisibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2000001A1B2C3D4E5F60718 /* UpdatePillReleaseVisibilityTests.swift */; };
|
F2000000A1B2C3D4E5F60718 /* UpdatePillReleaseVisibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2000001A1B2C3D4E5F60718 /* UpdatePillReleaseVisibilityTests.swift */; };
|
||||||
F3000000A1B2C3D4E5F60718 /* CJKIMEInputTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3000001A1B2C3D4E5F60718 /* CJKIMEInputTests.swift */; };
|
F3000000A1B2C3D4E5F60718 /* CJKIMEInputTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3000001A1B2C3D4E5F60718 /* CJKIMEInputTests.swift */; };
|
||||||
F4000000A1B2C3D4E5F60718 /* GhosttyConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4000001A1B2C3D4E5F60718 /* GhosttyConfigTests.swift */; };
|
F4000000A1B2C3D4E5F60718 /* GhosttyConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4000001A1B2C3D4E5F60718 /* GhosttyConfigTests.swift */; };
|
||||||
F5000000A1B2C3D4E5F60718 /* SessionPersistenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5000001A1B2C3D4E5F60718 /* SessionPersistenceTests.swift */; };
|
F5000000A1B2C3D4E5F60718 /* SessionPersistenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5000001A1B2C3D4E5F60718 /* SessionPersistenceTests.swift */; };
|
||||||
F6000000A1B2C3D4E5F60718 /* AppDelegateShortcutRoutingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6000001A1B2C3D4E5F60718 /* AppDelegateShortcutRoutingTests.swift */; };
|
F6000000A1B2C3D4E5F60718 /* AppDelegateShortcutRoutingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6000001A1B2C3D4E5F60718 /* AppDelegateShortcutRoutingTests.swift */; };
|
||||||
/* End PBXBuildFile section */
|
F7000000A1B2C3D4E5F60718 /* WorkspaceContentViewVisibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7000001A1B2C3D4E5F60718 /* WorkspaceContentViewVisibilityTests.swift */; };
|
||||||
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXCopyFilesBuildPhase section */
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
A5001020 /* Embed Frameworks */ = {
|
A5001020 /* Embed Frameworks */ = {
|
||||||
|
|
@ -199,16 +200,17 @@
|
||||||
B9000016A1B2C3D4E5F60719 /* MultiWindowNotificationsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiWindowNotificationsUITests.swift; sourceTree = "<group>"; };
|
B9000016A1B2C3D4E5F60719 /* MultiWindowNotificationsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiWindowNotificationsUITests.swift; sourceTree = "<group>"; };
|
||||||
B9000019A1B2C3D4E5F60719 /* CloseWorkspaceConfirmDialogUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseWorkspaceConfirmDialogUITests.swift; sourceTree = "<group>"; };
|
B9000019A1B2C3D4E5F60719 /* CloseWorkspaceConfirmDialogUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseWorkspaceConfirmDialogUITests.swift; sourceTree = "<group>"; };
|
||||||
B9000022A1B2C3D4E5F60719 /* CloseWorkspaceCmdDUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseWorkspaceCmdDUITests.swift; sourceTree = "<group>"; };
|
B9000022A1B2C3D4E5F60719 /* CloseWorkspaceCmdDUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseWorkspaceCmdDUITests.swift; sourceTree = "<group>"; };
|
||||||
D0E0F0B1A1B2C3D4E5F60718 /* BrowserPaneNavigationKeybindUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserPaneNavigationKeybindUITests.swift; sourceTree = "<group>"; };
|
D0E0F0B1A1B2C3D4E5F60718 /* BrowserPaneNavigationKeybindUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserPaneNavigationKeybindUITests.swift; sourceTree = "<group>"; };
|
||||||
D0E0F0B3A1B2C3D4E5F60718 /* BrowserOmnibarSuggestionsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserOmnibarSuggestionsUITests.swift; sourceTree = "<group>"; };
|
D0E0F0B3A1B2C3D4E5F60718 /* BrowserOmnibarSuggestionsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserOmnibarSuggestionsUITests.swift; sourceTree = "<group>"; };
|
||||||
E1000001A1B2C3D4E5F60718 /* MenuKeyEquivalentRoutingUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuKeyEquivalentRoutingUITests.swift; sourceTree = "<group>"; };
|
E1000001A1B2C3D4E5F60718 /* MenuKeyEquivalentRoutingUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuKeyEquivalentRoutingUITests.swift; sourceTree = "<group>"; };
|
||||||
F1000001A1B2C3D4E5F60718 /* CmuxWebViewKeyEquivalentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxWebViewKeyEquivalentTests.swift; sourceTree = "<group>"; };
|
F1000001A1B2C3D4E5F60718 /* CmuxWebViewKeyEquivalentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxWebViewKeyEquivalentTests.swift; sourceTree = "<group>"; };
|
||||||
F2000001A1B2C3D4E5F60718 /* UpdatePillReleaseVisibilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePillReleaseVisibilityTests.swift; sourceTree = "<group>"; };
|
F2000001A1B2C3D4E5F60718 /* UpdatePillReleaseVisibilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePillReleaseVisibilityTests.swift; sourceTree = "<group>"; };
|
||||||
F3000001A1B2C3D4E5F60718 /* CJKIMEInputTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CJKIMEInputTests.swift; sourceTree = "<group>"; };
|
F3000001A1B2C3D4E5F60718 /* CJKIMEInputTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CJKIMEInputTests.swift; sourceTree = "<group>"; };
|
||||||
F4000001A1B2C3D4E5F60718 /* GhosttyConfigTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyConfigTests.swift; sourceTree = "<group>"; };
|
F4000001A1B2C3D4E5F60718 /* GhosttyConfigTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyConfigTests.swift; sourceTree = "<group>"; };
|
||||||
F5000001A1B2C3D4E5F60718 /* SessionPersistenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionPersistenceTests.swift; sourceTree = "<group>"; };
|
F5000001A1B2C3D4E5F60718 /* SessionPersistenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionPersistenceTests.swift; sourceTree = "<group>"; };
|
||||||
F6000001A1B2C3D4E5F60718 /* AppDelegateShortcutRoutingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegateShortcutRoutingTests.swift; sourceTree = "<group>"; };
|
F6000001A1B2C3D4E5F60718 /* AppDelegateShortcutRoutingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegateShortcutRoutingTests.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
F7000001A1B2C3D4E5F60718 /* WorkspaceContentViewVisibilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceContentViewVisibilityTests.swift; sourceTree = "<group>"; };
|
||||||
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
A5001030 /* Frameworks */ = {
|
A5001030 /* Frameworks */ = {
|
||||||
|
|
@ -408,19 +410,20 @@
|
||||||
path = cmuxUITests;
|
path = cmuxUITests;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
F1000003A1B2C3D4E5F60718 /* cmuxTests */ = {
|
F1000003A1B2C3D4E5F60718 /* cmuxTests */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
F1000001A1B2C3D4E5F60718 /* CmuxWebViewKeyEquivalentTests.swift */,
|
F1000001A1B2C3D4E5F60718 /* CmuxWebViewKeyEquivalentTests.swift */,
|
||||||
F2000001A1B2C3D4E5F60718 /* UpdatePillReleaseVisibilityTests.swift */,
|
F2000001A1B2C3D4E5F60718 /* UpdatePillReleaseVisibilityTests.swift */,
|
||||||
F3000001A1B2C3D4E5F60718 /* CJKIMEInputTests.swift */,
|
F3000001A1B2C3D4E5F60718 /* CJKIMEInputTests.swift */,
|
||||||
F4000001A1B2C3D4E5F60718 /* GhosttyConfigTests.swift */,
|
F4000001A1B2C3D4E5F60718 /* GhosttyConfigTests.swift */,
|
||||||
F5000001A1B2C3D4E5F60718 /* SessionPersistenceTests.swift */,
|
F5000001A1B2C3D4E5F60718 /* SessionPersistenceTests.swift */,
|
||||||
F6000001A1B2C3D4E5F60718 /* AppDelegateShortcutRoutingTests.swift */,
|
F6000001A1B2C3D4E5F60718 /* AppDelegateShortcutRoutingTests.swift */,
|
||||||
);
|
F7000001A1B2C3D4E5F60718 /* WorkspaceContentViewVisibilityTests.swift */,
|
||||||
path = cmuxTests;
|
);
|
||||||
sourceTree = "<group>";
|
path = cmuxTests;
|
||||||
};
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
|
|
@ -611,20 +614,21 @@
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
F1000005A1B2C3D4E5F60718 /* Sources */ = {
|
F1000005A1B2C3D4E5F60718 /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
F1000000A1B2C3D4E5F60718 /* CmuxWebViewKeyEquivalentTests.swift in Sources */,
|
F1000000A1B2C3D4E5F60718 /* CmuxWebViewKeyEquivalentTests.swift in Sources */,
|
||||||
F2000000A1B2C3D4E5F60718 /* UpdatePillReleaseVisibilityTests.swift in Sources */,
|
F2000000A1B2C3D4E5F60718 /* UpdatePillReleaseVisibilityTests.swift in Sources */,
|
||||||
F3000000A1B2C3D4E5F60718 /* CJKIMEInputTests.swift in Sources */,
|
F3000000A1B2C3D4E5F60718 /* CJKIMEInputTests.swift in Sources */,
|
||||||
F4000000A1B2C3D4E5F60718 /* GhosttyConfigTests.swift in Sources */,
|
F4000000A1B2C3D4E5F60718 /* GhosttyConfigTests.swift in Sources */,
|
||||||
F5000000A1B2C3D4E5F60718 /* SessionPersistenceTests.swift in Sources */,
|
F5000000A1B2C3D4E5F60718 /* SessionPersistenceTests.swift in Sources */,
|
||||||
F6000000A1B2C3D4E5F60718 /* AppDelegateShortcutRoutingTests.swift in Sources */,
|
F6000000A1B2C3D4E5F60718 /* AppDelegateShortcutRoutingTests.swift in Sources */,
|
||||||
);
|
F7000000A1B2C3D4E5F60718 /* WorkspaceContentViewVisibilityTests.swift in Sources */,
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
);
|
||||||
};
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
B9000006A1B2C3D4E5F60719 /* Sources */ = {
|
};
|
||||||
|
B9000006A1B2C3D4E5F60719 /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
|
|
||||||
|
|
@ -4185,9 +4185,11 @@ final class GhosttySurfaceScrollView: NSView {
|
||||||
isHidden = !visible
|
isHidden = !visible
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
if wasVisible != visible {
|
if wasVisible != visible {
|
||||||
|
let transition = "\(wasVisible ? 1 : 0)->\(visible ? 1 : 0)"
|
||||||
|
let suffix = debugVisibilityStateSuffix(transition: transition)
|
||||||
debugLogWorkspaceSwitchTiming(
|
debugLogWorkspaceSwitchTiming(
|
||||||
event: "ws.term.visible",
|
event: "ws.term.visible",
|
||||||
suffix: "surface=\(surfaceView.terminalSurface?.id.uuidString.prefix(5) ?? "nil") value=\(visible ? 1 : 0)"
|
suffix: suffix
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -4207,9 +4209,11 @@ final class GhosttySurfaceScrollView: NSView {
|
||||||
isActive = active
|
isActive = active
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
if wasActive != active {
|
if wasActive != active {
|
||||||
|
let transition = "\(wasActive ? 1 : 0)->\(active ? 1 : 0)"
|
||||||
|
let suffix = debugVisibilityStateSuffix(transition: transition)
|
||||||
debugLogWorkspaceSwitchTiming(
|
debugLogWorkspaceSwitchTiming(
|
||||||
event: "ws.term.active",
|
event: "ws.term.active",
|
||||||
suffix: "surface=\(surfaceView.terminalSurface?.id.uuidString.prefix(5) ?? "nil") value=\(active ? 1 : 0)"
|
suffix: suffix
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -4231,6 +4235,37 @@ final class GhosttySurfaceScrollView: NSView {
|
||||||
let dtMs = (CACurrentMediaTime() - snapshot.startedAt) * 1000
|
let dtMs = (CACurrentMediaTime() - snapshot.startedAt) * 1000
|
||||||
dlog("\(event) id=\(snapshot.id) dt=\(String(format: "%.2fms", dtMs)) \(suffix)")
|
dlog("\(event) id=\(snapshot.id) dt=\(String(format: "%.2fms", dtMs)) \(suffix)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func debugFirstResponderLabel() -> String {
|
||||||
|
guard let window, let firstResponder = window.firstResponder else { return "nil" }
|
||||||
|
if let view = firstResponder as? NSView {
|
||||||
|
if view === surfaceView {
|
||||||
|
return "surfaceView"
|
||||||
|
}
|
||||||
|
if view.isDescendant(of: surfaceView) {
|
||||||
|
return "surfaceDescendant"
|
||||||
|
}
|
||||||
|
return String(describing: type(of: view))
|
||||||
|
}
|
||||||
|
return String(describing: type(of: firstResponder))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func debugVisibilityStateSuffix(transition: String) -> String {
|
||||||
|
let surface = surfaceView.terminalSurface?.id.uuidString.prefix(5) ?? "nil"
|
||||||
|
let hiddenInHierarchy = (isHiddenOrHasHiddenAncestor || surfaceView.isHiddenOrHasHiddenAncestor) ? 1 : 0
|
||||||
|
let inWindow = window != nil ? 1 : 0
|
||||||
|
let hasSuperview = superview != nil ? 1 : 0
|
||||||
|
let hostHidden = isHidden ? 1 : 0
|
||||||
|
let surfaceHidden = surfaceView.isHidden ? 1 : 0
|
||||||
|
let boundsText = String(format: "%.1fx%.1f", bounds.width, bounds.height)
|
||||||
|
let frameText = String(format: "%.1fx%.1f", frame.width, frame.height)
|
||||||
|
let responder = debugFirstResponderLabel()
|
||||||
|
return
|
||||||
|
"surface=\(surface) transition=\(transition) active=\(isActive ? 1 : 0) " +
|
||||||
|
"visibleFlag=\(surfaceView.isVisibleInUI ? 1 : 0) hostHidden=\(hostHidden) surfaceHidden=\(surfaceHidden) " +
|
||||||
|
"hiddenHierarchy=\(hiddenInHierarchy) inWindow=\(inWindow) hasSuperview=\(hasSuperview) " +
|
||||||
|
"bounds=\(boundsText) frame=\(frameText) firstResponder=\(responder)"
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
func moveFocus(from previous: GhosttySurfaceScrollView? = nil, delay: TimeInterval? = nil) {
|
func moveFocus(from previous: GhosttySurfaceScrollView? = nil, delay: TimeInterval? = nil) {
|
||||||
|
|
@ -5082,32 +5117,36 @@ struct GhosttyTerminalView: NSViewRepresentable {
|
||||||
func updateNSView(_ nsView: NSView, context: Context) {
|
func updateNSView(_ nsView: NSView, context: Context) {
|
||||||
let hostedView = terminalSurface.hostedView
|
let hostedView = terminalSurface.hostedView
|
||||||
let coordinator = context.coordinator
|
let coordinator = context.coordinator
|
||||||
#if DEBUG
|
|
||||||
let previousDesiredIsActive = coordinator.desiredIsActive
|
let previousDesiredIsActive = coordinator.desiredIsActive
|
||||||
#endif
|
|
||||||
let previousDesiredIsVisibleInUI = coordinator.desiredIsVisibleInUI
|
let previousDesiredIsVisibleInUI = coordinator.desiredIsVisibleInUI
|
||||||
let previousDesiredShowsUnreadNotificationRing = coordinator.desiredShowsUnreadNotificationRing
|
let previousDesiredShowsUnreadNotificationRing = coordinator.desiredShowsUnreadNotificationRing
|
||||||
let previousDesiredPortalZPriority = coordinator.desiredPortalZPriority
|
let previousDesiredPortalZPriority = coordinator.desiredPortalZPriority
|
||||||
|
let desiredStateChanged =
|
||||||
|
previousDesiredIsActive != isActive ||
|
||||||
|
previousDesiredIsVisibleInUI != isVisibleInUI ||
|
||||||
|
previousDesiredPortalZPriority != portalZPriority
|
||||||
coordinator.desiredIsActive = isActive
|
coordinator.desiredIsActive = isActive
|
||||||
coordinator.desiredIsVisibleInUI = isVisibleInUI
|
coordinator.desiredIsVisibleInUI = isVisibleInUI
|
||||||
coordinator.desiredShowsUnreadNotificationRing = showsUnreadNotificationRing
|
coordinator.desiredShowsUnreadNotificationRing = showsUnreadNotificationRing
|
||||||
coordinator.desiredPortalZPriority = portalZPriority
|
coordinator.desiredPortalZPriority = portalZPriority
|
||||||
coordinator.hostedView = hostedView
|
coordinator.hostedView = hostedView
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
if previousDesiredIsActive != isActive ||
|
if desiredStateChanged {
|
||||||
previousDesiredIsVisibleInUI != isVisibleInUI ||
|
|
||||||
previousDesiredPortalZPriority != portalZPriority {
|
|
||||||
if let snapshot = AppDelegate.shared?.tabManager?.debugCurrentWorkspaceSwitchSnapshot() {
|
if let snapshot = AppDelegate.shared?.tabManager?.debugCurrentWorkspaceSwitchSnapshot() {
|
||||||
let dtMs = (CACurrentMediaTime() - snapshot.startedAt) * 1000
|
let dtMs = (CACurrentMediaTime() - snapshot.startedAt) * 1000
|
||||||
dlog(
|
dlog(
|
||||||
"ws.swiftui.update id=\(snapshot.id) dt=\(String(format: "%.2fms", dtMs)) " +
|
"ws.swiftui.update id=\(snapshot.id) dt=\(String(format: "%.2fms", dtMs)) " +
|
||||||
"surface=\(terminalSurface.id.uuidString.prefix(5)) visible=\(isVisibleInUI ? 1 : 0) " +
|
"surface=\(terminalSurface.id.uuidString.prefix(5)) visible=\(isVisibleInUI ? 1 : 0) " +
|
||||||
"active=\(isActive ? 1 : 0) z=\(portalZPriority)"
|
"active=\(isActive ? 1 : 0) z=\(portalZPriority) " +
|
||||||
|
"hostWindow=\(nsView.window != nil ? 1 : 0) hostedWindow=\(hostedView.window != nil ? 1 : 0) " +
|
||||||
|
"hostedSuperview=\(hostedView.superview != nil ? 1 : 0)"
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
dlog(
|
dlog(
|
||||||
"ws.swiftui.update id=none surface=\(terminalSurface.id.uuidString.prefix(5)) " +
|
"ws.swiftui.update id=none surface=\(terminalSurface.id.uuidString.prefix(5)) " +
|
||||||
"visible=\(isVisibleInUI ? 1 : 0) active=\(isActive ? 1 : 0) z=\(portalZPriority)"
|
"visible=\(isVisibleInUI ? 1 : 0) active=\(isActive ? 1 : 0) z=\(portalZPriority) " +
|
||||||
|
"hostWindow=\(nsView.window != nil ? 1 : 0) hostedWindow=\(hostedView.window != nil ? 1 : 0) " +
|
||||||
|
"hostedSuperview=\(hostedView.superview != nil ? 1 : 0)"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5195,6 +5234,16 @@ struct GhosttyTerminalView: NSViewRepresentable {
|
||||||
// Bind is deferred until host moves into a window. Update the
|
// Bind is deferred until host moves into a window. Update the
|
||||||
// existing portal entry's visibleInUI now so that any portal sync
|
// existing portal entry's visibleInUI now so that any portal sync
|
||||||
// that runs before the deferred bind completes won't hide the view.
|
// that runs before the deferred bind completes won't hide the view.
|
||||||
|
#if DEBUG
|
||||||
|
if desiredStateChanged {
|
||||||
|
dlog(
|
||||||
|
"ws.hostState.deferBind surface=\(terminalSurface.id.uuidString.prefix(5)) " +
|
||||||
|
"reason=hostNoWindow visible=\(coordinator.desiredIsVisibleInUI ? 1 : 0) " +
|
||||||
|
"active=\(coordinator.desiredIsActive ? 1 : 0) z=\(coordinator.desiredPortalZPriority) " +
|
||||||
|
"hostedWindow=\(hostedView.window != nil ? 1 : 0) hostedSuperview=\(hostedView.superview != nil ? 1 : 0)"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
TerminalWindowPortalRegistry.updateEntryVisibility(
|
TerminalWindowPortalRegistry.updateEntryVisibility(
|
||||||
for: hostedView,
|
for: hostedView,
|
||||||
visibleInUI: coordinator.desiredIsVisibleInUI
|
visibleInUI: coordinator.desiredIsVisibleInUI
|
||||||
|
|
@ -5218,6 +5267,16 @@ struct GhosttyTerminalView: NSViewRepresentable {
|
||||||
} else {
|
} else {
|
||||||
// Preserve portal entry visibility while a stale host is still receiving SwiftUI updates.
|
// Preserve portal entry visibility while a stale host is still receiving SwiftUI updates.
|
||||||
// The currently bound host remains authoritative for immediate visible/active state.
|
// The currently bound host remains authoritative for immediate visible/active state.
|
||||||
|
#if DEBUG
|
||||||
|
if desiredStateChanged {
|
||||||
|
dlog(
|
||||||
|
"ws.hostState.deferApply surface=\(terminalSurface.id.uuidString.prefix(5)) " +
|
||||||
|
"reason=staleHostBinding hostWindow=\(hostWindowAttached ? 1 : 0) " +
|
||||||
|
"boundToCurrent=\(isBoundToCurrentHost ? 1 : 0) hostedSuperview=\(hostedView.superview != nil ? 1 : 0) " +
|
||||||
|
"visible=\(isVisibleInUI ? 1 : 0) active=\(isActive ? 1 : 0)"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
TerminalWindowPortalRegistry.updateEntryVisibility(
|
TerminalWindowPortalRegistry.updateEntryVisibility(
|
||||||
for: hostedView,
|
for: hostedView,
|
||||||
visibleInUI: isVisibleInUI
|
visibleInUI: isVisibleInUI
|
||||||
|
|
|
||||||
|
|
@ -3265,17 +3265,19 @@ extension Workspace: BonsplitDelegate {
|
||||||
|
|
||||||
if isDetaching, let panel {
|
if isDetaching, let panel {
|
||||||
let browserPanel = panel as? BrowserPanel
|
let browserPanel = panel as? BrowserPanel
|
||||||
|
let cachedTitle = panelTitles[panelId]
|
||||||
|
let transferFallbackTitle = cachedTitle ?? panel.displayTitle
|
||||||
pendingDetachedSurfaces[tabId] = DetachedSurfaceTransfer(
|
pendingDetachedSurfaces[tabId] = DetachedSurfaceTransfer(
|
||||||
panelId: panelId,
|
panelId: panelId,
|
||||||
panel: panel,
|
panel: panel,
|
||||||
title: resolvedPanelTitle(panelId: panelId, fallback: panel.displayTitle),
|
title: resolvedPanelTitle(panelId: panelId, fallback: transferFallbackTitle),
|
||||||
icon: panel.displayIcon,
|
icon: panel.displayIcon,
|
||||||
iconImageData: browserPanel?.faviconPNGData,
|
iconImageData: browserPanel?.faviconPNGData,
|
||||||
kind: surfaceKind(for: panel),
|
kind: surfaceKind(for: panel),
|
||||||
isLoading: browserPanel?.isLoading ?? false,
|
isLoading: browserPanel?.isLoading ?? false,
|
||||||
isPinned: pinnedPanelIds.contains(panelId),
|
isPinned: pinnedPanelIds.contains(panelId),
|
||||||
directory: panelDirectories[panelId],
|
directory: panelDirectories[panelId],
|
||||||
cachedTitle: panelTitles[panelId],
|
cachedTitle: cachedTitle,
|
||||||
customTitle: panelCustomTitles[panelId],
|
customTitle: panelCustomTitles[panelId],
|
||||||
manuallyUnread: manualUnreadPanelIds.contains(panelId)
|
manuallyUnread: manualUnreadPanelIds.contains(panelId)
|
||||||
)
|
)
|
||||||
|
|
@ -3364,14 +3366,35 @@ extension Workspace: BonsplitDelegate {
|
||||||
}
|
}
|
||||||
debugLastDidMoveTabTimestamp = now
|
debugLastDidMoveTabTimestamp = now
|
||||||
debugDidMoveTabEventCount += 1
|
debugDidMoveTabEventCount += 1
|
||||||
let movedPanel = panelIdFromSurfaceId(tab.id)?.uuidString.prefix(5) ?? "unknown"
|
let movedPanelId = panelIdFromSurfaceId(tab.id)
|
||||||
|
let movedPanel = movedPanelId?.uuidString.prefix(5) ?? "unknown"
|
||||||
|
let selectedBefore = controller.selectedTab(inPane: destination)
|
||||||
|
.map { String(String(describing: $0.id).prefix(5)) } ?? "nil"
|
||||||
|
let focusedPaneBefore = controller.focusedPaneId?.id.uuidString.prefix(5) ?? "nil"
|
||||||
|
let focusedPanelBefore = focusedPanelId?.uuidString.prefix(5) ?? "nil"
|
||||||
dlog(
|
dlog(
|
||||||
"split.moveTab idx=\(debugDidMoveTabEventCount) dtSincePrevMs=\(sincePrev) panel=\(movedPanel) " +
|
"split.moveTab idx=\(debugDidMoveTabEventCount) dtSincePrevMs=\(sincePrev) panel=\(movedPanel) " +
|
||||||
"from=\(source.id.uuidString.prefix(5)) to=\(destination.id.uuidString.prefix(5)) " +
|
"from=\(source.id.uuidString.prefix(5)) to=\(destination.id.uuidString.prefix(5)) " +
|
||||||
"sourceTabs=\(controller.tabs(inPane: source).count) destTabs=\(controller.tabs(inPane: destination).count)"
|
"sourceTabs=\(controller.tabs(inPane: source).count) destTabs=\(controller.tabs(inPane: destination).count)"
|
||||||
)
|
)
|
||||||
|
dlog(
|
||||||
|
"split.moveTab.state.before idx=\(debugDidMoveTabEventCount) panel=\(movedPanel) " +
|
||||||
|
"destSelected=\(selectedBefore) focusedPane=\(focusedPaneBefore) focusedPanel=\(focusedPanelBefore)"
|
||||||
|
)
|
||||||
#endif
|
#endif
|
||||||
applyTabSelection(tabId: tab.id, inPane: destination)
|
applyTabSelection(tabId: tab.id, inPane: destination)
|
||||||
|
#if DEBUG
|
||||||
|
let selectedAfter = controller.selectedTab(inPane: destination)
|
||||||
|
.map { String(String(describing: $0.id).prefix(5)) } ?? "nil"
|
||||||
|
let focusedPaneAfter = controller.focusedPaneId?.id.uuidString.prefix(5) ?? "nil"
|
||||||
|
let focusedPanelAfter = focusedPanelId?.uuidString.prefix(5) ?? "nil"
|
||||||
|
let movedPanelFocused = (movedPanelId != nil && movedPanelId == focusedPanelId) ? 1 : 0
|
||||||
|
dlog(
|
||||||
|
"split.moveTab.state.after idx=\(debugDidMoveTabEventCount) panel=\(movedPanel) " +
|
||||||
|
"destSelected=\(selectedAfter) focusedPane=\(focusedPaneAfter) focusedPanel=\(focusedPanelAfter) " +
|
||||||
|
"movedFocused=\(movedPanelFocused)"
|
||||||
|
)
|
||||||
|
#endif
|
||||||
normalizePinnedTabs(in: source)
|
normalizePinnedTabs(in: source)
|
||||||
normalizePinnedTabs(in: destination)
|
normalizePinnedTabs(in: destination)
|
||||||
scheduleTerminalGeometryReconcile()
|
scheduleTerminalGeometryReconcile()
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,17 @@ struct WorkspaceContentView: View {
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
@EnvironmentObject var notificationStore: TerminalNotificationStore
|
@EnvironmentObject var notificationStore: TerminalNotificationStore
|
||||||
|
|
||||||
|
static func panelVisibleInUI(
|
||||||
|
isWorkspaceVisible: Bool,
|
||||||
|
isSelectedInPane: Bool,
|
||||||
|
isFocused: Bool
|
||||||
|
) -> Bool {
|
||||||
|
guard isWorkspaceVisible else { return false }
|
||||||
|
// During pane/tab reparenting, Bonsplit can transiently report selected=false
|
||||||
|
// for the currently focused panel. Keep focused content visible to avoid blank frames.
|
||||||
|
return isSelectedInPane || isFocused
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
let appearance = PanelAppearance.fromConfig(config)
|
let appearance = PanelAppearance.fromConfig(config)
|
||||||
let isSplit = workspace.bonsplitController.allPaneIds.count > 1 ||
|
let isSplit = workspace.bonsplitController.allPaneIds.count > 1 ||
|
||||||
|
|
@ -47,7 +58,11 @@ struct WorkspaceContentView: View {
|
||||||
if let panel = workspace.panel(for: tab.id) {
|
if let panel = workspace.panel(for: tab.id) {
|
||||||
let isFocused = isWorkspaceInputActive && workspace.focusedPanelId == panel.id
|
let isFocused = isWorkspaceInputActive && workspace.focusedPanelId == panel.id
|
||||||
let isSelectedInPane = workspace.bonsplitController.selectedTab(inPane: paneId)?.id == tab.id
|
let isSelectedInPane = workspace.bonsplitController.selectedTab(inPane: paneId)?.id == tab.id
|
||||||
let isVisibleInUI = isWorkspaceVisible && isSelectedInPane
|
let isVisibleInUI = Self.panelVisibleInUI(
|
||||||
|
isWorkspaceVisible: isWorkspaceVisible,
|
||||||
|
isSelectedInPane: isSelectedInPane,
|
||||||
|
isFocused: isFocused
|
||||||
|
)
|
||||||
let hasUnreadNotification = Workspace.shouldShowUnreadIndicator(
|
let hasUnreadNotification = Workspace.shouldShowUnreadIndicator(
|
||||||
hasUnreadNotification: notificationStore.hasUnreadNotification(forTabId: workspace.id, surfaceId: panel.id),
|
hasUnreadNotification: notificationStore.hasUnreadNotification(forTabId: workspace.id, surfaceId: panel.id),
|
||||||
isManuallyUnread: workspace.manualUnreadPanelIds.contains(panel.id)
|
isManuallyUnread: workspace.manualUnreadPanelIds.contains(panel.id)
|
||||||
|
|
|
||||||
|
|
@ -3147,6 +3147,51 @@ final class WorkspacePanelGitBranchTests: XCTestCase {
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testDetachAttachAcrossWorkspacesPreservesNonCustomPanelTitle() {
|
||||||
|
let source = Workspace()
|
||||||
|
guard let panelId = source.focusedPanelId else {
|
||||||
|
XCTFail("Expected source focused panel")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertTrue(source.updatePanelTitle(panelId: panelId, title: "detached-runtime-title"))
|
||||||
|
|
||||||
|
guard let detached = source.detachSurface(panelId: panelId) else {
|
||||||
|
XCTFail("Expected detach to succeed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertEqual(detached.cachedTitle, "detached-runtime-title")
|
||||||
|
XCTAssertNil(detached.customTitle)
|
||||||
|
XCTAssertEqual(
|
||||||
|
detached.title,
|
||||||
|
"detached-runtime-title",
|
||||||
|
"Detached transfer should carry the cached non-custom title"
|
||||||
|
)
|
||||||
|
|
||||||
|
let destination = Workspace()
|
||||||
|
guard let destinationPane = destination.bonsplitController.allPaneIds.first else {
|
||||||
|
XCTFail("Expected destination pane")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let attachedPanelId = destination.attachDetachedSurface(
|
||||||
|
detached,
|
||||||
|
inPane: destinationPane,
|
||||||
|
focus: false
|
||||||
|
)
|
||||||
|
XCTAssertEqual(attachedPanelId, panelId)
|
||||||
|
XCTAssertEqual(destination.panelTitle(panelId: panelId), "detached-runtime-title")
|
||||||
|
|
||||||
|
guard let attachedTabId = destination.surfaceIdFromPanelId(panelId),
|
||||||
|
let attachedTab = destination.bonsplitController.tab(attachedTabId) else {
|
||||||
|
XCTFail("Expected attached tab mapping")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
XCTAssertEqual(attachedTab.title, "detached-runtime-title")
|
||||||
|
XCTAssertFalse(attachedTab.hasCustomTitle)
|
||||||
|
}
|
||||||
|
|
||||||
func testBrowserSplitWithFocusFalseRecoversFromDelayedStaleSelection() {
|
func testBrowserSplitWithFocusFalseRecoversFromDelayedStaleSelection() {
|
||||||
let workspace = Workspace()
|
let workspace = Workspace()
|
||||||
guard let originalFocusedPanelId = workspace.focusedPanelId else {
|
guard let originalFocusedPanelId = workspace.focusedPanelId else {
|
||||||
|
|
|
||||||
49
cmuxTests/WorkspaceContentViewVisibilityTests.swift
Normal file
49
cmuxTests/WorkspaceContentViewVisibilityTests.swift
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
#if canImport(cmux_DEV)
|
||||||
|
@testable import cmux_DEV
|
||||||
|
#elseif canImport(cmux)
|
||||||
|
@testable import cmux
|
||||||
|
#endif
|
||||||
|
|
||||||
|
final class WorkspaceContentViewVisibilityTests: XCTestCase {
|
||||||
|
func testPanelVisibleInUIReturnsFalseWhenWorkspaceHidden() {
|
||||||
|
XCTAssertFalse(
|
||||||
|
WorkspaceContentView.panelVisibleInUI(
|
||||||
|
isWorkspaceVisible: false,
|
||||||
|
isSelectedInPane: true,
|
||||||
|
isFocused: true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPanelVisibleInUIReturnsTrueForSelectedPanel() {
|
||||||
|
XCTAssertTrue(
|
||||||
|
WorkspaceContentView.panelVisibleInUI(
|
||||||
|
isWorkspaceVisible: true,
|
||||||
|
isSelectedInPane: true,
|
||||||
|
isFocused: false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPanelVisibleInUIReturnsTrueForFocusedPanelDuringTransientSelectionGap() {
|
||||||
|
XCTAssertTrue(
|
||||||
|
WorkspaceContentView.panelVisibleInUI(
|
||||||
|
isWorkspaceVisible: true,
|
||||||
|
isSelectedInPane: false,
|
||||||
|
isFocused: true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPanelVisibleInUIReturnsFalseWhenNeitherSelectedNorFocused() {
|
||||||
|
XCTAssertFalse(
|
||||||
|
WorkspaceContentView.panelVisibleInUI(
|
||||||
|
isWorkspaceVisible: true,
|
||||||
|
isSelectedInPane: false,
|
||||||
|
isFocused: false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue