1 module pixelperfectengine.concrete.elements.listview;
2 
3 import pixelperfectengine.concrete.elements.base;
4 import pixelperfectengine.concrete.elements.scrollbar;
5 
6 import pixelperfectengine.system.etc : clamp, min, max;
7 
8 //import pixelperfectengine.system.input.types : TextInputFieldType;
9 
10 /**
11  * Defines a single item in the listview.
12  * Draws directly onto the canvas to avoit using multiple framebuffers.
13  * Can be inherited from, and that's when non-alphabetical ordering can be implemented.
14  */
15 public class ListViewItem {
16 	/**
17 	 * Defines a single field (cell) in a listview.
18 	 */
19 	public struct Field {
20 		private uint		flags;		///Stores various flags (constraints, etc.)
21 		public Text			text;		///Stores the text of this field if there's any.
22 		public ABitmap		bitmap;		///Custom bitmap, can be 32 bit if target enables it.
23 		static enum IS_EDITABLE		= 1 << 0;///Set if field is text editable.
24 		static enum INTEGER			= 1 << 1;///Set if field only accepts integer numbers.
25 		static enum NUMERIC			= 1 << 3;///Set if field only accepts numeric values (integer or floating-point).
26 		static enum POSITIVE_ONLY	= 1 << 4;///Set if field only accepts positive numeric values (integer or floating-point).
27 		
28 		/**
29 		 * Default constructor.
30 		 */
31 		this(Text text, ABitmap bitmap, bool editable = false, bool numOnly = false, bool fp = false, 
32 				bool posOnly = false) @nogc @safe pure nothrow {
33 			this.text = text;
34 			this.bitmap = bitmap;
35 			if (editable) flags |= IS_EDITABLE;
36 			if (numOnly) {
37 				if (fp) {
38 					flags |= NUMERIC;
39 				} else {
40 					flags |= INTEGER;
41 				}
42 				if (posOnly) {
43 					flags |= POSITIVE_ONLY;
44 				}
45 			}
46 		}
47 		///Returns whether the field is editable.
48 		public @property bool editable() @nogc @safe pure nothrow const {
49 			return flags & IS_EDITABLE ? true : false;
50 		}
51 		///Sets whether the field is editable. Returns the new value.
52 		public @property bool editable(bool val) @nogc @safe pure nothrow {
53 			if (val) flags |= IS_EDITABLE;
54 			else flags &= ~IS_EDITABLE;
55 			return flags & IS_EDITABLE ? true : false;
56 		}
57 		///Returns whether the field is integer only.
58 		public @property bool integer() @nogc @safe pure nothrow const {
59 			return flags & INTEGER ? true : false;
60 		}
61 		///Sets whether the field is integer only. Returns the new value.
62 		public @property bool integer(bool val) @nogc @safe pure nothrow {
63 			if (val) flags |= INTEGER;
64 			else flags &= ~INTEGER;
65 			return flags & INTEGER ? true : false;
66 		}
67 		///Returns whether the field is numeric only.
68 		public @property bool numeric() @nogc @safe pure nothrow const {
69 			return flags & NUMERIC ? true : false;
70 		}
71 		///Sets whether the field is numeric only. Returns the new value.
72 		public @property bool numeric(bool val) @nogc @safe pure nothrow {
73 			if (val) flags |= NUMERIC;
74 			else flags &= ~NUMERIC;
75 			return flags & NUMERIC ? true : false;
76 		}
77 		///Returns whether the field is positive only.
78 		public @property bool positiveOnly() @nogc @safe pure nothrow const {
79 			return flags & POSITIVE_ONLY ? true : false;
80 		}
81 		///Sets whether the field is positive only. Returns the new value.
82 		public @property bool positiveOnly(bool val) @nogc @safe pure nothrow {
83 			if (val) flags |= POSITIVE_ONLY;
84 			else flags &= ~POSITIVE_ONLY;
85 			return flags & POSITIVE_ONLY ? true : false;
86 		}
87 	}
88 	/**
89 	 * Stores the list of items to be displayed.
90 	 */
91 	public Field[]		fields;
92 	///Height of this item.
93 	public int			height;
94 	/**
95 	 * Creates a list view item from texts.
96 	 * Parameters:
97 	 *  height = the height of the entry in pixels.
98 	 *  fields = automatically defined text input fields.
99 	 */
100 	this (int height, Text[] fields) @safe pure nothrow {
101 		this.height = height;
102 		this.fields.reserve = fields.length;
103 		foreach (Text key; fields) {
104 			this.fields ~= Field(key, null);
105 		}
106 	}
107 	/**
108 	 * Creates a ListViewItem from fields directly.
109 	 * Parameters:
110 	 *  height = the height of the entry in pixels.
111 	 *  fields = each field directly defined through a Field struct.
112 	 */
113 	this (int height, Field[] fields) @nogc @safe pure nothrow {
114 		this.height = height;
115 		this.fields = fields;
116 	}
117 	/**
118 	 * Creates a ListViewItem with default text formatting.
119 	 * Parameters:
120 	 *  height = the height of the entry in pixels.
121 	 *  ds = a string array containing the text of each field. Default formatting will be used.
122 	 */
123 	this (int height, dstring[] ds) @safe nothrow {
124 		this.height = height;
125 		fields.reserve = ds.length;
126 		foreach (dstring key ; ds) {
127 			this.fields ~= Field(new Text(key, globalDefaultStyle.getChrFormatting("ListViewHeader")), null);
128 		}
129 	}
130 	/**
131 	 * Creates a ListViewItem with default text formatting and input type.
132 	 * Parameters:
133 	 *  height = the height of the entry in pixels.
134 	 *  ds = a string array containing the text of each field. Default formatting will be used.
135 	 *  inputTypes = specifies each field's input type. Mus be the same length as parameter `ds`.
136 	 */
137 	this (int height, dstring[] ds, TextInputFieldType[] inputTypes) @safe nothrow {
138 		this.height = height;
139 		fields.reserve = ds.length;
140 		assert (ds.length == inputTypes.length, "Mismatch in inputTypes and text length");
141 		for (size_t i ; i < ds.length ; i++) {
142 			const bool isNumeric = inputTypes[i] == TextInputFieldType.Hex || inputTypes[i] == TextInputFieldType.Integer ||
143 					inputTypes[i] == TextInputFieldType.IntegerP || inputTypes[i] == TextInputFieldType.Decimal ||
144 					inputTypes[i] == TextInputFieldType.DecimalP;
145 			Field f = Field(new Text(ds[i], globalDefaultStyle.getChrFormatting("ListViewHeader")), null, 
146 					inputTypes[i] != TextInputFieldType.None, isNumeric, inputTypes[i] == TextInputFieldType.Decimal ||
147 					inputTypes[i] == TextInputFieldType.DecimalP, inputTypes[i] == TextInputFieldType.IntegerP ||
148 					inputTypes[i] == TextInputFieldType.DecimalP);
149 			fields ~= f;
150 		}
151 	}
152 	/**
153 	 * Accesses fields like an array.
154 	 */
155 	public ref Field opIndex(size_t index) @nogc @safe pure nothrow {
156 		return fields[index];
157 	}
158 	/**
159 	 * Accesses fields like an array.
160 	 */
161 	public Field opIndexAssign(Field value, size_t index) @nogc @safe pure nothrow {
162 		fields[index] = value;
163 		return value;
164 	}
165 	///Returns the amount of fields in this item.
166 	public size_t length() @nogc @safe pure nothrow {
167 		return fields.length;
168 	}
169 	/**
170 	 * Draws the ListViewItem. Draw parameters are supplied via a nested class found in ListView.
171 	 * Parameters:
172 	 *  parent: the ListView this instance belongs to.
173 	 */
174 	public void draw(ListView parent) {
175 		StyleSheet ss = parent.drawParams.ss;
176 		Box target = parent.drawParams.target;
177 		Box t = Box(target.left, target.top, target.left, target.bottom);
178 		Point offset = Point(parent.drawParams.offsetP, parent.drawParams.offsetFR);
179 		for (int i = parent.drawParams.offsetC ; i <= parent.drawParams.targetC ; i++) {
180 			t.right = min(t.left + parent.drawParams.columnWidths[i] - offset.x, target.right);
181 			parent.drawTextSL(t.pad(ss.drawParameters["ListViewColPadding"], ss.drawParameters["ListViewRowPadding"]), 
182 					fields[i].text, offset);
183 			t.left = t.right;
184 			offset.x = 0;
185 		}
186 		parent.drawParams.target.relMove(0, height - parent.drawParams.offsetFR);
187 		parent.drawParams.offsetFR = 0;
188 	}
189 }
190 /**
191  * Defines the header of a ListView.
192  * Extended from a ListViewItem.
193  */
194 public class ListViewHeader : ListViewItem {
195 	public int[]				columnWidths;	///Width of each columns
196 	/**
197 	 * Default CTOR.
198 	 * Parameters:
199 	 *  height = the height of the header.
200 	 *  columnWidths = the width of each column. Array must match the length of the next parameter
201 	 *  fields: specifies the text of each field. Custom formatting is supported
202 	 */
203 	this(int height, int[] columnWidths, Text[] fields) @safe pure nothrow {
204 		assert (columnWidths.length == fields.length, "Lenght mismatch between the two arrays!");
205 		this.columnWidths = columnWidths;
206 		super(height, fields);
207 	}
208 	/**
209 	 * CTOR for creating fields with default text formatting
210 	 * Parameters:
211 	 *  height = the height of the header.
212 	 *  columnWidths = the width of each column. Array must match the length of the next parameter
213 	 */
214 	this(int height, int[] columnWidths, dstring[] ds) @safe nothrow {
215 		Text[] fields;
216 		fields.reserve = ds.length;
217 		foreach (dstring key; ds) {
218 			fields ~= new Text(key, globalDefaultStyle.getChrFormatting("ListViewHeader"));
219 		}
220 		this(height, columnWidths, fields);
221 	}
222 	/**
223 	 * Draws the header. Draw parameters are supplied via a nested class found in ListView.
224 	 * Parameters:
225 	 *  parent: the ListView this instance belongs to.
226 	 */
227 	public override void draw(ListView parent) {
228 		if (!height) return;
229 		StyleSheet ss = parent.drawParams.ss;
230 		Box target = parent.drawParams.target;
231 		Box t = Box(target.left, target.top, target.left, target.bottom);
232 		Point offset = Point(parent.drawParams.offsetP, 0);
233 		for (int i = parent.drawParams.offsetC ; i <= parent.drawParams.targetC ; i++) {
234 			t.right = min(t.left + parent.drawParams.columnWidths[i] - offset.x, target.right);
235 			if (!offset.x) {
236 				parent.drawLine(t.cornerUL, t.cornerLL, ss.getColor("windowascent"));
237 			}
238 			if (t.left + parent.drawParams.columnWidths[i] < target.right) {
239 				Point from = t.cornerUR, to = t.cornerLR;
240 				from.x = from.x - 1;
241 				to.x = to.x - 1;
242 				parent.drawLine(from, to, ss.getColor("windowdescent"));
243 			}
244 			with (parent) {
245 				drawLine(t.cornerUL, t.cornerUR, ss.getColor("windowascent"));
246 				drawLine(t.cornerLL, t.cornerLR, ss.getColor("windowdescent"));
247 				drawTextSL(t.pad(ss.drawParameters["ListViewColPadding"], ss.drawParameters["ListViewRowPadding"]), fields[i].text, 
248 						offset);
249 			}
250 			t.left = t.right;
251 			offset.x = 0;
252 		}
253 		parent.drawParams.target.relMove(0, height);
254 	}
255 }
256 /**
257  * Implements a basic ListView 
258  */
259 public class ListView : WindowElement, ElementContainer, TextInputListener {
260 	///Supplies draw parameters to the items
261 	public class DrawParameters {
262 		///StyleSheet that is being used currently
263 		StyleSheet 				ss;
264 		///Contains the reference to the header's columnWidth attribute
265 		int[]					columnWidths;
266 		///The first column to be drawn
267 		const int				offsetC;
268 		///The last column to be drawn
269 		const int				targetC;
270 		///Offset in pixels for the first column
271 		const int				offsetP;
272 		///Offset of the first row. Should be set to zero after the first row has been drawn.
273 		int						offsetFR;
274 		///The prelimiter where the item should be drawn.
275 		Box						target;
276 		///CTOR
277 		this (StyleSheet ss, int[] columnWidths, const int offsetC, const int targetC, const int offsetP, 
278 				int offsetFR) @safe @nogc pure nothrow {
279 			this.ss = ss;
280 			this.columnWidths = columnWidths;
281 			this.offsetC = offsetC;
282 			this.targetC = targetC;
283 			this.offsetP = offsetP;
284 			this.offsetFR = offsetFR;
285 		}
286 	}
287 	protected HorizScrollBar	horizSlider;	///Horizontal scroll bar.
288 	protected VertScrollBar		vertSlider;		///Vertical scroll bar.
289 	///The header of the ListView. 
290 	///Accessed in a safe manner to ensure it's being updated on the output raster.
291 	protected ListViewHeader	_header;
292 	///Entries in the ListView.
293 	///Accessed in a safe manner to ensure it's being updated on the output raster and that the number of columns match.
294 	protected ListViewItem[]	entries;
295 	protected int				selection;		///Selected item's number, or -1 if none selected.
296 	protected int				hSelection;		///Horizontal selection for text editing.
297 	protected int				tselect;		///Lenght of selected characters.
298 	protected int				cursorPos;		///Position of cursor.
299 	protected int				horizTextOffset;///Horizontal text offset if text cannot fit the cell.
300 	///Text editing area.
301 	protected Box				textArea;
302 	///Filters the input to the cell if not null.
303 	protected InputFilter		filter;
304 	///Holds shared draw parameters that are used when the element is being drawn.
305 	///Should be set to null otherwise.
306 	public DrawParameters		drawParams;
307 	///Called when an item is selected
308 	public EventDeleg			onItemSelect;
309 	///Called when text input is finished and accepted
310 	public EventDeleg			onTextInput;
311 	protected static enum	EDIT_EN = 1<<9;
312 	protected static enum	MULTICELL_EDIT_EN = 1<<10;
313 	protected static enum	TEXTINPUT_EN = 1<<11;
314 	protected static enum	INSERT = 1<<12;
315 	/**
316 	 * Creates an instance of a ListView with the supplied parameters.
317 	 * Parameters:
318 	 *  header: Specifies an initial header for the element. Null if there's none.
319 	 *  entries: Specifies initial entries for the element. Null if there're none.
320 	 *  source: Sets all event output's source parameter.
321 	 *  position: Tells where the element should be drawn on the window.
322 	 */
323 	public this(ListViewHeader header, ListViewItem[] entries, string source, Box position) {
324 		_header = header;
325 		this.entries = entries;
326 		this.source = source;
327 		this.position = position;
328 		recalculateTotalSizes();
329 	}
330 	/**
331 	 * Accesses data entries in a safe manner.
332 	 */
333 	public ListViewItem opIndex(size_t index) @nogc @safe pure nothrow {
334 		return entries[index];
335 		/+scope(exit) {
336 			assert(entries[index].length == _header.length, "Column number mismatch error!");
337 		}+/
338 	}
339 	/**
340 	 * Accesses data entries in a safe manner.
341 	 */
342 	public ListViewItem opIndexAssign(ListViewItem value, size_t index) @safe pure {
343 		if (value.length == _header.length) {
344 			if (entries.length == index) {
345 				entries ~= value;
346 			} else {
347 				entries[index] = value;
348 			}
349 		} else throw new Exception("Column number mismatch!");
350 		return value;
351 	}
352 	/**
353 	 * Allows to append a single element to the entry list.
354 	 */
355 	public ListViewItem opOpAssign(string op)(ListViewItem value) {
356 		static if (op == "~" || op == "+") {
357 			if (value.length == _header.length) {
358 				entries ~= value;
359 			} else throw new Exception("Column number mismatch!");
360 		} else static assert (0, "Unsupported operator!");
361 		return value;
362 	}
363 	/**
364 	 * Allows to append multiple elements to the entry list.
365 	 */
366 	public ListViewItem[] opOpAssign(string op)(ListViewItem[] value) {
367 		static if (op == "~" || op == "+") {
368 			foreach (ListViewItem key; value) {
369 				if (key.length == _header.length) {
370 					entries ~= key;
371 				} else throw new Exception("Column number mismatch!");
372 			}
373 		} else static assert (0, "Unsupported operator!");
374 		return value;
375 	}
376 	override public void draw() {
377 		StyleSheet ss = getStyleSheet;
378 		if (flags & TEXTINPUT_EN) { //only redraw the editing cell in this case
379 			const int textPadding = ss.drawParameters["TextSpacingSides"];
380 			
381 			clearArea(textArea);
382 			//drawBox(position, ss.getColor("windowascent"));
383 			
384 			//draw cursor
385 			//if (flags & ENABLE_TEXT_EDIT) {
386 			//calculate cursor first
387 			Box cursor = Box(textArea.left + textPadding, textArea.top + textPadding, textArea.left + textPadding, 
388 					textArea.bottom - textPadding);
389 			cursor.left += text.getWidth(0, cursorPos) - horizTextOffset;
390 			//cursor must be at least single pixel wide
391 			cursor.right = cursor.left;
392 			if (tselect) {
393 				cursor.right += text.getWidth(cursorPos, cursorPos + tselect);
394 			} else if (flags & INSERT) {
395 				if (cursorPos < text.charLength) cursor.right += text.getWidth(cursorPos, cursorPos+1);
396 				else cursor.right += text.font.chars(' ').xadvance;
397 			} else {
398 				cursor.right++;
399 			}
400 			//Clamp down if cursor is wider than the text editing area
401 			cursor.right = cursor.right <= textArea.right - textPadding ? cursor.right : textArea.right - textPadding;
402 			//Draw cursor
403 			parent.drawFilledBox(cursor, ss.getColor("selection"));
404 
405 			//}
406 			//draw text
407 			parent.drawTextSL(textArea - textPadding, text, Point(horizTextOffset, 0));
408 		} else {
409 			parent.clearArea(position);
410 
411 			parent.drawBox(position, ss.getColor("windowascent"));
412 			Point upper = Point(0, position.top + _header.height);
413 			Point lower = Point(0, position.bottom);
414 			{	///Calculate first column stuff
415 				int offsetP, offsetC, targetC, targetP;
416 				if (horizSlider) {
417 					offsetP = horizSlider.value();
418 					for (; _header.columnWidths[offsetC] < offsetP ; offsetC++) {
419 						offsetP -= _header.columnWidths[offsetC];
420 					}
421 					offsetP = max(0, offsetP);
422 					///Calculate last column number
423 					targetP = horizSlider.value() + position.width;
424 					for (; _header.columnWidths.length > targetC && _header.columnWidths[targetC] < targetP ; targetC++) {
425 						targetP -= _header.columnWidths[targetC];
426 					}
427 					targetC = min(cast(int)(_header.columnWidths.length) - 1, targetC);
428 					lower.y -= horizSlider.getPosition().height;
429 				} else {
430 					targetC = cast(int)_header.columnWidths.length - 1;
431 				}
432 				drawParams = new DrawParameters(ss, _header.columnWidths, offsetC, targetC, offsetP, 0);
433 			}
434 
435 			drawParams.target = Box(position.left, position.top, position.right, position.top + _header.height);
436 
437 			if (vertSlider) {
438 				drawParams.target.right -= vertSlider.getPosition.width;
439 
440 			}
441 
442 			_header.draw(this);
443 			/+Point upper = Point(drawParams.columnWidths[drawParams.offsetC] + position.left, position.top + _header.height);
444 			Point lower = Point(upper.x, position.bottom);+/
445 			int firstRow, lastRow;
446 			if (vertSlider) {
447 				int pixelsTotal = vertSlider.value();
448 				for (; entries[firstRow].height < pixelsTotal ; firstRow++) {
449 					pixelsTotal -= entries[firstRow].height;
450 				}
451 				drawParams.offsetFR = pixelsTotal;
452 				pixelsTotal += position.height;
453 				pixelsTotal -= _header.height;
454 				if (horizSlider) pixelsTotal -= horizSlider.getPosition().height;
455 				lastRow = firstRow;
456 				for (; entries.length > lastRow && entries[lastRow].height < pixelsTotal ; lastRow++) {
457 					pixelsTotal -= entries[lastRow].height;
458 				}
459 				lastRow = min(cast(int)(entries.length) - 1, lastRow);
460 			} else {
461 				lastRow = cast(int)entries.length - 1;
462 			}
463 
464 			for (int i = firstRow ; i <= lastRow ; i++) {
465 				if (ss.getColor("ListViewHSep") && i != lastRow) {
466 					parent.drawLine(drawParams.target.cornerLL, drawParams.target.cornerLR, ss.getColor("ListViewHSep"));
467 				}
468 				if (selection == i) {
469 					Box target = drawParams.target - 1;
470 					target.bottom -= drawParams.offsetFR;
471 					parent.drawFilledBox(target, ss.getColor("selection"));
472 				}
473 				entries[i].draw(this);	
474 			}
475 
476 			if (ss.getColor("ListViewVSep")) {
477 				for (int i = drawParams.offsetC ; i <= drawParams.targetC ; i++) {
478 					upper.x = drawParams.columnWidths[i];
479 					lower.x = drawParams.columnWidths[i];
480 					parent.drawLine(upper, lower, ss.getColor("ListViewVSep"));
481 				}
482 			}
483 			if (horizSlider) horizSlider.draw;
484 			if (vertSlider) vertSlider.draw;
485 
486 			drawParams = null;
487 		}
488 		if (onDraw !is null) {
489 			onDraw();
490 		}
491 	}
492 	/**
493 	 * Returns the number of the selected item.
494 	 */
495 	public @property int value() @nogc @safe pure nothrow const {
496 		return selection;
497 	}
498 	/**
499 	 * Sets the selected item and then does a redraw.
500 	 * -1 sets selection to none.
501 	 */
502 	public @property int value(int val) {
503 		selection = val;
504 		clamp(val, -1, cast(int)(entries.length) - 1);
505 		draw;
506 		return selection;
507 	}
508 	/**
509 	 * Enables or disables the text editing of this element.
510 	 */
511 	public @property bool editEnable(bool val) @nogc @safe pure nothrow {
512 		if (val) flags |= EDIT_EN;
513 		else flags &= ~EDIT_EN;
514 		return flags & EDIT_EN ? true : false;
515 	}
516 	/**
517 	 * Returns true if text editing is enabled.
518 	 */
519 	public @property bool editEnable() @nogc @safe pure nothrow const {
520 		return flags & EDIT_EN ? true : false;
521 	}
522 	/**
523 	 * Enables or disables editing for multiple cells.
524 	 * If disabled, the first cell with editing enabled will be able to be edited.
525 	 */
526 	public @property bool multicellEditEnable(bool val) @nogc @safe pure nothrow {
527 		if (val) flags |= MULTICELL_EDIT_EN;
528 		else flags &= ~MULTICELL_EDIT_EN;
529 		return flags & MULTICELL_EDIT_EN ? true : false;
530 	}
531 	/**
532 	 * Returns true if text editing for multiple cells is enabled.
533 	 */
534 	public @property bool multicellEditEnable() @nogc @safe pure nothrow const {
535 		return flags & MULTICELL_EDIT_EN ? true : false;
536 	}
537 	/**
538 	 * Sets a new header, also able to supply new entries.
539 	 */
540 	public void setHeader(ListViewHeader header, ListViewItem[] entries) {
541 		_header = header;
542 		foreach (ListViewItem key; entries) {
543 			assert(key.length == header.length);
544 		}
545 		this.entries = entries;
546 		refresh();
547 		draw();
548 	}
549 	/**
550 	 * Removes an item from the entries.
551 	 * Returns the removed entry.
552 	 */
553 	public ListViewItem removeEntry(size_t index) {
554 		import std.algorithm.mutation : remove;
555 		ListViewItem result = entries[index];
556 		entries = entries.remove(index);
557 		if (selection >= entries.length) selection--;
558 		//draw;
559 		return result;
560 	}
561 	/**
562 	 * Removes all entries in the list.
563 	 */
564 	public void clear() @safe {
565 		entries.length = 0;
566 	}
567 	/**
568 	 * Refreshes the list view.
569 	 * Must be called every time when adding new items is finished.
570 	 */
571 	public void refresh() {
572 		selection = -1;
573 		recalculateTotalSizes;
574 		draw;
575 	}
576 	/**
577 	 * Recalculates the total width and height of the list view's field, also generates scrollbars if needed.
578 	 */
579 	protected void recalculateTotalSizes() {
580 		int totalWidth, totalHeight;
581 		foreach (i ; _header.columnWidths) {
582 			totalWidth += i;
583 		}
584 		foreach (ListViewItem key; entries) {
585 			totalHeight += key.height;
586 		}
587 		totalHeight += _header.height;
588 		StyleSheet ss = getStyleSheet();
589 		bool needsVSB, needsHSB;
590 		if (totalWidth > position.width) 
591 			needsHSB = true;
592 		if (totalHeight > position.height)
593 			needsVSB = true;
594 		if (needsVSB && totalWidth > position.width - ss.drawParameters["VertScrollBarSize"])
595 			needsHSB = true;
596 		if (needsHSB && totalHeight > position.height - ss.drawParameters["HorizScrollBarSize"])
597 			needsVSB = true;
598 		totalHeight -= _header.height;
599 		if (needsVSB) {
600 			const int maxvalue = needsHSB ? totalHeight - (position.height - ss.drawParameters["HorizScrollBarSize"]
601 					- _header.height) : totalHeight - (position.height - _header.height);
602 			
603 			const Box target = Box(position.right - ss.drawParameters["HorizScrollBarSize"] + 2, position.top, 
604 					position.right, needsHSB ? position.bottom - ss.drawParameters["VertScrollBarSize"] : position.bottom);
605 			vertSlider = new VertScrollBar(maxvalue, source ~ "VSB", target);
606 			vertSlider.setParent(this);
607 			vertSlider.onScrolling = &scrollBarEventOut;
608 		} else vertSlider = null;
609 		if (needsHSB){
610 			const int maxvalue = needsVSB ? totalWidth - (position.width - ss.drawParameters["VertScrollBarSize"]) : 
611 					totalWidth - position.width;
612 			const Box target = Box(position.left, position.bottom - ss.drawParameters["VertScrollBarSize"] + 2, 
613 					needsVSB ? position.right - ss.drawParameters["HorizScrollBarSize"] : position.right,
614 					position.bottom);
615 			horizSlider = new HorizScrollBar(maxvalue, source ~ "VSB", target);
616 			horizSlider.setParent(this);
617 			horizSlider.onScrolling = &scrollBarEventOut;
618 		} else horizSlider = null;
619 	}
620 	protected void scrollBarEventOut(Event ev) {
621 		draw;
622 	}
623 	/**
624 	 * Returns the absolute position of the element.
625 	 */
626 	public Box getAbsolutePosition(WindowElement sender) {
627 		return parent.getAbsolutePosition(sender);
628 	}
629 	/**
630 	 * Gives focus to the element if applicable
631 	 */
632 	public void requestFocus(WindowElement sender) {
633 		
634 	}
635 	/**
636 	 * Sets the cursor to the given type on request.
637 	 */
638 	public void requestCursor(CursorType type) {
639 
640 	}
641 	///Draws a line.
642 	public void drawLine(Point from, Point to, ubyte color) @trusted {
643 		parent.drawLine(from, to, color);
644 	}
645 	///Draws a line pattern.
646 	public void drawLinePattern(Point from, Point to, ubyte[] pattern) @trusted {
647 		parent.drawLinePattern(from, to, pattern);
648 	}
649 	///Draws an empty rectangle.
650 	public void drawBox(Box target, ubyte color) @trusted {
651 		parent.drawBox(target, color);
652 	}
653 	///Draws an empty rectangle with line patterns.
654 	public void drawBoxPattern(Box target, ubyte[] pattern) @trusted {
655 		parent.drawBoxPattern(target, pattern);
656 	}
657 	///Draws a filled rectangle with a specified color.
658 	public void drawFilledBox(Box target, ubyte color) @trusted {
659 		parent.drawFilledBox(target, color);
660 	}
661 	///Pastes a bitmap to the given point using blitter, which threats color #0 as transparency.
662 	public void bitBLT(Point target, ABitmap source) @trusted {
663 		parent.bitBLT(target, source);
664 	}
665 	///Pastes a slice of a bitmap to the given point using blitter, which threats color #0 as transparency.
666 	public void bitBLT(Point target, ABitmap source, Box slice) @trusted {
667 		parent.bitBLT(target, source, slice);
668 	}
669 	///Pastes a repeated bitmap pattern over the specified area.
670 	public void bitBLTPattern(Box target, ABitmap pattern) @trusted {
671 		parent.bitBLTPattern(target, pattern);
672 	}
673 	///XOR blits a repeated bitmap pattern over the specified area.
674 	public void xorBitBLT(Box target, ABitmap pattern) @trusted {
675 		parent.xorBitBLT(target, pattern);
676 	}
677 	///XOR blits a color index over a specified area.
678 	public void xorBitBLT(Box target, ubyte color) @trusted {
679 		parent.xorBitBLT(target, color);
680 	}
681 	///Fills an area with the specified color.
682 	public void fill(Point target, ubyte color, ubyte background = 0) @trusted {
683 		parent.fill(target, color, background);
684 	}
685 	///Draws a single line text within the given prelimiter.
686 	public void drawTextSL(Box target, Text text, Point offset) @trusted {
687 		parent.drawTextSL(target, text, offset);
688 	}
689 	///Draws a multi line text within the given prelimiter.
690 	public void drawTextML(Box target, Text text, Point offset) @trusted {
691 		parent.drawTextML(target, text, offset);
692 	}
693 	///Clears the area within the target
694 	public void clearArea(Box target) @trusted {
695 		parent.clearArea(target);
696 	}
697 	///Passes mouse click event
698 	public override void passMCE(MouseEventCommons mec, MouseClickEvent mce) {
699 		///TODO: Handle mouse click when in text editing mode
700 		if (state != ElementState.Enabled) return;
701 		if (vertSlider) {
702 			const Box p = vertSlider.getPosition();
703 			if (p.isBetween(mce.x, mce.y)) {
704 				mce.x -= p.left;
705 				mce.y -= p.top;
706 				vertSlider.passMCE(mec, mce);
707 				return;
708 			}
709 		}
710 		if (horizSlider) {
711 			const Box p = horizSlider.getPosition();
712 			if (p.isBetween(mce.x, mce.y)) {
713 				mce.x -= p.left;
714 				mce.y -= p.top;
715 				horizSlider.passMCE(mec, mce);
716 				return;
717 			}
718 		}
719 
720 		//if (mce.button != MouseButton.Left && !mce.state) return;
721 
722 		mce.x -= position.left;
723 		mce.y -= position.top;
724 		if (entries.length && mce.y > _header.height && mce.button == MouseButton.Left && mce.state) {
725 			textArea.top = position.top;
726 			textArea.left = position.left;
727 			mce.y -= _header.height;
728 			int pixelsTotal = mce.y, pos;
729 			if (vertSlider) ///calculate outscrolled area
730 				pixelsTotal += vertSlider.value;
731 			while (pos < entries.length) {
732 				if (pixelsTotal > entries[pos].height) {
733 					pixelsTotal -= entries[pos].height;
734 					textArea.top += entries[pos].height;
735 					if (pos + 1 < entries.length)
736 						pos++;
737 				} else {
738 					break;
739 				}
740 			}
741 			if (pos >= entries.length) {
742 				selection = -1;
743 			} else if (selection == pos && (flags & EDIT_EN)) {
744 				//Calculate horizontal selection for Multicell editing if needed
745 				/+if (flags & MULTICELL_EDIT_EN) {
746 				
747 				} else {+/
748 				textArea.top += _header.height;
749 				foreach (size_t i, ListViewItem.Field f ; entries[selection].fields) {
750 					if (f.editable) {
751 						hSelection = cast(int)i;
752 						if (vertSlider) textArea.top -= vertSlider.value;
753 						if (horizSlider) textArea.left -= horizSlider.value;
754 						
755 						with (textArea) {
756 							bottom = entries[selection].height + textArea.top;
757 							right = _header.columnWidths[i] + textArea.left;
758 							left = max(textArea.left, position.left);
759 							top = max(textArea.top, position.top);
760 							right = min(textArea.right, position.right);
761 							bottom = min(textArea.bottom, position.bottom);
762 						}
763 						text = new Text(entries[selection][hSelection].text);
764 						cursorPos = 0;
765 						tselect = cast(int)text.charLength;
766 						//oldText = text;
767 						if (flags & MULTICELL_EDIT_EN) {
768 							if (textArea.left < mce.x && textArea.right > mce.x) {
769 								inputHandler.startTextInput(this);
770 								break;
771 							}
772 						} else {
773 							inputHandler.startTextInput(this);
774 							break;
775 						}
776 						
777 					}
778 					textArea.left += _header.columnWidths[i];
779 				}
780 				//}
781 				selection = pos;
782 			} else 
783 				selection = pos;
784 
785 			if (onItemSelect !is null && selection != -1)
786 				onItemSelect(new Event(this, entries[selection], EventType.Selection, SourceType.WindowElement));
787 		} else if (!entries.length)
788 			selection = -1;
789 		
790 		draw();
791 	}
792 	///Passes mouse move event
793 	public override void passMME(MouseEventCommons mec, MouseMotionEvent mme) {
794 		if (state != ElementState.Enabled) return;
795 		if (vertSlider) {
796 			const Box p = vertSlider.getPosition();
797 			if (p.isBetween(mme.x, mme.y)) {
798 				mme.x -= p.left - position.left;
799 				mme.y -= p.top - position.top;
800 				vertSlider.passMME(mec, mme);
801 				return;
802 			}
803 		}
804 		if (horizSlider) {
805 			const Box p = horizSlider.getPosition();
806 			if (p.isBetween(mme.x, mme.y)) {
807 				mme.x -= p.left - position.left;
808 				mme.y -= p.top - position.top;
809 				horizSlider.passMME(mec, mme);
810 				return;
811 			}
812 		}
813 	}
814 	///Passes mouse scroll event
815 	public override void passMWE(MouseEventCommons mec, MouseWheelEvent mwe) {
816 		if (state != ElementState.Enabled) return;
817 		if (horizSlider) horizSlider.passMWE(mec, mwe);
818 		if (vertSlider) vertSlider.passMWE(mec, mwe);
819 	}
820 	/**
821 	 * Puts a PopUpElement on the GUI.
822 	 */
823 	public void addPopUpElement(PopUpElement p) {
824 		parent.addPopUpElement(p);
825 	}
826 	/**
827 	 * Puts a PopUpElement on the GUI at the given position.
828 	 */
829 	public void addPopUpElement(PopUpElement p, int x, int y) {
830 		parent.addPopUpElement(p, x, y);
831 	}
832 	/** 
833 	 * Ends the popup session and closes all popups.
834 	 */
835 	public void endPopUpSession(PopUpElement p) {
836 		parent.endPopUpSession(p);
837 	}
838 	/**
839 	 * Closes a single popup element.
840 	 */
841 	public void closePopUp(PopUpElement p) {
842 		parent.closePopUp(p);
843 	}
844 	//Interface `TextInputListener` starts here
845 	/**
846 	 * Passes the inputted text to the target, alongside with a window ID and a timestamp.
847 	 */
848 	public void textInputEvent(uint timestamp, uint windowID, dstring text) {
849 		import pixelperfectengine.system.etc : removeUnallowedSymbols;
850 		/+if (allowedChars.length) {
851 			text = removeUnallowedSymbols(text, allowedChars);
852 			if (!text.length) return;
853 		}+/
854 		if (filter) {
855 			filter.use(text);
856 			if (!text.length) return;
857 		}
858 		if (tselect) {
859 			this.text.removeChar(cursorPos, tselect);
860 			tselect = 0;
861 			for(int j ; j < text.length ; j++){
862 				this.text.insertChar(cursorPos++, text[j]);
863 			}
864 		} else if (flags & INSERT) {
865 			for(int j ; j < text.length ; j++){
866 				this.text.overwriteChar(cursorPos++, text[j]);
867 			}
868 		} else {
869 			for(int j ; j < text.length ; j++){
870 				this.text.insertChar(cursorPos++, text[j]);
871 			}
872 		}
873 		const int textPadding = getStyleSheet.drawParameters["TextSpacingSides"];
874 		const Coordinate textPos = Coordinate(textPadding,(position.height / 2) - (this.text.font.size / 2) ,
875 				position.width,position.height - textPadding);
876 		const int x = this.text.getWidth(), cursorPixelPos = this.text.getWidth(0, cursorPos);
877 		if(x > textPos.width) {
878 			 if(cursorPos == this.text.text.length) {
879 				horizTextOffset = x - textPos.width;
880 			 } else if(cursorPixelPos < horizTextOffset) { //Test for whether the cursor would fall out from the current text area
881 				horizTextOffset = cursorPixelPos;
882 			 } else if(cursorPixelPos > horizTextOffset + textPos.width) {
883 				horizTextOffset = horizTextOffset + textPos.width;
884 			 }
885 		}
886 		draw();
887 	}
888 	/**
889 	 * Passes text editing events to the target, alongside with a window ID and a timestamp.
890 	 */
891 	public void textEditingEvent(uint timestamp, uint windowID, dstring text, int start, int length) {
892 		for (int i ; i < length ; i++) {
893 			this.text.overwriteChar(start + i, text[i]);
894 		}
895 		cursorPos = start + length;
896 	}
897 	/**
898 	 * Passes text input key events to the target, e.g. cursor keys.
899 	 */
900 	public void textInputKeyEvent(uint timestamp, uint windowID, TextInputKey key, ushort modifier) {
901 		switch(key) {
902 			case TextInputKey.Enter:
903 				entries[selection][hSelection].text = text;
904 				inputHandler.stopTextInput();
905 				if(onTextInput !is null)
906 					onTextInput(new CellEditEvent(this, entries[selection], selection, hSelection));
907 					//onTextInput(new Event(source, null, null, null, text, 0, EventType.T, null, this));
908 				break;
909 			case TextInputKey.Escape:
910 				//text = oldText;
911 				inputHandler.stopTextInput();
912 				
913 
914 				break;
915 			case TextInputKey.Backspace:
916 				if(cursorPos > 0){
917 					deleteCharacter(cursorPos - 1);
918 					cursorPos--;
919 					draw();
920 				}
921 				break;
922 			case TextInputKey.Delete:
923 				if (tselect) {
924 					for (int i ; i < tselect ; i++) {
925 						deleteCharacter(cursorPos);
926 					}
927 					tselect = 0;
928 				} else {
929 					deleteCharacter(cursorPos);
930 				}
931 				draw();
932 				break;
933 			case TextInputKey.CursorLeft:
934 				if (modifier != KeyModifier.Shift) {
935 					tselect = 0;
936 					if(cursorPos > 0){
937 						--cursorPos;
938 						draw();
939 					}
940 				}
941 				break;
942 			case TextInputKey.CursorRight:
943 				if (modifier != KeyModifier.Shift) {
944 					tselect = 0;
945 					if(cursorPos < text.charLength){
946 						++cursorPos;
947 						draw();
948 					}
949 				}
950 				break;
951 			case TextInputKey.Home:
952 				if (modifier != KeyModifier.Shift) {
953 					tselect = 0;
954 					cursorPos = 0;
955 					draw();
956 				}
957 				break;
958 			case TextInputKey.End:
959 				if (modifier != KeyModifier.Shift) {
960 					tselect = 0;
961 					cursorPos = cast(int)text.charLength;
962 					draw();
963 				}
964 				break;
965 			case TextInputKey.Insert:
966 				flags ^= INSERT;
967 				draw();
968 				break;
969 			default:
970 				break;
971 		}
972 	}
973 	/**
974 	 * When called, the listener should drop all text input.
975 	 */
976 	public void dropTextInput() {
977 		flags &= ~TEXTINPUT_EN;
978 		draw;
979 	}
980 	/**
981 	 * Called if text input should be initialized.
982 	 */
983 	public void initTextInput() {
984 		flags |= TEXTINPUT_EN;
985 		ListViewItem.Field f = opIndex(selection)[hSelection];
986 		if (f.numeric) {
987 			if (f.positiveOnly)
988 				filter = new DecimalFilter!false(f.text);
989 			else
990 				filter = new DecimalFilter!true(f.text);
991 		} else if (f.integer) {
992 			if (f.positiveOnly)
993 				filter = new IntegerFilter!false(f.text);
994 			else
995 				filter = new IntegerFilter!true(f.text);
996 		} else {
997 			filter = null;
998 		}
999 	}
1000 	private void deleteCharacter(size_t n){
1001 		text.removeChar(n);
1002 	}
1003 }