tcell/views/text.go

280 lines
6.3 KiB
Go

// Copyright 2015 The Tcell Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use file except in compliance with the License.
// You may obtain a copy of the license at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package views
import (
"github.com/mattn/go-runewidth"
"github.com/gdamore/tcell"
)
// Text is a Widget with containing a block of text, which can optionally
// be styled.
type Text struct {
view View
align Alignment
style tcell.Style
text []rune
widths []int
styles []tcell.Style
lengths []int
width int
height int
WidgetWatchers
}
func (t *Text) clear() {
v := t.view
w, h := v.Size()
v.Clear()
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
v.SetContent(x, y, ' ', nil, t.style)
}
}
}
// calcY figures the initial Y offset. Alignment is top by default.
func (t *Text) calcY(height int) int {
if t.align&VAlignCenter != 0 {
return (height - len(t.lengths)) / 2
}
if t.align&VAlignBottom != 0 {
return height - len(t.lengths)
}
return 0
}
// calcX figures the initial X offset for the given line.
// Alignment is left by default.
func (t *Text) calcX(width, line int) int {
if t.align&HAlignCenter != 0 {
return (width - t.lengths[line]) / 2
}
if t.align&HAlignRight != 0 {
return width - t.lengths[line]
}
return 0
}
// Draw draws the Text.
func (t *Text) Draw() {
v := t.view
if v == nil {
return
}
width, height := v.Size()
if width == 0 || height == 0 {
return
}
t.clear()
// Note that we might wind up with a negative X if the width
// is larger than the length. That's OK, and correct even.
// The view will clip it properly in that case.
// We align to the left & top by default.
y := t.calcY(height)
r := rune(0)
w := 0
x := 0
var styl tcell.Style
var comb []rune
line := 0
newline := true
for i, l := range t.text {
if newline {
x = t.calcX(width, line)
newline = false
}
if l == '\n' {
if w != 0 {
v.SetContent(x, y, r, comb, styl)
}
newline = true
w = 0
comb = nil
line++
y++
continue
}
if t.widths[i] == 0 {
comb = append(comb, l)
continue
}
if w != 0 {
v.SetContent(x, y, r, comb, styl)
x += w
}
r = l
w = t.widths[i]
styl = t.styles[i]
comb = nil
}
if w != 0 {
v.SetContent(x, y, r, comb, styl)
}
}
// Size returns the width and height in character cells of the Text.
func (t *Text) Size() (int, int) {
if len(t.text) != 0 {
return t.width, t.height
}
return 0, 0
}
// SetAlignment sets the alignment. Negative values
// indicate right justification, positive values are left,
// and zero indicates center aligned.
func (t *Text) SetAlignment(align Alignment) {
if align != t.align {
t.align = align
t.PostEventWidgetContent(t)
}
}
// Alignment returns the alignment of the Text.
func (t *Text) Alignment() Alignment {
return t.align
}
// SetView sets the View object used for the text bar.
func (t *Text) SetView(view View) {
t.view = view
}
// HandleEvent implements a tcell.EventHandler, but does nothing.
func (t *Text) HandleEvent(tcell.Event) bool {
return false
}
// SetText sets the text used for the string. Any previously set
// styles on individual rune indices are reset, and the default style
// for the widget is set.
func (t *Text) SetText(s string) {
t.width = 0
t.text = []rune(s)
if len(t.widths) < len(t.text) {
t.widths = make([]int, len(t.text))
} else {
t.widths = t.widths[0:len(t.text)]
}
if len(t.styles) < len(t.text) {
t.styles = make([]tcell.Style, len(t.text))
} else {
t.styles = t.styles[0:len(t.text)]
}
t.lengths = []int{}
length := 0
for i, r := range t.text {
t.widths[i] = runewidth.RuneWidth(r)
t.styles[i] = t.style
if r == '\n' {
t.lengths = append(t.lengths, length)
if length > t.width {
t.width = length
length = 0
}
} else if t.widths[i] == 0 && length == 0 {
// If first character on line is combining, inject
// a leading space. (Shame on the caller!)
t.widths = append(t.widths, 0)
copy(t.widths[i+1:], t.widths[i:])
t.widths[i] = 1
t.text = append(t.text, ' ')
copy(t.text[i+1:], t.text[i:])
t.text[i] = ' '
t.styles = append(t.styles, t.style)
copy(t.styles[i+1:], t.styles[i:])
t.styles[i] = t.style
length++
} else {
length += t.widths[i]
}
}
if length > 0 {
t.lengths = append(t.lengths, length)
if length > t.width {
t.width = length
}
}
t.height = len(t.lengths)
t.PostEventWidgetContent(t)
}
// Text returns the text that was set.
func (t *Text) Text() string {
return string(t.text)
}
// SetStyle sets the style used. This applies to every cell in the
// in the text.
func (t *Text) SetStyle(style tcell.Style) {
t.style = style
for i := 0; i < len(t.text); i++ {
if t.widths[i] != 0 {
t.styles[i] = t.style
}
}
t.PostEventWidgetContent(t)
}
// Style returns the previously set default style. Note that
// individual characters may have different styles.
func (t *Text) Style() tcell.Style {
return t.style
}
// SetStyleAt sets the style at the given rune index. Note that for
// strings containing combining characters, it is not possible to
// change the style at the position of the combining character, but
// those positions *do* count for calculating the index. A lot of
// complexity can be avoided by avoiding the use of combining characters.
func (t *Text) SetStyleAt(pos int, style tcell.Style) {
if pos < 0 || pos >= len(t.text) || t.widths[pos] < 1 {
return
}
t.styles[pos] = style
t.PostEventWidgetContent(t)
}
// StyleAt gets the style at the given rune index. If an invalid
// index is given, or the index is a combining character, then
// tcell.StyleDefault is returned.
func (t *Text) StyleAt(pos int) tcell.Style {
if pos < 0 || pos >= len(t.text) || t.widths[pos] < 1 {
return tcell.StyleDefault
}
return t.styles[pos]
}
// Resize is called when our View changes sizes.
func (t *Text) Resize() {
t.PostEventWidgetResize(t)
}
// NewText creates an empty Text.
func NewText() *Text {
return &Text{}
}