VVVVVV/desktop_version/src/Font.cpp

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