Files
MeowOS-SDK/sdk/test_meow_lyric_view.c
AskaEth c0396e40ce sync(SDK): 增量同步主仓 M6.37–M6.39 至公开 SDK 仓
sdk/:
- 同步 meow_ui_widget/input/gfx/lazygrid/audio/media/img 等源码与头
  (SDK 默认列表/容器控件默认 MD3 — meow_list_set_container 可回退;
   全局 App 内底部上滑回桌面 — meow_ui_input_is_home_swipe 系统保留手势;
   LazyGrid/LazyColumn/LazyRow MD3 化;叠加让位自抑改 app_id 判定;
   DSD 音频格式等)
- 新增独立模块源码 meow_img_color / meow_lyric / meow_lyric_view 及对应宿主 test_*.c
- 保留公开仓自包含适配:vendored meow_font.c/meow_font.h/meow_notif.h 不动;
  build_sdk.sh 自包含版本不动;meow_ui_gfx.c 仅改 meow_font.h include 路径;
  meow_ui_internal.h 去除 FreeType 内部声明
- meow_ipc.h 同步到主仓 launcher 版本(含 AUDIO_FMT_DOP/媒体歌词/蓝牙输出协议)
- 不含 FreeType 矢量字 meow_ft_*(需 freetype vendor,公开仓以位图 meow_font
  自包含);不含 mipsel 二进制 / .o / .a / 固件 / 密钥 / devdocs

examples/hello/:
- 新增 ui/demo/sdk2/sdk2.c;build_hello.sh 纳入该文件(保持自包含)

docs / README / .gitignore:
- docs/MEOW_APP_SDK.md 补 §12.5(默认 MD3 容器 + 底部上滑回桌面 + 两个新 API)
- README 能力清单补「默认 MD3 容器」与「底部上滑回桌面」
- .gitignore 补新增宿主测试二进制

验证:sdk/meow_ui.a 与 examples/hello/hello 交叉编译均 0 warning;工作区无生成物。
2026-09-04 18:05:36 +08:00

320 lines
15 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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;
}