0
|
1 // Written in the D programming language
|
|
2 // www.digitalmars.com/d/
|
|
3
|
|
4 /*
|
|
5 * The contents of this file are subject to the Mozilla Public License Version
|
|
6 * 1.1 (the "License"); you may not use this file except in compliance with
|
|
7 * the License. You may obtain a copy of the License at
|
|
8 * http://www.mozilla.org/MPL/
|
|
9 *
|
|
10 * Software distributed under the License is distributed on an "AS IS" basis,
|
|
11 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
|
12 * for the specific language governing rights and limitations under the
|
|
13 * License.
|
|
14 *
|
|
15 * The Original Code is the Dynamin library.
|
|
16 *
|
|
17 * The Initial Developer of the Original Code is Jordan Miner.
|
|
18 * Portions created by the Initial Developer are Copyright (C) 2007-2009
|
|
19 * the Initial Developer. All Rights Reserved.
|
|
20 *
|
|
21 * Contributor(s):
|
|
22 * Jordan Miner <jminer7@gmail.com>
|
|
23 *
|
|
24 */
|
|
25
|
|
26 module dynamin.gui.layout;
|
|
27
|
|
28 import dynamin.all_gui;
|
|
29 import dynamin.gui.control;
|
|
30 import dynamin.all_painting;
|
|
31 import dynamin.core.string;
|
|
32 import tango.io.Stdout;
|
|
33 import dynamin.core.benchmark;
|
|
34
|
|
35 // this is a temporary file to hold layout code until I figure out what
|
|
36 // files to put it in
|
|
37
|
|
38 /*
|
|
39 Opera's find dialog:
|
|
40
|
|
41 auto whatLabel = win.content.add(new Label("Find What"));
|
|
42 ...
|
|
43
|
|
44 V( whatLabel
|
|
45 H( findBox findButton )
|
|
46 H( V(wholeWordCheck caseCheck) ~ V(upRadio downRadio) ~)
|
|
47 H( ~ closeButton )
|
|
48 )
|
|
49 */
|
|
50
|
|
51 enum LayoutType {
|
|
52 None, Table, Control, Filler, Spacer
|
|
53 }
|
|
54 enum Elasticity {
|
|
55 No, Semi, Yes
|
|
56 }
|
|
57 struct LayoutGroup {
|
|
58 LayoutType type;
|
|
59 LayoutGroup* parent;
|
|
60 LayoutGroup[] children; // used if type == LayoutType.Horiz or Vert or Table
|
|
61 Control control; // used if type == LayoutType.Control
|
|
62 int numColumns; // used if type == LayoutType.Table
|
|
63 int numRows() { return children.length / numColumns; }
|
|
64
|
|
65 bool cacheActive;
|
|
66 private Elasticity _elasticXCache, _elasticYCache;
|
|
67 private Size _bestSizeCache;
|
|
68 private int _baselineCache;
|
|
69
|
|
70 // spacing variables
|
|
71 int spacing = 8;
|
|
72 static LayoutGroup opCall(LayoutType type, LayoutGroup* parent) {
|
|
73 LayoutGroup layout;
|
|
74 layout.type = type;
|
|
75 layout.parent = parent;
|
|
76 layout.children.length = 3;
|
|
77 layout.children.length = 0;
|
|
78 return layout;
|
|
79 }
|
|
80
|
|
81 void setCache() {
|
|
82 for(int i = 0; i < children.length; ++i) // can't use foreach--copies
|
|
83 children[i].setCache();
|
|
84 _elasticXCache = _elasticX;
|
|
85 _elasticYCache = _elasticY;
|
|
86 _bestSizeCache = _bestSize;
|
|
87 _baselineCache = _baseline;
|
|
88 cacheActive = true;
|
|
89 }
|
|
90 void clearCache() {
|
|
91 cacheActive = false;
|
|
92 for(int i = 0; i < children.length; ++i) // can't use foreach--copies
|
|
93 children[i].clearCache();
|
|
94 }
|
|
95 Elasticity elasticX() { return cacheActive ? _elasticXCache : _elasticX; }
|
|
96 Elasticity elasticY() { return cacheActive ? _elasticYCache : _elasticY; }
|
|
97 Size bestSize() { return cacheActive ? _bestSizeCache : _bestSize; }
|
|
98 int baseline() { return cacheActive ? _baselineCache : _baseline; }
|
|
99
|
|
100 //{{{ _elasticX()
|
|
101 private Elasticity _elasticX() {
|
|
102 switch(type) {
|
|
103 case LayoutType.Control:
|
|
104 return control.elasticX ? Elasticity.Yes : Elasticity.No;
|
|
105 case LayoutType.Table:
|
|
106 auto e = Elasticity.No;
|
|
107 foreach(layout; children) {
|
|
108 if(layout.elasticX > e)
|
|
109 e = layout.elasticX;
|
|
110 if(e == Elasticity.Yes)
|
|
111 return e;
|
|
112 }
|
|
113 return e;
|
|
114 case LayoutType.Filler:
|
|
115 return Elasticity.Semi;
|
|
116 case LayoutType.Spacer:
|
|
117 return Elasticity.No;
|
|
118 }
|
|
119 }
|
|
120 //}}}
|
|
121 //{{{ _elasticY()
|
|
122 private Elasticity _elasticY() {
|
|
123 switch(type) {
|
|
124 case LayoutType.Control:
|
|
125 return control.elasticY ? Elasticity.Yes : Elasticity.No;
|
|
126 case LayoutType.Table:
|
|
127 auto e = Elasticity.No;
|
|
128 foreach(layout; children) {
|
|
129 if(layout.elasticY > e)
|
|
130 e = layout.elasticY;
|
|
131 if(e == Elasticity.Yes)
|
|
132 return e;
|
|
133 }
|
|
134 return e;
|
|
135 case LayoutType.Filler:
|
|
136 return Elasticity.Semi;
|
|
137 case LayoutType.Spacer:
|
|
138 return Elasticity.No;
|
|
139 }
|
|
140 }
|
|
141 //}}}
|
|
142
|
|
143 //{{{ _bestSize()
|
|
144 private Size _bestSize() {
|
|
145 switch(type) {
|
|
146 case LayoutType.Control:
|
|
147 return control.bestSize;
|
|
148 case LayoutType.Table:
|
|
149 scope colsInfo = new ColRowInfo[numColumns];
|
|
150 scope rowsInfo = new ColRowInfo[numRows];
|
|
151 TableInfo info;
|
|
152 getTableSizes(colsInfo, rowsInfo, info);
|
|
153 return info.bestSize;
|
|
154 case LayoutType.Filler:
|
|
155 case LayoutType.Spacer:
|
|
156 return Size(0, 0);
|
|
157 }
|
|
158 }
|
|
159 //}}}
|
|
160 //{{{ _baseline()
|
|
161 private int _baseline() {
|
|
162 switch(type) {
|
|
163 case LayoutType.Control:
|
|
164 return control.baseline;
|
|
165 case LayoutType.Table:
|
|
166 case LayoutType.Filler:
|
|
167 case LayoutType.Spacer:
|
|
168 return 0;
|
|
169 }
|
|
170 }
|
|
171 //}}}
|
|
172
|
|
173 //{{{ layout()
|
|
174 void layout(Rect rect) {
|
|
175 switch(type) {
|
|
176 case LayoutType.Control:
|
|
177 control.location = Point(rect.x, rect.y);
|
|
178 control.size = Size(rect.width, rect.height);
|
|
179 return;
|
|
180 case LayoutType.Table:
|
|
181 scope colsInfo = new ColRowInfo[numColumns];
|
|
182 scope rowsInfo = new ColRowInfo[numRows];
|
|
183 TableInfo info;
|
|
184 getTableSizes(colsInfo, rowsInfo, info);
|
|
185
|
|
186 real extraWidth = rect.width - bestSize.width;
|
|
187 real extraHeight = rect.height - bestSize.height;
|
|
188
|
|
189 void distExtra(ref real extra, ref ColRowInfo info,
|
|
190 ref real totalElastic, ref int semis, Elasticity e) {
|
|
191 if(info.elastic == Elasticity.No || extra <= 0)
|
|
192 return;
|
|
193 if(e == Elasticity.Semi &&
|
|
194 info.elastic == Elasticity.Semi) {
|
|
195 auto thisExtra = extra / semis;
|
|
196 extra -= thisExtra;
|
|
197 semis--;
|
|
198 info.bestSize += thisExtra;
|
|
199 } else if(e == Elasticity.Yes &&
|
|
200 info.elastic == Elasticity.Yes) {
|
|
201 auto thisExtra = extra * info.bestSize/totalElastic;
|
|
202 extra -= thisExtra;
|
|
203 totalElastic -= info.bestSize; // subtract original size
|
|
204 info.bestSize += thisExtra;
|
|
205 }
|
|
206 }
|
|
207 real y = 0;
|
|
208 for(int row = 0; row < numRows; ++row) { // go over each row
|
|
209 distExtra(extraHeight, rowsInfo[row], info.elasticHeight, info.semiRows, elasticY);
|
|
210
|
|
211 real x = 0;
|
|
212 for(int col = 0; col < numColumns; ++col) {
|
|
213 distExtra(extraWidth, colsInfo[col], info.elasticWidth, info.semiColumns, elasticX);
|
|
214
|
|
215 auto layout = children[row * numColumns + col];
|
|
216
|
|
217 Rect r = Point(x, y) + layout.bestSize;
|
|
218
|
|
219 if(layout.baseline > 0)
|
|
220 r.y = r.y + rowsInfo[row].baseline - layout.baseline;
|
|
221 if(layout.elasticX)
|
|
222 r.width = colsInfo[col].bestSize;
|
|
223 if(layout.elasticY)
|
|
224 r.height = rowsInfo[row].bestSize;
|
|
225
|
|
226 layout.layout(r + Point(rect.x, rect.y));
|
|
227
|
|
228 x += colsInfo[col].bestSize +
|
|
229 (colsInfo[col].filler ? 0 : spacing);
|
|
230 }
|
|
231 y += rowsInfo[row].bestSize +
|
|
232 (rowsInfo[row].filler ? 0 : spacing);
|
|
233 }
|
|
234 return;
|
|
235 case LayoutType.Filler:
|
|
236 case LayoutType.Spacer:
|
|
237 return;
|
|
238 }
|
|
239 }
|
|
240 //}}}
|
|
241
|
|
242 struct ColRowInfo {
|
|
243 real bestSize = 0; // large enough to hold the largest control
|
|
244 Elasticity elastic = Elasticity.No;
|
|
245 bool filler = true; // if the entire column/row is filler
|
|
246 real baseline; // only applies to rows: max baseline in row
|
|
247 }
|
|
248 struct TableInfo {
|
|
249 // number of semi-elastic columns/rows
|
|
250 int semiColumns = 0; int semiRows = 0;
|
|
251 // the sum of fully elastic width/height, not including semi
|
|
252 real elasticWidth = 0, elasticHeight = 0;
|
|
253 Size bestSize = Size(0, 0);
|
|
254 }
|
|
255 //{{{ getTableSizes()
|
|
256 // Fills in the passed in array with the column and row sizes, as well
|
|
257 // as whether they are elastic. The passed in arrays must be the right
|
|
258 // sizes. They may be stack allocated. The table best size does
|
|
259 // including spacing, but column and row best sizes do not.
|
|
260 private void getTableSizes(ColRowInfo[] colsInfo, ColRowInfo[] rowsInfo, ref TableInfo info) {
|
|
261 assert(children.length % numColumns == 0);
|
|
262 assert(type == LayoutType.Table);
|
|
263
|
|
264 assert(colsInfo.length == numColumns);
|
|
265 assert(rowsInfo.length == numRows);
|
|
266
|
|
267 real max = 0, temp;
|
|
268 LayoutGroup* l;
|
|
269
|
|
270 int sp = 0;
|
|
271 for(int col = 0; col < numColumns; ++col) { // go down each column
|
|
272 for(int row = 0; row < numRows; ++row) {
|
|
273 l = &children[row * numColumns + col];
|
|
274 max = l.bestSize.width > max ? l.bestSize.width : max;
|
|
275 if(l.elasticX > colsInfo[col].elastic)
|
|
276 colsInfo[col].elastic = l.elasticX;
|
|
277 if(l.type != LayoutType.Filler)
|
|
278 colsInfo[col].filler = false;
|
|
279 }
|
|
280 colsInfo[col].bestSize = max;
|
|
281 if(colsInfo[col].elastic == Elasticity.Yes)
|
|
282 info.elasticWidth += max;
|
|
283 else if(colsInfo[col].elastic == Elasticity.Semi)
|
|
284 info.semiColumns++;
|
|
285 info.bestSize.width = info.bestSize.width + sp + max;
|
|
286 sp = (colsInfo[col].filler ? 0 : spacing);
|
|
287 max = 0;
|
|
288 }
|
|
289
|
|
290 real maxBl = 0;
|
|
291 sp = 0;
|
|
292 for(int row = 0; row < numRows; ++row) { // go over each row
|
|
293 for(int col = 0; col < numColumns; ++col) {
|
|
294 l = &children[row * numColumns + col];
|
|
295 max = l.bestSize.height > max ? l.bestSize.height : max;
|
|
296 maxBl = l.baseline > maxBl ? l.baseline : maxBl;
|
|
297 if(l.elasticY > rowsInfo[row].elastic)
|
|
298 rowsInfo[row].elastic = l.elasticY;
|
|
299 if(l.type != LayoutType.Filler)
|
|
300 rowsInfo[row].filler = false;
|
|
301 }
|
|
302 rowsInfo[row].bestSize = max;
|
|
303 rowsInfo[row].baseline = maxBl;
|
|
304 if(rowsInfo[row].elastic == Elasticity.Yes)
|
|
305 info.elasticHeight += max;
|
|
306 else if(rowsInfo[row].elastic == Elasticity.Semi)
|
|
307 info.semiRows++;
|
|
308 info.bestSize.height = info.bestSize.height + sp + max;
|
|
309 sp = (rowsInfo[row].filler ? 0 : spacing);
|
|
310 max = maxBl = 0;
|
|
311 }
|
|
312 }
|
|
313 //}}}
|
|
314 }
|
|
315
|
|
316 //{{{ LayoutPanel class
|
|
317 class LayoutPanel : Panel {
|
|
318 LayoutGroup root;
|
|
319 LayoutGroup* current;
|
|
320 void startLayout(int ncolumns) {
|
|
321 if(current is null) {
|
|
322 root = LayoutGroup(LayoutType.Table, null);
|
|
323 root.numColumns = ncolumns;
|
|
324 current = &root;
|
|
325 return;
|
|
326 }
|
|
327 current.children.length = current.children.length+1;
|
|
328 current.children[$-1] = LayoutGroup(LayoutType.Table, current);
|
|
329 current.children[$-1].numColumns = ncolumns;
|
|
330 current = ¤t.children[$-1];
|
|
331 }
|
|
332 void endLayout() {
|
|
333 current = current.parent;
|
|
334 }
|
|
335 override void add(Control c) {
|
|
336 if(current is null)
|
|
337 throw new Exception("Cannot add a control until a layout is started");
|
|
338 current.children.length = current.children.length+1;
|
|
339 current.children[$-1] = LayoutGroup(LayoutType.Control, current);
|
|
340 current.children[$-1].control = c;
|
|
341 super.add(c);
|
|
342 }
|
|
343 void addFiller() {
|
|
344 current.children.length = current.children.length+1;
|
|
345 current.children[$-1] = LayoutGroup(LayoutType.Filler, current);
|
|
346 }
|
|
347 void addSpacer() {
|
|
348 current.children.length = current.children.length+1;
|
|
349 current.children[$-1] = LayoutGroup(LayoutType.Spacer, current);
|
|
350 }
|
|
351
|
|
352 override Size bestSize() {
|
|
353 return root.bestSize + Size(root.spacing*2, root.spacing*2);
|
|
354 }
|
|
355 override bool elasticX() { return root.elasticX == Elasticity.Yes; }
|
|
356 override bool elasticY() { return root.elasticY == Elasticity.Yes; }
|
|
357 override void layout() {
|
|
358 //benchmarkAndWrite("layout", {
|
|
359 root.setCache();
|
|
360 int sp = root.spacing;
|
|
361 root.layout(Rect(sp, sp, width-2*sp, height-2*sp));
|
|
362 root.clearCache();
|
|
363 //});
|
|
364 }
|
|
365 }
|
|
366 //}}}
|
|
367
|
|
368 //{{{ createLayout() etc.
|
|
369 /**
|
|
370 * Note: if you do this:
|
|
371 * -----
|
|
372 * char[] s = createLayout("V( b1 H(b2 b3) )");
|
|
373 * -----
|
|
374 * Then the program will crash when compiled with the -release flag. (I am
|
|
375 * pretty sure it is a DMD bug, but I don't have time to make a testcase
|
|
376 * for a bug that does not bother me.) This will work correctly:
|
|
377 * -----
|
|
378 * const char[] s = createLayout("V( b1 H(b2 b3) )");
|
|
379 * -----
|
|
380 * Because then the function is interpreted at compile time with CTFE.
|
|
381 */
|
|
382 string createLayout(string layout) {
|
|
383 string code = "delegate LayoutPanel() {\n";
|
|
384 code ~= "auto panel = new LayoutPanel;\n";
|
|
385 assert(getToken(layout) == "H" || getToken(layout) == "V" ||
|
|
386 getToken(layout) == "T", "layout type 'H', 'V', or 'T' expected");
|
|
387 code ~= parseLayout(layout);
|
|
388 code ~= "return panel;\n";
|
|
389 code ~= "}()";
|
|
390 return code;
|
|
391 }
|
|
392
|
|
393 void skipWS(ref string str) {
|
|
394 int i = 0;
|
|
395 while(" \t\n\r\v\f".contains(str[i]))
|
|
396 i++;
|
|
397 str = str[i..$];
|
|
398 }
|
|
399 // advances to the next token and returns it
|
|
400 string nextToken(ref string str) {
|
|
401 skipWS(str);
|
|
402 str = str[getToken(str).length..$];
|
|
403 return getToken(str);
|
|
404 }
|
|
405 // returns H or V or ( or ) or myControl
|
|
406 // gets the current token
|
|
407 string getToken(string str) {
|
|
408 string idChars =
|
|
409 "_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
|
410
|
|
411 // TODO: // for line comments?
|
|
412 skipWS(str);
|
|
413 if("()~[]-".contains(str[0])) {
|
|
414 return str[0..1];
|
|
415 } else if(idChars.contains(str[0])) {
|
|
416 int i = 1;
|
|
417 while(idChars.contains(str[i]))
|
|
418 i++;
|
|
419 return str[0..i];
|
|
420 } else {
|
|
421 assert(0, "unknown character: " ~ str[0]);
|
|
422 }
|
|
423 }
|
|
424
|
|
425 // {{{ copied from Phobos
|
|
426 char[] ctfeUintToString(uint u) {
|
|
427 char[uint.sizeof * 3] buffer = void;
|
|
428 int ndigits;
|
|
429 char[] result;
|
|
430 char[] digits = "0123456789";
|
|
431
|
|
432 ndigits = 0;
|
|
433 if (u < 10)
|
|
434 // Avoid storage allocation for simple stuff
|
|
435 result = digits[u .. u + 1];
|
|
436 else
|
|
437 {
|
|
438 while (u)
|
|
439 {
|
|
440 uint c = (u % 10) + '0';
|
|
441 u /= 10;
|
|
442 ndigits++;
|
|
443 buffer[buffer.length - ndigits] = cast(char)c;
|
|
444 }
|
|
445 result = new char[ndigits];
|
|
446 result[] = buffer[buffer.length - ndigits .. buffer.length];
|
|
447 }
|
|
448 return result;
|
|
449 }
|
|
450 uint ctfeStringToUint(char[] s)
|
|
451 {
|
|
452 int length = s.length;
|
|
453
|
|
454 if (!length)
|
|
455 return 0;
|
|
456
|
|
457 uint v = 0;
|
|
458
|
|
459 for (int i = 0; i < length; i++)
|
|
460 {
|
|
461 char c = s[i];
|
|
462 if (c >= '0' && c <= '9')
|
|
463 {
|
|
464 if (v < uint.max/10 || (v == uint.max/10 && c <= '5'))
|
|
465 v = v * 10 + (c - '0');
|
|
466 else
|
|
467 return 0;
|
|
468 }
|
|
469 else
|
|
470 return 0;
|
|
471 }
|
|
472 return v;
|
|
473 }
|
|
474 //}}}
|
|
475
|
|
476 uint parseBody(ref string layout, ref string bcode) {
|
|
477 uint count = 0;
|
|
478 assert(nextToken(layout) == "(", "open parenthesis expected");
|
|
479 while(nextToken(layout) != ")") {
|
|
480 if(getToken(layout) == "~")
|
|
481 bcode = bcode ~ "panel.addFiller();\n";
|
|
482 else if(getToken(layout) == "-")
|
|
483 bcode = bcode ~ "panel.addSpacer();\n";
|
|
484 else
|
|
485 bcode = bcode ~ parseLayout(layout);
|
|
486 count++;
|
|
487 }
|
|
488 bcode = bcode ~ "panel.endLayout();\n";
|
|
489 return count;
|
|
490 }
|
|
491
|
|
492 string parseLayout(ref string layout) {
|
|
493 string code = "";
|
|
494
|
|
495 if(getToken(layout) == "H") {
|
|
496 string bodyCode;
|
|
497 auto count = parseBody(layout, bodyCode);
|
|
498 code ~= "panel.startLayout(" ~ ctfeUintToString(count) ~ ");\n";
|
|
499 code ~= bodyCode;
|
|
500 } else if(getToken(layout) == "V") {
|
|
501 code ~= "panel.startLayout(1);\n";
|
|
502 parseBody(layout, code);
|
|
503 } else if(getToken(layout) == "T") {
|
|
504 assert(nextToken(layout) == "[", "open bracket expected");
|
|
505 nextToken(layout);
|
|
506 assert("0123456789".contains(getToken(layout)[0]),
|
|
507 "number of table columns expected");
|
|
508 uint columns = ctfeStringToUint(getToken(layout));
|
|
509 code ~= "panel.startLayout(" ~ getToken(layout) ~ ");\n";
|
|
510 assert(nextToken(layout) == "]", "close bracket expected");
|
|
511 assert(parseBody(layout, code) % columns == 0,
|
|
512 "number of controls must be a multiple of number of columns");
|
|
513 } else {
|
|
514 code ~= "panel.add(" ~ getToken(layout) ~ ");\n";
|
|
515 }
|
|
516
|
|
517 return code;
|
|
518 }
|
|
519
|
|
520 //{{{ parser tests
|
|
521 static assert(createLayout("H()") != "not evaluatable at compile time");
|
|
522 //pragma(msg, createLayout("V()"));
|
|
523
|
|
524 static assert(createLayout("V(c1 c2)") ==
|
|
525 `delegate LayoutPanel() {
|
|
526 auto panel = new LayoutPanel;
|
|
527 panel.startLayout(1);
|
|
528 panel.add(c1);
|
|
529 panel.add(c2);
|
|
530 panel.endLayout();
|
|
531 return panel;
|
|
532 }()`);
|
|
533 static assert(createLayout("V(c1 ~ c2 H(c3 -) c4)") ==
|
|
534 `delegate LayoutPanel() {
|
|
535 auto panel = new LayoutPanel;
|
|
536 panel.startLayout(1);
|
|
537 panel.add(c1);
|
|
538 panel.addFiller();
|
|
539 panel.add(c2);
|
|
540 panel.startLayout(2);
|
|
541 panel.add(c3);
|
|
542 panel.addSpacer();
|
|
543 panel.endLayout();
|
|
544 panel.add(c4);
|
|
545 panel.endLayout();
|
|
546 return panel;
|
|
547 }()`);
|
|
548 static assert(createLayout("V( c1 T[2](c2 c3) c4 )") ==
|
|
549 `delegate LayoutPanel() {
|
|
550 auto panel = new LayoutPanel;
|
|
551 panel.startLayout(1);
|
|
552 panel.add(c1);
|
|
553 panel.startLayout(2);
|
|
554 panel.add(c2);
|
|
555 panel.add(c3);
|
|
556 panel.endLayout();
|
|
557 panel.add(c4);
|
|
558 panel.endLayout();
|
|
559 return panel;
|
|
560 }()`);
|
|
561 //}}}
|
|
562
|
|
563 //}}}
|
|
564
|
|
565 unittest {
|
|
566 // TODO: set to basic theme
|
|
567 // test a few basic layouts and verify pixel locations and sizes
|
|
568 }
|
|
569
|