12. A User-Defined Control

When the predefined GUI facilities (buttons, slider bars, etc.) provided by CLEAN are insufficient, it is possible to write a control facility whose look and feel are completely user-defined. A generic control can be specified similarly to the operations in section 11. However, to separately compile a specification of a control specifically for LIFE animation, we require a header file specifying the types used by the GUI-based animation program:

IMPLEMENTATION MODULE winhead;
IMPORT lifestate, winmisc, lifeio;

The user state for this program contains a boolean flag indicating if the animation is currently running, the size (in pixels) of cells in the viewing window, a boolean flag indicating if tracking is used, the Lifestate abstract data type, and the file system. The last three of these were also used as function parameters to DoRun in the text-based animation of section 10.

TYPE
:: BlockSize -> INT;
:: RunState  -> BOOL;
:: UNQ State -> (!RunState, !BlockSize, !TRACKMODE, !Lifestate, !FILES);
:: UNQ IO    -> IOState State;

The winhead definition module (winhead.dcl) is identical to the implementation module:

DCLwinhead.dcl

The control which we define is used to reposition the viewing window around the LIFE pattern. It provides a reduced view of the rectangle occupied by the picture and the square occupied by the viewing window (the look ) and also the ability to move the viewing window with the mouse (the feel ).

A control in CLEAN cannot affect either the user or system states in the way that e.g. dialog boxes can. However, a control is always embedded inside a dialog box, and it can alter both the state of the dialog box in which it is placed, as well as its own internal control state. For this example, the control cannot cause the viewing window to actually move. However, it activates a button in its parent dialog box, and clicking this button causes the actual movement to take place.

IMPLEMENTATION MODULE wincontrol;
IMPORT winhead, deltaDialog, deltaPicture;

The Scaletype type is used to specify a translation and scaling of the viewing window to the control's own drawing domain. The size of this domain is specified by a macro, and the graphics defining the control's look and feel are automatically cropped by CLEAN to fit the domain. The control's internal state is an algebraic data type which provides a kind of dynamic typing. As a dummy initial state we use IntCS 0, while the state PairCS (IntCS x ) (IntCS y ) is used to specify that the centre of the viewing window should be moved to the coordinates (x,y).

Because of the complexity of this control, we have chosen a simple control state which alters in response to mouse events, and a look and feel which alter in response to changes in the LIFE pattern. It would also be possible to define this control using static look and feel functions, but this would require a much more complex control state to specify properties of the current pattern and viewing window. Programming the GUI interface is much easier if we allow the look and feel to change with time.

TYPE
:: ScaleType -> (!Pnt, !INT);

MACRO
   ControlSize -> 80;
   ControlDomain -> ((0,0),(ControlSize,ControlSize));
   CrossSize -> 4;
   InitCS -> IntCS 0;

The control is parameterised on the identity number and position it will have inside its parent dialog box, the current RunState and Lifestate which it will show, and the identity number of its associated dialog button. If the animation is running or the LIFE pattern is dead, the feel of the control is disabled:

RULE
:: TheControl DialogItemId ItemPos RunState Lifestate DialogItemId 
                                                -> DialogItem State IO;
   TheControl id pos run life buttonid 
     -> Control id pos ControlDomain ability InitCS look feel fn,
        ability: BTOSelectState (NOT (OR run (Dead life))),
        (look, feel): LookAndFeel life,
        fn: TheControlFn buttonid;

The look and feel of the control both depend on the Lifestate in similar ways, so are partly calculated by one function. If the LIFE pattern is dead, the look is specified by DeadLook (which simply fills the control domain with a light grey pattern), and the feel is ignored because it is disabled. Otherwise, the look and feel are specified by TheControlLook and TheControlFeel, which depend on the rectangle ((a,b),(c,d)) occupied by the pattern, the size (w xh) of the viewing window, the 'centre of gravity' cofg of the pattern, the upper left corner upleft of the viewing window, and the scaling factor necessary:

:: LookAndFeel Lifestate -> (ControlLook, ControlFeel);
   LookAndFeel life
     -> (DeadLook, feel), IF Dead life
     -> (TheControlLook scale abcd upleft w h cofg, feel),
        feel: TheControlFeel scale upleft w h,
        abcd: Picrect life,
        (ab,cd): abcd,
        (a,b): ab,
        (c,d): cd,
        win: Winrect life,
        (upleft,botright): win,
        x: + (* 2 w) (- c a),
        y: + (* 2 h) (- d b),
        scale:((- a w, - b h), Max  x y),
        w: Width life,
        h: Height life,
        cofg: CentOfGrav life;

:: DeadLook SelectState ControlState -> [DrawFunction];
   DeadLook ability anycs
     -> [SetPenPattern LtGreyPattern, FillRectangle ControlDomain];

The look is defined by a function which accepts (in addition to parameters supplied by LookAndFeel) the current selection ability of the control, and the current control state. If the control is active, the rectangle occupied by the pattern is drawn by ControlRect, the viewing window is drawn as a solid square by ControlActive or ControlBox, and the centre of gravity of the pattern is drawn as a cross by ControlCross. The ControlActive function is used when the position of the viewing window box has already been moved from its original position, and the ControlBox function when it has not. The translation between viewing window coordinates and control domain coordinates is done by ControlScale and ControlUnScale. An example of an active control (after dragging the black square representing the viewing window, and just before clicking the GoTo button) is shown in Figure 6. Clicking on the GoTo button causes the window to be moved to the new area of the pattern, and the button to become disabled. If the control is inactive, the square representing the current viewing window is shown in dark grey instead of black, as shown in Figure 7.

:: TheControlLook ScaleType Rect Pnt !WIDTH !HEIGHT Pnt 
            !SelectState ControlState -> [DrawFunction];
   TheControlLook scale rect upleft w h cofg 
            ability (PairCS (IntCS x) (IntCS y))
     -> [ControlRect scale rect, SetPenMode XorMode,
         ControlActive scale w h x y | ControlCross scale cofg];
   TheControlLook scale rect upleft w h cofg Unable notactivecs
     -> [ControlRect scale rect, SetPenMode XorMode,
         SetPenPattern DkGreyPattern,
         ControlBox scale upleft w h | ControlCross scale cofg];
   TheControlLook scale rect upleft w h cofg Able notactivecs
     -> [ControlRect scale rect, SetPenMode XorMode,
         ControlBox scale upleft w h | ControlCross scale cofg];

:: ControlRect ScaleType Rect -> DrawFunction;
   ControlRect scale ((a,b),(c,d))
     -> DrawLine (ControlScale scale a b, ControlScale scale c d), 
                                                        IF = a c || = b d
     -> DrawRectangle (ControlScale scale a b, ControlScale scale c d);

:: ControlActive ScaleType !WIDTH !HEIGHT !INT !INT -> DrawFunction;
   ControlActive scale w h p q
     -> FillRectangle (ControlScale scale a b, ControlScale scale c d),
        a: - p (Half w),
        b: - q (Half h),
        c: -- (+ a w),
        d: -- (+ b h);

:: ControlBox ScaleType Pnt !WIDTH !HEIGHT -> DrawFunction;
   ControlBox scale (a,b) w h
     -> FillRectangle (ControlScale scale a b, ControlScale scale c d),
        c: -- (+ a w),
        d: -- (+ b h);

:: ControlCross ScaleType Pnt -> [DrawFunction];
   ControlCross scale (x,y)
     -> [SetPenSize (2,2),
         DrawLine ((- x' CrossSize, y'), (+ x' CrossSize, y')),
         DrawLine (( x', - y' CrossSize), (x', + y' CrossSize)),
         SetPenNormal], 
        (x',y'): ControlScale scale x y;

:: ControlScale ScaleType !INT !INT -> Point;
   ControlScale ((a,b),s) x y 
     -> (/ (* (- x a) ControlSize) s, / (* (- y b) ControlSize) s);

:: ControlUnScale ScaleType !INT !INT -> Pnt;
   ControlUnScale ((a,b),s) x y 
     -> (+ a x', + b y'),
        x': / (* x s) ControlSize,
        y': / (* y s) ControlSize;

The feel specifies the control's response to mouse events, and is defined by a function which accepts (in addition to parameters supplied by LookAndFeel) the mouse event and the current control state. The response to the mouse event is a new control state and a graphical change. This control reponds to all mouse events in the same way, by scaling the mouse position (a,b) and taking it as the desired centre for the viewing window. The previous box is erased by redrawing it in XorMode, and a box is drawn at the new position. The net effect is that the black square (representing the viewing window) follows the mouse as long as the mouse button is pressed. The control state is set to the new position with each mouse event. It is important to note that CLEAN always resets the pen to its defaults before applying a draw function list to a control, so the feel must set the pen mode on every event:

:: TheControlFeel ScaleType Pnt !WIDTH !HEIGHT 
            MouseState ControlState -> (ControlState, [DrawFunction]);
   TheControlFeel scale upleft w h 
            ((a,b),button,mods) cs:(PairCS (IntCS x) (IntCS y))
     -> (cs, []), IF = x x' && = y y'
     -> (PairCS (IntCS x') (IntCS y'),
         [SetPenMode XorMode,
          ControlActive scale w h x y,
          ControlActive scale w h x' y',
          SetPenNormal]),
        (x',y'): ControlUnScale scale a b;
   TheControlFeel scale upleft w h ((a,b),button,mods) notactivecs
     -> (PairCS (IntCS x) (IntCS y),
         [SetPenMode XorMode,
          ControlBox scale upleft w h,
          ControlActive scale w h x y,
          SetPenNormal]),
        (x,y): ControlUnScale scale a b;

In addition to the control feel, mouse events occurring inside the control domain also cause the execution of a state transition function on the parent dialog's own state. In this case, a specified button in the dialog is enabled (the GoTo button in Figure 6):

:: TheControlFn DialogItemId DialogInfo (DialogState State IO) 
                                                -> DialogState State IO;
   TheControlFn buttonid dinfo dstate
     -> EnableDialogItems [buttonid] dstate;

We also specify in this module the method for the associated button in the parent dialog. Since the button is only enabled on a mouse event in the control, the control's internal state should have been set to contain a desired window position (x,y). The button alters the window position in the Lifestate, changes the parent dialog by disabling the button, and resets its look to reflect the new state (the actual visible window is not updated by this function, but by the calling program itself). The method function is parameterised on the identity numbers of the parent dialog, the control itself, and the associated button:

:: ControlApplyButton !DialogId !DialogItemId !ControlState 
            !DialogItemId !State !IO -> (!State, !IO);
   ControlApplyButton thedialogid id (PairCS (IntCS x) (IntCS y)) 
            buttonid s:(run,bsize,track,life,files) io 
     -> (s',io'),
        life': CentreAt (x,y) life,
        s': (run, bsize, track, life', files),
        io': ChangeDialog thedialogid 
                [DisableDialogItems [buttonid],
                 ChangeTheControl id run life'] io;
   ControlApplyButton thedialogid id notactivecs buttonid s io
     -> Crash "GoTo button unexpectedly enabled" s io;

Finally the following function resets the control's look, feel, and internal state in response to a change in the RunState or Lifestate:

:: ChangeTheControl !DialogItemId !RunState 
            !Lifestate !(DialogState State IO) -> DialogState State IO;
   ChangeTheControl id run life dstate
     -> DisableDialogItems [id] dstate''', IF run || Dead life
     -> EnableDialogItems [id] dstate''',
        dstate': ChangeControlState id InitCS dstate,
        dstate'': ChangeControlLook id look dstate',
        dstate''': ChangeControlFeel id feel dstate'',
        (look, feel): LookAndFeel life;

The wincontrol definition module (wincontrol.dcl) exports only the definitions of the control and its associated button and update function:

UpTop Level

BackSome Useful GUI Operations

DCLwincontrol.dcl

ForwardThe Final GUI Animation Program