-
Notifications
You must be signed in to change notification settings - Fork 70
/
text.go
1463 lines (1347 loc) · 44.5 KB
/
text.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
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright (c) 2018, The GoKi Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gi
import (
"bytes"
"encoding/xml"
"errors"
"fmt"
"image"
"image/color"
"io"
"math"
"strings"
"sync"
"log"
"unicode"
"unicode/utf8"
"github.com/chewxy/math32"
"github.com/goki/gi/oswin"
"github.com/goki/gi/units"
"github.com/goki/ki"
"github.com/goki/ki/bitflag"
"github.com/goki/ki/kit"
"github.com/goki/prof"
"golang.org/x/image/draw"
"golang.org/x/image/font"
"golang.org/x/image/math/f64"
"golang.org/x/net/html/charset"
)
// text.go contains all the core text rendering and formatting code -- see
// font.go for basic font-level style and management
//
// Styling, Formatting / Layout, and Rendering are each handled separately as
// three different levels in the stack -- simplifies many things to separate
// in this way, and makes the final render pass maximally efficient and
// high-performance, at the potential cost of some memory redundancy.
////////////////////////////////////////////////////////////////////////////////////////
// RuneRender
// RuneRender contains fully explicit data needed for rendering a single rune
// -- Face and Color can be nil after first element, in which case the last
// non-nil is used -- likely slightly more efficient to avoid setting all
// those pointers -- float32 values used to support better accuracy when
// transforming points
type RuneRender struct {
Face font.Face `desc:"fully-specified font rendering info, includes fully computed font size -- this is exactly what will be drawn -- no further transforms"`
Color color.Color `desc:"color to draw characters in"`
BgColor color.Color `desc:"background color to fill background of color -- for highlighting, <mark> tag, etc -- unlike Face, Color, this must be non-nil for every case that uses it, as nil is also used for default transparent background"`
Deco TextDecorations `desc:"additional decoration to apply -- underline, strike-through, etc -- also used for encoding a few special layout hints to pass info from styling tags to separate layout algorithms (e.g., <P> vs <BR>)"`
RelPos Vec2D `desc:"relative position from start of TextRender for the lower-left baseline rendering position of the font character"`
Size Vec2D `desc:"size of the rune itself, exclusive of spacing that might surround it"`
RotRad float32 `desc:"rotation in radians for this character, relative to its lower-left baseline rendering position"`
ScaleX float32 `desc:"scaling of the X dimension, in case of non-uniform scaling, 0 = no separate scaling"`
}
// HasNil returns error if any of the key info (face, color) is nil -- only
// the first element must be non-nil
func (rr *RuneRender) HasNil() error {
if rr.Face == nil {
return errors.New("gi.RuneRender: Face is nil")
}
if rr.Color == nil {
return errors.New("gi.RuneRender: Color is nil")
}
// note: BgColor can be nil -- transparent
return nil
}
// CurFace is convenience for updating current font face if non-nil
func (rr *RuneRender) CurFace(curFace font.Face) font.Face {
if rr.Face != nil {
return rr.Face
}
return curFace
}
// CurColor is convenience for updating current color if non-nil
func (rr *RuneRender) CurColor(curColor color.Color) color.Color {
if rr.Color != nil {
return rr.Color
}
return curColor
}
// RelPosAfterLR returns the relative position after given rune for LR order: RelPos.X + Size.X
func (rr *RuneRender) RelPosAfterLR() float32 {
return rr.RelPos.X + rr.Size.X
}
// RelPosAfterRL returns the relative position after given rune for RL order: RelPos.X - Size.X
func (rr *RuneRender) RelPosAfterRL() float32 {
return rr.RelPos.X - rr.Size.X
}
// RelPosAfterTB returns the relative position after given rune for TB order: RelPos.Y + Size.Y
func (rr *RuneRender) RelPosAfterTB() float32 {
return rr.RelPos.Y + rr.Size.Y
}
//////////////////////////////////////////////////////////////////////////////////
// SpanRender
// SpanRender contains fully explicit data needed for rendering a span of text
// as a slice of runes, with rune and RuneRender elements in one-to-one
// correspondence (but any nil values will use prior non-nil value -- first
// rune must have all non-nil). Text can be oriented in any direction -- the
// only constraint is that it starts from a single starting position.
// Typically only text within a span will obey kerning. In standard
// TextRender context, each span is one line of text -- should not have new
// lines within the span itself. In SVG special cases (e.g., TextPath), it
// can be anything. It is NOT synonymous with the HTML <span> tag, as many
// styling applications of that tag can be accommodated within a larger
// span-as-line. The first RuneRender RelPos for LR text should be at X=0
// (LastPos = 0 for RL) -- i.e., relpos positions are minimal for given span.
type SpanRender struct {
Text []rune `desc:"text as runes"`
Render []RuneRender `desc:"render info for each rune in one-to-one correspondence"`
RelPos Vec2D `desc:"position for start of text relative to an absolute coordinate that is provided at the time of rendering -- individual rune RelPos are added to this plus the render-time offset to get the final position"`
LastPos Vec2D `desc:"rune position for further edge of last rune -- for standard flat strings this is the overall length of the string -- used for size / layout computations"`
Dir TextDirections `desc:"where relevant, this is the (default, dominant) text direction for the span"`
HasDeco TextDecorations `desc:"mask of decorations that have been set on this span -- optimizes rendering passes"`
}
// Init initializes a new span with given capacity
func (sr *SpanRender) Init(capsz int) {
sr.Text = make([]rune, 0, capsz)
sr.Render = make([]RuneRender, 0, capsz)
sr.HasDeco = 0
}
// IsValid ensures that at least some text is represented and the sizes of
// Text and Render slices are the same, and that the first render info is non-nil
func (sr *SpanRender) IsValid() error {
if len(sr.Text) == 0 {
return errors.New("gi.TextRender: Text is empty")
}
if len(sr.Text) != len(sr.Render) {
return fmt.Errorf("gi.TextRender: Render length %v != Text length %v for text: %v", len(sr.Render), len(sr.Text), string(sr.Text))
}
return sr.Render[0].HasNil()
}
// SizeHV computes the size of the text span from the first char to the last
// position, which is valid for purely horizontal or vertical text lines --
// either X or Y will be zero depending on orientation
func (sr *SpanRender) SizeHV() Vec2D {
if sr.IsValid() != nil {
return Vec2D{}
}
sz := sr.Render[0].RelPos.Sub(sr.LastPos)
if sz.X < 0 {
sz.X = -sz.X
}
if sz.Y < 0 {
sz.Y = -sz.Y
}
return sz
}
// AppendRune adds one rune and associated formatting info
func (sr *SpanRender) HasDecoUpdate(bg color.Color, deco TextDecorations) {
sr.HasDeco |= deco
if bg != nil {
bitflag.Set32((*int32)(&sr.HasDeco), int(DecoBgColor))
}
}
// IsNewPara returns true if this span starts a new paragraph
func (sr *SpanRender) IsNewPara() bool {
if len(sr.Render) == 0 {
return false
}
return bitflag.Has32(int32(sr.Render[0].Deco), int(DecoParaStart))
}
// SetNewPara sets this as starting a new paragraph
func (sr *SpanRender) SetNewPara() {
if len(sr.Render) > 0 {
bitflag.Set32((*int32)(&sr.Render[0].Deco), int(DecoParaStart))
}
}
// AppendRune adds one rune and associated formatting info
func (sr *SpanRender) AppendRune(r rune, face font.Face, clr, bg color.Color, deco TextDecorations) {
sr.Text = append(sr.Text, r)
rr := RuneRender{Face: face, Color: clr, BgColor: bg, Deco: deco}
sr.Render = append(sr.Render, rr)
sr.HasDecoUpdate(bg, deco)
}
// AppendString adds string and associated formatting info, optimized with
// only first rune having non-nil face and color settings
func (sr *SpanRender) AppendString(str string, face font.Face, clr, bg color.Color, deco TextDecorations, sty *FontStyle, ctxt *units.Context) {
if len(str) == 0 {
return
}
ucfont := FontStyle{}
if oswin.TheApp.Platform() == oswin.MacOS {
ucfont.Family = "Arial Unicode"
} else {
ucfont.Family = "Arial"
}
ucfont.Size = sty.Size
ucfont.LoadFont(ctxt)
nwr := []rune(str)
sz := len(nwr)
sr.Text = append(sr.Text, nwr...)
rr := RuneRender{Face: face, Color: clr, BgColor: bg, Deco: deco}
r := nwr[0]
lastUc := false
if r > 0xFF && unicode.IsSymbol(r) {
rr.Face = ucfont.Face
lastUc = true
}
sr.HasDecoUpdate(bg, deco)
sr.Render = append(sr.Render, rr)
for i := 1; i < sz; i++ { // optimize by setting rest to nil for same
rp := RuneRender{Deco: deco, BgColor: bg}
r := nwr[i]
if oswin.TheApp.Platform() == oswin.MacOS {
if r > 0xFF && unicode.IsSymbol(r) {
if !lastUc {
rp.Face = ucfont.Face
lastUc = true
}
} else {
if lastUc {
rp.Face = face
lastUc = false
}
}
}
sr.Render = append(sr.Render, rp)
}
}
// SetRenders sets rendering parameters based on style
func (sr *SpanRender) SetRenders(sty *FontStyle, ctxt *units.Context, noBG bool, rot, scalex float32) {
sz := len(sr.Text)
if sz == 0 {
return
}
bgc := (color.Color)(&sty.BgColor.Color)
if noBG {
bgc = nil
}
ucfont := FontStyle{}
ucfont.Family = "Arial Unicode"
ucfont.Size = sty.Size
ucfont.LoadFont(ctxt)
sr.HasDecoUpdate(bgc, sty.Deco)
sr.Render = make([]RuneRender, sz)
sr.Render[0].Face = sty.Face
sr.Render[0].Color = sty.Color
sr.Render[0].BgColor = bgc
sr.Render[0].RotRad = rot
sr.Render[0].ScaleX = scalex
if bgc != nil {
for i := range sr.Text {
sr.Render[i].BgColor = bgc
}
}
if rot != 0 || scalex != 0 {
for i := range sr.Text {
sr.Render[i].RotRad = rot
sr.Render[i].ScaleX = scalex
}
}
if sty.Deco != DecoNone {
for i := range sr.Text {
sr.Render[i].Deco = sty.Deco
}
}
// use unicode font for all non-ascii symbols
lastUc := false
for i, r := range sr.Text {
if r > 0xFF && unicode.IsSymbol(r) {
if !lastUc {
sr.Render[i].Face = ucfont.Face
lastUc = true
}
} else {
if lastUc {
sr.Render[i].Face = sty.Face
lastUc = false
}
}
}
}
// SetString initializes to given plain text string, with given default style
// parameters that are set for the first render element -- constructs Render
// slice of same size as Text
func (sr *SpanRender) SetString(str string, sty *FontStyle, ctxt *units.Context, noBG bool, rot, scalex float32) {
sr.Text = []rune(str)
sr.SetRenders(sty, ctxt, noBG, rot, scalex)
}
// SetRunes initializes to given plain rune string, with given default style
// arameters that are set for the first render element -- constructs Render
// slice of same size as Text
func (sr *SpanRender) SetRunes(str []rune, sty *FontStyle, ctxt *units.Context, noBG bool, rot, scalex float32) {
sr.Text = str
sr.SetRenders(sty, ctxt, noBG, rot, scalex)
}
// this mutex is required because multiple different goroutines associated
// with different windows can (and often will be) call curFace.GyphAdvance at
// the same time, on the same font face -- and that turns out not to work!
var glyphAdvanceMu sync.Mutex
// SetRunePosLR sets relative positions of each rune using a flat
// left-to-right text layout, based on font size info and additional extra
// letter and word spacing parameters (which can be negative)
func (sr *SpanRender) SetRunePosLR(letterSpace, wordSpace float32) {
if err := sr.IsValid(); err != nil {
// log.Println(err)
return
}
sr.Dir = LRTB
sz := len(sr.Text)
prevR := rune(-1)
lspc := letterSpace
wspc := wordSpace
var fpos float32
curFace := sr.Render[0].Face
glyphAdvanceMu.Lock()
defer glyphAdvanceMu.Unlock()
for i, r := range sr.Text {
rr := &(sr.Render[i])
curFace = rr.CurFace(curFace)
fht := curFace.Metrics().Height
if prevR >= 0 {
fpos += FixedToFloat32(curFace.Kern(prevR, r))
}
rr.RelPos.X = fpos
rr.RelPos.Y = 0
if bitflag.Has32(int32(rr.Deco), int(DecoSuper)) {
rr.RelPos.Y = -0.45 * FixedToFloat32(curFace.Metrics().Ascent)
}
if bitflag.Has32(int32(rr.Deco), int(DecoSub)) {
rr.RelPos.Y = 0.15 * FixedToFloat32(curFace.Metrics().Ascent)
}
a, ok := curFace.GlyphAdvance(r)
if !ok {
// TODO: is falling back on the U+FFFD glyph the responsibility of
// the Drawer or the Face?
// TODO: set prevC = '\ufffd'?
continue
}
a32 := FixedToFloat32(a)
rr.Size = Vec2D{a32, FixedToFloat32(fht)}
fpos += a32
if i < sz-1 {
fpos += lspc
if unicode.IsSpace(r) {
fpos += wspc
}
}
prevR = r
}
sr.LastPos.X = fpos
sr.LastPos.Y = 0
}
// FindWrapPosLR finds a position to do word wrapping to fit within trgSize --
// RelPos positions must have already been set (e.g., SetRunePosLR)
func (sr *SpanRender) FindWrapPosLR(trgSize, curSize float32) int {
sz := len(sr.Text)
if sz == 0 {
return -1
}
idx := int(float32(sz) * (trgSize / curSize))
if idx >= sz {
idx = sz - 1
}
for idx > 0 && !unicode.IsSpace(sr.Text[idx]) {
idx--
}
csz := sr.RelPos.X + sr.Render[idx].RelPos.X
lstgoodi := -1
if csz <= trgSize {
lstgoodi = idx
}
for {
if csz > trgSize {
for idx > 0 {
if unicode.IsSpace(sr.Text[idx]) {
csz = sr.RelPos.X + sr.Render[idx].RelPos.X
if csz <= trgSize {
return idx
}
}
idx--
}
return -1 // oops.
} else { // too small, go up
for idx < sz {
if unicode.IsSpace(sr.Text[idx]) {
csz = sr.RelPos.X + sr.Render[idx].RelPos.X
if csz <= trgSize {
if csz == trgSize {
return idx
}
lstgoodi = idx
} else if lstgoodi > 0 {
return lstgoodi
} else {
break // go back down
}
}
idx++
}
return lstgoodi
}
}
return -1
}
// ZeroPos ensures that the positions start at 0, for LR direction
func (sr *SpanRender) ZeroPosLR() {
sz := len(sr.Text)
if sz == 0 {
return
}
sx := sr.Render[0].RelPos.X
if sx == 0 {
return
}
for i, _ := range sr.Render {
sr.Render[i].RelPos.X -= sx
}
sr.LastPos.X -= sx
}
// TrimSpaceLeft trims leading space elements from span, and updates the
// relative positions accordingly, for LR direction
func (sr *SpanRender) TrimSpaceLeftLR() {
srr0 := sr.Render[0]
for range sr.Text {
if unicode.IsSpace(sr.Text[0]) {
sr.Text = sr.Text[1:]
sr.Render = sr.Render[1:]
if sr.Render[0].Face == nil {
sr.Render[0].Face = srr0.Face
}
if sr.Render[0].Color == nil {
sr.Render[0].Color = srr0.Color
}
} else {
break
}
}
sr.ZeroPosLR()
}
// TrimSpaceRight trims trailing space elements from span, and updates the
// relative positions accordingly, for LR direction
func (sr *SpanRender) TrimSpaceRightLR() {
for range sr.Text {
lidx := len(sr.Text) - 1
if unicode.IsSpace(sr.Text[lidx]) {
sr.Text = sr.Text[:lidx]
sr.Render = sr.Render[:lidx]
lidx--
if lidx >= 0 {
sr.LastPos.X = sr.Render[lidx].RelPosAfterLR()
} else {
sr.LastPos.X = sr.Render[0].Size.X
}
} else {
break
}
}
}
// TrimSpace trims leading and trailing space elements from span, and updates
// the relative positions accordingly, for LR direction
func (sr *SpanRender) TrimSpaceLR() {
sr.TrimSpaceLeftLR()
sr.TrimSpaceRightLR()
}
// SplitAt splits current span at given index, returning a new span with
// remainder after index -- space is trimmed from both spans and relative
// positions updated, for LR direction
func (sr *SpanRender) SplitAtLR(idx int) *SpanRender {
if idx <= 0 || idx >= len(sr.Text)-1 { // shouldn't happen
return nil
}
nsr := SpanRender{Text: sr.Text[idx:], Render: sr.Render[idx:], Dir: sr.Dir, HasDeco: sr.HasDeco}
sr.Text = sr.Text[:idx]
sr.Render = sr.Render[:idx]
sr.LastPos.X = sr.Render[idx-1].RelPosAfterLR()
// sr.TrimSpaceLR()
nsr.TrimSpaceLR()
// go back and find latest face and color -- each sr must start with valid one
nrr0 := &(nsr.Render[0])
face, color := sr.LastFont()
if nrr0.Face == nil {
nrr0.Face = face
}
if nrr0.Color == nil {
nrr0.Color = color
}
return &nsr
}
// LastFont finds the last font and color from given span
func (sr *SpanRender) LastFont() (face font.Face, color color.Color) {
for i := len(sr.Render) - 1; i >= 0; i-- {
srr := sr.Render[i]
if face == nil && srr.Face != nil {
face = srr.Face
if face != nil && color != nil {
break
}
}
if color == nil && srr.Color != nil {
color = srr.Color
if face != nil && color != nil {
break
}
}
}
return
}
// todo: TB, RL cases -- layout is complicated.. with unicode-bidi, direction,
// writing-mode styles all interacting: https://www.w3.org/TR/SVG11/text.html#TextLayout
//////////////////////////////////////////////////////////////////////////////////
// TextLink
// TextLink represents a hyperlink within rendered text
type TextLink struct {
Label string `desc:"text label for the link"`
URL string `desc:"full URL for the link"`
Props ki.Props `desc:"proerties defined for the link"`
StartSpan int `desc:"span index where link starts"`
StartIdx int `desc:"index in StartSpan where link starts"`
EndSpan int `desc:"span index where link ends (can be same as EndSpan)"`
EndIdx int `desc:"index in EndSpan where link ends (index of last rune in label)"`
}
// Bounds returns the bounds of the link
func (tl *TextLink) Bounds(tr *TextRender, pos Vec2D) image.Rectangle {
stsp := &tr.Spans[tl.StartSpan]
tpos := pos.Add(stsp.RelPos)
sr := &(stsp.Render[tl.StartIdx])
sp := tpos.Add(sr.RelPos)
sp.Y -= sr.Size.Y
ep := sp
if tl.EndSpan == tl.StartSpan {
er := &(stsp.Render[tl.EndIdx])
ep = tpos.Add(er.RelPos)
ep.X += er.Size.X
} else {
er := &(stsp.Render[len(stsp.Render)-1])
ep = tpos.Add(er.RelPos)
ep.X += er.Size.X
}
return image.Rectangle{Min: sp.ToPointFloor(), Max: ep.ToPointCeil()}
}
//////////////////////////////////////////////////////////////////////////////////
// TextRender
// TextRender contains one or more SpanRender elements, typically with each
// representing a separate line of text (but they can be anything).
type TextRender struct {
Spans []SpanRender
Size Vec2D `desc:"last size of overall rendered text"`
Dir TextDirections `desc:"where relevant, this is the (default, dominant) text direction for the span"`
Links []TextLink `desc:"hyperlinks within rendered text"`
}
// InsertSpan inserts a new span at given index
func (tr *TextRender) InsertSpan(at int, ns *SpanRender) {
sz := len(tr.Spans)
tr.Spans = append(tr.Spans, SpanRender{})
if at > sz-1 {
tr.Spans[sz] = *ns
return
}
copy(tr.Spans[at+1:], tr.Spans[at:])
tr.Spans[at] = *ns
}
// Render does text rendering into given image, within given bounds, at given
// absolute position offset (specifying position of text baseline) -- any
// applicable transforms (aside from the char-specific rotation in Render)
// must be applied in advance in computing the relative positions of the
// runes, and the overall font size, etc. todo: does not currently support
// stroking, only filling of text -- probably need to grab path from font and
// use paint rendering for stroking
func (tr *TextRender) Render(rs *RenderState, pos Vec2D) {
pr := prof.Start("RenderText")
defer pr.End()
rs.BackupPaint()
defer rs.RestorePaint()
rs.PushXForm(Identity2D()) // needed for SVG
defer rs.PopXForm()
rs.XForm = Identity2D()
for _, sr := range tr.Spans {
if sr.IsValid() != nil {
continue
}
curFace := sr.Render[0].Face
curColor := sr.Render[0].Color
tpos := pos.Add(sr.RelPos)
d := &font.Drawer{
Dst: rs.Image,
Src: image.NewUniform(curColor),
Face: curFace,
}
// todo: cache flags if these are actually needed
if bitflag.Has32(int32(sr.HasDeco), int(DecoBgColor)) {
sr.RenderBg(rs, tpos)
}
if bitflag.HasAny32(int32(sr.HasDeco), int(DecoUnderline), int(DecoDottedUnderline)) {
sr.RenderUnderline(rs, tpos)
}
if bitflag.Has32(int32(sr.HasDeco), int(DecoOverline)) {
sr.RenderLine(rs, tpos, DecoOverline, 1.1)
}
for i, r := range sr.Text {
rr := &(sr.Render[i])
curFace = rr.CurFace(curFace)
dsc32 := FixedToFloat32(curFace.Metrics().Descent)
rp := tpos.Add(rr.RelPos)
scx := float32(1)
if rr.ScaleX != 0 {
scx = rr.ScaleX
}
tx := Scale2D(scx, 1).Rotate(rr.RotRad)
ll := rp.Add(tx.TransformVectorVec2D(Vec2D{0, dsc32}))
ur := ll.Add(tx.TransformVectorVec2D(Vec2D{rr.Size.X, -rr.Size.Y}))
if int(math32.Floor(ll.X)) > rs.Bounds.Max.X || int(math32.Floor(ur.Y)) > rs.Bounds.Max.Y ||
int(math32.Ceil(ur.X)) < rs.Bounds.Min.X || int(math32.Ceil(ll.Y)) < rs.Bounds.Min.Y {
continue
}
if rr.Color != nil {
curColor = rr.Color
d.Src = image.NewUniform(curColor)
}
d.Face = curFace
d.Dot = rp.Fixed()
dr, mask, maskp, _, ok := d.Face.Glyph(d.Dot, r)
if !ok {
// fmt.Printf("not ok rendering rune: %v\n", string(r))
continue
}
idr := dr.Intersect(rs.Bounds)
soff := idr.Min.Sub(dr.Min)
if rr.RotRad == 0 && (rr.ScaleX == 0 || rr.ScaleX == 1) {
draw.DrawMask(d.Dst, idr, d.Src, soff, mask, maskp, draw.Over)
} else {
srect := dr.Sub(dr.Min)
dbase := Vec2D{rp.X - float32(dr.Min.X), rp.Y - float32(dr.Min.Y)}
transformer := draw.BiLinear
fx, fy := float32(dr.Min.X), float32(dr.Min.Y)
m := Translate2D(fx+dbase.X, fy+dbase.Y).Scale(scx, 1).Rotate(rr.RotRad).Translate(-dbase.X, -dbase.Y)
s2d := f64.Aff3{float64(m.XX), float64(m.XY), float64(m.X0), float64(m.YX), float64(m.YY), float64(m.Y0)}
transformer.Transform(d.Dst, s2d, d.Src, srect, draw.Over, &draw.Options{
SrcMask: mask,
SrcMaskP: maskp,
})
}
}
if bitflag.Has32(int32(sr.HasDeco), int(DecoLineThrough)) {
sr.RenderLine(rs, tpos, DecoLineThrough, 0.25)
}
}
}
// RenderBg renders the background behind chars
func (sr *SpanRender) RenderBg(rs *RenderState, tpos Vec2D) {
curFace := sr.Render[0].Face
didLast := false
// first := true
pc := &rs.Paint
for i := range sr.Text {
rr := &(sr.Render[i])
if rr.BgColor == nil {
if didLast {
pc.Fill(rs)
}
didLast = false
continue
}
curFace = rr.CurFace(curFace)
dsc32 := FixedToFloat32(curFace.Metrics().Descent)
rp := tpos.Add(rr.RelPos)
scx := float32(1)
if rr.ScaleX != 0 {
scx = rr.ScaleX
}
tx := Scale2D(scx, 1).Rotate(rr.RotRad)
ll := rp.Add(tx.TransformVectorVec2D(Vec2D{0, dsc32}))
ur := ll.Add(tx.TransformVectorVec2D(Vec2D{rr.Size.X, -rr.Size.Y}))
if int(math32.Floor(ll.X)) > rs.Bounds.Max.X || int(math32.Floor(ur.Y)) > rs.Bounds.Max.Y ||
int(math32.Ceil(ur.X)) < rs.Bounds.Min.X || int(math32.Ceil(ll.Y)) < rs.Bounds.Min.Y {
if didLast {
pc.Fill(rs)
}
didLast = false
continue
}
pc.FillStyle.Color.SetColor(rr.BgColor)
szt := Vec2D{rr.Size.X, -rr.Size.Y}
sp := rp.Add(tx.TransformVectorVec2D(Vec2D{0, dsc32}))
ul := sp.Add(tx.TransformVectorVec2D(Vec2D{0, szt.Y}))
lr := sp.Add(tx.TransformVectorVec2D(Vec2D{szt.X, 0}))
pc.DrawPolygon(rs, []Vec2D{sp, ul, ur, lr})
didLast = true
}
if didLast {
pc.Fill(rs)
}
}
// RenderUnderline renders the underline for span -- ensures continuity to do it all at once
func (sr *SpanRender) RenderUnderline(rs *RenderState, tpos Vec2D) {
curFace := sr.Render[0].Face
curColor := sr.Render[0].Color
didLast := false
pc := &rs.Paint
for i := range sr.Text {
rr := &(sr.Render[i])
if !bitflag.HasAny32(int32(rr.Deco), int(DecoUnderline), int(DecoDottedUnderline)) {
if didLast {
pc.Stroke(rs)
}
didLast = false
continue
}
curFace = rr.CurFace(curFace)
dsc32 := FixedToFloat32(curFace.Metrics().Descent)
rp := tpos.Add(rr.RelPos)
scx := float32(1)
if rr.ScaleX != 0 {
scx = rr.ScaleX
}
tx := Scale2D(scx, 1).Rotate(rr.RotRad)
ll := rp.Add(tx.TransformVectorVec2D(Vec2D{0, dsc32}))
ur := ll.Add(tx.TransformVectorVec2D(Vec2D{rr.Size.X, -rr.Size.Y}))
if int(math32.Floor(ll.X)) > rs.Bounds.Max.X || int(math32.Floor(ur.Y)) > rs.Bounds.Max.Y ||
int(math32.Ceil(ur.X)) < rs.Bounds.Min.X || int(math32.Ceil(ll.Y)) < rs.Bounds.Min.Y {
if didLast {
pc.Stroke(rs)
}
continue
}
if rr.Color != nil {
curColor = rr.Color
}
dw := .05 * rr.Size.Y
if !didLast {
pc.StrokeStyle.Width.Dots = dw
pc.StrokeStyle.Color.SetColor(curColor)
}
if bitflag.Has32(int32(rr.Deco), int(DecoDottedUnderline)) {
pc.StrokeStyle.Dashes = []float64{float64(dw), float64(dw)}
}
sp := rp.Add(tx.TransformVectorVec2D(Vec2D{0, 2 * dw}))
ep := rp.Add(tx.TransformVectorVec2D(Vec2D{rr.Size.X, 2 * dw}))
if didLast {
pc.LineTo(rs, sp.X, sp.Y)
} else {
pc.NewSubPath(rs)
pc.MoveTo(rs, sp.X, sp.Y)
}
pc.LineTo(rs, ep.X, ep.Y)
didLast = true
}
if didLast {
pc.Stroke(rs)
}
pc.StrokeStyle.Dashes = nil
}
// RenderLine renders overline or line-through -- anything that is a function of ascent
func (sr *SpanRender) RenderLine(rs *RenderState, tpos Vec2D, deco TextDecorations, ascPct float32) {
curFace := sr.Render[0].Face
curColor := sr.Render[0].Color
didLast := false
pc := &rs.Paint
for i := range sr.Text {
rr := &(sr.Render[i])
if !bitflag.Has32(int32(rr.Deco), int(deco)) {
if didLast {
pc.Stroke(rs)
}
didLast = false
continue
}
curFace = rr.CurFace(curFace)
dsc32 := FixedToFloat32(curFace.Metrics().Descent)
asc32 := FixedToFloat32(curFace.Metrics().Ascent)
rp := tpos.Add(rr.RelPos)
scx := float32(1)
if rr.ScaleX != 0 {
scx = rr.ScaleX
}
tx := Scale2D(scx, 1).Rotate(rr.RotRad)
ll := rp.Add(tx.TransformVectorVec2D(Vec2D{0, dsc32}))
ur := ll.Add(tx.TransformVectorVec2D(Vec2D{rr.Size.X, -rr.Size.Y}))
if int(math32.Floor(ll.X)) > rs.Bounds.Max.X || int(math32.Floor(ur.Y)) > rs.Bounds.Max.Y ||
int(math32.Ceil(ur.X)) < rs.Bounds.Min.X || int(math32.Ceil(ll.Y)) < rs.Bounds.Min.Y {
if didLast {
pc.Stroke(rs)
}
continue
}
if rr.Color != nil {
curColor = rr.Color
}
dw := 0.05 * rr.Size.Y
if !didLast {
pc.StrokeStyle.Width.Dots = dw
pc.StrokeStyle.Color.SetColor(curColor)
}
yo := ascPct * asc32
sp := rp.Add(tx.TransformVectorVec2D(Vec2D{0, -yo}))
ep := rp.Add(tx.TransformVectorVec2D(Vec2D{rr.Size.X, -yo}))
if didLast {
pc.LineTo(rs, sp.X, sp.Y)
} else {
pc.NewSubPath(rs)
pc.MoveTo(rs, sp.X, sp.Y)
}
pc.LineTo(rs, ep.X, ep.Y)
didLast = true
}
if didLast {
pc.Stroke(rs)
}
}
// RenderTopPos renders at given top position -- uses first font info to
// compute baseline offset and calls overall Render -- convenience for simple
// widget rendering without layouts
func (tr *TextRender) RenderTopPos(rs *RenderState, tpos Vec2D) {
if len(tr.Spans) == 0 {
return
}
sr := &(tr.Spans[0])
if sr.IsValid() != nil {
return
}
curFace := sr.Render[0].Face
pos := tpos
pos.Y += FixedToFloat32(curFace.Metrics().Ascent)
tr.Render(rs, pos)
}
// SetString is for basic text rendering with a single style of text (see
// SetHTML for tag-formatted text) -- configures a single SpanRender with the
// entire string, and does standard layout (LR currently). rot and scalex are
// general rotation and x-scaling to apply to all chars -- alternatively can
// apply these per character after. Be sure that LoadFont has been run so a
// valid Face is available. noBG ignores any BgColor in font style, and never
// renders background color
func (tr *TextRender) SetString(str string, fontSty *FontStyle, ctxt *units.Context, txtSty *TextStyle, noBG bool, rot, scalex float32) {
if len(tr.Spans) != 1 {
tr.Spans = make([]SpanRender, 1)
}
tr.Links = nil
sr := &(tr.Spans[0])
sr.SetString(str, fontSty, ctxt, noBG, rot, scalex)
sr.SetRunePosLR(txtSty.LetterSpacing.Dots, txtSty.WordSpacing.Dots)
ssz := sr.SizeHV()
vht := fontSty.Face.Metrics().Height
tr.Size = Vec2D{ssz.X, FixedToFloat32(vht)}
}
// SetRunes is for basic text rendering with a single style of text (see
// SetHTML for tag-formatted text) -- configures a single SpanRender with the
// entire string, and does standard layout (LR currently). rot and scalex are
// general rotation and x-scaling to apply to all chars -- alternatively can
// apply these per character after Be sure that LoadFont has been run so a
// valid Face is available. noBG ignores any BgColor in font style, and never
// renders background color
func (tr *TextRender) SetRunes(str []rune, fontSty *FontStyle, ctxt *units.Context, txtSty *TextStyle, noBG bool, rot, scalex float32) {
if len(tr.Spans) != 1 {
tr.Spans = make([]SpanRender, 1)
}
tr.Links = nil
sr := &(tr.Spans[0])
sr.SetRunes(str, fontSty, ctxt, noBG, rot, scalex)
sr.SetRunePosLR(txtSty.LetterSpacing.Dots, txtSty.WordSpacing.Dots)
ssz := sr.SizeHV()
vht := fontSty.Face.Metrics().Height
tr.Size = Vec2D{ssz.X, FixedToFloat32(vht)}
}
// SetHTML sets text by decoding all standard inline HTML text style
// formatting tags in the string and sets the per-character font information
// appropriately, using given font style info. <P> and <BR> tags create new
// spans, with <P> marking start of subsequent span with DecoParaStart.
// Critically, it does NOT deal at all with layout (positioning) -- only sets
// font, color, and decoration info, and strips out the tags it processes --
// result can then be processed by different layout algorithms as needed.
// cssAgg, if non-nil, should contain CSSAgg properties -- will be tested for
// special css styling of each element
func (tr *TextRender) SetHTML(str string, font *FontStyle, ctxt *units.Context, cssAgg ki.Props) {
sz := len(str)
if sz == 0 {
return
}
tr.Spans = make([]SpanRender, 1)
tr.Links = nil
curSp := &(tr.Spans[0])
initsz := kit.MinInt(sz, 1020)
curSp.Init(initsz)
spcstr := bytes.Join(bytes.Fields([]byte(str)), []byte(" "))
reader := bytes.NewReader(spcstr)
decoder := xml.NewDecoder(reader)
decoder.Strict = false
decoder.AutoClose = xml.HTMLAutoClose
decoder.Entity = xml.HTMLEntity
decoder.CharsetReader = charset.NewReaderLabel
font.LoadFont(ctxt)
// set when a </p> is encountered
nextIsParaStart := false
curLinkIdx := -1 // if currently processing an <a> link element
fstack := make([]*FontStyle, 1, 10)
fstack[0] = font
for {
t, err := decoder.Token()
if err != nil {
if err == io.EOF {
break
}
log.Printf("gi.TextRender DecodeHTML parsing error: %v\n", err)
break
}
switch se := t.(type) {
case xml.StartElement:
curf := fstack[len(fstack)-1]
fs := *curf
nm := strings.ToLower(se.Name.Local)
curLinkIdx = -1
// https://www.w3schools.com/cssref/css_default_values.asp
switch nm {
case "b", "strong":
fs.Weight = WeightBold
fs.LoadFont(ctxt)
case "i", "em", "var", "cite":
fs.Style = FontItalic
fs.LoadFont(ctxt)
case "ins":
fallthrough
case "u":
fs.SetDeco(DecoUnderline)
case "a":
fs.Color.SetColor(Prefs.Colors.Link)
fs.SetDeco(DecoUnderline)
curLinkIdx = len(tr.Links)
tl := &TextLink{StartSpan: len(tr.Spans) - 1, StartIdx: len(curSp.Text)}
sprop := make(ki.Props, len(se.Attr))
tl.Props = sprop
for _, attr := range se.Attr {
if attr.Name.Local == "href" {
tl.URL = attr.Value
}