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("history-sidebar-empty")
107
108 if query != "" {
109 label.SetText(noResultsText(query))
110 } else {
111 label.SetText("No browsing history")
112 }
113
114 label.SetWrap(false)
115 label.SetXalign(0.0)
116
117 row := gtk.NewListBoxRow()
118 if row == nil {
119 return
120 }
121 row.SetSelectable(false)
122 row.SetCanFocus(false)
123 row.SetActivatable(false)
124 row.SetChild(&label.Widget)
125 listBox.Append(&row.Widget)
126}
127
128// appendGroupHeader adds a non-selectable group header label to the list.
129func (hs *HistorySidebar) appendGroupHeader(listBox *gtk.ListBox, labelText string) {
130 label := gtk.NewLabel(&labelText)
131 if label == nil {
132 return
133 }
134 label.AddCssClass("history-sidebar-group-header")
135 label.SetXalign(0.0)
136 label.SetHexpand(true)
137
138 row := gtk.NewListBoxRow()
139 if row == nil {
140 return
141 }
142 row.SetSelectable(false)
143 row.SetCanFocus(false)
144 row.SetActivatable(false)
145 row.SetChild(&label.Widget)
146 listBox.Append(&row.Widget)
147}
148
149// appendEntryRow adds a selectable two-line entry row to the list.
150func (hs *HistorySidebar) appendEntryRow(listBox *gtk.ListBox, entry *entity.HistoryEntry) {
151 if entry == nil {
152 return
153 }
154 // Outer vertical box for two-line layout
155 rowBox := gtk.NewBox(gtk.OrientationVerticalValue, 1)
156 if rowBox == nil {
157 return
158 }
159 rowBox.SetHexpand(true)
160
161 // Title line (first line)
162 titleLabel := gtk.NewLabel(nil)
163 if titleLabel == nil {
164 return
165 }
166 titleLabel.AddCssClass("history-sidebar-row-title")
167 titleLabel.SetText(safeSidebarString(entry.Title, entry.URL))
168 titleLabel.SetXalign(0.0)
169 titleLabel.SetHexpand(true)
170 titleLabel.SetEllipsize(pango.EllipsizeEndValue)
171
172 // Subtitle line with URL and time
173 subBox := gtk.NewBox(gtk.OrientationHorizontalValue, 0)
174 if subBox == nil {
175 return
176 }
177 subBox.SetHexpand(true)
178
179 urlLabel := gtk.NewLabel(nil)
180 if urlLabel == nil {
181 return
182 }
183 urlLabel.AddCssClass("history-sidebar-row-subtitle")
184 urlLabel.SetText(readableURL(entry.URL))
185 urlLabel.SetXalign(0.0)
186 urlLabel.SetHexpand(true)
187 urlLabel.SetEllipsize(pango.EllipsizeEndValue)
188
189 timeLabel := gtk.NewLabel(nil)
190 if timeLabel == nil {
191 return
192 }
193 timeLabel.AddCssClass("history-sidebar-row-time")
194 timeLabel.SetText(relativeTimeAt(entry.LastVisited, hs.currentTime()))
195 timeLabel.SetXalign(1.0)
196
197 subBox.Append(&urlLabel.Widget)
198 subBox.Append(&timeLabel.Widget)
199
200 rowBox.Append(&titleLabel.Widget)
201 rowBox.Append(&subBox.Widget)
202
203 // Create the list box row
204 row := gtk.NewListBoxRow()
205 if row == nil {
206 return
207 }
208 row.AddCssClass("history-sidebar-row")
209 row.SetSelectable(true)
210 row.SetActivatable(true)
211 row.SetCanFocus(true)
212 row.SetFocusOnClick(true)
213 row.SetChild(&rowBox.Widget)
214
215 listBox.Append(&row.Widget)
216 hs.bindRelativeTimeLabel(timeLabel, entry.LastVisited)
217}
218
219// ensureAtLeastOneSelection selects the first selectable row if nothing is selected.
220func (hs *HistorySidebar) ensureAtLeastOneSelectionInListBox(listBox *gtk.ListBox) {
221 if listBox == nil || listBox.GetSelectedRow() != nil {
222 return
223 }
224 for i := 0; ; i++ {
225 row := listBox.GetRowAtIndex(i)
226 if row == nil {
227 break
228 }
229 if row.GetSelectable() {
230 listBox.SelectRow(row)
231 return
232 }
233 }
234}