sdk(ui): W_LISTVIEW 虚拟化层 — 固定 item 池 + 多行富 item + 完全自绘 (§5.5)

在既有 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 一览/池语义/集成/回归).
This commit is contained in:
AskaEth committed 2026-09-02 01:36:48 +08:00
1 parent 85e1d03157
commit 9d54cf89c5
4 files changed
+797 -69

No files matched your search

+46 -1
View File
@@ -372,7 +372,7 @@ int meow_ui_setform_hit_row(meow_widget *f, int x, int y);/* 命中行号(-1=
### 5.5 设计模板 A② 列表+选择 [源:2026-08-30] ### 5.5 设计模板 A② 列表+选择 [源:2026-08-30]
> **性质**:SDK 设计模板,供 App 主页/子页按需选用;对既有 `meow_list_create`(B1 最小集,§5,仅 label 文本)做**结构化扩展**(行=图标+标题+副标题+右侧操作 + 选中态)。API「草案/待定案」,不实现。**(✅ 已实现:结构化列表控件 `W_LISTVIEW`,M2.76 · agent-186 · 243148e,见 §5.8.8 分批;长按事件 `MEOW_EV_LONG_PRESS` + `meow_ui_set_long_press_cb/ms`,M2.77 · agent-191 · e95552b。)** > **性质**:SDK 设计模板,供 App 主页/子页按需选用;对既有 `meow_list_create`(B1 最小集,§5,仅 label 文本)做**结构化扩展**(行=图标+标题+副标题+右侧操作 + 选中态)。API「草案/待定案」,不实现。**(✅ 已实现:结构化列表控件 `W_LISTVIEW`,M2.76 · agent-186 · 243148e,见 §5.8.8 分批;长按事件 `MEOW_EV_LONG_PRESS` + `meow_ui_set_long_press_cb/ms`,M2.77 · agent-191 · e95552b;虚拟化层(固定 item 池 + 多行富 item + 完全自绘 + 段头 + 空态),2026-09-02,见 §5.5「虚拟化层」。)**
**摘要**:可滚动选择容器。**行数据源回调**逐行给出(图标 / 标题 / 副标题 / 右侧操作 / 是否可选),容器负责绘制/滚动/选中/命中;支持**单选、多选、长按**(上下文操作)。适合「已保存网络 / 附近WiFi / 蓝牙设备 / 歌曲列表 / 设置子菜单」等。 **摘要**:可滚动选择容器。**行数据源回调**逐行给出(图标 / 标题 / 副标题 / 右侧操作 / 是否可选),容器负责绘制/滚动/选中/命中;支持**单选、多选、长按**(上下文操作)。适合「已保存网络 / 附近WiFi / 蓝牙设备 / 歌曲列表 / 设置子菜单」等。
@@ -419,6 +419,51 @@ int meow_ui_listview_hit(meow_widget *lv, int x, int y); /* 命中行;
``` ```
> 说明:既有 `meow_list_create`(label-only)保留;`meow_ui_listview_*` 为**结构化扩展**(图标/副标题/右侧/选中),可作为真则替换裸 label 列表。 > 说明:既有 `meow_list_create`(label-only)保留;`meow_ui_listview_*` 为**结构化扩展**(图标/副标题/右侧/选中),可作为真则替换裸 label 列表。
#### 虚拟化层(2026-09-02,W_LISTVIEW 扩展;API 追加式,旧 API 一字不改)
> **痛点**:W_LISTVIEW(src2 每帧按需取行)只画可见行,但**每帧重取全部可见行**;大列表(几千/
> 上万首)App 只能自行造「滑动窗口」补载(如 ncm playlist.c 的 `pl_maybe_ensure` + `appstate_song_copy`)。
> 本层把「固定 item 池 + 多行富 item + 完全自绘」收编为 SDK 级能力,让 App 只提供「总数 + bind」回调。
**关键交互/数据模型**:
- **数据源**:`meow_listview_count_fn`(返回总行数,任意长 ≥65535,不常驻 item 对象)+ `meow_listview_bind_fn`
(`index, meow_lv_item*, ctx`,仅在行「进入视口」(绑定到固定池槽)时被调**一次**)。`count` 每帧实时查
(搜索过滤/懒加载动态长度)。
- **固定 item 池**:池容量 = 视口行数 + 2 静态槽(有界,`MEOW_LV_POOL_MAX`=96),行离开视口不销毁、
**回滚不重绑**(同一 index 命中同一槽 → bind 不重调);滚动只改 viewport 偏移(复用既有 set_scroll/max_scroll),
不重绑未进视口的项。**整表扫过后池发生 wrap(cap<count),回收合法重绑但每次 draw 有界(≤视口窗口)**。
- **多行富 item**:`meow_lv_item` 含 主标题 / 副标题(第二行小字)/ 左图标·缩略图 / 右尾件(chevron·开关·复选·⋯)/
`section`(段头占位行)/ `enabled`。行高可配:`set_item_h`(精确 px)或 `set_item_tier(lines)`(1→48 / 2→56)。
- **默认 renderer**(`meow_listview_set_item_renderer(lines, show_icon, show_right)`)用主题语义色绘制富 item;
**完全自绘**:`meow_listview_set_item_draw(draw_fn, ctx)` 覆盖默认 renderer(App 收 idx/rect/state 自绘整行)。
- **段头**:bind 返回 `item->section=1`(title=段名)→ 段头占位行(非粘性;粘性为后续增强)。
- **空态**:`meow_listview_set_empty_text(text)`——count==0 时视口内居中画文案。
**与既有机制集成**:焦点/事件走 §6.4(`meow_listview_tap_at` 命中行自动 `meow_ui_focus_acquire`,点外由
`meow_ui_focus_tap` 释放——list 焦点残留根治下沉);滚动接 §12.3 `meow_scroll`(`meow_scroll_wrap(lv, 1)`,
`track`+`fling` 跟手/惯性,`set_snap(item_h)` 吸附到行边界);选中/按压态样式走主题
(`primaryContainer`/半透明 `primary` 按压层);`enabled=0` 行变淡且命中不响应。
**API 一览(`include/meow_ui_widgets.h`,§5.5)**:
```c
typedef struct meow_lv_item { const char *title, *subtitle; const meow_img *icon; int icon_kind;
int right, on, enabled, section; void *user; } meow_lv_item;
typedef int (*meow_listview_count_fn)(void *ctx);
typedef void (*meow_listview_bind_fn)(int index, meow_lv_item *item, void *ctx);
typedef void (*meow_listview_draw_fn)(int idx, int x, int y, int w, int h, int state, void *ctx);
int meow_listview_set_vsource(meow_widget *lv, meow_listview_count_fn count,
meow_listview_bind_fn bind, void *ctx); /* 切 pool 模式 */
int meow_listview_set_item_renderer(meow_widget *lv, int lines, int show_icon, int show_right);
int meow_listview_set_item_tier(meow_widget *lv, int lines); /* 1→48 / 2→56 */
int meow_listview_set_item_draw(meow_widget *lv, meow_listview_draw_fn draw, void *ctx);
int meow_listview_set_empty_text(meow_widget *lv, const char *text);
```
> 兼容:既有 `meow_listview_*`(src2 模式)全部保留不动(`src2==NULL` 走既有路径);本层为叠加。
> `meow_widget` 尾部追加虚拟化字段(`calloc` 归零,安全);`meow_ui.a` 需重编,App 链新库(老 ABI 不重建名)。
> 宿主验证 `test_meow_ui_listvirt`(T1 虚拟化/bind 有界/回滚、T2 多行/截断/图标/尾件、T3 段头/空态、
> T4 自绘、T5 焦点/fling 联动)+ 全量回归 widgets/dispatch/box/anim/scroll/ime/pinyin/overlay/notif/
> sdk_input_keys/p1p2p3 全 PASS;交叉 -Wall -Wextra 0 warning。
--- ---
### 5.6 设计模板 B③ 弹窗/对话框 [源:2026-08-30] ### 5.6 设计模板 B③ 弹窗/对话框 [源:2026-08-30]
+59
View File
@@ -293,6 +293,65 @@ void meow_listview_set_on_tap(meow_widget *lv, meow_listview_tap_fn cb, void *ct
/* 命中行:置选中 + 回调(若设)+ 返回 idx;未命中返回 -1。 */ /* 命中行:置选中 + 回调(若设)+ 返回 idx;未命中返回 -1。 */
int meow_listview_tap_at(meow_widget *lv, int x, int y); 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 #ifdef __cplusplus
} }
#endif #endif
+327 -68
View File
@@ -90,6 +90,20 @@ struct meow_widget {
int sel_bits_n; /* 位图字数 */ int sel_bits_n; /* 位图字数 */
meow_listview_tap_fn lv_tap_cb; /* 行点击回调(idx, op) */ meow_listview_tap_fn lv_tap_cb; /* 行点击回调(idx, op) */
void *lv_tap_ctx; /* 回调 ctx */ 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 时按控件内建默认)---- /* ---- §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 失效勿用) */ meow_draw_widget_unregister(w); /* §6.2:绘制物注册表自动注销(stale id 失效勿用) */
live_btn_unregister(w); live_btn_unregister(w);
if (w->sel_bits) { free(w->sel_bits); w->sel_bits = NULL; } /* W_LISTVIEW 多选位图 */ 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); 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; } 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 的逐字符宽度累加)。 */ /* 文本截断:超出 maxw 加 "..."(复用 browser_text_trunc 的逐字符宽度累加)。 */
static void listv_text_trunc(int x, int y, const char *s, int maxw, int size, uint32_t col) 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->count = count > 0 ? count : 0; t->src2 = src; t->ctx = ctx;
t->sel = -1; t->item_h = 56; t->scroll = 0; 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->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) { if (t->sel_mode == MEOW_LIST_SEL_MULTI && t->count > 0) {
t->sel_bits_n = (t->count + 31) / 32; t->sel_bits_n = (t->count + 31) / 32;
t->sel_bits = (uint32_t *)calloc((size_t)t->sel_bits_n, sizeof(uint32_t)); 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) int meow_listview_set_item_h(meow_widget *w, int item)
{ {
if (!w || w->type != W_LISTVIEW) return -1; 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; return w->item_h;
} }
void meow_listview_set_count(meow_widget *w, int count) 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 < 0) count = 0;
if (count == w->count) return; if (count == w->count) return;
w->count = count; w->count = count;
if (w->sel >= w->count) w->sel = -1; int n = listv_count(w); /* pool 模式忽略 set_count(count 实时查) */
if (w->sel_mode == MEOW_LIST_SEL_MULTI) { /* 重分配多选位图 */ 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; 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) { if (need > 0) {
w->sel_bits_n = need; w->sel_bits_n = need;
w->sel_bits = (uint32_t *)calloc((size_t)need, sizeof(uint32_t)); 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) int meow_listview_max_scroll(meow_widget *w)
{ {
if (!w || w->type != W_LISTVIEW) return 0; if (!w || w->type != W_LISTVIEW) return 0;
int ms = w->count * listv_item_h(w) - w->h; long long ms = (long long)listv_count(w) * listv_item_h(w) - w->h;
return ms < 0 ? 0 : ms; if (ms < 0) return 0;
return ms > 0x7FFFFFFF ? 0x7FFFFFFF : (int)ms;
} }
void meow_listview_set_scroll(meow_widget *w, int offset) 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 || w->type != W_LISTVIEW) return;
if (w->sel_mode == MEOW_LIST_SEL_NONE) return; if (w->sel_mode == MEOW_LIST_SEL_NONE) return;
if (w->count <= 0) { w->sel = -1; return; } int n = listv_count(w);
if (idx >= w->count) idx = w->count - 1; /* 越界 clamp(§5.5 首/末行钳制) */ 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 (w->sel_mode == MEOW_LIST_SEL_SINGLE) {
if (idx == w->sel) return; if (idx == w->sel) return;
w->sel = idx; /* idx<0 = 清除 */ 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 (!w || w->type != W_LISTVIEW) return 0;
if (!out || max <= 0) 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) { 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; if (w->sel_bits && (w->sel_bits[i >> 5] & (1u << (i & 31)))) out[n++] = i;
} else if (w->sel >= 0 && n < max) { } else if (w->sel >= 0 && n < max) {
out[n++] = w->sel; 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 || w->type != W_LISTVIEW) return 0;
if (w->sel_mode == MEOW_LIST_SEL_MULTI) { if (w->sel_mode == MEOW_LIST_SEL_MULTI) {
int n = 0; int n = 0, total = listv_count(w);
for (int i = 0; i < w->count; i++) for (int i = 0; i < total; i++)
if (w->sel_bits && (w->sel_bits[i >> 5] & (1u << (i & 31)))) n++; if (w->sel_bits && (w->sel_bits[i >> 5] & (1u << (i & 31)))) n++;
return 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; 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 item = listv_item_h(w);
int idx = (y - w->y + w->scroll) / item; 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) */ if (w->src2) { /* 禁用行不响应(§5.5) */
meow_list_item it; meow_list_item it;
memset(&it, 0, sizeof it); memset(&it, 0, sizeof it);
if (!w->src2(idx, &it, w->ctx)) return -1; if (!w->src2(idx, &it, w->ctx)) return -1;
if (!it.enabled) 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; return idx;
} }
@@ -1151,6 +1233,11 @@ int meow_listview_hit_op(meow_widget *w, int x, int y)
memset(&it, 0, sizeof it); memset(&it, 0, sizeof it);
if (!w->src2(idx, &it, w->ctx)) return 0; if (!w->src2(idx, &it, w->ctx)) return 0;
if (it.right == MEOW_LIST_RIGHT_NONE) 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; return 1;
} }
@@ -1170,6 +1257,70 @@ int meow_listview_tap_at(meow_widget *w, int x, int y)
return idx; 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) static int hit(meow_widget *w, int x, int y)
{ {
if (!w) return 0; if (!w) return 0;
@@ -1366,79 +1517,187 @@ void meow_widget_draw(meow_widget *w)
break; break;
} }
case W_LISTVIEW: { case W_LISTVIEW: {
/* §5.5 结构化列表(纵向 LazyColumn):只画可见行(虚拟化)+ 视口裁剪(M2.73 纪律)。 /* §5.5 结构化列表(纵向):只画可见行 + 视口裁剪(M2.73)。三路:
* 行 = 图标(左) + 标题 + 副标题(次要色) + 右侧操作区;选中行 = primaryContainer / * ① src2 模式(既有):每帧按需取行(meow_list2_source),行为不变;
* onPrimaryContainer(§5.8.3);禁用行变淡不响应(命中由 hit 层保证)。全语义色。 */ * ② 虚拟化 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 item = listv_item_h(w);
int vp = w->h; /* 纵向 */ int vp = w->h; /* 纵向 */
int off = w->scroll; 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 (maxs < 0) maxs = 0;
if (off < 0) off = 0; if (off < 0) off = 0;
if (off > maxs) off = maxs; if (off > maxs) off = maxs;
int first = (item > 0) ? off / item : 0; int first = (item > 0) ? off / item : 0;
int last = (item > 0) ? (off + vp) / item + 1 : 0; int last = (item > 0) ? (off + vp) / item + 1 : 0;
if (first < 0) first = 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 vx0 = w->x, vy0 = w->y, vy1 = w->y + w->h;
int thh = meow_ui_text_height(th->font_body); int thh = meow_ui_text_height(th->font_body);
int pad = MEOW_LISTV_PAD, isz = MEOW_LISTV_ICON; int pad = MEOW_LISTV_PAD, isz = MEOW_LISTV_ICON;
int text_x = vx0 + pad + isz + pad; int text_x = vx0 + pad + ((w->show_icon) ? (isz + pad) : 0); /* 无图标则无图标间隙 */
int op_start = vx0 + (w->w * 2 / 3); /* 行右 1/3 = 操作区(§5.5 命中) */ int op_start = (w->show_right) ? (vx0 + (w->w * 2 / 3)) /* 行右 1/3 = 操作区 */
int text_maxw = op_start - pad - text_x; /* 标题/副标题可用宽(不压操作区) */ : (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; 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);
/* 图标(左) */ /* 空态占位(§5.5):count==0 且设了 empty_text → 视口内居中画文案。 */
uint32_t iccol = sel ? th->onPrimaryContainer if (count == 0 && w->empty_text[0]) {
: ((it.icon_kind == MEOW_LIST_ICON_FOLDER) ? th->primary int ty = vy0 + (w->h - thh) / 2;
: th->onSurfaceVariant); meow_ui_text_center(vx0 + w->w / 2, ty, w->empty_text, th->font_body,
int icy = ry + (item - isz) / 2; th->onSurfaceVariant);
if (icy + isz >= vy0 && icy < vy1) /* 图标不越视口边界才画(防泄漏) */ if (disabled) meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->onSurface, 90);
listv_draw_icon(th, vx0 + pad, icy, it.icon_kind, it.icon, iccol); break;
}
/* 标题 + 副标题(两行居中;无副标题=单行居中;越界不画防泄漏) */ if (w->vs_count && w->vs_bind) { /* 虚拟化 pool 模式 */
int ty; listv_ensure_pool(w); /* 防御:视口/行高变化时确保池容量(cap 已够则零开销保留绑定) */
if (it.subtitle && it.subtitle[0]) { if (w->vs_draw) { /* ③ 完全自绘:App 画整行(widget clip 已裁到列表视口) */
ty = ry + (item - (2 * thh + MEOW_LISTV_SUB_GAP)) / 2; for (int i = first; i < last; i++) {
if (it.title && ty >= vy0 && ty + thh <= vy1) int ry = w->y + (i * item - off);
listv_text_trunc(text_x, ty, it.title, text_maxw, th->font_body, textcol); if (ry >= vy1) break;
int sy = ty + thh + MEOW_LISTV_SUB_GAP; int st = 0;
if (sy >= vy0 && sy + thh <= vy1) if (i == w->sel && w->sel_mode != MEOW_LIST_SEL_NONE) st |= MEOW_LISTVIEW_ST_SELECTED;
listv_text_trunc(text_x, sy, it.subtitle, text_maxw, th->font_body, subcol); if (meow_input_touch_down() &&
} else { meow_input_touch_x() >= vx0 && meow_input_touch_x() < vx0 + w->w &&
ty = ry + (item - thh) / 2; meow_input_touch_y() >= ry && meow_input_touch_y() < ry + item)
if (it.title && ty >= vy0 && ty + thh <= vy1) st |= MEOW_LISTVIEW_ST_PRESSED;
listv_text_trunc(text_x, ty, it.title, text_maxw, th->font_body, textcol); 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) { uint32_t iccol = sel ? th->onPrimaryContainer
int ax = w->x + w->w - pad - 24; /* 右缘动作槽(约 24px) */ : ((it.icon_kind == MEOW_LIST_ICON_FOLDER) ? th->primary
if (ax >= op_start) : th->onSurfaceVariant);
listv_draw_right(th, ax, ry, item, it.right, it.on, subcol); 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); if (disabled) meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->onSurface, 90);
break; break;
+365
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
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<count),回收合法重绑——但**有界**,
* 每次 draw 至多重绑当前视口窗口(≤ win),绝不整表 N 重绑(可复用回收语义)。 */
int before2 = g_bind_total;
int win = (lvh / itemh) + 1; if (win > 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;
}