421 lines
12 KiB
C++
421 lines
12 KiB
C++
#include "core/rendering/text.hpp"
|
|
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <sstream>
|
|
#include <string>
|
|
|
|
#include "core/resources/resource_helper.hpp"
|
|
|
|
// Forward declarations de gif.h (inclòs des de jdraw8.cpp, no es pot incloure dos vegades)
|
|
struct rgb;
|
|
extern unsigned char* LoadGif(unsigned char* data, unsigned short* w, unsigned short* h);
|
|
|
|
Text::Text(const char* fnt_file, const char* gif_file) {
|
|
loadBitmap(gif_file);
|
|
loadFont(fnt_file);
|
|
}
|
|
|
|
Text::~Text() {
|
|
if (bitmap_) free(bitmap_);
|
|
}
|
|
|
|
// --- UTF-8 ---
|
|
|
|
auto Text::nextCodepoint(const char*& ptr) -> uint32_t {
|
|
auto byte = static_cast<uint8_t>(*ptr);
|
|
if (byte == 0) return 0;
|
|
|
|
uint32_t cp = 0;
|
|
int extra = 0;
|
|
|
|
if (byte < 0x80) {
|
|
cp = byte;
|
|
} else if ((byte & 0xE0) == 0xC0) {
|
|
cp = byte & 0x1F;
|
|
extra = 1;
|
|
} else if ((byte & 0xF0) == 0xE0) {
|
|
cp = byte & 0x0F;
|
|
extra = 2;
|
|
} else if ((byte & 0xF8) == 0xF0) {
|
|
cp = byte & 0x07;
|
|
extra = 3;
|
|
} else {
|
|
ptr++;
|
|
return 0xFFFD;
|
|
}
|
|
|
|
ptr++;
|
|
for (int i = 0; i < extra; i++) {
|
|
auto cont = static_cast<uint8_t>(*ptr);
|
|
if ((cont & 0xC0) != 0x80) return 0xFFFD;
|
|
cp = (cp << 6) | (cont & 0x3F);
|
|
ptr++;
|
|
}
|
|
|
|
return cp;
|
|
}
|
|
|
|
// --- Càrrega de font ---
|
|
|
|
void Text::loadFont(const char* fnt_file) {
|
|
auto buffer = ResourceHelper::loadFile(fnt_file);
|
|
if (buffer.empty()) {
|
|
std::cerr << "Text: unable to load font file: " << fnt_file << '\n';
|
|
return;
|
|
}
|
|
|
|
std::istringstream stream(std::string(reinterpret_cast<const char*>(buffer.data()), buffer.size()));
|
|
|
|
std::string line;
|
|
int glyph_index = 0;
|
|
|
|
while (std::getline(stream, line)) {
|
|
// Ignora comentaris i línies buides
|
|
if (line.empty() || line[0] == '#') continue;
|
|
|
|
// Elimina comentaris inline
|
|
auto comment_pos = line.find('#');
|
|
if (comment_pos != std::string::npos) {
|
|
line.resize(comment_pos);
|
|
}
|
|
|
|
// Parseja directives
|
|
if (line.starts_with("box_width")) {
|
|
sscanf(line.c_str(), "box_width %d", &box_width_);
|
|
continue;
|
|
}
|
|
if (line.starts_with("box_height")) {
|
|
sscanf(line.c_str(), "box_height %d", &box_height_);
|
|
continue;
|
|
}
|
|
if (line.starts_with("columns")) {
|
|
sscanf(line.c_str(), "columns %d", &columns_);
|
|
continue;
|
|
}
|
|
if (line.starts_with("cell_spacing")) {
|
|
sscanf(line.c_str(), "cell_spacing %d", &cell_spacing_);
|
|
continue;
|
|
}
|
|
if (line.starts_with("row_spacing")) {
|
|
sscanf(line.c_str(), "row_spacing %d", &row_spacing_);
|
|
continue;
|
|
}
|
|
|
|
// Línies de glifos: "codepoint width"
|
|
uint32_t codepoint = 0;
|
|
int visual_width = 0;
|
|
if (sscanf(line.c_str(), "%u %d", &codepoint, &visual_width) == 2) {
|
|
int col = glyph_index % columns_;
|
|
int row = glyph_index / columns_;
|
|
|
|
GlyphInfo glyph{};
|
|
glyph.x = col * (box_width_ + cell_spacing_) + cell_spacing_;
|
|
glyph.y = row * (box_height_ + (row_spacing_ > 0 ? row_spacing_ : cell_spacing_)) + cell_spacing_;
|
|
glyph.w = visual_width;
|
|
|
|
glyphs_[codepoint] = glyph;
|
|
glyph_index++;
|
|
}
|
|
}
|
|
|
|
std::cout << "Text: loaded font with " << glyphs_.size() << " glyphs (" << box_width_ << "x" << box_height_ << ")\n";
|
|
}
|
|
|
|
void Text::loadBitmap(const char* gif_file) {
|
|
auto buffer = ResourceHelper::loadFile(gif_file);
|
|
if (buffer.empty()) {
|
|
std::cerr << "Text: unable to load bitmap: " << gif_file << '\n';
|
|
return;
|
|
}
|
|
|
|
// Extrau dimensions del header GIF (bytes 6-7 = width, 8-9 = height, little-endian)
|
|
auto* raw = buffer.data();
|
|
int w = raw[6] | (raw[7] << 8);
|
|
int h = raw[8] | (raw[9] << 8);
|
|
|
|
unsigned short gw = 0, gh = 0;
|
|
Uint8* pixels = LoadGif(raw, &gw, &gh);
|
|
if (!pixels) {
|
|
std::cerr << "Text: unable to decode GIF: " << gif_file << '\n';
|
|
return;
|
|
}
|
|
|
|
bitmap_width_ = w;
|
|
bitmap_height_ = h;
|
|
bitmap_ = pixels;
|
|
|
|
std::cout << "Text: bitmap loaded " << w << "x" << h << '\n';
|
|
}
|
|
|
|
// --- Renderitzat ---
|
|
|
|
void Text::draw(Uint32* pixel_data, int x, int y, const char* text, Uint32 color) const {
|
|
if (!bitmap_ || !pixel_data) return;
|
|
|
|
const char* ptr = text;
|
|
int cursor_x = x;
|
|
|
|
while (*ptr) {
|
|
uint32_t cp = nextCodepoint(ptr);
|
|
if (cp == 0) break;
|
|
|
|
auto it = glyphs_.find(cp);
|
|
if (it == glyphs_.end()) {
|
|
it = glyphs_.find('?');
|
|
if (it == glyphs_.end()) {
|
|
cursor_x += box_width_;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
const auto& glyph = it->second;
|
|
|
|
// Pinta glifo pixel a pixel
|
|
for (int gy = 0; gy < box_height_; gy++) {
|
|
int dst_y = y + gy;
|
|
if (dst_y < 0 || dst_y >= SCREEN_HEIGHT) continue;
|
|
|
|
for (int gx = 0; gx < glyph.w; gx++) {
|
|
int dst_x = cursor_x + gx;
|
|
if (dst_x < 0 || dst_x >= SCREEN_WIDTH) continue;
|
|
|
|
int src_x = glyph.x + gx;
|
|
int src_y = glyph.y + gy;
|
|
|
|
if (src_x >= bitmap_width_ || src_y >= bitmap_height_) continue;
|
|
|
|
Uint8 pixel = bitmap_[src_x + src_y * bitmap_width_];
|
|
// Píxel no transparent (índex 0 és fons típicament)
|
|
if (pixel != 0) {
|
|
pixel_data[dst_x + dst_y * SCREEN_WIDTH] = color;
|
|
}
|
|
}
|
|
}
|
|
|
|
cursor_x += glyph.w + 1; // +1 kerning
|
|
}
|
|
}
|
|
|
|
void Text::drawCentered(Uint32* pixel_data, int y, const char* text, Uint32 color) const {
|
|
int w = width(text);
|
|
int x = (SCREEN_WIDTH - w) / 2;
|
|
draw(pixel_data, x, y, text, color);
|
|
}
|
|
|
|
void Text::drawClipped(Uint32* pixel_data, int x, int y, const char* text, Uint32 color, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) const {
|
|
if (!bitmap_ || !pixel_data) return;
|
|
|
|
// Descart ràpid si el glifo sencer cau fora verticalment
|
|
if (y + box_height_ <= clip_y_min || y >= clip_y_max) return;
|
|
|
|
const char* ptr = text;
|
|
int cursor_x = x;
|
|
|
|
while (*ptr) {
|
|
uint32_t cp = nextCodepoint(ptr);
|
|
if (cp == 0) break;
|
|
|
|
auto it = glyphs_.find(cp);
|
|
if (it == glyphs_.end()) {
|
|
it = glyphs_.find('?');
|
|
if (it == glyphs_.end()) {
|
|
cursor_x += box_width_;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
const auto& glyph = it->second;
|
|
|
|
// Si el glifo està completament fora del clip horitzontal, salta
|
|
if (cursor_x + glyph.w <= clip_x_min || cursor_x >= clip_x_max) {
|
|
cursor_x += glyph.w + 1;
|
|
continue;
|
|
}
|
|
|
|
for (int gy = 0; gy < box_height_; gy++) {
|
|
int dst_y = y + gy;
|
|
if (dst_y < 0 || dst_y >= SCREEN_HEIGHT) continue;
|
|
if (dst_y < clip_y_min || dst_y >= clip_y_max) continue;
|
|
|
|
for (int gx = 0; gx < glyph.w; gx++) {
|
|
int dst_x = cursor_x + gx;
|
|
if (dst_x < 0 || dst_x >= SCREEN_WIDTH) continue;
|
|
if (dst_x < clip_x_min || dst_x >= clip_x_max) continue;
|
|
|
|
int src_x = glyph.x + gx;
|
|
int src_y = glyph.y + gy;
|
|
if (src_x >= bitmap_width_ || src_y >= bitmap_height_) continue;
|
|
|
|
Uint8 pixel = bitmap_[src_x + src_y * bitmap_width_];
|
|
if (pixel != 0) {
|
|
pixel_data[dst_x + dst_y * SCREEN_WIDTH] = color;
|
|
}
|
|
}
|
|
}
|
|
|
|
cursor_x += glyph.w + 1;
|
|
}
|
|
}
|
|
|
|
void Text::drawMono(Uint32* pixel_data, int x, int y, const char* text, Uint32 color, int cell_w) const {
|
|
if (!bitmap_ || !pixel_data) return;
|
|
|
|
const char* ptr = text;
|
|
int cursor_x = x;
|
|
|
|
while (*ptr) {
|
|
uint32_t cp = nextCodepoint(ptr);
|
|
if (cp == 0) break;
|
|
|
|
auto it = glyphs_.find(cp);
|
|
if (it == glyphs_.end()) {
|
|
it = glyphs_.find('?');
|
|
if (it == glyphs_.end()) {
|
|
cursor_x += cell_w;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
const auto& glyph = it->second;
|
|
// Centra el glif dins la cel·la
|
|
int glyph_x = cursor_x + (cell_w - glyph.w) / 2;
|
|
|
|
for (int gy = 0; gy < box_height_; gy++) {
|
|
int dst_y = y + gy;
|
|
if (dst_y < 0 || dst_y >= SCREEN_HEIGHT) continue;
|
|
|
|
for (int gx = 0; gx < glyph.w; gx++) {
|
|
int dst_x = glyph_x + gx;
|
|
if (dst_x < 0 || dst_x >= SCREEN_WIDTH) continue;
|
|
|
|
int src_x = glyph.x + gx;
|
|
int src_y = glyph.y + gy;
|
|
if (src_x >= bitmap_width_ || src_y >= bitmap_height_) continue;
|
|
|
|
Uint8 pixel = bitmap_[src_x + src_y * bitmap_width_];
|
|
if (pixel != 0) {
|
|
pixel_data[dst_x + dst_y * SCREEN_WIDTH] = color;
|
|
}
|
|
}
|
|
}
|
|
|
|
cursor_x += cell_w;
|
|
}
|
|
}
|
|
|
|
void Text::drawMonoDigits(Uint32* pixel_data, int x, int y, const char* text, Uint32 color, int digit_cell_w) const {
|
|
if (!bitmap_ || !pixel_data) return;
|
|
|
|
const char* ptr = text;
|
|
int cursor_x = x;
|
|
bool first = true;
|
|
|
|
while (*ptr) {
|
|
uint32_t cp = nextCodepoint(ptr);
|
|
if (cp == 0) break;
|
|
|
|
auto it = glyphs_.find(cp);
|
|
if (it == glyphs_.end()) {
|
|
it = glyphs_.find('?');
|
|
if (it == glyphs_.end()) {
|
|
if (!first) cursor_x += 1;
|
|
cursor_x += box_width_;
|
|
first = false;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
const auto& glyph = it->second;
|
|
bool is_digit = (cp >= '0' && cp <= '9');
|
|
|
|
if (!first) cursor_x += 1; // kerning
|
|
|
|
int glyph_x = is_digit ? cursor_x + (digit_cell_w - glyph.w) / 2 : cursor_x;
|
|
|
|
for (int gy = 0; gy < box_height_; gy++) {
|
|
int dst_y = y + gy;
|
|
if (dst_y < 0 || dst_y >= SCREEN_HEIGHT) continue;
|
|
for (int gx = 0; gx < glyph.w; gx++) {
|
|
int dst_x = glyph_x + gx;
|
|
if (dst_x < 0 || dst_x >= SCREEN_WIDTH) continue;
|
|
int src_x = glyph.x + gx;
|
|
int src_y = glyph.y + gy;
|
|
if (src_x >= bitmap_width_ || src_y >= bitmap_height_) continue;
|
|
Uint8 pixel = bitmap_[src_x + src_y * bitmap_width_];
|
|
if (pixel != 0) {
|
|
pixel_data[dst_x + dst_y * SCREEN_WIDTH] = color;
|
|
}
|
|
}
|
|
}
|
|
|
|
cursor_x += is_digit ? digit_cell_w : glyph.w;
|
|
first = false;
|
|
}
|
|
}
|
|
|
|
auto Text::widthMonoDigits(const char* text, int digit_cell_w) const -> int {
|
|
const char* ptr = text;
|
|
int w = 0;
|
|
bool first = true;
|
|
while (*ptr) {
|
|
uint32_t cp = nextCodepoint(ptr);
|
|
if (cp == 0) break;
|
|
if (!first) w += 1; // kerning
|
|
first = false;
|
|
bool is_digit = (cp >= '0' && cp <= '9');
|
|
if (is_digit) {
|
|
w += digit_cell_w;
|
|
} else {
|
|
auto it = glyphs_.find(cp);
|
|
if (it == glyphs_.end()) it = glyphs_.find('?');
|
|
if (it != glyphs_.end())
|
|
w += it->second.w;
|
|
else
|
|
w += box_width_;
|
|
}
|
|
}
|
|
return w;
|
|
}
|
|
|
|
auto Text::widthMono(const char* text, int cell_w) const -> int {
|
|
const char* ptr = text;
|
|
int count = 0;
|
|
while (*ptr) {
|
|
uint32_t cp = nextCodepoint(ptr);
|
|
if (cp == 0) break;
|
|
count++;
|
|
}
|
|
return count * cell_w;
|
|
}
|
|
|
|
auto Text::width(const char* text) const -> int {
|
|
const char* ptr = text;
|
|
int w = 0;
|
|
bool first = true;
|
|
|
|
while (*ptr) {
|
|
uint32_t cp = nextCodepoint(ptr);
|
|
if (cp == 0) break;
|
|
|
|
auto it = glyphs_.find(cp);
|
|
if (it == glyphs_.end()) {
|
|
it = glyphs_.find('?');
|
|
}
|
|
|
|
if (!first) w += 1; // kerning
|
|
first = false;
|
|
|
|
if (it != glyphs_.end()) {
|
|
w += it->second.w;
|
|
} else {
|
|
w += box_width_;
|
|
}
|
|
}
|
|
|
|
return w;
|
|
}
|