Pad/internal/editor/selection_menu_track_test.go
Greg Pomerantz 2b7c9cd3eb Collapse editor top bar to one row (back + filename)
The cut/copy/paste icons were dead placeholders (no handlers);
clipboard actions live in the floating selection menu, so the second
icon row is removed. The bar goes from two rows at 52dp to one row at
32dp, giving the editor 20dp more height.

- filename_test.go now finds the label by type (back icon precedes it)
- menu tracking tests updated for the new editorY (reg.Y = 10+32)
- scroll_cursor e2e simulation updated to match
- On-device verified: back button works, floating menu places above
  with room and flips below on the first lines, handles unaffected.
2026-08-18 08:32:19 -04:00

160 lines
5.5 KiB
Go

package editor
import (
"math"
"strings"
"testing"
"pad/internal/ui"
)
// dpeq compares two Dp values within 0.01 Dp (all menu geometry is
// float32 Dp; this is far below one screen pixel at any density).
func dpeq(a, b ui.Dp) bool { return math.Abs(float64(a)-float64(b)) < 0.01 }
// menuTrackState builds a small-file state: a 200-line buffer ("aa" per
// line), a selection on line 5 (bytes [15,17)), and a shaped GlyphLayout
// covering the WHOLE buffer (the small-file window is the whole buffer, so
// the layout is identical before and after a scroll — the scroll changes
// only k/r, exactly as in production). Baselines use the shaper's real
// convention (drawWrappedText): ascent + lineHeight*lineIndex, ascent = 14.
func menuTrackState(t *testing.T) (lh float64) {
const ascent float64 = 14 // FontSize; 0 < ascent < lh is the invariant
t.Helper()
lh = float64(EffectiveLineHeight())
// Long enough that the 1904-Dp test viewport overflows and scrolling is
// allowed (EditorLayout clamps ScrollOffset to LastLineY-based maxScroll;
// a short file would clamp the test scroll back to 0).
lines := 200
var buf strings.Builder
for i := 0; i < lines; i++ {
buf.WriteString("aa\n")
}
TheState = NewState()
TheState.Editor.Buffer = buf.String()
SetSelection(15, 17) // "aa" on line 5
gl := ui.GlyphLayout{LineHeight: EffectiveLineHeight()}
for i := 0; i < lines; i++ {
for c := 0; c < 2; c++ {
gl.ByteOffsets = append(gl.ByteOffsets, i*3+c)
gl.X = append(gl.X, ui.Dp(10+10*c))
gl.Y = append(gl.Y, ui.Dp(ascent+float64(i)*lh))
gl.Advance = append(gl.Advance, 10)
}
}
TheState.Editor.GlyphLayout = gl
// LastLineY is normally set by the shaper's layout feedback; emulate it.
TheState.LastLineY = ui.Dp(float64(lines) * lh)
TheState.Editor.IMEWindowStartByte = 0
TheState.ScrollOffset = 0
return lh
}
// TestSelectionMenu_FollowsTextAcrossScroll is a regression test for the
// selection menu staying at a FIXED SCREEN POSITION while the user scrolled:
// the menu must track the selected text (the menu is anchored to the text,
// not to where it was first shown). The menu's Y must move down/up by exactly
// the scroll delta while the anchor stays in view.
func TestSelectionMenu_FollowsTextAcrossScroll(t *testing.T) {
lh := menuTrackState(t)
// Lay out once so EditorRegion (the text region the menu is placed in)
// is established, exactly as in production.
EditorLayout(ui.Dp(1000), ui.Dp(2000), false)
showSelectionMenu()
e := &TheState.Editor
if !e.MenuVisible {
t.Fatal("menu not shown for a visible selection")
}
y0 := e.MenuRect.Y
// The menu sits ABOVE the anchor's line: lineTop(line 5) = reg.Y + 5*lh
// (reg.Y = 10+32, the anchor's visual line index is 5).
wantY0 := ui.Dp(42) + ui.Dp(5*lh) - menuH - 8
if !dpeq(y0, wantY0) {
t.Fatalf("initial menu Y = %v, want %v (above the anchor line)", y0, wantY0)
}
// Scroll down by exactly 2 lines and lay out again.
TheState.ScrollOffset = ui.Dp(2 * lh)
EditorLayout(ui.Dp(1000), ui.Dp(2000), false)
// The anchor moved up 2 lines on screen; the menu must follow by exactly
// the same amount.
want := wantY0 - ui.Dp(2*lh)
if got := e.MenuRect.Y; !dpeq(got, want) {
t.Fatalf("menu Y after 2-line scroll = %v, want %v (menu must track the text)", got, want)
}
if !e.MenuVisible {
t.Fatal("menu hidden while its anchor is still in view")
}
}
// TestSelectionMenu_ClampsWhenTextLeavesView checks the small-file edge: the
// buffer is the whole window, so the anchor never "leaves"; when the selected
// text scrolls off the top, the menu pins to the top of the window (it never
// detaches into mid-screen stale territory).
func TestSelectionMenu_ClampsWhenTextLeavesView(t *testing.T) {
lh := menuTrackState(t)
EditorLayout(ui.Dp(1000), ui.Dp(2000), false)
showSelectionMenu()
e := &TheState.Editor
if !e.MenuVisible {
t.Fatal("menu not shown")
}
// Scroll 10 lines: line 5 is 4 lines ABOVE the viewport top.
TheState.ScrollOffset = ui.Dp(10 * lh)
EditorLayout(ui.Dp(1000), ui.Dp(2000), false)
if !e.MenuVisible {
t.Fatal("menu hidden, but small-file layout can still track it (pinned)")
}
if got := e.MenuRect.Y; !dpeq(got, 8) {
t.Fatalf("menu Y = %v, want 8 (pinned to the window top when the text is above the viewport)", got)
}
}
// TestSelectionMenu_FlipsBelowOnFirstLine checks the edge where there is no
// room above the anchor (selection on the first line): the menu must flip
// below the line rather than clamping to the window top and sitting over the
// anchor's own text.
func TestSelectionMenu_FlipsBelowOnFirstLine(t *testing.T) {
lh := menuTrackState(t)
TheState.Editor.Buffer = "aa\nbb\n"
SetSelection(0, 2) // "aa" on line 0
gl := ui.GlyphLayout{LineHeight: EffectiveLineHeight()}
const ascent float64 = 14
lines := 2
for i := 0; i < lines; i++ {
for c := 0; c < 2; c++ {
gl.ByteOffsets = append(gl.ByteOffsets, i*3+c)
gl.X = append(gl.X, ui.Dp(10+10*c))
gl.Y = append(gl.Y, ui.Dp(ascent+float64(i)*lh))
gl.Advance = append(gl.Advance, 10)
}
}
TheState.Editor.GlyphLayout = gl
TheState.LastLineY = ui.Dp(float64(lines) * lh)
TheState.Editor.IMEWindowStartByte = 0
TheState.ScrollOffset = 0
EditorLayout(ui.Dp(1000), ui.Dp(2000), false)
showSelectionMenu()
e := &TheState.Editor
if !e.MenuVisible {
t.Fatal("menu not shown")
}
// Line 0 top = reg.Y = 42; above would be 42-52-8 = -18 < 8, so the menu
// flips below: 42 + lh + 8.
wantY := ui.Dp(42) + EffectiveLineHeight() + 8
if !dpeq(e.MenuRect.Y, wantY) {
t.Fatalf("menu Y = %v, want %v (flipped below the first line)", e.MenuRect.Y, wantY)
}
}