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