1 module evael.graphics.gui.GuiManager; 2 3 import std.algorithm; 4 import std.array : array; 5 import std.file : read; 6 import std.json : parseJSON, JSONValue; 7 import std.conv : to; 8 import std.string : format; 9 import jsonizer; 10 11 import evael.graphics.GUI; 12 import evael.graphics.GraphicsDevice; 13 14 import evael.system.AssetLoader; 15 import evael.system.Input; 16 17 import evael.utils.Math; 18 import evael.utils.Rectangle; 19 20 /** 21 * GuiManager. 22 */ 23 class GuiManager 24 { 25 private GraphicsDevice m_graphicsDevice; 26 private NVGcontext* m_nvg; 27 28 /// Container list 29 private Container[] m_containers; 30 31 /// Control that have the focus 32 private Container m_focusedControl; 33 34 /// Control under the mouse 35 private Container m_controlUnderMouse; 36 37 /// Drag'n'dropped control 38 private Control m_dragAndDropControl; 39 40 private vec2 m_mousePosition; 41 42 /// Default theme 43 private Theme m_defaultTheme; 44 45 /// Current theme 46 private Theme m_currentTheme; 47 48 private string[] m_customControlsTypes = ["button", "textBlock", "textBox", "progressBar", "textArea", "slider", 49 "panel", "listBox", "listBoxItem", "checkBox", "contextMenuStrip", 50 "contextMenuStripItem", "pictureBox", "tooltip", "comboBox", "scrollBar"]; 51 52 /** 53 * GuiManager constructor. 54 */ 55 public this(GraphicsDevice graphics) 56 { 57 this.m_graphicsDevice = graphics; 58 this.m_nvg = graphics.nvgContext; 59 60 this.m_defaultTheme = this.loadTheme("medias\\ui\\themes\\default.json"); 61 this.m_currentTheme = this.m_defaultTheme; 62 } 63 64 /** 65 * GuiManager destructor. 66 */ 67 @nogc @safe 68 public void dispose() pure nothrow 69 { 70 71 } 72 73 public void update(in float deltaTime) 74 { 75 nvgBeginFrame(this.m_nvg, this.m_graphicsDevice.viewportSize.width, this.m_graphicsDevice.viewportSize.height, 1); 76 77 foreach (control; this.m_containers) 78 { 79 control.draw(deltaTime); 80 } 81 82 nvgEndFrame(this.m_nvg); 83 } 84 85 public void fixedUpdate(in float deltaTime) 86 { 87 foreach (control; this.m_containers) 88 { 89 control.update(deltaTime); 90 } 91 } 92 93 94 /** 95 * Adds a container and initializes it. 96 * Params: 97 * container : container to add 98 */ 99 public void add(Container container) 100 { 101 container.nvg = this.m_nvg; 102 103 if (container.name in this.m_currentTheme.subThemes) 104 { 105 // This container have a custom theme 106 container.theme = this.m_currentTheme.subThemes[container.name]; 107 } 108 else 109 { 110 container.theme = this.m_currentTheme.copy(); 111 } 112 113 container.initialize(); 114 115 this.m_containers ~= container; 116 } 117 118 /** 119 * Removes a control. 120 * Params: 121 * control : control to remove 122 */ 123 public void remove(Control toRemove) 124 { 125 foreach (i, control; this.m_containers) 126 { 127 if (control == toRemove) 128 { 129 this.m_containers = this.m_containers.remove(i); 130 return; 131 } 132 } 133 } 134 135 136 /** 137 * Loads a theme. 138 * Params: 139 * fileName : theme to load 140 */ 141 public Theme loadTheme(in string fileName) 142 { 143 // We load base theme 144 auto themeJson = fileName.read().to!string().parseJSON(); 145 auto theme = themeJson.fromJSON!(Theme); 146 147 auto assetLoader = AssetLoader.getInstance(); 148 149 if (theme.font !is null) 150 { 151 theme.font = assetLoader.load!(Font)(theme.font.name, this.m_nvg); 152 theme.iconFont = assetLoader.load!(Font)(theme.iconFont.name, this.m_nvg); 153 } 154 155 theme.parent = null; 156 theme.name = "base"; 157 theme.subThemes = loadSubThemes(theme, themeJson); 158 159 return theme; 160 } 161 162 private Theme[string] loadSubThemes(Theme parentTheme, JSONValue json) 163 { 164 Theme[string] toRet; 165 166 // We load controls themes 167 foreach (controlType; this.m_customControlsTypes) 168 { 169 if (controlType in json) 170 { 171 auto controlJson = json[controlType]; 172 173 // Theme has been found for this control, inherited from base theme 174 auto controlTheme = parentTheme.copy(); 175 controlTheme.parent = parentTheme; 176 controlTheme.name = parentTheme.name ~ "." ~ controlType; 177 178 // We need to update custom fields values 179 foreach (i, dummy ; typeof(Theme.tupleof)) 180 { 181 enum name = Theme.tupleof[i].stringof; 182 enum type = typeof(Theme.tupleof[i]).stringof; 183 184 static if (type != "Theme*") 185 { 186 if(name in controlJson) 187 { 188 static if(type == "BorderType") 189 { 190 mixin(`controlTheme.%s = controlJson["%s"].fromJSON!(Theme.%s);`.format(name, name, type)); 191 } 192 else 193 { 194 mixin(`controlTheme.%s = controlJson["%s"].fromJSON!(%s);`.format(name, name, type)); 195 } 196 } 197 } 198 } 199 200 toRet[controlType] = controlTheme; 201 202 controlTheme.subThemes = loadSubThemes(controlTheme, controlJson); 203 } 204 205 } 206 207 return toRet; 208 } 209 210 211 /** 212 * Sets current theme. 213 * Params: 214 * theme : current theme for next controls 215 */ 216 @nogc @safe 217 public void setTheme()(in auto ref Theme theme) pure nothrow 218 { 219 this.m_currentTheme = theme; 220 } 221 222 /** 223 * Loads and sets current theme. 224 * Params: 225 * themeName : theme to load 226 */ 227 public void setTheme(in string themeName) 228 { 229 this.m_currentTheme = this.loadTheme(themeName); 230 } 231 232 /** 233 * Adds a custom control type for theme loading. 234 * Params: 235 * type : custom type 236 */ 237 public void addCustomControlType(in string type) 238 { 239 this.m_customControlsTypes ~= type; 240 } 241 242 /** 243 * Event called on mouse button click action. 244 * Params: 245 * position : mouse position 246 * mouseButton : clicked mouse button 247 */ 248 public void onMouseClick(in MouseButton mouseButton, in ref vec2 mousePosition) 249 { 250 if (this.m_controlUnderMouse !is null) 251 { 252 this.m_controlUnderMouse.onMouseClick(mouseButton, mousePosition); 253 254 if (this.m_dragAndDropControl is null || !this.m_dragAndDropControl.isClicked) 255 { 256 this.m_dragAndDropControl = this.getDeepestControlUnderMouse(this.m_mousePosition); 257 258 if (this.m_dragAndDropControl !is null) 259 { 260 if(this.m_dragAndDropControl.canBeDragged()) 261 { 262 this.m_dragAndDropControl.positionBeforeDragAndDrop = this.m_dragAndDropControl.realPosition; 263 } 264 else 265 { 266 this.m_dragAndDropControl = null; 267 } 268 } 269 } 270 } 271 } 272 273 /** 274 * Event called on mouse button release action. 275 * Params: 276 * position : mouse position 277 * mouseButton : released mouse button 278 */ 279 public void onMouseUp(in MouseButton mouseButton) 280 { 281 if (this.m_controlUnderMouse !is null) 282 { 283 // We stop drag and drop only if user released the button used to drag the control 284 if (this.m_dragAndDropControl !is null 285 && (this.m_dragAndDropControl.draggingMouseButton.isNull || mouseButton == this.m_dragAndDropControl.draggingMouseButton)) 286 { 287 // We need to check if mouse has been moved while clicked 288 if (this.m_dragAndDropControl.realPosition != this.m_dragAndDropControl.positionBeforeDragAndDrop) 289 { 290 this.m_dragAndDropControl.onDrop(this.m_mousePosition); 291 this.m_dragAndDropControl = null; 292 } 293 } 294 295 this.m_controlUnderMouse.onMouseUp(mouseButton); 296 297 if (this.m_controlUnderMouse.isFocusable) 298 { 299 this.focus(this.m_controlUnderMouse); 300 } 301 } 302 } 303 304 /** 305 * Event called on mouse movement action. 306 * Params: 307 * position : mouse position 308 */ 309 public void onMouseMove(in ref vec2 position) 310 { 311 scope (exit) 312 { 313 this.m_mousePosition = position; 314 } 315 316 // We draw the control that is under the mouse at the end 317 if (this.m_controlUnderMouse !is null) 318 { 319 this.m_containers.sort!((a, b) => b == this.m_controlUnderMouse); 320 } 321 322 /** 323 * Special case : drag and drop 324 */ 325 if (this.m_dragAndDropControl !is null && this.m_dragAndDropControl.isClicked && this.m_dragAndDropControl.movable) 326 { 327 // Yes. We handle this. 328 immutable newPosition = vec2(position.x - this.m_dragAndDropControl.size.halfWidth, position.y - this.m_dragAndDropControl.size.halfHeight); 329 330 this.m_dragAndDropControl.position = newPosition; 331 this.m_dragAndDropControl.realPosition = newPosition; 332 333 this.m_dragAndDropControl.onDrag(position); 334 335 return; 336 } 337 338 /** 339 * Normal case 340 */ 341 auto lastControlUnderMouse = this.m_controlUnderMouse; 342 343 this.m_controlUnderMouse = this.getControlUnderMouse(position); 344 345 if (this.m_controlUnderMouse !is null) 346 { 347 this.m_controlUnderMouse.onMouseMove(position); 348 349 Control tooltipControl = this.m_controlUnderMouse; 350 351 // We search for the control under the mouse and update the tooltip text 352 auto deepestFocusedControl = this.getDeepestControlUnderMouse(position); 353 354 // At this point we have the last-level container under the mouse, 355 // if his tooltiptext is set or if he doesn't have child control under mouse, 356 // we will use his tooltiptext 357 if (deepestFocusedControl.tooltipText !is null || deepestFocusedControl.controlUnderMouse is null) 358 { 359 tooltipControl = deepestFocusedControl; 360 } 361 else 362 { 363 tooltipControl = deepestFocusedControl.controlUnderMouse; 364 } 365 366 this.m_controlUnderMouse.tooltip.realPosition = vec2(position.x + 13, position.y + 5); 367 this.m_controlUnderMouse.tooltip.text = tooltipControl.tooltipText; 368 this.m_controlUnderMouse.tooltip.show(); 369 } 370 371 // We call onMouseLeave on last control under mouse if its different 372 if (lastControlUnderMouse !is null && this.m_controlUnderMouse != lastControlUnderMouse) 373 { 374 lastControlUnderMouse.onMouseLeave(); 375 } 376 } 377 378 /** 379 * Event called on character input. 380 * Params: 381 * text : 382 */ 383 public void onText(in int text) 384 { 385 // %, esc, del, ctrl + a 386 if (text == 37 || text == 27 || text == 8 || text == 1) 387 return; 388 389 if (this.m_focusedControl !is null) 390 { 391 this.m_focusedControl.onText(text); 392 } 393 } 394 395 /** 396 * Event called on key action. 397 * Params: 398 * key : pressed key 399 */ 400 public void onKey(in int key) 401 { 402 if(this.m_focusedControl !is null) 403 { 404 this.m_focusedControl.onKey(key); 405 } 406 } 407 408 /** 409 * Returns the first-level container under the mouse. 410 */ 411 private Container getControlUnderMouse(in ref vec2 position) 412 { 413 foreach(container; this.m_containers) 414 { 415 if(!container.isVisible) 416 { 417 continue; 418 } 419 420 immutable rect = Rectanglef(container.position.x, container.position.y, container.size); 421 422 if(rect.isIn(position)) 423 { 424 return container; 425 } 426 } 427 428 return null; 429 } 430 431 /** 432 * Returns the last-level container under the mouse. 433 */ 434 private Container getDeepestControlUnderMouse(in ref vec2 position) 435 { 436 auto control = this.getControlUnderMouse(position); 437 438 if (control is null) 439 { 440 return null; 441 } 442 443 auto deepestFocusedControl = control; 444 445 while (control !is null) 446 { 447 deepestFocusedControl = control; 448 control = cast(Container) control.controlUnderMouse; 449 } 450 451 return deepestFocusedControl; 452 } 453 454 455 /** 456 * Returns a control by his id. 457 * Params: 458 * id : control id 459 */ 460 public Control getControlById(in uint id) nothrow 461 { 462 auto range = this.m_containers.filter!(c => c.id == id).array; 463 464 return range.length ? range[0] : null; 465 } 466 467 /** 468 * Removes all controls. 469 */ 470 @nogc @safe 471 public void clear() pure nothrow 472 { 473 this.m_containers = null; 474 } 475 476 /** 477 * Gives focus to a control. 478 */ 479 public void focus(Container control) 480 { 481 control.focus(); 482 483 this.m_focusedControl = control; 484 } 485 486 /** 487 * Removes focus from a control. 488 */ 489 public void unfocus(Container control) 490 { 491 control.unfocus(); 492 493 this.m_focusedControl = null; 494 } 495 496 @nogc @safe 497 @property pure nothrow 498 { 499 public Container focusedControl() 500 { 501 return this.m_focusedControl; 502 } 503 504 public Control controlUnderMouse() 505 { 506 return this.m_controlUnderMouse; 507 } 508 } 509 }