blob: cc2ea11aa1b38112792919369a95d7fa290799af [file]
#include "rive/semantic/semantic_data.hpp"
#include "rive/artboard.hpp"
#include "rive/artboard_host.hpp"
#include "rive/component_dirt.hpp"
#include "rive/container_component.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);
}
}
}
// 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_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.
SemanticData* SemanticData::findParentSemanticData() const
{
auto* current = parent();
while (current != nullptr)
{
if (current->is<Node>())
{
auto* node = current->as<Node>();
for (auto* child : node->children())
{
if (child->is<SemanticData>() && child != this)
{
return child->as<SemanticData>();
}
}
}
current = current->parent();
}
return nullptr;
}
// Finds the closest SemanticNode for a component, crossing artboard
// boundaries via ArtboardHost. Walks up the component hierarchy; when
// it hits an Artboard, jumps to the host component in the parent artboard.
// Starts from the component itself so that SemanticData attached directly
// to the component (e.g. a NestedArtboard with role=button) is found.
rcp<SemanticNode> SemanticData::findClosestSemanticNode(Component* component)
{
if (component == nullptr)
{
return nullptr;
}
auto* current = component;
while (current != nullptr)
{
if (current->is<Artboard>())
{
auto* artboard = current->as<Artboard>();
// Try to cross the artboard boundary via the host component
auto* host = artboard->host();
if (host != nullptr)
{
auto* hostComponent = host->hostComponent();
if (hostComponent != nullptr &&
hostComponent->is<ContainerComponent>())
{
// Continue searching from the host component (e.g.,
// NestedArtboard)
current = hostComponent->as<ContainerComponent>();
continue;
}
}
// No way to traverse up, stop searching
return nullptr;
}
if (current->is<Node>())
{
if (auto* data = current->as<Node>()->firstChild<SemanticData>())
{
return data->semanticNode();
}
}
current = current->parent();
}
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;
}
// Collapse/uncollapse: removes or re-adds SemanticNode to the tree.
// Collapse caches the manager before removal. Uncollapse re-adds with
// correct parent, recomputes bounds, and forces a WorldTransform dirt.
bool SemanticData::collapse(bool value)
{
if (!Super::collapse(value))
{
return false;
}
if (m_semanticNode == nullptr)
{
return true;
}
if (value)
{
// Capture the manager before removeChild clears it.
auto* manager = m_semanticNode->manager();
if (manager != nullptr)
{
m_semanticManager = manager;
manager->removeChild(m_semanticNode);
}
}
else
{
// Re-add to semantic tree using the cached manager.
auto* manager = m_semanticManager;
if (manager == nullptr)
{
manager = m_semanticNode->manager();
}
if (manager != nullptr)
{
m_semanticManager = manager;
manager->addChild(resolveParentNode(), m_semanticNode);
// Re-apply current bounds and content so the node is up to date.
m_boundsRetryPending = true;
updateWorldBounds();
// Force a WorldTransform update so bounds are recomputed
// in the next update cycle after layout has been recalculated.
addDirt(ComponentDirt::WorldTransform, false);
applyInferredSemanticsIfNeeded();
}
}
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 tree membership — see
// shouldExcludeFromSemanticTree(). Only Collapsed + Hidden-state matter.
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;
}
m_semanticNode->stateFlags(m_StateFlags);
markContentDirty();
// Hidden bit changes affect tree membership, not just content.
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();
}
rcp<SemanticNode> SemanticData::resolveParentNode() const
{
auto* localParent = findParentSemanticData();
rcp<SemanticNode> parentNode =
localParent != nullptr ? localParent->semanticNode() : nullptr;
if (parentNode == nullptr)
{
auto* ab = artboard();
if (ab != nullptr)
{
parentNode = ab->semanticBoundaryNode();
}
}
return parentNode;
}
bool SemanticData::shouldExcludeFromSemanticTree() const
{
if (hasSemanticState(m_StateFlags, SemanticState::Hidden))
{
return true;
}
auto* current = parent();
if (current == nullptr)
{
return true;
}
if (current->isCollapsed())
{
return true;
}
if (current->is<Drawable>() && current->as<Drawable>()->isHidden())
{
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;
}
void SemanticData::syncSemanticTreeVisibility()
{
if (m_semanticNode == nullptr)
{
return;
}
const bool shouldExclude = shouldExcludeFromSemanticTree();
if (shouldExclude == m_excludedFromTree)
{
return;
}
m_excludedFromTree = shouldExclude;
auto* manager = m_semanticNode->manager();
if (shouldExclude)
{
if (manager != nullptr)
{
m_semanticManager = manager;
manager->removeChild(m_semanticNode);
}
return;
}
// Already tracked in the semantic tree.
if (manager != nullptr)
{
return;
}
manager = m_semanticManager;
if (manager == nullptr)
{
return;
}
manager->addChild(resolveParentNode(), m_semanticNode);
m_boundsRetryPending = true;
updateWorldBounds();
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();
}
}