Index: ps/trunk/binaries/data/mods/_test.minimal/fonts/console.fnt
===================================================================
--- ps/trunk/binaries/data/mods/_test.minimal/fonts/console.fnt (revision 26914)
+++ ps/trunk/binaries/data/mods/_test.minimal/fonts/console.fnt (revision 26915)
@@ -1,610 +1,612 @@
-100
+101
256 256
+a
606
-20
+15
+12
32 0 256 0 0 0 0 5
33 250 154 3 11 1 11 4
34 121 18 5 4 0 11 5
35 80 214 12 11 0 11 12
36 190 39 10 13 0 12 10
37 204 11 15 11 0 11 15
38 187 25 13 11 0 11 12
39 253 188 3 4 0 11 3
40 252 79 4 11 0 11 4
41 251 67 5 11 0 11 5
42 73 186 7 7 1 12 8
43 48 138 9 9 2 9 13
44 253 184 3 4 2 2 6
45 20 226 4 2 1 5 6
46 253 180 3 2 0 2 5
47 146 89 6 13 -1 11 4
48 154 73 10 11 0 11 10
49 250 11 6 11 2 11 11
50 164 73 10 11 0 11 10
51 86 75 10 11 1 11 11
52 174 75 10 11 0 11 10
53 232 75 10 11 0 11 10
54 12 77 10 11 0 11 10
55 226 166 8 11 1 11 10
56 72 77 10 11 0 11 10
57 96 79 10 11 0 11 10
58 32 224 3 8 1 8 5
59 29 224 3 10 1 8 5
60 74 138 9 9 2 9 13
61 111 61 9 5 2 7 13
62 84 138 9 9 2 9 13
63 81 190 9 11 0 11 8
64 0 14 16 14 0 11 16
65 39 49 11 11 0 11 11
66 204 79 10 11 1 11 11
67 51 49 11 11 0 11 11
68 63 49 11 11 1 11 12
69 117 190 9 11 1 11 10
70 122 162 8 11 1 11 10
71 161 49 11 11 0 11 11
72 173 49 11 11 1 11 12
73 58 128 3 11 1 11 4
74 145 188 9 11 0 11 9
75 214 79 10 11 1 11 11
76 13 186 9 11 1 11 10
77 204 23 14 11 1 11 15
78 97 51 11 11 1 11 12
79 185 51 11 11 0 11 11
80 242 79 10 11 1 11 11
81 201 48 11 12 0 11 11
82 0 81 10 11 1 11 11
83 241 51 11 11 0 11 11
84 23 186 9 11 0 11 9
85 75 53 11 11 0 11 11
86 184 83 10 11 0 11 10
87 126 11 16 11 0 11 16
88 122 214 12 11 -1 11 10
89 109 55 11 11 -1 11 9
90 170 156 8 11 1 11 10
91 185 107 5 11 0 11 5
92 114 99 6 13 -1 11 4
93 191 107 5 11 0 11 5
94 150 124 10 4 3 11 16
95 12 242 8 2 0 -3 8
96 12 49 3 3 2 11 8
97 145 132 9 8 0 8 9
98 33 186 9 11 0 11 9
99 155 132 9 8 0 8 9
100 43 186 9 11 0 11 9
101 165 132 9 8 0 8 9
102 197 107 5 11 0 11 5
103 53 186 9 11 0 8 9
104 178 156 8 11 0 11 8
105 62 128 3 11 0 11 3
106 230 192 4 14 0 11 4
107 227 142 7 11 0 11 7
108 84 128 3 11 0 11 3
109 157 200 13 8 1 8 14
110 224 76 8 8 0 8 8
111 175 132 9 8 0 8 9
112 63 186 9 11 0 8 9
113 91 186 9 11 0 8 9
114 74 106 8 8 0 8 7
115 185 132 9 8 0 8 9
116 57 138 7 10 0 10 7
117 136 106 8 8 0 8 8
118 144 106 8 8 0 8 7
119 60 94 12 8 0 8 12
120 222 106 8 8 0 8 8
121 101 186 9 11 -1 8 7
122 146 114 6 8 1 8 8
123 142 76 6 14 1 11 8
124 252 55 3 15 3 11 9
125 148 76 6 14 1 11 8
126 160 124 10 4 1 7 12
160 0 256 0 0 0 0 11
161 88 128 3 11 1 11 4
162 127 186 9 11 1 9 11
163 194 83 10 11 1 11 11
164 235 132 9 8 0 10 10
165 213 55 11 11 -1 11 9
166 250 192 3 14 3 11 9
167 1 184 9 11 0 11 9
168 121 24 5 2 1 11 7
169 97 27 13 11 0 11 13
170 103 174 7 7 0 11 7
171 40 115 7 5 0 7 8
172 134 136 11 5 2 7 14
173 128 202 4 2 1 5 6
174 29 35 13 11 0 11 13
175 0 186 4 2 2 11 8
176 238 108 6 6 0 11 5
177 102 138 9 9 2 8 13
178 10 108 8 8 0 11 8
179 176 116 7 8 0 11 7
180 200 17 4 3 2 11 8
181 22 85 10 11 -1 8 9
182 186 156 8 11 0 11 8
183 207 168 3 2 1 7 5
184 28 19 5 5 2 0 8
185 125 150 5 7 1 11 8
186 171 124 7 7 0 11 7
187 55 115 7 5 0 7 8
188 220 11 15 11 1 11 16
189 142 11 16 11 1 11 17
190 158 11 16 11 0 11 16
191 155 184 9 11 0 11 9
192 139 256 11 15 0 15 11
193 151 256 11 15 0 15 11
194 163 256 11 15 0 15 11
195 113 240 11 14 0 14 11
196 137 240 11 14 0 14 11
197 112 256 12 15 0 15 12
198 52 256 17 11 -2 11 15
199 175 256 11 15 0 11 11
200 246 27 9 15 1 15 10
201 88 29 9 15 1 15 10
202 112 29 9 15 1 15 10
203 216 226 9 14 1 14 10
204 8 200 4 15 0 15 4
205 112 200 4 15 1 15 4
206 15 53 5 15 0 15 4
207 48 242 5 14 -1 14 3
208 0 212 12 11 0 11 12
209 149 240 11 14 1 14 12
210 187 256 11 15 0 15 11
211 199 256 11 15 0 15 11
212 211 256 11 15 0 15 11
213 161 240 11 14 0 14 11
214 173 240 11 14 0 14 11
215 112 138 9 9 2 9 13
216 136 226 11 13 0 12 11
217 223 256 11 15 0 15 11
218 235 256 11 15 0 15 11
219 53 244 11 15 0 15 11
220 185 240 11 14 0 14 11
221 65 244 11 15 -1 15 9
222 32 85 10 11 1 11 11
223 194 156 8 11 0 11 8
224 165 180 9 11 0 11 9
225 175 180 9 11 0 11 9
226 185 180 9 11 0 11 9
227 195 180 9 11 0 11 9
228 205 180 9 11 0 11 9
229 103 224 9 12 0 12 9
230 212 44 14 8 0 8 14
231 13 198 9 12 0 8 9
232 235 180 9 11 0 11 9
233 73 178 9 11 0 11 9
234 111 178 9 11 0 11 9
235 215 178 9 11 0 11 9
236 56 109 4 11 -1 11 3
237 70 109 4 11 0 11 3
238 20 109 5 11 -1 11 3
239 26 109 5 11 -1 11 3
240 93 198 9 12 0 12 9
241 234 156 8 11 0 11 8
242 225 178 9 11 0 11 9
243 245 178 9 11 0 11 9
244 137 176 9 11 0 11 9
245 11 174 9 11 0 11 9
246 21 174 9 11 0 11 9
247 184 71 9 7 2 8 13
248 243 142 9 10 0 9 9
249 70 154 8 11 0 11 8
250 100 154 8 11 0 11 8
251 202 154 8 11 0 11 8
252 210 154 8 11 0 11 8
253 226 226 9 14 -1 11 7
254 236 226 9 14 0 11 9
255 0 200 8 14 0 11 8
884 121 22 5 4 0 11 5
885 241 28 5 4 0 2 5
890 200 21 4 3 1 0 5
894 253 200 3 11 1 8 4
900 12 53 3 3 1 11 4
901 94 228 7 3 -1 11 5
902 104 212 12 11 -1 11 10
903 211 168 3 2 1 6 5
904 225 55 11 11 -1 11 11
905 134 212 12 11 -1 11 11
906 32 109 5 11 -1 11 5
908 122 35 13 11 -1 11 12
910 136 35 13 11 -1 11 12
911 150 35 13 11 -1 11 12
912 9 138 7 11 -2 11 3
913 146 212 12 11 -1 11 10
914 42 85 10 11 0 11 10
915 218 154 8 11 1 11 9
916 1 57 11 11 0 11 11
917 52 85 10 11 1 11 11
918 31 174 9 11 0 11 10
919 62 85 10 11 1 11 11
920 158 212 12 11 0 11 12
921 253 154 3 11 1 11 5
922 106 85 10 11 0 11 10
923 197 59 11 11 0 11 10
924 170 212 12 11 0 11 12
925 116 85 10 11 1 11 11
926 226 154 8 11 1 11 10
927 182 212 12 11 0 11 12
928 154 85 10 11 1 11 11
929 164 85 10 11 0 11 10
931 242 154 8 11 1 11 10
932 41 174 9 11 0 11 9
933 27 61 11 11 0 11 11
934 48 26 13 12 0 11 13
935 194 212 12 11 -1 11 10
936 200 35 13 11 0 11 13
937 206 212 12 11 0 11 12
938 120 99 6 13 -1 13 4
939 58 228 12 13 -1 13 10
940 82 87 10 11 0 11 10
941 17 138 7 11 0 11 7
942 24 200 8 14 0 11 8
943 113 128 3 11 0 11 3
944 130 152 8 11 0 11 8
945 244 100 10 8 0 8 10
946 246 226 9 14 0 11 9
947 51 174 9 11 0 8 8
948 61 174 9 11 0 11 9
949 184 116 7 8 0 8 7
950 32 200 8 14 0 11 7
951 10 150 8 11 0 8 8
952 83 174 9 11 0 11 9
953 10 184 3 8 0 8 3
954 82 108 8 8 0 8 8
955 18 150 8 11 0 11 8
956 26 150 8 11 0 8 8
957 114 108 8 8 0 8 8
958 40 200 8 14 0 11 7
959 245 132 9 8 0 8 9
960 0 102 10 8 -1 8 9
961 93 174 9 11 0 8 9
962 34 150 8 11 0 8 8
963 224 97 10 9 0 9 10
964 66 118 6 8 0 8 6
965 202 108 8 8 0 8 8
966 39 61 11 11 0 8 11
967 121 174 9 11 0 8 8
968 51 61 11 11 0 8 11
969 171 200 13 8 0 8 13
970 38 109 5 11 -1 11 3
971 42 150 8 11 0 11 8
972 1 172 9 11 0 11 9
973 50 150 8 11 0 11 8
974 214 35 13 11 0 11 13
976 147 172 9 11 0 11 9
977 126 87 10 11 0 11 10
978 63 61 11 11 0 11 10
979 228 35 13 11 -1 11 11
980 147 226 11 13 -1 13 10
981 197 240 11 14 0 11 11
982 185 200 13 8 0 8 13
986 209 240 11 14 0 11 11
988 48 200 8 14 1 11 9
990 20 224 9 14 0 11 9
992 121 61 11 11 0 11 11
994 98 15 13 15 1 11 14
995 61 26 13 12 0 8 13
996 146 200 10 12 0 12 10
997 169 192 9 12 0 9 9
998 246 256 10 15 1 11 11
999 0 136 9 9 0 9 9
1000 157 168 9 11 0 11 9
1001 58 150 8 11 0 8 8
1002 136 87 10 11 0 11 10
1003 60 102 10 8 0 8 9
1004 179 192 9 12 0 12 9
1005 10 99 10 9 0 9 10
1006 174 87 10 11 0 11 9
1007 234 192 8 12 -1 9 6
1008 94 102 10 8 0 8 10
1009 189 192 9 12 0 8 9
1010 244 108 8 8 0 8 8
1011 219 192 5 14 -2 11 3
1025 110 43 10 13 1 13 11
1026 1 37 13 11 0 11 13
1027 134 75 8 13 1 13 9
1028 133 61 11 11 0 11 11
1029 224 87 10 11 0 11 10
1030 117 128 3 11 1 11 5
1031 152 99 6 13 -1 13 4
1032 78 150 8 11 0 11 8
1033 32 11 18 11 -1 11 17
1034 174 11 16 11 1 11 17
1035 15 37 13 11 0 11 13
1036 136 200 9 13 1 13 10
1038 221 240 11 14 0 14 10
1039 0 226 10 14 1 11 11
1040 218 212 12 11 -1 11 10
1041 10 89 10 11 1 11 11
1042 72 89 10 11 0 11 10
1043 86 150 8 11 1 11 9
1044 36 242 12 14 -1 11 11
1045 92 91 10 11 1 11 11
1046 235 11 15 11 0 11 15
1047 204 91 10 11 0 11 10
1048 214 91 10 11 1 11 11
1049 28 49 10 13 1 13 11
1050 167 168 9 11 1 11 10
1051 145 61 11 11 -1 11 10
1052 230 212 12 11 0 11 12
1053 234 91 10 11 1 11 11
1054 242 212 12 11 0 11 12
1055 244 91 10 11 1 11 11
1056 0 93 10 11 0 11 10
1057 157 61 11 11 0 11 11
1058 177 168 9 11 0 11 9
1059 169 61 11 11 0 11 10
1060 12 210 12 11 0 11 12
1061 92 210 12 11 -1 11 10
1062 88 242 12 14 1 11 13
1063 184 95 10 11 0 11 10
1064 43 37 13 11 1 11 14
1065 51 14 15 14 1 11 16
1066 57 37 13 11 0 11 13
1067 71 37 13 11 1 11 14
1068 194 95 10 11 1 11 11
1069 87 63 11 11 0 11 11
1070 33 23 15 11 1 11 16
1071 80 202 12 11 -1 11 11
1072 65 130 9 8 0 8 9
1073 187 168 9 11 0 11 9
1074 0 110 8 8 0 8 8
1075 92 118 6 8 0 8 6
1076 86 52 10 10 -1 8 9
1077 93 130 9 8 0 8 9
1078 158 94 11 8 0 8 11
1079 192 116 7 8 0 8 7
1080 60 110 8 8 0 8 8
1081 108 150 8 11 0 11 8
1082 200 116 7 8 0 8 7
1083 121 130 9 8 0 8 9
1084 104 102 10 8 0 8 10
1085 90 110 8 8 0 8 8
1086 131 130 9 8 0 8 9
1087 98 110 8 8 0 8 8
1088 197 168 9 11 0 8 9
1089 106 110 8 8 0 8 8
1090 238 116 7 8 0 8 7
1091 116 150 8 11 0 8 8
1092 191 14 13 14 0 11 13
1093 152 110 8 8 0 8 8
1094 235 168 9 11 0 8 9
1095 160 110 8 8 0 8 8
1096 198 200 13 8 0 8 13
1097 163 37 13 11 0 8 13
1098 158 102 10 8 0 8 10
1099 127 96 11 8 0 8 11
1100 230 110 8 8 0 8 8
1101 246 116 7 8 0 8 8
1102 102 94 12 8 0 8 12
1103 122 112 8 8 0 8 8
1105 71 166 9 11 0 11 9
1106 94 224 9 14 0 11 9
1107 216 103 6 11 0 11 6
1108 0 118 7 8 1 8 8
1109 168 112 8 8 0 8 8
1110 41 126 3 11 0 11 3
1111 44 109 5 11 -1 11 4
1112 224 192 5 14 -2 11 3
1113 226 44 14 8 0 8 14
1114 212 200 13 8 0 8 13
1115 103 166 9 11 0 11 9
1116 25 138 7 11 0 11 7
1118 56 200 8 14 0 11 8
1119 0 148 8 11 0 8 8
1120 127 23 15 11 0 11 15
1121 226 200 13 8 0 8 13
1122 181 63 11 11 0 11 11
1123 207 166 9 11 0 11 9
1124 218 23 14 11 1 11 15
1125 170 96 11 8 0 8 11
1126 237 63 11 11 0 11 11
1127 72 98 11 8 -1 8 9
1128 232 23 14 11 1 11 15
1129 240 200 13 8 0 8 12
1130 13 65 11 11 -1 11 9
1131 138 98 11 8 -1 8 9
1132 143 23 15 11 1 11 15
1133 156 192 13 8 0 8 12
1134 102 256 10 17 0 14 10
1135 64 200 8 14 0 11 8
1136 177 37 13 11 0 11 13
1137 75 65 11 11 0 8 11
1138 99 67 11 11 1 11 13
1139 210 112 8 8 1 8 10
1140 209 67 11 11 0 11 11
1141 74 114 8 8 0 8 8
1142 232 240 11 14 0 14 11
1143 138 148 8 11 0 11 8
1144 0 256 20 14 0 11 20
1145 70 256 17 11 0 8 17
1146 84 42 12 12 0 11 12
1147 195 130 9 8 0 8 9
1148 20 256 15 15 0 15 15
1149 75 26 13 12 0 12 13
1150 67 14 15 14 0 14 15
1151 97 39 13 11 0 11 13
1152 158 226 11 13 0 11 11
1153 136 186 9 10 0 8 9
1154 20 97 10 11 0 11 11
1155 110 184 7 3 -1 12 5
1156 146 176 6 3 0 12 6
1157 82 178 7 3 -1 12 6
1158 120 178 7 3 0 12 6
1168 20 52 7 14 1 14 7
1169 178 107 6 11 0 11 5
1170 217 166 9 11 0 11 9
1171 98 118 6 8 0 8 6
1172 10 226 10 14 1 11 11
1173 72 200 8 14 0 11 8
1174 16 14 16 14 0 11 15
1175 116 202 12 11 0 8 11
1176 30 214 9 14 0 11 9
1177 33 138 7 11 0 8 7
1178 40 214 9 14 1 11 9
1179 146 144 8 11 0 8 7
1180 245 166 9 11 1 11 10
1181 16 118 7 8 0 8 7
1182 30 97 10 11 0 11 10
1183 24 118 7 8 0 8 7
1184 242 39 13 11 -1 11 12
1185 126 104 10 8 0 8 9
1186 244 240 11 14 1 11 12
1187 131 164 9 11 0 8 9
1188 0 25 14 11 1 11 15
1189 168 104 10 8 0 8 10
1190 76 244 11 15 1 11 12
1191 199 192 9 12 0 8 9
1192 124 242 12 14 0 11 12
1193 11 162 9 11 0 8 9
1194 101 238 11 14 0 11 11
1195 21 162 9 11 0 8 9
1196 50 214 9 14 0 11 9
1197 41 138 7 11 0 8 7
1198 221 67 11 11 0 11 11
1199 31 162 9 11 0 8 8
1200 1 69 11 11 0 11 10
1201 41 162 9 11 -1 8 7
1202 12 240 12 14 -1 11 10
1203 51 162 9 11 0 8 8
1204 83 14 15 14 0 11 15
1205 193 71 11 11 0 8 11
1206 35 228 11 14 0 11 10
1207 61 162 9 11 0 8 8
1208 40 97 10 11 0 11 10
1209 130 114 8 8 0 8 8
1210 50 97 10 11 1 11 11
1211 154 144 8 11 0 11 8
1212 14 25 14 11 -1 11 13
1213 83 100 11 8 -1 8 10
1214 112 14 14 14 -1 11 13
1215 25 73 11 11 -1 8 10
1216 45 126 3 11 1 11 5
1217 87 256 15 13 0 13 15
1218 37 73 11 11 0 11 11
1219 24 240 10 15 1 11 10
1220 242 192 8 12 0 8 7
1223 0 242 11 15 1 11 12
1224 209 192 9 12 0 8 9
1227 112 226 10 14 0 11 10
1228 162 144 8 11 0 8 8
1232 70 228 12 13 -1 13 10
1233 81 162 9 11 0 11 9
1234 82 228 12 13 -1 13 10
1235 170 144 8 11 0 11 9
1236 158 23 15 11 0 11 16
1237 0 46 14 8 0 8 15
1238 60 214 9 14 1 14 11
1239 91 162 9 11 0 11 9
1240 49 73 11 11 0 11 11
1241 205 130 9 8 0 8 9
1242 169 226 11 13 0 13 11
1243 113 162 9 11 0 11 9
1244 36 256 16 13 -1 13 14
1245 61 73 11 11 0 11 11
1246 120 49 10 13 0 13 10
1247 178 144 8 11 0 11 8
1248 1 160 9 11 0 11 9
1249 186 144 8 11 0 8 8
1250 130 49 10 13 1 13 11
1251 194 144 8 11 0 11 8
1252 140 49 10 13 1 13 11
1253 234 144 8 11 0 11 9
1254 124 228 12 13 0 13 12
1255 141 160 9 11 0 11 9
1256 111 73 11 11 1 11 13
1257 138 114 8 8 1 8 10
1258 181 226 11 13 1 13 13
1259 66 142 8 11 1 11 10
1262 193 226 11 13 0 13 10
1263 70 214 9 14 0 11 8
1264 205 226 11 13 0 13 11
1265 104 200 8 14 0 11 8
1266 46 228 11 14 0 14 10
1267 128 200 8 14 0 11 8
1268 150 49 10 13 0 13 10
1269 94 142 8 11 0 11 8
1272 173 25 13 13 1 13 14
1273 123 73 11 11 0 11 11
1425 43 26 5 2 1 -1 7
1426 12 212 6 2 0 10 6
1427 34 240 2 5 2 12 6
1428 24 55 3 3 2 11 6
1429 84 29 4 3 1 11 6
1430 131 166 3 2 1 -1 5
1431 48 14 2 2 2 10 6
1432 28 23 5 3 1 10 6
1433 243 68 5 4 -1 11 6
1434 90 190 3 4 3 0 6
1435 4 186 4 2 1 -1 6
1436 152 176 3 4 1 11 5
1437 162 172 3 4 3 11 6
1438 196 43 5 3 1 11 6
1439 99 56 7 4 -1 11 5
1440 164 184 4 4 3 11 6
1441 196 47 5 3 0 11 6
1443 110 180 4 2 1 -1 6
1444 80 166 3 4 1 0 6
1445 186 14 5 2 1 -1 7
1446 156 172 6 3 0 0 6
1447 208 55 4 3 1 0 6
1448 83 92 5 4 1 11 7
1449 140 164 4 4 -1 11 6
1450 164 26 5 2 1 -1 7
1451 109 154 3 4 1 12 6
1452 214 180 4 2 1 11 6
1453 203 156 3 2 3 -1 6
1454 236 47 5 3 -1 10 6
1455 192 59 4 3 1 10 6
1456 106 55 3 3 2 0 6
1457 112 166 6 3 0 0 6
1458 208 51 5 3 0 0 5
1459 236 51 5 3 0 0 5
1460 67 150 3 2 2 -1 6
1461 244 180 4 2 1 -1 6
1462 232 59 4 3 1 0 6
1463 67 148 3 2 1 -1 5
1464 147 148 3 2 1 -1 5
1465 67 146 3 2 2 10 6
1467 192 55 5 3 0 0 6
1468 147 146 3 2 2 5 6
1469 67 144 3 2 2 -1 7
1470 38 38 5 2 0 8 5
1471 75 142 3 2 1 10 5
1472 117 212 3 10 0 9 3
1473 103 142 3 2 2 10 6
1474 125 142 3 2 2 10 6
1475 70 174 3 8 0 8 3
1476 253 142 3 2 2 10 6
1488 215 130 9 8 0 8 9
1489 218 114 8 8 1 8 9
1490 32 118 7 8 0 8 7
1491 48 118 7 8 0 8 7
1492 225 130 9 8 0 8 9
1493 130 174 5 8 0 8 5
1494 24 210 5 9 0 9 5
1495 49 128 9 8 0 8 9
1496 75 128 9 8 0 8 9
1497 211 104 5 4 0 8 5
1498 202 142 8 11 0 8 8
1499 8 116 8 8 0 8 8
1500 210 142 8 11 0 11 8
1501 103 128 9 8 0 8 9
1502 1 126 9 8 0 8 9
1503 50 109 5 11 0 8 5
1504 94 150 5 8 0 8 5
1505 11 126 9 8 0 8 9
1506 124 140 9 10 0 8 9
1507 151 156 9 11 0 8 9
1508 21 126 9 8 0 8 9
1509 218 142 8 11 0 8 8
1510 82 116 8 8 0 8 8
1511 161 156 9 11 0 8 9
1512 114 116 8 8 0 8 8
1513 204 100 11 8 0 8 11
1514 234 101 10 9 0 8 10
1520 31 126 9 8 0 8 9
1521 141 124 9 8 0 8 9
1522 75 42 9 4 0 8 9
1523 150 160 4 4 0 10 3
1524 235 80 7 4 0 10 6
65075 248 67 3 15 -1 13 16
65533 124 256 14 13 1 12 16
Index: ps/trunk/source/gui/CGUIText.cpp
===================================================================
--- ps/trunk/source/gui/CGUIText.cpp (revision 26914)
+++ ps/trunk/source/gui/CGUIText.cpp (revision 26915)
@@ -1,474 +1,497 @@
/* Copyright (C) 2022 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "CGUIText.h"
#include "graphics/Canvas2D.h"
#include "graphics/FontMetrics.h"
#include "graphics/TextRenderer.h"
#include "gui/CGUI.h"
#include "gui/ObjectBases/IGUIObject.h"
#include "gui/SettingTypes/CGUIString.h"
#include "ps/CStrInternStatic.h"
#include "ps/VideoMode.h"
#include "renderer/backend/IDeviceCommandContext.h"
#include "renderer/Renderer.h"
#include
extern int g_xres, g_yres;
// TODO Gee: CRect => CPoint ?
void SGenerateTextImage::SetupSpriteCall(
const bool left, CGUIText::SSpriteCall& spriteCall, const float width, const float y,
const CSize2D& size, const CStr& textureName, const float bufferZone)
{
// TODO Gee: Temp hardcoded values
spriteCall.m_Area.top = y + bufferZone;
spriteCall.m_Area.bottom = y + bufferZone + size.Height;
if (left)
{
spriteCall.m_Area.left = bufferZone;
spriteCall.m_Area.right = size.Width + bufferZone;
}
else
{
spriteCall.m_Area.left = width - bufferZone - size.Width;
spriteCall.m_Area.right = width - bufferZone;
}
spriteCall.m_Sprite = textureName;
m_YFrom = spriteCall.m_Area.top - bufferZone;
m_YTo = spriteCall.m_Area.bottom + bufferZone;
m_Indentation = size.Width + bufferZone * 2;
}
CGUIText::CGUIText(const CGUI& pGUI, const CGUIString& string, const CStrW& fontW, const float width, const float bufferZone, const EAlign align, const IGUIObject* pObject)
{
if (string.m_Words.empty())
return;
CStrIntern font(fontW.ToUTF8());
float y = bufferZone; // drawing pointer
float lineWidth = 0.f;
int from = 0;
bool firstLine = true; // Necessary because text in the first line is shorter
// (it doesn't count the line spacing)
// Images on the left or the right side.
SGenerateTextImages images;
int posLastImage = -1; // Position in the string where last img (either left or right) were encountered.
// in order to avoid duplicate processing.
- // Go through string word by word
+ // The calculated width of each word includes the space between the current
+ // word and the next. When we're wrapping, we need subtract the width of the
+ // space after the last word on the line before the wrap.
+ CFontMetrics currentFont(font);
+ float spaceWidth = currentFont.GetCharacterWidth(L' ');
+
+ // Go through string word by word.
+ // a word is defined as [start, end[ in string.m_Words so we skip the last item.
for (int i = 0; i < static_cast(string.m_Words.size()) - 1; ++i)
{
// Pre-process each line one time, so we know which floating images
// will be added for that line.
// Generated stuff is stored in feedback.
CGUIString::SFeedback feedback;
// Preliminary line height, used for word-wrapping with floating images.
float prelimLineHeight = 0.f;
// Width and height of all text calls generated.
string.GenerateTextCall(pGUI, feedback, font, string.m_Words[i], string.m_Words[i+1], firstLine);
SetupSpriteCalls(pGUI, feedback.m_Images, y, width, bufferZone, i, posLastImage, images);
posLastImage = std::max(posLastImage, i);
lineWidth += feedback.m_Size.Width;
prelimLineHeight = std::max(prelimLineHeight, feedback.m_Size.Height);
+ float spaceCorrection = feedback.m_EndsWithSpace ? spaceWidth : 0.f;
+
// If width is 0, then there's no word-wrapping, disable NewLine.
- if ((width != 0 && from != i && (lineWidth + 2 * bufferZone > width || feedback.m_NewLine)) || i == static_cast(string.m_Words.size()) - 2)
+ if ((width != 0 && from != i && (lineWidth - spaceCorrection + 2 * bufferZone > width || feedback.m_NewLine)) || i == static_cast(string.m_Words.size()) - 2)
{
if (ProcessLine(pGUI, string, font, pObject, images, align, prelimLineHeight, width, bufferZone, firstLine, y, i, from))
return;
lineWidth = 0.f;
}
}
}
// Loop through our images queues, to see if images have been added.
void CGUIText::SetupSpriteCalls(
const CGUI& pGUI,
const std::array, 2>& feedbackImages,
const float y,
const float width,
const float bufferZone,
const int i,
const int posLastImage,
SGenerateTextImages& images)
{
// Check if this has already been processed.
// Also, floating images are only applicable if Word-Wrapping is on
if (width == 0 || i <= posLastImage)
return;
// Loop left/right
for (int j = 0; j < 2; ++j)
for (const CStr& imgname : feedbackImages[j])
{
SSpriteCall spriteCall;
SGenerateTextImage image;
// Y is if no other floating images is above, y. Else it is placed
// after the last image, like a stack downwards.
float _y;
if (!images[j].empty())
_y = std::max(y, images[j].back().m_YTo);
else
_y = y;
const SGUIIcon& icon = pGUI.GetIcon(imgname);
image.SetupSpriteCall(j == CGUIString::SFeedback::Left, spriteCall, width, _y, icon.m_Size, icon.m_SpriteName, bufferZone);
// Check if image is the lowest thing.
m_Size.Height = std::max(m_Size.Height, image.m_YTo);
images[j].emplace_back(image);
m_SpriteCalls.emplace_back(std::move(spriteCall));
}
}
// Now we'll do another loop to figure out the height and width of
// the line (the height of the largest character and the width is
// the sum of all of the individual widths). This
// couldn't be determined in the first loop (main loop)
// because it didn't regard images, so we don't know
// if all characters processed, will actually be involved
// in that line.
void CGUIText::ComputeLineSize(
const CGUI& pGUI,
const CGUIString& string,
const CStrIntern& font,
const bool firstLine,
const float width,
const float widthRangeFrom,
const float widthRangeTo,
const int i,
const int tempFrom,
CSize2D& lineSize) const
{
+ // The calculated width of each word includes the space between the current
+ // word and the next. When we're wrapping, we need subtract the width of the
+ // space after the last word on the line before the wrap.
+ CFontMetrics currentFont(font);
+ float spaceWidth = currentFont.GetCharacterWidth(L' ');
+
+ float spaceCorrection = 0.f;
+
float x = widthRangeFrom;
for (int j = tempFrom; j <= i; ++j)
{
// We don't want to use feedback now, so we'll have to use another one.
CGUIString::SFeedback feedback2;
// Don't attach object, it'll suppress the errors
// we want them to be reported in the final GenerateTextCall()
// so that we don't get duplicates.
string.GenerateTextCall(pGUI, feedback2, font, string.m_Words[j], string.m_Words[j+1], firstLine);
// Append X value.
x += feedback2.m_Size.Width;
- if (width != 0 && x > widthRangeTo && j != tempFrom && !feedback2.m_NewLine)
- {
- // The calculated width of each word includes the space between the current
- // word and the next. When we're wrapping, we need subtract the width of the
- // space after the last word on the line before the wrap.
- CFontMetrics currentFont(font);
- lineSize.Width -= currentFont.GetCharacterWidth(*L" ");
+ if (width != 0 && x - spaceCorrection > widthRangeTo && j != tempFrom && !feedback2.m_NewLine)
break;
- }
+
+ // Update after the line-break detection, because otherwise spaceCorrection above
+ // will refer to the wrapped word and not the last-word-before-the-line-break.
+ spaceCorrection = feedback2.m_EndsWithSpace ? spaceWidth : 0.f;
// Let lineSize.cy be the maximum m_Height we encounter.
lineSize.Height = std::max(lineSize.Height, feedback2.m_Size.Height);
// If the current word is an explicit new line ("\n"),
// break now before adding the width of this character.
// ("\n" doesn't have a glyph, thus is given the same width as
// the "missing glyph" character by CFont::GetCharacterWidth().)
if (width != 0 && feedback2.m_NewLine)
break;
lineSize.Width += feedback2.m_Size.Width;
}
+ // Remove the space if necessary.
+ lineSize.Width -= spaceCorrection;
}
bool CGUIText::ProcessLine(
const CGUI& pGUI,
const CGUIString& string,
const CStrIntern& font,
const IGUIObject* pObject,
const SGenerateTextImages& images,
const EAlign align,
const float prelimLineHeight,
const float width,
const float bufferZone,
bool& firstLine,
float& y,
int& i,
int& from)
{
// Change 'from' to 'i', but first keep a copy of its value.
int tempFrom = from;
from = i;
float widthRangeFrom = bufferZone;
float widthRangeTo = width - bufferZone;
ComputeLineRange(images, y, width, prelimLineHeight, widthRangeFrom, widthRangeTo);
CSize2D lineSize;
ComputeLineSize(pGUI, string, font, firstLine, width, widthRangeFrom, widthRangeTo, i, tempFrom, lineSize);
// Move down, because font drawing starts from the baseline
y += lineSize.Height;
// Do the real processing now
const bool done = AssembleCalls(pGUI, string, font, pObject, firstLine, width, widthRangeTo, GetLineOffset(align, widthRangeFrom, widthRangeTo, lineSize), y, tempFrom, i, from);
// Update dimensions
m_Size.Width = std::max(m_Size.Width, lineSize.Width + bufferZone * 2);
m_Size.Height = std::max(m_Size.Height, y + bufferZone);
firstLine = false;
// Now if we entered as from = i, then we want
// i being one minus that, so that it will become
// the same i in the next loop. The difference is that
// we're on a new line now.
i = from - 1;
return done;
}
// Decide width of the line. We need to iterate our floating images.
// this won't be exact because we're assuming the lineSize.cy
// will be as our preliminary calculation said. But that may change,
// although we'd have to add a couple of more loops to try straightening
// this problem out, and it is very unlikely to happen noticeably if one
// structures his text in a stylistically pure fashion. Even if not, it
// is still quite unlikely it will happen.
// Loop through left and right side, from and to.
void CGUIText::ComputeLineRange(
const SGenerateTextImages& images,
const float y,
const float width,
const float prelimLineHeight,
float& widthRangeFrom,
float& widthRangeTo) const
{
// Floating images are only applicable if word-wrapping is enabled.
if (width == 0)
return;
for (int j = 0; j < 2; ++j)
for (const SGenerateTextImage& img : images[j])
{
// We're working with two intervals here, the image's and the line height's.
// let's find the union of these two.
float unionFrom, unionTo;
unionFrom = std::max(y, img.m_YFrom);
unionTo = std::min(y + prelimLineHeight, img.m_YTo);
// The union is not empty
if (unionTo > unionFrom)
{
if (j == 0)
widthRangeFrom = std::max(widthRangeFrom, img.m_Indentation);
else
widthRangeTo = std::min(widthRangeTo, width - img.m_Indentation);
}
}
}
// compute offset based on what kind of alignment
float CGUIText::GetLineOffset(
const EAlign align,
const float widthRangeFrom,
const float widthRangeTo,
const CSize2D& lineSize) const
{
switch (align)
{
case EAlign::LEFT:
return widthRangeFrom;
case EAlign::CENTER:
return (widthRangeTo + widthRangeFrom - lineSize.Width) / 2;
case EAlign::RIGHT:
return widthRangeTo - lineSize.Width;
default:
debug_warn(L"Broken EAlign in CGUIText()");
return 0.f;
}
}
bool CGUIText::AssembleCalls(
const CGUI& pGUI,
const CGUIString& string,
const CStrIntern& font,
const IGUIObject* pObject,
const bool firstLine,
const float width,
const float widthRangeTo,
const float dx,
const float y,
const int tempFrom,
const int i,
int& from)
{
bool done = false;
float x = dx;
for (int j = tempFrom; j <= i; ++j)
{
// We don't want to use feedback now, so we'll have to use another one.
CGUIString::SFeedback feedback2;
// Defaults
string.GenerateTextCall(pGUI, feedback2, font, string.m_Words[j], string.m_Words[j+1], firstLine, pObject);
// Iterate all and set X/Y values
// Since X values are not set, we need to make an internal
// iteration with an increment that will append the internal
// x, that is what xPointer is for.
float xPointer = 0.f;
for (STextCall& tc : feedback2.m_TextCalls)
{
tc.m_Pos = CVector2D(x + xPointer, y);
xPointer += tc.m_Size.Width;
if (tc.m_pSpriteCall)
tc.m_pSpriteCall->m_Area += tc.m_Pos - CSize2D(0, tc.m_pSpriteCall->m_Area.GetHeight());
}
- // Append X value.
x += feedback2.m_Size.Width;
- // The first word overrides the width limit, what we
- // do, in those cases, are just drawing that word even
- // though it'll extend the object.
if (width != 0) // only if word-wrapping is applicable
{
+ // Check if we need to wrap, using the same algorithm as ComputeLineSize
+ // This means we must ignore the 'space before the next word' for the purposes of wrapping.
+ CFontMetrics currentFont(font);
+ float spaceWidth = currentFont.GetCharacterWidth(L' ');
+ float spaceCorrection = feedback2.m_EndsWithSpace ? spaceWidth : 0.f;
+
if (feedback2.m_NewLine)
{
from = j + 1;
// Sprite call can exist within only a newline segment,
// therefore we need this.
if (!feedback2.m_SpriteCalls.empty())
{
auto newEnd = std::remove_if(feedback2.m_TextCalls.begin(), feedback2.m_TextCalls.end(), [](const STextCall& call) { return !call.m_pSpriteCall; });
m_TextCalls.insert(
m_TextCalls.end(),
std::make_move_iterator(feedback2.m_TextCalls.begin()),
std::make_move_iterator(newEnd));
m_SpriteCalls.insert(
m_SpriteCalls.end(),
std::make_move_iterator(feedback2.m_SpriteCalls.begin()),
std::make_move_iterator(feedback2.m_SpriteCalls.end()));
}
break;
}
- else if (x > widthRangeTo && j == tempFrom)
+ else if (x - spaceCorrection > widthRangeTo && j == tempFrom)
{
+ // The first word overrides the width limit, what we do,
+ // in those cases, is just drawing that word even
+ // though it'll extend the object.
+ // Ergo: do not break, since we want it to be added to m_TextCalls.
from = j+1;
- // do not break, since we want it to be added to m_TextCalls
+ // To avoid doing redundant computations, set up j to exit the loop right away.
+ j = i + 1;
}
- else if (x > widthRangeTo)
+ else if (x - spaceCorrection > widthRangeTo)
{
from = j;
break;
}
}
// Add the whole feedback2.m_TextCalls to our m_TextCalls.
m_TextCalls.insert(
m_TextCalls.end(),
std::make_move_iterator(feedback2.m_TextCalls.begin()),
std::make_move_iterator(feedback2.m_TextCalls.end()));
m_SpriteCalls.insert(
m_SpriteCalls.end(),
std::make_move_iterator(feedback2.m_SpriteCalls.begin()),
std::make_move_iterator(feedback2.m_SpriteCalls.end()));
if (j == static_cast(string.m_Words.size()) - 2)
done = true;
}
return done;
}
void CGUIText::Draw(CGUI& pGUI, CCanvas2D& canvas, const CGUIColor& DefaultColor, const CVector2D& pos, CRect clipping) const
{
Renderer::Backend::IDeviceCommandContext* deviceCommandContext =
g_Renderer.GetDeviceCommandContext();
const bool isClipped = clipping != CRect();
if (isClipped)
{
// Make clipping rect as small as possible to prevent rounding errors
clipping.top = std::ceil(clipping.top);
clipping.bottom = std::floor(clipping.bottom);
clipping.left = std::ceil(clipping.left);
clipping.right = std::floor(clipping.right);
if (clipping.GetWidth() <= 0.0f || clipping.GetHeight() <= 0.0f)
return;
const float scale = g_VideoMode.GetScale();
Renderer::Backend::IDeviceCommandContext::Rect scissorRect;
scissorRect.x = std::ceil(clipping.left * scale);
scissorRect.y = std::ceil(g_yres - clipping.bottom * scale);
scissorRect.width = std::floor(clipping.GetWidth() * scale);
scissorRect.height = std::floor(clipping.GetHeight() * scale);
// TODO: move scissors to CCanvas2D.
deviceCommandContext->SetScissors(1, &scissorRect);
}
CTextRenderer textRenderer;
textRenderer.SetClippingRect(clipping);
textRenderer.Translate(0.0f, 0.0f);
for (const STextCall& tc : m_TextCalls)
{
// If this is just a placeholder for a sprite call, continue
if (tc.m_pSpriteCall)
continue;
textRenderer.SetCurrentColor(tc.m_UseCustomColor ? tc.m_Color : DefaultColor);
textRenderer.SetCurrentFont(tc.m_Font);
textRenderer.Put(floorf(pos.X + tc.m_Pos.X), floorf(pos.Y + tc.m_Pos.Y), &tc.m_String);
}
canvas.DrawText(textRenderer);
for (const SSpriteCall& sc : m_SpriteCalls)
pGUI.DrawSprite(sc.m_Sprite, canvas, sc.m_Area + pos);
if (isClipped)
deviceCommandContext->SetScissors(0, nullptr);
}
Index: ps/trunk/source/gui/SettingTypes/CGUIString.cpp
===================================================================
--- ps/trunk/source/gui/SettingTypes/CGUIString.cpp (revision 26914)
+++ ps/trunk/source/gui/SettingTypes/CGUIString.cpp (revision 26915)
@@ -1,478 +1,489 @@
/* Copyright (C) 2022 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "CGUIString.h"
#include "graphics/FontMetrics.h"
#include "gui/CGUI.h"
#include "gui/ObjectBases/IGUIObject.h"
#include "lib/utf8.h"
#include "ps/CLogger.h"
#include
#include
// List of word delimiter bounds
// The list contains ranges of word delimiters. The odd indexed chars are the start
// of a range, the even are the end of a range. The list must be sorted in INCREASING ORDER
static const int NUM_WORD_DELIMITERS = 4*2;
static const u16 WordDelimiters[NUM_WORD_DELIMITERS] = {
' ' , ' ', // spaces
'-' , '-', // hyphens
0x3000, 0x31FF, // ideographic symbols
0x3400, 0x9FFF
// TODO add unicode blocks of other languages that don't use spaces
};
void CGUIString::SFeedback::Reset()
{
m_Images[Left].clear();
m_Images[Right].clear();
m_TextCalls.clear();
m_SpriteCalls.clear();
m_Size = CSize2D();
m_NewLine = false;
}
void CGUIString::GenerateTextCall(const CGUI& pGUI, SFeedback& Feedback, CStrIntern DefaultFont, const int& from, const int& to, const bool FirstLine, const IGUIObject* pObject) const
{
// Reset width and height, because they will be determined with incrementation
// or comparisons.
Feedback.Reset();
// Check out which text chunk this is within.
for (const TextChunk& textChunk : m_TextChunks)
{
// Get the area that is overlapped by both the TextChunk and
// by the from/to inputted.
int _from = std::max(from, textChunk.m_From);
int _to = std::min(to, textChunk.m_To);
// If from is larger than to, then they are not overlapping
if (_to == _from && textChunk.m_From == textChunk.m_To)
{
// These should never be able to have more than one tag.
ENSURE(textChunk.m_Tags.size() == 1);
// Icons and images are placed on exactly one position
// in the words-list, and they can be counted twice if placed
// on an edge. But there is always only one logical preference
// that we want. This check filters the unwanted.
// it's in the end of one word, and the icon
// should really belong to the beginning of the next one
if (_to == to && to >= 1 && to < (int)m_RawString.length())
{
if (m_RawString[to-1] == ' ' ||
m_RawString[to-1] == '-' ||
m_RawString[to-1] == '\n')
continue;
}
// This std::string is just a break
if (_from == from && from >= 1)
{
if (m_RawString[from] == '\n' &&
m_RawString[from-1] != '\n' &&
m_RawString[from-1] != ' ' &&
m_RawString[from-1] != '-')
continue;
}
const TextChunk::Tag& tag = textChunk.m_Tags[0];
ENSURE(tag.m_TagType == TextChunk::Tag::TAG_IMGLEFT ||
tag.m_TagType == TextChunk::Tag::TAG_IMGRIGHT ||
tag.m_TagType == TextChunk::Tag::TAG_ICON);
const std::string& path = utf8_from_wstring(tag.m_TagValue);
if (!pGUI.HasIcon(path))
{
if (pObject)
LOGERROR("Trying to use an icon, imgleft or imgright-tag with an undefined icon (\"%s\").", path.c_str());
continue;
}
switch (tag.m_TagType)
{
case TextChunk::Tag::TAG_IMGLEFT:
Feedback.m_Images[SFeedback::Left].push_back(path);
break;
case TextChunk::Tag::TAG_IMGRIGHT:
Feedback.m_Images[SFeedback::Right].push_back(path);
break;
case TextChunk::Tag::TAG_ICON:
{
// We'll need to setup a text-call that will point
// to the icon, this is to be able to iterate
// through the text-calls without having to
// complex the structure virtually for nothing more.
CGUIText::STextCall TextCall;
// Also add it to the sprites being rendered.
CGUIText::SSpriteCall SpriteCall;
// Get Icon from icon database in pGUI
const SGUIIcon& icon = pGUI.GetIcon(path);
const CSize2D& size = icon.m_Size;
// append width, and make maximum height the height.
Feedback.m_Size.Width += size.Width;
Feedback.m_Size.Height = std::max(Feedback.m_Size.Height, size.Height);
// These are also needed later
TextCall.m_Size = size;
SpriteCall.m_Area = size;
// Handle additional attributes
for (const TextChunk::Tag::TagAttribute& tagAttrib : tag.m_TagAttributes)
{
if (tagAttrib.attrib == L"displace" && !tagAttrib.value.empty())
{
// Displace the sprite
CSize2D displacement;
// Parse the value
if (!CGUI::ParseString(&pGUI, tagAttrib.value, displacement))
LOGERROR("Error parsing 'displace' value for tag [ICON]");
else
SpriteCall.m_Area += displacement;
}
else if (tagAttrib.attrib == L"tooltip")
TextCall.m_Tooltip = tagAttrib.value;
}
SpriteCall.m_Sprite = icon.m_SpriteName;
// Add sprite call
Feedback.m_SpriteCalls.push_back(std::move(SpriteCall));
// Finalize text call
TextCall.m_pSpriteCall = &Feedback.m_SpriteCalls.back();
// Add text call
Feedback.m_TextCalls.emplace_back(std::move(TextCall));
break;
}
NODEFAULT;
}
}
else if (_to > _from && !Feedback.m_NewLine)
{
CGUIText::STextCall TextCall;
// Set defaults
TextCall.m_Font = DefaultFont;
TextCall.m_UseCustomColor = false;
TextCall.m_String = m_RawString.substr(_from, _to-_from);
// Go through tags and apply changes.
for (const TextChunk::Tag& tag : textChunk.m_Tags)
{
switch (tag.m_TagType)
{
case TextChunk::Tag::TAG_COLOR:
TextCall.m_UseCustomColor = true;
if (!CGUI::ParseString(&pGUI, tag.m_TagValue, TextCall.m_Color) && pObject)
LOGERROR("Error parsing the value of a [color]-tag in GUI text when reading object \"%s\".", pObject->GetPresentableName().c_str());
break;
case TextChunk::Tag::TAG_FONT:
// TODO Gee: (2004-08-15) Check if Font exists?
TextCall.m_Font = CStrIntern(utf8_from_wstring(tag.m_TagValue));
break;
case TextChunk::Tag::TAG_TOOLTIP:
TextCall.m_Tooltip = tag.m_TagValue;
break;
default:
LOGERROR("Encountered unexpected tag applied to text");
break;
}
}
// Calculate the size of the font.
CSize2D size;
int cx, cy;
CFontMetrics font (TextCall.m_Font);
font.CalculateStringSize(TextCall.m_String.c_str(), cx, cy);
size.Width = static_cast(cx);
// For anything other than the first line, the line spacing
// needs to be considered rather than just the height of the text.
if (FirstLine)
size.Height = static_cast(font.GetHeight());
else
size.Height = static_cast(font.GetLineSpacing());
// Append width, and make maximum height the height.
Feedback.m_Size.Width += size.Width;
Feedback.m_Size.Height = std::max(Feedback.m_Size.Height, size.Height);
// These are also needed later
TextCall.m_Size = size;
if (!TextCall.m_String.empty() && TextCall.m_String[0] == '\n')
Feedback.m_NewLine = true;
+ // Multiple empty spaces are treated as individual words (one per space),
+ // and for coherence we'll do the 'ignore space after the word' thing
+ // only if the word actually has some other text in it, so process this only if size >= 2
+ else if (TextCall.m_String.size() >= 2)
+ {
+ const wchar_t lastChar = TextCall.m_String.back();
+ // If the last word ends with a 'space', we'll ignore it when aligning, so mark it.
+ Feedback.m_EndsWithSpace = lastChar == ' ' || lastChar == 0x3000;
+ }
+ else
+ Feedback.m_EndsWithSpace = false;
// Add text-chunk
Feedback.m_TextCalls.emplace_back(std::move(TextCall));
}
}
}
bool CGUIString::TextChunk::Tag::SetTagType(const CStrW& tagtype)
{
TagType t = GetTagType(tagtype);
if (t == TAG_INVALID)
return false;
m_TagType = t;
return true;
}
CGUIString::TextChunk::Tag::TagType CGUIString::TextChunk::Tag::GetTagType(const CStrW& tagtype) const
{
if (tagtype == L"color")
return TAG_COLOR;
if (tagtype == L"font")
return TAG_FONT;
if (tagtype == L"icon")
return TAG_ICON;
if (tagtype == L"imgleft")
return TAG_IMGLEFT;
if (tagtype == L"imgright")
return TAG_IMGRIGHT;
if (tagtype == L"tooltip")
return TAG_TOOLTIP;
return TAG_INVALID;
}
void CGUIString::SetValue(const CStrW& str)
{
m_OriginalString = str;
m_TextChunks.clear();
m_Words.clear();
m_RawString.clear();
// Current Text Chunk
CGUIString::TextChunk CurrentTextChunk;
CurrentTextChunk.m_From = 0;
int l = str.length();
int rawpos = 0;
CStrW tag;
std::vector tags;
bool closing = false;
for (int p = 0; p < l; ++p)
{
TextChunk::Tag tag_;
switch (str[p])
{
case L'[':
CurrentTextChunk.m_To = rawpos;
// Add the current chunks if it is not empty
if (CurrentTextChunk.m_From != rawpos)
m_TextChunks.push_back(CurrentTextChunk);
CurrentTextChunk.m_From = rawpos;
closing = false;
if (++p == l)
{
LOGERROR("Partial tag at end of string '%s'", utf8_from_wstring(str));
break;
}
if (str[p] == L'/')
{
closing = true;
if (tags.empty())
{
LOGERROR("Encountered closing tag without having any open tags. At %d in '%s'", p, utf8_from_wstring(str));
break;
}
if (++p == l)
{
LOGERROR("Partial closing tag at end of string '%s'", utf8_from_wstring(str));
break;
}
}
tag.clear();
// Parse tag
for (; p < l && str[p] != L']'; ++p)
{
CStrW name, param;
switch (str[p])
{
case L' ':
if (closing) // We still parse them to make error handling cleaner
LOGERROR("Closing tags do not support parameters (at pos %d '%s')", p, utf8_from_wstring(str));
// parse something="something else"
for (++p; p < l && str[p] != L'='; ++p)
name.push_back(str[p]);
if (p == l)
{
LOGERROR("Parameter without value at pos %d '%s'", p, utf8_from_wstring(str));
break;
}
FALLTHROUGH;
case L'=':
// parse a quoted parameter
if (closing) // We still parse them to make error handling cleaner
LOGERROR("Closing tags do not support parameters (at pos %d '%s')", p, utf8_from_wstring(str));
if (++p == l)
{
LOGERROR("Expected parameter, got end of string '%s'", utf8_from_wstring(str));
break;
}
if (str[p] != L'"')
{
LOGERROR("Unquoted parameters are not supported (at pos %d '%s')", p, utf8_from_wstring(str));
break;
}
for (++p; p < l && str[p] != L'"'; ++p)
{
switch (str[p])
{
case L'\\':
if (++p == l)
{
LOGERROR("Escape character at end of string '%s'", utf8_from_wstring(str));
break;
}
// NOTE: We do not support \n in tag parameters
FALLTHROUGH;
default:
param.push_back(str[p]);
}
}
if (!name.empty())
{
TextChunk::Tag::TagAttribute a = {name, param};
tag_.m_TagAttributes.push_back(a);
}
else
tag_.m_TagValue = param;
break;
default:
tag.push_back(str[p]);
break;
}
}
if (!tag_.SetTagType(tag))
{
LOGERROR("Invalid tag '%s' at %d in '%s'", utf8_from_wstring(tag), p, utf8_from_wstring(str));
break;
}
if (!closing)
{
if (tag_.m_TagType == TextChunk::Tag::TAG_IMGRIGHT
|| tag_.m_TagType == TextChunk::Tag::TAG_IMGLEFT
|| tag_.m_TagType == TextChunk::Tag::TAG_ICON)
{
TextChunk FreshTextChunk = { rawpos, rawpos };
FreshTextChunk.m_Tags.push_back(tag_);
m_TextChunks.push_back(FreshTextChunk);
}
else
{
tags.push_back(tag);
CurrentTextChunk.m_Tags.push_back(tag_);
}
}
else
{
if (tag != tags.back())
{
LOGERROR("Closing tag '%s' does not match last opened tag '%s' at %d in '%s'", utf8_from_wstring(tag), utf8_from_wstring(tags.back()), p, utf8_from_wstring(str));
break;
}
tags.pop_back();
CurrentTextChunk.m_Tags.pop_back();
}
break;
case L'\\':
if (++p == l)
{
LOGERROR("Escape character at end of string '%s'", utf8_from_wstring(str));
break;
}
if (str[p] == L'n')
{
++rawpos;
m_RawString.push_back(L'\n');
break;
}
FALLTHROUGH;
default:
++rawpos;
m_RawString.push_back(str[p]);
break;
}
}
// Add the chunk after the last tag
if (CurrentTextChunk.m_From != rawpos)
{
CurrentTextChunk.m_To = rawpos;
m_TextChunks.push_back(CurrentTextChunk);
}
// Add a delimiter at start and at end, it helps when
// processing later, because we don't have make exceptions for
// those cases.
m_Words.push_back(0);
// Add word boundaries in increasing order
for (u32 i = 0; i < m_RawString.length(); ++i)
{
wchar_t c = m_RawString[i];
if (c == '\n')
{
m_Words.push_back((int)i);
m_Words.push_back((int)i+1);
continue;
}
for (int n = 0; n < NUM_WORD_DELIMITERS; n += 2)
{
if (c <= WordDelimiters[n+1])
{
if (c >= WordDelimiters[n])
m_Words.push_back((int)i+1);
// assume the WordDelimiters list is stored in increasing order
break;
}
}
}
m_Words.push_back((int)m_RawString.length());
// Remove duplicates (only if larger than 2)
if (m_Words.size() <= 2)
return;
m_Words.erase(std::unique(m_Words.begin(), m_Words.end()), m_Words.end());
}
Index: ps/trunk/source/gui/SettingTypes/CGUIString.h
===================================================================
--- ps/trunk/source/gui/SettingTypes/CGUIString.h (revision 26914)
+++ ps/trunk/source/gui/SettingTypes/CGUIString.h (revision 26915)
@@ -1,220 +1,225 @@
-/* Copyright (C) 2021 Wildfire Games.
+/* Copyright (C) 2022 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#ifndef INCLUDED_CGUISTRING
#define INCLUDED_CGUISTRING
#include "gui/CGUIText.h"
#include "ps/CStrIntern.h"
#include
#include
#include
class CGUI;
/**
* String class, substitute for CStr, but that parses
* the tags and builds up a list of all text that will
* be different when outputted.
*
* The difference between CGUIString and CGUIText is that
* CGUIString is a string-class that parses the tags
* when the value is set. The CGUIText is just a container
* which stores the positions and settings of all text-calls
* that will have to be made to the Renderer.
*/
class CGUIString
{
public:
/**
* A chunk of text that represents one call to the renderer.
* In other words, all text in one chunk, will be drawn
* exactly with the same settings.
*/
struct TextChunk
{
/**
* A tag looks like this "Hello [b]there[/b] little"
*/
struct Tag
{
/**
* Tag Type
*/
enum TagType
{
TAG_B,
TAG_I,
TAG_FONT,
TAG_SIZE,
TAG_COLOR,
TAG_IMGLEFT,
TAG_IMGRIGHT,
TAG_ICON,
TAG_TOOLTIP,
TAG_INVALID
};
struct TagAttribute
{
std::wstring attrib;
std::wstring value;
};
/**
* Set tag from string
*
* @param tagtype TagType by string, like 'img' for [img]
* @return True if m_TagType was set.
*/
bool SetTagType(const CStrW& tagtype);
TagType GetTagType(const CStrW& tagtype) const;
/**
* In [b="Hello"][/b]
* m_TagType is TAG_B
*/
TagType m_TagType;
/**
* In [b="Hello"][/b]
* m_TagValue is 'Hello'
*/
std::wstring m_TagValue;
/**
* Some tags need an additional attributes
*/
std::vector m_TagAttributes;
};
/**
* m_From and m_To is the range of the string
*/
int m_From, m_To;
/**
* Tags that are present. [a][b]
*/
std::vector m_Tags;
};
/**
* All data generated in GenerateTextCall()
*/
struct SFeedback
{
// Avoid copying the vector and list containers.
NONCOPYABLE(SFeedback);
MOVABLE(SFeedback);
SFeedback() = default;
// Constants
static const int Left = 0;
static const int Right = 1;
/**
* Reset all member data.
*/
void Reset();
/**
* Image stacks, for left and right floating images.
*/
std::array, 2> m_Images; // left and right
/**
* Text and Sprite Calls.
*/
std::vector m_TextCalls;
// list for consistent mem addresses so that we can point to elements.
std::list m_SpriteCalls;
/**
* Width and Height *feedback*
*/
CSize2D m_Size;
/**
* If the word inputted was a new line.
*/
bool m_NewLine;
+
+ /**
+ * If the word inputted ends with a space that can be collapsed when aligning.
+ */
+ bool m_EndsWithSpace;
};
/**
* Set the value, the string will automatically
* be parsed when set.
*/
void SetValue(const CStrW& str);
/**
* Get String, with tags
*/
const CStrW& GetOriginalString() const { return m_OriginalString; }
/**
* Get String, stripped of tags
*/
const CStrW& GetRawString() const { return m_RawString; }
/**
* Generate Text Call from specified range. The range
* must span only within ONE TextChunk though. Otherwise
* it can't be fit into a single Text Call
*
* Notice it won't make it complete, you will have to add
* X/Y values and such.
*
* @param pGUI Pointer to CGUI object making this call, for e.g. icon retrieval.
* @param Feedback contains all info that is generated.
* @param DefaultFont Default Font
* @param from From character n,
* @param to to character n.
* @param FirstLine Whether this is the first line of text, to calculate its height correctly
* @param pObject Only for Error outputting, optional! If nullptr
* then no Errors will be reported! Useful when you need
* to make several GenerateTextCall in different phases,
* it avoids duplicates.
*/
void GenerateTextCall(const CGUI& pGUI, SFeedback& Feedback, CStrIntern DefaultFont, const int& from, const int& to, const bool FirstLine, const IGUIObject* pObject = nullptr) const;
/**
* Words
*/
std::vector m_Words;
private:
/**
* TextChunks
*/
std::vector m_TextChunks;
/**
* The full raw string. Stripped of tags.
*/
CStrW m_RawString;
/**
* The original string value passed to SetValue.
*/
CStrW m_OriginalString;
};
#endif // INCLUDED_CGUISTRING
Index: ps/trunk/source/gui/tests/test_CGUIText.h
===================================================================
--- ps/trunk/source/gui/tests/test_CGUIText.h (nonexistent)
+++ ps/trunk/source/gui/tests/test_CGUIText.h (revision 26915)
@@ -0,0 +1,254 @@
+/* Copyright (C) 2022 Wildfire Games.
+ * This file is part of 0 A.D.
+ *
+ * 0 A.D. is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 0 A.D. is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with 0 A.D. If not, see .
+ */
+
+#include "lib/self_test.h"
+
+#include "graphics/FontManager.h"
+#include "gui/CGUI.h"
+#include "gui/CGUIText.h"
+#include "gui/SettingTypes/CGUIString.h"
+#include "ps/CLogger.h"
+#include "ps/ConfigDB.h"
+#include "ps/ProfileViewer.h"
+#include "ps/VideoMode.h"
+#include "renderer/Renderer.h"
+
+class TestCGUIText : public CxxTest::TestSuite
+{
+ CProfileViewer* m_Viewer = nullptr;
+ CRenderer* m_Renderer = nullptr;
+public:
+ void setUp()
+ {
+ g_VFS = CreateVfs();
+ TS_ASSERT_OK(g_VFS->Mount(L"", DataDir() / "mods" / "_test.minimal" / "", VFS_MOUNT_MUST_EXIST));
+ TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir() / "_testcache" / "", 0, VFS_MAX_PRIORITY));
+
+ CXeromyces::Startup();
+
+ // The renderer spews messages.
+ TestLogger logger;
+
+ // We need to initialise the renderer to initialise the font manager.
+ // TODO: decouple this.
+ CConfigDB::Initialise();
+ CConfigDB::Instance()->SetValueString(CFG_SYSTEM, "rendererbackend", "dummy");
+ g_VideoMode.InitNonSDL();
+ g_VideoMode.CreateBackendDevice(false);
+ m_Viewer = new CProfileViewer;
+ m_Renderer = new CRenderer;
+ }
+
+ void tearDown()
+ {
+ delete m_Renderer;
+ delete m_Viewer;
+ g_VideoMode.Shutdown();
+ CConfigDB::Shutdown();
+ CXeromyces::Terminate();
+ g_VFS.reset();
+ DeleteDirectory(DataDir()/"_testcache");
+ }
+
+ void test_empty()
+ {
+ CGUI gui(g_ScriptContext);
+ CGUIText empty;
+ }
+
+ void test_wrapping()
+ {
+ CGUI gui(g_ScriptContext);
+
+ static CStrW font = L"console";
+ // Make sure this matches the value of the file.
+ // TODO: load dynamically.
+ static const float lineHeight = 12.f;
+ static const float lineSpacing = 15.f;
+
+ CGUIString string;
+ CGUIText text;
+ float width = 0.f;
+ float renderedWidth = 0.f;
+ float padding = 0.f;
+ EAlign align = EAlign::LEFT;
+
+ // Thing to note: the space before the newline should collapse in right-alignment.
+ string.SetValue(L"Some long text that will wrap-around. \n New line.");
+ text = CGUIText(gui, string, font, width, padding, align, nullptr);
+
+ // Width 0 means no wrapping, so we should be getting one render call & one line.
+ // TODO: is it wanted that \n doesn't wrap in that case?
+ // We have 11 calls: the 9 words (wrap-around is split in two), the space after the newline, and the newline itself.
+ TS_ASSERT_EQUALS(text.GetTextCalls().size(), 11);
+ TS_ASSERT_EQUALS(text.GetSize().Height, lineHeight);
+
+ width = 100.f;
+ padding = 2.0f;
+ align = EAlign::LEFT;
+ text = CGUIText(gui, string, font, width, padding, align, nullptr);
+ renderedWidth = text.GetSize().Width;
+ // We have 10 calls: the 9 words (wrap-around is split in two), the space after the newline.
+ TS_ASSERT_EQUALS(text.GetTextCalls().size(), 10);
+ TS_ASSERT_LESS_THAN(text.GetSize().Width, width);
+ TS_ASSERT_EQUALS(text.GetSize().Height, padding * 2 + lineHeight + lineSpacing * 4);
+
+ align = EAlign::RIGHT;
+ text = CGUIText(gui, string, font, width, padding, align, nullptr);
+ TS_ASSERT_EQUALS(text.GetTextCalls().size(), 10);
+ TS_ASSERT_EQUALS(text.GetSize().Width, renderedWidth); // Should be the same width as the left-case.
+ TS_ASSERT_EQUALS(text.GetSize().Height, padding * 2 + lineHeight + lineSpacing * 4);
+
+ width = 400.f;
+ padding = 3.0f;
+ align = EAlign::LEFT;
+ text = CGUIText(gui, string, font, width, padding, align, nullptr);
+ TS_ASSERT_EQUALS(text.GetTextCalls().size(), 10);
+ TS_ASSERT_LESS_THAN(text.GetSize().Width, width);
+ TS_ASSERT_EQUALS(text.GetSize().Height, padding * 2 + lineHeight + lineSpacing);
+
+ width = 400.f;
+ padding = 5.0f;
+ align = EAlign::CENTER;
+ text = CGUIText(gui, string, font, width, padding, align, nullptr);
+ renderedWidth = text.GetSize().Width;
+ TS_ASSERT_LESS_THAN(text.GetSize().Width, width);
+ TS_ASSERT_EQUALS(text.GetSize().Height, padding * 2 + lineHeight + lineSpacing);
+
+ align = EAlign::RIGHT;
+ text = CGUIText(gui, string, font, width, padding, align, nullptr);
+ TS_ASSERT_EQUALS(text.GetSize().Width, renderedWidth); // Should be the same width as the center-case.
+ TS_ASSERT_EQUALS(text.GetSize().Height, padding * 2 + lineHeight + lineSpacing);
+
+ align = EAlign::LEFT;
+ text = CGUIText(gui, string, font, width, padding, align, nullptr);
+ TS_ASSERT_EQUALS(text.GetSize().Width, renderedWidth); // Should be the same width as the center-case.
+ TS_ASSERT_EQUALS(text.GetSize().Height, padding * 2 + lineHeight + lineSpacing);
+
+ width = 400.f;
+ padding = 100.0f;
+ align = EAlign::LEFT;
+ text = CGUIText(gui, string, font, width, padding, align, nullptr);
+ renderedWidth = text.GetSize().Width;
+ TS_ASSERT_LESS_THAN(text.GetSize().Width, width);
+ TS_ASSERT_EQUALS(text.GetSize().Height, padding * 2 + lineHeight + lineSpacing * 2);
+
+ align = EAlign::RIGHT;
+ text = CGUIText(gui, string, font, width, padding, align, nullptr);
+ TS_ASSERT_EQUALS(text.GetSize().Width, renderedWidth); // Should be the same width as the left-case.
+ TS_ASSERT_EQUALS(text.GetSize().Height, padding * 2 + lineHeight + lineSpacing * 2);
+ }
+
+ void test_overflow()
+ {
+ CGUI gui(g_ScriptContext);
+
+ static CStrW font = L"console";
+ // Make sure this matches the value of the file.
+ // TODO: load dynamically.
+ static const float lineHeight = 12.f;
+ static const float lineSpacing = 15.f;
+
+ float renderedWidth = 0.f;
+ const float width = 200.f;
+ const float padding = 20.f;
+
+ CGUIString string;
+ CGUIText text;
+ string.SetValue(L"wordthatisverylonganddefinitelywontfitinaline and other words");
+ text = CGUIText(gui, string, font, width, padding, EAlign::LEFT, nullptr);
+ renderedWidth = text.GetSize().Width;
+ TS_ASSERT_EQUALS(text.GetSize().Height, lineHeight + lineSpacing * 1 + padding * 2);
+ text = CGUIText(gui, string, font, width, padding, EAlign::CENTER, nullptr);
+ TS_ASSERT_EQUALS(renderedWidth, text.GetSize().Width);
+ TS_ASSERT_EQUALS(text.GetSize().Height, lineHeight + lineSpacing * 1 + padding * 2);
+ text = CGUIText(gui, string, font, width, padding, EAlign::RIGHT, nullptr);
+ TS_ASSERT_EQUALS(renderedWidth, text.GetSize().Width);
+ TS_ASSERT_EQUALS(text.GetSize().Height, lineHeight + lineSpacing * 1 + padding * 2);
+
+ string.SetValue(L"other words and wordthatisverylonganddefinitelywontfitinaline");
+ text = CGUIText(gui, string, font, width, padding, EAlign::LEFT, nullptr);
+ renderedWidth = text.GetSize().Width;
+ TS_ASSERT_EQUALS(text.GetSize().Height, lineHeight + lineSpacing * 1 + padding * 2);
+ text = CGUIText(gui, string, font, width, padding, EAlign::CENTER, nullptr);
+ TS_ASSERT_EQUALS(renderedWidth, text.GetSize().Width);
+ TS_ASSERT_EQUALS(text.GetSize().Height, lineHeight + lineSpacing * 1 + padding * 2);
+ text = CGUIText(gui, string, font, width, padding, EAlign::RIGHT, nullptr);
+ TS_ASSERT_EQUALS(renderedWidth, text.GetSize().Width);
+ TS_ASSERT_EQUALS(text.GetSize().Height, lineHeight + lineSpacing * 1 + padding * 2);
+
+ string.SetValue(L"wordthatisverylonganddefinitelywontfitinaline");
+ text = CGUIText(gui, string, font, width, padding, EAlign::LEFT, nullptr);
+ renderedWidth = text.GetSize().Width;
+ TS_ASSERT_EQUALS(text.GetSize().Height, lineHeight + padding * 2);
+ text = CGUIText(gui, string, font, width, padding, EAlign::CENTER, nullptr);
+ TS_ASSERT_EQUALS(renderedWidth, text.GetSize().Width);
+ TS_ASSERT_EQUALS(text.GetSize().Height, lineHeight + padding * 2);
+ text = CGUIText(gui, string, font, width, padding, EAlign::RIGHT, nullptr);
+ TS_ASSERT_EQUALS(renderedWidth, text.GetSize().Width);
+ TS_ASSERT_EQUALS(text.GetSize().Height, lineHeight + padding * 2);
+ }
+
+ void test_regression_rP26522()
+ {
+ TS_ASSERT_OK(g_VFS->Mount(L"", DataDir() / "mods" / "mod" / "", VFS_MOUNT_MUST_EXIST));
+
+ CGUI gui(g_ScriptContext);
+
+ static CStrW font = L"sans-bold-13";
+ CGUIString string;
+ CGUIText text;
+
+ // rP26522 introduced a bug that triggered in rare cases with word-wrapping.
+ string.SetValue(L"90–120 min");
+ text = CGUIText(gui, string, L"sans-bold-13", 53, 8.f, EAlign::LEFT, nullptr);
+
+ TS_ASSERT_EQUALS(text.GetTextCalls().size(), 2);
+ TS_ASSERT_EQUALS(text.GetSize().Height, 14 + 9 + 8 * 2);
+ }
+
+ void test_multiple_blank_spaces()
+ {
+ CGUI gui(g_ScriptContext);
+
+ static CStrW font = L"console";
+ // Make sure this matches the value of the file.
+ // TODO: load dynamically.
+ static const float lineHeight = 12.f;
+ static const float lineSpacing = 15.f;
+
+ CGUIString string;
+ CGUIText text;
+ float width = 100.f;
+ float renderedWidth = 0.f;
+ float padding = 0.f;
+ EAlign align = EAlign::LEFT;
+
+ string.SetValue(L" word another \n spaces \n \n word ");
+ text = CGUIText(gui, string, font, width, padding, align, nullptr);
+
+ // Blank spaces are treated as a word.
+ TS_ASSERT_EQUALS(text.GetTextCalls().size(), 26);
+ TS_ASSERT_EQUALS(text.GetSize().Height, lineHeight + lineSpacing * 4);
+ TS_ASSERT_EQUALS(text.GetSize().Width, 89.f);
+ renderedWidth = text.GetSize().Width;
+
+ align = EAlign::RIGHT;
+ text = CGUIText(gui, string, font, width, padding, align, nullptr);
+ TS_ASSERT_EQUALS(text.GetSize().Width, renderedWidth);
+ }
+};
Property changes on: ps/trunk/source/gui/tests/test_CGUIText.h
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property