1 module fluid.typeface; 2 3 import bindbc.freetype; 4 5 import std.range; 6 import std.traits; 7 import std.string; 8 import std.algorithm; 9 10 import fluid.rope; 11 import fluid.utils; 12 import fluid.backend; 13 14 15 @safe: 16 17 18 version (BindFT_Static) { 19 debug (Fluid_BuildMessages) { 20 pragma(msg, "Fluid: Using static freetype"); 21 } 22 } 23 else { 24 version = BindFT_Dynamic; 25 debug (Fluid_BuildMessages) { 26 pragma(msg, "Fluid: Using dynamic freetype"); 27 } 28 } 29 30 /// Low-level interface for drawing text. Represents a single typeface. 31 /// 32 /// Unlike the rest of Fluid, Typeface uses screen-space dots directly, instead of fixed-size pixels. Consequently, DPI 33 /// must be specified manually. 34 /// 35 /// See: [fluid.text.Text] for an interface on a higher level. 36 interface Typeface { 37 38 /// List glyphs in the typeface. 39 long glyphCount() const; 40 41 /// Get initial pen position. 42 Vector2 penPosition() const; 43 44 /// Get line height. 45 int lineHeight() const; 46 47 /// Width of an indent/tab character, in dots. 48 /// `Text` sets `indentWidth` automatically. 49 ref inout(int) indentWidth() inout; 50 51 /// Get advance vector for the given glyph. Uses dots, not pixels, as the unit. 52 Vector2 advance(dchar glyph); 53 54 /// Set font scale. This should be called at least once before drawing. 55 /// `Text` sets DPI automatically. 56 /// 57 /// Font renderer should cache this and not change the scale unless updated. 58 /// 59 /// Params: 60 /// scale = Horizontal and vertical DPI value, for example (96, 96) 61 deprecated("Use setSize instead.") 62 Vector2 dpi(Vector2 scale); 63 64 /// Get curently set DPI. 65 Vector2 dpi() const; 66 67 /// Set the font size. This should be called at least once before drawing. 68 /// `Text`, if used, sets this automatically. 69 /// 70 /// Font renderer should cache this and not change the scale unless updated. 71 /// 72 /// Params: 73 /// dpi = Horizontal and vertical DPI value, for example (96, 96). 74 /// size = Size of the font, in pixels. 75 void setSize(Vector2 dpi, float size); 76 77 /// Draw a line of text. 78 /// Note: This API is unstable and might change over time. 79 /// Params: 80 /// target = Image to draw to. 81 /// penPosition = Pen position for the beginning of the line. Updated to the pen position at the end of th line. 82 /// text = Text to draw. 83 /// paletteIndex = If the image has a palette, this is the index to get colors from. 84 void drawLine(ref Image target, ref Vector2 penPosition, Rope text, ubyte paletteIndex = 0) const; 85 86 /// Instances of Typeface have to be comparable in a memory-safe manner. 87 bool opEquals(const Object object) @safe const; 88 89 /// Get the default Fluid typeface. 90 static defaultTypeface() { 91 return FreetypeTypeface.defaultTypeface; 92 } 93 94 /// Default word splitter used by measure/draw. 95 alias defaultWordChunks = .breakWords; 96 97 /// Updated version of `std.string.lineSplitter` that includes trailing empty lines. 98 /// 99 /// `lineSplitterIndex` will produce a tuple with the index into the original text as the first element. 100 static lineSplitter(KeepTerminator keepTerm = No.keepTerminator, Range)(Range text) 101 if (isSomeChar!(ElementType!Range)) 102 do { 103 104 enum dchar lineSep = '\u2028'; // Line separator. 105 enum dchar paraSep = '\u2029'; // Paragraph separator. 106 enum dchar nelSep = '\u0085'; // Next line. 107 108 import std.utf : byDchar; 109 110 const hasEmptyLine = byDchar(text).endsWith('\r', '\n', '\v', '\f', "\r\n", lineSep, paraSep, nelSep) != 0; 111 auto split = .lineSplitter!keepTerm(text); 112 113 // Include the empty line if present 114 return hasEmptyLine.choose( 115 split.chain(only(typeof(text).init)), 116 split, 117 ); 118 119 } 120 121 /// ditto 122 static lineSplitterIndex(Range)(Range text) { 123 124 import std.typecons : tuple; 125 126 auto initialValue = tuple(size_t.init, Range.init, size_t.init); 127 128 return Typeface.lineSplitter!(Yes.keepTerminator)(text) 129 130 // Insert the index, remove the terminator 131 // Position [2] is line end index 132 .cumulativeFold!((a, line) => tuple(a[2], line.chomp, a[2] + line.length))(initialValue) 133 134 // Remove item [2] 135 .map!(a => tuple(a[0], a[1])); 136 137 } 138 139 unittest { 140 141 import std.typecons : tuple; 142 143 auto myLine = "One\nTwo\r\nThree\vStuff\nï\nö"; 144 auto result = [ 145 tuple(0, "One"), 146 tuple(4, "Two"), 147 tuple(9, "Three"), 148 tuple(15, "Stuff"), 149 tuple(21, "ï"), 150 tuple(24, "ö"), 151 ]; 152 153 assert(lineSplitterIndex(myLine).equal(result)); 154 assert(lineSplitterIndex(Rope(myLine)).equal(result)); 155 156 } 157 158 unittest { 159 160 assert(lineSplitter(Rope("ą")).equal(lineSplitter("ą"))); 161 162 } 163 164 /// Measure space the given text would span. Uses dots as the unit. 165 /// 166 /// If `availableSpace` is specified, assumes text wrapping. Text wrapping is only performed on whitespace 167 /// characters. 168 /// 169 /// Params: 170 /// chunkWords = Algorithm to use to break words when wrapping text; separators must be preserved as separate 171 /// words. 172 /// availableSpace = Amount of available space the text can take up (dots), used to wrap text. 173 /// text = Text to measure. 174 /// wrap = Toggle text wrapping. Defaults to on, unless using the single argument overload. 175 /// 176 /// Returns: 177 /// Vector2 representing the text size, if `TextRuler` is not specified as an argument. 178 final Vector2 measure(alias chunkWords = defaultWordChunks, String) 179 (Vector2 availableSpace, String text, bool wrap = true) 180 do { 181 182 auto ruler = TextRuler(this, wrap ? availableSpace.x : float.nan); 183 184 measure!chunkWords(ruler, text, wrap); 185 186 return ruler.textSize; 187 188 } 189 190 /// ditto 191 final Vector2 measure(String)(String text) { 192 193 // No wrap, only explicit in-text line breaks 194 auto ruler = TextRuler(this); 195 196 measure(ruler, text, false); 197 198 return ruler.textSize; 199 200 } 201 202 /// ditto 203 static void measure(alias chunkWords = defaultWordChunks, String) 204 (ref TextRuler ruler, String text, bool wrap = true) 205 do { 206 207 // TODO don't fail on decoding errors 208 // TODO RTL layouts 209 // TODO vertical text 210 211 // Split on lines 212 foreach (line; lineSplitter(text)) { 213 214 ruler.startLine(); 215 216 // Split on words; do nothing in particular, just run the measurements 217 foreach (word, penPosition; eachWord!chunkWords(ruler, line, wrap)) { } 218 219 } 220 221 } 222 223 /// Helper function 224 static auto eachWord(alias chunkWords = defaultWordChunks, String) 225 (ref TextRuler ruler, String text, bool wrap = true) 226 do { 227 228 struct Helper { 229 230 alias ElementType = CommonType!(String, typeof(chunkWords(text).front)); 231 232 // I'd use `choose` but it's currently broken 233 int opApply(int delegate(ElementType, Vector2) @safe yield) { 234 235 // Text wrapping on 236 if (wrap) { 237 238 auto range = chunkWords(text); 239 240 // Empty line, yield an empty word 241 if (range.empty) { 242 243 const penPosition = ruler.addWord(String.init); 244 if (const ret = yield(String.init, penPosition)) return ret; 245 246 } 247 248 // Split each word 249 else foreach (word; chunkWords(text)) { 250 251 const penPosition = ruler.addWord(word); 252 if (const ret = yield(word, penPosition)) return ret; 253 254 } 255 256 return 0; 257 258 } 259 260 // Text wrapping off 261 else { 262 263 const penPosition = ruler.addWord(text); 264 return yield(text, penPosition); 265 266 } 267 268 } 269 270 } 271 272 return Helper(); 273 274 } 275 276 /// Draw text within the given rectangle in the image. 277 final void draw(alias chunkWords = defaultWordChunks, String) 278 (ref Image image, Rectangle rectangle, String text, ubyte paletteIndex, bool wrap = true) 279 const { 280 281 auto ruler = TextRuler(this, rectangle.w); 282 283 // TODO decoding errors 284 285 // Split on lines 286 foreach (line; this.lineSplitter(text)) { 287 288 ruler.startLine(); 289 290 // Split on words 291 foreach (word, penPosition; eachWord!chunkWords(ruler, line, wrap)) { 292 293 auto wordPenPosition = rectangle.start + penPosition; 294 295 drawLine(image, wordPenPosition, word, paletteIndex); 296 297 } 298 299 } 300 301 } 302 303 /// Helper function for typeface implementations, providing a "draw" function for tabs, adjusting the pen position 304 /// automatically. 305 protected final void drawTab(ref Vector2 penPosition) const { 306 307 penPosition.x += _tabWidth(penPosition.x); 308 309 } 310 311 private final float _tabWidth(float xoffset) const { 312 313 return indentWidth - (xoffset % indentWidth); 314 315 } 316 317 } 318 319 /// Break words on whitespace and punctuation. Splitter characters stick to the word that precedes them, e.g. 320 /// `foo!! bar.` is split as `["foo!! ", "bar."]`. 321 auto breakWords(Range)(Range range) { 322 323 import std.uni : isAlphaNum, isWhite; 324 import std.utf : decodeFront; 325 326 /// Pick the group the character belongs to. 327 static int pickGroup(dchar a) { 328 329 return a.isAlphaNum ? 0 330 : a.isWhite ? 1 331 : 2; 332 333 } 334 335 /// Splitter function that splits in any case two 336 static bool isSplit(dchar a, dchar b) { 337 338 return !a.isAlphaNum && !b.isWhite && pickGroup(a) != pickGroup(b); 339 340 } 341 342 struct BreakWords { 343 344 Range range; 345 Range front = Range.init; 346 347 bool empty() const { 348 return front.empty; 349 } 350 351 void popFront() { 352 353 dchar lastChar = 0; 354 auto originalRange = range.save; 355 356 while (!range.empty) { 357 358 if (lastChar && isSplit(lastChar, range.front)) break; 359 lastChar = range.decodeFront; 360 361 } 362 363 front = originalRange[0 .. $ - range.length]; 364 365 } 366 367 } 368 369 auto chunks = BreakWords(range); 370 chunks.popFront; 371 return chunks; 372 373 } 374 375 unittest { 376 377 const test = "hellö world! 123 hellö123*hello -- hello -- - &&abcde!a!!?!@!@#3"; 378 const result = [ 379 "hellö ", 380 "world! ", 381 "123 ", 382 "hellö123*", 383 "hello ", 384 "-- ", 385 "hello ", 386 "-- ", 387 "- ", 388 "&&", 389 "abcde!", 390 "a!!?!@!@#", 391 "3" 392 ]; 393 394 assert(breakWords(test).equal(result)); 395 assert(breakWords(Rope(test)).equal(result)); 396 397 const test2 = "Аа Бб Вв Гг Дд Ее Ëë Жж Зз Ии " 398 ~ "Йй Кк Лл Мм Нн Оо Пп Рр Сс Тт " 399 ~ "Уу Фф Хх Цц Чч Шш Щщ Ъъ Ыы Ьь " 400 ~ "Ээ Юю Яя "; 401 402 assert(breakWords(test2).equal(breakWords(Rope(test2)))); 403 404 } 405 406 /// Low level interface for measuring text. 407 struct TextRuler { 408 409 /// Typeface to use for the text. 410 Typeface typeface; 411 412 /// Maximum width for a single line. If `NaN`, no word breaking is performed. 413 float lineWidth; 414 415 /// Current pen position. 416 Vector2 penPosition; 417 418 /// Total size of the text. 419 Vector2 textSize; 420 421 /// Index of the word within the line. 422 size_t wordLineIndex; 423 424 this(Typeface typeface, float lineWidth = float.nan) { 425 426 this.typeface = typeface; 427 this.lineWidth = lineWidth; 428 this.penPosition = typeface.penPosition; 429 430 } 431 432 /// Get the caret as a 0 width rectangle. 433 Rectangle caret() const { 434 435 return caret(penPosition); 436 437 } 438 439 /// Get the caret as a 0 width rectangle for the given pen position. 440 Rectangle caret(Vector2 penPosition) const { 441 442 const start = penPosition - Vector2(0, typeface.penPosition.y); 443 444 return Rectangle( 445 start.tupleof, 446 0, typeface.lineHeight, 447 ); 448 449 } 450 451 /// Begin a new line. 452 void startLine() { 453 454 const lineHeight = typeface.lineHeight; 455 456 if (textSize != Vector2.init) { 457 458 // Move the pen to the next line 459 penPosition.x = typeface.penPosition.x; 460 penPosition.y += lineHeight; 461 462 } 463 464 // Allocate space for the line 465 textSize.y += lineHeight; 466 wordLineIndex = 0; 467 468 } 469 470 /// Add the given word to the text. The text must be a single line. 471 /// Returns: Pen position for the word. It might differ from the original penPosition, because the word may be 472 /// moved onto the next line. 473 Vector2 addWord(String)(String word) { 474 475 import std.utf : byDchar; 476 477 const maxWordWidth = lineWidth - penPosition.x; 478 479 float wordSpan = 0; 480 481 // Measure each glyph 482 foreach (glyph; byDchar(word)) { 483 484 // Tab aligns to set indent width 485 if (glyph == '\t') 486 wordSpan += typeface._tabWidth(penPosition.x + wordSpan); 487 488 // Other characters use their regular advance value 489 else 490 wordSpan += typeface.advance(glyph).x; 491 492 } 493 494 // Exceeded line width 495 // Automatically false if lineWidth is NaN 496 if (maxWordWidth < wordSpan && wordLineIndex != 0) { 497 498 // Start a new line 499 startLine(); 500 501 } 502 503 const wordPosition = penPosition; 504 505 // Increment word index 506 wordLineIndex++; 507 508 // Update pen position 509 penPosition.x += wordSpan; 510 511 // Allocate space 512 if (penPosition.x > textSize.x) { 513 514 textSize.x = penPosition.x; 515 516 // Limit space to not exceed maximum width (false if NaN) 517 if (textSize.x > lineWidth) { 518 519 textSize.x = lineWidth; 520 521 } 522 523 } 524 525 return wordPosition; 526 527 } 528 529 } 530 531 /// Represents a freetype2-powered typeface. 532 class FreetypeTypeface : Typeface { 533 534 public { 535 536 /// Underlying face. 537 FT_Face face; 538 539 /// Adjust line height. `1` uses the original line height, `2` doubles it. 540 float lineHeightFactor = 1; 541 542 } 543 544 protected { 545 546 struct AdvanceCacheKey { 547 dchar ch; 548 float size; 549 } 550 551 /// Cache for character sizes. 552 Vector2[AdvanceCacheKey] advanceCache; 553 554 } 555 556 private { 557 558 /// If true, this typeface has been loaded using this class, making the class responsible for freeing the font. 559 bool _isOwner; 560 561 /// Font size loaded (in pixels). 562 float _size; 563 564 /// Current DPI set for the typeface. 565 int _dpiX, _dpiY; 566 567 int _indentWidth; 568 569 } 570 571 static FreetypeTypeface defaultTypeface; 572 573 static this() @trusted { 574 575 // Set the default typeface 576 FreetypeTypeface.defaultTypeface = new FreetypeTypeface; 577 578 } 579 580 /// Load the default typeface. 581 this() @trusted { 582 583 static typefaceFile = cast(ubyte[]) import("ruda-regular.ttf"); 584 const typefaceSize = cast(int) typefaceFile.length; 585 586 // Load the font 587 if (auto error = FT_New_Memory_Face(freetype, typefaceFile.ptr, typefaceSize, 0, &face)) { 588 589 assert(false, format!"Failed to load default Fluid typeface, error no. %s"(error)); 590 591 } 592 593 // Mark self as the owner 594 this.isOwner = true; 595 this.lineHeightFactor = 1.16; 596 597 } 598 599 /// Params: 600 /// face = Existing freetype2 typeface to use. 601 this(FT_Face face) { 602 603 this.face = face; 604 605 } 606 607 /// Load a font from a file. 608 /// Params: 609 /// filename = Filename of the font file. 610 this(string filename) @trusted { 611 612 this._isOwner = true; 613 614 // TODO proper exceptions 615 if (auto error = FT_New_Face(freetype, filename.toStringz, 0, &this.face)) { 616 617 throw new Exception(format!"Failed to load `%s`, error no. %s"(filename, error)); 618 619 } 620 621 } 622 623 ~this() @trusted { 624 625 // Ignore if the resources used by the class have been borrowed 626 if (!_isOwner) return; 627 628 FT_Done_Face(face); 629 630 } 631 632 /// Instances of Typeface have to be comparable in a memory-safe manner. 633 override bool opEquals(const Object object) @safe const { 634 635 return this is object; 636 637 } 638 639 ref inout(int) indentWidth() inout { 640 return _indentWidth; 641 } 642 bool isOwner() const { 643 return _isOwner; 644 } 645 bool isOwner(bool value) @system { 646 return _isOwner = value; 647 } 648 649 long glyphCount() const { 650 651 return face.num_glyphs; 652 653 } 654 655 /// Get initial pen position. 656 Vector2 penPosition() const { 657 658 return Vector2(0, face.size.metrics.ascender) / 64; 659 660 } 661 662 /// Line height. 663 int lineHeight() const { 664 665 // +1 is an error margin 666 return cast(int) (face.size.metrics.height * lineHeightFactor / 64) + 1; 667 668 } 669 670 Vector2 dpi() const { 671 672 return Vector2(_dpiX, _dpiY); 673 674 } 675 676 deprecated("Use setSize instead") 677 Vector2 dpi(Vector2 dpi) { 678 679 setSize(dpi, _size); 680 return dpi; 681 682 } 683 684 void setSize(Vector2 dpi, float size) @trusted { 685 686 const dpiX = cast(int) dpi.x; 687 const dpiY = cast(int) dpi.y; 688 689 // Ignore if there's no change 690 if (dpiX == _dpiX && dpiY == _dpiY && size == _size) return; 691 692 _dpiX = dpiX; 693 _dpiY = dpiY; 694 _size = size; 695 696 // dunno why, but FT_Set_Char_Size yields better results, kerning specifically, than FT_Set_Pixel_Sizes 697 version (all) { 698 const error = FT_Set_Char_Size(face, 0, cast(int) (size.pxToPt * 64 + 1), dpiX, dpiY); 699 } 700 701 // Load size 702 else { 703 const dotsX = cast(int) (size * dpi.x / 96); 704 const dotsY = cast(int) (size * dpi.y / 96); 705 const error = FT_Set_Pixel_Sizes(face, dotsX, dotsY); 706 } 707 708 // Test for errors 709 if (error) { 710 711 throw new Exception( 712 format!"Failed to load font at size %s at DPI %sx%s, error no. %s"(size, dpiX, dpiY, error) 713 ); 714 715 } 716 717 } 718 719 Vector2 advance(dchar glyph) @trusted { 720 721 assert(_dpiX && _dpiY, "Font DPI hasn't been set"); 722 723 const key = AdvanceCacheKey(glyph, _size); 724 725 // Return the stored value if the result is cached 726 if (auto result = key in advanceCache) { 727 728 return *result; 729 730 } 731 732 // Load the glyph 733 if (auto error = FT_Load_Char(cast(FT_Face) face, glyph, FT_LOAD_DEFAULT)) { 734 735 return advanceCache[key] = Vector2(0, 0); 736 737 } 738 739 // Advance the cursor position 740 // TODO RTL layouts 741 return advanceCache[key] = Vector2(face.glyph.advance.tupleof) / 64; 742 743 } 744 745 /// Draw a line of text 746 void drawLine(ref Image target, ref Vector2 penPosition, const Rope text, ubyte paletteIndex) const @trusted { 747 748 assert(_dpiX && _dpiY, "Font DPI hasn't been set"); 749 750 foreach (glyph; text.byDchar) { 751 752 // Tab character 753 if (glyph == '\t') { 754 755 drawTab(penPosition); 756 continue; 757 758 } 759 760 // Load the glyph 761 if (auto error = FT_Load_Char(cast(FT_Face) face, glyph, FT_LOAD_RENDER)) { 762 763 continue; 764 765 } 766 767 const bitmap = face.glyph.bitmap; 768 769 assert(bitmap.pixel_mode == FT_PIXEL_MODE_GRAY); 770 771 // Draw it to the image 772 foreach (y; 0..bitmap.rows) { 773 774 foreach (x; 0..bitmap.width) { 775 776 // Each pixel is a single byte 777 const pixel = *cast(ubyte*) (bitmap.buffer + bitmap.pitch*y + x); 778 779 const targetX = cast(int) penPosition.x + face.glyph.bitmap_left + x; 780 const targetY = cast(int) penPosition.y - face.glyph.bitmap_top + y; 781 782 // Don't draw pixels out of bounds 783 if (targetX >= target.width || targetY >= target.height) continue; 784 785 // Choose the stronger color 786 const ubyte oldAlpha = target.get(targetX, targetY).a; 787 const ubyte newAlpha = ubyte.max * pixel / pixel.max; 788 789 if (newAlpha >= oldAlpha) 790 target.set(targetX, targetY, PalettedColor(paletteIndex, newAlpha)); 791 792 } 793 794 } 795 796 // Advance pen positon 797 penPosition += Vector2(face.glyph.advance.tupleof) / 64; 798 799 } 800 801 } 802 803 } 804 805 unittest { 806 807 auto image = generateColorImage(10, 10, color("#fff")); 808 auto tf = FreetypeTypeface.defaultTypeface; 809 tf.setSize(Vector2(96, 96), 14.pt); 810 tf.indentWidth = cast(int) (tf.advance(' ').x * 4); 811 812 Vector2 measure(string text) { 813 814 Vector2 penPosition; 815 tf.drawLine(image, penPosition, Rope(text), 0); 816 return penPosition; 817 818 } 819 820 // Draw 4 spaces to use as reference in the test 821 const indentReference = measure(" "); 822 823 assert(indentReference.x > 0); 824 assert(indentReference.x == tf.advance(' ').x * 4); 825 assert(indentReference.x == tf.indentWidth); 826 827 assert(measure("\t") == indentReference); 828 assert(measure("a\t") == indentReference); 829 830 const doubleAIndent = measure("aa").x > indentReference.x 831 ? 2 832 : 1; 833 const tripleAIndent = measure("aaa").x > doubleAIndent * indentReference.x 834 ? doubleAIndent + 1 835 : doubleAIndent; 836 837 assert(measure("aa\t") == indentReference * doubleAIndent); 838 assert(measure("aaa\t") == indentReference * tripleAIndent); 839 assert(measure("\t\t") == indentReference * 2); 840 assert(measure("a\ta\t") == indentReference * 2); 841 assert(measure("aa\taa\t") == 2 * indentReference * doubleAIndent); 842 843 } 844 845 version (BindFT_Dynamic) 846 shared static this() @system { 847 848 // Ignore if freetype was already loaded 849 if (isFreeTypeLoaded) return; 850 851 // Load freetype 852 FTSupport ret = loadFreeType(); 853 854 // Check version 855 if (ret != ftSupport) { 856 857 if (ret == FTSupport.noLibrary) { 858 859 assert(false, "freetype2 failed to load"); 860 861 } 862 else if (FTSupport.badLibrary) { 863 864 assert(false, format!"found freetype2 is of incompatible version %s (needed %s)"(ret, ftSupport)); 865 866 } 867 868 } 869 870 } 871 872 /// Get the thread-local freetype reference. 873 FT_Library freetype() @trusted { 874 875 static FT_Library library; 876 877 // Load the library 878 if (library != library.init) return library; 879 880 if (auto error = FT_Init_FreeType(&library)) { 881 882 assert(false, format!"Failed to load freetype2: %s"(error)); 883 884 } 885 886 return library; 887 888 }