1 /++ 2 Module for rendering text using the `FreeType` library. 3 4 Macros: 5 LREF = <a href="#$1">$1</a> 6 HREF = <a href="$1">$2</a> 7 PHOBREF = <a href="https://dlang.org/phobos/$1.html#$2">$2</a> 8 9 Authors: $(HREF https://github.com/TodNaz,TodNaz) 10 Copyright: Copyright (c) 2020 - 2021, TodNaz. 11 License: $(HREF https://github.com/TodNaz/Tida/blob/master/LICENSE,MIT) 12 +/ 13 module tida.text; 14 15 import bindbc.freetype; 16 import std.traits; 17 import tida.drawable; 18 19 __gshared FT_Library _FTLibrary; 20 21 /++ 22 Loads a library for loading and rendering fonts. 23 24 $(PHOBREF object,Exception) if the library was not found on the system. 25 +/ 26 void initFontLibrary() 27 { 28 import std.exception : enforce; 29 30 enforce!Exception(loadFreeType() != FTSupport.noLibrary, "Not find FreeType library!"); 31 enforce!Exception(!FT_Init_FreeType(&_FTLibrary), "Not a initialize FreeType Library!"); 32 } 33 34 /++ 35 The object to load and use the font. 36 +/ 37 class Font 38 { 39 import std.exception : enforce; 40 import std..string : toStringz; 41 import std.file : exists; 42 43 private: 44 FT_Face _face; 45 size_t _size; 46 47 public @trusted: 48 /// Font face object. 49 @property FT_Face face() 50 { 51 return _face; 52 } 53 54 /// Font size. 55 @property size_t size() 56 { 57 return _size; 58 } 59 60 /++ 61 Loads a font. 62 63 Params: 64 path = The path to the font. 65 size = Font size. 66 67 Throws: 68 $(PHOBREF object,Exception) if the font was not found in the file system, 69 or if the font is damaged. 70 +/ 71 Font load(string path, size_t size) 72 { 73 enforce!Exception(exists(path), "Not file file `"~path~"`"); 74 75 enforce!Exception(!FT_New_Face(_FTLibrary, path.toStringz, 0, &_face), "Font damaged!"); 76 77 FT_CharMap found; 78 79 foreach(i; 0 .. _face.num_charmaps) 80 { 81 FT_CharMap charmap = _face.charmaps[i]; 82 if ((charmap.platform_id == 3 && charmap.encoding_id == 1) 83 || (charmap.platform_id == 3 && charmap.encoding_id == 0) 84 || (charmap.platform_id == 2 && charmap.encoding_id == 1) 85 || (charmap.platform_id == 0)) { 86 found = charmap; 87 break; 88 } 89 } 90 91 FT_Set_Charmap(_face, found); 92 FT_Set_Char_Size(_face, 0, cast(int) size*32, 300, 300); 93 FT_Set_Pixel_Sizes(_face, 0, cast(int) size*2); 94 95 this._size = size; 96 97 return this; 98 } 99 100 /++ 101 Changes the font size to the specified parameter. 102 103 Params: 104 newSize = Font new size. 105 +/ 106 void resize(size_t newSize) @trusted 107 { 108 this._size = newSize; 109 110 FT_Set_Char_Size(_face, 0, cast(int) _size*32, 300, 300); 111 FT_Set_Pixel_Sizes(_face, 0, cast(int) size*2); 112 } 113 114 /// Free memory 115 void free() @trusted 116 { 117 FT_Done_Face(_face); 118 } 119 120 ~this() @safe 121 { 122 free(); 123 } 124 } 125 126 /++ 127 Determining the type of character depending on the type of the string. 128 +/ 129 template TypeChar(T) 130 { 131 static if (is(T : string)) 132 alias TypeChar = char; 133 else 134 static if (is(T : wstring)) 135 alias TypeChar = wchar; 136 else 137 static if (is(T : dstring)) 138 alias Typechar = dchar; 139 } 140 141 /// The unit for rendering the text. 142 class Symbol 143 { 144 import tida.image; 145 import tida.color; 146 import tida.vector; 147 148 public: 149 Image image; /// Symbol image. 150 Vecf position; /// Symbol releative position. 151 Color!ubyte color; /// Symbol color. 152 Vecf advance; /// Symbol releative position. 153 size_t size; /// Symbol size. 154 float offsetY = 0.0f; /// Offset symbol 155 156 @safe: 157 this( Image image, 158 Vecf position, 159 Vecf rel, 160 size_t size, 161 Color!ubyte color = rgb(255,255,255)) 162 { 163 this.image = image; 164 this.position = position; 165 this.color = color; 166 this.advance = rel; 167 this.size = size; 168 169 if (this.image !is null) 170 if (this.image.texture is null) 171 this.image.toTexture(); 172 } 173 } 174 175 /++ 176 Cuts off formatting blocks. 177 178 Params: 179 symbols = Symbols. 180 +/ 181 T cutFormat(T)(T symbols) @safe 182 { 183 static assert(isSomeString!T, T.stringof ~ " is not a string!"); 184 for(int i = 0; i < symbols.length; i++) 185 { 186 if(symbols[i] == '$') 187 { 188 if(symbols[i+1] == '<') { 189 __symEachCutter: for(int j = i; j < symbols.length; j++) 190 { 191 if(symbols[j] == '>') 192 { 193 symbols = symbols[0 .. i] ~ symbols[j + 1 .. $]; 194 break __symEachCutter; 195 } 196 } 197 } 198 } 199 } 200 201 return symbols; 202 } 203 204 unittest 205 { 206 assert("Hello, $<FF0000>World!".cutFormat == ("Hello, World!")); 207 } 208 209 /++ 210 Returns the size of the rendered text for the given font. 211 212 Params: 213 T = String type. 214 text = Text. 215 font = Font. 216 +/ 217 int widthText(T)(T text, Font font) @safe 218 { 219 import std.algorithm : reduce; 220 static assert(isSomeString!T, T.stringof ~ " is not a string!"); 221 222 text = cutFormat!T(text); 223 224 return new Text(font) 225 .toSymbols!T(text) 226 .widthSymbols; 227 } 228 229 /++ 230 Shows the width of the displayed characters. 231 232 Params: 233 text = Displayed characters. 234 +/ 235 int widthSymbols(Symbol[] text) @safe 236 { 237 import std.algorithm : reduce; 238 import std.conv : to; 239 240 int width = int.init; 241 242 foreach(s; text) 243 width += s.advance.x.to!int >> 6; 244 245 return width; 246 } 247 248 /++ 249 Draw text object. An array of letters and offset attributes already rendered 250 into the image is fed into the constructor, and the text is output 251 from these data. 252 +/ 253 class SymbolRender : IDrawable, IDrawableEx 254 { 255 import tida.render; 256 import tida.vector; 257 import tida.color; 258 import tida.shader; 259 import std.conv : to; 260 261 private: 262 Symbol[] symbols; 263 264 public @safe: 265 266 /++ 267 Constructor of the text rendering object. 268 269 Params: 270 symbols = Already drawn ready-made in image text with offset 271 parameters for correct presentation. 272 +/ 273 this(Symbol[] symbols) 274 { 275 this.symbols = symbols; 276 } 277 278 override void draw(IRenderer render, Vecf position) 279 { 280 Shader!Program currShader; 281 282 if (render.type != RenderType.software) 283 { 284 if (render.currentShader !is null) 285 { 286 currShader = render.currentShader; 287 } 288 } 289 290 position.y += (symbols[0].size + (symbols[0].size / 2)); 291 292 foreach (s; symbols) 293 { 294 if (s.image !is null) 295 { 296 if (render.type != RenderType.software) 297 render.currentShader = currShader; 298 299 render.drawEx( s.image, position - vecf(0, s.position.y), 0.0f, 300 vecfNaN, vecfNaN, ubyte.max, s.color); 301 } 302 303 position = position + vecf(s.advance.x.to!int >> 6, s.offsetY); 304 } 305 } 306 307 override void drawEx( IRenderer render, 308 Vecf position, 309 float angle, 310 Vecf center, 311 Vecf size, 312 ubyte alpha, 313 Color!ubyte color) 314 { 315 Shader!Program currShader; 316 317 if (render.type != RenderType.software) 318 { 319 if (render.currentShader !is null) 320 { 321 currShader = render.currentShader; 322 } 323 } 324 325 position.y += (symbols[0].size + (symbols[0].size / 2)); 326 327 foreach (s; symbols) 328 { 329 if (s.image !is null) 330 { 331 if (render.type != RenderType.software) 332 render.currentShader = currShader; 333 334 render.drawEx(s.image, position, angle, center, vecfNaN, alpha, 335 s.color); 336 } 337 338 position = position + vecf(s.advance.x.to!int >> 6, 0); 339 } 340 } 341 } 342 343 /++ 344 An object for rendering drawing symbols. 345 +/ 346 class Text 347 { 348 import tida.color; 349 import tida.image; 350 import tida.vector; 351 352 private: 353 Font font; 354 355 public @trusted: 356 this(Font font) 357 { 358 this.font = font; 359 } 360 361 /++ 362 Draws each character and gives them a relative position to position 363 them correctly in the text. 364 365 Params: 366 T = String type. 367 text = Text. 368 color = Color text. 369 +/ 370 Symbol[] toSymbols(T)(T text, Color!ubyte color = rgb(0, 0, 0)) 371 { 372 static assert(isSomeString!T, T.stringof ~ " is not a string!"); 373 374 Symbol[] symbols; 375 376 for (size_t i = 0; i < text.length; ++i) 377 { 378 TypeChar!T s = text[i]; 379 Image image; 380 FT_GlyphSlot glyph; 381 382 FT_Load_Char(font.face, s, FT_LOAD_RENDER | FT_LOAD_TARGET_NORMAL); 383 const glyphIndex = FT_Get_Char_Index(font.face, s); 384 385 FT_Load_Glyph(font.face, glyphIndex, FT_LOAD_DEFAULT); 386 FT_Render_Glyph(font.face.glyph, FT_RENDER_MODE_NORMAL); 387 388 glyph = font.face.glyph; 389 390 if (s != ' ' && glyph.bitmap.width > 0 && glyph.bitmap.rows > 0) 391 { 392 auto bitmap = glyph.bitmap; 393 394 image = new Image(); 395 image.allocatePlace(bitmap.width,bitmap.rows); 396 397 auto pixels = image.pixels; 398 399 foreach(j; 0 .. bitmap.width * bitmap.rows) 400 { 401 pixels[j] = rgba(255, 255, 255, bitmap.buffer[j]); 402 } 403 } 404 405 symbols ~= new Symbol(image, 406 Vecf(glyph.bitmap_left, glyph.bitmap_top), 407 Vecf(glyph.advance.x, 0), 408 font.size, 409 color); 410 } 411 412 return symbols; 413 } 414 415 /++ 416 Outputs text with color and positional formatting. 417 418 Using the special block `$ <...>`, you can highlight text with color, 419 for example: 420 --- 421 new Text(font).toSymbolsFormat("Black text! $<ffffff> white text!\nNew line!"); 422 --- 423 424 There must be a hex color inside the special block, which will be used later. 425 Also, line wrapping is supported. 426 427 Params: 428 T = string type. 429 symbols = Text. 430 color = Default color. 431 +/ 432 Symbol[] toSymbolsFormat(T)(T symbols, 433 Color!ubyte defaultColor = rgba(255,255,255,255)) 434 { 435 static assert(isSomeString!T, T.stringof ~ " is not a string!"); 436 437 Symbol[] result; 438 int previous = 0; 439 int j = 0; 440 441 Color!ubyte color = defaultColor; 442 443 for (int i = 0; i < symbols.length; i++) 444 { 445 if (symbols[i] == '$') 446 { 447 if (symbols[i+1] == '<') { 448 T colorText; 449 450 __symEach: for (j = i; j < symbols.length; j++) 451 { 452 if (symbols[j] == '>') 453 { 454 colorText = symbols[i+2 .. j]; 455 break __symEach; 456 } 457 } 458 459 if (colorText != "") 460 { 461 result ~= this.toSymbols(symbols[previous .. i], color); 462 463 color = Color!ubyte(colorText); 464 i = j + 1; 465 previous = i; 466 } 467 } 468 } else 469 if (symbols[i] == '\n') 470 { 471 Symbol[] temp = this.toSymbols(symbols[previous .. i], color); 472 Symbol last = temp[$ - 1]; 473 last.offsetY += last.size * 2; 474 float tc = -(result.widthSymbols + widthSymbols(temp)); 475 int ic = cast(int) tc; 476 last.advance = vecf((ic << 6) + last.advance.x , 0); 477 478 previous = i + 1; 479 480 result ~= temp; 481 } 482 } 483 484 result ~= this.toSymbols(symbols[previous .. $], color); 485 486 return result; 487 } 488 489 /++ 490 Creates symbols for rendering and immediately returns the object of 491 their renderer. 492 493 Params: 494 T = String type. 495 text = Text. 496 color = Color text. 497 +/ 498 SymbolRender renderSymbols(T)(T text, Color!ubyte color = rgb(0, 0, 0)) 499 { 500 static assert(isSomeString!T, T.stringof ~ " is not a string!"); 501 return new SymbolRender(toSymbols(text, color)); 502 } 503 504 /++ 505 Outputs characters with color formatting and sequel, renders such text. 506 507 Params: 508 T = String type. 509 text = Text. 510 color = Color text. 511 +/ 512 SymbolRender renderFormat(T)(T text, Color!ubyte color = rgb(0, 0, 0)) 513 { 514 static assert(isSomeString!T, T.stringof ~ " is not a string!"); 515 return new SymbolRender(toSymbolsFormat(text, color)); 516 } 517 }