libgui 2.01
libgui is a GUI library for the GameCube, Wii, and Wii U
 
Loading...
Searching...
No Matches
GuiLayoutSolver.cpp
1/****************************************************************************
2 * libgui
3 * Daryl Borth 2026
4 * GuiLayoutSolver.cpp
5 ***************************************************************************/
6
7#include "GuiLayoutSolver.h"
8#include <algorithm>
9#include <cmath>
10
11namespace {
12
13float placeAcross(LAYOUT_ALIGN a, float start, float space, float size)
14{
15 switch(a)
16 {
17 case LAYOUT_ALIGN::CENTRE: return start + (space - size) * 0.5f;
18 case LAYOUT_ALIGN::END: return start + space - size;
19 default: return start;
20 }
21}
22
23} // namespace
24
25GuiLayoutResult GuiSolveStack(const GuiStackSpec& spec, const std::vector<GuiStackItem>& items)
26{
28 const size_t n = items.size();
29 out.rects.assign(n, GuiRect{ 0, 0, 0, 0 });
30
31 const bool v = spec.vertical;
32 const float padMainStart = v ? spec.padding.top : spec.padding.left;
33 const float padMainEnd = v ? spec.padding.bottom : spec.padding.right;
34 const float padCrossStart = v ? spec.padding.left : spec.padding.top;
35 const float padCrossEnd = v ? spec.padding.right : spec.padding.bottom;
36 const float fixedMain = v ? spec.fixedH : spec.fixedW;
37 const float fixedCross = v ? spec.fixedW : spec.fixedH;
38
39 float sumMain = 0.0f, maxCross = 0.0f, growSum = 0.0f;
40 for(size_t i = 0; i < n; i++)
41 {
42 sumMain += v ? items[i].h : items[i].w;
43 maxCross = std::max(maxCross, v ? items[i].w : items[i].h);
44 growSum += std::max(items[i].grow, 0.0f);
45 }
46 if(n > 1)
47 sumMain += spec.spacing * (float)(n - 1);
48
49 const float totalMain = fixedMain > 0.0f ? fixedMain : sumMain + padMainStart + padMainEnd;
50 const float totalCross = fixedCross > 0.0f ? fixedCross : maxCross + padCrossStart + padCrossEnd;
51 out.width = v ? totalCross : totalMain;
52 out.height = v ? totalMain : totalCross;
53
54 const float innerCross = std::max(0.0f, totalCross - padCrossStart - padCrossEnd);
55 float freeMain = totalMain - padMainStart - padMainEnd - sumMain;
56
57 float offset = 0.0f, extraGap = 0.0f;
58 std::vector<float> mainSize(n);
59 for(size_t i = 0; i < n; i++)
60 mainSize[i] = v ? items[i].h : items[i].w;
61
62 if(freeMain > 0.0f)
63 {
64 if(growSum > 0.0f)
65 {
66 for(size_t i = 0; i < n; i++)
67 mainSize[i] += freeMain * std::max(items[i].grow, 0.0f) / growSum;
68 }
69 else
70 {
71 switch(spec.justify)
72 {
73 case LAYOUT_JUSTIFY::CENTRE: offset = freeMain * 0.5f; break;
74 case LAYOUT_JUSTIFY::END: offset = freeMain; break;
75 case LAYOUT_JUSTIFY::SPACE_BETWEEN: if(n > 1) extraGap = freeMain / (float)(n - 1); break;
76 default: break;
77 }
78 }
79 }
80
81 float pos = padMainStart + offset;
82 for(size_t i = 0; i < n; i++)
83 {
84 const LAYOUT_ALIGN align = items[i].useContainerAlign ? spec.alignItems : items[i].align;
85 float cross = v ? items[i].w : items[i].h;
86 if(align == LAYOUT_ALIGN::STRETCH)
87 cross = innerCross;
88 const float crossPos = placeAcross(align, padCrossStart, innerCross, cross);
89
90 GuiRect r;
91 if(v) { r.x = crossPos; r.y = pos; r.w = cross; r.h = mainSize[i]; }
92 else { r.x = pos; r.y = crossPos; r.w = mainSize[i]; r.h = cross; }
93 out.rects[i] = r;
94 pos += mainSize[i] + spec.spacing + extraGap;
95 }
96 return out;
97}
98
99GuiLayoutResult GuiSolveGrid(const GuiGridSpec& spec, const std::vector<GuiGridItem>& items)
100{
101 GuiLayoutResult out;
102 const int cols = std::max(1, spec.columns);
103 const size_t n = items.size();
104 out.rects.assign(n, GuiRect{ 0, 0, 0, 0 });
105 const int rows = n == 0 ? 0 : (int)((n + cols - 1) / cols);
106
107 std::vector<float> colW((size_t)cols, 0.0f), rowH((size_t)std::max(rows, 0), 0.0f);
108 for(size_t i = 0; i < n; i++)
109 {
110 const size_t c = i % cols, r = i / cols;
111 colW[c] = std::max(colW[c], items[i].w);
112 rowH[r] = std::max(rowH[r], items[i].h);
113 }
114
115 const float padH = spec.padding.left + spec.padding.right;
116 const float padV = spec.padding.top + spec.padding.bottom;
117 if(spec.fixedW > 0.0f)
118 {
119 const float each = std::max(0.0f, (spec.fixedW - padH - spec.spacingX * (float)(cols - 1)) / (float)cols);
120 for(int c = 0; c < cols; c++)
121 colW[(size_t)c] = each;
122 }
123
124 float sumW = 0.0f, sumH = 0.0f;
125 for(int c = 0; c < cols; c++)
126 sumW += colW[(size_t)c];
127 for(int r = 0; r < rows; r++)
128 sumH += rowH[(size_t)r];
129 sumW += spec.spacingX * (float)(cols - 1);
130 if(rows > 1)
131 sumH += spec.spacingY * (float)(rows - 1);
132
133 out.width = spec.fixedW > 0.0f ? spec.fixedW : sumW + padH;
134 out.height = spec.fixedH > 0.0f ? spec.fixedH : sumH + padV;
135
136 std::vector<float> colX((size_t)cols), rowY((size_t)std::max(rows, 0));
137 float x = spec.padding.left;
138 for(int c = 0; c < cols; c++)
139 {
140 colX[(size_t)c] = x;
141 x += colW[(size_t)c] + spec.spacingX;
142 }
143 float y = spec.padding.top;
144 for(int r = 0; r < rows; r++)
145 {
146 rowY[(size_t)r] = y;
147 y += rowH[(size_t)r] + spec.spacingY;
148 }
149
150 for(size_t i = 0; i < n; i++)
151 {
152 const size_t c = i % cols, r = i / cols;
153 float w = items[i].alignH == LAYOUT_ALIGN::STRETCH ? colW[c] : items[i].w;
154 float h = items[i].alignV == LAYOUT_ALIGN::STRETCH ? rowH[r] : items[i].h;
155 GuiRect rect;
156 rect.x = placeAcross(items[i].alignH, colX[c], colW[c], w);
157 rect.y = placeAcross(items[i].alignV, rowY[r], rowH[r], h);
158 rect.w = w;
159 rect.h = h;
160 out.rects[i] = rect;
161 }
162 return out;
163}
164
165GuiLayoutResult GuiSolveOverlay(const GuiOverlaySpec& spec, const std::vector<GuiOverlayItem>& items)
166{
167 GuiLayoutResult out;
168 const size_t n = items.size();
169 out.rects.assign(n, GuiRect{ 0, 0, 0, 0 });
170
171 float maxW = 0.0f, maxH = 0.0f;
172 for(size_t i = 0; i < n; i++)
173 {
174 if(items[i].background)
175 continue;
176 maxW = std::max(maxW, items[i].w);
177 maxH = std::max(maxH, items[i].h);
178 }
179 const float padH = spec.padding.left + spec.padding.right;
180 const float padV = spec.padding.top + spec.padding.bottom;
181 out.width = spec.fixedW > 0.0f ? spec.fixedW : maxW + padH;
182 out.height = spec.fixedH > 0.0f ? spec.fixedH : maxH + padV;
183
184 const float innerW = std::max(0.0f, out.width - padH);
185 const float innerH = std::max(0.0f, out.height - padV);
186 for(size_t i = 0; i < n; i++)
187 {
188 if(items[i].background)
189 {
190 out.rects[i] = GuiRect{ 0, 0, out.width, out.height };
191 continue;
192 }
193 const float w = items[i].alignH == LAYOUT_ALIGN::STRETCH ? innerW : items[i].w;
194 const float h = items[i].alignV == LAYOUT_ALIGN::STRETCH ? innerH : items[i].h;
195 out.rects[i] = GuiRect{ placeAcross(items[i].alignH, spec.padding.left, innerW, w), placeAcross(items[i].alignV, spec.padding.top, innerH, h), w, h };
196 }
197 return out;
198}
199
200namespace {
201
202struct Cand {
203 int index;
204 float cx, cy;
205};
206
207void centreOf(const GuiRect& r, float& cx, float& cy)
208{
209 cx = r.x + r.w * 0.5f;
210 cy = r.y + r.h * 0.5f;
211}
212
213//Length by which [a0,a1] and [b0,b1] overlap, or minus the gap between them
214float overlapOrGap(float a0, float a1, float b0, float b1)
215{
216 return std::min(a1, b1) - std::max(a0, b0);
217}
218
219} // namespace
220
221int GuiNavigate(const std::vector<GuiNavItem>& items, int current, NAV_DIR dir)
222{
223 const int n = (int)items.size();
224 const bool horizontal = dir == NAV_DIR::LEFT || dir == NAV_DIR::RIGHT;
225 const float sign = (dir == NAV_DIR::RIGHT || dir == NAV_DIR::DOWN) ? 1.0f : -1.0f;
226
227 // nothing selected: the first selectable item in reading order
228 if(current < 0 || current >= n)
229 {
230 int best = -1;
231 float bx = 0, by = 0;
232 for(int i = 0; i < n; i++)
233 {
234 if(!items[i].selectable)
235 continue;
236 float cx, cy;
237 centreOf(items[i].r, cx, cy);
238 if(best < 0 || cy < by - 1.0f || (fabsf(cy - by) <= 1.0f && cx < bx))
239 {
240 best = i;
241 bx = cx;
242 by = cy;
243 }
244 }
245 return best;
246 }
247
248 float ccx, ccy;
249 centreOf(items[current].r, ccx, ccy);
250 const GuiRect& cur = items[current].r;
251
252 // nearest item in the direction, preferring those that line up with the current one
253 int best = -1;
254 float bestScore = 0.0f;
255 for(int i = 0; i < n; i++)
256 {
257 if(i == current || !items[i].selectable)
258 continue;
259 float cx, cy;
260 centreOf(items[i].r, cx, cy);
261 const float along = horizontal ? (cx - ccx) * sign : (cy - ccy) * sign;
262 if(along <= 1.0f)
263 continue;
264 const GuiRect& r = items[i].r;
265 const float ov = horizontal ? overlapOrGap(cur.y, cur.y + cur.h, r.y, r.y + r.h) : overlapOrGap(cur.x, cur.x + cur.w, r.x, r.x + r.w);
266 const float across = ov > 0.0f ? 0.0f : -ov;
267 const float offAxis = fabsf(horizontal ? cy - ccy : cx - ccx);
268 const float score = along + 4.0f * across + 0.01f * offAxis;
269 if(best < 0 || score < bestScore)
270 {
271 best = i;
272 bestScore = score;
273 }
274 }
275 if(best >= 0 || !horizontal)
276 return best;
277
278 // off the end of a row: continue on the next row (right) or the previous row (left)
279 for(int i = 0; i < n; i++)
280 {
281 if(i == current || !items[i].selectable)
282 continue;
283 float cx, cy;
284 centreOf(items[i].r, cx, cy);
285 const bool otherRow = sign > 0 ? (cy > cur.y + cur.h) : (cy < cur.y);
286 if(!otherRow)
287 continue;
288 if(best < 0)
289 {
290 best = i;
291 continue;
292 }
293 float bx, by;
294 centreOf(items[best].r, bx, by);
295 const bool closerRow = sign > 0 ? (cy < by - 1.0f) : (cy > by + 1.0f);
296 const bool sameRow = fabsf(cy - by) <= 1.0f;
297 const bool betterInRow = sign > 0 ? (cx < bx) : (cx > bx);
298 if(closerRow || (sameRow && betterInRow))
299 best = i;
300 }
301 return best;
302}
NAV_DIR
Directions for moving the selection.
LAYOUT_ALIGN
How an item sits across the main direction of a stack, or inside a grid cell.
GuiLayoutResult GuiSolveGrid(const GuiGridSpec &spec, const std::vector< GuiGridItem > &items)
Lays items out in rows.
int GuiNavigate(const std::vector< GuiNavItem > &items, int current, NAV_DIR dir)
Picks which item the selection moves to.
GuiLayoutResult GuiSolveStack(const GuiStackSpec &spec, const std::vector< GuiStackItem > &items)
Lays items out along one axis.
GuiLayoutResult GuiSolveOverlay(const GuiOverlaySpec &spec, const std::vector< GuiOverlayItem > &items)
Stacks items on top of each other in one box.
@ STRETCH
Fills the available space.
@ CENTRE
Centered.
@ END
Bottom or right.
@ SPACE_BETWEEN
First item at the start, last at the end, equal gaps between.
@ CENTRE
Items packed in the middle.
@ END
Items packed at the end.
Rules for a grid, filled row by row.
float fixedH
Container size; 0 = fit.
The solver's answer: the container's size and each item's rectangle.
Rules for an overlay: children share one box.
float fixedH
Container size; 0 = fit.
An axis-aligned rectangle (top-left origin) in design-canvas pixels.
Definition GuiGeometry.h:30
float x
Left edge.
Definition GuiGeometry.h:31
float w
Width.
Definition GuiGeometry.h:33
float h
Height.
Definition GuiGeometry.h:34
float y
Top edge.
Definition GuiGeometry.h:32
Rules for a row or column.
float spacing
Gap between neighbouring items.
bool vertical
true = column, false = row.
float fixedH
Container size; 0 = fit.