Tutorials»Calculator

Calculator


Note: Like in the HelloWorld tutorial, you'll have to tweak the vfs.mountDir path. This tutorial assumes that we're two directories above "xf/hybrid", e.g. in "xf/hybrid/demos/calc".

Layout

Let's start from a simple program and a config file that will define the whole calculator GUI:

module Calc;

private {
    import xf.hybrid.Hybrid;
    import xf.hybrid.backend.GL;

    // for Thread.yield
    import tango.core.Thread;
    import tango.util.Convert;
}



void main() {
    gui.vfsMountDir(`../../`);
    scope cfg = loadHybridConfig(`./Calc.cfg`);
    scope renderer = new Renderer;

    bool programRunning = true;
    while (programRunning) {
        gui.begin(cfg);
            if (gui().getProperty!(bool)("main.frame.closeClicked")) {
                programRunning = false;
            }

            gui.push(`main.buttons`);

            for (int dig = 0; dig <= 9; ++dig) {
                if (GenericButton("d" ~ cast(char)('0'+dig)).clicked) {
                    onDigit(dig);
                }
            }

            gui.pop();
        gui.end();
        gui.render(renderer);
        Thread.yield();
    }
}


void onDigit(int d) {
}
import "themes/default.cfg"



new FramedTopLevelWindow main {
    frame.text = "Calc";
    layout = {
        padding = 5 5;
        spacing = 5;
    }

    [hexpand hfill] new Group {
        layout = Bin;
        layout = {
            padding = 3 3;
        }
        shape = Rectangle;
        style.normal = {
            background = solid(rgb(.1, .1, .1));
        }

        new Label display {
            halign = 2;
            text = "0";
        }
    }

    new VBox buttons {
        layout = { spacing = 2; }

        new HBox {
            layout = { spacing = 2; }
            new Button d7 { text = "7"; size = 50 30; }
            new Button d8 { text = "8"; size = 50 30; }
            new Button d9 { text = "9"; size = 50 30; }
            new Button div { text = "/"; size = 50 30; }
        }

        new HBox {
            layout = { spacing = 2; }
            new Button d4 { text = "4"; size = 50 30; }
            new Button d5 { text = "5"; size = 50 30; }
            new Button d6 { text = "6"; size = 50 30; }
            new Button mul { text = "*"; size = 50 30; }
        }

        new HBox {
            layout = { spacing = 2; }
            new Button d1 { text = "1"; size = 50 30; }
            new Button d2 { text = "2"; size = 50 30; }
            new Button d3 { text = "3"; size = 50 30; }
            new Button sub { text = "-"; size = 50 30; }
        }

        new HBox {
            layout = { spacing = 2; }
            new Button d0 { text = "0"; size = 50 30; }
            new Button chSign { text = "+/-"; size = 50 30; }
            new Button equals { text = "="; size = 50 30; }
            new Button add { text = "+"; size = 50 30; }
        }
    }
} @overlay {
    [hexpand vexpand hfill vfill] new Group .overlay {
        layout = Ghost;
    }
}

If we run the compiled program, we should have a calculator that doesn't yet do anything useful, but looks like a calculator.

Stepping through the code, we have the segment enclosed in gui.push(`main.buttons`) .. gui.pop(). This allows us to access the buttons inside our .cfg file, that reside within main.buttons.

The loop within simply references the d0 .. d9 buttons and calls our digit handler if any of these are clicked. We use a GenericButton here, since the config might use any other Button type and we don't want to force the standard Button.

As for the config file, it doesn't contain anything fancy at all. Perhaps just the display Label and its surroundings needs a comment. We wrap it in a Group widget with a Bin layout in order to put it in on a darker background. Group doesn't normally have a shape assigned, so we set it to Rectangle. The Label itself has its halign value set to 2, which means right-alignment.


Entering digits

Ok, let's make this baby do something. We need to implement the onDigit function, so it adds digits to the display. Long story short, let's use the following code at the global scope:

long                                        prevNum;
bool                                        enteringNewNum = true;
long delegate(long a, long b)   curOp;


void onDigit(int d) {
    auto lab = Label(`.main.display`);

    char[] text;
    if (enteringNewNum || "0" == lab.text) {
        text = "" ~ cast(char)(d + '0');
        enteringNewNum = false;
    } else {
        text = lab.text ~ cast(char)(d + '0');
    }

    char[] convText = to!(char[])(to!(long)(text));
    if (convText == text) {
        lab.text = text;
    }
}

Ok now, prevNum will hold the first operand of any operation we'll be doing, while the second one will be the current numeric value of the display.
enteringNewNum will be set to true after clicking a math operation button, in order to be able to enter the second operand.
curOp will be the delegate we'll use to perform the math operation.

onDigit is pretty simple too. If we're enteringNewNum or the current text in the display is a single zero digit, reset the display to the digit the function is called for. Otherwise, add the digit to the display.

There's a little twist at the end of onDigit, which makes digits appear in the display only if they don't overflow the long type.

If you run the program now, you should be able to enter digits into the display, but not do anything about them. Let's handle that.


Doing the math

We'll start by adding code that will handle our math operations:

void evalOp() {
    auto lab = Label(`.main.display`);

    if (curOp !is null) {
        lab.text = to!(char[])(curOp(prevNum, to!(long)(lab.text)));
        curOp = null;
    }

    enteringNewNum = true;
}


void onOp(long delegate(long a, long b) op) {
    evalOp();
    curOp = op;
    prevNum = to!(long)(Label(`.main.display`).text);
}

evalOp will take the prevNum number and the current value from the display and perform the operation on them. It should not do anything if curOp is null.
When an operation is performed, we'd like to be able to enter another number into the display. We force this by setting enteringNewNum to true.

As for onOp, it will be called by operation buttons' handlers with an appropriate delegate. We'd like it to evaluate the previous operation (since selecting a new operation means that we're done entering the second operand for the previous one) and update the display with its result. The function should be self-explanatory.


Making buttons do the math

Let's bind some actions to our buttons! Addition, subtraction, multiplication and division will use similar code (we add it after the digit button handling, in the same push .. pop block):

if (GenericButton("div").clicked) {
    onOp((long a, long b) { return b == 0 ? 0 : a / b; });
}

if (GenericButton("mul").clicked) {
    onOp((long a, long b) { return a * b; });
}

if (GenericButton("add").clicked) {
    onOp((long a, long b) { return a + b; });
}

if (GenericButton("sub").clicked) {
    onOp((long a, long b) { return a - b; });
}

Simple, isn't it? Division checks for the second argument being zero, in order to avoid a division-by-zero exception.

We'd also like to have a button that changes the sign of the number currently on the display. That may be accomplished by simple text operations. An assumption here is that we don't like "-0":

if (GenericButton("chSign").clicked) {
    auto lab = Label(`.main.display`);
    if (lab.text != "0") {
        if (lab.text.length > 0 && '-' == lab.text[0]) {
            lab.text = lab.text[1..$];
        } else {
            lab.text = '-' ~ lab.text;
        }
    }
}

... and one last button to handle, the equals sign. Nothing simpler:

if (GenericButton("equals").clicked) {
    evalOp();
}


The calculator should work fine by now, but there's something missing. It's the ability to operate it with the keyboard. We'll accomplish it by adding a KeyboardEvent handler to the main window. Let's use the following code, inserted just before the main loop.

gui.begin(cfg);
    Group(`main`)
        .grabKeyboardFocus()
        .addHandler((KeyboardEvent e) {
            if (e.sinking && e.down) {
                if (e.unicode >= '0' && e.unicode <= '9') {
                    onDigit(e.unicode - '0');
                } else if (KeySym.Return == e.keySym) {
                    evalOp();
                } else switch (e.unicode) {
                    case '/':
                        onOp((long a, long b) { return b == 0 ? 0 : a / b; });
                        break;
                    case '*':
                        onOp((long a, long b) { return a * b; });
                        break;
                    case '+':
                        onOp((long a, long b) { return a + b; });
                        break;
                    case '-':
                        onOp((long a, long b) { return a - b; });
                        break;
                    case '=':
                        evalOp();
                        break;
                    default: break;
                }
            }
            return EventHandling.Continue;
        });
gui.end();

Ok, what happens here, step by step? First, we access the main window as a Group wigdet. We might use a TopLevelWindow, but again, we won't be forcing the user to keep it like that in the config.

No widget receives keyboard input by default. We can make that happen by giving it focus. this is what the grabKeyboardFocus() function is for.

Then follows the body our our custom KeyboardEvent handler. As described in the API Overview section, events use a two-phase propagation mechanism. We only want to catch keyboard input once, so we'll restrict it to the sinking phase. We'll also handle the event only if the key was pushed down.

The body is pretty simple. We may use the e.unicode value to check what key has been pressed. Since the Return key doesn't really have any Unicode value, we'll handle it through its KeySym value.

At the end, the function returns EventHandling.Continue, since we don't really care if any other widget gets the keyboard events as well.


Summing it up

If you followed everything so far, you should have the following code at hands:

module Calc;

private {
    import xf.hybrid.Hybrid;
    import xf.hybrid.backend.GL;

    // for Thread.yield
    import tango.core.Thread;
    import tango.util.Convert;
}



void main() {
    gui.vfsMountDir(`../../`);
    scope cfg = loadHybridConfig(`./Calc.cfg`);
    scope renderer = new Renderer;

    gui.begin(cfg);
        Group(`main`)
            .grabKeyboardFocus()
            .addHandler((KeyboardEvent e) {
                if (e.sinking && e.down) {
                    if (e.unicode >= '0' && e.unicode <= '9') {
                        onDigit(e.unicode - '0');
                    } else if (KeySym.Return == e.keySym) {
                        evalOp();
                    } else switch (e.unicode) {
                        case '/':
                            onOp((long a, long b) { return b == 0 ? 0 : a / b; });
                            break;
                        case '*':
                            onOp((long a, long b) { return a * b; });
                            break;
                        case '+':
                            onOp((long a, long b) { return a + b; });
                            break;
                        case '-':
                            onOp((long a, long b) { return a - b; });
                            break;
                        case '=':
                            evalOp();
                            break;
                        default: break;
                    }
                }
                return EventHandling.Continue;
            });
    gui.end();

    bool programRunning = true;
    while (programRunning) {
        gui.begin(cfg);
            if (gui().getProperty!(bool)("main.frame.closeClicked")) {
                programRunning = false;
            }

            gui.push(`main.buttons`);

            for (int dig = 0; dig <= 9; ++dig) {
                if (GenericButton("d" ~ cast(char)('0'+dig)).clicked) {
                    onDigit(dig);
                }
            }

            if (GenericButton("div").clicked) {
                onOp((long a, long b) { return b == 0 ? 0 : a / b; });
            }

            if (GenericButton("mul").clicked) {
                onOp((long a, long b) { return a * b; });
            }

            if (GenericButton("add").clicked) {
                onOp((long a, long b) { return a + b; });
            }

            if (GenericButton("sub").clicked) {
                onOp((long a, long b) { return a - b; });
            }

            if (GenericButton("chSign").clicked) {
                auto lab = Label(`.main.display`);
                if (lab.text != "0") {
                    if (lab.text.length > 0 && '-' == lab.text[0]) {
                        lab.text = lab.text[1..$];
                    } else {
                        lab.text = '-' ~ lab.text;
                    }
                }
            }

            if (GenericButton("equals").clicked) {
                evalOp();
            }

            gui.pop();
        gui.end();
        gui.render(renderer);
        Thread.yield();
    }
}


long                                        prevNum;
bool                                        enteringNewNum = true;
long delegate(long a, long b)   curOp;


void onDigit(int d) {
    auto lab = Label(`.main.display`);

    char[] text;
    if (enteringNewNum || "0" == lab.text) {
        text = "" ~ cast(char)(d + '0');
        enteringNewNum = false;
    } else {
        text = lab.text ~ cast(char)(d + '0');
    }

    char[] convText = to!(char[])(to!(long)(text));
    if (convText == text) {
        lab.text = text;
    }
}


void evalOp() {
    auto lab = Label(`.main.display`);

    if (curOp !is null) {
        lab.text = to!(char[])(curOp(prevNum, to!(long)(lab.text)));
        curOp = null;
    }

    enteringNewNum = true;
}


void onOp(long delegate(long a, long b) op) {
    evalOp();
    curOp = op;
    prevNum = to!(long)(Label(`.main.display`).text);
}

And this concludes the tutorial.