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:
1 parent
85e1d03157
commit
9d54cf89c5
4 files changed
+797
-69
No files matched your search
@@ -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
|
||||
|
||||
+327
-68
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user