feat(apple): add semantics support (#12642) 9f3eef8634 Co-authored-by: David Skuza <david@rive.app>
diff --git a/.rive_head b/.rive_head index 1819944..f8c5e45 100644 --- a/.rive_head +++ b/.rive_head
@@ -1 +1 @@ -bd587cb83fb48eb8ab67c9bf23eb9ecf184f7125 +9f3eef86340a3ef53568116c197fd343a9e1d4e3
diff --git a/include/rive/command_queue.hpp b/include/rive/command_queue.hpp index 8208fac..8abc809 100644 --- a/include/rive/command_queue.hpp +++ b/include/rive/command_queue.hpp
@@ -627,6 +627,10 @@ uint32_t semanticNodeId, uint64_t requestId = 0); + // Clear all semantic focus. Called when the screen reader moves + // focus entirely away from the hosting view. + void clearSemanticFocus(StateMachineHandle, uint64_t requestId = 0); + RenderImageHandle decodeImage(std::vector<uint8_t> imageEncodedBytes, RenderImageListener* listener = nullptr, uint64_t requestId = 0); @@ -912,6 +916,7 @@ drainSemanticsDiff, fireSemanticAction, requestSemanticFocus, + clearSemanticFocus, bindViewModelInstance, runOnce, draw,
diff --git a/src/command_queue.cpp b/src/command_queue.cpp index 54f4220..25d2148 100644 --- a/src/command_queue.cpp +++ b/src/command_queue.cpp
@@ -720,6 +720,15 @@ m_commandStream << semanticNodeId; } +void CommandQueue::clearSemanticFocus(StateMachineHandle stateMachineHandle, + uint64_t requestId) +{ + AutoLockAndNotify lock(m_commandMutex, m_commandConditionVariable); + m_commandStream << Command::clearSemanticFocus; + m_commandStream << stateMachineHandle; + m_commandStream << requestId; +} + RenderImageHandle CommandQueue::decodeImage( std::vector<uint8_t> imageEncodedBytes, RenderImageListener* listener,
diff --git a/src/command_server.cpp b/src/command_server.cpp index c272c51..2a85442 100644 --- a/src/command_server.cpp +++ b/src/command_server.cpp
@@ -2113,6 +2113,35 @@ break; } + case CommandQueue::Command::clearSemanticFocus: + { + StateMachineHandle handle; + uint64_t requestId; + commandStream >> handle; + commandStream >> requestId; + lock.unlock(); + if (auto wrapper = getStateMachineWrapper(handle)) + { + std::unique_lock<std::mutex> stateMachineLock( + wrapper->m_mutex); + if (auto* focusManager = wrapper->instance->focusManager()) + { + focusManager->clearFocus(); + } + } + else + { + ErrorReporter<StateMachineHandle>( + this, + handle, + requestId, + CommandQueue::Message::stateMachineError) + << "State machine " << handle + << " not found for clearSemanticFocus."; + } + break; + } + case CommandQueue::Command::runOnce: { CommandServerCallback callback;
diff --git a/tests/unit_tests/runtime/command_queue_test.cpp b/tests/unit_tests/runtime/command_queue_test.cpp index b5e4e15..9c25063 100644 --- a/tests/unit_tests/runtime/command_queue_test.cpp +++ b/tests/unit_tests/runtime/command_queue_test.cpp
@@ -9,6 +9,7 @@ #include "rive/file.hpp" #include "rive/semantic/semantic_role.hpp" #include "rive/semantic/semantic_state.hpp" +#include "rive/semantic/semantic_trait.hpp" #include "common/render_context_null.hpp" #include <fstream> @@ -5633,14 +5634,15 @@ ViewModelInstanceHandle viewModelHandle = RIVE_NULL_HANDLE; StateMachineHandle stateMachineHandle = RIVE_NULL_HANDLE; - SemanticFixture(SemanticTestListener* listener = nullptr) + SemanticFixture(SemanticTestListener* listener = nullptr, + const char* rivPath = "assets/semantic/simpsons.riv") { commandQueue = make_rcp<CommandQueue>(); nullContext = RenderContextNULL::MakeContext(); server = std::make_unique<CommandServer>(commandQueue, nullContext.get()); - std::ifstream stream("assets/semantic/simpsons.riv", std::ios::binary); + std::ifstream stream(rivPath, std::ios::binary); fileHandle = commandQueue->loadFile( std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {})); artboardHandle = commandQueue->instantiateDefaultArtboard(fileHandle); @@ -5856,12 +5858,13 @@ SemanticActionType::tap, 0xE3); fx.commandQueue->requestSemanticFocus(bogusHandle, 42, 0xE4); + fx.commandQueue->clearSemanticFocus(bogusHandle, 0xE5); fx.pump(); // One stateMachineError per semantic command; the instantiation failure is // an artboardError, so it does not reach this listener. - CHECK(bogusListener.m_errorCount == 4); + CHECK(bogusListener.m_errorCount == 5); CHECK(bogusListener.m_diffCount == 0); } @@ -6000,6 +6003,53 @@ CHECK(listener.m_errorCount == 0); } +TEST_CASE("Semantics clearSemanticFocus removes Focused bit from focused node", + "[CommandQueue]") +{ + SemanticTestListener listener; + SemanticFixture fx(&listener, + "assets/semantic/semantic_list_scroll_focus_fixed.riv"); + + fx.commandQueue->enableSemantics(fx.stateMachineHandle); + fx.warmup(); + fx.drain(); + fx.pump(); + + // Find a node with the Focusable trait. + uint32_t focusableId = 0; + for (auto& kv : listener.m_model.nodes) + { + if (hasSemanticTrait(kv.second.traitFlags, SemanticTrait::Focusable)) + { + focusableId = kv.second.id; + break; + } + } + REQUIRE(focusableId != 0); + + // Focus it through the command queue. + fx.commandQueue->requestSemanticFocus(fx.stateMachineHandle, focusableId); + fx.warmup(); + fx.drain(); + fx.pump(); + + auto it = listener.m_model.nodes.find(focusableId); + REQUIRE(it != listener.m_model.nodes.end()); + CHECK(hasSemanticState(it->second.stateFlags, SemanticState::Focused)); + + // Clear focus through the command queue. + fx.commandQueue->clearSemanticFocus(fx.stateMachineHandle); + fx.warmup(); + fx.drain(); + fx.pump(); + + it = listener.m_model.nodes.find(focusableId); + REQUIRE(it != listener.m_model.nodes.end()); + CHECK_FALSE( + hasSemanticState(it->second.stateFlags, SemanticState::Focused)); + CHECK(listener.m_errorCount == 0); +} + TEST_CASE("Semantics drainSemanticsDiff honors scaleFactor when the view " "matches the artboard", "[CommandQueue]")