RenderText work
Introduce a new header : render_text.hpp
This has new types, including a new virtual class RenderFont that clients need to subclass and provide.
This PR includes two experimental implementations:
- skia based (but trivial shaper : no shaping, no kerning, no intl support)
- harfbuzz based (good shaper, but no support for color glyphs)
Neither try to perform font substitution yet.
This PR has **no impact** on Runtimes yet -- it is just play code that Viewer can test.
Diffs=
5da5fa606 RenderText work
diff --git a/.rive_head b/.rive_head
index b487f8a..fc25c85 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-a798ea7de2d9189a24fa9101813c1b42498f69df
+5da5fa606df8b3e05ab15e9a1297e4c941d35c60
diff --git a/include/rive/render_text.hpp b/include/rive/render_text.hpp
index 20d7206..f610b74 100644
--- a/include/rive/render_text.hpp
+++ b/include/rive/render_text.hpp
@@ -14,8 +14,18 @@
using Unichar = int32_t;
using GlyphID = uint16_t;
+struct RenderTextRun;
+struct RenderGlyphRun;
+
class RenderFont : public RefCnt {
public:
+ struct LineMetrics {
+ float ascent,
+ descent;
+ };
+
+ const LineMetrics& lineMetrics() const { return m_LineMetrics; }
+
// This is experimental
// -- may only be needed by Editor
// -- so it may be removed from here later
@@ -55,6 +65,18 @@
// relative to (0,0) with the typographic baseline at y = 0.
//
virtual RawPath getPath(GlyphID) const = 0;
+
+ std::vector<RenderGlyphRun> shapeText(rive::Span<const rive::Unichar> text,
+ rive::Span<const rive::RenderTextRun> runs) const;
+
+protected:
+ RenderFont(const LineMetrics& lm) : m_LineMetrics(lm) {}
+
+ virtual std::vector<RenderGlyphRun> onShapeText(rive::Span<const rive::Unichar> text,
+ rive::Span<const rive::RenderTextRun> runs) const = 0;
+
+private:
+ const LineMetrics m_LineMetrics;
};
struct RenderTextRun {
@@ -66,10 +88,10 @@
struct RenderGlyphRun {
rcp<RenderFont> font;
float size;
- uint32_t startTextIndex;
- std::vector<GlyphID> glyphs;
- std::vector<float> xpos; // xpos.size() == glyphs.size() + 1
+ std::vector<GlyphID> glyphs; // [#glyphs]
+ std::vector<uint32_t> textOffsets; // [#glyphs]
+ std::vector<float> xpos; // [#glyphs + 1]
};
} // namespace rive
diff --git a/skia/renderer/build/premake5.lua b/skia/renderer/build/premake5.lua
index c1f7051..2726453 100644
--- a/skia/renderer/build/premake5.lua
+++ b/skia/renderer/build/premake5.lua
@@ -10,7 +10,11 @@
toolset "clang"
targetdir "%{cfg.system}/bin/%{cfg.buildcfg}"
objdir "%{cfg.system}/obj/%{cfg.buildcfg}"
- includedirs {"../include", "../../../include"}
+ includedirs {
+ "../../../../../third_party/externals/harfbuzz/src",
+ "../include",
+ "../../../include"
+ }
if os.host() == "macosx" then
links {"Cocoa.framework", "rive", "skia"}
@@ -30,7 +34,9 @@
libdirs {"../../../build/%{cfg.system}/bin/%{cfg.buildcfg}"}
- files {"../src/**.cpp"}
+ files {
+ "../src/skia_factory.cpp",
+ }
buildoptions {"-Wall", "-fno-exceptions", "-fno-rtti", "-Werror=format"}
@@ -84,6 +90,7 @@
libdirs {"../../dependencies/" .. SKIA_DIR.. "/out/arm64"}
filter "configurations:debug"
+ buildoptions {"-g"}
defines {"DEBUG"}
symbols "On"
diff --git a/skia/renderer/include/line_breaker.hpp b/skia/renderer/include/line_breaker.hpp
new file mode 100644
index 0000000..aa45448
--- /dev/null
+++ b/skia/renderer/include/line_breaker.hpp
@@ -0,0 +1,44 @@
+#ifndef _RIVE_RENDER_GLYPH_LINE_H_
+#define _RIVE_RENDER_GLYPH_LINE_H_
+
+#include "rive/render_text.hpp"
+
+namespace rive {
+
+struct RenderGlyphLine {
+ int startRun;
+ int startIndex;
+ int endRun;
+ int endIndex;
+ int wsRun;
+ int wsIndex;
+ 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)
+ {}
+
+ static std::vector<RenderGlyphLine> BreakLines(Span<const RenderGlyphRun> runs,
+ Span<const int> breaks,
+ float width);
+ // Compute vaues for top/baseline/bottom per line
+ static void ComputeLineSpacing(rive::Span<RenderGlyphLine>,
+ rive::Span<const RenderGlyphRun>);
+};
+
+} // namespace
+
+#endif
diff --git a/skia/renderer/include/renderfont_hb.hpp b/skia/renderer/include/renderfont_hb.hpp
new file mode 100644
index 0000000..fce1980
--- /dev/null
+++ b/skia/renderer/include/renderfont_hb.hpp
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#ifndef _RIVE_RENDERFONT_HB_HPP_
+#define _RIVE_RENDERFONT_HB_HPP_
+
+#include "rive/factory.hpp"
+#include "rive/render_text.hpp"
+
+struct hb_font_t;
+struct hb_draw_funcs_t;
+
+class HBRenderFont : public rive::RenderFont {
+ hb_draw_funcs_t* m_DrawFuncs;
+
+public:
+ hb_font_t* m_Font;
+
+ // We assume ownership of font!
+ HBRenderFont(hb_font_t* font);
+ ~HBRenderFont() override;
+
+ std::vector<Axis> getAxes() const override;
+ std::vector<Coord> getCoords() const override;
+ rive::rcp<rive::RenderFont> makeAtCoords(rive::Span<const Coord>) const override;
+ rive::RawPath getPath(rive::GlyphID) const override;
+ std::vector<rive::RenderGlyphRun> onShapeText(rive::Span<const rive::Unichar>,
+ rive::Span<const rive::RenderTextRun>) const override;
+
+ static rive::rcp<rive::RenderFont> Decode(rive::Span<const uint8_t>);
+};
+
+#endif
diff --git a/skia/renderer/include/renderfont_skia.hpp b/skia/renderer/include/renderfont_skia.hpp
new file mode 100644
index 0000000..c7593ba
--- /dev/null
+++ b/skia/renderer/include/renderfont_skia.hpp
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#ifndef _RIVE_RENDERFONT_SKIA_HPP_
+#define _RIVE_RENDERFONT_SKIA_HPP_
+
+#include "rive/render_text.hpp"
+#include "include/core/SkTypeface.h"
+
+class SkiaRenderFont : public rive::RenderFont {
+public:
+ sk_sp<SkTypeface> m_Typeface;
+
+ SkiaRenderFont(sk_sp<SkTypeface>);
+
+ std::vector<Axis> getAxes() const override;
+ std::vector<Coord> getCoords() const override;
+ rive::rcp<rive::RenderFont> makeAtCoords(rive::Span<const Coord>) const override;
+ rive::RawPath getPath(rive::GlyphID) const override;
+ std::vector<rive::RenderGlyphRun> onShapeText(rive::Span<const rive::Unichar>,
+ rive::Span<const rive::RenderTextRun>) const override;
+
+ static rive::rcp<rive::RenderFont> Decode(rive::Span<const uint8_t>);
+};
+
+#endif
diff --git a/skia/renderer/src/line_breaker.cpp b/skia/renderer/src/line_breaker.cpp
new file mode 100644
index 0000000..53f8db5
--- /dev/null
+++ b/skia/renderer/src/line_breaker.cpp
@@ -0,0 +1,144 @@
+#include "line_breaker.hpp"
+
+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 run.glyphs.size() - 1;
+ }
+ return 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];
+ int 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 = runs.size() - 1;
+ const int tailIndex = 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(toSpan(lines), runs);
+
+ return lines;
+ }
+
+void RenderGlyphLine::ComputeLineSpacing(Span<RenderGlyphLine> lines,
+ Span<const RenderGlyphRun> runs) {
+ 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) {
+ const auto& run = runs[i];
+
+ asc = std::min(asc, run.font->lineMetrics().ascent * run.size);
+ des = std::max(des, run.font->lineMetrics().descent * run.size);
+ }
+ line.top = Y;
+ Y -= asc;
+ line.baseline = Y;
+ Y += des;
+ line.bottom = Y;
+ }
+ // TODO: good place to perform left/center/right alignment
+
+}
diff --git a/skia/renderer/src/renderfont_hb.cpp b/skia/renderer/src/renderfont_hb.cpp
new file mode 100644
index 0000000..7d9c441
--- /dev/null
+++ b/skia/renderer/src/renderfont_hb.cpp
@@ -0,0 +1,203 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#include "renderfont_hb.hpp"
+
+#include "rive/factory.hpp"
+#include "rive/render_text.hpp"
+
+#include "hb.h"
+#include "hb-ot.h"
+//#include "harfbuzz/hb.h"
+//#include "harfbuzz/hb-ot.h"
+
+rive::rcp<rive::RenderFont> HBRenderFont::Decode(rive::Span<const uint8_t> span) {
+ auto blob = hb_blob_create_or_fail((const char*)span.data(), (unsigned)span.size(),
+ HB_MEMORY_MODE_DUPLICATE, nullptr, nullptr);
+ if (blob) {
+ auto face = hb_face_create(blob, 0);
+ hb_blob_destroy(blob);
+ if (face) {
+ auto font = hb_font_create(face);
+ hb_face_destroy(face);
+ if (font) {
+ return rive::rcp<rive::RenderFont>(new HBRenderFont(font));
+ }
+ }
+ }
+ return nullptr;
+}
+
+//////////////
+
+constexpr int kStdScale = 2048;
+constexpr float gInvScale = 1.0f / kStdScale;
+
+extern "C" {
+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*) {
+ ((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*) {
+ ((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*) {
+ ((rive::RawPath*)rpath)->cubicTo(x1 * gInvScale, -y1 * gInvScale,
+ x2 * gInvScale, -y2 * gInvScale,
+ x3 * gInvScale, -y3 * gInvScale);
+}
+void rpath_close(hb_draw_funcs_t*, void* rpath, hb_draw_state_t*, void*) {
+ ((rive::RawPath*)rpath)->close();
+}
+}
+
+static rive::RenderFont::LineMetrics make_lmx(hb_font_t* font) {
+ // premable on font...
+ hb_ot_font_set_funcs(font);
+ hb_font_set_scale(font, kStdScale, kStdScale);
+
+ hb_font_extents_t extents;
+ hb_font_get_h_extents(font, &extents);
+ return {-extents.ascender * gInvScale, -extents.descender * gInvScale};
+}
+
+HBRenderFont::HBRenderFont(hb_font_t* font) :
+ RenderFont(make_lmx(font)),
+ m_Font(font) // we just take ownership, no need to call reference()
+{
+ m_DrawFuncs = hb_draw_funcs_create();
+ hb_draw_funcs_set_move_to_func(m_DrawFuncs, rpath_move_to, nullptr, nullptr);
+ hb_draw_funcs_set_line_to_func(m_DrawFuncs, rpath_line_to, nullptr, nullptr);
+ hb_draw_funcs_set_quadratic_to_func(m_DrawFuncs, rpath_quad_to, nullptr, nullptr);
+ hb_draw_funcs_set_cubic_to_func(m_DrawFuncs, rpath_cubic_to, nullptr, nullptr);
+ hb_draw_funcs_set_close_path_func(m_DrawFuncs, rpath_close, nullptr, nullptr);
+ hb_draw_funcs_make_immutable(m_DrawFuncs);
+}
+
+HBRenderFont::~HBRenderFont() {
+ hb_draw_funcs_destroy(m_DrawFuncs);
+ hb_font_destroy(m_Font);
+}
+
+std::vector<rive::RenderFont::Axis> HBRenderFont::getAxes() const {
+ auto face = hb_font_get_face(m_Font);
+ std::vector<rive::RenderFont::Axis> axes;
+
+ const int count = hb_ot_var_get_axis_count(face);
+ if (count > 0) {
+ axes.resize(count);
+
+ hb_ot_var_axis_info_t info;
+ for (int i = 0; i < count; ++i) {
+ unsigned n = 1;
+ hb_ot_var_get_axis_infos(face, i, &n, &info);
+ assert(n == 1);
+ axes[i] = { info.tag, info.min_value, info.default_value, info.max_value };
+ // printf("[%d] %08X %g %g %g\n", i, info.tag, info.min_value, info.default_value, info.max_value);
+ }
+ }
+ return axes;
+}
+
+std::vector<rive::RenderFont::Coord> HBRenderFont::getCoords() const {
+ auto axes = this->getAxes();
+ // const int count = (int)axes.size();
+
+ unsigned length;
+ const float* values = hb_font_get_var_coords_design(m_Font, &length);
+
+ std::vector<rive::RenderFont::Coord> coords(length);
+ for (unsigned i = 0; i < length; ++i) {
+ coords[i] = { axes[i].tag, values[i] };
+ }
+ return coords;
+}
+
+rive::rcp<rive::RenderFont> HBRenderFont::makeAtCoords(rive::Span<const Coord> coords) const {
+ const int count = (int)coords.size();
+ std::vector<hb_variation_t> vars(count);
+ for (int i = 0; i < count; ++i) {
+ vars[i].tag = coords[i].axis;
+ vars[i].value = coords[i].value;
+ }
+
+ auto font = hb_font_create_sub_font(m_Font);
+ hb_font_set_variations(font, vars.data(), count);
+ return rive::rcp<rive::RenderFont>(new HBRenderFont(font));
+}
+
+rive::RawPath HBRenderFont::getPath(rive::GlyphID glyph) const {
+ rive::RawPath rpath;
+ hb_font_get_glyph_shape(m_Font, glyph, m_DrawFuncs, &rpath);
+ return rpath;
+}
+
+///////////////////////////////////////////////////////////
+
+std::vector<rive::RenderGlyphRun>
+HBRenderFont::onShapeText(rive::Span<const rive::Unichar> text,
+ rive::Span<const rive::RenderTextRun> truns) const {
+ std::vector<rive::RenderGlyphRun> gruns;
+ gruns.reserve(truns.size());
+
+ /////////////////
+
+ const hb_feature_t features[] = {
+ { 'liga', 1, HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END },
+ { 'dlig', 1, HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END },
+ { 'kern', 1, HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END },
+ };
+ constexpr int numFeatures = sizeof(features) / sizeof(features[0]);
+
+ uint32_t unicharIndex = 0;
+ rive::Vec2D origin = {0, 0};
+ for (const auto& tr : truns) {
+ hb_buffer_t *buf = hb_buffer_create();
+ hb_buffer_add_utf32(buf, (const uint32_t*)&text[unicharIndex], tr.unicharCount, 0, tr.unicharCount);
+
+ hb_buffer_set_direction(buf, HB_DIRECTION_LTR);
+ hb_buffer_set_script(buf, HB_SCRIPT_LATIN);
+ hb_buffer_set_language(buf, hb_language_from_string("en", -1));
+
+ auto hbfont = (HBRenderFont*)tr.font.get();
+ hb_shape(hbfont->m_Font, buf, features, numFeatures);
+
+ unsigned int glyph_count;
+ hb_glyph_info_t *glyph_info = hb_buffer_get_glyph_infos(buf, &glyph_count);
+ hb_glyph_position_t *glyph_pos = hb_buffer_get_glyph_positions(buf, &glyph_count);
+
+ // todo: check for missing glyphs, and perform font-substitution
+
+ rive::RenderGlyphRun gr;
+ gr.font = tr.font;
+ gr.size = tr.size;
+ gr.glyphs.resize(glyph_count);
+ gr.textOffsets.resize(glyph_count);
+ gr.xpos.resize(glyph_count + 1);
+
+ const float scale = tr.size / kStdScale;
+
+ for (unsigned int i = 0; i < glyph_count; i++) {
+// hb_position_t x_offset = glyph_pos[i].x_offset;
+// hb_position_t y_offset = glyph_pos[i].y_offset;
+
+ gr.glyphs[i] = (uint16_t)glyph_info[i].codepoint;
+ gr.textOffsets[i] = unicharIndex + glyph_info[i].cluster;
+ gr.xpos[i] = origin.x;
+
+ origin.x += glyph_pos[i].x_advance * scale;
+ }
+ gr.xpos[glyph_count] = origin.x;
+ gruns.push_back(std::move(gr));
+
+ unicharIndex += tr.unicharCount;
+ hb_buffer_destroy(buf);
+ }
+
+ return gruns;
+}
diff --git a/skia/viewer/src/fontmgr.cpp b/skia/renderer/src/renderfont_skia.cpp
similarity index 69%
rename from skia/viewer/src/fontmgr.cpp
rename to skia/renderer/src/renderfont_skia.cpp
index 4298c3c..825fa5d 100644
--- a/skia/viewer/src/fontmgr.cpp
+++ b/skia/renderer/src/renderfont_skia.cpp
@@ -4,23 +4,38 @@
#include "rive/factory.hpp"
#include "rive/render_text.hpp"
-#include "skia_rive_fontmgr.hpp"
+#include "renderfont_skia.hpp"
-#include "include/core/SkTypeface.h"
+#include "include/core/SkData.h"
#include "include/core/SkFont.h"
+#include "include/core/SkFontMetrics.h"
#include "include/core/SkPath.h"
+#include "include/core/SkTypeface.h"
-class SkiaRenderFont : public rive::RenderFont {
-public:
- sk_sp<SkTypeface> m_Typeface;
+static void setupFont(SkFont* font) {
+ font->setLinearMetrics(true);
+ font->setBaselineSnap(false);
+ font->setHinting(SkFontHinting::kNone);
+}
- SkiaRenderFont(sk_sp<SkTypeface> tf) : m_Typeface(std::move(tf)) {}
+static rive::RenderFont::LineMetrics make_lmx(sk_sp<SkTypeface> tf) {
+ SkFont font(tf, 1.0f);
+ setupFont(&font);
+ SkFontMetrics metrics;
+ (void)font.getMetrics(&metrics);
+ return {metrics.fAscent, metrics.fDescent};
+}
- std::vector<Axis> getAxes() const override;
- std::vector<Coord> getCoords() const override;
- rive::rcp<rive::RenderFont> makeAtCoords(rive::Span<const Coord>) const override;
- rive::RawPath getPath(rive::GlyphID) const override;
-};
+SkiaRenderFont::SkiaRenderFont(sk_sp<SkTypeface> tf) :
+ RenderFont(make_lmx(tf)),
+ m_Typeface(std::move(tf))
+{}
+
+rive::rcp<rive::RenderFont> SkiaRenderFont::Decode(rive::Span<const uint8_t> span) {
+ auto tf = SkTypeface::MakeFromData(SkData::MakeWithCopy(span.data(),
+ span.size()));
+ return tf ? rive::rcp<rive::RenderFont>(new SkiaRenderFont(std::move(tf))) : nullptr;
+}
std::vector<rive::RenderFont::Axis> SkiaRenderFont::getAxes() const {
std::vector<rive::RenderFont::Axis> axes;
@@ -70,12 +85,6 @@
static inline rive::Vec2D rv(SkPoint p) { return rive::Vec2D(p.fX, p.fY); }
-static void setupFont(SkFont* font) {
- font->setLinearMetrics(true);
- font->setBaselineSnap(false);
- font->setHinting(SkFontHinting::kNone);
-}
-
rive::RawPath SkiaRenderFont::getPath(rive::GlyphID glyph) const {
SkFont font(m_Typeface, 1.0f);
setupFont(&font);
@@ -103,62 +112,45 @@
///////////////////////////////////////////////////////////
-rive::rcp<rive::RenderFont> SkiaRiveFontMgr::decodeFont(rive::Span<const uint8_t> data) {
- auto tf = SkTypeface::MakeDefault(); // ignoring data for now
- return rive::rcp<rive::RenderFont>(new SkiaRenderFont(std::move(tf)));
-}
-
-rive::rcp<rive::RenderFont> SkiaRiveFontMgr::findFont(const char name[]) {
- auto tf = SkTypeface::MakeFromName(name, SkFontStyle());
- if (!tf) {
- tf = SkTypeface::MakeDefault();
- }
- return rive::rcp<rive::RenderFont>(new SkiaRenderFont(std::move(tf)));
-}
-
static float shapeRun(rive::RenderGlyphRun* grun, const rive::RenderTextRun& trun,
const rive::Unichar text[], size_t textOffset, float origin) {
+ const int glyphCount = SkToInt(trun.unicharCount); // simple shaper, no ligatures
+
grun->font = trun.font;
grun->size = trun.size;
- grun->startTextIndex = textOffset;
- grun->glyphs.resize(trun.unicharCount);
- grun->xpos.resize(trun.unicharCount + 1);
- const int count = SkToInt(trun.unicharCount);
+ grun->glyphs.resize(glyphCount);
+ grun->textOffsets.resize(glyphCount);
+ grun->xpos.resize(glyphCount + 1);
SkiaRenderFont* rfont = static_cast<SkiaRenderFont*>(trun.font.get());
- rfont->m_Typeface->unicharsToGlyphs(text + textOffset, count, grun->glyphs.data());
+ rfont->m_Typeface->unicharsToGlyphs(text + textOffset, glyphCount, grun->glyphs.data());
+
+ // simple shaper, assume one glyph per char
+ for (int i = 0; i < glyphCount; ++i) {
+ grun->textOffsets[i] = textOffset + i;
+ }
SkFont font(rfont->m_Typeface, grun->size);
setupFont(&font);
// We get 'widths' from skia, but then turn them into xpos
// this will write count values, but xpos has count+1 slots
- font.getWidths(grun->glyphs.data(), count, grun->xpos.data());
+ font.getWidths(grun->glyphs.data(), glyphCount, grun->xpos.data());
for (auto& xp : grun->xpos) {
auto width = xp;
xp = origin;
origin += width;
}
- return grun->xpos.data()[count];
+ return grun->xpos.data()[glyphCount];
}
std::vector<rive::RenderGlyphRun>
-SkiaRiveFontMgr::shapeText(rive::Span<const rive::Unichar> text,
- rive::Span<const rive::RenderTextRun> truns) {
- std::vector<rive::RenderGlyphRun> gruns;
+SkiaRenderFont::onShapeText(rive::Span<const rive::Unichar> text,
+ rive::Span<const rive::RenderTextRun> truns) const {
+ std::vector<rive::RenderGlyphRun> gruns(truns.size());
- // sanity check
- size_t count = 0;
- for (const auto& tr : truns) {
- count += tr.unicharCount;
- }
- if (count > text.size()) {
- return gruns; // not enough text, so abort
- }
-
- gruns.resize(truns.size());
int i = 0;
size_t offset = 0;
float origin = 0;
diff --git a/skia/viewer/build/premake5.lua b/skia/viewer/build/premake5.lua
index c2d5b58..8202faf 100644
--- a/skia/viewer/build/premake5.lua
+++ b/skia/viewer/build/premake5.lua
@@ -5,6 +5,10 @@
location("./")
dofile(path.join(BASE_DIR, "premake5.lua"))
+BASE_DIR = path.getabsolute("../../../../../third_party/harfbuzz/build")
+location("./")
+dofile(path.join(BASE_DIR, "premake5.lua"))
+
BASE_DIR = path.getabsolute("../../renderer/build")
location("./")
dofile(path.join(BASE_DIR, "premake5.lua"))
@@ -27,7 +31,8 @@
"../../dependencies/skia/include/config",
"../../dependencies/imgui",
"../../dependencies",
- "../../dependencies/gl3w/build/include"
+ "../../dependencies/gl3w/build/include",
+ "../../../../../third_party/externals/harfbuzz/src",
}
links {
@@ -35,12 +40,14 @@
"IOKit.framework",
"CoreVideo.framework",
"rive",
+ "rive_harfbuzz",
"skia",
"rive_skia_renderer",
"glfw3"
}
libdirs {
+ "../../../../../third_party/harfbuzz/build/%{cfg.buildcfg}/bin",
"../../../build/%{cfg.system}/bin/%{cfg.buildcfg}",
"../../dependencies/glfw_build/src",
"../../dependencies/skia/out/static",
@@ -49,6 +56,11 @@
files {
"../src/**.cpp",
+
+ "../../renderer/src/line_breaker.cpp",
+ "../../renderer/src/renderfont_hb.cpp",
+ "../../renderer/src/renderfont_skia.cpp",
+
"../../dependencies/gl3w/build/src/gl3w.c",
"../../dependencies/imgui/backends/imgui_impl_glfw.cpp",
"../../dependencies/imgui/backends/imgui_impl_opengl3.cpp",
diff --git a/skia/viewer/src/drawtext.cpp b/skia/viewer/src/drawtext.cpp
deleted file mode 100644
index 6a5af71..0000000
--- a/skia/viewer/src/drawtext.cpp
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright 2022 Rive
- */
-
-#include "skia_factory.hpp"
-#include "skia_renderer.hpp"
-#include "skia_rive_fontmgr.hpp"
-
-#include <string>
-
-static void drawrun(rive::Factory* factory, rive::Renderer* renderer,
- const rive::RenderGlyphRun& run, rive::Vec2D origin) {
- auto font = run.font.get();
- const auto scale = rive::Mat2D::fromScale(run.size, run.size);
- auto paint = factory->makeRenderPaint();
- paint->color(0xFFFFFFFF);
-
- for (size_t i = 0; i < run.glyphs.size(); ++i) {
- auto trans = rive::Mat2D::fromTranslate(origin.x + run.xpos[i], origin.y);
- auto rawpath = font->getPath(run.glyphs[i]);
- rawpath.transformInPlace(trans * scale);
- auto path = factory->makeRenderPath(rawpath.points(), rawpath.verbsU8(), rive::FillRule::nonZero);
- renderer->drawPath(path.get(), paint.get());
- }
-}
-
-struct UniString {
- std::vector<rive::Unichar> array;
-
- UniString(const char text[]) {
- while (*text) {
- array.push_back(*text++);
- }
- }
-};
-
-static std::string tagstr(uint32_t tag) {
- std::string s("abcd");
- s[0] = (tag >> 24) & 0xFF;
- s[1] = (tag >> 16) & 0xFF;
- s[2] = (tag >> 8) & 0xFF;
- s[3] = (tag >> 0) & 0xFF;
- return s;
-}
-
-void drawtext(rive::Factory* factory, rive::Renderer* renderer) {
- static std::vector<rive::RenderGlyphRun> gruns;
-
- if (gruns.size() == 0) {
- SkiaRiveFontMgr fmgr;
-
- auto font0 = fmgr.findFont("Times New Roman");
- auto font1 = fmgr.findFont("Skia");
-
- if (false) {
- auto axes = font1->getAxes();
- for (auto a : axes) {
- printf("%s %g %g %g\n", tagstr(a.tag).c_str(), a.min, a.def, a.max);
- }
- auto coords = font1->getCoords();
- for (auto c : coords) {
- printf("%s %g\n", tagstr(c.axis).c_str(), c.value);
- }
- }
-
- rive::RenderFont::Coord c0 = {'wght', 0.5f},
- c1 = {'wght', 2.0f};
-
- UniString str("Uneasy lies the head that wears a crown.");
- rive::RenderTextRun truns[] = {
- { font0, 60, 1 },
- { font0, 30, 6 },
- { font1->makeAtCoord(c0), 30, 4 },
- { font1, 30, 4 },
- { font1->makeAtCoord(c1), 30, 5 },
- { font0, 30, 20 },
- };
-
- gruns = fmgr.shapeText(rive::toSpan(str.array), rive::Span(truns, 6));
- }
-
- renderer->save();
- renderer->scale(3, 3);
- for (const auto& g : gruns) {
- drawrun(factory, renderer, g, {10, 50});
- }
- renderer->restore();
-}
-
diff --git a/skia/viewer/src/image_content.cpp b/skia/viewer/src/image_content.cpp
index 91c7c4c..276ae40 100644
--- a/skia/viewer/src/image_content.cpp
+++ b/skia/viewer/src/image_content.cpp
@@ -20,8 +20,8 @@
void handleImgui() override {}
};
-std::unique_ptr<ViewerContent> ViewerContent::Image(const char filename[],
- rive::Span<const uint8_t> bytes) {
+std::unique_ptr<ViewerContent> ViewerContent::Image(const char filename[]) {
+ auto bytes = LoadFile(filename);
auto data = SkData::MakeWithCopy(bytes.data(), bytes.size());
if (auto image = SkImage::MakeFromEncoded(data)) {
return std::make_unique<ImageContent>(std::move(image));
diff --git a/skia/viewer/src/main.cpp b/skia/viewer/src/main.cpp
index 39814bd..386a359 100644
--- a/skia/viewer/src/main.cpp
+++ b/skia/viewer/src/main.cpp
@@ -29,6 +29,30 @@
std::unique_ptr<ViewerContent> gContent;
+std::vector<uint8_t> ViewerContent::LoadFile(const char filename[]) {
+ std::vector<uint8_t> bytes;
+
+ FILE* fp = fopen(filename, "rb");
+ if (!fp) {
+ fprintf(stderr, "Can't find file: %s\n", filename);
+ return bytes;
+ }
+
+ fseek(fp, 0, SEEK_END);
+ size_t size = ftell(fp);
+ fseek(fp, 0, SEEK_SET);
+
+ bytes.resize(size);
+ size_t bytesRead = fread(bytes.data(), 1, size, fp);
+ fclose(fp);
+
+ if (bytesRead != size) {
+ fprintf(stderr, "Failed to read all of %s\n", filename);
+ bytes.resize(0);
+ }
+ return bytes;
+}
+
static void glfwCursorPosCallback(GLFWwindow* window, double x, double y) {
if (gContent) {
float xscale, yscale;
@@ -56,21 +80,7 @@
// Just get the last dropped file for now...
const char* filename = paths[count - 1];
- FILE* fp = fopen(filename, "rb");
- fseek(fp, 0, SEEK_END);
- size_t size = ftell(fp);
- fseek(fp, 0, SEEK_SET);
-
- std::vector<uint8_t> bytes(size);
- size_t bytesRead = fread(bytes.data(), 1, size, fp);
- fclose(fp);
-
- if (bytesRead != size) {
- fprintf(stderr, "failed to read all of %s\n", filename);
- return;
- }
-
- auto newContent = ViewerContent::FindHandler(filename, rive::toSpan(bytes));
+ auto newContent = ViewerContent::FindHandler(filename);
if (newContent) {
gContent = std::move(newContent);
gContent->handleResize(lastScreenWidth, lastScreenHeight);
diff --git a/skia/viewer/src/scene_content.cpp b/skia/viewer/src/scene_content.cpp
index 364f98d..bb443d2 100644
--- a/skia/viewer/src/scene_content.cpp
+++ b/skia/viewer/src/scene_content.cpp
@@ -282,9 +282,9 @@
rive::CGSkiaFactory skiaFactory;
-std::unique_ptr<ViewerContent> ViewerContent::Scene(const char filename[],
- rive::Span<const uint8_t> data) {
- if (auto file = rive::File::import(data, &skiaFactory)) {
+std::unique_ptr<ViewerContent> ViewerContent::Scene(const char filename[]) {
+ auto bytes = LoadFile(filename);
+ if (auto file = rive::File::import(rive::toSpan(bytes), &skiaFactory)) {
return std::make_unique<SceneContent>(filename, std::move(file));
}
return nullptr;
diff --git a/skia/viewer/src/skia_rive_fontmgr.hpp b/skia/viewer/src/skia_rive_fontmgr.hpp
deleted file mode 100644
index 9777b11..0000000
--- a/skia/viewer/src/skia_rive_fontmgr.hpp
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * Copyright 2022 Rive
- */
-
-#ifndef _RIVE_SKIA_FONTMGR_HPP_
-#define _RIVE_SKIA_FONTMGR_HPP_
-
-#include "rive/refcnt.hpp"
-#include "rive/span.hpp"
-#include "rive/render_text.hpp"
-
-// This is a working model for how we might extend Factory.hpp
-class SkiaRiveFontMgr {
-public:
- rive::rcp<rive::RenderFont> decodeFont(rive::Span<const uint8_t>);
- rive::rcp<rive::RenderFont> findFont(const char name[]);
- std::vector<rive::RenderGlyphRun> shapeText(rive::Span<const rive::Unichar> text,
- rive::Span<const rive::RenderTextRun> runs);
-
-};
-
-#endif
diff --git a/skia/viewer/src/text_content.cpp b/skia/viewer/src/text_content.cpp
new file mode 100644
index 0000000..8536cbb
--- /dev/null
+++ b/skia/viewer/src/text_content.cpp
@@ -0,0 +1,181 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#include "viewer_content.hpp"
+
+#include "rive/refcnt.hpp"
+#include "rive/render_text.hpp"
+
+#include "skia_factory.hpp"
+#include "skia_renderer.hpp"
+#include "line_breaker.hpp"
+
+static bool ws(rive::Unichar c) {
+ return c <= ' ';
+}
+
+std::vector<int> compute_word_breaks(rive::Span<rive::Unichar> chars) {
+ std::vector<int> breaks;
+
+ const unsigned len = chars.size();
+ for (unsigned i = 0; i < len;) {
+ // skip ws
+ while (i < len && ws(chars[i])) {
+ ++i;
+ }
+ breaks.push_back(i); // word start
+ // skip non-ws
+ while (i < len && !ws(chars[i])) {
+ ++i;
+ }
+ breaks.push_back(i); // word end
+ }
+ assert(breaks[breaks.size()-1] == len);
+ return breaks;
+}
+
+static void drawrun(rive::Factory* factory, rive::Renderer* renderer,
+ const rive::RenderGlyphRun& run, unsigned startIndex, unsigned endIndex, rive::Vec2D origin) {
+ auto font = run.font.get();
+ const auto scale = rive::Mat2D::fromScale(run.size, run.size);
+ auto paint = factory->makeRenderPaint();
+ paint->color(0xFFFFFFFF);
+
+ assert(startIndex >= 0 && endIndex <= run.glyphs.size());
+ for (size_t i = startIndex; i < endIndex; ++i) {
+ auto trans = rive::Mat2D::fromTranslate(origin.x + run.xpos[i], origin.y);
+ auto rawpath = font->getPath(run.glyphs[i]);
+ rawpath.transformInPlace(trans * scale);
+ auto path = factory->makeRenderPath(rawpath.points(), rawpath.verbsU8(), rive::FillRule::nonZero);
+ renderer->drawPath(path.get(), paint.get());
+ }
+}
+
+static void drawpara(rive::Factory* factory, rive::Renderer* renderer,
+ rive::Span<const rive::RenderGlyphLine> lines,
+ rive::Span<const rive::RenderGlyphRun> runs,
+ rive::Vec2D origin) {
+ for (const auto& line : lines) {
+ const float x0 = runs[line.startRun].xpos[line.startIndex];
+ int startGIndex = line.startIndex;
+ for (int runIndex = line.startRun; runIndex <= line.endRun; ++runIndex) {
+ const auto& run = runs[runIndex];
+ int endGIndex = runIndex == line.endRun ? line.endIndex : run.glyphs.size();
+ drawrun(factory, renderer, run, startGIndex, endGIndex,
+ {origin.x - x0, origin.y + line.baseline});
+ startGIndex = 0;
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////////
+
+typedef rive::rcp<rive::RenderFont> (*RenderFontFactory)(rive::Span<const uint8_t>);
+
+#include "renderfont_skia.hpp"
+#include "renderfont_hb.hpp"
+#include "include/core/SkData.h"
+
+static void draw_line(rive::Factory* factory, rive::Renderer* renderer, float x) {
+ auto paint = factory->makeRenderPaint();
+ paint->style(rive::RenderPaintStyle::stroke);
+ paint->thickness(1);
+ paint->color(0xFFFFFFFF);
+ auto path = factory->makeEmptyRenderPath();
+ path->move({x, 0});
+ path->line({x, 1000});
+ renderer->drawPath(path.get(), paint.get());
+}
+
+static rive::SkiaFactory skiaFactory;
+
+static rive::RenderTextRun append(std::vector<rive::Unichar>* unichars,
+ rive::rcp<rive::RenderFont> font,
+ float size, const char text[]) {
+ uint32_t n = 0;
+ while (text[n]) {
+ unichars->push_back(text[n]); // todo: utf8 -> unichar
+ n += 1;
+ }
+ return { std::move(font), size, n };
+}
+
+class TextContent : public ViewerContent {
+ std::vector<rive::Unichar> m_unichars;
+ std::vector<int> m_breaks;
+ std::vector<rive::RenderTextRun> m_truns;
+ std::vector<rive::RenderGlyphRun> m_gruns;
+
+ void make_truns() {
+ auto loader = [](const char filename[]) -> rive::rcp<rive::RenderFont> {
+ auto bytes = ViewerContent::LoadFile(filename);
+ if (bytes.size() == 0) {
+ return nullptr;
+ }
+ return HBRenderFont::Decode(rive::toSpan(bytes));
+ };
+
+ const char* fontFiles[] = {
+ "../../test/assets/RobotoFlex.ttf",
+ "../../test/assets/LibreBodoni-Italic-VariableFont_wght.ttf",
+ };
+
+ auto font0 = loader(fontFiles[0]);
+ auto font1 = loader(fontFiles[1]);
+ assert(font0);
+ assert(font1);
+
+ rive::RenderFont::Coord c1 = {'wght', 100.f},
+ c2 = {'wght', 700.f};
+
+ m_truns.push_back(append(&m_unichars, font0->makeAtCoord(c2), 60, "U"));
+ m_truns.push_back(append(&m_unichars, font0->makeAtCoord(c1), 30, "neasy"));
+ m_truns.push_back(append(&m_unichars, font1, 30, " fits the crown"));
+ m_truns.push_back(append(&m_unichars, font1->makeAtCoord(c1), 30, " that often"));
+ m_truns.push_back(append(&m_unichars, font0, 30, " lies the head."));
+
+ m_breaks = compute_word_breaks(rive::toSpan(m_unichars));
+ }
+
+ void make_gruns() {
+ m_gruns = m_truns[0].font->shapeText(rive::toSpan(m_unichars), rive::toSpan(m_truns));
+ }
+
+public:
+ TextContent() {
+ this->make_truns();
+ }
+
+ void handleDraw(SkCanvas* canvas, double) override {
+ rive::SkiaRenderer renderer(canvas);
+
+ this->make_gruns();
+
+ renderer.save();
+ renderer.translate(10, 0);
+
+ renderer.save();
+ renderer.scale(3, 3);
+
+ static float width = 300;
+ static float dw = 1;
+ width += dw; if (width > 600) { dw = -dw; } if (width < 50) { dw = -dw; }
+ auto lines = rive::RenderGlyphLine::BreakLines(rive::toSpan(m_gruns), rive::toSpan(m_breaks), width);
+
+ drawpara(&skiaFactory, &renderer, rive::toSpan(lines), rive::toSpan(m_gruns), {0, 0});
+
+ draw_line(&skiaFactory, &renderer, width);
+
+ renderer.restore();
+
+ renderer.restore();
+ }
+
+ void handleResize(int width, int height) override {}
+ void handleImgui() override {}
+};
+
+std::unique_ptr<ViewerContent> ViewerContent::Text(const char filename[]) {
+ return std::make_unique<TextContent>();
+}
diff --git a/skia/viewer/src/viewer_content.hpp b/skia/viewer/src/viewer_content.hpp
index 650b375..3fd2697 100644
--- a/skia/viewer/src/viewer_content.hpp
+++ b/skia/viewer/src/viewer_content.hpp
@@ -23,15 +23,13 @@
virtual void handlePointerDown() {}
virtual void handlePointerUp() {}
- using Factory = std::unique_ptr<ViewerContent> (*)(const char filename[],
- rive::Span<const uint8_t>);
+ using Factory = std::unique_ptr<ViewerContent> (*)(const char filename[]);
// Searches all handlers and returns a content if it is found.
- static std::unique_ptr<ViewerContent> FindHandler(const char filename[],
- rive::Span<const uint8_t> data) {
- Factory factories[] = { Scene, Image };
+ static std::unique_ptr<ViewerContent> FindHandler(const char filename[]) {
+ Factory factories[] = { Scene, Image, Text };
for (auto f : factories) {
- if (auto content = f(filename, data)) {
+ if (auto content = f(filename)) {
return content;
}
}
@@ -39,8 +37,11 @@
}
// Private factories...
- static std::unique_ptr<ViewerContent> Scene(const char[], rive::Span<const uint8_t>);
- static std::unique_ptr<ViewerContent> Image(const char[], rive::Span<const uint8_t>);
+ static std::unique_ptr<ViewerContent> Scene(const char[]);
+ static std::unique_ptr<ViewerContent> Image(const char[]);
+ static std::unique_ptr<ViewerContent> Text(const char[]);
+
+ static std::vector<uint8_t> LoadFile(const char path[]);
};
#endif
diff --git a/src/renderer.cpp b/src/renderer.cpp
index 87111ee..9780a2a 100644
--- a/src/renderer.cpp
+++ b/src/renderer.cpp
@@ -110,3 +110,26 @@
RenderPath::RenderPath() { gCounter.update(kPath, 1); }
RenderPath::~RenderPath() { gCounter.update(kPath, -1); }
+
+#include "rive/render_text.hpp"
+
+std::vector<RenderGlyphRun> RenderFont::shapeText(rive::Span<const rive::Unichar> text,
+ rive::Span<const rive::RenderTextRun> runs) const {
+#ifdef DEBUG
+ size_t count = 0;
+ for (const auto& tr : runs) {
+ assert(tr.unicharCount > 0);
+ count += tr.unicharCount;
+ }
+ assert(count <= text.size());
+#endif
+ auto gruns = this->onShapeText(text, runs);
+#ifdef DEBUG
+ for (const auto& gr : gruns) {
+ assert(gr.glyphs.size() > 0);
+ assert(gr.glyphs.size() == gr.textOffsets.size());
+ assert(gr.glyphs.size() + 1 == gr.xpos.size());
+ }
+#endif
+ return gruns;
+}
diff --git a/test/assets/LibreBodoni-Italic-VariableFont_wght.ttf b/test/assets/LibreBodoni-Italic-VariableFont_wght.ttf
new file mode 100644
index 0000000..2c0891e
--- /dev/null
+++ b/test/assets/LibreBodoni-Italic-VariableFont_wght.ttf
Binary files differ
diff --git a/test/assets/RobotoFlex.ttf b/test/assets/RobotoFlex.ttf
new file mode 100644
index 0000000..4cf1ecb
--- /dev/null
+++ b/test/assets/RobotoFlex.ttf
Binary files differ