view src/net/kryshen/indyvon/core.clj @ 64:702a4939312d

New mechanism for layers to trigger repaints (beginning).
author Mikhail Kryshen <mikhail@kryshen.net>
date Thu, 26 Aug 2010 06:29:30 +0400
parents 88bb47e3a401
children fd1bcb67bc32
line wrap: on
line source

;;
;; Copyright (C) 2010 Mikhail Kryshen <mikhail@kryshen.net>
;;
;; This file is part of Indyvon.
;;

(ns net.kryshen.indyvon.core
  (:import
   (java.awt Graphics2D Component Color Font AWTEvent Shape)
   (java.awt.geom AffineTransform Point2D$Double Rectangle2D$Double Area)
   (java.awt.event MouseListener MouseMotionListener)
   (java.awt.font FontRenderContext)))

;;
;; Layer context
;;

(def ^Graphics2D *graphics*)

(def ^FontRenderContext *font-context*)

(def ^{:tag Component
       :doc "Target AWT component, may be nil if drawing off-screen."}
     *target*)

(def *width*)

(def *height*)

(def ^Shape *clip*)

(def *event-dispatcher*)

(def ^{:doc "Fn to be called in a layer context to request redraw."}
     *update*)

(def ^{:tag AffineTransform
       :doc "Initial transform associated with the graphics context"}
     *initial-transform*)

(def ^{:tag AffineTransform
       :doc "Inversion of the initial transform associated with
            the graphics context"}
     *inverse-initial-transform*)

(defrecord Theme [fore-color back-color alt-back-color border-color font])

;; REMIND: use system colors, see java.awt.SystemColor.
(defn default-theme []
  (Theme. Color/BLACK Color/LIGHT_GRAY Color/WHITE
          Color/BLUE (Font. "Sans" Font/PLAIN 12)))

(def *theme* (default-theme))

(defrecord Location [x y])
(defrecord Size [width height])
(defrecord Bounds [x y width height])

;;
;; Core protocols and types
;;

(defprotocol Layer
  "Basic UI element."
  (render! [this])
  (layer-size [this]))

;; TODO: modifiers
(defrecord MouseEvent [id when x y x-on-screen y-on-screen button])

(defprotocol EventDispatcher
  (listen! [this ^Component component]
     "Listen for events on the specified AWT Component.")
  (create-dispatcher [this handle handlers]
     "Returns new event dispatcher associated with the specified event
      handlers (an event-id -> handler-fn map). Handle is used to
      match the contexts between commits.")
  (commit [this]
     "Apply the registered handlers for event processing."))

(defprotocol Anchored
  "Provide anchor point for Layers. Used by viewport."
  (anchor [this h-align v-align]
     "Anchor point: [x y], h-align could be :left, :center or :right,
      v-align is :top, :center or :bottom"))

;; Default implementation of Anchored for any Layer.
(extend-protocol Anchored
  net.kryshen.indyvon.core.Layer
  (anchor [this h-align v-align]
     (if (and (= h-align :left)
              (= v-align :top))
       (Location. 0 0)
       (let [size (layer-size this)]
         (Location.
          (case h-align
            :top 0
            :center (/ (:width size) 2)
            :right (:width size))
          (case v-align
            :left 0
            :center (/ (:height size) 2)
            :bottom (:height size)))))))

(defn- assoc-cons [m key val]
  (->> (get m key) (cons val) (assoc m key)))

(defn- assoc-in-cons [m keys val]
  (->> (get-in m keys) (cons val) (assoc-in m keys)))

;;
;; Observers
;;

(def observers (atom nil))

(defn add-observer
  ([target f]
     (add-observer target f :default))
  ([target f group-id]
     (swap! observers assoc-in-cons [group-id target] f)
     nil))

(defn remove-group
  "Remove group of observers."
  [group-id]
  (swap! observers dissoc group-id)
  nil)

(defn- change-group-id*
  [observers old-id new-id]
  (let [group (get observers old-id)]
    (assoc (dissoc observers old-id)
      new-id group)))

(defn- change-group-id
  [old-id new-id]
  (swap! observers change-group-id* old-id new-id))

(defn update
  "Notify observers."
  [target & args]
  (doseq [f (reduce #(concat %1 (get %2 target)) nil (vals @observers))]
    (apply f target args)))

;;
;; Rendering
;;

(defn- relative-transform
  "AffineTransform: layer context -> awt component."
  []
  (let [tr (.getTransform *graphics*)]
    (.preConcatenate tr *inverse-initial-transform*)
    tr))

(defn- inverse-relative-transform
  "AffineTransform: awt component -> layer context."
  []
  (let [tr (.getTransform *graphics*)]
    (.invert tr)                          ; absolute -> layer
    (.concatenate tr *initial-transform*) ; component -> absolute
    tr))

(defn- clip
  "Intersect clipping area with the specified shape or bounds.
   Returns new clip (Shape or nil if empty)."
  ([x y w h]
     (clip (Rectangle2D$Double. x y w h)))
  ([shape]
     (let [a1 (Area. shape)
           a2 (if (instance? Area *clip*) *clip* (Area. *clip*))]
       (.transform a1 (relative-transform))
       (.intersect a1 a2)
       (if (.isEmpty a1)
         nil
         a1))))

(defn- ^Graphics2D apply-theme
  "Set graphics' color and font to match theme.
   Modifies and returns the first argument."
  ([]
     (apply-theme *graphics* *theme*))
  ([^Graphics2D graphics theme]
  (doto graphics
    (.setColor (:fore-color theme))
    (.setFont (:font theme)))))

(defn- ^Graphics2D create-graphics
  ([]
     (create-graphics 0 0 *width* *height*))
  ([x y w h]
     (apply-theme (.create *graphics* x y w h) *theme*)))

(defn with-bounds*
  [x y w h f & args]
  (when-let [clip (clip x y w h)]
    (let [graphics (create-graphics x y w h)]
      (try
        (binding [*width* w
                  *height* h
                  *clip* clip
                  *graphics* graphics]
          (apply f args))
        (finally
         (.dispose graphics))))))

(defmacro with-bounds
  [x y w h & body]
  `(with-bounds* ~x ~y ~w ~h (fn [] ~@body)))

(defn with-handlers*
  [handle handlers f & args]
  (binding
      [*event-dispatcher* (create-dispatcher
                           *event-dispatcher* handle handlers)]
    (apply f args)))

(defmacro with-handlers
  "specs => (:event-id name & handler-body)*

  Execute form with the specified event handlers."
  [handle form & specs]
  `(with-handlers* ~handle
     ~(reduce (fn [m spec]
                (assoc m (first spec)
                       `(fn [~(second spec)]
                          ~@(nnext spec)))) {}
                          specs)
     (fn [] ~form)))

(defmacro with-theme
  [theme & body]
  `(binding [*theme* (merge *theme* ~theme)]
     ~@body))

(defmacro with-color
  [color & body]
  `(let [color# (.getColor *graphics*)]
     (try
       (.setColor *graphics* ~color)
       ~@body
       (finally
        (.setColor *graphics* color#)))))

;; TODO:
;;
;; (with-transform
;;   (rotate ...)
;;   (draw ...)
;;   (scale ...)
;;   (draw ...))

(defmacro with-transform [transform & body]
  `(let [old-t# (.getTransform *graphics*)]
     (try
       (.transform *graphics* ~transform)
       ~@body
       (finally
        (.setTransform *graphics* old-t#)))))

(defmacro with-rotate [theta ax ay & body]
  `(let [transform# (AffineTransform/getRotateInstance ~theta ~ax ~ay)]
     (with-transform transform# ~@body)))

(defn- geometry-vec [geometry]
  (if (vector? geometry)
    geometry
    [(:x geometry) (:y geometry) (:width geometry) (:height geometry)]))

(defn draw!
  "Draws layer."
  ([layer]
     (let [graphics (create-graphics)]
       (try
         (binding [*graphics* graphics]
           (render! layer))
         (finally
          (.dispose graphics)))))
  ([layer x y]
     (let [size (layer-size layer)]
       (draw! layer x y (:width size) (:height size))))
  ([layer x y width height]
     (with-bounds* x y width height render! layer)))

;; TODO: объект-сцена вместо context-draw!
;; Сцена устанавливает контекст и рисует слой. Сцена - слой?
;; Сцена идентифицирует группу обозревателя слоев.
;; Обновления любого слоя, изображенного в сцене, вызывает обновление сцены.
;; Обозреватель сцены вызывает repaint().

(defn context-draw!
  "Sets up layer context, draws layer and commits event dispatcher."
  ([layer graphics event-dispatcher update-fn width height]
     (context-draw! layer nil graphics event-dispatcher update-fn width height))
  ([layer component ^Graphics2D graphics event-dispatcher update-fn
    width height]
     (binding [*graphics* graphics
               *font-context* (.getFontRenderContext graphics)
               *initial-transform* (.getTransform graphics)
               *inverse-initial-transform*
                 (-> graphics .getTransform .createInverse)
               *target* component
               *update* update-fn
               *event-dispatcher* event-dispatcher
               *width* width
               *height* height
               *clip* (Rectangle2D$Double. 0 0 width height)]
       (apply-theme)
       (render! layer)
       (commit event-dispatcher))))

(defn draw-anchored!
  "Draws layer. Location is relative to the layer's anchor point for
   the specified alignment."
  ([layer h-align v-align x y]
     (let [anchor (anchor layer h-align v-align)]
       (draw! layer (- x (:x anchor)) (- y (:y anchor)))))
  ([layer h-align v-align x y w h]
     (let [anchor (anchor layer h-align v-align)]
       (draw! layer (- x (:x anchor)) (- y (:y anchor)) w h))))

;;
;; EventDispatcher implementation
;;

(def awt-events
     {java.awt.event.MouseEvent/MOUSE_CLICKED  :mouse-clicked
      java.awt.event.MouseEvent/MOUSE_DRAGGED  :mouse-dragged
      java.awt.event.MouseEvent/MOUSE_ENTERED  :mouse-entered
      java.awt.event.MouseEvent/MOUSE_EXITED   :mouse-exited
      java.awt.event.MouseEvent/MOUSE_MOVED    :mouse-moved
      java.awt.event.MouseEvent/MOUSE_PRESSED  :mouse-pressed
      java.awt.event.MouseEvent/MOUSE_RELEASED :mouse-released})

(def dummy-event-dispatcher
     (reify
      EventDispatcher
      (listen! [this component])
      (create-dispatcher [this handle handlers] this)
      (commit [this])))

(defrecord DispatcherNode [handle handlers parent
                           ^Shape clip ^AffineTransform transform
                           bindings]
  EventDispatcher
  (listen! [this component]
     (listen! parent component))
  (create-dispatcher [this handle handlers]
     (create-dispatcher parent handle handlers))
  (commit [this]
     (commit parent)))

(defn- make-node [handle handlers]
  (DispatcherNode. handle handlers *event-dispatcher* *clip*
                   (inverse-relative-transform)
                   (get-thread-bindings)))

(defn- add-node [tree node]
  (assoc-cons tree (:parent node) node))

(defn- under-cursor
  "Returns a vector of child nodes under cursor."
  [x y tree node]
  (some #(if (.contains ^Shape (:clip %) x y)
           (conj (vec (under-cursor x y tree %)) %))
        (get tree node)))

(defn- remove-all [coll1 coll2 pred]
  (filter #(not (some (partial pred %) coll2)) coll1))

(defn- transform [^AffineTransform tr x y]
  (let [p (Point2D$Double. x y)]
    (.transform tr p p)
    [(.x p) (.y p)]))

(defn- translate-mouse-event [^java.awt.event.MouseEvent event
                              ^AffineTransform tr id]
  (let [[x y] (transform tr (.getX event) (.getY event))]
    (MouseEvent. id (.getWhen event) x y
                 (.getXOnScreen event) (.getYOnScreen event)
                 (.getButton event))))

(defn- translate-and-dispatch
  ([nodes first-only ^java.awt.event.MouseEvent event]
     (translate-and-dispatch nodes first-only
       event (awt-events (.getID event))))
  ([nodes first-only event id]
     (if-let [node (first nodes)]
       (if-let [handler (get (:handlers node) id)]
         (do
           (with-bindings* (:bindings node)
             handler
             (translate-mouse-event event (:transform node) id))
           (if-not first-only
             (recur (rest nodes) false event id)))
         (recur (rest nodes) first-only event id)))))

(defn- dispatch-mouse-motion
  "Dispatches mouse motion events."
  [hovered-ref tree root ^java.awt.event.MouseEvent event]
  (let [x (.getX event)
        y (.getY event)
        [hovered hovered2] (dosync
                            [@hovered-ref
                             (ref-set hovered-ref
                                      (under-cursor x y tree root))])
        pred #(= (:handle %1) (:handle %2))
        exited (remove-all hovered hovered2 pred)
        entered (remove-all hovered2 hovered pred)
        moved (remove-all hovered2 entered pred)]
    (translate-and-dispatch exited false event :mouse-exited)
    (translate-and-dispatch entered false event :mouse-entered)
    (translate-and-dispatch moved true event :mouse-moved)))

(defn- dispatch-mouse-button
  [picked-ref hovered-ref ^java.awt.event.MouseEvent event]
  (let [id (awt-events (.getID event))
        hovered (if (= id :mouse-pressed)
                  (dosync (ref-set picked-ref @hovered-ref))
                  @hovered-ref)]
    (translate-and-dispatch hovered true event id)))

(defn root-event-dispatcher []
  (let [tree-r (ref {})   ; register
        tree (ref {})     ; dispatch
        hovered (ref '())
        picked (ref '())]
    (reify
     EventDispatcher
     (listen! [this component]
        (doto component
          (.addMouseListener this)
          (.addMouseMotionListener this)))
     (create-dispatcher [this handle handlers]
        (let [node (make-node handle handlers)]
          (dosync (alter tree-r add-node node))
          node))
     (commit [this]
        (dosync (ref-set tree @tree-r)
                (ref-set tree-r {})))
     MouseListener
     (mouseEntered [this event]
        (dispatch-mouse-motion hovered @tree this event))
     (mouseExited [this event]
        (dispatch-mouse-motion hovered @tree this event))
     (mouseClicked [this event]
        (dispatch-mouse-button picked hovered event))
     (mousePressed [this event]
        (dispatch-mouse-button picked hovered event))
     (mouseReleased [this event]
        (translate-and-dispatch @picked true event))
        ;;(dispatch-mouse-button picked hovered event))
     MouseMotionListener
     (mouseDragged [this event]
        (translate-and-dispatch @picked true event))
     (mouseMoved [this event]
        (dispatch-mouse-motion hovered @tree this event)))))

;;
;; ИДЕИ:
;;
;; Контекст: биндинги или запись?
;;
;; Установка обработчиков (в контексте слоя):
;;
;; (listen
;;   (:mouse-entered e
;;     ...)
;;   (:mouse-exited e
;;     ...))
;;
;; Не надо IMGUI.
;; Построение сцены путем декорирования слоев:
;;
;; (listener
;;  (:action e (println e))
;;  (:mouse-dragged e (println e))
;;  (theme :font "Helvetica-14"
;;    (vbox
;;      (button (text-layer "Button 1"))
;;      (button (text-layer "Button 2")))))
;;