1
0
mirror of https://github.com/TerryCavanagh/VVVVVV.git synced 2024-06-01 18:43:33 +02:00
VVVVVV/desktop_version/src/Font.cpp
Misa a9f0d81804 Properly fix setfont/setrtl in between text boxes
There used to be a problem with the setfont and setrtl script commands.
Namely, if you used them in between text boxes naïvely, without any
careful thought, then the fading out text box would suddenly gain the
font of the new one. A kludge solution to this was implemented by simply
blocking the script until the existing text box faded out before
switching the font or RTL, and shipped for 2.4.0.

However, a better solution is to simply bake the font flags in to the
text box, so that way, if the level font switches, then the text box
keeps its font.

This is only for custom levels, because in the main game, the font in a
text box needs to be able to change depending on language. But it seems
like custom level translations weren't much on the roadmap, and so even
the existing hack didn't support changing the font based on translation
(even though translation of custom level cutscenes is supported). So
baking the font flags into the text box here doesn't make things any
worse.

It also makes things better, arguably, by allowing multiple text boxes
to exist on screen at once with different fonts.

Maybe in the future we'll need a flag that specifies that the font
should change depending on language if a translation in said language
exists for the text box, or something like that.

For people that want to override the fonts of every existing text box on
screen, you can specify "all" as the second parameter of setfont or
setrtl to do so.
2024-01-23 15:33:38 -08:00

1482 lines
37 KiB
C++

#include "Font.h"
#include <tinyxml2.h>
#include "Alloc.h"
#include "Constants.h"
#include "CustomLevels.h"
#include "FileSystemUtils.h"
#include "FontBidi.h"
#include "Graphics.h"
#include "GraphicsResources.h"
#include "GraphicsUtil.h"
#include "Localization.h"
#include "UTF8.h"
#include "UtilityClass.h"
#include "Vlogging.h"
#include "XMLUtils.h"
extern "C"
{
#include <c-hashmap/map.h>
}
namespace font
{
#define GLYPH_EXISTS 0x1
#define GLYPH_COLOR 0x2
struct GlyphInfo
{
uint16_t image_idx;
uint8_t advance;
uint8_t flags;
};
/* Codepoints go up to U+10FFFF, so we have 0x110 (272) pages
* of 0x1000 (4096) glyphs, allocated as needed */
#define FONT_N_PAGES 0x110
#define FONT_PAGE_SIZE 0x1000
enum FontType
{
FontType_FONT,
FontType_BUTTONS
};
struct Font
{
char name[64];
char display_name[SCREEN_WIDTH_CHARS + 1];
FontType type;
uint8_t glyph_w;
uint8_t glyph_h;
SDL_Texture* image;
GlyphInfo* glyph_page[FONT_N_PAGES];
char fallback_key[64];
uint8_t fallback_idx;
bool fallback_idx_valid;
};
struct FontContainer
{
uint8_t count;
Font* fonts;
hashmap* map_name_idx;
};
struct PrintFlags
{
uint8_t scale;
Font* font_sel;
uint8_t brightness;
bool border;
bool full_border;
bool align_cen;
bool align_right;
bool cjk_low;
bool cjk_high;
bool rtl;
bool rtl_xflip;
};
static FontContainer fonts_main = {};
static FontContainer fonts_custom = {};
static uint8_t font_idx_8x8 = 0;
uint8_t font_idx_options_n = 0;
uint8_t font_idx_options[20];
static bool font_level_is_interface = false;
bool font_idx_level_is_custom = false;
uint8_t font_idx_level = 0;
static void codepoint_split(
const uint32_t codepoint,
short* page,
short* glyph
)
{
// Splits a code point (0x10FFFF) into page (0x10F) and glyph (0xFFF)
if (codepoint > 0x10FFFF)
{
codepoint_split(0xFFFD, page, glyph);
return;
}
*page = codepoint >> 12;
*glyph = codepoint % FONT_PAGE_SIZE;
}
static GlyphInfo* get_glyphinfo(
const Font* f,
const uint32_t codepoint
)
{
short page, glyph;
codepoint_split(codepoint, &page, &glyph);
if (f->glyph_page[page] == NULL)
{
return NULL;
}
return &f->glyph_page[page][glyph];
}
static void add_glyphinfo(
Font* f,
const uint32_t codepoint,
const int image_idx
)
{
if (image_idx < 0 || image_idx > 65535)
{
return;
}
short page, glyph;
codepoint_split(codepoint, &page, &glyph);
if (f->glyph_page[page] == NULL)
{
f->glyph_page[page] = (GlyphInfo*) SDL_calloc(FONT_PAGE_SIZE, sizeof(GlyphInfo));
if (f->glyph_page[page] == NULL)
{
return;
}
}
f->glyph_page[page][glyph].image_idx = image_idx;
f->glyph_page[page][glyph].advance = f->glyph_w;
f->glyph_page[page][glyph].flags = GLYPH_EXISTS;
}
static bool glyph_is_valid(const GlyphInfo* glyph)
{
return glyph->flags & GLYPH_EXISTS;
}
static Font* fallback_for(const Font* f)
{
if (!f->fallback_idx_valid)
{
return NULL;
}
return &fonts_main.fonts[f->fallback_idx];
}
static GlyphInfo* find_glyphinfo(const Font* f, const uint32_t codepoint, const Font** f_glyph)
{
/* Get the GlyphInfo for a specific codepoint, or <?> or ? if it doesn't exist.
* f_glyph may be either set to f (the main specified font) or its fallback font, if it exists.
* As a last resort, may return NULL. */
*f_glyph = f;
GlyphInfo* glyph = get_glyphinfo(f, codepoint);
if (glyph != NULL && glyph_is_valid(glyph))
{
return glyph;
}
Font* f_fallback = fallback_for(f);
if (f_fallback != NULL && (glyph = get_glyphinfo(f_fallback, codepoint)) != NULL && glyph_is_valid(glyph))
{
*f_glyph = f_fallback;
return glyph;
}
glyph = get_glyphinfo(f, 0xFFFD);
if (glyph != NULL && glyph_is_valid(glyph))
{
return glyph;
}
glyph = get_glyphinfo(f, '?');
if (glyph != NULL && glyph_is_valid(glyph))
{
return glyph;
}
return NULL;
}
static int get_advance_ff(const Font* f, const Font* f_glyph, const GlyphInfo* glyph)
{
/* Internal function - get the correct advance after we have
* determined whether the glyph is from the fallback font or not. */
if (glyph == NULL)
{
return f->glyph_w;
}
/* If the glyph is a fallback glyph, center it relative to the main font
* instead of trusting the fallback's width */
if (f_glyph != f)
{
return f->glyph_w;
}
return glyph->advance;
}
int get_advance(const Font* f, const uint32_t codepoint)
{
// Get the width of a single character in a font
if (f == NULL)
{
return 8;
}
const Font* f_glyph;
GlyphInfo* glyph = find_glyphinfo(f, codepoint, &f_glyph);
return get_advance_ff(f, f_glyph, glyph);
}
static bool decode_xml_range(tinyxml2::XMLElement* elem, unsigned* start, unsigned* end)
{
// We do support hexadecimal start/end like "0x10FFFF"
if (elem->QueryUnsignedAttribute("start", start) != tinyxml2::XML_SUCCESS
|| elem->QueryUnsignedAttribute("end", end) != tinyxml2::XML_SUCCESS
|| *end < *start || *start > 0x10FFFF
)
{
return false;
}
*end = SDL_min(*end, 0x10FFFF);
return true;
}
static uint8_t load_font(FontContainer* container, const char* name)
{
if (container->count >= 254)
{
return 0;
}
Font* new_fonts = (Font*) SDL_realloc(container->fonts, sizeof(Font)*(container->count+1));
if (new_fonts == NULL)
{
return 0;
}
container->fonts = new_fonts;
uint8_t f_idx = container->count++;
Font* f = &container->fonts[f_idx];
vlog_info("Loading font \"%s\"...", name);
char name_png[256];
char name_txt[256];
char name_xml[256];
SDL_snprintf(name_png, sizeof(name_png), "graphics/%s.png", name);
SDL_snprintf(name_txt, sizeof(name_txt), "graphics/%s.txt", name);
SDL_snprintf(name_xml, sizeof(name_xml), "graphics/%s.fontmeta", name);
SDL_strlcpy(f->name, name, sizeof(f->name));
SDL_strlcpy(f->display_name, name, sizeof(f->display_name));
f->type = FontType_FONT;
f->glyph_w = 8;
f->glyph_h = 8;
f->fallback_key[0] = '\0';
f->fallback_idx_valid = false;
bool white_teeth = false;
tinyxml2::XMLDocument doc;
tinyxml2::XMLHandle hDoc(&doc);
tinyxml2::XMLElement* pElem;
bool xml_loaded = false;
if (FILESYSTEM_areAssetsInSameRealDir(name_png, name_xml)
&& FILESYSTEM_loadAssetTiXml2Document(name_xml, doc)
)
{
xml_loaded = true;
hDoc = hDoc.FirstChildElement("font_metadata");
if ((pElem = hDoc.FirstChildElement("display_name").ToElement()) != NULL)
{
SDL_strlcpy(f->display_name, pElem->GetText(), sizeof(f->display_name));
}
if ((pElem = hDoc.FirstChildElement("type").ToElement()) != NULL)
{
if (SDL_strcmp(pElem->GetText(), "buttons") == 0)
{
f->type = FontType_BUTTONS;
}
}
if ((pElem = hDoc.FirstChildElement("width").ToElement()) != NULL)
{
f->glyph_w = help.Int(pElem->GetText());
}
if ((pElem = hDoc.FirstChildElement("height").ToElement()) != NULL)
{
f->glyph_h = help.Int(pElem->GetText());
}
if ((pElem = hDoc.FirstChildElement("white_teeth").ToElement()) != NULL)
{
// If 1, we don't need to whiten the entire font (like in old versions)
white_teeth = help.Int(pElem->GetText());
}
if ((pElem = hDoc.FirstChildElement("fallback").ToElement()) != NULL)
{
SDL_strlcpy(f->fallback_key, pElem->GetText(), sizeof(f->fallback_key));
}
}
f->image = LoadImage(name_png, white_teeth ? TEX_COLOR : TEX_WHITE);
SDL_zeroa(f->glyph_page);
if (f->image == NULL)
{
return f_idx;
}
/* We may have a 2.3-style font.txt with all the characters.
* font.txt takes priority over <chars> in the XML.
* If neither exist, it's just ASCII. */
bool charset_loaded = false;
bool special_loaded = false;
unsigned char* charmap = NULL;
size_t length;
if (FILESYSTEM_areAssetsInSameRealDir(name_png, name_txt))
{
/* The .txt can contain null bytes, but it's still null-terminated - it protects
* against incomplete sequences getting the UTF-8 decoder to read out of bounds. */
FILESYSTEM_loadAssetToMemory(name_txt, &charmap, &length);
}
if (charmap != NULL)
{
// We have a .txt! It's an obsolete system, but it takes priority if the file exists.
const char* current = (char*) charmap;
const char* end = (char*) charmap + length;
int pos = 0;
while (current < end)
{
uint32_t codepoint = UTF8_next(&current);
add_glyphinfo(f, codepoint, pos);
++pos;
}
VVV_free(charmap);
charset_loaded = true;
}
if (xml_loaded && !charset_loaded && (pElem = hDoc.FirstChildElement("chars").ToElement()) != NULL)
{
// <chars> in the XML is the preferred system.
int pos = 0;
tinyxml2::XMLElement* subElem;
FOR_EACH_XML_SUB_ELEMENT(pElem, subElem)
{
EXPECT_ELEM(subElem, "range");
unsigned start, end;
if (!decode_xml_range(subElem, &start, &end))
{
continue;
}
for (uint32_t codepoint = start; codepoint <= end; codepoint++)
{
add_glyphinfo(f, codepoint, pos);
++pos;
}
}
charset_loaded = true;
}
if (!charset_loaded)
{
/* If we don't have font.txt and no <chars> tag either,
* this font is 2.2-and-below-style plain ASCII.
* Or well... 2.3 interpreted these as
* "all unicode from 0 to however much is in the image"... */
SDL_Surface* temp_surface = LoadImageSurface(name_png);
if (temp_surface != NULL)
{
const uint32_t chars_per_line = temp_surface->w / f->glyph_w;
const uint32_t max_codepoint = (temp_surface->h / f->glyph_h) * chars_per_line;
for (uint32_t codepoint = 0x00; codepoint < max_codepoint; codepoint++)
{
if (codepoint > 0x7F)
{
/* Only include characters with actual pixels...
* If the font.png is too big (normally it is) we _want_ question marks. */
const int glyph_x = (codepoint % chars_per_line) * f->glyph_w;
const int glyph_y = (codepoint / chars_per_line) * f->glyph_h;
bool found_pixel = false;
for (int pixel_y = 0; pixel_y < f->glyph_h; pixel_y++)
{
for (int pixel_x = 0; pixel_x < f->glyph_w; pixel_x++)
{
if (ReadPixel(temp_surface, glyph_x+pixel_x, glyph_y+pixel_y).a > 0)
{
found_pixel = true;
goto no_more_pixels;
}
}
}
no_more_pixels:
if (!found_pixel)
{
// Do not add it
continue;
}
}
add_glyphinfo(f, codepoint, codepoint);
}
VVV_freefunc(SDL_FreeSurface, temp_surface);
}
}
if (xml_loaded && (pElem = hDoc.FirstChildElement("special").ToElement()) != NULL)
{
tinyxml2::XMLElement* subElem;
FOR_EACH_XML_SUB_ELEMENT(pElem, subElem)
{
EXPECT_ELEM(subElem, "range");
unsigned start, end;
if (!decode_xml_range(subElem, &start, &end))
{
continue;
}
int advance = subElem->IntAttribute("advance", -1);
int color = subElem->IntAttribute("color", -1);
for (uint32_t codepoint = start; codepoint <= end; codepoint++)
{
GlyphInfo* glyph = get_glyphinfo(f, codepoint);
if (glyph == NULL)
{
continue;
}
if (advance >= 0 && advance < 256)
{
glyph->advance = advance;
}
if (color == 0)
{
glyph->flags &= ~GLYPH_COLOR;
}
else if (color == 1)
{
glyph->flags |= GLYPH_COLOR;
}
}
}
special_loaded = true;
}
if (!special_loaded && f->glyph_w == 8 && f->glyph_h == 8)
{
/* If we don't have <special>, and the font is 8x8,
* 0x00-0x1F will be less wide because that's how it has always been. */
for (uint32_t codepoint = 0x00; codepoint < 0x20; codepoint++)
{
GlyphInfo* glyph = get_glyphinfo(f, codepoint);
if (glyph != NULL)
{
glyph->advance = 6;
}
}
}
return f_idx;
}
static bool find_font_by_name(FontContainer* container, const char* name, uint8_t* idx)
{
// Returns true if font found (and idx is set), false if not found
if (container->map_name_idx == NULL)
{
// No fonts yet...
return false;
}
uintptr_t i;
if (hashmap_get(container->map_name_idx, name, SDL_strlen(name), &i))
{
*idx = i;
return true;
}
return false;
}
bool find_main_font_by_name(const char* name, uint8_t* idx)
{
return find_font_by_name(&fonts_main, name, idx);
}
uint8_t get_font_idx_8x8(void)
{
return font_idx_8x8;
}
bool level_font_is_main_idx(const uint8_t idx)
{
return !font_idx_level_is_custom && font_idx_level == idx;
}
void set_level_font(const char* name)
{
/* Apply the choice for a certain level-specific font.
* This function is for custom levels. */
font_level_is_interface = false;
if (find_font_by_name(&fonts_custom, name, &font_idx_level))
{
font_idx_level_is_custom = true;
}
else
{
font_idx_level_is_custom = false;
if (!find_font_by_name(&fonts_main, name, &font_idx_level))
{
if (SDL_strcmp(name, "font") != 0)
{
set_level_font("font");
}
else
{
font_idx_level = font_idx_8x8;
}
}
}
cl.rtl = SDL_strcmp(name, "font_ar") == 0; // FIXME: make different menu options for choosing LTR/RTL of the same font
}
void set_level_font_interface(void)
{
/* Set the level font equal to the interface font.
* This function is for the main game. */
font_level_is_interface = true;
}
void set_level_font_new(void)
{
/* Set the level font to the default font for new levels.
* This function is for starting the editor. */
font_level_is_interface = false;
font_idx_level_is_custom = false;
if (loc::new_level_font == "")
{
/* Just take the language's font
* (Japanese VVVVVV can make Japanese levels by default, etc) */
font_idx_level = loc::get_langmeta()->font_idx;
}
else
{
/* If the user has changed the font (wants to make levels
* for a different userbase) then remember that choice. */
if (!find_main_font_by_name(loc::new_level_font.c_str(), &font_idx_level))
{
font_idx_level = font_idx_8x8;
}
}
cl.level_font_name = get_main_font_name(font_idx_level);
cl.rtl = cl.level_font_name == "font_ar"; // FIXME: make different menu options for choosing LTR/RTL of the same font
}
static void fill_map_name_idx(FontContainer* container)
{
/* Initialize the name->idx hashmap for the fonts in this container.
* This should only be done once, after all the fonts are added. */
container->map_name_idx = hashmap_create();
for (uint8_t i = 0; i < container->count; i++)
{
Font* f = &container->fonts[i];
hashmap_set(container->map_name_idx, f->name, SDL_strlen(f->name), i);
}
}
static void set_fallbacks(FontContainer* container)
{
/* Initialize the value of fallback_idx for all fonts in this container.
* Only main fonts can be fallback fonts. */
for (uint8_t i = 0; i < container->count; i++)
{
Font* f = &container->fonts[i];
if (find_main_font_by_name(f->fallback_key, &f->fallback_idx))
{
f->fallback_idx_valid = true;
}
}
}
static void load_font_filename(bool is_custom, const char* filename)
{
// Load font.png, and everything that matches *.fontmeta (but not font.fontmeta)
size_t expected_ext_start;
bool is_fontpng = SDL_strcmp(filename, "font.png") == 0;
if (is_fontpng)
{
expected_ext_start = SDL_strlen(filename)-4;
}
else
{
expected_ext_start = SDL_strlen(filename)-9;
}
if (is_fontpng || (endsWith(filename, ".fontmeta") && SDL_strcmp(filename, "font.fontmeta") != 0))
{
char font_name[64];
SDL_strlcpy(font_name, filename, sizeof(font_name));
font_name[SDL_min(63, expected_ext_start)] = '\0';
uint8_t f_idx = load_font(is_custom ? &fonts_custom : &fonts_main, font_name);
if (is_fontpng && !is_custom)
{
font_idx_8x8 = f_idx;
}
}
}
void load_main(void)
{
// Load all global fonts
EnumHandle handle = {};
const char* item;
while ((item = FILESYSTEM_enumerate("graphics", &handle)) != NULL)
{
load_font_filename(false, item);
}
FILESYSTEM_freeEnumerate(&handle);
font_idx_level = font_idx_8x8;
fill_map_name_idx(&fonts_main);
set_fallbacks(&fonts_main);
// Initialize the font menu options, 8x8 font first
font_idx_options[0] = font_idx_8x8;
font_idx_options_n = 1;
for (uint8_t i = 0; i < fonts_main.count; i++)
{
if (i == font_idx_8x8)
{
continue;
}
if (fonts_main.fonts[i].type != FontType_FONT)
{
continue;
}
font_idx_options[font_idx_options_n++] = i;
if (font_idx_options_n >= sizeof(font_idx_options))
{
break;
}
}
}
void load_custom(const char* name)
{
// Load all custom (level-specific assets) fonts
unload_custom();
EnumHandle handle = {};
const char* item;
while ((item = FILESYSTEM_enumerateAssets("graphics", &handle)) != NULL)
{
load_font_filename(true, item);
}
FILESYSTEM_freeEnumerate(&handle);
fill_map_name_idx(&fonts_custom);
set_fallbacks(&fonts_custom);
set_level_font(name);
}
void unload_font(Font* f)
{
VVV_freefunc(SDL_DestroyTexture, f->image);
for (int i = 0; i < FONT_N_PAGES; i++)
{
VVV_free(f->glyph_page[i]);
}
}
void unload_font_container(FontContainer* container)
{
VVV_freefunc(hashmap_free, container->map_name_idx);
for (uint8_t i = 0; i < container->count; i++)
{
unload_font(&container->fonts[i]);
}
VVV_free(container->fonts);
container->fonts = NULL;
container->count = 0;
}
void unload_custom(void)
{
// Unload all custom fonts
unload_font_container(&fonts_custom);
}
void destroy(void)
{
// Unload all fonts (main and custom) for exiting
unload_custom();
unload_font_container(&fonts_main);
}
static Font* container_get(FontContainer* container, uint8_t idx)
{
/* Get a certain font from the given container (with bounds checking).
* Does its best to return at least something,
* but if there are no fonts whatsoever, can return NULL. */
if (idx < container->count)
{
return &container->fonts[idx];
}
if (font_idx_8x8 < fonts_main.count)
{
return &fonts_main.fonts[font_idx_8x8];
}
if (fonts_main.count > 0)
{
return &fonts_main.fonts[0];
}
if (fonts_custom.count > 0)
{
return &fonts_custom.fonts[0];
}
return NULL;
}
static Font* fontsel_to_font(int sel, bool* rtl, bool custom)
{
/* Take font selection integer (0-31) and turn it into the correct Font
* 0: PR_FONT_INTERFACE - use interface font
* 1: PR_FONT_LEVEL - use level font
* 2: PR_FONT_8X8 - use 8x8 font no matter what
* 3-31: - use font index 0-28 (depending on custom, from
* PR_FONT_IDX_IS_CUSTOM)
*
* rtl will be set depending on whether we're requesting the interface
* font (take it from the lang attributes) or level font (take it from
* the level attributes), or false otherwise. */
*rtl = false;
if (sel < 0)
{
// Shouldn't happen but better safe than sorry...
return NULL;
}
switch (sel)
{
case 1:
if (!font_level_is_interface)
{
*rtl = cl.rtl;
if (font_idx_level_is_custom)
{
return container_get(&fonts_custom, font_idx_level);
}
else
{
return container_get(&fonts_main, font_idx_level);
}
}
SDL_FALLTHROUGH;
case 0:
*rtl = loc::get_langmeta()->rtl;
return container_get(&fonts_main, loc::get_langmeta()->font_idx);
case 2:
return container_get(&fonts_main, font_idx_8x8);
}
if (custom)
{
return container_get(&fonts_custom, sel-3);
}
return container_get(&fonts_main, sel-3);
}
#define FLAG_PART(start, count) ((flags >> start) % (1 << count))
static PrintFlags decode_print_flags(uint32_t flags)
{
PrintFlags pf;
pf.scale = FLAG_PART(0, 3) + 1;
pf.font_sel = fontsel_to_font(
FLAG_PART(3, 5), &pf.rtl, flags & PR_FONT_IDX_IS_CUSTOM
);
if (flags & PR_RTL_FORCE)
{
pf.rtl = true;
}
pf.brightness = ~FLAG_PART(8, 8) & 0xff;
pf.border = flags & PR_BOR;
pf.full_border = flags & PR_FULLBOR;
pf.align_cen = flags & PR_CEN;
pf.align_right = flags & PR_RIGHT;
pf.cjk_low = flags & PR_CJK_LOW;
pf.cjk_high = flags & PR_CJK_HIGH;
pf.rtl_xflip = flags & PR_RTL_XFLIP;
if (pf.full_border)
{
pf.border = false;
}
return pf;
}
#undef FLAG_PART
static bool next_wrap(
Font* f,
size_t* start,
size_t* len,
const char* str,
const int maxwidth
) {
/* Get information about the current line in wordwrapped text,
* given this line starts at str[*start].
* *start is updated to the start of the next line. */
size_t idx = 0;
size_t lenfromlastspace = 0;
size_t lastspace = 0;
int linewidth = 0;
*len = 0;
if (str[idx] == '\0')
{
return false;
}
while (true)
{
uint8_t codepoint_nbytes;
uint32_t codepoint = UTF8_peek_next(&str[idx], &codepoint_nbytes);
switch (codepoint)
{
case ' ':
if (loc::get_langmeta()->autowordwrap)
{
lenfromlastspace = idx;
lastspace = *start;
}
break;
case '\n':
case '|':
*start += 1;
SDL_FALLTHROUGH;
case '\0':
return true;
}
linewidth += get_advance(f, codepoint);
if (linewidth > maxwidth)
{
if (lenfromlastspace != 0)
{
*len = lenfromlastspace;
*start = lastspace + 1;
}
if (idx == 0)
{
// Oops, we're stuck at a single character
*len = 1;
*start += 1;
}
return true;
}
idx += codepoint_nbytes;
*start += codepoint_nbytes;
*len += codepoint_nbytes;
}
}
static bool next_wrap_buf(
Font* f,
char buffer[],
const size_t buffer_size,
size_t* start,
const char* str,
const int maxwidth
) {
/* Get each line of wordwrapped text, writing one line at a time to a buffer.
* Call as follows:
*
* char buf[256];
* size_t start = 0;
* while (next_wrap_buf(font, buf, sizeof(buf), &start, "String to wordwrap", 320))
* {
* // buf contains a line of text
* }
*/
size_t len = 0;
const size_t prev_start = *start;
const bool retval = next_wrap(f, start, &len, &str[*start], maxwidth);
if (retval)
{
/* Like next_split_s(), don't use SDL_strlcpy() here. */
const size_t length = SDL_min(buffer_size - 1, len);
SDL_memcpy(buffer, &str[prev_start], length);
buffer[length] = '\0';
}
return retval;
}
std::string string_wordwrap(const uint32_t flags, const std::string& s, int maxwidth, short *lines /*= NULL*/)
{
/* Return a string wordwrapped to a maximum limit by adding newlines.
* CJK will need to have autowordwrap disabled and have manually inserted newlines. */
if (lines != NULL)
{
*lines = 1;
}
PrintFlags pf = decode_print_flags(flags);
if (pf.font_sel == NULL)
{
return s;
}
const char* orig = s.c_str();
std::string result;
size_t start = 0;
bool first = true;
while (true)
{
size_t len = 0;
const char* part = &orig[start];
const bool retval = next_wrap(pf.font_sel, &start, &len, part, maxwidth);
if (!retval)
{
return result;
}
if (first)
{
first = false;
}
else
{
result.push_back('\n');
if (lines != NULL)
{
(*lines)++;
}
}
result.append(part, len);
}
}
std::string string_wordwrap_balanced(const uint32_t flags, const std::string& s, int maxwidth)
{
/* Return a string wordwrapped to a limit of maxwidth by adding newlines.
* Try to fill the lines as far as possible, and return result where lines are most filled.
* Goal is to have all lines in textboxes be about as long and to avoid wrapping just one word to a new line.
* CJK will need to have autowordwrap disabled and have manually inserted newlines. */
if (!loc::get_langmeta()->autowordwrap)
{
return s;
}
short lines;
string_wordwrap(flags, s, maxwidth, &lines);
int bestwidth = maxwidth;
if (lines > 1)
{
for (int curlimit = maxwidth; curlimit > 1; curlimit -= 8)
{
short try_lines;
string_wordwrap(flags, s, curlimit, &try_lines);
if (try_lines > lines)
{
bestwidth = curlimit + 8;
break;
}
}
}
return string_wordwrap(flags, s, bestwidth);
}
std::string string_unwordwrap(const std::string& s)
{
/* Takes a string wordwrapped by newlines, and turns it into a single line, undoing the wrapping.
* Also trims any leading/trailing whitespace and collapses multiple spaces into one (to undo manual centering)
* Only applied to English, so langmeta.autowordwrap isn't used here (it'd break looking up strings) */
std::string result;
result.reserve(s.length());
bool latest_was_space = true; // last character was a space (or the beginning, don't want leading whitespace)
int consecutive_newlines = 0; // number of newlines currently encountered in a row (multiple newlines should stay!)
const char* str = s.c_str();
uint32_t ch;
while ((ch = UTF8_next(&str)))
{
if (ch == '\n')
{
if (consecutive_newlines == 0)
{
ch = ' ';
}
else if (consecutive_newlines == 1)
{
if (!result.empty())
{
// The last character was already a newline, so change it back from the space we thought it should have become.
result[result.size()-1] = '\n';
}
else
{
// The string starts with two or more newlines, in this case we didn't add the first one at all.
result.append("\n\n");
}
}
consecutive_newlines++;
}
else
{
consecutive_newlines = 0;
}
if (ch != ' ' || !latest_was_space)
{
result.append(UTF8_encode(ch).bytes);
}
latest_was_space = (ch == ' ' || ch == '\n');
}
// We could have one trailing space
if (!result.empty() && result[result.size()-1] == ' ')
{
result.erase(result.end()-1);
}
return result;
}
static int print_char(
const Font* f,
const uint32_t codepoint,
int x,
int y,
const int scale,
uint8_t r,
uint8_t g,
uint8_t b,
const uint8_t brightness
)
{
/* Draws the glyph for a codepoint at x,y.
* Returns the amount of pixels to advance the cursor. */
if (is_directional_character(codepoint) || is_joiner(codepoint))
{
// Some characters should be completely invisible
return 0;
}
const Font* f_glyph;
GlyphInfo* glyph = find_glyphinfo(f, codepoint, &f_glyph);
if (glyph == NULL)
{
return f->glyph_w * scale;
}
if (glyph->flags & GLYPH_COLOR && (r | g | b) != 0)
{
r = g = b = brightness;
}
else if (brightness < 255)
{
float bri_factor = brightness / (float) 255;
r *= bri_factor;
g *= bri_factor;
b *= bri_factor;
}
// If the glyph is a fallback glyph, center it
if (f_glyph != f)
{
x += (f->glyph_w - f_glyph->glyph_w) / 2;
y += (f->glyph_h - f_glyph->glyph_h) / 2;
}
graphics.draw_grid_tile(
f_glyph->image,
glyph->image_idx,
x,
y,
f_glyph->glyph_w,
f_glyph->glyph_h,
r, g, b,
scale,
scale * (graphics.flipmode ? -1 : 1)
);
return get_advance_ff(f, f_glyph, glyph) * scale;
}
const char* get_main_font_name(uint8_t idx)
{
Font* f = container_get(&fonts_main, idx);
if (f == NULL)
{
return "";
}
return f->name;
}
const char* get_main_font_display_name(uint8_t idx)
{
Font* f = container_get(&fonts_main, idx);
if (f == NULL)
{
return "";
}
if (idx == font_idx_8x8)
{
// Deciding the name for the 8x8 font was harder than I'd like to admit.
if (loc::lang == "en" || loc::get_langmeta()->font_idx != font_idx_8x8)
{
// If you use English, or a CJK language: "english/..."
SDL_strlcpy(
f->display_name,
"english/…",
sizeof(f->display_name)
);
}
else
{
// If you use another, e.g. German: "english/deutsch/..."
SDL_snprintf(
f->display_name, sizeof(f->display_name),
"english/%s/…",
loc::get_langmeta()->nativename.c_str()
);
}
}
return f->display_name;
}
const char* get_level_font_display_name(void)
{
if (font_idx_level_is_custom)
{
Font* f = container_get(&fonts_custom, font_idx_level);
if (f == NULL)
{
return "";
}
return f->display_name;
}
return get_main_font_display_name(font_idx_level);
}
bool glyph_dimensions(uint32_t flags, uint8_t* glyph_w, uint8_t* glyph_h)
{
/* Gets the dimensions (glyph_w and glyph_h) of a certain font.
* Returns true if the font is valid (glyph_w and/or glyph_h were written to if not NULL), false if not. */
PrintFlags pf = decode_print_flags(flags);
if (pf.font_sel == NULL)
{
return false;
}
if (glyph_w != NULL)
{
*glyph_w = pf.font_sel->glyph_w;
}
if (glyph_h != NULL)
{
*glyph_h = pf.font_sel->glyph_h;
}
return true;
}
int len(const uint32_t flags, const char* text)
{
PrintFlags pf = decode_print_flags(flags);
if (bidi_should_transform(pf.rtl, text))
{
text = bidi_transform(pf.rtl, text);
}
int text_len = 0;
uint32_t codepoint;
while ((codepoint = UTF8_next(&text)))
{
if (!is_directional_character(codepoint) && !is_joiner(codepoint))
{
text_len += get_advance(pf.font_sel, codepoint);
}
}
return text_len * pf.scale;
}
int height(const uint32_t flags)
{
PrintFlags pf = decode_print_flags(flags);
if (pf.font_sel == NULL)
{
return 8;
}
return pf.font_sel->glyph_h * pf.scale;
}
bool is_rtl(const uint32_t flags)
{
PrintFlags pf = decode_print_flags(flags);
return pf.rtl;
}
void print(
const uint32_t flags,
int x,
int y,
const char* text,
const uint8_t r,
const uint8_t g,
const uint8_t b
)
{
PrintFlags pf = decode_print_flags(flags);
if (pf.font_sel == NULL)
{
return;
}
if (pf.rtl && pf.rtl_xflip && (!pf.align_cen || x != -1))
{
x = SCREEN_WIDTH_PIXELS - x;
pf.align_right = !pf.align_right;
}
if (pf.align_cen || pf.align_right)
{
const int textlen = len(flags, text);
if (pf.align_cen)
{
if (x == -1)
{
x = SCREEN_WIDTH_PIXELS / 2;
}
x -= textlen/2;
if (!pf.rtl)
{
x = SDL_max(x, 0);
}
else
{
x = SDL_min(x, SCREEN_WIDTH_PIXELS - textlen);
}
}
else
{
x -= textlen;
}
}
if (pf.border && !graphics.notextoutline)
{
static const int offsets[4][2] = {{0,-1}, {-1,0}, {1,0}, {0,1}};
for (int offset = 0; offset < 4; offset++)
{
print(
flags & ~PR_BOR & ~PR_CEN & ~PR_RIGHT & ~PR_RTL_XFLIP,
x + offsets[offset][0]*pf.scale,
y + offsets[offset][1]*pf.scale,
text,
0, 0, 0
);
}
}
if (pf.full_border)
{
static const int offsets[8][2] = { {0,-1}, {-1,0}, {1,0}, {0,1}, {-1,-1}, {1,1}, {-1,1}, {1,-1} };
for (int offset = 0; offset < 8; offset++)
{
print(
flags & ~PR_FULLBOR & ~PR_CEN & ~PR_RIGHT & ~PR_RTL_XFLIP,
x + offsets[offset][0] * pf.scale,
y + offsets[offset][1] * pf.scale,
text,
0, 0, 0
);
}
}
int h_diff_8 = (pf.font_sel->glyph_h-8)*pf.scale;
if (h_diff_8 < 0)
{
/* If the font is less high than 8,
* just center it (lower on screen) */
y -= h_diff_8/2;
}
else if (pf.cjk_high)
{
y -= h_diff_8;
}
else if (!pf.cjk_low)
{
y -= h_diff_8/2;
}
if (bidi_should_transform(pf.rtl, text))
{
text = bidi_transform(pf.rtl, text);
}
int position = 0;
uint32_t codepoint;
while ((codepoint = UTF8_next(&text)))
{
position += font::print_char(
pf.font_sel,
codepoint,
x + position,
y,
pf.scale,
r,
g,
b,
pf.brightness
);
}
}
void print(
const uint32_t flags,
int x,
int y,
const std::string& text,
const uint8_t r,
const uint8_t g,
const uint8_t b
)
{
// Just a std::string overload for now because it's more .c_str() to add than I'm comfortable with...
print(flags, x, y, text.c_str(), r, g, b);
}
int print_wrap(
uint32_t flags,
const int x,
int y,
const char* text,
const uint8_t r,
const uint8_t g,
const uint8_t b,
int linespacing /*= -1*/,
int maxwidth /*= -1*/
)
{
PrintFlags pf = decode_print_flags(flags);
if (pf.font_sel == NULL)
{
return y;
}
if (linespacing == -1)
{
linespacing = 10;
}
linespacing = SDL_max(linespacing, pf.font_sel->glyph_h * pf.scale);
if (maxwidth == -1)
{
maxwidth = 304;
}
if (pf.border && !graphics.notextoutline && (r|g|b) != 0)
{
print_wrap(flags, x, y, text, 0, 0, 0, linespacing, maxwidth);
flags &= ~PR_BOR;
}
if (pf.full_border && (r | g | b) != 0)
{
print_wrap(flags, x, y, text, 0, 0, 0, linespacing, maxwidth);
flags &= ~PR_FULLBOR;
}
// This could fit 64 non-BMP characters onscreen, should be plenty
char buffer[256];
size_t start = 0;
if (graphics.flipmode)
{
// Correct for the height of the resulting print.
size_t len = 0;
while (next_wrap(pf.font_sel, &start, &len, &text[start], maxwidth))
{
y += linespacing;
}
y -= linespacing;
start = 0;
}
while (next_wrap_buf(pf.font_sel, buffer, sizeof(buffer), &start, text, maxwidth))
{
print(flags, x, y, buffer, r, g, b);
if (graphics.flipmode)
{
y -= linespacing;
}
else
{
y += linespacing;
}
}
return y + linespacing;
}
} // namespace font