Clojure/FX My Life 0.1.1: Once More, With Functions

·

8 min read

When I started this series over at blakefx.medium.com, the thought was always to learn enough about JavaFX to be able to use it efficiently in Clojure. So, here is a parallel path to those articles and the subsequent ones that show how the same things work, but in a dynamic, functional language.

We'll start by aping the Java version as closely as necessary, and along the way will learn where Clojure excels and where it doesn't fit so well with Java-based concepts.

We don't need to rehash my rationale/rant . That, sadly, is unchanged: Everything is still awful and there's no relief in sight. (Maybe Flutter but then again, maybe it'll just get abandoned like every other damn thing in this crazy business.)

Weirdly, it's somewhat easier to get started with JavaFX in Clojure (cf Java. You just add all your JavaFX dependencies to your lein project file, and it downloads everything for you and you have access to them, voila.

(defproject fxgamesc "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
            :url  "https://www.eclipse.org/legal/epl-2.0/"}
  :dependencies [[org.clojure/clojure "1.10.1"]
                 [org.openjfx/javafx-controls "17"]
                 [org.openjfx/javafx-swing "17"]
                 [org.openjfx/javafx-media "17"]
                 [org.openjfx/javafx-fxml "17"]
                 [org.openjfx/javafx-web "17"]
                 [org.openjfx/javafx-fxml "17"]
                 [org.openjfx/javafx-graphics "17"]]
  :aot [fxgamesc.Controller]
  :repl-options {:init-ns fxgamesc.core})

Note the :aot line. We'll get to that in a bit. Note also that we are going to be running from the REPL primarily, so there's no app generated here.

Actually doing anything with JavaFX requires doing initializing it and having a way to do things on the Java thread. Used to be that your best bet was to create a Swing panel and put your JavaFX stuff in that panel, because otherwise you have to create an Application descendent.

(defn initialize []
  (try
    (Platform/startup #(Platform/setImplicitExit false))
    (catch IllegalStateException _
      :already-initialized)))

(initialize)

The setImplicitExit thing is important. If you set that to true, JavaFX will terminate when your main window is closed. I'm not sure what how we'll interact with that, but for development purposes we'll err on the side of leaving everything up all the time.

Most of the time we won't need to be on the JavaFX thread, but to actually show the scene, for example, we will. I lifted the following code, mostly unexamined, from github.com/cljfx, which is a nearly full-featured approach to JavaFX programming that we won't be using, at least not right away.

(ns fxgamesc.platform
  (:import (javafx.application Platform)))

;Lifted from https://github.com/cljfx
;I don't know that I'll stick with this, however. I've seen alternatives and when I understand what's there for what reason...
(defmacro run-later [& body]
  `(let [*result# (promise)]
     (Platform/runLater
       (fn []
         (let [result# (try
                         [nil (do ~@body)]
                         (catch Exception e#
                           [e# nil]))
               [err# ~'_] result#]
           (deliver *result# result#)
           (when err#
             (.printStackTrace ^Throwable err#)))))
     (delay
       (let [[err# val#] @*result#]
         (if err#
           (throw err#)
           val#)))))

(defmacro on-fx-thread [& body]
  `(if (Platform/isFxApplicationThread)
     (deliver (promise) (do ~@body))
     (run-later ~@body)))

OK, that's a little ugly, but we don't have to think about it much for now. Let's see it in use: To get to our part 2 conclusion, we just need to pop-up a window:

(def s (on-fx-thread (Stage.)))
(.setTitle @s "Hello, world!")
(on-fx-thread (.show @s))

Bam! Pretty easy and succinct. On to part 3, which involved loading an FXML. You may recall this unlovely bit of Java:

FXMLLoader.load(getClass().getResource("../resources/main/frame.fxml"))

Frame is our FXML with buttons, though at the time, we called it "sample.fxml", I believe. In Clojure, that becomes:

(FXMLLoader/load (io/resource resource-name)))

Which is nice, but it also presents us our first big problem: FXML loading requires a controller. Demands it!

So I made a controller:

(ns fxgamesc.Controller)

(gen-class
  :name fxgamesc.Controller
  :prefix ct-
  :main true
  :init init
  :constructors {[] []})

(defn ct-init[]
  (println "INITIALIZING!"))

Once we make sure this gets compiled (remember this from project.clj?):

  :aot [fxgamesc.Controller]

We can now load our FXML!

I've never created a named Java class like this—never needed to—but there's a lot of talk about the issues that arise from using aot and gen-class, so we may very well find this is an unhappy way to work.

Switching Screens

Right now, our code just looks like this:

(def s (on-fx-thread (Stage.)))
(.setTitle @s "Fun 'n' Games with JavaFX -- Clojure Style")
(def t (Scene. (Group.) 800 600))
(on-fx-thread (.setScene @s t))
(on-fx-thread (.show @s))

And we want to replace it with the "frame" fxml we just loaded. Back in the original part 3, we created a whole class for controlling the screen before deciding we would do most things by swapping out nodes.

As Java classes go, it was pretty sleek:

package fxgames;

import javafx.scene.Scene;
import javafx.scene.layout.Pane;

import java.util.HashMap;

public class ScreenController {
    private final HashMap<String, Pane> screenMap = new HashMap<>();
    private final Scene main;
    public ScreenController(Scene main) {
        this.main = main;
    }
    protected void addScreen(String name, Pane pane) {
        screenMap.put(name, pane);
    }
    protected void removeScreen(String name) {
        screenMap.remove(name);
    }
    protected void activate(String name) { main.setRoot(screenMap.get(name));
    }
}

But it's just not how we roll in Clojure. We wouldn't have a class. Honestly, this probably wouldn't warrant its own file:

(def screens (atom nil))
(defn add-screen [handle resource-name]
  (swap! screens assoc handle (FXMLLoader/load (io/resource resource-name))))
(defn switch-screen [scene new-root]
  (.setRoot scene (new-root @screens)))

The screens atom holds a hash-map which we've set up to be key=>loaded-fxml in add-screen, and switch-screen just sets the root of whatever scene is passed to whatever fxml is requested.

Note that this makes our switch-screen code completely independent of any particular screen: We can open a zillion windows and use this with impunity on any of them.

To actually use it:

(add-screen :frame "frame.fxml")
(switch-screen t :frame)

CSS Time

To wrap up the third entry in the original—and to be fair, note that there was a lot of discussion in those blog posts about how and why things were done, and no small amount of flailing about—we just need to style the buttons with CSS.

Right now, we get this:

image.png

And we fixed this in Java with:

scene.getStylesheets().add(getClass().getResource("../resources/css/styles.css").toExternalForm());

Good lord. OK, let's break this down. I didn't really understand it before; it's just what is shown everywhere for loading stylesheets. The getClass gives us the meta-information for getResource. That is, it tells getResource where to look.

A terrific thing about the REPL is we can actually see what's going on:

(io/resource "styles.css")
=> #object[java.net.URL 0x56adf77e "file:/C:/Users/edward/projects/clojx/fxgamesc/resources/styles.css"]

OK, so it gets us a URL with a filename. Now, what if we wrap it with a toExternalForm?

(.toExternalForm (io/resource "styles.css"))
=> "file:/C:/Users/edward/projects/clojx/fxgamesc/resources/styles.css"

OK, now it's literally just a string. Possibly some other things are done in other circumstances but what that tells us is our scene's style-sheets: What is it? Yeah, just an array. Confirmable with:

(.getStylesheets t)
=> []

What if we add a stylesheet to it?

(.add (.getStylesheets t)  (.toExternalForm (io/resource "styles.css")))
=> true
(.getStylesheets t)
=> ["file:/C:/Users/edward/projects/clojx/fxgamesc/resources/styles.css"]

And the instant we do that (in the REPL no less!) the screen changes:

image.png

Damn, son! You can literally make changes in the CSS file and type:

(.clear (.getStylesheets t))
=> nil
(.add (.getStylesheets t)  (.toExternalForm (io/resource "styles.css")))

in the REPL and they will appear instantly. There are probably cleaner ways to do this, but what an amazing amount of power we have now.

Closing Thoughts

The next section of the Java version had us creating the main tile pane, and a tic-tac-toe scene so we could experiment with animations. It will be interesting to see what exciting things await us with our REPL-based development. Too, the REPL gives us insights into how things work under the covers of Java, which can only help us on that front.

Java's tooling is remarkably smooth: IntelliJ's code completion, debugging and quick execution is really remarkable. But it's amazing how irrelevant a lot of that can become in Clojure: The syntax is so simple, you don't need all these options for how to build a for-loop or whether something should be a lambda; you need less debugging when you can see what's going on directly in the REPL; and it's hard to beat not having to fire up execution at all, because you already have a REPL and it's live and interactive.

Like, this code here is not great:

(ns fxgamesc.core
  (:require [clojure.java.io :as io]
            [fxgamesc.platform :refer [on-fx-thread initialize]]
            [fxgamesc.Controller])
  (:import (javafx.stage Stage)
           (javafx.scene Scene Group)
           (javafx.fxml FXMLLoader)))

(initialize)

(def screens (atom nil))
(defn add-screen [handle resource-name]
  (swap! screens assoc handle (FXMLLoader/load (io/resource resource-name))))
(defn switch-screen [scene new-root]
  (.setRoot scene (new-root @screens)))

(def s (on-fx-thread (Stage.)))
(.setTitle @s "Fun 'n' Games with JavaFX -- Clojure Style")
(def t (Scene. (Group.) 800 600))
(on-fx-thread (.setScene @s t))
(on-fx-thread (.show @s))
(add-screen :frame "frame.fxml")
(switch-screen t :frame)
(.add (.getStylesheets t)  (.toExternalForm (io/resource "styles.css")))

Using s and t for variable names is needlessly obscure (except you can just look and see what they are, since they're used in an eight-line scope) but you can actually look at and interact with them:

s
=>
#object[clojure.lang.Delay
        0x7b7fe83f
        {:status :ready, :val #object[javafx.stage.Stage 0x23cbe482 "javafx.stage.Stage@23cbe482"]}]
t
=> #object[javafx.scene.Scene 0x3ed60675 "javafx.scene.Scene@3ed60675"]

t is just a Scene and s is just a Stage wrapped in a Delay—an artifact of creating it on the javafx thread. (That just means to get to the stage, you have to dereference it: @s.) But we can literally call any method on these classes and see immediately what will happen.

Very cool.

As always, we'll be finding ways to be more efficient, more elegant and less...dopey, but this is a good start. Code is here .