From 2d974bed6219e3c0f38227100d24844196318a29 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Wed, 2 Sep 2026 01:36:48 +0800 Subject: [PATCH] =?UTF-8?q?sdk(ui):=20W=5FLISTVIEW=20=E8=99=9A=E6=8B=9F?= =?UTF-8?q?=E5=8C=96=E5=B1=82=20=E2=80=94=20=E5=9B=BA=E5=AE=9A=20item=20?= =?UTF-8?q?=E6=B1=A0=20+=20=E5=A4=9A=E8=A1=8C=E5=AF=8C=20item=20+=20?= =?UTF-8?q?=E5=AE=8C=E5=85=A8=E8=87=AA=E7=BB=98=20(=C2=A75.5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在既有 W_LISTVIEW (src2 每帧取行) 之上叠加虚拟化层 (API 追加式, 旧 API 一字不改): - 数据源 count_fn + bind_fn (index, meow_lv_item*, ctx); count 每帧实时查 (任意长度≥65535, 不常驻 item 对象); bind 仅在行进入视口时被调一次 - 固定 item 池: 视口行数+2 静态槽 (有界 ≤96), 回滚不重绑 (同 index 命中同槽); 整表扫过后 wrap 回收合法但每次 draw 有界 (≤视口窗口) - 多行富 item: 主标题/副标题/左图标·缩略图/右尾件; 行高 set_item_h(px) 或 set_item_tier(1→48/2→56); 默认 renderer 主题语义色绘制; set_item_draw 完全自绘; section 段头占位行(非粘性); 空态占位 - 集成: tap_at→meow_ui_focus_acquire (§6.4); meow_scroll_wrap(lv,1) track+fling+snap(行边界); 选中/按压态走主题; enabled=0 变淡不响应 - 兼容: src2==NULL 走既有路径; W_LIST/W_LISTVIEW 单行用法零改动 验证(宿主): 新测试 test_meow_ui_listvirt T1-T5 全 44 PASS (10000 项滚动窗口内容正确/hit 映射/ bind 有界=count/回滚不重绑/多行渲染/截断/图标占位/段头/空态/自绘/焦点/fling 吸附); 全量回归 widgets/dispatch/box/anim/scroll/ime/pinyin/overlay/notif/sdk_input_keys/p1p2p3 全 PASS; 交叉 zig-mipsel -Wall -Wextra 全 SDK TU 0 warning; meow_ui.a 新 md5=2a2d8000b95dd211d09dc736d5538ad0 文档: MEOW_APP_SDK.md §5.5 同步虚拟化层(API 一览/池语义/集成/回归). --- include/meow_ui_widgets.h | 59 ++++++ meow_ui_widget.c | 395 +++++++++++++++++++++++++++++++------- test_meow_ui_listvirt.c | 365 +++++++++++++++++++++++++++++++++++ 3 files changed, 751 insertions(+), 68 deletions(-) create mode 100644 test_meow_ui_listvirt.c diff --git a/include/meow_ui_widgets.h b/include/meow_ui_widgets.h index b78d677..90c6faf 100644 --- a/include/meow_ui_widgets.h +++ b/include/meow_ui_widgets.h @@ -293,6 +293,65 @@ void meow_listview_set_on_tap(meow_widget *lv, meow_listview_tap_fn cb, void *ct /* 命中行:置选中 + 回调(若设)+ 返回 idx;未命中返回 -1。 */ int meow_listview_tap_at(meow_widget *lv, int x, int y); +/* ============================================================ */ +/* §5.5 虚拟化列表(W_LISTVIEW 扩展层,2026-09-02):固定 item 池 + 多行富 item + 完全自绘 + * ------------------------------------------------------------ + * 在既有 W_LISTVIEW(src2 每帧按需取行,行为不变)之上做**虚拟化层**(API 追加式,旧 API 保留): + * - 数据源:count_fn() 返回总行数(任意长度,≥65535 不常驻 item 对象); + * bind_fn(index, item, ctx) 仅在行「进入视口」(绑定到固定池槽)时调用一次; + * - 固定 item 池:池容量 = 视口行数 + 2 静态槽(有界,见实现 MEOW_LV_POOL_MAX); + * 行离开视口不销毁,回滚不重绑(同一 index 命中同一槽 → bind 不重调); + * - 滚动只改 viewport 偏移(复用既有 set_scroll/max_scroll),不重绑未进视口的项; + * - 多行富 item:主标题+副标题(第二行小字)+左图标/缩略图+右尾件(chevron/开关/复选/⋯); + * 行高可配(set_item_h 精确 px / set_item_tier 单/双行档位默认高); + * - 默认 renderer 用主题语义色绘制富 item;App 也可经 set_item_draw 完全自绘。 + * 段头:bind 回调把 item->section=1(title=段名)→ 段头占位行(非粘性),默认 renderer 特殊绘制。 + * 空态:set_empty_text 设置文案,count==0 时视口内居中画出。 + * 兼容:既有 meow_listview_*(src2 模式)全部保留不动;本层为叠加,src2==NULL 时走既有路径。 */ + +/* 虚拟化行 item(bind 回调填充;SDK 固定池持有;不常驻 src2 那样的全量行对象)。 */ +typedef struct meow_lv_item { + const char *title; /* 主标题(必填,NULL 视为空行) */ + const char *subtitle; /* 副标题(第二行小字),可空 */ + const meow_img *icon; /* 可选:左图标/缩略图(icon_kind==MEOW_LIST_ICON_IMG 时用) */ + int icon_kind; /* MEOW_LIST_ICON_*(NONE=无图标) */ + int right; /* MEOW_LIST_RIGHT_* 右尾件 */ + int on; /* right==SWITCH 时开关态(0/1) */ + int enabled; /* 1=可交互;0=变淡不响应 */ + int section; /* 1=段头占位行(title=段名;默认 renderer 特殊绘制,非粘性) */ + void *user; /* App 私有(跨 bind 保存业务态/图/index;SDK 不碰) */ +} meow_lv_item; + +/* 虚拟化数据源:count 返回当前行数(>=0,任意长);bind 在行进入视口时被调一次填充 item。 + * 二者均不常驻 item 对象;count 每帧实时查(支持搜索过滤/懒加载动态长度)。 */ +typedef int (*meow_listview_count_fn)(void *ctx); +typedef void (*meow_listview_bind_fn)(int index, meow_lv_item *item, void *ctx); + +/* 完全自绘回调(覆盖默认 renderer):idx=行号;x,y,w,h=行视口矩形(已裁剪到列表视口); + * state=MEOW_LISTVIEW_ST_* 状态位(选中/按压/禁用组合)。App 用 §3 绘制 API 自绘整行。 */ +typedef void (*meow_listview_draw_fn)(int idx, int x, int y, int w, int h, + int state, void *ctx); +/* state 位(set_item_draw 的 draw 回调收到;选中/按压/禁用组合,供自绘整行)。 */ +#define MEOW_LISTVIEW_ST_SELECTED (1u<<0) +#define MEOW_LISTVIEW_ST_PRESSED (1u<<1) +#define MEOW_LISTVIEW_ST_DISABLED (1u<<2) + +/* 挂虚拟化数据源:切到 pool 模式(count_fn/bind_fn 均非 NULL 才生效;否则回退 src2 模式)。 + * 返回 0=成功;NULL 回调/无效=负值。挂虚拟源后既有 sel/滚动/命中/焦点照常。 */ +int meow_listview_set_vsource(meow_widget *lv, meow_listview_count_fn count, + meow_listview_bind_fn bind, void *ctx); +/* 默认 renderer 配置:lines=1/2(单行/双行档位,影响副标题显示与行高默认); + * 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)互补。 */ +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, + void *ctx); +/* 空态占位:count==0 时视口内居中画 empty_text(NULL 不画)。返回 0=成功。 */ +int meow_listview_set_empty_text(meow_widget *lv, const char *text); + #ifdef __cplusplus } #endif diff --git a/meow_ui_widget.c b/meow_ui_widget.c index a30419f..88c25a2 100644 --- a/meow_ui_widget.c +++ b/meow_ui_widget.c @@ -90,6 +90,20 @@ struct meow_widget { int sel_bits_n; /* 位图字数 */ meow_listview_tap_fn lv_tap_cb; /* 行点击回调(idx, op) */ void *lv_tap_ctx; /* 回调 ctx */ + + /* §5.5 虚拟化层(W_LISTVIEW 扩展:固定 item 池 + 多行渲染器 + 完全自绘 + 空态。 + * calloc 归零,安全;vs_count/vs_bind 均非 NULL 才进入 pool 模式,src2 模式不受影响。) */ + meow_listview_count_fn vs_count; /* 虚拟化总数回调(NULL=src2 模式) */ + meow_listview_bind_fn vs_bind; /* 虚拟化 bind 回调(NULL=src2 模式) */ + meow_listview_draw_fn vs_draw; /* 完全自绘回调(非 NULL 覆盖默认 renderer) */ + void *vs_ctx; /* 上述回调的 ctx */ + meow_lv_item *vs_pool; /* 固定 item 池(槽数 vs_pool_n) */ + int vs_pool_n; /* 池容量(视口行数 + 2,静态有界) */ + int *vs_bound; /* 每槽绑定的行号(-1=空闲;pool 索引 mod 映射) */ + int lines; /* 行档位 1/2(默认 2=双行) */ + int show_icon; /* 1=画左图标/缩略图(默认 1) */ + int show_right;/* 1=画右尾件(默认 1) */ + char empty_text[64]; /* 空态占位文案(首字符 0=不画) */ }; /* ---- §5.8 batch1 内部辅助:opts 取值(opts_used==0 时按控件内建默认)---- @@ -338,6 +352,8 @@ void meow_widget_free(meow_widget *w) meow_draw_widget_unregister(w); /* §6.2:绘制物注册表自动注销(stale id 失效勿用) */ live_btn_unregister(w); if (w->sel_bits) { free(w->sel_bits); w->sel_bits = NULL; } /* W_LISTVIEW 多选位图 */ + if (w->vs_pool) { free(w->vs_pool); w->vs_pool = NULL; } /* W_LISTVIEW 虚拟化池 */ + if (w->vs_bound) { free(w->vs_bound); w->vs_bound = NULL; } free(w); } @@ -923,6 +939,58 @@ extern void meow_ui_img_scaled(int x, int y, int w, int h, meow_img *img) __attr static int listv_item_h(const meow_widget *w) { return (w->item_h > 0) ? w->item_h : 56; } +/* §5.5 虚拟化辅助:当前行数(pool 模式实时查 count_fn;src2 模式用缓存的 w->count)。 */ +static int listv_count(const meow_widget *w) +{ + if (!w) return 0; + if (w->vs_count) { int n = w->vs_count(w->vs_ctx); return n < 0 ? 0 : n; } + return w->count; +} + +/* 池容量估算:视口行数 + 2 静态槽(按最小行高 MEOW_LV_MIN_ITEM_H 估算,静态有界)。 + * 真实列表行高远大于最小行高,故 `viewport/item_h + 1 <= cap` 恒成立 → 无槽冲突重绑。 */ +#define MEOW_LV_MIN_ITEM_H 8 /* 池容量估算用的最小行高(真实列表远大于此) */ +#define MEOW_LV_POOL_MAX 96 /* 池容量上限(固定;保证静态有界) */ +static int listv_pool_cap(const meow_widget *w) +{ + int cap = (w->h > 0) ? (w->h / MEOW_LV_MIN_ITEM_H + 2) : 8; + if (cap < 8) cap = 8; + if (cap > MEOW_LV_POOL_MAX) cap = MEOW_LV_POOL_MAX; + return cap; +} +/* 确保池容量 ≥ cap(有界扩张;pool 模式调用)。返回 0=成功;-1=分配失败(仍可用,槽冲突时防御性重绑)。 */ +static int listv_ensure_pool(meow_widget *w) +{ + if (!w->vs_count || !w->vs_bind) return 0; + int cap = listv_pool_cap(w); + if (w->vs_pool && w->vs_pool_n >= cap) return 0; + /* 重分配(有界:cap ≤ MEOW_LV_POOL_MAX,与 item 总数无关)。 */ + meow_lv_item *np = (meow_lv_item *)calloc((size_t)cap, sizeof *np); + int *nb = (int *)calloc((size_t)cap, sizeof *nb); + if (!np || !nb) { free(np); free(nb); return -1; } + if (w->vs_pool && w->vs_pool_n > 0) { + memcpy(np, w->vs_pool, (size_t)(w->vs_pool_n < cap ? w->vs_pool_n : cap) * sizeof *np); + memcpy(nb, w->vs_bound, (size_t)(w->vs_pool_n < cap ? w->vs_pool_n : cap) * sizeof *nb); + } + for (int i = 0; i < cap; i++) nb[i] = -1; /* 全新/重分配:全部槽空闲 → 下一帧按需绑定 */ + free(w->vs_pool); free(w->vs_bound); + w->vs_pool = np; w->vs_bound = nb; w->vs_pool_n = cap; + return 0; +} +/* 取 index 对应的池槽:pool 槽按 index mod cap 映射(回滚命中同槽 → 不重绑)。 + * 槽绑定的 index 不匹配 → 防御性重新 bind(保证内容正确,即使池不足)。返回槽指针。 */ +static meow_lv_item *listv_slot(meow_widget *w, int index) +{ + if (!w->vs_pool || w->vs_pool_n <= 0) return NULL; + int s = (int)((unsigned)index % (unsigned)w->vs_pool_n); + if (w->vs_bound[s] != index) { + memset(&w->vs_pool[s], 0, sizeof w->vs_pool[s]); + w->vs_bind(index, &w->vs_pool[s], w->vs_ctx); + w->vs_bound[s] = index; + } + return &w->vs_pool[s]; +} + /* 文本截断:超出 maxw 加 "..."(复用 browser_text_trunc 的逐字符宽度累加)。 */ static void listv_text_trunc(int x, int y, const char *s, int maxw, int size, uint32_t col) { @@ -1023,6 +1091,7 @@ meow_widget *meow_listview_create(int x, int y, int w, int h, int count, t->count = count > 0 ? count : 0; t->src2 = src; t->ctx = ctx; t->sel = -1; t->item_h = 56; t->scroll = 0; 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 布局不变) */ if (t->sel_mode == MEOW_LIST_SEL_MULTI && t->count > 0) { t->sel_bits_n = (t->count + 31) / 32; t->sel_bits = (uint32_t *)calloc((size_t)t->sel_bits_n, sizeof(uint32_t)); @@ -1034,7 +1103,11 @@ meow_widget *meow_listview_create(int x, int y, int w, int h, int count, int meow_listview_set_item_h(meow_widget *w, int item) { if (!w || w->type != W_LISTVIEW) return -1; - if (item > 0) { w->item_h = item; meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); } + if (item > 0) { + w->item_h = item; + listv_ensure_pool(w); /* 行高变小 → 可见行增多 → 池容量随之调整 */ + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); + } return w->item_h; } void meow_listview_set_count(meow_widget *w, int count) @@ -1043,10 +1116,11 @@ void meow_listview_set_count(meow_widget *w, int count) if (count < 0) count = 0; if (count == w->count) return; w->count = count; - if (w->sel >= w->count) w->sel = -1; - if (w->sel_mode == MEOW_LIST_SEL_MULTI) { /* 重分配多选位图 */ + int n = listv_count(w); /* pool 模式忽略 set_count(count 实时查) */ + if (w->sel >= n) w->sel = -1; + if (w->sel_mode == MEOW_LIST_SEL_MULTI && !w->vs_count) { /* 重分配多选位图(src2 模式) */ free(w->sel_bits); w->sel_bits = NULL; w->sel_bits_n = 0; - int need = (count + 31) / 32; + int need = (n + 31) / 32; if (need > 0) { w->sel_bits_n = need; w->sel_bits = (uint32_t *)calloc((size_t)need, sizeof(uint32_t)); @@ -1060,8 +1134,9 @@ void meow_listview_set_count(meow_widget *w, int count) int meow_listview_max_scroll(meow_widget *w) { if (!w || w->type != W_LISTVIEW) return 0; - int ms = w->count * listv_item_h(w) - w->h; - return ms < 0 ? 0 : ms; + long long ms = (long long)listv_count(w) * listv_item_h(w) - w->h; + if (ms < 0) return 0; + return ms > 0x7FFFFFFF ? 0x7FFFFFFF : (int)ms; } void meow_listview_set_scroll(meow_widget *w, int offset) @@ -1081,8 +1156,9 @@ void meow_listview_set_sel(meow_widget *w, int idx) { if (!w || w->type != W_LISTVIEW) return; if (w->sel_mode == MEOW_LIST_SEL_NONE) return; - if (w->count <= 0) { w->sel = -1; return; } - if (idx >= w->count) idx = w->count - 1; /* 越界 clamp(§5.5 首/末行钳制) */ + int n = listv_count(w); + if (n <= 0) { w->sel = -1; return; } + if (idx >= n) idx = n - 1; /* 越界 clamp(§5.5 首/末行钳制) */ if (w->sel_mode == MEOW_LIST_SEL_SINGLE) { if (idx == w->sel) return; w->sel = idx; /* idx<0 = 清除 */ @@ -1103,9 +1179,9 @@ int meow_listview_get_sel(meow_widget *w, int *out, int max) { if (!w || w->type != W_LISTVIEW) return 0; if (!out || max <= 0) return 0; - int n = 0; + int n = 0, total = listv_count(w); if (w->sel_mode == MEOW_LIST_SEL_MULTI) { - for (int i = 0; i < w->count && n < max; i++) + for (int i = 0; i < total && n < max; i++) if (w->sel_bits && (w->sel_bits[i >> 5] & (1u << (i & 31)))) out[n++] = i; } else if (w->sel >= 0 && n < max) { out[n++] = w->sel; @@ -1116,8 +1192,8 @@ int meow_listview_sel_count(meow_widget *w) { if (!w || w->type != W_LISTVIEW) return 0; if (w->sel_mode == MEOW_LIST_SEL_MULTI) { - int n = 0; - for (int i = 0; i < w->count; i++) + int n = 0, total = listv_count(w); + for (int i = 0; i < total; i++) if (w->sel_bits && (w->sel_bits[i >> 5] & (1u << (i & 31)))) n++; return n; } @@ -1131,12 +1207,18 @@ int meow_listview_hit(meow_widget *w, int x, int y) if (x < w->x || x >= w->x + w->w || y < w->y || y >= w->y + w->h) return -1; int item = listv_item_h(w); int idx = (y - w->y + w->scroll) / item; - if (idx < 0 || idx >= w->count) return -1; + int n = listv_count(w); + if (idx < 0 || idx >= n) return -1; if (w->src2) { /* 禁用行不响应(§5.5) */ meow_list_item it; memset(&it, 0, sizeof it); if (!w->src2(idx, &it, w->ctx)) return -1; if (!it.enabled) return -1; + } else if (w->vs_bind) { /* pool 模式:bind 临时查 enabled */ + meow_lv_item it; + memset(&it, 0, sizeof it); + w->vs_bind(idx, &it, w->vs_ctx); + if (!it.enabled) return -1; } return idx; } @@ -1151,6 +1233,11 @@ int meow_listview_hit_op(meow_widget *w, int x, int y) memset(&it, 0, sizeof it); if (!w->src2(idx, &it, w->ctx)) return 0; if (it.right == MEOW_LIST_RIGHT_NONE) return 0; + } else if (w->vs_bind) { + meow_lv_item it; + memset(&it, 0, sizeof it); + w->vs_bind(idx, &it, w->vs_ctx); + if (it.right == MEOW_LIST_RIGHT_NONE) return 0; } return 1; } @@ -1170,6 +1257,70 @@ int meow_listview_tap_at(meow_widget *w, int x, int y) return idx; } +/* ============================================================ */ +/* §5.5 虚拟化层 API(W_LISTVIEW 扩展;pool 模式;src2 模式不变) */ +/* ============================================================ */ +int meow_listview_set_vsource(meow_widget *w, meow_listview_count_fn count, + meow_listview_bind_fn bind, void *ctx) +{ + if (!w || w->type != W_LISTVIEW || !count || !bind) return -1; + w->vs_count = count; w->vs_bind = bind; w->vs_ctx = ctx; + w->vs_draw = NULL; /* 重挂源:清自绘(默认 renderer) */ + if (listv_ensure_pool(w) != 0) { /* 分配固定 item 池(有界) */ + w->vs_count = NULL; w->vs_bind = NULL; /* 池分配失败:回退 src2 模式 */ + return -1; + } + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); + return 0; +} + +int meow_listview_set_item_renderer(meow_widget *w, int lines, int show_icon, + int show_right) +{ + if (!w || w->type != W_LISTVIEW) return -1; + 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; /* 未显式定行高 → 档位默认 */ + listv_ensure_pool(w); /* 行档位可能变化 → 确保池容量 */ + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); + return 0; +} + +int meow_listview_set_item_tier(meow_widget *w, int lines) +{ + if (!w || w->type != W_LISTVIEW) return -1; + if (lines < 1) lines = 1; + if (lines > 2) lines = 2; + w->lines = lines; + w->item_h = (lines == 1) ? 48 : 56; /* 档位默认高(可随后 set_item_h 覆盖) */ + listv_ensure_pool(w); + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); + return 0; +} + +int meow_listview_set_item_draw(meow_widget *w, meow_listview_draw_fn draw, + void *ctx) +{ + if (!w || w->type != W_LISTVIEW) return -1; + w->vs_draw = draw; w->vs_ctx = ctx; /* 自绘覆盖默认 renderer;NULL=用默认 */ + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); + return 0; +} + +int meow_listview_set_empty_text(meow_widget *w, const char *text) +{ + if (!w || w->type != W_LISTVIEW) return -1; + if (text) { + strncpy(w->empty_text, text, sizeof w->empty_text - 1); + w->empty_text[sizeof w->empty_text - 1] = '\0'; + } else { + w->empty_text[0] = '\0'; + } + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); + return 0; +} + static int hit(meow_widget *w, int x, int y) { if (!w) return 0; @@ -1366,79 +1517,187 @@ void meow_widget_draw(meow_widget *w) break; } case W_LISTVIEW: { - /* §5.5 结构化列表(纵向 LazyColumn):只画可见行(虚拟化)+ 视口裁剪(M2.73 纪律)。 - * 行 = 图标(左) + 标题 + 副标题(次要色) + 右侧操作区;选中行 = primaryContainer / - * onPrimaryContainer(§5.8.3);禁用行变淡不响应(命中由 hit 层保证)。全语义色。 */ + /* §5.5 结构化列表(纵向):只画可见行 + 视口裁剪(M2.73)。三路: + * ① src2 模式(既有):每帧按需取行(meow_list2_source),行为不变; + * ② 虚拟化 pool 模式(set_vsource + set_item_renderer):固定 item 池(进入视口才 bind), + * 默认 renderer 画多行富 item(图标+标题+副标题+右尾件),行高可配;滚动只移 viewport; + * ③ 完全自绘(set_item_draw):App 自绘每可见行(SDK 只给矩形+状态位)。 + * 空态:count==0 且设 empty_text → 视口内居中。全语义色;选中/按压/禁用态走主题。 */ int item = listv_item_h(w); int vp = w->h; /* 纵向 */ int off = w->scroll; - int maxs = w->count * item - vp; + int count = listv_count(w); + int maxs = (int)((long long)count * item - vp); if (maxs < 0) maxs = 0; if (off < 0) off = 0; if (off > maxs) off = maxs; int first = (item > 0) ? off / item : 0; int last = (item > 0) ? (off + vp) / item + 1 : 0; if (first < 0) first = 0; - if (last > w->count) last = w->count; + if (last > count) last = count; int vx0 = w->x, vy0 = w->y, vy1 = w->y + w->h; int thh = meow_ui_text_height(th->font_body); int pad = MEOW_LISTV_PAD, isz = MEOW_LISTV_ICON; - int text_x = vx0 + pad + isz + pad; - int op_start = vx0 + (w->w * 2 / 3); /* 行右 1/3 = 操作区(§5.5 命中) */ - int text_maxw = op_start - pad - text_x; /* 标题/副标题可用宽(不压操作区) */ + int text_x = vx0 + pad + ((w->show_icon) ? (isz + pad) : 0); /* 无图标则无图标间隙 */ + int op_start = (w->show_right) ? (vx0 + (w->w * 2 / 3)) /* 行右 1/3 = 操作区 */ + : (vx0 + w->w - pad); + int text_maxw = (w->show_right) ? (op_start - pad - text_x) /* 标题/副标题可用宽(不压操作区) */ + : ((vx0 + w->w - pad) - text_x); if (text_maxw < 8) text_maxw = 8; - for (int i = first; i < last; i++) { - meow_list_item it; - memset(&it, 0, sizeof it); - if (!w->src2 || !w->src2(i, &it, w->ctx)) continue; - int ry = w->y + (i * item - off); - if (ry >= vy1) break; - if (ry + item <= vy0) continue; - int cl = ry > vy0 ? ry : vy0; /* 可见顶界(裁剪用) */ - int cr = (ry + item) < vy1 ? (ry + item) : vy1; /* 可见底界 */ - int sel = (i == w->sel); - uint32_t textcol = sel ? th->onPrimaryContainer : th->onSurface; - uint32_t subcol = sel ? th->onPrimaryContainer : th->onSurfaceVariant; - if (sel) meow_ui_fill_rect(vx0, cl, w->w, cr - cl, th->primaryContainer); - /* 图标(左) */ - uint32_t iccol = sel ? th->onPrimaryContainer - : ((it.icon_kind == MEOW_LIST_ICON_FOLDER) ? th->primary - : th->onSurfaceVariant); - int icy = ry + (item - isz) / 2; - if (icy + isz >= vy0 && icy < vy1) /* 图标不越视口边界才画(防泄漏) */ - listv_draw_icon(th, vx0 + pad, icy, it.icon_kind, it.icon, iccol); + /* 空态占位(§5.5):count==0 且设了 empty_text → 视口内居中画文案。 */ + if (count == 0 && w->empty_text[0]) { + int ty = vy0 + (w->h - thh) / 2; + meow_ui_text_center(vx0 + w->w / 2, ty, w->empty_text, th->font_body, + th->onSurfaceVariant); + if (disabled) meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->onSurface, 90); + break; + } - /* 标题 + 副标题(两行居中;无副标题=单行居中;越界不画防泄漏) */ - int ty; - if (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(text_x, ty, it.title, text_maxw, th->font_body, textcol); - int sy = ty + thh + MEOW_LISTV_SUB_GAP; - if (sy >= vy0 && sy + thh <= vy1) - listv_text_trunc(text_x, sy, it.subtitle, text_maxw, th->font_body, subcol); - } else { - ty = ry + (item - thh) / 2; - if (it.title && ty >= vy0 && ty + thh <= vy1) - listv_text_trunc(text_x, ty, it.title, text_maxw, th->font_body, textcol); + if (w->vs_count && w->vs_bind) { /* 虚拟化 pool 模式 */ + listv_ensure_pool(w); /* 防御:视口/行高变化时确保池容量(cap 已够则零开销保留绑定) */ + if (w->vs_draw) { /* ③ 完全自绘:App 画整行(widget clip 已裁到列表视口) */ + for (int i = first; i < last; i++) { + int ry = w->y + (i * item - off); + if (ry >= vy1) break; + int st = 0; + if (i == w->sel && w->sel_mode != MEOW_LIST_SEL_NONE) st |= MEOW_LISTVIEW_ST_SELECTED; + if (meow_input_touch_down() && + meow_input_touch_x() >= vx0 && meow_input_touch_x() < vx0 + w->w && + meow_input_touch_y() >= ry && meow_input_touch_y() < ry + item) + st |= MEOW_LISTVIEW_ST_PRESSED; + w->vs_draw(i, vx0, ry, w->w, item, st, w->vs_ctx); + } + } else { /* ② 默认 renderer:固定 item 池(进入视口才 bind) */ + for (int i = first; i < last; i++) { + meow_lv_item *it = listv_slot(w, i); /* 绑定到固定池槽(回滚同槽不重绑) */ + if (!it) continue; + int ry = w->y + (i * item - off); + if (ry >= vy1) break; + if (ry + item <= vy0) continue; + int cl = ry > vy0 ? ry : vy0; /* 可见顶界(裁剪用) */ + int cr = (ry + item) < vy1 ? (ry + item) : vy1; + int sel = (i == w->sel && w->sel_mode != MEOW_LIST_SEL_NONE); + int press = meow_input_touch_down() && + meow_input_touch_x() >= vx0 && meow_input_touch_x() < vx0 + w->w && + meow_input_touch_y() >= ry && meow_input_touch_y() < ry + item; + + if (it->section) { /* 段头占位行(非粘性):surfaceContainerHigh 底 + primary 段名 */ + meow_ui_fill_rect(vx0, cl, w->w, cr - cl, th->surfaceContainerHigh); + int sty = ry + (item - thh) / 2; + if (it->title && sty >= vy0 && sty + thh <= vy1) + listv_text_trunc(text_x, sty, it->title, text_maxw, th->font_body, th->primary); + int sy = ry + item - 1; + if (sy >= cl && sy < cr) + meow_ui_fill_rect(vx0, sy, w->w, 1, th->outlineVariant); + if (!it->enabled) + meow_ui_fill_rect_alpha(vx0, cl, w->w, cr - cl, th->onSurface, 90); + continue; + } + + /* 选中/按压背景(primaryContainer / 半透明主题色按压层) */ + if (sel) meow_ui_fill_rect(vx0, cl, w->w, cr - cl, th->primaryContainer); + else if (press) meow_ui_fill_rect_alpha(vx0, cl, w->w, cr - cl, th->primary, 26); + uint32_t textcol = sel ? th->onPrimaryContainer : th->onSurface; + uint32_t subcol = sel ? th->onPrimaryContainer : th->onSurfaceVariant; + + /* 左图标/缩略图(show_icon=1;越界不画防泄漏) */ + if (w->show_icon) { + uint32_t iccol = sel ? th->onPrimaryContainer + : ((it->icon_kind == MEOW_LIST_ICON_FOLDER) ? th->primary + : th->onSurfaceVariant); + int icy = ry + (item - isz) / 2; + if (icy + isz >= vy0 && icy < vy1) + listv_draw_icon(th, vx0 + pad, icy, it->icon_kind, it->icon, iccol); + } + + /* 标题 + 副标题(双行居中;单行(lines==1 或无副标题)单行居中);越界不画 */ + if (w->lines == 2 && it->subtitle && it->subtitle[0]) { + int ty = ry + (item - (2 * thh + MEOW_LISTV_SUB_GAP)) / 2; + if (it->title && ty >= vy0 && ty + thh <= vy1) + listv_text_trunc(text_x, ty, it->title, text_maxw, th->font_body, textcol); + int sy = ty + thh + MEOW_LISTV_SUB_GAP; + if (sy >= vy0 && sy + thh <= vy1) + listv_text_trunc(text_x, sy, it->subtitle, text_maxw, th->font_body, subcol); + } else { + int ty = ry + (item - thh) / 2; + if (it->title && ty >= vy0 && ty + thh <= vy1) + listv_text_trunc(text_x, ty, it->title, text_maxw, th->font_body, textcol); + } + + /* 右尾件(show_right=1:箭头/开关/复选/⋯) */ + if (w->show_right && it->right != MEOW_LIST_RIGHT_NONE) { + int ax = w->x + w->w - pad - 24; /* 右缘动作槽(约 24px) */ + if (ax >= op_start) + listv_draw_right(th, ax, ry, item, it->right, it->on, subcol); + } + + /* 分隔线(仅落在可见带内才画) */ + int sy = ry + item - 1; + if (sy >= cl && sy < cr) + meow_ui_fill_rect(vx0, sy, w->w, 1, th->outlineVariant); + + /* 禁用行整体变淡(§5.5/§5.8.3 enabled=0 → 变暗) */ + if (!it->enabled) + meow_ui_fill_rect_alpha(vx0, cl, w->w, cr - cl, th->onSurface, 90); + } } + } else { + /* ① src2 模式(既有):每帧按需取行(meow_list2_source),行为不变。 + * 行 = 图标(左) + 标题 + 副标题(次要色) + 右侧操作区;选中行 = primaryContainer。 */ + for (int i = first; i < last; i++) { + meow_list_item it; + memset(&it, 0, sizeof it); + if (!w->src2 || !w->src2(i, &it, w->ctx)) continue; + int ry = w->y + (i * item - off); + if (ry >= vy1) break; + if (ry + item <= vy0) continue; + int cl = ry > vy0 ? ry : vy0; /* 可见顶界(裁剪用) */ + int cr = (ry + item) < vy1 ? (ry + item) : vy1; /* 可见底界 */ + int sel = (i == w->sel); + uint32_t textcol = sel ? th->onPrimaryContainer : th->onSurface; + uint32_t subcol = sel ? th->onPrimaryContainer : th->onSurfaceVariant; + if (sel) meow_ui_fill_rect(vx0, cl, w->w, cr - cl, th->primaryContainer); - /* 右侧操作区 */ - if (it.right != MEOW_LIST_RIGHT_NONE) { - int ax = w->x + w->w - pad - 24; /* 右缘动作槽(约 24px) */ - if (ax >= op_start) - listv_draw_right(th, ax, ry, item, it.right, it.on, subcol); + /* 图标(左) */ + uint32_t iccol = sel ? th->onPrimaryContainer + : ((it.icon_kind == MEOW_LIST_ICON_FOLDER) ? th->primary + : th->onSurfaceVariant); + int icy = ry + (item - isz) / 2; + if (icy + isz >= vy0 && icy < vy1) /* 图标不越视口边界才画(防泄漏) */ + listv_draw_icon(th, vx0 + pad, icy, it.icon_kind, it.icon, iccol); + + /* 标题 + 副标题(两行居中;无副标题=单行居中;越界不画防泄漏) */ + int ty; + if (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(text_x, ty, it.title, text_maxw, th->font_body, textcol); + int sy = ty + thh + MEOW_LISTV_SUB_GAP; + if (sy >= vy0 && sy + thh <= vy1) + listv_text_trunc(text_x, sy, it.subtitle, text_maxw, th->font_body, subcol); + } else { + ty = ry + (item - thh) / 2; + if (it.title && ty >= vy0 && ty + thh <= vy1) + listv_text_trunc(text_x, ty, it.title, text_maxw, th->font_body, textcol); + } + + /* 右侧操作区 */ + if (it.right != MEOW_LIST_RIGHT_NONE) { + int ax = w->x + w->w - pad - 24; /* 右缘动作槽(约 24px) */ + if (ax >= op_start) + listv_draw_right(th, ax, ry, item, it.right, it.on, subcol); + } + + /* 分隔线(仅落在可见带内才画) */ + int sy = ry + item - 1; + if (sy >= cl && sy < cr) + meow_ui_fill_rect(vx0, sy, w->w, 1, th->outlineVariant); + + /* 禁用行整体变淡(§5.5/§5.8.3 enabled=0 → 变暗) */ + if (!it.enabled) + meow_ui_fill_rect_alpha(vx0, cl, w->w, cr - cl, th->onSurface, 90); } - - /* 分隔线(仅落在可见带内才画) */ - int sy = ry + item - 1; - if (sy >= cl && sy < cr) - meow_ui_fill_rect(vx0, sy, w->w, 1, th->outlineVariant); - - /* 禁用行整体变淡(§5.5/§5.8.3 enabled=0 → 变暗) */ - if (!it.enabled) - meow_ui_fill_rect_alpha(vx0, cl, w->w, cr - cl, th->onSurface, 90); } if (disabled) meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->onSurface, 90); break; diff --git a/test_meow_ui_listvirt.c b/test_meow_ui_listvirt.c new file mode 100644 index 0000000..8d99e60 --- /dev/null +++ b/test_meow_ui_listvirt.c @@ -0,0 +1,365 @@ +/* test_meow_ui_listvirt.c — 宿主验证 §5.5 虚拟化列表(W_LISTVIEW pool 模式): + * T1 虚拟化正确性:10000 项滚动窗口内容正确(hit 行号映射)/ bind 次数有界(全量扫过=count)/ + * 回滚不重绑(scroll 回顶 bind 计数不变)。 + * T2 多行富 item 渲染:双行(标题+副标题)/ 单行(仅标题)/ 左图标占位 / 右尾件 / + * 副标题超宽截断(不越过操作区)。 + * T3 段头占位行(bind 返回 section=1)+ 空态占位(count=0 置 empty_text)。 + * T4 完全自绘(set_item_draw 覆盖默认 renderer)。 + * T5 焦点(tap_at → meow_ui_focus_owner)+ fling 联动(meow_scroll_wrap 包裹 pool 列表, + * track+fling 吸附到行边界;滚动只改 viewport 偏移)。 + * + * 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global): + * gcc -o test_meow_ui_listvirt test_meow_ui_listvirt.c test_host_stub.c \ + * meow_ui_gfx.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_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 \ + * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm + * 运行:./test_meow_ui_listvirt + */ +#include "meow_ui.h" +#include "meow_ui_internal.h" /* meow_ui_back_buf/pitch/bpp:真实像素断言 */ +#include +#include +#include +#include + +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) + +/* ---- 虚拟化数据源(10000 项):bind 计数有界 / 内容正确 / 段头 ---- */ +#define N 10000 +static int g_bind_total; /* pool bind 总数(经 listv_slot 调用;hit 走独立路径不计数) */ +static int g_bind_touch_count; /* bind 回调的独立计数(hit 也会调 bind,故仅命中计数用) */ +static int g_count_override; /* <0 = 用默认 N;>=0 = 覆盖(空态/小列表用) */ +static int g_section_at; /* 该 index 返回段头(-1=无) */ + +static int vcount(void *ctx) { (void)ctx; return g_count_override >= 0 ? g_count_override : N; } +static void vbind(int idx, meow_lv_item *it, void *ctx) +{ + (void)ctx; + g_bind_total++; + g_bind_touch_count++; + memset(it, 0, sizeof *it); + it->icon_kind = MEOW_LIST_ICON_FOLDER; + it->right = MEOW_LIST_RIGHT_ARROW; + it->on = 1; it->enabled = 1; + if (g_section_at == idx) { + it->section = 1; + it->title = "SECTION A"; it->subtitle = NULL; it->icon_kind = MEOW_LIST_ICON_NONE; + it->right = MEOW_LIST_RIGHT_NONE; + return; + } + static char t[64], s[64]; + snprintf(t, sizeof t, "title %d", idx); + snprintf(s, sizeof s, "sub %d", idx); + it->title = t; it->subtitle = s; +} + +/* ---- 数据源 A:单行(无副标题/无图标/无尾件),供 T2 单行断言 ---- */ +static int via_count(void *ctx) { (void)ctx; return 3; } +static void via_bind(int idx, meow_lv_item *it, void *ctx) +{ + (void)ctx; (void)idx; + memset(it, 0, sizeof *it); + static char t[32]; snprintf(t, sizeof t, "single %d", idx); + it->title = t; it->subtitle = NULL; it->icon_kind = MEOW_LIST_ICON_NONE; + it->right = MEOW_LIST_RIGHT_NONE; it->enabled = 1; +} + +/* ---- 数据源:超宽副标题(T2 截断) ---- */ +static int long_count(void *ctx) { (void)ctx; return 1; } +static void long_bind(int idx, meow_lv_item *it, void *ctx) +{ + (void)ctx; (void)idx; + memset(it, 0, sizeof *it); + it->title = "Title"; + it->subtitle = "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW"; /* 超宽 */ + it->icon_kind = MEOW_LIST_ICON_NONE; + it->right = MEOW_LIST_RIGHT_NONE; it->enabled = 1; +} + +/* ---- T4 完全自绘:整行画实心块 + 标题 ---- */ +static int g_draw_calls; +static void mydraw(int idx, int x, int y, int w, int h, int st, void *ctx) +{ + (void)idx; (void)ctx; + g_draw_calls++; + meow_ui_fill_rect(x, y, w, h, 0x334455); /* 自定义行底 */ + meow_ui_fill_rect(x + 4, y + 4, 8, 8, 0xFF0000); /* 角标记(自绘内容) */ + if (st & MEOW_LISTVIEW_ST_SELECTED) meow_ui_fill_rect(x + w - 12, y + 4, 8, 8, 0x00FF00); +} + +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 sb_r, sb_g, sb_b, os_r, os_g, os_b, pc_r, pc_g, pc_b, ospv_r, ospv_g, ospv_b, prim_r, prim_g, prim_b; + UNPK(th->bg, sb_r, sb_g, sb_b); + UNPK(th->onSurface, os_r, os_g, os_b); + UNPK(th->primaryContainer, pc_r, pc_g, pc_b); + UNPK(th->onSurfaceVariant, ospv_r, ospv_g, ospv_b); + UNPK(th->primary, prim_r, prim_g, prim_b); + int thh = meow_ui_text_height(th->font_body); + int itemh = 56; + + /* ============ T1 虚拟化正确性 / bind 有界 / 回滚不重绑 ============ */ + { + g_count_override = -1; g_section_at = -1; g_bind_total = 0; g_bind_touch_count = 0; + int lvx = 20, lvy = 50, lvw = 440, lvh = 300; + meow_widget *lv = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_NONE); + CHECK(meow_listview_set_item_h(lv, itemh) == itemh, "item_h"); + CHECK(meow_listview_set_vsource(lv, vcount, vbind, NULL) == 0, "vsource"); + CHECK(meow_listview_max_scroll(lv) == N * itemh - lvh, "max_scroll %d", meow_listview_max_scroll(lv)); /* 560000-300=559700 */ + CHECK(meow_listview_scroll(lv) == 0, "scroll0"); + + /* 首帧 draw:绑定视口行(first..last) */ + meow_widget_draw(lv); + int first0 = 0; + int last0 = (0 + lvh) / itemh + 1; if (last0 > N) last0 = N; + int B0 = g_bind_total; + CHECK(B0 == (last0 - first0), "first draw binds %d rows got %d", last0 - first0, B0); + + /* 下滚 1 行:仅新入视口的行被 bind(delta==1),未离屏行不重绑 */ + meow_listview_set_scroll(lv, itemh); + meow_widget_draw(lv); + int B1 = g_bind_total; + CHECK(B1 == B0 + 1, "scroll+1row binds exactly 1 more got %d (was %d)", B1 - B0, B0); + + /* 回滚到顶:视口行槽位仍在 → 不重绑(delta==0) */ + meow_listview_set_scroll(lv, 0); + meow_widget_draw(lv); + CHECK(g_bind_total == B1, "scroll-back no rebind got %d (was %d)", g_bind_total, B1); + + /* 全量扫过:每个 index 进入视口时 bind 恰一次 → 总数 == N(有界,非 per-frame × count) */ + int prev = 1; + while (prev <= N) { + int off = prev * itemh; + meow_listview_set_scroll(lv, off); + meow_widget_draw(lv); + if (off >= meow_listview_max_scroll(lv)) break; + prev++; + } + meow_listview_set_scroll(lv, meow_listview_max_scroll(lv)); + meow_widget_draw(lv); + CHECK(g_bind_total == N, "full sweep binds each index once got %d (expect %d)", g_bind_total, N); + CHECK(g_bind_touch_count == N, "bind callback called %d times (expect %d)", g_bind_touch_count, N); + + /* 回滚(扫到末尾后拉回顶):池发生整表 wrap(cap N) win = N; + meow_listview_set_scroll(lv, 0); + meow_widget_draw(lv); + CHECK((g_bind_total - before2) <= win, "scroll-back rebind bounded (delta %d ≤ %d)", + g_bind_total - before2, win); + + /* 内容正确:命中行号 = 滚动换算(视口窗口映射正确) */ + CHECK(meow_listview_hit(lv, lvx + lvw - 10, lvy + 0 * itemh + 20) == 0, "hit0"); + meow_listview_set_scroll(lv, 5 * itemh); + int idx_mid = meow_listview_hit(lv, lvx + lvw - 10, lvy + itemh / 2); + CHECK(idx_mid == 5, "hit maps scroll to row got %d (expect 5)", idx_mid); + meow_listview_set_scroll(lv, 888 * itemh); + int idx_mid2 = meow_listview_hit(lv, lvx + lvw - 10, lvy + itemh * 2 + itemh / 2); + CHECK(idx_mid2 == 890, "hit row 2 of window got %d (expect 890)", idx_mid2); + meow_widget_free(lv); + } + + /* ============ T2 多行富 item 渲染:双行 / 单行 / 图标 / 尾件 / 截断 ============ */ + { + /* 双行(lines=2,默认):行 0 title+subtitle + icon(FOLDER) + 尾件(ARROW) */ + int lvx = 20, lvy = 50, lvw = 440, lvh = 120; + meow_widget *lv = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_NONE); + CHECK(meow_listview_set_item_h(lv, itemh) == itemh, "item_h"); + CHECK(meow_listview_set_vsource(lv, vcount, vbind, NULL) == 0, "vsource"); + CHECK(meow_listview_set_item_renderer(lv, 2, 1, 1) == 0, "renderer"); + meow_ui_clear(th->bg); + meow_widget_draw(lv); + int text_x = lvx + 16 + 24 + 16; /* show_icon=1 → 76 */ + int op_start = lvx + (lvw * 2 / 3); /* 20+293=313 */ + int ty0 = lvy + (itemh - (2 * thh + 2)) / 2; /* 双行标题 y */ + int sy0 = ty0 + thh + 2; /* 副标题 y */ + CHECK(region_has(text_x, ty0, 60, thh, os_r, os_g, os_b) > 2, "double-line title drawn"); + CHECK(region_has(text_x, sy0, 60, thh, ospv_r, ospv_g, ospv_b) > 2, "double-line subtitle drawn"); + CHECK(region_has(lvx + 16, lvy + (itemh - 24) / 2, 24, 24, prim_r, prim_g, prim_b) > 30, "icon placeholder drawn"); + int ax = lvx + lvw - 16 - 24; + CHECK(region_has(ax, lvy + itemh / 2 - 8, 20, 16, ospv_r, ospv_g, ospv_b) > 2, "right tail (arrow) drawn"); + + /* 单行(lines=1):仅标题居中,无副标题带 */ + g_count_override = 3; + meow_widget *lv1 = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_NONE); + meow_listview_set_item_h(lv1, itemh); + meow_listview_set_vsource(lv1, via_count, via_bind, NULL); + meow_listview_set_item_renderer(lv1, 1, 0, 0); /* 单行、无图标、无尾件 */ + meow_ui_clear(th->bg); + meow_widget_draw(lv1); + int t1y = lvy + (itemh - thh) / 2; + CHECK(region_has(lvx + 16, t1y, 60, thh, os_r, os_g, os_b) > 2, "single-line title drawn"); + CHECK(region_has(lvx + 16, t1y + thh + 2, 60, thh, ospv_r, ospv_g, ospv_b) == 0, "single-line no subtitle"); + /* 单行档位默认行高 48(set_item_tier) */ + meow_widget *lv2 = meow_listview_create(lvx, lvy, lvw, lvh, 3, NULL, NULL, MEOW_LIST_SEL_NONE); + CHECK(meow_listview_set_item_tier(lv2, 1) == 0, "tier"); + CHECK(meow_listview_scroll(lv2) == 0 && meow_listview_max_scroll(lv2) == 3 * 48 - lvh, "tier-h %d", + meow_listview_max_scroll(lv2)); + meow_widget_free(lv2); + + /* 副标题超宽截断:不画过 text_maxw(op_start 之前) */ + g_count_override = -1; + meow_widget *lv3 = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_NONE); + meow_listview_set_item_h(lv3, itemh); + meow_listview_set_vsource(lv3, long_count, long_bind, NULL); + meow_listview_set_item_renderer(lv3, 2, 0, 0); /* 无图标 → text_x = lvx+16=36 */ + meow_ui_clear(th->bg); + meow_widget_draw(lv3); + int text_x3 = lvx + 16; + int op3 = lvx + lvw - 16; /* show_right=0 → op_start=右缘-pad */ + int maxw3 = op3 - text_x3; /* 20+440-16-36 = 408 */ + int ty3 = lvy + (itemh - (2 * thh + 2)) / 2; + int sy3 = ty3 + thh + 2; + CHECK(region_has(text_x3, sy3, 40, thh, ospv_r, ospv_g, ospv_b) > 2, "long subtitle leading drawn"); + CHECK(region_has(text_x3 + maxw3 + 4, sy3, 40, thh, ospv_r, ospv_g, ospv_b) == 0, "long subtitle truncated (no overflow)"); + meow_widget_free(lv3); + meow_widget_free(lv1); + + /* 截断不越界 + 双行副标题带(回归:无图标/无尾件时 text_x/pad 正确) */ + CHECK(region_has(text_x3, ty3, 40, thh, os_r, os_g, os_b) > 2, "truncate-r title drawn"); + meow_widget_free(lv); + } + + /* ============ T3 段头占位 + 空态 ============ */ + { + int lvx = 20, lvy = 50, lvw = 440, lvh = 120; + g_count_override = 3; g_section_at = 1; + meow_widget *lv = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_NONE); + meow_listview_set_item_h(lv, itemh); + meow_listview_set_vsource(lv, vcount, vbind, NULL); + meow_ui_clear(th->bg); + meow_widget_draw(lv); + int sch_r, sch_g, sch_b; UNPK(th->surfaceContainerHigh, sch_r, sch_g, sch_b); + CHECK(region_has(lvx, lvy + itemh, lvw, itemh - 4, sch_r, sch_g, sch_b) > 40, "section header band drawn"); + CHECK(region_has(lvx + 16, lvy + itemh + (itemh - thh) / 2, 60, thh, prim_r, prim_g, prim_b) > 2, + "section header title (primary) drawn"); + + /* 空态:count=0 + empty_text → 视口内居中画文案(onSurfaceVariant) */ + g_count_override = 0; g_section_at = -1; + meow_widget *lve = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_NONE); + meow_listview_set_vsource(lve, vcount, vbind, NULL); + meow_listview_set_empty_text(lve, "No data"); /* ASCII(宿主 5x7 点阵仅含 ASCII/CJK 用真机字库) */ + meow_ui_clear(th->bg); + meow_widget_draw(lve); + int ety = lvy + (lvh - thh) / 2; + CHECK(region_has(lvx + lvw / 2 - 60, ety, 120, thh, ospv_r, ospv_g, ospv_b) > 2, "empty state text drawn"); + meow_widget_free(lve); + meow_widget_free(lv); + } + + /* ============ T4 完全自绘(set_item_draw 覆盖默认 renderer) ============ */ + { + int lvx = 20, lvy = 200, lvw = 440, lvh = 120; + g_count_override = 3; g_section_at = -1; g_draw_calls = 0; + meow_widget *lv = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_SINGLE); + meow_listview_set_item_h(lv, itemh); + meow_listview_set_vsource(lv, vcount, vbind, NULL); + CHECK(meow_listview_set_item_draw(lv, mydraw, NULL) == 0, "set_draw"); + meow_ui_clear(th->bg); + meow_widget_draw(lv); + int vis = (lvh / itemh) + 1; if (vis > 3) vis = 3; + CHECK(g_draw_calls == vis, "draw_item called per visible row got %d (expect %d)", g_draw_calls, vis); + CHECK(region_has(lvx + 4, lvy + 4, 8, 8, 0xFF, 0x00, 0x00) > 0, "custom row content drawn"); + meow_widget_free(lv); + } + + /* ============ T5 焦点 + fling 联动(meow_scroll_wrap 包裹 pool 列表) ============ */ + { + int lvx = 20, lvy = 330, lvw = 440, lvh = 300; + g_count_override = -1; g_section_at = -1; + meow_widget *lv = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_SINGLE); + meow_listview_set_item_h(lv, itemh); + meow_listview_set_vsource(lv, vcount, vbind, NULL); + + /* 焦点:tap_at 命中行 → 列表成为唯一焦点(§6.4) */ + int tidx = meow_listview_tap_at(lv, lvx + lvw - 10, lvy + itemh / 2); + CHECK(tidx >= 0, "tap_at returns row got %d", tidx); + CHECK(meow_ui_focus_owner() == (void *)lv, "list acquired focus after tap_at"); + + /* fling:track+fling → meow_scroll_wrap 包裹;吸附到行边界(snap=itemh) */ + meow_scroll *s = meow_scroll_wrap(lv, 1); /* 1=W_LISTVIEW */ + CHECK(s != NULL, "scroll wrap pool list"); + meow_scroll_set_snap(s, itemh); + uint64_t t = meow_ui_time_ms(); + for (int i = 0; i < 12; i++) meow_scroll_track(s, 30, t + (uint64_t)i * 16); + int start = meow_scroll_offset(s); + CHECK(start > 0, "drag advances offset got %d", start); + meow_scroll_fling(s); + CHECK(meow_scroll_active(s) == 1, "fling active"); + meow_scroll_stop(s); + CHECK(meow_scroll_active(s) == 0, "stop ok"); + + /* 再次 fling 到自然停:offset 只改 viewport,且 snap 到行边界 */ + for (int i = 0; i < 12; i++) meow_scroll_track(s, 30, t + (uint64_t)i * 16); + start = meow_scroll_offset(s); + meow_scroll_fling(s); + int max_off = start, guard = 0; + while (meow_scroll_active(s) && guard++ < 300) { + meow_anim_tick(); + int cur = meow_scroll_offset(s); + if (max_off < cur) max_off = cur; + usleep(10000); + } + meow_anim_tick(); + int end = meow_scroll_offset(s); + CHECK(end >= start, "fling advances got %d vs %d", end, start); + CHECK((end % itemh) == 0, "snap to item boundary got %d (%%%d)", end, itemh); + CHECK(end <= meow_listview_max_scroll(lv), "fling in range %d (max %d)", end, meow_listview_max_scroll(lv)); + /* 滚动只改 viewport:meow_listview_scroll 与物理层一致 */ + CHECK(meow_listview_scroll(lv) == end, "listview_scroll mirrors scroll offset %d vs %d", meow_listview_scroll(lv), end); + /* 停稳后 draw 反映最终窗口(bind 只补新入视口的行,不整表重绑) */ + int before_draw = g_bind_total; + int win = (lvh / itemh) + 1; if (win > N) win = N; /* 视口窗口行数(含底缘 1 行) */ + meow_widget_draw(lv); + CHECK(g_bind_total >= before_draw && (g_bind_total - before_draw) <= win, + "settle draw rebinds only window (delta %d ≤ %d)", g_bind_total - before_draw, win); + meow_scroll_free(s); + meow_widget_free(lv); + } + + meow_ui_shutdown(); + printf("listvirt: %d pass / %d fail\n", g_pass, g_fail); + return g_fail ? 1 : 0; +}