├── .gitattributes ├── .gitignore ├── .vscode └── tasks.json ├── README.md ├── pawn.json ├── td-string-width.inc └── test.pwn /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pwn linguist-language=Pawn 2 | *.inc linguist-language=Pawn 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # 2 | # Package only files 3 | # 4 | 5 | # Compiled Bytecode, precompiled output and assembly 6 | *.amx 7 | *.lst 8 | *.asm 9 | 10 | # Vendor directory for dependencies 11 | dependencies/ 12 | 13 | # Dependency versions lockfile 14 | pawn.lock 15 | 16 | 17 | # 18 | # Server/gamemode related files 19 | # 20 | 21 | # compiled settings file 22 | # keep `samp.json` file on version control 23 | # but make sure the `rcon_password` field is set externally 24 | # you can use the environment variable `SAMP_RCON_PASSWORD` to do this. 25 | server.cfg 26 | 27 | # Plugins directory 28 | plugins/ 29 | 30 | # binaries 31 | *.exe 32 | *.dll 33 | *.so 34 | announce 35 | samp03svr 36 | samp-npc 37 | 38 | # logs 39 | logs/ 40 | server_log.txt 41 | crashinfo.txt 42 | 43 | # Ban list 44 | samp.ban 45 | 46 | # 47 | # Common files 48 | # 49 | 50 | *.sublime-workspace 51 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build only", 6 | "type": "shell", 7 | "command": "sampctl package build", 8 | "group": { 9 | "kind": "build", 10 | "isDefault": true 11 | }, 12 | "isBackground": false, 13 | "presentation": { 14 | "reveal": "silent", 15 | "panel": "dedicated" 16 | }, 17 | "problemMatcher": "$sampctl" 18 | }, 19 | { 20 | "label": "build watcher", 21 | "type": "shell", 22 | "command": "sampctl package build --watch", 23 | "group": "build", 24 | "isBackground": true, 25 | "presentation": { 26 | "reveal": "silent", 27 | "panel": "dedicated" 28 | }, 29 | "problemMatcher": "$sampctl" 30 | }, 31 | { 32 | "label": "run tests", 33 | "type": "shell", 34 | "command": "sampctl package run", 35 | "group": { 36 | "kind": "test", 37 | "isDefault": true 38 | }, 39 | "isBackground": true, 40 | "presentation": { 41 | "reveal": "silent", 42 | "panel": "dedicated" 43 | }, 44 | "problemMatcher": "$sampctl" 45 | }, 46 | { 47 | "label": "run tests watcher", 48 | "type": "shell", 49 | "command": "sampctl package run --watch", 50 | "group": "test", 51 | "isBackground": true, 52 | "presentation": { 53 | "reveal": "silent", 54 | "panel": "dedicated" 55 | }, 56 | "problemMatcher": "$sampctl" 57 | } 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SA-MP TextDraw String Width 2 | 3 | [![sampctl](https://img.shields.io/badge/sampctl-samp--td--string--width-2f2f2f.svg?style=for-the-badge)](https://github.com/kristoisberg/samp-td-string-width) 4 | 5 | **Notice:** This repository is not being actively maintained anymore. If anyone wishes to continue the development of the project, please create a fork of the repository and release future versions there. 6 | 7 | About two years ago an user called Alcatrik came up with a concept of calculating the width of textdraw strings in [this thread](https://forum.sa-mp.com/showthread.php?t=618883). In those two years a few functions using this concept appeared in that thread, but all of them had major flaws. The first one by Crayder didn't include the first 32 characters of the ASCII table. The next one by Freaksten fixed that flaw, but neither of the functions also didn't take into account that there are four different textdraw fonts and just used the first array in `fonts.dat`. 8 | 9 | About two or three months ago I needed a correct version of that function so I started digging into the data in `fonts.dat`. The first discovery I made is that fonts 2 and 3 are actually subfonts of fonts 0 and 1 (fun fact: you can actually access the characters of fonts 2 and 3 from fonts 0 and 1 by using values larger than 175 as characters). I manually reassembled the data into four arrays, but soon after that I temporarily abandoned the project I collected this data for. 10 | 11 | A few days ago I mentioned the data I had collected and Y_Less sent me a [repository](https://github.com/On3d4y/TextDrawColour.inc) that contained a function that generated the same dataset automatically. I decided to compare the two datasets and found out that they had different widths for 16 characters, which I re-measured manually. Both got about half of the 16 widths right and half of them wrong. 12 | 13 | Another mistake that all existing functions did was presuming that characters 1-31 are always rendered as regular whitespace. In reality, the widths of the invisible characters differed in font 2 and the characters in font 3 actually have two different widths. One of them is the visual width: most of the characters don't actually render as whitespace, instead of this you see either one or two stripes with varying widths. The other width is the "physical" width, which is either 0 (the stripes overlap with the next character or two) or a relatively large number, up to 255. 14 | 15 | Now, after multiple days of work that contained manually reassembling data, manually measuring almost 100 characters and a lot more, this library is ready. What could it be used for? I personally needed this to calculate the width of buttons based on their strings. The previously mentioned repository uses code similar to this to achieve full colouring of textdraws. A few more usages can be found from the original topic explaining the concept. 16 | 17 | ## Installation 18 | 19 | Simply install to your project: 20 | 21 | ```bash 22 | sampctl package install kristoisberg/samp-td-string-width 23 | ``` 24 | 25 | Include in your code and begin using the library: 26 | 27 | ```pawn 28 | #include 29 | ``` 30 | 31 | ## Usage 32 | 33 | - `GetTextDrawCharacterWidth(character, font, bool:proportional = true)` 34 | - Returns the width of a single character. 35 | - `GetTextDrawStringWidth(const string[], font, outline = 0, bool:proportional = true)` 36 | - Finds the longest line in the string and returns the width of it. Skips everything between `~` characters except `~n~`, which indicates the start of a new line. Odd number of `~` characters returns the width of "Error: unmatched tilde". 37 | - `GetTextDrawLineWidth(const string[], font, outline = 0, bool:proportional = true)` 38 | - Works just like `GetTextDrawStringWidth`, but presumes that the string only contains one line. Skips `~n~` just like everything else between `~` characters. 39 | 40 | ## Testing 41 | 42 | To test, simply run the package: 43 | 44 | ```bash 45 | sampctl package run 46 | ``` 47 | -------------------------------------------------------------------------------- /pawn.json: -------------------------------------------------------------------------------- 1 | { 2 | "user": "kristoisberg", 3 | "repo": "samp-td-string-width", 4 | "entry": "test.pwn", 5 | "output": "test.amx", 6 | "dependencies": [ 7 | "sampctl/samp-stdlib" 8 | ], 9 | "dev_dependencies": [ 10 | "Zeex/samp-plugin-crashdetect", 11 | "IllidanS4/PawnPlus" 12 | ] 13 | } -------------------------------------------------------------------------------- /td-string-width.inc: -------------------------------------------------------------------------------- 1 | #include 2 | #tryinclude 3 | 4 | 5 | static const TDCharacterDefaultWidth[4] = {27, 20, 27, 20}; 6 | 7 | static const TDCharacterWidth[4][176] = { 8 | { 9 | 0, 12, 12, 12, 12, 12, 12, 12, 10 | 12, 12, 12, 12, 12, 12, 12, 12, 11 | 12, 12, 12, 12, 12, 12, 12, 12, 12 | 12, 12, 12, 12, 12, 12, 12, 12, 13 | 12, 13, 13, 28, 28, 28, 28, 8, 14 | 17, 17, 30, 28, 28, 12, 9, 21, 15 | 28, 14, 28, 28, 28, 28, 28, 28, 16 | 28, 28, 13, 13, 30, 30, 30, 30, 17 | 10, 25, 23, 21, 24, 22, 20, 24, 18 | 24, 17, 20, 22, 20, 30, 27, 27, 19 | 26, 26, 24, 23, 24, 31, 23, 31, 20 | 24, 23, 21, 28, 33, 33, 14, 28, 21 | 10, 11, 12, 9, 11, 10, 10, 12, 22 | 12, 7, 7, 13, 5, 18, 12, 10, 23 | 12, 11, 10, 12, 8, 13, 13, 18, 24 | 17, 13, 12, 30, 30, 37, 35, 37, 25 | 25, 25, 25, 25, 33, 21, 24, 24, 26 | 24, 24, 17, 17, 17, 17, 27, 27, 27 | 27, 27, 31, 31, 31, 31, 11, 11, 28 | 11, 11, 11, 20, 9, 10, 10, 10, 29 | 10, 7, 7, 7, 7, 10, 10, 10, 30 | 10, 13, 13, 13, 13, 27, 12, 30 31 | }, { 32 | 0, 15, 15, 15, 15, 15, 15, 15, 33 | 15, 15, 15, 15, 15, 15, 15, 15, 34 | 15, 15, 15, 15, 15, 15, 15, 15, 35 | 15, 15, 15, 15, 15, 15, 15, 15, 36 | 15, 9, 17, 27, 20, 34, 23, 12, 37 | 12, 12, 21, 20, 12, 14, 12, 15, 38 | 23, 15, 21, 21, 21, 21, 21, 21, 39 | 20, 21, 12, 12, 24, 24, 24, 19, 40 | 10, 22, 19, 19, 22, 16, 19, 24, 41 | 22, 11, 16, 21, 15, 28, 24, 27, 42 | 20, 25, 19, 19, 18, 23, 23, 31, 43 | 23, 19, 21, 21, 13, 35, 11, 21, 44 | 10, 19, 20, 14, 20, 19, 13, 20, 45 | 19, 9, 9, 19, 9, 29, 19, 21, 46 | 19, 19, 15, 15, 14, 18, 19, 27, 47 | 20, 20, 17, 21, 17, 20, 15, 15, 48 | 22, 22, 22, 22, 29, 19, 16, 16, 49 | 16, 16, 11, 11, 11, 11, 27, 27, 50 | 27, 27, 23, 23, 23, 23, 20, 19, 51 | 19, 19, 19, 30, 14, 19, 19, 19, 52 | 19, 9, 9, 9, 9, 21, 21, 21, 53 | 21, 18, 18, 18, 18, 24, 19, 19 54 | }, { 55 | 0, 15, 23, 15, 21, 21, 21, 21, 56 | 21, 21, 20, 21, 12, 12, 24, 24, 57 | 24, 19, 10, 22, 19, 19, 22, 16, 58 | 19, 24, 22, 11, 16, 21, 15, 28, 59 | 12, 13, 13, 28, 37, 28, 30, 8, 60 | 17, 17, 30, 28, 28, 12, 9, 21, 61 | 27, 16, 27, 27, 27, 27, 27, 27, 62 | 27, 27, 18, 13, 30, 30, 30, 30, 63 | 10, 29, 26, 25, 28, 26, 25, 27, 64 | 28, 12, 24, 25, 24, 30, 27, 29, 65 | 26, 26, 25, 26, 25, 26, 28, 32, 66 | 27, 26, 26, 28, 33, 33, 10, 28, 67 | 10, 29, 26, 25, 28, 26, 25, 27, 68 | 28, 12, 24, 25, 24, 30, 27, 29, 69 | 26, 26, 25, 26, 25, 26, 28, 32, 70 | 27, 26, 26, 30, 30, 37, 35, 37, 71 | 29, 29, 29, 29, 33, 25, 26, 26, 72 | 26, 26, 14, 14, 14, 14, 29, 29, 73 | 29, 29, 26, 26, 26, 26, 21, 29, 74 | 29, 29, 29, 33, 25, 26, 26, 26, 75 | 26, 14, 14, 14, 14, 29, 29, 29, 76 | 29, 26, 26, 26, 26, 25, 25, 30 77 | }, { 78 | 0, 9, 9, 18, 18, 18, 18, 18, 79 | 18, 18, 18, 19, 19, 19, 0, 9, 80 | 9, 9, 9, 18, 18, 18, 18, 18, 81 | 18, 18, 18, 19, 19, 19, 0, 9, 82 | 15, 10, 17, 27, 20, 34, 23, 10, 83 | 15, 15, 21, 20, 12, 14, 9, 15, 84 | 20, 18, 19, 19, 21, 19, 19, 19, 85 | 19, 19, 16, 12, 24, 24, 24, 21, 86 | 10, 19, 19, 19, 20, 19, 16, 19, 87 | 19, 9, 19, 20, 14, 29, 19, 19, 88 | 19, 19, 19, 19, 21, 19, 20, 32, 89 | 21, 19, 19, 21, 13, 35, 10, 21, 90 | 10, 19, 19, 19, 20, 19, 16, 19, 91 | 19, 9, 19, 20, 14, 29, 19, 19, 92 | 19, 19, 19, 19, 21, 19, 20, 32, 93 | 21, 19, 19, 21, 17, 20, 15, 15, 94 | 19, 19, 19, 19, 29, 19, 19, 19, 95 | 19, 19, 9, 9, 9, 9, 19, 19, 96 | 19, 19, 19, 19, 19, 19, 19, 19, 97 | 19, 19, 19, 29, 19, 19, 19, 19, 98 | 19, 9, 9, 9, 9, 19, 19, 19, 99 | 19, 19, 19, 19, 19, 21, 21, 19 100 | } 101 | }; 102 | 103 | static const TDFont3CharacterInlineWidth[32] = { 104 | 0, 255, 0, 0, 128, 63, 147, 36, 105 | 19, 64, 0, 0, 0, 0, 0, 0, 106 | 32, 68, 0, 0, 0, 0, 0, 0, 107 | 0, 0, 0, 0, 0, 0, 0, 0 108 | }; 109 | 110 | 111 | stock GetTextDrawCharacterWidth(character, font, bool:proportional = true) { 112 | if (!(0 <= font <= 3) || !(0 <= character < 176)) { 113 | return 0; 114 | } 115 | 116 | new width; 117 | 118 | if (!proportional || character >= sizeof(TDCharacterWidth[])) { 119 | width = TDCharacterDefaultWidth[font]; 120 | } else { 121 | width = TDCharacterWidth[font][character]; 122 | } 123 | 124 | return width; 125 | } 126 | 127 | 128 | stock GetTextDrawStringWidth(const string[], font, outline = 0, bool:proportional = true) { 129 | new other, result, width; 130 | 131 | for (new i, length = strlen(string); i < length; i++) { 132 | if (string[i] == '~') { 133 | if ((other = strfind(string, "~", .pos = i + 1)) == -1) { 134 | return GetTextDrawLineWidth("Error: unmatched tilde", font, outline, proportional); 135 | } 136 | 137 | if (other == i + 2 && string[i + 1] == 'n') { 138 | if (result < width) { 139 | result = width; 140 | } 141 | 142 | width = 0; 143 | } 144 | 145 | i = other + 1; 146 | } else { 147 | if (font == 3 && (0 < string[i] < 32) && i != length - 1 && strfind(string, "~n~", .pos = i + 1) != i + 1) { 148 | width += TDFont3CharacterInlineWidth[string[i]]; 149 | } else { 150 | width += GetTextDrawCharacterWidth(string[i], font, proportional); 151 | } 152 | } 153 | } 154 | 155 | if (result < width) { 156 | result = width; 157 | } 158 | 159 | return result + (outline * 2); 160 | } 161 | 162 | 163 | stock GetTextDrawLineWidth(const string[], font, outline = 0, bool:proportional = true, start = 0, end = -1) { 164 | new other, width; 165 | 166 | if (end == -1) { 167 | end = strlen(string); 168 | } 169 | 170 | for (; start < end; start++) { 171 | if (string[start] == '~') { 172 | if ((other = strfind(string, "~", .pos = start + 1)) == -1) { 173 | return GetTextDrawLineWidth("Error: unmatched tilde", font, outline, proportional); 174 | } 175 | 176 | start = other + 1; 177 | } else { 178 | if (font == 3 && (0 < string[start] < 32) && start != end - 1) { 179 | width += TDFont3CharacterInlineWidth[string[start]]; 180 | } else { 181 | width += GetTextDrawCharacterWidth(string[start], font, proportional); 182 | } 183 | } 184 | } 185 | 186 | return width + (outline * 2); 187 | } 188 | 189 | 190 | stock GetTextDrawLineCount(const string[]) { 191 | new count = 1, pos = -3; 192 | 193 | while ((pos = strfind(string, "~n~", true, pos + 3)) != -1) { 194 | count++; 195 | } 196 | 197 | return count; 198 | } 199 | 200 | 201 | static stock _SplitTryToReplace(string[], Float:max_width, Float:letter_size, font, outline, bool:proportional, line_start, previous_space, size, pos, length) { 202 | if (letter_size * float(GetTextDrawLineWidth(string, font, outline, proportional, line_start, pos)) <= max_width) { 203 | return 0; 204 | } 205 | 206 | if (previous_space != -1) { 207 | if (length + 2 < size) { 208 | strdel(string, previous_space, previous_space + 1); 209 | strins(string, "~n~", previous_space, size); 210 | return 1; 211 | } 212 | 213 | return -1; 214 | } 215 | 216 | // todo: splitting long words (with a separate size check of course), return value 2 217 | return 0; 218 | } 219 | 220 | 221 | stock bool:SplitTextDrawString(string[], Float:max_width, Float:letter_size, font, outline = 0, bool:proportional = true, size = sizeof(string)) { 222 | new other, line_start, previous_space = -1, length = strlen(string); 223 | 224 | for (new i; i < length; ) { 225 | switch (string[i]) { 226 | case '~': { 227 | if ((other = strfind(string, "~", .pos = i + 1)) == -1) { 228 | return false; 229 | } 230 | 231 | if (other == i + 2 && string[i + 1] == 'n') { 232 | switch (_SplitTryToReplace(string, max_width, letter_size, font, outline, proportional, line_start, previous_space, size, i, length)) { 233 | case -1: { 234 | return true; 235 | } 236 | 237 | case 1: { 238 | i += 5; 239 | length += 2; 240 | previous_space = -1; 241 | } 242 | 243 | case 0: { 244 | i += 3; 245 | } 246 | } 247 | 248 | line_start = i; 249 | } else { 250 | i = other + 1; 251 | } 252 | } 253 | 254 | case ' ': { 255 | switch (_SplitTryToReplace(string, max_width, letter_size, font, outline, proportional, line_start, previous_space, size, i, length)) { 256 | case -1: { 257 | return true; 258 | } 259 | 260 | case 1: { 261 | i += 3; 262 | length += 2; 263 | line_start = previous_space + 3; 264 | previous_space = i - 1; 265 | } 266 | 267 | case 0: { 268 | previous_space = i; 269 | i++; 270 | } 271 | } 272 | } 273 | 274 | default: { 275 | i++; 276 | } 277 | } 278 | } 279 | 280 | _SplitTryToReplace(string, max_width, letter_size, font, outline, proportional, line_start, previous_space, size, length, length); 281 | return true; 282 | } 283 | 284 | 285 | #if defined _PawnPlus_included 286 | stock GetTextDrawStringWidth_s(String:string, font, outline = 0, bool:proportional = true) { 287 | new ref[1][] = {{}}, size = str_len(string) + 1, Var:var = amx_alloc(size, false); 288 | 289 | amx_to_ref(var, ref); 290 | str_get(string, ref[0], size); 291 | 292 | new result = GetTextDrawStringWidth(ref[0], font, outline, proportional); 293 | 294 | amx_free(var); 295 | amx_delete(var); 296 | return result; 297 | } 298 | 299 | 300 | stock GetTextDrawLineWidth_s(String:string, font, outline = 0, bool:proportional = true, start = 0, end = -1) { 301 | new ref[1][] = {{}}, size = str_len(string) + 1, Var:var = amx_alloc(size, false); 302 | 303 | amx_to_ref(var, ref); 304 | str_get(string, ref[0], size); 305 | 306 | new result = GetTextDrawLineWidth(ref[0], font, outline, proportional, start, end); 307 | 308 | amx_free(var); 309 | amx_delete(var); 310 | return result; 311 | } 312 | 313 | 314 | stock GetTextDrawLineCount_s(String:string) { 315 | new ref[1][] = {{}}, size = str_len(string) + 1, Var:var = amx_alloc(size, false); 316 | 317 | amx_to_ref(var, ref); 318 | str_get(string, ref[0], size); 319 | 320 | new result = GetTextDrawLineCount(ref[0]); 321 | 322 | amx_free(var); 323 | amx_delete(var); 324 | return result; 325 | } 326 | 327 | 328 | stock bool:SplitTextDrawString_s(String:string, Float:max_width, Float:letter_size, font, outline = 0, bool:proportional = true) { 329 | new ref[1][] = {{}}, temp = (str_len(string) + 1) * 2, size = 1; 330 | 331 | while (size < temp) { 332 | size *= 2; 333 | } 334 | 335 | new Var:var = amx_alloc(size, false); 336 | 337 | amx_to_ref(var, ref); 338 | str_get(string, ref[0], size); 339 | 340 | new bool:result = SplitTextDrawString(ref[0], max_width, letter_size, font, outline, proportional, size); 341 | 342 | str_set_format(string, ref[0]); 343 | 344 | amx_free(var); 345 | amx_delete(var); 346 | return result; 347 | } 348 | #endif -------------------------------------------------------------------------------- /test.pwn: -------------------------------------------------------------------------------- 1 | #include "td-string-width.inc" 2 | 3 | 4 | main() { 5 | printf("%i", GetTextDrawCharacterWidth('!', 2)); 6 | 7 | printf("%i", GetTextDrawStringWidth("!!!", 2)); 8 | printf("%i", GetTextDrawStringWidth("!!!!!!", 2)); 9 | printf("%i", GetTextDrawStringWidth("!!!~n~!!!", 2)); 10 | 11 | printf("%i", GetTextDrawStringWidth("!!!~!!!", 2)); 12 | printf("%i", GetTextDrawStringWidth("Error: unmatched tilde", 2)); 13 | 14 | new temp[5]; 15 | 16 | temp[0] = 1; 17 | printf("%i %i", GetTextDrawStringWidth(temp, 3), GetTextDrawStringWidth_s(str_new(temp), 3)); // 9 18 | 19 | temp[1] = '!'; 20 | printf("%i %i", GetTextDrawStringWidth(temp, 3), GetTextDrawStringWidth_s(str_new(temp), 3)); // 255 + 10 = 265 21 | 22 | temp[1] = EOS; 23 | strcat(temp, "~n~"); 24 | printf("%i %i", GetTextDrawStringWidth(temp, 3), GetTextDrawStringWidth_s(str_new(temp), 3)); // 9 25 | 26 | printf("%i %i", GetTextDrawLineWidth("!!!", 2), GetTextDrawLineWidth_s(str_new_static("!!!"), 2)); 27 | printf("%i %i", GetTextDrawLineWidth("!!!!!!", 2)), GetTextDrawLineWidth_s(str_new_static("!!!!!!"), 2); 28 | 29 | printf("%i %i", GetTextDrawLineCount("aaaa"), GetTextDrawLineCount_s(str_new_static("aaaa"))); 30 | printf("%i %i", GetTextDrawLineCount("aaaa~n~aaaa"), GetTextDrawLineCount_s(str_new_static("aaaa~n~aaaa"))); 31 | printf("%i %i", GetTextDrawLineCount("aaaa~n~aaaa~n~aaaa"), GetTextDrawLineCount_s(str_new_static("aaaa~n~aaaa~n~aaaa"))); 32 | 33 | SplitTest("a a a a a a a a a a a a a a a"); 34 | SplitTest("asd asd asd asd asd asd asd"); 35 | SplitTest("wat is going on asd asd asd"); 36 | SplitTest("now watch me whip, now watch me nae nae"); 37 | SplitTest("now watch me whip,~n~now watch me nae nae"); 38 | SplitTest("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaa aaa"); 39 | return 1; 40 | } 41 | 42 | 43 | SplitTest(const string[]) { 44 | new buffer[128]; 45 | strcat(buffer, string); 46 | 47 | SplitTextDrawString(buffer, 100.0, 1.0, 0); 48 | printf("%s %i %i", buffer, GetTextDrawStringWidth(buffer, 0), GetTextDrawLineCount(buffer)); 49 | 50 | new String:string2 = str_new(string); 51 | SplitTextDrawString_s(string2, 100.0, 1.0, 0); 52 | str_get(string2, buffer); 53 | printf("%s %i %i", buffer, GetTextDrawStringWidth_s(string2, 0), GetTextDrawLineCount_s(string2)); 54 | } 55 | --------------------------------------------------------------------------------