Mercurial > projects > mde
view mde/gui/widget/layout.d @ 179:1f9d00f392bd default tip
Fixed a bug where (non-resizible) widgets wouldn't get shrunk when minimal size decreases, meaning optional context menus are hiden properly now.
Optimised when ServiceContentList.opCall is called, I think without breaking anything.
author | Diggory Hardy <diggory.hardy@gmail.com> |
---|---|
date | Tue, 15 Sep 2009 20:09:59 +0200 |
parents | af40e9679436 |
children |
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.Content; import tango.util.container.HashMap; 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; } override void setContent (IContent c) { // Pass the content on to sub-widgets, in case they want it foreach (widg; subWidgets) widg.setContent = c; } 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; WDCMinCheck (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 IContent content () { return cList; } override void setContent (IContent) { logger.warn ("ContentListWidget: resetting content is not yet supported"); } public override bool dropContent (IContent content) { if (cList.set (content)) return true; return parent.dropContent (content); } 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 this widget id)) * $(TR $(TD 2) $(TD Row alignment is shared against other instances of this 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, potentially sharing if (data.ints[1] & 1) col = AlignColumns.getInstance (id, parent, cols, false); else col = (new AlignColumns (cols,null)); if (data.ints[1] & 2) row = AlignColumns.getInstance (id, parent, rows, true); else row = (new AlignColumns (rows,null)); 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 || n == 0); } //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); if (!col.newMinWidth (i%cols, i/cols + colR, nmw)) { // don't propegate call to parent; set position as required widg.setPosition (x + col.pos[i % cols], y + row.pos[i / cols]); mgr.requestRedraw; } // else callbacks to all sharing layouts do the rest } override void minHChange (IChildWidget widg, wdim nmh) { size_t i = getWidgetIndex(widg); if (!row.newMinWidth (i/cols, i%cols + rowR, nmh)) { widg.setPosition (x + col.pos[i % cols], y + row.pos[i / cols]); mgr.requestRedraw; } } //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; return 2; } return 0; } //BEGIN Col/row resizing callback override void dragMotion (wdim cx, wdim cy, IChildWidget) { 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 dragRelease (wdabs, wdabs, ubyte, IChildWidget) { return true; // we've handled the up-click } //END Col/row resizing callback 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..$]); } private: /* 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; } } } } } 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 () { w = col.w; parent.minWChange (this, col.mw); } void rowNewMW () { h = row.w; parent.minHChange (this, row.mw); } protected: // Data for resizing cols/rows: wdim dragX, dragY; // coords where drag starts 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. *************************************************************************************************/ private class AlignColumns { /** Get an aligner. * * Will be shared with other layouts with the same id which have the same * parent or if the parents share an aligner. * * Also ensures each widget sharing an instance expects the same number of * columns. */ static AlignColumns getInstance (widgetID id, IParentWidget parent, size_t columns, bool horiz) { if (horiz) id ~= "H"; // make the ID different for each aligner AlignColumns[]* p = id in instances; if (p) { foreach (ac; *p) { if (parent !is ac.parent) { // If parents are different GridWidget parGrid = cast(GridWidget) parent, acPGrid = cast(GridWidget) ac.parent; // and either is not a GridWidget // or their aligners are different if (parGrid is null || acPGrid is null || horiz ? parGrid.row !is acPGrid.row : parGrid.col !is acPGrid.col) { continue; // don't share with it } } if (ac.cols != columns) continue; // Alignment sharing with ContentListWidgets?? return ac; } } auto a = new AlignColumns (columns, parent); if (p) *p ~= a; else instances[id] = [a]; return a; } /** Create an aligner. * * After creation, minimal widths should be set for all columns (minWidth) and * setWidths must be called before other functions are used. * * Params: * columns = Number of columns. Not expected to change. * parent = The parent of the GridWidget using this aligner. (Used when sharing aligners.) */ this (size_t columns, IParentWidget parent) { 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; this.parent = parent; } /** 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. * * Handles l right-of-last-column fine, asserts if l < pos[0]. */ 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. * * Returns: the final width */ wdim resizeWidth (wdim nw, int dir) { debug assert (width, "AlignColumns not initialized when resizeWidth called (code error)"); debug if (nw < mw) { logger.warn ("Widget dimension set below minimal (code error)"); return w; } if (nw == w) return w; wdim diff = nw - w; if (diff > 0) { if (firstSizable == -1) diff = adjustCellSizes (diff, cols-1, -1); else diff = adjustCellSizes (diff, (dir == -1 ? lastSizable : firstSizable), dir); debug assert (diff == 0); } else { // For decreasing, visit all cols; it's possible some have size above minimal diff = adjustCellSizes (diff, (dir == -1 ? cols-1 : 0), dir); debug if (diff != 0) logger.warn ("Unable to meet target width ({}); outstanding diff: {}", nw, diff); } genPositions; debug if (nw != w) { logger.error ("resizeWidth on {} to {} failed, new width: {}, diff {}, firstSizable {}, columns {}",cast(void*)this, nw,w, diff, firstSizable, cols); /+ Also print column widths & positions: logger.error ("resizeWidth to {} failed! Column dimensions and positions:",nw); foreach (i,w; width) logger.error ("\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 if (resizeU >= cols) { // right of last column; cannot resize resizeU = -1; return true; } 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) - diff; diff = adjustCellSizes (diff, resizeD, -1); } else { diff = adjustCellSizes (diff, resizeD, -1) - diff; diff = adjustCellSizes (diff, resizeU, 1); } debug assert (diff == 0); 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. * * Returns: true if min-width changes (callbacks do necessary work), * false if no change to mw (position should still be reset). */ bool 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 if (minWidth[col] < nmw) { // increase minimal minWidth[col] = nmw; nd = width[col] - nmw; // negative diff if (nd > 0) nd = 0; // Don't decrease if already larger (mustn't shrink self) } else if (minWidth[col] > nmw) { // potentially decrease minimal // set nmw to max of all cell min widths in col: for (size_t r = 0; r < rows; ++r) { wdim mcw = minCellWidths[col+r*cols]; if (nmw < mcw) nmw = mcw; } if (minWidth[col] == nmw) // no change return false; minWidth[col] = nmw; if (!sizable[col]) nd = width[col] - nmw; // Not resizable, so shrink // Else resizable so leave } mw = spacing * cast(wdim)(cols - 1); foreach (imw; minWidth) mw += imw; if (nd == 0) return false; if (nd < 0) { // needs enlarging; we should do so here // set new width: width[col] = nmw; foreach (cb; cbs) cb.setWidth (col, nmw, -1); // Try to compensate (keep overall size the same by shrinking larger // than necessary columns); may not be possible: adjustCellSizes (nd, 0, 1); } else if (lastSizable >= 0) { // If another column can be increased, do that (otherwise the call // to parent.minXChange should decrease, if possible): // Although it _may_ not, if e.g. alignment forces larger-than-minimal size. width[col] = nmw; foreach (cb; cbs) cb.setWidth (col, nmw, -1); nd = adjustCellSizes (nd, lastSizable, -1); debug assert (nd == 0); } //else debug logger.trace ("expecting parent.minXChange to shrink"); debug wdim ow = w; genPositions; debug if (w < ow) logger.error ("newMinWidth: shrunk (code error); w={}, ow={}, nd={}", w,ow,nd); //debug logger.trace ("newMW for col: minWidth: {}, nmw: {}, col: {}, nd: {}, mw: {}, w: {}", minWidth, nmw, col, nd, mw, w); foreach (cb; cbs) cb.newMW (); return true; } /* 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: * (diff - "the actual amount adjusted") - 0 if met exactly, <0 if unable * to shrink as much as requested. * * Will shrink non-sizable columns if they're over minimal size. * Will increase column start, since it's 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); return 0; } else if (diff < 0) { // decrease wdim rd = diff; // running diff while (i >= 0 && i < cols) { if (width[i] > minWidth[i]) { 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 } i += incr; } return rd; // still had rd left to decrease (may be 0) } // else no adjustment needed (diff == 0) return 0; } /** 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 private 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 () newMW; // propegate or finalize minimal width change } CallbackStruct cbs[]; private: /* 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 IParentWidget parent; // Used to determine when to share aligner static HashMap!(widgetID,AlignColumns[]) instances; static this () { instances = new HashMap!(widgetID,AlignColumns[]); } alias IParentWidget.SIZABILITY SIZABILITY; alias IParentWidget.SIZABILITY_ENUM SIZABILITY_ENUM; invariant { if (setupWidths) { assert (width.length == cols, "invariant: bad width length"); assert (minCellWidths.length == rows * cols, "minCellWidths: bad length"); wdim[] checkMinWidth = new wdim[cols]; for (size_t c = 0; c < cols; ++c) { for (size_t r = 0; r < rows; ++r) { wdim mcw = minCellWidths[c+r*cols]; if (checkMinWidth[c] < mcw) checkMinWidth[c] = mcw; } } assert (checkMinWidth == minWidth); 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 { AlignColumns a, a2, b; a = getInstance ("a", null, 2, false); a2 = getInstance ("a", null, 2, false); b = getInstance ("a", null, 5, true); assert (a is a2); assert (a !is b); assert (getInstance ("a", null, 4, false) !is a); 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."); } }