Merge pull request #2054 from r3claimer/next

SDL2Text, filter out unrecognized chars
This commit is contained in:
r3claimer
2025-11-21 15:43:22 -08:00
committed by GitHub

View File

@@ -11,6 +11,7 @@
#include <dirent.h>
#include <filesystem>
#include <map>
#include <cstdint>
// -------------------- Config Helpers --------------------
std::filesystem::path get_config_file() {
@@ -19,7 +20,7 @@ std::filesystem::path get_config_file() {
return cfgDir / "sdl2text.conf";
}
struct FileConfig { int scroll = 0; int fontSize = 40; }; // default font size 32
struct FileConfig { int scroll = 0; int fontSize = 40; }; // default font size 40
std::map<std::string, FileConfig> load_config() {
std::map<std::string, FileConfig> cfg;
@@ -33,10 +34,12 @@ std::map<std::string, FileConfig> load_config() {
std::string val = line.substr(sep+1);
size_t comma = val.find(',');
int scroll = 0;
int font = 32;
int font = 40;
if(comma != std::string::npos){
scroll = std::stoi(val.substr(0,comma));
font = std::stoi(val.substr(comma+1));
try {
scroll = std::stoi(val.substr(0,comma));
font = std::stoi(val.substr(comma+1));
} catch(...) { /* ignore parse errors, use defaults */ }
}
cfg[key] = {scroll,font};
}
@@ -85,7 +88,64 @@ std::string find_any_ttf_font() {
return "";
}
// Split long line into chunks to fit screen width
// -------------------- UTF-8 safe filtering for unsupported glyphs --------------------
static bool font_can_render_codepoint(TTF_Font* font, uint32_t cp) {
if (cp <= 0xFFFF) {
return TTF_GlyphIsProvided(font, static_cast<Uint16>(cp)) != 0;
}
return false;
}
std::string filter_invalid_chars(const std::string& s, TTF_Font* font) {
std::string out;
size_t i = 0;
while (i < s.size()) {
unsigned char c = static_cast<unsigned char>(s[i]);
if (c < 0x80) {
if (font_can_render_codepoint(font, c)) out.push_back(static_cast<char>(c));
++i;
continue;
}
uint32_t cp = 0;
size_t seqLen = 0;
if ((c & 0xE0) == 0xC0) {
if (i + 1 >= s.size()) { ++i; continue; }
unsigned char c1 = static_cast<unsigned char>(s[i+1]);
if ((c1 & 0xC0) != 0x80) { ++i; continue; }
cp = ((c & 0x1F) << 6) | (c1 & 0x3F);
seqLen = 2;
if (cp < 0x80) { i += 2; continue; }
} else if ((c & 0xF0) == 0xE0) {
if (i + 2 >= s.size()) { ++i; continue; }
unsigned char c1 = static_cast<unsigned char>(s[i+1]);
unsigned char c2 = static_cast<unsigned char>(s[i+2]);
if ((c1 & 0xC0) != 0x80 || (c2 & 0xC0) != 0x80) { ++i; continue; }
cp = ((c & 0x0F) << 12) | ((c1 & 0x3F) << 6) | (c2 & 0x3F);
seqLen = 3;
} else if ((c & 0xF8) == 0xF0) {
if (i + 3 >= s.size()) { ++i; continue; }
unsigned char c1 = static_cast<unsigned char>(s[i+1]);
unsigned char c2 = static_cast<unsigned char>(s[i+2]);
unsigned char c3 = static_cast<unsigned char>(s[i+3]);
if ((c1 & 0xC0) != 0x80 || (c2 & 0xC0) != 0x80 || (c3 & 0xC0) != 0x80) { ++i; continue; }
cp = ((c & 0x07) << 18) | ((c1 & 0x3F) << 12) | ((c2 & 0x3F) << 6) | (c3 & 0x3F);
seqLen = 4;
} else { ++i; continue; }
if (seqLen == 0) { ++i; continue; }
if (font_can_render_codepoint(font, cp)) {
out.append(s.substr(i, seqLen));
}
i += seqLen;
}
return out;
}
// -------------------- Split long line into chunks to fit screen width --------------------
std::vector<std::string> wrap_line(const std::string& line, TTF_Font* font, int maxWidth) {
std::vector<std::string> result;
size_t start = 0;
@@ -136,15 +196,22 @@ int main(int argc, char* argv[]) {
auto lines = load_file_lines(textFile);
if(lines.empty()){ std::cout << "Failed to load file\n"; return 1; }
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER);
TTF_Init();
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) != 0) {
std::cerr << "SDL_Init error: " << SDL_GetError() << "\n";
return 1;
}
if (TTF_Init() != 0) {
std::cerr << "TTF_Init error: " << TTF_GetError() << "\n";
SDL_Quit();
return 1;
}
std::string fontPath = find_any_ttf_font();
if(fontPath.empty()){ std::cout << "No TTF font found\n"; return 1; }
if(fontPath.empty()){ std::cout << "No TTF font found\n"; TTF_Quit(); SDL_Quit(); return 1; }
std::cout << "Using font: " << fontPath << "\n";
auto cfg = load_config();
FileConfig fcfg = cfg[textFile]; // default scroll=0, fontSize=32
FileConfig fcfg = cfg[textFile];
int fontSize = fcfg.fontSize;
auto loadFont = [&](int size) -> TTF_Font* {
@@ -153,14 +220,17 @@ int main(int argc, char* argv[]) {
return f;
};
TTF_Font* font = loadFont(fontSize);
if (!font) { TTF_Quit(); SDL_Quit(); return 1; }
SDL_Window* win = SDL_CreateWindow("Text Viewer",
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
0,0,
SDL_WINDOW_FULLSCREEN_DESKTOP|SDL_WINDOW_BORDERLESS);
if (!win) { std::cerr << "SDL_CreateWindow error\n"; TTF_CloseFont(font); TTF_Quit(); SDL_Quit(); return 1; }
SDL_Renderer* ren = SDL_CreateRenderer(win,-1,SDL_RENDERER_ACCELERATED);
if (!ren) { SDL_DestroyWindow(win); TTF_CloseFont(font); TTF_Quit(); SDL_Quit(); return 1; }
int WINDOW_W, WINDOW_H;
int WINDOW_W = 0, WINDOW_H = 0;
SDL_GetWindowSize(win,&WINDOW_W,&WINDOW_H);
SDL_Color white = {255,255,255,255};
@@ -171,12 +241,17 @@ int main(int argc, char* argv[]) {
SDL_GameController* pad = nullptr;
for(int i=0;i<SDL_NumJoysticks();i++){
if(SDL_IsGameController(i)){ pad=SDL_GameControllerOpen(i); break; }
if(SDL_IsGameController(i)){
pad = SDL_GameControllerOpen(i);
if (pad) break;
}
}
// Wrap and pre-render
std::vector<std::string> wrapped;
for(auto &line: lines){
auto chunks = wrap_line(line,font,WINDOW_W-20);
std::string clean = filter_invalid_chars(line, font);
auto chunks = wrap_line(clean,font,WINDOW_W-20);
wrapped.insert(wrapped.end(),chunks.begin(),chunks.end());
}
auto textures = create_textures(ren, font, wrapped, white);
@@ -185,45 +260,64 @@ int main(int argc, char* argv[]) {
bool running=true;
SDL_Event e;
// Touch drag-only state
bool touchActive = false;
float lastTouchY = 0.0f;
const float TOUCH_MULTIPLIER = 1.75f;
// Help overlay
bool showHelp = false;
SDL_Color helpColor = {255,255,0,255};
SDL_Color boxColor = {0,0,0,200};
while(running){
while(SDL_PollEvent(&e)){
if(e.type==SDL_QUIT) running=false;
if(e.type==SDL_QUIT) { running=false; break; }
// Controller input
if(e.type==SDL_CONTROLLERBUTTONDOWN){
switch(e.cbutton.button){
case SDL_CONTROLLER_BUTTON_DPAD_UP: upPressed=true; break;
case SDL_CONTROLLER_BUTTON_DPAD_DOWN: downPressed=true; break;
case SDL_CONTROLLER_BUTTON_LEFTSHOULDER: l1Pressed=true; break;
case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: r1Pressed=true; break;
case SDL_CONTROLLER_BUTTON_Y: // North - increase font
case SDL_CONTROLLER_BUTTON_X:
fontSize += 2;
TTF_CloseFont(font);
font = loadFont(fontSize);
if (!font) { fontSize -= 2; font = loadFont(fontSize); }
wrapped.clear();
for(auto &line: lines){
auto chunks = wrap_line(line,font,WINDOW_W-20);
std::string clean = filter_invalid_chars(line, font);
auto chunks = wrap_line(clean,font,WINDOW_W-20);
wrapped.insert(wrapped.end(),chunks.begin(),chunks.end());
}
for(auto &lt: textures) SDL_DestroyTexture(lt.tex);
for(auto &lt: textures) if (lt.tex) SDL_DestroyTexture(lt.tex);
textures = create_textures(ren, font, wrapped, white);
lineHeight = TTF_FontHeight(font);
break;
case SDL_CONTROLLER_BUTTON_X: // West - decrease font
case SDL_CONTROLLER_BUTTON_Y:
fontSize -= 2;
if(fontSize<8) fontSize=8;
TTF_CloseFont(font);
font = loadFont(fontSize);
if (!font) { fontSize += 2; font = loadFont(fontSize); }
wrapped.clear();
for(auto &line: lines){
auto chunks = wrap_line(line,font,WINDOW_W-20);
std::string clean = filter_invalid_chars(line, font);
auto chunks = wrap_line(clean,font,WINDOW_W-20);
wrapped.insert(wrapped.end(),chunks.begin(),chunks.end());
}
for(auto &lt: textures) SDL_DestroyTexture(lt.tex);
for(auto &lt: textures) if (lt.tex) SDL_DestroyTexture(lt.tex);
textures = create_textures(ren, font, wrapped, white);
lineHeight = TTF_FontHeight(font);
break;
case SDL_CONTROLLER_BUTTON_B: // South - close
case SDL_CONTROLLER_BUTTON_B:
running=false;
break;
case SDL_CONTROLLER_BUTTON_BACK: // SELECT button
showHelp = !showHelp;
break;
}
}
if(e.type==SDL_CONTROLLERBUTTONUP){
@@ -234,55 +328,109 @@ int main(int argc, char* argv[]) {
case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: r1Pressed=false; break;
}
}
// Touch events
if(e.type == SDL_FINGERDOWN) {
touchActive = true;
lastTouchY = e.tfinger.y;
}
if(e.type == SDL_FINGERMOTION && touchActive) {
float y = e.tfinger.y;
float dy_norm = y - lastTouchY;
lastTouchY = y;
int deltaPixels = (int)(-dy_norm * WINDOW_H * TOUCH_MULTIPLIER);
scroll_y += deltaPixels;
}
if(e.type == SDL_FINGERUP) touchActive = false;
}
// scrolling
// Controller continuous scrolling
if(upPressed) scroll_y -= SCROLL_SPEED;
if(downPressed) scroll_y += SCROLL_SPEED;
if(l1Pressed) scroll_y -= SKIP_LINES*lineHeight;
if(r1Pressed) scroll_y += SKIP_LINES*lineHeight;
// Clamp
int total_height = (int)textures.size()*lineHeight;
if(total_height < WINDOW_H) total_height = WINDOW_H;
if(scroll_y < 0) scroll_y=0;
if(scroll_y > total_height-WINDOW_H) scroll_y = total_height-WINDOW_H;
// render
// Render
SDL_SetRenderDrawColor(ren,0,0,0,255);
SDL_RenderClear(ren);
int firstLine = scroll_y/lineHeight;
int offsetY = -(scroll_y%lineHeight);
for(size_t i=firstLine; i<textures.size() && offsetY<WINDOW_H; i++){
SDL_Rect dst = {10, offsetY, textures[i].w, textures[i].h};
SDL_RenderCopy(ren, textures[i].tex, nullptr, &dst);
if (textures[i].tex) {
SDL_Rect dst = {10, offsetY, textures[i].w, textures[i].h};
SDL_RenderCopy(ren, textures[i].tex, nullptr, &dst);
}
offsetY += textures[i].h;
}
// scroll bar
// Scroll bar
int barWidth = 8;
float scrollPercent = (float)scroll_y / (float)(total_height - WINDOW_H);
if(scrollPercent < 0) { scrollPercent = 0; }
if(scrollPercent > 1) { scrollPercent = 1; }
int barHeight = (int)((float)WINDOW_H * (float)WINDOW_H / total_height);
float scrollPercent = 0.0f;
if (total_height > WINDOW_H) scrollPercent = (float)scroll_y / (float)(total_height - WINDOW_H);
if(scrollPercent < 0) scrollPercent = 0;
if(scrollPercent > 1) scrollPercent = 1;
int barHeight = (int)((float)WINDOW_H * (float)WINDOW_H / (float)total_height);
if(barHeight < 10) barHeight = 10;
int barY = (int)(scrollPercent*(WINDOW_H - barHeight));
SDL_Rect scrollbar = {WINDOW_W - barWidth - 2, barY, barWidth, barHeight};
SDL_SetRenderDrawColor(ren, 200,200,200,255);
SDL_RenderFillRect(ren, &scrollbar);
// Help overlay
if(showHelp) {
int boxW = WINDOW_W / 2;
int boxH = WINDOW_H / 2;
int boxX = (WINDOW_W - boxW)/2;
int boxY = (WINDOW_H - boxH)/2;
SDL_Rect helpBox = {boxX, boxY, boxW, boxH};
SDL_SetRenderDrawBlendMode(ren, SDL_BLENDMODE_BLEND);
SDL_SetRenderDrawColor(ren, boxColor.r, boxColor.g, boxColor.b, boxColor.a);
SDL_RenderFillRect(ren, &helpBox);
std::vector<std::string> helpLines = {
"CONTROLS:",
"UP/DOWN DPAD - Scroll",
"L1/R1 - Skip lines",
"X/Y - Increase/Decrease font",
"B - Exit",
"SELECT - Toggle this help",
"Touch Drag - Scroll"
};
int ty = boxY + 20;
for(auto &line : helpLines){
SDL_Surface* surf = TTF_RenderUTF8_Blended(font, line.c_str(), helpColor);
if(!surf) continue;
SDL_Texture* tex = SDL_CreateTextureFromSurface(ren, surf);
SDL_Rect dst = {boxX + 20, ty, surf->w, surf->h};
SDL_RenderCopy(ren, tex, nullptr, &dst);
ty += surf->h + 8;
SDL_DestroyTexture(tex);
SDL_FreeSurface(surf);
}
}
SDL_RenderPresent(ren);
SDL_Delay(16);
}
// save scroll and fontSize using full path
// Save config
cfg[textFile] = {scroll_y, fontSize};
save_config(cfg);
for(auto &lt: textures) SDL_DestroyTexture(lt.tex);
for(auto &lt: textures) if (lt.tex) SDL_DestroyTexture(lt.tex);
if(pad) SDL_GameControllerClose(pad);
TTF_CloseFont(font);
SDL_DestroyRenderer(ren);
SDL_DestroyWindow(win);
if(font) TTF_CloseFont(font);
if(ren) SDL_DestroyRenderer(ren);
if(win) SDL_DestroyWindow(win);
TTF_Quit();
SDL_Quit();
return 0;