- SurfaceView windows are always translucent, so adjustResize cannot resize them: the keyboard overlapped the bottom of the window and the top bar sat under the status bar. Consume the insets in app instead: a smali-injected PadInsetsListener (identical block in build_emu.sh and build_phone.sh) shrinks the GioView to statusBarBottom..keyboardTop (or ..navBarTop) on every API 30+ insets dispatch, with setDecorFitsSystemWindows(false) so IME insets are delivered at targetSdk 34. - Fix a change-detection bug in the listener: the height write skipped the topMargin/bottomMargin checks on every IME state transition, so the margins were never applied while the keyboard animated. All three params are now checked with a single requestLayout on any change. - Selection menu now re-anchors to the selected word every frame while it is visible and hides when the word scrolls out of the viewport (positionSelectionMenu + regression test). - Doc: development_plan.md section 14 (invariants, geometry contract, build-script identity invariant). Verified on device: top bar below status bar and clickable, bottom bar flush with keyboard top (view 128..1517 at 1080x2400), BACK dismiss + re-tap re-raise without a show loop, typing saves, menu tracks scrolls.
120 lines
4.2 KiB
Go
120 lines
4.2 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 below the anchor's line: lineTop(line 5) = reg.Y + 5*lh
|
|
// (reg.Y = 10+52, the anchor's visual line index is 5).
|
|
wantY0 := ui.Dp(62) + ui.Dp(5*lh) + EffectiveLineHeight() + 8
|
|
if !dpeq(y0, wantY0) {
|
|
t.Fatalf("initial menu Y = %v, want %v (below 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)
|
|
}
|
|
}
|