From d3192a3e70216d6f8a61807ed00ced40969ad9f9 Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Thu, 11 Jun 2020 20:43:32 -0400 Subject: [PATCH] Update text input viewport scrolling to handle unicode characters --- go.mod | 1 + go.sum | 2 ++ textinput/textinput.go | 71 ++++++++++++++++++++++++++++++------------ 3 files changed, 54 insertions(+), 20 deletions(-) diff --git a/go.mod b/go.mod index d07f1d0..dc0f833 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.13 require ( github.com/charmbracelet/bubbletea v0.8.0 + github.com/mattn/go-runewidth v0.0.9 // indirect github.com/muesli/termenv v0.5.3-0.20200526053627-d728968dcf83 golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 // indirect golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 // indirect diff --git a/go.sum b/go.sum index 142a344..f91382c 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tW github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/muesli/termenv v0.5.3-0.20200526053627-d728968dcf83 h1:AfshZBlqAwhCZ27NJ1aPlMcPBihF1squ1GpaollhLQk= github.com/muesli/termenv v0.5.3-0.20200526053627-d728968dcf83/go.mod h1:O1/I6sw+6KcrgAmcs6uiUVr7Lui+DNVbHTzt9Lm/PlI= github.com/pkg/term v0.0.0-20200520122047-c3ffed290a03 h1:pd4YKIqCB0U7O2I4gWHgEUA2mCEOENmco0l/bM957bU= diff --git a/textinput/textinput.go b/textinput/textinput.go index f002965..d9ae1b9 100644 --- a/textinput/textinput.go +++ b/textinput/textinput.go @@ -6,6 +6,7 @@ import ( "unicode" tea "github.com/charmbracelet/bubbletea" + rw "github.com/mattn/go-runewidth" "github.com/muesli/termenv" ) @@ -59,7 +60,8 @@ type Model struct { // Used to emulate a viewport when width is set and the content is // overflowing - offset int + offset int + offsetRight int } // SetValue sets the value of the text input. @@ -84,7 +86,7 @@ func (m Model) Value() string { // Cursor start moves the cursor to the given position. If the position is out // of bounds the cursor will be moved to the start or end accordingly. func (m *Model) SetCursor(pos int) { - m.pos = max(0, min(len(m.value), pos)) + m.pos = clamp(pos, 0, len(m.value)) m.handleOverflow() } @@ -120,7 +122,6 @@ func (m *Model) Blur() { // Reset sets the input to its default state with no input. func (m *Model) Reset() { m.value = nil - m.offset = 0 m.pos = 0 m.blink = false } @@ -128,14 +129,46 @@ func (m *Model) Reset() { // If a max width is defined, perform some logic to treat the visible area // as a horizontally scrolling viewport. func (m *Model) handleOverflow() { - if m.Width > 0 { - overflow := max(0, len(m.value)-m.Width) + if m.Width <= 0 || rw.StringWidth(string(m.value)) <= m.Width { + m.offset = 0 + m.offsetRight = len(m.value) + return + } - if overflow > 0 && m.pos < m.offset { - m.offset = max(0, min(len(m.value), m.pos)) - } else if overflow > 0 && m.pos >= m.offset+m.Width { - m.offset = max(0, m.pos-m.Width) + if m.pos < m.offset { + + m.offset = m.pos + + w := 0 + i := 0 + runes := m.value[m.offset:] + + for i < len(runes) && w <= m.Width { + w += rw.RuneWidth(runes[i]) + if w <= m.Width+1 { + i++ + } } + + m.offsetRight = m.offset + i + + } else if m.pos >= m.offsetRight { + + m.offsetRight = m.pos + + w := 0 + runes := m.value[:m.offsetRight] + i := len(runes) - 1 + + for i > 0 && w < m.Width { + w += rw.RuneWidth(runes[i]) + if w <= m.Width { + i-- + } + } + + m.offset = m.offsetRight - (len(runes) - 1 - i) + } } @@ -329,14 +362,7 @@ func View(model tea.Model) string { return placeholderView(m) } - left := m.offset - right := 0 - if m.Width > 0 { - right = min(len(m.value), m.offset+m.Width+1) - } else { - right = len(m.value) - } - value := m.value[left:right] + value := m.value[m.offset:m.offsetRight] pos := m.pos - m.offset v := m.colorText(string(value[:pos])) @@ -350,9 +376,10 @@ func View(model tea.Model) string { // If a max width and background color were set fill the empty spaces with // the background color. - if m.Width > 0 && len(m.BackgroundColor) > 0 && len(value) <= m.Width { - padding := m.Width - len(value) - if len(value)+padding <= m.Width && pos < len(value) { + valWidth := rw.StringWidth(string(value)) + if m.Width > 0 && len(m.BackgroundColor) > 0 && valWidth <= m.Width { + padding := max(0, m.Width-valWidth) + if valWidth+padding <= m.Width && pos < len(value) { padding++ } v += strings.Repeat( @@ -413,6 +440,10 @@ func Blink(model Model) tea.Cmd { } } +func clamp(v, low, high int) int { + return min(high, max(low, v)) +} + func min(a, b int) int { if a < b { return a