blob: a140bbe8b01b55ccd5913e27f39b84bb7b1b7d0a [file] [edit]
#ifndef _RIVE_DRAWABLE_HPP_
#define _RIVE_DRAWABLE_HPP_
#include "rive/generated/drawable_base.hpp"
#include "rive/hit_info.hpp"
#include "rive/lazy_vector.hpp"
#include "rive/renderer.hpp"
#include "rive/clip_result.hpp"
#include "rive/math/aabb.hpp"
#include "rive/drawable_flag.hpp"
#include "rive/shapes/paint/blend_mode.hpp"
#include <vector>
namespace rive
{
class ClippingShape;
class CustomProperty;
class LayerMask;
class Artboard;
class DrawRules;
class LayoutComponent;
class ShapePaintContainer;
// How much to trust a drawable's answer to paintedWorldBounds().
//
// Anything sizing a surface from content bounds needs this: a box that is too
// small crops, and a crop at a raster edge is a hard line across the middle of
// someone's artwork. So a drawable that cannot honestly bound itself has to say
// so rather than return its best guess.
enum class BoundsFidelity : uint8_t
{
// Cannot measure at all. The caller must fall back to something it knows
// covers everything, such as the artboard's own bounds.
none,
// The footprint is right but the paint's full reach is not known -- glyph
// ink escaping a text layout box, a stroke effect that can displace
// geometry. The caller should pad before using it.
approximate,
// Geometry plus every stroke, join, cap and feather outset is accounted
// for.
exact,
};
class Drawable : public DrawableBase
{
friend class Artboard;
friend class StateMachineInstance;
private:
LazyVector<ClippingShape*> m_ClippingShapes;
// Kept separate from m_ClippingShapes rather than unified into one
// bracket list: the clip interleave in Artboard::sortDrawOrder and its
// goldens are load-bearing, and a mask bracket has different nesting
// rules (it must sit inside a clip, and it must hug real drawables).
LazyVector<LayerMask*> m_layerMasks;
// The other direction: masks that rasterize this drawable as coverage.
// Disjoint from m_layerMasks -- a mask cannot mask its own source -- and
// needed because a change to a source drawable has to invalidate the
// coverage canvas, and the only party that can be asked is the drawable.
LazyVector<LayerMask*> m_sourceOfLayerMasks;
/// Used exclusively by the artboard;
DrawRules* flattenedDrawRules = nullptr;
Drawable* prev = nullptr;
Drawable* next = nullptr;
enum class RuntimeFlags : uint8_t
{
none = 0,
needsSaveOperation = 1 << 0,
// Set by a property so untagged drawables pay no child scan.
hasCustomProperties = 1 << 1,
};
RuntimeFlags m_runtimeFlags = RuntimeFlags::needsSaveOperation;
bool runtimeFlag(RuntimeFlags flag) const
{
return (m_runtimeFlags & flag) == flag;
}
void runtimeFlag(RuntimeFlags flag, bool value)
{
m_runtimeFlags = value ? m_runtimeFlags | flag : m_runtimeFlags & ~flag;
}
protected:
bool needsSaveOperation() const
{
return runtimeFlag(RuntimeFlags::needsSaveOperation);
}
#ifdef WITH_RIVE_EDITOR
void flagsChanged() override
{
// Preserve runtime-only bits when authoring the shared component flag.
constexpr auto mask = static_cast<uint16_t>(DrawableFlag::Selectable);
drawableFlags((drawableFlags() & ~mask) | (flags() & mask));
}
#endif
public:
bool isSelectable() const
{
return (static_cast<DrawableFlag>(drawableFlags()) &
DrawableFlag::Selectable) != DrawableFlag::None;
}
BlendMode blendMode() const { return (BlendMode)blendModeValue(); }
/// additiveAmount() as the renderer wants it: 0 (plain srcOver) to 1
/// (fully additive), and always 0 for the modes that ignore it.
float additiveness() const
{
return additivenessFor(blendMode(), additiveAmount());
}
virtual void draw(Renderer* renderer) = 0;
virtual Core* hitTest(HitInfo*, const Mat2D&) = 0;
bool hitTestPoint(const Vec2D& position,
bool skipOnUnclipped,
bool isPrimaryHit) override;
void addClippingShape(ClippingShape* shape);
inline const std::vector<ClippingShape*>& clippingShapes() const
{
return m_ClippingShapes.view();
}
void addLayerMask(LayerMask* mask);
void addSourceOfLayerMask(LayerMask* mask);
// The masks that apply to this drawable, outermost ancestor first (they are
// stamped in m_Objects order, which is hierarchy order). Virtual because
// DrawableProxy has to answer for the component it stands in for: a
// LayoutComponent's proxy carries the save()+clipPath() whose restore()
// lives in LayoutComponent::draw(), so leaving the proxy out of the bracket
// would strand the save outside it and underflow the offscreen renderer's
// save stack.
virtual const std::vector<LayerMask*>& layerMasks() const
{
return m_layerMasks.view();
}
// The masks this drawable supplies coverage for. Virtual for the same
// reason layerMasks() is: a proxy answers for what it stands in for.
virtual const std::vector<LayerMask*>& sourceOfLayerMasks() const
{
return m_sourceOfLayerMasks.view();
}
#ifdef WITH_RIVE_EDITOR
// Editor-only removal companion to `addClippingShape`. Fires from
// `ClippingShape::editorParentChanged` when a ClippingShape is
// reparented away from this drawable's clip-affecting ancestor (or
// about to be freed) so the typed list doesn't dangle. Runtime
// assumes immutable hierarchy and never needs this. Body in
// `editor_native/.../component_parent_editor.cpp`.
void removeClippingShapeForEditor(ClippingShape* shape);
// The same companions for the two LayerMask lists, which onAddedClean
// registers the same one-shot way: m_layerMasks from the mask's parent
// subtree, m_sourceOfLayerMasks from its source subtree. Without these a
// mask deleted or reparented in the editor leaves a freed pointer that
// sortDrawOrder or a colour edit will dereference.
void removeLayerMaskForEditor(LayerMask* mask);
void removeSourceOfLayerMaskForEditor(LayerMask* mask);
#endif
virtual bool isHidden() const
{
if ((static_cast<DrawableFlag>(drawableFlags()) &
DrawableFlag::Hidden) == DrawableFlag::Hidden ||
hasDirt(ComponentDirt::Collapsed))
{
return true;
}
#ifdef WITH_RIVE_EDITOR
// Editor-only: respect the hierarchy panel's eye toggle.
// Walks the parent chain since `ComponentFlags::hidden`
// doesn't physically cascade to children. Mirrors dart's
// [stage_hideable.dart:76-80](packages/editor/lib/rive/stage/stage_hideable.dart#L76-L80).
// Runtime flags() is always 0, so this branch is dead weight
// there but free at runtime build time.
if (isFlaggedRecursive(EditorFlagHidden))
{
return true;
}
// Isolation: when any component in the artboard has the
// isolated bit set, everything outside the isolated subtree
// is hidden on stage. After-Effects-style.
if (isOutsideEditorIsolationScope())
{
return true;
}
#endif
return false;
}
#ifdef WITH_RIVE_EDITOR
/// True iff the active artboard has at least one component with
/// `EditorFlagIsolated` set AND this drawable is NOT a descendant
/// of one of those isolated subtrees. Walks the artboard's full
/// hierarchy on each call — cheap because flagged components are
/// rare and the walk short-circuits on the first match. Mirrors
/// dart's [Artboard.hasIsolatedComponents]/[isInIsolationScope]
/// pair used by
/// [stage_hideable.dart:87-92](packages/editor/lib/rive/stage/stage_hideable.dart#L87-L92).
/// Body in `editor_native/native/src/editor/drawable_editor.cpp`
/// so this header doesn't depend on the full Artboard def.
bool isOutsideEditorIsolationScope() const;
#endif
virtual bool isTargetOpaque()
{
return (static_cast<DrawableFlag>(drawableFlags()) &
DrawableFlag::Opaque) == DrawableFlag::Opaque;
}
virtual bool isProxy() { return false; }
virtual bool isClipStart() { return false; }
virtual bool isClipEnd() { return false; }
// Opens / closes a layer-mask bracket. Unlike a clip bracket these are not
// drawn in place: the walk hands the whole enclosed range to
// Artboard::drawMasked and then skips past it.
virtual bool isMaskStart() { return false; }
virtual bool isMaskEnd() { return false; }
virtual bool willClip() { return false; }
virtual bool willDraw();
// World-space bounds of everything this drawable actually paints: geometry
// plus stroke, join, cap and feather outset. NOT the same as
// Shape::worldBounds(), which is geometry only and would crop a feather.
//
// Defaults to `none` so a Drawable added later is safe by construction --
// the day someone adds a drawable that paints somewhere unpredictable,
// every caller degrades instead of silently cropping it. Override only
// where the answer is genuinely conservative. *out is untouched when `none`
// is returned.
virtual BoundsFidelity paintedWorldBounds(AABB* out)
{
return BoundsFidelity::none;
}
protected:
// The shape every override below takes: lift a local-space box into world
// space, then pad it by `paints` (null for a drawable that has none).
//
// Answers `none` for an empty local box, because
// TransformComponent::localBounds() returns an empty AABB to mean "I do not
// know my size" -- and a caller must not read that as "I paint nothing".
BoundsFidelity paintedBoundsFromLocal(const AABB& local,
const Mat2D& world,
const ShapePaintContainer* paints,
AABB* out);
public:
void needsSaveOperation(bool value)
{
runtimeFlag(RuntimeFlags::needsSaveOperation, value);
}
bool isChildOfLayout(LayoutComponent* layout);
StatusCode onAddedDirty(CoreContext* context) override;
virtual Drawable* hittableComponent() { return this; }
// Whether custom properties tag this drawable for a draw visitor.
bool hasCustomProperties();
void markHasCustomProperties()
{
runtimeFlag(RuntimeFlags::hasCustomProperties, true);
}
// The tagging property with this name id, null when there is none.
CustomProperty* customProperty(uint32_t nameId);
virtual int emptyClipCount() { return 0; }
// Public accessors for the artboard's draw-order linked list.
// Order is back-to-front (firstDrawable() is bottom-most).
Drawable* nextDrawable() const { return next; }
Drawable* prevDrawable() const { return prev; }
};
class ProxyDrawing
{
public:
virtual void drawProxy(Renderer* renderer) = 0;
virtual bool isProxyHidden() = 0;
};
class DrawableProxy : public Drawable
{
private:
ProxyDrawing* m_proxyDrawing;
public:
DrawableProxy(ProxyDrawing* proxy) : m_proxyDrawing(proxy) {}
void draw(Renderer* renderer) override
{
m_proxyDrawing->drawProxy(renderer);
}
bool isHidden() const override { return m_proxyDrawing->isProxyHidden(); }
Drawable* hittableComponent() override;
bool isTargetOpaque() override;
Core* hitTest(HitInfo*, const Mat2D&) override { return nullptr; }
bool isProxy() override { return true; }
// Delegated for the same reason as layerMasks(): the proxy paints on behalf
// of another component, so that component's reach is the answer.
BoundsFidelity paintedWorldBounds(AABB* out) override;
// A proxy stands in for a real component, so it has to be inside the same
// mask bracket that component is in. A LayoutComponent's proxy issues
// save() + clipPath() whose matching restore() is in
// LayoutComponent::draw(); if the proxy were excluded from the bracket the
// save would land outside the offscreen frame and the restore inside it.
const std::vector<LayerMask*>& layerMasks() const override;
const std::vector<LayerMask*>& sourceOfLayerMasks() const override;
ProxyDrawing* proxyDrawing() const { return m_proxyDrawing; }
};
} // namespace rive
#endif