view mde/gui/widget/layout.d @ 45:0fd51d2c6c8a

Several changes to resising windows and layout widgets. This commit still has some bugs. Moved the implementable widgets from mde.gui.widget.Widget to miscWidgets, leaving base widgets in Widget. Rewrote some of GridLayoutWidget's implementation. Made many operations general to work for either columns or rows. Some optimisations were intended but ended up being removed due to problems. Allowed layout's to resize from either direction (only with window resizes). committer: Diggory Hardy <diggory.hardy@gmail.com>
author Diggory Hardy <diggory.hardy@gmail.com>
date Thu, 22 May 2008 11:34:09 +0100
parents 1530d9c04d4d
children 03fa79a48c48
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 : Widget
{
    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;
        super (wind, data);
        
        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 = window.makeWidget (data[i+3]);
        }
        
        // Calculate cached construction data
        genCachedConstructionData;
    }
    
    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 (data[0..cols]);
            row.setCheck (data[cols..lenUsed]);
        }
        
        
        // 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 "high direction":
            widget.setSize (col.width[i % cols], row.width[i / cols], true, true);
        
        return data[lenUsed..$];
    }
    
    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;
    }
    int[] getMutableData () {
        int[] ret;
        foreach (widget; subWidgets)
            ret ~= widget.getMutableData;
        
        ret ~= col.width ~ row.width;
        return ret;
    }
    
    bool isWSizable () {
        return col.firstSizable >= 0;
    }
    bool isHSizable () {
        return row.firstSizable >= 0;
    }
    
    /* Calculates the minimal size from all rows and columns of widgets. */
    void getMinimalSize (out int mw, out int mh) {
        mw = this.mw;
        mh = this.mh;
    }
    
    void setSize (int nw, int nh, bool wHigh, bool hHigh) {
        debug scope (failure) {
            char[128] tmp;
                logger.trace ("setSize failed: hHigh = " ~ (hHigh ? "true" : "false"));
                logger.trace (logger.format (tmp, "rows to resize: {}, {}", row.firstSizable, row.lastSizable));
        }
        // Optimisation (could easily be called with same sizes if a parent layout widget is
        // resized, since many columns/rows may not be resized).
        if (nw == w && nh == h) return;
        
        // calculate the row/column sizes (and new positions)
        if (wHigh)
            w += col.adjustCellSizes (nw - w, col.lastSizable, -1);
        else
            w += col.adjustCellSizes (nw - w, col.firstSizable, 1);
        if (hHigh)
            h += row.adjustCellSizes (nh - h, row.lastSizable, -1);
        else
            h += row.adjustCellSizes (nh - h, row.firstSizable, 1);
        
        // set the sub-widget's sizes & positions
        setSubWidgetSP (wHigh, hHigh);
    }
    
    void setPosition (int x, int y) {
        this.x = x;
        this.y = y;
        
        foreach (i,widget; subWidgets)
            widget.setPosition (x + col.pos[i % cols], y + row.pos[i / cols]);
    }
    
    
    // Find the relevant widget.
    IWidget getWidget (int cx, int 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 (ushort cx, ushort 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). */
    void genCachedConstructionData () {
        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 int[cols];
        row.minWidth = new int[rows];
        int ww, wh;     // sub-widget minimal sizes
        foreach (i,widget; subWidgets) {
            widget.getMinimalSize (ww, wh);
            
            // Increase dimensions if current minimal size is larger:
            myIt n = i % cols;	// column
            if (col.minWidth[n] < ww) col.minWidth[n] = ww;
            n = i / cols;		// row
            if (row.minWidth[n] < wh) row.minWidth[n] = wh;
        }
        
        
        // Calculate the overall minimal size, starting with the spacing:
        mh = window.renderer.layoutSpacing;	// use mh temporarily
        mw = mh * (cols - 1);
        mh *= (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;
        }
    }
    
    // set sub-widgets size & position (done after resizing widget or rows/columns)
    void setSubWidgetSP (bool wH, bool hH) {
        for (myIt i = 0; i < cols; ++i)
            for (myIt j = 0; j < rows; ++j)
        {
            IWidget widget = subWidgets[i + cols*j];
            widget.setSize (col.width[i], row.width[j], wH, hH);
            widget.setPosition (x + col.pos[i], y + row.pos[j]);
        }
    }
    //END Cache calculation functions
    
    
    //BEGIN Col/row resizing
    void resizeCallback (ushort cx, ushort 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;
        
        // NOTE: Resizing direction is set to "high direction" which isn't always going to be
        // correct. A more accurate but more complex approach might be to get
        // adjustCellSizes to do the work.
        setSubWidgetSP (true, true);
        window.requestRedraw;
    }
    bool endCallback (ushort cx, ushort 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:
    int dragX, dragY;	// coords where drag starts
    //END Col/row resizing
    
    
    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 {
        int[] 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
        int spacing;	// used by genPositions (which cannot access the layout class's data)
        
        void dupMin () {
            width = minWidth.dup;
        }
        void setCheck (int[] 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)
        int genPositions () {
            pos.length = minWidth.length;
            
            int 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 (int 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 (int 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.
        */
        int adjustCellSizes (int diff, myDiff start, myDiff incr)
        in {// Could occur if adjust isn't called first, but this would be a code error:
            char[128] tmp;
            logger.trace (logger.format (tmp, "start is {}", start));
            assert (width !is null, "adjustCellSizes: width is null");
            assert (start >= 0 && start < minWidth.length, "adjustCellSizes: invalid start");
            assert (incr == 1 || incr == -1, "adjustCellSizes: invalid incr");
        } body {
            debug scope(failure)
                    logger.trace ("adjustCellSizes: failure");
            myDiff i = start;
            if (diff > 0) {		// increase size of first resizable cell
                width[i] += diff;
            }
            else if (diff < 0) {	// decrease
                int 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
                        break;		// we hit the mark exactly: diff is correct
                    
                    // else we decreased it too much!
                    width[i] = minWidth[i];
                    // 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 (int 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.
    alias size_t myIt;
    alias ptrdiff_t myDiff;
}