From 5d7f6db2fc25d8d4b559be822d272c6f4e1f367d Mon Sep 17 00:00:00 2001 From: Gleb Mazovetskiy Date: Wed, 2 Oct 2019 21:10:46 +0100 Subject: [PATCH] Wrapped text rendering for TTF Does not do centering yet, but should be easy to add in the future --- CMakeLists.txt | 1 + SourceX/DiabloUI/dialogs.cpp | 6 +- SourceX/DiabloUI/text_draw.cpp | 5 +- SourceX/DiabloUI/ttf_render_wrapped.cpp | 154 ++++++++++++++++++++++++ SourceX/DiabloUI/ttf_render_wrapped.h | 14 +++ 5 files changed, 175 insertions(+), 5 deletions(-) create mode 100644 SourceX/DiabloUI/ttf_render_wrapped.cpp create mode 100644 SourceX/DiabloUI/ttf_render_wrapped.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 27a47956f..e34e3a8b0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -215,6 +215,7 @@ set(devilutionx_SRCS SourceX/DiabloUI/text_draw.cpp SourceX/DiabloUI/text.cpp SourceX/DiabloUI/title.cpp + SourceX/DiabloUI/ttf_render_wrapped.cpp SourceX/main.cpp ./Packaging/macOS/AppIcon.icns ./Packaging/resources/CharisSILB.ttf) diff --git a/SourceX/DiabloUI/dialogs.cpp b/SourceX/DiabloUI/dialogs.cpp index c483ff11a..fd0767d1b 100644 --- a/SourceX/DiabloUI/dialogs.cpp +++ b/SourceX/DiabloUI/dialogs.cpp @@ -44,7 +44,7 @@ constexpr auto DIALOG_ART_L = UiImage(&dialogArt, { 127, 100, 385, 280 }); UiItem OKCANCEL_DIALOG[] = { DIALOG_ART_S, - UiText(dialogText, { 198, 211, 240, 80 }, UIS_CENTER), + UiText(dialogText, { 200, 211, 240, 80 }, UIS_CENTER), MakeSmlButton("OK", &DialogActionOK, 200, 265), MakeSmlButton("Cancel", &DialogActionCancel, 330, 265), }; @@ -57,8 +57,8 @@ UiItem OK_DIALOG[] = { UiItem OK_DIALOG_WITH_CAPTION[] = { DIALOG_ART_L, - UiText(dialogText, SDL_Color { 255, 255, 0, 0 }, { 200, 110, 240, 20 }, UIS_CENTER), - UiText(dialogCaption, { 200, 141, 240, 80 }, UIS_CENTER), + UiText(dialogText, SDL_Color { 255, 255, 0, 0 }, { 147, 110, 345, 20 }, UIS_CENTER), + UiText(dialogCaption, { 147, 141, 345, 190 }, UIS_CENTER), MakeSmlButton("OK", &DialogActionOK, 264, 335), }; diff --git a/SourceX/DiabloUI/text_draw.cpp b/SourceX/DiabloUI/text_draw.cpp index c63400add..34f964ae3 100644 --- a/SourceX/DiabloUI/text_draw.cpp +++ b/SourceX/DiabloUI/text_draw.cpp @@ -4,6 +4,7 @@ #include "DiabloUI/fonts.h" #include "DiabloUI/text.h" #include "DiabloUI/ui_item.h" +#include "DiabloUI/ttf_render_wrapped.h" namespace dvl { @@ -28,8 +29,8 @@ void DrawTTF(const char *text, const SDL_Rect &rect, int flags, { if (*render_cache == nullptr) { *render_cache = new TtfSurfaceCache(); - (*render_cache)->text = TTF_RenderUTF8_Solid(font, text, text_color); - (*render_cache)->shadow = TTF_RenderUTF8_Solid(font, text, shadow_color); + (*render_cache)->text = RenderUTF8_Solid_Wrapped(font, text, text_color, rect.w); + (*render_cache)->shadow = RenderUTF8_Solid_Wrapped(font, text, shadow_color, rect.w); } SDL_Surface *text_surface = (*render_cache)->text; SDL_Surface *shadow_surface = (*render_cache)->shadow; diff --git a/SourceX/DiabloUI/ttf_render_wrapped.cpp b/SourceX/DiabloUI/ttf_render_wrapped.cpp new file mode 100644 index 000000000..d24bc241f --- /dev/null +++ b/SourceX/DiabloUI/ttf_render_wrapped.cpp @@ -0,0 +1,154 @@ +#include "DiabloUI/ttf_render_wrapped.h" + +#include + +namespace dvl { + +namespace { + +SDL_bool CharacterIsDelimiter(char c, const char *delimiters) +{ + while (*delimiters) { + if (c == *delimiters) + return SDL_TRUE; + ++delimiters; + } + return SDL_FALSE; +} + +} // namespace + +// Based on SDL 2.0.12 TTF_RenderUTF8_Blended_Wrapped +SDL_Surface *RenderUTF8_Solid_Wrapped(TTF_Font *font, const char *text, SDL_Color fg, Uint32 wrapLength) +{ + int width, height; + SDL_Surface *textbuf; + const int lineSpace = 2; + char *str, **strLines; + + /* Get the dimensions of the text surface */ + if ((TTF_SizeUTF8(font, text, &width, &height) < 0) || !width) { + TTF_SetError("Text has zero width"); + return nullptr; + } + + std::size_t numLines = 1; + str = nullptr; + strLines = nullptr; + if (wrapLength > 0 && *text) { + const char *wrapDelims = " \t\r\n"; + int w, h; + char *spot, *tok, *next_tok, *end; + char delim; + size_t str_len = SDL_strlen(text); + + numLines = 0; + + str = SDL_stack_alloc(char, str_len + 1); + if (str == nullptr) { + TTF_SetError("Out of memory"); + return nullptr; + } + + SDL_strlcpy(str, text, str_len + 1); + tok = str; + end = str + str_len; + do { + strLines = (char **)SDL_realloc(strLines, (numLines + 1) * sizeof(*strLines)); + if (!strLines) { + TTF_SetError("Out of memory"); + return nullptr; + } + strLines[numLines++] = tok; + + /* Look for the end of the line */ + if ((spot = SDL_strchr(tok, '\r')) != nullptr || (spot = SDL_strchr(tok, '\n')) != nullptr) { + if (*spot == '\r') { + ++spot; + } + if (*spot == '\n') { + ++spot; + } + } else { + spot = end; + } + next_tok = spot; + + /* Get the longest string that will fit in the desired space */ + for (;;) { + /* Strip trailing whitespace */ + while (spot > tok && CharacterIsDelimiter(spot[-1], wrapDelims)) { + --spot; + } + if (spot == tok) { + if (CharacterIsDelimiter(*spot, wrapDelims)) { + *spot = '\0'; + } + break; + } + delim = *spot; + *spot = '\0'; + + TTF_SizeUTF8(font, tok, &w, &h); + if ((Uint32)w <= wrapLength) { + break; + } else { + /* Back up and try again... */ + *spot = delim; + } + + while (spot > tok && !CharacterIsDelimiter(spot[-1], wrapDelims)) { + --spot; + } + if (spot > tok) { + next_tok = spot; + } + } + tok = next_tok; + } while (tok < end); + } + + if (!strLines) { + return TTF_RenderUTF8_Solid(font, text, fg); + } + + /* Create the target surface */ + textbuf = SDL_CreateRGBSurface(SDL_SWSURFACE, (numLines > 1) ? wrapLength : width, height * numLines + (lineSpace * (numLines - 1)), 8, 0, 0, 0, 0); + if (textbuf == nullptr) { + if (strLines) { + SDL_free(strLines); + SDL_stack_free(str); + } + return nullptr; + } + + /* Fill the palette with the foreground color */ + SDL_Palette *palette = textbuf->format->palette; + palette->colors[0].r = 255 - fg.r; + palette->colors[0].g = 255 - fg.g; + palette->colors[0].b = 255 - fg.b; + palette->colors[1].r = fg.r; + palette->colors[1].g = fg.g; + palette->colors[1].b = fg.b; +#ifdef USE_SDL1 + SDL_SetColorKey(textbuf, SDL_SRCCOLORKEY, 0); +#else + SDL_SetColorKey(textbuf, SDL_TRUE, 0); +#endif + + SDL_Rect dest = { 0, 0, 0, 0 }; + for (std::size_t line = 0; line < numLines; line++) { + text = strLines[line]; + SDL_Surface *tmp = TTF_RenderUTF8_Solid(font, text, fg); + dest.w = static_cast(tmp->w); + dest.h = static_cast(tmp->h); + SDL_BlitSurface(tmp, nullptr, textbuf, &dest); + dest.y += tmp->h; + SDL_FreeSurface(tmp); + } + SDL_free(strLines); + SDL_stack_free(str); + return textbuf; +} + +} // namespace dvl diff --git a/SourceX/DiabloUI/ttf_render_wrapped.h b/SourceX/DiabloUI/ttf_render_wrapped.h new file mode 100644 index 000000000..2f8b1f4bf --- /dev/null +++ b/SourceX/DiabloUI/ttf_render_wrapped.h @@ -0,0 +1,14 @@ +#pragma once + +#include "devilution.h" + +#include + +namespace dvl { + +// `TTF_RenderUTF8_Solid_Wrapped` is only available in SDL2 2.0.16 (unreleased +// as of this writing). This is a hacky alternative. +SDL_Surface *RenderUTF8_Solid_Wrapped( + TTF_Font *font, const char *text, SDL_Color fg, Uint32 wrapLength); + +} // namespace dvl