view src/indyvon/core.clj @ 162:4aa8979938ee

Faster dynamic bindings.
author Mikhail Kryshen <mikhail@kryshen.net>
date Mon, 24 Nov 2014 02:09:13 +0300
parents d149f03d1feb
children 5b80af180da0
line source
1 ;;
2 ;; Copyright 2010-2014 Mikhail Kryshen <mikhail@kryshen.net>
3 ;;
4 ;; This file is part of Indyvon.
5 ;;
6 ;; Indyvon is free software: you can redistribute it and/or modify it
7 ;; under the terms of the GNU Lesser General Public License version 3
8 ;; only, as published by the Free Software Foundation.
9 ;;
10 ;; Indyvon is distributed in the hope that it will be useful, but
11 ;; WITHOUT ANY WARRANTY; without even the implied warranty of
12 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 ;; Lesser General Public License for more details.
14 ;;
15 ;; You should have received a copy of the GNU Lesser General Public
16 ;; License along with Indyvon. If not, see
17 ;; <http://www.gnu.org/licenses/>.
18 ;;
20 (ns indyvon.core
21 (:import
22 (java.awt Graphics2D RenderingHints Component Color Font Shape
23 Rectangle Cursor EventQueue)
24 (java.awt.geom AffineTransform Point2D$Double Rectangle2D$Double Area)
25 (java.awt.event MouseListener MouseMotionListener
26 MouseWheelListener MouseWheelEvent)
27 (java.awt.font FontRenderContext)
28 java.util.concurrent.ConcurrentMap
29 com.google.common.collect.MapMaker))
31 ;;
32 ;; View context
33 ;;
35 (def ^:dynamic ^Graphics2D *graphics*)
37 (def ^:dynamic ^FontRenderContext *font-context*
38 "FontRenderContext to use when Graphics2D is not available."
39 (FontRenderContext.
40 nil
41 RenderingHints/VALUE_TEXT_ANTIALIAS_DEFAULT
42 RenderingHints/VALUE_FRACTIONALMETRICS_DEFAULT))
44 (def ^:dynamic *width*
45 "Width of the rendering area.")
47 (def ^:dynamic *height*
48 "Height of the rendering area.")
50 (def ^:dynamic ^Shape *clip*)
52 (def ^:dynamic ^Shape *input-clip*
53 "Clipping area used for dispatching pointer events (intersected with
54 *clip*). If nil, *clip* will be used.")
56 (def ^:dynamic *time*
57 "Timestamp of the current frame (in nanoseconds).")
59 (def ^:dynamic *scene*
60 "Encloses state that should be retained between repaints.")
62 (def ^:dynamic *states*
63 "Transient scene states, a map.")
65 (def ^:dynamic *event-dispatcher*)
67 (def ^:dynamic ^AffineTransform *initial-transform*
68 "Initial transform associated with the graphics context.")
70 (def ^:dynamic ^AffineTransform *inverse-initial-transform*
71 "Inversion of the initial transform associated with the graphics
72 context.")
74 (defrecord Theme [fore-color back-color alt-back-color border-color
75 shadow-color font])
77 ;; REMIND: use system colors, see java.awt.SystemColor.
78 (defn default-theme []
79 (Theme. Color/BLACK
80 Color/WHITE
81 (Color. 0xDD 0xDD 0xDD)
82 (Color. 0 0 0xCC)
83 (Color. 0x44 0x44 0x44)
84 (Font. "Sans" Font/PLAIN 12)))
86 (def ^:dynamic *theme* (default-theme))
88 ;;
89 ;; Core protocols and types
90 ;;
92 (defprotocol View
93 "Basic UI element."
94 (render! [view]
95 "Draws the view in the current *graphics* context.")
96 (geometry [view]
97 "Returns the preferred Geometry for the view."))
99 (defprotocol Geometry
100 "Describes geometry of a View. Prefer using the available
101 implementations (Size, FixedGeometry and NestedGeometry) over
102 extending this protocol directly as it is likely to be changed in
103 the future versions."
104 (width [geom] [geom height])
105 (height [geom] [geom width])
106 (anchor-x [geom h-align width]
107 "Returns the x coordinate of the anchor point for the specified
108 horizontal alignment and width, h-align could be :left, :center
109 or :right.")
110 (anchor-y [geom v-align height]
111 "Returns the y coordinate of the anchor point for the specified
112 vertical alignment and height, v-align could be :top, :center
113 or :bottom."))
115 (defn- emit-align-xy [align size first center last]
116 `(case ~align
117 ~first 0
118 ~center (/ ~size 2)
119 ~last ~size))
121 ;; Define as macro to avoid unnecessary calculation of width or height.
122 (defmacro align-x
123 ([align inner outer]
124 `(align-x ~align (- ~outer ~inner)))
125 ([align width]
126 (emit-align-xy align width :left :center :right)))
128 (defmacro align-y
129 ([align inner outer]
130 `(align-y ~align (- ~outer ~inner)))
131 ([align height]
132 (emit-align-xy align height :top :center :bottom)))
134 (defrecord Size [width height]
135 Geometry
136 (width [_] width)
137 (width [_ _] width)
138 (height [_] height)
139 (height [_ _] height)
140 (anchor-x [_ h-align width]
141 (align-x h-align width))
142 (anchor-y [_ v-align height]
143 (align-y v-align height)))
145 (defrecord FixedGeometry [ax ay width height]
146 Geometry
147 (width [_] width)
148 (width [_ _] width)
149 (height [_] height)
150 (height [_ _] height)
151 (anchor-x [_ _ _] ax)
152 (anchor-y [_ _ _] ay))
154 (defrecord NestedGeometry [geometry top left bottom right]
155 Geometry
156 (width [_]
157 (+ left right (width geometry)))
158 (width [_ h]
159 (+ left right (width geometry (- h top bottom))))
160 (height [_]
161 (+ top bottom (height geometry)))
162 (height [_ w]
163 (+ top bottom (height geometry (- w left right))))
164 (anchor-x [_ h-align w]
165 (+ left (anchor-x geometry h-align (- w left right))))
166 (anchor-y [_ v-align h]
167 (+ top (anchor-y geometry v-align (- h top bottom)))))
169 (defrecord ScaledGeometry [geometry sx sy]
170 Geometry
171 (width [_]
172 (* sx (width geometry)))
173 (width [_ h]
174 (* sx (width geometry (/ h sy))))
175 (height [_]
176 (* sy (height geometry)))
177 (height [_ w]
178 (* sy (height geometry (/ w sx))))
179 (anchor-x [_ h-align w]
180 (* sx (anchor-x geometry h-align (/ w sx))))
181 (anchor-y [_ v-align h]
182 (* sy (anchor-y geometry v-align (/ h sy)))))
184 (defrecord TransposedGeometry [geometry]
185 Geometry
186 (width [_]
187 (height geometry))
188 (width [_ h]
189 (height geometry h))
190 (height [_]
191 (width geometry))
192 (height [_ w]
193 (width geometry w))
194 (anchor-x [_ h-align w]
195 (anchor-y geometry
196 (get {:left :top
197 :center :center
198 :right :bottom}
199 h-align)
200 w))
201 (anchor-y [_ v-align h]
202 (anchor-x geometry
203 (get {:top :left
204 :center :center
205 :bottom :right}
206 v-align)
207 h)))
209 ;; (defn ^:private to-integer
210 ;; ^long [align x]
211 ;; (if (integer? x)
212 ;; x
213 ;; (let [x (double x)]
214 ;; (Math/round
215 ;; (case align
216 ;; (:top :left) (Math/floor x)
217 ;; :center x
218 ;; (:bottom :right) (Math/ceil x))))))
220 ;; (defrecord IntegerGeometry [geometry]
221 ;; Geometry
222 ;; (width [_]
223 ;; (to-integer :right (width geometry)))
224 ;; (width [_ h]
225 ;; (to-integer :right (width geometry h)))
226 ;; (height [_]
227 ;; (to-integer :bottom (height geometry)))
228 ;; (height [_ w]
229 ;; (to-integer :bottom (height geometry w)))
230 ;; (anchor-x [_ h-align w]
231 ;; (to-integer h-align (anchor-x geometry h-align w)))
232 ;; (anchor-y [_ v-align h]
233 ;; (to-integer v-align (anchor-y geometry v-align h))))
235 ;; TODO: modifiers
236 (defrecord MouseEvent [id when x y x-on-screen y-on-screen button
237 wheel-rotation transform component])
239 ;; TODO: KeyEvent
241 (defprotocol EventDispatcher
242 (listen! [this component]
243 "Listen for events on the specified AWT Component.")
244 (create-dispatcher [this handle handlers]
245 "Returns new event dispatcher associated with the specified event
246 handlers (an event-id -> handler-fn map). Handle is used to match
247 the contexts between commits.")
248 (commit [this]
249 "Apply the registered handlers for event processing.")
250 (handle-picked? [this handle]
251 "Returns true if the specified handle received the :mouse-pressed
252 event and have not yet received :moused-released.")
253 (handle-hovered? [this handle]
254 "Returns true if the specified handle received the :mouse-entered
255 event and have not yet received :mouse-exited."))
257 (defn- assoc-cons [m key val]
258 (->> (get m key) (cons val) (assoc m key)))
260 ;;
261 ;; Observers
262 ;; The mechanism used by views to request repaints
263 ;;
265 (def ^ConcurrentMap observers
266 (-> (MapMaker.) (.weakKeys) (.makeMap)))
268 (defn- cm-replace!
269 "Wrap ConcurrentMap replace method to treat nil value as absent
270 mapping. Use with maps that does not support nil values."
271 [^ConcurrentMap cmap key old new]
272 (if (nil? old)
273 (nil? (.putIfAbsent cmap key new))
274 (.replace cmap key old new)))
276 (defn- cm-swap!
277 "Atomically swaps the value associated with key in ConcurrentMap
278 to be (apply f current-value args). Returns the new value."
279 [^ConcurrentMap cmap key f & args]
280 (loop []
281 (let [old (.get cmap key)
282 new (apply f old args)]
283 (if (cm-replace! cmap key old new)
284 new
285 (recur)))))
287 (defn add-observer
288 "Add observer fn for the target. Watcher identifies the group of
289 observers and could be used to remove the group. Watcher is weakly
290 referenced, all associated observers will be removed when the
291 wathcer is removed by gc. The observer fn will be called with
292 watcher and target arguments and any additional arguments specified
293 in update call."
294 [watcher target f]
295 (cm-swap! observers watcher assoc-cons target f)
296 nil)
298 (defn remove-observers
299 "Remove group of observers associated with the specified watcher."
300 [watcher]
301 (.remove observers watcher)
302 nil)
304 (defn- replace-observers-watcher
305 [old-watcher new-watcher]
306 (if-let [old (.remove observers old-watcher)]
307 (.put observers new-watcher old))
308 nil)
310 (defn update
311 "Notify observers."
312 [target & args]
313 (doseq [entry observers
314 f (get (val entry) target)]
315 (apply f (key entry) target args)))
317 (defn add-context-observer
318 "Observer registered with this function will be automatically
319 removed after the next repaint is complete."
320 [target f]
321 (add-observer *scene* target f))
323 (defn repaint-on-update
324 "Trigger repaint of the current scene when the target updates."
325 [target]
326 (let [scene *scene*]
327 (if-not (identical? scene target)
328 (add-observer scene target (fn [w _] (update w))))))
330 (defn repaint
331 "Requests repaint of the current scene. If handle and state are
332 specified, the handle will be associated with the state in the
333 *states* map for the next paint iteration."
334 ([]
335 (update *scene*))
336 ([handle state]
337 (let [scene *scene*]
338 (swap! (:next-state scene) assoc handle state)
339 (update scene))))
341 (defmacro binding-fast
342 "Faster alternative to core/binding. Works only with vars that are
343 already thread-bound. Uses set! instead of push-thread-bindings and
344 pop-thread-bindings."
345 [bindings & body]
346 {:pre [(vector? bindings)
347 (even? (count bindings))]}
348 (let [bindings (partition 2 bindings)
349 var-syms (map first bindings)
350 var-vals (map second bindings)
351 syms (map (comp gensym name) var-syms)]
352 `(let [~@(mapcat vector syms var-syms)]
353 (try
354 ~@(map #(list `set! %1 %2) var-syms var-vals)
355 ~@body
356 (finally
357 ~@(map #(list `set! %1 %2) var-syms syms))))))
359 ;;
360 ;; Rendering
361 ;;
363 (defn ^FontRenderContext font-context
364 "Returns FontRenderContext for the current view context."
365 []
366 (if (bound? (var *graphics*))
367 (.getFontRenderContext *graphics*)
368 *font-context*))
370 (defn ^AffineTransform relative-transform
371 "Returns AffineTransform: view context -> AWT component."
372 []
373 (let [tr (.getTransform *graphics*)]
374 (.preConcatenate tr *inverse-initial-transform*)
375 tr))
377 (defn ^AffineTransform inverse-relative-transform
378 "Returns AffineTransform: AWT component -> view context."
379 []
380 (let [tr (.getTransform *graphics*)]
381 (.invert tr) ; absolute -> view
382 (.concatenate tr *initial-transform*) ; component -> absolute
383 tr))
385 (defn transform-point [^AffineTransform tr ^double x ^double y]
386 (let [p (Point2D$Double. x y)]
387 (.transform tr p p)
388 [(.x p) (.y p)]))
390 (defn inverse-transform-point [^AffineTransform tr ^double x ^double y]
391 (let [p (Point2D$Double. x y)]
392 (.inverseTransform tr p p)
393 [(.x p) (.y p)]))
395 ;; (defn- clip
396 ;; "Intersect clipping area with the specified shape or bounds.
397 ;; Returns new clip (Shape or nil if empty)."
398 ;; ([x y w h]
399 ;; (clip (Rectangle2D$Double. x y w h)))
400 ;; ([shape]
401 ;; (let [a1 (Area. shape)
402 ;; a2 (if (instance? Area *clip*) *clip* (Area. *clip*))]
403 ;; (.transform a1 (relative-transform))
404 ;; (.intersect a1 a2)
405 ;; (if (.isEmpty a1)
406 ;; nil
407 ;; a1))))
409 ;; Use faster clipping calculation provided by Graphics2D.
410 (defn- clip
411 "Intersect clipping area with the specified Shape in current
412 transform coordinates. Returns new clip in the AWT component
413 coordinates (Shape or nil if empty)."
414 [^Shape shape]
415 (let [^Graphics2D clip-g (.create *graphics*)]
416 (try
417 (doto clip-g
418 (.setClip shape)
419 (.setTransform *initial-transform*)
420 (.clip *clip*))
421 (if (.isEmpty (.getClipBounds clip-g))
422 nil
423 (.getClip clip-g))
424 (finally
425 (.dispose clip-g)))))
427 (defn- theme-get*
428 ([theme key]
429 (theme-get* theme key nil))
430 ([theme key not-found]
431 (if-let [e (find theme key)]
432 (loop [k (val e)]
433 (if-let [e1 (and (keyword? k)
434 (find theme k))]
435 (recur (val e1))
436 k))
437 not-found)))
439 (defn theme-get
440 ([key]
441 (theme-get* *theme* key))
442 ([key not-found]
443 (theme-get* *theme* key not-found)))
445 (defn ^Graphics2D apply-theme
446 "Set graphics' color and font to match theme.
447 Modifies and returns the first argument."
448 ([]
449 (apply-theme *graphics* *theme*))
450 ([^Graphics2D graphics theme]
451 (doto graphics
452 (.setColor (theme-get :fore-color))
453 (.setBackground (theme-get :back-color))
454 (.setFont (theme-get :font)))))
456 (defn- ^Graphics2D create-graphics
457 ([]
458 (apply-theme (.create *graphics*) *theme*))
459 ([^long x ^long y ^long w ^long h]
460 (apply-theme (.create *graphics* x y w h) *theme*)))
462 (defn- with-bounds-noclip*
463 [x y w h f & args]
464 (let [graphics (create-graphics)]
465 (try
466 (.translate graphics (double x) (double y))
467 (binding-fast [*width* w
468 *height* h
469 *input-clip* (Rectangle2D$Double. 0.0 0.0 w h)
470 *graphics* graphics]
471 (apply f args))
472 (finally
473 (.dispose graphics)))))
475 (defn with-bounds*
476 [x y w h f & args]
477 (let [x (double x)
478 y (double y)
479 bounds (Rectangle2D$Double. x y w h)]
480 (when-let [clip (clip bounds)]
481 (let [^Graphics2D graphics (create-graphics)]
482 (try
483 (.clip graphics bounds)
484 (.translate graphics x y)
485 (binding-fast [*width* w
486 *height* h
487 *clip* clip
488 *input-clip* nil
489 *graphics* graphics]
490 (apply f args))
491 (finally
492 (.dispose graphics)))))))
494 (defmacro with-bounds
495 [x y w h & body]
496 `(with-bounds* ~x ~y ~w ~h (fn [] ~@body)))
498 (defmacro with-theme
499 [theme & body]
500 `(binding [*theme* (merge *theme* ~theme)]
501 ~@body))
503 (defn with-theme* [theme f & args]
504 (with-theme theme
505 (apply f args)))
507 (defmacro with-color [color-or-key & body]
508 `(let [color# ~color-or-key
509 color# (theme-get color# color#)
510 g# *graphics*
511 old-color# (.getColor g#)]
512 (try
513 (.setColor g# color#)
514 ~@body
515 (finally
516 (.setColor g# old-color#)))))
518 (defmacro with-font [font-or-key & body]
519 `(let [font# ~font-or-key
520 font# (theme-get font# font#)
521 g# *graphics*
522 old-font# (.getFont g#)]
523 (try
524 (.setFont g# font#)
525 ~@body
526 (finally
527 (.setColor g# old-font#)))))
529 (defmacro with-stroke [stroke & body]
530 `(let [g# *graphics*
531 old-stroke# (.getStroke g#)]
532 (try
533 (.setStroke g# ~stroke)
534 ~@body
535 (finally
536 (.setStroke g# old-stroke#)))))
538 (defmacro with-hints
539 [hints & body]
540 `(let [h# ~hints
541 g# *graphics*
542 old# (.getRenderingHints g#)]
543 (try
544 (.addRenderingHints g# h#)
545 ~@body
546 (finally
547 (.setRenderingHints g# old#)))))
549 (defn with-hints* [hints f & args]
550 (with-hints hints
551 (apply f args)))
553 ;; TODO: constructor for AffineTransform.
554 ;; (transform :scale 0.3 0.5
555 ;; :translate 5 10
556 ;; :rotate (/ Math/PI 2))
558 (defmacro with-transform [transform & body]
559 `(let [g# *graphics*
560 old-t# (.getTransform g#)]
561 (try
562 (.transform g# ~transform)
563 ~@body
564 (finally
565 (.setTransform g# old-t#)))))
567 (defmacro with-rotate [theta ax ay & body]
568 `(let [transform# (AffineTransform/getRotateInstance ~theta ~ax ~ay)]
569 (with-transform transform# ~@body)))
571 (defmacro with-translate [x y & body]
572 `(let [x# ~x
573 y# ~y
574 g# *graphics*]
575 (try
576 (.translate g# x# y#)
577 ~@body
578 (finally
579 (.translate g# (- x#) (- y#))))))
581 (defn draw!
582 "Draws the View."
583 ([view]
584 (let [graphics (create-graphics)]
585 (try
586 (binding-fast [*graphics* graphics]
587 (render! view))
588 (finally
589 (.dispose graphics)))))
590 ([view x y]
591 (draw! view x y true))
592 ([view x y clip?]
593 (let [geom (geometry view)]
594 (draw! view x y (width geom) (height geom) clip?)))
595 ([view x y width height]
596 (draw! view x y width height true))
597 ([view x y width height clip?]
598 (if clip?
599 (with-bounds* x y width height render! view)
600 (with-bounds-noclip* x y width height render! view))))
602 (defn draw-aligned!
603 "Draws the View. Location is relative to the view's anchor point
604 for the specified alignment."
605 ([view h-align v-align x y]
606 (draw-aligned! view (geometry view) h-align v-align x y))
607 ([view geom h-align v-align x y]
608 (draw-aligned! view geom h-align v-align x y (width geom) (height geom)))
609 ([view h-align v-align x y w h]
610 (draw-aligned! view (geometry view) h-align v-align x y w h))
611 ([view geom h-align v-align x y w h]
612 (draw! view
613 (- x (anchor-x geom h-align w))
614 (- y (anchor-y geom v-align h))
615 w h)))
617 ;;
618 ;; Event handling.
619 ;;
621 (defn with-handlers*
622 [handle handlers f & args]
623 (binding-fast [*event-dispatcher* (create-dispatcher
624 *event-dispatcher* handle handlers)]
625 (apply f args)))
627 (defmacro with-handlers
628 "specs => (:event-id name & handler-body)*
630 Execute form with the specified event handlers."
631 [handle form & specs]
632 `(with-handlers* ~handle
633 ~(reduce (fn [m spec]
634 (assoc m (first spec)
635 `(fn [~(second spec)]
636 ~@(nnext spec)))) {}
637 specs)
638 (fn [] ~form)))
640 (defn picked? [handle]
641 (handle-picked? *event-dispatcher* handle))
643 (defn hovered? [handle]
644 (handle-hovered? *event-dispatcher* handle))
646 ;;
647 ;; EventDispatcher implementation
648 ;;
650 (def awt-events
651 {java.awt.event.MouseEvent/MOUSE_CLICKED :mouse-clicked
652 java.awt.event.MouseEvent/MOUSE_DRAGGED :mouse-dragged
653 java.awt.event.MouseEvent/MOUSE_ENTERED :mouse-entered
654 java.awt.event.MouseEvent/MOUSE_EXITED :mouse-exited
655 java.awt.event.MouseEvent/MOUSE_MOVED :mouse-moved
656 java.awt.event.MouseEvent/MOUSE_PRESSED :mouse-pressed
657 java.awt.event.MouseEvent/MOUSE_RELEASED :mouse-released
658 java.awt.event.MouseEvent/MOUSE_WHEEL :mouse-wheel})
660 (def dummy-event-dispatcher
661 (reify EventDispatcher
662 (listen! [_ _])
663 (create-dispatcher [this _ _] this)
664 (commit [_])
665 (handle-picked? [_ _])
666 (handle-hovered? [_ _])))
668 ;; Not using defrecord to avoid unacceptable overhead of recursive
669 ;; hash code calculation.
670 (deftype DispatcherNode [handle handlers parent
671 ^Shape clip ^AffineTransform transform
672 bindings]
673 EventDispatcher
674 (listen! [this component]
675 (listen! parent component))
676 (create-dispatcher [this handle handlers]
677 (create-dispatcher parent handle handlers))
678 (commit [this]
679 (commit parent))
680 (handle-picked? [this handle]
681 (handle-picked? parent handle))
682 (handle-hovered? [this handle]
683 (handle-hovered? parent handle)))
685 (defn- make-node [handle handlers]
686 (let [clip (if *input-clip*
687 (clip *input-clip*)
688 *clip*)
689 bindings (-> (get-thread-bindings)
690 (dissoc (var *graphics*))
691 (assoc (var *font-context*) (font-context)))]
692 (DispatcherNode. handle handlers *event-dispatcher* clip
693 (relative-transform)
694 bindings)))
696 (defn- add-node [tree ^DispatcherNode node]
697 (assoc-cons tree (.parent node) node))
699 (defn- nodes [tree]
700 (apply concat (vals tree)))
702 (defn- under-cursor
703 "Returns a vector of child nodes under cursor."
704 [node tree ^long x ^long y]
705 (some (fn [^DispatcherNode n]
706 (if (and (.clip n) (.contains ^Shape (.clip n) x y))
707 (conj (vec (under-cursor n tree x y)) n)))
708 (get tree node)))
710 (defn- translate-mouse-event [^java.awt.event.MouseEvent event
711 ^AffineTransform tr id]
712 (let [[x y] (inverse-transform-point tr (.getX event) (.getY event))
713 rotation (if (instance? MouseWheelEvent event)
714 (.getWheelRotation ^MouseWheelEvent event)
715 nil)]
716 (->MouseEvent id (.getWhen event) x y
717 (.getXOnScreen event) (.getYOnScreen event)
718 (.getButton event)
719 rotation
720 tr
721 (.getComponent event))))
723 (defn- translate-and-dispatch
724 ([nodes first-only ^java.awt.event.MouseEvent event]
725 (translate-and-dispatch nodes first-only
726 event (awt-events (.getID event))))
727 ([nodes first-only event id]
728 (if-let [^DispatcherNode node (first nodes)]
729 (let [handlers (.handlers node)
730 handler (get handlers id)]
731 (if handler
732 (do
733 (with-bindings* (.bindings node)
734 handler
735 (translate-mouse-event event (.transform node) id))
736 (when-not first-only
737 (recur (rest nodes) false event id)))
738 (when-not (and (= id :mouse-dragged)
739 (or (contains? handlers :mouse-pressed)
740 (contains? handlers :mouse-released)))
741 (recur (rest nodes) first-only event id)))))))
743 (defn- process-mouse-event
744 [dispatcher ^java.awt.event.MouseEvent source-event]
745 (let [{active-ref :active
746 hovered-ref :hovered
747 picked-ref :picked
748 last-ref :last-motion
749 tree-ref :tree} dispatcher
750 pressed (and source-event
751 (== (.getID source-event)
752 java.awt.event.MouseEvent/MOUSE_PRESSED))
753 released (and source-event
754 (== (.getID source-event)
755 java.awt.event.MouseEvent/MOUSE_RELEASED))
756 ^java.awt.event.MouseEvent last-event @last-ref
757 ^java.awt.event.MouseEvent event (or source-event last-event)]
758 (when event
759 (let [x (.getX event)
760 y (.getY event)
761 active @active-ref
762 active (if (and active
763 source-event
764 (== (.getX last-event) x)
765 (== (.getY last-event) y))
766 active
767 (ref-set active-ref
768 (under-cursor dispatcher @tree-ref x y)))
769 acted (cond
770 pressed (ref-set picked-ref active)
771 released (let [picked @picked-ref]
772 (ref-set picked-ref nil)
773 picked)
774 :else active)
775 picked (seq @picked-ref)
776 pred #(= (.handle ^DispatcherNode %1) (.handle ^DispatcherNode %2))
777 hovered (if picked
778 (filter #(some (partial pred %) picked) active)
779 active)
780 remove-all (fn [c1 c2]
781 (filter #(not (some (partial pred %) c2)) c1))
782 old-hovered @hovered-ref
783 exited (remove-all old-hovered hovered)
784 entered (remove-all hovered old-hovered)
785 moved (or picked (remove-all hovered entered))]
786 (ref-set hovered-ref hovered)
787 (ref-set last-ref event)
788 [exited entered moved acted event]))))
790 (defn- dispatch-mouse-event
791 [dispatcher source-event button?]
792 (when-let [[exited
793 entered
794 moved
795 acted
796 event] (dosync (process-mouse-event dispatcher source-event))]
797 (when button?
798 (translate-and-dispatch acted true event))
799 (translate-and-dispatch exited false event :mouse-exited)
800 (translate-and-dispatch entered false event :mouse-entered)
801 (when-not button?
802 (translate-and-dispatch moved true source-event))))
804 (defrecord RootEventDispatcher [tree-r ;; register
805 tree ;; dispatch
806 active ;; nodes under cursor
807 hovered ;; mouse entered
808 picked ;; mouse pressed
809 last-motion]
810 EventDispatcher
811 (listen! [dispatcher component]
812 (doto ^Component component
813 (.addMouseListener dispatcher)
814 (.addMouseWheelListener dispatcher)
815 (.addMouseMotionListener dispatcher)))
816 (create-dispatcher [dispatcher handle handlers]
817 (let [node (make-node handle handlers)]
818 (dosync (alter tree-r add-node node))
819 node))
820 (commit [dispatcher]
821 (let [[exited
822 entered
823 _ _
824 event] (dosync
825 ;; TODO: retain contexts that do
826 ;; not intersect graphics
827 ;; clipping area in tree.
828 (ref-set tree @tree-r)
829 (ref-set tree-r {})
830 (process-mouse-event dispatcher nil))]
831 ;; Send mouse entered and exited events if necessary due to
832 ;; updated layout.
833 (translate-and-dispatch exited false event :mouse-exited)
834 (translate-and-dispatch entered false event :mouse-entered)))
835 (handle-picked? [dispatcher handle]
836 (some #(= handle (.handle ^DispatcherNode %)) @picked))
837 (handle-hovered? [dispatcher handle]
838 (some #(= handle (.handle ^DispatcherNode %)) @hovered))
839 MouseListener
840 (mouseEntered [dispatcher event]
841 (dispatch-mouse-event dispatcher event false))
842 (mouseExited [dispatcher event]
843 (dispatch-mouse-event dispatcher event false))
844 (mouseClicked [dispatcher event]
845 (dispatch-mouse-event dispatcher event true))
846 (mousePressed [dispatcher event]
847 (dispatch-mouse-event dispatcher event true))
848 (mouseReleased [dispatcher event]
849 (dispatch-mouse-event dispatcher event true))
850 MouseWheelListener
851 (mouseWheelMoved [dispatcher event]
852 (dispatch-mouse-event dispatcher event true))
853 MouseMotionListener
854 (mouseDragged [dispatcher event]
855 (dispatch-mouse-event dispatcher event false))
856 (mouseMoved [dispatcher event]
857 (dispatch-mouse-event dispatcher event false)))
859 (defn root-event-dispatcher []
860 (->RootEventDispatcher
861 (ref {}) (ref {}) ;; trees
862 (ref nil) (ref nil) (ref nil) ;; node states
863 (ref nil))) ;; last event
865 ;;
866 ;; Scene
867 ;;
869 (defrecord Scene [view
870 event-dispatcher
871 component
872 rendering-hints
873 next-state])
875 ;; Define rendering hints that affect font metrics to make sure that
876 ;; Graphics and Scene FontRenderContexts are consistent.
877 (def ^:private default-rendering-hints
878 {RenderingHints/KEY_TEXT_ANTIALIASING
879 RenderingHints/VALUE_TEXT_ANTIALIAS_DEFAULT,
880 RenderingHints/KEY_FRACTIONALMETRICS
881 RenderingHints/VALUE_FRACTIONALMETRICS_DEFAULT})
883 (defn make-scene
884 ([view]
885 (make-scene view dummy-event-dispatcher nil))
886 ([view event-dispatcher]
887 (make-scene view event-dispatcher nil))
888 ([view event-dispatcher ^Component component]
889 (make-scene view event-dispatcher component nil))
890 ([view event-dispatcher ^Component component hints]
891 (let [hints (merge default-rendering-hints hints)]
892 (->Scene view
893 event-dispatcher
894 component
895 hints
896 (atom nil)))))
898 (defn- get-and-set!
899 "Atomically sets the value of atom to newval and returns the old
900 value."
901 [atom newval]
902 (loop [v @atom]
903 (if (compare-and-set! atom v newval)
904 v
905 (recur @atom))))
907 (defn draw-scene!
908 [scene ^Graphics2D graphics width height]
909 (.addRenderingHints graphics (:rendering-hints scene))
910 (binding [*states* (get-and-set! (:next-state scene) nil)
911 *scene* scene
912 *graphics* graphics
913 *initial-transform* (.getTransform graphics)
914 *inverse-initial-transform* (-> graphics
915 .getTransform
916 .createInverse)
917 *event-dispatcher* (:event-dispatcher scene)
918 *width* width
919 *height* height
920 *clip* (Rectangle2D$Double. 0.0 0.0 width height)
921 *input-clip* nil
922 *time* (System/nanoTime)]
923 (apply-theme)
924 (let [tmp-watcher (Object.)]
925 ;; Keep current context observers until the rendering is
926 ;; complete. Some observers may be invoked twice if they
927 ;; appear in both groups until tmp-watcher is removed.
928 (replace-observers-watcher scene tmp-watcher)
929 (try
930 (render! (:view scene))
931 (finally
932 (remove-observers tmp-watcher)
933 (commit (:event-dispatcher scene)))))))
935 (defn- scene-font-context [scene]
936 (let [hints (:rendering-hints scene)
937 ^Component c (:component scene)
938 t (if c (->> c
939 .getFont
940 (.getFontMetrics c)
941 .getFontRenderContext
942 .getTransform))]
943 (FontRenderContext.
944 t
945 (get hints RenderingHints/KEY_TEXT_ANTIALIASING)
946 (get hints RenderingHints/KEY_FRACTIONALMETRICS))))
948 (defn scene-geometry [scene]
949 (binding [*scene* scene
950 *font-context* (scene-font-context scene)]
951 (geometry (:view scene))))
953 (defn set-cursor! [^Cursor cursor]
954 (when-let [^Component component (:component *scene*)]
955 (EventQueue/invokeLater #(.setCursor component cursor))))