P2-1(用户可感知):shelf_on_touch_move 每收到一条 TOUCH_MOVE 就同步全量重绘。 输入线程「坐标一变就一条 MOVE」且不合并(队列 128 条),folder ≤200 行走圆角卡 ≈40ms/帧 → 主循环追不上手指、抬手后还会继续滑、极端情况丢 release。修法: - 滚动位置仍每条 move 立即生效(纯算术,便宜);上屏节流:首帧立即画,之后每 max(16ms, 上一帧实测耗时) 一次(自适应窗口,圆角卡 40ms 帧自动降到 ~40ms 周期, 不再把队列拖垮)。 - 手势收尾(shelf_on_tap / shelf_on_swipe)无条件补终帧再清手势态;下一次按下也会 先把上一次遗留的未上屏帧补上(HOME/长按等没有 TAP/SWIPE 收尾的手势)。 - 位置没变(已到顶/底)连帧都不画。 P2-2(测试盲区): - 新增三条边界:顶栏起手不拖、副题区起手不拖、横向手势判向取消后不再滚动。 - 吞 TAP 边界带:20/40/60px(>8px 且 <65px,SDK 报 TAP)抬在目录行上不得进目录; 6px 抖动(≤8px)仍算点击能进目录。 - 断言从「像素变了」升级为「像素**恰好**位移 N px」(内容区快照逐行比对,避开视口 边缘裁剪带):swipe 120 → 恰好 120;拖动 60 → 恰好 60;并新增 6 步×10px 多步拖动 断言终帧恰好 60(专测节流不丢最后一段)。 - 修掉把 20px 写成「< SHELF_DRAG_TAP_PX」的错误注释。 顺手: - shelf_draw_notice 用 th->bg 而不是只在 shelf_redraw 赋值的 g_bg。 - build_reader.sh 用 trap 保证测试失败时也清 $TMPDIR/m41_scroll_sd。 验证:shelf_scroll_smoke 38/38、shelf_group_smoke 99/99、shelf_index_smoke 170/170。 红绿对照:dist=dy(旧 swipe 语义)→ 9 条 FAIL;拿掉「抬手补终帧」→ 恰好 1 条 FAIL (6 步拖动终帧那条),证明节流与终帧都有测试盯着。 reader 1719220B md5 890175ec…;mpk 818162B md5 ffbb7097…(0.1.8/code9,未签名)。
304 lines
14 KiB
C
304 lines
14 KiB
C
/* 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 <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <sys/stat.h>
|
||
#include <sys/types.h>
|
||
#include <unistd.h>
|
||
|
||
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;
|
||
}
|