File: DrawFormattedText2.m

package info (click to toggle)
psychtoolbox-3 3.0.19.14.dfsg1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 86,796 kB
  • sloc: ansic: 176,245; cpp: 20,103; objc: 5,393; sh: 2,753; python: 1,397; php: 384; makefile: 193; java: 113
file content (1646 lines) | stat: -rw-r--r-- 67,963 bytes parent folder | download
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
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
function [nx, ny, textbounds, cache, wordbounds] = DrawFormattedText2(varargin)
% [nx, ny, textbounds, cache, wordbounds] = DrawFormattedText2(tstring, key-value pairs)
% or:
% [nx, ny, textbounds, cache, wordbounds] = DrawFormattedText2(cache, key-value pairs)
% 
% When called with a string, the following key-value pairs are understood:
% win [, sx][, sy][, xalign][, yalign][, xlayout][, baseColor][, wrapat][, transform][, vSpacing][, righttoleft][, winRect][, resetStyle][, cacheOnly]
% Those enclosed in square braces are optional.
% 
% When called with a cache struct, the following optional key-value pair
% arguments are accepted:
% [, win][, sx][, sy][, xalign][, yalign][, transform][, winRect][, cacheOnly]
% 
% example call:
% DrawFormattedText2('test text', 'win',window_pointer,'baseColor',[255 0 0])
% 
% Draws a string of text 'tstring' into Psychtoolbox window 'win'. Allows
% some formatting and precise positioning. Only works with the FTGL-based
% text plugin (see help DrawTextPlugin), activate it with
% Screen('Preference','TextRenderer', 1);
% This function is _not_ made to be fast. Test carefully if you want it to
% work within a screen refresh if you throw a lot of text at it.
%
% The text string 'tstring' may contain newline characters '\n'.
% Whenever a newline character '\n' is encountered, a linefeed and
% carriage return is performed, breaking the text string into lines.
%
% The text may also contain the formatting tags listed below. Each tag
% changes the formatting of the text from that point onward. Tags do not
% need to be closed. At the exit of the function, the state of the font
% renderer (font, text size, etc) is reset to the state upon function entry
% or the state stored in the cache if drawing from cache.
% - <i>:                toggles italicization
% - <b>:                toggles bolding
% - <u>:                toggles underlining
% - <color=colorFmt>    switches to a new color
% - <font=name>         switches to a new font
% - <size=number>       switches to a new font size
% The <i>, <b> and <u> tags toggle whether text is italicized, bolded or
% underlined and remain active (possibly until the end of the input string)
% until the same <i>, <b> or <u> tag is encountered again.
% The <color>, <font> and <size> tags can be provided empty (i.e., without
% argument), in which case they cause to revert back to the color, font or
% size active before the previous switch. Multiple of these in a row go
% back further in history (until start color, font, size is reached).
% To escape a tag, prepend it with a slash, e.g., /<color>. If you want a
% slash right in front of a tag, escape it by making it a double /:
% //<color>. No other slashes should be escaped.
% <color>'s argument can have one of two formats, it can either be a
% hexadecimal string (HEX), or a comma-separated floating point array
% (FPN). If a HEX input is provided, it should encode colors using a 1, 2,
% 6 or 8 hexadecimal digit string. If 1 or 2 digits are provided, this is
% interpreted as a grayscale value. 6 hexadecimal digits are interpreted as
% R, G and B values (2 digits each). 8 digits further includes an alpha
% value. Using the HEX values, color values range from 0 to 255. Example:
% <color=ff0000> changes the text color to red (ff corresponds to 255). The
% FPN format consists of 1, 3 or 4 comma separated floating point color
% values (luminance, RGB, or RGBA) that can take on any value, but will be
% processed according to the current color settings for the window as
% indicated by Screen('ColorRange'). Typically, the values provided will
% range from 0.0--1.0 per component. Floating point values should include
% the decimal point to ensure that single element FPN values are parsed
% correctly. Examples: <color=1.,0.,0.> and <color=.5>. If a floating point
% value is specified but Screen('ColorRange') returns 255, indicating 8bit
% color values are used for the window, the floating point color values
% provided are automatically scaled and rounded to the 0-255 range. If the
% HEX format is used but Screen('ColorRange') returns 1.0, indicating
% floating point color values are used for the window, the uint8 color
% values are automatically converted to the 0.0-1.0 range.
% A size/font command before a newline can change the height of the line on
% which it occurs. So if you want to space two words 'test' and 'text'
% vertically by white space equivalent to an 80pts line, use:
% 'test\n<size=80>\n<size>text'. There is an empty line between the two new
% lines, and the size=80 says that this line has height of 80pts in the
% selected font.
%
% 'sx' and 'sy' provide a location on the screen with respect to which the
% textbox is positioned. 'sx' and 'sy' can be a pixel location provided as
% a number. Alternatively, 'sx' can also be 'left' (default), 'center', or
% 'right' signifying the left side, horizontal middle, or right side of the
% window rect. 'sy' can also be 'top' (default), 'center', or 'bottom'
% signifying the top, vertical middle, or bottom of the window rect. 
%
% 'xalign' and 'yalign' are text strings indicating how the textbox should
% be aligned to the screen location provided in 'sx' and 'sy'. For
% 'xalign', 'left' (default), signifies that the left of the text's
% bounding box is aligned to the screen location; 'center' that the
% bounding box is centered on this location; and 'right' that the right of
% the text's bounding box is aligned with this location. For 'yalign',
% 'top' (default), signifies that the top of the text's bounding box is
% aligned to the screen location; 'center' that the bounding box is
% centered on this location; and 'bottom' that the bottom of the text's
% bounding box is aligned with this location.
%
% 'xlayout' indicates how each line is positioned horizontally in the
% bounding box. If 'left' (default), all lines are aligned to the left of
% the text's bounding box; if 'center', all lines are centered in the
% bounding box; and if 'right', all lines are aligned to the right of the
% bounding box. Justification options are currently not supported.
%
% 'baseColor' is the color in which the text will be drawn (until changed
% by a <color> format call). This is also the color that will remain active
% after invocation of this function.
%
% 'wrapat', if provided, will automatically break text strings longer than
% 'wrapat' characters into newline separated strings of roughly 'wrapat'
% characters. This is done by calling the WrapString function (See 'help
% WrapString'). 'wrapat' mode may not work reliably with non-ASCII text
% strings, e.g., UTF-8 encoded uint8 strings on all systems. It also does
% not necessarily lead to lines of roughly equal length, unless using a
% monospace font.
%
% 'transform' allows transforming the text to be drawn as a whole. The
% 'transform' input is a cell array of key-value parameters, indicating
% which transform to do in which order. The following transform are
% supported:
% 'translate', [dx dy]: translates text by dx horizontally and dy
%                       vertically
% 'flip', axis        : mirrors text horizontally if axis is 1, vertically
%                       if axis is 2, and along both axes if number is 3
%                       (which is equal to a 180 deg rotation)
% 'scale', [sx sy]    : scale text horizontally by sx and vertically by sy.
%                       Set to 1 if you want no scaling.
% 'rotate', angle     : Rotate text by angle (degrees). Note that for the
%                       PTB screen, a positive rotation is clockwise.
% example {'translate',[100 0],'rotate',45} first translates text by 100
% pixels, then rotates it by 45 degree clockwise. Note that the order of
% operations is important. The above is equal to
% {'rotate',45,'translate',100*sqrt([2 2])/2}. Advanced note: for OpenGL
% transform are applied in the reverse order from how they're specified.
% That is not the case for this interface, transform are applied in the
% order specified.
%
% The optional argument 'vSpacing' sets the spacing between the lines.
% Default value is 1.
%
% The optional argument 'winRect' allows to specify a [left top right
% bottom] rectangle, in which the text should be placed etc. By default, the
% rectangle of the whole 'win'dow is used.
%
% 'resetStyle'. If true, we reset the base text style to normal before
% interpreting formatting commands that are present in the input text
% string. If not (false), active text style at function entry is taken into
% account when processing style toggle tags
%
% 'cache'. Upon invocation of the function, it provides an optional output
% 'cache'. Providing this output back to DrawFormattedText2 instead of the
% 'tstring' input allows direct drawing of the exact same text without all
% the preprocessing having to be done again, potentially saving significant
% time (and simplifying the call syntax). In this mode, a subset of the
% below arguments can be used to, e.g., draw to a different window or
% reposition the text. This cache can be generated without any actual
% drawing being done by setting the 'cacheOnly' argument to true. The cache
% is an implementation detail and is subject to change at any time.
% When drawing from cache, the text can be repositioned with the 'sx',
% 'sy', 'xalign', 'yalign' and 'winRect' inputs described above. If only
% 'sx' and 'sy' are provided, these are taken to be offsets to move the
% bounding box of the cached text. 'sx' and 'sy' must be numerical in this
% case and cannot be empty. If 'xalign' and/or 'yalign' are provided,
% full-fledged parsing of 'sx', 'sy', 'xalign' and 'yalign' is done, same
% as during a normal call to DrawFormattedText2. The bounding box is then
% repositioned according to these for inputs. The winRect argument is only
% used in this case. It is optional (defaulting to the whole windows), and
% works as described above, specifying the rect to which 'xalign' and
% 'yalign' apply. The input option 'transform' can furthermore be set,
% which appends additional transformations to what is already in the cache.
%
%
% Return variables:
%
% The function returns the new (nx, ny) position of the text drawing cursor
% and the bounding rectangle 'textbounds' of the drawn string. (nx,ny) can
% be used as new start position for connecting further text strings to the
% bottom of the drawn text string. Calculation of textbounds is
% approximative, so it may give wrong results with some text fonts and
% styles on some operating systems, depending on the various settings. The
% optional 'cache' output argument is discussed above.
%
% When rotating by angles that are not a multiple of 90 degrees, the
% bounding box may not be accurate (too large). The returned bounding box
% is the rect that tightly fits the rotated original bounding box, not the
% box that tightly fits the rotated ink of the letters.
%
% The optional return argument 'wordbounds', if assigned in the calling
% function, returns a n-by-4 matrix of per-word bounding boxes. Each row
% defines a [left,top,right,bottom] rectangle with the bounding box of a
% word in the text string, ie. row 1 = first word, row 2 = 2nd word, ...
% white-space characters delimit single words, as do style changes and line-
% feeds, and these delimiters are not taken into account for the bounding box,
% ie. they don't get their own bounding boxes. The white-space separating
% successive words is as defined by the function isspace(tstring), or by a
% change of text style, color, formatting, etc. Use of 'wordbounds' may cause
% a significant slow-down in text drawing, so only assign this return argument
% if you actually need it. A current limitation is that returned bounding boxes
% will be likely incorrect if you apply multiple transformations like 'scale'
% 'rotate', 'translate' and 'flip' at once. A single transformation will work,
% but multiple ones will cause misplaced per word bounding boxes. If you want
% to get proper 'wordbounds' when drawing text from the 'cache' then you must
% assign 'wordbounds' already in the DrawFormattedText2() invocation which
% returns the 'cache', otherwise bounding boxes might be wrong.
%
% One difference in the return values from this function and
% DrawFormattedText is that the new (nx, ny) position of the text drawing
% cursor output is the baseline of the text. So to use (nx,ny) as the new
% start position for connecting further text strings, you need to draw
% these strings with yPositionIsBaseline==true. Another difference is that
% the returned textbounds bounding box includes the height of an empty line
% at the end if the input string ended with a carriage return.
% DrawFormattedText only moved (nx,ny) but did not include the empty line
% in the bounding box. The empty line is also taken into account when
% centering text
%
%
% Further Notes:
% 
% Please note that while positioning and bounding boxes are pixel accurate
% with the fonts tested during development, i cannot guarantee this is the
% case with all fonts you throw at it. Also note that this function is not
% made to be fast. It has a make draw from cache mode so that all
% preprocessing is done only once and stored in a cache from which the text
% can be drawn directly. Nonetheless, especially if you throw a lot of
% formatting at it, this function may still not be fast in this mode. Use
% with caution in timing critical paths (test and measure to know if its
% fast enough). That said, with reasonable inputs it should manage to draw
% text to screen well within the inter frame interval of most screens.
%
% The function employs clipping by default. Text lines that are detected as
% laying completely outside the 'win'dow or optional 'winRect' will not be
% drawn, but clipped away. This allows to draw multi-page text (multiple
% screen heights) without too much loss of drawing speed. If you find the
% clipping to interfere with text layout of exotic texts/fonts at exotic
% sizes and formatting, you can define the global variable...
% global ptb_drawformattedtext2_disableClipping;
% ... and set it like this ...
% ptb_drawformattedtext2_disableClipping = 1;
% ... to disable the clipping.
%
% Regardless of the clipping setting, the optional 3rd return parameter
% 'textbounds' always covers the complete text.
%
% See DrawFormattedText2Demo for a usage example.

% TODO:
% - justification. I have included an interface, but not implemented (or
%   documented it in the help above) yet.
% - Fix per word bounding boxes (optionally grow them to be proper AOIs for
%   eye-tracking) for multiple concatenated 'Transform's.

% History:
% 2015--2017    Written (DCN).
% 7-May-2017    Add support for 'wordbounds' - per word bounding boxes (MK).
% 18-Apr-2021   Fix UTF-8 string handling WRT new Octave 6 regexp behavior (MR).


global ptb_drawformattedtext2_disableClipping;
global ptb_drawformattedtext2_padthresh;

if isempty(ptb_drawformattedtext2_disableClipping)
    % Text clipping on by default:
    ptb_drawformattedtext2_disableClipping = 0;
end
% Boundinx boxes are always for the whole text, but text out of the screen
% is culling/clipping. User can forcefully disable or enable this clipping.
disableClip = (ptb_drawformattedtext2_disableClipping ~= -1) && ...
              ((ptb_drawformattedtext2_disableClipping > 0));

if isempty(ptb_drawformattedtext2_padthresh)
    % Threshold for skipping of text justification is 33% by default:
    ptb_drawformattedtext2_padthresh = 0.333;
end
padthresh = ptb_drawformattedtext2_padthresh;

assert(Screen('Preference','TextRenderer') == 1, 'DrawFormattedText2 only works with the FTGL based text drawing plugin, but this plugin is not selected activated with Screen(''Preference'',''TextRenderer'',1), or did not load correctly. See help DrawTextPlugin for more information.');

% Optional per-word bounding boxes requested or should generate them
% because cache requested?
if (nargout >= 4) && (~IsOctave || isargout(4) || isargout(5))
    dowordbounds = 1;
else
    dowordbounds = 0;
end

if IsOctave
    % char() casts of unicode values > 255 map to zero, because Octave
    % uses UTF-8 encoding for unicode, instead of UTF-32 as Matlab. We
    % take care of this in the code, but necessary casts() also trigger
    % an out-of-range warning in Octave, which we can't selectively disable,
    % as it lacks a unique warning id (duh!). Therefore disable all warnings
    % on Octave and re-enable to previous setting whenever the we exit, and
    % therefore the canary variable reenablewarn goes out of scope:
    warningstate = warning('query');
    warning('off');
    reenablewarn = onCleanup(@() restorewarningstate(warningstate));
end

%% process key-value input
[opt,qCalledWithCache] = parseInputs(varargin,nargout);
if isempty(opt)
    % nothing to do
    [nx, ny, textbounds, cache, wordbounds] = deal([]);
    return;
elseif qCalledWithCache
    cache = opt.cache;
    if isfield(cache,'tex')
        [nx, ny] = deal(cache.nx,cache.ny);
        if ~opt.cacheOnly
            DoDrawTexture(opt.win,cache.tex.number,cache.bbox,cache.transform);
        end
    else
        if ~opt.cacheOnly
            [nx, ny, textbounds, wordbounds] = DoDraw(opt.win,...
                disableClip,...
                cache.px,...
                cache.py,...
                cache.bbox,...
                cache.subStrings,...
                cache.switches,...
                cache.fmts,...
                cache.fmtCombs,...
                cache.ssBaseLineOff,...
                cache.winRect,...
                cache.previous,...
                cache.righttoleft,...
                cache.transform,...
                cache.wordbounds);
        end
    end
    if isfield(cache,'tex') || opt.cacheOnly
        wordbounds = cache.wordbounds;
        if isempty(cache.transform)
            textbounds = cache.bbox;
        else
            % transform BBox and wordbounds to reflect transforms applied by
            % DoDrawSetup
            textbounds = transformBBox(cache.bbox,cache.transform);
            for b=1:size(wordbounds,1)
                wordbounds(b,:)  = transformBBox2(wordbounds(b,:), cache.transform, cache.bbox);
            end
        end
        if ~isfield(cache,'tex')
            [nx,ny] = deal([]);
        end
    end
    % done
    return;
end

% normal draw, unpack input arguments
[tstring,win,sx,sy,xalign,yalign,xlayout,baseColor,wrapat,transform,vSpacing,righttoleft,winRect,resetStyle,cacheOnly,cacheMode] = ...
    deal(opt.tstring,opt.win,opt.sx,opt.sy,opt.xalign,opt.yalign,opt.xlayout,opt.baseColor,opt.wrapat,opt.transform,opt.vSpacing,opt.righttoleft,opt.winRect,opt.resetStyle,opt.cacheOnly,opt.cacheMode);


% Need different encoding for returnChar that matches class of input
% tstring:
returnChar = cast(10,class(tstring));

% Convert all conventional linefeeds into C-style newlines.
% But if '\n' is already encoded as a char(10) as in Octave, then
% there's no need for replacement.
if char(10) ~= '\n' 
    newlinepos = strfind(char(tstring), '\n');
    while ~isempty(newlinepos)
        % Replace first occurrence of '\n' by ASCII or double code 10 aka 'repchar':
        tstring = [ tstring(1:min(newlinepos)-1) returnChar tstring(min(newlinepos)+2:end)];
        % Search next occurrence of linefeed (if any) in new expanded string:
        newlinepos = strfind(char(tstring), '\n');
    end
end



% string can contain HTML-like formatting commands. Parse them and turn
% them into formatting indicators, then remove them from the string to draw
[tstring,fmtCombs,fmts,switches,previous] = getFormatting(win,tstring,baseColor,resetStyle);
% check we still have anything to render after formatting tags removed
if isempty(tstring)
    % Empty text string -> Nothing to do, but assign dummy values:
    [nx, ny]    = Screen('DrawText', win, '');
    textbounds  = [nx, ny, nx, ny];
    wordbounds  = textbounds;
    cache       = [];
    return;
end

% Text wrapping requested? NB: formatting tags are removed above, so
% wrapping is correct. Also NB that WrapString only replaces spaces by
% linebreaks and thus does not alter the length of the string or where
% words are placed in it. Our codes.style and codes.color vectors thus remain
% correct.
if wrapat > 0
    % Call WrapString to create a broken up version of the input string
    % that is wrapped around column 'wrapat'
    tstring = WrapString(tstring, wrapat);
end

% now, split text into segments, either when there is a carriage return or
% when the format changes
% find format changes
qSwitch = any(switches,1);
% find carriage returns (can occur at same spot as format change)
% make them their own substring so we can process format changes happening
% at the carriage return properly.
if dowordbounds
    % For per-word bounding boxes, each space as classified by isspace()
    % counts as a "carriage return" to force the code to split strings into
    % subStrings at word boundaries as well, not only carriage returns or
    % style changes:
    qCRet = tstring==returnChar | isspace(tstring);
else
    % No per word bounding boxes needed:
    qCRet = tstring==returnChar;
end
qCRet = ismember(1:length(tstring),[find(qCRet) find(qCRet)+1]);
% split strings
qSplit = qSwitch|qCRet;
% subStrings = accumarray(cumsum(qSplit).'+1,tstring(:),[],@(x) {x.'});
% own implementation to make sure it works on all platform (not sure how
% well the accumarray trick works on Octave). Not any slower either
strI = cumsum(qSplit).'+1;
subStrings = cell(strI(end),1);
for p=1:strI(end)
    subStrings{p} = tstring(strI==p);
end
% get which format to use for each substring, and what attributes are
% changed (if any)
fmtCombs = fmtCombs(qSplit);
switches = switches(:,qSplit);
% code when to perform linefeeds.
qLineFeed= cellfun(@(x) ~isempty(x) && x(1)==returnChar,subStrings).';
% we have an empty up front if there is a format change or carriage return
% first in the string
if isempty(subStrings{1}) && ~qCRet(1)
    % remove it if it is a switch from the default format, but not if we
    % start with a carriage return
    subStrings(1) = [];
    qLineFeed(1)  = [];
else
    % we also need to know about the substring before the first split
    fmtCombs = [1          fmtCombs];
    switches = [false(4,1) switches];
end
% if trailing carriage return, this should lead to a trailing empty line,
% add it here.
if tstring(end)==returnChar
    subStrings{end+1}   = '';
    fmtCombs(end+1)     = fmtCombs(end);
    switches(:,end+1)   = switches(:,end);
    qLineFeed(end+1)    = false;
end
% remove those linefeeds from the characters to draw
qLineFeed             = logical(qLineFeed); % i saw qLineFeed become a double on some older matlab versions...
subStrings(qLineFeed) = cellfun(@(x) x(2:end),subStrings(qLineFeed),'uni',false);
% NB: keep substrings that are now empty as they still signal linefeeds and
% empty lines, and format changes can still occur for those empty substrings

% get number of lines.
numlines        = length(strfind(char(tstring), char(10))) + 1;
% vectors for metrics (like width and height) for each line, andor each
% substring
lWidth          = zeros(1,numlines);
lWidthOff       = zeros(2,numlines);
lBaseLineSkip   = zeros(1,numlines);
lBaseLineOff    = zeros(2,numlines);
lWidthOffLine   = zeros(3,length(subStrings));
sWidth          = zeros(1,length(subStrings));
px              = zeros(1,length(subStrings));
py              = zeros(1,length(subStrings));
ssBaseLineSkip  = zeros(1,length(subStrings));
ssBaseLineOff   = zeros(2,length(subStrings));
% process each substring, collect info per substring and line
% get which substrings belong to each line
substrIdxs = [0 cumsum(qLineFeed(1:end-1))];
if ~qLineFeed(1)
    substrIdxs = substrIdxs+1;
end
for p=1:numlines
    % get which substrings belong to this line
    qSubStr = substrIdxs==p;
    
    % to get line width and height, get textbounds of each string and add
    % them together
    for q=find(qSubStr)
        % do format change if needed
        if any(switches(:,q))
            fmt = fmts(:,fmtCombs(q));
            DoFormatChange(win,switches(:,q),fmt);
        end
        if isempty(subStrings{q})
            [~,bbox,h]      = Screen('TextBounds', win,           'x',0,0,1,righttoleft);
            xAdv = 0;
        else
            [~,bbox,h,xAdv] = Screen('TextBounds', win, subStrings{q},0,0,1,righttoleft);
        end
        
        % get amount cursor moves when drawing this substring
        sWidth(q) = xAdv;
        
        % get amount the substring's pixels extend below and above baseline
        ssBaseLineOff(:,q) = bbox([2 4]);
        
        % for proper linespacing, get text height (distance between
        % baselines as indicated in the font)
        ssBaseLineSkip(q) = h;
        
        % get info we need to construct precise bounding boxes, bbox is
        % pixel-precise, word length based on xAdv is too long. and first
        % pixels may appear after drawing cursor position, take that into
        % account
        if xAdv
            lWidthOffLine(1,q) = bbox(3)-bbox(1);
            lWidthOffLine(2,q) = bbox(1);
            lWidthOffLine(3,q) = xAdv-bbox(3);
        end
    end
    
    % get width of each line
    lWidth(p)           = sum(sWidth(qSubStr));
    
    % get largest baseline skip for each line
    lBaseLineSkip(p)    = max(ssBaseLineSkip(qSubStr));
    
    % get largest offset of ink from baseline for each line
    lBaseLineOff(:,p)   = [min(ssBaseLineOff(1,qSubStr)) max(ssBaseLineOff(2,qSubStr))];
    
    % get pixel offset for line
    % floor to deal with fractional positions, tiny bit of overlap with
    % pixel still leads to some paint being put there.
    is = find(lWidthOffLine(1,:),1);
    il = find(lWidthOffLine(1,:),1,'last');
    if righttoleft
        % boxes get drawn in opposite order from input text, so:
        % find last with non-zero length
        if ~isempty(il)
            lWidthOff(1,p) = floor(lWidthOffLine(2,il));
        end
        % find first with non-zero length
        if ~isempty(is)
            lWidthOff(2,p) = floor(lWidthOffLine(3,is));
        end
    else
        % find first with non-zero length
        if ~isempty(is)
            lWidthOff(1,p) = floor(lWidthOffLine(2,is));
        end
        % find last with non-zero length
        if ~isempty(il)
            lWidthOff(2,p) = floor(lWidthOffLine(3,il));
        end
    end
end
% don't forget to set style back to what it should be
ResetTextSetup(win,previous,false);

% get pixel precise line widths
lWidthPrecise = lWidth-sum(lWidthOff,1);
% position the bounding box of the whole text
mWidth      = max(lWidthPrecise);
if xlayout>4
    justOff = ceil((xlayout-3)/3)*3;
    if justOff == 6
        % justify to full width of winRect
        mWidth  = winRect(3)-winRect(1);
    end
end
% from observing how Word layouts the text, about .22 of text height is
% below the baseline (meaning .78 is above). It is important to mimic this,
% a line with text that is much larger than the previous line will end up
% too low, and a line with text that is much smaller than the previous line
% will probably end up on top of the previous line's ink.
totHeight   = -lBaseLineOff(1,1) + sum(round((.22*lBaseLineSkip(1:end-1)+.78*lBaseLineSkip(2:end))*vSpacing)) + lBaseLineOff(2,end);
bbox        = [0 0 mWidth totHeight];
bbox        = positionBbox(bbox,sx,sy,xalign,yalign);

% now, figure out where to place individual lines and substrings into this
% bbox
for p=1:numlines
    % get which substrings belong to this line
    qSubStr = substrIdxs==p;
    idxs = find(qSubStr);
    
    % get center of line w.r.t. bbox left edge
    xlayoutLine = xlayout;
    if xlayout>3
        % do some form of justification, setup
        % check if there are spaces so padding would be possible:
        % TODO
        % Required padding less than padthresh fraction of total width? If
        % not we skip justification, as it would lead to ridiculous looking
        % results:
        if lWidth(p) < mWidth * padthresh
            xlayoutLine = xlayout-justOff;  % remove offset like this makes sure we have line aligned to left or to right of bbox, as requested
        end
    end
    switch xlayoutLine
        case 1
            % align to left at sx
            lc  = lWidth(p)/2;
        case {2,4}
            % center or justify line in bbox
            lc  = (bbox(3)-bbox(1))/2;
        case 3
            % align to right of window
            lc  = bbox(3)-bbox(1) - lWidth(p)/2;
    end
    
    if righttoleft
        off = -cumsum(   sWidth(   qSubStr)    ) + lWidth(p)/2;
    else
        off =  cumsum([0 sWidth(idxs(1:end-1))]) - lWidth(p)/2;
    end
    if xlayoutLine==4
        xSpace = 0;
    else
        xSpace = 0;
    end
    px(qSubStr) = lc+off-lWidthOff(1,p);    % NB: we've been calculating with pixel/paint positions, go back to text cursor position to have the pixels end up where we want them
    
    if p>1
        % add baseline skip for current line if not first line, that's the
        % carriage return. See note above about how Word does text layout.
        idx = find(qSubStr,1,'first');
        py(idx:end) = py(idx) + round((.22*lBaseLineSkip(p-1)+.78*lBaseLineSkip(p))*vSpacing);
    else
        % we're drawing with yPositionIsBaseline==true, correct for that
        py(:) = -min(ssBaseLineOff(1,qSubStr));
    end
end
% determine word (actually segment) bounds
qNotEmpty   = sWidth>0 & sum(abs(ssBaseLineOff),1);
wordboundsbase  = zeros(sum(qNotEmpty),4);
wordboundsbase(:,1) = floor(px(qNotEmpty)+lWidthOffLine(2,qNotEmpty));
wordboundsbase(:,2) = floor(py(qNotEmpty)+ssBaseLineOff(1,qNotEmpty));
wordboundsbase(:,3) = ceil( px(qNotEmpty)+lWidthOffLine(2,qNotEmpty)+lWidthOffLine(1,qNotEmpty));
wordboundsbase(:,4) = ceil( py(qNotEmpty)+ssBaseLineOff(2,qNotEmpty));

% check if we're doing drawing via a texture. If so, we need a texture the
% size of the bounding box, and we need to keep (px,py) w.r.t the bounding
% box, not w.r.t. the screen
qDrawToTexture  = nargout >= 3 && cacheMode==1;

% now we have positions and wordbounds w.r.t. the bbox, add bbox position
% to place them in the right place on the screen
if ~qDrawToTexture
    px = px+bbox(1);
    py = py+bbox(2);
    wordboundsbase(:,1) = wordboundsbase(:,1)+bbox(1);
    wordboundsbase(:,2) = wordboundsbase(:,2)+bbox(2);
    wordboundsbase(:,3) = wordboundsbase(:,3)+bbox(1);
    wordboundsbase(:,4) = wordboundsbase(:,4)+bbox(2);
end


%% done processing inputs, do text drawing
% do draw to texture if wanted
if qDrawToTexture
    texBbox = OffsetRect(bbox,-bbox(1),-bbox(2));
    drawRect = transformBBox(texBbox,transform);
    extraTransOff = [0 0];
    if ~isempty(transform)
        % Extra translate so that drawn text has top-left at (0,0)
        extraTransOff = -drawRect(1:2);
    end
    [tex.number,tex.rect] = Screen('OpenOffscreenWindow', win, [0 0 0 0], [0 0 RectWidth(drawRect) RectHeight(drawRect)]);
    ResetTextSetup(tex.number,previous,true);
    [nx, ny, ~, wordbounds] = DoDraw(tex.number,disableClip,px,py,texBbox,subStrings,switches,fmts,fmtCombs,ssBaseLineOff,winRect,previous,righttoleft,transform,wordboundsbase,extraTransOff);
    nx = nx+bbox(1);
    ny = ny+bbox(2);
    wordbounds(:,1) = wordbounds(:,1)+bbox(1);
    wordbounds(:,2) = wordbounds(:,2)+bbox(2);
    wordbounds(:,3) = wordbounds(:,3)+bbox(1);
    wordbounds(:,4) = wordbounds(:,4)+bbox(2);
    textbounds = OffsetRect(drawRect,bbox(1),bbox(2));
end
if ~cacheOnly
    % draw to screen
    if qDrawToTexture
        DoDrawTexture(win,tex.number,textbounds,[]);
    else
        [nx, ny, textbounds, wordbounds] = DoDraw(win,disableClip,px,py,bbox,subStrings,switches,fmts,fmtCombs,ssBaseLineOff,winRect,previous,righttoleft,transform,wordboundsbase);
    end
elseif cacheMode~=1
    % don't do any drawing to screen, nor to texture
    [nx,ny] = deal([]);
    % the bbox in the cache is untranslated (we need to know the original
    % after all when drawing). Output transformed one here for user's info,
    % so they know what'll appear on screen eventually.
    textbounds = transformBBox(bbox,transform);
    wordbounds = wordboundsbase;
    for b=1:size(wordbounds,1)
        wordbounds(b,:)  = transformBBox2(wordbounds(b,:), transform, bbox);
    end
end
if nargout>3
    % make cache
    cache.opt = opt;
    cache.win = win;
    if cacheMode==1
        cache.tex       = tex;
        cache.bbox      = textbounds;
        cache.transform = [];           % no need to reapply transforms as they are already "hardcoded" into the texture. But user can add new ones
        cache.nx        = nx;
        cache.ny        = ny;
        cache.wordbounds = wordbounds;  % store transformed wordbounds as they're "hardcoded" into the texture
    else
        cache.px = px;
        cache.py = py;
        cache.bbox = bbox;
        cache.subStrings = subStrings;
        cache.substrIdxs = substrIdxs;
        cache.switches = switches;
        cache.fmts = fmts;
        cache.fmtCombs = fmtCombs;
        cache.ssBaseLineOff = ssBaseLineOff;
        cache.winRect = winRect;
        cache.previous = previous;
        cache.righttoleft = righttoleft;
        cache.transform = transform;
        cache.wordbounds = wordboundsbase;
    end
end
end

% Restore warning() settings to initial at onCleanup():
function restorewarningstate(warningstate)
    warning(warningstate);
end

function [previouswin, IsOpenGLRendering] = DoDrawSetup(win,transform,bbox,extraTransOff)
if nargin<4
    extraTransOff = [0 0];
end
% Is the OpenGL userspace context for this 'windowPtr' active, as required?
[previouswin, IsOpenGLRendering] = Screen('GetOpenGLDrawMode');

% OpenGL rendering for this window active?
if IsOpenGLRendering
    % Yes. We need to disable OpenGL mode for that other window and
    % switch to our window:
    Screen('EndOpenGL', win);
end

if ~isempty(transform)
    [xc, yc] = RectCenterd(bbox);
    
    % Make a backup copy of the current transformation matrix for later
    % use/restoration of default state:
    Screen('glPushMatrix', win);
    
    % We need to undo the translation
    Screen('glTranslate', win, xc+extraTransOff(1), yc+extraTransOff(2));
    
    % apply transforms
    % as OpenGL transform should be specified in reversed order but i
    % don't want to bother the interface with that, apply transform
    % back to front here.
    for p=length(transform)-1:-2:1
        switch transform{p}
            case 'translate'
                Screen('glTranslate', win, transform{p+1}(1), transform{p+1}(2));
            case 'flip'
                % argument is which axis, x (1), y (2), or both (3). Both
                % equals 180 degree rotation.
                if transform{p+1}(1)==1
                    Screen('glScale', win, -1,  1);
                elseif transform{p+1}(1)==2
                    Screen('glScale', win,  1, -1);
                elseif transform{p+1}(1)==3
                    Screen('glScale', win, -1, -1);
                end
            case 'scale'
                Screen('glScale', win, transform{p+1}(1), transform{p+1}(2));
            case 'rotate'
                ang = transform{p+1}(1);
                % note that for the PTB screen, a positive rotation is
                % clockwise. 
                Screen('glRotate', win, ang);
        end
    end
    
    % Translate origin to the geometric center of the text
    Screen('glTranslate', win, -xc, -yc);
end
end

function DoDrawCleanup(win, previouswin, IsOpenGLRendering, transform)
% undo transform if any
if ~isempty(transform)
    Screen('glPopMatrix', win);
end

% If a different window than our target window was active, we'll switch
% back to that window and its state:
if previouswin > 0
    if previouswin ~= win
        % Different window was active before our invocation:
        
        % Was that window in 3D mode, i.e., OpenGL rendering for that window was active?
        if IsOpenGLRendering
            % Yes. We need to switch that window back into 3D OpenGL mode:
            Screen('BeginOpenGL', previouswin);
        else
            % No. We just perform a dummy call that will switch back to that
            % window:
            Screen('GetWindowInfo', previouswin);
        end
    else
        % Our window was active beforehand.
        if IsOpenGLRendering
            % Was in 3D mode. We need to switch back to 3D:
            Screen('BeginOpenGL', previouswin);
        end
    end
end
end

function DoDrawTexture(win,texNum,texDrawRect,transform)
[previouswin, IsOpenGLRendering] = DoDrawSetup(win,transform,texDrawRect);
Screen('DrawTexture',win,texNum,[],texDrawRect);
DoDrawCleanup(win, previouswin, IsOpenGLRendering, transform);
end

function [nx, ny, bbox, wordbounds] = DoDraw(win,disableClip,sx,sy,bbox,subStrings,switches,fmts,fmtCombs,ssBaseLineOff,winRect,previous,righttoleft,transform,wordboundsbase,extraTransOff)

[nx,ny]     = deal(nan);
wordbounds  = wordboundsbase;
if nargin<16
    extraTransOff = [0 0];
end

[previouswin, IsOpenGLRendering] = DoDrawSetup(win, transform, bbox, extraTransOff);
if ~isempty(transform)
    % transform BBox and wordbounds to reflect transforms applied by
    % DoDrawSetup
    for b=1:size(wordbounds,1)
        wordbounds(b,:)  = transformBBox2(wordbounds(b,:), transform, bbox);
    end
    bbox = transformBBox(bbox,transform);
end

% Draw the substrings
for p=1:length(subStrings)
    curstring = subStrings{p};
    yp = sy(p);
    xp = sx(p);
    
    % do format change if needed
    if any(switches(:,p))
        fmt = fmts(:,fmtCombs(p));
        DoFormatChange(win,switches(:,p),fmt);
    end
    
    % Perform crude clipping against upper and lower window borders for this text snippet.
    % If it is clearly outside the window and would get clipped away by the renderer anyway,
    % we can safe ourselves the trouble of processing it:
    if ~isempty(curstring) && (disableClip || ((yp + ssBaseLineOff(2,p) >= winRect(2)) && (yp + ssBaseLineOff(1,p) <= winRect(4))))
        % Inside crude clipping area. Need to draw.
        clipOrEmpty = false;
    else
        % Skip this text line draw call, as it is empty or would be clipped
        % away anyway.
        clipOrEmpty = true;
    end
    
    % Any string to draw?
    if ~clipOrEmpty
        % The cursor is positioned (nx,ny output) to allow to continue to
        % print text directly after the drawn text (if you set
        % yPositionIsBaseline==true at least). Basically behaves like
        % printf or fprintf formatting.
        [nx,ny] = Screen('DrawText', win, curstring, xp, yp,[],[],1, righttoleft);

        % for debug, draw bounding box and baseline
        % [~,sbbox,~,xAdv] = Screen('TextBounds', win, curstring, xp, yp, 1, righttoleft);
        % Screen('FrameRect',win,[0 255 0 128],sbbox);
        % Screen('DrawDots',win,[xp+xAdv ny],1,[0 0 255 128]);    % xp+xAdv equal nx, but not necessarily left edge of next bbox
        % Screen('DrawLine',win,[0 255 255],xp,yp,nx,ny);
    end
end

% Our work is done. clean up
% reset text style etc
ResetTextSetup(win,previous,false);

DoDrawCleanup(win, previouswin, IsOpenGLRendering, transform);

end

%% helpers
function [tstring,fmtCombs,fmts,switches,previous] = getFormatting(win,tstring,startColor,resetStyle)
% This function parses tags out of the text and turns them into formatting
% textstyles, colors, font and text sizes to use when drawing.
% allowable codes:
% - <i>                 To toggle italicization
% - <b>                 To toggle bolding
% - <u>                 To toggle underlining
% - <color=HEX or FPN>  To switch to a new color
% - <font=name>         To switch to a new font
% - <size=number>       To switch to a new font size

% get string type, store original as octave can't deal with string values outside uint8 range
tstringOri  = tstring;
tstring     = char(tstring);
tstring(tstring>127) = 0;

% get colorrange of window, to interpret colors
cr = Screen('ColorRange',win);

% get current active text options
previous.style  = Screen('TextStyle', win);
previous.size   = Screen('TextSize' , win);
previous.font   = Screen('TextFont' , win);
% baseColor is given as input as its convenient for user to be able to set
% it and consistent with other text drawing functions.
previous.color  = startColor;    % keep copy of numeric representation (if its hex, its converted to numeric below when checking base.color)

% get starting text options
base = previous;
if resetStyle
    % start with a clean-slate style (no bold, italic, underlined, etc)
    % when interpreting formatting commands
    base.style = 0;
end

% convert color to hex
if isnumeric(base.color)
    if cr==1.0
        % convert to comma separated floating point
        base.color = sprintf('%f,',base.color);
        base.color(end) = [];   % remove trailing comma
    else
        % convert to hex
        base.color = sprintf('%0*X',[repmat(2,size(base.color));base.color]);
    end
else
    % if user provided color input, store in format we can use in PTB later
    if any(previous.color=='.')||any(previous.color==',')
        % user provided floating point input
        previous.color = sscanf(previous.color,'%f,').';
    else
        % user provided hex input
        previous.color = hex2dec(reshape(previous.color,2,[]).').';
        base.color     = upper(base.color); % ensure hex color is uppercase so below logic works correctly
    end
end

% prepare outputs
% these outputs have same length as string to draw and for each character
% indicate its style and color
codes.style     = repmat(base.style,size(tstring));
tables.color    = {base.color};
tables.font     = {base.font};
codes.color     = ones(size(tstring));                  % 1 indicates startColor, all is in startColor unless user provides color tags telling us otherwise
codes.font      = codes.color;                          % 1 indicates default font, all is in default font unless user provides color tags telling us otherwise
codes.size      = repmat(previous.size,size(tstring));

%% first process tags that don't have further specifiers (<b>, <i>, <u>)
% find tag locations. (?<!(?<!/)/) matches tags with zero or more than one
% slashes in front of them
[tagis ,tagie ,tagt ] = regexp(tstring,'(?i)(?<!(?<!/)/)<(i|b|u)>','start','end','tokens');
if ~isempty(tagis)
    % get full text for each tags and indices to where it is in the input
    % string
    tagi  = [tagis; tagie].';
    tagt  = cat(1,tagt{:});
    
    % fill up output, indicating the style code applicable to each
    % character
    if ~isempty(tagt)
        currStyle = codes.style(1);
        for p=1:length(tagt)
            % the below code snippet is a comment, describing what the line
            % below does
            % switch formatCodes{p}
            %     case 'i'
            %         fBit = log2(2)+1;
            %     case 'b'
            %         fBit = log2(1)+1;
            %     case 'u'
            %         fBit = log2(4)+1;
            % end
            fBit = floor((double(tagt{p})-'b')/7)+1;
            currStyle = bitset(currStyle,fBit,~bitget(currStyle,fBit));
            codes.style(tagi(p,2):end) = currStyle;
        end
    end
    % now mark active formatting commands to be stripped from text
    toStrip = bsxfun(@plus,tagi(:,1),0:2);      % tags are always three characters long
    toStrip = toStrip(:).';
else
    toStrip = [];
end

%% now process tag that have further specifiers (<color=x>, <font=x>, <size=x>)
% find tag locations. also match empty tags. even if only empty tags, we
% still want to remove them. Ill formed tags with equals sign but no
% argument, or tags with tags inside, are not matched. (?<!(?<!/)/) matches
% tags with zero or more than one slashes in front of them
[tagis ,tagie ,tagt ] = regexp(tstring,'(?i)(?<!(?<!/)/)<(color|font|size)=([^<>]+?)>|(?<!(?<!/)/)<(color|font|size)>','start','end','tokens');
if ~isempty(tagis)
    % get full text for each tag and indices to where it is in the input
    % string
    tagi  = [tagis; tagie].';
    
    % use a simple stack/state machine as we need to maintain a history.
    % empty tags means go back to previous color/size/font
    % (crappy stacks, end of array is top of stack)
    colorStack = 1;         % index in tables.color
    fontStack  = 1;         % index in tables.font
    sizeStack  = codes.size(1);
    
    for p=1:size(tagi,1)
        % check if tag has argument
        if ~isscalar(tagt{p})
            switch tagt{p}{1}
                case 'color'
                    color = tagt{p}{2};
                    % check color is valid
                    % detect FPN: if comma or decimal point
                    qComma = color==',';
                    if any(color=='.') || any(qComma)
                        assert(all(isstrprop(color,'digit')|color=='.'|qComma),'DrawFormattedText2: color tag argument must be specified as comma-separated floating point values, or hexadecimal values')
                    else
                        assert(any(length(color)==[1 2 6 8]),'DrawFormattedText2: if color tag argument is a hexadecimal value, it should have length 1, 2, 6, or 8')
                        assert(all(isstrprop(color,'xdigit')),'DrawFormattedText2: color tag argument must be specified as hexadecimal values, or comma-separated floating point values')
                    end
                    % find new color or add to table
                    iColor = find(strcmpi(tables.color,color),1);
                    if isempty(iColor)
                        tables.color{end+1} = upper(color);
                        iColor = length(tables.color);
                    end
                    % add to stack front
                    colorStack(end+1) = iColor; %#ok<AGROW>
                    % mark all next text as having this color
                    codes.color(tagi(p,2):end) = iColor;
                case 'font'
                    font = tagt{p}{2};  % no checks on whether it is valid
                    % find new color or add to table
                    iFont = find(strcmpi(tables.font,font),1);
                    if isempty(iFont)
                        tables.font{end+1} = font;
                        iFont = length(tables.font);
                    end
                    % add to stack front
                    fontStack(end+1) = iFont; %#ok<AGROW>
                    % mark all next text as having this color
                    codes.font(tagi(p,2):end) = iFont;
                case 'size'
                    fsize = str2double(tagt{p}{2});
                    assert(~isnan(fsize),'DrawFormattedText2: size tag argument must be a number')
                    % add to stack front
                    sizeStack(end+1) = fsize; %#ok<AGROW>
                    % mark all next text as having this color
                    codes.size(tagi(p,2):end) = fsize;
            end
        else
            switch tagt{p}{1}
                case 'color'
                    % if not already reached end of history, revert to
                    % previous color for rest of text
                    if ~isscalar(colorStack)
                        % pop color of stack
                        colorStack(end) = [];
                        % mark all next text as having this color
                        codes.color(tagi(p,2):end) = colorStack(end);
                    end
                case 'font'
                    % if not already reached end of history, revert to
                    % previous color for rest of text
                    if ~isscalar(fontStack)
                        % pop font of stack
                        fontStack(end) = [];
                        % mark all next text as having this font
                        codes.font(tagi(p,2):end) = fontStack(end);
                    end
                case 'size'
                    % if not already reached end of history, revert to
                    % previous color for rest of text
                    if ~isscalar(sizeStack)
                        % pop size of stack
                        sizeStack(end) = [];
                        % mark all next text as having this size
                        codes.size(tagi(p,2):end) = sizeStack(end);
                    end
            end
        end
    end
    
    % now mark active formatting commands to be stripped from text (NB:
    % despite growing array, this is faster than something preallocated)
    for p=1:size(tagi,1)
        toStrip = [toStrip tagi(p,1):tagi(p,2)]; %#ok<AGROW>
    end
end
% now strip active formatting commands from text
% add escape slashes from any escaped tags. also when double slashed,
% we should remove one
toStrip = [toStrip regexp(tstring,'(?i)/<(i|b|u|color|font|size)','start')];
tstringOri (toStrip) = [];
codes.style(toStrip) = [];
codes.color(toStrip) = [];
codes.font (toStrip) = [];
codes.size (toStrip) = [];

% replace tstring with tstringOri again in case input was outside char range, so Octave can handle this all just fine..
tstring = tstringOri;

if isempty(tstring)
    % string was only formatting commands, nothing to draw, ignore
    [fmtCombs,fmts,switches,previous] = deal([]);
    return;
end

% process colors, hex->dec
for p=1:length(tables.color)
    qComma = tables.color{p}==',';
    if any(tables.color{p}=='.') || any(qComma)
        % at this point, all we know is that the string contains digits,
        % decimal points and commas (checked above).
        tables.color{p} = sscanf(tables.color{p},'%f,');
        assert(any(length(tables.color{p})==[1 3 4]),'DrawFormattedText2: if color tag argument is a comma-separated floating point value, it should have length 1, 3, or 4')
        if cr==255
            % scale
            tables.color{p} = round(tables.color{p}.*255);
        end
    else
        % above we made sure all colors are uppercase and valid hex
        % then, convert letter to their numerical value
        % -48 for numbers (ascii<=64)
        % -55 for letters (ascii>64)
        tables.color{p} = tables.color{p}-48-(tables.color{p}>64)*7;
        % then, sum in pairs, while multiplying first of each pair by its base, 16
        tables.color{p} = sum([tables.color{p}(1:2:end)*16;tables.color{p}(2:2:end)]);
        if cr==1.0
            % scale
            tables.color{p} = tables.color{p}./255;
        end
    end
end

% consolidate codes into one, indicating unique combinations. Also produce
% four boolean vectors indicating what changed upon a style change.
% last, output a table that for each unique combination indicates what the
% style, font, color and size are
c = [codes.style; codes.color; codes.font; codes.size];
% where do changes occur?
switches = logical(diff([[previous.style; 1; 1; previous.size] c],[],2));
% make sure color is always applied, may have been changed under our feet
% if drawn later, or may have been provided as baseColor by user
switches(2,1) = true;
% get unique formats and where each of these formats is to be applied
% the below is equivalent to:
% [format,~,fmtCombs] = unique(c.','rows');
% format = format.';
% fmtCombs = fmtCombs.';
% but do required functionality myself to be way faster:
if IsOctave()
    [~,i] = sortrows(c.',1:4);
else
    i=sortrowsc(c.',1:4);
end
groupsSortA = [true any(c(:,i(1:end-1)) ~= c(:,i(2:end)),1)];
format = c(:,i(groupsSortA));
fmtCombs = cumsum(groupsSortA);
fmtCombs(i) = fmtCombs;
% build table with info about each unique format combination
fmts = num2cell(format);
% two columns are indices into table, do indexing
fmts(2,:) = tables.color(format(2,:));
fmts(3,:) = tables.font (format(3,:));
% last, store which need to be changed back when drawing finished
previous.changed = logical(diff([[previous.style; 1; 1; previous.size] c(:,end)],[],2));
end


function DoFormatChange(win,switches,fmt)
% rows in switches / columns in format:
% 1: style, 2: color, 3: font, 4: size

% font and style: if we change font, always set style with the same
% command. Always works and sometimes needed with some exotic fonts
% (see Screen('TextFont?') )
if switches(3)
    Screen('TextFont', win,fmt{3},fmt{1});
elseif switches(1)
    Screen('TextStyle',win,fmt{1});
end
% color, set through this command. drawing commands below do not
% set color
if switches(2)
    Screen('TextColor',win,fmt{2});
end
% size
if switches(4)
    Screen('TextSize',win,fmt{4});
end
end

function ResetTextSetup(win,previous,qDoAll)
if qDoAll || previous.changed(3)
    Screen('TextFont',win,previous.font,previous.style);
elseif previous.changed(1)
    Screen('TextStyle',win,previous.style);
end
if qDoAll || previous.changed(2)
    Screen('TextColor',win,previous.color); % setting the baseColor input, not color before function entered. Consistent with other text drawing functions
end
if qDoAll || previous.changed(4)
    Screen('TextSize',win,previous.size);
end
end

function bbox = positionBbox(bbox,sx,sy,xalign,yalign)

bWidth = bbox(3)-bbox(1);
bHeight= bbox(4)-bbox(2);

switch xalign
    case 1
        xoff = 0;
    case 2
        xoff = -bWidth/2;
    case 3
        xoff = -bWidth;
end
switch yalign
    case 1
        yoff = 0;
    case 2
        yoff = -bHeight/2;
    case 3
        yoff = -bHeight;
end

bbox = OffsetRect(bbox,round(sx+xoff),round(sy+yoff));
end

function bbox = transformBBox(bbox,transform)
if ~isempty(transform)
    [xc, yc] = RectCenterd(bbox);
    bbox = OffsetRect(bbox,-xc,-yc);
    
    % apply transforms
    for p=1:2:length(transform)
        switch transform{p}
            case 'translate'
                bbox = OffsetRect(bbox, transform{p+1}(1), transform{p+1}(2));
            case 'flip'
                % argument is which axis, x (1), y (2), or both (3). Both
                % equals 180 degree rotation. All are no-ops here as
                % bounding box does not change
            case 'scale'
                bbox = ScaleRect(bbox, transform{p+1}(1), transform{p+1}(2));
            case 'rotate'
                ang = transform{p+1}(1);
                % note that for the PTB screen, a positive rotation is
                % clockwise. 
                bbox = [cosd(ang) -sind(ang); sind(ang) cosd(ang)] * bbox([1 3 1 3; 2 4 4 2]);
                bbox = [min(bbox,[],2).' max(bbox,[],2).'];
        end
    end
    
    % We need to undo the translations...
    bbox = OffsetRect(bbox,xc,yc);
end
end

function bbox = transformBBox2(bbox,transform,refbox)
if ~isempty(transform)
    M = eye(3,3);

    [xc, yc] = RectCenterd(refbox);
    M = M * [[1, 0, xc]; [0, 1, yc]; [0, 0, 1]];

    % apply transforms
    for p=1:2:length(transform)
        switch transform{p}
            case 'translate'
                M = M * [[1, 0, transform{p+1}(1)]; [0, 1, transform{p+1}(2)]; [0, 0, 1]];
            case 'flip'
                % argument is flip around which axis, y (1), x (2), or both (3).
                if transform{p+1}(1) == 1
                    M = M * [[-1, 0, 0]; [0, 1, 0]; [0, 0, 1]];
                end

                if transform{p+1}(1) == 2
                    M = M * [[1, 0, 0]; [0, -1, 0]; [0, 0, 1]];
                end

                if transform{p+1}(1) == 3
                    M = M * [[-1, 0, 0]; [0, -1, 0]; [0, 0, 1]];
                end
            case 'scale'
                M = M * [[transform{p+1}(1), 0, 0]; [0, transform{p+1}(2), 0]; [0, 0, 1]];
            case 'rotate'
                ang = transform{p+1}(1);
                % note that for the PTB screen, a positive rotation is
                % clockwise.
                M = M * [[cosd(ang), -sind(ang), 0] ; [sind(ang), cosd(ang) 0] ; [0, 0, 1]];
        end
    end

    % We need to undo the translations...
    M = M * [[1, 0, -xc]; [0, 1, -yc]; [0, 0, 1]];

    % Apply combined transforms:
    v1 = M * [bbox(1); bbox(2); 1];
    v2 = M * [bbox(3); bbox(2); 1];
    v3 = M * [bbox(3); bbox(4); 1];
    v4 = M * [bbox(1); bbox(4); 1];

    % Project back to 2D plane:
    v1 = v1 / v1(3);
    v2 = v2 / v2(3);
    v3 = v3 / v3(3);
    v4 = v4 / v4(3);

    % Make axis-aligned:
    bbox = [min([v1(1),v2(1),v3(1),v4(1)]), min([v1(2),v2(2),v3(2),v4(2)]), max([v1(1),v2(1),v3(1),v4(1)]), max([v1(2),v2(2),v3(2),v4(2)])];
end
end

function [opt,qCalledWithCache] = parseInputs(varargs,nOutArg)

if isempty(varargs) || isempty(varargs{1})
    % Empty text string -> Nothing to do.
    opt                 = [];
    qCalledWithCache    = false;
    return;
elseif isstruct(varargs{1})
    % called with cache
    qCalledWithCache = true;
    qTextureCache    = isfield(varargs{1},'tex');
    opt = struct(...
        'cache',varargs{1},...
        'win',[],...
        'sx' ,[],...
        'sy' ,[],...
        'xalign' ,[],...
        'yalign' ,[],...
        'transform',[],...
        'cacheOnly',false,...
        'winRect',[]...
        );
else
    opt = struct(...
        'tstring',varargs{1},...
        'win',[],...
        'sx' ,[],...
        'sy' ,[],...
        'xalign' ,[],...
        'yalign' ,[],...
        'xlayout' ,1,...
        'baseColor',[],...
        'wrapat',0,...
        'transform',[],...
        'vSpacing',1,...
        'righttoleft',0,...
        'winRect',[],...
        'resetStyle',1,...
        'cacheOnly',false,...
        'cacheMode',1 ...
        );
    qCalledWithCache = false;
end


% parse inputs
assert(mod(length(varargs),2)==1,'function should be called with key-value pairs for all inputs except the first.')
for p=2:2:length(varargs)
    assert(isfield(opt,varargs{p}),'option %s not understood',varargs{p})
    opt.(varargs{p}) = varargs{p+1};
end


% further check and process inputs
if qCalledWithCache
    if isempty(opt.win)
        opt.win = opt.cache.win;
    end
    if ~qTextureCache
        ResetTextSetup(opt.win,opt.cache.previous,true);
    end
    % check which if any of the below are set
    ignorep = [isempty(opt.sx) isempty(opt.sy) isempty(opt.xalign) isempty(opt.yalign)];
else
    if isempty(opt.win)
        error('DrawFormattedText2: Windowhandle missing!');
    end
    
    % layout of individual lines within the bounding box
    % default is aligned to left of box
    if ~ischar(opt.xlayout)
        opt.xlayout = 1;
    else
        switch opt.xlayout
            case 'left'
                opt.xlayout = 1;
            case 'center'
                opt.xlayout = 2;
            case 'right'
                opt.xlayout = 3;
                
                % not implemented: 4, 6, 7, 9
            case 'ljustifylongest'
                % justify to width of longest sentence. if line is too short (see ptb_drawformattedtext2_padthresh), left align
                opt.xlayout = 4;    % -3==1
            case 'rjustifylongest'
                % justify to width of longest sentence. if line is too short (see ptb_drawformattedtext2_padthresh), right align
                opt.xlayout = 6;    % -3==3
            case 'ljustifyfullwidth'
                % justify to width of winRect. if line is too short (see ptb_drawformattedtext2_padthresh), left align
                opt.xlayout = 7;    % -6==1
            case 'rjustifyfullwidth'
                % justify to width of winRect. if line is too short (see ptb_drawformattedtext2_padthresh), right align
                opt.xlayout = 9;    % -6==3
            otherwise
                % ignore anything else user may have provided
                opt.xlayout = 1;
        end
    end
    
    % Keep current text color if none provided:
    if isempty(opt.baseColor)
        opt.baseColor = Screen('TextColor', opt.win);
    else
        opt.baseColor = opt.baseColor(:).'; % ensure row vector
    end
    
    % No text wrapping by default:
    if isinf(opt.wrapat)
        opt.wrapat = 0;
    end
    
    % option to only generate cache but not draw
    if ~isempty(opt.cacheOnly)
        opt.cacheOnly = logical(opt.cacheOnly);
    end
    
    % check cache mode
    if ischar(opt.cacheMode)
        switch opt.cacheMode
            case 'texture'
                opt.cacheMode = 1;
            case 'fullCache'
                opt.cacheMode = 2;
            otherwise
                error('cache mode "%s" not understood, possible values: "texture" and "fullCache"',opt.cacheMode);
        end
    else
        assert(ismember(opt.cacheMode,[1 2]),'cacheMode must be 1 or 2')
    end
    
    if opt.cacheOnly && nOutArg < 4
        error('cacheOnly requested but fourth output (cache) is not requested')
    end
end


% No mirroring/rotating/etc of text by default:
if ~isempty(opt.transform)
    checktransform(opt.transform);
end

% Default rectangle for centering/formatting text is the client rectangle
% of the window, but usercode can specify arbitrary override it:
if isempty(opt.winRect)
    opt.winRect = Screen('Rect', opt.win);
    qWinRectSpecified = false;
else
    assert(isnumeric(opt.winRect) && numel(opt.winRect)==4,'Provided winRect is not valid')
    qWinRectSpecified = true;
end

% position text box on screen
[opt.sx,opt.sy,opt.xalign,opt.yalign] = parseTextBoxPositioning(opt.sx,opt.sy,opt.xalign,opt.yalign,opt.winRect);

% do any last extra processing if drawing from cache.
if qCalledWithCache
    if ~all(ignorep) % all true: no repositioning or offsetting
        % move bbox as requested
        bbox = opt.cache.bbox;
        if any(~ignorep(3:4))
            % first, fill out missing arguments.
            for f={'sx','sy','xalign','yalign'}
                if isempty(opt.(f{1}))
                    opt.(f{1}) = opt.cache.opt.(f{1});
                end
            end
            % reposition bbox:
            bbox = positionBbox([0 0 bbox(3)-bbox(1) bbox(4)-bbox(2)],opt.sx,opt.sy,opt.xalign,opt.yalign);
            % bbox(1:2) now contain new top-left for text. Make that into
            % offsets to reposition text
            off = bbox(1:2)-opt.cache.bbox(1:2);
        else
            % first, fill out missing arguments.
            for f={'sx','sy'}
                if isempty(opt.(f{1}))
                    opt.(f{1}) = 0;
                end
            end
            % only sx and sy provided, do offsetting
            assert(isnumeric(opt.sx)&&isnumeric(opt.sy),'When drawing from cache and providing horizontal and vertical offsets with the ''sx'' and ''sy'' inputs, these offsets must be numeric')
            off = [opt.sx opt.sy];
            bbox = OffsetRect(opt.cache.bbox,off(1),off(2));
        end
        % common logic: put bounding box and text in new place
        if ~qTextureCache
            opt.cache.px = opt.cache.px + off(1);
            opt.cache.py = opt.cache.py + off(2);
        end
        % apply offsets to word bounding boxes too
        off = bbox-opt.cache.bbox;
        opt.cache.wordbounds(:,1) = opt.cache.wordbounds(:,1)+off(1);
        opt.cache.wordbounds(:,2) = opt.cache.wordbounds(:,2)+off(2);
        opt.cache.wordbounds(:,3) = opt.cache.wordbounds(:,3)+off(1);
        opt.cache.wordbounds(:,4) = opt.cache.wordbounds(:,4)+off(2);
        opt.cache.bbox = bbox;
    end
    % overwrite winRect in cache if set by user
    if qWinRectSpecified
        opt.cache.winRect = opt.winRect;
    end
    % append to transforms, if any provided by user
    if ~isempty(opt.transform)
        opt.cache.transform = [opt.cache.transform opt.transform];
    end
end
end

function [sx,sy,xalign,yalign] = parseTextBoxPositioning(sx,sy,xalign,yalign,winRect)
xpos = 0;   % default: use provided sx (w.r.t. winRect)
% Default x start position is left of window:
if isempty(sx)
    sx = 0;
else
    % have text specifying a position at the edge of windowrect?
    if ischar(sx)
        if strcmpi(sx, 'left')
            xpos = 1;
        elseif strcmpi(sx, 'center')
            xpos = 2;
        elseif strcmpi(sx, 'right')
            xpos = 3;
        else
            % Ignore any other crap user may have provided, align to left.
            xpos = 1;
        end
    elseif ~isnumeric(sx)
        % Ignore any other crap user may have provided.
        sx = 0;
    end
end

ypos = 0;   % default: use provided sy (w.r.t. winRect)
% Default y start position is top of window:
if isempty(sy)
    sy = 0;
else
    % have text specifying a position at the edge of windowrect?
    if ischar(sy)
        if strcmpi(sy, 'top')
            ypos = 1;
        elseif strcmpi(sy, 'center')
            ypos = 2;
        elseif strcmpi(sy, 'bottom')
            ypos = 3;
        else
            % Ignore any other crap user may have provided, align to left.
            ypos = 1;
        end
    elseif ~isnumeric(sy)
        % Ignore any other crap user may have provided.
        sy = 0;
    end
end

% now process xpos and ypos
switch xpos
    case 0
        % provided sx is in winRect
        sx = sx+winRect(1);
    case 1
        sx = winRect(1);
    case 2
        sx = (winRect(1)+winRect(3))/2;
    case 3
        sx = winRect(3);
end
switch ypos
    case 0
        % provided sy is in winRect
        sy = sy+winRect(2);
    case 1
        sy = winRect(2);
    case 2
        sy = (winRect(2)+winRect(4))/2;
    case 3
        sy = winRect(4);
end


%%% now we have a position, figure out how to position box with respect to
%%% that position
% Default x layout is align box to left of specified position
if isempty(xalign) || ~ischar(xalign)
    xalign = 1;
else
    if strcmpi(xalign, 'left')
        xalign = 1;
    elseif strcmpi(xalign, 'center')
        xalign = 2;
    elseif strcmpi(xalign, 'right')
        xalign = 3;
    else
        % ignore anything else user may have provided
        xalign = 1;
    end
end

% Default y layout is align box below of specified position
if isempty(yalign) || ~ischar(yalign)
    yalign = 1;
else
    if strcmpi(yalign, 'top')
        yalign = 1;
    elseif strcmpi(yalign, 'center')
        yalign = 2;
    elseif strcmpi(yalign, 'bottom')
        yalign = 3;
    else
        % ignore anything else user may have provided
        yalign = 1;
    end
end
end

function checktransform(transform)
if isempty(transform)
    return;
end

assert(mod(numel(transform),2)==0,'transform input must be key-value')
assert(iscellstr(transform(1:2:end)),'transform: all keys should be a character array')
assert(all(cellfun(@isnumeric,transform(2:2:end))),'transform: all values should be a numeric')
end