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 }