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 xml = undead.xml; 13 import std.utf : toUTF32, toUTF8; 14 import std.conv : to; 15 import std.algorithm : countUntil; 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 23 protected dchar[] _text; ///The text to be displayed 24 public CharacterFormattingInfo!BitmapType formatting; ///The formatting of this text block 25 public TextTempl!BitmapType next; ///The next piece of formatted text block 26 public int frontTab; ///Space before the text chunk in pixels. Can be negative. 27 public BitmapType icon; ///Icon inserted in front of the text chunk. 28 public byte iconOffsetX; ///X offset of the icon if any 29 public byte iconOffsetY; ///Y offset of the icon if any 30 public byte iconSpacing; ///Spacing after the icon if any 31 /** 32 * Creates a unit of formatted text from the supplied data. 33 */ 34 public this(dstring text, CharacterFormattingInfo!BitmapType formatting, TextTempl!BitmapType next = null, int frontTab = 0, 35 BitmapType icon = null) @safe pure nothrow { 36 this.text = text; 37 this.formatting = formatting; 38 this.next = next; 39 this.frontTab = frontTab; 40 this.icon = icon; 41 } 42 ///Copy CTOR 43 public this(TextTempl!BitmapType orig) @safe pure nothrow { 44 this.text = orig.text.dup; 45 this.formatting = orig.formatting; 46 if(orig.next) 47 this.next = new TextTempl!BitmapType(orig.next); 48 this.frontTab = orig.frontTab; 49 this.icon = orig.icon; 50 this.iconOffsetX = orig.iconOffsetX; 51 this.iconOffsetY = orig.iconOffsetY; 52 this.iconSpacing = orig.iconSpacing; 53 } 54 /** 55 * Returns the text as a 32bit string without the formatting. 56 */ 57 public dstring toDString() @safe pure nothrow { 58 if (next) 59 return text ~ next.toDString(); 60 else 61 return text; 62 } 63 /** 64 * Indexing to refer to child items. 65 * Returns null if the given element isn't available. 66 */ 67 public Text!BitmapType opIndex(size_t index) @safe pure nothrow { 68 if (index) { 69 if (next) { 70 return next[index - 1]; 71 } else { 72 return null; 73 } 74 } else { 75 return this; 76 } 77 } 78 /** 79 * Returns the character lenght. 80 */ 81 public @property size_t charLength() @safe pure nothrow @nogc const { 82 if (next) return _text.length + next.charLength; 83 else return _text.length; 84 } 85 /** 86 * Removes the character at the given position. 87 * Returns the removed character if within bound, or dchar.init if not. 88 */ 89 public dchar removeChar(size_t pos) @safe pure { 90 import std.algorithm.mutation : remove; 91 void _removeChar() @safe pure { 92 if(pos == 0) { 93 _text = _text[1..$]; 94 } else if(pos == _text.length - 1) { 95 if(_text.length) _text.length = _text.length - 1; 96 } else { 97 _text = _text[0..pos] ~ _text[(pos + 1)..$]; 98 } 99 } 100 if(pos < _text.length) { 101 const dchar result = _text[pos]; 102 _removeChar();/+text = text.remove(pos);+/ 103 return result; 104 } else if(next) { 105 return next.removeChar(pos - _text.length); 106 } else return dchar.init; 107 } 108 /** 109 * Removes a range of characters described by the begin and end position. 110 */ 111 public void removeChar(size_t begin, size_t end) @safe pure { 112 for (size_t i = begin ; i < end ; i++) { 113 removeChar(begin); 114 } 115 } 116 /** 117 * Inserts a given character at the given position. 118 * Return the inserted character if within bound, or dchar.init if position points to a place where it 119 * cannot be inserted easily. 120 */ 121 public dchar insertChar(size_t pos, dchar c) @trusted pure { 122 import std.array : insertInPlace; 123 if(pos <= _text.length) { 124 _text.insertInPlace(pos, c); 125 return c; 126 } else if(next) { 127 return next.insertChar(pos - _text.length, c); 128 } else return dchar.init; 129 } 130 /** 131 * Overwrites a character at the given position. 132 * Returns the original character if within bound, or or dchar.init if position points to a place where it 133 * cannot be inserted easily. 134 */ 135 public dchar overwriteChar(size_t pos, dchar c) @safe pure { 136 if(pos < _text.length) { 137 const dchar orig = _text[pos]; 138 _text[pos] = c; 139 return orig; 140 } else if(next) { 141 return next.overwriteChar(pos - _text.length, c); 142 } else if (pos == _text.length) { 143 _text ~= c; 144 return c; 145 } else return dchar.init; 146 } 147 /** 148 * Returns a character from the given position. 149 */ 150 public dchar getChar(size_t pos) @safe pure { 151 if(pos < _text.length) { 152 return _text[pos]; 153 } else if(next) { 154 return next.getChar(pos - _text.length); 155 } else return dchar.init; 156 } 157 /** 158 * Returns the width of the current text block in pixels. 159 */ 160 public int getBlockWidth() @safe pure nothrow { 161 auto f = font; 162 dchar prev; 163 int localWidth = frontTab; 164 foreach (c; _text) { 165 localWidth += f.chars(c).xadvance + formatting.getKerning(prev, c); 166 prev = c; 167 } 168 if(icon) localWidth += iconOffsetX + iconSpacing; 169 return localWidth; 170 } 171 /** 172 * Returns the width of the text chain in pixels. 173 */ 174 public int getWidth() @safe pure nothrow { 175 auto f = font; 176 dchar prev; 177 int localWidth = frontTab; 178 foreach (c; _text) { 179 localWidth += f.chars(c).xadvance + formatting.getKerning(prev, c); 180 prev = c; 181 } 182 if(icon) localWidth += iconOffsetX + iconSpacing; 183 if(next) return localWidth + next.getWidth(); 184 else return localWidth; 185 } 186 /** 187 * Returns the width of a slice of the text chain in pixels. 188 */ 189 public int getWidth(sizediff_t begin, sizediff_t end) @safe pure { 190 if(end > _text.length && next is null) 191 throw new Exception("Text boundary have been broken!"); 192 int localWidth; 193 if (!begin) { 194 localWidth += frontTab; 195 if (icon) 196 localWidth += iconOffsetX + iconSpacing; 197 } 198 if (begin < _text.length) { 199 auto f = font; 200 dchar prev; 201 foreach (c; _text[begin..end]) { 202 localWidth += f.chars(c).xadvance + formatting.getKerning(prev, c); 203 prev = c; 204 } 205 } 206 begin -= _text.length; 207 end -= _text.length; 208 if (begin < 0) begin = 0; 209 if (next && end > 0) return localWidth + next.getWidth(begin, end); 210 else return localWidth; 211 } 212 /** 213 * Returns the number of characters fully offset by the amount of pixel. 214 */ 215 public int offsetAmount(int pixel) @safe pure nothrow { 216 int chars; 217 dchar prev; 218 while (chars < _text.length && pixel - font.chars(_text[chars]).xadvance + formatting.getKerning(prev, _text[chars]) > 0) { 219 pixel -= font.chars(_text[chars]).xadvance + formatting.getKerning(prev, _text[chars]); 220 prev = _text[chars]; 221 chars++; 222 } 223 if (chars == _text.length && pixel > 0 && next) 224 chars += next.offsetAmount(pixel); 225 return chars; 226 } 227 /** 228 * Returns the used font type. 229 */ 230 public Fontset!BitmapType font() @safe @nogc pure nothrow { 231 return formatting.font; 232 } 233 ///Text accessor 234 public @property dstring text() @safe pure nothrow { 235 return _text.idup; 236 } 237 ///Text accessor 238 public @property dstring text(dstring val) @safe pure nothrow { 239 _text = val.dup; 240 return val; 241 } 242 } 243 alias Text = TextTempl!Bitmap8Bit; 244 /** 245 * Parses text from XML/ETML 246 * 247 * See "ETML.md" for info. 248 * 249 * Constraints: 250 * * Due to the poor documentation of the replacement XML libraries, I have to use Phobos's own and outdated library. 251 * * <text> chunks are mandatory with ID. 252 * * Currently line formatting (understrike, etc.) is not supported, and every line uses default formatting. 253 */ 254 public class TextParserTempl(BitmapType = Bitmap8Bit) 255 /+if((typeof(BitmapType) is Bitmap8Bit || typeof(BitmapType) is Bitmap16Bit || 256 typeof(BitmapType) is Bitmap32Bit) && (typeof(StringType) is string || typeof(StringType) is dstring))+/ { 257 //private Text!BitmapType current; ///Current element 258 //private Text!BitmapType _output; ///Root/output element 259 //private Text!BitmapType[] stack; ///Mostly to refer back to previous elements 260 public TextTempl!BitmapType[string] output; ///All texts found within the ETML file 261 private TextTempl!BitmapType chunkRoot; 262 private TextTempl!BitmapType currTextBlock; 263 private CharacterFormattingInfo!BitmapType currFrmt;///currently parsed formatting 264 public CharacterFormattingInfo!BitmapType[] chrFrmt;///All character format, that has been parsed so far 265 public Fontset!BitmapType[] fontStack; ///Fonttype formatting stack. Used for referring back to previous font types. 266 public uint[] colorStack; 267 private CharacterFormattingInfo!BitmapType defFrmt;///Default character formatting. Must be set before parsing 268 public Fontset!BitmapType[string] fontsets;///Fontset name association table 269 public BitmapType[string] icons; ///Icon name association table 270 private string _input; ///The source XML/ETML document 271 ///Constructor with no arguments 272 public this() @safe pure nothrow { 273 reset(); 274 } 275 ///Creates a new instance with a select string input. 276 public this(string _input) @safe pure nothrow { 277 this._input = _input; 278 __ctor(); 279 } 280 ///Resets the output, but keeps the public parameters. 281 ///Input must be set to default or to new target separately. 282 public void reset() @safe pure nothrow { 283 current = new Text!BitmapType("", null); 284 //_output = current; 285 stack ~= current; 286 } 287 ///Sets the default formatting 288 public CharacterFormattingInfo!BitmapType defaultFormatting(CharacterFormattingInfo!BitmapType val) @property @safe 289 pure nothrow { 290 if (stack[0].formatting is null) { 291 stack[0].formatting = val; 292 } 293 defFrmt = val; 294 return defFrmt; 295 } 296 ///Gets the default formatting 297 public CharacterFormattingInfo!BitmapType defaultFormatting()@property @safe pure nothrow @nogc { 298 return defFrmt; 299 } 300 ///Returns the root/output element 301 /+public Text!BitmapType output() @property @safe @nogc pure nothrow { 302 return stack[0]; 303 }+/ 304 ///Sets/gets the input 305 public string input() @property @safe @nogc pure nothrow inout { 306 return _input; 307 } 308 /** 309 * Parses the formatted text, then returns the output. 310 */ 311 public void parse() @trusted { 312 xml.check(_input); 313 string currTextChunkID; 314 currFrmt = new CharacterFormattingInfo!BitmapType(defFrmt); 315 fontStack ~= currFrmt.fontType; 316 colorStack ~= currFrmt.color; 317 auto parser = new xml.DocumentParser(_input); 318 parser.onStartTag["text"] = (xml.ElementParser parser) { 319 currTextChunkID = parser.tag.attr["id"]; 320 chunkRoot = new TextTempl!BitmapType(); 321 currTextBlock = chunkRoot; 322 323 output[currTextChunkID] = chunkRoot; 324 parseRecursively(parser); 325 }; 326 parser.parse; 327 } 328 //block for parsing 329 private void onText(string s) { 330 currTextBlock.text ~= toUTF32(s); 331 } 332 private void onEndTag_br(xml.Element e) { 333 currTextBlock.text ~= FormattingCharacters.newLine; 334 } 335 private void onStartTag_p(xml.ElementParser parser) { 336 currTextBlock.text ~= FormattingCharacters.newParagraph; 337 if(parser.tag.attr.length) { 338 currFrmt.paragraphSpace = parser.tag.attr.get("paragraphSpace", defFrmt.paragraphSpace); 339 currFrmt.rowHeight = parser.tag.attr.get("rowHeight", defFrmt.paragraphSpace); 340 createNextTextChunk(); 341 } 342 parseRecursively (parser); 343 } 344 private void onStartTag_u(xml.ElementParser parser) { 345 currFrmt.formatFlags |= FormattingFlags.underline; 346 createNextTextChunk(); 347 parseRecursively (parser); 348 } 349 private void onStartTag_s(xml.ElementParser parser) { 350 currFrmt.formatFlags |= FormattingFlags.strikeThrough; 351 createNextTextChunk(); 352 parseRecursively (parser); 353 } 354 private void onStartTag_o(xml.ElementParser parser) { 355 currFrmt.formatFlags |= FormattingFlags.overline; 356 createNextTextChunk(); 357 parseRecursively (parser); 358 } 359 private void onStartTag_i(xml.ElementParser parser) { 360 const string amount = parser.tag.attr.get("amount", "0"); 361 currFrmt.formatFlags &= !FormattingFlags.forceItalicsMask; 362 switch(amount) { 363 case "1/2": 364 currFrmt.formatFlags |= FormattingFlags.forceItalics1per2; 365 break; 366 case "1/3": 367 currFrmt.formatFlags |= FormattingFlags.forceItalics1per3; 368 break; 369 case "1/4": 370 currFrmt.formatFlags |= FormattingFlags.forceItalics1per4; 371 break; 372 default: 373 currFrmt.formatFlags |= defFrmt.formatFlags & FormattingFlags.forceItalicsMask; 374 break; 375 } 376 createNextTextChunk(); 377 parseRecursively (parser); 378 } 379 private void onStartTag_font(xml.ElementParser parser) { 380 const string fontName = parser.tag.attr.get("type", ""); 381 const uint color = to!uint(parser.tag.attr.get(color, colorStack[$-1])); 382 Fontset!BitmapType fontType; 383 if(fontType.length) { 384 fontType = fontsets.get(fontName, null); 385 if (fontType is null) 386 throw new XMLTextParsingException("Unknown fonttype!"); 387 } else { 388 fontType = fontStack[$-1]; 389 } 390 fontStack ~= fontType; 391 colorStack ~= color; 392 currFrmt.color = cast(ubyte)color; 393 currFrmt.fontType = fontType; 394 createNextTextChunk(); 395 parseRecursively (parser); 396 } 397 private void onEndTag_u(xml.Element e) { 398 currFrmt.formatFlags &= !FormattingFlags.underline; 399 createNextTextChunk(); 400 } 401 private void onEndTag_s(xml.Element e) { 402 currFrmt.formatFlags &= !FormattingFlags.strikeThrough; 403 createNextTextChunk(); 404 } 405 private void onEndTag_o(xml.Element e) { 406 currFrmt.formatFlags &= !FormattingFlags.overline; 407 createNextTextChunk(); 408 } 409 private void onEndTag_i(xml.Element e) { 410 currFrmt.formatFlags &= !FormattingFlags.forceItalicsMask; 411 createNextTextChunk(); 412 } 413 private void onEndTag_font(xml.Element e) { 414 currFrmt.color = cast(ubyte)colorStack[$-2]; 415 currFrmt.fontType = fontStack[$-2]; 416 colorStack.length--; 417 fontStack.length--; 418 createNextTextChunk(); 419 } 420 private void onStartTag_frontTab(xml.ElementParser parser) { 421 parseRecursively (parser); 422 } 423 private void onStartTag_image(xml.ElementParser parser) { 424 parseRecursively (parser); 425 } 426 ///Creates the next text chunk if needed, and also checks the formatting stack. 427 ///Called by the appropriate functions. 428 ///Also handles the character formatting stack 429 private void createNextTextChunk() { 430 if(currTextBlock.text.length || currTextBlock.icon !is null){ 431 currTextBlock = new TextTempl!BitmapType(null, null, currTextBlock); 432 const ptrdiff_t frmtPos = countUntil(chrFrmt, currFrmt); 433 if (frmtPos == -1){ 434 chrFrmt ~= new CharacterFormattingInfo(currFrmt); 435 currTextBlock.formatting = chrFrmt[$-1]; 436 } else { 437 currTextBlock.formatting = chrFrmt[frmtPos]; 438 } 439 } 440 } 441 ///Called for every instance when there might be additional tags to be parsed. 442 private void parseRecursively(xml.ElementParser parser) { 443 parser.onText = &onText; 444 parser.onEndTag["br"] = &onEndTag_br; 445 parser.onStartTag["p"] = &onStartTag_p; 446 parser.parse; 447 } 448 } 449 450 public class XMLTextParsingException : Exception { 451 @nogc @safe pure nothrow this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable nextInChain = null) { 452 super(msg, file, line, nextInChain); 453 } 454 455 @nogc @safe pure nothrow this(string msg, Throwable nextInChain, string file = __FILE__, size_t line = __LINE__) { 456 super(msg, file, line, nextInChain); 457 } 458 }