/* test/shelf_scroll_smoke.c — M41 书架滚动宿主回归(真实 shelf.c + 真实 SDK 绘制,离屏 480x720)。 * * 背景(真机 bug):0.1.7 书架「列表和宫格都滑不动」。根因是书架**没有任何滚动输入通路**: * ① app_on_touch_down/move 只路由给阅读页 → 没有跟手拖动; * ② shelf_on_swipe 用 dy 去 scroll_by,而 SDK 固定 on_swipe(dir, dist, 0)(位移在第 2 参)→ 恒滚 0。 * 本测试直接打 App 层入口(与 app.c 的调用顺序一致),用 back_buf 像素证明「视口确实滚了」: * - format/folder × list/grid 四种组合都能滚(swipe 的 dist 在第 2 参); * - 跟手拖动(on_touch_down/move)能滚,往返回到原位; * - 内容不足一屏(3 本书的目录)时滑动**不动**(根层 3 行不该滚); * - 拖动后的抬手不当作点击(不会误进目录),干净的点击仍能进目录; * - folder 导航(点目录进入 / back 回上一级)在滚动改动后仍工作。 * * 编译/运行见 build_reader.sh(宿主 gcc + -DMEOW_UI_HOST_TEST + -DREADER_SD_ROOT="<临时目录>"; * reader_open 在本文件打桩,不链阅读页)。 */ #include "reader_internal.h" #include "meow_ui_internal.h" /* meow_ui_back_buf / pitch / bpp */ #include #include #include #include #include #include static int g_checks, g_fails; #define CHECK(cond, fmt, ...) do { \ g_checks++; \ if (cond) printf(" ok " fmt "\n", ##__VA_ARGS__); \ else { g_fails++; printf(" FAIL " fmt "\n", ##__VA_ARGS__); } \ } while (0) /* 阅读页打桩:书架点书会调它;本测试只验滚动/导航,不打开书。 */ int reader_open(const char *path) { (void)path; return -1; } /* ---- 造一棵假 Books/ 树 ---------------------------------------------------- */ static void mkdir_p(const char *p) { char tmp[600]; snprintf(tmp, sizeof tmp, "%s", p); for (char *c = tmp + 1; *c; c++) { if (*c != '/') continue; *c = 0; mkdir(tmp, 0755); *c = '/'; } mkdir(tmp, 0755); } static void mkbook(const char *root, const char *sub, int n, int idx) { char dir[600], path[640]; snprintf(dir, sizeof dir, "%s/Books/%s", root, sub); mkdir_p(dir); snprintf(path, sizeof path, "%s/book%03d.txt", dir, idx); FILE *f = fopen(path, "wb"); if (!f) { printf(" FAIL 无法创建 %s\n", path); g_fails++; return; } fprintf(f, "test book %d\n", idx); fclose(f); (void)n; } /* ---- 视口像素签名(内容区,步进采样;只比像素,不依赖内部控件状态) ---- */ static unsigned long content_hash(void) { unsigned char *bb = meow_ui_back_buf(); int pitch = meow_ui_back_buf_pitch(), bpp = meow_ui_back_buf_bpp(); int y0 = meow_ui_statusbar_height() + 56 + 22; /* 与 shelf_content_top 同口径(M41 宏) */ if (!bb || pitch <= 0) return 0; unsigned long h = 5381; for (int y = y0; y < 720; y += 2) { for (int x = 0; x < 480; x += 2) { uint32_t v; if (bpp == 2) v = *(uint16_t *)(bb + (size_t)y * pitch + (size_t)x * 2); else v = *(uint32_t *)(bb + (size_t)y * pitch + (size_t)x * 4); h = h * 33u ^ v; } } return h; } static int content_top(void) { return meow_ui_statusbar_height() + 56 + 22; } /* list 行高:与 SDK `listv_item_h` 同口径(双行 = max(56, 2*line_h+2));测试只用它划安全比对带。 */ static int list_item_h(void) { const meow_theme *th = meow_theme_get(); int lh = meow_ui_text_height(th ? th->font_body : 20); int h = 2 * lh + 2; return h > 56 ? h : 56; } /* ---- 内容区逐行快照:断言「像素恰好位移 N px」,而不是只断言「变了」 ---- */ static unsigned char *g_snap; static int g_snap_h; static void snap_take(void) { unsigned char *bb = meow_ui_back_buf(); int pitch = meow_ui_back_buf_pitch(), bpp = meow_ui_back_buf_bpp(); int top = content_top(); int h = 720 - top; size_t rowsz = (size_t)480 * (size_t)(bpp > 0 ? bpp : 4); free(g_snap); g_snap = NULL; g_snap_h = 0; if (!bb || pitch <= 0 || h <= 0) return; g_snap = (unsigned char *)malloc((size_t)h * rowsz); if (!g_snap) return; for (int y = 0; y < h; y++) memcpy(g_snap + (size_t)y * rowsz, bb + (size_t)(top + y) * pitch, rowsz); g_snap_h = h; } /* 当前画面 == 快照整体上移 shift px(内容上滚 shift)。只比中部带:视口顶/底各裁掉行高, * 避开「行被视口裁一半时文字不画」的绘制差异;底界再减去 shift(超出快照范围的部分是滚动后才露出的新内容)。 */ static int shift_equal(int shift) { unsigned char *bb = meow_ui_back_buf(); int pitch = meow_ui_back_buf_pitch(), bpp = meow_ui_back_buf_bpp(); int top = content_top(); if (!g_snap || !bb || pitch <= 0 || shift < 0) return 0; size_t rowsz = (size_t)480 * (size_t)(bpp > 0 ? bpp : 4); int ih = list_item_h(); int y0 = 2 * ih; int y1 = (720 - top) - ih - 4 - shift; if (y1 <= y0) return 0; for (int y = y0; y < y1; y++) { int sy = y + shift; if (sy < 0 || sy >= g_snap_h) return 0; if (memcmp(g_snap + (size_t)sy * rowsz, bb + (size_t)(top + y) * pitch, rowsz) != 0) return 0; } return 1; } /* 模拟一次完整 SDK 手势:按下 → 移动(可多步)→ 抬手。 * 抬手事件按 SDK 判据:|dy| ≥ 65(SWIPE_THRESH)→ SWIPE(UP/DOWN, dist);否则 TAP(release 点)。 * 节流后「抬手补终帧」正是靠这两条收尾路径触发的,测试必须照实模拟,不能直接改断言。 */ static void gesture_steps(int x, int y0, int y1, int steps) { shelf_on_touch_down(x, y0); for (int i = 1; i <= steps; i++) shelf_on_touch_move(x, y0 + (y1 - y0) * i / steps); int dy = y1 - y0; int ady = dy < 0 ? -dy : dy; if (ady >= 65) shelf_on_swipe(dy < 0 ? MEOW_SWIPE_UP : MEOW_SWIPE_DOWN, ady, 0); else (void)shelf_on_tap(x, y1); } static void gesture(int x, int y0, int y1) { gesture_steps(x, y0, y1, 1); } int main(int argc, char **argv) { (void)argc; (void)argv; /* 扫描根 = 编译期 READER_SD_ROOT(宿主编译时指向临时目录;见 build_reader.sh) */ const char *root = READER_SD_ROOT; /* 树:Books 根 40 本 + Books/big 40 本(可滚);Books/small 3 本(不足一屏,不该滚)。 */ for (int i = 0; i < 40; i++) mkbook(root, "", 40, i); for (int i = 0; i < 40; i++) mkbook(root, "big/", 40, i); for (int i = 0; i < 3; i++) mkbook(root, "small/", 3, i); if (g_fails) return 1; meow_ui_opts o = { 480, 720 }; if (meow_ui_init(&o) != 0) { printf("meow_ui_init failed\n"); return 1; } meow_view *shelf = shelf_view_create(); meow_view_page_push(ACT_SHELF); /* → shelf_on_enter:扫描 + 建控件 + 首绘 */ if (!shelf_is_active()) { printf("shelf 未激活\n"); return 1; } int top = content_top(); int ih = list_item_h(); /* ===== 1) format + list:swipe 的位移在第 2 参(这就是真机 bug) ===== */ printf("[1] format/list swipe\n"); unsigned long h0 = content_hash(); shelf_on_swipe(MEOW_SWIPE_DOWN, 120, 0); /* 已在顶:往下滑不该动 */ CHECK(content_hash() == h0, "顶部下滑不动"); shelf_on_swipe(MEOW_SWIPE_UP, 0, 0); /* 位移 0:不动(旧实现在这里恒滚 0) */ CHECK(content_hash() == h0, "dist=0 不滚"); snap_take(); shelf_on_swipe(MEOW_SWIPE_UP, 120, 0); /* SDK 形态:on_swipe(dir, dist, 0) */ CHECK(content_hash() != h0, "swipe UP(dist 在 dx) 视口变化"); CHECK(shift_equal(120), "swipe UP 恰好位移 120px(不是只动 1px)"); unsigned long h1 = content_hash(); shelf_on_swipe(MEOW_SWIPE_UP, 120, 0); unsigned long h2 = content_hash(); CHECK(h2 != h1, "连续 swipe UP 继续前进"); shelf_on_swipe(MEOW_SWIPE_DOWN, 100000, 0); /* 一次滑回顶(clamp) */ CHECK(content_hash() == h0, "swipe DOWN 越界 clamp 回顶部像素一致"); shelf_on_swipe(MEOW_SWIPE_UP, 60, 0); /* dy 兼容分支 */ unsigned long h3 = content_hash(); CHECK(h3 != h0, "swipe UP 亦认 dy 参数(兼容旧调用)"); shelf_on_swipe(MEOW_SWIPE_DOWN, 60, 0); CHECK(content_hash() == h0, "dy 分支回来像素一致"); /* ===== 2) format + list:跟手拖动 ===== */ printf("[2] format/list 跟手拖动\n"); snap_take(); gesture(240, top + 100, top + 40); /* 手指上移 60 → 内容上滚 60 */ CHECK(content_hash() != h0, "拖动 60px 视口变化"); CHECK(shift_equal(60), "拖动 60px 恰好位移 60px"); gesture(240, top + 100, top + 160); /* 反向拖 60 回原位 */ CHECK(content_hash() == h0, "反向拖回像素复原"); gesture(240, top + 160, top + 200); /* 继续下滑(越过顶部 → clamp) */ CHECK(content_hash() == h0, "顶部继续下滑仍 clamp"); /* 节流回归:6 步 ×10px 的拖动中途可能被节流跳过,抬手**必须**补终帧把最后一段画出来 */ snap_take(); gesture_steps(240, top + 200, top + 140, 6); CHECK(shift_equal(60), "6 步拖动节流合并后终帧仍恰好位移 60px"); gesture(240, top + 200, top + 260); /* 回原位 */ CHECK(content_hash() == h0, "多步拖动后回顶部像素复原"); /* ===== 2b) 安全边界:顶栏/副题起手不拖、横向手势不滚 ===== */ printf("[2b] 边界:顶栏/副题/横向\n"); shelf_on_touch_down(240, 10); /* 顶栏(含状态栏带):不起手 */ shelf_on_touch_move(240, 200); shelf_on_touch_move(240, 10); CHECK(content_hash() == h0, "顶栏起手的拖动不滚列表"); shelf_on_touch_down(240, top - 6); /* 副题行(内容区上沿之上):不起手 */ shelf_on_touch_move(240, top + 120); CHECK(content_hash() == h0, "副题区起手的拖动不滚"); shelf_on_touch_down(240, top + 100); /* 横向 80px / 纵向 4px → 判向取消 */ shelf_on_touch_move(320, top + 104); shelf_on_touch_move(400, top + 106); CHECK(content_hash() == h0, "横向手势不滚(不抢左缘返回)"); shelf_on_touch_move(240, top + 300); /* 取消后纵向移动也不该复活 */ CHECK(content_hash() == h0, "横向锁存后纵向移动仍不滚"); /* 该手势的抬手由 SDK 判成 SWIPE_LEFT(app.c 处理,根层退出)或 TAP(仅选中行), * 都不进滚动路径;这里不呼叫,直接由下一次按下收尾。 */ /* ===== 3) folder + list:导航 + 滚动 + 不足一屏不动 ===== */ printf("[3] folder/list 导航与滚动\n"); shelf_set_group(1); CHECK(shelf_back() == 0, "folder 根层 shelf_back=0(未误进目录)"); /* 20px 纵向拖动:> 8px 吞 TAP 阈值(所以它会吞掉抬手),但 < 65px 所以 SDK 报的是 TAP。 * 抬手坐标落在行 0(目录)上:不得进目录。 */ gesture(240, top + 10, top + 30); CHECK(shelf_back() == 0, "20px 拖动后的抬手不误进目录(行 0=目录)"); /* 40px / 60px(仍 <65,SDK 报 TAP):同上,必须被吞 */ gesture(240, top + 10, top + 50); CHECK(shelf_back() == 0, "40px 拖动后的抬手不误进目录"); gesture(240, top + 10, top + 70); CHECK(shelf_back() == 0, "60px 拖动后的抬手不误进目录"); /* 4–8px 抖动带:达不到吞 TAP 阈值 → 仍算点击(用户在顶部往下抖 6px 不该被当成滑动) */ gesture(240, top + 10, top + 16); CHECK(shelf_back() == 1, "6px 抖动仍算点击(能进目录,back 有上一级)"); /* 干净点击行 0(目录 big,40 本)→ 再进去做滚动测试 */ gesture(240, top + 10, top + 10); /* 现在在 Books/big(40 本)→ 可滚 */ unsigned long f0 = content_hash(); snap_take(); shelf_on_swipe(MEOW_SWIPE_UP, 120, 0); CHECK(content_hash() != f0, "folder 子目录内 swipe UP 生效"); CHECK(shift_equal(120), "folder 子目录内 swipe 恰好位移 120px"); snap_take(); gesture(240, top + 200, top + 140); CHECK(content_hash() != f0, "folder 子目录内拖动生效"); CHECK(shift_equal(60), "folder 子目录内拖动恰好位移 60px"); CHECK(shelf_back() == 1, "从 Books/big 回 Books 根"); CHECK(shelf_back() == 0, "Books 根再 back=0(调用方据此退出 App)"); /* 进入 small(3 本,不足一屏)→ 滑动不动 */ gesture(240, top + 10 + ih, top + 10 + ih); /* 行 1 = small(行 0=big) */ unsigned long s0 = content_hash(); shelf_on_swipe(MEOW_SWIPE_UP, 300, 0); CHECK(content_hash() == s0, "3 本书的目录:滑动不动(不足一屏)"); gesture(240, top + 100, top + 20); CHECK(content_hash() == s0, "3 本书的目录:拖动不动"); CHECK(shelf_back() == 1, "从 small 返回 Books 根"); /* ===== 4) folder + grid ===== */ printf("[4] folder/grid\n"); shelf_set_view(1); unsigned long g0 = content_hash(); shelf_on_swipe(MEOW_SWIPE_UP, 120, 0); CHECK(content_hash() != g0, "grid swipe UP 生效"); shelf_on_swipe(MEOW_SWIPE_DOWN, 100000, 0); CHECK(content_hash() == g0, "grid 回到顶部"); gesture(240, top + 100, top + 20); CHECK(content_hash() != g0, "grid 拖动生效"); /* ===== 5) format + grid ===== */ printf("[5] format/grid\n"); shelf_set_group(0); shelf_set_view(0); shelf_set_view(1); unsigned long fg0 = content_hash(); shelf_on_swipe(MEOW_SWIPE_UP, 120, 0); CHECK(content_hash() != fg0, "format/grid swipe UP 生效"); gesture(240, top + 200, top + 140); CHECK(content_hash() != fg0, "format/grid 拖动生效"); /* ===== 6) format + list 回归:format 行序列仍在(40 本 → 可滚) ===== */ printf("[6] format/list 收尾\n"); shelf_set_view(0); unsigned long l0 = content_hash(); shelf_on_swipe(MEOW_SWIPE_UP, 120, 0); CHECK(content_hash() != l0, "回到 format/list 仍可滑"); free(g_snap); meow_view_free(shelf); meow_ui_shutdown(); printf("\n%d checks, %d failed\n", g_checks, g_fails); return g_fails ? 1 : 0; }