1 /**
2     Text layout engine on top of `printed` graphics package.
3 
4     Copyright: © 2021 Arne Ludwig <arne.ludwig@posteo.de>
5     License: Subject to the terms of the MIT license, as written in the
6              included LICENSE file.
7     Authors: Arne Ludwig <arne.ludwig@posteo.de>
8 */
9 module printed.text;
10 
11 import printed.canvas;
12 import printed.font;
13 
14 
15 enum TextAlign
16 {
17     /// Align to the start edge of the text (left side in left-to-right text, right side in right-to-left text).
18     /// This is the default.
19     start,
20 
21     /// Align to the end edge of the text (right side in left-to-right text, left side in right-to-left text).
22     end,
23 
24     /// Align to the left.
25     left,
26 
27     /// Align to the right.
28     right,
29 
30     /// Align to the center.
31     center,
32 
33     /// The line contents are justified. Text should be spaced to line up its
34     /// left and right edges to the left and right edges of the line box,
35     /// except for the last line.
36     justify,
37 }
38 
39 
40 interface ITextLayouter
41 {
42     /// Size of the area were the text is laid out. The text is automatically
43     /// broken across pages. Both dimensions may be infinite which suppresses
44     /// line breaks in case of the width and page breaks in case of the
45     /// height.
46     void textWidth(float x);
47     /// ditto
48     float textWidth();
49     /// ditto
50     void textHeight(float y);
51     /// ditto
52     float textHeight();
53 
54     /// Current line spacing relative to the current font size. This applies
55     /// to the whole paragraph when it ends either implicitly via `layout()`
56     /// or explicitly via `newParagraph()`.
57     void lineSpacing(float factor);
58     /// ditto
59     float lineSpacing();
60 
61     /// End the current and start a new paragraph. The first paragraph is
62     /// created implicitly. This is equivalent to writing two newline
63     /// characters:
64     ///
65     ///     writeln();
66     ///     writeln();
67     ///
68     /// Two or more newline characters separate paragraphs.
69     void endParagraph();
70 
71     /// Continue layout on a new page.
72     void endPage();
73 
74     /// Current font size. This applies to all subsequent text until it is
75     /// changed.
76     void fontSize(float mm);
77     /// ditto
78     float fontSize();
79 
80     /// Current text color. This applies to all subsequent text until it is
81     /// changed.
82     void color(Brush color);
83     /// ditto
84     Brush color();
85 
86     /// Current font size. This applies to all subsequent text until it is
87     /// changed.
88     void fontFace(string face);
89     /// ditto
90     string fontFace();
91 
92     /// Current font weight. This applies to all subsequent text until it is
93     /// changed.
94     void fontWeight(FontWeight weight);
95     /// ditto
96     FontWeight fontWeight();
97 
98     /// Current font style. This applies to all subsequent text until it is
99     /// changed.
100     void fontStyle(FontStyle style);
101     /// ditto
102     FontStyle fontStyle();
103 
104     /// Current text alignment strategy. This applies to the whole paragraph
105     /// when it ends either implicitly via `layout()` or explicitly via
106     /// `newParagraph()`.
107     void textAlign(TextAlign align_);
108     /// ditto
109     TextAlign textAlign();
110 
111     /// Save the current state, i.e. `fontSize`, `color`, `fontFace`,
112     /// `fontWeight`, `fontStyle` and `textAlign`.
113     void save();
114 
115     /// Restore the last saved state.
116     ///
117     /// See_also: `save()`
118     void restore();
119 
120     /// Append `text` to the current page.
121     ///
122     /// The alias `put` makes `ITextLayouter` act as an output range for
123     /// strings.
124     void write(string text);
125     /// ditto
126     alias put = write;
127 
128     /// Compute the text layout with the current text and text dimensions and
129     /// return the pages which can be rendered by an `IRenderingContext2D`.
130     ILayoutBlock[] layout(ILocale locale = null);
131 
132     /// Clear all text buffers.
133     void clear();
134 }
135 
136 
137 /// Wrap `yield` with `save()` and `restore()`.
138 void group(ITextLayouter layouter, void delegate() yield)
139 in (layouter !is null)
140 {
141     layouter.save();
142     yield();
143     layouter.restore();
144 }
145 
146 ///
147 unittest
148 {
149     void varyingTextSize(ITextLayouter layouter) {
150         layouter.write("Small text. ");
151         layouter.group({
152             layouter.fontSize = 12f;
153 
154             layouter.write("Large text. ");
155         });
156         layouter.write("Small again.");
157     }
158 }
159 
160 
161 /// Defines how a text is split into words and words into syllables and
162 /// provides the hyphen character used for hyphenation.
163 interface ILocale
164 {
165     import std.array : join;
166 
167     /// Returns slices of `text` that represent individual words interleaved
168     /// with the content that separates them.
169     string[] breakWords(string text)
170     out (words; words.isPartitionOf(text));
171 
172     /// Returns slices of `word` that represent its syllables.
173     string[] breakSyllables(string word)
174     out (syllables; syllables.isPartitionOf(word));
175 
176     /// Character used for hyphenation.
177     wchar hypen();
178 }
179 
180 
181 /// Represents a laid out text of a single page that can be rendered using a
182 /// `IRenderingContext2D`.
183 interface ILayoutBlock
184 {
185     /// Render this layout block with `renderer` and an offset of given by
186     /// `x` and `y`.
187     void renderWith(IRenderingContext2D renderer, float x = 0f, float y = 0f);
188 
189     /// Return the text size that was in effect when this layout was created.
190     float textWidth();
191     /// ditto
192     float textHeight();
193 
194     /// Return the size of the area that will be filled with content when
195     /// rendering.
196     float filledWidth();
197     /// ditto
198     float filledHeight();
199 }
200 
201 
202 /// Returns true iff `parts` is a partition of `whole`. This is equivalent
203 /// to `join(parts) == whole` but more efficient.
204 bool isPartitionOf(T)(const T[][] parts, const T[] whole) pure nothrow @safe @nogc
205 {
206     alias haveSameElements = {
207         size_t i;
208 
209         foreach (part; parts)
210             foreach (element; part)
211                 if (element != whole[i++])
212                     return false;
213 
214         return true;
215     };
216     alias adjancentInMemory = (lhs, rhs) @trusted {
217         return &lhs[0] + lhs.length == &rhs[0];
218     };
219     alias countElementsInParts = {
220         size_t elementsInParts;
221         foreach (part; parts)
222             elementsInParts += part.length;
223 
224         return elementsInParts;
225     };
226 
227     if (parts.length == 0)
228         return false;
229     else if (whole.length == 0)
230         return parts.length == 1 && parts[0].length == 0;
231     else if (&parts[0][0] != &whole[0])
232         return haveSameElements();
233     else if (countElementsInParts() != whole.length)
234         return false;
235 
236     foreach (i, part; parts[1 .. $])
237         if (!adjancentInMemory(parts[i], part))
238             return haveSameElements();
239 
240     return true;
241 }
242 
243 ///
244 unittest
245 {
246     enum text = "lorem ipsum dolor sit amet";
247     enum words = [
248         text[0 .. 5],
249         text[5 .. 6],
250         text[6 .. 11],
251         text[11 .. 12],
252         text[12 .. 17],
253         text[17 .. 18],
254         text[18 .. 21],
255         text[21 .. 22],
256         text[22 .. 26],
257     ];
258 
259     assert(words.isPartitionOf(text));
260 }
261 
262 unittest
263 {
264     auto text = "lorem ipsum dolor sit amet";
265     auto words = [
266         text[0 .. 5],
267         text[5 .. 6],
268         text[6 .. 11],
269         text[11 .. 12],
270         text[12 .. 17],
271         text[17 .. 18],
272         text[18 .. 21],
273         text[21 .. 22],
274         text[22 .. 26],
275     ];
276 
277     assert(words.isPartitionOf(text));
278 }
279 
280 unittest
281 {
282     auto text = "lorem ipsum dolor sit amet";
283     auto words = [text[]];
284 
285     assert(words.isPartitionOf(text));
286 }
287 
288 unittest
289 {
290     auto text = "";
291     auto words = [text[]];
292 
293     assert(words.isPartitionOf(text));
294 }
295 
296 unittest
297 {
298     auto text = "lorem ipsum dolor sit amet";
299     auto words = [text[0 .. 5]];
300 
301     assert(!words.isPartitionOf(text));
302 }
303 
304 unittest
305 {
306     auto text = "lorem ipsum dolor sit amet";
307     string[] words = [];
308 
309     assert(!words.isPartitionOf(text));
310 }