diff --git a/README.md b/README.md index eca7824..1ebfc01 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,8 @@ Add your issue here on GitHub. Feel free to get in touch if you have any questio (There are no corresponding tags in the project. I only keep such a history in this README.) +- v0.15 (2018-05-02) + - `Flex` and `Grid` don't clear their background per default, thus allowing for custom modals. See the [Wiki](https://github.com/rivo/tview/wiki/Modal) for an example. - v0.14 (2018-04-13) - Added an `Escape()` function which keep strings like color or region tags from being recognized as such. - Added `ANSIIWriter()` and `TranslateANSII()` which convert ANSII escape sequences to `tview` color tags. diff --git a/box.go b/box.go index eec1ec6..7c6f00a 100644 --- a/box.go +++ b/box.go @@ -302,9 +302,12 @@ func (b *Box) Draw(screen tcell.Screen) { // Fill background. background := def.Background(b.backgroundColor) - for y := b.y; y < b.y+height; y++ { - for x := b.x; x < b.x+width; x++ { - screen.SetContent(x, y, ' ', nil, background) + + if b.backgroundColor != tcell.ColorDefault { + for y := b.y; y < b.y+b.height; y++ { + for x := b.x; x < b.x+b.width; x++ { + screen.SetContent(x, y, ' ', nil, background) + } } } diff --git a/demos/modal/centered.png b/demos/modal/centered.png new file mode 100644 index 0000000..7511a05 Binary files /dev/null and b/demos/modal/centered.png differ diff --git a/flex.go b/flex.go index 33460c0..e23808a 100644 --- a/flex.go +++ b/flex.go @@ -33,17 +33,24 @@ type Flex struct { // FlexRow or FlexColumn. direction int - // If set to true, will use the entire screen as its available space instead - // its box dimensions. + // If set to true, Flex will use the entire screen as its available space + // instead its box dimensions. fullScreen bool } // NewFlex returns a new flexbox layout container with no primitives and its // direction set to FlexColumn. To add primitives to this layout, see AddItem(). // To change the direction, see SetDirection(). +// +// Note that Box, the superclass of Flex, will have its background color set to +// transparent so that any nil flex items will leave their background unchanged. +// To clear a Flex's background before any items are drawn, set it to the +// desired color: +// +// flex.SetBackgroundColor(tview.Styles.PrimitiveBackgroundColor) func NewFlex() *Flex { f := &Flex{ - Box: NewBox(), + Box: NewBox().SetBackgroundColor(tcell.ColorDefault), direction: FlexColumn, } f.focus = f @@ -102,8 +109,6 @@ func (f *Flex) RemoveItem(p Primitive) *Flex { // Draw draws this primitive onto the screen. func (f *Flex) Draw(screen tcell.Screen) { - f.Box.Draw(screen) - // Calculate size and position of the items. // Do we use the entire screen? diff --git a/grid.go b/grid.go index cc21dfe..6c64287 100644 --- a/grid.go +++ b/grid.go @@ -59,9 +59,16 @@ type Grid struct { } // NewGrid returns a new grid-based layout container with no initial primitives. +// +// Note that Box, the superclass of Grid, will have its background color set to +// transparent so that any grid areas not covered by any primitives will leave +// their background unchanged. To clear a Grid's background before any items are +// drawn, set it to the desired color: +// +// grid.SetBackgroundColor(tview.Styles.PrimitiveBackgroundColor) func NewGrid() *Grid { g := &Grid{ - Box: NewBox(), + Box: NewBox().SetBackgroundColor(tcell.ColorDefault), bordersColor: Styles.GraphicsColor, } g.focus = g diff --git a/inputfield.go b/inputfield.go index 580a039..df0821e 100644 --- a/inputfield.go +++ b/inputfield.go @@ -367,6 +367,7 @@ func (i *InputField) Draw(screen tcell.Screen) { text := i.text if text == "" && i.placeholder != "" { Print(screen, i.placeholder, x, y, fieldWidth, AlignLeft, i.placeholderTextColor) + } textWidth := runewidth.StringWidth(text) diff --git a/textview.go b/textview.go index ced400b..32278d1 100644 --- a/textview.go +++ b/textview.go @@ -832,10 +832,60 @@ func (t *TextView) Draw(screen tcell.Screen) { } // Print the line. - var currentTag, currentRegion, currentEscapeTag, skipped int + var currentTag, currentRegion, currentEscapeTag, skipped, runeSeqWidth int + runeSequence := make([]rune, 0, 10) + flush := func() { + if len(runeSequence) == 0 { + return + } + + // Mix the existing style with the new style. + _, _, existingStyle, _ := screen.GetContent(x+posX, y+line-t.lineOffset) + _, background, _ := existingStyle.Decompose() + style := overlayStyle(background, defaultStyle, foregroundColor, backgroundColor, attributes) + + // Do we highlight this character? + var highlighted bool + if len(regionID) > 0 { + if _, ok := t.highlights[regionID]; ok { + highlighted = true + } + } + if highlighted { + fg, bg, _ := style.Decompose() + if bg == tcell.ColorDefault { + r, g, b := fg.RGB() + c := colorful.Color{R: float64(r) / 255, G: float64(g) / 255, B: float64(b) / 255} + _, _, li := c.Hcl() + if li < .5 { + bg = tcell.ColorWhite + } else { + bg = tcell.ColorBlack + } + } + style = style.Background(fg).Foreground(bg) + } + + // Draw the character. + var comb []rune + if len(runeSequence) > 1 { + // Allocate space for the combining characters only when necessary. + comb = make([]rune, len(runeSequence)-1) + copy(comb, runeSequence[1:]) + } + for offset := 0; offset < runeSeqWidth; offset++ { + screen.SetContent(x+posX+offset, y+line-t.lineOffset, runeSequence[0], comb, style) + } + + // Advance. + posX += runeSeqWidth + runeSequence = runeSequence[:0] + runeSeqWidth = 0 + } for pos, ch := range text { // Get the color. if currentTag < len(colorTags) && pos >= colorTagIndices[currentTag][0] && pos < colorTagIndices[currentTag][1] { + flush() if pos == colorTagIndices[currentTag][1]-1 { foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colorTags[currentTag]) currentTag++ @@ -845,6 +895,7 @@ func (t *TextView) Draw(screen tcell.Screen) { // Get the region. if currentRegion < len(regionIndices) && pos >= regionIndices[currentRegion][0] && pos < regionIndices[currentRegion][1] { + flush() if pos == regionIndices[currentRegion][1]-1 { regionID = regions[currentRegion][1] currentRegion++ @@ -854,6 +905,7 @@ func (t *TextView) Draw(screen tcell.Screen) { // Skip the second-to-last character of an escape tag. if currentEscapeTag < len(escapeIndices) && pos >= escapeIndices[currentEscapeTag][0] && pos < escapeIndices[currentEscapeTag][1] { + flush() if pos == escapeIndices[currentEscapeTag][1]-1 { currentEscapeTag++ } else if pos == escapeIndices[currentEscapeTag][1]-2 { @@ -864,7 +916,14 @@ func (t *TextView) Draw(screen tcell.Screen) { // Determine the width of this rune. chWidth := runewidth.RuneWidth(ch) if chWidth == 0 { - continue + // If this is not a modifier, we treat it as a space character. + if len(runeSequence) == 0 { + ch = ' ' + chWidth = 1 + } else { + runeSequence = append(runeSequence, ch) + continue + } } // Skip to the right. @@ -874,15 +933,12 @@ func (t *TextView) Draw(screen tcell.Screen) { } // Stop at the right border. - if posX+chWidth > width { + if posX+runeSeqWidth+chWidth > width { break } - // Mix the existing style with the new style. - _, _, existingStyle, _ := screen.GetContent(x+posX, y+line-t.lineOffset) - _, background, _ := existingStyle.Decompose() - style := overlayStyle(background, defaultStyle, foregroundColor, backgroundColor, attributes) - + // Flush the rune sequence. + flush() // Do we highlight this character? var highlighted bool if len(regionID) > 0 { diff --git a/util.go b/util.go index 6c4fbe4..67e3fcc 100644 --- a/util.go +++ b/util.go @@ -339,6 +339,9 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int, width += w start = index } + for runewidth.RuneWidth(runes[start]) == 0 && start < len(runes) { + start++ + } return printWithStyle(screen, substring(start, len(runes)), x+maxWidth-width, y, width, AlignLeft, style) } else if align == AlignCenter { width := runewidth.StringWidth(strippedText) @@ -354,12 +357,15 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int, var choppedLeft, choppedRight, leftIndex, rightIndex int rightIndex = len(runes) - 1 for rightIndex > leftIndex && width-choppedLeft-choppedRight > maxWidth { - leftWidth := runewidth.RuneWidth(runes[leftIndex]) - rightWidth := runewidth.RuneWidth(runes[rightIndex]) if choppedLeft < choppedRight { + leftWidth := runewidth.RuneWidth(runes[leftIndex]) choppedLeft += leftWidth leftIndex++ + for runewidth.RuneWidth(runes[leftIndex]) == 0 && leftIndex < len(runes) && leftIndex < rightIndex { + leftIndex++ + } } else { + rightWidth := runewidth.RuneWidth(runes[rightIndex]) choppedRight += rightWidth rightIndex-- } @@ -375,9 +381,39 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int, colorPos, escapePos int foregroundColor, backgroundColor, attributes string ) + runeSequence := make([]rune, 0, 10) + runeSeqWidth := 0 + flush := func() { + if len(runeSequence) == 0 { + return // Nothing to flush. + } + + // Print the rune sequence. + finalX := x + drawnWidth + _, _, finalStyle, _ := screen.GetContent(finalX, y) + _, background, _ := finalStyle.Decompose() + finalStyle = overlayStyle(background, style, foregroundColor, backgroundColor, attributes) + var comb []rune + if len(runeSequence) > 1 { + // Allocate space for the combining characters only when necessary. + comb = make([]rune, len(runeSequence)-1) + copy(comb, runeSequence[1:]) + } + for offset := 0; offset < runeSeqWidth; offset++ { + // To avoid undesired effects, we place the same character in all cells. + screen.SetContent(finalX+offset, y, runeSequence[0], comb, finalStyle) + } + + // Advance and reset. + drawn += len(runeSequence) + drawnWidth += runeSeqWidth + runeSequence = runeSequence[:0] + runeSeqWidth = 0 + } for pos, ch := range text { // Handle color tags. if colorPos < len(colorIndices) && pos >= colorIndices[colorPos][0] && pos < colorIndices[colorPos][1] { + flush() if pos == colorIndices[colorPos][1]-1 { foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos]) colorPos++ @@ -387,6 +423,7 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int, // Handle escape tags. if escapePos < len(escapeIndices) && pos >= escapeIndices[escapePos][0] && pos < escapeIndices[escapePos][1] { + flush() if pos == escapeIndices[escapePos][1]-1 { escapePos++ } else if pos == escapeIndices[escapePos][1]-2 { @@ -397,21 +434,26 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int, // Check if we have enough space for this rune. chWidth := runewidth.RuneWidth(ch) if drawnWidth+chWidth > maxWidth { - break + break // No. We're done then. } - finalX := x + drawnWidth - // Print the rune. - _, _, finalStyle, _ := screen.GetContent(finalX, y) - _, background, _ := finalStyle.Decompose() - finalStyle = overlayStyle(background, style, foregroundColor, backgroundColor, attributes) - for offset := 0; offset < chWidth; offset++ { - // To avoid undesired effects, we place the same character in all cells. - screen.SetContent(finalX+offset, y, ch, nil, finalStyle) + // Put this rune in the queue. + if chWidth == 0 { + // If this is not a modifier, we treat it as a space character. + if len(runeSequence) == 0 { + ch = ' ' + chWidth = 1 + } + } else { + // We have a character. Flush all previous runes. + flush() } + runeSequence = append(runeSequence, ch) + runeSeqWidth += chWidth - drawn++ - drawnWidth += chWidth + } + if drawnWidth+runeSeqWidth <= maxWidth { + flush() } return drawn, drawnWidth