1 /**
2 Copyright: Copyright (c) 2017-2018 Andrey Penechko.
3 License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0).
4 Authors: Andrey Penechko.
5 */
6 
7 module voxelman.gui.widgets;
8 
9 import std.stdio;
10 import voxelman.gui;
11 import voxelman.math;
12 import voxelman.graphics;
13 import datadriven.entityman : EntityManager;
14 
15 void registerComponents(ref EntityManager widgets)
16 {
17 	widgets.registerComponent!ButtonState;
18 	widgets.registerComponent!ChildrenStash;
19 	widgets.registerComponent!ConditionData;
20 	widgets.registerComponent!DraggableSettings;
21 	widgets.registerComponent!DropDownData;
22 	widgets.registerComponent!IconData;
23 	widgets.registerComponent!ImageData;
24 	widgets.registerComponent!LinearLayoutSettings;
25 	widgets.registerComponent!ListData;
26 	widgets.registerComponent!ScrollableData;
27 	widgets.registerComponent!SingleLayoutSettings;
28 	widgets.registerComponent!TextData;
29 	widgets.registerComponent!UserCheckHandler;
30 	widgets.registerComponent!UserClickHandler;
31 	widgets.registerComponent!WidgetIndex;
32 	widgets.registerComponent!WidgetReference;
33 }
34 
35 @Component("gui.WidgetReference", Replication.none)
36 struct WidgetReference
37 {
38 	WidgetId widgetId;
39 }
40 
41 struct WidgetProxy
42 {
43 	WidgetId wid;
44 	GuiContext ctx;
45 
46 	alias wid this;
47 
48 	WidgetProxy set(Components...)(Components components) { ctx.widgets.set(wid, components); return this; }
49 	C* get(C)() { return ctx.widgets.get!C(wid); }
50 	C* getOrCreate(C)(C defVal = C.init) { return ctx.widgets.getOrCreate!C(wid, defVal); }
51 	bool has(C)() { return ctx.widgets.has!C(wid); }
52 	WidgetProxy remove(C)() { ctx.widgets.remove!C(wid); return this; }
53 	WidgetProxy createChild(Components...)(Components components) { return ctx.createWidget(wid, components); }
54 	WidgetProxy handlers(Handlers...)(Handlers h) { ctx.widgets.getOrCreate!WidgetEvents(wid).addEventHandlers(h); return this; }
55 	void addChild(WidgetId child) { ctx.addChild(wid, child); }
56 	bool postEvent(Event)(auto ref Event event) { return ctx.postEvent(this, event); }
57 	void toggleFlag(Component)() { if (ctx.widgets.has!Component(wid)) ctx.widgets.remove!Component(wid); else ctx.widgets.set(wid, Component()); }
58 
59 	void focus() { ctx.focusedWidget = wid; }
60 	void unfocus() { ctx.focusedWidget = 0; }
61 	void setFocus(bool isFocused) { if (isFocused) ctx.focusedWidget = wid; else ctx.focusedWidget = 0; }
62 }
63 
64 static struct ChildrenRange
65 {
66 	GuiContext ctx;
67 	WidgetId[] children;
68 	size_t length(){ return children.length; }
69 	WidgetProxy opIndex(size_t i) { return WidgetProxy(children[i], ctx); }
70 	int opApply(scope int delegate(WidgetProxy) del)
71 	{
72 		foreach(childId; children)
73 		{
74 			if (ctx.widgets.has!hidden(childId)) continue;
75 			if (auto ret = del(WidgetProxy(childId, ctx)))
76 				return ret;
77 		}
78 		return 0;
79 	}
80 }
81 
82 
83 enum baseColor = rgb(26, 188, 156);
84 enum hoverColor = rgb(22, 160, 133);
85 enum color_clouds = rgb(236, 240, 241);
86 enum color_silver = rgb(189, 195, 199);
87 enum color_concrete = rgb(149, 165, 166);
88 enum color_asbestos = rgb(127, 140, 141);
89 enum color_white = rgb(250, 250, 250);
90 enum color_gray = rgb(241, 241, 241);
91 
92 enum color_wet_asphalt = rgb(52, 73, 94);
93 
94 struct FrameParts
95 {
96 	WidgetProxy frame;
97 	alias frame this;
98 	WidgetProxy header;
99 	WidgetProxy container;
100 }
101 
102 struct Frame
103 {
104 	static:
105 	FrameParts create(WidgetProxy parent)
106 	{
107 		WidgetProxy frame = parent.createChild(WidgetType("Frame"))
108 			.setVLayout(0, padding4(0))
109 			.addBackground(color_clouds)
110 			.consumeMouse;
111 
112 		auto header = frame.createChild(WidgetType("Header"))
113 			.addBackground(color_white)
114 			.hexpand;
115 
116 		auto container = frame.createChild(WidgetType("Container")).hvexpand;
117 
118 		return FrameParts(frame, header, container);
119 	}
120 }
121 
122 @Component("gui.ConditionData", Replication.none)
123 struct ConditionData
124 {
125 	bool delegate() condition;
126 	bool invert;
127 }
128 
129 WidgetProxy visible_if(WidgetProxy widget, bool delegate() condition)
130 {
131 	widget.set(ConditionData(condition)).handlers(&updateVisibility); return widget;
132 }
133 
134 WidgetProxy visible_if_not(WidgetProxy widget, bool delegate() condition)
135 {
136 	widget.set(ConditionData(condition, true)).handlers(&updateVisibility); return widget;
137 }
138 
139 void updateVisibility(WidgetProxy widget, ref GuiUpdateEvent event)
140 {
141 	if (event.bubbling) return;
142 	auto data = widget.get!ConditionData;
143 	if (data.condition is null) return;
144 	bool isVisible = data.condition();
145 	if (data.invert) isVisible = !isVisible;
146 	if (isVisible)
147 		widget.remove!hidden;
148 	else
149 		widget.set(hidden());
150 }
151 
152 WidgetProxy addBackground(WidgetProxy widget, Color4ub color)
153 {
154 	widget.getOrCreate!WidgetStyle.color = color;
155 	widget.handlers(&PanelLogic.drawBackground);
156 	return widget;
157 }
158 
159 WidgetProxy addBorder(WidgetProxy widget, Color4ub color)
160 {
161 	widget.getOrCreate!WidgetStyle.borderColor = color;
162 	widget.handlers(&PanelLogic.drawBorder);
163 	return widget;
164 }
165 
166 struct PanelLogic
167 {
168 	static:
169 	WidgetProxy create(WidgetProxy parent, Color4ub color)
170 	{
171 		WidgetProxy panel = parent.createChild(
172 			WidgetEvents(&drawBackground),
173 			WidgetStyle(color), WidgetType("Panel"));
174 		return panel;
175 	}
176 
177 	void drawBackground(WidgetProxy widget, ref DrawEvent event)
178 	{
179 		if (event.sinking) {
180 			auto transform = widget.getOrCreate!WidgetTransform;
181 			auto style = widget.get!WidgetStyle;
182 			event.renderQueue.drawRectFill(vec2(transform.absPos), vec2(transform.size), event.depth, style.color);
183 			event.depth += 1;
184 			event.renderQueue.pushClipRect(irect(transform.absPos, transform.size));
185 		} else {
186 			event.renderQueue.popClipRect();
187 		}
188 	}
189 
190 	void drawBorder(WidgetProxy widget, ref DrawEvent event)
191 	{
192 		if (event.sinking) {
193 			auto transform = widget.getOrCreate!WidgetTransform;
194 			auto style = widget.get!WidgetStyle;
195 			event.renderQueue.drawRectLine(vec2(transform.absPos), vec2(transform.size), event.depth, style.borderColor);
196 			event.depth += 1;
197 		}
198 	}
199 }
200 
201 WidgetProxy createImage(WidgetProxy parent, ImageData data)
202 {
203 	return ImageLogic.create(parent, data);
204 }
205 
206 @Component("gui.ImageData", Replication.none)
207 struct ImageData
208 {
209 	Texture texture;
210 	irect subRect;
211 	int scale;
212 	Color4ub color = Colors.white;
213 }
214 
215 struct ImageLogic
216 {
217 	static:
218 	WidgetProxy create(WidgetProxy parent, ImageData data)
219 	{
220 		WidgetProxy image = parent.createChild(
221 			WidgetEvents(&measure, &drawWidget), data, WidgetType("Image"));
222 		return image;
223 	}
224 
225 	void measure(WidgetProxy widget, ref MeasureEvent event)
226 	{
227 		auto transform = widget.get!WidgetTransform;
228 		auto data = widget.get!ImageData;
229 		transform.measuredSize = data.subRect.size * data.scale;
230 	}
231 
232 	void drawWidget(WidgetProxy image, ref DrawEvent event)
233 	{
234 		if (event.sinking) {
235 			auto transform = image.getOrCreate!WidgetTransform;
236 			auto data = image.get!ImageData;
237 			event.renderQueue.texBatch.putRect(frect(transform.absPos, transform.size), frect(data.subRect), event.depth, data.color, data.texture);
238 			event.depth += 1;
239 		}
240 	}
241 }
242 
243 @Component("gui.IconData", Replication.none)
244 struct IconData
245 {
246 	SpriteRef sprite;
247 	Color4ub color;
248 	Alignment halign;
249 	Alignment valign;
250 }
251 
252 WidgetProxy createIcon(WidgetProxy parent, string iconId, ivec2 size, Color4ub color = Colors.white)
253 {
254 	return IconLogic.create(parent, parent.ctx.style.icon(iconId), color).minSize(size);
255 }
256 
257 WidgetProxy createIcon(WidgetProxy parent, SpriteRef sprite, ivec2 size, Color4ub color = Colors.white)
258 {
259 	return IconLogic.create(parent, sprite, color).minSize(size);
260 }
261 
262 WidgetProxy createIcon(WidgetProxy parent, SpriteRef sprite, Color4ub color = Colors.white,
263 	Alignment halign = Alignment.center, Alignment valign = Alignment.center)
264 {
265 	return IconLogic.create(parent, sprite, color, halign, valign);
266 }
267 
268 struct IconLogic
269 {
270 	static:
271 	WidgetProxy create(WidgetProxy parent, SpriteRef sprite, Color4ub color = Colors.white,
272 		Alignment halign = Alignment.center, Alignment valign = Alignment.center)
273 	{
274 		WidgetProxy icon = parent.createChild(
275 			WidgetEvents(&drawWidget), IconData(sprite, color, halign, valign), WidgetType("Icon"))
276 			.measuredSize(sprite.atlasRect.size);
277 		return icon;
278 	}
279 
280 	void drawWidget(WidgetProxy icon, ref DrawEvent event)
281 	{
282 		if (event.sinking) {
283 			auto transform = icon.getOrCreate!WidgetTransform;
284 			auto data = icon.get!IconData;
285 			auto alignmentOffset = rectAlignmentOffset(transform.measuredSize, data.halign, data.valign, transform.size);
286 			event.renderQueue.draw(*data.sprite, vec2(transform.absPos+alignmentOffset), event.depth, data.color);
287 			event.depth += 1;
288 		}
289 	}
290 }
291 
292 @Component("gui.WidgetIndex", Replication.none)
293 struct WidgetIndex
294 {
295 	size_t index;
296 	WidgetId master;
297 }
298 
299 /// Used for widgets that hide some of the children, to store original list of children
300 @Component("gui.ChildrenStash", Replication.none)
301 struct ChildrenStash
302 {
303 	WidgetId[] widgets;
304 }
305 
306 struct PagedWidget
307 {
308 	static:
309 	// Move all children to
310 	void convert(WidgetProxy widget, size_t initialIndex)
311 	{
312 		if (auto cont = widget.get!WidgetContainer)
313 		{
314 			WidgetId[] pages = cont.children;
315 			widget.set(ChildrenStash(pages), WidgetEvents(&measure, &layout));
316 			cont.children = null;
317 			if (pages)
318 			{
319 				cont.put(pages[initialIndex]);
320 			}
321 		}
322 	}
323 
324 	void switchPage(WidgetProxy widget, size_t newPage)
325 	{
326 		if (auto cont = widget.get!WidgetContainer)
327 		{
328 			auto pages = widget.get!ChildrenStash.widgets;
329 			if (newPage < pages.length)
330 				cont.children[0] = pages[newPage];
331 		}
332 	}
333 
334 	void attachToButton(WidgetProxy selectorButton, size_t index)
335 	{
336 		selectorButton.set(WidgetIndex(index));
337 		selectorButton.getOrCreate!WidgetEvents.addEventHandler(&onButtonClick);
338 	}
339 
340 	void onButtonClick(WidgetProxy widget, ref PointerClickEvent event)
341 	{
342 		auto data = widget.get!UserClickHandler;
343 		data.onClick();
344 	}
345 
346 	void measure(WidgetProxy widget, ref MeasureEvent event)
347 	{
348 		auto transform = widget.get!WidgetTransform;
349 		foreach(child; widget.children)
350 		{
351 			auto childTransform = child.get!WidgetTransform;
352 			transform.measuredSize = childTransform.size;
353 		}
354 	}
355 
356 	void layout(WidgetProxy widget, ref LayoutEvent event)
357 	{
358 		auto transform = widget.get!WidgetTransform;
359 		foreach(child; widget.children)
360 		{
361 			auto childTransform = child.get!WidgetTransform;
362 			childTransform.relPos = ivec2(0,0);
363 			childTransform.size = transform.size;
364 		}
365 	}
366 }
367 
368 struct CollapsableParts
369 {
370 	WidgetProxy collapsable;
371 	alias collapsable this;
372 	WidgetProxy header;
373 	WidgetProxy container;
374 }
375 
376 /// On user click toggles
377 struct CollapsableWidget
378 {
379 	static:
380 	CollapsableParts create(WidgetProxy parent, bool expanded = false)
381 	{
382 		auto collapsable = parent.createChild(
383 			WidgetType("Collapsable")).hexpand;
384 		VLayout.attachTo(collapsable, 2, padding4(0));
385 
386 		auto header = collapsable.createChild(
387 			WidgetType("Header"),
388 			ButtonState(),
389 			WidgetEvents(&onHeaderClick, &drawButtonStateBack, &pointerMoved, &pointerPressed,
390 					&pointerReleased, &enterWidget, &leaveWidget)).hexpand;
391 
392 		auto container = collapsable.createChild().hexpand;
393 
394 		if (!expanded) toggle(collapsable);
395 		return CollapsableParts(collapsable, header, container);
396 	}
397 
398 	void onHeaderClick(WidgetProxy header, ref PointerClickEvent event)
399 	{
400 		auto tran = header.get!WidgetTransform;
401 		toggle(WidgetProxy(tran.parent, header.ctx));
402 	}
403 
404 	void toggle(WidgetProxy collapsable)
405 	{
406 		collapsable.children[1].toggleFlag!hidden;
407 	}
408 
409 	mixin ButtonPointerLogic!ButtonState;
410 }
411 
412 @Component("gui.TextData", Replication.none)
413 struct TextData
414 {
415 	string text;
416 	Alignment halign;
417 	Alignment valign;
418 	Color4ub color;
419 }
420 
421 WidgetProxy createText(WidgetProxy parent, string text,
422 	Alignment halign = Alignment.center, Alignment valign = Alignment.center)
423 {
424 	return TextLogic.create(parent, text,
425 		parent.ctx.style.font,
426 		parent.ctx.style.color,
427 		halign, valign);
428 }
429 
430 struct TextLogic
431 {
432 	static:
433 	WidgetProxy create(
434 		WidgetProxy parent,
435 		string text,
436 		FontRef font,
437 		Color4ub color,
438 		Alignment halign,
439 		Alignment valign)
440 	{
441 		WidgetProxy textWidget = parent.createChild(
442 			TextData(text, halign, valign, color),
443 			WidgetEvents(&drawText),
444 			WidgetType("Text"))
445 				.minSize(0, font.metrics.height);
446 		setText(textWidget, text);
447 		return textWidget;
448 	}
449 
450 	void setText(WidgetProxy widget, string text)
451 	{
452 		auto data = widget.get!TextData;
453 		data.text = text;
454 
455 		TextMesherParams params;
456 		params.font = widget.ctx.style.font;
457 		params.monospaced = false;
458 		measureText(params, text);
459 
460 		widget.measuredSize(ivec2(params.size));
461 	}
462 
463 	void drawText(WidgetProxy widget, ref DrawEvent event)
464 	{
465 		if (event.bubbling) return;
466 
467 		auto data = widget.get!TextData;
468 		auto transform = widget.getOrCreate!WidgetTransform;
469 		auto alignmentOffset = rectAlignmentOffset(transform.measuredSize, data.halign, data.valign, transform.size);
470 
471 		auto params = event.renderQueue.startTextAt(vec2(transform.absPos));
472 		params.monospaced = false;
473 		params.depth = event.depth;
474 		params.color = data.color;
475 		params.origin += alignmentOffset;
476 		params.meshText(data.text);
477 
478 		event.depth += 1;
479 	}
480 }
481 
482 enum BUTTON_PRESSED = 0b0001;
483 enum BUTTON_HOVERED = 0b0010;
484 enum BUTTON_SELECTED = 0b0100;
485 
486 enum buttonNormalColor = rgb(255, 255, 255);
487 enum buttonHoveredColor = rgb(241, 241, 241);
488 enum buttonPressedColor = rgb(229, 229, 229);
489 enum buttonSelectedColor = rgb(229, 229, 255);
490 Color4ub[8] buttonColors = [
491 buttonNormalColor, buttonNormalColor,
492 buttonHoveredColor, buttonPressedColor,
493 buttonSelectedColor, buttonSelectedColor,
494 buttonSelectedColor, buttonSelectedColor];
495 
496 @Component("gui.ButtonState", Replication.none)
497 struct ButtonState
498 {
499 	uint data;
500 	bool pressed() { return (data & BUTTON_PRESSED) != 0; }
501 	bool hovered() { return (data & BUTTON_HOVERED) != 0; }
502 	bool selected() { return (data & BUTTON_SELECTED) != 0; }
503 	void toggleSelected() { data = data.toggle_flag(BUTTON_SELECTED); }
504 }
505 
506 WidgetProxy createIconTextButton(WidgetProxy parent, SpriteRef icon, string text, ClickHandler handler = null) {
507 	return IconTextButtonLogic.create(parent, icon, text, handler);
508 }
509 WidgetProxy createIconTextButton(WidgetProxy parent, string iconId, string text, ClickHandler handler = null) {
510 	return IconTextButtonLogic.create(parent, parent.ctx.style.icon(iconId), text, handler);
511 }
512 
513 struct IconTextButtonLogic
514 {
515 	static:
516 	WidgetProxy create(WidgetProxy parent, SpriteRef icon, string text, ClickHandler handler = null)
517 	{
518 		WidgetProxy button = parent.createChild(
519 			UserClickHandler(), ButtonState(),
520 			WidgetEvents(
521 				&drawButtonStateBack, &pointerMoved, &pointerPressed,
522 				&pointerReleased, &enterWidget, &leaveWidget),
523 			WidgetType("IconTextButton"))
524 			.setHLayout(2, padding4(2), Alignment.center);
525 
526 		button.createIcon(icon, ivec2(16, 16), Colors.black);
527 		button.createText(text);
528 		setHandler(button, handler);
529 
530 		return button;
531 	}
532 
533 	mixin ButtonPointerLogic!ButtonState;
534 	mixin ButtonClickLogic!UserClickHandler;
535 }
536 
537 WidgetProxy createTextButton(WidgetProxy parent, string text, ClickHandler handler = null) {
538 	return TextButtonLogic.create(parent, text, handler);
539 }
540 
541 struct TextButtonLogic
542 {
543 	static:
544 	WidgetProxy create(WidgetProxy parent, string text, ClickHandler handler = null)
545 	{
546 		WidgetProxy button = parent.createChild(
547 			UserClickHandler(), ButtonState(),
548 			WidgetEvents(
549 				&drawButtonStateBack, &pointerMoved, &pointerPressed,
550 				&pointerReleased, &enterWidget, &leaveWidget),
551 			WidgetType("TextButton"));
552 
553 		button.createText(text);
554 		setHandler(button, handler);
555 		SingleLayout.attachTo(button, 2);
556 
557 		return button;
558 	}
559 
560 	mixin ButtonPointerLogic!ButtonState;
561 	mixin ButtonClickLogic!UserClickHandler;
562 }
563 
564 void drawButtonStateBack(WidgetProxy widget, ref DrawEvent event)
565 {
566 	if (event.bubbling) return;
567 
568 	auto state = widget.get!ButtonState;
569 	auto transform = widget.getOrCreate!WidgetTransform;
570 
571 	event.renderQueue.drawRectFill(vec2(transform.absPos), vec2(transform.size), event.depth, buttonColors[state.data & 0b111]);
572 	//event.renderQueue.drawRectLine(vec2(transform.absPos), vec2(transform.size), event.depth+1, rgb(230,230,230));
573 	event.depth += 1;
574 }
575 
576 /// Assumes that parent has ToggleButtonData data and uses its data and isChecked fields
577 struct CheckIconLogic
578 {
579 	static:
580 	WidgetProxy create(WidgetProxy parent, ivec2 size)
581 	{
582 		WidgetTransform t;
583 		t.measuredSize = size;
584 		return parent.createChild(t, WidgetEvents(&drawWidget), WidgetType("CheckIcon"));
585 	}
586 
587 	void drawWidget(WidgetProxy widget, ref DrawEvent event)
588 	{
589 		if (event.bubbling) return;
590 
591 		auto tran = widget.getOrCreate!WidgetTransform;
592 		auto parentData = widget.ctx.get!UserCheckHandler(tran.parent);
593 		auto parentState = widget.ctx.get!ButtonState(tran.parent);
594 
595 		event.renderQueue.drawRectFill(vec2(tran.absPos), vec2(tran.size), event.depth, buttonColors[parentState.data & 0b11]);
596 		if (parentData.isChecked)
597 			event.renderQueue.drawRectFill(vec2(tran.absPos + 2), vec2(tran.size - 4), event.depth+1, color_wet_asphalt);
598 		event.renderQueue.drawRectLine(vec2(tran.absPos), vec2(tran.size), event.depth+1, color_wet_asphalt);
599 		event.depth += 2;
600 	}
601 }
602 
603 WidgetProxy createCheckButton(WidgetProxy parent, string text, CheckHandler handler = null) {
604 	return CheckButtonLogic.create(parent, text, handler);
605 }
606 
607 WidgetProxy createCheckButton(WidgetProxy parent, string text, bool* flag) {
608 	alias Del = ref bool delegate();
609 	alias Fun = ref bool function();
610 	Del dg;
611 	dg.ptr = flag;
612 	dg.funcptr = cast(Fun)&bool_delegate;
613 	return CheckButtonLogic.create(parent, text, dg);
614 }
615 
616 ref bool bool_delegate(bool* value)
617 {
618 	return *value;
619 }
620 
621 struct CheckButtonLogic
622 {
623 	static:
624 	WidgetProxy create(WidgetProxy parent, string text, CheckHandler handler = null)
625 	{
626 		WidgetProxy check = parent.createChild(
627 			UserCheckHandler(), ButtonState(),
628 			WidgetEvents(&pointerMoved, &pointerPressed, &pointerReleased, &enterWidget, &leaveWidget),
629 			WidgetType("CheckButton"));
630 
631 		auto iconSize = parent.ctx.style.font.metrics.height;
632 		auto icon = CheckIconLogic.create(check, ivec2(iconSize, iconSize));
633 
634 		check.createText(text);
635 
636 		setHandler(check, handler);
637 		HLayout.attachTo(check, 2, padding4(2));
638 
639 		return check;
640 	}
641 
642 	mixin ButtonPointerLogic!ButtonState;
643 	mixin ButtonClickLogic!UserCheckHandler;
644 }
645 
646 alias OptionSelectHandler = void delegate(size_t);
647 @Component("gui.DropDownData", Replication.none)
648 struct DropDownData
649 {
650 	OptionSelectHandler handler;
651 	void onClick(size_t index) {
652 		if (selectedOption == index) return;
653 		selectedOption = index;
654 		if (handler) handler(selectedOption);
655 	}
656 	string[] options;
657 	size_t selectedOption;
658 	string optionText() { return options[selectedOption]; }
659 }
660 
661 struct DropDown
662 {
663 	static:
664 	WidgetProxy create(WidgetProxy parent, string[] options, size_t selectedOption, OptionSelectHandler handler = null)
665 	{
666 		WidgetProxy dropdown = BaseButton.create(parent)
667 			.handlers(&drawButtonStateBack, &onWidgetClick)
668 			.set(
669 				WidgetType("DropDown"),
670 				DropDownData(handler, options, selectedOption))
671 			.setHLayout(0, padding4(2), Alignment.center);
672 
673 		dropdown.createText(options[selectedOption]);
674 		dropdown.hfill;
675 		dropdown.createIcon(parent.ctx.style.icon("arrow-up-down"), ivec2(16, 16), Colors.black);
676 
677 		return dropdown;
678 	}
679 
680 	void onWidgetClick(WidgetProxy widget, ref PointerClickEvent event)
681 	{
682 		toggleDropDown(widget);
683 	}
684 
685 	void toggleDropDown(WidgetProxy widget)
686 	{
687 		auto tr = widget.get!WidgetTransform;
688 		auto data = widget.get!DropDownData;
689 		auto state = widget.get!ButtonState;
690 		state.toggleSelected;
691 
692 		if (state.selected)
693 		{
694 			auto optionsOverlay = widget.ctx.createOverlay
695 				.consumeMouse
696 				.handlers(&onOverlayPress)
697 				.set(WidgetReference(widget));
698 
699 			widget.set(WidgetReference(optionsOverlay));
700 
701 			auto options = optionsOverlay.createChild()
702 				.pos(tr.absPos+ivec2(0, tr.size.y))
703 				.addBackground(color_gray)
704 				.minSize(tr.size.x, 0)
705 				.setVLayout(2, padding4(2));
706 
707 			foreach(i, option; data.options)
708 			{
709 				auto button = BaseButton.create(options)
710 					.set(WidgetIndex(i, widget), WidgetType("DropDownOption"))
711 					.handlers(&onOptionClick, &drawButtonStateBack)
712 					.hexpand
713 					.setSingleLayout(2, Alignment.min);
714 				button.createText(option);
715 			}
716 		}
717 		else
718 		{
719 			auto overlayRef = widget.get!WidgetReference;
720 			widget.ctx.removeWidget(overlayRef.widgetId);
721 			widget.remove!WidgetReference;
722 		}
723 	}
724 
725 	void onOverlayPress(WidgetProxy overlay, ref PointerPressEvent event)
726 	{
727 		if (event.sinking) return;
728 		auto dropdownRef = overlay.get!WidgetReference;
729 		toggleDropDown(WidgetProxy(dropdownRef.widgetId, overlay.ctx));
730 		event.handled = true;
731 	}
732 
733 	void onOptionClick(WidgetProxy option, ref PointerClickEvent event)
734 	{
735 		auto index = option.get!WidgetIndex;
736 		auto dropdown = WidgetProxy(index.master, option.ctx);
737 
738 		auto data = dropdown.get!DropDownData;
739 		data.onClick(index.index);
740 		toggleDropDown(dropdown);
741 		TextLogic.setText(dropdown.children[0], data.optionText);
742 	}
743 
744 	mixin ButtonPointerLogic!ButtonState;
745 }
746 
747 struct BaseButton
748 {
749 	static:
750 	WidgetProxy create(WidgetProxy parent)
751 	{
752 		return parent.createChild(ButtonState(),
753 			WidgetEvents(
754 				&pointerMoved, &pointerPressed,
755 				&pointerReleased, &enterWidget, &leaveWidget),
756 			WidgetType("BaseButton"));
757 	}
758 	mixin ButtonPointerLogic!ButtonState;
759 }
760 
761 mixin template ButtonPointerLogic(State)
762 {
763 	static:
764 	void pointerMoved(WidgetProxy widget, ref PointerMoveEvent event) { event.handled = true; }
765 
766 	void pointerPressed(WidgetProxy widget, ref PointerPressEvent event)
767 	{
768 		if (event.sinking) return;
769 		widget.get!State.data |= BUTTON_PRESSED;
770 		event.handled = true;
771 	}
772 
773 	void pointerReleased(WidgetProxy widget, ref PointerReleaseEvent event)
774 	{
775 		widget.get!State.data &= ~BUTTON_PRESSED;
776 		event.handled = true;
777 	}
778 
779 	void enterWidget(WidgetProxy widget, ref PointerEnterEvent event)
780 	{
781 		widget.get!State.data |= BUTTON_HOVERED;
782 	}
783 
784 	void leaveWidget(WidgetProxy widget, ref PointerLeaveEvent event)
785 	{
786 		widget.get!State.data &= ~BUTTON_HOVERED;
787 	}
788 }
789 
790 alias CheckHandler = ref bool delegate();
791 
792 @Component("gui.BoolBinding", Replication.none)
793 struct BoolBinding
794 {
795 	bool* value;
796 }
797 
798 @Component("gui.UserCheckHandler", Replication.none)
799 struct UserCheckHandler
800 {
801 	CheckHandler handler;
802 	void onClick() { if (handler) toggle_bool(handler()); }
803 	bool isChecked() { return handler ? handler() : false; }
804 }
805 
806 alias ClickHandler = void delegate();
807 
808 @Component("gui.UserClickHandler", Replication.none)
809 struct UserClickHandler
810 {
811 	void onClick() { if (handler) handler(); }
812 	ClickHandler handler;
813 }
814 
815 mixin template ButtonClickLogic(Data)
816 {
817 	void clickWidget(WidgetProxy widget, ref PointerClickEvent event)
818 	{
819 		auto data = widget.get!Data;
820 		data.onClick();
821 	}
822 
823 	void setHandler(WidgetProxy button, typeof(Data.handler) handler) {
824 		auto data = button.get!Data;
825 		auto events = button.get!WidgetEvents;
826 		if (!data.handler) events.addEventHandler(&clickWidget);
827 		data.handler = handler;
828 	}
829 }
830 
831 /// Widget will catch mouse events from bubbling
832 WidgetProxy consumeMouse(WidgetProxy widget) { widget.handlers(&handlePointerMoved); return widget; }
833 
834 void handlePointerMoved(WidgetProxy widget, ref PointerMoveEvent event) { event.handled = true; }
835 
836 WidgetProxy hline(WidgetProxy parent) { HLine.create(parent); return parent; }
837 WidgetProxy vline(WidgetProxy parent) { VLine.create(parent); return parent; }
838 
839 alias HLine = Line!true;
840 alias VLine = Line!false;
841 
842 struct Line(bool horizontal)
843 {
844 	static:
845 	static if (horizontal) {
846 		WidgetProxy create(WidgetProxy parent) {
847 			return parent.createChild(WidgetEvents(&drawWidget, &measure), WidgetType("HLine"), WidgetStyle(parent.ctx.style.color)).hexpand;
848 		}
849 		void measure(WidgetProxy widget, ref MeasureEvent event) {
850 			auto transform = widget.getOrCreate!WidgetTransform;
851 			transform.measuredSize = ivec2(0,1);
852 		}
853 	} else {
854 		WidgetProxy create(WidgetProxy parent) {
855 			return parent.createChild(WidgetEvents(&drawWidget, &measure), WidgetType("VLine"), WidgetStyle(parent.ctx.style.color)).vexpand;
856 		}
857 		void measure(WidgetProxy widget, ref MeasureEvent event) {
858 			auto transform = widget.getOrCreate!WidgetTransform;
859 			transform.measuredSize = ivec2(1,0);
860 		}
861 	}
862 	void drawWidget(WidgetProxy widget, ref DrawEvent event) {
863 		if (event.bubbling) return;
864 		auto transform = widget.getOrCreate!WidgetTransform;
865 		auto color = widget.get!WidgetStyle.color;
866 		event.renderQueue.drawRectFill(vec2(transform.absPos), vec2(transform.size), event.depth, color);
867 	}
868 }
869 
870 WidgetProxy hfill(WidgetProxy parent) { HFill.create(parent); return parent; }
871 WidgetProxy vfill(WidgetProxy parent) { VFill.create(parent); return parent; }
872 
873 alias HFill = Fill!true;
874 alias VFill = Fill!false;
875 
876 struct Fill(bool horizontal)
877 {
878 	static:
879 	WidgetProxy create(WidgetProxy parent)
880 	{
881 		static if (horizontal)
882 			return parent.createChild(WidgetType("Fill")).hexpand;
883 		else
884 			return parent.createChild(WidgetType("Fill")).vexpand;
885 	}
886 }
887 
888 WidgetProxy moveToTop(WidgetProxy widget) {
889 	AutoMoveToTop.attachTo(widget);
890 	return widget;
891 }
892 struct AutoMoveToTop
893 {
894 	static void attachTo(WidgetProxy widget)
895 	{
896 		widget.handlers(&onPress);
897 	}
898 	static void onPress(WidgetProxy widget, ref PointerPressEvent event)
899 	{
900 		auto parentId = widget.get!WidgetTransform.parent;
901 		if (parentId)
902 		{
903 			if (auto children = widget.ctx.get!WidgetContainer(parentId))
904 			{
905 				children.bringToFront(widget.wid);
906 			}
907 		}
908 	}
909 }
910 
911 @Component("DraggableSettings", Replication.none)
912 struct DraggableSettings
913 {
914 	PointerButton onButton;
915 }
916 
917 WidgetProxy makeDraggable(WidgetProxy widget, PointerButton onButton = PointerButton.PB_1) {
918 	DraggableLogic.attachTo(widget, onButton); return widget; }
919 
920 struct DraggableLogic
921 {
922 	static:
923 	void attachTo(WidgetProxy widget, PointerButton onButton)
924 	{
925 		widget.handlers(&onPress, &onDrag).set(DraggableSettings(onButton));
926 	}
927 
928 	void onPress(WidgetProxy widget, ref PointerPressEvent event)
929 	{
930 		if (event.sinking) return;
931 		if (event.button == widget.get!DraggableSettings.onButton)
932 		{
933 			event.handled = true;
934 			event.beginDrag = true;
935 		}
936 	}
937 
938 	void onDrag(WidgetProxy widget, ref DragEvent event)
939 	{
940 		widget.get!WidgetTransform.relPos += event.delta;
941 	}
942 }
943 
944 struct ScrollableAreaParts
945 {
946 	WidgetProxy scrollable;
947 	alias scrollable this;
948 	WidgetProxy canvas;
949 }
950 
951 @Component("ScrollableData", Replication.none)
952 struct ScrollableData
953 {
954 	ivec2 contentOffset;
955 	ivec2 contentSize;
956 	ivec2 windowSize;
957 	bool isScrollbarNeeded() { return contentSize.y > windowSize.y; }
958 	ivec2 maxPos() { return vector_max(ivec2(0,0), contentSize - windowSize); }
959 	void clampPos() {
960 		contentOffset = vector_clamp(contentOffset, ivec2(0,0), maxPos);
961 	}
962 }
963 
964 struct ScrollableArea
965 {
966 	static:
967 	ScrollableAreaParts create(WidgetProxy parent) {
968 		auto scrollable = parent.createChild(WidgetType("Scrollable"),
969 			ScrollableData(),
970 			WidgetEvents(&onScroll, &measure, &layout, &onSliderDrag)).setHLayout(0, padding4(0)).measuredSize(10, 10);
971 		auto container = scrollable.createChild(WidgetType("Container"), WidgetEvents(&clipDraw)).hvexpand;
972 		auto canvas = container.createChild(WidgetType("Canvas"));
973 		auto scrollbar = ScrollBarLogic.create(scrollable, scrollable/*receive drag event*/);
974 		scrollbar.vexpand;
975 
976 		return ScrollableAreaParts(scrollable, canvas);
977 	}
978 
979 	void onSliderDrag(WidgetProxy scrollable, ref ScrollBarEvent event)
980 	{
981 		auto data = scrollable.get!ScrollableData;
982 		if (event.maxPos)
983 			data.contentOffset.y = (event.pos * data.maxPos.y) / event.maxPos;
984 		else
985 			data.contentOffset.y = 0;
986 		//data.clampPos; unnesessary
987 	}
988 
989 	void onScroll(WidgetProxy scrollable, ref ScrollEvent event)
990 	{
991 		if (event.sinking) return;
992 		auto data = scrollable.get!ScrollableData;
993 		data.contentOffset += ivec2(event.delta) * 50; // TODO settings
994 		data.clampPos;
995 		event.handled = true; // TODO do not handle if not scrolled, so higher scrolls will work
996 	}
997 
998 	void clipDraw(WidgetProxy scrollable, ref DrawEvent event)
999 	{
1000 		if (event.sinking) {
1001 			auto transform = scrollable.getOrCreate!WidgetTransform;
1002 			event.renderQueue.pushClipRect(irect(transform.absPos, transform.size));
1003 		} else {
1004 			event.renderQueue.popClipRect();
1005 		}
1006 	}
1007 
1008 	void measure(WidgetProxy scrollable, ref MeasureEvent event)
1009 	{
1010 		// scrollable -> container -> canvas
1011 		auto canvasTransform = scrollable.children[0].children[0].get!WidgetTransform;
1012 		scrollable.get!ScrollableData.contentSize = canvasTransform.measuredSize;
1013 	}
1014 
1015 	void layout(WidgetProxy scrollable, ref LayoutEvent event)
1016 	{
1017 		auto data = scrollable.get!ScrollableData;
1018 		auto rootTransform = scrollable.get!WidgetTransform;
1019 		data.windowSize = rootTransform.size;
1020 		data.clampPos;
1021 
1022 		WidgetProxy cont = scrollable.children[0];
1023 		auto contTran = cont.get!WidgetTransform;
1024 
1025 		auto containerTransform = cont.get!WidgetTransform;
1026 		containerTransform.minSize = ivec2(0, 0);
1027 		containerTransform.measuredSize = ivec2(0, 0);
1028 
1029 		auto canvasTransform = cont.children[0].get!WidgetTransform;
1030 		auto scrollbar = scrollable.children[1];
1031 
1032 		if (data.isScrollbarNeeded)
1033 		{
1034 			// set scrollbar
1035 			scrollbar.remove!hidden;
1036 			ScrollBarLogic.setPosSize(scrollable.children[1], data.contentOffset.y, data.contentSize.y, data.windowSize.y);
1037 			canvasTransform.relPos.y = -data.contentOffset.y;
1038 		}
1039 		else
1040 		{
1041 			// no scrollbar
1042 			scrollbar.set(hidden());
1043 			canvasTransform.relPos.y = 0;
1044 		}
1045 	}
1046 }
1047 
1048 struct ScrollBarEvent
1049 {
1050 	int pos;
1051 	int maxPos;
1052 	mixin GuiEvent!();
1053 }
1054 
1055 struct ScrollBarParts
1056 {
1057 	WidgetProxy scrollbar;
1058 	alias scrollbar this;
1059 	WidgetProxy slider;
1060 }
1061 
1062 struct ScrollBarLogic
1063 {
1064 	static:
1065 	ScrollBarParts create(WidgetProxy parent, WidgetId eventReceiver = WidgetId(0)) {
1066 		auto scroll = parent.createChild(WidgetType("ScrollBar"))
1067 			.vexpand.minSize(10, 20).addBackground(color_gray);
1068 		auto slider = scroll.createChild(WidgetType("ScrollHandle"), WidgetReference(eventReceiver))
1069 			.minSize(10, 10)
1070 			.measuredSize(0, 100)
1071 			.handlers(&onSliderDrag, &DraggableLogic.onPress)
1072 			.addBackground(color_asbestos);
1073 		return ScrollBarParts(scroll, slider);
1074 	}
1075 
1076 	void onSliderDrag(WidgetProxy slider, ref DragEvent event)
1077 	{
1078 		auto tr = slider.get!WidgetTransform;
1079 		auto parent_tr = slider.ctx.get!WidgetTransform(tr.parent);
1080 		int pos = slider.ctx.state.curPointerPos.y - slider.ctx.state.draggedWidgetOffset.y - parent_tr.absPos.y;
1081 		int maxPos = parent_tr.size.y - tr.size.y;
1082 		pos = clamp(pos, 0, maxPos);
1083 		tr.relPos.y = pos;
1084 		auto reference = slider.get!WidgetReference;
1085 		if (reference.widgetId)
1086 			slider.ctx.postEvent(reference.widgetId, ScrollBarEvent(pos, maxPos));
1087 	}
1088 
1089 	// Assumes clamped canvasPos
1090 	void setPosSize(WidgetProxy scroll, int canvasPos, int canvasSize, int windowSize)
1091 	{
1092 		assert(canvasSize > windowSize);
1093 		auto scrollTransform = scroll.get!WidgetTransform;
1094 		auto sliderTransform = scroll.children[0].get!WidgetTransform;
1095 		int scrollSize = windowSize;
1096 		int sliderSize = (windowSize * scrollSize) / canvasSize;
1097 		int sliderMaxPos = scrollSize - sliderSize;
1098 
1099 		sliderTransform.relPos.y = canvasPos * sliderMaxPos / (canvasSize - windowSize);
1100 		sliderTransform.size.y = sliderSize;
1101 	}
1102 }
1103 
1104 @Component("gui.SingleLayoutSettings", Replication.none)
1105 struct SingleLayoutSettings
1106 {
1107 	int padding; /// borders around items
1108 	Alignment halign;
1109 	Alignment valign;
1110 }
1111 
1112 WidgetProxy setSingleLayout(WidgetProxy widget, int padding, Alignment halign = Alignment.center, Alignment valign = Alignment.center)
1113 {
1114 	SingleLayout.attachTo(widget, padding, halign, valign);
1115 	return widget;
1116 }
1117 
1118 /// For layouting single child with alignment and padding.
1119 struct SingleLayout
1120 {
1121 	static:
1122 	void attachTo(
1123 		WidgetProxy widget,
1124 		int padding,
1125 		Alignment halign = Alignment.center,
1126 		Alignment valign = Alignment.center)
1127 	{
1128 		widget.set(SingleLayoutSettings(padding, halign, valign));
1129 		widget.handlers(&measure, &layout);
1130 	}
1131 
1132 	void measure(WidgetProxy widget, ref MeasureEvent event)
1133 	{
1134 		auto settings = widget.get!SingleLayoutSettings;
1135 		ivec2 childSize;
1136 
1137 		ChildrenRange children = widget.children;
1138 		if (children.length > 0)
1139 		{
1140 			auto childTransform = children[0].get!WidgetTransform;
1141 			childSize = childTransform.size;
1142 		}
1143 
1144 		widget.get!WidgetTransform.measuredSize = childSize + settings.padding*2;
1145 	}
1146 
1147 	void layout(WidgetProxy widget, ref LayoutEvent event)
1148 	{
1149 		auto settings = widget.get!SingleLayoutSettings;
1150 		auto rootTransform = widget.get!WidgetTransform;
1151 
1152 		ivec2 childArea = rootTransform.size - settings.padding * 2;
1153 
1154 		ChildrenRange children = widget.children;
1155 		if (children.length > 0)
1156 		{
1157 			WidgetProxy child = children[0];
1158 			auto childTransform = child.get!WidgetTransform;
1159 
1160 			ivec2 childSize;
1161 			ivec2 relPos;
1162 
1163 			//widget.ctx.debugText.putfln("SLayout.layout %s tr %s",
1164 			//	widget.wid, *childTransform);
1165 
1166 			if (childTransform.hasVexpand) {
1167 				childSize.x = childArea.x;
1168 				relPos.x = settings.padding;
1169 			} else {
1170 				childSize.x = childTransform.measuredSize.x;
1171 				relPos.x = settings.padding + alignOnAxis(childSize.x, settings.halign, childArea.x);
1172 			}
1173 
1174 			if (childTransform.hasHexpand) {
1175 				childSize.y = childArea.y;
1176 				relPos.y = settings.padding;
1177 			} else {
1178 				childSize.y = childTransform.measuredSize.y;
1179 				relPos.y = settings.padding + alignOnAxis(childSize.y, settings.valign, childArea.y);
1180 			}
1181 
1182 			childTransform.relPos = relPos;
1183 			childTransform.size = childSize;
1184 		}
1185 	}
1186 }
1187 
1188 @Component("gui.LinearLayoutSettings", Replication.none)
1189 struct LinearLayoutSettings
1190 {
1191 	int spacing; /// distance between items
1192 	padding4 padding; /// borders around items
1193 	Alignment alignment;
1194 
1195 	// internal state
1196 	int numExpandableChildren;
1197 }
1198 
1199 alias HLayout = LinearLayout!true;
1200 alias VLayout = LinearLayout!false;
1201 
1202 alias setHLayout = setLinearLayout!true;
1203 alias setVLayout = setLinearLayout!false;
1204 
1205 WidgetProxy setLinearLayout(bool hori)(WidgetProxy widget, int spacing, padding4 padding, Alignment alignTo = Alignment.min)
1206 {
1207 	LinearLayout!hori.attachTo(widget, spacing, padding, alignTo);
1208 	return widget;
1209 }
1210 
1211 struct LinearLayout(bool horizontal)
1212 {
1213 	static:
1214 	WidgetProxy create(WidgetProxy parent, int spacing, padding4 padding, Alignment alignTo = Alignment.min)
1215 	{
1216 		WidgetProxy layout = parent.createChild(WidgetType("LinearLayout"));
1217 		attachTo(layout, spacing, padding, alignTo);
1218 		return layout;
1219 	}
1220 
1221 	void attachTo(WidgetProxy widget, int spacing, padding4 padding, Alignment alignTo = Alignment.min)
1222 	{
1223 		//writefln("attachTo %s %s", widget.widgetType, widget.wid);
1224 		widget.set(LinearLayoutSettings(spacing, padding, alignTo));
1225 		widget.getOrCreate!WidgetEvents.addEventHandlers(&measure, &layout);
1226 	}
1227 
1228 	void measure(WidgetProxy widget, ref MeasureEvent event)
1229 	{
1230 		auto settings = widget.get!LinearLayoutSettings;
1231 		settings.numExpandableChildren = 0;
1232 
1233 		int maxChildWidth = 0;
1234 		int childrenLength;
1235 
1236 		ChildrenRange children = widget.children;
1237 		foreach(child; children)
1238 		{
1239 			auto childTransform = child.get!WidgetTransform;
1240 			childrenLength += length(childTransform.size);
1241 			maxChildWidth = max(width(childTransform.size), maxChildWidth);
1242 			if (hasExpandableLength(child)) ++settings.numExpandableChildren;
1243 		}
1244 
1245 		static if (horizontal) int widthPad = settings.padding.vert;
1246 		else int widthPad = settings.padding.hori;
1247 
1248 		static if (horizontal) int lengthPad = settings.padding.hori;
1249 		else int lengthPad = settings.padding.vert;
1250 
1251 		int minRootWidth = maxChildWidth + widthPad;
1252 		int minRootLength = childrenLength + cast(int)(children.length-1)*settings.spacing + lengthPad;
1253 		auto transform = widget.get!WidgetTransform;
1254 		transform.measuredSize = sizeFromWidthLength(minRootWidth, minRootLength);
1255 	}
1256 
1257 	void layout(WidgetProxy widget, ref LayoutEvent event)
1258 	{
1259 		auto settings = widget.get!LinearLayoutSettings;
1260 		auto rootTransform = widget.get!WidgetTransform;
1261 
1262 		static if (horizontal) int widthPad = settings.padding.vert;
1263 		else int widthPad = settings.padding.hori;
1264 
1265 		int maxChildWidth = width(rootTransform.size) - widthPad;
1266 
1267 		int extraLength = length(rootTransform.size) - length(rootTransform.measuredSize);
1268 		int extraPerWidget = settings.numExpandableChildren > 0 ? extraLength/settings.numExpandableChildren : 0;
1269 
1270 		static if (horizontal) int topOffset = settings.padding.left;
1271 		else int topOffset = settings.padding.top;
1272 		static if (horizontal) int widthOffset = settings.padding.top;
1273 		else int widthOffset = settings.padding.left;
1274 
1275 		topOffset -= settings.spacing; // compensate extra spacing before first child
1276 
1277 		foreach(child; widget.children)
1278 		{
1279 			topOffset += settings.spacing;
1280 			auto childTransform = child.get!WidgetTransform;
1281 			childTransform.relPos = sizeFromWidthLength(widthOffset, topOffset);
1282 
1283 			ivec2 childSize = childTransform.constrainedSize;
1284 			if (hasExpandableLength(child)) length(childSize) += extraPerWidget;
1285 			if (hasExpandableWidth(child)) width(childSize) = maxChildWidth;
1286 			else width(childTransform.relPos) += alignOnAxis(width(childSize), settings.alignment, maxChildWidth);
1287 			childTransform.size = childSize;
1288 
1289 			//widget.ctx.debugText.putfln("LLayout.layout %s tr %s extra %s",
1290 			//	child.wid, *childTransform, extraPerWidget);
1291 
1292 			topOffset += length(childSize);
1293 		}
1294 	}
1295 
1296 	private:
1297 
1298 	bool hasExpandableWidth(WidgetProxy widget) {
1299 		static if (horizontal) return widget.hasVexpand;
1300 		else return widget.hasHexpand;
1301 	}
1302 
1303 	bool hasExpandableLength(WidgetProxy widget) {
1304 		static if (horizontal) return widget.hasHexpand;
1305 		else return widget.hasVexpand;
1306 	}
1307 
1308 	ivec2 sizeFromWidthLength(int width, int length) {
1309 		static if (horizontal) return ivec2(length, width);
1310 		else return ivec2(width, length);
1311 	}
1312 
1313 	ref int length(return ref ivec2 vector) {
1314 		static if (horizontal) return vector.x;
1315 		else return vector.y;
1316 	}
1317 
1318 	ref int width(ref ivec2 vector) {
1319 		static if (horizontal) return vector.y;
1320 		else return vector.x;
1321 	}
1322 }
1323 
1324 struct ColumnInfo
1325 {
1326 	string name;
1327 	int width = 100;
1328 	Alignment alignment;
1329 	enum int minWidth = 80;
1330 }
1331 
1332 enum TreeLineType
1333 {
1334 	leaf,
1335 	collapsedNode,
1336 	expandedNode
1337 }
1338 
1339 abstract class ListModel
1340 {
1341 	int numLines();
1342 	int numColumns();
1343 	ref ColumnInfo columnInfo(int column);
1344 	void getColumnText(int column, scope void delegate(const(char)[]) sink);
1345 	void getCellText(int column, int line, scope void delegate(const(char)[]) sink);
1346 	bool isLineSelected(int line);
1347 	void onLineClick(int line);
1348 	TreeLineType getLineType(int line) { return TreeLineType.leaf; }
1349 	int getLineIndent(int line) { return 0; }
1350 	void toggleLineFolding(int line) { }
1351 }
1352 
1353 
1354 alias SinkT = void delegate(const(char)[]);
1355 alias Formatter(Row) = void function(Row row, scope SinkT sink);
1356 
1357 struct Column(Row)
1358 {
1359 	string name;
1360 	int width;
1361 	Formatter!Row formatter;
1362 }
1363 
1364 struct ListInfo(Row)
1365 {
1366 	ColumnInfo[] columnInfos;
1367 	void function(Row row, scope SinkT sink)[] formatters;
1368 }
1369 
1370 ListInfo!Row parseListInfo(Row)()
1371 {
1372 	import std.traits;
1373 	ListInfo!Row result;
1374 	Row r;
1375 	foreach(string memberName; __traits(allMembers, Row))
1376 	{
1377 		foreach(attr; __traits(getAttributes, __traits(getMember, r, memberName)))
1378 		{
1379 			static if (is(typeof(attr) == Column!Row))
1380 			{
1381 				result.columnInfos ~= ColumnInfo(attr.name, attr.width);
1382 				result.formatters ~= attr.formatter;
1383 			}
1384 		}
1385 	}
1386 	return result;
1387 }
1388 
1389 class AutoListModel(Model) : ListModel
1390 {
1391 	alias Row = typeof(Model.init[0]);
1392 	Model model;
1393 	ListInfo!Row info = parseListInfo!Row;
1394 	int selectedRow = -1;
1395 
1396 	this(Model)(Model model)
1397 	{
1398 		this.model = model;
1399 	}
1400 
1401 	override int numLines() { return cast(int)model.length; }
1402 	override int numColumns() { return cast(int)info.columnInfos.length; }
1403 	override ref ColumnInfo columnInfo(int column) {
1404 		return info.columnInfos[column];
1405 	}
1406 	override void getColumnText(int column, scope SinkT sink) {
1407 		sink(info.columnInfos[column].name);
1408 	}
1409 	override void getCellText(int column, int row, scope SinkT sink) {
1410 		info.formatters[column](model[row], sink);
1411 	}
1412 	override bool isLineSelected(int row) {
1413 		return row == selectedRow;
1414 	}
1415 	override void onLineClick(int row) {
1416 		selectedRow = row;
1417 	}
1418 
1419 	bool hasSelected() @property {
1420 		return selectedRow < model.length && selectedRow >= 0;
1421 	}
1422 }
1423 
1424 class ArrayListModel(Row) : AutoListModel!(Row[])
1425 {
1426 	this(Row[] rows) { super(rows); }
1427 }
1428 
1429 @Component("gui.ListData", Replication.none)
1430 struct ListData
1431 {
1432 	ListModel model;
1433 	FontRef font;
1434 	ivec2 headerPadding = ivec2(5, 5);
1435 	ivec2 contentPadding = ivec2(5, 2);
1436 	enum scrollSpeedLines = 1;
1437 	int hoveredLine = -1;
1438 	ivec2 viewOffset; // on canvas
1439 
1440 	bool hasHoveredLine() { return hoveredLine >= 0 && hoveredLine < model.numLines; }
1441 	int textHeight() { return font.metrics.height; }
1442 	int lineHeight() { return textHeight + contentPadding.y*2; }
1443 	int headerHeight() { return textHeight + headerPadding.y*2; }
1444 	int canvasHeight() { return lineHeight * model.numLines; }
1445 }
1446 
1447 struct ColumnListLogic
1448 {
1449 	static:
1450 	WidgetProxy create(WidgetProxy parent, ListModel model)
1451 	{
1452 		WidgetProxy list = parent.createChild(
1453 			ListData(model, parent.ctx.style.font),
1454 			WidgetEvents(
1455 				&drawWidget, &pointerMoved, &pointerPressed, &pointerReleased,
1456 				&enterWidget, &leaveWidget, &clickWidget, &onScroll),
1457 			WidgetStyle(baseColor),
1458 			WidgetType("List"));
1459 		return list;
1460 	}
1461 
1462 	void drawWidget(WidgetProxy widget, ref DrawEvent event)
1463 	{
1464 		if (event.bubbling) return;
1465 
1466 		auto data = widget.get!ListData;
1467 		auto transform = widget.getOrCreate!WidgetTransform;
1468 		auto style = widget.get!WidgetStyle;
1469 
1470 		irect transformRect = irect(transform.absPos, transform.size);
1471 
1472 		int numLines = data.model.numLines;
1473 		int numColumns = data.model.numColumns;
1474 
1475 		int textHeight = data.textHeight;
1476 		int lineHeight = data.lineHeight;
1477 		int headerHeight = data.headerHeight;
1478 		int canvasHeight = lineHeight * data.model.numLines;
1479 
1480 		// calc content size in pixels
1481 		ivec2 canvasSize;
1482 		canvasSize.y = lineHeight * numLines;
1483 		foreach(column; 0..numColumns)
1484 			canvasSize.x += data.model.columnInfo(column).width;
1485 		int lastLine = numLines ? numLines-1 : 0;
1486 
1487 		// calc visible lines
1488 		data.viewOffset = vector_clamp(data.viewOffset, ivec2(0, 0), canvasSize);
1489 		int firstVisibleLine = clamp(data.viewOffset.y / lineHeight, 0, lastLine);
1490 		int viewEndPos = data.viewOffset.y + canvasSize.y;
1491 		int lastVisibleLine = clamp(viewEndPos / lineHeight, 0, lastLine);
1492 
1493 		int numVisibleLines = clamp(lastVisibleLine - firstVisibleLine + 1, 0, numLines);
1494 
1495 		// for folding arrow positioning
1496 		int charW = data.font.metrics.advanceX;
1497 
1498 		bool isLineHovered(int line) { return data.hoveredLine == line; }
1499 
1500 		void drawBackground()
1501 		{
1502 			int lineY = transform.absPos.y + headerHeight;
1503 			foreach(visibleLine; 0..numVisibleLines)
1504 			{
1505 				int line = firstVisibleLine + visibleLine;
1506 				auto color_selected = rgb(217, 235, 249);
1507 				auto color_hovered = rgb(207, 225, 239);
1508 
1509 				Color4ub color;
1510 				if (isLineHovered(line)) color = color_hovered;
1511 				else if (data.model.isLineSelected(line)) color = color_selected;
1512 				else color = line % 2 ? rgb(255, 255, 255) : rgb(250, 250, 250); // color_white
1513 
1514 				event.renderQueue.drawRectFill(
1515 					vec2(transform.absPos.x, lineY),
1516 					vec2(transform.size.x, lineHeight),
1517 					event.depth, color);
1518 				lineY += lineHeight;
1519 			}
1520 		}
1521 
1522 		void drawColumnHeader(int column, ivec2 pos, ivec2 size)
1523 		{
1524 			auto params = event.renderQueue.startTextAt(vec2(pos));
1525 			params.font = data.font;
1526 			params.color = color_wet_asphalt;
1527 			params.depth = event.depth+2;
1528 			//params.monospaced = true;
1529 			params.scissors = rectIntersection(irect(pos, size), transformRect);
1530 			//params.scissors = irect(pos, size);
1531 			params.meshText(data.model.columnInfo(column).name);
1532 		}
1533 
1534 		void drawCell(int column, int line, irect rect)
1535 		{
1536 			auto params = event.renderQueue.startTextAt(vec2(rect.position));
1537 			params.font = data.font;
1538 			params.color = color_wet_asphalt;
1539 			params.depth = event.depth+2;
1540 			//params.monospaced = true;
1541 			params.scissors = rectIntersection(rect, transformRect);
1542 
1543 			void sinkHandler(const(char)[] str) {
1544 				params.meshText(str);
1545 			}
1546 
1547 			if (column == 0)
1548 			{
1549 				params.origin.x += charW * data.model.getLineIndent(line);
1550 				final switch(data.model.getLineType(line))
1551 				{
1552 					case TreeLineType.leaf: params.meshText("  "); break;
1553 					case TreeLineType.collapsedNode: params.meshText("► "); break;
1554 					case TreeLineType.expandedNode: params.meshText("▼ "); break;
1555 				}
1556 			}
1557 
1558 			data.model.getCellText(column, line, &sinkHandler);
1559 			params.alignMeshedText(data.model.columnInfo(column).alignment, Alignment.min, rect.size);
1560 		}
1561 
1562 		event.renderQueue.pushClipRect(transformRect);
1563 		drawBackground();
1564 
1565 		int colX = transform.absPos.x;
1566 		// columns
1567 		foreach(column; 0..data.model.numColumns)
1568 		{
1569 			int colW = data.model.columnInfo(column).width;
1570 			int cellW = colW - data.contentPadding.x*2;
1571 
1572 			// separator
1573 			ivec2 separatorStart = ivec2(colX + colW-1, transform.absPos.y);
1574 			event.renderQueue.drawRectFill(vec2(separatorStart), vec2(1, transform.size.y), event.depth+3, color_silver);
1575 
1576 			// clip
1577 			event.renderQueue.pushClipRect(irect(colX+data.contentPadding.x, transform.absPos.y, cellW, transform.size.y));
1578 
1579 			// header
1580 			ivec2 headerPos  = ivec2(colX, transform.absPos.y) + data.headerPadding;
1581 			ivec2 headerSize = ivec2(colW, headerHeight) - data.headerPadding*2;
1582 			drawColumnHeader(column, headerPos, headerSize);
1583 			int lineY = transform.absPos.y + headerHeight;
1584 
1585 			// cells
1586 			foreach(line; 0..numVisibleLines)
1587 			{
1588 				ivec2 cellPos = ivec2(colX, lineY);
1589 				ivec2 cellSize = ivec2(colW, lineHeight);
1590 
1591 				ivec2 cellContentPos = cellPos + data.contentPadding;
1592 				ivec2 cellContentSize = cellSize - data.contentPadding*2;
1593 
1594 				drawCell(column, firstVisibleLine+line, irect(cellContentPos, cellContentSize));
1595 				lineY += lineHeight;
1596 			}
1597 
1598 			event.renderQueue.popClipRect();
1599 			colX += colW;
1600 		}
1601 		event.renderQueue.popClipRect();
1602 
1603 		event.depth += 3;
1604 	}
1605 
1606 	void updateHoveredLine(WidgetProxy widget, ivec2 pointerPos)
1607 	{
1608 		auto transform = widget.getOrCreate!WidgetTransform;
1609 		auto data = widget.get!ListData;
1610 		int localPointerY = pointerPos.y - transform.absPos.y;
1611 		int viewY = localPointerY - data.headerHeight;
1612 		double canvasY = viewY + data.viewOffset.y;
1613 		data.hoveredLine = cast(int)floor(canvasY / data.lineHeight);
1614 		if (data.hoveredLine < 0 || data.hoveredLine >= data.model.numLines)
1615 			data.hoveredLine = -1;
1616 	}
1617 
1618 	void onScroll(WidgetProxy widget, ref ScrollEvent event)
1619 	{
1620 		auto data = widget.get!ListData;
1621 		data.viewOffset += ivec2(event.delta * data.scrollSpeedLines * data.lineHeight);
1622 	}
1623 
1624 	void pointerMoved(WidgetProxy widget, ref PointerMoveEvent event)
1625 	{
1626 		updateHoveredLine(widget, event.newPointerPos);
1627 		event.handled = true;
1628 	}
1629 
1630 	void pointerPressed(WidgetProxy widget, ref PointerPressEvent event)
1631 	{
1632 		event.handled = true;
1633 	}
1634 
1635 	void pointerReleased(WidgetProxy widget, ref PointerReleaseEvent event)
1636 	{
1637 		event.handled = true;
1638 	}
1639 
1640 	void enterWidget(WidgetProxy widget, ref PointerEnterEvent event)
1641 	{
1642 		widget.get!ListData.hoveredLine = -1;
1643 	}
1644 
1645 	void leaveWidget(WidgetProxy widget, ref PointerLeaveEvent event)
1646 	{
1647 		widget.get!ListData.hoveredLine = -1;
1648 	}
1649 
1650 	void clickWidget(WidgetProxy widget, ref PointerClickEvent event)
1651 	{
1652 		auto data = widget.get!ListData;
1653 		if (data.hasHoveredLine)
1654 		{
1655 			auto line = data.hoveredLine;
1656 			if (data.model.numColumns < 1) return;
1657 
1658 			auto lineType = data.model.getLineType(line);
1659 			if (lineType == TreeLineType.leaf)
1660 			{
1661 				data.model.onLineClick(line);
1662 				return;
1663 			}
1664 
1665 			int firstColW = data.model.columnInfo(0).width;
1666 			auto transform = widget.getOrCreate!WidgetTransform;
1667 			auto leftBorder = transform.absPos.x + data.contentPadding.x;
1668 			auto indentW = data.font.metrics.advanceX;
1669 			auto buttonStart = leftBorder + indentW * data.model.getLineIndent(line);
1670 			auto buttonW = indentW*3;
1671 			auto buttonEnd = buttonStart + buttonW;
1672 			auto clickX = event.pointerPosition.x;
1673 
1674 			if (clickX >= buttonStart && clickX < buttonEnd)
1675 			{
1676 				data.model.toggleLineFolding(line);
1677 			}
1678 			else
1679 				data.model.onLineClick(line);
1680 		}
1681 		else
1682 		{
1683 			data.model.onLineClick(-1);
1684 		}
1685 	}
1686 }
1687 
1688 /// Creates a frame that shows a tree of all widgets.
1689 /// Highlights clicked widgets
1690 WidgetProxy createGuiDebugger(WidgetProxy root)
1691 {
1692 	WidgetId highlightedWidget;
1693 
1694 	// Tree widget
1695 	struct TreeNode
1696 	{
1697 		WidgetId wid;
1698 		TreeLineType nodeType;
1699 		int indent;
1700 		int numExpandedChildren;
1701 	}
1702 
1703 	class WidgetTreeModel : ListModel
1704 	{
1705 		import std.format : formattedWrite;
1706 		import voxelman.container.gapbuffer;
1707 		GapBuffer!TreeNode nodeList;
1708 		WidgetProxy widgetAt(size_t i) { return WidgetProxy(nodeList[i].wid, root.ctx); }
1709 		int selectedLine = -1;
1710 		ColumnInfo[2] columnInfos = [ColumnInfo("Type", 200), ColumnInfo("Id", 50, Alignment.max)];
1711 
1712 		void clear()
1713 		{
1714 			nodeList.clear();
1715 			selectedLine = -1;
1716 		}
1717 
1718 		override int numLines() { return cast(int)nodeList.length; }
1719 		override int numColumns() { return 2; }
1720 		override ref ColumnInfo columnInfo(int column) {
1721 			return columnInfos[column];
1722 		}
1723 		override void getColumnText(int column, scope void delegate(const(char)[]) sink) {
1724 			if (column == 0) sink("Widget type");
1725 			else if (column == 1) sink("Widget id");
1726 			else assert(false);
1727 		}
1728 		override void getCellText(int column, int line, scope void delegate(const(char)[]) sink) {
1729 			if (column == 0) sink(widgetAt(line).widgetType);
1730 			else formattedWrite(sink, "%s", nodeList[line].wid);
1731 		}
1732 		override bool isLineSelected(int line) { return line == selectedLine; }
1733 		override void onLineClick(int line) {
1734 			selectedLine = line;
1735 			if (selectedLine == -1)
1736 				highlightedWidget = 0;
1737 			else
1738 				highlightedWidget = nodeList[line].wid;
1739 		}
1740 		override TreeLineType getLineType(int line) {
1741 			return nodeList[line].nodeType;
1742 		}
1743 		override int getLineIndent(int line) { return nodeList[line].indent; }
1744 		override void toggleLineFolding(int line) {
1745 			if (nodeList[line].nodeType == TreeLineType.collapsedNode) expandWidget(line);
1746 			else collapseWidget(line);
1747 		}
1748 		void expandWidget(int line)
1749 		{
1750 			//writefln("expand %s %s", line, nodeList[line].wid);
1751 			auto container = root.ctx.get!WidgetContainer(nodeList[line].wid);
1752 			if (container is null || container.children.length == 0) {
1753 				nodeList[line].nodeType = TreeLineType.leaf;
1754 				return;
1755 			}
1756 			auto insertPos = line+1;
1757 			auto indent = nodeList[line].indent+1;
1758 			foreach(wid; container.children)
1759 			{
1760 				TreeLineType nodeType = numberOfChildren(root.ctx, wid) ? TreeLineType.collapsedNode : TreeLineType.leaf;
1761 				nodeList.putAt(insertPos++, TreeNode(wid, nodeType, indent));
1762 			}
1763 			nodeList[line].nodeType = TreeLineType.expandedNode;
1764 		}
1765 		void collapseWidget(int line)
1766 		{
1767 			//writefln("collapse %s", line, nodeList[line].wid);
1768 			if (line+1 == nodeList.length) {
1769 				nodeList[line].nodeType = TreeLineType.leaf;
1770 				return;
1771 			}
1772 
1773 			auto parentIndent = nodeList[line].indent;
1774 			size_t numItemsToRemove;
1775 			foreach(node; nodeList[line+1..$])
1776 			{
1777 				if (node.indent <= parentIndent) break;
1778 				++numItemsToRemove;
1779 			}
1780 			nodeList.remove(line+1, numItemsToRemove);
1781 			nodeList[line].nodeType = TreeLineType.collapsedNode;
1782 		}
1783 	}
1784 
1785 	void drawHighlight(WidgetProxy widget, ref DrawEvent event)
1786 	{
1787 		if (event.bubbling) return;
1788 
1789 		// highlight widget
1790 		auto t = widget.ctx.get!WidgetTransform(highlightedWidget);
1791 		if (t)
1792 		{
1793 			event.renderQueue.pushClipRect(irect(ivec2(0,0), event.renderQueue.renderer.framebufferSize));
1794 			event.renderQueue.drawRectLine(vec2(t.absPos), vec2(t.size), 10_000, Colors.red);
1795 			event.renderQueue.popClipRect();
1796 		}
1797 	}
1798 
1799 	auto model = new WidgetTreeModel;
1800 	auto tree_frame = Frame.create(root);
1801 	tree_frame.getOrCreate!WidgetEvents.addEventHandler(&drawHighlight);
1802 	tree_frame.minSize(250, 400).pos(10, 10).makeDraggable.moveToTop;
1803 	tree_frame.container.setVLayout(2, padding4(2));
1804 	tree_frame.header.setHLayout(2, padding4(4), Alignment.center);
1805 	tree_frame.header.createIcon("tree", ivec2(16, 16), Colors.black);
1806 	tree_frame.header.createText("Widget tree");
1807 	auto widget_tree = ColumnListLogic.create(tree_frame.container, model).minSize(250, 300).hvexpand;
1808 
1809 	void refillTree()
1810 	{
1811 		model.clear;
1812 		foreach(rootId; root.ctx.roots)
1813 		{
1814 			TreeLineType nodeType = numberOfChildren(root.ctx, rootId) ? TreeLineType.collapsedNode : TreeLineType.leaf;
1815 			model.nodeList.put(TreeNode(rootId, nodeType));
1816 		}
1817 	}
1818 	refillTree();
1819 	createTextButton(tree_frame.container, "Refresh", &refillTree).hexpand;
1820 
1821 	return tree_frame;
1822 }