Files
MeowOS-SDK/test_meow_ui_widgets.c
T

448 lines
23 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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_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 "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");
/* ---------- 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;
}