feat(focus): ui pass 1 (#13029) 72963a4d12 * core updates * wip * icons * rebased * rive remote and initial value fix * forgot the riv... * feat(editor): stage focus overlay indicating focus during playback Add a corner-bracket overlay on the design stage that shows where focus is while a state machine plays, without needing to wire focus state through view model properties. - Blue brackets around a directly focused element in the active artboard - Grey brackets around a nested artboard/component when focus is contained within it (matched via shared focus-node identity) - d-pad next/previous now wakes the stage so the bracket updates immediately instead of only on hover * feat(focus): unify focus listener actions, add Clear focus, and focus transition conditions Editor + core changes for focus authoring in state machines: Listener actions - Merge the separate "Focus Target" and "Focus Traversal" listener behaviours into a single "Focus" action with a leading mode combo (target / traverse / clear). Combos size to their widest option. - Add a new FocusActionClear core type (Dart + C++ runtime + rml) that clears the artboard focus at runtime. Viewport - Move the "Focus Indicator" option above "Show Final Playback" via a decoupled ViewportOption.displayOrder (enum declaration order stays fixed for the persisted bitmask). Transition conditions - Add a "Focus" left-hand condition option (in the artboard section, behind the focusManagement flag): `focus == / != <focusable component>`, with a right-hand dropdown listing components that have a FocusData child. - New TransitionFocusCondition core type (extends TransitionViewModelCondition), reusing a component comparator to store the target. Self-contained evaluation in Dart and C++ (queries FocusManager::hasPrimaryFocus); added a const focusManager() accessor on StateMachineInstance. Registered in rive_core, runtime, and rml core registries. Note: focus_overlay.dart carries a small pre-existing change (gating the overlay on the focusBrackets viewport option) included with this branch's focus work. * review: address Copilot feedback on focus condition - TransitionFocusCondition (Dart): resolve the target component comparator from either the right OR left slot, matching the C++ runtime. A .riv re-imported into the editor assigns the single exported comparator to the left slot, which previously made target/validateCondition/evaluate/uniqueId treat the condition as invalid and always evaluate false. - state_machine_manager: focus condition create/select helpers now use the slot-agnostic targetComparator so editing an imported focus condition works. - StageViewport: assert ViewportOption.displayOrder contains every option so a future enum addition can't silently drop a toggle from the menus. * fix(editor): remove unused imports failing flutter analyze CI runs `flutter analyze --no-fatal-infos --fatal-warnings`, which promoted two unused_import warnings in focus_data_inspector_builder.dart (component.dart and core_text_field.dart) to fatal errors. Neither symbol is referenced; removed them so the editor analyze step passes. * test(editor): allowlist condition_row.dart in theme rename audit The new `_FocusConditionFlow` paints `colors.warning` (0xFFF6C566) on its node connectors in the invalid state, mirroring `_DataPropertyFlow` — a legitimate +1 warning-hex site vs mainline, not a rename miswire. Add condition_row.dart to the audit allowlist with a justification (baseline regeneration only applies to changes already on mainline). * test(editor): allowlist backboard_inspector_builder.dart in theme rename audit The focus feature removed the legacy _FocusButton / _DPad / _DPadButton widgets from the backboard inspector (superseded by per-component focus authoring). Those widgets held all of the file's RiveColors sites (border, trackEmphasis, textSecondary x2 each), so the branch paints 0 vs mainline's 6 — an intentional removal, not a rename. Add it to the audit allowlist with a justification. * chore(rive_core): regenerate core from defs (focus types) * fix(core): reassign focus type keys to avoid collision with master Master's data-bind font PR (#13031) added ViewModelPropertyAssetFont (1034) and ViewModelInstanceAssetFont (1035), which now collide with the focus types introduced here. Reassign FocusActionClear -> 1037 and TransitionFocusCondition -> 1038 (next free keys after master's 1036) in the defs and generated base files (Dart + runtime + rml). Registrations reference the typeKey constants, so no other changes are needed. * style(runtime,rml): clang-format focus C++ files * test(runtime): add C++ tests for FocusActionClear and TransitionFocusCondition * test(runtime): avoid ODR-use of TransitionFocusCondition typeKey static * runtime test * feedback Co-authored-by: Alex Gibson <agibson.uk@gmail.com>
diff --git a/.rive_head b/.rive_head index 786a329..69cffd2 100644 --- a/.rive_head +++ b/.rive_head
@@ -1 +1 @@ -b20256f36800e04af741647860cb9013d148c76e +72963a4d12749be5f85af405b9cb903d8a82504c
diff --git a/dev/defs/animation/focus_action_clear.json b/dev/defs/animation/focus_action_clear.json new file mode 100644 index 0000000..10ea749 --- /dev/null +++ b/dev/defs/animation/focus_action_clear.json
@@ -0,0 +1,8 @@ +{ + "name": "FocusActionClear", + "key": { + "int": 1037, + "string": "focusactionclear" + }, + "extends": "animation/focus_action.json" +}
diff --git a/dev/defs/animation/transition_focus_condition.json b/dev/defs/animation/transition_focus_condition.json new file mode 100644 index 0000000..c3e7519 --- /dev/null +++ b/dev/defs/animation/transition_focus_condition.json
@@ -0,0 +1,8 @@ +{ + "name": "TransitionFocusCondition", + "key": { + "int": 1038, + "string": "transitionfocuscondition" + }, + "extends": "animation/transition_viewmodel_condition.json" +}
diff --git a/dev/defs/focus_data.json b/dev/defs/focus_data.json index 817271f..d0b84e9 100644 --- a/dev/defs/focus_data.json +++ b/dev/defs/focus_data.json
@@ -6,32 +6,52 @@ }, "extends": "component.json", "properties": { + "focusFlags": { + "type": "uint", + "initialValue": "7", + "animates": true, + "key": { + "int": 1033, + "string": "focusflags" + }, + "description": "Bitmask for focus flags", + "bindable": true + }, "canFocus": { "type": "bool", - "initialValue": "true", + "animates": true, "key": { "int": 953, "string": "canfocus" }, - "description": "Master switch - can this node receive focus at all?" + "description": "Master switch - can this node receive focus at all?", + "bindable": true, + "passthroughForBitmask": "focusFlags", + "passthroughBit": 0 }, "canTouch": { "type": "bool", - "initialValue": "true", + "animates": true, "key": { "int": 954, "string": "cantouch" }, - "description": "Can receive focus via pointer/touch click" + "description": "Can receive focus via pointer/touch click", + "bindable": true, + "passthroughForBitmask": "focusFlags", + "passthroughBit": 1 }, "canTraverse": { "type": "bool", - "initialValue": "true", + "animates": true, "key": { "int": 955, "string": "cantraverse" }, - "description": "Included in Tab/Shift+Tab traversal" + "description": "Included in Tab/Shift+Tab traversal", + "bindable": true, + "passthroughForBitmask": "focusFlags", + "passthroughBit": 2 }, "edgeBehaviorValue": { "type": "uint",
diff --git a/include/rive/animation/focus_action_clear.hpp b/include/rive/animation/focus_action_clear.hpp new file mode 100644 index 0000000..a17d954 --- /dev/null +++ b/include/rive/animation/focus_action_clear.hpp
@@ -0,0 +1,14 @@ +#ifndef _RIVE_FOCUS_ACTION_CLEAR_HPP_ +#define _RIVE_FOCUS_ACTION_CLEAR_HPP_ +#include "rive/generated/animation/focus_action_clear_base.hpp" +namespace rive +{ +class FocusActionClear : public FocusActionClearBase +{ +public: + void perform(StateMachineInstance* stateMachineInstance, + const ListenerInvocation& invocation) const override; +}; +} // namespace rive + +#endif
diff --git a/include/rive/animation/state_machine_instance.hpp b/include/rive/animation/state_machine_instance.hpp index a45b556..24774a8 100644 --- a/include/rive/animation/state_machine_instance.hpp +++ b/include/rive/animation/state_machine_instance.hpp
@@ -249,6 +249,14 @@ : &m_focusManager; } + /// Const overload of [focusManager], used by read-only consumers such as + /// condition evaluation. + const FocusManager* focusManager() const + { + return m_externalFocusManager ? m_externalFocusManager + : &m_focusManager; + } + /// Check if this state machine is using an external focus manager. bool hasExternalFocusManager() const {
diff --git a/include/rive/animation/transition_focus_condition.hpp b/include/rive/animation/transition_focus_condition.hpp new file mode 100644 index 0000000..7e7eaa5 --- /dev/null +++ b/include/rive/animation/transition_focus_condition.hpp
@@ -0,0 +1,19 @@ +#ifndef _RIVE_TRANSITION_FOCUS_CONDITION_HPP_ +#define _RIVE_TRANSITION_FOCUS_CONDITION_HPP_ +#include "rive/generated/animation/transition_focus_condition_base.hpp" +#include <stdio.h> +namespace rive +{ +// Condition that evaluates whether a specific focusable component currently +// holds focus. The target component is stored on the right comparator (a +// TransitionPropertyComponentComparator pointing at the focusable node); the +// left side is the implicit "current focus". Only ==/!= are meaningful. +class TransitionFocusCondition : public TransitionFocusConditionBase +{ +public: + bool evaluate(const StateMachineInstance* stateMachineInstance, + StateMachineLayerInstance* layerInstance) const override; +}; +} // namespace rive + +#endif
diff --git a/include/rive/focus_data.hpp b/include/rive/focus_data.hpp index 10b21f1..87dfbf1 100644 --- a/include/rive/focus_data.hpp +++ b/include/rive/focus_data.hpp
@@ -87,9 +87,7 @@ void update(ComponentDirt value) override; protected: - void canFocusChanged() override; - void canTouchChanged() override; - void canTraverseChanged() override; + void focusFlagsChanged() override; void edgeBehaviorValueChanged() override; void nameChanged() override;
diff --git a/include/rive/generated/animation/focus_action_clear_base.hpp b/include/rive/generated/animation/focus_action_clear_base.hpp new file mode 100644 index 0000000..1b67298 --- /dev/null +++ b/include/rive/generated/animation/focus_action_clear_base.hpp
@@ -0,0 +1,38 @@ +#ifndef _RIVE_FOCUS_ACTION_CLEAR_BASE_HPP_ +#define _RIVE_FOCUS_ACTION_CLEAR_BASE_HPP_ +#include "rive/animation/focus_action.hpp" +namespace rive +{ +class FocusActionClearBase : public FocusAction +{ +protected: + typedef FocusAction Super; + +public: + static const uint16_t typeKey = 1037; + + /// Helper to quickly determine if a core object extends another without + /// RTTI at runtime. + bool isTypeOf(uint16_t typeKey) const override + { + switch (typeKey) + { + case FocusActionClearBase::typeKey: + case FocusActionBase::typeKey: + case ListenerActionBase::typeKey: + return true; + default: + return false; + } + } + + uint16_t coreType() const override { return typeKey; } + + Core* clone() const override; + void copy(const FocusActionClearBase& object) { FocusAction::copy(object); } + +protected: +}; +} // namespace rive + +#endif
diff --git a/include/rive/generated/animation/transition_focus_condition_base.hpp b/include/rive/generated/animation/transition_focus_condition_base.hpp new file mode 100644 index 0000000..a9759a0 --- /dev/null +++ b/include/rive/generated/animation/transition_focus_condition_base.hpp
@@ -0,0 +1,37 @@ +#ifndef _RIVE_TRANSITION_FOCUS_CONDITION_BASE_HPP_ +#define _RIVE_TRANSITION_FOCUS_CONDITION_BASE_HPP_ +#include "rive/animation/transition_viewmodel_condition.hpp" +namespace rive +{ +class TransitionFocusConditionBase : public TransitionViewModelCondition +{ +protected: + typedef TransitionViewModelCondition Super; + +public: + static const uint16_t typeKey = 1038; + + /// Helper to quickly determine if a core object extends another without + /// RTTI at runtime. + bool isTypeOf(uint16_t typeKey) const override + { + switch (typeKey) + { + case TransitionFocusConditionBase::typeKey: + case TransitionViewModelConditionBase::typeKey: + case TransitionConditionBase::typeKey: + return true; + default: + return false; + } + } + + uint16_t coreType() const override { return typeKey; } + + Core* clone() const override; + +protected: +}; +} // namespace rive + +#endif
diff --git a/include/rive/generated/core_registry.hpp b/include/rive/generated/core_registry.hpp index 36e1fc9..adf504c 100644 --- a/include/rive/generated/core_registry.hpp +++ b/include/rive/generated/core_registry.hpp
@@ -21,6 +21,7 @@ #include "rive/animation/entry_state.hpp" #include "rive/animation/exit_state.hpp" #include "rive/animation/focus_action.hpp" +#include "rive/animation/focus_action_clear.hpp" #include "rive/animation/focus_action_target.hpp" #include "rive/animation/focus_action_traversal.hpp" #include "rive/animation/interpolating_keyframe.hpp" @@ -78,6 +79,7 @@ #include "rive/animation/state_transition.hpp" #include "rive/animation/transition_artboard_condition.hpp" #include "rive/animation/transition_bool_condition.hpp" +#include "rive/animation/transition_focus_condition.hpp" #include "rive/animation/transition_comparator.hpp" #include "rive/animation/transition_condition.hpp" #include "rive/animation/transition_input_condition.hpp" @@ -555,6 +557,8 @@ return new TransitionViewModelCondition(); case TransitionArtboardConditionBase::typeKey: return new TransitionArtboardCondition(); + case TransitionFocusConditionBase::typeKey: + return new TransitionFocusCondition(); case AnyStateBase::typeKey: return new AnyState(); case BlendState1DInputBase::typeKey: @@ -583,6 +587,8 @@ return new KeyFrameColor(); case FocusActionTraversalBase::typeKey: return new FocusActionTraversal(); + case FocusActionClearBase::typeKey: + return new FocusActionClear(); case StateMachineBase::typeKey: return new StateMachine(); case StateMachineFireEventBase::typeKey: @@ -1458,6 +1464,9 @@ case ImageBase::fitPropertyKey: object->as<ImageBase>()->fit(value); break; + case FocusDataBase::focusFlagsPropertyKey: + object->as<FocusDataBase>()->focusFlags(value); + break; case FocusDataBase::edgeBehaviorValuePropertyKey: object->as<FocusDataBase>()->edgeBehaviorValue(value); break; @@ -1977,14 +1986,56 @@ object->as<ClippingShapeBase>()->isVisible(value); break; case FocusDataBase::canFocusPropertyKey: - object->as<FocusDataBase>()->canFocus(value); + { + auto* _o = object->as<FocusDataBase>(); + if (_o) + { + const uint32_t _cur = _o->focusFlags(); + const uint32_t _bm = static_cast<uint32_t>(1u << 0); + const uint32_t _next = static_cast<uint32_t>( + (_cur & ~_bm) | + (value ? _bm : static_cast<uint32_t>(0))); + if (_cur != _next) + { + _o->focusFlags(_next); + } + } break; + } case FocusDataBase::canTouchPropertyKey: - object->as<FocusDataBase>()->canTouch(value); + { + auto* _o = object->as<FocusDataBase>(); + if (_o) + { + const uint32_t _cur = _o->focusFlags(); + const uint32_t _bm = static_cast<uint32_t>(1u << 1); + const uint32_t _next = static_cast<uint32_t>( + (_cur & ~_bm) | + (value ? _bm : static_cast<uint32_t>(0))); + if (_cur != _next) + { + _o->focusFlags(_next); + } + } break; + } case FocusDataBase::canTraversePropertyKey: - object->as<FocusDataBase>()->canTraverse(value); + { + auto* _o = object->as<FocusDataBase>(); + if (_o) + { + const uint32_t _cur = _o->focusFlags(); + const uint32_t _bm = static_cast<uint32_t>(1u << 2); + const uint32_t _next = static_cast<uint32_t>( + (_cur & ~_bm) | + (value ? _bm : static_cast<uint32_t>(0))); + if (_cur != _next) + { + _o->focusFlags(_next); + } + } break; + } case CustomPropertyBooleanBase::propertyValuePropertyKey: object->as<CustomPropertyBooleanBase>()->propertyValue(value); break; @@ -3477,6 +3528,8 @@ return object->as<ImageBase>()->assetId(); case ImageBase::fitPropertyKey: return object->as<ImageBase>()->fit(); + case FocusDataBase::focusFlagsPropertyKey: + return object->as<FocusDataBase>()->focusFlags(); case FocusDataBase::edgeBehaviorValuePropertyKey: return object->as<FocusDataBase>()->edgeBehaviorValue(); case DrawRulesBase::drawTargetIdPropertyKey: @@ -3830,12 +3883,6 @@ return object->as<RectangleBase>()->linkCornerRadius(); case ClippingShapeBase::isVisiblePropertyKey: return object->as<ClippingShapeBase>()->isVisible(); - case FocusDataBase::canFocusPropertyKey: - return object->as<FocusDataBase>()->canFocus(); - case FocusDataBase::canTouchPropertyKey: - return object->as<FocusDataBase>()->canTouch(); - case FocusDataBase::canTraversePropertyKey: - return object->as<FocusDataBase>()->canTraverse(); case CustomPropertyBooleanBase::propertyValuePropertyKey: return object->as<CustomPropertyBooleanBase>()->propertyValue(); case LayoutComponentBase::clipPropertyKey: @@ -4501,6 +4548,7 @@ case PolygonBase::pointsPropertyKey: case ImageBase::assetIdPropertyKey: case ImageBase::fitPropertyKey: + case FocusDataBase::focusFlagsPropertyKey: case FocusDataBase::edgeBehaviorValuePropertyKey: case DrawRulesBase::drawTargetIdPropertyKey: case LayoutComponentBase::styleIdPropertyKey: @@ -4657,9 +4705,6 @@ case PointsCommonPathBase::isClosedPropertyKey: case RectangleBase::linkCornerRadiusPropertyKey: case ClippingShapeBase::isVisiblePropertyKey: - case FocusDataBase::canFocusPropertyKey: - case FocusDataBase::canTouchPropertyKey: - case FocusDataBase::canTraversePropertyKey: case CustomPropertyBooleanBase::propertyValuePropertyKey: case LayoutComponentBase::clipPropertyKey: case DataBindPathBase::isRelativePropertyKey: @@ -5259,6 +5304,8 @@ return object->is<ImageBase>(); case ImageBase::fitPropertyKey: return object->is<ImageBase>(); + case FocusDataBase::focusFlagsPropertyKey: + return object->is<FocusDataBase>(); case FocusDataBase::edgeBehaviorValuePropertyKey: return object->is<FocusDataBase>(); case DrawRulesBase::drawTargetIdPropertyKey:
diff --git a/include/rive/generated/focus_data_base.hpp b/include/rive/generated/focus_data_base.hpp index e680398..25ea166 100644 --- a/include/rive/generated/focus_data_base.hpp +++ b/include/rive/generated/focus_data_base.hpp
@@ -29,52 +29,30 @@ uint16_t coreType() const override { return typeKey; } + static const uint16_t focusFlagsPropertyKey = 1033; static const uint16_t canFocusPropertyKey = 953; + static const uint32_t canFocusBitmask = 1u << 0; static const uint16_t canTouchPropertyKey = 954; + static const uint32_t canTouchBitmask = 1u << 1; static const uint16_t canTraversePropertyKey = 955; + static const uint32_t canTraverseBitmask = 1u << 2; static const uint16_t edgeBehaviorValuePropertyKey = 956; protected: - bool m_CanFocus = true; - bool m_CanTouch = true; - bool m_CanTraverse = true; + uint32_t m_FocusFlags = 7; uint32_t m_EdgeBehaviorValue = 0; public: - inline bool canFocus() const { return m_CanFocus; } - void canFocus(bool value) + inline uint32_t focusFlags() const { return m_FocusFlags; } + void focusFlags(uint32_t value) { - if (m_CanFocus == value) + if (m_FocusFlags == value) { return; } - m_CanFocus = value; - canFocusChanged(); - notifyPropertyChanged(canFocusPropertyKey); - } - - inline bool canTouch() const { return m_CanTouch; } - void canTouch(bool value) - { - if (m_CanTouch == value) - { - return; - } - m_CanTouch = value; - canTouchChanged(); - notifyPropertyChanged(canTouchPropertyKey); - } - - inline bool canTraverse() const { return m_CanTraverse; } - void canTraverse(bool value) - { - if (m_CanTraverse == value) - { - return; - } - m_CanTraverse = value; - canTraverseChanged(); - notifyPropertyChanged(canTraversePropertyKey); + m_FocusFlags = value; + focusFlagsChanged(); + notifyPropertyChanged(focusFlagsPropertyKey); } inline uint32_t edgeBehaviorValue() const { return m_EdgeBehaviorValue; } @@ -92,9 +70,7 @@ Core* clone() const override; void copy(const FocusDataBase& object) { - m_CanFocus = object.m_CanFocus; - m_CanTouch = object.m_CanTouch; - m_CanTraverse = object.m_CanTraverse; + m_FocusFlags = object.m_FocusFlags; m_EdgeBehaviorValue = object.m_EdgeBehaviorValue; Component::copy(object); } @@ -103,14 +79,8 @@ { switch (propertyKey) { - case canFocusPropertyKey: - m_CanFocus = CoreBoolType::deserialize(reader); - return true; - case canTouchPropertyKey: - m_CanTouch = CoreBoolType::deserialize(reader); - return true; - case canTraversePropertyKey: - m_CanTraverse = CoreBoolType::deserialize(reader); + case focusFlagsPropertyKey: + m_FocusFlags = CoreUintType::deserialize(reader); return true; case edgeBehaviorValuePropertyKey: m_EdgeBehaviorValue = CoreUintType::deserialize(reader); @@ -120,9 +90,7 @@ } protected: - virtual void canFocusChanged() {} - virtual void canTouchChanged() {} - virtual void canTraverseChanged() {} + virtual void focusFlagsChanged() {} virtual void edgeBehaviorValueChanged() {} }; } // namespace rive
diff --git a/src/animation/focus_action_clear.cpp b/src/animation/focus_action_clear.cpp new file mode 100644 index 0000000..c5d2ebb --- /dev/null +++ b/src/animation/focus_action_clear.cpp
@@ -0,0 +1,26 @@ +/* + * Copyright 2024 Rive + */ + +#include "rive/animation/focus_action_clear.hpp" +#include "rive/animation/listener_invocation.hpp" +#include "rive/animation/state_machine_instance.hpp" +#include "rive/input/focus_manager.hpp" + +using namespace rive; + +void FocusActionClear::perform(StateMachineInstance* stateMachineInstance, + const ListenerInvocation& invocation) const +{ + (void)invocation; + if (stateMachineInstance == nullptr) + { + return; + } + + FocusManager* manager = stateMachineInstance->focusManager(); + if (manager != nullptr) + { + manager->clearFocus(); + } +}
diff --git a/src/animation/transition_focus_condition.cpp b/src/animation/transition_focus_condition.cpp new file mode 100644 index 0000000..7e19f3e --- /dev/null +++ b/src/animation/transition_focus_condition.cpp
@@ -0,0 +1,73 @@ +/* + * Copyright 2024 Rive + */ + +#include "rive/animation/transition_focus_condition.hpp" +#include "rive/animation/state_machine_instance.hpp" +#include "rive/animation/transition_condition_op.hpp" +#include "rive/animation/transition_property_component_comparator.hpp" +#include "rive/artboard.hpp" +#include "rive/focus_data.hpp" +#include "rive/input/focus_manager.hpp" +#include "rive/node.hpp" + +using namespace rive; + +bool TransitionFocusCondition::evaluate( + const StateMachineInstance* stateMachineInstance, + StateMachineLayerInstance* layerInstance) const +{ + (void)layerInstance; + if (stateMachineInstance == nullptr) + { + return false; + } + + // The target focusable component is stored on a component comparator. In + // the editor it lives on the right (the left side is the implicit "current + // focus"), but on runtime import the single exported comparator is assigned + // to the left slot, so accept it from either side. + TransitionComparator* comparator = rightComparator(); + if (comparator == nullptr || + !comparator->is<TransitionPropertyComponentComparator>()) + { + comparator = leftComparator(); + } + if (comparator == nullptr || + !comparator->is<TransitionPropertyComponentComparator>()) + { + return false; + } + auto focusComparator = + comparator->as<TransitionPropertyComponentComparator>(); + + const FocusManager* manager = stateMachineInstance->focusManager(); + if (manager == nullptr) + { + return false; + } + + auto target = + stateMachineInstance->artboard()->resolve(focusComparator->objectId()); + + bool focused = false; + if (target != nullptr && target->is<Node>()) + { + auto node = target->as<Node>(); + for (auto child : node->children()) + { + if (child->is<FocusData>()) + { + auto focusNode = child->as<FocusData>()->focusNode(); + // Use hasFocus (not hasPrimaryFocus): the target is considered + // focused when it or any descendant is the primary focus, so a + // scope/container counts as focused while a child leaf holds + // focus. + focused = focusNode != nullptr && manager->hasFocus(focusNode); + break; + } + } + } + + return op() == TransitionConditionOp::equal ? focused : !focused; +}
diff --git a/src/file.cpp b/src/file.cpp index 3d66d88..9bb1150 100644 --- a/src/file.cpp +++ b/src/file.cpp
@@ -546,6 +546,7 @@ break; case TransitionViewModelCondition::typeKey: case TransitionArtboardCondition::typeKey: + case TransitionFocusCondition::typeKey: stackObject = std::make_unique<TransitionViewModelConditionImporter>( object->as<TransitionViewModelCondition>());
diff --git a/src/focus_data.cpp b/src/focus_data.cpp index 631e619..d4a3387 100644 --- a/src/focus_data.cpp +++ b/src/focus_data.cpp
@@ -57,9 +57,9 @@ if (m_focusNode == nullptr) { m_focusNode = rcp<FocusNode>(new FocusNode(this)); - m_focusNode->canFocus(m_CanFocus); - m_focusNode->canTouch(m_CanTouch); - m_focusNode->canTraverse(m_CanTraverse); + m_focusNode->canFocus((focusFlags() & canFocusBitmask) != 0); + m_focusNode->canTouch((focusFlags() & canTouchBitmask) != 0); + m_focusNode->canTraverse((focusFlags() & canTraverseBitmask) != 0); m_focusNode->edgeBehavior( static_cast<EdgeBehavior>(m_EdgeBehaviorValue)); m_focusNode->name(name()); @@ -369,27 +369,13 @@ } } -void FocusData::canFocusChanged() +void FocusData::focusFlagsChanged() { if (m_focusNode != nullptr) { - m_focusNode->canFocus(m_CanFocus); - } -} - -void FocusData::canTouchChanged() -{ - if (m_focusNode != nullptr) - { - m_focusNode->canTouch(m_CanTouch); - } -} - -void FocusData::canTraverseChanged() -{ - if (m_focusNode != nullptr) - { - m_focusNode->canTraverse(m_CanTraverse); + m_focusNode->canFocus((focusFlags() & canFocusBitmask) != 0); + m_focusNode->canTouch((focusFlags() & canTouchBitmask) != 0); + m_focusNode->canTraverse((focusFlags() & canTraverseBitmask) != 0); } }
diff --git a/src/generated/animation/focus_action_clear_base.cpp b/src/generated/animation/focus_action_clear_base.cpp new file mode 100644 index 0000000..adec5af --- /dev/null +++ b/src/generated/animation/focus_action_clear_base.cpp
@@ -0,0 +1,11 @@ +#include "rive/generated/animation/focus_action_clear_base.hpp" +#include "rive/animation/focus_action_clear.hpp" + +using namespace rive; + +Core* FocusActionClearBase::clone() const +{ + auto cloned = new FocusActionClear(); + cloned->copy(*this); + return cloned; +}
diff --git a/src/generated/animation/transition_focus_condition_base.cpp b/src/generated/animation/transition_focus_condition_base.cpp new file mode 100644 index 0000000..6ff444a --- /dev/null +++ b/src/generated/animation/transition_focus_condition_base.cpp
@@ -0,0 +1,11 @@ +#include "rive/generated/animation/transition_focus_condition_base.hpp" +#include "rive/animation/transition_focus_condition.hpp" + +using namespace rive; + +Core* TransitionFocusConditionBase::clone() const +{ + auto cloned = new TransitionFocusCondition(); + cloned->copy(*this); + return cloned; +}
diff --git a/tests/unit_tests/assets/focus_test.riv b/tests/unit_tests/assets/focus_test.riv new file mode 100644 index 0000000..a328f39 --- /dev/null +++ b/tests/unit_tests/assets/focus_test.riv Binary files differ
diff --git a/tests/unit_tests/runtime/focus_test.cpp b/tests/unit_tests/runtime/focus_test.cpp index e688758..0e856b6 100644 --- a/tests/unit_tests/runtime/focus_test.cpp +++ b/tests/unit_tests/runtime/focus_test.cpp
@@ -1,5 +1,9 @@ #include <catch.hpp> +#include "rive/animation/focus_action_clear.hpp" #include "rive/animation/focus_action_traversal.hpp" +#include "rive/animation/transition_condition_op.hpp" +#include "rive/animation/transition_focus_condition.hpp" +#include "rive/animation/transition_property_component_comparator.hpp" #include "rive/animation/listener_invocation.hpp" #include "rive/animation/state_machine.hpp" #include "rive/animation/state_machine_instance.hpp" @@ -1089,6 +1093,104 @@ CHECK(manager.primaryFocus() == leaf2); } +// ============================================================================= +// FocusActionClear Tests +// ============================================================================= + +TEST_CASE("FocusActionClear perform clears the primary focus", + "[FocusActionClear]") +{ + NoOpFactory factory; + Artboard artboard(&factory); + auto instance = artboard.instance(); + StateMachine machine; + StateMachineInstance smi(&machine, instance.get()); + + FocusManager* fm = smi.focusManager(); + MockFocusable f1; + auto node1 = make_rcp<FocusNode>(&f1); + fm->addChild(nullptr, node1); + fm->setFocus(node1); + REQUIRE(fm->primaryFocus() == node1); + + FocusActionClear action; + action.perform(&smi, ListenerInvocation::none()); + + CHECK(fm->primaryFocus() == nullptr); +} + +TEST_CASE("FocusActionClear perform is a no-op when nothing is focused", + "[FocusActionClear]") +{ + NoOpFactory factory; + Artboard artboard(&factory); + auto instance = artboard.instance(); + StateMachine machine; + StateMachineInstance smi(&machine, instance.get()); + + REQUIRE(smi.focusManager()->primaryFocus() == nullptr); + + FocusActionClear action; + action.perform(&smi, ListenerInvocation::none()); + + CHECK(smi.focusManager()->primaryFocus() == nullptr); +} + +TEST_CASE("FocusActionClear perform ignores null StateMachineInstance", + "[FocusActionClear]") +{ + FocusActionClear action; + // Must not dereference the null instance. + action.perform(nullptr, ListenerInvocation::none()); +} + +// ============================================================================= +// TransitionFocusCondition Tests +// ============================================================================= + +TEST_CASE("TransitionFocusCondition uses the reassigned core type key", + "[TransitionFocusCondition]") +{ + // Locks in the collision fix: master's font PR claimed 1035, so this + // condition was reassigned to 1038. A regression here means a type-key + // clash on import/export. + // Copy into a local to avoid ODR-using the in-class static constant + // (which has no out-of-line definition) when binding it to Catch2's + // by-reference comparison expressions. + uint16_t typeKey = TransitionFocusConditionBase::typeKey; + CHECK(typeKey == 1038); + + auto condition = std::make_unique<TransitionFocusCondition>(); + CHECK(condition->coreType() == typeKey); + CHECK(condition->is<TransitionFocusCondition>()); +} + +TEST_CASE("TransitionFocusCondition evaluate returns false for a null " + "StateMachineInstance", + "[TransitionFocusCondition]") +{ + // Heap allocation value-initializes the (comparator) members to null, so + // the guard clauses and destructor are well-defined even without import. + auto condition = std::make_unique<TransitionFocusCondition>(); + CHECK(condition->evaluate(nullptr, nullptr) == false); +} + +TEST_CASE("TransitionFocusCondition evaluate returns false when no target " + "comparator is configured", + "[TransitionFocusCondition]") +{ + NoOpFactory factory; + Artboard artboard(&factory); + auto instance = artboard.instance(); + StateMachine machine; + StateMachineInstance smi(&machine, instance.get()); + + auto condition = std::make_unique<TransitionFocusCondition>(); + // With neither comparator set to a TransitionPropertyComponentComparator, + // there is no focus target to evaluate against, so the condition is false. + CHECK(condition->evaluate(&smi, nullptr) == false); +} + } // namespace rive TEST_CASE("FocusManager skips collapsed nodes and fully transparent nodes", @@ -1723,4 +1825,39 @@ artboard->draw(renderer.get()); CHECK(silver.matches("list_focus_order")); -} \ No newline at end of file +} + +TEST_CASE("Focus based transitions work", "[silver]") +{ + rive::SerializingFactory silver; + auto file = ReadRiveFile("assets/focus_test.riv", &silver); + + auto artboard = file->artboardDefault(); + REQUIRE(artboard != nullptr); + + silver.frameSize(artboard->width(), artboard->height()); + + auto stateMachine = artboard->stateMachineAt(0); + auto focusManager = stateMachine->focusManager(); + + auto vmi = file->createDefaultViewModelInstance(artboard.get()); + + auto renderer = silver.makeRenderer(); + stateMachine->bindViewModelInstance(vmi); + stateMachine->advanceAndApply(0.016f); + artboard->draw(renderer.get()); + silver.addFrame(); + + stateMachine->pointerDown(rive::Vec2D(55.0, 65.0)); + stateMachine->pointerUp(rive::Vec2D(55.0, 65.0)); + stateMachine->advanceAndApply(0.016f); + artboard->draw(renderer.get()); + silver.addFrame(); + + stateMachine->pointerDown(rive::Vec2D(442.0, 65.0)); + stateMachine->pointerUp(rive::Vec2D(442.0, 65.0)); + stateMachine->advanceAndApply(0.016f); + artboard->draw(renderer.get()); + + CHECK(silver.matches("focus_test")); +}
diff --git a/tests/unit_tests/silvers/focus_test.sriv b/tests/unit_tests/silvers/focus_test.sriv new file mode 100644 index 0000000..79cc14b --- /dev/null +++ b/tests/unit_tests/silvers/focus_test.sriv Binary files differ