Merge branch 'feat/sdk-listview-row-height'

This commit is contained in:
AskaEth committed 2026-11-05 20:38:09 +08:00
commit cfcb94b8d2
3 files changed
+291 -13

No files matched your search

+9 -3
View File
@@ -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,
+35 -10
View File
@@ -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);
+247
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
/* --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;
}