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 }