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