Bidi Text Support

Adding support for bidirectional text using SheenBidi to break styled runs into directional runs. This also needs to introduce the "baseDirection" of a paragraph (and the concept of a paragraph) in order to properly flow the runs after shaping.

Diffs=
291a3a02b Bidi Text Support (#4282)
diff --git a/.rive_head b/.rive_head
index ec72cc2..2cc4a4c 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-0ffa502c470709177f941dca669e3a9cfb9612bf
+291a3a02bce12a2c92a456f6a71ad3a410162f0f
diff --git a/build.sh b/build.sh
index 1534111..f6430b7 100755
--- a/build.sh
+++ b/build.sh
@@ -1,6 +1,7 @@
 #!/bin/bash
-set -e 
+set -e
 
+source dependencies/config_directories.sh
 pushd build &>/dev/null
 
 while getopts p: flag; do
diff --git a/build/dependency.lua b/build/dependency.lua
new file mode 100644
index 0000000..bef9145
--- /dev/null
+++ b/build/dependency.lua
@@ -0,0 +1,49 @@
+local m = {}
+
+local last_str = ''
+
+function iop(str)
+    io.write(('\b \b'):rep(#last_str)) -- erase old line
+    io.write(str) -- write new line
+    io.flush()
+    last_str = str
+end
+
+function m.github(project, tag)
+    local dependencies = os.getenv('DEPENDENCIES')
+    if dependencies == nil then
+        dependencies = path.getabsolute(_WORKING_DIR) .. '/dependencies'
+        os.mkdir(dependencies)
+    end
+    local hash = string.sha1(project .. tag)
+    if not os.isdir(dependencies .. '/' .. hash) then
+        function progress(total, current)
+            local ratio = current / total
+            ratio = math.min(math.max(ratio, 0), 1)
+            local percent = math.floor(ratio * 100)
+            if total == current then
+                iop('')
+            else
+                iop('Downloading ' .. project .. ' ' .. percent .. '%')
+            end
+        end
+
+        local downloadFilename = dependencies .. '/' .. hash .. '.zip'
+        http.download(
+            'https://github.com/' .. project .. '/archive/' .. tag .. '.zip',
+            downloadFilename,
+            {progress = progress}
+        )
+        print('Downloaded ' .. project .. '.')
+        zip.extract(downloadFilename, dependencies .. '/' .. hash)
+        os.remove(downloadFilename)
+    end
+    local dirs = os.matchdirs(dependencies .. '/' .. hash .. '/*')
+
+    local iter = pairs(dirs)
+    local currentKey, currentValue = iter(dirs)
+    print('Dependency ' .. project .. ' located at:')
+    print('  ' .. currentValue)
+    return currentValue
+end
+return m
diff --git a/build/premake5.lua b/build/premake5.lua
index 9cafaf2..507d7c2 100644
--- a/build/premake5.lua
+++ b/build/premake5.lua
@@ -4,11 +4,14 @@
 do
     defines {'WITH_RIVE_TOOLS'}
 end
-filter {'options:with_rive_tools'}
+filter {'options:with_rive_text'}
 do
-    defines {'WITH_RIVE_TOOLS'}
+    defines {'WITH_RIVE_TEXT'}
 end
 
+dofile(path.join(path.getabsolute('../dependencies/'), 'premake5_harfbuzz.lua'))
+dofile(path.join(path.getabsolute('../dependencies/'), 'premake5_sheenbidi.lua'))
+
 WINDOWS_CLANG_CL_SUPPRESSED_WARNINGS = {
     '-Wno-c++98-compat',
     '-Wno-c++98-compat-pedantic',
@@ -28,7 +31,9 @@
     '-Wno-sign-compare',
     '-Wno-sign-conversion',
     '-Wno-unused-macros',
-    '-Wno-unused-parameter'
+    '-Wno-unused-parameter',
+    '-Wno-switch-enum',
+    '-Wno-missing-field-initializers'
 }
 
 project 'rive'
@@ -39,7 +44,11 @@
     toolset 'clang'
     targetdir '%{cfg.system}/bin/%{cfg.buildcfg}'
     objdir '%{cfg.system}/obj/%{cfg.buildcfg}'
-    includedirs {'../include'}
+    includedirs {
+        '../include',
+        harfbuzz .. '/src',
+        sheenbidi .. '/Headers'
+    }
 
     files {'../src/**.cpp'}
 
diff --git a/dependencies/linux/get_harfbuzz.sh b/dependencies/linux/get_harfbuzz.sh
deleted file mode 120000
index 994888b..0000000
--- a/dependencies/linux/get_harfbuzz.sh
+++ /dev/null
@@ -1 +0,0 @@
-../macosx/get_harfbuzz.sh
\ No newline at end of file
diff --git a/dependencies/macosx/get_harfbuzz.sh b/dependencies/macosx/get_harfbuzz.sh
deleted file mode 100755
index 6ad6b12..0000000
--- a/dependencies/macosx/get_harfbuzz.sh
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/bin/bash
-
-# if you're looking at this in dependencies/linux, please note it's a symbolic
-# link to dependencies/macosx/get_harfbuzz.sh if changes need to be made here we
-# should let these two scripts diverge.
-set -e
-
-if [ -z "${DEPENDENCIES}" ]; then
-    echo "DEPENDENCIES env variable must be set. This script is usually called by other scripts."
-    exit 1
-fi
-pushd $DEPENDENCIES
-
-if [ ! -d harfbuzz ]; then
-    echo "Cloning Harfbuzz."
-    git clone https://github.com/harfbuzz/harfbuzz
-    cd harfbuzz
-    git checkout 858570b1d9912a1b746ab39fbe62a646c4f7a5b1 .
-fi
diff --git a/dependencies/premake5_harfbuzz.lua b/dependencies/premake5_harfbuzz.lua
index 9c31248..3fefe81 100644
--- a/dependencies/premake5_harfbuzz.lua
+++ b/dependencies/premake5_harfbuzz.lua
@@ -1,6 +1,5 @@
-dependencies = os.getenv('DEPENDENCIES')
-
-harfbuzz = dependencies .. '/harfbuzz'
+local dependency = require 'dependency'
+harfbuzz = dependency.github('harfbuzz/harfbuzz', '858570b1d9912a1b746ab39fbe62a646c4f7a5b1')
 
 workspace 'rive'
 configurations {'debug', 'release'}
diff --git a/dependencies/premake5_sheenbidi.lua b/dependencies/premake5_sheenbidi.lua
new file mode 100644
index 0000000..bebb582
--- /dev/null
+++ b/dependencies/premake5_sheenbidi.lua
@@ -0,0 +1,116 @@
+local dependency = require 'dependency'
+sheenbidi = dependency.github('Tehreer/SheenBidi', 'v2.6')
+
+workspace 'rive'
+configurations {'debug', 'release'}
+
+project 'rive_sheenbidi'
+do
+    kind 'StaticLib'
+    language 'C'
+    toolset 'clang'
+    targetdir '%{cfg.system}/cache/bin/%{cfg.buildcfg}/'
+    objdir '%{cfg.system}/cache/obj/%{cfg.buildcfg}/'
+
+    includedirs {
+        sheenbidi .. '/Headers'
+    }
+
+    filter 'configurations:debug'
+    do
+        files {
+            sheenbidi .. '/Source/BidiChain.c',
+            sheenbidi .. '/Source/BidiTypeLookup.c',
+            sheenbidi .. '/Source/BracketQueue.c',
+            sheenbidi .. '/Source/GeneralCategoryLookup.c',
+            sheenbidi .. '/Source/IsolatingRun.c',
+            sheenbidi .. '/Source/LevelRun.c',
+            sheenbidi .. '/Source/PairingLookup.c',
+            sheenbidi .. '/Source/RunQueue.c',
+            sheenbidi .. '/Source/SBAlgorithm.c',
+            sheenbidi .. '/Source/SBBase.c',
+            sheenbidi .. '/Source/SBCodepointSequence.c',
+            sheenbidi .. '/Source/SBLine.c',
+            sheenbidi .. '/Source/SBLog.c',
+            sheenbidi .. '/Source/SBMirrorLocator.c',
+            sheenbidi .. '/Source/SBParagraph.c',
+            sheenbidi .. '/Source/SBScriptLocator.c',
+            sheenbidi .. '/Source/ScriptLookup.c',
+            sheenbidi .. '/Source/ScriptStack.c',
+            sheenbidi .. '/Source/StatusStack.c'
+        }
+    end
+    filter 'configurations:release'
+    do
+        files {
+            sheenbidi .. '/Source/SheenBidi.c'
+        }
+    end
+
+    buildoptions {
+        '-Wall',
+        '-ansi',
+        '-pedantic'
+    }
+
+    linkoptions {'-r'}
+
+    filter 'configurations:debug'
+    do
+        buildoptions {'-g', '-O0'}
+        defines {'DEBUG'}
+        symbols 'On'
+    end
+
+    filter 'configurations:release'
+    do
+        buildoptions {'-Oz'}
+        defines {'RELEASE', 'NDEBUG', 'SB_CONFIG_UNITY'}
+        optimize 'On'
+    end
+
+    filter 'system:windows'
+    do
+        removebuildoptions {
+            -- vs clang doesn't recognize these on windows
+            '-fno-exceptions',
+            '-fno-rtti'
+        }
+        architecture 'x64'
+        buildoptions {
+            '-Wno-c++98-compat',
+            '-Wno-c++98-compat-pedantic',
+            '-Wno-c99-extensions',
+            '-Wno-ctad-maybe-unsupported',
+            '-Wno-deprecated-copy-with-user-provided-dtor',
+            '-Wno-deprecated-declarations',
+            '-Wno-documentation',
+            '-Wno-documentation-pedantic',
+            '-Wno-documentation-unknown-command',
+            '-Wno-double-promotion',
+            '-Wno-exit-time-destructors',
+            '-Wno-float-equal',
+            '-Wno-global-constructors',
+            '-Wno-implicit-float-conversion',
+            '-Wno-newline-eof',
+            '-Wno-old-style-cast',
+            '-Wno-reserved-identifier',
+            '-Wno-shadow',
+            '-Wno-sign-compare',
+            '-Wno-sign-conversion',
+            '-Wno-unused-macros',
+            '-Wno-unused-parameter',
+            '-Wno-used-but-marked-unused',
+            '-Wno-cast-qual',
+            '-Wno-unused-template',
+            '-Wno-zero-as-null-pointer-constant',
+            '-Wno-extra-semi',
+            '-Wno-undef',
+            '-Wno-comma',
+            '-Wno-nonportable-system-include-path',
+            '-Wno-covered-switch-default',
+            '-Wno-microsoft-enum-value',
+            '-Wno-deprecated-declarations'
+        }
+    end
+end
diff --git a/dependencies/windows/get_harfbuzz.bat b/dependencies/windows/get_harfbuzz.bat
deleted file mode 100644
index e225413..0000000
--- a/dependencies/windows/get_harfbuzz.bat
+++ /dev/null
@@ -1,11 +0,0 @@
-@echo off
-pushd %DEPENDENCIES%
-@echo off
-if not exist ".\harfbuzz" (
-    echo "Cloning Harfbuzz."
-    git clone https://github.com/harfbuzz/harfbuzz
-    pushd harfbuzz
-    git checkout 858570b1d9912a1b746ab39fbe62a646c4f7a5b1 .
-    popd
-)
-popd
\ No newline at end of file
diff --git a/dev/test.bat b/dev/test.bat
index a238f75..3846a44 100644
--- a/dev/test.bat
+++ b/dev/test.bat
@@ -7,15 +7,9 @@
     popd
 )
 
-if not exist "%DEPENDENCIES%\harfbuzz\" (
-    pushd "%DEPENDENCIES_SCRIPTS%"
-    call .\get_harfbuzz.bat || goto :error
-    popd
-)
-
 set "PREMAKE=%DEPENDENCIES%\bin\premake5.exe"
 pushd test
-%PREMAKE% vs2022
+%PREMAKE% --scripts=..\..\build vs2022
 
 MSBuild.exe /?  2> NUL
 if not %ERRORLEVEL%==9009 (
diff --git a/dev/test.sh b/dev/test.sh
index 3a0ea33..2707558 100755
--- a/dev/test.sh
+++ b/dev/test.sh
@@ -8,19 +8,13 @@
 OPTION=$1
 UTILITY=
 
-if [[ ! -d "$DEPENDENCIES/harfbuzz" ]]; then
-  pushd $DEPENDENCIES_SCRIPTS
-  ./get_harfbuzz.sh
-  popd
-fi
-
 if [ "$OPTION" = "help" ]; then
   echo test.sh - run the tests
   echo test.sh clean - clean and run the tests
   exit
 elif [ "$OPTION" = "clean" ]; then
   echo Cleaning project ...
-  premake5 clean || exit 1
+  premake5 --scripts=../../build clean || exit 1
   shift
 elif [ "$OPTION" = "memory" ]; then
   echo Will perform memory checks...
@@ -32,7 +26,7 @@
   shift
 fi
 
-premake5 gmake2 || exit 1
+premake5 --scripts=../../build gmake2 || exit 1
 make -j7 || exit 1
 
 for file in ./build/bin/debug/*; do
diff --git a/dev/test/premake5.lua b/dev/test/premake5.lua
index 07a3d35..501c4f6 100644
--- a/dev/test/premake5.lua
+++ b/dev/test/premake5.lua
@@ -16,8 +16,8 @@
 workspace 'rive'
 configurations {'debug'}
 
-dependencies = os.getenv('DEPENDENCIES')
-dofile(path.join(path.getabsolute(dependencies) .. '/../..', 'premake5_harfbuzz.lua'))
+dofile(path.join(path.getabsolute('../../dependencies/'), 'premake5_harfbuzz.lua'))
+dofile(path.join(path.getabsolute('../../dependencies/'), 'premake5_sheenbidi.lua'))
 
 project('tests')
 do
@@ -30,9 +30,15 @@
 
     buildoptions {'-Wall', '-fno-exceptions', '-fno-rtti'}
 
-    includedirs {'./include', '../../include', dependencies .. '/harfbuzz/src'}
+    includedirs {
+        './include',
+        '../../include',
+        harfbuzz .. '/src',
+        sheenbidi .. '/Headers'
+    }
     links {
-        'rive_harfbuzz'
+        'rive_harfbuzz',
+        'rive_sheenbidi'
     }
 
     files {
@@ -86,7 +92,9 @@
             '-Wno-unused-macros',
             '-Wno-unused-parameter',
             '-Wno-four-char-constants',
-            '-Wno-unreachable-code'
+            '-Wno-unreachable-code',
+            '-Wno-switch-enum',
+            '-Wno-missing-field-initializers'
         }
     end
 end
diff --git a/include/rive/factory.hpp b/include/rive/factory.hpp
index fd0232d..6d69bad 100644
--- a/include/rive/factory.hpp
+++ b/include/rive/factory.hpp
@@ -6,7 +6,7 @@
 #define _RIVE_FACTORY_HPP_
 
 #include "rive/renderer.hpp"
-#include "rive/render_text.hpp"
+#include "rive/text.hpp"
 #include "rive/refcnt.hpp"
 #include "rive/span.hpp"
 #include "rive/math/aabb.hpp"
@@ -58,7 +58,7 @@
 
     virtual std::unique_ptr<RenderImage> decodeImage(Span<const uint8_t>) = 0;
 
-    virtual rcp<RenderFont> decodeFont(Span<const uint8_t>) { return nullptr; }
+    virtual rcp<Font> decodeFont(Span<const uint8_t>) { return nullptr; }
 
     // Non-virtual helpers
 
diff --git a/include/rive/render_text.hpp b/include/rive/render_text.hpp
deleted file mode 100644
index d7827ed..0000000
--- a/include/rive/render_text.hpp
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * Copyright 2022 Rive
- */
-
-#ifndef _RIVE_RENDER_TEXT_HPP_
-#define _RIVE_RENDER_TEXT_HPP_
-
-#include "rive/math/raw_path.hpp"
-#include "rive/refcnt.hpp"
-#include "rive/span.hpp"
-#include "rive/simple_array.hpp"
-
-namespace rive
-{
-
-using Unichar = uint32_t;
-using GlyphID = uint16_t;
-
-struct RenderTextRun;
-struct RenderGlyphRun;
-
-class RenderFont : public RefCnt<RenderFont>
-{
-public:
-    virtual ~RenderFont() {}
-
-    struct LineMetrics
-    {
-        float ascent, descent;
-    };
-
-    const LineMetrics& lineMetrics() const { return m_LineMetrics; }
-
-    // This is experimental
-    // -- may only be needed by Editor
-    // -- so it may be removed from here later
-    //
-    struct Axis
-    {
-        uint32_t tag;
-        float min;
-        float def; // default value
-        float max;
-    };
-
-    // Returns the canonical set of Axes for this font. Use this to know
-    // what variations are possible. If you want to know the specific
-    // coordinate within that variations space for *this* font, call
-    // getCoords().
-    //
-    virtual std::vector<Axis> getAxes() const = 0;
-
-    struct Coord
-    {
-        uint32_t axis;
-        float value;
-    };
-
-    // Returns the specific coords in variation space for this font.
-    // If you want to have a description of the entire variation space,
-    // call getAxes().
-    //
-    virtual std::vector<Coord> getCoords() const = 0;
-
-    virtual rcp<RenderFont> makeAtCoords(Span<const Coord>) const = 0;
-
-    rcp<RenderFont> makeAtCoord(Coord c) { return this->makeAtCoords(Span<const Coord>(&c, 1)); }
-
-    // Returns a 1-point path for this glyph. It will be positioned
-    // relative to (0,0) with the typographic baseline at y = 0.
-    //
-    virtual RawPath getPath(GlyphID) const = 0;
-
-    rive::SimpleArray<RenderGlyphRun> shapeText(rive::Span<const rive::Unichar> text,
-                                                rive::Span<const rive::RenderTextRun> runs) const;
-
-protected:
-    RenderFont(const LineMetrics& lm) : m_LineMetrics(lm) {}
-
-    virtual rive::SimpleArray<RenderGlyphRun>
-    onShapeText(rive::Span<const rive::Unichar> text,
-                rive::Span<const rive::RenderTextRun> runs) const = 0;
-
-private:
-    const LineMetrics m_LineMetrics;
-};
-
-struct RenderTextRun
-{
-    rcp<RenderFont> font;
-    float size;
-    uint32_t unicharCount;
-};
-
-struct RenderGlyphRun
-{
-    RenderGlyphRun(size_t glyphCount = 0) :
-        glyphs(glyphCount), textIndices(glyphCount), xpos(glyphCount + 1)
-    {}
-
-    RenderGlyphRun(rive::SimpleArray<GlyphID> glyphIds,
-                   rive::SimpleArray<uint32_t> offsets,
-                   rive::SimpleArray<float> xs) :
-        glyphs(glyphIds), textIndices(offsets), xpos(xs)
-    {}
-
-    rcp<RenderFont> font;
-    float size;
-    // List of glyphs, represented by font specific glyph ids. Length is equal to number of glyphs
-    // in the run.
-    rive::SimpleArray<GlyphID> glyphs;
-
-    // Index in the unicode text array representing the text displayed in this run. Because each
-    // glyph can be composed of multiple unicode values, this index points to the first index in the
-    // unicode text. Length is equal to number of glyphs in the run.
-    rive::SimpleArray<uint32_t> textIndices;
-
-    // X position of each glyph, with an extra value at the end for the right most extent of the
-    // last glyph.
-    rive::SimpleArray<float> xpos;
-
-    // List of possible indices to line break at. Has a stride of 2 uint32_ts where each pair marks
-    // the start and end of a word, with the exception of a return character (forced linebreak)
-    // which is represented as a 0 length word (where start/end index is the same).
-    rive::SimpleArray<uint32_t> breaks;
-};
-
-} // namespace rive
-#endif
diff --git a/include/rive/text.hpp b/include/rive/text.hpp
new file mode 100644
index 0000000..13aa7cd
--- /dev/null
+++ b/include/rive/text.hpp
@@ -0,0 +1,212 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#ifndef _RIVE_TEXT_HPP_
+#define _RIVE_TEXT_HPP_
+
+#include "rive/math/raw_path.hpp"
+#include "rive/refcnt.hpp"
+#include "rive/span.hpp"
+#include "rive/simple_array.hpp"
+
+namespace rive
+{
+
+// Representation of a single unicode codepoint.
+using Unichar = uint32_t;
+// Id for a glyph within a font.
+using GlyphID = uint16_t;
+
+struct TextRun;
+struct GlyphRun;
+
+// Direction a paragraph or run flows in.
+enum class TextDirection : uint8_t
+{
+    ltr = 0,
+    rtl = 1
+};
+
+// The alignment of each word wrapped line in a paragraph.
+enum class TextAlign : uint8_t
+{
+    left = 0,
+    right = 1,
+    center = 2
+};
+
+// A horizontal line of text with a paragraph, after line-breaking.
+struct GlyphLine
+{
+    uint32_t startRunIndex;
+    uint32_t startGlyphIndex;
+    uint32_t endRunIndex;
+    uint32_t endGlyphIndex;
+    float startX;
+    float top = 0, baseline = 0, bottom = 0;
+
+    bool operator==(const GlyphLine& o) const
+    {
+        return startRunIndex == o.startRunIndex && startGlyphIndex == o.startGlyphIndex &&
+               endRunIndex == o.endRunIndex && endGlyphIndex == o.endGlyphIndex;
+    }
+
+    GlyphLine() :
+        startRunIndex(0), startGlyphIndex(0), endRunIndex(0), endGlyphIndex(0), startX(0.0f)
+    {}
+    GlyphLine(uint32_t run, uint32_t index) :
+        startRunIndex(run),
+        startGlyphIndex(index),
+        endRunIndex(run),
+        endGlyphIndex(index),
+        startX(0.0f)
+    {}
+
+    bool empty() const { return startRunIndex == endRunIndex && startGlyphIndex == endGlyphIndex; }
+
+    static SimpleArray<GlyphLine> BreakLines(Span<const GlyphRun> runs, float width);
+
+    // Compute values for top/baseline/bottom per line
+    static void
+    ComputeLineSpacing(Span<GlyphLine>, Span<const GlyphRun>, float width, TextAlign align);
+
+    static float ComputeMaxWidth(Span<GlyphLine> lines, Span<const GlyphRun> runs);
+};
+
+// A paragraph represents of set of runs that flow in a specific direction. The
+// runs are always provided in LTR and must be drawn in reverse when the
+// baseDirection is RTL. These are built by the system during shaping where the
+// user provided string and text styling is converted to shaped paragraphs.
+struct Paragraph
+{
+    SimpleArray<GlyphRun> runs;
+    TextDirection baseDirection;
+};
+
+// An abstraction for interfacing with an individual font.
+class Font : public RefCnt<Font>
+{
+public:
+    virtual ~Font() {}
+
+    struct LineMetrics
+    {
+        float ascent, descent;
+    };
+
+    const LineMetrics& lineMetrics() const { return m_LineMetrics; }
+
+    // This is experimental
+    // -- may only be needed by Editor
+    // -- so it may be removed from here later
+    //
+    struct Axis
+    {
+        uint32_t tag;
+        float min;
+        float def; // default value
+        float max;
+    };
+
+    // Returns the canonical set of Axes for this font. Use this to know
+    // what variations are possible. If you want to know the specific
+    // coordinate within that variations space for *this* font, call
+    // getCoords().
+    //
+    virtual std::vector<Axis> getAxes() const = 0;
+
+    struct Coord
+    {
+        uint32_t axis;
+        float value;
+    };
+
+    // Returns the specific coords in variation space for this font.
+    // If you want to have a description of the entire variation space,
+    // call getAxes().
+    //
+    virtual std::vector<Coord> getCoords() const = 0;
+
+    virtual rcp<Font> makeAtCoords(Span<const Coord>) const = 0;
+
+    rcp<Font> makeAtCoord(Coord c) { return this->makeAtCoords(Span<const Coord>(&c, 1)); }
+
+    // Returns a 1-point path for this glyph. It will be positioned
+    // relative to (0,0) with the typographic baseline at y = 0.
+    //
+    virtual RawPath getPath(GlyphID) const = 0;
+
+    SimpleArray<Paragraph> shapeText(Span<const Unichar> text, Span<const TextRun> runs) const;
+
+protected:
+    Font(const LineMetrics& lm) : m_LineMetrics(lm) {}
+
+    virtual SimpleArray<Paragraph> onShapeText(Span<const Unichar> text,
+                                               Span<const TextRun> runs) const = 0;
+
+private:
+    const LineMetrics m_LineMetrics;
+};
+
+// A user defined styling guide for a set of unicode codepoints within a larger text string.
+struct TextRun
+{
+    rcp<Font> font;
+    float size;
+    uint32_t unicharCount;
+    uint32_t script;
+    uint16_t styleId;
+    TextDirection dir;
+};
+
+// The corresponding system generated run for the user provided TextRuns. GlyphRuns may not match
+// TextRuns if the system needs to split the run (for fallback fonts) or if codepoints get
+// ligated/shaped to a single glyph.
+struct GlyphRun
+{
+    GlyphRun(size_t glyphCount = 0) :
+        glyphs(glyphCount), textIndices(glyphCount), advances(glyphCount), xpos(glyphCount + 1)
+    {}
+
+    GlyphRun(SimpleArray<GlyphID> glyphIds,
+             SimpleArray<uint32_t> offsets,
+             SimpleArray<float> ws,
+             SimpleArray<float> xs) :
+        glyphs(glyphIds), textIndices(offsets), advances(ws), xpos(xs)
+    {}
+
+    rcp<Font> font;
+    float size;
+
+    // List of glyphs, represented by font specific glyph ids. Length is equal to number of glyphs
+    // in the run.
+    SimpleArray<GlyphID> glyphs;
+
+    // Index in the unicode text array representing the text displayed in this run. Because each
+    // glyph can be composed of multiple unicode values, this index points to the first index in the
+    // unicode text. Length is equal to number of glyphs in the run.
+    SimpleArray<uint32_t> textIndices;
+
+    // X position of each glyph in visual order (xpos is in logical/memory order).
+    SimpleArray<float> advances;
+
+    // X position of each glyph, with an extra value at the end for the right most extent of the
+    // last glyph.
+    SimpleArray<float> xpos;
+
+    // List of possible indices to line break at. Has a stride of 2 uint32_ts where each pair marks
+    // the start and end of a word, with the exception of a return character (forced linebreak)
+    // which is represented as a 0 length word (where start/end index is the same).
+    SimpleArray<uint32_t> breaks;
+
+    // The unique identifier for the styling (fill/stroke colors, anything not determined by the
+    // font or font size) applied to this run.
+    uint16_t styleId;
+
+    // The text direction (LTR = 0/RTL = 1)
+    TextDirection dir;
+};
+
+} // namespace rive
+#endif
diff --git a/include/rive/text/font_hb.hpp b/include/rive/text/font_hb.hpp
new file mode 100644
index 0000000..d4c290f
--- /dev/null
+++ b/include/rive/text/font_hb.hpp
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#ifndef _RIVE_FONT_HB_HPP_
+#define _RIVE_FONT_HB_HPP_
+
+#include "rive/factory.hpp"
+#include "rive/text.hpp"
+
+struct hb_font_t;
+struct hb_draw_funcs_t;
+
+class HBFont : public rive::Font
+{
+    hb_draw_funcs_t* m_DrawFuncs;
+
+public:
+    hb_font_t* m_Font;
+
+    // We assume ownership of font!
+    HBFont(hb_font_t* font);
+    ~HBFont() override;
+
+    std::vector<Axis> getAxes() const override;
+    std::vector<Coord> getCoords() const override;
+    rive::rcp<rive::Font> makeAtCoords(rive::Span<const Coord>) const override;
+    rive::RawPath getPath(rive::GlyphID) const override;
+    rive::SimpleArray<rive::Paragraph> onShapeText(rive::Span<const rive::Unichar>,
+                                                   rive::Span<const rive::TextRun>) const override;
+
+    static rive::rcp<rive::Font> Decode(rive::Span<const uint8_t>);
+
+    // If the platform can supply fallback font(s), set this function pointer.
+    // It will be called with a span of unichars, and the platform attempts to
+    // return a font that can draw (at least some of) them. If no font is available
+    // just return nullptr.
+
+    using FallbackProc = rive::rcp<rive::Font> (*)(rive::Span<const rive::Unichar>);
+
+    static FallbackProc gFallbackProc;
+};
+
+#endif
diff --git a/include/rive/text/line_breaker.hpp b/include/rive/text/line_breaker.hpp
deleted file mode 100644
index 827873d..0000000
--- a/include/rive/text/line_breaker.hpp
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright 2022 Rive
- */
-
-#ifndef _RIVE_RENDER_GLYPH_LINE_H_
-#define _RIVE_RENDER_GLYPH_LINE_H_
-
-#include "rive/render_text.hpp"
-
-namespace rive
-{
-
-enum class RenderTextAlign : uint8_t
-{
-    left = 0,
-    right = 1,
-    center = 2
-};
-
-struct RenderGlyphLine
-{
-    uint32_t startRun;
-    uint32_t startIndex;
-    uint32_t endRun;
-    uint32_t endIndex;
-    float startX;
-    float top = 0, baseline = 0, bottom = 0;
-
-    bool operator==(const RenderGlyphLine& o) const
-    {
-        return startRun == o.startRun && startIndex == o.startIndex && endRun == o.endRun &&
-               endIndex == o.endIndex;
-    }
-
-    RenderGlyphLine() : startRun(0), startIndex(0), endRun(0), endIndex(0), startX(0.0f) {}
-    RenderGlyphLine(uint32_t run, uint32_t index) :
-        startRun(run), startIndex(index), endRun(run), endIndex(index), startX(0.0f)
-    {}
-
-    bool empty() const { return startRun == endRun && startIndex == endIndex; }
-    static std::vector<RenderGlyphLine> BreakLines(Span<const RenderGlyphRun> runs,
-                                                   float width,
-                                                   RenderTextAlign align);
-
-    // Compute values for top/baseline/bottom per line
-    static void ComputeLineSpacing(rive::Span<RenderGlyphLine>,
-                                   rive::Span<const RenderGlyphRun>,
-                                   float width,
-                                   RenderTextAlign align);
-};
-
-} // namespace rive
-
-#endif
diff --git a/include/rive/text/renderfont_hb.hpp b/include/rive/text/renderfont_hb.hpp
deleted file mode 100644
index 865afc2..0000000
--- a/include/rive/text/renderfont_hb.hpp
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright 2022 Rive
- */
-
-#ifndef _RIVE_RENDERFONT_HB_HPP_
-#define _RIVE_RENDERFONT_HB_HPP_
-
-#include "rive/factory.hpp"
-#include "rive/render_text.hpp"
-
-struct hb_font_t;
-struct hb_draw_funcs_t;
-
-class HBRenderFont : public rive::RenderFont
-{
-    hb_draw_funcs_t* m_DrawFuncs;
-
-public:
-    hb_font_t* m_Font;
-
-    // We assume ownership of font!
-    HBRenderFont(hb_font_t* font);
-    ~HBRenderFont() override;
-
-    std::vector<Axis> getAxes() const override;
-    std::vector<Coord> getCoords() const override;
-    rive::rcp<rive::RenderFont> makeAtCoords(rive::Span<const Coord>) const override;
-    rive::RawPath getPath(rive::GlyphID) const override;
-    rive::SimpleArray<rive::RenderGlyphRun>
-        onShapeText(rive::Span<const rive::Unichar>,
-                    rive::Span<const rive::RenderTextRun>) const override;
-
-    static rive::rcp<rive::RenderFont> Decode(rive::Span<const uint8_t>);
-
-    // If the platform can supply fallback font(s), set this function pointer.
-    // It will be called with a span of unichars, and the platform attempts to
-    // return a font that can draw (at least some of) them. If no font is available
-    // just return nullptr.
-
-    using FallbackProc = rive::rcp<rive::RenderFont> (*)(rive::Span<const rive::Unichar>);
-
-    static FallbackProc gFallbackProc;
-};
-
-#endif
diff --git a/include/utils/rive_utf.hpp b/include/utils/rive_utf.hpp
index 780292f..e5a79aa 100644
--- a/include/utils/rive_utf.hpp
+++ b/include/utils/rive_utf.hpp
@@ -5,7 +5,7 @@
 #ifndef _RIVE_UTF_HPP_
 #define _RIVE_UTF_HPP_
 
-#include "rive/render_text.hpp"
+#include "rive/text.hpp"
 
 namespace rive
 {
diff --git a/skia/renderer/build.sh b/skia/renderer/build.sh
index 5e51dae..4683ea1 100755
--- a/skia/renderer/build.sh
+++ b/skia/renderer/build.sh
@@ -4,12 +4,6 @@
 export SKIA_DIR="skia"
 source ../../dependencies/config_directories.sh
 
-if [[ ! -d "$DEPENDENCIES/harfbuzz" ]]; then
-    pushd $DEPENDENCIES_SCRIPTS
-    ./get_harfbuzz.sh
-    popd
-fi
-
 # build main rive
 cd ../..
 ./build.sh "$@"
@@ -48,7 +42,7 @@
 else
     build() {
         echo "Building Rive Renderer for platform=$platform option=$OPTION"
-        PREMAKE="premake5 gmake2 $1"
+        PREMAKE="premake5 --scripts=../../../build gmake2 $1"
         eval "$PREMAKE"
         if [ "$OPTION" = "clean" ]; then
             make clean
diff --git a/skia/renderer/include/renderfont_coretext.hpp b/skia/renderer/include/renderfont_coretext.hpp
deleted file mode 100644
index de13daf..0000000
--- a/skia/renderer/include/renderfont_coretext.hpp
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright 2022 Rive
- */
-
-#ifndef _RIVE_RENDERFONT_CORETEXT_HPP_
-#define _RIVE_RENDERFONT_CORETEXT_HPP_
-
-#include "rive/factory.hpp"
-#include "rive/render_text.hpp"
-
-#if defined(RIVE_BUILD_FOR_OSX)
-#include <ApplicationServices/ApplicationServices.h>
-#elif defined(RIVE_BUILD_FOR_IOS)
-#include <CoreText/CoreText.h>
-#endif
-
-class CoreTextRenderFont : public rive::RenderFont
-{
-public:
-    CTFontRef m_font;
-    const std::vector<Axis> m_axes;
-    const std::vector<Coord> m_coords;
-
-    // We assume ownership of font!
-    CoreTextRenderFont(CTFontRef, std::vector<Axis>);
-    ~CoreTextRenderFont() override;
-
-    std::vector<Axis> getAxes() const override { return m_axes; }
-    std::vector<Coord> getCoords() const override { return m_coords; }
-    rive::rcp<rive::RenderFont> makeAtCoords(rive::Span<const Coord>) const override;
-    rive::RawPath getPath(rive::GlyphID) const override;
-    rive::SimpleArray<rive::RenderGlyphRun>
-        onShapeText(rive::Span<const rive::Unichar>,
-                    rive::Span<const rive::RenderTextRun>) const override;
-
-    static rive::rcp<rive::RenderFont> Decode(rive::Span<const uint8_t>);
-    static rive::rcp<rive::RenderFont> FromCT(CTFontRef);
-};
-
-#endif
diff --git a/skia/renderer/src/renderfont_coretext.cpp b/skia/renderer/src/renderfont_coretext.cpp
deleted file mode 100644
index 36771da..0000000
--- a/skia/renderer/src/renderfont_coretext.cpp
+++ /dev/null
@@ -1,372 +0,0 @@
-/*
- * Copyright 2022 Rive
- */
-
-#include "rive/rive_types.hpp"
-#include "utils/rive_utf.hpp"
-
-#if defined(RIVE_BUILD_FOR_APPLE) && defined(WITH_RIVE_TEXT)
-#include "renderfont_coretext.hpp"
-#include "mac_utils.hpp"
-
-#include "rive/factory.hpp"
-#include "rive/render_text.hpp"
-#include "rive/core/type_conversions.hpp"
-
-#if defined(RIVE_BUILD_FOR_OSX)
-#include <ApplicationServices/ApplicationServices.h>
-#elif defined(RIVE_BUILD_FOR_IOS)
-#include <CoreText/CoreText.h>
-#include <CoreText/CTFontManager.h>
-#include <CoreGraphics/CoreGraphics.h>
-#include <CoreFoundation/CoreFoundation.h>
-#endif
-
-constexpr int kStdScale = 2048;
-constexpr float gInvScale = 1.0f / kStdScale;
-
-static std::vector<rive::RenderFont::Axis> compute_axes(CTFontRef font)
-{
-    std::vector<rive::RenderFont::Axis> axes;
-
-    AutoCF array = CTFontCopyVariationAxes(font);
-    if (auto count = array.get() ? CFArrayGetCount(array.get()) : 0)
-    {
-        axes.reserve(count);
-
-        for (auto i = 0; i < count; ++i)
-        {
-            auto axis = (CFDictionaryRef)CFArrayGetValueAtIndex(array, i);
-
-            auto tag = find_u32(axis, kCTFontVariationAxisIdentifierKey);
-            auto min = find_float(axis, kCTFontVariationAxisMinimumValueKey);
-            auto def = find_float(axis, kCTFontVariationAxisDefaultValueKey);
-            auto max = find_float(axis, kCTFontVariationAxisMaximumValueKey);
-            //     printf("%08X %g %g %g\n", tag, min, def, max);
-
-            axes.push_back({tag, min, def, max});
-        }
-    }
-    return axes;
-}
-
-static std::vector<rive::RenderFont::Coord> compute_coords(CTFontRef font)
-{
-    std::vector<rive::RenderFont::Coord> coords(0);
-    AutoCF dict = CTFontCopyVariation(font);
-    if (dict)
-    {
-        int count = CFDictionaryGetCount(dict);
-        if (count > 0)
-        {
-            coords.resize(count);
-
-            AutoSTArray<100, const void*> ptrs(count * 2);
-            const void** keys = &ptrs[0];
-            const void** values = &ptrs[count];
-            CFDictionaryGetKeysAndValues(dict, keys, values);
-            for (int i = 0; i < count; ++i)
-            {
-                uint32_t tag = number_as_u32((CFNumberRef)keys[i]);
-                float value = number_as_float((CFNumberRef)values[i]);
-                //                printf("[%d] %08X %s %g\n", i, tag, tag2str(tag).c_str(), value);
-                coords[i] = {tag, value};
-            }
-        }
-    }
-    return coords;
-}
-
-static rive::RenderFont::LineMetrics make_lmx(CTFontRef font)
-{
-    return {
-        (float)-CTFontGetAscent(font) * gInvScale,
-        (float)CTFontGetDescent(font) * gInvScale,
-    };
-}
-
-CoreTextRenderFont::CoreTextRenderFont(CTFontRef font, std::vector<rive::RenderFont::Axis> axes) :
-    rive::RenderFont(make_lmx(font)),
-    m_font(font), // we take ownership of font
-    m_axes(std::move(axes)),
-    m_coords(compute_coords(font))
-{}
-
-CoreTextRenderFont::~CoreTextRenderFont() { CFRelease(m_font); }
-
-rive::rcp<rive::RenderFont> CoreTextRenderFont::makeAtCoords(rive::Span<const Coord> coords) const
-{
-    AutoCF vars = CFDictionaryCreateMutable(kCFAllocatorDefault,
-                                            coords.size(),
-                                            &kCFTypeDictionaryKeyCallBacks,
-                                            &kCFTypeDictionaryValueCallBacks);
-    for (const auto& c : coords)
-    {
-        AutoCF tagNum = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &c.axis);
-        AutoCF valueNum = CFNumberCreate(kCFAllocatorDefault, kCFNumberFloat32Type, &c.value);
-        CFDictionaryAddValue(vars.get(), tagNum.get(), valueNum.get());
-    }
-
-    AutoCF attrs = CFDictionaryCreateMutable(kCFAllocatorDefault,
-                                             1,
-                                             &kCFTypeDictionaryKeyCallBacks,
-                                             &kCFTypeDictionaryValueCallBacks);
-    CFDictionarySetValue(attrs.get(), kCTFontVariationAttribute, vars.get());
-
-    AutoCF desc = (CTFontDescriptorRef)CTFontDescriptorCreateWithAttributes(attrs.get());
-
-    auto font = CTFontCreateCopyWithAttributes(m_font, 0, nullptr, desc.get());
-
-    return rive::rcp<rive::RenderFont>(new CoreTextRenderFont(font, compute_axes(font)));
-}
-
-static CTFontRef font_from_run(CTRunRef run)
-{
-    auto attr = CTRunGetAttributes(run);
-    assert(attr);
-    CTFontRef ct = (CTFontRef)CFDictionaryGetValue(attr, kCTFontAttributeName);
-    assert(ct);
-    return ct;
-}
-
-static rive::rcp<rive::RenderFont> convert_to_renderfont(CTFontRef ct,
-                                                         rive::rcp<rive::RenderFont> rf)
-{
-    auto ctrf = static_cast<CoreTextRenderFont*>(rf.get());
-    if (ctrf->m_font == ct)
-    {
-        return rf;
-    }
-    CFRetain(ct);
-    return rive::rcp<rive::RenderFont>(new CoreTextRenderFont(ct, compute_axes(ct)));
-}
-
-static void apply_element(void* ctx, const CGPathElement* element)
-{
-    auto path = (rive::RawPath*)ctx;
-    const CGPoint* points = element->points;
-
-    switch (element->type)
-    {
-        case kCGPathElementMoveToPoint:
-            path->moveTo(points[0].x, points[0].y);
-            break;
-
-        case kCGPathElementAddLineToPoint:
-            path->lineTo(points[0].x, points[0].y);
-            break;
-
-        case kCGPathElementAddQuadCurveToPoint:
-            path->quadTo(points[0].x, points[0].y, points[1].x, points[1].y);
-            break;
-
-        case kCGPathElementAddCurveToPoint:
-            path->cubicTo(points[0].x,
-                          points[0].y,
-                          points[1].x,
-                          points[1].y,
-                          points[2].x,
-                          points[2].y);
-            break;
-
-        case kCGPathElementCloseSubpath:
-            path->close();
-            break;
-
-        default:
-            assert(false);
-            break;
-    }
-}
-
-rive::RawPath CoreTextRenderFont::getPath(rive::GlyphID glyph) const
-{
-    rive::RawPath rpath;
-
-    AutoCF cgPath = CTFontCreatePathForGlyph(m_font, glyph, nullptr);
-    if (!cgPath)
-    {
-        return rpath;
-    }
-
-    CGPathApply(cgPath.get(), &rpath, apply_element);
-    rpath.transformInPlace(rive::Mat2D::fromScale(gInvScale, -gInvScale));
-    return rpath;
-}
-
-////////////////////////////////////////////////////////////////////////////////////
-
-struct AutoUTF16
-{
-    std::vector<uint16_t> array;
-
-    AutoUTF16(const rive::Unichar uni[], int count)
-    {
-        array.reserve(count);
-        for (int i = 0; i < count; ++i)
-        {
-            uint16_t tmp[2];
-            int n = rive::UTF::ToUTF16(uni[i], tmp);
-
-            for (int i = 0; i < n; ++i)
-            {
-                array.push_back(tmp[i]);
-            }
-        }
-    }
-};
-
-static rive::RenderGlyphRun add_run(CTRunRef run, uint32_t textStart, float textSize, float& startX)
-{
-    if (auto count = CTRunGetGlyphCount(run))
-    {
-        const float scale = textSize * gInvScale;
-
-        rive::RenderGlyphRun gr(count);
-        gr.size = textSize;
-
-        CTRunGetGlyphs(run, {0, count}, gr.glyphs.data());
-
-        AutoSTArray<1024, CFIndex> indices(count);
-        AutoSTArray<1024, CGSize> advances(count);
-
-        CTRunGetAdvances(run, {0, count}, advances.data());
-        CTRunGetStringIndices(run, {0, count}, indices.data());
-
-        for (CFIndex i = 0; i < count; ++i)
-        {
-            gr.xpos[i] = startX;
-            gr.textIndices[i] = textStart + indices[i]; // utf16 offsets, will fix-up later
-            startX += advances[i].width * scale;
-        }
-        gr.xpos[count] = startX;
-        return gr;
-    }
-    return rive::RenderGlyphRun();
-}
-
-rive::SimpleArray<rive::RenderGlyphRun>
-CoreTextRenderFont::onShapeText(rive::Span<const rive::Unichar> text,
-                                rive::Span<const rive::RenderTextRun> truns) const
-{
-    rive::SimpleArrayBuilder<rive::RenderGlyphRun> gruns(truns.size());
-
-    uint32_t textIndex = 0;
-    float startX = 0;
-    for (const auto& tr : truns)
-    {
-        CTFontRef font = ((CoreTextRenderFont*)tr.font.get())->m_font;
-
-        AutoUTF16 utf16(&text[textIndex], tr.unicharCount);
-        const bool hasSurrogates = utf16.array.size() != tr.unicharCount;
-        assert(!hasSurrogates);
-
-        AutoCF string = CFStringCreateWithCharactersNoCopy(nullptr,
-                                                           utf16.array.data(),
-                                                           utf16.array.size(),
-                                                           kCFAllocatorNull);
-
-        AutoCF attr = CFDictionaryCreateMutable(kCFAllocatorDefault,
-                                                0,
-                                                &kCFTypeDictionaryKeyCallBacks,
-                                                &kCFTypeDictionaryValueCallBacks);
-        CFDictionaryAddValue(attr.get(), kCTFontAttributeName, font);
-
-        AutoCF attrString = CFAttributedStringCreate(kCFAllocatorDefault, string.get(), attr.get());
-
-        AutoCF typesetter = CTTypesetterCreateWithAttributedString(attrString.get());
-
-        AutoCF line = CTTypesetterCreateLine(typesetter.get(), {0, tr.unicharCount});
-
-        CFArrayRef run_array = CTLineGetGlyphRuns(line.get());
-        CFIndex runCount = CFArrayGetCount(run_array);
-        for (CFIndex i = 0; i < runCount; ++i)
-        {
-            CTRunRef runref = (CTRunRef)CFArrayGetValueAtIndex(run_array, i);
-            rive::RenderGlyphRun grun = add_run(runref, textIndex, tr.size, startX);
-            if (grun.glyphs.size() > 0)
-            {
-                auto ct = font_from_run(runref);
-                grun.font = convert_to_renderfont(ct, tr.font);
-                grun.size = tr.size;
-                gruns.add(std::move(grun));
-            }
-        }
-        textIndex += tr.unicharCount;
-    }
-
-    return std::move(gruns);
-}
-
-////////////////////////////////////////////////////////////////////////////////////////////////
-
-rive::rcp<rive::RenderFont> CoreTextRenderFont::FromCT(CTFontRef ctfont)
-{
-    if (!ctfont)
-    {
-        return nullptr;
-    }
-
-    // We always want the ctfont at our magic size
-    if (CTFontGetSize(ctfont) != kStdScale)
-    {
-        ctfont = CTFontCreateCopyWithAttributes(ctfont, kStdScale, nullptr, nullptr);
-    }
-    else
-    {
-        CFRetain(ctfont);
-    }
-
-    // Apple may have secretly set the opsz axis based on the size. We want to undo this
-    // since our stdsize isn't really the size we'll show it at.
-    auto axes = compute_axes(ctfont);
-    if (axes.size() > 0)
-    {
-        constexpr uint32_t kOPSZ = make_tag('o', 'p', 's', 'z');
-        for (const auto& ax : axes)
-        {
-            if (ax.tag == kOPSZ)
-            {
-                auto xform = CGAffineTransformMakeScale(kStdScale / ax.def, kStdScale / ax.def);
-                // Recreate the font at this size, but with a balancing transform,
-                // so we get the 'default' shapes w.r.t. the opsz axis
-                auto newfont = CTFontCreateCopyWithAttributes(ctfont, ax.def, &xform, nullptr);
-                CFRelease(ctfont);
-                ctfont = newfont;
-                break;
-            }
-        }
-    }
-
-    return rive::rcp<rive::RenderFont>(new CoreTextRenderFont(ctfont, std::move(axes)));
-}
-
-rive::rcp<rive::RenderFont> CoreTextRenderFont::Decode(rive::Span<const uint8_t> span)
-{
-    AutoCF data = CFDataCreate(nullptr, span.data(), span.size()); // makes a copy
-    if (!data)
-    {
-        assert(false);
-        return nullptr;
-    }
-
-    AutoCF desc = CTFontManagerCreateFontDescriptorFromData(data.get());
-    if (!desc)
-    {
-        assert(false);
-        return nullptr;
-    }
-
-    CTFontOptions options = kCTFontOptionsPreventAutoActivation;
-
-    AutoCF ctfont =
-        CTFontCreateWithFontDescriptorAndOptions(desc.get(), kStdScale, nullptr, options);
-    if (!ctfont)
-    {
-        assert(false);
-        return nullptr;
-    }
-    return FromCT(ctfont.get());
-}
-
-#endif
diff --git a/src/renderer.cpp b/src/renderer.cpp
index ebcabb1..5447c66 100644
--- a/src/renderer.cpp
+++ b/src/renderer.cpp
@@ -99,17 +99,15 @@
 RenderPath::RenderPath() { Counter::update(Counter::kPath, 1); }
 RenderPath::~RenderPath() { Counter::update(Counter::kPath, -1); }
 
-#include "rive/render_text.hpp"
+#include "rive/text.hpp"
 
-static bool isWhiteSpace(rive::Unichar c) { return c <= ' '; }
+static bool isWhiteSpace(Unichar c) { return c <= ' ' || c == 0x2028; }
 
-rive::SimpleArray<RenderGlyphRun>
-RenderFont::shapeText(rive::Span<const rive::Unichar> text,
-                      rive::Span<const rive::RenderTextRun> runs) const
+SimpleArray<Paragraph> Font::shapeText(Span<const Unichar> text, Span<const TextRun> runs) const
 {
 #ifdef DEBUG
     size_t count = 0;
-    for (const auto& tr : runs)
+    for (const TextRun& tr : runs)
     {
         assert(tr.unicharCount > 0);
         count += tr.unicharCount;
@@ -117,39 +115,40 @@
     assert(count <= text.size());
 #endif
 
-    auto gruns = onShapeText(text, runs);
+    SimpleArray<Paragraph> paragraphs = onShapeText(text, runs);
     bool wantWhiteSpace = false;
-
-    rive::RenderGlyphRun* lastRun = nullptr;
+    GlyphRun* lastRun = nullptr;
     size_t reserveSize = text.size() / 4;
-    rive::SimpleArrayBuilder<uint32_t> breakBuilder(reserveSize);
-    for (auto& gr : gruns)
+    SimpleArrayBuilder<uint32_t> breakBuilder(reserveSize);
+    for (const Paragraph& para : paragraphs)
     {
-        if (lastRun != nullptr)
+        for (GlyphRun& gr : para.runs)
         {
-            lastRun->breaks = std::move(breakBuilder);
-            // Reset the builder.
-            breakBuilder = rive::SimpleArrayBuilder<uint32_t>(reserveSize);
-        }
-        uint32_t glyphIndex = 0;
-        for (auto offset : gr.textIndices)
-        {
-
-            auto unicode = text[offset];
-            if (unicode == '\n')
+            if (lastRun != nullptr)
             {
-                breakBuilder.add(glyphIndex);
-                breakBuilder.add(glyphIndex);
+                lastRun->breaks = std::move(breakBuilder);
+                // Reset the builder.
+                breakBuilder = SimpleArrayBuilder<uint32_t>(reserveSize);
             }
-            if (wantWhiteSpace == isWhiteSpace(unicode))
+            uint32_t glyphIndex = 0;
+            for (uint32_t offset : gr.textIndices)
             {
-                breakBuilder.add(glyphIndex);
-                wantWhiteSpace = !wantWhiteSpace;
+                Unichar unicode = text[offset];
+                if (unicode == '\n' || unicode == 0x2028)
+                {
+                    breakBuilder.add(glyphIndex);
+                    breakBuilder.add(glyphIndex);
+                }
+                if (wantWhiteSpace == isWhiteSpace(unicode))
+                {
+                    breakBuilder.add(glyphIndex);
+                    wantWhiteSpace = !wantWhiteSpace;
+                }
+                glyphIndex++;
             }
-            glyphIndex++;
-        }
 
-        lastRun = &gr;
+            lastRun = &gr;
+        }
     }
     if (lastRun != nullptr)
     {
@@ -161,12 +160,15 @@
     }
 
 #ifdef DEBUG
-    for (const auto& gr : gruns)
+    for (const Paragraph& para : paragraphs)
     {
-        assert(gr.glyphs.size() > 0);
-        assert(gr.glyphs.size() == gr.textIndices.size());
-        assert(gr.glyphs.size() + 1 == gr.xpos.size());
+        for (const GlyphRun& gr : para.runs)
+        {
+            assert(gr.glyphs.size() > 0);
+            assert(gr.glyphs.size() == gr.textIndices.size());
+            assert(gr.glyphs.size() + 1 == gr.xpos.size());
+        }
     }
 #endif
-    return gruns;
+    return paragraphs;
 }
diff --git a/src/text/font_hb.cpp b/src/text/font_hb.cpp
new file mode 100644
index 0000000..d1bea79
--- /dev/null
+++ b/src/text/font_hb.cpp
@@ -0,0 +1,465 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#include "rive/text.hpp"
+
+#ifdef WITH_RIVE_TEXT
+#include "rive/text/font_hb.hpp"
+
+#include "rive/factory.hpp"
+#include "rive/renderer_utils.hpp"
+
+#include "hb.h"
+#include "hb-ot.h"
+extern "C"
+{
+#include "SheenBidi.h"
+}
+
+// Initialized to null. Client can set this to a callback.
+HBFont::FallbackProc HBFont::gFallbackProc;
+
+rive::rcp<rive::Font> HBFont::Decode(rive::Span<const uint8_t> span)
+{
+    auto blob = hb_blob_create_or_fail((const char*)span.data(),
+                                       (unsigned)span.size(),
+                                       HB_MEMORY_MODE_DUPLICATE,
+                                       nullptr,
+                                       nullptr);
+    if (blob)
+    {
+        auto face = hb_face_create(blob, 0);
+        hb_blob_destroy(blob);
+        if (face)
+        {
+            auto font = hb_font_create(face);
+            hb_face_destroy(face);
+            if (font)
+            {
+                return rive::rcp<rive::Font>(new HBFont(font));
+            }
+        }
+    }
+    return nullptr;
+}
+
+//////////////
+
+constexpr int kStdScale = 2048;
+constexpr float gInvScale = 1.0f / kStdScale;
+
+extern "C"
+{
+    static void
+    rpath_move_to(hb_draw_funcs_t*, void* rpath, hb_draw_state_t*, float x, float y, void*)
+    {
+        ((rive::RawPath*)rpath)->moveTo(x * gInvScale, -y * gInvScale);
+    }
+    static void
+    rpath_line_to(hb_draw_funcs_t*, void* rpath, hb_draw_state_t*, float x1, float y1, void*)
+    {
+        ((rive::RawPath*)rpath)->lineTo(x1 * gInvScale, -y1 * gInvScale);
+    }
+    static void rpath_quad_to(hb_draw_funcs_t*,
+                              void* rpath,
+                              hb_draw_state_t*,
+                              float x1,
+                              float y1,
+                              float x2,
+                              float y2,
+                              void*)
+    {
+        ((rive::RawPath*)rpath)
+            ->quadTo(x1 * gInvScale, -y1 * gInvScale, x2 * gInvScale, -y2 * gInvScale);
+    }
+    static void rpath_cubic_to(hb_draw_funcs_t*,
+                               void* rpath,
+                               hb_draw_state_t*,
+                               float x1,
+                               float y1,
+                               float x2,
+                               float y2,
+                               float x3,
+                               float y3,
+                               void*)
+    {
+        ((rive::RawPath*)rpath)
+            ->cubicTo(x1 * gInvScale,
+                      -y1 * gInvScale,
+                      x2 * gInvScale,
+                      -y2 * gInvScale,
+                      x3 * gInvScale,
+                      -y3 * gInvScale);
+    }
+    static void rpath_close(hb_draw_funcs_t*, void* rpath, hb_draw_state_t*, void*)
+    {
+        ((rive::RawPath*)rpath)->close();
+    }
+}
+
+static rive::Font::LineMetrics make_lmx(hb_font_t* font)
+{
+    // premable on font...
+    hb_ot_font_set_funcs(font);
+    hb_font_set_scale(font, kStdScale, kStdScale);
+
+    hb_font_extents_t extents;
+    hb_font_get_h_extents(font, &extents);
+    return {-extents.ascender * gInvScale, -extents.descender * gInvScale};
+}
+
+HBFont::HBFont(hb_font_t* font) :
+    Font(make_lmx(font)), m_Font(font) // we just take ownership, no need to call reference()
+{
+    m_DrawFuncs = hb_draw_funcs_create();
+    hb_draw_funcs_set_move_to_func(m_DrawFuncs, rpath_move_to, nullptr, nullptr);
+    hb_draw_funcs_set_line_to_func(m_DrawFuncs, rpath_line_to, nullptr, nullptr);
+    hb_draw_funcs_set_quadratic_to_func(m_DrawFuncs, rpath_quad_to, nullptr, nullptr);
+    hb_draw_funcs_set_cubic_to_func(m_DrawFuncs, rpath_cubic_to, nullptr, nullptr);
+    hb_draw_funcs_set_close_path_func(m_DrawFuncs, rpath_close, nullptr, nullptr);
+    hb_draw_funcs_make_immutable(m_DrawFuncs);
+}
+
+HBFont::~HBFont()
+{
+    hb_draw_funcs_destroy(m_DrawFuncs);
+    hb_font_destroy(m_Font);
+}
+
+std::vector<rive::Font::Axis> HBFont::getAxes() const
+{
+    auto face = hb_font_get_face(m_Font);
+    std::vector<rive::Font::Axis> axes;
+
+    const int count = hb_ot_var_get_axis_count(face);
+    if (count > 0)
+    {
+        axes.resize(count);
+
+        hb_ot_var_axis_info_t info;
+        for (int i = 0; i < count; ++i)
+        {
+            unsigned n = 1;
+            hb_ot_var_get_axis_infos(face, i, &n, &info);
+            assert(n == 1);
+            axes[i] = {info.tag, info.min_value, info.default_value, info.max_value};
+            //       printf("[%d] %08X %g %g %g\n", i, info.tag, info.min_value, info.default_value,
+            //       info.max_value);
+        }
+    }
+    return axes;
+}
+
+std::vector<rive::Font::Coord> HBFont::getCoords() const
+{
+    auto axes = this->getAxes();
+    //  const int count = (int)axes.size();
+
+    unsigned length;
+    const float* values = hb_font_get_var_coords_design(m_Font, &length);
+
+    std::vector<rive::Font::Coord> coords(length);
+    for (unsigned i = 0; i < length; ++i)
+    {
+        coords[i] = {axes[i].tag, values[i]};
+    }
+    return coords;
+}
+
+rive::rcp<rive::Font> HBFont::makeAtCoords(rive::Span<const Coord> coords) const
+{
+    AutoSTArray<16, hb_variation_t> vars(coords.size());
+    for (size_t i = 0; i < coords.size(); ++i)
+    {
+        vars[i] = {coords[i].axis, coords[i].value};
+    }
+    auto font = hb_font_create_sub_font(m_Font);
+    hb_font_set_variations(font, vars.data(), (unsigned int)vars.size());
+    return rive::rcp<rive::Font>(new HBFont(font));
+}
+
+rive::RawPath HBFont::getPath(rive::GlyphID glyph) const
+{
+    rive::RawPath rpath;
+    hb_font_get_glyph_shape(m_Font, glyph, m_DrawFuncs, &rpath);
+    return rpath;
+}
+
+///////////////////////////////////////////////////////////
+
+const hb_feature_t gFeatures[] = {
+    {'liga', 1, HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END},
+    {'dlig', 1, HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END},
+    // {'clig', 1, HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END},
+    // {'calt', 1, HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END},
+    {'kern', 1, HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END},
+};
+constexpr int gNumFeatures = sizeof(gFeatures) / sizeof(gFeatures[0]);
+
+static rive::GlyphRun shape_run(const rive::Unichar text[],
+                                const rive::TextRun& tr,
+                                unsigned textOffset)
+{
+    hb_buffer_t* buf = hb_buffer_create();
+    hb_buffer_add_utf32(buf, text, tr.unicharCount, 0, tr.unicharCount);
+
+    hb_buffer_set_direction(buf,
+                            tr.dir == rive::TextDirection::rtl ? HB_DIRECTION_RTL
+                                                               : HB_DIRECTION_LTR);
+    hb_buffer_set_script(buf, (hb_script_t)tr.script); // HB_SCRIPT_ARABIC);
+    hb_buffer_set_language(buf, hb_language_from_string("fa", -1));
+
+    auto hbfont = (HBFont*)tr.font.get();
+    hb_shape(hbfont->m_Font, buf, gFeatures, gNumFeatures);
+
+    unsigned int glyph_count;
+    hb_glyph_info_t* glyph_info = hb_buffer_get_glyph_infos(buf, &glyph_count);
+    hb_glyph_position_t* glyph_pos = hb_buffer_get_glyph_positions(buf, &glyph_count);
+
+    // todo: check for missing glyphs, and perform font-substitution
+
+    rive::GlyphRun gr(glyph_count);
+    gr.font = tr.font;
+    gr.size = tr.size;
+    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.xpos[glyph_count] = 0; // so the next run can line up snug
+    hb_buffer_destroy(buf);
+    return gr;
+}
+
+static rive::GlyphRun extract_subset(const rive::GlyphRun& orig, size_t start, size_t end)
+{
+    auto count = end - start;
+    rive::GlyphRun subset(rive::SimpleArray<rive::GlyphID>(&orig.glyphs[start], count),
+                          rive::SimpleArray<uint32_t>(&orig.textIndices[start], count),
+                          rive::SimpleArray<float>(&orig.advances[start], count),
+                          rive::SimpleArray<float>(&orig.xpos[start], count));
+    subset.font = std::move(orig.font);
+    subset.size = orig.size;
+    subset.xpos.back() = 0; // since we're now the end of a run
+    subset.styleId = orig.styleId;
+
+    return subset;
+}
+
+static void perform_fallback(rive::rcp<rive::Font> fallbackFont,
+                             rive::SimpleArrayBuilder<rive::GlyphRun>& gruns,
+                             const rive::Unichar text[],
+                             const rive::GlyphRun& orig,
+                             const rive::TextRun& origTextRun)
+{
+    assert(orig.glyphs.size() > 0);
+
+    const size_t count = orig.glyphs.size();
+    size_t startI = 0;
+    while (startI < count)
+    {
+        size_t endI = startI + 1;
+        if (orig.glyphs[startI] == 0)
+        {
+            while (endI < count && orig.glyphs[endI] == 0)
+            {
+                ++endI;
+            }
+            auto textStart = orig.textIndices[startI];
+            auto textCount = orig.textIndices[endI - 1] - textStart + 1;
+            auto tr = rive::TextRun{
+                fallbackFont,
+                orig.size,
+                textCount,
+                origTextRun.script,
+                orig.styleId,
+                orig.dir,
+            };
+            gruns.add(shape_run(&text[textStart], tr, textStart));
+        }
+        else
+        {
+            while (endI < count && orig.glyphs[endI] != 0)
+            {
+                ++endI;
+            }
+            gruns.add(extract_subset(orig, startI, endI));
+        }
+        startI = endI;
+    }
+}
+
+rive::SimpleArray<rive::Paragraph> HBFont::onShapeText(rive::Span<const rive::Unichar> text,
+                                                       rive::Span<const rive::TextRun> truns) const
+{
+
+    rive::SimpleArrayBuilder<rive::Paragraph> paragraphs;
+    SBCodepointSequence codepointSequence = {SBStringEncodingUTF32,
+                                             (void*)text.data(),
+                                             text.size()};
+
+    hb_unicode_funcs_t* ufuncs = hb_unicode_funcs_get_default();
+
+    // Split runs by bidi types.
+    uint32_t textIndex = 0;
+    uint32_t runIndex = 0;
+    uint32_t runStartTextIndex = 0;
+
+    SBUInteger paragraphStart = 0;
+
+    SBAlgorithmRef bidiAlgorithm = SBAlgorithmCreate(&codepointSequence);
+    uint32_t unicharIndex = 0;
+    uint32_t runTextIndex = 0;
+
+    while (paragraphStart < text.size())
+    {
+        SBParagraphRef paragraph =
+            SBAlgorithmCreateParagraph(bidiAlgorithm, paragraphStart, INT32_MAX, SBLevelDefaultLTR);
+        SBUInteger paragraphLength = SBParagraphGetLength(paragraph);
+        // Next iteration reads the next paragraph (if any remain).
+        paragraphStart += paragraphLength;
+        const SBLevel* bidiLevels = SBParagraphGetLevelsPtr(paragraph);
+        SBLevel paragraphLevel = SBParagraphGetBaseLevel(paragraph);
+        uint32_t paragraphTextIndex = 0;
+
+        std::vector<rive::TextRun> bidiRuns;
+        bidiRuns.reserve(truns.size());
+
+        while (runIndex < truns.size())
+        {
+            const auto& tr = truns[runIndex];
+            assert(tr.unicharCount != 0);
+            SBLevel lastLevel = bidiLevels[paragraphTextIndex];
+            hb_script_t lastScript = hb_unicode_script(ufuncs, text[textIndex]);
+            rive::TextRun splitRun = {
+                .font = tr.font,
+                .size = tr.size,
+                .unicharCount = tr.unicharCount - runTextIndex,
+                .script = (uint32_t)lastScript,
+                .styleId = tr.styleId,
+                .dir = lastLevel & 1 ? rive::TextDirection::rtl : rive::TextDirection::ltr,
+            };
+
+            runStartTextIndex = textIndex;
+
+            runTextIndex++;
+            textIndex++;
+            paragraphTextIndex++;
+            bidiRuns.push_back(splitRun);
+
+            while (runTextIndex < tr.unicharCount && paragraphTextIndex < paragraphLength)
+            {
+                hb_script_t script = hb_unicode_script(ufuncs, text[textIndex]);
+                switch (script)
+                {
+                    case HB_SCRIPT_COMMON:
+                    case HB_SCRIPT_INHERITED:
+                        // Propagate last seen "real" script value.
+                        script = lastScript;
+                        break;
+                    default:
+                        break;
+                }
+                if (bidiLevels[paragraphTextIndex] != lastLevel || script != lastScript)
+                {
+                    lastScript = script;
+                    auto& back = bidiRuns.back();
+                    back.unicharCount = textIndex - runStartTextIndex;
+                    lastLevel = bidiLevels[paragraphTextIndex];
+
+                    rive::TextRun splitRun = {
+                        .font = back.font,
+                        .size = back.size,
+                        .unicharCount = tr.unicharCount - runTextIndex,
+                        .script = (uint32_t)script,
+                        .styleId = back.styleId,
+                        .dir = lastLevel & 1 ? rive::TextDirection::rtl : rive::TextDirection::ltr,
+                    };
+                    runStartTextIndex = textIndex;
+                    bidiRuns.push_back(splitRun);
+                }
+                runTextIndex++;
+                textIndex++;
+                paragraphTextIndex++;
+            }
+            // Reached the end of the run?
+            if (runTextIndex == tr.unicharCount)
+            {
+                runIndex++;
+                runTextIndex = 0;
+            }
+            // We consumed the whole paragraph.
+            if (paragraphTextIndex == paragraphLength)
+            {
+                // Close off the last run.
+                auto& back = bidiRuns.back();
+                back.unicharCount = textIndex - runStartTextIndex;
+                break;
+            }
+        }
+
+        rive::SimpleArrayBuilder<rive::GlyphRun> gruns(bidiRuns.size());
+
+        for (const auto& tr : bidiRuns)
+        {
+            auto gr = shape_run(&text[unicharIndex], tr, unicharIndex);
+            unicharIndex += tr.unicharCount;
+
+            auto end = gr.glyphs.end();
+            auto iter = std::find(gr.glyphs.begin(), end, 0);
+            if (!gFallbackProc || iter == end)
+            {
+                gruns.add(std::move(gr));
+            }
+            else
+            {
+                // found at least 1 zero in glyphs, so need to perform font-fallback
+                size_t index = iter - gr.glyphs.begin();
+                rive::Unichar missing = text[gr.textIndices[index]];
+                // todo: consider sending more chars if that helps choose a font
+                auto fallback = gFallbackProc({&missing, 1});
+                if (fallback)
+                {
+                    perform_fallback(fallback, gruns, text.data(), gr, tr);
+                }
+                else
+                {
+                    gruns.add(std::move(gr)); // oh well, just keep the missing glyphs
+                }
+            }
+        }
+
+        // turn the advances we stored in xpos[] into actual x-positions
+        // for logical order.
+        float pos = 0;
+        for (auto& gr : gruns)
+        {
+            for (auto& xp : gr.xpos)
+            {
+                float adv = xp;
+                xp = pos;
+                pos += adv;
+            }
+        }
+
+        paragraphs.add({
+            std::move(gruns),
+            paragraphLevel & 1 ? rive::TextDirection::rtl : rive::TextDirection::ltr,
+        });
+        SBParagraphRelease(paragraph);
+    }
+
+    SBAlgorithmRelease(bidiAlgorithm);
+    return paragraphs;
+}
+
+#endif
\ No newline at end of file
diff --git a/src/text/line_breaker.cpp b/src/text/line_breaker.cpp
index 190cac0..996d6dd 100644
--- a/src/text/line_breaker.cpp
+++ b/src/text/line_breaker.cpp
@@ -2,40 +2,36 @@
  * Copyright 2022 Rive
  */
 
-#include "rive/text/line_breaker.hpp"
+#include "rive/text.hpp"
 #include <limits>
+#include <algorithm>
 using namespace rive;
 
 static bool autowidth(float width) { return width < 0.0f; }
-void RenderGlyphLine::ComputeLineSpacing(Span<RenderGlyphLine> lines,
-                                         Span<const RenderGlyphRun> runs,
-                                         float width,
-                                         RenderTextAlign align)
-{
 
+float GlyphLine::ComputeMaxWidth(Span<GlyphLine> lines, Span<const GlyphRun> runs)
+{
     float maxLineWidth = 0.0f;
-    if (autowidth(width))
+    for (auto& line : lines)
     {
-        for (auto& line : lines)
-        {
-            auto lineWidth =
-                runs[line.endRun].xpos[line.endIndex] - runs[line.startRun].xpos[line.startIndex];
-            if (lineWidth > maxLineWidth)
-            {
-                maxLineWidth = lineWidth;
-            }
-        }
+        maxLineWidth = std::max(maxLineWidth,
+                                runs[line.endRunIndex].xpos[line.endGlyphIndex] -
+                                    runs[line.startRunIndex].xpos[line.startGlyphIndex]);
     }
-    else
-    {
-        maxLineWidth = width;
-    }
+    return maxLineWidth;
+}
+
+void GlyphLine::ComputeLineSpacing(Span<GlyphLine> lines,
+                                   Span<const GlyphRun> runs,
+                                   float width,
+                                   TextAlign align)
+{
     float Y = 0; // top of our frame
     for (auto& line : lines)
     {
         float asc = 0;
         float des = 0;
-        for (int i = line.startRun; i <= line.endRun; ++i)
+        for (int i = line.startRunIndex; i <= line.endRunIndex; ++i)
         {
             const auto& run = runs[i];
 
@@ -47,18 +43,18 @@
         line.baseline = Y;
         Y += des;
         line.bottom = Y;
-        auto lineWidth =
-            runs[line.endRun].xpos[line.endIndex] - runs[line.startRun].xpos[line.startIndex];
+        auto lineWidth = runs[line.endRunIndex].xpos[line.endGlyphIndex] -
+                         runs[line.startRunIndex].xpos[line.startGlyphIndex];
         switch (align)
         {
-            case RenderTextAlign::right:
-                line.startX = maxLineWidth - lineWidth;
+            case TextAlign::right:
+                line.startX = width - lineWidth;
                 break;
-            case RenderTextAlign::left:
+            case TextAlign::left:
                 line.startX = 0;
                 break;
-            case RenderTextAlign::center:
-                line.startX = maxLineWidth / 2.0f - lineWidth / 2.0f;
+            case TextAlign::center:
+                line.startX = width / 2.0f - lineWidth / 2.0f;
                 break;
         }
     }
@@ -66,15 +62,14 @@
 
 struct WordMarker
 {
-    const RenderGlyphRun* run;
+    const GlyphRun* run;
     uint32_t index;
 
-    bool next(Span<const RenderGlyphRun> runs)
+    bool next(Span<const GlyphRun> runs)
     {
         index += 2;
         while (index >= run->breaks.size())
         {
-
             index -= run->breaks.size();
             run++;
             if (run == runs.end())
@@ -88,12 +83,12 @@
 
 class RunIterator
 {
-    Span<const RenderGlyphRun> m_runs;
-    const RenderGlyphRun* m_run;
+    Span<const GlyphRun> m_runs;
+    const GlyphRun* m_run;
     uint32_t m_index;
 
 public:
-    RunIterator(Span<const RenderGlyphRun> runs, const RenderGlyphRun* run, uint32_t index) :
+    RunIterator(Span<const GlyphRun> runs, const GlyphRun* run, uint32_t index) :
         m_runs(runs), m_run(run), m_index(index)
     {}
 
@@ -147,20 +142,17 @@
 
     float x() const { return m_run->xpos[m_index]; }
 
-    const RenderGlyphRun* run() const { return m_run; }
+    const GlyphRun* run() const { return m_run; }
     uint32_t index() const { return m_index; }
 
     bool operator==(const RunIterator& o) const { return m_run == o.m_run && m_index == o.m_index; }
 };
 
-std::vector<RenderGlyphLine> RenderGlyphLine::BreakLines(Span<const RenderGlyphRun> runs,
-                                                         float width,
-                                                         RenderTextAlign align)
+SimpleArray<GlyphLine> GlyphLine::BreakLines(Span<const GlyphRun> runs, float width)
 {
-
     float maxLineWidth = autowidth(width) ? std::numeric_limits<float>::max() : width;
 
-    std::vector<RenderGlyphLine> lines;
+    SimpleArrayBuilder<GlyphLine> lines;
 
     if (runs.empty())
     {
@@ -171,19 +163,26 @@
 
     bool advanceWord = false;
     WordMarker start = {runs.begin(), 0};
-    WordMarker end = {runs.begin(), 1};
+    WordMarker end = {runs.begin(), (uint32_t)-1};
+    if (!end.next(runs))
+    {
+        return lines;
+    }
 
-    RenderGlyphLine line = RenderGlyphLine();
+    GlyphLine line = GlyphLine();
 
     uint32_t breakIndex = end.run->breaks[end.index];
-    uint32_t lastEndIndex = end.index;
+    const GlyphRun* breakRun = end.run;
+    uint32_t lastEndGlyphIndex = end.index;
     uint32_t startBreakIndex = start.run->breaks[start.index];
+    const GlyphRun* startBreakRun = start.run;
+
     float x = end.run->xpos[breakIndex];
     while (true)
     {
         if (advanceWord)
         {
-            lastEndIndex = end.index;
+            lastEndGlyphIndex = end.index;
 
             if (!start.next(runs))
             {
@@ -197,26 +196,29 @@
             advanceWord = false;
 
             breakIndex = end.run->breaks[end.index];
+            breakRun = end.run;
             startBreakIndex = start.run->breaks[start.index];
+            startBreakRun = start.run;
             x = end.run->xpos[breakIndex];
         }
 
-        if (breakIndex != startBreakIndex && x > limit)
+        bool isForcedBreak = breakRun == startBreakRun && breakIndex == startBreakIndex;
+
+        if (!isForcedBreak && x > limit)
         {
-            uint32_t startRun = (uint32_t)(start.run - runs.begin());
+            uint32_t startRunIndex = (uint32_t)(start.run - runs.begin());
 
             // A whole word overflowed, break until we can no longer break (or
             // it fits).
-            if (line.startRun == startRun && line.startIndex == startBreakIndex)
+            if (line.startRunIndex == startRunIndex && line.startGlyphIndex == startBreakIndex)
             {
                 bool canBreakMore = true;
                 while (canBreakMore && x > limit)
                 {
 
                     RunIterator lineStart =
-                        RunIterator(runs, runs.begin() + line.startRun, line.startIndex);
+                        RunIterator(runs, runs.begin() + line.startRunIndex, line.startGlyphIndex);
                     RunIterator lineEnd = RunIterator(runs, end.run, end.run->breaks[end.index]);
-
                     // Look for the next character that doesn't overflow.
                     while (true)
                     {
@@ -237,8 +239,8 @@
                             }
                             else
                             {
-                                line.endRun = (uint32_t)(lineEnd.run() - runs.begin());
-                                line.endIndex = lineEnd.index();
+                                line.endRunIndex = (uint32_t)(lineEnd.run() - runs.begin());
+                                line.endGlyphIndex = lineEnd.index();
                             }
                             break;
                         }
@@ -249,11 +251,10 @@
                         limit = lineEnd.x() + maxLineWidth;
                         if (!line.empty())
                         {
-                            lines.push_back(line);
+                            lines.add(line);
                         }
                         // Setup the next line.
-                        line = RenderGlyphLine((uint32_t)(lineEnd.run() - runs.begin()),
-                                               lineEnd.index());
+                        line = GlyphLine((uint32_t)(lineEnd.run() - runs.begin()), lineEnd.index());
                     }
                 }
             }
@@ -263,35 +264,34 @@
                 auto startX = start.run->xpos[start.run->breaks[start.index]];
                 limit = startX + maxLineWidth;
 
-                if (!line.empty() || start.index - lastEndIndex > 1)
+                if (!line.empty() || start.index - lastEndGlyphIndex > 1)
                 {
-                    lines.push_back(line);
+                    lines.add(line);
                 }
 
-                line = RenderGlyphLine(startRun, startBreakIndex);
+                line = GlyphLine(startRunIndex, startBreakIndex);
             }
         }
         else
         {
-            line.endRun = (uint32_t)(end.run - runs.begin());
-            line.endIndex = end.run->breaks[end.index];
+            line.endRunIndex = (uint32_t)(end.run - runs.begin());
+            line.endGlyphIndex = end.run->breaks[end.index];
             advanceWord = true;
             // Forced BR.
-            if (breakIndex == startBreakIndex)
+            if (isForcedBreak)
             {
-                lines.push_back(line);
-                auto startX = start.run->xpos[start.run->breaks[start.index]];
+                lines.add(line);
+                auto startX = start.run->xpos[start.run->breaks[start.index] + 1];
                 limit = startX + maxLineWidth;
-                line = RenderGlyphLine((uint32_t)(start.run - runs.begin()), startBreakIndex + 1);
+                line = GlyphLine((uint32_t)(start.run - runs.begin()), startBreakIndex + 1);
             }
         }
     }
     // Don't add a line that starts/ends at the same spot.
     if (!line.empty())
     {
-        lines.push_back(line);
+        lines.add(line);
     }
 
-    ComputeLineSpacing(lines, runs, width, align);
     return lines;
 }
\ No newline at end of file
diff --git a/src/text/renderfont_hb.cpp b/src/text/renderfont_hb.cpp
deleted file mode 100644
index 5b536aa..0000000
--- a/src/text/renderfont_hb.cpp
+++ /dev/null
@@ -1,335 +0,0 @@
-/*
- * Copyright 2022 Rive
- */
-
-#include "rive/render_text.hpp"
-
-#ifdef WITH_RIVE_TEXT
-#include "rive/text/renderfont_hb.hpp"
-
-#include "rive/factory.hpp"
-#include "rive/renderer_utils.hpp"
-
-#include "hb.h"
-#include "hb-ot.h"
-
-// Initialized to null. Client can set this to a callback.
-HBRenderFont::FallbackProc HBRenderFont::gFallbackProc;
-
-rive::rcp<rive::RenderFont> HBRenderFont::Decode(rive::Span<const uint8_t> span)
-{
-    auto blob = hb_blob_create_or_fail((const char*)span.data(),
-                                       (unsigned)span.size(),
-                                       HB_MEMORY_MODE_DUPLICATE,
-                                       nullptr,
-                                       nullptr);
-    if (blob)
-    {
-        auto face = hb_face_create(blob, 0);
-        hb_blob_destroy(blob);
-        if (face)
-        {
-            auto font = hb_font_create(face);
-            hb_face_destroy(face);
-            if (font)
-            {
-                return rive::rcp<rive::RenderFont>(new HBRenderFont(font));
-            }
-        }
-    }
-    return nullptr;
-}
-
-//////////////
-
-constexpr int kStdScale = 2048;
-constexpr float gInvScale = 1.0f / kStdScale;
-
-extern "C"
-{
-    static void
-    rpath_move_to(hb_draw_funcs_t*, void* rpath, hb_draw_state_t*, float x, float y, void*)
-    {
-        ((rive::RawPath*)rpath)->moveTo(x * gInvScale, -y * gInvScale);
-    }
-    static void
-    rpath_line_to(hb_draw_funcs_t*, void* rpath, hb_draw_state_t*, float x1, float y1, void*)
-    {
-        ((rive::RawPath*)rpath)->lineTo(x1 * gInvScale, -y1 * gInvScale);
-    }
-    static void rpath_quad_to(hb_draw_funcs_t*,
-                              void* rpath,
-                              hb_draw_state_t*,
-                              float x1,
-                              float y1,
-                              float x2,
-                              float y2,
-                              void*)
-    {
-        ((rive::RawPath*)rpath)
-            ->quadTo(x1 * gInvScale, -y1 * gInvScale, x2 * gInvScale, -y2 * gInvScale);
-    }
-    static void rpath_cubic_to(hb_draw_funcs_t*,
-                               void* rpath,
-                               hb_draw_state_t*,
-                               float x1,
-                               float y1,
-                               float x2,
-                               float y2,
-                               float x3,
-                               float y3,
-                               void*)
-    {
-        ((rive::RawPath*)rpath)
-            ->cubicTo(x1 * gInvScale,
-                      -y1 * gInvScale,
-                      x2 * gInvScale,
-                      -y2 * gInvScale,
-                      x3 * gInvScale,
-                      -y3 * gInvScale);
-    }
-    static void rpath_close(hb_draw_funcs_t*, void* rpath, hb_draw_state_t*, void*)
-    {
-        ((rive::RawPath*)rpath)->close();
-    }
-}
-
-static rive::RenderFont::LineMetrics make_lmx(hb_font_t* font)
-{
-    // premable on font...
-    hb_ot_font_set_funcs(font);
-    hb_font_set_scale(font, kStdScale, kStdScale);
-
-    hb_font_extents_t extents;
-    hb_font_get_h_extents(font, &extents);
-    return {-extents.ascender * gInvScale, -extents.descender * gInvScale};
-}
-
-HBRenderFont::HBRenderFont(hb_font_t* font) :
-    RenderFont(make_lmx(font)), m_Font(font) // we just take ownership, no need to call reference()
-{
-    m_DrawFuncs = hb_draw_funcs_create();
-    hb_draw_funcs_set_move_to_func(m_DrawFuncs, rpath_move_to, nullptr, nullptr);
-    hb_draw_funcs_set_line_to_func(m_DrawFuncs, rpath_line_to, nullptr, nullptr);
-    hb_draw_funcs_set_quadratic_to_func(m_DrawFuncs, rpath_quad_to, nullptr, nullptr);
-    hb_draw_funcs_set_cubic_to_func(m_DrawFuncs, rpath_cubic_to, nullptr, nullptr);
-    hb_draw_funcs_set_close_path_func(m_DrawFuncs, rpath_close, nullptr, nullptr);
-    hb_draw_funcs_make_immutable(m_DrawFuncs);
-}
-
-HBRenderFont::~HBRenderFont()
-{
-    hb_draw_funcs_destroy(m_DrawFuncs);
-    hb_font_destroy(m_Font);
-}
-
-std::vector<rive::RenderFont::Axis> HBRenderFont::getAxes() const
-{
-    auto face = hb_font_get_face(m_Font);
-    std::vector<rive::RenderFont::Axis> axes;
-
-    const int count = hb_ot_var_get_axis_count(face);
-    if (count > 0)
-    {
-        axes.resize(count);
-
-        hb_ot_var_axis_info_t info;
-        for (int i = 0; i < count; ++i)
-        {
-            unsigned n = 1;
-            hb_ot_var_get_axis_infos(face, i, &n, &info);
-            assert(n == 1);
-            axes[i] = {info.tag, info.min_value, info.default_value, info.max_value};
-            //       printf("[%d] %08X %g %g %g\n", i, info.tag, info.min_value, info.default_value,
-            //       info.max_value);
-        }
-    }
-    return axes;
-}
-
-std::vector<rive::RenderFont::Coord> HBRenderFont::getCoords() const
-{
-    auto axes = this->getAxes();
-    //  const int count = (int)axes.size();
-
-    unsigned length;
-    const float* values = hb_font_get_var_coords_design(m_Font, &length);
-
-    std::vector<rive::RenderFont::Coord> coords(length);
-    for (unsigned i = 0; i < length; ++i)
-    {
-        coords[i] = {axes[i].tag, values[i]};
-    }
-    return coords;
-}
-
-rive::rcp<rive::RenderFont> HBRenderFont::makeAtCoords(rive::Span<const Coord> coords) const
-{
-    AutoSTArray<16, hb_variation_t> vars(coords.size());
-    for (size_t i = 0; i < coords.size(); ++i)
-    {
-        vars[i] = {coords[i].axis, coords[i].value};
-    }
-    auto font = hb_font_create_sub_font(m_Font);
-    hb_font_set_variations(font, vars.data(), (unsigned int)vars.size());
-    return rive::rcp<rive::RenderFont>(new HBRenderFont(font));
-}
-
-rive::RawPath HBRenderFont::getPath(rive::GlyphID glyph) const
-{
-    rive::RawPath rpath;
-    hb_font_get_glyph_shape(m_Font, glyph, m_DrawFuncs, &rpath);
-    return rpath;
-}
-
-///////////////////////////////////////////////////////////
-
-const hb_feature_t gFeatures[] = {
-    {'liga', 1, HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END},
-    {'dlig', 1, HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END},
-    {'kern', 1, HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END},
-};
-constexpr int gNumFeatures = sizeof(gFeatures) / sizeof(gFeatures[0]);
-
-static rive::RenderGlyphRun shape_run(const rive::Unichar text[],
-                                      const rive::RenderTextRun& tr,
-                                      unsigned textOffset)
-{
-    hb_buffer_t* buf = hb_buffer_create();
-    hb_buffer_add_utf32(buf, text, tr.unicharCount, 0, tr.unicharCount);
-
-    hb_buffer_set_direction(buf, HB_DIRECTION_LTR);
-    hb_buffer_set_script(buf, HB_SCRIPT_LATIN);
-    hb_buffer_set_language(buf, hb_language_from_string("en", -1));
-
-    auto hbfont = (HBRenderFont*)tr.font.get();
-    hb_shape(hbfont->m_Font, buf, gFeatures, gNumFeatures);
-
-    unsigned int glyph_count;
-    hb_glyph_info_t* glyph_info = hb_buffer_get_glyph_infos(buf, &glyph_count);
-    hb_glyph_position_t* glyph_pos = hb_buffer_get_glyph_positions(buf, &glyph_count);
-
-    // todo: check for missing glyphs, and perform font-substitution
-
-    rive::RenderGlyphRun gr(glyph_count);
-    gr.font = tr.font;
-    gr.size = tr.size;
-
-    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;
-
-        gr.glyphs[i] = (uint16_t)glyph_info[i].codepoint;
-        gr.textIndices[i] = textOffset + glyph_info[i].cluster;
-        gr.xpos[i] = glyph_pos[i].x_advance * scale;
-    }
-    gr.xpos[glyph_count] = 0; // so the next run can line up snug
-    hb_buffer_destroy(buf);
-    return gr;
-}
-
-static rive::RenderGlyphRun extract_subset(const rive::RenderGlyphRun& orig,
-                                           size_t start,
-                                           size_t end)
-{
-    auto count = end - start;
-    rive::RenderGlyphRun subset(rive::SimpleArray<rive::GlyphID>(&orig.glyphs[start], count),
-                                rive::SimpleArray<uint32_t>(&orig.textIndices[start], count),
-                                rive::SimpleArray<float>(&orig.xpos[start], count));
-    subset.font = std::move(orig.font);
-    subset.size = orig.size;
-    subset.xpos.back() = 0; // since we're now the end of a run
-
-    return subset;
-}
-
-static void perform_fallback(rive::rcp<rive::RenderFont> fallbackFont,
-                             rive::SimpleArrayBuilder<rive::RenderGlyphRun>& gruns,
-                             const rive::Unichar text[],
-                             const rive::RenderGlyphRun& orig)
-{
-    assert(orig.glyphs.size() > 0);
-
-    const size_t count = orig.glyphs.size();
-    size_t startI = 0;
-    while (startI < count)
-    {
-        size_t endI = startI + 1;
-        if (orig.glyphs[startI] == 0)
-        {
-            while (endI < count && orig.glyphs[endI] == 0)
-            {
-                ++endI;
-            }
-            auto textStart = orig.textIndices[startI];
-            auto textCount = orig.textIndices[endI - 1] - textStart + 1;
-            auto tr = rive::RenderTextRun{fallbackFont, orig.size, textCount};
-            gruns.add(shape_run(&text[textStart], tr, textStart));
-        }
-        else
-        {
-            while (endI < count && orig.glyphs[endI] != 0)
-            {
-                ++endI;
-            }
-            gruns.add(extract_subset(orig, startI, endI));
-        }
-        startI = endI;
-    }
-}
-
-rive::SimpleArray<rive::RenderGlyphRun>
-HBRenderFont::onShapeText(rive::Span<const rive::Unichar> text,
-                          rive::Span<const rive::RenderTextRun> truns) const
-{
-    rive::SimpleArrayBuilder<rive::RenderGlyphRun> gruns(truns.size());
-
-    /////////////////
-
-    uint32_t unicharIndex = 0;
-    for (const auto& tr : truns)
-    {
-        auto gr = shape_run(&text[unicharIndex], tr, unicharIndex);
-        unicharIndex += tr.unicharCount;
-
-        auto end = gr.glyphs.end();
-        auto iter = std::find(gr.glyphs.begin(), end, 0);
-        if (!gFallbackProc || iter == end)
-        {
-            gruns.add(std::move(gr));
-        }
-        else
-        {
-            // found at least 1 zero in glyphs, so need to perform font-fallback
-            size_t index = iter - gr.glyphs.begin();
-            rive::Unichar missing = text[gr.textIndices[index]];
-            // todo: consider sending more chars if that helps choose a font
-            auto fallback = gFallbackProc({&missing, 1});
-            if (fallback)
-            {
-                perform_fallback(fallback, gruns, text.data(), gr);
-            }
-            else
-            {
-                gruns.add(std::move(gr)); // oh well, just keep the missing glyphs
-            }
-        }
-    }
-
-    // now turn the advances (widths) we stored in xpos[] into actual x-positions
-    float pos = 0;
-    for (auto& gr : gruns)
-    {
-        for (auto& xp : gr.xpos)
-        {
-            float adv = xp;
-            xp = pos;
-            pos += adv;
-        }
-    }
-    return std::move(gruns);
-}
-
-#endif
\ No newline at end of file
diff --git a/tess/build/macosx/build_tess.sh b/tess/build/macosx/build_tess.sh
index 0054e3d..13df959 100755
--- a/tess/build/macosx/build_tess.sh
+++ b/tess/build/macosx/build_tess.sh
@@ -51,7 +51,7 @@
     fi
 done
 
-$PREMAKE --file=./premake5_tess.lua gmake2 --graphics=$GRAPHICS --with_rive_tools
+$PREMAKE --scripts=../../build --file=./premake5_tess.lua gmake2 --graphics=$GRAPHICS --with_rive_tools
 
 for var in "$@"; do
     if [[ $var = "clean" ]]; then
diff --git a/test/assets/IBMPlexSansArabic-Regular.ttf b/test/assets/IBMPlexSansArabic-Regular.ttf
new file mode 100644
index 0000000..da3eba1
--- /dev/null
+++ b/test/assets/IBMPlexSansArabic-Regular.ttf
Binary files differ
diff --git a/test/assets/NotoSansArabic-VariableFont_wdth,wght.ttf b/test/assets/NotoSansArabic-VariableFont_wdth,wght.ttf
new file mode 100644
index 0000000..0356020
--- /dev/null
+++ b/test/assets/NotoSansArabic-VariableFont_wdth,wght.ttf
Binary files differ
diff --git a/test/line_break_test.cpp b/test/line_break_test.cpp
index da18244..ecde0ac 100644
--- a/test/line_break_test.cpp
+++ b/test/line_break_test.cpp
@@ -4,17 +4,16 @@
 
 #include <rive/simple_array.hpp>
 #include <catch.hpp>
-#include <rive/render_text.hpp>
-#include <rive/text/renderfont_hb.hpp>
-#include <rive/text/line_breaker.hpp>
+#include <rive/text.hpp>
+#include <rive/text/font_hb.hpp>
 #include "utils/rive_utf.hpp"
 
 using namespace rive;
 
-static rive::RenderTextRun append(std::vector<rive::Unichar>* unichars,
-                                  rive::rcp<rive::RenderFont> font,
-                                  float size,
-                                  const char text[])
+static rive::TextRun append(std::vector<rive::Unichar>* unichars,
+                            rive::rcp<rive::Font> font,
+                            float size,
+                            const char text[])
 {
     const uint8_t* ptr = (const uint8_t*)text;
     uint32_t n = 0;
@@ -23,10 +22,10 @@
         unichars->push_back(rive::UTF::NextUTF8(&ptr));
         n += 1;
     }
-    return {std::move(font), size, n};
+    return {std::move(font), size, n, 0};
 }
 
-static rcp<RenderFont> loadFont(const char* filename)
+static rcp<Font> loadFont(const char* filename)
 {
     FILE* fp = fopen("../../test/assets/RobotoFlex.ttf", "rb");
     REQUIRE(fp != nullptr);
@@ -38,7 +37,7 @@
     REQUIRE(fread(bytes.data(), 1, length, fp) == length);
     fclose(fp);
 
-    return HBRenderFont::Decode(bytes);
+    return HBFont::Decode(bytes);
 }
 
 TEST_CASE("line breaker separates words", "[line break]")
@@ -47,13 +46,15 @@
     REQUIRE(font != nullptr);
 
     // one two⏎ three
-    std::vector<rive::RenderTextRun> truns;
+    std::vector<rive::TextRun> truns;
     std::vector<rive::Unichar> unichars;
     truns.push_back(append(&unichars, font, 32.0f, "one two three"));
 
-    auto shape = font->shapeText(unichars, truns);
-    REQUIRE(shape.size() == 1);
-    auto run = shape.front();
+    auto paragraphs = font->shapeText(unichars, truns);
+    REQUIRE(paragraphs.size() == 1);
+    const auto& paragraph = paragraphs.front();
+    REQUIRE(paragraph.runs.size() == 1);
+    const auto& run = paragraph.runs.front();
     REQUIRE(run.breaks.size() == 6);
     REQUIRE(run.breaks[0] == 0);
     REQUIRE(run.breaks[1] == 3);
@@ -68,15 +69,18 @@
     auto font = loadFont("../../test/assets/RobotoFlex.ttf");
     REQUIRE(font != nullptr);
 
-    std::vector<rive::RenderTextRun> truns;
+    std::vector<rive::TextRun> truns;
     std::vector<rive::Unichar> unichars;
     truns.push_back(append(&unichars, font, 32.0f, "one two thr"));
     truns.push_back(append(&unichars, font, 60.0f, "ee four"));
 
-    auto shape = font->shapeText(unichars, truns);
-    REQUIRE(shape.size() == 2);
+    auto paragraphs = font->shapeText(unichars, truns);
+    REQUIRE(paragraphs.size() == 1);
+    const auto& paragraph = paragraphs.front();
+
+    REQUIRE(paragraph.runs.size() == 2);
     {
-        auto run = shape.front();
+        const auto& run = paragraph.runs.front();
         REQUIRE(run.breaks.size() == 5);
         REQUIRE(run.breaks[0] == 0);
         REQUIRE(run.breaks[1] == 3);
@@ -85,7 +89,7 @@
         REQUIRE(run.breaks[4] == 8);
     }
     {
-        auto run = shape.back();
+        const auto& run = paragraph.runs.back();
         REQUIRE(run.breaks.size() == 3);
         REQUIRE(run.breaks[0] == 2);
         REQUIRE(run.breaks[1] == 3);
@@ -98,15 +102,16 @@
     auto font = loadFont("../../test/assets/RobotoFlex.ttf");
     REQUIRE(font != nullptr);
 
-    std::vector<rive::RenderTextRun> truns;
+    std::vector<rive::TextRun> truns;
     std::vector<rive::Unichar> unichars;
     truns.push_back(append(&unichars, font, 32.0f, "one two thr"));
-    truns.push_back(append(&unichars, font, 60.0f, "ee\n four"));
+    truns.push_back(append(&unichars, font, 60.0f, "ee\u2028 four"));
 
-    auto shape = font->shapeText(unichars, truns);
-    REQUIRE(shape.size() == 2);
+    auto paragraphs = font->shapeText(unichars, truns);
+    const auto& paragraph = paragraphs.front();
+    REQUIRE(paragraph.runs.size() == 2);
     {
-        auto run = shape.front();
+        const auto& run = paragraph.runs.front();
         REQUIRE(run.breaks.size() == 5);
         REQUIRE(run.breaks[0] == 0);
         REQUIRE(run.breaks[1] == 3);
@@ -115,7 +120,7 @@
         REQUIRE(run.breaks[4] == 8);
     }
     {
-        auto run = shape.back();
+        const auto& run = paragraph.runs.back();
         REQUIRE(run.breaks.size() == 5);
         REQUIRE(run.breaks[0] == 2);
         REQUIRE(run.breaks[1] == 2);
@@ -131,42 +136,43 @@
     REQUIRE(font != nullptr);
 
     // one two⏎ three
-    std::vector<rive::RenderTextRun> truns;
+    std::vector<rive::TextRun> truns;
     std::vector<rive::Unichar> unichars;
     truns.push_back(append(&unichars, font, 32.0f, "one two three"));
 
-    auto shape = font->shapeText(unichars, truns);
-    REQUIRE(shape.size() == 1);
-    auto run = shape.front();
+    auto paragraphs = font->shapeText(unichars, truns);
+    REQUIRE(paragraphs.size() == 1);
+    const auto& paragraph = paragraphs.front();
+    REQUIRE(paragraph.runs.size() == 1);
 
     // at 194 everything fits in one line
     {
-        auto lines = RenderGlyphLine::BreakLines(shape, 194.0f, RenderTextAlign::left);
+        auto lines = GlyphLine::BreakLines(paragraph.runs, 194.0f);
         REQUIRE(lines.size() == 1);
         auto line = lines.back();
-        REQUIRE(line.startRun == 0);
-        REQUIRE(line.startIndex == 0);
-        REQUIRE(line.endRun == 0);
-        REQUIRE(line.endIndex == 13);
+        REQUIRE(line.startRunIndex == 0);
+        REQUIRE(line.startGlyphIndex == 0);
+        REQUIRE(line.endRunIndex == 0);
+        REQUIRE(line.endGlyphIndex == 13);
     }
     // at 191 "three" should pop to second line
     {
-        auto lines = RenderGlyphLine::BreakLines(shape, 191.0f, RenderTextAlign::left);
+        auto lines = GlyphLine::BreakLines(paragraph.runs, 191.0f);
         REQUIRE(lines.size() == 2);
         {
             auto line = lines.front();
-            REQUIRE(line.startRun == 0);
-            REQUIRE(line.startIndex == 0);
-            REQUIRE(line.endRun == 0);
-            REQUIRE(line.endIndex == 7);
+            REQUIRE(line.startRunIndex == 0);
+            REQUIRE(line.startGlyphIndex == 0);
+            REQUIRE(line.endRunIndex == 0);
+            REQUIRE(line.endGlyphIndex == 7);
         }
 
         {
             auto line = lines.back();
-            REQUIRE(line.startRun == 0);
-            REQUIRE(line.startIndex == 8);
-            REQUIRE(line.endRun == 0);
-            REQUIRE(line.endIndex == 13);
+            REQUIRE(line.startRunIndex == 0);
+            REQUIRE(line.startGlyphIndex == 8);
+            REQUIRE(line.endRunIndex == 0);
+            REQUIRE(line.endGlyphIndex == 13);
         }
     }
 }
@@ -177,51 +183,52 @@
     REQUIRE(font != nullptr);
 
     // one two⏎ three
-    std::vector<rive::RenderTextRun> truns;
+    std::vector<rive::TextRun> truns;
     std::vector<rive::Unichar> unichars;
     truns.push_back(append(&unichars, font, 32.0f, "ab"));
 
-    auto shape = font->shapeText(unichars, truns);
-    REQUIRE(shape.size() == 1);
-    auto run = shape.front();
+    auto paragraphs = font->shapeText(unichars, truns);
+    REQUIRE(paragraphs.size() == 1);
+    const auto& paragraph = paragraphs.front();
+    REQUIRE(paragraph.runs.size() == 1);
 
     {
-        auto lines = RenderGlyphLine::BreakLines(shape, 17.0f, RenderTextAlign::left);
+        auto lines = GlyphLine::BreakLines(paragraph.runs, 17.0f);
         REQUIRE(lines.size() == 2);
         {
             auto line = lines.front();
-            REQUIRE(line.startRun == 0);
-            REQUIRE(line.startIndex == 0);
-            REQUIRE(line.endRun == 0);
-            REQUIRE(line.endIndex == 1);
+            REQUIRE(line.startRunIndex == 0);
+            REQUIRE(line.startGlyphIndex == 0);
+            REQUIRE(line.endRunIndex == 0);
+            REQUIRE(line.endGlyphIndex == 1);
         }
 
         {
             auto line = lines.back();
-            REQUIRE(line.startRun == 0);
-            REQUIRE(line.startIndex == 1);
-            REQUIRE(line.endRun == 0);
-            REQUIRE(line.endIndex == 2);
+            REQUIRE(line.startRunIndex == 0);
+            REQUIRE(line.startGlyphIndex == 1);
+            REQUIRE(line.endRunIndex == 0);
+            REQUIRE(line.endGlyphIndex == 2);
         }
     }
     // Test that it also handles 0 width.
     {
-        auto lines = RenderGlyphLine::BreakLines(shape, 0.0f, RenderTextAlign::left);
+        auto lines = GlyphLine::BreakLines(paragraph.runs, 0.0f);
         REQUIRE(lines.size() == 2);
         {
             auto line = lines.front();
-            REQUIRE(line.startRun == 0);
-            REQUIRE(line.startIndex == 0);
-            REQUIRE(line.endRun == 0);
-            REQUIRE(line.endIndex == 1);
+            REQUIRE(line.startRunIndex == 0);
+            REQUIRE(line.startGlyphIndex == 0);
+            REQUIRE(line.endRunIndex == 0);
+            REQUIRE(line.endGlyphIndex == 1);
         }
 
         {
             auto line = lines.back();
-            REQUIRE(line.startRun == 0);
-            REQUIRE(line.startIndex == 1);
-            REQUIRE(line.endRun == 0);
-            REQUIRE(line.endIndex == 2);
+            REQUIRE(line.startRunIndex == 0);
+            REQUIRE(line.startGlyphIndex == 1);
+            REQUIRE(line.endRunIndex == 0);
+            REQUIRE(line.endGlyphIndex == 2);
         }
     }
 }
@@ -232,16 +239,78 @@
     REQUIRE(font != nullptr);
 
     // one two⏎ three
-    std::vector<rive::RenderTextRun> truns;
+    std::vector<rive::TextRun> truns;
     std::vector<rive::Unichar> unichars;
-    truns.push_back(append(&unichars, font, 32.0f, "hello look\nhere"));
+    truns.push_back(append(&unichars, font, 32.0f, "hello look\u2028here"));
 
-    auto shape = font->shapeText(unichars, truns);
-    REQUIRE(shape.size() == 1);
-    auto run = shape.front();
-
+    auto paragraphs = font->shapeText(unichars, truns);
+    REQUIRE(paragraphs.size() == 1);
+    const auto& paragraph = paragraphs.front();
+    REQUIRE(paragraph.runs.size() == 1);
     {
-        auto lines = RenderGlyphLine::BreakLines(shape, 300.0f, RenderTextAlign::left);
+        auto lines = GlyphLine::BreakLines(paragraph.runs, 300.0f);
         REQUIRE(lines.size() == 2);
     }
 }
+
+TEST_CASE("shaper separates paragraphs", "[shaper]")
+{
+    auto font = loadFont("../../test/assets/RobotoFlex.ttf");
+    REQUIRE(font != nullptr);
+
+    // one two⏎ three
+    std::vector<rive::TextRun> truns;
+    std::vector<rive::Unichar> unichars;
+    truns.push_back(append(&unichars, font, 32.0f, "hello look\u2028here\nsecond paragraph"));
+
+    auto paragraphs = font->shapeText(unichars, truns);
+    REQUIRE(paragraphs.size() == 2);
+    {
+        const auto& paragraph = paragraphs.front();
+        REQUIRE(paragraph.runs.size() == 1);
+        REQUIRE(paragraph.baseDirection == rive::TextDirection::ltr);
+
+        auto lines = GlyphLine::BreakLines(paragraph.runs, 300.0f);
+        REQUIRE(lines.size() == 2);
+    }
+    {
+        const auto& paragraph = paragraphs.back();
+        REQUIRE(paragraph.runs.size() == 1);
+        REQUIRE(paragraph.baseDirection == rive::TextDirection::ltr);
+
+        auto lines = GlyphLine::BreakLines(paragraph.runs, 300.0f);
+        REQUIRE(lines.size() == 1);
+    }
+}
+
+TEST_CASE("shaper handles RTL", "[shaper]")
+{
+    auto font = loadFont("../../test/assets/IBMPlexSansArabic-Regular.ttf");
+    REQUIRE(font != nullptr);
+
+    // one two⏎ three
+    std::vector<rive::TextRun> truns;
+    std::vector<rive::Unichar> unichars;
+    truns.push_back(append(&unichars, font, 32.0f, "لمفاتيح ABC DEF"));
+
+    auto paragraphs = font->shapeText(unichars, truns);
+    REQUIRE(paragraphs.size() == 1);
+    const auto& paragraph = paragraphs.front();
+    REQUIRE(paragraph.baseDirection == rive::TextDirection::rtl);
+    {
+        auto lines = GlyphLine::BreakLines(paragraph.runs, 300.0f);
+        REQUIRE(lines.size() == 1);
+    }
+    {
+        auto lines = GlyphLine::BreakLines(paragraph.runs, 196.0f);
+        REQUIRE(lines.size() == 2);
+
+        // The second line should start with DEF as it's the first word to wrap.
+        const auto& line = lines.back();
+        const auto& run = paragraph.runs[line.startRunIndex];
+        auto index = run.textIndices[line.startGlyphIndex];
+        REQUIRE(unichars[index] == 'D');
+        REQUIRE(unichars[index + 1] == 'E');
+        REQUIRE(unichars[index + 2] == 'F');
+    }
+}
diff --git a/test/simple_array_test.cpp b/test/simple_array_test.cpp
index 78e7463..3263ba7 100644
--- a/test/simple_array_test.cpp
+++ b/test/simple_array_test.cpp
@@ -4,7 +4,7 @@
 
 #include <rive/simple_array.hpp>
 #include <catch.hpp>
-#include <rive/render_text.hpp>
+#include <rive/text.hpp>
 
 using namespace rive;
 
diff --git a/viewer/build/macosx/build_viewer.sh b/viewer/build/macosx/build_viewer.sh
index dbb4803..9dcac0d 100755
--- a/viewer/build/macosx/build_viewer.sh
+++ b/viewer/build/macosx/build_viewer.sh
@@ -46,12 +46,6 @@
     popd
 fi
 
-if [[ ! -d "$DEPENDENCIES/harfbuzz" ]]; then
-    pushd $DEPENDENCIES_SCRIPTS
-    ./get_harfbuzz.sh
-    popd
-fi
-
 if [ $RENDERER = "skia" ]; then
     pushd ../../../skia/renderer/build/macosx
     ./build_skia_renderer.sh text $@
@@ -68,7 +62,7 @@
 
 pushd ..
 
-$PREMAKE --file=./premake5_viewer.lua gmake2 --graphics=$GRAPHICS --renderer=$RENDERER --with_rive_tools --with_rive_text
+$PREMAKE --scripts=../../build --file=./premake5_viewer.lua gmake2 --graphics=$GRAPHICS --renderer=$RENDERER --with_rive_tools --with_rive_text
 
 for var in "$@"; do
     if [[ $var = "clean" ]]; then
diff --git a/viewer/build/premake5_viewer.lua b/viewer/build/premake5_viewer.lua
index a7d79a0..d01b75e 100644
--- a/viewer/build/premake5_viewer.lua
+++ b/viewer/build/premake5_viewer.lua
@@ -12,7 +12,6 @@
 skia = dependencies .. '/skia'
 libpng = dependencies .. '/libpng'
 
-dofile(path.join(path.getabsolute(dependencies) .. '/../..', 'premake5_harfbuzz.lua'))
 if _OPTIONS.renderer == 'tess' then
     dofile(path.join(path.getabsolute(dependencies) .. '/../..', 'premake5_libpng.lua'))
     dofile(path.join(path.getabsolute(rive_tess) .. '/build', 'premake5_tess.lua'))
@@ -38,16 +37,17 @@
     includedirs {
         '../include',
         rive .. '/include',
-        rive .. '/skia/renderer/include', -- for renderfont backends
+        rive .. '/skia/renderer/include', -- for font backends
         dependencies,
         dependencies .. '/sokol',
-        dependencies .. '/imgui',
-        dependencies .. '/harfbuzz/src'
+        dependencies .. '/imgui'
     }
 
     links {
         'rive',
-        'rive_harfbuzz'
+        'rive_harfbuzz',
+        -- 'rive_fribidi'
+        'rive_sheenbidi'
     }
 
     libdirs {
@@ -57,7 +57,6 @@
     files {
         '../src/**.cpp',
         rive .. '/utils/**.cpp',
-        rive .. '/skia/renderer/src/renderfont_coretext.cpp',
         dependencies .. '/imgui/imgui.cpp',
         dependencies .. '/imgui/imgui_widgets.cpp',
         dependencies .. '/imgui/imgui_tables.cpp',
diff --git a/viewer/include/viewer/viewer_content.hpp b/viewer/include/viewer/viewer_content.hpp
index e607929..ad7ba60 100644
--- a/viewer/include/viewer/viewer_content.hpp
+++ b/viewer/include/viewer/viewer_content.hpp
@@ -14,7 +14,7 @@
 {
 class Renderer;
 class Factory;
-class RenderFont;
+class Font;
 } // namespace rive
 
 class ViewerContent
@@ -65,7 +65,7 @@
     static rive::Factory* RiveFactory();
 
     // Abstracts which font backend is currently used.
-    static rive::rcp<rive::RenderFont> DecodeFont(rive::Span<const uint8_t>);
+    static rive::rcp<rive::Font> DecodeFont(rive::Span<const uint8_t>);
 };
 
 #endif
diff --git a/viewer/include/viewer/viewer_host.hpp b/viewer/include/viewer/viewer_host.hpp
index bf64439..1993cfe 100644
--- a/viewer/include/viewer/viewer_host.hpp
+++ b/viewer/include/viewer/viewer_host.hpp
@@ -7,7 +7,7 @@
 
 #include "rive/factory.hpp"
 #include "rive/renderer.hpp"
-#include "rive/render_text.hpp"
+#include "rive/text.hpp"
 
 #include "sokol_gfx.h"
 
diff --git a/viewer/src/viewer_content/text_content.cpp b/viewer/src/viewer_content/text_content.cpp
index 44e70f4..d054482 100644
--- a/viewer/src/viewer_content/text_content.cpp
+++ b/viewer/src/viewer_content/text_content.cpp
@@ -8,12 +8,12 @@
 #include "rive/math/raw_path.hpp"
 #include "rive/factory.hpp"
 #include "rive/refcnt.hpp"
-#include "rive/render_text.hpp"
-#include "rive/text/line_breaker.hpp"
+#include "rive/text.hpp"
+#include <algorithm>
 
-using RenderFontTextRuns = std::vector<rive::RenderTextRun>;
-using RenderFontGlyphRuns = rive::SimpleArray<rive::RenderGlyphRun>;
-using RenderFontFactory = rive::rcp<rive::RenderFont> (*)(const rive::Span<const uint8_t>);
+using FontTextRuns = std::vector<rive::TextRun>;
+using FontGlyphRuns = rive::SimpleArray<rive::GlyphRun>;
+using FontFactory = rive::rcp<rive::Font> (*)(const rive::Span<const uint8_t>);
 
 static bool ws(rive::Unichar c) { return c <= ' '; }
 
@@ -41,60 +41,95 @@
     return breaks;
 }
 
-static void drawrun(rive::Factory* factory,
-                    rive::Renderer* renderer,
-                    const rive::RenderGlyphRun& run,
-                    unsigned startIndex,
-                    unsigned endIndex,
-                    rive::Vec2D origin)
+static float drawrun(rive::Factory* factory,
+                     rive::Renderer* renderer,
+                     const rive::GlyphRun& run,
+                     unsigned startIndex,
+                     unsigned endIndex,
+                     rive::Vec2D origin)
 {
     auto font = run.font.get();
     const auto scale = rive::Mat2D::fromScale(run.size, run.size);
     auto paint = factory->makeRenderPaint();
     paint->color(0xFFFFFFFF);
 
+    float x = origin.x;
     assert(startIndex >= 0 && endIndex <= run.glyphs.size());
-    for (size_t i = startIndex; i < endIndex; ++i)
+    int i, end, inc;
+    if (run.dir == rive::TextDirection::rtl)
     {
-        auto trans = rive::Mat2D::fromTranslate(origin.x + run.xpos[i], origin.y);
+        i = endIndex - 1;
+        end = startIndex - 1;
+        inc = -1;
+    }
+    else
+    {
+        i = startIndex;
+        end = endIndex;
+        inc = 1;
+    }
+    while (i != end)
+    {
+        auto trans = rive::Mat2D::fromTranslate(x, origin.y);
+        x += run.advances[i];
         auto rawpath = font->getPath(run.glyphs[i]);
         rawpath.transformInPlace(trans * scale);
         auto path = factory->makeRenderPath(rawpath, rive::FillRule::nonZero);
         renderer->drawPath(path.get(), paint.get());
+        i += inc;
     }
+    return x;
 }
 
-static void drawpara(rive::Factory* factory,
-                     rive::Renderer* renderer,
-                     rive::Span<const rive::RenderGlyphLine> lines,
-                     rive::Span<const rive::RenderGlyphRun> runs,
-                     rive::Vec2D origin)
+static float drawpara(rive::Factory* factory,
+                      rive::Renderer* renderer,
+                      const rive::Paragraph& paragraph,
+                      const rive::SimpleArray<rive::GlyphLine>& lines,
+                      rive::Vec2D origin)
 {
+
     for (const auto& line : lines)
     {
-        const float x0 = runs[line.startRun].xpos[line.startIndex];
-        int startGIndex = line.startIndex;
-        for (int runIndex = line.startRun; runIndex <= line.endRun; ++runIndex)
+
+        float x = line.startX + origin.x;
+        int runIndex, endRun, runInc;
+        if (paragraph.baseDirection == rive::TextDirection::rtl)
         {
-            const auto& run = runs[runIndex];
-            int endGIndex = runIndex == line.endRun ? line.endIndex : run.glyphs.size();
-            drawrun(factory,
-                    renderer,
-                    run,
-                    startGIndex,
-                    endGIndex,
-                    {origin.x - x0 + line.startX, origin.y + line.baseline});
-            startGIndex = 0;
+            runIndex = line.endRunIndex;
+            endRun = line.startRunIndex - 1;
+            runInc = -1;
+        }
+        else
+        {
+            runIndex = line.startRunIndex;
+            endRun = line.endRunIndex + 1;
+            runInc = 1;
+        }
+        while (runIndex != endRun)
+        {
+            const auto& run = paragraph.runs[runIndex];
+            int startGIndex = runIndex == line.startRunIndex ? line.startGlyphIndex : 0;
+            int endGIndex = runIndex == line.endRunIndex ? line.endGlyphIndex : run.glyphs.size();
+
+            x = drawrun(factory,
+                        renderer,
+                        run,
+                        startGIndex,
+                        endGIndex,
+                        {x, origin.y + line.baseline});
+
+            runIndex += runInc;
         }
     }
+    return origin.y + lines.back().bottom;
 }
 
 ////////////////////////////////////////////////////////////////////////////////////
 
 #ifdef RIVE_USING_HAFBUZZ_FONTS
-static rive::rcp<rive::RenderFont> load_fallback_font(rive::Span<const rive::Unichar> missing)
+static rive::rcp<rive::Font> load_fallback_font(rive::Span<const rive::Unichar> missing)
 {
-    static rive::rcp<rive::RenderFont> gFallbackFont;
+    static rive::rcp<rive::Font> gFallbackFont;
 
     printf("missing chars:");
     for (auto m : missing)
@@ -121,7 +156,7 @@
         fclose(fp);
 
         assert(bytesRead == size);
-        gFallbackFont = HBRenderFont::Decode(bytes);
+        gFallbackFont = HBFont::Decode(bytes);
     }
     return gFallbackFont;
 }
@@ -147,10 +182,10 @@
     renderer->drawPath(path.get(), paint.get());
 }
 
-static rive::RenderTextRun append(std::vector<rive::Unichar>* unichars,
-                                  rive::rcp<rive::RenderFont> font,
-                                  float size,
-                                  const char text[])
+static rive::TextRun append(std::vector<rive::Unichar>* unichars,
+                            rive::rcp<rive::Font> font,
+                            float size,
+                            const char text[])
 {
     const uint8_t* ptr = (const uint8_t*)text;
     uint32_t n = 0;
@@ -167,15 +202,15 @@
     std::vector<rive::Unichar> m_unichars;
     std::vector<int> m_breaks;
 
-    std::vector<RenderFontGlyphRuns> m_gruns;
+    rive::SimpleArray<rive::Paragraph> m_paragraphs;
     rive::Mat2D m_xform;
     float m_width = 300;
     bool m_autoWidth = false;
     int m_align = 0;
 
-    RenderFontTextRuns make_truns(RenderFontFactory fact)
+    FontTextRuns make_truns(FontFactory fact)
     {
-        auto loader = [fact](const char filename[]) -> rive::rcp<rive::RenderFont> {
+        auto loader = [fact](const char filename[]) -> rive::rcp<rive::Font> {
             auto bytes = ViewerContent::LoadFile(filename);
             if (bytes.size() == 0)
             {
@@ -188,27 +223,55 @@
         const char* fontFiles[] = {
             "../../../test/assets/RobotoFlex.ttf",
             "../../../test/assets/LibreBodoni-Italic-VariableFont_wght.ttf",
+            "../../../test/assets/IBMPlexSansArabic-Regular.ttf",
         };
 
         auto font0 = loader(fontFiles[0]);
-        auto font1 = loader(fontFiles[1]);
-        assert(font0);
-        assert(font1);
+        // auto font1 = loader(fontFiles[1]);
+        auto font2 = loader(fontFiles[2]);
+        // assert(font0);
+        // assert(font1);
+        assert(font2);
 
-        rive::RenderFont::Coord c1 = {'wght', 100.f}, c2 = {'wght', 800.f};
+        rive::Font::Coord c1 = {'wght', 100.f}, c2 = {'wght', 800.f};
 
-        RenderFontTextRuns truns;
+        FontTextRuns truns;
 
-        // truns.push_back(append(&m_unichars, font0->makeAtCoord(c2), 60, "U"));
+        // truns.push_back(
+        //     append(&m_unichars, font0->makeAtCoord(c2), 32, "No one ever left alive in "));
+        // truns.push_back(append(&m_unichars, font0->makeAtCoord(c2), 54, "nineteen hundred"));
         // truns.push_back(append(&m_unichars, font0->makeAtCoord(c1), 30, "ne漢字asy"));
-        // truns.push_back(append(&m_unichars, font1, 30, " fits the \ncRown"));
+
+        // truns.push_back(append(&m_unichars, font2, 30, " its the 
"));
+        // truns.push_back(append(&m_unichars, font2, 40, "cRown"));
+        // truns.push_back(append(&m_unichars, font2, 30, "a b c d"));
+
         // truns.push_back(append(&m_unichars, font1->makeAtCoord(c1), 30, " that often"));
         // truns.push_back(append(&m_unichars, font0, 30, " lies the head."));
         // truns.push_back(append(&m_unichars, font0->makeAtCoord(c2), 60, "hi one two"));
 
-        truns.push_back(append(&m_unichars, font0, 32.0f, "one two three"));
-        truns.push_back(append(&m_unichars, font0, 42.0f, "OT\nHER\n"));
-        truns.push_back(append(&m_unichars, font1, 62.0f, "VERY LARGE FONT HERE"));
+        // truns.push_back(append(&m_unichars, font2, 32.0f, "في 10-12 آذار 1997 بمدينة"));
+        // truns.push_back(append(&m_unichars, font2, 32.0f, "في 10-12 آذار 1997 بمدينة"));
+
+        truns.push_back(append(&m_unichars,
+                               font2,
+                               32.0f,
+                               // clang-format off
+                               "لمفاتيح ABC DEF"));
+        //    "لمفاتيح ABC DEF
في 10-12 آذار 1997 بمدينة\nabc def ghi jkl mnop\nلكن لا بد أن أوضح لك
+        //    أن كل"));
+        // "hello look\u2028here\nsecond paragraph"));
+
+        // clang-format on
+
+        // truns.push_back(append(&m_unichars, font2, 32.0f, "abc
def\nghijkl"));
+
+        // truns.push_back(append(&m_unichars, font2, 32.0f, "DEF
دنة"));
+
+        // truns.push_back(append(&m_unichars, font2, 32.0f, "AفيB"));
+
+        // truns.push_back(append(&m_unichars, font0, 42.0f, "OT\nHER\n"));
+        // truns.push_back(append(&m_unichars, font1, 62.0f, "VERY LARGE FONT HERE"));
         // truns.push_back(
         //     append(&m_unichars, font0, 52.0f, "one two three\n\n\nfour five six seven"));
 
@@ -224,21 +287,47 @@
     TextContent()
     {
         auto truns = this->make_truns(ViewerContent::DecodeFont);
-        m_gruns.push_back(truns[0].font->shapeText(m_unichars, truns));
+        m_paragraphs = truns[0].font->shapeText(m_unichars, truns);
 
         m_xform = rive::Mat2D::fromTranslate(10, 0) * rive::Mat2D::fromScale(3, 3);
     }
 
-    void draw(rive::Renderer* renderer, float width, const RenderFontGlyphRuns& gruns)
+    void draw(rive::Renderer* renderer,
+              float width,
+              const rive::SimpleArray<rive::Paragraph>& paragraphs)
     {
+
         renderer->save();
         renderer->transform(m_xform);
+        float y = 0.0f;
+        float paragraphWidth = m_autoWidth ? -1.0f : width;
 
-        auto lines = rive::RenderGlyphLine::BreakLines(gruns,
-                                                       m_autoWidth ? -1.0f : width,
-                                                       (rive::RenderTextAlign)m_align);
+        rive::SimpleArray<rive::SimpleArray<rive::GlyphLine>>* lines =
+            new rive::SimpleArray<rive::SimpleArray<rive::GlyphLine>>(paragraphs.size());
+        rive::SimpleArray<rive::SimpleArray<rive::GlyphLine>>& linesRef = *lines;
+        size_t paragraphIndex = 0;
+        for (auto& para : paragraphs)
+        {
+            linesRef[paragraphIndex] =
+                rive::GlyphLine::BreakLines(para.runs, m_autoWidth ? -1.0f : width);
 
-        drawpara(RiveFactory(), renderer, lines, gruns, {0, 0});
+            if (m_autoWidth)
+            {
+                paragraphWidth =
+                    std::max(paragraphWidth,
+                             rive::GlyphLine::ComputeMaxWidth(linesRef[paragraphIndex], para.runs));
+            }
+        }
+        paragraphIndex = 0;
+        for (auto& para : paragraphs)
+        {
+            rive::SimpleArray<rive::GlyphLine>& lines = linesRef[paragraphIndex++];
+            rive::GlyphLine::ComputeLineSpacing(lines,
+                                                para.runs,
+                                                paragraphWidth,
+                                                (rive::TextAlign)m_align);
+            y = drawpara(RiveFactory(), renderer, para, lines, {0, y}) + 20.0f;
+        }
         if (!m_autoWidth)
         {
             draw_line(RiveFactory(), renderer, width);
@@ -249,11 +338,7 @@
 
     void handleDraw(rive::Renderer* renderer, double) override
     {
-        for (auto& grun : m_gruns)
-        {
-            this->draw(renderer, m_width, grun);
-            renderer->translate(1200, 0);
-        }
+        this->draw(renderer, m_width, m_paragraphs);
     }
 
     void handleResize(int width, int height) override {}
diff --git a/viewer/src/viewer_content/textpath_content.cpp b/viewer/src/viewer_content/textpath_content.cpp
index f511a63..0b78ae6 100644
--- a/viewer/src/viewer_content/textpath_content.cpp
+++ b/viewer/src/viewer_content/textpath_content.cpp
@@ -1,3 +1,4 @@
+
 /*
  * Copyright 2022 Rive
  */
@@ -8,18 +9,16 @@
 #include "rive/math/raw_path.hpp"
 #include "rive/refcnt.hpp"
 #include "rive/factory.hpp"
-#include "rive/render_text.hpp"
+#include "rive/text.hpp"
 #include "rive/math/contour_measure.hpp"
-#include "rive/text/line_breaker.hpp"
 
 using namespace rive;
+#if 0
+using FontTextRuns = std::vector<TextRun>;
+using FontGlyphRuns = rive::SimpleArray<GlyphRun>;
+using FontFactory = rcp<Font> (*)(const Span<const uint8_t>);
 
-using RenderFontTextRuns = std::vector<RenderTextRun>;
-using RenderFontGlyphRuns = rive::SimpleArray<RenderGlyphRun>;
-using RenderFontFactory = rcp<RenderFont> (*)(const Span<const uint8_t>);
-
-template <typename Handler>
-void visit(const Span<RenderGlyphRun>& gruns, Vec2D origin, Handler proc)
+template <typename Handler> void visit(const Span<GlyphRun>& gruns, Vec2D origin, Handler proc)
 {
     for (const auto& gr : gruns)
     {
@@ -99,8 +98,7 @@
     fill_rect(renderer, {p.x - r, p.y - r, p.x + r, p.y + r}, paint);
 }
 
-static RenderTextRun
-append(std::vector<Unichar>* unichars, rcp<RenderFont> font, float size, const char text[])
+static TextRun append(std::vector<Unichar>* unichars, rcp<Font> font, float size, const char text[])
 {
     const uint8_t* ptr = (const uint8_t*)text;
     uint32_t n = 0;
@@ -115,7 +113,7 @@
 class TextPathContent : public ViewerContent
 {
     std::vector<Unichar> m_unichars;
-    RenderFontGlyphRuns m_gruns;
+    FontGlyphRuns m_gruns;
     std::unique_ptr<RenderPaint> m_paint;
     AABB m_gbounds;
 
@@ -133,9 +131,9 @@
           m_windowWidth = 1, // %
         m_windowOffset = 0;  // %
 
-    RenderFontTextRuns make_truns(RenderFontFactory fact)
+    FontTextRuns make_truns(FontFactory fact)
     {
-        auto loader = [fact](const char filename[]) -> rcp<RenderFont> {
+        auto loader = [fact](const char filename[]) -> rcp<Font> {
             auto bytes = ViewerContent::LoadFile(filename);
             if (bytes.size() == 0)
             {
@@ -155,9 +153,9 @@
         assert(font0);
         assert(font1);
 
-        RenderFont::Coord c1 = {'wght', 100.f}, c2 = {'wght', 800.f};
+        Font::Coord c1 = {'wght', 100.f}, c2 = {'wght', 800.f};
 
-        RenderFontTextRuns truns;
+        FontTextRuns truns;
 
         truns.push_back(append(&m_unichars, font0->makeAtCoord(c2), 60, "U"));
         truns.push_back(append(&m_unichars, font0->makeAtCoord(c1), 30, "ne漢字asy"));
@@ -171,7 +169,7 @@
 public:
     TextPathContent()
     {
-        auto compute_bounds = [](const rive::SimpleArray<RenderGlyphRun>& gruns) {
+        auto compute_bounds = [](const rive::SimpleArray<GlyphRun>& gruns) {
             AABB bounds = {};
             for (const auto& gr : gruns)
             {
@@ -216,7 +214,7 @@
         }
     }
 
-    static size_t count_glyphs(const RenderFontGlyphRuns& gruns)
+    static size_t count_glyphs(const FontGlyphRuns& gruns)
     {
         size_t n = 0;
         for (const auto& gr : gruns)
@@ -228,9 +226,9 @@
 
     void modify(float amount) { m_paint->color(0xFFFFFFFF); }
 
-    void draw(Renderer* renderer, const RenderFontGlyphRuns& gruns)
+    void draw(Renderer* renderer, const FontGlyphRuns& gruns)
     {
-        auto get_path = [this](const RenderGlyphRun& run, int index, float dx) {
+        auto get_path = [this](const GlyphRun& run, int index, float dx) {
             auto path = run.font->getPath(run.glyphs[index]);
             path.transformInPlace(Mat2D::fromTranslate(run.xpos[index] + dx, m_offsetY) *
                                   Mat2D::fromScale(run.size, run.size * m_scaleY));
@@ -406,3 +404,6 @@
 {
     return std::make_unique<TextPathContent>();
 }
+#else
+std::unique_ptr<ViewerContent> ViewerContent::TextPath(const char filename[]) { return nullptr; }
+#endif
diff --git a/viewer/src/viewer_content/viewer_content.cpp b/viewer/src/viewer_content/viewer_content.cpp
index e60dfd2..44a63b3 100644
--- a/viewer/src/viewer_content/viewer_content.cpp
+++ b/viewer/src/viewer_content/viewer_content.cpp
@@ -67,17 +67,8 @@
 
 rive::Factory* ViewerContent::RiveFactory() { return ViewerHost::Factory(); }
 
-#ifdef RIVE_BUILD_FOR_APPLE
-// note: we can use harfbuzz even on apple ... (if we want)
-#include "renderfont_coretext.hpp"
-rive::rcp<rive::RenderFont> ViewerContent::DecodeFont(rive::Span<const uint8_t> span)
+#include "rive/text/font_hb.hpp"
+rive::rcp<rive::Font> ViewerContent::DecodeFont(rive::Span<const uint8_t> span)
 {
-    return CoreTextRenderFont::Decode(span);
+    return HBFont::Decode(span);
 }
-#else
-#include "renderfont_hb.hpp"
-rive::rcp<rive::RenderFont> ViewerContent::DecodeFont(rive::Span<const uint8_t> span)
-{
-    return HBRenderFont::Decode(span);
-}
-#endif