Skip to content

Type Safe CSS

Edvin Syse edited this page Mar 10, 2017 · 49 revisions

WikiDocumentationType Safe CSS

Type-Safe CSS

TornadoFX has a type safe DSL for generating CSS, including type safe selector declarations. You can always write your stylesheets manually, but you'll find there are many advantages to using the DSL. It is discoverable, easy to refactor, and is intuitive to use. There is also support for mixins and the ability to generate CSS with code, even CSS based on state or configuration within your app.

Defining a style sheet

A stylesheet is defined by implementing the Stylesheet interface and adding selectors in the init function. It's a best practice to define styles and colors in the companion object of the stylesheet and then reference these constants in the stylesheet and in the view to add classes to nodes.

class Styles : Stylesheet() {
    companion object {
        // Define our styles
        val wrapper by cssclass()
        val bob by cssclass()
        val alice by cssclass()

        // Define our colors
        val dangerColor = c("#a94442")
        val hoverColor = c("#d49942")
    }

    init {
        wrapper {
            padding = box(10.px)
            spacing = 10.px
        }

        label {
            fontSize = 56.px
            padding = box(5.px, 10.px)
            maxWidth = infinity

            and(bob, alice) {
                borderColor += box(dangerColor)
                borderStyle += BorderStrokeStyle(StrokeType.INSIDE, StrokeLineJoin.MITER, StrokeLineCap.BUTT, 10.0, 0.0, listOf(25.0, 5.0))
                borderWidth += box(5.px)

                and(hover) {
                    backgroundColor += hoverColor
                }
            }
        }
    }
}

Importing the stylesheet

To use a particular stylesheet, add it as the second parameter to the App constructor. Optionally, tell TornadoFX to reload your stylesheet whenever the app gets focus, so you can make hot changes and recompile without restarting your app.

class MyApp : App(MyView::class, Styles::class) {
    init {
        reloadStylesheetsOnFocus()
    }
}

Note that if you use the reload function, you can add println(this) to the bottom of your stylesheet you output the rendered stylesheet every time it changes. You'll notice that camelCased selectors are converted to camel-cased names. Stylesheets added this way will be applied to the primary scene as well as all scenes opened via the openModal function.

It is also possible to import a stylesheet manually using the importStylesheet(Styles::class) function, but it is seldom needed.

Applying style classes to components

You can choose to use the type safe selectors shown above, or use strings, both for defining selectors and adding classes to nodes. It is a best practice to use type safe selectors everywhere, so that you can track where/if your css is defined and applied.

To add a class to a node, you first define the class in your stylesheet (see above) and then add the class to your node:

class MyView: View() {
    override val root = vbox {
        addClass(Styles.wrapper)

        label("Alice") {
            addClass(Styles.alice)
        }
        label("Bob") {
            addClass(Styles.bob)
        }
    }
}

RENDERED UI:

The other functions removeClass() and hasClass() do just that. removeClass() removes a specified class from a Node and hasClass() returns a boolean indicating if a class is applied to that Node. The toggleClass() will add or remove that class based on a boolean condition. There is also a version of toggleClass that takes an observable boolean property as the second argument. This will make sure the class is only available on the class when the boolean observable is true.

Just like normal CSS, attributes will cascade down and be added or overridden as defined by specific scope. For example, we can override alice to use a blue box color on hover and underline the text.

and(bob, alice) {
    borderColor += box(dangerColor)
    borderStyle += BorderStrokeStyle(StrokeType.INSIDE, StrokeLineJoin.MITER, StrokeLineCap.BUTT, 10.0, 0.0, listOf(25.0, 5.0))
    borderWidth += box(5.px)

    and(hover) {
        backgroundColor += hoverColor
    }
}
and(alice) {
    and(hover) {
        underline = true
        borderColor += box(c("blue"))
    }
}

RENDERED UI:

It is also possible to manipulate classes of multiple components at once. Any List<Node> can be manipulated by addClass(), removeClass() and toggleClass() as they are extension functions on Iterable<Node>. Example:

hbox {
    // Build a very complicated UI here

    // Apply the class 'wrapper' to all children of type HBox
    children.filter { it is HBox }.addClass(wrapper)
}

Default style classes

The Stylesheet class defines constants for all pseudo classes and node classes used in all the default JavaFX components, to there is no need to define classes like hover, label, button and listView.

You can also define #id with the cssid delegate and ':pseudoclasseswith thecsspseudoclass` delegate.

Defining colors

As you may have noticed above, colors are defined in the companion object of your stylesheet. All colors are of type Paint but there are convenience functions to create colors from strings, such as c("#a94442") and c("green"). You can even specify opacity as in c("green", 0.25)`.

Dimensions

All measurements are type safe as well using units. (There is support for linear units (px, %, mm, pt, em, infinity, etc.) and angular units (deg, rad, grad, and turn)). Simply call the wanted unit on any number to convert it to the internal representation:

label {
    minWidth = 100.px    
}

Box

Earlier you saw the box() function. Some properties require you to supply values for top, right, bottom and left in one go. The box function helps you with this:

s(label) {
    padding = box(10.px) // all dimensions have the same value
    padding = box(10.px, 20.px) // vertical = 10, horizontal = 20
    padding = box(10.px, 20.px, 7.px, 14.px) // top, right, bottom, left with individual values
}

Mixins

A mixin defines common properties that can be applied to multiple selectors. Let's imagine that you're creating a flat design for your UI, so you define a mixin and then apply it to your control selectors:

val flat = mixin {
    backgroundInsets += box(0.px)
    borderColor += box(Color.DARKGRAY)
}

s(button, textInput) {
    +flat
    fontWeight = FontWeight.BOLD
}

passwordField {
    +flat
    backgroundColor += Color.RED
}

Modifier selections (and())

Similar to & in SCSS, TornadoFX stylesheets supports modifier selections by wrapping the selection statement with a call to the and() function.

s(button, label) {
    textFill = Color.GREEN
    and(hover) {
        fontWeight = FontWeight.BOLD
    }
}

The rendered stylesheet will contain:

.button, .label {
    -fx-text-fill: rgba(0, 128, 0, 1);
}
.button:hover, .label:hover {
    -fx-font-weight: 700;
}

Selecting nodes in a type safe way

When you have applied your style classes to your nodes, you can use select and selectAll to retrieve the nodes based on their classes:

val wrapper = root.select(wrapper)
val hboxes = root.selectAll(hbox)

Remember that the hbox class is not added to HBoxes by default, so you would have to add it yourself for the above selectAll statement to work.

Live reloading

To have the Stylesheets reload automatically every time the Stage gains focus, start the app with the program parameter --live-stylesheets or call reloadStylesheetsOnFocus() in the init block of your App class. See built in startup parameters for more information.

Control specific stylesheets

JavaFX has a mechanism for including a stylesheet only if a certain control is ever loaded. You can utilise this method even for TornadoFX Type Safe CSS as well. The mechanism requires you to return an string representing an url of the stylesheet in the getUserAgentStylesheet method of the Control class. The Stylesheet class has a function to turn your type safe stylesheet into an url that actually contains the whole base64 encoded stylesheet in the url itself.

class DangerButton : Button("Danger!") {
    init {
        addClass(DangerButtonStyles.dangerButton)
    }
    override fun getUserAgentStylesheet() = DangerButtonStyles().base64URL.toExternalForm()
}

class DangerButtonStyles : Stylesheet() {
    companion object {
        val dangerButton by cssclass()
    }

    init {
        dangerButton {
            backgroundInsets += box(0.px)
            fontWeight = FontWeight.BOLD
            fontSize = 20.px
            padding = box(10.px)
        }
    }
}

Inline styles

You can even use type safe styles for inline style declarations. Here is an example of a TableView column being styled based on the input data:

tableview<Person> {
    items = persons
    column("ID", Person::idProperty)
    column("Name", Person::nameProperty)
    column("Birthday", Person::birthdayProperty)
    column("Age", Person::ageProperty).cellFormat {
        text = it.toString()
        style {
            if (it < 18) {
                backgroundColor += c("#8b0000")
                textFill = Color.WHITE
            }
        }
    }
}

Next: Async Task Execution