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 -
-
A decrement
-
button that decreases a counter. -
An increment
+
button that increases a counter. -
A label displaying counter value.
-
A reset button for the counter that exists only while the counter is not at its default value.
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 -
-
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
. -
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. -
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.