changeset 90:b525ff28774b

Widgets generated dynamically from a list can now be standard widgets selected from data files. Started on allowing alignment to be shared between instances of a layout widget in a dynamic list (to allow column alignment of list's rows).
author Diggory Hardy <diggory.hardy@gmail.com>
date Wed, 01 Oct 2008 23:37:51 +0100
parents 97e6dce08037
children 4d5d53e4f881
files codeDoc/jobs.txt codeDoc/todo.txt data/conf/gui.mtt mde/gui/WidgetManager.d mde/gui/content/Content.d mde/gui/widget/Floating.d mde/gui/widget/Ifaces.d mde/gui/widget/TextWidget.d mde/gui/widget/createWidget.d mde/gui/widget/layout.d mde/types/Colour.d
diffstat 11 files changed, 358 insertions(+), 302 deletions(-) [+]
line wrap: on
line diff
--- a/codeDoc/jobs.txt	Mon Sep 29 18:27:17 2008 +0100
+++ b/codeDoc/jobs.txt	Wed Oct 01 23:37:51 2008 +0100
@@ -3,6 +3,7 @@
 
 
 In progress:
+Layout alignment sharing for instances of same widgetID.
 
 
 
@@ -13,6 +14,7 @@
 
 To do (importance 0-5: 0 pointless, 1 no obvious impact now, 2 todo sometime, 3 useful, 4 important, 5 urgent):
 Also see todo.txt and FIXME/NOTE comment marks.
+4   Data saving for widgetIDs with multiple instances?
 3   Try to correlate names of option sections more. (i.e. symbol name, class name, name of i18n translation file)
 3   Use of dtors - don't rely on them? Or what happens when init throws during creation - relying on undefined behaviour.
 3   glBindTexture not working with non-0 index - perhaps use a higher level graphics library at some point.
--- a/codeDoc/todo.txt	Mon Sep 29 18:27:17 2008 +0100
+++ b/codeDoc/todo.txt	Wed Oct 01 23:37:51 2008 +0100
@@ -4,43 +4,22 @@
 
 GUI:
 ->  Widgets:
-    ->  rethink how widgets are created and receive creation data, so that they don't have to be created by the Window
-    ->  scripted widgets
-    ->  decent rendering/theme system
-    ->  lists from content lists
-->  Content:
-    ->  lists
+->  rethink how widgets are created and receive creation data, so that they don't have to be created by the Window
+->  scripted widgets
+->  decent rendering/theme system
+->  lists from content lists
 
 
-+ shows current preference:
+Scratchpad area for ideas:
+
 
 Redesign:
-->  requirements
-    ->  widgets can receive int and string data types
 ->  possibilities
-    ->  per-widget merging (i.e. separate tag(s) for each widget's data)?
-        ->  if a widget with sub-widgets is defined in a base file, but redesigned in a derived file, any unused widgets with data resulting are not created
-        ->  if design changes in a base file, while the old design was modified in a derived file, will the result be sane?
-        ->  if a locally modified gui is updated upstream (so the base files change), should:
-            ->  the local modifications persist?
-            ->  the local modifications be lost?
-            ->  the widgets be merged?
-            	->  potentially gives the best of both worlds
-            	->  should original widgets used as sub-widgets of modified widgets:
-                    ->  use original data (from base file), so they can be updated?
-                    ->  have data copied, also impacting unmodified portions of window using same widget?
-                    ->  have data copied under a new ID?
-            ->  the user be asked?
-            	->  requires copy of old data...
-    +>  widget data per-gui instead of per-window?
-    	->  identical sub-widget could be used in multiple windows more easily, when data file is hand edited
-    	->  in-gui editor will probably copy data under a new ID anyway
-    	+>  Maybe change to:
-            ->  One section per version, containing widget data
-            	->  can inherit from other sections
-            ->  Which section (version of gui design) to use saved in header
-            ->  Either/or:
-            	->  Windows defined in header by primary widget
-            	+>  Windows become ordinary sub-widgets or some parent widget, gui becomes a parent widget
-    +>  widget data no longer has "construction" and "mutable" variations
-    	+>  widgets know their IDs and update their data whenever necessary, via the gui, into a new data set, which is saved upon exit
\ No newline at end of file
+    ->  How widgets get their sub-widgets:
+        ->  (current) Ask manager to create widget from manager's data with ID from widget's data.
+            ->  seems to work well; allows widgets some control over creation of children
+            ->  now extended to support instancing & passing content; seems to fulfill requirements
+        ->  (alternate) Pass widget a list of pointers to all sub-widgets
+            ->  manager creates all static (from data files) widets
+            ->  other widgets can create dynamic widgets as or to pass to sub-widgets
+            ->  widget data needs a portion which is explicitly IDs for subwidgets, or data needs reforming into a tree
--- a/data/conf/gui.mtt	Mon Sep 29 18:27:17 2008 +0100
+++ b/data/conf/gui.mtt	Wed Oct 01 23:37:51 2008 +0100
@@ -2,12 +2,15 @@
 <char[]|Renderer="Simple">
 <char[]|Design="Working">
 {Working}
-<WidgetData|root={0:[0x8100,3,3],1:["square","blank","square","blank","content","blank","square","blank","square"]}>
+<WidgetData|root={0:[0xC100,3,3],1:["square","blank","square","blank","content","blank","square","blank","square"]}>
 <WidgetData|square={0:[0x1,6,6]}>
-<WidgetData|content={0:[0x8100,4,2],1:["floating","button","blank","blank","blank","opts","blank","blank"]}>
+<WidgetData|content={0:[0xC100,4,2],1:["floating","button","blank","blank","blank","opts","blank","blank"]}>
 <WidgetData|button={0:[0x10,200,200]}>
 <WidgetData|blank={0:[0x2]}>
-<WidgetData|opts={0:[0x8110, 0xfe8c00]}>
+<WidgetData|opts={0:[0x8110],1:["optBox"]}>
+<WidgetData|optBox={0:[0xC100, 1,3],1:["optVal","optSep","optVal"]}>
+<WidgetData|optVal={0:[0x4020, 0xfe8c00]}>
+<WidgetData|optSep={0:[0x21, 0xff],1:["-=-"]}>
 <WidgetData|floating={0:[0x8200,20,20],1:["text"]}>
 <WidgetData|text={0:[0x21,0xFF0000],1:["Floating text"]}>
 {Basic}
--- a/mde/gui/WidgetManager.d	Mon Sep 29 18:27:17 2008 +0100
+++ b/mde/gui/WidgetManager.d	Wed Oct 01 23:37:51 2008 +0100
@@ -190,6 +190,7 @@
 
 import mde.gui.exception;
 import mde.gui.widget.Ifaces;
+import mde.gui.content.Content; //NOTE - maybe move IContent to a separate module
 import mde.gui.widget.createWidget;
 
 import mde.file.mergetag.Reader;
@@ -376,10 +377,17 @@
     }
     
     
-    /** Create a widget by ID. */
-    IChildWidget makeWidget (widgetID id, IParentWidget parent = null) {
+    /** Create a widget by ID.
+     *
+     * A widget instance is created from data found under ID. Multiple instances may be created.
+     * FIXME - data conflicts when saving?
+     *
+     * An IContent may be passed. This could contain many things, e.g. some basic data, a widget,
+     * multiple sub-IContents. It is only passed to the widget by createWidget if it's enumeration
+     * given in that module has the flag TAKES_CONTENT. */
+    IChildWidget makeWidget (widgetID id, IContent content = null) {
         debug (mdeWidgets) logger.trace ("Creating widget \""~id~'"');
-        return createWidget (this, curData[id], parent);
+        return createWidget (this, curData[id], content);
     }
     
     /** For making changes. */
--- a/mde/gui/content/Content.d	Mon Sep 29 18:27:17 2008 +0100
+++ b/mde/gui/content/Content.d	Wed Oct 01 23:37:51 2008 +0100
@@ -26,6 +26,13 @@
  *
  * Currently Content instances can only have their data set on creation.
  * Each Content class should provide a method to get it's content, e.g. the method text().
+ *
+ * Extensions to content:
+ * 
+ * These extensions require that a content can notify any dependants of changes.
+ * 
+ * Use as a state switch (one option from an enumeration). E.g. a button/selection box could set a
+ * state, and a tabbed box could show a tab based on this. Or could represent an option.
  */
 // Don't include dimension/drawing stuff because it's renderer specific and content should not be!
 // NOTE: an interface or a class?
--- a/mde/gui/widget/Floating.d	Mon Sep 29 18:27:17 2008 +0100
+++ b/mde/gui/widget/Floating.d	Wed Oct 01 23:37:51 2008 +0100
@@ -53,12 +53,12 @@
  * Ints supplied may consist of just the widget type or
  * additionally an (x,y) coordinate for each subwidget (all x coords first, then all y coords).
  */
-class FloatingAreaWidget : SizableWidget, IParentWidget
+class FloatingAreaWidget : SizableWidget
 {
     this (IWidgetManager mgr, WidgetData data) {
         subWidgets.length = data.strings.length;
         foreach (i,s; data.strings)
-            subWidgets[i] = mgr.makeWidget (s, this);
+            subWidgets[i] = mgr.makeWidget (s);
         sWCoords.length = subWidgets.length;
         
         if (data.ints.length != 1) {
--- a/mde/gui/widget/Ifaces.d	Mon Sep 29 18:27:17 2008 +0100
+++ b/mde/gui/widget/Ifaces.d	Wed Oct 01 23:37:51 2008 +0100
@@ -25,6 +25,7 @@
 
 public import mde.gui.types;
 public import mde.gui.renderer.IRenderer;
+import mde.gui.content.Content; //NOTE - maybe move IContent to a separate module
 
 
 /*************************************************************************************************
@@ -46,21 +47,19 @@
  * 
  * This class handles widget rendering, input, loading and saving.
  *************************************************************************************************/
-interface IWidgetManager : IParentWidget
+interface IWidgetManager : IWidget
 {
     // Loading/saving:
     /** Create a widget by ID.
      *
      * Params:
      *  id      = Identifier, within data files, of the data for the widget.
-     *  parent  = Reference to the widget's parent, passed if the widget supports recieving it.
+     *  content = An IContent may be passed to some widgets on creation.
      *
      * Creates a widget, using the widget data with index id. Widget data is loaded from files,
      * and per design (multiple gui layouts, called designs, may exist; data is per design).
-     * 
-     * Note: this method is only for widgets with an identifier; generic widgets instanciated in
-     * lists are created directly and have no WidgetData of their own. */
-    IChildWidget makeWidget (widgetID id, IParentWidget parent = null);
+     */
+    IChildWidget makeWidget (widgetID id, IContent content = null);
     
     /** Record some changes, for saving. */
     void setData (widgetID id, WidgetData);
@@ -106,22 +105,11 @@
 
 
 /*************************************************************************************************
- * Interface for parent widgets, including the gui manager.
- *
- * A widget may call these methods on its parent, and on the gui manager.
- *************************************************************************************************/
-interface IParentWidget : IWidget
-{
-    // NOTE: Likely some day this interface will really be used.
-    // NOTE: What widget is NOT going to implement this? It will probably be inherited.
-}
-
-
-/*************************************************************************************************
  * Interface for (child) widgets, i.e. all widgets other than the manager.
  *
  * A widget is a region of a GUI window which handles rendering and user-interaction for itself
- * and is able to communicate with its manager and parent/child widgets as necessary.
+ * and is able to communicate with its manager and child widgets as necessary. (Passing widgets
+ * a reference to their parent has not been found useful.)
  *
  * If a widget is to be creatable by IWidgetManager.makeWidget, it must be listed in the
  * createWidget module, and have a constructor of the following form. It should also update it's
@@ -135,6 +123,9 @@
  *  + [widgetID, x, y]
  *  + where x is ... and y is ... +/
  * this (IWidgetManager mgr, WidgetData data);
+ * 
+ * /// The CTOR may take an IContent reference:
+ * this (IWidgetManager mgr, WidgetData data, IContent content);
  * ----------------------------------
  * Where mgr is the widget manager and data is
  * initialisation data. The method should throw a WidgetDataException (created without
--- a/mde/gui/widget/TextWidget.d	Mon Sep 29 18:27:17 2008 +0100
+++ b/mde/gui/widget/TextWidget.d	Wed Oct 01 23:37:51 2008 +0100
@@ -26,42 +26,8 @@
 
 import mde.font.font;
 
-import tango.io.Stdout;
-
-/// Adapter to ease use of ContentText
-struct ContentAdapter(ContentT : IContent) {
-    void set (char[] cID, int col) {
-        if (font is null) font = FontStyle.get("default");
-        
-        static if (is(ContentT == ContentText)) {
-            content = getContentText (cID);
-        } else static if (is(ContentT == ContentInt)) {
-            content = getContentInt (cID);
-        } else static assert (false, "Unsupported content type");
-        colour = Colour (cast(ubyte) (col >> 16u),
-                         cast(ubyte) (col >> 8u),
-                         cast(ubyte) col );
-    }
-    
-    void getDimensions (out wdsize w, out wdsize h) {
-        font.updateBlock (content.toString, textCache);
-        w = cast(wdim) textCache.w;
-        h = cast(wdim) textCache.h;
-    }
-    
-    void draw (wdabs x, wdabs y) {
-        font.textBlock (x,y, content.toString, textCache, colour);
-    }
-    
-    ContentT content;
-    TextBlock textCache;
-    Colour colour;
-    static FontStyle font;
-}
-
 /// Basic text widget
-// FIXME (content, creation for different types)
-class ContentWidget(ContentT : IContent) : Widget
+class TextLabelWidget : Widget
 {
     /** Constructor for a widget containing [fixed] content.
      *
@@ -71,26 +37,29 @@
      * and colour is an 8-bit-per-channel RGB colour of the form 0xRRGGBB. */
     this (IWidgetManager mgr, WidgetData data) {
         WDCheck (data, 2, 1);
-        text.set (data.strings[0], data.ints[1]);
-        text.getDimensions (mw, mh);
+        if (font is null) font = FontStyle.get("default");
+        font.updateBlock (data.strings[0], textCache);
+        mw = cast(wdim) textCache.w;
+        mh = cast(wdim) textCache.h;
+        colour = Colour (data.ints[1]);
         super (mgr,data);
     }
     
     void draw () {
         super.draw();
-        text.draw (x,y);
+        font.textBlock (x,y, text, textCache, colour);
     }
     
 protected:
-    ContentAdapter!(ContentT) text;
+    char[] text;
+    Colour colour;
+    TextBlock textCache;
+    static FontStyle font;
 }
 
-alias ContentWidget!(ContentText) TextWidget;
-alias ContentWidget!(ContentInt) IntWidget;
 
-
-/// Adapter to ease use of ContentOptionWidget
-struct ContentOptionAdapter {
+/// Adapter to ease use of ContentLabelWidget
+struct ContentLabelAdapter {
     void set (IContent c, int col) {
         if (font is null) font = FontStyle.get("default");
         
@@ -116,21 +85,21 @@
     static FontStyle font;
 }
 
-/// Basic text widget
-class ContentOptionWidget : Widget
+/// Basic widget displaying a label from a content.
+class ContentLabelWidget : Widget
 {
     this (IWidgetManager mgr, WidgetData data, IContent c) {
         WDCheck (data, 2, 0);
-        content.set (c, data.ints[1]);
-        content.getDimensions (mw, mh);
+        adapter.set (c, data.ints[1]);
+        adapter.getDimensions (mw, mh);
         super (mgr,data);
     }
     
     void draw () {
         super.draw();
-        content.draw (x,y);
+        adapter.draw (x,y);
     }
     
 protected:
-    ContentOptionAdapter content;
+    ContentLabelAdapter adapter;
 }
--- a/mde/gui/widget/createWidget.d	Mon Sep 29 18:27:17 2008 +0100
+++ b/mde/gui/widget/createWidget.d	Wed Oct 01 23:37:51 2008 +0100
@@ -13,11 +13,15 @@
 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.
+/** This module contains code to create a widget based on an enumeration value passed at runtime.
+ *
+ * It could be a part of the WidgetLoader.makeWidget function, but having it here makes things
+ * tidier. */
 module mde.gui.widget.createWidget;
 
 import mde.gui.widget.Ifaces;
 import mde.gui.exception : WidgetDataException;
+import mde.gui.content.Content; //NOTE - maybe move IContent to a separate module
 
 // Widgets to create:
 import mde.gui.widget.layout;
@@ -38,11 +42,11 @@
  * Widget created of type data.ints[0] (see enum WIDGET_TYPES), with one of the following CTORs:
  * ---
  * this (IWidgetManager mgr, WidgetData data);
- * // Called if (data.ints[0] & WIDGET_TYPES.TAKES_PARENT):
- * this (IWidgetManager mgr, WidgetData data, IParentWidget parent);
+ * // Called if (data.ints[0] & WIDGET_TYPES.TAKES_CONTENT):
+ * this (IWidgetManager mgr, WidgetData data, IContent content);
  * ---
  *************************************************************************************************/
-IChildWidget createWidget (IWidgetManager mgr, WidgetData data, IParentWidget parent)
+IChildWidget createWidget (IWidgetManager mgr, WidgetData data, IContent content)
 in {
     assert (mgr !is null, "createWidget: mgr is null");
 } body {
@@ -73,8 +77,8 @@
 private:
 /// Widget types.
 enum WIDGET_TYPE : int {
-    TAKES_PARENT            = 0x4000,   // CTOR takes reference to its parent
-    PARENT                  = 0x8000,   // widget can have children
+    TAKES_CONTENT           = 0x4000,   // Flag indicates widget's this should be passed an IContent reference.
+    PARENT                  = 0x8000,   // widget can have children; not used by code (except in data files)
     
     // Use widget names rather than usual capitals convention
     Unnamed                 = 0x0,      // Only for use by widgets not created with createWidget
@@ -87,11 +91,11 @@
     // buttons: 0x10
     Button                  = 0x10,
     
-    // content: 0x20
-    Text		    = 0x21,
-    Int			    = 0x22,
+    // labels: 0x20
+    ContentLabel	    = TAKES_CONTENT | 0x20,
+    TextLabel               = 0x21,
     
-    GridLayout              = PARENT | 0x100,
+    GridLayout              = TAKES_CONTENT | PARENT | 0x100,
     TrialContentLayout      = PARENT | 0x110,
     
     FloatingArea            = PARENT | 0x200,
@@ -105,11 +109,11 @@
         "SizableBlank",
         "Debug",
         "Button",
-        "Text",
-        "Int",
-        "GridLayout",
+        "TextLabel",
+        "ContentLabel",
         "TrialContentLayout",
-        "FloatingArea"];
+        "FloatingArea",
+        "GridLayout"];
 
 /* Generates a binary search algorithm. */
 char[] binarySearch (char[] var, char[][] consts) {
@@ -124,8 +128,8 @@
         foreach (c; consts) {
             ret ~=  "if (" ~ var ~ " == WIDGET_TYPE." ~ c ~ ") {\n" ~
                     "   debug (mdeWidgets) logger.trace (\"Creating new "~c~"Widget.\");\n" ~
-                    "   static if (WIDGET_TYPE."~c~" & WIDGET_TYPE.TAKES_PARENT)\n" ~
-                    "       return new " ~ c ~ "Widget (mgr, data, parent);\n" ~
+                    "   static if (WIDGET_TYPE."~c~" & WIDGET_TYPE.TAKES_CONTENT)\n" ~
+                    "       return new " ~ c ~ "Widget (mgr, data, content);\n" ~
                     "   else\n" ~
                     "       return new " ~ c ~ "Widget (mgr, data);\n" ~
                     "} else ";
@@ -134,3 +138,16 @@
         return ret;
     }
 }
+
+debug { // check items in WIDGETS are listed in order
+    char[] WIDGETS_check () {
+        char[] ret;
+        for (int i = WIDGETS.length-2; i > 0; --i) {
+            ret ~= "WIDGET_TYPE."~WIDGETS[i] ~" >= WIDGET_TYPE."~ WIDGETS[i+1];
+            if (i>1) ret ~= " || ";
+        }
+        return ret;
+    }
+    mixin ("static if ("~WIDGETS_check~")
+        static assert (false, \"WIDGETS is not in order!\");");
+}
\ No newline at end of file
--- a/mde/gui/widget/layout.d	Mon Sep 29 18:27:17 2008 +0100
+++ b/mde/gui/widget/layout.d	Wed Oct 01 23:37:51 2008 +0100
@@ -21,6 +21,7 @@
 
 import mde.gui.widget.TextWidget;
 import mde.gui.content.options;
+import mde.gui.content.Content;
 
 debug {
     import tango.util.log.Log : Log, Logger;
@@ -47,8 +48,10 @@
      * 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 (IWidgetManager mgr, WidgetData data) {
+     * 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, WidgetData data, IContent content) {
         // Get grid size and check data
         // Check sufficient data for rows, cols, and possibly row/col widths.
         if (data.ints.length < 3) throw new WidgetDataException;
@@ -66,17 +69,17 @@
         // Get all sub-widgets
         subWidgets.length = rows*cols;
         foreach (i, ref subWidget; subWidgets) {
-            subWidget = mgr.makeWidget (data.strings[i]);
+            subWidget = mgr.makeWidget (data.strings[i], content);
         }
         
         super (mgr, data);
         
         if (data.ints.length == 3 + rows + cols) {
-            col.setCheck (cast(wdim[]) data.ints[3..cols+3]);
-            row.setCheck (cast(wdim[]) data.ints[cols+3..$]);
+            col.setWidths (cast(wdim[]) data.ints[3..cols+3]);
+            row.setWidths (cast(wdim[]) data.ints[cols+3..$]);
         } else {
-            col.dupMin;
-            row.dupMin;
+            col.setWidths;
+            row.setWidths;
         }
         adjustCache;
     }
@@ -105,7 +108,7 @@
     this (IWidgetManager mgr, WidgetData data) {
         debug scope (failure)
                 logger.warn ("TrialContentLayoutWidget: failure");
-        WDCheck (data, 2);
+        WDCheck (data, 1, 1);
         
         OptionList optsList = OptionList.trial();
         rows = optsList.list.length;
@@ -113,16 +116,14 @@
         
         // Get all sub-widgets
         subWidgets.length = rows*cols;
-        WidgetData COWData;
-        COWData.ints = [0, data.ints[1]];
         foreach (i, c; optsList.list) {
-            subWidgets[i] = new ContentOptionWidget (mgr, COWData, c);
+            subWidgets[i] = mgr.makeWidget (data.strings[0], c);
         }
         super (mgr, data);
         
         // Set col/row widths to minimals.
-        col.dupMin;
-        row.dupMin;
+        col.setWidths;
+        row.setWidths;
         adjustCache;
     }
     
@@ -150,14 +151,14 @@
      * 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.)
      * 
-     * Derived constructors should call either dupMin or setCheck on col and row, and then call
+     * Derived constructors should call setWidths on col and row, and then call
      * adjustCache, after calling this. */
     protected this (IWidgetManager mgr, WidgetData data) {
         super (mgr, data);
         
-        // Needn't be set before genCachedConstructionData is called:
-        col.setColWidth = &setColWidth;
-        row.setColWidth = &setRowHeight;
+        // Create cell aligners with appropriate col/row adjustment function
+        col = (new AlignColumns (cols)).addSetCallback (&setColWidth);
+        row = (new AlignColumns (rows)).addSetCallback (&setRowHeight);
         
         // Calculate cached construction data
         genCachedConstructionData;
@@ -165,8 +166,7 @@
     
     /** Generates cached mutable data.
      *
-     * Should be called by adjust() after setting col and row widths (currently via dupMin or
-     * setCheck). */
+     * Should be called by adjust() after calling setWidths. */
     void adjustCache () {
         // Generate cached mutable data
         // Calculate column and row locations:
@@ -374,173 +374,245 @@
      *                                          [ 2 3 ] */
     IChildWidget[] 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
+    AlignColumns col, row;
+}
+
+/// Position information on top of widths.
+//FIXME - merge classes back together?
+class AlignColumns : AlignWidths
+{
+    /// See AlignWidths.this
+    this (myIt columns) {
+        super (columns);
+    }
+    
+    /** Add a callback to be called to notify changes in a column's width.
+    * 
+    * All callbacks added are called on a width change so that multiple objects may share a
+    * CellAlign object. */
+    typeof(this) addSetCallback (void delegate (myIt,wdim,int) setCW) {
+        assert (setCW, "CellAlign.this: setCW is null (code error)");
+        setWidthCb ~= setCW;
+        return this;
+    }
+    
+    /** Generate position infomation and return total width of columns. */
+    wdim genPositions () {
+        pos.length = minWidth.length;
         
-        void dupMin () {
-            width = minWidth.dup;
+        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
+            debug assert (i+1 < minWidth.length, "getCell: l >= pos[$-1] + width[$-1] (code error)");
+            return -i - 1;                  // note: i might be 0 so cannot just return -i
         }
-        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;
+        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;
+            }
         }
         
-        // 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;
+        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 {
+        assert (width, "CellAlign.adjustCellSizes: width is null (code error)");
+        // Most likely if passed negative when sizing is disabled:
+        assert (start >= 0 && start < minWidth.length, "adjustCellSizes: invalid start");
+        debug 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;
+            foreach (dg; setWidthCb)
+                dg(i, width[i], incr);
         }
-        
-        // 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
-                debug assert (i+1 < minWidth.length, "getCell: l >= pos[$-1] + width[$-1] (code error)");
-                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;
+        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
+                    foreach (dg; setWidthCb)
+                        dg(i, width[i], incr);
+                    break;          // we hit the mark exactly: diff is correct
                 }
-            }
-            
-            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;
+                
+                // else we decreased it too much!
+                width[i] = minWidth[i];
+                foreach (dg; setWidthCb)
+                    dg(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
                 }
             }
-            
-            return false;			// can resize
         }
+        // else no adjustment needed (diff == 0)
         
-        /* 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 constructor doesn't call dupMin/setCheck (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;
-        }
+        genPositions;
+        return diff;
+    }
+    
+    void resize (wdim diff)
+    {
+        if (resizeU <= 0) return;
         
-        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);
-            }
+        // 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;
+    
+private:
+    wdim[]  pos;                // relative position (cumulative width[i-1] plus spacing)
+    wdim    spacing;            // used by genPositions (which cannot access the layout class's data)
+    // Callbacks used to actually adjust a column's width:
+    void delegate (myIt,wdim,int) setWidthCb[]; // set width of a column, with resize direction
+}
+
+/**************************************************************************************************
+ * 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.
+ * 
+ * FIXME:
+ * 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.
+ *************************************************************************************************/
+//FIXME: how to set cell positions after resizes?
+/+ FIXME - remove position information from here, removing need of horizontal alignment of cells
+ into columns. On resizing, don't permanently adjust sizes until callback ends (mouse button
+ released). Until then, always readjust from current "permanent" size. +/
+class AlignWidths
+{
+    /** Create an instance. After creation, the number of columns can only be changed by calling
+     * reset.
+     * 
+     * After creation, minimal widths should be set for all columns (minWidth) and
+     * setWidths must be called before other functions are used. */
+    this (myIt columns) {
+        reset (columns);
+    }
+    
+    /** Reset all column information (only keep set callbacks).
+     *
+     * Widths should be set after calling, as on creation. */
+    void reset (myIt columns) {
+        if (columns < 1)
+            throw new GuiException("AlignWidths: created with <1 column");
+        minWidth = new wdim[columns];
+        width = null;   // enforce calling setWidths after this
+    }
     
-    // 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;
+    /** Initialize widths as minimal widths. */
+    void setWidths () {
+        width = minWidth.dup;
+    }
+    /** Initialize widths from supplied list, checking validity. */
+    void setWidths (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;
+    }
+    
+    /** Minimal width for each column.
+     *
+     * Initialized to zero. Each class using this AlignWidths should, for each column, increase
+     * this value to the maximum of the minimal widths (in other words, set
+     * minWidth[i] = max(minWidth[i], cell.minWidth) for each cell in column i). */
+    wdim[]  minWidth;           // minimal widths (set by genCachedConstructionData)
+    
+    /** For each column i, sizable[i] is true if that column is resizable.
+     * firstSizable and lastSizable are the indicies of the first/last resizable column (negative
+     * if none are resizable).
+     * 
+     * Set along with minWidth before calling setWidths. */
+    bool[]  sizable;            // set by genCachedConstructionData
+    /// ditto
+    myDiff  firstSizable, lastSizable;  // set by genCachedConstructionData
+    
+    /** Current width for each column.
+     *
+     * Treat as READ ONLY! */
+    wdim[]  width;              // only adjusted within the class
+protected:
+    myDiff  resizeD,            // resize down from this index (<0 if not resizing)
+            resizeU;            // and up from this index
 }
+
+// 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.
+alias size_t myIt;
+alias ptrdiff_t myDiff;
--- a/mde/types/Colour.d	Mon Sep 29 18:27:17 2008 +0100
+++ b/mde/types/Colour.d	Wed Oct 01 23:37:51 2008 +0100
@@ -46,5 +46,13 @@
             c.b = cast(float) b / 255f;
             return c;
         }
+        /// Construct from 0xRRGGBB
+        Colour opCall (int col) {
+            Colour c;
+            c.r = cast(float) (col >> 16u) / 255f;
+            c.g = cast(float) (col >> 8u)  / 255f;
+            c.b = cast(float) col          / 255f;
+            return c;
+        }
     }
 }