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 }