## Monday, August 23, 2010

### Calculator with GUI

Update: Kyle Cordes has made some nice refactoring to avoid the "code smell" of passing global variables around while building the gadgets.

I started playing around with the Factor GUI framework recently. The documentation is very detailed, but sometimes it is nice to have simple examples to learn from.

I thought it would be fun to build a simple calculator application. A teaser of what it will look like when we are done:

First, some imports and a namespace.

```USING: accessors colors.constants combinators.smart kernel fry
math math.parser models namespaces sequences ui ui.gadgets
ui.gadgets.borders ui.gadgets.buttons ui.gadgets.labels
ui.gadgets.tracks ui.pens.solid ;

FROM: models => change-model ;

IN: calc-ui```
Note: we have to specifically import `change-model` from the `models` vocabulary, since it might conflict with an `accessor`.

Factor user interface elements are called gadgets. Many of them support being dynamically updated by being connected to models. Each model maintains a list of connections that should be updated when the value being held by the model changes.

## The Model

Our `calculator` model is based on the notion that we have two numbers (`x` and `y`) and an operator that can be applied to produce a new value.

```TUPLE: calculator < model x y op valid ;

: <calculator> ( -- model )
"0" calculator new-model 0 >>x ;```

If we want to reset the model (such as when we press the "clear" button):

```: reset ( model -- )
0 >>x f >>y f >>op f >>valid "0" swap set-model ;```

We're storing all values as floating-point numbers, but (for display purposes) we'll show integers when possible:

```: display ( n -- str )
>float number>string dup ".0" tail? [
dup length 2 - head
] when ;```

Each of `x` and `y` can be set based on the `value`, and the `op` is specified as a quotation:

```: set-x ( model -- model )
dup value>> string>number >>x ;

: set-y ( model -- model )
dup value>> string>number >>y ;

: set-op ( model quot: ( x y -- z ) -- )
>>op set-x f >>y f >>valid drop ;```

Pushing the "=" button triggers the calculation:

```: (solve) ( model -- )
dup [ x>> ] [ y>> ] [ op>> ] tri call( x y -- z )
[ >>x ] keep display swap set-model ;

: solve ( model -- )
dup op>> [ dup y>> [ set-y ] unless (solve) ] [ drop ] if ;```

We support negating the number:

```: negate ( model -- )
dup valid>> [
dup value>> "-" head?
[ [ 1 tail ] change-model ]
[ [ "-" prepend ] change-model ] if
] [ drop ] if ;```

And pushing the "." button (to add a decimal), or a number (to add a digit):

```: decimal ( model -- )
dup valid>>
[ [ dup "." subseq? [ "." append ] unless ] change-model ]
[ t >>valid "0." swap set-model ] if ;

: digit ( n model -- )
dup valid>>
[ swap [ append ] curry change-model ]
[ t >>valid set-model ] if ;```

That pretty much rounds out the basic features of the model.

## The GUI

For convenience, I store the calculator model in a global symbol:

```SYMBOL: calc
<calculator> calc set-global```

I can use that to create buttons for each type (using short names and unicode characters to make the code a bit prettier):

```: [C] ( -- button )
"C" calc get-global '[ drop _ reset ] <border-button> ;

: [±] ( -- button )
"±" calc get-global '[ drop _ negate ] <border-button> ;

: [+] ( -- button )
"+" calc get-global '[ drop _ [ + ] set-op ] <border-button> ;

: [-] ( -- button )
"-" calc get-global '[ drop _ [ - ] set-op ] <border-button> ;

: [×] ( -- button )
"×" calc get-global '[ drop _ [ * ] set-op ] <border-button> ;

: [÷] ( -- button )
"÷" calc get-global '[ drop _ [ / ] set-op ] <border-button> ;

: [=] ( -- button )
"=" calc get-global '[ drop _ solve ] <border-button> ;

: [.] ( -- button )
"." calc get-global '[ drop _ decimal ] <border-button> ;

: [#] ( n -- button )
dup calc get-global '[ drop _ _ digit ] <border-button> ;

: [_] ( -- label )
"" <label> ;```

We will create a label that is updated when the model changes.

```: <display> ( -- label )
calc get-global <label-control> { 5 5 } <border>
{ 1 1/2 } >>align
COLOR: gray <solid> >>boundary ;```

And, finally, creating the GUI (using vertical and horizontal track layouts):

```: <col> ( quot -- track )
vertical <track> 1 >>fill { 5 5 } >>gap
swap output>array [ 1 track-add ] each ; inline

: <row> ( quot -- track )
horizontal <track> 1 >>fill { 5 5 } >>gap
swap output>array [ 1 track-add ] each ; inline

: calc-ui ( -- )
[
<display>
[     [C]     [±]     [÷]    [×] ] <row>
[ "7" [#] "8" [#] "9" [#]    [-] ] <row>
[ "4" [#] "5" [#] "6" [#]    [+] ] <row>
[ "1" [#] "2" [#] "3" [#]    [=] ] <row>
[ "0" [#]     [.]     [_]    [_] ] <row>
] <col> { 10 10 } <border> "Calculator" open-window ;

MAIN: calc-ui```

Then, running the calculator application:

`( scratchpad ) "calc-ui" run`

The code for this is on my Github.

#### 3 comments:

kobi said...

the factor ui is a part that I couldn't get (properly).
sample code like that is of benefit, Thanks.

xiackok said...

hi i am newbie to factor so thank you for this tutorials and for others. but i change decimal word for a bit. your version adding dots forever and it doesn't remove dot. whatever here it is

: decimal ( model -- )
dup valid>>
[ [ "." split dup length 1 > [ first ] [ first "." append ] if ] change-model ]
[ t >>valid "0." swap set-model ] if ;

vip said...

Thanks for this tutorial.
I'm approaching Factor since a couple of days and didn't know ui support even existed ;-)