label.go raw
1 package gel
2
3 import (
4 "fmt"
5 "image"
6 "image/color"
7 "unicode/utf8"
8
9 l "github.com/p9c/gio/layout"
10 "github.com/p9c/gio/op"
11 "github.com/p9c/gio/op/clip"
12 "github.com/p9c/gio/op/paint"
13 "github.com/p9c/gio/text"
14 "github.com/p9c/gio/unit"
15 "golang.org/x/image/math/fixed"
16 )
17
18 // Label is text drawn inside an empty box
19 type Label struct {
20 *Window
21 // Face defines the text style.
22 font text.Font
23 // Color is the text color.
24 color color.NRGBA
25 // Alignment specify the text alignment.
26 alignment text.Alignment
27 // MaxLines limits the number of lines. Zero means no limit.
28 maxLines int
29 text string
30 textSize unit.Value
31 shaper text.Shaper
32 }
33
34 // screenPos describes a character position (in text line and column numbers,
35 // not pixels): Y = line number, X = rune column.
36 type screenPos image.Point
37
38 type segmentIterator struct {
39 Lines []text.Line
40 Clip image.Rectangle
41 Alignment text.Alignment
42 Width int
43 Offset image.Point
44 startSel screenPos
45 endSel screenPos
46
47 pos screenPos // current position
48 line text.Line // current line
49 layout text.Layout // current line's Layout
50
51 // pixel positions
52 off fixed.Point26_6
53 y, prevDesc fixed.Int26_6
54 }
55
56 func (l *segmentIterator) Next() (text.Layout, image.Point, bool, int, image.Point, bool) {
57 for l.pos.Y < len(l.Lines) {
58 if l.pos.X == 0 {
59 l.line = l.Lines[l.pos.Y]
60
61 // Calculate X & Y pixel coordinates of left edge of line. We need y
62 // for the next line, so it's in l, but we only need x here, so it's
63 // not.
64 x := align(l.Alignment, l.line.Width, l.Width) + fixed.I(l.Offset.X)
65 l.y += l.prevDesc + l.line.Ascent
66 l.prevDesc = l.line.Descent
67 // Align baseline and line start to the pixel grid.
68 l.off = fixed.Point26_6{X: fixed.I(x.Floor()), Y: fixed.I(l.y.Ceil())}
69 l.y = l.off.Y
70 l.off.Y += fixed.I(l.Offset.Y)
71 if (l.off.Y + l.line.Bounds.Min.Y).Floor() > l.Clip.Max.Y {
72 break
73 }
74
75 if (l.off.Y + l.line.Bounds.Max.Y).Ceil() < l.Clip.Min.Y {
76 // This line is outside/before the clip area; go on to the next line.
77 l.pos.Y++
78 continue
79 }
80
81 // Copy the line's Layout, since we slice it up later.
82 l.layout = l.line.Layout
83
84 // Find the left edge of the text visible in the l.Clip clipping
85 // area.
86 for len(l.layout.Advances) > 0 {
87 _, n := utf8.DecodeRuneInString(l.layout.Text)
88 adv := l.layout.Advances[0]
89 if (l.off.X + adv + l.line.Bounds.Max.X - l.line.Width).Ceil() >= l.Clip.Min.X {
90 break
91 }
92 l.off.X += adv
93 l.layout.Text = l.layout.Text[n:]
94 l.layout.Advances = l.layout.Advances[1:]
95 l.pos.X++
96 }
97 }
98
99 selected := l.inSelection()
100 endx := l.off.X
101 rune := 0
102 nextLine := true
103 retLayout := l.layout
104 for n := range l.layout.Text {
105 selChanged := selected != l.inSelection()
106 beyondClipEdge := (endx + l.line.Bounds.Min.X).Floor() > l.Clip.Max.X
107 if selChanged || beyondClipEdge {
108 retLayout.Advances = l.layout.Advances[:rune]
109 retLayout.Text = l.layout.Text[:n]
110 if selChanged {
111 // Save the rest of the line
112 l.layout.Advances = l.layout.Advances[rune:]
113 l.layout.Text = l.layout.Text[n:]
114 nextLine = false
115 }
116 break
117 }
118 endx += l.layout.Advances[rune]
119 rune++
120 l.pos.X++
121 }
122 offFloor := image.Point{X: l.off.X.Floor(), Y: l.off.Y.Floor()}
123
124 // Calculate the width & height if the returned text.
125 //
126 // If there's a better way to do this, I'm all ears.
127 var d fixed.Int26_6
128 for _, adv := range retLayout.Advances {
129 d += adv
130 }
131 size := image.Point{
132 X: d.Ceil(),
133 Y: (l.line.Ascent + l.line.Descent).Ceil(),
134 }
135
136 if nextLine {
137 l.pos.Y++
138 l.pos.X = 0
139 } else {
140 l.off.X = endx
141 }
142
143 return retLayout, offFloor, selected, l.prevDesc.Ceil() - size.Y, size, true
144 }
145 return text.Layout{}, image.Point{}, false, 0, image.Point{}, false
146 }
147
148 func (l *segmentIterator) inSelection() bool {
149 return l.startSel.LessOrEqual(l.pos) &&
150 l.pos.Less(l.endSel)
151 }
152
153 func (p1 screenPos) LessOrEqual(p2 screenPos) bool {
154 return p1.Y < p2.Y || (p1.Y == p2.Y && p1.X <= p2.X)
155 }
156
157 func (p1 screenPos) Less(p2 screenPos) bool {
158 return p1.Y < p2.Y || (p1.Y == p2.Y && p1.X < p2.X)
159 }
160
161 // Fn renders the label as specified
162 func (l *Label) Fn(gtx l.Context) l.Dimensions {
163 cs := gtx.Constraints
164 textSize := fixed.I(gtx.Px(l.textSize))
165 lines := l.shaper.LayoutString(l.font, textSize, cs.Max.X, l.text)
166 if max := l.maxLines; max > 0 && len(lines) > max {
167 lines = lines[:max]
168 }
169 dims := linesDimens(lines)
170 dims.Size = cs.Constrain(dims.Size)
171 cl := textPadding(lines)
172 cl.Max = cl.Max.Add(dims.Size)
173 it := segmentIterator{
174 Lines: lines,
175 Clip: cl,
176 Alignment: l.alignment,
177 Width: dims.Size.X,
178 }
179 for {
180 lb, off, _, _, _, ok := it.Next()
181 if !ok {
182 break
183 }
184 stack := op.Save(gtx.Ops)
185 op.Offset(Fpt(off)).Add(gtx.Ops)
186 l.shaper.Shape(l.font, textSize, lb).Add(gtx.Ops)
187 clip.Rect(cl.Sub(off)).Add(gtx.Ops)
188 paint.ColorOp{Color: l.color}.Add(gtx.Ops)
189 paint.PaintOp{}.Add(gtx.Ops)
190 stack.Load()
191 }
192 return dims
193 }
194
195 func textPadding(lines []text.Line) (padding image.Rectangle) {
196 if len(lines) == 0 {
197 return
198 }
199 first := lines[0]
200 if d := first.Ascent + first.Bounds.Min.Y; d < 0 {
201 padding.Min.Y = d.Ceil()
202 }
203 last := lines[len(lines)-1]
204 if d := last.Bounds.Max.Y - last.Descent; d > 0 {
205 padding.Max.Y = d.Ceil()
206 }
207 if d := first.Bounds.Min.X; d < 0 {
208 padding.Min.X = d.Ceil()
209 }
210 if d := first.Bounds.Max.X - first.Width; d > 0 {
211 padding.Max.X = d.Ceil()
212 }
213 return
214 }
215
216 func linesDimens(lines []text.Line) l.Dimensions {
217 var width fixed.Int26_6
218 var h int
219 var baseline int
220 if len(lines) > 0 {
221 baseline = lines[0].Ascent.Ceil()
222 var prevDesc fixed.Int26_6
223 for _, l := range lines {
224 h += (prevDesc + l.Ascent).Ceil()
225 prevDesc = l.Descent
226 if l.Width > width {
227 width = l.Width
228 }
229 }
230 h += lines[len(lines)-1].Descent.Ceil()
231 }
232 w := width.Ceil()
233 return l.Dimensions{
234 Size: image.Point{
235 X: w,
236 Y: h,
237 },
238 Baseline: h - baseline,
239 }
240 }
241
242 func align(align text.Alignment, width fixed.Int26_6, maxWidth int) fixed.Int26_6 {
243 mw := fixed.I(maxWidth)
244 switch align {
245 case text.Middle:
246 return fixed.I(((mw - width) / 2).Floor())
247 case text.End:
248 return fixed.I((mw - width).Floor())
249 case text.Start:
250 return 0
251 default:
252 panic(fmt.Errorf("unknown alignment %v", align))
253 }
254 }
255
256 // ScaleType is a map of the set of label txsizes
257 type ScaleType map[string]float32
258
259 // Scales is the ratios against
260 //
261 // TODO: shouldn't that 16.0 be the text size in the theme?
262 var Scales = ScaleType{
263 "H1": 96.0 / 16.0,
264 "H2": 60.0 / 16.0,
265 "H3": 48.0 / 16.0,
266 "H4": 34.0 / 16.0,
267 "H5": 24.0 / 16.0,
268 "H6": 20.0 / 16.0,
269 "Body1": 1,
270 "Body2": 14.0 / 16.0,
271 "Caption": 12.0 / 16.0,
272 }
273
274 // Label creates a label that prints a block of text
275 func (w *Window) Label() (l *Label) {
276 var f text.Font
277 var e error
278 var fon text.Font
279 if fon, e = w.Theme.collection.Font("plan9"); !E.Chk(e) {
280 f = fon
281 }
282 return &Label{
283 Window: w,
284 text: "",
285 font: f,
286 color: w.Colors.GetNRGBAFromName("DocText"),
287 textSize: unit.Sp(1),
288 shaper: w.shaper,
289 }
290 }
291
292 // Text sets the text to render in the label
293 func (l *Label) Text(text string) *Label {
294 l.text = text
295 return l
296 }
297
298 // TextScale sets the size of the text relative to the base font size
299 func (l *Label) TextScale(scale float32) *Label {
300 l.textSize = l.Theme.TextSize.Scale(scale)
301 return l
302 }
303
304 // MaxLines sets the maximum number of lines to render
305 func (l *Label) MaxLines(maxLines int) *Label {
306 l.maxLines = maxLines
307 return l
308 }
309
310 // Alignment sets the text alignment, left, right or centered
311 func (l *Label) Alignment(alignment text.Alignment) *Label {
312 l.alignment = alignment
313 return l
314 }
315
316 // Color sets the color of the label font
317 func (l *Label) Color(color string) *Label {
318 l.color = l.Theme.Colors.GetNRGBAFromName(color)
319 return l
320 }
321
322 // Font sets the font out of the available font collection
323 func (l *Label) Font(font string) *Label {
324 var e error
325 var fon text.Font
326 if fon, e = l.Theme.collection.Font(font); !E.Chk(e) {
327 l.font = fon
328 }
329 return l
330 }
331
332 // H1 header 1
333 func (w *Window) H1(txt string) (l *Label) {
334 l = w.Label().TextScale(Scales["H1"]).Font("plan9").Text(txt)
335 return
336 }
337
338 // H2 header 2
339 func (w *Window) H2(txt string) (l *Label) {
340 l = w.Label().TextScale(Scales["H2"]).Font("plan9").Text(txt)
341 return
342 }
343
344 // H3 header 3
345 func (w *Window) H3(txt string) (l *Label) {
346 l = w.Label().TextScale(Scales["H3"]).Font("plan9").Text(txt)
347 return
348 }
349
350 // H4 header 4
351 func (w *Window) H4(txt string) (l *Label) {
352 l = w.Label().TextScale(Scales["H4"]).Font("plan9").Text(txt)
353 return
354 }
355
356 // H5 header 5
357 func (w *Window) H5(txt string) (l *Label) {
358 l = w.Label().TextScale(Scales["H5"]).Font("plan9").Text(txt)
359 return
360 }
361
362 // H6 header 6
363 func (w *Window) H6(txt string) (l *Label) {
364 l = w.Label().TextScale(Scales["H6"]).Font("plan9").Text(txt)
365 return
366 }
367
368 // Body1 normal body text 1
369 func (w *Window) Body1(txt string) (l *Label) {
370 l = w.Label().TextScale(Scales["Body1"]).Font("bariol regular").Text(txt)
371 return
372 }
373
374 // Body2 normal body text 2
375 func (w *Window) Body2(txt string) (l *Label) {
376 l = w.Label().TextScale(Scales["Body2"]).Font("bariol regular").Text(txt)
377 return
378 }
379
380 // Caption caption text
381 func (w *Window) Caption(txt string) (l *Label) {
382 l = w.Label().TextScale(Scales["Caption"]).Font("bariol regular").Text(txt)
383 return
384 }
385