mirror of
https://github.com/ocornut/imgui.git
synced 2026-09-26 16:05:45 +03:00
FreeType: opt-in HarfBuzz shaping (RenderShapedText) for GSUB ligatures
Add an opt-in ImGuiFreeType::RenderShapedText API, guarded by
IMGUI_ENABLE_HARFBUZZ_SHAPING, that shapes a text cluster with HarfBuzz
and renders the resulting (possibly ligature) glyphs. This forms GSUB
ligatures -- ZWJ families (U+1F468 U+200D U+1F469 ...) and regional-
indicator flags (U+1F1EB U+1F1F7 -> 🇫🇷) -- that ImFont::RenderText
cannot, because RenderText is codepoint-by-codepoint and never consults
the font's GSUB table.
Implementation (all in misc/freetype/imgui_freetype.cpp, guarded):
- an hb_font_t* per FT_Face (hb_ft_font_create in InitFont, destroyed in
CloseFont);
- a per-(src,baked) shaped-glyph cache (ImVector<ImFontGlyph> +
ImGuiStorage glyph-index -> cache slot) for ligature glyphs that have
no Unicode codepoint and so cannot live in ImFontBaked's
codepoint-indexed tables;
- ImGui_ImplFreeType_BakeGlyphByID: bake a glyph by its FT glyph INDEX
(skipping FT_Get_Char_Index) on demand, pack+blit into the atlas, set
UVs/Colored, cache it;
- ImGuiFreeType::RenderShapedText: hb_shape the cluster, then for each
shaped glyph bake-by-ID and emit a quad with the shaped advance,
mirroring RenderText's mid-frame texture-growth retry (cmd_count).
The existing RenderText hot path is untouched: this is a separate opt-in
API for renderers that need ligatures (e.g. a TUI cell grid that draws
emoji clusters). Compiled with and without IMGUI_ENABLE_HARFBUZZ_SHAPING;
without it the shaping code is absent and the build is identical to
today's FreeType builder.
Requires HarfBuzz headers + lib when IMGUI_ENABLE_HARFBUZZ_SHAPING is
defined.
This commit is contained in:
parent
46d39d56fe
commit
56480a0708
2 changed files with 239 additions and 0 deletions
|
|
@ -48,6 +48,10 @@
|
|||
#include FT_GLYPH_H // <freetype/ftglyph.h>
|
||||
#include FT_SIZES_H // <freetype/ftsizes.h>
|
||||
#include FT_SYNTHESIS_H // <freetype/ftsynth.h>
|
||||
#if defined(IMGUI_ENABLE_HARFBUZZ_SHAPING)
|
||||
#include <hb.h> // HarfBuzz shaping (GSUB ligatures: ZWJ families, regional-indicator flags)
|
||||
#include <hb-ft.h> // hb_ft_font_create
|
||||
#endif
|
||||
|
||||
// Handle LunaSVG and PlutoSVG
|
||||
#if defined(IMGUI_ENABLE_FREETYPE_LUNASVG) && defined(IMGUI_ENABLE_FREETYPE_PLUTOSVG)
|
||||
|
|
@ -168,6 +172,9 @@ struct ImGui_ImplFreeType_FontSrcData
|
|||
ImGuiFreeTypeLoaderFlags UserFlags; // = ImFontConfig::FontLoaderFlags
|
||||
FT_Int32 LoadFlags;
|
||||
ImFontBaked* BakedLastActivated;
|
||||
#if defined(IMGUI_ENABLE_HARFBUZZ_SHAPING)
|
||||
hb_font_t* HbFont; // HarfBuzz font for shaping (GSUB ligatures). Created in InitFont, destroyed in CloseFont.
|
||||
#endif
|
||||
};
|
||||
|
||||
// Stored in ImFontBaked::FontLoaderDatas: pointer to SourcesCount instances of this. ALLOCATED BY CORE.
|
||||
|
|
@ -175,6 +182,17 @@ struct ImGui_ImplFreeType_FontSrcBakedData
|
|||
{
|
||||
FT_Size FtSize; // This represent a FT_Face with a given size.
|
||||
ImGui_ImplFreeType_FontSrcBakedData() { memset((void*)this, 0, sizeof(*this)); }
|
||||
#if defined(IMGUI_ENABLE_HARFBUZZ_SHAPING)
|
||||
~ImGui_ImplFreeType_FontSrcBakedData() { ShapedGlyphs.clear_destruct(); }
|
||||
|
||||
// Shaped-glyph cache: glyphs baked by FT glyph INDEX (not codepoint),
|
||||
// used by ImGui_ImplFreeType_RenderShapedText for GSUB ligatures
|
||||
// (ZWJ families, regional-indicator flags) whose combined glyph has no
|
||||
// Unicode codepoint and therefore cannot live in ImFontBaked's
|
||||
// codepoint-indexed Glyphs/IndexLookup tables.
|
||||
ImVector<ImFontGlyph> ShapedGlyphs;
|
||||
ImGuiStorage ShapedGlyphsByGlyphID; // FT glyph index -> index in ShapedGlyphs
|
||||
#endif
|
||||
};
|
||||
|
||||
bool ImGui_ImplFreeType_FontSrcData::InitFont(FT_Library ft_library, const ImFontConfig* src, ImGuiFreeTypeLoaderFlags extra_font_loader_flags)
|
||||
|
|
@ -208,11 +226,27 @@ bool ImGui_ImplFreeType_FontSrcData::InitFont(FT_Library ft_library, const ImFon
|
|||
if (UserFlags & ImGuiFreeTypeLoaderFlags_LoadColor)
|
||||
LoadFlags |= FT_LOAD_COLOR;
|
||||
|
||||
// Create a HarfBuzz font backed by this FT_Face. Used by
|
||||
// ImGui_ImplFreeType_RenderShapedText to GSUB-shape grapheme clusters
|
||||
// (ZWJ families, regional-indicator flags) into their ligature glyph,
|
||||
// which ImGui's codepoint-by-codepoint RenderText cannot do.
|
||||
#if defined(IMGUI_ENABLE_HARFBUZZ_SHAPING)
|
||||
HbFont = hb_ft_font_create(FtFace, NULL);
|
||||
hb_ft_font_set_load_flags(HbFont, LoadFlags);
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ImGui_ImplFreeType_FontSrcData::CloseFont()
|
||||
{
|
||||
#if defined(IMGUI_ENABLE_HARFBUZZ_SHAPING)
|
||||
if (HbFont)
|
||||
{
|
||||
hb_font_destroy(HbFont);
|
||||
HbFont = NULL;
|
||||
}
|
||||
#endif
|
||||
if (FtFace)
|
||||
{
|
||||
FT_Done_Face(FtFace);
|
||||
|
|
@ -570,6 +604,201 @@ static bool ImGui_ImplFreeType_FontBakedLoadGlyph(ImFontAtlas* atlas, ImFontConf
|
|||
return true;
|
||||
}
|
||||
|
||||
#if defined(IMGUI_ENABLE_HARFBUZZ_SHAPING)
|
||||
// Bake a glyph identified by its FreeType glyph INDEX (not a Unicode
|
||||
// codepoint) into the atlas, cache it in the per-(src,baked) shaped-glyph
|
||||
// cache, and return it. This is the on-demand baking path used by
|
||||
// RenderShapedText for GSUB ligature glyphs (ZWJ families, regional-
|
||||
// indicator flags) whose combined glyph has no codepoint and thus cannot
|
||||
// live in ImFontBaked's codepoint-indexed tables.
|
||||
static ImFontGlyph* ImGui_ImplFreeType_BakeGlyphByID(ImFontAtlas* atlas,
|
||||
ImFontConfig* src,
|
||||
ImFontBaked* baked,
|
||||
void* loader_data_for_baked_src,
|
||||
FT_UInt glyph_index)
|
||||
{
|
||||
if (glyph_index == 0)
|
||||
return nullptr;
|
||||
ImGui_ImplFreeType_FontSrcData* bd_font_data = (ImGui_ImplFreeType_FontSrcData*)src->FontLoaderData;
|
||||
ImGui_ImplFreeType_FontSrcBakedData* bd_baked_data = (ImGui_ImplFreeType_FontSrcBakedData*)loader_data_for_baked_src;
|
||||
|
||||
// Cache lookup
|
||||
int cached_idx = bd_baked_data->ShapedGlyphsByGlyphID.GetInt((ImGuiID)glyph_index, -1);
|
||||
if (cached_idx >= 0 && cached_idx < bd_baked_data->ShapedGlyphs.Size)
|
||||
return &bd_baked_data->ShapedGlyphs[cached_idx];
|
||||
|
||||
if (atlas->Locked || (baked->OwnerFont->Flags & ImFontFlags_NoLoadGlyphs))
|
||||
return nullptr;
|
||||
|
||||
FT_Activate_Size(bd_baked_data->FtSize);
|
||||
bd_font_data->BakedLastActivated = baked;
|
||||
|
||||
// Load by glyph index directly (no FT_Get_Char_Index).
|
||||
FT_Face face = bd_font_data->FtFace;
|
||||
FT_Error error = FT_Load_Glyph(face, glyph_index, bd_font_data->LoadFlags);
|
||||
if (error != 0)
|
||||
return nullptr;
|
||||
FT_GlyphSlot slot = face->glyph;
|
||||
if (slot->format != FT_GLYPH_FORMAT_BITMAP)
|
||||
FT_Render_Glyph(slot, FT_RENDER_MODE_NORMAL);
|
||||
const FT_Bitmap* ft_bitmap = &slot->bitmap;
|
||||
if (ft_bitmap == nullptr)
|
||||
return nullptr;
|
||||
|
||||
const float rasterizer_density = src->RasterizerDensity * baked->RasterizerDensity;
|
||||
const float advance_x = (slot->advance.x / FT_SCALEFACTOR) / rasterizer_density;
|
||||
const int w = (int)ft_bitmap->width;
|
||||
const int h = (int)ft_bitmap->rows;
|
||||
|
||||
ImFontGlyph g;
|
||||
memset(&g, 0, sizeof(g));
|
||||
g.Codepoint = (ImWchar)glyph_index; // not a real codepoint; cache key only
|
||||
g.SourceIdx = 0;
|
||||
g.AdvanceX = advance_x;
|
||||
g.Visible = false;
|
||||
g.Colored = false;
|
||||
g.PackId = ImFontAtlasRectId_Invalid;
|
||||
|
||||
if (w != 0 && h != 0)
|
||||
{
|
||||
ImFontAtlasRectId pack_id = ImFontAtlasPackAddRect(atlas, w, h);
|
||||
if (pack_id == ImFontAtlasRectId_Invalid)
|
||||
return nullptr;
|
||||
ImTextureRect* r = ImFontAtlasPackGetRect(atlas, pack_id);
|
||||
atlas->Builder->TempBuffer.resize(w * h * 4);
|
||||
uint32_t* temp_buffer = (uint32_t*)atlas->Builder->TempBuffer.Data;
|
||||
ImGui_ImplFreeType_BlitGlyph(ft_bitmap, temp_buffer, w);
|
||||
|
||||
const float ref_size = baked->OwnerFont->Sources[0]->SizePixels;
|
||||
const float offsets_scale = (ref_size != 0.0f) ? (baked->Size / ref_size) : 1.0f;
|
||||
float font_off_x = ImFloor(src->GlyphOffset.x * offsets_scale + 0.5f);
|
||||
float font_off_y = ImFloor(src->GlyphOffset.y * offsets_scale + 0.5f) + baked->Ascent;
|
||||
float recip = 1.0f / rasterizer_density;
|
||||
float glyph_off_x = (float)slot->bitmap_left;
|
||||
float glyph_off_y = (float)-slot->bitmap_top;
|
||||
g.X0 = glyph_off_x * recip + font_off_x;
|
||||
g.Y0 = glyph_off_y * recip + font_off_y;
|
||||
g.X1 = (glyph_off_x + w) * recip + font_off_x;
|
||||
g.Y1 = (glyph_off_y + h) * recip + font_off_y;
|
||||
g.Visible = true;
|
||||
g.Colored = (ft_bitmap->pixel_mode == FT_PIXEL_MODE_BGRA);
|
||||
g.PackId = pack_id;
|
||||
g.U0 = r->x * atlas->TexUvScale.x;
|
||||
g.V0 = r->y * atlas->TexUvScale.y;
|
||||
g.U1 = (r->x + r->w) * atlas->TexUvScale.x;
|
||||
g.V1 = (r->y + r->h) * atlas->TexUvScale.y;
|
||||
ImFontAtlasBakedSetFontGlyphBitmap(atlas, baked, src, &g, r, (const unsigned char*)temp_buffer, ImTextureFormat_RGBA32, w * 4);
|
||||
}
|
||||
|
||||
int idx = bd_baked_data->ShapedGlyphs.Size;
|
||||
bd_baked_data->ShapedGlyphs.push_back(g);
|
||||
bd_baked_data->ShapedGlyphsByGlyphID.SetInt((ImGuiID)glyph_index, idx);
|
||||
return &bd_baked_data->ShapedGlyphs[idx];
|
||||
}
|
||||
|
||||
// Shape `text` with HarfBuzz and render it via `draw_list`, baking
|
||||
// ligature glyphs on demand by FT glyph index. This is the shaped
|
||||
// counterpart of ImFont::RenderText: it forms GSUB ligatures (ZWJ
|
||||
// families, regional-indicator flags) that the codepoint-by-codepoint
|
||||
// RenderText cannot. On-demand baking may grow the atlas texture
|
||||
// mid-frame; we mirror RenderText's cmd_count retry to handle the
|
||||
// resulting draw-cmd split.
|
||||
IMGUI_API void ImGuiFreeType::RenderShapedText(ImDrawList* draw_list,
|
||||
ImFont* font, ImFontBaked* baked,
|
||||
float size, const ImVec2& pos,
|
||||
ImU32 col, const ImVec4& clip_rect,
|
||||
const char* text_begin, const char* text_end)
|
||||
{
|
||||
begin:
|
||||
float x = IM_TRUNC(pos.x);
|
||||
float y = IM_TRUNC(pos.y);
|
||||
if (!text_end)
|
||||
text_end = text_begin + ImStrlen(text_begin);
|
||||
if (y > clip_rect.w)
|
||||
return;
|
||||
|
||||
ImFontConfig* src = font->Sources[0];
|
||||
ImGui_ImplFreeType_FontSrcData* bd_font_data = (ImGui_ImplFreeType_FontSrcData*)src->FontLoaderData;
|
||||
if (bd_font_data == nullptr || bd_font_data->HbFont == nullptr)
|
||||
return; // no HarfBuzz font (non-FreeType loader) -> caller should fall back to AddText
|
||||
|
||||
// Single-source fonts: the per-(src,baked) slot is at offset 0.
|
||||
void* loader_data_for_baked_src = (char*)baked->FontLoaderDatas;
|
||||
|
||||
const float scale = size / baked->Size;
|
||||
const ImU32 col_untinted = col | ~IM_COL32_A_MASK;
|
||||
|
||||
// Shape the whole cluster.
|
||||
hb_buffer_t* buf = hb_buffer_create();
|
||||
hb_buffer_add_utf8(buf, text_begin, (int)(text_end - text_begin), 0, -1);
|
||||
hb_buffer_guess_segment_properties(buf);
|
||||
hb_shape(bd_font_data->HbFont, buf, nullptr, 0);
|
||||
unsigned int n_glyph = 0;
|
||||
hb_glyph_info_t* infos = hb_buffer_get_glyph_infos(buf, &n_glyph);
|
||||
hb_glyph_position_t* positions = hb_buffer_get_glyph_positions(buf, &n_glyph);
|
||||
|
||||
const int vtx_count_max = (int)(n_glyph + 1) * 4;
|
||||
const int idx_count_max = (int)(n_glyph + 1) * 6;
|
||||
draw_list->PrimReserve(idx_count_max, vtx_count_max);
|
||||
ImDrawVert* vtx_write = draw_list->_VtxWritePtr;
|
||||
ImDrawIdx* idx_write = draw_list->_IdxWritePtr;
|
||||
unsigned int vtx_index = draw_list->_VtxCurrentIdx;
|
||||
const int cmd_count = draw_list->CmdBuffer.Size;
|
||||
|
||||
for (unsigned int i = 0; i < n_glyph; i++)
|
||||
{
|
||||
FT_UInt glyph_id = (FT_UInt)infos[i].codepoint; // after shaping: glyph index
|
||||
float x_advance = (float)positions[i].x_advance / 64.0f; // 26.6 -> px
|
||||
float x_offset = (float)positions[i].x_offset / 64.0f;
|
||||
|
||||
ImFontGlyph* glyph = ImGui_ImplFreeType_BakeGlyphByID(font->OwnerAtlas, src, baked, loader_data_for_baked_src, glyph_id);
|
||||
if (glyph == nullptr)
|
||||
{
|
||||
x += x_advance * scale;
|
||||
continue;
|
||||
}
|
||||
if (glyph->Visible)
|
||||
{
|
||||
float x1 = x + (glyph->X0 + x_offset) * scale;
|
||||
float x2 = x + (glyph->X1 + x_offset) * scale;
|
||||
float y1 = y + glyph->Y0 * scale;
|
||||
float y2 = y + glyph->Y1 * scale;
|
||||
if (x1 <= clip_rect.z && x2 >= clip_rect.x)
|
||||
{
|
||||
ImU32 glyph_col = glyph->Colored ? col_untinted : col;
|
||||
float u1 = glyph->U0, v1 = glyph->V0, u2 = glyph->U1, v2 = glyph->V1;
|
||||
vtx_write[0].pos.x = x1; vtx_write[0].pos.y = y1; vtx_write[0].col = glyph_col; vtx_write[0].uv.x = u1; vtx_write[0].uv.y = v1;
|
||||
vtx_write[1].pos.x = x2; vtx_write[1].pos.y = y1; vtx_write[1].col = glyph_col; vtx_write[1].uv.x = u2; vtx_write[1].uv.y = v1;
|
||||
vtx_write[2].pos.x = x2; vtx_write[2].pos.y = y2; vtx_write[2].col = glyph_col; vtx_write[2].uv.x = u2; vtx_write[2].uv.y = v2;
|
||||
vtx_write[3].pos.x = x1; vtx_write[3].pos.y = y2; vtx_write[3].col = glyph_col; vtx_write[3].uv.x = u1; vtx_write[3].uv.y = v2;
|
||||
idx_write[0] = (ImDrawIdx)(vtx_index); idx_write[1] = (ImDrawIdx)(vtx_index + 1); idx_write[2] = (ImDrawIdx)(vtx_index + 2);
|
||||
idx_write[3] = (ImDrawIdx)(vtx_index); idx_write[4] = (ImDrawIdx)(vtx_index + 2); idx_write[5] = (ImDrawIdx)(vtx_index + 3);
|
||||
vtx_write += 4; vtx_index += 4; idx_write += 6;
|
||||
}
|
||||
}
|
||||
x += x_advance * scale;
|
||||
}
|
||||
|
||||
hb_buffer_destroy(buf);
|
||||
|
||||
// On-demand baking may have split the draw cmd (texture grew). Retry.
|
||||
if (cmd_count != draw_list->CmdBuffer.Size)
|
||||
{
|
||||
IM_ASSERT(draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 1].ElemCount == 0);
|
||||
draw_list->CmdBuffer.pop_back();
|
||||
draw_list->PrimUnreserve(idx_count_max, vtx_count_max);
|
||||
draw_list->AddDrawCmd();
|
||||
goto begin;
|
||||
}
|
||||
|
||||
draw_list->VtxBuffer.Size = (int)(vtx_write - draw_list->VtxBuffer.Data);
|
||||
draw_list->IdxBuffer.Size = (int)(idx_write - draw_list->IdxBuffer.Data);
|
||||
draw_list->_VtxWritePtr = vtx_write;
|
||||
draw_list->_IdxWritePtr = idx_write;
|
||||
draw_list->_VtxCurrentIdx = (unsigned int)draw_list->VtxBuffer.Size;
|
||||
}
|
||||
#endif // IMGUI_ENABLE_HARFBUZZ_SHAPING
|
||||
|
||||
static bool ImGui_ImplFreetype_FontSrcContainsGlyph(ImFontAtlas* atlas, ImFontConfig* src, ImWchar codepoint)
|
||||
{
|
||||
IM_UNUSED(atlas);
|
||||
|
|
|
|||
|
|
@ -73,6 +73,16 @@ namespace ImGuiFreeType
|
|||
// Display UI to edit ImFontAtlas::FontLoaderFlags (shared) or ImFontConfig::FontLoaderFlags (single source)
|
||||
IMGUI_API bool DebugEditFontLoaderFlags(ImGuiFreeTypeLoaderFlags* p_font_loader_flags);
|
||||
|
||||
#if defined(IMGUI_ENABLE_HARFBUZZ_SHAPING)
|
||||
// Shape `text` with HarfBuzz and render it with `draw_list`, forming
|
||||
// GSUB ligatures (ZWJ families, regional-indicator flags) that the
|
||||
// codepoint-by-codepoint ImFont::RenderText cannot. Requires the
|
||||
// FreeType loader (uses the per-face hb_font_t). Ligature glyphs are
|
||||
// baked on demand by FT glyph index into the atlas. Opt-in API: does
|
||||
// not modify the existing RenderText hot path.
|
||||
IMGUI_API void RenderShapedText(ImDrawList* draw_list, ImFont* font, ImFontBaked* baked, float size, const ImVec2& pos, ImU32 col, const ImVec4& clip_rect, const char* text_begin, const char* text_end = nullptr);
|
||||
#endif
|
||||
|
||||
// Obsolete names (will be removed)
|
||||
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
|
||||
//IMGUI_API const ImFontBuilderIO* GetBuilderForFreeType(); // Renamed/changed in 1.92. Change 'io.Fonts->FontBuilderIO = ImGuiFreeType::GetBuilderForFreeType()' to 'io.Fonts->SetFontLoader(ImGuiFreeType::GetFontLoader())' if you need runtime selection.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue