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