fix(reader): M41 书架补滚动输入通路 — 列表/宫格、format/folder 全都能滑

书架(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 完成后部署(设备验证需用户手滑,面板无任意滑动注入端点)。
This commit is contained in:
AskaEth committed 2026-11-11 13:01:12 +08:00
1 parent a364932145
commit 2de47154bf
7 files changed
+386 -16

No files matched your search

+149 -14
View File
@@ -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);
}