254 lines
9.0 KiB
C++
254 lines
9.0 KiB
C++
#include "core/rendering/text.hpp"
|
|
|
|
#include <SDL3/SDL.h>
|
|
|
|
#include <cstddef>
|
|
#include <sstream>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
|
|
#include "core/rendering/sprite/sprite.hpp"
|
|
#include "core/rendering/surface.hpp"
|
|
#include "core/resources/resource_helper.hpp"
|
|
#include "utils/log.hpp"
|
|
#include "utils/utils.hpp"
|
|
|
|
// Decodifica el pròxim codepoint UTF-8 de la cadena i avança 'pos' al byte
|
|
// següent. Retorna U+FFFD davant seqüències malformades perquè el caller
|
|
// seguisca dibuixant (cau al glif '?' més endavant).
|
|
auto Text::nextCodepoint(const std::string& s, std::size_t& pos) -> std::uint32_t {
|
|
auto c = static_cast<unsigned char>(s[pos]);
|
|
std::uint32_t cp = 0;
|
|
std::size_t extra = 0;
|
|
|
|
if (c < 0x80) {
|
|
cp = c;
|
|
extra = 0;
|
|
} else if (c < 0xC0 || c >= 0xF8) {
|
|
pos++;
|
|
return 0xFFFD;
|
|
} else if (c < 0xE0) {
|
|
cp = c & 0x1F;
|
|
extra = 1;
|
|
} else if (c < 0xF0) {
|
|
cp = c & 0x0F;
|
|
extra = 2;
|
|
} else {
|
|
cp = c & 0x07;
|
|
extra = 3;
|
|
}
|
|
|
|
pos++;
|
|
for (std::size_t i = 0; i < extra && pos < s.size(); ++i, ++pos) {
|
|
auto cb = static_cast<unsigned char>(s[pos]);
|
|
if ((cb & 0xC0) != 0x80) { return 0xFFFD; }
|
|
cp = (cp << 6) | (cb & 0x3F);
|
|
}
|
|
return cp;
|
|
}
|
|
|
|
// Format .fnt: claus "box_width/box_height/columns/cell_spacing/row_spacing"
|
|
// i una línia per glif amb "codepoint_decimal amplada_visual". Els glifs
|
|
// s'ordenen al png per ordre d'aparició al .fnt (glyph_index).
|
|
auto Text::loadTextFile(const std::string& file_path) -> std::shared_ptr<File> {
|
|
auto tf = std::make_shared<File>();
|
|
|
|
auto file_data = Resource::Helper::loadFile(file_path);
|
|
if (file_data.empty()) {
|
|
Log::error("Text File", "fitxer no trobat: {}", getFileName(file_path));
|
|
throw std::runtime_error("Fitxer no trobat: " + getFileName(file_path));
|
|
}
|
|
|
|
std::string content(file_data.begin(), file_data.end());
|
|
std::istringstream stream(content);
|
|
std::string line;
|
|
int glyph_index = 0;
|
|
|
|
while (std::getline(stream, line)) {
|
|
if (!line.empty() && line.back() == '\r') { line.pop_back(); }
|
|
if (line.empty() || line[0] == '#') { continue; }
|
|
|
|
std::istringstream ls(line);
|
|
std::string key;
|
|
ls >> key;
|
|
|
|
if (key == "box_width") {
|
|
ls >> tf->box_width;
|
|
} else if (key == "box_height") {
|
|
ls >> tf->box_height;
|
|
} else if (key == "columns") {
|
|
ls >> tf->columns;
|
|
} else if (key == "cell_spacing") {
|
|
ls >> tf->cell_spacing;
|
|
} else if (key == "row_spacing") {
|
|
ls >> tf->row_spacing;
|
|
} else {
|
|
std::uint32_t codepoint = 0;
|
|
int width = 0;
|
|
try {
|
|
codepoint = static_cast<std::uint32_t>(std::stoul(key));
|
|
ls >> width;
|
|
} catch (...) {
|
|
continue;
|
|
}
|
|
Offset off{};
|
|
const int ROW_SP = tf->row_spacing > 0 ? tf->row_spacing : tf->cell_spacing;
|
|
off.x = ((glyph_index % tf->columns) * (tf->box_width + tf->cell_spacing)) + tf->cell_spacing;
|
|
off.y = ((glyph_index / tf->columns) * (tf->box_height + ROW_SP)) + tf->cell_spacing;
|
|
off.w = width;
|
|
tf->offset[codepoint] = off;
|
|
++glyph_index;
|
|
}
|
|
}
|
|
|
|
printStatus("Text File", getFileName(file_path), StatusKind::SUCCESS);
|
|
return tf;
|
|
}
|
|
|
|
Text::Text(const std::shared_ptr<Surface>& surface, const std::string& text_file) {
|
|
auto tf = loadTextFile(text_file);
|
|
box_height_ = tf->box_height;
|
|
box_width_ = tf->box_width;
|
|
offset_ = tf->offset;
|
|
sprite_ = std::make_unique<Sprite>(surface, SDL_FRect{.x = 0.0F, .y = 0.0F, .w = static_cast<float>(box_width_), .h = static_cast<float>(box_height_)});
|
|
}
|
|
|
|
Text::Text(const std::shared_ptr<Surface>& surface, const std::shared_ptr<File>& text_file)
|
|
: sprite_(std::make_unique<Sprite>(surface, SDL_FRect{.x = 0.0F, .y = 0.0F, .w = static_cast<float>(text_file->box_width), .h = static_cast<float>(text_file->box_height)})),
|
|
box_width_(text_file->box_width),
|
|
box_height_(text_file->box_height),
|
|
offset_(text_file->offset) {
|
|
}
|
|
|
|
// Dibuixa la cadena amb la tinta per defecte. Els glifs amb codepoint no
|
|
// registrat es substitueixen pel glif '?'.
|
|
void Text::write(int x, int y, const std::string& text, int kerning, int length) {
|
|
writeColored(x, y, text, DEFAULT_TEXT_COLOR, kerning, length);
|
|
}
|
|
|
|
void Text::writeColored(int x, int y, const std::string& text, Uint8 color, int kerning, int length) {
|
|
int shift = 0;
|
|
int glyphs_done = 0;
|
|
std::size_t pos = 0;
|
|
|
|
sprite_->setY(static_cast<float>(y));
|
|
while (pos < text.size()) {
|
|
if (length != -1 && glyphs_done >= length) { break; }
|
|
std::uint32_t cp = nextCodepoint(text, pos);
|
|
if (const Offset* off = resolveGlyph(cp); off != nullptr) {
|
|
sprite_->setClip(static_cast<float>(off->x), static_cast<float>(off->y), static_cast<float>(box_width_), static_cast<float>(box_height_));
|
|
sprite_->setX(static_cast<float>(x + shift));
|
|
sprite_->render(INK_COLOR_INDEX, color);
|
|
shift += off->w + kerning;
|
|
}
|
|
++glyphs_done;
|
|
}
|
|
}
|
|
|
|
void Text::writeDX(Uint8 flags, int x, int y, const std::string& text, int kerning, Uint8 text_color, Uint8 shadow_distance, Uint8 shadow_color, int length) {
|
|
const bool CENTERED = ((flags & CENTER_FLAG) == CENTER_FLAG);
|
|
const bool SHADOWED = ((flags & SHADOW_FLAG) == SHADOW_FLAG);
|
|
const bool COLORED = ((flags & COLOR_FLAG) == COLOR_FLAG);
|
|
const bool STROKED = ((flags & STROKE_FLAG) == STROKE_FLAG);
|
|
const bool STROKED_4 = ((flags & STROKE4_FLAG) == STROKE4_FLAG);
|
|
|
|
if (CENTERED) { x -= (Text::length(text, kerning) / 2); }
|
|
|
|
if (SHADOWED) {
|
|
writeColored(x + shadow_distance, y + shadow_distance, text, shadow_color, kerning, length);
|
|
}
|
|
|
|
if (STROKED_4) {
|
|
const int MAX_DIST = static_cast<int>(shadow_distance);
|
|
for (int dist = 1; dist <= MAX_DIST; ++dist) {
|
|
writeColored(x + dist, y, text, shadow_color, kerning, length);
|
|
writeColored(x - dist, y, text, shadow_color, kerning, length);
|
|
writeColored(x, y + dist, text, shadow_color, kerning, length);
|
|
writeColored(x, y - dist, text, shadow_color, kerning, length);
|
|
}
|
|
}
|
|
|
|
if (STROKED) {
|
|
const int MAX_DIST = static_cast<int>(shadow_distance);
|
|
for (int dist = 1; dist <= MAX_DIST; ++dist) {
|
|
for (int dy = -dist; dy <= dist; ++dy) {
|
|
for (int dx = -dist; dx <= dist; ++dx) {
|
|
writeColored(x + dx, y + dy, text, shadow_color, kerning, length);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (COLORED) {
|
|
writeColored(x, y, text, text_color, kerning, length);
|
|
} else {
|
|
write(x, y, text, kerning, length);
|
|
}
|
|
}
|
|
|
|
// Amplada fixa per dígits: cada '0'-'9' ocupa digit_cell_w centrat dins de
|
|
// la cel·la; la resta de glifs avancen amb la seua amplada natural. Emprat
|
|
// pel comptador de temps de l'overlay RenderInfo per a que els dígits no
|
|
// balleteigen quan canvien.
|
|
void Text::writeMonoDigits(int x, int y, const std::string& text, Uint8 color, int digit_cell_w, int kerning) {
|
|
int shift = 0;
|
|
std::size_t pos = 0;
|
|
|
|
sprite_->setY(static_cast<float>(y));
|
|
while (pos < text.size()) {
|
|
std::uint32_t cp = nextCodepoint(text, pos);
|
|
const Offset* off = resolveGlyph(cp);
|
|
if (off == nullptr) { continue; }
|
|
|
|
const bool IS_DIGIT = (cp >= '0' && cp <= '9');
|
|
const int GLYPH_W = off->w;
|
|
const int CELL_W = IS_DIGIT ? digit_cell_w : GLYPH_W;
|
|
const int GLYPH_X = IS_DIGIT ? x + shift + ((CELL_W - GLYPH_W) / 2) : x + shift;
|
|
|
|
sprite_->setClip(static_cast<float>(off->x),
|
|
static_cast<float>(off->y),
|
|
static_cast<float>(box_width_),
|
|
static_cast<float>(box_height_));
|
|
sprite_->setX(static_cast<float>(GLYPH_X));
|
|
sprite_->render(INK_COLOR_INDEX, color);
|
|
|
|
shift += CELL_W + kerning;
|
|
}
|
|
}
|
|
|
|
auto Text::lengthMonoDigits(const std::string& text, int digit_cell_w, int kerning) const -> int {
|
|
int shift = 0;
|
|
std::size_t pos = 0;
|
|
|
|
while (pos < text.size()) {
|
|
std::uint32_t cp = nextCodepoint(text, pos);
|
|
const Offset* off = resolveGlyph(cp);
|
|
if (off == nullptr) { continue; }
|
|
|
|
const bool IS_DIGIT = (cp >= '0' && cp <= '9');
|
|
shift += (IS_DIGIT ? digit_cell_w : off->w) + kerning;
|
|
}
|
|
|
|
return shift > 0 ? shift - kerning : 0;
|
|
}
|
|
|
|
auto Text::length(const std::string& text, int kerning) const -> int {
|
|
int shift = 0;
|
|
std::size_t pos = 0;
|
|
|
|
while (pos < text.size()) {
|
|
std::uint32_t cp = nextCodepoint(text, pos);
|
|
if (const Offset* off = resolveGlyph(cp); off != nullptr) {
|
|
shift += off->w + kerning;
|
|
}
|
|
}
|
|
|
|
return shift > 0 ? shift - kerning : 0;
|
|
}
|
|
|
|
auto Text::resolveGlyph(std::uint32_t cp) const -> const Offset* {
|
|
auto it = offset_.find(cp);
|
|
if (it == offset_.end()) { it = offset_.find('?'); }
|
|
return (it != offset_.end()) ? &it->second : nullptr;
|
|
}
|