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

@@ -91,6 +91,47 @@ void main() {
});
});
group('navigation math', () {
final m = PageStackMetrics(pageHeights: List.filled(10, 100)); // 0..1000
test('maxScrollExtent leaves the last page bottom at the viewport bottom',
() {
expect(m.maxScrollExtent(300), 700); // 1000 - 300
});
test('maxScrollExtent is 0 when content is shorter than the viewport', () {
final short = PageStackMetrics(pageHeights: [100]);
expect(short.maxScrollExtent(800), 0);
});
test('clampScroll keeps offset within [0, maxScrollExtent]', () {
expect(m.clampScroll(-50, 300), 0);
expect(m.clampScroll(5000, 300), 700);
expect(m.clampScroll(420, 300), 420);
});
test('dominantPageAt picks the page covering most of the viewport', () {
// viewport [0,100) fully on page 0.
expect(m.dominantPageAt(0, 100), 0);
// viewport [180,280): page1 [100,200) covers 20, page2 [200,300) covers
// 80 → page 2 dominates.
expect(m.dominantPageAt(180, 100), 2);
// viewport [150,250): page1 covers [150,200)=50, page2 [200,250)=50 →
// tie resolves to the first (page1) since overlap must strictly exceed.
expect(m.dominantPageAt(150, 100), 1);
});
test('dominantPageAt clamps past the ends', () {
expect(m.dominantPageAt(-500, 100), 0);
expect(m.dominantPageAt(99999, 100), 9);
});
test('dominantPageAt is 0 for an empty document', () {
final empty = PageStackMetrics(pageHeights: []);
expect(empty.dominantPageAt(0, 800), 0);
});
});
group('visibleRange with gaps', () {
test('gap bands are not page bands (a scroll inside a gap shows neighbors)',
() {