package ui import "testing" // TestCaretPoint_EmptyFirstLine is a regression test for the caret jumping // to an adjacent line while scrolling: when the shaped window's first visual // line is empty (no recorded glyphs), the old code anchored the case-B // baseline on the smallest recorded Y — the first NON-empty line — so every // boundary caret (line-end "\n", an empty line's lone byte, EOF) drew one // line too low per leading empty line, and the caret visibly jumped a line // when the window scrolled past the empty line. func TestCaretPoint_EmptyFirstLine(t *testing.T) { const ( lh = Dp(16.8) ascent = Dp(13.3) ) // Window text starting on an empty line: line 0 is a lone "\n" (byte 0, // no glyphs), line 1 is "abc" (bytes 1-3, "\n" at 4), line 2 is "def" // (bytes 5-7, no trailing newline). Baselines are the uniform shaper // grid: line i at ascent + i*lh; line 0 has no recorded glyphs. str := "\nabc\ndef" layout := GlyphLayout{ LineHeight: lh, // VisualLineStarts: one entry per line start (0, 1, 5). The last // line has no trailing break; the entry for it is byte 5. VisualLineStarts: []int{0, 1, 5}, ByteOffsets: []int{1, 2, 3, 5, 6, 7}, X: []Dp{0, 10, 20, 0, 10, 20}, Y: []Dp{ascent + lh, ascent + lh, ascent + lh, ascent + 2*lh, ascent + 2*lh, ascent + 2*lh}, Advance: []Dp{10, 10, 10, 10, 10, 10}, } near := func(got, want Dp) bool { d := float64(got - want) return d < 0.01 && d > -0.01 } check := func(name string, byteOff int, wantX, wantY Dp) { t.Helper() x, y := CaretPoint(layout, str, byteOff, ascent, lh) if !near(x, wantX) || !near(y, wantY) { t.Errorf("%s: CaretPoint(%d) = (%v, %v); want (%v, %v)", name, byteOff, x, y, wantX, wantY) } } // Case A: a glyph-start byte is unaffected (always was) — "a" of line 1. check("glyph start (line 1)", 1, 0, ascent+lh) // Case B: the empty first line's lone insertion point sits on line 0. // Old code: minY = ascent+lh, line 0 -> y = ascent+lh (one line low). check("empty line 0 origin", 0, 0, ascent) // Case B: the terminating "\n" of line 1 sits at line 1's right edge. // Old code: minY = ascent+lh, line 1 -> y = ascent+2*lh (one line low). check("line 1 end (\\n)", 4, 30, ascent+lh) // Case B: EOF on the last line sits at line 2's right edge. // Old code: y = ascent+lh+2*lh (one line low). check("EOF (line 2 end)", 8, 30, ascent+2*lh) } // TestCaretPoint_EmptyFirstLines covers several leading empty lines: the // anchor must walk back j full line heights, not one. func TestCaretPoint_EmptyFirstLines(t *testing.T) { const ( lh = Dp(16.8) ascent = Dp(13.3) ) // Two empty lines, then "abc". str := "\n\nabc" layout := GlyphLayout{ LineHeight: lh, VisualLineStarts: []int{0, 1, 2}, ByteOffsets: []int{2, 3, 4}, X: []Dp{0, 10, 20}, Y: []Dp{ascent + 2*lh, ascent + 2*lh, ascent + 2*lh}, Advance: []Dp{10, 10, 10}, } near := func(got, want Dp) bool { d := float64(got - want) return d < 0.01 && d > -0.01 } // End of the "abc" line (no trailing newline): right edge of line 2. x, y := CaretPoint(layout, str, 5, ascent, lh) wantX, wantY := Dp(30), ascent+2*lh if !near(x, wantX) || !near(y, wantY) { t.Errorf("EOF after two empty lines = (%v, %v); want (%v, %v)", x, y, wantX, wantY) } // The second empty line's origin: line 1. x, y = CaretPoint(layout, str, 1, ascent, lh) wantX, wantY = Dp(0), ascent+lh if !near(x, wantX) || !near(y, wantY) { t.Errorf("empty line 1 origin = (%v, %v); want (%v, %v)", x, y, wantX, wantY) } } // TestCaretPoint_NonEmptyFirstLine guards against regressions in the common // case: with a glyph on the window's first line the anchor is the first // recorded baseline (the old behaviour), unchanged. func TestCaretPoint_NonEmptyFirstLine(t *testing.T) { const ( lh = Dp(16.8) ascent = Dp(13.3) ) str := "abc\ndef" layout := GlyphLayout{ LineHeight: lh, VisualLineStarts: []int{0, 4}, ByteOffsets: []int{0, 1, 2, 4, 5, 6}, X: []Dp{0, 10, 20, 0, 10, 20}, Y: []Dp{ascent, ascent, ascent, ascent + lh, ascent + lh, ascent + lh}, Advance: []Dp{10, 10, 10, 10, 10, 10}, } near := func(got, want Dp) bool { d := float64(got - want) return d < 0.01 && d > -0.01 } // Line 0's "\n": line 0's right edge at line 0's baseline (ascent). x, y := CaretPoint(layout, str, 3, ascent, lh) wantX, wantY := Dp(30), ascent if !near(x, wantX) || !near(y, wantY) { t.Errorf("line 0 end = (%v, %v); want (%v, %v)", x, y, wantX, wantY) } // EOF: line 1's right edge. x, y = CaretPoint(layout, str, 7, ascent, lh) wantX, wantY = Dp(30), ascent+lh if !near(x, wantX) || !near(y, wantY) { t.Errorf("EOF = (%v, %v); want (%v, %v)", x, y, wantX, wantY) } } // TestCaretPoint_NoGlyphs covers a window of pure newlines: the ascent // fallback anchor applies. func TestCaretPoint_NoGlyphs(t *testing.T) { const ( lh = Dp(16.8) ascent = Dp(13.3) ) str := "\n\n\n" layout := GlyphLayout{ LineHeight: lh, VisualLineStarts: []int{0, 1, 2}, } near := func(got, want Dp) bool { d := float64(got - want) return d < 0.01 && d > -0.01 } // The second empty line's origin: line 1. x, y := CaretPoint(layout, str, 1, ascent, lh) wantX, wantY := Dp(0), ascent+lh if !near(x, wantX) || !near(y, wantY) { t.Errorf("pure-newline window, empty line 1 = (%v, %v); want (%v, %v)", x, y, wantX, wantY) } }