feat(p0.5): continuous-single navigation math (current-page + scroll clamp)
Some checks failed
CI / Windows build (push) Has been cancelled

Extends PageStackMetrics with the navigation geometry continuous-single needs:
maxScrollExtent (last page bottom rests at viewport bottom, never negative),
clampScroll, and dominantPageAt — the page covering most of the viewport, which
drives the page-number indicator + thumbnail-grid highlight + jump-to-page (F4).
Pure; clamps past both ends; 0 for empty documents.

flutter analyze lib/editor clean; 129/129 tests (+6).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-23 03:16:20 +08:00
parent c03513da4f
commit c31bfd3445
2 changed files with 84 additions and 0 deletions

View File

@@ -114,6 +114,49 @@ class PageStackMetrics {
return PageWindow(first, last);
}
/// Maximum scroll offset so the last page bottom rests at the viewport
/// bottom (never negative — a document shorter than the viewport can't
/// scroll).
double maxScrollExtent(double viewportExtent) {
final max = totalExtent - viewportExtent;
return max > 0 ? max : 0.0;
}
/// Clamps [scrollOffset] into the legal `[0, maxScrollExtent]` range.
double clampScroll(double scrollOffset, double viewportExtent) {
final max = maxScrollExtent(viewportExtent);
if (scrollOffset < 0) return 0.0;
return scrollOffset > max ? max : scrollOffset;
}
/// The "current" page for a scroll position: the page covering the LARGEST
/// portion of the viewport `[scrollOffset, scrollOffset + viewportExtent)`.
/// Drives the page-number indicator + thumbnail-grid highlight (F4). Returns
/// 0 for an empty document.
int dominantPageAt(double scrollOffset, double viewportExtent) {
if (_heights.isEmpty) return 0;
final window = visibleRange(scrollOffset, viewportExtent);
if (window.isEmpty) {
// Past the end / before the start → clamp to nearest real page.
return scrollOffset <= 0 ? 0 : pageCount - 1;
}
final viewTop = scrollOffset;
final viewBottom = scrollOffset + viewportExtent;
var best = window.first;
var bestOverlap = -1.0;
for (var i = window.first; i <= window.last; i++) {
final top = _tops[i];
final bottom = top + _heights[i];
final overlap =
math.min(bottom, viewBottom) - math.max(top, viewTop);
if (overlap > bestOverlap) {
bestOverlap = overlap;
best = i;
}
}
return best;
}
/// Lowest index whose band bottom is strictly after [y] (i.e. the first page
/// that the window's top edge does not sit fully below).
int _firstIntersecting(double y) {