view mde/gui/widget/layout.d @ 71:77c7d3235114

Separated the grid layout widget's implementation into a base and a derived class, to allow other uses of layout.
author Diggory Hardy <diggory.hardy@gmail.com>
date Sat, 05 Jul 2008 15:36:39 +0100
parents f54ae4fc2b2f
children 159775502bb4
line wrap: on
line source

/* LICENSE BLOCK
Part of mde: a Modular D game-oriented Engine
Copyright © 2007-2008 Diggory Hardy

This program is free software: you can redistribute it and/or modify it under the terms
of the GNU General Public License as published by the Free Software Foundation, either
version 2 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>. */

/// Gui layout widgets.
module mde.gui.widget.layout;

import mde.gui.widget.Widget;
import mde.gui.exception;

debug {
    import tango.util.log.Log : Log, Logger;
    private Logger logger;
    static this () {
        logger = Log.getLogger ("mde.gui.widget.layout");
    }
}

/*************************************************************************************************
 * Encapsulates a grid of Widgets.
 *
 * Currently there is no support for changing number of cells, sub-widgets or sub-widget properties
 * (namely isW/HSizable and minimal size) after this() has run.
 *
 * Since a grid with either dimension zero is not useful, there must be at least one sub-widget.
 *
 * The grid has no border but has spacing between widgets.
 *************************************************************************************************/
class GridLayoutWidget : GridWidget
{
    /** Constructor for a grid layout widget.
     *
     * Widget uses the initialisation data:
     * [widgetID, r, c, w11, w12, ..., w1c, ..., wr1, ..., wrc]
     * where r and c are the number of rows and columns, and wij is the ID (from parent Window's
     * list) for the widget in row i and column j. The number of parameters must be r*c + 3. */
    this (IWindow wind, int[] data) {
        // Get grid size and check data
        // Check sufficient data for rows, cols, and at least one widget:
        if (data.length < 4) throw new WidgetDataException;
        
        rows = data[1];
        cols = data[2];
        if (data.length != 3 + rows * cols) throw new WidgetDataException;
        /* data.length >= 4 so besides checking the length is correct, this tells us:
         *      rows * cols >= 4 - 3 = 1            a free check!
         * The only thing not checked is whether both rows and cols are negative, which would
         * cause an exception when dynamic arrays are allocated by genCachedConstructionData, which
         * is an acceptible method of failure (and is unlikely anyway). */
        
        // Get all sub-widgets
        subWidgets.length = rows*cols;
        foreach (i, ref subWidget; subWidgets) {
            subWidget = wind.makeWidget (data[i+3]);
        }
        super (wind, data);
    }
    /** Return construction data to recreate this GridLayoutWidget. */
    int[] getCreationData () {
        int[] ret;
        ret.length = 3 + subWidgets.length;
        
        ret [0..3] = [widgetType, rows, cols];  // first data
        
        foreach (i,widget; subWidgets)          // sub widgets
            ret[i+3] = window.addCreationData (widget);
        
        return ret;
    }
}


/*************************************************************************************************
 * Trial layout of sub-widgets of one type only.
 *************************************************************************************************/
class TrialLayout : GridWidget
{
    this (IWindow wind, int[] data) {
        assert (false, "Not ready");
        if (data.length != 6) throw new WidgetDataException;
        super (wind, data);
        
        rows = data[1];
        cols = data[2];
        
        // Get all sub-widgets
        subWidgets.length = rows*cols;
        foreach (i, ref subWidget; subWidgets) {
            //subWidget = new ContentWidget (data[3..6]);
        }
    }
    
    
}


/*************************************************************************************************
 * Backend for grid-based (includes column/row) layout widgets.
 *
 * A deriving class must at least do some work in it's constructor (see Ddoc for this() below)
 * and provide an implementation of getCreationData() (unless Widget's version is sufficient).
 *
 * Since a grid with either dimension zero is not useful, there must be at least one sub-widget.
 *
 * The grid has no border but has spacing between widgets.
 *************************************************************************************************/
class GridWidget : Widget
{
    //BEGIN Creation & saving
    /** Partial constructor for a grid layout widget.
     *
     * Deriving classes should check data length, and set rows, cols, and the subWidgets array,
     * before calling this super constructor. (If it's necessary to call super(...) first,
     * the call to genCachedConstructionData can be moved to the derived this() methods.) */
    protected this (IWindow wind, int[] data) {
        super (wind, data);
        
        // Needn't be set before genCachedConstructionData is called:
        col.setColWidth = &setColWidth;
        row.setColWidth = &setRowHeight;
        
        // Calculate cached construction data
        genCachedConstructionData;
    }
    
    /** This implementation of adjust() does two things:
     * 1. Pass adjust data on to sub-widgets
     * 2. Set the size, from the adjust data if possible
     *
     * Can be overridden (probably along with getMutableData()) if a different implementation is
     * wanted. adjustCache() may still be useful. */
    int[] adjust (int[] data) {
        // Give all sub-widgets their data:
        foreach (widget; subWidgets)
            data = widget.adjust (data);
        
        /** We basically short-cut setSize by loading previous col/row sizes and doing the final
         * calculations.
         * Note: if setSize gets called afterwards, it should have same dimensions and so not do
         * anything. */
        int lenUsed = 0;
        if (data.length < rows + cols) {    // data error; use defaults
            col.dupMin;
            row.dupMin;
        } else {                            // sufficient data
            lenUsed = rows+cols;
            col.setCheck (cast(wdim[])data[0..cols]);
            row.setCheck (cast(wdim[])data[cols..lenUsed]);
        }
        
        adjustCache();
        return data[lenUsed..$];
    }
    /** Generates cached mutable data.
     *
     * Should be called by adjust() after setting col and row widths (currently via dupMin or
     * setCheck). */
    void adjustCache () {
        // Generate cached mutable data
        // Calculate column and row locations:
        w = col.genPositions;
        h = row.genPositions;
        
        // Tell subwidgets their new sizes. Positions are given by a later call to setPosition.
        foreach (i,widget; subWidgets) {
            // Resizing direction is arbitrarily set to negative:
            widget.setWidth  (col.width[i % cols], -1);
            widget.setHeight (row.width[i / cols], -1);
        }
    }
    /** Returns sub-widget mutable data along with column widths and row heights, as used by
     *  adjust(). */
    int[] getMutableData () {
        int[] ret;
        foreach (widget; subWidgets)
            ret ~= widget.getMutableData;
        
        return ret ~ cast(int[])col.width ~ cast(int[])row.width;
    }
    //END Creation & saving
    
    //BEGIN Size & position
    bool isWSizable () {
        return col.firstSizable >= 0;
    }
    bool isHSizable () {
        return row.firstSizable >= 0;
    }
    
    void setWidth (wdim nw, int dir) {
        if (nw == w) return;
        
        w += col.adjustCellSizes (nw - w, (dir == -1 ? col.lastSizable : col.firstSizable), dir);
        
        // Note: setPosition must be called after!
    }
    void setHeight (wdim nh, int dir) {
        if (nh == h) return;
        
        h += row.adjustCellSizes (nh - h, (dir == -1 ? row.lastSizable : row.firstSizable), dir);
        
        // Note: setPosition must be called after!
    }
    
    void setPosition (wdim x, wdim y) {
        this.x = x;
        this.y = y;
        
        foreach (i,widget; subWidgets)
            widget.setPosition (x + col.pos[i % cols], y + row.pos[i / cols]);
    }
    //END Size & position
    
    
    // Find the relevant widget.
    IWidget getWidget (wdim cx, wdim cy) {
        debug scope (failure)
                logger.warn ("getWidget: failure");
        // Find row/column:
        myDiff i = col.getCell (cx - x);
        myDiff j = row.getCell (cy - y);
        if (i < 0 || j < 0)	// on a space between widgets
            return this;
        
        // On a subwidget; recurse call:
        return subWidgets[i + j*cols].getWidget (cx, cy);
    }
    
    // Resizing columns & rows
    void clickEvent (wdabs cx, wdabs cy, ubyte b, bool state) {
        debug scope (failure)
                logger.warn ("clickEvent: failure");
        if (b == 1 && state == true) {
            /* Note: Because of getWidget, this function is only called if the click is not on a
            * sub-widget, so we know it's on some divisor (so at least one of resizeCol and
            * resizeRow is non-negative). */
            
            // find col/row's resizeD & resizeU
            if (col.findResize (cx - x) && row.findResize (cy - y))
                return;		// unable to resize
            
            dragX = cx;
            dragY = cy;
            
            window.gui.addClickCallback (&endCallback);
            window.gui.addMotionCallback (&resizeCallback);
        }
    }
    
    void draw () {
        super.draw ();
        
        foreach (widget; subWidgets)
            widget.draw ();
    }
    
private:
    //BEGIN Cache calculation functions
    /* Calculations which need to be run whenever a new sub-widget structure is set
     * (i.e. to produce cached data calculated from construction data).
     * Also need to be re-run if the renderer changes.
     *
     * rows, cols and subWidgets must be set before calling. */
    void genCachedConstructionData () {
        // Will only change if renderer changes:
        col.spacing = row.spacing = window.renderer.layoutSpacing;
        
        // Calculate the minimal column and row sizes:
        // set length, making sure the arrays are initialised to zero:
        col.minWidth = new wdim[cols];
        row.minWidth = new wdim[rows];
        foreach (i,widget; subWidgets) {
            // Increase dimensions if current minimal size is larger:
            myIt n = i % cols;	// column
            wdim md = widget.minWidth;
            if (col.minWidth[n] < md) col.minWidth[n] = md;
            n = i / cols;		// row
            md = widget.minHeight;
            if (row.minWidth[n] < md) row.minWidth[n] = md;
        }
        
        
        // Calculate the overall minimal size, starting with the spacing:
        mh = window.renderer.layoutSpacing;	// use mh temporarily
        mw = mh * cast(wdim)(cols - 1);
        mh *= cast(wdim)(rows - 1);
        
        foreach (x; col.minWidth)		// add the column/row's dimensions
            mw += x;
        foreach (x; row.minWidth)
            mh += x;
        
        
        // Find which cols/rows are resizable:
        // reset:
        col.sizable = new bool[cols];
        row.sizable = new bool[rows];
        col.firstSizable = row.firstSizable = -1;
        
        forCols:
        for (myIt i = 0; i < cols; ++i) {				// for each column
            for (myIt j = 0; j < subWidgets.length; j += cols)	// for each row
                if (!subWidgets[i+j].isWSizable)	// column not resizable
                    continue forCols;			// continue the outer for loop
                
            // column is resizable if we get to here
            col.sizable[i] = true;
            if (col.firstSizable < 0)
                col.firstSizable = i;
            col.lastSizable = i;
        }
        
        forRows:
        for (myIt i = 0; i < subWidgets.length; i += cols) {	// for each row
            for (myIt j = 0; j < cols; ++j)			// for each column
                if (!subWidgets[i+j].isHSizable)
                    continue forRows;
            
            row.lastSizable = i / cols;
            row.sizable[row.lastSizable] = true;
            if (row.firstSizable < 0)
                row.firstSizable = row.lastSizable;
        }
    }
    //END Cache calculation functions
    
    
    void setColWidth (myIt i, wdim w, int dir) {
        for (myIt j = 0; j < rows; ++j) {
            subWidgets[i + cols*j].setWidth (w, dir);
        }
    }
    void setRowHeight (myIt j, wdim h, int dir) {
        for (myIt i = 0; i < cols; ++i) {
            subWidgets[i + cols*j].setHeight (h, dir);
        }
    }
    
    
    //BEGIN Col/row resizing callback
    void resizeCallback (wdim cx, wdim cy) {
        col.resize (cx - dragX);
        row.resize (cy - dragY);
        
        // NOTE: all adjustments are relative; might be better if they were absolute?
        dragX = cx;
        dragY = cy;
        
        foreach (i,widget; subWidgets)
            widget.setPosition (x + col.pos[i % cols],
                                y + row.pos[i / cols]);
        window.requestRedraw;
    }
    bool endCallback (wdabs cx, wdabs cy, ubyte b, bool state) {
        if (b == 1 && state == false) {
            window.gui.removeCallbacks (cast(void*) this);
            return true;	// we've handled the up-click
        }
        return false;		// we haven't handled it
    }
    
protected:
    // Data for resizing cols/rows:
    wdim dragX, dragY;	// coords where drag starts
    //END Col/row resizing callback
    
    
    myIt cols, rows;	// number of cells in grid
    
    /* All widgets in the grid, by row. Order:  [ 0 1 ]
     *                                          [ 2 3 ] */
    IWidget[] subWidgets;
    
    /* Widths, positions, etc., either of columns or of rows
     *
     * The purpose of this struct is mostly to unify functionality which must work the same on both
     * horizontal and vertical cell placement.
     *
     * Most notation corresponds to horizontal layout (columns), simply for easy of naming. */
    struct CellDimensions {
        wdim[] pos,	// relative position (cumulative width[i-1] plus spacing)
              width,	// current widths
              minWidth;	// minimal widths (set by genCachedConstructionData)
        bool[] sizable;	// true if col is resizable (set by genCachedConstructionData)
        myDiff firstSizable,	// first col which is resizable, negative if none
               lastSizable;	// as above, but last (set by genCachedConstructionData)
        myDiff resizeD,	// resize down from this index (<0 if not resizing)
               resizeU;	// and up from this index
        wdim spacing;	// used by genPositions (which cannot access the layout class's data)
        /* This is a delegate to a enclosing class's function, since:
         * a different implementation is needed for cols or rows
         * we're unable to access enclosing class members directly */
        void delegate (myIt,wdim,int) setColWidth;	// set width of a column, with resize direction
        
        void dupMin () {
            width = minWidth.dup;
        }
        void setCheck (wdim[] data) {
            // Set to provided data:
            width = data;
            // And check sizes are valid:
            foreach (i, m; minWidth)
                // if fixed width or width is less than minimum:
                if (!sizable[i] || width[i] < m)
                    width[i] = m;
        }
        
        // Generate position infomation and return total width (i.e. widget width/height)
        wdim genPositions () {
            pos.length = minWidth.length;
            
            wdim x = 0;
            foreach (i, w; width) {
                pos[i] = x;
                x += w + spacing;
            }
            return x - spacing;
        }
        
        // Get the row/column a click occured in
        // Returns -i if in space to left of col i, or i if on col i
        myDiff getCell (wdim l) {
            myDiff i = minWidth.length - 1;	// starting from right...
            while (l < pos[i]) {		// decrement while left of this column
                debug assert (i > 0, "getCell: l < pos[0] (code error)");
                --i;
            }					// now (l >= pos[i])
            if (l >= pos[i] + width[i])		// between columns
                return -i - 1;			// note: i might be 0 so cannot just return -i
            return i;
        }
        
        // Calculate resizeU/resizeD, and return true if unable to resize.
        bool findResize (wdim l) {
            resizeU = -getCell (l);		// potential start for upward-resizes
            if (resizeU <= 0) return true;	// not on a space between cells
            resizeD = resizeU - 1;		// potential start for downward-resizes
            
            while (!sizable[resizeU]) {		// find first actually resizable column (upwards)
                ++resizeU;
                if (resizeU >= minWidth.length) {	// cannot resize
                    resizeU = -1;
                    return true;
                }
            }
            
            while (!sizable[resizeD]) {		// find first actually resizable column (downwards)
                --resizeD;
                if (resizeD < 0) {		// cannot resize
                    resizeU = -1;	// resizeU is tested to check whether resizes are possible
                    return true;
                }
            }
            
            return false;			// can resize
        }
        
        /* Adjust the total size of rows/columns (including spacing) by diff.
        *
        * Params:
        *  diff	= amount to increase/decrease the total size
        *  start= index for col/row to start resizing on
        *  incr	= direction to resize in (added to index each step). Must be either -1 or +1.
        *
        * Returns:
        *  The amount adjusted. This may be larger than diff, since cellD is clamped by cellDMin.
        *
        * Note: Check variable used for start is valid before calling! If a non-sizable column's
        *  index is passed, this should get increased (if diff > 0) but not decreased.
        */
        wdim adjustCellSizes (wdim diff, myDiff start, int incr)
        in {
            // Could occur if adjust isn't called first, but this would be a code error:
            assert (width !is null, "adjustCellSizes: width is null");
            // Most likely if passed negative when sizing is disabled:
            assert (start >= 0 && start < minWidth.length, "adjustCellSizes: invalid start");
            assert (incr == 1 || incr == -1, "adjustCellSizes: invalid incr");
            assert (setColWidth !is null, "adjustCellSizes: setColWidth is null");
        } body {
            debug scope(failure)
                    logger.trace ("adjustCellSizes: failure");
            myDiff i = start;
            if (diff > 0) {		// increase size of first resizable cell
                width[i] += diff;
                setColWidth (i, width[i], incr);
            }
            else if (diff < 0) {	// decrease
                wdim rd = diff;		// running diff
                aCSwhile:
                while (true) {
                    width[i] += rd;	// decrease this cell's size (but may be too much)
                    rd = width[i] - minWidth[i];
                    if (rd >= 0) {	// OK; we're done
                        setColWidth (i, width[i], incr);	// set new width
                        break;		// we hit the mark exactly: diff is correct
                    }
                    
                    // else we decreased it too much!
                    width[i] = minWidth[i];
                    setColWidth (i, width[i], incr);
                    // rd is remainder to decrease by
                    
                    
                    bool it = true;	// iterate (force first time)
                    while (it) {
                        i += incr;
                        if (i < 0 || i >= minWidth.length) {	// run out of next cells
                            diff -= rd;	// still had rd left to decrease
                            break aCSwhile;	// exception: Array index out of bounds
                        }
                        it = !sizable[i];	// iterate again if row/col isn't resizable
                    }
                }
            }
            // else no adjustment needed (diff == 0)
            
            genPositions;
            return diff;
        }
        
        void resize (wdim diff) {
            if (resizeU <= 0) return;
            
            // do shrinking first (in case we hit the minimum)
            if (diff >= 0) {
                diff = -adjustCellSizes (-diff, resizeU, 1);
                adjustCellSizes (diff, resizeD, -1);
            } else {
                diff = -adjustCellSizes (diff, resizeD, -1);
                adjustCellSizes (diff, resizeU, 1);
            }
        }
    }
    CellDimensions col, row;
    
    // Index types. Note that in some cases they need to hold negative values.
    // int is used for resizing direction (although ptrdiff_t would be more appropriate),
    // since the value must always be -1 or +1 and int is smaller on X86_64.
    alias size_t myIt;
    alias ptrdiff_t myDiff;
}