Sketch of pointer/message APIs
diff --git a/include/rive/artboard.hpp b/include/rive/artboard.hpp
index a246f5b..e9a32d8 100644
--- a/include/rive/artboard.hpp
+++ b/include/rive/artboard.hpp
@@ -6,9 +6,13 @@
 #include "rive/core_context.hpp"
 #include "rive/generated/artboard_base.hpp"
 #include "rive/hit_info.hpp"
+#include "rive/message.hpp"
+#include "rive/pointer_event.hpp"
 #include "rive/math/aabb.hpp"
 #include "rive/renderer.hpp"
 #include "rive/shapes/shape_paint_container.hpp"
+
+#include <queue>
 #include <vector>
 
 namespace rive {
@@ -40,6 +44,8 @@
         bool m_IsInstance = false;
         bool m_FrameOrigin = true;
 
+        std::queue<Message> m_MessageQueue;
+
         void sortDependencies();
         void sortDrawOrder();
 
@@ -51,6 +57,8 @@
         void addStateMachine(StateMachine* object);
         void addNestedArtboard(NestedArtboard* object);
 
+        void testing_only_enque_message(const Message&);
+
     public:
         ~Artboard();
         StatusCode initialize();
@@ -70,6 +78,24 @@
 
         bool advance(double elapsedSeconds);
 
+        // Call this to forward pointer events to the artboard
+        // They will be processed when advance() is called.
+        //
+        void postPointerEvent(const PointerEvent&);
+
+        // Returns true iff calling popMessage() will return true.
+        bool hasMessages() const;
+        
+        // If there are any queued messages...
+        //   copies the first message into msg parameter
+        //   removes that message from the queue
+        //   returns true
+        // else
+        //   ignores msg parameter
+        //   returns false
+        //
+        bool nextMessage(Message* msg);
+
         enum class DrawOption {
             kNormal,
             kHideBG,
diff --git a/include/rive/message.hpp b/include/rive/message.hpp
new file mode 100644
index 0000000..5fda832
--- /dev/null
+++ b/include/rive/message.hpp
@@ -0,0 +1,19 @@
+#ifndef _RIVE_MESSAGE_HPP_
+#define _RIVE_MESSAGE_HPP_
+
+#include "rive/math/vec2d.hpp"
+#include <string>
+
+namespace rive {
+
+struct Message {
+    // TODO -- how to represent this?
+    // Perhaps some sort of JSON-like object: key-value pairs?
+    // For now, store a string so we can test it...
+    
+    std::string m_Str;
+};
+                              
+} // namespace rive
+
+#endif
diff --git a/include/rive/pointer_event.hpp b/include/rive/pointer_event.hpp
new file mode 100644
index 0000000..7b29707
--- /dev/null
+++ b/include/rive/pointer_event.hpp
@@ -0,0 +1,24 @@
+#ifndef _RIVE_POINTER_EVENT_HPP_
+#define _RIVE_POINTER_EVENT_HPP_
+
+#include "rive/math/vec2d.hpp"
+
+namespace rive {
+
+enum class PointerEventType {
+    down,       // The button has gone from up to down
+    move,       // The pointer's position has changed
+    up,         // The button has gone from down to up
+};
+
+struct PointerEvent {
+    PointerEventType m_Type;
+    Vec2D            m_Position;
+    int              m_PointerIndex;
+
+    // add more fields as needed
+};
+                              
+} // namespace rive
+
+#endif
diff --git a/skia/viewer/build/premake5.lua b/skia/viewer/build/premake5.lua
index acc6885..1442293 100644
--- a/skia/viewer/build/premake5.lua
+++ b/skia/viewer/build/premake5.lua
@@ -31,7 +31,7 @@
        "../../dependencies/imgui/imgui.cpp", "../../dependencies/imgui/imgui_tables.cpp",
        "../../dependencies/imgui/imgui_draw.cpp"}
 
-buildoptions {"-Wall", "-fno-exceptions", "-fno-rtti", "-flto=full"}
+buildoptions {"-Wall", "-fno-exceptions", "-fno-rtti", "-flto=full", "-g"}
 filter "configurations:debug"
 defines {"DEBUG"}
 symbols "On"
diff --git a/skia/viewer/src/main.cpp b/skia/viewer/src/main.cpp
index c14e6e1..6ce3d05 100644
--- a/skia/viewer/src/main.cpp
+++ b/skia/viewer/src/main.cpp
@@ -129,6 +129,61 @@
     initAnimation(0);
 }
 
+// returns the mouse position, transforming it through the inverse of
+// the canvas' CTM -- which may have been altered to scale/translate
+// the artboard into the window.
+//
+static void post_mouse_event(rive::Artboard* artboard, const SkMatrix& ctm) {
+    static ImVec2 gPrevMousePos = {-1000, -1000};
+    const auto mouse = ImGui::GetMousePos();
+
+    static bool gPrevMouseButtonDown = false;
+    const bool isDown = ImGui::IsMouseDown(ImGuiMouseButton_Left);
+
+    if (mouse.x == gPrevMousePos.x &&
+        mouse.y == gPrevMousePos.y &&
+        isDown == gPrevMouseButtonDown)
+    {
+        return;
+    }
+    
+    auto evtType = rive::PointerEventType::move;
+    if (isDown && !gPrevMouseButtonDown) {
+        evtType = rive::PointerEventType::down; // we just went down
+    } else if (!isDown && gPrevMouseButtonDown) {
+        evtType = rive::PointerEventType::up; // we just went up
+    }
+
+    gPrevMousePos = mouse;
+    gPrevMouseButtonDown = isDown;
+
+    SkMatrix inv;
+    (void)ctm.invert(&inv);
+    
+    // scale by 2 for the DPI of a high-res monitor
+    const auto pt = inv.mapXY(mouse.x * 2, mouse.y * 2);
+    
+    const int pointerIndex = 0; // til we track more than one button/mouse
+    rive::PointerEvent evt = {
+        evtType,
+        {pt.fX, pt.fY},
+        pointerIndex,
+    };
+    artboard->postPointerEvent(evt);
+}
+
+static void test_messages(rive::Artboard* artboard) {
+    rive::Message msg;
+    int i = 0;
+    bool hasAny = artboard->hasMessages();
+
+    while (artboard->nextMessage(&msg)) {
+        printf("-- message[%d]: '%s'\n", i, msg.m_Str.c_str());
+        i += 1;
+    }
+    assert((hasAny && i > 0) || (!hasAny && i == 0));
+}
+
 int main() {
     if (!glfwInit()) {
         fprintf(stderr, "Failed to initialize glfw.\n");
@@ -244,6 +299,9 @@
                            rive::Alignment::center,
                            rive::AABB(0, 0, width, height),
                            artboard->bounds());
+
+            post_mouse_event(artboard, canvas->getTotalMatrix());
+
             artboard->draw(&renderer);
             renderer.restore();
         }
@@ -334,6 +392,8 @@
                 ImGui::Columns(1);
             }
             ImGui::End();
+            
+            test_messages(artboard);
         } else {
             ImGui::Text("Drop a .riv file to preview.");
         }
@@ -359,4 +419,4 @@
     glfwTerminate();
 
     return 0;
-}
\ No newline at end of file
+}
diff --git a/src/artboard.cpp b/src/artboard.cpp
index bdd4614..d9cb7cb 100644
--- a/src/artboard.cpp
+++ b/src/artboard.cpp
@@ -543,3 +543,60 @@
     }
     return result;
 }
+
+void Artboard::postPointerEvent(const PointerEvent& evt) {
+    if (true) {
+        static bool gButtonIsDown;
+
+        switch (evt.m_Type) {
+            case PointerEventType::down:
+                assert(!gButtonIsDown);
+                gButtonIsDown = true;
+                break;
+            case PointerEventType::up:
+                assert(gButtonIsDown);
+                gButtonIsDown = false;
+                break;
+            default: break;
+        }
+
+#if 0
+        const char* typeNames[] = {
+            "down", "move", "up  ",
+        };
+        printf("pointer: %s [%g %g] %s\n",
+               typeNames[(int)evt.m_Type],
+               evt.m_Position.x(), evt.m_Position.y(),
+               gButtonIsDown ? "DOWN" : "UP");
+#endif
+    }
+
+#if 0
+    // TESTING ONLY
+    // This is the sort of message that the Artboard would post...
+    // e.g. if the down AND up were inside the same clickable shape.
+    if (evt.m_Type == PointerEventType::up) {
+        Message msg;
+        msg.m_Str = "ClickEvent";
+        this->testing_only_enque_message(msg);
+    }
+#endif
+}
+
+void Artboard::testing_only_enque_message(const Message& msg) {
+    m_MessageQueue.push(msg);
+}
+
+bool Artboard::hasMessages() const {
+    return !m_MessageQueue.empty();
+}
+
+bool Artboard::nextMessage(Message* msg) {
+    if (m_MessageQueue.empty()) {
+        return false;
+    } else {
+        *msg = m_MessageQueue.front();
+        m_MessageQueue.pop();
+        return true;
+    }
+}