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
Bitmap
s 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. ListView
s 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(); }