diff options
author | Hubert Hirtz <hubert.hirtz@laposte.net> | 2020-06-04 22:55:15 +0200 |
---|---|---|
committer | Hubert Hirtz <hubert.hirtz@laposte.net> | 2020-06-04 22:55:15 +0200 |
commit | f7331d1c665bd997fe5bc41c6d375d30ea9f4457 (patch) | |
tree | fe3ba9f932ace2b8582fa0f9b903349aa44017cb /ui | |
parent | Fix color codes (diff) |
Buffers are now shown (mostly) correctly
senpai must now split on whitespace when drawing
Diffstat (limited to 'ui')
-rw-r--r-- | ui/buffers.go | 112 | ||||
-rw-r--r-- | ui/buffers_test.go | 63 | ||||
-rw-r--r-- | ui/ui.go | 93 | ||||
-rw-r--r-- | ui/width.go | 75 | ||||
-rw-r--r-- | ui/width_test.go | 25 |
5 files changed, 300 insertions, 68 deletions
diff --git a/ui/buffers.go b/ui/buffers.go index 8ab5b44..dfef365 100644 --- a/ui/buffers.go +++ b/ui/buffers.go @@ -1,6 +1,10 @@ package ui -import "time" +import ( + "fmt" + "strings" + "time" +) var homeMessages = []string{ "\x1dYou open an IRC client.", @@ -11,10 +15,74 @@ var homeMessages = []string{ "Student? No, I'm an IRC \x02client\x02!", } +func IsSplitRune(c rune) bool { + return c == ' ' || c == '\t' +} + +type Point struct { + X int + I int +} + type Line struct { Time time.Time IsStatus bool Content string + + SplitPoints []Point + renderedHeight int +} + +func (line *Line) Invalidate() { + line.renderedHeight = -1 +} + +func (line *Line) RenderedHeight(screenWidth int) (height int) { + if line.renderedHeight < 0 { + line.computeRenderedHeight(screenWidth) + } + height = line.renderedHeight + return +} + +func (line *Line) computeRenderedHeight(screenWidth int) { + line.renderedHeight = 1 + residualWidth := 0 + + for _, sp := range line.SplitPoints { + w := sp.X + residualWidth + + if line.renderedHeight*screenWidth < w { + line.renderedHeight += 1 + residualWidth = w % screenWidth + } + } +} + +func (line *Line) computeSplitPoints() { + var wb widthBuffer + lastWasSplit := true + + for i, r := range line.Content { + curIsSplit := IsSplitRune(r) + + if !lastWasSplit && curIsSplit { + line.SplitPoints = append(line.SplitPoints, Point{ + X: wb.Width(), + I: i, + }) + } + + lastWasSplit = curIsSplit + wb.WriteRune(r) + } + + if !lastWasSplit { + line.SplitPoints = append(line.SplitPoints, Point{ + X: wb.Width(), + I: len(line.Content), + }) + } } type Buffer struct { @@ -100,15 +168,49 @@ func (bs *BufferList) Idx(title string) (idx int) { } func (bs *BufferList) AddLine(idx int, line string, t time.Time, isStatus bool) { + b := &bs.List[idx] n := len(bs.List[idx].Content) - if isStatus && n != 0 && bs.List[idx].Content[n-1].IsStatus { - bs.List[idx].Content[n-1].Content += " " + line + line = strings.TrimRight(line, "\t ") + + if isStatus && n != 0 && b.Content[n-1].IsStatus { + l := &b.Content[n-1] + l.Content += " " + line + + lineWidth := StringWidth(line) + lastSP := l.SplitPoints[len(l.SplitPoints)-1] + sp := Point{ + X: lastSP.X + 1 + lineWidth, + I: len(l.SplitPoints), + } + + l.SplitPoints = append(l.SplitPoints, sp) + l.Invalidate() } else { - bs.List[idx].Content = append(bs.List[idx].Content, Line{ + if n == 0 || b.Content[n-1].Time.Truncate(time.Minute) != t.Truncate(time.Minute) { + hour := t.Hour() + minute := t.Minute() + + line = fmt.Sprintf("\x02%02d:%02d\x00 %s", hour, minute, line) + } + + l := Line{ Time: t, IsStatus: isStatus, Content: line, - }) + } + + l.computeSplitPoints() + l.Invalidate() + + b.Content = append(b.Content, l) + } +} + +func (bs *BufferList) Invalidate() { + for i := range bs.List { + for j := range bs.List[i].Content { + bs.List[i].Content[j].Invalidate() + } } } diff --git a/ui/buffers_test.go b/ui/buffers_test.go new file mode 100644 index 0000000..fdcbece --- /dev/null +++ b/ui/buffers_test.go @@ -0,0 +1,63 @@ +package ui + +import "testing" + +func assertSplitPoints(t *testing.T, line string, expected []Point) { + l := Line{Content: line} + l.computeSplitPoints() + + if len(l.SplitPoints) != len(expected) { + t.Errorf("%q: expected %d split points got %d", line, len(expected), len(l.SplitPoints)) + } + + for i := 0; i < len(expected); i++ { + e := expected[i] + a := l.SplitPoints[i] + + if e.X != a.X { + t.Errorf("%q, point #%d: expected X=%d got %d", line, i, e.X, a.X) + } + if e.I != a.I { + t.Errorf("%q, point #%d: expected I=%d got %d", line, i, e.I, a.I) + } + } +} + +func TestLineSplitPoints(t *testing.T) { + assertSplitPoints(t, "hello", []Point{ + {X: 5, I: 5}, + }) + assertSplitPoints(t, "hello world", []Point{ + {X: 5, I: 5}, + {X: 11, I: 11}, + }) + assertSplitPoints(t, "lorem ipsum dolor shit amet", []Point{ + {X: 5, I: 5}, + {X: 11, I: 11}, + {X: 17, I: 17}, + {X: 22, I: 22}, + {X: 27, I: 27}, + }) +} + +func assertRenderedHeight(t *testing.T, line string, width int, expected int) { + l := Line{Content: line} + l.computeSplitPoints() + l.Invalidate() + + actual := l.RenderedHeight(width) + + if actual != expected { + t.Errorf("%q (width=%d) expected to take %d lines, takes %d", line, width, expected, actual) + } +} + +func TestRenderedHeight(t *testing.T) { + assertRenderedHeight(t, "hello world", 100, 1) + assertRenderedHeight(t, "hello world", 10, 2) + + assertRenderedHeight(t, "have a good day!", 100, 1) + assertRenderedHeight(t, "have a good day!", 10, 2) + assertRenderedHeight(t, "have a good day!", 6, 3) + assertRenderedHeight(t, "have a good day!", 4, 4) +} @@ -58,7 +58,7 @@ func New() (ui *UI, err error) { ui.textInput = []rune{} - ui.Draw() + ui.Resize() return } @@ -177,7 +177,12 @@ func (ui *UI) InputEnter() (content string) { return } -func (ui *UI) Draw() { +func (ui *UI) Resize() { + ui.bufferList.Invalidate() + ui.draw() +} + +func (ui *UI) draw() { ui.drawStatus() ui.drawEditor() ui.drawBuffer() @@ -220,7 +225,7 @@ func (ui *UI) drawEditor() { func (ui *UI) drawBuffer() { st := tcell.StyleDefault w, h := ui.screen.Size() - if h < 2 { + if h < 3 { return } @@ -236,67 +241,30 @@ func (ui *UI) drawBuffer() { return } - y := h - 2 - start := len(b.Content) - 1 - for { - lw := runewidth.StringWidth(b.Content[start].Content) - - if y-(lw/w+1) < 0 { - break - } - - y -= lw/w + 1 - - if start <= 0 { - break - } - start-- - } - if start > 0 { - start++ - } - var bold, italic, underline bool var colorState int var fgColor, bgColor int - var lastHour, lastMinute int - - for _, line := range b.Content[start:] { - rs := []rune(line.Content) - x := 0 - - hour := line.Time.Hour() - minute := line.Time.Minute() + y0 := h - 2 - if hour != lastHour || minute != lastMinute { - t := []rune{rune(hour/10) + '0', rune(hour%10) + '0', ':', rune(minute/10) + '0', rune(minute%10) + '0', ' '} - tst := tcell.StyleDefault.Bold(true) + for i := len(b.Content) - 1; 0 <= i; i-- { + line := &b.Content[i] - for _, r := range t { - if w <= x { - y++ - x = 0 - } - if h-2 <= y { - break - } - - ui.screen.SetContent(x, y, r, nil, tst) - x += runewidth.RuneWidth(r) - } + lineHeight := line.RenderedHeight(w) + y0 -= lineHeight + y := y0 - lastHour = hour - lastMinute = minute - } + rs := []rune(line.Content) + x := 0 for _, r := range rs { if w <= x { y++ x = 0 } - if h-2 <= y { - break + + if x == 0 && IsSplitRune(r) { + continue } if colorState == 1 { @@ -345,19 +313,16 @@ func (ui *UI) drawBuffer() { ui.screen.SetContent(x, y, ',', nil, st) x++ } else if colorState == 5 { + colorState = 0 + st = st.Foreground(colorFromCode(fgColor)) + if '0' <= r && r <= '9' { bgColor = bgColor*10 + int(r-'0') - colorState = 6 + st = st.Background(colorFromCode(bgColor)) continue } - st = st.Foreground(colorFromCode(fgColor)) st = st.Background(colorFromCode(bgColor)) - colorState = 0 - } else if colorState == 6 { - st = st.Foreground(colorFromCode(fgColor)) - st = st.Background(colorFromCode(bgColor)) - colorState = 0 } if r == 0x00 { @@ -388,19 +353,21 @@ func (ui *UI) drawBuffer() { continue } - ui.screen.SetContent(x, y, r, nil, st) + if 0 <= y { + ui.screen.SetContent(x, y, r, nil, st) + } x += runewidth.RuneWidth(r) } - y++ + if y0 < 0 { + break + } + st = tcell.StyleDefault bold = false italic = false underline = false colorState = 0 - if h-2 <= y { - break - } } ui.screen.Show() diff --git a/ui/width.go b/ui/width.go new file mode 100644 index 0000000..6634993 --- /dev/null +++ b/ui/width.go @@ -0,0 +1,75 @@ +package ui + +import ( + "github.com/mattn/go-runewidth" +) + +type widthBuffer struct { + width int + color int + comma bool +} + +func (wb *widthBuffer) Width() int { + return wb.width +} + +func (wb *widthBuffer) WriteString(s string) { + for _, r := range s { + wb.WriteRune(r) + } +} + +func (wb *widthBuffer) WriteRune(r rune) { + if wb.color == 1 { + if '0' <= r && r <= '9' { + wb.color = 2 + return + } + wb.color = 0 + } else if wb.color == 2 { + if '0' <= r && r <= '9' { + wb.color = 3 + return + } + if r == ',' { + wb.color = 4 + return + } + wb.color = 0 + } else if wb.color == 3 { + if r == ',' { + wb.color = 4 + return + } + wb.color = 0 + } else if wb.color == 4 { + if '0' <= r && r <= '9' { + wb.color = 5 + return + } + + wb.width++ + wb.color = 0 + } else if wb.color == 5 { + wb.color = 0 + if '0' <= r && r <= '9' { + return + } + } + + if r == 0x03 { + wb.color = 1 + return + } + + wb.width += runewidth.RuneWidth(r) +} + +func StringWidth(s string) int { + var wb widthBuffer + + wb.WriteString(s) + + return wb.Width() +} diff --git a/ui/width_test.go b/ui/width_test.go new file mode 100644 index 0000000..ae478ab --- /dev/null +++ b/ui/width_test.go @@ -0,0 +1,25 @@ +package ui + +import "testing" + +func assertStringWidth(t *testing.T, input string, expected int) { + actual := StringWidth(input) + if actual != expected { + t.Errorf("%q: expected width of %d got %d", input, expected, actual) + } +} + +func TestStringWidth(t *testing.T) { + assertStringWidth(t, "", 0) + + assertStringWidth(t, "hello", 5) + assertStringWidth(t, "\x02hello", 5) + assertStringWidth(t, "\x035hello", 5) + assertStringWidth(t, "\x0305hello", 5) + assertStringWidth(t, "\x0305,0hello", 5) + assertStringWidth(t, "\x0305,09hello", 5) + + assertStringWidth(t, "\x0305,hello", 6) + assertStringWidth(t, "\x03050hello", 6) + assertStringWidth(t, "\x0305,090hello", 6) +} |