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