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