diff --git a/Makefile b/Makefile index 08cc82f..1d66e9d 100644 --- a/Makefile +++ b/Makefile @@ -139,9 +139,9 @@ export OFILES_SRC := $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o) export OFILES := $(OFILES_BIN) $(OFILES_SRC) export HFILES_BIN := $(addsuffix .h,$(subst .,_,$(BINFILES))) -export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \ +export INCLUDE := -I$(CURDIR)/mupdf/include \ + $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \ $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ - -I$(CURDIR)/mupdf/include \ -I$(CURDIR)/$(BUILD) export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib) @@ -201,10 +201,10 @@ $(BUILD): $(CURDIR)/lib/libmupdf.a @[ -d $@ ] || mkdir -p $@ @$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile -# Сборка libmupdf.a через Makefile.switch из корня проекта +# Сборка libmupdf.a через Makefile.mupdf из корня проекта $(CURDIR)/lib/libmupdf.a: @echo ">>> Сборка libmupdf.a ..." - @$(MAKE) -f $(CURDIR)/Makefile.switch + @$(MAKE) -f $(CURDIR)/Makefile.mupdf #--------------------------------------------------------------------------------- clean: @@ -218,12 +218,12 @@ endif #--------------------------------------------------------------------------------- mupdf-clean: @echo cleaning mupdf ... - @$(MAKE) -f $(CURDIR)/Makefile.switch clean + @$(MAKE) -f $(CURDIR)/Makefile.mupdf clean #--------------------------------------------------------------------------------- # Ручная пересборка только libmupdf.a (без пересборки всего проекта) mupdf: - @$(MAKE) -f $(CURDIR)/Makefile.switch + @$(MAKE) -f $(CURDIR)/Makefile.mupdf #--------------------------------------------------------------------------------- else diff --git a/WookReader.lst b/WookReader.lst new file mode 100644 index 0000000..682cbc7 --- /dev/null +++ b/WookReader.lst @@ -0,0 +1,2 @@ +-CSn +/home/valen/.gemini/antigravity/scratch/levelr/WookReader/WookReader.elf diff --git a/include/config.h b/include/config.h index 43c267f..d8d2a64 100644 --- a/include/config.h +++ b/include/config.h @@ -2,6 +2,11 @@ #define WOOK_READER_CONFIG_H extern bool configDarkMode; +extern bool configStatusBar; +extern bool configScreenButtons; +extern bool configKeepZoom; +extern bool configKeepPosition; +extern bool configStatusBarPageTurn; extern const char* configFile; diff --git a/include/helpers/SDL_helper.h b/include/helpers/SDL_helper.h index ecc65e4..e36453c 100644 --- a/include/helpers/SDL_helper.h +++ b/include/helpers/SDL_helper.h @@ -36,6 +36,7 @@ void SDL_DrawCircle(SDL_Renderer *renderer, int x, int y, int r, SDL_Color colou void SDL_DrawText(SDL_Renderer *renderer, TTF_Font *font, int x, int y, SDL_Color colour, const char *text); void SDL_DrawRotatedText(SDL_Renderer *renderer, TTF_Font *font, double rotation, int x, int y, SDL_Color colour, const char *text); void SDL_DrawTextf(SDL_Renderer *renderer, TTF_Font *font, int x, int y, SDL_Color colour, const char* text, ...); +void SDL_DrawTextWrapped(SDL_Renderer *renderer, TTF_Font *font, int x, int y, int wrapWidth, int maxH, SDL_Color colour, const char *text); void SDL_LoadImage(SDL_Texture **texture, char *path); void SDL_LoadImageBuf(SDL_Renderer *renderer, SDL_Texture **texture, void *mem, int size); void SDL_DrawImage(SDL_Renderer *renderer, SDL_Texture *texture, int x, int y); diff --git a/lib/libmupdf.a b/lib/libmupdf.a new file mode 100644 index 0000000..1d0c3e4 Binary files /dev/null and b/lib/libmupdf.a differ diff --git a/source/main.cpp b/source/main.cpp index 62c7822..93b4209 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -110,6 +110,9 @@ TTF_Font *ROBOTO_35, *ROBOTO_30, *ROBOTO_27, *ROBOTO_25, *ROBOTO_20, *ROBOTO_15; bool configDarkMode; bool configScreenButtons = false; bool configStatusBar = true; +bool configKeepZoom = false; +bool configKeepPosition = false; +bool configStatusBarPageTurn = false; void Term_Services() { Log_Write("Terminate Services"); @@ -152,17 +155,22 @@ bool Init_Services() { twiliInitialize(); #endif - // Однократная миграция: если /switch/eBookReader существует, а - // /switch/WookReader ещё нет — переименовываем папку целиком (books, configs, - // logs переезжают вместе). + // Однократная миграция: если /switch/eBookReader o /switch/WookReader existe, y + // /switch/WookReader no — reubicamos la carpeta para no perder la configuración/libros. { - struct stat st_old, st_new; - bool old_exists = - (stat("/switch/eBookReader", &st_old) == 0 && S_ISDIR(st_old.st_mode)); + struct stat st_old_eb, st_old_wook, st_new; + bool eb_exists = + (stat("/switch/eBookReader", &st_old_eb) == 0 && S_ISDIR(st_old_eb.st_mode)); + bool wook_exists = + (stat("/switch/WookReader", &st_old_wook) == 0 && S_ISDIR(st_old_wook.st_mode)); bool new_exists = (stat("/switch/WookReader", &st_new) == 0 && S_ISDIR(st_new.st_mode)); - if (old_exists && !new_exists) - rename("/switch/eBookReader", "/switch/WookReader"); + if (!new_exists) { + if (wook_exists) + rename("/switch/WookReader", "/switch/WookReader"); + else if (eb_exists) + rename("/switch/eBookReader", "/switch/WookReader"); + } } // Сначала создаём папку и открываем лог @@ -202,8 +210,14 @@ bool Init_Services() { } Log_Write("Initialized Image"); - // MuPDF context deferred to first PDF/EPUB/XPS open (BookReader.cpp) - Log_Write("MuPDF init deferred to first use"); + // Initialize MuPDF context on startup so covers are rendered immediately in chooser + ctx = fz_new_context(NULL, NULL, 0); + if (ctx) { + fz_register_document_handlers(ctx); + Log_Write("Initialized MuPDF context on startup"); + } else { + Log_Error("Failed to initialize MuPDF context on startup"); + } if (TTF_Init() == -1) { Log_Error(std::string("TTF_Init failed: ") + TTF_GetError()); diff --git a/source/menus/book-chooser/MenuChooser.cpp b/source/menus/book-chooser/MenuChooser.cpp index 32ae92e..25cc024 100644 --- a/source/menus/book-chooser/MenuChooser.cpp +++ b/source/menus/book-chooser/MenuChooser.cpp @@ -36,10 +36,16 @@ static const char* CHOOSER_NOTES_DIR = "/switch/WookReader/.notes"; static string chooser_sanitize(const string& fpath) { string name = fpath.substr(fpath.find_last_of("/\\") + 1); - const string bad = " :/?#[]@!$&'()*+,;=."; - for (char c : bad) - name.erase(remove(name.begin(), name.end(), c), name.end()); - return name; + string sanitized = ""; + for (char c : name) { + if (isalnum((unsigned char)c) || c == '-' || c == '_') { + sanitized += c; + } + } + if (!sanitized.empty() && !isalpha((unsigned char)sanitized[0])) { + sanitized = "b" + sanitized; + } + return sanitized; } static string chooser_note_path(const string& fpath) { @@ -68,16 +74,17 @@ static string chooser_note_read(const string& fpath) { static void chooser_note_save(const string& fpath, const string& text) { FS_RecursiveMakeDir(CHOOSER_NOTES_DIR); string p = chooser_note_path(fpath); - if (text.empty()) { remove(p.c_str()); return; } + if (text.empty()) { remove(p.c_str()); fsdevCommitDevice("sdmc"); return; } FILE* f = fopen(p.c_str(), "w"); if (!f) return; fwrite(text.data(), 1, text.size(), f); fclose(f); + fsdevCommitDevice("sdmc"); } // ── Reading history ─────────────────────────────────────────────────────────── static const char RECENT_FILE[] = "/switch/WookReader/.recent.lst"; -static const char RECENT_SENTINEL[] = "/WOOK_RECENT"; +static const char RECENT_SENTINEL[] = "/MEEP_RECENT"; static vector g_recent; static void load_recent() { @@ -211,6 +218,18 @@ static int load_config(unsigned int* chosenFolderColor, config_setting_get_member(config_root_setting(optionConfig), "StatusBar"); if (statbar) configStatusBar = config_setting_get_bool(statbar); + config_setting_t* kzoom = + config_setting_get_member(config_root_setting(optionConfig), "KeepZoom"); + if (kzoom) configKeepZoom = config_setting_get_bool(kzoom); + + config_setting_t* kpos = + config_setting_get_member(config_root_setting(optionConfig), "KeepPosition"); + if (kpos) configKeepPosition = config_setting_get_bool(kpos); + + config_setting_t* sbpt = + config_setting_get_member(config_root_setting(optionConfig), "StatusBarPageTurn"); + if (sbpt) configStatusBarPageTurn = config_setting_get_bool(sbpt); + return 0; } @@ -264,7 +283,32 @@ static void save_config(unsigned int chosenFolderColor, if (statbar) config_setting_set_bool(statbar, configStatusBar); + config_setting_t* kzoom = + config_setting_get_member(config_root_setting(optionConfig), "KeepZoom"); + if (!kzoom) + kzoom = config_setting_add(config_root_setting(optionConfig), "KeepZoom", + CONFIG_TYPE_BOOL); + if (kzoom) + config_setting_set_bool(kzoom, configKeepZoom); + + config_setting_t* kpos = + config_setting_get_member(config_root_setting(optionConfig), "KeepPosition"); + if (!kpos) + kpos = config_setting_add(config_root_setting(optionConfig), "KeepPosition", + CONFIG_TYPE_BOOL); + if (kpos) + config_setting_set_bool(kpos, configKeepPosition); + + config_setting_t* sbpt = + config_setting_get_member(config_root_setting(optionConfig), "StatusBarPageTurn"); + if (!sbpt) + sbpt = config_setting_add(config_root_setting(optionConfig), "StatusBarPageTurn", + CONFIG_TYPE_BOOL); + if (sbpt) + config_setting_set_bool(sbpt, configStatusBarPageTurn); + config_write_file(optionConfig, optionFile); + fsdevCommitDevice("sdmc"); } // Natural sort: compare strings so that embedded numbers sort numerically. @@ -729,13 +773,24 @@ static unsigned char* extract_pdf_cover_pixels(const char* path, int tw, int th, fz_throw(bg_ctx, FZ_ERROR_GENERIC, "no pages"); page = fz_load_page(bg_ctx, doc, 0); fz_rect bounds = fz_bound_page(bg_ctx, page); - float scale = fminf((float)tw / bounds.x1, (float)th / bounds.y1); + float pw = bounds.x1 - bounds.x0; + float ph = bounds.y1 - bounds.y0; + if (pw <= 0.0f || ph <= 0.0f) + fz_throw(bg_ctx, FZ_ERROR_GENERIC, "invalid page bounds"); + float scale = fminf((float)tw / pw, (float)th / ph); pix = fz_new_pixmap_from_page_contents( bg_ctx, page, fz_scale(scale, scale), fz_device_rgb(bg_ctx), 1); - size_t nbytes = (size_t)pix->h * pix->stride; - result = (unsigned char*)malloc(nbytes); - if (result) { memcpy(result, pix->samples, nbytes); } - *out_iw = pix->w; *out_ih = pix->h; + int stride = pix->stride; + int w = pix->w; + int h = pix->h; + int row_bytes = w * 4; + result = (unsigned char*)malloc((size_t)w * h * 4); + if (result) { + for (int y = 0; y < h; y++) { + memcpy(result + (size_t)y * row_bytes, pix->samples + (size_t)y * stride, row_bytes); + } + } + *out_iw = w; *out_ih = h; } fz_catch(bg_ctx) { result = nullptr; } @@ -855,7 +910,7 @@ void Menu_StartChoosing() { bool drawOption = false; int option_index = 0; - int amountOfOptions = 6; + int amountOfOptions = 9; string path = "/switch/WookReader"; @@ -925,11 +980,15 @@ void Menu_StartChoosing() { // Load saved progress (last page + total pages) for all book entries. auto load_entry_progress = [&]() { - if (!config) { - config = (config_t*)malloc(sizeof(config_t)); - config_init(config); - config_read_file(config, configFile); + if (config) { + config_destroy(config); + free(config); + config = nullptr; } + config = (config_t*)malloc(sizeof(config_t)); + config_init(config); + int read_rc = config_read_file(config, configFile); + Log_Write("load_entry_progress: read saved_pages.cfg, result=" + to_string(read_rc)); int numBooks = (int)sorted_entries.size() - numFolders; entry_progress.assign(numBooks, {0, 0}); for (int i = 0; i < numBooks; i++) { @@ -937,7 +996,10 @@ void Menu_StartChoosing() { int last = 0, total = 0; config_setting_t* s = config_setting_get_member(config_root_setting(config), key.c_str()); - if (s) last = config_setting_get_int(s); + if (s) { + last = config_setting_get_int(s); + Log_Write("load_entry_progress: key=" + key + " loaded last_page=" + to_string(last)); + } string tkey = key + "_T"; config_setting_t* st = config_setting_get_member(config_root_setting(config), tkey.c_str()); @@ -1110,6 +1172,15 @@ void Menu_StartChoosing() { case 5: configStatusBar = !configStatusBar; break; + case 6: + configKeepZoom = !configKeepZoom; + break; + case 7: + configKeepPosition = !configKeepPosition; + break; + case 8: + configStatusBarPageTurn = !configStatusBarPageTurn; + break; default: break; } } @@ -1137,6 +1208,7 @@ void Menu_StartChoosing() { update_recent(book); Menu_OpenBook((char*)book.c_str(), scroll_speed, zoom_amount); if (inRecentFolder) enter_recent(); + else load_entry_progress(); } else { isWarningOnScreen = true; } @@ -1148,6 +1220,7 @@ void Menu_StartChoosing() { update_recent(book); Menu_OpenBook((char*)book.c_str(), scroll_speed, zoom_amount); if (inRecentFolder) enter_recent(); + else load_entry_progress(); } } } @@ -1227,6 +1300,15 @@ void Menu_StartChoosing() { case 5: configStatusBar = !configStatusBar; break; + case 6: + configKeepZoom = !configKeepZoom; + break; + case 7: + configKeepPosition = !configKeepPosition; + break; + case 8: + configStatusBarPageTurn = !configStatusBarPageTurn; + break; default: break; } } else if (!isWarningOnScreen && chosen_index >= numFolders) { @@ -1267,6 +1349,15 @@ void Menu_StartChoosing() { case 5: configStatusBar = !configStatusBar; break; + case 6: + configKeepZoom = !configKeepZoom; + break; + case 7: + configKeepPosition = !configKeepPosition; + break; + case 8: + configStatusBarPageTurn = !configStatusBarPageTurn; + break; default: break; } } else if (!isWarningOnScreen && chosen_index >= numFolders) { @@ -1428,6 +1519,7 @@ void Menu_StartChoosing() { update_recent(book); Menu_OpenBook((char*)book.c_str(), scroll_speed, zoom_amount); if (inRecentFolder) enter_recent(); + else load_entry_progress(); } } } @@ -1516,6 +1608,26 @@ void Menu_StartChoosing() { string display_path = path.size() > 18 ? path.substr(18) : ""; SDL_DrawText(RENDERER, ROBOTO_25, 10, windowY - 40, textColor, display_path.c_str()); + + if (chosen_index >= numFolders) { + int bi = chosen_index - numFolders; + if (bi >= 0 && bi < (int)entry_progress.size()) { + int last_pg = entry_progress[bi].first; + int total_pg = entry_progress[bi].second; + if (total_pg > 0) { + int percent = (total_pg > 1) ? (int)((float)last_pg / (total_pg - 1) * 100.0f) : 100; + if (percent > 100) percent = 100; + if (percent < 0) percent = 0; + int display_page = last_pg + 1; + if (display_page > total_pg) display_page = total_pg; + char prog_buf[128]; + snprintf(prog_buf, sizeof(prog_buf), "Progress: %d%% (Page %d/%d)", percent, display_page, total_pg); + SDL_DrawText(RENDERER, ROBOTO_20, 10, windowY - 18, textColor, prog_buf); + } else { + SDL_DrawText(RENDERER, ROBOTO_20, 10, windowY - 18, textColor, "Progress: Not started"); + } + } + } } // Clip content area to avoid overflowing into the bottom bar @@ -1631,6 +1743,27 @@ void Menu_StartChoosing() { SDL_Rect nb = { cell_x + 4, cell_y + 4, 12, 12 }; SDL_RenderFillRect(RENDERER, &nb); } + + // Reading progress bar (drawn at the bottom of the thumbnail/cell) + if (bi >= 0 && bi < (int)entry_progress.size()) { + int last_pg = entry_progress[bi].first; + int total_pg = entry_progress[bi].second; + if (total_pg > 0) { + float pct = (float)last_pg / total_pg; + if (pct > 1.0f) pct = 1.0f; + if (pct < 0.0f) pct = 0.0f; + int pbar_h = 6; + int pbar_y = dy + dh - pbar_h; + SDL_SetRenderDrawBlendMode(RENDERER, SDL_BLENDMODE_BLEND); + SDL_SetRenderDrawColor(RENDERER, 80, 80, 80, 200); + SDL_Rect pbar_bg = { dx, pbar_y, dw, pbar_h }; + SDL_RenderFillRect(RENDERER, &pbar_bg); + SDL_SetRenderDrawColor(RENDERER, 0, 180, 160, 255); // Nice teal + SDL_Rect pbar_fill = { dx, pbar_y, (int)(dw * pct), pbar_h }; + SDL_RenderFillRect(RENDERER, &pbar_fill); + SDL_SetRenderDrawBlendMode(RENDERER, SDL_BLENDMODE_NONE); + } + } } // ── Empty folder label ──────────────────────────────────────────────────── @@ -1673,7 +1806,7 @@ void Menu_StartChoosing() { if (drawOption) { int helpWidth = 680; - int helpHeight = 395; + int helpHeight = 515; if (!configDarkMode) SDL_DrawRect(RENDERER, 0, 0, 1280, 720, SDL_MakeColour(50, 50, 50, 150)); @@ -1722,6 +1855,21 @@ void Menu_StartChoosing() { textColor, "Status Bar: "); SDL_DrawText(RENDERER, ROBOTO_25, optTextX + 400, optTextY + 38 * 5, textColor, configStatusBar ? "On" : "Off"); + + SDL_DrawText(RENDERER, ROBOTO_25, optTextX, optTextY + 38 * 6, + textColor, "Keep Zoom: "); + SDL_DrawText(RENDERER, ROBOTO_25, optTextX + 400, optTextY + 38 * 6, + textColor, configKeepZoom ? "On" : "Off"); + + SDL_DrawText(RENDERER, ROBOTO_25, optTextX, optTextY + 38 * 7, + textColor, "Keep Position: "); + SDL_DrawText(RENDERER, ROBOTO_25, optTextX + 400, optTextY + 38 * 7, + textColor, configKeepPosition ? "On" : "Off"); + + SDL_DrawText(RENDERER, ROBOTO_25, optTextX, optTextY + 38 * 8, + textColor, "Page Turn Status: "); + SDL_DrawText(RENDERER, ROBOTO_25, optTextX + 400, optTextY + 38 * 8, + textColor, configStatusBarPageTurn ? "On" : "Off"); } // ── Notes overlay ───────────────────────────────────────────────────────── @@ -1789,4 +1937,10 @@ void Menu_StartChoosing() { free(optionConfig); optionConfig = nullptr; } + + if (config) { + config_destroy(config); + free(config); + config = nullptr; + } } diff --git a/source/menus/book/BookReader.cpp b/source/menus/book/BookReader.cpp index 26dec03..4d29b53 100644 --- a/source/menus/book/BookReader.cpp +++ b/source/menus/book/BookReader.cpp @@ -54,19 +54,30 @@ static void notes_save(const char* book_name, const std::string& text) { std::string path = notes_path(book_name); if (text.empty()) { remove(path.c_str()); + fsdevCommitDevice("sdmc"); return; } FILE* f = fopen(path.c_str(), "w"); if (!f) return; fwrite(text.data(), 1, text.size(), f); fclose(f); + fsdevCommitDevice("sdmc"); } static int load_last_page(const char* book_name) { if (!config) { config = (config_t*)malloc(sizeof(config_t)); config_init(config); - config_read_file(config, configFile); + if (config_read_file(config, configFile) == CONFIG_FALSE) { + if (config_error_type(config) == CONFIG_ERR_PARSE) { + Log_Error("load_last_page: config parse error: " + + std::string(config_error_text(config)) + " at line " + + std::to_string(config_error_line(config))); + remove(configFile); + config_destroy(config); + config_init(config); + } + } } config_setting_t* setting = @@ -90,7 +101,9 @@ static void save_last_page(const char* book_name, int current_page) { if (setting) { config_setting_set_int(setting, current_page); - config_write_file(config, configFile); + int rc = config_write_file(config, configFile); + fsdevCommitDevice("sdmc"); + Log_Write("save_last_page: book=" + std::string(book_name) + " page=" + std::to_string(current_page) + " write_result=" + std::to_string(rc)); } } @@ -105,9 +118,110 @@ static void save_total_pages(const char* book_name, int total) { if (setting) { config_setting_set_int(setting, total); config_write_file(config, configFile); + fsdevCommitDevice("sdmc"); + } +} + +static float load_saved_zoom(const char* book_name) { + std::string key = std::string(book_name) + "_Z"; + config_setting_t* setting = + config_setting_get_member(config_root_setting(config), key.c_str()); + if (setting) { + return (float)config_setting_get_float(setting); + } + return 1.0f; +} + +static void get_saved_position(const char* book_name, float& x, float& y) { + std::string key_x = std::string(book_name) + "_X"; + std::string key_y = std::string(book_name) + "_Y"; + config_setting_t* setting_x = + config_setting_get_member(config_root_setting(config), key_x.c_str()); + config_setting_t* setting_y = + config_setting_get_member(config_root_setting(config), key_y.c_str()); + if (setting_x && setting_y) { + x = (float)config_setting_get_float(setting_x); + y = (float)config_setting_get_float(setting_y); + } else { + x = 0.0f; + y = 0.0f; } } +static BookPageLayout load_saved_orientation(const char* book_name) { + std::string key = std::string(book_name) + "_O"; + config_setting_t* setting = + config_setting_get_member(config_root_setting(config), key.c_str()); + if (setting) { + int val = config_setting_get_int(setting); + if (val >= 0 && val <= 2) { + return (BookPageLayout)val; + } + } + return BookPageLayoutPortrait; +} + +static void get_saved_rotation_and_spread(const char* book_name, int& rot, bool& spread) { + std::string key_r = std::string(book_name) + "_ROT"; + std::string key_s = std::string(book_name) + "_SPR"; + config_setting_t* setting_r = + config_setting_get_member(config_root_setting(config), key_r.c_str()); + config_setting_t* setting_s = + config_setting_get_member(config_root_setting(config), key_s.c_str()); + if (setting_r) { + rot = config_setting_get_int(setting_r); + } else { + rot = 0; + } + if (setting_s) { + spread = (config_setting_get_int(setting_s) != 0); + } else { + spread = false; + } +} + +static void save_view_state(const char* book_name, float zoom, float x, float y, BookPageLayout orient, int rot, bool spread) { + std::string key_z = std::string(book_name) + "_Z"; + std::string key_x = std::string(book_name) + "_X"; + std::string key_y = std::string(book_name) + "_Y"; + std::string key_o = std::string(book_name) + "_O"; + std::string key_r = std::string(book_name) + "_ROT"; + std::string key_s = std::string(book_name) + "_SPR"; + + // ZOOM + config_setting_t* s_z = config_setting_get_member(config_root_setting(config), key_z.c_str()); + if (!s_z) s_z = config_setting_add(config_root_setting(config), key_z.c_str(), CONFIG_TYPE_FLOAT); + if (s_z) config_setting_set_float(s_z, zoom); + + // X + config_setting_t* s_x = config_setting_get_member(config_root_setting(config), key_x.c_str()); + if (!s_x) s_x = config_setting_add(config_root_setting(config), key_x.c_str(), CONFIG_TYPE_FLOAT); + if (s_x) config_setting_set_float(s_x, x); + + // Y + config_setting_t* s_y = config_setting_get_member(config_root_setting(config), key_y.c_str()); + if (!s_y) s_y = config_setting_add(config_root_setting(config), key_y.c_str(), CONFIG_TYPE_FLOAT); + if (s_y) config_setting_set_float(s_y, y); + + // ORIENTATION + config_setting_t* s_o = config_setting_get_member(config_root_setting(config), key_o.c_str()); + if (!s_o) s_o = config_setting_add(config_root_setting(config), key_o.c_str(), CONFIG_TYPE_INT); + if (s_o) config_setting_set_int(s_o, (int)orient); + + // ROTATION + config_setting_t* s_r = config_setting_get_member(config_root_setting(config), key_r.c_str()); + if (!s_r) s_r = config_setting_add(config_root_setting(config), key_r.c_str(), CONFIG_TYPE_INT); + if (s_r) config_setting_set_int(s_r, rot); + + // SPREAD + config_setting_t* s_s = config_setting_get_member(config_root_setting(config), key_s.c_str()); + if (!s_s) s_s = config_setting_add(config_root_setting(config), key_s.c_str(), CONFIG_TYPE_INT); + if (s_s) config_setting_set_int(s_s, spread ? 1 : 0); + + config_write_file(config, configFile); + fsdevCommitDevice("sdmc"); +} + // Returns true if the path is a comic archive (CBZ/CBR/CBT/CB7, // case-insensitive) static bool path_is_cbz(const char* path) { @@ -124,13 +238,21 @@ BookReader::BookReader(const char* path, int* result) { SDL_GetWindowSize(WINDOW, &windowX, &windowY); book_path = path; - book_name = + std::string filename = std::string(path).substr(std::string(path).find_last_of("/\\") + 1); - std::string invalid_chars = " :/?#[]@!$&'()*+,;=."; - for (char& c : invalid_chars) - book_name.erase(std::remove(book_name.begin(), book_name.end(), c), - book_name.end()); + std::string sanitized = ""; + for (char c : filename) { + if (isalnum((unsigned char)c) || c == '-' || c == '_') { + sanitized += c; + } + } + if (!sanitized.empty() && !isalpha((unsigned char)sanitized[0])) { + sanitized = "b" + sanitized; + } + book_name = sanitized; + + Log_Write("BookReader: book_name resolved to: " + book_name); _notes = notes_load(book_name.c_str()); @@ -139,12 +261,28 @@ BookReader::BookReader(const char* path, int* result) { if (_is_cbz) { Log_Write(std::string("BookReader: opening as comic ZIP: ") + path); int current_page = load_last_page(book_name.c_str()); + _currentPageLayout = load_saved_orientation(book_name.c_str()); switch_current_page_layout(_currentPageLayout, current_page); if (!layout) { Log_Error(std::string("BookReader: CBZ/CBR layout creation failed: ") + path); *result = -1; return; } + // Set saved rotation, spread, zoom and position if they exist + int rot = 0; + bool spread = false; + get_saved_rotation_and_spread(book_name.c_str(), rot, spread); + layout->set_rotation_and_spread(rot, spread); + + std::string key_z = std::string(book_name) + "_Z"; + config_setting_t* setting_z = + config_setting_get_member(config_root_setting(config), key_z.c_str()); + if (setting_z) { + float z = (float)config_setting_get_float(setting_z); + float x = 0.0f, y = 0.0f; + get_saved_position(book_name.c_str(), x, y); + layout->set_zoom_and_position(z, x, y); + } // Layout was created — enumeration may still be running in background. // Validity is checked in draw() after enumeration completes. if (current_page > 0) show_status_bar(); @@ -179,7 +317,7 @@ BookReader::BookReader(const char* path, int* result) { } int current_page = load_last_page(book_name.c_str()); - + _currentPageLayout = load_saved_orientation(book_name.c_str()); switch_current_page_layout(_currentPageLayout, current_page); if (!layout) { @@ -189,6 +327,18 @@ BookReader::BookReader(const char* path, int* result) { return; } + // Set saved zoom and position if they exist + std::string key_z = std::string(book_name) + "_Z"; + config_setting_t* setting_z = + config_setting_get_member(config_root_setting(config), key_z.c_str()); + if (setting_z) { + float z = (float)config_setting_get_float(setting_z); + float x = 0.0f, y = 0.0f; + get_saved_position(book_name.c_str(), x, y); + layout->set_zoom_and_position(z, x, y); + layout->render_page_to_texture(current_page, false); + } + Log_Write("BookReader: MuPDF opened OK, starting page=" + std::to_string(current_page)); if (current_page > 0) show_status_bar(); @@ -197,13 +347,14 @@ BookReader::BookReader(const char* path, int* result) { save_total_pages(book_name.c_str(), _total_pages); } fz_catch(ctx) { - Log_Error(std::string("BookReader: fz_catch on open: ") + path); + Log_Error(std::string("BookReader: fz_catch on open: ") + path + " - error: " + fz_caught_message(ctx)); *result = -2; return; } } BookReader::~BookReader() { + save_progress(); if (_nav_tex_left) { SDL_DestroyTexture(_nav_tex_left); _nav_tex_left = nullptr; } if (_nav_tex_right) { SDL_DestroyTexture(_nav_tex_right); _nav_tex_right = nullptr; } if (_nav_tex_up) { SDL_DestroyTexture(_nav_tex_up); _nav_tex_up = nullptr; } @@ -229,21 +380,27 @@ BookReader::~BookReader() { void BookReader::previous_page(int n) { if (!layout) return; layout->previous_page(n); - show_status_bar(); + if (configStatusBarPageTurn) { + show_status_bar(); + } save_progress(); } void BookReader::next_page(int n) { if (!layout) return; layout->next_page(n); - show_status_bar(); + if (configStatusBarPageTurn) { + show_status_bar(); + } save_progress(); } void BookReader::goto_page(int page_1indexed) { if (!layout) return; layout->goto_page(page_1indexed - 1); // convert 1-indexed → 0-indexed - show_status_bar(); + if (configStatusBarPageTurn) { + show_status_bar(); + } save_progress(); } @@ -495,9 +652,9 @@ void BookReader::draw(bool drawHelp, bool drawNotes) { StatusBar_DisplayTime(false); } else if (_currentPageLayout == BookPageLayoutLandscape) { - SDL_DrawRect(RENDERER, 1280 - 45, 0, 45, 720, SDL_MakeColour(0, 0, 0, 180)); - int x = (1280 - title_width) - ((40 - title_height) / 2); - int y = (720 - title_height) / 2; + SDL_DrawRect(RENDERER, 0, 0, 45, 720, SDL_MakeColour(0, 0, 0, 180)); + int x = 45 - ((45 - title_height) / 2); + int y = (720 - title_width) / 2; SDL_DrawRotatedText(RENDERER, ROBOTO_25, (double)90, x, y, WHITE, title); @@ -566,8 +723,13 @@ void BookReader::reset_nav_buttons() { _btn_hide_at = SDL_GetTicks() + 3000; } void BookReader::save_progress() { if (!layout) return; save_last_page(book_name.c_str(), layout->current_page()); - // Total pages is invariant while a book is open; written once at open time. - // Removed from here to eliminate the double SD write per page navigation. + + float z = layout->get_zoom(); + float x = 0.0f, y = 0.0f; + layout->get_position(x, y); + int rot = layout->get_rotation(); + bool spread = layout->get_spread_mode(); + save_view_state(book_name.c_str(), z, x, y, _currentPageLayout, rot, spread); } void BookReader::switch_current_page_layout(BookPageLayout bookPageLayout, diff --git a/source/menus/book/CBZPageLayout.cpp b/source/menus/book/CBZPageLayout.cpp index 49bf5d0..55e2448 100644 --- a/source/menus/book/CBZPageLayout.cpp +++ b/source/menus/book/CBZPageLayout.cpp @@ -591,9 +591,19 @@ void CBZPageLayout::apply_first_image() _min_zoom = std::min((float)_viewport.w / ew, (float)_viewport.h / eh); _max_zoom = std::max((float)_viewport.w / ew, (float)_viewport.h / eh) * 2.0f; _max_zoom = std::max(_max_zoom, _min_zoom * 4.0f); - _zoom = _min_zoom; - _cx = _viewport.w / 2.0f; - _cy = eh * _zoom / 2.0f; + if (_has_saved_view) + { + _zoom = std::fmin(std::fmax(_saved_zoom, _min_zoom), _max_zoom); + _cx = _saved_cx; + _cy = _saved_cy; + _has_saved_view = false; + } + else + { + _zoom = _min_zoom; + _cx = _viewport.w / 2.0f; + _cy = eh * _zoom / 2.0f; + } _current_page = 0; _page_count = 1; // placeholder until finish_enumeration() knows the real count _valid = true; @@ -720,7 +730,7 @@ void CBZPageLayout::finish_enumeration() FreeTextureIfNeeded(&_tex); FreeTextureIfNeeded(&_tex_r); int clamped = std::max(0, std::min(_start_page, _page_count - 1)); - load_page_texture(clamped, true); + render_page_to_texture(clamped, true); _valid = true; } } @@ -760,7 +770,7 @@ CBZPageLayout::CBZPageLayout(const char *path, int start_page) Log_Write("CBZPageLayout: opened OK (sync), pages=" + std::to_string(_page_count) + " path=" + path); int clamped = std::max(0, std::min(start_page, _page_count - 1)); - load_page_texture(clamped, true); + render_page_to_texture(clamped, true); _valid = true; } @@ -814,8 +824,28 @@ static SDL_Texture *load_one_page_texture(const char *archive_path, return tex; } -void CBZPageLayout::load_page_texture(int page_num, bool reset_zoom) +void CBZPageLayout::render_page_to_texture(int page_num, bool reset_zoom) { + float zoom_factor = 1.0f; + if (_min_zoom > 0.0f) + zoom_factor = _zoom / _min_zoom; + + float eff_w_old = (_rotation == 90) ? (float)_tex_h : (float)_tex_w; + float eff_h_old = (_rotation == 90) ? (float)_tex_w : (float)_tex_h; + float w_old = eff_w_old * _zoom; + float h_old = eff_h_old * _zoom; + + float frac_x = 0.5f; + if (w_old > _viewport.w) { + frac_x = (w_old / 2.0f - _cx) / (w_old - _viewport.w); + frac_x = std::fmin(std::fmax(frac_x, 0.0f), 1.0f); + } + float frac_y = 0.0f; // default to top of page + if (h_old > _viewport.h) { + frac_y = (h_old / 2.0f - _cy) / (h_old - _viewport.h); + frac_y = std::fmin(std::fmax(frac_y, 0.0f), 1.0f); + } + FreeTextureIfNeeded(&_tex); FreeTextureIfNeeded(&_tex_r); @@ -923,19 +953,53 @@ void CBZPageLayout::load_page_texture(int page_num, bool reset_zoom) _tex_h = h; } - if (reset_zoom) + float eff_w = (_rotation == 90) ? (float)_tex_h : (float)_tex_w; + float eff_h = (_rotation == 90) ? (float)_tex_w : (float)_tex_h; + _min_zoom = std::min((float)_viewport.w / eff_w, + (float)_viewport.h / eff_h); + _max_zoom = std::max((float)_viewport.w / eff_w, + (float)_viewport.h / eff_h) * 2.0f; + _max_zoom = std::max(_max_zoom, _min_zoom * 4.0f); + + bool should_reset_zoom = reset_zoom || !configKeepZoom; + if (_has_saved_view) + { + _zoom = std::fmin(std::fmax(_saved_zoom, _min_zoom), _max_zoom); + _cx = _saved_cx; + _cy = _saved_cy; + _has_saved_view = false; + } + else if (should_reset_zoom) { - float eff_w = (_rotation == 90) ? (float)_tex_h : (float)_tex_w; - float eff_h = (_rotation == 90) ? (float)_tex_w : (float)_tex_h; - _min_zoom = std::min((float)_viewport.w / eff_w, - (float)_viewport.h / eff_h); - _max_zoom = std::max((float)_viewport.w / eff_w, - (float)_viewport.h / eff_h) * 2.0f; - _max_zoom = std::max(_max_zoom, _min_zoom * 4.0f); _zoom = _min_zoom; _cx = _viewport.w / 2.0f; - _cy = eff_h * _zoom / 2.0f; + _cy = _viewport.h / 2.0f; } + else + { + _zoom = std::fmin(std::fmax(zoom_factor * _min_zoom, _min_zoom), _max_zoom); + + float w_new = eff_w * _zoom; + float h_new = eff_h * _zoom; + + if (configKeepPosition) { + if (w_new > _viewport.w) { + _cx = w_new / 2.0f - frac_x * (w_new - _viewport.w); + } else { + _cx = _viewport.w / 2.0f; + } + if (h_new > _viewport.h) { + _cy = h_new / 2.0f - frac_y * (h_new - _viewport.h); + } else { + _cy = _viewport.h / 2.0f; + } + } else { + _cx = _viewport.w / 2.0f; + _cy = (h_new > _viewport.h) ? (h_new / 2.0f) : (_viewport.h / 2.0f); + } + } + + clamp_center(); // Start async prefetch: N+1 (slot 0/core 1), N-1 (slot 1/core 2), N+2 (slot 2/core 3) if (!do_spread) @@ -1104,12 +1168,14 @@ void CBZPageLayout::clamp_center() { float vw = ((_rotation == 90 && !_tex_r) ? (float)_tex_h : (float)_tex_w) * _zoom; float vh = ((_rotation == 90 && !_tex_r) ? (float)_tex_w : (float)_tex_h) * _zoom; - { - float cx_lo = std::min(vw / 2.0f, _viewport.w - vw / 2.0f); - float cx_hi = std::max(vw / 2.0f, _viewport.w - vw / 2.0f); - _cx = std::fmin(std::fmax(_cx, cx_lo), cx_hi); - } - _cy = std::fmin(std::fmax(_cy, _viewport.h - vh / 2.0f), vh / 2.0f); + + float cx_lo = std::min(vw / 2.0f, (float)_viewport.w - vw / 2.0f); + float cx_hi = std::max(vw / 2.0f, (float)_viewport.w - vw / 2.0f); + _cx = std::fmin(std::fmax(_cx, cx_lo), cx_hi); + + float cy_lo = std::min(vh / 2.0f, (float)_viewport.h - vh / 2.0f); + float cy_hi = std::max(vh / 2.0f, (float)_viewport.h - vh / 2.0f); + _cy = std::fmin(std::fmax(_cy, cy_lo), cy_hi); } void CBZPageLayout::apply_zoom(float v) @@ -1215,10 +1281,33 @@ void CBZPageLayout::previous_page(int n) if (new_page == _current_page) return; + float zoom_factor = 1.0f; + if (_min_zoom > 0.0f) + zoom_factor = _zoom / _min_zoom; + // Try instant swap from ready texture (single-page, step==1 only) if (!_spread_mode && step == 1 && _ready_prev && _ready_prev_pg == new_page) { + // 1. Calculate old scroll fractions before page dimensions change + float eff_w_old = (_rotation == 90) ? (float)_tex_h : (float)_tex_w; + float eff_h_old = (_rotation == 90) ? (float)_tex_w : (float)_tex_h; + float w_old = eff_w_old * _zoom; + float h_old = eff_h_old * _zoom; + + float frac_x = 0.5f; + if (w_old > _viewport.w) { + frac_x = (w_old / 2.0f - _cx) / (w_old - _viewport.w); + frac_x = std::fmin(std::fmax(frac_x, 0.0f), 1.0f); + } + float frac_y = 0.0f; // default to top of page + if (h_old > _viewport.h) { + frac_y = (h_old / 2.0f - _cy) / (h_old - _viewport.h); + frac_y = std::fmin(std::fmax(frac_y, 0.0f), 1.0f); + } + + _current_page = new_page; + // Recycle old current texture as the new _ready_next (instant forward-nav) FreeTextureIfNeeded(&_ready_next); FreeTextureIfNeeded(&_tex_r); @@ -1237,8 +1326,6 @@ void CBZPageLayout::previous_page(int n) _ready_prev_w = 0; _ready_prev_h = 0; - _current_page = new_page; - // Reset zoom for the new page dimensions float eff_w = (_rotation == 90) ? (float)_tex_h : (float)_tex_w; float eff_h = (_rotation == 90) ? (float)_tex_w : (float)_tex_h; @@ -1247,9 +1334,38 @@ void CBZPageLayout::previous_page(int n) _max_zoom = std::max((float)_viewport.w / eff_w, (float)_viewport.h / eff_h) * 2.0f; _max_zoom = std::max(_max_zoom, _min_zoom * 4.0f); - _zoom = _min_zoom; - _cx = _viewport.w / 2.0f; - _cy = eff_h * _zoom / 2.0f; + + bool should_reset_zoom = !configKeepZoom; + if (should_reset_zoom) + { + _zoom = _min_zoom; + _cx = _viewport.w / 2.0f; + _cy = _viewport.h / 2.0f; + } + else + { + _zoom = std::fmin(std::fmax(zoom_factor * _min_zoom, _min_zoom), _max_zoom); + + float w_new = eff_w * _zoom; + float h_new = eff_h * _zoom; + + if (configKeepPosition) { + if (w_new > _viewport.w) { + _cx = w_new / 2.0f - frac_x * (w_new - _viewport.w); + } else { + _cx = _viewport.w / 2.0f; + } + if (h_new > _viewport.h) { + _cy = h_new / 2.0f - frac_y * (h_new - _viewport.h); + } else { + _cy = _viewport.h / 2.0f; + } + } else { + _cx = _viewport.w / 2.0f; + _cy = (h_new > _viewport.h) ? (h_new / 2.0f) : (_viewport.h / 2.0f); + } + } + clamp_center(); // Launch new prefetch for N+1 and N-1 int pf_limit = _enumerating ? __atomic_load_n(&_enum_count, __ATOMIC_ACQUIRE) : _page_count; @@ -1262,7 +1378,7 @@ void CBZPageLayout::previous_page(int n) { // Fallback: blocking load free_ready_textures(); - load_page_texture(new_page, true); + render_page_to_texture(new_page, false); } if (_enumerating) @@ -1278,10 +1394,33 @@ void CBZPageLayout::next_page(int n) if (new_page >= limit) return; + float zoom_factor = 1.0f; + if (_min_zoom > 0.0f) + zoom_factor = _zoom / _min_zoom; + // Try instant swap from ready texture (single-page, step==1 only) if (!_spread_mode && step == 1 && _ready_next && _ready_next_pg == new_page) { + // 1. Calculate old scroll fractions before page dimensions change + float eff_w_old = (_rotation == 90) ? (float)_tex_h : (float)_tex_w; + float eff_h_old = (_rotation == 90) ? (float)_tex_w : (float)_tex_h; + float w_old = eff_w_old * _zoom; + float h_old = eff_h_old * _zoom; + + float frac_x = 0.5f; + if (w_old > _viewport.w) { + frac_x = (w_old / 2.0f - _cx) / (w_old - _viewport.w); + frac_x = std::fmin(std::fmax(frac_x, 0.0f), 1.0f); + } + float frac_y = 0.0f; // default to top of page + if (h_old > _viewport.h) { + frac_y = (h_old / 2.0f - _cy) / (h_old - _viewport.h); + frac_y = std::fmin(std::fmax(frac_y, 0.0f), 1.0f); + } + + _current_page = new_page; + // Recycle old current texture as the new _ready_prev (instant back-nav) FreeTextureIfNeeded(&_ready_prev); FreeTextureIfNeeded(&_tex_r); @@ -1300,8 +1439,6 @@ void CBZPageLayout::next_page(int n) _ready_next_w = 0; _ready_next_h = 0; - _current_page = new_page; - // Reset zoom for the new page dimensions float eff_w = (_rotation == 90) ? (float)_tex_h : (float)_tex_w; float eff_h = (_rotation == 90) ? (float)_tex_w : (float)_tex_h; @@ -1310,9 +1447,38 @@ void CBZPageLayout::next_page(int n) _max_zoom = std::max((float)_viewport.w / eff_w, (float)_viewport.h / eff_h) * 2.0f; _max_zoom = std::max(_max_zoom, _min_zoom * 4.0f); - _zoom = _min_zoom; - _cx = _viewport.w / 2.0f; - _cy = eff_h * _zoom / 2.0f; + + bool should_reset_zoom = !configKeepZoom; + if (should_reset_zoom) + { + _zoom = _min_zoom; + _cx = _viewport.w / 2.0f; + _cy = _viewport.h / 2.0f; + } + else + { + _zoom = std::fmin(std::fmax(zoom_factor * _min_zoom, _min_zoom), _max_zoom); + + float w_new = eff_w * _zoom; + float h_new = eff_h * _zoom; + + if (configKeepPosition) { + if (w_new > _viewport.w) { + _cx = w_new / 2.0f - frac_x * (w_new - _viewport.w); + } else { + _cx = _viewport.w / 2.0f; + } + if (h_new > _viewport.h) { + _cy = h_new / 2.0f - frac_y * (h_new - _viewport.h); + } else { + _cy = _viewport.h / 2.0f; + } + } else { + _cx = _viewport.w / 2.0f; + _cy = (h_new > _viewport.h) ? (h_new / 2.0f) : (_viewport.h / 2.0f); + } + } + clamp_center(); // Launch new prefetch for N+1 and N-1 int pf_limit = _enumerating ? __atomic_load_n(&_enum_count, __ATOMIC_ACQUIRE) : _page_count; @@ -1325,7 +1491,7 @@ void CBZPageLayout::next_page(int n) { // Fallback: blocking load (spread mode, multi-step skip, or no ready texture) free_ready_textures(); - load_page_texture(new_page, true); + render_page_to_texture(new_page, false); } if (_enumerating) @@ -1341,7 +1507,7 @@ void CBZPageLayout::goto_page(int num) if (target == _current_page) return; free_ready_textures(); - load_page_texture(target, true); + render_page_to_texture(target, false); if (_enumerating) _enum_current_name = _page_names[_current_page]; } @@ -1363,7 +1529,7 @@ void CBZPageLayout::toggle_spread() _spread_mode = false; } free_ready_textures(); - load_page_texture(_current_page, true); + render_page_to_texture(_current_page, true); } void CBZPageLayout::zoom_in(float zoom_amount) { apply_zoom(_zoom + zoom_amount); } diff --git a/source/menus/book/CBZPageLayout.hpp b/source/menus/book/CBZPageLayout.hpp index e7f002d..e4e1900 100644 --- a/source/menus/book/CBZPageLayout.hpp +++ b/source/menus/book/CBZPageLayout.hpp @@ -32,6 +32,18 @@ class CBZPageLayout : public PageLayout void draw_page() override; char *info() override; + float get_zoom() const override { return _zoom; } + void get_position(float &x, float &y) const override { + x = _cx; + y = _cy; + } + int get_rotation() const override { return _rotation; } + bool get_spread_mode() const override { return _spread_mode; } + void set_rotation_and_spread(int rot, bool spread) override { + _rotation = rot; + _spread_mode = spread; + } + bool is_valid() const { return _valid; } bool is_enumerating() const { return _enumerating && !__atomic_load_n(&_enum_done, __ATOMIC_ACQUIRE); } bool is_enumerating_internal() const { return _enumerating; } // true until finish_enumeration() called @@ -57,9 +69,9 @@ class CBZPageLayout : public PageLayout return vh <= _viewport.h + 0.5f; } + void render_page_to_texture(int num, bool reset_zoom) override; + private: - // Load image at page index from the archive, build SDL_Texture, optionally reset zoom. - void load_page_texture(int page_num, bool reset_zoom); void clamp_center(); void apply_zoom(float new_zoom); void free_ready_textures(); diff --git a/source/menus/book/LandscapePageLayout.cpp b/source/menus/book/LandscapePageLayout.cpp index 85de481..1468104 100644 --- a/source/menus/book/LandscapePageLayout.cpp +++ b/source/menus/book/LandscapePageLayout.cpp @@ -39,6 +39,80 @@ void LandscapePageLayout::move_page(float x, float y) { float w = page_bounds.x1 * zoom, h = page_bounds.y1 * zoom; - page_center.x = fmin(fmax(page_center.x + y, h / 2), viewport.h - h / 2); - page_center.y = fmin(fmax(page_center.y + x, viewport.w - w / 2), w / 2); + float cx_lo = std::min(h / 2.0f, (float)viewport.h - h / 2.0f); + float cx_hi = std::max(h / 2.0f, (float)viewport.h - h / 2.0f); + page_center.x = std::fmin(std::fmax(page_center.x + y, cx_lo), cx_hi); + + float cy_lo = std::min(w / 2.0f, (float)viewport.w - w / 2.0f); + float cy_hi = std::max(w / 2.0f, (float)viewport.w - w / 2.0f); + page_center.y = std::fmin(std::fmax(page_center.y + x, cy_lo), cy_hi); +} + +void LandscapePageLayout::get_scroll_fractions(float &frac_x, float &frac_y) const +{ + float w_old = page_bounds.x1 * zoom; + float h_old = page_bounds.y1 * zoom; + + frac_x = 0.5f; + if (h_old > viewport.h) { + frac_x = (page_center.x - viewport.h + h_old / 2.0f) / (h_old - viewport.h); + frac_x = std::fmin(std::fmax(frac_x, 0.0f), 1.0f); + } + + frac_y = 0.5f; + if (w_old > viewport.w) { + frac_y = (w_old / 2.0f - page_center.y) / (w_old - viewport.w); + frac_y = std::fmin(std::fmax(frac_y, 0.0f), 1.0f); + } +} + +void LandscapePageLayout::apply_position(float old_zoom_factor, bool should_reset_zoom, bool keep_position, float frac_x, float frac_y) +{ + if (_has_saved_view) + { + zoom = std::fmin(std::fmax(_saved_zoom, min_zoom), max_zoom); + page_center.x = _saved_cx; + page_center.y = _saved_cy; + move_page(0, 0); // Clamp to viewport limits + _has_saved_view = false; + } + else if (should_reset_zoom) + { + zoom = min_zoom; + page_center.y = viewport.w / 2.0f; + float h_new = page_bounds.y1 * zoom; + page_center.x = (h_new > viewport.h) ? (viewport.h - h_new / 2.0f) : (viewport.h / 2.0f); + } + else + { + zoom = std::fmin(std::fmax(old_zoom_factor * min_zoom, min_zoom), max_zoom); + + float w_new = page_bounds.x1 * zoom; + float h_new = page_bounds.y1 * zoom; + + if (keep_position) { + // Keep relative scroll position + if (h_new > viewport.h) { + page_center.x = (viewport.h - h_new / 2.0f) + frac_x * (h_new - viewport.h); + } else { + page_center.x = viewport.h / 2.0f; + } + if (w_new > viewport.w) { + page_center.y = w_new / 2.0f - frac_y * (w_new - viewport.w); + } else { + page_center.y = viewport.w / 2.0f; + } + } else { + // Reset position to right-aligned (top of page in rotated view) and Y-centered + page_center.y = viewport.w / 2.0f; + page_center.x = (h_new > viewport.h) ? (viewport.h - h_new / 2.0f) : (viewport.h / 2.0f); + } + } +} + +void LandscapePageLayout::top_of_page() +{ + float h_new = page_bounds.y1 * zoom; + page_center.y = viewport.w / 2.0f; + page_center.x = (h_new > viewport.h) ? (viewport.h - h_new / 2.0f) : (viewport.h / 2.0f); } diff --git a/source/menus/book/LandscapePageLayout.hpp b/source/menus/book/LandscapePageLayout.hpp index bb3aa18..7541395 100644 --- a/source/menus/book/LandscapePageLayout.hpp +++ b/source/menus/book/LandscapePageLayout.hpp @@ -13,7 +13,10 @@ class LandscapePageLayout : public PageLayout virtual void next_page(int n); protected: - void move_page(float x, float y); + void move_page(float x, float y) override; + void get_scroll_fractions(float &frac_x, float &frac_y) const override; + void apply_position(float old_zoom_factor, bool should_reset_zoom, bool keep_position, float frac_x, float frac_y) override; + void top_of_page() override; }; #endif diff --git a/source/menus/book/PageLayout.cpp b/source/menus/book/PageLayout.cpp index 4f3c4cd..ae65089 100644 --- a/source/menus/book/PageLayout.cpp +++ b/source/menus/book/PageLayout.cpp @@ -10,6 +10,8 @@ extern "C" #include "SDL_helper.h" } +static bool get_page_bounds(fz_context *c, fz_document *d, int num, fz_rect *out_bounds); + PageLayout::PageLayout(fz_document *doc, int current_page) : doc(doc), pdf(ctx ? pdf_specifics(ctx, doc) : nullptr), pages_count(ctx && doc ? fz_count_pages(ctx, doc) : 0) { if (!ctx || !doc) @@ -93,13 +95,15 @@ void PageLayout::previous_page(int n) void PageLayout::next_page(int n) { render_page_to_texture(_current_page + n, false); - top_of_page(); + if (!configKeepPosition) + top_of_page(); } void PageLayout::goto_page(int num) { render_page_to_texture(num, false); // clamps 0..pages_count-1 internally - top_of_page(); + if (!configKeepPosition) + top_of_page(); } void PageLayout::zoom_in(float zoom_amount) @@ -256,6 +260,45 @@ void PageLayout::cancel_bg_render() void PageLayout::poll_bg_render() { + // Defer current page rasterization during zoom gesture + if (zoom_dirty && (SDL_GetTicks() - last_zoom_time > 250)) + { + zoom_dirty = false; + + // Invalidate prefetch cache since they are at the old zoom + cancel_pf_slot(_pf_fwd, &_pf_fwd_tex, &_pf_fwd_page, &_pf_fwd_zoom); + cancel_pf_slot(_pf_bwd, &_pf_bwd_tex, &_pf_bwd_page, &_pf_bwd_zoom); + + if (_bg.bg_ctx && _bg.bg_doc) + { + start_bg_render(_current_page, zoom); + } + else + { + // Synchronous fallback + FreeTextureIfNeeded(&page_texture); + fz_page *page = nullptr; + fz_pixmap *pix = nullptr; + fz_try(ctx) + { + page = fz_load_page(ctx, doc, _current_page); + pix = fz_new_pixmap_from_page_contents(ctx, page, fz_scale(zoom, zoom), fz_device_rgb(ctx), 0); + if (configDarkMode) fz_invert_pixmap(ctx, pix); + SDL_Surface *img = SDL_CreateRGBSurfaceFrom(pix->samples, pix->w, pix->h, pix->n * 8, pix->w * pix->n, 0x000000FF, 0x0000FF00, 0x00FF0000, 0); + page_texture = SDL_CreateTextureFromSurface(RENDERER, img); + SDL_FreeSurface(img); + fz_drop_pixmap(ctx, pix); + fz_drop_page(ctx, page); + rendered_zoom = zoom; + } + fz_catch(ctx) + { + fz_drop_pixmap(ctx, pix); + fz_drop_page(ctx, page); + } + } + } + // Poll current-page render if (_bg.active && __atomic_load_n(&_bg.done, __ATOMIC_ACQUIRE)) { @@ -282,6 +325,7 @@ void PageLayout::poll_bg_render() { FreeTextureIfNeeded(&page_texture); page_texture = new_tex; + rendered_zoom = _bg.target_zoom; } } @@ -361,6 +405,25 @@ void PageLayout::poll_pf_slot(BgRender& slot, SDL_Texture **tex, int *page_ref, } } +float PageLayout::get_min_zoom_for_bounds(const fz_rect &bounds) const +{ + if (bounds.x1 <= 0.0f || bounds.y1 <= 0.0f) return 1.0f; + return fmin(viewport.w / bounds.x1, viewport.h / bounds.y1); +} + +float PageLayout::get_max_zoom_for_bounds(const fz_rect &bounds, float min_z) const +{ + if (bounds.x1 <= 0.0f || bounds.y1 <= 0.0f) return 4.0f; + float val = fmax(viewport.w / bounds.x1, viewport.h / bounds.y1); + return fmax(val, min_z * 4.0f); +} + +void PageLayout::update_min_max_zoom() +{ + min_zoom = get_min_zoom_for_bounds(page_bounds); + max_zoom = get_max_zoom_for_bounds(page_bounds, min_zoom); +} + void PageLayout::start_prefetch() { if (!_pf_fwd.bg_ctx || !_pf_bwd.bg_ctx) return; @@ -368,24 +431,48 @@ void PageLayout::start_prefetch() int target_fwd = _current_page + 1; int target_bwd = _current_page - 1; + float zoom_factor = 1.0f; + if (min_zoom > 0.0f) + zoom_factor = zoom / min_zoom; + // Forward: skip if already ready or already rendering the right page at the right zoom - bool fwd_ready = (_pf_fwd_tex && _pf_fwd_page == target_fwd && fabsf(_pf_fwd_zoom - zoom) < 0.001f); - bool fwd_running = (_pf_fwd.active && _pf_fwd.target_page == target_fwd && fabsf(_pf_fwd.target_zoom - zoom) < 0.001f); + float target_fwd_zoom = zoom; + if (target_fwd < pages_count) { + fz_rect bounds = fz_empty_rect; + if (get_page_bounds(ctx, doc, target_fwd, &bounds)) { + float t_min = get_min_zoom_for_bounds(bounds); + float t_max = get_max_zoom_for_bounds(bounds, t_min); + target_fwd_zoom = std::fmin(std::fmax(zoom_factor * t_min, t_min), t_max); + } + } + + bool fwd_ready = (_pf_fwd_tex && _pf_fwd_page == target_fwd && fabsf(_pf_fwd_zoom - target_fwd_zoom) < 0.001f); + bool fwd_running = (_pf_fwd.active && _pf_fwd.target_page == target_fwd && fabsf(_pf_fwd.target_zoom - target_fwd_zoom) < 0.001f); if (!fwd_ready && !fwd_running) { cancel_pf_slot(_pf_fwd, &_pf_fwd_tex, &_pf_fwd_page, &_pf_fwd_zoom); if (target_fwd < pages_count) - start_pf_slot(_pf_fwd, target_fwd, zoom, pf_fwd_thread_entry, 1); + start_pf_slot(_pf_fwd, target_fwd, target_fwd_zoom, pf_fwd_thread_entry, 1); } // Backward: same logic - bool bwd_ready = (_pf_bwd_tex && _pf_bwd_page == target_bwd && fabsf(_pf_bwd_zoom - zoom) < 0.001f); - bool bwd_running = (_pf_bwd.active && _pf_bwd.target_page == target_bwd && fabsf(_pf_bwd.target_zoom - zoom) < 0.001f); + float target_bwd_zoom = zoom; + if (target_bwd >= 0) { + fz_rect bounds = fz_empty_rect; + if (get_page_bounds(ctx, doc, target_bwd, &bounds)) { + float t_min = get_min_zoom_for_bounds(bounds); + float t_max = get_max_zoom_for_bounds(bounds, t_min); + target_bwd_zoom = std::fmin(std::fmax(zoom_factor * t_min, t_min), t_max); + } + } + + bool bwd_ready = (_pf_bwd_tex && _pf_bwd_page == target_bwd && fabsf(_pf_bwd_zoom - target_bwd_zoom) < 0.001f); + bool bwd_running = (_pf_bwd.active && _pf_bwd.target_page == target_bwd && fabsf(_pf_bwd.target_zoom - target_bwd_zoom) < 0.001f); if (!bwd_ready && !bwd_running) { cancel_pf_slot(_pf_bwd, &_pf_bwd_tex, &_pf_bwd_page, &_pf_bwd_zoom); if (target_bwd >= 0) - start_pf_slot(_pf_bwd, target_bwd, zoom, pf_bwd_thread_entry, 2); + start_pf_slot(_pf_bwd, target_bwd, target_bwd_zoom, pf_bwd_thread_entry, 2); } } @@ -408,6 +495,15 @@ static bool get_page_bounds(fz_context *c, fz_document *d, int num, fz_rect *out void PageLayout::render_page_to_texture(int num, bool reset_zoom) { + // 1. Calculate old scroll fractions before page bounds change + float frac_x = 0.5f; + float frac_y = 0.0f; // default to top of page + get_scroll_fractions(frac_x, frac_y); + + float old_zoom_factor = 1.0f; + if (min_zoom > 0.0f) + old_zoom_factor = zoom / min_zoom; + _current_page = std::min(std::max(0, num), pages_count - 1); // Fast sync path: load bounds only (no rasterization) to update zoom/center. @@ -418,43 +514,46 @@ void PageLayout::render_page_to_texture(int num, bool reset_zoom) return; } - if (bounds.x1 != page_bounds.x1 || bounds.y1 != page_bounds.y1 || reset_zoom) - { - page_bounds = bounds; - page_center = fz_make_point(viewport.w / 2, viewport.h / 2); - min_zoom = fmin(viewport.w / bounds.x1, viewport.h / bounds.y1); - max_zoom = fmax(viewport.w / bounds.x1, viewport.h / bounds.y1); - zoom = min_zoom; - } + page_bounds = bounds; + update_min_max_zoom(); + + // Determine reset_zoom based on configKeepZoom + bool should_reset_zoom = reset_zoom || !configKeepZoom; + + // Apply zoom and position settings using virtual method + apply_position(old_zoom_factor, should_reset_zoom, configKeepPosition, frac_x, frac_y); // Check prefetch cache — instant texture swap with no rendering - if (!reset_zoom) + if (_pf_fwd_tex && _pf_fwd_page == _current_page && fabsf(_pf_fwd_zoom - zoom) < 0.001f) { - if (_pf_fwd_tex && _pf_fwd_page == _current_page && fabsf(_pf_fwd_zoom - zoom) < 0.001f) - { - cancel_bg_render(); - FreeTextureIfNeeded(&page_texture); - page_texture = _pf_fwd_tex; - _pf_fwd_tex = nullptr; - _pf_fwd_page = -1; - start_prefetch(); - return; - } - if (_pf_bwd_tex && _pf_bwd_page == _current_page && fabsf(_pf_bwd_zoom - zoom) < 0.001f) - { - cancel_bg_render(); - FreeTextureIfNeeded(&page_texture); - page_texture = _pf_bwd_tex; - _pf_bwd_tex = nullptr; - _pf_bwd_page = -1; - start_prefetch(); - return; - } + cancel_bg_render(); + FreeTextureIfNeeded(&page_texture); + page_texture = _pf_fwd_tex; + _pf_fwd_tex = nullptr; + _pf_fwd_page = -1; + rendered_zoom = zoom; + zoom_dirty = false; + start_prefetch(); + return; + } + if (_pf_bwd_tex && _pf_bwd_page == _current_page && fabsf(_pf_bwd_zoom - zoom) < 0.001f) + { + cancel_bg_render(); + FreeTextureIfNeeded(&page_texture); + page_texture = _pf_bwd_tex; + _pf_bwd_tex = nullptr; + _pf_bwd_page = -1; + rendered_zoom = zoom; + zoom_dirty = false; + start_prefetch(); + return; } // No cache hit — use async background rasterization if available. if (_bg.bg_ctx && _bg.bg_doc) { + rendered_zoom = -1.0f; // texture not ready yet + zoom_dirty = false; start_bg_render(_current_page, zoom); return; } @@ -476,6 +575,8 @@ void PageLayout::render_page_to_texture(int num, bool reset_zoom) SDL_FreeSurface(image); fz_drop_pixmap(ctx, pix); fz_drop_page(ctx, page); + rendered_zoom = zoom; + zoom_dirty = false; } fz_catch(ctx) { @@ -493,38 +594,9 @@ void PageLayout::set_zoom(float value) return; zoom = value; + last_zoom_time = SDL_GetTicks(); + zoom_dirty = true; - // Prefetch cache is at the old zoom — invalidate it - cancel_pf_slot(_pf_fwd, &_pf_fwd_tex, &_pf_fwd_page, &_pf_fwd_zoom); - cancel_pf_slot(_pf_bwd, &_pf_bwd_tex, &_pf_bwd_page, &_pf_bwd_zoom); - - if (_bg.bg_ctx && _bg.bg_doc) - { - start_bg_render(_current_page, zoom); - // start_prefetch() will be called once the current-page render completes - } - else - { - FreeTextureIfNeeded(&page_texture); - fz_page *page = nullptr; - fz_pixmap *pix = nullptr; - fz_try(ctx) - { - page = fz_load_page(ctx, doc, _current_page); - pix = fz_new_pixmap_from_page_contents(ctx, page, fz_scale(zoom, zoom), fz_device_rgb(ctx), 0); - if (configDarkMode) fz_invert_pixmap(ctx, pix); - SDL_Surface *img = SDL_CreateRGBSurfaceFrom(pix->samples, pix->w, pix->h, pix->n * 8, pix->w * pix->n, 0x000000FF, 0x0000FF00, 0x00FF0000, 0); - page_texture = SDL_CreateTextureFromSurface(RENDERER, img); - SDL_FreeSurface(img); - fz_drop_pixmap(ctx, pix); - fz_drop_page(ctx, page); - } - fz_catch(ctx) - { - fz_drop_pixmap(ctx, pix); - fz_drop_page(ctx, page); - } - } move_page(0, 0); } @@ -532,8 +604,13 @@ void PageLayout::move_page(float x, float y) { float w = page_bounds.x1 * zoom, h = page_bounds.y1 * zoom; - page_center.x = fmin(fmax(page_center.x + x, w / 2), viewport.w - w / 2); - page_center.y = fmin(fmax(page_center.y + y, viewport.h - h / 2), h / 2); + float cx_lo = std::min(w / 2.0f, (float)viewport.w - w / 2.0f); + float cx_hi = std::max(w / 2.0f, (float)viewport.w - w / 2.0f); + page_center.x = std::fmin(std::fmax(page_center.x + x, cx_lo), cx_hi); + + float cy_lo = std::min(h / 2.0f, (float)viewport.h - h / 2.0f); + float cy_hi = std::max(h / 2.0f, (float)viewport.h - h / 2.0f); + page_center.y = std::fmin(std::fmax(page_center.y + y, cy_lo), cy_hi); } void PageLayout::top_of_page() @@ -550,3 +627,62 @@ void PageLayout::zoom_max() zoom_out(0.6); zoom_in(0.6); } + +void PageLayout::get_scroll_fractions(float &frac_x, float &frac_y) const +{ + float w_old = page_bounds.x1 * zoom; + float h_old = page_bounds.y1 * zoom; + + frac_x = 0.5f; + if (w_old > viewport.w) { + frac_x = (w_old / 2.0f - page_center.x) / (w_old - viewport.w); + frac_x = std::fmin(std::fmax(frac_x, 0.0f), 1.0f); + } + frac_y = 0.0f; // default to top of page + if (h_old > viewport.h) { + frac_y = (h_old / 2.0f - page_center.y) / (h_old - viewport.h); + frac_y = std::fmin(std::fmax(frac_y, 0.0f), 1.0f); + } +} + +void PageLayout::apply_position(float old_zoom_factor, bool should_reset_zoom, bool keep_position, float frac_x, float frac_y) +{ + if (_has_saved_view) + { + zoom = std::fmin(std::fmax(_saved_zoom, min_zoom), max_zoom); + page_center.x = _saved_cx; + page_center.y = _saved_cy; + move_page(0, 0); // Clamp to viewport limits + _has_saved_view = false; + } + else if (should_reset_zoom) + { + zoom = min_zoom; + page_center = fz_make_point(viewport.w / 2.0f, viewport.h / 2.0f); + } + else + { + zoom = std::fmin(std::fmax(old_zoom_factor * min_zoom, min_zoom), max_zoom); + + float w_new = page_bounds.x1 * zoom; + float h_new = page_bounds.y1 * zoom; + + if (keep_position) { + // Keep relative scroll position + if (w_new > viewport.w) { + page_center.x = w_new / 2.0f - frac_x * (w_new - viewport.w); + } else { + page_center.x = viewport.w / 2.0f; + } + if (h_new > viewport.h) { + page_center.y = h_new / 2.0f - frac_y * (h_new - viewport.h); + } else { + page_center.y = viewport.h / 2.0f; + } + } else { + // Reset position to top of page (or center if fits) + page_center.x = viewport.w / 2.0f; + page_center.y = (h_new > viewport.h) ? (h_new / 2.0f) : (viewport.h / 2.0f); + } + } +} diff --git a/source/menus/book/PageLayout.hpp b/source/menus/book/PageLayout.hpp index 5af7c60..1ed6b25 100644 --- a/source/menus/book/PageLayout.hpp +++ b/source/menus/book/PageLayout.hpp @@ -44,6 +44,21 @@ class PageLayout return _current_page; } + virtual float get_zoom() const { return zoom; } + virtual void get_position(float &x, float &y) const { + x = page_center.x; + y = page_center.y; + } + virtual void set_zoom_and_position(float z, float x, float y) { + _has_saved_view = true; + _saved_zoom = z; + _saved_cx = x; + _saved_cy = y; + } + virtual int get_rotation() const { return 0; } + virtual bool get_spread_mode() const { return false; } + virtual void set_rotation_and_spread(int rot, bool spread) {} + // Call from BookReader after constructing layout to enable background rasterization. void set_bg_path(const char *path); @@ -67,12 +82,17 @@ class PageLayout virtual char *info(); virtual bool pageFitsWidth() const { return page_bounds.x1 * zoom <= viewport.w + 0.5f; } virtual bool pageFitsHeight() const { return page_bounds.y1 * zoom <= viewport.h + 0.5f; } + virtual float get_min_zoom_for_bounds(const fz_rect &bounds) const; + virtual float get_max_zoom_for_bounds(const fz_rect &bounds, float min_z) const; + virtual void update_min_max_zoom(); + virtual void render_page_to_texture(int num, bool reset_zoom); protected: - virtual void render_page_to_texture(int num, bool reset_zoom); virtual void set_zoom(float value); virtual void move_page(float x, float y); virtual void top_of_page(); + virtual void get_scroll_fractions(float &frac_x, float &frac_y) const; + virtual void apply_position(float old_zoom_factor, bool should_reset_zoom, bool keep_position, float frac_x, float frac_y); fz_document *doc = NULL; pdf_document *pdf = NULL; @@ -85,6 +105,14 @@ class PageLayout SDL_Rect viewport; SDL_Texture *page_texture = NULL; + float rendered_zoom = -1.0f; + uint32_t last_zoom_time = 0; + bool zoom_dirty = false; + + bool _has_saved_view = false; + float _saved_zoom = 0.0f; + float _saved_cx = 0.0f; + float _saved_cy = 0.0f; private: std::string _bg_path; diff --git a/source/menus/book/VerticalPageLayout.cpp b/source/menus/book/VerticalPageLayout.cpp index ea9f69f..952e485 100644 --- a/source/menus/book/VerticalPageLayout.cpp +++ b/source/menus/book/VerticalPageLayout.cpp @@ -1,43 +1,26 @@ #include "VerticalPageLayout.hpp" #include -// Fit the page so its width exactly fills the viewport width. -// After this call, a second render_page_to_texture(_current_page, false) is needed -// to produce the texture at the new zoom level. -void VerticalPageLayout::apply_fit_to_width() +float VerticalPageLayout::get_min_zoom_for_bounds(const fz_rect &bounds) const { - if (page_bounds.x1 <= 0) - return; + if (bounds.x1 <= 0.0f) + return 1.0f; + return (float)viewport.w / bounds.x1; +} - float fit_width = (float)viewport.w / page_bounds.x1; - min_zoom = fit_width; - max_zoom = std::fmax(fit_width, max_zoom); // never shrink max_zoom - zoom = fit_width; +float VerticalPageLayout::get_max_zoom_for_bounds(const fz_rect &bounds, float min_z) const +{ + return std::max(min_z, min_z * 4.0f); } VerticalPageLayout::VerticalPageLayout(fz_document *doc, int current_page) : PageLayout(doc, current_page) { - // PageLayout constructor already rendered the page at standard fit-to-screen zoom. - // Now re-render at fit-to-width zoom. - apply_fit_to_width(); - render_page_to_texture(_current_page, false); - top_of_page(); + reset(); } void VerticalPageLayout::reset() { - apply_fit_to_width(); - render_page_to_texture(_current_page, false); - top_of_page(); -} - -void VerticalPageLayout::next_page(int n) -{ - // Load new page and reset bounds/zoom (reset_zoom=true). - render_page_to_texture(_current_page + n, true); - // Override zoom to fit-to-width and re-render. - apply_fit_to_width(); - render_page_to_texture(_current_page, false); + PageLayout::reset(); top_of_page(); } diff --git a/source/menus/book/VerticalPageLayout.hpp b/source/menus/book/VerticalPageLayout.hpp index e92a9a9..93d0749 100644 --- a/source/menus/book/VerticalPageLayout.hpp +++ b/source/menus/book/VerticalPageLayout.hpp @@ -10,11 +10,9 @@ class VerticalPageLayout : public PageLayout VerticalPageLayout(fz_document *doc, int current_page); void reset(); - void next_page(int n); -private: - // Override min/max/zoom so the page always fills the viewport width. - void apply_fit_to_width(); + float get_min_zoom_for_bounds(const fz_rect &bounds) const override; + float get_max_zoom_for_bounds(const fz_rect &bounds, float min_z) const override; }; #endif diff --git a/source/status_bar.c b/source/status_bar.c index 0f309d7..b8bc310 100644 --- a/source/status_bar.c +++ b/source/status_bar.c @@ -137,17 +137,15 @@ void StatusBar_DisplayTime(bool portriat) { TTF_SizeText(ROBOTO_20, "\"+\" - Help", &helpWidth, &helpHeight); if (portriat) { - int timeX = (1280 - timeWidth) + timeHeight; - int timeY = (720 - timeWidth) + 15; + int timeX = 45 - (45 - timeHeight) / 2; + int timeY = 720 - timeWidth - 15; SDL_DrawRotatedText(RENDERER, ROBOTO_25, (double)90, timeX, timeY, WHITE, Clock_GetCurrentTime()); - int helpX = (1280 - helpWidth) + 21; - int helpY = (720 - helpHeight) - (720 - timeY) - 75; + int helpX = 45 - (45 - helpHeight) / 2; + int helpY = 15; SDL_DrawRotatedText(RENDERER, ROBOTO_20, (double)90, helpX, helpY, WHITE, "\"+\" - Help"); - // SDL_DrawRotatedText(RENDERER, ROBOTO_25, (double) 90, 1270 - width, (720 - // - height), WHITE, Clock_GetCurrentTime()); } else { SDL_DrawText(RENDERER, ROBOTO_25, 1260 - timeWidth, (40 - timeHeight) / 2, WHITE, Clock_GetCurrentTime());