Bringing Clojure to the Desktop
with cljfx
The problem: complex stateful applications
The problem: complex stateful applications
The problem: a place for tooling (rant)
The problem: a place for tooling (rant)
The problem: a place for tooling (rant)
(require 'clojure.inspector)
The problem: a place for tooling (rant)
(require 'clojure.inspector)
(clojure.inspector/inspect-table
[{:a 1 :b 2}
{:a 3 :b 4}])
(clojure.inspector/inspect-tree
{:a 1
:b {:a 1
:b 2}})
The problem: a place for tooling (rant)
The problem: a place for tooling (rant)
The problem: a place for tooling (rant)
Solution
Somewhat unbiased comparison to Electron
| clj + cljfx | cljs + electron |
Memory use | high | high |
CPU use | high | high |
Language semantics | powerful | great but limited |
Concurrency ergonomics | great | great |
Performance | great | good |
Ecosystem size | huge | huge |
Ecosystem maturity | high | low |
Industry support | low | high |
Typesetting | bad | good |
Getting started: component description
{:fx/type :text-field
:text "Hello world"
:on-text-changed println}
Getting started: component creation
(require '[cljfx.api :as fx])
(def component
(fx/create-component
{:fx/type :text-field
:text "Hello world"
:on-text-changed println}))
(.getText (fx/instance component))
=> "Hello world"
Getting started: component creation
(doto (TextField.)
(.setText "Hello world")
(-> .textProperty
(.addListener (reify ChangeListener
(changed [_ _ _ value]
(println value))))))
Getting started: component prop evolution
(def component-2
(fx/advance-component
component
{:fx/type :text-field
:text "Hello cljfx"
:on-text-changed println}))
(.getText (fx/instance component-2))
=> "Hello cljfx"
(identical? (fx/instance component) (fx/instance component-2))
=> true
Getting started: component type evolution
(def component-3
(fx/advance-component
component-2
{:fx/type :text-area
:text "Hello cljfx"
:on-text-changed println}))
(class (fx/instance component-2))
=> javafx.scene.control.TextField
(class (fx/instance component-3))
=> javafx.scene.control.TextArea
Getting started: function components
(defn username-input [{:keys [username on-username-changed]}]
{:fx/type :text-field
:text username
:on-text-changed on-username-changed})
(def username-component
(fx/create-component
{:fx/type username-input
:username "vlaaad"
:on-username-changed println}))
(class (fx/instance username-component))
=> javafx.scene.control.TextField
Getting started: renderer
(def renderer
(fx/create-renderer))
(renderer
{:fx/type :stage
:showing true
:scene {:fx/type :scene
:root {:fx/type :text-field
:text "Hello world"
:on-text-changed println}}})
Getting started: renderer
(renderer
{:fx/type :stage
:showing true
:scene {:fx/type :scene
:root {:fx/type :text-field
:text "Hello cljfx"
:on-text-changed println}}})
Getting started: renderer
(renderer
{:fx/type :stage
:showing true
:scene {:fx/type :scene
:root {:fx/type :v-box
:children [{:fx/type :label
:text "Username"}
{:fx/type username-input
:username ""
:on-username-changed println}]}}})
Getting started: event handling
=> v
Getting started: event handling
=> v
=> vl
Getting started: event handling
=> v
=> vl
=> vla
Getting started: event handling
=> v
=> vl
=> vla
=> vlaa
Getting started: event handling
=> v
=> vl
=> vla
=> vlaa
=> vlaaa
Getting started: event handling
=> v
=> vl
=> vla
=> vlaa
=> vlaaa
=> vlaaad
Getting started: state
(def *state (atom {:username "vlaaad"}))
(defn root [{:keys [username]}]
{:fx/type :stage
:showing true
:scene {:fx/type :scene
:root
{:fx/type :v-box
:children
[{:fx/type :label
:text "Username"}
{:fx/type username-input
:username username
:on-username-changed #(swap! *state assoc :username %)}]}}})
Getting started: state
(renderer (root @*state))
Getting started: state
(def renderer
(fx/create-renderer
:middleware (fx/wrap-map-desc root)))
(fx/mount-renderer *state renderer)
There is more: map events
(def renderer
(fx/create-renderer :opts {:fx.opt/map-event-handler prn}))
(renderer
{:fx/type :stage
:showing true
:scene {:fx/type :scene
:root {:fx/type username-input
:username ""
:on-username-changed {::event ::set-username}}}})
=> {::event ::set-username, :fx/event "cljfx"}
There is more: pure event handling
(defn- my-event-handler [{:keys [state fx/event]}]
(case (::event event)
::add-todo {:set-state (update state :todos conj {:text (:text event)
:done false})}))
(-> my-event-handler
(fx/wrap-co-effects {:state (fx/make-deref-co-effect *state)})
(fx/wrap-effects {:set-state (fx/make-reset-effect *state)}))
(my-event-handler {:state {:todos []}
:fx/event {::event ::add-todo
:text "Play Animal Crossing"}})
=> {:set-state {:todos [{:text "Play Animal Crossing"
:done false}]}}
There is more: many examples
There is more: cljfx/css
(require '[cljfx.css :as css])
(def background-color "#ffc")
(def text-color "#400")
(def style
(css/register ::style
{".my-label" {:-fx-padding 20
:-fx-text-fill text-color}
".my-root" {:-fx-background-color background-color}}))
There is more: cljfx/css
(renderer
{:fx/type :stage
:showing true
:scene {:fx/type :scene
:stylesheets [(::css/url style)]
:root {:fx/type :v-box
:style-class "my-root"
:children [{:fx/type :label
:style-class "my-label"
:effect {:fx/type :drop-shadow
:color text-color
:offset-y 2}
:text "hello styling"}]}}})
There is more: special keys
(def todos
[{:id 1 :done false :text "Buy groceries"}
{:id 2 :done true :text "Play Minecraft"}
{:id 3 :done false :text "Prepare talk"}])
{:fx/type :v-box
:children (for [todo (sort-by :done todos)]
{:fx/type :check-box
:fx/key (:id todo)
:v-box/margin 10
:selected (:done todo)
:text (:text todo)})}
There is more: packaging with Java 14
$ jpackage --input dist \
--main-jar uberjar.jar \
--main-class my.app.core \
--name my-app
There is more: advanced stuff
;; re-frame-like subscriptions
(fx/sub-ctx context sorted-todos)
;; extension lifecycles
{:fx/type fx/ext-instance-factory
:create #(Duration/valueOf "10ms")}
;; tools to create custom props
(def ext-with-tooltip
(fx/make-ext-with-props
{:tooltip (fx.prop/make ...)}))
Summary
Thank you!
Any questions?