1 /* 2 * Copyright (C) 2015-2019, by Laszlo Szeremi under the Boost license. 3 * 4 * Pixel Perfect Engine, concrete.text module 5 */ 6 7 module pixelperfectengine.graphics.text; 8 9 public import pixelperfectengine.graphics.fontsets; 10 public import pixelperfectengine.graphics.bitmap; 11 12 import std.utf : toUTF32, toUTF8; 13 import std.conv : to; 14 import std.algorithm : countUntil; 15 import std.typecons : BitFlags; 16 17 /** 18 * Implements a formatted text chunk, that can be serialized in XML form. 19 * Has a linked list structure to easily link multiple chunks after each other. 20 */ 21 public class TextTempl(BitmapType = Bitmap8Bit) { 22 enum Flags : ubyte { 23 newLine = 1 << 0, 24 newParagraph = 1 << 1, 25 } 26 protected dchar[] _text; ///The text to be displayed 27 public CharacterFormattingInfo!BitmapType formatting; ///The formatting of this text block 28 public TextTempl!BitmapType next; ///The next piece of formatted text block 29 public int frontTab; ///Space before the text chunk in pixels. Can be negative. 30 public BitmapType icon; ///Icon inserted in front of the text chunk. 31 public byte iconOffsetX; ///X offset of the icon if any 32 public byte iconOffsetY; ///Y offset of the icon if any 33 public byte iconSpacing; ///Spacing after the icon if any 34 //public BitFlags!Flags flags; 35 public ubyte flags; ///Text flags 36 /** 37 * Creates a unit of formatted text from the supplied data. 38 */ 39 public this(dstring text, CharacterFormattingInfo!BitmapType formatting, TextTempl!BitmapType next = null, 40 int frontTab = 0, BitmapType icon = null) @safe pure nothrow { 41 this.text = text; 42 this.formatting = formatting; 43 this.next = next; 44 this.frontTab = frontTab; 45 this.icon = icon; 46 } 47 ///Copy CTOR 48 public this(TextTempl!BitmapType orig) @safe pure nothrow { 49 this.text = orig.text.dup; 50 this.formatting = orig.formatting; 51 if(orig.next) 52 this.next = new TextTempl!BitmapType(orig.next); 53 this.frontTab = orig.frontTab; 54 this.icon = orig.icon; 55 this.iconOffsetX = orig.iconOffsetX; 56 this.iconOffsetY = orig.iconOffsetY; 57 this.iconSpacing = orig.iconSpacing; 58 } 59 /** 60 * Returns the text as a 32bit string without the formatting. 61 */ 62 public dstring toDString() @safe pure nothrow { 63 if (next) 64 return text ~ next.toDString(); 65 else 66 return text; 67 } 68 /** 69 * Indexing to refer to child items. 70 * Returns null if the given element isn't available. 71 */ 72 public TextTempl!BitmapType opIndex(size_t index) @safe pure nothrow { 73 if (index) { 74 if (next) { 75 return next[index - 1]; 76 } else { 77 return null; 78 } 79 } else { 80 return this; 81 } 82 } 83 /** 84 * Returns the character lenght. 85 */ 86 public @property size_t charLength() @safe pure nothrow @nogc const { 87 if (next) return _text.length + next.charLength; 88 else return _text.length; 89 } 90 /** 91 * Removes the character at the given position. 92 * Returns the removed character if within bound, or dchar.init if not. 93 */ 94 public dchar removeChar(size_t pos) @safe pure { 95 import std.algorithm.mutation : remove; 96 void _removeChar() @safe pure { 97 if(pos == 0) { 98 _text = _text[1..$]; 99 } else if(pos == _text.length - 1) { 100 if(_text.length) _text.length = _text.length - 1; 101 } else { 102 _text = _text[0..pos] ~ _text[(pos + 1)..$]; 103 } 104 } 105 if(pos < _text.length) { 106 const dchar result = _text[pos]; 107 _removeChar();/+text = text.remove(pos);+/ 108 return result; 109 } else if(next) { 110 return next.removeChar(pos - _text.length); 111 } else return dchar.init; 112 } 113 /** 114 * Removes a range of characters described by the begin and end position. 115 */ 116 public void removeChar(size_t begin, size_t end) @safe pure { 117 for (size_t i = begin ; i < end ; i++) { 118 removeChar(begin); 119 } 120 } 121 /** 122 * Inserts a given character at the given position. 123 * Return the inserted character if within bound, or dchar.init if position points to a place where it 124 * cannot be inserted easily. 125 */ 126 public dchar insertChar(size_t pos, dchar c) @trusted pure { 127 import std.array : insertInPlace; 128 if(pos <= _text.length) { 129 _text.insertInPlace(pos, c); 130 return c; 131 } else if(next) { 132 return next.insertChar(pos - _text.length, c); 133 } else return dchar.init; 134 } 135 /** 136 * Overwrites a character at the given position. 137 * Returns the original character if within bound, or or dchar.init if position points to a place where it 138 * cannot be inserted easily. 139 */ 140 public dchar overwriteChar(size_t pos, dchar c) @safe pure { 141 if(pos < _text.length) { 142 const dchar orig = _text[pos]; 143 _text[pos] = c; 144 return orig; 145 } else if(next) { 146 return next.overwriteChar(pos - _text.length, c); 147 } else if (pos == _text.length) { 148 _text ~= c; 149 return c; 150 } else return dchar.init; 151 } 152 /** 153 * Returns a character from the given position. 154 */ 155 public dchar getChar(size_t pos) @safe pure { 156 if(pos < _text.length) { 157 return _text[pos]; 158 } else if(next) { 159 return next.getChar(pos - _text.length); 160 } else return dchar.init; 161 } 162 /** 163 * Returns the width of the current text block in pixels. 164 */ 165 public int getBlockWidth() @safe pure nothrow { 166 auto f = font; 167 dchar prev; 168 int localWidth = frontTab; 169 foreach (c; _text) { 170 localWidth += f.chars(c).xadvance + formatting.getKerning(prev, c); 171 prev = c; 172 } 173 if(icon) localWidth += iconOffsetX + iconSpacing; 174 return localWidth; 175 } 176 /** 177 * Returns the width of the text chain in pixels. 178 */ 179 public int getWidth() @safe pure nothrow { 180 auto f = font; 181 dchar prev; 182 int localWidth = frontTab; 183 foreach (c; _text) { 184 localWidth += f.chars(c).xadvance + formatting.getKerning(prev, c); 185 prev = c; 186 } 187 if(icon) localWidth += iconOffsetX + iconSpacing; 188 if(next) return localWidth + next.getWidth(); 189 else return localWidth; 190 } 191 /** 192 * Returns the width of a slice of the text chain in pixels. 193 */ 194 public int getWidth(sizediff_t begin, sizediff_t end) @safe pure { 195 if(end > _text.length && next is null) 196 throw new Exception("Text boundary have been broken!"); 197 int localWidth; 198 if (!begin) { 199 localWidth += frontTab; 200 if (icon) 201 localWidth += iconOffsetX + iconSpacing; 202 } 203 if (begin < _text.length) { 204 auto f = font; 205 dchar prev; 206 foreach (c; _text[begin..end]) { 207 localWidth += f.chars(c).xadvance + formatting.getKerning(prev, c); 208 prev = c; 209 } 210 } 211 begin -= _text.length; 212 end -= _text.length; 213 if (begin < 0) begin = 0; 214 if (next && end > 0) return localWidth + next.getWidth(begin, end); 215 else return localWidth; 216 } 217 public int getHeight() @safe pure { 218 return formatting.rowHeight; 219 } 220 /** 221 * Returns the number of characters fully offset by the amount of pixel. 222 */ 223 public int offsetAmount(int pixel) @safe pure nothrow { 224 int chars; 225 dchar prev; 226 while (chars < _text.length && pixel - font.chars(_text[chars]).xadvance + formatting.getKerning(prev, _text[chars]) > 0) { 227 pixel -= font.chars(_text[chars]).xadvance + formatting.getKerning(prev, _text[chars]); 228 prev = _text[chars]; 229 chars++; 230 } 231 if (chars == _text.length && pixel > 0 && next) 232 chars += next.offsetAmount(pixel); 233 return chars; 234 } 235 /** 236 * Returns the used font type. 237 */ 238 public Fontset!BitmapType font() @safe @nogc pure nothrow { 239 return formatting.font; 240 } 241 ///Text accessor 242 public @property dstring text() @safe pure nothrow { 243 return _text.idup; 244 } 245 ///Text accessor 246 public @property dstring text(dstring val) @safe pure nothrow { 247 _text = val.dup; 248 return val; 249 } 250 protected void addToEnd(TextTempl!(BitmapType) chunk) @safe pure nothrow { 251 if (next is null) { 252 next = chunk; 253 } else { 254 next.addToEnd(chunk); 255 } 256 } 257 /** 258 * Breaks this text object into multiple lines 259 * Params: 260 * width = The width of the text. 261 * Returns: An array of text objects, with each new element representing a new line. Each text objects might still 262 * have more subelements for formatting reasons. 263 */ 264 public TextTempl!(BitmapType)[] breakTextIntoMultipleLines(int width) { 265 TextTempl!BitmapType curr = this; 266 TextTempl!BitmapType currentLine = new Text(null, curr.formatting, null, curr.frontTab, curr.icon); 267 TextTempl!BitmapType currentChunk = currentLine; 268 dchar[] currentWord; 269 TextTempl!(BitmapType)[] result; 270 int currentWordLength, currentLineLength; 271 while (curr) { 272 foreach(size_t i, dchar ch ; curr._text) { 273 currentWordLength += curr.formatting.font.chars(ch).xadvance; 274 if (isWhiteSpaceMB(ch)){ 275 if (currentLineLength + currentWordLength <= width) { //Check if there's enough space in the line for the current word, if no, then start new word. 276 currentLineLength += currentWordLength; 277 currentChunk._text ~= currentWord ~ ch; 278 currentWord.length = 0; 279 currentWordLength = 0; 280 } else { 281 result ~= currentLine; 282 currentLine = new TextTempl!(BitmapType)(null, curr.formatting, null, 0, null); 283 currentLine._text ~= currentWord ~ ch; 284 currentChunk = currentLine; 285 currentWord.length = 0; 286 currentWordLength = 0; 287 currentLineLength = 0; 288 } 289 } else { 290 if (currentWordLength > width) { //Break word to avoid going out of the line 291 currentLine._text = currentWord.dup; 292 result ~= currentLine; 293 //result ~= new TextTempl!(BitmapType)(null, curr.formatting, null, 0, null); 294 currentWordLength = curr.formatting.font.chars(ch).xadvance; 295 currentLine = new TextTempl!(BitmapType)(null, curr.formatting, null, 0, null); 296 currentChunk = currentLine; 297 currentWord.length = 0; 298 currentWordLength = 0; 299 } 300 currentWord ~= ch; 301 } 302 } 303 if (currentWord.length) { //Flush any remaining words to current chunk. 304 if (currentLineLength + currentWordLength <= width) { //Check if there's enough space in the line for the current word, if no, then start new word. 305 currentLineLength += currentWordLength; 306 currentChunk._text ~= currentWord; 307 } else { 308 result ~= currentLine; 309 currentLine = new TextTempl!(BitmapType)(null, curr.formatting, null, 0, null); 310 currentLine._text ~= currentWord; 311 currentChunk = currentLine; 312 currentLineLength = 0; 313 } 314 currentWord.length = 0; 315 currentWordLength = 0; 316 } 317 //if (currentLine._text.length) { 318 //result ~= currentLine; 319 //currentLine = new TextTempl!(BitmapType)(null, curr.formatting, null, curr.frontTab, curr.icon); 320 //currentWord.length = 0; 321 //currentWordLength = 0; 322 //currentLineLength = 0; 323 //} 324 curr = curr.next; 325 if (curr) { 326 if ((curr.flags & Flags.newLine) || (curr.flags & Flags.newParagraph)) { //Force text breakage, put text chunk into the array. 327 result ~= currentLine; 328 currentLine = new TextTempl!(BitmapType)(null, curr.formatting, null, 0, curr.icon); 329 currentChunk = currentLine; 330 currentLineLength = 0; 331 currentWord.length = 0; 332 currentWordLength = 0; 333 } else { 334 currentChunk = new TextTempl!(BitmapType)(null, curr.formatting, null, 0, curr.icon); 335 currentLine.addToEnd(currentChunk); 336 } 337 } 338 } 339 if (!result.length) return [currentLine]; 340 return result; 341 } 342 } 343 alias Text = TextTempl!Bitmap8Bit; 344 //Text helper functions 345 ///Checks character `c` if it's a whitespace character that may break but is not an absolute break, then returns the 346 ///true if it is. 347 public bool isWhiteSpaceMB(dchar c) @safe pure nothrow { 348 import std.algorithm : countUntil; 349 static immutable dchar[] whitespaces = [0x0009, 0x0020, 0x1680, 0x2000, 0x2001, 0x2002, 0x2003, 0X2004, 0x2005, 350 0x2006, 0x2007, 0x2008, 0x2009, 0x200A, 0x205F, 0x3000, 0x180E, 0x200B, 0x200C, 0x200D]; 351 try { 352 return countUntil(whitespaces, c) != -1; 353 } catch (Exception e) { 354 return false; 355 } 356 }