Files
MeowOS-For-R3P2/patch_v2/reader_app/test/shelf_scroll_smoke.c
T
AskaEth bd6fc8da3c fix(reader): M41 r1 评审 P2 — 拖动重绘节流 + 终帧补齐 + 三条边界测试
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,未签名)。
2026-11-11 13:15:54 +08:00

304 lines
14 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.
/* 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;
}