/* test_meow_ui_listview_rowh.c — 宿主回归(M15):listview 默认行高必须由字体行高派生, * 首行(scroll=0)标题必须真正画出来。 * * 根因(M12 finding):双行 renderer 把「标题+副标题」文本块 * block = 2 * meow_ui_text_height(font_body) + MEOW_LISTV_SUB_GAP(2) * 在行内垂直居中(ty = 行顶 + (item - block)/2),且文本只在 `ty >= 视口顶` 时才画。 * 真机 source_han_serif 20px(ascent 24 + descent 6)行高 = 30 → block = 62; * 旧 SDK 默认行高固定 56 < 62 → ty = 行顶 - 3,首行(行顶 == 视口顶)标题被整条跳过, * 只剩副标题 → 所有 App 的两行列表首行标题消失。 * * 宿主 5x7 字体行高是 14,照原样永远复现不了。故用链接期 `-Wl,--wrap=meow_ui_text_height` * 把行高替换成真机实测的 30,驱动同一份 SDK 代码(同 M12 app/test/test_first_row_title_render.c): * A 行高 30 + 显式 item_h=56 → 首行标题带 0 像素(bug 形态,防回归哨兵) * B 行高 30 + 不设 item_h → 派生 62 = block,首行标题/副标题都有像素 * C 行高 30 + 显式 item_h=70 → ncm 语义(App 自管卡高)原样生效,标题可见 * 断言全部读 back_buf 真实像素(region_has),非目检。 * * 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global): * gcc -o test_meow_ui_listview_rowh test_meow_ui_listview_rowh.c test_host_stub.c \ * meow_ui_gfx.c meow_ui_time.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 meow_ui_nav.c \ * meow_jpeg.c meow_png_stream.c \ * -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm -lz \ * -Wl,--wrap=meow_ui_text_height * 运行:./test_meow_ui_listview_rowh */ #include "meow_ui.h" #include "meow_ui_internal.h" /* meow_ui_back_buf/pitch/bpp:真实像素断言 */ #include #include #include /* --wrap 替换实现:真机 source_han_serif 20px 实测行高(font.idx ascent+descent)。 */ int __wrap_meow_ui_text_height(int size) { (void)size; return 30; } #define LH 30 /* 真机正文行高 */ #define BLOCK (2 * LH + 2) /* 双行文本块 = 62(与 MEOW_LISTV_SUB_GAP 同口径) */ #define LV_X 24 #define LV_Y 40 #define LV_W 432 #define LV_H 180 #define NR 5 /* 行数 */ 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) /* ---- 数据源:src2(每帧取行)与 pool(count/bind)各一套,行内容相同 ---- */ static const char *const k_title[NR] = { "T1", "T2", "T3", "T4", "T5" }; static const char *const k_sub[NR] = { "S1", "S2", "S3", "S4", "S5" }; static int src2_cb(int idx, meow_list_item *out, void *ctx) { (void)ctx; if (idx < 0 || idx >= NR) return 0; memset(out, 0, sizeof *out); out->title = k_title[idx]; out->subtitle = k_sub[idx]; out->right = MEOW_LIST_RIGHT_NONE; out->enabled = 1; return 1; } static int pool_count(void *ctx) { (void)ctx; return NR; } static void pool_bind(int idx, meow_lv_item *it, void *ctx) { (void)ctx; memset(it, 0, sizeof *it); if (idx < 0 || idx >= NR) return; it->title = k_title[idx]; it->subtitle = k_sub[idx]; it->enabled = 1; } /* 造一个 src2 双行列表(扁平基线,便于按颜色断言行内文本带)。不设 item_h → 走派生。 */ static meow_widget *make_lv(void) { meow_widget *lv = meow_listview_create(LV_X, LV_Y, LV_W, LV_H, NR, src2_cb, NULL, MEOW_LIST_SEL_NONE); if (!lv) return NULL; meow_listview_set_container(lv, 0); meow_listview_set_item_renderer(lv, 2, 0, 0); /* 双行、无图标、无尾件 → text_x = LV_X+16 */ return lv; } 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 os_r, os_g, os_b, osv_r, osv_g, osv_b; UNPK(th->onSurface, os_r, os_g, os_b); UNPK(th->onSurfaceVariant, osv_r, osv_g, osv_b); const int text_x = LV_X + 16; /* show_icon=0, container=0 */ /* ---- T0 --wrap 生效:宿主行高被替换为真机 30 ---- */ CHECK(meow_ui_text_height(th->font_body) == LH, "wrap line_h=%d (expect %d)", meow_ui_text_height(th->font_body), LH); /* ---- T1 默认(不设 item_h):行高按字体行高派生,首行标题可见 ---- */ { meow_widget *lv = make_lv(); CHECK(lv != NULL, "create"); CHECK(meow_listview_item_h(lv) == BLOCK, "derived item_h=%d (expect %d = 2*%d+2)", meow_listview_item_h(lv), BLOCK, LH); CHECK(meow_listview_item_h(lv) >= 2 * LH + 2, "item_h >= 2*line_h + gap"); CHECK(meow_listview_scroll(lv) == 0, "scroll=0"); CHECK(meow_listview_max_scroll(lv) == NR * BLOCK - LV_H, "max_scroll=%d", meow_listview_max_scroll(lv)); CHECK(meow_listview_hit(lv, LV_X + 10, LV_Y) == 0, "hit row0"); CHECK(meow_listview_hit(lv, LV_X + 10, LV_Y + BLOCK) == 1, "hit row1 (item_h=%d)", meow_listview_item_h(lv)); int ty = LV_Y + (BLOCK - BLOCK) / 2; /* 62-62 → 首行标题顶 == 视口顶 */ int sy = ty + LH + 2; meow_ui_clear(th->bg); meow_widget_draw(lv); CHECK(region_has(text_x, ty, 60, LH, os_r, os_g, os_b) > 2, "首行标题可见(scroll=0, ty=%d)", ty); CHECK(region_has(text_x, sy, 60, LH, osv_r, osv_g, osv_b) > 2, "首行副标题可见(sy=%d)", sy); CHECK(region_has(text_x, LV_Y - 20, 60, 20, os_r, os_g, os_b) == 0, "视口上方无标题泄漏"); meow_widget_free(lv); } /* ---- T2 裸 create(不调 renderer):默认 lines=2 同样派生 ---- */ { meow_widget *lv = meow_listview_create(LV_X, LV_Y, LV_W, LV_H, NR, src2_cb, NULL, MEOW_LIST_SEL_NONE); CHECK(lv != NULL && meow_listview_item_h(lv) == BLOCK, "bare create item_h=%d", lv ? meow_listview_item_h(lv) : -1); /* set_item_h(<=0) 忽略:仍返回派生值 */ CHECK(meow_listview_set_item_h(lv, 0) == BLOCK, "set_item_h(0) keeps derived"); CHECK(meow_listview_item_h(lv) == BLOCK, "item_h unchanged after set_item_h(0)"); meow_widget_free(lv); } /* ---- T3 显式 set_item_h 优先(ncm 语义:App 按自身卡高传 70)---- */ { meow_widget *lv = make_lv(); CHECK(meow_listview_set_item_h(lv, 70) == 70, "explicit item_h returns 70"); CHECK(meow_listview_item_h(lv) == 70, "explicit item_h=70 wins over derived %d", BLOCK); CHECK(meow_listview_max_scroll(lv) == NR * 70 - LV_H, "max_scroll uses 70"); int ty = LV_Y + (70 - BLOCK) / 2; /* 4 */ int sy = ty + LH + 2; meow_ui_clear(th->bg); meow_widget_draw(lv); CHECK(region_has(text_x, ty, 60, LH, os_r, os_g, os_b) > 2, "显式 70 首行标题可见"); CHECK(region_has(text_x, sy, 60, LH, osv_r, osv_g, osv_b) > 2, "显式 70 首行副标题可见"); meow_widget_free(lv); } /* ---- T4 档位:tier 恢复派生默认(2→62 / 1→48),单行不画副标题 ---- */ { meow_widget *lv = make_lv(); CHECK(meow_listview_set_item_h(lv, 70) == 70, "pre-set explicit 70"); CHECK(meow_listview_set_item_tier(lv, 2) == 0, "tier(2)"); CHECK(meow_listview_item_h(lv) == BLOCK, "tier(2) 派生 item_h=%d", meow_listview_item_h(lv)); CHECK(meow_listview_set_item_tier(lv, 1) == 0, "tier(1)"); CHECK(meow_listview_item_h(lv) == 48, "tier(1) item_h=%d (expect 48)", meow_listview_item_h(lv)); int ty = LV_Y + (48 - LH) / 2; /* 9 */ meow_ui_clear(th->bg); meow_widget_draw(lv); CHECK(region_has(text_x, ty, 60, LH, os_r, os_g, os_b) > 2, "单行档位标题可见"); CHECK(region_has(text_x, LV_Y + 41, 60, 6, osv_r, osv_g, osv_b) == 0, "单行档位不画副标题"); meow_widget_free(lv); } /* ---- T5 pool 模式(set_vsource + 默认 renderer):派生行高 + 首行标题可见 ---- */ { meow_widget *lv = meow_listview_create(LV_X, LV_Y, LV_W, LV_H, 0, NULL, NULL, MEOW_LIST_SEL_NONE); meow_listview_set_container(lv, 0); CHECK(meow_listview_set_vsource(lv, pool_count, pool_bind, NULL) == 0, "vsource"); CHECK(meow_listview_set_item_renderer(lv, 2, 0, 0) == 0, "renderer(2,0,0)"); CHECK(meow_listview_item_h(lv) == BLOCK, "pool item_h=%d", meow_listview_item_h(lv)); int ty = LV_Y + (BLOCK - BLOCK) / 2; int sy = ty + LH + 2; meow_ui_clear(th->bg); meow_widget_draw(lv); CHECK(region_has(text_x, ty, 60, LH, os_r, os_g, os_b) > 2, "pool 首行标题可见"); CHECK(region_has(text_x, sy, 60, LH, osv_r, osv_g, osv_b) > 2, "pool 首行副标题可见"); meow_widget_free(lv); } /* ---- T6 旧行为哨兵:显式 56(< block 62)→ 首行标题仍被裁剪守卫跳过 ---- * 记录 bug 形态,证明上面的像素断言确实能区分「画出」与「跳过」; * 派生默认生效后这条路径只剩「App 明确要 56」时才会走到。 */ { meow_widget *lv = make_lv(); CHECK(meow_listview_set_item_h(lv, 56) == 56, "explicit 56"); CHECK(meow_listview_item_h(lv) == 56, "explicit 56 stays 56"); meow_ui_clear(th->bg); meow_widget_draw(lv); int ty = LV_Y + (56 - BLOCK) / 2; /* -3 → 行顶上方 */ int sy = ty + LH + 2; /* 29 */ CHECK(region_has(text_x, LV_Y, 60, LH, os_r, os_g, os_b) == 0, "旧 56 行高:首行标题被跳过(bug 形态)"); CHECK(region_has(text_x, sy, 60, LH, osv_r, osv_g, osv_b) > 2, "旧 56 行高:首行副标题仍画(症状「只有副标题」)"); meow_widget_free(lv); } printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail); return g_fail ? 1 : 0; }