From 2de47154bf41dc528c175ee2321b66a44be1ecbb Mon Sep 17 00:00:00 2001 From: AskaEth Date: Wed, 11 Nov 2026 13:01:12 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(reader):=20M41=20=E4=B9=A6=E6=9E=B6?= =?UTF-8?q?=E8=A1=A5=E6=BB=9A=E5=8A=A8=E8=BE=93=E5=85=A5=E9=80=9A=E8=B7=AF?= =?UTF-8?q?=20=E2=80=94=20=E5=88=97=E8=A1=A8/=E5=AE=AB=E6=A0=BC=E3=80=81fo?= =?UTF-8?q?rmat/folder=20=E5=85=A8=E9=83=BD=E8=83=BD=E6=BB=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 书架(shelf.c)从 M6.40 首版起就**没有**滚动输入通路,不是 M32/M33 的回归: 1) app_on_touch_down/move 只路由给阅读页 → 书架没有跟手拖动; 2) shelf_on_swipe 用 dy 去 scroll_by,而 SDK 固定按 on_swipe(dir, dist, 0) 分发(总位移在第 2 参 dx,dy 恒 0)→ 恒 scroll_by(0),死代码。 两条都在 shelf.c 自己身上,list/grid × format/folder 共用同一段,所以四种组合 全都「滑不动」(files/ncm 能滑是因为它们各自实现了 touch_down/move → scroll_by)。 修复: - app.c:touch down/move 也路由给书架(阅读页优先级不变)。 - shelf.c: * 新增 shelf_on_touch_down/move 跟手拖动:按下点在内容区才起手;纵向主导(≥4px) 才锁滚动,横向主导直接取消(不抢左缘返回);逐帧 scroll_by(-dy) 只重画内容区。 * shelf_on_swipe 改按 dist(=dx,兼容 dy 分支)+ 方向定符号;本次手势已跟手滚过 就不再叠加(防一划滚两倍)。 * 拖动后的抬手吞掉 TAP(<65px 的拖动 SDK 报 TAP)→ 滑动时不会误开书/误进目录。 * 控件重建(进/退目录、布局/分组切换、刷新)清滚动手势态,避免旧标记吃掉下一次滑动。 * 提示条抽成 shelf_draw_notice(),滚动局部重画时照常重贴。 - 新增宿主回归 test/shelf_scroll_smoke.c(真实 shelf.c + 真实 SDK 绘制 + back_buf 像素断言,27 条):format/folder × list/grid 都能滑;3 本书的目录滑不动; 拖动后不误进目录、干净点击仍能进目录;folder 进出目录仍正常。把 dist=dx 改回 dist=dy 后同一测试 6 条 FAIL(红绿对照证明它抓的就是这个 bug)。 - build_reader.sh 接入该测试;READER_SD_ROOT 加 #ifndef 以便宿主把扫描根指到临时目录。 - app.json 版本 0.1.7/code8 → 0.1.8/code9(同一版本号不发两个构建)。 宿主验证:shelf_scroll_smoke 27/27、shelf_group_smoke 99/99、shelf_index_smoke 170/170、其余 core/seg/chapter/loaders 全绿;reader 1718468B md5 259cb6ed5a60… (meow_ui.a 用本仓 SDK 源码重编:79 成员全 MIPS32r2 o32)。真机验证待刷机 0.8.49 完成后部署(设备验证需用户手滑,面板无任意滑动注入端点)。 --- patch_v2/reader_app/.gitignore | 1 + patch_v2/reader_app/app.c | 2 + patch_v2/reader_app/app.json | 2 +- patch_v2/reader_app/app/shelf.c | 163 ++++++++++++-- patch_v2/reader_app/build_reader.sh | 21 ++ patch_v2/reader_app/reader_internal.h | 6 +- patch_v2/reader_app/test/shelf_scroll_smoke.c | 207 ++++++++++++++++++ 7 files changed, 386 insertions(+), 16 deletions(-) create mode 100644 patch_v2/reader_app/test/shelf_scroll_smoke.c diff --git a/patch_v2/reader_app/.gitignore b/patch_v2/reader_app/.gitignore index d708bce..03d1589 100644 --- a/patch_v2/reader_app/.gitignore +++ b/patch_v2/reader_app/.gitignore @@ -13,6 +13,7 @@ /test/shelf_scan_bench_host /test/shelf_index_smoke_host /test/shelf_index_bench_host +/test/shelf_scroll_smoke_host /test/shelf_scan_bench_mipsel /test/rdft_smoke_mipsel /test/book_smoke_mipsel diff --git a/patch_v2/reader_app/app.c b/patch_v2/reader_app/app.c index a88d237..f38f14e 100644 --- a/patch_v2/reader_app/app.c +++ b/patch_v2/reader_app/app.c @@ -53,10 +53,12 @@ static void app_on_tick(void) static void app_on_touch_down(int x, int y) { if (reader_is_active()) { reader_on_touch_down(x, y); return; } + if (shelf_is_active()) { shelf_on_touch_down(x, y); return; } /* M41:书架跟手拖动滚动 */ } static void app_on_touch_move(int x, int y) { if (reader_is_active()) { reader_on_touch_move(x, y); return; } + if (shelf_is_active()) { shelf_on_touch_move(x, y); return; } } int main(void) diff --git a/patch_v2/reader_app/app.json b/patch_v2/reader_app/app.json index e143c22..9351b70 100644 --- a/patch_v2/reader_app/app.json +++ b/patch_v2/reader_app/app.json @@ -1 +1 @@ -{"name":"Reader","exec":"/usr/data/mnt/sd_0/MeowAPPs/os.meow.reader/reader","package":"os.meow.reader","version":"0.1.7","version_code":8,"framework":1,"fonts":"common","author":"MeowOS","icon":"icon.png","volume_page":true} +{"name":"Reader","exec":"/usr/data/mnt/sd_0/MeowAPPs/os.meow.reader/reader","package":"os.meow.reader","version":"0.1.8","version_code":9,"framework":1,"fonts":"common","author":"MeowOS","icon":"icon.png","volume_page":true} diff --git a/patch_v2/reader_app/app/shelf.c b/patch_v2/reader_app/app/shelf.c index a9889f0..d4d1612 100644 --- a/patch_v2/reader_app/app/shelf.c +++ b/patch_v2/reader_app/app/shelf.c @@ -33,6 +33,16 @@ * 需点「刷新」(与 M29「新书靠刷新」同一约定)。任何异常 → 照常全量重扫(fail-open)。 * 扫描完成后原子写回索引(写失败只丢缓存,不影响书架)。 * M29 语义不变:首次进书架=索引或全扫;从阅读页返回=不扫;顶栏「刷新」=清索引+全扫。 + * + * M41 滚动(用户 2026-11-11:「列表和宫格都滑不动」,178 本目录同样): + * 书架此前**从来没有**滚动输入通路——不是回归,是 M6.40 首版就缺: + * ① `app_on_touch_down/move` 只路由给阅读页 → 没有跟手拖动(files/ncm 能滑正是因为 + * 它们自己实现了 touch_down/move → meow_listview_scroll_by); + * ② `shelf_on_swipe` 用 dy 去 scroll_by,而 SDK 固定按 `on_swipe(dir, dist, 0)` 分发 + * (位移在第 2 参,dy 恒 0)→ 恒 scroll_by(0)。 + * 本版补齐:跟手拖动(含横向手势锁定,别抢左缘返回)+ swipe 按 dist 滚(兼容 dy), + * 两者共用 shelf_scroll_by;拖动后的抬手吞掉 TAP(位移 <65px 的拖动 SDK 会报 TAP, + * 不吞就会在滑动时误开书/误进目录)。滚动只重画内容区,顶栏/副题不动。 */ #include "../reader_internal.h" #include "font_resolve.h" @@ -48,8 +58,13 @@ #define SHELF_APPBAR_H 56 #define SHELF_LIST_X 0 #define SHELF_LIST_W 480 +#define SHELF_SUB_H 22 /* 顶栏下「副题行」高度:内容视口从 top+SHELF_SUB_H 开始 */ /* 圆角卡容器阈值:行数 ≤ 该值用 MD3 圆角卡(观感优先),超过则扁平行(滚动流畅优先)。 */ #define SHELF_ROUND_MAX_ROWS 200 +/* M41 触摸拖动:纵向累计位移达到 START 才锁定为滚动;达到 TAP 后吞掉随后的 TAP + * (SDK 只在位移 ≥65px 时报 SWIPE,小于它的拖动会落成 TAP,不吞就会误进目录/误开书)。 */ +#define SHELF_DRAG_START_PX 4 +#define SHELF_DRAG_TAP_PX 8 static shelf_book g_books[SHELF_MAX_BOOKS]; static int g_nbooks; @@ -64,6 +79,13 @@ static meow_lazygrid *g_grid; /* lazygrid(布局=grid) */ static meow_widget *g_appbar; /* meow_topappbar */ static int g_layout; /* 0=list 1=grid(读 rdconf->shelf) */ static int g_group_folder;/* 0=format 1=folder(读 rdconf->shelf_group) */ +/* M41 滚动:SDK 只在 on_swipe 里给总位移(且放在第 2 参,dy 恒 0),跟手拖动走 + * on_touch_down/on_touch_move。g_drag_state:0=无手势 1=按下待判向 2=纵向滚动中 + * 3=横向手势(左缘返回等,不滚)。 */ +static int g_drag_state; +static int g_drag_x0, g_drag_y0; /* 按下原点(判向:纵向 vs 横向) */ +static int g_drag_last_y; /* 上一帧 Y(逐帧增量) */ +static int g_drag_scrolled; /* 本次手势已实际滚过(抬手不再叠加/不当作点击) */ static uint32_t g_bg, g_fg, g_dim; static char g_notice[160]; /* 提示第 1 行(opendir 失败/超上限截断等) */ static char g_notice2[160]; /* 提示第 2 行(超量+超深并存时才用;空=单行) */ @@ -77,6 +99,7 @@ static int g_nrows; /* 控件装配/顶栏定义在本文件后部;目录导航的进/退要重建它们。 */ static void shelf_appbar_build(void); static void shelf_build_content(void); +static void shelf_drag_reset(void); /* 控件重建时清滚动手势态(M41) */ static const char *fmt_name(int f) { return shelf_fmt_name(f); } @@ -382,10 +405,13 @@ static void shelf_build_content(void) const meow_theme *th = meow_theme_get(); int sbh = meow_ui_statusbar_height(); int top = sbh + SHELF_APPBAR_H; - int list_y = top + 22; /* 副题区下 */ + int list_y = top + SHELF_SUB_H; /* 副题区下(与 shelf_content_top 同口径) */ int list_h = 720 - list_y; if (list_h < 0) list_h = 0; + /* 控件重建(进/退目录、布局/分组切换、刷新):在途滚动手势的旧 g_list/g_grid 已失效, + * 清掉拖动状态,避免上一次拖动遗留的「已滚过」标记吃掉下一次抬手滚动。 */ + shelf_drag_reset(); if (g_list) { meow_widget_free(g_list); g_list = NULL; } if (g_grid) { meow_lazygrid_free(g_grid); g_grid = NULL; } @@ -582,6 +608,20 @@ void shelf_set_group(int folder) } } +/* 提示条(扫描/索引报告,6s 后由 shelf_on_tick 清掉):滚动只重画内容区时也要重贴, + * 否则滑一下就把提示抹掉了。 */ +static void shelf_draw_notice(void) +{ + if (g_notice_ms <= 0) return; + const meow_theme *th = meow_theme_get(); + int two = g_notice2[0] != 0; + int top = two ? 610 : 620; + int h = two ? 58 : 44; + meow_ui_fill_rect(0, top, 480, h, g_bg); + meow_ui_text(20, top + 4, g_notice, 20, th->primary); + if (two) meow_ui_text(20, top + 30, g_notice2, 20, th->primary); +} + void shelf_redraw(void) { const meow_theme *th = meow_theme_get(); @@ -626,19 +666,108 @@ void shelf_redraw(void) /* 「去放书」文本按钮(提示) */ meow_ui_text_center(480 / 2, cy + 150, "Books/ 目录", 20, th->primary); } - if (g_notice_ms > 0) { - int two = g_notice2[0] != 0; - int top = two ? 610 : 620; - int h = two ? 58 : 44; - meow_ui_fill_rect(0, top, 480, h, g_bg); - meow_ui_text(20, top + 4, g_notice, 20, th->primary); - if (two) meow_ui_text(20, top + 30, g_notice2, 20, th->primary); - } + if (g_notice_ms > 0) shelf_draw_notice(); meow_ui_flush(); } +/* ---- M41 滚动:唯一入口(swipe / 跟手拖动都走这里)---- + * 历史 bug:SHELF 从来没有滚动输入通路——①`app_on_touch_down/move` 只路由给阅读页, + * 所以没有跟手拖动;②`shelf_on_swipe` 用 dy 去 scroll_by,而 SDK 固定按 + * `on_swipe(dir, dist, 0)` 分发(位移在第 2 参 dx,dy 恒 0)→ scroll_by(0) 恒为死代码。 + * list/grid、format/folder 都走同一段,所以两种分组、两种布局全都「滑不动」。 */ +static int shelf_content_top(void) +{ + return meow_ui_statusbar_height() + SHELF_APPBAR_H + SHELF_SUB_H; /* = shelf_build_content 的 list_y */ +} + +static int shelf_content_bottom(void) +{ + int h = meow_ui_height(); + return h > 0 ? h : 720; +} + +/* 当前布局的滚动上限(0=内容不足一屏,无需滚动;也是「根层 3 行不该动」的判据)。 */ +static int shelf_scroll_max(void) +{ + if (g_layout == 1) return g_grid ? meow_lazygrid_max_scroll(g_grid) : 0; + return g_list ? meow_listview_max_scroll(g_list) : 0; +} + +static int shelf_scroll_get(void) +{ + if (g_layout == 1) return g_grid ? meow_lazygrid_scroll(g_grid) : 0; + return g_list ? meow_listview_scroll(g_list) : 0; +} + +/* 只重画内容区:顶栏/副题/提示不动,避免整帧重绘(滚动每帧都走这里)。 */ +static void shelf_redraw_content(void) +{ + const meow_theme *th = meow_theme_get(); + int top = shelf_content_top(); + int h = shelf_content_bottom() - top; + if (h <= 0) return; + meow_ui_fill_rect(0, top, SHELF_LIST_W, h, th->bg); + if (g_layout == 1 && g_grid) meow_lazygrid_draw(g_grid, th); + else if (g_list) meow_widget_draw(g_list); + shelf_draw_notice(); + meow_ui_flush_region(0, top, SHELF_LIST_W, h); +} + +/* 滚动 del 像素(正=内容上移/显示更下面的行)。返回 1=视口确实变了(已重画)。 */ +static int shelf_scroll_by(int del) +{ + if (!del) return 0; + int before = shelf_scroll_get(); + if (g_layout == 1) { if (g_grid) meow_lazygrid_scroll_by(g_grid, del); } + else { if (g_list) meow_listview_scroll_by(g_list, del); } + if (shelf_scroll_get() == before) return 0; + shelf_redraw_content(); + return 1; +} + +static void shelf_drag_reset(void) +{ + g_drag_state = 0; + g_drag_scrolled = 0; +} + +void shelf_on_touch_down(int x, int y) +{ + shelf_drag_reset(); + if (!g_active || g_nbooks <= 0) return; + if (y < shelf_content_top() || y >= shelf_content_bottom()) return; /* 顶栏/副题不拖 */ + if (shelf_scroll_max() <= 0) return; /* 不足一屏:没有可滚的 */ + g_drag_state = 1; + g_drag_x0 = x; g_drag_y0 = y; g_drag_last_y = y; +} + +void shelf_on_touch_move(int x, int y) +{ + if (g_drag_state != 1 && g_drag_state != 2) return; /* 无手势 / 横向手势已取消 */ + int adx = x - g_drag_x0, ady = y - g_drag_y0; + if (adx < 0) adx = -adx; + if (ady < 0) ady = -ady; + if (g_drag_state == 1) { + if (ady >= SHELF_DRAG_START_PX && ady >= adx) g_drag_state = 2; /* 纵向主导 → 滚动 */ + else if (adx >= SHELF_DRAG_START_PX && adx > ady) { g_drag_state = 3; return; } /* 横向(左缘返回等):不滚 */ + else return; + } + int step = y - g_drag_last_y; + g_drag_last_y = y; + if (!step) return; + if (shelf_scroll_by(-step)) g_drag_scrolled = 1; /* 手指上移(y 减)→ 显示更下面的行 */ +} + int shelf_on_tap(int x, int y) { + /* 拖动后的抬手不是点击:累计纵向位移 ≥ SHELF_DRAG_TAP_PX 就吞掉这次 TAP, + * 否则「滑动时手指落点在一行书上」会把书打开(位移 <65px 的拖动 SDK 报 TAP)。 */ + int dragged = (g_drag_state == 2 && + (g_drag_scrolled || + (g_drag_last_y - g_drag_y0 > SHELF_DRAG_TAP_PX) || + (g_drag_y0 - g_drag_last_y > SHELF_DRAG_TAP_PX))); + shelf_drag_reset(); + if (dragged) return 0; /* 顶栏动作先命中 */ if (g_appbar) { int bi = meow_topappbar_hit(g_appbar, x, y); @@ -693,11 +822,17 @@ int shelf_on_tap(int x, int y) return 0; } -/* 书架的滚动(list/grid;app.c on_swipe 路由到本页时调用)。 */ +/* 书架的滚动(list/grid;app.c on_swipe 路由到本页时调用)。 + * SDK 契约:`on_swipe(dir, dist, 0)` —— 总位移在**第 2 参**,dy 恒 0(见 meow_ui_input.c + * MEOW_EV_SWIPE 分发)。旧实现按 dy 滚,等于永远滚 0,这就是「怎么划都不动」的直接原因。 + * 跟手拖动已经在 on_touch_move 里滚过本次手势时,抬手不再叠加一次(否则一划滚两倍)。 */ void shelf_on_swipe(int dir, int dx, int dy) { - (void)dir; (void)dx; - if (g_nbooks <= 0) return; - if (g_layout == 1 && g_grid) { meow_lazygrid_scroll_by(g_grid, dy); meow_lazygrid_draw(g_grid, meow_theme_get()); meow_ui_flush(); } - else if (g_list) { meow_listview_scroll_by(g_list, dy); meow_widget_draw(g_list); meow_ui_flush(); } + int already = (g_drag_state == 2 && g_drag_scrolled); + shelf_drag_reset(); + if (g_nbooks <= 0 || already) return; + if (dir != MEOW_SWIPE_UP && dir != MEOW_SWIPE_DOWN) return; + int dist = dx > 0 ? dx : dy; /* SDK: dist 在 dx;dy 分支只为兼容「位移放 dy」的旧/宿主编排 */ + if (dist <= 0) return; + shelf_scroll_by(dir == MEOW_SWIPE_UP ? dist : -dist); } diff --git a/patch_v2/reader_app/build_reader.sh b/patch_v2/reader_app/build_reader.sh index 9f10a82..c7e77bb 100755 --- a/patch_v2/reader_app/build_reader.sh +++ b/patch_v2/reader_app/build_reader.sh @@ -126,6 +126,27 @@ $HOST_CC -O2 -Wall -Wextra -Iapp -I../sdk test/shelf_index_smoke.c app/shelf_mod ./test/shelf_index_smoke_host $HOST_CC -O2 -Wall -Wextra -Iapp -I../sdk test/shelf_index_bench.c app/shelf_model.c app/shelf_index.c -o test/shelf_index_bench_host +# M41:书架滚动宿主回归(真实 shelf.c + 真实 SDK 绘制,离屏 480x720)。临时 Books 树写在 +# $TMPDIR 下(不碰设备路径),READER_SD_ROOT 编译期指过去。 +echo "==> host shelf scroll test (M41)" +SCROLL_ROOT="${TMPDIR:-/tmp}/m41_scroll_sd" +rm -rf "$SCROLL_ROOT" +SCROLL_SDK="test_host_stub meow_ui_gfx meow_ui_time meow_ui_sys meow_ui_theme meow_ui_vol meow_ui_log \ + meow_ui_widget meow_ui_input meow_ui_activity meow_ui_dialog meow_ui_ime meow_pinyin meow_ui_str \ + meow_ui_draw meow_ui_view meow_ui_img meow_notify_ipc stb_image meow_jpeg meow_png_stream \ + meow_ui_config meow_ui_nav meow_ui_anim meow_ui_box meow_ui_scroll meow_lazygrid meow_select \ + meow_ui_media meow_ui_audio meow_time32" +SCROLL_SRC="" +for f in $SCROLL_SDK; do SCROLL_SRC="$SCROLL_SRC ../sdk/$f.c"; done +# shellcheck disable=SC2086 +$HOST_CC -O2 -Wall -Wextra -DMEOW_UI_HOST_TEST -DREADER_SD_ROOT="\"$SCROLL_ROOT\"" \ + -I. -Iapp -I../sdk -I../meow_launcherd \ + test/shelf_scroll_smoke.c app/shelf.c app/shelf_model.c app/shelf_index.c app/progress.c \ + app/reader_conf.c app/font_resolve.c $SCROLL_SRC \ + -o test/shelf_scroll_smoke_host -pthread -lm -lz +./test/shelf_scroll_smoke_host +rm -rf "$SCROLL_ROOT" + echo "==> host font resolve test (M31)" $HOST_CC -O2 -Wall -Wextra -Iapp test/font_resolve_smoke.c app/font_resolve.c -o test/font_resolve_smoke_host ./test/font_resolve_smoke_host diff --git a/patch_v2/reader_app/reader_internal.h b/patch_v2/reader_app/reader_internal.h index 187d6a0..c3e647a 100644 --- a/patch_v2/reader_app/reader_internal.h +++ b/patch_v2/reader_app/reader_internal.h @@ -18,6 +18,8 @@ meow_view *shelf_view_create(void); int shelf_is_active(void); int shelf_on_tap(int x, int y); void shelf_on_swipe(int dir, int dx, int dy); +void shelf_on_touch_down(int x, int y); /* M41 跟手拖动滚动(列表/宫格同一通路) */ +void shelf_on_touch_move(int x, int y); void shelf_redraw(void); int shelf_back(void); /* M33:folder 模式返回上一级;1=已回退 0=已在根 */ void shelf_set_view(int grid); /* 1=grid 0=list;读 reader.conf ui.shelf_layout(A) */ @@ -53,7 +55,9 @@ void settings_reload(void); /* 改配置后让设置页/其它页重 void reader_page_key(int dir); /* dir=+1 下一页;-1 上一页 */ /* 返回可执行文件所在包根(用于定位 /usr/data/mnt/sd_0)——由 app.c 注入与否; - * R1 用固定 TF 根 /usr/data/mnt/sd_0。 */ + * R1 用固定 TF 根 /usr/data/mnt/sd_0。宿主单测用 -DREADER_SD_ROOT="..." 指到临时目录。 */ +#ifndef READER_SD_ROOT #define READER_SD_ROOT "/usr/data/mnt/sd_0" +#endif #endif /* READER_INTERNAL_H */ diff --git a/patch_v2/reader_app/test/shelf_scroll_smoke.c b/patch_v2/reader_app/test/shelf_scroll_smoke.c new file mode 100644 index 0000000..14e14cb --- /dev/null +++ b/patch_v2/reader_app/test/shelf_scroll_smoke.c @@ -0,0 +1,207 @@ +/* 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; +} From bd6fc8da3ca0cdb57bfc5cb9dfc493fc7897085e Mon Sep 17 00:00:00 2001 From: AskaEth Date: Wed, 11 Nov 2026 13:15:54 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(reader):=20M41=20r1=20=E8=AF=84?= =?UTF-8?q?=E5=AE=A1=20P2=20=E2=80=94=20=E6=8B=96=E5=8A=A8=E9=87=8D?= =?UTF-8?q?=E7=BB=98=E8=8A=82=E6=B5=81=20+=20=E7=BB=88=E5=B8=A7=E8=A1=A5?= =?UTF-8?q?=E9=BD=90=20+=20=E4=B8=89=E6=9D=A1=E8=BE=B9=E7=95=8C=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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,未签名)。 --- patch_v2/reader_app/app/shelf.c | 60 ++++++- patch_v2/reader_app/build_reader.sh | 4 + patch_v2/reader_app/test/shelf_scroll_smoke.c | 154 ++++++++++++++---- 3 files changed, 181 insertions(+), 37 deletions(-) diff --git a/patch_v2/reader_app/app/shelf.c b/patch_v2/reader_app/app/shelf.c index d4d1612..3b6dc5d 100644 --- a/patch_v2/reader_app/app/shelf.c +++ b/patch_v2/reader_app/app/shelf.c @@ -65,6 +65,10 @@ * (SDK 只在位移 ≥65px 时报 SWIPE,小于它的拖动会落成 TAP,不吞就会误进目录/误开书)。 */ #define SHELF_DRAG_START_PX 4 #define SHELF_DRAG_TAP_PX 8 +/* 拖动重绘节流:滚动位置每个 move 都立即生效(便宜),上屏最多每 FRAME_MS 一次; + * 输入线程「坐标一变就一条 TOUCH_MOVE」(无合并、队列 128 条),圆角卡一帧 40ms+, + * 不节流会让主循环永远追不上手指、抬手后还继续滑。窗口再取 max(16ms, 上一帧实测耗时)。 */ +#define SHELF_DRAG_FRAME_MS 16 static shelf_book g_books[SHELF_MAX_BOOKS]; static int g_nbooks; @@ -86,6 +90,9 @@ static int g_drag_state; static int g_drag_x0, g_drag_y0; /* 按下原点(判向:纵向 vs 横向) */ static int g_drag_last_y; /* 上一帧 Y(逐帧增量) */ static int g_drag_scrolled; /* 本次手势已实际滚过(抬手不再叠加/不当作点击) */ +static int g_drag_dirty; /* 有已生效但未上屏的滚动(抬手补终帧) */ +static uint64_t g_drag_draw_ms; /* 上次拖动重绘时刻(0=本手势还没画过 → 首帧立即画) */ +static int g_drag_frame_ms; /* 上次拖动重绘耗时(节流窗口自适应) */ static uint32_t g_bg, g_fg, g_dim; static char g_notice[160]; /* 提示第 1 行(opendir 失败/超上限截断等) */ static char g_notice2[160]; /* 提示第 2 行(超量+超深并存时才用;空=单行) */ @@ -617,7 +624,7 @@ static void shelf_draw_notice(void) int two = g_notice2[0] != 0; int top = two ? 610 : 620; int h = two ? 58 : 44; - meow_ui_fill_rect(0, top, 480, h, g_bg); + meow_ui_fill_rect(0, top, 480, h, th->bg); meow_ui_text(20, top + 4, g_notice, 20, th->primary); if (two) meow_ui_text(20, top + 30, g_notice2, 20, th->primary); } @@ -713,27 +720,56 @@ static void shelf_redraw_content(void) meow_ui_flush_region(0, top, SHELF_LIST_W, h); } -/* 滚动 del 像素(正=内容上移/显示更下面的行)。返回 1=视口确实变了(已重画)。 */ -static int shelf_scroll_by(int del) +/* 只改滚动位置(不动屏幕):返回 1=位置变了。 */ +static int shelf_scroll_apply(int del) { if (!del) return 0; int before = shelf_scroll_get(); if (g_layout == 1) { if (g_grid) meow_lazygrid_scroll_by(g_grid, del); } else { if (g_list) meow_listview_scroll_by(g_list, del); } - if (shelf_scroll_get() == before) return 0; + return shelf_scroll_get() != before; +} + +/* 改位置 + 立即上屏:swipe 的一次性滚动走这里。 */ +static int shelf_scroll_by(int del) +{ + if (!shelf_scroll_apply(del)) return 0; shelf_redraw_content(); return 1; } +/* 拖动中的重绘:把已生效的滚动上屏,并记录帧耗时给节流窗口用。 */ +static void shelf_drag_flush(void) +{ + if (!g_drag_dirty) return; + uint64_t t0 = meow_ui_time_ms(); + shelf_redraw_content(); + uint64_t t1 = meow_ui_time_ms(); + g_drag_dirty = 0; + g_drag_draw_ms = t0 ? t0 : 1; /* 记**起始**时刻:下一帧的周期 ≈ max(16ms, 本帧耗时)。0 是「没画过」哨兵 */ + g_drag_frame_ms = (t1 > t0) ? (int)(t1 - t0) : 0; /* 10ms 分辨率:小帧记 0 → 窗口仍 ≥16ms */ +} + static void shelf_drag_reset(void) { g_drag_state = 0; g_drag_scrolled = 0; + g_drag_dirty = 0; + g_drag_draw_ms = 0; + g_drag_frame_ms = 0; +} + +/* 手势收尾:节流窗内没来得及上屏的最后一段**必须补一帧**,否则抬手后画面停在旧位置 + * (滚动位置已经生效,视觉上就是「拖了但没跟上」)。 */ +static void shelf_drag_finish(void) +{ + if (g_drag_state == 2 && g_drag_dirty) shelf_drag_flush(); + shelf_drag_reset(); } void shelf_on_touch_down(int x, int y) { - shelf_drag_reset(); + shelf_drag_finish(); /* 上一次手势若没走 TAP/SWIPE 收尾(HOME/长按等),先把遗留帧补上 */ if (!g_active || g_nbooks <= 0) return; if (y < shelf_content_top() || y >= shelf_content_bottom()) return; /* 顶栏/副题不拖 */ if (shelf_scroll_max() <= 0) return; /* 不足一屏:没有可滚的 */ @@ -755,7 +791,15 @@ void shelf_on_touch_move(int x, int y) int step = y - g_drag_last_y; g_drag_last_y = y; if (!step) return; - if (shelf_scroll_by(-step)) g_drag_scrolled = 1; /* 手指上移(y 减)→ 显示更下面的行 */ + /* 位置每次都立即生效(便宜);上屏节流:首帧立即画,之后每 max(16ms, 上一帧耗时) 一次。 + * 输入线程每条 MOVE 都入队且不合并,逐条全量重绘会让队列永远追不上手指。 */ + if (!shelf_scroll_apply(-step)) return; /* 已到顶/底:位置没变,连帧都不用画 */ + g_drag_scrolled = 1; /* 手指上移(y 减)→ 显示更下面的行 */ + g_drag_dirty = 1; + uint64_t now = meow_ui_time_ms(); + int win = g_drag_frame_ms > SHELF_DRAG_FRAME_MS ? g_drag_frame_ms : SHELF_DRAG_FRAME_MS; + if (g_drag_draw_ms == 0 || (now >= g_drag_draw_ms && now - g_drag_draw_ms >= (uint64_t)win)) + shelf_drag_flush(); } int shelf_on_tap(int x, int y) @@ -766,7 +810,7 @@ int shelf_on_tap(int x, int y) (g_drag_scrolled || (g_drag_last_y - g_drag_y0 > SHELF_DRAG_TAP_PX) || (g_drag_y0 - g_drag_last_y > SHELF_DRAG_TAP_PX))); - shelf_drag_reset(); + shelf_drag_finish(); /* 先补终帧再清手势态 */ if (dragged) return 0; /* 顶栏动作先命中 */ if (g_appbar) { @@ -829,7 +873,7 @@ int shelf_on_tap(int x, int y) void shelf_on_swipe(int dir, int dx, int dy) { int already = (g_drag_state == 2 && g_drag_scrolled); - shelf_drag_reset(); + shelf_drag_finish(); /* 先补终帧(节流窗内最后一段)再清手势态 */ if (g_nbooks <= 0 || already) return; if (dir != MEOW_SWIPE_UP && dir != MEOW_SWIPE_DOWN) return; int dist = dx > 0 ? dx : dy; /* SDK: dist 在 dx;dy 分支只为兼容「位移放 dy」的旧/宿主编排 */ diff --git a/patch_v2/reader_app/build_reader.sh b/patch_v2/reader_app/build_reader.sh index c7e77bb..09be1a5 100755 --- a/patch_v2/reader_app/build_reader.sh +++ b/patch_v2/reader_app/build_reader.sh @@ -9,6 +9,8 @@ # test/shelf_group_smoke_host 宿主书架分组单测(M27/M31 副题) # test/shelf_index_smoke_host 宿主书架索引单测(M32:命中/失效/fail-open/ts 不缓存) # test/shelf_index_bench_host 宿主索引耗时基准(M32,只编不跑;bench/indexonly) +# test/shelf_scroll_smoke_host 宿主书架滚动回归(M41:format/folder × list/grid 滑/拖, +# back_buf 像素断言;临时 Books 树写 $TMPDIR) # test/font_resolve_smoke_host 宿主字体路径记忆化单测(M31) # test/font_resolve_bench_host 宿主字体 walk/命中耗时对比(M31,只编不跑) # test/loaders_smoke_host 宿主四格式 loader 单测 @@ -131,6 +133,7 @@ $HOST_CC -O2 -Wall -Wextra -Iapp -I../sdk test/shelf_index_bench.c app/shelf_mod echo "==> host shelf scroll test (M41)" SCROLL_ROOT="${TMPDIR:-/tmp}/m41_scroll_sd" rm -rf "$SCROLL_ROOT" +trap 'rm -rf "$SCROLL_ROOT"' EXIT # 编译或测试失败(set -e)也要清临时 Books 树 SCROLL_SDK="test_host_stub meow_ui_gfx meow_ui_time meow_ui_sys meow_ui_theme meow_ui_vol meow_ui_log \ meow_ui_widget meow_ui_input meow_ui_activity meow_ui_dialog meow_ui_ime meow_pinyin meow_ui_str \ meow_ui_draw meow_ui_view meow_ui_img meow_notify_ipc stb_image meow_jpeg meow_png_stream \ @@ -146,6 +149,7 @@ $HOST_CC -O2 -Wall -Wextra -DMEOW_UI_HOST_TEST -DREADER_SD_ROOT="\"$SCROLL_ROOT\ -o test/shelf_scroll_smoke_host -pthread -lm -lz ./test/shelf_scroll_smoke_host rm -rf "$SCROLL_ROOT" +trap - EXIT echo "==> host font resolve test (M31)" $HOST_CC -O2 -Wall -Wextra -Iapp test/font_resolve_smoke.c app/font_resolve.c -o test/font_resolve_smoke_host diff --git a/patch_v2/reader_app/test/shelf_scroll_smoke.c b/patch_v2/reader_app/test/shelf_scroll_smoke.c index 14e14cb..3dfb6e5 100644 --- a/patch_v2/reader_app/test/shelf_scroll_smoke.c +++ b/patch_v2/reader_app/test/shelf_scroll_smoke.c @@ -79,6 +79,73 @@ static unsigned long content_hash(void) 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; @@ -99,6 +166,7 @@ int main(int argc, char **argv) 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"); @@ -107,9 +175,11 @@ int main(int argc, char **argv) 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(); - 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 继续前进"); @@ -123,48 +193,75 @@ int main(int argc, char **argv) /* ===== 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); /* 拖回原位 */ + 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, "反向拖回像素复原"); - shelf_on_touch_move(240, top + 300); /* 继续下滑(越过顶部 → clamp) */ + gesture(240, top + 160, top + 200); /* 继续下滑(越过顶部 → 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)"); + /* 节流回归: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(< 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"); + /* 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 生效"); - shelf_on_touch_down(240, top + 200); - shelf_on_touch_move(240, top + 140); + 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 本,不足一屏)→ 滑动不动 */ - shelf_on_touch_down(240, top + 10 + 56); /* 行 1 = small(行 0=big) */ - (void)shelf_on_tap(240, top + 10 + 56); + 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 本书的目录:滑动不动(不足一屏)"); - shelf_on_touch_down(240, top + 100); - shelf_on_touch_move(240, top + 20); + gesture(240, top + 100, top + 20); CHECK(content_hash() == s0, "3 本书的目录:拖动不动"); CHECK(shelf_back() == 1, "从 small 返回 Books 根"); @@ -176,8 +273,7 @@ int main(int argc, char **argv) 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); + gesture(240, top + 100, top + 20); CHECK(content_hash() != g0, "grid 拖动生效"); /* ===== 5) format + grid ===== */ @@ -188,8 +284,7 @@ int main(int argc, char **argv) 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); + gesture(240, top + 200, top + 140); CHECK(content_hash() != fg0, "format/grid 拖动生效"); /* ===== 6) format + list 回归:format 行序列仍在(40 本 → 可滚) ===== */ @@ -199,6 +294,7 @@ int main(int argc, char **argv) shelf_on_swipe(MEOW_SWIPE_UP, 120, 0); CHECK(content_hash() != l0, "回到 format/list 仍可滑"); + free(g_snap); meow_view_free(shelf); meow_ui_shutdown();