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

pull/395/merge
Oliver 3 years ago
parent 745e4ceeb7
commit dfabe788d4

@ -189,7 +189,7 @@ func (c *Checkbox) Draw(screen tcell.Screen) {
if !c.checked { if !c.checked {
checkedString = strings.Repeat(" ", checkboxWidth) 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. // InputHandler returns the handler for this primitive.

@ -54,8 +54,18 @@ type List struct {
// Whether or not navigating the list will wrap around. // Whether or not navigating the list will wrap around.
wrapAround bool wrapAround bool
// The number of list items skipped at the top before the first item is drawn. // The number of list items skipped at the top before the first item is
offset int // 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 // An optional function which is called when the user has navigated to a list
// item. // item.
@ -116,6 +126,27 @@ func (l *List) GetCurrentItem() int {
return l.currentItem 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 // 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 // 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 // (-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. // Adjust offset to keep the current selection in view.
if l.currentItem < l.offset { if l.currentItem < l.itemOffset {
l.offset = l.currentItem l.itemOffset = l.currentItem
} else if l.showSecondaryText { } else if l.showSecondaryText {
if 2*(l.currentItem-l.offset) >= height-1 { if 2*(l.currentItem-l.itemOffset) >= height-1 {
l.offset = (2*l.currentItem + 3 - height) / 2 l.itemOffset = (2*l.currentItem + 3 - height) / 2
} }
} else { } else {
if l.currentItem-l.offset >= height { if l.currentItem-l.itemOffset >= height {
l.offset = l.currentItem + 1 - height l.itemOffset = l.currentItem + 1 - height
} }
} }
if l.horizontalOffset < 0 {
l.horizontalOffset = 0
}
// Draw the list items. // 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 { for index, item := range l.items {
if index < l.offset { if index < l.itemOffset {
continue continue
} }
@ -439,7 +477,13 @@ func (l *List) Draw(screen tcell.Screen) {
} }
// Main text. // 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. // Background color of selected text.
if index == l.currentItem && (!l.selectedFocusOnly || l.HasFocus()) { if index == l.currentItem && (!l.selectedFocusOnly || l.HasFocus()) {
@ -469,10 +513,25 @@ func (l *List) Draw(screen tcell.Screen) {
// Secondary text. // Secondary text.
if l.showSecondaryText { 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++ 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. // 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 previousItem := l.currentItem
switch key := event.Key(); key { switch key := event.Key(); key {
case tcell.KeyTab, tcell.KeyDown, tcell.KeyRight: case tcell.KeyTab, tcell.KeyDown:
l.currentItem++ l.currentItem++
case tcell.KeyBacktab, tcell.KeyUp, tcell.KeyLeft: case tcell.KeyBacktab, tcell.KeyUp:
l.currentItem-- 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: case tcell.KeyHome:
l.currentItem = 0 l.currentItem = 0
case tcell.KeyEnd: case tcell.KeyEnd:
@ -573,7 +644,7 @@ func (l *List) indexAtPoint(x, y int) int {
if l.showSecondaryText { if l.showSecondaryText {
index /= 2 index /= 2
} }
index += l.offset index += l.itemOffset
if index >= len(l.items) { if index >= len(l.items) {
return -1 return -1
@ -608,17 +679,17 @@ func (l *List) MouseHandler() func(action MouseAction, event *tcell.EventMouse,
} }
consumed = true consumed = true
case MouseScrollUp: case MouseScrollUp:
if l.offset > 0 { if l.itemOffset > 0 {
l.offset-- l.itemOffset--
} }
consumed = true consumed = true
case MouseScrollDown: case MouseScrollDown:
lines := len(l.items) - l.offset lines := len(l.items) - l.itemOffset
if l.showSecondaryText { if l.showSecondaryText {
lines *= 2 lines *= 2
} }
if _, _, _, height := l.GetInnerRect(); lines > height { if _, _, _, height := l.GetInnerRect(); lines > height {
l.offset++ l.itemOffset++
} }
consumed = true consumed = true
} }

@ -886,10 +886,10 @@ ColumnLoop:
finalWidth = width - columnX - 1 finalWidth = width - columnX - 1
} }
cell.x, cell.y, cell.width = x+columnX+1, y+rowY, finalWidth 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 { if TaggedStringWidth(cell.Text)-printed > 0 && printed > 0 {
_, _, style, _ := screen.GetContent(x+columnX+finalWidth, y+rowY) _, _, 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)
} }
} }

@ -699,7 +699,7 @@ func (t *TreeView) Draw(screen tcell.Screen) {
if node == t.currentNode { if node == t.currentNode {
style = tcell.StyleDefault.Background(node.color).Foreground(t.backgroundColor) 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)
} }
} }

@ -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) // Returns the number of actual bytes of the text printed (including color tags)
// and the actual width used for the printed runes. // 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) { 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 // printWithStyle works like Print() but it takes a style instead of just a
// foreground color. // foreground color. The skipWidth parameter specifies the number of cells
func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int, style tcell.Style) (int, int) { // 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() totalWidth, totalHeight := screen.Size()
if maxWidth <= 0 || len(text) == 0 || y < 0 || y >= totalHeight { if maxWidth <= 0 || len(text) == 0 || y < 0 || y >= totalHeight {
return 0, 0 return 0, 0, 0, 0
} }
// Decompose the text. // 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. // We want to reduce all alignments to AlignLeft.
if align == AlignRight { if align == AlignRight {
if strippedWidth <= maxWidth { if strippedWidth-skipWidth <= maxWidth {
// There's enough space for the entire text. // 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. // Trim characters off the beginning.
var ( var (
bytes, width, colorPos, escapePos, tagOffset int bytes, width, colorPos, escapePos, tagOffset, from, to int
foregroundColor, backgroundColor, attributes string foregroundColor, backgroundColor, attributes string
) )
_, originalBackground, _ := style.Decompose() _, originalBackground, _ := style.Decompose()
iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { 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++ tagOffset++
escapePos++ escapePos++
} }
if strippedWidth-screenPos < maxWidth { if strippedWidth-screenPos <= maxWidth {
// We chopped off enough. // We chopped off enough.
if escapePos > 0 && textPos+tagOffset-1 >= escapeIndices[escapePos-1][0] && textPos+tagOffset-1 < escapeIndices[escapePos-1][1] { if escapePos > 0 && textPos+tagOffset-1 >= escapeIndices[escapePos-1][0] && textPos+tagOffset-1 < escapeIndices[escapePos-1][1] {
// Unescape open escape sequences. // 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:] text = text[:escapeCharPos] + text[escapeCharPos+1:]
} }
// Print and return. // 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 true
} }
return false return false
}) })
return bytes, width return bytes, width, from, to
} else if align == AlignCenter { } else if align == AlignCenter {
if strippedWidth == maxWidth { if strippedWidth-skipWidth == maxWidth {
// Use the exact space. // Use the exact space.
return printWithStyle(screen, text, x, y, maxWidth, AlignLeft, style) return printWithStyle(screen, text, x, y, skipWidth, maxWidth, AlignLeft, style)
} else if strippedWidth < maxWidth { } else if strippedWidth-skipWidth < maxWidth {
// We have more space than we need. // We have more space than we need.
half := (maxWidth - strippedWidth) / 2 half := (maxWidth - strippedWidth + skipWidth) / 2
return printWithStyle(screen, text, x+half, y, maxWidth-half, AlignLeft, style) return printWithStyle(screen, text, x+half, y, skipWidth, maxWidth-half, AlignLeft, style)
} else { } else {
// Chop off runes until we have a perfect fit. // Chop off runes until we have a perfect fit.
var choppedLeft, choppedRight, leftIndex, rightIndex int var choppedLeft, choppedRight, leftIndex, rightIndex int
rightIndex = len(strippedText) rightIndex = len(strippedText)
for rightIndex-1 > leftIndex && strippedWidth-choppedLeft-choppedRight > maxWidth { for rightIndex-1 > leftIndex && strippedWidth-skipWidth-choppedLeft-choppedRight > maxWidth {
if choppedLeft < choppedRight { if skipWidth > 0 || choppedLeft < choppedRight {
// Iterate on the left by one character. // Iterate on the left by one character.
iterateString(strippedText[leftIndex:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { 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 leftIndex += textWidth
return true return true
}) })
@ -351,16 +361,27 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int,
escapePos++ 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. // Draw text.
var ( var (
drawn, drawnWidth, colorPos, escapePos, tagOffset int drawn, drawnWidth, colorPos, escapePos, tagOffset, from, to int
foregroundColor, backgroundColor, attributes string foregroundColor, backgroundColor, attributes string
) )
iterateString(strippedText, func(main rune, comb []rune, textPos, length, screenPos, screenWidth int) bool { 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. // Only continue if there is still space.
if drawnWidth+screenWidth > maxWidth || x+drawnWidth >= totalWidth { if drawnWidth+screenWidth > maxWidth || x+drawnWidth >= totalWidth {
return true return true
@ -373,7 +394,7 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int,
colorPos++ colorPos++
} }
// Handle scape tags. // Handle escape tags.
if escapePos < len(escapeIndices) && textPos+tagOffset >= escapeIndices[escapePos][0] && textPos+tagOffset < escapeIndices[escapePos][1] { if escapePos < len(escapeIndices) && textPos+tagOffset >= escapeIndices[escapePos][0] && textPos+tagOffset < escapeIndices[escapePos][1] {
if textPos+tagOffset == escapeIndices[escapePos][1]-2 { if textPos+tagOffset == escapeIndices[escapePos][1]-2 {
tagOffset++ 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. // Print the rune sequence.
finalX := x + drawnWidth finalX := x + drawnWidth
_, _, finalStyle, _ := screen.GetContent(finalX, y) _, _, finalStyle, _ := screen.GetContent(finalX, y)
@ -402,7 +426,7 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int,
return false 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. // PrintSimple prints white text to the screen at the given position.

Loading…
Cancel
Save