Pad/internal/browser/layout.go
Greg Pomerantz 6a43e3c0db Add e2e test harness for testing editor logic without Gio display
- internal/test/e2e/: FrameCapture, Harness, ElementAssertions, helpers
- internal/editor/logic.go: Add done channel and Done() method for graceful shutdown
- internal/editor/mock_setup.go: Mock filesystem for tests
- internal/browser/: Browser layout and search logic
- internal/io/: Worker pool for async tasks
- Update architecture docs and spec
2026-05-31 08:56:30 -04:00

146 lines
4.1 KiB
Go

package browser
import (
"pad/internal/ui"
)
// BrowserLayout computes the element tree for the browser (file listing) page.
// Pure function: (screen dimensions, browser state) → []ui.Element
func BrowserLayout(screenW, screenH ui.Dp, state *BrowserState) []ui.Element {
margin := ui.Dp(10)
contentWidth := screenW - margin*2
// --- Header bar: directory path + sort toggle ---
headerHeight := ui.Dp(24)
headerRegion := ui.Region{
X: margin, Y: margin,
W: contentWidth, H: headerHeight,
}
sortLabel := sortModeLabel(state.SortMode)
headerBar := ui.NewContainer(
headerRegion,
ui.Color{R: 230, G: 230, B: 230, A: 255},
[]ui.Element{
ui.NewLabel(state.CurrentPath, 14, ui.Region{X: 0, Y: 0, W: contentWidth, H: headerHeight}, ui.AlignStart, "", nil),
ui.NewLabel(sortLabel, 12, ui.Region{X: 0, Y: 0, W: contentWidth, H: headerHeight}, ui.AlignEnd, "sort",
[]ui.Interaction{{Gesture: ui.Tap, Handler: ToggleSortOrder}}),
},
)
// --- Search bar (Gio editor) ---
searchHeight := ui.Dp(36)
searchY := headerRegion.Y + headerRegion.H + margin/2
searchRegion := ui.Region{
X: margin, Y: searchY,
W: contentWidth, H: searchHeight,
}
state.SearchEditor.SingleLine = true
searchBar := ui.NewGioEditor("search_bar", searchRegion, &state.SearchEditor)
var searchPlaceholder ui.Element
if state.SearchEditor.Len() == 0 {
searchPlaceholder = ui.NewLabel("Search…", 14,
ui.Region{X: margin + ui.Dp(8), Y: searchY + ui.Dp(8), W: contentWidth - ui.Dp(16), H: searchHeight - ui.Dp(16)},
ui.AlignStart, "", nil)
}
// --- ListView ---
listY := searchY + searchHeight + margin/2
listHeight := screenH - listY - margin
listRegion := ui.Region{
X: margin, Y: listY,
W: contentWidth, H: listHeight,
}
// Compute visible entries
visibleEntries := computeVisibleEntries(state)
// Create a scroll handler that captures the current browser state
scrollHandler := func(data any) {
delta := data.(int) // entries
HandleScroll(state, delta)
}
listView := ui.NewListView(
"browser_list",
visibleEntries,
listRegion,
ui.Dp(state.ScrollIndex)*ui.Dp(48), // scroll offset in pixels
state.SelectedIndex,
[]ui.Interaction{{Gesture: ui.Scroll, Handler: scrollHandler}},
)
elems := []ui.Element{headerBar, searchBar}
if searchPlaceholder != nil {
elems = append(elems, searchPlaceholder)
}
elems = append(elems, listView)
return elems
}
// sortModeLabel returns the display label for the given sort mode.
func sortModeLabel(mode SortMode) string {
switch mode {
case SortModeNameAsc:
return "Name ↑"
case SortModeNameDesc:
return "Name ↓"
case SortModeDateAsc:
return "Date ↑"
case SortModeDateDesc:
return "Date ↓"
default:
return "Name ↑"
}
}
// ToggleSortOrder cycles the browser sort mode through four modes.
func ToggleSortOrder(data any) {
if currentBrowserState == nil {
return
}
currentBrowserState.SortMode = (currentBrowserState.SortMode + 1) % 4
currentBrowserState.ScrollIndex = 0 // reset scroll on sort change
}
// currentBrowserState is set by the editor before calling BrowserLayout.
// Used by handlers that need access to the browser state.
var currentBrowserState *BrowserState
// SetBrowserState sets the global browser state reference for handlers.
func SetBrowserState(s *BrowserState) {
currentBrowserState = s
}
// computeVisibleEntries builds the list of ui.ListItem entries that should be
// rendered based on the current scroll position and visible count.
func computeVisibleEntries(state *BrowserState) []ui.ListItem {
if state.TotalEntries == 0 {
return nil
}
startIndex := state.ScrollIndex
endIndex := startIndex + state.VisibleCount
if endIndex > state.TotalEntries {
endIndex = state.TotalEntries
}
var items []ui.ListItem
for idx := startIndex; idx < endIndex; idx++ {
entry, ok := getEntryByIndex(state, idx)
if !ok {
// Page not loaded yet; add placeholder
items = append(items, ui.ListItem{
Text: "...",
Subtext: "Loading...",
})
continue
}
item := entry.ToListItem()
item.Selected = (idx == state.SelectedIndex)
items = append(items, item)
}
return items
}