SDK 冲刺批①+②+⑤: meow_box 线性布局 + meow_anim 动画基座 + meow_scroll 滚动物理

- meow_box(include/meow_ui_box.h + meow_ui_box.c):竖直/水平容器;子项=固定|weight|wrap;
  margin/padding/gap;主轴对齐 start/center/end/space-between/around/evenly;交叉轴 start/center/end/stretch;
  嵌套(深度≤16 静态栈,单遍求解,无递归爆炸)。可作 meow_view 布局提供者
  (meow_view_set_layout/relayout,opt-in;未设布局的 view 保持现有绝对坐标行为)。
  新增 meow_widget_set_rect(只写字段不标脏)作为 box 写坐标的通道。
- meow_anim(include/meow_ui_anim.h + meow_ui_anim.c):时间驱动 tween(from/to/duration/easing:
  linear/ease-out/ease-in-out/overshoot)+ update/done 回调;静态池≤16、取消、并行多动画;
  meow_anim_set_region 让每帧只 flush 动画影响区(不整屏)。meow_anim_tick 挂主循环 on_tick,
  无动画零开销(只查计数即返回)。
- meow_scroll(include/meow_ui_scroll.h + meow_ui_scroll.c):速度跟踪(最近 N 采样加权)+ 松手 fling
  (减速度曲线,用 anim 基座驱动)+ 边界过冲 clamp + 简单回弹(不做 spring)+ 可选 snap;
  跟手拖动行为不变(track 内部调既有 set_scroll);每帧只 redraw 列表视口区;不引 libm。
- 追加式:既有 meow_row/column/box 与全部 meow_*_create/hit/scroll 签名一行未改;旧绝对坐标永远有效;
  既有 App 免改源码。
- 验证:宿主新测试 box(42)/anim(32)/scroll(17) 全 PASS;既存 9 回归(widgets/sdk_input_keys/ime_fix/
  pinyin/overlay_suppress/notif_gesture/sdk_fixes/sb_collapse/p1p2p3_repro)全 PASS;
  离屏 demo(demo_box_scroll.c)渲染 box 布局 + 列表 fling 正常;zig-mipsel -Wall -Wextra 全 TU 0 warning;
  meow_ui.a md5 83dd6a2d0d24cc748a8659c29f1de2b5(未重链/部署 hello/files/ncm/reader,由主会话统一处理)。
This commit is contained in:
MeowOS committed 2026-09-01 23:49:24 +08:00
1 parent 553a7d5d9e
commit 5a5e995ebd
19 files changed
+1839 -2

No files matched your search

+1 -1
View File
@@ -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 =="
+89
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#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;
}
+75
View File
@@ -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 */
+113
View File
@@ -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 */
+65
View File
@@ -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 */
+2
View File
@@ -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);
+6
View File
@@ -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 */
+128
View File
@@ -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 <string.h>
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);
}
}
}
+307
View File
@@ -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 <string.h>
#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;
}
}
}
+3
View File
@@ -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)也保持时钟走动。
+293
View File
@@ -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 <string.h>
#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; }
+48
View File
@@ -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 <stdlib.h>
#include <string.h>
@@ -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;
}
+8
View File
@@ -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
+11
View File
@@ -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) */
/* ============================================================ */
+133
View File
@@ -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 <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 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;
}
+231
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.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)
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;
}
+184
View File
@@ -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 <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);
}
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;
}
+15
View File
@@ -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;
}
/* 供宿主回归驱动直接调用;不链接本库也没关系(纯地址丢弃)。 */