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 : MouseButton, Vector2f, Vector2i;
15 import nanogui.experimental.utils : Model, TreePathVisitorImpl;
16 
17 /**
18  * Tree view widget.
19  */
20 class TreeView(Data) : Widget
21 {
22 public:
23 
24 	enum modelHasCollapsed = is(typeof(Model!Data.collapsed) == bool);
25 
26 	/**
27 	 * Adds a TreeView to the specified `parent`.
28 	 *
29 	 * Params:
30 	 *     parent   = The Widget to add this TreeView to.
31 	 *     caption  = The caption text of the TreeView (default `"Untitled"`).
32 	 *     callback = If provided, the callback to execute when the TreeView is 
33 	 *     checked or unchecked.  Default parameter function does nothing.  See
34 	 *     `nanogui.TreeView.mPushed` for the difference between "pushed"
35 	 *     and "checked".
36 	 */
37 	this(Widget parent, string caption, Data data, void delegate(bool) callback)
38 	{
39 		super(parent);
40 		mCaption = caption;
41 		static if (modelHasCollapsed)
42 		{
43 			mPushed = false;
44 			mChecked = false;
45 			mCallback = callback;
46 		}
47 		_data = data;
48 		_model = makeModel(_data);
49 		import nanogui.experimental.utils : MeasuringVisitor;
50 		auto v = MeasuringVisitor([size.x, fontSize]);
51 		_model.visitForward(_data, v);
52 	}
53 
54 	/// The caption of this TreeView.
55 	final string caption() const { return mCaption; }
56 
57 	/// Sets the caption of this TreeView.
58 	final void caption(string caption) { mCaption = caption; }
59 
60 	static if (modelHasCollapsed)
61 	{
62 		/// Whether or not this TreeView is currently checked.
63 		final bool checked() const { return _model.collapsed; }
64 
65 		/// Sets whether or not this TreeView is currently checked.
66 		final void checked(bool checked) { _model.collapsed = checked; }
67 
68 		/// Whether or not this TreeView is currently pushed.  See `nanogui.TreeView.mPushed`.
69 		final bool pushed() const { return mPushed; }
70 
71 		/// Sets whether or not this TreeView is currently pushed.  See `nanogui.TreeView.mPushed`.
72 		final void pushed(bool pushed) { mPushed = pushed; }
73 
74 		/// Returns the current callback of this TreeView.
75 		final void delegate(bool) callback() const { return mCallback; }
76 
77 		/// Sets the callback to be executed when this TreeView is checked / unchecked.
78 		final void callback(void delegate(bool) callback) { mCallback = callback; }
79 	}
80 
81 	/**
82 	 * The mouse button callback will return `true` when all three conditions are met:
83 	 *
84 	 * 1. This TreeView is "enabled" (see `nanogui.Widget.mEnabled`).
85 	 * 2. `p` is inside this TreeView.
86 	 * 3. `button` is `MouseButton.Left`.
87 	 *
88 	 * Since a mouse button event is issued for both when the mouse is pressed, as well
89 	 * as released, this function sets `nanogui.TreeView.mPushed` to `true` when
90 	 * parameter `down == true`.  When the second event (`down == false`) is fired,
91 	 * `nanogui.TreeView.mChecked` is inverted and `nanogui.TreeView.mCallback`
92 	 * is called.
93 	 *
94 	 * That is, the callback provided is only called when the mouse button is released,
95 	 * **and** the click location remains within the TreeView boundaries.  If the user
96 	 * clicks on the TreeView and releases away from the bounds of the TreeView,
97 	 * `nanogui.TreeView.mPushed` is simply set back to `false`.
98 	 */
99 	override bool mouseButtonEvent(Vector2i p, MouseButton button, bool down, int modifiers)
100 	{
101 		if (!mEnabled)
102 			return false;
103 
104 		static if (modelHasCollapsed)
105 		{
106 			import nanogui.experimental.utils : isPointInRect;
107 			const rect_size = Vector2i(mSize.x, !_model.collapsed ? cast(int) (fontSize() * 1.3f) : mSize.y);
108 
109 			if (button == MouseButton.Left)
110 			{
111 				import nanogui.experimental.utils : setPropertyByTreePath, getPropertyByTreePath;
112 				if (!down && tree_path.value.length)
113 				{
114 					const value = getPropertyByTreePath!("collapsed", bool)(_data, _model, tree_path.value[]);
115 					if (!value.isNull)
116 					{
117 						setPropertyByTreePath!"collapsed"(_data, _model, tree_path.value[], !value.get);
118 						import nanogui.experimental.utils : MeasuringVisitor;
119 						auto mv = MeasuringVisitor([size.x, fontSize]);
120 						_model.visitForward(_data, mv);
121 						screen.needToPerfomLayout = true;
122 					}
123 				}
124 				if (!isPointInRect(mPos, rect_size, p))
125 					return false;
126 				if (down)
127 				{
128 					mPushed = true;
129 				}
130 				else if (mPushed)
131 				{
132 					mChecked = !mChecked;
133 					if (mCallback)
134 						mCallback(mChecked);
135 					mPushed = false;
136 				}
137 				return true;
138 			}
139 		}
140 
141 		return super.mouseButtonEvent(p, button, down, modifiers);
142 	}
143 
144 	/// The preferred size of this TreeView.
145 	override Vector2i preferredSize(NanoContext ctx) const
146 	{
147 		if (mFixedSize != Vector2i())
148 			return mFixedSize;
149 		ctx.fontSize(fontSize());
150 		ctx.fontFace("sans");
151 		float[4] bounds;
152 
153 		return cast(Vector2i) Vector2f(
154 			(ctx.textBounds(0, 0, mCaption, bounds[]) +
155 				1.8f * fontSize()),
156 			_model.size);
157 	}
158 
159 	/// Draws this TreeView.
160 	override void draw(ref NanoContext ctx)
161 	{
162 		import auxil.location : SizeType;
163 		// do not call super.draw() because we do custom drawing
164 
165 		//ctx.fontSize(theme.mButtonFontSize);
166 		//ctx.fontFace("sans-bold");
167 
168 		ctx.theme = theme;
169 		ctx.size = Vector2f(size.x, ctx.fontSize);
170 		ctx.position = mPos;
171 
172 		//ctx.mouse -= mPos;
173 		//scope(exit) ctx.mouse += mPos;
174 
175 		auto renderer = RenderingVisitor(ctx, [cast(SizeType) ctx.size.x, cast(SizeType) mSize.y]);
176 		renderer.loc.y.destination = cast(SizeType) (ctx.position.y + size.y);
177 		import nanogui.layout : Orientation;
178 		renderer.ctx.orientation = Orientation.Vertical;
179 		_model.visitForward(_data, renderer);
180 		tree_path = renderer.selected_item;
181 	}
182 
183 // // Saves this TreeView to the specified Serializer.
184 //override void save(Serializer &s) const;
185 
186 // // Loads the state of the specified Serializer to this TreeView.
187 //override bool load(Serializer &s);
188 
189 protected:
190 
191 	import nanogui.experimental.utils : makeModel, visit, visitForward, TreePath;
192 
193 	/// The caption text of this TreeView.
194 	string mCaption;
195 
196 	static if (modelHasCollapsed)
197 	{
198 		/**
199 		* Internal tracking variable to distinguish between mouse click and release.
200 		* `nanogui.TreeView.mCallback` is only called upon release.  See
201 		* `nanogui.TreeView.mouseButtonEvent` for specific conditions.
202 		*/
203 		bool mPushed;
204 
205 		bool mChecked() const
206 		{
207 			static if (is(typeof(_model.collapsed) == bool))
208 				return !_model.collapsed;
209 			else
210 				return false;
211 		}
212 
213 		auto mChecked(bool v)
214 		{
215 			static if (is(typeof(_model.collapsed) == bool))
216 				if (_model.collapsed == v)
217 				{
218 					_model.collapsed = !v;
219 					import nanogui.experimental.utils : MeasuringVisitor;
220 					auto mv = MeasuringVisitor([size.x, fontSize]);
221 					_model.visitForward(_data, mv);
222 					screen.needToPerfomLayout = true;
223 				}
224 		}
225 
226 		/// The function to execute when `nanogui.TreeView.mChecked` is changed.
227 		void delegate(bool) mCallback;
228 	}
229 
230 	Data _data;
231 	typeof(makeModel(_data)) _model;
232 
233 	// sequence of indices to get access to current element of current treeview
234 	TreePath tree_path;
235 }
236 
237 private struct RenderingVisitor
238 {
239 	import nanogui.experimental.utils : drawItem, indent, unindent, TreePath;
240 	import auxil.model;
241 	import auxil.location : SizeType;
242 
243 	TreePathVisitorImpl!(typeof(this)) default_visitor;
244 	alias default_visitor this;
245 
246 	NanoContext ctx;
247 	TreePath selected_item;
248 	float finish;
249 	Vector2f origin;
250 
251 	this(ref NanoContext ctx, SizeType[2] size)
252 	{
253 		this.ctx = ctx;
254 		default_visitor = typeof(default_visitor)(size);
255 	}
256 
257 	bool complete()
258 	{
259 		return default_visitor.complete || ctx.position.y > finish;
260 	}
261 
262 	void beforeChildren(Order order, Data, Model)(ref const(Data) data, ref Model model)
263 	{
264 		ctx.indent;
265 	}
266 
267 	void afterChildren(Order order, Data, Model)(ref const(Data) data, ref Model model)
268 	{
269 		ctx.unindent;
270 	}
271 
272 	void enterTree(Order order, Data, Model)(ref const(Data) data, ref Model model)
273 	{
274 		origin = ctx.position;
275 	}
276 
277 	void enterNode(Order order, Data, Model)(ref const(Data) data, ref Model model)
278 		if (Model.Collapsable)
279 	{
280 		ctx.position.x = origin.x + loc.x.position;
281 		ctx.position.y = origin.y + loc.y.position;
282 		ctx.size[Orientation.Horizontal] = loc.x.size;
283 		ctx.size[Orientation.Vertical] = loc.y.size;
284 
285 		if (orientation == Orientation.Vertical)
286 		{
287 			// background for icon
288 			NVGPaint bg = ctx.boxGradient(
289 				ctx.position.x + 1.5f, ctx.position.y + 1.5f,
290 				ctx.size[ctx.orientation] - 2.0f, ctx.size[ctx.orientation] - 2.0f, 3, 3,
291 				true/*pushed*/ ? Color(0, 0, 0, 100) : Color(0, 0, 0, 32),
292 				Color(0, 0, 0, 180)
293 			);
294 
295 			ctx.beginPath;
296 			ctx.roundedRect(ctx.position.x + 1.0f, ctx.position.y + 1.0f,
297 				ctx.size[ctx.orientation] - 2.0f, ctx.size[ctx.orientation] - 2.0f, 3);
298 			ctx.fillPaint(bg);
299 			ctx.fill;
300 
301 			// icon
302 			ctx.fontSize(ctx.size.y);
303 			ctx.fontFace("icons");
304 			ctx.fillColor(model.enabled ? ctx.theme.mIconColor
305 			                            : ctx.theme.mDisabledTextColor);
306 			NVGTextAlign algn;
307 			algn.center = true;
308 			algn.middle = true;
309 			ctx.textAlign(algn);
310 
311 			import nanogui.entypo : Entypo;
312 			int axis2 = (cast(int)ctx.orientation+1)%2;
313 			const old = ctx.size[axis2];
314 			ctx.size[axis2] = ctx.size[ctx.orientation]; // icon has width equals to its height
315 			dchar symb = model.collapsed ? Entypo.ICON_CHEVRON_RIGHT :
316 			                               Entypo.ICON_CHEVRON_DOWN;
317 			if (drawItem(ctx, ctx.size[ctx.orientation], [symb]))
318 				selected_item = loc.current_path;
319 			ctx.position.y += ctx.size.y;
320 			ctx.size[axis2] = old; // restore full width
321 			ctx.position[ctx.orientation] -= ctx.size[ctx.orientation];
322 
323 			// Caption
324 			ctx.position.x += 1.6f * ctx.size.y;
325 			scope(exit) ctx.position.x -= 1.6f * ctx.size.y;
326 			ctx.fontSize(ctx.size.y);
327 			ctx.fontFace("sans");
328 			ctx.fillColor(model.enabled ? ctx.theme.mTextColor : ctx.theme.mDisabledTextColor);
329 			if (drawItem(ctx, ctx.size.y, Data.stringof))
330 				selected_item = loc.current_path;
331 		}
332 	}
333 
334 	void enterNode(Order order, Data, Model)(ref const(Data) data, ref Model model)
335 		if (!Model.Collapsable)
336 	{
337 		ctx.position.x = origin.x + loc.x.position;
338 		ctx.position.y = origin.y + loc.y.position;
339 		ctx.size[Orientation.Horizontal] = loc.x.size;
340 		ctx.size[Orientation.Vertical] = loc.y.size;
341 
342 		ctx.fontSize(ctx.size.y);
343 		ctx.fontFace("sans");
344 		ctx.fillColor(ctx.theme.mTextColor);
345 		ctx.save;
346 		ctx.intersectScissor(ctx.position.x, ctx.position.y, ctx.size.x, ctx.size.y);
347 		if (drawItem(ctx, cast(int) ctx.size[ctx.orientation], data))
348 			selected_item = loc.current_path;
349 		ctx.restore;
350 	}
351 
352 	void leaveNode(Order order, Data, Model)(ref const(Data) data, ref Model model) {}
353 }