An FP game engine for Scala.

UI Components: Component List

Component groups and component lists are containers for other components. The difference between the two is that a component group is a known collection of components that can be dynamically sized; while a component list is a dynamic collection of components with more limited layout options.

Using component groups and lists, component UI system provides out of the box functionality for standard requirements like re-flowing / responsively positioned UI elements. Indigo's UI system is not as clever as using something like HTML, but should satisfy many common use cases.

How to set up a component list

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 our component list

We're going to set up a component list of labels. It isn't necessary, but in this example our list will have a dynamic number of elements based on a value in the game's model, passed across as reference data in the UIContext.

This dynamic layout ability is something ComponentGroup cannot do, but reduces the available layout options. So you need to choice the right component type for your needs.

object CustomComponents:

  val listOfLabels: ComponentList[Int] =
    ComponentList(Dimensions(200, 80)) { (labelCount: Int) =>
      (1 to labelCount).toBatch.map { i =>
        ComponentId("lbl" + i) -> Label[Int](
          "Custom label " + i,
          (_, label) => Bounds(0, 0, 250, 20)
        ) { case (offset, label, dimensions) =>
          Outcome(
            Layer(
              TextBox(label)
                .withColor(RGBA.White)
                .moveTo(offset.unsafeToPoint)
                .withSize(dimensions.unsafeToSize)
                .withFontSize(20.pixels)
            )
          )
        }
      }
    }
      .withLayout(ComponentLayout.Vertical(Padding(10)))

Setting up the Model

The model contains the component list. Component lists define collections of components, and how they should be laid out. They have various options that affect their layout behaviour.

Here we initialise our model with the component list.

final case class Model(numOfLabels: Int, components: ComponentList[Int])
object Model:

  val initial: Model =
    Model(4, CustomComponents.listOfLabels)

Updating the Model

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.

In this example we need to update the context with the model value, and supply it along with the event to the component list's update method.

  def updateModel(context: Context[Unit], model: Model): GlobalEvent => Outcome[Model] =
    case e =>
      val ctx = UIContext(context.forSubSystems, Size(1), 1)
        .moveBoundsBy(Coords(50, 50))
        .copy(reference = model.numOfLabels)

      model.components.update(ctx)(e).map { cl =>
        model.copy(components = cl)
      }

Presenting the component list

The component list knows how to render everything, we just 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))
      .copy(reference = model.numOfLabels)

    model.components
      .present(ctx)
      .map(l => SceneUpdateFragment(l))