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, 35 int frontTab = 0, 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;