Fix splitter hitbox overlap and terminal scrollbar width resync (#1950)
* test: add splitter and scrollbar regressions * fix: narrow sidebar overlap and resync terminal width * test: unwrap pending surface width in scrollbar regression * fix: restore hosted inspector divider drag path
This commit is contained in:
parent
5ced3134d3
commit
fd279bdcec
7 changed files with 283 additions and 39 deletions
|
|
@ -732,8 +732,8 @@ final class WindowBrowserHostView: NSView {
|
|||
return false
|
||||
}
|
||||
|
||||
let regionMinX = dividerX - SidebarResizeInteraction.hitWidthPerSide
|
||||
let regionMaxX = dividerX + SidebarResizeInteraction.hitWidthPerSide
|
||||
let regionMinX = dividerX - SidebarResizeInteraction.sidebarSideHitWidth
|
||||
let regionMaxX = dividerX + SidebarResizeInteraction.contentSideHitWidth
|
||||
return point.x >= regionMinX && point.x <= regionMaxX
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -336,11 +336,13 @@ final class SidebarState: ObservableObject {
|
|||
}
|
||||
|
||||
enum SidebarResizeInteraction {
|
||||
static let handleWidth: CGFloat = 6
|
||||
static let hitInset: CGFloat = 3
|
||||
// Keep a generous drag target inside the sidebar itself, but make the
|
||||
// terminal-side overlap very small so column-0 text selection still wins.
|
||||
static let sidebarSideHitWidth: CGFloat = 6
|
||||
static let contentSideHitWidth: CGFloat = 2
|
||||
|
||||
static var hitWidthPerSide: CGFloat {
|
||||
hitInset + (handleWidth / 2)
|
||||
static var totalHitWidth: CGFloat {
|
||||
sidebarSideHitWidth + contentSideHitWidth
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2025,8 +2027,12 @@ struct ContentView: View {
|
|||
case divider
|
||||
}
|
||||
|
||||
private var sidebarResizerHitWidthPerSide: CGFloat {
|
||||
SidebarResizeInteraction.hitWidthPerSide
|
||||
private var sidebarResizerSidebarHitWidth: CGFloat {
|
||||
SidebarResizeInteraction.sidebarSideHitWidth
|
||||
}
|
||||
|
||||
private var sidebarResizerContentHitWidth: CGFloat {
|
||||
SidebarResizeInteraction.contentSideHitWidth
|
||||
}
|
||||
|
||||
private func maxSidebarWidth(availableWidth: CGFloat? = nil) -> CGFloat {
|
||||
|
|
@ -2102,8 +2108,8 @@ struct ContentView: View {
|
|||
|
||||
private func dividerBandContains(pointInContent point: NSPoint, contentBounds: NSRect) -> Bool {
|
||||
guard point.y >= contentBounds.minY, point.y <= contentBounds.maxY else { return false }
|
||||
let minX = sidebarWidth - sidebarResizerHitWidthPerSide
|
||||
let maxX = sidebarWidth + sidebarResizerHitWidthPerSide
|
||||
let minX = sidebarWidth - sidebarResizerSidebarHitWidth
|
||||
let maxX = sidebarWidth + sidebarResizerContentHitWidth
|
||||
return point.x >= minX && point.x <= maxX
|
||||
}
|
||||
|
||||
|
|
@ -2283,7 +2289,7 @@ struct ContentView: View {
|
|||
GeometryReader { proxy in
|
||||
let totalWidth = max(0, proxy.size.width)
|
||||
let dividerX = min(max(sidebarWidth, 0), totalWidth)
|
||||
let leadingWidth = max(0, dividerX - sidebarResizerHitWidthPerSide)
|
||||
let leadingWidth = max(0, dividerX - sidebarResizerSidebarHitWidth)
|
||||
|
||||
HStack(spacing: 0) {
|
||||
Color.clear
|
||||
|
|
@ -2292,7 +2298,7 @@ struct ContentView: View {
|
|||
|
||||
sidebarResizerHandleOverlay(
|
||||
.divider,
|
||||
width: sidebarResizerHitWidthPerSide * 2,
|
||||
width: SidebarResizeInteraction.totalHitWidth,
|
||||
availableWidth: totalWidth,
|
||||
accessibilityIdentifier: "SidebarResizer"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4468,6 +4468,12 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
|
|||
updateSurfaceSize(size: size)
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
fileprivate func debugPendingSurfaceSize() -> CGSize? {
|
||||
pendingSurfaceSize
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Force a full size reconciliation for the current bounds.
|
||||
/// Keep the drawable-size cache intact so redundant refresh paths do not
|
||||
/// reallocate Metal drawables when the pixel size is unchanged.
|
||||
|
|
@ -7032,6 +7038,17 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
) { [weak self] _ in
|
||||
self?.synchronizeScrollView()
|
||||
})
|
||||
|
||||
observers.append(NotificationCenter.default.addObserver(
|
||||
forName: NSScroller.preferredScrollerStyleDidChangeNotification,
|
||||
object: nil,
|
||||
// Match AppKit's geometry change immediately so the terminal width
|
||||
// does not stay stuck behind a legacy scrollbar gutter.
|
||||
queue: nil
|
||||
) { [weak self] _ in
|
||||
self?.handlePreferredScrollerStyleChange()
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
|
|
@ -8061,6 +8078,10 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
surfaceView.debugSimulateFileDrop(paths: paths)
|
||||
}
|
||||
|
||||
func debugPendingSurfaceSize() -> CGSize? {
|
||||
surfaceView.debugPendingSurfaceSize()
|
||||
}
|
||||
|
||||
func debugRegisteredDropTypes() -> [String] {
|
||||
surfaceView.debugRegisteredDropTypes()
|
||||
}
|
||||
|
|
@ -9072,6 +9093,21 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
synchronizeScrollView()
|
||||
}
|
||||
|
||||
private func handlePreferredScrollerStyleChange() {
|
||||
guard Thread.isMainThread else {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.handlePreferredScrollerStyleChange()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Retile just the scroll view so contentSize reflects the current
|
||||
// scrollbar mode without perturbing viewport origin or hosted view
|
||||
// geometry; the broader reconcile path caused visible content glitches.
|
||||
scrollView.tile()
|
||||
_ = synchronizeCoreSurface()
|
||||
}
|
||||
|
||||
private func documentHeight() -> CGFloat {
|
||||
let contentHeight = scrollView.contentSize.height
|
||||
let cellHeight = surfaceView.cellSize.height
|
||||
|
|
|
|||
|
|
@ -5037,10 +5037,12 @@ struct WebViewRepresentable: NSViewRepresentable {
|
|||
// Origin-only frame churn is common while the surrounding split layout
|
||||
// settles. Reapplying the side-docked inspector at the same size fights
|
||||
// WebKit's own dock layout and shows up as visible flicker.
|
||||
if !isHostedInspectorSideDockActive() &&
|
||||
!isHostedInspectorDividerDragActive &&
|
||||
!hasStoredHostedInspectorWidthPreference {
|
||||
captureHostedInspectorPreferredWidthFromCurrentLayout(reason: "host.layout.sameSize")
|
||||
if !isHostedInspectorDividerDragActive {
|
||||
if hasStoredHostedInspectorWidthPreference {
|
||||
reapplyHostedInspectorDividerToStoredWidthIfNeeded(reason: "host.layout.sameSize")
|
||||
} else if !isHostedInspectorSideDockActive() {
|
||||
captureHostedInspectorPreferredWidthFromCurrentLayout(reason: "host.layout.sameSize")
|
||||
}
|
||||
}
|
||||
updateHostedInspectorDockControlAvailabilityIfNeeded(reason: "host.layout.sameSize")
|
||||
notifyGeometryChangedIfNeeded()
|
||||
|
|
@ -5052,7 +5054,9 @@ struct WebViewRepresentable: NSViewRepresentable {
|
|||
lastHostedInspectorLayoutBoundsSize = bounds.size
|
||||
if isHostedInspectorSideDockActive() {
|
||||
layoutHostedInspectorSideDockIfNeeded(reason: "host.layout.sideDock")
|
||||
} else if !hasStoredHostedInspectorWidthPreference {
|
||||
} else if hasStoredHostedInspectorWidthPreference {
|
||||
reapplyHostedInspectorDividerToStoredWidthIfNeeded(reason: "host.layout")
|
||||
} else {
|
||||
captureHostedInspectorPreferredWidthFromCurrentLayout(reason: "host.layout")
|
||||
}
|
||||
updateHostedInspectorDockControlAvailabilityIfNeeded(reason: "host.layout")
|
||||
|
|
@ -5130,26 +5134,24 @@ struct WebViewRepresentable: NSViewRepresentable {
|
|||
return nil
|
||||
}
|
||||
if let hostedInspectorHit {
|
||||
let isSideDockHit = isHostedInspectorSideDockHit(hostedInspectorHit)
|
||||
if let nativeHit = nativeHostedInspectorHit(at: point, hostedInspectorHit: hostedInspectorHit) {
|
||||
#if DEBUG
|
||||
debugLogHitTest(stage: "hitTest.hostedInspectorNative", point: point, passThrough: false, hitView: nativeHit)
|
||||
#endif
|
||||
if !isSideDockHit ||
|
||||
(nativeHit !== hostedInspectorHit.inspectorView &&
|
||||
!hostedInspectorHit.inspectorView.isDescendant(of: nativeHit)) {
|
||||
if nativeHit !== hostedInspectorHit.inspectorView &&
|
||||
!hostedInspectorHit.inspectorView.isDescendant(of: nativeHit) {
|
||||
return nativeHit
|
||||
}
|
||||
}
|
||||
#if DEBUG
|
||||
debugLogHitTest(
|
||||
stage: isSideDockHit ? "hitTest.hostedInspectorManual" : "hitTest.hostedInspectorFallback",
|
||||
stage: "hitTest.hostedInspectorManual",
|
||||
point: point,
|
||||
passThrough: false,
|
||||
hitView: hostedInspectorHit.inspectorView
|
||||
hitView: self
|
||||
)
|
||||
#endif
|
||||
return isSideDockHit ? self : hostedInspectorHit.inspectorView
|
||||
return self
|
||||
}
|
||||
let hit = super.hitTest(point)
|
||||
#if DEBUG
|
||||
|
|
@ -5160,8 +5162,7 @@ struct WebViewRepresentable: NSViewRepresentable {
|
|||
|
||||
override func mouseDown(with event: NSEvent) {
|
||||
let point = convert(event.locationInWindow, from: nil)
|
||||
guard let hostedInspectorHit = hostedInspectorDividerHit(at: point),
|
||||
isHostedInspectorSideDockHit(hostedInspectorHit) else {
|
||||
guard let hostedInspectorHit = hostedInspectorDividerHit(at: point) else {
|
||||
super.mouseDown(with: event)
|
||||
return
|
||||
}
|
||||
|
|
@ -5258,7 +5259,7 @@ struct WebViewRepresentable: NSViewRepresentable {
|
|||
)
|
||||
)
|
||||
#endif
|
||||
layoutHostedInspectorSideDockIfNeeded(reason: "drag.end")
|
||||
reapplyHostedInspectorDividerToStoredWidthIfNeeded(reason: "drag.end")
|
||||
}
|
||||
super.mouseUp(with: event)
|
||||
}
|
||||
|
|
@ -5273,7 +5274,7 @@ struct WebViewRepresentable: NSViewRepresentable {
|
|||
// Pass through a narrow leading-edge band so the shared sidebar divider
|
||||
// handle can receive hover/click even when WKWebView is attached here.
|
||||
// Keeping this deterministic avoids flicker from dynamic left-edge scans.
|
||||
guard point.x >= 0, point.x <= SidebarResizeInteraction.hitWidthPerSide else {
|
||||
guard point.x >= 0, point.x <= SidebarResizeInteraction.contentSideHitWidth else {
|
||||
return false
|
||||
}
|
||||
guard let window, let contentView = window.contentView else {
|
||||
|
|
@ -5495,9 +5496,9 @@ struct WebViewRepresentable: NSViewRepresentable {
|
|||
guard let self else { return }
|
||||
self.hostedInspectorReapplyWorkItem = nil
|
||||
_ = self.promoteHostedInspectorSideDockFromCurrentLayoutIfNeeded()
|
||||
if self.isHostedInspectorSideDockActive() {
|
||||
if self.hasStoredHostedInspectorWidthPreference {
|
||||
self.reapplyHostedInspectorDividerToStoredWidthIfNeeded(reason: reason)
|
||||
} else if !self.hasStoredHostedInspectorWidthPreference {
|
||||
} else {
|
||||
self.captureHostedInspectorPreferredWidthFromCurrentLayout(reason: reason)
|
||||
}
|
||||
}
|
||||
|
|
@ -5558,7 +5559,6 @@ struct WebViewRepresentable: NSViewRepresentable {
|
|||
private func reapplyHostedInspectorDividerToStoredWidthIfNeeded(reason: String) {
|
||||
guard !isApplyingHostedInspectorLayout else { return }
|
||||
guard let hit = hostedInspectorDividerCandidate() else { return }
|
||||
guard isHostedInspectorSideDockHit(hit) else { return }
|
||||
guard let preferredWidth = resolvedPreferredHostedInspectorWidth(in: hit.containerView.bounds) else {
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -243,8 +243,8 @@ final class WindowTerminalHostView: NSView {
|
|||
return false
|
||||
}
|
||||
|
||||
let regionMinX = dividerX - SidebarResizeInteraction.hitWidthPerSide
|
||||
let regionMaxX = dividerX + SidebarResizeInteraction.hitWidthPerSide
|
||||
let regionMinX = dividerX - SidebarResizeInteraction.sidebarSideHitWidth
|
||||
let regionMaxX = dividerX + SidebarResizeInteraction.contentSideHitWidth
|
||||
return point.x >= regionMinX && point.x <= regionMaxX
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -547,7 +547,7 @@ final class WindowBrowserHostViewTests: XCTestCase {
|
|||
|
||||
XCTAssertLessThanOrEqual(inspectorSplit.arrangedSubviews[0].frame.width, 1.5)
|
||||
XCTAssertTrue(
|
||||
abs(dividerPointInHost.x - slot.frame.minX) <= SidebarResizeInteraction.hitWidthPerSide,
|
||||
abs(dividerPointInHost.x - slot.frame.minX) <= 2,
|
||||
"Expected collapsed hosted divider to overlap the browser slot leading-edge resizer zone"
|
||||
)
|
||||
|
||||
|
|
@ -905,7 +905,7 @@ final class WindowBrowserHostViewTests: XCTestCase {
|
|||
let dividerPointInWindow = slot.convert(dividerPointInSlot, to: nil)
|
||||
let dividerPointInHost = host.convert(dividerPointInWindow, from: nil)
|
||||
|
||||
XCTAssertLessThanOrEqual(dividerPointInHost.x - slot.frame.minX, SidebarResizeInteraction.hitWidthPerSide)
|
||||
XCTAssertLessThanOrEqual(dividerPointInHost.x - slot.frame.minX, 2)
|
||||
let dividerHit = host.hitTest(dividerPointInHost)
|
||||
XCTAssertTrue(
|
||||
isInspectorOwnedHit(dividerHit, inspectorView: inspectorView, pageView: pageView),
|
||||
|
|
|
|||
|
|
@ -1510,6 +1510,34 @@ final class WindowTerminalHostViewTests: XCTestCase {
|
|||
|
||||
private final class BonsplitMockSplitDelegate: NSObject, NSSplitViewDelegate {}
|
||||
|
||||
private func makeHostedTerminalView(frame: NSRect) -> GhosttySurfaceScrollView {
|
||||
let surfaceView = GhosttyNSView(frame: frame)
|
||||
let hostedView = GhosttySurfaceScrollView(surfaceView: surfaceView)
|
||||
hostedView.frame = frame
|
||||
hostedView.autoresizingMask = [.width, .height]
|
||||
return hostedView
|
||||
}
|
||||
|
||||
private func assertHitFallsInsideHostedTerminal(
|
||||
_ hitView: NSView?,
|
||||
hostedView: GhosttySurfaceScrollView,
|
||||
message: String,
|
||||
file: StaticString = #filePath,
|
||||
line: UInt = #line
|
||||
) {
|
||||
guard let hitView else {
|
||||
XCTFail(message, file: file, line: line)
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertTrue(
|
||||
hitView === hostedView || hitView.isDescendant(of: hostedView),
|
||||
message,
|
||||
file: file,
|
||||
line: line
|
||||
)
|
||||
}
|
||||
|
||||
func testHostViewPassesThroughWhenNoTerminalSubviewIsHit() {
|
||||
let host = WindowTerminalHostView(frame: NSRect(x: 0, y: 0, width: 200, height: 120))
|
||||
|
||||
|
|
@ -1555,9 +1583,8 @@ final class WindowTerminalHostViewTests: XCTestCase {
|
|||
|
||||
let host = WindowTerminalHostView(frame: contentView.bounds)
|
||||
host.autoresizingMask = [.width, .height]
|
||||
let child = CapturingView(frame: host.bounds)
|
||||
child.autoresizingMask = [.width, .height]
|
||||
host.addSubview(child)
|
||||
let hostedView = makeHostedTerminalView(frame: host.bounds)
|
||||
host.addSubview(hostedView)
|
||||
contentView.addSubview(host)
|
||||
|
||||
let dividerPointInSplit = NSPoint(
|
||||
|
|
@ -1575,7 +1602,73 @@ final class WindowTerminalHostViewTests: XCTestCase {
|
|||
let contentPointInSplit = NSPoint(x: dividerPointInSplit.x + 40, y: splitView.bounds.midY)
|
||||
let contentPointInWindow = splitView.convert(contentPointInSplit, to: nil)
|
||||
let contentPointInHost = host.convert(contentPointInWindow, from: nil)
|
||||
XCTAssertTrue(host.hitTest(contentPointInHost) === child)
|
||||
assertHitFallsInsideHostedTerminal(
|
||||
host.hitTest(contentPointInHost),
|
||||
hostedView: hostedView,
|
||||
message: "Terminal content should keep receiving hits after the divider region"
|
||||
)
|
||||
}
|
||||
|
||||
func testHostViewStopsSidebarPassThroughJustInsideTerminalContent() {
|
||||
let terminalSideOverlapWidth: CGFloat = 2
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 300, height: 180),
|
||||
styleMask: [.titled, .closable],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
defer { window.orderOut(nil) }
|
||||
guard let contentView = window.contentView else {
|
||||
XCTFail("Expected content view")
|
||||
return
|
||||
}
|
||||
|
||||
let splitView = NSSplitView(frame: contentView.bounds)
|
||||
splitView.autoresizingMask = [.width, .height]
|
||||
splitView.isVertical = true
|
||||
splitView.dividerStyle = .thin
|
||||
let splitDelegate = BonsplitMockSplitDelegate()
|
||||
splitView.delegate = splitDelegate
|
||||
let first = NSView(frame: NSRect(x: 0, y: 0, width: 120, height: contentView.bounds.height))
|
||||
let second = NSView(frame: NSRect(x: 121, y: 0, width: 179, height: contentView.bounds.height))
|
||||
splitView.addSubview(first)
|
||||
splitView.addSubview(second)
|
||||
contentView.addSubview(splitView)
|
||||
splitView.setPosition(1, ofDividerAt: 0)
|
||||
splitView.adjustSubviews()
|
||||
contentView.layoutSubtreeIfNeeded()
|
||||
|
||||
let host = WindowTerminalHostView(frame: contentView.bounds)
|
||||
host.autoresizingMask = [.width, .height]
|
||||
let hostedView = makeHostedTerminalView(frame: host.bounds)
|
||||
host.addSubview(hostedView)
|
||||
contentView.addSubview(host)
|
||||
|
||||
let dividerPointInSplit = NSPoint(
|
||||
x: splitView.arrangedSubviews[0].frame.maxX + (splitView.dividerThickness * 0.5),
|
||||
y: splitView.bounds.midY
|
||||
)
|
||||
let dividerPointInWindow = splitView.convert(dividerPointInSplit, to: nil)
|
||||
let dividerPointInHost = host.convert(dividerPointInWindow, from: nil)
|
||||
|
||||
let resizeBandPoint = NSPoint(
|
||||
x: dividerPointInHost.x + terminalSideOverlapWidth,
|
||||
y: dividerPointInHost.y
|
||||
)
|
||||
XCTAssertNil(
|
||||
host.hitTest(resizeBandPoint),
|
||||
"The narrow terminal-side overlap should still pass through to the sidebar resizer"
|
||||
)
|
||||
|
||||
let textSelectionPoint = NSPoint(
|
||||
x: dividerPointInHost.x + terminalSideOverlapWidth + 1,
|
||||
y: dividerPointInHost.y
|
||||
)
|
||||
assertHitFallsInsideHostedTerminal(
|
||||
host.hitTest(textSelectionPoint),
|
||||
hostedView: hostedView,
|
||||
message: "Once the pointer moves past the reduced terminal-side overlap, terminal content should win hit-testing"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1690,6 +1783,115 @@ final class GhosttySurfaceOverlayTests: XCTestCase {
|
|||
XCTAssertTrue(state.isHidden)
|
||||
}
|
||||
|
||||
func testPreferredScrollerStyleChangeRecalculatesTerminalSurfaceWidth() {
|
||||
let surface = TerminalSurface(
|
||||
tabId: UUID(),
|
||||
context: GHOSTTY_SURFACE_CONTEXT_SPLIT,
|
||||
configTemplate: nil,
|
||||
workingDirectory: nil
|
||||
)
|
||||
let hostedView = surface.hostedView
|
||||
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 360, height: 240),
|
||||
styleMask: [.titled, .closable],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
defer { window.orderOut(nil) }
|
||||
|
||||
guard let contentView = window.contentView else {
|
||||
XCTFail("Expected content view")
|
||||
return
|
||||
}
|
||||
|
||||
hostedView.frame = contentView.bounds
|
||||
hostedView.autoresizingMask = [.width, .height]
|
||||
contentView.addSubview(hostedView)
|
||||
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
window.displayIfNeeded()
|
||||
contentView.layoutSubtreeIfNeeded()
|
||||
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
|
||||
|
||||
guard let scrollView = hostedView.subviews.first(where: { $0 is NSScrollView }) as? NSScrollView else {
|
||||
XCTFail("Expected hosted terminal scroll view")
|
||||
return
|
||||
}
|
||||
guard let initialSurfaceSize = hostedView.debugPendingSurfaceSize() else {
|
||||
XCTFail("Expected an initial terminal surface size")
|
||||
return
|
||||
}
|
||||
|
||||
func assertPendingSurfaceWidth(
|
||||
_ expectedWidth: CGFloat,
|
||||
_ message: String,
|
||||
file: StaticString = #filePath,
|
||||
line: UInt = #line
|
||||
) {
|
||||
guard let pendingSurfaceWidth = hostedView.debugPendingSurfaceSize()?.width else {
|
||||
XCTFail("Expected a pending terminal surface size", file: file, line: line)
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(
|
||||
pendingSurfaceWidth,
|
||||
expectedWidth,
|
||||
accuracy: 0.5,
|
||||
message,
|
||||
file: file,
|
||||
line: line
|
||||
)
|
||||
}
|
||||
|
||||
let initialContentWidth = scrollView.contentSize.width
|
||||
XCTAssertEqual(initialSurfaceSize.width, initialContentWidth, accuracy: 0.5)
|
||||
|
||||
scrollView.scrollerStyle = .legacy
|
||||
scrollView.layoutSubtreeIfNeeded()
|
||||
let legacyContentWidth = scrollView.contentSize.width
|
||||
XCTAssertLessThan(
|
||||
legacyContentWidth,
|
||||
initialContentWidth,
|
||||
"Legacy scrollbars should reserve width in the scroll view content area"
|
||||
)
|
||||
assertPendingSurfaceWidth(
|
||||
initialSurfaceSize.width,
|
||||
"Changing the scroll view style alone should leave the terminal grid stale until the scroller-style observer runs"
|
||||
)
|
||||
|
||||
NotificationCenter.default.post(name: NSScroller.preferredScrollerStyleDidChangeNotification, object: nil)
|
||||
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
|
||||
|
||||
XCTAssertEqual(scrollView.scrollerStyle, .legacy)
|
||||
assertPendingSurfaceWidth(
|
||||
legacyContentWidth,
|
||||
"Preferred scroller style changes should recalculate the terminal grid width immediately"
|
||||
)
|
||||
|
||||
scrollView.scrollerStyle = .overlay
|
||||
scrollView.layoutSubtreeIfNeeded()
|
||||
let overlayContentWidth = scrollView.contentSize.width
|
||||
XCTAssertGreaterThan(
|
||||
overlayContentWidth,
|
||||
legacyContentWidth,
|
||||
"Overlay scrollbars should restore the full terminal content width"
|
||||
)
|
||||
assertPendingSurfaceWidth(
|
||||
legacyContentWidth,
|
||||
"Changing the scroll view style alone should leave the terminal grid stale until the scroller-style observer runs"
|
||||
)
|
||||
|
||||
NotificationCenter.default.post(name: NSScroller.preferredScrollerStyleDidChangeNotification, object: nil)
|
||||
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
|
||||
|
||||
XCTAssertEqual(scrollView.scrollerStyle, .overlay)
|
||||
assertPendingSurfaceWidth(
|
||||
overlayContentWidth,
|
||||
"Preferred scroller style changes should also restore the wider terminal grid when overlay scrollbars return"
|
||||
)
|
||||
}
|
||||
|
||||
func testWindowResignKeyClearsFocusedTerminalFirstResponder() {
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 360, height: 240),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue