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(); } };