Letter spacing!

Still needs a new icon and one consideration around computed width but generally ready for action and can animate!
<img width="927" alt="CleanShot 2023-08-09 at 11 49 55@2x" src="https://github.com/rive-app/rive/assets/454182/d11a3fba-b5a8-4bac-9624-ed7282b293d6">

Diffs=
30351d475 Letter spacing! (#5774)

Co-authored-by: Alex Gibson <agibson.uk@gmail.com>
Co-authored-by: Luigi Rosso <luigi-rosso@users.noreply.github.com>
diff --git a/.rive_head b/.rive_head
index d5eb3a6..bca50bc 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-3ab062dd27b3769005e530e8f3cc9bdfc8a3458e
+30351d475a956cf0fd13dbe26257e057cc14524f
diff --git a/dev/defs/text/text_style.json b/dev/defs/text/text_style.json
index 346731c..c20fba6 100644
--- a/dev/defs/text/text_style.json
+++ b/dev/defs/text/text_style.json
@@ -24,6 +24,15 @@
         "string": "lineheight"
       }
     },
+    "letterSpacing": {
+      "type": "double",
+      "initialValue": "0.0",
+      "animates": true,
+      "key": {
+        "int": 390,
+        "string": "letterspacing"
+      }
+    },
     "fontAssetId": {
       "type": "Id",
       "typeRuntime": "uint",
diff --git a/include/rive/generated/core_registry.hpp b/include/rive/generated/core_registry.hpp
index cac7f25..efc7388 100644
--- a/include/rive/generated/core_registry.hpp
+++ b/include/rive/generated/core_registry.hpp
@@ -981,6 +981,9 @@
             case TextStyleBase::lineHeightPropertyKey:
                 object->as<TextStyleBase>()->lineHeight(value);
                 break;
+            case TextStyleBase::letterSpacingPropertyKey:
+                object->as<TextStyleBase>()->letterSpacing(value);
+                break;
             case TextStyleAxisBase::axisValuePropertyKey:
                 object->as<TextStyleAxisBase>()->axisValue(value);
                 break;
@@ -1531,6 +1534,8 @@
                 return object->as<TextStyleBase>()->fontSize();
             case TextStyleBase::lineHeightPropertyKey:
                 return object->as<TextStyleBase>()->lineHeight();
+            case TextStyleBase::letterSpacingPropertyKey:
+                return object->as<TextStyleBase>()->letterSpacing();
             case TextStyleAxisBase::axisValuePropertyKey:
                 return object->as<TextStyleAxisBase>()->axisValue();
             case TextBase::widthPropertyKey:
@@ -1830,6 +1835,7 @@
             case TextModifierGroupBase::scaleYPropertyKey:
             case TextStyleBase::fontSizePropertyKey:
             case TextStyleBase::lineHeightPropertyKey:
+            case TextStyleBase::letterSpacingPropertyKey:
             case TextStyleAxisBase::axisValuePropertyKey:
             case TextBase::widthPropertyKey:
             case TextBase::heightPropertyKey:
diff --git a/include/rive/generated/text/text_style_base.hpp b/include/rive/generated/text/text_style_base.hpp
index cabef2b..6076d91 100644
--- a/include/rive/generated/text/text_style_base.hpp
+++ b/include/rive/generated/text/text_style_base.hpp
@@ -32,11 +32,13 @@
 
     static const uint16_t fontSizePropertyKey = 274;
     static const uint16_t lineHeightPropertyKey = 370;
+    static const uint16_t letterSpacingPropertyKey = 390;
     static const uint16_t fontAssetIdPropertyKey = 279;
 
 private:
     float m_FontSize = 12.0f;
     float m_LineHeight = -1.0f;
+    float m_LetterSpacing = 0.0f;
     uint32_t m_FontAssetId = -1;
 
 public:
@@ -62,6 +64,17 @@
         lineHeightChanged();
     }
 
+    inline float letterSpacing() const { return m_LetterSpacing; }
+    void letterSpacing(float value)
+    {
+        if (m_LetterSpacing == value)
+        {
+            return;
+        }
+        m_LetterSpacing = value;
+        letterSpacingChanged();
+    }
+
     inline uint32_t fontAssetId() const { return m_FontAssetId; }
     void fontAssetId(uint32_t value)
     {
@@ -78,6 +91,7 @@
     {
         m_FontSize = object.m_FontSize;
         m_LineHeight = object.m_LineHeight;
+        m_LetterSpacing = object.m_LetterSpacing;
         m_FontAssetId = object.m_FontAssetId;
         ContainerComponent::copy(object);
     }
@@ -92,6 +106,9 @@
             case lineHeightPropertyKey:
                 m_LineHeight = CoreDoubleType::deserialize(reader);
                 return true;
+            case letterSpacingPropertyKey:
+                m_LetterSpacing = CoreDoubleType::deserialize(reader);
+                return true;
             case fontAssetIdPropertyKey:
                 m_FontAssetId = CoreUintType::deserialize(reader);
                 return true;
@@ -102,6 +119,7 @@
 protected:
     virtual void fontSizeChanged() {}
     virtual void lineHeightChanged() {}
+    virtual void letterSpacingChanged() {}
     virtual void fontAssetIdChanged() {}
 };
 } // namespace rive
diff --git a/include/rive/text/text.hpp b/include/rive/text/text.hpp
index 05ea78e..95f4278 100644
--- a/include/rive/text/text.hpp
+++ b/include/rive/text/text.hpp
@@ -47,6 +47,7 @@
     void append(rcp<Font> font,
                 float size,
                 float lineHeight,
+                float letterSpacing,
                 const std::string& text,
                 uint16_t styleId);
     const std::vector<Unichar>& unichars() const { return m_value; }
diff --git a/include/rive/text/text_style.hpp b/include/rive/text/text_style.hpp
index a1b6802..3bef063 100644
--- a/include/rive/text/text_style.hpp
+++ b/include/rive/text/text_style.hpp
@@ -43,6 +43,7 @@
 protected:
     void fontSizeChanged() override;
     void lineHeightChanged() override;
+    void letterSpacingChanged() override;
 
 private:
     std::unique_ptr<TextVariationHelper> m_variationHelper;
diff --git a/include/rive/text_engine.hpp b/include/rive/text_engine.hpp
index 9d0da4b..d244f04 100644
--- a/include/rive/text_engine.hpp
+++ b/include/rive/text_engine.hpp
@@ -38,7 +38,7 @@
     center = 2
 };
 
-// A horizontal line of text with a paragraph, after line-breaking.
+// A horizontal line of text within a paragraph, after line-breaking.
 struct GlyphLine
 {
     uint32_t startRunIndex;
@@ -187,6 +187,7 @@
     rcp<Font> font;
     float size;
     float lineHeight;
+    float letterSpacing;
     uint32_t unicharCount;
     uint32_t script;
     uint16_t styleId;
@@ -217,6 +218,7 @@
     rcp<Font> font;
     float size;
     float lineHeight;
+    float letterSpacing;
 
     // List of glyphs, represented by font specific glyph ids. Length is equal to number of glyphs
     // in the run.
diff --git a/src/text/font_hb.cpp b/src/text/font_hb.cpp
index 0e75107..a1af534 100644
--- a/src/text/font_hb.cpp
+++ b/src/text/font_hb.cpp
@@ -345,18 +345,17 @@
     gr.font = tr.font;
     gr.size = tr.size;
     gr.lineHeight = tr.lineHeight;
+    gr.letterSpacing = tr.letterSpacing;
     gr.styleId = tr.styleId;
     gr.dir = tr.dir;
 
     const float scale = tr.size / kStdScale;
     for (unsigned int i = 0; i < glyph_count; i++)
     {
-        //            hb_position_t x_offset  = glyph_pos[i].x_offset;
-        //            hb_position_t y_offset  = glyph_pos[i].y_offset;
         unsigned int index = tr.dir == rive::TextDirection::rtl ? glyph_count - 1 - i : i;
         gr.glyphs[i] = (uint16_t)glyph_info[index].codepoint;
         gr.textIndices[i] = textOffset + glyph_info[index].cluster;
-        gr.advances[i] = gr.xpos[i] = glyph_pos[index].x_advance * scale;
+        gr.advances[i] = gr.xpos[i] = glyph_pos[index].x_advance * scale + tr.letterSpacing;
         gr.offsets[i] =
             rive::Vec2D(glyph_pos[index].x_offset * scale, -glyph_pos[index].y_offset * scale);
     }
@@ -376,6 +375,7 @@
     subset.font = std::move(orig.font);
     subset.size = orig.size;
     subset.lineHeight = orig.lineHeight;
+    subset.letterSpacing = orig.letterSpacing;
     subset.dir = orig.dir;
     subset.xpos.back() = 0; // since we're now the end of a run
     subset.styleId = orig.styleId;
@@ -408,6 +408,7 @@
                 fallbackFont,
                 orig.size,
                 orig.lineHeight,
+                origTextRun.letterSpacing,
                 textCount,
                 origTextRun.script,
                 orig.styleId,
@@ -477,6 +478,7 @@
                 tr.font,
                 tr.size,
                 tr.lineHeight,
+                tr.letterSpacing,
                 tr.unicharCount - runTextIndex,
                 (uint32_t)lastScript,
                 tr.styleId,
@@ -514,6 +516,7 @@
                         back.font,
                         back.size,
                         back.lineHeight,
+                        back.letterSpacing,
                         tr.unicharCount - runTextIndex,
                         (uint32_t)script,
                         back.styleId,
diff --git a/src/text/line_breaker.cpp b/src/text/line_breaker.cpp
index a3be8ef..298c3e7 100644
--- a/src/text/line_breaker.cpp
+++ b/src/text/line_breaker.cpp
@@ -16,7 +16,8 @@
     {
         maxLineWidth = std::max(maxLineWidth,
                                 runs[line.endRunIndex].xpos[line.endGlyphIndex] -
-                                    runs[line.startRunIndex].xpos[line.startGlyphIndex]);
+                                    runs[line.startRunIndex].xpos[line.startGlyphIndex] -
+                                    runs[line.endRunIndex].letterSpacing);
     }
     return maxLineWidth;
 }
@@ -84,7 +85,8 @@
         line.bottom = Y;
 
         auto lineWidth = runs[line.endRunIndex].xpos[line.endGlyphIndex] -
-                         runs[line.startRunIndex].xpos[line.startGlyphIndex];
+                         runs[line.startRunIndex].xpos[line.startGlyphIndex] -
+                         runs[line.endRunIndex].letterSpacing;
         switch (align)
         {
             case TextAlign::right:
diff --git a/src/text/text.cpp b/src/text/text.cpp
index c0cfafe..b2bb540 100644
--- a/src/text/text.cpp
+++ b/src/text/text.cpp
@@ -169,6 +169,7 @@
             TextRun ellipsisRuns[] = {{ellipsisFont,
                                        ellipsisFontSize,
                                        run.lineHeight,
+                                       run.letterSpacing,
                                        (uint32_t)ellipsisCodePoints.size()}};
             auto nextEllipsisShape =
                 ellipsisFont->shapeText(ellipsisCodePoints, Span<TextRun>(ellipsisRuns, 1));
@@ -264,7 +265,8 @@
         {
             const GlyphRun& endRun = paragraph.runs[line.endRunIndex];
             const GlyphRun& startRun = paragraph.runs[line.startRunIndex];
-            float width = endRun.xpos[line.endGlyphIndex] - startRun.xpos[line.startGlyphIndex];
+            float width = endRun.xpos[line.endGlyphIndex] - startRun.xpos[line.startGlyphIndex] -
+                          endRun.letterSpacing;
             if (width > maxWidth)
             {
                 maxWidth = width;
@@ -544,6 +546,7 @@
 void StyledText::append(rcp<Font> font,
                         float size,
                         float lineHeight,
+                        float letterSpacing,
                         const std::string& text,
                         uint16_t styleId)
 {
@@ -554,7 +557,7 @@
         m_value.push_back(UTF::NextUTF8(&ptr));
         n += 1;
     }
-    m_runs.push_back({std::move(font), size, lineHeight, n, 0, styleId});
+    m_runs.push_back({std::move(font), size, lineHeight, letterSpacing, n, 0, styleId});
 }
 
 bool Text::makeStyled(StyledText& styledText, bool withModifiers) const
@@ -570,7 +573,12 @@
             runIndex++;
             continue;
         }
-        styledText.append(style->font(), style->fontSize(), style->lineHeight(), text, runIndex++);
+        styledText.append(style->font(),
+                          style->fontSize(),
+                          style->lineHeight(),
+                          style->letterSpacing(),
+                          text,
+                          runIndex++);
     }
     if (withModifiers)
     {
diff --git a/src/text/text_modifier_group.cpp b/src/text/text_modifier_group.cpp
index 44f3859..3569ca2 100644
--- a/src/text/text_modifier_group.cpp
+++ b/src/text/text_modifier_group.cpp
@@ -174,6 +174,7 @@
         source.font,
         source.size,
         source.lineHeight,
+        source.letterSpacing,
         unicharCount,
         source.script,
         source.styleId,
@@ -210,6 +211,7 @@
             m_variableFont,
             run.size,
             run.lineHeight,
+            run.letterSpacing,
             run.unicharCount,
             run.script,
             run.styleId,
diff --git a/src/text/text_style.cpp b/src/text/text_style.cpp
index 45439ec..21e5b65 100644
--- a/src/text/text_style.cpp
+++ b/src/text/text_style.cpp
@@ -212,6 +212,8 @@
 
 void TextStyle::lineHeightChanged() { parent()->as<Text>()->markShapeDirty(); }
 
+void TextStyle::letterSpacingChanged() { parent()->as<Text>()->markShapeDirty(); }
+
 Core* TextStyle::clone() const
 {
     TextStyle* twin = TextStyleBase::clone()->as<TextStyle>();
diff --git a/test/font_test.cpp b/test/font_test.cpp
index fed4fb8..2bdfce0 100644
--- a/test/font_test.cpp
+++ b/test/font_test.cpp
@@ -19,7 +19,7 @@
         unichars->push_back(rive::UTF::NextUTF8(&ptr));
         n += 1;
     }
-    return {std::move(font), size, -1.0f, n, 0};
+    return {std::move(font), size, -1.0f, 0.0f, n, 0};
 }
 
 static rcp<Font> loadFont(const char* filename)
diff --git a/test/line_break_test.cpp b/test/line_break_test.cpp
index e993596..34dad5b 100644
--- a/test/line_break_test.cpp
+++ b/test/line_break_test.cpp
@@ -22,7 +22,7 @@
         unichars->push_back(rive::UTF::NextUTF8(&ptr));
         n += 1;
     }
-    return {std::move(font), size, -1.0f, n, 0};
+    return {std::move(font), size, -1.0f, 0.0f, n, 0};
 }
 
 static rcp<Font> loadFont(const char* filename)
diff --git a/viewer/src/viewer_content/text_content.cpp b/viewer/src/viewer_content/text_content.cpp
index 29470ff..81f2637 100644
--- a/viewer/src/viewer_content/text_content.cpp
+++ b/viewer/src/viewer_content/text_content.cpp
@@ -147,7 +147,7 @@
         unichars->push_back(rive::UTF::NextUTF8(&ptr));
         n += 1;
     }
-    return {std::move(font), size, lineHeight, n};
+    return {std::move(font), size, lineHeight, 0.0f, n};
 }
 
 class TextContent : public ViewerContent