Skip to content
This repository has been archived by the owner on Sep 28, 2024. It is now read-only.

Type Safe CSS

Edvin Syse edited this page Apr 26, 2016 · 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 {
        s(wrapper) {
            padding = box(10.px)
            spacing = 10.px
        }

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

            +s(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)

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

Importing the stylesheet

To use a particular stylesheet, import it in the init block of your app class. 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() {
    override val primaryView = MyView::class

    init {
        importStylesheet(Styles::class)
        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.

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()

    init {
        with(root) {
            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.

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.

+s(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)

                +s(hover) {
                    backgroundColor = hoverColor
                }
            }
            +s(alice) {
                +s(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.), angular units (deg, rad, grad, and turn), and temporal units (s, ms)). Simply call the wanted unit on any number to convert it to the internal representation:

s(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
}

s(passwordField) {
    +flat
    backgroundColor = Color.RED
}

Modifier selections (+s)

Similar to & in SCSS, TornadoFX stylesheets supports modifier selections by appending + to the selector statement.

s(button, label) {
    textFill = Color.GREEN
    +s(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-views or call reloadStylesheetsOnFocus() in the init block of your App class. See the built in startup parameters for more information.

Next: Async Task Execution

Clone this wiki locally