Files
MeowOS-SDK/test_meow_ui_widgets.c
T
MeowOS 5d44128357 feat(sdk): 通用长按事件 (agent-191, 用户定案) — MEOW_EV_LONG_PRESS 坐标式事件+meow_ui_set_long_press_cb/ms(opt-in,默认500ms,移动>12px取消,长按后抬手抑制TAP/SWIPE); 按钮按住视觉阈值同源; files 长按从App层迁入 SDK
meow_ui.a 4335c33a→6de89c27; hello→72fb20da; files→b1c4f58f
宿主 widgets(含6条长按断言)/p1p2p3/ime_fix/test_fsx 全 PASS; 未改 meow_ui_events 结构(防 -Wmissing-field-initializers 破坏既有App)
2026-08-31 05:04:26 +08:00

376 lines
19 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 <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; }
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");
}
/* ---------- 绘制所有控件到 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;
}