Tuesday, August 31, 2010

Calculator with GUI

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.


factorization calculator

No comments:

Post a Comment