GodMode9i/arm9/source/font.cpp
2021-09-02 20:19:00 -05:00

444 lines
10 KiB
C++

#include "font.h"
#include "font_default_frf.h"
#include "tonccpy.h"
#include <algorithm>
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
u8 Font::textBuf[2][256 * 192];
bool Font::mainScreen = false;
Font *font = nullptr;
// Specifically the Arabic letters that have supported presentation forms
bool Font::isArabic(char16_t c) {
return c >= 0x0622 && c <= 0x064A;
}
bool Font::isStrongRTL(char16_t c) {
// Hebrew, Arabic, or RLM
return (c >= 0x0590 && c <= 0x05FF) || (c >= 0x0600 && c <= 0x06FF) || c == 0x200F;
}
bool Font::isWeak(char16_t c) {
return c < 'A' || (c > 'Z' && c < 'a') || (c > 'z' && c < 127);
}
bool Font::isNumber(char16_t c) {
return c >= '0' && c <= '9';
}
char16_t Font::arabicForm(char16_t current, char16_t prev, char16_t next) {
if(isArabic(current)) {
// If previous should be connected to
if((prev >= 0x626 && prev <= 0x62E && prev != 0x627 && prev != 0x629) || (prev >= 0x633 && prev <= 0x64A && prev != 0x648)) {
if(isArabic(next)) // If next is arabic, medial
return arabicPresentationForms[current - 0x622][1];
else // If not, final
return arabicPresentationForms[current - 0x622][2];
} else {
if(isArabic(next)) // If next is arabic, initial
return arabicPresentationForms[current - 0x622][0];
else // If not, isolated
return current;
}
}
return current;
}
bool Font::load(const char *path) {
FILE *file = fopen(path, "rb");
const u8 *fileBuffer = font_default_frf;
if(file) {
fseek(file, 0, SEEK_END);
size_t size = ftell(file);
fileBuffer = new u8[size];
if(!fileBuffer) {
fclose(file);
return false;
}
fseek(file, 0, SEEK_SET);
fread((void *)fileBuffer, 1, size, file);
}
const u8 *ptr = fileBuffer;
// Check header magic, then skip over
if(memcmp(ptr, "RIFF", 4) != 0) {
if(fileBuffer != font_default_frf)
delete[] fileBuffer;
return false;
}
ptr += 8;
// check for and load META section
if(memcmp(ptr, "META", 4) == 0) {
tileWidth = ptr[8];
tileHeight = ptr[9];
tonccpy(&tileCount, ptr + 10, sizeof(u16));
if(tileWidth > TILE_MAX_WIDTH || tileHeight > TILE_MAX_HEIGHT) {
if(fileBuffer != font_default_frf)
delete[] fileBuffer;
return false;
}
u32 section_size;
tonccpy(&section_size, ptr + 4, sizeof(u32));
ptr += 8 + section_size;
} else {
if(fileBuffer != font_default_frf)
delete[] fileBuffer;
return false;
}
// Character data
if(memcmp(ptr, "CDAT", 4) == 0) {
fontTiles = new u8[tileHeight * tileCount];
if(!fontTiles) {
if(fileBuffer != font_default_frf)
delete[] fileBuffer;
return false;
}
tonccpy(fontTiles, ptr + 8, tileHeight * tileCount);
u32 section_size;
tonccpy(&section_size, ptr + 4, sizeof(u32));
ptr += 8 + section_size;
} else {
if(fileBuffer != font_default_frf)
delete[] fileBuffer;
return false;
}
// character map
if(memcmp(ptr, "CMAP", 4) == 0) {
fontMap = new u16[tileCount];
if(!fontMap) {
if(fileBuffer != font_default_frf)
delete[] fileBuffer;
delete[] fontTiles;
return false;
}
tonccpy(fontMap, ptr + 8, sizeof(u16) * tileCount);
u32 section_size;
tonccpy(&section_size, ptr + 4, sizeof(u32));
ptr += 8 + section_size;
} else {
if(fileBuffer != font_default_frf)
delete[] fileBuffer;
delete[] fontTiles;
return false;
}
questionMark = getCharIndex('?');
// Copy palette to VRAM
for(uint i = 0; i < sizeof(palette) / sizeof(palette[0]); i++) {
tonccpy(BG_PALETTE + i * 0x10, palette[i], 4);
tonccpy(BG_PALETTE_SUB + i * 0x10, palette[i], 4);
}
if(fileBuffer != font_default_frf)
delete[] fileBuffer;
return true;
}
Font::Font(const char *path) {
if(!load(path)) {
load(nullptr);
}
}
Font::~Font(void) {
if(fontTiles)
delete[] fontTiles;
if(fontMap)
delete[] fontMap;
}
u16 Font::getCharIndex(char16_t c) {
// Try a binary search
int left = 0;
int right = tileCount;
while(left <= right) {
int mid = left + ((right - left) / 2);
if(fontMap[mid] == c) {
return mid;
}
if(fontMap[mid] < c) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return questionMark;
}
std::u16string Font::utf8to16(std::string_view text) {
std::u16string out;
for(uint i = 0; i < text.size();) {
char16_t c;
if(!(text[i] & 0x80)) {
c = text[i++];
} else if((text[i] & 0xE0) == 0xC0) {
c = (text[i++] & 0x1F) << 6;
c |= text[i++] & 0x3F;
} else if((text[i] & 0xF0) == 0xE0) {
c = (text[i++] & 0x0F) << 12;
c |= (text[i++] & 0x3F) << 6;
c |= text[i++] & 0x3F;
} else {
i++; // out of range or something (This only does up to U+FFFF since it goes to a U16 anyways)
}
out += c;
}
return out;
}
int Font::calcHeight(std::u16string_view text, int xPos) {
int lines = 1, chars = xPos + 1;
for(auto it = text.begin(); it != text.end(); it++) {
if(*it == '\n' || (*it == ' ' && 256 / tileWidth - chars < 10 && text.end() - it > (256 / tileWidth - chars) && *std::find(it + 1, std::min(it + (256 / tileWidth - chars), text.end()), ' ') != ' ')) {
lines++;
chars = xPos + 1;
} else if(chars > 256 / tileWidth) {
lines++;
chars = xPos + 1;
// Skip to next char if a space
if(*it == ' ')
it++;
} else {
chars++;
}
}
return lines;
}
void Font::printf(int xPos, int yPos, bool top, Alignment align, Palette palette, const char *format, ...) {
char str[0x100];
va_list va;
va_start(va, format);
vsniprintf(str, 0x100, format, va);
va_end(va);
print(xPos, yPos, top, str, align, palette);
}
ITCM_CODE void Font::print(int xPos, int yPos, bool top, std::u16string_view text, Alignment align, Palette palette, bool rtl) {
int x = xPos * tileWidth, y = yPos * tileHeight;
if(x < 0 && align != Alignment::center)
x += 256;
if(y < 0)
y += 192;
// If RTL isn't forced, check for RTL text
if(!rtl) {
for(const auto c : text) {
if(isStrongRTL(c)) {
rtl = true;
break;
}
}
}
auto ltrBegin = text.end(), ltrEnd = text.end();
// Adjust x for alignment
switch(align) {
case Alignment::left: {
break;
} case Alignment::center: {
size_t newline = text.find('\n');
while(newline != text.npos) {
print(xPos, yPos, top, text.substr(0, newline), align, palette, rtl);
text = text.substr(newline + 1);
newline = text.find('\n');
yPos++;
y += tileHeight;
}
x = ((256 - (text.length() * tileWidth)) / 2) + x;
break;
} case Alignment::right: {
size_t newline = text.find('\n');
while(newline != text.npos) {
print(xPos, yPos, top, text.substr(0, newline), Alignment::left, palette, rtl);
text = text.substr(newline + 1);
newline = text.find('\n');
yPos++;
y += tileHeight;
}
break;
}
}
if(align == Alignment::right)
x -= (text.length() - 1) * tileWidth;
// Align to grid
x -= x % tileWidth;
y -= y % tileHeight;
x += (256 % tileWidth) / 2;
y += (192 % tileHeight) / 2;
const int xStart = x;
// Loop through string and print it
for(auto it = (rtl ? text.end() - 1 : text.begin()); true; it += (rtl ? -1 : 1)) {
// If we hit the end of the string in an LTR section of an RTL
// string, it may not be done, if so jump back to printing RTL
if(it == (rtl ? text.begin() - 1 : text.end())) {
if(ltrBegin == text.end() || (ltrBegin == text.begin() && ltrEnd == text.end())) {
break;
} else {
it = ltrBegin;
ltrBegin = text.end();
rtl = true;
}
}
// If at the end of an LTR section within RTL, jump back to the RTL
if(it == ltrEnd && ltrBegin != text.end()) {
if(ltrBegin == text.begin() && (!isWeak(*ltrBegin) || isNumber(*ltrBegin)))
break;
it = ltrBegin;
ltrBegin = text.end();
rtl = true;
// If in RTL and hit a non-RTL character that's not punctuation, switch to LTR
} else if(rtl && !isStrongRTL(*it) && (!isWeak(*it) || isNumber(*it))) {
// Save where we are as the end of the LTR section
ltrEnd = it + 1;
// Go back until an RTL character or the start of the string
bool allNumbers = true;
while(!isStrongRTL(*it) && it != text.begin()) {
// Check for if the LTR section is only numbers,
// if so they won't be removed from the end
if(allNumbers && !isNumber(*it) && !isWeak(*it))
allNumbers = false;
it--;
}
// Save where we are to return to after printing the LTR section
ltrBegin = it;
// If on an RTL char right now, add one
if(isStrongRTL(*it)) {
it++;
}
// Remove all punctuation and, if the section isn't only numbers,
// numbers from the end of the LTR section
if(allNumbers) {
while(isWeak(*it) && !isNumber(*it)) {
if(it != text.begin())
ltrBegin++;
it++;
}
} else {
while(isWeak(*it)) {
if(it != text.begin())
ltrBegin++;
it++;
}
}
// But then allow all numbers directly touching the strong LTR or with 1 weak between
while((it - 1 >= text.begin() && isNumber(*(it - 1))) || (it - 2 >= text.begin() && isWeak(*(it - 1)) && isNumber(*(it - 2)))) {
if(it - 1 != text.begin())
ltrBegin--;
it--;
}
rtl = false;
}
// Line break on newline or last space within 10 chars of edge in left align
if(*it == '\n' || (*it == ' ' && align == Alignment::left && 256 - x < tileWidth * 10 && text.end() - it > (256 - x) / tileWidth && *std::find(it + 1, std::min(it + (256 - x) / tileWidth, text.end()), ' ') != ' ')) {
x = xStart;
y += tileHeight;
continue;
}
// Wrap at edge if left aligning
if(x + tileWidth > 256 && align == Alignment::left) {
x = xStart;
y += tileHeight;
// Skip to next char if a space
if(*it == ' ')
it++;
}
// Brackets are flipped in RTL
u16 index;
if(rtl) {
switch(*it) {
case '(':
index = getCharIndex(')');
break;
case ')':
index = getCharIndex('(');
break;
case '[':
index = getCharIndex(']');
break;
case ']':
index = getCharIndex('[');
break;
case '<':
index = getCharIndex('>');
break;
case '>':
index = getCharIndex('<');
break;
default:
index = getCharIndex(arabicForm(*it, *(it - 1), *(it + 1)));
break;
}
} else {
index = getCharIndex(*it);
}
// Don't draw off screen chars
if(x >= 0 && x + tileWidth <= 256 && y >= 0 && y + tileHeight <= 192) {
u8 *dst = textBuf[top] + x;
for(int i = 0; i < tileHeight; i++) {
u8 px = fontTiles[(index * tileHeight) + i];
for(int j = 0; j < tileWidth; j++) {
dst[(y + i) * 256 + j] = u8(palette) * 0x10 + ((px >> (7 - j)) & 1);
}
}
}
x += tileWidth;
}
}