Mercurial > projects > mde
view mde/gui/widget/layout.d @ 71:77c7d3235114
Separated the grid layout widget's implementation into a base and a derived class, to allow other uses of layout.
author | Diggory Hardy <diggory.hardy@gmail.com> |
---|---|
date | Sat, 05 Jul 2008 15:36:39 +0100 |
parents | f54ae4fc2b2f |
children | 159775502bb4 |
line wrap: on
line source
/* LICENSE BLOCK Part of mde: a Modular D game-oriented Engine Copyright © 2007-2008 Diggory Hardy This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ /// Gui layout widgets. module mde.gui.widget.layout; import mde.gui.widget.Widget; import mde.gui.exception; debug { import tango.util.log.Log : Log, Logger; private Logger logger; static this () { logger = Log.getLogger ("mde.gui.widget.layout"); } } /************************************************************************************************* * Encapsulates a grid of Widgets. * * Currently there is no support for changing number of cells, sub-widgets or sub-widget properties * (namely isW/HSizable and minimal size) after this() has run. * * Since a grid with either dimension zero is not useful, there must be at least one sub-widget. * * The grid has no border but has spacing between widgets. *************************************************************************************************/ class GridLayoutWidget : GridWidget { /** Constructor for a grid layout widget. * * Widget uses the initialisation data: * [widgetID, r, c, w11, w12, ..., w1c, ..., wr1, ..., wrc] * where r and c are the number of rows and columns, and wij is the ID (from parent Window's * list) for the widget in row i and column j. The number of parameters must be r*c + 3. */ this (IWindow wind, int[] data) { // Get grid size and check data // Check sufficient data for rows, cols, and at least one widget: if (data.length < 4) throw new WidgetDataException; rows = data[1]; cols = data[2]; if (data.length != 3 + rows * cols) throw new WidgetDataException; /* data.length >= 4 so besides checking the length is correct, this tells us: * rows * cols >= 4 - 3 = 1 a free check! * The only thing not checked is whether both rows and cols are negative, which would * cause an exception when dynamic arrays are allocated by genCachedConstructionData, which * is an acceptible method of failure (and is unlikely anyway). */ // Get all sub-widgets subWidgets.length = rows*cols; foreach (i, ref subWidget; subWidgets) { subWidget = wind.makeWidget (data[i+3]); } super (wind, data); } /** Return construction data to recreate this GridLayoutWidget. */ int[] getCreationData () { int[] ret; ret.length = 3 + subWidgets.length; ret [0..3] = [widgetType, rows, cols]; // first data foreach (i,widget; subWidgets) // sub widgets ret[i+3] = window.addCreationData (widget); return ret; } } /************************************************************************************************* * Trial layout of sub-widgets of one type only. *************************************************************************************************/ class TrialLayout : GridWidget { this (IWindow wind, int[] data) { assert (false, "Not ready"); if (data.length != 6) throw new WidgetDataException; super (wind, data); rows = data[1]; cols = data[2]; // Get all sub-widgets subWidgets.length = rows*cols; foreach (i, ref subWidget; subWidgets) { //subWidget = new ContentWidget (data[3..6]); } } } /************************************************************************************************* * Backend for grid-based (includes column/row) layout widgets. * * A deriving class must at least do some work in it's constructor (see Ddoc for this() below) * and provide an implementation of getCreationData() (unless Widget's version is sufficient). * * Since a grid with either dimension zero is not useful, there must be at least one sub-widget. * * The grid has no border but has spacing between widgets. *************************************************************************************************/ class GridWidget : Widget { //BEGIN Creation & saving /** Partial constructor for a grid layout widget. * * Deriving classes should check data length, and set rows, cols, and the subWidgets array, * before calling this super constructor. (If it's necessary to call super(...) first, * the call to genCachedConstructionData can be moved to the derived this() methods.) */ protected this (IWindow wind, int[] data) { super (wind, data); // Needn't be set before genCachedConstructionData is called: col.setColWidth = &setColWidth; row.setColWidth = &setRowHeight; // Calculate cached construction data genCachedConstructionData; } /** This implementation of adjust() does two things: * 1. Pass adjust data on to sub-widgets * 2. Set the size, from the adjust data if possible * * Can be overridden (probably along with getMutableData()) if a different implementation is * wanted. adjustCache() may still be useful. */ int[] adjust (int[] data) { // Give all sub-widgets their data: foreach (widget; subWidgets) data = widget.adjust (data); /** We basically short-cut setSize by loading previous col/row sizes and doing the final * calculations. * Note: if setSize gets called afterwards, it should have same dimensions and so not do * anything. */ int lenUsed = 0; if (data.length < rows + cols) { // data error; use defaults col.dupMin; row.dupMin; } else { // sufficient data lenUsed = rows+cols; col.setCheck (cast(wdim[])data[0..cols]); row.setCheck (cast(wdim[])data[cols..lenUsed]); } adjustCache(); return data[lenUsed..$]; } /** Generates cached mutable data. * * Should be called by adjust() after setting col and row widths (currently via dupMin or * setCheck). */ void adjustCache () { // Generate cached mutable data // Calculate column and row locations: w = col.genPositions; h = row.genPositions; // Tell subwidgets their new sizes. Positions are given by a later call to setPosition. foreach (i,widget; subWidgets) { // Resizing direction is arbitrarily set to negative: widget.setWidth (col.width[i % cols], -1); widget.setHeight (row.width[i / cols], -1); } } /** Returns sub-widget mutable data along with column widths and row heights, as used by * adjust(). */ int[] getMutableData () { int[] ret; foreach (widget; subWidgets) ret ~= widget.getMutableData; return ret ~ cast(int[])col.width ~ cast(int[])row.width; } //END Creation & saving //BEGIN Size & position bool isWSizable () { return col.firstSizable >= 0; } bool isHSizable () { return row.firstSizable >= 0; } void setWidth (wdim nw, int dir) { if (nw == w) return; w += col.adjustCellSizes (nw - w, (dir == -1 ? col.lastSizable : col.firstSizable), dir); // Note: setPosition must be called after! } void setHeight (wdim nh, int dir) { if (nh == h) return; h += row.adjustCellSizes (nh - h, (dir == -1 ? row.lastSizable : row.firstSizable), dir); // Note: setPosition must be called after! } void setPosition (wdim x, wdim y) { this.x = x; this.y = y; foreach (i,widget; subWidgets) widget.setPosition (x + col.pos[i % cols], y + row.pos[i / cols]); } //END Size & position // Find the relevant widget. IWidget getWidget (wdim cx, wdim cy) { debug scope (failure) logger.warn ("getWidget: failure"); // Find row/column: myDiff i = col.getCell (cx - x); myDiff j = row.getCell (cy - y); if (i < 0 || j < 0) // on a space between widgets return this; // On a subwidget; recurse call: return subWidgets[i + j*cols].getWidget (cx, cy); } // Resizing columns & rows void clickEvent (wdabs cx, wdabs cy, ubyte b, bool state) { debug scope (failure) logger.warn ("clickEvent: failure"); if (b == 1 && state == true) { /* Note: Because of getWidget, this function is only called if the click is not on a * sub-widget, so we know it's on some divisor (so at least one of resizeCol and * resizeRow is non-negative). */ // find col/row's resizeD & resizeU if (col.findResize (cx - x) && row.findResize (cy - y)) return; // unable to resize dragX = cx; dragY = cy; window.gui.addClickCallback (&endCallback); window.gui.addMotionCallback (&resizeCallback); } } void draw () { super.draw (); foreach (widget; subWidgets) widget.draw (); } private: //BEGIN Cache calculation functions /* Calculations which need to be run whenever a new sub-widget structure is set * (i.e. to produce cached data calculated from construction data). * Also need to be re-run if the renderer changes. * * rows, cols and subWidgets must be set before calling. */ void genCachedConstructionData () { // Will only change if renderer changes: col.spacing = row.spacing = window.renderer.layoutSpacing; // Calculate the minimal column and row sizes: // set length, making sure the arrays are initialised to zero: col.minWidth = new wdim[cols]; row.minWidth = new wdim[rows]; foreach (i,widget; subWidgets) { // Increase dimensions if current minimal size is larger: myIt n = i % cols; // column wdim md = widget.minWidth; if (col.minWidth[n] < md) col.minWidth[n] = md; n = i / cols; // row md = widget.minHeight; if (row.minWidth[n] < md) row.minWidth[n] = md; } // Calculate the overall minimal size, starting with the spacing: mh = window.renderer.layoutSpacing; // use mh temporarily mw = mh * cast(wdim)(cols - 1); mh *= cast(wdim)(rows - 1); foreach (x; col.minWidth) // add the column/row's dimensions mw += x; foreach (x; row.minWidth) mh += x; // Find which cols/rows are resizable: // reset: col.sizable = new bool[cols]; row.sizable = new bool[rows]; col.firstSizable = row.firstSizable = -1; forCols: for (myIt i = 0; i < cols; ++i) { // for each column for (myIt j = 0; j < subWidgets.length; j += cols) // for each row if (!subWidgets[i+j].isWSizable) // column not resizable continue forCols; // continue the outer for loop // column is resizable if we get to here col.sizable[i] = true; if (col.firstSizable < 0) col.firstSizable = i; col.lastSizable = i; } forRows: for (myIt i = 0; i < subWidgets.length; i += cols) { // for each row for (myIt j = 0; j < cols; ++j) // for each column if (!subWidgets[i+j].isHSizable) continue forRows; row.lastSizable = i / cols; row.sizable[row.lastSizable] = true; if (row.firstSizable < 0) row.firstSizable = row.lastSizable; } } //END Cache calculation functions void setColWidth (myIt i, wdim w, int dir) { for (myIt j = 0; j < rows; ++j) { subWidgets[i + cols*j].setWidth (w, dir); } } void setRowHeight (myIt j, wdim h, int dir) { for (myIt i = 0; i < cols; ++i) { subWidgets[i + cols*j].setHeight (h, dir); } } //BEGIN Col/row resizing callback void resizeCallback (wdim cx, wdim cy) { col.resize (cx - dragX); row.resize (cy - dragY); // NOTE: all adjustments are relative; might be better if they were absolute? dragX = cx; dragY = cy; foreach (i,widget; subWidgets) widget.setPosition (x + col.pos[i % cols], y + row.pos[i / cols]); window.requestRedraw; } bool endCallback (wdabs cx, wdabs cy, ubyte b, bool state) { if (b == 1 && state == false) { window.gui.removeCallbacks (cast(void*) this); return true; // we've handled the up-click } return false; // we haven't handled it } protected: // Data for resizing cols/rows: wdim dragX, dragY; // coords where drag starts //END Col/row resizing callback myIt cols, rows; // number of cells in grid /* All widgets in the grid, by row. Order: [ 0 1 ] * [ 2 3 ] */ IWidget[] subWidgets; /* Widths, positions, etc., either of columns or of rows * * The purpose of this struct is mostly to unify functionality which must work the same on both * horizontal and vertical cell placement. * * Most notation corresponds to horizontal layout (columns), simply for easy of naming. */ struct CellDimensions { wdim[] pos, // relative position (cumulative width[i-1] plus spacing) width, // current widths minWidth; // minimal widths (set by genCachedConstructionData) bool[] sizable; // true if col is resizable (set by genCachedConstructionData) myDiff firstSizable, // first col which is resizable, negative if none lastSizable; // as above, but last (set by genCachedConstructionData) myDiff resizeD, // resize down from this index (<0 if not resizing) resizeU; // and up from this index wdim spacing; // used by genPositions (which cannot access the layout class's data) /* This is a delegate to a enclosing class's function, since: * a different implementation is needed for cols or rows * we're unable to access enclosing class members directly */ void delegate (myIt,wdim,int) setColWidth; // set width of a column, with resize direction void dupMin () { width = minWidth.dup; } void setCheck (wdim[] data) { // Set to provided data: width = data; // And check sizes are valid: foreach (i, m; minWidth) // if fixed width or width is less than minimum: if (!sizable[i] || width[i] < m) width[i] = m; } // Generate position infomation and return total width (i.e. widget width/height) wdim genPositions () { pos.length = minWidth.length; wdim x = 0; foreach (i, w; width) { pos[i] = x; x += w + spacing; } return x - spacing; } // Get the row/column a click occured in // Returns -i if in space to left of col i, or i if on col i myDiff getCell (wdim l) { myDiff i = minWidth.length - 1; // starting from right... while (l < pos[i]) { // decrement while left of this column debug assert (i > 0, "getCell: l < pos[0] (code error)"); --i; } // now (l >= pos[i]) if (l >= pos[i] + width[i]) // between columns return -i - 1; // note: i might be 0 so cannot just return -i return i; } // Calculate resizeU/resizeD, and return true if unable to resize. bool findResize (wdim l) { resizeU = -getCell (l); // potential start for upward-resizes if (resizeU <= 0) return true; // not on a space between cells resizeD = resizeU - 1; // potential start for downward-resizes while (!sizable[resizeU]) { // find first actually resizable column (upwards) ++resizeU; if (resizeU >= minWidth.length) { // cannot resize resizeU = -1; return true; } } while (!sizable[resizeD]) { // find first actually resizable column (downwards) --resizeD; if (resizeD < 0) { // cannot resize resizeU = -1; // resizeU is tested to check whether resizes are possible return true; } } return false; // can resize } /* Adjust the total size of rows/columns (including spacing) by diff. * * Params: * diff = amount to increase/decrease the total size * start= index for col/row to start resizing on * incr = direction to resize in (added to index each step). Must be either -1 or +1. * * Returns: * The amount adjusted. This may be larger than diff, since cellD is clamped by cellDMin. * * Note: Check variable used for start is valid before calling! If a non-sizable column's * index is passed, this should get increased (if diff > 0) but not decreased. */ wdim adjustCellSizes (wdim diff, myDiff start, int incr) in { // Could occur if adjust isn't called first, but this would be a code error: assert (width !is null, "adjustCellSizes: width is null"); // Most likely if passed negative when sizing is disabled: assert (start >= 0 && start < minWidth.length, "adjustCellSizes: invalid start"); assert (incr == 1 || incr == -1, "adjustCellSizes: invalid incr"); assert (setColWidth !is null, "adjustCellSizes: setColWidth is null"); } body { debug scope(failure) logger.trace ("adjustCellSizes: failure"); myDiff i = start; if (diff > 0) { // increase size of first resizable cell width[i] += diff; setColWidth (i, width[i], incr); } else if (diff < 0) { // decrease wdim rd = diff; // running diff aCSwhile: while (true) { width[i] += rd; // decrease this cell's size (but may be too much) rd = width[i] - minWidth[i]; if (rd >= 0) { // OK; we're done setColWidth (i, width[i], incr); // set new width break; // we hit the mark exactly: diff is correct } // else we decreased it too much! width[i] = minWidth[i]; setColWidth (i, width[i], incr); // rd is remainder to decrease by bool it = true; // iterate (force first time) while (it) { i += incr; if (i < 0 || i >= minWidth.length) { // run out of next cells diff -= rd; // still had rd left to decrease break aCSwhile; // exception: Array index out of bounds } it = !sizable[i]; // iterate again if row/col isn't resizable } } } // else no adjustment needed (diff == 0) genPositions; return diff; } void resize (wdim diff) { if (resizeU <= 0) return; // do shrinking first (in case we hit the minimum) if (diff >= 0) { diff = -adjustCellSizes (-diff, resizeU, 1); adjustCellSizes (diff, resizeD, -1); } else { diff = -adjustCellSizes (diff, resizeD, -1); adjustCellSizes (diff, resizeU, 1); } } } CellDimensions col, row; // Index types. Note that in some cases they need to hold negative values. // int is used for resizing direction (although ptrdiff_t would be more appropriate), // since the value must always be -1 or +1 and int is smaller on X86_64. alias size_t myIt; alias ptrdiff_t myDiff; }