Files
MeowOS-SDK/sdk/meow_lyric_view.c
T
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

363 lines
16 KiB
C
Raw 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.
/*
* meow_lyric_view.c — SDK 通用歌词页渲染控件实现(见 meow_lyric_view.h 的契约说明)。
*
* 上收自 ncm_app/app/ui/player.c:lyr_layout(活动行块中心偏上锚定、上方 ≥LYR_ABOVE_MIN 行已过、
* 下填满、总 ≤LYR_MAX_ROWS)+ plr_draw_lyrics(活动行纯白、上下行逐级灰阶、翻译紧跟随不叠行,
* 块高含翻译行是 agent-380 修好 s_lyr_bh 的关键)+ lyrics_line_tap(命中=布局逐块扫描)。折行复用
* 批1 meow_lyric_wrap(=ncm lyr_wrap_lines 的通用化),宽度测量经注入的 meow_lyric_width_fn。
*
* 控件形态:W_CUSTOM 自绘画布(meow_widget_custom_create),render_cb/hit_cb 由本 TU 实现。
* 布局(lv_compute_layout)计算结果缓存在 lv->vis[0..vis_n),绘制/命中/公开查询共用,三者一致。
*
* 内存纪律:per-instance struct(calloc);布局数组定长 [MEOW_LYRIC_VIEW_MAX_ROWS];折行复用缓冲
* 一块 [MEOW_LYRIC_VIEW_MAX_ROWS][MEOW_LYRIC_SEG_SZ](逐条目复用,同 ncm s_lyr_seg 模式);
* 零临时堆分配(meow_lyric_wrap 静态缓冲)。
*/
#include "meow_lyric_view.h"
#include "meow_ui_internal.h" /* meow_widget_rect:widget 为不透明类型,取几何用访问器 */
#include <stdlib.h>
#include <string.h>
/* 折行宽测量的兜底:字节数近似(仅当使用方未注入 width 时;非像素精确)。
* 保证无渲染引擎下行为确定(maxw 作字节预算,仍按中西文断行)——同 meow_lyric_wrap 的 NULL 语义。 */
static int lv_default_width(const char *u, int size, void *ctx)
{
(void)size; (void)ctx;
return (int)strlen(u);
}
/* 逐通道比例缩放(t=0..255,255=原色)。灰度递减用:对 cur 逐级缩灰(对纯白即复现 ncm 灰阶表)。 */
static uint32_t lv_scale_col(uint32_t c, int t)
{
int r = ((int)((c >> 16) & 255) * t) / 255;
int g = ((int)((c >> 8) & 255) * t) / 255;
int b = ((int)( c & 255) * t) / 255;
return ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)b;
}
/* 灰阶比例(距活动行 0..5):0xFFFFFF→{FFFFFF,BBBBBB,929292,808080,6A6A6A,555555}(ncm lyr_gray_tbl)。
* 对非纯白 cur 按同比例缩灰,仍单调变灰。 */
static const int lv_gray_fac[6] = { 255, 187, 146, 128, 106, 85 };
/* 两色线性混合(t=0..255,0=纯 a、255=纯 b)。翻译行融入背景用(无 GPU,逐通道整数混)。 */
static uint32_t lv_mix(uint32_t a, uint32_t b, int t)
{
if (t < 0) t = 0;
if (t > 255) t = 255;
int ar = (a >> 16) & 255, ag = (a >> 8) & 255, ab = a & 255;
int br = (b >> 16) & 255, bg = (b >> 8) & 255, bb = b & 255;
int r = ar + ((br - ar) * t + 128) / 255;
int g = ag + ((bg - ag) * t + 128) / 255;
int bl = ab + ((bb - ab) * t + 128) / 255;
return ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)bl;
}
struct meow_lyric_view {
meow_widget *w;
/* 宽度测量(复用批1 meow_lyric_wrap 的 width_fn 约定;NULL→字节近似兜底) */
meow_lyric_width_fn width;
void *width_ctx;
int64_t pos_ms; /* 当前播放位置(UI 线程写) */
meow_lyric_view_colors colors; /* 用户覆盖(has_colors=1 生效;否则主题默认) */
int has_colors;
meow_lyric_view_layout l; /* 布局参数(内建默认/用户覆盖) */
char empty[64]; /* 无歌词占位文案 */
meow_lyric_seek_fn on_seek;
void *seek_ctx;
/* 布局暂存(每次 lv_compute_layout 重建;UI 线程串行,per-instance 复用) */
meow_lyric_view_vis vis[MEOW_LYRIC_VIEW_MAX_ROWS];
int vis_n;
char seg[MEOW_LYRIC_VIEW_MAX_ROWS][MEOW_LYRIC_SEG_SZ]; /* 折行复用缓冲(逐条目覆盖) */
};
/* 内建默认布局(对齐 ncm LYR_PAD/SUB_LH/CUR_SZ/OTH_SZ/TL_SZ/MAX_ROWS/ABOVE_MIN/ABOVE_OFF)。 */
static const meow_lyric_view_layout k_def_layout = {
24, 44, 28, 24, 20, MEOW_LYRIC_VIEW_MAX_ROWS, 2, 50
};
static const char k_def_empty[] = "歌词加载中…";
/* 解析生效色:!has_colors 或字段 0/scrim_alpha<0 → 主题默认;否则用覆盖。out 必填。 */
static void lv_resolve_colors(const meow_lyric_view *lv, const meow_theme *th,
meow_lyric_view_colors *out)
{
/* 内建默认(theme 驱动) */
uint32_t scrim = th->scrim;
if (scrim == 0) scrim = th->surfaceDim; /* scrim 缺省为黑:用 surfaceDim 兜底(非纯黑) */
out->scrim = scrim;
out->cur = th->onSurface; /* dark=0xFFFFFF 纯白(活动行高亮) */
out->scrim_alpha = 176;
if (!lv->has_colors) {
out->gray[0] = out->cur;
for (int i = 1; i < 6; i++) out->gray[i] = lv_scale_col(out->cur, lv_gray_fac[i]);
return;
}
/* 用户覆盖(0 字段=沿用内建默认;scrim_alpha<0=沿用默认 176)。
* gray 显式字段优先;未给字段以「解析后 cur」为基准自动递减(对齐头文档
* "gray[1..5]=对 cur 逐级缩灰"——覆盖 cur 后灰阶也应随 cur,而非随主题默认 onSurface)。 */
if (lv->colors.scrim != 0) out->scrim = lv->colors.scrim;
if (lv->colors.cur != 0) out->cur = lv->colors.cur;
if (lv->colors.scrim_alpha >= 0) out->scrim_alpha = lv->colors.scrim_alpha;
out->gray[0] = (lv->colors.gray[0] != 0) ? lv->colors.gray[0] : out->cur;
for (int i = 1; i < 6; i++)
out->gray[i] = (lv->colors.gray[i] != 0) ? lv->colors.gray[i]
: lv_scale_col(out->cur, lv_gray_fac[i]);
}
/* 距活动行 dist 的生效行色(dist 越界 clamp 0..5)。 */
static uint32_t lv_line_color(const meow_lyric_view *lv, const meow_theme *th, int dist)
{
meow_lyric_view_colors c;
lv_resolve_colors(lv, th, &c);
if (dist < 0) dist = 0;
if (dist > 5) dist = 5;
return c.gray[dist];
}
/* 折行封套(width 缺省兜底;越界 max_rows 内 clamp 到缓冲容量)。返回段数(≥1)。 */
static int lv_wrap(const meow_lyric_view *lv, const char *txt, int size, int maxw,
char seg[][MEOW_LYRIC_SEG_SZ], int max_rows)
{
meow_lyric_width_fn w = lv->width ? lv->width : lv_default_width;
if (max_rows > MEOW_LYRIC_VIEW_MAX_ROWS) max_rows = MEOW_LYRIC_VIEW_MAX_ROWS;
int rc = meow_lyric_wrap(txt, size, maxw, seg, max_rows, w, lv->width_ctx);
if (rc > max_rows) rc = max_rows;
return rc;
}
/* 核心布局:按 pos_ms 计算可见条目(vis[0..n)。算法与 ncm lyr_layout 逐行为一致)——
* 活动行块中心锚定在视图中线 - above_off(偏上);上方保底 ≥above_min 行已过;下填满;总 ≤max_rows;
* 块高 bh = 原文折行数 rw + 当前行翻译占 1(其余条目无翻译,bh=rw;翻译不叠行,点行命中一致)。 */
static int lv_compute_layout(meow_lyric_view *lv, int64_t pos_ms, int maxw)
{
int n = meow_lyric_count();
int cur = (n > 0) ? meow_lyric_line_at(pos_ms) : -1;
memset(lv->vis, 0, sizeof(lv->vis));
lv->vis_n = 0;
if (cur < 0 || cur >= n) return 0;
int MAX = lv->l.max_rows;
if (MAX > MEOW_LYRIC_VIEW_MAX_ROWS) MAX = MEOW_LYRIC_VIEW_MAX_ROWS;
if (MAX < 1) MAX = 1;
int rc = lv_wrap(lv, meow_lyric_line_text(cur), lv->l.cur_sz, maxw, lv->seg, MAX);
const char *tl = meow_lyric_line_tl(cur);
int has_tl = (tl && tl[0]) ? 1 : 0;
int rc_tot = rc + has_tl; /* 活动行块总子行数(原文折行 + 翻译行) */
int cap = MAX - rc_tot; /* 活动行之外可用的视觉行数 */
if (cap < 0) cap = 0;
int above_want = (cap * 2) / 5; /* 偏上锚定:约 40% 行给上方 */
if (above_want > cur) above_want = cur; /* 上方行数上限 = 已过行数 */
if (above_want < lv->l.above_min) above_want = (cur >= lv->l.above_min) ? lv->l.above_min : cur;
if (above_want > cap) above_want = cap; /* 总行数 ≤ MAX */
/* 1) 上方(cur-1 向上)前插到头部,保持自顶向下 */
int a_cnt = 0, usedA = 0;
for (int i = cur - 1; i >= 0 && usedA < above_want; i--) {
int rr = lv_wrap(lv, meow_lyric_line_text(i), lv->l.oth_sz, maxw, lv->seg, MAX);
if (usedA + rr > above_want) break;
for (int k = a_cnt; k > 0; k--) lv->vis[k] = lv->vis[k - 1];
lv->vis[0].ent = i; lv->vis[0].rw = rr; lv->vis[0].bh = rr; lv->vis[0].sz = lv->l.oth_sz;
a_cnt++; usedA += rr;
}
/* 2) 当前行 */
lv->vis[a_cnt].ent = cur; lv->vis[a_cnt].rw = rc; lv->vis[a_cnt].bh = rc_tot; lv->vis[a_cnt].sz = lv->l.cur_sz;
/* 3) 下方(cur+1 向下) */
int below_want = cap - usedA;
if (below_want > (n - 1 - cur)) below_want = n - 1 - cur;
int b_cnt = 0, usedB = 0;
for (int i = cur + 1; i < n && usedB < below_want; i++) {
int rr = lv_wrap(lv, meow_lyric_line_text(i), lv->l.oth_sz, maxw, lv->seg, MAX);
if (usedB + rr > below_want) break;
int k = a_cnt + 1 + b_cnt;
lv->vis[k].ent = i; lv->vis[k].rw = rr; lv->vis[k].bh = rr; lv->vis[k].sz = lv->l.oth_sz;
b_cnt++; usedB += rr;
}
int cnt = a_cnt + 1 + b_cnt;
if (cnt == 0) return 0;
/* 4) y 定位:活动行原文块中心 = 视图中线 - above_off(偏上);上方向上、下方向下(块间无缝隙) */
int rect[4]; meow_widget_rect(lv->w, rect);
int anchor = rect[1] + rect[3] / 2 - lv->l.above_off; /* rect[1]=vy, rect[3]=vh */
lv->vis[a_cnt].y = anchor - rc * lv->l.sub_lh / 2;
for (int k = a_cnt + 1; k < cnt; k++)
lv->vis[k].y = lv->vis[k - 1].y + lv->vis[k - 1].bh * lv->l.sub_lh;
for (int k = a_cnt - 1; k >= 0; k--)
lv->vis[k].y = lv->vis[k + 1].y - lv->vis[k].bh * lv->l.sub_lh;
lv->vis_n = cnt;
return cnt;
}
/* 自绘回调:scrim 底 + 滚动歌词(活动行纯白偏上锚定 + 上下行逐级灰阶)+ 翻译行(不叠行)+ 空态占位。 */
static void lv_render(meow_widget *w, const meow_theme *th, void *ctx)
{
meow_lyric_view *lv = (meow_lyric_view *)ctx;
int rect[4]; meow_widget_rect(w, rect);
int x = rect[0], vy = rect[1], W = rect[2], H = rect[3];
meow_lyric_view_colors col;
lv_resolve_colors(lv, th, &col);
/* 背景 scrim(半透明叠在封面之上;scrim_alpha==0=不画,App 已自绘叠加层)。 */
if (col.scrim_alpha > 0) meow_ui_fill_rect_alpha(x, vy, W, H, col.scrim, col.scrim_alpha);
int n = meow_lyric_count();
int cur = (n > 0) ? meow_lyric_line_at(lv->pos_ms) : -1;
int maxw = W - 2 * lv->l.pad; if (maxw < 1) maxw = 1;
int cnt = lv_compute_layout(lv, lv->pos_ms, maxw);
uint32_t bgcol = col.scrim; /* 翻译行融入背景(渐隐) */
for (int k = 0; k < cnt; k++) {
int i = lv->vis[k].ent, rows = lv->vis[k].rw, sz = lv->vis[k].sz, vsy = lv->vis[k].y;
int is_cur = (i == cur);
int dist = is_cur ? 0 : ((i - cur) < 0 ? (cur - i) : (i - cur));
uint32_t c = lv_line_color(lv, th, dist);
int rr = lv_wrap(lv, meow_lyric_line_text(i), sz, maxw, lv->seg, rows);
if (rr > rows) rr = rows;
for (int r = 0; r < rr; r++)
meow_ui_text(x + lv->l.pad, vsy + r * lv->l.sub_lh, lv->seg[r], sz, c);
if (is_cur) {
const char *tl = meow_lyric_line_tl(i);
if (tl && tl[0])
meow_ui_text(x + lv->l.pad, vsy + rows * lv->l.sub_lh + 2, tl,
lv->l.tl_sz, lv_mix(0xFFFFFF, bgcol, 120));
}
}
if (n == 0)
meow_ui_text(x + lv->l.pad, vy + H / 2 - 10, lv->empty, 20, col.cur);
}
/* 命中回调:点行 seek(用与绘制一致的布局逐块扫描;块外就近取整)。
* 命中某行 → 存 last 行 + 回调 on_seek(ms)(若有),返回 1=消费;否则 0。 */
static int lv_hit_cb(meow_widget *w, int gx, int gy, void *ctx)
{
meow_lyric_view *lv = (meow_lyric_view *)ctx;
int rect[4]; meow_widget_rect(w, rect);
int rx = rect[0], ry = rect[1], rw = rect[2], rh = rect[3];
/* 视图矩形内含判定(含边缘;点必须在控件内)。 */
if (gx < rx || gx >= rx + rw || gy < ry || gy >= ry + rh) return 0;
int n = meow_lyric_count();
if (n <= 0) return 0;
int maxw = rw - 2 * lv->l.pad; if (maxw < 1) maxw = 1;
int cnt = lv_compute_layout(lv, lv->pos_ms, maxw);
if (cnt <= 0) return 0;
int line = -1;
for (int k = 0; k < cnt; k++) {
int yb = lv->vis[k].y;
int yh = lv->vis[k].bh * lv->l.sub_lh; /* 块高含当前行翻译,点翻译行归本行 */
if (gy >= yb && gy < yb + yh) { line = lv->vis[k].ent; break; }
}
if (line < 0) /* 落在可见块之外 → 就近取整(顶部/底部可见行) */
line = (gy < lv->vis[0].y) ? lv->vis[0].ent : lv->vis[cnt - 1].ent;
if (lv->on_seek) {
int64_t t = meow_lyric_line_time(line);
if (t >= 0) lv->on_seek(t, lv->seek_ctx);
}
return 1;
}
/* ---- 生命周期 ---- */
meow_lyric_view *meow_lyric_view_create(int x, int y, int w, int h,
meow_lyric_width_fn width, void *width_ctx)
{
meow_lyric_view *lv = (meow_lyric_view *)calloc(1, sizeof *lv);
if (!lv) return NULL;
lv->w = meow_widget_custom_create(x, y, w, h, lv_render, lv_hit_cb, lv);
if (!lv->w) { free(lv); return NULL; }
lv->width = width;
lv->width_ctx = width_ctx;
lv->l = k_def_layout;
memcpy(lv->empty, k_def_empty, sizeof k_def_empty);
return lv;
}
void meow_lyric_view_free(meow_lyric_view *lv)
{
if (!lv) return;
if (lv->w) meow_widget_free(lv->w);
free(lv);
}
meow_widget *meow_lyric_view_widget(const meow_lyric_view *lv)
{
return lv ? lv->w : NULL;
}
void meow_lyric_view_set_pos(meow_lyric_view *lv, int64_t pos_ms)
{
if (lv) lv->pos_ms = pos_ms;
}
void meow_lyric_view_set_colors(meow_lyric_view *lv, const meow_lyric_view_colors *c)
{
if (!lv) return;
if (!c) { lv->has_colors = 0; return; }
lv->colors = *c;
lv->has_colors = 1;
}
void meow_lyric_view_set_layout(meow_lyric_view *lv, const meow_lyric_view_layout *ls)
{
if (!lv) return;
lv->l = ls ? *ls : k_def_layout;
}
void meow_lyric_view_set_empty(meow_lyric_view *lv, const char *txt)
{
if (!lv) return;
if (!txt || !txt[0]) { lv->empty[0] = 0; return; }
strncpy(lv->empty, txt, sizeof lv->empty - 1);
lv->empty[sizeof lv->empty - 1] = 0;
}
void meow_lyric_view_set_on_seek(meow_lyric_view *lv, meow_lyric_seek_fn cb, void *ctx)
{
if (!lv) return;
lv->on_seek = cb;
lv->seek_ctx = ctx;
}
/* ---- 查询 ---- */
uint32_t meow_lyric_view_cur_color(const meow_lyric_view *lv)
{
if (!lv) return 0;
return lv_line_color(lv, meow_theme_get(), 0);
}
uint32_t meow_lyric_view_line_color(const meow_lyric_view *lv, int dist)
{
if (!lv) return 0;
return lv_line_color(lv, meow_theme_get(), dist);
}
int meow_lyric_view_hit_line(meow_lyric_view *lv, int x, int y)
{
if (!lv || !lv->w) return -1;
int rect[4]; meow_widget_rect(lv->w, rect);
int rx = rect[0], ry = rect[1], rw = rect[2], rh = rect[3];
if (x < rx || x >= rx + rw || y < ry || y >= ry + rh) return -1;
int n = meow_lyric_count();
if (n <= 0) return -1;
int maxw = rw - 2 * lv->l.pad; if (maxw < 1) maxw = 1;
int cnt = lv_compute_layout(lv, lv->pos_ms, maxw);
if (cnt <= 0) return -1;
for (int k = 0; k < cnt; k++) {
int yb = lv->vis[k].y, yh = lv->vis[k].bh * lv->l.sub_lh;
if (y >= yb && y < yb + yh) return lv->vis[k].ent;
}
return (y < lv->vis[0].y) ? lv->vis[0].ent : lv->vis[cnt - 1].ent;
}
int meow_lyric_view_visible(meow_lyric_view *lv, int64_t pos_ms,
meow_lyric_view_vis *out, int cap)
{
if (!lv || !lv->w) return 0;
int rect[4]; meow_widget_rect(lv->w, rect);
int maxw = rect[2] - 2 * lv->l.pad; if (maxw < 1) maxw = 1;
int cnt = lv_compute_layout(lv, pos_ms, maxw);
if (out && cnt > 0) {
int n = (cnt < cap) ? cnt : cap;
for (int i = 0; i < n; i++) out[i] = lv->vis[i];
}
return cnt;
}