editor: don't clamp the restored scroll offset against an unmeasured MaxScroll
maybeApplyRestoreScroll applied the offset as soon as the content had landed, then clamped it to MaxScroll. For a string buffer (small file) MaxScroll derives from the shaper's last baseline, which is still 0 at that point - nothing has shaped yet - so every restored offset was clamped to 0 and the scroll position was silently lost on relaunch (the line-pin that re-derives the offset had already been armed from the zeroed value). Wait for the first shaped frame (its feedback triggers the emitFrame that re-runs the apply; a chunked buffer's MaxScroll is measured from the line index and needs no such wait). A deadline bounds the wait so content that never produces a baseline (empty, newlines-only) cannot leave the restore armed forever - it lands best-effort. Verified on the emulator: 120-line file, scroll to line 75, relaunch - lands at line 75 (log: RESTORE scroll off=1272.7 line=75 maxScroll=1272.7; before the fix: off=0.0 maxScroll=0.0).
This commit is contained in:
parent
7a6b130078
commit
6bd5588ade
|
|
@ -125,6 +125,10 @@ type Logic struct {
|
||||||
// the stat result, and is not a content-arrival signal).
|
// the stat result, and is not a content-arrival signal).
|
||||||
restoreScroll ui.Dp
|
restoreScroll ui.Dp
|
||||||
restoreScrollArmed bool
|
restoreScrollArmed bool
|
||||||
|
// restoreArmedAt is when the restore scroll was armed; bounds how long
|
||||||
|
// maybeApplyRestoreScroll may wait for a trustworthy MaxScroll (see
|
||||||
|
// there) before landing best-effort.
|
||||||
|
restoreArmedAt time.Time
|
||||||
// restoreScrollLine/restoreScrollSub/restorePinDeadline implement the
|
// restoreScrollLine/restoreScrollSub/restorePinDeadline implement the
|
||||||
// restore line-pin (see refreshRestorePin in session.go): the
|
// restore line-pin (see refreshRestorePin in session.go): the
|
||||||
// line-derived scroll offset is only consistent with the all-estimate
|
// line-derived scroll offset is only consistent with the all-estimate
|
||||||
|
|
|
||||||
|
|
@ -258,6 +258,9 @@ func (l *Logic) BeginRestore(s SessionState) {
|
||||||
// struct).
|
// struct).
|
||||||
l.restoreScroll = ui.Dp(s.Scroll)
|
l.restoreScroll = ui.Dp(s.Scroll)
|
||||||
l.restoreScrollArmed = s.Scroll > 0
|
l.restoreScrollArmed = s.Scroll > 0
|
||||||
|
if l.restoreScrollArmed {
|
||||||
|
l.restoreArmedAt = time.Now()
|
||||||
|
}
|
||||||
// The app-local font scale lands immediately, BEFORE any layout: the
|
// The app-local font scale lands immediately, BEFORE any layout: the
|
||||||
// restore's line-based offset math reads it through EffectiveLineHeight.
|
// restore's line-based offset math reads it through EffectiveLineHeight.
|
||||||
if as := s.AppFontScale; as > 0 {
|
if as := s.AppFontScale; as > 0 {
|
||||||
|
|
@ -326,6 +329,19 @@ func (l *Logic) maybeApplyRestoreScroll() bool {
|
||||||
if l.state.Page() != EditorPage || !l.restoreContentLanded {
|
if l.state.Page() != EditorPage || !l.restoreContentLanded {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
// The clamp below needs a trustworthy MaxScroll. For a chunked buffer it
|
||||||
|
// is measured from the line index (or the large pre-index estimate) and
|
||||||
|
// is valid as soon as the content has landed. For a string buffer it
|
||||||
|
// derives from the shaper's last baseline, which is still 0 until the
|
||||||
|
// first non-empty frame has shaped — applying now would clamp the
|
||||||
|
// offset to 0 and the scroll position would be silently lost. Wait for
|
||||||
|
// that frame (its feedback triggers the emitFrame that re-runs this);
|
||||||
|
// a deadline covers content that never produces a baseline (empty or
|
||||||
|
// newlines-only), which lands best-effort.
|
||||||
|
if l.state.Editor.ChunkedBuffer == nil && l.state.LastLineY <= 0 &&
|
||||||
|
time.Since(l.restoreArmedAt) < restoreScrollArmTimeout {
|
||||||
|
return false
|
||||||
|
}
|
||||||
// Re-derive the offset from the persisted logical line (see the
|
// Re-derive the offset from the persisted logical line (see the
|
||||||
// SessionState doc): the snapshot's pixel offset mapped to its position
|
// SessionState doc): the snapshot's pixel offset mapped to its position
|
||||||
// only while the saving session's WrapIndex was valid. On relaunch the
|
// only while the saving session's WrapIndex was valid. On relaunch the
|
||||||
|
|
@ -371,6 +387,7 @@ func (l *Logic) maybeApplyRestoreScroll() bool {
|
||||||
}
|
}
|
||||||
l.state.ScrollOffset = l.restoreScroll
|
l.state.ScrollOffset = l.restoreScroll
|
||||||
l.restoreScrollArmed = false
|
l.restoreScrollArmed = false
|
||||||
|
log.Printf("RESTORE scroll off=%.1f line=%d maxScroll=%.1f", l.restoreScroll, s.ScrollLine, l.state.MaxScroll)
|
||||||
// The snapshot has fully landed (cursor/selection/find with the
|
// The snapshot has fully landed (cursor/selection/find with the
|
||||||
// content, scroll now): lift the save suppression.
|
// content, scroll now): lift the save suppression.
|
||||||
l.session = SessionState{}
|
l.session = SessionState{}
|
||||||
|
|
@ -381,6 +398,13 @@ func (l *Logic) maybeApplyRestoreScroll() bool {
|
||||||
// the restored window has either settled or the user has moved on.
|
// the restored window has either settled or the user has moved on.
|
||||||
const restorePinTimeout = 2 * time.Second
|
const restorePinTimeout = 2 * time.Second
|
||||||
|
|
||||||
|
// restoreScrollArmTimeout bounds how long maybeApplyRestoreScroll may wait
|
||||||
|
// for a trustworthy MaxScroll before landing the offset best-effort (see
|
||||||
|
// the guard there). Shaping a real file completes in well under this; it
|
||||||
|
// exists so degenerate content (empty, newlines-only) cannot leave the
|
||||||
|
// restore armed forever.
|
||||||
|
const restoreScrollArmTimeout = 2 * time.Second
|
||||||
|
|
||||||
// releaseRestorePin clears the restore line-pin (user or search took over
|
// releaseRestorePin clears the restore line-pin (user or search took over
|
||||||
// the viewport). Must be called on the logic goroutine.
|
// the viewport). Must be called on the logic goroutine.
|
||||||
func (l *Logic) releaseRestorePin() {
|
func (l *Logic) releaseRestorePin() {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user