From b85de9a2d017b52c392fac1b5bcf8d6407da39cf Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Sat, 17 Oct 2026 10:52:39 -0700 Subject: [PATCH] jumpy scroll 2 --- mac/Sources/Byre/Chat.swift | 15 ++++++++++++--- mac/Sources/Byre/MarkdownView.swift | 4 ++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/mac/Sources/Byre/Chat.swift b/mac/Sources/Byre/Chat.swift index d6ff725..669285e 100644 --- a/mac/Sources/Byre/Chat.swift +++ b/mac/Sources/Byre/Chat.swift @@ -1,5 +1,6 @@ import AppKit import ByreAPI +import os import SwiftUI enum ChatKind: Equatable { @@ -484,10 +485,13 @@ struct CompactButton: View { } } +let scrollLog = Logger(subsystem: "run.fc.byre.mac", category: "scroll") + private struct ScrollMetrics: Equatable { let content: CGFloat let visible: CGFloat let distance: CGFloat + let offset: CGFloat } private struct TranscriptScroll: View { @@ -535,25 +539,30 @@ private struct TranscriptScroll: View { .scrollPosition($position) .defaultScrollAnchor(.bottom) .onScrollGeometryChange(for: ScrollMetrics.self) { g in - ScrollMetrics(content: g.contentSize.height, visible: g.visibleRect.height, distance: g.contentSize.height - g.visibleRect.maxY) + ScrollMetrics(content: g.contentSize.height, visible: g.visibleRect.height, distance: g.contentSize.height - g.visibleRect.maxY, offset: g.contentOffset.y) } action: { old, new in + let snap = !scrolling && model.stuck && new.distance > 1 && (new.content != old.content || new.visible != old.visible) + scrollLog.notice("geom rows=\(model.builder.root.rows.count) content=\(new.content) dContent=\(new.content - old.content) offset=\(new.offset) dOffset=\(new.offset - old.offset) visible=\(new.visible) distance=\(new.distance) scrolling=\(scrolling) stuck=\(model.stuck) snap=\(snap)") if scrolling { model.stuck = new.distance < 80 return } - if model.stuck && new.distance > 1 && (new.content != old.content || new.visible != old.visible) { + if snap { position.scrollTo(edge: .bottom) } } - .onScrollPhaseChange { _, phase in + .onScrollPhaseChange { old, phase in + scrollLog.notice("phase \(String(describing: old), privacy: .public) -> \(String(describing: phase), privacy: .public)") scrolling = phase.isScrolling } .onChange(of: model.builder.pending.count) { + scrollLog.notice("pending scrollTo bottom") model.stuck = true position.scrollTo(edge: .bottom) } .onChange(of: model.scrollRequest?.seq, initial: true) { if let r = model.scrollRequest { + scrollLog.notice("request scrollTo \(String(describing: r.id), privacy: .public)") position.scrollTo(id: r.id, anchor: r.anchor) } } diff --git a/mac/Sources/Byre/MarkdownView.swift b/mac/Sources/Byre/MarkdownView.swift index bb85016..5544840 100644 --- a/mac/Sources/Byre/MarkdownView.swift +++ b/mac/Sources/Byre/MarkdownView.swift @@ -1,3 +1,4 @@ +import os import SwiftUI enum MDSegment { @@ -449,8 +450,10 @@ private struct FlowText: NSViewRepresentable { func size(_ text: NSAttributedString, _ width: CGFloat?) -> CGSize { let same = measured.map { $0.text === text || $0.text.isEqual(to: text) } ?? false if same, let m = measured, m.width == width { + scrollLog.notice("measure hit len=\(text.length) width=\(width ?? -1) height=\(m.size.height)") return m.size } + let start = ContinuousClock.now if !same { storage.setAttributedString(text) } @@ -458,6 +461,7 @@ private struct FlowText: NSViewRepresentable { layout.ensureLayout(for: container) let used = layout.usedRect(for: container) let size = CGSize(width: width ?? ceil(used.width), height: ceil(used.height)) + scrollLog.notice("measure miss len=\(text.length) width=\(width ?? -1) oldWidth=\(self.measured?.width ?? -1) newText=\(!same) height=\(size.height) ms=\((ContinuousClock.now - start) / .milliseconds(1))") measured = (text, width, size) return size }