Line breaker

Updating the line breaker to support:
- forced line breaks (return characters)
- store break points in context of runs (no searching when breaking)
- perform alignment (center/left/right)

In order to implement tests for this in Rive, I moved the Harfbuzz shaper into Rive (conditionally compiled out) but now we can test line-breaking logic in Rive core.

Since I had to update some of the build scripts, I ran the same new premake formatter we've been using.

<img width="910" alt="CleanShot 2022-09-19 at 21 03 53@2x" src="https://user-images.githubusercontent.com/454182/191164998-6b7b131b-b624-41c7-81d2-c2e01a050680.png">

Diffs=
a1d921c85 Line breaker (#4249)
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index b3b272f..a38c6f7 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -58,7 +58,7 @@
         run: |
           cd dev/test
           C:\premake5.exe vs2022
-          MSBuild rive_tests.sln
+          MSBuild rive.sln
           build\bin\debug\tests.exe
 
   build-macos:
diff --git a/.gitignore b/.gitignore
index 46e81b3..f2ba18f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -83,3 +83,4 @@
 /build/android
 /skia/dependencies/skia_rive_optimized
 /skia/dependencies/skia-experimental
+dependencies/windows/cache
\ No newline at end of file
diff --git a/.rive_head b/.rive_head
index 4f0e506..f2f0e15 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-e62737cf99c7e9f0d94288425903d581c88dea87
+a1d921c85a7c4057dbebf972939e188a3d515c86
diff --git a/build/macosx/build_rive.sh b/build/macosx/build_rive.sh
index cf1149a..d202602 100755
--- a/build/macosx/build_rive.sh
+++ b/build/macosx/build_rive.sh
@@ -12,6 +12,9 @@
     if [[ $var = "tools" ]]; then
         TOOLS="--with_rive_tools"
     fi
+    if [[ $var = "text" ]]; then
+        TOOLS="--with_rive_text"
+    fi
 done
 
 if [[ ! -f "$DEPENDENCIES/bin/premake5" ]]; then
diff --git a/build/premake5.lua b/build/premake5.lua
index 685ba8e..9cafaf2 100644
--- a/build/premake5.lua
+++ b/build/premake5.lua
@@ -1,136 +1,171 @@
-workspace "rive"
-    configurations {"debug", "release"}
-    filter {"options:with_rive_tools" }
-        defines {"WITH_RIVE_TOOLS"}
-        
+workspace 'rive'
+configurations {'debug', 'release'}
+filter {'options:with_rive_tools'}
+do
+    defines {'WITH_RIVE_TOOLS'}
+end
+filter {'options:with_rive_tools'}
+do
+    defines {'WITH_RIVE_TOOLS'}
+end
+
 WINDOWS_CLANG_CL_SUPPRESSED_WARNINGS = {
-    "-Wno-c++98-compat",
-    "-Wno-c++98-compat-pedantic",
-    "-Wno-deprecated-copy-with-user-provided-dtor",
-    "-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-c++98-compat',
+    '-Wno-c++98-compat-pedantic',
+    '-Wno-deprecated-copy-with-user-provided-dtor',
+    '-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'
 }
 
-project "rive"
-    kind "StaticLib"
-    language "C++"
-    cppdialect "C++17"
-    toolset "clang"
-    targetdir "%{cfg.system}/bin/%{cfg.buildcfg}"
-    objdir "%{cfg.system}/obj/%{cfg.buildcfg}"
-    includedirs {"../include"}
+project 'rive'
+do
+    kind 'StaticLib'
+    language 'C++'
+    cppdialect 'C++17'
+    toolset 'clang'
+    targetdir '%{cfg.system}/bin/%{cfg.buildcfg}'
+    objdir '%{cfg.system}/obj/%{cfg.buildcfg}'
+    includedirs {'../include'}
 
-    files {"../src/**.cpp"}
+    files {'../src/**.cpp'}
 
     buildoptions {
-        "-Wall", "-fno-exceptions",
-        "-fno-rtti",
-        "-Werror=format",
-        "-Wimplicit-int-conversion",
-        "-Werror=vla",
+        '-Wall',
+        '-fno-exceptions',
+        '-fno-rtti',
+        '-Werror=format',
+        '-Wimplicit-int-conversion',
+        '-Werror=vla'
     }
 
-    filter {"system:macosx" }
+    filter {'system:macosx'}
+    do
         buildoptions {
             -- this triggers too much on linux, so just enable here for now
-            "-Wimplicit-float-conversion",
+            '-Wimplicit-float-conversion'
         }
-
-    filter { "system:macosx", "configurations:release" }
-        buildoptions {"-flto=full"}
-
-    filter {"system:ios" }
-        buildoptions {"-flto=full"}
-
-    filter "system:windows"
-        architecture "x64"
-        defines {"_USE_MATH_DEFINES"}
-        flags { "FatalCompileWarnings" }
+    end
+    filter {'system:macosx', 'configurations:release'}
+    do
+        buildoptions {'-flto=full'}
+    end
+    filter {'system:ios'}
+    do
+        buildoptions {'-flto=full'}
+    end
+    filter 'system:windows'
+    do
+        architecture 'x64'
+        defines {'_USE_MATH_DEFINES'}
+        flags {'FatalCompileWarnings'}
         buildoptions {WINDOWS_CLANG_CL_SUPPRESSED_WARNINGS}
-        staticruntime "on"  -- Match Skia's /MT flag for link compatibility
-        runtime "Release"  -- Use /MT even in debug (/MTd is incompatible with Skia)
+        staticruntime 'on' -- Match Skia's /MT flag for link compatibility
+        runtime 'Release' -- Use /MT even in debug (/MTd is incompatible with Skia)
         removebuildoptions {
-            "-fno-exceptions",
-            "-fno-rtti",
+            '-fno-exceptions',
+            '-fno-rtti'
         }
+    end
+    filter {'system:ios', 'options:variant=system'}
+    do
+        buildoptions {
+            '-mios-version-min=10.0 -fembed-bitcode -arch armv7 -arch arm64 -arch arm64e -isysroot ' ..
+                (os.getenv('IOS_SYSROOT') or '')
+        }
+    end
+    filter {'system:ios', 'options:variant=emulator'}
+    do
+        buildoptions {
+            '-mios-version-min=10.0 -arch arm64 -arch x86_64 -arch i386 -isysroot ' .. (os.getenv('IOS_SYSROOT') or '')
+        }
+        targetdir '%{cfg.system}_sim/bin/%{cfg.buildcfg}'
+        objdir '%{cfg.system}_sim/obj/%{cfg.buildcfg}'
+    end
 
-    filter {"system:ios", "options:variant=system" }
-        buildoptions {"-mios-version-min=10.0 -fembed-bitcode -arch armv7 -arch arm64 -arch arm64e -isysroot " .. (os.getenv("IOS_SYSROOT") or "")}
-    
-    filter {"system:ios", "options:variant=emulator" }
-        buildoptions {"-mios-version-min=10.0 -arch arm64 -arch x86_64 -arch i386 -isysroot " .. (os.getenv("IOS_SYSROOT") or "")}
-        targetdir "%{cfg.system}_sim/bin/%{cfg.buildcfg}"
-        objdir "%{cfg.system}_sim/obj/%{cfg.buildcfg}"
-
-    filter { "system:android", "configurations:release" }
-        buildoptions {"-flto=full"}
+    filter {'system:android', 'configurations:release'}
+    do
+        buildoptions {'-flto=full'}
+    end
 
     -- Is there a way to pass 'arch' as a variable here?
-    filter { "system:android", "options:arch=x86" }
-        targetdir "%{cfg.system}/x86/bin/%{cfg.buildcfg}"
-        objdir "%{cfg.system}/x86/obj/%{cfg.buildcfg}"
+    filter {'system:android', 'options:arch=x86'}
+    do
+        targetdir '%{cfg.system}/x86/bin/%{cfg.buildcfg}'
+        objdir '%{cfg.system}/x86/obj/%{cfg.buildcfg}'
+    end
 
-    filter { "system:android", "options:arch=x64" }
-        targetdir "%{cfg.system}/x64/bin/%{cfg.buildcfg}"
-        objdir "%{cfg.system}/x64/obj/%{cfg.buildcfg}"
+    filter {'system:android', 'options:arch=x64'}
+    do
+        targetdir '%{cfg.system}/x64/bin/%{cfg.buildcfg}'
+        objdir '%{cfg.system}/x64/obj/%{cfg.buildcfg}'
+    end
+    filter {'system:android', 'options:arch=arm'}
+    do
+        targetdir '%{cfg.system}/arm/bin/%{cfg.buildcfg}'
+        objdir '%{cfg.system}/arm/obj/%{cfg.buildcfg}'
+    end
+    filter {'system:android', 'options:arch=arm64'}
+    do
+        targetdir '%{cfg.system}/arm64/bin/%{cfg.buildcfg}'
+        objdir '%{cfg.system}/arm64/obj/%{cfg.buildcfg}'
+    end
+    filter 'configurations:debug'
+    do
+        defines {'DEBUG'}
+        symbols 'On'
+    end
 
-    filter { "system:android", "options:arch=arm" }
-        targetdir "%{cfg.system}/arm/bin/%{cfg.buildcfg}"
-        objdir "%{cfg.system}/arm/obj/%{cfg.buildcfg}"
-
-    filter { "system:android", "options:arch=arm64" }
-        targetdir "%{cfg.system}/arm64/bin/%{cfg.buildcfg}"
-        objdir "%{cfg.system}/arm64/obj/%{cfg.buildcfg}"
-
-    filter "configurations:debug"
-        defines {"DEBUG"}
-        symbols "On"
-
-    filter "configurations:release"
-        defines {"RELEASE"}
-        defines {"NDEBUG"}
-        optimize "On"
-
+    filter 'configurations:release'
+    do
+        defines {'RELEASE'}
+        defines {'NDEBUG'}
+        optimize 'On'
+    end
+end
 
 newoption {
-    trigger = "variant",
-    value = "type",
-    description = "Choose a particular variant to build",
+    trigger = 'variant',
+    value = 'type',
+    description = 'Choose a particular variant to build',
     allowed = {
-        { "system",   "Builds the static library for the provided system" },
-        { "emulator",  "Builds for an emulator/simulator for the provided system" }
+        {'system', 'Builds the static library for the provided system'},
+        {'emulator', 'Builds for an emulator/simulator for the provided system'}
     },
-    default = "system"
+    default = 'system'
 }
 
 newoption {
-    trigger = "arch",
-    value = "ABI",
-    description = "The ABI with the right toolchain for this build, generally with Android",
+    trigger = 'arch',
+    value = 'ABI',
+    description = 'The ABI with the right toolchain for this build, generally with Android',
     allowed = {
-        { "x86" },
-        { "x64" },
-        { "arm" },
-        { "arm64" }
+        {'x86'},
+        {'x64'},
+        {'arm'},
+        {'arm64'}
     }
-
 }
 
 newoption {
-    trigger = "with_rive_tools",
-    description = "Enables tools usually not necessary for runtime."
-}
\ No newline at end of file
+    trigger = 'with_rive_tools',
+    description = 'Enables tools usually not necessary for runtime.'
+}
+
+newoption {
+    trigger = 'with_rive_text',
+    description = 'Compiles in text features.'
+}
diff --git a/dependencies/premake5_harfbuzz.lua b/dependencies/premake5_harfbuzz.lua
index 57c3947..9c31248 100644
--- a/dependencies/premake5_harfbuzz.lua
+++ b/dependencies/premake5_harfbuzz.lua
@@ -250,4 +250,49 @@
         defines {'NDEBUG'}
         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/config_directories.bat b/dependencies/windows/config_directories.bat
new file mode 100644
index 0000000..2dcdd50
--- /dev/null
+++ b/dependencies/windows/config_directories.bat
@@ -0,0 +1,6 @@
+set "SCRIPT_DIR=%~dp0"
+
+set "DEPENDENCIES_SCRIPTS=%SCRIPT_DIR%"
+set "DEPENDENCIES=%SCRIPT_DIR%cache"
+
+if not exist %DEPENDENCIES% mkdir %DEPENDENCIES%    
\ No newline at end of file
diff --git a/dependencies/windows/get_harfbuzz.bat b/dependencies/windows/get_harfbuzz.bat
new file mode 100644
index 0000000..e225413
--- /dev/null
+++ b/dependencies/windows/get_harfbuzz.bat
@@ -0,0 +1,11 @@
+@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/dependencies/windows/get_premake5.bat b/dependencies/windows/get_premake5.bat
new file mode 100644
index 0000000..77687b7
--- /dev/null
+++ b/dependencies/windows/get_premake5.bat
@@ -0,0 +1,12 @@
+@echo off
+pushd %DEPENDENCIES%
+if not exist ".\bin" mkdir bin
+echo Downloading Premake5
+curl https://github.com/premake/premake-core/releases/download/v5.0.0-beta1/premake-5.0.0-beta1-windows.zip -L -o .\bin\premake_windows.zip 
+pushd bin
+:: Export premake5 into bin
+tar -xf premake_windows.zip
+:: Delete downloaded archive
+del premake_windows.zip
+popd
+popd
\ No newline at end of file
diff --git a/dev/test.bat b/dev/test.bat
new file mode 100644
index 0000000..a238f75
--- /dev/null
+++ b/dev/test.bat
@@ -0,0 +1,27 @@
+@echo off
+call ..\dependencies\windows\config_directories.bat
+
+if not exist "%DEPENDENCIES%\bin\premake5.exe" (
+    pushd "%DEPENDENCIES_SCRIPTS%"
+    call .\get_premake5.bat || goto :error
+    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
+
+MSBuild.exe /?  2> NUL
+if not %ERRORLEVEL%==9009 (
+    set "MSBuild=MSBuild.exe"
+) else (
+    set "MSBuild=%ProgramFiles%\Microsoft Visual Studio\2022\Community\Msbuild\Current\Bin\MSBuild.exe"
+)
+call "%MSBuild%" rive.sln
+build\bin\debug\tests.exe
\ No newline at end of file
diff --git a/dev/test.sh b/dev/test.sh
index 7235c68..3a0ea33 100755
--- a/dev/test.sh
+++ b/dev/test.sh
@@ -2,28 +2,31 @@
 
 set -e
 
+source ../dependencies/config_directories.sh
 pushd test &>/dev/null
 
 OPTION=$1
 UTILITY=
 
-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
-    shift
-elif [ "$OPTION" = "memory" ]
-then
+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
+  shift
+elif [ "$OPTION" = "memory" ]; then
   echo Will perform memory checks...
   UTILITY='valgrind --leak-check=full'
   shift
-elif [ "$OPTION" = "debug" ]
-then
+elif [ "$OPTION" = "debug" ]; then
   echo Starting debugger...
   UTILITY='lldb'
   shift
@@ -31,9 +34,10 @@
 
 premake5 gmake2 || exit 1
 make -j7 || exit 1
+
 for file in ./build/bin/debug/*; do
   echo testing $file
   $UTILITY $file "$1"
 done
 
-popd &>/dev/null
\ No newline at end of file
+popd &>/dev/null
diff --git a/dev/test/premake5.lua b/dev/test/premake5.lua
index 30acf4a..07a3d35 100644
--- a/dev/test/premake5.lua
+++ b/dev/test/premake5.lua
@@ -1,82 +1,92 @@
 -- require "lfs"
 -- Clean Function --
 newaction {
-    trigger = "clean",
-    description = "clean the build",
+    trigger = 'clean',
+    description = 'clean the build',
     execute = function()
-        print("clean the build...")
-        os.rmdir("build")
-        os.remove("Makefile")
+        print('clean the build...')
+        os.rmdir('build')
+        os.remove('Makefile')
         -- no wildcards in os.remove, so use shell
-        os.execute("rm *.make")
-        print("build cleaned")
+        os.execute('rm *.make')
+        print('build cleaned')
     end
 }
 
+workspace 'rive'
+configurations {'debug'}
 
-WINDOWS_CLANG_CL_SUPPRESSED_WARNINGS = {
-    "-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",
-}
+dependencies = os.getenv('DEPENDENCIES')
+dofile(path.join(path.getabsolute(dependencies) .. '/../..', 'premake5_harfbuzz.lua'))
 
+project('tests')
+do
+    kind 'ConsoleApp'
+    language 'C++'
+    cppdialect 'C++17'
+    toolset 'clang'
+    targetdir 'build/bin/%{cfg.buildcfg}'
+    objdir 'build/obj/%{cfg.buildcfg}'
 
-workspace "rive_tests"
-configurations {"debug"}
+    buildoptions {'-Wall', '-fno-exceptions', '-fno-rtti'}
 
-project("tests")
-kind "ConsoleApp"
-language "C++"
-cppdialect "C++17"
-toolset "clang"
-targetdir "build/bin/%{cfg.buildcfg}"
-objdir "build/obj/%{cfg.buildcfg}"
-
-buildoptions {"-Wall", "-fno-exceptions", "-fno-rtti"}
-
-includedirs {"./include", "../../include"}
-
-files {
-    "../../src/**.cpp",   -- the Rive runtime source
-    "../../test/**.cpp",  -- the tests
-    "../../utils/**.cpp", -- no_op utils
-}
-
-defines {"TESTING", "ENABLE_QUERY_FLAT_VERTICES", "WITH_RIVE_TOOLS"}
-
-filter "configurations:debug"
-defines {"DEBUG"}
-symbols "On"
-
-filter "system:windows"
-    flags { "FatalWarnings" }
-    removebuildoptions {
-        -- vs clang doesn't recognize these on windows
-        "-fno-exceptions",
-        "-fno-rtti",
+    includedirs {'./include', '../../include', dependencies .. '/harfbuzz/src'}
+    links {
+        'rive_harfbuzz'
     }
-    architecture "x64"
-    defines {
-        "_USE_MATH_DEFINES",
-        "_CRT_SECURE_NO_WARNINGS"
+
+    files {
+        '../../src/**.cpp', -- the Rive runtime source
+        '../../test/**.cpp', -- the tests
+        '../../utils/**.cpp' -- no_op utils
     }
-    buildoptions {WINDOWS_CLANG_CL_SUPPRESSED_WARNINGS}
+
+    defines {'TESTING', 'ENABLE_QUERY_FLAT_VERTICES', 'WITH_RIVE_TOOLS', 'WITH_RIVE_TEXT'}
+
+    filter 'configurations:debug'
+    do
+        defines {'DEBUG'}
+        symbols 'On'
+    end
+
+    filter 'system:windows'
+    do
+        flags {'FatalWarnings'}
+        removebuildoptions {
+            -- vs clang doesn't recognize these on windows
+            '-fno-exceptions',
+            '-fno-rtti'
+        }
+        architecture 'x64'
+        defines {
+            '_USE_MATH_DEFINES',
+            '_CRT_SECURE_NO_WARNINGS'
+        }
+        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-four-char-constants',
+            '-Wno-unreachable-code'
+        }
+    end
+end
diff --git a/include/rive/render_text.hpp b/include/rive/render_text.hpp
index 6c5b70b..b9c35a1 100644
--- a/include/rive/render_text.hpp
+++ b/include/rive/render_text.hpp
@@ -88,18 +88,32 @@
 
 struct RenderGlyphRun {
     RenderGlyphRun(size_t glyphCount = 0) :
-        glyphs(glyphCount), textOffsets(glyphCount), xpos(glyphCount + 1) {}
+        glyphs(glyphCount), textIndices(glyphCount), xpos(glyphCount + 1) {}
 
     RenderGlyphRun(rive::SimpleArray<GlyphID> glyphIds,
                    rive::SimpleArray<uint32_t> offsets,
                    rive::SimpleArray<float> xs) :
-        glyphs(glyphIds), textOffsets(offsets), xpos(xs) {}
+        glyphs(glyphIds), textIndices(offsets), xpos(xs) {}
 
     rcp<RenderFont> font;
     float size;
-    rive::SimpleArray<GlyphID> glyphs;       // [#glyphs]
-    rive::SimpleArray<uint32_t> textOffsets; // [#glyphs]
-    rive::SimpleArray<float> xpos;           // [#glyphs + 1]
+    // 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
diff --git a/skia/renderer/include/renderer_utils.hpp b/include/rive/renderer_utils.hpp
similarity index 100%
rename from skia/renderer/include/renderer_utils.hpp
rename to include/rive/renderer_utils.hpp
diff --git a/include/rive/simple_array.hpp b/include/rive/simple_array.hpp
index 270a780..48fd694 100644
--- a/include/rive/simple_array.hpp
+++ b/include/rive/simple_array.hpp
@@ -19,6 +19,7 @@
 namespace SimpleArrayTesting {
 extern int mallocCount;
 extern int reallocCount;
+extern int freeCount;
 void resetCounters();
 } // namespace SimpleArrayTesting
 #endif
@@ -30,8 +31,8 @@
 /// array. This saves the structure from needing to store extra ptrs and keeps
 /// it optimally sized for marshaling. See SimpleArrayBuilder<T> below for push
 /// functionality.
-template <typename T> class SimpleArray {
 
+template <typename T> class SimpleArray {
 public:
     SimpleArray() : m_ptr(nullptr), m_size(0) {}
     SimpleArray(size_t size) : m_ptr(static_cast<T*>(malloc(size * sizeof(T)))), m_size(size) {
@@ -75,6 +76,8 @@
         return *this;
     }
 
+    SimpleArray<T>& operator=(SimpleArrayBuilder<T>&& other);
+
     template <typename Container>
     constexpr SimpleArray(Container& c) : SimpleArray(std::data(c), std::size(c)) {}
     constexpr SimpleArray(std::initializer_list<T> il) :
@@ -86,6 +89,9 @@
             }
         }
         free(m_ptr);
+#ifdef TESTING
+        SimpleArrayTesting::freeCount++;
+#endif
     }
 
     constexpr T& operator[](size_t index) const {
@@ -147,6 +153,7 @@
     // Allows iterating just the written content.
     constexpr size_t capacity() const { return this->m_size; }
     constexpr size_t size() const { return m_write - this->m_ptr; }
+    constexpr bool empty() const { return size() == 0; }
     constexpr T* begin() const { return this->m_ptr; }
     constexpr T* end() const { return m_write; }
 
@@ -200,6 +207,15 @@
     other.m_size = 0;
 }
 
+template <typename T> SimpleArray<T>& SimpleArray<T>::operator=(SimpleArrayBuilder<T>&& other) {
+    other.resize(other.size());
+    this->m_ptr = other.m_ptr;
+    this->m_size = other.m_size;
+    other.m_ptr = nullptr;
+    other.m_size = 0;
+    return *this;
+}
+
 } // namespace rive
 
 #endif
diff --git a/include/rive/text/line_breaker.hpp b/include/rive/text/line_breaker.hpp
index d8477d3..37d50d0 100644
--- a/include/rive/text/line_breaker.hpp
+++ b/include/rive/text/line_breaker.hpp
@@ -9,37 +9,35 @@
 
 namespace rive {
 
+enum class RenderTextAlign : uint8_t { left = 0, right = 1, center = 2 };
+
 struct RenderGlyphLine {
-    int startRun;
-    int startIndex;
-    int endRun;
-    int endIndex;
-    int wsRun;
-    int wsIndex;
+    uint32_t startRun;
+    uint32_t startIndex;
+    uint32_t endRun;
+    uint32_t endIndex;
     float startX;
     float top = 0, baseline = 0, bottom = 0;
 
-    RenderGlyphLine(int startRun,
-                    int startIndex,
-                    int endRun,
-                    int endIndex,
-                    int wsRun,
-                    int wsIndex,
-                    float startX) :
-        startRun(startRun),
-        startIndex(startIndex),
-        endRun(endRun),
-        endIndex(endIndex),
-        wsRun(wsRun),
-        wsIndex(wsIndex),
-        startX(startX) {}
+    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,
-                                                   Span<const int> breaks,
-                                                   float width);
+                                                   float width,
+                                                   RenderTextAlign align);
 
     // Compute values for top/baseline/bottom per line
-    static void ComputeLineSpacing(rive::Span<RenderGlyphLine>, rive::Span<const RenderGlyphRun>);
+    static void ComputeLineSpacing(rive::Span<RenderGlyphLine>,
+                                   rive::Span<const RenderGlyphRun>,
+                                   float width,
+                                   RenderTextAlign align);
 };
 
 } // namespace rive
diff --git a/skia/renderer/include/renderfont_hb.hpp b/include/rive/text/renderfont_hb.hpp
similarity index 100%
rename from skia/renderer/include/renderfont_hb.hpp
rename to include/rive/text/renderfont_hb.hpp
diff --git a/skia/renderer/build/macosx/build_skia_renderer.sh b/skia/renderer/build/macosx/build_skia_renderer.sh
index 05601b1..d3710a5 100755
--- a/skia/renderer/build/macosx/build_skia_renderer.sh
+++ b/skia/renderer/build/macosx/build_skia_renderer.sh
@@ -21,7 +21,7 @@
         GRAPHICS=metal
     fi
     if [[ $var = "text" ]]; then
-        OTHER_OPTIONS+=--with-text
+        OTHER_OPTIONS+=--with_rive_text
     fi
 done
 
diff --git a/skia/renderer/build/premake5.lua b/skia/renderer/build/premake5.lua
index d779fe8..b2c71c0 100644
--- a/skia/renderer/build/premake5.lua
+++ b/skia/renderer/build/premake5.lua
@@ -148,14 +148,14 @@
         optimize 'On'
     end
 
-    filter {'options:with-text'}
+    filter {'options:with_rive_text'}
     do
-        defines {'RIVE_TEXT'}
+        defines {'WITH_RIVE_TEXT'}
     end
 end
 
 newoption {
-    trigger = 'with-text',
+    trigger = 'with_rive_text',
     description = 'Enables text experiments'
 }
 
diff --git a/skia/renderer/src/renderfont_coretext.cpp b/skia/renderer/src/renderfont_coretext.cpp
index 89dc0cb..8cf73e9 100644
--- a/skia/renderer/src/renderfont_coretext.cpp
+++ b/skia/renderer/src/renderfont_coretext.cpp
@@ -5,7 +5,7 @@
 #include "rive/rive_types.hpp"
 #include "utils/rive_utf.hpp"
 
-#if defined(RIVE_BUILD_FOR_APPLE) && defined(RIVE_TEXT)
+#if defined(RIVE_BUILD_FOR_APPLE) && defined(WITH_RIVE_TEXT)
 #include "renderfont_coretext.hpp"
 #include "mac_utils.hpp"
 
@@ -204,7 +204,7 @@
 
         for (CFIndex i = 0; i < count; ++i) {
             gr.xpos[i] = startX;
-            gr.textOffsets[i] = textStart + indices[i]; // utf16 offsets, will fix-up later
+            gr.textIndices[i] = textStart + indices[i]; // utf16 offsets, will fix-up later
             startX += advances[i].width * scale;
         }
         gr.xpos[count] = startX;
diff --git a/src/renderer.cpp b/src/renderer.cpp
index 4f46023..6fb5c17 100644
--- a/src/renderer.cpp
+++ b/src/renderer.cpp
@@ -90,6 +90,8 @@
 
 #include "rive/render_text.hpp"
 
+static bool isWhiteSpace(rive::Unichar c) { return c <= ' '; }
+
 rive::SimpleArray<RenderGlyphRun>
 RenderFont::shapeText(rive::Span<const rive::Unichar> text,
                       rive::Span<const rive::RenderTextRun> runs) const {
@@ -101,11 +103,47 @@
     }
     assert(count <= text.size());
 #endif
-    auto gruns = this->onShapeText(text, runs);
+
+    auto gruns = onShapeText(text, runs);
+    bool wantWhiteSpace = false;
+
+    rive::RenderGlyphRun* lastRun = nullptr;
+    size_t reserveSize = text.size() / 4;
+    rive::SimpleArrayBuilder<uint32_t> breakBuilder(reserveSize);
+    for (auto& gr : gruns) {
+        if (lastRun != nullptr) {
+            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') {
+                breakBuilder.add(glyphIndex);
+                breakBuilder.add(glyphIndex);
+            }
+            if (wantWhiteSpace == isWhiteSpace(unicode)) {
+                breakBuilder.add(glyphIndex);
+                wantWhiteSpace = !wantWhiteSpace;
+            }
+            glyphIndex++;
+        }
+
+        lastRun = &gr;
+    }
+    if (lastRun != nullptr) {
+        if (wantWhiteSpace) {
+            breakBuilder.add((uint32_t)lastRun->glyphs.size());
+        }
+        lastRun->breaks = std::move(breakBuilder);
+    }
+
 #ifdef DEBUG
     for (const auto& gr : gruns) {
         assert(gr.glyphs.size() > 0);
-        assert(gr.glyphs.size() == gr.textOffsets.size());
+        assert(gr.glyphs.size() == gr.textIndices.size());
         assert(gr.glyphs.size() + 1 == gr.xpos.size());
     }
 #endif
diff --git a/src/simple_array.cpp b/src/simple_array.cpp
index ef567d8..cd4ecfc 100644
--- a/src/simple_array.cpp
+++ b/src/simple_array.cpp
@@ -4,9 +4,11 @@
 namespace SimpleArrayTesting {
 int mallocCount = 0;
 int reallocCount = 0;
+int freeCount = 0;
 void resetCounters() {
     mallocCount = 0;
     reallocCount = 0;
+    freeCount = 0;
 }
 } // namespace SimpleArrayTesting
 } // namespace rive
diff --git a/src/text/line_breaker.cpp b/src/text/line_breaker.cpp
index 5bb7899..d862e41 100644
--- a/src/text/line_breaker.cpp
+++ b/src/text/line_breaker.cpp
@@ -3,138 +3,32 @@
  */
 
 #include "rive/text/line_breaker.hpp"
-
+#include <limits>
 using namespace rive;
 
-// Return the index for the run that contains the char at textOffset
-static int _offsetToRunIndex(Span<const RenderGlyphRun> runs, size_t textOffset) {
-    assert(textOffset >= 0);
-    for (int i = 0; i < (int)runs.size() - 1; ++i) {
-        if (textOffset <= runs[i].textOffsets.back()) {
-            return i;
-        }
-    }
-    return (int)runs.size() - 1;
-}
-
-static int textOffsetToGlyphIndex(const RenderGlyphRun& run, size_t textOffset) {
-    assert(textOffset >= run.textOffsets.front());
-    //    assert(textOffset <= run.textOffsets.back()); // not true for last run
-
-    // todo: bsearch?
-    auto begin = run.textOffsets.begin();
-    auto end = run.textOffsets.end();
-    auto iter = std::find(begin, end, textOffset);
-    if (iter == end) { // end of run
-        return (int)run.glyphs.size() - 1;
-    }
-    return (int)(iter - begin);
-}
-
-std::vector<RenderGlyphLine> RenderGlyphLine::BreakLines(Span<const RenderGlyphRun> runs,
-                                                         Span<const int> breaks,
-                                                         float width) {
-    assert(breaks.size() >= 2);
-
-    std::vector<RenderGlyphLine> lines;
-    int startRun = 0;
-    int startIndex = 0;
-    double xlimit = width;
-
-    int prevRun = 0;
-    int prevIndex = 0;
-
-    int wordStart = breaks[0];
-    int wordEnd = breaks[1];
-    size_t nextBreakIndex = 2;
-    int lineStartTextOffset = wordStart;
-
-    for (;;) {
-        assert(wordStart <= wordEnd); // == means trailing spaces?
-
-        int endRun = _offsetToRunIndex(runs, wordEnd);
-        int endIndex = textOffsetToGlyphIndex(runs[endRun], wordEnd);
-        float pos = runs[endRun].xpos[endIndex];
-        bool bumpBreakIndex = true;
-        if (pos > xlimit) {
-            int wsRun = _offsetToRunIndex(runs, wordStart);
-            int wsIndex = textOffsetToGlyphIndex(runs[wsRun], wordStart);
-
-            bumpBreakIndex = false;
-            // does just one word not fit?
-            if (lineStartTextOffset == wordStart) {
-                // walk backwards a letter at a time until we fit, stopping at
-                // 1 letter.
-                int wend = wordEnd;
-                while (pos > xlimit && wend - 1 > wordStart) {
-                    wend -= 1;
-                    prevRun = _offsetToRunIndex(runs, wend);
-                    prevIndex = textOffsetToGlyphIndex(runs[prevRun], wend);
-                    pos = runs[prevRun].xpos[prevIndex];
-                }
-                assert(wend < wordEnd || wend == wordEnd && wordStart + 1 == wordEnd);
-                if (wend == wordEnd) {
-                    bumpBreakIndex = true;
-                }
-
-                // now reset our "whitespace" marker to just be prev, since
-                // by defintion we have no extra whitespace on this line
-                wsRun = prevRun;
-                wsIndex = prevIndex;
-                wordStart = wend;
-            }
-
-            // bulid the line
-            const auto lineStartX = runs[startRun].xpos[startIndex];
-            lines.push_back(RenderGlyphLine(startRun,
-                                            startIndex,
-                                            prevRun,
-                                            prevIndex,
-                                            wsRun,
-                                            wsIndex,
-                                            lineStartX));
-
-            // update for the next line
-            xlimit = runs[wsRun].xpos[wsIndex] + width;
-            startRun = prevRun = wsRun;
-            startIndex = prevIndex = wsIndex;
-            lineStartTextOffset = wordStart;
-        } else {
-            // we didn't go too far, so remember this word-end boundary
-            prevRun = endRun;
-            prevIndex = endIndex;
-        }
-
-        if (bumpBreakIndex) {
-            if (nextBreakIndex < breaks.size()) {
-                wordStart = breaks[nextBreakIndex++];
-                wordEnd = breaks[nextBreakIndex++];
-            } else {
-                break; // bust out of the loop
-            }
-        }
-    }
-    // scoop up the last line (if present)
-    const int tailRun = (int)runs.size() - 1;
-    const int tailIndex = (int)runs[tailRun].glyphs.size();
-    if (startRun != tailRun || startIndex != tailIndex) {
-        const auto startX = runs[startRun].xpos[startIndex];
-        lines.push_back(
-            RenderGlyphLine(startRun, startIndex, tailRun, tailIndex, tailRun, tailIndex, startX));
-    }
-
-    ComputeLineSpacing(lines, runs);
-
-    return lines;
-}
-
+static bool autowidth(float width) { return width < 0.0f; }
 void RenderGlyphLine::ComputeLineSpacing(Span<RenderGlyphLine> lines,
-                                         Span<const RenderGlyphRun> runs) {
+                                         Span<const RenderGlyphRun> runs,
+                                         float width,
+                                         RenderTextAlign align) {
+
+    float maxLineWidth = 0.0f;
+    if (autowidth(width)) {
+        for (auto& line : lines) {
+            auto lineWidth =
+                runs[line.endRun].xpos[line.endIndex] - runs[line.startRun].xpos[line.startIndex];
+            if (lineWidth > maxLineWidth) {
+                maxLineWidth = lineWidth;
+            }
+        }
+    } else {
+        maxLineWidth = width;
+    }
     float Y = 0; // top of our frame
     for (auto& line : lines) {
         float asc = 0;
         float des = 0;
-        for (int i = line.startRun; i <= line.wsRun; ++i) {
+        for (int i = line.startRun; i <= line.endRun; ++i) {
             const auto& run = runs[i];
 
             asc = std::min(asc, run.font->lineMetrics().ascent * run.size);
@@ -145,6 +39,201 @@
         line.baseline = Y;
         Y += des;
         line.bottom = Y;
+        auto lineWidth =
+            runs[line.endRun].xpos[line.endIndex] - runs[line.startRun].xpos[line.startIndex];
+        switch (align) {
+            case RenderTextAlign::right: line.startX = maxLineWidth - lineWidth; break;
+            case RenderTextAlign::left: line.startX = 0; break;
+            case RenderTextAlign::center:
+                line.startX = maxLineWidth / 2.0f - lineWidth / 2.0f;
+                break;
+        }
     }
-    // TODO: good place to perform left/center/right alignment
 }
+
+struct WordMarker {
+    const RenderGlyphRun* run;
+    uint32_t index;
+
+    bool next(Span<const RenderGlyphRun> runs) {
+        index += 2;
+        while (index >= run->breaks.size()) {
+
+            index -= run->breaks.size();
+            run++;
+            if (run == runs.end()) {
+                return false;
+            }
+        }
+        return true;
+    }
+};
+
+class RunIterator {
+    Span<const RenderGlyphRun> m_runs;
+    const RenderGlyphRun* m_run;
+    uint32_t m_index;
+
+public:
+    RunIterator(Span<const RenderGlyphRun> runs, const RenderGlyphRun* run, uint32_t index) :
+        m_runs(runs), m_run(run), m_index(index) {}
+
+    bool back() {
+        if (m_index == 0) {
+            if (m_run == m_runs.begin()) {
+                return false;
+            }
+            m_run--;
+            if (m_run->glyphs.size() == 0) {
+                m_index = 0;
+                return back();
+            } else {
+                m_index = m_run->glyphs.size() == 0 ? 0 : (uint32_t)m_run->glyphs.size() - 1;
+            }
+        } else {
+            m_index--;
+        }
+        return true;
+    }
+
+    bool forward() {
+        if (m_index == m_run->glyphs.size()) {
+            if (m_run == m_runs.end()) {
+                return false;
+            }
+            m_run++;
+            m_index = 0;
+            if (m_index == m_run->glyphs.size()) {
+                return forward();
+            }
+        } else {
+            m_index++;
+        }
+        return true;
+    }
+
+    float x() const { return m_run->xpos[m_index]; }
+
+    const RenderGlyphRun* 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) {
+
+    float maxLineWidth = autowidth(width) ? std::numeric_limits<float>::max() : width;
+
+    std::vector<RenderGlyphLine> lines;
+
+    if (runs.empty()) {
+        return lines;
+    }
+
+    auto limit = maxLineWidth;
+
+    bool advanceWord = false;
+    WordMarker start = {runs.begin(), 0};
+    WordMarker end = {runs.begin(), 1};
+
+    RenderGlyphLine line = RenderGlyphLine();
+
+    uint32_t breakIndex = end.run->breaks[end.index];
+    uint32_t lastEndIndex = end.index;
+    uint32_t startBreakIndex = start.run->breaks[start.index];
+    float x = end.run->xpos[breakIndex];
+    while (true) {
+        if (advanceWord) {
+            lastEndIndex = end.index;
+
+            if (!start.next(runs)) {
+                break;
+            }
+            if (!end.next(runs)) {
+                break;
+            }
+
+            advanceWord = false;
+
+            breakIndex = end.run->breaks[end.index];
+            startBreakIndex = start.run->breaks[start.index];
+            x = end.run->xpos[breakIndex];
+        }
+
+        if (breakIndex != startBreakIndex && x > limit) {
+            uint32_t startRun = (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) {
+                bool canBreakMore = true;
+                while (canBreakMore && x > limit) {
+
+                    RunIterator lineStart =
+                        RunIterator(runs, runs.begin() + line.startRun, line.startIndex);
+                    RunIterator lineEnd = RunIterator(runs, end.run, end.run->breaks[end.index]);
+
+                    // Look for the next character that doesn't overflow.
+                    while (true) {
+                        if (!lineEnd.back()) {
+                            // Hit the start of the text, can't go back.
+                            canBreakMore = false;
+                            break;
+                        } else if (lineEnd.x() <= limit) {
+                            if (lineStart == lineEnd && !lineEnd.forward()) {
+                                // Hit the start of the line and could not
+                                // go forward to consume a single character.
+                                // We can't break any further.
+                                canBreakMore = false;
+                            } else {
+                                line.endRun = (uint32_t)(lineEnd.run() - runs.begin());
+                                line.endIndex = lineEnd.index();
+                            }
+                            break;
+                        }
+                    }
+                    if (canBreakMore) {
+                        // Add the line and push the limit out.
+                        limit = lineEnd.x() + maxLineWidth;
+                        if (!line.empty()) {
+                            lines.push_back(line);
+                        }
+                        // Setup the next line.
+                        line = RenderGlyphLine((uint32_t)(lineEnd.run() - runs.begin()),
+                                               lineEnd.index());
+                    }
+                }
+            } else {
+                // word overflowed, knock it to a new line
+                auto startX = start.run->xpos[start.run->breaks[start.index]];
+                limit = startX + maxLineWidth;
+
+                if (!line.empty() || start.index - lastEndIndex > 1) {
+                    lines.push_back(line);
+                }
+
+                line = RenderGlyphLine(startRun, startBreakIndex);
+            }
+        } else {
+            line.endRun = (uint32_t)(end.run - runs.begin());
+            line.endIndex = end.run->breaks[end.index];
+            advanceWord = true;
+            // Forced BR.
+            if (breakIndex == startBreakIndex) {
+                lines.push_back(line);
+                auto startX = start.run->xpos[start.run->breaks[start.index]];
+                limit = startX + maxLineWidth;
+                line = RenderGlyphLine((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);
+    }
+
+    ComputeLineSpacing(lines, runs, width, align);
+    return lines;
+}
\ No newline at end of file
diff --git a/skia/renderer/src/renderfont_hb.cpp b/src/text/renderfont_hb.cpp
similarity index 86%
rename from skia/renderer/src/renderfont_hb.cpp
rename to src/text/renderfont_hb.cpp
index 72b8a5b..7e97f2a 100644
--- a/skia/renderer/src/renderfont_hb.cpp
+++ b/src/text/renderfont_hb.cpp
@@ -4,11 +4,11 @@
 
 #include "rive/render_text.hpp"
 
-#ifdef RIVE_TEXT
-#include "renderfont_hb.hpp"
+#ifdef WITH_RIVE_TEXT
+#include "rive/text/renderfont_hb.hpp"
 
 #include "rive/factory.hpp"
-#include "renderer_utils.hpp"
+#include "rive/renderer_utils.hpp"
 
 #include "hb.h"
 #include "hb-ot.h"
@@ -42,33 +42,35 @@
 constexpr float gInvScale = 1.0f / kStdScale;
 
 extern "C" {
-void rpath_move_to(hb_draw_funcs_t*, void* rpath, hb_draw_state_t*, float x, float y, void*) {
+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);
 }
-void rpath_line_to(hb_draw_funcs_t*, void* rpath, hb_draw_state_t*, float x1, float y1, void*) {
+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);
 }
-void rpath_quad_to(hb_draw_funcs_t*,
-                   void* rpath,
-                   hb_draw_state_t*,
-                   float x1,
-                   float y1,
-                   float x2,
-                   float y2,
-                   void*) {
+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);
 }
-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*) {
+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,
@@ -77,7 +79,7 @@
                   x3 * gInvScale,
                   -y3 * gInvScale);
 }
-void rpath_close(hb_draw_funcs_t*, void* rpath, hb_draw_state_t*, void*) {
+static void rpath_close(hb_draw_funcs_t*, void* rpath, hb_draw_state_t*, void*) {
     ((rive::RawPath*)rpath)->close();
 }
 }
@@ -151,7 +153,7 @@
         vars[i] = {coords[i].axis, coords[i].value};
     }
     auto font = hb_font_create_sub_font(m_Font);
-    hb_font_set_variations(font, vars.data(), vars.size());
+    hb_font_set_variations(font, vars.data(), (unsigned int)vars.size());
     return rive::rcp<rive::RenderFont>(new HBRenderFont(font));
 }
 
@@ -199,7 +201,7 @@
         //            hb_position_t y_offset  = glyph_pos[i].y_offset;
 
         gr.glyphs[i] = (uint16_t)glyph_info[i].codepoint;
-        gr.textOffsets[i] = textOffset + glyph_info[i].cluster;
+        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
@@ -212,7 +214,7 @@
                                            size_t end) {
     auto count = end - start;
     rive::RenderGlyphRun subset(rive::SimpleArray<rive::GlyphID>(&orig.glyphs[start], count),
-                                rive::SimpleArray<uint32_t>(&orig.textOffsets[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;
@@ -235,8 +237,8 @@
             while (endI < count && orig.glyphs[endI] == 0) {
                 ++endI;
             }
-            auto textStart = orig.textOffsets[startI];
-            auto textCount = orig.textOffsets[endI - 1] - textStart + 1;
+            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 {
@@ -268,7 +270,7 @@
         } 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.textOffsets[index]];
+            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) {
diff --git a/test/line_break_test.cpp b/test/line_break_test.cpp
new file mode 100644
index 0000000..c56b831
--- /dev/null
+++ b/test/line_break_test.cpp
@@ -0,0 +1,238 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#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 "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[]) {
+    const uint8_t* ptr = (const uint8_t*)text;
+    uint32_t n = 0;
+    while (*ptr) {
+        unichars->push_back(rive::UTF::NextUTF8(&ptr));
+        n += 1;
+    }
+    return {std::move(font), size, n};
+}
+
+static rcp<RenderFont> loadFont(const char* filename) {
+    FILE* fp = fopen("../../test/assets/RobotoFlex.ttf", "rb");
+    REQUIRE(fp != nullptr);
+
+    fseek(fp, 0, SEEK_END);
+    const size_t length = ftell(fp);
+    fseek(fp, 0, SEEK_SET);
+    std::vector<uint8_t> bytes(length);
+    REQUIRE(fread(bytes.data(), 1, length, fp) == length);
+    fclose(fp);
+
+    return HBRenderFont::Decode(bytes);
+}
+
+TEST_CASE("line breaker separates words", "[line break]") {
+    auto font = loadFont("../../test/assets/RobotoFlex.ttf");
+    REQUIRE(font != nullptr);
+
+    // one two⏎ three
+    std::vector<rive::RenderTextRun> 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();
+    REQUIRE(run.breaks.size() == 6);
+    REQUIRE(run.breaks[0] == 0);
+    REQUIRE(run.breaks[1] == 3);
+    REQUIRE(run.breaks[2] == 4);
+    REQUIRE(run.breaks[3] == 7);
+    REQUIRE(run.breaks[4] == 8);
+    REQUIRE(run.breaks[5] == 13);
+}
+
+TEST_CASE("line breaker handles multiple runs", "[line break]") {
+    auto font = loadFont("../../test/assets/RobotoFlex.ttf");
+    REQUIRE(font != nullptr);
+
+    std::vector<rive::RenderTextRun> 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 run = shape.front();
+        REQUIRE(run.breaks.size() == 5);
+        REQUIRE(run.breaks[0] == 0);
+        REQUIRE(run.breaks[1] == 3);
+        REQUIRE(run.breaks[2] == 4);
+        REQUIRE(run.breaks[3] == 7);
+        REQUIRE(run.breaks[4] == 8);
+    }
+    {
+        auto run = shape.back();
+        REQUIRE(run.breaks.size() == 3);
+        REQUIRE(run.breaks[0] == 2);
+        REQUIRE(run.breaks[1] == 3);
+        REQUIRE(run.breaks[2] == 7);
+    }
+}
+
+TEST_CASE("line breaker handles returns", "[line break]") {
+    auto font = loadFont("../../test/assets/RobotoFlex.ttf");
+    REQUIRE(font != nullptr);
+
+    std::vector<rive::RenderTextRun> 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"));
+
+    auto shape = font->shapeText(unichars, truns);
+    REQUIRE(shape.size() == 2);
+    {
+        auto run = shape.front();
+        REQUIRE(run.breaks.size() == 5);
+        REQUIRE(run.breaks[0] == 0);
+        REQUIRE(run.breaks[1] == 3);
+        REQUIRE(run.breaks[2] == 4);
+        REQUIRE(run.breaks[3] == 7);
+        REQUIRE(run.breaks[4] == 8);
+    }
+    {
+        auto run = shape.back();
+        REQUIRE(run.breaks.size() == 5);
+        REQUIRE(run.breaks[0] == 2);
+        REQUIRE(run.breaks[1] == 2);
+        REQUIRE(run.breaks[2] == 2);
+        REQUIRE(run.breaks[3] == 4);
+        REQUIRE(run.breaks[4] == 8);
+    }
+}
+
+TEST_CASE("line breaker builds lines", "[line break]") {
+    auto font = loadFont("../../test/assets/RobotoFlex.ttf");
+    REQUIRE(font != nullptr);
+
+    // one two⏎ three
+    std::vector<rive::RenderTextRun> 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();
+
+    // at 194 everything fits in one line
+    {
+        auto lines = RenderGlyphLine::BreakLines(shape, 194.0f, RenderTextAlign::left);
+        REQUIRE(lines.size() == 1);
+        auto line = lines.back();
+        REQUIRE(line.startRun == 0);
+        REQUIRE(line.startIndex == 0);
+        REQUIRE(line.endRun == 0);
+        REQUIRE(line.endIndex == 13);
+    }
+    // at 191 "three" should pop to second line
+    {
+        auto lines = RenderGlyphLine::BreakLines(shape, 191.0f, RenderTextAlign::left);
+        REQUIRE(lines.size() == 2);
+        {
+            auto line = lines.front();
+            REQUIRE(line.startRun == 0);
+            REQUIRE(line.startIndex == 0);
+            REQUIRE(line.endRun == 0);
+            REQUIRE(line.endIndex == 7);
+        }
+
+        {
+            auto line = lines.back();
+            REQUIRE(line.startRun == 0);
+            REQUIRE(line.startIndex == 8);
+            REQUIRE(line.endRun == 0);
+            REQUIRE(line.endIndex == 13);
+        }
+    }
+}
+
+TEST_CASE("line breaker deals with extremes", "[line break]") {
+    auto font = loadFont("../../test/assets/RobotoFlex.ttf");
+    REQUIRE(font != nullptr);
+
+    // one two⏎ three
+    std::vector<rive::RenderTextRun> 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 lines = RenderGlyphLine::BreakLines(shape, 17.0f, RenderTextAlign::left);
+        REQUIRE(lines.size() == 2);
+        {
+            auto line = lines.front();
+            REQUIRE(line.startRun == 0);
+            REQUIRE(line.startIndex == 0);
+            REQUIRE(line.endRun == 0);
+            REQUIRE(line.endIndex == 1);
+        }
+
+        {
+            auto line = lines.back();
+            REQUIRE(line.startRun == 0);
+            REQUIRE(line.startIndex == 1);
+            REQUIRE(line.endRun == 0);
+            REQUIRE(line.endIndex == 2);
+        }
+    }
+    // Test that it also handles 0 width.
+    {
+        auto lines = RenderGlyphLine::BreakLines(shape, 0.0f, RenderTextAlign::left);
+        REQUIRE(lines.size() == 2);
+        {
+            auto line = lines.front();
+            REQUIRE(line.startRun == 0);
+            REQUIRE(line.startIndex == 0);
+            REQUIRE(line.endRun == 0);
+            REQUIRE(line.endIndex == 1);
+        }
+
+        {
+            auto line = lines.back();
+            REQUIRE(line.startRun == 0);
+            REQUIRE(line.startIndex == 1);
+            REQUIRE(line.endRun == 0);
+            REQUIRE(line.endIndex == 2);
+        }
+    }
+}
+
+TEST_CASE("line breaker breaks return characters", "[line break]") {
+    auto font = loadFont("../../test/assets/RobotoFlex.ttf");
+    REQUIRE(font != nullptr);
+
+    // one two⏎ three
+    std::vector<rive::RenderTextRun> truns;
+    std::vector<rive::Unichar> unichars;
+    truns.push_back(append(&unichars, font, 32.0f, "hello look\nhere"));
+
+    auto shape = font->shapeText(unichars, truns);
+    REQUIRE(shape.size() == 1);
+    auto run = shape.front();
+
+    {
+        auto lines = RenderGlyphLine::BreakLines(shape, 300.0f, RenderTextAlign::left);
+        REQUIRE(lines.size() == 2);
+    }
+}
diff --git a/test/simple_array_test.cpp b/test/simple_array_test.cpp
index e3847a0..cdfd3f2 100644
--- a/test/simple_array_test.cpp
+++ b/test/simple_array_test.cpp
@@ -139,4 +139,33 @@
     REQUIRE(SimpleArrayTesting::mallocCount == 4);
     // Realloc one more time as we sized down.
     REQUIRE(SimpleArrayTesting::reallocCount == 2);
-}
\ No newline at end of file
+}
+
+TEST_CASE("builders can be reset", "[simple array]") {
+    SimpleArrayTesting::resetCounters();
+    SimpleArrayBuilder<uint32_t> builder(3);
+    builder.add(1);
+    builder.add(2);
+    builder.add(3);
+    REQUIRE(SimpleArrayTesting::mallocCount == 1);
+    REQUIRE(SimpleArrayTesting::freeCount == 0);
+    REQUIRE(SimpleArrayTesting::reallocCount == 0);
+
+    builder = SimpleArrayBuilder<uint32_t>(4);
+    // Previous builder got freed.
+    REQUIRE(SimpleArrayTesting::freeCount == 1);
+    // We allocated more memory.
+    REQUIRE(SimpleArrayTesting::mallocCount == 2);
+    REQUIRE(SimpleArrayTesting::reallocCount == 0);
+    builder.add(3);
+    builder.add(2);
+
+    SimpleArrayTesting::resetCounters();
+    SimpleArray<uint32_t> array = std::move(builder);
+    // Realloc'd down
+    REQUIRE(SimpleArrayTesting::reallocCount == 1);
+    // Free and malloc counts didn't move.
+    REQUIRE(SimpleArrayTesting::freeCount == 0);
+    REQUIRE(SimpleArrayTesting::mallocCount == 0);
+    REQUIRE(array.size() == 2);
+}
diff --git a/viewer/build/macosx/build_viewer.sh b/viewer/build/macosx/build_viewer.sh
index f5c5cd1..dbb4803 100755
--- a/viewer/build/macosx/build_viewer.sh
+++ b/viewer/build/macosx/build_viewer.sh
@@ -68,7 +68,7 @@
 
 pushd ..
 
-$PREMAKE --file=./premake5_viewer.lua gmake2 --graphics=$GRAPHICS --renderer=$RENDERER --with_rive_tools
+$PREMAKE --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 417cbac..a7d79a0 100644
--- a/viewer/build/premake5_viewer.lua
+++ b/viewer/build/premake5_viewer.lua
@@ -33,7 +33,7 @@
     targetdir('%{cfg.system}/bin/%{cfg.buildcfg}/' .. _OPTIONS.renderer .. '/' .. _OPTIONS.graphics)
     objdir('%{cfg.system}/obj/%{cfg.buildcfg}/' .. _OPTIONS.renderer .. '/' .. _OPTIONS.graphics)
 
-    defines {'RIVE_TEXT'}
+    defines {'WITH_RIVE_TEXT'}
 
     includedirs {
         '../include',
@@ -58,7 +58,6 @@
         '../src/**.cpp',
         rive .. '/utils/**.cpp',
         rive .. '/skia/renderer/src/renderfont_coretext.cpp',
-        rive .. '/skia/renderer/src/renderfont_hb.cpp',
         dependencies .. '/imgui/imgui.cpp',
         dependencies .. '/imgui/imgui_widgets.cpp',
         dependencies .. '/imgui/imgui_tables.cpp',
diff --git a/viewer/src/viewer_content/text_content.cpp b/viewer/src/viewer_content/text_content.cpp
index fc1990c..1897700 100644
--- a/viewer/src/viewer_content/text_content.cpp
+++ b/viewer/src/viewer_content/text_content.cpp
@@ -74,7 +74,7 @@
                     run,
                     startGIndex,
                     endGIndex,
-                    {origin.x - x0, origin.y + line.baseline});
+                    {origin.x - x0 + line.startX, origin.y + line.baseline});
             startGIndex = 0;
         }
     }
@@ -152,6 +152,8 @@
     std::vector<RenderFontGlyphRuns> m_gruns;
     rive::Mat2D m_xform;
     float m_width = 300;
+    bool m_autoWidth = false;
+    int m_align = 0;
 
     RenderFontTextRuns make_truns(RenderFontFactory fact) {
         auto loader = [fact](const char filename[]) -> rive::rcp<rive::RenderFont> {
@@ -177,11 +179,21 @@
 
         RenderFontTextRuns truns;
 
-        truns.push_back(append(&m_unichars, font0->makeAtCoord(c2), 60, "U"));
-        truns.push_back(append(&m_unichars, font0->makeAtCoord(c1), 30, "ne漢字asy"));
-        truns.push_back(append(&m_unichars, font1, 30, " fits the crown"));
-        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, "U"));
+        // 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, 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, font0, 52.0f, "one two three\n\n\nfour five six seven"));
+
+        // truns.push_back(append(&m_unichars, font0, 32.0f, "ab"));
+        // truns.push_back(append(&m_unichars, font0, 60.0f, "ee\n four"));
 
         m_breaks = compute_word_breaks(m_unichars);
 
@@ -200,10 +212,14 @@
         renderer->save();
         renderer->transform(m_xform);
 
-        auto lines = rive::RenderGlyphLine::BreakLines(gruns, m_breaks, width);
+        auto lines = rive::RenderGlyphLine::BreakLines(gruns,
+                                                       m_autoWidth ? -1.0f : width,
+                                                       (rive::RenderTextAlign)m_align);
 
         drawpara(RiveFactory(), renderer, lines, gruns, {0, 0});
-        draw_line(RiveFactory(), renderer, width);
+        if (!m_autoWidth) {
+            draw_line(RiveFactory(), renderer, width);
+        }
 
         renderer->restore();
     }
@@ -217,8 +233,11 @@
 
     void handleResize(int width, int height) override {}
     void handleImgui() override {
+        const char* alignOptions[] = {"left", "right", "center"};
         ImGui::Begin("text", nullptr);
-        ImGui::SliderFloat("Width", &m_width, 10, 400);
+        ImGui::SliderFloat("Width", &m_width, 1, 400);
+        ImGui::Checkbox("Autowidth", &m_autoWidth);
+        ImGui::Combo("combo", &m_align, alignOptions, IM_ARRAYSIZE(alignOptions));
         ImGui::End();
     }
 };