package editor import ( "fmt" "math" "math/rand" "testing" "pad/internal/io/pool/types" "pad/internal/ui" ) // These tests prove the tap/scroll bookkeeping follows the RENDERED line // pitch under a non-default user font-size setting. On Android, // Metric.PxPerSp = fontScale * PxPerDp, and the shaper draws baselines at // Sp(fontSize*LineHeightScale) physical px, so the rendered line pitch in // density-dp is EditorLineHeight()*fontScale. Every consumer must use // EffectiveLineHeight (window start, sub-line remainder, tap mapping, scroll // clamp); at fontScale != 1 the raw EditorLineHeight would misplace taps by // up to (fontScale-1) viewportfuls of lines. func TestEffectiveLineHeight_FollowsFontScale(t *testing.T) { TheState = NewState() defer func() { TheState.fontScale = 0 }() cases := []struct { fs float32 want float64 }{ {0, float64(EditorLineHeight())}, // unknown -> 1.0 {1, float64(EditorLineHeight())}, // default {1.3, float64(EditorLineHeight()) * 1.3}, {0.8, float64(EditorLineHeight()) * 0.8}, } for _, c := range cases { TheState.fontScale = c.fs got := float64(EffectiveLineHeight()) if math.Abs(got-c.want) > 1e-5 { // float32 ui.Dp rounding t.Errorf("fontScale=%v: EffectiveLineHeight=%v want %v", c.fs, got, c.want) } } } // Generalizes TestTapPosition_ScrollInvariant_Property to fontScale=1.3: // the windowed layout is fabricated the way the shaper produces it with the // scaled line pitch (baseline of display line j = (j+1)*lh, lh already // font-scaled), and the tapped line must equal the drawn line under the // finger for random scroll offsets and finger positions. func TestTapPosition_FontScale_Property(t *testing.T) { const ( lines = 400 bytesPerLine = 5 regionTop = float64(62) viewportH = float64(760) fs = float32(1.3) ) var file string for i := 0; i < lines; i++ { file += fmt.Sprintf("L%03d\n", i) } cb := NewChunkedBuffer("/fs.txt", DefaultChunkSize, nil, "") cb.SetContent([]byte(file)) offsets := []int32{0} for i := 0; i < len(file); i++ { if file[i] == '\n' { offsets = append(offsets, int32(i+1)) } } cb.LineIndex = types.NewLineIndex(offsets, 0, int64(len(file))) lh := float64(EffectiveLineHeightAt(fs)) if math.Abs(lh-float64(EditorLineHeight())*float64(fs)) > 1e-5 { // float32 ui.Dp rounding t.Fatalf("effective line height %v != %v*%v", lh, float64(EditorLineHeight()), fs) } rng := rand.New(rand.NewSource(7)) maxS := float64(lines) * lh for tc := 0; tc < 2000; tc++ { var s float64 switch tc % 4 { case 0: s = rng.Float64() * maxS case 1: s = float64(int(rng.Float64() * maxS)) case 2: s = float64(rng.Intn(lines)) * lh case 3: s = float64(rng.Intn(lines))*lh + rng.Float64()*(lh-1) } sOff := ui.Dp(s) // What layoutFrame/visibleByteRangePrecise does with the effective // line height: window starts at content line k = floor(s/lh). start, _, _ := cb.VisibleByteRange(sOff, 0, ui.Dp(viewportH), EffectiveLineHeightAt(fs), false, ui.GlyphLayout{}, nil) if start%bytesPerLine != 0 { t.Fatalf("test setup: window start %d not on a line boundary", start) } k := start / bytesPerLine // Fabricate the windowed GlyphLayout exactly as the shaper would // with the scaled line pitch: display line j = content line k+j. nWin := int(math.Min(viewportH/lh+2, float64(lines-k))) var gl ui.GlyphLayout gl.LineHeight = EffectiveLineHeightAt(fs) for j := 0; j < nWin; j++ { gl.ByteOffsets = append(gl.ByteOffsets, j*bytesPerLine) gl.X = append(gl.X, 10) gl.Y = append(gl.Y, ui.Dp(float64(j+1)*lh)) gl.Advance = append(gl.Advance, 10) } TheState = NewState() TheState.fontScale = fs TheState.Editor.ChunkedBuffer = cb TheState.Editor.GlyphLayout = gl TheState.Editor.IMEWindowStartByte = start TheState.ScrollOffset = sOff TheState.EditorRegion = ui.Region{X: 10, Y: ui.Dp(regionTop), W: 390, H: ui.Dp(viewportH)} a := rng.Float64() * viewportH // tap y relative to region top HandleTapAt(15, ui.Dp(regionTop+a)) // Independent ground truth: content_y = a + s -> content line. wantLine := int(math.Floor((a + s) / lh)) if wantLine >= lines { wantLine = lines - 1 } if k+nWin-1 < wantLine { wantLine = k + nWin - 1 } got := TheState.Editor.CursorPosition wantLo, wantHi := wantLine*bytesPerLine, wantLine*bytesPerLine+bytesPerLine if got < wantLo || got >= wantHi { t.Fatalf("fs=%v s=%.2f (k=%d) tap a=%.2f: cursor=%d, want line %d [bytes %d,%d)", fs, s, k, a, got, wantLine, wantLo, wantHi) } } }