Fix tab drag blank state and preserve non-custom titles across window drops
This commit is contained in:
parent
fb1802a54d
commit
cd03073240
6 changed files with 234 additions and 39 deletions
|
|
@ -77,6 +77,7 @@
|
|||
F2000000A1B2C3D4E5F60718 /* UpdatePillReleaseVisibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2000001A1B2C3D4E5F60718 /* UpdatePillReleaseVisibilityTests.swift */; };
|
||||
F3000000A1B2C3D4E5F60718 /* CJKIMEInputTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3000001A1B2C3D4E5F60718 /* CJKIMEInputTests.swift */; };
|
||||
F4000000A1B2C3D4E5F60718 /* GhosttyConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4000001A1B2C3D4E5F60718 /* GhosttyConfigTests.swift */; };
|
||||
F5000000A1B2C3D4E5F60718 /* WorkspaceContentViewVisibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5000001A1B2C3D4E5F60718 /* WorkspaceContentViewVisibilityTests.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
|
|
@ -198,10 +199,11 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
F4000001A1B2C3D4E5F60718 /* GhosttyConfigTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyConfigTests.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>"; };
|
||||
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>"; };
|
||||
F5000001A1B2C3D4E5F60718 /* WorkspaceContentViewVisibilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceContentViewVisibilityTests.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
|
@ -401,17 +403,18 @@
|
|||
path = cmuxUITests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F1000003A1B2C3D4E5F60718 /* cmuxTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F1000001A1B2C3D4E5F60718 /* CmuxWebViewKeyEquivalentTests.swift */,
|
||||
F2000001A1B2C3D4E5F60718 /* UpdatePillReleaseVisibilityTests.swift */,
|
||||
F3000001A1B2C3D4E5F60718 /* CJKIMEInputTests.swift */,
|
||||
F4000001A1B2C3D4E5F60718 /* GhosttyConfigTests.swift */,
|
||||
);
|
||||
path = cmuxTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F1000003A1B2C3D4E5F60718 /* cmuxTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F1000001A1B2C3D4E5F60718 /* CmuxWebViewKeyEquivalentTests.swift */,
|
||||
F2000001A1B2C3D4E5F60718 /* UpdatePillReleaseVisibilityTests.swift */,
|
||||
F3000001A1B2C3D4E5F60718 /* CJKIMEInputTests.swift */,
|
||||
F4000001A1B2C3D4E5F60718 /* GhosttyConfigTests.swift */,
|
||||
F5000001A1B2C3D4E5F60718 /* WorkspaceContentViewVisibilityTests.swift */,
|
||||
);
|
||||
path = cmuxTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
|
|
@ -601,17 +604,18 @@
|
|||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
F1000005A1B2C3D4E5F60718 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
F1000000A1B2C3D4E5F60718 /* CmuxWebViewKeyEquivalentTests.swift in Sources */,
|
||||
F2000000A1B2C3D4E5F60718 /* UpdatePillReleaseVisibilityTests.swift in Sources */,
|
||||
F3000000A1B2C3D4E5F60718 /* CJKIMEInputTests.swift in Sources */,
|
||||
F4000000A1B2C3D4E5F60718 /* GhosttyConfigTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
F1000005A1B2C3D4E5F60718 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
F1000000A1B2C3D4E5F60718 /* CmuxWebViewKeyEquivalentTests.swift in Sources */,
|
||||
F2000000A1B2C3D4E5F60718 /* UpdatePillReleaseVisibilityTests.swift in Sources */,
|
||||
F3000000A1B2C3D4E5F60718 /* CJKIMEInputTests.swift in Sources */,
|
||||
F4000000A1B2C3D4E5F60718 /* GhosttyConfigTests.swift in Sources */,
|
||||
F5000000A1B2C3D4E5F60718 /* WorkspaceContentViewVisibilityTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
B9000006A1B2C3D4E5F60719 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
|
|
|
|||
|
|
@ -4104,9 +4104,11 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
isHidden = !visible
|
||||
#if DEBUG
|
||||
if wasVisible != visible {
|
||||
let transition = "\(wasVisible ? 1 : 0)->\(visible ? 1 : 0)"
|
||||
let suffix = debugVisibilityStateSuffix(transition: transition)
|
||||
debugLogWorkspaceSwitchTiming(
|
||||
event: "ws.term.visible",
|
||||
suffix: "surface=\(surfaceView.terminalSurface?.id.uuidString.prefix(5) ?? "nil") value=\(visible ? 1 : 0)"
|
||||
suffix: suffix
|
||||
)
|
||||
}
|
||||
#endif
|
||||
|
|
@ -4126,9 +4128,11 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
isActive = active
|
||||
#if DEBUG
|
||||
if wasActive != active {
|
||||
let transition = "\(wasActive ? 1 : 0)->\(active ? 1 : 0)"
|
||||
let suffix = debugVisibilityStateSuffix(transition: transition)
|
||||
debugLogWorkspaceSwitchTiming(
|
||||
event: "ws.term.active",
|
||||
suffix: "surface=\(surfaceView.terminalSurface?.id.uuidString.prefix(5) ?? "nil") value=\(active ? 1 : 0)"
|
||||
suffix: suffix
|
||||
)
|
||||
}
|
||||
#endif
|
||||
|
|
@ -4150,6 +4154,37 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
let dtMs = (CACurrentMediaTime() - snapshot.startedAt) * 1000
|
||||
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
|
||||
|
||||
func moveFocus(from previous: GhosttySurfaceScrollView? = nil, delay: TimeInterval? = nil) {
|
||||
|
|
@ -5001,32 +5036,36 @@ struct GhosttyTerminalView: NSViewRepresentable {
|
|||
func updateNSView(_ nsView: NSView, context: Context) {
|
||||
let hostedView = terminalSurface.hostedView
|
||||
let coordinator = context.coordinator
|
||||
#if DEBUG
|
||||
let previousDesiredIsActive = coordinator.desiredIsActive
|
||||
#endif
|
||||
let previousDesiredIsVisibleInUI = coordinator.desiredIsVisibleInUI
|
||||
let previousDesiredShowsUnreadNotificationRing = coordinator.desiredShowsUnreadNotificationRing
|
||||
let previousDesiredPortalZPriority = coordinator.desiredPortalZPriority
|
||||
let desiredStateChanged =
|
||||
previousDesiredIsActive != isActive ||
|
||||
previousDesiredIsVisibleInUI != isVisibleInUI ||
|
||||
previousDesiredPortalZPriority != portalZPriority
|
||||
coordinator.desiredIsActive = isActive
|
||||
coordinator.desiredIsVisibleInUI = isVisibleInUI
|
||||
coordinator.desiredShowsUnreadNotificationRing = showsUnreadNotificationRing
|
||||
coordinator.desiredPortalZPriority = portalZPriority
|
||||
coordinator.hostedView = hostedView
|
||||
#if DEBUG
|
||||
if previousDesiredIsActive != isActive ||
|
||||
previousDesiredIsVisibleInUI != isVisibleInUI ||
|
||||
previousDesiredPortalZPriority != portalZPriority {
|
||||
if desiredStateChanged {
|
||||
if let snapshot = AppDelegate.shared?.tabManager?.debugCurrentWorkspaceSwitchSnapshot() {
|
||||
let dtMs = (CACurrentMediaTime() - snapshot.startedAt) * 1000
|
||||
dlog(
|
||||
"ws.swiftui.update id=\(snapshot.id) dt=\(String(format: "%.2fms", dtMs)) " +
|
||||
"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 {
|
||||
dlog(
|
||||
"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)"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -5114,6 +5153,16 @@ struct GhosttyTerminalView: NSViewRepresentable {
|
|||
// Bind is deferred until host moves into a window. Update the
|
||||
// existing portal entry's visibleInUI now so that any portal sync
|
||||
// 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(
|
||||
for: hostedView,
|
||||
visibleInUI: coordinator.desiredIsVisibleInUI
|
||||
|
|
@ -5137,6 +5186,16 @@ struct GhosttyTerminalView: NSViewRepresentable {
|
|||
} else {
|
||||
// Preserve portal entry visibility while a stale host is still receiving SwiftUI updates.
|
||||
// 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(
|
||||
for: hostedView,
|
||||
visibleInUI: isVisibleInUI
|
||||
|
|
|
|||
|
|
@ -2761,17 +2761,19 @@ extension Workspace: BonsplitDelegate {
|
|||
|
||||
if isDetaching, let panel {
|
||||
let browserPanel = panel as? BrowserPanel
|
||||
let cachedTitle = panelTitles[panelId]
|
||||
let transferFallbackTitle = cachedTitle ?? panel.displayTitle
|
||||
pendingDetachedSurfaces[tabId] = DetachedSurfaceTransfer(
|
||||
panelId: panelId,
|
||||
panel: panel,
|
||||
title: resolvedPanelTitle(panelId: panelId, fallback: panel.displayTitle),
|
||||
title: resolvedPanelTitle(panelId: panelId, fallback: transferFallbackTitle),
|
||||
icon: panel.displayIcon,
|
||||
iconImageData: browserPanel?.faviconPNGData,
|
||||
kind: surfaceKind(for: panel),
|
||||
isLoading: browserPanel?.isLoading ?? false,
|
||||
isPinned: pinnedPanelIds.contains(panelId),
|
||||
directory: panelDirectories[panelId],
|
||||
cachedTitle: panelTitles[panelId],
|
||||
cachedTitle: cachedTitle,
|
||||
customTitle: panelCustomTitles[panelId],
|
||||
manuallyUnread: manualUnreadPanelIds.contains(panelId)
|
||||
)
|
||||
|
|
@ -2859,14 +2861,35 @@ extension Workspace: BonsplitDelegate {
|
|||
}
|
||||
debugLastDidMoveTabTimestamp = now
|
||||
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(
|
||||
"split.moveTab idx=\(debugDidMoveTabEventCount) dtSincePrevMs=\(sincePrev) panel=\(movedPanel) " +
|
||||
"from=\(source.id.uuidString.prefix(5)) to=\(destination.id.uuidString.prefix(5)) " +
|
||||
"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
|
||||
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: destination)
|
||||
scheduleTerminalGeometryReconcile()
|
||||
|
|
|
|||
|
|
@ -19,6 +19,17 @@ struct WorkspaceContentView: View {
|
|||
@Environment(\.colorScheme) private var colorScheme
|
||||
@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 {
|
||||
let appearance = PanelAppearance.fromConfig(config)
|
||||
let isSplit = workspace.bonsplitController.allPaneIds.count > 1 ||
|
||||
|
|
@ -47,7 +58,11 @@ struct WorkspaceContentView: View {
|
|||
if let panel = workspace.panel(for: tab.id) {
|
||||
let isFocused = isWorkspaceInputActive && workspace.focusedPanelId == panel.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(
|
||||
hasUnreadNotification: notificationStore.hasUnreadNotification(forTabId: workspace.id, surfaceId: panel.id),
|
||||
isManuallyUnread: workspace.manualUnreadPanelIds.contains(panel.id)
|
||||
|
|
|
|||
|
|
@ -2973,6 +2973,51 @@ final class WorkspacePanelGitBranchTests: XCTestCase {
|
|||
#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() {
|
||||
let workspace = Workspace()
|
||||
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