diff --git a/examples/text_shape.cpp b/examples/text_shape.cpp index 9d585add8..01381071b 100644 --- a/examples/text_shape.cpp +++ b/examples/text_shape.cpp @@ -45,14 +45,11 @@ struct TextEdit { // TextBlob::RunHandler impl void commitRunBuffer(TextBlob::RunInfo& info) override { - if (info.clusters && - info.glyphCount > 0) { - Box box; - for (int i=0; i 0) { - os::Paint paint; - paint.style(os::Paint::Fill); - - for (int i=0; i 0) { - // On macOS and Linux wchar_t has 32-bits - if constexpr (sizeof(wchar_t) >= 4) { - codepoint = widetext[0]; - } - // On Windows wchar_t has 16-bits (wide strings are UTF-16 strings) - else if constexpr (sizeof(wchar_t) == 2) { - codepoint = base::utf16_to_codepoint( - widetext.size() > 1 ? widetext[1]: widetext[0], - widetext.size() > 1 ? widetext[0]: 0); - } - else { - codepoint = 0; - } - } - - m_delegate->preProcessChar(utf8Begin, codepoint, - m_fg, m_bg, bounds); - } - - if (m_delegate) - m_delegate->preDrawChar(bounds); - - if (m_bg != gfx::ColorNone) { - paint.color(m_bg); - m_surface->drawRect(bounds, paint); - } - -#if LAF_SKIA - if (m_surface) { - SkGlyphID glyphs = info.glyphs[i]; - SkPoint positions = SkPoint::Make(info.positions[i].x, - info.positions[i].y); - uint32_t clusters = info.clusters[i]; - paint.color(m_fg); - static_cast(m_surface) - ->canvas().drawGlyphs( - 1, &glyphs, &positions, &clusters, - utf8text.size(), - utf8text.data(), - SkPoint::Make(m_origin.x, m_origin.y), - static_cast(info.font.get())->skFont(), - paint.skPaint()); - } -#endif - - if (m_delegate) - m_delegate->postDrawChar(bounds); - } - } +void draw_text( + os::Surface* surface, + const FontRef& font, + const std::string& text, + gfx::PointF pos, + const os::Paint* paint, + const TextAlign textAlign) +{ + ASSERT(surface); + if (!surface) + return; + + const TextBlobRef blob = TextBlob::Make(font, text); + if (!blob) + return; + + switch (textAlign) { + case TextAlign::Left: break; + case TextAlign::Center: pos.x -= blob->bounds().w / 2.0f; break; + case TextAlign::Right: pos.x -= blob->bounds().w; break; } -private: - os::Surface* m_surface; - const std::string& m_text; - gfx::Color m_fg; - gfx::Color m_bg; - gfx::PointF m_origin; - DrawTextDelegate* m_delegate; -}; - + draw_text(surface, blob, pos, paint); } -gfx::Rect draw_text(os::Surface* surface, - const FontMgrRef& fontMgr, - const FontRef& fontRef, - const std::string& text, - gfx::Color fg, gfx::Color bg, - int x, int y, - DrawTextDelegate* delegate) +void draw_text( + os::Surface* surface, + const TextBlobRef& blob, + const gfx::PointF& pos, + const os::Paint* paint) { - Font* font = fontRef.get(); - - base::utf8_decode decode(text); - gfx::Rect textBounds; - -retry:; - // Check if this font is enough to draw the given string or we will - // need the fallback for some special Unicode chars - if (font->fallback()) { - // TODO compose unicode characters and check those codepoints, the - // same in the drawing code of sprite sheet font - while (const codepoint_t code = decode.next()) { - if (code && !font->hasCodePoint(code)) { - Font* newFont = font->fallback(); - - // Search a valid fallback - while (newFont && !newFont->hasCodePoint(code)) - newFont = newFont->fallback(); - if (!newFont) - break; - - y += font->height()/2 - newFont->height()/2; + ASSERT(surface); + ASSERT(blob); + if (!surface || !blob) + return; - font = newFont; - goto retry; - } - } +#if LAF_SKIA + if (const auto* skiaBlob = dynamic_cast(blob.get())) { + static_cast(surface) + ->canvas().drawTextBlob(skiaBlob->skTextBlob(), + pos.x, pos.y, + (paint ? paint->skPaint(): SkPaint())); } +#endif - // Use the same antialias setting as the original font. - if (font != fontRef) - font->setAntialias(fontRef->antialias()); - - switch (font->type()) { - - case FontType::Unknown: - // Do nothing - break; - - case FontType::SpriteSheet: { - SpriteSheetFont* ssFont = static_cast(font); - os::Surface* sheet = ssFont->sheetSurface(); - - if (surface) { - sheet->lock(); - surface->lock(); - } - - decode = base::utf8_decode(text); - while (true) { - const int i = decode.pos() - text.begin(); - const codepoint_t chr = decode.next(); - if (!chr) - break; - - const gfx::Rect charBounds = ssFont->getCharBounds(chr); - const gfx::Rect outCharBounds(x, y, charBounds.w, charBounds.h); - - if (delegate) - delegate->preProcessChar(i, chr, fg, bg, outCharBounds); - - if (delegate && !delegate->preDrawChar(outCharBounds)) - break; - - if (!charBounds.isEmpty()) { - if (surface) - surface->drawColoredRgbaSurface(sheet, fg, bg, gfx::Clip(x, y, charBounds)); - } - - textBounds |= outCharBounds; - if (delegate) - delegate->postDrawChar(outCharBounds); - - x += charBounds.w; - } - - if (surface) { - surface->unlock(); - sheet->unlock(); - } - break; - } - - case FontType::FreeType: { -#if LAF_FREETYPE - FreeTypeFont* ttFont = static_cast(font); - const int fg_alpha = gfx::geta(fg); - - gfx::Rect clipBounds; - os::SurfaceFormatData fd; - if (surface) { - clipBounds = surface->getClipBounds(); - surface->getFormat(&fd); - surface->lock(); - } - - ft::ForEachGlyph feg(ttFont->face(), text); - while (feg.next()) { - gfx::Rect origDstBounds; - const auto* glyph = feg.glyph(); - if (glyph) - origDstBounds = gfx::Rect( - x + int(glyph->startX), - y + int(glyph->y), - int(glyph->endX) - int(glyph->startX), - int(glyph->bitmap->rows) ? int(glyph->bitmap->rows): 1); - - if (delegate) { - delegate->preProcessChar( - feg.charIndex(), - feg.unicodeChar(), - fg, bg, origDstBounds); - } - - if (!glyph) - continue; - - if (delegate && !delegate->preDrawChar(origDstBounds)) - break; - - origDstBounds.x = x + int(glyph->x); - origDstBounds.w = int(glyph->bitmap->width); - origDstBounds.h = int(glyph->bitmap->rows); - - gfx::Rect dstBounds = origDstBounds; - if (surface) - dstBounds &= clipBounds; - - if (surface && !dstBounds.isEmpty()) { - const int clippedRows = dstBounds.y - origDstBounds.y; - int dst_y = dstBounds.y; - int t; - for (int v=0; vbitmap->buffer - + (v+clippedRows)*glyph->bitmap->pitch; - int dst_x = dstBounds.x; - uint32_t* dst_address = - (uint32_t*)surface->getData(dst_x, dst_y); - - // TODO maybe if we are trying to draw in a SkiaSurface with a nullptr m_bitmap - // (when GPU-acceleration is enabled) - if (!dst_address) - break; - - // Skip first clipped pixels - for (int u=0; ubitmap->pixel_mode == FT_PIXEL_MODE_GRAY) { - ++p; - } - else if (glyph->bitmap->pixel_mode == FT_PIXEL_MODE_MONO) { - if (++bit == 8) { - bit = 0; - ++p; - } - } - } - - for (int u=0; ubitmap->pixel_mode == FT_PIXEL_MODE_GRAY) { - alpha = *(p++); - } - else if (glyph->bitmap->pixel_mode == FT_PIXEL_MODE_MONO) { - alpha = ((*p) & (1 << (7 - (bit++))) ? 255: 0); - if (bit == 8) { - bit = 0; - ++p; - } - } - else - alpha = 0; - - const uint32_t backdrop = *dst_address; - const gfx::Color backdropColor = - gfx::rgba( - ((backdrop & fd.redMask) >> fd.redShift), - ((backdrop & fd.greenMask) >> fd.greenShift), - ((backdrop & fd.blueMask) >> fd.blueShift), - ((backdrop & fd.alphaMask) >> fd.alphaShift)); - - gfx::Color output = gfx::rgba(gfx::getr(fg), - gfx::getg(fg), - gfx::getb(fg), - MUL_UN8(fg_alpha, alpha, t)); - if (gfx::geta(bg) > 0) - output = os::blend(os::blend(backdropColor, bg), output); - else - output = os::blend(backdropColor, output); - - *dst_address = - ((gfx::getr(output) << fd.redShift ) & fd.redMask ) | - ((gfx::getg(output) << fd.greenShift) & fd.greenMask) | - ((gfx::getb(output) << fd.blueShift ) & fd.blueMask ) | - ((gfx::geta(output) << fd.alphaShift) & fd.alphaMask); - - ++dst_address; - } - } - } - - if (!origDstBounds.w) origDstBounds.w = 1; - if (!origDstBounds.h) origDstBounds.h = 1; - textBounds |= origDstBounds; - if (delegate) - delegate->postDrawChar(origDstBounds); + if (const auto* spriteBlob = dynamic_cast(blob.get())) { + const auto* spriteFont = static_cast(spriteBlob->font().get()); + const os::Surface* sheet = spriteFont->sheetSurface(); + + for (const auto& run : spriteBlob->runs()) { + if (run.subBlob) { + gfx::PointF subPos = pos; + if (!run.positions.empty()) + subPos += run.positions[0]; + draw_text(surface, run.subBlob, subPos, paint); + continue; } - if (surface) - surface->unlock(); -#endif // LAF_FREETYPE - break; - } - - case FontType::Native: { -#if LAF_SKIA - TextBlobRef blob; - if (delegate) { - AdapterBuilder handler(surface, text, fg, bg, - gfx::PointF(x, y), delegate); - // We use AddRef(font) and not fontRef because font might have - // changed now that we searched for a fallback font. - blob = TextBlob::MakeWithShaper(fontMgr, AddRef(font), text, &handler); + const size_t n = run.glyphs.size(); + for (int i=0; igetGlyphBoundsOnSheet(run.glyphs[i]); + + surface->drawColoredRgbaSurface( + sheet, + (paint ? paint->color(): gfx::ColorNone), + gfx::ColorNone, + gfx::Clip(gfx::Point(run.positions[i]+pos), + glyphBounds)); } - else { - os::Paint paint; - paint.color(fg); - // TODO Draw background with bg - blob = TextBlob::MakeWithShaper(fontMgr, AddRef(font), text); - draw_text(surface, blob, gfx::PointF(x, y), &paint); - } -#endif // LAF_SKIA - break; } - } - return textBounds; + // TODO impl } } // namespace text diff --git a/text/draw_text_shaper.cpp b/text/draw_text_shaper.cpp new file mode 100644 index 000000000..347e03248 --- /dev/null +++ b/text/draw_text_shaper.cpp @@ -0,0 +1,217 @@ +// LAF Text Library +// Copyright (c) 2024 Igara Studio S.A. +// +// This file is released under the terms of the MIT license. +// Read LICENSE.txt for more information. + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "text/draw_text.h" + +#include "os/paint.h" +#include "os/surface.h" +#include "text/sprite_sheet_font.h" +#include "text/text_blob.h" + +#if LAF_SKIA + #include "os/skia/skia_helpers.h" + #include "os/skia/skia_surface.h" + #include "text/skia_font.h" + + #include "include/core/SkCanvas.h" +#endif + +namespace text { + +namespace { + +// Adapts the old DrawTextDelegate with new TextBlob run handlers. +class AdapterBuilder : public TextBlob::RunHandler { +public: + AdapterBuilder(os::Surface* surface, + const std::string& text, + gfx::Color fg, + gfx::Color bg, + const gfx::PointF& origin, + DrawTextDelegate* delegate) + : m_surface(surface) + , m_text(text) + , m_fg(fg), m_bg(bg) + , m_origin(origin) + , m_delegate(delegate) { } + + // TextBlob::RunHandler impl + void commitRunBuffer(TextBlob::RunInfo& info) override { + if (info.clusters && + info.glyphCount > 0) { + float advanceX = 0.0f; + + os::Paint paint; + paint.style(os::Paint::Fill); + + for (int i=0; i= 4) { + codepoint = widetext[0]; + } + // On Windows wchar_t has 16-bits (wide strings are UTF-16 strings) + else if constexpr (sizeof(wchar_t) == 2) { + codepoint = base::utf16_to_codepoint( + widetext.size() > 1 ? widetext[1]: widetext[0], + widetext.size() > 1 ? widetext[0]: 0); + } + else { + codepoint = 0; + } + } + + m_delegate->preProcessChar(utf8Begin, codepoint, + m_fg, m_bg, bounds); + } + + if (m_delegate) + m_delegate->preDrawChar(bounds); + + if (m_bg != gfx::ColorNone) { + paint.color(m_bg); + m_surface->drawRect(bounds, paint); + } + + if (m_surface && info.font) { + if (info.font->type() == FontType::SpriteSheet) { + const auto* spriteFont = static_cast(info.font.get()); + const os::Surface* sheet = spriteFont->sheetSurface(); + const gfx::Rect sourceBounds = + spriteFont->getGlyphBoundsOnSheet(info.glyphs[i]); + + m_surface->drawColoredRgbaSurface( + sheet, + m_fg, + gfx::ColorNone, + gfx::Clip(gfx::Point(info.positions[i]+m_origin+info.point), + sourceBounds)); + } +#if LAF_SKIA + else if (info.font->type() == FontType::Native) { + SkGlyphID glyphs = info.glyphs[i]; + SkPoint positions = os::to_skia(info.positions[i]); // + uint32_t clusters = info.clusters[i]; + paint.color(m_fg); + static_cast(m_surface) + ->canvas().drawGlyphs( + 1, &glyphs, &positions, &clusters, + utf8text.size(), + utf8text.data(), + os::to_skia(m_origin+info.point), + static_cast(info.font.get())->skFont(), + paint.skPaint()); + } +#endif + } + + if (m_delegate) + m_delegate->postDrawChar(bounds); + } + } + } + +private: + os::Surface* m_surface; + const std::string& m_text; + gfx::Color m_fg; + gfx::Color m_bg; + gfx::PointF m_origin; + DrawTextDelegate* m_delegate; +}; + +} // anonymous namespace + +gfx::Rect draw_text(os::Surface* surface, + const FontMgrRef& fontMgr, + const FontRef& font, + const std::string& text, + gfx::Color fg, gfx::Color bg, + int x, int y, + DrawTextDelegate* delegate) +{ + TextBlobRef blob; + if (delegate) { + AdapterBuilder handler(surface, text, fg, bg, + gfx::PointF(x, y), delegate); + blob = TextBlob::MakeWithShaper(fontMgr, font, text, &handler); + } + else { + blob = TextBlob::MakeWithShaper(fontMgr, font, text); + if (surface) { + os::Paint paint; + paint.color(fg); + draw_text( + surface, + blob, + gfx::PointF(x, y), + &paint); + } + } + + if (blob) + return blob->bounds(); + return gfx::Rect(); +} + +void draw_text_with_shaper( + os::Surface* surface, + const FontMgrRef& fontMgr, + const FontRef& font, + const std::string& text, + gfx::PointF pos, + const os::Paint* paint, + const TextAlign textAlign) +{ + if (!fontMgr || !font || font->type() != FontType::Native) + return; + + const TextBlobRef blob = TextBlob::MakeWithShaper( + fontMgr, font, text, nullptr); + if (!blob) + return; + + switch (textAlign) { + case TextAlign::Left: break; + case TextAlign::Center: pos.x -= blob->bounds().w / 2.0f; break; + case TextAlign::Right: pos.x -= blob->bounds().w; break; + } + + draw_text(surface, blob, pos, paint); +} + +} // namespace text diff --git a/text/font_mgr.h b/text/font_mgr.h index 064a348b0..10ec13269 100644 --- a/text/font_mgr.h +++ b/text/font_mgr.h @@ -20,6 +20,7 @@ namespace text { class FontMgr : public base::RefCount { public: + [[nodiscard]] static FontMgrRef Make(); FontRef loadSpriteSheetFont(const char* filename, int scale); diff --git a/text/skia_draw_text.cpp b/text/skia_draw_text.cpp deleted file mode 100644 index eaa3595fb..000000000 --- a/text/skia_draw_text.cpp +++ /dev/null @@ -1,67 +0,0 @@ -// LAF Text Library -// Copyright (C) 2019-2024 Igara Studio S.A. -// -// This file is released under the terms of the MIT license. -// Read LICENSE.txt for more information. - -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include "text/draw_text.h" - -#include "os/paint.h" -#include "os/skia/skia_helpers.h" -#include "os/skia/skia_surface.h" -#include "os/system.h" -#include "text/font_mgr.h" -#include "text/skia_font.h" -#include "text/skia_text_blob.h" - -#include "include/core/SkCanvas.h" - -namespace text { - -void draw_text( - os::Surface* surface, - const FontRef& font, - const std::string& text, - gfx::PointF pos, - const os::Paint* paint, - const TextAlign textAlign) -{ - ASSERT(surface); - if (!surface) - return; - - TextBlobRef blob = TextBlob::Make(font, text); - if (!blob) - return; - - switch (textAlign) { - case TextAlign::Left: break; - case TextAlign::Center: pos.x -= blob->bounds().w / 2.0f; break; - case TextAlign::Right: pos.x -= blob->bounds().w; break; - } - - draw_text(surface, blob, pos, paint); -} - -void draw_text( - os::Surface* surface, - const TextBlobRef& blob, - const gfx::PointF& pos, - const os::Paint* paint) -{ - ASSERT(surface); - if (!surface) - return; - - static_cast(surface) - ->canvas().drawTextBlob( - static_cast(blob.get())->skTextBlob(), - pos.x, pos.y, - (paint ? paint->skPaint(): SkPaint())); -} - -} // namespace text diff --git a/text/skia_text_blob.cpp b/text/skia_text_blob.cpp index b65822624..c1bd5af46 100644 --- a/text/skia_text_blob.cpp +++ b/text/skia_text_blob.cpp @@ -11,7 +11,6 @@ #include "text/skia_font_mgr.h" #include "include/core/SkTextBlob.h" -#include "modules/skshaper/include/SkShaper.h" #include @@ -22,6 +21,7 @@ SkiaTextBlob::SkiaTextBlob(const sk_sp& skTextBlob, : TextBlob(bounds) , m_skTextBlob(skTextBlob) { + ASSERT(skTextBlob); } void SkiaTextBlob::visitRuns(const RunVisitor& visitor) @@ -48,15 +48,22 @@ void SkiaTextBlob::visitRuns(const RunVisitor& visitor) } } -TextBlobRef TextBlob::Make( +TextBlobRef SkiaTextBlob::Make( const FontRef& font, const std::string& text) { + ASSERT(font); + ASSERT(font->type() == FontType::Native); + ASSERT(dynamic_cast(font.get())); + SkFont skFont = static_cast(font.get())->skFont(); sk_sp textBlob; textBlob = SkTextBlob::MakeFromText(text.c_str(), text.size(), skFont, SkTextEncoding::kUTF8); - return base::make_ref(textBlob); + if (textBlob) + return base::make_ref(textBlob); + + return nullptr; } } // namespace text diff --git a/text/skia_text_blob.h b/text/skia_text_blob.h index 9f26efcea..a53a7abd7 100644 --- a/text/skia_text_blob.h +++ b/text/skia_text_blob.h @@ -16,6 +16,7 @@ namespace text { class SkiaTextBlob : public TextBlob { public: + SkiaTextBlob() = delete; SkiaTextBlob(const sk_sp& skTextBlob, const gfx::RectF& bounds = gfx::RectF()); @@ -23,6 +24,16 @@ class SkiaTextBlob : public TextBlob { void visitRuns(const RunVisitor& visitor) override; + static TextBlobRef Make( + const FontRef& font, + const std::string& text); + + static TextBlobRef MakeWithShaper( + const FontMgrRef& fontMgr, + const FontRef& font, + const std::string& text, + TextBlob::RunHandler* handler); + private: sk_sp m_skTextBlob; }; diff --git a/text/skia_with_shaper.cpp b/text/skia_text_blob_shaper.cpp similarity index 77% rename from text/skia_with_shaper.cpp rename to text/skia_text_blob_shaper.cpp index 3c7145037..d2b3ff5c2 100644 --- a/text/skia_with_shaper.cpp +++ b/text/skia_text_blob_shaper.cpp @@ -30,9 +30,10 @@ namespace { class ShaperRunHandler final : public SkShaper::RunHandler { public: - ShaperRunHandler(const char* utf8Text, SkPoint offset, + ShaperRunHandler(const char* utf8Text, + const gfx::PointF& offset, TextBlob::RunHandler* subHandler) - : m_builder(utf8Text, offset) + : m_builder(utf8Text, os::to_skia(offset)) , m_subHandler(subHandler) { } sk_sp makeBlob() { @@ -61,17 +62,12 @@ class ShaperRunHandler final : public SkShaper::RunHandler { } void commitRunBuffer(const RunInfo& info) override { - SkString family; - info.fFont.getTypeface() - ->getFamilyName(&family); - m_builder.commitRunBuffer(info); // Now the m_buffer field is valid and can be used size_t n = info.glyphCount; TextBlob::RunInfo subInfo; - FontRef font = base::make_ref(info.fFont); - subInfo.font = font; + subInfo.font = base::make_ref(info.fFont); subInfo.glyphCount = n; subInfo.rtl = (info.fBidiLevel & 1); subInfo.utf8Range.begin = info.utf8Range.begin(); @@ -97,14 +93,16 @@ class ShaperRunHandler final : public SkShaper::RunHandler { } subInfo.clusters = m_buffer.clusters; - subInfo.point = gfx::PointF(m_buffer.point.x(), - m_buffer.point.y()); - for (int i=0; icommitRunBuffer(subInfo); + + for (int i=0; itype() == FontType::Native); ASSERT(dynamic_cast(font.get())); SkFont skFont = static_cast(font.get())->skFont(); @@ -150,33 +150,10 @@ TextBlobRef TextBlob::MakeWithShaper( skFont, SkTextEncoding::kUTF8); } - return base::make_ref(textBlob, bounds); -} - -void draw_text_with_shaper( - os::Surface* surface, - const FontMgrRef& fontMgr, - const FontRef& font, - const std::string& text, - gfx::PointF pos, - const os::Paint* paint, - const TextAlign textAlign) -{ - if (!fontMgr || !font || font->type() != FontType::Native) - return; - - TextBlobRef blob = TextBlob::MakeWithShaper( - fontMgr, font, text, nullptr); - if (!blob) - return; - - switch (textAlign) { - case TextAlign::Left: break; - case TextAlign::Center: pos.x -= blob->bounds().w / 2.0f; break; - case TextAlign::Right: pos.x -= blob->bounds().w; break; - } - - draw_text(surface, blob, pos, paint); + if (textBlob) + return base::make_ref(textBlob, bounds); + else + return nullptr; } } // namespace text diff --git a/text/sprite_sheet_font.h b/text/sprite_sheet_font.h index 066754827..749126c8a 100644 --- a/text/sprite_sheet_font.h +++ b/text/sprite_sheet_font.h @@ -99,6 +99,13 @@ class SpriteSheetFont : public Font { } gfx::RectF getGlyphBounds(glyph_t glyph) const override { + if (glyph >= 0 && glyph < (int)m_glyphs.size()) + return gfx::RectF(0, 0, m_glyphs[glyph].w, m_glyphs[glyph].h); + + return getCharBounds(128); + } + + gfx::RectF getGlyphBoundsOnSheet(glyph_t glyph) const { if (glyph >= 0 && glyph < (int)m_glyphs.size()) return m_glyphs[glyph]; diff --git a/text/sprite_text_blob.cpp b/text/sprite_text_blob.cpp new file mode 100644 index 000000000..7fc24326a --- /dev/null +++ b/text/sprite_text_blob.cpp @@ -0,0 +1,143 @@ +// LAF Text Library +// Copyright (c) 2024 Igara Studio S.A. +// +// This file is released under the terms of the MIT license. +// Read LICENSE.txt for more information. + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "text/sprite_text_blob.h" + +#include "base/ref.h" +#include "text/font.h" +#include "text/font_mgr.h" +#include "text/sprite_sheet_font.h" + +namespace text { + +namespace { + +// Used for sub-TextBlobs created from SpriteTextBlob to handle runs +// of text with callback fonts. +// +// As SpriteTextBlob can create other TextBlobs with sub-strings +// (using other kind of fonts like SkiaFont), those TextBlob +// (SkiaTextBlob) only see the sub-string part for the run (they don't +// know the whole string). With this OffsetHandler we adapt that +// sub-string information to the global string, adjusting the UTF-8 +// range and the output position. +class OffsetHandler : public TextBlob::RunHandler { +public: + OffsetHandler(RunHandler* original, + const int offsetUtf8, + const gfx::PointF& offsetOrigin) + : m_original(original) + , m_offsetUtf8(offsetUtf8) + , m_offsetOrigin(offsetOrigin) + { } + + // TextBlob::RunHandler impl + void commitRunBuffer(TextBlob::RunInfo& info) override { + // Adjust UTF8 range and position. + info.utf8Range.begin += m_offsetUtf8; + info.utf8Range.end += m_offsetUtf8; + info.point = m_offsetOrigin; + + // Call the original RunHandler with the global info. + if (m_original) + m_original->commitRunBuffer(info); + } + +private: + RunHandler* m_original; + int m_offsetUtf8; + gfx::PointF m_offsetOrigin; +}; + +} // anonymous namespace + +void SpriteTextBlob::Run::add(const glyph_t glyph, + const gfx::PointF& pos, + const uint32_t cluster) +{ + glyphs.push_back(glyph); + positions.push_back(pos); + clusters.push_back(cluster); +} + +void SpriteTextBlob::Run::clear() +{ + subBlob.reset(); + utf8Range.begin = utf8Range.end; + glyphs.clear(); + positions.clear(); + clusters.clear(); +} + +void SpriteTextBlob::visitRuns(const RunVisitor& visitor) +{ + RunInfo info; + info.font = m_font; + for (Run& run : m_runs) { + if (run.subBlob) { + run.subBlob->visitRuns(visitor); + continue; + } + + info.utf8Range = run.utf8Range; + info.glyphCount = run.glyphs.size(); + info.glyphs = run.glyphs.data(); + info.positions = run.positions.data(); + info.clusters = run.clusters.data(); + visitor(info); + } +} + +TextBlobRef SpriteTextBlob::Make( + const FontRef& font, + const std::string& text) +{ + ASSERT(font); + ASSERT(font->type() == FontType::SpriteSheet); + ASSERT(dynamic_cast(font.get())); + + const auto* spriteFont = static_cast(font.get()); + + Runs runs; + Run run; + + gfx::Rect textBounds; + gfx::PointF pos(0.0f, 0.0f); + base::utf8_decode decode(text); + while (true) { + const int i = decode.pos() - text.begin(); + const codepoint_t chr = decode.next(); + run.utf8Range.end = i; + if (chr == 0) + break; + + // Ignore code point that are not present in the font. + const glyph_t glyph = spriteFont->codePointToGlyph(chr); + if (glyph == 0) + continue; + + gfx::Rect glyphBounds = spriteFont->getGlyphBounds(glyph); + if (glyphBounds.isEmpty()) + continue; + + run.add(glyph, pos, i - run.utf8Range.begin); + + glyphBounds.offset(pos); + textBounds |= glyphBounds; + pos.x += glyphBounds.w; + } + + if (!run.empty()) + runs.push_back(run); + + return base::make_ref(textBounds, font, std::move(runs)); +} + +} // namespace text diff --git a/text/sprite_text_blob.h b/text/sprite_text_blob.h new file mode 100644 index 000000000..665c25271 --- /dev/null +++ b/text/sprite_text_blob.h @@ -0,0 +1,72 @@ +// LAF Text Library +// Copyright (c) 2024 Igara Studio S.A. +// +// This file is released under the terms of the MIT license. +// Read LICENSE.txt for more information. + +#ifndef LAF_SPRITE_TEXT_BLOB_H_INCLUDED +#define LAF_SPRITE_TEXT_BLOB_H_INCLUDED +#pragma once + +#include "gfx/point.h" +#include "text/text_blob.h" + +#include + +namespace text { + +// A TextBlob created from a SpriteSheetFont. It can include sub-runs +// created using fallback fonts TextBlobs (i.e. SkiaTextBlob) when a +// sequence of non-existent codepoints in the SpriteSheetFont is +// present. +class SpriteTextBlob : public TextBlob { +public: + struct Run { + TextBlobRef subBlob; + + Utf8Range utf8Range; + std::vector glyphs; + std::vector positions; + std::vector clusters; + + size_t size() const { return glyphs.size(); } + bool empty() const { return glyphs.empty(); } + + void add(glyph_t glyph, + const gfx::PointF& pos, + uint32_t cluster); + void clear(); + }; + using Runs = std::vector; + + SpriteTextBlob(const gfx::RectF& bounds, + const FontRef& font, + Runs&& runs) + : TextBlob(bounds) + , m_font(font) + , m_runs(std::move(runs)) { } + ~SpriteTextBlob() { } + + void visitRuns(const RunVisitor& visitor) override; + + static TextBlobRef Make( + const FontRef& font, + const std::string& text); + + static TextBlobRef MakeWithShaper( + const FontMgrRef& fontMgr, + const FontRef& font, + const std::string& text, + TextBlob::RunHandler* handler); + + const FontRef& font() const { return m_font; } + const Runs& runs() const { return m_runs; } + +private: + FontRef m_font; + Runs m_runs; +}; + +} // namespace text + +#endif diff --git a/text/sprite_text_blob_shaper.cpp b/text/sprite_text_blob_shaper.cpp new file mode 100644 index 000000000..319c18f44 --- /dev/null +++ b/text/sprite_text_blob_shaper.cpp @@ -0,0 +1,186 @@ +// LAF Text Library +// Copyright (c) 2024 Igara Studio S.A. +// +// This file is released under the terms of the MIT license. +// Read LICENSE.txt for more information. + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "text/sprite_text_blob.h" + +#include "base/ref.h" +#include "text/font.h" +#include "text/font_mgr.h" +#include "text/sprite_sheet_font.h" + +namespace text { + +namespace { + +// Used for sub-TextBlobs created from SpriteTextBlob to handle runs +// of text with callback fonts. +// +// As SpriteTextBlob can create other TextBlobs with sub-strings +// (using other kind of fonts like SkiaFont), those TextBlob +// (SkiaTextBlob) only see the sub-string part for the run (they don't +// know the whole string). With this OffsetHandler we adapt that +// sub-string information to the global string, adjusting the UTF-8 +// range and the output position. +class OffsetHandler : public TextBlob::RunHandler { +public: + OffsetHandler(RunHandler* original, + const int offsetUtf8, + const gfx::PointF& offsetOrigin) + : m_original(original) + , m_offsetUtf8(offsetUtf8) + , m_offsetOrigin(offsetOrigin) + { } + + // TextBlob::RunHandler impl + void commitRunBuffer(TextBlob::RunInfo& info) override { + // Adjust UTF8 range and position. + info.utf8Range.begin += m_offsetUtf8; + info.utf8Range.end += m_offsetUtf8; + info.point = m_offsetOrigin; + + // Call the original RunHandler with the global info. + if (m_original) + m_original->commitRunBuffer(info); + } + +private: + RunHandler* m_original; + int m_offsetUtf8; + gfx::PointF m_offsetOrigin; +}; + +} // anonymous namespace + +TextBlobRef SpriteTextBlob::MakeWithShaper( + const FontMgrRef& fontMgr, + const FontRef& font, + const std::string& text, + TextBlob::RunHandler* handler) +{ + ASSERT(font); + ASSERT(font->type() == FontType::SpriteSheet); + ASSERT(dynamic_cast(font.get())); + + const auto* spriteFont = static_cast(font.get()); + + Runs runs; + Run run; + auto addRun = [&runs, &run, &font, &text, handler](){ + if (handler && !run.subBlob) { + TextBlob::RunInfo info; + + info.font = font; + info.utf8Range = run.utf8Range; + info.glyphCount = run.glyphs.size(); + info.glyphs = run.glyphs.data(); + info.positions = run.positions.data(); + info.clusters = run.clusters.data(); + + handler->commitRunBuffer(info); + } + runs.push_back(run); + run.clear(); + }; + + gfx::Rect textBounds; + gfx::PointF pos(0.0f, 0.0f); + base::utf8_decode decode(text); + while (true) { + const int i = decode.pos() - text.begin(); + const codepoint_t chr = decode.next(); + run.utf8Range.end = i; + if (chr == 0) + break; + + const glyph_t glyph = spriteFont->codePointToGlyph(chr); + // Code point not found, use the fallback font or the FontMgr and + // create a run using another TextBlob. + if (glyph == 0) { + // Add run with original glyph + if (!run.empty()) + addRun(); + + base::utf8_decode subDecode = decode; + while (true) { + const base::utf8_decode prevSubDecode = subDecode; + const codepoint_t subChr = subDecode.next(); + if (subChr == 0) { + decode = subDecode; + break; + } + + // Continue the run until we find a glyph that can be + // represent with the original font. + if (spriteFont->codePointToGlyph(subChr) != 0) { + decode = prevSubDecode; // Go back to the previous decode point + break; + } + } + + const int j = decode.pos() - text.begin(); + + // Add a run with "native" TextBlob (i.e. SkiaTextBlob). + run.utf8Range.begin = i; + run.utf8Range.end = j; + + // TODO add configuration of the default fallback font + auto fallbackFont = fontMgr->defaultFont(); + fallbackFont->setSize(font->height()); + fallbackFont->setAntialias(font->antialias()); + + // Align position between both fonts (font and fallbackFont) + // in the baseline pos of the original font. + FontMetrics metrics; + FontMetrics fallbackMetrics; + font->metrics(&metrics); + fallbackFont->metrics(&fallbackMetrics); + + gfx::PointF alignedPos; + alignedPos.x = pos.x; + alignedPos.y = pos.y - metrics.ascent + fallbackMetrics.ascent; + + OffsetHandler subHandler(handler, i, alignedPos); + run.subBlob = TextBlob::MakeWithShaper( + fontMgr, fallbackFont, + text.substr(i, j-i), // TODO use std::string_view + &subHandler); + if (run.subBlob) { + run.positions.push_back(pos); + + textBounds |= gfx::RectF(run.subBlob->bounds()); + pos.x += run.subBlob->bounds().w; + + addRun(); + } + + // Restore beginning of UTF8 range for the next run + run.utf8Range.begin = decode.pos() - text.begin(); + continue; + } + + gfx::Rect glyphBounds = spriteFont->getGlyphBounds(glyph); + if (glyphBounds.isEmpty()) + continue; + + run.add(glyph, pos, i - run.utf8Range.begin); + + glyphBounds.offset(pos); + textBounds |= glyphBounds; + pos.x += glyphBounds.w; + } + + // Add last run + if (!run.empty()) + addRun(); + + return base::make_ref(textBounds, font, std::move(runs)); +} + +} // namespace text diff --git a/text/text_blob.cpp b/text/text_blob.cpp index 57a500d3d..c106e5a87 100644 --- a/text/text_blob.cpp +++ b/text/text_blob.cpp @@ -13,6 +13,11 @@ #include "gfx/rect.h" #include "text/font.h" #include "text/font_metrics.h" +#include "text/sprite_text_blob.h" + +#if LAF_SKIA + #include "text/skia_text_blob.h" +#endif namespace text { @@ -31,8 +36,9 @@ TextBlob::Utf8Range TextBlob::RunInfo::getGlyphUtf8Range(size_t i) const { Utf8Range subRange; + ASSERT(clusters); ASSERT(i < glyphCount); - if (i >= glyphCount) + if (i >= glyphCount || !clusters) return subRange; // LTR @@ -73,7 +79,34 @@ gfx::RectF TextBlob::RunInfo::getGlyphBounds(const size_t i) const bounds.offset(offsets[i].x, offsets[i].y); } + + // Add global "point" offset to the bounds. + bounds.offset(point); return bounds; } +TextBlobRef TextBlob::Make( + const FontRef& font, + const std::string& text) +{ + ASSERT(font); + switch (font->type()) { + + case FontType::SpriteSheet: + return SpriteTextBlob::Make(font, text); + + case FontType::FreeType: + ASSERT(false); // TODO impl + return nullptr; + +#if LAF_SKIA + case FontType::Native: + return SkiaTextBlob::Make(font, text); +#endif + + default: + return nullptr; + } +} + } // namespace text diff --git a/text/text_blob.h b/text/text_blob.h index fd12343b7..7bb3c6f6b 100644 --- a/text/text_blob.h +++ b/text/text_blob.h @@ -63,7 +63,7 @@ namespace text { virtual void commitRunBuffer(RunInfo& info) = 0; }; - TextBlob(gfx::RectF bounds) : m_bounds(bounds) { } + TextBlob(const gfx::RectF& bounds) : m_bounds(bounds) { } virtual ~TextBlob() { } // Returns exact bounds that are required to draw this TextBlob. diff --git a/text/text_blob_shaper.cpp b/text/text_blob_shaper.cpp new file mode 100644 index 000000000..f33553748 --- /dev/null +++ b/text/text_blob_shaper.cpp @@ -0,0 +1,48 @@ +// LAF Text Library +// Copyright (c) 2024 Igara Studio S.A. +// +// This file is released under the terms of the MIT license. +// Read LICENSE.txt for more information. + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "text/text_blob.h" + +#include "text/font.h" +#include "text/sprite_text_blob.h" + +#if LAF_SKIA + #include "text/skia_text_blob.h" +#endif + +namespace text { + +TextBlobRef TextBlob::MakeWithShaper( + const FontMgrRef& fontMgr, + const FontRef& font, + const std::string& text, + TextBlob::RunHandler* handler) +{ + ASSERT(font); + switch (font->type()) { + + case FontType::SpriteSheet: + return SpriteTextBlob::MakeWithShaper(fontMgr, font, text, handler); + + case FontType::FreeType: + ASSERT(false); // TODO impl + return nullptr; + +#if LAF_SKIA + case FontType::Native: + return SkiaTextBlob::MakeWithShaper(fontMgr, font, text, handler); +#endif + + default: + return nullptr; + } +} + +} // namespace text