@@ -529,6 +529,10 @@ func (l *list[T]) viewPosition() (int, int) {
}
func (l *list[T]) render() tea.Cmd {
+ return l.renderWithScrollToSelection(true)
+}
+
+func (l *list[T]) renderWithScrollToSelection(scrollToSelection bool) tea.Cmd {
if l.width <= 0 || l.height <= 0 || l.items.Len() == 0 {
return nil
}
@@ -549,8 +553,8 @@ func (l *list[T]) render() tea.Cmd {
l.rendered = l.renderVirtualScrolling()
l.renderMu.Unlock()
- // Scroll to selected item if focused
- if l.focused {
+ // Scroll to selected item if focused and requested
+ if l.focused && scrollToSelection {
l.scrollToSelection()
}
@@ -677,12 +681,12 @@ func (l *list[T]) changeSelectionWhenScrolling() tea.Cmd {
// If the item is bigger than the viewport, select it
if renderedItem.start <= start && renderedItem.end >= end {
l.selectedItem = item.ID()
- return l.render()
+ return l.renderWithScrollToSelection(false)
}
// item is in the view
if renderedItem.start >= start && renderedItem.start <= end {
l.selectedItem = item.ID()
- return l.render()
+ return l.renderWithScrollToSelection(false)
}
}
} else if itemMiddle > end {
@@ -705,12 +709,12 @@ func (l *list[T]) changeSelectionWhenScrolling() tea.Cmd {
// If the item is bigger than the viewport, select it
if renderedItem.start <= start && renderedItem.end >= end {
l.selectedItem = item.ID()
- return l.render()
+ return l.renderWithScrollToSelection(false)
}
// item is in the view
if renderedItem.end >= start && renderedItem.end <= end {
l.selectedItem = item.ID()
- return l.render()
+ return l.renderWithScrollToSelection(false)
}
}
}
@@ -912,24 +916,10 @@ func (l *list[T]) renderVirtualScrolling() string {
// Calculate viewport bounds
viewStart, viewEnd := l.viewPosition()
- // Debug: Check if viewport is valid
- if viewEnd < viewStart {
- // Return empty viewport
- var lines []string
- for i := 0; i < l.height; i++ {
- lines = append(lines, "")
- }
- return strings.Join(lines, "\n")
- }
-
// Check if we have any positions calculated
if len(l.itemPositions) == 0 {
- // No items have been calculated yet, return empty
- var lines []string
- for i := 0; i < l.height; i++ {
- lines = append(lines, "")
- }
- return strings.Join(lines, "\n")
+ // No positions calculated yet, return empty viewport
+ return ""
}
// Find which items are visible
@@ -941,13 +931,7 @@ func (l *list[T]) renderVirtualScrolling() string {
itemsLen := l.items.Len()
for i := 0; i < itemsLen; i++ {
- item, ok := l.items.Get(i)
- if !ok {
- continue
- }
-
if i >= len(l.itemPositions) {
- // Item not yet calculated, skip it
continue
}
@@ -955,6 +939,10 @@ func (l *list[T]) renderVirtualScrolling() string {
// Check if item is visible (overlaps with viewport)
if pos.end >= viewStart && pos.start <= viewEnd {
+ item, ok := l.items.Get(i)
+ if !ok {
+ continue
+ }
visibleItems = append(visibleItems, struct {
item T
pos itemPosition
@@ -968,106 +956,60 @@ func (l *list[T]) renderVirtualScrolling() string {
}
}
- if len(visibleItems) == 0 {
- // No visible items found - this shouldn't happen if viewport is valid
- // Return empty lines to maintain height
- var lines []string
- for i := 0; i < l.height; i++ {
- lines = append(lines, "")
- }
- return strings.Join(lines, "\n")
- }
-
- // Render visible items
- var b strings.Builder
+ // Build the rendered output
+ var lines []string
currentLine := viewStart
- // Handle first visible item
- firstVisible := visibleItems[0]
- if firstVisible.pos.start < viewStart {
- // We're starting mid-item, render partial
- if cached, ok := l.viewCache.Get(firstVisible.item.ID()); ok && cached != "" {
- lines := strings.Split(cached, "\n")
- skipLines := viewStart - firstVisible.pos.start
- if skipLines >= 0 && skipLines < len(lines) {
- for i := skipLines; i < len(lines) && currentLine <= viewEnd; i++ {
- if b.Len() > 0 {
- b.WriteByte('\n')
- }
- b.WriteString(lines[i])
- currentLine++
- }
- }
- }
- } else if firstVisible.pos.start > viewStart {
- // Add empty lines before first item
- for currentLine < firstVisible.pos.start && currentLine <= viewEnd {
- if b.Len() > 0 {
- b.WriteByte('\n')
- }
- currentLine++
- }
- }
-
- // Render fully visible items
- for i, vis := range visibleItems {
- if currentLine > viewEnd {
- break
- }
-
- // Skip first item if we already rendered it partially
- if i == 0 && firstVisible.pos.start < viewStart {
- // Update currentLine to where we left off after partial rendering
- currentLine = viewStart + (firstVisible.pos.end - firstVisible.pos.start + 1) - (viewStart - firstVisible.pos.start)
- continue
- }
-
- // Add gap before item (except for first visible item in viewport)
- if i > 0 || (i == 0 && firstVisible.pos.start >= viewStart) {
- // Only add gap if this isn't the very first item in the viewport
- if currentLine > viewStart && currentLine <= viewEnd {
- for j := 0; j < l.gap && currentLine <= viewEnd; j++ {
- if b.Len() > 0 {
- b.WriteByte('\n')
- }
- currentLine++
- }
- }
- }
-
- // Render item or use cache
+ for _, vis := range visibleItems {
+ // Get or render the item's view
var view string
- if cached, ok := l.viewCache.Get(vis.item.ID()); ok && cached != "" {
+ if cached, ok := l.viewCache.Get(vis.item.ID()); ok {
view = cached
} else {
view = vis.item.View()
- // Update cache
l.viewCache.Set(vis.item.ID(), view)
}
- // Handle partial rendering if item extends beyond viewport
- lines := strings.Split(view, "\n")
- for _, line := range lines {
- if currentLine > viewEnd {
- break
- }
- if b.Len() > 0 {
- b.WriteByte('\n')
+ itemLines := strings.Split(view, "\n")
+
+ // Add gap lines before item if needed (except for first item)
+ if vis.index > 0 && currentLine < vis.pos.start {
+ gapLines := vis.pos.start - currentLine
+ for i := 0; i < gapLines; i++ {
+ lines = append(lines, "")
+ currentLine++
}
- b.WriteString(line)
+ }
+
+ // Determine which lines of this item to include
+ startLine := 0
+ if vis.pos.start < viewStart {
+ // Item starts before viewport, skip some lines
+ startLine = viewStart - vis.pos.start
+ }
+
+ // Add the item's visible lines
+ for i := startLine; i < len(itemLines) && currentLine <= viewEnd; i++ {
+ lines = append(lines, itemLines[i])
currentLine++
}
}
- // Fill remaining viewport with empty lines if needed
- for currentLine <= viewEnd {
- if b.Len() > 0 {
- b.WriteByte('\n')
+ // For content that fits entirely in viewport, don't pad with empty lines
+ // Only pad if we have scrolled or if content is larger than viewport
+ if l.virtualHeight > l.height || l.offset > 0 {
+ // Fill remaining viewport with empty lines if needed
+ for len(lines) < l.height {
+ lines = append(lines, "")
+ }
+
+ // Trim to viewport height
+ if len(lines) > l.height {
+ lines = lines[:l.height]
}
- currentLine++
}
- return b.String()
+ return strings.Join(lines, "\n")
}
@@ -1307,27 +1249,38 @@ func (l *list[T]) PrependItem(item T) tea.Cmd {
if l.width > 0 && l.height > 0 {
cmds = append(cmds, item.SetSize(l.width, l.height))
}
- cmds = append(cmds, l.render())
+
+ // Recalculate positions after prepending
+ l.calculateItemPositions()
+
if l.direction == DirectionForward {
if l.offset == 0 {
+ // If we're at the top, stay at the top
+ cmds = append(cmds, l.render())
cmd := l.GoToTop()
if cmd != nil {
cmds = append(cmds, cmd)
}
} else {
- // Get the new item's position to adjust offset
- newInx := l.items.Len() - 1
- if newInx < len(l.itemPositions) {
- newItem := l.itemPositions[newInx]
+ // Adjust offset to maintain viewport position
+ // The prepended item is at index 0
+ if len(l.itemPositions) > 0 {
+ newItem := l.itemPositions[0]
newLines := newItem.height
if l.items.Len() > 1 {
newLines += l.gap
}
+ // Increase offset to keep the same content visible
if l.virtualHeight > 0 {
- l.offset = min(l.virtualHeight-1, l.offset+newLines)
+ l.offset = min(l.virtualHeight-l.height, l.offset+newLines)
}
}
+ cmds = append(cmds, l.renderWithScrollToSelection(false))
}
+ } else {
+ // For backward direction, prepending doesn't affect the offset
+ // since offset is from the bottom
+ cmds = append(cmds, l.render())
}
return tea.Batch(cmds...)
}
@@ -1460,22 +1413,13 @@ func (l *list[T]) SetSize(width int, height int) tea.Cmd {
func (l *list[T]) UpdateItem(id string, item T) tea.Cmd {
var cmds []tea.Cmd
if inx, ok := l.indexMap.Get(id); ok {
- // Store old height if we have it
- var oldHeight int
+ // Store old item position info before update
+ var oldItemPos itemPosition
hasOldItem := false
if inx < len(l.itemPositions) {
- oldHeight = l.itemPositions[inx].height
+ oldItemPos = l.itemPositions[inx]
hasOldItem = true
}
-
- oldPosition := l.offset
- if l.direction == DirectionBackward {
- if l.virtualHeight > 0 {
- oldPosition = (l.virtualHeight - 1) - l.offset
- } else {
- oldPosition = 0
- }
- }
// Update the item
l.items.Set(inx, item)
@@ -1483,32 +1427,50 @@ func (l *list[T]) UpdateItem(id string, item T) tea.Cmd {
// Clear cache for this item
l.viewCache.Del(id)
- cmd := l.render()
-
- // need to check for nil because of sequence not handling nil
- if cmd != nil {
- cmds = append(cmds, cmd)
- }
+ // Recalculate positions to get new height
+ l.calculateItemPositions()
- // Adjust offset if needed based on height change
+ // Adjust offset if item height changed and it's outside the viewport
if hasOldItem && inx < len(l.itemPositions) {
- newHeight := l.itemPositions[inx].height
- diff := newHeight - oldHeight
+ newItemPos := l.itemPositions[inx]
+ heightDiff := newItemPos.height - oldItemPos.height
- if l.direction == DirectionBackward {
- // if we are the last item and there is no offset
- // make sure to go to the bottom
- if oldPosition < l.itemPositions[inx].end {
- if diff != 0 && l.virtualHeight > 0 {
- l.offset = util.Clamp(l.offset+diff, 0, l.virtualHeight-1)
+ if heightDiff != 0 {
+ // Get current viewport position
+ viewStart, viewEnd := l.viewPosition()
+
+ if l.direction == DirectionForward {
+ // Item is above viewport if its end is before viewport start
+ if oldItemPos.end < viewStart {
+ // Adjust offset to maintain viewport content
+ l.offset = max(0, l.offset + heightDiff)
+ }
+ } else {
+ // For backward direction:
+ // Check if item is outside the current viewport
+ // Item is completely below viewport if its start is after viewport end
+ if oldItemPos.start > viewEnd {
+ // Item below viewport increased height, increase offset to maintain view
+ l.offset = max(0, l.offset + heightDiff)
+ } else if oldItemPos.end < viewStart {
+ // Item is completely above viewport
+ // No offset adjustment needed for items above in backward direction
+ // because they don't affect the view from bottom
}
- }
- } else if hasOldItem && l.offset > l.itemPositions[inx].start {
- if diff != 0 && l.virtualHeight > 0 {
- l.offset = util.Clamp(l.offset+diff, 0, l.virtualHeight-1)
}
}
}
+
+ // Re-render with updated positions and offset
+ cmd := l.renderWithScrollToSelection(false)
+ if cmd != nil {
+ cmds = append(cmds, cmd)
+ }
+
+ cmds = append(cmds, item.Init())
+ if l.width > 0 && l.height > 0 {
+ cmds = append(cmds, item.SetSize(l.width, l.height))
+ }
}
return tea.Sequence(cmds...)
}