Building a simple application with Spec2

Rakshit
6 min readJul 22, 2021

Spec2 is a framework in Pharo for describing User Interfaces.

I am building a simple application in Spec2 to run Pharo Matrix Benchmarks, as part of my GSoC 2021 project.

I will go over the steps to build the GUI in this blog.

Learning Spec2

If you are looking to learn Spec2, the Spec2 examples in Help in the Pharo image are really helpful.

They come with a lot of demos for using the Spec2 UI framework and also show the code for it alongside.

The Spec Handbook is also a really great place to start learning about Spec2.

Also, check out the to-do application tutorial to get a hang of developing applications with spec2.

Installing Spec2

Load Spec2 if you haven’t yet. You can load using -

Metacello new
githubUser: 'pharo-spec' project: 'Spec' commitish: 'Pharo9.0' path: 'src';
baseline: 'Spec2';
onConflict: [ :e | e useIncoming ];
onUpgrade: [ :e | e useIncoming ];
ignoreImage;
load

Developing a benchmark UI in Spec2

The benchmark UI that I had to build had to do the following

  • Allow the user to specify a size for the matrix.
  • Allow the user to select the operation to benchmark from the list of available operations.
  • Provide a benchmark button and stop button to run and stop the benchmarks.

There are more features that have to be taken care of by the UI, but we will look at the above-mentioned functionalities for now.

Application

We will add our application in a package called Matrix-Benchmark-Application

We will start by defining our application, MBApplication.

SpApplication subclass: #MBApplication
instanceVariableNames: ''
classVariableNames: ''
package: 'Matrix-Benchmark-Application'

This acts as the starting point for our application, from where we can run the GUI. We extend SpApplication in Spec2 for this class.

We now need a presenter, that will display all the contents. We have to make a subclass under SpPresenter for this.

SpPresenter subclass: #MBPresenter
instanceVariableNames: 'matrixSizeLabel matrixSizeInput operations benchmarkButton stopButton'
classVariableNames: ''
package: 'Matrix-Benchmark-Application'

The instance variables represent different items displayed in the UI. For example, benchmarkButton represents the button, which we click to execute the benchmarks. We will go through each of them below.

Now, In the class side of MBPresenter, we need to define the method, defaultSpec. We do this to specify the layout of the UI.

Then, on the instance side, we need to define the method, initializePresenters to initialize all the presenter variables we had in the layout.

Let us understand this through our application.

Building the layout

We will first add the label where we can input the matrix size.

The defaultSpec layout for this would be

MBPresenter class >> defaultSpec^ SpBoxLayout newTopToBottom

add: #matrixSizeLabel withConstraints: [ :constraints | constraints height: self labelledPresenterHeight ];
yourself

We are defining the layout to be a SpBoxLayout, which is the most common layout in spec2.

When we say topToBottom, we basically have all the presenters stacked up vertically in the layout. Currently, we have just added one presenter, matrixSizeLabel.

We have added constraints on matrixSizeLabel to restrict the height of this presenter. Feel free to remove the constraints and check the output that you get.

We also need to initialize matrixSizeLabel.

MBPresenter >> initializePresentersmatrixSizeInput := self newNumberInput  placeholder: '100'. matrixSizeLabel := self instantiate: (SpLabelledPresenter label: 'Matrix Size' input: matrixSizeInput  description: 'Size of the square matrices on which the benchmarks will be run.').

We are initializing matrixSizeLabel as an instance of SpLabelledPresenter. We specify the label, input, and description of it.

matrixSizeInput is the numberInputthat stores the value entered by the user for matrix size.

We will now add the list of operations available for benchmarks.

MBPresenter class >> defaultSpec^ SpBoxLayout newTopToBottom

add: #matrixSizeLabel withConstraints: [ :constraints | constraints height: self labelledPresenterHeight ];

add: 'Select the operation to benchmark' expand: false;
add: #operations ;

In our BoxLayout, we add the presenter operations and a small text.

expand: false prevents the text from expanding and restricts its size. Experiment removing this method and check the difference in output.

We also need to initialize operations like how we initialized matrixSizeLabel and matrixSizeInput.

MBPresent >> initializePresentersmatrixSizeInput := self newNumberInput  placeholder: '100'. matrixSizeLabel := self instantiate: (SpLabelledPresenter label: 'Matrix Size' input: matrixSizeInput  description: 'Size of the square matrices on which the benchmarks will be run.').

operations := self newList.

operations
items: MBAbstract subclasses.

We will initialize operations as a List and specify the items present in the list. MBAbstract subclasses just represents the benchmarks that are available and are not relevant here.

We now have both the presenters, operations and matrixSizeLabel ready. We now just need to add the buttons to our layout.

Since the buttons are present side-by-side, we can’t use newTopToBottom now.

We can implement this using LeftToRight as

SpBoxLayout newLeftToRight 
add: #benchmarkButton ;
add: #stopButton ; yourself

So, now the whole defaultSpec code becomes,

MBPresenter class >> defaultSpec^ SpBoxLayout newTopToBottom

add: #matrixSizeLabel withConstraints: [ :constraints | constraints height: self labelledPresenterHeight ];

add: 'Select the operation to benchmark' expand: false;
add: #operations ;

addLast: (SpBoxLayout newLeftToRight
add: #benchmarkButton ;
add: #stopButton ; yourself
);
yourself

Notice how we are having a horizontal box layout inside a vertical box layout. Thus, even though SpBoxLayout seems simple, we can pretty much implement any layout we want using this.

We now have to initialize the presenters benchmarkButton and stopButton.

We can initialize these buttons as

benchmarkButton  := self newButton label: 'benchmark'; color: Color orange ; yourself. stopButton := self newButton label: 'Stop'; color: Color red; yourself.

We just need to specify the color and label of the buttons.

So, the whole initializePresenters code becomes,

MBPresenter >> initializePresentersmatrixSizeInput := self newNumberInput  placeholder: '100'. matrixSizeLabel := self instantiate: (SpLabelledPresenter label: 'Matrix Size' input: matrixSizeInput  description: 'Size of the square matrices on which the benchmarks will be run.').

operations := self newList.
operations
items: MBAbstract subclasses.
benchmarkButton := self newButton label: 'benchmark'; color: Color orange ; yourself. stopButton := self newButton label: 'Stop'; color: Color red; yourself.

Now, all that is left is to connect the presenters i.e whenever the user clicks on the benchmark button, we need to execute the benchmarks for the user-specified operation and matrix size.

We can do this as

MBPresenter >> connectPresenters

self benchmarkButton action: [
|benchmarkClass matrixSize|
benchmarkClass := self operations selection selectedItem.
matrixSize := self matrixSizeInput number.
benchmarkClass perform: #runBenchmarks: with: matrixSize ]

Everything inside the block action: represents the set of commands that have to be executed when the user clicks on the benchmarkButton.

We can easily retrieve the operation selected in the list, and the number inputted for matrixSize.

Then, we can just go ahead and execute the benchmarks.

Running the Application

Finally, we need to tell MBApplication to open MBPresenter. We have to do this using the start method.

MBApplication >> start^ ( self new: MBPresenter  ) openWithSpec.

Now, to open our application, in the Playground, just execute

MBApplication new run

You should now see the GUI up and running.

Seeking Help

If you have doubts regarding Spec2, you can always ask in the #spec channel of the Pharo Discord community and people in the community will be more than happy to help :)

--

--