diff --git a/internal/editor/selection_menu_track_test.go b/internal/editor/selection_menu_track_test.go index ec69eac..ceff2f6 100644 --- a/internal/editor/selection_menu_track_test.go +++ b/internal/editor/selection_menu_track_test.go @@ -70,8 +70,9 @@ func TestSelectionMenu_FollowsTextAcrossScroll(t *testing.T) { } 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 + // (reg.Y = 32: the top bar is margin-free, the anchor's visual line + // index is 5). + wantY0 := ui.Dp(32) + ui.Dp(5*lh) - menuH - 8 if !dpeq(y0, wantY0) { t.Fatalf("initial menu Y = %v, want %v (above the anchor line)", y0, wantY0) } @@ -119,10 +120,10 @@ func TestSelectionMenu_ClampsWhenTextLeavesView(t *testing.T) { if !e.MenuVisible { t.Fatal("menu hidden, but the selection is still in the shaped window") } - lineTop := ui.Dp(42) + ui.Dp(5*lh) - ui.Dp(10*lh) // line 5 after the scroll - want := lineTop + ui.Dp(lh) + ui.Dp(handleDropDp) + 8 - if got := e.MenuRect.Y; !dpeq(got, want) { - t.Fatalf("menu Y = %v, want %v (tracked below the off-screen-top line, near the window top)", got, want) + // Below-placement: lineTop(32) + 5*lh - 10*lh + lh + handleDropDp + 8 + // = 6.8 < 8, so the menu clamps to the window top. + if got := e.MenuRect.Y; !dpeq(got, 8) { + t.Fatalf("menu Y = %v, want 8 (below-placement clamped to the window top)", got) } _ = yTop @@ -170,9 +171,9 @@ func TestSelectionMenu_FlipsBelowOnFirstLine(t *testing.T) { 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 + // Line 0 top = reg.Y = 32; above would be 32-52-8 = -28 < 8, so the menu // flips below the selection HANDLES: line bottom + handleDropDp + 8. - wantY := ui.Dp(42) + EffectiveLineHeight() + ui.Dp(handleDropDp) + 8 + wantY := ui.Dp(32) + EffectiveLineHeight() + ui.Dp(handleDropDp) + 8 if !dpeq(e.MenuRect.Y, wantY) { t.Fatalf("menu Y = %v, want %v (flipped below the first line, clear of the handles)", e.MenuRect.Y, wantY) } @@ -187,14 +188,15 @@ func TestSelectionMenu_FlipsBelowOnFirstLine(t *testing.T) { func TestSelectionMenu_AnchorsToStableEnd(t *testing.T) { lh := menuTrackState(t) - // 8 lines; selection from line 2 (stable start) to line 5 (moving end), - // so the menu has room ABOVE the top line (line 0 would flip below). + // 8 lines; selection from line 3 (stable start) to line 5 (moving end), + // so the menu has room ABOVE the top line (a lower line would flip + // below now that the margin-free top bar leaves less headroom). var buf strings.Builder for i := 0; i < 8; i++ { buf.WriteString("aaa\n") } TheState.Editor.Buffer = buf.String() - SetSelection(8, 18) // lines 2..4 (anchor at line 2's first byte) + SetSelection(12, 22) // lines 3..5 (anchor at line 3's first byte) gl := ui.GlyphLayout{LineHeight: EffectiveLineHeight()} const ascent float64 = 14 @@ -217,15 +219,15 @@ func TestSelectionMenu_AnchorsToStableEnd(t *testing.T) { if !e.MenuVisible { t.Fatal("menu not shown") } - // Above line 2 (the stable start), NOT above line 4 (the moving end). - wantY := ui.Dp(42) + ui.Dp(2*lh) - menuH - 8 + // Above line 3 (the stable start), NOT above line 5 (the moving end). + wantY := ui.Dp(32) + ui.Dp(3*lh) - menuH - 8 if !dpeq(e.MenuRect.Y, wantY) { t.Fatalf("menu Y = %v, want %v (above the selection's TOP line)", e.MenuRect.Y, wantY) } // Drag the END handle down to line 7: the menu must stay put (still - // anchored to the stable start at line 2), not follow the end down. - SetSelection(8, 30) // lines 2..7 + // anchored to the stable start at line 3), not follow the end down. + SetSelection(12, 30) // lines 3..7 EditorLayout(ui.Dp(1000), ui.Dp(2000), false) if !e.MenuVisible { t.Fatal("menu hidden while the stable end is in view") diff --git a/internal/editor/state.go b/internal/editor/state.go index 21bce43..75641ae 100644 --- a/internal/editor/state.go +++ b/internal/editor/state.go @@ -1851,10 +1851,12 @@ func EditorLayout(screenWidth, screenHeight ui.Dp, wordWrap bool) []ui.Element { // --- Top bar: one row, back icon + filename. Cut/copy/paste live in the // floating selection menu (the native Android pattern); the dead icon row - // is gone, saving 20dp of editor height. --- + // is gone, saving 20dp of editor height. The bar spans the full screen + // width with no outer margin (it merges with the system UI); the inner + // content keeps the margin so it aligns with the text area below. --- statusBarRegion := ui.Region{ - X: margin, Y: margin, - W: screenWidth - margin*2, + X: 0, Y: 0, + W: screenWidth, H: ui.Dp(32), } statusBarW := statusBarRegion.W @@ -1866,18 +1868,19 @@ func EditorLayout(screenWidth, screenHeight ui.Dp, wordWrap bool) []ui.Element { statusBarRegion, ui.Color{R: 230, G: 230, B: 230, A: 255}, []ui.Element{ - ui.NewIcon("back", ui.Region{X: ui.Dp(0), Y: ui.Dp(4), W: ui.IconSize, H: ui.IconSize}, 0, + ui.NewIcon("back", ui.Region{X: margin, Y: ui.Dp(4), W: ui.IconSize, H: ui.IconSize}, 0, []ui.Interaction{{Gesture: ui.Tap, Handler: GoToBrowser}}), - ui.NewLabel(filename, 14, ui.Region{X: ui.Dp(32), Y: ui.Dp(6), W: statusBarW - ui.Dp(32), H: ui.Dp(20)}, ui.AlignStart, "", nil), + ui.NewLabel(filename, 14, ui.Region{X: ui.Dp(32) + margin, Y: ui.Dp(6), W: statusBarW - ui.Dp(32) - margin, H: ui.Dp(20)}, ui.AlignStart, "", nil), }, ) - // --- Bottom bar --- + // --- Bottom bar: full screen width, flush with the screen bottom (no + // outer margin, same as the top bar). Inner labels keep the margin. --- bottomBarHeight := ui.BottomBarHeight - bottomBarY := screenHeight - margin - bottomBarHeight + bottomBarY := screenHeight - bottomBarHeight bottomBarRegion := ui.Region{ - X: margin, Y: bottomBarY, - W: screenWidth - margin*2, + X: 0, Y: bottomBarY, + W: screenWidth, H: bottomBarHeight, } bottomBarW := bottomBarRegion.W @@ -1908,9 +1911,9 @@ func EditorLayout(screenWidth, screenHeight ui.Dp, wordWrap bool) []ui.Element { bottomBarRegion, ui.Color{R: 230, G: 230, B: 230, A: 255}, []ui.Element{ - ui.NewLabel(statusText, 12, ui.Region{X: 0, Y: ui.Dp(2), W: bottomBarW, H: ui.Dp(20)}, ui.AlignStart, "", nil), + ui.NewLabel(statusText, 12, ui.Region{X: margin, Y: ui.Dp(2), W: bottomBarW - margin, H: ui.Dp(20)}, ui.AlignStart, "", nil), ui.NewLabel(cursorPosText, 12, ui.Region{X: 0, Y: ui.Dp(2), W: bottomBarW, H: ui.Dp(20)}, ui.AlignCenter, "", nil), - ui.NewLabel(wrapText, 12, ui.Region{X: 0, Y: ui.Dp(2), W: bottomBarW, H: ui.Dp(20)}, ui.AlignEnd, "wrap", []ui.Interaction{ + ui.NewLabel(wrapText, 12, ui.Region{X: 0, Y: ui.Dp(2), W: bottomBarW - margin, H: ui.Dp(20)}, ui.AlignEnd, "wrap", []ui.Interaction{ {Gesture: ui.Tap, Handler: ToggleWordWrap}, }), }, diff --git a/internal/test/e2e/scroll_cursor_test.go b/internal/test/e2e/scroll_cursor_test.go index dc2319f..27d8ec4 100644 --- a/internal/test/e2e/scroll_cursor_test.go +++ b/internal/test/e2e/scroll_cursor_test.go @@ -85,13 +85,13 @@ func TestEditorClickToMoveCursorWithScroll(t *testing.T) { Handler: func(data any) { if pt, ok := data.(ui.Point); ok { // Manually simulate the offset correction that EditorLayout does - // editorRegion.Y is margin(10) + statusBarH(32) = 42. - // Let's set pt.Y to 10 + 42 = 52. - localY := float64(pt.Y) - 42.0 + float64(editor.TheState.ScrollOffset) + // editorRegion.Y is the (margin-free) statusBarH(32) = 32. + // Let's set pt.Y to 10 + 32 = 42. + localY := float64(pt.Y) - 32.0 + float64(editor.TheState.ScrollOffset) editor.SetCursorFromPoint(float64(pt.X), localY) } }, - Data: ui.Point{X: 10, Y: 52}, + Data: ui.Point{X: 10, Y: 42}, }, })