jumpy scroll 2
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import AppKit
|
import AppKit
|
||||||
import ByreAPI
|
import ByreAPI
|
||||||
|
import os
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
enum ChatKind: Equatable {
|
enum ChatKind: Equatable {
|
||||||
@@ -484,10 +485,13 @@ struct CompactButton: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let scrollLog = Logger(subsystem: "run.fc.byre.mac", category: "scroll")
|
||||||
|
|
||||||
private struct ScrollMetrics: Equatable {
|
private struct ScrollMetrics: Equatable {
|
||||||
let content: CGFloat
|
let content: CGFloat
|
||||||
let visible: CGFloat
|
let visible: CGFloat
|
||||||
let distance: CGFloat
|
let distance: CGFloat
|
||||||
|
let offset: CGFloat
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct TranscriptScroll: View {
|
private struct TranscriptScroll: View {
|
||||||
@@ -535,25 +539,30 @@ private struct TranscriptScroll: View {
|
|||||||
.scrollPosition($position)
|
.scrollPosition($position)
|
||||||
.defaultScrollAnchor(.bottom)
|
.defaultScrollAnchor(.bottom)
|
||||||
.onScrollGeometryChange(for: ScrollMetrics.self) { g in
|
.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
|
} 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 {
|
if scrolling {
|
||||||
model.stuck = new.distance < 80
|
model.stuck = new.distance < 80
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if model.stuck && new.distance > 1 && (new.content != old.content || new.visible != old.visible) {
|
if snap {
|
||||||
position.scrollTo(edge: .bottom)
|
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
|
scrolling = phase.isScrolling
|
||||||
}
|
}
|
||||||
.onChange(of: model.builder.pending.count) {
|
.onChange(of: model.builder.pending.count) {
|
||||||
|
scrollLog.notice("pending scrollTo bottom")
|
||||||
model.stuck = true
|
model.stuck = true
|
||||||
position.scrollTo(edge: .bottom)
|
position.scrollTo(edge: .bottom)
|
||||||
}
|
}
|
||||||
.onChange(of: model.scrollRequest?.seq, initial: true) {
|
.onChange(of: model.scrollRequest?.seq, initial: true) {
|
||||||
if let r = model.scrollRequest {
|
if let r = model.scrollRequest {
|
||||||
|
scrollLog.notice("request scrollTo \(String(describing: r.id), privacy: .public)")
|
||||||
position.scrollTo(id: r.id, anchor: r.anchor)
|
position.scrollTo(id: r.id, anchor: r.anchor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import os
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
enum MDSegment {
|
enum MDSegment {
|
||||||
@@ -449,8 +450,10 @@ private struct FlowText: NSViewRepresentable {
|
|||||||
func size(_ text: NSAttributedString, _ width: CGFloat?) -> CGSize {
|
func size(_ text: NSAttributedString, _ width: CGFloat?) -> CGSize {
|
||||||
let same = measured.map { $0.text === text || $0.text.isEqual(to: text) } ?? false
|
let same = measured.map { $0.text === text || $0.text.isEqual(to: text) } ?? false
|
||||||
if same, let m = measured, m.width == width {
|
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
|
return m.size
|
||||||
}
|
}
|
||||||
|
let start = ContinuousClock.now
|
||||||
if !same {
|
if !same {
|
||||||
storage.setAttributedString(text)
|
storage.setAttributedString(text)
|
||||||
}
|
}
|
||||||
@@ -458,6 +461,7 @@ private struct FlowText: NSViewRepresentable {
|
|||||||
layout.ensureLayout(for: container)
|
layout.ensureLayout(for: container)
|
||||||
let used = layout.usedRect(for: container)
|
let used = layout.usedRect(for: container)
|
||||||
let size = CGSize(width: width ?? ceil(used.width), height: ceil(used.height))
|
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)
|
measured = (text, width, size)
|
||||||
return size
|
return size
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user