@@ -17,7 +17,7 @@ import (
// ListItem represents a selectable and searchable item in a dialog list.
type ListItem interface {
list.FilterableItem
- list.FocusStylable
+ list.Focusable
list.MatchSettable
// ID returns the unique identifier of the item.
@@ -27,8 +27,10 @@ type ListItem interface {
// SessionItem wraps a [session.Session] to implement the [ListItem] interface.
type SessionItem struct {
session.Session
- t *styles.Styles
- m fuzzy.Match
+ t *styles.Styles
+ m fuzzy.Match
+ cache map[int]string
+ focused bool
}
var _ ListItem = &SessionItem{}
@@ -45,13 +47,34 @@ func (s *SessionItem) ID() string {
// SetMatch sets the fuzzy match for the session item.
func (s *SessionItem) SetMatch(m fuzzy.Match) {
+ s.cache = nil
s.m = m
}
// Render returns the string representation of the session item.
func (s *SessionItem) Render(width int) string {
+ if s.cache == nil {
+ s.cache = make(map[int]string)
+ }
+
+ cached, ok := s.cache[width]
+ if ok {
+ return cached
+ }
+
+ style := s.t.Dialog.NormalItem
+ if s.focused {
+ style = s.t.Dialog.SelectedItem
+ }
+
+ width -= style.GetHorizontalFrameSize()
age := humanize.Time(time.Unix(s.Session.UpdatedAt, 0))
- age = s.t.Subtle.Render(age)
+ if s.focused {
+ age = s.t.Base.Render(age)
+ } else {
+ age = s.t.Subtle.Render(age)
+ }
+
age = " " + age
ageLen := lipgloss.Width(age)
title := s.Session.Title
@@ -59,12 +82,10 @@ func (s *SessionItem) Render(width int) string {
title = ansi.Truncate(title, max(0, width-ageLen), "…")
right := lipgloss.NewStyle().AlignHorizontal(lipgloss.Right).Width(width - titleLen).Render(age)
+ content := title
if matches := len(s.m.MatchedIndexes); matches > 0 {
var lastPos int
parts := make([]string, 0)
- // TODO: Use [ansi.Style].Underline true/false to underline only the
- // matched parts instead of using [lipgloss.StyleRanges] since it can
- // be cheaper with less allocations.
ranges := matchedRanges(s.m.MatchedIndexes)
for _, rng := range ranges {
start, stop := bytePosToVisibleCharPos(title, rng)
@@ -86,19 +107,21 @@ func (s *SessionItem) Render(width int) string {
if lastPos < len(title) {
parts = append(parts, title[lastPos:])
}
- return strings.Join(parts, "") + right
+
+ content = strings.Join(parts, "")
}
- return title + right
-}
-// FocusStyle returns the style to be applied when the item is focused.
-func (s *SessionItem) FocusStyle() lipgloss.Style {
- return s.t.Dialog.SelectedItem
+ content = style.Render(content + right)
+ s.cache[width] = content
+ return content
}
-// BlurStyle returns the style to be applied when the item is blurred.
-func (s *SessionItem) BlurStyle() lipgloss.Style {
- return s.t.Dialog.NormalItem
+// SetFocused sets the focus state of the session item.
+func (s *SessionItem) SetFocused(focused bool) {
+ if s.focused != focused {
+ s.cache = nil
+ }
+ s.focused = focused
}
// sessionItems takes a slice of [session.Session]s and convert them to a slice
@@ -1,10 +1,8 @@
package list
import (
- "image"
"strings"
- "charm.land/lipgloss/v2"
"github.com/charmbracelet/x/ansi"
)
@@ -35,9 +33,6 @@ type List struct {
mouseDragY int // Current Y in item
lastHighlighted map[int]bool // Track which items were highlighted in last update
- // Rendered content and cache
- renderedItems map[int]renderedItem
-
// offsetIdx is the index of the first visible item in the viewport.
offsetIdx int
// offsetLine is the number of lines of the item at offsetIdx that are
@@ -56,7 +51,6 @@ type renderedItem struct {
func NewList(items ...Item) *List {
l := new(List)
l.items = items
- l.renderedItems = make(map[int]renderedItem)
l.selectedIdx = -1
l.mouseDownItem = -1
l.mouseDragItem = -1
@@ -66,9 +60,6 @@ func NewList(items ...Item) *List {
// SetSize sets the size of the list viewport.
func (l *List) SetSize(width, height int) {
- if width != l.width {
- l.renderedItems = make(map[int]renderedItem)
- }
l.width = width
l.height = height
// l.normalizeOffsets()
@@ -96,16 +87,16 @@ func (l *List) Len() int {
// getItem renders (if needed) and returns the item at the given index.
func (l *List) getItem(idx int) renderedItem {
- return l.renderItem(idx, false)
-}
+ if idx < 0 || idx >= len(l.items) {
+ return renderedItem{}
+ }
-// applyHighlight applies highlighting to the given rendered item.
-func (l *List) applyHighlight(idx int, ri *renderedItem) {
- // Apply highlight if item supports it
- if highlightable, ok := l.items[idx].(HighlightStylable); ok {
+ item := l.items[idx]
+ if hi, ok := item.(Highlightable); ok {
+ // Apply highlight
startItemIdx, startLine, startCol, endItemIdx, endLine, endCol := l.getHighlightRange()
+ sLine, sCol, eLine, eCol := -1, -1, -1, -1
if idx >= startItemIdx && idx <= endItemIdx {
- var sLine, sCol, eLine, eCol int
if idx == startItemIdx && idx == endItemIdx {
// Single item selection
sLine = startLine
@@ -116,8 +107,8 @@ func (l *List) applyHighlight(idx int, ri *renderedItem) {
// First item - from start position to end of item
sLine = startLine
sCol = startCol
- eLine = ri.height - 1
- eCol = 9999 // 9999 = end of line
+ eLine = -1
+ eCol = -1
} else if idx == endItemIdx {
// Last item - from start of item to end position
sLine = 0
@@ -128,82 +119,29 @@ func (l *List) applyHighlight(idx int, ri *renderedItem) {
// Middle item - fully highlighted
sLine = 0
sCol = 0
- eLine = ri.height - 1
- eCol = 9999
+ eLine = -1
+ eCol = -1
}
-
- // Apply offset for styling frame
- contentArea := image.Rect(0, 0, l.width, ri.height)
-
- hiStyle := highlightable.HighlightStyle()
- rendered := Highlight(ri.content, contentArea, sLine, sCol, eLine, eCol, ToHighlighter(hiStyle))
- ri.content = rendered
}
- }
-}
-// renderItem renders (if needed) and returns the item at the given index. If
-// process is true, it applies focus and highlight styling.
-func (l *List) renderItem(idx int, process bool) renderedItem {
- if idx < 0 || idx >= len(l.items) {
- return renderedItem{}
+ hi.Highlight(sLine, sCol, eLine, eCol)
}
- var style lipgloss.Style
- focusable, isFocusable := l.items[idx].(FocusStylable)
- if isFocusable {
- style = focusable.BlurStyle()
- if l.focused && idx == l.selectedIdx {
- style = focusable.FocusStyle()
- }
+ if focusable, isFocusable := item.(Focusable); isFocusable {
+ focusable.SetFocused(l.focused && idx == l.selectedIdx)
}
- ri, ok := l.renderedItems[idx]
- if !ok {
- item := l.items[idx]
- rendered := item.Render(l.width - style.GetHorizontalFrameSize())
- rendered = strings.TrimRight(rendered, "\n")
- height := countLines(rendered)
-
- ri = renderedItem{
- content: rendered,
- height: height,
- }
-
- l.renderedItems[idx] = ri
- }
-
- if !process {
- // Simply return cached rendered item with frame size applied
- if vfs := style.GetVerticalFrameSize(); vfs > 0 {
- ri.height += vfs
- }
- return ri
- }
-
- // We apply highlighting before focus styling so that focus styling
- // overrides highlight styles.
- if l.mouseDownItem >= 0 {
- l.applyHighlight(idx, &ri)
- }
-
- if isFocusable {
- // Apply focus/blur styling if needed
- rendered := style.Render(ri.content)
- height := countLines(rendered)
- ri.content = rendered
- ri.height = height
+ rendered := item.Render(l.width)
+ rendered = strings.TrimRight(rendered, "\n")
+ height := countLines(rendered)
+ ri := renderedItem{
+ content: rendered,
+ height: height,
}
return ri
}
-// invalidateItem invalidates the cached rendered content of the item at the
-// given index.
-func (l *List) invalidateItem(idx int) {
- delete(l.renderedItems, idx)
-}
-
// ScrollToIndex scrolls the list to the given item index.
func (l *List) ScrollToIndex(index int) {
if index < 0 {
@@ -330,7 +268,7 @@ func (l *List) Render() string {
linesNeeded := l.height
for linesNeeded > 0 && currentIdx < len(l.items) {
- item := l.renderItem(currentIdx, true)
+ item := l.getItem(currentIdx)
itemLines := strings.Split(item.content, "\n")
itemHeight := len(itemLines)
@@ -372,13 +310,6 @@ func (l *List) Render() string {
func (l *List) PrependItems(items ...Item) {
l.items = append(items, l.items...)
- // Shift cache
- newCache := make(map[int]renderedItem)
- for idx, val := range l.renderedItems {
- newCache[idx+len(items)] = val
- }
- l.renderedItems = newCache
-
// Keep view position relative to the content that was visible
l.offsetIdx += len(items)
@@ -397,9 +328,6 @@ func (l *List) SetItems(items ...Item) {
// rendered item cache.
func (l *List) setItems(evict bool, items ...Item) {
l.items = items
- if evict {
- l.renderedItems = make(map[int]renderedItem)
- }
l.selectedIdx = min(l.selectedIdx, len(l.items)-1)
l.offsetIdx = min(l.offsetIdx, len(l.items)-1)
l.offsetLine = 0
@@ -580,8 +508,6 @@ func (l *List) HandleMouseDown(x, y int) bool {
if clickable, ok := l.items[itemIdx].(MouseClickable); ok {
clickable.HandleMouseClick(ansi.MouseButton1, x, itemY)
- l.items[itemIdx] = clickable.(Item)
- l.invalidateItem(itemIdx)
}
return true