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.
Note: we have to specifically importUSING: 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
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.
the factor ui is a part that I couldn't get (properly).
ReplyDeletesample code like that is of benefit, Thanks.
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
ReplyDelete: decimal ( model -- )
dup valid>>
[ [ "." split dup length 1 > [ first ] [ first "." append ] if ] change-model ]
[ t >>valid "0." swap set-model ] if ;
Thanks for this tutorial.
ReplyDeleteI'm approaching Factor since a couple of days and didn't know ui support even existed ;-)