1 module pixelperfectengine.concrete.windowhandler; 2 3 public import pixelperfectengine.concrete.interfaces; 4 public import pixelperfectengine.concrete.window; 5 public import pixelperfectengine.concrete.types; 6 public import pixelperfectengine.concrete.popup; 7 public import pixelperfectengine.concrete.dialogs; 8 9 public import pixelperfectengine.system.input.interfaces; 10 11 public import pixelperfectengine.graphics.layers : ISpriteLayer; 12 13 import collections.linkedlist; 14 import pixelperfectengine.system.etc : cmpObjPtr; 15 16 import bindbc.sdl.bind.sdlmouse; 17 import std.math : nearbyint; 18 19 /** 20 * Handles windows as well as PopUpElements. 21 */ 22 public class WindowHandler : InputListener, MouseListener, PopUpHandler { 23 alias WindowSet = LinkedList!(Window, false, "a is b"); 24 alias PopUpSet = LinkedList!(PopUpElement, false, "a is b"); 25 protected WindowSet windows; 26 protected PopUpSet popUpElements; 27 private int numOfPopUpElements; 28 //private int[] priorities; 29 protected int screenWidth, screenHeight, rasterWidth, rasterHeight, moveX, moveY, mouseX, mouseY; 30 protected double mouseConvX, mouseConvY; 31 //public Bitmap16Bit[wchar] basicFont, altFont, alarmFont; 32 ///Sets the default style for the windowhandler. 33 ///If null, the global default will be used instead. 34 public StyleSheet defaultStyle; 35 //public Bitmap16Bit[int] styleBrush; 36 protected ABitmap background; 37 ///A window that is used for top-level stuff, like elements in the background, or an integrated window. 38 protected Window baseWindow; 39 ///The type of the current cursor 40 protected CursorType cursor; 41 ///SDL cursor pointer to operate it 42 protected SDL_Cursor* sdlCursor; 43 private ISpriteLayer spriteLayer; 44 //private Window windowToMove; 45 protected MouseEventReceptor dragEventSrc; 46 private PopUpElement dragEventDestPopUp; 47 //private ubyte lastMouseButton; 48 /** 49 * Default CTOR. 50 * sW and sH set the screen width and height. 51 * rW and rH set the raster width and height. 52 * ISpriteLayer sets the SpriteLayer, that will display the windows and popups as sprites. 53 */ 54 public this(int sW, int sH, int rW, int rH, ISpriteLayer sl) { 55 screenWidth = sW; 56 screenHeight = sH; 57 rasterWidth = rW; 58 rasterHeight = rH; 59 spriteLayer = sl; 60 mouseConvX = cast(double)screenWidth / rasterWidth; 61 mouseConvY = cast(double)screenHeight / rasterHeight; 62 } 63 /** 64 * Sets the cursor to the given type. 65 */ 66 public CursorType setCursor(CursorType type) { 67 cursor = type; 68 sdlCursor = SDL_CreateSystemCursor(cast(SDL_SystemCursor)cursor); 69 SDL_SetCursor(sdlCursor); 70 return cursor; 71 } 72 /** 73 * Returns the current cursor type. 74 */ 75 public CursorType getCursor() @nogc @safe pure nothrow { 76 return cursor; 77 } 78 /** 79 * Adds a window to the handler. 80 */ 81 public void addWindow(Window w) @trusted { 82 windows.put(w); 83 w.addHandler(this); 84 w.draw(); 85 setWindowToTop(w); 86 } 87 88 /** 89 * Adds a DefaultDialog as a message box 90 */ 91 public void message(dstring title, dstring message, int width = 256) { 92 import pixelperfectengine.concrete.dialogs.defaultdialog; 93 StyleSheet ss = getStyleSheet(); 94 dstring[] formattedMessage = ss.getChrFormatting("label").font.breakTextIntoMultipleLines(message, width - 95 ss.drawParameters["WindowLeftPadding"] - ss.drawParameters["WindowRightPadding"]); 96 int height = cast(int)(formattedMessage.length * (ss.getChrFormatting("label").font.size + 97 ss.drawParameters["TextSpacingTop"] + ss.drawParameters["TextSpacingBottom"])); 98 height += ss.drawParameters["WindowTopPadding"] + ss.drawParameters["WindowBottomPadding"] + 99 ss.drawParameters["ComponentHeight"]; 100 Coordinate c = Coordinate(mouseX - width / 2, mouseY - height / 2, mouseX + width / 2, mouseY + height / 2); 101 //Text title0 = new Text(title, ss.getChrFormatting("windowHeader")); 102 addWindow(new DefaultDialog(c, null, title, formattedMessage)); 103 } 104 /** 105 * Adds a background. 106 */ 107 public void addBackground(ABitmap b) { 108 background = b; 109 spriteLayer.addSprite(background, 65_536, 0, 0); 110 } 111 /** 112 * Returns the window priority or -1 if the window can't be found. 113 */ 114 private int whichWindow(Window w) @safe pure nothrow { 115 try 116 return cast(int)windows.which(w); 117 catch (Exception e) 118 return -1; 119 } 120 /** 121 * Sets sender to be top priority. 122 */ 123 public void setWindowToTop(Window w) { 124 windows[0].focusTaken(); 125 sizediff_t pri = whichWindow(w); 126 windows.setAsFirst(pri); 127 updateSpriteOrder(); 128 windows[0].focusGiven(); 129 } 130 /** 131 * Updates the sprite order by removing everything, then putting them back again. 132 */ 133 protected void updateSpriteOrder() { 134 spriteLayer.clear(); 135 for (int i ; i < windows.length ; i++) 136 spriteLayer.addSprite(windows[i].getOutput, i, windows[i].getPosition); 137 if (background) spriteLayer.addSprite(background, 65_536, 0, 0); 138 if (baseWindow) spriteLayer.addSprite(baseWindow.getOutput, 65_535, 0, 0); 139 } 140 /** 141 * Returns the default stylesheet. 142 */ 143 public StyleSheet getStyleSheet() { 144 if (defaultStyle) 145 return defaultStyle; 146 else 147 return globalDefaultStyle; 148 } 149 /** 150 * Closes the given window. 151 * 152 * NOTE: The closed window should be dereferenced in other places in order to be deallocated by the GC. If not, 153 * then it can be used to restore the window without creating a new one, potentially saving it's states. 154 */ 155 public void closeWindow(Window sender) { 156 const int p = whichWindow(sender); 157 windows.remove(p); 158 159 updateSpriteOrder(); 160 } 161 162 /** 163 * Initializes drag event. 164 * Used to avoid issues from stray mouse release, etc. 165 */ 166 public void initDragEvent(MouseEventReceptor dragEventSrc) @safe nothrow { 167 this.dragEventSrc = dragEventSrc; 168 } 169 /** 170 * Updates the sender's coordinates. 171 */ 172 public void updateWindowCoord(Window sender) @safe nothrow { 173 const int n = whichWindow(sender); 174 spriteLayer.replaceSprite(sender.getOutput(), n, sender.getPosition()); 175 } 176 //implementation of the MouseListener interface starts here 177 /** 178 * Called on mouse click events. 179 */ 180 public void mouseClickEvent(MouseEventCommons mec, MouseClickEvent mce) { 181 mce.x = cast(int)(mce.x / mouseConvX); 182 mce.y = cast(int)(mce.y / mouseConvY); 183 if (!mce.state && dragEventSrc) { 184 dragEventSrc.passMCE(mec, mce); 185 dragEventSrc = null; 186 } 187 if (numOfPopUpElements < 0) { 188 foreach (PopUpElement pe ; popUpElements) { 189 if (pe.getPosition().isBetween(mce.x, mce. y)) { 190 pe.passMCE(mec, mce); 191 return; 192 } 193 } 194 if (mce.state) { 195 removeAllPopUps(); 196 return; 197 } 198 } else if (mce.state) { 199 foreach (Window w ; windows) { 200 const Box pos = w.getPosition(); 201 if (pos.isBetween(mce.x, mce.y)) { 202 if (!w.active && mce.state) { //If window is not active, then the window order must be reset 203 //windows[0].focusTaken(); 204 setWindowToTop(w); 205 } 206 w.passMCE(mec, mce); 207 dragEventSrc = w; 208 return; 209 } 210 } 211 } 212 if (baseWindow) baseWindow.passMCE(mec, mce); 213 } 214 /** 215 * Called on mouse wheel events. 216 */ 217 public void mouseWheelEvent(MouseEventCommons mec, MouseWheelEvent mwe) { 218 if (numOfPopUpElements < 0) popUpElements[$ - 1].passMWE(mec, mwe); 219 else if (windows.length) windows[0].passMWE(mec, mwe); 220 else if (baseWindow) baseWindow.passMWE(mec, mwe); 221 } 222 /** 223 * Called on mouse motion events. 224 */ 225 public void mouseMotionEvent(MouseEventCommons mec, MouseMotionEvent mme) { 226 mme.relX = cast(int)nearbyint(mme.relX / mouseConvX); 227 mme.relY = cast(int)nearbyint(mme.relY / mouseConvY); 228 mme.x = cast(int)nearbyint(mme.x / mouseConvX); 229 mme.y = cast(int)nearbyint(mme.y / mouseConvY); 230 mouseX = mme.x; 231 mouseY = mme.y; 232 if (dragEventSrc) { 233 dragEventSrc.passMME(mec, mme); 234 return; 235 } 236 if (numOfPopUpElements < 0) { 237 popUpElements[$ - 1].passMME(mec, mme); 238 return; 239 } 240 foreach (Window key; windows) { 241 if (key.getPosition.isBetween(mme.x, mme.y)) { 242 key.passMME(mec, mme); 243 return; 244 } 245 } 246 if (baseWindow) baseWindow.passMME(mec, mme); 247 } 248 /** 249 * Sets the BaseWindow to the given object. 250 * 251 * The base window has no priority and will reside forever in the background. Can be used for various ends. 252 */ 253 public Window setBaseWindow(Window w) @safe nothrow { 254 import pixelperfectengine.graphics.layers.base : RenderingMode; 255 w.addHandler(this); 256 baseWindow = w; 257 spriteLayer.addSprite(w.getOutput, 65_535, w.getPosition); 258 spriteLayer.setSpriteRenderingMode(65_535, RenderingMode.Blitter); 259 return baseWindow; 260 } 261 262 263 /** 264 * Replaces the window's old sprite in the spritelayer's display list with the new one. 265 * 266 * Needed to be called each time the window's sprite is being replaced, or else the previous one will be continued to 267 * be displayed without any updates. 268 */ 269 public void refreshWindow(Window sender) @safe { 270 const int n = whichWindow(sender); 271 spriteLayer.replaceSprite(windows[n].getOutput, n, windows[n].getPosition); 272 } 273 /** 274 * Adds a popup element and moves it to the current cursor position. 275 */ 276 public void addPopUpElement(PopUpElement p){ 277 popUpElements.put(p); 278 p.addParent(this); 279 p.draw; 280 /+mouseX -= (p.getPosition.width/2); 281 mouseY -= (p.getPosition.height/2);+/ 282 283 p.move(mouseX, mouseY); 284 numOfPopUpElements--; 285 spriteLayer.addSprite(p.getOutput(), numOfPopUpElements, p.getPosition()); 286 287 } 288 public void addPopUpElement(PopUpElement p, int x, int y){ 289 popUpElements.put(p); 290 p.addParent(this); 291 p.draw; 292 p.move(x, y); 293 numOfPopUpElements--; 294 spriteLayer.addSprite(p.getOutput,numOfPopUpElements, x, y); 295 } 296 private void removeAllPopUps(){ 297 for ( ; numOfPopUpElements < 0 ; numOfPopUpElements++){ 298 spriteLayer.removeSprite(numOfPopUpElements); 299 } 300 /+foreach (key ; popUpElements) { 301 key.destroy; 302 }+/ 303 ///Why didn't I add a method to clear linked lists? (slams head into wall) 304 popUpElements = PopUpSet(new PopUpElement[](0)); 305 /+while (popUpElements.length) { 306 popUpElements.remove(0); 307 }+/ 308 } 309 private void removeTopPopUp(){ 310 311 spriteLayer.removeSprite(numOfPopUpElements++); 312 313 popUpElements.remove(popUpElements.length - 1); 314 } 315 public StyleSheet getDefaultStyleSheet(){ 316 return defaultStyle; 317 } 318 public void endPopUpSession(PopUpElement p){ 319 removeAllPopUps(); 320 } 321 public void closePopUp(PopUpElement p){ 322 popUpElements.removeByElem(p); 323 } 324 325 326 /*public Coordinate getAbsolutePosition(PopUpElement sender){ 327 for(int i ; i < popUpElements.length ; i++){ 328 if(popUpElements[i] = sender){ 329 330 } 331 } 332 return Coordinate(); 333 }*/ 334 //implementation of the `InputListener` interface 335 /** 336 * Called when a keybinding event is generated. 337 * The `id` should be generated from a string, usually the name of the binding. 338 * `code` is a duplicate of the code used for fast lookup of the binding, which also contains other info (deviceID, etc). 339 * `timestamp` is the time lapsed since the start of the program, can be used to measure time between keypresses. 340 * NOTE: Hat events on joysticks don't generate keyReleased events, instead they generate keyPressed events on release. 341 */ 342 public void keyEvent(uint id, BindingCode code, uint timestamp, bool isPressed) { 343 if (isPressed) { 344 345 } 346 } 347 /** 348 * Called when an axis is being operated. 349 * The `id` should be generated from a string, usually the name of the binding. 350 * `code` is a duplicate of the code used for fast lookup of the binding, which also contains other info (deviceID, etc). 351 * `timestamp` is the time lapsed since the start of the program, can be used to measure time between keypresses. 352 * `value` is the current position of the axis normalized between -1.0 and +1.0 for joysticks, and 0.0 and +1.0 for analog 353 * triggers. 354 */ 355 public void axisEvent(uint id, BindingCode code, uint timestamp, float value) { 356 357 } 358 } 359 360