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 ;-)