fix(ncm): 列表首行标题不显示 — 行高按字体行高派生(M12)

根因(真机 seated,非数据/颜色/遮挡):SDK meow_listview 双行渲染把
「2*行高 + 2」的文本块在行内垂直居中,且标题只在 ty >= 视口顶时才画
(meow_ui_widget.c:2061 的越界守卫)。真机 source_han_serif 20px 面
ascent 24 + descent 6 → 行高 30 → 文本块 62px,而 ncm 行高 APP_ROW_H=56
→ ty = 行顶 - 3;首行行顶 == 视口顶 → ty < 视口顶 → 标题被整条跳过,
副标题(ty+32)仍在视口内照画。两类列表同用该路径 + 同一行高,故症状一致。

修复(不动 SDK):ncm_app_internal.h 新增 ncm_row_h()/ncm_row_h_for_line_h(),
行高 = max(56, 2*字体行高 + 2 + 8)(偶数;本机 30 → 70,卡高 62 == 文本块);
home/playlist/queue/cache/search 在 page create 取一次 g_item_h,统一用于
set_item_h 与卡片几何/可见行/页脚/补载视口换算。

回归:
- app/test/test_meow_row_text_fit.c(新):真机字库行高矩阵 14/21/26/27/30
  48 断言(文本块 ≤ 行高、首行 ty ≥ 0、卡高 ≥ 文本块、旧 56 哨兵)。
- app/test/test_first_row_title_render.c(新):--wrap 行高=30 驱动真实 SDK
  渲染 + 读回 PPM 像素计数:旧 56 → 首行标题 0 像素/副标题 186;新 70 →
  标题 84/副标题 100。
- test_meow_row_gap、plpage_sim 全 PASS;ncm_host 7 页出图与改动前逐字节相同
  (宿主行高 14 → 行高仍 56,证重构无视觉回归);5 页自检 PASS;0 warning。
- plpage_sim 临时目录改 $TMPDIR(Termux /tmp 不可写,原写死无法运行)。
- REPORT §43:根因/排除方向/修复/验证/真机验收要点。
This commit is contained in:
AskaEth committed 2026-11-05 19:51:01 +08:00
1 parent 98b9169b86
commit 22f9dab2ce
11 files changed
+440 -34

No files matched your search

+30 -4
View File
@@ -62,14 +62,14 @@ enum {
#define APP_W 480
#define APP_H 720
#define APP_TOPBAR_H 56
#define APP_ROW_H 56 /* 列表行高 */
#define APP_ROW_H 56 /* 列表行高下限(旧固定值;实际行高按字体行高派生,见 ncm_row_h) */
#define APP_PAD 16
#define APP_OP_W 48 /* 行右侧操作区触达宽 */
#define APP_FOOTER_H 26
/* 圆角卡列表项外边距(item margin/gap):相邻卡片间可见空隙 px。SDK listview 行距(=APP_ROW_H)
* 不变——卡片在其行高内上下对称内缩 NCM_ROW_GAP/2,故文本(SDK 按 APP_ROW_H 垂直居中)仍落卡中心;
* 相邻卡顶间距恒=APP_ROW_H=卡高+NCM_ROW_GAP。要求偶数,保证上下对称。 */
/* 圆角卡列表项外边距(item margin/gap):相邻卡片间可见空隙 px。SDK listview 行距(=ncm_row_h())
* 不变——卡片在其行高内上下对称内缩 NCM_ROW_GAP/2,故文本(SDK 按行高垂直居中)仍落卡中心;
* 相邻卡顶间距恒=行高=卡高+NCM_ROW_GAP。要求偶数,保证上下对称。 */
#define NCM_ROW_GAP 8
/* 第 idx 行(行高 item,滚动 off,列表顶 list_y)的圆角卡:顶 y 与高 h。 */
static inline int ncm_row_card_y(int item, int idx, int off, int list_y)
@@ -78,6 +78,32 @@ static inline int ncm_row_card_y(int item, int idx, int off, int list_y)
}
static inline int ncm_row_card_h(int item) { return item - NCM_ROW_GAP; }
/* ---- 列表行高(M12:首行标题不显示根因修复)----
* SDK meow_listview 的双行渲染(sdk/meow_ui_widget.c 的 2 行分支)把「标题+副标题」文本块
* block = 2 * meow_ui_text_height(font_body) + MEOW_LISTV_SUB_GAP(2)
* 在行内垂直居中(ty = 行顶 + (item - block) / 2),且标题只在 `ty >= 视口顶` 时才画
* (越界钳制,防泄漏)。行高 item 小于 block 时 ty = 行顶 - Δ,于是:
* - 第一行(行顶 == 视口顶)标题被钳掉、副标题(ty + thh + 2)仍在视口内 → 真机
* 「歌单列表/歌曲列表第 1 行只有副标题」,其余行标题画在自己行顶上方 Δ(不易察觉);
* - 文本块也比圆角卡高,两行文字溢出卡外。
* 真机字库实测(source_han_serif 20px:ascent 24 + descent 6 = 30)block = 62,旧固定行高
* 56 → Δ = 3。故行高必须由字体实测行高派生:block + NCM_ROW_GAP → 卡高 == 文本块,
* 文本完整落在卡内并仍居中。行高由 NCM_ROW_GAP 为偶数、2*行高 为偶数保证偶数(卡对称内缩)。 */
#define NCM_ROW_SUB_GAP 2 /* 与 SDK MEOW_LISTV_SUB_GAP 同口径(标题/副标题行间距) */
static inline int ncm_row_h_for_line_h(int line_h)
{
if (line_h < 1) line_h = 1; /* 字体未就绪的防御(0 高 → 退回下限) */
int h = 2 * line_h + NCM_ROW_SUB_GAP + NCM_ROW_GAP;
if (h < APP_ROW_H) h = APP_ROW_H;
if (h & 1) h++;
return h;
}
/* 当前主题字体下的两行列表行高(页面 create 时取值并缓存;字体档位切换走重建页面)。 */
static inline int ncm_row_h(void)
{
return ncm_row_h_for_line_h(meow_ui_text_height(meow_theme_get()->font_body));
}
/* ---- 播放页(§22 重做:封面页/歌词页双页,NCM_CACHE_DESIGN §6)----
* 沉浸底恒 NCM_PLAYER_BG(不随系统明暗反转——「不动沉浸底」);日夜间主题配色只映射到
* 半透明叠加层(歌词页 scrim)与 accent(红)。 */
@@ -0,0 +1,174 @@
/* app/test/test_first_row_title_render.c — 宿主渲染回归(M12):列表首行标题必须真正画出来。
*
* 目的:在宿主上用**真实 SDK listview 双行渲染**复现/验证真机根因,而不只是验几何公式。
* 真机字体(source_han_serif 20px,font.idx: ascent 24 + descent 6)行高 = 30,
* 双行文本块 = 2*30 + 2 = 62。旧固定行高 56 < 62 → SDK 的 2 行分支
* ty = 行顶 + (item - block)/2 = 行顶 - 3
* 而标题只在 `ty >= 视口顶` 时才画 → 第一行(行顶 == 视口顶)标题被整条跳过、副标题仍在。
*
* 宿主 5x7 字体的行高是 14(gfx 回退),照原样渲染永远复现不了。故用链接期
* `-Wl,--wrap=meow_ui_text_height` 把行高替换成真机实测的 30,驱动同一份 SDK 代码:
* A 旧行高 56 → 首行标题带无文字像素(bug 形态)
* B ncm_row_h_for_line_h(30) = 70 → 首行标题带/副标题带都有文字像素(修复形态)
* 断言直接读回 MEOW_UI_DUMP 写出的 PPM(按像素计数)。
*
* 编译/运行(先跑 build_ncm_host.sh 产出 out/host/sdk_*.o):
* gcc -O2 -Wall -Wextra -DMEOW_UI_HOST_TEST -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
* -Icore -Ivendor/jsmn -Ivendor/mbedtls/include -Ivendor/minimp3 -Ivendor/dr_flac \
* app/test/test_first_row_title_render.c out/host/sdk_*.o \
* -Wl,--wrap=meow_ui_text_height -lz -lpthread -lm -o test_first_row_title_render
* ./test_first_row_title_render # 产物落 $TMPDIR/first_row_*.ppm
*
* 断言:
* ① 行高替换生效(meow_ui_text_height(20) == 30);
* ② A:首行标题带零文字像素、副标题带有 → 复现「只有副标题」;
* ③ B:首行标题带与副标题带都有像素 → 修复后首行标题正常;
* ④ A/B 副标题带像素数都不为 0(副标题路径未受影响)。
*/
#include "../ncm_app_internal.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
/* --wrap 的替换实现:真机 source_han_serif 20px 实测行高(font.idx ascent+descent)。 */
int __wrap_meow_ui_text_height(int size)
{
(void)size;
return 30;
}
#define LV_X 16
#define LV_Y 40
#define LV_W 448
#define LV_H 300
static int src2(int idx, meow_list_item *out, void *ctx)
{
(void)ctx;
static char title[32], sub[32];
if (idx < 0 || idx >= 5) return 0;
snprintf(title, sizeof title, "T%d", idx + 1);
snprintf(sub, sizeof sub, "S%d", idx + 1);
memset(out, 0, sizeof *out);
out->icon_kind = MEOW_LIST_ICON_NONE;
out->title = title;
out->subtitle = sub;
out->right = MEOW_LIST_RIGHT_NONE;
out->enabled = 1;
return 1;
}
/* ---- 极简 PPM(P6) 读取 + 区域文字像素计数 ---- */
static unsigned char *ppm_load(const char *path, int *w, int *h)
{
FILE *f = fopen(path, "rb");
if (!f) { fprintf(stderr, "open %s failed\n", path); return NULL; }
char magic[3] = {0};
int maxv = 0;
if (fscanf(f, "%2s %d %d %d", magic, w, h, &maxv) != 4 ||
strcmp(magic, "P6") != 0 || *w <= 0 || *h <= 0) {
fclose(f); fprintf(stderr, "bad ppm %s\n", path); return NULL;
}
if (fgetc(f) < 0) { fclose(f); fprintf(stderr, "truncated ppm %s\n", path); return NULL; } /* 单个空白 */
size_t n = (size_t)*w * (size_t)*h * 3u;
unsigned char *buf = (unsigned char *)malloc(n);
if (!buf || fread(buf, 1, n, f) != n) { free(buf); fclose(f); return NULL; }
fclose(f);
return buf;
}
/* 区域内「与页底色不同」的像素数。页底色取 (470,700)(远离列表控件)。 */
static int ink_count(const unsigned char *img, int w, int x0, int y0, int x1, int y1)
{
const unsigned char *bg = img + ((size_t)700 * (size_t)w + 470u) * 3u;
int n = 0;
for (int y = y0; y < y1; y++)
for (int x = x0; x < x1; x++) {
const unsigned char *p = img + ((size_t)y * (size_t)w + (size_t)x) * 3u;
int d = abs((int)p[0] - (int)bg[0]) + abs((int)p[1] - (int)bg[1]) +
abs((int)p[2] - (int)bg[2]);
if (d > 24) n++;
}
return n;
}
/* 造一个双行列表(容器=关、无图标/右尾件)并整帧绘制(paint_full 内部 flush → dump PPM)。 */
static void render_once(int item)
{
meow_widget *lv = meow_listview_create(LV_X, LV_Y, LV_W, LV_H, 5,
src2, NULL, MEOW_LIST_SEL_NONE);
meow_listview_set_container(lv, 0);
meow_listview_set_item_renderer(lv, 2, 0, 0);
meow_listview_set_item_h(lv, item);
meow_listview_set_count(lv, 5);
meow_ui_set_page_bg(meow_theme_get()->bg);
meow_ui_paint_full(); /* 画 + meow_ui_flush → MEOW_UI_DUMP 落 PPM */
meow_widget_free(lv);
}
static char g_dump[256];
int main(void)
{
const char *td = getenv("TMPDIR");
if (!td || !td[0]) td = "/tmp";
snprintf(g_dump, sizeof g_dump, "%s/first_row.ppm", td);
setenv("MEOW_UI_DUMP", g_dump, 1);
meow_ui_opts o = { APP_W, APP_H };
if (meow_ui_init(&o) != 0) { fprintf(stderr, "init failed\n"); return 1; }
meow_theme_set_mode(MEOW_THEME_DARK);
int fail = 0;
if (meow_ui_text_height(20) != 30) {
printf(" FAIL 行高替换未生效(expected 30,got %d)\n", meow_ui_text_height(20));
fail = 1;
}
const int lh = 30;
const int block = 2 * lh + NCM_ROW_SUB_GAP; /* 62 */
const int fixed = ncm_row_h_for_line_h(lh); /* 70 */
const int ty_legacy = (APP_ROW_H - block) / 2; /* -3 */
const int ty_fixed = (fixed - block) / 2; /* +4 */
int n_title_a = 0, n_sub_a = 0, n_title_b = 0, n_sub_b = 0;
/* A:旧固定行高 56(bug 形态)。标题带取「文本行顶 ∩ 控件视口」(ty<0 时从 0 起),
* 这样「被裁剪守卫整条跳过」与「只画出上半截」也能区分开。 */
render_once(APP_ROW_H);
{
int pw = 0, ph = 0;
unsigned char *img = ppm_load(g_dump, &pw, &ph);
if (!img) return 1;
int ty0 = (ty_legacy > 0) ? ty_legacy : 0;
n_title_a = ink_count(img, pw, LV_X + 16, LV_Y + ty0,
LV_X + 16 + 40, LV_Y + ty_legacy + lh);
n_sub_a = ink_count(img, pw, LV_X + 16, LV_Y + ty_legacy + lh + NCM_ROW_SUB_GAP,
LV_X + 16 + 40, LV_Y + ty_legacy + 2 * lh + NCM_ROW_SUB_GAP);
free(img);
}
/* B:修复后的行高(ncm_row_h_for_line_h) */
render_once(fixed);
{
int pw = 0, ph = 0;
unsigned char *img = ppm_load(g_dump, &pw, &ph);
if (!img) return 1;
n_title_b = ink_count(img, pw, LV_X + 16, LV_Y + ty_fixed,
LV_X + 16 + 40, LV_Y + ty_fixed + lh);
n_sub_b = ink_count(img, pw, LV_X + 16, LV_Y + ty_fixed + lh + NCM_ROW_SUB_GAP,
LV_X + 16 + 40, LV_Y + ty_fixed + 2 * lh + NCM_ROW_SUB_GAP);
free(img);
}
printf("legacy item=%d ty=%d: title_ink=%d sub_ink=%d\n", APP_ROW_H, ty_legacy, n_title_a, n_sub_a);
printf("fixed item=%d ty=%d: title_ink=%d sub_ink=%d\n", fixed, ty_fixed, n_title_b, n_sub_b);
if (!(n_title_a == 0 && n_sub_a > 0)) fail = 1; /* ② 旧行高首行标题被跳过(bug 复现) */
if (!(n_title_b > 0 && n_sub_b > 0)) fail = 1; /* ③ 修复后首行标题/副标题都在 */
printf("%s: first-row title render (legacy skipped=%d, fixed drawn=%d)\n",
fail ? "FAIL" : "PASS", n_title_a == 0, n_title_b > 0);
return fail ? 1 : 0;
}
@@ -9,10 +9,12 @@
* test_meow_row_gap.c -o test_meow_row_gap && ./test_meow_row_gap
*
* 断言集(对应「相邻 item 顶之间 = item 高 + gap」的可见形式):
* ① 相邻卡顶间距 = APP_ROW_H(= 卡高 + gap;SDK 行距不变);
* ① 相邻卡顶间距 = item 高(= 卡高 + gap;SDK 行距不变);
* ② 卡片间可见外距 = NCM_ROW_GAP;
* ③ 卡在其行高内上下对称内缩 → SDK 文本(按 APP_ROW_H 垂直居中)落卡中心;
* ③ 卡在其行高内上下对称内缩 → SDK 文本(按行高垂直居中)落卡中心;
* ④ 滚动偏移只整体平移,不改变上述间距/对称。
* 行高在这里固定用 APP_ROW_H(几何对任意 item 成立;M12 起真机行高由 ncm_row_h() 按字体
* 行高派生,文本块必须装得下,见 test_meow_row_text_fit.c)。
*/
#include "../ncm_app_internal.h" /* APP_ROW_H / NCM_ROW_GAP / ncm_row_card_y / ncm_row_card_h */
#include <stdio.h>
@@ -20,7 +22,7 @@
int main(void)
{
const int item = APP_ROW_H; /* 行距(SDK listview item_h,不变) */
const int item = APP_ROW_H; /* 几何取固定行高(对任意 item 成立) */
const int gap = NCM_ROW_GAP;
const int list_y = 100;
@@ -0,0 +1,93 @@
/* app/test/test_meow_row_text_fit.c — 宿主回归(M12):两行列表行高必须容纳行文本块。
*
* 背景(真机根因):SDK meow_listview 的双行渲染(sdk/meow_ui_widget.c 的 2 行分支)把
* block = 2 * meow_ui_text_height(font_body) + 2 (标题行 + 2px 行距 + 副标题行)
* 在行内垂直居中:ty = 行顶 + (item - block) / 2;且标题只在 `ty >= 视口顶` 时才画(越界钳制)。
* 行高 item < block 时 ty < 行顶 → **第一行(行顶 == 视口顶)标题被整条跳过**、副标题仍在,
* 即用户报的「歌单列表/歌曲列表第 1 行只有副标题,名字空白」。
* 真机字库实测(source_han_serif 20px:ascent 24 + descent 6 → 行高 30)block = 62,旧固定
* 行高 56 → ty = 行顶 - 3。
*
* 本测试只验 app 侧行高派生(ncm_row_h_for_line_h,纯整数、不链 SDK):
* 行高 = max(APP_ROW_H, block + NCM_ROW_GAP)(偶数)→ 卡高 == 文本块,文本完整落在卡内。
*
* 编译/运行(同 test_meow_row_gap.c,需仓库根 ROOT 下的 SDK 头):
* gcc -O2 -Wall -Wextra -I. -I../sdk -I../meow_launcherd -Icore \
* -Ivendor/mbedtls/include -Ivendor/jsmn -Ivendor/qrcodegen/c \
* -Ivendor/minimp3 -Ivendor/dr_flac \
* test_meow_row_text_fit.c -o test_meow_row_text_fit && ./test_meow_row_text_fit
*
* 断言矩阵 = 真机已装字库 20px 面实测行高(font.idx 的 ascent+descent):
* source_han_serif 30(当前生效)、misans/default 27、google_sans/meslo 26、
* source_han_sans 21;14 = 宿主 5x7 回退。逐项断言:
* ① block ≤ item(否则居中偏移为负)
* ② 首行 ty ≥ 行顶(绝对:ty_abs ≥ 视口顶)→ 标题不被裁剪守卫跳过
* ③ 副标题底边 sy + 行高 ≤ item(副标题不越行)
* ④ 卡高(item - NCM_ROW_GAP)≥ block(两行文本全落在圆角卡内)
* ⑤ item ≥ APP_ROW_H 下限且为偶数(卡上下对称内缩 gap/2)
* ⑥ 旧固定行高 56 在行高 30 下 ty < 0(bug 形态复现,防止再写死 56)
*/
#include "../ncm_app_internal.h"
#include <stdio.h>
static int g_checks, g_fail;
static void chk(int cond, int line_h, const char *what)
{
g_checks++;
if (!cond) {
g_fail++;
printf(" FAIL lh=%d: %s\n", line_h, what);
}
}
int main(void)
{
static const int lh_all[] = { 14, 21, 26, 27, 30 };
const int n = (int)(sizeof lh_all / sizeof lh_all[0]);
const int list_y = 120; /* 任意列表顶(只验相对几何) */
for (int k = 0; k < n; k++) {
int lh = lh_all[k];
int item = ncm_row_h_for_line_h(lh);
int block = 2 * lh + NCM_ROW_SUB_GAP;
int ty = (item - block) / 2; /* SDK 双行文本块居中偏移 */
int sy = ty + lh + NCM_ROW_SUB_GAP; /* 副标题行顶 */
int card_h = ncm_row_card_h(item);
printf("lh=%2d item=%3d block=%3d ty=%d sy=%d card_h=%3d\n",
lh, item, block, ty, sy, card_h);
chk(block <= item, lh, "文本块必须 <= 行高(否则首行标题被视口顶裁剪)");
chk(ty >= 0, lh, "首行标题 y 不得高于行顶/视口顶");
chk(sy + lh <= item, lh, "副标题底边仍在行内");
chk(card_h >= block, lh, "两行文本块必须完整落在圆角卡内");
chk(item >= APP_ROW_H, lh, "行高不得小于 APP_ROW_H 下限");
chk((item & 1) == 0, lh, "行高必须偶数(卡上下对称内缩 gap/2)");
/* 首行(scroll=0 → 行顶 == 视口顶)的绝对坐标 */
int ty_abs = list_y + ty;
chk(ty_abs >= list_y, lh, "首行标题绝对 y 不得被视口顶裁掉");
chk(ty_abs + lh <= list_y + item, lh, "首行标题底边在行内");
/* 卡在行内对称 → 卡中心 == 行中心 */
int cy = ncm_row_card_y(item, 0, 0, list_y);
chk(cy + card_h / 2 == list_y + item / 2, lh, "卡中心 == 行中心");
}
/* 行高随字体行高单调不减 */
chk(ncm_row_h_for_line_h(30) >= ncm_row_h_for_line_h(26), 30, "行高随字体行高单调不减");
/* ⑥ 回归哨兵:旧固定行高 56 + 当前默认字体(行高 30)就是 bug 形态 */
{
const int lh = 30;
int block = 2 * lh + NCM_ROW_SUB_GAP; /* 62 */
int ty_legacy = (APP_ROW_H - block) / 2; /* -3 */
chk(ty_legacy < 0, lh, "旧行高 56 + 行高 30 → ty<0(首行标题被跳过,bug 复现)");
chk(ncm_row_h_for_line_h(lh) > APP_ROW_H, lh, "默认字体下行高必须大于旧值 56");
}
printf("%s: %d checks, %d fail (font line-height matrix: %d)\n",
g_fail ? "FAIL" : "PASS", g_checks, g_fail, n);
return g_fail ? 1 : 0;
}
+5 -3
View File
@@ -25,6 +25,7 @@ static ncm_rect_t r_cont, r_list;
static int g_bg_id, g_draw_id, g_empty_id, g_footer_id;
static int g_press_row, g_press_delta, g_dragging, g_drag_last_y;
static int g_last_count;
static int g_item_h = APP_ROW_H; /* 两行列表行高(M12:按字体行高派生,见 ncm_row_h) */
/* 扫描 worker(磁盘 IO 不进 UI 线程;gen 计数做完成信号) */
static pthread_mutex_t g_cmu = PTHREAD_MUTEX_INITIALIZER;
@@ -89,7 +90,7 @@ static void c_row_bg(void *ctx)
if (cnt <= 0) return;
const meow_theme *th = meow_theme_get();
const ncm_pal_t *pa = ncm_palette(g_dark);
int item = APP_ROW_H;
int item = g_item_h;
int off = meow_listview_scroll(g_list);
int first = off / item, last = (off + r_list.h) / item + 1;
if (last > cnt) last = cnt;
@@ -116,7 +117,7 @@ static void c_draw(void *ctx)
const ncm_pal_t *pa = ncm_palette(g_dark);
int cnt = appstate_cache_count();
if (cnt <= 0) return;
int item = APP_ROW_H, isz = 24;
int item = g_item_h, isz = 24;
int off = meow_listview_scroll(g_list);
int first = off / item, last = (off + r_list.h) / item + 1;
if (last > cnt) last = cnt;
@@ -351,6 +352,7 @@ static void c_page_create(meow_view *v, void *ctx)
(void)v;
((cache_ctrl *)ctx)->view = v;
r_cont = ncm_content_rect();
g_item_h = ncm_row_h(); /* M12:行高按当前字体两行文本块派生(首行标题不被视口顶钳掉) */
r_list = NR(16, r_cont.y + 8, r_cont.w - 32,
r_cont.y + r_cont.h - APP_FOOTER_H - (r_cont.y + 8));
if (r_list.h < 0) r_list.h = 0;
@@ -358,7 +360,7 @@ static void c_page_create(meow_view *v, void *ctx)
g_tb = ncm_topbar_create("已缓存", 1, "清空"); /* 文本动作(动作0)=清空缓存 */
g_list = meow_listview_create(r_list.x, r_list.y, r_list.w, r_list.h, 0,
c_src2, NULL, MEOW_LIST_SEL_NONE);
meow_listview_set_item_h(g_list, APP_ROW_H);
meow_listview_set_item_h(g_list, g_item_h);
meow_ui_activity_pop();
meow_widget_set_z(g_list, 10);
g_bg_id = meow_draw_custom_register(5, r_list.x, r_list.y, r_list.w, r_list.h,
+5 -3
View File
@@ -33,6 +33,7 @@ static ncm_rect_t r_cont, r_list, r_tabs, r_group;
static int g_bg_id, g_draw_id, g_empty_id, g_footer_id;
static volatile int g_load_err;
static int g_tab; /* 0=我创建的 1=我收藏的 */
static int g_item_h = APP_ROW_H; /* 两行列表行高(M12:按字体行高派生,见 ncm_row_h) */
static int g_press_row, g_press_delta, g_dragging, g_drag_last_y;
/* 歌单加载后台 worker(P1:从 UI 线程挪出,gen 代数做完成信号)。 */
@@ -100,7 +101,7 @@ static void home_row_bg(void *ctx)
if (meow_ui_activity_active() != ACT_HOME) return;
if (g_load_err != 0 || g_st.playlist_count == 0) return;
const meow_theme *th = meow_theme_get();
int item = APP_ROW_H;
int item = g_item_h;
int off = meow_listview_scroll(g_list);
int first = off / item, last = (off + r_list.h) / item + 1;
int n = tab_count(g_tab);
@@ -194,7 +195,7 @@ static void home_draw(void *ctx)
/* 歌单行:红 mini-vinyl 缩略 + 按压红条(文本由 listview 画在 z=10)。 */
if (g_load_err == 0 && g_st.playlist_count > 0) {
int item = APP_ROW_H;
int item = g_item_h;
int off = meow_listview_scroll(g_list);
int isz = 24;
int first = off / item, last = (off + r_list.h) / item + 1;
@@ -453,6 +454,7 @@ static void home_page_create(meow_view *v, void *ctx)
((home_ctrl *)ctx)->view = v;
home_retry_reset(); /* M10:离线重试节奏闸(首次进离线立即探测一次) */
r_cont = ncm_content_rect();
g_item_h = ncm_row_h(); /* M12:行高按当前字体两行文本块派生(首行标题不被视口顶钳掉) */
ncm_rect_t pc = NR(16, r_cont.y + 8, r_cont.w - 32, 72);
r_tabs = NR(16, pc.y + pc.h + 8, r_cont.w - 32, 48);
r_group = NR(16, r_tabs.y + r_tabs.h + 4, r_cont.w - 32, 36);
@@ -481,7 +483,7 @@ static void home_page_create(meow_view *v, void *ctx)
meow_tabbar_set_selected(g_tabs, 0);
g_list = meow_listview_create(r_list.x, r_list.y, r_list.w, r_list.h, 0,
home_src2, NULL, MEOW_LIST_SEL_NONE);
meow_listview_set_item_h(g_list, APP_ROW_H);
meow_listview_set_item_h(g_list, g_item_h);
meow_ui_activity_pop();
/* 显式 z:行卡片(5) < listview(10) < tabbar(20) < 红态叠层(60) < topbar(999) */
meow_widget_set_z(g_list, 10);
+9 -7
View File
@@ -24,6 +24,7 @@ static int g_bg_id, g_draw_id, g_empty_id, g_footer_id;
static char g_title[256];
static volatile int g_load_err;
static int g_press_row, g_press_delta, g_dragging, g_drag_last_y;
static int g_item_h = APP_ROW_H; /* 两行列表行高(M12:按字体行高派生,见 ncm_row_h) */
/* 歌单详情加载后台 worker(大歌单可达数十秒,不能占 UI 线程)。
* §17 滑动窗口:worker 三种任务——①首屏(g_ens_first=1):重建 trackIds 索引 + 物化第 0 页
@@ -125,7 +126,7 @@ static void pl_row_bg(void *ctx)
if (meow_ui_activity_active() != ACT_PLAYLIST) return;
if (g_load_err != 0 || g_st.track_ids_count == 0) return;
const meow_theme *th = meow_theme_get();
int item = APP_ROW_H;
int item = g_item_h;
int off = meow_listview_scroll(g_list);
int first = off / item, last = (off + r_list.h) / item + 1;
if (last > g_st.track_ids_count) last = g_st.track_ids_count;
@@ -195,7 +196,7 @@ static void pl_draw(void *ctx)
/* 曲目行:红 mini-vinyl 缩略 + 按压红条(文本由 listview z=10 画) */
if (g_load_err == 0 && g_st.track_ids_count > 0) {
int item = APP_ROW_H, isz = 24;
int item = g_item_h, isz = 24;
int off = meow_listview_scroll(g_list);
int first = off / item, last = (off + r_list.h) / item + 1;
if (last > g_st.track_ids_count) last = g_st.track_ids_count;
@@ -238,8 +239,8 @@ static void pl_draw_footer(void *ctx)
int total = g_st.track_ids_count;
if (total <= 0) return;
/* 视口位置 / 总数(滑窗模型下「已物化数」无用户意义,视口区间更直观) */
int first = meow_listview_scroll(g_list) / APP_ROW_H + 1;
int last = first + r_list.h / APP_ROW_H;
int first = meow_listview_scroll(g_list) / g_item_h + 1;
int last = first + r_list.h / g_item_h;
if (last > total) last = total;
char foot[96];
snprintf(foot, sizeof foot, "%d-%d / %d 首", first, last, total);
@@ -361,8 +362,8 @@ static void pl_maybe_ensure(void)
int total = g_st.track_ids_count;
if (total <= 0) return;
int scroll = meow_listview_scroll(g_list);
int first = scroll / APP_ROW_H;
int last = (scroll + r_list.h) / APP_ROW_H + 1;
int first = scroll / g_item_h;
int last = (scroll + r_list.h) / g_item_h + 1;
if (last > total) last = total;
int paged = g_paged_hi;
if (paged > total) paged = total; /* 防御:索引重建后高水位越界 */
@@ -581,6 +582,7 @@ static void pl_page_create(meow_view *v, void *ctx)
(void)v;
((playlist_ctrl *)ctx)->view = v;
r_cont = ncm_content_rect();
g_item_h = ncm_row_h(); /* M12:行高按当前字体两行文本块派生(首行标题不被视口顶钳掉) */
r_card = NR(16, r_cont.y + 8, r_cont.w - 32, 148);
r_cov = NR(r_card.x + 16, r_card.y + 20, 80, 80);
int by = r_card.y + r_card.h + 12;
@@ -594,7 +596,7 @@ static void pl_page_create(meow_view *v, void *ctx)
g_tb = ncm_topbar_create("歌单", 1, NULL);
g_list = meow_listview_create(r_list.x, r_list.y, r_list.w, r_list.h, 0,
pl_src2, NULL, MEOW_LIST_SEL_NONE);
meow_listview_set_item_h(g_list, APP_ROW_H);
meow_listview_set_item_h(g_list, g_item_h);
meow_ui_activity_pop();
meow_widget_set_z(g_list, 10);
g_bg_id = meow_draw_custom_register(5, r_list.x, r_list.y, r_list.w, r_list.h,
+9 -7
View File
@@ -28,6 +28,7 @@ static ncm_rect_t r_cont, r_list;
static int g_bg_id, g_draw_id, g_empty_id, g_footer_id;
static int g_press_row, g_press_delta, g_dragging, g_drag_last_y;
static int g_last_count, g_last_cur;
static int g_item_h = APP_ROW_H; /* 两行列表行高(M12:按字体行高派生,见 ncm_row_h) */
/* 队列补载(PLAYLIST)后台 worker 参数:目标区间 [lo,hi)(全局下标)。SEARCH 无需补载。 */
static pthread_mutex_t g_qmu = PTHREAD_MUTEX_INITIALIZER;
@@ -92,7 +93,7 @@ static void q_row_bg(void *ctx)
if (cnt <= 0) return;
const meow_theme *th = meow_theme_get();
const ncm_pal_t *pa = ncm_palette(g_dark);
int item = APP_ROW_H;
int item = g_item_h;
int off = meow_listview_scroll(g_list);
int first = off / item, last = (off + r_list.h) / item + 1;
if (last > cnt) last = cnt;
@@ -120,7 +121,7 @@ static void q_draw(void *ctx)
const ncm_pal_t *pa = ncm_palette(g_dark);
int cnt = appstate_queue_count();
if (cnt <= 0) return;
int item = APP_ROW_H, isz = 24;
int item = g_item_h, isz = 24;
int off = meow_listview_scroll(g_list);
int first = off / item, last = (off + r_list.h) / item + 1;
if (last > cnt) last = cnt;
@@ -156,8 +157,8 @@ static void q_draw_footer(void *ctx)
if (meow_ui_activity_active() != ACT_QUEUE) return;
int total = appstate_queue_count();
if (total <= 0) return;
int first = meow_listview_scroll(g_list) / APP_ROW_H + 1;
int last = first + r_list.h / APP_ROW_H;
int first = meow_listview_scroll(g_list) / g_item_h + 1;
int last = first + r_list.h / g_item_h;
if (last > total) last = total;
char foot[96];
snprintf(foot, sizeof foot, "%d-%d / %d 首", first, last, total);
@@ -203,8 +204,8 @@ static void q_maybe_ensure(void)
int total = appstate_queue_count();
if (total <= 0) return;
int scroll = meow_listview_scroll(g_list);
int first = scroll / APP_ROW_H;
int last = (scroll + r_list.h) / APP_ROW_H + 1;
int first = scroll / g_item_h;
int last = (scroll + r_list.h) / g_item_h + 1;
int lo = first - 24, hi = last + 24;
if (lo < 0) lo = 0;
if (hi > total) hi = total;
@@ -325,6 +326,7 @@ static void q_page_create(meow_view *v, void *ctx)
(void)v;
((queue_ctrl *)ctx)->view = v;
r_cont = ncm_content_rect();
g_item_h = ncm_row_h(); /* M12:行高按当前字体两行文本块派生(首行标题不被视口顶钳掉) */
r_list = NR(16, r_cont.y + 8, r_cont.w - 32,
r_cont.y + r_cont.h - APP_FOOTER_H - (r_cont.y + 8));
if (r_list.h < 0) r_list.h = 0;
@@ -332,7 +334,7 @@ static void q_page_create(meow_view *v, void *ctx)
g_tb = ncm_topbar_create("播放列表", 1, NULL);
g_list = meow_listview_create(r_list.x, r_list.y, r_list.w, r_list.h, 0,
q_src2, NULL, MEOW_LIST_SEL_NONE);
meow_listview_set_item_h(g_list, APP_ROW_H);
meow_listview_set_item_h(g_list, g_item_h);
meow_ui_activity_pop();
meow_widget_set_z(g_list, 10);
g_bg_id = meow_draw_custom_register(5, r_list.x, r_list.y, r_list.w, r_list.h,
+4 -2
View File
@@ -33,6 +33,7 @@ static ncm_rect_t r_cont, r_list, r_btn, r_tabs[2];
static int g_empty_id, g_footer_id, g_btn_id, g_tabbar_id;
static int g_press_row, g_press_delta, g_dragging, g_drag_last_y;
static int g_scroll_song, g_scroll_pl; /* 两页签各自滚动位置(§23)*/
static int g_item_h = APP_ROW_H; /* 两行列表行高(M12:按字体行高派生,见 ncm_row_h) */
static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER;
static pthread_t g_thr;
@@ -206,7 +207,7 @@ static void sr_maybe_load_more(void)
int n = appstate_search_count(g_tab);
int cap = (g_tab == 0) ? NCM_SEARCH_SONG_MAX : NCM_SEARCH_PL_MAX;
if (n <= 0 || n >= cap || !appstate_search_has_more(g_tab)) return;
int item = APP_ROW_H;
int item = g_item_h;
int scroll = meow_listview_scroll(g_list);
if (scroll + r_list.h < n * item - item * 2) return; /* 未近底 */
g_req_tab = g_tab; g_req_offset = n; g_req_replace = 0;
@@ -445,6 +446,7 @@ static void sr_page_create(meow_view *v, void *ctx)
(void)v;
((search_ctrl *)ctx)->view = v;
r_cont = ncm_content_rect();
g_item_h = ncm_row_h(); /* M12:行高按当前字体两行文本块派生(首行标题不被视口顶钳掉) */
ncm_rect_t field = NR(16, r_cont.y + 12, r_cont.w - 32, 48);
r_btn = NR(field.x, field.y + field.h + 12, field.w, 44);
int by = r_btn.y + r_btn.h + 8;
@@ -462,7 +464,7 @@ static void sr_page_create(meow_view *v, void *ctx)
sizeof g_st.search_query, MEOW_IME_PINYIN);
g_list = meow_listview_create(r_list.x, r_list.y, r_list.w, r_list.h, 0,
sr_src2, NULL, MEOW_LIST_SEL_NONE);
meow_listview_set_item_h(g_list, APP_ROW_H);
meow_listview_set_item_h(g_list, g_item_h);
meow_ui_activity_pop();
meow_widget_set_z(g_list, 10);
g_btn_id = meow_draw_custom_register(60, r_btn.x, r_btn.y, r_btn.w, r_btn.h,