diff --git a/checkbox.go b/checkbox.go index c494088..1537a72 100644 --- a/checkbox.go +++ b/checkbox.go @@ -189,7 +189,7 @@ func (c *Checkbox) Draw(screen tcell.Screen) { if !c.checked { checkedString = strings.Repeat(" ", checkboxWidth) } - printWithStyle(screen, checkedString, x, y, checkboxWidth, AlignLeft, fieldStyle) + printWithStyle(screen, checkedString, x, y, 0, checkboxWidth, AlignLeft, fieldStyle) } // InputHandler returns the handler for this primitive. diff --git a/list.go b/list.go index c291a53..cd282c9 100644 --- a/list.go +++ b/list.go @@ -54,8 +54,18 @@ type List struct { // Whether or not navigating the list will wrap around. wrapAround bool - // The number of list items skipped at the top before the first item is drawn. - offset int + // The number of list items skipped at the top before the first item is + // drawn. + itemOffset int + + // The number of cells skipped on the left side of an item text. Shortcuts + // are not affected. + horizontalOffset int + + // Set to true if a currently visible item flows over the right border of + // the box. This is set by the Draw() function. It determines the behaviour + // of the right arrow key. + overflowing bool // An optional function which is called when the user has navigated to a list // item. @@ -116,6 +126,27 @@ func (l *List) GetCurrentItem() int { return l.currentItem } +// SetOffset sets the number of items to be skipped (vertically) as well as the +// number of cells skipped horizontally when the list is drawn. Note that one +// item corresponds to two rows when there are secondary texts. Shortcuts are +// always drawn. +// +// These values may change when the list is drawn to ensure the currently +// selected item is visible and item texts move out of view. Users can also +// modify these values by interacting with the list. +func (l *List) SetOffset(items, horizontal int) *List { + l.itemOffset = items + l.horizontalOffset = horizontal + return l +} + +// GetOffset returns the number of items skipped while drawing, as well as the +// number of cells item text is moved to the left. See also SetOffset() for more +// information on these values. +func (l *List) GetOffset() (int, int) { + return l.itemOffset, l.horizontalOffset +} + // RemoveItem removes the item with the given index (starting at 0) from the // list. If a negative index is provided, items are referred to from the back // (-1 = last item, -2 = second-to-last item, and so on). Out of range indices @@ -411,21 +442,28 @@ func (l *List) Draw(screen tcell.Screen) { } // Adjust offset to keep the current selection in view. - if l.currentItem < l.offset { - l.offset = l.currentItem + if l.currentItem < l.itemOffset { + l.itemOffset = l.currentItem } else if l.showSecondaryText { - if 2*(l.currentItem-l.offset) >= height-1 { - l.offset = (2*l.currentItem + 3 - height) / 2 + if 2*(l.currentItem-l.itemOffset) >= height-1 { + l.itemOffset = (2*l.currentItem + 3 - height) / 2 } } else { - if l.currentItem-l.offset >= height { - l.offset = l.currentItem + 1 - height + if l.currentItem-l.itemOffset >= height { + l.itemOffset = l.currentItem + 1 - height } } + if l.horizontalOffset < 0 { + l.horizontalOffset = 0 + } // Draw the list items. + var ( + maxWidth int // The maximum printed item width. + overflowing bool // Whether a text's end exceeds the right border. + ) for index, item := range l.items { - if index < l.offset { + if index < l.itemOffset { continue } @@ -439,7 +477,13 @@ func (l *List) Draw(screen tcell.Screen) { } // Main text. - Print(screen, item.MainText, x, y, width, AlignLeft, l.mainTextColor) + _, printedWidth, _, end := printWithStyle(screen, item.MainText, x, y, l.horizontalOffset, width, AlignLeft, tcell.StyleDefault.Foreground(l.mainTextColor)) + if printedWidth > maxWidth { + maxWidth = printedWidth + } + if end < len(item.MainText) { + overflowing = true + } // Background color of selected text. if index == l.currentItem && (!l.selectedFocusOnly || l.HasFocus()) { @@ -469,10 +513,25 @@ func (l *List) Draw(screen tcell.Screen) { // Secondary text. if l.showSecondaryText { - Print(screen, item.SecondaryText, x, y, width, AlignLeft, l.secondaryTextColor) + _, printedWidth, _, end := printWithStyle(screen, item.SecondaryText, x, y, l.horizontalOffset, width, AlignLeft, tcell.StyleDefault.Foreground(l.secondaryTextColor)) + if printedWidth > maxWidth { + maxWidth = printedWidth + } + if end < len(item.SecondaryText) { + overflowing = true + } y++ } } + + // We don't want the item text to get out of view. If the horizontal offset + // is too high, we reset it and redraw. (That should be about as efficient + // as calculating everything up front.) + if l.horizontalOffset > 0 && maxWidth < width { + l.horizontalOffset -= width - maxWidth + l.Draw(screen) + } + l.overflowing = overflowing } // InputHandler returns the handler for this primitive. @@ -490,10 +549,22 @@ func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primit previousItem := l.currentItem switch key := event.Key(); key { - case tcell.KeyTab, tcell.KeyDown, tcell.KeyRight: + case tcell.KeyTab, tcell.KeyDown: l.currentItem++ - case tcell.KeyBacktab, tcell.KeyUp, tcell.KeyLeft: + case tcell.KeyBacktab, tcell.KeyUp: l.currentItem-- + case tcell.KeyRight: + if l.overflowing { + l.horizontalOffset += 2 // We shift by 2 to account for two-cell characters. + } else { + l.currentItem++ + } + case tcell.KeyLeft: + if l.horizontalOffset > 0 { + l.horizontalOffset -= 2 + } else { + l.currentItem-- + } case tcell.KeyHome: l.currentItem = 0 case tcell.KeyEnd: @@ -573,7 +644,7 @@ func (l *List) indexAtPoint(x, y int) int { if l.showSecondaryText { index /= 2 } - index += l.offset + index += l.itemOffset if index >= len(l.items) { return -1 @@ -608,17 +679,17 @@ func (l *List) MouseHandler() func(action MouseAction, event *tcell.EventMouse, } consumed = true case MouseScrollUp: - if l.offset > 0 { - l.offset-- + if l.itemOffset > 0 { + l.itemOffset-- } consumed = true case MouseScrollDown: - lines := len(l.items) - l.offset + lines := len(l.items) - l.itemOffset if l.showSecondaryText { lines *= 2 } if _, _, _, height := l.GetInnerRect(); lines > height { - l.offset++ + l.itemOffset++ } consumed = true } diff --git a/table.go b/table.go index c1c5c4b..9ad0c10 100644 --- a/table.go +++ b/table.go @@ -886,10 +886,10 @@ ColumnLoop: finalWidth = width - columnX - 1 } cell.x, cell.y, cell.width = x+columnX+1, y+rowY, finalWidth - _, printed := printWithStyle(screen, cell.Text, x+columnX+1, y+rowY, finalWidth, cell.Align, tcell.StyleDefault.Foreground(cell.Color).Attributes(cell.Attributes)) + _, printed, _, _ := printWithStyle(screen, cell.Text, x+columnX+1, y+rowY, 0, finalWidth, cell.Align, tcell.StyleDefault.Foreground(cell.Color).Attributes(cell.Attributes)) if TaggedStringWidth(cell.Text)-printed > 0 && printed > 0 { _, _, style, _ := screen.GetContent(x+columnX+finalWidth, y+rowY) - printWithStyle(screen, string(SemigraphicsHorizontalEllipsis), x+columnX+finalWidth, y+rowY, 1, AlignLeft, style) + printWithStyle(screen, string(SemigraphicsHorizontalEllipsis), x+columnX+finalWidth, 0, y+rowY, 1, AlignLeft, style) } } diff --git a/treeview.go b/treeview.go index 04aa4a4..64df222 100644 --- a/treeview.go +++ b/treeview.go @@ -699,7 +699,7 @@ func (t *TreeView) Draw(screen tcell.Screen) { if node == t.currentNode { style = tcell.StyleDefault.Background(node.color).Foreground(t.backgroundColor) } - printWithStyle(screen, node.text, x+node.textX+prefixWidth, posY, width-node.textX-prefixWidth, AlignLeft, style) + printWithStyle(screen, node.text, x+node.textX+prefixWidth, posY, 0, width-node.textX-prefixWidth, AlignLeft, style) } } diff --git a/util.go b/util.go index b9bb8eb..47a6b80 100644 --- a/util.go +++ b/util.go @@ -237,15 +237,18 @@ func decomposeString(text string, findColors, findRegions bool) (colorIndices [] // Returns the number of actual bytes of the text printed (including color tags) // and the actual width used for the printed runes. func Print(screen tcell.Screen, text string, x, y, maxWidth, align int, color tcell.Color) (int, int) { - return printWithStyle(screen, text, x, y, maxWidth, align, tcell.StyleDefault.Foreground(color)) + bytes, width, _, _ := printWithStyle(screen, text, x, y, 0, maxWidth, align, tcell.StyleDefault.Foreground(color)) + return bytes, width } // printWithStyle works like Print() but it takes a style instead of just a -// foreground color. -func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int, style tcell.Style) (int, int) { +// foreground color. The skipWidth parameter specifies the number of cells +// skipped at the beginning of the text. It also returns the start and end index +// (exclusively) of the text actually printed. +func printWithStyle(screen tcell.Screen, text string, x, y, skipWidth, maxWidth, align int, style tcell.Style) (int, int, int, int) { totalWidth, totalHeight := screen.Size() if maxWidth <= 0 || len(text) == 0 || y < 0 || y >= totalHeight { - return 0, 0 + return 0, 0, 0, 0 } // Decompose the text. @@ -253,14 +256,14 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int, // We want to reduce all alignments to AlignLeft. if align == AlignRight { - if strippedWidth <= maxWidth { + if strippedWidth-skipWidth <= maxWidth { // There's enough space for the entire text. - return printWithStyle(screen, text, x+maxWidth-strippedWidth, y, maxWidth, AlignLeft, style) + return printWithStyle(screen, text, x+maxWidth-strippedWidth+skipWidth, y, skipWidth, maxWidth, AlignLeft, style) } // Trim characters off the beginning. var ( - bytes, width, colorPos, escapePos, tagOffset int - foregroundColor, backgroundColor, attributes string + bytes, width, colorPos, escapePos, tagOffset, from, to int + foregroundColor, backgroundColor, attributes string ) _, originalBackground, _ := style.Decompose() iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { @@ -275,7 +278,7 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int, tagOffset++ escapePos++ } - if strippedWidth-screenPos < maxWidth { + if strippedWidth-screenPos <= maxWidth { // We chopped off enough. if escapePos > 0 && textPos+tagOffset-1 >= escapeIndices[escapePos-1][0] && textPos+tagOffset-1 < escapeIndices[escapePos-1][1] { // Unescape open escape sequences. @@ -283,29 +286,36 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int, text = text[:escapeCharPos] + text[escapeCharPos+1:] } // Print and return. - bytes, width = printWithStyle(screen, text[textPos+tagOffset:], x, y, maxWidth, AlignLeft, style) + bytes, width, from, to = printWithStyle(screen, text[textPos+tagOffset:], x, y, 0, maxWidth, AlignLeft, style) + from += textPos + tagOffset + to += textPos + tagOffset return true } return false }) - return bytes, width + return bytes, width, from, to } else if align == AlignCenter { - if strippedWidth == maxWidth { + if strippedWidth-skipWidth == maxWidth { // Use the exact space. - return printWithStyle(screen, text, x, y, maxWidth, AlignLeft, style) - } else if strippedWidth < maxWidth { + return printWithStyle(screen, text, x, y, skipWidth, maxWidth, AlignLeft, style) + } else if strippedWidth-skipWidth < maxWidth { // We have more space than we need. - half := (maxWidth - strippedWidth) / 2 - return printWithStyle(screen, text, x+half, y, maxWidth-half, AlignLeft, style) + half := (maxWidth - strippedWidth + skipWidth) / 2 + return printWithStyle(screen, text, x+half, y, skipWidth, maxWidth-half, AlignLeft, style) } else { // Chop off runes until we have a perfect fit. var choppedLeft, choppedRight, leftIndex, rightIndex int rightIndex = len(strippedText) - for rightIndex-1 > leftIndex && strippedWidth-choppedLeft-choppedRight > maxWidth { - if choppedLeft < choppedRight { + for rightIndex-1 > leftIndex && strippedWidth-skipWidth-choppedLeft-choppedRight > maxWidth { + if skipWidth > 0 || choppedLeft < choppedRight { // Iterate on the left by one character. iterateString(strippedText[leftIndex:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { - choppedLeft += screenWidth + if skipWidth > 0 { + skipWidth -= screenWidth + strippedWidth -= screenWidth + } else { + choppedLeft += screenWidth + } leftIndex += textWidth return true }) @@ -351,16 +361,27 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int, escapePos++ } } - return printWithStyle(screen, text[leftIndex+tagOffset:], x, y, maxWidth, AlignLeft, style) + bytes, width, from, to := printWithStyle(screen, text[leftIndex+tagOffset:], x, y, 0, maxWidth, AlignLeft, style) + from += leftIndex + tagOffset + to += leftIndex + tagOffset + return bytes, width, from, to } } // Draw text. var ( - drawn, drawnWidth, colorPos, escapePos, tagOffset int - foregroundColor, backgroundColor, attributes string + drawn, drawnWidth, colorPos, escapePos, tagOffset, from, to int + foregroundColor, backgroundColor, attributes string ) iterateString(strippedText, func(main rune, comb []rune, textPos, length, screenPos, screenWidth int) bool { + // Skip character if necessary. + if skipWidth > 0 { + skipWidth -= screenWidth + from = textPos + length + to = from + return false + } + // Only continue if there is still space. if drawnWidth+screenWidth > maxWidth || x+drawnWidth >= totalWidth { return true @@ -373,7 +394,7 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int, colorPos++ } - // Handle scape tags. + // Handle escape tags. if escapePos < len(escapeIndices) && textPos+tagOffset >= escapeIndices[escapePos][0] && textPos+tagOffset < escapeIndices[escapePos][1] { if textPos+tagOffset == escapeIndices[escapePos][1]-2 { tagOffset++ @@ -381,6 +402,9 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int, } } + // Memorize positions. + to = textPos + length + // Print the rune sequence. finalX := x + drawnWidth _, _, finalStyle, _ := screen.GetContent(finalX, y) @@ -402,7 +426,7 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int, return false }) - return drawn + tagOffset + len(escapeIndices), drawnWidth + return drawn + tagOffset + len(escapeIndices), drawnWidth, from, to } // PrintSimple prints white text to the screen at the given position.