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,未签名)。
This commit is contained in:
AskaEth committed 2026-11-11 13:15:54 +08:00
1 parent 2de47154bf
commit bd6fc8da3c
3 files changed
+181 -37

No files matched your search

+52 -8
View File
@@ -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」的旧/宿主编排 */
+4
View File
@@ -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
+125 -29
View File
@@ -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();