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

+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,