Sassy JavaFX

As may JavaFX developers know, it's possible to style a JavaFX application using CSS, which is quite refreshing compared to previous alternatives. Unfortunately this CSS support is confined to a subset of CSS 2.1 (see here for more information). This means we can't rely on the latest and greatest features available in CSS3 and beyond, bummer.

However we can get some of those missing features (such as variables!) if we turn to less or Sass. I've picked Sass as at the time of writing both Gradle and Maven plugins for less seem to have stopped development altogether. Now, there are a handful of Sass plugins for Gradle out there, I've selected the compass plugin by Robert Fletcher as it's the most versatile even though it does not work well with Gradle 4.5+ (ticket). In the case of Maven there's also a Sass plugin. An example project with both Gradle and Maven setup is available at https://github.com/aalmiray/javafx-sass-demo; it makes use of DesktopPaneFX to show different colors applied to similar elements. Each one of the internal windows follows a similar setup regarding style, albeit the color scheme is different; it makes perfect sense to apply Sass mixins and variables to distill the style configuration to its minimum. To give you an idea of what I'm talking about, here's how the application may look like

The Sass file required to generate the stylesheet looks like this

demo.scss

$red-1:   #af0c1e;
$red-2:   #ff112d;
$red-3:   #ff7595;
$blue-1:  #120caf;
$blue-2:  #1f52ff;
$blue-3:  #5ca6ff;
$green-1: #056606;
$green-2: #05b606;
$green-3: #13fa6e;
$black-1: #222222;
$black-2: #444444;
$black-3: #777777;

@mixin titlebar_style($color1, $color2) {
  -fx-background-color: $color1, linear-gradient($color2 10%, $color1 30%, $color2 100%);
  -fx-border-color: $color1;
}

@mixin internal_window_style($color1, $color2, $color3) {
  @include titlebar_style($color2, $color3);

  & .internal-window-titlebar {
    @include titlebar_style($color2, $color3);
  }

  & .taskbar-icon-button .ikonli-font-icon,
  & .window-0 .internal-window-titlebar-button .ikonli-font-icon {
    -fx-icon-color: $color2;
  }

  &:active {
    @include titlebar_style($color1, $color2);

    & .internal-window-titlebar {
      @include titlebar_style($color1, $color2);
    }

    & .internal-window-titlebar-button .ikonli-font-icon {
      -fx-icon-color: $color1;
    }
  }
}

.taskbar-icon .label,
.internal-window .internal-window-titlebar-title {
  -fx-text-fill: white;
}

.taskbar-icon {
  @include titlebar_style($black-2, $black-3);
  -fx-border-radius: 6;
}

.internal-window {
  @include internal_window_style($black-1, $black-2, $black-3);
}

.window-0 {
  @include internal_window_style($blue-1, $blue-2, $blue-3);
}

.window-1 {
  @include internal_window_style($green-1, $green-2, $green-3);
}

.window-2 {
  @include internal_window_style($red-1, $red-2, $red-3);
}

I'm particularly fond of mixins, as you can see you can embed mixins as much as you like. The use of the & operator also simplifies aggregating selectors. This Sass file will produce a much larger CSS file once processed, which looks like this

demo.css

/* line 44, ../sass/demo.scss */
.taskbar-icon .label,
.internal-window .internal-window-titlebar-title {
  -fx-text-fill: white;
}

/* line 49, ../sass/demo.scss */
.taskbar-icon {
  -fx-background-color: #444444, linear-gradient(#777777 10%, #444444 30%, #777777 100%);
  -fx-border-color: #444444;
  -fx-border-radius: 6;
}

/* line 54, ../sass/demo.scss */
.internal-window {
  -fx-background-color: #444444, linear-gradient(#777777 10%, #444444 30%, #777777 100%);
  -fx-border-color: #444444;
}
/* line 22, ../sass/demo.scss */
.internal-window .internal-window-titlebar {
  -fx-background-color: #444444, linear-gradient(#777777 10%, #444444 30%, #777777 100%);
  -fx-border-color: #444444;
}
/* line 26, ../sass/demo.scss */
.internal-window .taskbar-icon-button .ikonli-font-icon, .internal-window .window-0 .internal-window-titlebar-button .ikonli-font-icon {
  -fx-icon-color: #444444;
}
/* line 31, ../sass/demo.scss */
.internal-window:active {
  -fx-background-color: #222222, linear-gradient(#444444 10%, #222222 30%, #444444 100%);
  -fx-border-color: #222222;
}
/* line 34, ../sass/demo.scss */
.internal-window:active .internal-window-titlebar {
  -fx-background-color: #222222, linear-gradient(#444444 10%, #222222 30%, #444444 100%);
  -fx-border-color: #222222;
}
/* line 38, ../sass/demo.scss */
.internal-window:active .internal-window-titlebar-button .ikonli-font-icon {
  -fx-icon-color: #222222;
}

/* line 58, ../sass/demo.scss */
.window-0 {
  -fx-background-color: #1f52ff, linear-gradient(#5ca6ff 10%, #1f52ff 30%, #5ca6ff 100%);
  -fx-border-color: #1f52ff;
}
/* line 22, ../sass/demo.scss */
.window-0 .internal-window-titlebar {
  -fx-background-color: #1f52ff, linear-gradient(#5ca6ff 10%, #1f52ff 30%, #5ca6ff 100%);
  -fx-border-color: #1f52ff;
}
/* line 26, ../sass/demo.scss */
.window-0 .taskbar-icon-button .ikonli-font-icon, .window-0 .window-0 .internal-window-titlebar-button .ikonli-font-icon {
  -fx-icon-color: #1f52ff;
}
/* line 31, ../sass/demo.scss */
.window-0:active {
  -fx-background-color: #120caf, linear-gradient(#1f52ff 10%, #120caf 30%, #1f52ff 100%);
  -fx-border-color: #120caf;
}
/* line 34, ../sass/demo.scss */
.window-0:active .internal-window-titlebar {
  -fx-background-color: #120caf, linear-gradient(#1f52ff 10%, #120caf 30%, #1f52ff 100%);
  -fx-border-color: #120caf;
}
/* line 38, ../sass/demo.scss */
.window-0:active .internal-window-titlebar-button .ikonli-font-icon {
  -fx-icon-color: #120caf;
}

/* line 62, ../sass/demo.scss */
.window-1 {
  -fx-background-color: #05b606, linear-gradient(#13fa6e 10%, #05b606 30%, #13fa6e 100%);
  -fx-border-color: #05b606;
}
/* line 22, ../sass/demo.scss */
.window-1 .internal-window-titlebar {
  -fx-background-color: #05b606, linear-gradient(#13fa6e 10%, #05b606 30%, #13fa6e 100%);
  -fx-border-color: #05b606;
}
/* line 26, ../sass/demo.scss */
.window-1 .taskbar-icon-button .ikonli-font-icon, .window-1 .window-0 .internal-window-titlebar-button .ikonli-font-icon {
  -fx-icon-color: #05b606;
}
/* line 31, ../sass/demo.scss */
.window-1:active {
  -fx-background-color: #056606, linear-gradient(#05b606 10%, #056606 30%, #05b606 100%);
  -fx-border-color: #056606;
}
/* line 34, ../sass/demo.scss */
.window-1:active .internal-window-titlebar {
  -fx-background-color: #056606, linear-gradient(#05b606 10%, #056606 30%, #05b606 100%);
  -fx-border-color: #056606;
}
/* line 38, ../sass/demo.scss */
.window-1:active .internal-window-titlebar-button .ikonli-font-icon {
  -fx-icon-color: #056606;
}

/* line 66, ../sass/demo.scss */
.window-2 {
  -fx-background-color: #ff112d, linear-gradient(#ff7595 10%, #ff112d 30%, #ff7595 100%);
  -fx-border-color: #ff112d;
}
/* line 22, ../sass/demo.scss */
.window-2 .internal-window-titlebar {
  -fx-background-color: #ff112d, linear-gradient(#ff7595 10%, #ff112d 30%, #ff7595 100%);
  -fx-border-color: #ff112d;
}
/* line 26, ../sass/demo.scss */
.window-2 .taskbar-icon-button .ikonli-font-icon, .window-2 .window-0 .internal-window-titlebar-button .ikonli-font-icon {
  -fx-icon-color: #ff112d;
}
/* line 31, ../sass/demo.scss */
.window-2:active {
  -fx-background-color: #af0c1e, linear-gradient(#ff112d 10%, #af0c1e 30%, #ff112d 100%);
  -fx-border-color: #af0c1e;
}
/* line 34, ../sass/demo.scss */
.window-2:active .internal-window-titlebar {
  -fx-background-color: #af0c1e, linear-gradient(#ff112d 10%, #af0c1e 30%, #ff112d 100%);
  -fx-border-color: #af0c1e;
}
/* line 38, ../sass/demo.scss */
.window-2:active .internal-window-titlebar-button .ikonli-font-icon {
  -fx-icon-color: #af0c1e;
}

We even get code comments linking a particular style to its corresponding Sass rule, neat! We've this setup it's now much easier to update a wide arrange of styles, as it only takes a moment to update one Sass mixin instead of painstakingly updating each one of them by hand. Setting up the Gradle build file is also easy, as witnessed by the following snippet

build.gradle

plugins {
    id 'application'
    id 'com.github.robfletcher.compass' version '2.0.6'
}

mainClassName = 'org.kordamp.javafx.sass.Demo'

repositories {
    jcenter()
}

dependencies {
    compile 'org.kordamp.desktoppanefx:desktoppanefx-core:0.8.0'
}

compass {
    cssDir = file("src/main/resources")
}

compileJava.finalizedBy('compassCompile')

I've set the project in such way that Sass sources are compiled and processed after Java sources are compiled but before resources are processed. The build file also places the generated CSS files into src/main/resources, which is not standard practice, but allows you to tweak the generated CSS files by hand if needed before running the application, plus this configuration is IDE friendly as well. The Maven setup is a bit more verbose and mirrors Gradle, that is, it also places the generated CSS files inside src/main/resources.

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.kordamp.javafx</groupId>
    <artifactId>javafx-sass-demo</artifactId>
    <version>0.1.0</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.kordamp.desktoppanefx</groupId>
            <artifactId>desktoppanefx-core</artifactId>
            <version>0.8.0</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>nl.geodienstencentrum.maven</groupId>
                <artifactId>sass-maven-plugin</artifactId>
                <version>3.5.4</version>
                <configuration>
                    <destination>${basedir}/src/main/resources</destination>
                </configuration>
                <executions>
                    <execution>
                        <id>update-stylesheets</id>
                        <phase>process-resources</phase>
                        <goals>
                            <goal>update-stylesheets</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>1.6.0</version>
                <inherited>true</inherited>
                <configuration>
                    <mainClass>org.kordamp.javafx.sass.Demo</mainClass>
                    <systemProperties>
                        <systemProperty>
                            <key>griffon.env</key>
                            <value>dev</value>
                        </systemProperty>
                    </systemProperties>
                </configuration>
                <executions>
                    <execution>
                        <id>run-app</id>
                        <phase>process-classes</phase>
                        <goals>
                            <goal>java</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

Unfortunately the Maven Sass plugin does not generate commented out links in the CSS files but you get the same generated styles. Now you know how to get Sass configured on your next JavaFX project.

Make it pop, make it shine, make it Sassy.

Keep coding!

Liked it? Take a second to support aalmiray on Patreon!
Become a patron at Patreon!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

ˆ Back To Top