As stated in a previous post I am in the process of building a GUI application and I would like to use Clojure for that. In this post I would like to write about my experiences building a small app for evaluation of the halgari/fn-fx Clojure library that aims at building GUIs with JavaFX in a declarative way, similar to what React does for the browser DOM.

Disclaimer: Why not Electron?

I also considered using Electron but I would prefer a solution that runs on the JVM so that I can make full use of that. However, if you can live with the JS runtime I would definitely recommend checking out Electron too. Probably I will have a follow-up post about Electron. 🙂

Description of the app to be built

I did implement a simple app concept with various technologies (e.g. fn-fx or Electron) to get some impressions how they differ. The app is quite simple but is supposed to cover some important basics like local file access, native dialogs, state updates and rich widgets:

The app should have a dialog to import a local csv file

The csv file should get imported (all numerical data)

The file contents should get rendered in a table

The data from the file gets rendered as a scatter plot, first column as x-values, each other column as y-values

If you prefer to get the complete code immediately, just check it out on GitHub (there is also an Electron version which is not yet finished though).

Project Setup

Leiningen

I am using Leiningen, here is what the project.clj looks like:

(defproject fn-fx-ui "0.1.0-SNAPSHOT" :description "FIXME: write description" :url "http://example.com/FIXME" :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} :dependencies [[org.clojure/clojure "1.8.0"] [halgari/fn-fx "0.3.0-SNAPSHOT"] [org.clojure/data.csv "0.1.3"]] :main fn-fx-ui.javafx-init :aot [fn-fx-ui.javafx-init] :target-path "target/%s" :profiles {:uberjar {:aot :all}})

Of course fn-fx was added as a dependency and also org.clojure/data.csv to handle the csv data. The only other important details are the declaration of the :main namespace and explicitly enabling :aot for that. This is a JavaFX initialisation namespace that we need for lein run as well as running the application from an Uberjar.

JavaFX initialisation

Here is the whole fn-fx-ui.javafx-init namespace:

(ns fn-fx-ui.javafx-init (:require [fn-fx-ui.core :as core]) (:gen-class :extends javafx.application.Application)) (defn -start [app stage] (core/start {:root-stage? false})) (defn -main [& args] (javafx.application.Application/launch fn_fx_ui.javafx_init (into-array String args)))

The :gen-class statement in the namespace declaration is important! We need to generate a class for being able to use the -main function as an entry point. But it is as important to extend the JavaFX Application class: This is needed for JavaFX to start properly. If you don’t do that you might run into compilation issues and hanging applications on exit.

Extending Application requires us to implement -main as well as -start . The main function just launches the app via direct Java interop passing along the application class ( fn_fx_ui.javafx_init is the name of the Java class generated due to our namespace declaration) and the main arguments.

Calling launch ultimately invokes the -start function. Here we simply call another start function from our core namespace core/start . I am going to explain the argument {:root-stage? false} in the next section.

Let us have a look at the core namespace now.

Application entry point and state handling

The application state is handled in a single atom on the namespace top-level:

(def initial-state {:options {:csv {:first-row-headers false}} :root-stage? true :data [[]]}) (defonce data-state (atom initial-state))

initial-state just exists to being able to reset the data-state atom easily which can be very useful when working from the REPL.

Here is the start function mentioned above, the entry point in our core application:

(defn start ([] (start {:root-stage? true})) ([{:keys [root-stage?]}] (swap! data-state assoc :root-stage? root-stage?) (let [handler-fn (fn [event] (println event) (try (swap! data-state handle-event event) (catch Throwable exception (println exception)))) ui-state (agent (fx-dom/app (stage @data-state) handler-fn))] (add-watch data-state :ui (fn [_ _ _ _] (send ui-state (fn [old-ui] (println "-- State Updated --") (println @data-state) (fx-dom/update-app old-ui (stage @data-state)))))))))

The root-stage? argument gets written to the application state atom. The handler-fn receives events triggered from UI elements like clicking a button. In addition to printing some logs and handling exceptions it only updates the data-state atom with whatever a function handle-event returns when called with the UI event . handle-event is a multimethod handling the various events, we will see it later.

The next let binding is very important ui-state is an agent holding the app created by the fn-fx library via fx-dom/app . The first argument is a “stage” returned by (stage @data-state) . This is the rendered root element of the declaratively defined UI tree. We will see the UI tree in the next section. The app function also takes the handler-fn as an argument so it can invoke that with any UI events.

The last thing we need to do is to register a watcher on the data-state atom. Whenever the state changes this watcher will be triggered and send an update function to the ui-state agent. This will call fx-dom/update-app with the old UI state and a new rendered “stage” based on the updated data-state . This function will do a diff between the old state and the new state and update only the UI elements that need an update. You basically get the same update mechanism like you would in React and similar frameworks. Every UI update is driven by a change in the application state instead of fiddling with UI updates yourself.

Declaring the GUI

The stage function we called above is being created by the following usage of the defui macro provided by fn-fx :

(defui Stage (render [this {:keys [root-stage? data options] :as state}] (controls/stage :title "fn-fx-ui" :on-close-request (force-exit root-stage?) :shown true :scene (controls/scene :root (controls/border-pane :top (controls/h-box :padding (javafx.geometry.Insets. 15 12 15 12) :spacing 10 :alignment (javafx.geometry.Pos/CENTER) :children [(controls/button :text "Import CSV" :on-action {:event :import-csv :fn-fx/include {:fn-fx/event #{:target}}}) (controls/check-box :text "Import first row as headers" :selected (get-in options [:csv :first-row-headers]) :on-action {:event :toggle-option :path [:csv :first-row-headers]}) (controls/button :text "Reset" :on-action {:event :reset})]) :center (controls/v-box :children [(table {:data data}) (plot {:data data})]))))))

This is a declarative description of our app root UI element as a simple Clojure map. Well, there is the initial definition of render as an implementation of a protocol function (see IUserComponent in fn-fx ) this is how you get the state passed to be used in the UI tree.

The function calls like controls/stage , controls/border-pane and controls/check-box are very thin wrappers around the JavaFX component APIs. The names arguments passed to those functions, like :text "Import CSV", are turned into corresponding setter methods of the Java API (e.g. .setText(“Import CSV”)`. This is pretty cool, luckily the JavaFX API is very consistent across components and thus you can pretty much use any setter like that to define the component. I hope most of the settings make sense without having to explain them but when in doubt, please just check the corresponding doc for the JavaFX component and you will see it match easily.

I think noteworthy are three things here:

the Stage component gets a handler for the :on-close-request (will trigger the Java setter [setOnCloseRequest] to force an application exit

(will trigger the Java setter [setOnCloseRequest] to force an application exit UI actions are being defined via :on-action (maps to setter setOnAction )

(maps to setter ) custom components table and plot are being used in the very end

I am going to explain these three things in the next sections.

Forcing application exit

When closing the window of our stage, the one described in the declarative tree above, we still have the stage from the javafx-init namespace if we used that (like we would with lein run or from an Uberjar). This means the application process would not stop because we still have a running stage. But that remaining stage does not have an actual window which could be closed. So by setting root-stage? to false , I enforce closing the whole application when we close our main stage:

(defn force-exit [root-stage?] (reify javafx.event.EventHandler (handle [this event] (when-not root-stage? (println "Closing application") (javafx.application.Platform/exit)))))

On a macOS machine you might not want to do that to mimic the OS behaviour of not closing applications automatically when closing the last window. You might want to tweak this to fit to your needs and maybe have an OS dependent solution.

Registering UI actions

Via :on-action the UI actions are being set. The value can be a map of whatever you want to pass along to the registered event handler. Here is an example from the code above:

(controls/button :text "Reset" :on-action {:event :reset})

I am using the :event key-value pair for dispatching in a multimethod handling all events. So this is a very simple example but you could have any additional data in the :on-action map that you would like to use. fn-fx has a cool mechanism to include contextual data in the map automatically. Here is an example that includes the target of an event (the component on which the event was triggered):

(controls/button :text "Import CSV" :on-action {:event :import-csv :fn-fx/include {:fn-fx/event #{:target}}})

Here is how to use that contextual data from the event handler for the :import-csv event:

(defmethod handle-event :import-csv [{:keys [options] :as state} {:keys [fn-fx/includes]}] (let [window (.getWindow (.getScene (:target (:fn-fx/event includes)))) dialog (doto (FileChooser.) (.setTitle "Import CSV")) file (util/run-and-wait (.showOpenDialog dialog window)) data (with-open [reader (io/reader file)] (doall (csv/read-csv reader)))] (assoc state :file file :data (if (get-in options [:csv :first-row-headers]) data (cons (map #(str "x" (inc %)) (range (count (first data)))) data)))))

You can see that the event handler gets the state and the value of the :on-action declaration (check back at start : handle-event is used as a function to update the data-state via swap! ). The contextual data can easily be retrieved from the event. Above, destructuring is being used in the function signature. The first let binding then uses the includes map to get a JavaFX Window object.

Open file dialog

A dialog is being built on the next binding. To show the dialog for opening a file, we use the fn-fx function util/run-and-wait to open the dialog on the JavaFX Application Thread (this is the UI thread!) and wait until it was closed. The return value is the selected file which then gets read, parsed and associated in the state map. The return value of handle-event will be swapped as the new value into data-state (see handler-fn in start function).

Custom components

Remember those calls to plot and table in the UI tree? Those are components defined in dedicated functions. Rendering a table requires an additional component for each column and also a factory to create table cell values. Everything we need for the table looks like this:

(defn cell-value-factory [f] (reify javafx.util.Callback (call [this entity] (ReadOnlyObjectWrapper. (f (.getValue entity)))))) (defui TableColumn (render [this {:keys [index name]}] (controls/table-column :text name :cell-value-factory (cell-value-factory #(nth % index))))) (defui Table (render [this {:keys [data]}] (controls/table-view :columns (map-indexed (fn [index name] (table-column {:index index :name name})) (first data)) :items (rest data) :placeholder (controls/label :text "Import some data first"))))

I think this should not be too hard to understand. Just nested components with columns being built via map-indexed . The stuff happening in cell-value-factory is a very basic implementation to make JavaFX happily accept it and render the values.

JavaFX component constructor with mandatory arguments

Ok, does that sound a bit scary? At first I though I hit a wall with fn-fx when I wanted to use the ScatterChart component of JavaFX because it has some mandatory arguments for its constructor. With everything we saw so far, fn-fx would first construct an object and then use setters to further specify attributes. This does not work with ScatterChart but here is a solution, pay attention to the call of diff/component :

(defui Plot (render [this {:keys [data]}] (diff/component [:javafx.scene.chart.ScatterChart [] [(javafx.scene.chart.NumberAxis.) (javafx.scene.chart.NumberAxis.)]] {:data (data->series data)})))

We call diff/component with two arguments: A “type” of component which in our case is a vector with the first element being a key for the JavaFX class, the second argument is a vector with the names of named arguments (in our case [] ) and the last element is a vector with the argument values (two JavaFX NumberAxis here which are the mandatory arguments). The second argument are the specifications for the component the same way as we saw them before, so :data 123 would call the setter .setData(123) .

So this way we can handle the situations where we have to invoke the constructor in a custom way instead of being able to just use whatever fn-fx has already wrapped nicely.

To conclude the plotting component I want to show you the data->series function:

(defn data->series [data] (if (some? data) (let [transpose #(apply mapv vector %) transposed-data (transpose data) xs (rest (first transposed-data)) build-series (fn [[name & ys]] (diff/component :javafx.scene.chart.XYChart$Series {:name name :data (map #(javafx.scene.chart.XYChart$Data. (bigdec (first %)) (bigdec (second %))) (transpose [xs ys]))}))] (map build-series transposed-data)) []))

I hope there are no big questions marks about that anymore now. We need to add multiple series to the plot each with a set of data points to be plotted.

Conclusions

Hopefully you got some insights about how to work with fn-fx and if this is something you would like to do. I actually like it a lot after having had to figure out a couple of things on my own. Also I had to patch fn-fx a bit as some things were not yet implemented or not working as I needed them to. So at the time writing this, there is an open PR for those changes. Kudos to Timothy for putting fn-fx out there!!

The full code is available on GitHub. In my opinion this could be a very viable way of building cross-platform GUIs with Clojure, expecting to run into some issues seems fair considering how fresh the library is. So please be aware of that risk should you plan to rely on it.

If you have any questions about the topic, would like to sync or chat, please get in touch with me or just leave a comment. I am looking forward to continue my work with fn-fx ! Nevertheless I also plan to write about my experience with other approaches including Electron, so stay tuned.

Happy hacking!