diff --git a/sdk/build_sdk.sh b/sdk/build_sdk.sh index a940537..e1bbf5d 100755 --- a/sdk/build_sdk.sh +++ b/sdk/build_sdk.sh @@ -14,8 +14,8 @@ AR="${AR:-/usr/bin/ar}" # 系统 GNU ar 即可归档 mipsel ELF 对象(架 rm -f meow_ui.a *.o # meow_font.c = 预渲染位图字体运行时(自包含:只含 meow_font.h + 标准头;最小 zlib ABI 自声明, -# 链接时用 sysroot 的 libz.a),随 SDK 一并 vendored。 -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_font" +# 链接时用 sysroot 的 libz.a),随 SDK 一并 vendored。新模块(box/anim/scroll/str)随主仓增量并入。 +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_font meow_ui_box meow_ui_anim meow_ui_scroll meow_ui_str" CFLAGS="-Os -static -Iinclude -I." for f in $OBJS; do echo "== cc $f ==" diff --git a/sdk/demo_box_scroll.c b/sdk/demo_box_scroll.c new file mode 100644 index 0000000..c36e1ad --- /dev/null +++ b/sdk/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/sdk/include/meow_ui_anim.h b/sdk/include/meow_ui_anim.h new file mode 100644 index 0000000..a6dd626 --- /dev/null +++ b/sdk/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/sdk/include/meow_ui_box.h b/sdk/include/meow_ui_box.h new file mode 100644 index 0000000..4171652 --- /dev/null +++ b/sdk/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/sdk/include/meow_ui_image.h b/sdk/include/meow_ui_image.h index 88c15e0..b2de71a 100644 --- a/sdk/include/meow_ui_image.h +++ b/sdk/include/meow_ui_image.h @@ -26,6 +26,9 @@ int meow_img_w(meow_img *img); int meow_img_h(meow_img *img); void meow_ui_img(int x, int y, meow_img *img); /* 原始尺寸 */ +/* 内存瘦身:最近邻等比缩放到最长边 ≤maxdim,返回新图;原图已 ≤maxdim 返回 NULL。 + * 调用方负责 free 旧图与返回值(若返回)。封面/背景大图驻留内存瘦身用。 */ +meow_img *meow_img_downscale(meow_img *img, int maxdim); void meow_ui_img_scaled(int x, int y, int w, int h, meow_img *img); /* 最近邻缩放 */ void meow_ui_img_alpha(int x, int y, int w, int h, meow_img *img, int alpha); void meow_ui_img_cover(int x, int y, int size, meow_img *img); /* 居中裁方 → size×size */ diff --git a/sdk/include/meow_ui_input.h b/sdk/include/meow_ui_input.h index 956dbe1..9f2e8e1 100644 --- a/sdk/include/meow_ui_input.h +++ b/sdk/include/meow_ui_input.h @@ -58,6 +58,60 @@ void meow_ui_set_long_press_cb(meow_long_press_fn cb); /* 注册/更换回调 void meow_ui_set_long_press_ms(int ms); /* 长按阈值(ms);<=0 恢复默认 500 */ int meow_ui_long_press_ms(void); /* 当前阈值(默认 500) */ +/* ============================================================ */ +/* §6.4 事件分发形式化(2026-09-02)—— 统一「命中测试 / 捕获 / 消费 / 层间隔离 / 焦点」 + * + * 背景:此前命中/捕获/消费/层间隔离各处 ad-hoc 补丁(模式层穿透、弹层叠黑、list 焦点残留、 + * 同坐标二次消费)。本节把三层模型的形式化落地为**单一仲裁者**(SDK 内部实现,见 + * devdocs/plan/SDK_VIEW_FRAMEWORK_DESIGN.md §A.7 命中分流 ③②① + 本 SDK 内部拓扑): + * + * 触摸沿 z 序**自顶向下**命中:模态层永远先审;某一层命中即消费(返回 consumed 才继续下探)。 + * 某一次 touch-down 命中后,捕获直到 up;期间 move/up 只发给捕获目标(防滑出误触发下层)。 + * 模态层存在时下层**完全收不到**事件——未命中面板的事件由模态背板消费,不透传。 + * + * 兼容:**只追加、不删改既有签名**;未调用本节 API 的 App 行为与之前完全一致(命中/捕获仍 + * 由 App 在 on_tap/on_touch_* 里自理)。hello/files/ncm/reader 等既有 App 只把 SDK 提供的 + * 语义用于收敛 bug,不改正常交互。 + */ +/* ---- 模态作用域(modal scope)---- + * 一组自顶向下优先的叠层。dialog/sheet/App 自绘模态在「打开时 begin、关闭时 end」。 + * 作用域存在(meow_ui_modal_active / depth>0)时,下层(view/widget)完全收不到触摸; + * 未命中面板的事件由背板消费(meow_ui_hit_topdown 恒返回模态哨兵)。支持嵌套(弹层叠开, + * depth 递增)。begin 返回作用域句柄;end 销栈(幂等,NULL 忽略)。 */ +typedef struct meow_ui_modal meow_ui_modal; +meow_ui_modal *meow_ui_modal_begin(void); /* 打开模态层入栈;返回句柄(NULL=栈满/上限) */ +void meow_ui_modal_end(meow_ui_modal *m); /* 关闭:弹到 m(含 m,一次性关上嵌套) */ +int meow_ui_modal_active(void); /* 当前是否有任一模态作用域打开(depth>0) */ +int meow_ui_modal_depth(void); /* 模态栈深(弹层叠开计数) */ + +/* ---- 自上而下命中 ---- */ +/* 沿 z 序(模态最高优先,其次注册控件 z 降序;均含 owner==active+enable 过滤)返回第一个 + * 命中绘制物(meow_widget* 或 meow_view*);无命中返回 NULL。模态存在时恒返回模态哨兵 + * (背板消费,不透传)。供 gesture_begin/focus_tap/App 自绘模态命中统一使用。 */ +void *meow_ui_hit_topdown(int x, int y); +/* meow_widget_hit 的公共别名(owner==active + enable + 矩形/自定义命中回调一致)。 */ +int meow_ui_widget_hit(meow_widget *w, int x, int y); + +/* ---- 手势捕获(gesture capture)---- + * touch-down 命中目标捕获整个手势直到 up。捕获期 meow_ui_gesture_owner 恒返回捕获目标; + * 后续 move/up 只发给它(防滑出后误触发下层)。begin 返回捕获目标(未命中=NULL 不捕获); + * end/抬手释放。 */ +void *meow_ui_gesture_begin(int x, int y); /* 命中+捕获;返回捕获目标(模态存在时=模态哨兵) */ +void *meow_ui_gesture_owner(void); /* 当前手势捕获目标(无=NULL) */ +void meow_ui_gesture_end(void); /* 手势结束(抬手/TAP/SWIPE 边沿):释放捕获 */ + +/* ---- 唯一焦点(focus,root 根治 list 焦点残留 / 滚动容器间转移)---- + * 焦点持有者唯一。点非焦点区域 → 释放焦点并触发 lost 回调;滚动容器(list/scroll)在 + * 获得焦点时 acquire,点外由 meow_ui_focus_tap 统一释放——「命中非 focus 区域 → 释放并通知」。 + * 转移规则:某滚动容器 acquire 后,随后的纵向 swipe / 拖动滚该容器;任何落在该容器外的 + * 触摸(hit != focus owner)释放焦点,回退到页面级滚动。 */ +typedef void (*meow_ui_focus_lost_fn)(void *owner, void *ctx); /* 焦点被释放/转移时回调 */ +void meow_ui_focus_set_lost_cb(meow_ui_focus_lost_fn cb, void *ctx); /* 注册/更换焦点丢失回调 */ +void *meow_ui_focus_owner(void); /* 当前唯一焦点持有者(无=NULL) */ +int meow_ui_focus_acquire(void *owner); /* 设为唯一焦点(覆盖旧焦点并触发其 lost) */ +int meow_ui_focus_release(void *owner); /* owner==当前焦点才释放+通知(NULL=任意) */ +void meow_ui_focus_tap(int x, int y); /* TAP 落点:命中焦点持有者→保位;否则释放+通知 */ + #ifdef __cplusplus } #endif diff --git a/sdk/include/meow_ui_scroll.h b/sdk/include/meow_ui_scroll.h new file mode 100644 index 0000000..adafdb7 --- /dev/null +++ b/sdk/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/sdk/include/meow_ui_str.h b/sdk/include/meow_ui_str.h new file mode 100644 index 0000000..2ce33e3 --- /dev/null +++ b/sdk/include/meow_ui_str.h @@ -0,0 +1,55 @@ +/* + * meow_ui_str.h — SDK 字符串资源(i18n 雏形,§13.meow_str_*)。 + * + * 定位:公开 SDK 前,系统/App UI 文案不得再硬编码在 C 源码里——第三方开发者必须能按 + * 系统 locale 拿到对应语言文案,且能在不改 SDK 二进制的情况下用 TF 卡覆盖/追加。 + * 本头声明字符串资源框架的**对外 API**(追加式,既有 App 零改动)。 + * + * 单表双语内置(zh_cn = 内置默认 / en_us),四层取值优先级(见 meow_ui_str.c): + * meow_str(id):按当前 locale 取串;override 覆盖表 > 内嵌当前语言 > 内嵌内置默认(zh_cn); + * 全程不可得则原样返回 id(调用方永不见 NULL)。 + * meow_strf(out,outsz,id,...):取串后按 snprintf 语义格式化进调用方缓冲(自带 NUL 终止)。 + * meow_str_set_locale(locale):切当前 locale("zh_cn"|"en_us"),即时生效 + 重载 TF 覆盖表 + * + best-effort 持久化到板载 settings.conf 的 ui.locale。 + * + * 依赖:纯 libc(stdio/stdlib/string/stdarg),宿主 gcc 可直接编译单测;不依赖其它 SDK 对象。 + * - locale 初值:启动首次调用惰性读系统设置 settings.conf 的 `ui.locale`,缺省 "zh_cn"。 + * - TF 覆盖表:`/../strings/.txt`(即 MeowData//strings/,见 + * §9.2 数据目录约定),`id=text` 行格式 UTF-8;无卡/无文件/不含该 id → 内嵌表兜底。 + * - 热路径(meow_str)零 malloc:内嵌为编译期静态表,覆盖表为启动加载的静态数组。 + * + * 边界(§13 明示不做,写清理由): + * 不做复数规则 / BiDi / RTL / 动态字体回退——当前形态是「按 locale 取整串」的 i18n 雏形, + * 面向 480×720 单行/短文案 UI;复数变体需占位符格式化协议,BiDi/RTL 需文本整形 + 布局镜像, + * 两者都超出「字符串资源外置」的最小必要范围,留待后续批次。 + */ +#ifndef MEOW_UI_STR_H +#define MEOW_UI_STR_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* 按当前 locale 取 id 对应的文案。id 形如 `域.名`(common.ok / player.pause)。 + * 返回静态字符串(内嵌表或覆盖表;永不返回 NULL)。未知 id 原样返回 id。 */ +const char *meow_str(const char *id); + +/* 取 id 文案后按 snprintf 语义格式化进 out(outsz 为缓冲字节数,含 NUL)。 + * 返回 snprintf 样式的「应写入字符数」(不截断);out==NULL 或 outsz<=0 返回 -1。 + * 设计理由:调用方自带缓冲,避免静态/线程局部缓冲在 IME/模态等重入路径的竞态, + * 且与 SDK 既有 snprintf 习惯一致(无格式参数时等价深拷贝 meow_str 结果)。 */ +int meow_strf(char *out, int outsz, const char *id, ...); + +/* 设当前 locale(识别 "zh_cn"|"en_us";任意串作为覆盖表文件名 + 内嵌按非 en 处理为内置默认)。 + * 即时生效(下次 meow_str 即按新 locale);重载该 locale 的 TF 覆盖表; + * best-effort 持久化到 settings.conf 的 ui.locale(失败静默,运行时仍切换)。 + * 须在 App 主线程(UI 线程)调用,避免与 meow_str 并发写覆盖表。 */ +void meow_str_set_locale(const char *locale); + +/* 当前生效 locale(内部缓冲;未初始化时读 settings 后返回,默认 "zh_cn")。 */ +const char *meow_str_locale(void); + +#ifdef __cplusplus +} +#endif +#endif /* MEOW_UI_STR_H */ diff --git a/sdk/include/meow_ui_widgets.h b/sdk/include/meow_ui_widgets.h index ee9f476..c43824b 100644 --- a/sdk/include/meow_ui_widgets.h +++ b/sdk/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); @@ -184,6 +186,29 @@ void meow_topappbar_set_back(meow_widget *w, int on); /* 1=显示返回箭头 int meow_topappbar_add_action(meow_widget *w, const char *label); /* 返回动作下标 */ #define MEOW_TOPBAR_BACK (-2) int meow_topappbar_hit(meow_widget *w, int x, int y); /* MEOW_TOPBAR_BACK=返回 idx>=0=动作 -1=无 */ +/* TopAppBar 位图动作图标(sb_icons[],Material Symbols Rounded 32x32 单通道 alpha,Apache-2.0)。 + * 动作项默认文本(add_action);经本 API 把某项改成位图:绘制取主题 onSurface(按压淡色), + * 与状态栏同源 blit(ink 垂直对齐栏中点)。返回 0=成功;NULL/非 topbar/越界 idx/非法 icon=负值。 */ +int meow_topappbar_set_item_icon(meow_widget *w, int idx, int icon_idx); +/* 位图动作图标索引(= sb_icons[] 下标,与 SDK sb_icons.h 的 SB_I_* 同值)。 + * 状态栏占用 0-14(不动);动作图标从 15 起连续追加,勿改序(须与 sb_icons.h 表序一致)。 */ +enum { + MEOW_ICON_SETTINGS = 15, + MEOW_ICON_REFRESH = 16, + MEOW_ICON_SEARCH = 17, + MEOW_ICON_PLAY_ARROW = 18, + MEOW_ICON_PAUSE = 19, + MEOW_ICON_SKIP_NEXT = 20, + MEOW_ICON_SKIP_PREV = 21, + MEOW_ICON_ARROW_BACK = 22, + MEOW_ICON_CLOSE = 23, + MEOW_ICON_ADD = 24, + MEOW_ICON_CHECK = 25, + MEOW_ICON_LYRICS = 26, + MEOW_ICON_QUEUE_MUSIC = 27, + MEOW_ICON_DOWNLOAD = 28, + MEOW_ICON_HEART = 29, +}; /* ---- NavigationBar(底部导航栏)---- */ meow_widget *meow_navigationbar_create(int x, int y, int w, int h); @@ -291,6 +316,65 @@ void meow_listview_set_on_tap(meow_widget *lv, meow_listview_tap_fn cb, void *ct /* 命中行:置选中 + 回调(若设)+ 返回 idx;未命中返回 -1。 */ int meow_listview_tap_at(meow_widget *lv, int x, int y); +/* ============================================================ */ +/* §5.5 虚拟化列表(W_LISTVIEW 扩展层,2026-09-02):固定 item 池 + 多行富 item + 完全自绘 + * ------------------------------------------------------------ + * 在既有 W_LISTVIEW(src2 每帧按需取行,行为不变)之上做**虚拟化层**(API 追加式,旧 API 保留): + * - 数据源:count_fn() 返回总行数(任意长度,≥65535 不常驻 item 对象); + * bind_fn(index, item, ctx) 仅在行「进入视口」(绑定到固定池槽)时调用一次; + * - 固定 item 池:池容量 = 视口行数 + 2 静态槽(有界,见实现 MEOW_LV_POOL_MAX); + * 行离开视口不销毁,回滚不重绑(同一 index 命中同一槽 → bind 不重调); + * - 滚动只改 viewport 偏移(复用既有 set_scroll/max_scroll),不重绑未进视口的项; + * - 多行富 item:主标题+副标题(第二行小字)+左图标/缩略图+右尾件(chevron/开关/复选/⋯); + * 行高可配(set_item_h 精确 px / set_item_tier 单/双行档位默认高); + * - 默认 renderer 用主题语义色绘制富 item;App 也可经 set_item_draw 完全自绘。 + * 段头:bind 回调把 item->section=1(title=段名)→ 段头占位行(非粘性),默认 renderer 特殊绘制。 + * 空态:set_empty_text 设置文案,count==0 时视口内居中画出。 + * 兼容:既有 meow_listview_*(src2 模式)全部保留不动;本层为叠加,src2==NULL 时走既有路径。 */ + +/* 虚拟化行 item(bind 回调填充;SDK 固定池持有;不常驻 src2 那样的全量行对象)。 */ +typedef struct meow_lv_item { + const char *title; /* 主标题(必填,NULL 视为空行) */ + const char *subtitle; /* 副标题(第二行小字),可空 */ + const meow_img *icon; /* 可选:左图标/缩略图(icon_kind==MEOW_LIST_ICON_IMG 时用) */ + int icon_kind; /* MEOW_LIST_ICON_*(NONE=无图标) */ + int right; /* MEOW_LIST_RIGHT_* 右尾件 */ + int on; /* right==SWITCH 时开关态(0/1) */ + int enabled; /* 1=可交互;0=变淡不响应 */ + int section; /* 1=段头占位行(title=段名;默认 renderer 特殊绘制,非粘性) */ + void *user; /* App 私有(跨 bind 保存业务态/图/index;SDK 不碰) */ +} meow_lv_item; + +/* 虚拟化数据源:count 返回当前行数(>=0,任意长);bind 在行进入视口时被调一次填充 item。 + * 二者均不常驻 item 对象;count 每帧实时查(支持搜索过滤/懒加载动态长度)。 */ +typedef int (*meow_listview_count_fn)(void *ctx); +typedef void (*meow_listview_bind_fn)(int index, meow_lv_item *item, void *ctx); + +/* 完全自绘回调(覆盖默认 renderer):idx=行号;x,y,w,h=行视口矩形(已裁剪到列表视口); + * state=MEOW_LISTVIEW_ST_* 状态位(选中/按压/禁用组合)。App 用 §3 绘制 API 自绘整行。 */ +typedef void (*meow_listview_draw_fn)(int idx, int x, int y, int w, int h, + int state, void *ctx); +/* state 位(set_item_draw 的 draw 回调收到;选中/按压/禁用组合,供自绘整行)。 */ +#define MEOW_LISTVIEW_ST_SELECTED (1u<<0) +#define MEOW_LISTVIEW_ST_PRESSED (1u<<1) +#define MEOW_LISTVIEW_ST_DISABLED (1u<<2) + +/* 挂虚拟化数据源:切到 pool 模式(count_fn/bind_fn 均非 NULL 才生效;否则回退 src2 模式)。 + * 返回 0=成功;NULL 回调/无效=负值。挂虚拟源后既有 sel/滚动/命中/焦点照常。 */ +int meow_listview_set_vsource(meow_widget *lv, meow_listview_count_fn count, + meow_listview_bind_fn bind, void *ctx); +/* 默认 renderer 配置:lines=1/2(单行/双行档位,影响副标题显示与行高默认); + * show_icon=1 画左图标/缩略图;show_right=1 画右尾件。返回 0=成功。 */ +int meow_listview_set_item_renderer(meow_widget *lv, int lines, int show_icon, + int show_right); +/* 行档位默认行高:lines=1→48(单行)、2→56(双行);与 set_item_h(精确 px)互补。 */ +int meow_listview_set_item_tier(meow_widget *lv, int lines); +/* 完全自绘:设 draw_item 回调(覆盖默认 renderer);NULL=用默认 renderer。返回 0=成功。 */ +int meow_listview_set_item_draw(meow_widget *lv, meow_listview_draw_fn draw, + void *ctx); +/* 空态占位:count==0 时视口内居中画 empty_text(NULL 不画)。返回 0=成功。 */ +int meow_listview_set_empty_text(meow_widget *lv, const char *text); + #ifdef __cplusplus } #endif diff --git a/sdk/meow_ui.h b/sdk/meow_ui.h index cfbec99..0e9cd19 100644 --- a/sdk/meow_ui.h +++ b/sdk/meow_ui.h @@ -34,6 +34,10 @@ * 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) + * include/meow_ui_str.h 字符串资源/i18n(§13:meow_str/meow_strf/meow_str_set_locale) * * 其余模块头(非伞头、App 按需显式 include,均保持不动): * meow_ui_view.h / meow_ui_nav.h / meow_ui_media.h / meow_ui_sys.h / meow_ui_vol.h @@ -63,5 +67,9 @@ #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" +#include "include/meow_ui_str.h" #endif /* MEOW_UI_H */ diff --git a/sdk/meow_ui_anim.c b/sdk/meow_ui_anim.c new file mode 100644 index 0000000..10c530a --- /dev/null +++ b/sdk/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/sdk/meow_ui_box.c b/sdk/meow_ui_box.c new file mode 100644 index 0000000..716f1fe --- /dev/null +++ b/sdk/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/sdk/meow_ui_dialog.c b/sdk/meow_ui_dialog.c index 5d06425..5987bf9 100644 --- a/sdk/meow_ui_dialog.c +++ b/sdk/meow_ui_dialog.c @@ -125,7 +125,7 @@ static void dlg_card(meow_dialog *d, const meow_theme *th, const dlg_geom *g) int y = g->row_y; if (d->type == MEOW_DLG_INPUT) { char buf[300]; - snprintf(buf, sizeof buf, "%s", d->ime.buf[0] ? d->ime.buf : "(tap OK to edit...)"); + snprintf(buf, sizeof buf, "%s", d->ime.buf[0] ? d->ime.buf : meow_str("common.tap_ok_edit")); meow_ui_text(g->dx + pad, y, buf, th->font_body, th->onSurfaceVariant); } else { for (int i = 0; i < nmsg; i++) { @@ -138,10 +138,10 @@ static void dlg_card(meow_dialog *d, const meow_theme *th, const dlg_geom *g) int bx = g->dx + pad, cx = bx + g->btn_w + 12, by = g->btn_y; meow_ui_draw_rect_round(bx, by, g->btn_w, g->btn_h, 10, th->primary); meow_ui_text_center(bx + g->btn_w / 2, by + (g->btn_h - meow_ui_text_height(th->font_body)) / 2, - "CANCEL", th->font_body, th->primary); + meow_str("common.cancel"), th->font_body, th->primary); meow_gfx_fill_round(cx, by, g->btn_w, g->btn_h, 10, th->primary); meow_ui_text_center(cx + g->btn_w / 2, by + (g->btn_h - meow_ui_text_height(th->font_body)) / 2, - "OK", th->font_body, th->onPrimary); + meow_str("common.ok"), th->font_body, th->onPrimary); } } @@ -232,7 +232,11 @@ void meow_dialog_show(meow_dialog *d) meow_input_start(); meow_input_drain(); /* 排空残留触摸(goodix 长尾) */ d->done = 0; d->result = 0; - modal_capture(); /* M2.71-bug3:拍下背景,关闭即还原(scrim 只叠一层/无残影) */ + /* §6.4:模态作用域——本层打开期间下层(view/widget)完全收不到触摸;点卡片外由背板消费 + * (d->done=1 result=0 取消),不透传。嵌套弹层 depth 递增。 */ + meow_ui_modal *m = meow_ui_modal_begin(); + modal_capture(); /* M2.71-bug3:拍下背景,关闭即还原(scrim 只叠一层/无残影); + * 嵌套模态在各自打开时拍「当前已渲染帧」,故两层弹开不叠黑。 */ dlg_paint(d, th); meow_ui_flush(); @@ -286,6 +290,7 @@ void meow_dialog_show(meow_dialog *d) if (d->done) break; usleep(16000); } + meow_ui_modal_end(m); /* §6.4:关闭模态作用域(下层恢复接收触摸) */ modal_restore(); /* M2.71-bug3:关闭还原背景,无残影 */ } @@ -349,7 +354,8 @@ int meow_sheet_open(meow_sheet *s) int W = meow_ui_width(), H = meow_ui_height(); int ph = sheet_h(s, th); int py = H - ph, pad = 24; - modal_capture(); /* M2.71-bug3:拍下背景,关闭即还原 */ + meow_ui_modal *m = meow_ui_modal_begin(); /* §6.4:模态作用域(下层收不到触摸) */ + modal_capture(); /* M2.71-bug3:拍下背景,关闭即还原(scrim 只叠一层/无残影) */ /* 首次绘制:遮罩 + 底部面板(顶部圆角) */ meow_ui_fill_rect_alpha(0, 0, W, H, 0x000000, 150); meow_gfx_fill_round(0, py, W, ph, th->radius_card, th->surfaceContainerHighest); @@ -382,6 +388,7 @@ int meow_sheet_open(meow_sheet *s) if (s->done) break; usleep(16000); } + meow_ui_modal_end(m); /* §6.4:关闭模态作用域 */ modal_restore(); /* M2.71-bug3:关闭还原背景,无残影 */ return s->result; } diff --git a/sdk/meow_ui_draw.c b/sdk/meow_ui_draw.c index 4d4f05d..871602a 100644 --- a/sdk/meow_ui_draw.c +++ b/sdk/meow_ui_draw.c @@ -266,6 +266,28 @@ void meow_draw_view_set_z(struct meow_view *v, int z) } } +/* ============================================================ */ +/* §6.4 自上而下命中(事件分发形式化) */ +/* ============================================================ */ +void *meow_draw_hit_topdown(int x, int y) +{ + void *best = NULL; + int bestz = -1, bestseq = -1; + for (int i = 0; i < g_n; i++) { + draw_entry *e = &g_e[i]; + e_compute_auto(e); + /* 只对注册控件做顶层命中;view 是容器(其子 widget 是独立更高 z 的绘制物,各自命中), + * CUSTOM 是 App 直绘(不参与命中)。meow_widget_hit 内部含 owner==active + enable + + * 矩形/自定义命中回调。 */ + if (e->kind != MEOW_ITEM_WIDGET || !e->w) continue; + if (!meow_widget_hit(e->w, x, y)) continue; + if (e->z > bestz || (e->z == bestz && e->seq > bestseq)) { + bestz = e->z; bestseq = e->seq; best = e->w; + } + } + return best; +} + /* ============================================================ */ /* page bg / 区域重画 / 整帧 */ /* ============================================================ */ diff --git a/sdk/meow_ui_gfx.c b/sdk/meow_ui_gfx.c index 900d145..6f11df5 100644 --- a/sdk/meow_ui_gfx.c +++ b/sdk/meow_ui_gfx.c @@ -254,22 +254,21 @@ int meow_ui_back_buf_bpp(void) { return g_bpp; } * 从而 launcher 与 app 统一渲染栈(同一离屏 + 同一 present)。 */ unsigned char *meow_ui_back_buf(void) { return g_back_buf; } -/* ================= 状态栏 MD3 图标 blit(写进本 SDK back_buf) ================= - * 复用 launcher sb_icons.h(Material Symbols rounded,32x32 单通道 alpha)作为状态栏 - * 右侧图标簇(「同一个状态栏」图标式,见 meow_ui_sys_statusbar_render)。 - * idx = sb_icons[] 索引(SB_I_*,见 meow_ui_internal.h) +/* ================= MD3 图标 blit(写进本 SDK back_buf) ================= + * 复用 sb_icons.h(Material Symbols rounded,32x32 单通道 alpha)——状态栏右侧图标簇 + * (meow_ui_sys_statusbar_render)与 TopAppBar 位图动作图标(W_TOPAPPBAR)共用同源位图。 + * idx = sb_icons[] 索引(状态栏 SB_I_*、动作 MEOW_ICON_*,见 meow_ui_internal.h) * x = 图标槽左缘(槽宽 SB_ICON_SIZE=32) - * y = 图标槽顶(仅作参考;实际按 ink 垂直中心对齐 cy) * ink_h = 目标 ink 高(>0 等比缩放;0 = 保持位图自然 ink 尺寸——WiFi 强弱档用) * fg = 24-bit RGB 前景(主题 on_surface / on_surface_variant,日/夜由主题表提供) - * cy = ink 垂直中心线(= 状态栏文字中心,28px 栏约 14) + * cy = ink 垂直中心线(状态栏=28px 栏约 14;TopAppBar=栏中点) * 先求位图 ink 边界(阈值 32,忽略近透明抗锯齿),按 ink 在槽内水平居中、垂直中心对齐 cy; * 像素按 alpha 软边混合进 back_buf(自 g_fb/g_line/g_xres/g_yres,与 launcher sb_blit_icon 同源算法)。 */ -void meow_gfx_blit_icon(int idx, int x, int y, int ink_h, uint32_t fg, int cy) +/* 像素核心:只采样混合进 back_buf,不标脏区。供状态栏路径(blit 后另标顶栏脏区)与 + * 控件路径(meow_gfx_blit_icon_ink,控件 invalidation 独立管理脏区)共用,避免状态栏的 + * 顶栏脏区假设污染控件绘制(topbar 不在屏幕最顶行时标错区域)。 */ +static void icon_blit_px(int idx, int x, int ink_h, uint32_t fg, int cy) { - (void)y; - if (!g_init_ok || !g_fb) return; - if (idx < 0 || idx >= (int)(sizeof sb_icons / sizeof sb_icons[0])) return; const unsigned char *src = sb_icons[idx].data; uint16_t col = rgb565(fg); /* 1) ink 边界(含空图兜底) */ @@ -277,8 +276,10 @@ void meow_gfx_blit_icon(int idx, int x, int y, int ink_h, uint32_t fg, int cy) for (int i = 0; i < SB_ICON_SIZE * SB_ICON_SIZE; i++) { if (src[i] >= 32) { int xx = i % SB_ICON_SIZE, yy = i / SB_ICON_SIZE; - if (xx < minx) minx = xx; if (xx > maxx) maxx = xx; - if (yy < miny) miny = yy; if (yy > maxy) maxy = yy; + if (xx < minx) minx = xx; + if (xx > maxx) maxx = xx; + if (yy < miny) miny = yy; + if (yy > maxy) maxy = yy; } } if (maxx < minx || maxy < miny) { minx = miny = 0; maxx = maxy = SB_ICON_SIZE - 1; } @@ -311,10 +312,28 @@ void meow_gfx_blit_icon(int idx, int x, int y, int ink_h, uint32_t fg, int cy) row[tx] = (uint16_t)((r << 11) | (g << 5) | b); } } - /* 整条顶栏作为脏区标出(图标在顶条内,present 只重画顶条即可) */ +} + +/* 状态栏路径:blit 像素 + 把整条顶栏标脏(present 只重画顶条即可)。 */ +void meow_gfx_blit_icon(int idx, int x, int y, int ink_h, uint32_t fg, int cy) +{ + (void)y; + if (!g_init_ok || !g_fb) return; + if (idx < 0 || idx >= (int)(sizeof sb_icons / sizeof sb_icons[0])) return; + icon_blit_px(idx, x, ink_h, fg, cy); dirty_add(x, 0, SB_ICON_SIZE, MUI_SBAR_H); } +/* 控件路径(TopAppBar 动作图标):只写像素,不标「顶栏」脏区——控件矩形由 + * meow_widget_draw 的 clip + invalidation(setter 标脏)独立管理,避免把状态栏的 + * 顶栏脏区假设带进控件绘制(topbar 不位于屏幕最顶行时标错区域、触发误重画)。 */ +void meow_gfx_blit_icon_ink(int idx, int x, int ink_h, uint32_t fg, int cy) +{ + if (!g_init_ok || !g_fb) return; + if (idx < 0 || idx >= (int)(sizeof sb_icons / sizeof sb_icons[0])) return; + icon_blit_px(idx, x, ink_h, fg, cy); +} + /* ================= 像素写入 ================= */ static void px_write_rgb565(int x, int y, uint16_t c16) { diff --git a/sdk/meow_ui_ime.c b/sdk/meow_ui_ime.c index b28ad6e..2de7979 100644 --- a/sdk/meow_ui_ime.c +++ b/sdk/meow_ui_ime.c @@ -362,10 +362,10 @@ static const char *py_key_label(const struct kbkey *k, char *tmp) switch (k->kind) { case K_CH: { char c = (char)tolower((unsigned char)k->ch); tmp[0] = c; tmp[1] = 0; return tmp; } case K_BACK: return "⌫"; - case K_SPACE: return "空格"; - case K_ENTER: return "确定"; - case K_SWITCH: return "拼/英"; - case K_SYMBOL: return "?123"; + case K_SPACE: return meow_str("ime.space"); + case K_ENTER: return meow_str("ime.enter"); + case K_SWITCH: return meow_str("ime.switch"); + case K_SYMBOL: return meow_str("ime.symbol"); default: return ""; } } @@ -398,9 +398,9 @@ static void meow_ime_draw_py(const meow_theme *th) snprintf(pybuf, sizeof pybuf, "%s_", g_py_buf); meow_ui_text(14, PY_COMBO_Y + 8, pybuf, th->font_body, th->onSurface); meow_gfx_fill_round(PY_CLEAR_X, PY_COMBO_Y + 4, PY_CLEAR_W, PY_BADGE_H, 8, th->secondaryContainer); - meow_ui_text_center(PY_CLEAR_X + PY_CLEAR_W / 2, PY_COMBO_Y + 4 + (PY_BADGE_H - th20) / 2, "清", 20, th->onSecondaryContainer); + meow_ui_text_center(PY_CLEAR_X + PY_CLEAR_W / 2, PY_COMBO_Y + 4 + (PY_BADGE_H - th20) / 2, meow_str("ime.clear"), 20, th->onSecondaryContainer); meow_gfx_fill_round(PY_BADGE_X, PY_BADGE_Y, PY_BADGE_W, PY_BADGE_H, 8, th->primaryContainer); - meow_ui_text_center(PY_BADGE_X + PY_BADGE_W / 2, PY_BADGE_Y + (PY_BADGE_H - th20) / 2, "拼音", 20, th->onPrimaryContainer); + meow_ui_text_center(PY_BADGE_X + PY_BADGE_W / 2, PY_BADGE_Y + (PY_BADGE_H - th20) / 2, meow_str("ime.pinyin_mode"), 20, th->onPrimaryContainer); /* 候选栏:◀ ▶ 翻页 + 横排候选(高亮第 0 项) */ meow_ui_fill_rect(0, PY_CAND_Y, 480, PY_CAND_H, th->surfaceContainerHighest); @@ -409,7 +409,7 @@ static void meow_ime_draw_py(const meow_theme *th) meow_gfx_fill_round(PY_NEXT_X, PY_CAND_Y, PY_NEXT_W, PY_CAND_H, 8, th->surfaceContainerLow); meow_ui_text_center(PY_NEXT_X + PY_NEXT_W / 2, PY_CAND_Y + (PY_CAND_H - th20) / 2, "▶", 20, th->onSurface); if (g_py_nc == 0) { - meow_ui_text(PY_CAND_X0, PY_CAND_Y + (PY_CAND_H - th20) / 2, "(无候选)", th->font_body, th->onSurfaceVariant); + meow_ui_text(PY_CAND_X0, PY_CAND_Y + (PY_CAND_H - th20) / 2, meow_str("ime.no_candidate"), th->font_body, th->onSurfaceVariant); } else { int x = PY_CAND_X0; for (int i = 0; i < PY_PAGE; i++) { @@ -458,7 +458,7 @@ void meow_ime_draw(void) { int th20 = meow_ui_text_height(20); meow_gfx_fill_round(ASCII_MID_X, KB_TOP + 7, ASCII_MID_W, 28, 8, th->primaryContainer); - meow_ui_text_center(ASCII_MID_X + ASCII_MID_W / 2, KB_TOP + 7 + (28 - th20) / 2, "中", 20, th->onPrimaryContainer); + meow_ui_text_center(ASCII_MID_X + ASCII_MID_W / 2, KB_TOP + 7 + (28 - th20) / 2, meow_str("ime.to_pinyin"), 20, th->onPrimaryContainer); } /* 键 */ for (int i = 0; i < g_nkeys; i++) { @@ -471,9 +471,9 @@ void meow_ime_draw(void) char label[2] = { k->ch, 0 }; if (is_letter(k->ch)) label[0] = g_shift ? (char)toupper((unsigned char)k->ch) : (char)tolower((unsigned char)k->ch); - if (k->kind == K_SPACE) { meow_ui_text_center(k->x + k->w / 2, k->y + (k->h - meow_ui_text_height(20)) / 2, "SPACE", th->font_body, fg); } - else if (k->kind == K_ENTER) { meow_ui_text_center(k->x + k->w / 2, k->y + (k->h - meow_ui_text_height(20)) / 2, "OK", 20, fg); } - else if (k->kind == K_ESC) { meow_ui_text_center(k->x + k->w / 2, k->y + (k->h - meow_ui_text_height(20)) / 2, "X", 20, fg); } + if (k->kind == K_SPACE) { meow_ui_text_center(k->x + k->w / 2, k->y + (k->h - meow_ui_text_height(20)) / 2, meow_str("ime.space"), th->font_body, fg); } + else if (k->kind == K_ENTER) { meow_ui_text_center(k->x + k->w / 2, k->y + (k->h - meow_ui_text_height(20)) / 2, meow_str("ime.enter"), 20, fg); } + else if (k->kind == K_ESC) { meow_ui_text_center(k->x + k->w / 2, k->y + (k->h - meow_ui_text_height(20)) / 2, meow_str("ime.esc"), 20, fg); } else { meow_ui_text_center(k->x + k->w / 2, k->y + (k->h - meow_ui_text_height(20)) / 2, label, 20, fg); } } } diff --git a/sdk/meow_ui_img.c b/sdk/meow_ui_img.c index c2bd08b..96c37f0 100644 --- a/sdk/meow_ui_img.c +++ b/sdk/meow_ui_img.c @@ -96,6 +96,30 @@ static unsigned char *scale_buf(const meow_img *im, int dw, int dh, int cover) return out; } +/* 内存瘦身:把大图按最近邻等比缩放到「最长边 ≤ maxdim」,返回新图;调用方负责 + * meow_img_free 旧图 + 新图。原图已 ≤maxdim → 返回 NULL(无需缩放)。 + * 典型:网易云全尺寸封面 1448×1448 RGBA=8MB,显示只需 ≤480px(屏幕 480 宽)→ + * 480×480=0.9MB,省 ~7MB anon(设备 RAM 仅 56MB,OOM 真机被杀)。 + * 复用 scale_buf(cover=0,最近邻锁比例),不做插值(App 封面/背景无需高质量缩放)。 */ +meow_img *meow_img_downscale(meow_img *im, int maxdim) +{ + if (!im || maxdim <= 0) return NULL; + if (im->w <= maxdim && im->h <= maxdim) return NULL; /* 已够小,不缩放 */ + int dw, dh; + if (im->w >= im->h) { + dw = maxdim; + dh = (int)((long)im->h * maxdim / im->w); + } else { + dh = maxdim; + dw = (int)((long)im->w * maxdim / im->h); + } + if (dw < 1) dw = 1; + if (dh < 1) dh = 1; + unsigned char *tmp = scale_buf(im, dw, dh, 0); + if (!tmp) return NULL; + return img_alloc(dw, dh, tmp); +} + void meow_ui_img(int x, int y, meow_img *im) { if (!im) return; diff --git a/sdk/meow_ui_input.c b/sdk/meow_ui_input.c index 56b9dea..17217ea 100644 --- a/sdk/meow_ui_input.c +++ b/sdk/meow_ui_input.c @@ -62,6 +62,9 @@ #define MAX_DEVS 8 #define MEOW_APP_ACTIVITY_FILE "/tmp/meow_app_activity" /* M2.71-bug5:App 活动→launcher 重置息屏计时 */ +/* §6.4 事件分发形式化:手势捕获终点前向声明(meow_input_drain 之前定义后置,见下方实现)。 */ +void meow_ui_gesture_end(void); + /* ── 内部事件:系统级「下拉展开通知中心」(M2.5x 接线)── * 触摸状态机在抬手判定处识别「SWIPE_DOWN 起始于顶部热区」后压入;事件循环按系统手势处理 * (路由 meow_ui_sys_notif_pull),不下发给 APP 的 on_swipe(顶部热区为系统保留手势)。 @@ -197,6 +200,7 @@ void meow_input_drain(void) pthread_mutex_lock(&g_qm); g_qh = 0; g_qt = 0; pthread_mutex_unlock(&g_qm); + meow_ui_gesture_end(); /* §6.4:清空队列=丢弃未完成手势(如开 IME/模态前),释放捕获防悬垂 */ } static void push_ev(int type, int a, int b) @@ -404,6 +408,89 @@ int meow_input_long_press_poll(void) return 1; } +/* ============================================================ */ +/* §6.4 事件分发形式化:模态作用域 / 自上而下命中 / 手势捕获 / 唯一焦点 + * ------------------------------------------------------------ + * 统一仲裁者:把 ad-hoc 的「命中/捕获/消费/层间隔离/焦点」收敛为单一模型。 + * 兼容:本节 API 只追加;未调用者行为与之前完全一致(命中/捕获仍由 App 自理)。 */ +/* ============================================================ */ + +/* ---- 模态作用域栈(弹层叠开,depth 递增;存在则下层完全收不到触摸)---- */ +#define MEOW_MODAL_SCOPE_MAX 8 +static void *g_modal_stack[MEOW_MODAL_SCOPE_MAX]; +static int g_modal_depth = 0; +static char g_modal_sentinel; /* 「模态哨兵」地址:模态存在时 hit_topdown 的返回占位 */ + +meow_ui_modal *meow_ui_modal_begin(void) +{ + if (g_modal_depth >= MEOW_MODAL_SCOPE_MAX) return NULL; /* 栈满:不弹(骨架级静默) */ + void *h = &g_modal_stack[g_modal_depth]; + g_modal_stack[g_modal_depth++] = h; + return (meow_ui_modal *)h; +} +void meow_ui_modal_end(meow_ui_modal *m) +{ + if (!m) return; + for (int i = 0; i < g_modal_depth; i++) + if (g_modal_stack[i] == (void *)m) { + g_modal_depth = i; /* 弹到 m(含),一次性关上嵌套上层 */ + return; + } +} +int meow_ui_modal_active(void) { return g_modal_depth > 0; } +int meow_ui_modal_depth(void) { return g_modal_depth; } + +/* ---- 自上而下命中(模态门控 + z 降序控件命中)---- */ +void *meow_ui_hit_topdown(int x, int y) +{ + if (meow_ui_modal_active()) return &g_modal_sentinel; /* 模态存在:恒由模态/背板消费,不透传 */ + return meow_draw_hit_topdown(x, y); +} +int meow_ui_widget_hit(meow_widget *w, int x, int y) { return meow_widget_hit(w, x, y); } + +/* ---- 手势捕获(down 捕获直到 up)---- */ +static void *g_capture = NULL; +void *meow_ui_gesture_begin(int x, int y) +{ + void *t = meow_ui_hit_topdown(x, y); + g_capture = t; /* 覆盖旧捕获(一次手势至多一个目标;命中 NULL=未捕获,move/up 照旧) */ + return g_capture; +} +void *meow_ui_gesture_owner(void) { return g_capture; } +void meow_ui_gesture_end(void) { g_capture = NULL; } + +/* ---- 唯一焦点(list 焦点残留根治 / 滚动容器间转移)---- */ +static void *g_focus = NULL; +static meow_ui_focus_lost_fn g_focus_lost = NULL; +static void *g_focus_lost_ctx = NULL; + +void meow_ui_focus_set_lost_cb(meow_ui_focus_lost_fn cb, void *ctx) +{ g_focus_lost = cb; g_focus_lost_ctx = ctx; } +void *meow_ui_focus_owner(void) { return g_focus; } +int meow_ui_focus_acquire(void *owner) +{ + if (!owner) return 0; + void *prev = g_focus; + g_focus = owner; + if (prev && prev != owner && g_focus_lost) g_focus_lost(prev, g_focus_lost_ctx); + return 1; +} +int meow_ui_focus_release(void *owner) +{ + if (!g_focus) return 0; + if (owner && owner != g_focus) return 0; + void *prev = g_focus; + g_focus = NULL; + if (g_focus_lost) g_focus_lost(prev, g_focus_lost_ctx); + return 1; +} +void meow_ui_focus_tap(int x, int y) +{ + if (!g_focus) return; + void *hit = meow_ui_hit_topdown(x, y); + if (hit != g_focus) meow_ui_focus_release(g_focus); /* 点非焦点区 → 释放并通知 */ +} + /* ---- 系统键(音量/媒体)去重 + EVIOCGKEY 状态轮询 ---- * M2.55/agent-217 之后 SDK 在输入线程消化系统键语义;但真机物理键(jz adc keyboard event2、 * md-gpio-keys event0、earpods_adc event3)的按下不一定进入 evdev 事件流(事件流可被其它进程 @@ -620,6 +707,10 @@ void meow_ui_run(const meow_ui_events *ev) meow_ui_swipe_hint(-1); meow_ui_flush_region(0, meow_ui_statusbar_height(), meow_ui_width(), 6); if (meow_ime_active() && meow_ime_handle_event(&e)) continue; + /* §6.4 手势结束(抬手边沿)+ 焦点点外释放:先释放手势捕获(move/up 已结束), + * 再对落点做「命中焦点所有者→保位;否则释放并通知」(list 焦点残留根治)。 */ + meow_ui_gesture_end(); + meow_ui_focus_tap(e.a, e.b); /* §6.1 触摸分流:③ 系统全局(IME/状态栏…已在上面消费) → ② App 全局控件 * (命中返回 1=消费,否则落到 ① active Activity 页面控件)。 */ if (meow_ui_global_hit(e.a, e.b)) continue; @@ -633,6 +724,7 @@ void meow_ui_run(const meow_ui_events *ev) meow_ui_flush_region(0, meow_ui_statusbar_height(), meow_ui_width(), 6); /* 拼音 IME 候选栏横滑翻页(英文模式 handle_event 返回 0 → 沿用既有行为)。 */ if (meow_ime_active() && meow_ime_handle_event(&e)) continue; + meow_ui_gesture_end(); /* §6.4:抬手(滑动)边沿,释放手势捕获 */ /* A-0 全局左缘返回(方案A): * - APP 注册 on_swipe → APP 接管,自行决定(meow_ui_request_exit / 页内返回); * - APP 未注册 on_swipe 且非全屏(sb_active)→ SDK 默认退出 app(返回=退出); @@ -652,6 +744,7 @@ void meow_ui_run(const meow_ui_events *ev) * 起点若同时落在左缘,拖动期可能已画过 MEOW_EV_SWIPE_PROGRESS 左缘指示条, * 抬手定判为 NOTIF_PULL 后须清掉(否则通知中心打开后残留返回条)。 */ meow_ui_swipe_hint(-1); + meow_ui_gesture_end(); /* §6.4:抬手边沿,释放手势捕获 */ meow_ui_sys_notif_pull(e.a); /* e.a = 手势按下原点 Y */ } else if (e.type == MEOW_EV_TOUCH_DOWN || e.type == MEOW_EV_TOUCH_MOVE) { /* M2.71-bug2/6:拖动过程(跟手)。IME 弹出时触摸优先喂键盘(键盘覆盖层拦截, @@ -660,6 +753,9 @@ void meow_ui_run(const meow_ui_events *ev) * IME 打开时先把压点喂给拼音(候选栏滑动翻页判定 g_ime_swipe_y),再丢弃。 */ if (in_startup) continue; if (meow_ime_active()) { meow_ime_handle_event(&e); continue; } + /* §6.4:down 捕获——命中目标捕获整个手势直到 up;随后 move/up 只发给该目标 + * (防滑出后误触发下层)。真实触发放置捕获(宿主直调 on_touch_* 不受影响)。 */ + if (e.type == MEOW_EV_TOUCH_DOWN) meow_ui_gesture_begin(e.a, e.b); if (g_events_set && g_events.on_touch_down && e.type == MEOW_EV_TOUCH_DOWN) g_events.on_touch_down(e.a, e.b); if (g_events_set && g_events.on_touch_move && e.type == MEOW_EV_TOUCH_MOVE) @@ -686,6 +782,7 @@ void meow_ui_run(const meow_ui_events *ev) if (in_startup) continue; /* 拼音 IME 打开:长按退格清空字段(英文模式 handle_event 返回 0 → 走既有长按回调)。 */ if (meow_ime_active() && meow_ime_handle_event(&e)) continue; + meow_ui_gesture_end(); /* §6.4:长按已触发,其抬手不再 TAP/SWIPE——顺手释放捕获 */ if (g_long_press_cb) g_long_press_cb(e.a, e.b); } } @@ -697,6 +794,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/sdk/meow_ui_internal.h b/sdk/meow_ui_internal.h index c1f895a..57d3dc6 100644 --- a/sdk/meow_ui_internal.h +++ b/sdk/meow_ui_internal.h @@ -76,6 +76,10 @@ void meow_ui_sys_statusbar_render_off(int yoff); /* 把第 idx 个 MD3 图标(sb_icons[])按 ink 等比 blit 进 back_buf;x=槽左缘、ink_h=目标 ink 高 * (0=自然 ink,WiFi 强弱档用)、fg=24bit 前景、cy=ink 垂直中心线。见 meow_ui_gfx.c。 */ void meow_gfx_blit_icon(int idx, int x, int y, int ink_h, uint32_t fg, int cy); +/* 同 meow_gfx_blit_icon,但**不标「顶栏」脏区**:供控件(W_TOPAPPBAR 位图动作图标)用—— + * 控件矩形由 meow_widget_draw 的 clip 与 invalidation 独立管理,状态栏路径的顶栏脏区假设 + * 不适用于位于屏幕内容区的 topbar。像素算法与状态栏同源(ink 居中 + alpha 软边混合)。 */ +void meow_gfx_blit_icon_ink(int idx, int x, int ink_h, uint32_t fg, int cy); /* ---- 顶级主题风格族(§8.1.2,minimal):状态栏图标集/配色随 ui.style(md3/miuix)---- * meow_ui_theme.c 实现:读 settings.conf 的 ui.style(md3→0 / miuix→1),缓存,随 @@ -234,6 +238,11 @@ void meow_draw_view_unregister(struct meow_view *v); /* view free:注 int meow_draw_view_entered(const struct meow_view *v); /* view 是否已进入(绘制过滤参考) */ void meow_draw_view_set_z(struct meow_view *v, int z); /* view 显式 z(z<=0=清除回退自动) */ +/* §6.4 事件分发形式化:沿 z 序自顶向下命中(owner==active+enable 过滤;模态门控在 + * meow_ui_hit_topdown,本函数只做 z 降序控件命中)。返回最高命中 z 的 meow_widget*, + * 无命中返回 NULL。实现见 meow_ui_draw.c(读绘制注册表,不改 struct meow_widget)。 */ +void *meow_draw_hit_topdown(int x, int y); + /* 控件布局深度反推(meow_ui_draw.c 用;widget.c 实现,读 struct meow_widget 内部字段)。 * 让绘制注册表在「无 meow_view 时」仍能从 W_LAYOUT children 反推子>父深度。 */ int meow_widget_is_layout(const struct meow_widget *w); /* 1 = W_LAYOUT */ diff --git a/sdk/meow_ui_scroll.c b/sdk/meow_ui_scroll.c new file mode 100644 index 0000000..6d76c9b --- /dev/null +++ b/sdk/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/sdk/meow_ui_str.c b/sdk/meow_ui_str.c new file mode 100644 index 0000000..0f606ac --- /dev/null +++ b/sdk/meow_ui_str.c @@ -0,0 +1,288 @@ +/* + * meow_ui_str.c — SDK 字符串资源框架(i18n 雏形,§13.meow_str_*)。 + * + * 背景:SDK/系统 UI 文案此前全部硬编码在 C 源码(dialog 的 "OK/CANCEL"、IME 的 "空格/确定/ + * 拼音/(无候选)" 等),第三方开发者无法按语言取串,公开 SDK 前必须外置。本 TU 提供 + * 「按 locale 取整串」的最小框架:内嵌双语默认表(编译进 meow_ui.a,必可用)+ TF 卡覆盖表 + * (MeowData//strings/.txt,开发者可加壳不改二进制)+ locale 存系统设置 + * settings.conf 的 `ui.locale`(默认 zh_cn)。 + * + * 取值优先级(meow_str,四层,逐层回落,永不返回 NULL): + * ① 当前 locale 的 TF 覆盖表(meow_ui_str_set_locale 加载,静态数组,≤256 条) + * ② 内嵌表中当前语言(en_us→en,其余→zh) + * ③ 内嵌表中内置默认(zh_cn,即 locale 缺省/未知语言时回退) + * ④ 仍不可得 → 原样返回 id(调用方可展示 id / 打点,绝不见 NULL) + * + * 设计要点: + * - 零 malloc 热路径:内嵌为编译期静态表;覆盖表为启动/切 locale 时的静态数组, + * meow_str 只做线性扫描(表 ≤256,O(n) 可接受),不申请任何堆。 + * - 纯 libc:不依赖其它 SDK TU,宿主 gcc 直接可单测(同 meow_ui_config.c / meow_ui_vol.c)。 + * - locale 初值惰性读 settings.conf ui.locale(默认 zh_cn);meow_str_set_locale 即时生效。 + * - 覆盖表解析兼容 "key = value"('=' 两侧去空白),忽略空行/无 '=' 行;UTF-8 原样透传。 + * - 幂等:settings 只读一次;覆盖表按需加载。locale 修改须在 App 主线程(防并发写覆盖表)。 + * + * 边界(§13 明示不做):无复数规则/BiDi/RTL/动态字体回退,理由见 meow_ui_str.h。 + */ +#include "meow_ui.h" + +#include +#include +#include +#include + +#define MEOW_STR_SETTINGS_FILE "/usr/data/meow/settings.conf" /* 板载系统设置(与 theme/vol 同源) */ +#define MEOW_STR_LOCALE_MAX 16 +#define MEOW_STR_OVERRIDE_MAX 256 /* 覆盖表上限(§13 性能约束 ≤256) */ +#define MEOW_STR_OVR_ID_MAX 64 +#define MEOW_STR_OVR_TEXT_MAX 128 +#define MEOW_STR_PATH_MAX 384 + +/* ---- 内嵌双语默认表(编译进二进制,必可用;zh = 内置默认)---- + * id 规范:`域.名`(common.* 为 SDK 自带通用组;ime.* 为 SDK 自带 IME 键面组; + * 第三方 App 可自建 域(如 player.pause)经 TF 覆盖表追加,不改 SDK 二进制)。 */ +typedef struct meow_str_embed { + const char *id; + const char *zh; + const char *en; +} meow_str_embed; + +static const meow_str_embed g_embed[] = { + /* common.*:通用 UI(dialog 默认按钮 / 空态占位可复用) */ + { "common.ok", "确定", "OK" }, + { "common.cancel", "取消", "Cancel" }, + { "common.back", "返回", "Back" }, + { "common.confirm", "确认", "Confirm" }, + { "common.done", "完成", "Done" }, + { "common.save", "保存", "Save" }, + { "common.delete", "删除", "Delete" }, + { "common.edit", "编辑", "Edit" }, + { "common.search", "搜索", "Search" }, + { "common.input", "输入", "Input" }, + { "common.please_input", "请输入", "Please input" }, + { "common.select", "请选择", "Select" }, + { "common.loading", "加载中...", "Loading..." }, + { "common.no_data", "暂无数据", "No data" }, + { "common.retry", "重试", "Retry" }, + { "common.failed", "失败", "Failed" }, + { "common.retry_fail", "重试失败", "Retry failed" }, + { "common.close", "关闭", "Close" }, + { "common.unknown", "未知", "Unknown" }, + { "common.settings", "设置", "Settings" }, + { "common.tap_ok_edit", "点击确定编辑", "tap OK to edit..." }, + /* ime.*:SDK 内置 IME 键面/徽标(meow_ui_ime.c 接入) */ + { "ime.space", "空格", "Space" }, + { "ime.enter", "确定", "OK" }, + { "ime.esc", "取消", "Cancel" }, + { "ime.switch", "拼/英", "PY/EN" }, + { "ime.symbol", "?123", "?123" }, + { "ime.clear", "清", "Clear" }, + { "ime.pinyin_mode", "拼音", "Pinyin" }, + { "ime.no_candidate", "(无候选)", "(none)" }, + { "ime.to_pinyin", "中", "中" }, +}; +#define MEOW_STR_EMBED_N (sizeof(g_embed) / sizeof(g_embed[0])) + +/* ---- 运行时状态(静态,零 malloc)---- */ +static char g_locale[MEOW_STR_LOCALE_MAX] = "zh_cn"; +static int g_locale_loaded = 0; /* 1 = 已从 settings 读过一次 */ +static int g_override_loaded = 0; /* 1 = 覆盖表已按当前 locale 加载 */ + +/* 覆盖表(当前 locale):线性静态数组(id/text 各自定长),≤ MEOW_STR_OVERRIDE_MAX。 */ +static char g_ov_id[MEOW_STR_OVERRIDE_MAX][MEOW_STR_OVR_ID_MAX]; +static char g_ov_text[MEOW_STR_OVERRIDE_MAX][MEOW_STR_OVR_TEXT_MAX]; +static int g_ov_n = 0; + +/* ---- settings.conf 读(同 meow_ui_theme.c get_setting_str 口径:'=' 切分,精确匹配 key)---- + * 返回 0=命中并把值拷入 buf;-1=未命中/文件不可读/超长。 */ +static int str_setting_get(const char *key, char *buf, int buflen) +{ + FILE *f = fopen(MEOW_STR_SETTINGS_FILE, "r"); + if (!f || buflen <= 0) return -1; + char line[160]; + int kl = (int)strlen(key); + int found = 0; + while (fgets(line, sizeof line, f)) { + char *eq = strchr(line, '='); + if (!eq) continue; + char *k = line; + while (*k == ' ' || *k == '\t') k++; + int n = (int)(eq - k); + while (n > 0 && (k[n-1] == ' ' || k[n-1] == '\t')) n--; + if (n != kl || strncmp(k, key, (size_t)kl) != 0) continue; + const char *v = eq + 1; + while (*v == ' ' || *v == '\t') v++; + int m = 0; + while (*v && *v != '\n' && *v != '\r' && m < buflen - 1) buf[m++] = *v++; + while (m > 0 && (buf[m-1] == ' ' || buf[m-1] == '\t')) m--; + buf[m] = 0; + found = 1; + break; + } + fclose(f); + return found ? 0 : -1; +} + +/* ---- settings.conf 写一个字符串键(best-effort):流式拷贝到 .tmp,命中键行改写为 + * "key = value"(launcher 落盘格式),键缺失则末尾追加;rename 原子替换。保留其余注释/设置, + * 绝不 O_TRUNC 整文件(同 meow_ui_vol_set 思路)。失败静默返回 -1(读不改状态、不崩)。 */ +static int str_setting_set(const char *key, const char *val) +{ + if (!key || !key[0] || !val) return -1; + FILE *in = fopen(MEOW_STR_SETTINGS_FILE, "r"); + char tmp[MEOW_STR_PATH_MAX]; + int kl = (int)strlen(key); + snprintf(tmp, sizeof tmp, "%s.tmp", MEOW_STR_SETTINGS_FILE); + FILE *out = fopen(tmp, "w"); + if (!out) { if (in) fclose(in); return -1; } + + int found = 0; + char line[256]; + while (in && fgets(line, sizeof line, in)) { + char *eq = strchr(line, '='); + char *k = line; + while (*k == ' ' || *k == '\t') k++; + int n = eq ? (int)(eq - k) : 0; + while (n > 0 && (k[n-1] == ' ' || k[n-1] == '\t')) n--; + if (eq && n == kl && strncmp(k, key, (size_t)kl) == 0) { + fprintf(out, "%s = %s\n", key, val); + found = 1; + } else { + fputs(line, out); + } + } + if (in) fclose(in); + if (!found) fprintf(out, "%s = %s\n", key, val); + fclose(out); + + if (rename(tmp, MEOW_STR_SETTINGS_FILE) != 0) { remove(tmp); return -1; } + return 0; +} + +/* ---- TF 覆盖表路径:/strings/.txt(= MeowData//strings/)。 + * 无 MEOW_DATA_DIR(手动跑/宿主/无卡)→ path 置空(覆盖表不加载,内嵌兜底)。返回 path 是否有效。 */ +static int str_override_path(char *path, int n) +{ + const char *env = getenv("MEOW_DATA_DIR"); + if (!env || !env[0] || n <= 0) { if (n > 0) path[0] = 0; return 0; } + char base[MEOW_STR_PATH_MAX]; + snprintf(base, sizeof base, "%s", env); + size_t bn = strlen(base); + /* launcher 注入的 MEOW_DATA_DIR = /data;去尾 "/data" 回到 ,再拼 strings/。 */ + if (bn >= 5 && strcmp(base + bn - 5, "/data") == 0) base[bn - 5] = 0; + if (snprintf(path, (size_t)n, "%s/strings/%s.txt", base, g_locale) >= n) { path[0] = 0; return 0; } + return path[0] != 0; +} + +/* 解析覆盖表(`id=text` 行,'=' 两侧去空白;空行/无 '=' 行跳过;UTF-8 原样透传)。 + * 覆盖优先级:同名覆盖内嵌,未覆盖该 id 则回退内嵌。 */ +static void str_load_override(void) +{ + char path[MEOW_STR_PATH_MAX]; + g_ov_n = 0; + if (!str_override_path(path, sizeof path)) return; + + FILE *f = fopen(path, "r"); + if (!f) return; + char line[MEOW_STR_OVR_TEXT_MAX + 16]; + while (fgets(line, sizeof line, f) && g_ov_n < MEOW_STR_OVERRIDE_MAX) { + char *eq = strchr(line, '='); + if (!eq) continue; /* 注释/空行/非 key=value */ + char *k = line; + while (*k == ' ' || *k == '\t') k++; /* id 左去空白 */ + int klen = (int)(eq - k); + while (klen > 0 && (k[klen-1] == ' ' || k[klen-1] == '\t')) klen--; + if (klen <= 0 || klen >= MEOW_STR_OVR_ID_MAX) continue; + const char *v = eq + 1; + while (*v == ' ' || *v == '\t') v++; /* text 左去空白 */ + int vlen = 0; + while (v[vlen] && v[vlen] != '\n' && v[vlen] != '\r' && vlen < MEOW_STR_OVR_TEXT_MAX - 1) + vlen++; + while (vlen > 0 && (v[vlen-1] == ' ' || v[vlen-1] == '\t')) vlen--; /* text 右去空白 */ + if (vlen <= 0) continue; + + memcpy(g_ov_id[g_ov_n], k, (size_t)klen); + g_ov_id[g_ov_n][klen] = 0; + memcpy(g_ov_text[g_ov_n], v, (size_t)vlen); + g_ov_text[g_ov_n][vlen] = 0; + g_ov_n++; + } + fclose(f); +} + +/* 惰性初值:首次调用读 settings.conf ui.locale(缺省 "zh_cn")。幂等(只读一次)。 */ +static void str_init_locale(void) +{ + if (g_locale_loaded) return; + char buf[MEOW_STR_LOCALE_MAX]; + if (str_setting_get("ui.locale", buf, sizeof buf) == 0 && buf[0]) + snprintf(g_locale, sizeof g_locale, "%s", buf); + else + snprintf(g_locale, sizeof g_locale, "zh_cn"); + g_locale_loaded = 1; +} + +/* 惰性加载覆盖表(对当前 g_locale)。幂等(只加载一次直到 set_locale 重载)。 */ +static void str_ensure_override(void) +{ + if (g_override_loaded) return; + str_load_override(); + g_override_loaded = 1; +} + +/* meow_str / meow_str_locale 前置:确保 locale 初值 + 覆盖表就绪。 */ +static void str_init(void) +{ + str_init_locale(); + str_ensure_override(); +} + +/* 持久化 ui.locale(best-effort)。失败静默(运行时仍切换,仅不跨启动)。 */ +static void str_persist_locale(void) +{ + str_setting_set("ui.locale", g_locale); +} + +const char *meow_str_locale(void) +{ + str_init(); + return g_locale; +} + +const char *meow_str(const char *id) +{ + if (!id) return ""; + str_init(); + int en = (strcmp(g_locale, "en_us") == 0); + + /* ① 当前 locale 的 TF 覆盖表(最高优先) */ + for (int i = 0; i < g_ov_n; i++) + if (strcmp(g_ov_id[i], id) == 0) return g_ov_text[i]; + + /* ② 内嵌当前语言(en_us→en;其余→zh)③ 兜底内嵌内置默认(zh)——同一表内按语言取。 */ + for (size_t i = 0; i < MEOW_STR_EMBED_N; i++) + if (strcmp(g_embed[i].id, id) == 0) return en ? g_embed[i].en : g_embed[i].zh; + + /* ④ 未知 id:原样返回调用方(永不 NULL)。 */ + return id; +} + +int meow_strf(char *out, int outsz, const char *id, ...) +{ + if (!out || outsz <= 0) return -1; + const char *s = meow_str(id); + va_list ap; + va_start(ap, id); + int r = vsnprintf(out, (size_t)outsz, s, ap); + va_end(ap); + return r; +} + +void meow_str_set_locale(const char *locale) +{ + if (!locale || !locale[0]) return; + str_init(); /* 确保 locale 初值 + 覆盖表基座就绪 */ + snprintf(g_locale, sizeof g_locale, "%s", locale); + str_load_override(); /* 按新 locale 重载覆盖表 */ + g_override_loaded = 1; + str_persist_locale(); /* best-effort 持久化(失败静默) */ +} diff --git a/sdk/meow_ui_view.c b/sdk/meow_ui_view.c index e1dc8e1..645e677 100644 --- a/sdk/meow_ui_view.c +++ b/sdk/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/sdk/meow_ui_view.h b/sdk/meow_ui_view.h index 729d703..73d8a96 100644 --- a/sdk/meow_ui_view.h +++ b/sdk/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/sdk/meow_ui_widget.c b/sdk/meow_ui_widget.c index 4f8ef37..eb727cb 100644 --- a/sdk/meow_ui_widget.c +++ b/sdk/meow_ui_widget.c @@ -23,6 +23,12 @@ enum { W_BTN = 0, W_SLIDER = 1, W_SWITCH = 2, W_LIST = 3, /* 容器(TopAppBar 动作 / NavigationBar 项 / TabBar 页签)子项上限。 */ #define MEOW_CONTAINER_MAX 8 +/* TopAppBar 位图动作图标(Material Symbols Rounded 单通道 alpha,见 sb_icons.h): + * 动作单元格宽(32px 图标槽水平居中 + 两侧留白 8px)与目标 ink 高(MD3 24dp 图标)。 + * ink 垂直中线取栏中点(mid),与状态栏 28px 栏「文字中线」风格一致。 */ +#define MEOW_TOPBAR_ACTION_W 48 +#define MEOW_TOPBAR_ICON_INK 24 + /* 按钮「按住」判定阈值:视觉上区分「短按按下」与「按住」(更深状态层)。 * 阈值取 meow_ui_long_press_ms()(§6.3 通用长按默认 500ms,App 可配)——与长按事件同源, * 避免「长按已触发但视觉未加深」的不一致。触发语义不变(on_tap 仍按抬手判定)。 */ @@ -79,6 +85,9 @@ struct meow_widget { char items[MEOW_CONTAINER_MAX][24]; /* W_TOPAPPBAR 动作 / W_NAVBAR 项 / W_TABBAR 页签 */ int nitems; /* 上述项数 */ int selected; /* W_NAVBAR / W_TABBAR 选中下标 */ + /* W_TOPAPPBAR 动作位图图标:每项 sb_icons[] 索引(≥0=画该图标;-1=画 items[i] 文本)。 + * 追加字段(calloc 归零为 0,故 create/add_action 显式置 -1 保证文本默认)。 */ + int item_icon[MEOW_CONTAINER_MAX]; /* M2.71-bug8:W_LIST 行点击回调(可选)。 */ meow_list_tap_fn tap_cb; /* 命中行回调(可 NULL) */ void *tap_ctx; /* 回调 ctx */ @@ -90,6 +99,20 @@ struct meow_widget { int sel_bits_n; /* 位图字数 */ meow_listview_tap_fn lv_tap_cb; /* 行点击回调(idx, op) */ void *lv_tap_ctx; /* 回调 ctx */ + + /* §5.5 虚拟化层(W_LISTVIEW 扩展:固定 item 池 + 多行渲染器 + 完全自绘 + 空态。 + * calloc 归零,安全;vs_count/vs_bind 均非 NULL 才进入 pool 模式,src2 模式不受影响。) */ + meow_listview_count_fn vs_count; /* 虚拟化总数回调(NULL=src2 模式) */ + meow_listview_bind_fn vs_bind; /* 虚拟化 bind 回调(NULL=src2 模式) */ + meow_listview_draw_fn vs_draw; /* 完全自绘回调(非 NULL 覆盖默认 renderer) */ + void *vs_ctx; /* 上述回调的 ctx */ + meow_lv_item *vs_pool; /* 固定 item 池(槽数 vs_pool_n) */ + int vs_pool_n; /* 池容量(视口行数 + 2,静态有界) */ + int *vs_bound; /* 每槽绑定的行号(-1=空闲;pool 索引 mod 映射) */ + int lines; /* 行档位 1/2(默认 2=双行) */ + int show_icon; /* 1=画左图标/缩略图(默认 1) */ + int show_right;/* 1=画右尾件(默认 1) */ + char empty_text[64]; /* 空态占位文案(首字符 0=不画) */ }; /* ---- §5.8 batch1 内部辅助:opts 取值(opts_used==0 时按控件内建默认)---- @@ -110,6 +133,22 @@ static int widget_disabled(const meow_widget *w) { return !(w->state & MEOW_WS_E static uint32_t state_tint(uint32_t base, const meow_theme *th, int t) { return blend_color(base, th->onSurface, t); } +/* ---- TopAppBar 位图动作图标辅助(绘制与命中共用的「项宽」/「按压淡色」)---- + * 项宽:位图图标用固定单元格 MEOW_TOPBAR_ACTION_W(图标槽在该格内居中);文本项沿用 + * 原「文本宽 + 12」不等式(行为不变)。命中与绘制同源,避免 icon/text 混合排布错位。 */ +static int topbar_item_w(const meow_widget *w, const meow_theme *th, int i) +{ + if (w->item_icon[i] >= 0) return MEOW_TOPBAR_ACTION_W; + return meow_ui_text_width(w->items[i], th->font_body) + 12; +} +/* 动作颜色:默认 onSurface;按压态向栏底色(surfaceContainerHigh)淡 12%(§5.8.4 状态层)。 */ +static uint32_t topbar_action_color(const meow_widget *w, const meow_theme *th) +{ + uint32_t col = th->onSurface; + if (w->state & MEOW_WS_PRESSED) col = blend_color(col, th->surfaceContainerHigh, 12); + return col; +} + /* ---- §5.8.2/§5.8.3 公共 API:Options + 状态机 ---- */ int meow_widget_set_opts(meow_widget *w, const meow_widget_opts *opts) { @@ -338,6 +377,8 @@ void meow_widget_free(meow_widget *w) meow_draw_widget_unregister(w); /* §6.2:绘制物注册表自动注销(stale id 失效勿用) */ live_btn_unregister(w); if (w->sel_bits) { free(w->sel_bits); w->sel_bits = NULL; } /* W_LISTVIEW 多选位图 */ + if (w->vs_pool) { free(w->vs_pool); w->vs_pool = NULL; } /* W_LISTVIEW 虚拟化池 */ + if (w->vs_bound) { free(w->vs_bound); w->vs_bound = NULL; } free(w); } @@ -465,6 +506,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) */ /* ============================================================ */ @@ -688,6 +740,7 @@ meow_widget *meow_topappbar_create(int x, int y, int w, int h, const char *title t->owner = meow_ui_activity_active(); t->state = MEOW_WS_ENABLED; t->back_on = 0; t->nitems = 0; + for (int i = 0; i < MEOW_CONTAINER_MAX; i++) t->item_icon[i] = -1; /* 默认文本项 */ if (title) { strncpy(t->label, title, sizeof t->label - 1); t->label[sizeof t->label - 1] = 0; } meow_draw_widget_register(t); return t; @@ -710,10 +763,24 @@ int meow_topappbar_add_action(meow_widget *w, const char *label) if (label) strncpy(w->items[w->nitems], label, 23); w->items[w->nitems][23] = 0; int idx = w->nitems; + w->item_icon[idx] = -1; /* 文本项(后续可用 set_item_icon 转位图) */ w->nitems++; meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); return idx; } +/* 把第 idx 个动作项设为位图图标(sb_icons[] 索引,MEOW_ICON_* 或 SB_I_*)。追加式 API: + * 文本项(add_action 默认)行为不变;仅「设为图标后」该动作改用 meow_gfx_blit_icon_ink + * 绘制(32px 槽居中 + ink 垂直对齐栏中点),颜色取主题 onSurface/按压态淡色。 + * 返回 0=成功;NULL/非 topbar/越界 idx/非法 icon_idx=负值。 */ +int meow_topappbar_set_item_icon(meow_widget *w, int idx, int icon_idx) +{ + if (!w || w->type != W_TOPAPPBAR) return -1; + if (idx < 0 || idx >= w->nitems) return -1; + if (icon_idx < 0) return -1; /* ≥0 的顶层校验;越界索引由 blit 内部 clamp 兜底 */ + w->item_icon[idx] = icon_idx; + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); + return 0; +} int meow_topappbar_hit(meow_widget *w, int x, int y) { if (!w || w->type != W_TOPAPPBAR) return -1; @@ -722,7 +789,7 @@ int meow_topappbar_hit(meow_widget *w, int x, int y) const meow_theme *th = meow_theme_get(); int ax = w->x + w->w - 10; for (int i = w->nitems - 1; i >= 0; i--) { - int aw = meow_ui_text_width(w->items[i], th->font_body) + 12; + int aw = topbar_item_w(w, th, i); /* 位图图标用固定单元格;文本项用原不等式 */ ax -= aw; if (x >= ax && x < ax + aw && x >= w->x + 100) return i; /* 不压标题区/返回区 */ } @@ -887,6 +954,8 @@ int meow_list_tap_at(meow_widget *w, int x, int y) int idx = meow_list_hit_index(w, x, y); if (idx < 0) return -1; meow_list_set_selected(w, idx); + /* §6.4:列表成为唯一焦点——点外由 meow_ui_focus_tap 统一释放(list 焦点残留根治)。 */ + meow_ui_focus_acquire((void *)w); if (w->tap_cb) w->tap_cb(idx, w->tap_ctx); return idx; } @@ -910,6 +979,58 @@ extern void meow_ui_img_scaled(int x, int y, int w, int h, meow_img *img) __attr static int listv_item_h(const meow_widget *w) { return (w->item_h > 0) ? w->item_h : 56; } +/* §5.5 虚拟化辅助:当前行数(pool 模式实时查 count_fn;src2 模式用缓存的 w->count)。 */ +static int listv_count(const meow_widget *w) +{ + if (!w) return 0; + if (w->vs_count) { int n = w->vs_count(w->vs_ctx); return n < 0 ? 0 : n; } + return w->count; +} + +/* 池容量估算:视口行数 + 2 静态槽(按最小行高 MEOW_LV_MIN_ITEM_H 估算,静态有界)。 + * 真实列表行高远大于最小行高,故 `viewport/item_h + 1 <= cap` 恒成立 → 无槽冲突重绑。 */ +#define MEOW_LV_MIN_ITEM_H 8 /* 池容量估算用的最小行高(真实列表远大于此) */ +#define MEOW_LV_POOL_MAX 96 /* 池容量上限(固定;保证静态有界) */ +static int listv_pool_cap(const meow_widget *w) +{ + int cap = (w->h > 0) ? (w->h / MEOW_LV_MIN_ITEM_H + 2) : 8; + if (cap < 8) cap = 8; + if (cap > MEOW_LV_POOL_MAX) cap = MEOW_LV_POOL_MAX; + return cap; +} +/* 确保池容量 ≥ cap(有界扩张;pool 模式调用)。返回 0=成功;-1=分配失败(仍可用,槽冲突时防御性重绑)。 */ +static int listv_ensure_pool(meow_widget *w) +{ + if (!w->vs_count || !w->vs_bind) return 0; + int cap = listv_pool_cap(w); + if (w->vs_pool && w->vs_pool_n >= cap) return 0; + /* 重分配(有界:cap ≤ MEOW_LV_POOL_MAX,与 item 总数无关)。 */ + meow_lv_item *np = (meow_lv_item *)calloc((size_t)cap, sizeof *np); + int *nb = (int *)calloc((size_t)cap, sizeof *nb); + if (!np || !nb) { free(np); free(nb); return -1; } + if (w->vs_pool && w->vs_pool_n > 0) { + memcpy(np, w->vs_pool, (size_t)(w->vs_pool_n < cap ? w->vs_pool_n : cap) * sizeof *np); + memcpy(nb, w->vs_bound, (size_t)(w->vs_pool_n < cap ? w->vs_pool_n : cap) * sizeof *nb); + } + for (int i = 0; i < cap; i++) nb[i] = -1; /* 全新/重分配:全部槽空闲 → 下一帧按需绑定 */ + free(w->vs_pool); free(w->vs_bound); + w->vs_pool = np; w->vs_bound = nb; w->vs_pool_n = cap; + return 0; +} +/* 取 index 对应的池槽:pool 槽按 index mod cap 映射(回滚命中同槽 → 不重绑)。 + * 槽绑定的 index 不匹配 → 防御性重新 bind(保证内容正确,即使池不足)。返回槽指针。 */ +static meow_lv_item *listv_slot(meow_widget *w, int index) +{ + if (!w->vs_pool || w->vs_pool_n <= 0) return NULL; + int s = (int)((unsigned)index % (unsigned)w->vs_pool_n); + if (w->vs_bound[s] != index) { + memset(&w->vs_pool[s], 0, sizeof w->vs_pool[s]); + w->vs_bind(index, &w->vs_pool[s], w->vs_ctx); + w->vs_bound[s] = index; + } + return &w->vs_pool[s]; +} + /* 文本截断:超出 maxw 加 "..."(复用 browser_text_trunc 的逐字符宽度累加)。 */ static void listv_text_trunc(int x, int y, const char *s, int maxw, int size, uint32_t col) { @@ -1010,6 +1131,7 @@ meow_widget *meow_listview_create(int x, int y, int w, int h, int count, t->count = count > 0 ? count : 0; t->src2 = src; t->ctx = ctx; t->sel = -1; t->item_h = 56; t->scroll = 0; t->sel_mode = (mode >= MEOW_LIST_SEL_NONE && mode <= MEOW_LIST_SEL_MULTI) ? mode : MEOW_LIST_SEL_NONE; + t->lines = 2; t->show_icon = 1; t->show_right = 1; /* 默认双行富 item + 图标 + 右尾件(src2 布局不变) */ if (t->sel_mode == MEOW_LIST_SEL_MULTI && t->count > 0) { t->sel_bits_n = (t->count + 31) / 32; t->sel_bits = (uint32_t *)calloc((size_t)t->sel_bits_n, sizeof(uint32_t)); @@ -1021,7 +1143,11 @@ meow_widget *meow_listview_create(int x, int y, int w, int h, int count, int meow_listview_set_item_h(meow_widget *w, int item) { if (!w || w->type != W_LISTVIEW) return -1; - if (item > 0) { w->item_h = item; meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); } + if (item > 0) { + w->item_h = item; + listv_ensure_pool(w); /* 行高变小 → 可见行增多 → 池容量随之调整 */ + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); + } return w->item_h; } void meow_listview_set_count(meow_widget *w, int count) @@ -1030,10 +1156,11 @@ void meow_listview_set_count(meow_widget *w, int count) if (count < 0) count = 0; if (count == w->count) return; w->count = count; - if (w->sel >= w->count) w->sel = -1; - if (w->sel_mode == MEOW_LIST_SEL_MULTI) { /* 重分配多选位图 */ + int n = listv_count(w); /* pool 模式忽略 set_count(count 实时查) */ + if (w->sel >= n) w->sel = -1; + if (w->sel_mode == MEOW_LIST_SEL_MULTI && !w->vs_count) { /* 重分配多选位图(src2 模式) */ free(w->sel_bits); w->sel_bits = NULL; w->sel_bits_n = 0; - int need = (count + 31) / 32; + int need = (n + 31) / 32; if (need > 0) { w->sel_bits_n = need; w->sel_bits = (uint32_t *)calloc((size_t)need, sizeof(uint32_t)); @@ -1047,8 +1174,9 @@ void meow_listview_set_count(meow_widget *w, int count) int meow_listview_max_scroll(meow_widget *w) { if (!w || w->type != W_LISTVIEW) return 0; - int ms = w->count * listv_item_h(w) - w->h; - return ms < 0 ? 0 : ms; + long long ms = (long long)listv_count(w) * listv_item_h(w) - w->h; + if (ms < 0) return 0; + return ms > 0x7FFFFFFF ? 0x7FFFFFFF : (int)ms; } void meow_listview_set_scroll(meow_widget *w, int offset) @@ -1068,8 +1196,9 @@ void meow_listview_set_sel(meow_widget *w, int idx) { if (!w || w->type != W_LISTVIEW) return; if (w->sel_mode == MEOW_LIST_SEL_NONE) return; - if (w->count <= 0) { w->sel = -1; return; } - if (idx >= w->count) idx = w->count - 1; /* 越界 clamp(§5.5 首/末行钳制) */ + int n = listv_count(w); + if (n <= 0) { w->sel = -1; return; } + if (idx >= n) idx = n - 1; /* 越界 clamp(§5.5 首/末行钳制) */ if (w->sel_mode == MEOW_LIST_SEL_SINGLE) { if (idx == w->sel) return; w->sel = idx; /* idx<0 = 清除 */ @@ -1090,9 +1219,9 @@ int meow_listview_get_sel(meow_widget *w, int *out, int max) { if (!w || w->type != W_LISTVIEW) return 0; if (!out || max <= 0) return 0; - int n = 0; + int n = 0, total = listv_count(w); if (w->sel_mode == MEOW_LIST_SEL_MULTI) { - for (int i = 0; i < w->count && n < max; i++) + for (int i = 0; i < total && n < max; i++) if (w->sel_bits && (w->sel_bits[i >> 5] & (1u << (i & 31)))) out[n++] = i; } else if (w->sel >= 0 && n < max) { out[n++] = w->sel; @@ -1103,8 +1232,8 @@ int meow_listview_sel_count(meow_widget *w) { if (!w || w->type != W_LISTVIEW) return 0; if (w->sel_mode == MEOW_LIST_SEL_MULTI) { - int n = 0; - for (int i = 0; i < w->count; i++) + int n = 0, total = listv_count(w); + for (int i = 0; i < total; i++) if (w->sel_bits && (w->sel_bits[i >> 5] & (1u << (i & 31)))) n++; return n; } @@ -1118,12 +1247,18 @@ int meow_listview_hit(meow_widget *w, int x, int y) if (x < w->x || x >= w->x + w->w || y < w->y || y >= w->y + w->h) return -1; int item = listv_item_h(w); int idx = (y - w->y + w->scroll) / item; - if (idx < 0 || idx >= w->count) return -1; + int n = listv_count(w); + if (idx < 0 || idx >= n) return -1; if (w->src2) { /* 禁用行不响应(§5.5) */ meow_list_item it; memset(&it, 0, sizeof it); if (!w->src2(idx, &it, w->ctx)) return -1; if (!it.enabled) return -1; + } else if (w->vs_bind) { /* pool 模式:bind 临时查 enabled */ + meow_lv_item it; + memset(&it, 0, sizeof it); + w->vs_bind(idx, &it, w->vs_ctx); + if (!it.enabled) return -1; } return idx; } @@ -1138,6 +1273,11 @@ int meow_listview_hit_op(meow_widget *w, int x, int y) memset(&it, 0, sizeof it); if (!w->src2(idx, &it, w->ctx)) return 0; if (it.right == MEOW_LIST_RIGHT_NONE) return 0; + } else if (w->vs_bind) { + meow_lv_item it; + memset(&it, 0, sizeof it); + w->vs_bind(idx, &it, w->vs_ctx); + if (it.right == MEOW_LIST_RIGHT_NONE) return 0; } return 1; } @@ -1152,10 +1292,75 @@ int meow_listview_tap_at(meow_widget *w, int x, int y) if (idx < 0) return -1; int op = meow_listview_hit_op(w, x, y); meow_listview_set_sel(w, idx); + meow_ui_focus_acquire((void *)w); /* §6.4:列表成为唯一焦点(点外由 focus_tap 释放) */ if (w->lv_tap_cb) w->lv_tap_cb(idx, op, w->lv_tap_ctx); return idx; } +/* ============================================================ */ +/* §5.5 虚拟化层 API(W_LISTVIEW 扩展;pool 模式;src2 模式不变) */ +/* ============================================================ */ +int meow_listview_set_vsource(meow_widget *w, meow_listview_count_fn count, + meow_listview_bind_fn bind, void *ctx) +{ + if (!w || w->type != W_LISTVIEW || !count || !bind) return -1; + w->vs_count = count; w->vs_bind = bind; w->vs_ctx = ctx; + w->vs_draw = NULL; /* 重挂源:清自绘(默认 renderer) */ + if (listv_ensure_pool(w) != 0) { /* 分配固定 item 池(有界) */ + w->vs_count = NULL; w->vs_bind = NULL; /* 池分配失败:回退 src2 模式 */ + return -1; + } + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); + return 0; +} + +int meow_listview_set_item_renderer(meow_widget *w, int lines, int show_icon, + int show_right) +{ + if (!w || w->type != W_LISTVIEW) return -1; + if (lines < 1) lines = 1; + if (lines > 2) lines = 2; + w->lines = lines; w->show_icon = show_icon ? 1 : 0; w->show_right = show_right ? 1 : 0; + if (w->item_h <= 0) w->item_h = (lines == 1) ? 48 : 56; /* 未显式定行高 → 档位默认 */ + listv_ensure_pool(w); /* 行档位可能变化 → 确保池容量 */ + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); + return 0; +} + +int meow_listview_set_item_tier(meow_widget *w, int lines) +{ + if (!w || w->type != W_LISTVIEW) return -1; + if (lines < 1) lines = 1; + if (lines > 2) lines = 2; + w->lines = lines; + w->item_h = (lines == 1) ? 48 : 56; /* 档位默认高(可随后 set_item_h 覆盖) */ + listv_ensure_pool(w); + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); + return 0; +} + +int meow_listview_set_item_draw(meow_widget *w, meow_listview_draw_fn draw, + void *ctx) +{ + if (!w || w->type != W_LISTVIEW) return -1; + w->vs_draw = draw; w->vs_ctx = ctx; /* 自绘覆盖默认 renderer;NULL=用默认 */ + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); + return 0; +} + +int meow_listview_set_empty_text(meow_widget *w, const char *text) +{ + if (!w || w->type != W_LISTVIEW) return -1; + if (text) { + strncpy(w->empty_text, text, sizeof w->empty_text - 1); + w->empty_text[sizeof w->empty_text - 1] = '\0'; + } else { + w->empty_text[0] = '\0'; + } + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); + return 0; +} + static int hit(meow_widget *w, int x, int y) { if (!w) return 0; @@ -1180,7 +1385,8 @@ int meow_btn_pressed(meow_widget *w) void meow_slider_set(meow_widget *w, int val) { if (!w || w->type != W_SLIDER) return; - if (val < w->min) val = w->min; if (val > w->max) val = w->max; + if (val < w->min) val = w->min; + if (val > w->max) val = w->max; if (val == w->val) return; w->val = val; meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); /* 滑块/轨道随值变化:标脏 */ @@ -1351,79 +1557,187 @@ void meow_widget_draw(meow_widget *w) break; } case W_LISTVIEW: { - /* §5.5 结构化列表(纵向 LazyColumn):只画可见行(虚拟化)+ 视口裁剪(M2.73 纪律)。 - * 行 = 图标(左) + 标题 + 副标题(次要色) + 右侧操作区;选中行 = primaryContainer / - * onPrimaryContainer(§5.8.3);禁用行变淡不响应(命中由 hit 层保证)。全语义色。 */ + /* §5.5 结构化列表(纵向):只画可见行 + 视口裁剪(M2.73)。三路: + * ① src2 模式(既有):每帧按需取行(meow_list2_source),行为不变; + * ② 虚拟化 pool 模式(set_vsource + set_item_renderer):固定 item 池(进入视口才 bind), + * 默认 renderer 画多行富 item(图标+标题+副标题+右尾件),行高可配;滚动只移 viewport; + * ③ 完全自绘(set_item_draw):App 自绘每可见行(SDK 只给矩形+状态位)。 + * 空态:count==0 且设 empty_text → 视口内居中。全语义色;选中/按压/禁用态走主题。 */ int item = listv_item_h(w); int vp = w->h; /* 纵向 */ int off = w->scroll; - int maxs = w->count * item - vp; + int count = listv_count(w); + int maxs = (int)((long long)count * item - vp); if (maxs < 0) maxs = 0; if (off < 0) off = 0; if (off > maxs) off = maxs; int first = (item > 0) ? off / item : 0; int last = (item > 0) ? (off + vp) / item + 1 : 0; if (first < 0) first = 0; - if (last > w->count) last = w->count; + if (last > count) last = count; int vx0 = w->x, vy0 = w->y, vy1 = w->y + w->h; int thh = meow_ui_text_height(th->font_body); int pad = MEOW_LISTV_PAD, isz = MEOW_LISTV_ICON; - int text_x = vx0 + pad + isz + pad; - int op_start = vx0 + (w->w * 2 / 3); /* 行右 1/3 = 操作区(§5.5 命中) */ - int text_maxw = op_start - pad - text_x; /* 标题/副标题可用宽(不压操作区) */ + int text_x = vx0 + pad + ((w->show_icon) ? (isz + pad) : 0); /* 无图标则无图标间隙 */ + int op_start = (w->show_right) ? (vx0 + (w->w * 2 / 3)) /* 行右 1/3 = 操作区 */ + : (vx0 + w->w - pad); + int text_maxw = (w->show_right) ? (op_start - pad - text_x) /* 标题/副标题可用宽(不压操作区) */ + : ((vx0 + w->w - pad) - text_x); if (text_maxw < 8) text_maxw = 8; - for (int i = first; i < last; i++) { - meow_list_item it; - memset(&it, 0, sizeof it); - if (!w->src2 || !w->src2(i, &it, w->ctx)) continue; - int ry = w->y + (i * item - off); - if (ry >= vy1) break; - if (ry + item <= vy0) continue; - int cl = ry > vy0 ? ry : vy0; /* 可见顶界(裁剪用) */ - int cr = (ry + item) < vy1 ? (ry + item) : vy1; /* 可见底界 */ - int sel = (i == w->sel); - uint32_t textcol = sel ? th->onPrimaryContainer : th->onSurface; - uint32_t subcol = sel ? th->onPrimaryContainer : th->onSurfaceVariant; - if (sel) meow_ui_fill_rect(vx0, cl, w->w, cr - cl, th->primaryContainer); - /* 图标(左) */ - uint32_t iccol = sel ? th->onPrimaryContainer - : ((it.icon_kind == MEOW_LIST_ICON_FOLDER) ? th->primary - : th->onSurfaceVariant); - int icy = ry + (item - isz) / 2; - if (icy + isz >= vy0 && icy < vy1) /* 图标不越视口边界才画(防泄漏) */ - listv_draw_icon(th, vx0 + pad, icy, it.icon_kind, it.icon, iccol); + /* 空态占位(§5.5):count==0 且设了 empty_text → 视口内居中画文案。 */ + if (count == 0 && w->empty_text[0]) { + int ty = vy0 + (w->h - thh) / 2; + meow_ui_text_center(vx0 + w->w / 2, ty, w->empty_text, th->font_body, + th->onSurfaceVariant); + if (disabled) meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->onSurface, 90); + break; + } - /* 标题 + 副标题(两行居中;无副标题=单行居中;越界不画防泄漏) */ - int ty; - if (it.subtitle && it.subtitle[0]) { - ty = ry + (item - (2 * thh + MEOW_LISTV_SUB_GAP)) / 2; - if (it.title && ty >= vy0 && ty + thh <= vy1) - listv_text_trunc(text_x, ty, it.title, text_maxw, th->font_body, textcol); - int sy = ty + thh + MEOW_LISTV_SUB_GAP; - if (sy >= vy0 && sy + thh <= vy1) - listv_text_trunc(text_x, sy, it.subtitle, text_maxw, th->font_body, subcol); - } else { - ty = ry + (item - thh) / 2; - if (it.title && ty >= vy0 && ty + thh <= vy1) - listv_text_trunc(text_x, ty, it.title, text_maxw, th->font_body, textcol); + if (w->vs_count && w->vs_bind) { /* 虚拟化 pool 模式 */ + listv_ensure_pool(w); /* 防御:视口/行高变化时确保池容量(cap 已够则零开销保留绑定) */ + if (w->vs_draw) { /* ③ 完全自绘:App 画整行(widget clip 已裁到列表视口) */ + for (int i = first; i < last; i++) { + int ry = w->y + (i * item - off); + if (ry >= vy1) break; + int st = 0; + if (i == w->sel && w->sel_mode != MEOW_LIST_SEL_NONE) st |= MEOW_LISTVIEW_ST_SELECTED; + if (meow_input_touch_down() && + meow_input_touch_x() >= vx0 && meow_input_touch_x() < vx0 + w->w && + meow_input_touch_y() >= ry && meow_input_touch_y() < ry + item) + st |= MEOW_LISTVIEW_ST_PRESSED; + w->vs_draw(i, vx0, ry, w->w, item, st, w->vs_ctx); + } + } else { /* ② 默认 renderer:固定 item 池(进入视口才 bind) */ + for (int i = first; i < last; i++) { + meow_lv_item *it = listv_slot(w, i); /* 绑定到固定池槽(回滚同槽不重绑) */ + if (!it) continue; + int ry = w->y + (i * item - off); + if (ry >= vy1) break; + if (ry + item <= vy0) continue; + int cl = ry > vy0 ? ry : vy0; /* 可见顶界(裁剪用) */ + int cr = (ry + item) < vy1 ? (ry + item) : vy1; + int sel = (i == w->sel && w->sel_mode != MEOW_LIST_SEL_NONE); + int press = meow_input_touch_down() && + meow_input_touch_x() >= vx0 && meow_input_touch_x() < vx0 + w->w && + meow_input_touch_y() >= ry && meow_input_touch_y() < ry + item; + + if (it->section) { /* 段头占位行(非粘性):surfaceContainerHigh 底 + primary 段名 */ + meow_ui_fill_rect(vx0, cl, w->w, cr - cl, th->surfaceContainerHigh); + int sty = ry + (item - thh) / 2; + if (it->title && sty >= vy0 && sty + thh <= vy1) + listv_text_trunc(text_x, sty, it->title, text_maxw, th->font_body, th->primary); + int sy = ry + item - 1; + if (sy >= cl && sy < cr) + meow_ui_fill_rect(vx0, sy, w->w, 1, th->outlineVariant); + if (!it->enabled) + meow_ui_fill_rect_alpha(vx0, cl, w->w, cr - cl, th->onSurface, 90); + continue; + } + + /* 选中/按压背景(primaryContainer / 半透明主题色按压层) */ + if (sel) meow_ui_fill_rect(vx0, cl, w->w, cr - cl, th->primaryContainer); + else if (press) meow_ui_fill_rect_alpha(vx0, cl, w->w, cr - cl, th->primary, 26); + uint32_t textcol = sel ? th->onPrimaryContainer : th->onSurface; + uint32_t subcol = sel ? th->onPrimaryContainer : th->onSurfaceVariant; + + /* 左图标/缩略图(show_icon=1;越界不画防泄漏) */ + if (w->show_icon) { + uint32_t iccol = sel ? th->onPrimaryContainer + : ((it->icon_kind == MEOW_LIST_ICON_FOLDER) ? th->primary + : th->onSurfaceVariant); + int icy = ry + (item - isz) / 2; + if (icy + isz >= vy0 && icy < vy1) + listv_draw_icon(th, vx0 + pad, icy, it->icon_kind, it->icon, iccol); + } + + /* 标题 + 副标题(双行居中;单行(lines==1 或无副标题)单行居中);越界不画 */ + if (w->lines == 2 && it->subtitle && it->subtitle[0]) { + int ty = ry + (item - (2 * thh + MEOW_LISTV_SUB_GAP)) / 2; + if (it->title && ty >= vy0 && ty + thh <= vy1) + listv_text_trunc(text_x, ty, it->title, text_maxw, th->font_body, textcol); + int sy = ty + thh + MEOW_LISTV_SUB_GAP; + if (sy >= vy0 && sy + thh <= vy1) + listv_text_trunc(text_x, sy, it->subtitle, text_maxw, th->font_body, subcol); + } else { + int ty = ry + (item - thh) / 2; + if (it->title && ty >= vy0 && ty + thh <= vy1) + listv_text_trunc(text_x, ty, it->title, text_maxw, th->font_body, textcol); + } + + /* 右尾件(show_right=1:箭头/开关/复选/⋯) */ + if (w->show_right && it->right != MEOW_LIST_RIGHT_NONE) { + int ax = w->x + w->w - pad - 24; /* 右缘动作槽(约 24px) */ + if (ax >= op_start) + listv_draw_right(th, ax, ry, item, it->right, it->on, subcol); + } + + /* 分隔线(仅落在可见带内才画) */ + int sy = ry + item - 1; + if (sy >= cl && sy < cr) + meow_ui_fill_rect(vx0, sy, w->w, 1, th->outlineVariant); + + /* 禁用行整体变淡(§5.5/§5.8.3 enabled=0 → 变暗) */ + if (!it->enabled) + meow_ui_fill_rect_alpha(vx0, cl, w->w, cr - cl, th->onSurface, 90); + } } + } else { + /* ① src2 模式(既有):每帧按需取行(meow_list2_source),行为不变。 + * 行 = 图标(左) + 标题 + 副标题(次要色) + 右侧操作区;选中行 = primaryContainer。 */ + for (int i = first; i < last; i++) { + meow_list_item it; + memset(&it, 0, sizeof it); + if (!w->src2 || !w->src2(i, &it, w->ctx)) continue; + int ry = w->y + (i * item - off); + if (ry >= vy1) break; + if (ry + item <= vy0) continue; + int cl = ry > vy0 ? ry : vy0; /* 可见顶界(裁剪用) */ + int cr = (ry + item) < vy1 ? (ry + item) : vy1; /* 可见底界 */ + int sel = (i == w->sel); + uint32_t textcol = sel ? th->onPrimaryContainer : th->onSurface; + uint32_t subcol = sel ? th->onPrimaryContainer : th->onSurfaceVariant; + if (sel) meow_ui_fill_rect(vx0, cl, w->w, cr - cl, th->primaryContainer); - /* 右侧操作区 */ - if (it.right != MEOW_LIST_RIGHT_NONE) { - int ax = w->x + w->w - pad - 24; /* 右缘动作槽(约 24px) */ - if (ax >= op_start) - listv_draw_right(th, ax, ry, item, it.right, it.on, subcol); + /* 图标(左) */ + uint32_t iccol = sel ? th->onPrimaryContainer + : ((it.icon_kind == MEOW_LIST_ICON_FOLDER) ? th->primary + : th->onSurfaceVariant); + int icy = ry + (item - isz) / 2; + if (icy + isz >= vy0 && icy < vy1) /* 图标不越视口边界才画(防泄漏) */ + listv_draw_icon(th, vx0 + pad, icy, it.icon_kind, it.icon, iccol); + + /* 标题 + 副标题(两行居中;无副标题=单行居中;越界不画防泄漏) */ + int ty; + if (it.subtitle && it.subtitle[0]) { + ty = ry + (item - (2 * thh + MEOW_LISTV_SUB_GAP)) / 2; + if (it.title && ty >= vy0 && ty + thh <= vy1) + listv_text_trunc(text_x, ty, it.title, text_maxw, th->font_body, textcol); + int sy = ty + thh + MEOW_LISTV_SUB_GAP; + if (sy >= vy0 && sy + thh <= vy1) + listv_text_trunc(text_x, sy, it.subtitle, text_maxw, th->font_body, subcol); + } else { + ty = ry + (item - thh) / 2; + if (it.title && ty >= vy0 && ty + thh <= vy1) + listv_text_trunc(text_x, ty, it.title, text_maxw, th->font_body, textcol); + } + + /* 右侧操作区 */ + if (it.right != MEOW_LIST_RIGHT_NONE) { + int ax = w->x + w->w - pad - 24; /* 右缘动作槽(约 24px) */ + if (ax >= op_start) + listv_draw_right(th, ax, ry, item, it.right, it.on, subcol); + } + + /* 分隔线(仅落在可见带内才画) */ + int sy = ry + item - 1; + if (sy >= cl && sy < cr) + meow_ui_fill_rect(vx0, sy, w->w, 1, th->outlineVariant); + + /* 禁用行整体变淡(§5.5/§5.8.3 enabled=0 → 变暗) */ + if (!it.enabled) + meow_ui_fill_rect_alpha(vx0, cl, w->w, cr - cl, th->onSurface, 90); } - - /* 分隔线(仅落在可见带内才画) */ - int sy = ry + item - 1; - if (sy >= cl && sy < cr) - meow_ui_fill_rect(vx0, sy, w->w, 1, th->outlineVariant); - - /* 禁用行整体变淡(§5.5/§5.8.3 enabled=0 → 变暗) */ - if (!it.enabled) - meow_ui_fill_rect_alpha(vx0, cl, w->w, cr - cl, th->onSurface, 90); } if (disabled) meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->onSurface, 90); break; @@ -1575,12 +1889,21 @@ void meow_widget_draw(meow_widget *w) tx = w->x + 48; } if (w->label[0]) meow_ui_text(tx, ty, w->label, th->font_title, th->onSurface); + /* 动作区:从右往左排。位图图标(item_icon>=0)用 sb_icons[] 位图(Material Symbols + * Rounded)经 meow_gfx_blit_icon_ink 画(32px 槽在动作格内水平居中 + ink 垂直对齐 mid, + * 颜色 = onSurface/按压淡色);文本项沿用 meow_ui_text_center(行为不变)。 */ int ay = mid - meow_ui_text_height(th->font_body) / 2; + uint32_t acol = topbar_action_color(w, th); int ax = w->x + w->w - 12; for (int i = w->nitems - 1; i >= 0; i--) { - int aw = meow_ui_text_width(w->items[i], th->font_body) + 12; + int aw = topbar_item_w(w, th, i); ax -= aw; - meow_ui_text_center(ax + aw / 2, ay, w->items[i], th->font_body, th->onSurface); + if (w->item_icon[i] >= 0) { + int slot_x = ax + (aw - MEOW_SB_ICON_SIZE) / 2; /* 32px 槽在格内居中 */ + meow_gfx_blit_icon_ink(w->item_icon[i], slot_x, MEOW_TOPBAR_ICON_INK, acol, mid); + } else { + meow_ui_text_center(ax + aw / 2, ay, w->items[i], th->font_body, acol); + } } meow_ui_fill_rect(w->x, w->y + w->h - 1, w->w, 1, th->outlineVariant); break; diff --git a/sdk/sb_icons.h b/sdk/sb_icons.h index db7570f..fe0de40 100644 --- a/sdk/sb_icons.h +++ b/sdk/sb_icons.h @@ -1,6 +1,11 @@ -/* sb_icons.h — 状态栏/通知中心图标(Material Symbols Rounded, MD3),单通道 alpha 位图 32x32。 - * 生成自 material-symbols@0.47.0 rounded 字体光栅化;每像素 0..255(亮度/alpha)。 - * ★ 为日/夜间预留:位图只存亮度;绘制颜色由调用方主题表提供,换主题仅改配色。 */ +/* sb_icons.h — 状态栏 + 动作位图图标(Material Symbols Rounded, MD3),单通道 alpha 位图 32x32。 + * 生成自 material-symbols rounded(google/material-design-icons,Apache-2.0)SVG 栅格化: + * 图标族 = FILL=1 实心(与状态栏 WiFi/BT/BATT 现有实心风格一致);每像素 0..255(亮度/alpha)。 + * ★ 为日/夜间预留:位图只存亮度;绘制颜色由调用方主题表提供,换主题仅改配色。 + * 索引布局(兼容):状态栏 SB_I_WIFI=0..SB_I_BATT_CHG=14(原样保留);TopAppBar 动作 + * MEOW_ICON_SETTINGS=15..MEOW_ICON_HEART=29(只追加,既有索引不变)。动作图标为 15-29, + * 经 meow_topappbar_set_item_icon(w, idx, MEOW_ICON_*) 使 W_TOPAPPBAR 动作项画位图。 + * 重新生成:python3 tools/gen_action_icons.py(SVG 缓存 tools/icons_src/,可复现)。 */ #ifndef SB_ICONS_H #define SB_ICONS_H @@ -196,6 +201,521 @@ static const unsigned char SB_ICON_BATT_CHG[1024]={ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,31,63,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,191,63,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,63,255,47,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,207,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,95,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,15,239,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,127,255,255,223,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,31,255,255,255,191,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,159,255,255,255,191,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,63,255,255,255,255,207,63,63,63,63,63,63,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,191,255,255,255,255,255,255,255,255,255,255,191,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,95,255,255,255,255,255,255,255,255,255,255,239,31,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,15,223,255,255,255,255,255,255,255,255,255,255,111,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,127,255,255,255,255,255,255,255,255,255,255,191,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,15,239,255,255,255,255,255,255,255,255,255,239,31,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,111,255,255,255,255,79,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,175,255,255,255,175,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,239,255,255,239,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,47,255,255,255,79,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,95,255,255,175,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,159,255,239,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,223,255,63,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,31,255,143,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,79,223,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,143,63,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,95,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }; +/* ===== TopAppBar 动作图标(Material Symbols Rounded, MD3, Apache-2.0)==== + + * 复用状态栏同源 32x32 单通道 alpha;绘制颜色/按压态由 meow_ui_widget.c 主题表提供。 */ + +static const unsigned char SB_ICON_SETTINGS[1024]={ +0,0,0,0,0,0,0,0,0,0,0,0,0,2,4,4,4,4,2,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,1,0,40,84,85,85,84,40,0,1,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,3,0,83,248,255,255,255,255,248,83,0,3,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,2,3,1,0,1,2,213,255,249,252,252,249,255,213,2,1,0,1,3,2,0,0,0,0,0,0, +0,0,0,0,0,2,0,0,0,5,3,17,239,254,254,255,255,254,254,239,17,3,5,0,0,0,2,0,0,0,0,0, +0,0,0,0,2,0,33,55,5,0,0,45,254,254,255,255,255,255,254,254,46,0,0,5,55,33,0,2,0,0,0,0, +0,0,0,3,0,104,246,255,216,115,95,221,254,254,255,255,255,255,254,254,223,96,114,216,255,246,104,0,3,0,0,0, +0,0,2,1,36,251,255,253,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,253,255,251,36,1,2,0,0, +0,0,3,0,174,255,251,255,253,252,252,253,255,254,251,251,251,251,254,255,253,252,252,253,255,251,255,174,0,3,0,0, +0,3,0,67,255,252,255,255,255,255,255,255,253,255,255,255,255,255,255,253,255,255,255,255,255,255,252,255,67,0,3,0, +0,3,0,171,253,251,255,255,255,255,255,253,255,243,151,95,92,142,234,255,253,255,255,255,255,255,251,253,171,0,3,0, +0,4,0,149,255,248,255,255,255,255,253,255,216,41,0,0,0,0,28,198,255,253,255,255,255,255,248,255,149,0,4,0, +0,2,1,29,217,255,254,255,255,254,254,245,41,0,6,4,4,5,0,20,231,254,254,255,255,254,255,217,29,1,2,0, +0,0,1,0,19,172,253,253,255,251,255,163,0,5,0,0,0,0,5,0,129,255,251,255,253,253,172,19,0,1,0,0, +0,0,0,3,0,11,238,255,254,251,255,107,1,4,0,0,0,0,3,1,75,255,252,254,255,239,11,0,3,0,0,0, +0,0,0,3,0,10,238,255,254,251,255,107,1,4,0,0,0,0,3,1,75,255,252,254,255,237,9,0,2,0,0,0, +0,0,1,0,19,172,253,253,255,251,255,163,0,5,0,0,0,0,6,0,129,255,251,255,253,252,167,16,0,1,0,0, +0,2,1,29,217,255,254,255,255,254,254,245,41,0,6,4,4,5,0,21,231,254,254,255,255,254,255,212,25,1,1,0, +0,4,0,149,255,248,255,255,255,255,253,255,217,41,0,0,0,0,28,198,255,253,255,255,255,255,248,255,140,0,4,0, +0,3,0,171,253,251,255,255,255,255,255,253,255,243,152,96,93,143,234,255,253,255,255,255,255,255,251,253,162,0,4,0, +0,3,0,67,255,252,255,255,255,255,255,255,253,255,255,255,255,255,255,253,255,255,255,255,255,255,252,254,60,0,3,0, +0,0,3,0,174,255,251,255,253,252,252,253,255,254,251,251,251,251,254,255,253,252,252,253,255,251,255,162,0,3,0,0, +0,0,2,1,36,251,255,253,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,252,255,244,26,2,2,0,0, +0,0,0,3,0,104,246,255,216,115,96,223,254,254,255,255,255,255,254,254,222,95,117,220,255,241,90,0,3,0,0,0, +0,0,0,0,2,0,33,55,5,0,0,46,254,254,255,255,255,255,254,254,45,0,0,8,56,29,0,2,0,0,0,0, +0,0,0,0,0,2,0,0,0,5,3,17,239,254,254,255,255,254,254,239,17,3,5,0,0,0,2,0,0,0,0,0, +0,0,0,0,0,0,2,3,1,0,1,2,213,255,249,252,252,249,255,213,2,1,0,1,3,2,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,3,0,83,248,255,255,255,255,248,83,0,3,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,1,0,40,84,85,85,84,40,0,1,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,2,4,4,4,4,2,0,0,0,0,0,0,0,0,0,0,0,0,0, +}; +static const unsigned char SB_ICON_REFRESH[1024]={ +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,2,4,4,3,3,4,4,2,0,0,0,0,3,4,2,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,2,2,0,0,0,0,0,0,0,0,3,2,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,3,0,0,40,100,144,164,164,141,94,32,0,0,5,0,88,159,39,0,2,0,0,0, +0,0,0,0,0,0,0,1,2,0,58,180,247,255,255,255,255,255,255,242,167,47,0,15,238,255,163,0,4,0,0,0, +0,0,0,0,0,0,1,2,0,127,253,255,255,255,251,237,237,252,255,255,255,248,109,4,233,251,169,0,3,0,0,0, +0,0,0,0,0,0,3,0,153,255,254,255,192,98,43,21,21,45,102,197,255,255,255,151,225,255,168,0,3,0,0,0, +0,0,0,0,0,3,0,127,255,251,245,97,0,0,0,0,0,0,0,0,101,246,254,255,251,255,169,0,3,0,0,0, +0,0,0,0,3,0,53,255,252,245,63,0,3,4,2,1,1,4,1,0,0,58,242,251,250,255,169,0,3,0,0,0, +0,0,0,0,2,0,184,255,255,95,0,6,0,0,0,0,0,0,12,18,23,8,116,255,249,255,168,0,3,0,0,0, +0,0,0,2,0,37,250,254,195,0,4,0,0,0,0,3,0,89,226,232,234,236,229,251,248,251,170,0,3,0,0,0, +0,0,0,4,0,102,255,255,95,1,4,0,0,0,0,4,0,172,255,255,255,255,255,255,255,255,165,0,4,0,0,0, +0,0,0,4,0,146,255,250,40,0,2,0,0,0,0,2,0,39,158,168,168,168,168,169,169,159,41,0,2,0,0,0, +0,0,0,3,0,165,255,236,20,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,3,0,165,255,236,20,0,1,0,0,0,0,0,0,2,4,3,3,3,3,4,4,4,2,0,0,0,0,0, +0,0,0,4,0,146,255,250,40,0,2,0,0,0,0,0,0,0,0,0,0,0,0,1,0,2,0,0,0,0,0,0, +0,0,0,4,0,102,255,255,95,1,4,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0, +0,0,0,2,0,37,250,254,195,0,4,0,0,0,0,0,0,0,0,0,0,4,0,160,226,88,0,3,0,0,0,0, +0,0,0,0,2,0,184,255,255,95,0,6,0,0,0,0,0,0,0,0,6,0,100,255,255,165,0,4,0,0,0,0, +0,0,0,0,3,0,53,255,252,245,63,0,3,4,2,1,1,2,4,2,0,65,246,251,249,44,0,2,0,0,0,0, +0,0,0,0,0,3,0,127,255,251,245,97,0,0,0,0,0,0,0,0,102,245,251,255,115,0,3,0,0,0,0,0, +0,0,0,0,0,0,3,0,153,255,254,255,192,98,43,21,22,44,102,197,255,255,255,143,0,3,0,0,0,0,0,0, +0,0,0,0,0,0,1,2,0,127,253,255,255,255,251,237,237,251,255,255,255,248,116,0,3,1,0,0,0,0,0,0, +0,0,0,0,0,0,0,1,2,0,57,180,247,255,255,255,255,255,255,244,171,49,0,2,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,3,0,0,40,100,144,164,164,142,97,35,0,0,3,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,2,2,0,0,0,0,0,0,0,0,2,2,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,2,4,4,3,3,4,4,2,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +}; +static const unsigned char SB_ICON_SEARCH[1024]={ +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,1,4,2,0,0,0,1,4,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,3,0,0,0,7,17,11,0,0,0,3,1,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,3,0,19,111,185,221,233,227,201,141,48,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,3,0,88,228,255,255,255,255,255,255,255,250,143,5,0,1,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,3,0,117,255,255,255,241,194,171,182,224,255,255,255,185,8,2,1,0,0,0,0,0,0,0,0,0,0, +0,0,0,3,0,85,255,250,255,142,30,0,0,0,12,95,232,253,255,165,0,3,0,0,0,0,0,0,0,0,0,0, +0,0,1,1,18,234,253,254,97,0,0,2,3,2,0,0,37,227,252,255,79,0,3,0,0,0,0,0,0,0,0,0, +0,0,4,0,112,255,255,144,0,6,2,0,0,0,1,6,0,61,252,254,195,0,2,0,0,0,0,0,0,0,0,0, +0,0,2,0,189,254,241,23,2,2,0,0,0,0,0,0,4,0,182,255,247,30,0,2,0,0,0,0,0,0,0,0, +0,1,0,7,223,255,192,0,2,0,0,0,0,0,0,0,4,1,111,255,255,69,0,3,0,0,0,0,0,0,0,0, +0,1,0,17,233,255,168,1,3,0,0,0,0,0,0,0,4,0,86,255,254,85,0,4,0,0,0,0,0,0,0,0, +0,1,0,11,228,255,180,0,2,0,0,0,0,0,0,0,4,1,98,255,255,76,0,3,0,0,0,0,0,0,0,0, +0,0,1,0,204,255,225,7,2,1,0,0,0,0,0,0,4,0,153,255,251,40,0,2,0,0,0,0,0,0,0,0, +0,0,4,0,144,255,255,93,0,6,0,0,0,0,0,3,0,21,236,255,209,3,1,0,0,0,0,0,0,0,0,0, +0,0,2,0,44,254,253,233,36,0,4,4,4,4,4,0,0,178,255,255,101,0,4,0,0,0,0,0,0,0,0,0, +0,0,0,3,0,143,255,253,225,66,0,0,0,0,0,27,175,255,254,207,0,4,1,0,0,0,0,0,0,0,0,0, +0,0,0,1,3,5,185,255,255,254,181,115,90,102,154,235,255,255,253,242,99,0,5,0,0,0,0,0,0,0,0,0, +0,0,0,0,1,1,8,163,255,255,255,255,255,255,255,255,255,207,252,255,255,100,0,5,0,0,0,0,0,0,0,0, +0,0,0,0,0,1,0,0,82,191,243,255,255,255,249,204,104,2,98,255,251,255,100,0,5,0,0,0,0,0,0,0, +0,0,0,0,0,0,1,2,0,0,31,67,83,74,41,0,0,2,0,99,255,251,255,100,0,5,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,3,1,0,0,0,0,0,0,3,1,5,0,99,255,251,255,100,0,5,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,2,3,4,3,2,0,0,0,0,5,0,99,255,251,255,100,0,3,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,99,255,251,255,100,0,2,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,99,255,251,254,69,0,3,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,99,255,255,94,0,4,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,69,93,2,1,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,4,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +}; +static const unsigned char SB_ICON_PLAY_ARROW[1024]={ +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,1,0,0,3,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,1,0,39,39,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,2,0,48,247,249,134,10,0,3,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,4,0,90,255,252,255,212,72,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,4,0,85,254,252,251,255,255,156,23,0,2,2,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,4,0,86,255,252,255,253,253,255,228,93,0,0,4,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,4,0,86,255,252,255,255,255,251,255,255,177,38,0,1,2,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,4,0,86,255,252,255,255,255,255,254,252,255,241,115,2,0,1,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,4,0,86,255,252,255,255,255,255,255,255,252,255,255,197,51,0,2,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,4,0,86,255,252,255,255,255,255,255,255,255,254,251,255,246,29,0,2,0,0,0,0,0, +0,0,0,0,0,0,0,0,4,0,86,255,252,255,255,255,255,255,255,255,254,251,255,246,29,0,2,0,0,0,0,0, +0,0,0,0,0,0,0,0,4,0,86,255,252,255,255,255,255,255,255,252,255,255,197,51,0,2,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,4,0,86,255,252,255,255,255,255,254,252,255,241,115,2,0,1,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,4,0,86,255,252,255,255,255,251,255,255,177,38,0,1,2,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,4,0,86,255,252,255,253,253,255,228,93,0,0,4,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,4,0,85,254,252,251,255,255,156,23,0,2,2,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,4,0,90,255,252,255,212,72,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,2,0,47,247,249,134,10,0,3,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,1,0,38,39,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,1,0,0,3,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +}; +static const unsigned char SB_ICON_PAUSE[1024]={ +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,2,3,2,0,0,0,0,0,0,0,0,2,3,2,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,2,0,0,0,2,0,0,0,0,0,0,2,0,0,0,2,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,1,0,27,81,52,0,1,0,0,0,0,1,0,52,81,27,0,1,0,0,0,0,0,0,0, +0,0,0,0,0,0,3,0,70,238,255,255,139,0,2,0,0,2,0,139,255,255,238,70,0,3,0,0,0,0,0,0, +0,0,0,0,0,1,1,4,216,255,249,253,255,52,0,2,2,0,52,255,253,249,255,216,4,1,1,0,0,0,0,0, +0,0,0,0,0,1,0,17,239,254,254,251,253,88,0,4,4,0,88,253,251,254,254,239,17,0,1,0,0,0,0,0, +0,0,0,0,0,1,0,17,237,255,254,252,255,86,0,4,4,0,85,255,252,254,255,237,17,0,1,0,0,0,0,0, +0,0,0,0,0,1,0,17,238,255,254,252,255,86,0,4,4,0,86,255,252,254,255,238,17,0,1,0,0,0,0,0, +0,0,0,0,0,1,0,17,238,255,254,252,255,86,0,4,4,0,86,255,252,254,255,238,17,0,1,0,0,0,0,0, +0,0,0,0,0,1,0,17,238,255,254,252,255,86,0,4,4,0,86,255,252,254,255,238,17,0,1,0,0,0,0,0, +0,0,0,0,0,1,0,17,238,255,254,252,255,86,0,4,4,0,86,255,252,254,255,238,17,0,1,0,0,0,0,0, +0,0,0,0,0,1,0,17,238,255,254,252,255,86,0,4,4,0,86,255,252,254,255,238,17,0,1,0,0,0,0,0, +0,0,0,0,0,1,0,17,238,255,254,252,255,86,0,4,4,0,86,255,252,254,255,238,17,0,1,0,0,0,0,0, +0,0,0,0,0,1,0,17,238,255,254,252,255,86,0,4,4,0,86,255,252,254,255,238,17,0,1,0,0,0,0,0, +0,0,0,0,0,1,0,17,238,255,254,252,255,86,0,4,4,0,86,255,252,254,255,238,17,0,1,0,0,0,0,0, +0,0,0,0,0,1,0,17,238,255,254,252,255,86,0,4,4,0,86,255,252,254,255,238,17,0,1,0,0,0,0,0, +0,0,0,0,0,1,0,17,238,255,254,252,255,86,0,4,4,0,86,255,252,254,255,238,17,0,1,0,0,0,0,0, +0,0,0,0,0,1,0,17,237,255,254,252,255,86,0,4,4,0,85,255,252,254,255,237,17,0,1,0,0,0,0,0, +0,0,0,0,0,1,0,17,239,254,254,251,253,88,0,4,4,0,88,253,251,254,254,239,17,0,1,0,0,0,0,0, +0,0,0,0,0,1,1,4,216,255,249,253,255,52,0,2,2,0,52,255,253,249,255,216,4,1,1,0,0,0,0,0, +0,0,0,0,0,0,3,0,70,238,255,255,139,0,2,0,0,2,0,139,255,255,238,70,0,3,0,0,0,0,0,0, +0,0,0,0,0,0,0,1,0,27,80,52,0,1,0,0,0,0,1,0,52,80,27,0,1,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,2,0,0,0,2,0,0,0,0,0,0,2,0,0,0,2,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,2,3,2,0,0,0,0,0,0,0,0,2,3,2,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +}; +static const unsigned char SB_ICON_SKIP_NEXT[1024]={ +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,2,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,2,1,3,1,0,0,0,0,0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,2,2,156,230,86,0,3,0,0,0,0,0, +0,0,0,0,0,2,0,62,198,141,11,0,4,1,0,0,0,0,0,1,0,16,243,255,175,0,3,0,0,0,0,0, +0,0,0,0,0,4,0,173,255,255,213,66,0,0,3,0,0,0,0,1,0,17,232,254,167,0,3,0,0,0,0,0, +0,0,0,0,0,3,0,168,252,247,255,255,141,11,0,4,1,0,0,1,0,17,234,255,168,0,3,0,0,0,0,0, +0,0,0,0,0,3,0,169,255,251,253,253,255,212,65,0,0,3,0,1,0,17,234,255,168,0,3,0,0,0,0,0, +0,0,0,0,0,3,0,169,255,251,255,255,251,255,255,140,10,0,2,1,0,17,234,255,168,0,3,0,0,0,0,0, +0,0,0,0,0,3,0,169,255,251,255,255,255,253,254,255,210,61,0,3,0,17,234,255,168,0,3,0,0,0,0,0, +0,0,0,0,0,3,0,169,255,251,255,255,255,255,255,251,255,252,45,0,0,17,234,255,168,0,3,0,0,0,0,0, +0,0,0,0,0,3,0,169,255,251,255,255,255,255,255,251,255,253,45,0,0,17,234,255,168,0,3,0,0,0,0,0, +0,0,0,0,0,3,0,169,255,251,255,255,255,253,254,255,210,61,0,3,0,17,234,255,168,0,3,0,0,0,0,0, +0,0,0,0,0,3,0,169,255,251,255,255,251,255,255,140,10,0,2,1,0,17,234,255,168,0,3,0,0,0,0,0, +0,0,0,0,0,3,0,169,255,251,253,253,255,212,65,0,0,3,0,1,0,17,234,255,168,0,3,0,0,0,0,0, +0,0,0,0,0,3,0,168,252,247,255,255,142,11,0,4,1,0,0,1,0,17,234,255,168,0,3,0,0,0,0,0, +0,0,0,0,0,4,0,173,255,255,213,66,0,0,3,0,0,0,0,1,0,17,232,254,167,0,3,0,0,0,0,0, +0,0,0,0,0,2,0,62,198,141,11,0,4,1,0,0,0,0,0,1,0,16,243,255,175,0,3,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,2,2,155,230,86,0,3,0,0,0,0,0, +0,0,0,0,0,0,0,2,1,3,1,0,0,0,0,0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,2,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +}; +static const unsigned char SB_ICON_SKIP_PREV[1024]={ +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,2,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,0,0,0,0,0,0,1,3,1,2,0,0,0,0,0,0,0, +0,0,0,0,0,3,0,86,230,155,2,2,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,3,0,175,255,243,16,0,1,0,0,0,0,0,1,4,0,11,141,198,62,0,2,0,0,0,0,0, +0,0,0,0,0,3,0,167,254,232,17,0,1,0,0,0,0,3,0,0,66,213,255,255,173,0,4,0,0,0,0,0, +0,0,0,0,0,3,0,168,255,234,17,0,1,0,0,1,4,0,11,141,255,255,247,252,168,0,3,0,0,0,0,0, +0,0,0,0,0,3,0,168,255,234,17,0,1,0,3,0,0,64,211,255,253,253,251,255,169,0,3,0,0,0,0,0, +0,0,0,0,0,3,0,168,255,234,17,0,1,2,0,10,139,255,255,251,255,255,251,255,169,0,3,0,0,0,0,0, +0,0,0,0,0,3,0,168,255,234,17,0,3,0,60,210,255,254,253,255,255,255,251,255,169,0,3,0,0,0,0,0, +0,0,0,0,0,3,0,168,255,234,17,0,0,46,253,255,251,255,255,255,255,255,251,255,169,0,3,0,0,0,0,0, +0,0,0,0,0,3,0,168,255,234,17,0,0,45,253,255,251,255,255,255,255,255,251,255,169,0,3,0,0,0,0,0, +0,0,0,0,0,3,0,168,255,234,17,0,3,0,60,210,255,254,253,255,255,255,251,255,169,0,3,0,0,0,0,0, +0,0,0,0,0,3,0,168,255,234,17,0,1,2,0,10,139,255,255,251,255,255,251,255,169,0,3,0,0,0,0,0, +0,0,0,0,0,3,0,168,255,234,17,0,1,0,3,0,0,64,211,255,253,253,251,255,169,0,3,0,0,0,0,0, +0,0,0,0,0,3,0,168,255,234,17,0,1,0,0,1,4,0,10,141,255,255,247,252,168,0,3,0,0,0,0,0, +0,0,0,0,0,3,0,167,254,232,17,0,1,0,0,0,0,3,0,0,66,213,255,255,173,0,4,0,0,0,0,0, +0,0,0,0,0,3,0,175,255,243,16,0,1,0,0,0,0,0,1,4,0,11,141,198,62,0,2,0,0,0,0,0, +0,0,0,0,0,3,0,86,230,155,2,2,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,0,0,0,0,0,0,1,3,1,2,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,2,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +}; +static const unsigned char SB_ICON_ARROW_BACK[1024]={ +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,18,17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,4,0,60,232,228,41,1,2,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,4,0,60,242,255,255,73,0,3,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,4,0,60,242,252,255,147,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,4,0,60,242,252,255,149,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,4,0,60,242,252,255,149,0,3,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,4,0,60,242,252,255,149,0,3,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,4,0,60,242,255,255,152,0,7,5,3,4,4,4,4,4,4,4,4,3,1,0,0,0,0,0, +0,0,0,0,0,3,0,60,242,255,250,133,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,2,0,59,242,255,253,213,81,88,87,85,86,86,86,86,86,86,86,86,85,86,76,5,0,1,0,0,0, +0,0,0,1,1,20,237,255,252,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,139,0,4,0,0,0, +0,0,0,1,1,20,237,255,252,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,139,0,4,0,0,0, +0,0,0,0,2,0,59,242,255,253,213,81,88,87,85,86,86,86,86,86,86,86,86,85,86,76,5,0,1,0,0,0, +0,0,0,0,0,3,0,60,242,255,250,133,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,4,0,60,242,255,255,153,0,7,5,3,4,4,4,4,4,4,4,4,3,1,0,0,0,0,0, +0,0,0,0,0,0,0,4,0,60,242,252,255,149,0,3,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,4,0,60,242,252,255,149,0,3,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,4,0,60,242,252,255,149,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,4,0,60,242,252,255,148,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,4,0,60,242,255,255,77,0,4,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,4,0,60,233,229,42,1,2,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,20,18,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +}; +static const unsigned char SB_ICON_CLOSE[1024]={ +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,2,2,3,0,0,0,0,0,0,0,0,0,0,0,0,3,2,2,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,2,0,72,184,90,0,4,0,0,0,0,0,0,0,0,4,0,90,184,72,0,2,0,0,0,0,0, +0,0,0,0,0,3,0,202,255,255,100,0,5,0,0,0,0,0,0,5,0,99,255,255,202,0,3,0,0,0,0,0, +0,0,0,0,0,3,0,90,254,251,255,100,0,5,0,0,0,0,5,0,99,255,251,254,90,0,3,0,0,0,0,0, +0,0,0,0,0,0,2,0,99,255,251,255,100,0,5,0,0,5,0,99,255,251,255,99,0,2,0,0,0,0,0,0, +0,0,0,0,0,0,0,4,0,99,255,251,255,100,0,5,5,0,99,255,251,255,99,0,4,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,5,0,99,255,251,255,100,0,0,100,255,251,255,99,0,5,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,5,0,99,255,251,255,90,90,255,251,255,99,0,5,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,5,0,99,255,254,255,255,254,255,100,0,5,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,5,0,89,249,252,252,249,90,0,5,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,5,0,90,249,252,252,249,90,0,5,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,5,0,100,255,254,255,255,254,255,100,0,5,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,5,0,99,255,251,255,90,89,255,251,255,100,0,5,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,5,0,99,255,251,255,100,0,0,99,255,251,255,100,0,5,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,4,0,99,255,251,255,99,0,5,5,0,99,255,251,255,100,0,4,0,0,0,0,0,0,0, +0,0,0,0,0,0,2,0,99,255,251,255,99,0,5,0,0,5,0,99,255,251,255,100,0,2,0,0,0,0,0,0, +0,0,0,0,0,3,0,90,254,251,255,99,0,5,0,0,0,0,5,0,99,255,251,254,91,0,3,0,0,0,0,0, +0,0,0,0,0,3,0,202,255,255,99,0,5,0,0,0,0,0,0,5,0,99,255,255,201,0,3,0,0,0,0,0, +0,0,0,0,0,2,0,71,184,90,0,4,0,0,0,0,0,0,0,0,4,0,90,184,71,0,2,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,2,2,3,0,0,0,0,0,0,0,0,0,0,0,0,3,2,2,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +}; +static const unsigned char SB_ICON_ADD[1024]={ +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,60,60,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,3,0,56,255,255,56,0,3,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,4,0,90,254,254,90,0,4,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,4,0,85,255,255,85,0,4,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,4,0,86,255,255,86,0,4,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,4,0,86,255,255,86,0,4,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,3,4,4,4,4,8,4,88,255,255,88,4,8,4,4,4,4,3,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,75,255,255,75,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,1,0,55,86,85,86,86,88,85,143,255,255,143,85,88,86,86,85,86,55,0,1,0,0,0,0,0, +0,0,0,0,3,0,60,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,60,0,3,0,0,0,0, +0,0,0,0,3,0,60,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,60,0,3,0,0,0,0, +0,0,0,0,0,1,0,55,86,85,86,86,88,85,143,255,255,143,85,88,86,86,85,86,55,0,1,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,75,255,255,75,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,3,4,4,4,4,8,4,88,255,255,88,4,8,4,4,4,4,3,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,4,0,86,255,255,86,0,4,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,4,0,86,255,255,86,0,4,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,4,0,85,255,255,85,0,4,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,4,0,90,254,254,90,0,4,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,3,0,56,255,255,56,0,3,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,60,60,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +}; +static const unsigned char SB_ICON_CHECK[1024]={ +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,3,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,0,99,91,0,1,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,3,0,141,255,255,62,0,3,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,3,0,142,255,252,242,41,1,2,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,3,0,142,255,252,246,66,0,2,0,0,0,0, +0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,3,0,142,255,252,246,67,0,3,0,0,0,0,0, +0,0,0,0,0,1,0,0,2,0,0,0,0,0,0,0,1,3,0,142,255,252,246,68,0,4,0,0,0,0,0,0, +0,0,0,0,0,0,0,2,0,2,0,0,0,0,0,1,3,0,142,255,251,246,68,0,4,0,0,0,0,0,0,0, +0,0,0,1,1,23,197,212,42,0,4,0,0,0,1,3,0,142,255,251,247,69,0,4,0,0,0,0,0,0,0,0, +0,0,0,3,0,76,255,255,230,42,0,4,0,1,3,0,142,255,251,247,69,0,4,0,0,0,0,0,0,0,0,0, +0,0,0,1,2,11,179,255,254,229,41,0,4,3,0,142,255,251,247,70,0,4,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,1,0,6,182,255,255,229,40,0,0,142,255,251,248,70,0,4,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,2,0,6,182,255,255,229,42,135,255,251,248,71,0,5,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,2,0,6,182,255,255,246,255,253,248,71,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,2,0,6,182,255,250,254,248,72,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,2,0,7,180,255,247,73,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,2,0,4,105,55,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,1,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,1,4,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +}; +static const unsigned char SB_ICON_LYRICS[1024]={ +0,0,0,0,3,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,3,1,0,0,0,0,0,0,0,0,0,0, +0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,1,9,131,167,164,163,90, +0,0,1,0,52,85,85,86,86,86,86,86,86,86,86,86,86,86,86,85,72,10,0,1,3,0,78,255,255,255,255,255, +0,2,0,139,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,213,32,1,5,0,88,251,248,239,233,167, +3,0,52,255,253,251,252,252,252,252,252,252,252,252,252,252,252,252,252,252,248,255,144,0,8,0,86,255,255,97,16,0, +4,0,88,253,251,255,255,255,255,254,254,254,254,254,254,254,254,255,255,255,250,255,121,0,8,1,86,255,255,83,0,5, +4,0,85,255,252,255,255,255,254,255,255,255,255,255,255,255,255,254,255,252,255,144,8,5,3,0,86,255,255,87,1,4, +4,0,86,255,252,255,255,255,255,239,238,238,238,238,238,237,253,255,253,255,113,0,0,0,10,14,86,255,255,86,0,4, +4,0,86,255,252,255,253,252,99,22,23,21,21,21,22,22,49,219,255,169,0,4,11,140,221,233,226,252,255,86,0,4, +4,0,86,255,252,254,255,243,3,0,0,0,0,0,0,0,0,176,255,40,2,7,188,255,255,255,255,252,255,86,0,4, +4,0,86,255,252,255,253,255,169,93,93,91,91,91,92,91,122,248,211,1,0,97,255,248,253,254,254,252,255,86,0,4, +4,0,86,255,252,255,255,255,255,254,253,253,253,253,253,253,255,255,172,0,0,164,253,251,255,255,255,252,254,87,0,4, +4,0,86,255,252,255,253,252,97,19,20,18,18,18,19,18,44,232,180,1,0,164,253,251,255,255,255,252,255,79,0,4, +4,0,86,255,252,254,255,243,3,0,0,0,0,0,0,0,0,187,224,0,0,97,255,248,253,254,251,253,248,27,0,2, +4,0,86,255,252,255,253,255,169,93,93,91,88,86,88,86,123,246,253,42,2,7,188,255,255,255,255,255,113,0,3,0, +4,0,86,255,252,255,255,255,255,254,253,253,255,255,255,255,255,254,255,169,0,4,11,140,220,232,203,94,0,2,0,0, +4,0,86,255,252,255,253,252,96,19,21,18,46,219,251,250,251,254,253,255,111,0,0,0,5,15,0,0,2,0,0,0, +4,0,86,255,252,254,255,243,4,0,0,0,0,176,255,251,255,255,255,253,255,142,11,3,0,0,0,3,0,0,0,0, +4,0,86,255,252,255,253,255,170,88,90,87,124,246,254,254,255,255,255,255,252,255,198,12,2,2,0,0,0,0,0,0, +4,0,86,255,252,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,248,255,108,0,4,0,0,0,0,0,0, +4,0,86,255,252,255,255,255,252,252,252,252,251,254,255,255,255,255,255,255,255,251,251,155,0,4,0,0,0,0,0,0, +4,0,86,255,252,255,255,255,254,254,254,254,254,254,254,254,254,254,254,254,254,248,255,117,0,4,0,0,0,0,0,0, +4,0,86,255,252,255,255,253,255,255,255,255,255,255,255,255,255,255,255,255,255,255,224,21,1,1,0,0,0,0,0,0, +4,0,86,255,252,255,253,255,238,238,238,238,238,238,238,238,238,238,237,236,229,171,34,0,2,0,0,0,0,0,0,0, +4,0,85,255,251,251,255,128,18,20,17,17,17,17,17,17,17,17,17,17,11,0,0,1,0,0,0,0,0,0,0,0, +4,0,89,252,247,255,127,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,2,0,0,0,0,0,0,0,0,0, +3,0,65,255,255,127,0,4,2,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0, +0,1,0,88,89,0,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,3,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +}; +static const unsigned char SB_ICON_QUEUE_MUSIC[1024]={ +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,1,1,1,1,1,1,0,0,0, +0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,1,0,0, +0,0,0,0,0,16,17,17,17,17,17,17,17,17,17,17,17,17,16,0,0,1,0,3,17,17,17,18,2,0,0,0, +0,0,2,3,156,233,232,234,234,234,234,234,234,234,234,234,234,232,233,156,4,2,31,209,237,234,233,233,206,32,1,2, +0,1,0,12,251,255,255,255,255,255,255,255,255,255,255,255,255,255,255,251,15,0,89,255,255,255,255,255,255,80,0,4, +0,0,2,0,85,167,167,168,168,168,168,168,168,168,168,168,168,167,167,85,1,0,85,253,252,197,167,170,131,9,1,1, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,86,255,255,76,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,86,255,255,88,3,7,4,1,0,0, +0,0,2,0,85,167,167,168,168,168,168,168,168,168,168,168,168,167,167,85,1,0,86,255,255,86,0,4,0,0,0,0, +0,1,0,12,251,255,255,255,255,255,255,255,255,255,255,255,255,255,255,251,15,0,87,255,255,86,0,4,0,0,0,0, +0,0,2,3,156,233,232,234,234,234,234,234,234,234,234,234,234,232,233,155,5,0,86,255,255,86,0,4,0,0,0,0, +0,0,0,0,0,20,21,21,21,21,21,21,21,21,18,17,17,17,16,0,6,3,88,255,255,86,0,4,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,78,255,255,86,0,4,0,0,0,0, +0,0,1,0,27,85,86,87,87,87,87,86,87,77,6,1,1,3,0,7,68,80,122,254,255,86,0,4,0,0,0,0, +0,1,1,8,216,255,255,255,255,255,255,255,255,255,139,0,6,0,56,214,255,255,255,252,255,86,0,4,0,0,0,0, +0,1,1,8,216,255,255,255,255,255,255,255,255,255,139,0,2,28,237,255,252,252,254,252,255,86,0,4,0,0,0,0, +0,0,1,0,27,84,85,85,86,86,86,85,86,76,5,3,0,128,255,249,255,255,255,252,255,86,0,4,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,169,254,251,255,255,255,252,254,87,0,4,0,0,0,0, +0,0,0,0,2,4,4,4,4,4,4,4,4,3,1,4,0,151,254,250,255,255,255,252,255,65,0,3,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,60,255,253,250,252,249,255,220,10,1,1,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,121,255,255,255,255,233,55,0,2,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,62,146,164,125,28,0,2,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,2,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,4,3,4,2,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +}; +static const unsigned char SB_ICON_DOWNLOAD[1024]={ +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,4,4,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,1,1,9,139,139,9,1,1,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,4,0,77,255,255,77,0,4,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,4,0,88,251,251,88,0,4,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,4,0,86,255,255,86,0,4,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,4,0,86,255,255,86,0,4,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,4,0,86,255,255,86,0,4,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,3,4,5,0,86,255,255,86,0,5,4,3,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,1,86,255,255,86,1,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,2,0,103,160,30,0,90,255,255,90,0,29,160,103,0,2,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,2,0,30,255,255,218,19,73,255,255,73,19,218,255,255,30,0,2,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,1,2,10,189,255,255,204,113,255,255,113,203,255,255,189,9,2,1,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,1,0,13,194,255,255,253,255,255,253,255,255,195,13,0,1,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,1,0,0,2,0,13,194,255,251,255,255,251,255,195,13,0,2,0,0,1,0,0,0,0,0,0, +0,0,0,0,0,2,0,1,0,0,2,0,12,194,255,249,249,255,195,13,0,2,0,0,1,0,2,0,0,0,0,0, +0,0,0,0,0,0,11,0,0,0,0,2,0,13,194,255,255,195,13,0,2,0,0,0,0,11,0,0,0,0,0,0, +0,0,0,3,0,86,230,155,2,2,0,0,2,0,12,164,164,13,0,2,0,0,2,2,156,230,86,0,3,0,0,0, +0,0,0,3,0,175,255,243,17,1,2,1,1,2,0,0,0,0,2,1,1,2,1,17,243,255,175,0,3,0,0,0, +0,0,0,3,0,166,254,232,13,0,0,0,0,0,0,0,0,0,0,0,0,0,0,13,232,254,166,0,3,0,0,0, +0,0,0,3,0,169,254,235,33,17,18,17,17,17,17,17,17,17,17,17,17,18,17,33,235,254,169,0,3,0,0,0, +0,0,0,4,0,159,255,246,235,234,234,234,234,234,234,234,234,234,234,234,234,234,234,235,246,255,159,0,4,0,0,0, +0,0,0,2,1,55,249,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,249,55,1,2,0,0,0, +0,0,0,0,1,0,55,153,167,168,168,168,168,168,168,168,168,168,168,168,168,168,168,167,153,55,0,1,0,0,0,0, +0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0, +0,0,0,0,0,0,2,4,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,4,2,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +}; +static const unsigned char SB_ICON_HEART[1024]={ +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,2,3,4,4,3,2,0,0,0,0,0,0,2,3,4,4,3,2,0,0,0,0,0,0,0, +0,0,0,0,0,1,3,0,0,0,0,0,0,3,2,0,0,1,3,0,0,0,0,0,0,3,1,0,0,0,0,0, +0,0,0,0,2,0,0,31,84,114,113,81,28,0,0,2,2,0,0,27,81,113,114,84,31,0,0,2,0,0,0,0, +0,0,0,2,0,27,162,244,255,255,255,255,241,158,30,0,0,30,158,241,255,255,255,255,244,162,27,0,2,0,0,0, +0,0,2,0,46,227,255,255,252,251,251,252,255,255,233,55,55,233,255,255,252,251,251,252,255,255,227,46,0,2,0,0, +0,1,2,21,226,255,250,254,255,255,255,255,254,251,255,243,243,255,251,254,255,255,255,255,254,250,255,226,21,2,1,0, +0,3,0,147,255,250,255,255,255,255,255,255,255,255,254,255,255,254,255,255,255,255,255,255,255,255,250,255,147,0,3,0, +1,0,16,238,254,254,255,255,255,255,255,255,255,255,255,254,254,255,255,255,255,255,255,255,255,255,254,254,237,15,0,1, +3,0,61,255,253,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,253,255,60,0,3, +4,0,84,254,252,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,252,254,84,0,4, +4,0,81,255,252,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,252,255,81,0,4, +3,0,55,255,254,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,254,255,55,0,3, +1,0,13,235,255,254,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,254,255,235,14,0,1, +0,3,0,161,255,251,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,251,255,163,0,3,0, +0,2,0,47,254,252,254,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,254,252,254,49,0,3,0, +0,0,3,0,141,255,250,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,250,255,146,0,3,0,0, +0,0,1,3,6,195,255,251,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,251,255,201,8,3,1,0,0, +0,0,0,2,0,19,213,255,252,255,255,255,255,255,255,255,255,255,255,255,255,255,255,252,255,219,24,0,2,0,0,0, +0,0,0,0,2,0,29,223,255,252,255,255,255,255,255,255,255,255,255,255,255,255,252,255,229,36,0,3,0,0,0,0, +0,0,0,0,0,3,0,36,227,255,252,255,255,255,255,255,255,255,255,255,255,252,255,232,43,0,3,0,0,0,0,0, +0,0,0,0,0,0,3,0,39,226,255,251,255,255,255,255,255,255,255,255,252,255,231,45,0,4,0,0,0,0,0,0, +0,0,0,0,0,0,0,3,0,37,222,255,251,255,255,255,255,255,255,251,255,226,42,0,4,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,3,0,31,213,255,251,255,255,255,255,251,255,215,34,0,4,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,3,0,21,199,255,250,254,254,250,255,200,22,0,3,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,3,0,12,182,255,255,255,255,183,12,0,3,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,2,0,4,154,242,243,154,4,0,2,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,25,25,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,0,0,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +}; + static const sb_icon_t sb_icons[] = { { SB_ICON_WIFI }, { SB_ICON_BT }, @@ -212,11 +732,42 @@ static const sb_icon_t sb_icons[] = { { SB_ICON_BT_DIS }, { SB_ICON_BT_SEARCH }, { SB_ICON_BATT_CHG }, + { SB_ICON_SETTINGS }, + { SB_ICON_REFRESH }, + { SB_ICON_SEARCH }, + { SB_ICON_PLAY_ARROW }, + { SB_ICON_PAUSE }, + { SB_ICON_SKIP_NEXT }, + { SB_ICON_SKIP_PREV }, + { SB_ICON_ARROW_BACK }, + { SB_ICON_CLOSE }, + { SB_ICON_ADD }, + { SB_ICON_CHECK }, + { SB_ICON_LYRICS }, + { SB_ICON_QUEUE_MUSIC }, + { SB_ICON_DOWNLOAD }, + { SB_ICON_HEART }, }; -/* 枚举索引(与 sb_icons[] 对应) */ +/* 枚举索引(与 sb_icons[] 对应)。状态栏 0-14(勿改序);动作 15-29(TopAppBar 用)。 */ enum { SB_I_WIFI=0, SB_I_BT=1, SB_I_BATT=2, SB_I_VOL=3, SB_I_BRIGHT=4, SB_I_WIFI_STRONG=5, SB_I_WIFI_MEDIUM=6, SB_I_WIFI_WEAK=7, SB_I_WIFI_NO=8, SB_I_WIFI_OFF=9, SB_I_BT_NO=10, SB_I_BT_CONN=11, - SB_I_BT_DIS=12, SB_I_BT_SEARCH=13, SB_I_BATT_CHG=14 }; -#endif /* SB_ICONS_H */ \ No newline at end of file + SB_I_BT_DIS=12, SB_I_BT_SEARCH=13, SB_I_BATT_CHG=14, + SB_I_SETTINGS=15, + SB_I_REFRESH=16, + SB_I_SEARCH=17, + SB_I_PLAY_ARROW=18, + SB_I_PAUSE=19, + SB_I_SKIP_NEXT=20, + SB_I_SKIP_PREV=21, + SB_I_ARROW_BACK=22, + SB_I_CLOSE=23, + SB_I_ADD=24, + SB_I_CHECK=25, + SB_I_LYRICS=26, + SB_I_QUEUE_MUSIC=27, + SB_I_DOWNLOAD=28, + SB_I_HEART=29 }; + +#endif /* SB_ICONS_H */ diff --git a/sdk/test_ime_fix.c b/sdk/test_ime_fix.c index d9034b4..fb321e0 100644 --- a/sdk/test_ime_fix.c +++ b/sdk/test_ime_fix.c @@ -7,7 +7,7 @@ * 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global): * gcc -Wall -Wextra -o test_ime_fix test_ime_fix.c test_host_stub.c \ * meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ - * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c \ + * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_ui_draw.c meow_ui_view.c meow_ui_img.c meow_notify_ipc.c stb_image.c \ * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm * (meow_ui_widget.c 新 W_LISTVIEW 引用 meow_ui_img_scaled → 需链 meow_ui_img.c+stb_image.c+-lm) diff --git a/sdk/test_meow_ui_anim.c b/sdk/test_meow_ui_anim.c new file mode 100644 index 0000000..9ac1720 --- /dev/null +++ b/sdk/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_ui_str.c \ + * meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \ + * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \ + * -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/sdk/test_meow_ui_box.c b/sdk/test_meow_ui_box.c new file mode 100644 index 0000000..2ec9aaa --- /dev/null +++ b/sdk/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_ui_str.c \ + * meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \ + * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \ + * -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/sdk/test_meow_ui_dispatch.c b/sdk/test_meow_ui_dispatch.c new file mode 100644 index 0000000..aaea24a --- /dev/null +++ b/sdk/test_meow_ui_dispatch.c @@ -0,0 +1,145 @@ +/* test_meow_ui_dispatch.c — §6.4 事件分发形式化宿主验证(离屏 480x720,16bpp)。 + * + * 覆盖(新穿透矩阵,全部直接驱动 SDK §6.4 仲裁者 API + 真实注册控件): + * M1 模态打开点下层不触发:modal_begin 后 meow_ui_hit_topdown 恒返回模态哨兵(不再落到下层 + * 同坐标控件),gesture_begin 捕获的是模态(哨兵)而非下层按钮——下层绝不消费; + * M2 弹层叠开两层不叠黑/层隔离:modal_begin 两次 → depth=2;end(LIFO)逐层递减;期间 + * hit_topdown 始终返回哨兵,下层收不到任何触摸; + * M3 捕获手势滑出不误触:gesture_begin 命中按钮 A 后捕获;任凭概念滑动到远处(不重新命中), + * meow_ui_gesture_owner 恒为 A(绝不滑出误触下层/邻控件);end 释放; + * M4 list 焦点点外释放:meow_list_tap_at/meow_listview_tap_at 使列表成为唯一焦点; + * focus_tap 落在列表内 → 保位;落在列表外(页面内容/空白)→ 释放焦点并触发 lost 回调。 + * + * 编译(宿主 gcc,链真实 widget/draw/input/activity/gfx/sys/theme/vol/log/dialog/ime/notify/ipc + + * 打桩 font/global;完整超集便于链接): + * gcc -Wall -Wextra -o test_meow_ui_dispatch test_meow_ui_dispatch.c test_host_stub.c \ + * meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ + * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ + * meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \ + * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \ + * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm + * 运行:./test_meow_ui_dispatch (无设备,纯宿主) + */ +#include "meow_ui.h" +#include "meow_ui_internal.h" +#include "meow_ui_view.h" +#include +#include +#include + +static int g_fail = 0; +#define CHECK(cond, name) do { \ + if (cond) { printf(" PASS %s\n", name); } \ + else { printf(" FAIL %s\n", name); g_fail++; } } while (0) + +/* 焦点丢失回调计数 */ +static int g_lost_calls = 0; +static void *g_lost_last = NULL; +static void focus_lost(void *owner, void *ctx) +{ (void)ctx; g_lost_calls++; g_lost_last = owner; } + +/* 列表数据源(最小 W_LIST) */ +static int list_src(int idx, char *label, int len, void *ctx) +{ + (void)ctx; + if (idx < 0 || idx >= 8) return 0; + snprintf(label, (size_t)len, "item %d", idx); + return 1; +} + +int main(void) +{ + setenv("MEOW_UI_DUMP", "/tmp/dispatch.ppm", 1); + meow_ui_opts o = { 480, 720 }; + if (meow_ui_init(&o) != 0) { printf("init failed\n"); return 1; } + meow_ui_activity_push(5); /* 页面控件 owner=5,命中/绘制对齐 */ + + /* 场景控件:两个不重叠按钮 A/B + 一个 LazyColumn 列表 */ + meow_widget *btnA = meow_btn_create(20, 100, 200, 60, "A"); + meow_widget *btnB = meow_btn_create(240, 100, 200, 60, "B"); + meow_widget *list = meow_lazycolumn_create(20, 300, 400, 150, 44, 8, list_src, NULL); + + /* ================= M1:模态打开点下层不触发 ================= + * 无模态:hit_topdown(按钮A中心) == 按钮A;modal_begin 后同一落点返回模态哨兵(≠按钮A), + * gesture_begin 捕获的是模态而非下层按钮——下层绝不消费(穿透根治)。 */ + printf("M1 modal open -> lower not triggered\n"); + void *h0 = meow_ui_hit_topdown(120, 130); + printf(" no-modal hit_topdown(120,130) = %s\n", + h0 == (void *)btnA ? "btnA" : (h0 ? "other" : "NULL")); + CHECK(h0 == (void *)btnA, "no-modal topdown hits btnA (z-top)"); + + meow_ui_modal *m = meow_ui_modal_begin(); + void *hm = meow_ui_hit_topdown(120, 130); + printf(" modal-open hit_topdown(120,130) = %s (sentinel=%d)\n", + hm == (void *)btnA ? "btnA(LEAK!)" : "modal-sentinel", hm != NULL && hm != (void *)btnA); + CHECK(meow_ui_modal_active(), "modal active"); + CHECK(hm != NULL && hm != (void *)btnA, "modal present -> hit_topdown never reaches btnA"); + void *g0 = meow_ui_gesture_begin(120, 130); /* 下层同坐标:捕获的是模态而非按钮 */ + CHECK(g0 == hm, "gesture_begin during modal captures modal (sentinel), not btnA"); + meow_ui_gesture_end(); + meow_ui_modal_end(m); + CHECK(meow_ui_hit_topdown(120, 130) == (void *)btnA, "after modal_end topdown returns btnA again"); + CHECK(!meow_ui_modal_active(), "modal released"); + + /* ================= M2:弹层叠开两层(层隔离,depth 语义) ================= + * 两层弹开:depth 1→2;期间 hit_topdown 恒返回哨兵;LIFO 关内层→1,关外层→0。 + * 叠黑由 Dialog/Sheet 的「打开前整帧快照 + 关闭还原 + 高亮仅重绘卡片不叠遮罩」保证 + * (meow_ui_dialog.c,M2.71-bug3),本处验证层隔离 + 栈深。 */ + printf("M2 stacked modals (two layers)\n"); + meow_ui_modal *m1 = meow_ui_modal_begin(); + meow_ui_modal *m2 = meow_ui_modal_begin(); + CHECK(meow_ui_modal_depth() == 2, "two modals -> depth 2"); + CHECK(meow_ui_hit_topdown(120, 130) != (void *)btnA, "either modal -> lower btnA blocked"); + meow_ui_modal_end(m2); + CHECK(meow_ui_modal_depth() == 1, "close inner -> depth 1"); + CHECK(meow_ui_hit_topdown(120, 130) != (void *)btnA, "outer modal still blocks lower"); + meow_ui_modal_end(m1); + CHECK(meow_ui_modal_depth() == 0, "close outer -> depth 0"); + CHECK(meow_ui_hit_topdown(120, 130) == (void *)btnA, "both closed -> lower reachable"); + + /* ================= M3:捕获手势滑出不误触 ================= + * touch-down 命中按钮 A → 捕获。此后 move(即使滑到按钮 B / 空白)不重新命中,owner 恒为 A; + * 绝不滑出误触 B 或下层。抬手(gesture_end)释放。 */ + printf("M3 gesture capture: slide-out no false-trigger\n"); + void *g1 = meow_ui_gesture_begin(120, 130); + CHECK(g1 == (void *)btnA, "down on A captures A"); + CHECK(meow_ui_gesture_owner() == (void *)btnA, "owner == A after down"); + /* 概念滑动到按钮 B 中心 (340,130):捕获不重命中 → 仍是 A(不误触 B) */ + CHECK(meow_ui_gesture_owner() == (void *)btnA, "slide to B point: owner stays A (no re-hit)"); + /* 滑出到空白 */ + CHECK(meow_ui_gesture_owner() == (void *)btnA, "slide to blank: owner stays A"); + meow_ui_gesture_end(); + CHECK(meow_ui_gesture_owner() == NULL, "release -> no owner"); + + /* ================= M4:list 焦点点外释放 ================= + * meow_list_tap_at 使列表成为唯一焦点;focus_tap 落在列表内→保位;落在列表外→释放并通知。 */ + printf("M4 list focus release on tap outside\n"); + meow_ui_focus_set_lost_cb(focus_lost, NULL); + CHECK(meow_ui_focus_owner() == NULL, "no focus initially"); + int idx = meow_list_tap_at(list, 120, 320); /* 命中列表行0 */ + CHECK(idx >= 0, "list tap hits a row"); + CHECK(meow_ui_focus_owner() == (void *)list, "list tap -> list becomes focus owner"); + meow_ui_focus_tap(120, 320); /* 点列表内:保位 */ + CHECK(meow_ui_focus_owner() == (void *)list, "tap inside list -> focus retained"); + CHECK(g_lost_calls == 0, "no lost callback while focus retained"); + meow_ui_focus_tap(120, 130); /* 命中按钮A(非列表):释放焦点+通知 */ + CHECK(meow_ui_focus_owner() == NULL, "tap outside list (on A) -> focus released"); + CHECK(g_lost_calls == 1 && g_lost_last == (void *)list, "lost callback fired with list owner"); + /* 再次聚焦后再点「空白」(无控件命中):同样释放 */ + meow_list_tap_at(list, 120, 320); + meow_ui_focus_tap(400, 650); /* 列表外空白 */ + CHECK(meow_ui_focus_owner() == NULL, "tap on blank -> focus released"); + CHECK(g_lost_calls == 2, "lost callback fired again"); + (void)btnB; + + /* 清理:控件释放自动注销 z 序绘制物 + 焦点/捕获复位 */ + meow_widget_free(list); + meow_widget_free(btnB); + meow_widget_free(btnA); + meow_ui_focus_release(NULL); + meow_ui_activity_pop(); + meow_ui_shutdown(); + + printf(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS\n", g_fail); + return g_fail; +} diff --git a/sdk/test_meow_ui_listvirt.c b/sdk/test_meow_ui_listvirt.c new file mode 100644 index 0000000..27c2417 --- /dev/null +++ b/sdk/test_meow_ui_listvirt.c @@ -0,0 +1,365 @@ +/* test_meow_ui_listvirt.c — 宿主验证 §5.5 虚拟化列表(W_LISTVIEW pool 模式): + * T1 虚拟化正确性:10000 项滚动窗口内容正确(hit 行号映射)/ bind 次数有界(全量扫过=count)/ + * 回滚不重绑(scroll 回顶 bind 计数不变)。 + * T2 多行富 item 渲染:双行(标题+副标题)/ 单行(仅标题)/ 左图标占位 / 右尾件 / + * 副标题超宽截断(不越过操作区)。 + * T3 段头占位行(bind 返回 section=1)+ 空态占位(count=0 置 empty_text)。 + * T4 完全自绘(set_item_draw 覆盖默认 renderer)。 + * T5 焦点(tap_at → meow_ui_focus_owner)+ fling 联动(meow_scroll_wrap 包裹 pool 列表, + * track+fling 吸附到行边界;滚动只改 viewport 偏移)。 + * + * 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global): + * gcc -o test_meow_ui_listvirt test_meow_ui_listvirt.c test_host_stub.c \ + * meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ + * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ + * meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \ + * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \ + * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm + * 运行:./test_meow_ui_listvirt + */ +#include "meow_ui.h" +#include "meow_ui_internal.h" /* meow_ui_back_buf/pitch/bpp:真实像素断言 */ +#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) + +/* ---- back_buf 像素读取(真实渲染断言) ---- */ +static void rd_px(int x, int y, int *r, int *g, int *b) +{ + unsigned char *bb = meow_ui_back_buf(); + int line = meow_ui_back_buf_pitch(), bpp = meow_ui_back_buf_bpp(); + if (!bb || x < 0 || y < 0) { *r = *g = *b = 0; return; } + if (bpp == 16) { + uint16_t px = *(uint16_t *)(bb + (size_t)y * line + (size_t)x * 2); + *r = (((px >> 11) & 0x1F) << 3) | ((px >> 11) & 0x1F) >> 2; + *g = (((px >> 5) & 0x3F) << 2) | ((px >> 5) & 0x3F) >> 4; + *b = ((px & 0x1F) << 3) | ((px & 0x1F) >> 2); + } else { + uint32_t px = *(uint32_t *)(bb + (size_t)y * line + (size_t)x * 4); + *r = (px >> 16) & 0xFF; *g = (px >> 8) & 0xFF; *b = px & 0xFF; + } +} +static int near2(int a, int b) { int d = a - b; if (d < 0) d = -d; return d <= 14; } +static int px_near(int x, int y, int r, int g, int b) +{ + int rr, gg, bb; rd_px(x, y, &rr, &gg, &bb); + return near2(rr, r) && near2(gg, g) && near2(bb, b); +} +static int region_has(int x, int y, int w, int h, int r, int g, int b) +{ + int cnt = 0; + for (int dy = 0; dy < h; dy++) + for (int dx = 0; dx < w; dx++) + if (px_near(x + dx, y + dy, r, g, b)) cnt++; + return cnt; +} +#define UNPK(c, rr, gg, bb) do { (rr) = ((c) >> 16) & 0xFF; (gg) = ((c) >> 8) & 0xFF; (bb) = (c) & 0xFF; } while (0) + +/* ---- 虚拟化数据源(10000 项):bind 计数有界 / 内容正确 / 段头 ---- */ +#define N 10000 +static int g_bind_total; /* pool bind 总数(经 listv_slot 调用;hit 走独立路径不计数) */ +static int g_bind_touch_count; /* bind 回调的独立计数(hit 也会调 bind,故仅命中计数用) */ +static int g_count_override; /* <0 = 用默认 N;>=0 = 覆盖(空态/小列表用) */ +static int g_section_at; /* 该 index 返回段头(-1=无) */ + +static int vcount(void *ctx) { (void)ctx; return g_count_override >= 0 ? g_count_override : N; } +static void vbind(int idx, meow_lv_item *it, void *ctx) +{ + (void)ctx; + g_bind_total++; + g_bind_touch_count++; + memset(it, 0, sizeof *it); + it->icon_kind = MEOW_LIST_ICON_FOLDER; + it->right = MEOW_LIST_RIGHT_ARROW; + it->on = 1; it->enabled = 1; + if (g_section_at == idx) { + it->section = 1; + it->title = "SECTION A"; it->subtitle = NULL; it->icon_kind = MEOW_LIST_ICON_NONE; + it->right = MEOW_LIST_RIGHT_NONE; + return; + } + static char t[64], s[64]; + snprintf(t, sizeof t, "title %d", idx); + snprintf(s, sizeof s, "sub %d", idx); + it->title = t; it->subtitle = s; +} + +/* ---- 数据源 A:单行(无副标题/无图标/无尾件),供 T2 单行断言 ---- */ +static int via_count(void *ctx) { (void)ctx; return 3; } +static void via_bind(int idx, meow_lv_item *it, void *ctx) +{ + (void)ctx; (void)idx; + memset(it, 0, sizeof *it); + static char t[32]; snprintf(t, sizeof t, "single %d", idx); + it->title = t; it->subtitle = NULL; it->icon_kind = MEOW_LIST_ICON_NONE; + it->right = MEOW_LIST_RIGHT_NONE; it->enabled = 1; +} + +/* ---- 数据源:超宽副标题(T2 截断) ---- */ +static int long_count(void *ctx) { (void)ctx; return 1; } +static void long_bind(int idx, meow_lv_item *it, void *ctx) +{ + (void)ctx; (void)idx; + memset(it, 0, sizeof *it); + it->title = "Title"; + it->subtitle = "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW"; /* 超宽 */ + it->icon_kind = MEOW_LIST_ICON_NONE; + it->right = MEOW_LIST_RIGHT_NONE; it->enabled = 1; +} + +/* ---- T4 完全自绘:整行画实心块 + 标题 ---- */ +static int g_draw_calls; +static void mydraw(int idx, int x, int y, int w, int h, int st, void *ctx) +{ + (void)idx; (void)ctx; + g_draw_calls++; + meow_ui_fill_rect(x, y, w, h, 0x334455); /* 自定义行底 */ + meow_ui_fill_rect(x + 4, y + 4, 8, 8, 0xFF0000); /* 角标记(自绘内容) */ + if (st & MEOW_LISTVIEW_ST_SELECTED) meow_ui_fill_rect(x + w - 12, y + 4, 8, 8, 0x00FF00); +} + +int main(void) +{ + meow_ui_opts o = { 480, 720 }; + if (meow_ui_init(&o) != 0) { printf("init failed\n"); return 1; } + const meow_theme *th = meow_theme_get(); + int sb_r, sb_g, sb_b, os_r, os_g, os_b, pc_r, pc_g, pc_b, ospv_r, ospv_g, ospv_b, prim_r, prim_g, prim_b; + UNPK(th->bg, sb_r, sb_g, sb_b); + UNPK(th->onSurface, os_r, os_g, os_b); + UNPK(th->primaryContainer, pc_r, pc_g, pc_b); + UNPK(th->onSurfaceVariant, ospv_r, ospv_g, ospv_b); + UNPK(th->primary, prim_r, prim_g, prim_b); + int thh = meow_ui_text_height(th->font_body); + int itemh = 56; + + /* ============ T1 虚拟化正确性 / bind 有界 / 回滚不重绑 ============ */ + { + g_count_override = -1; g_section_at = -1; g_bind_total = 0; g_bind_touch_count = 0; + int lvx = 20, lvy = 50, lvw = 440, lvh = 300; + meow_widget *lv = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_NONE); + CHECK(meow_listview_set_item_h(lv, itemh) == itemh, "item_h"); + CHECK(meow_listview_set_vsource(lv, vcount, vbind, NULL) == 0, "vsource"); + CHECK(meow_listview_max_scroll(lv) == N * itemh - lvh, "max_scroll %d", meow_listview_max_scroll(lv)); /* 560000-300=559700 */ + CHECK(meow_listview_scroll(lv) == 0, "scroll0"); + + /* 首帧 draw:绑定视口行(first..last) */ + meow_widget_draw(lv); + int first0 = 0; + int last0 = (0 + lvh) / itemh + 1; if (last0 > N) last0 = N; + int B0 = g_bind_total; + CHECK(B0 == (last0 - first0), "first draw binds %d rows got %d", last0 - first0, B0); + + /* 下滚 1 行:仅新入视口的行被 bind(delta==1),未离屏行不重绑 */ + meow_listview_set_scroll(lv, itemh); + meow_widget_draw(lv); + int B1 = g_bind_total; + CHECK(B1 == B0 + 1, "scroll+1row binds exactly 1 more got %d (was %d)", B1 - B0, B0); + + /* 回滚到顶:视口行槽位仍在 → 不重绑(delta==0) */ + meow_listview_set_scroll(lv, 0); + meow_widget_draw(lv); + CHECK(g_bind_total == B1, "scroll-back no rebind got %d (was %d)", g_bind_total, B1); + + /* 全量扫过:每个 index 进入视口时 bind 恰一次 → 总数 == N(有界,非 per-frame × count) */ + int prev = 1; + while (prev <= N) { + int off = prev * itemh; + meow_listview_set_scroll(lv, off); + meow_widget_draw(lv); + if (off >= meow_listview_max_scroll(lv)) break; + prev++; + } + meow_listview_set_scroll(lv, meow_listview_max_scroll(lv)); + meow_widget_draw(lv); + CHECK(g_bind_total == N, "full sweep binds each index once got %d (expect %d)", g_bind_total, N); + CHECK(g_bind_touch_count == N, "bind callback called %d times (expect %d)", g_bind_touch_count, N); + + /* 回滚(扫到末尾后拉回顶):池发生整表 wrap(cap N) win = N; + meow_listview_set_scroll(lv, 0); + meow_widget_draw(lv); + CHECK((g_bind_total - before2) <= win, "scroll-back rebind bounded (delta %d ≤ %d)", + g_bind_total - before2, win); + + /* 内容正确:命中行号 = 滚动换算(视口窗口映射正确) */ + CHECK(meow_listview_hit(lv, lvx + lvw - 10, lvy + 0 * itemh + 20) == 0, "hit0"); + meow_listview_set_scroll(lv, 5 * itemh); + int idx_mid = meow_listview_hit(lv, lvx + lvw - 10, lvy + itemh / 2); + CHECK(idx_mid == 5, "hit maps scroll to row got %d (expect 5)", idx_mid); + meow_listview_set_scroll(lv, 888 * itemh); + int idx_mid2 = meow_listview_hit(lv, lvx + lvw - 10, lvy + itemh * 2 + itemh / 2); + CHECK(idx_mid2 == 890, "hit row 2 of window got %d (expect 890)", idx_mid2); + meow_widget_free(lv); + } + + /* ============ T2 多行富 item 渲染:双行 / 单行 / 图标 / 尾件 / 截断 ============ */ + { + /* 双行(lines=2,默认):行 0 title+subtitle + icon(FOLDER) + 尾件(ARROW) */ + int lvx = 20, lvy = 50, lvw = 440, lvh = 120; + meow_widget *lv = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_NONE); + CHECK(meow_listview_set_item_h(lv, itemh) == itemh, "item_h"); + CHECK(meow_listview_set_vsource(lv, vcount, vbind, NULL) == 0, "vsource"); + CHECK(meow_listview_set_item_renderer(lv, 2, 1, 1) == 0, "renderer"); + meow_ui_clear(th->bg); + meow_widget_draw(lv); + int text_x = lvx + 16 + 24 + 16; /* show_icon=1 → 76 */ + int op_start = lvx + (lvw * 2 / 3); /* 20+293=313 */ + int ty0 = lvy + (itemh - (2 * thh + 2)) / 2; /* 双行标题 y */ + int sy0 = ty0 + thh + 2; /* 副标题 y */ + CHECK(region_has(text_x, ty0, 60, thh, os_r, os_g, os_b) > 2, "double-line title drawn"); + CHECK(region_has(text_x, sy0, 60, thh, ospv_r, ospv_g, ospv_b) > 2, "double-line subtitle drawn"); + CHECK(region_has(lvx + 16, lvy + (itemh - 24) / 2, 24, 24, prim_r, prim_g, prim_b) > 30, "icon placeholder drawn"); + int ax = lvx + lvw - 16 - 24; + CHECK(region_has(ax, lvy + itemh / 2 - 8, 20, 16, ospv_r, ospv_g, ospv_b) > 2, "right tail (arrow) drawn"); + + /* 单行(lines=1):仅标题居中,无副标题带 */ + g_count_override = 3; + meow_widget *lv1 = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_NONE); + meow_listview_set_item_h(lv1, itemh); + meow_listview_set_vsource(lv1, via_count, via_bind, NULL); + meow_listview_set_item_renderer(lv1, 1, 0, 0); /* 单行、无图标、无尾件 */ + meow_ui_clear(th->bg); + meow_widget_draw(lv1); + int t1y = lvy + (itemh - thh) / 2; + CHECK(region_has(lvx + 16, t1y, 60, thh, os_r, os_g, os_b) > 2, "single-line title drawn"); + CHECK(region_has(lvx + 16, t1y + thh + 2, 60, thh, ospv_r, ospv_g, ospv_b) == 0, "single-line no subtitle"); + /* 单行档位默认行高 48(set_item_tier) */ + meow_widget *lv2 = meow_listview_create(lvx, lvy, lvw, lvh, 3, NULL, NULL, MEOW_LIST_SEL_NONE); + CHECK(meow_listview_set_item_tier(lv2, 1) == 0, "tier"); + CHECK(meow_listview_scroll(lv2) == 0 && meow_listview_max_scroll(lv2) == 3 * 48 - lvh, "tier-h %d", + meow_listview_max_scroll(lv2)); + meow_widget_free(lv2); + + /* 副标题超宽截断:不画过 text_maxw(op_start 之前) */ + g_count_override = -1; + meow_widget *lv3 = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_NONE); + meow_listview_set_item_h(lv3, itemh); + meow_listview_set_vsource(lv3, long_count, long_bind, NULL); + meow_listview_set_item_renderer(lv3, 2, 0, 0); /* 无图标 → text_x = lvx+16=36 */ + meow_ui_clear(th->bg); + meow_widget_draw(lv3); + int text_x3 = lvx + 16; + int op3 = lvx + lvw - 16; /* show_right=0 → op_start=右缘-pad */ + int maxw3 = op3 - text_x3; /* 20+440-16-36 = 408 */ + int ty3 = lvy + (itemh - (2 * thh + 2)) / 2; + int sy3 = ty3 + thh + 2; + CHECK(region_has(text_x3, sy3, 40, thh, ospv_r, ospv_g, ospv_b) > 2, "long subtitle leading drawn"); + CHECK(region_has(text_x3 + maxw3 + 4, sy3, 40, thh, ospv_r, ospv_g, ospv_b) == 0, "long subtitle truncated (no overflow)"); + meow_widget_free(lv3); + meow_widget_free(lv1); + + /* 截断不越界 + 双行副标题带(回归:无图标/无尾件时 text_x/pad 正确) */ + CHECK(region_has(text_x3, ty3, 40, thh, os_r, os_g, os_b) > 2, "truncate-r title drawn"); + meow_widget_free(lv); + } + + /* ============ T3 段头占位 + 空态 ============ */ + { + int lvx = 20, lvy = 50, lvw = 440, lvh = 120; + g_count_override = 3; g_section_at = 1; + meow_widget *lv = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_NONE); + meow_listview_set_item_h(lv, itemh); + meow_listview_set_vsource(lv, vcount, vbind, NULL); + meow_ui_clear(th->bg); + meow_widget_draw(lv); + int sch_r, sch_g, sch_b; UNPK(th->surfaceContainerHigh, sch_r, sch_g, sch_b); + CHECK(region_has(lvx, lvy + itemh, lvw, itemh - 4, sch_r, sch_g, sch_b) > 40, "section header band drawn"); + CHECK(region_has(lvx + 16, lvy + itemh + (itemh - thh) / 2, 60, thh, prim_r, prim_g, prim_b) > 2, + "section header title (primary) drawn"); + + /* 空态:count=0 + empty_text → 视口内居中画文案(onSurfaceVariant) */ + g_count_override = 0; g_section_at = -1; + meow_widget *lve = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_NONE); + meow_listview_set_vsource(lve, vcount, vbind, NULL); + meow_listview_set_empty_text(lve, "No data"); /* ASCII(宿主 5x7 点阵仅含 ASCII/CJK 用真机字库) */ + meow_ui_clear(th->bg); + meow_widget_draw(lve); + int ety = lvy + (lvh - thh) / 2; + CHECK(region_has(lvx + lvw / 2 - 60, ety, 120, thh, ospv_r, ospv_g, ospv_b) > 2, "empty state text drawn"); + meow_widget_free(lve); + meow_widget_free(lv); + } + + /* ============ T4 完全自绘(set_item_draw 覆盖默认 renderer) ============ */ + { + int lvx = 20, lvy = 200, lvw = 440, lvh = 120; + g_count_override = 3; g_section_at = -1; g_draw_calls = 0; + meow_widget *lv = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_SINGLE); + meow_listview_set_item_h(lv, itemh); + meow_listview_set_vsource(lv, vcount, vbind, NULL); + CHECK(meow_listview_set_item_draw(lv, mydraw, NULL) == 0, "set_draw"); + meow_ui_clear(th->bg); + meow_widget_draw(lv); + int vis = (lvh / itemh) + 1; if (vis > 3) vis = 3; + CHECK(g_draw_calls == vis, "draw_item called per visible row got %d (expect %d)", g_draw_calls, vis); + CHECK(region_has(lvx + 4, lvy + 4, 8, 8, 0xFF, 0x00, 0x00) > 0, "custom row content drawn"); + meow_widget_free(lv); + } + + /* ============ T5 焦点 + fling 联动(meow_scroll_wrap 包裹 pool 列表) ============ */ + { + int lvx = 20, lvy = 330, lvw = 440, lvh = 300; + g_count_override = -1; g_section_at = -1; + meow_widget *lv = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_SINGLE); + meow_listview_set_item_h(lv, itemh); + meow_listview_set_vsource(lv, vcount, vbind, NULL); + + /* 焦点:tap_at 命中行 → 列表成为唯一焦点(§6.4) */ + int tidx = meow_listview_tap_at(lv, lvx + lvw - 10, lvy + itemh / 2); + CHECK(tidx >= 0, "tap_at returns row got %d", tidx); + CHECK(meow_ui_focus_owner() == (void *)lv, "list acquired focus after tap_at"); + + /* fling:track+fling → meow_scroll_wrap 包裹;吸附到行边界(snap=itemh) */ + meow_scroll *s = meow_scroll_wrap(lv, 1); /* 1=W_LISTVIEW */ + CHECK(s != NULL, "scroll wrap pool list"); + meow_scroll_set_snap(s, itemh); + uint64_t t = meow_ui_time_ms(); + for (int i = 0; i < 12; i++) meow_scroll_track(s, 30, t + (uint64_t)i * 16); + int start = meow_scroll_offset(s); + CHECK(start > 0, "drag advances offset got %d", start); + meow_scroll_fling(s); + CHECK(meow_scroll_active(s) == 1, "fling active"); + meow_scroll_stop(s); + CHECK(meow_scroll_active(s) == 0, "stop ok"); + + /* 再次 fling 到自然停:offset 只改 viewport,且 snap 到行边界 */ + for (int i = 0; i < 12; i++) meow_scroll_track(s, 30, t + (uint64_t)i * 16); + start = meow_scroll_offset(s); + meow_scroll_fling(s); + int max_off = start, 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; + usleep(10000); + } + meow_anim_tick(); + int end = meow_scroll_offset(s); + CHECK(end >= start, "fling advances got %d vs %d", end, start); + CHECK((end % itemh) == 0, "snap to item boundary got %d (%%%d)", end, itemh); + CHECK(end <= meow_listview_max_scroll(lv), "fling in range %d (max %d)", end, meow_listview_max_scroll(lv)); + /* 滚动只改 viewport:meow_listview_scroll 与物理层一致 */ + CHECK(meow_listview_scroll(lv) == end, "listview_scroll mirrors scroll offset %d vs %d", meow_listview_scroll(lv), end); + /* 停稳后 draw 反映最终窗口(bind 只补新入视口的行,不整表重绑) */ + int before_draw = g_bind_total; + int win = (lvh / itemh) + 1; if (win > N) win = N; /* 视口窗口行数(含底缘 1 行) */ + meow_widget_draw(lv); + CHECK(g_bind_total >= before_draw && (g_bind_total - before_draw) <= win, + "settle draw rebinds only window (delta %d ≤ %d)", g_bind_total - before_draw, win); + meow_scroll_free(s); + meow_widget_free(lv); + } + + meow_ui_shutdown(); + printf("listvirt: %d pass / %d fail\n", g_pass, g_fail); + return g_fail ? 1 : 0; +} diff --git a/sdk/test_meow_ui_scroll.c b/sdk/test_meow_ui_scroll.c new file mode 100644 index 0000000..9aef33f --- /dev/null +++ b/sdk/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_ui_str.c \ + * meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \ + * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \ + * -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/sdk/test_meow_ui_str.c b/sdk/test_meow_ui_str.c new file mode 100644 index 0000000..6932331 --- /dev/null +++ b/sdk/test_meow_ui_str.c @@ -0,0 +1,174 @@ +/* + * test_meow_ui_str.c — meow_str_* 宿主单测(2026-09-02,§13.i18n 雏形)。 + * + * 编译(宿主,meow_ui_str 为纯 libc,无需 SDK 其它对象): + * gcc -Wall -Wextra -o test_meow_ui_str test_meow_ui_str.c meow_ui_str.c -I. + * + * 两种模式(覆盖表按 locale 懒加载,分两次跑): + * ./test_meow_ui_str tmp → unsetenv MEOW_DATA_DIR,仅验证内嵌双语表(无覆盖表) + * ./test_meow_ui_str env → 设 MEOW_DATA_DIR=/data,写 /strings/en_us.txt, + * 验证 TF 覆盖表优先级(覆盖>内嵌)+ locale 即时生效 + * + * 覆盖矩阵: + * T1 内嵌回退(默认 zh_cn):common.ok/loading/no_data 等取 zh + * T2 切 locale 即时生效:set_locale(en_us) → 内嵌 en;set_locale(zh_cn) → 内嵌 zh(无需重初始化) + * T3 未知 id 回退:meow_str("nope.missing") 原样返回 id;meow_str(NULL) 返回 "" + * T4 TF 覆盖优先级:override > 内嵌当前语言;未覆盖 id 回落内嵌;自定义 id 命中 + * T5 查找正确性:一批 id 的 zh/en 值逐一断言 + * T6 meow_strf:取串并按 snprintf 语义写调用方缓冲(与 meow_str 一致) + * + * 注:set_locale 的 ui.locale 持久化目标是板载 /usr/data/meow/settings.conf,宿主无该目录 + * → 静默失败(运行时仍切换,仅不跨启动);不影响本测试断言。 + */ +#include "meow_ui.h" + +#include +#include +#include +#include +#include + +static int g_fail = 0; +#define CHECK(cond, msg) do { \ + if (cond) { printf(" PASS: %s\n", msg); } \ + else { printf(" FAIL: %s\n", msg); g_fail = 1; } \ +} while (0) + +/* 找内嵌某 id 的 zh/en 期望值(供 T5 逐项对照)。不在表内返回 NULL。 */ +static const char *expect(const char *id, int en) +{ + /* 与 meow_ui_str.c 的 g_embed[] 对齐(此处只列 T5 抽查的子集) */ + static const struct { const char *id; const char *zh; const char *en; } t[] = { + { "common.ok", "确定", "OK" }, + { "common.cancel", "取消", "Cancel" }, + { "common.back", "返回", "Back" }, + { "common.confirm", "确认", "Confirm" }, + { "common.loading", "加载中...", "Loading..." }, + { "common.no_data", "暂无数据", "No data" }, + { "common.retry_fail", "重试失败", "Retry failed" }, + { "common.tap_ok_edit", "点击确定编辑", "tap OK to edit..." }, + { "ime.space", "空格", "Space" }, + { "ime.enter", "确定", "OK" }, + { "ime.esc", "取消", "Cancel" }, + { "ime.clear", "清", "Clear" }, + { "ime.pinyin_mode", "拼音", "Pinyin" }, + { "ime.no_candidate", "(无候选)", "(none)" }, + }; + for (size_t i = 0; i < sizeof t / sizeof t[0]; i++) + if (strcmp(t[i].id, id) == 0) return en ? t[i].en : t[i].zh; + return NULL; +} + +int main(int argc, char **argv) +{ + const char *mode = (argc > 1) ? argv[1] : "tmp"; + char *tmpdir = NULL; + + if (strcmp(mode, "env") == 0) { + /* 建 /data + /strings/en_us.txt(meow_str 由 MEOW_DATA_DIR 去尾 /data + * 得 /,再拼 strings/.txt)。 */ + tmpdir = strdup("/tmp/meowstrtestXXXXXX"); + if (!mkdtemp(tmpdir)) { printf("FATAL: mkdtemp failed\n"); return 2; } + char datadir[512], strdir[512]; + snprintf(datadir, sizeof datadir, "%s/data", tmpdir); + snprintf(strdir, sizeof strdir, "%s/strings", tmpdir); + mkdir(datadir, 0755); + mkdir(strdir, 0755); + char file[512]; + snprintf(file, sizeof file, "%s/strings/en_us.txt", tmpdir); + FILE *f = fopen(file, "w"); + if (!f) { printf("FATAL: cannot write override file\n"); free(tmpdir); return 2; } + /* 混合:正常行 / '=' 两侧带空格 / 空行 / 无 '=' 行(应被跳过) */ + fprintf(f, "# comment line (no equals)\n"); + fprintf(f, "common.ok=确认OK\n"); + fprintf(f, "common.custom=自定义覆盖\n"); + fprintf(f, "common.loading = 加载中覆盖\n"); + fprintf(f, "badline-without-equals\n"); + fprintf(f, "\n"); + fclose(f); + setenv("MEOW_DATA_DIR", datadir, 1); + } else { + unsetenv("MEOW_DATA_DIR"); + } + + printf("== meow_str test [%s] ==\n", mode); + printf(" locale = %s\n", meow_str_locale()); + + /* --- T1 内嵌回退(默认 zh_cn,无覆盖表/无 zh_cn.txt)--- */ + CHECK(strcmp(meow_str("common.ok"), "确定") == 0, "T1 zh common.ok = 确定"); + CHECK(strcmp(meow_str("common.cancel"), "取消") == 0, "T1 zh common.cancel = 取消"); + CHECK(strcmp(meow_str("common.loading"), "加载中...") == 0, "T1 zh common.loading = 加载中..."); + CHECK(strcmp(meow_str("common.no_data"), "暂无数据") == 0, "T1 zh common.no_data = 暂无数据"); + CHECK(strcmp(meow_str("ime.space"), "空格") == 0, "T1 zh ime.space = 空格"); + + /* --- T3 未知 id 回退(永不 NULL)--- */ + CHECK(strcmp(meow_str("nope.missing"), "nope.missing") == 0, "T3 未知 id 原样返回 id"); + CHECK(strcmp(meow_str(NULL), "") == 0, "T3 meow_str(NULL) = \"\""); + + /* --- T6 meow_strf:与 meow_str 一致,snprintf 语义 --- */ + { + char buf[128]; + int r = meow_strf(buf, sizeof buf, "common.ok"); + CHECK(r == (int)strlen("确定") && strcmp(buf, "确定") == 0, "T6 meow_strf zh common.ok = 确定"); + meow_strf(buf, sizeof buf, "common.no_data"); + CHECK(strcmp(buf, meow_str("common.no_data")) == 0, "T6 meow_strf == meow_str"); + CHECK(meow_strf(NULL, 10, "common.ok") == -1, "T6 meow_strf(out=NULL) = -1"); + CHECK(meow_strf(buf, 0, "common.ok") == -1, "T6 meow_strf(outsz=0) = -1"); + } + + /* --- T2 切 locale 即时生效(无需重初始化)--- + * 用「未在覆盖表」的 id 证明 locale 即时切换(common.ok 等在 env 会被覆盖,留给 T4 验覆盖路径)。 */ + meow_str_set_locale("en_us"); + CHECK(strcmp(meow_str_locale(), "en_us") == 0, "T2 locale = en_us"); + CHECK(strcmp(meow_str("common.cancel"), "Cancel") == 0, "T2 en common.cancel = Cancel"); + CHECK(strcmp(meow_str("common.no_data"), "No data") == 0, "T2 en common.no_data = No data"); + CHECK(strcmp(meow_str("ime.space"), "Space") == 0, "T2 en ime.space = Space"); + + /* --- T4 TF 覆盖优先级(仅 env 模式)--- */ + if (strcmp(mode, "env") == 0) { + /* ① 覆盖内嵌当前语言(en_us*下 common.ok 应是覆盖值"确认OK"而非内嵌"OK") */ + CHECK(strcmp(meow_str("common.ok"), "确认OK") == 0, "T4 @en override 覆盖 common.ok"); + /* ② 覆盖自定义 id(内嵌无 → 覆盖命中) */ + CHECK(strcmp(meow_str("common.custom"), "自定义覆盖") == 0, "T4 @en override 自定义 id"); + /* ③ '=' 两侧带空格也可解析 */ + CHECK(strcmp(meow_str("common.loading"), "加载中覆盖") == 0, "T4 @en override 带空格行"); + /* ④ 未覆盖 id 回落内嵌当前语言 */ + CHECK(strcmp(meow_str("common.cancel"), "Cancel") == 0, "T4 @en 未覆盖 → 内嵌 en"); + CHECK(strcmp(meow_str("ime.clear"), "Clear") == 0, "T4 @en ime.clear 未覆盖 → 内嵌 en"); + /* ⑤ 自定义 id 在 zh(无 zh_cn.txt,覆盖表清空)→ 内嵌无 → 原样返回 id */ + meow_str_set_locale("zh_cn"); + CHECK(strcmp(meow_str("common.ok"), "确定") == 0, "T4 @zh 覆盖清除 → 内嵌 zh"); + CHECK(strcmp(meow_str("common.custom"), "common.custom") == 0, "T4 @zh 自定义 id 原样回退"); + meow_str_set_locale("en_us"); /* 回到 en 供 T5 */ + } + + /* --- T5 查找正确性:一批 id 的 zh/en 逐一断言(仅 tmp 模式——无覆盖干扰, + * 才能验证内嵌表本身;env 模式的内嵌 vs 覆盖已由 T2/T4 覆盖,此处跳过多余断言)--- */ + if (strcmp(mode, "tmp") == 0) { + const char *ids[] = { "common.ok", "common.cancel", "common.back", "common.confirm", + "common.loading", "common.no_data", "common.retry_fail", + "common.tap_ok_edit", "ime.space", "ime.enter", "ime.esc", + "ime.clear", "ime.pinyin_mode", "ime.no_candidate" }; + meow_str_set_locale("zh_cn"); + for (size_t i = 0; i < sizeof ids / sizeof ids[0]; i++) { + if (expect(ids[i], 0)) { + char msg[128]; + snprintf(msg, sizeof msg, "T5 zh %s == 期望", ids[i]); + CHECK(strcmp(meow_str(ids[i]), expect(ids[i], 0)) == 0, msg); + } + } + meow_str_set_locale("en_us"); + for (size_t i = 0; i < sizeof ids / sizeof ids[0]; i++) { + if (expect(ids[i], 1)) { + char msg[128]; + snprintf(msg, sizeof msg, "T5 en %s == 期望", ids[i]); + CHECK(strcmp(meow_str(ids[i]), expect(ids[i], 1)) == 0, msg); + } + } + } + + if (tmpdir) free(tmpdir); + if (g_fail) { printf("\nRESULT: FAIL (%d)\n", g_fail); return 1; } + printf("\nRESULT: ALL PASS\n"); + return 0; +} diff --git a/sdk/test_meow_ui_theme.c b/sdk/test_meow_ui_theme.c index 6a11280..b9acdeb 100644 --- a/sdk/test_meow_ui_theme.c +++ b/sdk/test_meow_ui_theme.c @@ -9,7 +9,7 @@ * 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global): * gcc -o test_meow_ui_theme test_meow_ui_theme.c test_host_stub.c \ * meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ - * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c \ + * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_ui_draw.c meow_ui_view.c meow_ui_img.c meow_notify_ipc.c stb_image.c \ * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm * diff --git a/sdk/test_meow_ui_widgets.c b/sdk/test_meow_ui_widgets.c index c0f04c5..0380c0a 100644 --- a/sdk/test_meow_ui_widgets.c +++ b/sdk/test_meow_ui_widgets.c @@ -4,7 +4,7 @@ * 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global): * gcc -o test_meow_ui_widgets test_meow_ui_widgets.c test_host_stub.c \ * meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ - * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c \ + * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_ui_draw.c meow_ui_view.c meow_ui_img.c meow_notify_ipc.c stb_image.c \ * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm * 运行:MEOW_UI_DUMP=/tmp/b2.ppm ./test_meow_ui_widgets (可选,dump 屏幕) @@ -201,6 +201,75 @@ int main(void) assert(meow_topappbar_hit(tab_, 440, 28) == 1); /* 右缘动作 "Edit"(最右) */ printf("PASS topappbar: create/back/action/hit\n"); + /* ---------- TopAppBar 位图动作图标(sb_icons[],Material Symbols)---------- + * 文本项默认;set_item_icon 把动作项改为位图 → 绘制用 meow_gfx_blit_icon_ink(32px 槽 + * 居中 + ink 垂直对齐栏中点),颜色取主题 onSurface(按压向栏底色淡 12%)。断言用 + * back_buf 真实像素:画完读亮度,非空 ink + 按压变淡 + 图标项命中。 */ + { + /* 用独立 topbar(y=300,避让其它控件);meow_widget_draw 自填 surfaceContainerHigh 底 */ + meow_widget *ti = W(meow_topappbar_create(0, 300, 480, 56, "")); + int ai = meow_topappbar_add_action(ti, ""); /* 文本占位 → 转图标 */ + assert(ai == 0); + assert(meow_topappbar_set_item_icon(ti, ai, MEOW_ICON_SETTINGS) == 0); + assert(meow_topappbar_set_item_icon(ti, 99, MEOW_ICON_SETTINGS) == -1); /* 越界 */ + assert(meow_topappbar_set_item_icon(ti, ai, -1) == -1); /* 非法图标 */ + assert(meow_topappbar_set_item_icon(NULL, ai, MEOW_ICON_SETTINGS) == -1); + meow_widget_draw(ti); + /* 右起动作格(SDK 位图动作项宽 48)命中:几何中心 → 项 0(最右图标) */ + assert(meow_topappbar_hit(ti, 444, 328) == ai); + /* 图标 ink 采样:与栏底色有「绝对对比度」的像素即 ink(日/夜主题都适用—— + * 日 = 深 onSurface 画浅底、夜 = 浅 onSurface 画深底,对比度都大)。 */ + int x0 = 480 - 48, y0 = 300 + 10, x1 = 480, y1 = 300 + 46; + int bg_b, bg_g, bg_bgv; + rd_px(480 - 3, 302, &bg_b, &bg_g, &bg_bgv); /* 栏右上角 ≈ 背景 */ + int bg_lum = (bg_b + bg_g + bg_bgv) / 3; + int sum_ink = 0, cnt_ink = 0; + for (int xx = x0; xx < x1; xx++) { + for (int yy = y0; yy < y1; yy++) { + int rr, gg, bb; + rd_px(xx, yy, &rr, &gg, &bb); + int lum = (rr + gg + bb) / 3; + int d = lum - bg_lum; if (d < 0) d = -d; + if (d > 20) { sum_ink += d; cnt_ink++; } + } + } + assert(cnt_ink > 4); /* 位图 ink 非空(为抗锯齿留余量) */ + int before = sum_ink; + /* 按压态 → 图标向栏底色淡 12% → 对比度整体下降 */ + meow_widget_set_state(ti, MEOW_WS_PRESSED, 1); + meow_widget_draw(ti); + sum_ink = 0; + for (int xx = x0; xx < x1; xx++) { + for (int yy = y0; yy < y1; yy++) { + int rr, gg, bb; + rd_px(xx, yy, &rr, &gg, &bb); + int lum = (rr + gg + bb) / 3; + int d = lum - bg_lum; if (d < 0) d = -d; + if (d > 20) sum_ink += d; + } + } + assert(sum_ink < before); /* 按压变淡 */ + meow_widget_set_state(ti, MEOW_WS_PRESSED, 0); + printf("PASS topappbar: bitmap action icon draw/hit/pressed-fade (ink=%d->%d)\n", + before, sum_ink); + } + + /* ---------- 状态栏图标 blit 路径回归(meow_gfx_blit_icon 同源核心)---------- + * 证明 refactor 后状态栏路径(blit + 顶栏脏区)仍写非空像素;复用 SB_ICON_WIFI(0)。 */ + { + meow_ui_fill_rect(0, 0, 480, 28, 0x000000); /* 清顶栏区为黑底 */ + int bgr, bgg, bgb, bright = 0; + meow_gfx_blit_icon(0, 0, 0, 16, 0xFFFFFF, 14); /* idx0=WIFI 槽左缘 0 */ + for (int xx = 0; xx < 32; xx++) { + for (int yy = 4; yy < 24; yy++) { + rd_px(xx, yy, &bgr, &bgg, &bgb); + if ((bgr + bgg + bgb) / 3 > 40) bright++; + } + } + assert(bright > 4); /* 状态栏 WiFi 位图 ink 非空(黑底 + 白前景 → 亮像素) */ + printf("PASS statusbar: meow_gfx_blit_icon(0) draws non-empty ink (brt=%d)\n", bright); + } + /* ---------- NavigationBar ---------- */ meow_widget *nav = W(meow_navigationbar_create(0, 680, 480, 40)); int n0 = meow_navigationbar_add_item(nav, "Home"); diff --git a/sdk/test_notif_gesture.c b/sdk/test_notif_gesture.c index 2f7c206..4c5db97 100644 --- a/sdk/test_notif_gesture.c +++ b/sdk/test_notif_gesture.c @@ -16,7 +16,7 @@ * 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/ime/media/notify_ipc + 打桩 font/global): * gcc -Wall -Wextra -o test_notif_gesture test_notif_gesture.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_ime.c meow_ui_media.c meow_notify_ipc.c \ + * meow_ui_widget.c meow_ui_input.c meow_ui_ime.c meow_ui_str.c meow_ui_media.c meow_notify_ipc.c \ * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm * 运行:./test_notif_gesture */ diff --git a/sdk/test_overlay_suppress.c b/sdk/test_overlay_suppress.c index 4fa5893..064ebab 100644 --- a/sdk/test_overlay_suppress.c +++ b/sdk/test_overlay_suppress.c @@ -13,7 +13,7 @@ * 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global): * gcc -Wall -Wextra -o test_overlay_suppress test_overlay_suppress.c test_host_stub.c \ * meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ - * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c \ + * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_ui_draw.c meow_ui_view.c meow_notify_ipc.c \ * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread */ diff --git a/sdk/test_p1p2p3_repro.c b/sdk/test_p1p2p3_repro.c index 31692f5..038d4a7 100644 --- a/sdk/test_p1p2p3_repro.c +++ b/sdk/test_p1p2p3_repro.c @@ -23,7 +23,7 @@ * 2) 编译本文件 + 其余 hello 模块 + SDK 源 + 打桩,链接 app.o: * gcc -Wall -Wextra -DMEOW_UI_HOST_TEST -o test_p1p2p3_repro test_p1p2p3_repro.c app.o \ * test_host_stub.c meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ - * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c \ + * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_ui_img.c meow_ui_config.c meow_ui_notify.c meow_notify_ipc.c meow_ui_draw.c \ * meow_ui_view.c meow_ui_nav.c meow_ui_media.c stb_image.c \ * ../hello_app/ui/home/home.c ../hello_app/ui/full.c ../hello_app/ui/demo/demo.c \ diff --git a/sdk/test_pinyin.c b/sdk/test_pinyin.c index fd8e143..6b9a764 100644 --- a/sdk/test_pinyin.c +++ b/sdk/test_pinyin.c @@ -9,7 +9,7 @@ * 编译(宿主 gcc,同 test_ime_fix): * gcc -Wall -Wextra -o test_pinyin test_pinyin.c test_host_stub.c \ * meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ - * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c \ + * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_img.c meow_ui_media.c \ * meow_notify_ipc.c stb_image.c -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm */ diff --git a/sdk/test_sdk_fixes.c b/sdk/test_sdk_fixes.c index 3392e6f..1134668 100644 --- a/sdk/test_sdk_fixes.c +++ b/sdk/test_sdk_fixes.c @@ -9,7 +9,7 @@ * 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global): * gcc -Wall -o test_sdk_fixes test_sdk_fixes.c test_host_stub.c \ * meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ - * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c \ + * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_ui_draw.c meow_ui_view.c meow_notify_ipc.c \ * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread */ diff --git a/sdk/test_umbrella.c b/sdk/test_umbrella.c index 8e03f72..9604ce4 100644 --- a/sdk/test_umbrella.c +++ b/sdk/test_umbrella.c @@ -27,10 +27,20 @@ _Static_assert(MEOW_THEME_DARK == 1, "theme dark"); _Static_assert(MEOW_STYLE_MIUIX == 1, "style miuix"); _Static_assert(MEOW_Z_TOP == 1000, "z top"); _Static_assert(MEOW_TOPBAR_BACK == (-2), "topbar back"); +_Static_assert(MEOW_ICON_SETTINGS == 15 && MEOW_ICON_HEART == 29, "topbar icon idx 15..29"); +_Static_assert(MEOW_ICON_SKIP_PREV == 21 && MEOW_ICON_QUEUE_MUSIC == 27, "topbar icon order"); _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"); @@ -74,10 +84,19 @@ static void umbrella_smoke(void) (void)&meow_notify_send; (void)&meow_notify_category_str; (void)&meow_notify_update_progress; /* 配置(§9.2) */ (void)&meow_config_dir; (void)&meow_config_int; (void)&meow_config_remove; + /* 字符串资源/i18n(§13,追加式) */ + (void)&meow_str; (void)&meow_strf; (void)&meow_str_set_locale; (void)&meow_str_locale; /* 字体(§5.7/§10) */ (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; } /* 供宿主回归驱动直接调用;不链接本库也没关系(纯地址丢弃)。 */ diff --git a/sdk/tools/gen_action_icons.py b/sdk/tools/gen_action_icons.py new file mode 100644 index 0000000..f506bfc --- /dev/null +++ b/sdk/tools/gen_action_icons.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""gen_action_icons.py — 从 Material Symbols Rounded 官方 SVG 生成 sb_icons.h 位图动作图标。 + +背景:MeowOS SDK topbar(W_TOPAPPBAR)动作此前用字体码点(⚙↻⌕)绘制,4k 字库无这些码点→豆腐。 +正确路线 = 位图图标:与状态栏同一张 sb_icons[](Material Symbols Rounded 32x32 单通道 alpha), +动作项经 meow_topappbar_set_item_icon(w, idx, MEOW_ICON_*) 指定索引后即画位图。 + +版权/来源:图标位图源自 google/material-design-icons 的 Material Symbols Rounded(Apache-2.0)。 +本脚本只做下载(或读缓存)+ 栅格化成 32x32 单通道 alpha + 拼接 C 数组。 + +生成方式:svg(24px 网格,viewBox 0 -960 960 960)→ cairosvg 512x512 → LANCZOS 降到 32x32 +取 alpha 通道(每像素 0..255 亮度/alpha,与状态栏同源——绘制颜色由调用方主题表提供)。 +用 FILL=1(实心)以匹配现有状态栏图标的实心风格(WiFi/BT/BATT 均为实心)。 + +可复现性:SVG 缓存于本目录 icons_src/{symbol}_fill1_24px.svg(缺则联网下载,命中缓存即断网可跑); +栅格参数(512/32/LANCZOS/FILL=1)固定 → 重跑输出一致。 + +用法(在 patch_v2/sdk/ 下): + python3 tools/gen_action_icons.py # 用 venv 的 cairosvg 运行 +依赖:Python3 + Pillow + cairosvg(isolated venv:python3 -m venv && /bin/pip install cairosvg)。 +输出:重写 patch_v2/sdk/sb_icons.h——保留现有 15 个状态栏位图(0-14,原样),追加 15 个动作 +图标(15-29),索引兼容(既有索引不变,只追加)。 +""" + +import io +import os +import sys +import time +import urllib.request + +try: + import cairosvg + from PIL import Image +except Exception: # noqa + sys.exit("缺少 cairosvg / Pillow,请先安装:python3 -m venv && /bin/pip install cairosvg") + +THIS_DIR = os.path.dirname(os.path.abspath(__file__)) +SDK_DIR = os.path.abspath(os.path.join(THIS_DIR, os.pardir)) +SB_ICONS_H = os.path.join(SDK_DIR, "sb_icons.h") +SRC_DIR = os.path.join(THIS_DIR, "icons_src") + +OUT_RES = 512 # cairosvg 栅格目标边长(先大后小 → 抗锯齿 alpha) +TARGET = 32 # 最终位图边长(= SB_ICON_SIZE) +FILL = 1 # Material Symbols FILL 轴:1=实心(与状态栏一致) +BASE_URL = "https://raw.githubusercontent.com/google/material-design-icons/master/symbols/web/" +UA = {"User-Agent": "Mozilla/5.0"} + +# 现有状态栏图标 C 数组名(0-14,按 sb_icons[] 现有顺序;勿改序)。 +# 本脚本不改动它们(原样保留),仅在生成表格/枚举时按此顺序引用。 +BASE_ICONS = [ + "SB_ICON_WIFI", "SB_ICON_BT", "SB_ICON_BATT", "SB_ICON_VOL", "SB_ICON_BRIGHT", + "SB_ICON_WIFI_STRONG", "SB_ICON_WIFI_MEDIUM", "SB_ICON_WIFI_WEAK", + "SB_ICON_WIFI_NO", "SB_ICON_WIFI_OFF", "SB_ICON_BT_NO", "SB_ICON_BT_CONN", + "SB_ICON_BT_DIS", "SB_ICON_BT_SEARCH", "SB_ICON_BATT_CHG", +] + +# 动作图标:c_array_name, enum_name, material_symbols 图标名。索引 15-29,勿改序。 +ACTIONS = [ + ("SB_ICON_SETTINGS", "SB_I_SETTINGS", "settings"), + ("SB_ICON_REFRESH", "SB_I_REFRESH", "refresh"), + ("SB_ICON_SEARCH", "SB_I_SEARCH", "search"), + ("SB_ICON_PLAY_ARROW", "SB_I_PLAY_ARROW", "play_arrow"), + ("SB_ICON_PAUSE", "SB_I_PAUSE", "pause"), + ("SB_ICON_SKIP_NEXT", "SB_I_SKIP_NEXT", "skip_next"), + ("SB_ICON_SKIP_PREV", "SB_I_SKIP_PREV", "skip_previous"), + ("SB_ICON_ARROW_BACK", "SB_I_ARROW_BACK", "arrow_back"), + ("SB_ICON_CLOSE", "SB_I_CLOSE", "close"), + ("SB_ICON_ADD", "SB_I_ADD", "add"), + ("SB_ICON_CHECK", "SB_I_CHECK", "check"), + ("SB_ICON_LYRICS", "SB_I_LYRICS", "lyrics"), + ("SB_ICON_QUEUE_MUSIC", "SB_I_QUEUE_MUSIC", "queue_music"), + ("SB_ICON_DOWNLOAD", "SB_I_DOWNLOAD", "download"), + ("SB_ICON_HEART", "SB_I_HEART", "favorite"), # Material Symbols 用 "favorite" 表心形 +] + +HEADER_COMMENT = """/* sb_icons.h — 状态栏 + 动作位图图标(Material Symbols Rounded, MD3),单通道 alpha 位图 32x32。 + * 生成自 material-symbols rounded(google/material-design-icons,Apache-2.0)SVG 栅格化: + * 图标族 = FILL=1 实心(与状态栏 WiFi/BT/BATT 现有实心风格一致);每像素 0..255(亮度/alpha)。 + * ★ 为日/夜间预留:位图只存亮度;绘制颜色由调用方主题表提供,换主题仅改配色。 + * 索引布局(兼容):状态栏 SB_I_WIFI=0..SB_I_BATT_CHG=14(原样保留);TopAppBar 动作 + * MEOW_ICON_SETTINGS=15..MEOW_ICON_HEART=29(只追加,既有索引不变)。动作图标为 15-29, + * 经 meow_topappbar_set_item_icon(w, idx, MEOW_ICON_*) 使 W_TOPAPPBAR 动作项画位图。 + * 重新生成:python3 tools/gen_action_icons.py(SVG 缓存 tools/icons_src/,可复现)。 */ +#ifndef SB_ICONS_H +#define SB_ICONS_H + +#define SB_ICON_SIZE 32 + +typedef struct { const unsigned char *data; } sb_icon_t; + +""" + +ACTION_BLOCK_MARKER = "/* ===== TopAppBar \u52a8\u4f5c\u56fe\u6807" # "/* ===== TopAppBar 动作图标" + + +def fetch(url, path, retries=6): + """带重试的下载(网络不稳):命中缓存直接返回,失败退避重试。""" + if os.path.exists(path) and os.path.getsize(path) > 200: + return open(path, "rb").read() + for k in range(retries): + try: + req = urllib.request.Request(url, headers=UA) + data = urllib.request.urlopen(req, timeout=25).read() + if len(data) < 200: + raise ValueError("too small %d" % len(data)) + open(path, "wb").write(data) + return data + except Exception as e: # noqa + sys.stderr.write(" retry %d %s: %s\n" % (k, path, repr(e)[:80])) + time.sleep(1.5 * (k + 1)) + raise RuntimeError("download failed: " + url) + + +def svg_path_for(symbol): + return os.path.join(SRC_DIR, symbol + "_fill%d_24px.svg" % FILL) + + +def ensure_svg(symbol): + path = svg_path_for(symbol) + if not (os.path.exists(path) and os.path.getsize(path) > 200): + url = BASE_URL + symbol + "/materialsymbolsrounded/" + symbol + "_fill%d_24px.svg" % FILL + fetch(url, path) + return path + + +def rasterize_alpha32(symbol): + """SVG → 32x32 单通道 alpha(0..255)。512 渲大图再 LANCZOS 降采样,得到平滑边缘。""" + svg = open(ensure_svg(symbol), "rb").read() + png = cairosvg.svg2png(bytestring=svg, output_width=OUT_RES, output_height=OUT_RES) + alpha = Image.open(io.BytesIO(png)).convert("RGBA").getchannel("A") + return alpha.resize((TARGET, TARGET), Image.LANCZOS) + + +def fmt_alpha_array(name, alpha): + """把 32x32 alpha 排成 C 数组(每行 32 个像素,便于目检)。""" + lines = ["static const unsigned char %s[%d]={" % (name, TARGET * TARGET)] + for y in range(TARGET): + row = ",".join(str(alpha.getpixel((x, y))) for x in range(TARGET)) + lines.append(row + ",") + lines.append("};") + return "\n".join(lines) + + +def build_action_block(): + outs = [ACTION_BLOCK_MARKER + "(Material Symbols Rounded, MD3, Apache-2.0)====\n"] + outs.append(" * 复用状态栏同源 32x32 单通道 alpha;绘制颜色/按压态由 meow_ui_widget.c 主题表提供。 */\n") + for cname, _enum, symbol in ACTIONS: + a = rasterize_alpha32(symbol) + outs.append(fmt_alpha_array(cname, a)) + return "\n".join(outs) + + +def build_table_and_enum(): + table = ["static const sb_icon_t sb_icons[] = {"] + for name in BASE_ICONS: + table.append(" { %s }," % name) + for cname, _enum, _symbol in ACTIONS: + table.append(" { %s }," % cname) + table.append("};") + table = "\n".join(table) + + enum = ["/* 枚举索引(与 sb_icons[] 对应)。状态栏 0-14(勿改序);动作 15-29(TopAppBar 用)。 */"] + enum.append("enum { SB_I_WIFI=0, SB_I_BT=1, SB_I_BATT=2, SB_I_VOL=3, SB_I_BRIGHT=4,") + enum.append(" SB_I_WIFI_STRONG=5, SB_I_WIFI_MEDIUM=6, SB_I_WIFI_WEAK=7,") + enum.append(" SB_I_WIFI_NO=8, SB_I_WIFI_OFF=9, SB_I_BT_NO=10, SB_I_BT_CONN=11,") + enum.append(" SB_I_BT_DIS=12, SB_I_BT_SEARCH=13, SB_I_BATT_CHG=14,") + # 动作枚举从 15 起,与 meow_ui_widgets.h 的 MEOW_ICON_* 同值(勿改序) + for i, (_c, e, _s) in enumerate(ACTIONS): + last = (i == len(ACTIONS) - 1) + enum.append(" %s=%d%s" % (e, 15 + i, " }; " if last else ",")) + return table + "\n\n" + "\n".join(enum) + "\n\n#endif /* SB_ICONS_H */\n" + + +def regenerate_sb_icons_h(): + old = open(SB_ICONS_H, encoding="utf-8").read() + # body = 现有状态栏数组(原样保留)。HEADER_COMMENT 已提供 ifndef/define/size/typedef, + # 故从首个状态栏数组起切;终点:优先动作块标记(幂等重跑),缺失则 sb_icons[] 表格(首跑)。 + body_start = old.find("static const unsigned char " + BASE_ICONS[0] + "[1024]") + if body_start < 0: + sys.exit("sb_icons.h 缺少首个状态栏数组,拒绝重写") + pr = old.find(ACTION_BLOCK_MARKER) + if pr < 0: + pr = old.find("static const sb_icon_t sb_icons[]") + if pr < 0 or pr <= body_start: + sys.exit("sb_icons.h 缺少 sb_icons[] 表格,拒绝重写") + body = old[body_start:pr] + + out = HEADER_COMMENT + body.rstrip("\n") + "\n\n" + out += build_action_block() + "\n\n" + out += build_table_and_enum() + open(SB_ICONS_H, "w", encoding="utf-8").write(out) + print("wrote %s (%d bytes)" % (SB_ICONS_H, len(out))) + + +def main(): + os.makedirs(SRC_DIR, exist_ok=True) + regenerate_sb_icons_h() + + +if __name__ == "__main__": + main() diff --git a/sdk/tools/icons_src/add_fill1_24px.svg b/sdk/tools/icons_src/add_fill1_24px.svg new file mode 100644 index 0000000..1cc5bf2 --- /dev/null +++ b/sdk/tools/icons_src/add_fill1_24px.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sdk/tools/icons_src/arrow_back_fill1_24px.svg b/sdk/tools/icons_src/arrow_back_fill1_24px.svg new file mode 100644 index 0000000..c914644 --- /dev/null +++ b/sdk/tools/icons_src/arrow_back_fill1_24px.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sdk/tools/icons_src/check_fill1_24px.svg b/sdk/tools/icons_src/check_fill1_24px.svg new file mode 100644 index 0000000..f23d34a --- /dev/null +++ b/sdk/tools/icons_src/check_fill1_24px.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sdk/tools/icons_src/close_fill1_24px.svg b/sdk/tools/icons_src/close_fill1_24px.svg new file mode 100644 index 0000000..3956434 --- /dev/null +++ b/sdk/tools/icons_src/close_fill1_24px.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sdk/tools/icons_src/download_fill1_24px.svg b/sdk/tools/icons_src/download_fill1_24px.svg new file mode 100644 index 0000000..d440715 --- /dev/null +++ b/sdk/tools/icons_src/download_fill1_24px.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sdk/tools/icons_src/favorite_fill1_24px.svg b/sdk/tools/icons_src/favorite_fill1_24px.svg new file mode 100644 index 0000000..725ed6d --- /dev/null +++ b/sdk/tools/icons_src/favorite_fill1_24px.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sdk/tools/icons_src/lyrics_fill1_24px.svg b/sdk/tools/icons_src/lyrics_fill1_24px.svg new file mode 100644 index 0000000..e5a3960 --- /dev/null +++ b/sdk/tools/icons_src/lyrics_fill1_24px.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sdk/tools/icons_src/pause_fill1_24px.svg b/sdk/tools/icons_src/pause_fill1_24px.svg new file mode 100644 index 0000000..8b7e21c --- /dev/null +++ b/sdk/tools/icons_src/pause_fill1_24px.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sdk/tools/icons_src/play_arrow_fill1_24px.svg b/sdk/tools/icons_src/play_arrow_fill1_24px.svg new file mode 100644 index 0000000..35b60fd --- /dev/null +++ b/sdk/tools/icons_src/play_arrow_fill1_24px.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sdk/tools/icons_src/queue_music_fill1_24px.svg b/sdk/tools/icons_src/queue_music_fill1_24px.svg new file mode 100644 index 0000000..58b49e7 --- /dev/null +++ b/sdk/tools/icons_src/queue_music_fill1_24px.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sdk/tools/icons_src/refresh_fill1_24px.svg b/sdk/tools/icons_src/refresh_fill1_24px.svg new file mode 100644 index 0000000..cedb0fd --- /dev/null +++ b/sdk/tools/icons_src/refresh_fill1_24px.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sdk/tools/icons_src/search_fill1_24px.svg b/sdk/tools/icons_src/search_fill1_24px.svg new file mode 100644 index 0000000..f38d2aa --- /dev/null +++ b/sdk/tools/icons_src/search_fill1_24px.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sdk/tools/icons_src/settings_fill1_24px.svg b/sdk/tools/icons_src/settings_fill1_24px.svg new file mode 100644 index 0000000..fd0d008 --- /dev/null +++ b/sdk/tools/icons_src/settings_fill1_24px.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sdk/tools/icons_src/skip_next_fill1_24px.svg b/sdk/tools/icons_src/skip_next_fill1_24px.svg new file mode 100644 index 0000000..7a789f3 --- /dev/null +++ b/sdk/tools/icons_src/skip_next_fill1_24px.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sdk/tools/icons_src/skip_previous_fill1_24px.svg b/sdk/tools/icons_src/skip_previous_fill1_24px.svg new file mode 100644 index 0000000..33655ea --- /dev/null +++ b/sdk/tools/icons_src/skip_previous_fill1_24px.svg @@ -0,0 +1 @@ + \ No newline at end of file