Giter VIP home page Giter VIP logo

cljfx's Introduction

Logo

Clojars Project Slack Channel Clojure CI

Cljfx is a declarative, functional and extensible wrapper of JavaFX inspired by better parts of react and re-frame.

Rationale

I wanted to have an elegant, declarative and composable UI library for JVM and couldn't find one. Cljfx is inspired by react, reagent, re-frame and fn-fx.

Like react, it allows to specify only desired layout, and handles all actual changes underneath. Unlike react (and web in general) it does not impose xml-like structure of everything possibly having multiple children, thus it uses maps instead of hiccup for describing layout.

Like reagent, it allows to specify component descriptions using simple constructs such as data and functions. Unlike reagent, it rejects using multiple stateful reactive atoms for state and instead prefers composing ui in more pure manner.

Like re-frame, it provides an approach to building large applications using subscriptions and events to separate view from logic. Unlike re-frame, it has no hard-coded global state, and subscriptions work on referentially transparent values instead of ever-changing atoms.

Like fn-fx, it wraps underlying JavaFX library so developer can describe everything with clojure data. Unlike fn-fx, it is more dynamic, allowing users to use maps and functions instead of macros and deftypes, and has more explicit and extensible lifecycle for components.

Installation and requirements

Cljfx uses tools.deps, so you can add this repo with latest sha as a dependency:

 cljfx {:git/url "https://github.com/cljfx/cljfx" :sha "<insert-sha-here>"}

Cljfx is also published on Clojars, so you can add cljfx as a maven dependency, current version is on this badge:

Cljfx on Clojars

Minimum required version of clojure is 1.10.

When depending on git coordinates, minimum required Java version is 11. When using maven dependency, both Java 8 (assumes it has JavaFX provided in JRE) and Java 11 (via openjfx dependency) are supported. You don't need to configure anything in this regard: correct classifiers are picked up automatically.

Please note that JavaFX 8 is outdated and has problems some people consider severe: it does not support HiDPI scaling on Linux, and sometimes crashes JVM on macOS Mojave. You should prefer JDK 11.

Overview

Hello world

Components in cljfx are described by maps with :fx/type key. By default, fx-type can be:

  • a keyword corresponding to some JavaFX class
  • a function, which receives this map as argument and returns another description
  • an implementation of Lifecycle protocol (more on that in extending cljfx section)

Minimal example:

(ns example
  (:require [cljfx.api :as fx]))

(fx/on-fx-thread
  (fx/create-component
    {:fx/type :stage
     :showing true
     :title "Cljfx example"
     :width 300
     :height 100
     :scene {:fx/type :scene
             :root {:fx/type :v-box
                    :alignment :center
                    :children [{:fx/type :label
                                :text "Hello world"}]}}}))

Evaluating this code will create and show a window:

The overall mental model of these descriptions is this:

  • whenever you need a JavaFX class, use map where :fx/type key has a value of a kebab-cased keyword derived from that class name
  • other keys in this map represent JavaFX properties of that class (also in kebab-case);
  • if prop x can be changed by user, there is a corresponding :on-x-changed prop for observing these changes

Renderer

To be truly useful, there should be some state and changes over time, for this matter there is a renderer abstraction, which is a function that you may call whenever you want with new description, and cljfx will advance all the mutable state underneath to match this description. Example:

(def renderer
  (fx/create-renderer))

(defn root [{:keys [showing]}]
  {:fx/type :stage
   :showing showing
   :scene {:fx/type :scene
           :root {:fx/type :v-box
                  :padding 50
                  :children [{:fx/type :button
                              :text "close"
                              :on-action (fn [_]
                                           (renderer {:fx/type root
                                                      :showing false}))}]}}})

(renderer {:fx/type root
           :showing true})

Evaluating this code will show this:

Clicking close button will hide this window.

Renderer batches descriptions and re-renders views on fx thread only with last received description, so it is safe to call many times at once. Calls to renderer function return derefable that will contain component value with most recent description.

Atoms

Example above works, but it's not very convenient: what we'd really like is to have a single global state as a value in an atom, derive our description of JavaFX state from this value, and change this atom's contents instead. Here is how it's done:

;; Define application state

(def *state
  (atom {:title "App title"}))

;; Define render functions

(defn title-input [{:keys [title]}]
  {:fx/type :text-field
   :on-text-changed #(swap! *state assoc :title %)
   :text title})

(defn root [{:keys [title]}]
  {:fx/type :stage
   :showing true
   :title title
   :scene {:fx/type :scene
           :root {:fx/type :v-box
                  :children [{:fx/type :label
                              :text "Window title input"}
                             {:fx/type title-input
                              :title title}]}}})

;; Create renderer with middleware that maps incoming data - description -
;; to component description that can be used to render JavaFX state.
;; Here description is just passed as an argument to function component.

(def renderer
  (fx/create-renderer
    :middleware (fx/wrap-map-desc assoc :fx/type root)))

;; Convenient way to add watch to an atom + immediately render app

(fx/mount-renderer *state renderer)

Evaluating code above pops up this window:

Editing input then immediately updates displayed app title.

Map events

Consider this example:

(defn todo-view [{:keys [text id done]}]
  {:fx/type :h-box
   :children [{:fx/type :check-box
               :selected done
               :on-selected-changed #(swap! *state assoc-in [:by-id id :done] %)}
              {:fx/type :label
               :style {:-fx-text-fill (if done :grey :black)}
               :text text}]})

There are problems with using functions as event handlers:

  1. Performing mutation from these handlers requires coupling with that state, thus making todo-view dependent on mutable *state
  2. Updating state from listeners complects logic with view, making application messier over time
  3. There are unnecessary reassignments to on-selected-changed: functions have no equality semantics other than their identity, so on every change to this view (for example, when changing it's text), on-selected-changed will be replaced with another function with same behavior.

To mitigate these problems, cljfx allows to define event handlers as arbitrary maps, and provide a function to a renderer that performs actual handling of these map-events (with additional :fx/event key containing dispatched event):

;; Define view as just data

(defn todo-view [{:keys [text id done]}]
  {:fx/type :h-box
   :spacing 5
   :padding 5
   :children [{:fx/type :check-box
               :selected done
               :on-selected-changed {:event/type ::set-done :id id}}
              {:fx/type :label
               :style {:-fx-text-fill (if done :grey :black)}
               :text text}]})

;; Define single map-event-handler that does mutation

(defn map-event-handler [event]
  (case (:event/type event)
    ::set-done (swap! *state assoc-in [:by-id (:id event) :done] (:fx/event event))))

;; Provide map-event-handler to renderer as an option

(fx/mount-renderer
  *state
  (fx/create-renderer
    :middleware (fx/wrap-map-desc assoc :fx/type root)
    :opts {:fx.opt/map-event-handler map-event-handler}))

You can see full example at examples/e09_todo_app.clj.

Interactive development

Another useful aspect of renderer function that should be used during development is refresh functionality: you can call renderer function with zero args and it will recreate all the components with current description.

See walk-through in examples/e12_interactive_development.clj as an example of how to iterate on cljfx app in REPL.

Dev tools

Check out cljfx/dev for tools that might help you when developing cljfx applications. These tools include:

  • specs and validation, both for individual cljfx descriptions and running apps;
  • helper reference for existing types and their props;
  • cljfx component stack reporting in exceptions to help with debugging.

Styling

Iterating on styling is usually cumbersome: styles are defined in external files, they are not reloaded on change, they are opaque: you can't refer from the code to values defined in CSS. Cljfx has a complementary library that aims to help with all those problems: cljfx/css.

Special keys

Sometimes components accept specially treated keys. Main uses are:

  1. Reordering of nodes (instead of re-creating them) in parents that may have many children. Descriptions that have :fx/key during advancing get reordered instead of recreated if their position in child list is changed. Consider this example:

    (let [component-1 (fx/create-component
                        {:fx/type :v-box
                         :children [{:fx/type :label
                                     :fx/key 1
                                     :text "- buy milk"}
                                    {:fx/type :label
                                     :fx/key 2
                                     :text "- buy socks"}]})
          [milk-1 socks-1] (vec (.getChildren (fx/instance component-1)))
          component-2 (fx/advance-component
                        component-1
                        {:fx/type :v-box
                         :children [{:fx/type :label
                                     :fx/key 2
                                     :text "- buy socks"}
                                    {:fx/type :label
                                     :fx/key 1
                                     :text "- buy milk"}]})
          [socks-2 milk-2] (vec (.getChildren (fx/instance component-2)))]
      (and (identical? milk-1 milk-2)
           (identical? socks-1 socks-2)))
    => true

    With :fx/key-s specified, advancing of this component reordered children of VBox, and didn't change text of any labels, because their descriptions stayed the same.

  2. Providing extra props available in certain contexts. If node is placed inside a pane, pane can layout it differently by looking into properties map of a node. Nodes placed in ButtonBar can have OS-specific ordering depending on assigned ButtonData. These properties can be specified via keywords namespaced by container's fx-type. Example:

    (fx/on-fx-thread
      (fx/create-component
        {:fx/type :stage
         :showing true
         :scene {:fx/type :scene
                 :root {:fx/type :stack-pane
                        :children [{:fx/type :rectangle
                                    :width 200
                                    :height 200
                                    :fill :lightgray}
                                   {:fx/type :label
                                    :stack-pane/alignment :bottom-left
                                    :stack-pane/margin 5
                                    :text "bottom-left"}
                                   {:fx/type :label
                                    :stack-pane/alignment :top-right
                                    :stack-pane/margin 5
                                    :text "top-right"}]}}}))

    Evaluating code above produces this window:

    For a more complete example of available pane keys, see examples/e07_extra_props.clj

Factory props

There are some props in JavaFX that represent not a value, but a way to construct a value from some input:

  • :page-factory in pagination, you can use function receiving page index and returning any component description for this prop (see example in examples/e06_pagination.clj)
  • various versions of :cell-factory in controls designed to display multiples of items (table views, list views etc.) can be described using the following form:
    {:fx/cell-type :list-cell
     :describe (fn [item] {:text (my.ns/item-as-text item)})} 
    The lifecycle of cells is a bit different than lifecycle of other components: JavaFX pools a minimal amount of cells needed to be shown at the same time and updates them on scrolling. This is great for performance, but it imposes a restriction: cell type is "static". That's why cljfx uses :fx/cell-type that has to be a keyword (like :list-cell or :table-cell) and a separate :describe function that receives an item and returns a prop map for that cell type. There are various usage examples available in examples/e16_cell_factories.clj

Subscriptions and contexts

Once application becomes complex enough, you can find yourself passing very big chunks of state everywhere. Consider this example: you develop a task tracker for an organization. A typical task view on a dashboard displays a description of that task and an assignee. Required state for this view is plain and simple, just a simple data like that:

{:title "Fix NPE on logout during full moon"
 :state :todo
 :assignee {:id 42 :name "Fred"}}

Then one day comes a requirement: users of this task tracker should be able to change assignee from the dashboard. Now, we need a combo-box with all assignable users to render such a view, and required data becomes this:

{:title "Fix NPE on logout during full moon"
 :state :todo
 :assignee {:id 42 :name "Fred"}
 :users [{:id 42 :name "Fred"}
         {:id 43 :name "Alice"}
         {:id 44 :name "Rick"}]}

And you need to compute it once in one place and then pass it along multiple layers of ui to this view. This is undesirable:

  • it will lead to unnecessary re-renderings of views that just pass data further when it changes
  • it complects reasoning about what actually a view needs: is it just a task? or a task with some precomputed attributes?

To mitigate this problem, cljfx introduces optional abstraction called context, which is inspired by re-frame's subscriptions. Context is a black-box wrapper around application state (usually a map), with 2 functions to look inside the wrapped state:

  1. fx/sub-val that subscribes a function to the value wrapped in the context directly (usually it's used for data accessors like get or get-in);
  2. fx/sub-ctx that subscribes a function to the context itself, which is then used by the function to subscribe to some view of the wrapped value indirectly (can be used for slower computations like sorting).

Returned values from subscription functions are memoized in this context (so it actually is a memoization context), and subsequent sub-* calls will result in cache lookup. The best thing about context is that not only does it support updating wrapped values via swap-context and reset-context, it also reuses this memoization cache to minimize re-calculation of subscription functions in successors of this context. This is done via tracking of fx/sub-* calls inside subscription functions, and checking if their dependencies changed. Example:

(def context-1
  (fx/create-context
    {:tasks [{:text "Buy milk" :done false}
             {:text "Buy socks" :done true}]}))

;; Simple subscription function that depends on :tasks key of wrapped map. Whenever value
;; of :tasks key "changes" (meaning whenever there will be created a new context with
;; different value on :tasks key), subscribing to this function will lead to a call to
;; this function instead of cache lookup
(defn task-count [context]
  (count (fx/sub-val context :tasks)))

;; Using subscription functions:
(fx/sub-ctx context-1 task-count) ; => 2

;; Another subscription function depends on :tasks key of wrapped map
(defn remaining-task-count [context]
  (count (remove :done (fx/sub-val context :tasks))))

(fx/sub-ctx context-1 remaining-task-count) ; => 1

;; Indirect subscription function that depends on 2 previously defined subscription
;; functions, which means that whenever value returned by `task-count` or
;; `remaining-task-count` changes, subscribing to this function will lead to a call
;; instead of cache lookup
(defn task-summary [context]
  (prn :task-summary)
  (format "Tasks: %d/%d"
          (fx/sub-ctx context remaining-task-count)
          (fx/sub-ctx context task-count)))

(fx/sub-ctx context-1 task-summary) ; (prints :task-summary) => "Tasks: 1/2"

;; Creating derived context that reuses cache from `context-1`
(def context-2
  (fx/swap-context context-1 assoc-in [:tasks 0 :text] "Buy bread"))

;; Validating that cache entry is reused. Even though we updated :tasks key, there is no
;; reason to call `task-summary` again, because it's dependencies, even though
;; recalculated, return the same values
(fx/sub-ctx context-2 task-summary) ; (does not print anything) => "Tasks: 1/2"

This tracking imposes a restriction on subscription functions: they should not call fx/sub-* after they return (which is possible if they return lazy sequence that calls fx/sub-* during element calculation).

Note that all functions subscribed with fx/sub-val are always invalidated for derived contexts, so they should be reasonably fast (like get). Their upside is that they are decoupled from context completely: since they receive wrapped value as first argument, any function can be used. Functions subscribed with fx/sub-ctx, on the other hand, are invalidated only when their dependencies change, so they can be slower (like sort). Their downside is coupling to cljfx โ€” they receive context as first argument.

Using context in cljfx application requires 2 things:

  • passing context to all lifecycles in a component graph, which is done by using fx/wrap-context-desc middleware
  • using special lifecycle (fx/fn->lifecycle-with-context) for function fx-types that uses this context

Minimal app example using contexts:

;; you will need core.cache dependency if you are going to use contexts!
(require '[clojure.core.cache :as cache])

;; Define application state as context

(def *state
  (atom (fx/create-context {:title "Hello world"} cache/lru-cache-factory)))

;; Every description function receives context at `:fx/context` key

(defn root [{:keys [fx/context]}]
  {:fx/type :stage
   :showing true
   :scene {:fx/type :scene
           :root {:fx/type :h-box
                  :children [{:fx/type :label
                              :text (fx/sub context :title)}]}}})

(def renderer
  (fx/create-renderer
    :middleware (comp
                  ;; Pass context to every lifecycle as part of option map
                  fx/wrap-context-desc
                  (fx/wrap-map-desc (fn [_] {:fx/type root})))
    :opts {:fx.opt/type->lifecycle #(or (fx/keyword->lifecycle %)
                                        ;; For functions in `:fx/type` values, pass
                                        ;; context from option map to these functions
                                        (fx/fn->lifecycle-with-context %))}))

(fx/mount-renderer *state renderer)

Using contexts effectively makes every fx-type function a subscription function, so no-lazy-fx-subs-in-returns restriction applies to them too. On a plus side, it makes re-rendering more efficient: fx-type components get re-rendered only when their subscription values change.

For a bigger example see examples/e15_task_tracker.clj.

Preventing cache from growing forever

Another point of concern for context is cache size. By default it will grow forever, which at certain point might become problematic, and we may want to trade some cpu cycles for recalculations to decrease memory consumption. There is a perfect library for it: core.cache. fx/create-context supports cache factory (a function taking initial cache map and returning cache) as a second argument. What kind of cache to use is a question with no easy answer, you probably should try different caches and see what is a better fit for your app.

Cljfx has a runtime optional dependency on core.cache: you need to add it yourself if you are going to use contexts.

Event handling on steroids

While using maps to describe events is a good step towards mostly pure applications, there is still a room for improvement:

  • many event handlers dereference app state, which makes them coupled with an atom: mutable place
  • almost every event handler still mutates app state, which also makes them coupled

Cljfx borrows solutions to these problems from re-frame, providing map event handler wrappers that allow having co-effects (pure inputs) and effects (pure outputs). Lets walk through this example event handler and see how we can make it pure:

(def *state
  (atom {:todos []}))

(defn handle [event]
  (let [state @*state
        {:keys [event/type text]} event]
    (case type
      ::add-todo (reset! *state (update state :todos conj {:text text :done false})))))

;; usage:
(handle {:event/type ::add-todo :text "Buy milk"})
  1. Co-effects: wrap-co-effects

    It would be nice to not have to deref state atom and instead receive it as an argument, and that is what co-effects are for. Co-effect is a term taken from re-frame, and it means current state as data, as presented to event handler. In cljfx you describe co-effects as a map from arbitrary key to function that produces some data that is then passed to handler:

    (defn handle [event]
      ;; receive state as part of an event
      (let [{:keys [event/type text state]} event]
        (case type
          ::add-todo (reset! *state (update state :todos conj {:text text :done false})))))
          
    (def actual-handler 
      (-> handle
          (fx/wrap-co-effects {:state #(deref *state)})))
    
    ;; usage:
    (actual-handler {:event/type ::add-todo :text "Buy milk"})
  2. Effects: wrap-effects

    Instead of performing side-effecting operations from handlers, we can return data that describes how to perform these side-effecting operations. fx/wrap-effects uses that data to perform side effects. You describe effects as a map from arbitrary keys to side-effecting function. A wrapped handler in turn should return a seqable of 2-element vectors. First element is a key used to find side-effecting function, and second is an argument to it:

    (defn handle [event]
      (let [{:keys [event/type text state]} event]
        (case type
          ;; Now handlers not only receive just data, they also return just data
          ;; Returning map is a convenience option that can be used as a return
          ;; value, and sequences like [[:state ...] [:state ...]] are fine too 
          ::add-todo {:state (update state :todos conj {:text text :done false})})))
    
    (def actual-handler
      (-> handle
          (fx/wrap-co-effects {:state #(deref *state)})
          (fx/wrap-effects {:state (fn [state _] (reset! *state state))})))

    In addition to value provided by wrapped handler, side-effecting function receives a function they can call to dispatch new events. While it's useless for resetting state, it can be useful in other circumstances. One is you can create a :dispatch effect that dispatches other events, and another is you can describe asynchronous operations such as http requests as just data. Since effect handlers are run on the UI thread, you should delegate the execution of potentially blocking effects to a different thread using this approach. Examples of both can be found at examples/e18_pure_event_handling.clj. This approach allows to specify side effects in a few places, and then have easily testable handlers:

    (handle {:event/type ::add-todo
             :text "Buy milk"
             :state {:todos []}})
    => {:state {:todos [{:text "Buy milk", :done false}]}}
    ;; data in, data out, no mocks necessary! 

How does it actually work

There are 3 main building blocks of cljfx: components, lifecycles and mutators. Each are represented by protocols, here they are:

(defprotocol Component
  :extend-via-metadata true
  (instance [this]))

(defprotocol Lifecycle
  :extend-via-metadata true
  (create [this desc opts])
  (advance [this component desc opts])
  (delete [this component opts]))

(defprotocol Mutator
  :extend-via-metadata true
  (assign! [this instance coerce value])
  (replace! [this instance coerce old-value new-value])
  (retract! [this instance coerce value]))

Component is an immutable value representing some object in some state (that object may be mutable โ€” usually it's a javafx object), that also has a reference to said object instance.

Lifecycle is well, a lifecycle of a component. Component gets created from a description once, advanced to new description zero or more times, and then deleted. Cljfx is a composition of multiple different lifecycles, each useful in their own place. opts is a map that contains some data used by different lifecycles. 2 opt keys that are used by default in cljfx are:

  • :fx.opt/type->lifecycle โ€” used in dynamic lifecycle to select what lifecycle will be actually used for description based by value in :fx/type key.
  • :fx.opt/map-event-handler โ€” used in event-handler lifecycle that checks if event handler is a map, and if it is, call function provided by this key when event happens. It should be noted, that such event handlers receive additional key in a map (:fx/event) that contains event object, which may be context dependent: for JavaFX change listeners it's a new value, for JavaFX event handlers it's an event, for runnables it's nil, etc.

Another notable lifecycle is cljfx.composite/lifecycle: it manages mutable JavaFX objects: creates instance in create, advances any changes to props (each individual prop may be seen as lifecycle + mutator), and has some useful macros to simplify generating composite lifecycles for concrete classes.

Finally, mutator is a part of prop in composite lifecycles that performs actual mutation on instance when values change. It also receives coerce function which is called on value before applying it. Most common mutator is setter, but there are some other, for example, property-change-listener, which uses addListener and removeListener.

Extending cljfx

Cljfx might have some missing parts that you'll want to fill. Not everything can be configured with lifecycle opts and renderer middleware, and in that case you are encouraged to create and use extension lifecycles. Fx-types in descriptions can be implementations of Lifecycle protocol, and with this escape hatch you get a lot more freedom. Since these lifecycles can introduce different meanings for what descriptions mean in their context, they should stand out from other keyword or function lifecycles, and convention is to have ext- prefix in their names.

Included extension lifecycles

  1. fx/ext-instance-factory

    Using this extension lifecycle you can simply create a component using 0-argument factory function:

    (fx/instance
      (fx/create-component
        {:fx/type fx/ext-instance-factory
         :create #(Duration/valueOf "10ms")}))
    => #object[javafx.util.Duration 0x2f5eb358 "10.0 ms"]
  2. fx/ext-on-instance-lifecycle

    You can use this lifecycle to additionally setup/tear down instance of otherwise declaratively created value:

    (fx/instance
      (fx/create-component
        {:fx/type fx/ext-on-instance-lifecycle
         :on-created #(prn "created" %)
         :desc {:fx/type fx/ext-instance-factory
                :create #(Duration/valueOf "10ms")}}))
    ;; prints "created" #object[javafx.util.Duration 0x284cdce9 "10.0 ms"]
    => #object[javafx.util.Duration 0x284cdce9 "10.0 ms"]
  3. fx/ext-let-refs and fx/ext-get-ref

    You can create managed components outside of component tree using fx/ext-let-refs, and then use instances of them, possibly in multiple places, using fx/ext-get-ref:

    {:fx/type fx/ext-let-refs
     :refs {::button-a {:fx/type :button
                        :text "Press Alt+A to focus on me"}}
     :desc {:fx/type :v-box
            :children [{:fx/type :label
                        :text "Mnemonic _A"
                        :mnemonic-parsing true
                        :label-for {:fx/type fx/ext-get-ref
                                    :ref ::button-a}}
                       {:fx/type fx/ext-get-ref
                        :ref ::button-a}]}}

    One use case is for using references in props that expect nodes in a scene graph (such as label's :label-for), and another is having dialogs defined close to usage places, you can find an example of such dialog at examples/e22_button_with_confirmation_dialog.clj

  4. fx/ext-set-env and fx/ext-get-env

    You can put any values into component tree environment with fx/ext-set-env, and then retrieve values from this environment with fx/ext-get-env:

    {:fx/type fx/ext-set-env
     :env {::global-text-style {:-fx-text-fill :red}}
     :desc {:fx/type :v-box
            :children [{:fx/type fx/ext-get-env
                        :env {::global-text-style :style}
                        :desc {:fx/type :label 
                               ;; will receive :style prop that makes text red
                               :text "Hello world"}}]}}
  5. fx/ext-many

    Usually props that expect collections of elements already ask for a collection of descriptions, but there might be cases where you want to manage a coll even though you are asked for a single element. In this case you can use fx/ext-many to describe multiple of components, for example, to show multiple windows at once:

    (fx/on-fx-thread
      (fx/create-component
        {:fx/type fx/ext-many
         :desc [{:fx/type :stage
                 :showing true}
                {:fx/type :stage
                 :showing true}]}))

    See examples/e10_multiple_windows.clj and examples/e17_dialogs.clj

  6. fx/make-ext-with-props

    Using this function you can create extension lifecycles that handle whatever additional props you need. These props will be applied after props of original lifecycle. There are some predefined lifecycles providing extra props:

Examples of included extension lifecycles are available at examples/e21_extension_lifecycles.clj.

Writing extension lifecycles

If that's not enough, you can write your own, but this requires more thorough knowledge of cljfx: take a look at cljfx.lifecycle namespace to see how other lifecycles are implemented.

Wrapping other java-based JavaFX components

There is cljfx.composite/props macro to create a prop-map for arbitrary Java class. Also there is a cljfx.composite/describe macro that allows to construct a lifecycle from a class and a prop map, and plenty of examples in cljfx.fx.* namespaces that can help you make custom java components for JavaFX cljfx-friendly.

Combining it all together

Now that every piece is laid out, it's time to combine them into application. What suits your needs is up to you, but if you plan to build something non-trivial, you'll probably want to combine all of the pieces, and easiest way to start is using create-app function. It accepts app atom, event handler and function producing view description and wires them all together:

(def app
  (fx/create-app *context
    :event-handler handle-event
    :desc-fn (fn [_]
               {:fx/type root-view})))

Using that as a starting point, you can build your application using pure functions for everything: views, subscriptions, events. create-app also allows some optional settings, such as :effects, :co-effects and :async-agent-options for configuring event handling and :renderer-middleware for configuring renderer. An example of such application can be found at examples/e20_markdown_editor.clj.

Gotchas

:fx/key should be put on descriptions in a list, not inside these descriptions

For example:

;; Don't do it, this won't work:

(defn item-view [{:keys [item]}]
  {:fx/type :label
   ;; Do not specify `:fx/key` here!
   :fx/key (:id item)
   :text (:title item)})

(defn item-list-view [items]
  {:fx/type :v-box
   :children (for [i items]
               {:fx/type item-view
                :item i})})

Lifecycle that manages lists of things (dynamics) can't see how it's elements will unfold, so it needs to have :fx/key-s where it can see them โ€” in the element descriptions that it gets:

;; Do this to specify `:fx/key`-s:

(defn item-view [{:keys [item]}]
  {:fx/type :label
   :text (:title item)})

(defn item-list-view [items]
  {:fx/type :v-box
   :children (for [i items]
               {:fx/type item-view
                ;; Put `:fx/key` to description that is a part of a list
                :fx/key (:id i)
                :item i})})

:fx/type is for mutable objects only

Lifecycles describe how things change, and some things in JavaFX don't change. For example, Insets class represents an immutable value, so when describing padding you don't need a map with :fx/type key:

{:fx/type :region
 :padding {:top 10 :bottom 10 :left 10 :right 10}}

It doesn't have to be a map at all:

{:fx/type :region
 :padding 10}

How does it work? Instead of using lifecycle there is a coercion mechanism that transforms values before assigning them to a model, most of them are in cljfx.coerce namespace.

Coercion

Some notable coercion examples and approaches:

  • all enums and enum-like things can be expressed as kebab-cased keywords, for example :red for colors, :crosshair for cursors
  • you still can use actual instances of target classes, for example Cursor/CROSSHAIR for cursors
  • for classes with 1-arg constructors you can supply just that, for example url string for images
  • for classes with multi-arg constructors you can supply args as a map, for example map with :url and :background-loading for images
  • styles can be specified as maps, for example {:-fx-background-color :lightgray}
  • durations can be specified as vector like [10 :ms] or [2 :h]
  • key combinations can be vectors. There are 2 flavors of key combinations in JavaFX: KeyCodeCombination, created if last element of that vector is keyword, for example, [:ctrl :period], and KeyCharacterCombination, created if last element of that vector is string, for example [:ctrl "."]

Differences with JavaFX

There are some "synthetic" properties that provide needed functionality usually used through some other API:

  • Canvas has a :draw prop that is a function that receives Canvas as an argument and should use it to draw on it (example)
  • MediaPlayer has :state prop that can be either :playing, :paused or :stopped, and will call play/pause/stop methods on media player when this prop is changed
  • :url prop of WebView will call load method on this view's web engine

AOT-compilation is complicated

Requiring cljfx starts a JavaFX application thread, which makes sense for repl and running application, but problematic for AOT compilation. To turn off this behavior for compilation, you should set cljfx.skip-javafx-initialization java property to true for your compilation task. This can be done in lein or clj by specifying the following jvm opts:

:jvm-opts ["-Dcljfx.skip-javafx-initialization=true"] 

Please note that while this will help in most cases, you still might have compilation related issues if your code imports JavaFX classes from javafx.scene.control package: classes defined there require JavaFX runtime to be running by accessing it in Control's static initializer. If you need to do that in your application code, you should not skip JavaFX initialization, and instead make your build tool call (javafx.application.Platform/exit) when it finished compiling.

No local mutable state

One thing that is easy to do in react/reagent, but actually complects things, is local mutable state: every component can have its own mutable state that lives independently of overall app state. This makes reasoning about state of the app harder: you need to take lots of small pieces into account. Another problem is this state is unreliable, because it is only here when a component is here. If it gets recreated, for example, after closing some panel it resides in and reopening it back, this state will be lost. Sometimes we want this behavior, sometimes we don't, and it's possible to choose whether this state will be retained or not only if it's a part of a global app state.

No controlled props

In react, setting value prop on text input makes it controlled, meaning it can't be changed unless there is also a change listener updating this value on typing. This is much harder to do in JavaFX, so there is no such thing. But you still can keep typed text in sync with internal state by having both :text and :on-text-changed props (see example in examples/e09_todo_app.clj)

More examples

There are various examples available in examples folder. To try them out:

  1. Clone this repo and cd into it:
    git clone https://github.com/cljfx/cljfx.git
    cd cljfx 
  2. Ensure you have java 11 installed.
  3. Launch repl with :examples alias and require examples:
    clj -A:examples
    # Clojure 1.10
    # user=> (require 'e15-task-tracker)
    # nil ;; window appears

Full project examples

Full project examples are in the example-projects directory. Consult the example project's README.md for usage.

More information

If you want to learn more about JavaFX, its documentation is available here.

If you want to learn more about React programming model cljfx is based on, there is an in-depth guide to it: React as UI Runtime. Feel free to skip sections about hooks since cljfx does not have them.

I also gave a talk about cljfx โ€” it goes from basic building blocks to how you build reactive applications and provides some context to why I created it. Slides are here.

API's stability, public and internal code

Newer versions of cljfx should never introduce breaking changes, so if an update broke something, please file a bug report. Growth of cljfx should happen only by accretion (providing more), relaxation (requiring less) and fixation (bashing bugs).

This applies to public API of cljfx. cljfx.api namespace and all behaviors that can be observed by using it are a public API. Other namespaces have a docstring stating what is and is not a public API.

Current shapes of values implementing Lifecycle, Component and Mutator protocols are internal and subject to change: treat them as a protocol implementations only. Context is not a protocol, but it's shape is internal too.

Keywords with fx namespace in component descriptions are reserved: new ones may be introduced.

Getting help

Feel free to ask questions on Slack or create an issue. Have a look at previously asked questions.

Food for thought

Internal list of ideas to explore:

  • missing observable maps: Scene's getMnemonics
  • :row-factory in tree-view/tree-table-view should be similar to cell factories
  • are controlled props possible? (controls, also stage's :showing)
  • wrap-factory may use some memoizing and advancing
  • add tests for various lifecycles and re-calculations
  • update to same desc should be identical (component-vec)
  • expand on props and composite lifecycle. What's known about them:
    • ctor:
      • scene requires root, root can be replaced afterwards
      • xy-chart requires axis, they can't be replaced afterwards
    • prop in composite lifecycle may be a map or a function taking instance and returning prop!
    • changing media should re-create media player
  • big app with everything in it to check if/how it works (generative tests maybe?)
  • if animation is to be implemented, it probably should be done as in https://popmotion.io/
  • declarative timers? problem is to figure out start/loop semantics. Examples:
    • caret in custom text input may have timer that restarts on typing
    • flipbook animation player needs to restart timer on FPS settings change

cljfx's People

Contributors

adwelly avatar atdixon avatar dergutemoritz avatar dimovich avatar eploko avatar ertugrulcetin avatar fdeitylink avatar frenchy64 avatar lvh avatar magemasher avatar manutter51 avatar michaelsbradleyjr avatar milt avatar vlaaad avatar yvern avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

cljfx's Issues

How do use canvas (or add a plain openJfx element?)

I'm attempting something like this:

 (defn my-canvas [_]                                                                                                 
   (let [canvas (new Canvas 250 250)                                                                                 
         g-ctx (.getGraphicsContext2D canvas)                                                                        
         box (fx/create-component {:fx/type :h-box})                                                                 
         ]                                                                                                           
     (.setFill g-ctx Color/BLUE)                                                                                     
     (.fillRect g-ctx 75 75 100 100)                                                                                 
     (-> (fx/instance box) .getChildren (.add canvas))                                                               
     box                                                                                                             
     ))

And attempting to use it in the declarative syntax area as such:

    :scene {:fx/type :scene
            :root {:fx/type :v-box
                   :alignment :center
                   :children
                   [
                    {:fx/type my-canvas}

This obviously isn't working. How can I instance an OpenJFX element and add it to the tree?

Add unmount function

Useful for reloaded workflows where cljfx-managed apps are a part of bigger system

Double update issue

I'm trying to write a large-ish app, so I've isolated the problem into a separate mini project:
https://github.com/geokon-gh/issue-cljfx-multiupdate1

Basically i have some nested data structure. In my application it's microphone parameters, but in the dummy example it looks like this:

(def *state
  ""
  (atom {:kingdoms {"plants" {"oak" {"old" 1100
                                     "new" 20}
                              "firn" {"christmas" 15
                                      "mast" 400}
                              "cactus" {"pokey" 1
                                        "decorative" 1}}
                    "animals" {"lion" {"african" 300
                                       "mountain" 500}
                               "bear" {"polar" 900
                                       "black" 300}
                               "human" {"woman" 120
                                        "man " 200}}
                    "mushrooms" {"toadstool" {"red" 1
                                              "white" 1}}}}))

The GUI has little drop down menus where I select from left to right the outermost parameter and then select deeper in the tree as I move the right.

When I select an outer parameter I'm also resetting the other parameters to the first available default so that the state doesn't get into some undefined limbo

So in the above example if I select "animal" then "lion" and "african" will get automatically selected

issue-cljfx-multiupdate1.core> *state
#<Atom@123ce438: 
  {:kingdoms
   {"plants"
    {"oak" {"old" 1100, "new" 20},
     "firn" {"christmas" 15, "mast" 400},
     "cactus" {"pokey" 1, "decorative" 1}},
    "animals"
    {"lion" {"african" 300, "mountain" 500},
     "bear" {"polar" 900, "black" 300},
     "human" {"woman" 120, "man " 200}},
    "mushrooms" {"toadstool" {"red" 1, "white" 1}}},
   :current-kingdom "animals",
   :current-type "lion",
   :current-subtype "african"}>

If I were to then select "bear" in the second dropdown then "polar" will get automatically selected - etc.

The issue is that, while the state looks correct, these automatic selections doesn't get reflected in the GUI unless I close and relaunch it. To make things more bizarre, the first level will autoredraw correctly but the second level will always remain blank (so in the above example, after selecting "animal" it will show "animal" AND "lion" but never "african") and I don't know how to trigger it to redraw

I'm not 100% sure if this is an issue or I just don't know what I'm doing. I'm still trying to digest all the stuff that comes after "Interactive development" in the README (I'm not really familiar with a lot of terms: "pagination"/"lifecycle"/etc.)

Thanks for the great library. Hope to wrap my brain around it all eventually :)

Enhance `:day-cell-factory` support or provide example

I'm trying to add a :day-cell-factory to a :date-picker. It seems that I have to reify Callback etc for this, in contrast to :list-view's :cell-factory which accepts a fn and reifies for me.

I don't quite understand enough about how the :list-view :cell-factory function arity rises from 1 (where it's defined, e.g.) to 4 (where it's called, in cljfx.fx.text-field-list-cell/create) to be able to implement the equivalent thing for DateCell, and so can't offer a PR.

Can you point me in the right direction?

Edit I found lifecycle/detached-prop-map which seems to be where the apparent arity change comes from. Will come back when have some spare time and have a go at PR...

Can't translate scene parallel camera

I'm trying to move a scene camera but when I try to set it's translate-x property I'm getting :

clojure.lang.ExceptionInfo: No such prop: :translate-x {:prop :translate-x}
	at cljfx.composite$create_props$fn__17058.invoke(composite.clj:20)
	at clojure.lang.PersistentArrayMap.kvreduce(PersistentArrayMap.java:377)
	at clojure.core$fn__8437.invokeStatic(core.clj:6845)
	at clojure.core$fn__8437.invoke(core.clj:6830)

Here is a minimal example that reproduce the issue :

(def app
  (fx/create-app (atom (fx/create-context {}))
                 :event-handler (fn [e])
                 :desc-fn (fn [_]
                            {:fx/type :stage
                             :showing true
                             :width 800
                             :height 600
                             :scene {:fx/type :scene                                       
                                     :camera {:fx/type :parallel-camera
                                              :translate-x 100}
                                     :root {:fx/type :group
                                            :children [{:fx/type :rectangle
                                                        :x 0
                                                        :y 0
                                                        :width 5
                                                        :height 5}]}}})))

Since cameras are javafx.scene.Node I should be able to move it like this or am I missing something?

Lein support?

I can get this to work with openjdk 11 + deps.edn, however openjdk 11 + lein seems to run into a dead-end (cljfx.api init not being found on classpath etc.).

In talking in the clojurians clojure channel, some seem to be able to use this just fine with lein under an Oracle Java setup.

I added the dependency to project.clj and after lein deps could never get it to launch correctly.

Has anyone had any luck with lein + openjdk 11? (or lein + openjdk 8 with openjfx via package manager on a GNU/Linux box?)

Can renderer middleware be replaced with extension lifecycles?

It can benefit from caching done in lifecycles, currently renderer middleware is invoked on every call to renderer.

  • wrap-many middleware can be done as ext-many that has some coll of descriptions and produces vec of components
  • wrap-context-desc can both put context in opts and wrap :fx.opt/type->lifecycle
  • wrap-map-desc is the real reason middleware exists: to transform renderer input into desc, can it be done differently in backwards-compatible way?

Transducer-based renderer

Something like that:

(fx/observe
  (comp
    (dedupe)
    (map
      (fn [progress]
        {:fx/type :stage
         :showing true
         :scene ...}))
    (render-on-fx-thread))
  (fx/ref->observable *progress))

Open questions:

  • what about opts? they can be changed! is it feasible to do opts-related stuff as extension lifecycles?

AOT stalls with only cljfx.api required

following the reddit thread here

user geokon failed to aot compile an uberjar with a main class, compilation stalled overnight. further dissection led to discovering that the require for cljfx.api caused a stall during AOT compilation, but not at the REPL.

minimal reproducible example repo

Hypothesis is that there's some deadlock happening in the javafx thread and initialization that's being tripped over during AOT compilation. This is on java 1.8 on my setup, no idea off the original user's. Seems odd since vlaad is apparently able to AOT stuff enough to get graal native image going....

Decomplect event handlers

Current implementation forces distinction between functions and maps on user. Actual event handler should be a function of 2 arguments: handler definition (which are currently functions or maps) and event object generated by javafx.

In a ListView how to detect a click?

Sorry if I missed it, I searched the repo via github search and on the readme page - if I have a list of items (derived from the cells example, the one that lists out 16r1000 gradient colors), how can I detect which item in the list is clicked?

Ideally using the split view / state management functionality as shown in the other example script for handling events like ::set-done without coupling functions to the view.

How to hook up a spinner to a factory?

I'm trying to make an input cell that's just for integers and I thought a integer spinner would be perfect for that - however I can't figure out how to hook up a integer-spinner-value-factory to a spinner

I tried to find other "factory" based code and I found this example:
https://github.com/cljfx/cljfx/blob/30a8c71d5bab559a733e4bb19d1412ef1227b0f5/examples/e16_cell_factories.clj

But all the elements have a :cell-factory key for the factory to be hooked up to. Is there any way you could maybe add an example? Thank you for the wonderful library as always. I've been using it regularly the past couple of months and it beats everything else for making native GUIs

Reacting to changes in width/height

I'm trying to have my code react to a change in the window's height/width (it's generating a custom plot though thing/geom and a SVG to JFX thing I have set up) and I'm a bit confused as to how to set this up properly

From what I'm seeing a :stage will include the properties of :window
https://github.com/cljfx/cljfx/blob/master/src/cljfx/fx/stage.clj#L14
And a :window will have a :on-width-changed property to which I can hook up an event handler
https://github.com/cljfx/cljfx/blob/master/src/cljfx/fx/window.clj#L29
However unlike most lines that of the form
:on-blahblah [:setter lifecycle/event-handler :coerce coerce/event-handler]
this one has a
:on-width-changed [:property-change-listener lifecycle/change-listener]

Hooking up another entry in my event handler multimethod doesn't seem to be working properly

It's defined here: https://github.com/cljfx/cljfx/blob/master/src/cljfx/lifecycle.clj#L156

But I don't really understand the code.

My guess is that somewhere in my renderer setup

(def renderer
  (fx/create-renderer
   :middleware (fx/wrap-map-desc assoc :fx/type root)
   :opts {:fx.opt/map-event-handler event-handler}))

I need to add a second type of event handler? However I can't hunt down the right incantation in the code

My million dollar stupid question is - what's a lifecycle? It's used freely in the README and all over the code, but the only explanation I'm finding is at the end of the README that starts with "Lifecycle is well, a lifecycle of a component. [...] "

Is this a term from React? Or some Clojure design pattern? I can't tease out what it means and yet it's the backbone of the whole system :)

NPE using :date-picker

Thanks for a magnificent library. I've been using it (version 1.5.1) very happily for a while now, via the fx/create-app function, using map events, wrapped effects, and subscriptions to an fx/context.

I've run into a problem with {:fx/type :date-picker}. Its :on-value-changed map event correctly reaches the handler function, which correctly generates an effect description. I can manually fire the corresponding effect using this description at the repl, and the UI updates correctly.

However, when I try to let it act on the effect description in the usual way, I get a NullPointerException:

java.lang.NullPointerException
	at cljfx.event_handler$wrap_effects$dispatch_sync_BANG___30311.invoke(event_handler.clj:28)
	at cljfx.event_handler$process_event.invokeStatic(event_handler.clj:31)
	at cljfx.event_handler$process_event.invoke(event_handler.clj:30)
	at clojure.lang.AFn.applyToHelper(AFn.java:171)
	at clojure.lang.AFn.applyTo(AFn.java:144)
	at clojure.core$apply.invokeStatic(core.clj:671)
	at clojure.core$binding_conveyor_fn$fn__5754.doInvoke(core.clj:2041)
	at clojure.lang.RestFn.applyTo(RestFn.java:146)
	at clojure.lang.Agent$Action.doRun(Agent.java:114)
	at clojure.lang.Agent$Action.run(Agent.java:163)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
	at java.base/java.lang.Thread.run(Thread.java:834)

If I mark the event map :fx/sync true, the application just hangs.

The effect description doesn't seem to make it to the effect function.

Can you tell what's going on from this description? I feel like I'm failing to understand some lifecycle or threading concept somewhere.

fxml support

Is there a way to use fxml files with this whilst still maintaining a repl based development flow?

Add :error-handler arg to create-renderer fn

Current error handler just prints stacktraces, which is not what users might want. Also renderer should catch Throwables, not Exceptions, so users can handle any kind of problems there. Default behavior should re-throw errors still.

Overly conservative leaked context error?

While writing unit tests for an unrelated context bug, I made this minimal example of an interesting leaked context error:

(require '[cljfx.context :as context])
(let [max-rounds 1
      *round (atom 0)
      f (fn f [context]
          (when (<= (swap! *round inc) max-rounds)
            (context/sub context f))
          (context/sub context :anything))
      context (context/create {} identity)
      _ (context/sub context f)]
  )

;Execution error (AssertionError) at cljfx.context/assert-not-leaked (context.clj:70).
;Assert failed: cljfx.context_test$eval2008$f__2009@508f2c52 is attempting to subscribe to bound context which already has value
;
;Possible reasons:
;- you return lazy seq which uses `cljfx.api/sub` while calculating elements
;- you leaked context from subscription function's scope without unbinding it first (call `cljfx.api/unbind-context` on it in that case)
;(not (has? cache sub-id))

There's no laziness or leaked contexts here, so is this a bug?

Updating line-chart produces weird result

I get this result when trying to update a line-chart:

image

using this code:

(ns cljfx.exploration
  (:require [cljfx.api :as fx]
            [user :refer [fx-help]]))


(defn root-view [{:keys [showing line-data]}]
  {:fx/type :stage
   :showing showing
   :width 960
   :height 540
   :on-close-request {::event ::close-window}
   :scene {:fx/type :scene
           :root {:fx/type :v-box
                  :children [{:fx/type :line-chart
   :x-axis {:fx/type :number-axis
            :auto-ranging true}
   :y-axis {:fx/type :number-axis
            :auto-ranging false}
   :data [{:fx/type :xy-chart-series
           :name "data"
           :data line-data}]}]}}})
;; events

(defmulti handle ::event)

(defmethod handle :default [event]
  (println (::event event) (dissoc event ::event :state)))

(defmethod handle ::close-window [{:keys [state]}]
  {:set-state (assoc state :showing false)})

;; renderer setup

(defonce *state
  (atom {:showing true}))

(def map-event-handler
  (-> handle
      (fx/wrap-co-effects
        {:state (fx/make-deref-co-effect *state)})
      (fx/wrap-effects
        {:set-state (fx/make-reset-effect *state)
         :dispatch fx/dispatch-effect})))

(defonce renderer
  (fx/create-renderer
    :middleware (fx/wrap-map-desc #'root-view)
    :opts {:fx.opt/map-event-handler #'map-event-handler}))

(fx/mount-renderer *state renderer)


(comment
  ;; Initial event setup binds state's :showing key to window's :showing prop. If you
  ;; close a window, you can open it again by changing atom:
  (swap! *state assoc :showing true :line-data
         [{:fx/type :xy-chart-data, :x-value 0, :y-value 0}
 {:fx/type :xy-chart-data, :x-value 1, :y-value 27}
 {:fx/type :xy-chart-data, :x-value 2, :y-value 45}
 {:fx/type :xy-chart-data, :x-value 3, :y-value 26}
 {:fx/type :xy-chart-data, :x-value 4, :y-value 88}
 {:fx/type :xy-chart-data, :x-value 5, :y-value 45}
 {:fx/type :xy-chart-data, :x-value 6, :y-value 72}
 {:fx/type :xy-chart-data, :x-value 7, :y-value 62}
 {:fx/type :xy-chart-data, :x-value 8, :y-value 35}
 {:fx/type :xy-chart-data, :x-value 9, :y-value 54}]
         )

  ;; You need to trigger UI refresh when modifying view components since `renderer` does
  ;; not watch them as it watches `*state`, this can be achieved by "touching" the state:
  (swap! *state identity)
  ;; Alternatively, you can just reload this namespace: `fx/mount-renderer` will also
  ;; trigger UI refresh.


  (swap! *state update :line-data conj
                        {:fx/type :xy-chart-data
                          :x-value 10
                          :y-value 100})

  ;; when in doubt, you can use `fx-help` function:
  ;; - to get short overall javafx/cljfx components overview
  (println (fx-help))
  ;; - to list all available props on a particular built-in component
  (fx-help :v-box)
  ;; - to show some prop information
  (fx-help :v-box :children)
  (fx-help :v-box :padding))

Using :owner prop on for Dialog causes IllegalArgumentException

Using the :owner property for Dialog results in an IllegalArgumentException. If I manually invoke initOwner and then show, there is no issue.

Run this script in a REPL. You should see the following IllegalArgumentException appear

java.lang.IllegalArgumentException: No implementation of method: :create of protocol: #'cljfx.lifecycle/Lifecycle found for class: nil
(require '[cljfx.api :as fx])

(defn main-stage
  [_]
  {:fx/type :stage
   :showing true
   :scene {:fx/type :scene
           :root {:fx/type :label
                  :text "Dialog owner IllegalArgumentException demo"}}})

(defn dialog-with-owner
  [{:keys [owner]}]
  {:fx/type :dialog
   :showing true
   :owner owner
   :dialog-pane {:fx/type :dialog-pane
                 :content-text "This dialog will have the owner set via cljfx and will result in an IllegalArgumentException"
                 :button-types [:ok]}})

(defn dialog-without-owner
  [_]
  {:fx/type :dialog
   :dialog-pane {:fx/type :dialog-pane
                 :content-text "This dialog will have the owner set manually and will have no issues"
                 :button-types [:ok]}})

(fx/on-fx-thread
  (let [owner (fx/instance (fx/create-component {:fx/type main-stage}))]
    (try
      (fx/create-component {:fx/type dialog-with-owner :owner owner})
      (catch IllegalArgumentException e (.printStackTrace e)))
    (doto (fx/instance (fx/create-component {:fx/type dialog-without-owner}))
      (.initOwner owner)
      (.show))))

Changing the :owner prop to use lifecycle/scalar instead of lifecycle/dynamic resolves this issue

Compilation seems to try to open a display

this is a weird one...
I can build an uber jar locally in a terminal emulator, but if I try to do it through a terminal directly it won't build.

I discovered this by accident through Github Actions:
https://github.com/geokon-gh/sausage/runs/640515413?check_suite_focus=true

I can replicate it locally as well by switching out of X (with Ctrl+Alt+F2) and trying to build an uberjar there (my setup is very similar to your setup in hn). The error reads

Caused by: java.lang.UnsupportedOperationException: Unable to open DISPLAY

And it's triggered by a namespace that seems to just define some effects functions. I also have a weird issues where when the application first launches it doesn't render immediately. I have to resize the window or move it around for the things to render. I feel it may be related

Anyways, if you've come across things - please let me know. Otherwise I'll keep trying to diagnose the issue and post a solution here for others to see once/if I do :)

classifier syntax in deps.edn

Hi, just wanted to let you know that the classifier syntax in deps.edn has changed in the latest release of clj (1.10.0.408). In your case, it looks like you don't actually even need the classifiers at all. The openjfx artifacts depend on the correct platform-specific (based on the caller's platform) classifier version of the artifact.
The latest release of deps.edn addresses many issues with classifiers and will correctly pull in those deps now. So, I believe all you need to do to work with the latest version is to simply remove all of the :classifier attributes from your deps.edn.
If you have any issues, please file a ticket at https://dev.clojure.org/jira/browse/TDEPS for tools.deps.alpha.

lein uberjar never finishes

Quoting @victorb:

@vlaaad another thing. Seems lein uberjar never finishes it's build, unsure of why. Seems there is maybe some issue with when clojure runs the classes static initializer and something gets started, which never gets closed afterwards.

Copying the example you made above:

(ns lein-cljfx-test.core
  (:require [cljfx.api :as fx])
  (:gen-class)
  (:import [javafx.application Platform]))

(defn -main [& args]
  (Platform/setImplicitExit true)
  (fx/on-fx-thread
    (fx/create-component
      {:fx/type :stage
       :showing true
       :scene {:fx/type :scene
               :root {:fx/type :v-box
                      :children [{:fx/type :label
                                  :text "Hello from lein!"}]}}})))

Runs fine with lein run (and thanks to setImplicitExit, also exits the full application when closing the javafx application)

However, running lein uberjar with that example, leads to the lein getting stuck at Compiling lein-cljfx-test.core forever (seemingly, I let it run for 20 minutes)

Running strace -f lein uberjar also confirms that it's waiting for something, but that something never happens.

Any advice on what could be happening?

(using clojure 1.10.0 + cljfx 1.2.10 with both openjdk and jdk version 11)

Lessons learned / Things I would have done differently

There are couple of higher level design decisions that I feel are somewhat limiting, so if I were to make cljfx-next (I'm not), I would do these differently:

  1. I would extend Lifecycle protocol to keywords and functions instead of relying on :fx.opt/type->lifecycle. This thing actually might be fixable in cljfx, but ensuring no breaking changes will be very difficult.
  2. I would not use renderer middleware at all (see #13), and instead rely more on extension lifecycles.
  3. I would use namespaced keywords (instead of plain ones) for props, and would try to allow extension lifecycles be parts of component description they wrap:
;; so that this:

{:fx/type fx/ext-on-instance-lifecycle
 :on-created prn
 :desc {:fx/type :label
        :text "text"}}

;; would become something like this:

{:fx/type :label
 fx/ext-on-instance-created prn
 :fx.labeled/text "text"}
  1. renderer abstraction would become "view description that has access to data" instead of "transformation from data to view description"
  2. I would explore making lifecycles or their surroundings a transducer context. It would be nice to write (comp (dedupe) (fx/advance-lifecycle)) to skip same descriptions, and it might prove itself useful on other areas.
  3. I would explore how to make it truly reactive instead of doing what react does, so when change happens, a system would know what exactly should be updated instead of doing "virtual dom diffing". See svelte.

Some problems I found that don't map well to cljfx/react model:

  • some props don't belong on any particular component: managing the focus between different components is "somewhere else".
  • inequality of prop values does not always mean reassignment of new value has to happen: event listeners are invoked when events happen, so they need to know their value only at a later point in time. Having the same event listener while the prop is set and looking up the latest value while the event is fired is more efficient.

Renderer-opt ":fx.opt/map-event-handler" not working

I have the following renderer:

(defmulti event-handler :event/type)

(defonce renderer
  (fx/create-renderer 
   :middleware (comp
                fx/wrap-context-desc
                (fx/wrap-map-desc (fn [state] {:fx/type sampletable
                                              :state state})))
   :opts {:fx.opt/type->lifecycle #(or (fx/keyword->lifecycle %)
                                       (fx/fn->lifecycle-with-context %))
          :fx.opt/map-event-hander event-handler}))

But event-handler never gets called when I trigger an event; instead, I get a console output starting with :cljfx.defaults/unhandled-map-event listing the event map.

How do I implement a logging window using cljfx?

When I wanted to implement a log window with automatic scrolling, I found it difficult to do so.

Major difficulties:

  1. The implementation of automatic scrolling, the text property (setText) of the text-area will reset the scroll bar to the top, you need to update the scroll-top after updating the text, but setting these two properties at the same time does not achieve the purpose.

  2. The nested call of the event handler, if the event handler uses the log function to record the log, and the log function triggers the event handler to add the log (the log-appender I added for timbre), the latter handler for the context Changes are overwritten by the top-level context, and the log event handler has no effect.

  3. It is also difficult to implement Select all / Copy text-area text in the custom context menuใ€‚

sorry for my bad english.

The example code is as follows:

(require '[cljfx.composite :as composite]
         '[cljfx.lifecycle :as lifecycle]
         '[cljfx.coerce :as coerce]
         '[cljfx.api :as fx]
         '[cljfx.prop :as prop]
         '[cljfx.mutator :as mutator])

(def with-scroll-change-prop
  (lifecycle/make-ext-with-props
   lifecycle/dynamic
   {:on-scroll-left-changed (prop/make
                             (mutator/property-change-listener #(.scrollLeftProperty %))
                             lifecycle/change-listener)
    :on-scroll-top-changed (prop/make
                            (mutator/property-change-listener #(.scrollTopProperty %))
                            lifecycle/change-listener)
    }))

(def *state
  (atom (fx/create-context
          {:logs ""
           :scroll-top 0
           :auto-scroll true
           :lbl-value "test log in event handler!"})))

(defn log-form [{:keys [fx/context]}]
  (let [log-scroll-top (fx/sub context :scroll-top)
        auto-scroll (fx/sub context :auto-scroll)]
    {:fx/type with-scroll-change-prop
     :props {:on-scroll-top-changed {:event/type :event/value-change
                                     :key :scroll-top}}
     :desc {:fx/type :text-area
            :editable false
            :text (fx/sub context :logs)
            :scroll-top log-scroll-top
            :context-menu {:fx/type :context-menu
                           :items [{:fx/type :check-menu-item
                                    :text "Auto Scroll"
                                    :selected auto-scroll
                                    :on-action {:event/type :event/auto-scroll-change
                                                :value (not auto-scroll)}}]}
            }}))

;;;;;;;; event-handler
(defmulti event-handler :event/type)

(defmethod event-handler :default [event]
  (prn event))

(defmethod event-handler :event/value-change [{:keys [fx/context fx/event key]}]
  {:context (fx/swap-context context assoc key event)})

(defmethod event-handler :event/auto-scroll-change [{:keys [fx/context value]}]
  {:context (fx/swap-context context assoc :auto-scroll value)})

(defmethod event-handler :event/add-log [{:keys [fx/context log]}]
  (let [old-log (fx/sub context :logs)]
    {:context (fx/swap-context context assoc :logs (str old-log log))}))

(defmethod event-handler :event/scroll-top [{:keys [fx/context]}]
  (let [auto-scroll (fx/sub context :auto-scroll)
        scroll-top (fx/sub context :scroll-top)]
    (prn "auto-scroll:" auto-scroll " scroll-top:" scroll-top)
    {:context
     (fx/swap-context context assoc
                      :scroll-top (if auto-scroll
                                        ;; Must be different from last value to rerenderer
                                        ;; Can I force a rerenderer in event-handler?
                                        (- Integer/MAX_VALUE (rand-int 100))
                                        (+ 0.01 scroll-top)))}))

(def real-event-handler
  (-> event-handler
      (fx/wrap-co-effects
       {:fx/context (fx/make-deref-co-effect *state)})
      (fx/wrap-effects
       {:context (fx/make-reset-effect *state)})))

;;;;;;;;;;;;;;;; logging, Use functions instead of log appender
(defn log
  [s]
  (real-event-handler {:event/type :event/add-log
                       :fx/sync true
                       :log (str s "\n")})
  ;;** Question 1
  ;; You must add sleep to wait for add-log(setText) to complete before sending the scrollTop event
  ;; Because setText resets scrollTop to 0
  (Thread/sleep 100)
  (real-event-handler {:event/type :event/scroll-top}))

(defmethod event-handler :event/button-click [{:keys [fx/context]}]
  ;;** Question 2
  (log "button click event-handler!") ;; !!! This will not work
  ;;  because the context modification below overwrites the log modified context here
  {:context (fx/swap-context context assoc :lbl-value "button clicked!")})

(defn test-event-form [{:keys [fx/context]}]
  {:fx/type :h-box
   :children [{:fx/type :button
               :text "test"
               :on-action {:event/type :event/button-click}}
              {:fx/type :label
               :text (fx/sub context :lbl-value)}]})

;;;;;;;;;;;;;;; renderer
(def renderer
  (fx/create-renderer
    :middleware (comp
                  fx/wrap-context-desc
                  (fx/wrap-map-desc (fn [_]
                                      {:fx/type :stage
                                       :showing true
                                       :scene {:fx/type :scene
                                               :root {:fx/type :v-box
                                                      :children [{:fx/type test-event-form}
                                                                 {:fx/type log-form}]}}})))
    :opts {:fx.opt/map-event-handler real-event-handler
           :fx.opt/type->lifecycle #(or (fx/keyword->lifecycle %)
                                        (fx/fn->lifecycle-with-context %))}))

(fx/mount-renderer *state renderer)

;;; test logging
(doseq [i (range 20)]
  (log (str "log:" i)))

cljfx reflection warning on un-resolved method updateItem on javafx.scene.control.TableCell

I run re-find.fx which using cljfx as GUI library.
Here is the command:

clj -Sdeps "{:deps                   {MageMasher/re-find.fx                      {:git/url \"https://github.com/MageMasher/re-find.fx\"                       :sha \"b29bde3519f7632b63eb71415943bf8a7cfa1462\"}}}" \    -m re-find.fx

Then I got following wanring.

Reflection warning, cljfx/fx/table_cell.clj:33:11 - call to method updateItem on javafx.scene.control.TableCell can't be resolved (no such method).

What triggers a redraw?

Hey vlaaad,

I'm trying to spruce up my code base and refactor things (maybe to eventually use the subscription/context stuff if I finally understand how it works) and I realize I don't quite fully understand my current event/atom dangerously growing monster GUI

Maybe a reading list would be helpful in the README :) - I'm still struggling to piece together what lifecycles, middleware and co-effects are

The way I thought it worked is that the system makes a big nested hiccup-style map of the GUI, then when the state is updated it will recreate the map, diffs it with the previous version, and then redraw the differences. Under the hood that maps to JavaFX Objects that are either getting updated or regenerated as it goes

However rereading now I realize that's probably simplistic and it's probably doing something more clever

For one the notation doesn't match my expectation. A lot of maps "call" other maps through the "value fields"

I'll try to make a simple example. Say I make two custom elements:

(defn my-first-button-thing
  [{:keys [first-width
           first-text]}]
  {:fx/type :v-box
   :children [{:fx/type :text
               :width first-width
               :text first-text}
              {:fx/type :button
               :width first-width
               :text first-text}]})

(defn my-second-button-thing
  [{:keys [second-width
           second-text]}]
  {:fx/type :v-box
   :children [{:fx/type :button
               :width second-width
               :text second-text}
              {:fx/type :text
               :width second-width
               :text second-text}]})

Canonically it seems to use them I would then write something like

(defn my-button-panel
  [{:keys [first-width
           first-text
           second-width
           second-text]}]
  {:fx/type :h-box
   :children [{:fx/type my-first-button-thing
               :first-width first-width
               :first-text first-text}
              {:fx/type my-second-button-thing
               :second-width second-width
               :second-text second-text}]})

But then I don't really get how that gets executed, and when. It's a key-value pair where the value is a function. I'm guessing that function gets called later? But only if something in the rest of the map has changed? Is that correct?

I'd say about half the fields are just sub-chunks of the overall state that are getting passed along to the children in different combinations. The other half of my values involve some bit of calculation. If the calculations are simply skipped when the element doesn't need updating - then I can more liberally do more complex things deeper in the GUI map. If the whole map is regenerated each time, then maybe I need to be more careful and cache things (I try to not overstuff the state with cached values b/c keeping the caches updated is a maintenance nightmare)

All the repeating keys are a bit of code smell as well b/c I keep wanting to leverage destructuring and write:

(defn my-button-panel
  [{:keys [first-width
           first-text
           second-width
           second-text] input-stuff}]
  {:fx/type :h-box
   :children [(my-first-button-thing input-stuff)
              ( my-second-button-thing input-stuff)]})

But since you never seem to do that.. i figure there is a good reason!

How do I select files/folders?

Is there some built-in way to call up a file/folder-picker/chooser?

I'd like to have the user be able to click an "Open" button and then your typical file-picker dialogue shows up and you can select an image or whatever to load. Having to type out a whole path would be onerous and writing a file-picker from scratch would be a big undertaking (and it'd probably end up feeling unnatural)

Could I get your advice @vlaaad on how I should approach this? I saw that JavaFX has a FileChooser built-in. Should I just be calling this through Java interop?

Extension lifecycle that adds extra props

It might be useful to have a way to define a bunch of props (mutator + lifecycle) that get applied to previously created component. That way some synthetic props may be provided by users and be opt in, instead of being specified in built-in lifecycles, thus making set of available props confusing.

Unsupported major.minor version 55.0

Just been looking into the lib post the reddit post announcement and tried to import it into the repl trying out this example:

(require '[cljfx.api :as fx]
         '[clojure.core.cache :as cache]
         '[datascript.core :as d])
Syntax error (UnsupportedClassVersionError) compiling at (cljfx/coerce.clj:1:1).
javafx/util/converter/LocalDateStringConverter : Unsupported major.minor version 55.0

*e
=>
#error{:cause "javafx/util/converter/LocalDateStringConverter : Unsupported major.minor version 55.0",
       :via [{:type clojure.lang.Compiler$CompilerException,
              :message "Syntax error compiling at (cljfx/coerce.clj:1:1).",
              :data #:clojure.error{:phase :compile-syntax-check, :line 1, :column 1, :source "cljfx/coerce.clj"},
              :at [clojure.lang.Compiler load "Compiler.java" 7648]}
             {:type java.lang.UnsupportedClassVersionError,
              :message "javafx/util/converter/LocalDateStringConverter : Unsupported major.minor version 55.0",
              :at [java.lang.ClassLoader defineClass1 "ClassLoader.java" -2]}],
       :trace [[java.lang.ClassLoader defineClass1 "ClassLoader.java" -2]
               [java.lang.ClassLoader defineClass "ClassLoader.java" 760]
               [java.security.SecureClassLoader defineClass "SecureClassLoader.java" 142]
               [java.net.URLClassLoader defineClass "URLClassLoader.java" 455]
               [java.net.URLClassLoader access$100 "URLClassLoader.java" 73]
               [java.net.URLClassLoader$1 run "URLClassLoader.java" 367]
               [java.net.URLClassLoader$1 run "URLClassLoader.java" 361]
               [java.security.AccessController doPrivileged "AccessController.java" -2]
               [java.net.URLClassLoader findClass "URLClassLoader.java" 360]
               [java.lang.ClassLoader loadClass "ClassLoader.java" 424]
               [sun.misc.Launcher$AppClassLoader loadClass "Launcher.java" 308]
               [java.lang.ClassLoader loadClass "ClassLoader.java" 411]
               [clojure.lang.DynamicClassLoader loadClass "DynamicClassLoader.java" 77]
               [java.lang.ClassLoader loadClass "ClassLoader.java" 411]
               [clojure.lang.DynamicClassLoader loadClass "DynamicClassLoader.java" 77]
               [java.lang.ClassLoader loadClass "ClassLoader.java" 411]
               [clojure.lang.DynamicClassLoader loadClass "DynamicClassLoader.java" 77]
               [java.lang.ClassLoader loadClass "ClassLoader.java" 411]
               [clojure.lang.DynamicClassLoader loadClass "DynamicClassLoader.java" 77]
               [java.lang.ClassLoader loadClass "ClassLoader.java" 411]
               [clojure.lang.DynamicClassLoader loadClass "DynamicClassLoader.java" 77]
               [java.lang.ClassLoader loadClass "ClassLoader.java" 411]
               [clojure.lang.DynamicClassLoader loadClass "DynamicClassLoader.java" 77]
               [java.lang.ClassLoader loadClass "ClassLoader.java" 411]
               [clojure.lang.DynamicClassLoader loadClass "DynamicClassLoader.java" 77]
               [java.lang.ClassLoader loadClass "ClassLoader.java" 411]
               [clojure.lang.DynamicClassLoader loadClass "DynamicClassLoader.java" 77]
               [java.lang.ClassLoader loadClass "ClassLoader.java" 411]
               [clojure.lang.DynamicClassLoader loadClass "DynamicClassLoader.java" 77]
               [java.lang.ClassLoader loadClass "ClassLoader.java" 411]
               [clojure.lang.DynamicClassLoader loadClass "DynamicClassLoader.java" 77]
               [java.lang.ClassLoader loadClass "ClassLoader.java" 411]
               [clojure.lang.DynamicClassLoader loadClass "DynamicClassLoader.java" 77]
               [java.lang.ClassLoader loadClass "ClassLoader.java" 411]
               [clojure.lang.DynamicClassLoader loadClass "DynamicClassLoader.java" 77]
               [java.lang.ClassLoader loadClass "ClassLoader.java" 411]
               [clojure.lang.DynamicClassLoader loadClass "DynamicClassLoader.java" 77]
               [java.lang.ClassLoader loadClass "ClassLoader.java" 411]
               [clojure.lang.DynamicClassLoader loadClass "DynamicClassLoader.java" 77]
               [java.lang.ClassLoader loadClass "ClassLoader.java" 411]
               [clojure.lang.DynamicClassLoader loadClass "DynamicClassLoader.java" 77]
               [java.lang.ClassLoader loadClass "ClassLoader.java" 411]
               [clojure.lang.DynamicClassLoader loadClass "DynamicClassLoader.java" 77]
               [java.lang.ClassLoader loadClass "ClassLoader.java" 411]
               [clojure.lang.DynamicClassLoader loadClass "DynamicClassLoader.java" 77]
               [java.lang.ClassLoader loadClass "ClassLoader.java" 357]
               [java.lang.Class forName0 "Class.java" -2]
               [java.lang.Class forName "Class.java" 340]
               [clojure.lang.RT classForName "RT.java" 2211]
               [clojure.lang.RT classForNameNonLoading "RT.java" 2224]
               [cljfx.coerce$eval32090$loading__6721__auto____32091 invoke "coerce.clj" 1]
               [cljfx.coerce$eval32090 invokeStatic "coerce.clj" 1]
               [cljfx.coerce$eval32090 invoke "coerce.clj" 1]
               [clojure.lang.Compiler eval "Compiler.java" 7177]
               [clojure.lang.Compiler eval "Compiler.java" 7166]
               [clojure.lang.Compiler load "Compiler.java" 7636]
               [clojure.lang.RT loadResourceScript "RT.java" 381]
               [clojure.lang.RT loadResourceScript "RT.java" 372]
               [clojure.lang.RT load "RT.java" 459]
               [clojure.lang.RT load "RT.java" 424]
               [clojure.core$load$fn__6839 invoke "core.clj" 6126]
               [clojure.core$load invokeStatic "core.clj" 6125]
               [clojure.core$load doInvoke "core.clj" 6109]
               [clojure.lang.RestFn invoke "RestFn.java" 408]
               [clojure.core$load_one invokeStatic "core.clj" 5908]
               [clojure.core$load_one invoke "core.clj" 5903]
               [clojure.core$load_lib$fn__6780 invoke "core.clj" 5948]
               [clojure.core$load_lib invokeStatic "core.clj" 5947]
               [clojure.core$load_lib doInvoke "core.clj" 5928]
               [clojure.lang.RestFn applyTo "RestFn.java" 142]
               [clojure.core$apply invokeStatic "core.clj" 667]
               [clojure.core$load_libs invokeStatic "core.clj" 5985]
               [clojure.core$load_libs doInvoke "core.clj" 5969]
               [clojure.lang.RestFn applyTo "RestFn.java" 137]
               [clojure.core$apply invokeStatic "core.clj" 667]
               [clojure.core$require invokeStatic "core.clj" 6007]
               [clojure.core$require doInvoke "core.clj" 6007]
               [clojure.lang.RestFn invoke "RestFn.java" 482]
               [cljfx.lifecycle$eval32082$loading__6721__auto____32083 invoke "lifecycle.clj" 1]
               [cljfx.lifecycle$eval32082 invokeStatic "lifecycle.clj" 1]
               [cljfx.lifecycle$eval32082 invoke "lifecycle.clj" 1]
               [clojure.lang.Compiler eval "Compiler.java" 7177]
               [clojure.lang.Compiler eval "Compiler.java" 7166]
               [clojure.lang.Compiler load "Compiler.java" 7636]
               [clojure.lang.RT loadResourceScript "RT.java" 381]
               [clojure.lang.RT loadResourceScript "RT.java" 372]
               [clojure.lang.RT load "RT.java" 459]
               [clojure.lang.RT load "RT.java" 424]
               [clojure.core$load$fn__6839 invoke "core.clj" 6126]
               [clojure.core$load invokeStatic "core.clj" 6125]
               [clojure.core$load doInvoke "core.clj" 6109]
               [clojure.lang.RestFn invoke "RestFn.java" 408]
               [clojure.core$load_one invokeStatic "core.clj" 5908]
               [clojure.core$load_one invoke "core.clj" 5903]
               [clojure.core$load_lib$fn__6780 invoke "core.clj" 5948]
               [clojure.core$load_lib invokeStatic "core.clj" 5947]
               [clojure.core$load_lib doInvoke "core.clj" 5928]
               [clojure.lang.RestFn applyTo "RestFn.java" 142]
               [clojure.core$apply invokeStatic "core.clj" 667]
               [clojure.core$load_libs invokeStatic "core.clj" 5985]
               [clojure.core$load_libs doInvoke "core.clj" 5969]
               [clojure.lang.RestFn applyTo "RestFn.java" 137]
               [clojure.core$apply invokeStatic "core.clj" 667]
               [clojure.core$require invokeStatic "core.clj" 6007]
               [clojure.core$require doInvoke "core.clj" 6007]
               [clojure.lang.RestFn invoke "RestFn.java" 436]
               [cljfx.defaults$eval32074$loading__6721__auto____32075 invoke "defaults.clj" 1]
               [cljfx.defaults$eval32074 invokeStatic "defaults.clj" 1]
               [cljfx.defaults$eval32074 invoke "defaults.clj" 1]
               [clojure.lang.Compiler eval "Compiler.java" 7177]
               [clojure.lang.Compiler eval "Compiler.java" 7166]
               [clojure.lang.Compiler load "Compiler.java" 7636]
               [clojure.lang.RT loadResourceScript "RT.java" 381]
               [clojure.lang.RT loadResourceScript "RT.java" 372]
               [clojure.lang.RT load "RT.java" 459]
               [clojure.lang.RT load "RT.java" 424]
               [clojure.core$load$fn__6839 invoke "core.clj" 6126]
               [clojure.core$load invokeStatic "core.clj" 6125]
               [clojure.core$load doInvoke "core.clj" 6109]
               [clojure.lang.RestFn invoke "RestFn.java" 408]
               [clojure.core$load_one invokeStatic "core.clj" 5908]
               [clojure.core$load_one invoke "core.clj" 5903]
               [clojure.core$load_lib$fn__6780 invoke "core.clj" 5948]
               [clojure.core$load_lib invokeStatic "core.clj" 5947]
               [clojure.core$load_lib doInvoke "core.clj" 5928]
               [clojure.lang.RestFn applyTo "RestFn.java" 142]
               [clojure.core$apply invokeStatic "core.clj" 667]
               [clojure.core$load_libs invokeStatic "core.clj" 5985]
               [clojure.core$load_libs doInvoke "core.clj" 5969]
               [clojure.lang.RestFn applyTo "RestFn.java" 137]
               [clojure.core$apply invokeStatic "core.clj" 667]
               [clojure.core$require invokeStatic "core.clj" 6007]
               [clojure.core$require doInvoke "core.clj" 6007]
               [clojure.lang.RestFn invoke "RestFn.java" 619]
               [cljfx.api$eval32066$loading__6721__auto____32067 invoke "api.clj" 1]
               [cljfx.api$eval32066 invokeStatic "api.clj" 1]
               [cljfx.api$eval32066 invoke "api.clj" 1]
               [clojure.lang.Compiler eval "Compiler.java" 7177]
               [clojure.lang.Compiler eval "Compiler.java" 7166]
               [clojure.lang.Compiler load "Compiler.java" 7636]
               [clojure.lang.RT loadResourceScript "RT.java" 381]
               [clojure.lang.RT loadResourceScript "RT.java" 372]
               [clojure.lang.RT load "RT.java" 459]
               [clojure.lang.RT load "RT.java" 424]
               [clojure.core$load$fn__6839 invoke "core.clj" 6126]
               [clojure.core$load invokeStatic "core.clj" 6125]
               [clojure.core$load doInvoke "core.clj" 6109]
               [clojure.lang.RestFn invoke "RestFn.java" 408]
               [clojure.core$load_one invokeStatic "core.clj" 5908]
               [clojure.core$load_one invoke "core.clj" 5903]
               [clojure.core$load_lib$fn__6780 invoke "core.clj" 5948]
               [clojure.core$load_lib invokeStatic "core.clj" 5947]
               [clojure.core$load_lib doInvoke "core.clj" 5928]
               [clojure.lang.RestFn applyTo "RestFn.java" 142]
               [clojure.core$apply invokeStatic "core.clj" 667]
               [clojure.core$load_libs invokeStatic "core.clj" 5985]
               [clojure.core$load_libs doInvoke "core.clj" 5969]
               [clojure.lang.RestFn applyTo "RestFn.java" 137]
               [clojure.core$apply invokeStatic "core.clj" 667]
               [clojure.core$require invokeStatic "core.clj" 6007]
               [clojure.core$require doInvoke "core.clj" 6007]
               [clojure.lang.RestFn invoke "RestFn.java" 436]
               [wagons_and_wizards.server$eval32060 invokeStatic "user.clj" 29]
               [wagons_and_wizards.server$eval32060 invoke "user.clj" 29]
               [clojure.lang.Compiler eval "Compiler.java" 7177]
               [clojure.lang.Compiler eval "Compiler.java" 7132]
               [clojure.core$eval invokeStatic "core.clj" 3214]
               [clojure.core$eval invoke "core.clj" 3210]
               [nrepl.middleware.interruptible_eval$evaluate$fn__30651 invoke "interruptible_eval.clj" 112]
               [clojure.main$repl$read_eval_print__9086$fn__9089 invoke "main.clj" 437]
               [clojure.main$repl$read_eval_print__9086 invoke "main.clj" 437]
               [clojure.main$repl$fn__9095 invoke "main.clj" 458]
               [clojure.main$repl invokeStatic "main.clj" 458]
               [clojure.main$repl doInvoke "main.clj" 368]
               [clojure.lang.RestFn invoke "RestFn.java" 1523]
               [nrepl.middleware.interruptible_eval$evaluate invokeStatic "interruptible_eval.clj" 105]
               [nrepl.middleware.interruptible_eval$evaluate invoke "interruptible_eval.clj" 74]
               [nrepl.middleware.interruptible_eval$interruptible_eval$fn__30675$fn__30679
                invoke
                "interruptible_eval.clj"
                174]
               [clojure.lang.AFn run "AFn.java" 22]
               [nrepl.middleware.session$session_exec$main_loop__30779$fn__30783 invoke "session.clj" 197]
               [nrepl.middleware.session$session_exec$main_loop__30779 invoke "session.clj" 196]
               [clojure.lang.AFn run "AFn.java" 22]
               [java.lang.Thread run "Thread.java" 745]]}

Looking at the issues, this could be related to #45 or the fact that I'm using it with cursive?

Add prop to set properties map

Extra care needed to make sure we mutate only specifed keys, because other props use properties map as well (such as :h-box/hgrow etc.)

How to stop event propagation?

Hi, first of all thanks for the amazing lib!

I have a :on-mouse-pressed defined for both a Scene and a Rectangle inside the scene. When clicking the rectangle both events handlers get called. How do I stop event bubbling up aka stopPropagation?

Thanks!!!

Is it possible to multiple states/renderers in one view?

I would like to make a 'widget' (a component with an encapsulated state, using the event dispatching, but usable as an fx/type in a larger component).

Is such a thing possible?

It seems if I do the setup work around a 'button' - make a unique atom, bind it to watchers with create-renderer etc. but then click the actual button in the main 'root' wrapper, the state is never updated on the child element (well, the event never seems to dispatch at all).

NPE running the example code

The GUI comes up but I get NPE:

Exception in thread "main" java.lang.NullPointerException
at clojure.core$apply.invokeStatic(core.clj:665)
at clojure.main$main_opt.invokeStatic(main.clj:491)
at clojure.main$main_opt.invoke(main.clj:487)
at clojure.main$main.invokeStatic(main.clj:598)
at clojure.main$main.doInvoke(main.clj:561)
at clojure.lang.RestFn.applyTo(RestFn.java:137)
at clojure.lang.Var.applyTo(Var.java:705)
at clojure.main.main(main.java:37)

JavaFX8 Support

I have reason to maintain presence on javafx8, namely oracle's Java8SE, due to legacy circumstances. I went ahead and patched cljfx to support Javafx8 here. I tried to be as non-invasive as possible, and ended up with one major change in platform init (checking to see if version 8 is present), and modifying the properties maps for several controls. There seem to be new properties in 11 (thankfully old properties weren't deprecated). So I defined a macro in cljfx.composite, modern-props, which wraps the props macro. If the version (defined in new ns cljfx.version) is :eight, the modern-props emits nil. This works well in bifurcating the existing map-based property definitions for controls. I went through all the included examples in /examples, and followed the same process (discovering reflection errors to due expanded properties, then cordoning them off with the macro). In some cases there were extant reflection warnings for proxies (typically proxy-super methods). I patched these with hints/casting.

In all, the examples all render and work on my end running ZuluFX (Zulu openjdk + OpenJFX8). I believe they will work with other compatible jdk's (Oracle, and BellSoft).

Due to the JavaFX versioning being tied to JDK, it's a bit of mess with dependencies. fn-fx manages this with different profiles basedon jfx11 or not.

Note: the only exercising of this patch is what was around in the examples and what clojure tossed up in reflection warnings. It's possible that users could be following a javafx11 tutorial and trying to use properties on a javafx8 system and run into problems. I think that the examples cover a good deal of functionality though. I'd think this is more of a legacy platform for users who need to maintain code on Java8 systems (since Oracle screwed the pooch and didn't provide a bridging strategy :/).

Subscribing to elements deeper in a state map

Hooking up Contexts/subscriptions is proving to be challenging. I'm looking at the examples and playing around with the API, but I don't see a way to subscribe to elements deeper in my state tree. Can you only subscribe to top level keys?

Going off the toy example in task-tracker it'd be very useful to be able to, for instance, subscribe to the task :state. When his information is updated then some corresponding GUI elements would be updated.

You could flatten the state, but it feels a bit goofy to have keys like :user-0-name :user-0-state :user-0-assignee :user-1-name :user-1-state :user-1-assignee etc.

My state is quite a bit larger than the one in the example - so flattening seems impractical - but I'm unsure of how to proceed (other than just not using contexts :)) )

I'm in particular trying to take advantage of memoization of subscription functions on the equivalent of tasks in my state.

javafx.application.Platform/startup is invalid

When I (require '[cljfx.api :as fx]). I got error:

2. Unhandled clojure.lang.Compiler$CompilerException
   Error compiling src/cljfx/platform.clj at (30:5)
   #:clojure.error{:phase :compile-syntax-check,
                   :line 30,
                   :column 5,
                   :source
                   "/home/stardiviner/Org/Wiki/Computer Technology/Programming/Programming Languages/Clojure/Data/Clojure Packages/data/code/cljfx/src/cljfx/platform.clj",
                   :symbol .}
             Compiler.java: 7114  clojure.lang.Compiler/analyzeSeq
             Compiler.java: 6789  clojure.lang.Compiler/analyze
             Compiler.java: 6745  clojure.lang.Compiler/analyze
             Compiler.java: 6120  clojure.lang.Compiler$BodyExpr$Parser/parse
             Compiler.java: 5467  clojure.lang.Compiler$FnMethod/parse
             Compiler.java: 4029  clojure.lang.Compiler$FnExpr/parse
             Compiler.java: 7104  clojure.lang.Compiler/analyzeSeq
             Compiler.java: 6789  clojure.lang.Compiler/analyze
             Compiler.java: 7173  clojure.lang.Compiler/eval
             Compiler.java: 7131  clojure.lang.Compiler/eval
                  core.clj: 3214  clojure.core/eval
                  core.clj: 3210  clojure.core/eval
                  main.clj:  414  clojure.main/repl/read-eval-print/fn
                  main.clj:  414  clojure.main/repl/read-eval-print
                  main.clj:  435  clojure.main/repl/fn
                  main.clj:  435  clojure.main/repl
                  main.clj:  345  clojure.main/repl
               RestFn.java:  137  clojure.lang.RestFn/applyTo
                  core.clj:  665  clojure.core/apply
                  core.clj:  660  clojure.core/apply
                regrow.clj:   18  refactor-nrepl.ns.slam.hound.regrow/wrap-clojure-repl/fn
               RestFn.java: 1523  clojure.lang.RestFn/invoke
    interruptible_eval.clj:   79  nrepl.middleware.interruptible-eval/evaluate
    interruptible_eval.clj:   55  nrepl.middleware.interruptible-eval/evaluate
    interruptible_eval.clj:  142  nrepl.middleware.interruptible-eval/interruptible-eval/fn/fn
                  AFn.java:   22  clojure.lang.AFn/run
               session.clj:  171  nrepl.middleware.session/session-exec/main-loop/fn
               session.clj:  170  nrepl.middleware.session/session-exec/main-loop
                  AFn.java:   22  clojure.lang.AFn/run
               Thread.java:  748  java.lang.Thread/run

1. Caused by java.lang.IllegalArgumentException
   No matching method startup found taking 1 args for class
   javafx.application.Platform

             Compiler.java: 1677  clojure.lang.Compiler$StaticMethodExpr/<init>
             Compiler.java: 1022  clojure.lang.Compiler$HostExpr$Parser/parse
             Compiler.java: 7106  clojure.lang.Compiler/analyzeSeq
             Compiler.java: 6789  clojure.lang.Compiler/analyze
             Compiler.java: 6745  clojure.lang.Compiler/analyze
             Compiler.java: 6120  clojure.lang.Compiler$BodyExpr$Parser/parse
             Compiler.java: 5467  clojure.lang.Compiler$FnMethod/parse
             Compiler.java: 4029  clojure.lang.Compiler$FnExpr/parse
             Compiler.java: 7104  clojure.lang.Compiler/analyzeSeq
             Compiler.java: 6789  clojure.lang.Compiler/analyze
             Compiler.java: 7173  clojure.lang.Compiler/eval
             Compiler.java: 7131  clojure.lang.Compiler/eval
                  core.clj: 3214  clojure.core/eval
                  core.clj: 3210  clojure.core/eval
                  main.clj:  414  clojure.main/repl/read-eval-print/fn
                  main.clj:  414  clojure.main/repl/read-eval-print
                  main.clj:  435  clojure.main/repl/fn
                  main.clj:  435  clojure.main/repl
                  main.clj:  345  clojure.main/repl
               RestFn.java:  137  clojure.lang.RestFn/applyTo
                  core.clj:  665  clojure.core/apply
                  core.clj:  660  clojure.core/apply
                regrow.clj:   18  refactor-nrepl.ns.slam.hound.regrow/wrap-clojure-repl/fn
               RestFn.java: 1523  clojure.lang.RestFn/invoke
    interruptible_eval.clj:   79  nrepl.middleware.interruptible-eval/evaluate
    interruptible_eval.clj:   55  nrepl.middleware.interruptible-eval/evaluate
    interruptible_eval.clj:  142  nrepl.middleware.interruptible-eval/interruptible-eval/fn/fn
                  AFn.java:   22  clojure.lang.AFn/run
               session.clj:  171  nrepl.middleware.session/session-exec/main-loop/fn
               session.clj:  170  nrepl.middleware.session/session-exec/main-loop
                  AFn.java:   22  clojure.lang.AFn/run
               Thread.java:  748  java.lang.Thread/run

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.