-
Notifications
You must be signed in to change notification settings - Fork 272
Type Safe CSS
Wiki ▸ Documentation ▸ 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.
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
}
}
}
}
}
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.
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)
}
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 the
csspseudoclass` delegate.
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)`.
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
}
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
}
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
}
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;
}
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.
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.
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)
}
}
}
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