/* * test_meow_lyric_view.c — meow_lyric_view(SDK 通用歌词页控件)宿主单测(2026-09-03,批2)。 * * 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global; * 另链批1 meow_lyric.c 提供歌词数据;meow_lyric_view.c 为被测控件): * gcc -Wall -Wextra -o test_meow_lyric_view test_meow_lyric_view.c test_host_stub.c \ * meow_lyric.c meow_lyric_view.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_lyric_view * * 覆盖矩阵(对齐任务批2:活动行纯白/灰阶、翻译行占位不叠行、偏上锚定、折行中西文、点行 seek 命中、 * 主题换色、scrim 像素渲染): * L1 活动行纯白 + 上下行逐级灰阶单调递减(dark 主题:onSurface=0xFFFFFF 纯白) * L2 翻译行占位不叠行:3 行原文+翻译 → 活动行块高 bh=rw+1,下一块始于翻译行之后(块间无重叠) * L3 偏上锚定:活动行原文块中心 = 视图中线 - above_off(严格偏上) * L4 折行中西文:长行折成多段(rw>1),且块间按子行高排列 * L5 点行 seek 命中:命中=布局一致(点块内/块外就近取整;点翻译行归本行不误触下一行) * L6 主题换色:set_colors 覆盖活动行/灰阶/背景;NULL 恢复主题默认 * L7 scrim 像素渲染:draw 后背底像素≈scrim 色(fill_rect_alpha 真实落地) */ #include "meow_ui.h" #include "meow_ui_internal.h" /* meow_ui_back_buf/pitch/bpp:真实像素断言 */ #include "meow_lyric_view.h" #include #include #include #include static int g_fail = 0; #define CHECK(cond, name) do { \ if (cond) printf(" PASS %s\n", name); \ else { printf(" FAIL %s (%s:%d)\n", name, __FILE__, __LINE__); g_fail++; } } while (0) /* on_seek 回调收(验证 widget hit 路径回调接线)。 */ static int64_t g_seek_ms = -1; static void seek_cb(int64_t ms, void *ctx) { (void)ctx; g_seek_ms = ms; } /* ---- 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); } /* 折行宽测量(宿主):ASCII=1 单位,CJK 多字节=2 单位(中西文断行用)。 */ static int lw(const char *u, int size, void *ctx) { (void)size; (void)ctx; int w = 0; for (const char *p = u; *p; ) { unsigned char c = (unsigned char)*p; int ch = (c < 0x80) ? 1 : (c < 0xE0) ? 2 : (c < 0xF0) ? 3 : 4; w += (c < 0x80) ? 1 : 2; p += ch; } return w; } /* 亮度(灰度单调性判断用)。 */ static int lum(uint32_t c) { return ((c >> 16) & 255) * 3 + ((c >> 8) & 255) * 6 + (c & 255); } /* * 测试歌词:7 行原创 + 两行翻译(与原文同戳合并)。下标: * 0 First(1000) / 1 Second(2000,tl=第二句翻译) / 2 Third(3000) / 3 Fourth(4000) * 4 Fifth(5000,tl=第五句翻译长文本折叠测试啊啊啊) / 5 Sixth(6000) / 6 Seventh(7000) */ static void load_lyric(void) { const char *lrc = "[00:01.00]First\n" "[00:02.00]Second\n" "[00:03.00]Third\n" "[00:04.00]Fourth\n" "[00:05.00]Fifth\n" "[00:06.00]Sixth\n" "[00:07.00]Seventh\n"; const char *tlrc = "[00:02.00]\u7b2c\u4e8c\u53e5\u7ffb\u8bd1\n" "[00:05.00]\u7b2c\u4e94\u53e5\u7ffb\u8bd1\u957f\u6587\u672c\u6298\u53e0\u6d4b\u8bd5\u554a\u554a\u554a\n"; int n = meow_lyric_parse(lrc, tlrc); assert(n == 7); /* 翻译合并校验(批1 语义,作前置保证) */ assert(strcmp(meow_lyric_line_tl(1), "\u7b2c\u4e8c\u53e5\u7ffb\u8bd1") == 0); assert(meow_lyric_line_tl(2)[0] == 0); } static void l1_white_gray(void) { printf("L1 活动行纯白 + 灰阶单调递减\n"); meow_theme_set_mode(MEOW_THEME_DARK); meow_lyric_view *lv = meow_lyric_view_create(0, 0, 480, 720, lw, NULL); assert(lv); const meow_theme *th = meow_theme_get(); /* 活动行 = 主题 onSurface(dark=0xFFFFFF 纯白) */ uint32_t cur = meow_lyric_view_cur_color(lv); CHECK(cur == th->onSurface, "cur == th->onSurface (dark=0xFFFFFF 纯白)"); CHECK((cur & 0xFFFFFF) == 0xFFFFFF, "cur 为纯白 0xFFFFFF"); CHECK(meow_lyric_view_line_color(lv, 0) == cur, "line_color(0)==cur"); /* 灰阶单调递减(距活动行越远越灰) */ int mono = 1; for (int d = 1; d <= 5; d++) if (lum(meow_lyric_view_line_color(lv, d)) >= lum(meow_lyric_view_line_color(lv, d - 1))) mono = 0; CHECK(mono, "gray dist 1..5 亮度单调递减"); /* 最远灰阶(对齐 ncm 0x555555) */ CHECK(meow_lyric_view_line_color(lv, 5) == 0x555555, "gray(5)==0x555555 (ncm lyr_gray_tbl)"); meow_lyric_view_free(lv); } static void l2_translation_block(void) { printf("L2 翻译行占位不叠行(3 行原文 + 翻译)\n"); meow_theme_set_mode(MEOW_THEME_DARK); meow_lyric_view *lv = meow_lyric_view_create(0, 0, 480, 720, lw, NULL); assert(lv); meow_lyric_view_set_pos(lv, 2000); /* cur = idx1 Second(有翻译) */ meow_lyric_view_vis vis[MEOW_LYRIC_VIEW_MAX_ROWS]; int cnt = meow_lyric_view_visible(lv, 2000, vis, MEOW_LYRIC_VIEW_MAX_ROWS); CHECK(cnt > 0, "可见条目数 > 0"); /* 找出当前行(idx1)所在块与下一块 */ int ci = -1, next = -1; for (int k = 0; k < cnt; k++) { if (vis[k].ent == 1) ci = k; else if (ci >= 0 && next < 0) next = k; /* 当前块之后的第一块 */ } CHECK(ci >= 0, "当前行(Second)在可见块中"); CHECK(vis[ci].bh == vis[ci].rw + 1, "当前行块高 bh=rw+1(原文折行 + 翻译行占 1 子行)"); if (next >= 0) { /* 下一块顶 = 当前块顶 + 当前块高×子行高(块间无重叠、翻译行不叠进下一行) */ CHECK(vis[next].y == vis[ci].y + vis[ci].bh * 44, "下一块始于翻译行之后(无叠行)"); } else { /* 当前行是最后可视块:其块高应把翻译行算进去(翻译行占位,否则行会被截断) */ CHECK(vis[ci].y + vis[ci].bh * 44 <= 720, "当前块含翻译行(占用垂直空间)"); } meow_lyric_view_free(lv); } static void l3_above_anchor(void) { printf("L3 偏上锚定(活动行块中心在视图中线上方)\n"); meow_theme_set_mode(MEOW_THEME_DARK); meow_lyric_view *lv = meow_lyric_view_create(0, 100, 480, 500, lw, NULL); /* 视图中线=350 */ assert(lv); meow_lyric_view_set_pos(lv, 5000); /* cur = idx4 Fifth(5 行之后,上方充足) */ meow_lyric_view_vis vis[MEOW_LYRIC_VIEW_MAX_ROWS]; int cnt = meow_lyric_view_visible(lv, 5000, vis, MEOW_LYRIC_VIEW_MAX_ROWS); CHECK(cnt > 0, "可见条目数 > 0"); int ci = -1; for (int k = 0; k < cnt; k++) if (vis[k].ent == 4) { ci = k; break; } CHECK(ci >= 0, "当前行(Fifth)在可见块中"); /* 活动行原文块中心 = 块顶 + rw×子行高/2 = 视图中线 - above_off(严格偏上 50px) */ int center = vis[ci].y + vis[ci].rw * 44 / 2; CHECK(center == 350 - 50, "活动行块中心=350-50=300(偏上锚定 above_off=50)"); CHECK(center < 350, "活动行块中心在视图中线(350)偏上"); meow_lyric_view_free(lv); } static void l4_wrap_cjk_west(void) { printf("L4 折行中西文(长行折多段,块间按子行高排列)\n"); meow_theme_set_mode(MEOW_THEME_DARK); /* 窄视图强制折行:w=120 → maxw=120-48=72 单位 */ meow_lyric_view *lv = meow_lyric_view_create(0, 0, 120, 720, lw, NULL); assert(lv); /* 用长英文+中文混排行,使当前行折成 >1 段 */ meow_lyric_parse( "[00:01.00]The quick brown fox jumps over the lazy dog and runs far away into the woods to find shelter from the cold rain today\n" "[00:05.00]\u4e2d\u6587\u4e2d\u6587\u4e2d\u6587\u4e2d\u6587\u4e2d\u6587\u4e2d\u6587\u4e2d\u6587\u4e2d\u6587\u4e2d\u6587\u8bba\u6587\u6d4b\u8bd5\u554a\u554a\u554a\u554a\n", NULL); meow_lyric_view_set_pos(lv, 1000); meow_lyric_view_vis vis[MEOW_LYRIC_VIEW_MAX_ROWS]; int cnt = meow_lyric_view_visible(lv, 1000, vis, MEOW_LYRIC_VIEW_MAX_ROWS); CHECK(cnt >= 2, "该行折成多段(可见条目含上下行)"); /* 当前行(idx0,长文本)应从 1 段折成多段 */ int ci = -1; for (int k = 0; k < cnt; k++) if (vis[k].ent == 0) { ci = k; break; } CHECK(ci >= 0, "当前行在可见块中"); CHECK(vis[ci].rw > 1, "长行折成 rw>1 段(中西文按 词/字符 断行)"); /* 块内子行间距为 sub_lh:子行间不重叠(块高=rw 子行) */ CHECK(vis[ci].bh == vis[ci].rw, "无翻译当前行 bh=rw(子行数=折行段数)"); /* 恢复 7 行歌词(供后续用例) */ load_lyric(); meow_lyric_view_free(lv); } static void l5_hit_line(void) { printf("L5 点行 seek 命中 = 布局一致\n"); meow_theme_set_mode(MEOW_THEME_DARK); meow_lyric_view *lv = meow_lyric_view_create(0, 100, 480, 500, lw, NULL); /* 视图中线=350 */ assert(lv); meow_lyric_view_set_pos(lv, 2000); /* cur = idx1 Second(有翻译) */ meow_lyric_view_vis vis[MEOW_LYRIC_VIEW_MAX_ROWS]; int cnt = meow_lyric_view_visible(lv, 2000, vis, MEOW_LYRIC_VIEW_MAX_ROWS); CHECK(cnt > 0, "可见条目数 > 0"); /* 命中必须与绘制布局一致:点每个可见块内子行,命中该块 ent */ int consistent = 1; for (int k = 0; k < cnt && consistent; k++) { int yb = vis[k].y, yh = vis[k].bh * 44; for (int dy = 0; dy < yh && consistent; dy += 4) { /* 块内等距采样 */ int hit = meow_lyric_view_hit_line(lv, 240, yb + dy); if (hit != vis[k].ent) consistent = 0; } } CHECK(consistent, "命中=布局(块内逐点命中 ent 一致)"); /* 点翻译行(当前行块内下一子行)归本行,不误触下一行 */ int ci = -1; for (int k = 0; k < cnt; k++) if (vis[k].ent == 1) { ci = k; break; } CHECK(ci >= 0, "当前行在可见块中"); if (ci >= 0 && vis[ci].bh > vis[ci].rw) { /* 有翻译行 */ int ty = vis[ci].y + vis[ci].rw * 44 + 10; /* 翻译子行内 */ CHECK(meow_lyric_view_hit_line(lv, 240, ty) == 1, "点翻译行归本行(Second)不误触下一行"); } /* 点可见块外(视图内)→ 就近取整(顶部/底部可见行) */ CHECK(meow_lyric_view_hit_line(lv, 240, 101) == vis[0].ent, "视图内顶缝→就近取第一可见块"); CHECK(meow_lyric_view_hit_line(lv, 240, 599) == vis[cnt - 1].ent, "视图内底缝→就近取最后可见块"); /* 视图外 → 未命中 */ CHECK(meow_lyric_view_hit_line(lv, 240, 50) == -1, "视图外(y=50)未命中"); CHECK(meow_lyric_view_hit_line(lv, 490, 300) == -1, "视图外(x=490)未命中"); /* widget 命中路径:命中当前行(有翻译)→ 触发 on_seek 回调 line_time(1)=2000 */ g_seek_ms = -1; meow_lyric_view_set_on_seek(lv, seek_cb, NULL); if (ci >= 0) { int cy = vis[ci].y + 2; /* 当前行原文块内 */ CHECK(meow_widget_hit(meow_lyric_view_widget(lv), 240, cy) == 1, "widget hit 命中当前行"); CHECK(g_seek_ms == 2000, "on_seek 回调 line_time(Second)=2000"); } else { CHECK(1, "on_seek 校验跳过(无当前行块)"); } meow_lyric_view_free(lv); } static void l6_theme_switch(void) { printf("L6 主题换色(set_colors 覆盖 / NULL 恢复默认)\n"); meow_theme_set_mode(MEOW_THEME_DARK); meow_lyric_view *lv = meow_lyric_view_create(0, 0, 480, 720, lw, NULL); assert(lv); uint32_t def_cur = meow_lyric_view_cur_color(lv); /* dark=0xFFFFFF */ /* 覆盖:活动行红、灰阶自定义、scrim 自定义 */ meow_lyric_view_colors c; memset(&c, 0, sizeof c); c.scrim = 0x1E1E22; c.cur = 0xFF0000; c.scrim_alpha = 255; c.gray[0] = 0xFF0000; c.gray[1] = 0xCC0000; c.gray[2] = 0x990000; c.gray[3] = 0x660000; c.gray[4] = 0x440000; c.gray[5] = 0x220000; meow_lyric_view_set_colors(lv, &c); CHECK(meow_lyric_view_cur_color(lv) == 0xFF0000, "set_colors 覆盖活动行为红"); CHECK(meow_lyric_view_line_color(lv, 3) == 0x660000, "set_colors 覆盖灰阶(3)=0x660000"); CHECK(meow_lyric_view_line_color(lv, 5) == 0x220000, "set_colors 覆盖灰阶(5)=0x220000"); /* 恢复默认:NULL → 回主题 onSurface 纯白 */ meow_lyric_view_set_colors(lv, NULL); CHECK(meow_lyric_view_cur_color(lv) == def_cur, "set_colors(NULL) 恢复主题默认"); meow_lyric_view_free(lv); } static void l7_scrim_pixel(void) { printf("L7 scrim 像素渲染(draw 落地)\n"); meow_theme_set_mode(MEOW_THEME_DARK); meow_lyric_view *lv = meow_lyric_view_create(0, 0, 480, 720, lw, NULL); assert(lv); /* 不透明 scrim → 背底像素应为 scrim 色(RGB565 舍入容差) */ meow_lyric_view_colors c; memset(&c, 0, sizeof c); c.scrim = 0x203040; c.scrim_alpha = 255; meow_lyric_view_set_colors(lv, &c); meow_lyric_view_set_pos(lv, 2000); static const char *lrc = "[00:01.00]First\n[00:02.00]Second\n[00:03.00]Third\n[00:04.00]Fourth\n"; meow_lyric_parse(lrc, NULL); meow_ui_clear(0); meow_widget *w = meow_lyric_view_widget(lv); meow_widget_draw(w); CHECK(px_near(240, 350, 0x20, 0x30, 0x40), "视图中线像素≈scrim 0x203040(文本打桩不影响 scrim)"); CHECK(px_near(50, 50, 0x20, 0x30, 0x40), "视图左上像素≈scrim 0x203040"); meow_lyric_view_free(lv); } int main(void) { setbuf(stdout, NULL); setenv("MEOW_UI_DUMP", "/tmp/lyricview.ppm", 1); meow_ui_opts o = { 480, 720 }; assert(meow_ui_init(&o) == 0); meow_ui_activity_push(0x53); load_lyric(); l1_white_gray(); l2_translation_block(); l3_above_anchor(); l4_wrap_cjk_west(); l5_hit_line(); l6_theme_switch(); l7_scrim_pixel(); meow_ui_activity_pop(); printf(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS\n", g_fail); return g_fail; }