/* 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; } 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(); /* ===== 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 不滚"); shelf_on_swipe(MEOW_SWIPE_UP, 120, 0); /* SDK 形态:on_swipe(dir, dist, 0) */ unsigned long h1 = content_hash(); CHECK(h1 != h0, "swipe UP(dist 在 dx) 视口变化"); 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"); shelf_on_touch_down(240, top + 100); shelf_on_touch_move(240, top + 40); /* 手指上移 60 → 内容上滚 */ unsigned long h4 = content_hash(); CHECK(h4 != h0, "拖动 60px 视口变化"); shelf_on_touch_move(240, top + 100); /* 拖回原位 */ CHECK(content_hash() == h0, "反向拖回像素复原"); shelf_on_touch_move(240, top + 300); /* 继续下滑(越过顶部 → clamp) */ CHECK(content_hash() == h0, "顶部继续下滑仍 clamp"); /* 拖动后的抬手不得当作点击(此时竖坐标恰好落在一本书上;打桩 reader_open 返回 -1, * 这里只验状态机不误触发:拖动结束 → shelf_on_tap 返回 0 且不产生副作用) */ int rt = shelf_on_tap(240, top + 300); CHECK(rt == 0, "拖动后的 TAP 被吞(返回 0)"); /* ===== 3) folder + list:导航 + 滚动 + 不足一屏不动 ===== */ printf("[3] folder/list 导航与滚动\n"); shelf_set_group(1); CHECK(shelf_back() == 0, "folder 根层 shelf_back=0(未误进目录)"); /* 拖动 20px(< SHELF_DRAG_TAP_PX 的 SDK TAP 阈值内)后抬在目录行上:不得进目录 */ shelf_on_touch_down(240, top + 10); shelf_on_touch_move(240, top + 30); (void)shelf_on_tap(240, top + 10); CHECK(shelf_back() == 0, "拖动后的抬手不误进目录(行 0=目录)"); /* 干净点击行 0(目录 big,40 本)→ 进入 */ shelf_on_touch_down(240, top + 10); CHECK(shelf_on_tap(240, top + 10) == 0, "点击目录行返回 0"); /* 现在在 Books/big(40 本)→ 可滚 */ unsigned long f0 = content_hash(); shelf_on_swipe(MEOW_SWIPE_UP, 120, 0); CHECK(content_hash() != f0, "folder 子目录内 swipe UP 生效"); shelf_on_touch_down(240, top + 200); shelf_on_touch_move(240, top + 140); CHECK(content_hash() != f0, "folder 子目录内拖动生效"); CHECK(shelf_back() == 1, "从 Books/big 回 Books 根"); CHECK(shelf_back() == 0, "Books 根再 back=0(调用方据此退出 App)"); /* 进入 small(3 本,不足一屏)→ 滑动不动 */ shelf_on_touch_down(240, top + 10 + 56); /* 行 1 = small(行 0=big) */ (void)shelf_on_tap(240, top + 10 + 56); unsigned long s0 = content_hash(); shelf_on_swipe(MEOW_SWIPE_UP, 300, 0); CHECK(content_hash() == s0, "3 本书的目录:滑动不动(不足一屏)"); shelf_on_touch_down(240, top + 100); shelf_on_touch_move(240, 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 回到顶部"); shelf_on_touch_down(240, top + 100); shelf_on_touch_move(240, 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 生效"); shelf_on_touch_down(240, top + 200); shelf_on_touch_move(240, 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 仍可滑"); meow_view_free(shelf); meow_ui_shutdown(); printf("\n%d checks, %d failed\n", g_checks, g_fails); return g_fails ? 1 : 0; }