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;