Simplest MMCC Example (Nelmish) - bryanedds/Nu GitHub Wiki

Let’s start by looking at the MMCC (Model-Message-Command-Content) way to implement the canonical Elm UI example, Elmish. This example shows the code for a Nu game program that contains 4 very basic elements -

  1. A decrement - button that decreases a counter.

  2. An increment + button that increases a counter.

  3. A label displaying counter value.

  4. A reset button for the counter that exists only while the counter is not at its default value.

image

The full code is as follows (see it on https://github.com/bryanedds/Nu/blob/master/Projects/Nelmish/Nelmish.fs) -

namespace Nelmish
open Prime
open Nu
open Nu.Declarative

// this is our Elm-style model type
type Model =
    int

// this is our Elm-style message type
type Message =
    | Decrement
    | Increment
    | Reset
    interface Nu.Message

// this is our Elm-style game dispatcher
type NelmishDispatcher () =
    inherit GameDispatcher<Model, Message, Command> (0) // initial model value

    // here we handle the Elm-style messages
    override this.Message (model, message, _, _) =
        match message with
        | Decrement -> just (model - 1)
        | Increment -> just (model + 1)
        | Reset -> just 0

    // here we describe the content of the game including its one screen, one group, three
    // button entities, and one text control.
    override this.Content (model, _) =
        [Content.screen "Screen" Vanilla []
            [Content.group "Group" []
                [Content.button "Decrement"
                    [Entity.Position == v3 -88.0f 64.0f 0.0f
                     Entity.Text == "-"
                     Entity.ClickEvent => Decrement]
                 Content.button "Increment"
                    [Entity.Position == v3 88.0f 64.0f 0.0f
                     Entity.Text == "+"
                     Entity.ClickEvent => Increment]
                 Content.text "Counter"
                    [Entity.Position == v3 0.0f 0.0f 0.0f
                     Entity.Text := string model
                     Entity.Justification == Justified (JustifyCenter, JustifyMiddle)]
                 if model <> 0 then
                    Content.button "Reset"
                       [Entity.Position == v3 0.0f -64.0f 0.0f
                        Entity.Text == "Reset"
                        Entity.ClickEvent => Reset]]]]

Let’s step through each part of the code, from the top -

// this is our Elm-style model type
type Model =
    int

Here we have the Model type that users may customize to represent their simulant’s ongoing state. Here we use just an int to represent the counter value shown by the Counter label. If we were to write, say, a custom Text widget, the Model type would be a string instead of an int. You’re not limited to primitive types, however — you may make your model type as sophisticated as you see fit.

// this is our Elm-style message type
type Message =
    | Decrement
    | Increment
    | Reset
    interface Nu.Message

This is the Message type that represents all possible changes that the Message function will handle.

// this is our Elm-style game dispatcher
type NelmishDispatcher () =
    inherit GameDispatcher<Model, Message, Command> (0) // initial model value

This code does three things -

  1. It declares a containing scope for the Elm-style function overrides (seen coming up next) and packages them as a single plug-in for use by external programs such as Nu's world editor, Gaia.

  2. It allows the user to specify the Elm-style model, message, and command types. Here we pass the empty Command type for the command parameter since this simulant doesn’t utilize commands.

  3. The base constructor function takes as a parameter the initial Model value (here, 0).

Let's look at the next bit -

        // here we handle the Elm-style messages
        override this.Message (model, message, _, _) =
            match message with
            | Decrement -> just (model - 1)
            | Increment -> just (model + 1)
            | Reset -> just 0

This is the Message function itself. All it does is match each message it receives to an expression that transform the Model value in an appropriate way.

“But what is that just function?”

Good question! Strictly speaking, the Message functions return both a new Model as well as a list of signals for processing by Message and (here unused) Command functions. But since we don't need to generate additional signals, I use the just function to automatically pair an empty signal list with the new model value. It’s just a little bit of syntactic sugar to keep things readable!

    // here we describe the content of the game including its one screen, one group, three
    // button entities, and one text control.
    override this.Content (model, _) =
        [Content.screen "Screen" Vanilla []
            [Content.group "Group" []
                [Content.button "Decrement"
                    [Entity.Position == v3 -88.0f 64.0f 0.0f
                     Entity.Text == "-"
                     Entity.ClickEvent => Decrement]
                 Content.button "Increment"
                    [Entity.Position == v3 88.0f 64.0f 0.0f
                     Entity.Text == "+"
                     Entity.ClickEvent => Increment]
                 Content.text "Counter"
                    [Entity.Position == v3 0.0f 0.0f 0.0f
                     Entity.Text := string model
                     Entity.Justification == Justified (JustifyCenter, JustifyMiddle)]
                 if model <> 0 then
                    Content.button "Reset"
                       [Entity.Position == v3 0.0f -64.0f 0.0f
                        Entity.Text == "Reset"
                        Entity.ClickEvent => Reset]]]]

Here we have the Content function. The Content function is mostly equivalent to the View function in Elm. Here the Content function defines the game’s automatically-created (and destroyed as is needed) simulants. Studying this structure, we can see that it describes a simulant structure like this -

Screen/
  Group/
    Decrement
    Increment
    Counter
    Reset

The Content function declares that the above hierarchy is instantiated at run-time. Each Content clause can also define its respective simulant’s properties and event handlers in a declarative way.

                [Content.button "Decrement"
                    [Entity.Position == v3 -88.0f 64.0f 0.0f
                     Entity.Text == "-"
                     Entity.ClickEvent ==> Decrement]

Here we have the Decrement button’s Text property defined as “-”, its Position translated up and to the left, and its ClickEvent producing the Decrement message that was handled above.

                 Content.button "Increment"
                    [Entity.Position == v3 88.0f 64.0f 0.0f
                     Entity.Text == "+"
                     Entity.ClickEvent ==> Increment]

Here is the Increment button, which produces the Increment message.

                 Content.text "Counter"
                    [Entity.Position == v3 0.0f 0.0f 0.0f
                     Entity.Text := string model
                     Entity.Justification == Justified (JustifyCenter, JustifyMiddle)]

Here we see first use of the := operator. This tells the Text property to sets its value to the result of the expression on the right hand side whenever the result of that expression changes.

                 if model <> 0 then
                    Content.button "Reset"
                       [Entity.Position == v3 0.0f -64.0f 0.0f
                        Entity.Text == "Reset"
                        Entity.ClickEvent => Reset]]]]

And lastly, here we have a button that exists only while the outer if expression evaluates to true. So, as long as the model value is non-zero, the button entity will exist. Otherwise, it will not. The engine takes care of creating and destroying the button entity accordingly.