13. The Final GUI Animation Program

We are finally in a position to fit together the concepts from previous sections, and present a GUI-based animation program. It uses several of CLEAN's GUI facilities, as well as our user-defined control:

MODULE winanimate;
IMPORT winhead, wincontrol;
IMPORT deltaWindow, deltaFileSelect, deltaDialog, deltaPicture;
FROM deltaTimer IMPORT EnableTimer, DisableTimer,
                       SetTimerInterval, TicksPerSecond;
FROM deltaMenu IMPORT EnableMenuItems, DisableMenuItems,
                      MarkMenuItems, UnmarkMenuItems,
                      ChangeMenuItemTitles, ChangeMenuItemFunctions;

Macros specify the size of the viewing window, and initial values for various parameters:

MACRO
   WinSize      -> 256;
   WinDomain    -> ((0,0),(WinSize,WinSize));
   InitTrack    -> FALSE; 
   InitSize     -> 16; 
   InitInterval -> 1; 
   InitRun      -> FALSE;
   InitLife     -> Emptystate height height,
                   height: / WinSize InitSize;

The Start rule is similar to that of section 8, but with several GUI facilities (two menus, a window, two dialog boxes, and a timer). The steps of the animation occur at each 'tick' of the timer. The initial state is created using initial parameter values and the initial file system. The final file system is extracted from the final state so that it can be closed. The StartIO function includes an action InitAction which is to be performed immediately after startup:

RULE
:: Start UNQ WORLD   -> UNQ WORLD;
   Start world
     -> world''',
        (filesystem, world'): OpenFiles world,
        (events, world''): OpenEvents world',
        state: (InitRun, InitSize, InitTrack, InitLife, filesystem),

        menu: MenuSystem [FileMenu, LifeMenu],
        window: WindowSystem [TheWindow],
        dialog: DialogSystem [HelpDialog, TheDialog InitRun InitLife],
        timer:  TimerSystem [TheTimer],
        
        (newstate, events'):  StartIO [menu, dialog, window, timer]
                                            state [InitAction] events,
        (run, bsize, track, life, filesystem'): newstate,
        world''': CloseEvents events' (CloseFiles filesystem' world'');

The initial startup action brings the viewing window to the front:

:: InitAction State IO -> (State, IO);
   InitAction s io
     -> (s, ActivateWindow TheWindowId io);

Each GUI facility we use will be defined to have an update function to bring it up-to-date. We define UpdateAll to bring the entire GUI system up-to-date:

:: UpdateTheMenus (State, IO) -> (State, IO);
   UpdateTheMenus pr
     -> UpdateFileMenu (UpdateLifeMenu pr);

:: UpdateAll (State, IO) -> (State, IO);
   UpdateAll pr
     -> UpdateTheMenus (UpdateTheWindow 
                (UpdateTheDialog (UpdateTheTimer pr)));

We first present the definition of the File menu. Each GUI facility of the same type must have a unique numerical identifier, which is 1 for this menu (and 2 for the second menu). The File menu contains items to Load, Store, and save as PostScript a file, as well as Quit. These items all have indices beginning with 1, while those of the second menu all have indices starting with 2. It is often useful to macro-define these indices, but in this case they are only used in the menu definition and the update function immediately following:

:: FileMenu -> MenuDef State IO;
   FileMenu 
     -> PullDownMenu 1 "File" Able [
            MenuItem 11 "Load"  (Key 'L') Able DoLoad,
            MenuItem 12 "Store" (Key 'S') Unable 
                                (DoWrite "Save as:" DoStore),
            MenuItem 13 "PostScript" (Key 'P') Unable
                                (DoWrite "Save as PostScript:" DoPost),
            MenuSeparator,
            MenuItem 14 "Quit"  (Key 'Q') Able DoQuit
        ];

The update function for this menu disables any menu items inappropriate to the current pattern, and also disables file operations in a running animation. The disabled menu items are still visible, but cannot be selected. The use of an update function like this localises the menu item indices to one part of the program.

:: UpdateFileMenu (State, IO) -> (State, IO);
   UpdateFileMenu (s:(run,bsize,track,life,files), io)
     -> (s, DisableMenuItems [11,12,13] io), IF run
     -> (s, EnableMenuItems [11] 
                (DisableMenuItems [12,13] io)), IF Dead life
     -> (s, EnableMenuItems [11,12,13] io);

The method for the Load item verifies the replacement of the current pattern (using the generic Verify operation defined in section 11), and then calls DoLoadAux which performs the file load similarly to section 8:

:: DoLoad State IO -> (State, IO);
   DoLoad s:(run,bsize,track,life,files) io
     -> DoLoadAux s io, IF Dead life
     -> Verify "Do you really want to replace this pattern?" 
                        DoLoadAux s io;

:: DoLoadAux State IO -> (State, IO);
   DoLoadAux s:(run,bsize,track,life,files) io
     -> (s', io'), IF NOT openselected
     -> Error (+S "Can't open file " input) s'' io', IF NOT ok
     -> Error (+S "No cells found in " input) s''' io', IF Dead life'
     -> UpdateAll (news,io'),

        (openselected, input, files', io'): SelectInputFile files io,
        s':(run, bsize, track, life, files'),
        (ok, g, files''): FOpen input FReadText files',
        s'':(run, bsize, track, life, files''),
        (points, g'): ReadPtsList g,
        life': Loadpoint points life,
        (dummy, files'''): FClose g' files'',
        s''':(run, bsize, track, life, files'''),
        news:(FALSE, bsize, track, life', files''');

The DoWrite function implements both Store and PostScript, being parameterised on the prompt to use in the output-file selection dialog box, and the specific file-write function (DoStore or DoPost):

:: DoWrite STRING (=> STRING (=> Lifestate (=> !UNQ FILE UNQ FILE)))
                                                State IO -> (State, IO);
   DoWrite prompt writefn s:(run,bsize,track,life,files) io
     -> (s', io'), IF NOT saveselected
     -> Error (+S "Can't open file " output) s'' io', IF NOT okopen
     -> Error (+S "Can't close file " output) s''' io', IF NOT okclose
     -> (s''', io'),
        
        (saveselected, output, files', io'): 
                SelectOutputFile prompt "" files io,
        s':(run, bsize, track, life, files'),
        (okopen, g, files''): FOpen output FWriteText files',
        s'':(run, bsize, track, life, files''),
        g': writefn output life g,
        (okclose, files'''): FClose g' files'',
        s''':(run, bsize, track, life, files''');

:: DoStore STRING Lifestate !UNQ FILE -> UNQ FILE;
   DoStore fname life f -> WritePtsList (Allpoints life) f;

:: DoPost STRING Lifestate !UNQ FILE -> UNQ FILE;
   DoPost fname life f
     -> WritePS head (Picrect life) (Allpoints life) f,
        head: Concat [fname, "   ", ITOS (Numcells life),
                      " cells   step ", ITOS (Stepno life)];

The method for Quit is built out of two generic operations (Verify and ExtendToState) from section 11:

:: DoQuit -> => State (=> IO (State, IO));
   DoQuit -> Verify "Do you really want to quit?" (ExtendToState QuitIO);

The second menu (with index 2) is the Life menu, which contains items to open the Controls dialog box, alter the Speed of the animation, alter the Size of cells in the viewing window, change the Track mode, Erase the entire pattern, and Run or stop the animation.

The Speed item is a submenu containing radio items (i.e. only one sub-item can be selected at any one time). This submenu specifies the time in seconds (0, 1, 5 or 10) between steps of the animation. It is convenient for the indices of the radio items to be computed from the time interval (as 2200 + t ), so that the radio item which is checked initially depends on the macro-specified initial time interval. The same strategy applies to the Size submenu:

:: LifeMenu -> MenuDef State IO;
   LifeMenu
     -> PullDownMenu 2 "Life" Able [
            MenuItem  21  "Controls"    (Key 'C') Able DoControls,
            SubMenuItem  22  "Speed" Able [
                MenuRadioItems  (+ 2200 InitInterval) [
                    MenuRadioItem 2200 "Fast" (Key 'F') Able (DoSpeed 0),
                    MenuRadioItem 2201 "Medium" (Key 'M') Able 
                                       (DoSpeed TicksPerSecond),
                    MenuRadioItem 2205 "Slow" NoKey Able 
                                       (DoSpeed (* 5 TicksPerSecond)),
                    MenuRadioItem 2210 "Very Slow" NoKey Able 
                                       (DoSpeed (* 10 TicksPerSecond))
                ]
            ],
            SubMenuItem  23  "Size" Able [
                MenuRadioItems  (+ 2300 InitSize) [
                    MenuRadioItem  2304 "4x4" NoKey Able (DoSize 4),
                    MenuRadioItem  2308 "8x8" NoKey Able (DoSize 8),
                    MenuRadioItem  2316 "16x16" NoKey Able (DoSize 16),
                    MenuRadioItem  2332 "32x32" NoKey Able (DoSize 32)
                ]
            ],
            CheckMenuItem 24 "Track" (Key 'T') Able
                (BTOMarkState InitTrack) DoTrack,  
            MenuItem 25 "Erase" (Key 'E') Unable
               (Verify "Do you really want to erase all cells?" DoErase),
            MenuItem 26 "Run" (Key 'R') Unable DoRun
        ];

The update function for the Life menu reflects the fact that a dead pattern cannot be run or erased, and a stable pattern cannot be run. More importantly, when the animation is running, the Run menu item is altered to Halt, with a corresponding change to the method function:

:: UpdateLifeMenu (State, IO) -> (State, IO);
   UpdateLifeMenu (s:(run,bsize,track,life,files), io)
     -> (s, DisableMenuItems [25] (EnableMenuItems [26] 
                (ChangeMenuItemTitles [(26,"Halt")]
                    (ChangeMenuItemFunctions [(26,DoHalt)] io)))), IF run
     -> (s, DisableMenuItems [25, 26] haltedio), IF Dead life
     -> (s, EnableMenuItems [25] 
                (DisableMenuItems [26] haltedio)), IF Stable life
     -> (s, EnableMenuItems [25, 26] haltedio),
        haltedio: ChangeMenuItemTitles [(26,"Run")]
                        (ChangeMenuItemFunctions [(26,DoRun)] io);

The Controls menu item opens the main dialog box (if it is not already open). The predefined GetDialogInfo function is used to indicate whether the dialog box is already open:

:: DoControls State IO -> (State, IO);
   DoControls s:(run,bsize,track,life,files) io
     -> (s, Beep io'), IF isopen
     -> (s, OpenDialog (TheDialog run life) io'),
        (isopen, dummyinf, io'): GetDialogInfo TheDialogId io;

The radio items in the Speed submenu alter the interval between 'ticks' of the timer. Those in the Size submenu alter the cell size in the viewing window, requiring update of the user state. The main dialog box and window are then updated to match:

:: DoSpeed !INT State IO -> (State, IO);
   DoSpeed interval s io
     -> (s, SetTimerInterval TheTimerId interval io);

:: DoSize !BlockSize State IO -> (State, IO);
   DoSize newsiz s:(run,bsize,track,life,files) io
     -> UpdateTheWindow (UpdateTheDialog (s',io)),
        height: / WinSize newsiz,
        life': Resize height height life,
        s': (run, newsiz, track, life', files);

The Track menu item toggles the tracking mode, which is recorded in the user state. The check-mark beside it in the menu is then adjusted to match. Unlike the marks beside radio items, those in a CheckMenuItem must be set and cleared explicitly:

:: DoTrack State IO -> (State, IO);
   DoTrack s:(run,bsize,track,life,files) io
     -> ((run,bsize,FALSE,life,files), UnmarkMenuItems [24] io), IF track
     -> ((run,bsize,TRUE,life,files), MarkMenuItems [24] io);

The Erase menu item stops the animation and and erases the current pattern. This requires updating the entire GUI system. The definition of the Life menu above only calls DoErase if the users confirms a verification notice.

:: DoErase State IO -> (State, IO);
   DoErase s:(run,bsize,track,life,files) io
     -> UpdateAll (s',io),
        s': (FALSE, bsize, track, Loadpoint [] life, files);

The last item in the menu calls either DoRun or DoHalt, depending on whether the animation is halted or running. The DoRun method enables the timer, disables mouse events in the viewing window (indicating this by altering the cursor icon locally within the window), sets the running flag in the user state, and updates the menus and main dialog box to match. The DoHalt method reverses all these changes. Figure 7 shows the viewing window and main dialog box while the animation is running.

:: DoRun State IO -> (State, IO);
   DoRun s:(run,bsize,track,life,files) io 
     -> UpdateTheMenus (UpdateTheDialog (s',io')),
        s': (TRUE, bsize, track, life, files),
        io': DisableMouse TheWindowId 
                (ChangeWindowCursor TheWindowId BusyCursor
                        (EnableTimer TheTimerId io));

:: DoHalt State IO -> (State, IO);
   DoHalt s:(run,bsize,track,life,files) io 
     -> UpdateTheMenus (UpdateTheDialog (s',io')),
        s': (FALSE, bsize, track, life, files),
        io': EnableMouse TheWindowId
                (ChangeWindowCursor TheWindowId CrossCursor
                        (DisableTimer TheTimerId io));

The animation is controlled by a timer device. Different timers must have different indices, but may have the same index as menus, windows, etc. (and similarly for menu, dialog-box, and window indices). The index is macro-defined to permit easy enabling and disabling of the timer elsewhere in the program:

MACRO
   TheTimerId -> 1;

RULE
:: TheTimer -> TimerDef State IO;
   TheTimer 
     -> Timer TheTimerId Unable (* InitInterval TicksPerSecond) ClockFn;

The update function for the timer disables the timer if the animation has stopped. The ClockFn method (which is called on each 'tick' of the timer) performs one animation step and updates the entire GUI interface to match the result:

:: UpdateTheTimer (State, IO) -> (State, IO);
   UpdateTheTimer (s:(run,bsize,track,life,files), io)
     -> (s, EnableTimer TheTimerId io), IF run
     -> (s, DisableTimer TheTimerId io);

:: ClockFn TimerState State IO -> (State, IO);
   ClockFn tstate s:(run,bsize,track,life,files) io
     -> UpdateAll (s',io),
        life': Trackstep track life,
        run': NOT (Stable life'),
        s': (run', bsize, track, life', files);

The main dialog box is a command dialog (the most general case) which is opened at a specific position on the screen with default button 108. It contains the user-defined control described in section 12, and information on the current pattern (in dynamic text entries which can be changed by appropriate functions). The dialog box is shown in Figure 6. There are three dialog buttons: a default GoTo button associated with the control, and Run and Halt buttons which start and stop the animation in the same way as the corresponding menu items. The definition of the dialog box is parameterised on the current RunState and LifeState at the time it is opened, and this is used to determine which button should be initially enabled:

MACRO
   TheDialogId -> 1;

RULE
:: TheDialog RunState Lifestate -> DialogDef State IO;
   TheDialog run life
     -> CommandDialog TheDialogId "LIFE Control"
           [DialogPos (Pixel 270) (Pixel 10),
            StandByDialog,
            DialogMargin (Pixel 6) (Pixel 6)]
           108  == default button
           [TheControl 101 Left run life 108,
            StaticText 102 (RightTo 101) "Step: ",
            DynamicText 103 (RightTo 102) (Pixel 60)
                (ITOS (Stepno life)),
            StaticText 104 (Below 102) "Cells: ",
            DynamicText 105 (RightTo 104) (Pixel 60)
                (ITOS (Numcells life)),
            StaticText 106 (Below 104) "Status: ",
            DynamicText 107 (RightTo 106) (Pixel 60)
                (ShStatus run life),
            DialogButton 108 (Below 101) "GoTo" Unable GoToButton,
            DialogButton 109 (RightTo 108) "Run"
                (BTOSelectState (NOT (OR run (Stable life)))) RunButton,
            DialogButton 110 (RightTo 109) "Halt"
                (BTOSelectState run) HaltButton
           ];

:: ShStatus RunState Lifestate -> STRING;
   ShStatus TRUE life -> "Running";
   ShStatus FALSE life -> "Dead", IF Dead life
                       -> "Stable", IF Stable life
                       -> "Stopped";

The update function uses ChangeDialog and a list of change functions to update the dynamic text entries, the control, and the buttons:

:: UpdateTheDialog (State, IO) -> (State, IO);
   UpdateTheDialog (s:(run,bsize,track,life,files), io)
     -> (s,io'), IF NOT isopen
     -> (s,ChangeDialog TheDialogId [d1,d2,d3,d4,d5,drun] io'), IF run
     -> (s,ChangeDialog TheDialogId [d1,d2,d3,d4,d5] io'), IF Stable life
     -> (s,ChangeDialog TheDialogId [d1,d2,d3,d4,d5,dstop] io'),
        (isopen, dummyinf, io'): GetDialogInfo TheDialogId io,
        d1: ChangeDynamicText 103 (ITOS (Stepno life)),
        d2: ChangeDynamicText 105 (ITOS (Numcells life)),
        d3: ChangeDynamicText 107 (ShStatus run life),
        d4: DisableDialogItems [108,109,110],
        d5: ChangeTheControl 101 run life,
        drun: EnableDialogItems [110],
        dstop: EnableDialogItems [109];

The methods for the Run and Halt buttons are based on the corresponding menu items, while that of the GoTo button is based on the ControlApplyButton function of section 12. The control state and appropriate button indices are fed to this function, and the window is updated to reflect the changes to window position it makes in the LifeState:

:: RunButton DialogInfo State IO -> (State, IO);
   RunButton dinfo s io -> DoRun s io;

:: HaltButton DialogInfo State IO -> (State, IO);
   HaltButton dinfo  s io -> DoHalt s io;

:: GoToButton DialogInfo State IO -> (State, IO);
   GoToButton dinfo s io
     -> UpdateTheWindow (ControlApplyButton TheDialogId 101 cs 108 s io),
        cs: GetControlState 101 dinfo;

The viewing window allows for mouse but not keyboard events. Clicking the window's 'go-away' box has the same action as the Quit menu item. The standby facility ensures that clicking on an inactive window not only activates the window, but performs the usual action as well. This feature ensures that, although the window and dialog box cannot be active at the same time, they behave as if they can. Within the window, the cursor is by default a cross instead of its usual shape (although this is altered when the animation is running):

MACRO
   TheWindowId  -> 1;
   TheWindowPos -> (5,5);

RULE
:: TheWindow -> WindowDef State IO;
   TheWindow
     -> FixedWindow TheWindowId TheWindowPos "LIFE" WinDomain LifeUpdate 
            [Mouse Able LifeMouse, 
             GoAway DoQuit,
             Cursor CrossCursor,
             StandByWindow];

The update function redraws the entire window, adjusts the cursor, and enables or disables mouse events:

:: UpdateTheWindow (State, IO) -> (State, IO);
   UpdateTheWindow (s:(run,bsize,track,life,files), io)
     -> (s', ChangeWindowCursor TheWindowId BusyCursor 
                (DisableMouse TheWindowId io')), IF run
     -> (s', ChangeWindowCursor TheWindowId CrossCursor 
                (EnableMouse TheWindowId io')),
        (s', drawlist): LifeUpdate [WinDomain] s,
        io': DrawInWindow TheWindowId 
                [EraseRectangle WinDomain | drawlist] io;

Every window definition must contain a function like LifeUpdate which takes an update area (a list of rectangles) and a user state, and computes a list of draw functions. If portions of a window are obscured and then uncovered, CLEAN erases the affected rectangles and applies the draw functions computed by this function (cropping to fit the rectangles). The LifeUpdate function can also be used explicitly when we wish to redraw the window (as in UpdateTheWindow above).

The draw functions returned by LifeUpdate completely ignore the list of rectangles, and draw a circle for each live cell in the viewing window:

:: LifeUpdate UpdateArea State -> (State, [DrawFunction]);
   LifeUpdate area s:(run,bsize,track,life,files) 
     -> (s, Map (DrawCell bsize) (Croppedpoints life));
   
:: CellCircle BlockSize Pnt -> Circle;
   CellCircle bsize (x,y)
     -> ((x',y'),radius),
        radius: Max 1 (/ (* bsize 8) 20),
        half: Half bsize,
        x': + (* x bsize) half,
        y': + (* y bsize) half;

:: DrawCell BlockSize Pnt -> DrawFunction;
   DrawCell bsize pnt
     -> FillCircle (CellCircle bsize pnt);

The method for mouse events is LifeMouse. When the mouse button is clicked inside the window, the state of the selected cell is toggled (and it is inverted on the screen). To avoid inverting twice, it is important that there be no response to ButtonStillDown or ButtonUp events.

:: LifeMouse MouseState State IO -> (State, IO);
   LifeMouse ((x,y), ButtonDown, mods) s:(run,bsize,track,life,files) io 
     -> UpdateTheMenus (UpdateTheDialog (s', io')),
        life': Changepoint RELATIVE (x',y') life,
        s': (run, bsize, track, life', files),
        io': DrawInActiveWindow [InvertCircle (CellCircle bsize (x',y'))] io,
        x': / x bsize,
        y': / y bsize;
   LifeMouse othermouse s io -> (s, io);

Finally CLEAN allows each application to have a special about-dialog, which is accessed in a system-dependent way (on the Macintosh it is selected from the Apple menu). This is a simple dialog box in which any desired graphics can be drawn, with an automatic OK button, and possibly also a Help button. It has no numerical index. In our case we use the DrawText function from section 11 to provide the contents of the dialog box, which is shown in Figure 8.

MACRO
   HelpDomain -> ((0,0),(300,100));

RULE
:: HelpDialog -> DialogDef State IO;
   HelpDialog
     -> AboutDialog "LIFE" HelpDomain 
            [DrawText HelpDomain "Palatino" ["Italic","Bold"] 14 (10,10)
                ["This program was written by A. H. Dekker",
                 "to demonstrate CLEAN window facilities"]
            ]
            (AboutHelp "Help" DoHelp);

The method for the Help button opens a help file, reads it, and displays the contents in a text window (using the TextWindow definition from section 11). The resulting window is shown in Figure 5.

MACRO
   HelpFileName -> "life.help";
   HelpWindowId -> 2;
   HelpWindowPos -> (50,20);
   HelpWindowSize -> (400,200);

RULE
:: DoHelp State IO -> (State, IO);
   DoHelp s:(run,bsize,track,life,files) io
     -> Error (+S "Can't open help file " HelpFileName) s' io, IF NOT ok
     -> (s'', OpenWindows [helpwin] io),
        (ok, g, files'): FOpen HelpFileName FReadText files,
        s':(run, bsize, track, life, files'),
        (text, g'): ReadText g,
        (dummy, files''): FClose g' files',
        s'':(run, bsize, track, life, files''),
        helpwin: TextWindow HelpWindowId HelpWindowPos "HELP" 
                        HelpWindowSize "Courier" [] 10 text;
                                             
:: ReadText !UNQ FILE -> ([STRING], !UNQ FILE);
   ReadText f 
     -> ([], f), IF SFEnd f
     -> ([s|rest], f''),
        (s, f'): FReadLine f,
        (rest,f''): ReadText f';        

UpTop Level

BackA User-Defined Control

DCLHelp File

ForwardConclusion