feat(sdk): 结构化列表控件 W_LISTVIEW (agent-186) — MEOW_APP_SDK §5.5 A②落地: 图标(内置folder/file符号或img)+标题+副标题+右侧操作(arrow/switch/check/more), 单选/多选/禁态, 行右1/3操作区命中, 视口裁剪, 全语义色; meow_ui_img_scaled 弱引用防二进制膨胀; files App 自绘列表完整迁移(长按留App层)
meow_ui.a ece0ecdb→4335c33a; hello 07b1ee37→07c0c5a5; files 6f6b0a6d→22b6b606 宿主: widgets(含4条listview新断言)/p1p2p3/ime_fix/test_fsx 全 PASS
This commit is contained in:
1 parent
d9c5791a47
commit
44685808e7
4 files changed
+558
-5
No files matched your search
@@ -567,6 +567,78 @@ typedef void (*meow_list_tap_fn)(int idx, void *ctx);
|
||||
void meow_list_set_on_tap(meow_widget *w, meow_list_tap_fn cb, void *ctx); /* 命中行→回调 */
|
||||
int meow_list_tap_at(meow_widget *w, int x, int y); /* 命中行:置选中+回调,返回 idx/-1 */
|
||||
|
||||
/* ============================================================ */
|
||||
/* §5.5 设计模板 A② 结构化列表(W_LISTVIEW):行=图标+标题+副标题+右侧操作 + 选中
|
||||
* ------------------------------------------------------------
|
||||
* 在既有「label-only」列表(meow_list_create/lazycolumn,行为不变)之上做**结构化扩展**:
|
||||
* 行数据源回调逐行给出 meow_list_item(图标 / 标题 / 副标题 / 右侧操作 / 可选中),容器负责
|
||||
* 绘制/滚动/选中/命中;支持单选/多选/无选择。向后兼容:既有 meow_list_* 全部保留不动。
|
||||
* 颜色全部取 meow_theme_get() 语义色(日/夜/style 自动),不硬编码。
|
||||
* 命中:meow_listview_hit 返回行号(含滚动换算 + 禁用行不响应);行右 1/3 为右侧操作区
|
||||
* (meow_listview_hit_op 另判)。抬手用最新坐标(goodix 免疫,参照既有 W_LIST 习惯)。 */
|
||||
/* ============================================================ */
|
||||
|
||||
/* 选中模式:无(纯入口列表)/ 单选 / 多选。 */
|
||||
typedef enum { MEOW_LIST_SEL_NONE = 0, MEOW_LIST_SEL_SINGLE, MEOW_LIST_SEL_MULTI } meow_list_sel;
|
||||
|
||||
/* 行图标:meow_img*(MEOW_LIST_ICON_IMG)或内置符号(FOLDER/FILE,无图 App 用)。 */
|
||||
typedef enum {
|
||||
MEOW_LIST_ICON_NONE = 0, /* 无图标 */
|
||||
MEOW_LIST_ICON_IMG, /* 用 item->icon(meow_img*) */
|
||||
MEOW_LIST_ICON_FOLDER, /* 内置目录符号(填充矩形+页签) */
|
||||
MEOW_LIST_ICON_FILE, /* 内置文件符号(空心矩形+折角) */
|
||||
} meow_list_icon_kind;
|
||||
|
||||
/* 行右侧操作类型:无 / 箭头 / 开关 / 复选 / 详情钮(⋯)。 */
|
||||
typedef enum {
|
||||
MEOW_LIST_RIGHT_NONE = 0, /* 无右侧操作 */
|
||||
MEOW_LIST_RIGHT_ARROW, /* 箭头 › */
|
||||
MEOW_LIST_RIGHT_SWITCH, /* 开关(on/off = item->on) */
|
||||
MEOW_LIST_RIGHT_CHECK, /* 复选标记 */
|
||||
MEOW_LIST_RIGHT_MORE, /* ⋯ 详情钮 */
|
||||
} meow_list_right;
|
||||
|
||||
/* 单行数据(回调填充到栈上,容器不长期持有)。title 必填;subtitle 可空。 */
|
||||
typedef struct meow_list_item {
|
||||
const meow_img *icon; /* 可选(icon_kind==MEOW_LIST_ICON_IMG 时用) */
|
||||
int icon_kind; /* MEOW_LIST_ICON_*(NONE=无图标) */
|
||||
const char *title; /* 主文本(必填,NULL 视为空行) */
|
||||
const char *subtitle; /* 副文本,可空 */
|
||||
int right; /* MEOW_LIST_RIGHT_* 右侧操作类型 */
|
||||
int on; /* right==SWITCH 时开关态(0/1) */
|
||||
int enabled; /* 1=可选中/可交互;0=变淡不响应命中 */
|
||||
} meow_list_item;
|
||||
|
||||
/* 行数据源回调:idx 有效时填充 out 并返回 1;越界返回 0。每行轻量填充(不做 IO,§5.5)。 */
|
||||
typedef int (*meow_list2_source)(int idx, meow_list_item *out, void *ctx);
|
||||
|
||||
/* 创建:视口(x,y,w,h) + 行数 count + 行数据源 + 选中模式。行高默认 56(可用 set_item_h 改)。 */
|
||||
meow_widget *meow_listview_create(int x, int y, int w, int h, int count,
|
||||
meow_list2_source src, void *ctx, meow_list_sel mode);
|
||||
/* 行高(固定;越界/<=0 忽略)。 */
|
||||
int meow_listview_set_item_h(meow_widget *lv, int item);
|
||||
/* 行数(搜索过滤/懒加载/目录切换时动态更新):截断 sel 到新范围 + clamp scroll。 */
|
||||
void meow_listview_set_count(meow_widget *lv, int count);
|
||||
/* 滚动:set 越界 clamp 到 [0, max_scroll];by 相对滚动(App on_swipe/touch_move 用)。 */
|
||||
void meow_listview_set_scroll(meow_widget *lv, int offset);
|
||||
void meow_listview_scroll_by(meow_widget *lv, int dy);
|
||||
int meow_listview_scroll(meow_widget *lv);
|
||||
int meow_listview_max_scroll(meow_widget *lv); /* max(0, count*item_h - viewport_h) */
|
||||
/* 选中集(mode 语义):
|
||||
* NONE :忽略;SINGLE:set_sel(idx) 设/清(idx<0 清除);MULTI:set_sel(idx) 翻转 idx。
|
||||
* get_sel 把当前选中下标写入 out[0..max),返回写入数;sel_count 返回选中总数。 */
|
||||
void meow_listview_set_sel(meow_widget *lv, int idx);
|
||||
int meow_listview_get_sel(meow_widget *lv, int *out, int max);
|
||||
int meow_listview_sel_count(meow_widget *lv);
|
||||
/* 命中:返回行号(含滚动换算;禁用行/越界返回 -1);行右 1/3 = 右侧操作区(另判)。 */
|
||||
int meow_listview_hit(meow_widget *lv, int x, int y);
|
||||
int meow_listview_hit_op(meow_widget *lv, int x, int y); /* 1=点在行右 1/3 操作区 */
|
||||
/* 行点击回调(idx=行号;op:0=行体,1=右侧操作区)。 */
|
||||
typedef void (*meow_listview_tap_fn)(int idx, int op, void *ctx);
|
||||
void meow_listview_set_on_tap(meow_widget *lv, meow_listview_tap_fn cb, void *ctx);
|
||||
/* 命中行:置选中 + 回调(若设)+ 返回 idx;未命中返回 -1。 */
|
||||
int meow_listview_tap_at(meow_widget *lv, int x, int y);
|
||||
|
||||
/* ============================================================ */
|
||||
/* §5.8.6 模态(App 层,非系统叠加):Dialog / BottomSheet
|
||||
* 复用 §5.6 B③ 弹窗 / §5.7 B④ 底部面板模板(与 launcher s_dd_modal 同构),控件化。
|
||||
|
||||
+354
-1
@@ -14,7 +14,8 @@ enum { W_BTN = 0, W_SLIDER = 1, W_SWITCH = 2, W_LIST = 3,
|
||||
W_CUSTOM = 4, W_LAYOUT = 5, W_SPACER = 6,
|
||||
W_TEXTFIELD = 7, W_CARD = 8, W_DIVIDER = 9, W_BADGE = 10,
|
||||
W_CHECKBOX = 11, W_RADIO = 12, W_PROGRESS = 13,
|
||||
W_TOPAPPBAR = 14, W_NAVBAR = 15, W_TABBAR = 16, W_FAB = 17 };
|
||||
W_TOPAPPBAR = 14, W_NAVBAR = 15, W_TABBAR = 16, W_FAB = 17,
|
||||
W_LISTVIEW = 18 }; /* §5.5 结构化列表(行=图标+标题+副标题+右侧操作+选中) */
|
||||
|
||||
/* 布局子项数组上限(固定尺寸子项先做;超出静默拒收)。 */
|
||||
#define MEOW_LAYOUT_MAX_CHILD 32
|
||||
@@ -81,6 +82,14 @@ struct meow_widget {
|
||||
/* M2.71-bug8:W_LIST 行点击回调(可选)。 */
|
||||
meow_list_tap_fn tap_cb; /* 命中行回调(可 NULL) */
|
||||
void *tap_ctx; /* 回调 ctx */
|
||||
|
||||
/* §5.5 结构化列表(W_LISTVIEW):行=图标+标题+副标题+右侧操作+选中(calloc 归零,安全) */
|
||||
meow_list2_source src2; /* 行数据源(meow_list_item) */
|
||||
int sel_mode; /* MEOW_LIST_SEL_* */
|
||||
uint32_t *sel_bits; /* 多选位图(sel_bits_n 个字;SINGLE 不分配) */
|
||||
int sel_bits_n; /* 位图字数 */
|
||||
meow_listview_tap_fn lv_tap_cb; /* 行点击回调(idx, op) */
|
||||
void *lv_tap_ctx; /* 回调 ctx */
|
||||
};
|
||||
|
||||
/* ---- §5.8 batch1 内部辅助:opts 取值(opts_used==0 时按控件内建默认)----
|
||||
@@ -328,6 +337,7 @@ void meow_widget_free(meow_widget *w)
|
||||
if (!w) return;
|
||||
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 多选位图 */
|
||||
free(w);
|
||||
}
|
||||
|
||||
@@ -881,6 +891,271 @@ int meow_list_tap_at(meow_widget *w, int x, int y)
|
||||
return idx;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* §5.5 结构化列表(W_LISTVIEW):行=图标+标题+副标题+右侧操作+选中
|
||||
* ------------------------------------------------------------
|
||||
* 行数据源(meow_list2_source)逐行填充 meow_list_item;容器只画可见行(虚拟化)+ 视口裁剪
|
||||
* (M2.73 纪律:只画视口内行/分隔线/文本,无标题栏/底部泄漏);局部 dirty;行高固定;首/末行
|
||||
* 钳制。颜色全部取 meow_theme_get() 语义色(日/夜/style 自动),禁止硬编码。 */
|
||||
/* ============================================================ */
|
||||
#define MEOW_LISTV_ICON 24 /* 图标盒尺寸(px) */
|
||||
#define MEOW_LISTV_PAD 16 /* 行内左右 padding(px) */
|
||||
#define MEOW_LISTV_SUB_GAP 2 /* 标题与副标题间隙(px) */
|
||||
|
||||
/* meow_ui_img_scaled 弱引用:W_LISTVIEW 的 MEOW_LIST_ICON_IMG 分支用它绘制图片图标。
|
||||
* 弱引用让「只用内置符号图标、不解码图片」的 App 不被牵连进 meow_ui_img.o/stb_image.o
|
||||
* (静态库惰性链接:无强引用则不拉入 stb 解码器,避免所有 App 无谓膨胀 ~77KB)。
|
||||
* 宿主/设备需图片图标的 App 经 meow_ui_img_load_* 强引用 meow_ui_img.o → 此处自动解析。 */
|
||||
extern void meow_ui_img_scaled(int x, int y, int w, int h, meow_img *img) __attribute__((weak));
|
||||
|
||||
static int listv_item_h(const meow_widget *w) { return (w->item_h > 0) ? w->item_h : 56; }
|
||||
|
||||
/* 文本截断:超出 maxw 加 "..."(复用 browser_text_trunc 的逐字符宽度累加)。 */
|
||||
static void listv_text_trunc(int x, int y, const char *s, int maxw, int size, uint32_t col)
|
||||
{
|
||||
if (!s) return;
|
||||
int full = meow_ui_text_width(s, size);
|
||||
if (full <= maxw) { meow_ui_text(x, y, s, size, col); return; }
|
||||
const char *dots = "...";
|
||||
int dw = meow_ui_text_width(dots, size);
|
||||
char tmp[128];
|
||||
int i;
|
||||
for (i = 0; s[i] && i < (int)sizeof tmp - 4; i++) {
|
||||
tmp[i] = s[i];
|
||||
tmp[i + 1] = '\0';
|
||||
if (meow_ui_text_width(tmp, size) > maxw - dw) break;
|
||||
}
|
||||
memcpy(tmp + i, dots, 4);
|
||||
meow_ui_text(x, y, tmp, size, col);
|
||||
}
|
||||
|
||||
/* 行图标绘制(内置符号 / meow_img)。col=图标前景(选中/未选中由调用方定)。 */
|
||||
static void listv_draw_icon(const meow_theme *th, int x, int y, int kind,
|
||||
const meow_img *img, uint32_t col)
|
||||
{
|
||||
(void)th;
|
||||
int s = MEOW_LISTV_ICON;
|
||||
if (kind == MEOW_LIST_ICON_IMG && img && meow_ui_img_scaled) { /* 弱符号非空才画(未链 stb 则跳过) */
|
||||
meow_ui_img_scaled(x, y, s, s, (meow_img *)img); /* img 为 opaque 句柄,const 丢弃 */
|
||||
return;
|
||||
}
|
||||
if (kind == MEOW_LIST_ICON_FOLDER) {
|
||||
meow_ui_fill_rect(x, y + 6, s, s - 6, col); /* 主体 */
|
||||
meow_ui_fill_rect(x, y + 2, 12, 6, col); /* 页签 */
|
||||
} else if (kind == MEOW_LIST_ICON_FILE) {
|
||||
meow_ui_draw_rect(x + 4, y + 2, s - 8, s - 4, col);/* 页体 */
|
||||
meow_ui_fill_rect(x + 4, y + 2, 7, 4, col); /* 文件名横条 */
|
||||
}
|
||||
}
|
||||
|
||||
/* 行右侧操作区绘制(箭头/开关/复选/⋯)。x=动作槽左缘;col=前景。 */
|
||||
static void listv_draw_right(const meow_theme *th, int x, int y, int row_h,
|
||||
int right, int on, uint32_t col)
|
||||
{
|
||||
switch (right) {
|
||||
case MEOW_LIST_RIGHT_ARROW: {
|
||||
int cx = x + 4, cy = y + row_h / 2;
|
||||
meow_gfx_line(cx - 4, cy - 6, cx + 4, cy, 2, col);
|
||||
meow_gfx_line(cx + 4, cy, cx - 4, cy + 6, 2, col);
|
||||
break;
|
||||
}
|
||||
case MEOW_LIST_RIGHT_SWITCH: {
|
||||
int sw = 36, sh = 20;
|
||||
int sx = x, sy = y + (row_h - sh) / 2;
|
||||
meow_gfx_fill_round(sx, sy, sw, sh, sh / 2, on ? th->primary : th->onSurfaceVariant);
|
||||
int kr = sh / 2 - 3;
|
||||
int kx = on ? sx + sw - kr * 2 - 3 : sx + 3;
|
||||
meow_gfx_fill_round(kx, sy + 3, kr * 2, kr * 2, kr, on ? th->onPrimary : th->surface);
|
||||
break;
|
||||
}
|
||||
case MEOW_LIST_RIGHT_CHECK: {
|
||||
int cx = x + 2, cy = y + row_h / 2;
|
||||
meow_gfx_line(cx - 6, cy + 2, cx - 1, cy + 7, 2, col);
|
||||
meow_gfx_line(cx - 1, cy + 7, cx + 7, cy - 6, 2, col);
|
||||
break;
|
||||
}
|
||||
case MEOW_LIST_RIGHT_MORE: {
|
||||
int cx = x;
|
||||
int cy0 = y + row_h / 2 - 8;
|
||||
for (int d = 0; d < 3; d++)
|
||||
meow_ui_fill_rect(cx, cy0 + d * 6, 4, 4, col);
|
||||
break;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
/* 多选位图扩容(MULTI 且 idx 超出当前容量时;SINGLE 不分配)。 */
|
||||
static void listv_bitmap_grow(meow_widget *w, int idx)
|
||||
{
|
||||
int need = (idx + 1 + 31) / 32;
|
||||
if (need <= w->sel_bits_n) return;
|
||||
uint32_t *nb = (uint32_t *)calloc((size_t)need, sizeof(uint32_t));
|
||||
if (!nb) return;
|
||||
if (w->sel_bits && w->sel_bits_n)
|
||||
memcpy(nb, w->sel_bits, (size_t)w->sel_bits_n * sizeof(uint32_t));
|
||||
free(w->sel_bits);
|
||||
w->sel_bits = nb;
|
||||
w->sel_bits_n = need;
|
||||
}
|
||||
|
||||
meow_widget *meow_listview_create(int x, int y, int w, int h, int count,
|
||||
meow_list2_source src, void *ctx, meow_list_sel mode)
|
||||
{
|
||||
meow_widget *t = (meow_widget *)calloc(1, sizeof *t);
|
||||
if (!t) return NULL;
|
||||
t->type = W_LISTVIEW; t->x = x; t->y = y; t->w = w; t->h = h;
|
||||
t->owner = meow_ui_activity_active();
|
||||
t->state = MEOW_WS_ENABLED;
|
||||
t->count = count > 0 ? count : 0; t->src2 = src; t->ctx = ctx;
|
||||
t->sel = -1; t->item_h = 56; t->scroll = 0;
|
||||
t->sel_mode = (mode >= MEOW_LIST_SEL_NONE && mode <= MEOW_LIST_SEL_MULTI) ? mode : MEOW_LIST_SEL_NONE;
|
||||
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));
|
||||
}
|
||||
meow_draw_widget_register(t);
|
||||
return t;
|
||||
}
|
||||
|
||||
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); }
|
||||
return w->item_h;
|
||||
}
|
||||
void meow_listview_set_count(meow_widget *w, int count)
|
||||
{
|
||||
if (!w || w->type != W_LISTVIEW) return;
|
||||
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) { /* 重分配多选位图 */
|
||||
free(w->sel_bits); w->sel_bits = NULL; w->sel_bits_n = 0;
|
||||
int need = (count + 31) / 32;
|
||||
if (need > 0) {
|
||||
w->sel_bits_n = need;
|
||||
w->sel_bits = (uint32_t *)calloc((size_t)need, sizeof(uint32_t));
|
||||
}
|
||||
}
|
||||
int ms = meow_listview_max_scroll(w); /* 行数减少时 clamp scroll */
|
||||
if (w->scroll > ms) w->scroll = ms;
|
||||
meow_gfx_mark_dirty(w->x, w->y, w->w, w->h);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void meow_listview_set_scroll(meow_widget *w, int offset)
|
||||
{
|
||||
if (!w || w->type != W_LISTVIEW) return;
|
||||
int ms = meow_listview_max_scroll(w);
|
||||
if (offset < 0) offset = 0;
|
||||
if (offset > ms) offset = ms;
|
||||
if (offset == w->scroll) return;
|
||||
w->scroll = offset;
|
||||
meow_gfx_mark_dirty(w->x, w->y, w->w, w->h);
|
||||
}
|
||||
void meow_listview_scroll_by(meow_widget *w, int dy) { if (w) meow_listview_set_scroll(w, w->scroll + dy); }
|
||||
int meow_listview_scroll(meow_widget *w) { return (w && w->type == W_LISTVIEW) ? w->scroll : 0; }
|
||||
|
||||
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 首/末行钳制) */
|
||||
if (w->sel_mode == MEOW_LIST_SEL_SINGLE) {
|
||||
if (idx == w->sel) return;
|
||||
w->sel = idx; /* idx<0 = 清除 */
|
||||
meow_gfx_mark_dirty(w->x, w->y, w->w, w->h);
|
||||
} else { /* MULTI:翻转 idx */
|
||||
if (idx < 0) {
|
||||
if (w->sel_bits) memset(w->sel_bits, 0, (size_t)w->sel_bits_n * sizeof(uint32_t));
|
||||
meow_gfx_mark_dirty(w->x, w->y, w->w, w->h);
|
||||
return;
|
||||
}
|
||||
listv_bitmap_grow(w, idx);
|
||||
if (!w->sel_bits) return;
|
||||
w->sel_bits[idx >> 5] ^= (1u << (idx & 31));
|
||||
meow_gfx_mark_dirty(w->x, w->y, w->w, w->h);
|
||||
}
|
||||
}
|
||||
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;
|
||||
if (w->sel_mode == MEOW_LIST_SEL_MULTI) {
|
||||
for (int i = 0; i < w->count && 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;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
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++)
|
||||
if (w->sel_bits && (w->sel_bits[i >> 5] & (1u << (i & 31)))) n++;
|
||||
return n;
|
||||
}
|
||||
return (w->sel >= 0) ? 1 : 0;
|
||||
}
|
||||
|
||||
/* 命中:返回行号(含滚动换算;禁用行/越界 -1)。抬手用最新坐标(goodix 免疫,调用方传抬手落点)。 */
|
||||
int meow_listview_hit(meow_widget *w, int x, int y)
|
||||
{
|
||||
if (!w || w->type != W_LISTVIEW) 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 idx = (y - w->y + w->scroll) / item;
|
||||
if (idx < 0 || idx >= w->count) 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;
|
||||
}
|
||||
return idx;
|
||||
}
|
||||
int meow_listview_hit_op(meow_widget *w, int x, int y)
|
||||
{
|
||||
int idx = meow_listview_hit(w, x, y);
|
||||
if (idx < 0) return 0;
|
||||
int opx = w->x + (w->w * 2 / 3); /* 行右 1/3 = 操作区(§5.5) */
|
||||
if (x < opx) return 0;
|
||||
if (w->src2) {
|
||||
meow_list_item it;
|
||||
memset(&it, 0, sizeof it);
|
||||
if (!w->src2(idx, &it, w->ctx)) return 0;
|
||||
if (it.right == MEOW_LIST_RIGHT_NONE) return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
void meow_listview_set_on_tap(meow_widget *w, meow_listview_tap_fn cb, void *ctx)
|
||||
{
|
||||
if (!w || w->type != W_LISTVIEW) return;
|
||||
w->lv_tap_cb = cb; w->lv_tap_ctx = ctx;
|
||||
}
|
||||
int meow_listview_tap_at(meow_widget *w, int x, int y)
|
||||
{
|
||||
int idx = meow_listview_hit(w, x, y);
|
||||
if (idx < 0) return -1;
|
||||
int op = meow_listview_hit_op(w, x, y);
|
||||
meow_listview_set_sel(w, idx);
|
||||
if (w->lv_tap_cb) w->lv_tap_cb(idx, op, w->lv_tap_ctx);
|
||||
return idx;
|
||||
}
|
||||
|
||||
static int hit(meow_widget *w, int x, int y)
|
||||
{
|
||||
if (!w) return 0;
|
||||
@@ -1071,6 +1346,84 @@ void meow_widget_draw(meow_widget *w)
|
||||
if (disabled) meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->onSurface, 90);
|
||||
break;
|
||||
}
|
||||
case W_LISTVIEW: {
|
||||
/* §5.5 结构化列表(纵向 LazyColumn):只画可见行(虚拟化)+ 视口裁剪(M2.73 纪律)。
|
||||
* 行 = 图标(左) + 标题 + 副标题(次要色) + 右侧操作区;选中行 = primaryContainer /
|
||||
* onPrimaryContainer(§5.8.3);禁用行变淡不响应(命中由 hit 层保证)。全语义色。 */
|
||||
int item = listv_item_h(w);
|
||||
int vp = w->h; /* 纵向 */
|
||||
int off = w->scroll;
|
||||
int maxs = w->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;
|
||||
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; /* 标题/副标题可用宽(不压操作区) */
|
||||
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);
|
||||
|
||||
/* 标题 + 副标题(两行居中;无副标题=单行居中;越界不画防泄漏) */
|
||||
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);
|
||||
}
|
||||
if (disabled) meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->onSurface, 90);
|
||||
break;
|
||||
}
|
||||
case W_CUSTOM:
|
||||
/* SDK 不替自定义画布画背景/状态层;App 在 render_cb 里自绘(th 供取色),
|
||||
* 可读 meow_widget_state(状态位)/ meow_widget_opts_get(通用修饰)自行叠。 */
|
||||
|
||||
+3
-2
@@ -8,8 +8,9 @@
|
||||
* gcc -Wall -Wextra -o test_ime_fix test_ime_fix.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_ui_draw.c meow_ui_view.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread
|
||||
* meow_ui_draw.c meow_ui_view.c meow_ui_img.c stb_image.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* (meow_ui_widget.c 新 W_LISTVIEW 引用 meow_ui_img_scaled → 需链 meow_ui_img.c+stb_image.c+-lm)
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
|
||||
+129
-2
@@ -5,16 +5,74 @@
|
||||
* gcc -o test_meow_ui_widgets test_meow_ui_widgets.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_ui_draw.c meow_ui_view.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread
|
||||
* meow_ui_draw.c meow_ui_view.c meow_ui_img.c stb_image.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 运行:MEOW_UI_DUMP=/tmp/b2.ppm ./test_meow_ui_widgets (可选,dump 屏幕)
|
||||
*
|
||||
* 注:W_LISTVIEW 的 MEOW_LIST_ICON_IMG 分支调用 meow_ui_img_scaled → 宿主链 meow_ui_img.c+stb_image.c+-lm。
|
||||
* 结构化列表断言用 back_buf 像素(region_has/px_near),画完即读,非目检。
|
||||
*/
|
||||
#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 <assert.h>
|
||||
|
||||
/* ---- 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);
|
||||
}
|
||||
/* 区域内含「接近 target 色」的像素数(>0 即该色被真实绘出)。 */
|
||||
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)
|
||||
|
||||
/* 结构化列表(W_LISTVIEW)测试数据源 */
|
||||
typedef struct { const char *title; const char *sub; int ik; int right; int enabled; } lvrow_t;
|
||||
static lvrow_t g_lvrows[4] = {
|
||||
{ "Row A", "sub A", MEOW_LIST_ICON_FOLDER, MEOW_LIST_RIGHT_MORE, 1 },
|
||||
{ "Row B", "sub B", MEOW_LIST_ICON_FILE, MEOW_LIST_RIGHT_NONE, 1 },
|
||||
{ "Row C", "sub C", MEOW_LIST_ICON_NONE, MEOW_LIST_RIGHT_ARROW, 0 }, /* 禁用 */
|
||||
{ "Row D", "sub D", MEOW_LIST_ICON_FOLDER, MEOW_LIST_RIGHT_CHECK, 1 },
|
||||
};
|
||||
static int lv_src(int idx, meow_list_item *out, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
if (idx < 0 || idx >= 4) return 0;
|
||||
out->icon_kind = g_lvrows[idx].ik;
|
||||
out->title = g_lvrows[idx].title;
|
||||
out->subtitle = g_lvrows[idx].sub;
|
||||
out->right = g_lvrows[idx].right;
|
||||
out->on = 1;
|
||||
out->enabled = g_lvrows[idx].enabled;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* 测试数据源(列表容器) */
|
||||
static int list_src(int idx, char *label, int len, void *ctx)
|
||||
{
|
||||
@@ -158,6 +216,75 @@ int main(void)
|
||||
assert(meow_list_hit_index(lr, 40, 660) == ((40 - 20 + 200) / 60));
|
||||
printf("PASS lazy list: scroll clamp/virtualize/hit_index/tap_at/select (col+row)\n");
|
||||
|
||||
/* ---------- 结构化列表(W_LISTVIEW):行渲染像素 + 命中 + 滚动裁剪 ----------
|
||||
* lv 位于 (20,240,320,112)item_h=56 → 行0 y240..296、行1 296..352。
|
||||
* 断言全部「画完读 back_buf 像素」(region_has/px_near),非目检。 */
|
||||
meow_widget *lv = W(meow_listview_create(20, 240, 320, 112, 4, lv_src, NULL, MEOW_LIST_SEL_SINGLE));
|
||||
assert(meow_listview_set_item_h(lv, 56) == 56);
|
||||
const meow_theme *lth = meow_theme_get();
|
||||
int lvx = 20, lvy = 240, lvw = 320, lvh = 112, itemh = 56;
|
||||
int textx = lvx + 16 + 24 + 16; /* 文本起 x = 76 */
|
||||
int op_start = lvx + (lvw * 2 / 3); /* 行右 1/3 操作区起 = 20+213=233 */
|
||||
int sb_r, sb_g, sb_b, os_r, os_g, os_b, pc_r, pc_g, pc_b;
|
||||
int ospv_r, ospv_g, ospv_b, prim_r, prim_g, prim_b;
|
||||
UNPK(lth->bg, sb_r, sb_g, sb_b);
|
||||
UNPK(lth->onSurface, os_r, os_g, os_b);
|
||||
UNPK(lth->primaryContainer, pc_r, pc_g, pc_b);
|
||||
UNPK(lth->onSurfaceVariant, ospv_r, ospv_g, ospv_b);
|
||||
UNPK(lth->primary, prim_r, prim_g, prim_b);
|
||||
int thh = meow_ui_text_height(lth->font_body); /* 20(宿主 5x7) */
|
||||
|
||||
/* 行 0 的标题/副标题 y(含 subtitle 的两行居中):ty=240+(56-42)/2=247 */
|
||||
int r0_ty = lvy + (itemh - (2 * thh + 2)) / 2;
|
||||
int r0_sy = r0_ty + thh + 2;
|
||||
|
||||
/* 1) 行内容渲染:清底 + 只画列表,断言图标/标题/副标题/右侧 ⋯ 真实像素 */
|
||||
meow_ui_clear(lth->bg);
|
||||
meow_widget_draw(lv);
|
||||
assert(region_has(lvx + 16, lvy + (itemh - 24) / 2, 24, 24, prim_r, prim_g, prim_b) > 100);
|
||||
assert(region_has(textx, r0_ty, 80, thh, os_r, os_g, os_b) > 2);
|
||||
assert(region_has(textx, r0_sy, 80, thh, ospv_r, ospv_g, ospv_b) > 2);
|
||||
assert(region_has(lvx + lvw - 16 - 24, lvy + (itemh - 20) / 2, 8, 20, ospv_r, ospv_g, ospv_b) > 20);
|
||||
printf("PASS listview: row render (icon/title/subtitle/right-more) pixels\n");
|
||||
|
||||
/* 2) 选中态:行 1 → primaryContainer 高亮;行 0 无高亮(bg) */
|
||||
meow_listview_set_sel(lv, 1);
|
||||
meow_ui_clear(lth->bg);
|
||||
meow_widget_draw(lv);
|
||||
assert(px_near(28, lvy + itemh + 48, pc_r, pc_g, pc_b)); /* 行1带底部空白 = primaryContainer */
|
||||
assert(px_near(28, lvy + 48, sb_r, sb_g, sb_b)); /* 行0带空白 = bg(未选中无高亮) */
|
||||
int selout[4];
|
||||
assert(meow_listview_get_sel(lv, selout, 4) == 1 && selout[0] == 1);
|
||||
assert(meow_listview_sel_count(lv) == 1);
|
||||
printf("PASS listview: selected row highlight + get_sel/sel_count\n");
|
||||
|
||||
/* 3) 命中:行号 + 右侧操作区分 + 禁用行不命中 */
|
||||
assert(meow_listview_hit(lv, 28, lvy + 20) == 0); /* 行0 体 */
|
||||
assert(meow_listview_hit_op(lv, 28, lvy + 20) == 0); /* 体非操作区 */
|
||||
assert(meow_listview_hit_op(lv, op_start + 10, lvy + 20) == 1); /* 行0 右1/3=操作区 */
|
||||
meow_listview_set_sel(lv, -1);
|
||||
/* 滚动到 3、4 行行内命中(scroll=112 → 行2 y240、行3 y296) */
|
||||
meow_listview_set_scroll(lv, 112);
|
||||
assert(meow_listview_scroll(lv) == 112);
|
||||
assert(meow_listview_max_scroll(lv) == 4 * 56 - 112);
|
||||
assert(meow_listview_hit(lv, 28, lvy + 28) == -1); /* 行2 禁用 → 不命中 */
|
||||
assert(meow_listview_hit(lv, 28, lvy + itemh + 28) == 3); /* 行3 命中 */
|
||||
printf("PASS listview: hit row/hit_op/disabled-not-hit\n");
|
||||
|
||||
/* 4) 滚动视口裁剪:滚到 112 后画,视口外(上缘/下缘)无泄漏(保持 bg) */
|
||||
meow_ui_clear(lth->bg);
|
||||
meow_widget_draw(lv);
|
||||
assert(px_near(21, lvy - 1, sb_r, sb_g, sb_b)); /* 视口上缘外 = bg(无上漏) */
|
||||
assert(px_near(21, lvy + lvh, sb_r, sb_g, sb_b)); /* 视口下缘外 = bg(无下漏) */
|
||||
/* 视口内 行3 标题真实渲染(行3 y=296,标题 y=303) */
|
||||
assert(region_has(textx, lvy + itemh + (itemh - (2 * thh + 2)) / 2,
|
||||
80, thh, os_r, os_g, os_b) > 2);
|
||||
/* 行 2 禁用行「变淡」:draw 后其左 padding 像素应被 dim 覆盖(偏离纯 bg) */
|
||||
assert(!px_near(28, lvy + 28, sb_r, sb_g, sb_b));
|
||||
printf("PASS listview: scroll clip no-leak + disabled-row dim\n");
|
||||
meow_listview_set_scroll(lv, 0);
|
||||
meow_listview_set_sel(lv, -1);
|
||||
|
||||
/* ---------- Dialog / BottomSheet 纯逻辑(create/set/add;show 阻塞故不调) ---------- */
|
||||
meow_dialog *dlg = meow_dialog_create(MEOW_DLG_SELECT, "Pick one");
|
||||
const char *items[] = { "Alpha", "Beta", "Gamma" };
|
||||
|
||||
Reference in new issue
Block a user