An FP game engine for Scala.

UI Components: Scroll Pane

This example shows how to set up a ScrollPane using Indigo's general UI system.

What is a 'Scroll Pane'

Scroll panes have a lot in common with masked panes, but they should be familiar to anyone who has used a windowing system.

Consider viewing a web page in your browser. If the page is too long for the window to show, then you are presented with scroll bars to allow you to reach the content at the bottom of the page.

In this UI System, that scrolling functionality is not built into the notion of windows, it is a standalone component called a ScrollPane. The original use case is to providing scrolling capabilities to windows, however.

Limitations / future enhancements

  1. ScrollPane instances currently provide vertical scrolling only.
  2. The draggable scroll button size is fixed, and does not resize proportionally to the content length and/or pane size.

Reminder: Set the magnification

It's important to note that if you're using a magnification other than 1 (the default), you must set the magnification to the correct value in the UIContext. Otherwise, the shader that handles the content masking / clipping will not mask off the correct area of the screen.

Setting up a ScrollPane

Much like the other examples, we need to define our components, and they've been placed in a separate object.

As well as the scroll pane, we also need something to put in it. In this case, we're going to make a scroll pane that is half the size of a label, so that you can see the masking in action.

We also need a scroll button to show or control (by dragging) how much the pane has been scrolled by.

Note that as with all components, there are a few different ways to constuct them. Here we're using fixed bounds / sizes for simplicity, but there are other options.

object CustomComponents:

  val scrollPaneBounds = Bounds(0, 0, 200, 100)

  val text =
    Text(
      "",
      DefaultFont.fontKey,
      Assets.assets.generated.DefaultFontMaterial
    )

  val listOfLabels: ComponentList[Int] =
    ComponentList(Dimensions(200, 200)) { (ctx: UIContext[Int]) =>
      (1 to ctx.reference).toBatch.map { i =>
        ComponentId("lbl" + i) -> Label[Int](
          "Custom label " + i,
          (_, label) => Bounds(0, 0, 250, 20)
        ) { case (ctx, label) =>
          Outcome(
            Layer(
              text
                .withText(label.text(ctx))
                .moveTo(ctx.parent.coords.unsafeToPoint)
            )
          )
        }
      }
    }
      .withLayout(ComponentLayout.Vertical(Padding(10)))

  val scrollButton: Button[Unit] =
    Button[Unit](Bounds(16, 16)) { (ctx, btn) =>
      Outcome(
        Layer(
          Shape
            .Box(
              Rectangle(
                ctx.parent.bounds.unsafeToRectangle.position,
                btn.bounds.dimensions.unsafeToSize
              ),
              Fill.Color(RGBA.Magenta.mix(RGBA.Black)),
              Stroke(1, RGBA.Magenta)
            )
        )
      )
    }
      .presentDown { (ctx, btn) =>
        Outcome(
          Layer(
            Shape
              .Box(
                Rectangle(
                  ctx.parent.bounds.unsafeToRectangle.position,
                  btn.bounds.dimensions.unsafeToSize
                ),
                Fill.Color(RGBA.Cyan.mix(RGBA.Black)),
                Stroke(1, RGBA.Cyan)
              )
          )
        )
      }
      .presentOver((ctx, btn) =>
        Outcome(
          Layer(
            Shape
              .Box(
                Rectangle(
                  ctx.parent.bounds.unsafeToRectangle.position,
                  btn.bounds.dimensions.unsafeToSize
                ),
                Fill.Color(RGBA.Yellow.mix(RGBA.Black)),
                Stroke(1, RGBA.Yellow)
              )
          )
        )
      )

  val pane: ScrollPane[ComponentList[Int], Int] =
    ScrollPane(
      BindingKey("scroll pane"),
      scrollPaneBounds.dimensions,
      listOfLabels,
      scrollButton
    )
      .withScrollBackground { bounds =>
        Layer(
          Shape.Box(
            bounds.unsafeToRectangle,
            Fill.Color(RGBA.Yellow.mix(RGBA.Black)),
            Stroke.None
          )
        )
      }

When using masked and scroll panes, you need to remember to register the shaders they use. The all import is conveniently a Set, so you can just concatenate it onto any other shaders you are using.

val shaders: Set[ShaderProgram] =
  Set() ++ indigoextras.ui.shaders.all

Rendering a scroll pane is the same as rendering any other component, but so that you can see where the scroll pane is, we're also going to render a shapes around it as a border.

  def present(context: Context[Unit], model: Model): Outcome[SceneUpdateFragment] =
    val ctx = UIContext(context)
      .withMagnification(2)
      .moveParentBy(Coords(50, 50))
      .copy(reference = model.count)

    val scrollPaneBorder =
      Shape.Box(
        CustomComponents.scrollPaneBounds.unsafeToRectangle.moveTo(ctx.parent.coords.unsafeToPoint),
        Fill.None,
        Stroke(1, RGBA.Cyan)
      )

    model.component
      .present(ctx)
      .map(c => SceneUpdateFragment(c).addLayer(scrollPaneBorder))