view mde/gui/Widget.d @ 31:baa87e68d7dc

GUI now supports basic interactible widgets, widget colour and border are more unified, and some code cleanup. Removed some circular dependencies which slipped in. As a result, the OpenGL code got separated into different files. Enabled widgets to recieve events. New IParentWidget interface allowing widgets to interact with their parents. New Widget base class. New WidgetDecoration class. New ButtonWidget class responding to events (in a basic way). committer: Diggory Hardy <diggory.hardy@gmail.com>
author Diggory Hardy <diggory.hardy@gmail.com>
date Tue, 29 Apr 2008 18:10:58 +0100
parents 467c74d4804d
children
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 Widget module.
module mde.gui.Widget;

import mde.gui.Ifaces;
import mde.gui.exception;

import gl = mde.gl.basic;

import tango.io.Stdout;

//BEGIN createWidget
/// Widget types. Start high so they can be reordered easily later.
enum WIDGET_TYPE : int {
    BOX = 1001, GRID, BUTTON
}

/** Create a widget of type data[0] (see enum WIDGET_TYPES) for _window window, with initialisation
* data [1..$]. */
IWidget createWidget (IWindow window, IParentWidget parent, int[] data)
in {
    assert (window !is null, "createWidget: window is null");
    assert (parent !is null, "createWidget: parent is null");
} body {
    if (data.length < 1) throw new WidgetDataException ("No widget data");
    int type = data[0];     // type is first element of data
    data = data[1..$];      // the rest is passed to the Widget
    
    if (type == WIDGET_TYPE.BOX) return new BoxWidget (window, parent, data);
    else if (type == WIDGET_TYPE.GRID) return new GridWidget (window, parent, data);
    else if (type == WIDGET_TYPE.BUTTON) return new ButtonWidget (window, parent, data);
    else throw new WidgetDataException ("Bad widget type");
}
//END createWidget

/** A base widget class. Widgets need not inherit this (they only need implement IWidget), but this
* class provides a useful basic implementation for widgets.
*
* Technically this class could be instantiated, but it wouldn't do anything, not even draw itself.
*/
class Widget : IWidget
{
    /** Basic draw method: draw the background */
    void draw (int x, int y) {
        deco.setColor();
        gl.drawBox (x,y, w,h);
    }
    
    /** Dummy event method (ignore) */
    void clickEvent (ushort cx, ushort cy, ubyte b, bool state) {}
    
    /** Minimum size is zero. */
    void getMinimumSize (out int w, out int h) {}   // w,h initialised to 0
    /** Current size. */
    void getCurrentSize (out int w, out int h) {
        w = this.w;
        h = this.h;
    }
    
    WidgetDecoration decoration () {
        return deco;
    }
    
protected:
    WidgetDecoration deco;  // the widget's decoration
    int w, h;               // size
}

//BEGIN Widgets
/// Draws a box. That's it.
class BoxWidget : Widget
{
    this (IWindow, IParentWidget parent, int[] data) {
        if (data.length != 2) throw new WidgetDataException;
        
        deco = new WidgetDecoration (parent.decoration);
        
        w = data[0] + 2*deco.border;
        h = data[1] + 2*deco.border;
    }
}

/// Encapsulates a grid of Widgets
class GridWidget : Widget
{
    this (IWindow window, IParentWidget parent, int[] data) {
        // Get grid size
        if (data.length < 2) throw new WidgetDataException;
        rows = data[0];
        cols = data[1];
        
        deco = new WidgetDecoration (parent.decoration, TypeFlags.LAYOUT);
        
        // Get all sub-widgets
        // Check: correct data length and rows*cols >= 0 (know data.length - 2 >= 0).
        if (data.length != 2 + rows * cols) throw new WidgetDataException;
        subWidgets.length = rows*cols;
        foreach (i, inout subWidget; subWidgets) {
            subWidget = window.getWidget (data[i+2], this);
        }
        
        getMinimumSize (w,h);   // Calculate the size (current size is not saved)
    }
    
    void draw (int x, int y) {
        deco.setColor;
        gl.drawBox (x,y, w,h);
        
        foreach (i,widget; subWidgets) {
            widget.draw (x + colX[i % cols], y + rowY[i / cols]);
        }
    }
    
    // Calculates from all rows and columns of widgets.
    void getMinimumSize (out int w, out int h) {
        if (rows*cols == 0) {    // special case
            w = h = 2*deco.border;
            return;
        }
        
        // Find the sizes of all subWidgets
        int[] widgetW = new int[subWidgets.length]; // dimensions
        int[] widgetH = new int[subWidgets.length];
        foreach (i,widget; subWidgets) widget.getCurrentSize (widgetW[i],widgetH[i]);
        
        // Find row heights and column widths (non cumulative)
        rowH.length = rows;
        colW.length = cols; //WARNING: code reliant on these being initialised to zero
        for (uint i = 0; i < subWidgets.length; ++i) {
            uint x = i / cols;  // row
            if (rowH[x] < widgetH[i]) rowH[x] = widgetH[i];
            x = i % cols;       // column
            if (colW[x] < widgetW[i]) colW[x] = widgetW[i];
        }
        
        // rowY / colX
        rowY.length = rows;
        colX.length = cols;
        int cum = deco.border;
        foreach (i, x; rowH) {
            rowY[i] = cum;
            cum += x + PADDING;
        }
        h = cum + deco.border - PADDING;     // total height
        cum = deco.border;
        foreach (i, x; colW) {
            colX[i] = cum;
            cum += x + PADDING;
        }
        w = cum + deco.border - PADDING;     // total width
    }
    
    // Pass event on to relevant widget. Simply return if not on a widget.
    void clickEvent (ushort cx, ushort cy, ubyte b, bool state) {
        if (rows*cols == 0) return; // special case
        
        // Find the column
        int i = cols - 1;          // starting from right...
        while (cx < colX[i]) {      // decrement while left of this column
            if (i == 0) return;     // left of first column
            --i;
        }                           // now (cx >= colX[i])
        if (cx >= colX[i] + colW[i]) return;    // between columns
        
        // Find the row;
        int j = rows - 1;
        while (cy < rowY[j]) {
            if (j == 0) return;
            --j;
        }
        if (cy >= rowY[j] + rowH[j]) return;
        
        // Now we know it's in widget (i,j)'s cell (but the widget may not take up the whole cell)
        cx -= colX[i];
        cy -= rowY[j];
        IWidget widg = subWidgets[i + j*cols];
        widg.getCurrentSize (i,j);
        if (cx < i && cy < j)
            widg.clickEvent (cx, cy, b, state);
    }
    
protected:
    const PADDING = 4;  // padding between rows/cols
    int rows, cols;     // number of cells in grid
    int[] rowH;         // row height (highest widget in the row)
    int[] colW;         // column width (widest widget)
    int[] rowY;         // cumulative rowH[i-1] + border and padding
    int[] colX;         // cumulative colW[i-1] + border and padding
    IWidget[] subWidgets;   // all widgets in the grid (by row):
    /* SubWidget order:    [ 2 3 ]
    *                      [ 0 1 ] */
}

/// First interactible widget
class ButtonWidget : Widget
{
    bool pushed = false;// true if button is pushed in
    
    this (IWindow, IParentWidget parent, int[] data) {
        if (data.length != 2) throw new WidgetDataException;
        
        deco = new WidgetDecoration (parent.decoration);
        
        w = data[0] + 2*deco.border;
        h = data[1] + 2*deco.border;
    }
    
    void draw (int x, int y) {
        if (pushed)
            gl.setColor (1f, 0f, 1f);
        else
            gl.setColor (.6f, 0f, .6f);
        gl.drawBox (x,y, w,h);
    }
    
    void getMinimumSize (out int w, out int h) {
        w = this.w; // button is not resizable
        h = this.h;
    }
    
    void clickEvent (ushort, ushort, ubyte b, bool state) {
        if (b == 1) pushed = state; // very basic
    }
}
//END Widgets