Skip to content

Commit

Permalink
editor: allow hit testing over text and text runs
Browse files Browse the repository at this point in the history
This PR includes all the changes on the editor and the runtime side to make text and text runs listen to mouse events.

This PR is best reviewed commit-by-commit. Roughly the changes include,
1. Previously, all hit targets had to be world transform components. I had to relax this and added a `isEligibleListenerTarget` function on Component
2. I refactored the adding listener logic to be... recursive. I thought this was easier to reason than to add the same text run logic in many places, since the scrolling handler part would also use this. I'm not sure if they should actually be different though. Moreover, because of HitDrawable's set up to hold a reference to both a drawable and the component, I changed the constructor to account for both. AFAIK, it needs a drawable for the opaque objects check. For text runs, the drawable is actually the parent, not itself. I'm not super happy about this, suggestions welcome.
3. The actual text run hit logic is that I added a 'cached' contours property on the text run, and it is calculated whenever the text recalculates its render styles. I did it here because only the Text object has context on what glyphs should render when clipping or ellipsis is applied.
4. I made sure that only the text runs that are targets would store contours. This is captured in the `Hittable` abstraction. I also made Shapes hittable, and reused the shape's _HitShape for both shapes and text runs.
5. All this logic is also applied to the runtime side. I also ported over the contour finding logic from dart.

I made sure that a hover effect works on a text run, as well as on the whole text (which just delegates to its constituent text runs), both in the editor playback and in a nested artboard. I'd like to write some editor tests to make sure the hover gets captured ok. But it could take me a while, so I'm sending this out first.

Some relevant Slack discussion: https://2dimensions.slack.com/archives/C07HQ4GS0BH/p1733523977504739

Testing
1. New editor tests
2. Verified hit testing works in scrolling containers and in layouts

Diffs=
f19a9c9399 editor: allow hit testing over text and text runs (#8719)

Co-authored-by: Susan Wang <[email protected]>
  • Loading branch information
susan101566 and susan101566 committed Dec 18, 2024
1 parent 9fdfcf0 commit 819348d
Show file tree
Hide file tree
Showing 14 changed files with 780 additions and 186 deletions.
2 changes: 1 addition & 1 deletion .rive_head
Original file line number Diff line number Diff line change
@@ -1 +1 @@
e37a0f285f937042974686b5022c002b0dd143bb
f19a9c9399997a1e1b4571fe860faf2ed60b7d49
22 changes: 22 additions & 0 deletions include/rive/animation/hittable.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#ifndef _RIVE_HITTABLE_HPP_
#define _RIVE_HITTABLE_HPP_

#include "rive/math/aabb.hpp"

namespace rive
{
class Component;

// A Component that can be hit-tested via two passes: a faster AABB pass, and a
// more accurate HiFi pass.
class Hittable
{
public:
static Hittable* from(Component* component);
virtual bool hitTestAABB(const Vec2D& position) = 0;
virtual bool hitTestHiFi(const Vec2D& position, float hitRadius) = 0;
virtual ~Hittable() {}
};
} // namespace rive

#endif
6 changes: 6 additions & 0 deletions include/rive/animation/state_machine_instance.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class KeyedProperty;
class EventReport;
class DataBind;
class BindableProperty;
class HitDrawable;

#ifdef WITH_RIVE_TOOLS
class StateMachineInstance;
Expand Down Expand Up @@ -67,6 +68,11 @@ class StateMachineInstance : public Scene,
StateTransition* findAllowedTransition(StateInstance* stateFromInstance,
bool ignoreTriggers);
DataContext* m_DataContext = nullptr;
void addToHitLookup(Component* target,
bool isLayoutComponent,
std::unordered_map<Component*, HitDrawable*>& hitLookup,
ListenerGroup* listenerGroup,
bool isOpaque);

public:
StateMachineInstance(const StateMachine* machine,
Expand Down
23 changes: 23 additions & 0 deletions include/rive/math/rect.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#ifndef _RIVE_RECT_HPP_
#define _RIVE_RECT_HPP_

namespace rive
{
struct Rect
{
float left, top, right, bottom;

Rect(float l, float t, float r, float b) :
left(l), top(t), right(r), bottom(b)
{}

float width() const { return right - left; }
float height() const { return bottom - top; }

static Rect fromLTRB(float l, float t, float r, float b)
{
return Rect(l, t, r, b);
}
};
} // namespace rive
#endif
37 changes: 37 additions & 0 deletions include/rive/math/rectangles_to_contour.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#ifndef _RIVE_RECTANGLES_TO_CONTOUR_HPP_
#define _RIVE_RECTANGLES_TO_CONTOUR_HPP_
#include <vector>
#include <unordered_set>
#include "rive/math/mat2d.hpp"
#include "rive/math/rect.hpp"

namespace rive
{
struct PolygonPoint
{
Vec2D vec;
int dir; // 0 for horizontal, 1 for vertical

PolygonPoint(const Vec2D& vec, int dir) : vec(vec), dir(dir) {}

bool operator==(const PolygonPoint& other) const
{
return vec == other.vec && dir == other.dir;
}
};

struct RectanglesToContour
{
private:
std::unordered_set<Vec2D> uniquePoints;
std::vector<Rect> rects;
void addUniquePoint(const Vec2D& point);
void addRect(const Rect& rect);
std::vector<std::vector<Vec2D>> computeContours();

public:
static std::vector<std::vector<Vec2D>> makeSelectionContours(
const std::vector<Rect>& rects);
};
} // namespace rive
#endif
17 changes: 16 additions & 1 deletion include/rive/math/vec2d.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,19 @@ inline bool operator!=(const Vec2D& a, const Vec2D& b)
Vec2D Vec2D::lerp(Vec2D a, Vec2D b, float t) { return a + (b - a) * t; }

} // namespace rive
#endif

namespace std
{
template <> struct hash<rive::Vec2D>
{
size_t operator()(const rive::Vec2D& v) const
{
// Combine the hashes of x and y to produce a hash for Vec2D
size_t h1 = std::hash<float>()(v.x); // Hash for x component
size_t h2 = std::hash<float>()(v.y); // Hash for y component
return h1 ^ (h2 << 1); // Combine them with bitwise XOR and shift
}
};
} // namespace std

#endif
7 changes: 5 additions & 2 deletions include/rive/shapes/shape.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#include "rive/hit_info.hpp"
#include "rive/generated/shapes/shape_base.hpp"
#include "rive/animation/hittable.hpp"
#include "rive/shapes/path_composer.hpp"
#include "rive/shapes/shape_paint_container.hpp"
#include "rive/drawable_flag.hpp"
Expand All @@ -15,7 +16,7 @@ class PathComposer;
class HitTester;
class RenderPathDeformer;

class Shape : public ShapeBase, public ShapePaintContainer
class Shape : public ShapeBase, public ShapePaintContainer, public Hittable
{
private:
PathComposer m_PathComposer;
Expand All @@ -41,7 +42,6 @@ class Shape : public ShapeBase, public ShapePaintContainer
void update(ComponentDirt value) override;
void draw(Renderer* renderer) override;
Core* hitTest(HitInfo*, const Mat2D&) override;
bool hitTest(const IAABB& area) const;

const PathComposer* pathComposer() const { return &m_PathComposer; }
PathComposer* pathComposer() { return &m_PathComposer; }
Expand Down Expand Up @@ -80,6 +80,9 @@ class Shape : public ShapeBase, public ShapePaintContainer
LayoutMeasureMode widthMode,
float height,
LayoutMeasureMode heightMode) override;

bool hitTestAABB(const Vec2D& position) override;
bool hitTestHiFi(const Vec2D& position, float hitRadius) override;
};
} // namespace rive

Expand Down
16 changes: 14 additions & 2 deletions include/rive/text/text_value_run.hpp
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
#ifndef _RIVE_TEXT_VALUE_RUN_HPP_
#define _RIVE_TEXT_VALUE_RUN_HPP_
#include "rive/generated/text/text_value_run_base.hpp"
#include "rive/animation/hittable.hpp"
#include "rive/text/utf.hpp"

namespace rive
{
class TextStyle;
class TextValueRun : public TextValueRunBase
class Text;
class TextValueRun : public TextValueRunBase, public Hittable
{
public:
StatusCode onAddedClean(CoreContext* context) override;
StatusCode onAddedDirty(CoreContext* context) override;
TextStyle* style() { return m_style; }
Text* textComponent() const;
uint32_t length()
{
if (m_length == -1)
Expand All @@ -30,14 +33,23 @@ class TextValueRun : public TextValueRunBase
}
uint32_t offset() const;

// Hit testing
AABB m_localBounds;
std::vector<std::vector<Vec2D>> m_contours;
bool m_isHitTarget = false;
void resetHitTest(); // clear m_contours and m_localBounds
bool hitTestAABB(const Vec2D& position) override;
bool hitTestHiFi(const Vec2D& position, float hitRadius) override;

protected:
void textChanged() override;
void styleIdChanged() override;

private:
TextStyle* m_style = nullptr;
uint32_t m_length = -1;
bool canHitTest() const;
};
} // namespace rive

#endif
#endif
18 changes: 18 additions & 0 deletions src/animation/hittable.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#include "rive/animation/hittable.hpp"
#include "rive/component.hpp"
#include "rive/shapes/shape.hpp"
#include "rive/text/text_value_run.hpp"

using namespace rive;

Hittable* Hittable::from(Component* component)
{
switch (component->coreType())
{
case Shape::typeKey:
return component->as<Shape>();
case TextValueRun::typeKey:
return component->as<TextValueRun>();
}
return nullptr;
}
Loading

0 comments on commit 819348d

Please sign in to comment.