« Back to article list

Simple non-Electron Native GUI Development

Table of Contents

Simple non-Electron Native GUI Development

Why would I prefix an article with "non-Electron"? Well, chances are, if you've been looking into native GUI development in the last 5 years and aren't already familiar with one of the existing toolkits (GTK, wxWidgets, QT, Swing, OpenJFX etc.) you've already been down the Electron path and may or may not be a fan.

In this article, I'm going to go over what I feel is a better alternative than all of those - that being a Clojure based React-like library/wrapper around Java OpenJFX called cljfx:

https://github.com/cljfx/cljfx

I'm in the process of writing a pretty expansive application using it - Insectarium (https://github.com/ahungry/insectarium), a native GUI application for bug tracking across many bug reporting/issue tracking platforms (so far, supporting Jira and Github issues).

This tutorial is going to be more of a gentle introduction to cljfx.

Setup

First off - cljfx is opinionated (and, in most cases, in good ways that I agree with). One such way is that it deigns to be forward thinking, and does not attempt to stagger in old ways for supporting old out of date versions of things. As such, it's going to require clojure 1.10 or greater, as well as (from what I've run into during personal testing) best supports java 12 or above (I'm using openjdk 12 under Arch Linux or Ubuntu 19.04).

I'm going to be writing from the vantage point of a GNU/Linux user, including a fully functional/done Dockerfile and source code you can find here:

https://github.com/ahungry/scratch/tree/master/blog/cljfx/counter-gui

If you are using a non-free OS, you can still follow along, but I'll leave the Java setup work to you.

I am assuming familiarity with Clojure in some capacity - if you have none at all, this is a great book to learn and available for free online, or for a small price on Amazon - I just got the Kindle copy and it's nice:

https://www.braveclojure.com/clojure-for-the-brave-and-true/

Dependencies

Please ensure you have the following installed/usable:

  • Java 12+
  • Leiningen
  • Docker (optional if you want to try the Dockerfile)

Setup instructions

Alright - by this point your command for lein version should show something similar to this:

lein -version
Leiningen 2.8.3 on Java 12.0.1 OpenJDK 64-Bit Server VM

Go ahead and prepare the project (if you didn't clone my repo / jump into the sample directory, you're going to want to make a project using lein now):

lein new app counter-gui

This will create a project skeleton.

Inside of it, locate and open the file "project.clj" and edit it as such:

(defproject counter-gui "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/"}
  :plugins [[io.aviso/pretty "0.1.37"]]
  :middleware [io.aviso.lein-pretty/inject]
  :dependencies [[org.clojure/clojure "1.10.0"]
                 [cljfx "1.2.9"]
                 [slingshot "0.12.2"]
                 ;; readability things
                 [io.aviso/pretty "0.1.37"]
                 [expound "0.7.2"]
                 ;; end rt
                 ]
  :main ^:skip-aot counter-gui.core
  :target-path "target/%s"
  :profiles {:uberjar {:aot :all
                       :injections [(javafx.application.Platform/exit)]}})

More or less, we just chose to add some extra dependencies (cljfx for our gui toolkit, clojure 1.10 as a pre-req for cljfx, slingshot for better try/catch handling, io.aviso/pretty and expound for improved REPL readability).

The uberjar injections is to work around an issue with building a jar for cljfx.

Now, back in your shell, pull in those dependencies via:

lein deps

Writing a widget

If you spent some time glancing at the cljfx README, you should be aware of how it structures 'views' very similarly to that of a React app (nested components in a tree like structure).

It also is opinionated on keeping local/encapsulated state far away from the render cycle and widgets - opting for using a global map/atom to track the world state and pushing it down from the top, into all sub-components that need to use it. Such an approach work(s/ed) fine for my actual project, Insectarium, but I don't feel that it lends itself well to having a broader community make stateful widgets/reusable pieces of a bigger cljfx application (the consumer would have to be aware of all the state the widgets needed to track/act on), so on this tutorial, I'm going to go over a possible solution/way of approaching this that allows a better 'localized' state.

We're also going to work on a bottom up approach, vs a top down approach (the existing examples/ directory on the cljfx project has some great examples of iterative development using top down and slowly growing/expanding nodes/custom fx/types as needed).

Set up the namespace

Ok, before you can go and build the widget, or anything to do with it, you'll want to ensure your namespace is set up - if all dependencies installed fine, you can add something like this to core.clj:

(ns counter-gui.core
  (:require
   [cljfx.api :as fx])
  (:import
   [javafx.application Platform]
   [javafx.scene.input KeyCode KeyEvent]
   [javafx.scene.paint Color]
   [javafx.scene.canvas Canvas])
  (:gen-class))

In this project, we probably won't use all the imports, but they are common enough on frequent cljfx use that you may end up using them elsewhere. Also - we are building it all in core.clj, as it barely breaks 100 LOC for this tutorial.

Making a make-able / localized state widget

First off, we need some state or world atom to track our various states, and we are going to follow cljfx convention and define a top level one to do that.

(def *state (atom {:event-queue []}))

(defn inc-or-make [n] (if n (inc n) 0))

(defn event-handler [event state]
  (case (:event/type event)
    ::stub (update-in state [:clicked] inc-or-make)
    state))

(defn push-event [state event]
  (update-in state [:event-queue] conj event))

(defn maybe-button-fire-event [prefix state]
  (let [{:keys [clicked]} (prefix state)]
    (if (> (or clicked 0) 3)
      (push-event state {:event/type ::click-threshold})
      state)))

Ok - we did a little more than define one atom to hold state - thats alright, we're going to come back to some of those functions in a bit (lets just say - we plan to allow our localized-state widget instances to push events up to the parent using them in our 'event queue' like key).

Now, onto the wrapper to generate a stateful widget:

(defn make-button-with-state
  "Wrapper to generate a stateful widget."
  [prefix]
  (let [handler (fn [event state]
                  "The event dispatching for received events."
                  [event state]
                  (->> (if (= prefix (:prefix event))
                         (case (:event/type event)
                           ::clicked (update-in state [prefix :clicked] inc-or-make)
                           state)
                         state)
                       ;; Maybe fire off an event-queue based on conditions
                       (maybe-button-fire-event prefix)))
        view (fn [state]
               (let [{:keys [clicked]} (prefix state)]
                 {:fx/type :button
                  :on-action {:event/type ::clicked
                              :prefix prefix}
                  :text (str "Click me more! x " clicked prefix)}))]
    ;; Send the handler and view back up to the caller.
    {:handler handler
     :view view}))

(def bws-1 (make-button-with-state ::bws-1))
(def bws-2 (make-button-with-state ::bws-2))

So - the function receives a prefix (this could be a randomized value, or a user specified one - the only caveat is that it should be unique, as we use it to dictate where to store stateful data in the top level state atom).

It then returns to us a handler (a way to process GUI triggered events) as well as the actual customized view component - a button that will ask the GUI user to click it more.

You can see where 'inc-or-make' is being used - it handles incrementing (or setting a value if it began as nil).

Lastly, we bind our made buttons to bws-1 and bws-2, to be used later in the wrapping view.

Handling events in our localized state widgets, as if each were a layer in a sandwhich

So - if you've poked at cljfx previously, you may be aware that it has great support for an event handling layer (that's what the event-handler function up above was for).

It is usually set up similar to this:

(defn renderer []
  (fx/create-renderer
   :middleware (fx/wrap-map-desc assoc :fx/type root)
   :opts {:fx.opt/map-event-handler #(swap! *state event-handler %)}))

(defn main []
  (fx/mount-renderer *state (renderer)))

Please see more of this on the official cljfx site though.

Anyways - we want to change this approach so that we keep a registry of event-handler functions, and pass each one through (think of the thread operator "->" in clojure - we want this map to filter through many different transformation / event processing layers).

One solution that works with our localized / prefixed button approach is to add a small registry (a vector of functions that handle this) and a runner for them:

(def event-handlers
  [event-handler
   (:handler bws-1)
   (:handler bws-2)])

(defn run-event-handlers
  "If we have many event handler layers, we want to run each one in sequence.
  This could let us have `private` widgets that maintain a state."
  ([m]
   (prn "REH received only one arg? " m))
  ([state event]
   (let [f (reduce comp (map #(partial % event) event-handlers))]
     (f state))))

This will allow each "layer" to handle any events it sees (and recall that the make-button-with-state function has a check to ensure it only responds to events that match it's prefix upon creation).

Putting it all together

So - we now have a localized state button, as well as 2 bindings for the button, and a way to process events without our parent event-handler function, or even our global state atom having to be aware of what's going on in them.

Lets try making a simple cljfx app that pulls in a stylesheet with some light styling (see styles.css in the linked repo up above) and shows a label with our 2 special buttons:

(defn root [{:keys [clicked] :as state}]
  {:fx/type :stage
   :showing true
   :title "Counter"
   :width 300
   :height 300
   :scene {:fx/type :scene
           :stylesheets #{"styles.css"}
           :root {:fx/type :v-box
                  :children
                  [
                   {:fx/type :label :text (str "Root state is: " clicked)}
                   ((:view bws-1) state)
                   ((:view bws-2) state)
                   ]}
           }
   })

Oops, you probably need to hook it up to cljfx rendering engine as well:

(defn renderer []
  (fx/create-renderer
   :middleware (fx/wrap-map-desc assoc :fx/type root)
   :opts {:fx.opt/map-event-handler #(swap! *state run-event-handlers %)}))

(defn main []
  (fx/mount-renderer *state (renderer)))

(defn -main
  "I don't do a whole lot ... yet."
  [& args]
  (println "Begin.")
  (Platform/setImplicitExit true)
  (apply main args))

You can now invoke (main) in your REPL, or run "lein run" on the CLI (or "make" to do the docker build + lein uberjar + execute it).

The setImplicitExit is to ensure the launching PID closes when the GUI app closes.

What do you think? did you end up with something like this?

Comments

comments powered by Disqus