diff --git a/build_sdk.sh b/build_sdk.sh index 8c67ac6..7253c20 100755 --- a/build_sdk.sh +++ b/build_sdk.sh @@ -10,7 +10,7 @@ AR="${AR:-/usr/bin/ar}" # 系统 GNU ar 即可归档 mipsel ELF 对象(架 rm -f meow_ui.a *.o # -I../meow_launcherd:meow_notify_ipc.c 复用共享协议头 meow_ipc.h(同一份、杜绝两端漂移) -OBJS="meow_ui_gfx meow_ui_log meow_ui_notify meow_notify_ipc meow_ui_img meow_ui_widget meow_ui_input meow_ui_theme meow_ui_ime meow_pinyin meow_ui_sys meow_ui_vol meow_ui_activity meow_ui_global meow_ui_dialog meow_ui_config meow_ui_draw meow_ui_view meow_ui_nav meow_ui_media meow_ui_audio stb_image" +OBJS="meow_ui_gfx meow_ui_log meow_ui_notify meow_notify_ipc meow_ui_img meow_ui_widget meow_ui_input meow_ui_theme meow_ui_ime meow_pinyin meow_ui_sys meow_ui_vol meow_ui_activity meow_ui_global meow_ui_dialog meow_ui_config meow_ui_draw meow_ui_view meow_ui_nav meow_ui_media meow_ui_audio stb_image meow_ui_box meow_ui_anim meow_ui_scroll" CFLAGS="-Os -static -I$ROOT/patch_v2/meow_launcherd" for f in $OBJS; do echo "== cc $f ==" diff --git a/demo_box_scroll.c b/demo_box_scroll.c new file mode 100644 index 0000000..c36e1ad --- /dev/null +++ b/demo_box_scroll.c @@ -0,0 +1,89 @@ +/* demo_box_scroll.c — 宿主离屏 demo:§12.1 meow_box 布局 + §12.3 meow_scroll fling 端到端。 + * 渲染一屏「标题(固定) / 列表(weight 撑满) / 底栏(固定)」的 box 布局(cross 拉伸), + * 再对列表跑一次 fling(速度跟踪→惯性滑动到停止处),并 dump PPM 供目检。 + * + * 编译(宿主 gcc,链真实 SDK 源 + 打桩 font/global): + * gcc -o demo_box_scroll demo_box_scroll.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_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 \ + * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm + * 运行:MEOW_UI_DUMP=/tmp/demo_box.ppm ./demo_box_scroll + */ +#include "meow_ui.h" +#include "meow_ui_internal.h" +#include +#include +#include +#include + +#define NROW 60 +#define ITEM 44 +static int src(int idx, char *label, int len, void *ctx) +{ + (void)ctx; + if (idx < 0 || idx >= NROW) return 0; + snprintf(label, (size_t)len, "item %d", idx); + return 1; +} + +int main(void) +{ + /* 离屏 dump */ + setenv("MEOW_UI_DUMP", "/tmp/demo_box.ppm", 1); + meow_ui_opts o = { 480, 720 }; + if (meow_ui_init(&o) != 0) { printf("DEMO FAIL: init\n"); return 1; } + const meow_theme *th = meow_theme_get(); + meow_ui_clear(th->bg); + + /* ---- box 布局:标题(固定) / 列表(weight) / 底栏(固定),交叉轴拉伸 ---- */ + meow_box *bx = meow_box_new(MEOW_BOX_V); + meow_box_set_rect(bx, 0, 0, 480, 720); + meow_box_set_padding(bx, 12, 12, 12, 12); + meow_box_set_gap(bx, 8); + meow_box_set_align(bx, MEOW_BOX_ALIGN_START, MEOW_BOX_CROSS_STRETCH); + + meow_widget *header = meow_btn_create(0, 0, 480, 44, "Header (fixed)"); + meow_widget *list = meow_list_create(0, 0, 480, 100, NROW, src, NULL); + meow_list_set_item_h(list, ITEM); + meow_widget *footer = meow_btn_create(0, 0, 480, 52, "Footer (fixed)"); + + meow_box_add(bx, header, MEOW_BOX_FIXED, 0); + meow_box_add(bx, list, MEOW_BOX_WEIGHT, 1); + meow_box_add(bx, footer, MEOW_BOX_FIXED, 0); + meow_box_arrange(bx); + + int hr[4], lr[4], fr[4]; + meow_widget_rect(header, hr); + meow_widget_rect(list, lr); + meow_widget_rect(footer, fr); + printf("DEMO layout: header(%d,%d,%d,%d) list(%d,%d,%d,%d) footer(%d,%d,%d,%d)\n", + hr[0], hr[1], hr[2], hr[3], lr[0], lr[1], lr[2], lr[3], fr[0], fr[1], fr[2], fr[3]); + + meow_widget_draw(header); + meow_widget_draw(list); + meow_widget_draw(footer); + meow_ui_flush(); /* 上屏并 MEOW_UI_DUMP 写 PPM *//* (host 离屏无 fb → 只落盘) */ + + /* ---- fling:对列表做一次惯性下滑(速度跟踪→减速→停)---- */ + meow_scroll *s = meow_scroll_wrap(list, 0); + int maxs = meow_list_max_scroll(list); + uint64_t t = meow_ui_time_ms(); + for (int i = 0; i < 14; i++) meow_scroll_track(s, 40, t + (uint64_t)i * 16); + meow_scroll_fling(s); + int guard = 0; + while (meow_scroll_active(s) && guard++ < 300) { meow_anim_tick(); usleep(10000); } + meow_anim_tick(); + int end = meow_scroll_offset(s); + printf("DEMO fling: max_scroll=%d final=%d (active=%d)\n", maxs, end, meow_scroll_active(s)); + + meow_scroll_free(s); + meow_box_free(bx); + meow_widget_free(list); + meow_widget_free(header); + meow_widget_free(footer); + meow_ui_shutdown(); + printf("DEMO OK\n"); + return 0; +} diff --git a/include/meow_ui_anim.h b/include/meow_ui_anim.h new file mode 100644 index 0000000..a6dd626 --- /dev/null +++ b/include/meow_ui_anim.h @@ -0,0 +1,75 @@ +/* + * meow_ui_anim.h — SDK 动画基座 + UI 节拍器(§12.2:meow_anim / meow_anim_tick)。 + * + * 定位:给应用提供**时间驱动 tween**(from/to/duration/easing)+ 完成/取消回调,并行多动画 + * (静态池≤MEOW_ANIM_MAX)+ 与脏区集成(动画帧只标动画影响区,不做整屏重绘)。 + * + * 节拍:**挂既有 on_tick 机制**(meow_ui_run 主循环 / meow_ui_feed_tick 阻塞 IME 循环), + * 无常驻高频轮询;无动画时 meow_anim_tick() 只做一个计数检查即返回(零开销)。 + * + * 内存纪律:静态池(≤MEOW_ANIM_MAX),start/cancel/tick 全程零 malloc。 + * + * 与 §7 的关系:§7(B1 最小可用)是「应用自己在 on_tick 里用 meow_ui_time_ms 做时间轴」; + * 本模块把这件事收敛成声明式 tween 对象 + 结束回调,是 §7 的**正式基座**(应用可任选其一或混用)。 + */ +#ifndef MEOW_UI_ANIM_H +#define MEOW_UI_ANIM_H + +#include "meow_ui_types.h" /* 基础类型(子头不引伞头,避免相对路径依赖) */ + +#ifdef __cplusplus +extern "C" { +#endif + +/* ---- 缓动曲线(t∈[0,1] → 曲线化的进度;OVERSHOOT 在中后段可略>1、末点恰=1)---- */ +typedef enum { + MEOW_EASE_LINEAR = 0, + MEOW_EASE_OUT = 1, /* 快进慢停(decelerate) */ + MEOW_EASE_IN_OUT = 2, /* 慢入慢出 */ + MEOW_EASE_OVERSHOOT = 3, /* 越过目标再回稳(小回弹) */ +} meow_ease; + +float meow_ease_apply(meow_ease e, float t); + +/* ---- 动画对象(不透明句柄;静态池)---- */ +#define MEOW_ANIM_MAX 16 + +typedef struct meow_anim meow_anim; + +/* 每帧回调:v = 当前插值值(from + (to-from)*ease(t))。app 在此更新状态并绘制。 */ +typedef void (*meow_anim_update_fn)(meow_anim *a, float v, void *ctx); +/* 完成/取消回调:canceled=1=被取消,0=自然完成。 */ +typedef void (*meow_anim_done_fn)(meow_anim *a, int canceled, void *ctx); + +typedef struct meow_anim_desc { + float from; + float to; + uint32_t duration_ms; /* 时长(ms);0 视为瞬发(下一 tick 完成) */ + meow_ease ease; + meow_anim_update_fn update; /* 可 NULL */ + meow_anim_done_fn done; /* 可 NULL */ + void *ctx; +} meow_anim_desc; + +/* 启动一个动画(空闲槽;池满返回 NULL)。start 即置 value=from 并回调一次 update(0)。 */ +meow_anim *meow_anim_start(const meow_anim_desc *d); +/* 取消(触发 done(canceled=1),不再 update)。 */ +void meow_anim_cancel(meow_anim *a); +/* 查询:仍在运行(value 未达终点)。 */ +int meow_anim_active(meow_anim *a); +/* 查询:当前插值值。 */ +float meow_anim_value(meow_anim *a); + +/* 与脏区集成:动画每 tick 只 flush 影响区(x,y,w,h;x<0 表示不自动 flush)。 + * app 的 update 回调通常也会用 meow_ui_flush_region 自刷;设了这个则 tick 自动刷一次, + * 保证「动画帧只标脏动画影响区,不整屏」。 */ +void meow_anim_set_region(meow_anim *a, int x, int y, int w, int h); + +/* ---- 节拍器(挂在 on_tick / meow_ui_feed_tick;无动画零开销)---- */ +void meow_anim_tick(void); /* 推进所有活动动画(完成/取消在内部处理) */ +unsigned meow_anim_count(void); /* 活动动画数(0=空闲) */ + +#ifdef __cplusplus +} +#endif +#endif /* MEOW_UI_ANIM_H */ diff --git a/include/meow_ui_box.h b/include/meow_ui_box.h new file mode 100644 index 0000000..4171652 --- /dev/null +++ b/include/meow_ui_box.h @@ -0,0 +1,113 @@ +/* + * meow_ui_box.h — SDK 线性布局引擎(§12.1:meow_box)。 + * + * 定位:既有的 meow_row/column/box(§5.8.5,W_LAYOUT)只支持「固定尺寸子项 + 居中」, + * 本模块是其**独立增强**:竖直/水平容器,子项 = 固定尺寸 | weight 权重 | wrap(内容测量), + * 支持 margin/padding / 主轴对齐(start/center/end/space-between/around/evenly)/ 交叉轴对齐 + * (start/center/end/stretch)/ 嵌套。 + * + * 与绝对定位完全兼容:meow_box 是**纯布局计算器**,不是控件,不参与 z 序注册表/命中/绘制; + * arrange 把每个子控件的 x/y/w/h 写进现有 meow_widget(用新增 meow_widget_set_rect), + * 此后照常 draw/hit/flush_region/owner 闸门——与既有控件绝对定位零冲突。 + * + * 与视图框架集成(opt-in):meow_box 可作 meow_view 的布局提供者(meow_view_set_layout); + * view 不设就保持现有绝对坐标行为(§12.1.4)。 + * + * 内存纪律:静态池 + 定长 items 数组(≥MEOW_BOX_MAX_CHILD),热路径(arrange)零 malloc; + * 嵌套用深度≤MEOW_BOX_MAX_DEPTH 的静态栈单遍求解,无递归爆炸。 + */ +#ifndef MEOW_UI_BOX_H +#define MEOW_UI_BOX_H + +#include "meow_ui_types.h" /* meow_widget 前向 + 基础类型(子头不引伞头,避免相对路径依赖) */ + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct meow_box meow_box; + +/* 主轴方向 */ +typedef enum { + MEOW_BOX_H = 0, /* 横向(row) */ + MEOW_BOX_V = 1, /* 纵向(column) */ +} meow_box_dir; + +/* 子项主轴尺寸策略 */ +typedef enum { + MEOW_BOX_FIXED = 0, /* 固定尺寸:内容主轴向取控件当前 w/h */ + MEOW_BOX_WEIGHT = 1, /* 权重:均分剩余主轴空间(weight>0) */ + MEOW_BOX_WRAP = 2, /* wrap:按内容测量(min/preferred 或 measure 回调) */ +} meow_box_size; + +/* 主轴对齐 */ +typedef enum { + MEOW_BOX_ALIGN_START = 0, + MEOW_BOX_ALIGN_CENTER = 1, + MEOW_BOX_ALIGN_END = 2, + MEOW_BOX_ALIGN_SPACE_BETWEEN = 3, + MEOW_BOX_ALIGN_SPACE_AROUND = 4, + MEOW_BOX_ALIGN_SPACE_EVENLY = 5, +} meow_box_align; + +/* 交叉轴对齐 */ +typedef enum { + MEOW_BOX_CROSS_START = 0, + MEOW_BOX_CROSS_CENTER = 1, + MEOW_BOX_CROSS_END = 2, + MEOW_BOX_CROSS_STRETCH = 3, +} meow_box_cross; + +/* 内容测量回调(size==MEOW_BOX_WRAP 时用;可 NULL):给出主轴向 min/preferred 尺寸。 + * ctx = meow_box_add_ex 的 mctx。 */ +typedef void (*meow_box_measure_fn)(void *ctx, int *min_main, int *pref_main); + +/* 子项描述(add/add_ex/add_sub 提供;app 只读)。w 与 sub 二选一。 */ +typedef struct meow_box_item { + meow_widget *w; /* 子控件(sub==NULL 时用) */ + meow_box *sub; /* 嵌套 box(w==NULL 时用) */ + int size; /* MEOW_BOX_* 尺寸策略 */ + int weight; /* size==WEIGHT 时>0 */ + int min; /* size==WRAP 的主轴下限 */ + int preferred;/* size==WRAP 的主轴目标 */ + meow_box_measure_fn measure; /* size==WRAP 内容测量(可 NULL) */ + void *mctx; +} meow_box_item; + +#define MEOW_BOX_MAX_CHILD 24 /* 每容器子项上限(静态数组) */ +#define MEOW_BOX_MAX_DEPTH 16 /* 嵌套深度上限(静态求解栈) */ + +/* ---- 生命周期(静态池;new 冷路径,arrange 热路径零 malloc)---- + * 注:命名用 meow_box_new(meow_box_create 已被 §5.8.5 的 W_LAYOUT box 占用)。 */ +meow_box *meow_box_new(meow_box_dir dir); +void meow_box_free(meow_box *b); + +/* ---- 容器配置 ---- */ +void meow_box_set_rect(meow_box *b, int x, int y, int w, int h); /* 容器矩形(绝对坐标) */ +void meow_box_set_padding(meow_box *b, int l, int t, int r, int btm); +void meow_box_set_gap(meow_box *b, int gap); +void meow_box_set_align(meow_box *b, int main_align, int cross_align); + +/* ---- 子项添加(返回 0=成功;越界/无效=负值,不清空既有子项)---- */ +int meow_box_add(meow_box *b, meow_widget *w, int size, int weight); +/* add_ex:WRAP 用 min/preferred(主轴向)+ 可选内容测量回调。 */ +int meow_box_add_ex(meow_box *b, meow_widget *w, int size, int weight, + int min, int preferred, meow_box_measure_fn m, void *mctx); +/* 嵌套子 box(parent 布局会为 sub 分配矩形并递归 arrange)。 */ +int meow_box_add_sub(meow_box *b, meow_box *sub, int size, int weight); + +/* ---- 求解(单遍;深度≤MEOW_BOX_MAX_DEPTH 静态栈,无递归)---- + * 把每个子控件 x/y/w/h 写进其 meow_widget(meow_widget_set_rect); + * 嵌套 sub 的矩形写进 sub 并递归。子控件的 margin(meow_widget_opts.margin_x/y)参与排布。 */ +void meow_box_arrange(meow_box *b); + +/* ---- 查询 ---- */ +int meow_box_count(const meow_box *b); +meow_widget *meow_box_widget_at(const meow_box *b, int idx); /* 子项控件(无则 NULL) */ +meow_box *meow_box_sub_at(const meow_box *b, int idx); /* 子项嵌套 box(无则 NULL) */ +void meow_box_rect(const meow_box *b, int out[4]); /* x,y,w,h */ + +#ifdef __cplusplus +} +#endif +#endif /* MEOW_UI_BOX_H */ diff --git a/include/meow_ui_scroll.h b/include/meow_ui_scroll.h new file mode 100644 index 0000000..adafdb7 --- /dev/null +++ b/include/meow_ui_scroll.h @@ -0,0 +1,65 @@ +/* + * meow_ui_scroll.h — SDK 滚动物理(§12.3:meow_scroll)。 + * + * 定位:给既有滚动容器(W_LIST / W_LISTVIEW / LazyColumn / LazyRow)加**可惯性滚动**的 + * **物理层**——速度跟踪(最近 N 个采样加权)、松手 fling(减速度曲线,用 §12.2 动画基座驱动)、 + * 边界过冲 clamp + 简单回弹(不做复杂 spring)、滚动停止可选 snap。**跟手拖动行为保持现状 + * 不变**(track 内部调既有 set_scroll,clamp 一致)。 + * + * 设计:meow_scroll 是**滚动会话**,包住一个滚动控件(或任意的 set/max/get/redraw 回调)。 + * 拖动态:app 在 on_touch_move 里把位移喂给 meow_scroll_track(dy, t)(等价于 app 直接调 + * meow_list_scroll_by,且额外记录速度采样);松手:app 调 meow_scroll_fling(s),内部用 + * 加权速度采样得出初速 → 起一条 ease-out 动画推进位移 → 每帧只 redraw(flush 列表视口区)。 + * + * 内存纪律:静态池(应用侧按需创建会话),track/fling/tick 全程零 malloc;无动画时 + * meow_scroll 不参与节拍(fling 由 meow_anim_tick 驱动,活动计数为 0 则零开销)。 + */ +#ifndef MEOW_UI_SCROLL_H +#define MEOW_UI_SCROLL_H + +#include "meow_ui_types.h" /* meow_widget 前向 + 基础类型(子头不引伞头,避免相对路径依赖) */ + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct meow_scroll meow_scroll; + +/* ---- 通用回调(wrap 之外的自定义滚动源用)---- */ +typedef void (*meow_scroll_set_fn)(void *ctx, int off); +typedef int (*meow_scroll_max_fn)(void *ctx); +typedef int (*meow_scroll_get_fn)(void *ctx); +typedef void (*meow_scroll_redraw_fn)(void *ctx); /* 只刷滚动视口区(不整屏) */ + +/* 创建会话:app 提供 set/max/get/redraw。返回 NULL=池满。 */ +meow_scroll *meow_scroll_create(meow_scroll_set_fn set, meow_scroll_max_fn max, + meow_scroll_get_fn get, meow_scroll_redraw_fn redraw, + void *ctx); +/* 便捷:包一个 meow_widget 列表(is_listview:1=W_LISTVIEW / 0=W_LIST 或 lazycolumn/row)。 + * redraw 只 flush 该控件矩形(视口区)。 */ +meow_scroll *meow_scroll_wrap(meow_widget *list, int is_listview); +void meow_scroll_free(meow_scroll *s); + +/* 拖动跟手 + 速度采样:dy=本次位移(px);t_ms=采样时刻(单调毫秒)。 + * 内部 set_scroll(pos+dy)(clamp 到 [0,max],与 app 直接 scroll_by 行为一致)。 */ +void meow_scroll_track(meow_scroll *s, int dy, uint64_t t_ms); + +/* 松手 fling:内部用「最近 N 个采样加权」算初速,起减速度曲线动画推进位移; + * 边界过冲 clamp + 简单回弹;每帧只 redraw 视口区。停止后可选 snap。 */ +void meow_scroll_fling(meow_scroll *s); +/* 立即停止(取消 fling/回弹/snap,停在当前 offset)。 */ +void meow_scroll_stop(meow_scroll *s); + +/* 当前加权初速(px/s;为 fling 内部所用,供 app 读取/调试)。 */ +int meow_scroll_velocity(meow_scroll *s); + +/* 查询/配置 */ +int meow_scroll_active(meow_scroll *s); /* 是否有活动 fling/回弹/snap */ +int meow_scroll_offset(meow_scroll *s); /* 当前 scroll(get) */ +void meow_scroll_set_snap(meow_scroll *s, int step); /* >0=fling 停止后吸附到 step 倍数 */ +void meow_scroll_set_friction(meow_scroll *s, int tau_ms); /* 摩擦时间常数(默认 180ms) */ + +#ifdef __cplusplus +} +#endif +#endif /* MEOW_UI_SCROLL_H */ diff --git a/include/meow_ui_widgets.h b/include/meow_ui_widgets.h index ee9f476..b78d677 100644 --- a/include/meow_ui_widgets.h +++ b/include/meow_ui_widgets.h @@ -33,6 +33,8 @@ void meow_widget_free(meow_widget *w); void meow_widget_draw(meow_widget *w); /* 每次 flush 前调用 */ int meow_widget_hit(meow_widget *w, int x, int y); /* 点是否落在控件内 */ +/* 写控件矩形(§12.1 meow_box 布局引擎用;也可供 app 布局器用)。只设字段不标脏。 */ +void meow_widget_set_rect(meow_widget *w, int x, int y, int wd, int ht); void meow_slider_set(meow_widget *s, int val); int meow_slider_get(meow_widget *s); diff --git a/meow_ui.h b/meow_ui.h index cfbec99..f792d70 100644 --- a/meow_ui.h +++ b/meow_ui.h @@ -34,6 +34,9 @@ * include/meow_ui_config.h 应用配置读写(§9.2) * include/meow_ui_font.h 字体列表(§5.7)+ 字体分层(§10) * include/meow_ui_audio.h 独占音频客户端(§11) + * include/meow_ui_box.h 线性布局引擎(§12.1:meow_box,固定/weight/wrap/对齐/嵌套) + * include/meow_ui_anim.h 动画基座+UI 节拍器(§12.2:tween/easing/池+脏区联动) + * include/meow_ui_scroll.h 滚动物理(§12.3:速度跟踪/fling/边界回弹/snap) * * 其余模块头(非伞头、App 按需显式 include,均保持不动): * meow_ui_view.h / meow_ui_nav.h / meow_ui_media.h / meow_ui_sys.h / meow_ui_vol.h @@ -63,5 +66,8 @@ #include "include/meow_ui_config.h" #include "include/meow_ui_font.h" #include "include/meow_ui_audio.h" +#include "include/meow_ui_box.h" +#include "include/meow_ui_anim.h" +#include "include/meow_ui_scroll.h" #endif /* MEOW_UI_H */ diff --git a/meow_ui_anim.c b/meow_ui_anim.c new file mode 100644 index 0000000..10c530a --- /dev/null +++ b/meow_ui_anim.c @@ -0,0 +1,128 @@ +/* + * meow_ui_anim.c — 动画基座 + UI 节拍器(§12.2)。 + * + * 时间驱动 tween:start 记录起始时刻(meow_ui_time_ms,单调),tick 按经过时间求 t, + * 代入 ease 得 v = from + (to-from)*ease(t),回调 update;t 到 1 → 回调 done(canceled=0) 并回收。 + * 取消 → 回调 done(canceled=1) 并回收。静态池(≤MEOW_ANIM_MAX),热路径零 malloc。 + * 无动画时 meow_anim_tick() 仅查活动计数即返回(零开销)。 + * + * 节拍挂接:由 meow_ui_run 主循环(on_tick 之后)与 meow_ui_feed_tick(阻塞 IME 循环)调用, + * 不引入常驻高频轮询。modal/dialog 阻塞循环不喂 tick(动画暂停,符合「模态期不做动画」惯例)。 + */ +#include "meow_ui.h" +#include "include/meow_ui_anim.h" +#include + +struct meow_anim { + int active; + float from, to; + uint32_t duration_ms; + meow_ease ease; + uint64_t start_ms; + float value; + int region_set; + int rx, ry, rw, rh; + meow_anim_update_fn update; + meow_anim_done_fn done; + void *ctx; +}; + +static struct meow_anim g_pool[MEOW_ANIM_MAX]; +static unsigned g_active = 0; /* 活动动画计数(0 = 空闲,tick 直退) */ + +/* ---- 缓动 ---- */ +float meow_ease_apply(meow_ease e, float t) +{ + if (e == MEOW_EASE_OUT) { + /* ease-out quad */ + float u = 1.0f - t; + return 1.0f - u * u; + } + if (e == MEOW_EASE_IN_OUT) { + /* ease-in-out quad */ + if (t < 0.5f) return 2.0f * t * t; + float u = -2.0f * t + 2.0f; + return 1.0f - u * u * 0.5f; + } + if (e == MEOW_EASE_OVERSHOOT) { + /* ease-out-back(越过目标再回稳);t=1 → 恰为 1 */ + const float c1 = 1.70158f; + const float c3 = c1 + 1.0f; + float u = t - 1.0f; + return 1.0f + c3 * u * u * u + c1 * u * u; + } + /* MEOW_EASE_LINEAR */ + return t; +} + +/* ---- 生命周期 ---- */ +meow_anim *meow_anim_start(const meow_anim_desc *d) +{ + if (!d) return NULL; + for (int i = 0; i < MEOW_ANIM_MAX; i++) { + struct meow_anim *a = &g_pool[i]; + if (a->active) continue; + a->active = 1; + a->from = d->from; + a->to = d->to; + a->duration_ms = d->duration_ms ? d->duration_ms : 1; /* 0 → 瞬发(下一 tick 完成) */ + a->ease = d->ease; + a->start_ms = meow_ui_time_ms(); + a->update = d->update; + a->done = d->done; + a->ctx = d->ctx; + a->region_set = 0; + a->value = d->from; + g_active++; + if (a->update) a->update(a, a->value, a->ctx); /* 起始帧回调(t=0) */ + return a; + } + return NULL; /* 池满 */ +} + +void meow_anim_cancel(meow_anim *a) +{ + if (!a || !a->active) return; + a->active = 0; + if (g_active > 0) g_active--; + if (a->done) a->done(a, 1, a->ctx); +} + +int meow_anim_active(meow_anim *a) { return (a && a->active) ? 1 : 0; } +float meow_anim_value(meow_anim *a) { return a ? a->value : 0.0f; } + +void meow_anim_set_region(meow_anim *a, int x, int y, int w, int h) +{ + if (!a) return; + if (x < 0) { a->region_set = 0; return; } + a->region_set = 1; + a->rx = x; a->ry = y; a->rw = w > 0 ? w : 0; a->rh = h > 0 ? h : 0; +} + +/* ---- 节拍器 ---- */ +unsigned meow_anim_count(void) { return (unsigned)g_active; } + +void meow_anim_tick(void) +{ + if (g_active == 0) return; /* 无动画:零开销直接返回 */ + uint64_t now = meow_ui_time_ms(); + for (int i = 0; i < MEOW_ANIM_MAX; i++) { + struct meow_anim *a = &g_pool[i]; + if (!a->active) continue; + uint64_t elapsed = now - a->start_ms; + float t = (float)(elapsed) / (float)a->duration_ms; + int finished = (t >= 1.0f); + if (t > 1.0f) t = 1.0f; + if (t < 0.0f) t = 0.0f; + float v = a->from + (a->to - a->from) * meow_ease_apply(a->ease, t); + a->value = v; + if (a->update) a->update(a, v, a->ctx); + if (a->region_set && a->rw > 0 && a->rh > 0) + meow_ui_flush_region(a->rx, a->ry, a->rw, a->rh); /* 只刷动画影响区 */ + if (finished) { + a->active = 0; + if (g_active > 0) g_active--; + if (a->done) a->done(a, 0, a->ctx); + } + } +} diff --git a/meow_ui_box.c b/meow_ui_box.c new file mode 100644 index 0000000..716f1fe --- /dev/null +++ b/meow_ui_box.c @@ -0,0 +1,307 @@ +/* + * meow_ui_box.c — 线性布局引擎(§12.1 meow_box)。 + * + * meow_box 是**纯布局计算器**(非控件):静态池创建、定长子项数组、arrange 单遍求解, + * 把每个子控件的 x/y/w/h 写进 meow_widget(meow_widget_set_rect);嵌套 sub-box 递归求解。 + * 用显式静态栈(深度≤MEOW_BOX_MAX_DEPTH)做嵌套展开,无 C 递归,杜绝栈爆炸。 + * + * 求解模型(主轴向): + * - 每个子项占位 = lead(margin) + extent + trail(margin);项间再叠 box->gap。 + * - FIXED/WRAP 的 extent 先定;WEIGHT 的 extent=0(待均分)。 + * - leftover = content_main - foot;若权重和>0 → 均分 leftover;否则按主轴对齐分配。 + * 交叉轴:按 START/CENTER/END/STRETCH 对齐,STRETCH 填满容器交叉向减 margin。 + */ +#include "meow_ui.h" +#include "include/meow_ui_box.h" +#include "meow_ui_internal.h" /* meow_widget_rect(只读几何) */ +#include + +#define MEOW_BOX_POOL 16 /* 静态池大小(并发 box 数) */ + +struct meow_box { + int dir; /* MEOW_BOX_H / MEOW_BOX_V */ + int x, y, w, h; /* 容器矩形(绝对坐标) */ + int pad_l, pad_t, pad_r, pad_b; + int gap; + int main_align; + int cross_align; + meow_box_item items[MEOW_BOX_MAX_CHILD]; + int n; +}; + +static meow_box g_pool[MEOW_BOX_POOL]; +static unsigned char g_used[MEOW_BOX_POOL]; + +/* ---- 静态池存取 ---- */ +meow_box *meow_box_new(meow_box_dir dir) +{ + for (int i = 0; i < MEOW_BOX_POOL; i++) { + if (!g_used[i]) { + g_used[i] = 1; + meow_box *b = &g_pool[i]; + memset(b, 0, sizeof *b); + b->dir = (dir == MEOW_BOX_H) ? MEOW_BOX_H : MEOW_BOX_V; + return b; + } + } + return NULL; +} +void meow_box_free(meow_box *b) +{ + if (!b) return; + for (int i = 0; i < MEOW_BOX_POOL; i++) + if (&g_pool[i] == b) { g_used[i] = 0; return; } +} + +void meow_box_set_rect(meow_box *b, int x, int y, int w, int h) +{ + if (!b) return; + b->x = x; b->y = y; + b->w = w > 0 ? w : 0; b->h = h > 0 ? h : 0; +} +void meow_box_set_padding(meow_box *b, int l, int t, int r, int btm) +{ + if (!b) return; + b->pad_l = l; b->pad_t = t; b->pad_r = r; b->pad_b = btm; +} +void meow_box_set_gap(meow_box *b, int gap) +{ + if (!b) return; + b->gap = gap > 0 ? gap : 0; +} +void meow_box_set_align(meow_box *b, int main_align, int cross_align) +{ + if (!b) return; + b->main_align = main_align; + b->cross_align = cross_align; +} + +/* ---- 子项 ---- */ +int meow_box_add(meow_box *b, meow_widget *w, int size, int weight) +{ + return meow_box_add_ex(b, w, size, weight, 0, 0, NULL, NULL); +} +int meow_box_add_ex(meow_box *b, meow_widget *w, int size, int weight, + int min, int preferred, meow_box_measure_fn m, void *mctx) +{ + if (!b || !w || b->n >= MEOW_BOX_MAX_CHILD) return -1; + if (size != MEOW_BOX_FIXED && size != MEOW_BOX_WEIGHT && size != MEOW_BOX_WRAP) return -1; + if (size == MEOW_BOX_WEIGHT && weight <= 0) return -1; + for (int i = 0; i < b->n; i++) + if (b->items[i].w == w) return -1; /* 同控件去重 */ + meow_box_item *it = &b->items[b->n++]; + memset(it, 0, sizeof *it); + it->w = w; it->size = size; it->weight = weight > 0 ? weight : 1; + it->min = min > 0 ? min : 0; + it->preferred = preferred > 0 ? preferred : 0; + it->measure = m; it->mctx = mctx; + return 0; +} +int meow_box_add_sub(meow_box *b, meow_box *sub, int size, int weight) +{ + if (!b || !sub || b->n >= MEOW_BOX_MAX_CHILD) return -1; + if (size != MEOW_BOX_FIXED && size != MEOW_BOX_WEIGHT && size != MEOW_BOX_WRAP) return -1; + if (size == MEOW_BOX_WEIGHT && weight <= 0) return -1; + for (int i = 0; i < b->n; i++) + if (b->items[i].sub == sub) return -1; + meow_box_item *it = &b->items[b->n++]; + memset(it, 0, sizeof *it); + it->sub = sub; it->size = size; it->weight = weight > 0 ? weight : 1; + return 0; +} + +/* ---- 查询 ---- */ +int meow_box_count(const meow_box *b) { return b ? b->n : 0; } +meow_widget *meow_box_widget_at(const meow_box *b, int idx) +{ return (b && idx >= 0 && idx < b->n) ? b->items[idx].w : NULL; } +meow_box *meow_box_sub_at(const meow_box *b, int idx) +{ return (b && idx >= 0 && idx < b->n) ? b->items[idx].sub : NULL; } +void meow_box_rect(const meow_box *b, int out[4]) +{ + if (!out) return; + if (!b) { out[0] = out[1] = out[2] = out[3] = 0; return; } + out[0] = b->x; out[1] = b->y; out[2] = b->w; out[3] = b->h; +} + +/* ---- 内部:取子项 margin(widget opts;sub 无 margin)---- */ +static void item_margin(const meow_box_item *it, meow_box_dir dir, + int *main_before, int *main_after, int *cross) +{ + *main_before = *main_after = *cross = 0; + if (!it->w) return; + meow_widget_opts o; + if (meow_widget_opts_get(it->w, &o) != 0) return; + int mx = o.margin_x > 0 ? o.margin_x : 0; + int my = o.margin_y > 0 ? o.margin_y : 0; + if (dir == MEOW_BOX_H) { *main_before = mx; *main_after = mx; *cross = my; } + else { *main_before = my; *main_after = my; *cross = mx; } +} + +/* ---- 内部:子项主轴向 extent ---- + * FIXED → 控件/嵌套盒当前主轴向尺寸 + * WRAP → measure 回调(若有)否则控件当前尺寸;下限 clamp 到 min + * WEIGHT → 0(由均分阶段填充) */ +static int item_extent(const meow_box_item *it, int main_is_w) +{ + int extent = 0; + if (it->w) { + int r[4]; meow_widget_rect(it->w, r); + extent = main_is_w ? r[2] : r[3]; + } else if (it->sub) { + extent = main_is_w ? it->sub->w : it->sub->h; + } + if (it->size == MEOW_BOX_WRAP && it->measure) { + int mn = 0, pf = 0; + it->measure(it->mctx, &mn, &pf); + if (pf > 0) extent = pf; + else if (mn > 0) extent = mn; + if (mn > 0 && extent < mn) extent = mn; /* clamp 到下限 */ + } + if (extent < 0) extent = 0; + return extent; +} + +/* ---- 单容器求解(一次 pass:算 extent + 位置,写全部子项矩形)---- + * 子项为嵌套 meow_box 时只写其矩形(父求结果),后续由 meow_box_arrange 的 + * 显式栈递归求解其内部。 */ +static void box_layout_one(meow_box *b) +{ + int n = b->n; + if (n <= 0) return; + int cx = b->x + b->pad_l; + int cy = b->y + b->pad_t; + int cw = b->w - b->pad_l - b->pad_r; if (cw < 0) cw = 0; + int ch = b->h - b->pad_t - b->pad_b; if (ch < 0) ch = 0; + int main_is_w = (b->dir == MEOW_BOX_H); + int main_size = main_is_w ? cw : ch; + int cross_size = main_is_w ? ch : cw; + int gap = b->gap; + + int ext[MEOW_BOX_MAX_CHILD], lead[MEOW_BOX_MAX_CHILD], + trail[MEOW_BOX_MAX_CHILD], wgt[MEOW_BOX_MAX_CHILD]; + int nw = 0, wsum = 0; + long long foot = 0; + + for (int i = 0; i < n; i++) { + const meow_box_item *it = &b->items[i]; + int mb, ma, cr; + item_margin(it, (meow_box_dir)b->dir, &mb, &ma, &cr); + lead[i] = mb; trail[i] = ma; (void)cr; + wgt[i] = (it->size == MEOW_BOX_WEIGHT) ? it->weight : 0; + if (it->size == MEOW_BOX_WEIGHT) { + ext[i] = 0; nw++; wsum += it->weight; + } else { + ext[i] = item_extent(it, main_is_w); + } + foot += (long long)lead[i] + trail[i] + ext[i]; + } + foot += (long long)gap * (n - 1); + + long long leftover = (long long)main_size - foot; + + if (nw > 0 && leftover > 0) { + /* 均分剩余到权重项;余数塞给最后一个权重项铺满精确 */ + long long consumed = 0; + for (int i = 0; i < n; i++) { + if (wgt[i] > 0) { ext[i] = (int)(leftover * wgt[i] / wsum); consumed += ext[i]; } + } + for (int i = n - 1; i >= 0; i--) { + if (wgt[i] > 0) { ext[i] += (int)(leftover - consumed); break; } + } + leftover = 0; + } + + /* 主轴对齐(仅当无权重、leftover != 0 有意义;有权重时 leftover≈0 → START 等效) */ + int off0 = 0; + int intergap = gap; + if (nw == 0) { + if (leftover > 0) { + switch (b->main_align) { + case MEOW_BOX_ALIGN_CENTER: off0 = (int)(leftover / 2); break; + case MEOW_BOX_ALIGN_END: off0 = (int)leftover; break; + case MEOW_BOX_ALIGN_SPACE_BETWEEN: off0 = 0; + intergap = (n > 1) ? gap + (int)(leftover / (n - 1)) : gap; break; + case MEOW_BOX_ALIGN_SPACE_AROUND: off0 = (int)(leftover / (2 * n)); + intergap = gap + (int)(leftover / n); break; + case MEOW_BOX_ALIGN_SPACE_EVENLY: off0 = (int)(leftover / (n + 1)); + intergap = gap + (int)(leftover / (n + 1)); break; + default: off0 = 0; break; /* START */ + } + } else { + off0 = 0; intergap = gap; /* 溢出:从起点排(可越界,交滚动容器) */ + } + } + + /* 主轴定位 + 交叉轴定位,写子控件矩形(主轴起点 = 内容原点 cx/cy)。 + * rel = 相对内容原点的主轴游标;content_rel = 本项内容相对起点;content_pos = 绝对主轴坐标。 */ + int main_base = main_is_w ? cx : cy; + int rel = off0; + for (int i = 0; i < n; i++) { + const meow_box_item *it = &b->items[i]; + int content_rel = rel + lead[i]; + int content_pos = main_base + content_rel; + if (it->w) { + int r[4]; meow_widget_rect(it->w, r); + meow_widget_opts o; int has = (meow_widget_opts_get(it->w, &o) == 0); + int cmx = (has && o.margin_x > 0) ? o.margin_x : 0; + int cmy = (has && o.margin_y > 0) ? o.margin_y : 0; + int cm = main_is_w ? cmy : cmx; + int base = main_is_w ? r[3] : r[2]; + int cross = 0, cross_ext = 0; + if (b->cross_align == MEOW_BOX_CROSS_STRETCH) { + cross_ext = cross_size - 2 * cm; if (cross_ext < 0) cross_ext = 0; + cross = cm; + } else { + cross_ext = base; + int pad = cross_size - 2 * cm - cross_ext; if (pad < 0) pad = 0; + if (b->cross_align == MEOW_BOX_CROSS_CENTER) cross = cm + pad / 2; + else if (b->cross_align == MEOW_BOX_CROSS_END) cross = cm + pad; + else cross = cm; /* START */ + } + if (main_is_w) + meow_widget_set_rect(it->w, content_pos, cy + cross, ext[i], cross_ext); + else + meow_widget_set_rect(it->w, cx + cross, content_pos, cross_ext, ext[i]); + } else if (it->sub) { + meow_box *sb = it->sub; + int base = main_is_w ? sb->h : sb->w; + int cross = 0, cross_ext = 0; + if (b->cross_align == MEOW_BOX_CROSS_STRETCH) { + cross_ext = cross_size; if (cross_ext < 0) cross_ext = 0; + cross = 0; + } else { + cross_ext = base; + int pad = cross_size - cross_ext; if (pad < 0) pad = 0; + if (b->cross_align == MEOW_BOX_CROSS_CENTER) cross = pad / 2; + else if (b->cross_align == MEOW_BOX_CROSS_END) cross = pad; + else cross = 0; + } + if (main_is_w) { sb->x = content_pos; sb->y = cy + cross; sb->w = ext[i]; sb->h = cross_ext; } + else { sb->x = cx + cross; sb->y = content_pos; sb->w = cross_ext; sb->h = ext[i]; } + } + if (i < n - 1) + rel = content_rel + ext[i] + trail[i] + intergap; + else + rel = content_rel + ext[i] + trail[i]; + } +} + +/* ---- 求解:单遍,显式静态栈展开嵌套(深度≤MEOW_BOX_MAX_DEPTH)---- */ +static meow_box *g_stack[MEOW_BOX_MAX_DEPTH]; + +void meow_box_arrange(meow_box *b) +{ + if (!b) return; + int top = 0; + g_stack[0] = b; + while (top >= 0) { + meow_box *cur = g_stack[top--]; + box_layout_one(cur); /* 写全部直接子项矩形(含 sub->rect) */ + for (int i = 0; i < cur->n; i++) { + meow_box *sb = cur->items[i].sub; + if (!sb) continue; + if (top + 1 >= MEOW_BOX_MAX_DEPTH) continue; /* 深度上限:跳过(矩形已被父写定) */ + g_stack[++top] = sb; + } + } +} diff --git a/meow_ui_input.c b/meow_ui_input.c index e105f5a..a1a7e5c 100644 --- a/meow_ui_input.c +++ b/meow_ui_input.c @@ -697,6 +697,9 @@ void meow_ui_run(const meow_ui_events *ev) g_events.on_tick(); } } + /* §12.2 动画节拍器:推进所有活动动画(无动画时零开销)。挂既有 on_tick 机制, + * 不引入常驻高频轮询;动画 update 回调只 flush 其影响区(脏区联动)。 */ + meow_anim_tick(); /* A-0 全局状态栏 1s 轻刷:电量/WiFi/音量 + 时钟。时钟每秒变 → 重画顶条并上屏, * 即使 APP 空闲(未主动 flush)也保持时钟走动。 diff --git a/meow_ui_scroll.c b/meow_ui_scroll.c new file mode 100644 index 0000000..6d76c9b --- /dev/null +++ b/meow_ui_scroll.c @@ -0,0 +1,293 @@ +/* + * meow_ui_scroll.c — 滚动物理(§12.3)。 + * + * 速度采样:track 时把 (pos, t) 记进定长环形(≤MEOW_SCROLL_SAMPLES),fling 时取 + * 最近 MEOW_SCROLL_WINDOW ms 窗口,按「间隔速度 × 近者权重」加权得出初速(px/s)。 + * 位置模型:ease-out quad 的位移曲线(末速恰为 0),初速与输入 v 对齐,时长 = 2×tau。 + * 边界:free 目标越界 → clamp 到 [0,max];若越界速度大 → 补两段短 IN_OUT 回弹 + * (overshoot 超过边界方向的内侧再回位),不做弹簧。停止后可选 snap 到 step 倍数。 + * + * 不引 libm(SDK 约定,免 -lm);只用整数 + 纯浮点四则。 + */ +#include "meow_ui.h" +#include "include/meow_ui_scroll.h" +#include "meow_ui_internal.h" /* meow_widget_rect(定位 redraw 视口区) */ +#include "include/meow_ui_anim.h" +#include + +#define MEOW_SCROLL_POOL 8 +#define MEOW_SCROLL_SAMPLES 8 /* 速度采样点数(最近 N 加权) */ +#define MEOW_SCROLL_WINDOW 110 /* 速度窗口(ms) */ +#define MEOW_SCROLL_TAU 180 /* 默认摩擦时间常数(ms) */ +#define MEOW_SCROLL_MIN_SPEED 60 /* 低于此的初速不 fling(px/s) */ +#define MEOW_SCROLL_MAX_DUR 900 /* fling 时长上限(ms) */ +#define MEOW_SCROLL_BOUNCE_MS 110 /* 每段回弹时长(ms) */ +#define MEOW_SCROLL_SNAP_MS 120 + +typedef enum { + MEOW_SCROLL_IDLE = 0, + MEOW_SCROLL_FLING, + MEOW_SCROLL_BOUNCE1, + MEOW_SCROLL_BOUNCE2, + MEOW_SCROLL_SNAP, +} meow_scroll_phase; + +struct meow_scroll { + meow_scroll_set_fn set; + meow_scroll_max_fn max; + meow_scroll_get_fn get; + meow_scroll_redraw_fn redraw; + void *ctx; + + int snap_step; /* >0 = 吸附步长 */ + int tau_ms; /* 摩擦时间常数 */ + /* 速度采样(顺序数组,满则前移,≤SAMPLES) */ + int v_pos[MEOW_SCROLL_SAMPLES]; + uint64_t v_t[MEOW_SCROLL_SAMPLES]; + int v_n; + + meow_scroll_phase phase; + meow_anim *anim; + int maxv; /* fling 开始时缓存 max_scroll */ + int bounce; /* fling 边界回弹幅度(带符号;0=不回弹) */ + int bounce_start; /* 回弹回位起点(= clamp 边界) */ + int snap_to; /* snap 目标 */ +}; + +static meow_scroll g_pool[MEOW_SCROLL_POOL]; +static unsigned char g_used[MEOW_SCROLL_POOL]; + +/* ---- 生命周期 ---- */ +meow_scroll *meow_scroll_create(meow_scroll_set_fn set, meow_scroll_max_fn max, + meow_scroll_get_fn get, meow_scroll_redraw_fn redraw, + void *ctx) +{ + if (!set || !max || !get) return NULL; + for (int i = 0; i < MEOW_SCROLL_POOL; i++) { + if (!g_used[i]) { + g_used[i] = 1; + meow_scroll *s = &g_pool[i]; + memset(s, 0, sizeof *s); + s->set = set; s->max = max; s->get = get; s->redraw = redraw; s->ctx = ctx; + s->tau_ms = MEOW_SCROLL_TAU; + return s; + } + } + return NULL; +} +void meow_scroll_free(meow_scroll *s) +{ + if (!s) return; + if (s->anim) meow_anim_cancel(s->anim); + s->anim = NULL; + for (int i = 0; i < MEOW_SCROLL_POOL; i++) + if (&g_pool[i] == s) { g_used[i] = 0; return; } +} + +/* ---- wrap:包一个 meow_widget 列表 ---- */ +typedef struct { meow_widget *w; int lv; } wrap_ctx; +static void wrap_set(void *c, int off) +{ wrap_ctx *wc = c; if (wc->lv) meow_listview_set_scroll(wc->w, off); else meow_list_set_scroll(wc->w, off); } +static int wrap_max(void *c) +{ wrap_ctx *wc = c; return wc->lv ? meow_listview_max_scroll(wc->w) : meow_list_max_scroll(wc->w); } +static int wrap_get(void *c) +{ wrap_ctx *wc = c; return wc->lv ? meow_listview_scroll(wc->w) : meow_list_scroll(wc->w); } +static void wrap_redraw(void *c) +{ + wrap_ctx *wc = c; + if (!wc->w) return; + int r[4]; meow_widget_rect(wc->w, r); + if (r[2] > 0 && r[3] > 0) meow_ui_flush_region(r[0], r[1], r[2], r[3]); /* 只刷视口区 */ +} + +static wrap_ctx g_wrap_ctx[MEOW_SCROLL_POOL]; +static int g_wrap_used[MEOW_SCROLL_POOL]; + +meow_scroll *meow_scroll_wrap(meow_widget *list, int is_listview) +{ + if (!list) return NULL; + meow_scroll *s = meow_scroll_create(wrap_set, wrap_max, wrap_get, wrap_redraw, NULL); + if (!s) return NULL; + for (int i = 0; i < MEOW_SCROLL_POOL; i++) { + if (!g_wrap_used[i]) { + g_wrap_used[i] = 1; + g_wrap_ctx[i].w = list; g_wrap_ctx[i].lv = is_listview ? 1 : 0; + s->ctx = &g_wrap_ctx[i]; + return s; + } + } + meow_scroll_free(s); + return NULL; +} + +/* ---- 速度采样 ---- */ +static void sample_push(meow_scroll *s, int pos, uint64_t t) +{ + if (s->v_n >= MEOW_SCROLL_SAMPLES) { /* 满:前移丢弃最旧 */ + for (int i = 1; i < s->v_n; i++) { s->v_pos[i - 1] = s->v_pos[i]; s->v_t[i - 1] = s->v_t[i]; } + s->v_n--; + } + s->v_pos[s->v_n] = pos; s->v_t[s->v_n] = t; s->v_n++; +} + +/* 加权初速:取窗口内采样,按间隔速度 × 近者权重加权平均(px/s)。 */ +static int sample_velocity(meow_scroll *s, uint64_t now) +{ + int lo = 0; + while (lo < s->v_n && now > s->v_t[lo] && (now - s->v_t[lo]) > MEOW_SCROLL_WINDOW) lo++; + if (s->v_n - lo < 2) return 0; + long long sum_w = 0, sum_vw = 0; + for (int i = lo; i < s->v_n - 1; i++) { + uint64_t dt = s->v_t[i + 1] - s->v_t[i]; + if (dt == 0) continue; + long long v = (long long)(s->v_pos[i + 1] - s->v_pos[i]) * 1000LL / (long long)dt; + long long w = (i - lo + 1); /* 近者权重高 */ + sum_vw += v * w; sum_w += w; + } + if (sum_w == 0) return 0; + return (int)(sum_vw / sum_w); +} + +int meow_scroll_velocity(meow_scroll *s) +{ + if (!s) return 0; + return sample_velocity(s, meow_ui_time_ms()); +} + +void meow_scroll_track(meow_scroll *s, int dy, uint64_t t_ms) +{ + if (!s || dy == 0) return; + int off = s->get(s->ctx) + dy; /* set 会 clamp 到 [0,max](与 scroll_by 一致) */ + s->set(s->ctx, off); + int got = s->get(s->ctx); /* clamp 后的真实 offset */ + sample_push(s, got, t_ms); +} + +/* ---- 动画驱动回调 ---- */ +static void fling_update(meow_anim *a, float v, void *ctx) +{ + (void)a; + meow_scroll *s = (meow_scroll *)ctx; + s->set(s->ctx, (int)v); + if (s->redraw) s->redraw(s->ctx); +} +static void fling_done(meow_anim *a, int canceled, void *ctx) +{ + (void)a; + meow_scroll *s = (meow_scroll *)ctx; + s->anim = NULL; + if (canceled) { s->phase = MEOW_SCROLL_IDLE; return; } + if (s->phase == MEOW_SCROLL_FLING) { + if (s->bounce != 0) { + /* 边界回弹段1:向内侧过冲 → 段2 回位(bounce 已带符号;顶界 +,底界 -) */ + int from = s->get(s->ctx); + int to = from + s->bounce; /* bounce 已带符号 */ + if (to < 0) to = 0; + if (to > s->maxv) to = s->maxv; + if (to == from) { s->phase = MEOW_SCROLL_IDLE; return; } + s->phase = MEOW_SCROLL_BOUNCE1; + meow_anim_desc d; memset(&d, 0, sizeof d); + d.from = (float)from; d.to = (float)to; d.duration_ms = MEOW_SCROLL_BOUNCE_MS; + d.ease = MEOW_EASE_IN_OUT; d.update = fling_update; d.done = fling_done; d.ctx = s; + s->anim = meow_anim_start(&d); + return; + } + /* 无回弹 → snap 或收尾 */ + if (s->snap_step > 0 && s->get(s->ctx) != s->snap_to) { + int from = s->get(s->ctx); + int to = s->snap_to; + if (to < 0) to = 0; + if (to > s->maxv) to = s->maxv; + if (to != from) { + s->phase = MEOW_SCROLL_SNAP; + meow_anim_desc d; memset(&d, 0, sizeof d); + d.from = (float)from; d.to = (float)to; d.duration_ms = MEOW_SCROLL_SNAP_MS; + d.ease = MEOW_EASE_IN_OUT; d.update = fling_update; d.done = fling_done; d.ctx = s; + s->anim = meow_anim_start(&d); + return; + } + } + s->phase = MEOW_SCROLL_IDLE; + return; + } + if (s->phase == MEOW_SCROLL_BOUNCE1) { + int from = s->get(s->ctx); + /* 回位到 bounce 起点(= clamp 边界) */ + int to = s->bounce_start; + if (to < 0) to = 0; + if (to > s->maxv) to = s->maxv; + s->phase = MEOW_SCROLL_BOUNCE2; + if (to == from) { s->phase = MEOW_SCROLL_IDLE; return; } + meow_anim_desc d; memset(&d, 0, sizeof d); + d.from = (float)from; d.to = (float)to; d.duration_ms = MEOW_SCROLL_BOUNCE_MS; + d.ease = MEOW_EASE_IN_OUT; d.update = fling_update; d.done = fling_done; d.ctx = s; + s->anim = meow_anim_start(&d); + return; + } + if (s->phase == MEOW_SCROLL_BOUNCE2 || s->phase == MEOW_SCROLL_SNAP) { + s->phase = MEOW_SCROLL_IDLE; + return; + } + s->phase = MEOW_SCROLL_IDLE; +} + +/* ---- fling ---- */ +void meow_scroll_fling(meow_scroll *s) +{ + if (!s) return; + if (s->anim) { meow_anim_cancel(s->anim); s->anim = NULL; } + s->phase = MEOW_SCROLL_IDLE; + int v = sample_velocity(s, meow_ui_time_ms()); + if (v > -MEOW_SCROLL_MIN_SPEED && v < MEOW_SCROLL_MIN_SPEED) return; /* 太慢不 fling */ + int max = s->max(s->ctx); + if (max < 0) max = 0; + s->maxv = max; + int pos0 = s->get(s->ctx); + int dist = (int)((long long)v * s->tau_ms / 1000LL); /* 摩擦滑行总位移(px) */ + int free_target = pos0 + dist; + int clamped = free_target, over = 0; + if (clamped > max) { over = clamped - max; clamped = max; } + if (clamped < 0) { over = -clamped; clamped = 0; } + if (clamped == pos0) return; /* 无位移 */ + + int dir; /* 回弹方向:底界向 -1(往内上),顶界向 +1(往内下) */ + if (free_target > max) dir = -1; + else if (free_target < 0) dir = 1; + else dir = 0; + + int dur = 2 * s->tau_ms; /* ease-out quad:初速与 v 对齐,末速为 0 */ + if (dur > MEOW_SCROLL_MAX_DUR) dur = MEOW_SCROLL_MAX_DUR; + + s->bounce = 0; + s->snap_to = pos0; + if (s->snap_step > 0) { + int half = s->snap_step / 2; + s->snap_to = ((pos0 + half) / s->snap_step) * s->snap_step; + if (s->snap_to > max) s->snap_to = max; + } + if (over > 12) { /* 快速撞墙:clamp + 简单回弹 */ + int amt = over / 10; if (amt < 3) amt = 3; if (amt > 14) amt = 14; + s->bounce = dir * amt; /* 带符号的向内幅度 */ + /* 记录回位起点 = clamp 边界 */ + s->bounce_start = clamped; + } + + s->phase = MEOW_SCROLL_FLING; + meow_anim_desc d; memset(&d, 0, sizeof d); + d.from = (float)pos0; d.to = (float)clamped; d.duration_ms = (uint32_t)dur; + d.ease = MEOW_EASE_OUT; d.update = fling_update; d.done = fling_done; d.ctx = s; + s->anim = meow_anim_start(&d); +} + +void meow_scroll_stop(meow_scroll *s) +{ + if (!s) return; + if (s->anim) { meow_anim_cancel(s->anim); s->anim = NULL; } + s->phase = MEOW_SCROLL_IDLE; +} + +int meow_scroll_active(meow_scroll *s) { return (s && s->phase != MEOW_SCROLL_IDLE) ? 1 : 0; } +int meow_scroll_offset(meow_scroll *s) { return s ? s->get(s->ctx) : 0; } +void meow_scroll_set_snap(meow_scroll *s, int step) { if (s) s->snap_step = step > 0 ? step : 0; } +void meow_scroll_set_friction(meow_scroll *s, int tau_ms) +{ if (s) s->tau_ms = (tau_ms > 20 && tau_ms <= 2000) ? tau_ms : MEOW_SCROLL_TAU; } diff --git a/meow_ui_view.c b/meow_ui_view.c index e1dc8e1..645e677 100644 --- a/meow_ui_view.c +++ b/meow_ui_view.c @@ -9,6 +9,7 @@ #include "meow_ui.h" #include "meow_ui_internal.h" #include "meow_ui_view.h" +#include "include/meow_ui_box.h" #include #include @@ -17,6 +18,50 @@ static meow_view *g_view_by_id[MEOW_VIEW_BY_ID_MAX]; /* id → 顶层页 view(page_push/pop 查找) */ static meow_view *g_top_view = NULL; /* 当前进入的顶层页 view(生命周期跟踪) */ +/* ---- 布局提供者注册表(view → meow_box,opt-in) ---- */ +static struct { meow_view *v; meow_box *box; } g_view_layout[MEOW_VIEW_BY_ID_MAX]; +static int g_view_layout_n = 0; + +static void layout_reg_set(meow_view *v, meow_box *box) +{ + for (int i = 0; i < g_view_layout_n; i++) { + if (g_view_layout[i].v == v) { + g_view_layout[i].box = box; + return; + } + } + if (g_view_layout_n < MEOW_VIEW_BY_ID_MAX) { + g_view_layout[g_view_layout_n].v = v; + g_view_layout[g_view_layout_n].box = box; + g_view_layout_n++; + } +} +static meow_box *layout_reg_get(const meow_view *v) +{ + for (int i = 0; i < g_view_layout_n; i++) + if (g_view_layout[i].v == v) return g_view_layout[i].box; + return NULL; +} + +/* 重排:把 view 的布局 box 解算一次(box 矩形未设时自动跟 view 内容矩形)。 */ +void meow_view_relayout(meow_view *v) +{ + if (!v) return; + meow_box *b = layout_reg_get(v); + if (!b) return; + int r[4]; meow_box_rect(b, r); + if (r[2] <= 0 || r[3] <= 0) /* w/h 未设 → 跟 view 内容矩形 */ + meow_box_set_rect(b, v->x, v->y, v->w, v->h); + meow_box_arrange(b); +} +void meow_view_set_layout(meow_view *v, struct meow_box *box) +{ + if (!v) return; + if (!box) { layout_reg_set(v, NULL); return; } + layout_reg_set(v, box); + meow_view_relayout(v); /* 立即按当前尺寸排一次 */ +} + static void run_fn(meow_view_fn f, meow_view *v) { if (f) f(v, v->ctx); @@ -146,6 +191,7 @@ void meow_view_page_push(int id) if (cur && cur != next) view_leave(cur); meow_ui_activity_push(id); view_enter(next); + meow_view_relayout(next); /* 布局提供者(meow_box)随页进入自动排一次 */ g_top_view = next ? next : cur; } @@ -156,6 +202,7 @@ void meow_view_page_pop(void) meow_ui_activity_pop(); meow_view *next = view_map_get(meow_ui_activity_active()); view_enter(next); + meow_view_relayout(next); g_top_view = next ? next : NULL; } @@ -167,6 +214,7 @@ void meow_view_page_replace(int id) meow_view *next = view_map_get(id); if (cur && cur != next) view_leave(cur); view_enter(next); + meow_view_relayout(next); g_top_view = next ? next : cur; } diff --git a/meow_ui_view.h b/meow_ui_view.h index 729d703..73d8a96 100644 --- a/meow_ui_view.h +++ b/meow_ui_view.h @@ -97,6 +97,14 @@ int meow_view_screen_y(const meow_view *v); meow_view *meow_view_parent(const meow_view *v); int meow_view_get_state(const meow_view *v); /* MEOW_VIEW_STATE_* 位掩码 */ +/* ---- 布局提供者(§12.1.4:meow_box 作为 view 的 layout,opt-in)---- + * meow_box(见 include/meow_ui_box.h)是**纯布局计算器**,arrange 把子控件算成绝对坐标。 + * 设了布局的 view:其直接挂载控件应**同时**加入该 box(box 会在 arrange 时写这些控件的 + * x/y/w/h);page 进入/relayout 时框架自动 arrange。未设布局的 view 保持现有绝对坐标行为 + * (on_layout 里自己算,零改动)。box 矩形默认跟 view 内容矩形(w/h 未设时自动填)。 */ +void meow_view_set_layout(meow_view *v, struct meow_box *box); /* NULL=取消 */ +void meow_view_relayout(meow_view *v); /* 手动重排(改 box 后调) */ + #ifdef __cplusplus } #endif diff --git a/meow_ui_widget.c b/meow_ui_widget.c index 4f8ef37..fc86847 100644 --- a/meow_ui_widget.c +++ b/meow_ui_widget.c @@ -465,6 +465,17 @@ void meow_widget_rect(const meow_widget *w, int out[4]) out[0] = w->x; out[1] = w->y; out[2] = w->w; out[3] = w->h; } +/* 写控件矩形(§12.1 meow_box 布局引擎用:把算出的绝对坐标写进控件)。 + * 只设字段,不标脏(arrange ≠ 绘制;app 在 arrange 后自行重绘 + flush_region)。 + * 尺寸 clamp 到 >=0。既有的 meow_layout_arrange 在 TU 内直接访问字段,不受影响。 */ +void meow_widget_set_rect(meow_widget *w, int x, int y, int wd, int ht) +{ + if (!w) return; + w->x = x; w->y = y; + w->w = wd > 0 ? wd : 0; + w->h = ht > 0 ? ht : 0; +} + /* ============================================================ */ /* §5.8.6 batch2 Material 组件子集 API(create/setter/getter/hit) */ /* ============================================================ */ diff --git a/test_meow_ui_anim.c b/test_meow_ui_anim.c new file mode 100644 index 0000000..531d9d3 --- /dev/null +++ b/test_meow_ui_anim.c @@ -0,0 +1,133 @@ +/* test_meow_ui_anim.c — §12.2 meow_anim 动画基座宿主验证: + * 缓动数值(tween 中点/终点)、生命周期(start/update/done/cancel)、并行多动画、脏区 flush。 + * 时间推进用真实 usleep(meow_ui_time_ms 是单调钟),对中点用宽容差、终点/取消做精确断言。 + * + * 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/draw/activity/ime + 打桩 font/global): + * gcc -o test_meow_ui_anim test_meow_ui_anim.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_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 \ + * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm + * 运行:./test_meow_ui_anim + */ +#include "meow_ui.h" +#include "meow_ui_internal.h" /* meow_ui_back_buf 等(脏区 flush 值域校验) */ +#include +#include +#include +#include + +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 NEAR(a, b) (((a)-(b)) < 0.0001f && ((b)-(a)) < 0.0001f) + +/* ---- 缓动数值 ---- */ +static void test_ease(void) +{ + CHECK(NEAR(meow_ease_apply(MEOW_EASE_LINEAR, 0.0f), 0.0f), "lin 0"); + CHECK(NEAR(meow_ease_apply(MEOW_EASE_LINEAR, 0.5f), 0.5f), "lin .5"); + CHECK(NEAR(meow_ease_apply(MEOW_EASE_LINEAR, 1.0f), 1.0f), "lin 1"); + CHECK(NEAR(meow_ease_apply(MEOW_EASE_OUT, 0.0f), 0.0f), "out 0"); + CHECK(NEAR(meow_ease_apply(MEOW_EASE_OUT, 1.0f), 1.0f), "out 1"); + CHECK(meow_ease_apply(MEOW_EASE_OUT, 0.5f) > 0.5f && meow_ease_apply(MEOW_EASE_OUT, 0.5f) < 1.0f, "out .5 between"); + CHECK(NEAR(meow_ease_apply(MEOW_EASE_IN_OUT, 0.0f), 0.0f), "inout 0"); + CHECK(NEAR(meow_ease_apply(MEOW_EASE_IN_OUT, 0.5f), 0.5f), "inout .5"); + CHECK(NEAR(meow_ease_apply(MEOW_EASE_IN_OUT, 1.0f), 1.0f), "inout 1"); + CHECK(NEAR(meow_ease_apply(MEOW_EASE_OVERSHOOT, 0.0f), 0.0f), "overshoot 0"); + CHECK(NEAR(meow_ease_apply(MEOW_EASE_OVERSHOOT, 1.0f), 1.0f), "overshoot 1"); + CHECK(meow_ease_apply(MEOW_EASE_OVERSHOOT, 0.85f) > 1.0f, "overshoot mid >1"); +} + +/* ---- update/done 记录 ---- */ +typedef struct { meow_anim *a; float last; int updates; int done; int canceled; } anim_track; +static void on_update(meow_anim *a, float v, void *ctx) +{ anim_track *t = ctx; t->last = v; t->updates++; } +static void on_done(meow_anim *a, int canceled, void *ctx) +{ anim_track *t = ctx; t->a = a; t->done++; t->canceled = canceled; } + +static void test_tween_complete(void) +{ + anim_track t; memset(&t, 0, sizeof t); + meow_anim_desc d; memset(&d, 0, sizeof d); + d.from = 0; d.to = 100; d.duration_ms = 200; d.ease = MEOW_EASE_LINEAR; + d.update = on_update; d.done = on_done; d.ctx = &t; + meow_anim *a = meow_anim_start(&d); + CHECK(a != NULL, "start"); + CHECK(meow_anim_active(a) == 1, "active at start"); + CHECK(NEAR(meow_anim_value(a), 0.0f), "value from"); + CHECK(t.updates == 1 && NEAR(t.last, 0.0f), "update t0"); + meow_anim_tick(); /* 与 start 同时刻:仍 ~0 */ + CHECK(t.done == 0, "not done yet"); + usleep(100000); meow_anim_tick(); /* 中点 */ + CHECK(t.last > 10.0f && t.last < 90.0f, "mid value %f", t.last); + usleep(150000); meow_anim_tick(); /* 超过 duration */ + CHECK(t.done == 1 && t.canceled == 0, "done once non-cancel"); + CHECK(NEAR(t.last, 100.0f), "final value 100 got %f", t.last); + CHECK(meow_anim_active(a) == 0, "inactive after done"); + usleep(10000); meow_anim_tick(); + CHECK(t.done == 1, "no re-done"); +} + +static void test_cancel(void) +{ + anim_track t; memset(&t, 0, sizeof t); + meow_anim_desc d; memset(&d, 0, sizeof d); + d.from = 0; d.to = 100; d.duration_ms = 500; d.ease = MEOW_EASE_LINEAR; + d.update = on_update; d.done = on_done; d.ctx = &t; + meow_anim *a = meow_anim_start(&d); + usleep(50000); meow_anim_tick(); + int updates_before = t.updates; + meow_anim_cancel(a); + CHECK(t.done == 1 && t.canceled == 1, "cancel done(canceled=1)"); + CHECK(meow_anim_active(a) == 0, "inactive after cancel"); + meow_anim_tick(); + CHECK(t.updates == updates_before, "no update after cancel"); +} + +static void test_parallel(void) +{ + anim_track t1, t2; memset(&t1, 0, sizeof t1); memset(&t2, 0, sizeof t2); + meow_anim_desc a, b; memset(&a, 0, sizeof a); memset(&b, 0, sizeof b); + a.from = 0; a.to = 10; a.duration_ms = 120; a.ease = MEOW_EASE_LINEAR; a.update = on_update; a.done = on_done; a.ctx = &t1; + b.from = 100; b.to = 0; b.duration_ms = 120; b.ease = MEOW_EASE_LINEAR; b.update = on_update; b.done = on_done; b.ctx = &t2; + meow_anim *x = meow_anim_start(&a); + meow_anim *y = meow_anim_start(&b); + CHECK(x && y, "parallel start"); + CHECK(meow_anim_count() == 2, "count 2"); + usleep(150000); meow_anim_tick(); + CHECK(t1.done == 1 && t2.done == 1, "both done"); + CHECK(NEAR(t1.last, 10.0f) && NEAR(t2.last, 0.0f), "endpoints"); + CHECK(meow_anim_count() == 0, "count 0"); +} + +static void test_region_flush(void) +{ + anim_track t; memset(&t, 0, sizeof t); + meow_anim_desc d; memset(&d, 0, sizeof d); + d.from = 0; d.to = 1; d.duration_ms = 80; d.ease = MEOW_EASE_LINEAR; + d.update = on_update; d.done = on_done; d.ctx = &t; + meow_anim *a = meow_anim_start(&d); + meow_anim_set_region(a, 100, 200, 60, 40); /* 每 tick 只 flush 此区 */ + usleep(100000); meow_anim_tick(); + CHECK(t.done == 1, "region anim done"); + CHECK(t.updates >= 1, "region updates"); +} + +int main(void) +{ + meow_ui_opts o = { 480, 720 }; + if (meow_ui_init(&o) != 0) { printf("init failed\n"); return 1; } + test_ease(); + test_tween_complete(); + test_cancel(); + test_parallel(); + test_region_flush(); + meow_ui_shutdown(); + printf("anim: %d pass / %d fail\n", g_pass, g_fail); + return g_fail ? 1 : 0; +} diff --git a/test_meow_ui_box.c b/test_meow_ui_box.c new file mode 100644 index 0000000..5567eee --- /dev/null +++ b/test_meow_ui_box.c @@ -0,0 +1,231 @@ +/* test_meow_ui_box.c — §12.1 meow_box 线性布局引擎宿主验证: + * 布局求解矩阵(固定/weight/wrap/嵌套/对齐)、margin/padding/gap、交叉轴对齐、view 集成。 + * 纯逻辑(只创控件 + arrange + 断言矩形),不依赖 /dev/fb0/字库;不绘制。 + * + * 编译(宿主 gcc,链真实 widget/draw/activity/theme/gfx + 打桩 font/global): + * gcc -o test_meow_ui_box test_meow_ui_box.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_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 \ + * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm + * 运行:./test_meow_ui_box + */ +#include "meow_ui.h" +#include "meow_ui_internal.h" /* meow_widget_rect:真实几何断言 */ +#include "meow_ui_view.h" +#include +#include +#include + +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) + +static void wr(meow_widget *w, int out[4]) { meow_widget_rect(w, out); } +static void boxr(meow_box *b, int out[4]) { meow_box_rect(b, out); } + +/* 创建固定尺寸按钮(既有无绝对定位;注册到绘制物注册表即可) */ +static meow_widget *mk(int w, int h) +{ + meow_widget *b = meow_btn_create(0, 0, w, h, "X"); + return b; +} + +/* ---- 竖直列:固定 + padding + gap + CROSS_START ---- */ +static void test_col_fixed(void) +{ + meow_box *bx = meow_box_new(MEOW_BOX_V); + CHECK(bx != NULL, "box_new V"); + meow_box_set_rect(bx, 0, 0, 480, 720); + meow_box_set_padding(bx, 20, 20, 20, 20); + meow_box_set_gap(bx, 10); + meow_box_set_align(bx, MEOW_BOX_ALIGN_START, MEOW_BOX_CROSS_START); + meow_widget *a = mk(100, 50), *b = mk(120, 50), *c = mk(80, 50); + CHECK(meow_box_add(bx, a, MEOW_BOX_FIXED, 0) == 0, "add a"); + CHECK(meow_box_add(bx, b, MEOW_BOX_FIXED, 0) == 0, "add b"); + CHECK(meow_box_add(bx, c, MEOW_BOX_FIXED, 0) == 0, "add c"); + CHECK(meow_box_count(bx) == 3, "count"); + meow_box_arrange(bx); + int r[4]; + wr(a, r); CHECK(r[0] == 20 && r[1] == 20 && r[2] == 100 && r[3] == 50, "A x=20 y=20 w100 h50 got %d,%d,%d,%d", r[0], r[1], r[2], r[3]); + wr(b, r); CHECK(r[0] == 20 && r[1] == 80 && r[2] == 120, "B y=80 got %d,%d,%d,%d", r[0], r[1], r[2], r[3]); + wr(c, r); CHECK(r[0] == 20 && r[1] == 140 && r[2] == 80, "C y=140 got %d,%d,%d,%d", r[0], r[1], r[2], r[3]); + meow_box_free(bx); +} + +/* ---- 横向行:END 对齐 + 交叉轴中心 ---- */ +static void test_row_end_center(void) +{ + meow_box *bx = meow_box_new(MEOW_BOX_H); + meow_box_set_rect(bx, 10, 0, 480, 200); + meow_box_set_padding(bx, 10, 10, 10, 10); + meow_box_set_gap(bx, 5); + meow_box_set_align(bx, MEOW_BOX_ALIGN_END, MEOW_BOX_CROSS_CENTER); + meow_widget *a = mk(100, 50), *b = mk(100, 50), *c = mk(100, 50); + CHECK(meow_box_add(bx, a, MEOW_BOX_FIXED, 0) == 0, "add"); + CHECK(meow_box_add(bx, b, MEOW_BOX_FIXED, 0) == 0, "addb"); + CHECK(meow_box_add(bx, c, MEOW_BOX_FIXED, 0) == 0, "addc"); + meow_box_arrange(bx); + /* 内容区 x∈[20,470] cw=460;foot=100*3+5*2=310;leftover=150;END → off0=150 */ + int r[4]; + wr(a, r); + CHECK(r[0] == 20 + 150, "END first x=170 got %d", r[0]); + /* 交叉轴中心:cy=10(pad_t);cross_size=ch=180,cross_base=50 → pad=130 → cross=65 → y=10+65=75 */ + CHECK(r[1] == 75 && r[3] == 50, "center y=75 got %d,%d", r[1], r[3]); + wr(b, r); CHECK(r[0] == 20 + 150 + 100 + 5, "END second x=%d", r[0]); + wr(c, r); CHECK(r[0] == 380, "END third x=380 got %d", r[0]); /* 170+100+5+100+5+? = 380 */ + meow_box_free(bx); +} + +/* ---- weight 均分:竖直,两个 weight 1:3 ---- */ +static void test_weight(void) +{ + meow_box *bx = meow_box_new(MEOW_BOX_H); + meow_box_set_rect(bx, 0, 0, 480, 200); + /* 内容区 480 宽(无 pad/gap)。child 固定 80 + 两个权重 1:3 */ + meow_widget *fixed = mk(80, 200), *w1 = mk(0, 200), *w2 = mk(0, 200); + CHECK(meow_box_add(bx, fixed, MEOW_BOX_FIXED, 0) == 0, "fixed"); + CHECK(meow_box_add(bx, w1, MEOW_BOX_WEIGHT, 1) == 0, "w1"); + CHECK(meow_box_add(bx, w2, MEOW_BOX_WEIGHT, 3) == 0, "w2"); + meow_box_arrange(bx); + /* leftover = 480 - 80 = 400;w1=100, w2=300 */ + int r[4]; + wr(fixed, r); CHECK(r[0] == 0 && r[2] == 80, "fixed x=0 w80 got %d,%d", r[0], r[2]); + wr(w1, r); CHECK(r[0] == 80 && r[2] == 100, "w1 x=80 w100 got %d,%d", r[0], r[2]); + wr(w2, r); CHECK(r[0] == 180 && r[2] == 300, "w2 x=180 w300 got %d,%d", r[0], r[2]); + meow_box_free(bx); +} + +/* ---- WRAP 内容测量(measure 回调给 min/preferred) ---- */ +static void wrap_measure(void *ctx, int *mn, int *pf) { (void)ctx; *mn = 40; *pf = 120; } +static void test_wrap(void) +{ + meow_box *bx = meow_box_new(MEOW_BOX_H); + meow_box_set_rect(bx, 0, 0, 480, 100); + meow_box_set_gap(bx, 10); + meow_box_set_align(bx, MEOW_BOX_ALIGN_START, MEOW_BOX_CROSS_START); + meow_widget *w = mk(5, 40); /* 尺寸无关,wrap 用 measure */ + CHECK(meow_box_add_ex(bx, w, MEOW_BOX_WRAP, 0, 40, 120, wrap_measure, NULL) == 0, "wrap-add"); + meow_box_arrange(bx); + int r[4]; wr(w, r); + /* wrap 主轴向 preferred=120;cross_start 交叉=40(widget h 落 V=40?这是 H box,交叉=h) */ + CHECK(r[2] == 120, "wrap pref w=120 got %d", r[2]); + meow_box_free(bx); +} + +/* ---- 嵌套:Row 内嵌 Column ---- */ +static void test_nested(void) +{ + meow_box *row = meow_box_new(MEOW_BOX_H); + meow_box *col = meow_box_new(MEOW_BOX_V); + meow_box_set_rect(row, 0, 0, 480, 200); + meow_box_set_rect(col, 0, 0, 200, 200); + meow_box_set_gap(row, 10); + meow_widget *a = mk(100, 50), *b = mk(100, 50), *c = mk(100, 50); + meow_box_add(row, a, MEOW_BOX_FIXED, 0); + meow_box_add_sub(row, col, MEOW_BOX_FIXED, 0); + CHECK(meow_box_add_sub(row, col, MEOW_BOX_FIXED, 0) < 0, "sub dedupe"); + meow_box_add(col, b, MEOW_BOX_FIXED, 0); + meow_box_add(col, c, MEOW_BOX_FIXED, 0); + meow_box_arrange(row); + int r[4]; + wr(a, r); CHECK(r[0] == 0 && r[2] == 100, "row a x0 w100 got %d,%d", r[0], r[2]); + int cr[4]; boxr(col, cr); CHECK(cr[0] == 110 && cr[1] == 0 && cr[2] == 200, "sub box x=110 got %d", cr[0]); + /* col 内 b,c 纵向:y 0,50(col y=0, gap0, cross_start) */ + meow_widget_rect(b, r); CHECK(r[0] == 110 && r[1] == 0, "col b y0 got %d", r[1]); + meow_widget_rect(c, r); CHECK(r[1] == 50, "col c y50 got %d", r[1]); + meow_box_free(row); meow_box_free(col); +} + +/* ---- margin(widget opts)+ 交叉轴 STRETCH ---- */ +static void test_margin_stretch(void) +{ + meow_box *bx = meow_box_new(MEOW_BOX_V); + meow_box_set_rect(bx, 0, 0, 480, 300); + meow_box_set_align(bx, MEOW_BOX_ALIGN_START, MEOW_BOX_CROSS_STRETCH); + meow_widget *a = mk(100, 60); + meow_widget_opts o; memset(&o, 0, sizeof o); + o.margin_x = 30; o.margin_y = 10; + CHECK(meow_widget_set_opts(a, &o) == 0, "set opts"); + meow_box_add(bx, a, MEOW_BOX_FIXED, 0); + meow_box_arrange(bx); + int r[4]; wr(a, r); + /* V:交叉轴=x,STRETCH → w = cw - 2*margin_x = 480 - 60 = 420;x = margin_x = 30。 + * 主轴 y:lead=margin_y=10 → y=10;ext=h=60。 */ + CHECK(r[0] == 30 && r[2] == 420, "stretch w=420 x=30 got %d,%d", r[0], r[2]); + CHECK(r[1] == 10 && r[3] == 60, "y=10 h=60 got %d,%d", r[1], r[3]); + meow_box_free(bx); +} + +/* ---- SPACE_BETWEEN ---- */ +static void test_space_between(void) +{ + meow_box *bx = meow_box_new(MEOW_BOX_H); + meow_box_set_rect(bx, 0, 0, 500, 100); + meow_box_set_align(bx, MEOW_BOX_ALIGN_SPACE_BETWEEN, MEOW_BOX_CROSS_START); + meow_widget *a = mk(100, 50), *b = mk(100, 50), *c = mk(100, 50); + meow_box_add(bx, a, MEOW_BOX_FIXED, 0); + meow_box_add(bx, b, MEOW_BOX_FIXED, 0); + meow_box_add(bx, c, MEOW_BOX_FIXED, 0); + meow_box_arrange(bx); + /* cw=500;foot=300;leftover=200;n-1=2 → 每 gap += 100 → A.x=0 B.x=200 C.x=400 */ + int r[4]; + wr(a, r); CHECK(r[0] == 0, "sb A x0 got %d", r[0]); + wr(b, r); CHECK(r[0] == 200, "sb B x200 got %d", r[0]); + wr(c, r); CHECK(r[0] == 400, "sb C x400 got %d", r[0]); + meow_box_free(bx); +} + +/* ---- view 集成:meow_view_set_layout + relayout ---- */ +static void test_view_layout(void) +{ + meow_view *v = meow_view_create(900, MEOW_VIEW_ACTIVITY, NULL, NULL); + CHECK(v != NULL, "view create"); + v->x = 0; v->y = 0; v->w = 480; v->h = 720; + meow_box *bx = meow_box_new(MEOW_BOX_V); + meow_box_set_gap(bx, 8); + meow_box_set_align(bx, MEOW_BOX_ALIGN_START, MEOW_BOX_CROSS_START); + meow_widget *a = mk(300, 50), *b = mk(300, 50); + meow_box_add(bx, a, MEOW_BOX_FIXED, 0); + meow_box_add(bx, b, MEOW_BOX_FIXED, 0); + meow_view_add_widget(v, a); + meow_view_add_widget(v, b); + meow_view_set_layout(v, bx); /* relayout 立即排一次 */ + int r[4]; + wr(a, r); CHECK(r[0] == 0 && r[1] == 0 && r[2] == 300, "view a x0 y0 w300 got %d,%d,%d", r[0], r[1], r[2]); + wr(b, r); CHECK(r[1] == 58, "view b y=58 got %d", r[1]); /* 0 + 50 + gap8 */ + meow_view_free(v); + meow_box_free(bx); +} + +/* ---- 越界去重 / 池满 ---- */ +static void test_edge(void) +{ + meow_box *bx = meow_box_new(MEOW_BOX_H); + meow_widget *a = mk(10, 10), *dup = mk(10, 10); + CHECK(meow_box_add(bx, dup, MEOW_BOX_FIXED, 0) == 0, "add dup"); + CHECK(meow_box_add(bx, dup, MEOW_BOX_FIXED, 0) < 0, "dedupe"); + CHECK(meow_box_add(bx, NULL, MEOW_BOX_FIXED, 0) < 0, "null add rejected"); + meow_box_widget_at(bx, 0) ? 0 : 0; + CHECK(meow_box_widget_at(bx, 0) == dup, "widget_at 0"); + CHECK(meow_box_sub_at(bx, 0) == NULL, "sub_at null"); + meow_box_free(bx); +} + +int main(void) +{ + test_col_fixed(); + test_row_end_center(); + test_weight(); + test_wrap(); + test_nested(); + test_margin_stretch(); + test_space_between(); + test_view_layout(); + test_edge(); + printf("box: %d pass / %d fail\n", g_pass, g_fail); + return g_fail ? 1 : 0; +} diff --git a/test_meow_ui_scroll.c b/test_meow_ui_scroll.c new file mode 100644 index 0000000..693701c --- /dev/null +++ b/test_meow_ui_scroll.c @@ -0,0 +1,184 @@ +/* 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_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_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 \ + * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm + * 运行:./test_meow_ui_scroll + */ +#include "meow_ui.h" +#include "meow_ui_internal.h" +#include +#include +#include +#include + +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); +} + +int main(void) +{ + meow_ui_opts o = { 480, 720 }; + if (meow_ui_init(&o) != 0) { printf("init failed\n"); return 1; } + test_track_follow(); + test_fling_no_boundary(); + test_fling_boundary_bounce(); + test_snap(); + test_stop(); + meow_ui_shutdown(); + printf("scroll: %d pass / %d fail\n", g_pass, g_fail); + return g_fail ? 1 : 0; +} diff --git a/test_umbrella.c b/test_umbrella.c index 8e03f72..5e801e1 100644 --- a/test_umbrella.c +++ b/test_umbrella.c @@ -31,6 +31,14 @@ _Static_assert(MEOW_SBAR_HOTZONE_FS == 4, "sbar hotzone fs"); _Static_assert(MEOW_WS_PRESSED == (1u << 3), "ws pressed"); _Static_assert(MEOW_BTN_ELEVATED == 4, "btn elevated"); _Static_assert(MEOW_LAYOUT_BOX == 2, "layout box"); +/* §12.1 线性布局引擎 / §12.2 动画基座 / §12.3 滚动物理(新增模块,不改变既有值) */ +_Static_assert(MEOW_BOX_V == 1, "box dir v"); +_Static_assert(MEOW_BOX_WEIGHT == 1, "box size weight"); +_Static_assert(MEOW_BOX_WRAP == 2, "box size wrap"); +_Static_assert(MEOW_BOX_ALIGN_SPACE_BETWEEN == 3, "box align between"); +_Static_assert(MEOW_BOX_CROSS_STRETCH == 3, "box cross stretch"); +_Static_assert(MEOW_EASE_OVERSHOOT == 3, "ease overshoot"); +_Static_assert(MEOW_ANIM_MAX == 16, "anim pool"); _Static_assert(MEOW_LIST_SEL_MULTI == 2, "list sel multi"); _Static_assert(MEOW_NOTIFY_CAT_MEDIA == 5, "notify cat media"); _Static_assert(MEOW_NOTIFY_URG_CRITICAL == 3, "notify urg critical"); @@ -78,6 +86,13 @@ static void umbrella_smoke(void) (void)&meow_font_enumerate; (void)&meow_font_tier; (void)&meow_font_select_by_id; /* 音频(§11) */ (void)&meow_audio_open; (void)&meow_audio_query; (void)&meow_audio_on_preempt; + /* §12.1 布局 / §12.2 动画 / §12.3 滚动 */ + (void)&meow_box_new; (void)&meow_box_arrange; (void)&meow_box_add; + (void)&meow_box_add_sub; (void)&meow_widget_set_rect; + (void)&meow_anim_start; (void)&meow_anim_cancel; (void)&meow_anim_tick; + (void)&meow_ease_apply; (void)&meow_anim_set_region; + (void)&meow_scroll_create; (void)&meow_scroll_wrap; (void)&meow_scroll_track; + (void)&meow_scroll_fling; (void)&meow_scroll_velocity; (void)&meow_scroll_set_snap; } /* 供宿主回归驱动直接调用;不链接本库也没关系(纯地址丢弃)。 */