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
ScrollPane
instances currently provide vertical scrolling only.- The draggable scroll button size is fixed, and does not resize proportionally to the content length and/or pane size.
Known issue in v0.18.0
Outside of a ComponentGroup
it is difficult to arrange for the scroll button not to leave the bottom of the scroll bar, meaning, the top of the button can be drag right down to the bottom pixel of the scroll area with the body of the button beyond it. Look out for an update in the next release when the issue has been resolved.
Example Links
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 labelBounds = Bounds(0, 0, 300, 70)
val label: Label[Int] =
Label[Int](
"Count: 0",
(_, label) => labelBounds
) { case (offset, label, dimensions) =>
Outcome(
Layer(
TextBox(label)
.withColor(RGBA.White)
.moveTo(offset.unsafeToPoint)
.withSize(dimensions.unsafeToSize)
.withFontSize(64.pixels)
)
)
}
.withText((i: Int) => "Count: " + i)
val scrollButton: Button[Unit] =
Button[Unit](Bounds(16, 16)) { (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)
)
)
)
val pane: ScrollPane[Label[Int], Int] =
ScrollPane(
BindingKey("scroll pane"),
labelBounds.dimensions.withHeight(labelBounds.dimensions.height / 2),
label,
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
For the present function, we're going to render something slightly more elaborate than usual.
Rendering a scroll pane is the same as rendering any other component, but so that you can see where the label and the mask are, we're also going to render a couple of shapes illustrating their boundaries.
def present(context: Context[Unit], model: Model): Outcome[SceneUpdateFragment] =
val ctx = UIContext(context.forSubSystems, Size(1), 1)
.moveBoundsBy(Coords(50, 50))
.copy(reference = model.count)
val labelBounds =
CustomComponents.labelBounds.unsafeToRectangle.moveBy(50, 50)
val labelBorder =
Shape.Box(
labelBounds,
Fill.None,
Stroke(1, RGBA.Green)
)
val maskBorder =
Shape.Box(
labelBounds.resize(labelBounds.size.width, labelBounds.size.height / 2),
Fill.None,
Stroke(1, RGBA.Cyan)
)
model.component
.present(ctx)
.map(c => SceneUpdateFragment(c).addLayer(labelBorder, maskBorder))