1 ///
2 module nanogui.screen;
3 
4 import std.algorithm : min;
5 import std.experimental.logger : Logger;
6 
7 import arsd.nanovega;
8 public import gfm.math : vec2i;
9 
10 import nanogui.widget : Widget;
11 import nanogui.common : Vector2i, Vector2f, MouseButton, MouseAction, KeyAction, Cursor, NanoContext;
12 
13 class Screen : Widget
14 {
15 	import nanogui.window : Window;
16 
17 	this(int w, int h, long timestamp)
18 	{
19 		super(null);
20 		size = vec2i(w, h);
21 		mNeedToDraw = true;
22 		mLastInteraction = mTimestamp = timestamp;
23 		mCursor = Cursor.Arrow;
24 		mPixelRatio = 1.0;
25 	}
26 
27 	auto currTime() const { return mTimestamp; }
28 	void currTime(long value)
29 	{
30 		mTimestamp = value;
31 		auto elapsed = value - mLastInteraction;
32 		if (!mTooltipShown && elapsed > 5_000_000)
33 		{
34 			const widget = findWidget(mMousePos);
35 			if (widget && widget.tooltip.length)
36 				mNeedToDraw = true;
37 		}
38 	}
39 
40 	auto lastInteraction() { return mLastInteraction; }
41 
42 	override void draw(ref NanoContext ctx)
43 	{
44 		import arsd.simpledisplay;
45 
46 		// draw GLCanvas widgets to textures
47 		foreach(glcanvas; mGLCanvases[])
48 		{
49 			// Resize GLCanvas in non optimal way
50 			if (glcanvas.lastWidth  != glcanvas.width ||
51 			    glcanvas.lastHeight != glcanvas.height)
52 			{
53 				// This is resource consuming work that
54 				// can/should be avoided but considering
55 				// resizing seldom operation
56 				// it is appropriate currently
57 				glcanvas.lastWidth  = glcanvas.width;
58 				glcanvas.lastHeight = glcanvas.height;
59 				glcanvas.releaseBuffers;
60 				glcanvas.initBuffers;
61 			}
62 			glcanvas.mFbo.use;
63 			glViewport(0, 0, glcanvas.width, glcanvas.height);
64 			const clr = glcanvas.backgroundColor;
65 			glClearColor(clr[0], clr[1], clr[2], clr[3]);
66 			glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
67 			glcanvas.drawGL;
68 			glcanvas.mFbo.unuse();
69 		}
70 
71 		// draw the rest
72 		glViewport(0, 0, size.x, size.y);
73 		glClearColor(0., 0., 0., 0);
74 		glClear(glNVGClearFlags); // use NanoVega API to get flags for OpenGL call
75 		ctx.beginFrame(size.x, size.y); // begin rendering
76 		scope(exit)
77 		{
78 			if (ctx.inFrame)
79 				ctx.endFrame(); // and flush render queue on exit
80 		}
81 
82 		if (mNeedToPerfomLayout)
83 		{
84 			performLayout(ctx);
85 			needToPerfomLayout = false;
86 		}
87 		
88 		super.draw(ctx);
89 
90 		mNeedToDraw = false;
91 
92 		float elapsed = (mTimestamp - mLastInteraction)/10_000_000.0f;
93 
94 		if (elapsed > 0.5f)
95 		{
96 			/* Draw tooltips */
97 			const widget = findWidget(mMousePos);
98 			if (widget && widget.tooltip.length) {
99 				int tooltipWidth = 150;
100 
101 				float[4] bounds;
102 				ctx.fontFace("sans");
103 				ctx.fontSize(15.0f);
104 				NVGTextAlign algn;
105 				algn.left = true;
106 				algn.top = true;
107 				ctx.textAlign(algn);
108 				ctx.textLineHeight(1.1f);
109 				Vector2i pos = widget.absolutePosition() +
110 							   Vector2i(widget.width() / 2, widget.height() + 10);
111 
112 				ctx.textBounds(pos.x, pos.y,
113 								widget.tooltip, bounds);
114 				int h = cast(int) (bounds[2] - bounds[0]) / 2;
115 
116 				if (h > tooltipWidth / 2) {
117 					algn.center = true;
118 					algn.top = true;
119 					ctx.textAlign(algn);
120 					ctx.textBoxBounds(pos.x, pos.y, tooltipWidth,
121 									widget.tooltip, bounds);
122 
123 					h = cast(int)(bounds[2] - bounds[0]) / 2;
124 				}
125 				enum threshold = 0.8f;
126 				auto alpha = min(1.0, 2 * (elapsed - 0.5f)) * threshold;
127 				ctx.globalAlpha(alpha);
128 				mTooltipShown = (alpha > threshold - 0.01) ? true : false;
129 
130 				ctx.beginPath;
131 				ctx.fillColor(Color(0, 0, 0, 255));
132 				ctx.roundedRect(bounds[0] - 4 - h, bounds[1] - 4,
133 							   cast(int) (bounds[2] - bounds[0]) + 8,
134 							   cast(int) (bounds[3] - bounds[1]) + 8, 3);
135 
136 				int px = cast(int) ((bounds[2] + bounds[0]) / 2) - h;
137 				ctx.moveTo(px, bounds[1] - 10);
138 				ctx.lineTo(px + 7, bounds[1] + 1);
139 				ctx.lineTo(px - 7, bounds[1] + 1);
140 				ctx.fill();
141 
142 				ctx.fillColor(Color(255, 255, 255, 255));
143 				ctx.fontBlur(0.0f);
144 				ctx.textBox(pos.x - h, pos.y, tooltipWidth,
145 						   widget.tooltip);
146 			}
147 		}
148 		else
149 			mTooltipShown = false;
150 	}
151 
152 	bool mouseButtonCallbackEvent(MouseButton button, MouseAction action, int modifiers, long timestamp)
153 	{
154 		mNeedToDraw = true;
155 		mModifiers = modifiers;
156 		mLastInteraction = timestamp;
157 		try
158 		{
159 			if (mFocusPath.length > 1)
160 			{
161 				const window = cast(Window) (mFocusPath[mFocusPath.length - 2]);
162 				if (window && window.modal)
163 				{
164 					if (!window.contains(mMousePos))
165 						return false;
166 				}
167 			}
168 
169 			if (action == MouseAction.Press)
170 				mMouseState |= 1 << button;
171 			else
172 				mMouseState &= ~(1 << button);
173 
174 			const dropWidget = findWidget(mMousePos);
175 			if (mDragActive && action == MouseAction.Release &&
176 				dropWidget !is mDragWidget)
177 				mDragWidget.mouseButtonEvent(
178 					mMousePos - mDragWidget.parent.absolutePosition, button,
179 					false, mModifiers);
180 
181 			if (dropWidget !is null && dropWidget.cursor != mCursor)
182 				cursor = dropWidget.cursor;
183 
184 			if (action == MouseAction.Press && (button ==MouseButton.Left || button == MouseButton.Right)) {
185 				mDragWidget = findWidget(mMousePos);
186 				if (mDragWidget is this)
187 					mDragWidget = null;
188 				mDragActive = mDragWidget !is null;
189 				if (!mDragActive)
190 					updateFocus(null);
191 			} else {
192 				mDragActive = false;
193 				mDragWidget = null;
194 			}
195 
196 			return mouseButtonEvent(mMousePos, button, action == MouseAction.Press,
197 									mModifiers);
198 		}
199 		catch (Exception e)
200 		{
201 			import std.stdio : stderr;
202 			stderr.writeln("Caught exception in event handler: ", e.msg);
203 			return false;
204 		}
205 	}
206 
207 	/// Return the last observed mouse position value
208 	Vector2i mousePos() const { return mMousePos; }
209 
210 	final void updateFocus(Widget widget)
211 	{
212 		mNeedToDraw = true;
213 		foreach (w; mFocusPath)
214 		{
215 			if (!w.focused)
216 				continue;
217 			w.focusEvent(false);
218 		}
219 		mFocusPath.clear;
220 		Widget window;
221 		while (widget)
222 		{
223 			mFocusPath.insertBack(widget);
224 			if (cast(Window)(widget))
225 				window = widget;
226 			widget = widget.parent;
227 		}
228 		foreach_reverse(it; mFocusPath)
229 			it.focusEvent(true);
230 
231 		if (window)
232 			moveWindowToFront(cast(Window) window);
233 	}
234 
235 	bool cursorPosCallbackEvent(double x, double y, long last_interaction)
236 	{
237 		mNeedToDraw = true;
238 		auto p = Vector2i(cast(int) x, cast(int) y);
239 
240 		//#if defined(_WIN32) || defined(__linux__)
241 		//	p = (p.cast<float>() / mPixelRatio).cast<int>();
242 		//#endif
243 
244 		bool ret;
245 		mLastInteraction = last_interaction;
246 		try
247 		{
248 			p -= Vector2i(1, 2);
249 
250 			if (!mDragActive)
251 			{
252 				const widget = findWidget(p);
253 				if (widget !is null && widget !is this)
254 				{
255 					if (widget.cursor != mCursor)
256 						cursor = widget.cursor;
257 				}
258 				else
259 				{
260 					if (Cursor.Arrow != mCursor)
261 						cursor = Cursor.Arrow;
262 				}
263 			}
264 			else
265 			{
266 				ret = mDragWidget.mouseDragEvent(
267 					p - mDragWidget.parent.absolutePosition, p - mMousePos,
268 					mMouseState, mModifiers);
269 			}
270 
271 			if (!ret)
272 				ret = mouseMotionEvent(p, p - mMousePos, mMouseState, mModifiers);
273 
274 			mMousePos = p;
275 
276 			return ret;
277 		}
278 		catch (Exception e)
279 		{
280 			import std.stdio : stderr;
281 			stderr.writeln("Caught exception in event handler: ", e.msg);
282 			return false;
283 		}
284 	}
285 
286 	void moveWindowToFront(Window window) {
287 		// mChildren.erase(std::remove(mChildren.begin(), mChildren.end(), window), mChildren.end());
288 		{
289 			// non-idiomatic way to implement erase-remove idiom in dlang
290 			size_t i;
291 			foreach(_; mChildren)
292 			{
293 				if (mChildren[i] is window)
294 					break;
295 				i++;
296 			}
297 			if (i < mChildren.length)
298 			{
299 				foreach(j; i..mChildren.length-1)
300 					mChildren[j] = mChildren[j+1];
301 				mChildren.removeBack;
302 			}
303 		}
304 		mChildren.insertBack(window);
305 		/* Brute force topological sort (no problem for a few windows..) */
306 		bool changed = false;
307 		do {
308 			size_t baseIndex = 0;
309 			for (size_t index = 0; index < mChildren.length; ++index)
310 				if (mChildren[index] == window)
311 					baseIndex = index;
312 			changed = false;
313 			for (size_t index = 0; index < mChildren.length; ++index)
314 			{
315 				import nanogui.popup : Popup;
316 				Popup pw = cast(Popup) mChildren[index];
317 				if (pw && pw.parentWindow is window && index < baseIndex) {
318 					moveWindowToFront(pw);
319 					changed = true;
320 					break;
321 				}
322 			}
323 		} while (changed);
324 		mNeedToDraw = true;
325 	}
326 
327 	bool scrollCallbackEvent(double x, double y, long timestamp)
328 	{
329 		mLastInteraction = timestamp;
330 		try
331 		{
332 			if (mFocusPath.length > 1)
333 			{
334 				const window = cast(Window) mFocusPath[mFocusPath.length - 2];
335 				if (window && window.modal)
336 				{
337 					if (!window.contains(mMousePos))
338 						return false;
339 				}
340 			}
341 			return scrollEvent(mMousePos, Vector2f(x, y));
342 		}
343 		catch (Exception e)
344 		{
345 			import std.stdio : stderr;
346 			stderr.writeln("Caught exception in event handler: ", e.msg);
347 			return false;
348 		}
349 	}
350 
351 	override bool keyboardEvent(int key, int scancode, KeyAction action, int modifiers)
352 	{
353 		if (mFocusPath.length > 0)
354 		{
355 			foreach_reverse(w; mFocusPath)
356 			{
357 				if (w is this)
358 					continue;
359 				if (w.focused && w.keyboardEvent(key, scancode, action, modifiers))
360 					return true;
361 			}
362 		}
363 
364 		return false;
365 	}
366 
367 	override bool keyboardCharacterEvent(dchar codepoint)
368 	{
369 		if (mFocusPath.length)
370 		{
371 			foreach_reverse(w; mFocusPath)
372 			{
373 				if (w is this)
374 					continue;
375 				if (w.focused && w.keyboardCharacterEvent(codepoint))
376 					return true;
377 			}
378 		}
379 		return false;
380 	}
381 
382 	/// Window resize event handler
383 	bool resizeEvent(Vector2i size)
384 	{
385 		if (mResizeCallback) {
386 			mResizeCallback(size);
387 			mNeedToDraw = true;
388 			return true;
389 		}
390 		return false;
391 	}
392 
393 	Logger logger() { return null; };
394 
395 	/// Return the ratio between pixel and device coordinates (e.g. >= 2 on Mac Retina displays)
396 	float pixelRatio() const { return mPixelRatio; }
397 
398 	package bool needToDraw() const pure @safe nothrow { return mNeedToDraw; }
399 	// TODO add package qualifier at least but better to remove it completely
400 	void needToDraw(bool value) pure @safe nothrow { if (value) mNeedToDraw = value; }
401 	package bool needToPerfomLayout() const pure @safe nothrow { return mNeedToPerfomLayout; }
402 	package void needToPerfomLayout(bool value) pure @safe nothrow { if (value) mNeedToPerfomLayout = value; }
403 	package bool blinkingCursorIsVisibleNow() const pure @safe nothrow { return mBlinkingCursorVisible; }
404 
405     package void resetBlinkingCursor() @safe
406     {
407         import std.datetime : Clock;
408         mBlinkingCursorVisible = true;
409         mBlinkingCursorTimestamp = Clock.currTime.stdTime;
410     }
411 
412 package:
413 	import nanogui.glcanvas : GLCanvas;
414 
415 	void addGLCanvas(GLCanvas dw)
416 	{
417 		mGLCanvases.insert(dw);
418 	}
419 
420 	void removeGLCanvas(GLCanvas dw)
421 	{
422 		import std.algorithm : find;
423 		auto r = mGLCanvases[].find!((a,b)=>a is b)(this);
424 		mGLCanvases.linearRemove(r);
425 	}
426 
427 protected:
428 	import std.container.array : Array;
429 
430 	// should the cursor be visible now
431 	bool         mBlinkingCursorVisible;
432 	// the moment in time when the cursor has changed its blinking visibility
433 	long         mBlinkingCursorTimestamp;
434 	Vector2i     mMousePos;
435 	int          mModifiers;
436 	MouseButton  mMouseState;
437 	long         mLastInteraction;
438 	Array!Widget mFocusPath;
439 	bool         mDragActive;
440 	Widget       mDragWidget;
441 	bool         mNeedToDraw, mNeedToPerfomLayout;
442 	long         mTimestamp;
443 	bool         mTooltipShown;
444 	Cursor       mCursor;
445 	float        mPixelRatio;
446 	void delegate(Vector2i) mResizeCallback;
447 	Array!GLCanvas mGLCanvases;
448 }