Simple UI Applications

This tutorial aims to introduce and illustrate the fundamental concepts of the UI library by creating the starting point of an application that visualizes chess games. The code produced in this tutorial is available in the directory root/tutorials/ui in the release of Storm. You can run it by typing tutorials:ui:main in the Basic Storm interactive top-loop.

Setup

Before we start writing code, we need somewhere to work. For this tutorial, we need to create a directory with an appropriate name. In this case we will name it ui. Inside the directory, we also need a .bs file that will contain the code for the application. We will also need a subdirectory for resources (images in this case). We will name this directory res. In our case, we will place images of chess pieces in the res directory. These can be copied from the root/tutorials/ui/res directory in the Storm release. The naming convention of the images is as follows. The first character indicates the type of the piece, and the second character indicates the color of the piece. This convention will be used to find the appropriate pieces later on.

The resulting directory structure will look as follows:

ui
├─ chess.bs
└─ res
   ├─ bb.png
   ├─ bw.png
   ├─ kb.png
   ├─ kw.png
   ├─ nb.png
   ├─ nw.png
   ├─ pb.png
   ├─ pw.png
   ├─ qb.png
   ├─ qw.png
   ├─ rb.png
   └─ rw.png

Creating a Frame

In the first part of the tutorial we will use the UI library to create a frame that will serve as the base window for the actual graphical application. Inside the frame, we will create another window where we will draw the chessboard.

The first step is to create the frame, which is a window that has borders and can contain other windows. In order to do so we need to create a subclass to Frame, which we will call ChessVisualizer. To initialize the Frame, we need to provide a title and an initial size as shown below.

use ui;
use core:geometry;

class ChessVisualizer extends Frame {
    init() {
        init("ChessVisualizer", Size(500, 500)) {}
        create();
    }
}

As seen above, we need to tell the library when we are done initializing a frame by calling the create function. Without this call, the window will not be shown. To avoid flickering, this is typically done last, after creating any components. Note that this is only needed for frames. Other windows are created lazily when they are attached to a parent, or when the parent is created.

To run the code, we also need a main function that creates our frame. It can be implemented as follows:

void main() {
    ChessVisualizer cv;
    cv.waitForClose();
}

In the code above, we simply create an instance of our frame. Since the constructor calls create, we do not need to do anything else for it to be shown on the screen. We do, however, need to call waitForClose. Without this call, the main function would return almost immediately, and Storm would shut down before we have a chance of interacting with the window.

At this point we can test the program by running the following in a terminal:

storm ui

The UI library also provides a syntax extension to Basic Storm to make it more convenient to work with windows. In particular, this syntax allows specifying layout using the layout DSL. This does, however, require that we define the ClassVisualizer class using the keyword frame instead of as above. This lets the syntax extension know that we are working with a frame, and that it should be possible to specify layout. As such, we change our code into the following. Note that we do not have to add extends Frame anymore, as that is implied by the frame keyword.

use ui;
use core:geometry;

frame ChessVisualizer {
    init() {
        init("ChessVisualizer", Size(500, 500)) {}
        create();
    }
}

void main() {
    ChessVisualizer cv;
    cv.waitForClose();
}

Creating and Drawing to the Window

Now that we have a basic frame to work with, the next step is to create a window inside the frame where we can draw the actual chess board. This is done by adding a layout to the ChessVisualizer class. In this particular case we keep it as minimal as possible in the beginning and simply add our inner window to a grid layout. We also specify a minimum height and width in the DSL of the layout library. We will expand further on the layout functionality as we keep adding things to the grid.

frame ChessVisualizer {
    layout Grid {
        Window chessBoard { minHeight: 500; minWidth: 500; }
    }

    init() {
        init("ChessVisualizer", Size(500, 500)) {}
        create();
    }
}

In order to draw to the window, we need to create a subclass of the painter class from the graphics library, and then attach the painter to the window. The simplest possible custom painter class is one that fills the window with a solid white color. It is created by giving the painter an empty init function and a render function that simply returns false as can be seen below.

class ChessPainter extends Painter {
    init() {
        init() {}
    }

    Bool render(Size me, Graphics g) {      
        return false; 
    }
}

Integrating the painter with the chessBoard window creates a frame with a white square inside of it. The complete code to get this frame up and running is seen below:

use ui;
use graphics;
use core:geometry;

frame ChessVisualizer {
    layout Grid {
        Window chessBoard { minHeight: 500; minWidth: 500; }
    }
    
    ChessPainter boardPainter;
    
    init() {
        init("ChessVisualizer", Size(500, 500)) {}
        chessBoard.painter(boardPainter);   
        create();
    }
}

class ChessPainter extends Painter {
    init() {
        init() {
        }
    }

    Bool render(Size me, Graphics g) {      
        return false; 
    }
}

void main(){
    ChessVisualizer cv;
    cv.waitForClose();
}

As before, we can run the program by typing the following in a terminal:

storm ui

The next problem to tackle is to turn the chessBoard window into something that actually looks like a chessboard and not just a white square. In order to do so we will set an appropriate background color for the playing field that will represent the light squares, and draw the dark squares on top of the background. We will also draw the labels commonly found on chess boards that indicate the file and rank. Both of these tasks are achieved by extending the functionality of our ChessPainter class.

To set the background color of the window, we can simply set the bgColor variable variable to the desired color in the init function of the class. In order to draw the dark squares however, we need to create a brush that the painter can use. We will use a so called SolidBrush, which can be initialized either using RGB values or one of the predefined colors available from the graphics library. We choose to store it as a member to the class so that we can reuse it each time the render function is called, rather than having to create it from scratch each time. This is particularly important for larger resources, such as images, to avoid transferring large resources to the GPU on each frame. More information about this can be found in the rendering reference, specifically the resources section.

class ChessPainter extends Painter {
    SolidBrush darkBrush;
    
    init() {
        init() {
            darkBrush(Color(118, 150, 86));

        }
        bgColor = Color(238, 238, 210);     
    }
    
    Bool render(Size me, Graphics g) {              
        return false; 
    }
}

In order to actually draw things to the window we need to extend the functionality of the render function. The parameters that are available to us in the render function are firstly, a parameter that represent the current size of the window we are drawing to, and secondly a ui.Graphics object that allows us to draw to the window. The graphics object has a number of members that can be used to draw to the window. For this part we will use the fill function that fills a rectangle using a specific brush.

If we look specifically at the problem of drawing a chess board we know that it is an 8x8 grid. The squares are light and dark in an alternating pattern, and the top left square is light. As such, if we divide the window into 64 parts and step through them we can place a rectangle at every other step in order to fill in the dark squares. Put another way, loop through an 8x8 grid and draw a dark square when the manhattan distance from top left is an odd number. To allow the user to resize the window, we will use the Size parameter to compute the size and position of the square. However, at this point the contents of the window is not actually resized. We will fix that later in the tutorial.

The below code draws a grid of light and dark squares in the window within the frame:

class ChessPainter extends Painter {
    SolidBrush darkBrush;
    
    init() {
        init() {
            darkBrush(Color(118, 150, 86));

        }
        bgColor = Color(238, 238, 210);     
    }
    
    Bool render(Size me, Graphics g) {      
        Size squareSz = me / 8;

        for (Nat y = 0; y < 8; y++) {
            for (Nat x = 0; x < 8; x++) {
                if((x + y) % 2 == 1) {
                    g.fill(Rect(Point(squareSz.w * x.float, squareSz.h * y.float), squareSz), darkBrush);
                }
            }
        }
        
        return false; 
    }
}

Drawing Text to the Window

Adding the labels to the chess board follows a similar procedure. We do however use the draw member in the graphics object instead of fill. To add the labels we will again have to modify the painter class a bit. In this case we want black text, so we add another SolidBrush for this purpose:

SolidBrush blackBrush;
blackBrush(black);

Formatted text is also similar to brushes in that we want to create them once and then re-use them where possible. This is to avoid computing the layout of the text each time we draw the window. As such, we store the labels in two arrays, one for row labels and one for column labels. We populate them in the init function with the appropriate text as follows:

Text[] rowLabels;
Text[] colLabels;

for (row in ["8", "7", "6", "5", "4", "3", "2", "1"]) {
    rowLabels << Text(row, defaultFont);
}

for (col in ["a", "b", "c", "d", "e", "f", "g", "h"]) {
    colLabels << Text(col, defaultFont);
}

We then iterate through them in the render function and use the loop variable as well as the squareSz variable to tell the draw function the location of the text. The location is relative to the top-left corner of the text. As can be seen below, the Text object contains the size of the laid-out text as the size member.

for (i, rowLabel in rowLabels) {
    g.draw(rowLabel, blackBrush, Point(0, squareSz.h * i.float));
}

for (i, colLabel in colLabels) {
    g.draw(colLabel, blackBrush, Point(squareSz.w * (i + 1).float - 1, me.h) - colLabel.size);
}

Putting it all together results in the below code that draws the chessboard with the labels in the window inside the frame:

class ChessPainter extends Painter {
    SolidBrush darkBrush;
    SolidBrush blackBrush;

    Text[] rowLabels;
    Text[] colLabels;
    
    init() {
        init() {
            blackBrush(black);
            darkBrush(Color(118, 150, 86));
        }
        
        bgColor = Color(238, 238, 210);
        
        for (row in ["8", "7", "6", "5", "4", "3", "2", "1"]) {
            rowLabels << Text(row, defaultFont);
        }
        
        for (col in ["a", "b", "c", "d", "e", "f", "g", "h"]) {
            colLabels << Text(col, defaultFont);
        }
    }
    
    Bool render(Size me, Graphics g) {      
        Size squareSz = me / 8;
        
        for (Nat y = 0; y < 8; y++) {
            for (Nat x = 0; x < 8; x++) {
                if ((x + y) % 2 == 1) {
                    g.fill(Rect(Point(squareSz.w * x.float, squareSz.h * y.float), squareSz), darkBrush);
                }
            }
        }

        for (i, rowLabel in rowLabels) {
            g.draw(rowLabel, blackBrush, Point(0, squareSz.h * i.float));
        }
        
        for (i, colLabel in colLabels) {
            g.draw(colLabel, blackBrush, Point(squareSz.w * (i + 1).float - 1, me.h) - colLabel.size);
        }
        
        return false; 
    }
}

Drawing Images to the Window

In this second part of the tutorial we will look at how we can load external resources into our program and then draw them to a window. This will be illustrated by loading png images of the various chess pieces and drawing them to a window. Since this tutorial is aimed at illustrating the UI librarys functionality the implementation will be delimited to simply drawing the pieces at their starting positions. There will be no data structure for the pieces themselves and there will be no possibility to move them.

For this part of the tutorial we assume that the images are placed in a subdirectory called res, as described in the top of this page. Furthermore we will assume you are familiar with the information presented in the files and streams tutorial since it will be directly relevant to what we are doing in this part of this tutorial.

We will also use the resUrl function from the files and streams tutorial to conveniently get the path of the files. In this tutorial, we will use a slightly modified version of the function that simply returns an Url to the res directory, rather than to a file inside the directory:

Url resUrl() {
    if (url = named{res}.url) {
        return url;
    } else {
        throw InternalError("Expected the package 'res' to be non-virtual.");
    }
}

If you do not wish to copy the files from the Storm release, you can replace named{res} above with named{tutorials:ui:res} to use the files in the Storm distribution instead.

In order to load an image from file into our program we will make use of the loadImage function. Looking at the function we see that the parameter is a Url object representing a path to the file and that it returns an graphics.Image. Unfortunately the draw function in the Graphics object expects a ui.Bitmap not an Image. As such, we need to convert the Image to a Bitmap before we can draw it. The reason for this is that a Bitmap is a resource that is stored on the GPU ready to be rendered, while an Image is always in the RAM of the CPU. For easy access, we store the Bitmaps in a Map in the ChessPainter class. The key is the filename of the image, and the value is the Bitmap that we can draw. Conveniently, we can use the title member of the Url object to retrieve the name of each file without the file extension.

As such, we add the following member to the ChessPainter:

Str->Bitmap images;

We then load the images in the end of the constructor as follows:

Url imgPath = resUrl(); 
for (child in imgPath.children()) {
    images.put(child.title, Bitmap(child.loadImage));
}

Drawing the images to the window is similar to what we have done in the previous parts. We modify the render function of our painter class to call the draw function from the Graphics class to draw the Bitmap. We also provide a rectangle to tell it that it should stretch the bitmap to fit the provided rectangle. There are a number of overloads of the draw function for drawing bitmaps for other situations as well. For example, if we wished to avoid scaling alltogether. As previously stated the full functionality of a chess board will not be implemented, so we will simply draw the pieces in their correct starting places. To do this, we first create a member variable order that contains the order of the pieces:

Str[] order;

We initialize the array with the correct values inside the init block of the init function:

order = ["r", "n", "b", "q", "k", "b", "n", "r"];

In the render function of the ChessPainter class we then iterate throgh the columns of the chess board and draw the appropriate images in the top and bottom rows. We use the index of the loop to get the correct piece and append 'b' or 'w' depending on if it is the black or white piece. This string is then used to fetch the correct Bitmap from the images container. These are then drawn to the screen in a Rect using the Graphics objects draw function similarly to the previous steps.

for (Nat x = 0; x < 8; x++) {
    Str blackPiece = order[x] + "b";
    g.draw(images.get("pb"), Rect(Point(squareSz.w * x.float, squareSz.h), squareSz));
    g.draw(images.get(blackPiece), Rect(Point(squareSz.w * x.float, squareSz.h * 0), squareSz));

    Str whitePiece = order[x] + "w";
    g.draw(images.get("pw"), Rect(Point(squareSz.w * x.float, squareSz.h * 6), squareSz));
    g.draw(images.get(whitePiece), Rect(Point(squareSz.w * x.float, squareSz.h * 7), squareSz));
}

Putting it all together results in the below code that draws the chessboard with the images of the pieces on their starting positions:

use ui;
use graphics;
use core:io;
use core:geometry;
use lang:bs:macro;

frame ChessVisualizer {
    layout Grid grid {
        Window chessBoard { minHeight: 500; minWidth: 500; }

    }
    
    ChessPainter boardPainter;
    
    init() {
        init("ChessVisualizer", Size(500, 500)) {}
        chessBoard.painter(boardPainter);

        create();
    }

}
    
class ChessPainter extends Painter {
    SolidBrush darkBrush;
    SolidBrush blackBrush;

    Text[] rowLabels;
    Text[] colLabels;

    Str[] order;

    Str->Bitmap images;

    init() {
        init() {
            blackBrush(black);
            darkBrush(Color(118, 150, 86));
            order = ["r", "n", "b", "q", "k", "b", "n", "r"];
        }
        
        bgColor = Color(238, 238, 210);
        
        for (row in ["8", "7", "6", "5", "4", "3", "2", "1"]) {
            rowLabels << Text(row, defaultFont);
        }
        
        for (col in ["a", "b", "c", "d", "e", "f", "g", "h"]) {
            colLabels << Text(col, defaultFont);
        }

        Url imgPath = resUrl();
        for (child in imgPath.children()) {
            print(child.title);
            images.put(child.title, Bitmap(child.loadImage));
        }
    }
    
    Bool render(Size me, Graphics g) {      
        Size squareSz = me / 8;
        
        for (Nat y = 0; y < 8; y++) {
            for (Nat x = 0; x < 8; x++) {
                if((x + y) % 2 == 1) {
                    g.fill(Rect(Point(squareSz.w * x.float, squareSz.h * y.float), squareSz), darkBrush);
                }
            }
        }

        for (i, rowLabel in rowLabels) {
            g.draw(rowLabel, blackBrush, Point(0, squareSz.h * i.float));
        }
        
        for (i, colLabel in colLabels) {
            g.draw(colLabel, blackBrush, Point(squareSz.w * (i + 1).float - 1, me.h) - colLabel.size);
        }

        for (Nat x = 0; x < 8; x++) {
            Str blackPiece = order[x] + "b";
            g.draw(images.get("pb"), Rect(Point(squareSz.w * x.float, squareSz.h), squareSz));
            g.draw(images.get(blackPiece), Rect(Point(squareSz.w * x.float, squareSz.h * 0), squareSz));

            Str whitePiece = order[x] + "w";
            g.draw(images.get("pw"), Rect(Point(squareSz.w * x.float, squareSz.h * 6), squareSz));
            g.draw(images.get(whitePiece), Rect(Point(squareSz.w * x.float, squareSz.h * 7), squareSz));
        }
        
        return false; 
    }
}

Url resUrl() {
    if (url = named{res}.url) {
        return url;
    } else {
        throw InternalError("Expected the package 'res' to be non-virtual.");
    }
}

void main(){
    ChessVisualizer cv;
    cv.waitForClose();
}

Adding a ListView to the Window

Now that we can draw the chess board, it would also be helpful if we could add a UI element that shows the moves made in Portable Game Notation (PGN) that is frequently used to record chess games. In order to do so we want a scrollable list with three of columns, and the ability to highlight a row to indicate the current move. Thankfully there is such a thing, called ui.ListView.

As mentioned before, we will focus on the UI elements in this tutorial, so we will use this example to illustrate how a ListView works without actually implementing the chess moves themselves on the board. This will simply show how to set up a listview and how to manipulate it.

Adding a ListView to our frame is straight forward, we add it to the grid layout as we did with our Window previously. Here we initialize the ListView with the column headers "Nr", "White" and "Black". We also specify a minimum height and width that fits with our other window.

layout Grid {
    Window chessBoard { minHeight: 500; minWidth: 500; }
    ListView moveList(["Nr", "White", "Black"]) { minHeight: 500; minWidth: 150;}
}

A real application should include the ability to load games. However, to keep the tutorial simple we will simply add moves from a specific game manually. The behaviour we want from the ListView is that the user should only be able to select one move at a time and that the first move should be highlighted by default. ListViews have a member add that adds a single row to the ListView. It also has the member multiSelect that can be used to enable/disable the multiselect feature of the ListView. Finally, it has a member selection that controls which row(s) are currently selected and highlighted. Thus we use them to initialize the ListView with the correct settings in the init function of the ChessVisualizer class in the manner as shown below:

frame ChessVisualizer {
    layout Grid grid {
        Window chessBoard { minHeight: 500; minWidth: 500; }
        ListView moveList(["Nr", "White", "Black"]) { minHeight: 500; minWidth: 150;}
    }
    
    ChessPainter boardPainter;
    
    init() {
        init("ChessVisualizer", Size(500, 500)) {}
        chessBoard.painter(boardPainter);

        moveList.add(["1." , "e4"   , "c6"]);
        moveList.add(["2." , "d4"   , "d5"]);
        moveList.add(["3." , "Nd2"  , "dxe4"]);
        moveList.add(["4." , "Nxe4" , "Nd7"]);
        moveList.add(["5." , "Ng5"  , "Ngf6"]);
        moveList.add(["6." , "Bc4"  , "e6"]);
        moveList.add(["7." , "Qe2"  , "Nb6"]);
        moveList.add(["8." , "Bb3"  , "h6"]);
        moveList.add(["9." , "N5f3" , "c5"]);
        moveList.add(["10.", "Bf4"  , "Bd6"]);
        moveList.add(["11.", "Bg3"  , "Qe7"]);
        moveList.add(["12.", "dxc5" , "Bxc5"]);
        moveList.add(["13.", "Ne5"  , "Bd7"]);
        moveList.add(["14.", "Ngf3" , "Nh5"]);
        moveList.add(["15.", "O-O-O", "Nxg3"]);
        moveList.add(["16.", "hxg3" , "O-O-O"]);
        moveList.add(["17.", "Rh5"  , "Be8"]);
        moveList.add(["18.", "Rxd8+", "Kxd8"]);
        moveList.add(["19.", "Qd2+" , "Bd6"]);
        moveList.add(["20.", "Nd3"  , "Qc7"]);
        moveList.add(["21.", "g4"   , "Kc8"]);
        moveList.add(["22.", "g5"   , "Bf8"]);
        moveList.add(["23.", "Rh4"  , "Kb8"]);
        moveList.add(["24.", "a4"   , "Be7"]);
        moveList.add(["25.", "a5"   , "Nd5"]);
        moveList.add(["26.", "Kb1"  , "Bd8"]);
        moveList.add(["27.", "a6"   , "Qa5"]);
        moveList.add(["28.", "Qe2"  , "Nb6"]);
        moveList.add(["29.", "axb7" , "Bxg5"]);
        moveList.add(["30.", "Nxg5" , "Qxg5"]);
        moveList.add(["31.", "Rh5"  , "Qf6"]);
        moveList.add(["32.", "Ra5"  , "Bc6"]);
        moveList.add(["33.", "Nc5"  , "Bxb7"]);
        moveList.add(["34.", "Nxb7" , "Kxb7"]);
        moveList.add(["35.", "Qa6+" , "Kc6"]);
        moveList.add(["36.", "Ba4+" , "Kd6"]);
        moveList.add(["37.", "Qd3+" , "Nd5"]);
        moveList.add(["38.", "Qg3+" , "Qe5"]);
        moveList.add(["39.", "Qa3+" , "Kc7"]);
        moveList.add(["40.", "Qc5+" , "Kd8"]);
        moveList.add(["41.", "Rxa7" , "1-0"]);

        moveList.multiSelect = false;
        moveList.selection = 0;

        create();
    }
}

The program now shows the chessboard to the left and a scrollable ListView of the moves of the game to the right. The ListView supports functionality for scrolling, clicks and moving up and down with the arrow keys out of the box. The next step illustrates how it is possible to manipulate the selection variable by using separate buttons for the user's convenience.

Adding buttons to a Window

In this step we will add 4 buttons to the window. One moves to the first row of the ListView, one moves to the last row, one that moves forward one row, and one that moves backwards one row. To achieve this, we need to consider two things. First, how the buttons will be placed in the frame, and second how to respond to button click events.

We choose to place the 4 buttons under the window that represents the chessboard. As such the grid will be divided into 5 columns and two rows. The chessboard will span the 4 leftmost columns of the first row. The ListView will span the last column in the first row. The buttons will each have one of the columns in order from the left in the second row. Additionally we want to ensure that everything scales with the window when it resized. The exception is that we wish that the buttons are always the same height regardless of the window size.

To achieve this, we first specify that columns 0-4 should be expanded to fit the available width. Similarly, we also specify that column 0 should expand to fit the available height. We then update the lines that initialize the chessBoard and moveList. We specify that the chessBoard should span 4 columns by adding colspan: 4;. We then place the moveList in the appropriate column by adding col: 4;. Otherwise, it would end up in column 1, partially obscuring the chess board. Finally, we add a nextLine; statement so that the four buttons will automatically be positioned in row 1, columns 0-3.

layout Grid {
    expandCol: 0;
    expandCol: 1;
    expandCol: 2;
    expandCol: 3;
    expandCol: 4;
    expandRow: 0;
    Window chessBoard { colspan: 4; minHeight: 500; minWidth: 500; }
    ListView moveList(["Nr", "White", "Black"]) { col: 4; minHeight: 500; minWidth: 150;}
    nextLine;
    Button a("|<"){}
    Button b("<") {}
    Button c(">") {}
    Button d(">|"){}
}

With this change it is now be possible to resize the window with all the UI elements automatically resizing themselves. Our rendered game board also adapts to the window since we used the Size variable to determine the size and placement of various things in the render function.

What remains is to associate the buttons with logic in our programs. This is achieved by setting the onClick member to a function of our choice. As such, we first need to define functions that modify the selection in the ListView. These will be implemented as member functions in the ChessVisualizer class.

One small note is that the following functions maintain a member variable that represents the currently hightlighted row. This is not strictly necessary since we could ask the ListView using the selection member directly. This choice was made to illustrate how we can listen for changes to the selection in the ListView itself. Similarly to buttons, the ListView has a member onSelect that can be set to a function. This function accepts two parameters. The first represents the row that the event regards. The second parameter states whether the row is now selected or not. When we select a new row, any previously selected rows will be deselected automatically. For our intents and pruposes we only care about updating the row to highlight when we press a new row. This can be implemented as follows:

void onSelect(Nat row, Bool selected) {
    if (selected) {
        currentHighlight = row;
    }
}

In order to associate the ListView with this new behaviour we assign the onSelect method to our new function in the init function of the ChessVisualizer class:

moveList.onSelect = &this.onSelect;

The functions for the buttons are simple to implement. They ensure that the highlight is not moved outside the range of the ListView when stepping, or simply sets it to the last or first index:

void highlightFirst() {
    currentHighlight = 0;
    moveList.selection = currentHighlight;
}

void highlightLast() {
    currentHighlight = moveList.count - 1;
    moveList.selection = currentHighlight;
}

void highlightNext() {
    if (currentHighlight + 1 < moveList.count) {
        currentHighlight++;
        moveList.selection = currentHighlight;
    }
}

void highlightPrevious() {
    if (currentHighlight > 0) {
        currentHighlight--;
        moveList.selection = currentHighlight;
    }
}

In order to associate the buttons with the functionality of these functions, we assign to the onClick method in the init-function of the ChessVisualizer class for each corresponding button:

a.onClick = &this.highlightFirst;
b.onClick = &this.highlightPrevious;
c.onClick = &this.highlightNext;
d.onClick = &this.highlightLast;

Putting it all together results in the code below, combined with the custom painter class. This implements an application with a chessboard, a list of moves, and buttons to step through the list of moves with a highlight indicating the current move.

use ui;
use graphics;
use layout;
use core:io;
use core:geometry;
use lang:bs:macro;

frame ChessVisualizer {
    layout Grid grid {
        expandCol: 0;
        expandCol: 1;
        expandCol: 2;
        expandCol: 3;
        expandCol: 4;
        expandRow: 0;
        Window chessBoard { colspan: 4; minHeight: 500; minWidth: 500; }
        ListView moveList(["Nr", "White", "Black"]) { col: 4; minHeight: 500; minWidth: 150;}
        nextLine;
        Button a("|<"){}
        Button b("<") {}
        Button c(">") {}
        Button d(">|"){}
    }
    
    ChessPainter boardPainter;
    Nat currentHighlight;
    
    init() {
        init("ChessVisualizer", Size(500, 500)) {}
        chessBoard.painter(boardPainter);

        moveList.multiSelect = false;
        moveList.add(["1." , "e4"   , "c6"]);
        moveList.add(["2." , "d4"   , "d5"]);
        moveList.add(["3." , "Nd2"  , "dxe4"]);
        moveList.add(["4." , "Nxe4" , "Nd7"]);
        moveList.add(["5." , "Ng5"  , "Ngf6"]);
        moveList.add(["6." , "Bc4"  , "e6"]);
        moveList.add(["7." , "Qe2"  , "Nb6"]);
        moveList.add(["8." , "Bb3"  , "h6"]);
        moveList.add(["9." , "N5f3" , "c5"]);
        moveList.add(["10.", "Bf4"  , "Bd6"]);
        moveList.add(["11.", "Bg3"  , "Qe7"]);
        moveList.add(["12.", "dxc5" , "Bxc5"]);
        moveList.add(["13.", "Ne5"  , "Bd7"]);
        moveList.add(["14.", "Ngf3" , "Nh5"]);
        moveList.add(["15.", "O-O-O", "Nxg3"]);
        moveList.add(["16.", "hxg3" , "O-O-O"]);
        moveList.add(["17.", "Rh5"  , "Be8"]);
        moveList.add(["18.", "Rxd8+", "Kxd8"]);
        moveList.add(["19.", "Qd2+" , "Bd6"]);
        moveList.add(["20.", "Nd3"  , "Qc7"]);
        moveList.add(["21.", "g4"   , "Kc8"]);
        moveList.add(["22.", "g5"   , "Bf8"]);
        moveList.add(["23.", "Rh4"  , "Kb8"]);
        moveList.add(["24.", "a4"   , "Be7"]);
        moveList.add(["25.", "a5"   , "Nd5"]);
        moveList.add(["26.", "Kb1"  , "Bd8"]);
        moveList.add(["27.", "a6"   , "Qa5"]);
        moveList.add(["28.", "Qe2"  , "Nb6"]);
        moveList.add(["29.", "axb7" , "Bxg5"]);
        moveList.add(["30.", "Nxg5" , "Qxg5"]);
        moveList.add(["31.", "Rh5"  , "Qf6"]);
        moveList.add(["32.", "Ra5"  , "Bc6"]);
        moveList.add(["33.", "Nc5"  , "Bxb7"]);
        moveList.add(["34.", "Nxb7" , "Kxb7"]);
        moveList.add(["35.", "Qa6+" , "Kc6"]);
        moveList.add(["36.", "Ba4+" , "Kd6"]);
        moveList.add(["37.", "Qd3+" , "Nd5"]);
        moveList.add(["38.", "Qg3+" , "Qe5"]);
        moveList.add(["39.", "Qa3+" , "Kc7"]);
        moveList.add(["40.", "Qc5+" , "Kd8"]);
        moveList.add(["41.", "Rxa7" , "1-0"]);
        moveList.onSelect = &this.onSelect;
        highlightFirst();
        
        a.onClick = &this.highlightFirst;
        b.onClick = &this.highlightPrevious;
        c.onClick = &this.highlightNext;
        d.onClick = &this.highlightLast;
        
        create();
    }

    void onSelect(Nat row, Bool selected) {
        if(selected){
            currentHighlight = row;
        }
    }
    
    void highlightFirst() {
        currentHighlight = 0;
        moveList.selection = currentHighlight;
    }

    void highlightLast() {
        currentHighlight = moveList.count - 1;
        moveList.selection = currentHighlight;
    }

    void highlightNext() {
        if (currentHighlight + 1 < moveList.count){
            currentHighlight++;
            moveList.selection = currentHighlight;
        }
    }

    void highlightPrevious(){
        if (currentHighlight > 0) {
            currentHighlight--;
            moveList.selection = currentHighlight;
        }
    }
}

Adding Menus to the Window

The last thing in the tutorial is to add a menu to the frame, and using the menu to show license information for the program.

The two things we want to add to achieve this is a ui.MenuBar and a ui.PopupMenu. We want the MenuBar to display the text "Help" and the dropdown menu to contain an item with the text "About..." that shows license information. We first create the about menu by initializing a PopupMenu, setting the menu text to "About" and associating that choice with a function called onAbout that will show the user the license of the program. The MenuBar itself then needs to be initialized with the text "Help" and then adding the previously created PopupMenu to it. Finally, we set the menu member of the Frame to our created MenuBar to show the menu in the window.

PopupMenu helpMenu;
helpMenu << Menu:Text(mnemonic("_About..."), &this.onAbout());

MenuBar m;
m << Menu:Submenu(mnemonic("_Help"), helpMenu);

menu = m;

The only thing that remains is to implement the onAbout function. We want this function to make use of the .license file functionality available in Storm, we start by creating the .license files themselves. For this particular implementation we want a .license file for the program itself, a .version file that indicates what version of the program it is. In this case, we also need to include the license and attribution for the images of the chess pieces we have used. We create a separate .license file for that. These files should all be placed next to the .bs file as indicated below:

ui
├─ chess.bs
├─ CHESS.license
├─ CHESS_VERSION.version
├─ IMAGES.license
└─ res
   ├─ bb.png
   ├─ bw.png
   ├─ kb.png
   ├─ kw.png
   ├─ nb.png
   ├─ nw.png
   ├─ pb.png
   ├─ pw.png
   ├─ qb.png
   ├─ qw.png
   ├─ rb.png
   └─ rw.png

Starting with the .version file since it is the easiest. It is a file that contains one thing, the current version number. There is nothing more to it than that. In our case, we can simply enter 0.1.0. Any version compliant with semantic versioning is supported.

The .license file is straight forward as well, but the file contains more information. The first line in the file is interpreted as a summary of the license that is used. The second line contains a list of authors. The rest of file is the full license text of the license.

After creating the files, we can implement the onAbout function that will make use of the information. To simplify our implementation, we use the showLicenseDialog function that is included in the UI library. It traverses the name tree to find all licenses that are currently used and displays them. As such, this automatically compiles a list of licenses that are used by the program, and the list will be up-to-date if we use additional libraries in the future.

To show the license of the current program in a more prominent place, the showLicenseDialog function accepts the license of the current program as a separate parameter. To get the license we created, we use the named{} syntax to get the named entity that corresponds to the .license file we just created. Similarly, to show version information, we need to retrieve the information from the .version file we created. Finally, we also provide information about the name of the program, and the primary author. In summary, we implement the onAbout function as follows. Note that we need to add use lang:bs:macro; to the top of the file to use the named{} syntax:

private void onAbout() {
    var license = named{CHESS};
    var version = named{CHESS_VERSION};
    showLicenseDialog(this, ProgramInfo("ChessVisualizer", "Simon Ahrenstedt", version.version, license));
}

This concludes the brief tutorial of various UI elements that exist in Storm. The code for the complete program follows below and is also available in the directory root/tutorials/ui in the Storm release as mentioned in the beginning of the tutorial.

use ui;
use graphics;
use layout;
use core:io;
use core:geometry;
use lang:bs:macro;

frame ChessVisualizer {

    layout Grid {
        expandCol: 0;
        expandCol: 1;
        expandCol: 2;
        expandCol: 3;
        expandCol: 4;
        expandRow: 0;
        border: 10, 10;
        Window chessBoard { colspan: 4; minHeight: 500; minWidth: 500; }
        ListView moveList(["Nr", "White", "Black"]) { col: 4; minHeight: 500; minWidth: 150;}
        nextLine;
        Button a("|<") {}
        Button b("<") {}
        Button c(">") {}
        Button d(">|") {}
    }

    ChessPainter boardPainter;
    Nat currentHighlight;

    init() {
        init("ChessVisualizer", Size(500, 500)) {}
        chessBoard.painter(boardPainter);

        moveList.multiSelect = false;
        moveList.onSelect = &this.onSelect;
        moveList.add(["1." , "e4"    , "c6"]);
        moveList.add(["2." , "d4"    , "d5"]);
        moveList.add(["3." , "Nd2"    , "dxe4"]);
        moveList.add(["4." , "Nxe4" , "Nd7"]);
        moveList.add(["5." , "Ng5"    , "Ngf6"]);
        moveList.add(["6." , "Bc4"    , "e6"]);
        moveList.add(["7." , "Qe2"    , "Nb6"]);
        moveList.add(["8." , "Bb3"    , "h6"]);
        moveList.add(["9." , "N5f3" , "c5"]);
        moveList.add(["10.", "Bf4"    , "Bd6"]);
        moveList.add(["11.", "Bg3"    , "Qe7"]);
        moveList.add(["12.", "dxc5" , "Bxc5"]);
        moveList.add(["13.", "Ne5"    , "Bd7"]);
        moveList.add(["14.", "Ngf3" , "Nh5"]);
        moveList.add(["15.", "O-O-O", "Nxg3"]);
        moveList.add(["16.", "hxg3" , "O-O-O"]);
        moveList.add(["17.", "Rh5"    , "Be8"]);
        moveList.add(["18.", "Rxd8+", "Kxd8"]);
        moveList.add(["19.", "Qd2+" , "Bd6"]);
        moveList.add(["20.", "Nd3"    , "Qc7"]);
        moveList.add(["21.", "g4"    , "Kc8"]);
        moveList.add(["22.", "g5"    , "Bf8"]);
        moveList.add(["23.", "Rh4"    , "Kb8"]);
        moveList.add(["24.", "a4"    , "Be7"]);
        moveList.add(["25.", "a5"    , "Nd5"]);
        moveList.add(["26.", "Kb1"    , "Bd8"]);
        moveList.add(["27.", "a6"    , "Qa5"]);
        moveList.add(["28.", "Qe2"    , "Nb6"]);
        moveList.add(["29.", "axb7" , "Bxg5"]);
        moveList.add(["30.", "Nxg5" , "Qxg5"]);
        moveList.add(["31.", "Rh5"    , "Qf6"]);
        moveList.add(["32.", "Ra5"    , "Bc6"]);
        moveList.add(["33.", "Nc5"    , "Bxb7"]);
        moveList.add(["34.", "Nxb7" , "Kxb7"]);
        moveList.add(["35.", "Qa6+" , "Kc6"]);
        moveList.add(["36.", "Ba4+" , "Kd6"]);
        moveList.add(["37.", "Qd3+" , "Nd5"]);
        moveList.add(["38.", "Qg3+" , "Qe5"]);
        moveList.add(["39.", "Qa3+" , "Kc7"]);
        moveList.add(["40.", "Qc5+" , "Kd8"]);
        moveList.add(["41.", "Rxa7" , "1-0"]);
        currentHighlight = 0;
        highlightFirst();

        a.onClick = &this.highlightFirst;
        b.onClick = &this.highlightPrevious;
        c.onClick = &this.highlightNext;
        d.onClick = &this.highlightLast;

        PopupMenu helpMenu;
        helpMenu << Menu:Text(mnemonic("_About..."), &this.onAbout());

        MenuBar m;
        m << Menu:Submenu(mnemonic("_Help"), helpMenu);

        menu = m;

        create();
    }

    private void onAbout() {
        var license = named{CHESS};
        var version = named{CHESS_VERSION};
        showLicenseDialog(this, ProgramInfo("ChessVisualizer", "Simon Ahrenstedt", version.version, license));
    }

    void onSelect(Nat row, Bool selected) {
        if(selected) {
            currentHighlight = row;
        }
    }

    void highlightFirst() {
        currentHighlight = 0;
        moveList.selection = currentHighlight;
    }

    void highlightLast() {
        currentHighlight = moveList.count - 1;
        moveList.selection = currentHighlight;
    }

    void highlightNext() {
        if (currentHighlight + 1 < moveList.count){
            currentHighlight++;
            moveList.selection = currentHighlight;
        }
    }

    void highlightPrevious(){
        if (currentHighlight > 0) {
            currentHighlight--;
            moveList.selection = currentHighlight;
        }
    }
}

Url resUrl() {
    if(url = named{res}.url) {
        return url;
    } else {
        throw InternalError("Expected the package 'res' to be non-virtual.");
    }
}

class ChessPainter extends Painter {
    SolidBrush blackBrush;
    SolidBrush whiteBrush;
    SolidBrush darkBrush;

    Text[] rowLabels;
    Text[] colLabels;

    Str[] order;

    Str->Bitmap images;

    init() {
        init() {
            blackBrush(black);
            darkBrush(Color(118, 150, 86));
            whiteBrush(white);
            order = ["r", "n", "b", "q", "k", "b", "n", "r"];
        }

        bgColor = Color(238, 238, 210);

        for (row in ["8", "7", "6", "5", "4", "3", "2", "1"]) {
            rowLabels << Text(row, defaultFont);
        }

        for (col in ["a", "b", "c", "d", "e", "f", "g", "h"]) {
            colLabels << Text(col, defaultFont);
        }

        Url imgPath = resUrl();
        for (child in imgPath.children()) {
            print(child.title);
            images.put(child.title, Bitmap(child.loadImage));
        }
    }

    Bool render(Size me, Graphics g) {
        Size squareSz = me / 8;

        for (Nat y = 0; y < 8; y++) {
            for (Nat x = 0; x < 8; x++) {
                if((x + y) % 2 == 1) {
                    g.fill(Rect(Point(squareSz.w * x.float, squareSz.h * y.float), squareSz), darkBrush);
                }
            }
        }

        for (i, rowLabel in rowLabels) {
            g.draw(rowLabel, blackBrush, Point(0, squareSz.h * i.float));
        }

        for (i, colLabel in colLabels) {
            g.draw(colLabel, blackBrush, Point(squareSz.w * (i + 1).float - 1, me.h) - colLabel.size);
        }

        for (Nat x = 0; x < 8; x++) {
            Str blackPiece = order[x] + "b";
            g.draw(images.get("pb"), Rect(Point(squareSz.w * x.float, squareSz.h), squareSz));
            g.draw(images.get(blackPiece), Rect(Point(squareSz.w * x.float, squareSz.h * 0), squareSz));

            Str whitePiece = order[x] + "w";
            g.draw(images.get("pw"), Rect(Point(squareSz.w * x.float, squareSz.h * 6), squareSz));
            g.draw(images.get(whitePiece), Rect(Point(squareSz.w * x.float, squareSz.h * 7), squareSz));
        }

        return false;
    }
}

void main() {
    ChessVisualizer cw;
    cw.waitForClose();
}