From c4dba46d2b745bb238f2c441ae4ac95f337f9722 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Mon, 27 Jan 2025 10:40:19 -0800
Subject: [PATCH] Improved support for right-to-left text flow
---
examples/editbox.c | 28 +++++++------
include/SDL3_ttf/SDL_ttf.h | 9 ++--
src/SDL_ttf.c | 84 ++++++++++++++++++++++++++++----------
3 files changed, 83 insertions(+), 38 deletions(-)
diff --git a/examples/editbox.c b/examples/editbox.c
index 1248ebee..f2adcbf6 100644
--- a/examples/editbox.c
+++ b/examples/editbox.c
@@ -138,7 +138,7 @@ static void DrawCompositionCursor(EditBox *edit)
SDL_FRect rect;
SDL_RectToFRect(&cursor.rect, &rect);
- if (TTF_GetFontDirection(edit->font) == TTF_DIRECTION_RTL) {
+ if ((cursor.flags & TTF_SUBSTRING_DIRECTION_MASK) == TTF_DIRECTION_RTL) {
rect.x += cursor.rect.w;
}
rect.x += edit->rect.x;
@@ -437,7 +437,7 @@ void EditBox_Draw(EditBox *edit)
if (TTF_GetTextSubString(edit->text, edit->cursor, &cursor)) {
SDL_FRect cursor_rect;
SDL_RectToFRect(&cursor.rect, &cursor_rect);
- if (TTF_GetFontDirection(edit->font) == TTF_DIRECTION_RTL) {
+ if ((cursor.flags & TTF_SUBSTRING_DIRECTION_MASK) == TTF_DIRECTION_RTL) {
cursor_rect.x += cursor.rect.w;
}
cursor_rect.x += edit->rect.x;
@@ -462,14 +462,14 @@ void EditBox_Draw(EditBox *edit)
}
}
-static int GetCursorTextIndex(TTF_Font *font, int x, const TTF_SubString *substring)
+static int GetCursorTextIndex(int x, const TTF_SubString *substring)
{
if (substring->flags & (TTF_SUBSTRING_LINE_END | TTF_SUBSTRING_TEXT_END)) {
return substring->offset;
}
bool round_down;
- if (TTF_GetFontDirection(font) == TTF_DIRECTION_RTL) {
+ if ((substring->flags & TTF_SUBSTRING_DIRECTION_MASK) == TTF_DIRECTION_RTL) {
round_down = (x > (substring->rect.x + substring->rect.w / 2));
} else {
round_down = (x < (substring->rect.x + substring->rect.w / 2));
@@ -519,7 +519,9 @@ void EditBox_MoveCursorLeft(EditBox *edit)
return;
}
- if (TTF_GetFontDirection(edit->font) == TTF_DIRECTION_RTL) {
+ TTF_SubString substring;
+ if (TTF_GetTextSubString(edit->text, edit->cursor, &substring) &&
+ (substring.flags & TTF_SUBSTRING_DIRECTION_MASK) == TTF_DIRECTION_RTL) {
MoveCursorIndex(edit, 1);
} else {
MoveCursorIndex(edit, -1);
@@ -532,7 +534,9 @@ void EditBox_MoveCursorRight(EditBox *edit)
return;
}
- if (TTF_GetFontDirection(edit->font) == TTF_DIRECTION_RTL) {
+ TTF_SubString substring;
+ if (TTF_GetTextSubString(edit->text, edit->cursor, &substring) &&
+ (substring.flags & TTF_SUBSTRING_DIRECTION_MASK) == TTF_DIRECTION_RTL) {
MoveCursorIndex(edit, -1);
} else {
MoveCursorIndex(edit, 1);
@@ -549,14 +553,14 @@ void EditBox_MoveCursorUp(EditBox *edit)
if (TTF_GetTextSubString(edit->text, edit->cursor, &substring)) {
int fontHeight = TTF_GetFontHeight(edit->font);
int x, y;
- if (TTF_GetFontDirection(edit->font) == TTF_DIRECTION_RTL) {
+ if ((substring.flags & TTF_SUBSTRING_DIRECTION_MASK) == TTF_DIRECTION_RTL) {
x = substring.rect.x + substring.rect.w - 1;
} else {
x = substring.rect.x;
}
y = substring.rect.y - fontHeight / 2;
if (TTF_GetTextSubStringForPoint(edit->text, x, y, &substring)) {
- SetCursorPosition(edit, GetCursorTextIndex(edit->font, x, &substring));
+ SetCursorPosition(edit, GetCursorTextIndex(x, &substring));
}
}
}
@@ -571,14 +575,14 @@ void EditBox_MoveCursorDown(EditBox *edit)
if (TTF_GetTextSubString(edit->text, edit->cursor, &substring)) {
int fontHeight = TTF_GetFontHeight(edit->font);
int x, y;
- if (TTF_GetFontDirection(edit->font) == TTF_DIRECTION_RTL) {
+ if ((substring.flags & TTF_SUBSTRING_DIRECTION_MASK) == TTF_DIRECTION_RTL) {
x = substring.rect.x + substring.rect.w - 1;
} else {
x = substring.rect.x;
}
y = substring.rect.y + substring.rect.h + fontHeight / 2;
if (TTF_GetTextSubStringForPoint(edit->text, x, y, &substring)) {
- SetCursorPosition(edit, GetCursorTextIndex(edit->font, x, &substring));
+ SetCursorPosition(edit, GetCursorTextIndex(x, &substring));
}
}
}
@@ -714,7 +718,7 @@ static bool HandleMouseDown(EditBox *edit, float x, float y)
return false;
}
- SetCursorPosition(edit, GetCursorTextIndex(edit->font, textX, &substring));
+ SetCursorPosition(edit, GetCursorTextIndex(textX, &substring));
edit->highlighting = true;
edit->highlight1 = edit->cursor;
edit->highlight2 = -1;
@@ -737,7 +741,7 @@ static bool HandleMouseMotion(EditBox *edit, float x, float y)
return false;
}
- SetCursorPosition(edit, GetCursorTextIndex(edit->font, textX, &substring));
+ SetCursorPosition(edit, GetCursorTextIndex(textX, &substring));
edit->highlight2 = edit->cursor;
return true;
diff --git a/include/SDL3_ttf/SDL_ttf.h b/include/SDL3_ttf/SDL_ttf.h
index d5037c02..5501b4bb 100644
--- a/include/SDL3_ttf/SDL_ttf.h
+++ b/include/SDL3_ttf/SDL_ttf.h
@@ -2338,10 +2338,11 @@ extern SDL_DECLSPEC bool SDLCALL TTF_GetTextSize(TTF_Text *text, int *w, int *h)
*/
typedef Uint32 TTF_SubStringFlags;
-#define TTF_SUBSTRING_TEXT_START 0x00000001 /**< This substring contains the beginning of the text */
-#define TTF_SUBSTRING_LINE_START 0x00000002 /**< This substring contains the beginning of line `line_index` */
-#define TTF_SUBSTRING_LINE_END 0x00000004 /**< This substring contains the end of line `line_index` */
-#define TTF_SUBSTRING_TEXT_END 0x00000008 /**< This substring contains the end of the text */
+#define TTF_SUBSTRING_DIRECTION_MASK 0x000000FF /**< The mask for the flow direction for this substring */
+#define TTF_SUBSTRING_TEXT_START 0x00000100 /**< This substring contains the beginning of the text */
+#define TTF_SUBSTRING_LINE_START 0x00000200 /**< This substring contains the beginning of line `line_index` */
+#define TTF_SUBSTRING_LINE_END 0x00000400 /**< This substring contains the end of line `line_index` */
+#define TTF_SUBSTRING_TEXT_END 0x00000800 /**< This substring contains the end of the text */
/**
* The representation of a substring within text.
diff --git a/src/SDL_ttf.c b/src/SDL_ttf.c
index 85d33d73..d4dd9c7e 100644
--- a/src/SDL_ttf.c
+++ b/src/SDL_ttf.c
@@ -1442,7 +1442,7 @@ static bool Render_Line(const render_mode_t render_mode, int subpixel, TTF_Font
#endif
}
-static bool Render_Line_TextEngine(TTF_Font *font, int xstart, int ystart, int width, int height, TTF_DrawOperation *ops, int *current_op, TTF_SubString *clusters, int *current_cluster, int cluster_offset, int line_index)
+static bool Render_Line_TextEngine(TTF_Font *font, TTF_Direction direction, int xstart, int ystart, int width, int height, TTF_DrawOperation *ops, int *current_op, TTF_SubString *clusters, int *current_cluster, int cluster_offset, int line_index)
{
int i;
int op_index = *current_op;
@@ -1525,6 +1525,28 @@ static bool Render_Line_TextEngine(TTF_Font *font, int xstart, int ystart, int w
cluster = &clusters[cluster_index++];
cluster->offset = cluster_offset + offset;
cluster->line_index = line_index;
+ if (direction == TTF_DIRECTION_INVALID) {
+ if (last_offset == -1) {
+ if (i < (font->positions->len - 1)) {
+ GlyphPosition *next = &font->positions->pos[i + 1];
+ if (offset < next->offset) {
+ cluster->flags = TTF_DIRECTION_LTR;
+ } else {
+ cluster->flags = TTF_DIRECTION_RTL;
+ }
+ } else {
+ cluster->flags = TTF_DIRECTION_INVALID;
+ }
+ } else {
+ if (offset > last_offset) {
+ cluster->flags = TTF_DIRECTION_LTR;
+ } else {
+ cluster->flags = TTF_DIRECTION_RTL;
+ }
+ }
+ } else {
+ cluster->flags = direction;
+ }
SDL_copyp(&cluster->rect, &bounds);
last_offset = offset;
@@ -3319,7 +3341,7 @@ static bool CollectGlyphsFromFont(TTF_Font *font, const char *text, size_t lengt
static bool ReplaceGlyphPositions(GlyphPositions *positions, int start, int length, GlyphPositions *replacement)
{
- int initial_offset = positions->pos[start].offset;
+ int initial_offset = SDL_min(positions->pos[start].offset, positions->pos[start + length - 1].offset);
int length_delta = (replacement->len - length);
if (length_delta != 0) {
@@ -3378,9 +3400,10 @@ static bool CollectGlyphsWithFallbacks(TTF_Font *font, const char *text, size_t
// Fill in this span with the fallback font
GlyphPositions span;
SDL_zero(span);
- int span_offset = positions->pos[start].offset;
- int span_length = pos->offset - span_offset;
- CollectGlyphsWithFallbacks(fallback->font, text + span_offset, span_length, direction, script, &span, initial_font);
+ int min_offset = SDL_min(positions->pos[start].offset, pos->offset);
+ int max_offset = SDL_max(positions->pos[start].offset, pos->offset);
+ int span_length = (max_offset - min_offset);
+ CollectGlyphsWithFallbacks(fallback->font, text + min_offset, span_length, direction, script, &span, initial_font);
if (span.len > 0) {
ReplaceGlyphPositions(positions, start, (i - start), &span);
SDL_free(span.pos);
@@ -3393,9 +3416,12 @@ static bool CollectGlyphsWithFallbacks(TTF_Font *font, const char *text, size_t
// Fill in this span with the fallback font
GlyphPositions span;
SDL_zero(span);
- int span_offset = positions->pos[start].offset;
- int span_length = (int)(length - span_offset);
- CollectGlyphsWithFallbacks(fallback->font, text + span_offset, span_length, direction, script, &span, initial_font);
+ int min_offset = SDL_min(positions->pos[start].offset, positions->pos[positions->len - 1].offset);
+ int max_offset = SDL_max(positions->pos[start].offset, positions->pos[positions->len - 1].offset);
+ const char *last_text = &text[max_offset];
+ SDL_StepUTF8(&last_text, NULL);
+ int span_length = (int)(last_text - &text[min_offset]);
+ CollectGlyphsWithFallbacks(fallback->font, text + min_offset, span_length, direction, script, &span, initial_font);
if (span.len > 0) {
ReplaceGlyphPositions(positions, start, (positions->len - start), &span);
SDL_free(span.pos);
@@ -4211,7 +4237,9 @@ static int CalculateClusterLengths(TTF_Text *text, TTF_SubString *clusters, int
cluster->rect.h = font->height;
} else {
SDL_copyp(&cluster->rect, &clusters[cluster->cluster_index - 1].rect);
- cluster->rect.x += cluster->rect.w;
+ if ((cluster->flags & TTF_SUBSTRING_DIRECTION_MASK) != TTF_DIRECTION_RTL) {
+ cluster->rect.x += cluster->rect.w;
+ }
cluster->rect.w = 0;
}
} else if (cluster->flags & TTF_SUBSTRING_TEXT_END) {
@@ -4223,7 +4251,9 @@ static int CalculateClusterLengths(TTF_Text *text, TTF_SubString *clusters, int
} else {
cluster->line_index = last->line_index;
SDL_copyp(&cluster->rect, &last->rect);
- cluster->rect.x += cluster->rect.w;
+ if ((cluster->flags & TTF_SUBSTRING_DIRECTION_MASK) != TTF_DIRECTION_RTL) {
+ cluster->rect.x += cluster->rect.w;
+ }
cluster->rect.w = 0;
}
} else {
@@ -4242,6 +4272,15 @@ static int CalculateClusterLengths(TTF_Text *text, TTF_SubString *clusters, int
return cluster_index;
}
+static TTF_SubStringFlags GetPreviousClusterDirection(TTF_SubString *clusters, int num_clusters, TTF_Direction direction)
+{
+ if (num_clusters > 1) {
+ return (clusters[num_clusters - 1].flags & TTF_SUBSTRING_DIRECTION_MASK);
+ } else {
+ return (TTF_SubStringFlags)direction;
+ }
+}
+
static bool LayoutText(TTF_Text *text)
{
TTF_Font *font = text->internal->font;
@@ -4293,7 +4332,7 @@ static bool LayoutText(TTF_Text *text)
if (strLines[i].length == 0) {
cluster = &clusters[num_clusters++];
- cluster->flags = TTF_SUBSTRING_LINE_END;
+ cluster->flags = GetPreviousClusterDirection(clusters, num_clusters - 1, direction) | TTF_SUBSTRING_LINE_END;
cluster->offset = (int)(uintptr_t)(strLines[i].text - text->text);
cluster->line_index = i;
continue;
@@ -4344,11 +4383,11 @@ static bool LayoutText(TTF_Text *text)
cluster_offset = (int)(uintptr_t)(strLines[i].text - text->text);
// Create the text drawing operations
- if (!Render_Line_TextEngine(font, xstart + xoffset, ystart, width, height, ops, &num_ops, clusters, &num_clusters, cluster_offset, i)) {
+ if (!Render_Line_TextEngine(font, direction, xstart + xoffset, ystart, width, height, ops, &num_ops, clusters, &num_clusters, cluster_offset, i)) {
goto done;
}
cluster = &clusters[num_clusters++];
- cluster->flags = TTF_SUBSTRING_LINE_END;
+ cluster->flags = GetPreviousClusterDirection(clusters, num_clusters - 1, direction) | TTF_SUBSTRING_LINE_END;
cluster->offset = (int)(uintptr_t)(strLines[i].text - text->text + strLines[i].length);
cluster->line_index = i;
@@ -4362,7 +4401,7 @@ static bool LayoutText(TTF_Text *text)
}
}
cluster = &clusters[num_clusters++];
- cluster->flags = TTF_SUBSTRING_TEXT_END;
+ cluster->flags = GetPreviousClusterDirection(clusters, num_clusters - 1, direction) | TTF_SUBSTRING_TEXT_END;
cluster->offset = (int)length;
num_clusters = CalculateClusterLengths(text, clusters, num_clusters, length, lines);
@@ -5031,7 +5070,7 @@ TTF_SubString **TTF_GetTextSubStringsForRange(TTF_Text *text, int offset, int le
SDL_copyp(substring, &substring1);
if (length == 0) {
substring->length = 0;
- if (TTF_GetTextDirection(text) != TTF_DIRECTION_RTL) {
+ if ((substring->flags & TTF_SUBSTRING_DIRECTION_MASK) != TTF_DIRECTION_RTL) {
substring->rect.x += substring->rect.w;
}
substring->rect.w = 0;
@@ -5106,8 +5145,7 @@ bool TTF_GetTextSubStringForPoint(TTF_Text *text, int x, int y, TTF_SubString *s
}
TTF_Direction direction = TTF_GetTextDirection(text);
- bool prefer_row = (direction == TTF_DIRECTION_INVALID || direction == TTF_DIRECTION_LTR || direction == TTF_DIRECTION_RTL);
- bool line_ends_right = (direction == TTF_DIRECTION_LTR);
+ bool prefer_row = (direction != TTF_DIRECTION_TTB && direction != TTF_DIRECTION_BTT);
const TTF_SubString *closest = NULL;
int closest_dist = INT_MAX;
int wrap_cost = 100;
@@ -5120,18 +5158,20 @@ bool TTF_GetTextSubStringForPoint(TTF_Text *text, int x, int y, TTF_SubString *s
if (cluster->flags & TTF_SUBSTRING_LINE_END) {
if (prefer_row && (cluster->flags & TTF_SUBSTRING_LINE_END)) {
+ bool line_ends_left = ((cluster->flags & TTF_SUBSTRING_DIRECTION_MASK) == TTF_DIRECTION_RTL);
if ((y >= cluster->rect.y && y < (cluster->rect.y + cluster->rect.h)) &&
- ((line_ends_right && x >= cluster->rect.x) ||
- (!line_ends_right && x <= cluster->rect.x))) {
+ ((!line_ends_left && x >= cluster->rect.x) ||
+ (line_ends_left && x <= cluster->rect.x))) {
closest = cluster;
break;
}
}
} else {
if (prefer_row && (cluster->flags & TTF_SUBSTRING_LINE_START)) {
+ bool line_ends_left = ((cluster->flags & TTF_SUBSTRING_DIRECTION_MASK) == TTF_DIRECTION_RTL);
if ((y >= cluster->rect.y && y < (cluster->rect.y + cluster->rect.h)) &&
- ((line_ends_right && x < cluster->rect.x) ||
- (!line_ends_right && x > cluster->rect.x))) {
+ ((!line_ends_left && x < cluster->rect.x) ||
+ (line_ends_left && x > cluster->rect.x))) {
closest = cluster;
break;
}
@@ -5390,7 +5430,7 @@ void TTF_SetFontStyle(TTF_Font *font, TTF_FontStyleFlags style)
TTF_CHECK_FONT(font,);
prev_style = font->style;
- face_style = font->face->style_flags;
+ face_style = (TTF_FontStyleFlags)font->face->style_flags;
// Don't add a style if already in the font, SDL_ttf doesn't need to handle them
if (face_style & FT_STYLE_FLAG_BOLD) {