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).
This commit is contained in:
Greg Pomerantz 2026-08-19 22:36:05 -04:00
parent eb725ee781
commit 49b60a79e8
3 changed files with 35 additions and 30 deletions

View File

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

View File

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

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