label.go raw
1 // SPDX-License-Identifier: Unlicense OR MIT
2
3 package widget
4
5 import (
6 "fmt"
7 "image"
8 "unicode/utf8"
9
10 "github.com/p9c/p9/pkg/gel/gio/layout"
11 "github.com/p9c/p9/pkg/gel/gio/op"
12 "github.com/p9c/p9/pkg/gel/gio/op/clip"
13 "github.com/p9c/p9/pkg/gel/gio/op/paint"
14 "github.com/p9c/p9/pkg/gel/gio/text"
15 "github.com/p9c/p9/pkg/gel/gio/unit"
16
17 "golang.org/x/image/math/fixed"
18 )
19
20 // Label is a widget for laying out and drawing text.
21 type Label struct {
22 // Alignment specify the text alignment.
23 Alignment text.Alignment
24 // MaxLines limits the number of lines. Zero means no limit.
25 MaxLines int
26 }
27
28 // screenPos describes a character position (in text line and column numbers,
29 // not pixels): Y = line number, X = rune column.
30 type screenPos image.Point
31
32 type segmentIterator struct {
33 Lines []text.Line
34 Clip image.Rectangle
35 Alignment text.Alignment
36 Width int
37 Offset image.Point
38 startSel screenPos
39 endSel screenPos
40
41 pos screenPos // current position
42 line text.Line // current line
43 layout text.Layout // current line's Layout
44
45 // pixel positions
46 off fixed.Point26_6
47 y, prevDesc fixed.Int26_6
48 }
49
50 const inf = 1e6
51
52 func (l *segmentIterator) Next() (text.Layout, image.Point, bool, int, image.Point, bool) {
53 for l.pos.Y < len(l.Lines) {
54 if l.pos.X == 0 {
55 l.line = l.Lines[l.pos.Y]
56
57 // Calculate X & Y pixel coordinates of left edge of line. We need y
58 // for the next line, so it's in l, but we only need x here, so it's
59 // not.
60 x := align(l.Alignment, l.line.Width, l.Width) + fixed.I(l.Offset.X)
61 l.y += l.prevDesc + l.line.Ascent
62 l.prevDesc = l.line.Descent
63 // Align baseline and line start to the pixel grid.
64 l.off = fixed.Point26_6{X: fixed.I(x.Floor()), Y: fixed.I(l.y.Ceil())}
65 l.y = l.off.Y
66 l.off.Y += fixed.I(l.Offset.Y)
67 if (l.off.Y + l.line.Bounds.Min.Y).Floor() > l.Clip.Max.Y {
68 break
69 }
70
71 if (l.off.Y + l.line.Bounds.Max.Y).Ceil() < l.Clip.Min.Y {
72 // This line is outside/before the clip area; go on to the next line.
73 l.pos.Y++
74 continue
75 }
76
77 // Copy the line's Layout, since we slice it up later.
78 l.layout = l.line.Layout
79
80 // Find the left edge of the text visible in the l.Clip clipping
81 // area.
82 for len(l.layout.Advances) > 0 {
83 _, n := utf8.DecodeRuneInString(l.layout.Text)
84 adv := l.layout.Advances[0]
85 if (l.off.X + adv + l.line.Bounds.Max.X - l.line.Width).Ceil() >= l.Clip.Min.X {
86 break
87 }
88 l.off.X += adv
89 l.layout.Text = l.layout.Text[n:]
90 l.layout.Advances = l.layout.Advances[1:]
91 l.pos.X++
92 }
93 }
94
95 selected := l.inSelection()
96 endx := l.off.X
97 rune := 0
98 nextLine := true
99 retLayout := l.layout
100 for n := range l.layout.Text {
101 selChanged := selected != l.inSelection()
102 beyondClipEdge := (endx + l.line.Bounds.Min.X).Floor() > l.Clip.Max.X
103 if selChanged || beyondClipEdge {
104 retLayout.Advances = l.layout.Advances[:rune]
105 retLayout.Text = l.layout.Text[:n]
106 if selChanged {
107 // Save the rest of the line
108 l.layout.Advances = l.layout.Advances[rune:]
109 l.layout.Text = l.layout.Text[n:]
110 nextLine = false
111 }
112 break
113 }
114 endx += l.layout.Advances[rune]
115 rune++
116 l.pos.X++
117 }
118 offFloor := image.Point{X: l.off.X.Floor(), Y: l.off.Y.Floor()}
119
120 // Calculate the width & height if the returned text.
121 //
122 // If there's a better way to do this, I'm all ears.
123 var d fixed.Int26_6
124 for _, adv := range retLayout.Advances {
125 d += adv
126 }
127 size := image.Point{
128 X: d.Ceil(),
129 Y: (l.line.Ascent + l.line.Descent).Ceil(),
130 }
131
132 if nextLine {
133 l.pos.Y++
134 l.pos.X = 0
135 } else {
136 l.off.X = endx
137 }
138
139 return retLayout, offFloor, selected, l.prevDesc.Ceil() - size.Y, size, true
140 }
141 return text.Layout{}, image.Point{}, false, 0, image.Point{}, false
142 }
143
144 func (l *segmentIterator) inSelection() bool {
145 return l.startSel.LessOrEqual(l.pos) &&
146 l.pos.Less(l.endSel)
147 }
148
149 func (p1 screenPos) LessOrEqual(p2 screenPos) bool {
150 return p1.Y < p2.Y || (p1.Y == p2.Y && p1.X <= p2.X)
151 }
152
153 func (p1 screenPos) Less(p2 screenPos) bool {
154 return p1.Y < p2.Y || (p1.Y == p2.Y && p1.X < p2.X)
155 }
156
157 func (l Label) Layout(gtx layout.Context, s text.Shaper, font text.Font, size unit.Value, txt string) layout.Dimensions {
158 cs := gtx.Constraints
159 textSize := fixed.I(gtx.Px(size))
160 lines := s.LayoutString(font, textSize, cs.Max.X, txt)
161 if max := l.MaxLines; max > 0 && len(lines) > max {
162 lines = lines[:max]
163 }
164 dims := linesDimens(lines)
165 dims.Size = cs.Constrain(dims.Size)
166 cl := textPadding(lines)
167 cl.Max = cl.Max.Add(dims.Size)
168 it := segmentIterator{
169 Lines: lines,
170 Clip: cl,
171 Alignment: l.Alignment,
172 Width: dims.Size.X,
173 }
174 for {
175 l, off, _, _, _, ok := it.Next()
176 if !ok {
177 break
178 }
179 stack := op.Save(gtx.Ops)
180 op.Offset(layout.FPt(off)).Add(gtx.Ops)
181 s.Shape(font, textSize, l).Add(gtx.Ops)
182 clip.Rect(cl.Sub(off)).Add(gtx.Ops)
183 paint.PaintOp{}.Add(gtx.Ops)
184 stack.Load()
185 }
186 return dims
187 }
188
189 func textPadding(lines []text.Line) (padding image.Rectangle) {
190 if len(lines) == 0 {
191 return
192 }
193 first := lines[0]
194 if d := first.Ascent + first.Bounds.Min.Y; d < 0 {
195 padding.Min.Y = d.Ceil()
196 }
197 last := lines[len(lines)-1]
198 if d := last.Bounds.Max.Y - last.Descent; d > 0 {
199 padding.Max.Y = d.Ceil()
200 }
201 if d := first.Bounds.Min.X; d < 0 {
202 padding.Min.X = d.Ceil()
203 }
204 if d := first.Bounds.Max.X - first.Width; d > 0 {
205 padding.Max.X = d.Ceil()
206 }
207 return
208 }
209
210 func linesDimens(lines []text.Line) layout.Dimensions {
211 var width fixed.Int26_6
212 var h int
213 var baseline int
214 if len(lines) > 0 {
215 baseline = lines[0].Ascent.Ceil()
216 var prevDesc fixed.Int26_6
217 for _, l := range lines {
218 h += (prevDesc + l.Ascent).Ceil()
219 prevDesc = l.Descent
220 if l.Width > width {
221 width = l.Width
222 }
223 }
224 h += lines[len(lines)-1].Descent.Ceil()
225 }
226 w := width.Ceil()
227 return layout.Dimensions{
228 Size: image.Point{
229 X: w,
230 Y: h,
231 },
232 Baseline: h - baseline,
233 }
234 }
235
236 func align(align text.Alignment, width fixed.Int26_6, maxWidth int) fixed.Int26_6 {
237 mw := fixed.I(maxWidth)
238 switch align {
239 case text.Middle:
240 return fixed.I(((mw - width) / 2).Floor())
241 case text.End:
242 return fixed.I((mw - width).Floor())
243 case text.Start:
244 return 0
245 default:
246 panic(fmt.Errorf("unknown alignment %v", align))
247 }
248 }
249