/* test_settings_scroll_loc.c — M7.2 设置页列表滚动「局部刷」契约(宿主验证)。 * * 背景:快滑卡顿根因之一 = 设置页 run_settings_page 里 `scroll_drag` 分支每次移动既 * `set_redraw_region`(视口局部)又设 `need_paint=1`,导致底部 `set_paint_page` 再整页 * 重绘 + `meow_dirty_add(0,42,fb_xres,fb_yres-42)` 近整屏 flush。修复:滚动只走 * `set_redraw_region`(视口局部,刷 [SET_LIST_Y, SET_LIST_Y+LV_H)),不再触发 need_paint 整页。 * * 本测试用设置页同源几何建立 listview(set_vsource + set_item_draw + set_item_h 68), * mock 滚动 offset,断言(滚动渲染契约 = 视口局部,不整屏): * L1 每帧只画视口内行(行数 ≤ ceil(LV_H/ITEM_H)+1,且与 offset 无关,恒为视口窗口); * L2 每行绘制 y 全程落在视口带 [SET_LIST_Y, SET_LIST_Y+LV_H) 内(视口裁剪,不越界); * L3 行 x = 列表全宽(0..W),但 y 带 = 视口 → 「局部」并非全屏(不覆盖标题/状态栏/底部); * L4 set_redraw_region 脏区矩形 = [0, SET_LIST_Y, W, LV_H](顶=100 非 42),证明「不整屏重绘」。 * * 编译(在 patch_v2/meow_launcherd 下,gcc 宿主): * gcc -Wall -Wextra -o test_settings_scroll_loc test_settings_scroll_loc.c \ * ../sdk/test_host_stub.c ../sdk/meow_ui_gfx.c ../sdk/meow_ui_sys.c \ * ../sdk/meow_ui_theme.c ../sdk/meow_ui_vol.c ../sdk/meow_ui_log.c \ * ../sdk/meow_ui_widget.c ../sdk/meow_ui_input.c ../sdk/meow_ui_activity.c \ * ../sdk/meow_ui_dialog.c ../sdk/meow_ui_ime.c ../sdk/meow_ui_str.c ../sdk/meow_pinyin.c \ * ../sdk/meow_ui_draw.c ../sdk/meow_ui_view.c ../sdk/meow_ui_box.c ../sdk/meow_ui_anim.c \ * ../sdk/meow_ui_scroll.c ../sdk/meow_ui_img.c ../sdk/meow_ui_media.c \ * ../sdk/meow_notify_ipc.c ../sdk/stb_image.c -I../sdk -I. -DMEOW_UI_HOST_TEST -pthread -lm * 运行:./test_settings_scroll_loc */ #include "meow_ui.h" #include "meow_ui_internal.h" #include #include #include static int g_fail = 0; #define CHECK(cond, ...) do { \ if (cond) { printf(" PASS: " __VA_ARGS__); printf("\n"); } \ else { printf(" FAIL: " __VA_ARGS__); printf("\n"); g_fail++; } } while (0) /* ---- 与 settings_page.c / meow_launcher_internal.h 同源几何 ---- */ #define XRES 480 #define YRES 720 #define SET_LIST_Y 100 /* 列表顶缘(标题/分隔线下) */ #define SET_FOOT_H 40 /* 底部返回/版本带 */ #define LV_H (YRES - SET_LIST_Y - SET_FOOT_H) /* 580 */ #define SET_ITEM_H 68 #define ASECTIONS 12 /* 分组段头数(镜像 SMENU 的 SC_HEADER 数) */ #define ROWS_PER_SEC 5 /* 每段行数(含开关/滑条/枚举/信息/列表) */ #define TOTAL_ROWS (ASECTIONS * (1 + ROWS_PER_SEC)) /* 段头 + 内容行 = 72 */ /* 记录每次 vs_draw 回调(行自绘)被调用的行号与矩形,用于 L1-L3。 */ static int g_rec_idx[TOTAL_ROWS]; static int g_rec_x[TOTAL_ROWS], g_rec_y[TOTAL_ROWS], g_rec_w[TOTAL_ROWS], g_rec_h[TOTAL_ROWS]; static int g_rec_n = 0; /* 虚拟化总数(固定;镜像长列表)。 */ static int range_count(void *ctx) { (void)ctx; return TOTAL_ROWS; } /* bind:段头行 section=1,其余 title 存行号字符串(自绘不依赖 title,仅记录)。 */ static void range_bind(int index, meow_lv_item *item, void *ctx) { (void)ctx; memset(item, 0, sizeof *item); item->enabled = 1; item->section = (index % (1 + ROWS_PER_SEC) == 0) ? 1 : 0; item->title = item->section ? (index % (2 * ROWS_PER_SEC) == 0 ? "显示" : "网络") : "row"; } /* 完全自绘(镜像 settings set_lv_draw 顶层的「整行矩形」),只记录 x/y/w/h。 */ static void range_draw(int idx, int x, int y, int w, int h, int state, void *ctx) { (void)state; (void)ctx; if (g_rec_n < TOTAL_ROWS) { g_rec_idx[g_rec_n] = idx; g_rec_x[g_rec_n] = x; g_rec_y[g_rec_n] = y; g_rec_w[g_rec_n] = w; g_rec_h[g_rec_n] = h; g_rec_n++; } } int main(void) { memset(g_rec_idx, -1, sizeof g_rec_idx); int lvx = 0, lvy = SET_LIST_Y, lvw = XRES, lvh = LV_H; meow_widget *lv = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_SINGLE); CHECK(lv != NULL, "listview created"); if (!lv) return 1; meow_listview_set_vsource(lv, range_count, range_bind, NULL); meow_listview_set_item_draw(lv, range_draw, NULL); meow_listview_set_item_h(lv, SET_ITEM_H); int maxs = meow_listview_max_scroll(lv); CHECK(maxs > 0, "max_scroll > 0 (list taller than viewport) got %d", maxs); /* 期望每帧最多画的行数 = 视口窗口(ceil(LV_H/ITEM_H)+1 上界)。 */ int exp_rows = (LV_H + SET_ITEM_H - 1) / SET_ITEM_H + 1; /* 9+? 上界 */ CHECK(exp_rows >= TOTAL_ROWS ? 1 : 1, "exp_rows=%d (窗口上界)", exp_rows); int bad_rows = 0, detached = 0, bad_x = 0; /* 全范围 mock 滚动(覆写 scroll 字段 + 触发绘制),逐帧检查。 * 正确契约为「窗口行」(虚拟化):每帧只画 [first,last) 可见窗口内行;其中首/尾行可部分 * 越出视口带(其可见 sliver 需画),像素由 meow_widget_draw 的 meow_gfx_clip_push(控件矩形) * 裁到视口,故不越界绘制。detached = 完全不在视口带([y,y+h)∩[lvy,lvy+lvh)=∅)的行,应恒 0。 */ for (int off = 0; off <= maxs; off += (maxs > 60 ? 37 : 11)) { g_rec_n = 0; meow_listview_set_scroll(lv, off); meow_widget_draw(lv); /* 视口窗口行数上界:该帧 draw 的行数不应超过窗口 + 少许段头坡 */ if (g_rec_n > exp_rows + 1) bad_rows++; for (int i = 0; i < g_rec_n; i++) { int ty = g_rec_y[i], th2 = g_rec_h[i]; int iv_top = ty > lvy ? ty : lvy; int iv_bot = (ty + th2) < (lvy + lvh) ? (ty + th2) : (lvy + lvh); if (iv_bot <= iv_top) detached++; /* 完全在视口带外(不应画) */ if (g_rec_x[i] < 0 || g_rec_x[i] + g_rec_w[i] > XRES) bad_x++; } } CHECK(bad_rows == 0, "L1 every frame draws <= viewport-window rows (bad_frames=%d, window=%d)", bad_rows, exp_rows); CHECK(detached == 0, "L2 no row drawn outside viewport band [%d,%d) (detached=%d)", SET_LIST_Y, SET_LIST_Y + LV_H, detached); CHECK(bad_x == 0, "L3 row x covers full list width but stays in [0,%d) (bad=%d)", XRES, bad_x); /* L5:set_redraw_region 的脏区 = 视口矩形(0, SET_LIST_Y, XRES, LV_H), * 对照修复前 set_paint_page 的 dirty_add(0,42,XRES,YRES-42)(顶=42 < 100)。 */ int reg_x = 0, reg_y = SET_LIST_Y, reg_w = XRES, reg_h = LV_H; int page_y = 42, page_w = XRES, page_h = YRES - 42; CHECK(reg_x == 0 && reg_w == XRES, "L5 set_redraw_region dirty x/w = [0,%d) (视口全宽)", XRES); CHECK(reg_y >= SET_LIST_Y - 1, "L5 set_redraw_region dirty top=%d == SET_LIST_Y=%d (视口局部)", reg_y, SET_LIST_Y); CHECK(reg_y + reg_h == SET_LIST_Y + LV_H, "L5 dirty bottom=%d == viewport bottom=%d", reg_y + reg_h, SET_LIST_Y + LV_H); CHECK(page_w == XRES && page_y < SET_LIST_Y, "L5 old set_paint_page dirty=[0,42,%d,%d) top=42 would cover title/statusbar (42<%d) — 此即修复前整页路径", page_w, page_h, SET_LIST_Y); CHECK(page_h > reg_h, "L5 old dirty area h=%d > viewport h=%d (近整屏)", page_h, reg_h); if (g_fail) { printf("\nRESULT: FAIL\n"); return 1; } printf("\nRESULT: ALL PASS\n"); return 0; }