1 /**
2  * Layout engine — box sizers.
3  *
4  * A `Sizer` arranges child widgets (and nested sizers) within a rectangle.
5  * `HBox` lays children out horizontally, `VBox` vertically. Each child carries
6  * a *proportion*: children with proportion 0 keep their preferred size along the
7  * main axis, while the leftover space is shared among the proportional children
8  * in proportion to their weights. Per-child `Padding` is reserved around the
9  * child's cell.
10  *
11  * The layout math is pure integer arithmetic with no dependency on a running
12  * message loop, so it is exercised directly by unit tests.
13  */
14 module deft.layout;
15 
16 version (Windows):
17 
18 import deft.widget : Padding, Rect, Size, Widget;
19 
20 /// Horizontal placement of a child within the space available to it.
21 enum HAlign
22 {
23 	/// Stretch to fill the width (the default).
24 	fill,
25 	/// Keep the preferred width, against the left edge.
26 	left,
27 	/// Keep the preferred width, centered.
28 	center,
29 	/// Keep the preferred width, against the right edge.
30 	right,
31 }
32 
33 /// Vertical placement of a child within the space available to it.
34 enum VAlign
35 {
36 	/// Stretch to fill the height (the default).
37 	fill,
38 	/// Keep the preferred height, against the top edge.
39 	top,
40 	/// Keep the preferred height, centered.
41 	middle,
42 	/// Keep the preferred height, against the bottom edge.
43 	bottom,
44 }
45 
46 /// The horizontal offset of a `child`-wide box within `available` for `a`.
47 private int hOffset(HAlign a, int available, int child) @safe pure nothrow @nogc
48 {
49 	final switch (a)
50 	{
51 	case HAlign.fill:
52 	case HAlign.left:
53 		return 0;
54 	case HAlign.center:
55 		return (available - child) / 2;
56 	case HAlign.right:
57 		return available - child;
58 	}
59 }
60 
61 /// The vertical offset of a `child`-tall box within `available` for `a`.
62 private int vOffset(VAlign a, int available, int child) @safe pure nothrow @nogc
63 {
64 	final switch (a)
65 	{
66 	case VAlign.fill:
67 	case VAlign.top:
68 		return 0;
69 	case VAlign.middle:
70 		return (available - child) / 2;
71 	case VAlign.bottom:
72 		return available - child;
73 	}
74 }
75 
76 /**
77  * One placed child of a sizer: a widget or a nested sizer, with its proportion,
78  * padding and in-cell alignment.
79  *
80  * Returned by `Sizer.add`/`addSizer` for fluent configuration — chain
81  * `proportion`, `pad`, `alignH`/`alignV` (each returns the same item):
82  *
83  * ---
84  * hbox.add(button).proportion(0).alignV(VAlign.middle).pad(Padding.all(4));
85  * vbox.add(label).alignH(HAlign.right);
86  * ---
87  *
88  * By default a child fills its cell on both axes. Setting `alignH`/`alignV` to a
89  * non-`fill` value keeps the child's preferred extent on that axis and pins it.
90  */
91 final class SizerItem
92 {
93 	private Widget widget_;
94 	private Sizer sizer_;
95 	private int proportion_;
96 	private Padding padding_;
97 	private Size fixedSize_;
98 	private HAlign halign_ = HAlign.fill;
99 	private VAlign valign_ = VAlign.fill;
100 
101 	private this(Widget widget, Sizer sizer, int proportion, Padding padding)
102 	{
103 		widget_ = widget;
104 		sizer_ = sizer;
105 		proportion_ = proportion;
106 		padding_ = padding;
107 	}
108 
109 	/// Set the main-axis weight (0 = keep the preferred size, non-stretching).
110 	SizerItem proportion(int weight) return
111 	{
112 		proportion_ = weight;
113 		return this;
114 	}
115 
116 	/**
117 	 * Keep the child's preferred size on the main axis — it neither grows nor
118 	 * shrinks as the container resizes. A readable alias for `proportion(0)`
119 	 * (the default), so `add(button).fixed()` states the intent at the call site.
120 	 */
121 	SizerItem fixed() return
122 	{
123 		proportion_ = 0;
124 		return this;
125 	}
126 
127 	/**
128 	 * Make the child grow to share the container's leftover space, with the given
129 	 * `weight` (default 1). A readable alias for `proportion(weight)`: two
130 	 * `stretch()` children split the slack evenly, `stretch(2)` versus `stretch(1)`
131 	 * splits it 2:1. So `add(list).stretch()` reads as "the list takes the slack."
132 	 */
133 	SizerItem stretch(int weight = 1) return
134 	{
135 		proportion_ = weight < 1 ? 1 : weight;
136 		return this;
137 	}
138 
139 	/// Reserve padding around the child within its cell.
140 	SizerItem pad(Padding padding) return
141 	{
142 		padding_ = padding;
143 		return this;
144 	}
145 
146 	/// Set the horizontal placement within the cell.
147 	SizerItem alignH(HAlign horizontal) return
148 	{
149 		halign_ = horizontal;
150 		return this;
151 	}
152 
153 	/// Set the vertical placement within the cell.
154 	SizerItem alignV(VAlign vertical) return
155 	{
156 		valign_ = vertical;
157 		return this;
158 	}
159 
160 	/**
161 	 * Override the child's content size with a fixed size that wins over its
162 	 * preferred size. This is an exact override, not a lower bound — the name says
163 	 * "fixed," not "minimum." Prefer `autoSize`/`stretch` and reach for this only
164 	 * for things with an inherently fixed extent (an icon, a fixed-width sidebar).
165 	 */
166 	SizerItem fixedSize(Size size) return
167 	{
168 		fixedSize_ = size;
169 		return this;
170 	}
171 
172 	/// The child's content size, before padding.
173 	private Size contentSize()
174 	{
175 		if (fixedSize_ != Size.init)
176 			return fixedSize_;
177 		if (widget_ !is null)
178 			return widget_.getPreferredSize();
179 		if (sizer_ !is null)
180 			return sizer_.preferredSize();
181 		return Size.init;
182 	}
183 
184 	/// The child's content size plus its padding.
185 	Size outerSize()
186 	{
187 		auto c = contentSize();
188 		return Size(
189 			c.width + padding_.left + padding_.right,
190 			c.height + padding_.top + padding_.bottom);
191 	}
192 
193 	/// Place the child into `cell`, insetting by padding and applying alignment.
194 	void place(Rect cell)
195 	{
196 		int innerX = cell.x + padding_.left;
197 		int innerY = cell.y + padding_.top;
198 		int innerW = cell.width - padding_.left - padding_.right;
199 		int innerH = cell.height - padding_.top - padding_.bottom;
200 		if (innerW < 0)
201 			innerW = 0;
202 		if (innerH < 0)
203 			innerH = 0;
204 
205 		int x = innerX, y = innerY, w = innerW, h = innerH;
206 
207 		if (halign_ != HAlign.fill || valign_ != VAlign.fill)
208 		{
209 			auto content = contentSize();
210 			if (halign_ != HAlign.fill)
211 			{
212 				w = content.width < innerW ? content.width : innerW;
213 				x = innerX + hOffset(halign_, innerW, w);
214 			}
215 			if (valign_ != VAlign.fill)
216 			{
217 				h = content.height < innerH ? content.height : innerH;
218 				y = innerY + vOffset(valign_, innerH, h);
219 			}
220 		}
221 
222 		if (widget_ !is null)
223 			widget_.setBounds(Rect(x, y, w, h));
224 		else if (sizer_ !is null)
225 			sizer_.layout(Rect(x, y, w, h));
226 	}
227 }
228 
229 /// Abstract base for box sizers.
230 abstract class Sizer
231 {
232 	protected SizerItem[] items;
233 
234 	/// Arrange the children within `availableArea`.
235 	abstract void layout(Rect availableArea);
236 
237 	/// The natural size this sizer would like, given its children.
238 	abstract Size preferredSize();
239 
240 	/**
241 	 * Add a widget child and return its `SizerItem` for fluent configuration:
242 	 * `box.add(w).proportion(1).pad(Padding.all(8)).alignV(VAlign.middle)`. A bare
243 	 * `add(w)` gives a non-stretching child (proportion 0) with no padding.
244 	 */
245 	SizerItem add(Widget widget)
246 	{
247 		auto item = new SizerItem(widget, null, 0, Padding.init);
248 		items ~= item;
249 		return item;
250 	}
251 
252 	/// Add a nested sizer and return its `SizerItem` for fluent configuration.
253 	SizerItem addSizer(Sizer sizer)
254 	{
255 		auto item = new SizerItem(null, sizer, 0, Padding.init);
256 		items ~= item;
257 		return item;
258 	}
259 
260 	/// Number of child items.
261 	size_t length() const @safe pure nothrow @nogc
262 	{
263 		return items.length;
264 	}
265 }
266 
267 /// Lays children out left to right.
268 class HBox : Sizer
269 {
270 	override void layout(Rect area)
271 	{
272 		if (items.length == 0)
273 			return;
274 
275 		int totalProp = 0;
276 		int fixedMain = 0;
277 		ptrdiff_t lastProp = -1;
278 		foreach (i, ref it; items)
279 		{
280 			if (it.proportion_ > 0)
281 			{
282 				totalProp += it.proportion_;
283 				fixedMain += it.padding_.left + it.padding_.right;
284 				lastProp = i;
285 			}
286 			else
287 			{
288 				fixedMain += it.outerSize().width;
289 			}
290 		}
291 
292 		int flexible = area.width - fixedMain;
293 		if (flexible < 0)
294 			flexible = 0;
295 
296 		int x = area.x;
297 		int allocated = 0;
298 		foreach (i, ref it; items)
299 		{
300 			int outerW;
301 			if (it.proportion_ == 0)
302 			{
303 				outerW = it.outerSize().width;
304 			}
305 			else
306 			{
307 				int content;
308 				if (i == lastProp)
309 					content = flexible - allocated;
310 				else
311 				{
312 					content = flexible * it.proportion_ / totalProp;
313 					allocated += content;
314 				}
315 				outerW = content + it.padding_.left + it.padding_.right;
316 			}
317 
318 			it.place(Rect(x, area.y, outerW, area.height));
319 			x += outerW;
320 		}
321 	}
322 
323 	override Size preferredSize()
324 	{
325 		Size total;
326 		foreach (ref it; items)
327 		{
328 			auto s = it.outerSize();
329 			total.width += s.width;
330 			if (s.height > total.height)
331 				total.height = s.height;
332 		}
333 		return total;
334 	}
335 }
336 
337 /// Lays children out top to bottom.
338 class VBox : Sizer
339 {
340 	override void layout(Rect area)
341 	{
342 		if (items.length == 0)
343 			return;
344 
345 		int totalProp = 0;
346 		int fixedMain = 0;
347 		ptrdiff_t lastProp = -1;
348 		foreach (i, ref it; items)
349 		{
350 			if (it.proportion_ > 0)
351 			{
352 				totalProp += it.proportion_;
353 				fixedMain += it.padding_.top + it.padding_.bottom;
354 				lastProp = i;
355 			}
356 			else
357 			{
358 				fixedMain += it.outerSize().height;
359 			}
360 		}
361 
362 		int flexible = area.height - fixedMain;
363 		if (flexible < 0)
364 			flexible = 0;
365 
366 		int y = area.y;
367 		int allocated = 0;
368 		foreach (i, ref it; items)
369 		{
370 			int outerH;
371 			if (it.proportion_ == 0)
372 			{
373 				outerH = it.outerSize().height;
374 			}
375 			else
376 			{
377 				int content;
378 				if (i == lastProp)
379 					content = flexible - allocated;
380 				else
381 				{
382 					content = flexible * it.proportion_ / totalProp;
383 					allocated += content;
384 				}
385 				outerH = content + it.padding_.top + it.padding_.bottom;
386 			}
387 
388 			it.place(Rect(area.x, y, area.width, outerH));
389 			y += outerH;
390 		}
391 	}
392 
393 	override Size preferredSize()
394 	{
395 		Size total;
396 		foreach (ref it; items)
397 		{
398 			auto s = it.outerSize();
399 			total.height += s.height;
400 			if (s.width > total.width)
401 				total.width = s.width;
402 		}
403 		return total;
404 	}
405 }
406 
407 /// How a grid track (one column or one row) is sized.
408 enum GridTrackKind
409 {
410 	/// Size to the largest preferred size of the cells in the track.
411 	autoSize,
412 	/// A fixed size in device pixels.
413 	absolute,
414 	/// A weighted share of the space left after auto and absolute tracks.
415 	percent,
416 }
417 
418 /**
419  * The size rule for one column or row of a `Grid`.
420  *
421  * Build with the factory helpers: `GridTrack.autoSize` (fit the content),
422  * `GridTrack.pixels(n)` (a fixed width/height), or `GridTrack.percent(w)` (a
423  * weighted share of the leftover space — the weights of all percent tracks are
424  * summed, so two `percent(50)` tracks split the remainder evenly, exactly like
425  * two `percent(1)` tracks would).
426  */
427 struct GridTrack
428 {
429 	/// The sizing rule.
430 	GridTrackKind kind;
431 
432 	/// Pixels for `absolute`, weight for `percent`, ignored for `autoSize`.
433 	int value;
434 
435 	/// A track sized to its content.
436 	static GridTrack autoSize() @safe pure nothrow @nogc
437 	{
438 		return GridTrack(GridTrackKind.autoSize, 0);
439 	}
440 
441 	/// A track of fixed pixel size.
442 	static GridTrack pixels(int px) @safe pure nothrow @nogc
443 	{
444 		return GridTrack(GridTrackKind.absolute, px);
445 	}
446 
447 	/// A track taking a `weight`-proportioned share of the leftover space.
448 	static GridTrack percent(int weight) @safe pure nothrow @nogc
449 	{
450 		return GridTrack(GridTrackKind.percent, weight);
451 	}
452 }
453 
454 /**
455  * A placed grid child, returned by `Grid.add`/`Grid.addSizer` for fluent
456  * configuration:
457  *
458  * ---
459  * grid.add(banner, 0, 0).span(2, 1);
460  * grid.add(label, 0, 1).aligned(HAlign.right, VAlign.middle).pad(Padding.all(4));
461  * ---
462  *
463  * Every modifier returns the same `GridItem`, so calls chain. The grid reads the
464  * item's final state at layout time, so configuration may continue after `add`.
465  * Alignment uses the same `HAlign`/`VAlign` as the box sizers.
466  */
467 final class GridItem
468 {
469 	private SizerItem item;
470 	private int column;
471 	private int row;
472 	private int columnSpan = 1;
473 	private int rowSpan = 1;
474 
475 	/// Make the child cover `columns` columns and `rows` rows from its cell.
476 	GridItem span(int columns, int rows = 1) return
477 	{
478 		columnSpan = columns < 1 ? 1 : columns;
479 		rowSpan = rows < 1 ? 1 : rows;
480 		return this;
481 	}
482 
483 	/// Set horizontal and vertical alignment within the cell.
484 	GridItem aligned(HAlign horizontal, VAlign vertical) return
485 	{
486 		item.alignH(horizontal);
487 		item.alignV(vertical);
488 		return this;
489 	}
490 
491 	/// Set the horizontal alignment within the cell.
492 	GridItem alignH(HAlign horizontal) return
493 	{
494 		item.alignH(horizontal);
495 		return this;
496 	}
497 
498 	/// Set the vertical alignment within the cell.
499 	GridItem alignV(VAlign vertical) return
500 	{
501 		item.alignV(vertical);
502 		return this;
503 	}
504 
505 	/// Reserve padding around the child inside its cell.
506 	GridItem pad(Padding padding) return
507 	{
508 		item.pad(padding);
509 		return this;
510 	}
511 }
512 
513 /**
514  * A table layout: a fixed grid of columns and rows, each independently sized to
515  * its content (`autoSize`), a fixed pixel size (`absolute`) or a weighted share
516  * of the leftover space (`percent`). Widgets and nested sizers are placed into
517  * cells by column/row and may span several columns or rows. Within its cell a
518  * child fills the available space by default, or keeps its preferred size and
519  * aligns (start/center/end) — see `GridItem`.
520  *
521  * This is Deft's analog of WinForms' `TableLayoutPanel`: pick the column and row
522  * counts, mark each track auto or percent (or pixels), and drop children into
523  * cells without computing any coordinates. `add` returns a `GridItem` whose
524  * fluent `span`/`aligned`/`pad` methods read better than positional arguments:
525  *
526  * ---
527  * auto grid = new Grid(2, 2);
528  * grid.setColumn(0, GridTrack.autoSize);
529  * grid.setColumn(1, GridTrack.percent(100));
530  * grid.add(label, 0, 0).aligned(HAlign.right, VAlign.middle);
531  * grid.add(field, 1, 0);                       // fills its cell
532  * grid.add(footer, 0, 1).span(2, 1);           // spans both columns
533  * ---
534  *
535  * Tracks default to `autoSize`. Auto track sizes are measured from the cells
536  * that do not span (a spanning child is placed across the already-computed
537  * tracks but does not enlarge them).
538  */
539 class Grid : Sizer
540 {
541 	private GridItem[] cells_;
542 	private GridTrack[] columns_;
543 	private GridTrack[] rows_;
544 	private int hgap_;
545 	private int vgap_;
546 
547 	/// Create a grid with `columns` columns and `rows` rows, all `autoSize`.
548 	this(int columns, int rows)
549 	{
550 		if (columns < 0)
551 			columns = 0;
552 		if (rows < 0)
553 			rows = 0;
554 		columns_ = new GridTrack[columns];
555 		rows_ = new GridTrack[rows];
556 		foreach (ref c; columns_)
557 			c = GridTrack.autoSize;
558 		foreach (ref r; rows_)
559 			r = GridTrack.autoSize;
560 	}
561 
562 	/// Number of columns.
563 	int columnCount() const @safe pure nothrow @nogc
564 	{
565 		return cast(int) columns_.length;
566 	}
567 
568 	/// Number of rows.
569 	int rowCount() const @safe pure nothrow @nogc
570 	{
571 		return cast(int) rows_.length;
572 	}
573 
574 	/// Set the sizing rule for column `index`.
575 	void setColumn(int index, GridTrack track)
576 	{
577 		if (index >= 0 && index < columns_.length)
578 			columns_[index] = track;
579 	}
580 
581 	/// Set the sizing rule for row `index`.
582 	void setRow(int index, GridTrack track)
583 	{
584 		if (index >= 0 && index < rows_.length)
585 			rows_[index] = track;
586 	}
587 
588 	/// Set the pixel gap between columns (`horizontal`) and rows (`vertical`).
589 	void setSpacing(int horizontal, int vertical)
590 	{
591 		hgap_ = horizontal < 0 ? 0 : horizontal;
592 		vgap_ = vertical < 0 ? 0 : vertical;
593 	}
594 
595 	/**
596 	 * Place `widget` in the cell at `column`/`row`. Returns a `GridItem` whose
597 	 * fluent `span`/`aligned`/`alignH`/`alignV`/`pad` methods configure it.
598 	 */
599 	GridItem add(Widget widget, int column, int row)
600 	{
601 		auto cell = new GridItem;
602 		cell.item = new SizerItem(widget, null, 0, Padding.init);
603 		cell.column = column;
604 		cell.row = row;
605 		cells_ ~= cell;
606 		return cell;
607 	}
608 
609 	/// Place a nested `sizer` in the cell at `column`/`row`; see `add`.
610 	GridItem addSizer(Sizer sizer, int column, int row)
611 	{
612 		auto cell = new GridItem;
613 		cell.item = new SizerItem(null, sizer, 0, Padding.init);
614 		cell.column = column;
615 		cell.row = row;
616 		cells_ ~= cell;
617 		return cell;
618 	}
619 
620 	/// Number of placed children.
621 	override size_t length() const @safe pure nothrow @nogc
622 	{
623 		return cells_.length;
624 	}
625 
626 	override void layout(Rect area)
627 	{
628 		if (columns_.length == 0 || rows_.length == 0)
629 			return;
630 
631 		auto colSizes = resolveTracks(columns_, area.width, hgap_, true);
632 		auto rowSizes = resolveTracks(rows_, area.height, vgap_, false);
633 
634 		auto colOffsets = trackOffsets(colSizes, hgap_, area.x);
635 		auto rowOffsets = trackOffsets(rowSizes, vgap_, area.y);
636 
637 		foreach (c; cells_)
638 		{
639 			if (c.column < 0 || c.row < 0
640 				|| c.column >= columns_.length || c.row >= rows_.length)
641 				continue;
642 
643 			immutable int lastCol = spanEnd(c.column, c.columnSpan, cast(int) columns_.length);
644 			immutable int lastRow = spanEnd(c.row, c.rowSpan, cast(int) rows_.length);
645 
646 			immutable int x = colOffsets[c.column];
647 			immutable int y = rowOffsets[c.row];
648 			immutable int w = colOffsets[lastCol] + colSizes[lastCol] - x;
649 			immutable int h = rowOffsets[lastRow] + rowSizes[lastRow] - y;
650 
651 			// SizerItem.place applies the item's padding and alignment.
652 			c.item.place(Rect(x, y, w, h));
653 		}
654 	}
655 
656 	override Size preferredSize()
657 	{
658 		Size total;
659 		foreach (i; 0 .. columns_.length)
660 			total.width += preferredTrackSize(columns_[i], cast(int) i, true);
661 		foreach (i; 0 .. rows_.length)
662 			total.height += preferredTrackSize(rows_[i], cast(int) i, false);
663 		if (columns_.length > 1)
664 			total.width += hgap_ * (cast(int) columns_.length - 1);
665 		if (rows_.length > 1)
666 			total.height += vgap_ * (cast(int) rows_.length - 1);
667 		return total;
668 	}
669 
670 	/// The largest preferred extent of the non-spanning cells in a track.
671 	private int autoTrackSize(int index, bool horizontal)
672 	{
673 		int best;
674 		foreach (c; cells_)
675 		{
676 			immutable int span = horizontal ? c.columnSpan : c.rowSpan;
677 			immutable int at = horizontal ? c.column : c.row;
678 			if (span != 1 || at != index)
679 				continue;
680 			auto s = c.item.outerSize();
681 			immutable int extent = horizontal ? s.width : s.height;
682 			if (extent > best)
683 				best = extent;
684 		}
685 		return best;
686 	}
687 
688 	/// The size a track contributes to `preferredSize` (percent uses its content).
689 	private int preferredTrackSize(GridTrack track, int index, bool horizontal)
690 	{
691 		final switch (track.kind)
692 		{
693 		case GridTrackKind.absolute:
694 			return track.value;
695 		case GridTrackKind.autoSize:
696 		case GridTrackKind.percent:
697 			return autoTrackSize(index, horizontal);
698 		}
699 	}
700 
701 	/// Resolve every track to a concrete pixel size within `available`.
702 	private int[] resolveTracks(GridTrack[] tracks, int available, int gap,
703 		bool horizontal)
704 	{
705 		immutable int n = cast(int) tracks.length;
706 		auto sizes = new int[n];
707 
708 		int used = n > 1 ? gap * (n - 1) : 0;
709 		int totalPercent;
710 		int lastPercent = -1;
711 		foreach (i, t; tracks)
712 		{
713 			final switch (t.kind)
714 			{
715 			case GridTrackKind.absolute:
716 				sizes[i] = t.value;
717 				used += sizes[i];
718 				break;
719 			case GridTrackKind.autoSize:
720 				sizes[i] = autoTrackSize(cast(int) i, horizontal);
721 				used += sizes[i];
722 				break;
723 			case GridTrackKind.percent:
724 				totalPercent += t.value;
725 				lastPercent = cast(int) i;
726 				break;
727 			}
728 		}
729 
730 		int remaining = available - used;
731 		if (remaining < 0)
732 			remaining = 0;
733 
734 		int allocated;
735 		foreach (i, t; tracks)
736 		{
737 			if (t.kind != GridTrackKind.percent)
738 				continue;
739 			int size;
740 			if (cast(int) i == lastPercent)
741 				size = remaining - allocated; // last gets the rounding remainder
742 			else
743 			{
744 				size = totalPercent > 0 ? remaining * t.value / totalPercent : 0;
745 				allocated += size;
746 			}
747 			sizes[i] = size;
748 		}
749 		return sizes;
750 	}
751 
752 	/// Prefix offsets (in screen/parent coordinates) for a resolved track list.
753 	private static int[] trackOffsets(int[] sizes, int gap, int origin)
754 	{
755 		auto offsets = new int[sizes.length];
756 		int pos = origin;
757 		foreach (i, s; sizes)
758 		{
759 			offsets[i] = pos;
760 			pos += s + gap;
761 		}
762 		return offsets;
763 	}
764 
765 	/// The index of the last track a span covers, clamped to the track count.
766 	private static int spanEnd(int start, int span, int count)
767 	{
768 		int last = start + span - 1;
769 		if (last >= count)
770 			last = count - 1;
771 		if (last < start)
772 			last = start;
773 		return last;
774 	}
775 }
776 
777 version (unittest)
778 {
779 	/// A headless widget for layout tests: no native handle, records its bounds.
780 	private final class FakeWidget : Widget
781 	{
782 		private Size pref;
783 
784 		this(Size preferred)
785 		{
786 			pref = preferred;
787 		}
788 
789 		override Size getPreferredSize()
790 		{
791 			return pref;
792 		}
793 	}
794 }
795 
796 unittest
797 {
798 	// A single proportional child fills the available area.
799 	auto w = new FakeWidget(Size(10, 10));
800 	auto box = new VBox();
801 	box.add(w).proportion(1);
802 	box.layout(Rect(0, 0, 100, 50));
803 	assert(w.bounds == Rect(0, 0, 100, 50));
804 }
805 
806 unittest
807 {
808 	// Two equal-proportion children split the width evenly.
809 	auto a = new FakeWidget(Size(0, 0));
810 	auto b = new FakeWidget(Size(0, 0));
811 	auto box = new HBox();
812 	box.add(a).proportion(1);
813 	box.add(b).proportion(1);
814 	box.layout(Rect(0, 0, 100, 20));
815 	assert(a.bounds == Rect(0, 0, 50, 20));
816 	assert(b.bounds == Rect(50, 0, 50, 20));
817 }
818 
819 unittest
820 {
821 	// A 2:1 proportion ratio splits 90px into 60 and 30.
822 	auto a = new FakeWidget(Size(0, 0));
823 	auto b = new FakeWidget(Size(0, 0));
824 	auto box = new HBox();
825 	box.add(a).proportion(2);
826 	box.add(b).proportion(1);
827 	box.layout(Rect(0, 0, 90, 10));
828 	assert(a.bounds == Rect(0, 0, 60, 10));
829 	assert(b.bounds == Rect(60, 0, 30, 10));
830 }
831 
832 unittest
833 {
834 	// A fixed (proportion 0) child keeps its preferred size; the proportional
835 	// child takes the rest.
836 	auto fixed = new FakeWidget(Size(30, 10));
837 	auto flex = new FakeWidget(Size(0, 0));
838 	auto box = new HBox();
839 	box.add(fixed);
840 	box.add(flex).proportion(1);
841 	box.layout(Rect(0, 0, 100, 10));
842 	assert(fixed.bounds == Rect(0, 0, 30, 10));
843 	assert(flex.bounds == Rect(30, 0, 70, 10));
844 }
845 
846 unittest
847 {
848 	// Nested sizers: a VBox inside an HBox lays out within its allotted column.
849 	auto left = new FakeWidget(Size(0, 0));
850 	auto top = new FakeWidget(Size(0, 0));
851 	auto bottom = new FakeWidget(Size(0, 0));
852 
853 	auto inner = new VBox();
854 	inner.add(top).proportion(1);
855 	inner.add(bottom).proportion(1);
856 
857 	auto outer = new HBox();
858 	outer.add(left).proportion(1);
859 	outer.addSizer(inner).proportion(1);
860 
861 	outer.layout(Rect(0, 0, 100, 40));
862 
863 	// Left column: x 0..50.
864 	assert(left.bounds == Rect(0, 0, 50, 40));
865 	// Right column (the VBox) occupies x 50..100 and stacks its children.
866 	assert(top.bounds == Rect(50, 0, 50, 20));
867 	assert(bottom.bounds == Rect(50, 20, 50, 20));
868 }
869 
870 unittest
871 {
872 	// Padding is reserved around the child.
873 	auto w = new FakeWidget(Size(0, 0));
874 	auto box = new VBox();
875 	box.add(w).proportion(1).pad(Padding.all(5));
876 	box.layout(Rect(0, 0, 100, 100));
877 	assert(w.bounds == Rect(5, 5, 90, 90));
878 }
879 
880 unittest
881 {
882 	// Zero available space must not crash and yields empty bounds.
883 	auto w = new FakeWidget(Size(0, 0));
884 	auto box = new HBox();
885 	box.add(w).proportion(1);
886 	box.layout(Rect(0, 0, 0, 0));
887 	assert(w.bounds == Rect(0, 0, 0, 0));
888 }
889 
890 unittest
891 {
892 	// An empty sizer has a zero preferred size and lays out without error.
893 	auto box = new VBox();
894 	assert(box.preferredSize() == Size(0, 0));
895 	box.layout(Rect(0, 0, 100, 100));
896 
897 	// preferredSize sums the main axis and maxes the cross axis.
898 	auto h = new HBox();
899 	h.add(new FakeWidget(Size(20, 8)));
900 	h.add(new FakeWidget(Size(30, 12)));
901 	assert(h.preferredSize() == Size(50, 12));
902 }
903 
904 unittest
905 {
906 	// Grid: two 50% columns and one 100% row split the width evenly and fill it.
907 	auto a = new FakeWidget(Size(10, 10));
908 	auto b = new FakeWidget(Size(10, 10));
909 	auto grid = new Grid(2, 1);
910 	grid.setColumn(0, GridTrack.percent(50));
911 	grid.setColumn(1, GridTrack.percent(50));
912 	grid.setRow(0, GridTrack.percent(100));
913 	grid.add(a, 0, 0);
914 	grid.add(b, 1, 0);
915 	grid.layout(Rect(0, 0, 100, 40));
916 	assert(a.bounds == Rect(0, 0, 50, 40));
917 	assert(b.bounds == Rect(50, 0, 50, 40));
918 }
919 
920 unittest
921 {
922 	// Grid: an auto column keeps its content width; a percent column takes the
923 	// rest. Two auto rows stack at their content heights.
924 	auto label = new FakeWidget(Size(30, 12));
925 	auto field = new FakeWidget(Size(0, 12));
926 	auto grid = new Grid(2, 1);
927 	grid.setColumn(0, GridTrack.autoSize);
928 	grid.setColumn(1, GridTrack.percent(100));
929 	grid.setRow(0, GridTrack.percent(100));
930 	grid.add(label, 0, 0);
931 	grid.add(field, 1, 0);
932 	grid.layout(Rect(0, 0, 200, 20));
933 	assert(label.bounds == Rect(0, 0, 30, 20));
934 	assert(field.bounds == Rect(30, 0, 170, 20));
935 }
936 
937 unittest
938 {
939 	// Grid: absolute pixel column, padding inside a cell, and spacing between
940 	// columns are all honored.
941 	auto a = new FakeWidget(Size(0, 0));
942 	auto b = new FakeWidget(Size(0, 0));
943 	auto grid = new Grid(2, 1);
944 	grid.setColumn(0, GridTrack.pixels(40));
945 	grid.setColumn(1, GridTrack.percent(100));
946 	grid.setRow(0, GridTrack.percent(100));
947 	grid.setSpacing(10, 0);
948 	grid.add(a, 0, 0);
949 	grid.add(b, 1, 0).pad(Padding.all(5));
950 	grid.layout(Rect(0, 0, 100, 30));
951 	// Column 0 = 40px, 10px gap, column 1 = remaining 50px starting at x=50.
952 	assert(a.bounds == Rect(0, 0, 40, 30));
953 	assert(b.bounds == Rect(55, 5, 40, 20));
954 }
955 
956 unittest
957 {
958 	// Grid: a child spanning two columns covers both tracks plus the gap between.
959 	auto wide = new FakeWidget(Size(0, 0));
960 	auto grid = new Grid(2, 2);
961 	grid.setColumn(0, GridTrack.percent(50));
962 	grid.setColumn(1, GridTrack.percent(50));
963 	grid.setRow(0, GridTrack.percent(50));
964 	grid.setRow(1, GridTrack.percent(50));
965 	grid.setSpacing(10, 10);
966 	grid.add(wide, 0, 0).span(2, 1);
967 	grid.layout(Rect(0, 0, 210, 110));
968 	// Each column = (210-10)/2 = 100; the span covers 100 + 10 gap + 100 = 210.
969 	// Each row = (110-10)/2 = 50.
970 	assert(wide.bounds == Rect(0, 0, 210, 50));
971 }
972 
973 unittest
974 {
975 	// Grid: preferredSize sums absolute/auto track sizes plus spacing; a degenerate
976 	// grid lays out without crashing.
977 	auto grid = new Grid(2, 1);
978 	grid.setColumn(0, GridTrack.pixels(40));
979 	grid.setColumn(1, GridTrack.autoSize);
980 	grid.setRow(0, GridTrack.autoSize);
981 	grid.setSpacing(8, 0);
982 	grid.add(new FakeWidget(Size(0, 0)), 0, 0);
983 	grid.add(new FakeWidget(Size(25, 16)), 1, 0);
984 	assert(grid.preferredSize() == Size(40 + 25 + 8, 16));
985 
986 	auto empty = new Grid(0, 0);
987 	empty.layout(Rect(0, 0, 100, 100)); // must not crash
988 	assert(empty.length == 0);
989 }
990 
991 unittest
992 {
993 	// Grid alignment: a fixed-size child centered in a larger cell keeps its size
994 	// and sits in the middle; fill (the default) stretches.
995 	auto centered = new FakeWidget(Size(20, 10));
996 	auto grid = new Grid(1, 1);
997 	grid.setColumn(0, GridTrack.percent(100));
998 	grid.setRow(0, GridTrack.percent(100));
999 	grid.add(centered, 0, 0).aligned(HAlign.center, VAlign.middle);
1000 	grid.layout(Rect(0, 0, 100, 50));
1001 	assert(centered.bounds == Rect(40, 20, 20, 10)); // (100-20)/2, (50-10)/2
1002 
1003 	// right/top alignment pins to the right/top edges.
1004 	auto pinned = new FakeWidget(Size(20, 10));
1005 	auto g2 = new Grid(1, 1);
1006 	g2.setColumn(0, GridTrack.percent(100));
1007 	g2.setRow(0, GridTrack.percent(100));
1008 	g2.add(pinned, 0, 0).alignH(HAlign.right).alignV(VAlign.top);
1009 	g2.layout(Rect(0, 0, 100, 50));
1010 	assert(pinned.bounds == Rect(80, 0, 20, 10));
1011 }
1012 
1013 unittest
1014 {
1015 	// Fluent box placement: proportion set via the returned handle matches the
1016 	// positional form (2:1 split of 90px into 60 and 30).
1017 	auto a = new FakeWidget(Size(0, 0));
1018 	auto b = new FakeWidget(Size(0, 0));
1019 	auto box = new HBox();
1020 	box.add(a).proportion(2);
1021 	box.add(b).proportion(1);
1022 	box.layout(Rect(0, 0, 90, 10));
1023 	assert(a.bounds == Rect(0, 0, 60, 10));
1024 	assert(b.bounds == Rect(60, 0, 30, 10));
1025 }
1026 
1027 unittest
1028 {
1029 	// fixed()/stretch() aliases match the equivalent proportion() forms: a fixed
1030 	// child keeps its preferred width and the stretch child takes the rest.
1031 	auto fixedW = new FakeWidget(Size(30, 10));
1032 	auto flex = new FakeWidget(Size(0, 0));
1033 	auto box = new HBox();
1034 	box.add(fixedW).fixed();
1035 	box.add(flex).stretch();
1036 	box.layout(Rect(0, 0, 100, 10));
1037 	assert(fixedW.bounds == Rect(0, 0, 30, 10));
1038 	assert(flex.bounds == Rect(30, 0, 70, 10));
1039 
1040 	// Weighted stretch: stretch(2) versus stretch(1) splits 90px into 60 and 30,
1041 	// and stretch(0) is clamped up to weight 1 (it must still grow).
1042 	auto a = new FakeWidget(Size(0, 0));
1043 	auto b = new FakeWidget(Size(0, 0));
1044 	auto two = new HBox();
1045 	two.add(a).stretch(2);
1046 	two.add(b).stretch(0); // clamped to 1
1047 	two.layout(Rect(0, 0, 90, 10));
1048 	assert(a.bounds == Rect(0, 0, 60, 10));
1049 	assert(b.bounds == Rect(60, 0, 30, 10));
1050 }
1051 
1052 unittest
1053 {
1054 	// HBox cross-axis (vertical) alignment: a fixed-height child fills the width
1055 	// (proportion 1) but is vertically centered at its preferred height.
1056 	auto w = new FakeWidget(Size(20, 10));
1057 	auto box = new HBox();
1058 	box.add(w).proportion(1).alignV(VAlign.middle);
1059 	box.layout(Rect(0, 0, 100, 50));
1060 	assert(w.bounds == Rect(0, 20, 100, 10)); // y = (50-10)/2
1061 
1062 	// VBox cross-axis (horizontal) alignment: right-pinned at preferred width.
1063 	auto w2 = new FakeWidget(Size(20, 10));
1064 	auto vb = new VBox();
1065 	vb.add(w2).proportion(1).alignH(HAlign.right);
1066 	vb.layout(Rect(0, 0, 100, 50));
1067 	assert(w2.bounds == Rect(80, 0, 20, 50)); // x = 100-20
1068 }