@@ -75,30 +75,24 @@ func (l *List) Gap() int {
7575 return l.gap
7676}
7777
78−// AtBottom returns whether the list is scrolled to the bottom.
78+// AtBottom returns whether the list is showing the last item at the bottom.
7979func (l *List) AtBottom() bool {
8080 if len(l.items) == 0 {
8181 return true
8282 }
8383
84− // Calculate total height of all items from the bottom.
84+ // Calculate the height from offsetIdx to the end.
8585 var totalHeight int
86− for i := len(l.items) - 1; i >= 0; i-- {
87− item := l.getItem(i)
88− totalHeight += item.height
89− if l.gap > 0 && i < len(l.items)-1 {
90− totalHeight += l.gap
91− }
92− if totalHeight >= l.height {
93− // This is the expected bottom position.
94− expectedIdx := i
95− expectedLine := totalHeight - l.height
96− return l.offsetIdx == expectedIdx && l.offsetLine >= expectedLine
86+ for idx := l.offsetIdx; idx < len(l.items); idx++ {
87+ item := l.getItem(idx)
88+ itemHeight := item.height
89+ if l.gap > 0 && idx > l.offsetIdx {
90+ itemHeight += l.gap
9791 }
92+ totalHeight += itemHeight
9893 }
9994
100− // All items fit in viewport - we're at bottom if at top.
101− return l.offsetIdx == 0 && l.offsetLine == 0
95+ return totalHeight-l.offsetLine <= l.height
10296}
10397
10498// SetReverse shows the list in reverse order.
@@ -121,6 +115,30 @@ func (l *List) Len() int {
121115 return len(l.items)
122116}
123117
118+// lastOffsetItem returns the index and line offsets of the last item that can
119+// be partially visible in the viewport.
120+func (l *List) lastOffsetItem() (int, int, int) {
121+ var totalHeight int
122+ var idx int
123+ for idx = len(l.items) - 1; idx >= 0; idx-- {
124+ item := l.getItem(idx)
125+ itemHeight := item.height
126+ if l.gap > 0 && idx < len(l.items)-1 {
127+ itemHeight += l.gap
128+ }
129+ totalHeight += itemHeight
130+ if totalHeight > l.height {
131+ break
132+ }
133+ }
134+
135+ // Calculate line offset within the item
136+ lineOffset := max(totalHeight-l.height, 0)
137+ idx = max(idx, 0)
138+
139+ return idx, lineOffset, totalHeight
140+}
141+
124142// getItem renders (if needed) and returns the item at the given index.
125143func (l *List) getItem(idx int) renderedItem {
126144 if idx < 0 || idx >= len(l.items) {
@@ -171,44 +189,29 @@ func (l *List) ScrollBy(lines int) {
171189
172190 if lines > 0 {
173191 // Scroll down
174− // Calculate from the bottom how many lines needed to anchor the last
175− // item to the bottom
176− var totalLines int
177− var lastItemIdx int // the last item that can be partially visible
178− for i := len(l.items) - 1; i >= 0; i-- {
179− item := l.getItem(i)
180− totalLines += item.height
181− if l.gap > 0 && i < len(l.items)-1 {
182− totalLines += l.gap
183− }
184− if totalLines > l.height-1 {
185− lastItemIdx = i
186− break
187− }
188− }
189−
190− // Now scroll down by lines
191− var item renderedItem
192192 l.offsetLine += lines
193− for {
194− item = l.getItem(l.offsetIdx)
195− totalHeight := item.height
193+ currentItem := l.getItem(l.offsetIdx)
194+ for l.offsetLine >= currentItem.height {
195+ l.offsetLine -= currentItem.height
196196 if l.gap > 0 {
197− totalHeight += l.gap
198− }
199−
200− if l.offsetIdx >= lastItemIdx || l.offsetLine < totalHeight {
201− // Valid offset
202− break
197+ l.offsetLine -= l.gap
203198 }
204199
205200 // Move to next item
206− l.offsetLine -= totalHeight
207201 l.offsetIdx++
202+ if l.offsetIdx > len(l.items)-1 {
203+ // Reached bottom
204+ l.ScrollToBottom()
205+ return
206+ }
207+ currentItem = l.getItem(l.offsetIdx)
208208 }
209209
210− if l.offsetLine >= item.height {
211− l.offsetLine = item.height
210+ lastOffsetIdx, lastOffsetLine, _ := l.lastOffsetItem()
211+ if l.offsetIdx > lastOffsetIdx || (l.offsetIdx == lastOffsetIdx && l.offsetLine > lastOffsetLine) {
212+ // Clamp to bottom
213+ l.offsetIdx = lastOffsetIdx
214+ l.offsetLine = lastOffsetLine
212215 }
213216 } else if lines < 0 {
214217 // Scroll up
@@ -408,24 +411,9 @@ func (l *List) ScrollToBottom() {
408411 return
409412 }
410413
411− // Scroll to the last item
412− var totalHeight int
413− for i := len(l.items) - 1; i >= 0; i-- {
414− item := l.getItem(i)
415− totalHeight += item.height
416− if l.gap > 0 && i < len(l.items)-1 {
417− totalHeight += l.gap
418− }
419− if totalHeight >= l.height {
420− l.offsetIdx = i
421− l.offsetLine = totalHeight - l.height
422− break
423− }
424− }
425− if totalHeight < l.height {
426− // All items fit in the viewport
427− l.ScrollToTop()
428− }
414+ lastOffsetIdx, lastOffsetLine, _ := l.lastOffsetItem()
415+ l.offsetIdx = lastOffsetIdx
416+ l.offsetLine = lastOffsetLine
429417}
430418
431419// ScrollToSelected scrolls the list to the selected item.