comparison dynamin/gui/layout.d @ 0:aa4efef0f0b1

Initial commit of code.
author Jordan Miner <jminer7@gmail.com>
date Mon, 15 Jun 2009 22:10:48 -0500
parents
children df1c8e659b75
comparison
equal deleted inserted replaced
-1:000000000000 0:aa4efef0f0b1
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 = &current.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