UI Components: Button
This example shows how to set up a lone Button
using Indigo's general UI system.
A word on appearance
Indigo's UI component system is a generic UI layout system, and as such does not come with any specific rendering / look-and-feel helpers. In this example, the component's visuals will be constructed using simple primitives like shapes and text boxes, but the look and feel are only limited by what you can get Indigo to render.
Example Links
How to set up a custom button
Imports
Before we do anything else, we'll need some additional imports:
import indigoextras.ui.*
import indigoextras.ui.syntax.*
And until issue #814 is resolved, we'll also need this import for convenience:
import indigo.shared.subsystems.SubSystemContext.*
Defining a custom button
To keep the code nice and tidy, we'll define our custom button in a separate object.
The button essentially only has a few elements to it:
- A reference type - unused here, so set to Unit. The reference data can be anything you like, and allows you to pass values down through the component hierarchy for reference during update or interaction.
- A set of bounds - the size of the button.
- Presentation functions - define how the button looks in different states. The normal state is required as part of the button definition, but over and down are optional.
- Actions - what happens when the button is clicked, pressed, or released. In this example, we just emit an event telling Indigo to log a message to the console (handling during model update).
To render the button, we're just using shapes, but you could use sprites, text, or anything else you can think of.
object CustomComponents:
val customButton: Button[Unit] =
Button[Unit](Bounds(32, 32)) { (coords, bounds, _) =>
Outcome(
Layer(
Shape
.Box(
bounds.unsafeToRectangle,
Fill.Color(RGBA.Magenta.mix(RGBA.Black)),
Stroke(1, RGBA.Magenta)
)
.moveTo(coords.unsafeToPoint)
)
)
}
.presentDown { (coords, bounds, _) =>
Outcome(
Layer(
Shape
.Box(
bounds.unsafeToRectangle,
Fill.Color(RGBA.Cyan.mix(RGBA.Black)),
Stroke(1, RGBA.Cyan)
)
.moveTo(coords.unsafeToPoint)
)
)
}
.presentOver((coords, bounds, _) =>
Outcome(
Layer(
Shape
.Box(
bounds.unsafeToRectangle,
Fill.Color(RGBA.Yellow.mix(RGBA.Black)),
Stroke(1, RGBA.Yellow)
)
.moveTo(coords.unsafeToPoint)
)
)
)
.onClick(Log("Button clicked"))
.onPress(Log("Button pressed"))
.onRelease(Log("Button released"))
Setting up the Model
Here we initialise our model with the custom button in it.
final case class Model(button: Button[Unit])
object Model:
val initial: Model =
Model(
CustomComponents.customButton
)
Updating the Model
We do two things here, first we handle the logging of messages when the button is clicked - nothing fancy. Then we pass all other events to the component group to handle in case it's interested.
One thing to note is that we need to construct a UIContext
to pass to the component group.
This is important, if a little cumbersome.
The UIContext
holds any custom reference data we might like to propagate down through the
component hierarchy, but also information about the grid size the UI is operating on (normally
1x1, this feature is really for use cases like ASCII / terminal UIs) and the magnification of
the UI.
The UIContext
also provides the top level position of the component hierarchy, so to move
the group and so the button to a new position, we need to tell the UIContext
to move the
bounds by the desired amount using moveBoundsBy
.
def updateModel(context: Context[Unit], model: Model): GlobalEvent => Outcome[Model] =
case Log(message) =>
println(message)
Outcome(model)
case e =>
val ctx = UIContext(context.forSubSystems, Size(1), 1).moveBoundsBy(Coords(50, 50))
model.button.update(ctx)(e).map { b =>
model.copy(button = b)
}
Presenting the Button
To render the button we need to call the present
method with, once again, and instance of
UIContext, and provide the results to a SceneUpdateFragment.
def present(context: Context[Unit], model: Model): Outcome[SceneUpdateFragment] =
val ctx = UIContext(context.forSubSystems, Size(1), 1).moveBoundsBy(Coords(50, 50))
model.button
.present(ctx)
.map(l => SceneUpdateFragment(l))