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.utils;
11 import fluid.backend;
12 
13 
14 @safe:
15 
16 
17 version (BindFT_Static) {
18     debug (Fluid_BuildMessages) {
19         pragma(msg, "Fluid: Using static freetype");
20     }
21 }
22 else {
23     version = BindFT_Dynamic;
24     debug (Fluid_BuildMessages) {
25         pragma(msg, "Fluid: Using dynamic freetype");
26     }
27 }
28 
29 
30 /// Low-level interface for drawing text. Represents a single typeface.
31 ///
32 /// Unlike the rest of Fluid, Typeface doesn't define pixels as 1/96th of an inch. DPI must also be specified manually.
33 ///
34 /// See: [fluid.text.Text] for an interface on a higher level.
35 interface Typeface {
36 
37     /// List glyphs  in the typeface.
38     long glyphCount() const;
39 
40     /// Get initial pen position.
41     Vector2 penPosition() const;
42 
43     /// Get line height.
44     int lineHeight() const;
45 
46     /// Get advance vector for the given glyph. Uses dots, not pixels, as the unit.
47     Vector2 advance(dchar glyph) const;
48 
49     /// Set font scale. This should be called at least once before drawing.
50     ///
51     /// Font renderer should cache this and not change the scale unless updated.
52     ///
53     /// Params:
54     ///     scale = Horizontal and vertical DPI value, for example (96, 96)
55     Vector2 dpi(Vector2 scale);
56 
57     /// Get curently set DPI.
58     Vector2 dpi() const;
59 
60     /// Draw a line of text.
61     /// Note: This API is unstable and might change over time.
62     void drawLine(ref Image target, ref Vector2 penPosition, string text, Color tint) const;
63 
64     /// Get the default Fluid typeface.
65     static defaultTypeface() => FreetypeTypeface.defaultTypeface;
66 
67     /// Default word splitter used by measure/draw.
68     final defaultWordChunks(Range)(Range range) const {
69 
70         import std.uni;
71 
72         /// Pick the group the character belongs to.
73         static bool pickGroup(dchar a) {
74 
75             return a.isAlphaNum || a.isPunctuation;
76 
77         }
78 
79         return range
80             .splitWhen!((a, b) => pickGroup(a) != pickGroup(b) && !b.isWhite)
81             .map!"text(a)"
82             .cache;
83         // TODO string allocation could probably be avoided
84 
85     }
86 
87     /// Measure space the given text would span. Uses dots as the unit.
88     ///
89     /// If `availableSpace` is specified, assumes text wrapping. Text wrapping is only performed on whitespace
90     /// characters.
91     ///
92     /// Params:
93     ///     chunkWords = Algorithm to use to break words when wrapping text; separators must be preserved.
94     ///     availableSpace = Amount of available space the text can take up (dots), used to wrap text.
95     ///     text = Text to measure.
96     ///     wrap = Toggle text wrapping.
97     final Vector2 measure(alias chunkWords = defaultWordChunks, String)
98         (Vector2 availableSpace, String text, bool wrap = true) const
99     if (isSomeString!String)
100     do {
101 
102         // Wrapping off
103         if (!wrap) return measure(text);
104 
105         auto ruler = TextRuler(this, availableSpace.x);
106 
107         // TODO don't fail on decoding errors
108         // TODO RTL layouts
109         // TODO vertical text
110         // TODO lineSplitter removes the last line feed if present, which is unwanted behavior
111 
112         // Split on lines
113         foreach (line; text.lineSplitter) {
114 
115             ruler.startLine();
116 
117             // Split on words
118             foreach (word; chunkWords(line)) {
119 
120                 ruler.addWord(word);
121 
122             }
123 
124         }
125 
126         return ruler.textSize;
127 
128     }
129 
130     /// ditto
131     final Vector2 measure(String)(String text) const
132     if (isSomeString!String)
133     do {
134 
135         // No wrap, only explicit in-text line breaks
136 
137         auto ruler = TextRuler(this);
138 
139         // TODO don't fail on decoding errors
140 
141         // Split on lines
142         foreach (line; text.lineSplitter) {
143 
144             ruler.startLine();
145             ruler.addWord(line);
146 
147         }
148 
149         return ruler.textSize;
150 
151     }
152 
153     /// Draw text within the given rectangle in the image.
154     final void draw(alias chunkWords = defaultWordChunks, String)
155         (ref Image image, Rectangle rectangle, String text, Color tint, bool wrap = true)
156     const {
157 
158         auto ruler = TextRuler(this, rectangle.w);
159 
160         // TODO decoding errors
161 
162         // Text wrapping on
163         if (wrap) {
164 
165             // Split on lines
166             foreach (line; text.lineSplitter) {
167 
168                 ruler.startLine();
169 
170                 // Split on words
171                 foreach (word; chunkWords(line)) {
172 
173                     auto penPosition = rectangle.start + ruler.addWord(word);
174 
175                     drawLine(image, penPosition, word, tint);
176 
177                 }
178 
179             }
180 
181         }
182 
183         // Text wrapping off
184         else {
185 
186             // Split on lines
187             foreach (line; text.lineSplitter) {
188 
189                 ruler.startLine();
190 
191                 auto penPosition = rectangle.start + ruler.addWord(line);
192 
193                 drawLine(image, penPosition, line, tint);
194 
195             }
196 
197         }
198 
199     }
200 
201 }
202 
203 /// Low level interface for measuring text.
204 struct TextRuler {
205 
206     /// Typeface to use for the text.
207     const Typeface typeface;
208 
209     /// Maximum width for a single line. If `NaN`, no word breaking is performed.
210     float lineWidth;
211 
212     /// Current pen position.
213     Vector2 penPosition;
214 
215     /// Total size of the text.
216     Vector2 textSize;
217 
218     this(const Typeface typeface, float lineWidth = float.nan) {
219 
220         this.typeface = typeface;
221         this.lineWidth = lineWidth;
222 
223         penPosition = typeface.penPosition - typeface.lineHeight;
224 
225     }
226 
227     /// Begin a new line.
228     void startLine() {
229 
230         const lineHeight = typeface.lineHeight;
231 
232         // Move the pen to the next line
233         penPosition.x = 0;
234         penPosition.y += lineHeight;
235         textSize.y += lineHeight;
236 
237     }
238 
239     /// Add the given word to the text. The text must be single line.;
240     /// Returns: Pen position for the word.
241     Vector2 addWord(String)(String word) {
242 
243         const maxWordWidth = lineWidth - penPosition.x;
244 
245         float wordSpan = 0;
246 
247         // Measure each glyph
248         foreach (dchar glyph; word) {
249 
250             wordSpan += typeface.advance(glyph).x;
251 
252         }
253 
254         // Exceeded line width
255         // Automatically false if lineWidth is NaN
256         if (maxWordWidth < wordSpan && wordSpan < lineWidth) {
257 
258             // Start a new line
259             startLine();
260 
261         }
262 
263         const wordPosition = penPosition;
264 
265         // Update pen position
266         penPosition.x += wordSpan;
267 
268         // Allocate space
269         if (penPosition.x > textSize.x) textSize.x = penPosition.x;
270 
271         return wordPosition;
272 
273     }
274 
275 }
276 
277 /// Represents a freetype2-powered typeface.
278 class FreetypeTypeface : Typeface {
279 
280     public {
281 
282         /// Underlying face.
283         FT_Face face;
284 
285         /// Adjust line height. `1` uses the original line height, `2` doubles it.
286         float lineHeightFactor = 1;
287 
288     }
289 
290     private {
291 
292         /// If true, this typeface has been loaded using this class, making the class responsible for freeing the font.
293         bool _isOwner;
294 
295         /// Font size loaded (points).
296         int _size;
297 
298         /// Current DPI set for the typeface.
299         int _dpiX, _dpiY;
300 
301     }
302 
303     static FreetypeTypeface defaultTypeface;
304 
305     static this() @trusted {
306 
307         // Set the default typeface
308         FreetypeTypeface.defaultTypeface = new FreetypeTypeface(14);
309 
310     }
311 
312     /// Load the default typeface
313     this(int size) @trusted {
314 
315         static typefaceFile = cast(ubyte[]) import("ruda-regular.ttf");
316         const typefaceSize = cast(int) typefaceFile.length;
317 
318         // Load the font
319         if (auto error = FT_New_Memory_Face(freetype, typefaceFile.ptr, typefaceSize, 0, &face)) {
320 
321             assert(false, format!"Failed to load default Fluid typeface at size %s, error no. %s"(size, error));
322 
323         }
324 
325         // Mark self as the owner
326         this._size = size;
327         this.isOwner = true;
328         this.lineHeightFactor = 1.16;
329 
330     }
331 
332     /// Use an existing freetype2 font.
333     this(FT_Face face, int size) {
334 
335         this.face = face;
336         this._size = size;
337 
338     }
339 
340     /// Load a font from a file.
341     /// Params:
342     ///     backend  = I/O Fluid backend, used to adjust the scale of the font.
343     ///     filename = Filename of the font file.
344     ///     size     = Size of the font to load (in points).
345     this(string filename, int size) @trusted {
346 
347         this._isOwner = true;
348         this._size = size;
349 
350         // TODO proper exceptions
351         if (auto error = FT_New_Face(freetype, filename.toStringz, 0, &this.face)) {
352 
353             throw new Exception(format!"Failed to load `%s`, error no. %s"(filename, error));
354 
355         }
356 
357     }
358 
359     ~this() @trusted {
360 
361         // Ignore if the resources used by the class have been borrowed
362         if (!_isOwner) return;
363 
364         FT_Done_Face(face);
365 
366     }
367 
368     bool isOwner() const => _isOwner;
369     bool isOwner(bool value) @system => _isOwner = value;
370 
371     long glyphCount() const {
372 
373         return face.num_glyphs;
374 
375     }
376 
377     /// Get initial pen position.
378     Vector2 penPosition() const {
379 
380         return Vector2(0, face.size.metrics.ascender) / 64;
381 
382     }
383 
384     /// Line height.
385     int lineHeight() const {
386 
387         // +1 is an error margin
388         return cast(int) (face.size.metrics.height * lineHeightFactor / 64) + 1;
389 
390     }
391 
392     Vector2 dpi() const {
393 
394         return Vector2(_dpiX, _dpiY);
395 
396     }
397 
398     Vector2 dpi(Vector2 dpi) @trusted {
399 
400         const dpiX = cast(int) dpi.x;
401         const dpiY = cast(int) dpi.y;
402 
403         // Ignore if there's no change
404         if (dpiX == _dpiX && dpiY == _dpiY) return dpi;
405 
406         _dpiX = dpiX;
407         _dpiY = dpiY;
408 
409         // Load size
410         if (auto error = FT_Set_Char_Size(face, 0, _size*64, dpiX, dpiY)) {
411 
412             throw new Exception(
413                 format!"Failed to load font at size %s at DPI %sx%s, error no. %s"(_size, dpiX, dpiY, error)
414             );
415 
416         }
417 
418         return dpi;
419 
420     }
421 
422     /// Get advance vector for the given glyph
423     Vector2 advance(dchar glyph) const @trusted {
424 
425         assert(_dpiX && _dpiY, "Font DPI hasn't been set");
426 
427         // Sadly, there is no way to make FreeType operate correctly in `const` environment.
428 
429         // Load the glyph
430         if (auto error = FT_Load_Char(cast(FT_FaceRec*) face, glyph, FT_LOAD_DEFAULT)) {
431 
432             return Vector2(0, 0);
433 
434         }
435 
436         // Advance the cursor position
437         // TODO RTL layouts
438         return Vector2(face.glyph.advance.tupleof) / 64;
439 
440     }
441 
442     /// Draw a line of text
443     void drawLine(ref Image target, ref Vector2 penPosition, string text, Color tint) const @trusted {
444 
445         assert(_dpiX && _dpiY, "Font DPI hasn't been set");
446 
447         foreach (dchar glyph; text) {
448 
449             // Load the glyph
450             if (auto error = FT_Load_Char(cast(FT_FaceRec*) face, glyph, FT_LOAD_RENDER)) {
451 
452                 continue;
453 
454             }
455 
456             const bitmap = face.glyph.bitmap;
457 
458             assert(bitmap.pixel_mode == FT_PIXEL_MODE_GRAY);
459 
460             // Draw it to the image
461             foreach (y; 0..bitmap.rows) {
462 
463                 foreach (x; 0..bitmap.width) {
464 
465                     // Each pixel is a single byte
466                     const pixel = *cast(ubyte*) (bitmap.buffer + bitmap.pitch*y + x);
467 
468                     const targetX = cast(int) penPosition.x + face.glyph.bitmap_left + x;
469                     const targetY = cast(int) penPosition.y - face.glyph.bitmap_top + y;
470 
471                     // Don't draw pixels out of bounds
472                     if (targetX >= target.width || targetY >= target.height) continue;
473 
474                     // Note: ImageDrawPixel overrides the pixel — alpha blending has to be done by us
475                     const oldColor = target.get(targetX, targetY);
476                     const newColor = tint.setAlpha(cast(float) pixel / pixel.max);
477                     const color = alphaBlend(oldColor, newColor);
478 
479                     target.get(targetX, targetY) = color;
480 
481                 }
482 
483             }
484 
485             // Advance pen positon
486             penPosition += Vector2(face.glyph.advance.tupleof) / 64;
487 
488         }
489 
490     }
491 
492 }
493 
494 /// Font rendering via Raylib. Discouraged, potentially slow, and not HiDPI-compatible. Use `FreetypeTypeface` instead.
495 version (Have_raylib_d)
496 class RaylibTypeface : Typeface {
497 
498     import raylib;
499 
500     public {
501 
502         /// Character spacing, as a fraction of the font size.
503         float spacing = 0.1;
504 
505         /// Line height relative to font height.
506         float relativeLineHeight = 1.4;
507 
508         /// Scale to apply for the typeface.
509         float scale = 1.0;
510 
511     }
512 
513     private {
514 
515         /// Underlying Raylib font.
516         Font _font;
517 
518         /// If true, this is the default font, and has to be available ahead of time.
519         ///
520         /// If the typeface is requested before Raylib is loaded (...and it is...), the default font won't be available,
521         /// so we must use late loading.
522         bool _isDefault;
523 
524         /// If true, this typeface has been loaded using this class, making the class responsible for freeing the font.
525         bool _isOwner;
526 
527     }
528 
529     /// Object holding the default typeface.
530     static RaylibTypeface defaultTypeface() @trusted {
531 
532         static RaylibTypeface typeface;
533 
534         // Load the typeface
535         if (!typeface) {
536             typeface = new RaylibTypeface(GetFontDefault);
537             typeface._isDefault = true;
538             typeface.scale = 2.0;
539         }
540 
541         return typeface;
542 
543     }
544 
545     /// Load a Raylib font.
546     this(Font font) {
547 
548         this._font = font;
549 
550     }
551 
552     /// Load a Raylib font from file.
553     deprecated("Raylib font rendering is inefficient and lacks scaling. Use FreetypeTypeface instead")
554     this(string filename, int size) @trusted {
555 
556         this._font = LoadFontEx(filename.toStringz, size, null, 0);
557         this.isOwner = true;
558 
559     }
560 
561     ~this() @trusted {
562 
563         if (isOwner) {
564 
565             UnloadFont(_font);
566 
567         }
568 
569     }
570 
571     Font font() @trusted {
572 
573         if (_isDefault)
574             return _font = GetFontDefault;
575         else
576             return _font;
577 
578     }
579 
580     const(Font) font() const @trusted {
581 
582         if (_isDefault)
583             return GetFontDefault;
584         else
585             return _font;
586 
587     }
588 
589     bool isOwner() const => _isOwner;
590     bool isOwner(bool value) @system => _isOwner = value;
591 
592     /// List glyphs  in the typeface.
593     long glyphCount() const {
594 
595         return font.glyphCount;
596 
597     }
598 
599     /// Get initial pen position.
600     Vector2 penPosition() const {
601 
602         return Vector2(0, 0);
603 
604     }
605 
606     /// Get font height in pixels.
607     int fontHeight() const {
608 
609         return cast(int) (font.baseSize * scale);
610 
611     }
612 
613     /// Get line height in pixels.
614     int lineHeight() const {
615 
616         return cast(int) (fontHeight * relativeLineHeight);
617 
618     }
619 
620     /// Changing DPI at runtime is not supported for Raylib typefaces.
621     Vector2 dpi(Vector2 dpi) {
622 
623         // Not supported for Raylib typefaces.
624         return Vector2(96, 96);
625 
626     }
627 
628     Vector2 dpi() const {
629 
630         return Vector2(96, 96);
631 
632     }
633 
634     /// Get advance vector for the given glyph.
635     Vector2 advance(dchar codepoint) const @trusted {
636 
637         const glyph = GetGlyphInfo(cast() font, codepoint);
638         const spacing = fontHeight * this.spacing;
639         const baseAdvanceX = glyph.advanceX
640             ? glyph.advanceX
641             : glyph.offsetX + glyph.image.width;
642         const advanceX = baseAdvanceX * scale;
643 
644         return Vector2(advanceX + spacing, 0);
645 
646     }
647 
648     /// Draw a line of text
649     /// Note: This API is unstable and might change over time.
650     void drawLine(ref .Image target, ref Vector2 penPosition, string text, Color tint) const @trusted {
651 
652         // Note: `DrawTextEx` doesn't scale `spacing`, but `ImageDrawTextEx` DOES. The image is first drawn at base size
653         //       and *then* scaled.
654         const spacing = font.baseSize * this.spacing;
655 
656         // We trust Raylib will not mutate the font
657         // Raylib is single-threaded, so it shouldn't cause much harm anyway...
658         auto font = cast() this.font;
659 
660         // Make a Raylib-compatible wrapper for image data
661         auto result = target.toRaylib;
662 
663         ImageDrawTextEx(&result, font, text.toStringz, penPosition, fontHeight, spacing, tint);
664 
665     }
666 
667 }
668 
669 version (BindFT_Dynamic)
670 shared static this() @system {
671 
672     // Ignore if freetype was already loaded
673     if (isFreeTypeLoaded) return;
674 
675     // Load freetype
676     FTSupport ret = loadFreeType();
677 
678     // Check version
679     if (ret != ftSupport) {
680 
681         if (ret == FTSupport.noLibrary) {
682 
683             assert(false, "freetype2 failed to load");
684 
685         }
686         else if (FTSupport.badLibrary) {
687 
688             assert(false, format!"found freetype2 is of incompatible version %s (needed %s)"(ret, ftSupport));
689 
690         }
691 
692     }
693 
694 }
695 
696 /// Get the thread-local freetype reference.
697 FT_Library freetype() @trusted {
698 
699     static FT_Library library;
700 
701     // Load the library
702     if (library != library.init) return library;
703 
704     if (auto error = FT_Init_FreeType(&library)) {
705 
706         assert(false, format!"Failed to load freetype2: %s"(error));
707 
708     }
709 
710     return library;
711 
712 }