842 lines
24 KiB
C++
842 lines
24 KiB
C++
#include "TextRenderer.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <cassert>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <limits>
|
|
#include <mutex>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <unordered_map>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include <fontconfig/fontconfig.h>
|
|
|
|
#include <raylib.h>
|
|
#include <rlgl.h>
|
|
|
|
#undef BLACK
|
|
#undef WHITE
|
|
#undef RED
|
|
#undef GREEN
|
|
#undef BLUE
|
|
#undef YELLOW
|
|
#undef MAGENTA
|
|
|
|
#include <ft2build.h>
|
|
#include FT_FREETYPE_H
|
|
#include FT_GLYPH_H
|
|
#include <hb-ft.h>
|
|
#include <hb.h>
|
|
|
|
#include <ext/import-font.h>
|
|
#include <msdfgen.h>
|
|
|
|
namespace {
|
|
|
|
constexpr int ATLAS_DIMENSION = 1024;
|
|
constexpr int ATLAS_PADDING = 2;
|
|
constexpr float DEFAULT_EM_SCALE = 48.0f;
|
|
|
|
constexpr float hb_to_em(hb_position_t value, unsigned upem)
|
|
{
|
|
return static_cast<float>(value)
|
|
/ (64.0f * static_cast<float>(upem ? upem : 1));
|
|
}
|
|
|
|
auto ft_library() -> FT_Library
|
|
{
|
|
static FT_Library library = nullptr;
|
|
static std::once_flag once;
|
|
std::call_once(once, [] {
|
|
if (FT_Init_FreeType(&library) != 0)
|
|
library = nullptr;
|
|
else
|
|
std::atexit([] {
|
|
if (library)
|
|
FT_Done_FreeType(library);
|
|
});
|
|
});
|
|
return library;
|
|
}
|
|
|
|
struct CodepointSpan {
|
|
uint32_t codepoint {};
|
|
usize start {};
|
|
usize end {};
|
|
};
|
|
|
|
auto decode_utf8(std::string_view text) -> std::vector<CodepointSpan>
|
|
{
|
|
std::vector<CodepointSpan> spans;
|
|
usize i = 0;
|
|
while (i < text.size()) {
|
|
u8 const byte = static_cast<u8>(text[i]);
|
|
usize const start = i;
|
|
usize length = 1;
|
|
uint32_t cp = 0xFFFD;
|
|
if (byte < 0x80) {
|
|
cp = byte;
|
|
} else if ((byte & 0xE0) == 0xC0) {
|
|
if (i + 1 < text.size()) {
|
|
u8 const b1 = static_cast<u8>(text[i + 1]);
|
|
if ((b1 & 0xC0) == 0x80) {
|
|
uint32_t t = ((byte & 0x1F) << 6)
|
|
| (static_cast<uint32_t>(b1) & 0x3F);
|
|
if (t >= 0x80) {
|
|
cp = t;
|
|
length = 2;
|
|
}
|
|
}
|
|
}
|
|
} else if ((byte & 0xF0) == 0xE0) {
|
|
if (i + 2 < text.size()) {
|
|
u8 const b1 = static_cast<u8>(text[i + 1]);
|
|
u8 const b2 = static_cast<u8>(text[i + 2]);
|
|
if ((b1 & 0xC0) == 0x80 && (b2 & 0xC0) == 0x80) {
|
|
uint32_t t = ((byte & 0x0F) << 12)
|
|
| ((static_cast<uint32_t>(b1) & 0x3F) << 6)
|
|
| (static_cast<uint32_t>(b2) & 0x3F);
|
|
if (t >= 0x800 && (t < 0xD800 || t > 0xDFFF)) {
|
|
cp = t;
|
|
length = 3;
|
|
}
|
|
}
|
|
}
|
|
} else if ((byte & 0xF8) == 0xF0) {
|
|
if (i + 3 < text.size()) {
|
|
u8 const b1 = static_cast<u8>(text[i + 1]);
|
|
u8 const b2 = static_cast<u8>(text[i + 2]);
|
|
u8 const b3 = static_cast<u8>(text[i + 3]);
|
|
if ((b1 & 0xC0) == 0x80 && (b2 & 0xC0) == 0x80
|
|
&& (b3 & 0xC0) == 0x80) {
|
|
uint32_t t = ((byte & 0x07) << 18)
|
|
| ((static_cast<uint32_t>(b1) & 0x3F) << 12)
|
|
| ((static_cast<uint32_t>(b2) & 0x3F) << 6)
|
|
| (static_cast<uint32_t>(b3) & 0x3F);
|
|
if (t >= 0x10000 && t <= 0x10FFFF) {
|
|
cp = t;
|
|
length = 4;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
spans.push_back(CodepointSpan {
|
|
.codepoint = cp,
|
|
.start = start,
|
|
.end = std::min(text.size(), start + length),
|
|
});
|
|
i += length;
|
|
}
|
|
return spans;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
auto TextRenderer::flush_font(FontRuntime &rt, FontData &fd) -> void
|
|
{
|
|
rt.glyph_cache.clear();
|
|
fd.glyphs.clear();
|
|
rt.pen_x = ATLAS_PADDING;
|
|
rt.pen_y = ATLAS_PADDING;
|
|
rt.row_height = 0;
|
|
if (fd.atlas_img.data)
|
|
ImageClearBackground(&fd.atlas_img, BLANK);
|
|
if (fd.atlas.id != 0 && fd.atlas_img.data)
|
|
UpdateTexture(fd.atlas, fd.atlas_img.data);
|
|
}
|
|
|
|
auto TextRenderer::allocate_region(FontRuntime &rt, FontData &fd, int width,
|
|
int height) -> std::optional<std::pair<int, int>>
|
|
{
|
|
(void)fd;
|
|
int padded_w = width + ATLAS_PADDING;
|
|
if (padded_w > rt.atlas_width || height + ATLAS_PADDING > rt.atlas_height)
|
|
return std::nullopt;
|
|
if (rt.pen_x + padded_w > rt.atlas_width) {
|
|
rt.pen_x = ATLAS_PADDING;
|
|
rt.pen_y += rt.row_height;
|
|
rt.row_height = 0;
|
|
}
|
|
if (rt.pen_y + height + ATLAS_PADDING > rt.atlas_height)
|
|
return std::nullopt;
|
|
int x = rt.pen_x;
|
|
int y = rt.pen_y;
|
|
rt.pen_x += padded_w;
|
|
rt.row_height = std::max(rt.row_height, height + ATLAS_PADDING);
|
|
return std::pair { x, y };
|
|
}
|
|
|
|
auto TextRenderer::upload_region(FontData &fd, int dst_x, int dst_y, int width,
|
|
int height, std::vector<Color> const &buffer) -> void
|
|
{
|
|
Rectangle rec { static_cast<float>(dst_x), static_cast<float>(dst_y),
|
|
static_cast<float>(width), static_cast<float>(height) };
|
|
if (fd.atlas.id != 0)
|
|
UpdateTextureRec(fd.atlas, rec, buffer.data());
|
|
if (!fd.atlas_img.data)
|
|
return;
|
|
auto *pixels = static_cast<Color *>(fd.atlas_img.data);
|
|
for (int row = 0; row < height; ++row) {
|
|
auto *dst = pixels + (dst_y + row) * fd.atlas_img.width + dst_x;
|
|
std::memcpy(dst, buffer.data() + row * width, sizeof(Color) * width);
|
|
}
|
|
}
|
|
|
|
auto TextRenderer::generate_glyph(FontRuntime &rt, FontData &fd,
|
|
u32 glyph_index) -> std::optional<GlyphCacheEntry>
|
|
{
|
|
auto const gen_start = std::chrono::steady_clock::now();
|
|
msdfgen::Shape shape;
|
|
double advance_em = 0.0;
|
|
msdfgen::GlyphIndex const index(glyph_index);
|
|
if (!rt.msdf_font
|
|
|| !msdfgen::loadGlyph(shape, rt.msdf_font, index,
|
|
msdfgen::FONT_SCALING_EM_NORMALIZED, &advance_em))
|
|
return std::nullopt;
|
|
shape.normalize();
|
|
// FIXME: Figure out shader
|
|
// msdfgen::edgeColoringInkTrap(shape, 3.0);
|
|
auto bounds = shape.getBounds();
|
|
float const width_em = static_cast<float>(bounds.r - bounds.l);
|
|
float const height_em = static_cast<float>(bounds.t - bounds.b);
|
|
double const scale = rt.em_scale;
|
|
int bmp_w = std::max(
|
|
1, static_cast<int>(std::ceil(width_em * scale + 2.0 * rt.px_range)));
|
|
int bmp_h = std::max(
|
|
1, static_cast<int>(std::ceil(height_em * scale + 2.0 * rt.px_range)));
|
|
|
|
if (bmp_w + ATLAS_PADDING > rt.atlas_width
|
|
|| bmp_h + ATLAS_PADDING > rt.atlas_height) {
|
|
TraceLog(LOG_WARNING, "Glyph %u bitmap %dx%d exceeds atlas %dx%d",
|
|
glyph_index, bmp_w, bmp_h, rt.atlas_width, rt.atlas_height);
|
|
GlyphCacheEntry too_large {};
|
|
too_large.width = 0;
|
|
too_large.height = 0;
|
|
return too_large;
|
|
}
|
|
|
|
auto place = allocate_region(rt, fd, bmp_w, bmp_h);
|
|
if (!place) {
|
|
TraceLog(LOG_INFO, "Atlas full, flushing before glyph %u", glyph_index);
|
|
flush_font(rt, fd);
|
|
place = allocate_region(rt, fd, bmp_w, bmp_h);
|
|
if (!place)
|
|
return std::nullopt;
|
|
}
|
|
|
|
msdfgen::Bitmap<float, 3> msdf_bitmap(bmp_w, bmp_h);
|
|
msdfgen::Vector2 scale_vec(scale, scale);
|
|
double const inv_scale = 1.0 / scale;
|
|
msdfgen::Vector2 translate(-bounds.l + rt.px_range * inv_scale,
|
|
-bounds.b + rt.px_range * inv_scale);
|
|
msdfgen::generateMSDF(
|
|
msdf_bitmap, shape, rt.px_range, scale_vec, translate);
|
|
|
|
std::vector<Color> buffer(static_cast<usize>(bmp_w) * bmp_h);
|
|
// FIXME: Figure out shader
|
|
// for (int y = 0; y < bmp_h; ++y) {
|
|
// int const dst_y = bmp_h - 1 - y;
|
|
// for (int x = 0; x < bmp_w; ++x) {
|
|
// float const *px = msdf_bitmap(x, y);
|
|
// auto const r = msdfgen::pixelFloatToByte(px[0]);
|
|
// auto const g = msdfgen::pixelFloatToByte(px[1]);
|
|
// auto const b = msdfgen::pixelFloatToByte(px[2]);
|
|
// buffer[static_cast<usize>(dst_y) * bmp_w + x]
|
|
// = Color { r, g, b, 255 };
|
|
// }
|
|
//}
|
|
|
|
auto c1 { (int)std::round(msdf_bitmap(0, 0)[3]) };
|
|
auto c4 { (int)std::round(msdf_bitmap(bmp_w - 1, bmp_h - 1)[3]) };
|
|
|
|
auto sum_white = 0;
|
|
auto sum_black = 0;
|
|
for (int y = 0; y < bmp_h; ++y) {
|
|
for (int x = 0; x < bmp_w; ++x) {
|
|
float const *px = msdf_bitmap(x, y);
|
|
auto const r = msdfgen::pixelFloatToByte(px[0]);
|
|
if (r > 127) {
|
|
sum_white++;
|
|
} else {
|
|
sum_black++;
|
|
}
|
|
}
|
|
}
|
|
bool flip { sum_white > sum_black && (float)bmp_w / (float)bmp_h > 0.6 };
|
|
if (c1 == c4) {
|
|
flip = false;
|
|
}
|
|
|
|
// This really isn't the most accurate thing in the world but should work
|
|
// for now. Things like commas might be fucked.
|
|
for (int y = 0; y < bmp_h; ++y) {
|
|
int const dst_y = bmp_h - 1 - y;
|
|
for (int x = 0; x < bmp_w; ++x) {
|
|
float const *px = msdf_bitmap(x, y);
|
|
auto const r = msdfgen::pixelFloatToByte(px[0]);
|
|
if (flip) {
|
|
buffer[static_cast<usize>(dst_y) * bmp_w + x] = Color { 255,
|
|
255, 255, static_cast<unsigned char>(255 - r) };
|
|
} else {
|
|
buffer[static_cast<usize>(dst_y) * bmp_w + x]
|
|
= Color { 255, 255, 255, r };
|
|
}
|
|
}
|
|
}
|
|
|
|
upload_region(fd, place->first, place->second, bmp_w, bmp_h, buffer);
|
|
|
|
GlyphCacheEntry entry;
|
|
entry.atlas_x = place->first;
|
|
entry.atlas_y = place->second;
|
|
entry.width = bmp_w;
|
|
entry.height = bmp_h;
|
|
|
|
entry.glyph.advance = static_cast<float>(advance_em);
|
|
entry.glyph.plane_bounds.left = static_cast<float>(bounds.l);
|
|
entry.glyph.plane_bounds.right = static_cast<float>(bounds.r);
|
|
entry.glyph.plane_bounds.top = static_cast<float>(bounds.t);
|
|
entry.glyph.plane_bounds.bottom = static_cast<float>(bounds.b);
|
|
entry.glyph.glyph_bounds.left = static_cast<float>(entry.atlas_x);
|
|
entry.glyph.glyph_bounds.top = static_cast<float>(entry.atlas_y);
|
|
entry.glyph.glyph_bounds.right
|
|
= static_cast<float>(entry.atlas_x + entry.width);
|
|
entry.glyph.glyph_bounds.bottom
|
|
= static_cast<float>(entry.atlas_y + entry.height);
|
|
|
|
auto const gen_end = std::chrono::steady_clock::now();
|
|
auto const gen_ms
|
|
= std::chrono::duration<double, std::milli>(gen_end - gen_start)
|
|
.count();
|
|
if (gen_ms > 2.0)
|
|
TraceLog(LOG_INFO, "Generated glyph %u in %.2f ms (%dx%d texels)",
|
|
glyph_index, gen_ms, entry.width, entry.height);
|
|
return entry;
|
|
}
|
|
|
|
auto TextRenderer::ensure_glyph(FontRuntime &rt, FontData &fd, u32 glyph_index,
|
|
bool mark_usage) -> GlyphCacheEntry *
|
|
{
|
|
auto it = rt.glyph_cache.find(glyph_index);
|
|
if (it != rt.glyph_cache.end()) {
|
|
if (mark_usage)
|
|
it->second.stamp = rt.frame_stamp;
|
|
return &it->second;
|
|
}
|
|
auto entry = generate_glyph(rt, fd, glyph_index);
|
|
if (!entry)
|
|
return nullptr;
|
|
auto [inserted_it, ok]
|
|
= rt.glyph_cache.emplace(glyph_index, std::move(*entry));
|
|
if (!ok)
|
|
return nullptr;
|
|
inserted_it->second.stamp
|
|
= mark_usage ? rt.frame_stamp : inserted_it->second.stamp;
|
|
fd.glyphs[glyph_index] = inserted_it->second.glyph;
|
|
return &inserted_it->second;
|
|
}
|
|
|
|
TextRenderer::TextRenderer()
|
|
{
|
|
static char const msdf_vs_data[] {
|
|
#embed "base.vert"
|
|
, 0 // cppcheck-suppress syntaxError
|
|
};
|
|
static char const msdf_fs_data[] {
|
|
#embed "msdf.frag"
|
|
, 0 // cppcheck-suppress syntaxError
|
|
};
|
|
m_msdf_shader = LoadShaderFromMemory(msdf_vs_data, msdf_fs_data);
|
|
assert(IsShaderValid(m_msdf_shader));
|
|
m_px_range_uniform = GetShaderLocation(m_msdf_shader, "pxRange");
|
|
}
|
|
|
|
TextRenderer::~TextRenderer()
|
|
{
|
|
for (usize i = 0; i < m_font_sets.size(); ++i) {
|
|
FontHandle handle;
|
|
handle.id = i;
|
|
unload_font(handle);
|
|
}
|
|
// Not unloading the shader... I have no clue why, but there's some sort of
|
|
// double free. I love C interop!!!!
|
|
}
|
|
|
|
auto TextRenderer::measure_text(FontHandle const font,
|
|
std::string_view const text, int const size) -> Vector2
|
|
{
|
|
usize const handle_id = font();
|
|
if (handle_id >= m_font_sets.size())
|
|
return Vector2 { 0.0f, 0.0f };
|
|
auto const &font_set = m_font_sets[handle_id];
|
|
if (font_set.font_indices.empty())
|
|
return Vector2 { 0.0f, 0.0f };
|
|
|
|
auto placements = shape_text(font, text);
|
|
|
|
auto primary_runtime_index = font_set.font_indices.front();
|
|
if (placements.empty()) {
|
|
if (primary_runtime_index >= m_font_runtime.size()
|
|
|| !m_font_runtime[primary_runtime_index])
|
|
return Vector2 { 0.0f, 0.0f };
|
|
auto const &rt_primary = *m_font_runtime[primary_runtime_index];
|
|
float height_em = rt_primary.ascent - rt_primary.descent;
|
|
return Vector2 { 0.0f, height_em * static_cast<float>(size) };
|
|
}
|
|
|
|
float advance_em = 0.0f;
|
|
float min_x_em = 0.0f;
|
|
float max_x_em = 0.0f;
|
|
bool first = true;
|
|
bool have_metrics = false;
|
|
float max_ascent = 0.0f;
|
|
float min_descent = 0.0f;
|
|
|
|
for (auto const &placement : placements) {
|
|
usize const runtime_index = placement.runtime_index;
|
|
if (runtime_index >= m_font_runtime.size()
|
|
|| !m_font_runtime[runtime_index])
|
|
continue;
|
|
auto &rt = *m_font_runtime[runtime_index];
|
|
auto &fd = m_font_data[runtime_index];
|
|
auto *entry = ensure_glyph(rt, fd, placement.glyph_index, false);
|
|
if (!entry || entry->width == 0 || entry->height == 0)
|
|
continue;
|
|
float const x_offset_em = hb_to_em(placement.x_offset, rt.units_per_em);
|
|
float const left
|
|
= advance_em + x_offset_em + entry->glyph.plane_bounds.left;
|
|
float const right
|
|
= advance_em + x_offset_em + entry->glyph.plane_bounds.right;
|
|
if (first) {
|
|
min_x_em = left;
|
|
max_x_em = right;
|
|
first = false;
|
|
} else {
|
|
min_x_em = std::min(min_x_em, left);
|
|
max_x_em = std::max(max_x_em, right);
|
|
}
|
|
if (!have_metrics) {
|
|
max_ascent = rt.ascent;
|
|
min_descent = rt.descent;
|
|
have_metrics = true;
|
|
} else {
|
|
max_ascent = std::max(max_ascent, rt.ascent);
|
|
min_descent = std::min(min_descent, rt.descent);
|
|
}
|
|
advance_em += hb_to_em(placement.x_advance, rt.units_per_em);
|
|
}
|
|
|
|
if (first) {
|
|
if (primary_runtime_index >= m_font_runtime.size()
|
|
|| !m_font_runtime[primary_runtime_index])
|
|
return Vector2 { 0.0f, 0.0f };
|
|
auto const &rt = *m_font_runtime[primary_runtime_index];
|
|
float height_em = rt.ascent - rt.descent;
|
|
return Vector2 { 0.0f, height_em * static_cast<float>(size) };
|
|
}
|
|
|
|
float width_em = std::max(max_x_em, advance_em) - min_x_em;
|
|
float height_em = 0.0f;
|
|
if (have_metrics) {
|
|
height_em = max_ascent - min_descent;
|
|
} else if (primary_runtime_index < m_font_runtime.size()
|
|
&& m_font_runtime[primary_runtime_index]) {
|
|
auto const &rt = *m_font_runtime[primary_runtime_index];
|
|
height_em = rt.ascent - rt.descent;
|
|
}
|
|
|
|
return Vector2 { width_em * static_cast<float>(size),
|
|
height_em * static_cast<float>(size) };
|
|
}
|
|
|
|
auto TextRenderer::draw_text(FontHandle const font, std::string_view const text,
|
|
Vector2 const pos, int const size, Color const color) -> void
|
|
{
|
|
auto const draw_start = std::chrono::steady_clock::now();
|
|
int const pos_x = pos.x;
|
|
int const pos_y = pos.y;
|
|
usize const handle_id = font();
|
|
if (handle_id >= m_font_sets.size())
|
|
return;
|
|
auto const &font_set = m_font_sets[handle_id];
|
|
if (font_set.font_indices.empty())
|
|
return;
|
|
|
|
auto placements = shape_text(font, text);
|
|
if (placements.empty())
|
|
return;
|
|
|
|
float const size_f = static_cast<float>(size);
|
|
float pen_x_em = 0.0f;
|
|
float pen_y_em = 0.0f;
|
|
std::vector<usize> updated_stamp;
|
|
updated_stamp.reserve(font_set.font_indices.size());
|
|
|
|
for (auto const &placement : placements) {
|
|
usize const runtime_index = placement.runtime_index;
|
|
if (runtime_index >= m_font_runtime.size()
|
|
|| !m_font_runtime[runtime_index])
|
|
continue;
|
|
auto &rt = *m_font_runtime[runtime_index];
|
|
auto &fd = m_font_data[runtime_index];
|
|
if (std::find(updated_stamp.begin(), updated_stamp.end(), runtime_index)
|
|
== updated_stamp.end()) {
|
|
rt.frame_stamp++;
|
|
updated_stamp.push_back(runtime_index);
|
|
}
|
|
|
|
auto *entry = ensure_glyph(rt, fd, placement.glyph_index, true);
|
|
if (!entry || entry->width == 0 || entry->height == 0)
|
|
continue;
|
|
|
|
float const advance_em = hb_to_em(placement.x_advance, rt.units_per_em);
|
|
float const x_offset_em = hb_to_em(placement.x_offset, rt.units_per_em);
|
|
float const y_offset_em = hb_to_em(placement.y_offset, rt.units_per_em);
|
|
float const x_base_em = pen_x_em + x_offset_em;
|
|
float const y_base_em = pen_y_em + y_offset_em;
|
|
float const scale_px = size_f / static_cast<float>(rt.em_scale);
|
|
float const margin_px = static_cast<float>(rt.px_range) * scale_px;
|
|
float const dest_x = pos_x
|
|
+ (x_base_em + entry->glyph.plane_bounds.left) * size_f - margin_px;
|
|
float const dest_y = pos_y
|
|
- (y_base_em + entry->glyph.plane_bounds.top) * size_f - margin_px;
|
|
float const dest_w = static_cast<float>(entry->width) * scale_px;
|
|
float const dest_h = static_cast<float>(entry->height) * scale_px;
|
|
|
|
Rectangle source {
|
|
entry->glyph.glyph_bounds.left,
|
|
entry->glyph.glyph_bounds.top,
|
|
static_cast<float>(entry->width),
|
|
static_cast<float>(entry->height),
|
|
};
|
|
Rectangle dest { dest_x, dest_y, dest_w, dest_h };
|
|
DrawTexturePro(
|
|
fd.atlas, source, dest, Vector2 { 0.0f, 0.0f }, 0.0f, color);
|
|
|
|
pen_x_em += advance_em;
|
|
pen_y_em += hb_to_em(placement.y_advance, rt.units_per_em);
|
|
}
|
|
|
|
auto const draw_end = std::chrono::steady_clock::now();
|
|
auto const draw_ms
|
|
= std::chrono::duration<double, std::milli>(draw_end - draw_start)
|
|
.count();
|
|
if (draw_ms > 5.0)
|
|
TraceLog(LOG_INFO, "draw_text took %.2f ms for %zu glyphs", draw_ms,
|
|
placements.size());
|
|
}
|
|
|
|
auto TextRenderer::load_single_font(std::filesystem::path const &path)
|
|
-> std::optional<usize>
|
|
{
|
|
FT_Library const ft = ft_library();
|
|
if (!ft)
|
|
return std::nullopt;
|
|
|
|
FT_Face face = nullptr;
|
|
if (FT_New_Face(ft, path.string().c_str(), 0, &face) != 0)
|
|
return std::nullopt;
|
|
if (FT_Select_Charmap(face, FT_ENCODING_UNICODE) != 0) {
|
|
FT_Done_Face(face);
|
|
return std::nullopt;
|
|
}
|
|
|
|
auto runtime = std::make_unique<FontRuntime>();
|
|
runtime->face = face;
|
|
runtime->atlas_width = ATLAS_DIMENSION;
|
|
runtime->atlas_height = ATLAS_DIMENSION;
|
|
runtime->pen_x = ATLAS_PADDING;
|
|
runtime->pen_y = ATLAS_PADDING;
|
|
runtime->row_height = 0;
|
|
runtime->px_range = 0.05; // kDefaultPxRange;
|
|
runtime->em_scale = DEFAULT_EM_SCALE;
|
|
runtime->frame_stamp = 0;
|
|
runtime->units_per_em
|
|
= static_cast<unsigned>(face->units_per_EM ? face->units_per_EM : 2048);
|
|
runtime->ascent = static_cast<float>(face->ascender)
|
|
/ (64.0f * static_cast<float>(runtime->units_per_em));
|
|
runtime->descent = static_cast<float>(face->descender)
|
|
/ (64.0f * static_cast<float>(runtime->units_per_em));
|
|
float line_height = static_cast<float>(face->height)
|
|
/ (64.0f * static_cast<float>(runtime->units_per_em));
|
|
float adv_height = runtime->ascent - runtime->descent;
|
|
runtime->line_gap = std::max(0.0f, line_height - adv_height);
|
|
|
|
runtime->hb_face = hb_ft_face_create_referenced(face);
|
|
if (!runtime->hb_face) {
|
|
FT_Done_Face(face);
|
|
return std::nullopt;
|
|
}
|
|
runtime->hb_font = hb_ft_font_create_referenced(face);
|
|
if (!runtime->hb_font) {
|
|
hb_face_destroy(runtime->hb_face);
|
|
FT_Done_Face(face);
|
|
return std::nullopt;
|
|
}
|
|
hb_font_set_scale(runtime->hb_font,
|
|
static_cast<int>(runtime->units_per_em) << 6,
|
|
static_cast<int>(runtime->units_per_em) << 6);
|
|
hb_ft_font_set_funcs(runtime->hb_font);
|
|
|
|
runtime->msdf_font = msdfgen::adoptFreetypeFont(face);
|
|
if (!runtime->msdf_font) {
|
|
hb_font_destroy(runtime->hb_font);
|
|
hb_face_destroy(runtime->hb_face);
|
|
FT_Done_Face(face);
|
|
return std::nullopt;
|
|
}
|
|
|
|
FontData font_data {};
|
|
font_data.font_path = path;
|
|
font_data.atlas_img
|
|
= GenImageColor(runtime->atlas_width, runtime->atlas_height, BLANK);
|
|
if (!font_data.atlas_img.data) {
|
|
msdfgen::destroyFont(runtime->msdf_font);
|
|
runtime->msdf_font = nullptr;
|
|
hb_font_destroy(runtime->hb_font);
|
|
hb_face_destroy(runtime->hb_face);
|
|
return std::nullopt;
|
|
}
|
|
font_data.atlas = LoadTextureFromImage(font_data.atlas_img);
|
|
if (font_data.atlas.id == 0) {
|
|
UnloadImage(font_data.atlas_img);
|
|
msdfgen::destroyFont(runtime->msdf_font);
|
|
runtime->msdf_font = nullptr;
|
|
hb_font_destroy(runtime->hb_font);
|
|
hb_face_destroy(runtime->hb_face);
|
|
return std::nullopt;
|
|
}
|
|
SetTextureFilter(font_data.atlas, TEXTURE_FILTER_BILINEAR);
|
|
SetTextureWrap(font_data.atlas, TEXTURE_WRAP_CLAMP);
|
|
flush_font(*runtime, font_data);
|
|
|
|
m_font_data.emplace_back(std::move(font_data));
|
|
m_font_runtime.emplace_back(std::move(runtime));
|
|
return m_font_data.size() - 1;
|
|
}
|
|
|
|
auto TextRenderer::load_font(std::filesystem::path const &path,
|
|
std::span<std::filesystem::path const> fallback_fonts)
|
|
-> std::optional<FontHandle>
|
|
{
|
|
auto primary_index = load_single_font(path);
|
|
if (!primary_index)
|
|
return std::nullopt;
|
|
|
|
FontSet set;
|
|
set.font_indices.push_back(*primary_index);
|
|
|
|
for (auto const &fallback_path : fallback_fonts) {
|
|
auto fallback_index = load_single_font(fallback_path);
|
|
if (!fallback_index) {
|
|
TraceLog(LOG_WARNING, "Failed to load fallback font: %s",
|
|
fallback_path.string().c_str());
|
|
continue;
|
|
}
|
|
set.font_indices.push_back(*fallback_index);
|
|
}
|
|
|
|
m_font_sets.emplace_back(std::move(set));
|
|
FontHandle handle;
|
|
handle.id = m_font_sets.size() - 1;
|
|
return handle;
|
|
}
|
|
|
|
auto TextRenderer::shape_text(FontHandle const font,
|
|
std::string_view const text) -> std::vector<GlyphPlacement>
|
|
{
|
|
std::vector<GlyphPlacement> shaped;
|
|
if (text.empty())
|
|
return shaped;
|
|
|
|
usize const handle_id = font();
|
|
if (handle_id >= m_font_sets.size())
|
|
return shaped;
|
|
auto const &font_set = m_font_sets[handle_id];
|
|
if (font_set.font_indices.empty())
|
|
return shaped;
|
|
|
|
auto codepoints = decode_utf8(text);
|
|
if (codepoints.empty())
|
|
return shaped;
|
|
|
|
constexpr usize kNoFont = std::numeric_limits<usize>::max();
|
|
std::vector<usize> selections(codepoints.size(), kNoFont);
|
|
for (usize i = 0; i < codepoints.size(); ++i) {
|
|
bool matched = false;
|
|
for (usize candidate = 0; candidate < font_set.font_indices.size();
|
|
++candidate) {
|
|
usize runtime_index = font_set.font_indices[candidate];
|
|
if (runtime_index >= m_font_runtime.size())
|
|
continue;
|
|
auto const &runtime_ptr = m_font_runtime[runtime_index];
|
|
if (!runtime_ptr || !runtime_ptr->face)
|
|
continue;
|
|
FT_UInt glyph
|
|
= FT_Get_Char_Index(runtime_ptr->face, codepoints[i].codepoint);
|
|
if (glyph != 0) {
|
|
selections[i] = candidate;
|
|
matched = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!matched)
|
|
selections[i] = kNoFont;
|
|
}
|
|
|
|
usize idx = 0;
|
|
while (idx < codepoints.size()) {
|
|
usize font_choice = selections[idx];
|
|
if (font_choice == kNoFont) {
|
|
++idx;
|
|
continue;
|
|
}
|
|
if (font_choice >= font_set.font_indices.size())
|
|
font_choice = 0;
|
|
usize runtime_index = font_set.font_indices[font_choice];
|
|
if (runtime_index >= m_font_runtime.size()
|
|
|| !m_font_runtime[runtime_index]
|
|
|| !m_font_runtime[runtime_index]->hb_font) {
|
|
++idx;
|
|
continue;
|
|
}
|
|
|
|
usize segment_start = codepoints[idx].start;
|
|
usize segment_end = codepoints[idx].end;
|
|
usize end_idx = idx + 1;
|
|
while (
|
|
end_idx < codepoints.size() && selections[end_idx] == font_choice) {
|
|
segment_end = codepoints[end_idx].end;
|
|
++end_idx;
|
|
}
|
|
|
|
if (segment_end <= segment_start) {
|
|
idx = end_idx;
|
|
continue;
|
|
}
|
|
|
|
std::string_view segment
|
|
= text.substr(segment_start, segment_end - segment_start);
|
|
if (segment.empty()) {
|
|
idx = end_idx;
|
|
continue;
|
|
}
|
|
|
|
hb_buffer_t *buffer = hb_buffer_create();
|
|
hb_buffer_add_utf8(buffer, segment.data(),
|
|
static_cast<int>(segment.size()), 0,
|
|
static_cast<int>(segment.size()));
|
|
hb_buffer_guess_segment_properties(buffer);
|
|
hb_shape(m_font_runtime[runtime_index]->hb_font, buffer, nullptr, 0);
|
|
|
|
unsigned length = hb_buffer_get_length(buffer);
|
|
auto *infos = hb_buffer_get_glyph_infos(buffer, nullptr);
|
|
auto *positions = hb_buffer_get_glyph_positions(buffer, nullptr);
|
|
for (unsigned i = 0; i < length; ++i) {
|
|
GlyphPlacement placement;
|
|
placement.runtime_index = runtime_index;
|
|
placement.glyph_index = infos[i].codepoint;
|
|
placement.x_advance = positions[i].x_advance;
|
|
placement.y_advance = positions[i].y_advance;
|
|
placement.x_offset = positions[i].x_offset;
|
|
placement.y_offset = positions[i].y_offset;
|
|
shaped.emplace_back(placement);
|
|
}
|
|
hb_buffer_destroy(buffer);
|
|
idx = end_idx;
|
|
}
|
|
|
|
return shaped;
|
|
}
|
|
|
|
auto TextRenderer::unload_font(FontHandle const font) -> void
|
|
{
|
|
usize const handle_id = font();
|
|
if (handle_id >= m_font_sets.size())
|
|
return;
|
|
|
|
auto &font_set = m_font_sets[handle_id];
|
|
for (usize runtime_index : font_set.font_indices) {
|
|
if (runtime_index >= m_font_runtime.size())
|
|
continue;
|
|
|
|
if (auto &runtime_ptr = m_font_runtime[runtime_index]) {
|
|
auto &rt = *runtime_ptr;
|
|
rt.glyph_cache.clear();
|
|
// No freeing here because they are already cleaned up somewhere...
|
|
// idk. fml.
|
|
rt.face = nullptr;
|
|
}
|
|
m_font_runtime[runtime_index].reset();
|
|
|
|
if (runtime_index < m_font_data.size()) {
|
|
auto &fd = m_font_data[runtime_index];
|
|
if (fd.atlas.id != 0)
|
|
UnloadTexture(fd.atlas);
|
|
if (fd.atlas_img.data)
|
|
UnloadImage(fd.atlas_img);
|
|
fd.atlas = Texture2D {};
|
|
fd.atlas_img = Image {};
|
|
fd.glyphs.clear();
|
|
}
|
|
}
|
|
font_set.font_indices.clear();
|
|
}
|
|
|
|
auto find_font_path(std::string_view path)
|
|
-> std::optional<std::filesystem::path>
|
|
{
|
|
static std::once_flag fc_once;
|
|
std::call_once(fc_once, []() {
|
|
if (FcInit())
|
|
std::atexit([] { FcFini(); });
|
|
});
|
|
|
|
static std::mutex m;
|
|
static std::unordered_map<std::string, std::optional<std::string>> cache;
|
|
|
|
std::string const key(path);
|
|
|
|
{
|
|
std::scoped_lock lock(m);
|
|
if (auto it = cache.find(key); it != cache.end())
|
|
return it->second;
|
|
}
|
|
|
|
FcPattern *pattern
|
|
= FcNameParse(reinterpret_cast<FcChar8 const *>(key.c_str()));
|
|
if (!pattern) {
|
|
std::scoped_lock lock(m);
|
|
return cache[key] = std::nullopt;
|
|
}
|
|
|
|
FcConfigSubstitute(nullptr, pattern, FcMatchPattern);
|
|
FcDefaultSubstitute(pattern);
|
|
|
|
FcResult result;
|
|
FcPattern *font = FcFontMatch(nullptr, pattern, &result);
|
|
|
|
std::optional<std::string> final_path;
|
|
if (font) {
|
|
FcChar8 *file;
|
|
if (FcPatternGetString(font, FC_FILE, 0, &file) == FcResultMatch)
|
|
final_path = reinterpret_cast<char *>(file);
|
|
FcPatternDestroy(font);
|
|
}
|
|
|
|
FcPatternDestroy(pattern);
|
|
|
|
{
|
|
std::scoped_lock lock(m);
|
|
cache[key] = final_path;
|
|
}
|
|
return final_path;
|
|
}
|