2
0
mirror of https://github.com/rivo/tview.git synced 2024-11-12 19:10:28 +00:00

Allowing list items to shift horizontally. Resolves #512, fixes #513

This commit is contained in:
Oliver 2021-02-15 18:26:27 +01:00
parent 745e4ceeb7
commit dfabe788d4
5 changed files with 141 additions and 46 deletions

View File

@ -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.

107
list.go
View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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)
}
}

72
util.go
View File

@ -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.