-
Notifications
You must be signed in to change notification settings - Fork 271
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 {
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
}
}
}
}
}
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.
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)
}
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.), 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
}
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
}
s(passwordField) {
+flat
backgroundColor = Color.RED
}
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;
}
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.
Next: Async Task Execution