/* app/reader.c — 阅读页:W_CUSTOM 画布 + 三栏交互 + 中央菜单占位 + 顶栏进度/章节名 + 进度持久化。 * * 渲染:W_CUSTOM 画布覆盖全屏;render_cb 画帧 = 填底色 → 翻页 blit(FreeType 页位图缓存 / * 无字体则 SDK 位图字库兜底直绘)→ 顶栏 → 菜单覆盖层。翻页 = 局部脏区(flush_region)。 * 交互:点击左/右 1/3 翻页;中央唤菜单(字号±/行距±/返回);左滑=下一页、右滑=上一页。 * 进度:打开自动续读;换字号重排回锚(段,字符偏移);离开/翻页时保存。 */ #include "../reader_internal.h" #include "meow_ui_internal.h" /* back_buf/pitch/bpp 访问器(决策见报告) */ #include "book.h" #include "progress.h" #include "reader_conf.h" #include #include #include #include #include #include #define RD_MENU_H 64 #define RD_MENU_BTN 4 /* 返回 | 目录 | 界面 | 设置(MVP 单条) */ #define RD_HEADER_H 30 /* 页头条高(书名+章节名) */ #define RD_FOOT_H 18 /* 页底条高(章节+页码) */ #define RD_UI_H 268 /* 「界面」排版面板高 */ static rd_book *g_book; static int g_reader_active; static meow_widget *g_canvas; static int g_pw, g_ph, g_top, g_mx, g_my; static int g_fs, g_ls; #define RD_PAGE_CACHE 3 #define RD_PAGE_SLOT_CUR 1 static uint16_t *g_page_buf[RD_PAGE_CACHE]; /* [0]=prev [1]=cur [2]=next(FreeType 页位图) */ static int g_has_font; /* 1=FreeType 路径;0=SDK 字库兜底 */ static int g_menu_open; static int g_ui_open; /* 「界面」排版面板(B) */ static int g_dir_open; /* 目录覆盖层 */ static int g_dir_top, g_dir_row_h; /* 目录列表几何(rdsrc 目录项) */ static char g_font_path[400]; static int g_notice_ms; /* ---- R3 翻页动画(轻量水平滑入) ---- */ static meow_anim *g_anim; /* 活动翻页动画;NULL=无 */ static uint16_t *g_anim_old; /* 动画「滑出」页位图(切页前快照) */ static int g_anim_dir; /* 1=下一页(旧左移/新右入);-1=上一页(旧右移/新左入) */ /* ---- R3 段刮擦(右缘拖动快速定位 + 预览) ---- */ #define RD_SCRUB_W 26 /* 右缘刮擦条宽度 */ static int g_scrub; /* 1=刮擦中 */ static int g_scrub_page; /* 刮擦目标页(实时预览用) */ /* ---- R3 编码切换(TXT;Big5 繁体书手动选定) ---- */ static int g_enc_idx; /* g_enc_cycle[] 下标(当前有效编码) */ static uint32_t g_page_bg, g_page_fg; /* 正文页底/字色(24bit) */ static char g_chapter[128]; /* 顶栏章节名(TXT 无章节=文件名) */ static int g_show_header, g_show_footer; /* 页头条/底条显隐(rdconf.rd.header/footer) */ static volatile int g_volpage_pending; /* 系统翻页信号待消费(IPC 线程置位,UI tick 消费,E) */ #define RD_READER_DEFAULT_FS 20 #define RD_READER_DEFAULT_LS 130 /* R3:编码循环(显式编码;Auto 由打开时探测决定,不在此列)。 * -1 哨兵不用;TXT 初始 = 打开探测结果(GB18030 兜底)。 */ static const rd_enc_t g_enc_cycle[] = { RD_ENC_GB18030, RD_ENC_BIG5, RD_ENC_UTF8, RD_ENC_UTF16LE, RD_ENC_UTF16BE }; #define RD_ENC_CYCLE_N ((int)(sizeof g_enc_cycle / sizeof g_enc_cycle[0])) static const char *enc_cycle_name(int idx) { switch (g_enc_cycle[idx]) { case RD_ENC_GB18030: return "GB18030"; case RD_ENC_BIG5: return "Big5"; case RD_ENC_UTF8: return "UTF-8"; case RD_ENC_UTF16LE: return "UTF-16LE"; case RD_ENC_UTF16BE: return "UTF-16BE"; default: return "?"; } } static int enc_to_cycle_idx(rd_enc_t e) { for (int i = 0; i < RD_ENC_CYCLE_N; i++) if (g_enc_cycle[i] == e) return i; return -1; } /* 前向声明(reader_reflow* 依赖 reader_refill/redraw,定义在后) */ static void reader_redraw(void); static int reader_refill_page(void); static void reader_reflow_persist(void); /* ---------- 工具 ---------- */ static uint16_t rgb565(uint32_t c) { int r = (c >> 16) & 0xFF, g = (c >> 8) & 0xFF, b = c & 0xFF; return (uint16_t)(((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3)); } /* 按 rdconf.bg/theme 解析阅读页底/字色(默认纯黑,B 定案)。 */ static void reader_resolve_bg(uint32_t *bg, uint32_t *fg) { const rdconf *c = rdconf_get(); if (!c) { *bg = 0x000000; *fg = 0x9AA4C8; return; } if (strcmp(c->bg, "black") == 0) { *bg = 0x000000; *fg = 0x9AA4C8; return; } if (strcmp(c->bg, "white") == 0) { *bg = 0xFFFFFF; *fg = 0x1A1A2A; return; } if (strcmp(c->bg, "sepia") == 0) { *bg = 0xF5EFDC; *fg = 0x3B3A32; return; } /* auto:跟随主题(rd.theme=light/dark 时强制该明暗的 surface/onSurface) */ meow_theme_mode m = MEOW_THEME_AUTO; if (strcmp(c->theme, "light") == 0) m = MEOW_THEME_LIGHT; else if (strcmp(c->theme, "dark") == 0) m = MEOW_THEME_DARK; if (m == MEOW_THEME_AUTO) { const meow_theme *th = meow_theme_get(); *bg = th->surface; *fg = th->onSurface; } else { meow_theme t = meow_theme_defaults(m); *bg = t.surface; *fg = t.onSurface; } } /* 「界面」面板改背景(写 rdconf.bg + 重解析颜色 + 重排版页缓冲)。 */ static void reader_set_bg(const char *bg) { rdconf *c = rdconf_get(); if (!c) return; strncpy(c->bg, bg, sizeof c->bg - 1); c->bg[sizeof c->bg - 1] = 0; if (strcmp(c->bg, "black") && strcmp(c->bg, "white") && strcmp(c->bg, "sepia") && strcmp(c->bg, "auto")) strcpy(c->bg, "black"); rdconf_persist(); reader_resolve_bg(&g_page_bg, &g_page_fg); reader_refill_page(); reader_redraw(); } /* 递归(深度≤3)扫 MeowOS/fonts/ 找第一个 .otf/.ttf。0=找到。 */ static int find_font_rec(const char *dir, int depth) { if (depth > 3) return 0; DIR *dp = opendir(dir); if (!dp) return 0; struct dirent *de; int found = 0; while ((de = readdir(dp)) && !found) { if (de->d_name[0] == '.') continue; char full[480]; if (snprintf(full, sizeof full, "%s/%s", dir, de->d_name) >= (int)sizeof full) continue; struct stat st; if (stat(full, &st) != 0) continue; if (S_ISDIR(st.st_mode)) { found = find_font_rec(full, depth + 1); } else { const char *d = strrchr(de->d_name, '.'); if (d && (strcasecmp(d, ".otf") == 0 || strcasecmp(d, ".ttf") == 0)) { if (strlen(full) < sizeof g_font_path) { strcpy(g_font_path, full); found = 1; } } } } closedir(dp); return found; } static const char *resolve_font(void) { g_font_path[0] = 0; if (find_font_rec(READER_SD_ROOT "/MeowOS/fonts", 0)) return g_font_path; return NULL; } static void chapter_name(void) { /* TXT 无章节 → 文件名;R1 固定。 */ snprintf(g_chapter, sizeof g_chapter, "%s", rd_book_basename(g_book)); } static void reader_reflow(void) { if (!g_book) return; if (g_anim) { meow_anim_cancel(g_anim); g_anim = NULL; } /* 重排/雷字号时终止翻页动画 */ int seg = 0; int32_t ch = 0; rd_book_anchor(g_book, &seg, &ch); /* 记录当前锚,重排后回锚 */ rd_book_jump_to_anchor(g_book, seg, ch, g_fs, g_ls); reader_refill_page(); reader_redraw(); reader_reflow_persist(); } static void reader_reflow_persist(void) { if (!g_book) return; int seg = 0; int32_t ch = 0; rd_book_anchor(g_book, &seg, &ch); rdprog p; p.valid = 1; p.seg = seg; p.ch = ch; p.fs = g_fs; p.ls = g_ls; p.enc = (g_book && !rd_book_is_image(g_book)) ? (int)g_enc_cycle[g_enc_idx] : -1; p.ts = (uint64_t)time(NULL); rdprog_save(rd_book_path(g_book), &p); } /* ---------- 渲染(W_CUSTOM render_cb) ---------- */ static void reader_draw_line_atlas(int x, int y, const char *utf8, int len, void *ctx) { (void)ctx; (void)len; meow_ui_text(x, y, utf8, (g_fs <= 20) ? 20 : 28, g_page_fg); } /* 正文内容区几何:y0=top(顶栏下沿),y1=ph-my(下边距)。 */ static int rect_y0(void) { return g_top; } static int rect_y1(void) { return g_ph - g_my; } /* 把一页位图的行区间 [y0,y1) 以水平偏移 dx 拷到 back_buf(裁剪到屏幕)。动画/局部重绘共用。 */ static void blit_rows(uint16_t *src, int dx, int y0, int y1) { unsigned char *bb = meow_ui_back_buf(); int pitch = meow_ui_back_buf_pitch(); int bpp = meow_ui_back_buf_bpp(); if (!bb || pitch <= 0 || bpp != 16) return; int sx0 = dx < 0 ? -dx : 0; int dx0 = dx > 0 ? dx : 0; int w = g_pw - (dx < 0 ? -dx : dx); if (w <= 0) return; for (int y = y0; y < y1; y++) { if (y < 0 || y >= g_ph) continue; memcpy(bb + (size_t)y * pitch + (size_t)dx0 * 2, src + (size_t)y * g_pw + sx0, (size_t)w * 2); } } /* 子矩形直拷(同屏幕坐标):用于右缘刮擦条区恢复/局部重绘。 */ static void blit_sub(uint16_t *src, int sx, int sy, int w, int h) { unsigned char *bb = meow_ui_back_buf(); int pitch = meow_ui_back_buf_pitch(); int bpp = meow_ui_back_buf_bpp(); if (!bb || pitch <= 0 || bpp != 16) return; for (int y = sy; y < sy + h; y++) { if (y < 0 || y >= g_ph) continue; memcpy(bb + (size_t)y * pitch + (size_t)sx * 2, src + (size_t)y * g_pw + sx, (size_t)w * 2); } } /* R3 右缘刮擦指示条:轨道 + 拇指(当前/目标位置)+ 刮擦中预览框。render_cb 调用。 */ static void reader_draw_scrub(const meow_theme *th) { int np = g_book ? rd_book_pages(g_book) : 0; if (np <= 0) return; int y0 = rect_y0(), y1 = rect_y1(), h = y1 - y0; if (h <= 0) return; int page = g_scrub ? g_scrub_page : rd_book_current(g_book); if (page < 0) page = 0; if (page >= np) page = np - 1; int x = g_pw - 10; meow_ui_fill_rect(x - 1, y0, 3, h, th->dim); /* 轨道 */ int denom = np > 1 ? (np - 1) : 1; int ty = y0 + (int)((long long)h * (np > 1 ? page : 0) / denom); meow_ui_fill_rect(x - 3, ty - 4, 7, 8, th->primary); /* 拇指 */ if (g_scrub) { char pv[24]; snprintf(pv, sizeof pv, "%d/%d", page + 1, np); int bw = 58, bh = 22; int px = x - bw - 6; int py = ty - bh / 2; if (py < y0) py = y0; if (py + bh > y1) py = y1 - bh; meow_ui_fill_rect(px, py, bw, bh, th->surfaceContainer); meow_ui_text_center(px + bw / 2, py + (bh - 20) / 2, pv, 16, th->fg); } } /* ----------------「界面」排版面板(B:字号/行距/背景/顶底栏/编码) ---------------- * 自绘 + 自制命中(README §5.2 建议:W_CUSTOM 画布 + 自制 hit,零 SDK widget 协同)。 * 几何 draw/hit 共用(常量)。 */ #define UI_LABEL_X 20 #define UI_CTRL_X 130 #define UI_CTRL_W (480 - UI_CTRL_X - 16) #define UI_ROW_H 34 static int ui_panel_y(void) { return g_ph - RD_UI_H; } /* 单个「值按钮」:[-]/[+] 或文本钮。cx=控件区起点;返回该按钮中点 x。 */ static void ui_draw_step(int x, int w, int y, const char *tag, int active, const meow_theme *th) { if (active) meow_ui_fill_rect(x, y, w, 28, th->primaryContainer); else meow_ui_fill_rect(x, y, w, 28, th->surfaceContainerHighest); uint32_t tc = active ? th->onPrimaryContainer : th->fg; meow_ui_text_center(x + w / 2, y + 4, tag, 20, tc); } static void ui_draw_chips(const char *const *labels, int n, int cur, int x, int y, int w, const meow_theme *th) { int gap = 4; int cw = (w - (n - 1) * gap) / n; for (int i = 0; i < n; i++) { int cx = x + i * (cw + gap); if (i == cur) meow_gfx_fill_round(cx, y, cw, 28, 14, th->primaryContainer); else meow_gfx_fill_round(cx, y, cw, 28, 14, th->surfaceContainerHighest); uint32_t tc = (i == cur) ? th->onPrimaryContainer : th->fg; meow_ui_text_center(cx + cw / 2, y + 4, labels[i], 20, tc); } } static void reader_draw_ui_panel(const meow_theme *th) { int y0 = ui_panel_y(); int top = y0 + 30; int ry; const rdconf *c = rdconf_get(); /* 面板底 + scrim */ meow_ui_fill_rect_alpha(0, g_top, g_pw, g_ph - g_top, 0x000000, 150); meow_ui_fill_rect(0, y0, g_pw, RD_UI_H, th->surfaceContainer); meow_ui_draw_rect(0, y0, g_pw, 1, th->outlineVariant); meow_ui_text_center(240, y0 + 5, "界面 · 排版", 20, th->fg); /* 0 字号 */ ry = top + 0 * UI_ROW_H; meow_ui_text(UI_LABEL_X, ry + 6, "字号", 20, th->fg); char v[16]; snprintf(v, sizeof v, "%d", g_fs); ui_draw_step(UI_CTRL_X, 40, ry + 3, "-", 0, th); meow_ui_text_center(UI_CTRL_X + 40 + (UI_CTRL_W - 80) / 2, ry + 6, v, 20, th->fg); ui_draw_step(UI_CTRL_X + UI_CTRL_W - 40, 40, ry + 3, "+", 0, th); /* 1 行距 */ ry = top + 1 * UI_ROW_H; meow_ui_text(UI_LABEL_X, ry + 6, "行距", 20, th->fg); snprintf(v, sizeof v, "%d", g_ls); ui_draw_step(UI_CTRL_X, 40, ry + 3, "-", 0, th); meow_ui_text_center(UI_CTRL_X + 40 + (UI_CTRL_W - 80) / 2, ry + 6, v, 20, th->fg); ui_draw_step(UI_CTRL_X + UI_CTRL_W - 40, 40, ry + 3, "+", 0, th); /* 2 背景(4 档:黑/白/米黄/自动;当前=标亮) */ ry = top + 2 * UI_ROW_H; meow_ui_text(UI_LABEL_X, ry + 6, "背景", 20, th->fg); static const char *bg_lbl[4] = { "黑", "白", "米黄", "自动" }; int bgcur = 0; if (c) { if (!strcmp(c->bg, "white")) bgcur = 1; else if (!strcmp(c->bg, "sepia")) bgcur = 2; else if (!strcmp(c->bg, "auto")) bgcur = 3; } ui_draw_chips(bg_lbl, 4, bgcur, UI_CTRL_X, ry + 3, UI_CTRL_W, th); /* 3 顶栏显隐(开/关) */ ry = top + 3 * UI_ROW_H; meow_ui_text(UI_LABEL_X, ry + 6, "顶栏", 20, th->fg); static const char *yn[2] = { "开", "关" }; ui_draw_chips(yn, 2, g_show_header ? 0 : 1, UI_CTRL_X, ry + 3, UI_CTRL_W, th); /* 4 底栏显隐 */ ry = top + 4 * UI_ROW_H; meow_ui_text(UI_LABEL_X, ry + 6, "底栏", 20, th->fg); ui_draw_chips(yn, 2, g_show_footer ? 0 : 1, UI_CTRL_X, ry + 3, UI_CTRL_W, th); /* 5 编码(TXT;显示当前,点循环) */ ry = top + 5 * UI_ROW_H; meow_ui_text(UI_LABEL_X, ry + 6, "编码", 20, th->fg); if (g_book && !rd_book_is_image(g_book)) { ui_draw_step(UI_CTRL_X + UI_CTRL_W - 120, 120, ry + 3, enc_cycle_name(g_enc_idx), 0, th); } else { meow_ui_text(UI_CTRL_X, ry + 6, "—", 20, th->dim); } /* 6 返回阅读 */ ry = top + 6 * UI_ROW_H; ui_draw_step(UI_CTRL_X, UI_CTRL_W, ry + 2, "返回阅读", 1, th); } /* 「界面」面板命中;返回动作码(见下方 enum)。0=无。 */ enum { UI_NONE = 0, UI_FS_DEC, UI_FS_INC, UI_LS_DEC, UI_LS_INC, UI_BG_0, UI_BG_1, UI_BG_2, UI_BG_3, UI_HDR_TOGGLE, UI_FTR_TOGGLE, UI_ENC, UI_CLOSE }; static int reader_ui_hit(int x, int y) { int y0 = ui_panel_y(); if (y < y0) return UI_NONE; int top = y0 + 30; int ry; int cx = UI_CTRL_X, cw = UI_CTRL_W; /* 0 字号 */ ry = top + 0 * UI_ROW_H; if (y >= ry && y < ry + UI_ROW_H) { if (x >= cx && x < cx + 40) return UI_FS_DEC; if (x >= cx + cw - 40 && x < cx + cw) return UI_FS_INC; return UI_NONE; } /* 1 行距 */ ry = top + 1 * UI_ROW_H; if (y >= ry && y < ry + UI_ROW_H) { if (x >= cx && x < cx + 40) return UI_LS_DEC; if (x >= cx + cw - 40 && x < cx + cw) return UI_LS_INC; return UI_NONE; } /* 2 背景 4 chips */ ry = top + 2 * UI_ROW_H; if (y >= ry && y < ry + UI_ROW_H && x >= cx && x < cx + cw) { int gap = 4, cwp = (cw - 3 * gap) / 4; int k = (x - cx) / (cwp + gap); if (k < 0) k = 0; if (k > 3) k = 3; return UI_BG_0 + k; } /* 3 顶栏 */ ry = top + 3 * UI_ROW_H; if (y >= ry && y < ry + UI_ROW_H && x >= cx && x < cx + cw) return UI_HDR_TOGGLE; /* 4 底栏 */ ry = top + 4 * UI_ROW_H; if (y >= ry && y < ry + UI_ROW_H && x >= cx && x < cx + cw) return UI_FTR_TOGGLE; /* 5 编码 */ ry = top + 5 * UI_ROW_H; if (y >= ry && y < ry + UI_ROW_H && x >= cx && x < cx + cw) return UI_ENC; /* 6 返回阅读 */ ry = top + 6 * UI_ROW_H; if (y >= ry && y < ry + UI_ROW_H && x >= cx && x < cx + cw) return UI_CLOSE; return UI_NONE; } static void reader_canvas_render(meow_widget *w, const meow_theme *th, void *ctx) { (void)w; (void)ctx; meow_ui_clear(g_page_bg); if (g_book) { int page = rd_book_current(g_book); if ((g_has_font || rd_book_is_image(g_book)) && g_page_buf[RD_PAGE_SLOT_CUR]) { /* 翻页 blit:行拷贝当前页位图 → back_buf(局部脏区由 flush_region 上屏) */ blit_rows(g_page_buf[RD_PAGE_SLOT_CUR], 0, 0, g_ph); } else { rd_book_render_text(g_book, page, reader_draw_line_atlas, NULL); } } /* 页头条(书名 + 章节名;可选,rd.header) */ int sbh = meow_ui_statusbar_height(); int pages = g_book ? rd_book_pages(g_book) : 0; int page = g_book ? rd_book_current(g_book) : 0; const char *bk = g_book ? rd_book_basename(g_book) : "阅读"; if (g_show_header) { meow_ui_fill_rect(0, sbh, g_pw, RD_HEADER_H, g_page_bg); meow_ui_text(16, sbh + 6, bk, 16, g_page_fg); meow_ui_text_right(g_pw - 16, sbh + 6, g_chapter[0] ? g_chapter : "阅读", 16, g_page_fg); meow_ui_draw_rect(0, sbh + RD_HEADER_H - 1, g_pw, 1, th->outlineVariant); } /* 页底条(章节 + 页码;可选,rd.footer) */ if (g_show_footer) { char ft[48]; if (pages > 0) snprintf(ft, sizeof ft, "%d/%d", page + 1, pages); else snprintf(ft, sizeof ft, "0/0"); int fy = g_ph - RD_FOOT_H; meow_ui_fill_rect(0, fy, g_pw, RD_FOOT_H, g_page_bg); meow_ui_text(16, fy + 1, g_chapter[0] ? g_chapter : bk, 16, g_page_fg); meow_ui_text_right(g_pw - 16, fy + 1, ft, 16, g_page_fg); } /* R3 刮擦指示条(非目录/面板覆盖时显示;菜单为底部条,与右缘条并存即可) */ if (g_book && !g_dir_open && !g_ui_open) reader_draw_scrub(th); if (g_menu_open) { int y = g_ph - RD_MENU_H; meow_ui_fill_rect_alpha(0, y, g_pw, RD_MENU_H, 0x000000, 160); /* 4 按钮(MVP 单条):返回 | 目录 | 界面 | 设置 */ int bw = g_pw / RD_MENU_BTN; const char *labels[RD_MENU_BTN] = { "返回", "目录", "界面", "设置" }; for (int i = 0; i < RD_MENU_BTN; i++) { int bx = i * bw; meow_ui_fill_rect(bx + 4, y + 12, bw - 8, RD_MENU_H - 24, th->surfaceContainer); meow_ui_text_center(bx + bw / 2, y + (RD_MENU_H - 20) / 2, labels[i], 20, th->fg); } } if (g_ui_open && g_book) reader_draw_ui_panel(th); if (g_dir_open && g_book) { /* 目录覆盖层:半透明底 + 条目列表(当前行高亮) */ int n = rd_book_toc_n(g_book); if (n > 0) { int top = g_top + 8; int row = 34; int maxshow = (g_ph - top - 70) / row; if (maxshow < 1) maxshow = 1; if (maxshow > n) maxshow = n; g_dir_top = top; g_dir_row_h = row; meow_ui_fill_rect_alpha(0, g_top, g_pw, g_ph - g_top, 0x000000, 150); char t[96]; int sg; int32_t cch; int page = rd_book_current(g_book); for (int i = 0; i < maxshow; i++) { if (rd_book_toc(g_book, i, t, sizeof t, &sg, &cch) != 0) continue; int y0 = top + i * row; if (page == sg) meow_ui_fill_rect(0, y0, g_pw, row - 4, th->surfaceContainer); meow_ui_text(24, y0, t, 20, th->fg); meow_ui_draw_rect(12, y0 + row - 6, g_pw - 24, 1, th->outline); } meow_ui_text(20, g_ph - 34, "点条目跳转 · 点空白关闭", 16, th->dim); } else { g_dir_open = 0; meow_ui_text(20, g_top + 40, "本书无目录", 20, th->dim); } } } static void reader_redraw(void) { if (!g_canvas) return; meow_widget_draw(g_canvas); /* → render_cb 画帧 */ meow_ui_flush_region(0, 0, g_pw, g_ph); } static void reader_free_page_bufs(void) { for (int i = 0; i < RD_PAGE_CACHE; i++) { free(g_page_buf[i]); g_page_buf[i] = NULL; } if (g_anim_old) { free(g_anim_old); g_anim_old = NULL; } } /* 渲染当前页 + 前后各一页到 3 槽缓存(对齐设计「当前±前±后」;动画档为 无/滑动/淡入 R1 默认无)。 */ static int reader_refill_page(void) { if (!g_book) return -1; if ((g_has_font || rd_book_is_image(g_book)) && g_page_buf[RD_PAGE_SLOT_CUR]) { uint16_t bg = rgb565(g_page_bg), fg = rgb565(g_page_fg); int cur = rd_book_current(g_book); int np = rd_book_pages(g_book); int slots[RD_PAGE_CACHE] = { cur - 1, cur, cur + 1 }; for (int i = 0; i < RD_PAGE_CACHE; i++) { int p = slots[i]; if (p >= 0 && p < np) rd_book_render(g_book, p, g_page_buf[i], g_pw * 2, bg, fg); else for (int y = 0; y < g_ph; y++) /* 空白槽填背景 */ for (int x = 0; x < g_pw; x++) g_page_buf[i][(size_t)y * g_pw + x] = bg; } return 0; } return 0; /* 兜底路径:render_text 直绘 */ } /* ---------- R3 翻页动画(重载页位图,轻量水平滑动,仅内容区分帧) ---------- */ static void reader_anim_update(meow_anim *a, float v, void *ctx) { (void)a; (void)ctx; if (!g_book || !g_anim_old || !g_page_buf[RD_PAGE_SLOT_CUR]) return; int off = (int)(v * g_pw); int y0 = rect_y0(), y1 = rect_y1(); meow_ui_fill_rect(0, y0, g_pw, y1 - y0, g_page_bg); /* 内容区清底色 */ int old_x, new_x; if (g_anim_dir == 1) { old_x = -off; new_x = g_pw - off; } /* 下一页:旧左移/新右入 */ else { old_x = off; new_x = off - g_pw; } /* 上一页:旧右移/新左入 */ blit_rows(g_anim_old, old_x, y0, y1); blit_rows(g_page_buf[RD_PAGE_SLOT_CUR], new_x, y0, y1); meow_ui_flush_region(0, y0, g_pw, y1 - y0); } static void reader_anim_done(meow_anim *a, int canceled, void *ctx) { (void)a; (void)ctx; g_anim = NULL; if (!canceled) { reader_redraw(); reader_reflow_persist(); } /* 顶栏 n/N+% 更新 + 进度保存 */ } static void reader_turn_animated(int dir) { if (!g_book) return; if (g_anim) { meow_anim_cancel(g_anim); g_anim = NULL; } /* 取消进行中动画,重开 */ /* 无页位图(atlas 兜底):瞬切 */ if (!(g_has_font || rd_book_is_image(g_book)) || !g_page_buf[RD_PAGE_SLOT_CUR]) { rd_book_turn(g_book, dir); reader_refill_page(); reader_redraw(); reader_reflow_persist(); return; } if (!g_anim_old) g_anim_old = (uint16_t *)malloc((size_t)g_pw * g_ph * 2); if (!g_anim_old) { rd_book_turn(g_book, dir); reader_refill_page(); reader_redraw(); return; } memcpy(g_anim_old, g_page_buf[RD_PAGE_SLOT_CUR], (size_t)g_pw * g_ph * 2); rd_book_turn(g_book, dir); reader_refill_page(); g_anim_dir = dir; meow_anim_desc d = { 0.0f, 1.0f, 140, MEOW_EASE_OUT, reader_anim_update, reader_anim_done, NULL }; g_anim = meow_anim_start(&d); if (!g_anim) reader_redraw(); /* 池满等:瞬时落定 */ } /* ---------- R3 段刮擦(右缘拖动快速定位 + 预览) ---------- */ static int scrub_page_for_y(int y) { int y0 = rect_y0(), y1 = rect_y1(), h = y1 - y0; int np = g_book ? rd_book_pages(g_book) : 0; if (h <= 0 || np <= 0) return g_book ? rd_book_current(g_book) : 0; int cy = y - y0; if (cy < 0) cy = 0; if (cy > h) cy = h; return (int)((long long)cy * (np - 1) / h); } static void reader_scrub_jump(int page) { if (!g_book) return; int np = rd_book_pages(g_book); if (np <= 0) return; rd_book_set_page(g_book, page); reader_refill_page(); reader_redraw(); /* 整页落定:正文 + 顶栏 + 刮擦条 */ reader_reflow_persist(); } /* 拖动中局部重绘(不动正文页,仅页底页码 + 右缘条/预览)。内容页保持(预览语义)。 */ static void reader_scrub_preview(int page) { g_scrub_page = page; if (!g_book) return; const meow_theme *th = meow_theme_get(); int pages = rd_book_pages(g_book); int y0 = rect_y0(), y1 = rect_y1(); char ft[48]; if (pages > 0) snprintf(ft, sizeof ft, "%d/%d", g_scrub_page + 1, pages); else snprintf(ft, sizeof ft, "0/0"); /* 页底条页码重画(页码已移到底条) */ if (g_show_footer) { int fy = g_ph - RD_FOOT_H; meow_ui_fill_rect(0, fy, g_pw, RD_FOOT_H, g_page_bg); meow_ui_text(16, fy + 1, g_chapter[0] ? g_chapter : "阅读", 16, g_page_fg); meow_ui_text_right(g_pw - 16, fy + 1, ft, 16, g_page_fg); } /* 右缘条区清残留(恢复页面内容)再画条 + 预览框;无页位图(字库兜底)则跳过恢复 */ if (g_page_buf[RD_PAGE_SLOT_CUR]) blit_sub(g_page_buf[RD_PAGE_SLOT_CUR], g_pw - 120, y0, 120, y1 - y0); reader_draw_scrub(th); if (g_show_footer) meow_ui_flush_region(0, g_ph - RD_FOOT_H, g_pw, RD_FOOT_H); meow_ui_flush_region(g_pw - 120, y0, 120, y1 - y0); } static void reader_scrub_begin(int x, int y) { (void)x; if (!g_book || g_menu_open || g_dir_open) return; if (g_pw - x > RD_SCRUB_W) return; if (y < rect_y0() || y >= rect_y1()) return; int np = rd_book_pages(g_book); if (np <= 1) return; g_scrub = 1; if (g_anim) { meow_anim_cancel(g_anim); g_anim = NULL; } reader_scrub_preview(scrub_page_for_y(y)); } /* ---------- 打开 ---------- */ void reader_open(const char *path) { if (!path || !path[0]) return; if (g_book) { rd_book_close(g_book); g_book = NULL; } reader_free_page_bufs(); const char *fp = resolve_font(); g_has_font = (fp != NULL); /* 全局默认字号/行距(reader.conf;进度里的每书偏好随后覆盖) */ const rdconf *rdc = rdconf_get(); g_fs = (rdc && rdc->fs >= 14 && rdc->fs <= 40) ? rdc->fs : RD_READER_DEFAULT_FS; g_ls = (rdc && rdc->ls >= 100 && rdc->ls <= 200) ? rdc->ls : RD_READER_DEFAULT_LS; g_show_header = rdc ? (rdc->header ? 1 : 0) : 1; g_show_footer = rdc ? (rdc->footer ? 1 : 0) : 1; /* 进度(含字号/行距偏好) */ rdprog prog; int hasprog = (rdprog_load(path, &prog) == 1) && prog.valid; if (hasprog) { if (prog.fs >= 14 && prog.fs <= 40) g_fs = prog.fs; if (prog.ls >= 100 && prog.ls <= 200) g_ls = prog.ls; } int sbh = meow_ui_statusbar_height(); g_pw = meow_ui_width(); g_ph = meow_ui_height(); g_top = sbh + RD_HEADER_H; g_mx = 16; g_my = 20; /* 阅读页底/字色:rd.bg(默认纯黑,B)→ 覆盖 th->surface */ reader_resolve_bg(&g_page_bg, &g_page_fg); /* 开书(FreeType 路径带齐几何;无字体则 g_has_font=0 → atlas 兜底) */ int rc = rd_book_open(&g_book, path, fp, g_fs, g_ls, g_pw, g_ph, g_mx, g_my, g_top); if (rc != 0) { meow_log(MEOW_LOG_WARN, "book open failed rc=%d path=%s", rc, path); g_book = NULL; g_has_font = 0; g_notice_ms = 3000; return; } chapter_name(); if (g_has_font || rd_book_is_image(g_book)) { reader_free_page_bufs(); for (int i = 0; i < RD_PAGE_CACHE; i++) g_page_buf[i] = (uint16_t *)malloc((size_t)g_pw * (size_t)g_ph * 2); if (!g_page_buf[RD_PAGE_SLOT_CUR]) { reader_free_page_bufs(); g_has_font = 0; } else g_anim_old = (uint16_t *)malloc((size_t)g_pw * (size_t)g_ph * 2); /* 动画快照位图 */ } /* 复位动画/刮擦状态(跨书) */ if (g_anim) { meow_anim_cancel(g_anim); g_anim = NULL; } g_scrub = 0; g_menu_open = 0; g_dir_open = 0; g_ui_open = 0; /* 编码:初始 = 打开探测结果;进度里的显式编码(Big5 用户选定)优先重开 */ if (g_book && !rd_book_is_image(g_book)) { int idx = enc_to_cycle_idx(rd_book_enc_get(g_book)); g_enc_idx = idx >= 0 ? idx : 0; if (hasprog && prog.enc >= 0) { int pi = enc_to_cycle_idx((rd_enc_t)prog.enc); if (pi >= 0) { if (g_enc_cycle[pi] != rd_book_enc_get(g_book)) { if (rd_book_set_enc(g_book, g_enc_cycle[pi]) == 0) { g_enc_idx = pi; chapter_name(); } } else g_enc_idx = pi; } } } else g_enc_idx = 0; /* 续读:进度锚(段,字符偏移)→ 跳页 */ if (hasprog && g_book) { rd_book_jump_to_anchor(g_book, prog.seg, prog.ch, g_fs, g_ls); } reader_refill_page(); if (g_reader_active) reader_redraw(); } /* ---------- 视图生命周期 ---------- */ static void reader_on_create(meow_view *v, void *ctx) { (void)v; (void)ctx; } static void reader_on_enter(meow_view *v, void *ctx) { (void)v; (void)ctx; g_reader_active = 1; if (g_book) { reader_refill_page(); chapter_name(); } reader_redraw(); } static void reader_on_leave(meow_view *v, void *ctx) { (void)v; (void)ctx; reader_reflow_persist(); /* 离开保存进度 */ g_reader_active = 0; } static void reader_on_destroy(meow_view *v, void *ctx) { (void)v; (void)ctx; if (g_anim) { meow_anim_cancel(g_anim); g_anim = NULL; } if (g_book) rd_book_close(g_book); g_book = NULL; reader_free_page_bufs(); g_menu_open = 0; g_dir_open = 0; g_ui_open = 0; g_scrub = 0; g_reader_active = 0; } meow_view *reader_view_create(void) { static meow_view_hooks hooks = { reader_on_create, reader_on_enter, reader_on_leave, reader_on_destroy, NULL, NULL, NULL }; meow_view *v = meow_view_create(ACT_READER, MEOW_VIEW_ACTIVITY, &hooks, NULL); g_canvas = meow_widget_custom_create(0, 0, 480, 720, reader_canvas_render, NULL, NULL); meow_view_add_widget(v, g_canvas); return v; } int reader_is_active(void) { return g_reader_active; } void reader_on_tap(int x, int y) { (void)x; if (!g_book) { return; } if (g_scrub) { /* 刮擦抬手 → 跳到目标页收尾 */ g_scrub = 0; reader_scrub_jump(g_scrub_page); return; } if (g_dir_open) { /* 目录覆盖层:点条目跳转,点空白关闭 */ int n = rd_book_toc_n(g_book); if (n > 0 && x >= 0 && y >= g_dir_top) { int row = g_dir_row_h > 0 ? g_dir_row_h : 34; int idx = (y - g_dir_top) / row; if (idx < n && idx >= 0) { char t[96]; int sg; int32_t cch; if (rd_book_toc(g_book, idx, t, sizeof t, &sg, &cch) == 0) { if (g_book && sg >= 0) { rd_book_jump_to_anchor(g_book, sg, cch, g_fs, g_ls); g_dir_open = 0; g_menu_open = 0; snprintf(g_chapter, sizeof g_chapter, "%s", t); reader_refill_page(); reader_redraw(); reader_reflow_persist(); return; } } } } g_dir_open = 0; reader_redraw(); return; } if (g_ui_open) { int a = reader_ui_hit(x, y); switch (a) { case UI_FS_DEC: if (g_fs > 14) { g_fs -= 2; reader_reflow(); } break; case UI_FS_INC: if (g_fs < 40) { g_fs += 2; reader_reflow(); } break; case UI_LS_DEC: if (g_ls > 100) { g_ls -= 5; reader_reflow(); } break; case UI_LS_INC: if (g_ls < 200) { g_ls += 5; reader_reflow(); } break; case UI_BG_0: reader_set_bg("black"); break; case UI_BG_1: reader_set_bg("white"); break; case UI_BG_2: reader_set_bg("sepia"); break; case UI_BG_3: reader_set_bg("auto"); break; case UI_HDR_TOGGLE: g_show_header = !g_show_header; if (rdconf_get()) { rdconf_get()->header = g_show_header; rdconf_persist(); } reader_redraw(); break; case UI_FTR_TOGGLE: g_show_footer = !g_show_footer; if (rdconf_get()) { rdconf_get()->footer = g_show_footer; rdconf_persist(); } reader_redraw(); break; case UI_ENC: /* 编码切换(TXT/Big5 繁体书) */ if (g_book && !rd_book_is_image(g_book)) { int ni = (g_enc_idx + 1) % RD_ENC_CYCLE_N; if (rd_book_set_enc(g_book, g_enc_cycle[ni]) == 0) { g_enc_idx = ni; chapter_name(); reader_refill_page(); reader_redraw(); reader_reflow_persist(); } } break; case UI_CLOSE: g_ui_open = 0; reader_redraw(); break; default: if (a == UI_NONE) { g_ui_open = 0; reader_redraw(); } break; } return; } if (g_menu_open) { /* 菜单按钮命中 */ int y0 = g_ph - RD_MENU_H; if (y >= y0) { int bw = g_pw / RD_MENU_BTN; int idx = x / bw; switch (idx) { case 0: /* 返回书架 */ g_menu_open = 0; reader_reflow_persist(); meow_view_page_pop(); return; case 1: /* 目录 */ if (rd_book_toc_n(g_book) > 0) { g_dir_open = 1; g_menu_open = 0; reader_redraw(); } else { g_menu_open = 0; reader_redraw(); } return; case 2: /* 界面(阅读排版设置) */ g_ui_open = 1; g_menu_open = 0; reader_redraw(); return; case 3: /* 设置 → App 设置页 */ reader_reflow_persist(); meow_view_page_push(ACT_SETTINGS); return; } if (y >= y0) return; } g_menu_open = 0; /* 点菜单外关闭 */ reader_redraw(); return; } /* 三栏:左 1/3 上一页、右 1/3 下一页、中央唤菜单 */ if (x < g_pw / 3) { reader_turn_animated(-1); } else if (x > g_pw * 2 / 3) { reader_turn_animated(1); } else { if (g_anim) { meow_anim_cancel(g_anim); g_anim = NULL; } g_menu_open = 1; reader_redraw(); } } void reader_on_swipe(int dir, int dx, int dy) { (void)dx; (void)dy; if (!g_book) return; if (g_scrub) g_scrub = 0; /* 刮擦中被滑走 → 退出刮擦 */ if (g_ui_open) { g_ui_open = 0; reader_redraw(); return; } if (g_menu_open) { g_menu_open = 0; reader_redraw(); return; } if (dir == MEOW_SWIPE_LEFT) reader_turn_animated(1); else if (dir == MEOW_SWIPE_RIGHT) reader_turn_animated(-1); } /* ---- R3 段刮擦触控(app.c on_touch_down/on_touch_move 路由) ---- */ void reader_on_touch_down(int x, int y) { reader_scrub_begin(x, y); } void reader_on_touch_move(int x, int y) { (void)x; if (g_scrub && g_book) reader_scrub_preview(scrub_page_for_y(y)); } /* 系统翻页信号(E):reader_ipc 线程收到 MSG_KEY_PAGEUP/PAGEDOWN 后置 pending 计数; * 真正翻页在 UI 线程 reader_on_tick 消费(避免跨线程直接改页面状态)。 */ void reader_page_key(int dir) { int p = g_volpage_pending + dir; if (p > 2) p = 2; if (p < -2) p = -2; g_volpage_pending = p; } void reader_on_tick(uint64_t now) { (void)now; if (g_volpage_pending) { int dir = g_volpage_pending > 0 ? 1 : -1; g_volpage_pending = 0; if (g_book && !g_menu_open && !g_dir_open && !g_ui_open) reader_turn_animated(dir); } if (g_notice_ms > 0) { g_notice_ms--; if (g_notice_ms == 0) reader_redraw(); } }