view src/net/kryshen/indyvon/core.clj @ 116:b76c0d00898b

Remember *graphics* binding in with-color and with-transform (performance + makes sure we restore state on the same instance). Update font context when changing transform.
author Mikhail Kryshen <mikhail@kryshen.net>
date Tue, 28 Feb 2012 02:31:10 +0400
parents dac8ff197a6a
children 91c341698f7e
line source
1 ;;
2 ;; Copyright 2010, 2011 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 net.kryshen.indyvon.core
21 (:import
22 (java.awt Graphics2D RenderingHints Component Color Font Shape
23 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 ;; Layer context
33 ;;
35 (def ^:dynamic ^Graphics2D *graphics*)
37 (def ^:dynamic ^FontRenderContext *font-context*)
39 (def ^:dynamic *width*
40 "Width of the rendering area.")
42 (def ^:dynamic *height*
43 "Height of the rendering area.")
45 (def ^:dynamic ^Shape *clip*)
47 (def ^:dynamic *time*
48 "Timestamp of the current frame (in nanoseconds).")
50 (def ^:dynamic *scene*
51 "Encloses state that should be retained between repaints.")
53 (def ^:dynamic *states*
54 "Transient scene states, a map.")
56 (def ^:dynamic *event-dispatcher*)
58 (def ^:dynamic ^AffineTransform *initial-transform*
59 "Initial transform associated with the graphics context.")
61 (def ^:dynamic ^AffineTransform *inverse-initial-transform*
62 "Inversion of the initial transform associated with the graphics
63 context.")
65 (defrecord Theme [fore-color back-color alt-back-color border-color
66 shadow-color font])
68 ;; REMIND: use system colors, see java.awt.SystemColor.
69 (defn default-theme []
70 (Theme. Color/BLACK
71 Color/WHITE
72 (Color. 0xC8 0xD2 0xD8)
73 (Color. 0 0 0xC8)
74 (Color. 0x44 0x44 0x44)
75 (Font. "Sans" Font/PLAIN 12)))
77 (def ^:dynamic *theme* (default-theme))
79 ;;
80 ;; Core protocols and types
81 ;;
83 (defprotocol Layer
84 "Basic UI element."
85 (render! [layer]
86 "Draws layer in the current *graphics* context.")
87 (geometry [layer]
88 "Returns the preferred layer Geometry."))
90 (defprotocol Geometry
91 "Describes geometry of a Layer. Prefer using the available
92 implementations (Size, FixedGeometry and NestedGeometry) over
93 extending this protocol directly as it is likely to be changed in
94 the future versions."
95 (width [geom] [geom height])
96 (height [geom] [geom width])
97 (anchor-x [geom h-align width]
98 "Returns the x coordinate of the anchor point for the specified
99 horizontal alignment and width, h-align could be :left, :center
100 or :right.")
101 (anchor-y [geom v-align height]
102 "Returns the y coordinate of the anchor point for the specified
103 vertical alignment and height, v-align could be :top, :center
104 or :bottom."))
106 (defrecord Size [width height]
107 Geometry
108 (width [_] width)
109 (width [_ _] width)
110 (height [_] height)
111 (height [_ _] height)
112 (anchor-x [_ h-align width]
113 (case h-align
114 :left 0
115 :center (/ width 2)
116 :right width))
117 (anchor-y [_ v-align height]
118 (case v-align
119 :top 0
120 :center (/ height 2)
121 :bottom height)))
123 (defrecord FixedGeometry [ax ay width height]
124 Geometry
125 (width [_] width)
126 (width [_ _] width)
127 (height [_] height)
128 (height [_ _] height)
129 (anchor-x [_ _ _] ax)
130 (anchor-y [_ _ _] ay))
132 (defrecord NestedGeometry [geometry top left bottom right]
133 Geometry
134 (width [_]
135 (+ left right (width geometry)))
136 (width [_ h]
137 (+ left right (width geometry (- h top bottom))))
138 (height [_]
139 (+ top bottom (height geometry)))
140 (height [_ w]
141 (+ top bottom (height geometry (- w left right))))
142 (anchor-x [_ h-align w]
143 (+ left (anchor-x geometry h-align (- w left right))))
144 (anchor-y [_ v-align h]
145 (+ top (anchor-y geometry v-align (- h top bottom)))))
147 (defrecord ScaledGeometry [geometry sx sy]
148 Geometry
149 (width [_]
150 (* sx (width geometry)))
151 (width [_ h]
152 (* sx (width geometry (/ h sy))))
153 (height [_]
154 (* sy (height geometry)))
155 (height [_ w]
156 (* sy (height geometry (/ w sx))))
157 (anchor-x [_ h-align w]
158 (* sx (anchor-x geometry h-align (/ w sx))))
159 (anchor-y [_ v-align h]
160 (* sy (anchor-y geometry v-align (/ h sy)))))
162 ;; TODO: modifiers
163 (defrecord MouseEvent [id when x y x-on-screen y-on-screen button
164 wheel-rotation])
166 ;; TODO: KeyEvent
168 (defprotocol EventDispatcher
169 (listen! [this component]
170 "Listen for events on the specified AWT Component.")
171 (create-dispatcher [this handle handlers]
172 "Returns new event dispatcher associated with the specified event
173 handlers (an event-id -> handler-fn map). Handle is used to
174 match the contexts between commits.")
175 (commit [this]
176 "Apply the registered handlers for event processing.")
177 (handle-picked? [this handle]
178 "Returns true if the specified handle received the :mouse-pressed
179 event and have not yet received :moused-released.")
180 (handle-hovered? [this handle]
181 "Returns true if the specified handle received the :mouse-entered
182 event and have not yet received :mouse-exited."))
184 (defn- assoc-cons [m key val]
185 (->> (get m key) (cons val) (assoc m key)))
187 ;;
188 ;; Observers
189 ;; The mechanism used by layers to request repaints
190 ;;
192 (def ^ConcurrentMap observers
193 (-> (MapMaker.) (.weakKeys) (.makeMap)))
195 (defn- cm-replace!
196 "Wrap ConcurrentMap replace method to treat nil value as absent
197 mapping. Use with maps that does not support nil values."
198 [^ConcurrentMap cmap key old new]
199 (if (nil? old)
200 (nil? (.putIfAbsent cmap key new))
201 (.replace cmap key old new)))
203 (defn- cm-swap!
204 "Atomically swaps the value associated with key in ConcurrentMap
205 to be (apply f current-value args). Returns the new value."
206 [^ConcurrentMap cmap key f & args]
207 (loop []
208 (let [old (.get cmap key)
209 new (apply f old args)]
210 (if (cm-replace! cmap key old new)
211 new
212 (recur)))))
214 (defn add-observer
215 "Add observer fn for the target. Watcher identifies the group of
216 observers and could be used to remove the group. Watcher is weakly
217 referenced, all associated observers will be removed when the
218 wathcer is removed by gc. The observer fn will be called with
219 watcher and target arguments and any additional arguments specified
220 in update call."
221 [watcher target f]
222 (cm-swap! observers watcher assoc-cons target f)
223 nil)
225 (defn remove-observers
226 "Remove group of observers associated with the specified watcher."
227 [watcher]
228 (.remove observers watcher)
229 nil)
231 (defn- replace-observers-watcher
232 [old-watcher new-watcher]
233 (if-let [old (.remove observers old-watcher)]
234 (.put observers new-watcher old))
235 nil)
237 (defn update
238 "Notify observers."
239 [target & args]
240 (doseq [entry observers
241 f (get (val entry) target)]
242 (apply f (key entry) target args)))
244 (defn add-context-observer
245 "Observer registered with this function will be automatically
246 removed after the next repaint is complete."
247 [target f]
248 (add-observer *scene* target f))
250 (defn repaint-on-update
251 "Trigger repaint of the current scene when the target updates."
252 [target]
253 (let [scene *scene*]
254 (if-not (identical? scene target)
255 (add-observer scene target (fn [w _] (update w))))))
257 (defn repaint
258 "Requests repaint of the current scene. If handle and state are
259 specified, the handle will be associated with the state in the
260 *states* map for the next paint iteration."
261 ([]
262 (update *scene*))
263 ([handle state]
264 (let [scene *scene*]
265 (swap! (:next-state scene) assoc handle state)
266 (update scene))))
268 ;;
269 ;; Rendering
270 ;;
272 (defn ^AffineTransform relative-transform
273 "Returns AffineTransform: layer context -> AWT component."
274 []
275 (let [tr (.getTransform *graphics*)]
276 (.preConcatenate tr *inverse-initial-transform*)
277 tr))
279 (defn ^AffineTransform inverse-relative-transform
280 "Returns AffineTransform: AWT component -> layer context."
281 []
282 (let [tr (.getTransform *graphics*)]
283 (.invert tr) ; absolute -> layer
284 (.concatenate tr *initial-transform*) ; component -> absolute
285 tr))
287 (defn transform-point [^AffineTransform tr x y]
288 (let [p (Point2D$Double. x y)]
289 (.transform tr p p)
290 [(.x p) (.y p)]))
292 ;; (defn- clip
293 ;; "Intersect clipping area with the specified shape or bounds.
294 ;; Returns new clip (Shape or nil if empty)."
295 ;; ([x y w h]
296 ;; (clip (Rectangle2D$Double. x y w h)))
297 ;; ([shape]
298 ;; (let [a1 (Area. shape)
299 ;; a2 (if (instance? Area *clip*) *clip* (Area. *clip*))]
300 ;; (.transform a1 (relative-transform))
301 ;; (.intersect a1 a2)
302 ;; (if (.isEmpty a1)
303 ;; nil
304 ;; a1))))
306 ;; Use faster clipping calculation provided by Graphics2D.
307 (defn- clip
308 "Intersect clipping area with the specified bounds in current
309 transform coordinates. Returns new clip in the AWT component
310 coordinates (Shape or nil if empty)."
311 [x y w h]
312 (let [^Graphics2D clip-g (.create *graphics*)]
313 (doto clip-g
314 (.setClip x y w h)
315 (.setTransform *initial-transform*)
316 (.clip *clip*))
317 (try
318 (if (.isEmpty (.getClipBounds clip-g))
319 nil
320 (.getClip clip-g))
321 (finally
322 (.dispose clip-g)))))
324 (defn- ^Graphics2D apply-theme
325 "Set graphics' color and font to match theme.
326 Modifies and returns the first argument."
327 ([]
328 (apply-theme *graphics* *theme*))
329 ([^Graphics2D graphics theme]
330 (doto graphics
331 (.setColor (:fore-color theme))
332 (.setFont (:font theme)))))
334 (defn- ^Graphics2D create-graphics
335 ([]
336 (apply-theme (.create *graphics*) *theme*))
337 ([x y w h]
338 (apply-theme (.create *graphics* x y w h) *theme*)))
340 (defn- with-bounds-noclip*
341 [x y w h f & args]
342 (let [graphics (create-graphics)]
343 (try
344 (.translate graphics (int x) (int y))
345 (binding [*width* w
346 *height* h
347 *graphics* graphics]
348 (apply f args))
349 (finally
350 (.dispose graphics)))))
352 (defn with-bounds*
353 [x y w h f & args]
354 (when-let [clip (clip x y w h)]
355 (let [graphics (create-graphics x y w h)]
356 (try
357 (binding [*width* w
358 *height* h
359 *clip* clip
360 *graphics* graphics]
361 (apply f args))
362 (finally
363 (.dispose graphics))))))
365 (defmacro with-bounds
366 [x y w h & body]
367 `(with-bounds* ~x ~y ~w ~h (fn [] ~@body)))
369 (defmacro with-theme
370 [theme & body]
371 `(binding [*theme* (merge *theme* ~theme)]
372 ~@body))
374 (defmacro with-color
375 [color-or-keyword & body]
376 (let [color-form (if (keyword? color-or-keyword)
377 `(~color-or-keyword *theme*)
378 color-or-keyword)]
379 `(let [color# ~color-form
380 g# *graphics*
381 old-color# (.getColor g#)]
382 (try
383 (.setColor g# color#)
384 ~@body
385 (finally
386 (.setColor g# old-color#))))))
388 (defn with-hints*
389 [hints f & args]
390 (if hints
391 (let [g *graphics*
392 old (.getRenderingHints g)]
393 (try
394 (.addRenderingHints g hints)
395 (binding [*font-context* (.getFontRenderContext g)]
396 (apply f args))
397 (finally
398 (.setRenderingHints g old))))
399 (apply f args)))
401 (defmacro with-hints
402 [hints & body]
403 `(with-hints ~hints (fn [] ~@body)))
405 ;; TODO: constructor for AffineTransform.
406 ;; (transform :scale 0.3 0.5
407 ;; :translate 5 10
408 ;; :rotate (/ Math/PI 2))
410 (defmacro with-transform [transform & body]
411 `(let [g# *graphics*
412 old-t# (.getTransform g#)]
413 (try
414 (.transform g# ~transform)
415 (binding [*font-context* (.getFontRenderContext g#)]
416 ~@body)
417 (finally
418 (.setTransform g# old-t#)))))
420 (defmacro with-rotate [theta ax ay & body]
421 `(let [transform# (AffineTransform/getRotateInstance ~theta ~ax ~ay)]
422 (with-transform transform# ~@body)))
424 (defmacro with-translate [x y & body]
425 `(let [x# ~x
426 y# ~y
427 g# *graphics*]
428 (try
429 (.translate g# x# y#)
430 ~@body
431 (finally
432 (.translate g# (- x#) (- y#))))))
434 (defn draw!
435 "Draws layer."
436 ([layer]
437 (let [graphics (create-graphics)]
438 (try
439 (binding [*graphics* graphics]
440 (render! layer))
441 (finally
442 (.dispose graphics)))))
443 ([layer x y]
444 (draw! layer x y true))
445 ([layer x y clip?]
446 (let [geom (geometry layer)]
447 (draw! layer x y (width geom) (height geom) clip?)))
448 ([layer x y width height]
449 (draw! layer x y width height true))
450 ([layer x y width height clip?]
451 (if clip?
452 (with-bounds* x y width height render! layer)
453 (with-bounds-noclip* x y width height render! layer))))
455 (defn draw-aligned!
456 "Draws layer. Location is relative to the layer's anchor point for
457 the specified alignment."
458 ([layer h-align v-align x y]
459 (let [geom (geometry layer)
460 w (width geom)
461 h (height geom)]
462 (draw! layer
463 (- x (anchor-x geom h-align w))
464 (- y (anchor-y geom v-align h))
465 w h)))
466 ([layer h-align v-align x y w h]
467 (let [geom (geometry layer)]
468 (draw! layer
469 (- x (anchor-x geom h-align w))
470 (- y (anchor-y geom v-align h))
471 w h))))
473 ;;
474 ;; Event handling.
475 ;;
477 (defn with-handlers*
478 [handle handlers f & args]
479 (binding [*event-dispatcher* (create-dispatcher
480 *event-dispatcher* handle handlers)]
481 (apply f args)))
483 (defmacro with-handlers
484 "specs => (:event-id name & handler-body)*
486 Execute form with the specified event handlers."
487 [handle form & specs]
488 `(with-handlers* ~handle
489 ~(reduce (fn [m spec]
490 (assoc m (first spec)
491 `(fn [~(second spec)]
492 ~@(nnext spec)))) {}
493 specs)
494 (fn [] ~form)))
496 (defn picked? [handle]
497 (handle-picked? *event-dispatcher* handle))
499 (defn hovered? [handle]
500 (handle-hovered? *event-dispatcher* handle))
502 ;;
503 ;; EventDispatcher implementation
504 ;;
506 (def awt-events
507 {java.awt.event.MouseEvent/MOUSE_CLICKED :mouse-clicked
508 java.awt.event.MouseEvent/MOUSE_DRAGGED :mouse-dragged
509 java.awt.event.MouseEvent/MOUSE_ENTERED :mouse-entered
510 java.awt.event.MouseEvent/MOUSE_EXITED :mouse-exited
511 java.awt.event.MouseEvent/MOUSE_MOVED :mouse-moved
512 java.awt.event.MouseEvent/MOUSE_PRESSED :mouse-pressed
513 java.awt.event.MouseEvent/MOUSE_RELEASED :mouse-released
514 java.awt.event.MouseEvent/MOUSE_WHEEL :mouse-wheel})
516 (def dummy-event-dispatcher
517 (reify EventDispatcher
518 (listen! [_ _])
519 (create-dispatcher [this _ _] this)
520 (commit [_])
521 (handle-picked? [_ _])
522 (handle-hovered? [_ _])))
524 (defrecord DispatcherNode [handle handlers parent
525 ^Shape clip ^AffineTransform transform
526 bindings]
527 EventDispatcher
528 (listen! [this component]
529 (listen! parent component))
530 (create-dispatcher [this handle handlers]
531 (create-dispatcher parent handle handlers))
532 (commit [this]
533 (commit parent))
534 (handle-picked? [this handle]
535 (handle-picked? parent handle))
536 (handle-hovered? [this handle]
537 (handle-hovered? parent handle)))
539 (defn- make-node [handle handlers]
540 (DispatcherNode. handle handlers *event-dispatcher* *clip*
541 (inverse-relative-transform)
542 (get-thread-bindings)))
544 (defn- add-node [tree node]
545 (assoc-cons tree (:parent node) node))
547 (defn- nodes [tree]
548 (apply concat (vals tree)))
550 (defn- under-cursor
551 "Returns a vector of child nodes under cursor."
552 [x y tree node]
553 (some #(if (.contains ^Shape (:clip %) x y)
554 (conj (vec (under-cursor x y tree %)) %))
555 (get tree node)))
557 (defn- remove-all [coll1 coll2 pred]
558 (filter #(not (some (partial pred %) coll2)) coll1))
560 (defn- translate-mouse-event [^java.awt.event.MouseEvent event
561 ^AffineTransform tr id]
562 (let [[x y] (transform-point tr (.getX event) (.getY event))
563 rotation (if (instance? MouseWheelEvent event)
564 (.getWheelRotation ^MouseWheelEvent event)
565 nil)]
566 (MouseEvent. id (.getWhen event) x y
567 (.getXOnScreen event) (.getYOnScreen event)
568 (.getButton event)
569 rotation)))
571 (defn- translate-and-dispatch
572 ([nodes first-only ^java.awt.event.MouseEvent event]
573 (translate-and-dispatch nodes first-only
574 event (awt-events (.getID event))))
575 ([nodes first-only event id]
576 (if-let [node (first nodes)]
577 (if-let [handler (get (:handlers node) id)]
578 (do
579 (let [translated (translate-mouse-event event (:transform node) id)]
580 (with-bindings* (:bindings node)
581 handler translated))
582 (if-not first-only
583 (recur (rest nodes) false event id)))
584 (recur (rest nodes) first-only event id)))))
586 (defn- dispatch-mouse-motion
587 "Dispatches mouse motion events."
588 [hovered-ref tree root ^java.awt.event.MouseEvent event]
589 (let [x (.getX event)
590 y (.getY event)
591 [hovered hovered2] (dosync
592 [@hovered-ref
593 (ref-set hovered-ref
594 (under-cursor x y tree root))])
595 pred #(= (:handle %1) (:handle %2))
596 exited (remove-all hovered hovered2 pred)
597 entered (remove-all hovered2 hovered pred)
598 moved (remove-all hovered2 entered pred)]
599 (translate-and-dispatch exited false event :mouse-exited)
600 (translate-and-dispatch entered false event :mouse-entered)
601 (translate-and-dispatch moved true event :mouse-moved)))
603 (defn- dispatch-mouse-button
604 [picked-ref hovered-ref ^java.awt.event.MouseEvent event]
605 (let [id (awt-events (.getID event))
606 nodes (case id
607 :mouse-pressed
608 (dosync
609 (ref-set picked-ref @hovered-ref))
610 :mouse-released
611 (dosync
612 (let [picked @picked-ref]
613 (ref-set picked-ref nil)
614 picked))
615 @hovered-ref)]
616 (translate-and-dispatch nodes true event id)))
618 (defn root-event-dispatcher []
619 (let [tree-r (ref {}) ; register
620 tree (ref {}) ; dispatch
621 hovered (ref '())
622 picked (ref '())]
623 (reify
624 EventDispatcher
625 (listen! [this component]
626 (doto ^Component component
627 (.addMouseListener this)
628 (.addMouseWheelListener this)
629 (.addMouseMotionListener this)))
630 (create-dispatcher [this handle handlers]
631 (let [node (make-node handle handlers)]
632 (dosync (alter tree-r add-node node))
633 node))
634 (commit [this]
635 ;; TODO: retain contexts that do not intersect graphics
636 ;; clipping area in tree.
637 (dosync (ref-set tree @tree-r)
638 (ref-set tree-r {})))
639 (handle-picked? [this handle]
640 (some #(= handle (:handle %)) @picked))
641 (handle-hovered? [this handle]
642 (some #(= handle (:handle %)) @hovered))
643 MouseListener
644 (mouseEntered [this event]
645 (dispatch-mouse-motion hovered @tree this event))
646 (mouseExited [this event]
647 (dispatch-mouse-motion hovered @tree this event))
648 (mouseClicked [this event]
649 (dispatch-mouse-button picked hovered event))
650 (mousePressed [this event]
651 (dispatch-mouse-button picked hovered event))
652 (mouseReleased [this event]
653 (dispatch-mouse-button picked hovered event))
654 MouseWheelListener
655 (mouseWheelMoved [this event]
656 (dispatch-mouse-button picked hovered event))
657 MouseMotionListener
658 (mouseDragged [this event]
659 (translate-and-dispatch @picked true event))
660 (mouseMoved [this event]
661 (dispatch-mouse-motion hovered @tree this event)))))
663 ;;
664 ;; Scene
665 ;;
667 (defrecord Scene [layer event-dispatcher component next-state])
669 (defn make-scene
670 ([layer]
671 (make-scene layer dummy-event-dispatcher nil))
672 ([layer event-dispatcher]
673 (make-scene layer event-dispatcher nil))
674 ([layer event-dispatcher component]
675 (->Scene layer event-dispatcher component (atom nil))))
677 (defn- get-and-set!
678 "Atomically sets the value of atom to newval and returns the old
679 value."
680 [atom newval]
681 (loop [v @atom]
682 (if (compare-and-set! atom v newval)
683 v
684 (recur @atom))))
686 (defn draw-scene!
687 [scene ^Graphics2D graphics width height]
688 ;; (.setRenderingHint graphics
689 ;; RenderingHints/KEY_INTERPOLATION
690 ;; RenderingHints/VALUE_INTERPOLATION_BILINEAR)
691 ;; (.setRenderingHint graphics
692 ;; RenderingHints/KEY_ALPHA_INTERPOLATION
693 ;; RenderingHints/VALUE_ALPHA_INTERPOLATION_QUALITY)
694 ;; (.setRenderingHint graphics
695 ;; RenderingHints/KEY_ANTIALIASING
696 ;; RenderingHints/VALUE_ANTIALIAS_ON)
697 ;; (.setRenderingHint graphics
698 ;; RenderingHints/KEY_TEXT_ANTIALIASING
699 ;; RenderingHints/VALUE_TEXT_ANTIALIAS_ON)
700 (binding [*states* (get-and-set! (:next-state scene) nil)
701 *scene* scene
702 *graphics* graphics
703 *font-context* (.getFontRenderContext graphics)
704 *initial-transform* (.getTransform graphics)
705 *inverse-initial-transform* (-> graphics
706 .getTransform
707 .createInverse)
708 *event-dispatcher* (:event-dispatcher scene)
709 *width* width
710 *height* height
711 *clip* (Rectangle2D$Double. 0 0 width height)
712 *time* (System/nanoTime)]
713 (apply-theme)
714 (let [tmp-watcher (Object.)]
715 ;; Keep current context observers until the rendering is
716 ;; complete. Some observers may be invoked twice if they
717 ;; appear in both groups until tmp-watcher is removed.
718 (replace-observers-watcher scene tmp-watcher)
719 (try
720 (render! (:layer scene))
721 (finally
722 (remove-observers tmp-watcher)
723 (commit (:event-dispatcher scene)))))))
725 (defn scene-geometry [scene font-context]
726 (binding [*scene* scene
727 *font-context* font-context]
728 (geometry (:layer scene))))
730 (defn set-cursor! [^Cursor cursor]
731 (when-let [^Component component (:component *scene*)]
732 (EventQueue/invokeLater #(.setCursor component cursor))))