Collapse editor top bar to one row (back + filename)

The cut/copy/paste icons were dead placeholders (no handlers);
clipboard actions live in the floating selection menu, so the second
icon row is removed. The bar goes from two rows at 52dp to one row at
32dp, giving the editor 20dp more height.

- filename_test.go now finds the label by type (back icon precedes it)
- menu tracking tests updated for the new editorY (reg.Y = 10+32)
- scroll_cursor e2e simulation updated to match
- On-device verified: back button works, floating menu places above
  with room and flips below on the first lines, handles unaffected.
This commit is contained in:
Greg Pomerantz 2026-08-18 08:32:19 -04:00
parent dc35aa2e36
commit 2b7c9cd3eb
5 changed files with 43 additions and 24 deletions

View File

@ -934,3 +934,15 @@ grabbable from the uncovered top strip of their boxes.
Regressions: `internal/editor/selection_menu_track_test.go` (tracking +
above-placement arithmetic + first-line flip).
## 16. Top bar: single row (2026-08-18)
The dead cut/copy/paste icon row was removed from the editor top bar
(clipboard actions live exclusively in the floating selection menu, the
native pattern). The bar is now one 32dp row — back icon + filename —
down from two rows at 52dp, giving the editor 20dp more height. The
menu's "above first" placement (section 15) is unchanged; with the
shorter bar, selections on the first lines flip below because there is
no room above, and the menu may overlap the top bar when clamped high —
both are the native toolbar's behaviour (transient, dismissed by
tapping elsewhere).

View File

@ -21,9 +21,14 @@ func TestEditorLayout_Filename(t *testing.T) {
// Find top bar (assuming it's the first container element)
topBar := elements[0].(ui.Container)
// Find filename label (assuming it's the first label in the top bar)
filenameLabel := topBar.Children[0].(ui.Label)
// Find the filename label in the top bar (the back icon precedes it).
var filenameLabel ui.Label
for _, c := range topBar.Children {
if l, ok := c.(ui.Label); ok {
filenameLabel = l
break
}
}
if filenameLabel.Text != expectedFilename {
t.Errorf("expected filename %q, got %q", expectedFilename, filenameLabel.Text)
}
@ -44,9 +49,14 @@ func TestEditorLayout_DefaultFilename(t *testing.T) {
// Find top bar (assuming it's the first container element)
topBar := elements[0].(ui.Container)
// Find filename label (assuming it's the first label in the top bar)
filenameLabel := topBar.Children[0].(ui.Label)
// Find the filename label in the top bar (the back icon precedes it).
var filenameLabel ui.Label
for _, c := range topBar.Children {
if l, ok := c.(ui.Label); ok {
filenameLabel = l
break
}
}
expectedDefault := "untitled.txt"
if filenameLabel.Text != expectedDefault {
t.Errorf("expected default filename %q, got %q", expectedDefault, filenameLabel.Text)

View File

@ -70,8 +70,8 @@ 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+52, the anchor's visual line index is 5).
wantY0 := ui.Dp(62) + ui.Dp(5*lh) - menuH - 8
// (reg.Y = 10+32, the anchor's visual line index is 5).
wantY0 := ui.Dp(42) + ui.Dp(5*lh) - menuH - 8
if !dpeq(y0, wantY0) {
t.Fatalf("initial menu Y = %v, want %v (above the anchor line)", y0, wantY0)
}
@ -150,9 +150,9 @@ func TestSelectionMenu_FlipsBelowOnFirstLine(t *testing.T) {
if !e.MenuVisible {
t.Fatal("menu not shown")
}
// Line 0 top = reg.Y = 62; above would be 62-52-8 = 2 < 8, so the menu
// flips below: 62 + lh + 8.
wantY := ui.Dp(62) + EffectiveLineHeight() + 8
// Line 0 top = reg.Y = 42; above would be 42-52-8 = -18 < 8, so the menu
// flips below: 42 + lh + 8.
wantY := ui.Dp(42) + EffectiveLineHeight() + 8
if !dpeq(e.MenuRect.Y, wantY) {
t.Fatalf("menu Y = %v, want %v (flipped below the first line)", e.MenuRect.Y, wantY)
}

View File

@ -1601,11 +1601,13 @@ func markDirty() {
func EditorLayout(screenWidth, screenHeight ui.Dp, wordWrap bool) []ui.Element {
margin := ui.Dp(10)
// --- Top bar: filename on row 1, icons on row 2 ---
// --- 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. ---
statusBarRegion := ui.Region{
X: margin, Y: margin,
W: screenWidth - margin*2,
H: ui.Dp(52),
H: ui.Dp(32),
}
statusBarW := statusBarRegion.W
filename := TheState.Editor.Filename
@ -1616,14 +1618,9 @@ func EditorLayout(screenWidth, screenHeight ui.Dp, wordWrap bool) []ui.Element {
statusBarRegion,
ui.Color{R: 230, G: 230, B: 230, A: 255},
[]ui.Element{
// Row 1: filename
ui.NewLabel(filename, 14, ui.Region{X: 0, Y: ui.Dp(2), W: statusBarW, H: ui.Dp(20)}, ui.AlignStart, "", nil),
// Row 2: back, cut, copy, paste icons
ui.NewIcon("back", ui.Region{X: ui.Dp(0), Y: ui.Dp(28), W: ui.IconSize, H: ui.IconSize}, 0,
ui.NewIcon("back", ui.Region{X: ui.Dp(0), Y: ui.Dp(4), W: ui.IconSize, H: ui.IconSize}, 0,
[]ui.Interaction{{Gesture: ui.Tap, Handler: GoToBrowser}}),
ui.NewIcon("cut", ui.Region{X: ui.Dp(48), Y: ui.Dp(28), W: ui.IconSize, H: ui.IconSize}, 0, nil),
ui.NewIcon("copy", ui.Region{X: ui.Dp(96), Y: ui.Dp(28), W: ui.IconSize, H: ui.IconSize}, 0, nil),
ui.NewIcon("paste", ui.Region{X: ui.Dp(144), Y: ui.Dp(28), W: ui.IconSize, H: ui.IconSize}, 0, nil),
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),
},
)

View File

@ -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(52) = 62.
// Let's set pt.Y to 10 + 62 = 72.
localY := float64(pt.Y) - 62.0 + float64(editor.TheState.ScrollOffset)
// 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)
editor.SetCursorFromPoint(float64(pt.X), localY)
}
},
Data: ui.Point{X: 10, Y: 72},
Data: ui.Point{X: 10, Y: 52},
},
})