Make the editor top and bottom bars ~30% taller for bigger tap targets

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.
This commit is contained in:
Greg Pomerantz 2026-09-03 12:34:18 -04:00
parent e96464ec79
commit ef54486683
5 changed files with 41 additions and 26 deletions

View File

@ -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 {

View File

@ -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)
}

View File

@ -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},
}),
},

View File

@ -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

View File

@ -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)