/* test_meow_ui_listvirt.c — 宿主验证 §5.5 虚拟化列表(W_LISTVIEW pool 模式): * T1 虚拟化正确性:10000 项滚动窗口内容正确(hit 行号映射)/ bind 次数有界(全量扫过=count)/ * 回滚不重绑(scroll 回顶 bind 计数不变)。 * T2 多行富 item 渲染:双行(标题+副标题)/ 单行(仅标题)/ 左图标占位 / 右尾件 / * 副标题超宽截断(不越过操作区)。 * T3 段头占位行(bind 返回 section=1)+ 空态占位(count=0 置 empty_text)。 * T4 完全自绘(set_item_draw 覆盖默认 renderer)。 * T5 焦点(tap_at → meow_ui_focus_owner)+ fling 联动(meow_scroll_wrap 包裹 pool 列表, * track+fling 吸附到行边界;滚动只改 viewport 偏移)。 * * 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global): * gcc -o test_meow_ui_listvirt test_meow_ui_listvirt.c test_host_stub.c \ * meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \ * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \ * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm * 运行:./test_meow_ui_listvirt */ #include "meow_ui.h" #include "meow_ui_internal.h" /* meow_ui_back_buf/pitch/bpp:真实像素断言 */ #include #include #include #include static int g_pass = 0, g_fail = 0; #define CHECK(cond, ...) do { \ if (cond) { g_pass++; } \ else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \ } while (0) /* ---- back_buf 像素读取(真实渲染断言) ---- */ static void rd_px(int x, int y, int *r, int *g, int *b) { unsigned char *bb = meow_ui_back_buf(); int line = meow_ui_back_buf_pitch(), bpp = meow_ui_back_buf_bpp(); if (!bb || x < 0 || y < 0) { *r = *g = *b = 0; return; } if (bpp == 16) { uint16_t px = *(uint16_t *)(bb + (size_t)y * line + (size_t)x * 2); *r = (((px >> 11) & 0x1F) << 3) | ((px >> 11) & 0x1F) >> 2; *g = (((px >> 5) & 0x3F) << 2) | ((px >> 5) & 0x3F) >> 4; *b = ((px & 0x1F) << 3) | ((px & 0x1F) >> 2); } else { uint32_t px = *(uint32_t *)(bb + (size_t)y * line + (size_t)x * 4); *r = (px >> 16) & 0xFF; *g = (px >> 8) & 0xFF; *b = px & 0xFF; } } static int near2(int a, int b) { int d = a - b; if (d < 0) d = -d; return d <= 14; } static int px_near(int x, int y, int r, int g, int b) { int rr, gg, bb; rd_px(x, y, &rr, &gg, &bb); return near2(rr, r) && near2(gg, g) && near2(bb, b); } static int region_has(int x, int y, int w, int h, int r, int g, int b) { int cnt = 0; for (int dy = 0; dy < h; dy++) for (int dx = 0; dx < w; dx++) if (px_near(x + dx, y + dy, r, g, b)) cnt++; return cnt; } #define UNPK(c, rr, gg, bb) do { (rr) = ((c) >> 16) & 0xFF; (gg) = ((c) >> 8) & 0xFF; (bb) = (c) & 0xFF; } while (0) /* ---- 虚拟化数据源(10000 项):bind 计数有界 / 内容正确 / 段头 ---- */ #define N 10000 static int g_bind_total; /* pool bind 总数(经 listv_slot 调用;hit 走独立路径不计数) */ static int g_bind_touch_count; /* bind 回调的独立计数(hit 也会调 bind,故仅命中计数用) */ static int g_count_override; /* <0 = 用默认 N;>=0 = 覆盖(空态/小列表用) */ static int g_section_at; /* 该 index 返回段头(-1=无) */ static int vcount(void *ctx) { (void)ctx; return g_count_override >= 0 ? g_count_override : N; } static void vbind(int idx, meow_lv_item *it, void *ctx) { (void)ctx; g_bind_total++; g_bind_touch_count++; memset(it, 0, sizeof *it); it->icon_kind = MEOW_LIST_ICON_FOLDER; it->right = MEOW_LIST_RIGHT_ARROW; it->on = 1; it->enabled = 1; if (g_section_at == idx) { it->section = 1; it->title = "SECTION A"; it->subtitle = NULL; it->icon_kind = MEOW_LIST_ICON_NONE; it->right = MEOW_LIST_RIGHT_NONE; return; } static char t[64], s[64]; snprintf(t, sizeof t, "title %d", idx); snprintf(s, sizeof s, "sub %d", idx); it->title = t; it->subtitle = s; } /* ---- 数据源 A:单行(无副标题/无图标/无尾件),供 T2 单行断言 ---- */ static int via_count(void *ctx) { (void)ctx; return 3; } static void via_bind(int idx, meow_lv_item *it, void *ctx) { (void)ctx; (void)idx; memset(it, 0, sizeof *it); static char t[32]; snprintf(t, sizeof t, "single %d", idx); it->title = t; it->subtitle = NULL; it->icon_kind = MEOW_LIST_ICON_NONE; it->right = MEOW_LIST_RIGHT_NONE; it->enabled = 1; } /* ---- 数据源:超宽副标题(T2 截断) ---- */ static int long_count(void *ctx) { (void)ctx; return 1; } static void long_bind(int idx, meow_lv_item *it, void *ctx) { (void)ctx; (void)idx; memset(it, 0, sizeof *it); it->title = "Title"; it->subtitle = "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW"; /* 超宽 */ it->icon_kind = MEOW_LIST_ICON_NONE; it->right = MEOW_LIST_RIGHT_NONE; it->enabled = 1; } /* ---- T4 完全自绘:整行画实心块 + 标题 ---- */ static int g_draw_calls; static void mydraw(int idx, int x, int y, int w, int h, int st, void *ctx) { (void)idx; (void)ctx; g_draw_calls++; meow_ui_fill_rect(x, y, w, h, 0x334455); /* 自定义行底 */ meow_ui_fill_rect(x + 4, y + 4, 8, 8, 0xFF0000); /* 角标记(自绘内容) */ if (st & MEOW_LISTVIEW_ST_SELECTED) meow_ui_fill_rect(x + w - 12, y + 4, 8, 8, 0x00FF00); } int main(void) { meow_ui_opts o = { 480, 720 }; if (meow_ui_init(&o) != 0) { printf("init failed\n"); return 1; } const meow_theme *th = meow_theme_get(); int sb_r, sb_g, sb_b, os_r, os_g, os_b, pc_r, pc_g, pc_b, ospv_r, ospv_g, ospv_b, prim_r, prim_g, prim_b; UNPK(th->bg, sb_r, sb_g, sb_b); UNPK(th->onSurface, os_r, os_g, os_b); UNPK(th->primaryContainer, pc_r, pc_g, pc_b); UNPK(th->onSurfaceVariant, ospv_r, ospv_g, ospv_b); UNPK(th->primary, prim_r, prim_g, prim_b); int thh = meow_ui_text_height(th->font_body); int itemh = 56; /* ============ T1 虚拟化正确性 / bind 有界 / 回滚不重绑 ============ */ { g_count_override = -1; g_section_at = -1; g_bind_total = 0; g_bind_touch_count = 0; int lvx = 20, lvy = 50, lvw = 440, lvh = 300; meow_widget *lv = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_NONE); CHECK(meow_listview_set_item_h(lv, itemh) == itemh, "item_h"); CHECK(meow_listview_set_vsource(lv, vcount, vbind, NULL) == 0, "vsource"); CHECK(meow_listview_max_scroll(lv) == N * itemh - lvh, "max_scroll %d", meow_listview_max_scroll(lv)); /* 560000-300=559700 */ CHECK(meow_listview_scroll(lv) == 0, "scroll0"); /* 首帧 draw:绑定视口行(first..last) */ meow_widget_draw(lv); int first0 = 0; int last0 = (0 + lvh) / itemh + 1; if (last0 > N) last0 = N; int B0 = g_bind_total; CHECK(B0 == (last0 - first0), "first draw binds %d rows got %d", last0 - first0, B0); /* 下滚 1 行:仅新入视口的行被 bind(delta==1),未离屏行不重绑 */ meow_listview_set_scroll(lv, itemh); meow_widget_draw(lv); int B1 = g_bind_total; CHECK(B1 == B0 + 1, "scroll+1row binds exactly 1 more got %d (was %d)", B1 - B0, B0); /* 回滚到顶:视口行槽位仍在 → 不重绑(delta==0) */ meow_listview_set_scroll(lv, 0); meow_widget_draw(lv); CHECK(g_bind_total == B1, "scroll-back no rebind got %d (was %d)", g_bind_total, B1); /* 全量扫过:每个 index 进入视口时 bind 恰一次 → 总数 == N(有界,非 per-frame × count) */ int prev = 1; while (prev <= N) { int off = prev * itemh; meow_listview_set_scroll(lv, off); meow_widget_draw(lv); if (off >= meow_listview_max_scroll(lv)) break; prev++; } meow_listview_set_scroll(lv, meow_listview_max_scroll(lv)); meow_widget_draw(lv); CHECK(g_bind_total == N, "full sweep binds each index once got %d (expect %d)", g_bind_total, N); CHECK(g_bind_touch_count == N, "bind callback called %d times (expect %d)", g_bind_touch_count, N); /* 回滚(扫到末尾后拉回顶):池发生整表 wrap(cap N) win = N; meow_listview_set_scroll(lv, 0); meow_widget_draw(lv); CHECK((g_bind_total - before2) <= win, "scroll-back rebind bounded (delta %d ≤ %d)", g_bind_total - before2, win); /* 内容正确:命中行号 = 滚动换算(视口窗口映射正确) */ CHECK(meow_listview_hit(lv, lvx + lvw - 10, lvy + 0 * itemh + 20) == 0, "hit0"); meow_listview_set_scroll(lv, 5 * itemh); int idx_mid = meow_listview_hit(lv, lvx + lvw - 10, lvy + itemh / 2); CHECK(idx_mid == 5, "hit maps scroll to row got %d (expect 5)", idx_mid); meow_listview_set_scroll(lv, 888 * itemh); int idx_mid2 = meow_listview_hit(lv, lvx + lvw - 10, lvy + itemh * 2 + itemh / 2); CHECK(idx_mid2 == 890, "hit row 2 of window got %d (expect 890)", idx_mid2); meow_widget_free(lv); } /* ============ T2 多行富 item 渲染:双行 / 单行 / 图标 / 尾件 / 截断 ============ */ { /* 双行(lines=2,默认):行 0 title+subtitle + icon(FOLDER) + 尾件(ARROW) */ int lvx = 20, lvy = 50, lvw = 440, lvh = 120; meow_widget *lv = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_NONE); CHECK(meow_listview_set_item_h(lv, itemh) == itemh, "item_h"); CHECK(meow_listview_set_vsource(lv, vcount, vbind, NULL) == 0, "vsource"); CHECK(meow_listview_set_item_renderer(lv, 2, 1, 1) == 0, "renderer"); meow_ui_clear(th->bg); meow_widget_draw(lv); int text_x = lvx + 16 + 24 + 16; /* show_icon=1 → 76 */ int op_start = lvx + (lvw * 2 / 3); /* 20+293=313 */ int ty0 = lvy + (itemh - (2 * thh + 2)) / 2; /* 双行标题 y */ int sy0 = ty0 + thh + 2; /* 副标题 y */ CHECK(region_has(text_x, ty0, 60, thh, os_r, os_g, os_b) > 2, "double-line title drawn"); CHECK(region_has(text_x, sy0, 60, thh, ospv_r, ospv_g, ospv_b) > 2, "double-line subtitle drawn"); CHECK(region_has(lvx + 16, lvy + (itemh - 24) / 2, 24, 24, prim_r, prim_g, prim_b) > 30, "icon placeholder drawn"); int ax = lvx + lvw - 16 - 24; CHECK(region_has(ax, lvy + itemh / 2 - 8, 20, 16, ospv_r, ospv_g, ospv_b) > 2, "right tail (arrow) drawn"); /* 单行(lines=1):仅标题居中,无副标题带 */ g_count_override = 3; meow_widget *lv1 = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_NONE); meow_listview_set_item_h(lv1, itemh); meow_listview_set_vsource(lv1, via_count, via_bind, NULL); meow_listview_set_item_renderer(lv1, 1, 0, 0); /* 单行、无图标、无尾件 */ meow_ui_clear(th->bg); meow_widget_draw(lv1); int t1y = lvy + (itemh - thh) / 2; CHECK(region_has(lvx + 16, t1y, 60, thh, os_r, os_g, os_b) > 2, "single-line title drawn"); CHECK(region_has(lvx + 16, t1y + thh + 2, 60, thh, ospv_r, ospv_g, ospv_b) == 0, "single-line no subtitle"); /* 单行档位默认行高 48(set_item_tier) */ meow_widget *lv2 = meow_listview_create(lvx, lvy, lvw, lvh, 3, NULL, NULL, MEOW_LIST_SEL_NONE); CHECK(meow_listview_set_item_tier(lv2, 1) == 0, "tier"); CHECK(meow_listview_scroll(lv2) == 0 && meow_listview_max_scroll(lv2) == 3 * 48 - lvh, "tier-h %d", meow_listview_max_scroll(lv2)); meow_widget_free(lv2); /* 副标题超宽截断:不画过 text_maxw(op_start 之前) */ g_count_override = -1; meow_widget *lv3 = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_NONE); meow_listview_set_item_h(lv3, itemh); meow_listview_set_vsource(lv3, long_count, long_bind, NULL); meow_listview_set_item_renderer(lv3, 2, 0, 0); /* 无图标 → text_x = lvx+16=36 */ meow_ui_clear(th->bg); meow_widget_draw(lv3); int text_x3 = lvx + 16; int op3 = lvx + lvw - 16; /* show_right=0 → op_start=右缘-pad */ int maxw3 = op3 - text_x3; /* 20+440-16-36 = 408 */ int ty3 = lvy + (itemh - (2 * thh + 2)) / 2; int sy3 = ty3 + thh + 2; CHECK(region_has(text_x3, sy3, 40, thh, ospv_r, ospv_g, ospv_b) > 2, "long subtitle leading drawn"); CHECK(region_has(text_x3 + maxw3 + 4, sy3, 40, thh, ospv_r, ospv_g, ospv_b) == 0, "long subtitle truncated (no overflow)"); meow_widget_free(lv3); meow_widget_free(lv1); /* 截断不越界 + 双行副标题带(回归:无图标/无尾件时 text_x/pad 正确) */ CHECK(region_has(text_x3, ty3, 40, thh, os_r, os_g, os_b) > 2, "truncate-r title drawn"); meow_widget_free(lv); } /* ============ T3 段头占位 + 空态 ============ */ { int lvx = 20, lvy = 50, lvw = 440, lvh = 120; g_count_override = 3; g_section_at = 1; meow_widget *lv = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_NONE); meow_listview_set_item_h(lv, itemh); meow_listview_set_vsource(lv, vcount, vbind, NULL); meow_ui_clear(th->bg); meow_widget_draw(lv); int sch_r, sch_g, sch_b; UNPK(th->surfaceContainerHigh, sch_r, sch_g, sch_b); CHECK(region_has(lvx, lvy + itemh, lvw, itemh - 4, sch_r, sch_g, sch_b) > 40, "section header band drawn"); CHECK(region_has(lvx + 16, lvy + itemh + (itemh - thh) / 2, 60, thh, prim_r, prim_g, prim_b) > 2, "section header title (primary) drawn"); /* 空态:count=0 + empty_text → 视口内居中画文案(onSurfaceVariant) */ g_count_override = 0; g_section_at = -1; meow_widget *lve = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_NONE); meow_listview_set_vsource(lve, vcount, vbind, NULL); meow_listview_set_empty_text(lve, "No data"); /* ASCII(宿主 5x7 点阵仅含 ASCII/CJK 用真机字库) */ meow_ui_clear(th->bg); meow_widget_draw(lve); int ety = lvy + (lvh - thh) / 2; CHECK(region_has(lvx + lvw / 2 - 60, ety, 120, thh, ospv_r, ospv_g, ospv_b) > 2, "empty state text drawn"); meow_widget_free(lve); meow_widget_free(lv); } /* ============ T4 完全自绘(set_item_draw 覆盖默认 renderer) ============ */ { int lvx = 20, lvy = 200, lvw = 440, lvh = 120; g_count_override = 3; g_section_at = -1; g_draw_calls = 0; meow_widget *lv = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_SINGLE); meow_listview_set_item_h(lv, itemh); meow_listview_set_vsource(lv, vcount, vbind, NULL); CHECK(meow_listview_set_item_draw(lv, mydraw, NULL) == 0, "set_draw"); meow_ui_clear(th->bg); meow_widget_draw(lv); int vis = (lvh / itemh) + 1; if (vis > 3) vis = 3; CHECK(g_draw_calls == vis, "draw_item called per visible row got %d (expect %d)", g_draw_calls, vis); CHECK(region_has(lvx + 4, lvy + 4, 8, 8, 0xFF, 0x00, 0x00) > 0, "custom row content drawn"); meow_widget_free(lv); } /* ============ T5 焦点 + fling 联动(meow_scroll_wrap 包裹 pool 列表) ============ */ { int lvx = 20, lvy = 330, lvw = 440, lvh = 300; g_count_override = -1; g_section_at = -1; meow_widget *lv = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_SINGLE); meow_listview_set_item_h(lv, itemh); meow_listview_set_vsource(lv, vcount, vbind, NULL); /* 焦点:tap_at 命中行 → 列表成为唯一焦点(§6.4) */ int tidx = meow_listview_tap_at(lv, lvx + lvw - 10, lvy + itemh / 2); CHECK(tidx >= 0, "tap_at returns row got %d", tidx); CHECK(meow_ui_focus_owner() == (void *)lv, "list acquired focus after tap_at"); /* fling:track+fling → meow_scroll_wrap 包裹;吸附到行边界(snap=itemh) */ meow_scroll *s = meow_scroll_wrap(lv, 1); /* 1=W_LISTVIEW */ CHECK(s != NULL, "scroll wrap pool list"); meow_scroll_set_snap(s, itemh); uint64_t t = meow_ui_time_ms(); for (int i = 0; i < 12; i++) meow_scroll_track(s, 30, t + (uint64_t)i * 16); int start = meow_scroll_offset(s); CHECK(start > 0, "drag advances offset got %d", start); meow_scroll_fling(s); CHECK(meow_scroll_active(s) == 1, "fling active"); meow_scroll_stop(s); CHECK(meow_scroll_active(s) == 0, "stop ok"); /* 再次 fling 到自然停:offset 只改 viewport,且 snap 到行边界 */ for (int i = 0; i < 12; i++) meow_scroll_track(s, 30, t + (uint64_t)i * 16); start = meow_scroll_offset(s); meow_scroll_fling(s); int max_off = start, guard = 0; while (meow_scroll_active(s) && guard++ < 300) { meow_anim_tick(); int cur = meow_scroll_offset(s); if (max_off < cur) max_off = cur; usleep(10000); } meow_anim_tick(); int end = meow_scroll_offset(s); CHECK(end >= start, "fling advances got %d vs %d", end, start); CHECK((end % itemh) == 0, "snap to item boundary got %d (%%%d)", end, itemh); CHECK(end <= meow_listview_max_scroll(lv), "fling in range %d (max %d)", end, meow_listview_max_scroll(lv)); /* 滚动只改 viewport:meow_listview_scroll 与物理层一致 */ CHECK(meow_listview_scroll(lv) == end, "listview_scroll mirrors scroll offset %d vs %d", meow_listview_scroll(lv), end); /* 停稳后 draw 反映最终窗口(bind 只补新入视口的行,不整表重绑) */ int before_draw = g_bind_total; int win = (lvh / itemh) + 1; if (win > N) win = N; /* 视口窗口行数(含底缘 1 行) */ meow_widget_draw(lv); CHECK(g_bind_total >= before_draw && (g_bind_total - before_draw) <= win, "settle draw rebinds only window (delta %d ≤ %d)", g_bind_total - before_draw, win); meow_scroll_free(s); meow_widget_free(lv); } meow_ui_shutdown(); printf("listvirt: %d pass / %d fail\n", g_pass, g_fail); return g_fail ? 1 : 0; }