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