1 ///
2 module nanogui.window;
3 
4 /*
5 	nanogui/window.d -- Top-level window widget
6 
7 	NanoGUI was developed by Wenzel Jakob <wenzel.jakob@epfl.ch>.
8 	The widget drawing code is based on the NanoVG demo application
9 	by Mikko Mononen.
10 
11 	All rights reserved. Use of this source code is governed by a
12 	BSD-style license that can be found in the LICENSE.txt file.
13 */
14 
15 import nanogui.widget;
16 import nanogui.common : Vector2i, Vector2f, MouseButton;
17 
18 /**
19  * Top-level window widget.
20  */
21 class Window : Widget
22 {
23 public:
24 	this(Widget parent, string title = "Untitled", bool resizable = false)
25 	{
26 		super(parent);
27 		mTitle = title;
28 		mButtonPanel = null;
29 		mModal = false;
30 		mDrag = false;
31 		mResizeDir = Vector2i();
32 		import std.algorithm : max;
33 		mMinSize = Vector2i(3*mTheme.mResizeAreaOffset, max(3*mTheme.mResizeAreaOffset, mTheme.mWindowHeaderHeight + mTheme.mResizeAreaOffset));
34 		mResizable = resizable;
35 	}
36 
37 	/// Return the window title
38 	final string title() const { return mTitle; }
39 	/// Set the window title
40 	final void title(string title) { mTitle = title; }
41 
42 	/// Is this a model dialog?
43 	final bool modal() const { return mModal; }
44 	/// Set whether or not this is a modal dialog
45 	final void modal(bool modal) { mModal = modal; }
46      /// Is this a resizable window?
47     bool resizable() const { return mResizable; }
48     /// Set whether or not this window is resizable
49     void resizable(bool value) { mResizable = value; }
50 
51 	/// Return the panel used to house window buttons
52 	final Widget buttonPanel()
53 	{
54 		import nanogui.layout : BoxLayout, Orientation, Alignment;
55 		if (!mButtonPanel) {
56 			mButtonPanel = new Widget(this);
57 			mButtonPanel.layout(new BoxLayout(Orientation.Horizontal, Alignment.Middle, 0, 4));
58 		}
59 		return mButtonPanel;
60 	}
61 
62 	/// Dispose the window
63 	final void dispose();
64 
65 	/// Center the window in the current `Screen`
66 	final void center();
67 
68 	/// Draw the window
69 	override void draw(ref NanoContext ctx)
70 	{
71 		assert (mTheme);
72 		int ds = mTheme.mWindowDropShadowSize, cr = mTheme.mWindowCornerRadius;
73 		int hh = mTheme.mWindowHeaderHeight;
74 
75 		/* Draw window */
76 		ctx.save;
77 		ctx.beginPath;
78 		ctx.roundedRect(mPos.x, mPos.y, mSize.x, mSize.y, cr);
79 
80 		ctx.fillColor(mMouseFocus ? mTheme.mWindowFillFocused
81 									  : mTheme.mWindowFillUnfocused);
82 		ctx.fill;
83 
84 
85 		/* Draw a drop shadow */
86 		NVGPaint shadowPaint = ctx.boxGradient(
87 			mPos.x, mPos.y, mSize.x, mSize.y, cr*2, ds*2,
88 			mTheme.mDropShadow, mTheme.mTransparent);
89 
90 		ctx.save;
91 		ctx.resetScissor;
92 		ctx.beginPath;
93 		ctx.rect(mPos.x-ds,mPos.y-ds, mSize.x+2*ds, mSize.y+2*ds);
94 		ctx.roundedRect(mPos.x, mPos.y, mSize.x, mSize.y, cr);
95 		ctx.pathWinding(NVGSolidity.Hole);
96 		ctx.fillPaint(shadowPaint);
97 		ctx.fill;
98 		ctx.restore;
99 
100 		if (mTitle.length)
101 		{
102 			/* Draw header */
103 			NVGPaint headerPaint = ctx.linearGradient(
104 				mPos.x, mPos.y, mPos.x,
105 				mPos.y + hh,
106 				mTheme.mWindowHeaderGradientTop,
107 				mTheme.mWindowHeaderGradientBot);
108 
109 			ctx.beginPath;
110 			ctx.roundedRect(mPos.x, mPos.y, mSize.x, hh, cr);
111 
112 			ctx.fillPaint(headerPaint);
113 			ctx.fill;
114 
115 			ctx.beginPath;
116 			ctx.roundedRect(mPos.x, mPos.y, mSize.x, hh, cr);
117 			ctx.strokeColor(mTheme.mWindowHeaderSepTop);
118 
119 			ctx.save;
120 			ctx.intersectScissor(mPos.x, mPos.y, mSize.x, 0.5f);
121 			ctx.stroke;
122 			ctx.restore;
123 
124 			ctx.beginPath;
125 			ctx.moveTo(mPos.x + 0.5f, mPos.y + hh - 1.5f);
126 			ctx.lineTo(mPos.x + mSize.x - 0.5f, mPos.y + hh - 1.5);
127 			ctx.strokeColor(mTheme.mWindowHeaderSepBot);
128 			ctx.stroke;
129 
130 			ctx.fontSize(18.0f);
131 			ctx.fontFace("sans-bold");
132 			auto algn = NVGTextAlign();
133 			algn.center = true;
134 			algn.middle = true;
135 			ctx.textAlign(algn);
136 
137 			ctx.fontBlur(2);
138 			ctx.fillColor(mTheme.mDropShadow);
139 			ctx.text(mPos.x + mSize.x / 2,
140 					mPos.y + hh / 2, mTitle);
141 
142 			ctx.fontBlur(0);
143 			ctx.fillColor(mFocused ? mTheme.mWindowTitleFocused
144 									   : mTheme.mWindowTitleUnfocused);
145 			ctx.text(mPos.x + mSize.x / 2, mPos.y + hh / 2 - 1,
146 					mTitle);
147 		}
148 
149 		ctx.restore;
150 		const old = ctx.mouse;
151 		if (window.contains(ctx.mouse))
152 			ctx.mouse -= window.absolutePosition;
153 		else
154 			ctx.mouse = Vector2i(-1, -1);
155 		scope(exit) ctx.mouse = old;
156 		Widget.draw(ctx);
157 	}
158 	/// Handle window drag events
159 	override bool mouseDragEvent(Vector2i p, Vector2i rel, MouseButton button, int modifiers)
160 	{
161 		import std.algorithm : min, max;
162 		import gfm.math : maxByElem;
163 
164 		if (mDrag && (button & (1 << MouseButton.Left)) != 0) {
165 			mPos += rel;
166 			{
167 				// mPos = mPos.cwiseMax(Vector2i::Zero());
168 				mPos[0] = max(mPos[0], 0);
169 				mPos[1] = max(mPos[1], 0);
170 			}
171 			{
172 				// mPos = mPos.cwiseMin(parent()->size() - mSize);
173 				auto other = parent.size - mSize;
174 				mPos[0] = min(mPos[0], other[0]);
175 				mPos[1] = min(mPos[1], other[1]);
176 			}
177 			return true;
178 		}
179 		else if (mResizable && mResize && (button & (1 << MouseButton.Left)) != 0)
180 		{
181 			const lowerRightCorner = mPos + mSize;
182 			const upperLeftCorner = mPos;
183 			bool resized = false;
184 
185 
186 			if (mResizeDir.x == 1) {
187 				if ((rel.x > 0 && p.x >= lowerRightCorner.x) || (rel.x < 0)) {
188 					mSize.x += rel.x;
189 					resized = true;
190 				}
191 			} else if (mResizeDir.x == -1) {
192 				if ((rel.x < 0 && p.x <= upperLeftCorner.x) ||
193 						(rel.x > 0)) {
194 					mSize.x += -rel.x;
195 					mSize = mSize.maxByElem(mMinSize);
196 					mPos = lowerRightCorner - mSize;
197 					resized = true;
198 				}
199 			}
200 
201 			if (mResizeDir.y == 1) {
202 				if ((rel.y > 0 && p.y >= lowerRightCorner.y) || (rel.y < 0)) {
203 					mSize.y += rel.y;
204 					resized = true;
205 				}
206 			}
207 			mSize = mSize.maxByElem(mMinSize);
208 			if (resized)
209 				screen.needToPerfomLayout = true;
210 			return true;
211 		}
212 		return false;
213 	}
214 	/// Handle a mouse motion event (default implementation: propagate to children)
215 	override bool mouseMotionEvent(const Vector2i p, const Vector2i rel, MouseButton button, int modifiers)
216 	{
217 		import nanogui.common : Cursor;
218 
219 		if (Widget.mouseMotionEvent(p, rel, button, modifiers))
220 			return true;
221 
222 		if (mResizable && mFixedSize.x == 0 && checkHorizontalResize(p) != 0)
223 		{
224 			mCursor = Cursor.HResize;
225 		}
226 		else if (mResizable && mFixedSize.y == 0 && checkVerticalResize(p) != 0)
227 		{
228 			mCursor = Cursor.VResize;
229 		}
230 		else
231 		{
232 			mCursor = Cursor.Arrow;
233 		}
234 		return false;
235 	}
236 
237 	/// Handle mouse events recursively and bring the current window to the top
238 	override bool mouseButtonEvent(Vector2i p, MouseButton button, bool down, int modifiers)
239 	{
240 		if (super.mouseButtonEvent(p, button, down, modifiers))
241 			return true;
242 
243 		if (button == MouseButton.Left)
244 		{
245 			mDrag = down && (p.y - mPos.y) < mTheme.mWindowHeaderHeight;
246 			mResize = false;
247 			if (mResizable && !mDrag && down)
248 			{
249 				mResizeDir.x = (mFixedSize.x == 0) ? checkHorizontalResize(p) : 0;
250 				mResizeDir.y = (mFixedSize.y == 0) ? checkVerticalResize(p) : 0;
251 				mResize = mResizeDir.x != 0 || mResizeDir.y != 0;
252 			}
253 			return true;
254 		}
255 		return false;
256 	}
257 	/// Accept scroll events and propagate them to the widget under the mouse cursor
258 	override bool scrollEvent(Vector2i p, Vector2f rel)
259 	{
260 		Widget.scrollEvent(p, rel);
261 		return true;
262 	}
263 
264 	/// Compute the preferred size of the widget
265 	override Vector2i preferredSize(NanoContext ctx) const
266 	{
267 		if (mResizable)
268 			return mSize;
269 
270 		Vector2i result = Widget.preferredSize(ctx, mButtonPanel);
271 
272 		ctx.fontSize(18.0f);
273 		ctx.fontFace("sans-bold");
274 		float[4] bounds;
275 		ctx.textBounds(0, 0, mTitle, bounds);
276 
277 		if (result.x < bounds[2]-bounds[0] + 20)
278 			result.x = cast(int) (bounds[2]-bounds[0] + 20);
279 		if (result.y < bounds[3]-bounds[1])
280 			result.y = cast(int) (bounds[3]-bounds[1]);
281 
282 		return result;
283 	}
284 	/// Invoke the associated layout generator to properly place child widgets, if any
285 	override void performLayout(NanoContext ctx)
286 	{
287 		if (!mButtonPanel) {
288 			Widget.performLayout(ctx);
289 		} else {
290 			mButtonPanel.visible(false);
291 			Widget.performLayout(ctx);
292 			foreach (w; mButtonPanel.children) {
293 				w.fixedSize(Vector2i(22, 22));
294 				w.fontSize(15);
295 			}
296 			mButtonPanel.visible(true);
297 			mButtonPanel.size(Vector2i(width(), 22));
298 			mButtonPanel.position(Vector2i(width() - (mButtonPanel.preferredSize(ctx).x + 5), 3));
299 			mButtonPanel.performLayout(ctx);
300 		}
301 	}
302 //override void save(Serializer &s) const;
303 //override bool load(Serializer &s);
304 public:
305 	/// Internal helper function to maintain nested window position values; overridden in \ref Popup
306 	void refreshRelativePlacement()
307 	{
308 		/* Overridden in \ref Popup */
309 	}
310 protected:
311 	int checkHorizontalResize(const Vector2i mousePos)
312 	{
313 		const offset = mTheme.mResizeAreaOffset;
314 		const lowerRightCorner = absolutePosition + size;
315 		const headerLowerLeftCornerY = absolutePosition.y + mTheme.mWindowHeaderHeight;
316 
317 		if (mousePos.y > headerLowerLeftCornerY &&
318 			mousePos.x <= absolutePosition.x + offset &&
319 			mousePos.x >= absolutePosition.x)
320 		{
321 			return -1;
322 		}
323 		else if (mousePos.y > headerLowerLeftCornerY && 
324 			mousePos.x >= lowerRightCorner.x - offset &&
325 			mousePos.x <= lowerRightCorner.x)
326 		{
327 			return 1;
328 		}
329 
330 		return 0;
331 	}
332 	int checkVerticalResize(const Vector2i mousePos)
333 	{
334 		const offset = mTheme.mResizeAreaOffset;
335 		const lowerRightCorner = absolutePosition + size;
336 
337 		// Do not check for resize area on top of the window. It is to prevent conflict drag and resize event.
338 		if (mousePos.y >= lowerRightCorner.y - offset && mousePos.y <= lowerRightCorner.y)
339 		{
340 			return 1;
341 		}
342 
343 		return 0;
344 	}
345 
346 	string mTitle;
347 	Widget mButtonPanel;
348 	bool mModal;
349 	bool mDrag;
350 	bool mResize;
351 	Vector2i mResizeDir;
352 	Vector2i mMinSize;
353 	bool mResizable;
354 }