view mde/gui/widget/layout.d @ 133:9fd705793568

Fixed menu popup bug, improved recursion detection. Menu popups can now determine whether or not they are sub-menus. Recursion detection can now also check content (if not the same, there's not a risk of infinite recursion).
author Diggory Hardy <diggory.hardy@gmail.com>
date Fri, 23 Jan 2009 16:05:05 +0000
parents 9cff74f68b84
children 4084f07f2c7a
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[1] = b.sizable[3] = true;
        b.setWidths;
        assert (b.w == 58);
        
        assert (b.resizeWidth (60, -1) == 60);
        assert (b.width[3] == 12);
        
        logger.info ("Unittest complete.");
    }
}