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 	/// Handle mouse events recursively and bring the current window to the top
237 	override bool mouseButtonEvent(Vector2i p, MouseButton button, bool down, int modifiers)
238 	{
239 		if (super.mouseButtonEvent(p, button, down, modifiers))
240 			return true;
241 		if (button == MouseButton.Left)
242 		{
243 			mDrag = down && (p.y - mPos.y) < mTheme.mWindowHeaderHeight;
244 			mResize = false;
245 			if (mResizable && !mDrag && down)
246 			{
247 				mResizeDir.x = (mFixedSize.x == 0) ? checkHorizontalResize(p) : 0;
248 				mResizeDir.y = (mFixedSize.y == 0) ? checkVerticalResize(p) : 0;
249 				mResize = mResizeDir.x != 0 || mResizeDir.y != 0;
250 			}
251 			return true;
252 		}
253 		return false;
254 	}
255 	/// Accept scroll events and propagate them to the widget under the mouse cursor
256 	override bool scrollEvent(Vector2i p, Vector2f rel)
257 	{
258 		Widget.scrollEvent(p, rel);
259 		return true;
260 	}
261 
262 	/// Compute the preferred size of the widget
263 	override Vector2i preferredSize(NanoContext ctx) const
264 	{
265 		if (mResizable)
266 			return mSize;
267 
268 		Vector2i result = Widget.preferredSize(ctx, mButtonPanel);
269 
270 		ctx.fontSize(18.0f);
271 		ctx.fontFace("sans-bold");
272 		float[4] bounds;
273 		ctx.textBounds(0, 0, mTitle, bounds);
274 
275 		if (result.x < bounds[2]-bounds[0] + 20)
276 			result.x = cast(int) (bounds[2]-bounds[0] + 20);
277 		if (result.y < bounds[3]-bounds[1])
278 			result.y = cast(int) (bounds[3]-bounds[1]);
279 
280 		return result;
281 	}
282 	/// Invoke the associated layout generator to properly place child widgets, if any
283 	override void performLayout(NanoContext ctx)
284 	{
285 		if (!mButtonPanel) {
286 			Widget.performLayout(ctx);
287 		} else {
288 			mButtonPanel.visible(false);
289 			Widget.performLayout(ctx);
290 			foreach (w; mButtonPanel.children) {
291 				w.fixedSize(Vector2i(22, 22));
292 				w.fontSize(15);
293 			}
294 			mButtonPanel.visible(true);
295 			mButtonPanel.size(Vector2i(width(), 22));
296 			mButtonPanel.position(Vector2i(width() - (mButtonPanel.preferredSize(ctx).x + 5), 3));
297 			mButtonPanel.performLayout(ctx);
298 		}
299 	}
300 //override void save(Serializer &s) const;
301 //override bool load(Serializer &s);
302 public:
303 	/// Internal helper function to maintain nested window position values; overridden in \ref Popup
304 	void refreshRelativePlacement()
305 	{
306 		/* Overridden in \ref Popup */
307 	}
308 protected:
309 	int checkHorizontalResize(const Vector2i mousePos)
310 	{
311 		const offset = mTheme.mResizeAreaOffset;
312 		const lowerRightCorner = absolutePosition + size;
313 		const headerLowerLeftCornerY = absolutePosition.y + mTheme.mWindowHeaderHeight;
314 
315 		if (mousePos.y > headerLowerLeftCornerY &&
316 			mousePos.x <= absolutePosition.x + offset &&
317 			mousePos.x >= absolutePosition.x)
318 		{
319 			return -1;
320 		}
321 		else if (mousePos.y > headerLowerLeftCornerY && 
322 			mousePos.x >= lowerRightCorner.x - offset &&
323 			mousePos.x <= lowerRightCorner.x)
324 		{
325 			return 1;
326 		}
327 
328 		return 0;
329 	}
330 	int checkVerticalResize(const Vector2i mousePos)
331 	{
332 		const offset = mTheme.mResizeAreaOffset;
333 		const lowerRightCorner = absolutePosition + size;
334 
335 		// Do not check for resize area on top of the window. It is to prevent conflict drag and resize event.
336 		if (mousePos.y >= lowerRightCorner.y - offset && mousePos.y <= lowerRightCorner.y)
337 		{
338 			return 1;
339 		}
340 
341 		return 0;
342 	}
343 
344 	string mTitle;
345 	Widget mButtonPanel;
346 	bool mModal;
347 	bool mDrag;
348 	bool mResize;
349 	Vector2i mResizeDir;
350 	Vector2i mMinSize;
351 	bool mResizable;
352 }