diff --git a/patch_v2/sdk/include/meow_ui_widgets.h b/patch_v2/sdk/include/meow_ui_widgets.h index f6088fc..af94af4 100644 --- a/patch_v2/sdk/include/meow_ui_widgets.h +++ b/patch_v2/sdk/include/meow_ui_widgets.h @@ -316,11 +316,16 @@ typedef struct meow_list_item { /* 行数据源回调:idx 有效时填充 out 并返回 1;越界返回 0。每行轻量填充(不做 IO,§5.5)。 */ typedef int (*meow_list2_source)(int idx, meow_list_item *out, void *ctx); -/* 创建:视口(x,y,w,h) + 行数 count + 行数据源 + 选中模式。行高默认 56(可用 set_item_h 改)。 */ +/* 创建:视口(x,y,w,h) + 行数 count + 行数据源 + 选中模式。 + * 行高默认按当前主题字体行高派生(双行 max(56, 2*行高+2) / 单行 max(48, 行高)), + * 20px 字库(行高 30)下双行=62;显式 set_item_h 可覆盖(App 自管卡高时用)。 */ meow_widget *meow_listview_create(int x, int y, int w, int h, int count, meow_list2_source src, void *ctx, meow_list_sel mode); -/* 行高(固定;越界/<=0 忽略)。 */ +/* 行高:显式 px(>0)优先并固定;越界/<=0 忽略(保持字体派生)。返回当前生效行高。 */ int meow_listview_set_item_h(meow_widget *lv, int item); +/* 当前生效行高(显式值,或未显式时按主题字体行高派生的值)。无效控件返回 0。 + * 行几何(draw/hit/max_scroll)全部按此值,App 自绘行装饰用它对齐,勿再硬编码 56。 */ +int meow_listview_item_h(meow_widget *lv); /* 行数(搜索过滤/懒加载/目录切换时动态更新):截断 sel 到新范围 + clamp scroll。 */ void meow_listview_set_count(meow_widget *lv, int count); /* 滚动:set 越界 clamp 到 [0, max_scroll];by 相对滚动(App on_swipe/touch_move 用)。 */ @@ -394,7 +399,8 @@ int meow_listview_set_vsource(meow_widget *lv, meow_listview_count_fn count, * show_icon=1 画左图标/缩略图;show_right=1 画右尾件。返回 0=成功。 */ int meow_listview_set_item_renderer(meow_widget *lv, int lines, int show_icon, int show_right); -/* 行档位默认行高:lines=1→48(单行)、2→56(双行);与 set_item_h(精确 px)互补。 */ +/* 行档位(默认行高口径):lines=1→max(48, 行高)、2→max(56, 2*行高+2),由当前字体行高派生 + * (与 set_item_h 互补:tier 恢复派生默认,set_item_h 设精确 px)。 */ int meow_listview_set_item_tier(meow_widget *lv, int lines); /* 完全自绘:设 draw_item 回调(覆盖默认 renderer);NULL=用默认 renderer。返回 0=成功。 */ int meow_listview_set_item_draw(meow_widget *lv, meow_listview_draw_fn draw, diff --git a/patch_v2/sdk/meow_ui_widget.c b/patch_v2/sdk/meow_ui_widget.c index 2a2bb5b..aca1cf3 100644 --- a/patch_v2/sdk/meow_ui_widget.c +++ b/patch_v2/sdk/meow_ui_widget.c @@ -79,7 +79,7 @@ struct meow_widget { int maxlen; /* W_TEXTFIELD 上限(含 NUL) */ meow_ime_mode ime_mode; /* W_TEXTFIELD IME 模式 */ int value; /* W_PROGRESS:0-100 / -1=不定; W_BADGE:计数 */ - int item_h; /* W_LIST:行高/列宽(lazy),默认 44 */ + int item_h; /* W_LIST:行高/列宽(lazy),默认 44;W_LISTVIEW:显式行高,0=按字体行高派生 */ int scroll; /* W_LIST:滚动偏移(lazy) */ int horizontal; /* W_LIST:1=横向(LazyRow) */ int back_on; /* W_TOPAPPBAR:显示返回箭头 */ @@ -1051,7 +1051,24 @@ int meow_list_set_container(meow_widget *w, int on) * 宿主/设备需图片图标的 App 经 meow_ui_img_load_* 强引用 meow_ui_img.o → 此处自动解析。 */ extern void meow_ui_img_scaled(int x, int y, int w, int h, meow_img *img) __attribute__((weak)); -static int listv_item_h(const meow_widget *w) { return (w->item_h > 0) ? w->item_h : 56; } +/* 行高:显式 meow_listview_set_item_h 优先(App 自管行几何时按自己算出的值传,如 ncm 70); + * 未显式(item_h<=0)→ 按当前主题正文字体行高派生,保证「文本块 ≤ 行高」: + * lines=2:max(56, 2*line_h + MEOW_LISTV_SUB_GAP) (20px 字库行高 30 → 62) + * lines=1:max(48, line_h) + * 旧默认固定 56 在真机 source_han_serif 20px(ascent 24 + descent 6 = 30)下小于双行 + * 文本块 62 → 居中后 ty = 行顶 - 3,首行(行顶==视口顶)标题被 `ty >= vy0` 裁剪守卫整条 + * 跳过,只剩副标题(M12 finding / M15 本源修复)。派生值由主题字库实时算出,draw/hit/ + * max_scroll 三路共用同一函数 → 行几何恒一致;改字库档位/主题后自动跟随。 */ +static int listv_item_h(const meow_widget *w) +{ + if (w->item_h > 0) return w->item_h; + const meow_theme *th = meow_theme_get(); + int line_h = th ? meow_ui_text_height(th->font_body) : 0; + if (w->lines == 1) + return (line_h > 48) ? line_h : 48; + int h = 2 * line_h + MEOW_LISTV_SUB_GAP; + return (h > 56) ? h : 56; +} /* §5.5 虚拟化辅助:当前行数(pool 模式实时查 count_fn;src2 模式用缓存的 w->count)。 */ static int listv_count(const meow_widget *w) @@ -1203,7 +1220,7 @@ meow_widget *meow_listview_create(int x, int y, int w, int h, int count, t->owner = meow_ui_activity_active(); t->state = MEOW_WS_ENABLED; t->count = count > 0 ? count : 0; t->src2 = src; t->ctx = ctx; - t->sel = -1; t->item_h = 56; t->scroll = 0; t->swipe_idx = -1; + t->sel = -1; t->item_h = 0; t->scroll = 0; t->swipe_idx = -1; /* item_h=0 → 按字体行高派生(见 listv_item_h) */ t->sel_mode = (mode >= MEOW_LIST_SEL_NONE && mode <= MEOW_LIST_SEL_MULTI) ? mode : MEOW_LIST_SEL_NONE; t->lines = 2; t->show_icon = 1; t->show_right = 1; /* 默认双行富 item + 图标 + 右尾件(src2 布局不变) */ t->container = 1; /* M6.37 P1:SDK 默认列表走 MD3 容器模式——行=surfaceContainer 圆角卡 + 空隙 + 去分隔线 + 卡内 tonal 选中, @@ -1225,7 +1242,12 @@ int meow_listview_set_item_h(meow_widget *w, int item) listv_ensure_pool(w); /* 行高变小 → 可见行增多 → 池容量随之调整 */ meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); } - return w->item_h; + return listv_item_h(w); /* 返回当前生效行高(显式值或字体派生值) */ +} +int meow_listview_item_h(meow_widget *w) +{ + if (!w || w->type != W_LISTVIEW) return 0; + return listv_item_h(w); } void meow_listview_set_count(meow_widget *w, int count) { @@ -1398,7 +1420,7 @@ int meow_listview_set_item_renderer(meow_widget *w, int lines, int show_icon, if (lines < 1) lines = 1; if (lines > 2) lines = 2; w->lines = lines; w->show_icon = show_icon ? 1 : 0; w->show_right = show_right ? 1 : 0; - if (w->item_h <= 0) w->item_h = (lines == 1) ? 48 : 56; /* 未显式定行高 → 档位默认 */ + /* 未显式定行高(item_h<=0)→ 保持派生模式(按 lines 档位从字体行高算),不写死像素 */ listv_ensure_pool(w); /* 行档位可能变化 → 确保池容量 */ meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); return 0; @@ -1410,7 +1432,7 @@ int meow_listview_set_item_tier(meow_widget *w, int lines) if (lines < 1) lines = 1; if (lines > 2) lines = 2; w->lines = lines; - w->item_h = (lines == 1) ? 48 : 56; /* 档位默认高(可随后 set_item_h 覆盖) */ + w->item_h = 0; /* 档位=字体派生默认(单行 max(48,行高) / 双行 max(56,2*行高+2));set_item_h 可覆盖 */ listv_ensure_pool(w); meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); return 0; @@ -2007,8 +2029,9 @@ void meow_widget_draw(meow_widget *w) } } } else { - /* ① src2 模式(既有):每帧按需取行(meow_list2_source),行为不变。 - * 行 = 图标(左) + 标题 + 副标题(次要色) + 右侧操作区;选中行 = primaryContainer。 */ + /* ① src2 模式(既有):每帧按需取行(meow_list2_source),虚拟化行为不变。 + * 行 = 图标(左) + 标题 + 副标题(次要色) + 右侧操作区;选中行 = primaryContainer。 + * lines=1(set_item_renderer/tier 单行档位)时不画副标题,与 pool 分支同口径。 */ for (int i = first; i < last; i++) { meow_list_item it; memset(&it, 0, sizeof it); @@ -2055,9 +2078,11 @@ void meow_widget_draw(meow_widget *w) if (icy + isz >= vy0 && icy < vy1) /* 图标不越视口边界才画(防泄漏) */ listv_draw_icon(th, rlx + pad, icy, it.icon_kind, it.icon, iccol); - /* 标题 + 副标题(两行居中;无副标题=单行居中;越界不画防泄漏) */ + /* 标题 + 副标题(lines=2 有副标题→两行居中;lines=1 或无副标题→单行居中;越界不画防泄漏)。 + * lines 档位与 pool 分支同口径:tier/renderer 设单行时明确不画副标题, + * 否则 48px 单行档位里塞 62px 文本块 → 首行标题被裁剪守卫跳过。 */ int ty; - if (it.subtitle && it.subtitle[0]) { + if (w->lines == 2 && it.subtitle && it.subtitle[0]) { ty = ry + (item - (2 * thh + MEOW_LISTV_SUB_GAP)) / 2; if (it.title && ty >= vy0 && ty + thh <= vy1) listv_text_trunc(rtx, ty, it.title, text_maxw, th->font_body, textcol); diff --git a/patch_v2/sdk/test_meow_ui_listview_rowh.c b/patch_v2/sdk/test_meow_ui_listview_rowh.c new file mode 100644 index 0000000..b0b688f --- /dev/null +++ b/patch_v2/sdk/test_meow_ui_listview_rowh.c @@ -0,0 +1,247 @@ +/* test_meow_ui_listview_rowh.c — 宿主回归(M15):listview 默认行高必须由字体行高派生, + * 首行(scroll=0)标题必须真正画出来。 + * + * 根因(M12 finding):双行 renderer 把「标题+副标题」文本块 + * block = 2 * meow_ui_text_height(font_body) + MEOW_LISTV_SUB_GAP(2) + * 在行内垂直居中(ty = 行顶 + (item - block)/2),且文本只在 `ty >= 视口顶` 时才画。 + * 真机 source_han_serif 20px(ascent 24 + descent 6)行高 = 30 → block = 62; + * 旧 SDK 默认行高固定 56 < 62 → ty = 行顶 - 3,首行(行顶 == 视口顶)标题被整条跳过, + * 只剩副标题 → 所有 App 的两行列表首行标题消失。 + * + * 宿主 5x7 字体行高是 14,照原样永远复现不了。故用链接期 `-Wl,--wrap=meow_ui_text_height` + * 把行高替换成真机实测的 30,驱动同一份 SDK 代码(同 M12 app/test/test_first_row_title_render.c): + * A 行高 30 + 显式 item_h=56 → 首行标题带 0 像素(bug 形态,防回归哨兵) + * B 行高 30 + 不设 item_h → 派生 62 = block,首行标题/副标题都有像素 + * C 行高 30 + 显式 item_h=70 → ncm 语义(App 自管卡高)原样生效,标题可见 + * 断言全部读 back_buf 真实像素(region_has),非目检。 + * + * 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global): + * gcc -o test_meow_ui_listview_rowh test_meow_ui_listview_rowh.c test_host_stub.c \ + * meow_ui_gfx.c meow_ui_time.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 meow_ui_nav.c \ + * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm \ + * -Wl,--wrap=meow_ui_text_height + * 运行:./test_meow_ui_listview_rowh + */ +#include "meow_ui.h" +#include "meow_ui_internal.h" /* meow_ui_back_buf/pitch/bpp:真实像素断言 */ +#include +#include +#include + +/* --wrap 替换实现:真机 source_han_serif 20px 实测行高(font.idx ascent+descent)。 */ +int __wrap_meow_ui_text_height(int size) +{ + (void)size; + return 30; +} + +#define LH 30 /* 真机正文行高 */ +#define BLOCK (2 * LH + 2) /* 双行文本块 = 62(与 MEOW_LISTV_SUB_GAP 同口径) */ +#define LV_X 24 +#define LV_Y 40 +#define LV_W 432 +#define LV_H 180 +#define NR 5 /* 行数 */ + +static int g_pass = 0, g_fail = 0; +#define CHECK(cond, ...) do { \ + if (cond) { g_pass++; } \ + else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \ +} while (0) + +/* ---- 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); +} +static int region_has(int x, int y, int w, int h, int r, int g, int b) +{ + int cnt = 0; + for (int dy = 0; dy < h; dy++) + for (int dx = 0; dx < w; dx++) + if (px_near(x + dx, y + dy, r, g, b)) cnt++; + return cnt; +} +#define UNPK(c, rr, gg, bb) do { (rr) = ((c) >> 16) & 0xFF; (gg) = ((c) >> 8) & 0xFF; (bb) = (c) & 0xFF; } while (0) + +/* ---- 数据源:src2(每帧取行)与 pool(count/bind)各一套,行内容相同 ---- */ +static const char *const k_title[NR] = { "T1", "T2", "T3", "T4", "T5" }; +static const char *const k_sub[NR] = { "S1", "S2", "S3", "S4", "S5" }; + +static int src2_cb(int idx, meow_list_item *out, void *ctx) +{ + (void)ctx; + if (idx < 0 || idx >= NR) return 0; + memset(out, 0, sizeof *out); + out->title = k_title[idx]; + out->subtitle = k_sub[idx]; + out->right = MEOW_LIST_RIGHT_NONE; + out->enabled = 1; + return 1; +} +static int pool_count(void *ctx) { (void)ctx; return NR; } +static void pool_bind(int idx, meow_lv_item *it, void *ctx) +{ + (void)ctx; + memset(it, 0, sizeof *it); + if (idx < 0 || idx >= NR) return; + it->title = k_title[idx]; + it->subtitle = k_sub[idx]; + it->enabled = 1; +} + +/* 造一个 src2 双行列表(扁平基线,便于按颜色断言行内文本带)。不设 item_h → 走派生。 */ +static meow_widget *make_lv(void) +{ + meow_widget *lv = meow_listview_create(LV_X, LV_Y, LV_W, LV_H, NR, + src2_cb, NULL, MEOW_LIST_SEL_NONE); + if (!lv) return NULL; + meow_listview_set_container(lv, 0); + meow_listview_set_item_renderer(lv, 2, 0, 0); /* 双行、无图标、无尾件 → text_x = LV_X+16 */ + return lv; +} + +int main(void) +{ + meow_ui_opts o = { 480, 720 }; + if (meow_ui_init(&o) != 0) { printf("init failed\n"); return 1; } + const meow_theme *th = meow_theme_get(); + int os_r, os_g, os_b, osv_r, osv_g, osv_b; + UNPK(th->onSurface, os_r, os_g, os_b); + UNPK(th->onSurfaceVariant, osv_r, osv_g, osv_b); + const int text_x = LV_X + 16; /* show_icon=0, container=0 */ + + /* ---- T0 --wrap 生效:宿主行高被替换为真机 30 ---- */ + CHECK(meow_ui_text_height(th->font_body) == LH, "wrap line_h=%d (expect %d)", + meow_ui_text_height(th->font_body), LH); + + /* ---- T1 默认(不设 item_h):行高按字体行高派生,首行标题可见 ---- */ + { + meow_widget *lv = make_lv(); + CHECK(lv != NULL, "create"); + CHECK(meow_listview_item_h(lv) == BLOCK, "derived item_h=%d (expect %d = 2*%d+2)", + meow_listview_item_h(lv), BLOCK, LH); + CHECK(meow_listview_item_h(lv) >= 2 * LH + 2, "item_h >= 2*line_h + gap"); + CHECK(meow_listview_scroll(lv) == 0, "scroll=0"); + CHECK(meow_listview_max_scroll(lv) == NR * BLOCK - LV_H, "max_scroll=%d", + meow_listview_max_scroll(lv)); + CHECK(meow_listview_hit(lv, LV_X + 10, LV_Y) == 0, "hit row0"); + CHECK(meow_listview_hit(lv, LV_X + 10, LV_Y + BLOCK) == 1, "hit row1 (item_h=%d)", + meow_listview_item_h(lv)); + + int ty = LV_Y + (BLOCK - BLOCK) / 2; /* 62-62 → 首行标题顶 == 视口顶 */ + int sy = ty + LH + 2; + meow_ui_clear(th->bg); + meow_widget_draw(lv); + CHECK(region_has(text_x, ty, 60, LH, os_r, os_g, os_b) > 2, + "首行标题可见(scroll=0, ty=%d)", ty); + CHECK(region_has(text_x, sy, 60, LH, osv_r, osv_g, osv_b) > 2, + "首行副标题可见(sy=%d)", sy); + CHECK(region_has(text_x, LV_Y - 20, 60, 20, os_r, os_g, os_b) == 0, + "视口上方无标题泄漏"); + meow_widget_free(lv); + } + + /* ---- T2 裸 create(不调 renderer):默认 lines=2 同样派生 ---- */ + { + meow_widget *lv = meow_listview_create(LV_X, LV_Y, LV_W, LV_H, NR, + src2_cb, NULL, MEOW_LIST_SEL_NONE); + CHECK(lv != NULL && meow_listview_item_h(lv) == BLOCK, "bare create item_h=%d", + lv ? meow_listview_item_h(lv) : -1); + /* set_item_h(<=0) 忽略:仍返回派生值 */ + CHECK(meow_listview_set_item_h(lv, 0) == BLOCK, "set_item_h(0) keeps derived"); + CHECK(meow_listview_item_h(lv) == BLOCK, "item_h unchanged after set_item_h(0)"); + meow_widget_free(lv); + } + + /* ---- T3 显式 set_item_h 优先(ncm 语义:App 按自身卡高传 70)---- */ + { + meow_widget *lv = make_lv(); + CHECK(meow_listview_set_item_h(lv, 70) == 70, "explicit item_h returns 70"); + CHECK(meow_listview_item_h(lv) == 70, "explicit item_h=70 wins over derived %d", BLOCK); + CHECK(meow_listview_max_scroll(lv) == NR * 70 - LV_H, "max_scroll uses 70"); + int ty = LV_Y + (70 - BLOCK) / 2; /* 4 */ + int sy = ty + LH + 2; + meow_ui_clear(th->bg); + meow_widget_draw(lv); + CHECK(region_has(text_x, ty, 60, LH, os_r, os_g, os_b) > 2, "显式 70 首行标题可见"); + CHECK(region_has(text_x, sy, 60, LH, osv_r, osv_g, osv_b) > 2, "显式 70 首行副标题可见"); + meow_widget_free(lv); + } + + /* ---- T4 档位:tier 恢复派生默认(2→62 / 1→48),单行不画副标题 ---- */ + { + meow_widget *lv = make_lv(); + CHECK(meow_listview_set_item_h(lv, 70) == 70, "pre-set explicit 70"); + CHECK(meow_listview_set_item_tier(lv, 2) == 0, "tier(2)"); + CHECK(meow_listview_item_h(lv) == BLOCK, "tier(2) 派生 item_h=%d", meow_listview_item_h(lv)); + CHECK(meow_listview_set_item_tier(lv, 1) == 0, "tier(1)"); + CHECK(meow_listview_item_h(lv) == 48, "tier(1) item_h=%d (expect 48)", + meow_listview_item_h(lv)); + int ty = LV_Y + (48 - LH) / 2; /* 9 */ + meow_ui_clear(th->bg); + meow_widget_draw(lv); + CHECK(region_has(text_x, ty, 60, LH, os_r, os_g, os_b) > 2, "单行档位标题可见"); + CHECK(region_has(text_x, LV_Y + 41, 60, 6, osv_r, osv_g, osv_b) == 0, + "单行档位不画副标题"); + meow_widget_free(lv); + } + + /* ---- T5 pool 模式(set_vsource + 默认 renderer):派生行高 + 首行标题可见 ---- */ + { + meow_widget *lv = meow_listview_create(LV_X, LV_Y, LV_W, LV_H, 0, + NULL, NULL, MEOW_LIST_SEL_NONE); + meow_listview_set_container(lv, 0); + CHECK(meow_listview_set_vsource(lv, pool_count, pool_bind, NULL) == 0, "vsource"); + CHECK(meow_listview_set_item_renderer(lv, 2, 0, 0) == 0, "renderer(2,0,0)"); + CHECK(meow_listview_item_h(lv) == BLOCK, "pool item_h=%d", meow_listview_item_h(lv)); + int ty = LV_Y + (BLOCK - BLOCK) / 2; + int sy = ty + LH + 2; + meow_ui_clear(th->bg); + meow_widget_draw(lv); + CHECK(region_has(text_x, ty, 60, LH, os_r, os_g, os_b) > 2, "pool 首行标题可见"); + CHECK(region_has(text_x, sy, 60, LH, osv_r, osv_g, osv_b) > 2, "pool 首行副标题可见"); + meow_widget_free(lv); + } + + /* ---- T6 旧行为哨兵:显式 56(< block 62)→ 首行标题仍被裁剪守卫跳过 ---- + * 记录 bug 形态,证明上面的像素断言确实能区分「画出」与「跳过」; + * 派生默认生效后这条路径只剩「App 明确要 56」时才会走到。 */ + { + meow_widget *lv = make_lv(); + CHECK(meow_listview_set_item_h(lv, 56) == 56, "explicit 56"); + CHECK(meow_listview_item_h(lv) == 56, "explicit 56 stays 56"); + meow_ui_clear(th->bg); + meow_widget_draw(lv); + int ty = LV_Y + (56 - BLOCK) / 2; /* -3 → 行顶上方 */ + int sy = ty + LH + 2; /* 29 */ + CHECK(region_has(text_x, LV_Y, 60, LH, os_r, os_g, os_b) == 0, + "旧 56 行高:首行标题被跳过(bug 形态)"); + CHECK(region_has(text_x, sy, 60, LH, osv_r, osv_g, osv_b) > 2, + "旧 56 行高:首行副标题仍画(症状「只有副标题」)"); + meow_widget_free(lv); + } + + printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail); + return g_fail ? 1 : 0; +}