blob: bebaf04bae07f5778cfb9b0a0d2c3c2a86a7eb00 [file]
#include "rive/semantic/semantic_data.hpp"
#include "rive/artboard.hpp"
#include "rive/component_dirt.hpp"
#include "rive/drawable.hpp"
#include "rive/focus_data.hpp"
#include "rive/input/focus_manager.hpp"
#include "rive/node.hpp"
#include "rive/semantic/semantic_listener.hpp"
#include "rive/semantic/semantic_manager.hpp"
#include "rive/semantic/semantic_node.hpp"
#include "rive/semantic/semantic_provider.hpp"
#include "rive/semantic/semantic_state.hpp"
#include "rive/semantic/semantic_trait.hpp"
#include "semantic_inference_registry.hpp"
using namespace rive;
SemanticData::~SemanticData()
{
if (m_semanticNode != nullptr)
{
auto* manager = m_semanticNode->manager();
if (manager != nullptr)
{
manager->removeChild(m_semanticNode);
}
}
}
// Read only when the node is created or its own visibility changes, never at
// flatten: ScriptedTransition flags a page Hidden while it composites the page
// itself, and that page is still on screen.
static bool isDrawableHidden(const Component* owner)
{
return owner != nullptr && owner->is<Drawable>() &&
owner->as<Drawable>()->isHidden();
}
// Lazily creates the SemanticNode with a globally unique ID, copies
// authored properties, applies text inference, and computes initial bounds.
rcp<SemanticNode> SemanticData::semanticNode()
{
if (m_semanticNode == nullptr)
{
// Id is assigned by SemanticManager on first addChild(); until
// then the node has id 0 and can't be looked up. All dirty-marking
// call sites in this file guard against a null manager, so id 0
// is safe to carry around pre-registration.
m_semanticNode = rcp<SemanticNode>(new SemanticNode());
m_semanticNode->coreOwner(parent());
m_ownerHidden = isDrawableHidden(parent());
m_semanticNode->semanticData(this);
m_semanticNode->role(m_Role);
m_semanticNode->label(m_Label);
m_semanticNode->value(m_Value);
m_semanticNode->hint(m_Hint);
m_semanticNode->headingLevel(m_HeadingLevel);
m_semanticNode->stateFlags(m_StateFlags);
// Check if a sibling FocusData exists — if so, set the Focusable
// trait so the platform knows this node can receive focus.
uint32_t initialTraits = m_TraitFlags;
auto* parentNode = parent();
if (parentNode != nullptr && parentNode->is<Node>())
{
for (auto* child : parentNode->as<Node>()->children())
{
if (child->is<FocusData>())
{
initialTraits |= static_cast<uint32_t>(SemanticTrait::Focusable);
break;
}
}
}
m_semanticNode->traitFlags(initialTraits);
applyInferredSemanticsIfNeeded();
// Set initial world bounds if available. Sibling shapes may not
// have built their paths yet (deferred due to renderOpacity == 0
// during nested artboard init). Flag a retry so updateWorldBounds
// re-dirts once if the initial bounds are degenerate.
m_boundsRetryPending = true;
updateWorldBounds();
}
return m_semanticNode;
}
// Finds parent SemanticData by walking up the component hierarchy. The walk
// starts above the component this SemanticData is attached to: another
// SemanticData on that same component is a sibling, and treating siblings as
// parents links them into a cycle that detaches all of them from the tree.
SemanticData* SemanticData::findParentSemanticData() const
{
auto* owner = parent();
auto* current = owner != nullptr ? owner->parent() : nullptr;
while (current != nullptr)
{
if (current->is<Node>())
{
if (auto* data = current->as<Node>()->firstChild<SemanticData>())
{
return data;
}
}
current = current->parent();
}
return nullptr;
}
// Finds the node a hosted artboard's boundary hangs under: the closest
// SemanticData at or above the component in its own artboard, else that
// artboard's boundary (null for a root). It never looks past the artboard,
// so a host's subtree contains everything below it and hiding the host's
// boundary hides all of it. SemanticData on the artboard itself is skipped,
// as it always was, so where nested content reads is unchanged.
rcp<SemanticNode> SemanticData::findClosestSemanticNode(Component* component)
{
for (auto* current = component; current != nullptr;
current = current->parent())
{
if (current->is<Artboard>())
{
return current->as<Artboard>()->semanticBoundaryNode();
}
if (current->is<Node>())
{
if (auto* data = current->as<Node>()->firstChild<SemanticData>())
{
return data->semanticNode();
}
}
}
return nullptr;
}
uint32_t SemanticData::semanticId() const
{
if (m_semanticNode == nullptr)
{
return 0;
}
return m_semanticNode->id();
}
void SemanticData::setFocusedState(bool focused)
{
if (m_semanticNode == nullptr)
{
return;
}
uint32_t flags = m_semanticNode->stateFlags();
if (focused)
{
flags |= static_cast<uint32_t>(SemanticState::Focused);
}
else
{
flags &= ~static_cast<uint32_t>(SemanticState::Focused);
}
m_semanticNode->stateFlags(flags);
markContentDirty();
}
bool SemanticData::requestFocus()
{
auto* parentNode = parent();
if (parentNode == nullptr || !parentNode->is<Node>())
{
return false;
}
auto* focusData = parentNode->as<Node>()->firstChild<FocusData>();
if (focusData == nullptr)
{
return false;
}
auto* ab = artboard();
if (ab == nullptr)
{
return false;
}
auto* focusManager = ab->focusManager();
if (focusManager == nullptr)
{
return false;
}
focusManager->setFocus(focusData->focusNode());
return true;
}
// A collapsed SemanticData stays in the tree but is skipped when the tree is
// flattened; see syncSemanticTreeVisibility.
bool SemanticData::collapse(bool value)
{
if (!Super::collapse(value))
{
return false;
}
syncSemanticTreeVisibility();
return true;
}
// Register dependency on parent's world transform so update() fires
// when the parent node moves.
void SemanticData::buildDependencies()
{
Super::buildDependencies();
auto* parentComponent = parent();
if (parentComponent != nullptr)
{
parentComponent->addDependent(this);
}
}
// Update cycle handler. Called when the parent node's world transform
// changes or when a sibling Shape's geometry is rebuilt. Re-evaluates
// inferred semantics and recomputes world-space bounds.
void SemanticData::update(ComponentDirt value)
{
// Paint dirt (opacity/color) does not affect whether the node is shown
// (see isExcludedFromSemanticTree). Only Collapsed dirt is handled here;
// the Hidden state goes through stateFlagsChanged.
if (hasDirt(value, ComponentDirt::Collapsed))
{
syncSemanticTreeVisibility();
}
if ((value & ComponentDirt::WorldTransform) == ComponentDirt::WorldTransform)
{
applyInferredSemanticsIfNeeded();
updateWorldBounds();
}
if ((value & ComponentDirt::Path) == ComponentDirt::Path)
{
updateWorldBounds();
}
}
// Applies inferred semantics if no explicit role/label was authored.
// Currently supports Text → role=label with concatenated run text.
void SemanticData::applyInferredSemanticsIfNeeded()
{
if (m_Role != 0 || !m_Label.empty() || m_semanticNode == nullptr)
{
return;
}
ResolvedSemanticData inferred;
if (!resolveInferredSemantics(parent(), inferred))
{
return;
}
if (m_semanticNode->role() == inferred.role &&
m_semanticNode->label() == inferred.label)
{
return;
}
m_semanticNode->role(inferred.role);
m_semanticNode->label(inferred.label);
markContentDirty();
}
// Marks semantic content as dirty so manager emits updated diffs.
void SemanticData::markContentDirty()
{
if (m_semanticNode == nullptr)
{
return;
}
auto* manager = m_semanticNode->manager();
if (manager != nullptr && parent() != nullptr)
{
manager->markNodeDirty(semanticId(), SemanticDirt::Content);
}
}
// Property change handlers. Called when role/label/stateFlags change from
// deserialization, animation, or data binding. Pushes the new value to the
// SemanticNode and marks content-dirty for the next diff.
void SemanticData::roleChanged()
{
if (m_semanticNode == nullptr || m_semanticNode->role() == m_Role)
{
return;
}
m_semanticNode->role(m_Role);
markContentDirty();
}
void SemanticData::labelChanged()
{
if (m_semanticNode == nullptr || m_semanticNode->label() == m_Label)
{
return;
}
m_semanticNode->label(m_Label);
markContentDirty();
}
void SemanticData::stateFlagsChanged()
{
if (m_semanticNode == nullptr)
{
return;
}
if (m_semanticNode->stateFlags() == m_StateFlags)
{
return;
}
const bool wasHidden = hasSemanticState(
m_semanticNode->stateFlags(),
SemanticState::Hidden);
m_semanticNode->stateFlags(m_StateFlags);
markContentDirty();
// The Hidden bit decides whether the node is shown, not just its content.
if (wasHidden != hasSemanticState(m_StateFlags, SemanticState::Hidden))
{
syncSemanticTreeVisibility();
}
}
void SemanticData::traitFlagsChanged()
{
if (m_semanticNode == nullptr || m_semanticNode->traitFlags() == m_TraitFlags)
{
return;
}
m_semanticNode->traitFlags(m_TraitFlags);
markContentDirty();
}
void SemanticData::valueChanged()
{
if (m_semanticNode == nullptr || m_semanticNode->value() == m_Value)
{
return;
}
m_semanticNode->value(m_Value);
markContentDirty();
}
void SemanticData::hintChanged()
{
if (m_semanticNode == nullptr || m_semanticNode->hint() == m_Hint)
{
return;
}
m_semanticNode->hint(m_Hint);
markContentDirty();
}
void SemanticData::headingLevelChanged()
{
if (m_semanticNode == nullptr ||
m_semanticNode->headingLevel() == m_HeadingLevel)
{
return;
}
m_semanticNode->headingLevel(m_HeadingLevel);
markContentDirty();
}
bool SemanticData::isExcludedFromSemanticTree() const
{
if (isCollapsed() || hasSemanticState(m_StateFlags, SemanticState::Hidden))
{
return true;
}
// A SemanticData with no owner has nothing hiding it.
auto* current = parent();
if (current == nullptr)
{
return false;
}
if (current->isCollapsed())
{
return true;
}
if (m_ownerHidden)
{
return true;
}
// Note: opacity 0 does NOT exclude from the semantic tree. A
// visually hidden component may still be interactive and should
// remain accessible to screen readers. Use the Hidden semantic
// state flag for intentional exclusion.
return false;
}
// Whether a node is shown is read when the tree is flattened (see
// isExcludedFromSemanticTree), so a change here only needs a re-flatten.
// Coming back, the node refreshes its bounds and content, which may have
// gone stale while it was hidden.
void SemanticData::syncSemanticTreeVisibility()
{
// Read even while unregistered: a node is reused when a new state
// machine registers it, without another sync.
m_ownerHidden = isDrawableHidden(parent());
if (m_semanticNode == nullptr)
{
return;
}
auto* manager = m_semanticNode->manager();
if (manager == nullptr)
{
return;
}
manager->markDirty(SemanticDirt::Structure);
if (isExcludedFromSemanticTree())
{
return;
}
m_boundsRetryPending = true;
updateWorldBounds();
addDirt(ComponentDirt::WorldTransform, false);
applyInferredSemanticsIfNeeded();
}
void SemanticData::updateWorldBounds()
{
if (m_semanticNode == nullptr)
{
return;
}
auto* parentComponent = parent();
if (parentComponent == nullptr)
{
return;
}
auto bounds = SemanticProvider::semanticBounds(parentComponent);
if (bounds.isEmptyOrNaN() && m_boundsRetryPending)
{
// Bounds not ready (e.g. sibling Shape paths still deferred).
// Re-dirty so updateComponents() runs another iteration after
// sibling paths have been rebuilt in this same pass.
addDirt(ComponentDirt::WorldTransform, false);
return;
}
m_boundsRetryPending = false;
if (bounds == m_semanticNode->bounds())
{
return;
}
m_semanticNode->bounds(bounds);
auto* manager = m_semanticNode->manager();
if (manager != nullptr)
{
manager->markNodeDirty(semanticId(), SemanticDirt::Bounds);
}
}
void SemanticData::addSemanticListener(SemanticListener* listener)
{
m_semanticListeners.push_back(listener);
}
void SemanticData::removeSemanticListener(SemanticListener* listener)
{
auto it = std::find(
m_semanticListeners.begin(),
m_semanticListeners.end(),
listener);
if (it != m_semanticListeners.end())
{
m_semanticListeners.erase(it);
}
}
void SemanticData::fireSemanticTap()
{
for (auto* listener : m_semanticListeners)
{
listener->onSemanticTap();
}
}
void SemanticData::fireSemanticIncrease()
{
for (auto* listener : m_semanticListeners)
{
listener->onSemanticIncrease();
}
}
void SemanticData::fireSemanticDecrease()
{
for (auto* listener : m_semanticListeners)
{
listener->onSemanticDecrease();
}
}