From 49b60a79e877ab0a9224223b52e5ce015e00ed90 Mon Sep 17 00:00:00 2001 From: Greg Pomerantz Date: Wed, 19 Aug 2026 22:36:05 -0400 Subject: [PATCH] Make editor top/bottom bars full-width; keep margin on the text area The 10dp margin used to surround the top and bottom bars, leaving a gap around them. The bars now span the full screen width and sit flush with the top/bottom edges (merging with the system UI); their inner content (icons, labels) keeps the margin so it still aligns with the text area. The editor text area and the browser file list keep the margin, as before, and the editor gains the 20dp of height the bar margins used to occupy. Tests: menu-geometry expectations shifted with editorRegion.Y 42 -> 32; the below-placement clamp test now expects the window-top clamp; the stable-end test anchors at line 3 (less headroom above line 2 now). --- internal/editor/selection_menu_track_test.go | 32 +++++++++++--------- internal/editor/state.go | 25 ++++++++------- internal/test/e2e/scroll_cursor_test.go | 8 ++--- 3 files changed, 35 insertions(+), 30 deletions(-) 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}, }, })