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 .. $]; 194 break __symEachCutter; 195 } 196 } 197 } 198 } 199 } 200 201 return symbols; 202 } 203 204 /++ 205 Returns the size of the rendered text for the given font. 206 207 Params: 208 T = String type. 209 text = Text. 210 font = Font. 211 +/ 212 int widthText(T)(T text, Font font) @safe 213 { 214 import std.algorithm : reduce; 215 static assert(isSomeString!T, T.stringof ~ " is not a string!"); 216 217 text = cutFormat!T(text); 218 219 return new Text(font) 220 .toSymbols!T(text) 221 .widthSymbols; 222 } 223 224 /++ 225 Shows the width of the displayed characters. 226 227 Params: 228 text = Displayed characters. 229 +/ 230 int widthSymbols(Symbol[] text) @safe 231 { 232 import std.algorithm : reduce; 233 import std.conv : to; 234 235 int width = int.init; 236 237 foreach(s; text) 238 width += s.advance.x.to!int >> 6; 239 240 return width; 241 } 242 243 /++ 244 Draw text object. An array of letters and offset attributes already rendered 245 into the image is fed into the constructor, and the text is output 246 from these data. 247 +/ 248 class SymbolRender : IDrawable, IDrawableEx 249 { 250 import tida.render; 251 import tida.vector; 252 import tida.color; 253 import tida.shader; 254 import std.conv : to; 255 256 private: 257 Symbol[] symbols; 258 259 public @safe: 260 261 /++ 262 Constructor of the text rendering object. 263 264 Params: 265 symbols = Already drawn ready-made in image text with offset 266 parameters for correct presentation. 267 +/ 268 this(Symbol[] symbols) 269 { 270 this.symbols = symbols; 271 } 272 273 override void draw(IRenderer render, Vecf position) 274 { 275 Shader!Program currShader; 276 277 if (render.type != RenderType.software) 278 { 279 if (render.currentShader !is null) 280 { 281 currShader = render.currentShader; 282 } 283 } 284 285 position.y += (symbols[0].size + (symbols[0].size / 2)); 286 287 foreach (s; symbols) 288 { 289 if (s.image !is null) 290 { 291 if (render.type != RenderType.software) 292 render.currentShader = currShader; 293 294 render.drawEx( s.image, position - vecf(0, s.position.y), 0.0f, 295 vecfNaN, vecfNaN, ubyte.max, s.color); 296 } 297 298 position = position + vecf(s.advance.x.to!int >> 6, s.offsetY); 299 } 300 } 301 302 override void drawEx( IRenderer render, 303 Vecf position, 304 float angle, 305 Vecf center, 306 Vecf size, 307 ubyte alpha, 308 Color!ubyte color) 309 { 310 Shader!Program currShader; 311 312 if (render.type != RenderType.software) 313 { 314 if (render.currentShader !is null) 315 { 316 currShader = render.currentShader; 317 } 318 } 319 320 position.y += (symbols[0].size + (symbols[0].size / 2)); 321 322 foreach (s; symbols) 323 { 324 if (s.image !is null) 325 { 326 if (render.type != RenderType.software) 327 render.currentShader = currShader; 328 329 render.drawEx(s.image, position, angle, center, vecfNaN, alpha, 330 s.color); 331 } 332 333 position = position + vecf(s.advance.x.to!int >> 6, 0); 334 } 335 } 336 } 337 338 /++ 339 An object for rendering drawing symbols. 340 +/ 341 class Text 342 { 343 import tida.color; 344 import tida.image; 345 import tida.vector; 346 347 private: 348 Font font; 349 350 public @trusted: 351 this(Font font) 352 { 353 this.font = font; 354 } 355 356 /++ 357 Draws each character and gives them a relative position to position 358 them correctly in the text. 359 360 Params: 361 T = String type. 362 text = Text. 363 color = Color text. 364 +/ 365 Symbol[] toSymbols(T)(T text, Color!ubyte color = rgb(0, 0, 0)) 366 { 367 static assert(isSomeString!T, T.stringof ~ " is not a string!"); 368 369 Symbol[] symbols; 370 371 for (size_t i = 0; i < text.length; ++i) 372 { 373 TypeChar!T s = text[i]; 374 Image image; 375 FT_GlyphSlot glyph; 376 377 FT_Load_Char(font.face, s, FT_LOAD_RENDER | FT_LOAD_TARGET_NORMAL); 378 const glyphIndex = FT_Get_Char_Index(font.face, s); 379 380 FT_Load_Glyph(font.face, glyphIndex, FT_LOAD_DEFAULT); 381 FT_Render_Glyph(font.face.glyph, FT_RENDER_MODE_NORMAL); 382 383 glyph = font.face.glyph; 384 385 if (s != ' ' && glyph.bitmap.width > 0 && glyph.bitmap.rows > 0) 386 { 387 auto bitmap = glyph.bitmap; 388 389 image = new Image(); 390 image.allocatePlace(bitmap.width,bitmap.rows); 391 392 auto pixels = image.pixels; 393 394 foreach(j; 0 .. bitmap.width * bitmap.rows) 395 { 396 pixels[j] = rgba(255, 255, 255, bitmap.buffer[j]); 397 } 398 } 399 400 symbols ~= new Symbol(image, 401 Vecf(glyph.bitmap_left, glyph.bitmap_top), 402 Vecf(glyph.advance.x, 0), 403 font.size, 404 color); 405 } 406 407 return symbols; 408 } 409 410 /++ 411 Outputs text with color and positional formatting. 412 413 Using the special block `$ <...>`, you can highlight text with color, 414 for example: 415 --- 416 new Text(font).toSymbolsFormat("Black text! $<ffffff> white text!\nNew line!"); 417 --- 418 419 There must be a hex color inside the special block, which will be used later. 420 Also, line wrapping is supported. 421 422 Params: 423 T = string type. 424 symbols = Text. 425 color = Default color. 426 +/ 427 Symbol[] toSymbolsFormat(T)(T symbols, 428 Color!ubyte defaultColor = rgba(255,255,255,255)) 429 { 430 static assert(isSomeString!T, T.stringof ~ " is not a string!"); 431 432 Symbol[] result; 433 int previous = 0; 434 int j = 0; 435 436 Color!ubyte color = defaultColor; 437 438 for (int i = 0; i < symbols.length; i++) 439 { 440 if (symbols[i] == '$') 441 { 442 if (symbols[i+1] == '<') { 443 T colorText; 444 445 __symEach: for (j = i; j < symbols.length; j++) 446 { 447 if (symbols[j] == '>') 448 { 449 colorText = symbols[i+2 .. j]; 450 break __symEach; 451 } 452 } 453 454 if (colorText != "") 455 { 456 result ~= this.toSymbols(symbols[previous .. i], color); 457 458 color = Color!ubyte(colorText); 459 i = j + 1; 460 previous = i; 461 } 462 } 463 } else 464 if (symbols[i] == '\n') 465 { 466 Symbol[] temp = this.toSymbols(symbols[previous .. i], color); 467 Symbol last = temp[$ - 1]; 468 last.offsetY += last.size * 2; 469 float tc = -(result.widthSymbols + widthSymbols(temp)); 470 int ic = cast(int) tc; 471 last.advance = vecf((ic << 6) + last.advance.x , 0); 472 473 previous = i + 1; 474 475 result ~= temp; 476 } 477 } 478 479 result ~= this.toSymbols(symbols[previous .. $], color); 480 481 return result; 482 } 483 484 /++ 485 Creates symbols for rendering and immediately returns the object of 486 their renderer. 487 488 Params: 489 T = String type. 490 text = Text. 491 color = Color text. 492 +/ 493 SymbolRender renderSymbols(T)(T text, Color!ubyte color = rgb(0, 0, 0)) 494 { 495 static assert(isSomeString!T, T.stringof ~ " is not a string!"); 496 return new SymbolRender(toSymbols(text, color)); 497 } 498 499 /++ 500 Outputs characters with color formatting and sequel, renders such text. 501 502 Params: 503 T = String type. 504 text = Text. 505 color = Color text. 506 +/ 507 SymbolRender renderFormat(T)(T text, Color!ubyte color = rgb(0, 0, 0)) 508 { 509 static assert(isSomeString!T, T.stringof ~ " is not a string!"); 510 return new SymbolRender(toSymbolsFormat(text, color)); 511 } 512 }