TopAppBar 动作项此前用字体码点(⚙↻⌕)绘制,4k 字库无这些码点→豆腐。改走位图: - sb_icons.h 追加 15 个 Material Symbols Rounded 动作位图(settings/refresh/search/ play_arrow/pause/skip_next/skip_previous/arrow_back/close/add/check/lyrics/ queue_music/download/heart),FILL=1 实心与状态栏一致;sb_icons[] 只追加(0-14 不变), 动作索引 15-29。 - 生成脚本 tools/gen_action_icons.py(cairosvg svg→32x32 单通道 alpha,SVG 缓存 icons_src/ 可复现、断网可跑)。 - meow_ui_gfx.c 抽取图标 blit 像素核心 icon_blit_px,新增 meow_gfx_blit_icon_ink (控件路径不标顶栏脏区);meow_gfx_blit_icon 状态栏路径行为不变。 - meow_ui_widget.c W_TOPAPPBAR 项支持位图模式:新 API meow_topappbar_set_item_icon (w, idx, icon_idx),绘制取主题 onSurface/按压向栏底色淡 12%,文本项行为不变; hit 用统一项宽。 - 公共头 widgets.h 加 MEOW_ICON_*(15-29) 枚举与 set_item_icon 声明;internal.h 声明 meow_gfx_blit_icon_ink。 - 宿主测试(test_meow_ui_widgets)加 topbar 位图图标绘制/命中/按压变淡 + 状态栏 blit 回归断言;test_umbrella 加 MEOW_ICON_* 编译期锁;MEOW_APP_SDK.md 补说明+索引表。 - 验证:宿主测试 ALL PASS(14项);交叉 zig 构建 0 warning;meow_ui.a 新 md5 1d69cdae55ab5dde3a98f5e2124f3656。
517 lines
27 KiB
C
517 lines
27 KiB
C
/* test_meow_ui_widgets.c — 宿主验证 §5.8.6 Material 子集纯逻辑(create/hit/state/滚动/选中)+
|
||
* 各组件绘制(5x7 字库兜底)dump PPM 目检。不依赖 /dev/fb0/字库(meow_font 打桩)。
|
||
*
|
||
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global):
|
||
* 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_str.c \
|
||
* meow_ui_draw.c meow_ui_view.c meow_ui_img.c meow_notify_ipc.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 "meow_ui_view.h" /* 任务② view 递归 + on_event 路由断言 */
|
||
#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)
|
||
{
|
||
(void)ctx;
|
||
if (idx < 0 || idx >= 100) return 0;
|
||
snprintf(label, (size_t)len, "item %d", idx);
|
||
return 1;
|
||
}
|
||
|
||
static meow_widget *g_w[64];
|
||
static int g_wn = 0;
|
||
static meow_widget *W(meow_widget *w) { g_w[g_wn++] = w; return w; }
|
||
|
||
static void free_all(void)
|
||
{
|
||
for (int i = 0; i < g_wn; i++) meow_widget_free(g_w[i]);
|
||
g_wn = 0;
|
||
}
|
||
|
||
/* §6.3 通用长按测试回调(开闸长按判定用;事件本身经 meow_input_pop 断言)。 */
|
||
static void lp_recv(int x, int y) { (void)x; (void)y; }
|
||
|
||
/* ---- 任务① 绘制期 clip:W_CUSTOM 越界绘制被裁掉(render 回调经 flag 切换越界)----
|
||
* 控件几何硬编码为 (40,40,80,40)(与下方创建一致):meow_widget 是对外不透明类型,
|
||
* 测试经 meow_widget_custom_create(40,40,80,40,...) 建在同址,回调按常量绘制。 */
|
||
static int g_custom_oob = 1; /* 1=画越界实心块+越界文本;0=只画控件内 */
|
||
static void clip_oob_render(meow_widget *w, const meow_theme *th, void *ctx)
|
||
{
|
||
(void)w; (void)th; (void)ctx;
|
||
meow_ui_fill_rect(45, 45, 20, 20, 0xFF0000); /* 控件 (40,40,80,40) 内:必画红块 */
|
||
if (g_custom_oob) {
|
||
/* 越界(右/下,出控件矩形 [40..120)×[40..80)):应被 widget clip 裁掉 */
|
||
meow_ui_fill_rect(140, 40, 60, 40, 0xFF0000);
|
||
meow_ui_text(160, 100, "OOB", 20, 0xFF0000);
|
||
}
|
||
}
|
||
|
||
/* ---- 任务② view 递归 enter/leave + on_event 子>父:trace 顺序 + 命中计数 ---- */
|
||
static int g_vtrace[16]; static int g_vtrace_n = 0;
|
||
static void vtrace_enter(meow_view *v, void *ctx) { (void)ctx; g_vtrace[g_vtrace_n++] = v->id; }
|
||
static void vtrace_leave(meow_view *v, void *ctx) { (void)ctx; g_vtrace[g_vtrace_n++] = -v->id; }
|
||
static int g_ev_child_hits = 0, g_ev_parent_hits = 0;
|
||
static int vev_child(meow_view *v, int ev, int a, int b, void *ctx)
|
||
{ (void)v; (void)ctx; if (ev == 7 && a == 10 && b == 10) { g_ev_child_hits++; return 1; } return 0; }
|
||
static int vev_parent(meow_view *v, int ev, int a, int b, void *ctx)
|
||
{ (void)v; (void)ctx; if (ev == 7) { g_ev_parent_hits++; return 1; } return 0; }
|
||
|
||
int main(void)
|
||
{
|
||
setenv("MEOW_UI_DUMP", "/tmp/b2.ppm", 1);
|
||
meow_ui_opts o = { 480, 720 };
|
||
assert(meow_ui_init(&o) == 0);
|
||
int xres = meow_ui_width(), yres = meow_ui_height();
|
||
assert(xres == 480 && yres == 720);
|
||
|
||
/* ---------- Button 五型:create_variant/set_variant + 命中 ---------- */
|
||
meow_widget *bf = W(meow_btn_create_variant(10, 10, 200, 56, "FILLED", MEOW_BTN_FILLED));
|
||
meow_widget *bt = W(meow_btn_create_variant(10, 80, 200, 56, "TONAL", MEOW_BTN_TONAL));
|
||
meow_widget *bo = W(meow_btn_create_variant(10, 150, 200, 56, "OUTLINED", MEOW_BTN_OUTLINED));
|
||
meow_widget *bx = W(meow_btn_create_variant(10, 220, 200, 56, "TEXT", MEOW_BTN_TEXT));
|
||
meow_widget *be = W(meow_btn_create_variant(10, 290, 200, 56, "ELEV", MEOW_BTN_ELEVATED));
|
||
assert(meow_btn_set_variant(bt, MEOW_BTN_TONAL) == 0); /* 同值 */
|
||
assert(meow_btn_set_variant(bo, MEOW_BTN_OUTLINED) == 0);
|
||
assert(meow_widget_hit(bf, 100, 30) == 1);
|
||
assert(meow_widget_hit(bf, 5, 5) == 0); /* 越界 */
|
||
meow_widget_set_enabled(bf, 0);
|
||
assert(meow_widget_hit(bf, 100, 30) == 0); /* 禁用不命中 */
|
||
meow_widget_set_enabled(bf, 1);
|
||
meow_widget_set_selected(be, 1);
|
||
assert(meow_widget_selected(be) == 1);
|
||
printf("PASS button-variant: create/set/hit/disabled/selected\n");
|
||
|
||
/* ---------- TextField ---------- */
|
||
char tfbuf[32] = "";
|
||
meow_widget *tf = W(meow_textfield_create(10, 360, 300, 44, "Type here...", tfbuf, 32, MEOW_IME_ASCII));
|
||
strcpy(tfbuf, "abc");
|
||
assert(strcmp(meow_textfield_text(tf), "abc") == 0);
|
||
assert(meow_widget_hit(tf, 20, 380) == 1);
|
||
meow_textfield_set_placeholder(tf, "placeholder");
|
||
printf("PASS textfield: create/text/placeholder/hit\n");
|
||
|
||
/* ---------- Card + content ---------- */
|
||
meow_widget *card = W(meow_card_create(240, 10, 220, 180));
|
||
meow_widget *inner = W(meow_btn_create_variant(260, 40, 180, 44, "IN CARD", MEOW_BTN_FILLED));
|
||
meow_card_set_content(card, inner);
|
||
assert(meow_widget_hit(card, 250, 20) == 1);
|
||
printf("PASS card: create/content/hit\n");
|
||
|
||
/* ---------- Divider / Badge ---------- */
|
||
meow_widget *dv = W(meow_divider_create(10, 470, 200, 0));
|
||
meow_widget *dv2 = W(meow_divider_create(230, 470, 8, 1));
|
||
meow_widget *bd = W(meow_badge_create(400, 100));
|
||
meow_badge_set(bd, 3);
|
||
meow_badge_set(bd, 0);
|
||
printf("PASS divider/badge: create/set\n");
|
||
|
||
/* ---------- Checkbox / Radio ---------- */
|
||
meow_widget *ck = W(meow_checkbox_create(300, 200));
|
||
meow_widget *rd = W(meow_radio_create(300, 250));
|
||
meow_widget_set_selected(ck, 1);
|
||
meow_widget_set_selected(rd, 1);
|
||
assert(meow_widget_selected(ck) == 1);
|
||
assert(meow_widget_hit(rd, 315, 265) == 1);
|
||
printf("PASS checkbox/radio: create/select/hit\n");
|
||
|
||
/* ---------- ProgressBar ---------- */
|
||
meow_widget *pg = W(meow_progress_create(10, 520, 300, 6, 40));
|
||
meow_progress_set(pg, 200); /* clamp 100(无 getter,仅不崩) */
|
||
meow_widget *pgc = W(meow_progress_circular_create(420, 520, 20, 66));
|
||
meow_progress_set(pgc, -1); /* 不定 */
|
||
printf("PASS progress: create/set linear+circular\n");
|
||
|
||
/* ---------- FAB ---------- */
|
||
meow_widget *fab = W(meow_fab_create(410, 620, 56, "+"));
|
||
assert(meow_widget_hit(fab, 430, 640) == 1);
|
||
printf("PASS fab: create/hit\n");
|
||
|
||
/* ---------- TopAppBar ---------- */
|
||
meow_widget *tab_ = W(meow_topappbar_create(0, 0, 480, 56, "Title"));
|
||
meow_topappbar_set_back(tab_, 1);
|
||
int a0 = meow_topappbar_add_action(tab_, "Save");
|
||
int a1 = meow_topappbar_add_action(tab_, "Edit");
|
||
assert(a0 == 0 && a1 == 1);
|
||
assert(meow_topappbar_hit(tab_, 10, 28) == MEOW_TOPBAR_BACK); /* 返回区 */
|
||
assert(meow_topappbar_hit(tab_, 440, 28) == 1); /* 右缘动作 "Edit"(最右) */
|
||
printf("PASS topappbar: create/back/action/hit\n");
|
||
|
||
/* ---------- TopAppBar 位图动作图标(sb_icons[],Material Symbols)----------
|
||
* 文本项默认;set_item_icon 把动作项改为位图 → 绘制用 meow_gfx_blit_icon_ink(32px 槽
|
||
* 居中 + ink 垂直对齐栏中点),颜色取主题 onSurface(按压向栏底色淡 12%)。断言用
|
||
* back_buf 真实像素:画完读亮度,非空 ink + 按压变淡 + 图标项命中。 */
|
||
{
|
||
/* 用独立 topbar(y=300,避让其它控件);meow_widget_draw 自填 surfaceContainerHigh 底 */
|
||
meow_widget *ti = W(meow_topappbar_create(0, 300, 480, 56, ""));
|
||
int ai = meow_topappbar_add_action(ti, ""); /* 文本占位 → 转图标 */
|
||
assert(ai == 0);
|
||
assert(meow_topappbar_set_item_icon(ti, ai, MEOW_ICON_SETTINGS) == 0);
|
||
assert(meow_topappbar_set_item_icon(ti, 99, MEOW_ICON_SETTINGS) == -1); /* 越界 */
|
||
assert(meow_topappbar_set_item_icon(ti, ai, -1) == -1); /* 非法图标 */
|
||
assert(meow_topappbar_set_item_icon(NULL, ai, MEOW_ICON_SETTINGS) == -1);
|
||
meow_widget_draw(ti);
|
||
/* 右起动作格(SDK 位图动作项宽 48)命中:几何中心 → 项 0(最右图标) */
|
||
assert(meow_topappbar_hit(ti, 444, 328) == ai);
|
||
/* 图标 ink 采样:与栏底色有「绝对对比度」的像素即 ink(日/夜主题都适用——
|
||
* 日 = 深 onSurface 画浅底、夜 = 浅 onSurface 画深底,对比度都大)。 */
|
||
int x0 = 480 - 48, y0 = 300 + 10, x1 = 480, y1 = 300 + 46;
|
||
int bg_b, bg_g, bg_bgv;
|
||
rd_px(480 - 3, 302, &bg_b, &bg_g, &bg_bgv); /* 栏右上角 ≈ 背景 */
|
||
int bg_lum = (bg_b + bg_g + bg_bgv) / 3;
|
||
int sum_ink = 0, cnt_ink = 0;
|
||
for (int xx = x0; xx < x1; xx++) {
|
||
for (int yy = y0; yy < y1; yy++) {
|
||
int rr, gg, bb;
|
||
rd_px(xx, yy, &rr, &gg, &bb);
|
||
int lum = (rr + gg + bb) / 3;
|
||
int d = lum - bg_lum; if (d < 0) d = -d;
|
||
if (d > 20) { sum_ink += d; cnt_ink++; }
|
||
}
|
||
}
|
||
assert(cnt_ink > 4); /* 位图 ink 非空(为抗锯齿留余量) */
|
||
int before = sum_ink;
|
||
/* 按压态 → 图标向栏底色淡 12% → 对比度整体下降 */
|
||
meow_widget_set_state(ti, MEOW_WS_PRESSED, 1);
|
||
meow_widget_draw(ti);
|
||
sum_ink = 0;
|
||
for (int xx = x0; xx < x1; xx++) {
|
||
for (int yy = y0; yy < y1; yy++) {
|
||
int rr, gg, bb;
|
||
rd_px(xx, yy, &rr, &gg, &bb);
|
||
int lum = (rr + gg + bb) / 3;
|
||
int d = lum - bg_lum; if (d < 0) d = -d;
|
||
if (d > 20) sum_ink += d;
|
||
}
|
||
}
|
||
assert(sum_ink < before); /* 按压变淡 */
|
||
meow_widget_set_state(ti, MEOW_WS_PRESSED, 0);
|
||
printf("PASS topappbar: bitmap action icon draw/hit/pressed-fade (ink=%d->%d)\n",
|
||
before, sum_ink);
|
||
}
|
||
|
||
/* ---------- 状态栏图标 blit 路径回归(meow_gfx_blit_icon 同源核心)----------
|
||
* 证明 refactor 后状态栏路径(blit + 顶栏脏区)仍写非空像素;复用 SB_ICON_WIFI(0)。 */
|
||
{
|
||
meow_ui_fill_rect(0, 0, 480, 28, 0x000000); /* 清顶栏区为黑底 */
|
||
int bgr, bgg, bgb, bright = 0;
|
||
meow_gfx_blit_icon(0, 0, 0, 16, 0xFFFFFF, 14); /* idx0=WIFI 槽左缘 0 */
|
||
for (int xx = 0; xx < 32; xx++) {
|
||
for (int yy = 4; yy < 24; yy++) {
|
||
rd_px(xx, yy, &bgr, &bgg, &bgb);
|
||
if ((bgr + bgg + bgb) / 3 > 40) bright++;
|
||
}
|
||
}
|
||
assert(bright > 4); /* 状态栏 WiFi 位图 ink 非空(黑底 + 白前景 → 亮像素) */
|
||
printf("PASS statusbar: meow_gfx_blit_icon(0) draws non-empty ink (brt=%d)\n", bright);
|
||
}
|
||
|
||
/* ---------- NavigationBar ---------- */
|
||
meow_widget *nav = W(meow_navigationbar_create(0, 680, 480, 40));
|
||
int n0 = meow_navigationbar_add_item(nav, "Home");
|
||
int n1 = meow_navigationbar_add_item(nav, "Library");
|
||
int n2 = meow_navigationbar_add_item(nav, "Settings");
|
||
assert(n0 == 0 && n1 == 1 && n2 == 2);
|
||
meow_navigationbar_set_selected(nav, 2);
|
||
assert(meow_navigationbar_selected(nav) == 2);
|
||
assert(meow_navigationbar_hit(nav, 420, 700) == 2); /* 第 3 项 */
|
||
assert(meow_navigationbar_hit(nav, 10, 700) == 0);
|
||
printf("PASS navigationbar: add/select/hit\n");
|
||
|
||
/* ---------- TabBar ---------- */
|
||
meow_widget *tbb = W(meow_tabbar_create(0, 660, 480, 36));
|
||
meow_tabbar_add_tab(tbb, "Playlist");
|
||
meow_tabbar_add_tab(tbb, "Queue");
|
||
meow_tabbar_set_selected(tbb, 1);
|
||
assert(meow_tabbar_hit(tbb, 300, 678) == 1);
|
||
printf("PASS tabbar: add/select/hit\n");
|
||
|
||
/* ---------- 列表容器(LazyColumn 虚拟化 + 滚动) ---------- */
|
||
meow_widget *lc = W(meow_lazycolumn_create(20, 460, 260, 180, 40, 100, list_src, NULL));
|
||
assert(meow_list_scroll(lc) == 0);
|
||
assert(meow_list_max_scroll(lc) == 100 * 40 - 180); /* 4000-180=3820 */
|
||
meow_list_set_scroll(lc, 5000);
|
||
assert(meow_list_scroll(lc) == 3820); /* clamp 到 max */
|
||
meow_list_scroll_by(lc, -320);
|
||
assert(meow_list_scroll(lc) == 3500);
|
||
assert(meow_list_hit_index(lc, 30, 480) == ((480 - 460 + 3500) / 40)); /* 可见项换算 */
|
||
meow_list_set_item_h(lc, 50);
|
||
assert(meow_widget_hit(lc, 30, 470) == 1);
|
||
/* M2.71-bug8:行选中 + 命中点击回调 */
|
||
meow_list_set_selected(lc, 5);
|
||
assert(meow_list_selected(lc) == 5);
|
||
/* 验证 tap_at 返回命中下标并置选中 */
|
||
int hidx = meow_list_tap_at(lc, 30, 480);
|
||
assert(hidx == ((480 - 460 + meow_list_scroll(lc)) / 50));
|
||
assert(meow_list_selected(lc) == hidx);
|
||
/* LazyRow 横向 */
|
||
meow_widget *lr = W(meow_lazyrow_create(20, 640, 380, 40, 60, 100, list_src, NULL));
|
||
meow_list_scroll_by(lr, 200);
|
||
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" };
|
||
meow_dialog_set_items(dlg, items, 3, 1);
|
||
assert(meow_dialog_result(dlg) == 0); /* 未 show 默认 */
|
||
meow_dialog_free(dlg);
|
||
meow_dialog *dc = meow_dialog_create(MEOW_DLG_CONFIRM, "Sure?");
|
||
assert(meow_dialog_result(dc) == 0);
|
||
meow_dialog_free(dc);
|
||
meow_sheet *sh = meow_sheet_create("Options", 0);
|
||
int s0 = meow_sheet_add_item(sh, "Copy");
|
||
int s1 = meow_sheet_add_item(sh, "Delete");
|
||
assert(s0 == 0 && s1 == 1);
|
||
meow_sheet_free(sh);
|
||
printf("PASS dialog/sheet: create/set_items/add_item (blocking show skipped)\n");
|
||
|
||
/* ---------- §6.3 通用长按:触发/取消/短按不受影响(模拟时序) ----------
|
||
* 用 meow_input_touch_set(按下+已按住时长回拨)驱动 SDK 触摸状态机,meow_input_long_press_poll
|
||
* 检测阈值跨越;meow_input_touch_release 走真实 touch_release(含长按后抑制 TAP/SWIPE)。
|
||
* 断言:① 长按触发(poll=1 + 入队 LONG_PRESS);② 长按后抬手不再产生 TAP;③ 短按不受影响;
|
||
* ④ 移动超容忍取消长按;⑤ 阈值可配;⑥ 未注册回调 = 长按关闭(opt-in,hello 等零回归)。 */
|
||
{
|
||
meow_input_ev e;
|
||
meow_input_drain();
|
||
meow_input_touch_set(0, 0, 0, 0); /* 复位(清任何残留按住态) */
|
||
meow_ui_set_long_press_ms(500); /* 显式回默认,防其它段干扰 */
|
||
|
||
/* ⑥ opt-in:未注册回调 → 长按判定关闭:poll=0(即使 hold≥阈值),抬手照常 TAP。 */
|
||
assert(meow_input_long_press_poll() == 0);
|
||
meow_input_touch_set(1, 130, 310, 700);
|
||
assert(meow_input_long_press_poll() == 0); /* 未注册回调:不触发 */
|
||
meow_input_touch_release();
|
||
assert(meow_input_pop(&e) == 1 && e.type == MEOW_EV_TAP);
|
||
meow_ui_set_long_press_cb(lp_recv);
|
||
|
||
/* ① 长按触发:hold=700ms(≥500)→ poll=1,LONG_PRESS 入队且坐标=按下点。 */
|
||
meow_input_touch_set(1, 120, 300, 700);
|
||
assert(meow_input_long_press_poll() == 1);
|
||
assert(meow_input_pop(&e) == 1 && e.type == MEOW_EV_LONG_PRESS && e.a == 120 && e.b == 300);
|
||
|
||
/* ② 长按后抬手:点击被抑制(不再产生 TAP)。 */
|
||
meow_input_touch_release();
|
||
assert(meow_input_pop(&e) == 0);
|
||
|
||
/* ③ 短按不受影响:hold=100ms(<500)→ poll=0(不触发);抬手 → TAP 正常。 */
|
||
meow_input_touch_set(1, 200, 400, 100);
|
||
assert(meow_input_long_press_poll() == 0);
|
||
meow_input_touch_release();
|
||
assert(meow_input_pop(&e) == 1 && e.type == MEOW_EV_TAP && e.a == 200 && e.b == 400);
|
||
|
||
/* ④ 长按取消(移动超容忍):hold=700ms 但按下后位移 30px(>12)→ poll=0;抬手仍 TAP
|
||
* (位移 < SWIPE_THRESH 65,手势判定为点击)。 */
|
||
meow_input_touch_set(1, 300, 500, 700);
|
||
meow_input_touch_move(300 + 30, 500);
|
||
assert(meow_input_long_press_poll() == 0);
|
||
meow_input_touch_release();
|
||
assert(meow_input_pop(&e) == 1 && e.type == MEOW_EV_TAP);
|
||
|
||
/* ⑤ 阈值可配:设 200ms → hold=250ms 也触发;恢复默认。 */
|
||
meow_ui_set_long_press_ms(200);
|
||
assert(meow_ui_long_press_ms() == 200);
|
||
meow_input_touch_set(1, 180, 260, 250);
|
||
assert(meow_input_long_press_poll() == 1);
|
||
meow_input_touch_release();
|
||
meow_ui_set_long_press_ms(500);
|
||
assert(meow_ui_long_press_ms() == 500);
|
||
|
||
meow_ui_set_long_press_cb(NULL); /* 复位,避免影响其它段 */
|
||
meow_input_touch_set(0, 0, 0, 0); /* 复位,避免影响后续绘制 */
|
||
meow_input_drain();
|
||
printf("PASS long-press: trigger/suppress-tap/short-tap/cancel-move/configurable/opt-in\n");
|
||
}
|
||
|
||
/* ---------- 任务① 绘制期 clip:W_CUSTOM 越界绘制被裁掉 ---------- */
|
||
{
|
||
const meow_theme *thc = meow_theme_get();
|
||
int bg_r, bg_g, bg_b; UNPK(thc->bg, bg_r, bg_g, bg_b);
|
||
meow_widget *cw = W(meow_widget_custom_create(40, 40, 80, 40, clip_oob_render, NULL, NULL));
|
||
/* a) clip 生效:经 meow_widget_draw(push 控件矩形)→ 越界块/文本被裁掉 */
|
||
g_custom_oob = 1;
|
||
meow_ui_clear(thc->bg);
|
||
meow_widget_draw(cw);
|
||
assert(region_has(45, 45, 20, 20, 0xFF, 0x00, 0x00) > 30); /* 控件内红块画出 */
|
||
assert(px_near(40 + 80 + 25, 40 + 10, bg_r, bg_g, bg_b)); /* 越界实心块被裁 → bg */
|
||
assert(px_near(40 + 80 + 45, 40 + 65, bg_r, bg_g, bg_b)); /* 越界文本被裁 → bg */
|
||
/* b) 对照:无 clip 直绘同尺寸块会画出(证明是 clip 在裁) */
|
||
meow_ui_clear(thc->bg);
|
||
meow_ui_fill_rect(40 + 80 + 20, 40, 60, 40, 0x00FF00);
|
||
assert(region_has(40 + 80 + 20, 40, 60, 40, 0x00, 0xFF, 0x00) > 100);
|
||
printf("PASS clip: W_CUSTOM out-of-bounds fill+text clipped by widget clip\n");
|
||
}
|
||
|
||
/* ---------- 任务② view 递归 enter/leave + on_event 子>父命中 ---------- */
|
||
{
|
||
static const meow_view_hooks ph = { .on_enter = vtrace_enter, .on_leave = vtrace_leave,
|
||
.on_event = vev_parent };
|
||
static const meow_view_hooks ch = { .on_enter = vtrace_enter, .on_leave = vtrace_leave,
|
||
.on_event = vev_child };
|
||
/* 页 id 须 < MEOW_VIEW_BY_ID_MAX(32) 才能进 g_view_by_id(page_push 查找) */
|
||
meow_view *pv = meow_view_create(3, MEOW_VIEW_ACTIVITY, &ph, NULL);
|
||
meow_view *cv = meow_view_create(4, MEOW_VIEW_CONTAINER, &ch, NULL);
|
||
assert(meow_view_add_child(pv, cv) == 0);
|
||
g_vtrace_n = 0; g_ev_child_hits = 0; g_ev_parent_hits = 0;
|
||
meow_view_page_push(3); /* 进入 pv → 递归进 cv:pv.on_enter→cv.on_enter */
|
||
assert(g_vtrace_n == 2 && g_vtrace[0] == 3 && g_vtrace[1] == 4);
|
||
/* 命中 (10,10):先发子(按 z 降序),子命中即消费,父不再收 */
|
||
assert(meow_view_dispatch_event(7, 10, 10) == 1);
|
||
assert(g_ev_child_hits == 1 && g_ev_parent_hits == 0);
|
||
/* 命中 (20,20):子不命中 → 落父 on_event */
|
||
assert(meow_view_dispatch_event(7, 20, 20) == 1);
|
||
assert(g_ev_child_hits == 1 && g_ev_parent_hits == 1);
|
||
/* 离开 pv → 递归离开:cv.on_leave(-4) → pv.on_leave(-3) */
|
||
g_vtrace_n = 0;
|
||
meow_view_page_pop();
|
||
assert(g_vtrace_n == 2 && g_vtrace[0] == -4 && g_vtrace[1] == -3);
|
||
meow_view_free(pv); /* 递归释放 cv */
|
||
printf("PASS view: recursive enter/leave order + on_event child>parent hit\n");
|
||
}
|
||
|
||
/* ---------- 绘制所有控件到 back_buf(5x7 兜底),flush 出屏幕 ---------- */
|
||
const meow_theme *th = meow_theme_get();
|
||
meow_ui_clear(th->bg);
|
||
for (int i = 0; i < g_wn; i++) meow_widget_draw(g_w[i]);
|
||
meow_ui_flush();
|
||
printf("PASS draw: all %d widgets rendered, screen dumps to /tmp/b2.ppm\n", g_wn);
|
||
|
||
free_all();
|
||
meow_ui_shutdown();
|
||
printf("ALL PASS\n");
|
||
return 0;
|
||
}
|