-
Notifications
You must be signed in to change notification settings - Fork 0
/
label.go
319 lines (293 loc) · 10.7 KB
/
label.go
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
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
// SPDX-License-Identifier: Unlicense OR MIT
package main
import (
"image"
"image/color"
"strings"
"time"
"gioui.org/f32"
"gioui.org/font"
"gioui.org/io/semantic"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/text"
"gioui.org/unit"
"github.com/viktomas/gritty/buffer"
"golang.org/x/image/math/fixed"
)
// Label is a widget code copied from https://git.sr.ht/~eliasnaur/gio/tree/313c488ec356872a14dab0c0ac0fd73b45a596cf/item/widget/label.go
// and changed so it can render grid of characters. Each character can have
// different background and foreground colors and a few other attributes
// see (buffer.BrushedRune)
type Label struct {
// Alignment specifies the text alignment.
Alignment text.Alignment
// MaxLines limits the number of lines. Zero means no limit.
MaxLines int
// Truncator is the text that will be shown at the end of the final
// line if MaxLines is exceeded. Defaults to "…" if empty.
Truncator string
// WrapPolicy configures how displayed text will be broken into lines.
WrapPolicy text.WrapPolicy
// LineHeight controls the distance between the baselines of lines of text.
// If zero, a sensible default will be used.
LineHeight unit.Sp
// LineHeightScale applies a scaling factor to the LineHeight. If zero, a
// sensible default will be used.
LineHeightScale float32
}
type paintedGlyph struct {
g text.Glyph
r rune
fg, bg color.NRGBA
}
// Layout the label with the given shaper, font, size, text, and material.
func (l Label) Layout(gtx layout.Context, lt *text.Shaper, font font.Font, size unit.Sp, txt []buffer.BrushedRune) layout.Dimensions {
dims, _ := l.LayoutDetailed(gtx, lt, font, size, txt)
return dims
}
// TextInfo provides metadata about shaped text.
type TextInfo struct {
// Truncated contains the number of runes of text that are represented by a truncator
// symbol in the text. If zero, there is no truncator symbol.
Truncated int
}
// Layout the label with the given shaper, font, size, text, and material, returning metadata about the shaped text.
func (l Label) LayoutDetailed(gtx layout.Context, lt *text.Shaper, font font.Font, size unit.Sp, txt []buffer.BrushedRune) (layout.Dimensions, TextInfo) {
cs := gtx.Constraints
textSize := fixed.I(gtx.Sp(size))
lineHeight := fixed.I(gtx.Sp(l.LineHeight))
var str strings.Builder
for _, pr := range txt {
str.WriteRune(pr.R)
}
lt.LayoutString(text.Parameters{
Font: font,
PxPerEm: textSize,
MaxLines: l.MaxLines,
Truncator: l.Truncator,
Alignment: l.Alignment,
WrapPolicy: l.WrapPolicy,
MaxWidth: cs.Max.X,
MinWidth: cs.Min.X,
Locale: gtx.Locale,
LineHeight: lineHeight,
LineHeightScale: l.LineHeightScale,
}, str.String())
m := op.Record(gtx.Ops)
viewport := image.Rectangle{Max: cs.Max}
it := textIterator{
viewport: viewport,
maxLines: l.MaxLines,
}
semantic.LabelOp(str.String()).Add(gtx.Ops)
var paintedGlyphs [32]paintedGlyph
line := paintedGlyphs[:0]
pos := 0
for g, ok := lt.NextGlyph(); ok; g, ok = lt.NextGlyph() {
// if txt[pos].r == '\n' {
// pos++
// }
var ok bool
if line, ok = it.paintGlyph(gtx, lt, g, line, txt[pos]); !ok {
break
}
if pos+1 >= len(txt) {
// log.Printf("incrementing pos will cause problems, txt length is %v, position is %v", len(txt), pos)
} else {
pos++
}
}
call := m.Stop()
viewport.Min = viewport.Min.Add(it.padding.Min)
viewport.Max = viewport.Max.Add(it.padding.Max)
clipStack := clip.Rect(viewport).Push(gtx.Ops)
call.Add(gtx.Ops)
dims := layout.Dimensions{Size: it.bounds.Size()}
dims.Size = cs.Constrain(dims.Size)
dims.Baseline = dims.Size.Y - it.baseline
clipStack.Pop()
return dims, TextInfo{Truncated: it.truncated}
}
func r2p(r clip.Rect) clip.Op {
return clip.Stroke{Path: r.Path(), Width: 1}.Op()
}
// textIterator computes the bounding box of and paints text.
type textIterator struct {
// viewport is the rectangle of document coordinates that the iterator is
// trying to fill with text.
viewport image.Rectangle
// maxLines is the maximum number of text lines that should be displayed.
maxLines int
// truncated tracks the count of truncated runes in the text.
truncated int
// linesSeen tracks the quantity of line endings this iterator has seen.
linesSeen int
// lineOff tracks the origin for the glyphs in the current line.
lineOff f32.Point
// padding is the space needed outside of the bounds of the text to ensure no
// part of a glyph is clipped.
padding image.Rectangle
// bounds is the logical bounding box of the text.
bounds image.Rectangle
// visible tracks whether the most recently iterated glyph is visible within
// the viewport.
visible bool
// first tracks whether the iterator has processed a glyph yet.
first bool
// baseline tracks the location of the first line of text's baseline.
baseline int
}
// processGlyph checks whether the glyph is visible within the iterator's configured
// viewport and (if so) updates the iterator's text dimensions to include the glyph.
func (it *textIterator) processGlyph(g text.Glyph, ok bool) (_ text.Glyph, visibleOrBefore bool) {
if it.maxLines > 0 {
if g.Flags&text.FlagTruncator != 0 && g.Flags&text.FlagClusterBreak != 0 {
// A glyph carrying both of these flags provides the count of truncated runes.
it.truncated = g.Runes
}
if g.Flags&text.FlagLineBreak != 0 {
it.linesSeen++
}
if it.linesSeen == it.maxLines && g.Flags&text.FlagParagraphBreak != 0 {
return g, false
}
}
// Compute the maximum extent to which glyphs overhang on the horizontal
// axis.
if d := g.Bounds.Min.X.Floor(); d < it.padding.Min.X {
// If the distance between the dot and the left edge of this glyph is
// less than the current padding, increase the left padding.
it.padding.Min.X = d
}
if d := (g.Bounds.Max.X - g.Advance).Ceil(); d > it.padding.Max.X {
// If the distance between the dot and the right edge of this glyph
// minus the logical advance of this glyph is greater than the current
// padding, increase the right padding.
it.padding.Max.X = d
}
if d := (g.Bounds.Min.Y + g.Ascent).Floor(); d < it.padding.Min.Y {
// If the distance between the dot and the top of this glyph is greater
// than the ascent of the glyph, increase the top padding.
it.padding.Min.Y = d
}
if d := (g.Bounds.Max.Y - g.Descent).Ceil(); d > it.padding.Max.Y {
// If the distance between the dot and the bottom of this glyph is greater
// than the descent of the glyph, increase the bottom padding.
it.padding.Max.Y = d
}
logicalBounds := image.Rectangle{
Min: image.Pt(g.X.Floor(), int(g.Y)-g.Ascent.Ceil()),
Max: image.Pt((g.X + g.Advance).Ceil(), int(g.Y)+g.Descent.Ceil()),
}
if !it.first {
it.first = true
it.baseline = int(g.Y)
it.bounds = logicalBounds
}
above := logicalBounds.Max.Y < it.viewport.Min.Y
below := logicalBounds.Min.Y > it.viewport.Max.Y
left := logicalBounds.Max.X < it.viewport.Min.X
right := logicalBounds.Min.X > it.viewport.Max.X
it.visible = !above && !below && !left && !right
if it.visible {
it.bounds.Min.X = min(it.bounds.Min.X, logicalBounds.Min.X)
it.bounds.Min.Y = min(it.bounds.Min.Y, logicalBounds.Min.Y)
it.bounds.Max.X = max(it.bounds.Max.X, logicalBounds.Max.X)
it.bounds.Max.Y = max(it.bounds.Max.Y, logicalBounds.Max.Y)
}
return g, ok && !below
}
func fixedToFloat(i fixed.Int26_6) float32 {
return float32(i) / 64.0
}
func shouldBlinkInvert() bool {
currentTime := time.Now()
return (currentTime.UnixNano()/int64(time.Millisecond)/500)%2 == 0
}
func convertColor(c buffer.Color) color.NRGBA {
return color.NRGBA{A: 0xff, R: c.R, G: c.G, B: c.B}
}
// toPaintedGlyph transfers GUI-agnostic BrushedRune into a specific way
// we render the characters in Gio
func toPaintedGlyph(g text.Glyph, br buffer.BrushedRune) paintedGlyph {
defaultGlyph := paintedGlyph{
r: br.R,
g: g,
fg: convertColor(br.Brush.FG),
bg: convertColor(br.Brush.BG),
}
if br.Brush.Bold {
defaultGlyph.bg = color.NRGBA{A: 255, R: 0, G: 0, B: 0}
}
if br.Brush.Invert {
fg := defaultGlyph.fg
defaultGlyph.fg = defaultGlyph.bg
defaultGlyph.bg = fg
}
if br.Brush.Blink && shouldBlinkInvert() {
fg := defaultGlyph.fg
defaultGlyph.fg = defaultGlyph.bg
defaultGlyph.bg = fg
}
return defaultGlyph
}
// paintGlyph buffers up and paints text glyphs. It should be invoked iteratively upon each glyph
// until it returns false. The line parameter should be a slice with
// a backing array of sufficient size to buffer multiple glyphs.
// A modified slice will be returned with each invocation, and is
// expected to be passed back in on the following invocation.
// This design is awkward, but prevents the line slice from escaping
// to the heap.
//
// this function has been heavily modified from the original in
// https://git.sr.ht/~eliasnaur/gio/tree/313c488ec356872a14dab0c0ac0fd73b45a596cf/item/widget/label.go
// to render grid of characters where each character can have a different color
func (it *textIterator) paintGlyph(gtx layout.Context, shaper *text.Shaper, glyph text.Glyph, line []paintedGlyph, br buffer.BrushedRune) ([]paintedGlyph, bool) {
_, visibleOrBefore := it.processGlyph(glyph, true)
if it.visible {
if len(line) == 0 {
it.lineOff = f32.Point{X: fixedToFloat(glyph.X), Y: float32(glyph.Y)}.Sub(layout.FPt(it.viewport.Min))
}
// we processed the glyph and now we take parameters from the brushed rune
// these parameters are then used in the next step (after we processed the whole line)
line = append(line, toPaintedGlyph(glyph, br))
}
// this section gets executed only at the end, after we filled our line with glyphs
// by repeatedly calling the it.ProcessGlyph
if glyph.Flags&text.FlagLineBreak != 0 || cap(line)-len(line) == 0 || !visibleOrBefore {
t := op.Affine(f32.Affine2D{}.Offset(it.lineOff)).Push(gtx.Ops)
var glyphLine []text.Glyph
for _, pg := range line {
// minX is where the glyph character starts
// thanks to setting an offset, the rectangle and the glyph can be drawn from X: 0
minX := pg.g.X.Floor() - it.lineOff.Round().X
glyphOffset := op.Affine(f32.Affine2D{}.Offset(f32.Point{X: float32(minX)})).Push(gtx.Ops)
// draw background
rect := clip.Rect{
Min: image.Point{X: 0, Y: 0 - glyph.Ascent.Ceil()},
Max: image.Point{X: pg.g.X.Floor() + pg.g.Advance.Ceil() - it.lineOff.Round().X, Y: 0 + glyph.Descent.Ceil()},
}
paint.FillShape(
gtx.Ops,
pg.bg,
rect.Op(),
)
// draw glyph
path := shaper.Shape([]text.Glyph{pg.g})
outline := clip.Outline{Path: path}.Op().Push(gtx.Ops)
paint.ColorOp{Color: pg.fg}.Add(gtx.Ops)
paint.PaintOp{}.Add(gtx.Ops)
outline.Pop()
if call := shaper.Bitmaps(glyphLine); call != (op.CallOp{}) {
call.Add(gtx.Ops)
}
glyphOffset.Pop()
}
t.Pop()
line = line[:0]
}
return line, visibleOrBefore
}