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