turbo-editors/turbo-corepublic Fork 0
main
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

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