Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4cc9f2407 |
No files matched your search
@@ -100,6 +100,7 @@ func (o *outputBuffer) flush(cells []Cell, width, height int) {
|
||||
}
|
||||
|
||||
w := &o.buf
|
||||
start := w.Len()
|
||||
|
||||
for y := 0; y < height; y++ {
|
||||
rowStart := y * width
|
||||
@@ -194,7 +195,10 @@ func (o *outputBuffer) flush(cells []Cell, width, height int) {
|
||||
}
|
||||
}
|
||||
|
||||
w.Write(csiSGR0)
|
||||
// A frame that changed no cell writes nothing, the reset included
|
||||
if w.Len() > start {
|
||||
w.Write(csiSGR0)
|
||||
}
|
||||
o.lastValid = false
|
||||
}
|
||||
|
||||
|
||||
@@ -121,6 +121,18 @@ func TestEachCellIsOneColumn(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// A frame that changes no cell writes nothing, so drawing an idle screen
|
||||
// again costs the terminal no bytes
|
||||
func TestUnchangedFrameWritesNothing(t *testing.T) {
|
||||
o := newOutputBuffer(ColorMode256)
|
||||
frame := []Cell{{Rune: 'a'}, {Rune: 'é'}}
|
||||
o.flush(frame, len(frame), 1)
|
||||
o.buf.Reset()
|
||||
if o.flush(frame, len(frame), 1); o.buf.Len() != 0 {
|
||||
t.Fatalf("unchanged frame wrote %q", o.buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
// A paste arrives as one event, whatever reads split it into: an escape
|
||||
// inside it is text even when a poll times out on it, and a paste the
|
||||
// terminal never ends ends when input pauses, whether reads time out or
|
||||
|
||||
+1
-4
@@ -149,10 +149,7 @@ func (r Region) Divider(y int, label string, line LineType, fg color.RGB) {
|
||||
text = Truncate(text, r.W-2)
|
||||
textLen = RuneLen(text)
|
||||
}
|
||||
startX := (r.W - textLen) / 2
|
||||
for i, ch := range text {
|
||||
r.Cell(startX+i, y, ch, fg, color.RGB{}, terminal.AttrBold)
|
||||
}
|
||||
r.Text((r.W-textLen)/2, y, text, fg, color.RGB{}, terminal.AttrBold)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-15
@@ -225,11 +225,7 @@ func (r Region) ConfirmDialog(state *ConfirmState, opts ConfirmOpts) Region {
|
||||
yesBg = style.ButtonFocusBg
|
||||
}
|
||||
}
|
||||
for i, ch := range yesLabel {
|
||||
if buttonX+i < content.W {
|
||||
content.Cell(buttonX+i, buttonY, ch, yesFg, yesBg, terminal.AttrNone)
|
||||
}
|
||||
}
|
||||
content.Text(buttonX, buttonY, yesLabel, yesFg, yesBg, terminal.AttrNone)
|
||||
|
||||
// No button
|
||||
noX := buttonX + yesW + buttonGap
|
||||
@@ -239,11 +235,7 @@ func (r Region) ConfirmDialog(state *ConfirmState, opts ConfirmOpts) Region {
|
||||
noFg = style.ButtonFocusFg
|
||||
noBg = style.ButtonFocusBg
|
||||
}
|
||||
for i, ch := range noLabel {
|
||||
if noX+i < content.W {
|
||||
content.Cell(noX+i, buttonY, ch, noFg, noBg, terminal.AttrNone)
|
||||
}
|
||||
}
|
||||
content.Text(noX, buttonY, noLabel, noFg, noBg, terminal.AttrNone)
|
||||
|
||||
return content.Sub(0, 0, content.W, buttonY-1)
|
||||
}
|
||||
@@ -317,11 +309,7 @@ func (r Region) AlertDialog(opts AlertOpts) Region {
|
||||
buttonW := RuneLen(buttonLabel)
|
||||
buttonX := (content.W - buttonW) / 2
|
||||
|
||||
for i, ch := range buttonLabel {
|
||||
if buttonX+i < content.W {
|
||||
content.Cell(buttonX+i, buttonY, ch, style.ButtonFocusFg, style.ButtonFocusBg, terminal.AttrNone)
|
||||
}
|
||||
}
|
||||
content.Text(buttonX, buttonY, buttonLabel, style.ButtonFocusFg, style.ButtonFocusBg, terminal.AttrNone)
|
||||
|
||||
return content.Sub(0, 0, content.W, buttonY-1)
|
||||
}
|
||||
|
||||
+1
-6
@@ -164,12 +164,7 @@ func (r Region) Form(state *FormState, opts FormOpts) int {
|
||||
|
||||
// Label
|
||||
label := field.Label + ":"
|
||||
for j, ch := range label {
|
||||
if j >= labelW || j >= r.W {
|
||||
break
|
||||
}
|
||||
r.Cell(j, y, ch, style.LabelFg, style.Bg, terminal.AttrNone)
|
||||
}
|
||||
r.Sub(0, y, labelW, 1).Text(0, 0, label, style.LabelFg, style.Bg, terminal.AttrNone)
|
||||
|
||||
// Field area
|
||||
fieldX := labelW
|
||||
|
||||
+2
-10
@@ -36,10 +36,7 @@ func (r Region) Modal(opts ModalOpts) Region {
|
||||
title = Truncate(title, r.W-4)
|
||||
titleLen = RuneLen(title)
|
||||
}
|
||||
x := (r.W - titleLen) / 2
|
||||
for i, ch := range title {
|
||||
r.Cell(x+i, 0, ch, opts.TitleFg, opts.Bg, terminal.AttrBold)
|
||||
}
|
||||
r.Text((r.W-titleLen)/2, 0, title, opts.TitleFg, opts.Bg, terminal.AttrBold)
|
||||
}
|
||||
|
||||
// Hint right-aligned on top edge
|
||||
@@ -54,12 +51,7 @@ func (r Region) Modal(opts ModalOpts) Region {
|
||||
if x < r.W/2 {
|
||||
x = r.W / 2
|
||||
}
|
||||
for i, ch := range hint {
|
||||
if x+i >= r.W-1 {
|
||||
break
|
||||
}
|
||||
r.Cell(x+i, 0, ch, opts.HintFg, opts.Bg, terminal.AttrNone)
|
||||
}
|
||||
r.Sub(0, 0, r.W-1, 1).Text(x, 0, hint, opts.HintFg, opts.Bg, terminal.AttrNone)
|
||||
}
|
||||
|
||||
// Return content region
|
||||
|
||||
+1
-7
@@ -33,13 +33,7 @@ func (r Region) Pane(opts PaneOpts) Region {
|
||||
if RuneLen(title) > r.W-4 {
|
||||
title = Truncate(title, r.W-4)
|
||||
}
|
||||
x := 2
|
||||
for i, ch := range title {
|
||||
if x+i >= r.W-1 {
|
||||
break
|
||||
}
|
||||
r.Cell(x+i, 0, ch, opts.TitleFg, opts.Bg, terminal.AttrBold)
|
||||
}
|
||||
r.Text(2, 0, title, opts.TitleFg, opts.Bg, terminal.AttrBold)
|
||||
}
|
||||
|
||||
// Content region is everything inside the border. The title sits ON the border row.
|
||||
|
||||
@@ -194,10 +194,7 @@ func (r Region) ProgressOverlay(opts ProgressOverlayOpts) Region {
|
||||
title = Truncate(title, overlayW-4)
|
||||
titleLen = RuneLen(title)
|
||||
}
|
||||
titleX := (overlayW - titleLen) / 2
|
||||
for i, ch := range title {
|
||||
overlay.Cell(titleX+i, 0, ch, opts.AccentFg, opts.Bg, terminal.AttrBold)
|
||||
}
|
||||
overlay.Text((overlayW-titleLen)/2, 0, title, opts.AccentFg, opts.Bg, terminal.AttrBold)
|
||||
}
|
||||
|
||||
// Content area
|
||||
|
||||
+1
-7
@@ -64,13 +64,7 @@ func (r Region) TabBar(y int, titles []string, active int, opts TabBarOpts) []Ta
|
||||
r.Cell(x+j, y, ' ', style.Fg, style.Bg, style.Attr)
|
||||
}
|
||||
|
||||
titleStart := x + opts.Padding
|
||||
for j, ch := range title {
|
||||
if titleStart+j >= r.W {
|
||||
break
|
||||
}
|
||||
r.Cell(titleStart+j, y, ch, style.Fg, style.Bg, style.Attr)
|
||||
}
|
||||
r.TextStyled(x+opts.Padding, y, title, style)
|
||||
|
||||
for j := 0; j < opts.Padding; j++ {
|
||||
pos := x + opts.Padding + RuneLen(title) + j
|
||||
|
||||
+1
-5
@@ -171,11 +171,7 @@ func (r Region) renderTableRow(y int, cells []string, widths []int, aligns []Ali
|
||||
startX = x
|
||||
}
|
||||
|
||||
for j, ch := range text {
|
||||
if startX+j < r.W {
|
||||
r.Cell(startX+j, y, ch, style.Fg, style.Bg, style.Attr)
|
||||
}
|
||||
}
|
||||
r.TextStyled(startX, y, text, style)
|
||||
|
||||
x += w
|
||||
|
||||
|
||||
+1
-6
@@ -108,12 +108,7 @@ func (r Region) TextField(state *TextFieldState, opts TextFieldOpts) int {
|
||||
if RuneLen(placeholder) > viewportW {
|
||||
placeholder = Truncate(placeholder, viewportW)
|
||||
}
|
||||
for i, ch := range placeholder {
|
||||
if x+i >= contentX+contentW {
|
||||
break
|
||||
}
|
||||
r.Cell(x+i, contentY, ch, style.PlaceholderFg, style.TextBg, terminal.AttrDim)
|
||||
}
|
||||
r.Text(x, contentY, placeholder, style.PlaceholderFg, style.TextBg, terminal.AttrDim)
|
||||
} else {
|
||||
// Scroll indicators
|
||||
if state.Scroll > 0 && x > contentX {
|
||||
|
||||
@@ -467,3 +467,37 @@ func TestWindowShowsTheRowsInView(t *testing.T) {
|
||||
}
|
||||
r.Sub(8, 0, 4, 3).Window(10, &v, MonoTheme.Text, content) // no columns, past the right edge
|
||||
}
|
||||
|
||||
// A label is drawn a column a rune, so one holding runes of more than a byte
|
||||
// keeps them together: each widget draws s as many times as it is given it
|
||||
func TestLabelsKeepTheirRunesTogether(t *testing.T) {
|
||||
const s = "● é · ñ"
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
n int
|
||||
draw func(r Region)
|
||||
}{
|
||||
{"pane", 1, func(r Region) { r.Pane(PaneOpts{Title: s}) }},
|
||||
{"modal", 2, func(r Region) { r.Modal(ModalOpts{Title: s, Hint: s}) }},
|
||||
{"divider", 1, func(r Region) { r.Divider(0, s, LineSingle, color.RGB{}) }},
|
||||
{"tabs", 2, func(r Region) { r.TabBar(0, []string{s, s}, 0, DefaultTabBarOpts()) }},
|
||||
{"table", 2, func(r Region) { r.Table([]string{s}, [][]string{{s}}, TableOpts{}) }},
|
||||
{"form", 1, func(r Region) { r.Form(NewFormState(s), FormOpts{}) }},
|
||||
{"field", 1, func(r Region) { r.TextField(NewTextFieldState(""), TextFieldOpts{Placeholder: s}) }},
|
||||
{"confirm", 4, func(r Region) {
|
||||
r.ConfirmDialog(NewConfirmState(true), ConfirmOpts{Title: s, Message: s, YesLabel: s, NoLabel: s})
|
||||
}},
|
||||
{"alert", 3, func(r Region) { r.AlertDialog(AlertOpts{Title: s, Message: s, Button: s}) }},
|
||||
{"progress", 1, func(r Region) { r.ProgressOverlay(DefaultProgressOpts(s, "", ProgressSpinner)) }},
|
||||
} {
|
||||
r := canvas(48, 14, MonoTheme)
|
||||
c.draw(r)
|
||||
rows := make([]string, r.H)
|
||||
for y := range rows {
|
||||
rows[y] = row(r, y)
|
||||
}
|
||||
if got := strings.Count(strings.Join(rows, "\n"), s); got != c.n {
|
||||
t.Errorf("%s: %d of %d, rows %q", c.name, got, c.n, rows)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user