From ef544866830a516af2311c4fc55f9b6070a3778d Mon Sep 17 00:00:00 2001 From: Greg Pomerantz Date: Thu, 3 Sep 2026 12:34:18 -0400 Subject: [PATCH] Make the editor top and bottom bars ~30% taller for bigger tap targets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The top bar's back/search icons (24dp in a 32dp bar) and the bottom bar's wrap toggle (a text label in a 24dp bar) were fiddly to hit. Top bar: 32 -> 42dp (new TopBarHeight const, which buildFindBar and the editor text area already hang off); its icons 24 -> 31dp (new TopBarIconSize; the find bar keeps the smaller ui.IconSize). Bottom bar: BottomBarHeight 24 -> 31dp, labels re-centred. While verifying, a relaunch into an app-unreadable saved directory (/storage/emulated is media_rw:media_rw 0750 on device; only /storage/emulated/0 is app-exposed) showed an empty browser: os.Stat passes for such dirs, so the restore guard now probes with a real os.ReadDir — the same operation the browser index does. Test expectations that hard-coded the old 32dp top bar now use TopBarHeight. Verified on emulator and phone: back/search/wrap all tappable, find bar still docks under the taller top bar. --- cmd/pad/main.go | 12 ++++++---- internal/editor/selection_menu_track_test.go | 22 +++++++++-------- internal/editor/state.go | 25 +++++++++++++------- internal/test/e2e/find_test.go | 6 ++--- internal/ui/layout.go | 2 +- 5 files changed, 41 insertions(+), 26 deletions(-) diff --git a/cmd/pad/main.go b/cmd/pad/main.go index 3eab990..b27b44e 100644 --- a/cmd/pad/main.go +++ b/cmd/pad/main.go @@ -120,10 +120,14 @@ func run(w *app.Window) error { if sess, ok := loadSession(sessPath); ok { if sess.InBrowser { log.Printf("restoring session: browser path=%q", sess.BrowserPath) - // The saved directory may have vanished (deleted, synced away): - // fall back to the startup directory rather than pointing the - // browser at a dead path. - if fi, err := os.Stat(sess.BrowserPath); err == nil && fi.IsDir() { + // The saved directory may have vanished (deleted, synced away) or + // be unreadable by this app (stat succeeds but the permission bits + // deny the read — e.g. /storage/emulated itself, which is + // media_rw:media_rw 0750; only /storage/emulated/0 is app-exposed). + // Probe with a real directory read — the same operation the + // browser's index does — rather than os.Stat, which passes for + // unreadable dirs. Fall back to the startup directory otherwise. + if _, err := os.ReadDir(sess.BrowserPath); err == nil { logic.BeginBrowserRestore(sess.BrowserPath) } } else { diff --git a/internal/editor/selection_menu_track_test.go b/internal/editor/selection_menu_track_test.go index ceff2f6..fa49214 100644 --- a/internal/editor/selection_menu_track_test.go +++ b/internal/editor/selection_menu_track_test.go @@ -72,7 +72,7 @@ func TestSelectionMenu_FollowsTextAcrossScroll(t *testing.T) { // The menu sits ABOVE the anchor's line: lineTop(line 5) = reg.Y + 5*lh // (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 + wantY0 := TopBarHeight + ui.Dp(5*lh) - menuH - 8 if !dpeq(y0, wantY0) { t.Fatalf("initial menu Y = %v, want %v (above the anchor line)", y0, wantY0) } @@ -109,19 +109,20 @@ func TestSelectionMenu_ClampsWhenTextLeavesView(t *testing.T) { } yTop := e.MenuRect.Y // above line 5 - // Scroll 10 lines: line 5 is 5 lines ABOVE the viewport top. Both ends + // Scroll 11 lines: line 5 is 6 lines ABOVE the viewport top. Both ends // of the selection stay inside the shaped window (small file = whole // buffer) so the menu tracks: the preferred above-placement overflows // (the line is off-screen top) and the menu flips below the line, which - // lands just under the window top (lineTop + lh + handleDropDp + 8). - TheState.ScrollOffset = ui.Dp(10 * lh) + // overflows the window top too (lineTop + lh + handleDropDp + 8 = 0 < 8) + // and clamps to it. + TheState.ScrollOffset = ui.Dp(11 * lh) EditorLayout(ui.Dp(1000), ui.Dp(2000), false) if !e.MenuVisible { t.Fatal("menu hidden, but the selection is still in the shaped window") } - // Below-placement: lineTop(32) + 5*lh - 10*lh + lh + handleDropDp + 8 - // = 6.8 < 8, so the menu clamps to the window top. + // Below-placement: lineTop(TopBarHeight) + 5*lh - 11*lh + lh + handleDropDp + 8 + // = 0 < 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) } @@ -171,9 +172,10 @@ func TestSelectionMenu_FlipsBelowOnFirstLine(t *testing.T) { if !e.MenuVisible { t.Fatal("menu not shown") } - // 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(32) + EffectiveLineHeight() + ui.Dp(handleDropDp) + 8 + // Line 0 top = reg.Y = TopBarHeight; above would be TopBarHeight-52-8 = + // -18 < 8, so the menu flips below the selection HANDLES: line bottom + + // handleDropDp + 8. + wantY := TopBarHeight + 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) } @@ -220,7 +222,7 @@ func TestSelectionMenu_AnchorsToStableEnd(t *testing.T) { t.Fatal("menu not shown") } // Above line 3 (the stable start), NOT above line 5 (the moving end). - wantY := ui.Dp(32) + ui.Dp(3*lh) - menuH - 8 + wantY := TopBarHeight + 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) } diff --git a/internal/editor/state.go b/internal/editor/state.go index b96aacc..ccd3ee3 100644 --- a/internal/editor/state.go +++ b/internal/editor/state.go @@ -2176,6 +2176,15 @@ func markDirty() { // (which shrinks the text area to make room) both use it. const FindBarHeight = ui.Dp(40) +// TopBarHeight is the editor top bar's height (buildFindBar hangs off it +// too); TopBarIconSize is that bar's icon size — both ~30% larger than the +// old 32dp/24dp pair so the back/search icons are a comfortable tap +// target. The find bar keeps the smaller ui.IconSize. +const ( + TopBarHeight = ui.Dp(42) + TopBarIconSize = ui.Dp(31) +) + // buildFindBar lays out the in-file search bar: [input][counter][prev][next] // [clear-X], full screen width, directly below the margin-free top bar (the // bars merge, so this one does too; inner content keeps the margin). The @@ -2186,7 +2195,7 @@ func buildFindBar(screenWidth ui.Dp) ui.Element { margin := ui.Dp(10) gap := ui.Dp(8) h := FindBarHeight - topY := ui.Dp(32) // top bar height (see EditorLayout) + topY := TopBarHeight // top bar height (see EditorLayout) // Right-hand button column: prev, next, clear (X). iconW := ui.IconSize @@ -2235,7 +2244,7 @@ func EditorLayout(screenWidth, screenHeight ui.Dp, wordWrap bool) []ui.Element { statusBarRegion := ui.Region{ X: 0, Y: 0, W: screenWidth, - H: ui.Dp(32), + H: TopBarHeight, } statusBarW := statusBarRegion.W filename := TheState.Editor.Filename @@ -2246,10 +2255,10 @@ 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: margin, Y: ui.Dp(4), W: ui.IconSize, H: ui.IconSize}, 0, + ui.NewIcon("back", ui.Region{X: margin, Y: ui.Dp(5), W: TopBarIconSize, H: TopBarIconSize}, 0, []ui.Interaction{{Gesture: ui.Tap, Handler: GoToBrowser}}), - ui.NewLabel(filename, 14, ui.Region{X: ui.Dp(32) + margin, Y: ui.Dp(6), W: statusBarW - ui.Dp(32) - margin - ui.IconSize - ui.Dp(8), H: ui.Dp(20)}, ui.AlignStart, "", nil), - ui.NewIcon("search", ui.Region{X: screenWidth - margin - ui.IconSize, Y: ui.Dp(4), W: ui.IconSize, H: ui.IconSize}, 0, + ui.NewLabel(filename, 14, ui.Region{X: margin + TopBarIconSize + ui.Dp(8), Y: ui.Dp(11), W: statusBarW - margin - TopBarIconSize - ui.Dp(8) - margin - TopBarIconSize, H: ui.Dp(20)}, ui.AlignStart, "", nil), + ui.NewIcon("search", ui.Region{X: screenWidth - margin - TopBarIconSize, Y: ui.Dp(5), W: TopBarIconSize, H: TopBarIconSize}, 0, []ui.Interaction{{Gesture: ui.Tap, Handler: ToggleFind}}), }, ) @@ -2291,9 +2300,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: 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 - margin, H: ui.Dp(20)}, ui.AlignEnd, "wrap", []ui.Interaction{ + ui.NewLabel(statusText, 12, ui.Region{X: margin, Y: ui.Dp(5), W: bottomBarW - margin, H: ui.Dp(20)}, ui.AlignStart, "", nil), + ui.NewLabel(cursorPosText, 12, ui.Region{X: 0, Y: ui.Dp(5), W: bottomBarW, H: ui.Dp(20)}, ui.AlignCenter, "", nil), + ui.NewLabel(wrapText, 12, ui.Region{X: 0, Y: ui.Dp(5), W: bottomBarW - margin, H: ui.Dp(20)}, ui.AlignEnd, "wrap", []ui.Interaction{ {Gesture: ui.Tap, Handler: ToggleWordWrap}, }), }, diff --git a/internal/test/e2e/find_test.go b/internal/test/e2e/find_test.go index ccafd07..70eef1c 100644 --- a/internal/test/e2e/find_test.go +++ b/internal/test/e2e/find_test.go @@ -108,10 +108,10 @@ func TestRealFile_FindBarAndNavigation(t *testing.T) { t.Fatalf("focus %q, want find_bar", focus) } // The text area must shrink from the top while the find bar is open so - // the first line is not covered: region top = top bar (32) + FindBarHeight. + // the first line is not covered: region top = TopBarHeight + FindBarHeight. reg, _ := h.Inspect(func(st *editor.State) any { return st.EditorRegion }) - if got := reg.(ui.Region); got.Y != ui.Dp(32)+editor.FindBarHeight { - t.Fatalf("editor region top %d, want %d (find bar must not cover text)", int(got.Y), int(ui.Dp(32)+editor.FindBarHeight)) + if got := reg.(ui.Region); got.Y != editor.TopBarHeight+editor.FindBarHeight { + t.Fatalf("editor region top %d, want %d (find bar must not cover text)", int(got.Y), int(editor.TopBarHeight+editor.FindBarHeight)) } // The editor field reports UNFOCUSED while the find bar is open (the // renderer hides its caret for unfocused fields; main hands key focus to diff --git a/internal/ui/layout.go b/internal/ui/layout.go index 43dc854..d8d8471 100644 --- a/internal/ui/layout.go +++ b/internal/ui/layout.go @@ -5,7 +5,7 @@ const ( StatusBarLineHeight = Dp(24) StatusBarFilenameLine = Dp(24) StatusBarIconsLine = Dp(24) - BottomBarHeight = Dp(24) + BottomBarHeight = Dp(31) // ~30% taller than the old 24dp: the bar's only tap target (the wrap toggle) is a text label, so the bar doubles as its hit area IconSize = Dp(24) IconGap = Dp(36) Padding = Dp(8)