359 lines
15 KiB
C
359 lines
15 KiB
C
/*
|
||
* 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 兜底(非纯黑) */
|
||
uint32_t cur = th->onSurface; /* dark=0xFFFFFF 纯白(活动行高亮) */
|
||
out->scrim = scrim;
|
||
out->cur = cur;
|
||
out->scrim_alpha = 176;
|
||
out->gray[0] = cur;
|
||
for (int i = 1; i < 6; i++) out->gray[i] = lv_scale_col(cur, lv_gray_fac[i]);
|
||
|
||
if (!lv->has_colors) return;
|
||
/* 用户覆盖(0 字段=沿用上面内建默认;scrim_alpha<0=沿用默认 176) */
|
||
if (lv->colors.scrim != 0) out->scrim = lv->colors.scrim;
|
||
if (lv->colors.cur != 0) { out->cur = lv->colors.cur; out->gray[0] = lv->colors.cur; }
|
||
if (lv->colors.scrim_alpha >= 0) out->scrim_alpha = lv->colors.scrim_alpha;
|
||
for (int i = 0; i < 6; i++)
|
||
if (lv->colors.gray[i] != 0) out->gray[i] = lv->colors.gray[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;
|
||
}
|