Implement four-way sort cycling for browser listview

- Added four sort modes: Date ↓, Date ↑, Name ↑, Name ↓
- Sorting happens only when sort mode changes, not every frame
- Sorted entries are cached in SortedEntries field
- Search query changes trigger re-filtering and re-sorting
- Scroll offset resets on sort change
This commit is contained in:
Greg Pomerantz 2026-05-28 16:42:12 -04:00
parent 8f185341b3
commit 831f0dac81

View File

@ -1,7 +1,7 @@
package editor package editor
import ( import (
"fmt" "sort"
"strings" "strings"
"gioui.org/widget" "gioui.org/widget"
@ -56,8 +56,10 @@ const (
type SortMode int type SortMode int
const ( const (
SortByDateNewest SortMode = iota // default: newest modified first SortByDateDesc SortMode = iota // default: newest first
SortByDateAsc
SortByNameAsc SortByNameAsc
SortByNameDesc
) )
// State holds all application state owned by the logic goroutine. // State holds all application state owned by the logic goroutine.
@ -78,6 +80,7 @@ type State struct {
SortOrderLabel string // label text for sort order toggle SortOrderLabel string // label text for sort order toggle
SearchQuery string // current search query SearchQuery string // current search query
SearchEditor widget.Editor // Gio editor for search input SearchEditor widget.Editor // Gio editor for search input
SortedEntries []ui.ListItem // pre-sorted entries (computed only when sort mode changes)
// Editor state // Editor state
ActiveFilename string // filename shown in editor status bar ActiveFilename string // filename shown in editor status bar
} }
@ -86,8 +89,9 @@ func NewState() *State {
return &State{ return &State{
scale: 1.0, scale: 1.0,
page: EditorPage, page: EditorPage,
SortMode: SortByDateNewest, SortMode: SortByDateDesc,
SortOrderLabel: "Date", SortOrderLabel: "Date ↓",
SortedEntries: sortEntries(browserEntries, SortByDateDesc),
} }
} }
@ -109,6 +113,9 @@ func (s *State) layout() []ui.Element {
newQuery := s.SearchEditor.Text() newQuery := s.SearchEditor.Text()
if newQuery != s.SearchQuery { if newQuery != s.SearchQuery {
s.BrowserScrollOffset = 0 // reset on query change s.BrowserScrollOffset = 0 // reset on query change
// Re-sort the filtered entries
filtered := getFilteredEntriesRaw()
s.SortedEntries = sortEntries(filtered, s.SortMode)
} }
s.SearchQuery = newQuery s.SearchQuery = newQuery
} }
@ -204,17 +211,22 @@ func OpenFile(data any) {
TheState.page = EditorPage TheState.page = EditorPage
} }
// ToggleSortOrder cycles the browser sort mode between date and name. // ToggleSortOrder cycles the browser sort mode through four modes.
func ToggleSortOrder(data any) { func ToggleSortOrder(data any) {
fmt.Printf("[ToggleSortOrder] called, mode was %v\n", TheState.SortMode) TheState.SortMode = (TheState.SortMode + 1) % 4
TheState.SortMode = (TheState.SortMode + 1) % 2
switch TheState.SortMode { switch TheState.SortMode {
case SortByDateNewest: case SortByDateDesc:
TheState.SortOrderLabel = "Date" TheState.SortOrderLabel = "Date ↓"
case SortByDateAsc:
TheState.SortOrderLabel = "Date ↑"
case SortByNameAsc: case SortByNameAsc:
TheState.SortOrderLabel = "Name" TheState.SortOrderLabel = "Name ↑"
case SortByNameDesc:
TheState.SortOrderLabel = "Name ↓"
} }
fmt.Printf("[ToggleSortOrder] mode is now %v\n", TheState.SortMode) // Sort once when mode changes, not every frame
TheState.SortedEntries = sortEntries(getFilteredEntriesRaw(), TheState.SortMode)
TheState.BrowserScrollOffset = 0 // reset scroll on sort change
} }
// EditorLayout computes the element tree for the editor page. // EditorLayout computes the element tree for the editor page.
@ -301,8 +313,9 @@ func EditorLayout(screenWidth, screenHeight ui.Dp, wordWrap bool) []ui.Element {
return []ui.Element{statusBar, editor, bottomBar} return []ui.Element{statusBar, editor, bottomBar}
} }
// getFilteredEntries returns browser entries filtered by the current search query. // getFilteredEntriesRaw returns browser entries filtered by the current search query.
func getFilteredEntries() []ui.ListItem { // Does not use the SortedEntries cache.
func getFilteredEntriesRaw() []ui.ListItem {
var filtered []ui.ListItem var filtered []ui.ListItem
query := strings.ToLower(TheState.SearchQuery) query := strings.ToLower(TheState.SearchQuery)
if query == "" { if query == "" {
@ -317,6 +330,52 @@ func getFilteredEntries() []ui.ListItem {
return filtered return filtered
} }
// getFilteredEntries returns browser entries filtered by the current search query.
// If SortedEntries is non-nil (sort mode has been set), returns that instead.
func getFilteredEntries() []ui.ListItem {
if TheState.SortedEntries != nil {
return TheState.SortedEntries
}
return getFilteredEntriesRaw()
}
// sortEntries sorts the entries according to the given sort mode.
func sortEntries(entries []ui.ListItem, mode SortMode) []ui.ListItem {
// Make a copy to avoid modifying the original slice
sorted := make([]ui.ListItem, len(entries))
copy(sorted, entries)
// Parse date from subtext (format: "2025-01-15 • 4.2 KB")
getDate := func(entry ui.ListItem) string {
// Extract date part before the first space
parts := strings.Split(entry.Subtext, " ")
if len(parts) > 0 {
return parts[0]
}
return ""
}
switch mode {
case SortByDateDesc:
sort.Slice(sorted, func(i, j int) bool {
return getDate(sorted[i]) > getDate(sorted[j])
})
case SortByDateAsc:
sort.Slice(sorted, func(i, j int) bool {
return getDate(sorted[i]) < getDate(sorted[j])
})
case SortByNameAsc:
sort.Slice(sorted, func(i, j int) bool {
return strings.ToLower(sorted[i].Text) < strings.ToLower(sorted[j].Text)
})
case SortByNameDesc:
sort.Slice(sorted, func(i, j int) bool {
return strings.ToLower(sorted[i].Text) > strings.ToLower(sorted[j].Text)
})
}
return sorted
}
// browserEntries is a static list of sample files for the browser page. // browserEntries is a static list of sample files for the browser page.
// It is longer than the viewport so scrolling can be tested. // It is longer than the viewport so scrolling can be tested.
var browserEntries = []ui.ListItem{ var browserEntries = []ui.ListItem{
@ -365,10 +424,14 @@ func BrowserLayout(screenWidth, screenHeight ui.Dp, sortMode SortMode) []ui.Elem
} }
var sortLabel string var sortLabel string
switch sortMode { switch sortMode {
case SortByDateNewest: case SortByDateDesc:
sortLabel = "Date ↓" sortLabel = "Date ↓"
case SortByDateAsc:
sortLabel = "Date ↑"
case SortByNameAsc: case SortByNameAsc:
sortLabel = "Name ↑" sortLabel = "Name ↑"
case SortByNameDesc:
sortLabel = "Name ↓"
} }
headerBar := ui.NewContainer( headerBar := ui.NewContainer(
headerRegion, headerRegion,