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