├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── build ├── compile.clj └── version.clj ├── deps.edn ├── doc └── logo.png ├── examples └── e01_instant_restyling.clj ├── pom.xml ├── release.sh └── src └── cljfx └── css.clj /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: vlaaad 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /css.iml 3 | /classes 4 | /.cpcache 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 cljfx 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo](doc/logo.png) 2 | 3 | [![Cljdoc documentation](https://cljdoc.org/badge/cljfx/css)](https://cljdoc.org/jump/release/cljfx/css) 4 | [![Clojars Project](https://img.shields.io/clojars/v/cljfx/css.svg)](https://clojars.org/cljfx/css) 5 | 6 | Charmingly Simple Styling for [cljfx](https://github.com/cljfx/cljfx) 7 | 8 | # Rationale 9 | 10 | JavaFX is designed to use CSS files for styling. CSS has it's own set of problems such as 11 | selectors unexpectedly overriding each other and having unclear priority. Because of that, 12 | inline styles are more predictable and, with cljfx, where styles can be described as maps, 13 | also more composable. 14 | 15 | Unfortunately, CSS is unavoidable, because controls don't provide access to their internal 16 | nodes, and they can be targeted only with CSS selectors. What's worse, JavaFX does not 17 | allow loading CSS from strings or some other data structures, instead expecting an URL 18 | pointing to a CSS file. In addition to that, CSS is not always enough for styling JavaFX 19 | application: not every Node is styleable (for example, Shapes aren't). All this leads to a 20 | slow iteration cycle on styling and also to duplication of styling information in CSS and 21 | code. 22 | 23 | Charmingly Simple Styling is a library and a set of recommendations that solve these 24 | problems. Library provides a way to configure application style using clojure data 25 | structures and then construct special URLs to load CSS for styling JavaFX nodes that is 26 | derived from the same data structures. Recommendations help setup cljfx application in 27 | a way that allows you to rapidly iterate on styling in a live app and keep some sanity in 28 | the world of CSS. 29 | 30 | # Installation and requirements 31 | 32 | Latest version on Clojars: 33 | 34 | [![cljfx/css](https://clojars.org/cljfx/css/latest-version.svg)](https://clojars.org/cljfx/css) 35 | 36 | Charmingly Simple Styling does not depend on cljfx itself, so it can be used in any JavaFX 37 | application built with Clojure. 38 | 39 | # Library overview 40 | 41 | You want to create style description, both usable from code and loadable as CSS from URL. 42 | To achieve that, Charmingly Simple Styling extends JVM URLs with custom protocol — 43 | `cljfxcss` — that loads CSS from globally-registered style maps. CSS is generated by 44 | recursively concatenating all string keys in a style map to construct selectors, at the 45 | same time using keyword keys for associated selectors to construct rules. 46 | 47 | Let's see how it looks with this walk-through: 48 | ```clj 49 | (ns my-app.style 50 | (:require [cljfx.css :as css])) 51 | 52 | (def style 53 | (css/register ::style 54 | (let [padding 10 55 | text-color "#111111"] 56 | 57 | ;; you can put style settings that you need to access from code at keyword keys in a 58 | ;; style map and access them directly in an app 59 | 60 | {::padding padding 61 | ::text-color text-color 62 | 63 | ;; string key ".root" defines `.root` selector with these rules: `-fx-padding: 10;` 64 | 65 | ".root" {:-fx-padding padding} 66 | ".label" {:-fx-text-fill text-color 67 | :-fx-wrap-text true} 68 | ".button" {:-fx-text-fill text-color 69 | ;; vector values are space-separated 70 | :-fx-padding ["4px" "8px"] 71 | ;; nested string key defines new selector: `.button:hover` 72 | ":hover" {:-fx-text-fill :black}}}))) 73 | 74 | 75 | ;; `css/register` registers this style map globally so it can be loaded by URL, and puts 76 | ;; URL string in a style map at `:cljfx.css/url` key. 77 | 78 | style 79 | => {:my-app.style/padding 10, 80 | :my-app.style/text-color "#111111", 81 | ".root" {:-fx-padding 10}, 82 | ".label" {:-fx-text-fill "#111111", :-fx-wrap-text true}, 83 | ".button" {:-fx-text-fill "#111111", 84 | :-fx-padding ["4px" "8px"], 85 | ":hover" {:-fx-text-fill :black}}, 86 | 87 | ;; URL has stringified version of keyword in query part of URL, and a hash of a style 88 | ;; map in a fragment part. Query part is used to lookup style map in a global 89 | ;; registry, and fragment is used to indicate that style is changed when it's 90 | ;; redefined to trigger CSS reload in JavaFX 91 | 92 | :cljfx.css/url "cljfxcss:?my-app.style/style#-1561130535"} 93 | 94 | 95 | ;; let's see how loaded CSS looks like: 96 | 97 | (println (slurp (::css/url style))) 98 | 99 | ;; prints: 100 | ;; .root { 101 | ;; -fx-padding: 10; 102 | ;; } 103 | ;; .label { 104 | ;; -fx-text-fill: #111111; 105 | ;; -fx-wrap-text: true; 106 | ;; } 107 | ;; .button { 108 | ;; -fx-text-fill: #111111; 109 | ;; -fx-padding: 4px 8px; 110 | ;; } 111 | ;; .button:hover { 112 | ;; -fx-text-fill: black; 113 | ;; } 114 | 115 | 116 | ;; Later, in app description: 117 | 118 | {:fx/type :stage 119 | :showing true 120 | :scene {:fx/type :scene 121 | :stylesheets [(::css/url style)] 122 | :root ...}} 123 | ``` 124 | 125 | That's it: you define styles, register them and feed constructed URL to JavaFX. 126 | 127 | # Recommendations 128 | 129 | ## Watch for changes while iterating on styles 130 | 131 | Usually styles are static during the application runtime, but when you develop application 132 | styling, it's very important to see your changes immediately. To achieve that with 133 | Charmingly Simple Styling, you need to take 2 steps: 134 | - put registered style into application state, so re-registered style can be picked up on 135 | next render; 136 | - watch for changes in registered style and update it in app state. 137 | 138 | When putting style in an app state, it might be useful to also put it into component 139 | environment with `fx/ext-set-env`, so you can access it easily. See 140 | [`ext-set-env`/`ext-get-env` section](https://github.com/cljfx/cljfx#extending-cljfx) in 141 | cljfx's manual. 142 | 143 | When you keep style `def`ed in a Var, you can just add a watch to that var that updates 144 | style in an app state to achieve instant reload. 145 | See [example](examples/e01_instant_restyling.clj) — it contains a style definition and 146 | a rich comment that you can use to start and stop watching for changes in style to 147 | instantly reapply styles in an app. 148 | 149 | There are also 2 resources I found invaluable while iterating on application styling: 150 | - Official JavaFX [CSS reference](https://openjfx.io/javadoc/12/javafx.graphics/javafx/scene/doc-files/cssref.html) — 151 | to see what you can style with CSS 152 | - [modena.css](https://gist.github.com/maxd/63691840fc372f22f470) — default CSS used by 153 | JavaFX, helpful when documentation is not enough 154 | 155 | ## Don't rely on priority rules 156 | 157 | CSS has confusing priority rules, which, when relied upon, usually results in CSS files 158 | becoming append only with more and more overrides. In Charmingly Simple Styling, on the 159 | other hand, style maps are unordered, which means resulted CSS selectors are emitted in 160 | undefined order. That's made intentionally to promote a more reasonable approach: create 161 | different CSS classes for different purposes and then switch between them. 162 | 163 | For example, instead of this: 164 | ```clojure 165 | ;; BAD! 166 | 167 | ;; style map: 168 | {".notification" {:-fx-background-color :black 169 | "> .label" {:-fx-text-fill :gray}} 170 | ".danger > .label" {:-fx-text-fill :red}} 171 | 172 | ;; component: 173 | (defn notification [{:keys [text variant] 174 | :or {variant "info"}}] 175 | {:fx/type :v-box 176 | :style-class ["notification" variant] 177 | :children [{:fx/type :label 178 | :text text}]}) 179 | ``` 180 | You should use this: 181 | ```clojure 182 | ;; GOOD! 183 | 184 | ;; style map: 185 | {".notification" {:-fx-background-color :black 186 | "-label" {"-info" {:-fx-text-fill :gray} 187 | "-danger" {:-fx-text-fill :red}}}} 188 | 189 | ;; component: 190 | (defn notification [{:keys [text variant] 191 | :or {variant "info"}}] 192 | {:fx/type :v-box 193 | :style-class "notification" 194 | :children [{:fx/type :label 195 | :style-class (str "notification-label-" variant) 196 | :text text}]}) 197 | ``` 198 | 199 | ## Be careful with indirect children CSS selector 200 | 201 | Some selectors are very easy and straightforward to write using style maps: 202 | ```clojure 203 | {".style-class" {:-fx-background-color :red 204 | "> .direct-child" {:-fx-text-fill :green 205 | ":pseudo-class" {:-fx-text-fill :blue}}}} 206 | ``` 207 | There is another type of selectors that looks ugly written that way: 208 | ```clojure 209 | {".style-class" {:-fx-background-color :red 210 | ;; ugly string starting with space, boo! 211 | " .indirect-child" {:-fx-text-fill :green}}} 212 | ``` 213 | This should serve as a reminder that such selectors are bad for application performance, 214 | since JavaFX has to look through all parents of a every Node with class `indirect-child` 215 | to see if it has `style-class` class to figure out if selector applies. As JavaFX's wiki 216 | states on it's [Performance Tips and Tricks](https://wiki.openjdk.java.net/display/OpenJFX/Performance+Tips+and+Tricks) 217 | page, you should follow these rules when doing CSS: 218 | - Avoid selectors that have to match against the entire set of parents 219 | - Use stylesheets not setStyles 220 | - Use pseudo-class state, not multiple style classes, for state-based styles 221 | 222 | ## Prefer custom style classes 223 | 224 | It might be tempting to use `label` class so it's applied automatically to all labels 225 | without a need to specify their style class. Unfortunately, it means that you will have to 226 | fight with default styles from modena.css, because it also targets `label` class. I 227 | think styling is more straightforward when you set your own style class on labels and 228 | don't have to worry about disabling unexpected insets or paddings. 229 | 230 | Alternatively, you can set CSS url globally with `Application/setUserAgentStylesheet`, but 231 | that means you'll have to provide CSS for every element in an app. 232 | -------------------------------------------------------------------------------- /build/compile.clj: -------------------------------------------------------------------------------- 1 | (ns compile 2 | (:require [clojure.java.io :as io])) 3 | 4 | (defn -main [] 5 | (run! io/delete-file (reverse (rest (file-seq (io/file "classes"))))) 6 | (.mkdir (io/file "classes")) 7 | (compile 'cljfx.css) 8 | (println "Compiled" 'cljfx.css)) 9 | -------------------------------------------------------------------------------- /build/version.clj: -------------------------------------------------------------------------------- 1 | (ns version 2 | (:require [clojure.data.xml :as xml] 3 | [clojure.zip :as zip] 4 | [clojure.java.io :as io])) 5 | 6 | (xml/alias-uri 'pom "http://maven.apache.org/POM/4.0.0") 7 | 8 | (defn- make-xml-element 9 | [{:keys [tag attrs] :as node} children] 10 | (with-meta 11 | (apply xml/element tag attrs children) 12 | (meta node))) 13 | 14 | (defn- xml-update 15 | [root tag-path replace-node] 16 | (let [z (zip/zipper xml/element? :content make-xml-element root)] 17 | (zip/root 18 | (loop [[tag & more-tags :as tags] tag-path 19 | parent z 20 | child (zip/down z)] 21 | (if child 22 | (if (= tag (:tag (zip/node child))) 23 | (if (seq more-tags) 24 | (recur more-tags child (zip/down child)) 25 | (zip/edit child (constantly replace-node))) 26 | (recur tags parent (zip/right child))) 27 | (zip/append-child parent replace-node)))))) 28 | 29 | (defn -main [& version] 30 | (with-open [reader (io/reader "pom.xml")] 31 | (let [xml (-> reader 32 | (xml/parse :skip-whitespace true) 33 | (xml-update [::pom/version] (xml/sexp-as-element [::pom/version version])) 34 | (xml-update [::pom/scm ::pom/tag] (xml/sexp-as-element [::pom/tag version])))] 35 | (with-open [writer (io/writer "pom.xml")] 36 | (xml/indent xml writer))))) -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "classes"] 2 | :aliases {:build {:extra-paths ["build"] 3 | :jvm-opts ["-Dclojure.spec.skip-macros=true"] 4 | :extra-deps {org.clojure/data.xml {:mvn/version "0.2.0-alpha6"}}} 5 | :examples {:extra-paths ["examples"] 6 | :extra-deps {cljfx {:mvn/version "1.6.0"}}} 7 | :compile {:main-opts ["-m" "compile"]} 8 | :depstar {:extra-deps {seancorfield/depstar {:mvn/version "0.2.1"}} 9 | :main-opts ["-m" "hf.depstar.jar"]} 10 | :deploy {:extra-deps {deps-deploy {:mvn/version "0.0.9"}} 11 | :main-opts ["-m" "deps-deploy.deps-deploy" "deploy"]}} 12 | :deps {}} 13 | -------------------------------------------------------------------------------- /doc/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cljfx/css/ccac52faf1dafc7d43d218da13a0f31c0696a580/doc/logo.png -------------------------------------------------------------------------------- /examples/e01_instant_restyling.clj: -------------------------------------------------------------------------------- 1 | (ns e01-instant-restyling 2 | (:require [cljfx.css :as css] 3 | [clojure.pprint :as pprint] 4 | [cljfx.api :as fx])) 5 | 6 | (def style 7 | (css/register ::style 8 | (let [base-color "#222" 9 | style {:app.style/text-color base-color 10 | :app.style/help-color (str base-color "8") 11 | :app.style/border-color (str base-color "4") 12 | :app.style/shadow-color (str base-color "3") 13 | :app.style/focus-color (str base-color "8") 14 | :app.style/control-color "#fff" 15 | :app.style/control-hover-color "#f4f4f4" 16 | :app.style/background-color "#eee" 17 | :app.style/spacing 10 18 | :app.style/scroll-bar-size 9 19 | :app.style/padding 20 20 | :app.style/corner-size 5 21 | :app.style/label-padding "2px 4px"} 22 | text (fn [size weight] 23 | {:-fx-text-fill (:app.style/text-color style) 24 | :-fx-wrap-text true 25 | :-fx-font-weight weight 26 | :-fx-font-size size}) 27 | control-shadow (format "dropshadow(gaussian, %s, 5, 0, 0, 1)" 28 | (:app.style/shadow-color style)) 29 | inner-shadow (format "innershadow(gaussian, %s, 5, 0, 0, 2)" 30 | (:app.style/shadow-color style)) 31 | hover-shadow (format "dropshadow(gaussian, %s, 7, 0, 0, 2)" 32 | (:app.style/shadow-color style)) 33 | armed-shadow (format "dropshadow(gaussian, %s, 3, 0, 0, 1)" 34 | (:app.style/shadow-color style)) 35 | border {:-fx-border-color (:app.style/border-color style) 36 | :-fx-background-color (:app.style/control-color style) 37 | :-fx-border-radius (:app.style/corner-size style) 38 | :-fx-background-radius (:app.style/corner-size style)} 39 | button (merge 40 | (text 13 :normal) 41 | border 42 | {:-fx-padding (:app.style/label-padding style) 43 | :-fx-effect control-shadow 44 | ":focused" {:-fx-border-color (:app.style/focus-color style)} 45 | ":hover" {:-fx-effect hover-shadow 46 | :-fx-background-color (:app.style/control-hover-color style)} 47 | ":armed" {:-fx-effect armed-shadow}})] 48 | (merge 49 | style 50 | {".app" {"-label" (text 13 :normal) 51 | "-header" (text 20 :bold) 52 | "-sub-header" (text 16 :bold) 53 | "-code" (merge 54 | (text 13 :normal) 55 | {:-fx-font-family "monospace" 56 | :-fx-padding (:app.style/spacing style)}) 57 | "-container" {:-fx-spacing (:app.style/spacing style)} 58 | "-root" {:-fx-padding (:app.style/padding style) 59 | :-fx-background-color (:app.style/background-color style)} 60 | "-button" {"-primary" button 61 | "-secondary" button} 62 | "-check-box" {:-fx-text-fill (:app.style/text-color style) 63 | :-fx-label-padding (format "0 0 0 %spx" 64 | (:app.style/spacing style)) 65 | ":focused > .box" {:-fx-border-color (:app.style/focus-color style)} 66 | ":hover > . box" {:-fx-effect hover-shadow 67 | :-fx-background-color (:app.style/control-hover-color style)} 68 | ":armed > .box" {:-fx-effect armed-shadow} 69 | "> .box" (merge 70 | border 71 | {:-fx-effect control-shadow 72 | :-fx-padding "3px 2px" 73 | "> .mark" {:-fx-padding "5px 6px" 74 | :-fx-shape "'M7.629,14.566c0.125,0.125,0.291,0.188,0.456,0.188c0.164,0,0.329-0.062,0.456-0.188l8.219-8.221c0.252-0.252,0.252-0.659,0-0.911c-0.252-0.252-0.659-0.252-0.911,0l-7.764,7.763L4.152,9.267c-0.252-0.251-0.66-0.251-0.911,0c-0.252,0.252-0.252,0.66,0,0.911L7.629,14.566z'"}}) 75 | ":selected > .box > .mark" {:-fx-background-color (:app.style/text-color style)}} 76 | "-text-field" (merge 77 | (text 13 :normal) 78 | border 79 | {:-fx-highlight-fill (:app.style/text-color style) 80 | :-fx-padding (:app.style/label-padding style) 81 | :-fx-prompt-text-fill (:app.style/help-color style) 82 | :-fx-highlight-text-fill (:app.style/background-color style) 83 | :-fx-effect inner-shadow 84 | ":focused" {:-fx-border-color (:app.style/focus-color style)}})} 85 | ".scroll-pane" (merge 86 | border 87 | {:-fx-effect inner-shadow 88 | :-fx-focus-traversable true 89 | ":focused" {:-fx-border-color (:app.style/focus-color style) 90 | :-fx-background-insets 0} 91 | "> .viewport" {:-fx-background-color (:app.style/control-color style)} 92 | "> .corner" {:-fx-background-color :transparent}}) 93 | ".scroll-bar" {:-fx-background-color :transparent 94 | "> .thumb" {:-fx-background-color (:app.style/focus-color style) 95 | :-fx-background-radius (:app.style/scroll-bar-size style) 96 | :-fx-background-insets 1 97 | ":pressed" {:-fx-background-color (:app.style/text-color style)}} 98 | ":horizontal" {"> .increment-button > .increment-arrow" {:-fx-pref-height (:app.style/scroll-bar-size style)} 99 | "> .decrement-button > .decrement-arrow" {:-fx-pref-height (:app.style/scroll-bar-size style)}} 100 | ":vertical" {"> .increment-button > .increment-arrow" {:-fx-pref-width (:app.style/scroll-bar-size style)} 101 | "> .decrement-button > .decrement-arrow" {:-fx-pref-width (:app.style/scroll-bar-size style)}} 102 | "> .decrement-button" {:-fx-padding 0 103 | "> .decrement-arrow" {:-fx-shape nil 104 | :-fx-padding 0}} 105 | "> .increment-button" {:-fx-padding 0 106 | "> .increment-arrow" {:-fx-shape nil 107 | :-fx-padding 0}}}})))) 108 | 109 | (def *state 110 | (atom {:check-box true 111 | :style style})) 112 | 113 | (comment 114 | ;; to iterate during development on style, add a watch to var that updates style in app 115 | ;; state... 116 | (add-watch #'style :refresh-app (fn [_ _ _ _] (swap! *state assoc :style style))) 117 | ;; ... and remove it when you are done 118 | (remove-watch #'style :refresh-app)) 119 | 120 | (def renderer 121 | (fx/create-renderer 122 | :middleware 123 | (fx/wrap-map-desc 124 | (fn [{:keys [check-box style]}] 125 | {:fx/type :stage 126 | :showing true 127 | :width 900 128 | :height 600 129 | :scene {:fx/type :scene 130 | :stylesheets [(::css/url style)] 131 | :root {:fx/type :v-box 132 | :style-class ["app-root" "app-container"] 133 | :children [{:fx/type :label 134 | :style-class "app-header" 135 | :text "Header"} 136 | {:fx/type :label 137 | :style-class "app-label" 138 | :text "label with some text"} 139 | {:fx/type :h-box 140 | :style-class "app-container" 141 | :children [{:fx/type :button 142 | :style-class "app-button-primary" 143 | :text "First Button"} 144 | {:fx/type :button 145 | :style-class "app-button-secondary" 146 | :text "Second Button"}]} 147 | {:fx/type :text-field 148 | :style-class "app-text-field" 149 | :prompt-text "type here something"} 150 | {:fx/type :label 151 | :style-class "app-sub-header" 152 | :text "css"} 153 | {:fx/type :check-box 154 | :style-class "app-check-box" 155 | :text "As text" 156 | :selected check-box 157 | :on-selected-changed #(swap! *state assoc :check-box %)} 158 | {:fx/type :scroll-pane 159 | :content {:fx/type :label 160 | :style-class "app-code" 161 | :text (with-out-str 162 | (if check-box 163 | (println (slurp (::css/url style))) 164 | (pprint/pprint style)))}}]}}})))) 165 | 166 | (fx/mount-renderer *state renderer) 167 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | cljfx 5 | css 6 | 1.1.0 7 | css 8 | 9 | 10 | org.clojure 11 | clojure 12 | 1.10.1 13 | 14 | 15 | 16 | src 17 | 18 | 19 | 20 | https://github.com/cljfx/css 21 | 1.1.0 22 | 23 | 24 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | if [ -z "$1" ] 4 | then 5 | echo "No version supplied" 6 | exit 1 7 | fi 8 | clj -A:build -m compile 9 | clj -A:build -m version "$1" 10 | clj -Spom 11 | git commit -am "Release $1" 12 | git tag "$1" 13 | git push 14 | git push origin "$1" 15 | clj -A:depstar css.jar 16 | printf "Clojars Username: " 17 | read -r username 18 | stty -echo 19 | printf "Clojars Password: " 20 | read -r password 21 | printf "\n" 22 | stty echo 23 | CLOJARS_USERNAME=${username} CLOJARS_PASSWORD=${password} clj -A:deploy css.jar 24 | rm css.jar -------------------------------------------------------------------------------- /src/cljfx/css.clj: -------------------------------------------------------------------------------- 1 | (ns cljfx.css 2 | "Charmingly Simple Styling library allows using clojure data structures to define 3 | application styles. 4 | 5 | It adds JVM-wide support for `cljfxcss` URL protocol that can then be used to retrieve 6 | registered style maps as CSS" 7 | (:require [clojure.string :as str]) 8 | (:import [clojure.lang Named] 9 | [java.net URL URLConnection] 10 | [java.io ByteArrayInputStream]) 11 | (:gen-class :name cljfx.css.cljfxcss.Handler 12 | :extends java.net.URLStreamHandler)) 13 | 14 | (-> (System/getProperty "java.protocol.handler.pkgs" "") 15 | (str/split #"[|]") 16 | (conj "cljfx.css") 17 | (distinct) 18 | (->> (str/join "|") 19 | (System/setProperty "java.protocol.handler.pkgs"))) 20 | 21 | (set! *warn-on-reflection* true) 22 | 23 | (def ^:private *registry (atom {})) 24 | 25 | (defn- write-val [^StringBuilder acc x] 26 | (cond 27 | (instance? Named x) (.append acc (name x)) 28 | (vector? x) (transduce (interpose " ") (completing #(write-val acc %2)) nil x) 29 | (sequential? x) (run! #(write-val acc %) x) 30 | :else (.append acc x))) 31 | 32 | (defn- write [^StringBuilder acc path m] 33 | (doseq [[k v] m 34 | :when (string? k) 35 | :let [path (str path k)]] 36 | (.append acc path) 37 | (.append acc " {\n") 38 | (doseq [[sub-k sub-v] v 39 | :when (keyword? sub-k)] 40 | (.append acc " ") 41 | (.append acc (name sub-k)) 42 | (.append acc ": ") 43 | (write-val acc sub-v) 44 | (.append acc ";\n")) 45 | (.append acc "}\n") 46 | (write acc path v))) 47 | 48 | (defn- -openConnection [this ^URL url] 49 | (proxy [URLConnection] [url] 50 | (getInputStream [] 51 | (let [style (get @*registry (keyword (.getQuery url))) 52 | acc (StringBuilder.)] 53 | (write acc "" style) 54 | (ByteArrayInputStream. (.getBytes (.toString acc) "UTF-8")))))) 55 | 56 | (defn register 57 | "Globally register style map describing CSS with associated keyword identifier 58 | 59 | Returns a map with additional key `:cljfx.css/url` containing URL string pointing to CSS 60 | derived from style map 61 | 62 | CSS is created by recursively concatenating string keys starting from root to define 63 | selectors, while using keyword keys in value maps to define rules, for example: 64 | ``` 65 | {\".button\" {:-fx-text-fill \"#ccc\" 66 | \":hover\" {:-fx-text-fill \"#aaa\"}}} 67 | ;; corresponds to this css: 68 | .button { 69 | -fx-text-fill: #ccc; 70 | } 71 | .button:hover { 72 | -fx-text-fill: #aaa; 73 | } 74 | ```" 75 | [id m] 76 | (let [css (assoc m ::url (str "cljfxcss:?" (symbol id) "#" (hash m)))] 77 | (swap! *registry assoc id css) 78 | css)) 79 | --------------------------------------------------------------------------------