Mercurial > projects > dynamin
diff 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 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dynamin/gui/layout.d Mon Jun 15 22:10:48 2009 -0500 @@ -0,0 +1,569 @@ +// Written in the D programming language +// www.digitalmars.com/d/ + +/* + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is the Dynamin library. + * + * The Initial Developer of the Original Code is Jordan Miner. + * Portions created by the Initial Developer are Copyright (C) 2007-2009 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Jordan Miner <jminer7@gmail.com> + * + */ + +module dynamin.gui.layout; + +import dynamin.all_gui; +import dynamin.gui.control; +import dynamin.all_painting; +import dynamin.core.string; +import tango.io.Stdout; +import dynamin.core.benchmark; + +// this is a temporary file to hold layout code until I figure out what +// files to put it in + +/* +Opera's find dialog: + +auto whatLabel = win.content.add(new Label("Find What")); +... + +V( whatLabel + H( findBox findButton ) + H( V(wholeWordCheck caseCheck) ~ V(upRadio downRadio) ~) + H( ~ closeButton ) +) +*/ + +enum LayoutType { + None, Table, Control, Filler, Spacer +} +enum Elasticity { + No, Semi, Yes +} +struct LayoutGroup { + LayoutType type; + LayoutGroup* parent; + LayoutGroup[] children; // used if type == LayoutType.Horiz or Vert or Table + Control control; // used if type == LayoutType.Control + int numColumns; // used if type == LayoutType.Table + int numRows() { return children.length / numColumns; } + + bool cacheActive; + private Elasticity _elasticXCache, _elasticYCache; + private Size _bestSizeCache; + private int _baselineCache; + + // spacing variables + int spacing = 8; + static LayoutGroup opCall(LayoutType type, LayoutGroup* parent) { + LayoutGroup layout; + layout.type = type; + layout.parent = parent; + layout.children.length = 3; + layout.children.length = 0; + return layout; + } + + void setCache() { + for(int i = 0; i < children.length; ++i) // can't use foreach--copies + children[i].setCache(); + _elasticXCache = _elasticX; + _elasticYCache = _elasticY; + _bestSizeCache = _bestSize; + _baselineCache = _baseline; + cacheActive = true; + } + void clearCache() { + cacheActive = false; + for(int i = 0; i < children.length; ++i) // can't use foreach--copies + children[i].clearCache(); + } + Elasticity elasticX() { return cacheActive ? _elasticXCache : _elasticX; } + Elasticity elasticY() { return cacheActive ? _elasticYCache : _elasticY; } + Size bestSize() { return cacheActive ? _bestSizeCache : _bestSize; } + int baseline() { return cacheActive ? _baselineCache : _baseline; } + + //{{{ _elasticX() + private Elasticity _elasticX() { + switch(type) { + case LayoutType.Control: + return control.elasticX ? Elasticity.Yes : Elasticity.No; + case LayoutType.Table: + auto e = Elasticity.No; + foreach(layout; children) { + if(layout.elasticX > e) + e = layout.elasticX; + if(e == Elasticity.Yes) + return e; + } + return e; + case LayoutType.Filler: + return Elasticity.Semi; + case LayoutType.Spacer: + return Elasticity.No; + } + } + //}}} + //{{{ _elasticY() + private Elasticity _elasticY() { + switch(type) { + case LayoutType.Control: + return control.elasticY ? Elasticity.Yes : Elasticity.No; + case LayoutType.Table: + auto e = Elasticity.No; + foreach(layout; children) { + if(layout.elasticY > e) + e = layout.elasticY; + if(e == Elasticity.Yes) + return e; + } + return e; + case LayoutType.Filler: + return Elasticity.Semi; + case LayoutType.Spacer: + return Elasticity.No; + } + } + //}}} + + //{{{ _bestSize() + private Size _bestSize() { + switch(type) { + case LayoutType.Control: + return control.bestSize; + case LayoutType.Table: + scope colsInfo = new ColRowInfo[numColumns]; + scope rowsInfo = new ColRowInfo[numRows]; + TableInfo info; + getTableSizes(colsInfo, rowsInfo, info); + return info.bestSize; + case LayoutType.Filler: + case LayoutType.Spacer: + return Size(0, 0); + } + } + //}}} + //{{{ _baseline() + private int _baseline() { + switch(type) { + case LayoutType.Control: + return control.baseline; + case LayoutType.Table: + case LayoutType.Filler: + case LayoutType.Spacer: + return 0; + } + } + //}}} + + //{{{ layout() + void layout(Rect rect) { + switch(type) { + case LayoutType.Control: + control.location = Point(rect.x, rect.y); + control.size = Size(rect.width, rect.height); + return; + case LayoutType.Table: + scope colsInfo = new ColRowInfo[numColumns]; + scope rowsInfo = new ColRowInfo[numRows]; + TableInfo info; + getTableSizes(colsInfo, rowsInfo, info); + + real extraWidth = rect.width - bestSize.width; + real extraHeight = rect.height - bestSize.height; + + void distExtra(ref real extra, ref ColRowInfo info, + ref real totalElastic, ref int semis, Elasticity e) { + if(info.elastic == Elasticity.No || extra <= 0) + return; + if(e == Elasticity.Semi && + info.elastic == Elasticity.Semi) { + auto thisExtra = extra / semis; + extra -= thisExtra; + semis--; + info.bestSize += thisExtra; + } else if(e == Elasticity.Yes && + info.elastic == Elasticity.Yes) { + auto thisExtra = extra * info.bestSize/totalElastic; + extra -= thisExtra; + totalElastic -= info.bestSize; // subtract original size + info.bestSize += thisExtra; + } + } + real y = 0; + for(int row = 0; row < numRows; ++row) { // go over each row + distExtra(extraHeight, rowsInfo[row], info.elasticHeight, info.semiRows, elasticY); + + real x = 0; + for(int col = 0; col < numColumns; ++col) { + distExtra(extraWidth, colsInfo[col], info.elasticWidth, info.semiColumns, elasticX); + + auto layout = children[row * numColumns + col]; + + Rect r = Point(x, y) + layout.bestSize; + + if(layout.baseline > 0) + r.y = r.y + rowsInfo[row].baseline - layout.baseline; + if(layout.elasticX) + r.width = colsInfo[col].bestSize; + if(layout.elasticY) + r.height = rowsInfo[row].bestSize; + + layout.layout(r + Point(rect.x, rect.y)); + + x += colsInfo[col].bestSize + + (colsInfo[col].filler ? 0 : spacing); + } + y += rowsInfo[row].bestSize + + (rowsInfo[row].filler ? 0 : spacing); + } + return; + case LayoutType.Filler: + case LayoutType.Spacer: + return; + } + } + //}}} + + struct ColRowInfo { + real bestSize = 0; // large enough to hold the largest control + Elasticity elastic = Elasticity.No; + bool filler = true; // if the entire column/row is filler + real baseline; // only applies to rows: max baseline in row + } + struct TableInfo { + // number of semi-elastic columns/rows + int semiColumns = 0; int semiRows = 0; + // the sum of fully elastic width/height, not including semi + real elasticWidth = 0, elasticHeight = 0; + Size bestSize = Size(0, 0); + } + //{{{ getTableSizes() + // Fills in the passed in array with the column and row sizes, as well + // as whether they are elastic. The passed in arrays must be the right + // sizes. They may be stack allocated. The table best size does + // including spacing, but column and row best sizes do not. + private void getTableSizes(ColRowInfo[] colsInfo, ColRowInfo[] rowsInfo, ref TableInfo info) { + assert(children.length % numColumns == 0); + assert(type == LayoutType.Table); + + assert(colsInfo.length == numColumns); + assert(rowsInfo.length == numRows); + + real max = 0, temp; + LayoutGroup* l; + + int sp = 0; + for(int col = 0; col < numColumns; ++col) { // go down each column + for(int row = 0; row < numRows; ++row) { + l = &children[row * numColumns + col]; + max = l.bestSize.width > max ? l.bestSize.width : max; + if(l.elasticX > colsInfo[col].elastic) + colsInfo[col].elastic = l.elasticX; + if(l.type != LayoutType.Filler) + colsInfo[col].filler = false; + } + colsInfo[col].bestSize = max; + if(colsInfo[col].elastic == Elasticity.Yes) + info.elasticWidth += max; + else if(colsInfo[col].elastic == Elasticity.Semi) + info.semiColumns++; + info.bestSize.width = info.bestSize.width + sp + max; + sp = (colsInfo[col].filler ? 0 : spacing); + max = 0; + } + + real maxBl = 0; + sp = 0; + for(int row = 0; row < numRows; ++row) { // go over each row + for(int col = 0; col < numColumns; ++col) { + l = &children[row * numColumns + col]; + max = l.bestSize.height > max ? l.bestSize.height : max; + maxBl = l.baseline > maxBl ? l.baseline : maxBl; + if(l.elasticY > rowsInfo[row].elastic) + rowsInfo[row].elastic = l.elasticY; + if(l.type != LayoutType.Filler) + rowsInfo[row].filler = false; + } + rowsInfo[row].bestSize = max; + rowsInfo[row].baseline = maxBl; + if(rowsInfo[row].elastic == Elasticity.Yes) + info.elasticHeight += max; + else if(rowsInfo[row].elastic == Elasticity.Semi) + info.semiRows++; + info.bestSize.height = info.bestSize.height + sp + max; + sp = (rowsInfo[row].filler ? 0 : spacing); + max = maxBl = 0; + } + } + //}}} +} + +//{{{ LayoutPanel class +class LayoutPanel : Panel { + LayoutGroup root; + LayoutGroup* current; + void startLayout(int ncolumns) { + if(current is null) { + root = LayoutGroup(LayoutType.Table, null); + root.numColumns = ncolumns; + current = &root; + return; + } + current.children.length = current.children.length+1; + current.children[$-1] = LayoutGroup(LayoutType.Table, current); + current.children[$-1].numColumns = ncolumns; + current = ¤t.children[$-1]; + } + void endLayout() { + current = current.parent; + } + override void add(Control c) { + if(current is null) + throw new Exception("Cannot add a control until a layout is started"); + current.children.length = current.children.length+1; + current.children[$-1] = LayoutGroup(LayoutType.Control, current); + current.children[$-1].control = c; + super.add(c); + } + void addFiller() { + current.children.length = current.children.length+1; + current.children[$-1] = LayoutGroup(LayoutType.Filler, current); + } + void addSpacer() { + current.children.length = current.children.length+1; + current.children[$-1] = LayoutGroup(LayoutType.Spacer, current); + } + + override Size bestSize() { + return root.bestSize + Size(root.spacing*2, root.spacing*2); + } + override bool elasticX() { return root.elasticX == Elasticity.Yes; } + override bool elasticY() { return root.elasticY == Elasticity.Yes; } + override void layout() { + //benchmarkAndWrite("layout", { + root.setCache(); + int sp = root.spacing; + root.layout(Rect(sp, sp, width-2*sp, height-2*sp)); + root.clearCache(); + //}); + } +} +//}}} + +//{{{ createLayout() etc. +/** + * Note: if you do this: + * ----- + * char[] s = createLayout("V( b1 H(b2 b3) )"); + * ----- + * Then the program will crash when compiled with the -release flag. (I am + * pretty sure it is a DMD bug, but I don't have time to make a testcase + * for a bug that does not bother me.) This will work correctly: + * ----- + * const char[] s = createLayout("V( b1 H(b2 b3) )"); + * ----- + * Because then the function is interpreted at compile time with CTFE. + */ +string createLayout(string layout) { + string code = "delegate LayoutPanel() {\n"; + code ~= "auto panel = new LayoutPanel;\n"; + assert(getToken(layout) == "H" || getToken(layout) == "V" || + getToken(layout) == "T", "layout type 'H', 'V', or 'T' expected"); + code ~= parseLayout(layout); + code ~= "return panel;\n"; + code ~= "}()"; + return code; +} + +void skipWS(ref string str) { + int i = 0; + while(" \t\n\r\v\f".contains(str[i])) + i++; + str = str[i..$]; +} +// advances to the next token and returns it +string nextToken(ref string str) { + skipWS(str); + str = str[getToken(str).length..$]; + return getToken(str); +} +// returns H or V or ( or ) or myControl +// gets the current token +string getToken(string str) { + string idChars = + "_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + + // TODO: // for line comments? + skipWS(str); + if("()~[]-".contains(str[0])) { + return str[0..1]; + } else if(idChars.contains(str[0])) { + int i = 1; + while(idChars.contains(str[i])) + i++; + return str[0..i]; + } else { + assert(0, "unknown character: " ~ str[0]); + } +} + +// {{{ copied from Phobos +char[] ctfeUintToString(uint u) { + char[uint.sizeof * 3] buffer = void; + int ndigits; + char[] result; + char[] digits = "0123456789"; + + ndigits = 0; + if (u < 10) + // Avoid storage allocation for simple stuff + result = digits[u .. u + 1]; + else + { + while (u) + { + uint c = (u % 10) + '0'; + u /= 10; + ndigits++; + buffer[buffer.length - ndigits] = cast(char)c; + } + result = new char[ndigits]; + result[] = buffer[buffer.length - ndigits .. buffer.length]; + } + return result; +} +uint ctfeStringToUint(char[] s) +{ + int length = s.length; + + if (!length) + return 0; + + uint v = 0; + + for (int i = 0; i < length; i++) + { + char c = s[i]; + if (c >= '0' && c <= '9') + { + if (v < uint.max/10 || (v == uint.max/10 && c <= '5')) + v = v * 10 + (c - '0'); + else + return 0; + } + else + return 0; + } + return v; +} +//}}} + +uint parseBody(ref string layout, ref string bcode) { + uint count = 0; + assert(nextToken(layout) == "(", "open parenthesis expected"); + while(nextToken(layout) != ")") { + if(getToken(layout) == "~") + bcode = bcode ~ "panel.addFiller();\n"; + else if(getToken(layout) == "-") + bcode = bcode ~ "panel.addSpacer();\n"; + else + bcode = bcode ~ parseLayout(layout); + count++; + } + bcode = bcode ~ "panel.endLayout();\n"; + return count; +} + +string parseLayout(ref string layout) { + string code = ""; + + if(getToken(layout) == "H") { + string bodyCode; + auto count = parseBody(layout, bodyCode); + code ~= "panel.startLayout(" ~ ctfeUintToString(count) ~ ");\n"; + code ~= bodyCode; + } else if(getToken(layout) == "V") { + code ~= "panel.startLayout(1);\n"; + parseBody(layout, code); + } else if(getToken(layout) == "T") { + assert(nextToken(layout) == "[", "open bracket expected"); + nextToken(layout); + assert("0123456789".contains(getToken(layout)[0]), + "number of table columns expected"); + uint columns = ctfeStringToUint(getToken(layout)); + code ~= "panel.startLayout(" ~ getToken(layout) ~ ");\n"; + assert(nextToken(layout) == "]", "close bracket expected"); + assert(parseBody(layout, code) % columns == 0, + "number of controls must be a multiple of number of columns"); + } else { + code ~= "panel.add(" ~ getToken(layout) ~ ");\n"; + } + + return code; +} + +//{{{ parser tests +static assert(createLayout("H()") != "not evaluatable at compile time"); +//pragma(msg, createLayout("V()")); + +static assert(createLayout("V(c1 c2)") == +`delegate LayoutPanel() { +auto panel = new LayoutPanel; +panel.startLayout(1); +panel.add(c1); +panel.add(c2); +panel.endLayout(); +return panel; +}()`); +static assert(createLayout("V(c1 ~ c2 H(c3 -) c4)") == +`delegate LayoutPanel() { +auto panel = new LayoutPanel; +panel.startLayout(1); +panel.add(c1); +panel.addFiller(); +panel.add(c2); +panel.startLayout(2); +panel.add(c3); +panel.addSpacer(); +panel.endLayout(); +panel.add(c4); +panel.endLayout(); +return panel; +}()`); +static assert(createLayout("V( c1 T[2](c2 c3) c4 )") == +`delegate LayoutPanel() { +auto panel = new LayoutPanel; +panel.startLayout(1); +panel.add(c1); +panel.startLayout(2); +panel.add(c2); +panel.add(c3); +panel.endLayout(); +panel.add(c4); +panel.endLayout(); +return panel; +}()`); +//}}} + +//}}} + +unittest { + // TODO: set to basic theme + // test a few basic layouts and verify pixel locations and sizes +} +