Files
MeowOS-SDK/tuku/ui/reader/reader.c
T
AskaEth 23c9db73df feat(tuku): 阅读器左右翻页核心——页源契约/预解码窗口/跟手翻页/方向感知 tap
- 页源契约(logic/reader):count/meta/open/close + 目录册(文件夹即书)为第一个实现;
  meta 由纯 C 图片头探测(PNG IHDR/JPEG SOF/BMP)提供原始尺寸,条漫可先排版再取图,
  zip 册(M9/M11)只需另写 mr_book_ops,两种消费方式零改动复用
- 翻页引擎:pthread 后台解码窗口=当前±1 页(目录册 downscale 到屏幕长边),翻页只 blit
  缓存页图 + meow_anim 150ms 滑动吸附;未命中立即切页显示加载态、不在翻页路径解码;
  只 flush 内容区;decode/flip 首帧/翻页完成/进出内存全部 meow_log 打点
- 阅读页 UI:fit 三档(适宽/适高/原始,TopAppBar 动作循环,标题显示页码 x/y)+
  双击以点击点为锚放大(临时缓冲超上限自动降倍率)+ 单指拖拽/滑动步进平移;
  tap 三分区方向感知(RTL 默认:左=下一页,LTR 反之),中区切 chrome;
  左缘返回交给 app.c、屏底热区交给 SDK 回桌面,两者都不拦截
- 跟手:app.c 仅补 2 处极小路由(ACT_READER 的 down/move),reader 自行算位移;
  屏底 28px/左缘 24px 不进拖动,竖向主导放弃横滑
- 内存纪律:驻留 ≤3 张 downscale 页图;放大时窗口收缩到当前页;on_leave 停解码线程、
  释放全部页图/册索引/TopAppBar,并记录 VmRSS/MemAvailable 前后对比
- 测试/构建:新增 test_reader_core(契约边界/空册/越界/重复 open-close/首末页/头探测/
  方向语义/fit/平移夹取,108 项);run_tests.sh 与 build_tuku.sh 源列表接线;
  宿主 46+108 全 PASS,交叉构建静态 MIPS32r2 ELF、-Wall -Wextra 零警告
2026-11-04 20:58:05 +08:00

1067 lines
35 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ui/reader/reader.c — 阅读器页(ACT_READER,全屏):左右翻页模式。
*
* 用户口径:优先流畅度,其次功能。翻页路径不解码、不重排:
* - 后台 pthread 解码窗口(当前 ±1 页),主循环只取已备好的图;
* - 跟手拖动只 blit 两张已缓存页图,松手后 meow_anim 一次短滑动吸附(150ms);
* - 只 flush 受影响区(本页=整屏内容区);翻页/解码/驻留耗时都 meow_log 打点。
*
* 页源:logic/reader/page_source 的 mr_book 契约(count/meta/open/close)。目录册
* (文件夹即书)是第一个实现;zip 册与条漫只需另写 provider / 消费方式,本页零改动。
* 方向:一律读 M3 的 mr_reader_get_direction()(默认 RTL),不另立全局。
*
* 手势(与 SDK 语义对齐,见 sdk/meow_ui_input.c touch_release):
* - 左缘(x<24)向右划 = 系统返回(MEOW_SWIPE_LEFT)→ 不消费,app.c page_pop 退出;
* - 屏底 28px 上滑 = 系统回桌面(MEOW_EV_HOME),SDK 不分发给 App,本页不起手;
* - 中部横拖 = 跟手翻页(本页用 app.c 路由来的原始 down/move 自己算位移);
* - tap 分区方向感知:屏左/右 1/3 = 上/下一页,中 1/3 = chrome 显隐;双击 = 放大。
* - 物理键不映射(app.c 只记日志)。
*
* 节拍:app.c 的 on_tick 只转给 library_on_tick(不在本页 scope)。本页用一条常驻
* meow_anim “ticker”(SDK 主循环每轮 meow_anim_tick)驱动结果收取与双击判定超时,
* 不新增常驻轮询线程,也不改 on_tick 路由。翻页动画本身也走同一节拍。
*
* 生命周期:on_enter 建状态 + 建 TopAppBar + 预取首页;on_leave 全量释放(停解码线程、
* 释放全部页图/册索引/控件)。控件在 on_enter 建 → owner=ACT_READER(非根页防泄漏)。
*/
#include "tuku_internal.h"
#include "ui/reader/reader.h"
#include "logic/reader/page_source.h"
#include "logic/reader/nav.h"
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MR_WIN 3 /* 预解码窗口:当前 ±1 页 */
#define MR_TOPBAR_H 56
#define MR_SLIDE_MS 150 /* 松手后一次短滑动吸附 */
#define MR_BOUNCE_MS 120 /* 未过阈值回弹 */
#define MR_DOUBLE_MS 240 /* 单击等待窗口(区分双击) */
#define MR_ZOOM_PCT 200 /* 双击放大倍率 */
#define MR_TICK_MS 16
#define MR_PAINT_MS 16 /* 跟手拖动重绘节流(≈60fps 上限) */
#define MR_DRAG_SLOP 8 /* 小于此位移算点击 */
#define MR_ZOOM_TEMP_CAP (6 * 1024 * 1024) /* 缩放临时缓冲软上限(>此值降倍率,保内存) */
#define MR_REQ_MAX 8
#define MR_RES_MAX 4
/* view on_event 的事件号不在公共头(sdk/meow_ui_internal.h §6.4);本页只用到 SWIPE=3。 */
#define MR_EV_SWIPE 3
static mr_page_ctrl g_ctrl;
static char g_dir[MR_PATH_MAX];
static mr_dir_book g_db;
static mr_book g_book;
static int g_count; /* 0=空册/不可读 */
static int g_open_err;
static int g_page;
static int g_vw, g_vh; /* 内容视口 = 全屏 */
static int g_fit = MR_FIT_WIDTH;
static int g_zoom_pct = 100;
static int g_pan_x, g_pan_y;
static int g_chrome = 1;
static meow_widget *g_tb;
/* ---- 页图缓存(≤3 张 downscale 后的图)---- */
typedef struct page_slot {
int idx; /* -1=空 */
meow_img *img; /* 已 downscale 的页图 */
int src_w, src_h;
} page_slot;
static page_slot g_slot[MR_WIN];
/* ---- 后台解码队列 ---- */
typedef struct page_req { int idx; unsigned gen; } page_req;
typedef struct page_res { int idx; unsigned gen; meow_img *img; } page_res;
static pthread_t g_worker;
static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t g_cv = PTHREAD_COND_INITIALIZER;
static int g_worker_on, g_stop;
static unsigned g_gen = 1;
static page_req g_req[MR_REQ_MAX];
static int g_req_head, g_req_tail, g_req_n;
static page_res g_res[MR_RES_MAX];
static int g_res_n;
/* ---- 节拍/动画 ---- */
static meow_anim *g_ticker;
static meow_anim *g_snap; /* 翻页滑动吸附 */
static meow_anim *g_bounce; /* 未过阈值回弹 */
static int g_slide_delta; /* 翻页目标增量(动画中) */
static int g_slide_from; /* 进场页起点(px) */
static int g_anim_v; /* 进场页当前位移(px) */
static uint64_t g_flip_t0;
static uint64_t g_last_tick;
static uint64_t g_last_paint;
static int g_quitting; /* on_leave 期间抑制回调 */
/* ---- 跟手拖动 ---- */
static int g_dragging; /* 1=本次按下正在拖动 */
static int g_drag_mode; /* 0=无 1=横滑翻页 2=放大平移 */
static int g_drag_x0, g_drag_y0;
static int g_drag_off; /* 当前页水平位移(px) */
static int g_drag_pan_x0, g_drag_pan_y0;
static int g_touch_moved; /* 本次按下是否已产生 >slop 的位移 */
/* ---- 单击延迟(双击判定窗口)---- */
static int g_tap_pending;
static int g_tap_x, g_tap_y;
static uint64_t g_tap_ms;
static uint64_t g_last_tap_ms; /* 上一次点击(跨按下保持,双击判定用) */
static int g_last_tap_x, g_last_tap_y;
/* ============================================================ */
/* 页源解码回调(注入 logic 层的 mr_book 契约;可在后台线程调用) */
/* ============================================================ */
static meow_img *reader_load_page(const char *path, void *ud, int maxdim)
{
(void)ud;
meow_img *im = meow_img_load_file(path);
if (!im) return NULL;
meow_img *small = meow_img_downscale(im, maxdim); /* 大图先缩再驻留 */
if (small) { meow_img_free(im); im = small; }
return im;
}
static void reader_unload_page(meow_img *img)
{
meow_img_free(img);
}
/* ============================================================ */
/* 内存观测(验收用:进出 reader 对比) */
/* ============================================================ */
static long mem_kb(const char *path, const char *key)
{
FILE *f = fopen(path, "r");
if (!f) return -1;
char line[128];
long v = -1;
size_t kl = strlen(key);
while (fgets(line, sizeof line, f)) {
if (strncmp(line, key, kl) == 0) { v = strtol(line + kl, NULL, 10); break; }
}
fclose(f);
return v;
}
static int resident_pages(void)
{
int n = 0;
for (int i = 0; i < MR_WIN; i++)
if (g_slot[i].img) n++;
return n;
}
static long resident_bytes(void)
{
long b = 0;
for (int i = 0; i < MR_WIN; i++)
if (g_slot[i].img) b += (long)g_slot[i].src_w * g_slot[i].src_h * 4;
return b;
}
static void log_mem(const char *tag)
{
meow_log(MEOW_LOG_INFO, "reader mem[%s]: avail=%ldKB rss=%ldKB pages=%d img=%ldKB",
tag, mem_kb("/proc/meminfo", "MemAvailable:"),
mem_kb("/proc/self/status", "VmRSS:"), resident_pages(), resident_bytes() / 1024);
}
/* ============================================================ */
/* 页图缓存 */
/* ============================================================ */
static page_slot *slot_find(int idx)
{
if (idx < 0) return NULL;
for (int i = 0; i < MR_WIN; i++)
if (g_slot[i].idx == idx && g_slot[i].img) return &g_slot[i];
return NULL;
}
static void slot_clear(int i)
{
if (i < 0 || i >= MR_WIN) return;
if (g_slot[i].img) meow_img_free(g_slot[i].img);
g_slot[i].img = NULL;
g_slot[i].idx = -1;
g_slot[i].src_w = g_slot[i].src_h = 0;
}
static void slots_free_all(void)
{
for (int i = 0; i < MR_WIN; i++) slot_clear(i);
}
/* 放进缓存;缓存满时淘汰离当前页最远的一张。返回 1=已放入。 */
static int slot_put(int idx, meow_img *img)
{
if (!img || idx < 0) return 0;
int empty = -1, far = -1, far_d = -1;
for (int i = 0; i < MR_WIN; i++) {
if (g_slot[i].idx == idx && g_slot[i].img) { meow_img_free(img); return 0; }
if (!g_slot[i].img && empty < 0) empty = i;
int d = !g_slot[i].img ? -1 : abs(g_slot[i].idx - g_page);
if (d > far_d) { far_d = d; far = i; }
}
int i = empty >= 0 ? empty : far;
if (i < 0) { meow_img_free(img); return 0; }
slot_clear(i);
g_slot[i].idx = idx;
g_slot[i].img = img;
g_slot[i].src_w = meow_img_w(img);
g_slot[i].src_h = meow_img_h(img);
return 1;
}
/* 当前窗口:正常=当前±1;放大时只驻留当前页(给缩放临时缓冲让内存)。 */
static int in_window(int idx)
{
if (g_count <= 0 || idx < 0 || idx >= g_count) return 0;
if (g_zoom_pct > 100) return idx == g_page;
return idx >= g_page - 1 && idx <= g_page + 1;
}
/* ============================================================ */
/* 后台解码 */
/* ============================================================ */
static int req_queued(int idx)
{
for (int i = 0; i < g_req_n; i++) {
int p = (g_req_head + i) % MR_REQ_MAX;
if (g_req[p].idx == idx) return 1;
}
return 0;
}
static void queue_page(int idx)
{
if (!mr_book_valid(&g_book) || idx < 0 || idx >= g_count) return;
if (slot_find(idx)) return;
pthread_mutex_lock(&g_lock);
if (!g_stop && !req_queued(idx) && g_req_n < MR_REQ_MAX) {
g_req[g_req_tail].idx = idx;
g_req[g_req_tail].gen = g_gen;
g_req_tail = (g_req_tail + 1) % MR_REQ_MAX;
g_req_n++;
pthread_cond_signal(&g_cv);
}
pthread_mutex_unlock(&g_lock);
}
static void *reader_worker(void *arg)
{
(void)arg;
for (;;) {
page_req req;
pthread_mutex_lock(&g_lock);
while (!g_stop && g_req_n == 0) pthread_cond_wait(&g_cv, &g_lock);
if (g_stop) { pthread_mutex_unlock(&g_lock); break; }
req = g_req[g_req_head];
g_req_head = (g_req_head + 1) % MR_REQ_MAX;
g_req_n--;
pthread_mutex_unlock(&g_lock);
uint64_t t0 = meow_ui_time_ms();
meow_img *img = mr_book_open(&g_book, req.idx);
uint64_t dt = meow_ui_time_ms() - t0;
int w = img ? meow_img_w(img) : 0;
int h = img ? meow_img_h(img) : 0;
meow_log(MEOW_LOG_INFO, "reader decode page=%d ms=%llu %dx%d %s",
req.idx, (unsigned long long)dt, w, h, img ? "ok" : "fail");
pthread_mutex_lock(&g_lock);
if (img && !g_stop && g_res_n < MR_RES_MAX) {
g_res[g_res_n].idx = req.idx;
g_res[g_res_n].gen = req.gen;
g_res[g_res_n].img = img;
g_res_n++;
img = NULL;
}
pthread_mutex_unlock(&g_lock);
if (img) meow_img_free(img);
}
return NULL;
}
static void worker_start(void)
{
if (g_worker_on) return;
g_stop = 0;
if (pthread_create(&g_worker, NULL, reader_worker, NULL) == 0) g_worker_on = 1;
}
static void worker_stop(void)
{
if (!g_worker_on) return;
pthread_mutex_lock(&g_lock);
g_stop = 1;
pthread_cond_broadcast(&g_cv);
pthread_mutex_unlock(&g_lock);
pthread_join(g_worker, NULL);
g_worker_on = 0;
pthread_mutex_lock(&g_lock);
for (int i = 0; i < g_res_n; i++) meow_img_free(g_res[i].img);
g_res_n = 0;
g_req_n = 0;
g_req_head = g_req_tail = 0;
pthread_mutex_unlock(&g_lock);
}
/* 丢弃窗口外的排队请求(主线程调用)。 */
static void queue_prune(void)
{
pthread_mutex_lock(&g_lock);
page_req keep[MR_REQ_MAX];
int n = 0;
for (int i = 0; i < g_req_n; i++) {
page_req r = g_req[(g_req_head + i) % MR_REQ_MAX];
if (r.gen == g_gen && in_window(r.idx)) keep[n++] = r;
}
for (int i = 0; i < n; i++) g_req[i] = keep[i];
g_req_head = 0;
g_req_tail = n;
g_req_n = n;
pthread_mutex_unlock(&g_lock);
}
/* 收取后台结果:只留窗口内新页。返回 1=有图落位。 */
static int results_drain(void)
{
page_res got[MR_RES_MAX];
int n;
pthread_mutex_lock(&g_lock);
n = g_res_n;
for (int i = 0; i < n; i++) got[i] = g_res[i];
g_res_n = 0;
pthread_mutex_unlock(&g_lock);
int changed = 0;
for (int i = 0; i < n; i++) {
if (got[i].gen == g_gen && in_window(got[i].idx) && !slot_find(got[i].idx)) {
if (slot_put(got[i].idx, got[i].img)) changed = 1;
} else {
meow_img_free(got[i].img);
}
}
return changed;
}
/* 窗口随当前页移动:淘汰窗口外图 → 清掉窗口外请求 → 补齐缺失页(当前页优先)。 */
static void window_update(void)
{
for (int i = 0; i < MR_WIN; i++)
if (g_slot[i].idx >= 0 && !in_window(g_slot[i].idx)) slot_clear(i);
queue_prune();
queue_page(g_page);
if (g_page - 1 >= 0) queue_page(g_page - 1);
if (g_page + 1 < g_count) queue_page(g_page + 1);
}
/* ============================================================ */
/* 布局/绘制 */
/* ============================================================ */
static void page_target_size(const page_slot *s, int *w, int *h)
{
*w = *h = 0;
if (!s) return;
if (mr_nav_fit_size(g_fit, s->src_w, s->src_h, g_vw, g_vh, w, h) != 0) return;
if (g_zoom_pct != 100) mr_nav_zoom_size(*w, *h, g_zoom_pct, w, h);
}
static void current_geom(int *w, int *h)
{
page_slot *s = slot_find(g_page);
if (s) { page_target_size(s, w, h); return; }
/* 页未就绪:用 meta 估算,保证平移/缩放的夹取稳定。 */
int sw = 0, sh = 0;
if (mr_book_meta(&g_book, g_page, &sw, &sh) && sw > 0 && sh > 0) {
mr_nav_fit_size(g_fit, sw, sh, g_vw, g_vh, w, h);
if (g_zoom_pct != 100) mr_nav_zoom_size(*w, *h, g_zoom_pct, w, h);
return;
}
*w = g_vw;
*h = g_vh;
}
static void clamp_pan(void)
{
int w, h;
current_geom(&w, &h);
mr_nav_pan_clamp(w, h, g_vw, g_vh, &g_pan_x, &g_pan_y);
}
/* 内容是否超出视口(原始档的方图/宽图即使不放大也需要平移)。 */
static int content_overflow(void)
{
int w, h;
current_geom(&w, &h);
return w > g_vw || h > g_vh;
}
/* 平移模式:放大,或原始档溢出。此时单指拖动=平移,左右 tap=平移步进。 */
static int pan_mode(void)
{
return g_zoom_pct > 100 || content_overflow();
}
/* 画一页(带水平位移);图与目标尺寸相等时走原生 blit(翻页热路径),否则最近邻缩放。 */
static void draw_page_at(int idx, int off_x, int off_y)
{
page_slot *s = slot_find(idx);
if (!s) return;
int w, h;
page_target_size(s, &w, &h);
if (w <= 0 || h <= 0) return;
int x = (g_vw - w) / 2 + g_pan_x + off_x;
int y = (g_vh - h) / 2 + g_pan_y + off_y;
if (w == s->src_w && h == s->src_h) meow_ui_img(x, y, s->img);
else meow_ui_img_scaled(x, y, w, h, s->img);
}
static void reader_draw(meow_view *v, const meow_theme *th, void *ctx)
{
(void)v; (void)ctx;
int w = g_vw, h = g_vh;
if (g_open_err) {
meow_ui_text_center(w / 2, h / 2 - 20, "无法打开目录", th->font_title, th->onSurface);
meow_ui_text_center(w / 2, h / 2 + 24, g_dir, th->font_body, th->onSurfaceVariant);
return;
}
if (g_count <= 0) {
meow_ui_text_center(w / 2, h / 2 - 20, "空册", th->font_title, th->onSurface);
meow_ui_text_center(w / 2, h / 2 + 24, "这个文件夹里没有图片", th->font_body,
th->onSurfaceVariant);
return;
}
if (g_slide_delta != 0) {
/* 动画帧:进场页从 from 滑到 0,当前页同步让位(只 blit 两张已缓存页图)。 */
draw_page_at(g_page, g_anim_v - g_slide_from, 0);
draw_page_at(g_page + g_slide_delta, g_anim_v, 0);
return;
}
if (g_drag_mode == 1 && g_drag_off != 0) {
/* 跟手:当前页按位移走,相邻页在另一侧同时露出(未就绪就只画当前页)。 */
int rtl = mr_reader_get_direction() == MR_DIR_RTL;
int delta = mr_nav_swipe_delta(rtl, g_drag_off);
int target = mr_nav_clamp_page(g_page + delta, g_count);
if (target != g_page && slot_find(target)) {
int from = mr_nav_slide_from(rtl, delta, g_vw);
draw_page_at(target, g_drag_off + from, 0);
}
draw_page_at(g_page, g_drag_off, 0);
return;
}
draw_page_at(g_page, 0, 0);
if (!slot_find(g_page)) {
meow_ui_text_center(w / 2, h / 2, "加载中…", th->font_body, th->onSurfaceVariant);
}
}
/* ============================================================ */
/* 状态动作 */
/* ============================================================ */
static void update_title(void)
{
if (!g_tb) return;
static const char *const fit_name[MR_FIT_COUNT] = { "适宽", "适高", "原始" };
const char *fit = (g_fit >= 0 && g_fit < MR_FIT_COUNT) ? fit_name[g_fit] : "适宽";
char buf[64];
if (g_count > 0) snprintf(buf, sizeof buf, "%d / %d %s", g_page + 1, g_count, fit);
else snprintf(buf, sizeof buf, "0 / 0");
meow_topappbar_set_title(g_tb, buf);
}
static void set_fit(int fit)
{
g_fit = (fit < 0 || fit >= MR_FIT_COUNT) ? MR_FIT_WIDTH : fit;
g_zoom_pct = 100;
g_pan_x = g_pan_y = 0;
update_title();
meow_ui_invalidate(0, 0, g_vw, g_vh);
}
static void set_chrome(int on)
{
on = on ? 1 : 0;
if (g_chrome == on) return;
g_chrome = on;
if (g_tb) meow_widget_set_rect(g_tb, 0, on ? 0 : -MR_TOPBAR_H, g_vw, MR_TOPBAR_H);
meow_ui_invalidate(0, 0, g_vw, MR_TOPBAR_H);
}
/* 平移(dx>0 = 内容右移);放大或原始档溢出时可用。 */
static void pan_by(int dx, int dy)
{
if (!pan_mode()) return;
g_pan_x += dx;
g_pan_y += dy;
clamp_pan();
meow_ui_invalidate(0, 0, g_vw, g_vh);
}
static void commit_page(int target)
{
target = mr_nav_clamp_page(target, g_count);
if (target < 0) return;
g_page = target;
g_zoom_pct = 100;
g_pan_x = g_pan_y = 0;
window_update();
update_title();
meow_ui_invalidate(0, 0, g_vw, g_vh);
}
/* ---- 翻页滑动吸附 ---- */
static void slide_update(meow_anim *a, float value, void *ctx)
{
(void)a; (void)ctx;
g_anim_v = (int)value;
meow_ui_invalidate(0, 0, g_vw, g_vh);
}
static void slide_done(meow_anim *a, int canceled, void *ctx)
{
(void)a; (void)ctx;
g_snap = NULL;
if (g_quitting) return;
int delta = g_slide_delta;
g_slide_delta = 0;
g_slide_from = 0;
g_anim_v = 0;
if (!canceled && delta)
commit_page(g_page + delta);
else
meow_ui_invalidate(0, 0, g_vw, g_vh);
meow_log(MEOW_LOG_INFO, "reader flip done page=%d/%d total_ms=%llu",
g_page + 1, g_count,
(unsigned long long)(meow_ui_time_ms() - g_flip_t0));
}
/* 从 start_off(松手时的位移,无跟手时为 0)滑到目标页。1=动画已启动。 */
static int start_slide(int delta, int start_off)
{
if (g_snap || g_bounce) return 0;
int rtl = mr_reader_get_direction() == MR_DIR_RTL;
g_slide_delta = delta;
g_slide_from = mr_nav_slide_from(rtl, delta, g_vw);
g_anim_v = start_off + g_slide_from;
meow_anim_desc d = {
(float)g_anim_v, 0.0f, MR_SLIDE_MS, MEOW_EASE_OUT,
slide_update, slide_done, NULL,
};
uint64_t t0 = meow_ui_time_ms();
g_snap = meow_anim_start(&d); /* start 即回调 update:首帧在这里绘制 */
uint64_t dt = meow_ui_time_ms() - t0;
if (!g_snap) {
g_slide_delta = 0;
g_slide_from = 0;
g_anim_v = 0;
commit_page(g_page + delta);
return 0;
}
meow_log(MEOW_LOG_INFO, "reader slide first_frame_ms=%llu page=%d->%d",
(unsigned long long)dt, g_page + 1, g_page + delta + 1);
return 1;
}
/* ---- 未过阈值回弹 ---- */
static void bounce_update(meow_anim *a, float value, void *ctx)
{
(void)a; (void)ctx;
g_drag_off = (int)value;
meow_ui_invalidate(0, 0, g_vw, g_vh);
}
static void bounce_done(meow_anim *a, int canceled, void *ctx)
{
(void)a; (void)canceled; (void)ctx;
g_bounce = NULL;
g_drag_off = 0;
g_drag_mode = 0;
if (!g_quitting) meow_ui_invalidate(0, 0, g_vw, g_vh);
}
static void start_bounce(int from_off)
{
if (g_bounce || g_snap) { g_drag_off = 0; g_drag_mode = 0; return; }
meow_anim_desc d = {
(float)from_off, 0.0f, MR_BOUNCE_MS, MEOW_EASE_OUT,
bounce_update, bounce_done, NULL,
};
g_bounce = meow_anim_start(&d);
if (!g_bounce) { g_drag_off = 0; g_drag_mode = 0; meow_ui_invalidate(0, 0, g_vw, g_vh); }
}
/* 点击触发的翻页(无跟手位移)。 */
static void page_flip(int delta)
{
if (delta == 0 || g_count <= 0) return;
int target = mr_nav_clamp_page(g_page + delta, g_count);
if (target == g_page) return; /* 首/末页不动 */
if (g_snap || g_bounce) return; /* 短动画中忽略连点 */
if (g_zoom_pct != 100) { /* 翻页回到基准缩放 */
g_zoom_pct = 100;
g_pan_x = g_pan_y = 0;
}
results_drain();
g_flip_t0 = meow_ui_time_ms();
if (slot_find(target)) {
if (start_slide(delta, 0))
meow_log(MEOW_LOG_INFO, "reader flip %d->%d hit", g_page + 1, target + 1);
else
meow_log(MEOW_LOG_INFO, "reader flip %d->%d hit (no anim slot)", g_page + 1, target + 1);
} else {
meow_log(MEOW_LOG_INFO, "reader flip %d->%d miss (decode pending)", g_page + 1, target + 1);
commit_page(target);
}
}
/* ============================================================ */
/* 跟手拖动 */
/* ============================================================ */
static void drag_paint(void)
{
uint64_t now = meow_ui_time_ms();
int gap = g_zoom_pct > 100 ? 33 : MR_PAINT_MS; /* 缩放帧要重采样:30fps 上限 */
if (now - g_last_paint < (uint64_t)gap) return;
g_last_paint = now;
meow_ui_invalidate(0, 0, g_vw, g_vh);
}
/* 松手:过阈值且目标页已缓存 → 从当前位移吸附过去;否则回弹。 */
static void drag_finish(void)
{
if (!g_dragging) return;
g_dragging = 0;
int dx = g_drag_off;
int mode = g_drag_mode;
g_drag_off = 0;
g_drag_mode = 0;
if (mode != 1 || abs(dx) < MR_DRAG_SLOP) {
meow_ui_invalidate(0, 0, g_vw, g_vh);
return;
}
int rtl = mr_reader_get_direction() == MR_DIR_RTL;
int delta = mr_nav_swipe_delta(rtl, dx);
int target = mr_nav_clamp_page(g_page + delta, g_count);
if (target != g_page && mr_nav_commit(dx, g_vw)) {
g_flip_t0 = meow_ui_time_ms();
if (slot_find(target) && start_slide(delta, dx)) {
meow_log(MEOW_LOG_INFO, "reader drag flip %d->%d hit", g_page + 1, target + 1);
} else {
meow_log(MEOW_LOG_INFO, "reader drag flip %d->%d miss (decode pending)",
g_page + 1, target + 1);
commit_page(target);
}
} else {
g_drag_off = dx; /* start_bounce 从这里起 */
g_drag_mode = 1;
start_bounce(dx);
}
}
void reader_on_touch_down(int x, int y)
{
if (!g_ctrl.active || g_quitting) return;
if (g_count <= 0) return;
if (y >= g_vh - 28) return; /* 屏底热区:系统回桌面,勿劫持 */
if (x < 24) return; /* 左缘:系统返回手势,勿劫持 */
if (g_chrome && y < MR_TOPBAR_H) return; /* chrome 上的点击交给 tap */
if (g_snap || g_bounce) return; /* 短动画中不接新手势 */
g_dragging = 1;
g_drag_mode = pan_mode() ? 2 : 1;
g_drag_x0 = x;
g_drag_y0 = y;
g_drag_off = 0;
g_drag_pan_x0 = g_pan_x;
g_drag_pan_y0 = g_pan_y;
g_touch_moved = 0;
}
void reader_on_touch_move(int x, int y)
{
if (!g_ctrl.active || !g_dragging || g_quitting) return;
int dx = x - g_drag_x0;
int dy = y - g_drag_y0;
int adx = abs(dx), ady = abs(dy);
if (!g_touch_moved && (adx >= MR_DRAG_SLOP || ady >= MR_DRAG_SLOP)) {
g_touch_moved = 1;
g_tap_pending = 0; /* 变成拖动:作废未决单击 */
g_last_tap_ms = 0;
}
if (g_drag_mode == 2) { /* 放大态:单指拖拽平移 */
g_pan_x = g_drag_pan_x0 + dx;
g_pan_y = g_drag_pan_y0 + dy;
clamp_pan();
drag_paint();
return;
}
/* 竖向主导 → 放弃横滑翻页(松手后不算点击,见 reader_on_tap)。 */
if (ady > adx * 3 / 2 && ady > 16) {
g_dragging = 0;
g_drag_mode = 0;
g_drag_off = 0;
meow_ui_invalidate(0, 0, g_vw, g_vh);
return;
}
int rtl = mr_reader_get_direction() == MR_DIR_RTL;
int delta = mr_nav_swipe_delta(rtl, dx);
int target = mr_nav_clamp_page(g_page + delta, g_count);
int off = dx;
if (target == g_page) { /* 首/末页:橡皮筋 */
int lim = g_vw / 4;
if (off > lim) off = lim;
if (off < -lim) off = -lim;
}
g_drag_off = off;
drag_paint();
}
/* ============================================================ */
/* 单击 / 双击 */
/* ============================================================ */
static void tap_topbar(int x, int y)
{
if (!g_tb) return;
int hit = meow_topappbar_hit(g_tb, x, y);
if (hit == MEOW_TOPBAR_BACK) {
meow_view_page_pop();
return;
}
if (hit >= 0) set_fit(mr_nav_fit_next(g_fit));
}
static void tap_content(int x, int y)
{
(void)y;
if (g_count <= 0) return;
int zone = mr_nav_tap_zone(x, g_vw);
if (zone == MR_ZONE_MIDDLE) {
set_chrome(!g_chrome);
return;
}
if (pan_mode()) {
/* 平移模式:左右分区改为平移步进(配合拖拽,补齐两个方向)。 */
pan_by(zone == MR_ZONE_LEFT ? g_vw / 3 : -g_vw / 3, 0);
return;
}
page_flip(mr_nav_tap_delta(mr_reader_get_direction() == MR_DIR_RTL, zone));
}
static void tap_double(int x, int y)
{
if (g_count <= 0) return;
if (g_zoom_pct > 100) {
g_zoom_pct = 100;
g_pan_x = g_pan_y = 0;
} else {
/* 以双击点为锚放大:该点保持在手指下(再夹到可平移范围)。
* 倍率按内存纪律夹取:缩放临时缓冲 = 目标宽×高×4,超过软上限就降档。 */
int bw = 0, bh = 0;
current_geom(&bw, &bh);
int z = MR_ZOOM_PCT;
while (z > 100 && (long long)bw * bh * 4 * z * z / 10000 > MR_ZOOM_TEMP_CAP) z -= 25;
g_zoom_pct = z;
g_pan_x = (g_vw / 2 - x) * (z - 100) / 100;
g_pan_y = (g_vh / 2 - y) * (z - 100) / 100;
clamp_pan();
}
window_update();
meow_ui_invalidate(0, 0, g_vw, g_vh);
}
static void tap_flush(void)
{
if (!g_tap_pending) return;
g_tap_pending = 0;
int x = g_tap_x, y = g_tap_y;
if (g_chrome && y >= 0 && y < MR_TOPBAR_H) tap_topbar(x, y);
else tap_content(x, y);
}
void reader_on_tap(int x, int y)
{
if (!g_ctrl.active || g_quitting) return;
/* 跟手手势收尾:移动过就按拖动处理(过阈值翻页 / 否则回弹),不再当点击。 */
if (g_dragging) {
if (g_touch_moved) {
drag_finish();
g_touch_moved = 0;
return;
}
g_dragging = 0;
g_drag_mode = 0;
g_drag_off = 0;
} else if (g_touch_moved) {
g_touch_moved = 0; /* 竖向拖动被 SDK 报成 TAP:忽略 */
return;
}
g_touch_moved = 0;
/* chrome 上的点击即时生效,不参与双击判定。 */
if (g_chrome && y >= 0 && y < MR_TOPBAR_H) {
g_tap_pending = 0;
g_last_tap_ms = 0;
tap_topbar(x, y);
return;
}
uint64_t now = meow_ui_time_ms();
if (g_last_tap_ms && now - g_last_tap_ms <= MR_DOUBLE_MS &&
abs(x - g_last_tap_x) <= 48 && abs(y - g_last_tap_y) <= 48) {
g_tap_pending = 0; /* 取消第一击的单击动作 */
g_last_tap_ms = 0;
tap_double(x, y);
return;
}
tap_flush(); /* 上一击已过期:先执行 */
g_tap_pending = 1;
g_tap_x = x;
g_tap_y = y;
g_tap_ms = now;
g_last_tap_ms = now;
g_last_tap_x = x;
g_last_tap_y = y;
if (!g_ticker) tap_flush(); /* 无节拍器兜底:不延迟单击 */
}
/* ============================================================ */
/* 节拍:收结果 + 单击超时 */
/* ============================================================ */
static void reader_tick(void)
{
if (!g_ctrl.active || g_quitting) return;
uint64_t now = meow_ui_time_ms();
if (now - g_last_tick < MR_TICK_MS) return;
g_last_tick = now;
int had_current = slot_find(g_page) != NULL;
if (results_drain() && !had_current && slot_find(g_page) && !g_dragging)
meow_ui_invalidate(0, 0, g_vw, g_vh);
if (g_tap_pending && now - g_tap_ms >= MR_DOUBLE_MS) tap_flush();
}
static void ticker_update(meow_anim *a, float value, void *ctx)
{
(void)a; (void)value; (void)ctx;
reader_tick();
}
static void ticker_done(meow_anim *a, int canceled, void *ctx)
{
(void)a; (void)ctx;
g_ticker = NULL;
if (canceled || g_quitting || !g_ctrl.active) return;
meow_anim_desc d = { 0.0f, 1.0f, 0xFFFFFFFFu, MEOW_EASE_LINEAR,
ticker_update, ticker_done, NULL };
g_ticker = meow_anim_start(&d);
}
static void ticker_start(void)
{
if (g_ticker) return;
meow_anim_desc d = { 0.0f, 1.0f, 0xFFFFFFFFu, MEOW_EASE_LINEAR,
ticker_update, ticker_done, NULL };
g_ticker = meow_anim_start(&d);
if (!g_ticker) meow_log(MEOW_LOG_WARN, "reader ticker unavailable (anim pool full)");
}
/* ============================================================ */
/* 输入:滑动手势(view on_event,先于 app.c 的 on_swipe) */
/* ============================================================ */
static int reader_event(meow_view *v, int ev, int a, int b, void *ctx)
{
(void)v; (void)ctx;
if (!g_ctrl.active || g_quitting) return 0;
if (ev != MR_EV_SWIPE) return 0;
if (a == MEOW_SWIPE_LEFT) { /* 左缘返回:交给 app.c page_pop */
g_dragging = 0;
g_drag_mode = 0;
g_drag_off = 0;
g_touch_moved = 0;
return 0;
}
if (a == MEOW_SWIPE_RIGHT) {
if (g_dragging) { drag_finish(); return 1; }
if (g_count <= 0) return 1;
if (pan_mode()) { pan_by(b, 0); return 1; }
page_flip(mr_nav_swipe_delta(mr_reader_get_direction() == MR_DIR_RTL, b));
return 1;
}
if (a == MEOW_SWIPE_UP || a == MEOW_SWIPE_DOWN) {
if (g_dragging) { drag_finish(); return 1; }
if (pan_mode()) { /* 放大 / 原始档溢出:纵向滑动平移 */
pan_by(0, a == MEOW_SWIPE_DOWN ? b : -b);
return 1;
}
return 0; /* 未放大:纵向滑动不归本页 */
}
return 0;
}
/* ============================================================ */
/* view 生命周期 */
/* ============================================================ */
static void reader_state_reset(void)
{
slots_free_all(); /* 静态槽位初值是 idx=0/img=NULL,先清空 */
g_count = 0;
g_open_err = 0;
g_page = 0;
g_fit = MR_FIT_WIDTH;
g_zoom_pct = 100;
g_pan_x = g_pan_y = 0;
g_chrome = 1;
g_slide_delta = 0;
g_slide_from = 0;
g_anim_v = 0;
g_dragging = 0;
g_drag_mode = 0;
g_drag_off = 0;
g_touch_moved = 0;
g_tap_pending = 0;
g_last_tap_ms = 0;
g_quitting = 0;
}
static void reader_page_enter(meow_view *v, void *ctx)
{
mr_page_ctrl *c = (mr_page_ctrl *)ctx;
c->active = 1;
g_quitting = 0;
g_vw = meow_ui_width();
g_vh = meow_ui_height();
v->x = 0;
v->y = 0;
v->w = g_vw;
v->h = g_vh;
meow_ui_set_fullscreen(1);
const meow_theme *th = meow_theme_get();
meow_ui_set_page_bg(th->surface);
reader_state_reset();
g_gen++; /* 作废旧册的异步结果 */
g_last_tick = 0;
g_last_paint = 0;
/* 非根页:控件在 on_enter 建 → owner=ACT_READER(§6.1 跨页泄漏防线)。 */
if (!g_tb) g_tb = meow_topappbar_create(0, 0, g_vw, MR_TOPBAR_H, "");
if (g_tb) {
meow_topappbar_set_back(g_tb, 1);
int ai = meow_topappbar_add_action(g_tb, "");
if (ai >= 0) meow_topappbar_set_item_icon(g_tb, ai, MEOW_ICON_REFRESH);
meow_widget_set_rect(g_tb, 0, 0, g_vw, MR_TOPBAR_H);
}
/* 页源:目录册(文件夹即书)。maxdim 取屏幕长边,页图驻留 ≈ 屏宽×屏高×4。 */
int maxdim = g_vw > g_vh ? g_vw : g_vh;
int n = mr_dir_book_init(&g_db, g_dir, maxdim,
reader_load_page, reader_unload_page, NULL);
if (n < 0) {
g_open_err = 1;
meow_log(MEOW_LOG_WARN, "reader open failed: %s", g_dir);
} else {
g_count = n;
}
mr_dir_book_bind(&g_book, g_count > 0 ? &g_db : NULL);
worker_start();
if (!g_worker_on && g_count > 0) {
/* 线程创建失败:退化为主线程同步解第一页(保证不是纯白屏)。 */
meow_img *first = mr_book_open(&g_book, 0);
if (first) slot_put(0, first);
meow_log(MEOW_LOG_WARN, "reader worker unavailable, sync first page");
}
window_update(); /* 当前页优先进队 */
ticker_start();
update_title();
meow_log(MEOW_LOG_INFO, "reader enter dir=%s pages=%d fit=%d", g_dir, g_count, g_fit);
log_mem("enter");
mr_nav_report();
meow_ui_paint_full();
/* 首页解码是异步的:先给一帧文字/页码,结果在 tick 里落位。 */
reader_tick();
}
static void reader_page_leave(meow_view *v, void *ctx)
{
(void)v;
((mr_page_ctrl *)ctx)->active = 0;
g_quitting = 1;
if (g_snap) { meow_anim_cancel(g_snap); g_snap = NULL; }
if (g_bounce) { meow_anim_cancel(g_bounce); g_bounce = NULL; }
if (g_ticker) { meow_anim_cancel(g_ticker); g_ticker = NULL; }
worker_stop(); /* 先停线程,再释放册/图 */
slots_free_all();
mr_book_destroy(&g_book);
mr_dir_book_free(&g_db);
if (g_tb) { meow_widget_free(g_tb); g_tb = NULL; }
g_dragging = 0;
g_drag_mode = 0;
g_drag_off = 0;
g_tap_pending = 0;
g_slide_delta = 0;
g_slide_from = 0;
g_anim_v = 0;
log_mem("leave");
meow_ui_set_fullscreen(0);
}
meow_view *reader_view_create(void)
{
static const meow_view_hooks h = {
.on_enter = reader_page_enter,
.on_leave = reader_page_leave,
.on_draw = reader_draw,
.on_event = reader_event,
};
return meow_view_create(ACT_READER, MEOW_VIEW_ACTIVITY, &h, &g_ctrl);
}
/* ============================================================ */
/* 书库 → 阅读器导航(app.c / library.c 既有接缝) */
/* ============================================================ */
const char *mr_reader_dir(void)
{
return g_dir;
}
void mr_reader_open_dir(const char *path)
{
if (!path) return;
snprintf(g_dir, sizeof g_dir, "%s", path);
meow_log(MEOW_LOG_INFO, "reader open: %s", g_dir);
meow_view_page_push(ACT_READER);
}