text modifier length calculation fix

This PR fixes an issue with text modifiers applied to text whose size in bytes (or characters) does not match its size in code points

Diffs=
27ac9fcbb text modifier length calculation fix (#6494)

Co-authored-by: hernan <hernan@rive.app>
diff --git a/.rive_head b/.rive_head
index 5f86ca1..72aa73a 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-f5e458f807056bf2522620aafaa60a048cd38d47
+27ac9fcbbe57e46afa449852b369401b4b579411
diff --git a/include/rive/text/text_value_run.hpp b/include/rive/text/text_value_run.hpp
index 3bdca17..5a61882 100644
--- a/include/rive/text/text_value_run.hpp
+++ b/include/rive/text/text_value_run.hpp
@@ -1,6 +1,7 @@
 #ifndef _RIVE_TEXT_VALUE_RUN_HPP_
 #define _RIVE_TEXT_VALUE_RUN_HPP_
 #include "rive/generated/text/text_value_run_base.hpp"
+#include "rive/text/utf.hpp"
 
 namespace rive
 {
@@ -11,6 +12,22 @@
     StatusCode onAddedClean(CoreContext* context) override;
     StatusCode onAddedDirty(CoreContext* context) override;
     TextStyle* style() { return m_style; }
+    uint32_t length()
+    {
+        if (m_length == -1)
+        {
+
+            const uint8_t* ptr = (const uint8_t*)text().c_str();
+            uint32_t n = 0;
+            while (*ptr)
+            {
+                UTF::NextUTF8(&ptr);
+                n += 1;
+            }
+            m_length = n;
+        }
+        return m_length;
+    }
     uint32_t offset() const;
 
 protected:
@@ -19,6 +36,7 @@
 
 private:
     TextStyle* m_style = nullptr;
+    uint32_t m_length = -1;
 };
 } // namespace rive
 
diff --git a/src/text/text_modifier_range.cpp b/src/text/text_modifier_range.cpp
index 5480d18..a2c0a46 100644
--- a/src/text/text_modifier_range.cpp
+++ b/src/text/text_modifier_range.cpp
@@ -62,7 +62,7 @@
     if (m_run != nullptr)
     {
         start = m_run->offset();
-        end = start + (uint32_t)m_run->text().size();
+        end = start + m_run->length();
     }
     switch (units())
     {
diff --git a/src/text/text_value_run.cpp b/src/text/text_value_run.cpp
index 3a86641..9db0396 100644
--- a/src/text/text_value_run.cpp
+++ b/src/text/text_value_run.cpp
@@ -7,7 +7,11 @@
 
 using namespace rive;
 
-void TextValueRun::textChanged() { parent()->as<Text>()->markShapeDirty(); }
+void TextValueRun::textChanged()
+{
+    m_length = -1;
+    parent()->as<Text>()->markShapeDirty();
+}
 
 StatusCode TextValueRun::onAddedClean(CoreContext* context)
 {
@@ -60,13 +64,13 @@
     Text* text = parent()->as<Text>();
     uint32_t offset = 0;
 
-    for (const TextValueRun* run : text->runs())
+    for (TextValueRun* run : text->runs())
     {
         if (run == this)
         {
             break;
         }
-        offset += (uint32_t)run->text().size();
+        offset += run->length();
     }
     return offset;
 #else
diff --git a/test/assets/test_modifier_run.riv b/test/assets/test_modifier_run.riv
new file mode 100644
index 0000000..deaab5d
--- /dev/null
+++ b/test/assets/test_modifier_run.riv
Binary files differ
diff --git a/test/text_test.cpp b/test/text_test.cpp
index 1afaf93..36e8d6b 100644
--- a/test/text_test.cpp
+++ b/test/text_test.cpp
@@ -221,7 +221,7 @@
         }
         // Run from 4-9 got selected.
         REQUIRE(firstRange->run()->offset() == 4);
-        REQUIRE(firstRange->run()->text().size() == 5);
+        REQUIRE(firstRange->run()->length() == 5);
         for (int i = 4; i < 9; i++)
         {
             REQUIRE(firstModifierGroup->coverage(i) != 0.0f);
@@ -266,6 +266,53 @@
     }
 }
 
+TEST_CASE("run modifier ranges select runs with varying text size", "[text]")
+{
+    auto file = ReadRiveFile("../../test/assets/test_modifier_run.riv");
+    auto artboard = file->artboard();
+
+    artboard->advance(0.0f);
+    rive::NoOpRenderer renderer;
+    artboard->draw(&renderer);
+
+    {
+        /*
+        Full text is:
+        text for first run[UN]line separator[UN]text for second run
+        [UN] is the united nations flag
+        the first run is 18 characters long
+        the second run is 16 characters long
+        the second run is 20 characters long
+        */
+        auto characterSelectedText = artboard->find<rive::Text>("MultiRunText");
+        REQUIRE(characterSelectedText != nullptr);
+        REQUIRE(characterSelectedText->haveModifiers());
+        REQUIRE(characterSelectedText->modifierGroups().size() == 1);
+        auto firstModifierGroup = characterSelectedText->modifierGroups()[0];
+        REQUIRE(firstModifierGroup->ranges().size() == 1);
+        auto firstRange = firstModifierGroup->ranges()[0];
+        REQUIRE(firstRange->run() != nullptr);
+        for (int i = 0; i < 18; i++)
+        {
+            REQUIRE(firstModifierGroup->coverage(i) == 0.0f);
+        }
+        // // Run from 18-33 got selected.
+        REQUIRE(firstRange->run()->offset() == 18);
+        REQUIRE(firstRange->run()->length() == 16);
+        // We confirm that the size and the length of the text are different and they're
+        // both correct
+        REQUIRE(firstRange->run()->text().size() == 22);
+        for (int i = 18; i < 34; i++)
+        {
+            REQUIRE(firstModifierGroup->coverage(i) != 0.0f);
+        }
+        for (int i = 34; i < 54; i++)
+        {
+            REQUIRE(firstModifierGroup->coverage(i) == 0.0f);
+        }
+    }
+}
+
 TEST_CASE("double new line type works", "[text]")
 {
     auto file = ReadRiveFile("../../test/assets/double_line.riv");