diff --git a/frontend/src/app/main/data/workspace/comments.cljs b/frontend/src/app/main/data/workspace/comments.cljs
index 46919efa4..3c3f71f74 100644
--- a/frontend/src/app/main/data/workspace/comments.cljs
+++ b/frontend/src/app/main/data/workspace/comments.cljs
@@ -20,6 +20,7 @@
    [app.main.data.workspace.viewport :as dwv]
    [app.main.repo :as rp]
    [app.main.streams :as ms]
+   [app.util.mouse :as mse]
    [app.util.router :as rt]
    [beicon.core :as rx]
    [potok.core :as ptk]))
@@ -37,7 +38,8 @@
         (rx/merge
          (rx/of (dcm/retrieve-comment-threads file-id))
          (->> stream
-              (rx/filter ms/mouse-click?)
+              (rx/filter mse/mouse-event?)
+              (rx/filter mse/mouse-click-event?)
               (rx/switch-map #(rx/take 1 ms/mouse-position))
               (rx/with-latest-from ms/keyboard-space)
               (rx/filter (fn [[_ space]] (not space)) )
diff --git a/frontend/src/app/main/data/workspace/drawing/box.cljs b/frontend/src/app/main/data/workspace/drawing/box.cljs
index 7df56e1f0..d8ccdf4c9 100644
--- a/frontend/src/app/main/data/workspace/drawing/box.cljs
+++ b/frontend/src/app/main/data/workspace/drawing/box.cljs
@@ -24,6 +24,7 @@
    [app.main.data.workspace.state-helpers :as wsh]
    [app.main.snap :as snap]
    [app.main.streams :as ms]
+   [app.util.mouse :as mse]
    [beicon.core :as rx]
    [potok.core :as ptk]))
 
@@ -76,7 +77,13 @@
   (ptk/reify ::handle-drawing
     ptk/WatchEvent
     (watch [_ state stream]
-      (let [stoper       (rx/filter #(or (ms/mouse-up? %) (= % :interrupt))  stream)
+      (let [stoper       (rx/merge
+                          (->> stream
+                               (rx/filter mse/mouse-event?)
+                               (rx/filter mse/mouse-up-event?))
+                          (->> stream
+                               (rx/filter #(= % :interrupt))))
+
             layout       (get state :workspace-layout)
             zoom         (dm/get-in state [:workspace-local :zoom] 1)
 
diff --git a/frontend/src/app/main/data/workspace/drawing/curve.cljs b/frontend/src/app/main/data/workspace/drawing/curve.cljs
index d0b1aed44..f77078281 100644
--- a/frontend/src/app/main/data/workspace/drawing/curve.cljs
+++ b/frontend/src/app/main/data/workspace/drawing/curve.cljs
@@ -21,6 +21,7 @@
    [app.main.data.workspace.drawing.common :as common]
    [app.main.data.workspace.state-helpers :as wsh]
    [app.main.streams :as ms]
+   [app.util.mouse :as mse]
    [app.util.path.simplify-curve :as ups]
    [beicon.core :as rx]
    [potok.core :as ptk]))
@@ -29,7 +30,8 @@
 
 (defn stoper-event?
   [{:keys [type] :as event}]
-  (ms/mouse-event? event) (= type :up))
+  (and (mse/mouse-event? event)
+       (= type :up)))
 
 (defn- insert-point
   [point]
diff --git a/frontend/src/app/main/data/workspace/interactions.cljs b/frontend/src/app/main/data/workspace/interactions.cljs
index 622eeb70b..a7610d2e5 100644
--- a/frontend/src/app/main/data/workspace/interactions.cljs
+++ b/frontend/src/app/main/data/workspace/interactions.cljs
@@ -19,6 +19,7 @@
    [app.main.data.workspace.state-helpers :as wsh]
    [app.main.data.workspace.undo :as dwu]
    [app.main.streams :as ms]
+   [app.util.mouse :as mse]
    [beicon.core :as rx]
    [potok.core :as ptk]))
 
@@ -187,7 +188,9 @@
     (watch [_ state stream]
       (let [initial-pos @ms/mouse-position
             selected (wsh/lookup-selected state)
-            stopper (rx/filter ms/mouse-up? stream)]
+            stopper  (->> stream
+                           (rx/filter mse/mouse-event?)
+                           (rx/filter mse/mouse-up-event?))]
         (when (= 1 (count selected))
           (rx/concat
             (->> ms/mouse-position
@@ -295,7 +298,9 @@
     (watch [_ state stream]
       (let [initial-pos @ms/mouse-position
             selected (wsh/lookup-selected state)
-            stopper (rx/filter ms/mouse-up? stream)]
+            stopper  (->> stream
+                           (rx/filter mse/mouse-event?)
+                           (rx/filter mse/mouse-up-event?))]
         (when (= 1 (count selected))
           (let [page-id     (:current-page-id state)
                 objects     (wsh/lookup-page-objects state page-id)
diff --git a/frontend/src/app/main/data/workspace/notifications.cljs b/frontend/src/app/main/data/workspace/notifications.cljs
index 4b5916055..8a88ddf70 100644
--- a/frontend/src/app/main/data/workspace/notifications.cljs
+++ b/frontend/src/app/main/data/workspace/notifications.cljs
@@ -16,8 +16,8 @@
    [app.main.data.workspace.changes :as dch]
    [app.main.data.workspace.libraries :as dwl]
    [app.main.data.workspace.persistence :as dwp]
-   [app.main.streams :as ms]
    [app.util.globals :refer [global]]
+   [app.util.mouse :as mse]
    [app.util.object :as obj]
    [app.util.time :as dt]
    [beicon.core :as rx]
@@ -81,7 +81,7 @@
                              ;; Emit to all other connected users the current pointer
                              ;; position changes.
                              (->> stream
-                                  (rx/filter ms/pointer-event?)
+                                  (rx/filter mse/pointer-event?)
                                   (rx/sample 50)
                                   (rx/map #(handle-pointer-send file-id (:pt %)))))
 
diff --git a/frontend/src/app/main/data/workspace/path/drawing.cljs b/frontend/src/app/main/data/workspace/path/drawing.cljs
index a0dd03249..912a90718 100644
--- a/frontend/src/app/main/data/workspace/path/drawing.cljs
+++ b/frontend/src/app/main/data/workspace/path/drawing.cljs
@@ -26,7 +26,7 @@
    [app.main.data.workspace.path.streams :as streams]
    [app.main.data.workspace.path.undo :as undo]
    [app.main.data.workspace.state-helpers :as wsh]
-   [app.main.streams :as ms]
+   [app.util.mouse :as mse]
    [beicon.core :as rx]
    [potok.core :as ptk]))
 
@@ -122,16 +122,12 @@
 
 (declare close-path-drag-end)
 
-(defn close-path-drag-start [position]
+(defn close-path-drag-start
+  [position]
   (ptk/reify ::close-path-drag-start
     ptk/WatchEvent
     (watch [_ state stream]
-      (let [stop-stream
-            (->> stream (rx/filter #(or (helpers/end-path-event? %)
-                                        (ms/mouse-up? %))))
-
-            content (st/get-path state :content)
-
+      (let [content  (st/get-path state :content)
             handlers (-> (upc/content->handlers content)
                          (get position))
 
@@ -140,8 +136,14 @@
 
             drag-events-stream
             (->> (streams/position-stream)
-                 (rx/take-until stop-stream)
-                 (rx/map #(drag-handler position idx prefix %)))]
+                 (rx/map #(drag-handler position idx prefix %))
+                 (rx/take-until
+                  (rx/merge
+                   (->> stream
+                        (rx/filter mse/mouse-event?)
+                        (rx/filter mse/mouse-up-event?))
+                   (->> stream
+                        (rx/filter helpers/end-path-event?)))))]
 
         (rx/concat
          (rx/of (add-node position))
@@ -163,12 +165,16 @@
   (ptk/reify ::start-path-from-point
     ptk/WatchEvent
     (watch [_ _ stream]
-      (let [mouse-up    (->> stream (rx/filter #(or (helpers/end-path-event? %)
-                                                    (ms/mouse-up? %))))
-            drag-events (->> (streams/position-stream)
-                             (rx/take-until mouse-up)
-                             (rx/map #(drag-handler %)))]
+      (let [stoper (rx/merge
+                    (->> stream
+                         (rx/filter mse/mouse-event?)
+                         (rx/filter mse/mouse-up-event?))
+                    (->> stream
+                         (rx/filter helpers/end-path-event?)))
 
+            drag-events (->> (streams/position-stream)
+                             (rx/map #(drag-handler %))
+                             (rx/take-until stoper))]
         (rx/concat
          (rx/of (add-node position))
          (streams/drag-stream
@@ -185,13 +191,16 @@
 
 (defn make-drag-stream
   [stream down-event]
-  (let [mouse-up    (->> stream (rx/filter #(or (helpers/end-path-event? %)
-                                                (ms/mouse-up? %))))
+  (let [stoper (rx/merge
+                (->> stream
+                     (rx/filter mse/mouse-event?)
+                     (rx/filter mse/mouse-up-event?))
+                (->> stream
+                     (rx/filter helpers/end-path-event?)))
 
         drag-events (->> (streams/position-stream)
-                         (rx/take-until mouse-up)
-                         (rx/map #(drag-handler %)))]
-
+                         (rx/map #(drag-handler %))
+                         (rx/take-until stoper))]
     (rx/concat
      (rx/of (add-node down-event))
      (streams/drag-stream
@@ -209,8 +218,11 @@
 
     ptk/WatchEvent
     (watch [_ _ stream]
-      (let [mouse-down      (->> stream (rx/filter ms/mouse-down?))
-            end-path-events (->> stream (rx/filter helpers/end-path-event?))
+      (let [mouse-down      (->> stream
+                                 (rx/filter mse/mouse-event?)
+                                 (rx/filter mse/mouse-down-event?))
+            end-path-events (->> stream
+                                 (rx/filter helpers/end-path-event?))
 
             ;; Mouse move preview
             mousemove-events
diff --git a/frontend/src/app/main/data/workspace/path/edition.cljs b/frontend/src/app/main/data/workspace/path/edition.cljs
index f1a9c8bc9..31080603c 100644
--- a/frontend/src/app/main/data/workspace/path/edition.cljs
+++ b/frontend/src/app/main/data/workspace/path/edition.cljs
@@ -26,6 +26,7 @@
    [app.main.data.workspace.path.undo :as undo]
    [app.main.data.workspace.state-helpers :as wsh]
    [app.main.streams :as ms]
+   [app.util.mouse :as mse]
    [app.util.path.tools :as upt]
    [beicon.core :as rx]
    [potok.core :as ptk]))
@@ -150,7 +151,10 @@
   (ptk/reify ::drag-selected-points
     ptk/WatchEvent
     (watch [_ state stream]
-      (let [stopper (->> stream (rx/filter ms/mouse-up?))
+      (let [stopper (->> stream
+                         (rx/filter mse/mouse-event?)
+                         (rx/filter mse/mouse-up-event?))
+
             id (dm/get-in state [:workspace-local :edition])
 
             selected-points (dm/get-in state [:workspace-local :edit-path id :selected-points] #{})
@@ -263,8 +267,6 @@
          (rx/concat
           (rx/of (dch/update-shapes [id] upsp/convert-to-path))
           (->> (streams/move-handler-stream handler point handler opposite points)
-               (rx/take-until (->> stream (rx/filter #(or (ms/mouse-up? %)
-                                                          (streams/finish-edition? %)))))
                (rx/map
                 (fn [{:keys [x y alt? shift?]}]
                   (let [pos (cond-> (gpt/point x y)
@@ -275,7 +277,15 @@
                      prefix
                      (+ start-delta-x (- (:x pos) (:x handler)))
                      (+ start-delta-y (- (:y pos) (:y handler)))
-                     (not alt?))))))
+                     (not alt?)))))
+               (rx/take-until
+                (rx/merge
+                 (->> stream
+                      (rx/filter mse/mouse-event?)
+                      (rx/filter mse/mouse-up-event?))
+                 (->> stream
+                      (rx/filter streams/finish-edition?)))))
+
           (rx/concat (rx/of (apply-content-modifiers)))))))))
 
 (declare stop-path-edit)
diff --git a/frontend/src/app/main/data/workspace/path/helpers.cljs b/frontend/src/app/main/data/workspace/path/helpers.cljs
index 5792a69f8..4249c23fe 100644
--- a/frontend/src/app/main/data/workspace/path/helpers.cljs
+++ b/frontend/src/app/main/data/workspace/path/helpers.cljs
@@ -14,16 +14,19 @@
    [app.common.svg.path.command :as upc]
    [app.common.svg.path.subpath :as ups]
    [app.main.data.workspace.path.common :as common]
-   [app.main.streams :as ms]
+   [app.util.mouse :as mse]
    [potok.core :as ptk]))
 
-(defn end-path-event? [event]
-  (or (= (ptk/type event) ::common/finish-path)
-      (= (ptk/type event) :app.main.data.workspace.path.shortcuts/esc-pressed)
-      (= :app.main.data.workspace.common/clear-edition-mode (ptk/type event))
-      (= :app.main.data.workspace/finalize-page (ptk/type event))
-      (= event :interrupt) ;; ESC
-      (ms/mouse-double-click? event)))
+(defn end-path-event?
+  [event]
+  (let [type (ptk/type event)]
+    (or (= type ::common/finish-path)
+        (= type :app.main.data.workspace.path.shortcuts/esc-pressed)
+        (= type :app.main.data.workspace.common/clear-edition-mode)
+        (= type :app.main.data.workspace/finalize-page)
+        (= event :interrupt) ;; ESC
+        (and ^boolean (mse/mouse-event? event)
+             ^boolean (mse/mouse-double-click-event? event)))))
 
 (defn content-center
   [content]
diff --git a/frontend/src/app/main/data/workspace/path/selection.cljs b/frontend/src/app/main/data/workspace/path/selection.cljs
index 4d517eaf6..24b7b0bf4 100644
--- a/frontend/src/app/main/data/workspace/path/selection.cljs
+++ b/frontend/src/app/main/data/workspace/path/selection.cljs
@@ -13,6 +13,7 @@
    [app.main.data.workspace.common :as dwc]
    [app.main.data.workspace.path.state :as st]
    [app.main.streams :as ms]
+   [app.util.mouse :as mse]
    [beicon.core :as rx]
    [potok.core :as ptk]))
 
@@ -118,16 +119,21 @@
     (ptk/reify ::handle-area-selection
       ptk/WatchEvent
       (watch [_ state stream]
-        (let [zoom (get-in state [:workspace-local :zoom] 1)
-              stop? (fn [event] (or (dwc/interrupt? event) (ms/mouse-up? event)))
-              stoper (->> stream (rx/filter stop?))
+        (let [zoom   (get-in state [:workspace-local :zoom] 1)
+              stoper (rx/merge
+                      (->> stream
+                           (rx/filter mse/mouse-event?)
+                           (rx/filter mse/mouse-up-event?))
+                      (->> stream
+                           (rx/filter dwc/interrupt?)))
+
               from-p @ms/mouse-position]
           (rx/concat
            (->> ms/mouse-position
-                (rx/take-until stoper)
                 (rx/map #(grc/points->rect [from-p %]))
                 (rx/filter (partial valid-rect? zoom))
-                (rx/map update-area-selection))
+                (rx/map update-area-selection)
+                (rx/take-until stoper))
 
            (rx/of (select-node-area shift?)
                   (clear-area-selection))))))))
diff --git a/frontend/src/app/main/data/workspace/path/streams.cljs b/frontend/src/app/main/data/workspace/path/streams.cljs
index 071e6ab12..264a145a1 100644
--- a/frontend/src/app/main/data/workspace/path/streams.cljs
+++ b/frontend/src/app/main/data/workspace/path/streams.cljs
@@ -14,6 +14,7 @@
    [app.main.snap :as snap]
    [app.main.store :as st]
    [app.main.streams :as ms]
+   [app.util.mouse :as mse]
    [beicon.core :as rx]
    [okulary.core :as l]
    [potok.core :as ptk]))
@@ -50,16 +51,20 @@
    (let [zoom  (get-in @st/state [:workspace-local :zoom] 1)
 
          start (-> @ms/mouse-position to-pixel-snap)
-         mouse-up (->> st/stream
-                       (rx/filter #(or (finish-edition? %)
-                                       (ms/mouse-up? %))))
+
+         stoper (rx/merge
+                    (->> st/stream
+                         (rx/filter mse/mouse-event?)
+                         (rx/filter mse/mouse-up-event?))
+                    (->> st/stream
+                         (rx/filter finish-edition?)))
 
          position-stream
          (->> ms/mouse-position
-              (rx/take-until mouse-up)
               (rx/map to-pixel-snap)
               (rx/filter (dragging? start zoom))
-              (rx/take 1))]
+              (rx/take 1)
+              (rx/take-until stoper))]
 
      (rx/merge
       (->> position-stream
diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs
index fdaaaeb99..aac220262 100644
--- a/frontend/src/app/main/data/workspace/selection.cljs
+++ b/frontend/src/app/main/data/workspace/selection.cljs
@@ -35,12 +35,15 @@
    [app.main.refs :as refs]
    [app.main.streams :as ms]
    [app.main.worker :as uw]
+   [app.util.mouse :as mse]
    [beicon.core :as rx]
    [clojure.set :as set]
    [linked.set :as lks]
    [potok.core :as ptk]))
 
-(defn interrupt? [e] (= e :interrupt))
+(defn interrupt?
+  [e]
+  (= e :interrupt))
 
 ;; --- Selection Rect
 
@@ -60,8 +63,12 @@
     ptk/WatchEvent
     (watch [_ state stream]
       (let [zoom   (dm/get-in state [:workspace-local :zoom] 1)
-            stop?  (fn [event] (or (interrupt? event) (ms/mouse-up? event)))
-            stoper (rx/filter stop? stream)
+            stoper (rx/merge
+                    (->> stream
+                         (rx/filter mse/mouse-event?)
+                         (rx/filter mse/mouse-up-event?))
+                    (->> stream
+                         (rx/filter interrupt?)))
 
             init-position @ms/mouse-position
 
diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs
index 2855b1686..9f3434396 100644
--- a/frontend/src/app/main/data/workspace/transforms.cljs
+++ b/frontend/src/app/main/data/workspace/transforms.cljs
@@ -34,6 +34,7 @@
    [app.main.streams :as ms]
    [app.util.dom :as dom]
    [app.util.keyboard :as kbd]
+   [app.util.mouse :as mse]
    [beicon.core :as rx]
    [potok.core :as ptk]))
 
@@ -215,7 +216,9 @@
       ptk/WatchEvent
       (watch [_ state stream]
         (let [initial-position @ms/mouse-position
-              stopper (rx/filter ms/mouse-up? stream)
+              stopper (->> stream
+                           (rx/filter mse/mouse-event?)
+                           (rx/filter mse/mouse-up-event?))
               layout  (:workspace-layout state)
               page-id (:current-page-id state)
               focus   (:workspace-focus-selected state)
@@ -306,7 +309,10 @@
 
     ptk/WatchEvent
     (watch [_ _ stream]
-      (let [stoper          (rx/filter ms/mouse-up? stream)
+      (let [stoper          (->> stream
+                                 (rx/filter mse/mouse-event?)
+                                 (rx/filter mse/mouse-up-event?))
+
             group           (gsh/shapes->rect shapes)
             group-center    (grc/rect->center group)
             initial-angle   (gpt/angle @ms/mouse-position group-center)
@@ -370,7 +376,10 @@
      (watch [_ state stream]
        (let [initial  (deref ms/mouse-position)
 
-             stopper  (rx/filter ms/mouse-up? stream)
+             stopper  (->> stream
+                           (rx/filter mse/mouse-event?)
+                           (rx/filter mse/mouse-up-event?))
+
              zoom    (get-in state [:workspace-local :zoom] 1)
 
              ;; We toggle the selection so we don't have to wait for the event
@@ -444,7 +453,11 @@
              ids     (if (nil? ids) selected ids)
              shapes  (mapv #(get objects %) ids)
              duplicate-move-started? (get-in state [:workspace-local :duplicate-move-started?] false)
-             stopper (rx/filter ms/mouse-up? stream)
+
+             stopper (->> stream
+                          (rx/filter mse/mouse-event?)
+                          (rx/filter mse/mouse-up-event?))
+
              layout  (get state :workspace-layout)
              zoom    (get-in state [:workspace-local :zoom] 1)
              focus   (:workspace-focus-selected state)
diff --git a/frontend/src/app/main/data/workspace/viewport.cljs b/frontend/src/app/main/data/workspace/viewport.cljs
index dacc3ec22..1bbb48ccf 100644
--- a/frontend/src/app/main/data/workspace/viewport.cljs
+++ b/frontend/src/app/main/data/workspace/viewport.cljs
@@ -15,7 +15,7 @@
    [app.common.geom.shapes :as gsh]
    [app.common.math :as mth]
    [app.main.data.workspace.state-helpers :as wsh]
-   [app.main.streams :as ms]
+   [app.util.mouse :as mse]
    [beicon.core :as rx]
    [potok.core :as ptk]))
 
@@ -155,7 +155,7 @@
           (rx/concat
            (rx/of #(-> % (assoc-in [:workspace-local :panning] true)))
            (->> stream
-                (rx/filter ms/pointer-event?)
+                (rx/filter mse/pointer-event?)
                 (rx/filter #(= :delta (:source %)))
                 (rx/map :pt)
                 (rx/take-until stopper)
diff --git a/frontend/src/app/main/data/workspace/zoom.cljs b/frontend/src/app/main/data/workspace/zoom.cljs
index 0117631fa..8aba81a0b 100644
--- a/frontend/src/app/main/data/workspace/zoom.cljs
+++ b/frontend/src/app/main/data/workspace/zoom.cljs
@@ -14,6 +14,7 @@
    [app.common.geom.shapes :as gsh]
    [app.main.data.workspace.state-helpers :as wsh]
    [app.main.streams :as ms]
+   [app.util.mouse :as mse]
    [beicon.core :as rx]
    [potok.core :as ptk]))
 
@@ -118,7 +119,7 @@
           (rx/concat
            (rx/of #(-> % (assoc-in [:workspace-local :zooming] true)))
            (->> stream
-                (rx/filter ms/pointer-event?)
+                (rx/filter mse/pointer-event?)
                 (rx/filter #(= :delta (:source %)))
                 (rx/map :pt)
                 (rx/take-until stopper)
diff --git a/frontend/src/app/main/streams.cljs b/frontend/src/app/main/streams.cljs
index 124ab9360..b17f5808d 100644
--- a/frontend/src/app/main/streams.cljs
+++ b/frontend/src/app/main/streams.cljs
@@ -11,76 +11,43 @@
    [app.main.store :as st]
    [app.util.globals :as globals]
    [app.util.keyboard :as kbd]
+   [app.util.mouse :as mse]
    [beicon.core :as rx]))
 
 ;; --- User Events
 
-(defrecord MouseEvent [type ctrl shift alt meta])
-(defrecord PointerEvent [source pt ctrl shift alt meta])
-(defrecord ScrollEvent [point])
-
-(defn mouse-event?
-  [v]
-  (instance? MouseEvent v))
-
-(defn mouse-down?
-  [v]
-  (and (mouse-event? v)
-       (= :down (:type v))))
-
-(defn mouse-up?
-  [v]
-  (and (mouse-event? v)
-       (= :up (:type v))))
-
-(defn mouse-click?
-  [v]
-  (and (mouse-event? v)
-       (= :click (:type v))))
-
-(defn mouse-double-click?
-  [v]
-  (and (mouse-event? v)
-       (= :double-click (:type v))))
-
-(defn pointer-event?
-  [v]
-  (instance? PointerEvent v))
-
-(defn scroll-event?
-  [v]
-  (instance? ScrollEvent v))
-
 (defn interaction-event?
   [event]
-  (or (kbd/keyboard-event? event)
-      (mouse-event? event)))
+  (or ^boolean (kbd/keyboard-event? event)
+      ^boolean (mse/mouse-event? event)))
 
 ;; --- Derived streams
 
+(defonce ^:private pointer
+  (->> st/stream
+       (rx/filter mse/pointer-event?)
+       (rx/share)))
+
 (defonce mouse-position
   (let [sub (rx/behavior-subject nil)
-        ob  (->> st/stream
-                 (rx/filter pointer-event?)
-                 (rx/filter #(= :viewport (:source %)))
-                 (rx/map :pt))]
+        ob  (->> pointer
+                 (rx/filter #(= :viewport (mse/get-pointer-source %)))
+                 (rx/map mse/get-pointer-position))]
     (rx/subscribe-with ob sub)
     sub))
 
 (defonce mouse-position-ctrl
   (let [sub (rx/behavior-subject nil)
-        ob  (->> st/stream
-                 (rx/filter pointer-event?)
-                 (rx/map :ctrl)
+        ob  (->> pointer
+                 (rx/map mse/get-pointer-ctrl-mod)
                  (rx/dedupe))]
     (rx/subscribe-with ob sub)
     sub))
 
 (defonce mouse-position-meta
   (let [sub (rx/behavior-subject nil)
-        ob  (->> st/stream
-                 (rx/filter pointer-event?)
-                 (rx/map :meta)
+        ob  (->> pointer
+                 (rx/map mse/get-pointer-meta-mod)
                  (rx/dedupe))]
     (rx/subscribe-with ob sub)
     sub))
@@ -92,18 +59,16 @@
 
 (defonce mouse-position-shift
   (let [sub (rx/behavior-subject nil)
-        ob  (->> st/stream
-                 (rx/filter pointer-event?)
-                 (rx/map :shift)
+        ob  (->> pointer
+                 (rx/map mse/get-pointer-shift-mod)
                  (rx/dedupe))]
     (rx/subscribe-with ob sub)
     sub))
 
 (defonce mouse-position-alt
   (let [sub (rx/behavior-subject nil)
-        ob  (->> st/stream
-                 (rx/filter pointer-event?)
-                 (rx/map :alt)
+        ob  (->> pointer
+                 (rx/map mse/get-pointer-alt-mod)
                  (rx/dedupe))]
     (rx/subscribe-with ob sub)
     sub))
diff --git a/frontend/src/app/main/ui/workspace/viewport/actions.cljs b/frontend/src/app/main/ui/workspace/viewport/actions.cljs
index 7c2280c42..82b983c0b 100644
--- a/frontend/src/app/main/ui/workspace/viewport/actions.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport/actions.cljs
@@ -21,12 +21,12 @@
    [app.main.data.workspace.specialized-panel :as-alias dwsp]
    [app.main.refs :as refs]
    [app.main.store :as st]
-   [app.main.streams :as ms]
    [app.main.ui.workspace.viewport.viewport-ref :as uwvv]
    [app.util.dom :as dom]
    [app.util.dom.dnd :as dnd]
    [app.util.dom.normalize-wheel :as nw]
    [app.util.keyboard :as kbd]
+   [app.util.mouse :as mse]
    [app.util.object :as obj]
    [app.util.timers :as timers]
    [app.util.webapi :as wapi]
@@ -85,7 +85,7 @@
 
              left-click?
              (do
-               (st/emit! (ms/->MouseEvent :down ctrl? shift? alt? meta?)
+               (st/emit! (mse/->MouseEvent :down ctrl? shift? alt? meta?)
                          ::dwsp/interrupt)
 
                (when (and (not= edition id) (or text-editing? grid-editing?))
@@ -173,7 +173,7 @@
              hovering? (some? @hover)
              raw-pt (dom/get-client-position event)
              pt     (uwvv/point->viewport raw-pt)]
-         (st/emit! (ms/->MouseEvent :click ctrl? shift? alt? meta?))
+         (st/emit! (mse/->MouseEvent :click ctrl? shift? alt? meta?))
 
          (when (and hovering?
                     (not @space?)
@@ -213,7 +213,7 @@
 
              grid-layout-id (->> @hover-ids reverse (d/seek (partial ctl/grid-layout? objects)))]
 
-         (st/emit! (ms/->MouseEvent :double-click ctrl? shift? alt? meta?))
+         (st/emit! (mse/->MouseEvent :double-click ctrl? shift? alt? meta?))
 
          ;; Emit asynchronously so the double click to exit shapes won't break
          (timers/schedule
@@ -283,7 +283,7 @@
            middle-click? (= 2 (.-which event))]
 
        (when left-click?
-         (st/emit! (ms/->MouseEvent :up ctrl? shift? alt? meta?)))
+         (st/emit! (mse/->MouseEvent :up ctrl? shift? alt? meta?)))
 
        (when middle-click?
          (dom/prevent-default event)
@@ -357,16 +357,16 @@
 
          (rx/push! move-stream pt)
          (reset! last-position raw-pt)
-         (st/emit! (ms/->PointerEvent :delta delta
-                                      (kbd/ctrl? event)
-                                      (kbd/shift? event)
-                                      (kbd/alt? event)
-                                      (kbd/meta? event)))
-         (st/emit! (ms/->PointerEvent :viewport pt
-                                      (kbd/ctrl? event)
-                                      (kbd/shift? event)
-                                      (kbd/alt? event)
-                                      (kbd/meta? event))))))))
+         (st/emit! (mse/->PointerEvent :delta delta
+                                       (kbd/ctrl? event)
+                                       (kbd/shift? event)
+                                       (kbd/alt? event)
+                                       (kbd/meta? event)))
+         (st/emit! (mse/->PointerEvent :viewport pt
+                                       (kbd/ctrl? event)
+                                       (kbd/shift? event)
+                                       (kbd/alt? event)
+                                       (kbd/meta? event))))))))
 
 (defn on-mouse-wheel [zoom]
   (mf/use-callback
diff --git a/frontend/src/app/main/ui/workspace/viewport/gradients.cljs b/frontend/src/app/main/ui/workspace/viewport/gradients.cljs
index f90343331..6fd05d586 100644
--- a/frontend/src/app/main/ui/workspace/viewport/gradients.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport/gradients.cljs
@@ -15,8 +15,8 @@
    [app.main.data.workspace.colors :as dc]
    [app.main.refs :as refs]
    [app.main.store :as st]
-   [app.main.streams :as ms]
    [app.util.dom :as dom]
+   [app.util.mouse :as mse]
    [beicon.core :as rx]
    [cuerdas.core :as str]
    [rumext.v2 :as mf]))
@@ -154,14 +154,14 @@
      (mf/deps @moving-point from-p to-p width-p)
      (fn []
        (let [subs (->> st/stream
-                       (rx/filter ms/pointer-event?)
-                       (rx/filter #(= :viewport (:source %)))
-                       (rx/map :pt)
+                       (rx/filter mse/pointer-event?)
+                       (rx/filter #(= :viewport (mse/get-pointer-source %)))
+                       (rx/map mse/get-pointer-position)
                        (rx/subs
                         (fn [pt]
                           (case @moving-point
-                            :from-p (when on-change-start (on-change-start pt))
-                            :to-p (when on-change-finish (on-change-finish pt))
+                            :from-p  (when on-change-start (on-change-start pt))
+                            :to-p    (when on-change-finish (on-change-finish pt))
                             :width-p (when on-change-width
                                        (let [width-v (gpt/unit (gpt/to-vec from-p width-p))
                                              distance (gpt/point-line-distance pt from-p to-p)
diff --git a/frontend/src/app/util/mouse.cljs b/frontend/src/app/util/mouse.cljs
new file mode 100644
index 000000000..6102fecfa
--- /dev/null
+++ b/frontend/src/app/util/mouse.cljs
@@ -0,0 +1,63 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) KALEIDOS INC
+
+(ns app.util.mouse)
+
+(defrecord MouseEvent [type ctrl shift alt meta])
+(defrecord PointerEvent [source pt ctrl shift alt meta])
+(defrecord ScrollEvent [point])
+
+(defn mouse-event?
+  [v]
+  (instance? MouseEvent v))
+
+(defn pointer-event?
+  [v]
+  (instance? PointerEvent v))
+
+(defn scroll-event?
+  [v]
+  (instance? ScrollEvent v))
+
+(defn mouse-down-event?
+  [^MouseEvent v]
+  (= :down (.-type v)))
+
+(defn mouse-up-event?
+  [^MouseEvent v]
+  (= :up (.-type v)))
+
+(defn mouse-click-event?
+  [^MouseEvent v]
+  (= :click (.-type v)))
+
+(defn mouse-double-click-event?
+  [^MouseEvent v]
+  (= :double-click (.-type v)))
+
+(defn get-pointer-source
+  [^PointerEvent ev]
+  (.-source ev))
+
+(defn get-pointer-position
+  [^PointerEvent ev]
+  (.-pt ev))
+
+(defn get-pointer-ctrl-mod
+  [^PointerEvent ev]
+  (.-ctrl ev))
+
+(defn get-pointer-meta-mod
+  [^PointerEvent ev]
+  (.-meta ev))
+
+(defn get-pointer-alt-mod
+  [^PointerEvent ev]
+  (.-meta ev))
+
+(defn get-pointer-shift-mod
+  [^PointerEvent ev]
+  (.-meta ev))