From f48a3edc33ca62dd9d6ff5f6a226f12f659ce57a Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Tue, 2 Dec 2025 11:33:13 -0500 Subject: [PATCH] feat: implement text highlighting in list items (#1536) --- internal/ui/list/item.go | 278 ++++++++++++++++++++++++- internal/ui/list/list.go | 376 +++++++++++++++++++++++++++++++++- internal/ui/list/list_test.go | 4 +- 3 files changed, 639 insertions(+), 19 deletions(-) diff --git a/internal/ui/list/item.go b/internal/ui/list/item.go index 8e8a5cf27a022bb1af9daefd4f162dee0acd9a48..1b85cff42a18e1f12f801605b301778773510590 100644 --- a/internal/ui/list/item.go +++ b/internal/ui/list/item.go @@ -1,6 +1,7 @@ package list import ( + "image" "strings" "charm.land/lipgloss/v2" @@ -10,6 +11,50 @@ import ( "github.com/charmbracelet/ultraviolet/screen" ) +// toUVStyle converts a lipgloss.Style to a uv.Style, stripping multiline attributes. +func toUVStyle(lgStyle lipgloss.Style) uv.Style { + var uvStyle uv.Style + + // Colors are already color.Color + uvStyle.Fg = lgStyle.GetForeground() + uvStyle.Bg = lgStyle.GetBackground() + + // Build attributes using bitwise OR + var attrs uint8 + + if lgStyle.GetBold() { + attrs |= uv.AttrBold + } + + if lgStyle.GetItalic() { + attrs |= uv.AttrItalic + } + + if lgStyle.GetUnderline() { + uvStyle.Underline = uv.UnderlineSingle + } + + if lgStyle.GetStrikethrough() { + attrs |= uv.AttrStrikethrough + } + + if lgStyle.GetFaint() { + attrs |= uv.AttrFaint + } + + if lgStyle.GetBlink() { + attrs |= uv.AttrBlink + } + + if lgStyle.GetReverse() { + attrs |= uv.AttrReverse + } + + uvStyle.Attrs = attrs + + return uvStyle +} + // Item represents a list item that can draw itself to a UV buffer. // Items implement the uv.Drawable interface. type Item interface { @@ -31,6 +76,17 @@ type Focusable interface { IsFocused() bool } +// Highlightable is an optional interface for items that support highlighting. +// When implemented, items can highlight specific regions (e.g. for search matches). +type Highlightable interface { + // SetHighlight sets the highlight region (startLine, startCol) to (endLine, endCol). + // Use -1 for all values to clear highlighting. + SetHighlight(startLine, startCol, endLine, endCol int) + + // GetHighlight returns the current highlight region. + GetHighlight() (startLine, startCol, endLine, endCol int) +} + // BaseFocusable provides common focus state and styling for items. // Embed this type to add focus behavior to any item. type BaseFocusable struct { @@ -74,11 +130,148 @@ func (b *BaseFocusable) SetFocusStyles(focusStyle, blurStyle *lipgloss.Style) { b.blurStyle = blurStyle } +// BaseHighlightable provides common highlight state for items. +// Embed this type to add highlight behavior to any item. +type BaseHighlightable struct { + highlightStartLine int + highlightStartCol int + highlightEndLine int + highlightEndCol int + highlightStyle CellStyler +} + +// SetHighlight implements Highlightable interface. +func (b *BaseHighlightable) SetHighlight(startLine, startCol, endLine, endCol int) { + b.highlightStartLine = startLine + b.highlightStartCol = startCol + b.highlightEndLine = endLine + b.highlightEndCol = endCol +} + +// GetHighlight implements Highlightable interface. +func (b *BaseHighlightable) GetHighlight() (startLine, startCol, endLine, endCol int) { + return b.highlightStartLine, b.highlightStartCol, b.highlightEndLine, b.highlightEndCol +} + +// HasHighlight returns true if a highlight region is set. +func (b *BaseHighlightable) HasHighlight() bool { + return b.highlightStartLine >= 0 || b.highlightStartCol >= 0 || + b.highlightEndLine >= 0 || b.highlightEndCol >= 0 +} + +// SetHighlightStyle sets the style function used for highlighting. +func (b *BaseHighlightable) SetHighlightStyle(style CellStyler) { + b.highlightStyle = style +} + +// GetHighlightStyle returns the current highlight style function. +func (b *BaseHighlightable) GetHighlightStyle() CellStyler { + return b.highlightStyle +} + +// InitHighlight initializes the highlight fields with default values. +func (b *BaseHighlightable) InitHighlight() { + b.highlightStartLine = -1 + b.highlightStartCol = -1 + b.highlightEndLine = -1 + b.highlightEndCol = -1 + b.highlightStyle = LipglossStyleToCellStyler(lipgloss.NewStyle().Reverse(true)) +} + +// ApplyHighlight applies highlighting to a screen buffer. +// This should be called after drawing content to the buffer. +func (b *BaseHighlightable) ApplyHighlight(buf *uv.ScreenBuffer, width, height int, style *lipgloss.Style) { + if b.highlightStartLine < 0 { + return + } + + var ( + topMargin, topBorder, topPadding int + rightMargin, rightBorder, rightPadding int + bottomMargin, bottomBorder, bottomPadding int + leftMargin, leftBorder, leftPadding int + ) + if style != nil { + topMargin, rightMargin, bottomMargin, leftMargin = style.GetMargin() + topBorder, rightBorder, bottomBorder, leftBorder = style.GetBorderTopSize(), + style.GetBorderRightSize(), + style.GetBorderBottomSize(), + style.GetBorderLeftSize() + topPadding, rightPadding, bottomPadding, leftPadding = style.GetPadding() + } + + // Calculate content area offsets + contentArea := image.Rectangle{ + Min: image.Point{ + X: leftMargin + leftBorder + leftPadding, + Y: topMargin + topBorder + topPadding, + }, + Max: image.Point{ + X: width - (rightMargin + rightBorder + rightPadding), + Y: height - (bottomMargin + bottomBorder + bottomPadding), + }, + } + + for y := b.highlightStartLine; y <= b.highlightEndLine && y < height; y++ { + if y >= buf.Height() { + break + } + + line := buf.Line(y) + + // Determine column range for this line + startCol := 0 + if y == b.highlightStartLine { + startCol = min(b.highlightStartCol, len(line)) + } + + endCol := len(line) + if y == b.highlightEndLine { + endCol = min(b.highlightEndCol, len(line)) + } + + // Track last non-empty position as we go + lastContentX := -1 + + // Single pass: check content and track last non-empty position + for x := startCol; x < endCol; x++ { + cell := line.At(x) + if cell == nil { + continue + } + + // Update last content position if non-empty + if cell.Content != "" && cell.Content != " " { + lastContentX = x + } + } + + // Only apply highlight up to last content position + highlightEnd := endCol + if lastContentX >= 0 { + highlightEnd = lastContentX + 1 + } else if lastContentX == -1 { + highlightEnd = startCol // No content on this line + } + + // Apply highlight style only to cells with content + for x := startCol; x < highlightEnd; x++ { + if !image.Pt(x, y).In(contentArea) { + continue + } + cell := line.At(x) + cell.Style = b.highlightStyle(cell.Style) + } + } +} + // StringItem is a simple string-based item with optional text wrapping. // It caches rendered content by width for efficient repeated rendering. // StringItem implements Focusable if focusStyle and blurStyle are set via WithFocusStyles. +// StringItem implements Highlightable for text selection/search highlighting. type StringItem struct { BaseFocusable + BaseHighlightable id string content string // Raw content string (may contain ANSI styles) wrap bool // Whether to wrap text @@ -88,24 +281,51 @@ type StringItem struct { cache map[int]string } +// CellStyler is a function that applies styles to UV cells. +type CellStyler = func(s uv.Style) uv.Style + +var noColor = lipgloss.NoColor{} + +// LipglossStyleToCellStyler converts a Lip Gloss style to a CellStyler function. +func LipglossStyleToCellStyler(lgStyle lipgloss.Style) CellStyler { + uvStyle := toUVStyle(lgStyle) + return func(s uv.Style) uv.Style { + if uvStyle.Fg != nil && lgStyle.GetForeground() != noColor { + s.Fg = uvStyle.Fg + } + if uvStyle.Bg != nil && lgStyle.GetBackground() != noColor { + s.Bg = uvStyle.Bg + } + s.Attrs |= uvStyle.Attrs + if uvStyle.Underline != 0 { + s.Underline = uvStyle.Underline + } + return s + } +} + // NewStringItem creates a new string item with the given ID and content. func NewStringItem(id, content string) *StringItem { - return &StringItem{ + s := &StringItem{ id: id, content: content, wrap: false, cache: make(map[int]string), } + s.InitHighlight() + return s } // NewWrappingStringItem creates a new string item that wraps text to fit width. func NewWrappingStringItem(id, content string) *StringItem { - return &StringItem{ + s := &StringItem{ id: id, content: content, wrap: true, cache: make(map[int]string), } + s.InitHighlight() + return s } // WithFocusStyles sets the focus and blur styles for the string item. @@ -153,6 +373,7 @@ func (s *StringItem) Height(width int) int { // Draw implements Item and uv.Drawable. func (s *StringItem) Draw(scr uv.Screen, area uv.Rectangle) { width := area.Dx() + height := area.Dy() // Check cache first content, ok := s.cache[width] @@ -167,21 +388,41 @@ func (s *StringItem) Draw(scr uv.Screen, area uv.Rectangle) { } // Apply focus/blur styling if configured - if style := s.CurrentStyle(); style != nil { + style := s.CurrentStyle() + if style != nil { content = style.Width(width).Render(content) } - // Draw the styled string + // Create temp buffer to draw content with highlighting + tempBuf := uv.NewScreenBuffer(width, height) + + // Draw content to temp buffer first styled := uv.NewStyledString(content) - styled.Draw(scr, area) + styled.Draw(&tempBuf, uv.Rect(0, 0, width, height)) + + // Apply highlighting if active + s.ApplyHighlight(&tempBuf, width, height, style) + + // Copy temp buffer to actual screen at the target area + tempBuf.Draw(scr, area) +} + +// SetHighlight implements Highlightable and extends BaseHighlightable. +// Clears the cache when highlight changes. +func (s *StringItem) SetHighlight(startLine, startCol, endLine, endCol int) { + s.BaseHighlightable.SetHighlight(startLine, startCol, endLine, endCol) + // Clear cache when highlight changes + s.cache = make(map[int]string) } // MarkdownItem renders markdown content using Glamour. // It caches all rendered content by width for efficient repeated rendering. // The wrap width is capped at 120 cells by default to ensure readable line lengths. // MarkdownItem implements Focusable if focusStyle and blurStyle are set via WithFocusStyles. +// MarkdownItem implements Highlightable for text selection/search highlighting. type MarkdownItem struct { BaseFocusable + BaseHighlightable id string markdown string // Raw markdown content styleConfig *ansi.StyleConfig // Optional style configuration @@ -204,7 +445,7 @@ func NewMarkdownItem(id, markdown string) *MarkdownItem { maxWidth: DefaultMarkdownMaxWidth, cache: make(map[int]string), } - + m.InitHighlight() return m } @@ -248,16 +489,27 @@ func (m *MarkdownItem) Height(width int) int { // Draw implements Item and uv.Drawable. func (m *MarkdownItem) Draw(scr uv.Screen, area uv.Rectangle) { width := area.Dx() + height := area.Dy() rendered := m.renderMarkdown(width) // Apply focus/blur styling if configured - if style := m.CurrentStyle(); style != nil { + style := m.CurrentStyle() + if style != nil { rendered = style.Render(rendered) } - // Draw the rendered markdown + // Create temp buffer to draw content with highlighting + tempBuf := uv.NewScreenBuffer(width, height) + + // Draw the rendered markdown to temp buffer styled := uv.NewStyledString(rendered) - styled.Draw(scr, area) + styled.Draw(&tempBuf, uv.Rect(0, 0, width, height)) + + // Apply highlighting if active + m.ApplyHighlight(&tempBuf, width, height, style) + + // Copy temp buffer to actual screen at the target area + tempBuf.Draw(scr, area) } // renderMarkdown renders the markdown content at the given width, using cache if available. @@ -302,6 +554,14 @@ func (m *MarkdownItem) renderMarkdown(width int) string { return rendered } +// SetHighlight implements Highlightable and extends BaseHighlightable. +// Clears the cache when highlight changes. +func (m *MarkdownItem) SetHighlight(startLine, startCol, endLine, endCol int) { + m.BaseHighlightable.SetHighlight(startLine, startCol, endLine, endCol) + // Clear cache when highlight changes + m.cache = make(map[int]string) +} + // Gap is a 1-line spacer item used to add gaps between items. var Gap = NewSpacerItem("spacer-gap", 1) diff --git a/internal/ui/list/list.go b/internal/ui/list/list.go index 0b87aca1790776666a97cfb7cb76fe83933375e2..7b2c72ecfc35262bbf53c33c43fb859b5ceb3068 100644 --- a/internal/ui/list/list.go +++ b/internal/ui/list/list.go @@ -32,6 +32,15 @@ type List struct { // Viewport state offset int // Scroll offset in lines from top + // Mouse state + mouseDown bool + mouseDownItem string // Item ID where mouse was pressed + mouseDownX int // X position in item content (character offset) + mouseDownY int // Y position in item (line offset) + mouseDragItem string // Current item being dragged over + mouseDragX int // Current X in item content + mouseDragY int // Current Y in item + // Dirty tracking dirty bool dirtyItems map[string]bool @@ -362,6 +371,16 @@ func (l *List) SetSize(width, height int) { } } +// Height returns the current viewport height. +func (l *List) Height() int { + return l.height +} + +// Width returns the current viewport width. +func (l *List) Width() int { + return l.width +} + // GetSize returns the current viewport size. func (l *List) GetSize() (int, int) { return l.width, l.height @@ -569,8 +588,8 @@ func (l *List) Blur() { l.blurSelectedItem() } -// IsFocused returns whether the list is focused. -func (l *List) IsFocused() bool { +// Focused returns whether the list is focused. +func (l *List) Focused() bool { return l.focused } @@ -612,16 +631,44 @@ func (l *List) SetSelectedIndex(idx int) { l.SetSelected(l.items[idx].ID()) } -// SelectNext selects the next item in the list (wraps to beginning). +// SelectFirst selects the first item in the list. +func (l *List) SelectFirst() { + l.SetSelectedIndex(0) +} + +// SelectLast selects the last item in the list. +func (l *List) SelectLast() { + l.SetSelectedIndex(len(l.items) - 1) +} + +// SelectNextWrap selects the next item in the list (wraps to beginning). +// When the list is focused, skips non-focusable items. +func (l *List) SelectNextWrap() { + l.selectNext(true) +} + +// SelectNext selects the next item in the list (no wrap). // When the list is focused, skips non-focusable items. func (l *List) SelectNext() { + l.selectNext(false) +} + +func (l *List) selectNext(wrap bool) { if len(l.items) == 0 { return } startIdx := l.selectedIdx for i := 0; i < len(l.items); i++ { - nextIdx := (startIdx + 1 + i) % len(l.items) + var nextIdx int + if wrap { + nextIdx = (startIdx + 1 + i) % len(l.items) + } else { + nextIdx = startIdx + 1 + i + if nextIdx >= len(l.items) { + return + } + } // If list is focused and item is not focusable, skip it if l.focused { @@ -632,21 +679,38 @@ func (l *List) SelectNext() { // Select and scroll to this item l.SetSelected(l.items[nextIdx].ID()) - l.ScrollToSelected() return } } -// SelectPrev selects the previous item in the list (wraps to end). +// SelectPrevWrap selects the previous item in the list (wraps to end). +// When the list is focused, skips non-focusable items. +func (l *List) SelectPrevWrap() { + l.selectPrev(true) +} + +// SelectPrev selects the previous item in the list (no wrap). // When the list is focused, skips non-focusable items. func (l *List) SelectPrev() { + l.selectPrev(false) +} + +func (l *List) selectPrev(wrap bool) { if len(l.items) == 0 { return } startIdx := l.selectedIdx for i := 0; i < len(l.items); i++ { - prevIdx := (startIdx - 1 - i + len(l.items)) % len(l.items) + var prevIdx int + if wrap { + prevIdx = (startIdx - 1 - i + len(l.items)) % len(l.items) + } else { + prevIdx = startIdx - 1 - i + if prevIdx < 0 { + return + } + } // If list is focused and item is not focusable, skip it if l.focused { @@ -657,7 +721,6 @@ func (l *List) SelectPrev() { // Select and scroll to this item l.SetSelected(l.items[prevIdx].ID()) - l.ScrollToSelected() return } } @@ -754,6 +817,27 @@ func (l *List) TotalHeight() int { return l.totalHeight } +// SelectedItemInView returns true if the selected item is currently visible in the viewport. +func (l *List) SelectedItemInView() bool { + if l.selectedIdx < 0 || l.selectedIdx >= len(l.items) { + return false + } + + // Get selected item ID and position + item := l.items[l.selectedIdx] + pos, ok := l.itemPositions[item.ID()] + if !ok { + return false + } + + // Check if item is within viewport bounds + viewportStart := l.offset + viewportEnd := l.offset + l.height + + // Item is visible if any part of it overlaps with the viewport + return pos.startLine < viewportEnd && (pos.startLine+pos.height) > viewportStart +} + // clampOffset ensures offset is within valid bounds. func (l *List) clampOffset() { maxOffset := l.totalHeight - l.height @@ -793,3 +877,279 @@ func (l *List) blurSelectedItem() { l.dirtyItems[item.ID()] = true } } + +// HandleMouseDown handles mouse button press events. +// x and y are viewport-relative coordinates (0,0 = top-left of visible area). +// Returns true if the event was handled. +func (l *List) HandleMouseDown(x, y int) bool { + l.ensureBuilt() + + // Convert viewport y to master buffer y + bufferY := y + l.offset + + // Find which item was clicked + itemID, itemY := l.findItemAtPosition(bufferY) + if itemID == "" { + return false + } + + // Calculate x position within item content + // For now, x is just the viewport x coordinate + // Items can interpret this as character offset in their content + + l.mouseDown = true + l.mouseDownItem = itemID + l.mouseDownX = x + l.mouseDownY = itemY + l.mouseDragItem = itemID + l.mouseDragX = x + l.mouseDragY = itemY + + // Select the clicked item + if idx, ok := l.indexMap[itemID]; ok { + l.SetSelectedIndex(idx) + } + + return true +} + +// HandleMouseDrag handles mouse drag events during selection. +// x and y are viewport-relative coordinates. +// Returns true if the event was handled. +func (l *List) HandleMouseDrag(x, y int) bool { + if !l.mouseDown { + return false + } + + l.ensureBuilt() + + // Convert viewport y to master buffer y + bufferY := y + l.offset + + // Find which item we're dragging over + itemID, itemY := l.findItemAtPosition(bufferY) + if itemID == "" { + return false + } + + l.mouseDragItem = itemID + l.mouseDragX = x + l.mouseDragY = itemY + + // Update highlight if item supports it + l.updateHighlight() + + return true +} + +// HandleMouseUp handles mouse button release events. +// Returns true if the event was handled. +func (l *List) HandleMouseUp(x, y int) bool { + if !l.mouseDown { + return false + } + + l.mouseDown = false + + // Final highlight update + l.updateHighlight() + + return true +} + +// ClearHighlight clears any active text highlighting. +func (l *List) ClearHighlight() { + for _, item := range l.items { + if h, ok := item.(Highlightable); ok { + h.SetHighlight(-1, -1, -1, -1) + l.dirtyItems[item.ID()] = true + } + } +} + +// findItemAtPosition finds the item at the given master buffer y coordinate. +// Returns the item ID and the y offset within that item. +func (l *List) findItemAtPosition(bufferY int) (itemID string, itemY int) { + if bufferY < 0 || bufferY >= l.totalHeight { + return "", 0 + } + + // Linear search through items to find which one contains this y + // This could be optimized with binary search if needed + for _, item := range l.items { + pos, ok := l.itemPositions[item.ID()] + if !ok { + continue + } + + if bufferY >= pos.startLine && bufferY < pos.startLine+pos.height { + return item.ID(), bufferY - pos.startLine + } + } + + return "", 0 +} + +// updateHighlight updates the highlight range for highlightable items. +// Supports highlighting across multiple items and respects drag direction. +func (l *List) updateHighlight() { + if l.mouseDownItem == "" { + return + } + + // Get start and end item indices + downItemIdx := l.indexMap[l.mouseDownItem] + dragItemIdx := l.indexMap[l.mouseDragItem] + + // Determine selection direction + draggingDown := dragItemIdx > downItemIdx || + (dragItemIdx == downItemIdx && l.mouseDragY > l.mouseDownY) || + (dragItemIdx == downItemIdx && l.mouseDragY == l.mouseDownY && l.mouseDragX >= l.mouseDownX) + + // Determine actual start and end based on direction + var startItemIdx, endItemIdx int + var startLine, startCol, endLine, endCol int + + if draggingDown { + // Normal forward selection + startItemIdx = downItemIdx + endItemIdx = dragItemIdx + startLine = l.mouseDownY + startCol = l.mouseDownX + endLine = l.mouseDragY + endCol = l.mouseDragX + } else { + // Backward selection (dragging up) + startItemIdx = dragItemIdx + endItemIdx = downItemIdx + startLine = l.mouseDragY + startCol = l.mouseDragX + endLine = l.mouseDownY + endCol = l.mouseDownX + } + + // Clear all highlights first + for _, item := range l.items { + if h, ok := item.(Highlightable); ok { + h.SetHighlight(-1, -1, -1, -1) + l.dirtyItems[item.ID()] = true + } + } + + // Highlight all items in range + for idx := startItemIdx; idx <= endItemIdx; idx++ { + item, ok := l.items[idx].(Highlightable) + if !ok { + continue + } + + if idx == startItemIdx && idx == endItemIdx { + // Single item selection + item.SetHighlight(startLine, startCol, endLine, endCol) + } else if idx == startItemIdx { + // First item - from start position to end of item + pos := l.itemPositions[l.items[idx].ID()] + item.SetHighlight(startLine, startCol, pos.height-1, 9999) // 9999 = end of line + } else if idx == endItemIdx { + // Last item - from start of item to end position + item.SetHighlight(0, 0, endLine, endCol) + } else { + // Middle item - fully highlighted + pos := l.itemPositions[l.items[idx].ID()] + item.SetHighlight(0, 0, pos.height-1, 9999) + } + + l.dirtyItems[l.items[idx].ID()] = true + } +} + +// GetHighlightedText returns the plain text content of all highlighted regions +// across items, without any styling. Returns empty string if no highlights exist. +func (l *List) GetHighlightedText() string { + l.ensureBuilt() + + if l.masterBuffer == nil { + return "" + } + + var result strings.Builder + + // Iterate through items to find highlighted ones + for _, item := range l.items { + h, ok := item.(Highlightable) + if !ok { + continue + } + + startLine, startCol, endLine, endCol := h.GetHighlight() + if startLine < 0 { + continue + } + + pos, ok := l.itemPositions[item.ID()] + if !ok { + continue + } + + // Extract text from highlighted region in master buffer + for y := startLine; y <= endLine && y < pos.height; y++ { + bufferY := pos.startLine + y + if bufferY >= l.masterBuffer.Height() { + break + } + + line := l.masterBuffer.Line(bufferY) + + // Determine column range for this line + colStart := 0 + if y == startLine { + colStart = startCol + } + + colEnd := len(line) + if y == endLine { + colEnd = min(endCol, len(line)) + } + + // Track last non-empty position to trim trailing spaces + lastContentX := -1 + for x := colStart; x < colEnd && x < len(line); x++ { + cell := line.At(x) + if cell == nil || cell.IsZero() { + continue + } + if cell.Content != "" && cell.Content != " " { + lastContentX = x + } + } + + // Extract text from cells using String() method, up to last content + endX := colEnd + if lastContentX >= 0 { + endX = lastContentX + 1 + } + + for x := colStart; x < endX && x < len(line); x++ { + cell := line.At(x) + if cell == nil || cell.IsZero() { + continue + } + result.WriteString(cell.String()) + } + + // Add newline between lines (but not after the last line) + if y < endLine && y < pos.height-1 { + result.WriteRune('\n') + } + } + + // Add newline between items if there are more highlighted items + if result.Len() > 0 { + result.WriteRune('\n') + } + } + + // Trim trailing newline if present + text := result.String() + return strings.TrimSuffix(text, "\n") +} diff --git a/internal/ui/list/list_test.go b/internal/ui/list/list_test.go index 5ec228e698fae6b52c6c44e981348d964fbd46ca..e4f6dcff6714af0e55dc7397809235f70b386766 100644 --- a/internal/ui/list/list_test.go +++ b/internal/ui/list/list_test.go @@ -213,7 +213,7 @@ func TestListFocus(t *testing.T) { // Focus the list l.Focus() - if !l.IsFocused() { + if !l.Focused() { t.Error("expected list to be focused") } @@ -236,7 +236,7 @@ func TestListFocus(t *testing.T) { // Blur the list l.Blur() - if l.IsFocused() { + if l.Focused() { t.Error("expected list to be blurred") } }