Mercurial > projects > dynamin
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 = ¤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 |