Themes

  Edit on GitHub

While we can set certain properties of a Control node to make it look the way we want, and in some cases it is easier, it can get difficult to handle when dealing with more advanced layouts and could be difficult to refactor.

Instead we can use a Theme that handles rendering the correct drawables, colors, fonts, and even sizes. If you have used a Control nodes that renders something to the screen then you have already used a theme without knowing it.

Using a Theme

Using a theme is dead simple. Assuming we have already created the theme we want, all we have to do is set the theme property on a Control to our new theme. The beauty of this is we only have to set that property on the top-most Control node we want to have the theme. Any children Control nodes under it will use the theme by default.

sceneGraph(context) {
    control {
        theme = myTheme

        // these all have the same theme!
        vBoxContainer {
            button {
                text = "Hello!"
            }

            label {
                text = "Nice!
            }
        }
    }
}

Any changes to the theme property from its parent will propagate down to its children and will recalculate its size based on the new theme values.

Multiple Themes

Being able to set the theme on the only on specific Control nodes that need it is very advantageous. For instance, we can have a top-level Control that uses our default theme. Then we could have a child Control that requires another specific theme, such as a dialog box based theme. Anything that isn’t defined in that dialog based theme resorts to the top-level theme.

sceneGraph(context) {
    control {
        theme = myDefaultTheme

        label { // uses myDefaultTheme
            text = "Title"
        }

        vBoxContainer { // children and itself use myDialogTheme
            theme = myDialogTheme

            button {
                text = "Hello!"
            }

            label {
                text = "Nice!
            }
        }
    }
}

Overriding Theme Values

Sometimes it is not practical to have to create an entire separate theme just for a single Control. Instead what we can do is directly override the theme property value right on the Control we need.

sceneGraph(context) {
    control {
        theme = myDefaultTheme

        label {
            text = "Title"
            font = myPixelFont // overrides theme Label.Font
        }
    }
}

Control Theme Value Order

The order that a Control determines a theme value to use is the following:

  1. Checks for any overridden theme values.
  2. Checks for a theme and the value on set specifically on itself.
  3. Moves up the tree checking each parent for a theme and value until no more Control nodes are available.
  4. Checks the default theme set a Theme.defaultTheme.
  5. And if all else fails, it will resort to either one of the fallback values:
    • Font: Theme.FALLBACK_FONT
    • Drawable: Theme.FALLBACK_DRAWABLE
    • Color: Color.WHITE
    • Constant: 0

Default Theme

LittleKt has a default theme built in that every Control node will fallback to if it doesn’t have a theme set. We can also replace the default theme with our own.

Theme.defaultTheme = myTheme // sets the default theme application wide

We can also reuse the default theme and add our own overrides to it:

val myTheme = createDefaultTheme( // top-level function we can access just like this
    extraDrawables = mapOf(
        // override the "normal" button state drawable with our own ninepatch
        "Button" to mapOf(
            Button.themeVars.normal to NinePatchDrawable(myNinePatch)
        )
    )
)
control.theme = myTheme

Creating a Custom Theme

Currently, the only way to create a custom theme is to directly create a Theme object and map our values to the resource we would like to use. At some point in the future, there could be a way to define a theme using a structure text file of some sort that can be assmebled at runtime.

Each theme come with a few properties that maps a Control theme variable to the value. We don’t have to set each value for each Control theme variable. If a value is missing, it will resort to either the parent Control theme or the default theme.

For example, this is what the default theme looks like:

val greyButtonNinePatch = NinePatch(
    Textures.atlas.getByPrefix("grey_button").slice,
    5,
    5,
    5,
    4
)
val panelNinePatch = NinePatch(
    Textures.atlas.getByPrefix("grey_panel").slice,
    6,
    6,
    6,
    6
)
val drawables = mapOf(
    "Button" to mapOf(
        Button.themeVars.normal to NinePatchDrawable(greyButtonNinePatch)
            .apply { modulate = Color.LIGHT_BLUE },
        Button.themeVars.normal to NinePatchDrawable(greyButtonNinePatch)
            .apply { modulate = Color.LIGHT_BLUE },
        Button.themeVars.pressed to NinePatchDrawable(greyButtonNinePatch)
            .apply { modulate = Color.LIGHT_BLUE.toMutableColor().also { it.scaleRgb(0.6f) } },
        Button.themeVars.hover to NinePatchDrawable(greyButtonNinePatch)
            .apply { modulate = Color.LIGHT_BLUE.toMutableColor().also { it.lighten(0.2f) } },
        Button.themeVars.disabled to NinePatchDrawable(greyButtonNinePatch)
            .apply { modulate = Color.LIGHT_BLUE.toMutableColor().also { it.lighten(0.5f) } },
    ),
    "Panel" to mapOf(
        Panel.themeVars.panel to NinePatchDrawable(panelNinePatch).apply {
            modulate = Color.LIGHT_BLUE
        }
    ),
    "ProgressBar" to mapOf(
        ProgressBar.themeVars.bg to NinePatchDrawable(panelNinePatch).apply {
            modulate = Color.DARK_BLUE
        },
        ProgressBar.themeVars.fg to NinePatchDrawable(panelNinePatch).apply {
            modulate = Color.LIGHT_BLUE.toMutableColor().also { it.lighten(0.5f) }
        }
    )
)

val fonts = mapOf()

val colors = mapOf(
    "Button" to mapOf(Button.themeVars.fontColor to Color.WHITE),
    "Label" to mapOf(Label.themeVars.fontColor to Color.WHITE)
)

val constants = mapOf()

Theme(
    drawables = drawables,
    fonts = fonts,
    colors = colors,
    constants = constants,
    defaultFont = defaultFont
)

Theme Map Types

Each theme expects a map of Drawables, Bitmap Fonts, Colors, and Constants.

Each of these maps are mapped to a String to another map of the specified types. For example, the drawables map expects a String that maps to another map of String that maps to a Drawable.

The first String value is the name of the Control while the second String value is the named of the Control’s theme variable.

val drawables = mapOf(
    "Button" to mapOf(
        Button.themeVars.normal to NinePatchDrawable(myNinePatch)
    )
)
  • Drawables: A map of drawables mapped by Control type, mapped by variable name and value.
  • Fonts: A map of fonts mapped by Control type, mapped by variable name and value.
  • Colors: A map of colors mapped by Control type, mapped by variable name and value. This can include things like font colors, drawable colors, etc.
  • Constants: A map of constants mapped by Control type, mapped by variable name and value. This can include things like padding sizes, boolean values (0 or 1), etc.

Each Control has a static variable called themeVars which references an object that contains any amount of String variables which are used for grabbing theme resources to render.

// Button.kt

// ...
class ThemeVars {
    val fontColor = "fontColor"
    val font = "font"
    val normal = "normal"
    val pressed = "pressed"
    val hover = "hover"
    val hoverPressed = "hoverPressed"
    val disabled = "disabled"
}

companion object {
    // ...
    val themeVars = ThemeVars()
}

Theme Level Font

On top of setting fonts for specific controls, we can also set a default font for the entire theme. We can do that by settings the defaultFont property on a Theme.

What this does is, if a Control requires a BitmapFont and the specific value for that Control is not available, it will resort to using the theme level font. See control theme value order for more info.