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:
MeowOS committed 2026-08-31 04:39:34 +08:00
1 parent 3f57d7fd4f
commit 243148e440
8 files changed
+667 -155

No files matched your search

+129 -2
View File
@@ -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" };