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
126 changes: 126 additions & 0 deletions cmuxTests/PortalTabDragRoutingTests.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import XCTest
import AppKit
import SwiftUI
@testable import Bonsplit

#if canImport(cmux_DEV)
Expand All @@ -22,6 +23,32 @@ final class PortalTabDragRoutingTests: XCTestCase {
}
}

func testCompactPaneTabChromeStaysBelowDragHitMinimum() throws {
let appearance = BonsplitConfiguration.Appearance(
tabMinWidth: 140,
tabMaxWidth: 220,
splitButtons: []
)
let measuredWidth = try XCTUnwrap(
renderedSelectedPaneTabIndicatorWidth(
title: "~",
icon: "terminal.fill",
appearance: appearance
)
)

XCTAssertGreaterThan(
measuredWidth,
40,
"The regression measurement must prove the selected tab indicator actually rendered"
)
XCTAssertLessThanOrEqual(
measuredWidth,
80,
"Short pane-tab visible chrome should stay compact; drag affordance must come from hit testing, not a wider rendered tab"
)
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

private func makeHostedTerminalView(frame: NSRect) -> GhosttySurfaceScrollView {
let surfaceView = GhosttyNSView(frame: frame)
let hostedView = GhosttySurfaceScrollView(surfaceView: surfaceView)
Expand All @@ -30,6 +57,105 @@ final class PortalTabDragRoutingTests: XCTestCase {
return hostedView
}

private func renderedSelectedPaneTabIndicatorWidth(
title: String,
icon: String?,
appearance: BonsplitConfiguration.Appearance
) -> CGFloat? {
let controller = BonsplitController(configuration: BonsplitConfiguration(appearance: appearance))
guard let pane = controller.internalController.rootNode.allPanes.first else { return nil }
let tab = TabItem(title: title, icon: icon)
pane.tabs = [tab]
pane.selectedTabId = tab.id

let size = NSSize(width: 180, height: appearance.tabBarHeight)
let hostingView = NSHostingView(
rootView: TabBarView(pane: pane, isFocused: true, showSplitButtons: false)
.environment(controller)
.environment(controller.internalController)
)
let window = NSWindow(
contentRect: NSRect(origin: .zero, size: size),
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
)
window.isOpaque = false
window.backgroundColor = .clear
defer { window.orderOut(nil) }
guard let contentView = window.contentView else { return nil }

contentView.wantsLayer = true
contentView.layer?.backgroundColor = NSColor.clear.cgColor
hostingView.frame = NSRect(origin: .zero, size: size)
hostingView.autoresizingMask = [.width, .height]
contentView.addSubview(hostingView)

window.makeKeyAndOrderFront(nil)
let sampleRect = NSRect(x: 0, y: 0, width: size.width, height: 4)
Comment thread
lawrencecchen marked this conversation as resolved.
return waitForHighSaturationWidth(
in: hostingView,
sampleRect: sampleRect
)
}

private func waitForHighSaturationWidth(
in view: NSView,
sampleRect: NSRect,
timeout: TimeInterval = 1.0
) -> CGFloat? {
let deadline = Date().addingTimeInterval(timeout)
repeat {
view.layoutSubtreeIfNeeded()
view.displayIfNeeded()
if let width = highSaturationWidth(in: view, sampleRect: sampleRect) {
return width
}
_ = RunLoop.current.run(mode: .default, before: Date().addingTimeInterval(0.01))
} while Date() < deadline
return highSaturationWidth(in: view, sampleRect: sampleRect)
}

private func highSaturationWidth(in view: NSView, sampleRect: NSRect) -> CGFloat? {
let integralBounds = view.bounds.integral
guard let bitmap = view.bitmapImageRepForCachingDisplay(in: integralBounds) else { return nil }
bitmap.size = integralBounds.size
view.cacheDisplay(in: integralBounds, to: bitmap)

let scaleX = CGFloat(bitmap.pixelsWide) / max(1, integralBounds.width)
let scaleY = CGFloat(bitmap.pixelsHigh) / max(1, integralBounds.height)
let minX = max(0, Int(floor(sampleRect.minX * scaleX)))
let maxX = min(bitmap.pixelsWide, Int(ceil(sampleRect.maxX * scaleX)))
let minY = max(0, Int(floor(sampleRect.minY * scaleY)))
let maxY = min(bitmap.pixelsHigh, Int(ceil(sampleRect.maxY * scaleY)))

var activeColumnCount = 0
for x in minX..<maxX {
var hasIndicatorPixel = false
for y in minY..<maxY {
guard let color = bitmap.colorAt(x: x, y: y),
let rgb = color.usingColorSpace(.sRGB),
rgb.alphaComponent > 0.05 else { continue }
let alpha = min(max(rgb.alphaComponent, 0), 1)
let red = rgb.redComponent * alpha
let green = rgb.greenComponent * alpha
let blue = rgb.blueComponent * alpha
let high = max(red, green, blue)
guard high > 0.01 else { continue }
let low = min(red, green, blue)
if (high - low) / high > 0.4 {
hasIndicatorPixel = true
break
}
}
if hasIndicatorPixel {
activeColumnCount += 1
}
}
guard activeColumnCount > 0 else { return nil }
return CGFloat(activeColumnCount) / scaleX
Comment thread
lawrencecchen marked this conversation as resolved.
}

private struct TabStripPassThroughFixture {
let host: WindowTerminalHostView
let pointInHost: NSPoint
Expand Down
Loading