text.go raw
1 package gel
2
3 import (
4 "image"
5 "unicode/utf8"
6
7 l "github.com/p9c/gio/layout"
8 "github.com/p9c/gio/op"
9 "github.com/p9c/gio/op/clip"
10 "github.com/p9c/gio/op/paint"
11 "github.com/p9c/gio/text"
12 "github.com/p9c/gio/unit"
13
14 "golang.org/x/image/math/fixed"
15 )
16
17 // Text is a widget for laying out and drawing text.
18 type Text struct {
19 *Window
20 // alignment specify the text alignment.
21 alignment text.Alignment
22 // maxLines limits the number of lines. Zero means no limit.
23 maxLines int
24 }
25
26 func (w *Window) Text() *Text {
27 return &Text{Window: w}
28 }
29
30 // Alignment sets the alignment for the text
31 func (t *Text) Alignment(alignment text.Alignment) *Text {
32 t.alignment = alignment
33 return t
34 }
35
36 // MaxLines sets the alignment for the text
37 func (t *Text) MaxLines(maxLines int) *Text {
38 t.maxLines = maxLines
39 return t
40 }
41
42 type lineIterator struct {
43 Lines []text.Line
44 Clip image.Rectangle
45 Alignment text.Alignment
46 Width int
47 Offset image.Point
48
49 y, prevDesc fixed.Int26_6
50 txtOff int
51 }
52
53 func (l *lineIterator) Next() (text.Layout, image.Point, bool) {
54 for len(l.Lines) > 0 {
55 line := l.Lines[0]
56 l.Lines = l.Lines[1:]
57 x := align(l.Alignment, line.Width, l.Width) + fixed.I(l.Offset.X)
58 l.y += l.prevDesc + line.Ascent
59 l.prevDesc = line.Descent
60 // Align baseline and line start to the pixel grid.
61 off := fixed.Point26_6{X: fixed.I(x.Floor()), Y: fixed.I(l.y.Ceil())}
62 l.y = off.Y
63 off.Y += fixed.I(l.Offset.Y)
64 if (off.Y + line.Bounds.Min.Y).Floor() > l.Clip.Max.Y {
65 break
66 }
67 lo := line.Layout
68 start := l.txtOff
69 l.txtOff += len(line.Layout.Text)
70 if (off.Y + line.Bounds.Max.Y).Ceil() < l.Clip.Min.Y {
71 continue
72 }
73 for len(lo.Advances) > 0 {
74 _, n := utf8.DecodeRuneInString(lo.Text)
75 adv := lo.Advances[0]
76 if (off.X + adv + line.Bounds.Max.X - line.Width).Ceil() >= l.Clip.Min.X {
77 break
78 }
79 off.X += adv
80 lo.Text = lo.Text[n:]
81 lo.Advances = lo.Advances[1:]
82 start += n
83 }
84 end := start
85 endx := off.X
86 rn := 0
87 for n, r := range lo.Text {
88 if (endx + line.Bounds.Min.X).Floor() > l.Clip.Max.X {
89 lo.Advances = lo.Advances[:rn]
90 lo.Text = lo.Text[:n]
91 break
92 }
93 end += utf8.RuneLen(r)
94 endx += lo.Advances[rn]
95 rn++
96 }
97 offf := image.Point{X: off.X.Floor(), Y: off.Y.Floor()}
98 return lo, offf, true
99 }
100 return text.Layout{}, image.Point{}, false
101 }
102
103 // func linesDimens(lines []text.Line) l.Dimensions {
104 // var width fixed.Int26_6
105 // var h int
106 // var baseline int
107 // if len(lines) > 0 {
108 // baseline = lines[0].Ascent.Ceil()
109 // var prevDesc fixed.Int26_6
110 // for _, l := range lines {
111 // h += (prevDesc + l.Ascent).Ceil()
112 // prevDesc = l.Descent
113 // if l.Width > width {
114 // width = l.Width
115 // }
116 // }
117 // h += lines[len(lines)-1].Descent.Ceil()
118 // }
119 // w := width.Ceil()
120 // return l.Dimensions{
121 // Size: image.Point{
122 // X: w,
123 // Y: h,
124 // },
125 // Baseline: h - baseline,
126 // }
127 // }
128
129 func (t *Text) Fn(gtx l.Context, s text.Shaper, font text.Font, size unit.Value, txt string) l.Dimensions {
130 cs := gtx.Constraints
131 textSize := fixed.I(gtx.Px(size))
132 lines := s.LayoutString(font, textSize, cs.Max.X, txt)
133 if max := t.maxLines; max > 0 && len(lines) > max {
134 lines = lines[:max]
135 }
136 dims := linesDimens(lines)
137 dims.Size = cs.Constrain(dims.Size)
138 cl := textPadding(lines)
139 cl.Max = cl.Max.Add(dims.Size)
140 it := lineIterator{
141 Lines: lines,
142 Clip: cl,
143 Alignment: t.alignment,
144 Width: dims.Size.X,
145 }
146 for {
147 ll, off, ok := it.Next()
148 if !ok {
149 break
150 }
151 stack := op.Save(gtx.Ops)
152 op.Offset(l.FPt(off)).Add(gtx.Ops)
153 s.Shape(font, textSize, ll).Add(gtx.Ops)
154 clip.Rect(cl.Sub(off)).Add(gtx.Ops)
155 paint.PaintOp{}.Add(gtx.Ops)
156 stack.Load()
157 }
158 return dims
159 }
160
161 // func textPadding(lines []text.Line) (padding image.Rectangle) {
162 // if len(lines) == 0 {
163 // return
164 // }
165 // first := lines[0]
166 // if d := first.Ascent + first.Bounds.Min.Y; d < 0 {
167 // padding.Min.Y = d.Ceil()
168 // }
169 // last := lines[len(lines)-1]
170 // if d := last.Bounds.Max.Y - last.Descent; d > 0 {
171 // padding.Max.Y = d.Ceil()
172 // }
173 // if d := first.Bounds.Min.X; d < 0 {
174 // padding.Min.X = d.Ceil()
175 // }
176 // if d := first.Bounds.Max.X - first.Width; d > 0 {
177 // padding.Max.X = d.Ceil()
178 // }
179 // return
180 // }
181
182 // func align(align text.Alignment, width fixed.Int26_6, maxWidth int) fixed.Int26_6 {
183 // mw := fixed.I(maxWidth)
184 // switch align {
185 // case text.Middle:
186 // return fixed.I(((mw - width) / 2).Floor())
187 // case text.End:
188 // return fixed.I((mw - width).Floor())
189 // case text.Start:
190 // return 0
191 // default:
192 // panic(fmt.Errorf("unknown alignment %v", align))
193 // }
194 // }
195