view mde/gui/widget/layout.d @ 161:e3fe6acc16fb

Replaced WidgetManager's click and motion callbacks with a drag event system. This is less flexible, but much closer to what is required (and is simpler and less open to bugs through unintended use). The widget under the mouse is now passed (although could just as easily have been before).
author Diggory Hardy <diggory.hardy@gmail.com>
date Thu, 21 May 2009 22:15:40 +0200
parents ccd01fde535e
children 2476790223b8
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;

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;
	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 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, 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 cx, wdabs cy, 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..$]);
    }
    
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 () {
        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)
    
    package 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
{
    /** 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. */
    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 (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.
     * 
     * 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] && lastSizable >= 0)
                nd = width[col] - nmw;	// Not resizable but another column is
            // Else leave larger; mustn't shrink ourself
        } 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);
        }
        
        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:
    *  The amount adjusted. This may be larger than diff, since cellD is clamped by cellDMin.
    *
    * 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);
        }
        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;
            }
            diff -= rd; // still had rd left to decrease (may be 0)
        }
        // 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 () 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
    
    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;
    
    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.");
    }
}