1 ///
2 module nanogui.experimental.treeview;
3 
4 /*
5 	NanoGUI was developed by Wenzel Jakob <wenzel.jakob@epfl.ch>.
6 	The widget drawing code is based on the NanoVG demo application
7 	by Mikko Mononen.
8 
9 	All rights reserved. Use of this source code is governed by a
10 	BSD-style license that can be found in the LICENSE.txt file.
11 */
12 
13 import nanogui.widget;
14 import nanogui.common : Vector2i, Vector2f, MouseButton;
15 
16 /**
17  * Tree view widget.
18  *
19  * Remarks:
20  *     This class overrides `nanogui.Widget.mIconExtraScale` to be `1.2f`,
21  *     which affects all subclasses of this Widget.  Subclasses must explicitly
22  *     set a different value if needed (e.g., in their constructor).
23  */
24 class TreeView(TreeModel) : Widget
25 {
26 public:
27 	/**
28 	 * Adds a TreeView to the specified `parent`.
29 	 *
30 	 * Params:
31 	 *     parent   = The Widget to add this TreeView to.
32 	 *     caption  = The caption text of the TreeView (default `"Untitled"`).
33 	 *     callback = If provided, the callback to execute when the TreeView is 
34 	 *     checked or unchecked.  Default parameter function does nothing.  See
35 	 *     `nanogui.TreeView.mPushed` for the difference between "pushed"
36 	 *     and "checked".
37 	 */
38 	this(Widget parent, const string caption, TreeModel model, void delegate(bool) callback)
39 	{
40 		super(parent);
41 		mCaption = caption;
42 		mPushed = false;
43 		mChecked = false;
44 		mCallback = callback;
45 		mIconExtraScale = 1.2f;// widget override
46 
47 		this.model = model;
48 	}
49 
50 	/// The caption of this TreeView.
51 	final string caption() const { return mCaption; }
52 
53 	/// Sets the caption of this TreeView.
54 	final void caption(string caption) { mCaption = caption; }
55 
56 	/// Whether or not this TreeView is currently checked.
57 	final bool checked() const { return mChecked; }
58 
59 	/// Sets whether or not this TreeView is currently checked.
60 	final void checked(bool checked) { mChecked = checked; }
61 
62 	/// Whether or not this TreeView is currently pushed.  See `nanogui.TreeView.mPushed`.
63 	final bool pushed() const { return mPushed; }
64 
65 	/// Sets whether or not this TreeView is currently pushed.  See `nanogui.TreeView.mPushed`.
66 	final void pushed(bool pushed) { mPushed = pushed; }
67 
68 	/// Returns the current callback of this TreeView.
69 	final void delegate(bool) callback() const { return mCallback; }
70 
71 	/// Sets the callback to be executed when this TreeView is checked / unchecked.
72 	final void callback(void delegate(bool) callback) { mCallback = callback; }
73 
74 	/**
75 	 * The mouse button callback will return `true` when all three conditions are met:
76 	 *
77 	 * 1. This TreeView is "enabled" (see `nanogui.Widget.mEnabled`).
78 	 * 2. `p` is inside this TreeView.
79 	 * 3. `button` is `MouseButton.Left`.
80 	 *
81 	 * Since a mouse button event is issued for both when the mouse is pressed, as well
82 	 * as released, this function sets `nanogui.TreeView.mPushed` to `true` when
83 	 * parameter `down == true`.  When the second event (`down == false`) is fired,
84 	 * `nanogui.TreeView.mChecked` is inverted and `nanogui.TreeView.mCallback`
85 	 * is called.
86 	 *
87 	 * That is, the callback provided is only called when the mouse button is released,
88 	 * **and** the click location remains within the TreeView boundaries.  If the user
89 	 * clicks on the TreeView and releases away from the bounds of the TreeView,
90 	 * `nanogui.TreeView.mPushed` is simply set back to `false`.
91 	 */
92 	override bool mouseButtonEvent(Vector2i p, MouseButton button, bool down, int modifiers)
93 	{
94 		super.mouseButtonEvent(p, button, down, modifiers);
95 		if (!mEnabled)
96 			return false;
97 
98 		import nanogui.experimental.utils : isPointInRect;
99 		const rect_size = Vector2i(mSize.x, mChecked ? cast(int) (fontSize() * 1.3f) : mSize.y);
100 
101 		if (button == MouseButton.Left)
102 		{
103 			import std.stdio;
104 			writeln(tree_path);
105 			if (!isPointInRect(mPos, rect_size, p))
106 				return false;
107 			if (down)
108 			{
109 				mPushed = true;
110 			}
111 			else if (mPushed)
112 			{
113 				mChecked = !mChecked;
114 				if (mCallback)
115 					mCallback(mChecked);
116 				mPushed = false;
117 			}
118 			return true;
119 		}
120 		return false;
121 	}
122 
123 	/// The preferred size of this TreeView.
124 	override Vector2i preferredSize(NanoContext ctx) const
125 	{
126 		if (mFixedSize != Vector2i())
127 			return mFixedSize;
128 		ctx.fontSize(fontSize());
129 		ctx.fontFace("sans");
130 		float[4] bounds;
131 		const extra = mChecked ? (fontSize() * 1.3f * 1/*model.length*/) : 0;
132 		return cast(Vector2i) Vector2f(
133 			(ctx.textBounds(0, 0, mCaption, bounds[]) +
134 				1.8f * fontSize()),
135 			fontSize() * 1.3f + extra);
136 	}
137 
138 	/// Draws this TreeView.
139 	override void draw(ref NanoContext ctx)
140 	{
141 		// do not call super.draw() because we do custom drawing
142 
143 		Vector2i titleSize = void;
144 		titleSize.x = mSize.x;
145 		titleSize.y = mChecked ? cast(int) (fontSize() * 1.3f) : mSize.y;
146 
147 		ctx.save;
148 		scope(exit) ctx.restore;
149 		{
150 			// background for icon
151 			NVGPaint bg = ctx.boxGradient(mPos.x + 1.5f, mPos.y + 1.5f,
152 										titleSize.y - 2.0f, titleSize.y - 2.0f, 3, 3,
153 										mPushed ? Color(0, 0, 0, 100) : Color(0, 0, 0, 32),
154 										Color(0, 0, 0, 180));
155 
156 			ctx.beginPath;
157 			ctx.roundedRect(mPos.x + 1.0f, mPos.y + 1.0f, titleSize.y - 2.0f,
158 						titleSize.y - 2.0f, 3);
159 			ctx.fillPaint(bg);
160 			ctx.fill;
161 		}
162 
163 		ctx.position = mPos;
164 		ctx.theme = theme;
165 		ctx.current_size = 0; // prevents highlighting of icon
166 		const old = ctx.mouse;
167 		ctx.mouse -= window.absolutePosition;
168 		scope(exit) ctx.mouse = old;
169 
170 		import nanogui.experimental.utils : drawItem, indent, unindent;
171 		{
172 			// icon
173 			ctx.fontSize(titleSize.y * icon_scale());
174 			ctx.fontFace("icons");
175 			ctx.fillColor(mEnabled ? mTheme.mIconColor
176 										: mTheme.mDisabledTextColor);
177 			// NVGTextAlign algn;
178 			// algn.center = true;
179 			// algn.middle = true;
180 			// ctx.textAlign(algn);
181 
182 			import nanogui.entypo : Entypo;
183 			drawItem(ctx, titleSize.y, 
184 					[mChecked ? cast(dchar)Entypo.ICON_CHEVRON_DOWN :
185 								cast(dchar)Entypo.ICON_CHEVRON_RIGHT
186 					]);
187 			ctx.position -= Vector2i(0, titleSize.y);
188 		}
189 
190 		ctx.current_size = size.x - titleSize.y;
191 
192 		ctx.tree_view_nesting_level = 0;
193 
194 		{
195 			// Caption
196 			ctx.position += Vector2i(cast(int)(1.6f * fontSize), 0);
197 			scope(exit) ctx.position -= Vector2i(cast(int)(1.6f * fontSize), 0);
198 			ctx.fontSize(fontSize);
199 			ctx.fontFace("sans");
200 			ctx.fillColor(mEnabled ? mTheme.mTextColor : mTheme.mDisabledTextColor);
201 			tree_path.length = 0;
202 			if (drawItem(ctx, titleSize.y, mCaption))
203 			{
204 				tree_path.length = 1;
205 				tree_path[ctx.tree_view_nesting_level] = 0;
206 			}
207 		}
208 
209 		// content of tree view
210 		if (mChecked)
211 		{
212 			ctx.indent;
213 			ctx.tree_view_nesting_level++;
214 			scope(exit)
215 			{
216 				assert(ctx.tree_view_nesting_level > 0);
217 				ctx.tree_view_nesting_level--;
218 				ctx.unindent;
219 			}
220 
221 			ctx.fontSize(fontSize);
222 			ctx.fontFace("sans");
223 
224 			import std.algorithm : min;
225 			import std.range : isRandomAccessRange;
226 			static if (isRandomAccessRange!TreeModel)
227 			{
228 				foreach(i, item; model)
229 				{
230 					ctx.save;
231 					scope(exit) ctx.restore;
232 
233 					if (drawItem(ctx, cast(int)(fontSize() * 1.3f), item))
234 					{
235 						if (tree_path.length < ctx.tree_view_nesting_level+1)
236 							tree_path.length = ctx.tree_view_nesting_level+1;
237 						tree_path[ctx.tree_view_nesting_level] = i;
238 					}
239 				}
240 			}
241 			else static if (is(TreeModel == struct))
242 			{
243 				ctx.save;
244 				scope(exit) ctx.restore;
245 
246 				if (drawItem(ctx, cast(int)(fontSize() * 1.3f), model))
247 				{
248 					if (tree_path.length < ctx.tree_view_nesting_level+1)
249 						tree_path.length = ctx.tree_view_nesting_level+1;
250 					tree_path[ctx.tree_view_nesting_level] = 0;
251 				}
252 			}
253 			else
254 			{
255 			 	// static assert(0, "Unsupported type of TreeModel: " ~ TreeModel.stringof);
256 				drawItem(ctx, cast(int)(fontSize() * 1.3f), model);
257 			}
258 		}
259 	}
260 
261 // // Saves this TreeView to the specified Serializer.
262 //override void save(Serializer &s) const;
263 
264 // // Loads the state of the specified Serializer to this TreeView.
265 //override bool load(Serializer &s);
266 
267 protected:
268 	/// The caption text of this TreeView.
269 	string mCaption;
270 
271 	/**
272 	 * Internal tracking variable to distinguish between mouse click and release.
273 	 * `nanogui.TreeView.mCallback` is only called upon release.  See
274 	 * `nanogui.TreeView.mouseButtonEvent` for specific conditions.
275 	 */
276 	bool mPushed;
277 
278 	/// Whether or not this TreeView is currently checked or unchecked.
279 	bool _mChecked;
280 	
281 	bool mChecked() const { return _mChecked; };
282 	auto mChecked(bool v)
283 	{
284 		if (_mChecked != v)
285 		{
286 			_mChecked = v;
287 			import std.stdio;
288 			writeln("mChecked changed: ", v);
289 			mSize += Vector2i(0, v ? 100 : -100);
290 			screen.needToPerfomLayout = true;
291 		}
292 	}
293 
294 	TreeModel model;
295 
296 	/// The function to execute when `nanogui.TreeView.mChecked` is changed.
297 	void delegate(bool) mCallback;
298 
299 	// sequence of indices to get access to current element of current treeview
300 	size_t[] tree_path;
301 	// // number of current dimension (nesting level) of current tree path
302 	// size_t current_dimension;
303 }