From 831f0dac8146564a1ed4e57c4db3c267a2d9b5a8 Mon Sep 17 00:00:00 2001 From: Greg Pomerantz Date: Thu, 28 May 2026 16:42:12 -0400 Subject: [PATCH] Implement four-way sort cycling for browser listview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- internal/editor/state.go | 91 +++++++++++++++++++++++++++++++++------- 1 file changed, 77 insertions(+), 14 deletions(-) diff --git a/internal/editor/state.go b/internal/editor/state.go index 98f84a6..031c047 100644 --- a/internal/editor/state.go +++ b/internal/editor/state.go @@ -1,7 +1,7 @@ package editor import ( - "fmt" + "sort" "strings" "gioui.org/widget" @@ -56,8 +56,10 @@ const ( type SortMode int const ( - SortByDateNewest SortMode = iota // default: newest modified first + SortByDateDesc SortMode = iota // default: newest first + SortByDateAsc SortByNameAsc + SortByNameDesc ) // 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 SearchQuery string // current search query SearchEditor widget.Editor // Gio editor for search input + SortedEntries []ui.ListItem // pre-sorted entries (computed only when sort mode changes) // Editor state ActiveFilename string // filename shown in editor status bar } @@ -86,8 +89,9 @@ func NewState() *State { return &State{ scale: 1.0, page: EditorPage, - SortMode: SortByDateNewest, - SortOrderLabel: "Date", + SortMode: SortByDateDesc, + SortOrderLabel: "Date ↓", + SortedEntries: sortEntries(browserEntries, SortByDateDesc), } } @@ -109,6 +113,9 @@ func (s *State) layout() []ui.Element { newQuery := s.SearchEditor.Text() if newQuery != s.SearchQuery { s.BrowserScrollOffset = 0 // reset on query change + // Re-sort the filtered entries + filtered := getFilteredEntriesRaw() + s.SortedEntries = sortEntries(filtered, s.SortMode) } s.SearchQuery = newQuery } @@ -204,17 +211,22 @@ func OpenFile(data any) { 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) { - fmt.Printf("[ToggleSortOrder] called, mode was %v\n", TheState.SortMode) - TheState.SortMode = (TheState.SortMode + 1) % 2 + TheState.SortMode = (TheState.SortMode + 1) % 4 switch TheState.SortMode { - case SortByDateNewest: - TheState.SortOrderLabel = "Date" + case SortByDateDesc: + TheState.SortOrderLabel = "Date ↓" + case SortByDateAsc: + TheState.SortOrderLabel = "Date ↑" 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. @@ -301,8 +313,9 @@ func EditorLayout(screenWidth, screenHeight ui.Dp, wordWrap bool) []ui.Element { return []ui.Element{statusBar, editor, bottomBar} } -// getFilteredEntries returns browser entries filtered by the current search query. -func getFilteredEntries() []ui.ListItem { +// getFilteredEntriesRaw returns browser entries filtered by the current search query. +// Does not use the SortedEntries cache. +func getFilteredEntriesRaw() []ui.ListItem { var filtered []ui.ListItem query := strings.ToLower(TheState.SearchQuery) if query == "" { @@ -317,6 +330,52 @@ func getFilteredEntries() []ui.ListItem { 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. // It is longer than the viewport so scrolling can be tested. var browserEntries = []ui.ListItem{ @@ -365,10 +424,14 @@ func BrowserLayout(screenWidth, screenHeight ui.Dp, sortMode SortMode) []ui.Elem } var sortLabel string switch sortMode { - case SortByDateNewest: + case SortByDateDesc: sortLabel = "Date ↓" + case SortByDateAsc: + sortLabel = "Date ↑" case SortByNameAsc: sortLabel = "Name ↑" + case SortByNameDesc: + sortLabel = "Name ↓" } headerBar := ui.NewContainer( headerRegion,