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