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 numberInput
that 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 :)