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