1 ///
2 module nanogui.experimental.list;
3 
4 import std.algorithm : min, max;
5 import nanogui.widget;
6 import nanogui.common : MouseButton, Vector2f, Vector2i, NanoContext;
7 import nanogui.experimental.utils : DataItem;
8 
9 private class IListImplementor
10 {
11 	import nanogui.layout : BoxLayout;
12 
13 	abstract void     size(Vector2i v);
14 	abstract void     position(Vector2i v);
15 	abstract void     layout(BoxLayout l);
16 	abstract Vector2i preferredSize(NanoContext ctx) const;
17 	abstract void     performLayout(NanoContext ctx);
18 	abstract void     currentItemIndicesToHeight(ref float start, ref float finish);
19 	abstract void     draw(NanoContext ctx);
20 }
21 
22 private class ListImplementor(T) : IListImplementor
23 {
24 	import std.range : isRandomAccessRange;
25 	import nanogui.layout : BoxLayout;
26 
27 	private
28 	{
29 		DataItem!T[] _data;
30 		BoxLayout    _layout;
31 		Vector2i     _size;
32 		Vector2i     _pos;
33 		List         _parent;
34 
35 		static int _last_id;
36 		int        _id;
37 
38 		size_t _scroll_position;
39 		size_t _start_item;
40 		size_t _finish_item;
41 		size_t _shift;
42 	}
43 
44 	@disable this();
45 
46 	this(R)(List p, R data) if (isRandomAccessRange!R)
47 	{
48 		import std.exception : enforce;
49 
50 		enforce(p);
51 
52 		_id = ++_id;
53 		_parent = p;
54 
55 		import std.array : array;
56 		_data = data.array;
57 		_scroll_position = _scroll_position.max-1;
58 	}
59 
60 	override void size(Vector2i v)
61 	{
62 		_size = v;
63 	}
64 
65 	override void position(Vector2i v)
66 	{
67 		_pos = v;
68 	}
69 
70 	override void layout(BoxLayout l)
71 	{
72 		_layout = l;
73 	}
74 
75 	/// Draw the widget (and all child widgets)
76 	override void draw(NanoContext ctx)
77 	{
78 		int fontSize = _parent.theme.mButtonFontSize;
79 		ctx.fontSize(fontSize);
80 		ctx.fontFace("sans-bold");
81 
82 		ctx.save;
83 
84 		int size_y = (_parent.fixedSize.y) ? _parent.fixedSize.y : _parent.size.y;
85 		assert(_size.y >= _parent.size.y);
86 		const scroll_position = cast(size_t) (_parent.mScroll * (_size.y - _parent.size.y));
87 		if (_scroll_position != scroll_position)
88 		{
89 			_shift = heightToItemIndex(_data, scroll_position, size_y, _layout.spacing, _start_item, _finish_item, _shift);
90 			_scroll_position = scroll_position;
91 		}
92 
93 		ctx.theme = _parent.theme;
94 		ctx.current_size = _parent.size.x;
95 		ctx.position.x = _pos.x;
96 		ctx.position.y = cast(int) _shift + _pos.y;
97 
98 		ctx.mouse -= _parent.absolutePosition;
99 		scope(exit) ctx.mouse += _parent.absolutePosition;
100 
101 		import std.algorithm : min;
102 		foreach(child; _data[_start_item..min(_finish_item, $)])
103 		{
104 			ctx.save;
105 			scope(exit) ctx.restore;
106 
107 			import std.conv : text;
108 			child.draw(ctx, text(child.size.y), child.size.y);
109 			ctx.position.y += cast(int) _layout.spacing;
110 		}
111 		ctx.restore;
112 	}
113 
114 	/// Convert given range of items indices to to corresponding List height range
115 	private auto itemIndexToHeight(size_t start_index, size_t last_index, ref float start, ref float finish)
116 	{
117 		import nanogui.layout : BoxLayout;
118 		double curr = 0;
119 		double spacing = _layout.spacing;
120 		size_t idx;
121 		assert(start_index < last_index);
122 		start = 0;
123 		finish = 0;
124 
125 		foreach(ref const e; _data)
126 		{
127 			if (idx >= start_index)
128 			{
129 				start = curr;
130 				idx++;
131 				curr += e.size.y + spacing;
132 				break;
133 			}
134 			idx++;
135 			curr += e.size.y + spacing;
136 		}
137 
138 		if (start_index >= _data.length)
139 		{
140 			finish = start;
141 			return;
142 		}
143 
144 		const low_boundary = ++idx;
145 		foreach(ref const e; _data[low_boundary..$])
146 		{
147 			if (idx >= last_index)
148 			{
149 				finish = curr;
150 				break;
151 			}
152 			idx++;
153 			curr += e.size.y + spacing;
154 		}
155 
156 		if (last_index >= _data.length)
157 			finish = curr + spacing;
158 	}
159 
160 	override void currentItemIndicesToHeight(ref float start, ref float finish)
161 	{
162 		return itemIndexToHeight(_start_item, _finish_item, start, finish);
163 	}
164 
165 	/// Compute the preferred size of the widget
166 	override Vector2i preferredSize(NanoContext ctx) const
167 	{
168 		static Vector2i[int] size_inited;
169 
170 		if (_id !in size_inited)
171 			size_inited[_id] = Vector2i();
172 		else if (size_inited[_id] != Vector2i())
173 			return size_inited[_id];
174 
175 		import nanogui.layout : BoxLayout, Orientation, axisIndex, nextAxisIndex;
176 
177 		Vector2i size;
178 		int yOffset = 0;
179 
180 		uint visible_widget_count;
181 		int axis1 = _layout.orientation.axisIndex;
182 		int axis2 = _layout.orientation.nextAxisIndex;
183 		foreach(ref dataitem; _data)
184 		{
185 			if (!dataitem.visible) 
186 				continue;
187 			visible_widget_count++;
188 			// accumulate the primary axis size
189 			size[axis1] += dataitem.size[axis1];
190 			// the secondary axis size is equal to the max size of dataitems
191 			size[axis2] = max(size[axis2], dataitem.size[axis2]);
192 		}
193 		if (visible_widget_count > 1)
194 			size[axis1] += (visible_widget_count - 1) * _layout.spacing;
195 		size_inited[_id] = size;
196 		return size + Vector2i(0, yOffset);
197 	}
198 
199 	/// Invoke the associated layout generator to properly place child widgets, if any
200 	override void performLayout(NanoContext ctx)
201 	{
202 		_scroll_position++; // little hack to force updating item indices
203 		foreach(ref dataitem; _data)
204 		{
205 			if (!dataitem.visible)
206 				continue;
207 
208 			dataitem.performLayout(ctx);
209 		}
210 	}
211 
212 	/// Handle a mouse button event (default implementation: propagate to children)
213 	bool mouseButtonEvent(Vector2i p, MouseButton button, bool down, int modifiers)
214 	{
215 		// foreach_reverse(ch; mChildren)
216 		// {
217 		// 	Widget child = ch;
218 		// 	if (child.visible && child.contains(p - mPos) &&
219 		// 		child.mouseButtonEvent(p - mPos, button, down, modifiers))
220 		// 		return true;
221 		// }
222 		// if (button == MouseButton.Left && down && !mFocused)
223 		// 	requestFocus();
224 		return false;
225 	}
226 }
227 
228 class List : Widget
229 {
230 	import std.range : isRandomAccessRange, ElementType;
231 public:
232 
233 	this(R)(Widget parent, R range) if (isRandomAccessRange!R)
234 	{
235 		super(parent);
236 		mChildPreferredHeight = 0;
237 		mScroll = 0.0f;
238 		mUpdateLayout = false;
239 
240 		alias T = ElementType!R;
241 		DataItem!T[] data;
242 		data.reserve(range.length);
243 		foreach(e; range)
244 		{
245 			import std.random : uniform;
246 			data ~= DataItem!T(e, Vector2i(80, 30 + uniform(0, 30)));
247 		}
248 
249 		list_implementor = new ListImplementor!string(this, data);
250 		list_implementor.size = Vector2i(width, height);
251 
252 		import nanogui.layout : BoxLayout, Orientation;
253 		auto layout = new BoxLayout(Orientation.Vertical);
254 		layout.margin = 40;
255 		layout.setSpacing = 20;
256 		list_implementor.layout = layout;
257 	}
258 
259 	/// Return the current scroll amount as a value between 0 and 1. 0 means scrolled to the top and 1 to the bottom.
260 	float scroll() const { return mScroll; }
261 	/// Set the scroll amount to a value between 0 and 1. 0 means scrolled to the top and 1 to the bottom.
262 	void setScroll(float scroll) { mScroll = scroll; }
263 
264 	override void performLayout(NanoContext ctx)
265 	{
266 		super.performLayout(ctx);
267 
268 		if (list_implementor is null)
269 			return;
270 
271 		const list_implementor_preferred_size = list_implementor.preferredSize(ctx);
272 		mSize.y = parent.size.y - 2*parent.layout.margin;
273 		if (mSize.y < 0)
274 			mSize.y = 0;
275 
276 		mChildPreferredHeight = list_implementor.preferredSize(ctx).y;
277 
278 		if (mChildPreferredHeight > mSize.y)
279 		{
280 			auto y = cast(int) (-mScroll*(mChildPreferredHeight - mSize.y));
281 			list_implementor.position = Vector2i(0, y);
282 			list_implementor.size = Vector2i(mSize.x-12, mChildPreferredHeight);
283 		}
284 		else 
285 		{
286 			list_implementor.position = Vector2i(0, 0);
287 			list_implementor.size = mSize;
288 			mScroll = 0;
289 		}
290 		list_implementor.performLayout(ctx);
291 	}
292 
293 	override Vector2i preferredSize(NanoContext ctx) const
294 	{
295 		// always return 0 because the size is defined by the parent container
296 		return Vector2i(0, 0);
297 	}
298 	
299 	override bool mouseDragEvent(Vector2i p, Vector2i rel, MouseButton button, int modifiers)
300 	{
301 		if (list_implementor !is null && mChildPreferredHeight > mSize.y)
302 		{
303 			float scrollh = height * min(1.0f, height / cast(float)mChildPreferredHeight);
304 
305 			mScroll = max(cast(float) 0.0f, min(cast(float) 1.0f,
306 						mScroll + rel.y / cast(float)(mSize.y - 8 - scrollh)));
307 			mUpdateLayout = true;
308 			return true;
309 		}
310 		else
311 		{
312 			return super.mouseDragEvent(p, rel, button, modifiers);
313 		}
314 	}
315 
316 	override bool scrollEvent(Vector2i p, Vector2f rel)
317 	{
318 		if (list_implementor !is null && mChildPreferredHeight > mSize.y)
319 		{
320 			const scrollAmount = rel.y * 10;
321 			mScroll = max(0.0f, min(1.0f, mScroll - scrollAmount/cast(typeof(mScroll))mChildPreferredHeight));
322 			mUpdateLayout = true;
323 			return true;
324 		}
325 		else
326 		{
327 			return super.scrollEvent(p, rel);
328 		}
329 	}
330 
331 	/// Handle a mouse button event (default implementation: propagate to children)
332 	override bool mouseButtonEvent(Vector2i p, MouseButton button, bool down, int modifiers)
333 	{
334 		const r = super.mouseButtonEvent(p, button, down, modifiers);
335 		if (p.x < mPos.x + mSize.x - 12)
336 			return r;
337 
338 		if (!down)
339 			return false;
340 
341 		const l = mScroll * height;
342 		if (list_implementor !is null && mChildPreferredHeight > mSize.y)
343 		{
344 			float s, f;
345 			list_implementor.currentItemIndicesToHeight(s, f);
346 			const scrollAmount = l > p.y ? (f - s) : -(f - s);
347 
348 			mScroll = max(0.0f, min(1.0f, mScroll - scrollAmount/2/cast(float)mChildPreferredHeight));
349 			mUpdateLayout = true;
350 			return true;
351 		}
352 		return false;
353 	}
354 
355 	override void draw(ref NanoContext ctx)
356 	{
357 		if (list_implementor is null)
358 			return;
359 		auto y = cast(int) (-mScroll*(mChildPreferredHeight - mSize.y));
360 		list_implementor.position = Vector2i(0, y);
361 		mChildPreferredHeight = list_implementor.preferredSize(ctx).y;
362 		float scrollh = max(16, height *
363 			min(1.0f, height / cast(float) mChildPreferredHeight));
364 
365 		if (mUpdateLayout)
366 		{
367 			list_implementor.performLayout(ctx);
368 			mUpdateLayout = false;
369 		}
370 
371 		ctx.save;
372 		ctx.translate(mPos.x, mPos.y);
373 		ctx.intersectScissor(0, 0, mSize.x, mSize.y);
374 		list_implementor.draw(ctx);
375 		ctx.restore;
376 
377 		if (mChildPreferredHeight <= mSize.y)
378 			return;
379 
380 		NVGPaint paint = ctx.boxGradient(
381 			mPos.x + mSize.x - 12 + 1, mPos.y + 4 + 1, 8,
382 			mSize.y - 8, 3, 4, Color(0, 0, 0, 32), Color(0, 0, 0, 92));
383 		ctx.beginPath;
384 		ctx.roundedRect(mPos.x + mSize.x - 12, mPos.y + 4, 8,
385 					mSize.y - 8, 3);
386 		ctx.fillPaint(paint);
387 		ctx.fill;
388 
389 		paint = ctx.boxGradient(
390 			mPos.x + mSize.x - 12 - 1,
391 			mPos.y + 4 + (mSize.y - 8 - scrollh) * mScroll - 1, 8, scrollh,
392 			3, 4, Color(220, 220, 220, 100), Color(128, 128, 128, 100));
393 
394 		ctx.beginPath;
395 		ctx.roundedRect(
396 			mPos.x + mSize.x - 12 + 1,
397 			mPos.y + 4 + 1 + (mSize.y - 8 - scrollh) * mScroll, 8 - 2,
398 			scrollh - 2, 2);
399 		ctx.fillPaint(paint);
400 		ctx.fill;
401 	}
402 	// override void save(Serializer &s) const;
403 	// override bool load(Serializer &s);
404 protected:
405 	int mChildPreferredHeight;
406 	float mScroll;
407 	bool mUpdateLayout;
408 	IListImplementor list_implementor;
409 }
410 
411 /// Convert given range of List height to corresponding items indices
412 private auto heightToItemIndex(R)(R data, double start, double delta, double spacing, ref size_t start_index, ref size_t last_index, double e0)
413 {
414 	const N = data.length;
415 	size_t idx = start_index;
416 	assert(delta >= 0);
417 
418 	if (e0 > start)
419 	{
420 		assert(0 <= idx && idx < N);
421 		for(; idx > 0; idx--)
422 		{
423 			if (e0 - data[idx-1].size.y - spacing <= start &&
424 				e0 > start)
425 			{
426 				start_index = idx-1;
427 				e0 -= data[idx-1].size.y + spacing;
428 				break;
429 			}
430 			else
431 			{
432 				e0 -= data[idx-1].size.y + spacing;
433 			}
434 		}
435 	}
436 	else
437 	{
438 		idx = start_index;
439 		for(; idx < N; idx++)
440 		{
441 			if (e0 <= start && e0 + data[idx].size.y + spacing > start)
442 			{
443 				start_index = idx;
444 				break;
445 			}
446 			else
447 			{
448 				e0 += data[idx].size.y + spacing;
449 			}
450 		}
451 		assert(0 <= idx);
452 		assert(idx <= N);
453 		assert(
454 			(e0 <= start && idx == data.length) ||
455 			(e0 <= start && (e0 + data[idx].size.y + spacing) > start) || 
456 			(idx == N /*&& e0 == E*/)
457 		);
458 	}
459 
460 	if (idx == N)
461 	{
462 		// assert(e0 == E);
463 		start_index = N - 1;
464 		last_index = N;
465 		return cast(size_t) e0; // start (and finish too) is beyond the last index
466 	}
467 
468 	auto e1 = e0;
469 	last_index = 0;
470 
471 	for(; idx < N; idx++)
472 	{
473 		if (e1 > start + delta)
474 		{
475 			last_index = idx + 1;
476 			break;
477 		}
478 		else
479 			e1 += data[idx].size.y + spacing;
480 	}
481 
482 	if (idx == data.length)
483 		last_index = idx; // start is before and finish is beyond the last index
484 
485 	return cast(size_t) e0;
486 }