Files
MeowOS-SDK/sdk/test_meow_ui_scroll.c
T
AskaEth 27a3750e36 SDK: 同步主仓 M15/M16/bug3/MD3 修复 — 时钟 ABI 补全/IME 冷却窗/列表行高派生/形状 token/翻页键
公开 SDK 落后主仓的一批 bugfix(主仓是固件构建源,这些修复已在设备上跑过)。
本次只做「主仓 → 公开仓」单向同步,图像栈方向由主仓侧另一单处理。

时钟 ABI(M16 第二弹,原文件自认「残余未修」):
- meow_ui_time.c 147→345 行:futex 相对超时接管(__timedwait/_cp)、
  timerfd_create/settime/gettime 的 itimerspec 64↔32 双向转换、
  __clock_settime64 越界返 ENOTSUP,另有 4 个纯换算函数
- meow_ui_internal.h:struct meow_time32 / meow_itimerspec32 契约移入内部头
- CLOCK_ABI.md:补接管机制、验证配方与 objdump 证据
- 新增 test_meow_time64.c

IME bug3(成对才有意义):
- meow_ui_ime.c:IME_TAP_COOLDOWN_MS 200 冷却窗 + 打开时二次 drain
- meow_ui_input.c:meow_input_touch_reset() 并在 drain 内调用(复位触摸状态机)
- test_ime_fix.c:新增用例④ stray-tap

列表行高(M15,修用户可见 bug):
- meow_ui_widget.c:listv_item_h() 由主题字体行高派生,不再写死 56
  (两行列表 62 > 56 会让首行标题被裁剪守卫整条跳过)
- include/meow_ui_widgets.h:新增 meow_listview_item_h()
- 新增 test_meow_ui_listview_rowh.c

其余:meow_select.c / meow_ui_dialog.c 统一 MD3 形状 token;
include/meow_ipc.h 补 MSG_KEY_PAGEUP/PAGEDOWN 枚举;
meow_ui_sys.c / meow_cachegov.c 取时统一走 meow_ui_time_ms()。

测试辅助函数标定:tuku/test/test_settings_lock_host.c 的注入前等待 150ms→350ms。
该等待按修复前 IME 标定,新冷却窗下首键 dt_open≈114ms 会被当迟到抬起吞掉,
只录到 "68" 两位。仅改等待时长,断言原样保留。

验证:tuku 四套脚本全绿(run_tests 644 PASS / zip 106 / cbz 296 / webtoon PASS);
12 个源文件与主仓逐字节一致;sdk/build_sdk.sh 34 成员;
tuku/build_tuku.sh 产出静态 MIPS32r2 ELF。
2026-11-06 21:31:54 +08:00

409 lines
16 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_scroll.c — §12.3 meow_scroll 滚动物理宿主验证:
* 跟手拖动(track 跟随与 clamp)、速度跟踪→松手 fling(位移序列/停止位置/单调/不越界)、
* 边界过冲 clamp + 简单回弹、snap。时间推进用真实 usleep(meow_anim_tick 驱动 fling)。
*
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/draw/activity/ime + 打桩 font/global):
* gcc -o test_meow_ui_scroll test_meow_ui_scroll.c test_host_stub.c \
* meow_ui_gfx.c meow_ui_time.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_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
* meow_jpeg.c meow_png_stream.c \
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm -lz
* 运行:./test_meow_ui_scroll
*/
#include "meow_ui.h"
#include "meow_ui_internal.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static int g_pass = 0, g_fail = 0;
#define CHECK(cond, ...) do { \
if (cond) { g_pass++; } \
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
} while (0)
#define LIST_H 400
#define ITEM_H 44
#define COUNT 50
/* max_scroll = COUNT*ITEM_H - LIST_H = 2200 - 400 = 1800 */
#define MAXS (COUNT * ITEM_H - LIST_H)
static int src(int idx, char *label, int len, void *ctx)
{
(void)ctx;
if (idx < 0 || idx >= COUNT) return 0;
snprintf(label, (size_t)len, "row %d", idx);
return 1;
}
static meow_widget *make_list(void)
{
meow_widget *lv = meow_list_create(0, 0, 480, LIST_H, COUNT, src, NULL);
meow_list_set_item_h(lv, ITEM_H);
return lv;
}
/* 喂一串向下(正)速度采样,模拟快速下划 */
static void feed_drag(meow_scroll *s, int step, int n)
{
uint64_t now = meow_ui_time_ms();
for (int i = 0; i < n; i++) {
meow_scroll_track(s, step, now + (uint64_t)i * 16);
}
/* 最后用一个真实稍晚时刻的 track 让采样时间戳真实(保持一致钟) */
meow_scroll_track(s, 0, meow_ui_time_ms());
}
static void pump_until_idle(meow_scroll *s, int *max_off, int *mono_ok)
{
int prev = meow_scroll_offset(s);
int guard = 0;
while (meow_scroll_active(s) && guard++ < 300) {
meow_anim_tick();
int cur = meow_scroll_offset(s);
if (*max_off < cur) *max_off = cur;
if (cur < prev) *mono_ok = 0; /* 下划 fling 应单调不减(回弹段除外:此处只测无回弹) */
prev = cur;
usleep(10000);
}
meow_anim_tick();
}
static void test_track_follow(void)
{
meow_widget *lv = make_list();
meow_scroll *s = meow_scroll_wrap(lv, 0);
CHECK(s != NULL, "wrap");
CHECK(meow_scroll_offset(s) == 0, "init offset 0");
meow_scroll_track(s, 10, meow_ui_time_ms());
CHECK(meow_scroll_offset(s) == 10, "follow +10 got %d", meow_scroll_offset(s));
/* 超量拖到底:clamp 到 max */
meow_scroll_track(s, 100000, meow_ui_time_ms());
CHECK(meow_scroll_offset(s) == MAXS, "clamp to max got %d", meow_scroll_offset(s));
meow_scroll_free(s);
meow_widget_free(lv);
}
static void test_fling_no_boundary(void)
{
meow_widget *lv = make_list();
meow_scroll *s = meow_scroll_wrap(lv, 0);
/* 从 0 起拖 ~300px,建正初速 */
uint64_t t = meow_ui_time_ms();
for (int i = 0; i < 12; i++) meow_scroll_track(s, 25, t + (uint64_t)i * 16);
int start = meow_scroll_offset(s);
CHECK(start > 0, "dragged start>0 got %d", start);
meow_scroll_fling(s);
CHECK(meow_scroll_active(s) == 1, "fling active");
int max_off = start, mono = 1, prev = start;
int guard = 0;
while (meow_scroll_active(s) && guard++ < 300) {
meow_anim_tick();
int cur = meow_scroll_offset(s);
if (max_off < cur) max_off = cur;
if (cur < prev) mono = 0;
prev = cur;
usleep(10000);
}
meow_anim_tick();
int end = meow_scroll_offset(s);
CHECK(mono == 1, "monotonic non-decreasing during fling");
CHECK(end >= start, "fling advances end=%d start=%d", end, start);
CHECK(end <= MAXS, "no overflow end=%d max=%d", end, MAXS);
CHECK(meow_scroll_active(s) == 0, "settles idle");
meow_scroll_free(s);
meow_widget_free(lv);
}
static void test_fling_boundary_bounce(void)
{
meow_widget *lv = make_list();
meow_scroll *s = meow_scroll_wrap(lv, 0);
/* 先拖到近底(~1700) */
meow_list_set_scroll(lv, 1700);
/* 快速下划,冲过底界 */
uint64_t t = meow_ui_time_ms();
for (int i = 0; i < 8; i++) meow_scroll_track(s, 40, t + (uint64_t)i * 16);
meow_scroll_fling(s);
int max_off = 0, mono = 1;
pump_until_idle(s, &max_off, &mono);
int end = meow_scroll_offset(s);
CHECK(max_off <= MAXS, "never exceed max, max_off=%d max=%d", max_off, MAXS);
CHECK(end == MAXS, "bounce settles at bottom end=%d", end); /* 回弹后回到底 */
CHECK(meow_scroll_active(s) == 0, "idle after bounce");
meow_scroll_free(s);
meow_widget_free(lv);
}
static void test_snap(void)
{
meow_widget *lv = make_list();
meow_scroll *s = meow_scroll_wrap(lv, 0);
meow_scroll_set_snap(s, ITEM_H); /* 吸附到行边界 */
/* 慢拖到某非边界位置并给 small velocity → fling 结束 snap 到行倍数 */
uint64_t t = meow_ui_time_ms();
for (int i = 0; i < 6; i++) meow_scroll_track(s, 20, t + (uint64_t)i * 16);
meow_scroll_fling(s);
int max_off = 0, mono = 1;
pump_until_idle(s, &max_off, &mono);
int end = meow_scroll_offset(s);
CHECK((end % ITEM_H) == 0, "snap to item multiple got %d", end);
CHECK(end <= MAXS, "snap in range %d", end);
meow_scroll_free(s);
meow_widget_free(lv);
}
static void test_stop(void)
{
meow_widget *lv = make_list();
meow_scroll *s = meow_scroll_wrap(lv, 0);
uint64_t t = meow_ui_time_ms();
for (int i = 0; i < 6; i++) meow_scroll_track(s, 20, t + (uint64_t)i * 16);
meow_scroll_fling(s);
CHECK(meow_scroll_active(s) == 1, "fling active (stop test)");
meow_scroll_stop(s);
CHECK(meow_scroll_active(s) == 0, "stopped immediately");
meow_scroll_free(s);
meow_widget_free(lv);
}
/* ============ 池槽归还回归(M19)============
* meow_scroll_wrap 从 MEOW_SCROLL_POOL(=8) 个 wrap 上下文槽里占一个;旧实现 free 只归还
* 会话槽、不归还 wrap 槽 → 进程内最多成功 wrap 8 次,之后恒 NULL(图库每次 load_dir 重建,
* 进 8 个目录后列表丢惯性)。以下用例覆盖:长循环回收、free 后重建、交错不串槽、
* 池满仍返回 NULL、直接 create 不误清槽、free(NULL)/二次 free 安全。 */
#define WRAP_POOL 8 /* = meow_ui_scroll.c 的 MEOW_SCROLL_POOL(内部常量,测试按契约固定) */
/* 连续 64 次 wrap/free:每次都要拿到可用会话,且绑定当前列表 */
static void test_wrap_free_recycle(void)
{
meow_widget *lv = make_list();
int ok = 0, first_fail = -1;
for (int i = 0; i < 64; i++) {
meow_scroll *s = meow_scroll_wrap(lv, 0);
if (!s) { if (first_fail < 0) first_fail = i; continue; }
meow_list_set_scroll(lv, 0);
meow_scroll_track(s, 7, meow_ui_time_ms());
if (meow_scroll_offset(s) == 7) ok++;
else if (first_fail < 0) first_fail = i; /* 串槽:set/get 不指向本列表 */
meow_scroll_free(s);
}
CHECK(ok == 64, "64 wrap/free cycles all usable, ok=%d first_fail=%d", ok, first_fail);
meow_widget_free(lv);
}
/* wrap → free → 同一列表再 wrap:必须重新可用 */
static void test_wrap_after_free(void)
{
meow_widget *lv = make_list();
meow_scroll *s = meow_scroll_wrap(lv, 0);
CHECK(s != NULL, "wrap#1 same list");
meow_scroll_free(s);
s = meow_scroll_wrap(lv, 0);
CHECK(s != NULL, "wrap#2 same list after free");
if (s) {
meow_list_set_scroll(lv, 0);
meow_scroll_track(s, 12, meow_ui_time_ms());
CHECK(meow_scroll_offset(s) == 12, "re-wrapped scroll works got %d", meow_scroll_offset(s));
meow_scroll_free(s);
}
meow_widget_free(lv);
}
/* 8 个同时活跃 → 第 9 个仍返回 NULL(池满不越界);释放其一的槽立刻可再用 */
static void test_wrap_pool_full(void)
{
meow_widget *lv[WRAP_POOL];
meow_scroll *s[WRAP_POOL];
int ok = 0;
for (int i = 0; i < WRAP_POOL; i++) {
lv[i] = make_list();
s[i] = meow_scroll_wrap(lv[i], 0);
if (s[i]) ok++;
}
CHECK(ok == WRAP_POOL, "all %d wraps active, ok=%d", WRAP_POOL, ok);
meow_widget *extra_lv = make_list();
meow_scroll *extra = meow_scroll_wrap(extra_lv, 0);
CHECK(extra == NULL, "9th simultaneous wrap returns NULL (pool full), got %p", (void *)extra);
/* 释放中间一个 → 槽立即回收;活着的实例仍各自绑定自己的列表 */
meow_scroll_free(s[3]);
s[3] = NULL;
meow_scroll *again = meow_scroll_wrap(extra_lv, 0);
CHECK(again != NULL, "wrap succeeds right after freeing one slot");
if (again) {
meow_list_set_scroll(extra_lv, 0);
meow_scroll_track(again, 9, meow_ui_time_ms());
CHECK(meow_scroll_offset(again) == 9, "new wrap bound to its own list got %d",
meow_scroll_offset(again));
meow_list_set_scroll(lv[5], 33);
CHECK(meow_scroll_offset(s[5]) == 33, "other live wrap keeps its own list got %d",
meow_scroll_offset(s[5]));
meow_scroll_free(again);
}
for (int i = 0; i < WRAP_POOL; i++) {
if (s[i]) meow_scroll_free(s[i]);
meow_widget_free(lv[i]);
}
meow_widget_free(extra_lv);
}
/* 交错:两个 wrap 活跃 → free 其一 → 再 wrap,三者不串槽 */
static void test_wrap_interleaved(void)
{
meow_widget *a = make_list(), *b = make_list(), *c = make_list();
meow_scroll *sa = meow_scroll_wrap(a, 0);
meow_scroll *sb = meow_scroll_wrap(b, 0);
CHECK(sa != NULL && sb != NULL, "two wraps (interleaved) a=%p b=%p", (void *)sa, (void *)sb);
if (!sa || !sb) {
meow_scroll_free(sa); meow_scroll_free(sb);
meow_widget_free(a); meow_widget_free(b); meow_widget_free(c);
return;
}
meow_list_set_scroll(a, 11);
meow_list_set_scroll(b, 22);
CHECK(meow_scroll_offset(sa) == 11 && meow_scroll_offset(sb) == 22,
"distinct ctx a=%d b=%d", meow_scroll_offset(sa), meow_scroll_offset(sb));
meow_scroll_free(sa);
CHECK(meow_scroll_offset(sb) == 22, "remaining wrap intact after freeing first got %d",
meow_scroll_offset(sb));
meow_scroll *sc = meow_scroll_wrap(c, 0);
CHECK(sc != NULL, "third wrap after freeing first");
if (sc) {
meow_list_set_scroll(c, 0);
meow_scroll_track(sc, 5, meow_ui_time_ms());
CHECK(meow_scroll_offset(sc) == 5, "third wrap bound to its own list got %d",
meow_scroll_offset(sc));
CHECK(meow_scroll_offset(sb) == 22, "second wrap not clobbered by third got %d",
meow_scroll_offset(sb));
meow_scroll_free(sc);
}
meow_scroll_free(sb);
meow_widget_free(a); meow_widget_free(b); meow_widget_free(c);
}
/* ---- 直接 create(非 wrap)的回调:用于验证 free 不误清别人的 wrap 槽 ---- */
static int g_direct_off;
static void direct_set(void *c, int off) { (void)c; g_direct_off = off; }
static int direct_max(void *c) { (void)c; return 1000; }
static int direct_get(void *c) { (void)c; return g_direct_off; }
static void test_direct_create_free_keeps_wrap_slot(void)
{
meow_widget *lv = make_list();
meow_scroll *w = meow_scroll_wrap(lv, 0);
CHECK(w != NULL, "wrap before direct create");
meow_scroll *d = meow_scroll_create(direct_set, direct_max, direct_get, NULL, NULL);
CHECK(d != NULL, "direct create");
if (d) {
meow_scroll_track(d, 40, meow_ui_time_ms());
CHECK(g_direct_off == 40, "direct scroll tracks got %d", g_direct_off);
meow_scroll_free(d); /* 非 wrap 实例:不得归还/清除任何 wrap 槽 */
}
if (w) {
meow_list_set_scroll(lv, 0);
meow_scroll_track(w, 10, meow_ui_time_ms());
CHECK(meow_scroll_offset(w) == 10, "wrap intact after freeing direct instance got %d",
meow_scroll_offset(w));
meow_scroll_free(w);
}
meow_widget_free(lv);
}
/* free(NULL) 安全;同一实例二次 free 不崩、不清活着的 wrap 槽 */
static void test_wrap_free_null_and_double(void)
{
meow_scroll_free(NULL); /* 必须安全 */
meow_widget *a = make_list(), *b = make_list();
meow_scroll *sa = meow_scroll_wrap(a, 0);
meow_scroll *sb = meow_scroll_wrap(b, 0);
CHECK(sa != NULL && sb != NULL, "two wraps (double free) a=%p b=%p", (void *)sa, (void *)sb);
if (sb) meow_list_set_scroll(b, 44);
if (sa) meow_scroll_free(sa);
if (sa) meow_scroll_free(sa); /* 二次 free:重复清零,不得清掉 sb 的 ctx */
if (sb) {
CHECK(meow_scroll_offset(sb) == 44, "live wrap intact after double free got %d",
meow_scroll_offset(sb));
meow_scroll_free(sb);
}
meow_scroll *sc = meow_scroll_wrap(a, 0);
CHECK(sc != NULL, "wrap still available after free(NULL)/double free");
if (sc) meow_scroll_free(sc);
meow_widget_free(a); meow_widget_free(b);
}
/* 虚拟化列表行源(仅需 count 有效,不绘制) */
static int lv_src2(int idx, meow_list_item *out, void *ctx)
{
(void)ctx;
if (idx < 0 || idx >= COUNT) return 0;
memset(out, 0, sizeof *out);
out->title = "row";
return 1;
}
/* 槽复用不继承上一个 wrap 的 kind:list → listview → list 交替必须各自生效
* (free 时清 lv 标记,wrap 时按参数重设;否则复用槽上的旧 kind 会让 set/get 打空) */
static void test_wrap_slot_kind_reset(void)
{
meow_widget *plain = make_list();
meow_widget *vl = meow_listview_create(0, 0, 480, LIST_H, COUNT, lv_src2, NULL,
MEOW_LIST_SEL_SINGLE);
meow_listview_set_item_h(vl, ITEM_H);
meow_scroll *s = meow_scroll_wrap(plain, 0);
CHECK(s != NULL, "wrap plain list");
if (s) meow_scroll_free(s);
s = meow_scroll_wrap(vl, 1);
CHECK(s != NULL, "wrap listview on recycled slot");
if (s) {
meow_listview_set_scroll(vl, 0);
meow_scroll_track(s, 20, meow_ui_time_ms());
CHECK(meow_scroll_offset(s) == 20, "listview wrapped scroll works got %d",
meow_scroll_offset(s));
meow_scroll_free(s);
}
s = meow_scroll_wrap(plain, 0);
CHECK(s != NULL, "wrap plain list again on recycled slot");
if (s) {
meow_list_set_scroll(plain, 0);
meow_scroll_track(s, 15, meow_ui_time_ms());
CHECK(meow_scroll_offset(s) == 15, "plain list after listview slot reuse got %d",
meow_scroll_offset(s));
meow_scroll_free(s);
}
meow_widget_free(vl);
meow_widget_free(plain);
}
int main(void)
{
meow_ui_opts o = { 480, 720 };
if (meow_ui_init(&o) != 0) { printf("init failed\n"); return 1; }
/* 池槽回归先跑:新建进程里第一个动 pool 的用例,便于对照「修复前 8/64、修复后 64/64」 */
test_wrap_free_recycle();
test_track_follow();
test_fling_no_boundary();
test_fling_boundary_bounce();
test_snap();
test_stop();
test_wrap_after_free();
test_wrap_pool_full();
test_wrap_interleaved();
test_direct_create_free_keeps_wrap_slot();
test_wrap_slot_kind_reset();
test_wrap_free_null_and_double();
meow_ui_shutdown();
printf("scroll: %d pass / %d fail\n", g_pass, g_fail);
return g_fail ? 1 : 0;
}