├── .gitignore ├── README.md ├── info.txt ├── lib └── clojure-1.3.0.jar ├── pom.xml ├── project.clj ├── src └── webfui │ ├── core.cljs │ ├── dom.cljs │ ├── dom_manipulation.cljs │ ├── framework.cljs │ ├── framework │ └── macros.clj │ ├── html.cljs │ ├── macros.clj │ ├── plugin │ ├── core.cljs │ ├── form_values.cljs │ ├── mouse.cljs │ └── scrolling.cljs │ ├── state_patches.cljs │ └── utilities.cljs ├── target └── stale │ └── dependencies ├── test └── webfui │ └── test │ └── core.clj └── webfui-examples ├── .gitignore ├── .lein-plugins └── checksum ├── README.md ├── project.clj ├── resources └── public │ ├── css │ ├── POCKC___.eot │ ├── POCKC___.svg │ ├── POCKC___.ttf │ ├── POCKC___.woff │ ├── add_two_numbers.css │ ├── add_two_numbers_low_level.css │ ├── calculator.css │ ├── calculator_ajax.css │ ├── calculator_many.css │ ├── inverse_kinematics.css │ ├── mouse_tracking.css │ ├── scrolling.css │ └── webfui_examples.css │ ├── index.html │ └── js │ └── .gitignore ├── src-clj └── webfui_examples │ ├── server.clj │ └── views │ ├── add_two_numbers.clj │ ├── add_two_numbers_low_level.clj │ ├── calculator.clj │ ├── calculator_ajax.clj │ ├── calculator_many.clj │ ├── common.clj │ ├── inverse_kinematics.clj │ ├── mouse_tracking.clj │ ├── scrolling.clj │ └── welcome.clj └── src-cljs ├── add_two_numbers └── core.cljs ├── add_two_numbers_low_level └── core.cljs ├── calculator └── core.cljs ├── calculator_ajax └── core.cljs ├── calculator_many └── core.cljs ├── inverse_kinematics └── core.cljs ├── mouse_tracking └── core.cljs └── scrolling └── core.cljs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebFUI - Client Side Web Development Framework For ClojureScript 2 | 3 | ![logo](http://lisperati.com/webfui/logo.png) 4 | 5 | ## Philosophy of WebFUI 6 | 7 | The goal of WebFUI is to let you do client-side Web programming in ClojureScript without having to ever deal with the DOM. Instead, all DOM is generated in realtime from a Clojure atom that contains just "plain old data" (called [EDN](https://github.com/edn-format/edn) in Clojure.) This "DOM EDN" is kept synchronized with a state atom that also contains EDN, where all the state for your program is kept. 8 | 9 | You, the programmer, is only responsible for providing the functions shown in red in the picture below: 10 | 11 | ![graph](http://lisperati.com/webfui/graph.png) 12 | 13 | These functions in red can be written 100% in the functional style, which is one of the benefits of WebFUI. Also, having EDN as the arguments/results for all your app functions makes debugging/unit testing extremely easy. Finally, WebFUI code is extremely succinct- [This fully-featured calculator app](http://lisperati.com/webfui/calculator.html) consists of [only 92 lines of code](https://github.com/drcode/webfui/blob/master/webfui-examples/src-cljs/calculator/core.cljs), which includes all html generation! 14 | 15 | *Note: WebFUI is still an alpha project and has limitations. Currently it only supports only Webkit based browsers (Chrome, Safari, including on Android/iOS).* 16 | 17 | ## Tech Demo Showing Off WebFUI 18 | 19 | Here is an [inverse kinematics demo written 100% in ClojureScript](http://lisperati.com/webfui/inverse_kinematics.html)- Browse the source code in the `webfui-examples` directory. This example is pure HTML5, and using WebFUI means none of the application code directly manipulates the DOM. Click on the figure to drag it around the screen. 20 | For further demos, check out this [calculator](http://lisperati.com/webfui/calculator.html) and [this example of mouse interaction](http://lisperati.com/webfui/mouse_tracking.html) (drag numbers in the circle on top of each other to see what it does.) 21 | 22 | ## Performance Challenges 23 | 24 | One of the key design choices in WebFUI is that the DOM and the program state are strongly linked via a super simple mechanism: *Any* change to the state updates *all* the DOM. Similarly *any* changes to the DOM (Such as by the user entering text into a form field) updates *all* the state. This makes your code very simple, but can affect performance of your app. 25 | 26 | However, WebFUI mitigates the inefficiency of this two-way synchronization of state and the DOM in various ways. First of all, ClojureScript has highly-efficient persistent data structures that WebFUI can use to minimize these performance penalties. Also, WebFUI performs delta calculations during DOM changes to send as few DOM updates as possible to the web browser. Luckily, all the browser vendors are currently in a javascript performance arms race, making this less of a problem over time. 27 | 28 | Because of these optimizations, WebFUI has reasonable performance in most real-world applications (use the examples included with webfui as a guide to judge app responsiveness.) However, on some devices, such as older iPad/iPhones, Javascript is still relatively slow, and you'll notice some delays in a WebFUI-based HTML5 app. 29 | 30 | ## Installing WebFUI 31 | 32 | WebFUI is best installed by using [leiningen](https://github.com/technomancy/leiningen). Include the following in the dependencies of your project.clj: 33 | 34 | ```clojure 35 | :dependencies [[webfui "0.2.1"]] 36 | ``` 37 | 38 | Your application needs to be a ClojureScript application, and I recommend you use the [lein-cljsbuild plugin](https://github.com/emezeske/lein-cljsbuild) for leiningen to build it. 39 | 40 | ## Compiling the Examples 41 | 42 | In the webfui-examples directory you'll see examples of webfui in action. Just run `lein deps;lein cljsbuild once;lein run` and fire up your Safari, Chrome, or iOS browser to "localhost:8080". 43 | 44 | ## A Simple WebFUI App 45 | 46 | Here is an entire concrete example program using WebFUI. It displays two edit fields and displays the sum of the numbers entered into those fields as a result (try it [here](http://lisperati.com/webfui/add_two_numbers.html)) 47 | 48 | ```clojure 49 | (ns webfui-examples.add-two-numbers.core 50 | (:use [webfui.framework :only [launch-app]]) 51 | (:use-macros [webfui.framework.macros :only [add-dom-watch]])) 52 | 53 | (defn render-all [state]❶ 54 | (let [{:keys [a b]} state] 55 | [:div [:input#a {:watch :watch :value a}]❷ 56 | " plus " 57 | [:input#b {:watch :watch :value b}]❸ 58 | [:p " equals "] 59 | [:span (+ a b)]])) 60 | 61 | (defn valid-integer [s]❹ 62 | (and (< (count s) 15) (re-matches #"^[0-9]+$" s))) 63 | 64 | (add-dom-watch :watch [state new-element]❺ 65 | (let [{:keys [value id]} (second new-element)] 66 | (when (valid-integer value) 67 | {id (js/parseInt value)})))❻ 68 | 69 | (launch-app (atom {:a 0 :b 0}) render-all)❼ 70 | ``` 71 | 72 | ## How State Changes Make it to the DOM 73 | 74 | We initialize a WebFUI app by calling `launch-app` ❼ and supplying a state atom and the `render-all` ❶ function. In this example, the state consists only of the values in the edit fields named `A` and `B`. As you can see, the `render-all` function just transforms the state into HTML. (using the same syntax used by [hiccup](https://github.com/weavejester/hiccup).) 75 | 76 | ## How DOM Changes Make it to the State 77 | 78 | This happens by attaching *DOM watchers* to the DOM. You can see right here ❷ and here ❸ that there is an attribute called `watch` attached to the generated html (WebFUI html can have special attributes that goes beyond standard html.) This will link the edit controls to a DOM watcher declared here ❺. The DOM watcher is responsible for updating the state to adjust to changes in the DOM: In this case, it means to update the A and B fields in the state (which it does by looking up the ID of the text field which is the same as the key in the state ❻) if the numbers entered in by the user into either text field changes. 79 | 80 | Note that the DOM watcher checks whether the fields contain valid integers with this function ❹. Because of the realtime nature of WebFUI you'll see that this automagically prevents a user from entering non-numeric values into the text fields. 81 | 82 | These DOM watchers are fundamentally different than javascript "on__" events in that they use a "Show, don't tell" design: You don't get an *event object* _telling_ you what changed, but instead get a concrete look at the proposed changes to the HTML via `new-element`❺ (which can be compared to the `state` to see what was there before) that _shows_ you what changed. 83 | 84 | ## Pinpoint Changes to the State 85 | 86 | As we've seen, in WebFUI the DOM is updated in a wholesale fashion from the program state, and the program state is updated wholesale from the DOM, as well. However, usually when we update our program state we want to do it in a pinpoint fashion. Because of this, the state returned from a DOM watcher is actually a *delta* applied to the program state: If a key is left unmentioned, it is left with its previous value, in a recursive fashion. To understand more about the model used for updating state using diffs, read the section on `webfui.state-patch` in [this document](https://docs.google.com/document/d/1KQn_saQurqgvxHiyuOZ7twK4K_w_VftBHrPKJolwEZ8/edit). 87 | 88 | ## WebFUI Plugins 89 | 90 | WebFUI has a plugin mechanism for adding support for different types of user interaction. Plugins are used to add support for mouse actions, calculating scroll bar positions, and many more plugins are planned for the future. 91 | 92 | [Read the following document to learn more about the design of WebFUI and the plugin architecture.](https://docs.google.com/document/d/1KQn_saQurqgvxHiyuOZ7twK4K_w_VftBHrPKJolwEZ8/edit) 93 | 94 | ## Mouse Support/Further Documentation 95 | 96 | To learn how to handle mouse interactions in WebFUI, please check the section on mouse support in [this document](https://docs.google.com/document/d/1KQn_saQurqgvxHiyuOZ7twK4K_w_VftBHrPKJolwEZ8/edit). 97 | 98 | ## License 99 | 100 | Distributed under the Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php), the same as Clojure. 101 | -------------------------------------------------------------------------------- /info.txt: -------------------------------------------------------------------------------- 1 | *presentation notes 2 | 3 | What is the DOM? 4 | 5 | Problems with the DOM 6 | - It changes 7 | - Changing it is slow 8 | 9 | What is EDN? 10 | 11 | What is AJAX? 12 | 13 | How to build a web framework: 14 | - Should do things the Clojure Way 15 | - Code should be in the functional style 16 | - The browser DOM should be EDN data stored in an atom 17 | 18 | DOM is not a complete representation of browser state 19 | - Can't set button as pushed 20 | - Can't set scrollbar position 21 | - Can't set text selection 22 | 23 | Problem: AJAX requires side effects. Reframed problem: Our state atom no longer contains the whole world 24 | 25 | Original sin: Our state is _always_ corrupt in a program that uses AJAX. 26 | 27 | We have to contain the corruption (Can be done with FP) 28 | We have to repair the corruption (Requires imperative programming) 29 | 30 | "State Corruption" Design Pattern 31 | 32 | AJAX=Corruption of state 33 | 34 | Corruption is implicit- Make it explicit 35 | 36 | GET: State item has value of nil 37 | or selection variable has id that points to nonexistent item 38 | 39 | POST: Create new item in state with id=nil 40 | 41 | PUT: Have "holding area" in state, set source of truth=nil. Move from holding area back to final area 42 | 43 | DELETE: Just delete item 44 | 45 | *todos 46 | item insert/deletion in delta system 47 | text selection 48 | full IK artwork 49 | "preview" in calculator 50 | 51 | *Hello World 52 | (ns my-app.core 53 | (:use [webfui.framework :only [launch-app]])) 54 | 55 | (defn render-all [] 56 | "hello world!") 57 | 58 | (launch-app (atom nil) render-all) 59 | 60 | 61 | 62 | *timing 63 | ~250 for mouseup 64 | ~150 for webfui code 65 | ~90 for parsing html 66 | ~90 for delta detection 67 | -------------------------------------------------------------------------------- /lib/clojure-1.3.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drcode/webfui/e8ba6e9224542755e5780488cb43f0c5510827f9/lib/clojure-1.3.0.jar -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | webfui 6 | webfui 7 | 0.2.1 8 | webfui 9 | Alpha release of Webfui- All Webfui source included in this repository release under Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) 10 | http://lisperati.com 11 | 12 | scm:git:git://github.com/drcode/webfui.git 13 | scm:git:ssh://git@github.com/drcode/webfui.git 14 | aba4347092761dfdcaa5605b807c23fa458da41b 15 | https://github.com/drcode/webfui 16 | 17 | 18 | src 19 | test 20 | 21 | 22 | resources 23 | 24 | 25 | 26 | 27 | test-resources 28 | 29 | 30 | 31 | 32 | 33 | central 34 | http://repo1.maven.org/maven2 35 | 36 | 37 | clojars 38 | http://clojars.org/repo/ 39 | 40 | 41 | 42 | 43 | 47 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject webfui "0.2.1" 2 | :description "Alpha release of Webfui- All Webfui source included in this repository release under Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)" 3 | :dev-dependencies [[lein-autodoc "0.9.0"]] 4 | :url "http://lisperati.com") -------------------------------------------------------------------------------- /src/webfui/core.cljs: -------------------------------------------------------------------------------- 1 | (ns webfui.core 2 | (:use [webfui.html :only [html parse-html parsed-html render-attribute-value html-delta]] 3 | [webfui.plugin.core :only [active-plugins Plugin declare-events fix-dom]] 4 | [cljs.reader :only [read-string]])) 5 | 6 | ;; This file contains the core engine for webfui. You usually don't want to load this file directly, instead load webfui.dom or webfui.framework. 7 | 8 | (defn body [] 9 | (.-body js/document)) 10 | 11 | (defn select-path-dom [node path] 12 | (if-let [[cur & more] (seq path)] 13 | (recur (.item (.-childNodes node) cur) more) 14 | node)) 15 | 16 | (defn dom-ready? [] 17 | (= (.-readyState js/document) "complete")) 18 | 19 | (defn dom-ready [fun] 20 | (set! (.-onload js/window) fun)) 21 | 22 | (defn parsed-html-watcher [key a old new] 23 | (let [delta (html-delta (.-children old) (.-children new) [])] 24 | (doseq [[typ path a b] delta] 25 | (let [node (select-path-dom (body) path)] 26 | (case typ 27 | :att (if (= a :value) 28 | (set! (.-value node) (str b)) 29 | (.setAttribute node (name a) (render-attribute-value a b))) 30 | :rem-att (.removeAttribute node (name a)) 31 | :html (set! (.-innerHTML node) (apply str (map html a)))))) 32 | (doseq [plugin @active-plugins] 33 | (fix-dom plugin)))) 34 | 35 | (def parsed-html-atom (atom (parsed-html. :body {} nil))) 36 | 37 | (defn update-parsed-html-atom [new old] 38 | (parsed-html. :body 39 | {} 40 | (if (or (seq? new) (list? new)) 41 | (parse-html new) 42 | (parse-html (list new))))) 43 | 44 | (defn html-watcher [key a old new] 45 | (swap! parsed-html-atom (partial update-parsed-html-atom new))) 46 | 47 | (def dom-watchers (atom {})) 48 | 49 | (defn core-add-dom-watch [id fun] 50 | (swap! dom-watchers assoc id fun)) 51 | 52 | (defn init-dom [html] 53 | (let [b (body)] 54 | (doseq [plugin @active-plugins] 55 | (declare-events plugin (body) dom-watchers parsed-html-atom)) 56 | (add-watch html :dom-watcher html-watcher) 57 | (add-watch parsed-html-atom :parsed-html-watcher parsed-html-watcher) 58 | (swap! html identity))) 59 | 60 | (defn core-defdom [clj-dom] 61 | (if (dom-ready?) 62 | (init-dom clj-dom) 63 | (dom-ready (partial init-dom clj-dom)))) 64 | -------------------------------------------------------------------------------- /src/webfui/dom.cljs: -------------------------------------------------------------------------------- 1 | (ns webfui.dom 2 | (:use [webfui.core :only [core-add-dom-watch core-defdom]] 3 | [webfui.plugin.core :only [register-plugin]] 4 | [webfui.plugin.form-values :only [form-values]])) 5 | 6 | (def add-dom-watch core-add-dom-watch) 7 | 8 | (def defdom core-defdom) 9 | 10 | (register-plugin (form-values.)) 11 | 12 | -------------------------------------------------------------------------------- /src/webfui/dom_manipulation.cljs: -------------------------------------------------------------------------------- 1 | (ns webfui.dom-manipulation 2 | (:use [webfui.html :only [unparse-html]])) 3 | 4 | ;; This file is for DOM manipulation functions used by both the core of webfui as well as plugins. 5 | 6 | (defn path-dom [node] 7 | (drop 3 8 | (reverse ((fn f [node] 9 | (lazy-seq (when node 10 | (cons (dec (count (take-while identity (iterate #(.-previousSibling %) node)))) (f (.-parentNode node)))))) 11 | node)))) 12 | 13 | (defn select-path-html [html path] 14 | (if-let [[cur & more] (seq path)] 15 | (recur (nth (.-children html) cur) more) 16 | html)) 17 | 18 | (defn resolve-target [parsed-html target] 19 | (let [path (path-dom target) 20 | parsed-element (select-path-html parsed-html path)] 21 | (unparse-html parsed-element))) 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/webfui/framework.cljs: -------------------------------------------------------------------------------- 1 | (ns webfui.framework 2 | (:use [webfui.dom :only [defdom add-dom-watch]] 3 | [webfui.plugin.core :only [register-plugin]] 4 | [webfui.plugin.mouse :only [mouse add-mouse-watch]] 5 | [webfui.state-patches :only [patch]] 6 | [webfui.utilities :only [get-attribute]])) 7 | 8 | (register-plugin (mouse.)) 9 | 10 | (defn state-watcher [dom renderer key state old new] 11 | (swap! dom (constantly (renderer new)))) 12 | 13 | (declare cur-state) 14 | 15 | (defn launch-app [state renderer] 16 | (let [dom (atom (renderer @state))] 17 | (defdom dom) 18 | (add-watch state :state-watcher (partial state-watcher dom renderer))) 19 | (def cur-state state)) 20 | 21 | (defn add-dom-watch-helper [id fun] 22 | (add-dom-watch id 23 | (fn [_ element-new] 24 | (swap! cur-state 25 | (fn [state] 26 | (let [diff (fun state element-new)] 27 | (patch state diff))))))) 28 | 29 | (def mouse-down-state (atom nil)) 30 | 31 | (defn add-mouse-watch-helper [id fun optimization] 32 | (add-mouse-watch id 33 | (fn [element-old element-new points] 34 | (swap! mouse-down-state 35 | (fn [old] 36 | (or old @cur-state))) 37 | (reset! cur-state 38 | (if (= optimization :incremental) 39 | (let [mds @mouse-down-state 40 | diff (fun mds element-old element-new (subvec points (max 0 (- (count points) 2)))) 41 | new-state (patch mds diff)] 42 | (reset! mouse-down-state 43 | (when (get-attribute element-old :active) 44 | new-state)) 45 | new-state) 46 | (let [mds @mouse-down-state 47 | diff (fun mds element-old element-new points)] 48 | (when-not (get-attribute element-old :active) 49 | (reset! mouse-down-state nil)) 50 | (patch mds diff))))))) 51 | 52 | -------------------------------------------------------------------------------- /src/webfui/framework/macros.clj: -------------------------------------------------------------------------------- 1 | ;*CLJSBUILD-MACRO-FILE*; 2 | 3 | (ns webfui.framework.macros) 4 | 5 | (defmacro add-dom-watch [id vars & more] 6 | `(webfui.framework/add-dom-watch-helper ~id (fn ~vars ~@more))) 7 | 8 | (defmacro add-mouse-watch [id vars & more] 9 | (if (vector? id) 10 | `(webfui.framework/add-mouse-watch-helper ~(first id) (fn ~vars ~@more) ~(second id)) 11 | `(webfui.framework/add-mouse-watch-helper ~id (fn ~vars ~@more) :full))) -------------------------------------------------------------------------------- /src/webfui/html.cljs: -------------------------------------------------------------------------------- 1 | (ns webfui.html 2 | (:use [clojure.set :only [union]])) 3 | 4 | (deftype parsed-tagname [tagname id classes]) 5 | (deftype parsed-html [tagname attributes children]) 6 | 7 | (defn parse-tagname [tagname] 8 | (let [[_ name _ id classes] (re-matches #"^([^.^#]+)(#([^.]+))?(\..+)?" tagname)] 9 | (parsed-tagname. (keyword name) 10 | (when id 11 | (keyword id)) 12 | (when classes 13 | (map second (re-seq #"\.([^.]+)" classes)))))) 14 | 15 | (let [cache (atom {})] 16 | (defn parse-tagname-memoized [tagname] 17 | (or (@cache tagname) 18 | (let [val (parse-tagname tagname)] 19 | (swap! cache assoc tagname val) 20 | val)))) 21 | 22 | (declare parse-html) 23 | 24 | (defn parse-element [element] 25 | (let [[tagname & more] element 26 | parsed (parse-tagname-memoized tagname) 27 | classes (.-classes parsed) 28 | id (.-id parsed) 29 | tagname (.-tagname parsed) 30 | attributes {} 31 | attributes (if classes 32 | (assoc attributes 33 | :class 34 | (apply str (interpose \ classes))) 35 | attributes) 36 | attributes (if id 37 | (assoc attributes :id id) 38 | attributes) 39 | [a & b] more 40 | [attributes children] (if (map? a) 41 | [(merge attributes a) b] 42 | [attributes more])] 43 | (parsed-html. tagname attributes (parse-html children)))) 44 | 45 | (defn merge-strings [lst] 46 | (if-let [[x & more] lst] 47 | (if-let [[y & more] more] 48 | (if (and (string? x) (string? y)) 49 | (merge-strings (cons (str x y) more)) 50 | (cons x (merge-strings (cons y more)))) 51 | lst) 52 | lst)) 53 | 54 | (defn parse-html [html] 55 | (mapcat (fn [x] 56 | (cond (vector? x) [(parse-element x)] 57 | (and (coll? x) (not (string? x))) (parse-html x) 58 | :else [x])) 59 | (merge-strings html))) 60 | 61 | (defn tag [tagname atts s] 62 | (if (#{:br} tagname) 63 | (str "<" (name tagname) atts ">") 64 | (str "<" (name tagname) atts ">" (apply str s) ""))) 65 | 66 | (defn pixels [k] 67 | (str (.toFixed k 3) "px")) 68 | 69 | (defn render-css [css] 70 | (apply str 71 | (interpose \; 72 | (for [[k v] css] 73 | (str (name k) 74 | ":" 75 | (cond (keyword? v) (name v) 76 | (and (number? v) (#{:line-height :top :bottom :left :right :width :height} k)) (pixels v) 77 | :else v)))))) 78 | 79 | (defn render-attribute-value [key value] 80 | (cond (keyword? value) (name value) 81 | (= key :data) (print-str value) 82 | (= key :style) (render-css value) 83 | :else value)) 84 | 85 | (defn render-attributes [atts] 86 | (apply str 87 | (for [[key value] atts] 88 | (str " " 89 | (name key) 90 | "=\"" 91 | (render-attribute-value key value) 92 | \")))) 93 | 94 | (defn html [content] 95 | (cond (instance? parsed-html content) (let [tagname (.-tagname content) 96 | attributes (.-attributes content) 97 | children (.-children content)] 98 | (if tagname 99 | (tag tagname (when attributes 100 | (render-attributes attributes)) 101 | (map html children)) 102 | "")) 103 | :else (str content))) 104 | 105 | (defn html-delta [old-html new-html path] 106 | (if (= (count old-html) (count new-html)) 107 | (let [pairs (map vector old-html new-html) 108 | fixable (every? (fn [[old-child new-child]] 109 | (if (and (instance? parsed-html old-child) (instance? parsed-html new-child)) 110 | (= (.-tagname old-child) (.-tagname new-child)) 111 | (= old-child new-child))) 112 | pairs)] 113 | (if fixable 114 | (apply concat 115 | (map-indexed (fn [i [old-element new-element]] 116 | (when (instance? parsed-html old-element) 117 | (let [old-tagname (.-tagname old-element) 118 | old-attributes (.-attributes old-element) 119 | old-children (.-children old-element) 120 | new-tagname (.-tagname new-element) 121 | new-attributes (.-attributes new-element) 122 | new-children (.-children new-element) 123 | path (conj path i) 124 | att-delta (when (not= old-attributes new-attributes) 125 | (mapcat (fn [key] 126 | (let [old-val (old-attributes key) 127 | new-val (new-attributes key)] 128 | (cond (not new-val) [[:rem-att path key]] 129 | (not= old-val new-val) [[:att path key new-val]] 130 | :else []))) 131 | (union (set (keys old-attributes)) (set (keys new-attributes))))) 132 | child-delta (html-delta old-children new-children path)] 133 | (concat att-delta child-delta)))) 134 | pairs)) 135 | [[:html path new-html]])) 136 | [[:html path new-html]])) 137 | 138 | (defn unparse-html [html] 139 | (if (or (string? html) (number? html)) 140 | html 141 | (let [tagname (.-tagname html) 142 | attributes (.-attributes html) 143 | children (.-children html)] 144 | (vec (concat [tagname attributes] (map unparse-html children)))))) 145 | 146 | -------------------------------------------------------------------------------- /src/webfui/macros.clj: -------------------------------------------------------------------------------- 1 | ;*CLJSBUILD-MACRO-FILE*; 2 | 3 | (ns webfui.macros) 4 | 5 | (defmacro dbg [cur & more] 6 | `(do (.log js/console (print-str '~cur "-->")) 7 | (let [x# (~cur ~@more)] 8 | (.log js/console (print-str '~cur "-->" (pr-str x#))) 9 | x#))) 10 | 11 | (defmacro dbgv [& vars] 12 | `(do ~@(map (fn [var] 13 | `(do (.log js/console (print-str '~var "==>" (pr-str ~var))) 14 | ~var)) 15 | vars))) -------------------------------------------------------------------------------- /src/webfui/plugin/core.cljs: -------------------------------------------------------------------------------- 1 | (ns webfui.plugin.core) 2 | 3 | (def active-plugins (atom [])) 4 | 5 | (defprotocol Plugin 6 | (declare-events [this body dom-watchers parsed-html]) 7 | (fix-dom [this])) 8 | 9 | (defn register-plugin [plugin] 10 | (swap! active-plugins conj plugin)) -------------------------------------------------------------------------------- /src/webfui/plugin/form_values.cljs: -------------------------------------------------------------------------------- 1 | (ns webfui.plugin.form-values 2 | (:use [webfui.plugin.core :only [Plugin]] 3 | [webfui.dom-manipulation :only [select-path-html parsed-html unparse-html path-dom resolve-target]])) 4 | 5 | (defn input [dom-watchers parsed-html event] 6 | (let [target (.-target event) 7 | [tagname attributes :as element] (resolve-target @parsed-html target) 8 | event (@dom-watchers (keyword (:watch attributes)))] 9 | (when (and event (contains? #{:input :textarea} tagname)) 10 | (let [value (.-value target) 11 | new-element (update-in element 12 | [1 :value] 13 | (fn [old] 14 | (set! (.-value target) old) 15 | value))] 16 | (event element new-element))))) 17 | 18 | (deftype form-values [] 19 | Plugin 20 | (declare-events [this body dom-watchers parsed-html] 21 | (.addEventListener body "input" (partial input dom-watchers parsed-html))) 22 | (fix-dom [this] 23 | nil)) 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/webfui/plugin/mouse.cljs: -------------------------------------------------------------------------------- 1 | (ns webfui.plugin.mouse 2 | (:use [webfui.plugin.core :only [Plugin]] 3 | [webfui.utilities :only [body]] 4 | [webfui.dom-manipulation :only [resolve-target]] 5 | [cljs.reader :only [read-string]])) 6 | 7 | (def mouse-watchers (atom {})) 8 | 9 | (declare mouse-down-element) 10 | 11 | (defn nodelist-to-seq [nl] 12 | (let [result-seq (map #(.item nl %) (range (.-length nl)))] 13 | result-seq)) 14 | 15 | (defn offset [node] 16 | (let [op (.-offsetParent node)] 17 | (if op 18 | (let [[x y] (offset op)] 19 | [(+ x (.-offsetLeft node)) (+ y (.-offsetTop node))]) 20 | [0 0]))) 21 | 22 | (defn all-elements-at-point [client-point] 23 | ((fn f [element] 24 | (let [[x y] client-point 25 | chi (mapcat f (nodelist-to-seq (.-childNodes element)))] 26 | (if (and (.-getBoundingClientRect element) 27 | (let [rect (.getBoundingClientRect element)] 28 | (and (<= (.-left rect) x) (<= (.-top rect) y) (> (.-right rect) x) (> (.-bottom rect) y)))) 29 | (cons element chi) 30 | chi))) 31 | (body))) 32 | 33 | (defn merge-data [acc lst] 34 | (if-let [[k & more] lst] 35 | (cond (not k) (recur acc more) 36 | (not acc) (recur k more) 37 | (and (map? k) (map? acc)) (recur (merge acc k) more) 38 | :else (recur k more)) 39 | acc)) 40 | 41 | (defn mouse-element [parsed-html ev] 42 | (let [target (.-target ev) 43 | typ (.-type ev) 44 | point (cond (#{"touchstart"} typ) (let [touch (.item (.-touches ev) 0)] 45 | [(.-clientX touch) (.-clientY touch)]) 46 | (#{"touchmove"} typ) (let [touch (.item (.-touches ev) 0)] 47 | [(.-clientX touch) (.-clientY touch)]) 48 | (= "touchend" typ) (let [touch (.item (.-changedTouches ev) 0)] 49 | [(.-clientX touch) (.-clientY touch)]) 50 | :else [(.-clientX ev) (.-clientY ev)]) 51 | elements (all-elements-at-point point) 52 | data (merge-data nil 53 | (for [element elements] 54 | (when-let [s (.getAttribute element "data")] 55 | (read-string s))))] 56 | [(update-in (resolve-target @parsed-html target) [1] assoc :offset (offset target) :data data) [(.-pageX ev) (.-pageY ev)]])) 57 | 58 | (defn update-offset [element target] 59 | (update-in element 60 | [1] 61 | (fn [attr] 62 | (assoc attr :offset (offset target) 63 | :data (if-let [data (.getAttribute target "data")] 64 | (read-string data) 65 | (:data attr)))))) 66 | 67 | (defn mouse-event [element] 68 | (@mouse-watchers (get-in element [1 :mouse]))) 69 | 70 | (defn add-mouse-watch [id fun] 71 | (swap! mouse-watchers assoc id fun)) 72 | 73 | (defn mouse-down [parsed-html ev] 74 | #_(.preventDefault ev) 75 | (let [target (.-target ev) 76 | [new-element point] (mouse-element parsed-html ev) 77 | event (mouse-event new-element)] 78 | (when event 79 | (let [new-element (assoc-in new-element [1 :active] true)] 80 | (def mouse-down-element new-element) 81 | (def mouse-down-target target) 82 | (def points [point]) 83 | (event new-element new-element points))))) 84 | 85 | (defn mouse-move [parsed-html ev] 86 | (.preventDefault ev) 87 | (when mouse-down-element 88 | (let [target (.-target ev) 89 | [new-element point] (mouse-element parsed-html ev) 90 | event (mouse-event mouse-down-element)] 91 | (def points (conj points point)) 92 | (event (update-offset mouse-down-element mouse-down-target) new-element points)))) 93 | 94 | (defn mouse-up [parsed-html ev] 95 | (let [target (.-target ev) 96 | [new-element point] (mouse-element parsed-html ev)] 97 | (when mouse-down-element 98 | (let [event (mouse-event mouse-down-element) 99 | first-element (update-in mouse-down-element [1] #(dissoc % :active))] 100 | (event (update-offset first-element mouse-down-target) new-element points) 101 | (def points nil) 102 | (def mouse-down-element nil) 103 | (def mouse-down-target nil))))) 104 | 105 | (deftype mouse [] 106 | Plugin 107 | (declare-events [this body dom-watchers parsed-html] 108 | (.setTimeout js/window 109 | (fn [] 110 | (.scrollTo js/window 0 1)) 111 | 100) 112 | (.addEventListener body "mousedown" (partial mouse-down parsed-html)) 113 | (.addEventListener body "mousemove" (partial mouse-move parsed-html)) 114 | (.addEventListener body "mouseup" (partial mouse-up parsed-html)) 115 | (.addEventListener js/window "touchstart" (partial mouse-down parsed-html)) 116 | (.addEventListener js/window "touchmove" (partial mouse-move parsed-html)) 117 | (.addEventListener js/window "touchend" (partial mouse-up parsed-html))) 118 | (fix-dom [this] 119 | nil)) 120 | 121 | 122 | -------------------------------------------------------------------------------- /src/webfui/plugin/scrolling.cljs: -------------------------------------------------------------------------------- 1 | (ns webfui.plugin.scrolling 2 | (:use [webfui.plugin.core :only [Plugin]] 3 | [webfui.dom-manipulation :only [select-path-html parsed-html unparse-html path-dom resolve-target]])) 4 | 5 | (def dom-watchers-atom (atom nil)) 6 | (def parsed-html-atom (atom nil)) 7 | 8 | (defn scroll [event] 9 | (let [target (.-target event) 10 | [tagname attributes :as element] (resolve-target @@parsed-html-atom target) 11 | scroll-top (.-scrollTop target) 12 | scroll-left (.-scrollLeft target) 13 | event (@@dom-watchers-atom (keyword (:watch attributes))) 14 | new-element (update-in element [1] assoc :scroll-top scroll-top :scroll-left scroll-left)] 15 | (event element new-element))) 16 | 17 | (defn check-for-scroll [event] 18 | (let [target (.-target event)] 19 | (.addEventListener target "scroll" scroll))) 20 | 21 | (defn xpath [s] 22 | (let [res (.evaluate js/document s js/document nil (.-ORDERED_NODE_ITERATOR_TYPE js/XPathResult) nil)] 23 | (doall (take-while identity (repeatedly (fn [] (.iterateNext res))))))) 24 | 25 | (deftype scrolling [] 26 | Plugin 27 | (declare-events [this body dom-watchers parsed-html] 28 | (reset! dom-watchers-atom dom-watchers) 29 | (reset! parsed-html-atom parsed-html) 30 | (.addEventListener body "mousedown" check-for-scroll) 31 | (.addEventListener body "keydown" check-for-scroll)) 32 | 33 | (fix-dom [this] 34 | (doseq [element (xpath "//*[@scroll-top]")] 35 | (let [scroll-top (js/parseInt (.getAttribute element "scroll-top"))] 36 | (when (> (js/Math.abs (- (.-scrollTop element) scroll-top)) 1) 37 | (set! (.-scrollTop element) scroll-top) 38 | (.addEventListener element "scroll" scroll) 39 | (.removeAttribute element "scroll-top")))))) 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/webfui/state_patches.cljs: -------------------------------------------------------------------------------- 1 | (ns webfui.state-patches 2 | (:use [clojure.set :only [union]])) 3 | 4 | (defn patch [state diff] 5 | (if diff 6 | (cond (map? state) (into {} 7 | (for [key (union (set (keys state)) (set (keys diff)))] 8 | [key (let [val1 (state key) 9 | val2 (diff key)] 10 | (cond (and val1 val2) (patch val1 val2) 11 | (contains? diff key) val2 12 | :else val1))])) 13 | (vector? state) (if (map? diff) 14 | (vec (map-indexed (fn [index item] 15 | (if-let [d (diff index)] 16 | (patch item d) 17 | item)) 18 | state)) 19 | diff) 20 | :else diff) 21 | state)) 22 | 23 | -------------------------------------------------------------------------------- /src/webfui/utilities.cljs: -------------------------------------------------------------------------------- 1 | (ns webfui.utilities) 2 | 3 | (defn body [] 4 | (.-body js/document)) 5 | 6 | (defn get-attribute [element key] 7 | (get-in element [1 key])) 8 | 9 | (defn clicked [first-element last-element] 10 | (and (= first-element last-element) (not (get-attribute first-element :active)))) 11 | 12 | -------------------------------------------------------------------------------- /target/stale/dependencies: -------------------------------------------------------------------------------- 1 | ([:dependencies [[codox/codox.core "0.6.4"]]]) -------------------------------------------------------------------------------- /test/webfui/test/core.clj: -------------------------------------------------------------------------------- 1 | (ns webfui.test.core 2 | (:use [webfui.core]) 3 | (:use [clojure.test])) 4 | 5 | (deftest replace-me ;; FIXME: write 6 | (is false "No tests have been written.")) 7 | -------------------------------------------------------------------------------- /webfui-examples/.gitignore: -------------------------------------------------------------------------------- 1 | pom.xml 2 | *jar 3 | /target 4 | /lib/ 5 | /classes/ 6 | .lein-deps-sum 7 | .lein-cljsbuild* 8 | -------------------------------------------------------------------------------- /webfui-examples/.lein-plugins/checksum: -------------------------------------------------------------------------------- 1 | b0bea0f2cf6c59e422426393bfdfae292119cbec -------------------------------------------------------------------------------- /webfui-examples/README.md: -------------------------------------------------------------------------------- 1 | # webfui-examples 2 | 3 | A website written in noir. 4 | 5 | ## Usage 6 | 7 | ```bash 8 | lein deps 9 | lein run 10 | ``` 11 | 12 | ## License 13 | 14 | Copyright (C) 2011 FIXME 15 | 16 | Distributed under the Eclipse Public License, the same as Clojure. 17 | 18 | -------------------------------------------------------------------------------- /webfui-examples/project.clj: -------------------------------------------------------------------------------- 1 | (defproject webfui-examples "0.2" 2 | :description "Examples for Webfui" 3 | :source-path "src-clj" 4 | :dependencies [[org.clojure/clojure "1.4.0"] 5 | [webfui "0.2.1"] 6 | [noir "1.3.0-beta10"] 7 | [hiccup "1.0.1"]] 8 | :min-lein-version "2.0.0" 9 | :main webfui-examples.server 10 | :plugins [[lein-cljsbuild "0.2.7"]] 11 | :source-paths ["src-clj"] 12 | :cljsbuild {:builds {:add_two_numbers {:source-path "src-cljs/add_two_numbers" 13 | :compiler {:output-to "resources/public/js/add_two_numbers.js" 14 | :optimizations :whitespace 15 | :pretty-print true}} 16 | :add_two_numbers_low_level {:source-path "src-cljs/add_two_numbers_low_level" 17 | :compiler {:output-to "resources/public/js/add_two_numbers_low_level.js" 18 | :optimizations :whitespace 19 | :pretty-print true}} 20 | :calculator {:source-path "src-cljs/calculator" 21 | :compiler {:output-to "resources/public/js/calculator.js" 22 | :optimizations :whitespace 23 | :pretty-print true}} 24 | :calculator_many {:source-path "src-cljs/calculator_many" 25 | :compiler {:output-to "resources/public/js/calculator_many.js" 26 | :optimizations :advanced 27 | :pretty-print false}} 28 | :calculator_ajax {:source-path "src-cljs/calculator_ajax" 29 | :compiler {:output-to "resources/public/js/calculator_ajax.js" 30 | :optimizations :whitespace 31 | :pretty-print true}} 32 | :scrolling {:source-path "src-cljs/scrolling" 33 | :compiler {:output-to "resources/public/js/scrolling.js" 34 | :optimizations :whitespace 35 | :pretty-print true}} 36 | :mouse_tracking {:source-path "src-cljs/mouse_tracking" 37 | :compiler {:output-to "resources/public/js/mouse_tracking.js" 38 | :optimizations :whitespace 39 | :pretty-print true}} 40 | :inverse_kinematics {:source-path "src-cljs/inverse_kinematics" 41 | :compiler {:output-to "resources/public/js/inverse_kinematics.js" 42 | :optimizations :advanced 43 | :pretty-print false}}}}) 44 | 45 | -------------------------------------------------------------------------------- /webfui-examples/resources/public/css/POCKC___.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drcode/webfui/e8ba6e9224542755e5780488cb43f0c5510827f9/webfui-examples/resources/public/css/POCKC___.eot -------------------------------------------------------------------------------- /webfui-examples/resources/public/css/POCKC___.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Created by FontForge 20110222 at Mon Mar 7 11:34:23 2011 6 | By www-data 7 | Copyright 1999 Blue Vinyl Fonts email: bluevinyl@aol.com 8 | 9 | 10 | 11 | 26 | 27 | 29 | 32 | 35 | 38 | 41 | 43 | 46 | 48 | 50 | 53 | 56 | 59 | 61 | 63 | 67 | 71 | 74 | 77 | 80 | 82 | 84 | 87 | 90 | 95 | 97 | 99 | 101 | 103 | 106 | 108 | 110 | 112 | 114 | 116 | 119 | 121 | 124 | 127 | 129 | 132 | 135 | 137 | 140 | 143 | 145 | 147 | 150 | 152 | 154 | 157 | 161 | 164 | 167 | 169 | 172 | 175 | 177 | 180 | 182 | 184 | 187 | 190 | 192 | 195 | 198 | 201 | 204 | 207 | 210 | 213 | 215 | 218 | 221 | 224 | 227 | 230 | 233 | 236 | 238 | 241 | 243 | 245 | 247 | 250 | 253 | 255 | 258 | 260 | 262 | 265 | 267 | 269 | 271 | 273 | 275 | 278 | 280 | 282 | 285 | 288 | 290 | 292 | 294 | 296 | 298 | 301 | 304 | 307 | 309 | 312 | 314 | 317 | 320 | 323 | 327 | 330 | 333 | 337 | 340 | 343 | 346 | 349 | 352 | 355 | 358 | 361 | 364 | 367 | 370 | 373 | 376 | 378 | 380 | 383 | 385 | 389 | 392 | 395 | 398 | 401 | 405 | 407 | 409 | 412 | 415 | 417 | 419 | 421 | 424 | 427 | 429 | 432 | 436 | 439 | 443 | 445 | 447 | 450 | 453 | 455 | 458 | 460 | 462 | 464 | 467 | 470 | 473 | 475 | 477 | 480 | 484 | 488 | 490 | 493 | 497 | 501 | 505 | 508 | 510 | 512 | 514 | 516 | 518 | 520 | 522 | 525 | 528 | 531 | 533 | 536 | 539 | 541 | 543 | 550 | 553 | 557 | 560 | 564 | 567 | 569 | 572 | 574 | 576 | 579 | 583 | 586 | 589 | 592 | 595 | 597 | 600 | 602 | 603 | 604 | -------------------------------------------------------------------------------- /webfui-examples/resources/public/css/POCKC___.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drcode/webfui/e8ba6e9224542755e5780488cb43f0c5510827f9/webfui-examples/resources/public/css/POCKC___.ttf -------------------------------------------------------------------------------- /webfui-examples/resources/public/css/POCKC___.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drcode/webfui/e8ba6e9224542755e5780488cb43f0c5510827f9/webfui-examples/resources/public/css/POCKC___.woff -------------------------------------------------------------------------------- /webfui-examples/resources/public/css/add_two_numbers.css: -------------------------------------------------------------------------------- 1 | body{background-color:dodgerblue; 2 | text-align:center; 3 | font-size:3em} 4 | div{padding:2em} 5 | input{font-size:1em; 6 | background-color:lightblue; 7 | width:5em} 8 | span{background-color:lightblue; 9 | padding:0.1em} 10 | -------------------------------------------------------------------------------- /webfui-examples/resources/public/css/add_two_numbers_low_level.css: -------------------------------------------------------------------------------- 1 | body{background-color:dodgerblue; 2 | text-align:center; 3 | font-family:sans-serif; 4 | font-size:3em} 5 | div{padding:2em} 6 | input{font-size:1em; 7 | background-color:lightblue; 8 | width:5em} 9 | span{background-color:lightblue; 10 | padding:0.1em} 11 | -------------------------------------------------------------------------------- /webfui-examples/resources/public/css/calculator.css: -------------------------------------------------------------------------------- 1 | @font-face {font-family:"Pocket Calculator";src:url("POCKC___.eot?") format("eot"),url("POCKC___.woff") format("woff"),url("POCKC___.ttf") format("truetype"),url("POCKC___.svg#PocketCalculator") format("svg");font-weight:normal;font-style:normal;} 2 | html, body, div, span, object,form,input,text,h1,h2,button,label,a,img{-moz-user-select:none; 3 | -webkit-user-select:none; 4 | -webkit-user-drag:none} 5 | textarea,input[type='text']{-webkit-user-select:text; 6 | -moz-user-select:text} 7 | body{background-color:grey; 8 | text-align:center; 9 | font-weight:bold; 10 | font-family:sans-serif; 11 | font-size:3em} 12 | table{margin-top:1em; 13 | background-color:black; 14 | margin-left:auto; 15 | margin-right:auto; 16 | padding:0.2em; 17 | -webkit-border-radius:0.5em; 18 | border-radius:0.5em} 19 | div{text-align:center; 20 | margin:0.2em; 21 | width:2em; 22 | padding-top:0.4em; 23 | padding-bottom:0.35em; 24 | -webkit-border-radius:0.3em; 25 | border-radius:0.3em; 26 | background-color:silver} 27 | #ms,#mr,#ac{background-color:crimson} 28 | #mult,#div,#plus,#minus,#period{background-color:tan} 29 | #eq{width:4.5em;background-color:orange} 30 | #display{background-color:turquoise; 31 | width:6.5em; 32 | -webkit-border-radius:0.2em; 33 | border-radius:0.2em; 34 | padding-top:0.1em; 35 | padding-bottom:0.0em; 36 | padding-right:0.35em; 37 | font-family:"Pocket Calculator"; 38 | font-size:1.4em; 39 | text-align:right} 40 | div{cursor:pointer} 41 | div:active{-webkit-transform:scale(0.9); 42 | -moz-transform:scale(0.9); 43 | opacity:0.7} -------------------------------------------------------------------------------- /webfui-examples/resources/public/css/calculator_ajax.css: -------------------------------------------------------------------------------- 1 | @font-face {font-family:"Pocket Calculator";src:url("POCKC___.eot?") format("eot"),url("POCKC___.woff") format("woff"),url("POCKC___.ttf") format("truetype"),url("POCKC___.svg#PocketCalculator") format("svg");font-weight:normal;font-style:normal;} 2 | html, body, div, span, object,form,input,text,h1,h2,button,label,a,img{-moz-user-select:none; 3 | -webkit-user-select:none; 4 | -webkit-user-drag:none} 5 | textarea,input[type='text']{-webkit-user-select:text; 6 | -moz-user-select:text} 7 | body{background-color:grey; 8 | text-align:center; 9 | font-weight:bold; 10 | font-family:sans-serif; 11 | font-size:3em} 12 | table{margin-top:1em; 13 | background-color:black; 14 | margin-left:auto; 15 | margin-right:auto; 16 | padding:0.2em; 17 | -webkit-border-radius:0.5em; 18 | border-radius:0.5em} 19 | div{text-align:center; 20 | margin:0.2em; 21 | width:2em; 22 | padding-top:0.4em; 23 | padding-bottom:0.35em; 24 | -webkit-border-radius:0.3em; 25 | border-radius:0.3em; 26 | background-color:silver} 27 | #ms,#mr,#ac{background-color:crimson} 28 | #mult,#div,#plus,#minus,#period{background-color:tan} 29 | #eq{width:4.5em;background-color:orange} 30 | #display{background-color:turquoise; 31 | width:6.5em; 32 | -webkit-border-radius:0.2em; 33 | border-radius:0.2em; 34 | padding-top:0.1em; 35 | padding-bottom:0.0em; 36 | padding-right:0.35em; 37 | font-family:"Pocket Calculator"; 38 | font-size:1.4em; 39 | text-align:right} 40 | div{cursor:pointer} 41 | div:active{-webkit-transform:scale(0.9); 42 | -moz-transform:scale(0.9); 43 | opacity:0.7} -------------------------------------------------------------------------------- /webfui-examples/resources/public/css/calculator_many.css: -------------------------------------------------------------------------------- 1 | @font-face {font-family:"Pocket Calculator";src:url("POCKC___.eot?") format("eot"),url("POCKC___.woff") format("woff"),url("POCKC___.ttf") format("truetype"),url("POCKC___.svg#PocketCalculator") format("svg");font-weight:normal;font-style:normal;} 2 | html, body, div, span, object,form,input,text,h1,h2,button,label,a,img{-moz-user-select:none; 3 | -webkit-user-select:none; 4 | -webkit-user-drag:none} 5 | textarea,input[type='text']{-webkit-user-select:text; 6 | -moz-user-select:text} 7 | body{background-color:grey; 8 | text-align:center; 9 | font-weight:bold; 10 | font-family:sans-serif; 11 | font-size:0.4em} 12 | table.calc{margin-top:1em; 13 | background-color:black; 14 | margin-left:auto; 15 | margin-right:auto; 16 | padding:0.2em; 17 | -webkit-border-radius:0.5em; 18 | border-radius:0.5em} 19 | div{text-align:center; 20 | margin:0.2em; 21 | width:2em; 22 | padding-top:0.4em; 23 | padding-bottom:0.35em; 24 | -webkit-border-radius:0.3em; 25 | border-radius:0.3em; 26 | background-color:silver} 27 | #ms,#mr,#ac{background-color:crimson} 28 | #mult,#div,#plus,#minus,#period{background-color:tan} 29 | #eq{width:4.5em;background-color:orange} 30 | #display{background-color:turquoise; 31 | width:6.5em; 32 | -webkit-border-radius:0.2em; 33 | border-radius:0.2em; 34 | padding-top:0.1em; 35 | padding-bottom:0.0em; 36 | padding-right:0.35em; 37 | font-family:"Pocket Calculator"; 38 | font-size:1.4em; 39 | text-align:right} 40 | div{cursor:pointer} 41 | div:active{-webkit-transform:scale(0.9); 42 | -moz-transform:scale(0.9); 43 | opacity:0.7} -------------------------------------------------------------------------------- /webfui-examples/resources/public/css/inverse_kinematics.css: -------------------------------------------------------------------------------- 1 | html, body, div, span, object,form,input,text,h1,h2,button,label,a,img{-moz-user-select:none; 2 | -webkit-user-select:none; 3 | -webkit-user-drag:none} 4 | body{margin:0; 5 | padding:0; 6 | position:absolute; 7 | bottom:0px; 8 | top:0px; 9 | left:0px; 10 | right:0px; 11 | overflow:hidden} 12 | .node{opacity:0.5; 13 | position:absolute; 14 | color:white; 15 | padding:5px; 16 | font-weight:bold; 17 | background-color:red} 18 | .destination{background-color:blue} 19 | .box{opacity:0.5; 20 | position:absolute; 21 | background-color:black; 22 | -webkit-transform-origin:0% 0%} -------------------------------------------------------------------------------- /webfui-examples/resources/public/css/mouse_tracking.css: -------------------------------------------------------------------------------- 1 | html, body, div, span, object,form,input,text,h1,h2,button,label,a,img{-moz-user-select:none; 2 | -webkit-user-select:none; 3 | -webkit-user-drag:none; 4 | } 5 | body{bottom:0px; 6 | top:0px; 7 | left:0px; 8 | right:0px; 9 | padding:0px; 10 | margin:0px; 11 | position:absolute; 12 | font-size:3em; 13 | overflow:hidden; 14 | color:white} 15 | div.layer{ 16 | background-color:blue; 17 | position:absolute; 18 | top:0; 19 | left:0; 20 | } 21 | div{background-color:blue; 22 | text-align:center; 23 | position:absolute; 24 | font-size:1em; 25 | padding:0px; 26 | margin:0px; 27 | -webkit-border-radius:0.6em; 28 | cursor:pointer} 29 | .dragged{position:absolute; 30 | opacity:0.5; 31 | background-color:black} 32 | -------------------------------------------------------------------------------- /webfui-examples/resources/public/css/scrolling.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drcode/webfui/e8ba6e9224542755e5780488cb43f0c5510827f9/webfui-examples/resources/public/css/scrolling.css -------------------------------------------------------------------------------- /webfui-examples/resources/public/css/webfui_examples.css: -------------------------------------------------------------------------------- 1 | body{background-color:dodgerblue; 2 | text-align:center; 3 | font-size:3em} 4 | div{padding:2em} 5 | input{font-size:1em; 6 | background-color:lightblue; 7 | width:5em} 8 | span{background-color:lightblue; 9 | padding:0.1em} 10 | -------------------------------------------------------------------------------- /webfui-examples/resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 |

Webfui Examples

3 |

(Right now Chrome & Safari work 100%, Firefox will be ready shortly.)

4 |

5 |

    6 |
  1. Add Two Numbers (low level version)
  2. 7 |
  3. Add Two Numbers (version with elegant code)
  4. 8 |
  5. Scrollbar Plugin Example
  6. 9 |
  7. Mouse Plugin Example
  8. 10 |
  9. Calculator
  10. 11 |
  11. Many Calculators
  12. 12 |
  13. Calculator with Serve-side Memory
  14. 13 |
  15. Inverse Kinematics
  16. 14 |
15 |

16 | -------------------------------------------------------------------------------- /webfui-examples/resources/public/js/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | -------------------------------------------------------------------------------- /webfui-examples/src-clj/webfui_examples/server.clj: -------------------------------------------------------------------------------- 1 | (ns webfui-examples.server 2 | (:use [noir.core :only [defpage]] 3 | [noir.response :only [redirect]]) 4 | (:require [noir.server :as server])) 5 | 6 | (server/load-views "src-clj/webfui_examples/views/") 7 | 8 | (defpage "/" [] 9 | (redirect "/index.html")) 10 | 11 | (def memory (atom 0)) 12 | 13 | (defpage [:get "/memory"] [] 14 | (str @memory)) 15 | 16 | (defpage [:put "/memory/:value"] {:keys [value]} 17 | (reset! memory value) 18 | (pr-str value)) 19 | 20 | (defn -main [& m] 21 | (let [mode (keyword (or (first m) :dev)) 22 | port (Integer. (get (System/getenv) "PORT" "8080"))] 23 | (server/start port {:mode mode 24 | :ns 'webfui-examples}))) 25 | 26 | -------------------------------------------------------------------------------- /webfui-examples/src-clj/webfui_examples/views/add_two_numbers.clj: -------------------------------------------------------------------------------- 1 | (ns webfui-examples.views.add-two-numbers 2 | (:require [webfui-examples.views.common :as common] 3 | [noir.content.getting-started]) 4 | (:use [noir.core :only [defpage]] 5 | [hiccup.core :only [html]])) 6 | 7 | (defpage "/add_two_numbers" [] 8 | (common/layout "add_two_numbers")) 9 | -------------------------------------------------------------------------------- /webfui-examples/src-clj/webfui_examples/views/add_two_numbers_low_level.clj: -------------------------------------------------------------------------------- 1 | (ns webfui-examples.views.add-two-numbers-low-level 2 | (:require [webfui-examples.views.common :as common] 3 | [noir.content.getting-started]) 4 | (:use [noir.core :only [defpage]] 5 | [hiccup.core :only [html]])) 6 | 7 | (defpage "/add_two_numbers_low_level" [] 8 | (common/layout "add_two_numbers_low_level")) 9 | -------------------------------------------------------------------------------- /webfui-examples/src-clj/webfui_examples/views/calculator.clj: -------------------------------------------------------------------------------- 1 | (ns webfui-examples.views.calculator 2 | (:require [webfui-examples.views.common :as common] 3 | [noir.content.getting-started]) 4 | (:use [noir.core :only [defpage]] 5 | [hiccup.core :only [html]])) 6 | 7 | (defpage "/calculator" [] 8 | (common/layout "calculator")) 9 | -------------------------------------------------------------------------------- /webfui-examples/src-clj/webfui_examples/views/calculator_ajax.clj: -------------------------------------------------------------------------------- 1 | (ns webfui-examples.views.calculator-ajax 2 | (:require [webfui-examples.views.common :as common] 3 | [noir.content.getting-started]) 4 | (:use [noir.core :only [defpage]] 5 | [hiccup.core :only [html]])) 6 | 7 | (defpage "/calculator_ajax" [] 8 | (common/layout "calculator_ajax")) 9 | -------------------------------------------------------------------------------- /webfui-examples/src-clj/webfui_examples/views/calculator_many.clj: -------------------------------------------------------------------------------- 1 | (ns webfui-examples.views.calculator-many 2 | (:require [webfui-examples.views.common :as common] 3 | [noir.content.getting-started]) 4 | (:use [noir.core :only [defpage]] 5 | [hiccup.core :only [html]])) 6 | 7 | (defpage "/calculator_many" [] 8 | (common/layout "calculator_many")) 9 | -------------------------------------------------------------------------------- /webfui-examples/src-clj/webfui_examples/views/common.clj: -------------------------------------------------------------------------------- 1 | (ns webfui-examples.views.common 2 | (:use [noir.core :only [defpartial]] 3 | [hiccup.page :only [include-css include-js html5]] 4 | [hiccup.element :only [javascript-tag]])) 5 | 6 | (defpartial layout [name] 7 | (html5 8 | [:head 9 | [:title "webfui-examples"] 10 | (include-css (str "/css/" name ".css")) 11 | (javascript-tag "var CLOSURE_NO_DEPS = true;") 12 | (include-js (str "/js/" name ".js"))] 13 | [:body])) 14 | -------------------------------------------------------------------------------- /webfui-examples/src-clj/webfui_examples/views/inverse_kinematics.clj: -------------------------------------------------------------------------------- 1 | (ns webfui-examples.views.inverse-kinematics 2 | (:require [webfui-examples.views.common :as common] 3 | [noir.content.getting-started]) 4 | (:use [noir.core :only [defpage]] 5 | [hiccup.core :only [html]])) 6 | 7 | (defpage "/inverse_kinematics" [] 8 | (common/layout "inverse_kinematics")) 9 | -------------------------------------------------------------------------------- /webfui-examples/src-clj/webfui_examples/views/mouse_tracking.clj: -------------------------------------------------------------------------------- 1 | (ns webfui-examples.views.mouse-tracking 2 | (:require [webfui-examples.views.common :as common] 3 | [noir.content.getting-started]) 4 | (:use [noir.core :only [defpage]] 5 | [hiccup.core :only [html]])) 6 | 7 | (defpage "/mouse_tracking" [] 8 | (common/layout "mouse_tracking")) 9 | -------------------------------------------------------------------------------- /webfui-examples/src-clj/webfui_examples/views/scrolling.clj: -------------------------------------------------------------------------------- 1 | (ns webfui-examples.views.scrolling 2 | (:require [webfui-examples.views.common :as common] 3 | [noir.content.getting-started]) 4 | (:use [noir.core :only [defpage]] 5 | [hiccup.core :only [html]])) 6 | 7 | (defpage "/scrolling" [] 8 | (common/layout "scrolling")) 9 | -------------------------------------------------------------------------------- /webfui-examples/src-clj/webfui_examples/views/welcome.clj: -------------------------------------------------------------------------------- 1 | (ns webfui-examples.views.welcome 2 | (:require [webfui-examples.views.common :as common] 3 | [noir.content.getting-started]) 4 | (:use [noir.core :only [defpage]] 5 | [hiccup.core :only [html]])) 6 | 7 | (defpage "/welcome" [] 8 | (common/layout 9 | [:p "Welcome to webfui-examples"])) 10 | -------------------------------------------------------------------------------- /webfui-examples/src-cljs/add_two_numbers/core.cljs: -------------------------------------------------------------------------------- 1 | (ns webfui-examples.add-two-numbers.core 2 | (:use [webfui.framework :only [launch-app]]) 3 | (:use-macros [webfui.framework.macros :only [add-dom-watch]])) 4 | 5 | (defn render-all [state] 6 | (let [{:keys [a b]} state] 7 | [:div [:input#a {:watch :watch :value a}] 8 | " plus " 9 | [:input#b {:watch :watch :value b}] 10 | [:p " equals "] 11 | [:span (+ a b)]])) 12 | 13 | (defn valid-integer [s] 14 | (and (< (count s) 15) (re-matches #"^[0-9]+$" s))) 15 | 16 | (add-dom-watch :watch [state new-element] 17 | (let [{:keys [value id]} (second new-element)] 18 | (when (valid-integer value) 19 | {id (js/parseInt value)}))) 20 | 21 | (launch-app (atom {:a 0 :b 0}) render-all) 22 | 23 | 24 | -------------------------------------------------------------------------------- /webfui-examples/src-cljs/add_two_numbers_low_level/core.cljs: -------------------------------------------------------------------------------- 1 | (ns webfui-examples.add-two-numbers-low-level.core 2 | (:use [webfui.dom :only [defdom add-dom-watch]] 3 | [webfui.state-patches :only [patch]])) 4 | 5 | (def my-dom (atom nil)) 6 | 7 | (defdom my-dom) 8 | 9 | (def a 0) 10 | (def b 0) 11 | 12 | (defn render-all [old-dom] 13 | [:div [:input {:type :text :watch :a-watch :value a}] 14 | " plus " 15 | [:input {:type :text :watch :b-watch :value b}] 16 | [:p " equals "] 17 | [:span (+ a b)]]) 18 | 19 | (defn update-dom [] 20 | (swap! my-dom render-all)) 21 | 22 | (update-dom) 23 | 24 | (defn valid-integer [s] 25 | (and (< (count s) 15) (re-matches #"^[0-9]+$" s))) 26 | 27 | (add-dom-watch :a-watch 28 | (fn [old-element new-element] 29 | (let [new-a (get-in new-element [1 :value])] 30 | (when (valid-integer new-a) 31 | (def a (js/parseInt new-a)))) 32 | (update-dom))) 33 | 34 | (add-dom-watch :b-watch 35 | (fn [old-element new-element] 36 | (let [new-b (get-in new-element [1 :value])] 37 | (when (valid-integer new-b) 38 | (def b (js/parseInt new-b)))) 39 | (update-dom))) 40 | 41 | -------------------------------------------------------------------------------- /webfui-examples/src-cljs/calculator/core.cljs: -------------------------------------------------------------------------------- 1 | (ns webfui-examples.calculator.core 2 | (:use [webfui.framework :only [launch-app]] 3 | [webfui.utilities :only [get-attribute clicked]]) 4 | (:use-macros [webfui.framework.macros :only [add-mouse-watch]])) 5 | 6 | (def initial-state {:amount nil 7 | :amount-decimal nil 8 | :accumulator 0 9 | :operation nil 10 | :memory 0}) 11 | 12 | (def calculator-keys [[[:ac "AC" :ac] [:ms "MS" :ms] [:mr "MR" :mr] [:div "÷" :op]] 13 | [[:7 "7" :num] [:8 "8" :num] [:9 "9" :num] [:mult "×" :op]] 14 | [[:4 "4" :num] [:5 "5" :num] [:6 "6" :num] [:minus "-" :op]] 15 | [[:1 "1" :num] [:2 "2" :num] [:3 "3" :num] [:plus "+" :op]] 16 | [[:period "." :period] [:0 "0" :num] [:eq "=" :op]]]) 17 | 18 | (def operations {:div / 19 | :mult * 20 | :minus - 21 | :plus + 22 | :eq identity}) 23 | 24 | (defn right-shorten [s] 25 | (subs s 0 (dec (count s)))) 26 | 27 | (defn format-accumulator [accumulator] 28 | (loop [s (.toFixed accumulator 12)] 29 | (case (last s) 30 | \0 (recur (right-shorten s)) 31 | \. (right-shorten s) 32 | s))) 33 | 34 | (defn check-overflow [s] 35 | (cond (and (<= (count s) 12) (not= (last s) \.)) s 36 | (some (partial = \.) s) (recur (right-shorten s)) 37 | :else "OVERFLOW")) 38 | 39 | (defn render-display [{:keys [amount amount-decimal accumulator]}] 40 | (check-overflow (cond (not amount) (format-accumulator accumulator) 41 | amount-decimal (.toFixed amount amount-decimal) 42 | :else (str amount)))) 43 | 44 | (defn render-all [state] 45 | [:table [:tbody [:tr [:td {:colspan 4} 46 | [:div#display (render-display state)]]] 47 | (for [row calculator-keys] 48 | [:tr (for [[sym label mouse] row] 49 | [:td {:colspan ({:eq 2} sym 1)} 50 | [:div {:id sym 51 | :mouse mouse} 52 | label]])])]]) 53 | 54 | (add-mouse-watch :num [state first-element last-element] 55 | (when (clicked first-element last-element) 56 | (let [{:keys [amount amount-decimal]} state 57 | digit (js/parseInt (name (get-attribute first-element :id)))] 58 | (if amount-decimal 59 | {:amount (+ amount (/ digit 10 (apply * (repeat amount-decimal 10)))) 60 | :amount-decimal (inc amount-decimal)} 61 | {:amount (+ (* amount 10) digit)})))) 62 | 63 | (add-mouse-watch :op [state first-element last-element] 64 | (when (clicked first-element last-element) 65 | (let [{:keys [amount operation accumulator]} state] 66 | {:amount nil 67 | :amount-decimal nil 68 | :accumulator (if (and amount operation) 69 | ((operations operation) accumulator amount) 70 | (or amount accumulator)) 71 | :operation (get-attribute first-element :id)}))) 72 | 73 | (add-mouse-watch :period [state first-element last-element] 74 | (when (clicked first-element last-element) 75 | (when-not (:amount-decimal state) 76 | {:amount-decimal 0}))) 77 | 78 | (add-mouse-watch :ac [state first-element last-element] 79 | (when (clicked first-element last-element) 80 | (assoc initial-state :memory (:memory state)))) 81 | 82 | (add-mouse-watch :ms [state first-element last-element] 83 | (when (clicked first-element last-element) 84 | (let [{:keys [amount accumulator]} state] 85 | {:memory (or amount accumulator)}))) 86 | 87 | (add-mouse-watch :mr [state first-element last-element] 88 | (when (clicked first-element last-element) 89 | (let [{:keys [memory]} state] 90 | {:amount memory}))) 91 | 92 | (launch-app (atom initial-state) render-all) -------------------------------------------------------------------------------- /webfui-examples/src-cljs/calculator_ajax/core.cljs: -------------------------------------------------------------------------------- 1 | (ns webfui-examples.calculator-ajax.core 2 | (:use [webfui.framework :only [launch-app]] 3 | [webfui.utilities :only [get-attribute clicked]] 4 | [cljs.reader :only [read-string]]) 5 | (:use-macros [webfui.framework.macros :only [add-mouse-watch]]) 6 | (:require [goog.net.XhrIo :as xhr])) 7 | 8 | (def initial-state {:amount nil 9 | :amount-decimal nil 10 | :accumulator 0 11 | :operation nil 12 | :memory :unknown}) 13 | 14 | (def calculator-keys [[[:ac "AC" :ac] [:ms "MS" :ms] [:mr "MR" :mr] [:div "÷" :op]] 15 | [[:7 "7" :num] [:8 "8" :num] [:9 "9" :num] [:mult "×" :op]] 16 | [[:4 "4" :num] [:5 "5" :num] [:6 "6" :num] [:minus "-" :op]] 17 | [[:1 "1" :num] [:2 "2" :num] [:3 "3" :num] [:plus "+" :op]] 18 | [[:period "." :period] [:0 "0" :num] [:eq "=" :op]]]) 19 | 20 | (def operations {:div / 21 | :mult * 22 | :minus - 23 | :plus + 24 | :eq identity}) 25 | 26 | (defn right-shorten [s] 27 | (subs s 0 (dec (count s)))) 28 | 29 | (defn format-accumulator [accumulator] 30 | (loop [s (.toFixed accumulator 12)] 31 | (case (last s) 32 | \0 (recur (right-shorten s)) 33 | \. (right-shorten s) 34 | s))) 35 | 36 | (defn check-overflow [s] 37 | (cond (and (<= (count s) 12) (not= (last s) \.)) s 38 | (some (partial = \.) s) (recur (right-shorten s)) 39 | :else "OVERFLOW")) 40 | 41 | (defn render-display [{:keys [amount amount-decimal accumulator]}] 42 | (check-overflow (cond (not amount) (format-accumulator accumulator) 43 | amount-decimal (.toFixed amount amount-decimal) 44 | :else (str amount)))) 45 | 46 | (defn render-all [state] 47 | [:table [:tbody [:tr [:td {:colspan 4} 48 | [:div#display (render-display state)]]] 49 | (for [row calculator-keys] 50 | [:tr (for [[sym label mouse] row] 51 | [:td {:colspan ({:eq 2} sym 1)} 52 | [:div {:id sym 53 | :mouse mouse} 54 | label]])])]]) 55 | 56 | (add-mouse-watch :num [state first-element last-element] 57 | (when (clicked first-element last-element) 58 | (let [{:keys [amount amount-decimal]} state 59 | digit (js/parseInt (name (get-attribute first-element :id)))] 60 | (if amount-decimal 61 | {:amount (+ amount (/ digit 10 (apply * (repeat amount-decimal 10)))) 62 | :amount-decimal (inc amount-decimal)} 63 | {:amount (+ (* amount 10) digit)})))) 64 | 65 | (add-mouse-watch :op [state first-element last-element] 66 | (when (clicked first-element last-element) 67 | (let [{:keys [amount operation accumulator]} state] 68 | {:amount nil 69 | :amount-decimal nil 70 | :accumulator (if (and amount operation) 71 | ((operations operation) accumulator amount) 72 | (or amount accumulator)) 73 | :operation (get-attribute first-element :id)}))) 74 | 75 | (add-mouse-watch :period [state first-element last-element] 76 | (when (clicked first-element last-element) 77 | (when-not (:amount-decimal state) 78 | {:amount-decimal 0}))) 79 | 80 | (add-mouse-watch :ac [state first-element last-element] 81 | (when (clicked first-element last-element) 82 | (assoc initial-state :memory (:memory state)))) 83 | 84 | (add-mouse-watch :ms [state first-element last-element] 85 | (when (clicked first-element last-element) 86 | (let [{:keys [amount accumulator]} state] 87 | {:memory (or amount accumulator)}))) 88 | 89 | (add-mouse-watch :mr [state first-element last-element] 90 | (when (clicked first-element last-element) 91 | (let [{:keys [memory]} state] 92 | {:amount memory}))) 93 | 94 | (def my-state (atom initial-state)) 95 | 96 | (defn send [safe-state method uri fun] 97 | (xhr/send uri 98 | (fn [event] 99 | (let [response (.-target event)] 100 | (if (.isSuccess response) 101 | (fun (.getResponseText response)) 102 | (reset! my-state safe-state)))) 103 | (name method))) 104 | 105 | (defn memory-loaded [text] 106 | (let [memory (read-string text)] 107 | (swap! my-state assoc :memory memory :amount memory))) 108 | 109 | (defn memory-saved [] 110 | (swap! my-state assoc :memory :unknown)) 111 | 112 | (add-watch my-state :my-watch 113 | (fn [_ _ old new] 114 | (let [{:keys [amount memory]} new] 115 | (when (= amount :unknown) 116 | (if (= (:amount old) :unknown) 117 | (reset! my-state old) 118 | (send old :get "memory" memory-loaded))) 119 | (when (not= memory :unknown) 120 | (if (not= (:memory old) :unknown) 121 | (reset! my-state old) 122 | (send old :put (str "memory/" memory) memory-saved)))))) 123 | 124 | (launch-app my-state render-all) -------------------------------------------------------------------------------- /webfui-examples/src-cljs/calculator_many/core.cljs: -------------------------------------------------------------------------------- 1 | (ns webfui-examples.calculator-many.core 2 | (:use [webfui.framework :only [launch-app]] 3 | [webfui.utilities :only [get-attribute clicked]]) 4 | (:use-macros [webfui.framework.macros :only [add-mouse-watch]])) 5 | 6 | (def calc-rows 4) 7 | (def calc-columns 10) 8 | (def calc-num (* calc-rows calc-columns)) 9 | 10 | (def initial-state (vec (repeat calc-num 11 | {:amount nil 12 | :amount-decimal nil 13 | :accumulator 0 14 | :operation nil 15 | :memory 0}))) 16 | 17 | (def calculator-keys [[[:ac "AC" :ac] [:ms "MS" :ms] [:mr "MR" :mr] [:div "÷" :op]] 18 | [[:7 "7" :num] [:8 "8" :num] [:9 "9" :num] [:mult "×" :op]] 19 | [[:4 "4" :num] [:5 "5" :num] [:6 "6" :num] [:minus "-" :op]] 20 | [[:1 "1" :num] [:2 "2" :num] [:3 "3" :num] [:plus "+" :op]] 21 | [[:period "." :period] [:0 "0" :num] [:eq "=" :op]]]) 22 | 23 | (def operations {:div / 24 | :mult * 25 | :minus - 26 | :plus + 27 | :eq identity}) 28 | 29 | (defn right-shorten [s] 30 | (subs s 0 (dec (count s)))) 31 | 32 | (defn format-accumulator [accumulator] 33 | (loop [s (.toFixed accumulator 12)] 34 | (case (last s) 35 | \0 (recur (right-shorten s)) 36 | \. (right-shorten s) 37 | s))) 38 | 39 | (defn check-overflow [s] 40 | (cond (and (<= (count s) 12) (not= (last s) \.)) s 41 | (some (partial = \.) s) (recur (right-shorten s)) 42 | :else "OVERFLOW")) 43 | 44 | (defn render-display [{:keys [amount amount-decimal accumulator]}] 45 | (check-overflow (cond (not amount) (format-accumulator accumulator) 46 | amount-decimal (.toFixed amount amount-decimal) 47 | :else (str amount)))) 48 | 49 | (defn render-all [state] 50 | [:table.grid [:tbody (for [row (range calc-rows)] 51 | [:tr (for [column (range calc-columns)] 52 | (let [calc-cur (+ (* row calc-columns) column)] 53 | [:td [:table.calc [:tbody [:tr [:td {:colspan 4} 54 | [:div#display 55 | (render-display (state calc-cur))]]] 56 | (for [row calculator-keys] 57 | [:tr (for [[sym label mouse] row] 58 | [:td {:colspan ({:eq 2} sym 1)} 59 | [:div {:id sym 60 | :data calc-cur 61 | :mouse mouse} 62 | label]])])]]]))])]]) 63 | 64 | (add-mouse-watch :num [state first-element last-element] 65 | (when (clicked first-element last-element) 66 | (let [index (get-attribute first-element :data) 67 | {:keys [amount amount-decimal]} (state index) 68 | digit (js/parseInt (name (get-attribute first-element :id)))] 69 | {index (if amount-decimal 70 | {:amount (+ amount (/ digit 10 (apply * (repeat amount-decimal 10)))) 71 | :amount-decimal (inc amount-decimal)} 72 | {:amount (+ (* amount 10) digit)})}))) 73 | 74 | (add-mouse-watch :op [state first-element last-element] 75 | (when (clicked first-element last-element) 76 | (let [index (get-attribute first-element :data) 77 | {:keys [amount operation accumulator]} (state index)] 78 | {index {:amount nil 79 | :amount-decimal nil 80 | :accumulator (if (and amount operation) 81 | ((operations operation) accumulator amount) 82 | (or amount accumulator)) 83 | :operation (get-attribute first-element :id)}}))) 84 | 85 | (add-mouse-watch :period [state first-element last-element] 86 | (when (clicked first-element last-element) 87 | (let [index (get-attribute first-element :data)] 88 | {index (when-not (:amount-decimal state) 89 | {:amount-decimal 0})}))) 90 | 91 | (add-mouse-watch :ac [state first-element last-element] 92 | (when (clicked first-element last-element) 93 | (let [index (get-attribute first-element :data)] 94 | {index (assoc (initial-state index) :memory (:memory (state index)))}))) 95 | 96 | (add-mouse-watch :ms [state first-element last-element] 97 | (when (clicked first-element last-element) 98 | (let [index (get-attribute first-element :data)] 99 | {index (let [{:keys [amount accumulator]} (state index)] 100 | {:memory (or amount accumulator)})}))) 101 | 102 | (add-mouse-watch :mr [state first-element last-element] 103 | (when (clicked first-element last-element) 104 | (let [index (get-attribute first-element :data)] 105 | {index (let [{:keys [memory]} (state index)] 106 | {:amount memory})}))) 107 | 108 | (launch-app (atom initial-state) render-all) -------------------------------------------------------------------------------- /webfui-examples/src-cljs/inverse_kinematics/core.cljs: -------------------------------------------------------------------------------- 1 | (ns webfui-examples.inverse-kinematics.core 2 | (:use [webfui.framework :only [launch-app]]) 3 | (:use-macros [webfui.framework.macros :only [add-mouse-watch]])) 4 | 5 | (defn average [& numbers] 6 | (/ (apply + numbers) (count numbers))) 7 | 8 | (defn average-points [& points] 9 | [(apply average (map first points)) (apply average (map second points))]) 10 | 11 | (defn translate [[tx ty] [x y]] 12 | [(+ x tx) (+ y ty)]) 13 | 14 | (defn segment-angle [[x1 y1] [x2 y2]] 15 | (js/Math.atan2 (- y1 y2) (- x2 x1))) 16 | 17 | (defn sqr [x] 18 | (* x x)) 19 | 20 | (defn distance-squared [[x1 y1] [x2 y2]] 21 | (+ (sqr (- x1 x2)) (sqr (- y1 y2)))) 22 | 23 | (defn distance [[x1 y1] [x2 y2]] 24 | (js/Math.sqrt (+ (sqr (- x1 x2)) (sqr (- y1 y2))))) 25 | 26 | (defn normalize-angle [ang] 27 | (let [p js/Math.PI 28 | p2 (* p 2)] 29 | (cond (< ang (- p)) (normalize-angle (+ ang p2)) 30 | (>= ang p) (normalize-angle (- ang p2)) 31 | :else ang))) 32 | 33 | (defn angle-delta [angle-1 angle-2] 34 | (let [angle (normalize-angle (- angle-1 angle-2))] 35 | (if (<= angle js/Math.PI) 36 | angle 37 | (- (* js/Math.PI 2) angle)))) 38 | 39 | (defn move-point [[x y] angle dist] 40 | [(+ x (* (js/Math.cos angle) dist)) (- y (* (js/Math.sin angle) dist))]) 41 | 42 | (defn negative-point [[x y]] 43 | [(- x) (- y)]) 44 | 45 | (defn dynamic-goal-tree [tree] 46 | (let [[type from-point & rest] tree] 47 | (case type 48 | :branch (let [to-point (apply average-points (map second rest)) 49 | angle (segment-angle from-point to-point)] 50 | {:from-point from-point 51 | :to-point to-point 52 | :children (for [child rest] 53 | (let [[_ from-point-child] child 54 | angle-child (segment-angle from-point from-point-child) 55 | dist-child (distance from-point from-point-child) 56 | relative-angle (normalize-angle (- angle-child angle))] 57 | (assoc (dynamic-goal-tree child) 58 | :relative-angle relative-angle 59 | :distance dist-child)))}) 60 | :leaf {:from-point from-point 61 | :to-point (first rest)}))) 62 | 63 | (defn static-goal-tree [tree] 64 | (let [{:keys [from-point to-point children]} tree] 65 | (if (seq children) 66 | (concat [:branch from-point] 67 | (map static-goal-tree children)) 68 | [:leaf from-point to-point]))) 69 | 70 | (defn procrustes [& point-vectors] 71 | (let [corrected-points (for [points point-vectors] 72 | (let [[cx cy] (average-points points) 73 | translated (map (partial translate [(- cx) (- cy)]) points) 74 | scale (js/Math.sqrt (apply average (map (partial distance-squared [0 0]) translated)))] 75 | (for [[x y] translated] 76 | [(/ x scale) (/ y scale)]))) 77 | totals (for [[[x y] [w z]] (apply map vector corrected-points)] 78 | [(- (* w y) (* z x)) (+ (* w x) (* z y))]) 79 | numerator (apply + (map first totals)) 80 | denominator (apply + (map second totals))] 81 | (js/Math.atan2 numerator denominator))) 82 | 83 | (defn anneal-step [depth dtree goal-from-point] 84 | (let [{:keys [from-point to-point children]} dtree] 85 | (if (and (pos? depth) (> (distance-squared from-point goal-from-point) 0.0001)) 86 | (let [dist (distance from-point to-point) 87 | too-close (< dist 40) 88 | angle (segment-angle from-point to-point) 89 | angle-new (if too-close 90 | angle 91 | (segment-angle goal-from-point to-point)) 92 | delta (angle-delta angle angle-new) 93 | child-count (count children)] 94 | (if (and (> delta 0.01) (<= child-count 1)) 95 | (assoc dtree :from-point (move-point to-point (+ angle-new js/Math.PI) dist)) 96 | (let [points (for [child children] 97 | (let [{:keys [relative-angle distance]} child] 98 | (move-point goal-from-point (+ angle-new relative-angle) distance))) 99 | children-new (map (partial anneal-step (dec depth)) children points) 100 | points-new (map :from-point children-new) 101 | to-point-new (if (zero? child-count) 102 | (move-point goal-from-point angle-new dist) 103 | (apply average-points points-new)) 104 | angle-back (if too-close 105 | (+ angle js/Math.PI) 106 | (segment-angle to-point-new goal-from-point)) 107 | from-point-back (move-point to-point-new 108 | (if (<= 1 child-count) 109 | angle-back 110 | (+ angle-back (procrustes points points-new)) dist) 111 | dist)] 112 | (assoc dtree 113 | :from-point from-point-back 114 | :to-point to-point-new 115 | :children children-new)))) 116 | dtree))) 117 | 118 | (defn anneal [dtree goal-from-point] 119 | (let [breadth-first (if (and (= (count (:children dtree)) 1) (= (count (:children (first (:children dtree)))) 1)) 120 | (reduce (fn [acc n] 121 | (let [{:keys [from-point]} acc] 122 | (anneal-step 2 123 | acc 124 | goal-from-point))) 125 | dtree 126 | (range 30)) 127 | dtree) 128 | increasing-stability (reduce (fn [acc n] 129 | (let [{:keys [from-point]} acc] 130 | (anneal-step 1000 131 | acc 132 | (cond (< n 10) goal-from-point 133 | (< n 20) (average-points from-point goal-from-point) 134 | :else from-point)))) 135 | breadth-first 136 | (range 30))] 137 | increasing-stability)) 138 | 139 | #_(def initial-state {:tree (dynamic-goal-tree [:branch [50 50] 140 | [:leaf [100 100] [160 140]]])}) 141 | 142 | #_(def initial-state {:tree (dynamic-goal-tree [:branch [50 50] 143 | [:leaf [110 90] [160 140]] 144 | [:branch [:leaf [120,200] [200,200]]]])}) 145 | 146 | (defn connect-pinholes [skeleton] 147 | (assoc skeleton 148 | :boxes 149 | (let [{:keys [boxes pins]} skeleton] 150 | (reduce (fn [boxes pin] 151 | (let [[[box-from-index pin-from-index] [box-to-index pin-to-index]] pin] 152 | (update-in boxes 153 | [box-to-index :pinholes pin-to-index] 154 | (fn [_] 155 | (let [box-from (boxes box-from-index) 156 | pin-from ((:pinholes box-from) pin-from-index) 157 | box-to (boxes box-to-index) 158 | {:keys [left top]} box-to] 159 | [(- (+ (:left box-from) (first pin-from)) left) (- (+ (:top box-from) (second pin-from)) top)]))))) 160 | boxes 161 | pins)))) 162 | 163 | #_(def initial-state {:skeleton {:boxes [{:left 50 164 | :top 50 165 | :width 50 166 | :height 150 167 | :color :red 168 | :pinholes [[37 137]] 169 | :angle 0 170 | } 171 | {:left 75 172 | :top 175 173 | :width 50 174 | :height 125 175 | :color :blue 176 | :pinholes [[12 12]] 177 | :angle 0}] 178 | :pins [[[0 0] [1 0]]]}}) 179 | 180 | (def initial-state {:skeleton (connect-pinholes {:boxes [{:left 400 181 | :top 50 182 | :width 80 183 | :height 150 184 | :color :red 185 | :pinholes [[40 140]] 186 | :angle 0} 187 | {:left 350 188 | :top 175 189 | :width 180 190 | :height 250 191 | :color :blue 192 | :pinholes [nil nil nil nil nil] 193 | :angle 0} 194 | {:left 200 195 | :top 175 196 | :width 180 197 | :height 50 198 | :color :green 199 | :pinholes [[170 25] nil] 200 | :angle 0} 201 | {:left 80 202 | :top 180 203 | :width 140 204 | :height 40 205 | :color :yellow 206 | :pinholes [[135 20]] 207 | :angle 0} 208 | {:left 500 209 | :top 175 210 | :width 180 211 | :height 50 212 | :color :orange 213 | :pinholes [[10 25] nil] 214 | :angle 0} 215 | {:left 660 216 | :top 180 217 | :width 140 218 | :height 40 219 | :color :purple 220 | :pinholes [[10 20]] 221 | :angle 0} 222 | {:left 350 223 | :top 400 224 | :width 70 225 | :height 160 226 | :color :grey 227 | :pinholes [[35 10] nil] 228 | :angle 0} 229 | {:left 460 230 | :top 400 231 | :width 70 232 | :height 160 233 | :color :red 234 | :pinholes [[35 10] nil] 235 | :angle 0} 236 | {:left 360 237 | :top 540 238 | :width 50 239 | :height 140 240 | :color :purple 241 | :pinholes [[35 10]] 242 | :angle 0} 243 | {:left 470 244 | :top 540 245 | :width 50 246 | :height 140 247 | :color :green 248 | :pinholes [[35 10]] 249 | :angle 0} 250 | {:left 700 251 | :top 30 252 | :width 100 253 | :height 100 254 | :color :black 255 | :pinholes [] 256 | :angle 0 257 | } 258 | 259 | ] 260 | :pins [[[0 0] [1 0]] 261 | [[2 0] [1 1]] 262 | [[3 0] [2 1]] 263 | [[4 0] [1 2]] 264 | [[5 0] [4 1]] 265 | [[6 0] [1 3]] 266 | [[7 0] [1 4]] 267 | [[8 0] [6 1]] 268 | [[9 0] [7 1]] 269 | ]})}) 270 | 271 | (defn pin-positions [pins] 272 | (into {} 273 | (concat pins (map (comp vec reverse) pins)))) 274 | 275 | (defn counterpoint [width height point] 276 | (let [[x y] point 277 | cx (/ width 2) 278 | cy (/ height 2)] 279 | (cond (or (> (js/Math.abs (- x cx)) (/ cx 2)) (> (js/Math.abs (- y cy)) (/ cy 2))) [(- width x) (- height y)] 280 | (and (< x cx) (< y cy)) (average-points point [width height]) 281 | (< x cx) (average-points point [width 0]) 282 | (< y cy) (average-points point [0 height]) 283 | :else (average-points point [0 0])))) 284 | 285 | (defn skeleton-to-annotated-tree [skeleton index from-point from-pin] 286 | (let [{:keys [boxes pins]} skeleton 287 | pinpos (pin-positions pins)] 288 | ((fn f [index from-point from-pin] 289 | (let [box (boxes index) 290 | {:keys [left top pinholes width height angle]} box 291 | child-pinhole-indexes (filter (partial not= from-pin) (range (count pinholes))) 292 | child-pos (map pinholes child-pinhole-indexes) 293 | to-point (if (seq child-pos) 294 | (apply average-points child-pos) 295 | (counterpoint width height from-point)) 296 | top-left-corner [left top] 297 | relative-from-angle (segment-angle [0 0] from-point) 298 | from-distance (distance [0 0] from-point) 299 | relative-to-angle (segment-angle [0 0] to-point) 300 | to-distance (distance [0 0] to-point)] 301 | {:from-point (move-point top-left-corner (+ angle relative-from-angle) from-distance) 302 | :to-point (move-point top-left-corner (+ angle relative-to-angle) to-distance) 303 | :top-left-corner [left top] 304 | :box-index index 305 | :children (for [idx child-pinhole-indexes] 306 | (let [[child-box-index child-pinhole] (pinpos [index idx]) 307 | child-box (boxes child-box-index)] 308 | (f child-box-index ((child-box :pinholes) child-pinhole) child-pinhole)))})) 309 | index 310 | from-point 311 | from-pin))) 312 | 313 | (defn annotated-tree-to-static-tree [tree] 314 | (let [{:keys [from-point to-point children]} tree] 315 | (if (seq children) 316 | (concat [:branch from-point] (map annotated-tree-to-static-tree children)) 317 | [:leaf from-point to-point]))) 318 | 319 | (defn adjust-annotated-tree [tree stree] 320 | (let [[type from-point-new & rest] stree 321 | {:keys [children]} tree] 322 | (case type 323 | :leaf (assoc tree 324 | :from-point-new from-point-new 325 | :to-point-new (first rest)) 326 | :branch (assoc tree 327 | :from-point-new from-point-new 328 | :to-point-new (apply average-points (map second rest)) 329 | :children (map adjust-annotated-tree children rest))))) 330 | 331 | (defn reposition-rectangle [& foo] 332 | (let [[top-left-corner angle from-point to-point from-point-new to-point-new] foo] 333 | (let [origin-angle (segment-angle from-point top-left-corner) 334 | spine-angle (segment-angle from-point to-point) 335 | spine-angle-new (segment-angle from-point-new to-point-new) 336 | angle-diff (- spine-angle-new spine-angle) 337 | origin-angle-new (+ origin-angle angle-diff) 338 | origin-distance (distance from-point top-left-corner)] 339 | [(move-point from-point-new origin-angle-new origin-distance) (+ angle angle-diff)]))) 340 | 341 | 342 | (defn adjust-skeleton [skeleton tree] 343 | (let [{:keys [boxes]} skeleton 344 | boxes-new ((fn f [boxes tree] 345 | (let [{:keys [box-index from-point to-point from-point-new to-point-new children]} tree 346 | box (boxes box-index) 347 | {:keys [left top angle]} box 348 | [[left-new top-new] angle-new] (reposition-rectangle [left top] angle from-point to-point from-point-new to-point-new)] 349 | (reduce f (assoc boxes box-index (assoc box :left left-new :top top-new :angle angle-new)) children))) 350 | boxes 351 | tree)] 352 | (assoc skeleton :boxes boxes-new))) 353 | 354 | (defn render-tree [kin] 355 | (let [[type p1 & rest] kin 356 | [x1 y1] p1] 357 | (concat 358 | [[:div.node {:style {:left x1 :top y1}} "5"]] 359 | (case type 360 | :leaf (let [[[x2 y2]] rest] 361 | [[:div.node.destination {:style {:left x2 :top y2}} "3"]]) 362 | :branch (map render-tree rest))))) 363 | 364 | (defn render-skeleton [skeleton] 365 | (let [{:keys [boxes pins]} skeleton] 366 | (concat (map-indexed (fn [n box] 367 | (let [{:keys [left top width height color angle]} box] 368 | [:div.box {:mouse :box-mouse 369 | :data {:index n 370 | :angle angle} 371 | :style {:left left 372 | :top top 373 | :width width 374 | :height height 375 | :background-color color 376 | :-webkit-transform (str "rotate(" (- angle) "rad)")}}])) 377 | boxes) 378 | [] ;pin stuff here... 379 | ))) 380 | 381 | (defn render-all [state] 382 | (let [{:keys [tree]} state] 383 | [:div [:button {:mouse :push} "push"] (render-tree (static-goal-tree tree))])) 384 | 385 | (defn render-all [state] 386 | (let [{:keys [skeleton tree]} state] 387 | [:div (concat (render-skeleton skeleton) 388 | #_(when tree 389 | (render-tree tree)))])) 390 | 391 | (defn correct-coordinate [[x y] angle] 392 | (let [sa (segment-angle [0 0] [x y]) 393 | d (distance [0 0] [x y]) 394 | angle-new (- sa angle)] 395 | (move-point [0 0] angle-new d))) 396 | 397 | (defn pinhole-pos [box pinhole-index] 398 | (let [{:keys [top left angle pinholes]} box 399 | pos (pinholes pinhole-index) 400 | angle-old (segment-angle [0 0] pos) 401 | dist (distance [0 0] pos) 402 | angle-new (+ angle angle-old)] 403 | (move-point [left top] angle-new dist))) 404 | 405 | (defn reconnect-pinholes [skeleton] 406 | (let [{:keys [boxes pins]} skeleton] 407 | (assoc skeleton 408 | :boxes 409 | (reduce (fn [boxes from-index] 410 | (let [pinpos (pin-positions pins) 411 | pin (filter (fn [[[a b] [c d]]] 412 | (and (= a from-index) 413 | (< c a))) 414 | pinpos)] 415 | (if (seq pin) 416 | (let [[[[_ from-pinhole-index] [to-box-index to-pinhole-index]]] pin 417 | from-box (boxes from-index) 418 | to-box (boxes to-box-index) 419 | {:keys [top left]} from-box 420 | [x1 y1] (pinhole-pos from-box from-pinhole-index) 421 | [x2 y2] (pinhole-pos to-box to-pinhole-index)] 422 | (assoc boxes from-index 423 | (assoc from-box 424 | :left (- (+ left x2) x1) 425 | :top (- (+ top y2) y1)))) 426 | boxes))) 427 | boxes 428 | (range (count boxes)))))) 429 | 430 | (add-mouse-watch [:box-mouse :incremental] [state first-element last-element points] 431 | (let [{:keys [offset data]} (second first-element) 432 | [ox oy] offset 433 | relative-position (translate [(- ox) (- oy)] (first points)) 434 | {:keys [skeleton]} state 435 | {:keys [index angle]} data 436 | [x y] (correct-coordinate relative-position angle) 437 | [p2x p2y :as ending-point] (last points) 438 | tree (skeleton-to-annotated-tree skeleton index [x y] nil) 439 | stree (annotated-tree-to-static-tree tree) 440 | dtree (dynamic-goal-tree stree) 441 | annealed (anneal dtree [p2x p2y]) 442 | stree-new (static-goal-tree annealed) 443 | tree-new (adjust-annotated-tree tree stree-new) 444 | skeleton-new (reconnect-pinholes (adjust-skeleton skeleton tree-new))] 445 | {:skeleton skeleton-new 446 | :tree stree-new}) ) 447 | 448 | (launch-app (atom initial-state) render-all) 449 | 450 | -------------------------------------------------------------------------------- /webfui-examples/src-cljs/mouse_tracking/core.cljs: -------------------------------------------------------------------------------- 1 | (ns webfui-examples.mouse-tracking.core 2 | (:use [webfui.framework :only [launch-app]] 3 | [webfui.utilities :only [get-attribute]]) 4 | (:use-macros [webfui.framework.macros :only [add-mouse-watch]])) 5 | 6 | (defn render-bubble [point size attributes] 7 | (let [[x y] point 8 | wid (* 50 (js/Math.sqrt size))] 9 | [:div (merge attributes 10 | {:style (merge {:left (- x (/ wid 2)) 11 | :top (- y (/ wid 2)) 12 | :width wid 13 | :height wid 14 | :line-height wid 15 | :background-color (str "rgb(" (* 4 size size) ",0," (- 260 (* 4 size size)) ")")} 16 | (:style attributes))}) 17 | size])) 18 | 19 | (defn render-all [state] 20 | (let [{:keys [dragged-item sizes]} state 21 | {:keys [point to from]} dragged-item] 22 | (list [:div.layer (for [index (range 13)] 23 | (render-bubble (map (fn [fun] 24 | (+ 450 (* 400 (fun (* index 0.4833))))) 25 | [js/Math.cos js/Math.sin]) 26 | (sizes index) 27 | {:style {:opacity (if dragged-item 28 | ({to 1 from 1} index 0.4) 29 | 1)} 30 | :data index 31 | :mouse :mouse}))] 32 | (if dragged-item ;this is a bit ugly because of a couple of webfui bug fixes that are pending 33 | [:div.layer (render-bubble point 34 | (sizes from) 35 | {:class :dragged 36 | :style {:background-color :black}})] 37 | [:div.layer])))) 38 | 39 | (add-mouse-watch :mouse [state first-element last-element points] 40 | (let [sizes (state :sizes) 41 | [from-index to-index] (map (comp :data second) [first-element last-element]) 42 | to-index (and (not= from-index to-index) to-index)] 43 | {:dragged-item (when (get-attribute first-element :active) 44 | {:point (last points) 45 | :from from-index 46 | :to to-index}) 47 | :sizes (if to-index 48 | (update-in sizes [to-index] + (sizes from-index)) 49 | sizes)})) 50 | 51 | (launch-app (atom {:sizes (vec (repeat 13 1))}) render-all) 52 | -------------------------------------------------------------------------------- /webfui-examples/src-cljs/scrolling/core.cljs: -------------------------------------------------------------------------------- 1 | (ns webfui-examples.scrolling.core 2 | (:use [webfui.framework :only [launch-app]] 3 | [webfui.plugin.core :only [register-plugin]] 4 | [webfui.plugin.scrolling :only [scrolling]] 5 | [webfui.utilities :only [get-attribute]]) 6 | (:use-macros [webfui.framework.macros :only [add-dom-watch]])) 7 | 8 | (register-plugin (scrolling.)) 9 | 10 | (defn render-all [state] 11 | (let [{:keys [position]} state] 12 | [:center [:div {:style {:width 200 13 | :height 200 14 | :overflow :auto} 15 | :scroll-top position 16 | :watch :container} 17 | [:div {:style {:height 400 18 | :background-color :cyan}}]] 19 | [:p [:input {:watch :text-field :value position}]]])) 20 | 21 | (defn try-parse [s] 22 | (let [n (js/parseInt s)] 23 | (if (js/isNaN n) 24 | 0 25 | n))) 26 | 27 | (add-dom-watch :container [state new-element] 28 | {:position (get-attribute new-element :scroll-top)}) 29 | 30 | (add-dom-watch :text-field [state new-element] 31 | {:position (try-parse (get-attribute new-element :value))}) 32 | 33 | (launch-app (atom {:position 40}) render-all) --------------------------------------------------------------------------------