view mde/gui/widget/layout.d @ 41:b3a6ca4516b4

The renderer now controls which parts of the window border allow resizing. committer: Diggory Hardy <diggory.hardy@gmail.com>
author Diggory Hardy <diggory.hardy@gmail.com>
date Tue, 13 May 2008 12:02:36 +0100
parents b28d7adc786b
children 8bf53e711cc7
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;

/** 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 >= 3 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 later (and is unlikely). */
        
        // 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
            colW = colWMin.dup;
            rowH = rowHMin.dup;
        } else {                            // sufficient data
            lenUsed = rows+cols;
            colW = data[0..cols];
            rowH = data[cols..lenUsed];
            
            // Check row sizes are valid:
            //NOTE: this could be made optional
            foreach (i, ref w; colW)
                if (w < colWMin[i]) w = colWMin[i];
            foreach (i, ref h; rowH)
                if (h < rowHMin[i]) h = rowHMin[i];
        }
        
        genCachedMutableData;
        w = colW[$-1] + colX[$-1];
        h = rowY[$-1] + rowH[$-1];
        
        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 ~= colW ~ rowH;
        return ret;
    }
    
    bool isWSizable () {
        return (sizableCols.length != 0);
    }
    
    bool isHSizable () {
        return (sizableRows.length != 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) {
        // Step 1: calculate the row/column sizes.
        setSizeImpl!(true)  (nw);
        setSizeImpl!(false) (nh);
        
        // Step 2: calculate the row/column offsets (positions) and set the sub-widgets sizes.
        genCachedMutableData;
        
        // Step 3: position needs to be set
        // Currently this happens by specifying that setPosition should be run after setSize.
    }
    
    void setPosition (int x, int y) {
        this.x = x;
        this.y = y;
        
        foreach (i,widget; subWidgets)
            widget.setPosition (x + colX[i % cols], y + rowY[i / cols]);
    }
    
    
    // Find the relevant widget.
    IWidget getWidget (int cx, int cy) {
        int lx = cx - x, ly = cy - y;       // use coords relative to this widget
        
        // Find the column
        int i = cols - 1;                   // starting from right...
        while (lx < colX[i]) {              // decrement while left of this column
            if (i == 0) return this;        // left of first column
            --i;
        }                                   // now (lx >= colX[i])
        if (lx >= colX[i] + colW[i]) return this;   // between columns
        
        // Find the row;
        int j = rows - 1;
        while (ly < rowY[j]) {
            if (j == 0) return this;
            --j;
        }
        if (ly >= rowY[j] + rowH[j]) return this;
        
        // Now we know it's in widget (i,j)'s cell (but the widget may not take up the whole cell)
        lx -= colX[i];
        ly -= rowY[j];
        IWidget widg = subWidgets[i + j*cols];
        widg.getCurrentSize (i,j);
        if (lx < i && ly < j)
            return widg.getWidget (cx, cy);
        return this;    // wasn't in cell
    }
    
    void draw () {
        super.draw ();
        
        foreach (widget; subWidgets)
            widget.draw ();
    }
    
private:
    /* 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 () {
        // Calculate the minimal column and row sizes:
        colWMin = new int[cols];    // set length, making sure the arrays are initialised to zero
        rowHMin = 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:
            uint n = i % cols;      // column
            if (colWMin[n] < ww) colWMin[n] = ww;
            n = i / cols;           // row
            if (rowHMin[n] < wh) rowHMin[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; colWMin)            // add the column/row's dimensions
            mw += x;
        foreach (x; rowHMin)
            mh += x;
        
        
        // Find which cols/rows are resizable:
        sizableCols = null;     // clear these because we append to them
        sizableRows = null;
        
        forCols:
        for (uint i = 0; i < cols; ++i) {                       // for each column
            for (uint 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
            sizableCols ~= i;
        }
        
        forRows:
        for (uint i = 0; i < subWidgets.length; i += cols) {    // for each row
            for (uint j = 0; j < cols; ++j)                     // for each column
                if (!subWidgets[i+j].isHSizable)
                    continue forRows;
                
            sizableRows ~= i / cols;    // the current row
        }
    }
    
    /* Calculations which need to be run whenever resizing occurs (or deeper alterations)
     * (i.e. to produce cached data calculated from construction and mutable data). */
    void genCachedMutableData () {
        // Calculate column and row locations:
        colX.length = cols;
        rowY.length = rows;
        int spacing = window.renderer.layoutSpacing;
        
        int cum = 0;
        foreach (i, x; rowH) {
            rowY[i] = cum;
            cum += x + spacing;
        }
        
        cum = 0;
        foreach (i, x; colW) {
            colX[i] = cum;
            cum += x + spacing;
        }
        
        // Tell subwidgets their new sizes:
        foreach (i,widget; subWidgets)
            widget.setSize (colW[i % cols], rowH[i / cols]);
    }
    
    /* setSize generalised for either dimension; w/h is renamed d, col/row renamed cell. */
    void setSizeImpl(bool W) (int nd) {
        static if (W) {
            alias w         d;
            alias mw        md;
            alias colW      cellD;
            alias colWMin   cellDMin;
            alias sizableCols sizableCells;
        } else {
            alias h         d;
            alias mh        md;
            alias rowH      cellD;
            alias rowHMin   cellDMin;
            alias sizableRows sizableCells;
        }
        // Could occur if adjust isn't called first, but this would be a code error:
        assert (cellD !is null, "setSizeImpl: cellD is null");
        
        /* For each of width and height, there are several cases:
        *  [new value is] more than old value
        *  ->  enlarge any row/column
        *  same as old value
        *  ->  do nothing
        *  more than min but less than current value
        *  ->  find an enlarged row/col and reduce size
        *  ->  repeat if necessary
        *  minimal value or less
        *  -> clamp to min and use min col/row sizes
        */
        if (nd > d) {       // expand (d < nd)
            if (sizableCells.length) {               // check there is a resizable col/row
                cellD[sizableCells[$-1]] += nd - d;  // new size
                d = nd;
            }
        } else if (nd < d) {
            if (nd > md) {  // shrink (md < nd < d)
                int toReduce = nd - d;  // negative
                foreach_reverse (cell; sizableCells) {
                    cellD[cell] += toReduce;
                    toReduce = cellD[cell] - cellDMin[cell];
                    if (toReduce < 0)   // reduced too far
                        cellD[cell] = cellDMin[cell];
                    else                // ok; cellD >= cellDMin
                        break;
                }
                d = nd;
            } else {        // clamp  (nd <= md)
                cellD = cellDMin.dup;   // duplicate so future edits don't affect cellDMin
                d = md;
            }
        }                   // third possibility: nd = d
    }
    
protected:
    // Construction data (saved):
    int cols, rows;     // number of cells in grid
    IWidget[] subWidgets;   // all widgets in the grid (by row):
    /* SubWidget order:    [ 0 1 ]
    *                      [ 2 3 ] */
    
    // Cached data calculated from construction data:
    int[] colWMin;      // minimal column width
    int[] rowHMin;      // minimal row height
    int mw, mh;         // minimal dimensions
    
    int[] sizableCols;   // all resizable columns / rows, empty if not resizable
    int[] sizableRows;   // resizing is done from last entry
    
    // Mutable data (saved):
    int[] colW;         // column width (widest widget)
    int[] rowH;         // row height (highest widget in the row)
    
    // Cached data calculated from construction and mutable data:
    int[] colX;         // cumulative colW[i-1] + padding (add x to get column's left x-coord)
    int[] rowY;         // cumulative rowH[i-1] + padding
}