Mercurial > projects > mde
view mde/gui/widget/layout.d @ 136:4084f07f2c7a
Added simpler mergetag readers and writers, with unittest.
author | Diggory Hardy <diggory.hardy@gmail.com> |
---|---|
date | Sun, 01 Feb 2009 12:36:21 +0000 |
parents | 9fd705793568 |
children | 9f035cd139c6 |
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.AParentWidget; import mde.gui.exception; import mde.content.miscContent; import tango.util.container.HashMap; 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) after this() has run. *************************************************************************************************/ class GridLayoutWidget : GridWidget { /** Constructor for a grid layout widget. * * Widget uses the initialisation data: * --- * ints = [widget_type, align_flags, rows, cols] * strings = [w11, w12, ..., w1C, ..., wR1, ..., wRC] * // dimData may be: * dimData = [col1width, ..., colCwidth, row1height, ..., rowRheight] * --- * 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. * * The content parameter is passed on to all children accepting an IContent. */ this (IWidgetManager mgr, IParentWidget parent, widgetID id, WidgetData data, IContent content) { // Get grid size and check data // Check sufficient data for type, align-flags, rows, cols, and possibly row/col widths. if (data.ints.length != 4) throw new WidgetDataException (this); rows = data.ints[2]; cols = data.ints[3]; // Check: at least one sub-widget and strings's length is correct: if (rows < 1 || cols < 1 || data.strings.length != rows * cols) throw new WidgetDataException (this); super (mgr, parent, id, data); // Get all sub-widgets subWidgets.length = rows*cols; foreach (i, ref subWidget; subWidgets) { subWidget = mgr.makeWidget (this, data.strings[i], content); } initWidths = mgr.dimData (id); // may be null, tested later } // Save column/row sizes. Currently always do so. override bool saveChanges () { foreach (widget; subWidgets) // recurse on subwidgets widget.saveChanges (); mgr.dimData (id, col.width ~ row.width); return true; } protected: } /************************************************************************************************* * Iterates on an ContentList to produce a list of widgets, each of which is created with widgetID * data.strings[0]. If an IContent is passed, this is cast to a ContentList, otherwise * content.Items is used to get an IContent. It is an error if the content fails to cast to * ContentList. *************************************************************************************************/ class ContentListWidget : GridWidget { this (IWidgetManager mgr, IParentWidget parent, widgetID id, WidgetData data, IContent content) { cList = cast(IContentList) content; WDCCheck (data, 2, 1, cList); cols = 1; rows = cList.list.length; subWidgets.length = rows; if (data.ints[1] & 8) { // orient horizontally cols = rows; rows = 1; } super (mgr, parent, id, data); if (subWidgets) { // i.e. rows*cols > 0 foreach (i, c; cList.list) { subWidgets[i] = mgr.makeWidget (this, data.strings[0], c); } } else { rows = cols = 1; subWidgets = [mgr.makeWidget (this, data.strings[0], new ErrorContent ("<empty list>",null)) ]; } } override void recursionCheck (widgetID wID, IContent c) { if (wID is id && c is cList) throw new WidgetRecursionException (wID); parent.recursionCheck (wID, c); } override bool saveChanges () { // Since all sub-widgets have the same id, it only makes sense to call on one if (subWidgets is null) return false; return subWidgets[0].saveChanges; } private: IContentList cList; } /************************************************************************************************* * 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 saveChanges() (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 optionally has spacing between widgets. * * Several flags are tested against ints[1]: * $(TABLE * $(TR $(TD 1) $(TD Column alignment is shared against other instances of thes widget id)) * $(TR $(TD 2) $(TD Row alignment is shared against other instances of thes widget id)) * $(TR $(TD 4) $(TD Spacing is inserted between elements; the renderer may draw this)) * $(TR $(TD 8) $(TD For ContentListWidget only, list is horizontal instead of vertical)) * ) *************************************************************************************************/ // Note: mw, mh inherited from AWidget are not used; use col.mw, row.mw instead. abstract class GridWidget : AParentWidget { //BEGIN Creation & saving /** Partial constructor for a grid layout widget. * * Deriving classes should check data lengths, and set rows and cols * before calling this super constructor. * * Derived constructors may also set initWidths to the array of column widths followed by * row heights used to initially set the row/column dimensions. */ protected this (IWidgetManager mgr, IParentWidget parent, widgetID id, WidgetData data) { super (mgr, parent, id); // Create cell aligners with appropriate col/row adjustment function if (data.ints[1] & 1) col = AlignColumns.getInstance (id, cols); else col = (new AlignColumns (cols)); if (data.ints[1] & 2) row = AlignColumns.getInstance (id~"R", rows); // id must be unique to that for cols! else row = (new AlignColumns (rows)); AlignColumns.CallbackStruct cbS; cbS.setWidth = &setColWidth; cbS.sADD = &setupAlignDimData; cbS.newMW = &colNewMW; col.cbs ~= cbS; cbS.setWidth = &setRowHeight; cbS.newMW = &rowNewMW; row.cbs ~= cbS; useSpacing = (data.ints[1] & 4) != 0; } /** Responsible for calculating the minimal size and initializing some stuff. * * As such, this must be the first function called after this(). */ override bool setup (uint n, uint flags) { debug (mdeWidgets) logger.trace ("GridWidget.setup"); // Run all internal calculations regardless of changes, then check dimensions for changes. // Don't try shortcutting internal calculations when there are no changes - I've tried, and // doing so adds enough overhead to make doing so almost(?) worthless (or at least large // increases in complexity). wdim ow = w, oh = h; col.setup (n, flags); row.setup (n, flags); if (initWidths.length == cols + rows) { col.setWidths (initWidths[0..cols]); row.setWidths (initWidths[cols..$]); } else { col.setWidths; row.setWidths; } initWidths = null; // free w = col.w; h = row.w; // 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); } return (ow != w || oh != h); } //END Creation & saving //BEGIN Size & position override bool isWSizable () { return col.firstSizable >= 0; } override bool isHSizable () { return row.firstSizable >= 0; } // mw, mh not used override wdim minWidth () { return col.mw; } override wdim minHeight () { return row.mw; } override void setWidth (wdim nw, int dir) { w = col.resizeWidth (nw, dir); // Note: setPosition must be called after! } override void setHeight (wdim nh, int dir) { h = row.resizeWidth (nh, dir); // Note: setPosition must be called after! } override void setPosition (wdim x, wdim y) { this.x = x; this.y = y; debug assert (col.pos && row.pos, "setPosition: col/row.pos not set (code error)"); foreach (i,widget; subWidgets) widget.setPosition (x + col.pos[i % cols], y + row.pos[i / cols]); } // Unlike for most widgets, these actually resize self and sub-widgets, since the parent // simply calling setWidth/setHeight wouldn't work. override void minWChange (IChildWidget widg, wdim nmw) { size_t i = getWidgetIndex(widg); col.newMinWidth (i%cols, i/cols + colR, nmw); // callbacks to all sharing layouts do the rest } override void minHChange (IChildWidget widg, wdim nmh) { size_t i = getWidgetIndex(widg); row.newMinWidth (i/cols, i%cols + rowR, nmh); } //END Size & position // Find the relevant widget. override IChildWidget getWidget (wdim cx, wdim cy) { debug scope (failure) logger.warn ("getWidget: failure; values: click; pos; width: {},{}; {},{}; {},{}", cx, cy, x, y, w, h); debug assert (cx >= x && cx < x + w && cy >= y && cy < y + h, "getWidget: not on widget (code error)"); // Find row/column: ptrdiff_t i = col.getCell (cx - x); ptrdiff_t 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 override int 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.findResizeCols (cx - x) && row.findResizeCols (cy - y)) return 0; // unable to resize dragX = cx; dragY = cy; mgr.addClickCallback (&endCallback); mgr.addMotionCallback (&resizeCallback); } return 0; } override void draw () { super.draw (); foreach (widget; subWidgets) widget.draw (); if (useSpacing) mgr.renderer.drawSpacers (x,y, w,h, col.pos[1..$], row.pos[1..$]); } package: /* Calculations which need to be run whenever a new sub-widget structure is set * or other changes affecting widget sizes. Most of these need to happen regardless of whether * changes have occurred, since AlignColumns have been reset. * * rows, cols and subWidgets must be set before calling. Part of the set-up for AlignColumns * (col and row). subWidgets need to know their minimal size and resizability. */ override void setupAlignDimData (uint n, uint flags) { if (sADD_n == n) return; // cached data is current sADD_n = n; debug (mdeWidgets) logger.trace ("GridWidget: setup on subWidgets..."); foreach (widg; subWidgets) { // make sure all subwidgets have been set up debug assert (widg, "null widg"); widg.setup (n,flags); } debug (mdeWidgets) logger.trace ("GridWidget: setup on subWidgets...done"); // make sure both AlignColumns are set up (since first call to setup(n) calls reset): col.setup (n, flags); row.setup (n, flags); // Note: shared AlignColumns get this set by all sharing GridWidgets col.spacing = row.spacing = useSpacing ? mgr.renderer.layoutSpacing : 0; // Calculate the minimal column and row sizes: if (colR == size_t.max) colR = col.addRows (rows); if (rowR == size_t.max) rowR = row.addRows (cols); // AlignColumns (row, col) takes care of initializing minWidth. for (size_t r = 0; r < rows; ++r) { for (size_t c = 0; c < cols; ++c) { size_t i = r*cols + c; col.minCellWidths[i+colR*cols] = subWidgets[i].minWidth; row.minCellWidths[(c+rowR)*rows+r] = subWidgets[i].minHeight; } } // Find which cols/rows are resizable: // AlignColumns initializes sizable, and sets first and last sizables. static if (!(SIZABILITY & SIZABILITY_ENUM.SUBWIDGETS)) return; forCols: for (size_t i = 0; i < cols; ++i) { // for each column for (size_t j = 0; j < subWidgets.length; j += cols) { // for each row static if (SIZABILITY == SIZABILITY_ENUM.ALL_SUBWIDGETS) { if (!subWidgets[i+j].isWSizable) { // column not resizable col.sizable[i] = false; continue forCols; // no point checking more } } else { if (subWidgets[i+j].isWSizable) { // column is resizable col.sizable[i] = true; continue forCols; } } } } forRows: for (size_t i = 0; i < subWidgets.length; i += cols) { // for each row for (size_t j = 0; j < cols; ++j) { // for each column static if (SIZABILITY == SIZABILITY_ENUM.ALL_SUBWIDGETS) { if (!subWidgets[i+j].isHSizable) { row.sizable[i / cols] = false; continue forRows; } } else { if (subWidgets[i+j].isHSizable) { row.sizable[i / cols] = true; continue forRows; } } } } } private: override void setColWidth (size_t i, wdim w, int dir) { for (size_t j = 0; j < rows; ++j) { subWidgets[i + cols*j].setWidth (w, dir); } } override void setRowHeight (size_t j, wdim h, int dir) { for (size_t i = 0; i < cols; ++i) { subWidgets[i + cols*j].setHeight (h, dir); } } void colNewMW (bool mwChange) { if (mwChange) { w = col.w; parent.minWChange (this, col.mw); } else { // don't propegate call to parent setPosition (x,y); mgr.requestRedraw; } } void rowNewMW (bool mwChange) { if (mwChange) { h = row.w; parent.minHChange (this, row.mw); } else { // don't propegate call to parent setPosition (x,y); mgr.requestRedraw; } } //BEGIN Col/row resizing callback override void resizeCallback (wdim cx, wdim cy) { col.resizeCols (cx - dragX); row.resizeCols (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]); mgr.requestRedraw; } override bool endCallback (wdabs cx, wdabs cy, ubyte b, bool state) { if (b == 1 && state == false) { mgr.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 size_t cols, rows; // number of cells in grid wdim[] initWidths; // see this / setInitialSize uint sADD_n = uint.max; // param n of last setup call after setupAlignDimData has run bool useSpacing; // add inter-row/col spacing? /* All widgets in the grid, by row. Order: [ 0 1 ] * [ 2 3 ] */ //IChildWidget[] subWidgets; (inherited from AParentWidget) AlignColumns col, row; // aligners for cols and rows // "rows" allocated in col and row; return value of *.addRows(): size_t colR = size_t.max, rowR = size_t.max; } /************************************************************************************************** * Alignment device * * E.g. can control widths of columns within a grid, and provide sensible resizing, respecting the * minimal width required by each cell in a column. Is not restricted to horizontal widths, but to * ease descriptions, a horizontal context (column widths) is assumed. * * Cells should be of type IChildWidget. * * Cells are not directly interacted with, but minimal widths for each column are passed, and * callback functions are used to adjust the width of any column. *************************************************************************************************/ package class AlignColumns { /** Instance returned will be shared with any other widgets of same widgetID. * * Also ensures each widget sharing an instance expects the same number of columns. */ static AlignColumns getInstance (widgetID id, size_t columns) { AlignColumns* p = id in instances; if (p) { if (p.cols != columns) throw new GuiException ("AlignColumns: no. of columns varies between sharing widgets (code error)"); //logger.trace ("Shared alignment for: "~id); return *p; } else { auto a = new AlignColumns (columns); instances[id] = a; return a; } } /** Create an instance. After creation, the number of columns can only be changed by calling * reset. * * After creation, minimal widths should be set for all columns (minWidth) and * setWidths must be called before other functions are used. */ this (size_t columns) { if (columns < 1) throw new GuiException("AlignColumns: created with <1 column (code error)"); minWidth.length = columns; sizable.length = columns; static if (SIZABILITY & SIZABILITY_ENUM.START_TRUE) sizable[] = true; cols = columns; } /** Like IChildWidget's setup; calls sADD delegates. */ void setup (uint n, uint flags) { if (n != setup_n) { setup_n = n; setupWidths = false; reset (cols); foreach (cb; cbs) cb.sADD (n, flags); // set flag 1 } } /** Reset all column information (only keep set callbacks). * * Widths should be set after calling, as on creation. */ void reset (size_t columns) { assert (columns == cols, "no support for changing number of columns for now"); minWidth[] = 0; static if (SIZABILITY & SIZABILITY_ENUM.START_TRUE) sizable[] = true; else sizable[] = false; firstSizable = -1; lastSizable = -1; } /** Add num "rows" to the aligner. They start at the returned index r, thus the values in * minCellWidths to set are minCellWidths[cols*r..cols*(r+num)]. * * Calling this function is necessary to allocate room in minCellWidths. */ size_t addRows (size_t num) { size_t r = rows; rows += num; minCellWidths.length = cols*rows; return r; } /** Initialize widths, either from minWidths or from supplied list, checking validity. * * Also calculates first/lastSizable from sizable, overall minimal width and column positions. */ void setWidths (wdim[] data = null) { if (!setupWidths) { setupWidths = true; // Set minWidth assert (minCellWidths.length == rows * cols, "minCellWidths: bad length"); for (size_t c = 0; c < cols; ++c) for (size_t r = 0; r < rows; ++r) { wdim mcw = minCellWidths[c+r*cols]; if (minWidth[c] < mcw) minWidth[c] = mcw; } /* Calculate the minimal width of all columns plus spacing. */ mw = spacing * cast(wdim)(cols - 1); foreach (imw; minWidth) mw += imw; // set width if (data || width) { // use existing/external data: need to check validity if (data) { assert (data.length == cols, "setWidths called with bad data length (code error)"); width = data.dup; // data is shared by other widgets with same id so must be .dup'ed } foreach (i, m; minWidth) { if (!sizable[i] || width[i] < m) // if width is fixed or less than minimum width[i] = m; } } else width = minWidth.dup; genPositions; foreach (i,s; sizable) { if (s) { firstSizable = i; goto gotFirst; } } return; // none resizable - don't search for lastSizable gotFirst: foreach_reverse (i,s; sizable) { if (s) { lastSizable = i; return; // done } } } } /** Get the row/column of relative position l. * * returns: * -i if in space to left of col i, or i if on col i. */ ptrdiff_t getCell (wdim l) { debug assert (width, "AlignColumns not initialized when getCell called (code error)"); ptrdiff_t i = cols - 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 debug assert (i+1 < cols, "getCell: l >= total width (code error)"); return -i - 1; // note: i might be 0 so cannot just return -i } return i; } /** Adjust total size with direction dir. * * nw should be at least the minimal width. */ wdim resizeWidth (wdim nw, int dir) { debug assert (width, "AlignColumns not initialized when resizeWidth called (code error)"); if (nw < mw) { debug logger.warn ("Widget dimension set below minimal"); nw = mw; } if (nw == w) return w; wdim diff = nw - w; if (firstSizable == -1) diff = adjustCellSizes (diff, cols-1, -1); else diff = adjustCellSizes (diff, (dir == -1 ? lastSizable : firstSizable), dir); genPositions; debug if (nw != w) { logger.trace ("resizeWidth on {} to {} failed, new width: {}, diff {}, firstSizable {}, columns {}",cast(void*)this, nw,w, diff, firstSizable, cols); /+ Also print column widths & positions: logger.trace ("resizeWidth to {} failed! Column dimensions and positions:",nw); foreach (i,w; width) logger.trace ("\t{}\t{}", w,pos[i]);+/ } return w; } /** Calculate resizeU/resizeD, and return true if unable to resize. * * This and resizeCols are for moving dividers between cells. */ bool findResizeCols (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 >= cols) { // 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 } /// Resize columns based on findResizeCols void resizeCols (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); } genPositions; } /** Called when one of the cells in column col now has minimal width nmw. * * Enlarges column minimal width if necessary; tries to keep total width the same. */ void newMinWidth (size_t col, size_t row, wdim nmw) { minCellWidths[col + row*cols] = nmw; wdim nd = 0; // negative diff to keep overall size constant if possible wdim omw = minWidth[col]; // to check if mw actually changes if (minWidth[col] < nmw) { // increase minimal minWidth[col] = nmw; nd = width[col] - nmw; // negative diff } else if (minWidth[col] > nmw) { // potentially decrease minimal nmw = 0; for (size_t r = 0; r < rows; ++r) { wdim mcw = minCellWidths[col+r*cols]; if (nmw < mcw) nmw = mcw; } minWidth[col] = nmw; if (!sizable[col]) nd = width[col] - nmw; } else return false; mw = spacing * cast(wdim)(cols - 1); foreach (imw; minWidth) mw += imw; if (nd != 0) { // needs enlarging or shrinking width[col] = nmw; foreach (cb; cbs) cb.setWidth (col, nmw, -1); if (lastSizable >= 0) adjustCellSizes (nd, lastSizable, -1); // doesn't necessarily resize exactly genPositions; } bool mwChange = nmw != omw; // size only changes if true, presuming old size is valid foreach (cb; cbs) cb.newMW (mwChange); } /* Generate position infomation for each column and set w. */ private void genPositions () { pos.length = cols; w = 0; foreach (i, cw; width) { pos[i] = w; w += cw + spacing; } w -= spacing; } /* 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; assumed to be sizable * 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. * * Doesn't touch non-sizable columns (except start which is only assumed sizable). * * 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. */ private wdim adjustCellSizes (wdim diff, ptrdiff_t start, int incr) in { assert (width.length == cols, "CellAlign.adjustCellSizes: width is invalid (code error)"); // Most likely if passed negative when sizing is disabled: assert (start >= 0 && start < cols, "adjustCellSizes: invalid start"); debug assert (incr == 1 || incr == -1, "adjustCellSizes: invalid incr"); } body { debug scope(failure) logger.trace ("adjustCellSizes: failure"); ptrdiff_t i = start; if (diff > 0) { // increase size of first resizable cell width[i] += diff; foreach (cb; cbs) cb.setWidth (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 foreach (cb; cbs) cb.setWidth (i, width[i], incr); break; // we hit the mark exactly: diff is correct } // else we decreased it too much! width[i] = minWidth[i]; foreach (cb; cbs) cb.setWidth (i, width[i], incr); // rd is remainder to decrease by do { i += incr; if (i < 0 || i >= cols) { // run out of next cells diff -= rd; // still had rd left to decrease break aCSwhile; // exception: Array index out of bounds } } while (!sizable[i]) // iterate again if row/col isn't resizable } } // else no adjustment needed (diff == 0) return diff; } /** Minimal widths per cell. * * Array of all cells, organised like GridLayoutWidget.subWidgets when representing columns, * with rows and columns swapped when representing rows. * * Then minWidth[i] = min(minCellWidths[i]) (where min acts on an array). */ wdim[] minCellWidths; /** For each column i, sizable[i] is true if that column is resizable. * * Set along with minWidth before calling setWidths. */ bool[] sizable; // set by genCachedConstructionData /** Current width, relative position (for each column) * * Treat as READ ONLY outside this class! */ wdim[] width; // only adjusted within the class wdim[] pos; /// ditto wdim spacing; // used by genPositions (which cannot access the layout class's data) wdim w,mw; // current & minimal widths package struct CallbackStruct { void delegate (size_t,wdim,int) setWidth; // set width of a column, with resize direction void delegate (uint,uint) sADD; // setupAlignDimData dlgs void delegate (bool) newMW; // propegate or finalize minimal width change } CallbackStruct cbs[]; protected: /* Minimal width for each column. * * Set by setWidths. */ wdim[] minWidth; size_t cols, rows; // number of columns and rows (wrong way round when AlignColumns // represents rows) ptrdiff_t resizeD, // resizeCols works down from this index (<0 if not resizing) resizeU; // and up from this index /* indicies of the first/last resizable column (negative if none are resizable). */ ptrdiff_t firstSizable = -1, lastSizable = -1; // set by calcFLSbl // Callbacks used to actually adjust a column's width: uint setup_n = uint.max; // param n of last setup call bool setupWidths; // setWidths has been run static HashMap!(widgetID,AlignColumns) instances; static this () { instances = new HashMap!(widgetID,AlignColumns); } alias IParentWidget.SIZABILITY SIZABILITY; alias IParentWidget.SIZABILITY_ENUM SIZABILITY_ENUM; debug invariant() { if (setupWidths) { assert (width.length == cols, "invariant: bad width length"); wdim x = 0; foreach (i,w; width) { assert (minWidth[i] <= w, "invariant: min size not reached"); // even when "not sizable", cols may get enlarged assert (x == pos[i], "invariant: position wrong"); x += w + spacing; } assert (x - spacing == w, "invariant: w is wrong"); x = spacing * cast(wdim)(cols - 1); foreach (mw; minWidth) x += mw; assert (x == mw, "invariant: mw is wrong"); } } debug (mdeUnitTest) unittest { bool throws (void delegate() dg) { bool r = false; try { dg(); } catch (Exception e) { r = true; } return r; } AlignColumns a, a2, b; a = getInstance ("a", 2); a2 = getInstance ("a", 2); b = getInstance ("b", 5); assert (a is a2); assert (a !is b); assert (throws ({ getInstance ("a", 4); })); a.setup (0, 3); a.spacing = 6; a.minWidth[0] = 50; a.minWidth[1] = 6; a.sizable[1] = true; a.setWidths; assert (a.w == 62); b.setup (0,3); b.spacing = 2; foreach (ref wd; b.minWidth) wd = 10; b.sizable = [false, true, false, true, false]; b.setWidths; assert (b.w == 58); assert (b.resizeWidth (60, -1) == 60); assert (b.width[3] == 12); logger.info ("Unittest complete."); } }