1 module evael.graphics.gui.controls.ListBox;
2 
3 import std.math : round;
4 import std.conv;
5 
6 import evael.graphics.gui.controls.Container;
7 import evael.graphics.gui.controls.ScrollBar;
8 import evael.graphics.gui.controls.Button;
9 import evael.graphics.Font;
10 
11 import evael.utils.Math;
12 
13 import evael.utils.Size;
14 import evael.utils.Rectangle;
15 
16 class ListBox : Container, IScrollable
17 {
18 	enum Type
19 	{
20 		List,
21 		Columns,
22 		Tile
23 	}
24 
25 	/// Column size
26 	private Size!int m_columnSize;
27 
28 	/// Item size
29 	private Size!int m_itemSize;
30 
31 	/// Position for the next column
32 	private vec2 m_nextColumnPosition;
33 
34 	/// Position for the next item
35 	private vec2 m_nextItemPosition;
36 
37 	/// Current selected item
38 	private Control m_selectedItem;
39 
40 	/// Clickable columns
41 	private Button[] m_columns;
42 
43 	/// Sub item index counter
44 	private ushort m_currentSubItemIndex;
45 
46 	/// ListBox type
47 	private Type m_type;
48 
49 	private uint m_currentIndex;
50 	
51 	protected alias OnItemSelected = void delegate(ListBoxItem item);
52 	protected OnItemSelected m_onItemSelectedEvent;
53 
54 	public this(in Type type, in float x, in float y, in int width, in int height)
55 	{
56 		this(type, vec2(x, y,), Size!int(width, height));
57 	}
58 
59 	public this(in Type type, in vec2 position, in Size!int size)
60 	{
61 		super(position, size);
62 		
63 		this.m_name = "listBox";
64 		this.m_itemSize = Size!int(this.m_size.width - 2, 20);
65 		this.m_nextItemPosition = vec2(1.0f, 1.0f);
66 
67 		this.m_verticalScrollBar = new ScrollBar(0, 0, 20, this.m_size.height);
68 		this.m_verticalScrollBar.hide();
69 
70 		this.type = type;
71 
72 		this.addChild(this.m_verticalScrollBar);
73 	}
74 
75 	/**
76 	 * Renders the listbox
77 	 */
78 	public override void draw(in float deltaTime)
79 	{
80 		if(!this.m_isVisible)
81 		{
82 			return;
83 		}
84 		
85 		Control.draw(deltaTime);
86 
87 		for(int i = 1; i < this.m_controls.length; i++)
88 		{
89 			this.m_controls[i].draw(deltaTime);
90 		}
91 
92 		// this.m_verticalScrollBar.draw(deltaTime);
93 	}
94 	
95 	/**
96 	 * Adds item
97 	 * Params:
98 	 *		 itemText : item text
99 	 *		 itemId : item identifier
100 	 */
101 	public ListBox addItem(in string itemText, in uint itemId = 0)
102 	{
103 		return this.addItem(to!wstring(itemText), itemId);
104 	}
105 
106 	public ListBox addItem(in wstring itemText, in uint itemId = 0)
107 	{
108 		int width = this.m_size.width;
109 
110 		if(this.m_type == Type.Columns)
111 		{
112 			width = this.m_columns[0].size.width;
113 		}
114 
115 		auto item = new ListBoxItem(this.m_currentIndex++, itemText, this.m_nextItemPosition, Size!int(width - 2, this.m_itemSize.height));
116 
117 		if(itemId != 0)
118 		{
119 			item.id = itemId;
120 		}
121 
122 		this.addChild(item);
123 
124 		this.m_nextItemPosition = vec2(this.m_nextItemPosition.x, this.m_nextItemPosition.y + this.m_itemSize.height);
125 
126 		immutable uint itemsHeight = this.m_itemSize.height * this.m_controls.length / (this.m_columns.length + 1);
127 
128 		if(itemsHeight > this.m_size.height)
129 		{
130 			this.m_verticalScrollBar.computeIncrementation(this.m_size.height, itemsHeight);
131 
132 			// We check if guihandler already initialized controls, if not we wait him to do it
133 			// otherwise it means a new item is added at runtime
134 			if(this.m_initialized)
135 			{
136 				this.m_verticalScrollBar.middleButton.initialize();
137 			}
138 
139 			this.m_verticalScrollBar.show();
140 		}
141 
142 		// We prepare next sub item index
143 		this.m_currentSubItemIndex = 1;
144 
145 		return this;
146 	}
147 
148 	/**
149 	 * Adds text sub item
150 	 */
151 	public ListBox addSubItem(in wstring itemText)
152 	{
153 		assert(this.m_currentSubItemIndex < this.m_columns.length);
154 		assert(this.m_currentSubItemIndex > 0);
155 
156 		int totalColumnsWidth = 0;
157 
158 		foreach(columnIndex; 0..this.m_currentSubItemIndex)
159 			totalColumnsWidth += this.m_columns[columnIndex].size.width;
160 
161 		auto subitem = new ListBoxItem(this.m_currentIndex++, itemText, vec2(totalColumnsWidth + this.m_currentSubItemIndex, this.m_nextItemPosition.y + this.m_itemSize.height), 
162 									   Size!int(this.m_columns[this.m_currentSubItemIndex].size.width - 2, this.m_itemSize.height));
163 		this.addChild(subitem);
164 
165 		this.m_currentSubItemIndex++;
166 
167 		return this;
168 	}
169 
170 
171 	/**
172 	 * Adds control sub item
173 	 */
174 	public ListBox addSubItem(Control control)
175 	{
176 		assert(this.m_currentSubItemIndex < this.m_columns.length);
177 		assert(this.m_currentSubItemIndex > 0);
178 
179 		int totalColumnsWidth = 0;
180 
181 		foreach(columnIndex; 0..this.m_currentSubItemIndex)
182 			totalColumnsWidth += this.m_columns[columnIndex].size.width;
183 
184 		auto subitem = new ListBoxItem(this.m_currentIndex++, "", vec2(totalColumnsWidth + this.m_currentSubItemIndex, this.m_nextItemPosition.y + this.m_itemSize.height), 
185 									   Size!int(this.m_columns[this.m_currentSubItemIndex].size.width - 2, this.m_itemSize.height));
186 
187 		subitem.addChild(control);
188 
189 		this.addChild(subitem);
190 
191 		this.m_currentSubItemIndex++;
192 
193 		return this;
194 	}
195 
196 	public ListBox addSubItem(Control[] controls)
197 	{
198 		assert(this.m_currentSubItemIndex < this.m_columns.length);
199 		assert(this.m_currentSubItemIndex > 0);
200 
201 		int totalColumnsWidth = 0;
202 
203 		foreach(columnIndex; 0..this.m_currentSubItemIndex)
204 			totalColumnsWidth += this.m_columns[columnIndex].size.width;
205 
206 		auto subitem = new ListBoxItem(this.m_currentIndex++, "", vec2(totalColumnsWidth + this.m_currentSubItemIndex, this.m_nextItemPosition.y + this.m_itemSize.height), 
207 									   Size!int(this.m_columns[this.m_currentSubItemIndex].size.width - 2, this.m_itemSize.height));
208 
209 		this.m_currentSubItemIndex++;
210 
211 		float x = 0.0f;
212 
213 		foreach(i, control; controls)
214 		{
215 			control.position = vec2(x, 0.0f);
216 			x += control.size.width + 3;
217 
218 			subitem.addChild(control);
219 		}
220 
221 		this.addChild(subitem);
222 
223 		return this;
224 	}
225 
226 	/**
227 	 * Adds column
228 	 * Params:
229 	 *		columntText : column title
230 	 *		width : column width
231 	 */
232 	public void addColumn(in wstring columnText, in int width)
233 	{
234 		auto columnButton = new Button(columnText, this.m_nextColumnPosition.x, this.m_nextColumnPosition.y, width - 2, this.m_columnSize.height);
235 
236 		this.m_columns ~= columnButton;
237 
238 		this.m_nextColumnPosition = vec2(this.m_nextColumnPosition.x + width, this.m_nextColumnPosition.y);
239 
240 		this.addChild(columnButton);
241 	}
242 
243 	/**
244 	 * Returns item by index
245 	 * Params:
246 	 *		index : item index
247 	 */
248 	public ListBoxItem getItem(in uint index)
249 	{
250 		// 1 tooltip + 1 scrollbar + columns 		
251 		immutable realIndex = 2 + (this.m_columns.length * (index + 1));
252 
253 		assert(realIndex < this.m_controls.length, "Invalid index");
254 		
255 		return cast(ListBoxItem)this.m_controls[realIndex];
256 	}
257 
258  	/**
259 	 * Returns items by row index
260 	 * Params:
261 	 *		index : item index
262 	 */
263 	public ListBoxItem[] getItems(in uint index)
264 	{
265 		immutable uint itemIndex = 2 + (this.m_columns.length * (index + 1));
266 
267 		return cast(ListBoxItem[])this.m_controls[itemIndex..itemIndex + this.m_columns.length];
268 	}
269 
270 	/**
271 	 * Event called on mouse button click
272 	 * Params:
273 	 *		mouseButton : mouse button	 
274 	 *		mousePosition : mouse position
275 	 */
276 	public override void onMouseClick(in MouseButton mouseButton, in ref vec2 mousePosition)
277 	{
278 		super.onMouseClick(mouseButton, mousePosition);
279 
280 		this.switchState!(State.Clicked);
281 
282 		if(this.m_focusedControl !is null && this.m_focusedControl != this.m_selectedItem)
283 		{
284 			// Unfocus last selected item
285 			if(this.m_selectedItem !is null)
286 			{
287 				this.m_selectedItem.onMouseLeave();
288 			}
289 
290 			this.m_selectedItem = this.m_focusedControl;
291 
292 			if(this.m_onItemSelectedEvent !is null)
293 			{
294 				this.m_onItemSelectedEvent(cast(ListBoxItem)this.m_selectedItem);
295 			}
296 		}
297 	}
298 
299 	/**
300 	 * Event called on mouse button release
301 	 * Params:
302 	 *		mouseButton : mouse button
303 	 */
304 	public override void onMouseUp(in MouseButton mouseButton)
305 	{
306 		super.onMouseUp(mouseButton);
307 	}
308 
309 	/**
310 	 * Event called when mouse enters in control's rect
311 	 * Params:
312 	 * 		 mousePosition : mouse position
313 	 */
314 	public override void onMouseMove(in ref vec2 mousePosition)
315 	{
316 		this.m_hasFocus = true;
317 
318 		foreach(childControl; this.m_controls)
319 		{
320 			Rectangle!float rect = Rectangle!float(childControl.realPosition.x, childControl.realPosition.y, childControl.size);
321 
322 			if(rect.isIn(mousePosition))
323 			{
324 				childControl.onMouseMove(mousePosition);
325 
326 				// We focus this child control but first we unfocus the last one
327 				if(this.m_focusedControl !is null && this.m_focusedControl != childControl)
328 				{
329 					if(this.m_selectedItem != this.m_focusedControl)
330 						this.m_focusedControl.onMouseLeave();
331 				}
332 
333 				this.m_focusedControl = childControl;
334 				return;
335 			}
336 		}
337 
338 		// When mouse is moving in listbox but not in the items, this condition is checked.
339 		// We unfocus focused item only if its not the selected one
340 		if(this.m_focusedControl !is null && this.m_focusedControl.isEnabled && this.m_focusedControl != this.m_selectedItem)
341 		{
342 			this.m_focusedControl.onMouseLeave();
343 
344 			if(this.m_focusedControl.isClicked == false)
345 				this.m_focusedControl = null;
346 		}
347 	}
348 
349 	/**
350 	 * Event called when mouse leaves control's rect
351 	 */
352 	public override void onMouseLeave()
353 	{
354 		Control.onMouseLeave();
355 
356 		// When mouse is leaving listbox's rect
357 		// We unfocus focused item only if its not the selected one
358 		if(this.m_focusedControl !is null && this.m_focusedControl.isEnabled && this.m_focusedControl != this.m_selectedItem)
359 		{
360 			this.m_focusedControl.onMouseLeave();
361 		}
362 
363 		this.switchState!(State.Normal);
364 	}
365 
366 	public void onScroll(ScrollBar.ScrollDirection direction, in float scrollBarPosition)
367 	{
368 		final switch(direction)
369 		{
370 			case ScrollBar.ScrollDirection.Bottom:
371 			case ScrollBar.ScrollDirection.Top:
372 
373 				foreach(control; this.m_controls[1..$])
374 				{
375 					control.realPosition = vec2(this.m_realPosition.x + control.position.x, this.m_realPosition.y + control.position.y + scrollBarPosition);
376 				}
377 
378 				break;
379 
380 			case ScrollBar.ScrollDirection.Left:
381 				break;
382 
383 			case ScrollBar.ScrollDirection.Right:
384 				break;
385 		}
386 	}
387 
388 	public override void initialize()
389 	{
390 		super.initialize();
391 	}
392 
393 	/**
394 	 * Properties
395 	 */
396 	@property
397 	{
398 		public Control selectedItem() nothrow @nogc
399 		{
400 			return this.m_selectedItem;
401 		}
402 
403 		public int selectedIndex() const nothrow @nogc
404 		{
405 			if(this.m_selectedItem !is null)
406 			{
407 				return (cast(ListBoxItem)this.m_selectedItem).index;
408 			}
409 
410 			return -1;
411 		}
412 
413 		private void type(in Type value) nothrow @nogc
414 		{
415 			this.m_type = value;
416 
417 			if(this.m_type == Type.Columns)
418 			{
419 				this.m_columnSize = Size!int(this.m_size.width - 2, 20);
420 				
421 				// 1.0f = Border pixel
422 				this.m_nextColumnPosition = vec2(1.0f, 1.0f);
423 				this.m_nextItemPosition = vec2(1.0f, 21.0f);
424 			}
425 		}
426 
427 		public void onItemSelectedEvent(OnItemSelected callback) nothrow @nogc
428 		{
429 			this.m_onItemSelectedEvent = callback;
430 		}
431 	}
432 }
433 
434 class ListBoxItem : Container
435 {
436 	/// Item text
437 	private wstring m_text;
438 
439 	/// Item text position
440 	private vec2 m_textPosition;
441 
442 	/// Item index
443 	private uint m_index;
444 
445 	public this(in uint index,in  wstring text, in vec2 position, in Size!int size)
446 	{
447 		this(index, text, position, size);
448 	}
449 
450 	public this(in uint index, in wstring text, in ref vec2 position, in ref Size!int size)
451 	{
452 		super(position, size);
453 
454 		this.m_name = "listBoxItem";
455 		this.m_index = index;
456 		this.m_text = text;
457 	}
458 
459 	/**
460 	 * Renders the item
461 	 */
462 	public override void draw(in float deltaTime)
463 	{
464 		super.draw(deltaTime);
465 
466 		if(this.m_text.length)
467 		{
468 			this.m_theme.font.draw(this.m_text, vec2(this.m_realPosition.x + 2, this.m_realPosition.y), this.m_theme.fontColor, this.m_theme.fontSize);
469 		}
470 	}
471 
472 	/**
473 	 * Event called on mouse button click
474 	 * Params:
475 	 *		mouseButton : mouse button	 
476 	 *		mousePosition : mouse position
477 	 */
478 	public override void onMouseClick(in MouseButton mouseButton, in ref vec2 mousePosition)
479 	{
480 		super.onMouseClick(mouseButton, mousePosition);
481 		this.switchState!(State.Clicked);
482 	}
483 
484 	/**
485 	 * Event called on mouse button release
486 	 * Params:
487 	 *		mouseButton : mouse button
488 	 */
489 	public override void onMouseUp(in MouseButton mouseButton)
490 	{
491 		super.onMouseUp(mouseButton);
492 		this.switchState!(State.Hovered);
493 	}
494 
495 	/**
496 	 * Event called when mouse enters in control's rect
497 	 * Params:
498 	 * 		 mousePosition : mouse position
499 	 */
500 	public override void onMouseMove(in ref vec2 mousePosition)
501 	{
502 		if(this.hasFocus)
503 		{
504 			return;
505 		}
506 
507 		super.onMouseMove(mousePosition);
508 
509 		if(this.isClicked)
510 		{
511 			this.switchState!(State.Clicked);
512 		}
513 		else
514 		{
515 			this.switchState!(State.Hovered);
516 		}
517 	}
518 
519 	/**
520 	 * Event called when mouse leaves control's rect
521 	 */
522 	public override void onMouseLeave()
523 	{
524 		super.onMouseLeave();
525 		this.switchState!(State.Normal);
526 	}
527 
528 	/**
529 	 * Properties
530 	 */
531 	@property
532 	{
533 		public uint index() const nothrow @nogc
534 		{
535 			return this.m_index;
536 		}
537 
538 		public wstring text() const nothrow @nogc
539 		{
540 			return this.m_text;
541 		}
542 
543 		public void text(in wstring value) nothrow @nogc
544 		{
545 			this.m_text = value;
546 		}
547 
548 		public void text(in string value)  
549 		{
550 			this.m_text = value.to!wstring();
551 		}
552 	}
553 }