master sewn/kohai / ui / draw_utils.go
  1package ui
  2
  3import (
  4	"fmt"
  5	"strings"
  6	"sync"
  7	"time"
  8
  9	"git.sr.ht/~rockorager/vaxis"
 10	"github.com/delthas/go-localeinfo"
 11	"github.com/rivo/uniseg"
 12)
 13
 14var asciiStringCache []string
 15
 16var FormattingChars = map[rune]rune{
 17	'*': '\x02', // bold
 18	'^': '\x03', // custom color (terminal color, 0-99)
 19	'#': '\x04', // custom color (hexadecimal color)
 20	'-': '\x0F', // reset all attributes
 21	'`': '\x11', // monospace text
 22	'|': '\x16', // reverse color
 23	'_': '\x1D', // italics
 24	'~': '\x1E', // strikethrough
 25	'=': '\x1F', // underline
 26}
 27
 28// allow for reverse lookup
 29var formattingRunes map[rune]rune
 30
 31func init() {
 32	formattingRunes = make(map[rune]rune, len(FormattingChars))
 33	for c, r := range FormattingChars {
 34		formattingRunes[r] = c
 35	}
 36
 37	asciiStringCache = make([]string, 0x80)
 38	for i := range asciiStringCache {
 39		asciiStringCache[i] = string(rune(i))
 40
 41		if f, ok := formattingRunes[rune(i)]; ok {
 42			asciiStringCache[i] = string(f)
 43		}
 44	}
 45}
 46
 47var runeWidthMap = make(map[rune]int)
 48
 49func runeWidth(vx *Vaxis, r rune) int {
 50	if vx == nil { // For tests only
 51		return 1
 52	}
 53	if r == '\n' {
 54		r = '↲'
 55	}
 56	if r <= 0x1F {
 57		if f, ok := formattingRunes[r]; ok {
 58			r = f
 59			return 1
 60		}
 61		return 0
 62	}
 63	if r <= 0x7F {
 64		return 1
 65	}
 66	if n, ok := runeWidthMap[r]; ok {
 67		return n
 68	}
 69	n := vx.RenderedWidth(string([]rune{r}))
 70	runeWidthMap[r] = n
 71	return n
 72}
 73
 74func stringWidth(vx *Vaxis, s string) int {
 75	if vx == nil { // For tests only
 76		return len(s)
 77	}
 78	if len(s) == 1 { // Single-character ASCII fast path
 79		if s[0] == '\n' { // Replaced with ↲
 80			return 1
 81		}
 82		if s[0] <= 0x1F {
 83			if _, ok := formattingRunes[rune(s[0])]; ok {
 84				return 1
 85			}
 86			return 0
 87		}
 88		if s[0] <= 0x7F {
 89			return 1
 90		}
 91	}
 92	r := []rune(s)
 93	if len(r) == 1 { // Single-character fast path
 94		return runeWidth(vx, r[0])
 95	}
 96	return vx.RenderedWidth(s)
 97}
 98
 99func truncate(vx *Vaxis, s string, w int, tail string) string {
100	if stringWidth(vx, s) <= w {
101		return s
102	}
103	w -= stringWidth(vx, tail)
104
105	width := 0
106	var sb strings.Builder
107	for _, c := range vaxis.Characters(s) {
108		chWidth := stringWidth(vx, c.Grapheme)
109		if width+chWidth > w {
110			break
111		}
112		width += chWidth
113		sb.WriteString(c.Grapheme)
114	}
115	sb.WriteString(tail)
116	return sb.String()
117}
118
119var clusterWidthMap = make(map[string]int)
120
121// width in cells
122func firstCluster(vx *Vaxis, r []rune) (c string, width int) {
123	if len(r) == 0 { // Empty fast-path
124		return "", 0
125	}
126	if r[0] == '\t' {
127		return " ", 1
128	}
129	if r[0] <= 0x7F { // ASCII fast-path
130		return asciiStringCache[int(r[0])], runeWidth(vx, r[0])
131	}
132	c, _, _, _ = uniseg.FirstGraphemeClusterInString(string(r), -1)
133
134	var cw int
135	if n, ok := clusterWidthMap[c]; ok {
136		cw = n
137	} else {
138		cw = stringWidth(vx, c)
139		clusterWidthMap[c] = cw
140	}
141	return c, cw
142}
143
144func setCell(vx *Vaxis, x int, y int, r rune, st vaxis.Style) {
145	vx.window.SetCell(x, y, vaxis.Cell{
146		Character: vaxis.Character{
147			Grapheme: string([]rune{r}),
148		},
149		Style: st,
150	})
151}
152
153// limit = -1 means no limit
154// di is an offset in runes
155func printCluster(vx *Vaxis, x int, y int, limit int, r []rune, st vaxis.Style) (dx int, di int) {
156	if limit >= 0 && x >= limit {
157		return 0, 0
158	}
159	var c string
160	var w int
161	if len(r) > 0 && r[0] <= 0x7F { // ASCII fast-path
162		c = asciiStringCache[int(r[0])]
163		w = runeWidth(vx, r[0])
164		di = 1
165	} else {
166		c, w = firstCluster(vx, r)
167		di = len([]rune(c))
168	}
169	if limit >= 0 && w > limit-x {
170		return 0, 0
171	}
172	vx.window.SetCell(x, y, vaxis.Cell{
173		Character: vaxis.Character{
174			Grapheme: c,
175		},
176		Style: st,
177	})
178	return w, di
179}
180
181func printString(vx *Vaxis, x *int, y int, s StyledString) {
182	var st vaxis.Style
183	nextStyles := s.styles
184
185	i := 0
186	sr := []rune(s.string)
187	for len(sr) > 0 {
188		if 0 < len(nextStyles) && nextStyles[0].Start == i {
189			st = nextStyles[0].Style
190			nextStyles = nextStyles[1:]
191		}
192		dx, di := printCluster(vx, *x, y, -1, sr, st)
193		*x += dx
194		i += len(string(sr[:di]))
195		sr = sr[di:]
196	}
197}
198
199func printIdent(vx *Vaxis, x, y, width int, s StyledString) (xb int, xe int) {
200	s.string = truncate(vx, s.string, width, "\u2026")
201	x += width - stringWidth(vx, s.string)
202	var st vaxis.Style
203	if len(s.styles) != 0 && s.styles[0].Start == 0 {
204		st = s.styles[0].Style
205	}
206	setCell(vx, x-1, y, ' ', st)
207	xb = x
208	printString(vx, &x, y, s)
209	if len(s.styles) != 0 {
210		// TODO check if it's not a style that is from the truncated
211		// part of s.
212		st = s.styles[len(s.styles)-1].Style
213	}
214	setCell(vx, x, y, ' ', st)
215	xe = x
216	return
217}
218
219func printNumber(vx *Vaxis, x *int, y int, st vaxis.Style, n int) {
220	s := Styled(fmt.Sprintf("%d", n), st)
221	printString(vx, x, y, s)
222}
223
224var dateConfig sync.Once
225var dateMonthFirst bool
226
227func loadDateInfo() {
228	// Very overkill, but I like it :)
229	// Try to extract from the user locale whether they'd rather have the date
230	// printed as dd/mm or mm/dd.
231	// If we're not sure, print dd/mm.
232	l, err := localeinfo.NewLocale("")
233	if err != nil {
234		return
235	}
236	format := l.DateFormat()
237	dayIndex := -1
238	for _, s := range []string{"%d", "%e"} {
239		dayIndex = strings.Index(format, s)
240		if dayIndex >= 0 {
241			break
242		}
243	}
244	if dayIndex == -1 {
245		return
246	}
247	monthIndex := -1
248	for _, s := range []string{"%m", "%b", "%B"} {
249		monthIndex = strings.Index(format, s)
250		if monthIndex >= 0 {
251			break
252		}
253	}
254	if monthIndex == -1 {
255		return
256	}
257	if monthIndex < dayIndex {
258		dateMonthFirst = true
259	}
260}
261
262func printDate(vx *Vaxis, x int, y int, st vaxis.Style, t time.Time) {
263	dateConfig.Do(loadDateInfo)
264	_, m, d := t.Date()
265	var left, right int
266	if dateMonthFirst {
267		left, right = int(m), d
268	} else {
269		left, right = d, int(m)
270	}
271	l0 := rune(left/10) + '0'
272	l1 := rune(left%10) + '0'
273	r0 := rune(right/10) + '0'
274	r1 := rune(right%10) + '0'
275
276	setCell(vx, x+0, y, l0, st)
277	setCell(vx, x+1, y, l1, st)
278	setCell(vx, x+2, y, '/', st)
279	setCell(vx, x+3, y, r0, st)
280	setCell(vx, x+4, y, r1, st)
281}
282
283func printTime(vx *Vaxis, x int, y int, st vaxis.Style, t time.Time) {
284	hr0 := rune(t.Hour()/10) + '0'
285	hr1 := rune(t.Hour()%10) + '0'
286	mn0 := rune(t.Minute()/10) + '0'
287	mn1 := rune(t.Minute()%10) + '0'
288	setCell(vx, x+0, y, hr0, st)
289	setCell(vx, x+1, y, hr1, st)
290	setCell(vx, x+2, y, ':', st)
291	setCell(vx, x+3, y, mn0, st)
292	setCell(vx, x+4, y, mn1, st)
293}
294
295func clearArea(vx *Vaxis, x0, y0, width, height int) {
296	vx.window.New(x0, y0, width, height).Clear()
297}