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 }