From 64b5ef4fb847b44be0b14c405e0bc615c39fba97 Mon Sep 17 00:00:00 2001 From: bones7456 Date: Thu, 18 Jun 2026 11:12:36 +0800 Subject: [PATCH] Fix terminal content hidden under the scroller after a font change resetFont() recomputed the column count from the raw frame width, while the live window-resize path (processSizeChange) uses getEffectiveWidth(), which subtracts the reserved scroller width. After zooming the font the terminal therefore claimed more columns than fit the visible area, so the right-most columns were drawn underneath the scroller and that content was hidden. Zooming in and back out also drifted the reported column count by the scroller width. Compute the columns from getEffectiveWidth() in resetFont() as well, so the font-change and live-resize paths agree and content always stays clear of the scroller. Add macOS regression tests asserting the content stays within the usable (non-scroller) width and that both code paths report the same column count. --- .../SwiftTerm/Apple/AppleTerminalView.swift | 6 ++- .../FontResizeColumnsTests.swift | 51 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 Tests/SwiftTermTests/FontResizeColumnsTests.swift diff --git a/Sources/SwiftTerm/Apple/AppleTerminalView.swift b/Sources/SwiftTerm/Apple/AppleTerminalView.swift index ed17f9a1..df6cde1a 100644 --- a/Sources/SwiftTerm/Apple/AppleTerminalView.swift +++ b/Sources/SwiftTerm/Apple/AppleTerminalView.swift @@ -146,7 +146,11 @@ extension TerminalView { resetCaches() self.cellDimension = computeFontDimensions () if (frame.width > 0) && (frame.height > 0) { - let newCols = Int(frame.width / cellDimension.width) + // Use getEffectiveWidth so the scroller's reserved width is taken + // into account, matching processSizeChange(). Computing columns from + // the raw frame width here would over-count by the scroller width, + // so zooming the font in and back out would drift the column count. + let newCols = Int(getEffectiveWidth(size: frame.size) / cellDimension.width) let newRows = Int(frame.height / cellDimension.height) resize(cols: newCols, rows: newRows) } diff --git a/Tests/SwiftTermTests/FontResizeColumnsTests.swift b/Tests/SwiftTermTests/FontResizeColumnsTests.swift new file mode 100644 index 00000000..04095841 --- /dev/null +++ b/Tests/SwiftTermTests/FontResizeColumnsTests.swift @@ -0,0 +1,51 @@ +// +// FontResizeColumnsTests.swift +// SwiftTerm +// +// Regression tests ensuring that after a font change the terminal content +// stays clear of the scroller, i.e. the recomputed geometry reserves the +// scroller width just like the live window-resize path does. +// + +#if os(macOS) +import Foundation +import Testing +import AppKit +@testable import SwiftTerm + +final class FontResizeColumnsTests { + /// The core invariant: after a font change the rendered columns must fit + /// within the area that is not covered by the scroller. If `resetFont` + /// sized the terminal from the raw frame width, the right-most columns + /// would be drawn underneath the scroller and become hidden. + @Test func testFontChangeKeepsContentClearOfScroller() { + let frame = CGRect(x: 0, y: 0, width: 400, height: 200) + let view = TerminalView(frame: frame) + view.font = NSFont.monospacedSystemFont(ofSize: 12, weight: .regular) + + let renderedWidth = CGFloat(view.getTerminal().cols) * view.cellDimension.width + let usableWidth = view.getEffectiveWidth(size: frame.size) + + // Content must not extend into the reserved scroller strip. + #expect(renderedWidth <= usableWidth) + // Sanity: the scroller actually reserves space in this configuration, + // so the test is exercising a non-trivial case. + #expect(usableWidth < frame.width) + } + + /// The font-change path (`resetFont`) and the live-resize path + /// (`processSizeChange`) must agree on the column count for a given frame, + /// so zooming the font in and back out never drifts the column count. + @Test func testFontChangeColumnsMatchResizePath() { + let frame = CGRect(x: 0, y: 0, width: 400, height: 200) + let view = TerminalView(frame: frame) + view.font = NSFont.monospacedSystemFont(ofSize: 12, weight: .regular) + + let colsAfterFontChange = view.getTerminal().cols + view.processSizeChange(newSize: frame.size) + let colsAfterResize = view.getTerminal().cols + + #expect(colsAfterFontChange == colsAfterResize) + } +} +#endif