From dc50444941241095b406dbbf7395ce146187f79e Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Wed, 2 Oct 2024 07:29:27 -0700
Subject: [PATCH] Added string offset information to TTF_Text
Also keep the text string around and drawing operations around and allow creating TTF_Text with no engine, for measurement or external rendering use.
---
include/SDL3_ttf/SDL_textengine.h | 37 ++++++----
include/SDL3_ttf/SDL_ttf.h | 6 +-
src/SDL_renderer_textengine.c | 18 +++--
src/SDL_surface_textengine.c | 10 +--
src/SDL_ttf.c | 113 +++++++++++++++++++++---------
5 files changed, 129 insertions(+), 55 deletions(-)
diff --git a/include/SDL3_ttf/SDL_textengine.h b/include/SDL3_ttf/SDL_textengine.h
index 96def874..f975e40e 100644
--- a/include/SDL3_ttf/SDL_textengine.h
+++ b/include/SDL3_ttf/SDL_textengine.h
@@ -38,14 +38,6 @@
extern "C" {
#endif
-/* Private data in TTF_Text, available to implementations */
-struct TTF_TextData
-{
- TTF_TextEngine *engine; /**< The engine used to create this text, read-only. */
- SDL_PropertiesID props; /**< Custom properties associated with this text, read-write. */
- void *textrep; /**< The implementation-specific representation of this text */
-};
-
/**
* A font atlas draw command.
*
@@ -81,6 +73,12 @@ typedef struct TTF_FillOperation
typedef struct TTF_CopyOperation
{
TTF_DrawCommand cmd; /**< TTF_DRAW_COMMAND_COPY */
+ int text_offset; /**< The offset in the text corresponding to this glyph.
+ There may be multiple glyphs with the same text offset
+ and the next text offset might be several Unicode codepoints
+ later. In this case the glyphs and codepoints are grouped
+ together and the group bounding box is the union of the dst
+ rectangles for the corresponding glyphs. */
Uint32 glyph_index; /**< The glyph index of the glyph to be drawn, can be passed to TTF_GetGlyphForIndex() */
SDL_Rect src; /**< The area within the glyph to be drawn */
SDL_Rect dst; /**< The drawing coordinates of the glyph, in pixels. The x coordinate is relative to the left side of the text area, going right, and the y coordinate is relative to the top side of the text area, going down. */
@@ -99,6 +97,22 @@ typedef union TTF_DrawOperation
TTF_CopyOperation copy;
} TTF_DrawOperation;
+
+/* Private data in TTF_Text, to assist in text measurement and layout */
+typedef struct TTF_TextLayout TTF_TextLayout;
+
+
+/* Private data in TTF_Text, available to implementations */
+struct TTF_TextData
+{
+ SDL_PropertiesID props; /**< Custom properties associated with this text, read-only. This field is created as-needed using TTF_GetTextProperties() and the properties may be then set and read normally */
+ int num_ops; /**< The number of drawing operations to render this text, read-only. */
+ TTF_DrawOperation *ops; /**< The drawing operations used to render this text, read-only. */
+ TTF_TextLayout *layout; /**< Cached layout information, read-only */
+ TTF_TextEngine *engine; /**< The engine used to create this text, read-only. */
+ void *engine_text; /**< The implementation-specific representation of this text */
+};
+
/**
* A text engine interface.
*
@@ -116,15 +130,14 @@ struct TTF_TextEngine
/* Create a text representation from draw instructions.
*
- * All fields of `text` except `internal` will already be filled out.
+ * All fields of `text` except `internal->engine_text` will already be filled out.
*
* \param userdata the userdata pointer in this interface.
* \param font the font being used.
* \param font_generation the unique ID of the font generation being used. This changes whenever the font changes size or style and needs new glyphs, and is unique across all fonts.
- * \param ops the text drawing operations
- * \param num_ops the number of text drawing operations
+ * \param text the text object being created.
*/
- bool (SDLCALL *CreateText)(void *userdata, TTF_Font *font, Uint32 font_generation, TTF_Text *text, TTF_DrawOperation *ops, int num_ops);
+ bool (SDLCALL *CreateText)(void *userdata, TTF_Font *font, Uint32 font_generation, TTF_Text *text);
/**
* Destroy a text representation.
diff --git a/include/SDL3_ttf/SDL_ttf.h b/include/SDL3_ttf/SDL_ttf.h
index 36967e84..49b15f01 100644
--- a/include/SDL3_ttf/SDL_ttf.h
+++ b/include/SDL3_ttf/SDL_ttf.h
@@ -1244,7 +1244,7 @@ typedef struct TTF_TextData TTF_TextData;
*/
typedef struct TTF_Text
{
- char *label; /**< A label that you can allocate with SDL_strdup() for debugging purposes, and will be automatically freed in TTF_DestroyText(). */
+ char *text; /**< A copy of the text used to create this text object, useful for layout and debugging. This will be freed automatically when the object is destroyed. */
int w; /**< The width of this text, in pixels, read-only. */
int h; /**< The height of this text, in pixels, read-only. */
SDL_FColor color; /**< The color of the text, read-write. You can change this anytime. */
@@ -1384,7 +1384,7 @@ extern SDL_DECLSPEC void SDLCALL TTF_DestroyRendererTextEngine(TTF_TextEngine *e
*
* This will not wrap on newline characters.
*
- * \param engine the text engine to use when creating the text object.
+ * \param engine the text engine to use when creating the text object, may be NULL.
* \param font the font to render with.
* \param text the text to use, in UTF-8 encoding.
* \param length the length of the text, in bytes, or 0 for null terminated
@@ -1410,7 +1410,7 @@ extern SDL_DECLSPEC TTF_Text * SDLCALL TTF_CreateText(TTF_TextEngine *engine, TT
*
* If wrapLength is 0, this function will only wrap on newline characters.
*
- * \param engine the text engine to use when creating the text object.
+ * \param engine the text engine to use when creating the text object, may be NULL.
* \param font the font to render with.
* \param text the text to use, in UTF-8 encoding.
* \param length the length of the text, in bytes, or 0 for null terminated
diff --git a/src/SDL_renderer_textengine.c b/src/SDL_renderer_textengine.c
index e0ff1c39..64234047 100644
--- a/src/SDL_renderer_textengine.c
+++ b/src/SDL_renderer_textengine.c
@@ -781,8 +781,10 @@ static TTF_RendererTextEngineData *CreateEngineData(SDL_Renderer *renderer)
return data;
}
-static bool SDLCALL CreateText(void *userdata, TTF_Font *font, Uint32 font_generation, TTF_Text *text, TTF_DrawOperation *ops, int num_ops)
+static bool SDLCALL CreateText(void *userdata, TTF_Font *font, Uint32 font_generation, TTF_Text *text)
{
+ int num_ops = text->internal->num_ops;
+ TTF_DrawOperation *ops;
TTF_RendererTextEngineData *enginedata = (TTF_RendererTextEngineData *)userdata;
TTF_RendererTextEngineFontData *fontdata;
TTF_RendererTextEngineTextData *data;
@@ -797,17 +799,25 @@ static bool SDLCALL CreateText(void *userdata, TTF_Font *font, Uint32 font_gener
fontdata->generation = font_generation;
}
+ // Make a sortable copy of the draw operations
+ ops = (TTF_DrawOperation *)SDL_malloc(num_ops * sizeof(*ops));
+ if (!ops) {
+ return false;
+ }
+ SDL_memcpy(ops, text->internal->ops, num_ops * sizeof(*ops));
+
data = CreateTextData(enginedata, fontdata, ops, num_ops);
+ SDL_free(ops);
if (!data) {
return false;
}
- text->internal->textrep = data;
+ text->internal->engine_text = data;
return true;
}
static void SDLCALL DestroyText(void *userdata, TTF_Text *text)
{
- TTF_RendererTextEngineTextData *data = (TTF_RendererTextEngineTextData *)text->internal->textrep;
+ TTF_RendererTextEngineTextData *data = (TTF_RendererTextEngineTextData *)text->internal->engine_text;
(void)userdata;
DestroyTextData(data);
@@ -846,7 +856,7 @@ bool TTF_DrawRendererText(TTF_Text *text, float x, float y)
}
renderer = ((TTF_RendererTextEngineData *)text->internal->engine->userdata)->renderer;
- data = (TTF_RendererTextEngineTextData *)text->internal->textrep;
+ data = (TTF_RendererTextEngineTextData *)text->internal->engine_text;
AtlasDrawSequence *sequence = data->draw_sequence;
while (sequence) {
float *position = sequence->positions;
diff --git a/src/SDL_surface_textengine.c b/src/SDL_surface_textengine.c
index df4f0212..7952cd8b 100644
--- a/src/SDL_surface_textengine.c
+++ b/src/SDL_surface_textengine.c
@@ -230,8 +230,10 @@ static TTF_SurfaceTextEngineData *CreateEngineData(void)
return data;
}
-static bool SDLCALL CreateText(void *userdata, TTF_Font *font, Uint32 font_generation, TTF_Text *text, TTF_DrawOperation *ops, int num_ops)
+static bool SDLCALL CreateText(void *userdata, TTF_Font *font, Uint32 font_generation, TTF_Text *text)
{
+ int num_ops = text->internal->num_ops;
+ const TTF_DrawOperation *ops = text->internal->ops;
TTF_SurfaceTextEngineData *enginedata = (TTF_SurfaceTextEngineData *)userdata;
TTF_SurfaceTextEngineFontData *fontdata;
TTF_SurfaceTextEngineTextData *data;
@@ -250,13 +252,13 @@ static bool SDLCALL CreateText(void *userdata, TTF_Font *font, Uint32 font_gener
if (!data) {
return false;
}
- text->internal->textrep = data;
+ text->internal->engine_text = data;
return true;
}
static void SDLCALL DestroyText(void *userdata, TTF_Text *text)
{
- TTF_SurfaceTextEngineTextData *data = (TTF_SurfaceTextEngineTextData *)text->internal->textrep;
+ TTF_SurfaceTextEngineTextData *data = (TTF_SurfaceTextEngineTextData *)text->internal->engine_text;
(void)userdata;
DestroyTextData(data);
@@ -331,7 +333,7 @@ bool TTF_DrawSurfaceText(TTF_Text *text, int x, int y, SDL_Surface *surface)
return SDL_InvalidParamError("surface");
}
- data = (TTF_SurfaceTextEngineTextData *)text->internal->textrep;
+ data = (TTF_SurfaceTextEngineTextData *)text->internal->engine_text;
if (text->color.r != data->fcolor.r ||
text->color.g != data->fcolor.g ||
diff --git a/src/SDL_ttf.c b/src/SDL_ttf.c
index 0cc00de6..b20bb2c2 100644
--- a/src/SDL_ttf.c
+++ b/src/SDL_ttf.c
@@ -216,6 +216,7 @@ typedef struct PosBuf {
FT_UInt index;
int x;
int y;
+ int offset;
} PosBuf_t;
// The structure used to hold internal font information
@@ -1409,6 +1410,7 @@ static bool Render_Line_TextEngine(TTF_Font *font, int xstart, int ystart, int w
FT_UInt idx = font->pos_buf[i].index;
int x = font->pos_buf[i].x;
int y = font->pos_buf[i].y;
+ int offset = font->pos_buf[i].offset;
c_glyph *glyph;
if (Find_GlyphByIndex(font, idx, 0, 0, 0, 0, 0, &glyph, NULL)) {
@@ -1453,6 +1455,7 @@ static bool Render_Line_TextEngine(TTF_Font *font, int xstart, int ystart, int w
op = &ops[op_index++];
op->cmd = TTF_DRAW_COMMAND_COPY;
+ op->copy.text_offset = offset;
op->copy.glyph_index = idx;
op->copy.src.x = glyph_x;
op->copy.src.y = glyph_y;
@@ -2856,6 +2859,24 @@ bool TTF_GetGlyphKerning(TTF_Font *font, Uint32 previous_ch, Uint32 ch, int *ker
return true;
}
+#if TTF_USE_HARFBUZZ
+static int CalculateUTF8Offset(const char *text, uint32_t cluster, size_t length)
+{
+ const char *end = text;
+ while (cluster > 0) {
+ Uint32 c = SDL_StepUTF8(&end, &length);
+ if (c == 0) {
+ break;
+ }
+ if (c == SDL_INVALID_UNICODE_CODEPOINT) {
+ continue;
+ }
+ --cluster;
+ }
+ return (int)(uintptr_t)(end - text);
+}
+#endif // TTF_USE_HARFBUZZ
+
static bool TTF_Size_Internal(TTF_Font *font, const char *text, size_t length, int *w, int *h, int *xstart, int *ystart, int measure_width, int *extent, int *count)
{
int x = 0;
@@ -2945,6 +2966,8 @@ static bool TTF_Size_Internal(TTF_Font *font, const char *text, size_t length, i
hb_glyph_position = hb_buffer_get_glyph_positions(hb_buffer, &glyph_count);
// Load and render each character
+ uint32_t last_cluster = 0;
+ int offset = 0;
for (g = 0; g < glyph_count; g++)
{
FT_UInt idx = hb_glyph_info[g].codepoint;
@@ -2952,12 +2975,24 @@ static bool TTF_Size_Internal(TTF_Font *font, const char *text, size_t length, i
int y_advance = hb_glyph_position[g].y_advance;
int x_offset = hb_glyph_position[g].x_offset;
int y_offset = hb_glyph_position[g].y_offset;
+
+ uint32_t cluster = hb_glyph_info[g].cluster;
+ if (cluster > last_cluster) {
+ offset += CalculateUTF8Offset(text + offset, cluster - last_cluster, length - offset);
+ } else if (cluster < last_cluster) {
+ offset = CalculateUTF8Offset(text, cluster, length);
+ }
+ last_cluster = cluster;
#else
// Load each character and sum it's bounding box
+ int offset = 0;
while (length > 0) {
+ const char *last = text;
Uint32 c = SDL_StepUTF8(&text, &length);
FT_UInt idx = get_char_index(font, c);
+ offset += (text - last);
+
if (c == UNICODE_BOM_NATIVE || c == UNICODE_BOM_SWAPPED) {
continue;
}
@@ -3022,9 +3057,10 @@ static bool TTF_Size_Internal(TTF_Font *font, const char *text, size_t length, i
pos_y = F26Dot6(font->ascent);
#endif
// Store things for Render_Line()
- font->pos_buf[font->pos_len].x = pos_x;
- font->pos_buf[font->pos_len].y = pos_y;
- font->pos_buf[font->pos_len].index = idx;
+ font->pos_buf[font->pos_len].x = pos_x;
+ font->pos_buf[font->pos_len].y = pos_y;
+ font->pos_buf[font->pos_len].index = idx;
+ font->pos_buf[font->pos_len].offset = offset;
font->pos_len += 1;
// Compute provisional global bounding box
@@ -3553,23 +3589,32 @@ typedef struct TTF_InternalText
TTF_TextData internal;
} TTF_InternalText;
-static TTF_Text *CreateText(TTF_TextEngine *engine, int width, int height)
+static TTF_Text *CreateText(TTF_TextEngine *engine, const char *text, size_t length, int width, int height, TTF_DrawOperation *ops, int num_ops)
{
TTF_InternalText *mem = (TTF_InternalText *)SDL_calloc(1, sizeof(*mem));
if (!mem) {
return NULL;
}
- TTF_Text *text = &mem->text;
- text->internal = &mem->internal;
- text->w = width;
- text->h = height;
- text->color.r = 1.0f;
- text->color.g = 1.0f;
- text->color.b = 1.0f;
- text->color.a = 1.0f;
- text->internal->engine = engine;
- return text;
+ TTF_Text *result = &mem->text;
+ result->internal = &mem->internal;
+ result->text = (char *)SDL_malloc(length + 1);
+ if (!result->text) {
+ SDL_free(mem);
+ return NULL;
+ }
+ SDL_memcpy(result->text, text, length);
+ result->text[length] = '\0';
+ result->w = width;
+ result->h = height;
+ result->color.r = 1.0f;
+ result->color.g = 1.0f;
+ result->color.b = 1.0f;
+ result->color.a = 1.0f;
+ result->internal->engine = engine;
+ result->internal->num_ops = num_ops;
+ result->internal->ops = ops;
+ return result;
}
TTF_Text *TTF_CreateText(TTF_TextEngine *engine, TTF_Font *font, const char *text, size_t length)
@@ -3580,11 +3625,10 @@ TTF_Text *TTF_CreateText(TTF_TextEngine *engine, TTF_Font *font, const char *tex
TTF_Text *result = NULL;
TTF_CHECK_INITIALIZED(NULL);
- TTF_CHECK_POINTER("engine", engine, NULL);
TTF_CHECK_POINTER("font", font, NULL);
TTF_CHECK_POINTER("text", text, NULL);
- if (engine->version < sizeof(*engine)) {
+ if (engine && engine->version < sizeof(*engine)) {
// Update this to handle older versions of this interface
SDL_SetError("Invalid engine, should be initialized with SDL_INIT_INTERFACE()");
return 0;
@@ -3628,16 +3672,17 @@ TTF_Text *TTF_CreateText(TTF_TextEngine *engine, TTF_Font *font, const char *tex
Draw_Line_TextEngine(font, width, height, 0, ystart + font->strikethrough_top_row, width, font->line_thickness, ops, &num_ops);
}
- result = CreateText(engine, width, height);
+ result = CreateText(engine, text, length, width, height, ops, num_ops);
if (!result) {
goto failure;
}
- if (!engine->CreateText(engine->userdata, font, font->generation, result, ops, num_ops)) {
- goto failure;
+ if (engine) {
+ if (!engine->CreateText(engine->userdata, font, font->generation, result)) {
+ goto failure;
+ }
}
- SDL_free(ops);
return result;
failure:
@@ -3655,14 +3700,16 @@ TTF_Text *TTF_CreateText_Wrapped(TTF_TextEngine *engine, TTF_Font *font, const c
int num_ops = 0, max_ops = 0, extra_ops = 0, additional_ops;
TTF_Text *result = NULL;
- TTF_CHECK_POINTER("engine", engine, NULL);
-
- if (engine->version < sizeof(*engine)) {
+ if (engine && engine->version < sizeof(*engine)) {
// Update this to handle older versions of this interface
SDL_SetError("Invalid engine, should be initialized with SDL_INIT_INTERFACE()");
return 0;
}
+ if (!length) {
+ length = SDL_strlen(text);
+ }
+
if (!GetWrappedLines(font, text, length, wrapLength, &strLines, &numLines, &width, &height)) {
return NULL;
}
@@ -3721,17 +3768,18 @@ TTF_Text *TTF_CreateText_Wrapped(TTF_TextEngine *engine, TTF_Font *font, const c
}
}
- result = CreateText(engine, width, height);
+ result = CreateText(engine, text, length, width, height, ops, num_ops);
if (!result) {
goto failure;
}
- if (!engine->CreateText(engine->userdata, font, font->generation, result, ops, num_ops)) {
- goto failure;
+ if (engine) {
+ if (!engine->CreateText(engine->userdata, font, font->generation, result)) {
+ goto failure;
+ }
}
SDL_free(strLines);
- SDL_free(ops);
return result;
failure:
@@ -3744,7 +3792,6 @@ TTF_Text *TTF_CreateText_Wrapped(TTF_TextEngine *engine, TTF_Font *font, const c
SDL_PropertiesID TTF_GetTextProperties(TTF_Text *text)
{
TTF_CHECK_POINTER("text", text, 0);
- TTF_CHECK_POINTER("text", text->internal, 0);
if (!text->internal->props) {
text->internal->props = SDL_CreateProperties();
@@ -3754,15 +3801,17 @@ SDL_PropertiesID TTF_GetTextProperties(TTF_Text *text)
void TTF_DestroyText(TTF_Text *text)
{
- if (!text || !text->internal) {
+ if (!text) {
return;
}
TTF_TextEngine *engine = text->internal->engine;
- engine->DestroyText(engine->userdata, text);
+ if (engine) {
+ engine->DestroyText(engine->userdata, text);
+ }
SDL_DestroyProperties(text->internal->props);
- text->internal = NULL;
- SDL_free(text->label);
+ SDL_free(text->internal->ops);
+ SDL_free(text->text);
SDL_free(text);
}