| /* |
| * Copyright 2022 Rive |
| */ |
| |
| #include "viewer/viewer_content.hpp" |
| #include "utils/rive_utf.hpp" |
| |
| #include "rive/math/raw_path.hpp" |
| #include "rive/factory.hpp" |
| #include "rive/refcnt.hpp" |
| #include "rive/text.hpp" |
| #include "rive/text/font_hb.hpp" |
| #include <algorithm> |
| |
| using FontTextRuns = std::vector<rive::TextRun>; |
| using FontGlyphRuns = rive::SimpleArray<rive::GlyphRun>; |
| using FontFactory = rive::rcp<rive::Font> (*)(const rive::Span<const uint8_t>); |
| |
| static float drawrun(rive::Factory* factory, |
| rive::Renderer* renderer, |
| const rive::GlyphRun& run, |
| unsigned startIndex, |
| unsigned endIndex, |
| rive::Vec2D origin) |
| { |
| auto font = run.font.get(); |
| const auto scale = rive::Mat2D::fromScale(run.size, run.size); |
| auto paint = factory->makeRenderPaint(); |
| paint->color(0xFFFFFFFF); |
| |
| float x = origin.x; |
| assert(startIndex >= 0 && endIndex <= run.glyphs.size()); |
| int i, end, inc; |
| if (run.dir == rive::TextDirection::rtl) |
| { |
| i = endIndex - 1; |
| end = startIndex - 1; |
| inc = -1; |
| } |
| else |
| { |
| i = startIndex; |
| end = endIndex; |
| inc = 1; |
| } |
| while (i != end) |
| { |
| auto trans = rive::Mat2D::fromTranslate(x, origin.y); |
| x += run.advances[i]; |
| auto rawpath = font->getPath(run.glyphs[i]); |
| rawpath.transformInPlace(trans * scale); |
| auto path = factory->makeRenderPath(rawpath, rive::FillRule::nonZero); |
| renderer->drawPath(path.get(), paint.get()); |
| i += inc; |
| } |
| return x; |
| } |
| |
| static float drawpara(rive::Factory* factory, |
| rive::Renderer* renderer, |
| const rive::Paragraph& paragraph, |
| const rive::SimpleArray<rive::GlyphLine>& lines, |
| rive::Vec2D origin) |
| { |
| |
| for (const auto& line : lines) |
| { |
| |
| float x = line.startX + origin.x; |
| int runIndex, endRun, runInc; |
| if (paragraph.baseDirection == rive::TextDirection::rtl) |
| { |
| runIndex = line.endRunIndex; |
| endRun = line.startRunIndex - 1; |
| runInc = -1; |
| } |
| else |
| { |
| runIndex = line.startRunIndex; |
| endRun = line.endRunIndex + 1; |
| runInc = 1; |
| } |
| while (runIndex != endRun) |
| { |
| const auto& run = paragraph.runs[runIndex]; |
| int startGIndex = runIndex == line.startRunIndex ? line.startGlyphIndex : 0; |
| int endGIndex = runIndex == line.endRunIndex ? line.endGlyphIndex : run.glyphs.size(); |
| |
| x = drawrun(factory, |
| renderer, |
| run, |
| startGIndex, |
| endGIndex, |
| {x, origin.y + line.baseline}); |
| |
| runIndex += runInc; |
| } |
| } |
| return origin.y + lines.back().bottom; |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////// |
| std::vector<rive::rcp<rive::Font>> fallbackFonts; |
| static rive::rcp<rive::Font> pickFallbackFont(rive::Span<const rive::Unichar> missing) |
| { |
| size_t length = fallbackFonts.size(); |
| for (size_t i = 0; i < length; i++) |
| { |
| HBFont* font = static_cast<HBFont*>(fallbackFonts[i].get()); |
| if (i == length - 1 || font->hasGlyph(missing)) |
| { |
| return fallbackFonts[i]; |
| } |
| } |
| return nullptr; |
| } |
| |
| #ifdef RIVE_USING_HAFBUZZ_FONTS |
| static rive::rcp<rive::Font> load_fallback_font(rive::Span<const rive::Unichar> missing) |
| { |
| static rive::rcp<rive::Font> gFallbackFont; |
| |
| printf("missing chars:"); |
| for (auto m : missing) |
| { |
| printf(" %X", m); |
| } |
| printf("\n"); |
| |
| if (!gFallbackFont) |
| { |
| // TODO: make this more sharable for our test apps |
| FILE* fp = fopen("/Users/mike/fonts/Arial Unicode.ttf", "rb"); |
| if (!fp) |
| { |
| return nullptr; |
| } |
| |
| 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); |
| |
| assert(bytesRead == size); |
| gFallbackFont = HBFont::Decode(bytes); |
| } |
| return gFallbackFont; |
| } |
| #endif |
| |
| static std::unique_ptr<rive::RenderPath> make_line(rive::Factory* factory, |
| rive::Vec2D a, |
| rive::Vec2D b) |
| { |
| rive::RawPath rawPath; |
| rawPath.move(a); |
| rawPath.line(b); |
| return factory->makeRenderPath(rawPath, rive::FillRule::nonZero); |
| } |
| |
| 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 = make_line(factory, {x, 0}, {x, 1000}); |
| renderer->drawPath(path.get(), paint.get()); |
| } |
| |
| static rive::TextRun append(std::vector<rive::Unichar>* unichars, |
| rive::rcp<rive::Font> font, |
| float size, |
| const char text[]) |
| { |
| const uint8_t* ptr = (const uint8_t*)text; |
| uint32_t n = 0; |
| while (*ptr) |
| { |
| unichars->push_back(rive::UTF::NextUTF8(&ptr)); |
| n += 1; |
| } |
| return {std::move(font), size, n}; |
| } |
| |
| class TextContent : public ViewerContent |
| { |
| std::vector<rive::Unichar> m_unichars; |
| |
| rive::SimpleArray<rive::Paragraph> m_paragraphs; |
| rive::Mat2D m_xform; |
| float m_width = 300; |
| bool m_autoWidth = false; |
| int m_align = 0; |
| |
| FontTextRuns make_truns(FontFactory fact) |
| { |
| auto loader = [fact](const char filename[]) -> rive::rcp<rive::Font> { |
| auto bytes = ViewerContent::LoadFile(filename); |
| if (bytes.size() == 0) |
| { |
| assert(false); |
| return nullptr; |
| } |
| return fact(bytes); |
| }; |
| |
| const char* fontFiles[] = { |
| "../../../test/assets/RobotoFlex.ttf", |
| "../../../test/assets/Montserrat.ttf", |
| "../../../test/assets/IBMPlexSansArabic-Regular.ttf", |
| }; |
| |
| auto font0 = loader(fontFiles[0]); |
| auto font1 = loader(fontFiles[1]); |
| auto font2 = loader(fontFiles[2]); |
| assert(font0); |
| assert(font1); |
| assert(font2); |
| |
| fallbackFonts.push_back(font2); |
| |
| rive::Font::Coord c1 = {'wght', 100.f}, c2 = {'wght', 800.f}; |
| |
| FontTextRuns truns; |
| |
| // truns.push_back( |
| // append(&m_unichars, font0->makeAtCoord(c2), 32, "No one ever left alive in ")); |
| // truns.push_back(append(&m_unichars, font0->makeAtCoord(c2), 54, "nineteen hundred")); |
| // truns.push_back(append(&m_unichars, font0->makeAtCoord(c1), 30, "ne漢字asy")); |
| |
| // truns.push_back(append(&m_unichars, font2, 30, " its the
")); |
| // truns.push_back(append(&m_unichars, font2, 40, "cRown")); |
| // truns.push_back(append(&m_unichars, font2, 30, "a b c d")); |
| |
| // truns.push_back(append(&m_unichars, font1->makeAtCoord(c1), 30, " that often")); |
| // truns.push_back(append(&m_unichars, font0, 30, " lies the head.")); |
| // truns.push_back(append(&m_unichars, font0->makeAtCoord(c2), 60, "hi one two")); |
| |
| // truns.push_back(append(&m_unichars, font2, 32.0f, "في 10-12 آذار 1997 بمدينة")); |
| // truns.push_back(append(&m_unichars, font2, 32.0f, "في 10-12 آذار 1997 بمدينة")); |
| truns.push_back(append(&m_unichars, |
| font1, |
| 32.0f, |
| // clang-format off |
| "لمفاتيح ABC")); |
| |
| // truns.push_back(append(&m_unichars, |
| // font1, |
| // 28.0f, |
| // // clang-format off |
| // " DEF\n")); |
| |
| // truns.push_back(append(&m_unichars, |
| // font0, |
| // 32.0f, |
| // // clang-format off |
| // " one ever ff ffi left alive in ")); |
| |
| // truns.push_back(append(&m_unichars, |
| // font1, |
| // 54.0f, |
| // // clang-format off |
| // "nineteen hundred")); |
| |
| // truns.push_back(append(&m_unichars, |
| // font0, |
| // 32.0f, |
| // // clang-format off |
| // "and eighty five")); |
| // TODO: test case from flutter causing assertion break |
| |
| // "لمفاتيح ABC DEF
في 10-12 آذار 1997 بمدينة\nabc def ghi jkl mnop\nلكن لا بد أن أوضح لك |
| // أن كل")); |
| // "hello look\u2028here\nsecond paragraph")); |
| |
| // clang-format on |
| |
| // truns.push_back(append(&m_unichars, font2, 32.0f, "abc
def\nghijkl")); |
| |
| // truns.push_back(append(&m_unichars, font2, 32.0f, "DEF
دنة")); |
| |
| // truns.push_back(append(&m_unichars, font2, 32.0f, "AفيB")); |
| |
| // truns.push_back(append(&m_unichars, font0, 42.0f, "OT\nHER\n")); |
| // truns.push_back(append(&m_unichars, font1, 62.0f, "VERY LARGE FONT HERE")); |
| // truns.push_back( |
| // append(&m_unichars, font0, 52.0f, "one two three\n\n\nfour five six seven")); |
| |
| // truns.push_back(append(&m_unichars, font0, 32.0f, "ab")); |
| // truns.push_back(append(&m_unichars, font0, 60.0f, "ee\n four")); |
| |
| return truns; |
| } |
| |
| public: |
| TextContent() |
| { |
| fallbackFonts.clear(); |
| HBFont::gFallbackProc = pickFallbackFont; |
| auto truns = this->make_truns(ViewerContent::DecodeFont); |
| m_paragraphs = truns[0].font->shapeText(m_unichars, truns); |
| |
| m_xform = rive::Mat2D::fromTranslate(10, 0) * rive::Mat2D::fromScale(3, 3); |
| } |
| |
| void draw(rive::Renderer* renderer, |
| float width, |
| const rive::SimpleArray<rive::Paragraph>& paragraphs) |
| { |
| |
| renderer->save(); |
| renderer->transform(m_xform); |
| float y = 0.0f; |
| float paragraphWidth = m_autoWidth ? -1.0f : width; |
| |
| rive::SimpleArray<rive::SimpleArray<rive::GlyphLine>>* lines = |
| new rive::SimpleArray<rive::SimpleArray<rive::GlyphLine>>(paragraphs.size()); |
| rive::SimpleArray<rive::SimpleArray<rive::GlyphLine>>& linesRef = *lines; |
| size_t paragraphIndex = 0; |
| |
| for (auto& para : paragraphs) |
| { |
| linesRef[paragraphIndex] = |
| rive::GlyphLine::BreakLines(para.runs, m_autoWidth ? -1.0f : width); |
| |
| if (m_autoWidth) |
| { |
| paragraphWidth = |
| std::max(paragraphWidth, |
| rive::GlyphLine::ComputeMaxWidth(linesRef[paragraphIndex], para.runs)); |
| } |
| paragraphIndex++; |
| } |
| paragraphIndex = 0; |
| for (auto& para : paragraphs) |
| { |
| rive::SimpleArray<rive::GlyphLine>& lines = linesRef[paragraphIndex++]; |
| rive::GlyphLine::ComputeLineSpacing(lines, |
| para.runs, |
| paragraphWidth, |
| (rive::TextAlign)m_align); |
| y = drawpara(RiveFactory(), renderer, para, lines, {0, y}) + 20.0f; |
| } |
| if (!m_autoWidth) |
| { |
| draw_line(RiveFactory(), renderer, width); |
| } |
| |
| renderer->restore(); |
| } |
| |
| void handleDraw(rive::Renderer* renderer, double) override |
| { |
| this->draw(renderer, m_width, m_paragraphs); |
| } |
| |
| 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, 1, 400); |
| ImGui::Checkbox("Autowidth", &m_autoWidth); |
| ImGui::Combo("combo", &m_align, alignOptions, IM_ARRAYSIZE(alignOptions)); |
| ImGui::End(); |
| } |
| }; |
| |
| static bool ends_width(const char str[], const char suffix[]) |
| { |
| size_t ln = strlen(str); |
| size_t lx = strlen(suffix); |
| if (lx > ln) |
| { |
| return false; |
| } |
| for (size_t i = 0; i < lx; ++i) |
| { |
| if (str[ln - lx + i] != suffix[i]) |
| { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| std::unique_ptr<ViewerContent> ViewerContent::Text(const char filename[]) |
| { |
| if (ends_width(filename, ".svg")) |
| { |
| return std::make_unique<TextContent>(); |
| } |
| return nullptr; |
| } |