Skip to content

Commit 2b12d00

Browse files
committed
Stabilize toolbar scroll position during resize
1 parent e207846 commit 2b12d00

3 files changed

Lines changed: 87 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
All notable changes to CodeViewerKit are documented in this file.
44

5+
## 0.6.5 - 2026-08-27
6+
7+
- prevent AppKit's clip view from independently changing the toolbar content
8+
inset during macOS window resizes, so the first visible code line and gutter
9+
number remain below the toolbar;
10+
511
## 0.6.4 - 2026-08-27
612

713
- expose each registered grammar's canonical identifier and aliases so a

Sources/CodeViewerKit/CodeTextView.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,10 @@ private final class MacCodeTextContainer: NSView {
406406
scrollView.hasHorizontalScroller = true
407407
scrollView.autohidesScrollers = true
408408
scrollView.scrollerStyle = .legacy
409+
// The scroll view and its clip view adjust insets independently. Own
410+
// both values so AppKit cannot move the clip bounds during live resize.
411+
scrollView.automaticallyAdjustsContentInsets = false
412+
scrollView.contentView.automaticallyAdjustsContentInsets = false
409413
scrollView.documentView = textView
410414

411415
textView.drawsBackground = false
@@ -523,8 +527,8 @@ private final class MacCodeTextContainer: NSView {
523527
)
524528
guard abs(scrollView.contentInsets.top - topInset) > 0.5 else { return }
525529

526-
scrollView.automaticallyAdjustsContentInsets = false
527530
scrollView.contentInsets.top = topInset
531+
scrollView.contentView.contentInsets.top = topInset
528532
}
529533

530534
private func updateLineWrapping(_ lineWrapping: CodeLineWrapping) {

Tests/CodeViewerKitTests/CodeTextCoreTests.swift

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,63 @@ final class CodeTextCoreTests: XCTestCase {
131131
CGPoint(x: 7, y: 140)
132132
)
133133
}
134+
135+
@MainActor
136+
func testMacViewerKeepsFirstLineBelowToolbarWhileWindowResizes() throws {
137+
let highlights = CodeHighlightStore(grammars: [])
138+
let sourceCode = (1...200)
139+
.map {
140+
"let line\($0) = \($0) // " + String(repeating: "x", count: 60)
141+
}
142+
.joined(separator: "\n")
143+
let hostingView = NSHostingView(
144+
rootView: CodeViewer(
145+
documentID: "resize-test",
146+
sourceCode: sourceCode,
147+
highlightStore: highlights,
148+
language: "plain"
149+
)
150+
.ignoresSafeArea()
151+
)
152+
let window = NSWindow(
153+
contentRect: NSRect(x: 0, y: 0, width: 700, height: 500),
154+
styleMask: [.titled, .resizable, .fullSizeContentView],
155+
backing: .buffered,
156+
defer: false
157+
)
158+
window.titleVisibility = .hidden
159+
window.titlebarAppearsTransparent = true
160+
window.toolbar = NSToolbar(identifier: "CodeViewerKit.ResizeTest")
161+
window.contentView = hostingView
162+
163+
hostingView.layoutSubtreeIfNeeded()
164+
165+
let scrollView = try XCTUnwrap(
166+
hostingView.firstDescendant(ofType: NSScrollView.self)
167+
)
168+
XCTAssertGreaterThan(scrollView.contentInsets.top, 0)
169+
XCTAssertFalse(scrollView.automaticallyAdjustsContentInsets)
170+
XCTAssertFalse(scrollView.contentView.automaticallyAdjustsContentInsets)
171+
XCTAssertEqual(
172+
scrollView.contentView.bounds.minY + scrollView.contentInsets.top,
173+
0,
174+
accuracy: 0.5
175+
)
176+
177+
for size in [
178+
NSSize(width: 300, height: 350),
179+
NSSize(width: 900, height: 600)
180+
] {
181+
window.setContentSize(size)
182+
hostingView.layoutSubtreeIfNeeded()
183+
184+
XCTAssertEqual(
185+
scrollView.contentView.bounds.minY + scrollView.contentInsets.top,
186+
0,
187+
accuracy: 0.5
188+
)
189+
}
190+
}
134191
#endif
135192

136193
func testPlainTextStyleUsesAnAppearanceAwareDefault() {
@@ -175,3 +232,22 @@ final class CodeTextCoreTests: XCTestCase {
175232
#endif
176233
}
177234
}
235+
236+
#if os(macOS)
237+
private extension NSView {
238+
func firstDescendant<ViewType: NSView>(
239+
ofType type: ViewType.Type
240+
) -> ViewType? {
241+
if let match = self as? ViewType {
242+
return match
243+
}
244+
245+
for subview in subviews {
246+
if let match = subview.firstDescendant(ofType: type) {
247+
return match
248+
}
249+
}
250+
return nil
251+
}
252+
}
253+
#endif

0 commit comments

Comments
 (0)