sort hit shapes when draw order changes and stop propagation on hit s…

sort hit shapes when draw order changes and stop propagation on hit success

Diffs=
8bca56dca sort hit shapes when draw order changes and stop propagation on hit s… (#6624)

Co-authored-by: hernan <hernan@rive.app>
diff --git a/.rive_head b/.rive_head
index a6def14..cf570cd 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-9d605a1feb39dcad526ac9dda6b53b547921c58d
+8bca56dcaffd0f563a91f628b0ed432eca71acb5
diff --git a/dev/defs/artboard.json b/dev/defs/artboard.json
index 628ce9b..8b1f39a 100644
--- a/dev/defs/artboard.json
+++ b/dev/defs/artboard.json
@@ -123,6 +123,17 @@
       "description": "List of selected animations",
       "runtime": false,
       "coop": false
+    },
+    "viewModelId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 434,
+        "string": "viewmodelid"
+      },
+      "description": "The view model attached to this artboard data context."
     }
   }
 }
\ No newline at end of file
diff --git a/include/rive/animation/nested_state_machine.hpp b/include/rive/animation/nested_state_machine.hpp
index a320c85..fe0cded 100644
--- a/include/rive/animation/nested_state_machine.hpp
+++ b/include/rive/animation/nested_state_machine.hpp
@@ -2,6 +2,7 @@
 #define _RIVE_NESTED_STATE_MACHINE_HPP_
 #include "rive/animation/state_machine_instance.hpp"
 #include "rive/generated/animation/nested_state_machine_base.hpp"
+#include "rive/hit_result.hpp"
 #include "rive/math/vec2d.hpp"
 #include <memory>
 
@@ -23,9 +24,10 @@
     void initializeAnimation(ArtboardInstance*) override;
     StateMachineInstance* stateMachineInstance();
 
-    void pointerMove(Vec2D position);
-    void pointerDown(Vec2D position);
-    void pointerUp(Vec2D position);
+    HitResult pointerMove(Vec2D position);
+    HitResult pointerDown(Vec2D position);
+    HitResult pointerUp(Vec2D position);
+    HitResult pointerExit(Vec2D position);
 
     void addNestedInput(NestedInput* input);
 };
diff --git a/include/rive/animation/state_machine_instance.hpp b/include/rive/animation/state_machine_instance.hpp
index a9c5ad5..67369e6 100644
--- a/include/rive/animation/state_machine_instance.hpp
+++ b/include/rive/animation/state_machine_instance.hpp
@@ -6,6 +6,7 @@
 #include <vector>
 #include "rive/animation/linear_animation_instance.hpp"
 #include "rive/core/field_types/core_callback_type.hpp"
+#include "rive/hit_result.hpp"
 #include "rive/listener_type.hpp"
 #include "rive/scene.hpp"
 
@@ -20,7 +21,7 @@
 class SMITrigger;
 class Shape;
 class StateMachineLayerInstance;
-class HitShape;
+class HitComponent;
 class NestedArtboard;
 class Event;
 class KeyedProperty;
@@ -41,23 +42,24 @@
 {
     friend class SMIInput;
     friend class KeyedProperty;
+    friend class HitComponent;
 
 private:
-    void markNeedsAdvance();
-
     /// Provide a hitListener if you want to process a down or an up for the pointer position
     /// too.
-    void updateListeners(Vec2D position, ListenerType hitListener);
+    HitResult updateListeners(Vec2D position, ListenerType hitListener);
 
     template <typename SMType, typename InstType>
     InstType* getNamedInput(const std::string& name) const;
     void notifyEventListeners(std::vector<EventReport> events, NestedArtboard* source);
+    void sortHitComponents();
 
 public:
     StateMachineInstance(const StateMachine* machine, ArtboardInstance* instance);
     StateMachineInstance(StateMachineInstance const&) = delete;
     ~StateMachineInstance() override;
 
+    void markNeedsAdvance();
     // Advance the state machine by the specified time. Returns true if the
     // state machine will continue to animate after this advance.
     bool advance(float seconds);
@@ -88,9 +90,10 @@
 
     bool advanceAndApply(float secs) override;
     std::string name() const override;
-    void pointerMove(Vec2D position) override;
-    void pointerDown(Vec2D position) override;
-    void pointerUp(Vec2D position) override;
+    HitResult pointerMove(Vec2D position) override;
+    HitResult pointerDown(Vec2D position) override;
+    HitResult pointerUp(Vec2D position) override;
+    HitResult pointerExit(Vec2D position) override;
 
     float durationSeconds() const override { return -1; }
     Loop loop() const override { return Loop::oneShot; }
@@ -125,8 +128,7 @@
     std::vector<SMIInput*> m_inputInstances; // we own each pointer
     size_t m_layerCount;
     StateMachineLayerInstance* m_layers;
-    std::vector<std::unique_ptr<HitShape>> m_hitShapes;
-    std::vector<NestedArtboard*> m_hitNestedArtboards;
+    std::vector<std::unique_ptr<HitComponent>> m_hitComponents;
     StateMachineInstance* m_parentStateMachineInstance = nullptr;
     NestedArtboard* m_parentNestedArtboard = nullptr;
 };
diff --git a/include/rive/artboard.hpp b/include/rive/artboard.hpp
index a110e0f..8338a6d 100644
--- a/include/rive/artboard.hpp
+++ b/include/rive/artboard.hpp
@@ -49,6 +49,7 @@
     std::vector<NestedArtboard*> m_NestedArtboards;
     std::vector<Joystick*> m_Joysticks;
     bool m_JoysticksApplyBeforeUpdate = true;
+    bool m_HasChangedDrawOrderInLastUpdate = false;
 
     unsigned int m_DirtDepth = 0;
     rcp<RenderPath> m_BackgroundPath;
@@ -100,6 +101,8 @@
     void onDirty(ComponentDirt dirt) override;
 
     bool advance(double elapsedSeconds);
+    bool hasChangedDrawOrderInLastUpdate() { return m_HasChangedDrawOrderInLastUpdate; };
+    Drawable* firstDrawable() { return m_FirstDrawable; };
 
     enum class DrawOption
     {
diff --git a/include/rive/drawable.hpp b/include/rive/drawable.hpp
index 0e4c494..547d6b5 100644
--- a/include/rive/drawable.hpp
+++ b/include/rive/drawable.hpp
@@ -4,6 +4,7 @@
 #include "rive/hit_info.hpp"
 #include "rive/renderer.hpp"
 #include "rive/clip_result.hpp"
+#include "rive/drawable_flag.hpp"
 #include <vector>
 
 namespace rive
@@ -15,6 +16,7 @@
 class Drawable : public DrawableBase
 {
     friend class Artboard;
+    friend class StateMachineInstance;
 
 private:
     std::vector<ClippingShape*> m_ClippingShapes;
@@ -34,9 +36,15 @@
 
     inline bool isHidden() const
     {
-        // For now we have a single drawable flag, when we have more we can
-        // make an actual enum for this.
-        return (drawableFlags() & 0x1) == 0x1 || hasDirt(ComponentDirt::Collapsed);
+        return (static_cast<DrawableFlag>(drawableFlags()) & DrawableFlag::Hidden) ==
+                   DrawableFlag::Hidden ||
+               hasDirt(ComponentDirt::Collapsed);
+    }
+
+    inline bool isTargetOpaque() const
+    {
+        return (static_cast<DrawableFlag>(drawableFlags()) & DrawableFlag::Opaque) ==
+               DrawableFlag::Opaque;
     }
 };
 } // namespace rive
diff --git a/include/rive/drawable_flag.hpp b/include/rive/drawable_flag.hpp
new file mode 100644
index 0000000..9f79a15
--- /dev/null
+++ b/include/rive/drawable_flag.hpp
@@ -0,0 +1,26 @@
+#ifndef _RIVE_DRAWABLE_FLAGS_HPP_
+#define _RIVE_DRAWABLE_FLAGS_HPP_
+
+#include "rive/enum_bitset.hpp"
+
+namespace rive
+{
+enum class DrawableFlag : unsigned short
+{
+    None = 0,
+
+    /// Whether the component should be drawn
+    Hidden = 1 << 0,
+
+    /// Editor only
+    Locked = 1 << 1,
+
+    /// Editor only
+    Disconnected = 1 << 2,
+
+    /// Whether this Component lets hit events pass through to components behind it
+    Opaque = 1 << 3,
+};
+RIVE_MAKE_ENUM_BITSET(DrawableFlag)
+} // namespace rive
+#endif
diff --git a/include/rive/hit_result.hpp b/include/rive/hit_result.hpp
new file mode 100644
index 0000000..ec0c7fd
--- /dev/null
+++ b/include/rive/hit_result.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_HIT_RESULT_HPP_
+#define _RIVE_HIT_RESULT_HPP_
+
+namespace rive
+{
+enum class HitResult : uint8_t
+{
+    none,
+    hit,
+    hitOpaque,
+};
+} // namespace rive
+#endif
diff --git a/include/rive/scene.hpp b/include/rive/scene.hpp
index 21cb606..fd96d8e 100644
--- a/include/rive/scene.hpp
+++ b/include/rive/scene.hpp
@@ -6,6 +6,7 @@
 #include "rive/math/vec2d.hpp"
 #include "rive/animation/keyed_callback_reporter.hpp"
 #include "rive/core/field_types/core_callback_type.hpp"
+#include "rive/hit_result.hpp"
 #include <string>
 
 namespace rive
@@ -46,9 +47,10 @@
 
     void draw(Renderer*);
 
-    virtual void pointerDown(Vec2D);
-    virtual void pointerMove(Vec2D);
-    virtual void pointerUp(Vec2D);
+    virtual HitResult pointerDown(Vec2D);
+    virtual HitResult pointerMove(Vec2D);
+    virtual HitResult pointerUp(Vec2D);
+    virtual HitResult pointerExit(Vec2D);
 
     virtual size_t inputCount() const;
     virtual SMIInput* input(size_t index) const;
diff --git a/src/animation/nested_state_machine.cpp b/src/animation/nested_state_machine.cpp
index a686f14..a6b5210 100644
--- a/src/animation/nested_state_machine.cpp
+++ b/src/animation/nested_state_machine.cpp
@@ -3,6 +3,7 @@
 #include "rive/animation/nested_number.hpp"
 #include "rive/animation/nested_state_machine.hpp"
 #include "rive/animation/state_machine_instance.hpp"
+#include "rive/hit_result.hpp"
 
 using namespace rive;
 
@@ -37,28 +38,40 @@
     return m_StateMachineInstance.get();
 }
 
-void NestedStateMachine::pointerMove(Vec2D position)
+HitResult NestedStateMachine::pointerMove(Vec2D position)
 {
     if (m_StateMachineInstance != nullptr)
     {
-        m_StateMachineInstance->pointerMove(position);
+        return m_StateMachineInstance->pointerMove(position);
     }
+    return HitResult::none;
 }
 
-void NestedStateMachine::pointerDown(Vec2D position)
+HitResult NestedStateMachine::pointerDown(Vec2D position)
 {
     if (m_StateMachineInstance != nullptr)
     {
-        m_StateMachineInstance->pointerDown(position);
+        return m_StateMachineInstance->pointerDown(position);
     }
+    return HitResult::none;
 }
 
-void NestedStateMachine::pointerUp(Vec2D position)
+HitResult NestedStateMachine::pointerUp(Vec2D position)
 {
     if (m_StateMachineInstance != nullptr)
     {
-        m_StateMachineInstance->pointerUp(position);
+        return m_StateMachineInstance->pointerUp(position);
     }
+    return HitResult::none;
+}
+
+HitResult NestedStateMachine::pointerExit(Vec2D position)
+{
+    if (m_StateMachineInstance != nullptr)
+    {
+        return m_StateMachineInstance->pointerExit(position);
+    }
+    return HitResult::none;
 }
 
 void NestedStateMachine::addNestedInput(NestedInput* input) { m_nestedInputs.push_back(input); }
\ No newline at end of file
diff --git a/src/animation/state_machine_instance.cpp b/src/animation/state_machine_instance.cpp
index 3ba76eb..fe7f27a 100644
--- a/src/animation/state_machine_instance.cpp
+++ b/src/animation/state_machine_instance.cpp
@@ -17,6 +17,7 @@
 #include "rive/animation/state_transition.hpp"
 #include "rive/animation/transition_condition.hpp"
 #include "rive/animation/state_machine_fire_event.hpp"
+#include "rive/hit_result.hpp"
 #include "rive/math/aabb.hpp"
 #include "rive/math/hit_test.hpp"
 #include "rive/nested_animation.hpp"
@@ -318,49 +319,48 @@
     float m_holdTime = 0.0f;
 };
 
+class HitComponent
+{
+public:
+    Component* component() const { return m_component; }
+    HitComponent(Component* component, StateMachineInstance* stateMachineInstance) :
+        m_component(component), m_stateMachineInstance(stateMachineInstance)
+    {}
+    virtual ~HitComponent(){};
+    virtual HitResult processEvent(Vec2D position, ListenerType hitType, bool canHit) = 0;
+
+protected:
+    Component* m_component;
+    StateMachineInstance* m_stateMachineInstance;
+};
+
 /// Representation of a Shape from the Artboard Instance and all the listeners it
 /// triggers. Allows tracking hover and performing hit detection only once on
 /// shapes that trigger multiple listeners.
-class HitShape
+class HitShape : public HitComponent
 {
 public:
-    Shape* shape() const { return m_shape; }
-    HitShape(Shape* shape) : m_shape(shape) {}
+    HitShape(Component* shape, StateMachineInstance* stateMachineInstance) :
+        HitComponent(shape, stateMachineInstance)
+    {}
+    ~HitShape() {}
     bool isHovered = false;
+    float hitRadius = 2;
     std::vector<const StateMachineListener*> listeners;
-
-private:
-    Shape* m_shape;
-};
-} // namespace rive
-
-void StateMachineInstance::updateListeners(Vec2D position, ListenerType hitType)
-{
-    if (m_artboardInstance->frameOrigin())
+    HitResult processEvent(Vec2D position, ListenerType hitType, bool canHit) override
     {
-        position -= Vec2D(m_artboardInstance->originX() * m_artboardInstance->width(),
-                          m_artboardInstance->originY() * m_artboardInstance->height());
-    }
+        auto shape = m_component->as<Shape>();
+        auto hitArea = AABB(position.x - hitRadius,
+                            position.y - hitRadius,
+                            position.x + hitRadius,
+                            position.y + hitRadius)
+                           .round();
+        bool isOver = canHit ? shape->hitTest(hitArea) : false;
+        bool hoverChange = isHovered != isOver;
+        isHovered = isOver;
 
-    const float hitRadius = 2;
-    auto hitArea = AABB(position.x - hitRadius,
-                        position.y - hitRadius,
-                        position.x + hitRadius,
-                        position.y + hitRadius)
-                       .round();
-
-    for (const auto& hitShape : m_hitShapes)
-    {
-
-        // TODO: quick reject.
-
-        bool isOver = hitShape->shape()->hitTest(hitArea);
-
-        bool hoverChange = hitShape->isHovered != isOver;
-        hitShape->isHovered = isOver;
-
-        // iterate all listeners associated with this hit shape
-        for (auto listener : hitShape->listeners)
+        // // iterate all listeners associated with this hit shape
+        for (auto listener : listeners)
         {
             // Always update hover states regardless of which specific listener type
             // we're trying to trigger.
@@ -368,38 +368,45 @@
             {
                 if (isOver && listener->listenerType() == ListenerType::enter)
                 {
-                    listener->performChanges(this, position);
-                    markNeedsAdvance();
+                    listener->performChanges(m_stateMachineInstance, position);
+                    m_stateMachineInstance->markNeedsAdvance();
                 }
                 else if (!isOver && listener->listenerType() == ListenerType::exit)
                 {
-                    listener->performChanges(this, position);
-                    markNeedsAdvance();
+                    listener->performChanges(m_stateMachineInstance, position);
+                    m_stateMachineInstance->markNeedsAdvance();
                 }
             }
             if (isOver && hitType == listener->listenerType())
             {
-                listener->performChanges(this, position);
-                markNeedsAdvance();
+                listener->performChanges(m_stateMachineInstance, position);
+                m_stateMachineInstance->markNeedsAdvance();
             }
         }
+        return isOver ? shape->isTargetOpaque() ? HitResult::hitOpaque : HitResult::hit
+                      : HitResult::none;
     }
-
-    // TODO: store a hittable abstraction for HitShape and NestedArtboard that
-    // can be sorted by drawOrder so they can be iterated in one loop and early
-    // out if any hit stops propagation (also require the ability to mark a hit
-    // as able to stop propagation)
-    for (auto nestedArtboard : m_hitNestedArtboards)
+};
+class HitNestedArtboard : public HitComponent
+{
+public:
+    HitNestedArtboard(Component* nestedArtboard, StateMachineInstance* stateMachineInstance) :
+        HitComponent(nestedArtboard, stateMachineInstance)
+    {}
+    ~HitNestedArtboard() {}
+    HitResult processEvent(Vec2D position, ListenerType hitType, bool canHit) override
     {
+        auto nestedArtboard = m_component->as<NestedArtboard>();
+        HitResult hitResult = HitResult::none;
         if (nestedArtboard->isCollapsed())
         {
-            continue;
+            return hitResult;
         }
         Vec2D nestedPosition;
         if (!nestedArtboard->worldToLocal(position, &nestedPosition))
         {
             // Mounted artboard isn't ready or has a 0 scale transform.
-            continue;
+            return hitResult;
         }
 
         for (auto nestedAnimation : nestedArtboard->nestedAnimations())
@@ -407,38 +414,90 @@
             if (nestedAnimation->is<NestedStateMachine>())
             {
                 auto nestedStateMachine = nestedAnimation->as<NestedStateMachine>();
-                switch (hitType)
+                if (canHit)
                 {
-                    case ListenerType::down:
-                        nestedStateMachine->pointerDown(nestedPosition);
-                        break;
-                    case ListenerType::up:
-                        nestedStateMachine->pointerUp(nestedPosition);
-                        break;
-                    case ListenerType::move:
-                        nestedStateMachine->pointerMove(nestedPosition);
-                        break;
-                    case ListenerType::enter:
-                    case ListenerType::exit:
-                    case ListenerType::event:
-                        break;
+                    switch (hitType)
+                    {
+                        case ListenerType::down:
+                            hitResult = nestedStateMachine->pointerDown(nestedPosition);
+                            break;
+                        case ListenerType::up:
+                            hitResult = nestedStateMachine->pointerUp(nestedPosition);
+                            break;
+                        case ListenerType::move:
+                            hitResult = nestedStateMachine->pointerMove(nestedPosition);
+                            break;
+                        case ListenerType::enter:
+                        case ListenerType::exit:
+                        case ListenerType::event:
+                            break;
+                    }
+                }
+                else
+                {
+                    switch (hitType)
+                    {
+                        case ListenerType::down:
+                        case ListenerType::up:
+                        case ListenerType::move:
+                            nestedStateMachine->pointerExit(nestedPosition);
+                            break;
+                        case ListenerType::enter:
+                        case ListenerType::exit:
+                        case ListenerType::event:
+                            break;
+                    }
                 }
             }
         }
+        return hitResult;
     }
+};
+} // namespace rive
+
+HitResult StateMachineInstance::updateListeners(Vec2D position, ListenerType hitType)
+{
+    if (m_artboardInstance->frameOrigin())
+    {
+        position -= Vec2D(m_artboardInstance->originX() * m_artboardInstance->width(),
+                          m_artboardInstance->originY() * m_artboardInstance->height());
+    }
+
+    bool hitSomething = false;
+    bool hitOpaque = false;
+    for (const auto& hitShape : m_hitComponents)
+    {
+
+        // TODO: quick reject.
+
+        HitResult hitResult = hitShape->processEvent(position, hitType, !hitOpaque);
+        if (hitResult != HitResult::none)
+        {
+            hitSomething = true;
+            if (hitResult == HitResult::hitOpaque)
+            {
+                hitOpaque = true;
+            }
+        }
+    }
+    return hitSomething ? hitOpaque ? HitResult::hitOpaque : HitResult::hit : HitResult::none;
 }
 
-void StateMachineInstance::pointerMove(Vec2D position)
+HitResult StateMachineInstance::pointerMove(Vec2D position)
 {
-    updateListeners(position, ListenerType::move);
+    return updateListeners(position, ListenerType::move);
 }
-void StateMachineInstance::pointerDown(Vec2D position)
+HitResult StateMachineInstance::pointerDown(Vec2D position)
 {
-    updateListeners(position, ListenerType::down);
+    return updateListeners(position, ListenerType::down);
 }
-void StateMachineInstance::pointerUp(Vec2D position)
+HitResult StateMachineInstance::pointerUp(Vec2D position)
 {
-    updateListeners(position, ListenerType::up);
+    return updateListeners(position, ListenerType::up);
+}
+HitResult StateMachineInstance::pointerExit(Vec2D position)
+{
+    return updateListeners(position, ListenerType::exit);
 }
 
 StateMachineInstance::StateMachineInstance(const StateMachine* machine,
@@ -497,9 +556,9 @@
                 auto shape = m_artboardInstance->resolve(id);
                 if (shape != nullptr && shape->is<Shape>())
                 {
-                    auto hs = rivestd::make_unique<HitShape>(shape->as<Shape>());
+                    auto hs = rivestd::make_unique<HitShape>(shape->as<Component>(), this);
                     hitShapeLookup[id] = hitShape = hs.get();
-                    m_hitShapes.push_back(std::move(hs));
+                    m_hitComponents.push_back(std::move(hs));
                 }
                 else
                 {
@@ -519,7 +578,11 @@
     {
         if (nestedArtboard->hasNestedStateMachines())
         {
-            m_hitNestedArtboards.push_back(nestedArtboard);
+
+            auto hn =
+                rivestd::make_unique<HitNestedArtboard>(nestedArtboard->as<Component>(), this);
+            m_hitComponents.push_back(std::move(hn));
+
             for (auto animation : nestedArtboard->nestedAnimations())
             {
                 if (animation->is<NestedStateMachine>())
@@ -534,6 +597,7 @@
             }
         }
     }
+    sortHitComponents();
 }
 
 StateMachineInstance::~StateMachineInstance()
@@ -545,8 +609,47 @@
     delete[] m_layers;
 }
 
+void StateMachineInstance::sortHitComponents()
+{
+    Drawable* last = m_artboardInstance->firstDrawable();
+    if (last)
+    {
+        // walk to the end, so we can visit in reverse-order
+        while (last->prev)
+        {
+            last = last->prev;
+        }
+    }
+    auto hitShapesCount = m_hitComponents.size();
+    auto currentSortedIndex = 0;
+    for (auto drawable = last; drawable; drawable = drawable->next)
+    {
+        for (size_t i = currentSortedIndex; i < hitShapesCount; i++)
+        {
+            if (m_hitComponents[i]->component() == drawable)
+            {
+                if (currentSortedIndex != i)
+                {
+                    std::iter_swap(m_hitComponents.begin() + currentSortedIndex,
+                                   m_hitComponents.begin() + i);
+                }
+                currentSortedIndex++;
+                break;
+            }
+        }
+        if (currentSortedIndex == hitShapesCount)
+        {
+            break;
+        }
+    }
+}
+
 bool StateMachineInstance::advance(float seconds)
 {
+    if (m_artboardInstance->hasChangedDrawOrderInLastUpdate())
+    {
+        sortHitComponents();
+    }
     this->notifyEventListeners(m_reportedEvents, nullptr);
     m_reportedEvents.clear();
     m_needsAdvance = false;
diff --git a/src/artboard.cpp b/src/artboard.cpp
index 6f18718..b386bf7 100644
--- a/src/artboard.cpp
+++ b/src/artboard.cpp
@@ -272,6 +272,7 @@
 
 void Artboard::sortDrawOrder()
 {
+    m_HasChangedDrawOrderInLastUpdate = true;
     for (auto target : m_DrawTargets)
     {
         target->first = target->last = nullptr;
@@ -486,6 +487,7 @@
 
 bool Artboard::advance(double elapsedSeconds)
 {
+    m_HasChangedDrawOrderInLastUpdate = false;
     if (m_JoysticksApplyBeforeUpdate)
     {
         for (auto joystick : m_Joysticks)
diff --git a/src/scene.cpp b/src/scene.cpp
index 2bf0a09..a987dfe 100644
--- a/src/scene.cpp
+++ b/src/scene.cpp
@@ -1,4 +1,5 @@
 #include "rive/artboard.hpp"
+#include "rive/hit_result.hpp"
 #include "rive/scene.hpp"
 #include "rive/generated/core_registry.hpp"
 using namespace rive;
@@ -14,9 +15,10 @@
 
 void Scene::draw(Renderer* renderer) { m_artboardInstance->draw(renderer); }
 
-void Scene::pointerDown(Vec2D) {}
-void Scene::pointerMove(Vec2D) {}
-void Scene::pointerUp(Vec2D) {}
+HitResult Scene::pointerDown(Vec2D) { return HitResult::none; }
+HitResult Scene::pointerMove(Vec2D) { return HitResult::none; }
+HitResult Scene::pointerUp(Vec2D) { return HitResult::none; }
+HitResult Scene::pointerExit(Vec2D) { return HitResult::none; }
 
 size_t Scene::inputCount() const { return 0; }
 SMIInput* Scene::input(size_t index) const { return nullptr; }
diff --git a/test/assets/opaque_hit_test.riv b/test/assets/opaque_hit_test.riv
new file mode 100644
index 0000000..65c0d81
--- /dev/null
+++ b/test/assets/opaque_hit_test.riv
Binary files differ
diff --git a/test/hittest_test.cpp b/test/hittest_test.cpp
index fa87032..cb4f8f8 100644
--- a/test/hittest_test.cpp
+++ b/test/hittest_test.cpp
@@ -4,6 +4,11 @@
 
 #include <rive/math/aabb.hpp>
 #include <rive/math/hit_test.hpp>
+#include <rive/nested_artboard.hpp>
+#include <rive/animation/state_machine_instance.hpp>
+#include <rive/animation/state_machine_input_instance.hpp>
+#include <rive/animation/nested_state_machine.hpp>
+#include "rive_file_reader.hpp"
 
 #include <catch.hpp>
 #include <cstdio>
@@ -57,3 +62,121 @@
     };
     REQUIRE(HitTester::testMesh(area, make_span(verts, 3), make_span(indices, 3)));
 }
+
+TEST_CASE("hit test on opaque target", "[hittest]")
+{
+    // This artboard has two rects of size 200 x 200, "red-activate" at [0, 0, 200, 200]
+    // and "green-activate" at [0, 100, 200, 300]
+    // "red-activate" is above "green-activate" in drawing order
+    // Both targets are set as opaque for its listeners
+    // "red-activate" sets "toGreen" to false
+    // "green-activate" sets "toGreen" to true
+    // There is also a "gray-activate" above the other 2 that is not opaque so events should
+    // traverse through the other targets
+    auto file = ReadRiveFile("../../test/assets/opaque_hit_test.riv");
+
+    auto artboard = file->artboard("main");
+    auto artboardInstance = artboard->instance();
+    auto stateMachine = artboard->stateMachine("main-state-machine");
+
+    REQUIRE(artboardInstance != nullptr);
+    REQUIRE(artboardInstance->stateMachineCount() == 1);
+
+    REQUIRE(stateMachine != nullptr);
+
+    rive::StateMachineInstance* stateMachineInstance =
+        new rive::StateMachineInstance(stateMachine, artboardInstance.get());
+
+    stateMachineInstance->advance(0.0f);
+    artboardInstance->advance(0.0f);
+    REQUIRE(stateMachineInstance->needsAdvance() == true);
+    stateMachineInstance->advance(0.0f);
+
+    auto toGreenToggle = stateMachineInstance->getBool("toGreen");
+    REQUIRE(toGreenToggle != nullptr);
+    auto grayToggle = stateMachineInstance->getBool("grayToggle");
+    REQUIRE(grayToggle != nullptr);
+
+    stateMachineInstance->pointerDown(rive::Vec2D(100.0f, 50.0f));
+    // "gray-activate" is clicked
+    REQUIRE(grayToggle->value() == true);
+    // Pointer only over "red-activate"
+    REQUIRE(toGreenToggle->value() == false);
+
+    stateMachineInstance->pointerDown(rive::Vec2D(100.0f, 250.0f));
+    // "gray-activate" is clicked
+    REQUIRE(grayToggle->value() == false);
+    // Pointer over "green-activate"
+    REQUIRE(toGreenToggle->value() == true);
+
+    stateMachineInstance->pointerDown(rive::Vec2D(100.0f, 110.0f));
+    // "gray-activate" is clicked
+    REQUIRE(grayToggle->value() == true);
+    // Pointer over "red-activate" and "green-activate", but "red-activate" is opaque and above
+    // so green activate does not trigger
+    REQUIRE(toGreenToggle->value() == false);
+    delete stateMachineInstance;
+}
+
+TEST_CASE("hit test on opaque nested artboard", "[hittest]")
+{
+    // This artboard (300x300) has a main rect at [0, 0, 300, 300]
+    // this rect has a listener that toggles "second-gray-toggle"
+    // and a nested artboard at [0, 0, 150, 150]
+    // the nested artboard and the rect have opaque targets
+    auto file = ReadRiveFile("../../test/assets/opaque_hit_test.riv");
+
+    auto artboard = file->artboard("second");
+    auto artboardInstance = artboard->instance();
+    auto stateMachine = artboard->stateMachine("second-state-machine");
+
+    REQUIRE(artboardInstance != nullptr);
+    REQUIRE(artboardInstance->stateMachineCount() == 1);
+
+    REQUIRE(stateMachine != nullptr);
+
+    rive::StateMachineInstance* stateMachineInstance =
+        new rive::StateMachineInstance(stateMachine, artboardInstance.get());
+
+    auto nestedArtboard =
+        stateMachineInstance->artboard()->find<rive::NestedArtboard>("second-nested");
+    REQUIRE(nestedArtboard != nullptr);
+    auto nestedArtboardStateMachine =
+        nestedArtboard->nestedAnimations()[0]->as<NestedStateMachine>();
+    REQUIRE(nestedArtboardStateMachine != nullptr);
+    auto nestedArtboardStateMachineInstance = nestedArtboardStateMachine->stateMachineInstance();
+
+    auto secondNestedBoolTarget = nestedArtboardStateMachineInstance->getBool("bool-target");
+    REQUIRE(secondNestedBoolTarget != nullptr);
+
+    artboardInstance->advance(0.0f);
+    stateMachineInstance->advanceAndApply(0.0f);
+
+    REQUIRE(secondNestedBoolTarget->value() == false);
+
+    auto secondGrayToggle = stateMachineInstance->getBool("second-gray-toggle");
+    REQUIRE(secondGrayToggle != nullptr);
+
+    stateMachineInstance->pointerDown(rive::Vec2D(100.0f, 250.0f));
+    // toggle changes value because it is not under an opaque nested artboard
+    REQUIRE(secondGrayToggle->value() == true);
+
+    stateMachineInstance->pointerDown(rive::Vec2D(100.0f, 50.0f));
+    // toggle does not change because it is under an opaque nested artboard
+    REQUIRE(secondGrayToggle->value() == true);
+
+    // nested toggle changes because it's on top of shape
+    REQUIRE(secondNestedBoolTarget->value() == true);
+
+    // A timeline switches draw order and the nested artboard is now below the rect
+    stateMachineInstance->advanceAndApply(1.0f);
+    stateMachineInstance->advance(0.0f);
+
+    stateMachineInstance->pointerDown(rive::Vec2D(100.0f, 50.0f));
+    // So now the pointer down is captured by the rect
+    REQUIRE(secondGrayToggle->value() == false);
+
+    // nested toggle does not change because it's below shape
+    REQUIRE(secondNestedBoolTarget->value() == true);
+    delete stateMachineInstance;
+}
\ No newline at end of file