history_sidebar_rendering.go RawHistoryFolder
1package component
2
3import (
4 "github.com/bnema/puregotk/v4/gtk"
5 "github.com/bnema/puregotk/v4/pango"
6
7 "github.com/bnema/dumber/internal/domain/entity"
8)
9
10// =============================================================================
11// List rendering
12// =============================================================================
13
14// rebuildList clears and repopulates the list box from current groups.
15// Must be called on the GTK main thread. Preserves scroll and selection.
16func (hs *HistorySidebar) rebuildList() {
17 hs.mu.Lock()
18 if hs.destroyed || hs.listBox == nil {
19 hs.mu.Unlock()
20 return
21 }
22 listBox := hs.listBox
23 rows := append([]historyDisplayRow(nil), hs.displayRows...)
24 query := hs.currentQuery
25 hasSearchResults := hs.searchResults != nil
26 searchDone := hs.searchDone
27 totalLoaded := hs.totalLoaded
28 isLoading := hs.isLoading
29 hs.clearRelativeTimeBindingsLocked()
30 hs.mu.Unlock()
31
32 // Remove all rows
33 listBox.RemoveAll()
34
35 if len(rows) == 0 {
36 if !hasSearchResults && totalLoaded == 0 {
37 // Browse has not loaded yet AND no search has completed.
38 hs.showLoadingOrEmpty(listBox, query, searchDone, isLoading)
39 return
40 }
41 // Search completed with 0 results, or browse loaded but empty (no history).
42 hs.showEmptyState(listBox, query)
43 hs.restoreScrollAndSelectionInListBox(listBox)
44 return
45 }
46
47 for _, row := range rows {
48 switch row.Kind {
49 case historyDisplayRowHeader:
50 hs.appendGroupHeader(listBox, row.Label)
51 case historyDisplayRowEntry:
52 hs.appendEntryRow(listBox, row.Entry)
53 }
54 }
55
56 listBox.Show()
57
58 // Restore previous scroll position and selection
59 hs.restoreScrollAndSelectionInListBox(listBox)
60
61 // If no selection was restored and this is the first load, select first entry
62 hs.ensureAtLeastOneSelectionInListBox(listBox)
63}
64
65func (hs *HistorySidebar) showLoadingOrEmpty(listBox *gtk.ListBox, query string, searchDone, isLoading bool) {
66 label := gtk.NewLabel(nil)
67 if label == nil {
68 return
69 }
70 label.AddCssClass("history-sidebar-loading")
71
72 switch {
73 case isLoading && query == "":
74 label.SetText("Loading history...")
75 case query != "" && !searchDone:
76 label.SetText("Searching...")
77 case query != "":
78 label.SetText(noResultsText(query))
79 default:
80 label.SetText("No browsing history")
81 }
82
83 label.SetWrap(false)
84 label.SetXalign(0.0)
85
86 row := gtk.NewListBoxRow()
87 if row == nil {
88 return
89 }
90 row.SetSelectable(false)
91 row.SetCanFocus(false)
92 row.SetActivatable(false)
93 row.SetChild(&label.Widget)
94 listBox.Append(&row.Widget)
95}
96
97func noResultsText(query string) string {
98 return "No results for \"" + query + "\""
99}
100
101func (hs *HistorySidebar) showEmptyState(listBox *gtk.ListBox, query string) {
102 label := gtk.NewLabel(nil)
103 if label == nil {
104 return
105 }
106 label.AddCssClass("sidebar-empty")
107 label.AddCssClass("history-sidebar-empty")
108
109 if query != "" {
110 label.SetText(noResultsText(query))
111 } else {
112 label.SetText("No browsing history")
113 }
114
115 label.SetWrap(false)
116 label.SetXalign(0.0)
117
118 row := gtk.NewListBoxRow()
119 if row == nil {
120 return
121 }
122 row.SetSelectable(false)
123 row.SetCanFocus(false)
124 row.SetActivatable(false)
125 row.SetChild(&label.Widget)
126 listBox.Append(&row.Widget)
127}
128
129// appendGroupHeader adds a non-selectable group header label to the list.
130func (hs *HistorySidebar) appendGroupHeader(listBox *gtk.ListBox, labelText string) {
131 label := gtk.NewLabel(&labelText)
132 if label == nil {
133 return
134 }
135 label.AddCssClass("history-sidebar-group-header")
136 label.SetXalign(0.0)
137 label.SetHexpand(true)
138
139 row := gtk.NewListBoxRow()
140 if row == nil {
141 return
142 }
143 row.SetSelectable(false)
144 row.SetCanFocus(false)
145 row.SetActivatable(false)
146 row.SetChild(&label.Widget)
147 listBox.Append(&row.Widget)
148}
149
150// appendEntryRow adds a selectable two-line entry row to the list.
151func (hs *HistorySidebar) appendEntryRow(listBox *gtk.ListBox, entry *entity.HistoryEntry) {
152 if entry == nil {
153 return
154 }
155 // Outer vertical box for two-line layout
156 rowBox := gtk.NewBox(gtk.OrientationVerticalValue, 1)
157 if rowBox == nil {
158 return
159 }
160 rowBox.SetHexpand(true)
161
162 // Title line (first line)
163 titleLabel := gtk.NewLabel(nil)
164 if titleLabel == nil {
165 return
166 }
167 titleLabel.AddCssClass("sidebar-row-title")
168 titleLabel.AddCssClass("history-sidebar-row-title")
169 titleLabel.SetText(safeSidebarString(entry.Title, entry.URL))
170 titleLabel.SetXalign(0.0)
171 titleLabel.SetHexpand(true)
172 titleLabel.SetEllipsize(pango.EllipsizeEndValue)
173
174 // Subtitle line with URL and time
175 subBox := gtk.NewBox(gtk.OrientationHorizontalValue, 0)
176 if subBox == nil {
177 return
178 }
179 subBox.SetHexpand(true)
180
181 urlLabel := gtk.NewLabel(nil)
182 if urlLabel == nil {
183 return
184 }
185 urlLabel.AddCssClass("sidebar-row-subtitle")
186 urlLabel.AddCssClass("history-sidebar-row-subtitle")
187 urlLabel.SetText(readableURL(entry.URL))
188 urlLabel.SetXalign(0.0)
189 urlLabel.SetHexpand(true)
190 urlLabel.SetEllipsize(pango.EllipsizeEndValue)
191
192 timeLabel := gtk.NewLabel(nil)
193 if timeLabel == nil {
194 return
195 }
196 timeLabel.AddCssClass("history-sidebar-row-time")
197 timeLabel.SetText(relativeTimeAt(entry.LastVisited, hs.currentTime()))
198 timeLabel.SetXalign(1.0)
199
200 subBox.Append(&urlLabel.Widget)
201 subBox.Append(&timeLabel.Widget)
202
203 rowBox.Append(&titleLabel.Widget)
204 rowBox.Append(&subBox.Widget)
205
206 // Create the list box row
207 row := gtk.NewListBoxRow()
208 if row == nil {
209 return
210 }
211 row.AddCssClass("sidebar-row")
212 row.AddCssClass("history-sidebar-row")
213 row.SetSelectable(true)
214 row.SetActivatable(true)
215 row.SetCanFocus(true)
216 row.SetFocusOnClick(true)
217 row.SetChild(&rowBox.Widget)
218
219 listBox.Append(&row.Widget)
220 hs.bindRelativeTimeLabel(timeLabel, entry.LastVisited)
221}
222
223// ensureAtLeastOneSelection selects the first selectable row if nothing is selected.
224func (hs *HistorySidebar) ensureAtLeastOneSelectionInListBox(listBox *gtk.ListBox) {
225 if listBox == nil || listBox.GetSelectedRow() != nil {
226 return
227 }
228 for i := 0; ; i++ {
229 row := listBox.GetRowAtIndex(i)
230 if row == nil {
231 break
232 }
233 if row.GetSelectable() {
234 listBox.SelectRow(row)
235 return
236 }
237 }
238}