1 module auxil.model;
2 
3 import std.traits : isInstanceOf;
4 import taggedalgebraic : TaggedAlgebraic, taget = get;
5 import auxil.traits;
6 import auxil.cursor : Cursor;
7 
8 version(unittest) import unit_threaded : Name;
9 
10 alias SizeType = Cursor.Type;
11 
12 struct FixedAppender(size_t Size)
13 {
14 	void put(char c) pure
15 	{
16 		import std.exception : enforce;
17 
18 		enforce(size < Size);
19 		buffer[size++] = c;
20 	}
21 
22 	void put(scope const(char)[] s) pure
23 	{
24 		import std.exception : enforce;
25 
26 		enforce(size + s.length <= Size);
27 		foreach(c; s)
28 			buffer[size++] = c;
29 	}
30 
31 	@property size_t length() const @safe nothrow @nogc pure
32 	{
33 		return size;
34 	}
35 
36 	string opSlice() return scope pure nothrow @property
37 	{
38 		import std.exception : assumeUnique;
39 		assert(size <= Size);
40 		return buffer[0..size].assumeUnique;
41 	}
42 
43 	void clear() @safe nothrow @nogc pure
44 	{
45 		size = 0;
46 	}
47 
48 private:
49 	char[Size] buffer;
50 	size_t size;
51 }
52 
53 version(unittest) @Name("modelHasCollapsed")
54 @safe
55 unittest
56 {
57 	import unit_threaded : should, be;
58 
59 	static struct Test
60 	{
61 		float f = 7.7;
62 		int i = 8;
63 		string s = "some text";
64 	}
65 
66 	static struct StructWithStruct
67 	{
68 		double d = 8.8;
69 		long l = 999;
70 		Test t;
71 	}
72 
73 	static class TestClass
74 	{
75 
76 	}
77 
78 	static struct StructWithPointerAndClass
79 	{
80 		double* d;
81 		TestClass tc;
82 	}
83 
84 	static struct StructWithNestedClass
85 	{
86 		TestClass tc;
87 	}
88 
89 	// check if Model!T has collapsed member
90 	enum modelHasCollapsed(T) = is(typeof(Model!T.collapsed) == bool);
91 
92 	// Model of plain old data has no collapsed member
93 	assert(!modelHasCollapsed!float);
94 	// Model of structures has collapsed member
95 	assert( modelHasCollapsed!Test );
96 	assert( modelHasCollapsed!StructWithStruct);
97 	// Model of unprocessible structures and classes do not
98 	// exist so they have nothing
99 	assert(!modelHasCollapsed!TestClass);
100 	assert(!modelHasCollapsed!StructWithPointerAndClass);
101 	assert(!modelHasCollapsed!StructWithNestedClass);
102 
103 	import std.traits : FieldNameTuple;
104 	FieldNameTuple!(Model!StructWithStruct).length.should.be == 6;
105 }
106 
107 private import std.range : isRandomAccessRange;
108 private import std.traits : isSomeString, isStaticArray, isAssociativeArray;
109 private enum dataHasStaticArrayModel(T) = isStaticArray!T;
110 private enum dataHasAssociativeArrayModel(T) = isAssociativeArray!T;
111 private enum dataHasRandomAccessRangeModel(T) = isRandomAccessRange!T && !isSomeString!T && !dataHasTaggedAlgebraicModel!T;
112 private enum dataHasAggregateModel(T) = (is(T == struct) || is(T == union)) && !dataHasRandomAccessRangeModel!T && !dataHasTaggedAlgebraicModel!T;
113 private enum dataHasTaggedAlgebraicModel(T) = is(T == struct) && isInstanceOf!(TaggedAlgebraic, T);
114 
115 mixin template State()
116 {
117 	enum SizeType Spacing = 1;
118 	SizeType size = 0, header_size = 0;
119 	int _placeholder = 1 << Field.Collapsed | 
120 	                   1 << Field.Enabled;
121 
122 	private enum Field { Collapsed, Enabled, }
123 
124 	@property void collapsed(bool v)
125 	{
126 		if (collapsed != v)
127 		{
128 			if (v)
129 				_placeholder |=   1 << Field.Collapsed;
130 			else
131 				_placeholder &= ~(1 << Field.Collapsed);
132 		}
133 	}
134 	@property bool collapsed() const { return (_placeholder & (1 << Field.Collapsed)) != 0; }
135 
136 	@property void enabled(bool v)
137 	{
138 		if (enabled != v)
139 		{
140 			if (v)
141 				_placeholder |=   1 << Field.Enabled;
142 			else
143 				_placeholder &= ~(1 << Field.Enabled);
144 		}
145 	}
146 	@property bool enabled() const { return (_placeholder & (1 << Field.Enabled)) != 0; }
147 }
148 
149 template Model(alias A)
150 {
151 	import std.typecons : Nullable;
152 	import std.datetime : Duration;
153 
154 	static if (dataHasStaticArrayModel!(TypeOf!A))
155 		alias Model = StaticArrayModel!A;
156 	else static if (dataHasRandomAccessRangeModel!(TypeOf!A))
157 		alias Model = RaRModel!A;
158 	else static if (dataHasAssociativeArrayModel!(TypeOf!A))
159 		alias Model = AssocArrayModel!A;
160 	else static if (dataHasTaggedAlgebraicModel!(TypeOf!A))
161 		alias Model = TaggedAlgebraicModel!A;
162 	else static if (dataHasAggregateModel!(TypeOf!A) && hasRenderedAs!A)
163 		alias Model = RenderedAsAggregateModel!A;
164 	else static if (dataHasAggregateModel!(TypeOf!A) && hasRenderedAsMember!(TypeOf!A))
165 		alias Model = RenderedAsMemberAggregateModel!A;
166 	else static if (dataHasAggregateModel!(TypeOf!A) && getRenderedAsMemberString!A.length == 1)
167 		alias Model = RenderedAsMemberStringAggregateModel!A;
168 	else static if (dataHasAggregateModel!(TypeOf!A) && getRenderedAsPointeeString!A.length == 1)
169 		alias Model = RenderedAsPointeeStringModel!A;
170 	else static if (is(TypeOf!A : Duration))
171 		alias Model = DurationModel!A;
172 	else static if (isNullable!(TypeOf!A))
173 		alias Model = NullableModel!A;
174 	else static if (isTimemarked!(TypeOf!A))
175 		alias Model = TimemarkedModel!A;
176 	else static if (dataHasAggregateModel!(TypeOf!A))
177 		alias Model = AggregateModel!A;
178 	else
179 		alias Model = ScalarModel!A;
180 }
181 
182 struct StaticArrayModel(alias A)// if (dataHasStaticArrayModel!(TypeOf!A))
183 {
184 	enum Collapsable = true;
185 
186 	mixin State;
187 
188 	alias Data = TypeOf!A;
189 	static assert(isProcessible!Data);
190 
191 	alias ElementType = typeof(Data.init[0]);
192 	Model!ElementType[Data.length] model;
193 	alias model this;
194 
195 	this()(const(Data) data) if (Data.sizeof <= (void*).sizeof)
196 	{
197 		foreach(i; 0..data.length)
198 			model[i] = Model!ElementType(data[i]);
199 	}
200 
201 	this()(ref const(Data) data) if (Data.sizeof > (void*).sizeof)
202 	{
203 		foreach(i; 0..data.length)
204 			model[i] = Model!ElementType(data[i]);
205 	}
206 
207 	mixin visitImpl;
208 }
209 
210 struct RaRModel(alias A)// if (dataHasRandomAccessRangeModel!(TypeOf!A))
211 {
212 	import automem : Vector;
213 	import std.experimental.allocator.mallocator : Mallocator;
214 
215 	enum Collapsable = true;
216 
217 	mixin State;
218 
219 	alias Data = TypeOf!A;
220 	static assert(isProcessible!Data);
221 
222 	alias ElementType = typeof(Data.init[0]);
223 	Vector!(Model!ElementType, Mallocator) model;
224 	alias model this;
225 
226 	this()(const(Data) data) if (Data.sizeof <= (void*).sizeof)
227 	{
228 		update(data);
229 	}
230 
231 	this()(ref const(Data) data) if (Data.sizeof > (void*).sizeof)
232 	{
233 		update(data);
234 	}
235 
236 	void update(ref const(Data) data)
237 	{
238 		model.length = data.length;
239 		foreach(i, ref e; model)
240 			e = Model!ElementType(data[i]);
241 	}
242 
243 	void update(T)(ref TaggedAlgebraic!T v)
244 	{
245 		update(taget!Data(v));
246 	}
247 
248 	mixin visitImpl;
249 }
250 
251 struct AssocArrayModel(alias A)// if (dataHasAssociativeArrayModel!(TypeOf!A))
252 {
253 	import automem : Vector;
254 	import std.experimental.allocator.mallocator : Mallocator;
255 
256 	enum Collapsable = true;
257 
258 	static assert(dataHasAssociativeArrayModel!(TypeOf!A));
259 
260 	mixin State;
261 
262 	alias Data = TypeOf!A;
263 	alias Key = typeof(Data.init.byKey.front);
264 	static assert(isProcessible!Data);
265 
266 	alias ElementType = typeof(Data.init[0]);
267 	Vector!(Model!ElementType, Mallocator) model;
268 	Vector!(Key, Mallocator) keys;
269 	alias model this;
270 
271 	this()(const(Data) data) if (Data.sizeof <= (void*).sizeof)
272 	{
273 		update(data);
274 	}
275 
276 	this()(ref const(Data) data) if (Data.sizeof > (void*).sizeof)
277 	{
278 		update(data);
279 	}
280 
281 	void update(ref const(Data) data)
282 	{
283 		model.length = data.length;
284 		keys.reserve(data.length);
285 		foreach(k; data.byKey)
286 			keys ~= k;
287 		foreach(i, ref e; model)
288 			e = Model!ElementType(data[keys[i]]);
289 	}
290 
291 	void update(T)(ref TaggedAlgebraic!T v)
292 	{
293 		update(taget!Data(v));
294 	}
295 
296 	mixin visitImpl;
297 }
298 
299 private enum isCollapsable(T) = is(typeof(T.Collapsable)) && T.Collapsable;
300 
301 struct TaggedAlgebraicModel(alias A)// if (dataHasTaggedAlgebraicModel!(TypeOf!A))
302 {
303 	alias Data = TypeOf!A;
304 	static assert(isProcessible!Data);
305 
306 	import std.traits : Fields;
307 	import std.meta : anySatisfy;
308 	enum Collapsable = anySatisfy!(isCollapsable, Fields!Payload);
309 
310 	private static struct Payload
311 	{
312 		static foreach(i, fname; Data.UnionType.fieldNames)
313 		{
314 			mixin("Model!(Data.UnionType.FieldTypes[__traits(getMember, Data.Kind, fname)]) " ~ fname ~ ";");
315 		}
316 	}
317 
318 	static struct TAModel
319 	{
320 		TaggedAlgebraic!Payload value;
321 		alias value this;
322 
323 		@property void collapsed(bool v)
324 		{
325 			final switch(value.kind)
326 			{
327 				foreach (i, FT; value.UnionType.FieldTypes)
328 				{
329 					case __traits(getMember, value.Kind, value.UnionType.fieldNames[i]):
330 						static if (is(typeof(taget!FT(value).collapsed) == bool))
331 							taget!FT(value).collapsed = v;
332 					return;
333 				}
334 			}
335 			assert(0); // never reached
336 		}
337 
338 		@property bool collapsed() const
339 		{
340 			final switch(value.kind)
341 			{
342 				foreach (i, FT; value.UnionType.FieldTypes)
343 				{
344 					case __traits(getMember, value.Kind, value.UnionType.fieldNames[i]):
345 						static if (is(typeof(taget!FT(value).collapsed) == bool))
346 							return taget!FT(value).collapsed;
347 						else
348 							assert(0);
349 				}
350 			}
351 			assert(0); // never reached
352 		}
353 
354 		@property size() const
355 		{
356 			final switch(value.kind)
357 			{
358 				foreach (i, FT; value.UnionType.FieldTypes)
359 				{
360 					case __traits(getMember, value.Kind, value.UnionType.fieldNames[i]):
361 						return taget!FT(value).size;
362 				}
363 			}
364 			assert(0);
365 		}
366 
367 		this(T)(T v)
368 		{
369 			value = v;
370 		}
371 	}
372 	TAModel tamodel;
373 	alias tamodel this;
374 
375 	/// returns a model corresponding to given data value
376 	static TAModel makeModel(ref const(Data) data)
377 	{
378 		final switch(data.kind)
379 		{
380 			foreach (i, FT; data.UnionType.FieldTypes)
381 			{
382 				case __traits(getMember, data.Kind, data.UnionType.fieldNames[i]):
383 					return TAModel(Model!FT(data.taget!FT));
384 			}
385 		}
386 	}
387 
388 	this()(auto ref const(Data) data)
389 	{
390 		tamodel = makeModel(data);
391 	}
392 
393 	bool visit(Order order, Visitor)(ref const(Data) data, ref Visitor visitor)
394 	{
395 		final switch (data.kind) {
396 			foreach (i, fname; Data.UnionType.fieldNames)
397 			{
398 				case __traits(getMember, data.Kind, fname):
399 					if (taget!(this.UnionType.FieldTypes[i])(tamodel).visit!order(
400 							taget!(Data.UnionType.FieldTypes[i])(data),
401 							visitor,
402 						))
403 					{
404 						return true;
405 					}
406 				break;
407 			}
408 		}
409 		return false;
410 	}
411 }
412 
413 template AggregateModel(alias A) // if (dataHasAggregateModel!(TypeOf!A) && !is(TypeOf!A : Duration) && !hasRenderedAs!A)
414 {
415 	alias D = TypeOf!A;
416 	static assert(isProcessible!D);
417 
418 	static if (DrawableMembers!D.length == 1)
419 	{
420 		struct SingleMemberAggregateModel(T)
421 		{
422 			alias Data = D;
423 			enum member = DrawableMembers!T[0];
424 			alias Member = TypeOf!(mixin("T." ~ member));
425 			Model!Member single_member_model;
426 			alias single_member_model this;
427 
428 			enum Collapsable = single_member_model.Collapsable;
429 
430 			this()(auto ref const(T) data)
431 			{
432 				import std.format : format;
433 
434 				static if (isNullable!(typeof(mixin("data." ~ member))) ||
435 						isTimemarked!(typeof(mixin("data." ~ member))))
436 				{
437 					if (!mixin("data." ~ member).isNull)
438 						mixin("single_member_model = Model!Member(data.%1$s);".format(member));
439 				}
440 				else
441 					mixin("single_member_model = Model!Member(data.%1$s);".format(member));
442 			}
443 
444 			bool visit(Order order, Visitor)(auto ref const(T) data, ref Visitor visitor)
445 			{
446 				return single_member_model.visit!order(mixin("data." ~ member), visitor);
447 			}
448 		}
449 		alias AggregateModel = SingleMemberAggregateModel!D;
450 	}
451 	else
452 	{
453 		struct AggregateModel
454 		{
455 			alias Data = D;
456 			enum Collapsable = true;
457 
458 			import std.format : format;
459 
460 			mixin State;
461 
462 			import auxil.traits : DrawableMembers;
463 			static foreach(member; DrawableMembers!Data)
464 				mixin("Model!(Data.%1$s) %1$s;".format(member));
465 
466 			this()(auto ref const(Data) data)
467 			{
468 				foreach(member; DrawableMembers!Data)
469 				{
470 					static if (isNullable!(typeof(mixin("data." ~ member))) ||
471 							isTimemarked!(typeof(mixin("data." ~ member))))
472 					{
473 						if (mixin("data." ~ member).isNull)
474 							continue;
475 					}
476 					else
477 						mixin("this.%1$s = Model!(Data.%1$s)(data.%1$s);".format(member));
478 				}
479 			}
480 
481 			mixin visitImpl;
482 		}
483 	}
484 }
485 
486 struct RenderedAsAggregateModel(alias A)// if (dataHasAggregateModel!(TypeOf!A) && hasRenderedAs!A)
487 {
488 	import auxil.traits : getRenderedAs;
489 
490 	alias Data = TypeOf!A;
491 	static assert(isProcessible!Data);
492 
493 	alias Proxy = getRenderedAs!A;
494 	static assert(isProcessible!Proxy);
495 	Proxy proxy;
496 	Model!proxy proxy_model;
497 
498 	enum Collapsable = proxy_model.Collapsable;
499 
500 	alias proxy_model this;
501 
502 	this()(auto ref const(Data) data)
503 	{
504 		import std.conv : to;
505 		proxy = data.to!Proxy;
506 		proxy_model = Model!proxy(proxy);
507 	}
508 
509 	bool visit(Order order, Visitor)(auto ref const(Data) ignored_data, ref Visitor visitor)
510 	{
511 		return proxy_model.visit!order(proxy, visitor);
512 	}
513 }
514 
515 struct RenderedAsMemberAggregateModel(alias A)// if (dataHasAggregateModel!Data && hasRenderedAsMember!Data)
516 {
517 	import auxil.traits : getRenderedAs;
518 
519 	alias Data = TypeOf!A;
520 	static assert(isProcessible!Data);
521 
522 	enum member_name = getRenderedAsMember!Data;
523 	Model!(mixin("Data." ~ member_name)) model;
524 
525 	enum Collapsable = model.Collapsable;
526 
527 	alias model this;
528 
529 	this()(auto ref const(Data) data)
530 	{
531 		model = typeof(model)(mixin("data." ~ member_name));
532 	}
533 
534 	bool visit(Order order, Visitor)(auto ref const(Data) data, ref Visitor visitor)
535 	{
536 		return model.visit!order(mixin("data." ~ member_name), visitor);
537 	}
538 }
539 
540 struct RenderedAsMemberStringAggregateModel(alias A)// if (dataHasAggregateModel!Data && getRenderedAsMemberString!Data.length == 1)
541 {
542 	alias Data = TypeOf!A;
543 	static assert(isProcessible!Data);
544 
545 	enum member_name = getRenderedAsMemberString!A[0];
546 	Model!(mixin("Data." ~ member_name)) model;
547 
548 	enum Collapsable = model.Collapsable;
549 
550 	alias model this;
551 
552 	this()(auto ref const(Data) data)
553 	{
554 		model = typeof(model)(mixin("data." ~ member_name));
555 	}
556 
557 	bool visit(Order order, Visitor)(auto ref const(Data) data, ref Visitor visitor)
558 	{
559 		return model.visit!order(mixin("data." ~ member_name), visitor);
560 	}
561 }
562 
563 struct RenderedAsPointeeStringModel(alias A)
564 {
565 	alias Data = TypeOf!A;
566 	static assert(isProcessible!Data);
567 
568 	enum member_name = getRenderedAsPointeeString!A[0];
569 	Model!(typeof(mixin("*Data.init." ~ member_name))) model;
570 
571 	enum Collapsable = model.Collapsable;
572 
573 	alias model this;
574 
575 	this()(auto ref const(Data) data)
576 	{
577 		model = typeof(model)(*mixin("data." ~ member_name));
578 	}
579 
580 	bool visit(Order order, Visitor)(auto ref const(Data) data, ref Visitor visitor)
581 	{
582 		return model.visit!order(*mixin("data." ~ member_name), visitor);
583 	}
584 }
585 
586 struct DurationModel(alias A)
587 {
588 	import std.datetime : Duration;
589 
590 	alias Data = TypeOf!A;
591 	static assert(isProcessible!Data);
592 	static assert(is(TypeOf!A : Duration));
593 
594 	enum Collapsable = false;
595 
596 	alias Proxy = string;
597 	static assert(isProcessible!Proxy);
598 	Proxy proxy;
599 	Model!proxy proxy_model;
600 
601 	alias proxy_model this;
602 
603 	this()(auto ref const(Data) data)
604 	{
605 		import std.conv : to;
606 		proxy = data.to!Proxy;
607 		proxy_model = Model!proxy(proxy);
608 	}
609 
610 	bool visit(Order order, Visitor)(auto ref const(Data) data, ref Visitor visitor)
611 	{
612 		return proxy_model.visit!order(proxy, visitor);
613 	}
614 }
615 
616 struct NullableModel(alias A)
617 {
618 	import std.typecons : Nullable;
619 
620 	alias Data = TypeOf!A;
621 	static assert(isProcessible!Data);
622 	static assert(isInstanceOf!(Nullable, Data));
623 	alias Payload = typeof(Data.get);
624 
625 	enum Collapsable = true;
626 
627 	enum NulledPayload = __traits(identifier, A) ~ ": null";
628 	Model!string  nulled_model = makeModel(NulledPayload);
629 	Model!Payload nullable_model;
630 	private bool isNull;
631 
632 	alias nullable_model this;
633 
634 	@property auto size()
635 	{
636 		return (isNull) ? nulled_model.size : nullable_model.size;
637 	}
638 
639 	@property auto size(SizeType v)
640 	{
641 		if (isNull)
642 			nulled_model.size = v;
643 		else
644 			nullable_model.size = v;
645 	}
646 
647 	this()(auto ref const(Data) data)
648 	{
649 		isNull = data.isNull;
650 		if (isNull)
651 			nullable_model = Model!Payload();
652 		else
653 			nullable_model = Model!Payload(data.get);
654 	}
655 
656 	bool visit(Order order, Visitor)(auto ref const(Data) data, ref Visitor visitor)
657 	{
658 		isNull = data.isNull;
659 		if (isNull)
660 			return nulled_model.visit!order(NulledPayload, visitor);
661 		else
662 			return nullable_model.visit!order(data.get, visitor);
663 	}
664 }
665 
666 private template NoInout(T)
667 {
668 	static if (is(T U == inout U))
669 		alias NoInout = U;
670 	else
671 		alias NoInout = T;
672 }
673 
674 version(HAVE_TIMEMARKED)
675 struct TimemarkedModel(alias A)
676 {
677 	import rdp.timemarked : Timemarked;
678 
679 	alias Data = TypeOf!A;
680 	static assert(isInstanceOf!(Timemarked, Data));
681 
682 	@("renderedAsPointee.payload")
683 	struct TimemarkedPayload
684 	{
685 		alias Payload = NoInout!(typeof(Data.value));
686 		const(Payload)* payload;
687 		static string prefix = __traits(identifier, A);
688 
689 		this(ref const(Payload) payload)
690 		{
691 			this.payload = &payload;
692 		}
693 	}
694 
695 	enum Collapsable = true;
696 
697 	enum NulledPayload = __traits(identifier, A) ~ ": null";
698 	Model!string nulled_model = Model!string(NulledPayload);
699 	Model!TimemarkedPayload timemarked_model;
700 	version(none) Model!(Data.value) value_model;
701 	version(none) Model!(Data.timestamp) timestamp_model;
702 	private bool isNull;
703 
704 	alias timemarked_model this;
705 
706 	@property auto size()
707 	{
708 		return (isNull) ? nulled_model.size : timemarked_model.size;
709 	}
710 
711 	@property auto size(double v)
712 	{
713 		if (isNull)
714 			nulled_model.size = v;
715 		else
716 			timemarked_model.size = v;
717 	}
718 
719 	this()(auto ref const(Data) data)
720 	{
721 		isNull = data.isNull;
722 		if (isNull)
723 			timemarked_model = Model!TimemarkedPayload();
724 		else
725 			timemarked_model = Model!TimemarkedPayload(data.value);
726 	}
727 
728 	bool visit(Order order, Visitor)(auto ref const(Data) data, ref Visitor visitor)
729 	{
730 		isNull = data.isNull;
731 		if (isNull)
732 			return nulled_model.visit!order(NulledPayload, visitor);
733 
734 		if (timemarked_model.visit!order(TimemarkedPayload(data.value), visitor))
735 			return true;
736 
737 		version(none)
738 		{
739 			visitor.indent;
740 			scope(exit) visitor.unindent;
741 
742 			if (timestamp_model.visit!order(data.timestamp, visitor))
743 				return true;
744 		}
745 
746 		return false;
747 	}
748 }
749 
750 struct ScalarModel(alias A)
751 	if (!dataHasAggregateModel!(TypeOf!A) && 
752 	    !dataHasStaticArrayModel!(TypeOf!A) &&
753 	    !dataHasRandomAccessRangeModel!(TypeOf!A) &&
754 	    !dataHasTaggedAlgebraicModel!(TypeOf!A) &&
755 	    !dataHasAssociativeArrayModel!(TypeOf!A))
756 {
757 	enum SizeType Spacing = 1;
758 	SizeType size = 0;
759 
760 	enum Collapsable = false;
761 
762 	alias Data = TypeOf!A;
763 	static assert(isProcessible!Data);
764 
765 	this()(auto ref const(Data) data)
766 	{
767 	}
768 
769 	private bool visit(Order order, Visitor)(auto ref const(Data) data, ref Visitor visitor)
770 	{
771 		import std.algorithm : among;
772 
773 		enum Sinking     = order == Order.Sinking;
774 		enum Bubbling    = !Sinking; 
775 		enum hasTreePath = Visitor.treePathEnabled;
776 		enum hasSize     = Visitor.sizeEnabled;
777 
778 		static if (hasTreePath)
779 		{
780 			with(visitor) final switch(state)
781 			{
782 				case State.seeking:
783 					if (tree_path.value == path.value)
784 						state = State.first;
785 				break;
786 				case State.first:
787 					state = State.rest;
788 				break;
789 				case State.rest:
790 					// do nothing
791 				break;
792 				case State.finishing:
793 				{
794 					return true;
795 				}
796 			}
797 		}
798 		if (visitor.complete)
799 		{
800 			return true;
801 		}
802 
803 		static if (hasSize) this.size = visitor.size + this.Spacing;
804 		static if (hasTreePath) with(visitor) 
805 		{
806 			position = position + deferred_change;
807 			deferred_change = (Sinking) ? this.size : -this.size;
808 			curr_shift += this.size;
809 
810 			if (state.among(State.first, State.rest))
811 			{
812 				static if (Sinking) visitor.processLeaf!order(data, this);
813 				if ((Sinking  && position+deferred_change > destination) ||
814 					(Bubbling && position                 < destination))
815 				// if (curr_shift >= total_shift)
816 				{
817 					state = State.finishing;
818 					path = tree_path;
819 				}
820 				static if (Bubbling) visitor.processLeaf!order(data, this);
821 			}
822 		}
823 		else
824 			visitor.processLeaf!order(data, this);
825 
826 		return false;
827 	}
828 }
829 
830 auto makeModel(T)(auto ref const(T) data)
831 {
832 	return Model!T(data);
833 }
834 
835 alias visitImpl = visitImpl1;
836 
837 mixin template visitImpl1()
838 {
839 	bool visit(Order order, Visitor)(ref const(Data) data, ref Visitor visitor)
840 		if (Data.sizeof > 24)
841 	{
842 		return baseVisit!order(data, visitor);
843 	}
844 
845 	bool visit(Order order, Visitor)(const(Data) data, ref Visitor visitor)
846 		if (Data.sizeof <= 24)
847 	{
848 		return baseVisit!order(data, visitor);
849 	}
850 
851 	bool baseVisit(Order order, Visitor)(auto ref const(Data) data, ref Visitor visitor)
852 	{
853 		static if (Data.sizeof > 24 && !__traits(isRef, data))
854 			pragma(msg, "Warning: ", Data, " is a value type and has size larger than 24 bytes");
855 
856 		// static assert(Data.sizeof <= 24 || __traits(isRef, data));
857 		import std.algorithm : among;
858 
859 		enum Sinking     = order == Order.Sinking;
860 		enum Bubbling    = !Sinking; 
861 		enum hasTreePath = Visitor.treePathEnabled;
862 		enum hasSize     = Visitor.sizeEnabled;
863 
864 		auto stepForward()
865 		{
866 			static if (hasTreePath && Sinking)
867 			{
868 				visitor.curr_shift += this.header_size;
869 				visitor.position = visitor.position + visitor.deferred_change;
870 				visitor.deferred_change = this.header_size;
871 			}
872 		}
873 
874 		auto stepBackward()
875 		{
876 			static if (hasTreePath && Bubbling)
877 			{
878 				visitor.curr_shift += this.header_size;
879 				visitor.position = visitor.position + visitor.deferred_change;
880 				visitor.deferred_change = -this.header_size;
881 			}
882 		}
883 
884 		auto checkIfCompleted()
885 		{
886 			static if (hasTreePath) with(visitor)
887 			{
888 				if (curr_shift >= total_shift)
889 				{
890 					state = State.finishing;
891 					path = tree_path;
892 				}
893 			}
894 		}
895 
896 		bool isVisitorProcessing()
897 		{
898 			import std.algorithm : among;
899 
900 			static if (hasTreePath)
901 				return !!visitor.state.among(visitor.State.first, visitor.State.rest);
902 			else
903 				return true;
904 		}
905 
906 		static if (hasTreePath)
907 		{
908 			with(visitor) final switch(state)
909 			{
910 				case State.seeking:
911 					if (tree_path.value == path.value)
912 						state = State.first;
913 				break;
914 				case State.first:
915 					state = State.rest;
916 				break;
917 				case State.rest:
918 					// do nothing
919 				break;
920 				case State.finishing:
921 				{
922 					return true;
923 				}
924 			}
925 		}
926 
927 		if (visitor.complete)
928 		{
929 			return true;
930 		}
931 
932 		static if (hasSize) size = header_size = visitor.size + Spacing;
933 
934 		if (isVisitorProcessing)
935 		{
936 			stepForward;
937 			checkIfCompleted;
938 			visitor.enterNode!(order, Data)(data, this);
939 		}
940 
941 		scope(exit)
942 		{
943 			if (isVisitorProcessing)
944 			{
945 				stepBackward;
946 				checkIfCompleted;
947 				visitor.leaveNode!order(data, this);
948 			}
949 		}
950 
951 		if (!this.collapsed)
952 		{
953 			visitor.indent;
954 			scope(exit) visitor.unindent;
955 
956 			static if (Bubbling && hasTreePath)
957 			{
958 				// Edge case if the start path starts from this collapsable exactly
959 				// then the childs of the collapsable aren't processed
960 				if (visitor.path.value.length && visitor.tree_path.value[] == visitor.path.value[])
961 				{
962 					return false;
963 				}
964 			}
965 
966 			const len = getLength!(Data, data);
967 			static if (is(typeof(model.length)))
968 				assert(len == model.length);
969 			if (!len)
970 				return false;
971 
972 			size_t start_value;
973 			static if (Bubbling)
974 			{
975 				start_value = len;
976 				start_value--;
977 			}
978 			static if (hasTreePath)
979 			{
980 				visitor.tree_path.put(0);
981 				scope(exit) visitor.tree_path.popBack;
982 				if (visitor.state.among(visitor.State.seeking, visitor.State.first))
983 				{
984 					auto idx = visitor.tree_path.value.length;
985 					if (idx && visitor.path.value.length >= idx)
986 					{
987 						start_value = visitor.path.value[idx-1];
988 						// position should change only if we've got the initial path
989 						// and don't get the end
990 						if (visitor.state == visitor.State.seeking) visitor.deferred_change = 0;
991 					}
992 				}
993 			}
994 			static if (dataHasStaticArrayModel!Data || 
995 			           dataHasRandomAccessRangeModel!Data ||
996 			           dataHasAssociativeArrayModel!Data)
997 			{
998 				foreach(i; TwoFacedRange!order(start_value, data.length))
999 				{
1000 					static if (hasTreePath) visitor.tree_path.back = i;
1001 					static if (hasSize) scope(exit) this.size += model[i].size;
1002 					auto idx = getIndex!(Data)(this, i);
1003 					if (model[i].visit!order(data[idx], visitor))
1004 					{
1005 						return true;
1006 					}
1007 				}
1008 			}
1009 			else static if (dataHasAggregateModel!Data)
1010 			{
1011 				switch(start_value)
1012 				{
1013 					enum len2 = getLength!(Data, data);
1014 					static foreach(i; 0..len2)
1015 					{
1016 						// reverse fields order if Order.Bubbling
1017 						case (Sinking) ? i : len2 - i - 1:
1018 						{
1019 							enum FieldNo = (Sinking) ? i : len2 - i - 1;
1020 							enum member = DrawableMembers!Data[FieldNo];
1021 							static if (hasTreePath) visitor.tree_path.back = cast(int) FieldNo;
1022 							static if (hasSize) scope(exit) this.size += mixin("this." ~ member).size;
1023 							if (mixin("this." ~ member).visit!order(mixin("data." ~ member), visitor))
1024 							{
1025 								return true;
1026 							}
1027 						}
1028 						goto case;
1029 					}
1030 					// the dummy case needed because every `goto case` should be followed by a case clause
1031 					case len2:
1032 						// flow cannot get here directly
1033 						if (start_value == len2)
1034 							assert(0);
1035 					break;
1036 					default:
1037 						assert(0);
1038 				}
1039 			}
1040 		}
1041 		else
1042 		{
1043 		}
1044 
1045 		return false;
1046 	}
1047 }
1048 
1049 mixin template visitImpl2()
1050 {
1051 	bool visit(Order order, Visitor)(ref const(Data) data, ref Visitor visitor)
1052 		if (Data.sizeof > 24)
1053 	{
1054 		return baseVisit!order(data, visitor);
1055 	}
1056 
1057 	bool visit(Order order, Visitor)(const(Data) data, ref Visitor visitor)
1058 		if (Data.sizeof <= 24)
1059 	{
1060 		return baseVisit!order(data, visitor);
1061 	}
1062 
1063 	bool baseVisit(Order order, Visitor)(auto ref const(Data) data, ref Visitor visitor)
1064 	{
1065 		static if (Data.sizeof > 24 && !__traits(isRef, data))
1066 			pragma(msg, "Warning: ", Data, " is a value type and has size larger than 24 bytes");
1067 
1068 		static assert(Data.sizeof <= 24 || __traits(isRef, data));
1069 		import std.algorithm : among;
1070 
1071 		enum Sinking     = order == Order.Sinking;
1072 		enum Bubbling    = !Sinking;
1073 		enum hasTreePathNG = Visitor.treePathNGEnabled;
1074 		enum hasTreePath = Visitor.treePathEnabled;
1075 
1076 		if (visitor.complete)
1077 			return true;
1078 
1079 		auto collapsed = visitor.enterNode!(order, Data)(data, this);
1080 
1081 		if (visitor.complete)
1082 		{
1083 			return true;
1084 		}
1085 
1086 		scope(exit)
1087 		{
1088 			visitor.leaveNode!order(data, this);
1089 		}
1090 
1091 		if (!collapsed)
1092 		{
1093 			// visitor.indent;
1094 			// scope(exit) visitor.unindent;
1095 
1096 			static if (Bubbling && (hasTreePath || hasTreePathNG))
1097 			{
1098 				// Edge case if the start path starts from this collapsable exactly
1099 				// then the childs of the collapsable aren't processed
1100 				if (visitor.path.value.length && visitor.tree_path.value[] == visitor.path.value[])
1101 				{
1102 					return false;
1103 				}
1104 			}
1105 
1106 			static if (hasTreePath || hasTreePathNG) visitor.tree_path.put(0);
1107 			static if (hasTreePath || hasTreePathNG) scope(exit) visitor.tree_path.popBack;
1108 			const len = getLength!(Data, data);
1109 			static if (is(typeof(model.length)))
1110 				assert(len == model.length);
1111 			if (!len)
1112 				return false;
1113 
1114 			size_t start_value;
1115 			static if (Bubbling)
1116 			{
1117 				start_value = len;
1118 				start_value--;
1119 			}
1120 			static if (hasTreePath)
1121 			{
1122 				if (visitor.state.among(visitor.State.seeking, visitor.State.first))
1123 				{
1124 					auto idx = visitor.tree_path.value.length;
1125 					if (idx && visitor.path.value.length >= idx)
1126 					{
1127 						start_value = visitor.path.value[idx-1];
1128 					}
1129 				}
1130 			}
1131 			static if (hasTreePathNG)
1132 			{
1133 				// probably some flag should be here defining
1134 				// should we skip elements (because we have random access range)
1135 				// or visit them consiquently
1136 				// for example when we seek the specific element we skip elements before
1137 				// the specific elements but then we iterate over the rest consequently
1138 				if (true)
1139 				{
1140 					auto idx = visitor.tree_path.value.length;
1141 					if (idx && visitor.path.value.length >= idx)
1142 					{
1143 						start_value = visitor.path.value[idx-1];
1144 					}
1145 				}
1146 			}
1147 			static if (dataHasStaticArrayModel!Data || 
1148 			           dataHasRandomAccessRangeModel!Data ||
1149 			           dataHasAssociativeArrayModel!Data)
1150 			{
1151 				foreach(i; TwoFacedRange!order(start_value, data.length))
1152 				{
1153 					static if (hasTreePath || hasTreePathNG) visitor.tree_path.back = i;
1154 					auto idx = getIndex!(Data)(this, i);
1155 					if (model[i].visit!order(data[idx], visitor))
1156 					{
1157 						// premature quitting
1158 						return true;
1159 					}
1160 				}
1161 			}
1162 			else static if (dataHasAggregateModel!Data)
1163 			{
1164 				switch(start_value)
1165 				{
1166 					enum len2 = getLength!(Data, data);
1167 					static foreach(i; 0..len2)
1168 					{
1169 						// reverse fields order if Order.Bubbling
1170 						case (Sinking) ? i : len2 - i - 1:
1171 						{
1172 							enum FieldNo = (Sinking) ? i : len2 - i - 1;
1173 							enum member = DrawableMembers!Data[FieldNo];
1174 							static if (hasTreePath || hasTreePathNG) visitor.tree_path.back = cast(int) FieldNo;
1175 							if (mixin("this." ~ member).visit!order(mixin("data." ~ member), visitor))
1176 							{
1177 								return true;
1178 							}
1179 						}
1180 						goto case;
1181 					}
1182 					// the dummy case needed because every `goto case` should be followed by a case clause
1183 					case len2:
1184 						// flow cannot get here directly
1185 						if (start_value == len2)
1186 							assert(0);
1187 					break;
1188 					default:
1189 						assert(0);
1190 				}
1191 			}
1192 		}
1193 
1194 		return false;
1195 	}
1196 }
1197 
1198 private auto getIndex(Data, M)(ref M model, size_t i)
1199 {
1200 	static if (dataHasStaticArrayModel!Data || 
1201 	           dataHasRandomAccessRangeModel!Data ||
1202 	           dataHasAggregateModel!Data)
1203 		return i;
1204 	else static if (dataHasAssociativeArrayModel!Data)
1205 		return model.keys[i];
1206 	else
1207 		static assert(0);
1208 }
1209 
1210 private auto getLength(Data, alias data)()
1211 {
1212 	static if (dataHasStaticArrayModel!Data || 
1213 	           dataHasRandomAccessRangeModel!Data ||
1214 	           dataHasAssociativeArrayModel!Data)
1215 		return data.length;
1216 	else static if (dataHasAggregateModel!Data)
1217 		return DrawableMembers!Data.length;
1218 	else
1219 		static assert(0);
1220 }
1221 
1222 private enum PropertyKind { setter, getter }
1223 
1224 auto setPropertyByTreePath(string propertyName, Value, Data, Model)(auto ref Data data, ref Model model, int[] path, Value value)
1225 {
1226 	auto pv = PropertyVisitor!(propertyName, Value)();
1227 	pv.path.value = path;
1228 	pv.value = value;
1229 	pv.propertyKind = PropertyKind.setter;
1230 	model.visitForward(data, pv);
1231 }
1232 
1233 auto getPropertyByTreePath(string propertyName, Value, Data, Model)(auto ref Data data, ref Model model, int[] path)
1234 {
1235 	auto pv = PropertyVisitor!(propertyName, Value)();
1236 	pv.path.value = path;
1237 	pv.propertyKind = PropertyKind.getter;
1238 	model.visitForward(data, pv);
1239 	return pv.value;
1240 }
1241 
1242 private struct PropertyVisitor(string propertyName, Value)
1243 {
1244 	import std.typecons : Nullable;
1245 
1246 	TreePathVisitor default_visitor;
1247 	alias default_visitor this;
1248 
1249 	PropertyKind propertyKind;
1250 	Nullable!Value value;
1251 	bool completed;
1252 
1253 	this(Value value)
1254 	{
1255 		this.value = value;
1256 	}
1257 
1258 	bool complete()
1259 	{
1260 		return completed;
1261 	}
1262 
1263 	void enterNode(Order order, Data, Model)(ref const(Data) data, ref Model model)
1264 	{
1265 		static if (is(typeof(mixin("model." ~ propertyName))))
1266 		{
1267 			if (propertyKind == PropertyKind.getter)
1268 				value = mixin("model." ~ propertyName);
1269 			else if (propertyKind == PropertyKind.setter)
1270 				mixin("model." ~ propertyName) = value.get;
1271 		}
1272 		else
1273 			value.nullify;
1274 
1275 		processLeaf!order(data, model);
1276 	}
1277 
1278 	void processLeaf(Order order, Data, Model)(ref const(Data) data, ref Model model)
1279 	{
1280 		assert(!completed);
1281 		completed = tree_path.value[] == path.value[];
1282 	}
1283 }
1284 
1285 void applyByTreePath(T, Data, Model)(auto ref Data data, ref Model model, const(int)[] path, void delegate(ref const(T) value) dg)
1286 {
1287 	auto pv = ApplyVisitor!T();
1288 	pv.path.value = path;
1289 	pv.dg = dg;
1290 	model.visitForward(data, pv);
1291 }
1292 
1293 private struct ApplyVisitor(T)
1294 {
1295 	import std.typecons : Nullable;
1296 
1297 	TreePathVisitor default_visitor;
1298 	alias default_visitor this;
1299 
1300 	void delegate(ref const(T) value) dg;
1301 	bool completed;
1302 
1303 	bool complete()
1304 	{
1305 		return completed;
1306 	}
1307 
1308 	void enterNode(Order order, Data, Model)(ref const(Data) data, ref Model model)
1309 	{
1310 		static if (is(Data == T))
1311 		{
1312 			completed = tree_path.value[] == path.value[];
1313 			if (completed)
1314 			{
1315 				dg(data);
1316 				return;
1317 			}
1318 		}
1319 
1320 		processLeaf!order(data, model);
1321 	}
1322 
1323 	void processLeaf(Order order, Data, Model)(ref const(Data) data, ref Model model)
1324 	{
1325 		static if (is(Data == T))
1326 		{
1327 			assert(!completed);
1328 			completed = tree_path.value[] == path.value[];
1329 			if (completed)
1330 				dg(data);
1331 		}
1332 	}
1333 }
1334 
1335 enum Order { Sinking, Bubbling, }
1336 
1337 private struct TwoFacedRange(Order order)
1338 {
1339 	int s, l;
1340 
1341 	@disable this();
1342 
1343 	this(size_t s, size_t l)
1344 	{
1345 		this.s = cast(int) s;
1346 		this.l = cast(int) l;
1347 	}
1348 
1349 	bool empty() const
1350 	{
1351 		return (-1 >= s) || (s >= l);
1352 	}
1353 
1354 	int front() const
1355 	{
1356 		assert(!empty);
1357 		return s;
1358 	}
1359 
1360 	void popFront()
1361 	{
1362 		assert(!empty);
1363 		static if (order == Order.Sinking)  s++; else
1364 		static if (order == Order.Bubbling) s--; else
1365 		static assert(0);
1366 	}
1367 }
1368 
1369 version(unittest) @Name("two_faced_range")
1370 unittest
1371 {
1372 	import unit_threaded;
1373 
1374 	int[] empty;
1375 
1376 	{
1377 		auto rf = TwoFacedRange!(Order.Sinking)(1, 2);
1378 		rf.should.be == [1];
1379 		// lower boundary is always inclusive
1380 		auto rb = TwoFacedRange!(Order.Bubbling)(1, 2);
1381 		rb.should.be == [1, 0];
1382 	}
1383 	{
1384 		auto rf = TwoFacedRange!(Order.Sinking)(2, 4);
1385 		rf.should.be == [2, 3];
1386 		// lower boundary is always inclusive
1387 		auto rb = TwoFacedRange!(Order.Bubbling)(2, 4);
1388 		rb.should.be == [2, 1, 0];
1389 	}
1390 	{
1391 		auto rf = TwoFacedRange!(Order.Sinking)(0, 4);
1392 		rf.should.be == [0, 1, 2, 3];
1393 		auto rb = TwoFacedRange!(Order.Bubbling)(0, 4);
1394 		rb.should.be == [0];
1395 	}
1396 	{
1397 		auto rf = TwoFacedRange!(Order.Sinking)(4, 4);
1398 		rf.should.be == empty;
1399 		auto rb = TwoFacedRange!(Order.Bubbling)(4, 4);
1400 		rb.should.be == empty;
1401 	}
1402 	{
1403 		auto rf = TwoFacedRange!(Order.Sinking)(0, 0);
1404 		rf.should.be == empty;
1405 		auto rb = TwoFacedRange!(Order.Bubbling)(0, 0);
1406 		rb.should.be == empty;
1407 	}
1408 }
1409 
1410 void visit(Model, Data, Visitor)(ref Model model, auto ref Data data, ref Visitor visitor, SizeType destination)
1411 {
1412 	if (destination == visitor.position)
1413 	{
1414 		// it's possible that position is equal to the destination
1415 		// but old destination is different so
1416 		// update destination
1417 		visitor.destination = destination;
1418 		return;
1419 	}
1420 
1421 	// visitor.position not visitor.destination!
1422 	// because the direction is defined by relations between
1423 	// the current position and the next destination
1424 	if (destination < visitor.position)
1425 	{
1426 		visitor.destination = destination;
1427 		model.visitBackward(data, visitor);
1428 	}
1429 	else
1430 	{
1431 		visitor.destination = destination;
1432 		model.visitForward(data, visitor);
1433 	}
1434 }
1435 
1436 void visitForward(Model, Data, Visitor)(ref Model model, auto ref const(Data) data, ref Visitor visitor)
1437 {
1438 	import std.traits : Unqual;
1439 	static assert(is(Unqual!(Model.Data) == Unqual!Data), "Data and Model is not corresponding: " ~ Data.stringof ~ " and " ~ Model.Data.stringof);
1440 	enum order = Order.Sinking;
1441 	static if (Visitor.treePathEnabled)
1442 	{
1443 		visitor.state = (visitor.path.value.length) ? visitor.State.seeking : visitor.State.rest;
1444 		visitor.deferred_change = 0;
1445 		visitor.total_shift = visitor.destination - visitor.position;
1446 		if (visitor.total_shift < 0)
1447 			visitor.total_shift = -visitor.total_shift;
1448 		visitor.curr_shift = 0;
1449 	}
1450 
1451 	visitor.enterTree!order(data, model);
1452 	model.visit!order(data, visitor);
1453 	// if the visitor traverse the whole tree
1454 	// its position points to the element behind
1455 	// the last one, so correct it
1456 	static if (Visitor.treePathEnabled)
1457 	{
1458 		import auxil.cursor;
1459 		if (!visitor.complete)
1460 			visitor.cursorY.fixUp;
1461 	}
1462 }
1463 
1464 void visitBackward(Model, Data, Visitor)(ref Model model, auto ref Data data, ref Visitor visitor)
1465 {
1466 	import std.traits : Unqual;
1467 	static assert(is(Unqual!(Model.Data) == Unqual!Data), "Data and Model is not corresponding: " ~ Data.stringof ~ " and " ~ Model.Data.stringof);
1468 	enum order = Order.Bubbling;
1469 	static if (Visitor.treePathEnabled)
1470 	{
1471 		visitor.state = (visitor.path.value.length) ? visitor.State.seeking : visitor.State.rest;
1472 		visitor.deferred_change = 0;
1473 		assert(visitor.position >= visitor.destination);
1474 		visitor.total_shift = visitor.position - visitor.destination;
1475 		if (visitor.total_shift < 0)
1476 			visitor.total_shift = -visitor.total_shift;
1477 		visitor.curr_shift = 0;
1478 	}
1479 	visitor.enterTree!order(data, model);
1480 	model.visit!order(data, visitor);
1481 }
1482 
1483 struct TreePath
1484 {
1485 	import std.algorithm : equal;
1486 	import std.experimental.allocator.mallocator : Mallocator;
1487 	import automem.vector : Vector;
1488 
1489 @safe:
1490 
1491 	Vector!(int, Mallocator)  value;
1492 
1493 	this(int[] v) @trusted
1494 	{
1495 		value.length = v.length;
1496 		import std.algorithm;
1497 		copy(v, value[]);
1498 	}
1499 
1500 	ref int back() return @nogc
1501 	{
1502 		assert(value.length);
1503 		return value[$-1];
1504 	}
1505 
1506 	void popBack() @nogc
1507 	{
1508 		value.popBack;
1509 	}
1510 
1511 	void clear() @nogc
1512 	{
1513 		value.clear;
1514 	}
1515 
1516 	auto put(int i) @nogc @trusted
1517 	{
1518 		value.put(i);
1519 	}
1520 
1521 	auto opEquals(typeof(this) other) @trusted
1522 	{
1523 		return value[].equal(other.value[]);
1524 	}
1525 
1526 	auto opEquals(int[] other) @trusted
1527 	{
1528 		return value[].equal(other);
1529 	}
1530 
1531 	import std.range : isOutputRange;
1532 	import std.format : FormatSpec;
1533 
1534 	void toString(Writer) (ref Writer w, scope const ref FormatSpec!char fmt) const  @trusted
1535 		if (isOutputRange!(Writer, char))
1536 	{
1537 		import std;
1538 		import std.conv : text;
1539 
1540 		copy("TreePath([", w);
1541 		if (value.length)
1542 		{
1543 			copy(text(value[0]), w);
1544 			if (value.length > 1)
1545 			{
1546 				foreach(e; value[1..$])
1547 					copy(text(", ", e), w);
1548 			}
1549 		}
1550 		w.put(']');
1551 		w.put(')');
1552 	}
1553 }
1554 
1555 version(unittest) @Name("TreePath")
1556 unittest
1557 {
1558 	assert(TreePath([1]).value == [1]);
1559 }
1560 
1561 version(unittest) @Name("null_visitor")
1562 unittest
1563 {
1564 	int[] data = [1, 2];
1565 	NullVisitor visitor;
1566 	auto model = makeModel(data);
1567 	model.visitForward(data, visitor);
1568 }
1569 
1570 import std.typecons : Flag;
1571 
1572 alias SizeEnabled     = Flag!"SizeEnabled";
1573 alias TreePathEnabled = Flag!"TreePathEnabled";
1574 
1575 alias NullVisitor      = DefaultVisitorImpl!(SizeEnabled.no,  TreePathEnabled.no );
1576 alias MeasuringVisitor = DefaultVisitorImpl!(SizeEnabled.yes, TreePathEnabled.no );
1577 alias TreePathVisitor  = DefaultVisitorImpl!(SizeEnabled.no,  TreePathEnabled.yes);
1578 alias DefaultVisitor   = DefaultVisitorImpl!(SizeEnabled.yes, TreePathEnabled.yes);
1579 
1580 /// Default implementation of Visitor
1581 struct DefaultVisitorImpl(
1582 	SizeEnabled _size_,
1583 	TreePathEnabled _tree_path_,
1584 )
1585 {
1586 	alias sizeEnabled     = _size_;
1587 	alias treePathEnabled = _tree_path_;
1588 
1589 	static if (sizeEnabled == SizeEnabled.yes)
1590 	{
1591 		SizeType size;
1592 
1593 		this(SizeType s) @safe @nogc nothrow
1594 		{
1595 			size = s;
1596 		}
1597 	}
1598 
1599 	static if (treePathEnabled == TreePathEnabled.yes)
1600 	{
1601 		enum State { seeking, first, rest, finishing, }
1602 		State state;
1603 		TreePath tree_path, path;
1604 		SizeType _position, deferred_change, _destination, total_shift, curr_shift;
1605 
1606 		import auxil.cursor;
1607 		Cursor cursorY;
1608 
1609 		@property position(SizeType v) { _position = v; }
1610 		@property position() { return _position; }
1611 		@property destination(SizeType v) { _destination = v; }
1612 		@property destination() { return _destination; }
1613 	}
1614 
1615 	void indent() {}
1616 	void unindent() {}
1617 	bool complete() @safe @nogc { return false; }
1618 	void enterTree(Order order, Data, Model)(auto ref const(Data) data, ref Model model) {}
1619 	void enterNode(Order order, Data, Model)(ref const(Data) data, ref Model model) {}
1620 	void leaveNode(Order order, Data, Model)(ref const(Data) data, ref Model model) {}
1621 	void processLeaf(Order order, Data, Model)(ref const(Data) data, ref Model model) {}
1622 }
1623 
1624 version(unittest) @Name("MeasuringVisitor")
1625 unittest
1626 {
1627 	import std.algorithm : map;
1628 	import unit_threaded : should, be;
1629 
1630 	auto data = [0, 1, 2, 3];
1631 	auto model = makeModel(data);
1632 	auto visitor = MeasuringVisitor(9);
1633 
1634 	model.collapsed = false;
1635 	model.visitForward(data, visitor);
1636 
1637 	model.size.should.be == 50;
1638 	model[].map!"a.size".should.be == [10, 10, 10, 10];
1639 }
1640 
1641 version(unittest) @Name("union")
1642 unittest
1643 {
1644 	union U
1645 	{
1646 		int i;
1647 		double d;
1648 	}
1649 
1650 	U u;
1651 	auto m = makeModel(u);
1652 }
1653 
1654 version(unittest) @Name("DurationTest")
1655 unittest
1656 {
1657 	import std.datetime : Duration;
1658 	import std.meta : AliasSeq;
1659 
1660 	@renderedAs!string
1661 	static struct DurationProxy
1662 	{
1663 		@ignored
1664 		Duration ptr;
1665 
1666 		this(Duration d)
1667 		{
1668 			ptr = d;
1669 		}
1670  
1671 		string opCast(T : string)()
1672 		{
1673 			return ptr.toISOExtString;
1674 		}
1675 	}
1676 
1677 	static struct Test
1678 	{
1679 		@renderedAs!DurationProxy
1680 		Duration d;
1681 	}
1682 
1683 	Test test;
1684 	auto m = makeModel(test);
1685 
1686 	import std.traits : FieldNameTuple;
1687 	import std.meta : AliasSeq;
1688 
1689 	version(none)
1690 	{
1691 		static assert(FieldNameTuple!(typeof(m))                                 == AliasSeq!("single_member_model"));
1692 		static assert(FieldNameTuple!(typeof(m.single_member_model))             == AliasSeq!("proxy", "proxy_model"));
1693 		static assert(FieldNameTuple!(typeof(m.single_member_model.proxy))       == AliasSeq!(""));
1694 		static assert(FieldNameTuple!(typeof(m.single_member_model.proxy_model)) == AliasSeq!("size"));
1695 	}
1696 
1697 	@renderedAs!string
1698 	Duration d;
1699 
1700 	static assert(dataHasAggregateModel!(TypeOf!d));
1701 	static assert(hasRenderedAs!d);
1702 
1703 	auto m2 = makeModel(d);
1704 	static assert(is(m2.Proxy == string));
1705 }