In the last episode, we built a GUI for a calculator in Elixir using Erlang's wx module. In this episode, we're going to wire it up and make it functional. Let's get started!

Project

We're going to start where we left off in the last episode. I've tagged it as after_episode_071 in the github repository, which is linked in this episode's resources section.

Wiring up the Quit button

We're going to be responding to events in order to make the calculator work. The first thing we ought to do is wire up the exit button to destroy our wx environment. First, we'll extract all of the GUI building to a function we'll just call 'setup'. Open up lib/wx/window.ex

  def start do
    wx = :wx.new
    panel = :wxFrame.new(wx, -1, 'WxCalc')
    setup(panel)
    :wxFrame.show(panel)
    loop(panel)
    :wxFrame.destroy(panel)
  end
#...
  def setup(panel) do
    first_row  = :wxBoxSizer.new(:wx_const.wx_horizontal)
    second_row = :wxBoxSizer.new(:wx_const.wx_horizontal)
    third_row  = :wxBoxSizer.new(:wx_const.wx_horizontal)
    fourth_row = :wxBoxSizer.new(:wx_const.wx_horizontal)
    rows       = :wxBoxSizer.new(:wx_const.wx_vertical)
    display    = :wxTextCtrl.new(panel, 30, value: '0')

    :wxSizer.add(rows, display, flag: (:wx_const.wx_expand ||| :wx_const.wx_all))
    :wxSizer.add(rows, first_row)
    :wxSizer.add(rows, second_row)
    :wxSizer.add(rows, third_row)
    :wxSizer.add(rows, fourth_row)

    one   = :wxButton.new(panel, 11, label: '1')
    two   = :wxButton.new(panel, 12, label: '2')
    three = :wxButton.new(panel, 13, label: '3')
    add   = :wxButton.new(panel, 41, label: '+')

    :wxSizer.add(first_row, one)
    :wxSizer.add(first_row, two)
    :wxSizer.add(first_row, three)
    :wxSizer.add(first_row, add)

    four  = :wxButton.new(panel, 14, label: '4')
    five  = :wxButton.new(panel, 15, label: '5')
    six   = :wxButton.new(panel, 16, label: '6')
    subtract = :wxButton.new(panel, 42, label: '-')

    :wxSizer.add(second_row, four)
    :wxSizer.add(second_row, five)
    :wxSizer.add(second_row, six)
    :wxSizer.add(second_row, subtract)

    seven = :wxButton.new(panel, 17, label: '7')
    eight = :wxButton.new(panel, 18, label: '8')
    nine  = :wxButton.new(panel, 19, label: '9')
    mult  = :wxButton.new(panel, 43, label: 'x')

    :wxSizer.add(third_row, seven)
    :wxSizer.add(third_row, eight)
    :wxSizer.add(third_row, nine)
    :wxSizer.add(third_row, mult)

    dot   = :wxButton.new(panel, 21, label: '.')
    zero  = :wxButton.new(panel, 20, label: '0')
    clear = :wxButton.new(panel, 22, label: 'C')
    div   = :wxButton.new(panel, 44, label: '%')

    :wxSizer.add(fourth_row, dot)
    :wxSizer.add(fourth_row, zero)
    :wxSizer.add(fourth_row, clear)
    :wxSizer.add(fourth_row, div)

    equal = :wxButton.new(panel, 45, label: '=')
    :wxSizer.add(rows, equal, flag: (:wx_const.wx_expand ||| :wx_const.wx_all))

    :wxPanel.setSizer(panel, rows)
  end

Now our main loop at least reads a little better, and we hid all the GUI setup elsewhere. Next, we need to subscribe to the close event:

  def start do
    wx = :wx.new
    panel = :wxFrame.new(wx, -1, 'WxCalc')
    setup(panel)
    :wxFrame.connect(panel, :close_window)
    :wxFrame.show(panel)
    :wxFrame.destroy(panel)
  end

So with that call to :wxFrame.connect, we'll start receiving messages in this process when a :close_window event is emitted. Now we need to be able to receive them. Let's do that in a loop:

  def start do
    #...
    :wxFrame.show(panel)
    loop(panel)
    :wxFrame.destroy(panel)
  end
  def loop(panel) do
    receive do
      event ->
        IO.inspect(event)
        IO.puts("Message received")
        loop(panel)
    end
  end

Alright, go ahead run it, and you should see the event output in the repl. This tuple is a wx record, which is defined in the wx include. We'll want to extract the record, and we'll use defrecordp to do it. Go to the top of the module and extract the wx record:

defmodule WxCalc.Window do
  #..
  require Record
  defrecordp :wx, Record.extract(:wx, from_lib: "wx/include/wx.hrl")
  # and then we'll actually extract the `wxClose` record as well, which is the
  # event that gets emitted when you close
  defrecordp :wxClose, Record.extract(:wxClose, from_lib: "wx/include/wx.hrl")
  #..
end

Alright, so we never covered defrecordp when I covered records - we should have. At any rate, the way that it works is that it defines a macro for building and matching against the record. Let's modify the loop to catch the close events now:

  def loop(panel) do
    receive do
      wx(event: wxClose()) ->
        IO.puts("close_window received")
      event ->
        IO.inspect(event)
        IO.puts("Message received")
        loop(panel)
    end
  end

So here, we'll match against a wx record that contains a close event. If we receive that, we don't loop anymore, so we fall through to destroying the frame. Let's try it out. (click the x to close the window here)

Alright, so you got to see we received the close_window event. Next, we'll want to be able to capture events from the buttons being pressed. For this, we'll need to extract another record, connect to another event, and add another clause to our receive block:

  # at the top...
  defrecordp :wxCommand, Record.extract(:wxCommand, from_lib: "wx/include/wx.hrl")
  # in start
  :wxFrame.connect(panel, :command_button_clicked)
  # in the loop
  wx(event: wxCommand(type: :command_button_clicked)) ->
    IO.puts("button clicked")
    loop(panel)

Run it again, and you should see 'button clicked' output to IO when you click any of the buttons. Next, we'll need to differentiate between the various buttons of course. This can be done with their IDs. Let's see that:

      wx(id: 11, event: wxCommand(type: :command_button_clicked)) ->
        IO.puts("1")
        loop(panel)
      wx(id: 12, event: wxCommand(type: :command_button_clicked)) ->
        IO.puts("2")
        loop(panel)

OK, so that's easy. It's kind of miserable to remember these IDs, so let's extract them to module attributes for each button:

  @one      11
  @two      12
  @three    13
  @four     14
  @five     15
  @six      16
  @seven    17
  @eight    18
  @nine     19
  @zero     20

  @display  30
  @dot      21
  @clear    22
  @add      41
  @subtract 42
  @mult     43
  @div      44
  @equal    45

  def setup(panel) do
    #...
    display    = :wxTextCtrl.new(panel, @display, value: '0')

    #...
    one   = :wxButton.new(panel, @one, label: '1')
    two   = :wxButton.new(panel, @two, label: '2')
    three = :wxButton.new(panel, @three, label: '3')
    add   = :wxButton.new(panel, @add, label: '+')

    #...
    four  = :wxButton.new(panel, @four, label: '4')
    five  = :wxButton.new(panel, @five, label: '5')
    six   = :wxButton.new(panel, @six, label: '6')
    subtract = :wxButton.new(panel, @subtract, label: '-')

    #...
    seven = :wxButton.new(panel, @seven, label: '7')
    eight = :wxButton.new(panel, @eight, label: '8')
    nine  = :wxButton.new(panel, @nine, label: '9')
    mult  = :wxButton.new(panel, @mult, label: 'x')

    #...
    dot   = :wxButton.new(panel, @dot, label: '.')
    zero  = :wxButton.new(panel, @zero, label: '0')
    clear = :wxButton.new(panel, @clear, label: 'C')
    div   = :wxButton.new(panel, @div, label: '%')

    #...
    equal = :wxButton.new(panel, @equal, label: '=')
    #...
  end

  def loop(panel) do
    receive do
      #...
      wx(id: @one, event: wxCommand(type: :command_button_clicked)) ->
        IO.puts("1")
        loop(panel)
      wx(id: @two, event: wxCommand(type: :command_button_clicked)) ->
        IO.puts("2")
        loop(panel)
      #...

Alright, let's go ahead and try that out. (do it) OK, so everything still works. Next, let's write a function that handles all of our command_button_clicked commands in such a way that we don't end up with a lot of verbosity:

  def loop(panel) do
    receive do
      #...
      wx(id: id, event: wxCommand(type: :command_button_clicked)) ->
        handle_button(id)
        loop(panel)
  end

  def handle_button(@one), do: IO.puts("1")
  def handle_button(@two), do: IO.puts("2")
  def handle_button(_),    do: IO.puts("something else")

Alright, so go ahead and run it again to make sure it still works. Now we've got the core of the UI working - inasmuch as we have a centralized way to manage events. Next, let's test drive the core of the calculator. For this, we'll build a quick genserver to manage the state for the calculation.

Open up test/calculator_server_test.exs:

defmodule CalculatorServerTest do
  use ExUnit.Case
  alias WxCalc.CalculatorServer, as: CS

  test "basic addition" do
    # OK, so our CalculatorServer will be a genserver
    {:ok, server} = CS.start
    # We should be able to read the display
    assert "0" == CS.get_display(server)
    # We press a number
    server |> CS.number(1)
    # That should have modified the display
    assert "1" == CS.get_display(server)
    # We press the add button
    server |> CS.add
    # That should not have modified the display
    assert "1" == CS.get_display(server)
    # We press another number
    server |> CS.number(2)
    # That should have modified the display
    assert "2" == CS.get_display(server)
    # We press equals
    server |> CS.equals
    # That should perform the calculation and modify the display
    assert "3" == CS.get_display(server)
  end
end

OK, so this covers a pretty good starting point for our calculator engine.

To run the tests, we'll need to avoid starting our app. To do that, we'll run the tests with mix test --no-start. I'll map this to ,t:

(.vimrc in the project)

augroup elixir
  au!
  au BufNewFile,BufRead *.ex,*.exs noremap <buffer> <leader>t :!iex -S mix<cr>
  au BufNewFile,BufRead *_test.exs noremap <buffer> <leader>t :!mix test --no-start<cr>
augroup END

OK, run the tests.

They fail because we have no CalculatorServer, so let's define the beginnings of that at the top of the module:

defmodule WxCalc.CalculatorServer do
  use ExActor.GenServer
end

We'll use ExActor, so let's add that dependency. Open up mix.exs:

  defp deps do
    [
      {:exactor, github: "sasa1977/exactor"}
    ]
  end

Go ahead and fetch it with mix deps.get. Try to run the tests again. Now it fails because it can't get the display. This is sensible - we haven't defined anything about this server yet. Let's store the state in a map and add the get_display function:

defmodule WxCalc.CalculatorServer do
  use ExActor.GenServer

  definit do: initial_state(%{display: "0"})

  defcall get_display, state: state, do: reply(state.display)
end

Go ahead and run the tests, and we get past the first assertion, on to an error where there's no number function. This will be a call that will just add a number to the internal 'numbers' field in the state and update the display string. First, we'll add a numbers attribute to the state:

  definit do: initial_state(%{display: "0", numbers: []})

Next, we'll define the number cast:

  defcast number(num), state: state do
    new_numbers = state.numbers ++ [num]
    new_state(%{state | numbers: new_numbers, display: Enum.join(new_numbers)})
  end

So here we're just adding any new numbers into our list of numbers, and updating the display. Run the tests, and we get past the next assertion, then fail when 'add' is called. Let's get that one passing next. First, we'll add a value attribute and a current_operation attribute to the state:

  definit do: initial_state(%{display: "0", numbers: [], value: 0, current_operation: :noop})

Next, we'll define the add cast. This is going to execute the current operation (which is a no-op), set the current operation to addition, clear the list of currently entered numbers, and update the display:

  defcast add, state: state do
    update_state(state, :addition) |> new_state
  end

  def update_state(state, new_operation) do
    entered_number = get_entered_number(state.numbers)
    new_value = get_new_value(state.value, entered_number, state.current_operation)
    new_display = "#{new_value}"
    %{state | numbers: [], display: new_display, value: new_value, current_operation: new_operation}
  end

  def get_new_value(current_value, number, operation) do
    case operation do
      :noop     -> number
      :addition -> current_value + number
    end
  end

  def get_entered_number(numbers) do
    {entered_number, _rest} = numbers |> Enum.join |> Integer.parse
    entered_number
  end

Run the tests, and our next test failure is on the cast to equals. Let's fix that. All equals should do is update the display and clear the numbers list. This can be accomplished by using the :noop operation we just defined:

  defcast equals, state: state do
    update_state(state, :noop) |> new_state
  end

Run the tests, and our basic addition test works. Let's add one for subtraction:

  test "basic subtraction" do
    {:ok, server} = CS.start
    server |> CS.number(1)
    server |> CS.subtract
    server |> CS.number(2)
    server |> CS.equals
    assert "-1" == CS.get_display(server)
  end

Run the tests, and they fail because we haven't defined the subtract cast yet. Go ahead and add it:

  defcast subtract, state: state do
    update_state(state, :subtraction) |> new_state
  end

Run the tests, and they'll fail because there's no :subtraction case defined in get_new_value. Define it:

  def get_new_value(current_value, number, operation) do
    case operation do
      :noop        -> number
      :addition    -> current_value + number
      :subtraction -> current_value - number
    end
  end

Alright, the tests pass. We'll add tests very quickly for multiplication and division:

  test "basic division" do
    {:ok, server} = CS.start
    server |> CS.number(4)
    server |> CS.divide
    server |> CS.number(2)
    server |> CS.equals
    assert "2.0" == CS.get_display(server)
  end

  test "basic multiplication" do
    {:ok, server} = CS.start
    server |> CS.number(2)
    server |> CS.multiply
    server |> CS.number(3)
    server |> CS.equals
    assert "6" == CS.get_display(server)
  end

And we know the tests won't pass yet, we'll just quickly add the implementations:

  defcast multiply, state: state do
    update_state(state, :multiplication) |> new_state
  end

  defcast divide, state: state do
    update_state(state, :division) |> new_state
  end

  def get_new_value(current_value, number, operation) do
    case operation do
      #...
      :multiplication -> current_value * number
      :division       -> current_value / number
    end
  end

Run the tests, and they should all pass. Now our core calculator logic is done, and all that remains is to spawn a calculator server when the window is started, and to wire up button presses to the appropriate genserver casts. Let's get into that. Before we do that, let's extract the CalculatorServer module out of the test, into its own file:

(do that)

Next, open up lib/wx_calc/window.ex again. We'll spin up a CalculatorServer after we show the window:

  # We'll alias it so we don't have to type quite as much
  alias WxCalc.CalculatorServer

  def start do
    #...
    # We need to capture the display so we can update it later.  It's hacky, but
    # we'll return it from the setup function
    display = setup(panel) # (also go to setup and return display)
    # We'll spin up the server
    {:ok, calc} = CalculatorServer.start
    # Then pass the pid into the loop, as well as the handle on the display text
    # control
    loop(panel, calc, display)
    :wxFrame.destroy(panel)
  end

All that's left is modifying the loop. At the beginning of each loop we want to update the display:

  def loop(panel, calc, display) do
    #...
    :wxTextCtrl.setValue(display, to_char_list(CalculatorServer.get_display(calc)))
    #...
  end

Then we want to handle each of the buttons appropriately. We'll need to modify handle_button to accept the server pid as a second argument as well. We also need to modify our tail recursive calls back into loop to pass in the two new arguments:

  def loop(panel, calc, display) do
    :wxTextCtrl.setValue(display, to_char_list(CalculatorServer.get_display(calc)))
    receive do
      wx(event: wxClose()) ->
        IO.puts "close_window received"
      wx(id: id, event: wxCommand(type: :command_button_clicked)) ->
        handle_button(id, calc)
        loop(panel, calc, display)
      event ->
        IO.inspect(event)
        IO.puts "Message received"
        loop(panel, calc, display)
    end
  end

Finally, modify handle_button to pass the appropriate casts into our CalculatorServer:

  def handle_button(@one, calc), do: calc |> CalculatorServer.number(1)
  def handle_button(@two, calc), do: calc |> CalculatorServer.number(2)
  def handle_button(@three, calc), do: calc |> CalculatorServer.number(3)
  def handle_button(@four, calc), do: calc |> CalculatorServer.number(4)
  def handle_button(@five, calc), do: calc |> CalculatorServer.number(5)
  def handle_button(@six, calc), do: calc |> CalculatorServer.number(6)
  def handle_button(@seven, calc), do: calc |> CalculatorServer.number(7)
  def handle_button(@eight, calc), do: calc |> CalculatorServer.number(8)
  def handle_button(@nine, calc), do: calc |> CalculatorServer.number(9)
  def handle_button(@zero, calc), do: calc |> CalculatorServer.number(0)
  def handle_button(@add, calc), do: calc |> CalculatorServer.add
  def handle_button(@subtract, calc), do: calc |> CalculatorServer.subtract
  def handle_button(@mult, calc), do: calc |> CalculatorServer.multiply
  def handle_button(@div, calc), do: calc |> CalculatorServer.divide
  def handle_button(@equal, calc), do: calc |> CalculatorServer.equals
  def handle_button(_, _),       do: IO.puts("something else")

Alright, fire it up, and you can use it for some basic calculations. Pretty neat!

Summary

In today's episode, we made our calculator work. In the process, we learned about defrecordp and we learned how to handle events from buttons in wx.

This calculator only works for integers, and it can get into at least one crashing state. At any rate, it was a fun exercise in building a functional GUI using Elixir and wx. I hope you enjoyed it. See you soon!

Resources