1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
|
package ui
import (
"strings"
"unicode"
"github.com/gdamore/tcell/v2"
)
// FrameKind picks the box-drawing characters a frame is made of. Turbo Vision
// drew the focused window with a double line and every other one with a single
// line, which is how the eye finds the active window without any colour.
type FrameKind int
// The two frame styles.
const (
FrameSingle FrameKind = iota
FrameDouble
)
// frameRunes is one set of box-drawing characters.
type frameRunes struct {
topLeft, top, topRight rune
left, right rune
bottomLeft, bottom, bottomRight rune
}
// runesFor returns the characters a frame kind is drawn with.
func (k FrameKind) runesFor() frameRunes {
if k == FrameDouble {
return frameRunes{'╔', '═', '╗', '║', '║', '╚', '═', '╝'}
}
return frameRunes{'┌', '─', '┐', '│', '│', '└', '─', '┘'}
}
// Horizontal returns the character this frame kind draws horizontal runs with,
// which a title bar needs in order to blend into the frame around it.
func (k FrameKind) Horizontal() rune { return k.runesFor().top }
// DrawFrame draws a box around the edge of r, leaving its interior untouched.
//
// p.DrawFrame(p.Size(), ui.FrameDouble, style)
func DrawFrame(p *Painter, r Rect, kind FrameKind, style tcell.Style) {
if r.W < 2 || r.H < 2 {
return
}
runes := kind.runesFor()
p.HLine(r.X+1, r.Y, r.W-2, runes.top, style)
p.HLine(r.X+1, r.Bottom()-1, r.W-2, runes.bottom, style)
p.VLine(r.X, r.Y+1, r.H-2, runes.left, style)
p.VLine(r.Right()-1, r.Y+1, r.H-2, runes.right, style)
p.SetCell(r.X, r.Y, runes.topLeft, style)
p.SetCell(r.Right()-1, r.Y, runes.topRight, style)
p.SetCell(r.X, r.Bottom()-1, runes.bottomLeft, style)
p.SetCell(r.Right()-1, r.Bottom()-1, runes.bottomRight, style)
}
// ShadowWidth and ShadowHeight are how far a window's shadow falls: two cells
// to the right, one below. Terminal cells are about twice as tall as they are
// wide, so this is what looks square.
const (
ShadowWidth = 2
ShadowHeight = 1
)
// DrawShadow dims the cells to the bottom right of r, as a window casts a
// shadow onto the desktop behind it. It must be called before the window
// itself is drawn, since it restyles whatever is already there.
func DrawShadow(p *Painter, r Rect, style tcell.Style) {
p.Shade(Rect{X: r.Right(), Y: r.Y + ShadowHeight, W: ShadowWidth, H: r.H}, style)
p.Shade(Rect{X: r.X + ShadowWidth, Y: r.Bottom(), W: r.W - ShadowWidth, H: ShadowHeight}, style)
}
// HotKeyMarker surrounds the letter a label can be reached by, the way Turbo
// Vision wrote "~F~ile" for a File menu opened with Alt-F.
const HotKeyMarker = '~'
// SplitHotKey takes a label written with tilde markers and returns the text to
// draw, the rune the label answers to, and where that rune sits in the text.
//
// A label with no marker has no hot key, and the index is then -1.
//
// text, key, at := ui.SplitHotKey("~F~ile") // "File", 'f', 0
func SplitHotKey(label string) (text string, key rune, index int) {
var out []rune
index = -1
runes := []rune(label)
for i := 0; i < len(runes); i++ {
if runes[i] != HotKeyMarker {
out = append(out, runes[i])
continue
}
// The marker before the hot letter is the one that names it; the
// closing marker just disappears.
if index < 0 && i+1 < len(runes) {
index = len(out)
key = unicode.ToLower(runes[i+1])
}
}
return string(out), key, index
}
// PlainLabel returns a label with its hot-key markers removed, which is what
// measuring the width of a label needs.
func PlainLabel(label string) string {
return strings.ReplaceAll(label, string(HotKeyMarker), "")
}
// LabelWidth returns how many cells a label occupies once drawn.
func LabelWidth(label string) int {
return len([]rune(PlainLabel(label)))
}
// DrawLabel draws a label, showing its hot key in a style of its own, and
// returns the column just after the last character.
func DrawLabel(p *Painter, x, y int, label string, normal, hot tcell.Style) int {
text, _, index := SplitHotKey(label)
for i, r := range []rune(text) {
style := normal
if i == index {
style = hot
}
p.SetCell(x+i, y, r, style)
}
return x + len([]rune(text))
}
// MatchesHotKey reports whether a key press selects the given label.
// The comparison ignores case, as Alt-F and Alt-f are the same request.
func MatchesHotKey(label string, key rune) bool {
_, hot, index := SplitHotKey(label)
return index >= 0 && hot == unicode.ToLower(key)
}
// Scroll-bar characters: a dotted track, a solid thumb, and the arrows at
// either end that Turbo Vision let you click on.
const (
scrollTrackRune = '░'
scrollThumbRune = '▓'
arrowUpRune = '▲'
arrowDownRune = '▼'
arrowLeftRune = '◄'
arrowRightRune = '►'
)
// DrawVScrollBar draws a vertical scroll bar down a one-cell-wide column.
//
// position is the first visible line, span how many lines are visible and
// total how many there are. A bar with nothing to scroll is drawn as an empty
// track rather than hidden, so the window's edge does not change shape.
func DrawVScrollBar(p *Painter, x, y, height, position, span, total int, track, thumb tcell.Style) {
if height < 2 {
return
}
p.SetCell(x, y, arrowUpRune, thumb)
p.SetCell(x, y+height-1, arrowDownRune, thumb)
trackHeight := height - 2
if trackHeight <= 0 {
return
}
p.VLine(x, y+1, trackHeight, scrollTrackRune, track)
p.SetCell(x, y+1+thumbOffset(position, span, total, trackHeight), scrollThumbRune, thumb)
}
// DrawHScrollBar draws a horizontal scroll bar along a one-cell-tall row.
func DrawHScrollBar(p *Painter, x, y, width, position, span, total int, track, thumb tcell.Style) {
if width < 2 {
return
}
p.SetCell(x, y, arrowLeftRune, thumb)
p.SetCell(x+width-1, y, arrowRightRune, thumb)
trackWidth := width - 2
if trackWidth <= 0 {
return
}
p.HLine(x+1, y, trackWidth, scrollTrackRune, track)
p.SetCell(x+1+thumbOffset(position, span, total, trackWidth), y, scrollThumbRune, thumb)
}
// thumbOffset returns where along a track of the given length the thumb sits.
//
// It is clamped to the track, so a position past the end — which happens for a
// moment while the view is being resized — cannot draw outside the bar.
func thumbOffset(position, span, total, trackLength int) int {
scrollable := total - span
if scrollable <= 0 || trackLength <= 1 {
return 0
}
offset := position * (trackLength - 1) / scrollable
return min(max(offset, 0), trackLength-1)
}
|