320 lines
15 KiB
C
320 lines
15 KiB
C
/*
|
||
* 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 <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <assert.h>
|
||
|
||
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;
|
||
}
|