Compare commits
72
Commits
35a623dfb6
...
picbrowser
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55a4a42488 | ||
|
|
c5985b2c67 | ||
|
|
f2c489d89b | ||
|
|
db92cbd95a | ||
|
|
f647a3c498 | ||
|
|
7036020005 | ||
|
|
bbc73635a6 | ||
|
|
eb0750051f | ||
|
|
e896a28fba | ||
|
|
62826e1d13 | ||
|
|
ce34974727 | ||
|
|
ffc41bed9c | ||
|
|
87f4ad0a82 | ||
|
|
606a04a44e | ||
|
|
2af7095d35 | ||
|
|
11e09af7a4 | ||
|
|
1725af631b | ||
|
|
668629bda3 | ||
|
|
6c8a1d3925 | ||
|
|
11a4fcd730 | ||
|
|
e9c0749609 | ||
|
|
d84dd949c7 | ||
|
|
88f3d861b0 | ||
|
|
40751c09bd | ||
|
|
773ab9ff92 | ||
|
|
a07f9752d3 | ||
|
|
4773ec1a61 | ||
|
|
4befbfe14c | ||
|
|
64336f95d9 | ||
|
|
d07b99056f | ||
|
|
c85877b447 | ||
|
|
782a5f7c90 | ||
|
|
ceb65dfd01 | ||
|
|
462cfd5bc0 | ||
|
|
73a9603ef0 | ||
|
|
94eeabd789 | ||
|
|
e6a9e53ae9 | ||
|
|
3cbcaaaac1 | ||
|
|
5ac227b225 | ||
|
|
66e598cb62 | ||
|
|
9f34417c49 | ||
|
|
c9cff99af3 | ||
|
|
c775f8558c | ||
|
|
562cc44931 | ||
|
|
19620a7957 | ||
|
|
fbd4db3aec | ||
|
|
6caf2bc6e6 | ||
|
|
ff97f3cee7 | ||
|
|
8cbb240931 | ||
|
|
cca3906c80 | ||
|
|
47325f2b81 | ||
|
|
21bffa1aeb | ||
|
|
5f40d6fd87 | ||
|
|
23c9db73df | ||
|
|
28b67a7b8d | ||
|
|
432875d4e6 | ||
|
|
2c60de141f | ||
|
|
429535e4d9 | ||
|
|
f4c21c74a1 | ||
|
|
bed6a95485 | ||
|
|
c0396e40ce | ||
|
|
a1687486bc | ||
|
|
a70cdbc0f7 | ||
|
|
64cde13c8d | ||
|
|
0af6af7c7e | ||
|
|
4b094d62a5 | ||
|
|
66e9cd5fa4 | ||
|
|
f44d702124 | ||
|
|
2e8fac2a07 | ||
|
|
2d974bed62 | ||
|
|
63ac0fd7dc | ||
|
|
962888b96f |
No files matched your search
+31
@@ -4,7 +4,26 @@
|
||||
|
||||
# ---- 宿主测试二进制(SDK test_*.c 编译出的可执行;源码 .c 仍跟踪)----
|
||||
test_ime_fix
|
||||
test_meow_cachegov
|
||||
test_meow_img_color
|
||||
test_meow_lyric
|
||||
test_meow_lyric_view
|
||||
test_meow_md3
|
||||
test_meow_ui_anim
|
||||
test_meow_ui_home_gesture
|
||||
test_meow_ui_iconbtn
|
||||
test_meow_ui_lazygrid
|
||||
test_meow_ui_select
|
||||
test_meow_ui_swipe
|
||||
test_meow_ui_theme_export
|
||||
test_meow_ui_view_event
|
||||
test_net_ipc
|
||||
test_meow_ui_box
|
||||
test_meow_ui_config
|
||||
test_meow_ui_dispatch
|
||||
test_meow_ui_listvirt
|
||||
test_meow_ui_scroll
|
||||
test_meow_ui_str
|
||||
test_meow_ui_theme
|
||||
test_meow_ui_vol
|
||||
test_meow_ui_widgets
|
||||
@@ -25,6 +44,18 @@ test_umbrella
|
||||
examples/hello/hello
|
||||
examples/hello/render_test*.png
|
||||
|
||||
# ---- 图库产物 ----
|
||||
tuku/picbrowser
|
||||
tuku/*.mpk
|
||||
tuku/build/
|
||||
tuku/test/test_library
|
||||
tuku/test/test_reader_core
|
||||
tuku/test/test_settings_reader_persist
|
||||
tuku/test/test_settings_lock_host
|
||||
tuku/test/test_pin
|
||||
tuku/test/test_zip
|
||||
tuku/test/test_cbz
|
||||
|
||||
# ---- 杂项 ----
|
||||
*.orig
|
||||
*.log
|
||||
|
||||
@@ -22,7 +22,7 @@ MeowOS 应用框架 SDK 的**公开仓库**——面向第三方开发者,让
|
||||
MeowOS-SDK/
|
||||
├── sdk/ # meow_ui 应用框架静态库源码(自包含,可交叉 → sdk/meow_ui.a)
|
||||
│ ├── meow_ui.h # 伞头(#include 聚合 include/ 子头;App 只 include 这个)
|
||||
│ ├── include/ # 16 个模块子头 + meow_ipc.h / meow_font.h / meow_notif.h(协议/字体)
|
||||
│ ├── include/ # 24 个模块子头(含 box/anim/scroll/str/lazygrid/select/cachegov/net 通道等)+ meow_ipc.h / meow_font.h / meow_notif.h(vendored)
|
||||
│ ├── meow_font.c # 预渲染位图字体运行时(meow_font.a 源码,自包含)
|
||||
│ ├── build_sdk.sh # 交叉编译 meow_ui.a(mipsel 静态库,内含 meow_font.o)
|
||||
│ └── test_*.c # SDK 宿主逻辑测试(test_host_stub.c 打桩,无硬件可跑)
|
||||
@@ -31,6 +31,12 @@ MeowOS-SDK/
|
||||
│ ├── ui/ # 每页一个 view 文件(home/demo/list/full/chrome)
|
||||
│ ├── logic/ # 纯 C 业务逻辑(不依赖 meow_ui.h,宿主可裸测)
|
||||
│ └── build_hello.sh # 构建 hello(链 meow_ui.a + libz.a + -lpthread)
|
||||
├── tuku/ # 图库:漫画书库 App(M3 骨架:主题同步/目录浏览/阅读器桩)
|
||||
│ ├── app.c # main + 装配 + meow_ui_run 回调路由 + 主题监听
|
||||
│ ├── logic/ # 纯 C:目录扫描/自然排序、阅读方向状态(宿主可裸测)
|
||||
│ ├── ui/ # library / reader / settings / lock 每页一个 view
|
||||
│ ├── build_tuku.sh # 交叉构建(SDK 用 ${MEOW_SDK_DIR:-$HOME/Proj/MeowOS-SDK/sdk})
|
||||
│ └── pack_tuku.sh # 出包 os.meow.picbrowser.mpk
|
||||
└── docs/
|
||||
├── APP_DEV_GUIDE.md # 第三方开发上手指南(环境 → 最小 App → 构建打包 → 签名安装 → 调试)
|
||||
└── MEOW_APP_SDK.md # SDK 设计全量规范
|
||||
@@ -46,8 +52,36 @@ cd sdk
|
||||
# 2) 构建示例 App hello
|
||||
cd ../examples/hello
|
||||
./build_hello.sh # 产出 examples/hello/hello(静态 MIPS32r2 ELF)
|
||||
|
||||
# 3) 构建图库(SDK 用绝对路径,可用 MEOW_SDK_DIR 覆盖)
|
||||
cd ../../tuku
|
||||
./build_tuku.sh # 产出 tuku/tuku(静态 MIPS32r2 ELF)
|
||||
./pack_tuku.sh # 产出 tuku/os.meow.picbrowser.mpk
|
||||
```
|
||||
|
||||
## SDK 能力速览
|
||||
|
||||
统一控件 / 输入 / 主题 / 字体 / 通知 / 导航之外,本版 SDK 还提供(详见 [docs/MEOW_APP_SDK.md](docs/MEOW_APP_SDK.md)):
|
||||
|
||||
- **布局引擎 `meow_box`**(§12.1):线性布局纯计算器——固定/权重/wrap 尺寸、间隙/边距、主轴与交叉轴对齐、嵌套子 box;`meow_view_set_layout` 与视图框架 opt-in 集成。
|
||||
- **动画基座 `meow_anim` / `meow_anim_tick`**(§12.2):声明式 tween(linear/ease-out/ease-in-out/overshoot),静态池 ≤16,无动画零开销;只 flush 动画影响区。
|
||||
- **滚动物理 `meow_scroll`**(§12.3):速度采样 + 松手 fling + 边界 clamp/简单回弹 + snap 吸附;给 `meow_list`/`W_LIST`/`W_LISTVIEW` 加惯性滚动。
|
||||
- **事件分发形式化**(§6.4):单一仲裁者——z 序自顶向下命中、命中即消费、down 捕获整个手势、模态层隔离、唯一焦点(点外释放/滚动容器转移)。
|
||||
- **视图事件路由**(§A.7,M7.0a):`meow_view_dispatch_event` 接入 `meow_ui_run`——TAP/KEY/SWIPE/LONG_PRESS 先走视图树 `on_event` 路由,命中即消费(不再落 App `on_*`);无视图树/未命中回落 App 原生 `on_*` 回调(完全向后兼容)。页内子导航按设计定案 b 标注**废弃**(不实现),页内「子页」用 `meow_view_add_child` 挂 CONTAINER 子视图(组合嵌套)。
|
||||
- **ListView 虚拟化**(§5.5):固定 item 池(视口+2)、多行富 item、段头、空态、完全自绘钩子;App 只提供「总数 + bind」回调,几千/上万行不卡。
|
||||
- **ListView 左滑删除**(§5.5):列表行横向手势 `meow_listview_swipe_begin/move/end` 三级喂入 + 删除/回弹动画;`meow_listview_set_swipe_delete` 绑删除回调,行自绘 `set_item_draw` 配合画右露删除区(主题 `error/onError`)。
|
||||
- **圆形图标按钮 `meow_iconbtn_create`**(M7.0a):圆形命中 + 位图图标(`sb_icons[]`/`MEOW_ICON_*`)+ 实时按压/按住态;供电源菜单/播放控制/锁屏件用。
|
||||
- **主题色板导出/注入**(§8.1.5,M7.0a):`meow_theme_export`/`meow_theme_inject`——「单一权威桥」,导出当前 `style×mode` 解析生效的权威主题(9 角色字段),或注入一份自定义板覆写当前生效主题(`NULL` 恢复内建,覆写随 `meow_theme_reload()` 保留);供 stateless 绘制原语读单一权威、或 launcher/App 自定义背景/强调/风格。
|
||||
- **字符串资源 / i18n**(§13):`meow_str`/`meow_strf`/`meow_str_set_locale`——内嵌 `zh_cn`/`en_us` 双语表 + TF 卡覆盖表;UI 文案零硬编码,按 locale 取整串。
|
||||
- **图标库 `sb_icons`**(§5.8):30 个 MD3 Material Symbols 位图动作图标(`MEOW_ICON_*`),`meow_topappbar_set_item_icon` 把 TopAppBar 动作项换成位图图标(修复 ⚙↻⌕ 豆腐)。
|
||||
- **图片降采样 `meow_img_downscale`**(`include/meow_ui_image.h`):把解码图降到 ≤maxdim 边长省内存(封面/大图用,如 8.28MB→0.78MB)。
|
||||
- **2 维虚拟化宫格 `meow_lazygrid`**(§12.4):卡片/图标宫格二维虚拟化——只实例化可见行(固定 item 池 ≤96、视口行数+2×cols),`count+bind` 数据源、长列表不卡;命中 `meow_lazygrid_hit/tap_at` 把滚动+网格坐标映射回 item idx;滚动复用 `meow_scroll`(fling/snap)。
|
||||
- **SDK 默认列表/容器控件默认 MD3**(M6.37–M6.39):`meow_list_create` / `meow_lazycolumn_create` / `meow_lazyrow_create` 与 `meow_listview` **默认即容器模式**——每项 `surfaceContainer` 圆角卡 + 项间空隙 + 去分隔线 + 卡内 tonal 选中/按压,用默认 renderer 的 App 开箱即 MD3;需扁平/满幅分隔线可用 `meow_list_set_container(w,0)` **回退**。
|
||||
- **全局 App 内底部上滑回桌面**(`meow_ui_input_is_home_swipe`,系统保留手势):任意 App 内屏底边缘上滑即回桌面(后台保活、进程保留转后台、present 自抑),命中优先于 App `on_swipe`/视图树——App **勿在 `on_swipe` 拦截上滑**,否则不生效;中部上滑/滚动(LazyColumn/LazyGrid/页面滚动)不受影响,仍交 App。
|
||||
- **枚举/预设选择器 `meow_select`**(§5.8.6):开箱即用下拉模态——BottomSheet(标题 + `meow_listview` SINGLE 单选列表)+ `meow_ui_modal_capture/restore` 背景快照(关闭无残影);点选项=选中+`on_change` 回调,点面板外/左缘=取消。
|
||||
- **磁盘页缓存治理 `meow_cachegov`**(OOM_PAGECACHE_GOVERNANCE):App 登记缓存文件 + read_pos 回调,SDK 做 DONTNEED 治理(L0 曲终整文件丢 / L1 播放中丢已读前缀保 keep_hot 窗口);refault 滚窗逐级回退(放大 keep_hot→降 L0→关 A);`meow_cachegov_download/prefetch_paused` 提供给下载/预取方降载查询。
|
||||
- **网络自愿通道 `meow_ui_net`**(netmgrd 期 4):联网前 `meow_net_policy_check(host)` 自查是否被策略放行(netmgrd 不在/超时→乐观放行);`meow_net_report(rx,tx)` 自愿上报本进程收发字节(best-effort,失败静默)。
|
||||
|
||||
## 构建要求
|
||||
|
||||
- **交叉工具链**(mipsel musl 静态):MeowOS 开发机默认路径
|
||||
|
||||
+10
-1
@@ -59,7 +59,7 @@ MEOW_SYSROOT="$HOME/Project/R3ProII/.tools/mipsel-linux-muslsf-cross/mipsel-linu
|
||||
SDK 依赖板载 `/dev/fb0` 与字库,宿主机没有——但你可以**链真实 SDK 目标文件 + 打桩**来跑逻辑单测(参考 `sdk/test_host_stub.c`):
|
||||
|
||||
- `test_host_stub.c` 提供了 `meow_font_*` / `meow_ui_global_render` 等宿主桩(无 fb → 5×7 点阵回退;无字库 → `meow_font_load` 返回 -1)。
|
||||
- 宿主可 `gcc` 直接编译 SDK 各 `.c` + 你的逻辑,跑纯 C 断言,**不碰硬件**(`hello` 的 `test_p1p2p3_repro` / `test_meow_ui_widgets` / `test_pinyin` 等都是这么跑的,宿主宿主验证全 PASS)。
|
||||
- 宿主可 `gcc` 直接编译 SDK 各 `.c` + 你的逻辑,跑纯 C 断言,**不碰硬件**(`hello` 的 `test_p1p2p3_repro` / `test_meow_ui_widgets` / `test_pinyin` 等都是这么跑的)。注意:公开仓 38 个 `sdk/test_*.c` 的头注释已按实测修订——按注释补齐 `-Iinclude`、`meow_ui_nav.c` 等源集后 **34/38 全部 PASS**;`test_notify_post` / `test_ui_audio_host` / `test_p1p2p3_repro` 依赖主仓组件(launcher/audiod/hello_app),公开仓跳过;`test_net_ipc` 需运行中的 netmgrd,属手工验证项。宿主 `/tmp` 需可写(Termux 本机可用 `proot -b <scratch>:/tmp` 提供)。
|
||||
- 注意:`logic/` 层要写**纯 C**(不依赖 `meow_ui.h`),才能裸宿主单测;`ui/` 层才碰 SDK。
|
||||
|
||||
### 1.3 qemu-mipsel 全系统仿真
|
||||
@@ -424,6 +424,15 @@ am resolve <pkg> <activity> # 路由决策(返回 CHAIN)
|
||||
- App 一般不需要联网;确需联网时**克制请求**(见 §8 风控式节制)。
|
||||
- 系统层面(`netmgrd` 期 0/1):无 iptables 用户态 / 无 cgroup / 无 conntrack → **内核级每应用管控不可行**;主杠杆 = DNS 代理(域名级)+ 连接普查(inet_diag)+ **SDK 自愿通道**。App 走 SDK 自愿通道配合管控是后续方向;当前 App 如联网,请自律、尊重断网降级。
|
||||
|
||||
### 7.11 字符串资源 / i18n(`meow_str_*`,2026-09-02 追加)
|
||||
|
||||
- **任何 UI 文案都不要硬编码**,用 `meow_str("域.名")` 按当前语言取串(示例:`meow_str("common.ok")`)。`meow_str` 永不返回 `NULL`:未知 id 原样返回 `id`,方便你打点/兜底展示。
|
||||
- **`meow_strf(out, outsz, id, ...)`**:取串后按 `snprintf` 语义格式化进调用方缓冲(自带 NUL;`out==NULL`/`outsz<=0` 返回 `-1`)。
|
||||
- **切换语言**:`meow_str_set_locale("zh_cn"|"en_us")` 即时生效;locale 存系统设置 `settings.conf` 的 `ui.locale`(默认 `zh_cn`,SDK 启动惰性读取)。App 里做语言切换页时调用它即可。
|
||||
- **覆盖/追加**:可不改二进制在 TF 卡放 `MeowData/<pkg>/strings/<locale>.txt`(每行 `id=text`,UTF-8)覆盖内嵌或新增自己的串(如 `player.pause=暂停`);无卡/无文件/无该 id → 回落内嵌双语默认表(`common.*` + `ime.*` 内置,见 `include/meow_ui_str.h`)。
|
||||
- **id 规范**:`域.名`;SDK 自带 `common.*`/`ime.*`,你自建 `域`(如 `player.*`)避免撞名。
|
||||
- **边界**:当前为 i18n 雏形,仅 `zh_cn`/`en_us` 双语**整串**取用;**不做复数规则 / BiDi / RTL / 动态字体回退**(详见 `MEOW_APP_SDK.md §13.7`)。
|
||||
|
||||
---
|
||||
|
||||
## 8. 规范与红线
|
||||
|
||||
+405
-2
@@ -30,6 +30,7 @@
|
||||
| `meow_ui_config.h` | 应用配置读写 | §9.2 |
|
||||
| `meow_ui_font.h` | 字体 | §5.7 卡片字体列表 + §10 字体分层 |
|
||||
| `meow_ui_audio.h` | 独占音频客户端 | §11 |
|
||||
| `meow_ui_str.h` | 字符串资源/i18n | §13 `meow_str`/`meow_strf`/`meow_str_set_locale` |
|
||||
|
||||
其余按需显式 include、**不**随伞头发布的模块头(原样保留、本次未改):`meow_ui_view.h` / `meow_ui_nav.h` / `meow_ui_media.h` / `meow_ui_sys.h` / `meow_ui_vol.h`。`meow_ui_internal.h` 为 SDK 内部件,不随伞头发布。
|
||||
|
||||
@@ -373,7 +374,7 @@ int meow_ui_setform_hit_row(meow_widget *f, int x, int y);/* 命中行号(-1=
|
||||
|
||||
### 5.5 设计模板 A② 列表+选择 [源:2026-08-30]
|
||||
|
||||
> **性质**:SDK 设计模板,供 App 主页/子页按需选用;对既有 `meow_list_create`(B1 最小集,§5,仅 label 文本)做**结构化扩展**(行=图标+标题+副标题+右侧操作 + 选中态)。API「草案/待定案」,不实现。**(✅ 已实现:结构化列表控件 `W_LISTVIEW`,M2.76 · agent-186 · 243148e,见 §5.8.8 分批;长按事件 `MEOW_EV_LONG_PRESS` + `meow_ui_set_long_press_cb/ms`,M2.77 · agent-191 · e95552b。)**
|
||||
> **性质**:SDK 设计模板,供 App 主页/子页按需选用;对既有 `meow_list_create`(B1 最小集,§5,仅 label 文本)做**结构化扩展**(行=图标+标题+副标题+右侧操作 + 选中态)。API「草案/待定案」,不实现。**(✅ 已实现:结构化列表控件 `W_LISTVIEW`,M2.76 · agent-186 · 243148e,见 §5.8.8 分批;长按事件 `MEOW_EV_LONG_PRESS` + `meow_ui_set_long_press_cb/ms`,M2.77 · agent-191 · e95552b;虚拟化层(固定 item 池 + 多行富 item + 完全自绘 + 段头 + 空态),2026-09-02,见 §5.5「虚拟化层」。)**
|
||||
|
||||
**摘要**:可滚动选择容器。**行数据源回调**逐行给出(图标 / 标题 / 副标题 / 右侧操作 / 是否可选),容器负责绘制/滚动/选中/命中;支持**单选、多选、长按**(上下文操作)。适合「已保存网络 / 附近WiFi / 蓝牙设备 / 歌曲列表 / 设置子菜单」等。
|
||||
|
||||
@@ -420,6 +421,51 @@ int meow_ui_listview_hit(meow_widget *lv, int x, int y); /* 命中行;
|
||||
```
|
||||
> 说明:既有 `meow_list_create`(label-only)保留;`meow_ui_listview_*` 为**结构化扩展**(图标/副标题/右侧/选中),可作为真则替换裸 label 列表。
|
||||
|
||||
#### 虚拟化层(2026-09-02,W_LISTVIEW 扩展;API 追加式,旧 API 一字不改)
|
||||
|
||||
> **痛点**:W_LISTVIEW(src2 每帧按需取行)只画可见行,但**每帧重取全部可见行**;大列表(几千/
|
||||
> 上万首)App 只能自行造「滑动窗口」补载(如超长歌单的滑动窗口补载)。
|
||||
> 本层把「固定 item 池 + 多行富 item + 完全自绘」收编为 SDK 级能力,让 App 只提供「总数 + bind」回调。
|
||||
|
||||
**关键交互/数据模型**:
|
||||
- **数据源**:`meow_listview_count_fn`(返回总行数,任意长 ≥65535,不常驻 item 对象)+ `meow_listview_bind_fn`
|
||||
(`index, meow_lv_item*, ctx`,仅在行「进入视口」(绑定到固定池槽)时被调**一次**)。`count` 每帧实时查
|
||||
(搜索过滤/懒加载动态长度)。
|
||||
- **固定 item 池**:池容量 = 视口行数 + 2 静态槽(有界,`MEOW_LV_POOL_MAX`=96),行离开视口不销毁、
|
||||
**回滚不重绑**(同一 index 命中同一槽 → bind 不重调);滚动只改 viewport 偏移(复用既有 set_scroll/max_scroll),
|
||||
不重绑未进视口的项。**整表扫过后池发生 wrap(cap<count),回收合法重绑但每次 draw 有界(≤视口窗口)**。
|
||||
- **多行富 item**:`meow_lv_item` 含 主标题 / 副标题(第二行小字)/ 左图标·缩略图 / 右尾件(chevron·开关·复选·⋯)/
|
||||
`section`(段头占位行)/ `enabled`。行高可配:`set_item_h`(精确 px)或 `set_item_tier(lines)`(1→48 / 2→56)。
|
||||
- **默认 renderer**(`meow_listview_set_item_renderer(lines, show_icon, show_right)`)用主题语义色绘制富 item;
|
||||
**完全自绘**:`meow_listview_set_item_draw(draw_fn, ctx)` 覆盖默认 renderer(App 收 idx/rect/state 自绘整行)。
|
||||
- **段头**:bind 返回 `item->section=1`(title=段名)→ 段头占位行(非粘性;粘性为后续增强)。
|
||||
- **空态**:`meow_listview_set_empty_text(text)`——count==0 时视口内居中画文案。
|
||||
|
||||
**与既有机制集成**:焦点/事件走 §6.4(`meow_listview_tap_at` 命中行自动 `meow_ui_focus_acquire`,点外由
|
||||
`meow_ui_focus_tap` 释放——list 焦点残留根治下沉);滚动接 §12.3 `meow_scroll`(`meow_scroll_wrap(lv, 1)`,
|
||||
`track`+`fling` 跟手/惯性,`set_snap(item_h)` 吸附到行边界);选中/按压态样式走主题
|
||||
(`primaryContainer`/半透明 `primary` 按压层);`enabled=0` 行变淡且命中不响应。
|
||||
|
||||
**API 一览(`include/meow_ui_widgets.h`,§5.5)**:
|
||||
```c
|
||||
typedef struct meow_lv_item { const char *title, *subtitle; const meow_img *icon; int icon_kind;
|
||||
int right, on, enabled, section; void *user; } meow_lv_item;
|
||||
typedef int (*meow_listview_count_fn)(void *ctx);
|
||||
typedef void (*meow_listview_bind_fn)(int index, meow_lv_item *item, void *ctx);
|
||||
typedef void (*meow_listview_draw_fn)(int idx, int x, int y, int w, int h, int state, void *ctx);
|
||||
int meow_listview_set_vsource(meow_widget *lv, meow_listview_count_fn count,
|
||||
meow_listview_bind_fn bind, void *ctx); /* 切 pool 模式 */
|
||||
int meow_listview_set_item_renderer(meow_widget *lv, int lines, int show_icon, int show_right);
|
||||
int meow_listview_set_item_tier(meow_widget *lv, int lines); /* 1→48 / 2→56 */
|
||||
int meow_listview_set_item_draw(meow_widget *lv, meow_listview_draw_fn draw, void *ctx);
|
||||
int meow_listview_set_empty_text(meow_widget *lv, const char *text);
|
||||
```
|
||||
> 兼容:既有 `meow_listview_*`(src2 模式)全部保留不动(`src2==NULL` 走既有路径);本层为叠加。
|
||||
> `meow_widget` 尾部追加虚拟化字段(`calloc` 归零,安全);`meow_ui.a` 需重编,App 链新库(老 ABI 不重建名)。
|
||||
> 宿主验证 `test_meow_ui_listvirt`(T1 虚拟化/bind 有界/回滚、T2 多行/截断/图标/尾件、T3 段头/空态、
|
||||
> T4 自绘、T5 焦点/fling 联动)+ 全量回归 widgets/dispatch/box/anim/scroll/ime/pinyin/overlay/notif/
|
||||
> sdk_input_keys/p1p2p3 全 PASS;交叉 -Wall -Wextra 0 warning。
|
||||
|
||||
---
|
||||
|
||||
### 5.6 设计模板 B③ 弹窗/对话框 [源:2026-08-30]
|
||||
@@ -755,6 +801,58 @@ meow_widget *meow_layout_spacer(int w, int h);
|
||||
|
||||
---
|
||||
|
||||
> **TopAppBar 位图动作图标(M2.9x,追加式)**:TopAppBar 动作项默认是**文本**(`meow_topappbar_add_action`)。为支持「⚙↻⌕」类**动作图标**(4k 字库无这些码点,字体码点画图标会出豆腐),新增把某动作项改为**位图图标**的接口:
|
||||
>
|
||||
> ```c
|
||||
> int meow_topappbar_set_item_icon(meow_widget *w, int idx, int icon_idx);
|
||||
> ```
|
||||
> - `idx` = 动作项下标(`meow_topappbar_add_action` 返回值;越界返回负值)。
|
||||
> - `icon_idx` = `sb_icons[]` 位图索引(用 `MEOW_ICON_*` 常量,见下表;<0 非法)。
|
||||
> - 返回 `0`=成功;`NULL`/非 TopAppBar/越界 `idx`/非法 `icon_idx`=负值。
|
||||
> - 绘制:与状态栏**同源**位图(Material Symbols Rounded 32x32 单通道 alpha,`meow_gfx_blit_icon_ink`),
|
||||
> 图标槽在动作格内水平居中、ink 垂直对齐栏中点,颜色取主题 `onSurface`、**按压态向栏底色淡 12%**。
|
||||
> - **兼容**:未调用本 API 的动作项保持文本(行为不变);`items` 文本条目不感知位图化。
|
||||
>
|
||||
> **sb_icons[] 索引表**(Material Symbols Rounded,MD3,单通道 alpha 32x32;Apache-2.0,
|
||||
> 来源 google/material-design-icons,重新生成 `sdk/tools/gen_action_icons.py`):
|
||||
>
|
||||
> | 索引 | 状态栏 `SB_I_*`(SDK 内部) | TopAppBar `MEOW_ICON_*`(App 用) | 图形 | 用途 |
|
||||
> |---|---|---|---|---|
|
||||
> | 0 | `SB_I_WIFI` | — | Wi-Fi | 状态栏 WiFi(满格) |
|
||||
> | 1 | `SB_I_BT` | — | 蓝牙 | 状态栏蓝牙(连接中) |
|
||||
> | 2 | `SB_I_BATT` | — | 电池 | 状态栏电量 |
|
||||
> | 3 | `SB_I_VOL` | — | 音量 | 状态栏音量 |
|
||||
> | 4 | `SB_I_BRIGHT` | — | 亮度 | 状态栏背光 |
|
||||
> | 5 | `SB_I_WIFI_STRONG` | — | Wi-Fi | 状态栏(-60dBm 强) |
|
||||
> | 6 | `SB_I_WIFI_MEDIUM` | — | Wi-Fi | 状态栏(-75dBm 中) |
|
||||
> | 7 | `SB_I_WIFI_WEAK` | — | Wi-Fi | 状态栏(更弱) |
|
||||
> | 8 | `SB_I_WIFI_NO` | — | Wi-Fi | 状态栏(开未连接) |
|
||||
> | 9 | `SB_I_WIFI_OFF` | — | Wi-Fi | 状态栏(关闭) |
|
||||
> | 10 | `SB_I_BT_NO` | — | 蓝牙 | 状态栏(未连接) |
|
||||
> | 11 | `SB_I_BT_CONN` | — | 蓝牙 | 状态栏(已连接) |
|
||||
> | 12 | `SB_I_BT_DIS` | — | 蓝牙 | 状态栏(关闭) |
|
||||
> | 13 | `SB_I_BT_SEARCH` | — | 蓝牙 | 状态栏(搜索中) |
|
||||
> | 14 | `SB_I_BATT_CHG` | — | 电池 | 状态栏(充电) |
|
||||
> | 15 | `SB_I_SETTINGS` | `MEOW_ICON_SETTINGS` | ⚙ 齿轮 | 设置 |
|
||||
> | 16 | `SB_I_REFRESH` | `MEOW_ICON_REFRESH` | ↻ 回转 | 刷新 |
|
||||
> | 17 | `SB_I_SEARCH` | `MEOW_ICON_SEARCH` | 🔍 放大镜 | 搜索 |
|
||||
> | 18 | `SB_I_PLAY_ARROW` | `MEOW_ICON_PLAY_ARROW` | ▶ | 播放 |
|
||||
> | 19 | `SB_I_PAUSE` | `MEOW_ICON_PAUSE` | ⏸ | 暂停 |
|
||||
> | 20 | `SB_I_SKIP_NEXT` | `MEOW_ICON_SKIP_NEXT` | ⏭ | 下一首 |
|
||||
> | 21 | `SB_I_SKIP_PREV` | `MEOW_ICON_SKIP_PREV` | ⏮ | 上一首 |
|
||||
> | 22 | `SB_I_ARROW_BACK` | `MEOW_ICON_ARROW_BACK` | ← | 返回 |
|
||||
> | 23 | `SB_I_CLOSE` | `MEOW_ICON_CLOSE` | × | 关闭 |
|
||||
> | 24 | `SB_I_ADD` | `MEOW_ICON_ADD` | + | 添加 |
|
||||
> | 25 | `SB_I_CHECK` | `MEOW_ICON_CHECK` | ✓ | 勾选 |
|
||||
> | 26 | `SB_I_LYRICS` | `MEOW_ICON_LYRICS` | 歌词 | 歌词 |
|
||||
> | 27 | `SB_I_QUEUE_MUSIC` | `MEOW_ICON_QUEUE_MUSIC` | 队列 | 播放列表 |
|
||||
> | 28 | `SB_I_DOWNLOAD` | `MEOW_ICON_DOWNLOAD` | ↧ | 下载 |
|
||||
> | 29 | `SB_I_HEART` | `MEOW_ICON_HEART` | ♥ | 收藏 |
|
||||
>
|
||||
> **索引约定**:状态栏占用 `0-14`(既有序号,**勿改序**);TopAppBar 动作从 `15` 起连续追加(只追加,既有索引不变)。`MEOW_ICON_*`(公共头 `meow_ui_widgets.h`)与 `SB_I_*`(SDK `sb_icons.h`)**同值**,二者须保持表序一致(`test_umbrella.c` 有 `_Static_assert` 锁 15/21/27/29)。
|
||||
|
||||
---
|
||||
|
||||
### 5.9 音频客户端 API(`meow_audio_*`,M2.90 落地)
|
||||
|
||||
> **权威规范:`devdocs/plan/AUDIOD_DESIGN.md`**(用户定案五项决策)。**已实现**:A1(agent-244/246,audiod+CLI+SDK 客户端,M2.90 入固件)+ A2(流媒体客户端迁 SINK_AUDIOD,e013621)+ 后续批次(M2.91 FPGA 开机武装 / M2.93 长流护栏 / M2.94 几何收窄 / M2.95 音量反转 / M2.96 炸耳修复 / 261 电平监控待 M2.97 固件化)。IPC 号段 `0x00C0-0x00CA` 见 `meow_ipc.h` / docs/MEOWOS_SPEC_REF §4。
|
||||
@@ -881,6 +979,99 @@ void meow_ui_global_remove(int id);
|
||||
|
||||
---
|
||||
|
||||
### 6.4 事件分发形式化(2026-09-02):命中测试 / 捕获 / 消费 / 层间隔离 / 唯一焦点
|
||||
|
||||
> **定位**:把此前各处 ad-hoc 的「命中/捕获/消费/层间隔离/焦点」补丁(模态穿透、弹层叠黑、
|
||||
> list 焦点残留、同坐标二次消费)**上收为单一仲裁者**。三个历史根因(设置页滑条穿透、
|
||||
> Dialog/Sheet 弹层叠黑、list 焦点残留导致主页面滑不动)与「注入事件被下层同坐标控件二次消费」
|
||||
> 都在本节模型内覆盖。**兼容铁律:只追加 API、不删改既有签名**;未调用本节 API 的 App 行为与
|
||||
> 之前完全一致(命中/捕获仍可由 App 在 `on_tap`/`on_touch_*` 自理)。hello/files/reader 等
|
||||
> 既有 App 只把 SDK 提供的语义用于**收敛 bug**,不改正常交互语义。
|
||||
|
||||
#### 6.4.1 分发模型(自上而下命中 + 命中即消费 + down 捕获 + 模态层间隔离)
|
||||
|
||||
触摸事件沿 **z 序自顶向下**命中(模态层**永远先审**);某一层命中即消费(返回 consumed 才继续
|
||||
下探)。一次 **touch-down 命中后捕获整个手势直到 up**——期间 move/up 只发给捕获目标,**防滑出后
|
||||
误触发下层**(根治「手指滑出控件仍点下层」)。**模态层存在时下层完全收不到事件**(含兜底:
|
||||
模态背板未命中的事件由背板消费,不透传)。
|
||||
|
||||
层序(顶 → 底)与 §6.1 三层模型 +0 模态层合成一张表:
|
||||
|
||||
| 层 | 命中语义 | 是否透传 |
|
||||
|---|---|---|
|
||||
| **+0 模态层**(Dialog/Sheet/通知中心/电源菜单等) | 模态始终先审;未命中的事件由**背板消费**(点外=取消/关闭) | **不透传** |
|
||||
| **③ 系统全局**(IME/状态栏/通知中心入口……已在运行循环消费) | 命中即消费 | 返回 1 才透传 |
|
||||
| **② App 跨 Activity 全局**(`meow_ui_global_hit`) | 命中返回 1=消费 | 否则落下层 |
|
||||
| **① 当前 Activity 页面控件**(view/widget,z 降序) | 命中即消费 | 否则让 App 自理 |
|
||||
|
||||
SDK 提供统一仲裁者(`meow_ui_hit_topdown` 沿 z 降序返回首个命中控件,含 `owner==active`+enable
|
||||
过滤)。**模态存在时它恒返回模态哨兵**——下层控件(即使同坐标)绝不会成为捕获/命中目标。
|
||||
|
||||
手势捕获由 `meow_ui_gesture_begin/owner/end` 表达:`meow_ui_run` 在 `MEOW_EV_TOUCH_DOWN` 时
|
||||
自动 `begin`(命中并捕获),随后 move/up 只发给捕获目标(`meow_ui_gesture_owner` 恒定),
|
||||
抬手(TAP/SWIPE/LONG_PRESS/NOTIF_PULL/清队列)自动 `end` 释放。模态期间 begin 捕获的是模态
|
||||
哨兵——下层不被捕获,天然根治穿透。
|
||||
|
||||
#### 6.4.2 焦点模型(唯一持有者 + 点外释放 + 滚动容器转移)
|
||||
|
||||
**焦点持有者唯一**(`meow_ui_focus_owner`)。点非 focus 区域 → **释放焦点并触发通知回调**
|
||||
(`meow_ui_focus_tap`,list 焦点残留根治)。滚动容器(W_LIST/W_LISTVIEW)在
|
||||
`meow_list_tap_at`/`meow_listview_tap_at` 命中行时自动 `meow_ui_focus_acquire`(列表成为唯一
|
||||
焦点);任何落在该容器外的触摸(`meow_ui_focus_tap` 判定 hit != focus owner)释放焦点并通知,
|
||||
回退到页面级滚动。
|
||||
|
||||
**滚动容器间焦点转移规则**:
|
||||
|
||||
1. 某滚动容器(list/lazycolumn/row)获得焦点后,随后的**纵向** swipe / 拖动滚动该容器;
|
||||
2. 任何命中落在该容器**外**的触摸(含空白、页面内容、其它控件)→ 焦点释放 + `lost` 回调,
|
||||
后续拖动滚动**页面**(不再滚列表);
|
||||
3. 焦点释放/转移不销毁容器控件(`meow_widget` 仍由 App/视图持有),下次命中行重新 acquire。
|
||||
|
||||
#### 6.4.3 公共 API(至 `meow_ui_input.h`,仅追加)
|
||||
|
||||
```c
|
||||
/* 模态作用域:dialog/sheet/App 自绘模态「打开时 begin、关闭时 end」;支持嵌套(depth 递增)。 */
|
||||
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(含)一次性关上嵌套;幂等 */
|
||||
int meow_ui_modal_active(void); /* 当前是否有任一模态作用域 */
|
||||
int meow_ui_modal_depth(void); /* 模态栈深(弹层叠开计数) */
|
||||
|
||||
/* 自上而下命中:z 降序返回首个命中控件;模态存在时恒返回模态哨兵(背板消费,不透传)。 */
|
||||
void *meow_ui_hit_topdown(int x, int y);
|
||||
int meow_ui_widget_hit(meow_widget *w, int x, int y); /* meow_widget_hit 公共别名 */
|
||||
|
||||
/* 手势捕获:down 命中即捕获直到 up;期间 move/up 只给捕获目标(滑出不误触)。 */
|
||||
void *meow_ui_gesture_begin(int x, int y);
|
||||
void *meow_ui_gesture_owner(void);
|
||||
void meow_ui_gesture_end(void);
|
||||
|
||||
/* 唯一焦点:点非 focus 区域 → 释放并通知(list 焦点残留根治)。 */
|
||||
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);
|
||||
int meow_ui_focus_acquire(void *owner);
|
||||
int meow_ui_focus_release(void *owner);
|
||||
void meow_ui_focus_tap(int x, int y);
|
||||
```
|
||||
|
||||
#### 6.4.4 迁移与兼容
|
||||
|
||||
- **dialog/sheet**(`meow_ui_dialog.c`):`meow_dialog_show`/`meow_sheet_open` 已迁到
|
||||
`meow_ui_modal_begin/end`——模态打开期间 `meow_ui_modal_active()` 为真,下层收不到触摸;
|
||||
点卡片外由背板消费(关闭)。**弹层叠黑**由既有「打开前整帧快照 + 关闭还原 + 高亮仅重绘卡片
|
||||
不叠遮罩」保证(M2.71-bug3),与本节模态层隔离正交、互不影响。
|
||||
- **list/scroll**(`meow_ui_widget.c`):`meow_list_tap_at`/`meow_listview_tap_at` 命中行自动
|
||||
`meow_ui_focus_acquire`,点外由 `meow_ui_run` 的 `meow_ui_focus_tap` 统一释放——list 焦点残留
|
||||
根治下沉到 SDK;App 层不再需要各自「点非 list 区域手动清 focus」补丁。
|
||||
- **注入/叠加让位**:`overlay_input_suppressed`(§10.5)是**系统叠加层**的层间隔离(面板输入由
|
||||
launcher 接管、App 排空副本),与本节模态层隔离互补共同防「同坐标二次消费」,语义不变。
|
||||
- 以上均为**行为收敛**(修 bug),正常交互语义不变;`p1p2p3_repro`/`widgets`/`ime_fix`/`pinyin`/
|
||||
`overlay_suppress`/`notif_gesture`/`sdk_fixes`/`sb_collapse`/`box`/`anim`/`scroll`/`sdk_input_keys`
|
||||
全量回归通过。
|
||||
|
||||
---
|
||||
|
||||
## 7. 动画/时间(B1 最小可用)
|
||||
|
||||
当前无动画框架。B1 提供**帧驱动回调 + 内置时间**,应用据此做低成本的帧序列/淡入淡出:
|
||||
@@ -1073,7 +1264,219 @@ patch_v2/meow_launcherd/meow_font.a # 复用(静态链接)
|
||||
|
||||
---
|
||||
|
||||
## 12. 遗留 / 待实现
|
||||
## 12. 控件层扩展:可组合布局引擎 / 动画基座 / 滚动物理
|
||||
|
||||
> **性质**:三块**独立增量**(对照 §5.8.5 的「固定尺寸 Row/Column/Box」与 §7 的「B1 最小动画」做升级),
|
||||
> **全部追加式**:新头 `include/meow_ui_box.h` / `include/meow_ui_anim.h` / `include/meow_ui_scroll.h` +
|
||||
> 新 TU `meow_ui_box.c` / `meow_ui_anim.c` / `meow_ui_scroll.c`;**不改任何既有 API 签名/行为**。
|
||||
> 既有控件仍是**绝对定位**(x,y,w,h);布局只是「帮你算坐标」的容器,不改变绘制/命中/状态机。
|
||||
> 旧绝对坐标**永远有效**——不用这三块的 App 完全照旧。
|
||||
|
||||
### 12.1 线性布局引擎(`meow_box`)
|
||||
|
||||
在既有 `meow_row/column/box`(§5.8.5,只支持固定尺寸+居中)之上,提供更完整的线性布局:
|
||||
|
||||
- **容器**:竖直(`MEOW_BOX_V`)/ 水平(`MEOW_BOX_H`)。
|
||||
- **子项尺寸策略**:固定(`MEOW_BOX_FIXED`,取控件当前 w/h)| 权重(`MEOW_BOX_WEIGHT`,均分剩余主轴空间)| wrap(`MEOW_BOX_WRAP`,按内容测量)。
|
||||
- **间距/边距**:容器 `padding`(left/top/right/bottom)+ 项间 `gap`;子项 margin 读取既有 `meow_widget_opts.margin_x/y`。
|
||||
- **主轴对齐**:start / center / end / space-between / space-around / space-evenly。
|
||||
- **交叉轴对齐**:start / center / end / stretch(填满容器交叉向减 margin)。
|
||||
- **嵌套**:box 内可再挂子 box(`meow_box_add_sub`),求解深度 ≤ `MEOW_BOX_MAX_DEPTH`(16,静态栈,无递归爆炸)。
|
||||
|
||||
```c
|
||||
typedef struct meow_box meow_box;
|
||||
typedef enum { MEOW_BOX_H = 0, MEOW_BOX_V = 1 } meow_box_dir;
|
||||
typedef enum { MEOW_BOX_FIXED = 0, MEOW_BOX_WEIGHT = 1, MEOW_BOX_WRAP = 2 } meow_box_size;
|
||||
typedef enum {
|
||||
MEOW_BOX_ALIGN_START = 0, MEOW_BOX_ALIGN_CENTER, MEOW_BOX_ALIGN_END,
|
||||
MEOW_BOX_ALIGN_SPACE_BETWEEN, MEOW_BOX_ALIGN_SPACE_AROUND, MEOW_BOX_ALIGN_SPACE_EVENLY,
|
||||
} meow_box_align;
|
||||
typedef enum { MEOW_BOX_CROSS_START = 0, MEOW_BOX_CROSS_CENTER, MEOW_BOX_CROSS_END, MEOW_BOX_CROSS_STRETCH } meow_box_cross;
|
||||
|
||||
meow_box *meow_box_new(meow_box_dir dir); /* 命名避开既有 meow_box_create(W_LAYOUT) */
|
||||
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);
|
||||
int meow_box_add(meow_box *b, meow_widget *w, int size, int weight); /* 固定/权重 */
|
||||
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); /* WRAP 测量 */
|
||||
int meow_box_add_sub(meow_box *b, meow_box *sub, int size, int weight); /* 嵌套 */
|
||||
|
||||
/* 单遍求解:把每个子控件 x/y/w/h 写进 meow_widget(新增 meow_widget_set_rect);深度≤16 静态栈。
|
||||
* 子控件 margin(widget opts.margin_x/y)参与排布;嵌套 sub 递归。 */
|
||||
void meow_box_arrange(meow_box *b);
|
||||
```
|
||||
|
||||
- **与视图框架集成(opt-in)**:box 可作 `meow_view` 的布局提供者;view **不设**就保持现有绝对坐标行为。
|
||||
`meow_view_set_layout(v, box)` 登记 + 立即排一次;`meow_view_relayout(v)` 手动重排;
|
||||
顶层页在 `meow_view_page_push/pop/replace` 进入时自动重排。box 矩形默认跟 view 内容矩形(w/h 未设时自动填)。
|
||||
```c
|
||||
void meow_view_set_layout(meow_view *v, meow_box *box); /* NULL=取消 */
|
||||
void meow_view_relayout(meow_view *v);
|
||||
```
|
||||
- **控件尺寸测量**:`MEOW_BOX_FIXED`/`WRAP`(无 measure 回调)直接取控件当前 `w/h`(对既有固定尺寸控件天然兼容);
|
||||
`WRAP` 可用 `meow_box_measure_fn`(给出主轴向 min/preferred)供内容自适应的自定义控件用。
|
||||
- **内存纪律**:box 静态池、定长子项数组(`MEOW_BOX_MAX_CHILD=24`),arrange 热路径零 malloc;嵌套用深度≤16 静态栈。
|
||||
|
||||
### 12.2 动画基座 + UI 节拍器(`meow_anim` / `meow_anim_tick`)
|
||||
|
||||
把 §7「应用在 on_tick 里用 `meow_ui_time_ms` 做时间轴」收敛成**声明式 tween**:
|
||||
|
||||
- **缓动**:linear / ease-out / ease-in-out / overshoot(`meow_ease_apply`)。
|
||||
- **动画对象**:`meow_anim_start` 一次性声明 from/to/duration/easing + `update`(每帧插值回调)+ `done`(完成/取消回调);**并行多动画**(静态池 ≤ `MEOW_ANIM_MAX=16`)、**取消**(`meow_anim_cancel`)。
|
||||
- **节拍**:`meow_anim_tick()` 挂既有 `meow_ui_run` 主循环(on_tick 之后)自动调用;**无动画时零开销**(只查计数即返回),不引入常驻高频轮询。阻塞 IME 循环(`meow_ui_feed_tick`)不喂 tick(动画在模态期暂停)。
|
||||
- **与脏区集成**:`meow_anim_set_region(a,x,y,w,h)` 让每帧**只 flush 动画影响区**,不做整屏重绘;app 的 `update` 回调也可自行 `meow_ui_flush_region`。
|
||||
|
||||
```c
|
||||
typedef enum { MEOW_EASE_LINEAR = 0, MEOW_EASE_OUT, MEOW_EASE_IN_OUT, MEOW_EASE_OVERSHOOT } meow_ease;
|
||||
float meow_ease_apply(meow_ease e, float t); /* t∈[0,1]→[0,1];overshoot 中后段可略>1、末点=1 */
|
||||
|
||||
typedef struct meow_anim meow_anim;
|
||||
typedef void (*meow_anim_update_fn)(meow_anim *a, float v, void *ctx);
|
||||
typedef void (*meow_anim_done_fn)(meow_anim *a, int canceled, void *ctx);
|
||||
typedef struct meow_anim_desc {
|
||||
float from, to; uint32_t duration_ms; meow_ease ease;
|
||||
meow_anim_update_fn update; meow_anim_done_fn done; void *ctx;
|
||||
} meow_anim_desc;
|
||||
|
||||
meow_anim *meow_anim_start(const meow_anim_desc *d);
|
||||
void meow_anim_cancel(meow_anim *a);
|
||||
int meow_anim_active(meow_anim *a);
|
||||
float meow_anim_value(meow_anim *a);
|
||||
void meow_anim_set_region(meow_anim *a, int x, int y, int w, int h);
|
||||
void meow_anim_tick(void); /* 推进所有活动动画(挂主循环;无动画零开销) */
|
||||
unsigned meow_anim_count(void);
|
||||
```
|
||||
|
||||
- **与 §7 的关系**:§7(B1 最小可用)是手写时间轴;本模块是正式基座。应用可任选其一或混用。
|
||||
- **内存纪律**:静态池 ≤16,start/cancel/tick 全程零 malloc。
|
||||
|
||||
### 12.3 滚动物理(`meow_scroll`)
|
||||
|
||||
给既有滚动容器(`meow_list` / `W_LIST` lazycolumn/lazyrow / `meow_listview`)加**可惯性滚动**物理层:
|
||||
|
||||
- **速度跟踪**:`meow_scroll_track(dy, t)`(拖动跟手 + 采样),fling 时用**最近 N 个采样加权**得初速。
|
||||
- **松手 fling**:`meow_scroll_fling(s)` 起一条减速度曲线动画(用 §12.2 动画基座驱动)推进位移;每帧**只 redraw 列表视口区**(不整屏)。
|
||||
- **边界过冲 clamp + 简单回弹**:free 目标越界 → clamp 到 `[0,max]`;快速撞墙补两段短 IN_OUT 回弹(不做复杂 spring)。
|
||||
- **滚动停止自动归位**:`meow_scroll_set_snap(step)` 可选吸附到 step 倍数。
|
||||
- **跟手拖动行为保持现状不变**:`track` 内部调既有 `meow_list(viewport)_set_scroll`(clamp 一致),与 app 直接 `scroll_by` 等价;app 把拖动的 `scroll_by` 换成 `track` 即可获得速度采样。
|
||||
|
||||
```c
|
||||
typedef struct meow_scroll meow_scroll;
|
||||
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_scroll *meow_scroll_wrap(meow_widget *list, int is_listview); /* 1=W_LISTVIEW / 0=W_LIST 或 lazycolumn/row */
|
||||
void meow_scroll_free(meow_scroll *s);
|
||||
void meow_scroll_track(meow_scroll *s, int dy, uint64_t t_ms); /* 跟手拖动 + 速度采样 */
|
||||
void meow_scroll_fling(meow_scroll *s); /* 松手惯性滑动(内部算初速) */
|
||||
void meow_scroll_stop(meow_scroll *s); /* 立即停止 */
|
||||
int meow_scroll_velocity(meow_scroll *s); /* 加权初速 px/s */
|
||||
int meow_scroll_active(meow_scroll *s);
|
||||
int meow_scroll_offset(meow_scroll *s);
|
||||
void meow_scroll_set_snap(meow_scroll *s, int step);
|
||||
void meow_scroll_set_friction(meow_scroll *s, int tau_ms); /* 默认 180ms */
|
||||
```
|
||||
|
||||
- **内存纪律**:静态池(应用侧按需建会话),track/fling/tick 全程零 malloc;无 fling 时 meow_scroll 不参与节拍(由 `meow_anim_tick` 活动计数保证零开销)。**不引 libm**(SDK 免 `-lm` 约定)。
|
||||
|
||||
### 12.4 对既有 App 的零影响(迁移说明)
|
||||
|
||||
- **追加式**:三块全为新 API;既有 `meow_row/column/box`(§5.8.5)与全部 `meow_*_create/hit/scroll` 签名一行未改。
|
||||
- **旧绝对坐标永远有效**:box 只是把坐标算进 `meow_widget`(`meow_widget_set_rect`);不用 box 的 App 照旧 `on_layout`/绝对定位。
|
||||
- **机制层新增**:`meow_ui_input.c` 主循环新增 `meow_anim_tick()` 调用(无动画零开销);`meow_ui_view.c` 新增布局登记表(未设布局的 view 行为不变);`meow_ui_widget.c` 新增 `meow_widget_set_rect`(只设字段不标脏,既有 `meow_layout_arrange` 不受影响)。
|
||||
- **同步重编注意**:`meow_ui.a` 内容变化(新增 box/anim/scroll 三个 `.o`,`meow_ui_widget.o`/`meow_ui_view.o`/`meow_ui_input.o` 重编)→ 链 `meow_ui.a` 的 App 需**重链**(`build_sdk.sh` 一键重编 + 各 App `build_*.sh` 重链)。既有 App **不改源码**即可零回归。
|
||||
|
||||
### 12.5 SDK 默认列表/容器控件默认 MD3(`meow_list_set_container`)+ 全局 App 内底部上滑回桌面
|
||||
|
||||
> **性质**:M6.37–M6.39 批,两条默认行为定案。
|
||||
> ① **SDK 默认列表/容器控件默认 MD3**——`meow_list_create` / `meow_lazycolumn_create` / `meow_lazyrow_create`(皆 `W_LIST`)
|
||||
> 与 `meow_listview` 容器控件**默认即容器模式**:每项套 `surfaceContainer` 圆角卡 + 项间空隙 + 去分隔线 + 卡内 tonal 选中/按压,
|
||||
> 用默认 renderer 的 App **开箱即 MD3**(此前默认扁平)。需扁平/满幅分隔线的宿主可显式 `meow_list_set_container(w,0)` **回退**。
|
||||
> ② **全局 App 内「底部上滑回桌面」为系统保留手势**——任意 App 内屏底边缘上滑即回桌面(后台保活、进程保留转后台),
|
||||
> App **勿在 `on_swipe` 拦截上滑**,否则该手势不生效。
|
||||
|
||||
```c
|
||||
/* MD3 容器模式(W_LIST 共用):0=扁平满宽项+分隔线(回退);1=每项外套 surfaceContainer
|
||||
* 圆角卡+项间空隙(去分隔线),选中=primaryContainer/按压=primary α26(卡内 tonal)。
|
||||
* 覆盖 meow_list_create / meow_lazycolumn_create / meow_lazyrow_create。返回 0=成功;无效=负值。 */
|
||||
int meow_list_set_container(meow_widget *w, int on);
|
||||
|
||||
/* 底部热区上滑 → 回桌面(home)判定(镜像 launcher np_gesture.h 同口径):屏底热区起滑 +
|
||||
* 上移 ≥HOME_SWIPE + 竖向主导(|dx|<|dy|)+ 避开左缘返回列。1=命中(系统保留手势);0=否。 */
|
||||
int meow_ui_input_is_home_swipe(int down_x, int down_y, int dx, int dy, int fb_h);
|
||||
```
|
||||
|
||||
- **MD3 默认**:`container` 字段默认 1;`meow_list_set_container(w,0)` 回退至「满幅扁平行 + 分隔线」旧观感。
|
||||
容器模式下未选中行以 `surfaceContainer` 呈现、选中 `primaryContainer`、按压 `primary α26`;分隔线去除,回退时以 `onSurfaceVariant` 分隔线。
|
||||
宿主测试 `test_meow_ui_widgets`/`test_meow_ui_listvirt` 用 `set_container(0)` 锁扁平基线。
|
||||
- **业务上滑不冲突**:`meow_ui_input_is_home_swipe` 只认「**屏底热区 + 竖向主导上滑**」;**中部上滑/滚动
|
||||
(LazyColumn/LazyGrid/页面滚动)不受影响**,仍交 App;底部热区横向滑仍走左缘返回/横向逻辑。
|
||||
- **App 勿拦截**:命中后 SDK 作为**系统保留手势**优先于 App `on_swipe`/视图树(否则已注册 `on_swipe` 的 App 会接管,
|
||||
导致 App 内上滑不回桌面),路由 `meow_ui_nav_request_exit()`(MSG_NAV EXIT → launcher 批①后台保活,进程保留转后台);
|
||||
**不**调 `meow_ui_request_exit()`(那会 `exit(0)` 断送保活)。IME 软键盘覆盖底部热区时手势让位 IME(防输入时误退)。
|
||||
- **系统边界**:与「顶部下拉通知中心」同理为系统保留手势;流媒体客户端/媒体 App 的列表、播放页照常用**中部**手势
|
||||
(滚动/翻页),不外溢到屏底热区拦截回桌面。
|
||||
|
||||
---
|
||||
|
||||
## 13. 字符串资源 / i18n(`meow_str_*`,2026-09-02 追加)
|
||||
|
||||
> **性质**:字符串资源外置(i18n 雏形)——公开 SDK 前,SDK/App 的 UI 文案不再硬编码在 C 源码:第三方开发者按系统 locale 取对应语言串,且可不改 SDK 二进制用 TF 卡覆盖/追加。**追加式**:新头 `include/meow_ui_str.h` + 新 `.o` `meow_ui_str.c`;既有 `meow_ui.h` 伞头自动聚合(仅新增 #include),既有 API/控件签名一行未改。
|
||||
|
||||
### 13.1 API(追加式,纯 libc,宿主 gcc 可直接单测)
|
||||
|
||||
```c
|
||||
const char *meow_str(const char *id); /* 按当前 locale 取 id 文案;静态串,永不 NULL;未知 id 原样返回 id */
|
||||
int meow_strf(char *out, int outsz, const char *id, ...); /* 取串后按 snprintf 语义格式化进 out(自带 NUL);out==NULL/outsz<=0→-1 */
|
||||
void meow_str_set_locale(const char *locale); /* 切 locale("zh_cn"|"en_us"),即时生效 + 重载 TF 覆盖表 + best-effort 持久化 ui.locale */
|
||||
const char *meow_str_locale(void); /* 当前生效 locale(缺省 "zh_cn") */
|
||||
```
|
||||
|
||||
> `meow_strf` 用**调用方自带缓冲**(而非静态/线程局部),避免 IME/模态等**重入路径**的缓冲竞态,且与 SDK 既有 `snprintf` 习惯一致(无格式参数时等价深拷贝 `meow_str`)。这是把任务书 `meow_strf(id, ...)` 落地为 `(out, outsz, id, ...)` 的定案(可重入、线程安全优先于少一个参数)。
|
||||
|
||||
### 13.2 取值优先级(四层,逐层回落,永不返回 NULL)
|
||||
|
||||
1. 当前 locale 的 **TF 覆盖表**:`MeowData/<pkg>/strings/<locale>.txt`,`id=text` 行格式 UTF-8,`meow_str_set_locale` 加载进静态数组(≤256 条);
|
||||
2. 内嵌表**当前语言**(`en_us→en`,其余→`zh`);
|
||||
3. 内嵌表**内置默认**(`zh_cn`,locale 缺省/未知语言回落);
|
||||
4. 仍不可得 → **原样返回 `id`**(调用方可展示 id/打点,绝不见 NULL)。
|
||||
|
||||
> 无卡/无覆盖文件/无该 id → 自动回落**内嵌双语默认表**(编译进二进制,必可用)。内嵌表含 `common.*`(确定/取消/返回/确认/完成/保存/删除/编辑/搜索/输入/请输入/请选择/加载中/暂无数据/重试/失败/重试失败/关闭/未知/设置/点击确定编辑,共 21 条)+ `ime.*`(空格/确定/取消/拼英/?123/清/拼音/(无候选)/中,共 9 条),`zh_cn`+`en_us` 双语。
|
||||
|
||||
### 13.3 ID 规范
|
||||
|
||||
`域.名`(`common.ok` / `common.cancel` / `player.pause`)。SDK 自带 `common.*` 通用组 + `ime.*` IME 键面组;第三方可自建 `域`(如 `player.*`),经 TF 覆盖表追加,不改 SDK 二进制。
|
||||
|
||||
### 13.4 locale 存系统设置
|
||||
|
||||
locale 权威值 = 板载 `settings.conf` 的 `ui.locale`(默认 `zh_cn`)。SDK 首次调用**惰性读取**;`meow_str_set_locale` 即时切换(运行时)并 **best-effort 持久化**回 `ui.locale`(流式改写、保留其它行——绝不 O_TRUNC 整文件;失败静默,切换仍生效)。SDK 与 launcher 系统组件读同一份,后续系统 UI 复用同键即当前语言。
|
||||
|
||||
### 13.5 性能
|
||||
|
||||
**启动时一次加载**到静态线性表:内嵌为编译期静态表;覆盖表为启动/切 locale 时的静态数组(≤256 条)。`meow_str` 只做 O(n) 线性扫描(表小可接受),**热路径零 malloc**(无堆申请、无引用计数)。
|
||||
|
||||
### 13.6 SDK 控件接入
|
||||
|
||||
- **dialog**(§5.8.6):默认按钮 `"CANCEL"/"OK"` → `meow_str("common.cancel"/"common.ok")`;INPUT 预览占位 `"(tap OK to edit...)"` → `meow_str("common.tap_ok_edit")`。
|
||||
- **IME**(§5.1):拼音键面/徽标(空格/确定/拼英/?123/清/拼音/(无候选))与 ASCII/NUM 顶条「中」→ `meow_str("ime.*")`。
|
||||
- **空态/加载态**:SDK 不硬编码空态文案;`common.loading`/`common.no_data` 供 App 空态/加载中直接用(如 `meow_listview_set_empty_text(w, meow_str("common.no_data"))`)。
|
||||
- **launcher 等系统组件**:后续经同框架接入(本任务只管 SDK 侧;launcher 复用同 `settings.conf ui.locale`)。
|
||||
|
||||
### 13.7 边界(明示不做,写清理由)
|
||||
|
||||
- **不做复数规则**:需占位符格式化协议 + 每语言复数表,超出「按 locale 取整串」的最小必要范围;
|
||||
- **不做 BiDi / RTL / 动态字体回退**:需文本整形 + 布局镜像 + 字体回退策略,面向 480×720 单行/短文案场景暂不需要;
|
||||
- 当前形态为 i18n **雏形**,复杂变体(复数/语言方向/字体重载)留后续批次;locale 仅 `zh_cn`/`en_us` 双语(任意串作覆盖表文件名,内嵌按非 `en` 视为内置默认)。
|
||||
|
||||
### 13.8 同步重编注意
|
||||
|
||||
`build_sdk.sh` OBJS 新增 `meow_ui_str`;`meow_ui_str.o`(含内嵌表)+ `meow_ui_dialog.o`/`meow_ui_ime.o` 重编 → **链 `meow_ui.a` 的 App 需重链**(一键 `build_sdk.sh`)。既有 App **不改源码**即零回归(仅按钮/键面文案随 locale 变化)。宿主回归测试需在编译命令补 `meow_ui_str.c`(各 test_*.c 头注释已同步)。
|
||||
|
||||
---
|
||||
|
||||
## 14. 遗留 / 待实现
|
||||
|
||||
1. `meow_ui` 渲染后端需从 launcher 里**抽公共绘制实现**(`back_buf`/`meow_dirty_*`/`meow_present`/`meow_font_bind` 目前是 launcher 静态函数——需提取为可链接的 `meow_ui_gfx` 或改为共享 obj,注意 launcher 与 app 是**不同进程**,不能共享同一 back_buf;`meow_ui.a` 是**独立库**,内部自建 fb 上下文,不复用 launcher 的全局 `g_dirty`)。
|
||||
2. 输入线程优先级:vs 主循环 10ms 热键轮询的竞态,需在 B1 实现时定义清楚(应用进程内独立事件循环,不与 launcher 抢)。
|
||||
|
||||
@@ -10,8 +10,8 @@ cd "$(dirname "$0")"
|
||||
CC="${MEOW_CC:-$HOME/Project/R3ProII/.tools/zig-mipsel-cc}"
|
||||
SYSROOT="${MEOW_SYSROOT:-$HOME/Project/R3ProII/.tools/mipsel-linux-muslsf-cross/mipsel-linux-muslsf}"
|
||||
"$CC" -Os -static -Wall -Wextra -I"$(pwd)/../../sdk" -I. \
|
||||
app.c ui/home/home.c ui/full.c ui/demo/demo.c ui/demo/list/list.c ui/common/chrome.c \
|
||||
logic/notify.c logic/state.c \
|
||||
app.c ui/home/home.c ui/full.c ui/demo/demo.c ui/demo/list/list.c ui/demo/sdk2/sdk2.c \
|
||||
ui/common/chrome.c logic/notify.c logic/state.c \
|
||||
"$(pwd)/../../sdk/meow_ui.a" "$SYSROOT/lib/libz.a" -lpthread -o hello
|
||||
ls -la hello
|
||||
file hello
|
||||
@@ -106,6 +106,21 @@ int list_view_down(int x, int y); /* touch_down 命中列表:置
|
||||
int list_view_move(int y); /* touch_move 拖动列表:1=在列表拖动 */
|
||||
int list_view_swipe(int del); /* swipe:列表聚焦时滚列表,1=已消费 */
|
||||
|
||||
/* ---- SDK 批次2 控件演示区 CONTAINER 子视图(ui/demo/sdk2/sdk2.c)----
|
||||
* 复刻 list.c 模式:demo 页内容区底部追加三节——枚举选择器 meow_select / 2D 虚拟化宫格
|
||||
* meow_lazygrid / 列表行左滑删除 meow_listview_set_swipe_delete。子视图自持三节控件与视图态
|
||||
* (grid 非 meow_widget:on_draw 直绘 meow_lazygrid_draw;swipe-list 行左滑 3 相位手势),
|
||||
* 对外暴露 sdk2_build_at(demo_build_at 调)与 hit/down/move/swipe/release(demo 分发改派)。 */
|
||||
meow_view *sdk2_view_create(void); /* 建 CONTAINER 子视图(须即 meow_view_add_child) */
|
||||
meow_view *sdk2_view_get(void); /* 返回子视图(宿主测嵌套生命周期断言) */
|
||||
int sdk2_build_at(int yb_base, int ct, int scroll); /* 在 base yb 处建三节控件(owner=ACT_DEMO),返回下一 yb */
|
||||
void sdk2_release(void); /* 内容释放后:释放 grid/摘控件引用/清几何 */
|
||||
int sdk2_hit(int x, int y); /* tap 分发:select 按钮/宫格 tap/列表行 tap/行左滑松手 swipe_end */
|
||||
int sdk2_down(int x, int y); /* touch_down:宫格拖拽或列表行左滑 begin;1=本区消费 */
|
||||
int sdk2_move(int x, int y); /* touch_move:宫格滚动/列表左滑 move 或列表竖向滚动;1=消费 */
|
||||
int sdk2_swipe(int del); /* swipe UP/DOWN:宫格或列表聚焦时滚自身;1=已消费 */
|
||||
int sdk2_dragging(void); /* 1=本子视图拖拽在途(demo_swipe 早退防 on_swipe 二次滚动) */
|
||||
|
||||
/* ---- chrome(ui/common/chrome.c)---- */
|
||||
extern meow_widget *g_tb; /* TopAppBar(chrome 固定) */
|
||||
extern meow_widget *g_nav; /* NavigationBar(chrome 固定) */
|
||||
|
||||
@@ -43,6 +43,7 @@ static int g_drag_slider = 0; /* 手指按在 slider */
|
||||
int g_drag_page = 0; /* 手指按在页内容(非 slider/list):拖拽滚页面 */
|
||||
static int g_drag_page_last_y = 0; /* 页面拖拽上一帧 y */
|
||||
static int g_drag_page_delta = 0; /* 页面拖拽累计位移(节流重建,≥8px 才重建一次) */
|
||||
static uint64_t g_drag_page_tlast = 0; /* 页面拖拽上次重建时刻(359:每 poll 合并一次 rebuild) */
|
||||
|
||||
/* SDK 控件不透明:App 自记几何,用于局部 flush / 命中 / scroll 重建。 */
|
||||
|
||||
@@ -157,6 +158,7 @@ static void demo_free_content(void)
|
||||
if (g_slider_cid > 0) { meow_draw_unregister(g_slider_cid); g_slider_cid = 0; }
|
||||
g_dtitle_n = 0;
|
||||
list_view_release(); /* 列表控件已在上方 g_dslot 释放;摘容器引用/几何 */
|
||||
sdk2_release(); /* SDK 批次2 子视图:释放 grid/btn/list 控件引用 + 清几何 */
|
||||
}
|
||||
|
||||
/* 按 scroll 重建全部内容控件(owner=ACT_DEMO:调用方须已 push ACT_DEMO)。
|
||||
@@ -246,6 +248,15 @@ static void demo_build_at(int scroll)
|
||||
d_title(DCL_X, SY(yb), "MEDIA SESSION (meta)"); yb += 26;
|
||||
r_media = RX(DCL_X, SY(yb), DCW, DBH); g_dbtn_media = meow_btn_create_variant(r_media.x, r_media.y, r_media.w, r_media.h, "SEND META", MEOW_BTN_FILLED); SLOT(g_dbtn_media, r_media.x, r_media.y, r_media.w, r_media.h); yb += DCH;
|
||||
|
||||
/* ---- SDK 批次2 新控件(meow_select / meow_lazygrid / swipe-delete)----
|
||||
* 三节由 sdk2 CONTAINER 子视图(ui/demo/sdk2/sdk2.c)承接:章节标题/宫格/select 值文本走
|
||||
* 子视图 on_draw 直绘(同列表区标题);select 按钮 + swipe 列表 widget 由 sdk2 自持、此处创建。
|
||||
* 控件创建后几何已内存;跨重建视图态(grid scroll/sel、swipe 行集/scroll、select 值)由
|
||||
* sdk2_build_at 从子视图持有态恢复。
|
||||
* 注:sdk2 置于 LazyColumn 之前(列表保持 demo 页内容**末尾**区块),确保「滚动到底列表上屏」
|
||||
* 的既有宿主 P3 断言坐标假设仍然成立(sdk2 增高在列表上方,列表仍是内容底)。 */
|
||||
yb = sdk2_build_at(yb, ct, scroll);
|
||||
|
||||
/* ---- LazyColumn(100 项列表滚动) ----
|
||||
* 本区块标题改由列表区 CONTAINER 子视图(ui/demo/list/list.c)的 on_draw 绘制(注册表→
|
||||
* invalidate 链路);列表滚动/选中由 list_view_bind 从子视图持有态恢复(不再经 g_ds)。 */
|
||||
@@ -268,7 +279,21 @@ void demo_paint(void)
|
||||
meow_ui_paint_full(); /* 整帧按 z 序重画 */
|
||||
}
|
||||
|
||||
/* 重建内容到指定 scroll(clamp 到 [0, max_scroll]);进入/滚动共用 */
|
||||
/* 内容视口坐标(每次 build 同步):屏 y ∈ [g_demo_ct, g_demo_cb),x 跨 0..width。 */
|
||||
static void demo_repaint_viewport(void)
|
||||
{
|
||||
int h = g_demo_cb - g_demo_ct;
|
||||
if (h <= 0) return;
|
||||
/* 滚动重绘:只清底+按 z 重画内容视口矩形 [0, ct, w, h],不整帧。
|
||||
* 359 教训(滚动只刷视口 + 局部脏区不整屏重绘铁律):meow_ui_invalidate 内部已把绘制 clip
|
||||
* 收敛到矩形(meow_gfx_clip_push)+ meow_ui_flush_region 只上屏该区;chrome(TopAppBar/
|
||||
* NavBar/FAB,z=999,位于视口外)不触及,上一帧像素保留,滚动不再重画 chrome 字形。 */
|
||||
meow_ui_invalidate(0, g_demo_ct, meow_ui_width(), h);
|
||||
}
|
||||
|
||||
/* 重建内容到指定 scroll(clamp 到 [0, max_scroll]);进入/滚动共用。
|
||||
* 滚动路径用视口局部刷(demo_repaint_viewport)替代整帧 demo_paint——每刻度重建仍发生,
|
||||
* 但不再整帧重绘(359 教训:SDK 控件内建局部刷,卡顿源于调用方整页重绘)。 */
|
||||
void demo_rebuild_to(int scroll)
|
||||
{
|
||||
int vp_h = g_demo_cb - g_demo_ct;
|
||||
@@ -279,7 +304,7 @@ void demo_rebuild_to(int scroll)
|
||||
demo_free_content();
|
||||
demo_build_at(scroll);
|
||||
g_demo_scroll = scroll;
|
||||
demo_paint();
|
||||
demo_repaint_viewport();
|
||||
}
|
||||
|
||||
/* ---- demo 交互 ---- */
|
||||
@@ -299,6 +324,8 @@ static int demo_content_tap(int x, int y)
|
||||
|
||||
/* 嵌套命中(子>父,§A.7 设计):列表区 CONTAINER 子视图先发,命中即消费。 */
|
||||
if (list_view_hit(x, y)) return 1;
|
||||
/* SDK 批次2 子视图(select 按钮/宫格/列表行左滑松手):命中即消费。 */
|
||||
if (sdk2_hit(x, y)) return 1;
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
if (meow_widget_hit(g_dbtn_b[i], x, y)) { d_toggle_sel(g_dbtn_b[i], r_btn[i]); g_ds.btn[i] = meow_widget_selected(g_dbtn_b[i]); return 1; }
|
||||
@@ -344,8 +371,18 @@ static void demo_media_send(void)
|
||||
}
|
||||
|
||||
/* demo 页 on_tap 分发(chrome → 内容) */
|
||||
/* 页面拖拽抬手把节流窗内遗留的 delta 一次性重建(视口局部刷)——slow drag 结束为 TAP 时也精确落点。 */
|
||||
static void demo_flush_page_delta(void)
|
||||
{
|
||||
if (g_drag_page && (g_drag_page_delta >= 1 || g_drag_page_delta <= -1)) {
|
||||
demo_rebuild_to(g_demo_scroll + g_drag_page_delta);
|
||||
g_drag_page_delta = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void demo_on_tap(int x, int y)
|
||||
{
|
||||
demo_flush_page_delta(); /* 手势结束:TAP 抬手边沿(<SWIPE_THRESH),把遗留页面 delta 落到位 */
|
||||
if (g_tb) {
|
||||
int ah = meow_topappbar_hit(g_tb, x, y);
|
||||
if (ah == MEOW_TOPBAR_BACK) { meow_view_page_pop(); return; }
|
||||
@@ -389,44 +426,59 @@ static void demo_slider_drag(int x)
|
||||
meow_ui_invalidate(r_slider_txt.x, r_slider_txt.y, r_slider_txt.w, r_slider_txt.h); /* 重画值文本 */
|
||||
}
|
||||
|
||||
/* demo 页 on_touch_down:锁一种拖拽(slider > list > 页内容)。
|
||||
* 列表区走 list_view_down(子>父:命中列表区由列表 CONTAINER 子视图收口)。 */
|
||||
/* demo 页 on_touch_down:锁一种拖拽(slider > list > sdk2 > 页内容)。
|
||||
* 列表区走 list_view_down(子>父:命中列表区由列表 CONTAINER 子视图收口);
|
||||
* SDK 批次2 子视图走 sdk2_down(宫格拖拽/列表行左滑 begin,子>父收口)。 */
|
||||
void demo_touch_down(int x, int y)
|
||||
{
|
||||
g_drag_slider = 0; g_drag_list = 0; g_drag_page = 0; g_drag_page_delta = 0;
|
||||
if (!g_demo) return;
|
||||
if (meow_widget_hit(g_dslider, x, y)) { g_drag_slider = 1; demo_slider_drag(x); return; }
|
||||
if (list_view_down(x, y)) return; /* 列表区:置 focus+drag_list(list.c) */
|
||||
if (sdk2_down(x, y)) return; /* SDK 批次2:宫格/列表行左滑 begin(sdk2.c) */
|
||||
g_demo_focus = 0;
|
||||
if (y >= g_demo_ct && y < g_demo_cb && x >= 0 && x < meow_ui_width()) {
|
||||
g_drag_page = 1; g_drag_page_last_y = y;
|
||||
}
|
||||
}
|
||||
|
||||
/* demo 页 on_touch_move:拖动过程实时更新(slider 值 / 列表滚动 / 页面滚动节流重建)。 */
|
||||
/* demo 页 on_touch_move:拖动过程实时更新(slider 值 / 列表滚动 / sdk2 滚动 / 页面滚动节流重建)。 */
|
||||
void demo_touch_move(int x, int y)
|
||||
{
|
||||
if (!g_demo) return;
|
||||
if (g_drag_slider) { demo_slider_drag(x); return; }
|
||||
if (list_view_move(y)) return; /* 列表拖动(list.c) */
|
||||
if (sdk2_move(x, y)) return; /* SDK 批次2:宫格滚动/列表左滑 move/列表竖向滚动(sdk2.c) */
|
||||
if (g_drag_page) {
|
||||
int dy = y - g_drag_page_last_y; g_drag_page_last_y = y;
|
||||
g_drag_page_delta += -dy; /* 手指上移(y 减) → scroll 增(显示更低内容),与 on_swipe 一致 */
|
||||
if (g_drag_page_delta >= 8 || g_drag_page_delta <= -8) {
|
||||
/* 359 教训:滚动期间每个 poll 合并一次 rebuild(勿每触摸事件各自重建/上屏)。
|
||||
* 累计位移超阈值(≥8) 且(距上次 ≥16ms 或 已是大步 ≥32)才重建一次;未到窗则继续
|
||||
* 累积 delta(不丢),下个窗口一次性重建到精确位置。 */
|
||||
uint64_t now = meow_ui_time_ms();
|
||||
int big = (g_drag_page_delta >= 8 || g_drag_page_delta <= -8);
|
||||
int merge = (now - g_drag_page_tlast >= 16) ||
|
||||
(g_drag_page_delta >= 32) || (g_drag_page_delta <= -32);
|
||||
if (big && merge) {
|
||||
demo_rebuild_to(g_demo_scroll + g_drag_page_delta);
|
||||
g_drag_page_tlast = now;
|
||||
g_drag_page_delta = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* demo 页 on_swipe:LEFT=回 home;UP/DOWN=列表聚焦滚列表,否则页滚动。 */
|
||||
/* demo 页 on_swipe:LEFT=回 home;UP/DOWN=列表/宫格/sdk2 聚焦滚自身,否则页滚动。 */
|
||||
void demo_swipe(int dir, int dist)
|
||||
{
|
||||
if (dir == MEOW_SWIPE_LEFT) { meow_view_page_pop(); return; }
|
||||
if (dir != MEOW_SWIPE_UP && dir != MEOW_SWIPE_DOWN) return;
|
||||
if (g_drag_page || g_drag_list) return; /* 本次手势已实时跟手滚动过 */
|
||||
if (g_drag_page || g_drag_list || sdk2_dragging()) { /* 本次手势已实时跟手滚动过 */
|
||||
demo_flush_page_delta(); /* 抬手:flush 节流窗内遗留页面 delta(视口局部刷)精确落点 */
|
||||
return;
|
||||
}
|
||||
int del = (dir == MEOW_SWIPE_UP) ? dist : -dist; /* 手指上移 → 偏移增大 */
|
||||
if (list_view_swipe(del)) return; /* 列表聚焦:滚列表(list.c,子>父消费) */
|
||||
if (sdk2_swipe(del)) return; /* SDK 批次2:宫格/列表聚焦滚自身(sdk2.c,子>父消费) */
|
||||
demo_rebuild_to(g_demo_scroll + del);
|
||||
}
|
||||
|
||||
@@ -441,7 +493,8 @@ static void demo_page_enter(meow_view *v, void *ctx)
|
||||
hello_nav_report(); /* ① 上报 demo 栈深/顶层 */
|
||||
/* 列表区 CONTAINER 子视图随本页进入由 SDK view_enter 递归驱动(其 on_draw 在 demo_rebuild_to
|
||||
* →demo_paint 中经注册表绘制;meow_draw_view_entered 含祖先已进入判定,首批可见)。 */
|
||||
demo_rebuild_to(0);
|
||||
demo_rebuild_to(0); /* 内容建到 scroll=0(视口局部刷) */
|
||||
demo_paint(); /* 进入页整帧首绘(含 chrome 置顶 z=999);仅进入一次,非滚动路径 */
|
||||
meow_log(MEOW_LOG_INFO, "demo enter scroll=0");
|
||||
}
|
||||
|
||||
@@ -475,14 +528,16 @@ int demo_is_active(void)
|
||||
}
|
||||
|
||||
/* demo view:on_create 建 chrome+内容(一次,owner=ACT_DEMO);进入时 rebuild 到顶。
|
||||
* 视图深度化:先挂列表区 CONTAINER 子视图到本 ACTIVITY view 的 children,再建内容。
|
||||
* 列表子视图 id 用非页面值,不占 g_view_by_id(见 list.c)。 */
|
||||
* 视图深度化:先挂列表区 + SDK 批次2 两个 CONTAINER 子视图到本 ACTIVITY view 的 children,再建内容。
|
||||
* 子视图 id 用非页面值,不占 g_view_by_id(见 list.c/sdk2.c)。 */
|
||||
static void demo_page_create(meow_view *v, void *ctx)
|
||||
{
|
||||
(void)v;
|
||||
((demo_ctrl *)ctx)->view = v;
|
||||
meow_view *lv = list_view_create();
|
||||
if (lv) meow_view_add_child(v, lv);
|
||||
meow_view *s2 = sdk2_view_create();
|
||||
if (s2) meow_view_add_child(v, s2);
|
||||
demo_init();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,455 @@
|
||||
/* ui/demo/sdk2/sdk2.c — 控件库 Demo 页内「SDK 批次2 新控件」CONTAINER 子视图(agent-363)。
|
||||
*
|
||||
* 目标(SDK_VIEW_FRAMEWORK_DESIGN.md §B.4/§E③,同 list.c):把 demo 页底部的三个 SDK 批次2
|
||||
* 新控件演示区块抽成一个 meow_view CONTAINER 子视图,挂到 demo ACTIVITY view 的 children:
|
||||
* ① meow_select (§5.8.9 枚举/预设选择器:下拉模态 + 单选列表 + on_change 回调)
|
||||
* ② meow_lazygrid (§12.4 2 维虚拟化宫格:非 meow_widget,on_draw 直绘 meow_lazygrid_draw)
|
||||
* ③ meow_listview 左滑删除(§5.5 swipe-delete:swipe_begin/move/end + on_delete + 回弹/删除动画)
|
||||
*
|
||||
* 职责边界(诚实,与 list.c 同折中):
|
||||
* - 本子视图承接三节**整体**:章节标题 + select 按钮 + 宫格 + swipe-列表 的创建/释放与视图态
|
||||
* (grid 虚拟源/选中/滚动、swipe 行集/滚动、select 选中值)。控件**创建**在 demo_build_at 链路内
|
||||
* (sdk2_build_at,owner=ACT_DEMO)、**释放**在 sdk2_release(demo_free_content 调)——与 list.c
|
||||
* 相同折中:完整「控件归容器拥有」需 SDK 侧支持(本批冻结 patch_v2/sdk/)。
|
||||
* - meow_lazygrid 是**非 meow_widget**:不注册 z 序/绘制注册表。本子视图 on_draw 直接调
|
||||
* meow_lazygrid_draw(view → 注册表 → invalidate 链路,与 list 区块标题同法)。
|
||||
* - 章节标题(三段)与 select 当前值文本也走 on_draw 直绘(列表区标题同法),不逐标题注册
|
||||
* CUSTOM 绘制物;on_draw 是回调、view 不缓存像素(非保留,§5.8.7)。
|
||||
* - 嵌套生命周期由 SDK view_enter/view_leave 递归驱动(随 demo 页进出,同 list.c);绘制经
|
||||
* meow_draw_view_entered(含祖先已进入判定)在 meow_ui_paint_full/invalidate 触发。
|
||||
*
|
||||
* 事件接线(demo 页把 touch/tap/swipe 分发改派到本子视图,实现「子>父」命中):
|
||||
* - tap → sdk2_hit (select 按钮 / 宫格 tap / 列表行 tap / 行左滑松手 swipe_end)
|
||||
* - down → sdk2_down (宫格聚焦拖拽 / 列表行左滑 begin)
|
||||
* - move → sdk2_move (宫格滚动 / 列表左滑 move 或列表竖向滚动)
|
||||
* - swipe → sdk2_swipe (宫格/列表聚焦时滚自身;否则回退页面滚动)
|
||||
*/
|
||||
#include "hello_internal.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* ---- 本子视图(CONTAINER,非页面:id 用越界值避免占用 g_view_by_id 顶层页槽位)---- */
|
||||
static meow_view *g_sdk2_view = NULL;
|
||||
|
||||
/* ---- demo 内容布局常量(与 demo.c 一致):内容区 x16 宽 448 ---- */
|
||||
#define S2_X 16
|
||||
#define S2_W 448
|
||||
#define S2_DCW 212
|
||||
#define S2_DBH 44
|
||||
#define S2_CH 52
|
||||
|
||||
/* 三节(章节标题几何在 sdk2_build_at 计算后存入,on_draw 直绘用;屏幕坐标)。 */
|
||||
static int s2_t1_y, s2_t2_y, s2_t3_y; /* 三节标题 y(on_draw 直绘) */
|
||||
|
||||
/* ============================================================ */
|
||||
/* ① meow_select — 枚举/预设选择器 */
|
||||
/* ============================================================ */
|
||||
static meow_widget *g_btn_sel = NULL; /* 「打开选择器」按钮(sdk2_release 释放) */
|
||||
static meow_select *g_sel = NULL; /* 选择器实例(sdk2_view_create 建一次,destroy 释放) */
|
||||
static rect_t r_selbtn = { 0, 0, 0, 0 };
|
||||
static rect_t r_selval = { 0, 0, 0, 0 }; /* 当前选择值文本区(on_draw 直绘) */
|
||||
static int g_sel_cur = 0; /* 当前选中项(跨重建保留;on_change 更新) */
|
||||
|
||||
static const char *g_sel_items[] = { /* 枚举演示:音频档位(对齐 M7.2 设置页采样率/码率) */
|
||||
"Auto", "44.1kHz", "48kHz", "96kHz", "192kHz",
|
||||
};
|
||||
#define SEL_N ((int)(sizeof g_sel_items / sizeof g_sel_items[0]))
|
||||
|
||||
/* on_change 回调(meow_select_open 确认后触发:更新选中下标 + 重画值文本区)。 */
|
||||
static void sdk2_sel_change(int idx, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
if (idx >= 0 && idx < SEL_N) g_sel_cur = idx;
|
||||
if (r_selval.w > 0) meow_ui_invalidate(r_selval.x, r_selval.y, r_selval.w, r_selval.h);
|
||||
}
|
||||
|
||||
/* 打开选择器(阻塞到确认/取消;on_change 已更新选中值)。 */
|
||||
static void sdk2_select_open(void)
|
||||
{
|
||||
if (!g_sel) return;
|
||||
int r = meow_select_open(g_sel);
|
||||
meow_log(MEOW_LOG_INFO, "sdk2 select return=%d (%s)",
|
||||
r, (r >= 0 && r < SEL_N) ? g_sel_items[r] : "cancel");
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* ② meow_lazygrid — 2 维虚拟化宫格(非 widget) */
|
||||
/* ============================================================ */
|
||||
static meow_lazygrid *g_grid = NULL;
|
||||
static rect_t r_grid = { 0, 0, 0, 0 };
|
||||
static int g_grid_scroll = 0; /* 跨重建保留:宫格滚动偏移 */
|
||||
static int g_grid_sel = -1; /* 跨重建保留:选中项 */
|
||||
static int g_drag_grid = 0; /* 触摸拖宫格(down 置位;move 滚动) */
|
||||
static int g_grid_focus = 0; /* 1=宫格聚焦(swipe 滚宫格而非页面) */
|
||||
static int g_grid_last_y = 0;
|
||||
|
||||
#define GRID_N 36 /* 总项数(count > 视口行数 → 可滚动) */
|
||||
#define GRID_COLS 3
|
||||
#define GRID_CW 138
|
||||
#define GRID_CH 86
|
||||
#define GRID_GAP 10
|
||||
#define GRID_H 190
|
||||
|
||||
/* 虚拟源:count 总项数(实时查);bind 项进入视口时调一次(同 listview 池语义)。 */
|
||||
static int grid_count(void *ctx) { (void)ctx; return GRID_N; }
|
||||
static void grid_bind(int idx, meow_lazygrid_item *item, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
static char t[24], s[24];
|
||||
snprintf(t, sizeof t, "App %d", idx);
|
||||
snprintf(s, sizeof s, "v1.%d", idx);
|
||||
memset(item, 0, sizeof *item);
|
||||
item->title = t; item->subtitle = s;
|
||||
item->enabled = (idx % 9 != 8); /* 演示禁用变淡(每 9 个禁用 1 个) */
|
||||
}
|
||||
|
||||
/* 完全自绘 cell(set_item_draw):icon+label 风格,对齐主界面用途。 */
|
||||
static void grid_cell(int idx, int x, int y, int w, int h, int st, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
const meow_theme *th = meow_theme_get();
|
||||
int sel = st & MEOW_LAZYGRID_ST_SELECTED;
|
||||
int dis = st & MEOW_LAZYGRID_ST_DISABLED;
|
||||
uint32_t bg = sel ? th->primaryContainer : th->surfaceContainerLow;
|
||||
uint32_t icon = dis ? th->onSurfaceVariant : th->primary;
|
||||
uint32_t txt = sel ? th->onPrimaryContainer
|
||||
: (dis ? th->onSurfaceVariant : th->onSurface);
|
||||
meow_ui_fill_rect(x, y, w, h, bg); /* 格底卡片 */
|
||||
/* icon 块(无图资源:主题 primary 方块代替主界面图标位) */
|
||||
int isz = w / 3; if (isz > h / 2) isz = h / 2;
|
||||
meow_ui_fill_rect(x + (w - isz) / 2, y + 10, isz, isz, icon);
|
||||
/* 名 + 状态(底部两行,中心对齐) */
|
||||
static char t[24], s[24];
|
||||
snprintf(t, sizeof t, "App %d", idx);
|
||||
snprintf(s, sizeof s, "v1.%d", idx);
|
||||
int thh = meow_ui_text_height(th->font_body);
|
||||
meow_ui_text_center(x + w / 2, y + h - thh - 22, t, th->font_body, txt);
|
||||
meow_ui_text_center(x + w / 2, y + h - thh - 3, s, th->font_body, txt);
|
||||
if (sel) meow_ui_draw_rect_round(x, y, w, h, th->radius_card, th->primary);
|
||||
if (dis) meow_ui_fill_rect_alpha(x, y, w, h, th->onSurface, 90);
|
||||
}
|
||||
|
||||
/* 宫格 tap:置选中 + 局部刷 + 日志。 */
|
||||
static void grid_tap(int idx, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
g_grid_sel = idx;
|
||||
meow_log(MEOW_LOG_INFO, "sdk2 grid tap idx=%d", idx);
|
||||
if (r_grid.w > 0) meow_ui_invalidate(r_grid.x, r_grid.y, r_grid.w, r_grid.h);
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* ③ meow_listview 行左滑删除(swipe-delete) */
|
||||
/* ============================================================ */
|
||||
#define LV_MAX 9
|
||||
static meow_widget *g_lv = NULL; /* W_LISTVIEW(注册表自动画:行 + 右露删除区) */
|
||||
static rect_t r_lv = { 0, 0, 0, 0 };
|
||||
static int g_lv_deleted[LV_MAX]; /* 1=已删除(行移除;count 实时收缩) */
|
||||
static char g_lv_label[LV_MAX][24]; /* 行标题(bind 填充) */
|
||||
static int g_lv_scroll = 0; /* 跨重建保留:列表滚动 */
|
||||
static int g_lv_focus = 0; /* 1=swipe-list 聚焦(swipe 滚列表而非页面) */
|
||||
static int g_lv_swipe = 0; /* 行左滑在途(swipe_begin 命中;hit 里 swipe_end) */
|
||||
static int g_drag_lv = 0; /* 触摸拖列表(竖向滚动);d. 左滑时同置位 */
|
||||
static int g_lv_last_y = 0;
|
||||
#define LV_H 190
|
||||
|
||||
static int lv_count_remaining(void)
|
||||
{
|
||||
int n = 0;
|
||||
for (int i = 0; i < LV_MAX; i++) if (!g_lv_deleted[i]) n++;
|
||||
return n;
|
||||
}
|
||||
static int lv_count(void *ctx) { (void)ctx; return lv_count_remaining(); }
|
||||
|
||||
/* 显示下标(0..count-1)→ 源行 id(跳过已删除行)。删除后行前移,身份按源行稳定。 */
|
||||
static int lv_real_index(int display)
|
||||
{
|
||||
if (display < 0) return -1;
|
||||
int c = 0;
|
||||
for (int i = 0; i < LV_MAX; i++) {
|
||||
if (g_lv_deleted[i]) continue;
|
||||
if (c == display) return i;
|
||||
c++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void lv_bind(int idx, meow_lv_item *item, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
int real = lv_real_index(idx);
|
||||
static char s[24];
|
||||
memset(item, 0, sizeof *item);
|
||||
snprintf(s, sizeof s, "swipe left");
|
||||
item->title = (real >= 0) ? g_lv_label[real] : "";
|
||||
item->subtitle = s;
|
||||
item->icon_kind = (real >= 0 && real % 3 == 0) ? MEOW_LIST_ICON_FOLDER : MEOW_LIST_ICON_NONE;
|
||||
item->right = MEOW_LIST_RIGHT_NONE;
|
||||
item->enabled = 1;
|
||||
}
|
||||
/* on_delete(swipe 删除动画滑出完成回调):删除该显示下标对应的源行 + 局部刷。
|
||||
* 注:swipe_end 传的是显示下标(含删除前定位的 swipe_idx);映射回源行 id 标记。 */
|
||||
static void lv_delete(int idx, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
int real = lv_real_index(idx);
|
||||
if (real >= 0) g_lv_deleted[real] = 1;
|
||||
meow_log(MEOW_LOG_INFO, "sdk2 swipe delete display=%d real=%d", idx, real);
|
||||
if (g_lv && r_lv.w > 0) meow_ui_invalidate(r_lv.x, r_lv.y, r_lv.w, r_lv.h);
|
||||
}
|
||||
|
||||
/* 建三节时填充行标题(demo_init 一次性;跨重建保留)。 */
|
||||
static void sdk2_init_rows(void)
|
||||
{
|
||||
for (int i = 0; i < LV_MAX; i++) {
|
||||
snprintf(g_lv_label[i], sizeof g_lv_label[i], "notif %d", i);
|
||||
g_lv_deleted[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static int s2_built = 0; /* 1=本子视图内容已建(on_draw / 事件分发判空) */
|
||||
|
||||
/* ============================================================ */
|
||||
/* 释放 / 建三节 */
|
||||
/* ============================================================ */
|
||||
|
||||
/* 内容释放(demo_free_content 调):清引用(本子视图建的控件在 sdk2_release 释放)。 */
|
||||
void sdk2_release(void)
|
||||
{
|
||||
if (g_grid) { meow_lazygrid_free(g_grid); g_grid = NULL; }
|
||||
if (g_btn_sel) { meow_widget_free(g_btn_sel); g_btn_sel = NULL; }
|
||||
if (g_lv) { meow_widget_free(g_lv); g_lv = NULL; }
|
||||
r_selbtn = r_selval = r_grid = r_lv = RX(0, 0, 0, 0);
|
||||
g_drag_grid = 0; g_grid_focus = 0; g_grid_last_y = 0;
|
||||
g_lv_focus = 0; g_lv_swipe = 0; g_drag_lv = 0; g_lv_last_y = 0;
|
||||
s2_built = 0;
|
||||
if (g_sdk2_view) g_sdk2_view->x = g_sdk2_view->y = g_sdk2_view->w = g_sdk2_view->h = 0;
|
||||
}
|
||||
|
||||
/* 内容重建后建三节(demo_build_at 调)。入参:yb_base=base 空间 y;ct/scroll 同 demo.c
|
||||
* (SY(base)=ct+base-scroll)。返回下一 base y。 */
|
||||
int sdk2_build_at(int yb_base, int ct, int scroll)
|
||||
{
|
||||
#define SY2(b) (ct + (b) - scroll)
|
||||
int yb = yb_base;
|
||||
|
||||
/* ---- ① SELECT ---- */
|
||||
s2_t1_y = SY2(yb);
|
||||
yb += 26;
|
||||
r_selbtn = RX(S2_X, SY2(yb), S2_DCW, S2_DBH);
|
||||
g_btn_sel = meow_btn_create_variant(r_selbtn.x, r_selbtn.y, r_selbtn.w, r_selbtn.h,
|
||||
"CHOOSE", MEOW_BTN_OUTLINED);
|
||||
r_selval = RX(S2_X + S2_DCW + 14, SY2(yb) + 10, S2_W - S2_DCW - 14, 24);
|
||||
yb += S2_CH;
|
||||
|
||||
/* ---- ② LAZYGRID ---- */
|
||||
s2_t2_y = SY2(yb);
|
||||
yb += 30;
|
||||
r_grid = RX(S2_X, SY2(yb), S2_W, GRID_H);
|
||||
g_grid = meow_lazygrid_create(r_grid.x, r_grid.y, r_grid.w, r_grid.h,
|
||||
GRID_COLS, GRID_CW, GRID_CH, GRID_GAP);
|
||||
if (g_grid) {
|
||||
meow_lazygrid_set_vsource(g_grid, grid_count, grid_bind, NULL);
|
||||
meow_lazygrid_set_item_draw(g_grid, grid_cell, NULL);
|
||||
meow_lazygrid_set_empty_text(g_grid, "No apps");
|
||||
meow_lazygrid_set_on_tap(g_grid, grid_tap, NULL);
|
||||
meow_lazygrid_set_scroll(g_grid, g_grid_scroll);
|
||||
meow_lazygrid_set_sel(g_grid, g_grid_sel);
|
||||
}
|
||||
yb += GRID_H + 30;
|
||||
|
||||
/* ---- ③ SWIPE-DELETE ---- */
|
||||
s2_t3_y = SY2(yb);
|
||||
yb += 26;
|
||||
r_lv = RX(S2_X, SY2(yb), S2_W, LV_H);
|
||||
g_lv = meow_listview_create(r_lv.x, r_lv.y, r_lv.w, r_lv.h, 0,
|
||||
NULL, NULL, MEOW_LIST_SEL_NONE);
|
||||
if (g_lv) {
|
||||
meow_listview_set_item_h(g_lv, 48);
|
||||
meow_listview_set_vsource(g_lv, lv_count, lv_bind, NULL);
|
||||
meow_listview_set_item_renderer(g_lv, 2, 1, 0); /* 双行、左图标、无右尾件 */
|
||||
meow_listview_set_empty_text(g_lv, "All rows deleted");
|
||||
meow_listview_set_swipe_delete(g_lv, 1, lv_delete, NULL);
|
||||
meow_listview_set_scroll(g_lv, g_lv_scroll);
|
||||
}
|
||||
yb += LV_H + 10;
|
||||
|
||||
if (g_sdk2_view) {
|
||||
g_sdk2_view->x = S2_X; g_sdk2_view->y = s2_t1_y;
|
||||
g_sdk2_view->w = S2_W; g_sdk2_view->h = (r_lv.y + r_lv.h) - s2_t1_y;
|
||||
}
|
||||
s2_built = 1;
|
||||
return yb;
|
||||
#undef SY2
|
||||
}
|
||||
|
||||
/* ---- on_draw:三节标题 + select 当前值 + 宫格(非保留,invalidate 触发重画)---- */
|
||||
static void sdk2_draw_on(meow_view *v, const meow_theme *th, void *ctx)
|
||||
{
|
||||
(void)v; (void)th; (void)ctx;
|
||||
if (!s2_built) return;
|
||||
const meow_theme *t = meow_theme_get();
|
||||
meow_ui_text(S2_X, s2_t1_y, "SELECT (enum/preset)", 20, t->primary);
|
||||
meow_ui_text(S2_X, s2_t2_y, "LAZYGRID (2D virtualized)", 20, t->primary);
|
||||
meow_ui_text(S2_X, s2_t3_y, "LISTVIEW swipe-delete", 20, t->primary);
|
||||
char buf[40];
|
||||
snprintf(buf, sizeof buf, "sel = %s", g_sel_items[g_sel_cur]);
|
||||
meow_ui_text(r_selval.x, r_selval.y, buf, 20, t->dim);
|
||||
if (g_grid) meow_lazygrid_draw(g_grid, t);
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 事件分发(demo 页转发) */
|
||||
/* ============================================================ */
|
||||
|
||||
/* tap:select 按钮 → 打开模态;列表行左滑松手 → swipe_end;宫格 → tap_at;列表行 → tap。 */
|
||||
int sdk2_hit(int x, int y)
|
||||
{
|
||||
if (!s2_built) return 0;
|
||||
/* 行左滑【松手】最优先:滑动后手指可能已移出列表矩形(只判在途标志)。 */
|
||||
if (g_lv && g_lv_swipe) {
|
||||
meow_listview_set_scroll(g_lv, g_lv_scroll);
|
||||
int c = meow_listview_swipe_end(g_lv, x, y); /* 判删/回弹;动画由 meow_anim_tick 推进 */
|
||||
g_lv_swipe = 0;
|
||||
if (c) return 1; /* 消费(删/回弹动画中);未滑动=0 落行选中 */
|
||||
}
|
||||
if (g_btn_sel && meow_widget_hit(g_btn_sel, x, y)) {
|
||||
sdk2_select_open();
|
||||
meow_ui_invalidate(r_selbtn.x, r_selbtn.y, r_selbtn.w, r_selbtn.h);
|
||||
return 1;
|
||||
}
|
||||
if (g_grid && meow_lazygrid_hit(g_grid, x, y) >= 0) {
|
||||
meow_lazygrid_tap_at(g_grid, x, y); /* 置选中 + grid_tap 回调 + 局部刷 */
|
||||
return 1;
|
||||
}
|
||||
if (g_lv && meow_listview_hit(g_lv, x, y) >= 0) {
|
||||
meow_listview_tap_at(g_lv, x, y); /* 行点击(演示选中)+ 局部刷 */
|
||||
meow_ui_invalidate(r_lv.x, r_lv.y, r_lv.w, r_lv.h);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* touch_down:命中区置拖拽/聚焦。宫格>列表。返回 1=本区消费。 */
|
||||
int sdk2_down(int x, int y)
|
||||
{
|
||||
if (!s2_built) return 0;
|
||||
/* 新手势先复位本子视图拖拽态:demo_touch_down 只复位 list/slider/page 标志,不触及 sdk2
|
||||
* 的 g_drag_grid/g_drag_lv/g_lv_swipe(sdk2.c 内部,demo 页经 extern 看不到);不复位则上一条
|
||||
* sdk2 拖拽残留会在本次「落页内容区」新手势的 move 里继续劫持页面拖动(sdk2_move 守卫误判)。 */
|
||||
g_drag_grid = 0; g_drag_lv = 0; g_lv_swipe = 0;
|
||||
if (g_grid && meow_lazygrid_hit(g_grid, x, y) >= 0) {
|
||||
g_drag_grid = 1; g_grid_focus = 1; g_grid_last_y = y;
|
||||
return 1;
|
||||
}
|
||||
if (g_lv && meow_listview_hit(g_lv, x, y) >= 0) {
|
||||
meow_listview_set_scroll(g_lv, g_lv_scroll);
|
||||
g_lv_swipe = meow_listview_swipe_begin(g_lv, x, y); /* 命中可滑行 → 1 */
|
||||
g_lv_focus = 1; g_drag_lv = 1; g_lv_last_y = y;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* touch_move:宫格滚动 / 列表左滑 move(消费→不滚页面)或列表竖向滚动。返回 1=消费。 */
|
||||
int sdk2_move(int x, int y)
|
||||
{
|
||||
if (!s2_built) return 0;
|
||||
if (g_drag_grid && g_grid) {
|
||||
int dy = y - g_grid_last_y; g_grid_last_y = y;
|
||||
meow_lazygrid_scroll_by(g_grid, -dy);
|
||||
g_grid_scroll = meow_lazygrid_scroll(g_grid);
|
||||
if (r_grid.w > 0) meow_ui_invalidate(r_grid.x, r_grid.y, r_grid.w, r_grid.h);
|
||||
return 1;
|
||||
}
|
||||
/* 列表滚动须在「本次手势命中列表」才消费(g_drag_lv,由 sdk2_down 命中列表置位)。
|
||||
* 若仅因 g_lv 存在就无条件消费,会劫持 demo 页内容区拖拽(任何 move 都被这里吃掉,
|
||||
* 页面永不滚动)——与 grid(g_drag_grid 守卫)同构;359 教训同源(SDK 控件局部刷对,
|
||||
* 错在调用方控制流把页面滚动让给了控件)。 */
|
||||
if (g_drag_lv && g_lv) {
|
||||
if (g_lv_swipe) { /* 行左滑在途:喂 move;1=横向消费;0=纵向主导→取消本行 swipe */
|
||||
meow_listview_set_scroll(g_lv, g_lv_scroll);
|
||||
if (meow_listview_swipe_move(g_lv, x, y)) return 1;
|
||||
g_lv_swipe = 0;
|
||||
}
|
||||
int dy = y - g_lv_last_y; g_lv_last_y = y; /* 竖向滚动(跟手) */
|
||||
meow_listview_scroll_by(g_lv, -dy);
|
||||
g_lv_scroll = meow_listview_scroll(g_lv);
|
||||
if (r_lv.w > 0) meow_ui_invalidate(r_lv.x, r_lv.y, r_lv.w, r_lv.h);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* swipe UP/DOWN:宫格/列表聚焦时滚自身(del 已按方向加权),1=已消费。 */
|
||||
int sdk2_swipe(int del)
|
||||
{
|
||||
if (!s2_built) return 0;
|
||||
if (g_grid_focus && g_grid) {
|
||||
meow_lazygrid_scroll_by(g_grid, del);
|
||||
g_grid_scroll = meow_lazygrid_scroll(g_grid);
|
||||
if (r_grid.w > 0) meow_ui_invalidate(r_grid.x, r_grid.y, r_grid.w, r_grid.h);
|
||||
return 1;
|
||||
}
|
||||
if (g_lv_focus && g_lv) {
|
||||
meow_listview_scroll_by(g_lv, del);
|
||||
g_lv_scroll = meow_listview_scroll(g_lv);
|
||||
if (r_lv.w > 0) meow_ui_invalidate(r_lv.x, r_lv.y, r_lv.w, r_lv.h);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 本次手势是否已被本子视图实时滚过(demo_swipe 早退用,防 on_swipe 二次滚动——同 list 的 g_drag_list)。 */
|
||||
int sdk2_dragging(void)
|
||||
{
|
||||
return (g_drag_grid || g_drag_lv) ? 1 : 0;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 子视图生命周期(on_enter/on_leave 由 SDK 递归驱动;on_destroy 清引用) */
|
||||
/* ============================================================ */
|
||||
static void sdk2_page_enter(meow_view *v, void *ctx)
|
||||
{
|
||||
(void)v; (void)ctx;
|
||||
meow_log(MEOW_LOG_INFO, "demo sdk2 view enter");
|
||||
}
|
||||
static void sdk2_page_leave(meow_view *v, void *ctx)
|
||||
{
|
||||
(void)v; (void)ctx;
|
||||
meow_log(MEOW_LOG_INFO, "demo sdk2 view leave");
|
||||
}
|
||||
static void sdk2_page_destroy(meow_view *v, void *ctx)
|
||||
{
|
||||
(void)v; (void)ctx;
|
||||
if (g_sel) { meow_select_free(g_sel); g_sel = NULL; }
|
||||
s2_built = 0;
|
||||
g_sdk2_view = NULL; /* meow_view_free 递归释放前清引用,防悬垂 */
|
||||
}
|
||||
|
||||
meow_view *sdk2_view_create(void)
|
||||
{
|
||||
static const meow_view_hooks h = {
|
||||
.on_enter = sdk2_page_enter,
|
||||
.on_leave = sdk2_page_leave,
|
||||
.on_destroy = sdk2_page_destroy,
|
||||
.on_draw = sdk2_draw_on,
|
||||
};
|
||||
/* id 用非页面值:容器不占 g_view_by_id(那是 page_push/pop 顶层页查找表)。 */
|
||||
g_sdk2_view = meow_view_create(0x2000, MEOW_VIEW_CONTAINER, &h, NULL);
|
||||
/* meow_select 实例一次性创建(控制态随本子视图存活,不受内容重建影响);行标题一次性填充。 */
|
||||
sdk2_init_rows();
|
||||
if (g_sdk2_view && !g_sel) {
|
||||
g_sel = meow_select_create("Sample rate", 0);
|
||||
if (g_sel) {
|
||||
for (int i = 0; i < SEL_N; i++) meow_select_add_item(g_sel, g_sel_items[i]);
|
||||
meow_select_set_on_change(g_sel, sdk2_sel_change, NULL);
|
||||
}
|
||||
}
|
||||
return g_sdk2_view;
|
||||
}
|
||||
meow_view *sdk2_view_get(void)
|
||||
{
|
||||
return g_sdk2_view;
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
# SDK 时钟 ABI 说明(meow_ui_time_ms 真机故障的根因与修复)
|
||||
|
||||
面向对象:维护 SDK / 工具链 / 重链 App 的人。**结论先行**:这不是 SDK 代码写错,
|
||||
是 zig 0.17 自带 musl 在 mipsel 上的 time64 ABI 自相矛盾;修复落在 SDK 链上
|
||||
(`patch_v2/sdk/meow_ui_time.c` + 链接期强定义),改后需重编 `meow_ui.a` 并重链四 App。
|
||||
|
||||
## 1. 现象(真机)
|
||||
|
||||
M10 抓到的日志首列(`meow_ui_time_ms()`,64 位)可拆成:
|
||||
|
||||
```
|
||||
6607363222717682072 → lo=1135000 hi=1538396632
|
||||
8754846870717682072 → lo=1135000 hi=2038396632
|
||||
18138485921460348192 → lo=1140000 hi=4223195352
|
||||
```
|
||||
|
||||
- 低 32 位 = 开机秒数 × 1000,**恒为 1000 的倍数 → 毫秒位丢失**;
|
||||
- 高 32 位每次调用随机、无单调性。
|
||||
|
||||
同进程另一份取时(旧容器编的 core)正常 → 问题只在「zig 0.17 编的那部分」。
|
||||
|
||||
## 2. 根因链(三段证据)
|
||||
|
||||
### 2.1 zig 头文件:`time_t` 是 64 位,`struct timespec` 16 字节
|
||||
|
||||
`<zig lib>/libc/include/mips-linux-musl/bits/alltypes.h`:
|
||||
|
||||
```c
|
||||
#define _REDIR_TIME64 1
|
||||
#define __USE_TIME_BITS64 1
|
||||
typedef _Int64 time_t; /* 无条件 64 位 */
|
||||
struct timespec { time_t tv_sec; ...; long tv_nsec; ... }; /* tv_sec@0(8B)、tv_nsec@8 */
|
||||
```
|
||||
|
||||
`include/generic-musl/time.h` 因此在 `_REDIR_TIME64` 下把 `clock_gettime` 重定向到
|
||||
`__clock_gettime64`。用该链编译的 C 代码一律按「16 字节、tv_sec 8 字节」取数
|
||||
(`patch_v2/sdk/meow_ui.a(meow_ui_gfx.o)` 的旧 `meow_ui_time_ms` 反汇编:`lw 0x10/0x14`
|
||||
取 tv_sec、`lw 0x18` 取 tv_nsec,正是这个布局)。
|
||||
|
||||
### 2.2 zig 的 clang 给 mipsel 预定义了 `__ILP32__`
|
||||
|
||||
```
|
||||
$ echo | zig cc --target=mipsel-linux-musleabi -dM -E - | grep __ILP32__
|
||||
#define __ILP32__ 1
|
||||
```
|
||||
|
||||
(arm-linux-musleabi 同样为 1;x86-linux-musl / aarch64 为 0。容器里的 gcc 对 mips o32
|
||||
**不定义** `__ILP32__`。)
|
||||
|
||||
### 2.3 musl 的 time64 回退分支被这句 `!__ILP32__` 整体编译掉
|
||||
|
||||
`<zig lib>/libc/musl/src/time/clock_gettime.c`:
|
||||
|
||||
```c
|
||||
#if defined(SYS_clock_gettime64) && !defined(__ILP32__)
|
||||
... __syscall(SYS_clock_gettime64, clk, ts); /* 4403 */
|
||||
... 失败(ENOSYS) 再 __syscall(SYS_clock_gettime, clk, ts32); /* 4263 + 宽度转换 */
|
||||
#else
|
||||
r = __syscall(SYS_clock_gettime, clk, ts); /* 直接把 8 字节 time32 写进调用者缓冲 */
|
||||
#endif
|
||||
```
|
||||
|
||||
这句 `!__ILP32__` 本意只排除 x32(`__ILP32__` 是 x32 的标记),但 clang 把它当成
|
||||
「32 位数据模型」的通名。于是 zig 的 mipsel libc 走进 `#else`:
|
||||
`__clock_gettime64` 实际执行 **SYS_clock_gettime(4263)**,只写 8 字节
|
||||
`{ int32 tv_sec; int32 tv_nsec; }`。
|
||||
|
||||
**证据(反汇编,当前 zig 0.17 静态链接的探针)**:
|
||||
|
||||
```
|
||||
0002576c <__clock_gettime64>:
|
||||
257c8: addiu $2,$zero,0x10a7 ; 4263 = SYS_clock_gettime(time32)
|
||||
257cc: syscall ; $5 直接是调用者的 16 字节 struct 指针,无 4403 尝试、无转换
|
||||
```
|
||||
|
||||
同一符号在**旧容器产物** `patch_v2/ncm_app/ncm`(已入库,gcc 构建)里是:
|
||||
|
||||
```
|
||||
115748: addiu $2,$zero,0x1133 ; 4403 = SYS_clock_gettime64
|
||||
11574c: syscall
|
||||
...
|
||||
115774: addiu $2,$zero,0x10a7 ; ENOSYS 后回退 4263,写入本地 ts32
|
||||
...
|
||||
; 再把 ts32 升成 64 位写回调用者(tv_sec=tv_sec、tv_nsec=tv_nsec)
|
||||
```
|
||||
|
||||
两者对比即根因:**头文件 ABI(64 位 time_t)与 libc 实现 ABI(32 位 time32)不一致**。
|
||||
|
||||
### 2.4 数值解释(为什么「低 32 = 秒×1000、高 32 随机」)
|
||||
|
||||
libc 把 `{sec, nsec}` 当两个 int32 写进 16 字节缓冲的前 8 字节;SDK 按 64 位读:
|
||||
|
||||
```
|
||||
tv_sec = sec | (nsec << 32) // 低字正确、高字=纳秒
|
||||
tv_nsec = 缓冲偏移 8 的未初始化值 // 毫秒项≈0
|
||||
result = tv_sec*1000 + tv_nsec/1e6
|
||||
= sec*1000 + (nsec*1000 mod 2^32)<<32 + 垃圾/1e6
|
||||
```
|
||||
|
||||
- 低 32 位只剩 `sec*1000`(nsec 项左移 32 位不落到低字)→ 恒为 1000 的倍数;
|
||||
- 高 32 位 = `nsec*1000 mod 2^32` → 每次随机、不单调;
|
||||
- `tv_nsec` 来自越界读 → 通常是小值/0(真机上恰好 0,所以没有额外尾数)。
|
||||
|
||||
## 3. 修复(`patch_v2/sdk/meow_ui_time.c`)
|
||||
|
||||
1. **取数走内核 o32 ABI**:`meow_ui_time_ms()` 用
|
||||
`syscall(SYS_clock_gettime32, CLOCK_MONOTONIC, &{int32 sec; int32 nsec;})`,
|
||||
再 `meow_time_ms_from_parts(sec, nsec)` 升成 64 位毫秒。完全不依赖 libc 头声明的
|
||||
`struct timespec` 宽度 —— 即使将来 zig 修好(或换回容器链),内核 time32 入口始终存在。
|
||||
2. **链接期修 libc 入口**:同一 TU 强定义 `__clock_gettime` / `__clock_gettime64`
|
||||
(musl 的 `time()`、`gettimeofday()`、`__timedwait_cp()`(pthread 定时等待)内部都调
|
||||
`__clock_gettime`)。应用链接顺序是 `... meow_ui.a ... -lc`,归档先解析符号,libc 里那份
|
||||
坏实现(及其 VDSO 前导)不会被拉入 —— 用 `nm` 验证链接产物里没有 `cgt_init/__vdsosym`,
|
||||
且 `__time64`/`__timedwait_cp` 的 `jal` 指向本 TU 的实现。
|
||||
3. **睡眠入口一并修正(同源缺陷)**:`__nanosleep_time64` / `__clock_nanosleep(_time64)`
|
||||
先按 time32 转换再发 `SYS_nanosleep`/`SYS_clock_nanosleep`,`rem` 再扩回 64 位。
|
||||
修复前 zig libc 把 16 字节 time64 结构直接交给 time32 系统调用,内核读到的 nsec 恒为 0 →
|
||||
SDK 里 `usleep(16000)`(帧节拍、输入轮询、IME 冷却)全部变成忙等空转。
|
||||
4. **去掉四处重复的 `now_ms()`**:`meow_ui_gfx.c` / `meow_ui_sys.c` / `meow_ui_input.c` /
|
||||
`meow_cachegov.c` 统一调 `meow_ui_time_ms()`(cachegov 保留宿主测试注入桩)。
|
||||
5. 构建脚本 `build_sdk.sh` 的 OBJS 增加 `meow_ui_time`;宿主单测 `test_meow_time.c`
|
||||
固定换算边界与「单调且前进」;探针 `tools/clock_abi_probe.c` 用于宿主/真机一眼验收。
|
||||
|
||||
非 mips 目标(宿主单测、aarch64 宿主)走普通 `clock_gettime`,行为不变。
|
||||
|
||||
## 4. 验证
|
||||
|
||||
宿主:
|
||||
|
||||
```
|
||||
cd patch_v2/sdk
|
||||
gcc -Os -Wall -Wextra -I. -Iinclude -c meow_ui_time.c -o /tmp/meow_ui_time.o
|
||||
gcc -Os -Wall -Wextra -I. -Iinclude test_meow_time.c /tmp/meow_ui_time.o -o /tmp/test_meow_time
|
||||
/tmp/test_meow_time # == PASS ==(换算边界 + 单调前进)
|
||||
```
|
||||
|
||||
交叉:
|
||||
|
||||
```
|
||||
bash patch_v2/sdk/build_sdk_host.sh --verify # 成员清单/架构自检 + 0 warning
|
||||
llvm-objdump -d meow_ui.a(meow_ui_time.o) # meow_ui_time_ms: syscall 4263, lw 0x18 取 nsec、sra 扩展 tv_sec
|
||||
```
|
||||
|
||||
链接期接管(用真实归档链一个探针,检查符号来源):
|
||||
|
||||
```
|
||||
# 不应出现 libc 的 VDSO 前导(cgt_init/__vdsosym)→ libc 的取时实现未被拉入
|
||||
strings -a clock_abi_probe_sdk | grep -c __vdso # 期望 0
|
||||
llvm-nm clock_abi_probe_sdk | grep -E 'clock_gettime|nanosleep'
|
||||
# __time64 / __timedwait_cp 的 jal 应指向本 TU 的 __clock_gettime;睡眠走 SYS_nanosleep(4166)/clock_nanosleep(4265)
|
||||
```
|
||||
|
||||
探针 `tools/clock_abi_probe.c`(宿主 `gcc` 或 mipsel 交叉;加 `-DMEOW_PROBE_WITH_SDK`
|
||||
并链 `meow_ui.a` 可同时验收 `meow_ui_time_ms`):把 `struct timespec` 预填 0xAA 后取时,
|
||||
若偏移 8..11 仍是 0xAA 即说明 libc 只写了 8 字节(ABI 不一致),退出码 2。
|
||||
|
||||
真机(塔台批准后):日志首列应连续、出现非整千差;`meow_ui_time_ms()` 两次取样差 = 真实毫秒。
|
||||
本次设备已离线,真机确认**待办**。
|
||||
|
||||
## 5. 影响面
|
||||
|
||||
- **SDK 内**:`meow_ui_time_ms()` 及其上层 —— 动画 tween(`meow_ui_anim.c`)、滚动/fling
|
||||
(`meow_ui_scroll.c`/`meow_select.c`)、IME 冷却(`meow_ui_ime.c`)、状态栏动画
|
||||
(`meow_ui_gfx.c`)、通知 TTL(`meow_ui_notify.c`)、输入层 tick/启动冷却
|
||||
(`meow_ui_input.c`)、cachegov 回退窗口(`meow_cachegov.c`)、日志首列(`meow_ui_log.c`)。
|
||||
- **App 侧**:`on_tick` 入参 `now`、`meow_anim_*`、以及任何自行调用 `meow_ui_time_ms()` 的
|
||||
App(hello/reader/files/ncm…)。
|
||||
- **产物**:`meow_ui.a` 变更 → 需**重编并重链四 App**(ncm/reader/files/hello);
|
||||
`time()`/`gettimeofday()` 的修正也在同一份归档里生效(链接期覆盖)。
|
||||
- 真机写入红线未触碰;本单只改 SDK 源与构建脚本。
|
||||
|
||||
## 6. 同源残余(未在本单修复,需单独安排)
|
||||
|
||||
同一句 `!__ILP32__` 还打穿了以下路径(睡眠族已在上文一并修掉,余下这些症状不同、
|
||||
修复面更大):
|
||||
|
||||
| 路径 | 症状 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `futex` 相对超时 | 亚秒 timed wait 立即超时 | `__timedwait_cp` 的 `__futex4_cp` 同款 `!__ILP32__` 守卫;`pthread_cond_timedwait` 的「now」已由本单修正,但相对超时仍按 8 字节读(>1s 的整秒等待基本可用,亚秒等待失效) |
|
||||
| `timerfd_settime` | 到期时间错乱 | `struct itimerspec`(32 字节)当 16 字节读;当前 SDK 未用 timerfd |
|
||||
| `clock_settime`/`clock_adjtime`/`pselect`/`ppoll`/`sigtimedwait`/`semtimedop` 等 | 未验证 | 同族守卫;SDK 目前未直接使用 |
|
||||
|
||||
建议:向 zig 上游报 musl `__ILP32__` 守卫问题(clang 对 32 位目标统一定义 `__ILP32__`,
|
||||
而该守卫本意只排除 x32);根治应换用容器旧链(gcc,不定义 `__ILP32__`)编 SDK,或
|
||||
downgrade/修补 zig 内置 musl。**不要**指望 `-D_TIME_BITS=32`:zig 的
|
||||
`alltypes.h` 是无条件 `#define __USE_TIME_BITS64 1`,命令行改不动;而只改用户侧头文件会让
|
||||
用户结构(8 字节)与 libc 内部(16 字节,pthread 等)再次错配。
|
||||
@@ -0,0 +1,59 @@
|
||||
# sdk/ — meow_ui 应用框架(源码 + 构建)
|
||||
|
||||
本目录是 `meow_ui.a` 的**自包含**源码树:公共头在 `include/`(伞头 `meow_ui.h`),
|
||||
构建脚本 `build_sdk.sh`,宿主逻辑测试 `test_*.c`。上手指南见仓库根
|
||||
[README.md](../README.md) 与 [docs/APP_DEV_GUIDE.md](../docs/APP_DEV_GUIDE.md)。
|
||||
|
||||
## 构建
|
||||
|
||||
```sh
|
||||
cd sdk
|
||||
./build_sdk.sh # 产出 sdk/meow_ui.a(mipsel 静态库)
|
||||
```
|
||||
|
||||
交叉编译器默认 `$HOME/Project/R3ProII/.tools/zig-mipsel-cc`,可用 `MEOW_CC` 覆盖。
|
||||
|
||||
## 时钟:mipsel o32 时钟/睡眠 ABI 修复(已内置)
|
||||
|
||||
**公开 SDK 现已内置 mipsel o32 时钟/睡眠 ABI 修复(见 [CLOCK_ABI.md](CLOCK_ABI.md))**,
|
||||
源码在 `meow_ui_time.c`。
|
||||
|
||||
- 背景:zig 内置 musl 在 mipsel 上「头文件声明 64 位 `time_t`、实现却走 time32 系统调用」,
|
||||
导致 `meow_ui_time_ms()` 毫秒位恒 0、高 32 位随机;`usleep()` 亚秒等待立即返回(忙等空转)。
|
||||
- 修复:`meow_ui_time_ms()` 直接按**内核 o32 ABI**(8 字节 `{int32 sec; int32 nsec;}`)取时,
|
||||
再自行升成 64 位毫秒;同一 TU 强定义 `__clock_gettime` / `__clock_gettime64` /
|
||||
`__clock_nanosleep` / `__clock_nanosleep_time64` / `__nanosleep_time64`,在**链接期接管**
|
||||
libc 里那份 ABI 不一致的实现。
|
||||
- **链接顺序要求**:应用必须把 `meow_ui.a` 放在 `-lc` **之前**(`cc ... meow_ui.a ... -lc`),
|
||||
强定义才能生效。仓库内 `examples/hello/build_hello.sh` 与 `tuku/build_tuku.sh` 均如此;
|
||||
自定义构建脚本请照抄这个顺序,否则取时/睡眠会静默回到坏实现。
|
||||
- 非 mipsel 目标(宿主单测、aarch64 等)走普通 `clock_gettime`,行为不变。
|
||||
|
||||
### 已知限制
|
||||
|
||||
- **2038 前有效**:内核侧仍是 time32(`tv_sec` 为 int32),秒数超出 `INT32_MAX` 即溢出;
|
||||
本修复只保证「头/实现的宽度一致」,不把内核升到 time64。
|
||||
- 同源缺陷**未修**:futex 相对超时(`pthread_cond_timedwait` 的亚秒等待)、`timerfd_settime`、
|
||||
`clock_settime`/`clock_adjtime`、`pselect`/`ppoll` 等仍受同一句 `!__ILP32__` 影响;
|
||||
清单与影响见 [CLOCK_ABI.md](CLOCK_ABI.md) §6。SDK 自身不直接使用这些路径。
|
||||
- 真机验收:本修复的证据是反汇编 + 宿主单测(本机无 qemu-mipsel),设备侧仍需实测确认。
|
||||
|
||||
## 宿主测试
|
||||
|
||||
无硬件即可跑(`gcc` 直接编译,不链 `meow_ui.a`):
|
||||
|
||||
```sh
|
||||
cd sdk
|
||||
# 时钟换算与单调性
|
||||
gcc -Os -Wall -Wextra -I. -Iinclude -c meow_ui_time.c -o meow_ui_time_host.o
|
||||
gcc -Os -Wall -Wextra -I. -Iinclude test_meow_time.c meow_ui_time_host.o -o test_meow_time
|
||||
./test_meow_time # == PASS == / checks=N
|
||||
|
||||
# 伞头与其余模块测试(示例)
|
||||
gcc -I. -Iinclude test_umbrella.c <各 sdk 源 .c> test_host_stub.c -lpthread -o test_umbrella
|
||||
./test_umbrella
|
||||
```
|
||||
|
||||
注意:宿主编译时 `meow_ui_time.c` 走 `MEOW_TIME32_KERNEL_ABI 0` 分支(普通 `clock_gettime`),
|
||||
因此**宿主测试不能证明 mipsel o32 路径正确** —— 那条路径的正确性靠 `CLOCK_ABI.md §4` 的
|
||||
反汇编证据(`nm` 符号来源 + `syscall 4263/4265` 的 8 字节转换)。
|
||||
+10
-2
@@ -6,6 +6,11 @@
|
||||
#
|
||||
# 交叉编译器:默认 MeowOS 开发机路径 $HOME/Project/R3ProII/.tools/zig-mipsel-cc;
|
||||
# 可用环境变量 MEOW_CC 覆盖(或用你自己装好的 zig 交叉 cc,--target=mipsel-linux-musleabi soft-float)。
|
||||
#
|
||||
# 时钟(meow_ui_time):mipsel 上 zig 内置 musl 的 time64 ABI 自相矛盾(头声明 64 位 time_t、
|
||||
# 实现走 time32 取数),该 TU 按内核 o32 ABI 取时并**强定义** __clock_gettime*/睡眠入口,靠链接期
|
||||
# 接管 libc 那份坏实现。因此应用链接顺序必须是 `... meow_ui.a ... -lc`(归档在 libc 之前,
|
||||
# 现有 build_tuku.sh 即如此);根因与证据链见 sdk/CLOCK_ABI.md。
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
CC="${MEOW_CC:-$HOME/Project/R3ProII/.tools/zig-mipsel-cc}"
|
||||
@@ -14,8 +19,11 @@ 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 + cachegov/lazygrid/
|
||||
# select/net_ipc)随主仓增量并入。
|
||||
# meow_ui_time.c 必须**编进归档**且排在前面:它提供 meow_ui_time_ms(gfx 已不再实现)与
|
||||
# __clock_gettime* 强定义,归档成员随 meow_ui_time_ms 的引用被拉入,从而在 -lc 之前解析。
|
||||
OBJS="meow_ui_time meow_ui_gfx meow_ui_log meow_ui_notify meow_notify_ipc meow_ui_img meow_jpeg meow_png_stream 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 meow_cachegov meow_lazygrid meow_select meow_net_ipc"
|
||||
CFLAGS="-Os -static -Iinclude -I."
|
||||
for f in $OBJS; do
|
||||
echo "== cc $f =="
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/* demo_box_scroll.c — 宿主离屏 demo:§12.1 meow_box 布局 + §12.3 meow_scroll fling 端到端。
|
||||
* 渲染一屏「标题(固定) / 列表(weight 撑满) / 底栏(固定)」的 box 布局(cross 拉伸),
|
||||
* 再对列表跑一次 fling(速度跟踪→惯性滑动到停止处),并 dump PPM 供目检。
|
||||
*
|
||||
* 编译(宿主 gcc,链真实 SDK 源 + 打桩 font/global):
|
||||
* gcc -o demo_box_scroll demo_box_scroll.c test_host_stub.c \
|
||||
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c \
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 运行:MEOW_UI_DUMP=/tmp/demo_box.ppm ./demo_box_scroll
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define NROW 60
|
||||
#define ITEM 44
|
||||
static int src(int idx, char *label, int len, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
if (idx < 0 || idx >= NROW) return 0;
|
||||
snprintf(label, (size_t)len, "item %d", idx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
/* 离屏 dump */
|
||||
setenv("MEOW_UI_DUMP", "/tmp/demo_box.ppm", 1);
|
||||
meow_ui_opts o = { 480, 720 };
|
||||
if (meow_ui_init(&o) != 0) { printf("DEMO FAIL: init\n"); return 1; }
|
||||
const meow_theme *th = meow_theme_get();
|
||||
meow_ui_clear(th->bg);
|
||||
|
||||
/* ---- box 布局:标题(固定) / 列表(weight) / 底栏(固定),交叉轴拉伸 ---- */
|
||||
meow_box *bx = meow_box_new(MEOW_BOX_V);
|
||||
meow_box_set_rect(bx, 0, 0, 480, 720);
|
||||
meow_box_set_padding(bx, 12, 12, 12, 12);
|
||||
meow_box_set_gap(bx, 8);
|
||||
meow_box_set_align(bx, MEOW_BOX_ALIGN_START, MEOW_BOX_CROSS_STRETCH);
|
||||
|
||||
meow_widget *header = meow_btn_create(0, 0, 480, 44, "Header (fixed)");
|
||||
meow_widget *list = meow_list_create(0, 0, 480, 100, NROW, src, NULL);
|
||||
meow_list_set_item_h(list, ITEM);
|
||||
meow_widget *footer = meow_btn_create(0, 0, 480, 52, "Footer (fixed)");
|
||||
|
||||
meow_box_add(bx, header, MEOW_BOX_FIXED, 0);
|
||||
meow_box_add(bx, list, MEOW_BOX_WEIGHT, 1);
|
||||
meow_box_add(bx, footer, MEOW_BOX_FIXED, 0);
|
||||
meow_box_arrange(bx);
|
||||
|
||||
int hr[4], lr[4], fr[4];
|
||||
meow_widget_rect(header, hr);
|
||||
meow_widget_rect(list, lr);
|
||||
meow_widget_rect(footer, fr);
|
||||
printf("DEMO layout: header(%d,%d,%d,%d) list(%d,%d,%d,%d) footer(%d,%d,%d,%d)\n",
|
||||
hr[0], hr[1], hr[2], hr[3], lr[0], lr[1], lr[2], lr[3], fr[0], fr[1], fr[2], fr[3]);
|
||||
|
||||
meow_widget_draw(header);
|
||||
meow_widget_draw(list);
|
||||
meow_widget_draw(footer);
|
||||
meow_ui_flush(); /* 上屏并 MEOW_UI_DUMP 写 PPM *//* (host 离屏无 fb → 只落盘) */
|
||||
|
||||
/* ---- fling:对列表做一次惯性下滑(速度跟踪→减速→停)---- */
|
||||
meow_scroll *s = meow_scroll_wrap(list, 0);
|
||||
int maxs = meow_list_max_scroll(list);
|
||||
uint64_t t = meow_ui_time_ms();
|
||||
for (int i = 0; i < 14; i++) meow_scroll_track(s, 40, t + (uint64_t)i * 16);
|
||||
meow_scroll_fling(s);
|
||||
int guard = 0;
|
||||
while (meow_scroll_active(s) && guard++ < 300) { meow_anim_tick(); usleep(10000); }
|
||||
meow_anim_tick();
|
||||
int end = meow_scroll_offset(s);
|
||||
printf("DEMO fling: max_scroll=%d final=%d (active=%d)\n", maxs, end, meow_scroll_active(s));
|
||||
|
||||
meow_scroll_free(s);
|
||||
meow_box_free(bx);
|
||||
meow_widget_free(list);
|
||||
meow_widget_free(header);
|
||||
meow_widget_free(footer);
|
||||
meow_ui_shutdown();
|
||||
printf("DEMO OK\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* meow_cachegov.h — 磁盘缓存 page-cache 治理 SDK 库(A 的主导/治本侧)。
|
||||
*
|
||||
* 归属(devdocs/plan/ARCHITECTURE_PRINCIPLES.md ③混合,2026-09-02 用户定案):
|
||||
* - A(DONTNEED 热路径,需要 read_pos) → 随 SDK 库(本文件,进程内直读 read_pos,无 IPC)。
|
||||
* - C(设备级 /proc/meminfo Cached 监控降载)→ 归系统服务 meow_svc_perf;本模块只读其共享状态
|
||||
* 文件(MEOW_CACHEGOV_C_FILE)作为「降载」信号,供 App 查询。
|
||||
* - App(如 ncm)侧只调本库接口:登记文件 + read_pos 回调 + tick + drop_finished + 查询降载,
|
||||
* 不写自己的治理算法。
|
||||
*
|
||||
* 权威方案:devdocs/plan/OOM_PAGECACHE_GOVERNANCE.md。红线:**L2(丢 read_pos 前方)禁止**,
|
||||
* 本库绝不 drop `[read_pos, ...)` 任何未解码区。
|
||||
*
|
||||
* 分级(§2.2):
|
||||
* L0(默认,安全): 曲终/commit 后对整文件 posix_fadvise(DONTNEED)——播放器已退出解码循环。
|
||||
* L1(可选,主杠杆): 播放中丢已读前缀 `[0, read_pos-keep_hot)`,保 keep_hot 窗口供倒带;
|
||||
* 仅 drop_mode>=1 且 A 未回退到 ≥2 时执行。
|
||||
* L2(禁止,红线): 绝不丢 read_pos 前方/当前未解码页。
|
||||
*
|
||||
* underrun 回退(§2.3,SDK 内置):App 在 pread 慢(数据在盘上但 >threshold ms)处上报
|
||||
* meow_cachegov_note_refault();本库滚窗 10s,超门限逐级回退(放大 keep_hot → 降 L0 → 关 A)。
|
||||
*
|
||||
* 接口按 OOM 文档 §4.2 + §4.4;值语义与 mem_watch 的「滞回三档」风格对齐(自含:pthread + meow_log)。
|
||||
*/
|
||||
#ifndef MEOW_CACHEGOV_H
|
||||
#define MEOW_CACHEGOV_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct meow_cachegov_cfg {
|
||||
int enabled; /* 总开关:0=A 完全不丢页(register/tick/drop_finished 全部 no-op) */
|
||||
int cached_threshold_kb; /* 保留:C 侧阈值归 svc_perf(本库恒 0,只读不控,防误用) */
|
||||
int dirty_warn_kb; /* 保留:同上(svc_perf 用;本库只透传日志) */
|
||||
size_t keep_hot_bytes; /* L1 保留窗口:read_pos 之后保多少不丢(回退时自动放大) */
|
||||
size_t min_hot_ahead; /* L2 红线:read_pos 前方至少保多少——仅防御注释,**绝不参与 drop** */
|
||||
int drop_mode; /* 0=仅曲终(L0);1=曲终+播放中丢已读前缀(L0+L1,配 seek 回退) */
|
||||
int refault_backoff; /* 10s 窗口内 refault 触发回退的门限(次数) */
|
||||
} meow_cachegov_cfg_t;
|
||||
|
||||
/* 登记本次治理的缓存文件(正式缓存文件路径 + 大小 + read_pos 回调 + 回调 ctx)。
|
||||
* 返回 0=ok / 负=错。重复 path 幂等(替换条目)。 */
|
||||
int meow_cachegov_register(const char *path, uint64_t size,
|
||||
uint64_t (*read_pos_fn)(void *ctx), void *ctx);
|
||||
|
||||
/* 注销该 path 的治理条目(曲终/切歌后清理)。返回 0=找到并删 / 负=不在册。 */
|
||||
int meow_cachegov_unregister(const char *path);
|
||||
|
||||
/* 周期调用(播放循环节拍 / 监控线程均可):
|
||||
* ① 按回退档执行 L1 丢弃(丢已读前缀 [0, read_pos-keep_hot));
|
||||
* ② 评估 refault 滚窗做逐级回退。 */
|
||||
int meow_cachegov_tick(void);
|
||||
|
||||
/* 曲终/commit 后:对刚播完文件做整文件 L0 DONTNEED + 注销该条目。
|
||||
* 回退档>=3(A 整体关闭)时不丢页(L0 也被关,见 §2.3 步骤 3)。 */
|
||||
int meow_cachegov_drop_finished(const char *path);
|
||||
|
||||
/* 下载/预取方查询(1=应暂停)。数据源 = A 侧回退硬暂停 OR svc_perf C(Cached 超阈值)降载。 */
|
||||
int meow_cachegov_download_paused(void);
|
||||
int meow_cachegov_prefetch_paused(void);
|
||||
|
||||
/* 设置/读取治理配置(运行时覆盖编译期默认)。 */
|
||||
int meow_cachegov_set_cfg(const meow_cachegov_cfg_t *cfg);
|
||||
int meow_cachegov_get_cfg(meow_cachegov_cfg_t *cfg);
|
||||
|
||||
/* ---- refault 上报 + A 回退状态(underrun 检测,§2.3)---- */
|
||||
/* App 在 pread 慢(数据已在盘上、读出 >threshold ms)处上报一次 refault。线程安全。 */
|
||||
void meow_cachegov_note_refault(void);
|
||||
/* A 当前回退档:0=正常 1=keep_hot 放大 2=降 L0(L1 关) 3=A 关。 */
|
||||
int meow_cachegov_backoff_level(void);
|
||||
|
||||
/* ---- 宿主测试桩(设备永不调用;-1=真实时钟,>=0 时用注入值替代 monotonic 时钟)---- */
|
||||
void meow_cachegov_test_set_time_ms(int64_t t);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MEOW_CACHEGOV_H */
|
||||
+125
-8
@@ -35,6 +35,12 @@
|
||||
/* 列表编码上限(count 个 NUL 字符串时,distinct 工作缓冲上限) */
|
||||
#define MEOW_IPC_CARGO 1024u /* 守护进程入队拷贝上限(可小于 MEOW_IPC_MAX) */
|
||||
|
||||
/* ---- page-cache 治理(OOM_PAGECACHE_GOVERNANCE.md,③混合)----
|
||||
* meow_svc_perf(C:/proc/meminfo Cached 监控降载)把「是否暂停下载/预取」写入此共享状态文件
|
||||
* (一行式 key=value;App/SDK meow_cachegov 只读,系统级单点,不重复起线程)。
|
||||
* 路径可被 env MEOW_CACHEGOV_C_FILE 覆盖(宿主测试隔离)。 */
|
||||
#define MEOW_CACHEGOV_C_FILE "/tmp/meow_cachegov_c.state"
|
||||
|
||||
/* ---- 消息类型(FRAMEWORK_DESIGN §1 表)---- */
|
||||
enum {
|
||||
MSG_NONE = 0x0000,
|
||||
@@ -75,6 +81,7 @@ enum {
|
||||
MSG_PICKER_RESULT = 0x0071, /* ↑ picker 返回值(候选结果回 A;→launcher→amsd) */
|
||||
MSG_META = 0x0072, /* ↑ 媒体会话上报元数据/进度/状态(阶段 D 附录 B) */
|
||||
MSG_PLAYCTL = 0x0073, /* ↓ 播放控制下发/↑ 控制器转发到最近活跃媒体会话 */
|
||||
MSG_META_LYRIC = 0x0074, /* ↑ 媒体会话上报当前曲歌词 lrc/tlrc(阶段 D · 批3,分片) */
|
||||
/* ---- amsd 控制(AMSD_DESIGN §4.2):amsd→launcher,申请拉起/终止某 App ----
|
||||
* 语义:amsd 为「启动/终止」决策者 → launcher(执行器)执行。payload 见下。 */
|
||||
MSG_AMS_START = 0x00A0, /* → 请求 launcher 拉起某 App(pkg + 可选 activity) */
|
||||
@@ -93,6 +100,10 @@ enum {
|
||||
MSG_AUDIO_STATE = 0x00C8, /* ↓ 状态快照文本(NUL 结尾;当前流/参数/帧数/underrun/路由/音量) */
|
||||
MSG_AUDIO_ROUTE = 0x00C9, /* ↑ 手动路由(u32 port:0=auto 2=3.5 3=4.4;调试/面板用) */
|
||||
MSG_AUDIO_VOLUME = 0x00CA, /* ↑ 音量(i32 pct:-1=查询 0-100=设置;v1 透传写 DAC numid=1/2) */
|
||||
/* P2 蓝牙(BLUETOOTH_FULL_OVERHAUL_PLAN §五):输出目标切换。audiod 读 settings.conf
|
||||
* `audio.output_bt`(0/1) 或收本消息改写目标(0=OUT_DAC 1=OUT_BT),重配 PCM。
|
||||
* 与物理路由 numid=9 解耦:蓝牙非物理端口,不写 numid=9、不触发 FPGA 烧录。 */
|
||||
MSG_AUDIO_OUTPUT = 0x00CB, /* ↑ 输出目标(u32 target:0=DAC 1=BT;ui/CLI 下发) */
|
||||
/* ---- meow_powermgr 功耗管理服务(devdocs/plan/POWER_MANAGER_DESIGN.md §6.1):0x00D0-0x00D7 ----
|
||||
* 语义:功耗维度的查看/分析/管理(svc_power 管动作、svc_perf 管性能,powermgr 管功耗)。
|
||||
* 独立 socket /usr/data/meow_pm.sock(--sock/MEOW_PM_SOCK 覆盖)。鉴权分级同 svc_power:
|
||||
@@ -268,6 +279,29 @@ typedef struct m_playctl {
|
||||
int64_t seek_ms; /* cmd=5: 目标位置 ms;否则 0 */
|
||||
} m_playctl_t; /* 4 (+4 pad) + 8 = 16 B(mipsel int64 8 对齐) */
|
||||
|
||||
/* ---- 媒体会话歌词上报(阶段 D · 批3:live lyric 数据源框架)----
|
||||
* App 播放时把当前曲歌词(lrc 原文 + tlrc 翻译)上报到 meow_media 会话,供桌面歌词页/播放页
|
||||
* 随播放位置实时滚动。约束:launcher 环缓冲 `payload[MEOW_IPC_CARGO(1024)]` 为单帧入队上限,
|
||||
* 故歌词按分片传输(每片 ≤ MEOW_LYRIC_CHUNK 字节)。
|
||||
* - seq = 歌曲代际(换歌/换词自增;launcher 见新 seq 清旧词缓存,同曲分片共用)
|
||||
* - kind = 0 原文 lrc / 1 翻译 tlrc
|
||||
* - total= 该 kind 总字节数(不含 NUL;0=清空该 kind)
|
||||
* - off/len = 本片起始偏移 / 有效字节(首片 off=0;分片按序发送)
|
||||
* 边界:lrc 上限 MEOW_MEDIA_LRC_MAX / tlrc 上限 MEOW_MEDIA_TLRC_MAX(会话内每 App 一份缓冲,
|
||||
* 超限截断)。payload 定长且 == MEOW_IPC_CARGO(即 sizeof(m_lyric_t)=1024)。 */
|
||||
#define MEOW_MEDIA_LRC_MAX 3584u /* 会话内歌词原文缓冲上限 */
|
||||
#define MEOW_MEDIA_TLRC_MAX 1568u /* 会话内歌词翻译缓冲上限 */
|
||||
#define MEOW_LYRIC_CHUNK (MEOW_IPC_CARGO - 44u) /* 单分片数据上限(44=头:32+4+2+2+2+2) */
|
||||
typedef struct m_lyric {
|
||||
char app_id[MEOW_APP_ID_MAX]; /* 32: 上报源头(与 HELLO/META 的 app_id 一致) */
|
||||
uint32_t seq; /* 歌曲代际(新歌/新词自增;分片同 seq) */
|
||||
uint16_t kind; /* 0=原文 lrc;1=翻译 tlrc */
|
||||
uint16_t total; /* 该 kind 总字节数(不含 NUL;0=清空该 kind) */
|
||||
uint16_t off; /* 本片起始偏移(≤ total;首片=0) */
|
||||
uint16_t len; /* 本片有效字节(≤ MEOW_LYRIC_CHUNK) */
|
||||
char data[MEOW_LYRIC_CHUNK]; /* 本片内容(len 后 NUL 结尾,接收侧再截断) */
|
||||
} m_lyric_t; /* 44 + 980 = 1024 B = MEOW_IPC_CARGO */
|
||||
|
||||
/* MSG_NOTIFY ↑:推送/更新通知(freedesktop 对齐 + MeowOS 扩展;定长 LE)。
|
||||
* - summary/body/app_icon/expire_timeout 直通 freedesktop 字段;app_icon 即 SDK `icon`
|
||||
* (icon name 或 path;映射为空串时 launcher 用该 app 默认图标回退);
|
||||
@@ -451,24 +485,32 @@ typedef struct m_picker_result {
|
||||
|
||||
/* ================= audiod 独占音频服务(AUDIOD_DESIGN.md) = */
|
||||
|
||||
/* PCM 格式枚举(客户端原生 fmt 与 grant_fmt 共用):1=S16_LE 2=S24_LE 3=S32_LE 4=FLOAT_LE。
|
||||
* v1 实装 S16/S32(S16 直通 + S32 升位);S24 按 32-in-32 处理;FLOAT v1 拒绝。 */
|
||||
/* PCM 格式枚举(客户端原生 fmt 与 grant_fmt 共用):1=S16_LE 2=S24_LE 3=S32_LE 4=FLOAT_LE
|
||||
* 5=AUDIO_FMT_DOP(DoP DSD 透明容器格式)。v1 实装 S16/S32(S16 直通 + S32 升位);S24 按 32-in-32
|
||||
* 处理;FLOAT v1 拒绝。AUDIO_FMT_DOP 为 **独立格式值**(A3+DSD 定案:避免 S24 名义(3B)/实际(4B)歧义),
|
||||
* 仅在 MSG_AUDIO_OPEN.flags 带 AUDIO_FLAG_DSD 时合法;audiod 只做 DoP 容器透传(不引 dsd2pcm)。 */
|
||||
enum {
|
||||
AUDIO_FMT_S16 = 1,
|
||||
AUDIO_FMT_S24 = 2,
|
||||
AUDIO_FMT_S32 = 3,
|
||||
AUDIO_FMT_FLOAT = 4
|
||||
AUDIO_FMT_FLOAT = 4,
|
||||
AUDIO_FMT_DOP = 5 /* DoP DSD(32-bit 容器:bits[31:16]=16bit DSD 数据,bits[15:8]=标记 0xFA/0x05,bits[7:0]=0),
|
||||
容器率 = DSD 位率/16(DSD64→176400 / DSD128→352800 / DSD256→705600) */
|
||||
};
|
||||
|
||||
/* MSG_AUDIO_OPEN.flags 位(预留曾恒 0;A3+DSD 起使用) */
|
||||
#define AUDIO_FLAG_DSD 0x00000001u /* fmt==AUDIO_FMT_DOP:本流为 DoP DSD(App 侧已解码),audiod 只透传 */
|
||||
|
||||
/* MSG_AUDIO_OPEN ↑:请求开流(App 原生声源参数)。grant_*(ACK)回 audiod 实际授权给客户端的
|
||||
* 产出格式(率/声道/位深)——客户端必须按 grant 格式产出 PCM(SDK meow_ui_audio 负责归一:
|
||||
* mono→stereo 复制 / S16↔S32 定值升降位)。ch>2 或 FLOAT → ERR_INVAL。 */
|
||||
* mono→stereo 复制 / S16↔S32 定值升降位)。ch>2 或 FLOAT → ERR_INVAL。fmt==AUDIO_FMT_DOP
|
||||
* 须带 AUDIO_FLAG_DSD(否则 ERR_INVAL);DSD 经 DoP 透传,audiod 不引 dsd2pcm。 */
|
||||
typedef struct m_audio_open {
|
||||
char app_id[MEOW_APP_ID_MAX]; /* 32 */
|
||||
uint32_t rate; /* 原生采样率(应用源,如 44100/48000) */
|
||||
uint32_t rate; /* 原生采样率(应用源,如 44100/48000;DSD 容器率) */
|
||||
uint16_t ch; /* 原生声道数(1=mono 2=stereo) */
|
||||
uint16_t fmt; /* 原生格式(AUDIO_FMT_*) */
|
||||
uint32_t flags; /* 预留(0) */
|
||||
uint16_t fmt; /* 原生格式(AUDIO_FMT_*;DSD 用 AUDIO_FMT_DOP) */
|
||||
uint32_t flags; /* AUDIO_FLAG_DSD 等 */
|
||||
} m_audio_open_t; /* 32+4+2+2+4 = 44 B */
|
||||
|
||||
/* MSG_AUDIO_OPEN_ACK ↓:授权结果。err=MEOW_OK 才有效(stream_id>0 且 grant_* 为实际授权值);
|
||||
@@ -499,11 +541,24 @@ typedef struct m_audio_route {
|
||||
} m_audio_route_t; /* 4 B */
|
||||
|
||||
/* MSG_AUDIO_VOLUME ↑:音量查询/设置。pct<0(-1)=查询当前音量;0-100=设置(写 DAC numid=1/2
|
||||
* + 持久化 settings.conf `audio.volume`,与 watchdog/launcher 同源同键)。 */
|
||||
* + **DSD_VOLUME(音量收编 v2 泛化双组写,保证 DSD↔PCM 切换连续/防爆音)** + 持久化 settings.conf
|
||||
* `audio.volume`,与 watchdog/launcher 同源同键)。BT 输出目标时**不下发 DAC 音量反转**
|
||||
* (音量由 AVRCP/耳机侧控,见 BLUETOOTH_FULL_OVERHAUL_PLAN §五)。 */
|
||||
typedef struct m_audio_volume {
|
||||
int32_t pct; /* -1=查询;0-100=设置 */
|
||||
} m_audio_volume_t; /* 4 B */
|
||||
|
||||
/* 输出目标枚举(msg AUDIO_OUTPUT / settings.conf `audio.output_bt` 共用;亦用于 CLI output) */
|
||||
enum {
|
||||
AUDIO_OUT_DAC = 0, /* 本地 DAC(hw:0,0,独占/period=1024×4/FPGA numid=9/音量反转) */
|
||||
AUDIO_OUT_BT = 1 /* 蓝牙 A2DP source(pcm.bluealsa 播放向,透传+格式归一,AVRCP 控音量) */
|
||||
};
|
||||
|
||||
/* MSG_AUDIO_OUTPUT ↑:切换输出目标(ui/CLI 下发)。0=DAC 1=BT。audiod 侧重配 PCM。 */
|
||||
typedef struct m_audio_output {
|
||||
uint32_t target; /* AUDIO_OUT_DAC / AUDIO_OUT_BT */
|
||||
} m_audio_output_t; /* 4 B */
|
||||
|
||||
/* ================= meow_netmgrd 网络管理服务(devdocs/plan/NETMGR_DESIGN.md §3/§10)=====
|
||||
* 语义:接口统一层(WiFi/BT/USB-eth/rfkill/IP/路由/DNS 只读)查询与控制的系统权威。
|
||||
* 独立 socket /usr/data/meow_net.sock(--sock/MEOW_NET_SOCK 覆盖)。进程形态对齐 powermgr:
|
||||
@@ -601,4 +656,66 @@ typedef struct m_net_dns_result {
|
||||
char text[MEOW_IPC_MAX - 8];
|
||||
} m_net_dns_result_t;
|
||||
|
||||
/* ---- 期 3:管控层(NETMGR_DESIGN §6;默认 allow,按实体手动 block + 域名白/黑)----
|
||||
* 0x00EA MSG_NET_POLICY(同时承载 GET/SET,op 区分)。GET=读级任意;SET=写级仅系统身份
|
||||
* (SO_PEERCRED → /usr/bin/meow_* / os.meow.* / MeowPanel)。防自锁实体(dropbear/wpa_supplicant/
|
||||
* udhcpc/meow_netmgrd/meow_svcd/meow_sysui/init)拒绝置 block(服务端硬校验)。
|
||||
* 策略持久化:`/data/meow/netmgrd/net_policy.conf`(/data=/usr/data 符号链接),读改写单键原子写。 */
|
||||
enum { /* 与主 enum 分列,追加式不改既有行 */
|
||||
MSG_NET_POLICY = 0x00EA, /* ↑ op=0 查询 / op=1 写(set net / set domain / del) */
|
||||
MSG_NET_POLICY_RESULT = 0x00EB /* ↓ 策略文本/结果 */
|
||||
/* 0x00EE 事件推送(预留,期2 订阅制)/ 0xEF 余量(均为预留) */
|
||||
};
|
||||
|
||||
/* MSG_NET_POLICY 操作码 */
|
||||
enum {
|
||||
NET_POLICY_SHOW = 0, /* show [entity]:entity 空=全部 */
|
||||
NET_POLICY_SET_NET = 1, /* set <entity> allow|block */
|
||||
NET_POLICY_SET_DOMAIN = 2, /* domain <entity> +host|-host */
|
||||
NET_POLICY_DEL = 3 /* del <entity> */
|
||||
};
|
||||
|
||||
typedef struct m_net_policy {
|
||||
uint32_t op; /* NET_POLICY_* */
|
||||
char entity[48]; /* app:<pkg> | svc:<svc> | proc:<comm> | global;空=适用 SHOW 的全量 */
|
||||
char arg[64]; /* SET_NET:allow|block;SET_DOMAIN:+host|-host;其余空 */
|
||||
} m_net_policy_t; /* 120 B */
|
||||
|
||||
typedef struct m_net_policy_result {
|
||||
uint32_t code; /* MEOW_OK / MEOW_ERR_PERM / MEOW_ERR_INVAL / (防自锁)MEOW_ERR_PERM */
|
||||
uint16_t text_len;
|
||||
uint16_t pad;
|
||||
char text[MEOW_IPC_MAX - 8];
|
||||
} m_net_policy_result_t;
|
||||
|
||||
/* ---- 期 4:SDK 网络自愿通道(NETMGR_DESIGN §6.3/§4;0xEE/0xEF)----
|
||||
* 一、meow_net_policy_check(host):框架 App 联网前自查 host 是否被 netmgrd 策略放行——
|
||||
* 服务端以调用方 app_id(msg.app_id)→ 实体 app:<pkg>,评估 policy_net_blocked(entity)
|
||||
* || dns_query_domain_denied(entity, host),放行=allow 1 / 拒绝=block 0;netmgrd 不在 → SDK 侧放行。
|
||||
* 二、meow_net_report(rx,tx):App 自愿上报本进程收发字节数(内核无 per-socket/per-uid 计数,
|
||||
* §4 诚实声明的「字节级记账走 SDK 自报」);netmgrd 记入 per-实体统计(status JSON entity_bytes)。
|
||||
* 读级/上报任意调用者(自愿自报,无副作用;block 判定由服务端策略引擎权威执行)。
|
||||
* (0x00EE 曾留作「事件推送 订阅制」,期 4 起改作 SDK 通道;事件推送期 2 从未实装。) */
|
||||
enum { /* 与主 enum 分列,追加式不改既有行 */
|
||||
MSG_NET_SDK = 0x00EE, /* ↑ op=0 策略检查 / op=1 字节上报 */
|
||||
MSG_NET_SDK_RESULT = 0x00EF /* ↓ code + (CHECK)allow + 状态文本 */
|
||||
};
|
||||
enum { NET_SDK_CHECK = 0, NET_SDK_REPORT = 1 };
|
||||
|
||||
typedef struct m_net_sdk {
|
||||
uint32_t op; /* NET_SDK_CHECK / NET_SDK_REPORT */
|
||||
char app_id[MEOW_APP_ID_MAX]; /* 调用方 application(SDK 用 env MEOW_LOG_APP;CLI/测试可任意) */
|
||||
char host[64]; /* CHECK:待查域名 */
|
||||
uint32_t rx; /* REPORT:本进程累计 RX 字节 */
|
||||
uint32_t tx; /* REPORT:本进程累计 TX 字节 */
|
||||
} m_net_sdk_t; /* 4+32+64+4+4 = 108 B */
|
||||
|
||||
typedef struct m_net_sdk_result {
|
||||
uint32_t code; /* MEOW_OK / MEOW_ERR_* */
|
||||
int32_t allow; /* CHECK:1=allow 0=block(block 由服务端策略引擎判定) */
|
||||
uint16_t text_len;
|
||||
uint16_t pad;
|
||||
char text[MEOW_IPC_MAX - 12];
|
||||
} m_net_sdk_result_t;
|
||||
|
||||
#endif /* MEOW_IPC_H */
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* meow_lazygrid.h — SDK 2 维虚拟化宫格(§12.4:meow_lazygrid,M7.4 主界面前置)。
|
||||
*
|
||||
* 定位:给「卡片/图标宫格」提供**二维虚拟化**——应用入口宫格、图标墙、设置快捷卡等。
|
||||
* 只实例化**可见行**(视口内),行高/列几何 + gap;滚动复用 §12.3 meow_scroll(fling/snap),
|
||||
* 与 meow_listview 同款惯性;命中 meow_lazygrid_hit/tap_at 把滚动+网格坐标映射回 item idx。
|
||||
*
|
||||
* 设计(仿 meow_listview 的 vsource 思想 + meow_scroll 滚动 + meow_box 列几何):
|
||||
* - 数据源:count_fn() 返回总项数(任意长),bind_fn(index, item) 仅当 item「进入视口」
|
||||
* (绑定到固定池槽)时调一次;回滚命中同槽不重绑(同 listview 池语义)。
|
||||
* - 固定 item 池:池容量 = 视口行数 + 2(× cols),静态有界(≤MEOW_LAZYGRID_POOL_MAX)。
|
||||
* - 列几何:cols + cell_w + cell_h + gap;行高 row_h = cell_h + gap;按 scroll offset 推可见行。
|
||||
* - 渲染:默认 renderer 画「圆角卡片(icon+名+状态)」;App 也可经 set_item_draw 完全自绘
|
||||
* (主界面卡片宫格用:icon+名+后台标注)。支持选中高亮/禁用变淡/空态占位。
|
||||
* - 滚动:接 meow_scroll(meow_lazygrid_scroll 返回会话;track/fling/snap 与 listview 同款)。
|
||||
*
|
||||
* 内存纪律(M7 §5.2):静态有界 item 池(冷路径 create/set_vsource 分配,热路径 draw/hit/scroll
|
||||
* 零 malloc),与 listview 池同源;不常驻 count 个 item 对象。
|
||||
*
|
||||
* 非 meow_widget:lazygrid 是**自包含控件**(不注册进 z 序/绘制注册表),由宿主(launcher session)
|
||||
* 在 draw 阶段调 meow_lazygrid_draw、命中阶段调 meow_lazygrid_hit/tap_at、滚动喂 meow_scroll——
|
||||
* 与 meow_scroll/meow_box 同为「正交模块」,不侵入 struct meow_widget(全追加式)。
|
||||
*/
|
||||
#ifndef MEOW_UI_LAZYGRID_H
|
||||
#define MEOW_UI_LAZYGRID_H
|
||||
|
||||
#include "meow_ui_types.h" /* meow_img 前向 + 基础类型(子头不引伞头,避免相对路径依赖) */
|
||||
#include "meow_ui_theme.h" /* meow_theme(draw 取语义色) */
|
||||
#include "meow_ui_scroll.h" /* meow_scroll(滚动集成;子头引依赖子头,同 widgets.h→theme.h) */
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct meow_lazygrid meow_lazygrid;
|
||||
|
||||
/* ---- cell / item ---- */
|
||||
/* 单格数据(bind 回调填充;SDK 固定池持有;App 跨 bind 用 user 存业务态/图/index)。 */
|
||||
typedef struct meow_lazygrid_item {
|
||||
const char *title; /* 主文本(名;必填,NULL 视为空格) */
|
||||
const char *subtitle; /* 副文本(状态/后台标注),可空 */
|
||||
const meow_img *icon; /* 可选:格内图标(NULL=无) */
|
||||
int enabled; /* 1=可交互;0=变淡不响应 */
|
||||
void *user; /* App 私有(跨 bind 保存业务态;SDK 不碰) */
|
||||
} meow_lazygrid_item;
|
||||
|
||||
/* 数据源:count 返回总项数(>=0,任意长);bind 在 item 进入视口时调一次填充 item。
|
||||
* 二者均不常驻 item 对象;count 每帧实时查(支持动态长度/搜索过滤)。 */
|
||||
typedef int (*meow_lazygrid_count_fn)(void *ctx);
|
||||
typedef void (*meow_lazygrid_bind_fn)(int index, meow_lazygrid_item *item, void *ctx);
|
||||
|
||||
/* 完全自绘回调(覆盖默认 renderer):index=项号;x,y,w,h=格矩形(已裁剪到宫格视口);
|
||||
* state=MEOW_LAZYGRID_ST_* 状态位(选中/禁用组合)。App 用 §3 绘制 API 自绘整格。 */
|
||||
typedef void (*meow_lazygrid_draw_fn)(int index, int x, int y, int w, int h,
|
||||
int state, void *ctx);
|
||||
/* state 位(draw 回调收到)。 */
|
||||
#define MEOW_LAZYGRID_ST_SELECTED (1u<<0)
|
||||
#define MEOW_LAZYGRID_ST_DISABLED (1u<<1)
|
||||
|
||||
/* 命中/点击回调(idx=项号;App 在 tap_at 命中后触发动作)。 */
|
||||
typedef void (*meow_lazygrid_tap_fn)(int idx, void *ctx);
|
||||
|
||||
#define MEOW_LAZYGRID_POOL_MAX 96 /* 池容量上限(固定;保证静态有界) */
|
||||
|
||||
/* ---- 生命周期(冷路径 create/free;热路径 draw/hit/scroll 零 malloc)----
|
||||
* cols=列数(>=1,越界 clamp);cell_w/cell_h=格宽高;gap=格间距(水平与垂直同用)。 */
|
||||
meow_lazygrid *meow_lazygrid_create(int x, int y, int w, int h, int cols,
|
||||
int cell_w, int cell_h, int gap);
|
||||
void meow_lazygrid_free(meow_lazygrid *g);
|
||||
|
||||
/* ---- 几何配置 ---- */
|
||||
void meow_lazygrid_set_rect(meow_lazygrid *g, int x, int y, int w, int h); /* 视口矩形(绝对坐标) */
|
||||
void meow_lazygrid_set_cols(meow_lazygrid *g, int cols);
|
||||
void meow_lazygrid_set_cell(meow_lazygrid *g, int cell_w, int cell_h, int gap);
|
||||
|
||||
/* ---- 数据源 + 渲染 ---- */
|
||||
int meow_lazygrid_set_vsource(meow_lazygrid *g, meow_lazygrid_count_fn count,
|
||||
meow_lazygrid_bind_fn bind, void *ctx);
|
||||
int meow_lazygrid_set_item_draw(meow_lazygrid *g, meow_lazygrid_draw_fn draw,
|
||||
void *ctx); /* NULL=用默认卡片 renderer */
|
||||
int meow_lazygrid_set_empty_text(meow_lazygrid *g, const char *text);
|
||||
|
||||
/* ---- 滚动(像素级;可视行数按 offset 实时推) ---- */
|
||||
void meow_lazygrid_set_scroll(meow_lazygrid *g, int off);
|
||||
void meow_lazygrid_scroll_by(meow_lazygrid *g, int dy);
|
||||
int meow_lazygrid_scroll(meow_lazygrid *g);
|
||||
int meow_lazygrid_max_scroll(meow_lazygrid *g); /* max(0, 内容高 - 视口高) */
|
||||
int meow_lazygrid_row_h(const meow_lazygrid *g); /* 行高(=cell_h+gap) */
|
||||
/* 便捷:包一个 meow_scroll 会话(set/max/get/redraw 指回本宫格;redraw 只刷视口区)。
|
||||
* App 在 on_touch_move 喂 meow_scroll_track、松手 meow_scroll_fling。返回 NULL=池满。 */
|
||||
meow_scroll *meow_lazygrid_scroll_attach(meow_lazygrid *g);
|
||||
|
||||
/* ---- 选中 / 命中 ---- */
|
||||
void meow_lazygrid_set_on_tap(meow_lazygrid *g, meow_lazygrid_tap_fn cb, void *ctx);
|
||||
void meow_lazygrid_set_sel(meow_lazygrid *g, int idx); /* 越界 clamp;<0 清除;标脏 */
|
||||
int meow_lazygrid_sel(const meow_lazygrid *g);
|
||||
int meow_lazygrid_hit(meow_lazygrid *g, int x, int y); /* 命中项 idx(滚动+网格坐标映射);-1=未命中/禁用 */
|
||||
int meow_lazygrid_tap_at(meow_lazygrid *g, int x, int y); /* 命中:置选中 + 回调 + 返回 idx/-1 */
|
||||
|
||||
/* ---- 绘制 ---- */
|
||||
void meow_lazygrid_draw(meow_lazygrid *g, const meow_theme *th);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MEOW_UI_LAZYGRID_H */
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* meow_select.h — SDK 枚举/预设选择器(§5.8.6 模态组件,M7.2 设置页缺口 #4)。
|
||||
*
|
||||
* 定位:给「枚举 / 预设选择器」一个开箱即用的**下拉模态**——设置页采样率/码率/主题/语言等
|
||||
* 枚举、均衡器预设等。封装 = BottomSheet(标题 + 选项列表)+ meow_listview(SINGLE) 单选列表:
|
||||
* 选项数组 + 当前选中(高亮)+ 点选项 = 选中 + 关闭 + on_change 回调;点面板外/左缘返回 = 取消。
|
||||
*
|
||||
* 设计(§3 缺口 #4):选项用 meow_listview(SINGLE) 渲染(单选高亮、可惯性滚动、虚拟化长列表),
|
||||
* 外层是模态 BottomSheet(复用 meow_dialog 的遮罩/快照恢复,scrim 只叠一层/关闭无残影)。
|
||||
* 阻塞式(内层 meow_input_pop 轮询,与 meow_dialog 同构);打开前排空残留触摸。
|
||||
*
|
||||
* 内存纪律:选项数组定长(≤MEOW_SELECT_MAX_ITEMS,栈/数组持有,非 malloc 长列表);
|
||||
* 列表在 open 时创建(冷路径)、close 时释放;open 循环零 malloc。
|
||||
*/
|
||||
#ifndef MEOW_UI_SELECT_H
|
||||
#define MEOW_UI_SELECT_H
|
||||
|
||||
#include "meow_ui_types.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct meow_select meow_select;
|
||||
|
||||
/* 选中回调(on_change):idx=被选中的选项下标(0..n-1)。 */
|
||||
typedef void (*meow_select_fn)(int idx, void *ctx);
|
||||
|
||||
#define MEOW_SELECT_MAX_ITEMS 32 /* 选项上限(静态数组;越界 add 忽略并返回 -1) */
|
||||
|
||||
/* ---- 组件式(可复用 + on_change 回调)---- */
|
||||
meow_select *meow_select_create(const char *title, int cur); /* cur=初始选中项;越界 clamp */
|
||||
int meow_select_add_item(meow_select *s, const char *label); /* 返回项下标;满/无效= -1 */
|
||||
void meow_select_set_on_change(meow_select *s, meow_select_fn cb, void *ctx);
|
||||
/* 打开模态(阻塞到确认/取消):选中 → 置 result=idx + 调 on_change(idx)+ 返回 idx;
|
||||
* 取消/点面板外/左缘返回 → 返回 -1(不调 on_change)。 */
|
||||
int meow_select_open(meow_select *s);
|
||||
void meow_select_free(meow_select *s);
|
||||
|
||||
/* ---- 一次性(无回调,返回值即选择)----
|
||||
* opts=选项数组(const char* 数组);n=个数;cur=初始选中。返回选中下标/-1=取消。 */
|
||||
int meow_select_show(const char *title, const char *const *opts, int n, int cur);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MEOW_UI_SELECT_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 */
|
||||
@@ -38,6 +38,7 @@ typedef struct meow_audio_state {
|
||||
int paused; /* 当前流是否暂停 */
|
||||
int route_port; /* 当前路由口(-1 未知) */
|
||||
int vol; /* 当前音量 0-100 */
|
||||
int dsd; /* 当前流是否为 DoP DSD(fmt==AUDIO_FMT_DOP 且 audiod 已授权) */
|
||||
} meow_audio_state;
|
||||
|
||||
/* 被抢占回调:by_app=抢占者 app_id(非空)。ctx 由 on_preempt 注册时传入。 */
|
||||
@@ -48,6 +49,10 @@ typedef void (*meow_audio_preempt_fn)(const char *by_app, void *ctx);
|
||||
* MEOW_ERR_NODEV=无可用 PCM 设备;MEOW_ERR_* 常量本身即负值)。open 后客户端须按内部记录的
|
||||
* grant 格式产出 PCM。 */
|
||||
int meow_audio_open(int rate, int ch, int fmt);
|
||||
/* 开流(带 flags)。flags=AUDIO_FLAG_DSD 且 fmt=AUDIO_FMT_DOP 时为 DoP DSD 流(A3+DSD 定案):
|
||||
* App 侧须把 DSD 解码封装为 DoP(每样本 32-bit 容器:16bit DSD 数据 + 标记 + 0),audiod 只透传
|
||||
* (不引 dsd2pcm、不做 DOP_EN 之外的任何处理)。DSD 打不开/不支持 → open 返回负错误码(报错关流)。 */
|
||||
int meow_audio_open_ex(int rate, int ch, int fmt, uint32_t flags);
|
||||
/* 写 PCM。pcm=native fmt/ch 样本(open 传的参数),frames=帧数;内部归一成 grant 并分批发送。
|
||||
* 返回写入帧数;<0=流已死(被抢占/audiod 断开),App 应重新 open。阻塞(socket 背压)。 */
|
||||
int meow_audio_write(const void *pcm, int frames);
|
||||
@@ -57,6 +62,10 @@ int meow_audio_pause(int sid, int paused);
|
||||
int meow_audio_close(int sid);
|
||||
/* 查询 audiod 快照(填充 out)。返回 0 成功;<0 失败。 */
|
||||
int meow_audio_query(meow_audio_state *out);
|
||||
/* 设置音量 0-100(audiod 唯一权威:写 DAC numid=1/2 + DSD_VOLUME 双组保切换连续 + 持久化
|
||||
* settings.conf audio.volume,与 launcher/watchdog 同源同键)。BT 输出目标时音量由 AVRCP 侧控,
|
||||
* audiod 不下发 DAC 反转。返回 0 成功;<0 失败(audiod 不在)。 */
|
||||
int meow_audio_set_volume(int pct);
|
||||
/* 注册被抢占回调(可选;内部读线程分发)。重复调用幂等。 */
|
||||
void meow_audio_on_preempt(meow_audio_preempt_fn cb, void *ctx);
|
||||
|
||||
|
||||
@@ -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 */
|
||||
@@ -50,6 +50,24 @@ void meow_ui_flush_region(int x, int y, int w, int h); /* 只刷新指定区 *
|
||||
/* 半透明(blend565:前景按 alpha 与背景混叠) */
|
||||
void meow_ui_fill_rect_alpha(int x, int y, int w, int h, uint32_t color, int alpha); /* 0-255 */
|
||||
|
||||
/* ---- RGB565 页图(M32)----
|
||||
* 页槽常驻 RGB565(2 字节/像素):真机 480×853 页 1599KB → 800KB。
|
||||
* 量化与 back_buf 用同一个 565 转换(逐位截断),565 屏上最终像素与 RGBA blit
|
||||
* 路径逐像素一致——不引入色偏、不会比现在多出色带。
|
||||
* 命中页(页图尺寸 == 显示尺寸)走 meow_gfx_blit_rgb565:每行一次拷贝,无重采样。
|
||||
*
|
||||
* alpha:565 页图**按不透明**处理(alpha 被忽略)。漫画页解码后 alpha 恒为 255,
|
||||
* 与旧路径等价;带透明像素的 PNG(调色板/tRNS/color type 6、或 32bpp BMP)会有可见
|
||||
* 差异:旧 RGBA 路径把透明像素混合到阅读器底色,565 路径直接画它的 RGB(通常是黑)。
|
||||
* 若将来要支持透明页,需要在上屏时按 alpha 混合(而不是在落槽时丢通道)。 */
|
||||
uint16_t meow_gfx_rgb565(uint32_t rgb); /* 24-bit RGB → 565(与 back_buf 同式) */
|
||||
void meow_gfx_rgba_to_rgb565(const uint8_t *rgba, uint16_t *out, int npx); /* 不透明像素(忽略 alpha) */
|
||||
/* 原生尺寸 blit(不透明):16bpp back_buf 上是每行一次 memcpy。 */
|
||||
void meow_gfx_blit_rgb565(int x, int y, int w, int h, const uint16_t *px);
|
||||
/* 最近邻缩放 blit(几何不符时的兜底;采样点与 meow_img_resample 同式)。 */
|
||||
void meow_gfx_blit_rgb565_scaled(int x, int y, int w, int h,
|
||||
int sw, int sh, const uint16_t *px);
|
||||
|
||||
/* ============================================================ */
|
||||
/* §7 动画/时间(B1 最小可用) */
|
||||
/* ============================================================ */
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/*
|
||||
* meow_ui_image.h — SDK 图片层(§4,B1)。
|
||||
*
|
||||
* 原 meow_ui.h 拆分的一部分(image 模块)。内部 stb_image(单头文件,静态编入 meow_ui.a)
|
||||
* 原 meow_ui.h 拆分的一部分(image 模块)。解码后端:M23 真缩解(JPEG DCT 域 / PNG 逐行 /
|
||||
* BMP 跳行,见 meow_jpeg.h)优先,回退 stb_image(单头文件,静态编入 meow_ui.a)。
|
||||
* 解 PNG/JPEG/BMP,统一转 RGBA。meow_img 为不透明句柄(前向声明见 meow_ui_types.h)。
|
||||
*/
|
||||
#ifndef MEOW_UI_IMAGE_H
|
||||
@@ -22,10 +23,70 @@ meow_img *meow_img_load_file(const char *path); /* 内部读文件 + stb 解
|
||||
meow_img *meow_img_load_mem(const void *buf, int len);
|
||||
void meow_img_free(meow_img *img);
|
||||
|
||||
int meow_img_w(meow_img *img);
|
||||
int meow_img_h(meow_img *img);
|
||||
/* 追加(M12)预算解码:只读文件头取原始尺寸,不解码像素(零像素内存)。成功 1,失败 0。
|
||||
* App 可用它在加载前判断「图太大」并给用户提示,而不必先撞 OOM。 */
|
||||
int meow_img_probe_file(const char *path, int *w, int *h, int *channels);
|
||||
|
||||
/* 追加(M12/M23)预算解码:解码期受长边约束,返回图最长边 ≤ maxdim,尺寸与
|
||||
* 「meow_img_load_file + meow_img_downscale(maxdim)」逐字节同口径(公式一致)。
|
||||
*
|
||||
* 实现(M23 起):先进真缩解后端(内部 meow_jpeg.c / meow_png_stream.c),解码期只算目标分辨率:
|
||||
* JPEG:DCT 域缩解(每 8×8 块只解出 N×N 像素,N 由 8·maxdim/长边 取「不欠采样」的最小值;
|
||||
* N=1..7,N≥8 交回 stb),渐进式跳过精细化扫描、只解各频带首次扫描 —— 低频带已完整,
|
||||
* 不整幅驻留系数阵列;
|
||||
* PNG :libz 逐行 inflate + 反滤波,按步长丢行丢列直写目标尺寸(gray/gray+A/RGB/RGBA/
|
||||
* 调色板/1-16bit,16bit 取高字节,tRNS 透明含 8bit 灰/RGB);
|
||||
* BMP :按目标行 seek 到源行 + 行内跳列(8bpp 调色板 / 16bpp RGB555 / 24bpp / 32bpp)。
|
||||
* 峰值只与输出尺寸相关,不随源图分辨率/压缩体积线性增长(2000×3000 页 ≤2.5MB 量级)。
|
||||
* 不支持的特性(CMYK/YCCK 4 分量 JPEG、算术编码、过订阅 Huffman 表、16bit 量化表、Adam7
|
||||
* 交错 PNG、RLE/1-4bpp BMP、几乎等尺寸)自动回退 stb 旧路径,对外语义完全不变。
|
||||
* 小图(长边 ≤ maxdim)走旧路径,与原 API 完全一致;maxdim<=0 等价旧入口。失败返回 NULL。
|
||||
*
|
||||
* 尺寸口径说明(M23 评审 P2-2):本入口按「原图 + meow_img_downscale」公式算目标尺寸;M12 旧
|
||||
* 实现经 stb 的 ceil(w/s) 缩解再精修,源尺寸不是档位整数倍时可能与本入口差 1px(实测 1999×2999:
|
||||
* 旧 320×480 vs 新 319×480)。阅读器按显示框缩放,无实际影响;PNG/BMP 无此差异。 */
|
||||
meow_img *meow_img_load_file_dim(const char *path, int maxdim);
|
||||
meow_img *meow_img_load_mem_dim(const void *buf, int len, int maxdim);
|
||||
|
||||
/* 追加(M22)缩解档位选择:取满足「长边/档位 ≥ maxdim」的最大整数档 s ∈ {1,2,4,8},
|
||||
* 保证缩解结果不欠采样。meow_img_load_file_dim 内部用的就是这个函数;调用方(阅读器)
|
||||
* 用它枚举候选档位做内存预算,避免两处各写一份而漂移。 */
|
||||
int meow_img_pick_scale(int longside, int maxdim);
|
||||
|
||||
/* 追加(M22)显式档位解码:语义与 meow_img_load_file_dim 相同(结果长边 ≤ maxdim,
|
||||
* 超了就再精修一次),只是缩解档位由调用方决定。内存吃紧时可以选更粗的档位——输出更小、
|
||||
* 峰值更低,代价是细节变软。step <= 1 等价长边约束入口;step > 8 按 8 处理。
|
||||
* M23 起:先走真缩解后端(step 折算成目标长边 ceil(长边/step),后端按该长边选 DCT 阶),
|
||||
* 峰值只与输出尺寸相关;不支持的特性回退 stb(step 原样传入)。
|
||||
* M27 起 PNG/BMP 也走同一后端入口(阅读器峰值模型已同步为「目标图 + 工作面」口径);
|
||||
* 断言与实测见 tuku/test/test_reader_membudget.c 的 B7。 */
|
||||
meow_img *meow_img_load_file_step(const char *path, int maxdim, int step);
|
||||
|
||||
/* 追加(M27):与 meow_img_load_file_step 相同,额外控制回退并回报**实际**路径:
|
||||
* allow_stb_fallback = 1:后端不支持/失败时回退 stb(与 _step 完全一致);
|
||||
* allow_stb_fallback = 0:只尝试后端,失败返回 NULL——**绝不**回落 stb。阅读器的门控
|
||||
* 按「后端峰值」放行时必须用 0,否则头部判定通过、解码中途失败的文件会静默走 stb,
|
||||
* 实测可放出 18–36MB(截断 12MP JPEG / 头-only BMP / 非法位深 PNG)。失败页由调用方
|
||||
* 按坏图占位。
|
||||
* used_backend = 1(M23 后端直出目标尺寸)/ 0(未走成后端)。NULL 时等价 _step(允许回退)。 */
|
||||
meow_img *meow_img_load_file_step_ex(const char *path, int maxdim, int step,
|
||||
int allow_stb_fallback, int *used_backend);
|
||||
|
||||
/* 追加(M22)精确重采样到 w×h(最近邻,缩小/放大都支持)。阅读器用它把「缩解档位」的
|
||||
* 结果一次性落到显示框,使命中页仍是单次原生 blit(不做每帧重采样)。同尺寸返回 NULL
|
||||
* (调用方保留原图);失败返回 NULL。 */
|
||||
meow_img *meow_img_resample(meow_img *im, int w, int h);
|
||||
|
||||
int meow_img_w(const meow_img *img);
|
||||
int meow_img_h(const meow_img *img);
|
||||
/* 访问器:取 RGBA 像素指针(4 通道 straight alpha;供取色/直读像素)。meow_img 对外 opaque,
|
||||
* 本函数为其唯一像素读入口;NULL 返回 NULL。 */
|
||||
const unsigned char *meow_img_pixels(const 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 */
|
||||
|
||||
@@ -36,6 +36,13 @@ void meow_ui_request_exit(void); /* 请求退出(相当于返回/
|
||||
* x<=0 时清掉此前画的条(填回背景色)。不发 flush,由调用方 meow_ui_flush_region 上屏。 */
|
||||
void meow_ui_swipe_hint(int x);
|
||||
|
||||
/* 批②(2026-09-04):底部热区上滑 → 回桌面(home)判定,镜像 launcher np_gesture.h
|
||||
* np_home_gesture 同口径。down_x/down_y=抬手原点,dx/dy=抬手位移(释放-原点),
|
||||
* fb_h=屏高(SDK 传 meow_ui_height())。命中=起点在底部热区(最后 HOME_EDGE_H≈28px)且
|
||||
* **不在左缘**(down_x>=EDGE_W,与左缘返回互斥)+ 上移 ≥HOME_SWIPE(65px) + 竖向主导(adx<-dy)。
|
||||
* 供宿主单测直调;触摸状态机(meow_ui_input.c touch_release)与 App 内判据共用,避免两端漂移。 */
|
||||
int meow_ui_input_is_home_swipe(int down_x, int down_y, int dx, int dy, int fb_h);
|
||||
|
||||
/* ============================================================ */
|
||||
/* §6.3 通用长按(2026-08-31) */
|
||||
/* ============================================================ */
|
||||
@@ -58,6 +65,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
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* meow_ui_net.h — MeowOS SDK 网络自愿通道(NETMGR_DESIGN §6.3/§4;追加式 API,含入 meow_ui.h)。
|
||||
*
|
||||
* 定位:框架 App 联网前的**自愿自治**通道 + **自愿字节上报**,二者都不强制(防君子不防小人)。
|
||||
* - meow_net_policy_check(host):联网前自查 host 是否被 netmgrd 管控策略放行。
|
||||
* 返回 0=allow(可联网);1=block(策略拒绝,App 应中止本次联网)。
|
||||
* netmgrd 不在 / 超时 → 0(乐观放行,绝不因监控不可用而误封业务)。
|
||||
* - meow_net_report(rx,tx):App 自愿上报本进程累计收发字节。内核无 per-socket/per-uid
|
||||
* 字节计数器(NETMGR_DESIGN §4 表),§4 的「字节级精确记账走 SDK 自报通道」由此落地;
|
||||
* netmgrd 记入 per-实体统计(status JSON `entity_bytes`)。best-effort,失败静默不影响业务。
|
||||
* 用法:App 在发起 HTTP/TLS 前调 meow_net_policy_check(host),若返回 1 则按产品语义处理(如提示被限)。
|
||||
*/
|
||||
#ifndef MEOW_UI_NET_H
|
||||
#define MEOW_UI_NET_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* 检查 host 是否被策略放行。0=allow;1=block;netmgrd 不在/超时 → 0(乐观放行)。
|
||||
* host 为域名(如 "music.163.com");判定以当前 App(MEOW_LOG_APP)为实体,服务端策略引擎权威执行。 */
|
||||
int meow_net_policy_check(const char *host);
|
||||
|
||||
/* 上报本进程累计收发字节(best-effort,失败静默)。 */
|
||||
void meow_net_report(uint32_t rx, uint32_t tx);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* MEOW_UI_NET_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 */
|
||||
@@ -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 覆盖表:`<MEOW_DATA_DIR>/../strings/<locale>.txt`(即 MeowData/<pkg>/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 */
|
||||
@@ -62,6 +62,28 @@ void meow_theme_remove_listener(meow_theme_listener cb);
|
||||
/* [D4] 脱机访问两套内置色板(无 settings 文件也可靠):返回当前解析生效的色板副本。 */
|
||||
meow_theme meow_theme_defaults(meow_theme_mode m);
|
||||
|
||||
/* ---- §8.1.5 色板导出/注入(单一权威桥,M7.0a)----
|
||||
* 用户在 M7 定案「SDK meow_theme 为唯一权威」:launcher 的 s_ui_theme() 将改为读 SDK 的薄壳,
|
||||
* 不再自持 4 套静态色板。本接口是那个前置(launcher 侧改动不在此批)——
|
||||
*
|
||||
* meow_theme_export(out) 把当前「style×mode」解析生效的完整权威主题拷给调用方。
|
||||
* 调用方是 stateless 的绘制原语时(如 launcher s_ui_theme),由此读
|
||||
* 单一权威,再按它的角色字段(9 色)映射到自己的绘制便利结构,即:
|
||||
* surface → surface
|
||||
* surfaceVariant → surface_variant
|
||||
* surfaceContainer → surface_container
|
||||
* primary → primary
|
||||
* onSurface → on_surface
|
||||
* onSurfaceVariant → on_surface_variant
|
||||
* outline → outline
|
||||
* error → error
|
||||
* surfaceTint → surface_tint
|
||||
* meow_theme_inject(src) 用一份自定义板覆写当前生效主题(如 launcher 自定义背景/强调/风格),
|
||||
* src==NULL 恢复内建(随 settings 解析)。覆写不随 meow_theme_reload() 丢弃
|
||||
* (程序化覆写优先于 settings,与 meow_ui_set_style 语义一致)。 */
|
||||
void meow_theme_export(meow_theme *out);
|
||||
void meow_theme_inject(const meow_theme *src);
|
||||
|
||||
/* ---- §8.1 顶级主题风格族(minimal,2026-08-30)----
|
||||
* 主题 = 顶级主题风格族(ui.style:MD3/MIUIx)× 明暗(display.theme)。本批只落「状态栏图标集
|
||||
* 随 ui.style 换集」的最小闭环:meow_ui_set_style 记录 style(同值忽略)+ 触发 theme_reload/
|
||||
|
||||
@@ -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);
|
||||
@@ -47,6 +49,7 @@ int meow_btn_pressed(meow_widget *w); /* 应用在 on_tap 命中后
|
||||
/* ---- §5.8.2 Modifier 式通用 Options(轻量结构体,非链式 DSL)----
|
||||
* 经 meow_widget_set_opts 一次性挂载,作用于任意控件(标准/自定义)。全部控件共用。
|
||||
* 哨兵语义:bg/fg==0=用控件/主题默认;stroke==0=无描边;stroke_w=描边宽;
|
||||
* 控件语义:W_PROGRESS 用 bg=轨道(0=surfaceVariant)、fg=填充(0=primary);见 meow_ui_widget.c 批5b;
|
||||
* radius==-1=控件默认(如按钮 10),>=0=指定圆角(0=直角);pad/margin=0=无;
|
||||
* disabled/selected=1=置位(立即反映到状态机位)。 */
|
||||
typedef struct meow_widget_opts {
|
||||
@@ -143,6 +146,17 @@ enum {
|
||||
meow_widget *meow_btn_create_variant(int x, int y, int w, int h, const char *label, int variant);
|
||||
int meow_btn_set_variant(meow_widget *w, int variant); /* 越界 clamp;0=成功 */
|
||||
|
||||
/* ---- MD3 shape 层级(§5.8.6,Material You 圆角体系,单位 px)----
|
||||
* 组件按角色落到五档 + 胶囊:按钮=full(全高一半)、对话框=extra-large(28)、卡=large(16)、
|
||||
* 输入框=small(8)、勾选=extrasmall(4)。theme.radius/radius_card 为风格族默认;App 可经
|
||||
* opts.radius 逐控件覆盖。全高一半用 MEOW_SHAPE_FULL(h) 宏(控件的胶囊圆角)。 */
|
||||
#define MEOW_SHAPE_EXTRASMALL 4 /* 勾选 Checkbox/Radio */
|
||||
#define MEOW_SHAPE_SMALL 8 /* 输入框 TextField */
|
||||
#define MEOW_SHAPE_MEDIUM 12 /* 主题默认 radius(列表卡/通用控件) */
|
||||
#define MEOW_SHAPE_LARGE 16 /* 卡片/容器(theme.radius_card 默认) */
|
||||
#define MEOW_SHAPE_EXTRA_LARGE 28 /* 对话框 Dialog(MD3 标准 extra-large) */
|
||||
#define MEOW_SHAPE_FULL(h) ((h) / 2) /* 全形=胶囊(按钮/导航等) */
|
||||
|
||||
/* ---- TextField(单行文本输入,接 IME §5.1)---- */
|
||||
/* placeholder=提示文案;buf=App 持有的文本缓冲(组件不回拷,只绘制/编辑其上);maxlen=含 NUL 预算 */
|
||||
meow_widget *meow_textfield_create(int x, int y, int w, int h, const char *placeholder,
|
||||
@@ -184,6 +198,40 @@ 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,
|
||||
};
|
||||
|
||||
/* ---- IconButton(圆形/位图图标按钮,M7.0a 缺口#2)----
|
||||
* 圆形命中 + 位图图标(sb_icons[],Material Symbols Rounded 单通道 alpha)+ 按压态(实时)。
|
||||
* 用途:电源菜单圆钮 / Now Playing 播放控制 / 锁屏常用件等「位图图标 + 圆形」按钮。
|
||||
* icon_idx = sb_icons[] 下标(用上面 MEOW_ICON_* 枚举或 SB_I_*;越界画空位图,命中照常)。
|
||||
* size = 按钮直径(外接正方形 w=h=size;图标 ink 高 = size*6/10,MD3 图标比)。
|
||||
* 语义色:底 surfaceContainerLow、图标 onSurface;按压叠 onSurface 状态层(实时,同 meow_btn);
|
||||
* 禁用变淡;选中态 primaryContainer/onPrimaryContainer(meow_widget_set_selected)。
|
||||
* 命中:圆形(半径=size/2,圆心=按钮中心),owner==active + enabled 双闸门。
|
||||
* 返回按钮句柄;App 在 on_tap 里用 meow_widget_hit 判定是否命中本钮(命中即触发动作)。 */
|
||||
meow_widget *meow_iconbtn_create(int x, int y, int size, int icon_idx);
|
||||
|
||||
/* ---- NavigationBar(底部导航栏)---- */
|
||||
meow_widget *meow_navigationbar_create(int x, int y, int w, int h);
|
||||
@@ -218,6 +266,10 @@ int meow_list_selected(meow_widget *w);
|
||||
typedef void (*meow_list_tap_fn)(int idx, void *ctx);
|
||||
void meow_list_set_on_tap(meow_widget *w, meow_list_tap_fn cb, void *ctx); /* 命中行→回调 */
|
||||
int meow_list_tap_at(meow_widget *w, int x, int y); /* 命中行:置选中+回调,返回 idx/-1 */
|
||||
/* MD3 容器模式(W_LIST 共用,§5.5/§5.8.6):0=扁平满宽项+分隔线(回退);1=每项外套 surfaceContainer
|
||||
* 圆角卡+项间空隙(去分隔线),选中=primaryContainer/按压=primary α26(卡内 tonal)。
|
||||
* 覆盖 meow_list_create / meow_lazycolumn_create / meow_lazyrow_create。返回 0=成功;无效=负值。 */
|
||||
int meow_list_set_container(meow_widget *w, int on);
|
||||
|
||||
/* ============================================================ */
|
||||
/* §5.5 设计模板 A② 结构化列表(W_LISTVIEW):行=图标+标题+副标题+右侧操作 + 选中
|
||||
@@ -291,6 +343,89 @@ 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);
|
||||
/* MD3 容器模式(默认 renderer 可选,P0 对齐):0=扁平满宽行+分隔线(默认,兼容);
|
||||
* 1=每行外套 surfaceContainer 圆角卡(12dp)+行间空隙,选中=primaryContainer/按压=primary α
|
||||
* (tonal,非满幅实心),去冗余分隔线;段头=surface 上的 primary 段标签(无卡无分隔线)。
|
||||
* 注意:仅对默认 renderer(未 set_item_draw)生效;完全自绘行由 App self-draw 自理。 */
|
||||
int meow_listview_set_container(meow_widget *lv, int on);
|
||||
/* 空态占位:count==0 时视口内居中画 empty_text(NULL 不画)。返回 0=成功。 */
|
||||
int meow_listview_set_empty_text(meow_widget *lv, const char *text);
|
||||
|
||||
/* ============================================================ */
|
||||
/* §5.5 左滑删除(M7.3 通知中心缺口 #9):列表行横向手势 + 删除/回弹动画 */
|
||||
/* ------------------------------------------------------------
|
||||
* 行自绘 set_item_draw 配合(SDK 在自绘行前把该行横向偏移 swipe_dx 传给 x,并画右露删除区)。
|
||||
* 手势由 App 在 on_touch_move 喂三级:begin(touch-down)/ move(touch-move)/ end(release)。
|
||||
* 判定:横向小位移|dx|>|dy| 且超容差=swipe(move 返回 1=本列表消费,App 勿滚动);纵向
|
||||
* |dy|>|dx| 且超容差=滚动手势优先(返回 0 并取消本行 swipe,App 照常 scroll_by);点按
|
||||
* 无位移(end 时未 started)=行点击(返回 0,App 照常 tap_at)。不修改 tap_at/滚动语义。
|
||||
* 动画:超阈值 → 删除动画(行滑出 meow_anim,结束后回调 on_delete(idx));未超 → 回弹。
|
||||
* 颜色:删除区用主题 error/onError;不与行点击/滚动冲突(见判定表)。 */
|
||||
typedef void (*meow_listview_swipe_fn)(int idx, void *ctx);
|
||||
void meow_listview_set_swipe_delete(meow_widget *lv, int enabled,
|
||||
meow_listview_swipe_fn on_delete, void *ctx);
|
||||
int meow_listview_swipe_begin(meow_widget *lv, int x, int y); /* touch-down:1=命中可滑行(喂后续 move) */
|
||||
int meow_listview_swipe_move(meow_widget *lv, int x, int y); /* touch-move:1=消费(横向);0=让位(取消/纵向) */
|
||||
int meow_listview_swipe_end(meow_widget *lv, int x, int y); /* release:1=消费(删/回弹动画);0=未滑动(tap) */
|
||||
int meow_listview_swipe_active(meow_widget *lv); /* 1=有行处于滑动/动画中 */
|
||||
int meow_listview_swipe_offset(meow_widget *lv); /* 当前滑动行的 dx(<0=左移;自绘行画用) */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
/* sdk/meow_cachegov.c — 磁盘缓存 page-cache 治理 SDK 库(A:DONTNEED 主导侧)。
|
||||
*
|
||||
* 权威方案:devdocs/plan/OOM_PAGECACHE_GOVERNANCE.md。归属(ARCHITECTURE_PRINCIPLES ③混合):
|
||||
* A 热路径(register/tick/drop_finished/refault)随 SDK 库;C(/proc/meminfo Cached 监控)
|
||||
* 归 meow_svc_perf,本库只读其共享状态文件 MEOW_CACHEGOV_C_FILE 作为降载信号。
|
||||
*
|
||||
* 红线(务必保留本注释):**L2 禁止**——绝不丢 `[read_pos, ...)` 任何未解码区(含 read_pos 前方
|
||||
* 刚写入尚未解码的页)。本库所有 drop 的上界只能是 read_pos-keep_hot(L1)或整文件曲终(L0)。
|
||||
*
|
||||
* 分层:只依赖 pthread + meow_log + POSIX(posix_fadvise),不依赖 core/App 其它 TU。
|
||||
* 自含:refault 滚窗 + 三级回退(放大 keep_hot → 降 L0 → 关 A)。
|
||||
*/
|
||||
#include "include/meow_cachegov.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* meow_ui.h:SDK 公共伞头(含 meow_log/meow_ui_log.h + 公共类型)。 */
|
||||
#include "meow_ui.h"
|
||||
/* 共享协议头:C 侧状态文件路径(meow_svc_perf 写入 / 各 App 读取,同源防漂移)。 */
|
||||
#include "meow_ipc.h"
|
||||
|
||||
#define MEOW_CACHEGOV_MAXREG 8 /* 同时治理的缓存文件上限(ncm 单点播放,远小于此) */
|
||||
#define MEOW_CACHEGOV_REFAULTRING 64 /* refault 滚窗环形缓冲 */
|
||||
#define MEOW_CACHEGOV_WINDOW_MS 10000 /* refault 滚窗 10s */
|
||||
#define MEOW_CACHEGOV_HOTESCALATE 1 /* 回退档>=1 时 keep_hot 放大倍数(翻倍) */
|
||||
#define MEOW_CACHEGOV_DROP_THROTTLE (64u * 1024u) /* L1 每次至少推进 64KB 才再 drop(省 open/fadvise) */
|
||||
|
||||
typedef struct cg_entry {
|
||||
char path[512];
|
||||
uint64_t size; /* 标称大小(流式=nominal;命中=实际) */
|
||||
uint64_t (*read_pos_fn)(void *ctx);
|
||||
void *ctx;
|
||||
uint64_t dropped_to; /* 已 DONTNEED 的已读前缀上界(只推进,不回退) */
|
||||
int used;
|
||||
} cg_entry_t;
|
||||
|
||||
static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER;
|
||||
static struct meow_cachegov_cfg g_cfg = {
|
||||
.enabled = 1,
|
||||
.cached_threshold_kb = 0, /* C 归属 svc_perf,本库只读 */
|
||||
.dirty_warn_kb = 0, /* 同上 */
|
||||
.keep_hot_bytes = (256u * 1024u), /* 保 256KB 解码窗口供倒带 */
|
||||
.min_hot_ahead = (0), /* L2 红线防御值;绝不参与 drop */
|
||||
.drop_mode = 0, /* 默认仅曲终(L0);L1 需显式开(带 seek 回退) */
|
||||
.refault_backoff = 3, /* 10s 内 3 次 refault → 回退 */
|
||||
};
|
||||
static cg_entry_t g_reg[MEOW_CACHEGOV_MAXREG];
|
||||
static int g_backoff = 0; /* 0=正常 1=keep_hot放大 2=降L0 3=A关 */
|
||||
static int64_t g_last_step_ms = -1000000000000LL; /* 上次升/降档时刻(初始很远 → 首次可动作) */
|
||||
static int64_t g_refault_ring[MEOW_CACHEGOV_REFAULTRING];
|
||||
static int g_refault_head = 0;
|
||||
static int g_refault_n = 0;
|
||||
|
||||
/* ---- 时钟(宿主测试桩可注入) ---- */
|
||||
static int64_t g_test_now_ms = -1;
|
||||
static int64_t now_ms(void)
|
||||
{
|
||||
if (g_test_now_ms >= 0) return g_test_now_ms;
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
||||
}
|
||||
void meow_cachegov_test_set_time_ms(int64_t t) { g_test_now_ms = t; }
|
||||
|
||||
/* ---- C 侧状态文件(svc_perf 写 / 本库读) ---- */
|
||||
static const char *c_file(void)
|
||||
{
|
||||
const char *e = getenv("MEOW_CACHEGOV_C_FILE");
|
||||
return (e && e[0]) ? e : MEOW_CACHEGOV_C_FILE;
|
||||
}
|
||||
|
||||
/* C 状态(svc_perf 写)最近一次解析缓存:下载 cb 每 4KB 块会查一次,避免每块都读文件(200ms TTL)。 */
|
||||
static int g_cached_c_dl = 0, g_cached_c_pf = 0;
|
||||
static int64_t g_cached_c_at = 0;
|
||||
|
||||
/* 读 C 状态文件(缺省 0=不降载)。解析失败/文件不存在 → 全部 0。 */
|
||||
static void read_c_state(int *dl_pause, int *pf_pause, long *cached_kb, long *dirty_kb)
|
||||
{
|
||||
if (dl_pause) *dl_pause = 0;
|
||||
if (pf_pause) *pf_pause = 0;
|
||||
if (cached_kb) *cached_kb = 0;
|
||||
if (dirty_kb) *dirty_kb = 0;
|
||||
FILE *f = fopen(c_file(), "r");
|
||||
if (!f) return;
|
||||
char line[160];
|
||||
while (fgets(line, sizeof line, f)) {
|
||||
if (!strncmp(line, "cached_kb=", 10)) { if (cached_kb) *cached_kb = atol(line + 10); }
|
||||
else if (!strncmp(line, "dirty_kb=", 9)) { if (dirty_kb) *dirty_kb = atol(line + 9); }
|
||||
else if (!strncmp(line, "dl_pause=", 9)) { if (dl_pause) *dl_pause = atol(line + 9) ? 1 : 0; }
|
||||
else if (!strncmp(line, "prefetch_pause=", 15)) { if (pf_pause) *pf_pause = atol(line + 15) ? 1 : 0; }
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
/* ---- refault 滚窗 ---- */
|
||||
void meow_cachegov_note_refault(void)
|
||||
{
|
||||
int64_t t = now_ms();
|
||||
pthread_mutex_lock(&g_mu);
|
||||
g_refault_ring[g_refault_head] = t;
|
||||
g_refault_head = (g_refault_head + 1) % MEOW_CACHEGOV_REFAULTRING;
|
||||
if (g_refault_n < MEOW_CACHEGOV_REFAULTRING) g_refault_n++;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
}
|
||||
|
||||
static int refaults_last_window(void)
|
||||
{
|
||||
int64_t now = now_ms();
|
||||
int cnt = 0, start = (g_refault_head - g_refault_n + MEOW_CACHEGOV_REFAULTRING) %
|
||||
MEOW_CACHEGOV_REFAULTRING;
|
||||
for (int i = 0; i < g_refault_n; i++) {
|
||||
int64_t t = g_refault_ring[(start + i) % MEOW_CACHEGOV_REFAULTRING];
|
||||
if (t >= now - MEOW_CACHEGOV_WINDOW_MS) cnt++;
|
||||
}
|
||||
return cnt;
|
||||
}
|
||||
|
||||
/* 曲终/cur_finished 后:整文件 L0 DONTNEED。 */
|
||||
int meow_cachegov_drop_finished(const char *path)
|
||||
{
|
||||
if (!path) return -1;
|
||||
pthread_mutex_lock(&g_mu);
|
||||
for (int i = 0; i < MEOW_CACHEGOV_MAXREG; i++) {
|
||||
if (g_reg[i].used && strcmp(g_reg[i].path, path) == 0) {
|
||||
g_reg[i].used = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
int lbf = g_backoff, en = g_cfg.enabled;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
|
||||
if (!en || lbf >= 3) { /* A 已整体关闭:连曲终 L0 也不丢(§2.3 步骤 3) */
|
||||
meow_log(MEOW_LOG_INFO, "[cachegov] drop_finished skip %s (A off, backoff=%d)", path, lbf);
|
||||
return 0;
|
||||
}
|
||||
int fd = open(path, O_RDONLY);
|
||||
if (fd < 0) return -1;
|
||||
/* len=0 → 到 EOF(整文件)。曲终播放器已不再读该文件,重播=冷缓存首读(等价未命中)。 */
|
||||
posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED);
|
||||
close(fd);
|
||||
meow_log(MEOW_LOG_INFO, "[cachegov] drop_finished L0 %s", path);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* L1:丢已读前缀 [dropped_to, read_pos-keep_hot)。返回 1=有丢。 */
|
||||
static int entry_drop_l1(cg_entry_t *e, size_t keep)
|
||||
{
|
||||
if (!e->read_pos_fn) return 0;
|
||||
uint64_t pos = e->read_pos_fn(e->ctx);
|
||||
if (pos <= keep) return 0; /* keep_hot 覆盖整个已读前缀,无需丢 */
|
||||
uint64_t drop_end = pos - (uint64_t)keep; /* L1 上界:read_pos 前方绝不丢(L2 红线) */
|
||||
if (drop_end <= e->dropped_to) return 0; /* 无推进(seek 回退 / 前进不足) */
|
||||
if (drop_end - e->dropped_to < MEOW_CACHEGOV_DROP_THROTTLE && e->dropped_to != 0)
|
||||
return 0; /* 节流:不足 64KB 不重复 open */
|
||||
int fd = open(e->path, O_RDONLY);
|
||||
if (fd < 0) return 0;
|
||||
off_t off = (off_t)e->dropped_to;
|
||||
off_t len = (off_t)(drop_end - e->dropped_to);
|
||||
ssize_t r = posix_fadvise(fd, off, len, POSIX_FADV_DONTNEED);
|
||||
close(fd);
|
||||
if (r != 0) return 0;
|
||||
e->dropped_to = drop_end;
|
||||
meow_log(MEOW_LOG_INFO, "[cachegov] L1 drop %s off=%lld len=%lld (pos=%lld keep=%zu)",
|
||||
e->path, (long long)off, (long long)len, (long long)pos, keep);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int meow_cachegov_tick(void)
|
||||
{
|
||||
pthread_mutex_lock(&g_mu);
|
||||
int64_t now = now_ms();
|
||||
/* ① 回退评估(滚窗 10s):refault 超门限 → 升一档;滚窗干净 ≥10s → 降一档。
|
||||
* 每升降一档需间隔 ≥10s(逐级回退,不因窗内持续 refault 一次跳过多档,防误回退)。 */
|
||||
int cnt = refaults_last_window();
|
||||
if (cnt >= g_cfg.refault_backoff && g_cfg.refault_backoff > 0 && g_backoff < 3 &&
|
||||
(now - g_last_step_ms) >= MEOW_CACHEGOV_WINDOW_MS) {
|
||||
g_backoff = g_backoff + 1;
|
||||
g_last_step_ms = now;
|
||||
meow_log(MEOW_LOG_INFO, "[cachegov] backoff -> %d (refault=%d/%lds)",
|
||||
g_backoff, cnt, MEOW_CACHEGOV_WINDOW_MS / 1000);
|
||||
} else if (cnt == 0 && g_backoff > 0 &&
|
||||
(now - g_last_step_ms) >= MEOW_CACHEGOV_WINDOW_MS) {
|
||||
g_backoff = g_backoff - 1;
|
||||
g_last_step_ms = now;
|
||||
meow_log(MEOW_LOG_INFO, "[cachegov] backoff -> %d (clean window)", g_backoff);
|
||||
}
|
||||
|
||||
size_t keep = g_cfg.keep_hot_bytes;
|
||||
if (g_backoff >= MEOW_CACHEGOV_HOTESCALATE) keep = g_cfg.keep_hot_bytes * 2; /* 档1:放大 keep_hot */
|
||||
int l1_on = g_cfg.enabled && g_cfg.drop_mode >= 1 && g_backoff < 2; /* 档2:L1 关 */
|
||||
int en = g_cfg.enabled;
|
||||
|
||||
/* ② L1 丢弃 */
|
||||
int dropped = 0;
|
||||
if (en && l1_on) {
|
||||
for (int i = 0; i < MEOW_CACHEGOV_MAXREG; i++)
|
||||
if (g_reg[i].used) dropped += entry_drop_l1(&g_reg[i], keep);
|
||||
}
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
return dropped;
|
||||
}
|
||||
|
||||
/* ---- 注册 / 注销 ---- */
|
||||
int meow_cachegov_register(const char *path, uint64_t size,
|
||||
uint64_t (*read_pos_fn)(void *ctx), void *ctx)
|
||||
{
|
||||
if (!path) return -1;
|
||||
pthread_mutex_lock(&g_mu);
|
||||
int slot = -1;
|
||||
for (int i = 0; i < MEOW_CACHEGOV_MAXREG; i++) {
|
||||
if (g_reg[i].used && strcmp(g_reg[i].path, path) == 0) { slot = i; break; }
|
||||
if (!g_reg[i].used && slot < 0) slot = i;
|
||||
}
|
||||
if (slot < 0) { pthread_mutex_unlock(&g_mu); return -1; }
|
||||
snprintf(g_reg[slot].path, sizeof g_reg[slot].path, "%s", path);
|
||||
g_reg[slot].size = size;
|
||||
g_reg[slot].read_pos_fn = read_pos_fn;
|
||||
g_reg[slot].ctx = ctx;
|
||||
g_reg[slot].dropped_to = 0;
|
||||
g_reg[slot].used = 1;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
meow_log(MEOW_LOG_INFO, "[cachegov] register %s size=%llu", path, (unsigned long long)size);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int meow_cachegov_unregister(const char *path)
|
||||
{
|
||||
if (!path) return -1;
|
||||
pthread_mutex_lock(&g_mu);
|
||||
int rc = -1;
|
||||
for (int i = 0; i < MEOW_CACHEGOV_MAXREG; i++)
|
||||
if (g_reg[i].used && strcmp(g_reg[i].path, path) == 0) { g_reg[i].used = 0; rc = 0; break; }
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* ---- 降载查询(A 回退 OR svc_perf C) ---- */
|
||||
static void c_state(int *dl_pause, int *pf_pause)
|
||||
{
|
||||
int64_t now = now_ms();
|
||||
pthread_mutex_lock(&g_mu);
|
||||
if (g_cached_c_at == 0 || (now - g_cached_c_at) > 200) {
|
||||
read_c_state(&g_cached_c_dl, &g_cached_c_pf, NULL, NULL);
|
||||
g_cached_c_at = now;
|
||||
}
|
||||
if (dl_pause) *dl_pause = g_cached_c_dl;
|
||||
if (pf_pause) *pf_pause = g_cached_c_pf;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
}
|
||||
|
||||
int meow_cachegov_download_paused(void)
|
||||
{
|
||||
pthread_mutex_lock(&g_mu);
|
||||
int a = (g_backoff >= 2);
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
int c = 0;
|
||||
c_state(&c, NULL);
|
||||
return (a || c) ? 1 : 0;
|
||||
}
|
||||
|
||||
int meow_cachegov_prefetch_paused(void)
|
||||
{
|
||||
pthread_mutex_lock(&g_mu);
|
||||
int a = (g_backoff >= 3);
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
int c = 0;
|
||||
c_state(NULL, &c);
|
||||
return (a || c) ? 1 : 0;
|
||||
}
|
||||
|
||||
/* ---- 配置 ---- */
|
||||
int meow_cachegov_set_cfg(const struct meow_cachegov_cfg *cfg)
|
||||
{
|
||||
if (!cfg) return -1;
|
||||
pthread_mutex_lock(&g_mu);
|
||||
g_cfg = *cfg;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int meow_cachegov_get_cfg(struct meow_cachegov_cfg *cfg)
|
||||
{
|
||||
if (!cfg) return -1;
|
||||
pthread_mutex_lock(&g_mu);
|
||||
*cfg = g_cfg;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int meow_cachegov_backoff_level(void)
|
||||
{
|
||||
pthread_mutex_lock(&g_mu);
|
||||
int v = g_backoff;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
return v;
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* meow_img_color.c — MeowOS SDK 封面双色渐变取色实现(见 meow_img_color.h 契约)。
|
||||
*
|
||||
* 独立源码,不并入 meow_ui.a(铁律:避免并发重编/四 App 重链)。取色算法纯函数(无 GPU/浮点/堆)。
|
||||
* 唯一 meow_* 依赖:仅 meow_img_pixels(SDK 正式访问器,meow_ui_img.c §4),并经 meow_img_w/h 取宽高
|
||||
* ——三者读 meow_img 的唯一入口,本 TU 不再局部补全其结构体(agent-402 遗留点已上收)。App/宿主
|
||||
* 链入 meow_img_color.c 时需能解析 meow_img_pixels/w/h(宿主测试已链 meow_ui_img.c,实际 App 恒链
|
||||
* meow_ui.a)。
|
||||
*/
|
||||
#include "meow_img_color.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#define MIC_BUCKETS 4 /* 每通道色阶数 ⇒ 4×4×4=64 桶 */
|
||||
#define MIC_NBUCKET (MIC_BUCKETS*MIC_BUCKETS*MIC_BUCKETS)
|
||||
#define MIC_ALPHA_MIN 64 /* 跳过近透明(alpha<64) */
|
||||
#define MIC_LUMA_MIN 24 /* 跳过近黑 */
|
||||
#define MIC_LUMA_MAX 244 /* 跳过近白 */
|
||||
#define MIC_MIN_VALID 8 /* 有效采样下限(低于=取色失败回退主题) */
|
||||
#define MIC_TARGET 900 /* 目标采样数(副采样步进自适,×4 作上限) */
|
||||
#define MIC_SEC_FRAC 7U /* 次桶显著门槛:≥主桶 1/7(≈14%) */
|
||||
|
||||
static int mic_luma(uint32_t c)
|
||||
{
|
||||
int r = (int)((c >> 16) & 255), g = (int)((c >> 8) & 255), b = (int)(c & 255);
|
||||
return (r * 299 + g * 587 + b * 114) / 1000;
|
||||
}
|
||||
|
||||
/* 两色线性混合(t=0..255,0=纯 a、255=纯 b;逐通道整数,无浮点)。 */
|
||||
static uint32_t mic_mix(uint32_t a, uint32_t b, int t)
|
||||
{
|
||||
if (t < 0) t = 0;
|
||||
if (t > 255) t = 255;
|
||||
int ar = (a >> 16) & 255, ag = (a >> 8) & 255, ab = a & 255;
|
||||
int br = (b >> 16) & 255, bg = (b >> 8) & 255, bb = b & 255;
|
||||
int r = ar + ((br - ar) * t + 128) / 255;
|
||||
int g = ag + ((bg - ag) * t + 128) / 255;
|
||||
int bl = ab + ((bb - ab) * t + 128) / 255;
|
||||
return ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)bl;
|
||||
}
|
||||
|
||||
int meow_img_duocolor_rgba(const uint8_t *rgba, int w, int h,
|
||||
uint32_t *main, uint32_t *second)
|
||||
{
|
||||
if (!rgba || w <= 0 || h <= 0 || !main || !second) return -1;
|
||||
|
||||
/* 副采样步进:使采样数≈MIC_TARGET(×4 上限),大图/缩略图成本恒定。 */
|
||||
int step = 1;
|
||||
while ((long)(w / step) * (long)(h / step) > (long)MIC_TARGET * 4 && step < 64)
|
||||
step++;
|
||||
|
||||
long cnt[MIC_NBUCKET];
|
||||
long sr[MIC_NBUCKET], sg[MIC_NBUCKET], sb[MIC_NBUCKET], sw[MIC_NBUCKET];
|
||||
memset(cnt, 0, sizeof cnt);
|
||||
memset(sr, 0, sizeof sr);
|
||||
memset(sg, 0, sizeof sg);
|
||||
memset(sb, 0, sizeof sb);
|
||||
memset(sw, 0, sizeof sw);
|
||||
|
||||
long total = 0;
|
||||
for (int y = 0; y < h; y += step) {
|
||||
for (int x = 0; x < w; x += step) {
|
||||
const uint8_t *p = rgba + ((size_t)y * (size_t)w + (size_t)x) * 4;
|
||||
int a = p[3];
|
||||
if (a < MIC_ALPHA_MIN) continue;
|
||||
int r = p[0], g = p[1], b = p[2];
|
||||
if ((r * 299 + g * 587 + b * 114) / 1000 < MIC_LUMA_MIN) continue; /* 近黑 */
|
||||
if ((r * 299 + g * 587 + b * 114) / 1000 > MIC_LUMA_MAX) continue; /* 近白 */
|
||||
int mx = r, mn = r;
|
||||
if (g > mx) mx = g;
|
||||
if (b > mx) mx = b;
|
||||
if (g < mn) mn = g;
|
||||
if (b < mn) mn = b;
|
||||
int sat = mx - mn; /* 饱和度 0..255 */
|
||||
long wt = 1 + sat / 24; /* 1..~11:偏向鲜艳(跳过灰蒙蒙) */
|
||||
int qr = r * MIC_BUCKETS / 256, qg = g * MIC_BUCKETS / 256, qb = b * MIC_BUCKETS / 256;
|
||||
int bi = (qr * MIC_BUCKETS + qg) * MIC_BUCKETS + qb;
|
||||
if (bi < 0 || bi >= MIC_NBUCKET) continue;
|
||||
cnt[bi] += wt;
|
||||
sr[bi] += (long)r * wt; sg[bi] += (long)g * wt; sb[bi] += (long)b * wt;
|
||||
sw[bi] += wt;
|
||||
total++;
|
||||
}
|
||||
}
|
||||
if (total < MIC_MIN_VALID) return -1; /* 全透明/近黑白 → 失败回退主题 */
|
||||
|
||||
/* 主色:加权最大桶中心 */
|
||||
int mi = -1;
|
||||
long best = -1;
|
||||
for (int i = 0; i < MIC_NBUCKET; i++)
|
||||
if (cnt[i] > best) { best = cnt[i]; mi = i; }
|
||||
if (mi < 0 || sw[mi] <= 0) return -1;
|
||||
long mr0 = sr[mi] / sw[mi], mg0 = sg[mi] / sw[mi], mb0 = sb[mi] / sw[mi];
|
||||
*main = ((uint32_t)mr0 << 16) | ((uint32_t)mg0 << 8) | (uint32_t)mb0;
|
||||
|
||||
/* 次色:与主色 RGB 色距最大的显著桶;无显著桶→主色缩暗 2/3(渐变互补)。 */
|
||||
long thresh = best > 0 ? best / MIC_SEC_FRAC : 1;
|
||||
int si = -1;
|
||||
long sd = -1;
|
||||
for (int i = 0; i < MIC_NBUCKET; i++) {
|
||||
if (i == mi || cnt[i] < thresh || sw[i] <= 0) continue;
|
||||
long dr = sr[i] / sw[i] - mr0, dg = sg[i] / sw[i] - mg0, db = sb[i] / sw[i] - mb0;
|
||||
long d = dr * dr + dg * dg + db * db;
|
||||
if (d > sd) { sd = d; si = i; }
|
||||
}
|
||||
if (si >= 0)
|
||||
*second = ((uint32_t)(sr[si] / sw[si]) << 16) | ((uint32_t)(sg[si] / sw[si]) << 8) |
|
||||
(uint32_t)(sb[si] / sw[si]);
|
||||
else
|
||||
*second = ((uint32_t)(mr0 * 2 / 3) << 16) | ((uint32_t)(mg0 * 2 / 3) << 8) |
|
||||
(uint32_t)(mb0 * 2 / 3);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int meow_img_duocolor(const meow_img *im, uint32_t *main, uint32_t *second)
|
||||
{
|
||||
if (!im) return -1;
|
||||
/* 经 SDK 正式访问器读像素(meow_img opaque;meow_img_pixels 为唯一读入口)。 */
|
||||
const unsigned char *rgba = meow_img_pixels(im);
|
||||
if (!rgba) return -1;
|
||||
return meow_img_duocolor_rgba(rgba, meow_img_w(im), meow_img_h(im), main, second);
|
||||
}
|
||||
|
||||
void meow_img_color_derive(uint32_t main, uint32_t second, const meow_theme *th,
|
||||
meow_img_color_theme *out)
|
||||
{
|
||||
if (!out) return;
|
||||
uint32_t mainv = main ? main : (th ? th->primary : 0x808080);
|
||||
uint32_t secv = second ? second : (th ? th->secondary : mainv);
|
||||
/* 背景平均亮度 → 文字可读色(黑/白) */
|
||||
int luavg = (mic_luma(mainv) + mic_luma(secv)) / 2;
|
||||
uint32_t fg = (luavg >= 140) ? 0x1C1C1C : 0xFFFFFF;
|
||||
uint32_t fgd = mic_mix(fg, mic_mix(mainv, 0x0A0A0A, 150), 90); /* 次要文字向背景靠(降对比) */
|
||||
/* 背景渐变:主/次各压暗 55%(防刺眼 + 保文字可读) */
|
||||
uint32_t bg_top = mic_mix(mainv, 0x0A0A0A, 150);
|
||||
uint32_t bg_bottom = mic_mix(secv, 0x0A0A0A, 150);
|
||||
/* 强调=次色(与主色底对比);其上图标按 accent 亮度选黑/白 */
|
||||
uint32_t accent = secv;
|
||||
uint32_t accent_fg = (mic_luma(accent) >= 140) ? 0x1C1C1C : 0xFFFFFF;
|
||||
|
||||
out->bg_top = bg_top; out->bg_bottom = bg_bottom;
|
||||
out->fg = fg; out->fg_dim = fgd;
|
||||
out->accent = accent; out->accent_fg = accent_fg;
|
||||
|
||||
/* 歌词页覆盖:scrim=主色压暗、cur=fg(可读)、gray 留 0(lyric_view 以 cur 自动递减)。 */
|
||||
meow_lyric_view_colors *lc = &out->lyr;
|
||||
memset(lc, 0, sizeof *lc);
|
||||
lc->scrim = mic_mix(mainv, 0x000000, 150);
|
||||
lc->cur = fg;
|
||||
lc->scrim_alpha = 176;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* meow_img_color.h — MeowOS SDK 封面双色渐变取色(批5)。
|
||||
*
|
||||
* 定位:从歌曲封面(已解码 RGBA 像素/meow_img 句柄)提取「主色 + 次色」,供播放页/歌词页动态主题
|
||||
* (背景渐变/文字 accent)。纯 C 分桶统计,无 GPU、无浮点、无堆分配。
|
||||
*
|
||||
* 组织:独立公共头(仿 meow_lyric_view.h 模式,不随 meow_ui.h 伞头发布),取色原语是独立源码
|
||||
* meow_img_color.c——**不并入 meow_ui.a**(铁律:避免并发重编/四 App 重链)。App/launcher 需显式
|
||||
* `#include "meow_img_color.h"` 并在构建时单链 meow_img_color.c(宿主单测同法)。
|
||||
*
|
||||
* 约束(0 warning / 最小侵入):
|
||||
* - meow_img 对外保持 opaque;像素直读经 SDK 正式访问器 `meow_img_pixels`(meow_ui_img.c §4,
|
||||
* 已上收「局部补全结构体」遗留),宽高经 `meow_img_w`/`meow_img_h`。本 TU 不再局部补全布局。
|
||||
* - 取色算法纯函数:meow_img_duocolor_rgba / meow_img_color_derive 纯 C、无 meow_* 依赖;
|
||||
* meow_img_duocolor(句柄版)仅经上述三访问器读,不触碰 meow_ui.a 内部实现。宿主单测链
|
||||
* meow_img_color.c + meow_ui_img.c 即可。
|
||||
*
|
||||
* 色彩语义:
|
||||
* - 输入 RGBA(stb_image 输出,4 通道 straight alpha),w×h,最多遍历 ~3.6k 采样(内部自适
|
||||
* 副采样步进,等效缩略图成本,避免全尺寸逐像素)。
|
||||
* - 色域分桶 4×4×4=64(RGB 各 4 段),跳过近黑(luma<24)/近白(luma>244)/透明(alpha<64),
|
||||
* 按「像素数 + 饱和度」加权;主色=加权最大桶中心;次色=与主色色距最大的显著桶(≥主桶 14%)
|
||||
* 中心;无显著桶→次色=主色缩暗 2/3(渐变互补)。
|
||||
* - 返回 0=成功;-1=参数非法 / 有效采样不足(调用方回退全局 meow_theme)。
|
||||
*/
|
||||
#ifndef MEOW_IMG_COLOR_H
|
||||
#define MEOW_IMG_COLOR_H
|
||||
|
||||
#include "meow_ui.h" /* meow_img 句柄 + meow_theme */
|
||||
#include "meow_lyric_view.h" /* meow_lyric_view_colors(派生歌词页覆盖) */
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* 从 RGBA 像素缓冲取主色+次色(纯算法;样本副采样)。返回 0=成功;-1=失败。 */
|
||||
int meow_img_duocolor_rgba(const uint8_t *rgba, int w, int h,
|
||||
uint32_t *main, uint32_t *second);
|
||||
|
||||
/* 从 meow_img 句柄取主色+次色(经 meow_img_pixels 读像素,见 .c)。返回 0=成功;-1=失败。 */
|
||||
int meow_img_duocolor(const meow_img *im, uint32_t *main, uint32_t *second);
|
||||
|
||||
/* 由主/次色派生播放页 + 歌词页封面主题(纯函数,可宿主单测)。
|
||||
* th 用于默认回退色(main/second 为 0 时);调用方应传 meow_theme_get()。
|
||||
* 派生语义:
|
||||
* - 背景渐变 = 主色(顶)→ 次色(底)各压暗 55%(防刺眼 + 保文字可读);
|
||||
* - fg = 可读文字色(按主次平均亮度选黑/白),fg_dim = 向背景靠拢的次要文字色;
|
||||
* - accent = 次色(与主色底对比),accent_fg = accent 上可读图标色;
|
||||
* - lyr = 歌词页 set_colors 覆盖:scrim=主色压暗(alpha 176)、cur=fg,gray 留 0(由 lyric_view
|
||||
* 以「cur 为基准自动递减」——批5 已修正对覆盖 cur 亦然)。 */
|
||||
typedef struct meow_img_color_theme {
|
||||
uint32_t bg_top; /* 播放页渐变顶 */
|
||||
uint32_t bg_bottom; /* 播放页渐变底 */
|
||||
uint32_t fg; /* 主要文字色(背景上可读) */
|
||||
uint32_t fg_dim; /* 次要文字色 */
|
||||
uint32_t accent; /* 强调色(控件/进度/封面占位) */
|
||||
uint32_t accent_fg; /* accent 上可读前景(图标) */
|
||||
meow_lyric_view_colors lyr; /* 歌词页 set_colors 覆盖 */
|
||||
} meow_img_color_theme;
|
||||
|
||||
void meow_img_color_derive(uint32_t main, uint32_t second, const meow_theme *th,
|
||||
meow_img_color_theme *out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MEOW_IMG_COLOR_H */
|
||||
+1005
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* meow_jpeg.h — M23 真缩解后端内部接口(**不对外导出**,不在 meow_ui.h 伞头里)。
|
||||
*
|
||||
* 后端直接产出「目标显示尺寸」的 RGBA,不做「先整幅解、再重采样」:
|
||||
* - JPEG(meow_jpeg.c):DCT 域缩解 1/8、1/4、1/2(N=1/2/4 点 IDCT),渐进式只解到
|
||||
* 首个精细化扫描(首个 Ah>0)为止——低频带此时已完整,再往后只是为了更细的量化精度。
|
||||
* - PNG(meow_png_stream.c):libz inflate 逐行 defilter,按步长丢行丢列直写目标尺寸。
|
||||
* - BMP(meow_png_stream.c):按行 seek + 行内跳列(同一套「只碰需要的行」思路)。
|
||||
* 三者峰值都只与「输出尺寸 + 目标图像」相关,不随源图分辨率线性增长——**前提是文件头
|
||||
* 声明的尺寸在合理范围内**:畸形/超大声明尺寸(例如 23 字节的 SOF 声明 65535×65535)会被
|
||||
* 直接拒绝(返回 0 → 回退 stb,安全失败),不会按声明尺寸分配平面。
|
||||
*
|
||||
* 不支持的特性一律返回 0,由 meow_ui_img.c 回退 stb(行为与 M23 之前完全一致):
|
||||
* 4 分量 CMYK/YCCK、算术编码/无损/分层 JPEG、Adam7 交错 PNG、1/2/4bpp 与 RLE BMP 等。
|
||||
*
|
||||
* 许可证:本文件与 meow_jpeg.c / meow_png_stream.c 为 MeowOS 自写代码(MIT,随 SDK 发布)。
|
||||
* 不引入第三方解码器源码——理由见 M23 报告「为什么不用 IJG libjpeg」:libjpeg 系列对
|
||||
* 渐进式 JPEG 必须整幅驻留系数阵列(≈1 字节/源像素/分量),正是审计里 +23.5MB 的来源,
|
||||
* 换成它也无法把渐进式峰值压到 2.5MB 以内。
|
||||
*/
|
||||
#ifndef MEOW_JPEG_H
|
||||
#define MEOW_JPEG_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* 解码 path / 内存里的图,直接产出「长边 ≤ maxdim 且按 meow_img_downscale 同一公式等比缩放」
|
||||
* 的 tw×th RGBA(4 通道 straight alpha,malloc 分配,调用方 free)。
|
||||
* 返回 1 成功(tw/th/rgba 已填);0 = 不支持/失败/不需要(调用方回退 stb 或旧入口)。 */
|
||||
int meow_img_backend_file(const char *path, int maxdim, int *tw, int *th, unsigned char **rgba);
|
||||
int meow_img_backend_mem(const void *buf, size_t len, int maxdim, int *tw, int *th, unsigned char **rgba);
|
||||
|
||||
/* 目标尺寸:与 meow_img_downscale 同一公式(长边 = maxdim,短边按比例截断,下限 1)。 */
|
||||
void meow_img_target_dims(int w, int h, int maxdim, int *tw, int *th);
|
||||
|
||||
/* meow_png_stream.c 提供(PNG 逐行 inflate / BMP 按行跳采样)。f 与 mem 二选一:
|
||||
* f != NULL 走文件流(只读需要的行/块),否则从 mem[0..len) 读。 */
|
||||
int meow_png_decode_stream(FILE *f, const unsigned char *mem, size_t len,
|
||||
int maxdim, int *tw, int *th, unsigned char **rgba);
|
||||
int meow_bmp_decode_stream(FILE *f, const unsigned char *mem, size_t len,
|
||||
int maxdim, int *tw, int *th, unsigned char **rgba);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MEOW_JPEG_H */
|
||||
@@ -0,0 +1,361 @@
|
||||
/*
|
||||
* meow_lazygrid.c — §12.4 2 维虚拟化宫格(M7.4 主界面前置)。
|
||||
*
|
||||
* 自包含控件(非 meow_widget,不注册 z 序/绘制注册表):宿主在 draw 阶段调 meow_lazygrid_draw、
|
||||
* 命中阶段调 meow_lazygrid_hit/tap_at、滚动喂 meow_scroll(meow_lazygrid_scroll 返回会话)。
|
||||
* 只实例化**可见行**:固定 item 池(视口行数 + 2 × cols,静态有界 ≤MEOW_LAZYGRID_POOL_MAX),
|
||||
* bind 仅在 item 进入视口时调一次(回滚同槽不重绑,同 listview 池语义)。
|
||||
*
|
||||
* 内存纪律:池在 create/set_vsource/set_cell(冷路径)分配;draw/hit/set_scroll 热路径零 malloc。
|
||||
* 颜色全取 meow_theme_get() 语义色(日/夜/style 自动),不硬编码。
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "include/meow_lazygrid.h"
|
||||
#include "meow_ui_internal.h" /* meow_gfx_clip_push/pop + meow_gfx_fill_round */
|
||||
#include "include/meow_ui_scroll.h"
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
struct meow_lazygrid {
|
||||
int x, y, w, h; /* 视口矩形(绝对坐标) */
|
||||
int cols; /* 列数 */
|
||||
int cell_w, cell_h, gap; /* 格宽/格高/间距 */
|
||||
int row_h; /* 行高 = cell_h + gap(缓存) */
|
||||
|
||||
int scroll; /* 滚动偏移(像素) */
|
||||
int sel; /* 选中项(-1=无) */
|
||||
|
||||
meow_lazygrid_count_fn count_fn; void *vctx;
|
||||
meow_lazygrid_bind_fn bind;
|
||||
meow_lazygrid_draw_fn draw_cb; void *draw_ctx;
|
||||
meow_lazygrid_tap_fn tap_cb; void *tap_ctx;
|
||||
|
||||
/* 固定 item 池(冷路径分配;slot 按 item index mod pool_n 映射) */
|
||||
meow_lazygrid_item *pool;
|
||||
int *pool_idx; /* 每槽绑定的 item index(-1=空闲) */
|
||||
int pool_n;
|
||||
|
||||
char empty_text[64]; /* 空态占位(首字符 0=不画) */
|
||||
};
|
||||
|
||||
/* 行高(= cell_h + gap)。 */
|
||||
int meow_lazygrid_row_h(const meow_lazygrid *g)
|
||||
{ return g ? g->row_h : 0; }
|
||||
|
||||
/* 总行数。 */
|
||||
static int lg_rows(const meow_lazygrid *g)
|
||||
{
|
||||
int n = g->count_fn ? g->count_fn(g->vctx) : 0;
|
||||
if (n < 0) n = 0;
|
||||
if (n == 0) return 0;
|
||||
return (n + g->cols - 1) / g->cols;
|
||||
}
|
||||
/* 总项数。 */
|
||||
static int lg_count(const meow_lazygrid *g)
|
||||
{ return g->count_fn ? g->count_fn(g->vctx) : 0; }
|
||||
|
||||
/* 内容总高(行高 × 行数 − 末行后无 gap)。 */
|
||||
static int lg_content_h(const meow_lazygrid *g)
|
||||
{
|
||||
int rows = lg_rows(g);
|
||||
if (rows <= 0) return 0;
|
||||
return rows * g->cell_h + (rows - 1) * g->gap;
|
||||
}
|
||||
|
||||
/* ---- 生命周期 ---- */
|
||||
meow_lazygrid *meow_lazygrid_create(int x, int y, int w, int h, int cols,
|
||||
int cell_w, int cell_h, int gap)
|
||||
{
|
||||
if (cols < 1) cols = 1;
|
||||
meow_lazygrid *g = (meow_lazygrid *)calloc(1, sizeof *g);
|
||||
if (!g) return NULL;
|
||||
g->x = x; g->y = y; g->w = w; g->h = h; g->cols = cols;
|
||||
g->cell_w = cell_w > 0 ? cell_w : 1;
|
||||
g->cell_h = cell_h > 0 ? cell_h : 1;
|
||||
g->gap = gap > 0 ? gap : 0;
|
||||
g->row_h = g->cell_h + g->gap;
|
||||
g->sel = -1;
|
||||
return g;
|
||||
}
|
||||
|
||||
void meow_lazygrid_free(meow_lazygrid *g)
|
||||
{
|
||||
if (!g) return;
|
||||
free(g->pool); free(g->pool_idx);
|
||||
free(g);
|
||||
}
|
||||
|
||||
void meow_lazygrid_set_rect(meow_lazygrid *g, int x, int y, int w, int h)
|
||||
{
|
||||
if (!g) return;
|
||||
g->x = x; g->y = y; g->w = w; g->h = h;
|
||||
}
|
||||
void meow_lazygrid_set_cols(meow_lazygrid *g, int cols)
|
||||
{
|
||||
if (!g) return;
|
||||
if (cols < 1) cols = 1;
|
||||
if (g->cols == cols) return;
|
||||
g->cols = cols;
|
||||
}
|
||||
void meow_lazygrid_set_cell(meow_lazygrid *g, int cell_w, int cell_h, int gap)
|
||||
{
|
||||
if (!g) return;
|
||||
g->cell_w = cell_w > 0 ? cell_w : 1;
|
||||
g->cell_h = cell_h > 0 ? cell_h : 1;
|
||||
g->gap = gap > 0 ? gap : 0;
|
||||
g->row_h = g->cell_h + g->gap; /* 行高变化 → 可见行数变 → 池容量随之调整 */
|
||||
}
|
||||
|
||||
/* ---- item 池(冷路径;有界 ≤MEOW_LAZYGRID_POOL_MAX)---- */
|
||||
static int lg_pool_cap(const meow_lazygrid *g)
|
||||
{
|
||||
int rh = g->row_h; if (rh < 8) rh = 8;
|
||||
int rows = (g->h / rh) + 1; if (rows < 1) rows = 1;
|
||||
int cap = (rows + 2) * g->cols;
|
||||
if (cap < 8) cap = 8;
|
||||
if (cap > MEOW_LAZYGRID_POOL_MAX) cap = MEOW_LAZYGRID_POOL_MAX;
|
||||
return cap;
|
||||
}
|
||||
/* 确保池容量 ≥ cap(有界扩张;重分配清空绑定 → 下一帧按需重绑)。返回 0=成功;-1=分配失败。 */
|
||||
static int lg_ensure_pool(meow_lazygrid *g)
|
||||
{
|
||||
if (!g->count_fn || !g->bind) return 0;
|
||||
int cap = lg_pool_cap(g);
|
||||
if (g->pool && g->pool_n >= cap) return 0;
|
||||
meow_lazygrid_item *np = (meow_lazygrid_item *)calloc((size_t)cap, sizeof *np);
|
||||
int *ni = (int *)calloc((size_t)cap, sizeof *ni);
|
||||
if (!np || !ni) { free(np); free(ni); return -1; }
|
||||
for (int i = 0; i < cap; i++) ni[i] = -1;
|
||||
free(g->pool); free(g->pool_idx);
|
||||
g->pool = np; g->pool_idx = ni; g->pool_n = cap;
|
||||
return 0;
|
||||
}
|
||||
/* 取 index 对应的池槽(按 item index mod pool_n 映射;槽绑定不匹配 → 防御性重绑)。 */
|
||||
static meow_lazygrid_item *lg_slot(meow_lazygrid *g, int index)
|
||||
{
|
||||
if (!g->pool || g->pool_n <= 0) return NULL;
|
||||
int s = (int)((unsigned)index % (unsigned)g->pool_n);
|
||||
if (g->pool_idx[s] != index) {
|
||||
memset(&g->pool[s], 0, sizeof g->pool[s]);
|
||||
g->bind(index, &g->pool[s], g->vctx);
|
||||
g->pool_idx[s] = index;
|
||||
}
|
||||
return &g->pool[s];
|
||||
}
|
||||
|
||||
/* ---- 数据源 + 渲染 ---- */
|
||||
int meow_lazygrid_set_vsource(meow_lazygrid *g, meow_lazygrid_count_fn count,
|
||||
meow_lazygrid_bind_fn bind, void *ctx)
|
||||
{
|
||||
if (!g || !count || !bind) return -1;
|
||||
g->count_fn = count; g->bind = bind; g->vctx = ctx;
|
||||
if (lg_ensure_pool(g) != 0) { /* 分配固定 item 池(有界,冷路径) */
|
||||
g->count_fn = NULL; g->bind = NULL; /* 池分配失败:回退为空 */
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
int meow_lazygrid_set_item_draw(meow_lazygrid *g, meow_lazygrid_draw_fn draw,
|
||||
void *ctx)
|
||||
{
|
||||
if (!g) return -1;
|
||||
g->draw_cb = draw; g->draw_ctx = ctx; /* NULL=用默认卡片 renderer */
|
||||
return 0;
|
||||
}
|
||||
int meow_lazygrid_set_empty_text(meow_lazygrid *g, const char *text)
|
||||
{
|
||||
if (!g) return -1;
|
||||
if (text) {
|
||||
strncpy(g->empty_text, text, sizeof g->empty_text - 1);
|
||||
g->empty_text[sizeof g->empty_text - 1] = '\0';
|
||||
} else {
|
||||
g->empty_text[0] = '\0';
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- 滚动 ---- */
|
||||
int meow_lazygrid_max_scroll(meow_lazygrid *g)
|
||||
{
|
||||
if (!g) return 0;
|
||||
int ch = lg_content_h(g);
|
||||
int ms = ch - g->h;
|
||||
return ms > 0 ? ms : 0;
|
||||
}
|
||||
void meow_lazygrid_set_scroll(meow_lazygrid *g, int off)
|
||||
{
|
||||
if (!g) return;
|
||||
int ms = meow_lazygrid_max_scroll(g);
|
||||
if (off < 0) off = 0;
|
||||
if (off > ms) off = ms;
|
||||
g->scroll = off;
|
||||
}
|
||||
void meow_lazygrid_scroll_by(meow_lazygrid *g, int dy)
|
||||
{ if (g) meow_lazygrid_set_scroll(g, g->scroll + dy); }
|
||||
int meow_lazygrid_scroll(meow_lazygrid *g) { return g ? g->scroll : 0; }
|
||||
|
||||
/* ---- meow_scroll 集成(callbacks 指回本宫格)---- */
|
||||
static void lg_s_set(void *c, int off) { meow_lazygrid_set_scroll((meow_lazygrid *)c, off); }
|
||||
static int lg_s_max(void *c) { return meow_lazygrid_max_scroll((meow_lazygrid *)c); }
|
||||
static int lg_s_get(void *c) { return meow_lazygrid_scroll((meow_lazygrid *)c); }
|
||||
static void lg_s_redraw(void *c)
|
||||
{
|
||||
meow_lazygrid *g = (meow_lazygrid *)c;
|
||||
if (g && g->w > 0 && g->h > 0) {
|
||||
meow_lazygrid_draw(g, meow_theme_get());
|
||||
meow_ui_flush_region(g->x, g->y, g->w, g->h); /* 只刷视口区 */
|
||||
}
|
||||
}
|
||||
meow_scroll *meow_lazygrid_scroll_attach(meow_lazygrid *g)
|
||||
{
|
||||
if (!g) return NULL;
|
||||
return meow_scroll_create(lg_s_set, lg_s_max, lg_s_get, lg_s_redraw, g);
|
||||
}
|
||||
|
||||
/* ---- 选中 ---- */
|
||||
void meow_lazygrid_set_sel(meow_lazygrid *g, int idx)
|
||||
{
|
||||
if (!g) return;
|
||||
int n = lg_count(g);
|
||||
if (idx < 0) { g->sel = -1; return; }
|
||||
if (n > 0 && idx >= n) idx = n - 1;
|
||||
g->sel = idx;
|
||||
}
|
||||
int meow_lazygrid_sel(const meow_lazygrid *g) { return g ? g->sel : -1; }
|
||||
void meow_lazygrid_set_on_tap(meow_lazygrid *g, meow_lazygrid_tap_fn cb, void *ctx)
|
||||
{
|
||||
if (!g) return;
|
||||
g->tap_cb = cb; g->tap_ctx = ctx;
|
||||
}
|
||||
int meow_lazygrid_tap_at(meow_lazygrid *g, int x, int y)
|
||||
{
|
||||
int idx = meow_lazygrid_hit(g, x, y);
|
||||
if (idx < 0) return -1;
|
||||
meow_lazygrid_set_sel(g, idx);
|
||||
if (g->tap_cb) g->tap_cb(idx, g->tap_ctx);
|
||||
return idx;
|
||||
}
|
||||
|
||||
/* ---- 命中(滚动 + 网格坐标 → item idx)----
|
||||
* 点在格内(跨过 gap 区域返回 -1);禁用格不响应(同 listview enabled=0 语义)。 */
|
||||
int meow_lazygrid_hit(meow_lazygrid *g, int x, int y)
|
||||
{
|
||||
if (!g) return -1;
|
||||
if (x < g->x || x >= g->x + g->w || y < g->y || y >= g->y + g->h) return -1;
|
||||
/* 列格(水平):col = px / (cell_w+gap),落点在 gap 内 → 未命中。 */
|
||||
int px = x - g->x;
|
||||
int cstep = g->cell_w + g->gap;
|
||||
int col = px / cstep;
|
||||
if (col < 0 || col >= g->cols) return -1;
|
||||
if ((px - col * cstep) >= g->cell_w) return -1;
|
||||
/* 行格(垂直,含滚动):row = (py+scroll)/row_h;落点在 gap 内 → 未命中。 */
|
||||
int py = y - g->y + g->scroll;
|
||||
int rstep = g->row_h;
|
||||
int row = py / rstep;
|
||||
if (row < 0) return -1;
|
||||
if ((py - row * rstep) >= g->cell_h) return -1;
|
||||
int idx = row * g->cols + col;
|
||||
int n = lg_count(g);
|
||||
if (idx < 0 || idx >= n) return -1;
|
||||
/* 禁用格不响应(顺手 bind 临时 enabled 查:与 listview hit 同法)。 */
|
||||
if (g->bind) {
|
||||
meow_lazygrid_item it;
|
||||
memset(&it, 0, sizeof it);
|
||||
g->bind(idx, &it, g->vctx);
|
||||
if (!it.enabled) return -1;
|
||||
}
|
||||
return idx;
|
||||
}
|
||||
|
||||
/* ---- 绘制(只画可见行;clip 钳到宫格视口)---- */
|
||||
/* MD3 容器(默认 renderer P0):格=格内对称内缩 surfaceContainer 圆角卡(radius_card)+ 卡内 tonal
|
||||
* 选中(primaryContainer)/ 按压(primary α26);画布=整格(触控目标),卡在其内对称内缩(同
|
||||
* meow_listview 行>卡范式:卡片外仍命中整格,视觉卡留出呼吸空隙)。完全自绘(set_item_draw)
|
||||
* 覆盖本 renderer,App 自理容器观感。 */
|
||||
#define MEOW_LAZYGRID_CARD_MARGIN 8 /* 格内对称边距(卡内缩;与 cell gap 叠加出卡间空隙) */
|
||||
static void lg_draw_cell(meow_lazygrid *g, const meow_theme *th, int index,
|
||||
int cell_x, int cell_y, meow_lazygrid_item *it)
|
||||
{
|
||||
int sel = (index == g->sel);
|
||||
int txtcol = sel ? th->onPrimaryContainer : th->onSurface;
|
||||
int subcol = sel ? th->onPrimaryContainer : th->onSurfaceVariant;
|
||||
int m = MEOW_LAZYGRID_CARD_MARGIN;
|
||||
int cw = g->cell_w - 2 * m, chh = g->cell_h - 2 * m; /* 卡尺寸(格内对称内缩) */
|
||||
int cx = cell_x + m, cy = cell_y + m; /* 卡左上 */
|
||||
int press = meow_input_touch_down() &&
|
||||
meow_input_touch_x() >= cell_x && meow_input_touch_x() < cell_x + g->cell_w &&
|
||||
meow_input_touch_y() >= cell_y && meow_input_touch_y() < cell_y + g->cell_h;
|
||||
meow_gfx_fill_round(cx, cy, cw, chh, th->radius_card, th->surfaceContainer);
|
||||
if (sel) meow_gfx_fill_round(cx, cy, cw, chh, th->radius_card, th->primaryContainer);
|
||||
else if (press) meow_gfx_fill_round_alpha(cx, cy, cw, chh, th->radius_card, th->primary, 26);
|
||||
|
||||
int pad = 8;
|
||||
int thh = meow_ui_text_height(th->font_body);
|
||||
/* 图标(可选):居中于卡顶 ~50%(卡中心=格中心,因对称内缩)。 */
|
||||
if (it->icon) {
|
||||
int isz = g->cell_w * 6 / 10; if (isz > g->cell_h * 5 / 10) isz = g->cell_h * 5 / 10;
|
||||
if (isz < 16) isz = 16;
|
||||
int icx = cx + cw / 2, icy = cy + pad + isz / 2;
|
||||
meow_ui_img_scaled(icx - isz / 2, icy - isz / 2, isz, isz, (meow_img *)it->icon);
|
||||
}
|
||||
/* 标题 + 副标题(主界面:名 + 状态)。卡内底部对齐。 */
|
||||
int has_sub = (it->subtitle && it->subtitle[0]);
|
||||
int ty = cy + chh - pad - thh - (has_sub ? (thh + 2) : 0);
|
||||
if (it->title) meow_ui_text_center(cx + cw / 2, ty, it->title, th->font_body, txtcol);
|
||||
if (has_sub) meow_ui_text_center(cx + cw / 2, ty + thh + 2, it->subtitle, th->font_body, subcol);
|
||||
|
||||
/* 选中描边(轮廓);禁用卡内变淡(圆角 α,防圆角外叠出灰方块)。 */
|
||||
if (sel) meow_ui_draw_rect_round(cx, cy, cw, chh, th->radius_card, th->primary);
|
||||
if (!it->enabled)
|
||||
meow_gfx_fill_round_alpha(cx, cy, cw, chh, th->radius_card, th->onSurface, 90);
|
||||
}
|
||||
|
||||
void meow_lazygrid_draw(meow_lazygrid *g, const meow_theme *th)
|
||||
{
|
||||
if (!g || !th) return;
|
||||
meow_gfx_clip_push(g->x, g->y, g->w, g->h); /* 格画不出宫格视口(防泄漏) */
|
||||
|
||||
int n = lg_count(g);
|
||||
int rows = lg_rows(g);
|
||||
lg_ensure_pool(g); /* 防御:视口/列几何变化时确保池容量 */
|
||||
|
||||
/* 空态占位:count==0 且设 empty_text → 视口内居中。 */
|
||||
if (n == 0 && g->empty_text[0]) {
|
||||
int thh = meow_ui_text_height(th->font_body);
|
||||
meow_ui_text_center(g->x + g->w / 2, g->y + (g->h - thh) / 2, g->empty_text,
|
||||
th->font_body, th->onSurfaceVariant);
|
||||
meow_gfx_clip_pop();
|
||||
return;
|
||||
}
|
||||
|
||||
int off = g->scroll;
|
||||
int cstep = g->cell_w + g->gap;
|
||||
int rstep = g->row_h;
|
||||
if (rstep <= 0) rstep = 1;
|
||||
int first = off / rstep;
|
||||
int last = (off + g->h) / rstep + 1; if (last > rows) last = rows;
|
||||
if (first < 0) first = 0;
|
||||
if (last > rows) last = rows;
|
||||
for (int r = first; r < last; r++) {
|
||||
int ry = g->y + (r * rstep - off); /* 本行格顶视口坐标 */
|
||||
for (int c = 0; c < g->cols; c++) {
|
||||
int index = r * g->cols + c;
|
||||
if (index >= n) break; /* 末行余下空格 */
|
||||
int cx = g->x + c * cstep;
|
||||
/* 行完全滚出视口(裁剪由 clip 兜底,这里跳过整行) */
|
||||
if (ry >= g->y + g->h) break;
|
||||
if (ry + g->cell_h <= g->y) continue;
|
||||
|
||||
if (g->draw_cb) {
|
||||
int st = (index == g->sel) ? MEOW_LAZYGRID_ST_SELECTED : 0;
|
||||
g->draw_cb(index, cx, ry, g->cell_w, g->cell_h, st, g->draw_ctx);
|
||||
} else {
|
||||
meow_lazygrid_item *it = lg_slot(g, index);
|
||||
if (!it) continue;
|
||||
lg_draw_cell(g, th, index, cx, ry, it);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
meow_gfx_clip_pop();
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
/*
|
||||
* meow_lyric.c — SDK 通用歌词组件实现(见 meow_lyric.h 的契约说明)。
|
||||
*
|
||||
* 本 TU 为 ncm_app/app/logic/lyric.c(解析/清空/查询)+ player.c 的 lyr_wrap_lines(折行)的
|
||||
* 通用化上收:去掉 ncm 特有命名(NCM_LYRIC_LINES / LRC_MAX_TEXT / lyrical 前缀),改用
|
||||
* meow_lyric_* 前缀与 SDK 命名;行上限由编译期宏变为「静态硬上限 + 运行时可配置」;
|
||||
* 时间戳存储用 int64_t(跨 32/64 位宿主一致);宽测量由使用方注入回调(不解耦渲染层)。
|
||||
* 逻辑(LRC 时间标签解析、时间升序排序、翻译按 timestamp 合并、字面 \n 当换行、
|
||||
* 流式有界、超宽折行中西文混排)与 ncm 逐行保持一致;改动仅限命名/接口/上限可配。
|
||||
*
|
||||
* 线程安全:单把 pthread 互斥锁(函数级,写侧整段持锁、读侧每次持锁,无嵌套;
|
||||
* 内部清空/解析用 nolock 私有版避免自锁死)。与 ncm lyric.c 的机制完全相同。
|
||||
*
|
||||
* 内存纪律:静态存储(行数组 + 文本缓冲)零 malloc;仅 meow_lyric_parse 排序时在**堆**上
|
||||
* 申请瞬态索引/拷贝数组(ncm 实测:600 行×192 字节的临时数组若放栈会占 ~227KB,超出后台
|
||||
* 线程 128KB 栈 → 栈溢出 SIGSEGV;放堆按 s_n 计、用后即 free)。任一 malloc 失败保留未排序
|
||||
* (不崩,查询仍可用)。
|
||||
*/
|
||||
#include "meow_lyric.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <pthread.h>
|
||||
|
||||
/* ---- 运行时状态(静态,解析/查询全程零 malloc;仅排序时瞬态堆)---- */
|
||||
static int64_t s_t[MEOW_LYRIC_LINES_MAX]; /* 每行时间戳(ms) */
|
||||
static char s_txt[MEOW_LYRIC_LINES_MAX][MEOW_LYRIC_TEXT_MAX]; /* 原文 */
|
||||
static char s_tl[MEOW_LYRIC_LINES_MAX][MEOW_LYRIC_TEXT_MAX]; /* 翻译 */
|
||||
static int s_n = 0; /* 有效行数 */
|
||||
static int g_max_lines = MEOW_LYRIC_LINES_MAX; /* 运行时可配行上限(默认 600) */
|
||||
static pthread_mutex_t g_lyric_mu = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
/* 清空(nolock,内部用;调用方须已持锁)。 */
|
||||
static void meow_lyric_clear_nolock(void)
|
||||
{
|
||||
s_n = 0;
|
||||
memset(s_t, 0, sizeof(s_t));
|
||||
memset(s_txt, 0, sizeof(s_txt));
|
||||
memset(s_tl, 0, sizeof(s_tl));
|
||||
}
|
||||
|
||||
/* 复制一行歌词正文 n 字节到 dst,把「字面反斜杠 n/r」序列(服务器用 \n 把多行文本塞进一行,
|
||||
* 字面 \n 渲染成豆腐块/凌乱)替换为空格;顺带把 \r\n / \n\r 折叠为单空格。有界 ≤max。
|
||||
* 语义与 ncm copy_lrc_text 一致。 */
|
||||
static void meow_lyric_copy_text(char *dst, const char *src, size_t n, size_t max)
|
||||
{
|
||||
size_t oi = 0, si = 0;
|
||||
while (si < n && oi < max - 1) {
|
||||
if (src[si] == '\\' && si + 1 < n && (src[si + 1] == 'n' || src[si + 1] == 'r')) {
|
||||
dst[oi++] = ' ';
|
||||
si += 2;
|
||||
/* \r\n / \n\r 成对 → 只留一个空格(跳过紧随的互补转义,防双空格) */
|
||||
if (si + 1 < n && src[si] == '\\' &&
|
||||
((src[si + 1] == 'r' && src[si - 1] == 'n') ||
|
||||
(src[si + 1] == 'n' && src[si - 1] == 'r')))
|
||||
si += 2;
|
||||
} else {
|
||||
dst[oi++] = src[si++];
|
||||
}
|
||||
}
|
||||
dst[oi] = 0;
|
||||
}
|
||||
|
||||
/* 解析单个 [mm:ss.ff] 时间标签为毫秒。返回 1=合法时间标签(*out_ms、*next=']' 之后);
|
||||
* 0=非时间标签(*next=']' 之后;NULL=无闭合 ']',调用方按残缺处理)。
|
||||
* 小数位自适应:1 位=0.1s(×100)、2 位=0.01s(×10,标准 LRC 厘秒)、3 位=0.001s(×1,
|
||||
* 网易云 LRC 毫秒)、>3 位截断到毫秒。 */
|
||||
static int meow_lyric_parse_tag(const char *p, int64_t *out_ms, const char **next)
|
||||
{
|
||||
const char *q = p + 1; /* 跳过 '[' */
|
||||
int m = 0, md = 0, s = 0, sd = 0;
|
||||
while (*q >= '0' && *q <= '9') { m = m * 10 + (*q - '0'); md++; q++; }
|
||||
if (md == 0 || *q != ':') goto notime;
|
||||
q++;
|
||||
while (*q >= '0' && *q <= '9') { s = s * 10 + (*q - '0'); sd++; q++; }
|
||||
if (sd == 0) goto notime;
|
||||
long frac = 0; int fd = 0; long scale = 10;
|
||||
if (*q == '.') {
|
||||
q++;
|
||||
while (*q >= '0' && *q <= '9') { frac = frac * 10 + (*q - '0'); fd++; q++; }
|
||||
if (fd == 1) scale = 100;
|
||||
else if (fd == 2) scale = 10;
|
||||
else if (fd >= 3) { scale = 1; while (fd > 3) { frac /= 10; fd--; } } /* >3 位截断到 ms */
|
||||
else scale = 0; /* 孤立 '.' 无数字:忽略小数 */
|
||||
}
|
||||
if (*q != ']') goto notime;
|
||||
*out_ms = (int64_t)m * 60000L + (int64_t)s * 1000L + frac * scale;
|
||||
*next = q + 1;
|
||||
return 1;
|
||||
notime:
|
||||
{
|
||||
const char *r = strchr(p, ']');
|
||||
*next = r ? r + 1 : NULL;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 解析一批 LRC 行(原文 is_trans=0 / 翻译 is_trans=1)。翻译按 first_ms 匹配到已有原文行合并。
|
||||
* 行上限取 g_max_lines;超出的原文行丢弃。不持锁(调用方已持锁)。 */
|
||||
static void meow_lyric_parse_lines(const char *lrc, int is_trans)
|
||||
{
|
||||
if (!lrc) return;
|
||||
const char *p = lrc;
|
||||
while (*p) {
|
||||
while (*p && *p != '[') p++;
|
||||
if (!*p) break;
|
||||
/* 解析一个或多个时间标签 */
|
||||
int have = 0;
|
||||
int64_t first_ms = 0;
|
||||
while (*p == '[') {
|
||||
int64_t ms;
|
||||
const char *after;
|
||||
if (meow_lyric_parse_tag(p, &ms, &after)) {
|
||||
if (!have) { first_ms = ms; have = 1; }
|
||||
p = after;
|
||||
} else {
|
||||
/* 非时间标签([ti:..] 等)——跳到行尾 */
|
||||
if (!after) return; /* 无闭合 ']':残缺文件 */
|
||||
p = after;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (have) {
|
||||
/* 正文 = 到行尾(去 \r\n) */
|
||||
const char *e = p;
|
||||
while (*e && *e != '\n' && *e != '\r') e++;
|
||||
size_t len = (size_t)(e - p);
|
||||
if (len > MEOW_LYRIC_TEXT_MAX - 1) len = MEOW_LYRIC_TEXT_MAX - 1;
|
||||
if (is_trans) {
|
||||
/* 翻译行:按 first_ms 匹配到已有原文行 */
|
||||
for (int i = 0; i < s_n; i++) {
|
||||
if (s_t[i] == first_ms) {
|
||||
meow_lyric_copy_text(s_tl[i], p, len, MEOW_LYRIC_TEXT_MAX);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (s_n < g_max_lines) {
|
||||
s_t[s_n] = first_ms;
|
||||
meow_lyric_copy_text(s_txt[s_n], p, len, MEOW_LYRIC_TEXT_MAX);
|
||||
s_tl[s_n][0] = 0;
|
||||
s_n++;
|
||||
}
|
||||
}
|
||||
}
|
||||
while (*p && *p != '\n') p++; /* 跳到下一行 */
|
||||
if (*p == '\n') p++;
|
||||
}
|
||||
}
|
||||
|
||||
/* qsort 比较:按时间戳升序(用索引数组。s_t 为静态数组,比较器直接读它)。 */
|
||||
static int meow_lyric_cmp(const void *a, const void *b)
|
||||
{
|
||||
const int *ia = (const int *)a, *ib = (const int *)b;
|
||||
if (s_t[*ia] < s_t[*ib]) return -1;
|
||||
if (s_t[*ia] > s_t[*ib]) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 折行宽测量的兜底:字节数近似(仅当使用方未注入 width 时;非像素精确)。
|
||||
* 保证 meow_lyric_wrap 在无渲染引擎下仍行为确定(maxw 作字节预算,仍按中西文断行)。 */
|
||||
static int meow_lyric_default_width(const char *u, int size, void *ctx)
|
||||
{
|
||||
(void)size; (void)ctx;
|
||||
return (int)strlen(u);
|
||||
}
|
||||
|
||||
void meow_lyric_clear(void)
|
||||
{
|
||||
pthread_mutex_lock(&g_lyric_mu);
|
||||
meow_lyric_clear_nolock();
|
||||
pthread_mutex_unlock(&g_lyric_mu);
|
||||
}
|
||||
|
||||
int meow_lyric_parse(const char *lrc, const char *tlrc)
|
||||
{
|
||||
pthread_mutex_lock(&g_lyric_mu);
|
||||
meow_lyric_clear_nolock();
|
||||
meow_lyric_parse_lines(lrc, 0);
|
||||
meow_lyric_parse_lines(tlrc, 1);
|
||||
/* 按时间排序(用索引数组)。临时数组放**堆**上:`char [600][192]`×2 + `int64[600]`
|
||||
* 若放栈会占 ~227KB,超过后台线程 128KB 栈 → 栈溢出 SIGSEGV。堆上按 s_n 计
|
||||
* (≤600 行),峰值 ~230KB 瞬态、用后即 free。 */
|
||||
if (s_n > 0) {
|
||||
int *idx = (int *)malloc(sizeof(int) * (size_t)s_n);
|
||||
int64_t *ttmp = (int64_t *)malloc(sizeof(int64_t) * (size_t)s_n);
|
||||
char (*txt)[MEOW_LYRIC_TEXT_MAX] =
|
||||
(char (*)[MEOW_LYRIC_TEXT_MAX])malloc((size_t)MEOW_LYRIC_TEXT_MAX * (size_t)s_n);
|
||||
char (*tl)[MEOW_LYRIC_TEXT_MAX] =
|
||||
(char (*)[MEOW_LYRIC_TEXT_MAX])malloc((size_t)MEOW_LYRIC_TEXT_MAX * (size_t)s_n);
|
||||
if (idx && ttmp && txt && tl) {
|
||||
for (int i = 0; i < s_n; i++) idx[i] = i;
|
||||
qsort(idx, (size_t)s_n, sizeof(int), meow_lyric_cmp);
|
||||
memcpy(ttmp, s_t, sizeof(int64_t) * (size_t)s_n);
|
||||
memcpy(txt, s_txt, (size_t)MEOW_LYRIC_TEXT_MAX * (size_t)s_n);
|
||||
memcpy(tl, s_tl, (size_t)MEOW_LYRIC_TEXT_MAX * (size_t)s_n);
|
||||
for (int i = 0; i < s_n; i++) {
|
||||
s_t[i] = ttmp[idx[i]];
|
||||
memcpy(s_txt[i], txt[idx[i]], (size_t)MEOW_LYRIC_TEXT_MAX);
|
||||
memcpy(s_tl[i], tl[idx[i]], (size_t)MEOW_LYRIC_TEXT_MAX);
|
||||
}
|
||||
}
|
||||
free(idx); free(ttmp); free(txt); free(tl); /* 任一 malloc 失败:保留未排序(不崩) */
|
||||
}
|
||||
int result = s_n;
|
||||
pthread_mutex_unlock(&g_lyric_mu);
|
||||
return result;
|
||||
}
|
||||
|
||||
int meow_lyric_count(void)
|
||||
{
|
||||
pthread_mutex_lock(&g_lyric_mu);
|
||||
int n = s_n;
|
||||
pthread_mutex_unlock(&g_lyric_mu);
|
||||
return n;
|
||||
}
|
||||
|
||||
int meow_lyric_line_at(int64_t ms)
|
||||
{
|
||||
pthread_mutex_lock(&g_lyric_mu);
|
||||
int line = 0;
|
||||
for (int i = 0; i < s_n; i++) {
|
||||
if (s_t[i] <= ms) line = i;
|
||||
else break;
|
||||
}
|
||||
pthread_mutex_unlock(&g_lyric_mu);
|
||||
return line;
|
||||
}
|
||||
|
||||
const char *meow_lyric_line_text(int i)
|
||||
{
|
||||
pthread_mutex_lock(&g_lyric_mu);
|
||||
const char *p = (i < 0 || i >= s_n) ? "" : s_txt[i];
|
||||
pthread_mutex_unlock(&g_lyric_mu);
|
||||
return p;
|
||||
}
|
||||
|
||||
const char *meow_lyric_line_tl(int i)
|
||||
{
|
||||
pthread_mutex_lock(&g_lyric_mu);
|
||||
const char *p = (i < 0 || i >= s_n) ? "" : s_tl[i];
|
||||
pthread_mutex_unlock(&g_lyric_mu);
|
||||
return p;
|
||||
}
|
||||
|
||||
int64_t meow_lyric_line_time(int i)
|
||||
{
|
||||
pthread_mutex_lock(&g_lyric_mu);
|
||||
int64_t t = (i < 0 || i >= s_n) ? -1 : s_t[i];
|
||||
pthread_mutex_unlock(&g_lyric_mu);
|
||||
return t;
|
||||
}
|
||||
|
||||
void meow_lyric_set_max_lines(int max)
|
||||
{
|
||||
pthread_mutex_lock(&g_lyric_mu);
|
||||
if (max < 1) max = 1;
|
||||
if (max > MEOW_LYRIC_LINES_MAX) max = MEOW_LYRIC_LINES_MAX;
|
||||
g_max_lines = max;
|
||||
pthread_mutex_unlock(&g_lyric_mu);
|
||||
}
|
||||
|
||||
int meow_lyric_wrap(const char *txt, int size, int maxw,
|
||||
char seg[][MEOW_LYRIC_SEG_SZ], int max_rows,
|
||||
meow_lyric_width_fn width, void *ctx)
|
||||
{
|
||||
meow_lyric_width_fn w = width ? width : meow_lyric_default_width;
|
||||
if (!txt || !txt[0] || !seg || max_rows <= 0) {
|
||||
if (seg) seg[0][0] = 0;
|
||||
return 1;
|
||||
}
|
||||
int n = 0;
|
||||
size_t len = strlen(txt), pos = 0;
|
||||
while (pos < len && n < max_rows) {
|
||||
size_t start = pos;
|
||||
int last_ws = -1; /* 本行最后一个空格(相对 start 的字节偏移) */
|
||||
int overflow = 0;
|
||||
while (pos < len) {
|
||||
unsigned char c = (unsigned char)txt[pos];
|
||||
int ch = (c < 0x80) ? 1 : (c < 0xE0) ? 2 : (c < 0xF0) ? 3 : 4;
|
||||
if (pos + (size_t)ch > len) ch = (int)(len - pos);
|
||||
size_t cand = (pos + (size_t)ch) - start;
|
||||
size_t cp = cand < MEOW_LYRIC_SEG_SZ - 1 ? cand : MEOW_LYRIC_SEG_SZ - 1;
|
||||
memcpy(seg[n], txt + start, cp); seg[n][cp] = 0;
|
||||
if (w(seg[n], size, ctx) > maxw && cand > 0) { overflow = 1; break; }
|
||||
if (txt[pos] == ' ' || txt[pos] == '\t') last_ws = (int)(pos - start);
|
||||
pos += (size_t)ch;
|
||||
}
|
||||
if (overflow) {
|
||||
size_t linelen = (last_ws > 0) ? (size_t)last_ws : (pos - start);
|
||||
if (linelen == 0) { /* 单字符超宽(罕见):至少放一个字符防死循环 */
|
||||
unsigned char c = (unsigned char)txt[start];
|
||||
int ch = (c < 0x80) ? 1 : (c < 0xE0) ? 2 : (c < 0xF0) ? 3 : 4;
|
||||
linelen = (size_t)ch;
|
||||
}
|
||||
size_t cp = linelen < MEOW_LYRIC_SEG_SZ - 1 ? linelen : MEOW_LYRIC_SEG_SZ - 1;
|
||||
memcpy(seg[n], txt + start, cp); seg[n][cp] = 0;
|
||||
pos = start + linelen;
|
||||
if (last_ws > 0) {
|
||||
size_t sp = start + (size_t)last_ws + 1;
|
||||
if (sp > pos) pos = sp; /* 跳过断点后的一个空格 */
|
||||
}
|
||||
n++;
|
||||
} else {
|
||||
size_t cp = (len - start) < MEOW_LYRIC_SEG_SZ - 1 ? (len - start) : MEOW_LYRIC_SEG_SZ - 1;
|
||||
memcpy(seg[n], txt + start, cp); seg[n][cp] = 0;
|
||||
n++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (n == 0) { seg[0][0] = 0; n = 1; }
|
||||
return n;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* meow_lyric.h — MeowOS SDK 通用歌词组件(LRC 时间轴解析 / 存取 / 查询 + 折行辅助)。
|
||||
*
|
||||
* 定位:把网易云 ncm_app 的歌词解析/查询能力(app/logic/lyric.c 的 parse/clear/count/
|
||||
* line_at/line_text/line_tl + copy_lrc_text 的「字面 \n 当换行」语义 + player.c 的
|
||||
* lyr_wrap_lines 折行)**通用化上收到 SDK**,供 launcher / ncm 等任意 App 复用。
|
||||
* 本头为**独立公共头**(仿 meow_ui_view.h 模式:不随 meow_ui.h 伞头发布,App 按需
|
||||
* `#include "meow_lyric.h"`),命名与 SDK 规范一致:API 一律 `meow_lyric_*`。
|
||||
*
|
||||
* 输入:标准 LRC 行 `[mm:ss.xx]正文`。lrc=原文,tlrc=翻译。解析把带时间戳的行按时间升序
|
||||
* 排序存 timestamp(ms)/text;翻译按相同 timestamp 合并到对应原文行(存 tl),无匹配的翻译行
|
||||
* 忽略。流式、有界:行数上限默认 MEOW_LYRIC_LINES_MAX(600),运行时可经
|
||||
* meow_lyric_set_max_lines 下调;单行文本上限 MEOW_LYRIC_TEXT_MAX(192)字节。
|
||||
*
|
||||
* 服务器字面序列:网易云服务器把多行文本用字面 `\n`/`\r` 塞进一行(渲染成豆腐块/凌乱),
|
||||
* 解析时按空格替换(`\r\n`/`\n\r` 折成单空格),语义与 ncm copy_lrc_text 一致。
|
||||
*
|
||||
* 线程安全(单写多读,机制与 ncm lyric.c 相同的交错安全):数据由后台线程写
|
||||
* (meow_lyric_parse / meow_lyric_clear / meow_lyric_set_max_lines),UI 线程读
|
||||
* (meow_lyric_count / line_at / line_text / line_tl / line_time)。全部函数内部持同一把
|
||||
* pthread 互斥锁,函数级粒度,写侧整段持锁、读侧每次访问持锁,无嵌套(清空/解析用 nolock
|
||||
* 私有版,避免二次取锁自锁死)。**读接口返回的指针指向静态存储,在下次 meow_lyric_parse /
|
||||
* meow_lyric_clear 之前有效**;调用方不得跨重建持有。这是与 ncm 完全相同的「指针在下次写前
|
||||
* 有效」契约。
|
||||
*
|
||||
* 依赖:纯 libc(stdlib/string/stdio)+ pthread,不依赖其它 SDK 对象(不含 meow_log,便于
|
||||
* launcher 等非 meow_ui.a 使用方独立链入)。宿主 gcc 可直接编译单测。
|
||||
*
|
||||
* 时间戳小数位自适应:1 位=0.1s(×100)、2 位=0.01s(×10,标准 LRC 厘秒)、3 位=0.001s(×1,
|
||||
* 网易云 LRC 毫秒)、>3 位截断到毫秒 —— 避免把网易云 3 位毫秒当 2 位厘秒解析导致时间放大。
|
||||
*/
|
||||
#ifndef MEOW_LYRIC_H
|
||||
#define MEOW_LYRIC_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ---- 容量 / 静态存储 ---- */
|
||||
#define MEOW_LYRIC_LINES_MAX 600 /* 静态行数组硬上限(对齐 ncm NCM_LYRIC_LINES) */
|
||||
#define MEOW_LYRIC_TEXT_MAX 192 /* 单行文本(原文/翻译)字节上限(对齐 ncm LRC_MAX_TEXT) */
|
||||
#define MEOW_LYRIC_SEG_SZ 256 /* 折行单段缓冲字节上限(对齐 ncm LYR_SEG_SZ) */
|
||||
|
||||
/* ---- 折行宽测量回调 ----
|
||||
* 由使用方注入(歌词视图传 meow_ui_text_width 的包装;宿主测传自己的测量函数)。
|
||||
* 返回 utf8 段在字号 size 下的宽度(px)。NULL 时回退为「字节数近似」(非像素精确,仅兜底)。 */
|
||||
typedef int (*meow_lyric_width_fn)(const char *utf8, int size, void *ctx);
|
||||
|
||||
/* ---- 解析 / 清空 ----
|
||||
* meow_lyric_parse:重置并解析 lrc(原文)+ tlrc(翻译),按时间升序排序后返回歌词行数
|
||||
* (=meow_lyric_count 的值;<- 解析失败/空输入返回 0)。翻译按相同时间戳合并到对应原文行。
|
||||
* meow_lyric_clear:清空全部数据(下次 read 返回空)。 */
|
||||
void meow_lyric_clear(void);
|
||||
int meow_lyric_parse(const char *lrc, const char *tlrc);
|
||||
|
||||
/* ---- 查询(全部线程安全;line_text/tl 返回静态串,越界返回 "")---- */
|
||||
int meow_lyric_count(void);
|
||||
int meow_lyric_line_at(int64_t ms); /* 当前行下标:last timestamp<=ms;首戳前=0;末戳后=n-1 */
|
||||
const char *meow_lyric_line_text(int i); /* 第 i 行原文;越界 "" */
|
||||
const char *meow_lyric_line_tl(int i); /* 第 i 行翻译;越界/无翻译 "" */
|
||||
int64_t meow_lyric_line_time(int i); /* 第 i 行时间戳(ms);越界 -1(供「点行跳转」seek) */
|
||||
|
||||
/* ---- 行上限配置 ----
|
||||
* 缺省 MEOW_LYRIC_LINES_MAX(600)。可下调至 [1, MEOW_LYRIC_LINES_MAX];下一次 parse 起生效
|
||||
* (解析行数超过上限时超出的行被截断丢弃)。 */
|
||||
void meow_lyric_set_max_lines(int max);
|
||||
|
||||
/* ---- 折行辅助(上收 ncm lyr_wrap_lines,非线程相关、纯函数)----
|
||||
* 把 txt 按「词/字符」折成 ≤max_rows 段,每段宽度 ≤maxw px(宽测由 width 回调给出;中文按字符、
|
||||
* 英文按词断行)。段写入调用方 seg[][MEOW_LYRIC_SEG_SZ](seg 须能容纳 max_rows 个段),
|
||||
* 返回段数(≥1;txt 空/超短返回 1 段空串)。单字符超宽时强制容纳该字符,防止死循环。 */
|
||||
int meow_lyric_wrap(const char *txt, int size, int maxw,
|
||||
char seg[][MEOW_LYRIC_SEG_SZ], int max_rows,
|
||||
meow_lyric_width_fn width, void *ctx);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MEOW_LYRIC_H */
|
||||
@@ -0,0 +1,362 @@
|
||||
/*
|
||||
* meow_lyric_view.c — SDK 通用歌词页渲染控件实现(见 meow_lyric_view.h 的契约说明)。
|
||||
*
|
||||
* 上收自 ncm_app/app/ui/player.c:lyr_layout(活动行块中心偏上锚定、上方 ≥LYR_ABOVE_MIN 行已过、
|
||||
* 下填满、总 ≤LYR_MAX_ROWS)+ plr_draw_lyrics(活动行纯白、上下行逐级灰阶、翻译紧跟随不叠行,
|
||||
* 块高含翻译行是 agent-380 修好 s_lyr_bh 的关键)+ lyrics_line_tap(命中=布局逐块扫描)。折行复用
|
||||
* 批1 meow_lyric_wrap(=ncm lyr_wrap_lines 的通用化),宽度测量经注入的 meow_lyric_width_fn。
|
||||
*
|
||||
* 控件形态:W_CUSTOM 自绘画布(meow_widget_custom_create),render_cb/hit_cb 由本 TU 实现。
|
||||
* 布局(lv_compute_layout)计算结果缓存在 lv->vis[0..vis_n),绘制/命中/公开查询共用,三者一致。
|
||||
*
|
||||
* 内存纪律:per-instance struct(calloc);布局数组定长 [MEOW_LYRIC_VIEW_MAX_ROWS];折行复用缓冲
|
||||
* 一块 [MEOW_LYRIC_VIEW_MAX_ROWS][MEOW_LYRIC_SEG_SZ](逐条目复用,同 ncm s_lyr_seg 模式);
|
||||
* 零临时堆分配(meow_lyric_wrap 静态缓冲)。
|
||||
*/
|
||||
#include "meow_lyric_view.h"
|
||||
#include "meow_ui_internal.h" /* meow_widget_rect:widget 为不透明类型,取几何用访问器 */
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* 折行宽测量的兜底:字节数近似(仅当使用方未注入 width 时;非像素精确)。
|
||||
* 保证无渲染引擎下行为确定(maxw 作字节预算,仍按中西文断行)——同 meow_lyric_wrap 的 NULL 语义。 */
|
||||
static int lv_default_width(const char *u, int size, void *ctx)
|
||||
{
|
||||
(void)size; (void)ctx;
|
||||
return (int)strlen(u);
|
||||
}
|
||||
|
||||
/* 逐通道比例缩放(t=0..255,255=原色)。灰度递减用:对 cur 逐级缩灰(对纯白即复现 ncm 灰阶表)。 */
|
||||
static uint32_t lv_scale_col(uint32_t c, int t)
|
||||
{
|
||||
int r = ((int)((c >> 16) & 255) * t) / 255;
|
||||
int g = ((int)((c >> 8) & 255) * t) / 255;
|
||||
int b = ((int)( c & 255) * t) / 255;
|
||||
return ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)b;
|
||||
}
|
||||
|
||||
/* 灰阶比例(距活动行 0..5):0xFFFFFF→{FFFFFF,BBBBBB,929292,808080,6A6A6A,555555}(ncm lyr_gray_tbl)。
|
||||
* 对非纯白 cur 按同比例缩灰,仍单调变灰。 */
|
||||
static const int lv_gray_fac[6] = { 255, 187, 146, 128, 106, 85 };
|
||||
|
||||
/* 两色线性混合(t=0..255,0=纯 a、255=纯 b)。翻译行融入背景用(无 GPU,逐通道整数混)。 */
|
||||
static uint32_t lv_mix(uint32_t a, uint32_t b, int t)
|
||||
{
|
||||
if (t < 0) t = 0;
|
||||
if (t > 255) t = 255;
|
||||
int ar = (a >> 16) & 255, ag = (a >> 8) & 255, ab = a & 255;
|
||||
int br = (b >> 16) & 255, bg = (b >> 8) & 255, bb = b & 255;
|
||||
int r = ar + ((br - ar) * t + 128) / 255;
|
||||
int g = ag + ((bg - ag) * t + 128) / 255;
|
||||
int bl = ab + ((bb - ab) * t + 128) / 255;
|
||||
return ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)bl;
|
||||
}
|
||||
|
||||
struct meow_lyric_view {
|
||||
meow_widget *w;
|
||||
/* 宽度测量(复用批1 meow_lyric_wrap 的 width_fn 约定;NULL→字节近似兜底) */
|
||||
meow_lyric_width_fn width;
|
||||
void *width_ctx;
|
||||
int64_t pos_ms; /* 当前播放位置(UI 线程写) */
|
||||
meow_lyric_view_colors colors; /* 用户覆盖(has_colors=1 生效;否则主题默认) */
|
||||
int has_colors;
|
||||
meow_lyric_view_layout l; /* 布局参数(内建默认/用户覆盖) */
|
||||
char empty[64]; /* 无歌词占位文案 */
|
||||
meow_lyric_seek_fn on_seek;
|
||||
void *seek_ctx;
|
||||
/* 布局暂存(每次 lv_compute_layout 重建;UI 线程串行,per-instance 复用) */
|
||||
meow_lyric_view_vis vis[MEOW_LYRIC_VIEW_MAX_ROWS];
|
||||
int vis_n;
|
||||
char seg[MEOW_LYRIC_VIEW_MAX_ROWS][MEOW_LYRIC_SEG_SZ]; /* 折行复用缓冲(逐条目覆盖) */
|
||||
};
|
||||
|
||||
/* 内建默认布局(对齐 ncm LYR_PAD/SUB_LH/CUR_SZ/OTH_SZ/TL_SZ/MAX_ROWS/ABOVE_MIN/ABOVE_OFF)。 */
|
||||
static const meow_lyric_view_layout k_def_layout = {
|
||||
24, 44, 28, 24, 20, MEOW_LYRIC_VIEW_MAX_ROWS, 2, 50
|
||||
};
|
||||
static const char k_def_empty[] = "歌词加载中…";
|
||||
|
||||
/* 解析生效色:!has_colors 或字段 0/scrim_alpha<0 → 主题默认;否则用覆盖。out 必填。 */
|
||||
static void lv_resolve_colors(const meow_lyric_view *lv, const meow_theme *th,
|
||||
meow_lyric_view_colors *out)
|
||||
{
|
||||
/* 内建默认(theme 驱动) */
|
||||
uint32_t scrim = th->scrim;
|
||||
if (scrim == 0) scrim = th->surfaceDim; /* scrim 缺省为黑:用 surfaceDim 兜底(非纯黑) */
|
||||
out->scrim = scrim;
|
||||
out->cur = th->onSurface; /* dark=0xFFFFFF 纯白(活动行高亮) */
|
||||
out->scrim_alpha = 176;
|
||||
if (!lv->has_colors) {
|
||||
out->gray[0] = out->cur;
|
||||
for (int i = 1; i < 6; i++) out->gray[i] = lv_scale_col(out->cur, lv_gray_fac[i]);
|
||||
return;
|
||||
}
|
||||
/* 用户覆盖(0 字段=沿用内建默认;scrim_alpha<0=沿用默认 176)。
|
||||
* gray 显式字段优先;未给字段以「解析后 cur」为基准自动递减(对齐头文档
|
||||
* "gray[1..5]=对 cur 逐级缩灰"——覆盖 cur 后灰阶也应随 cur,而非随主题默认 onSurface)。 */
|
||||
if (lv->colors.scrim != 0) out->scrim = lv->colors.scrim;
|
||||
if (lv->colors.cur != 0) out->cur = lv->colors.cur;
|
||||
if (lv->colors.scrim_alpha >= 0) out->scrim_alpha = lv->colors.scrim_alpha;
|
||||
out->gray[0] = (lv->colors.gray[0] != 0) ? lv->colors.gray[0] : out->cur;
|
||||
for (int i = 1; i < 6; i++)
|
||||
out->gray[i] = (lv->colors.gray[i] != 0) ? lv->colors.gray[i]
|
||||
: lv_scale_col(out->cur, lv_gray_fac[i]);
|
||||
}
|
||||
|
||||
/* 距活动行 dist 的生效行色(dist 越界 clamp 0..5)。 */
|
||||
static uint32_t lv_line_color(const meow_lyric_view *lv, const meow_theme *th, int dist)
|
||||
{
|
||||
meow_lyric_view_colors c;
|
||||
lv_resolve_colors(lv, th, &c);
|
||||
if (dist < 0) dist = 0;
|
||||
if (dist > 5) dist = 5;
|
||||
return c.gray[dist];
|
||||
}
|
||||
|
||||
/* 折行封套(width 缺省兜底;越界 max_rows 内 clamp 到缓冲容量)。返回段数(≥1)。 */
|
||||
static int lv_wrap(const meow_lyric_view *lv, const char *txt, int size, int maxw,
|
||||
char seg[][MEOW_LYRIC_SEG_SZ], int max_rows)
|
||||
{
|
||||
meow_lyric_width_fn w = lv->width ? lv->width : lv_default_width;
|
||||
if (max_rows > MEOW_LYRIC_VIEW_MAX_ROWS) max_rows = MEOW_LYRIC_VIEW_MAX_ROWS;
|
||||
int rc = meow_lyric_wrap(txt, size, maxw, seg, max_rows, w, lv->width_ctx);
|
||||
if (rc > max_rows) rc = max_rows;
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* 核心布局:按 pos_ms 计算可见条目(vis[0..n)。算法与 ncm lyr_layout 逐行为一致)——
|
||||
* 活动行块中心锚定在视图中线 - above_off(偏上);上方保底 ≥above_min 行已过;下填满;总 ≤max_rows;
|
||||
* 块高 bh = 原文折行数 rw + 当前行翻译占 1(其余条目无翻译,bh=rw;翻译不叠行,点行命中一致)。 */
|
||||
static int lv_compute_layout(meow_lyric_view *lv, int64_t pos_ms, int maxw)
|
||||
{
|
||||
int n = meow_lyric_count();
|
||||
int cur = (n > 0) ? meow_lyric_line_at(pos_ms) : -1;
|
||||
memset(lv->vis, 0, sizeof(lv->vis));
|
||||
lv->vis_n = 0;
|
||||
if (cur < 0 || cur >= n) return 0;
|
||||
|
||||
int MAX = lv->l.max_rows;
|
||||
if (MAX > MEOW_LYRIC_VIEW_MAX_ROWS) MAX = MEOW_LYRIC_VIEW_MAX_ROWS;
|
||||
if (MAX < 1) MAX = 1;
|
||||
|
||||
int rc = lv_wrap(lv, meow_lyric_line_text(cur), lv->l.cur_sz, maxw, lv->seg, MAX);
|
||||
const char *tl = meow_lyric_line_tl(cur);
|
||||
int has_tl = (tl && tl[0]) ? 1 : 0;
|
||||
int rc_tot = rc + has_tl; /* 活动行块总子行数(原文折行 + 翻译行) */
|
||||
|
||||
int cap = MAX - rc_tot; /* 活动行之外可用的视觉行数 */
|
||||
if (cap < 0) cap = 0;
|
||||
int above_want = (cap * 2) / 5; /* 偏上锚定:约 40% 行给上方 */
|
||||
if (above_want > cur) above_want = cur; /* 上方行数上限 = 已过行数 */
|
||||
if (above_want < lv->l.above_min) above_want = (cur >= lv->l.above_min) ? lv->l.above_min : cur;
|
||||
if (above_want > cap) above_want = cap; /* 总行数 ≤ MAX */
|
||||
|
||||
/* 1) 上方(cur-1 向上)前插到头部,保持自顶向下 */
|
||||
int a_cnt = 0, usedA = 0;
|
||||
for (int i = cur - 1; i >= 0 && usedA < above_want; i--) {
|
||||
int rr = lv_wrap(lv, meow_lyric_line_text(i), lv->l.oth_sz, maxw, lv->seg, MAX);
|
||||
if (usedA + rr > above_want) break;
|
||||
for (int k = a_cnt; k > 0; k--) lv->vis[k] = lv->vis[k - 1];
|
||||
lv->vis[0].ent = i; lv->vis[0].rw = rr; lv->vis[0].bh = rr; lv->vis[0].sz = lv->l.oth_sz;
|
||||
a_cnt++; usedA += rr;
|
||||
}
|
||||
/* 2) 当前行 */
|
||||
lv->vis[a_cnt].ent = cur; lv->vis[a_cnt].rw = rc; lv->vis[a_cnt].bh = rc_tot; lv->vis[a_cnt].sz = lv->l.cur_sz;
|
||||
/* 3) 下方(cur+1 向下) */
|
||||
int below_want = cap - usedA;
|
||||
if (below_want > (n - 1 - cur)) below_want = n - 1 - cur;
|
||||
int b_cnt = 0, usedB = 0;
|
||||
for (int i = cur + 1; i < n && usedB < below_want; i++) {
|
||||
int rr = lv_wrap(lv, meow_lyric_line_text(i), lv->l.oth_sz, maxw, lv->seg, MAX);
|
||||
if (usedB + rr > below_want) break;
|
||||
int k = a_cnt + 1 + b_cnt;
|
||||
lv->vis[k].ent = i; lv->vis[k].rw = rr; lv->vis[k].bh = rr; lv->vis[k].sz = lv->l.oth_sz;
|
||||
b_cnt++; usedB += rr;
|
||||
}
|
||||
int cnt = a_cnt + 1 + b_cnt;
|
||||
if (cnt == 0) return 0;
|
||||
|
||||
/* 4) y 定位:活动行原文块中心 = 视图中线 - above_off(偏上);上方向上、下方向下(块间无缝隙) */
|
||||
int rect[4]; meow_widget_rect(lv->w, rect);
|
||||
int anchor = rect[1] + rect[3] / 2 - lv->l.above_off; /* rect[1]=vy, rect[3]=vh */
|
||||
lv->vis[a_cnt].y = anchor - rc * lv->l.sub_lh / 2;
|
||||
for (int k = a_cnt + 1; k < cnt; k++)
|
||||
lv->vis[k].y = lv->vis[k - 1].y + lv->vis[k - 1].bh * lv->l.sub_lh;
|
||||
for (int k = a_cnt - 1; k >= 0; k--)
|
||||
lv->vis[k].y = lv->vis[k + 1].y - lv->vis[k].bh * lv->l.sub_lh;
|
||||
lv->vis_n = cnt;
|
||||
return cnt;
|
||||
}
|
||||
|
||||
/* 自绘回调:scrim 底 + 滚动歌词(活动行纯白偏上锚定 + 上下行逐级灰阶)+ 翻译行(不叠行)+ 空态占位。 */
|
||||
static void lv_render(meow_widget *w, const meow_theme *th, void *ctx)
|
||||
{
|
||||
meow_lyric_view *lv = (meow_lyric_view *)ctx;
|
||||
int rect[4]; meow_widget_rect(w, rect);
|
||||
int x = rect[0], vy = rect[1], W = rect[2], H = rect[3];
|
||||
meow_lyric_view_colors col;
|
||||
lv_resolve_colors(lv, th, &col);
|
||||
|
||||
/* 背景 scrim(半透明叠在封面之上;scrim_alpha==0=不画,App 已自绘叠加层)。 */
|
||||
if (col.scrim_alpha > 0) meow_ui_fill_rect_alpha(x, vy, W, H, col.scrim, col.scrim_alpha);
|
||||
|
||||
int n = meow_lyric_count();
|
||||
int cur = (n > 0) ? meow_lyric_line_at(lv->pos_ms) : -1;
|
||||
int maxw = W - 2 * lv->l.pad; if (maxw < 1) maxw = 1;
|
||||
int cnt = lv_compute_layout(lv, lv->pos_ms, maxw);
|
||||
uint32_t bgcol = col.scrim; /* 翻译行融入背景(渐隐) */
|
||||
for (int k = 0; k < cnt; k++) {
|
||||
int i = lv->vis[k].ent, rows = lv->vis[k].rw, sz = lv->vis[k].sz, vsy = lv->vis[k].y;
|
||||
int is_cur = (i == cur);
|
||||
int dist = is_cur ? 0 : ((i - cur) < 0 ? (cur - i) : (i - cur));
|
||||
uint32_t c = lv_line_color(lv, th, dist);
|
||||
int rr = lv_wrap(lv, meow_lyric_line_text(i), sz, maxw, lv->seg, rows);
|
||||
if (rr > rows) rr = rows;
|
||||
for (int r = 0; r < rr; r++)
|
||||
meow_ui_text(x + lv->l.pad, vsy + r * lv->l.sub_lh, lv->seg[r], sz, c);
|
||||
if (is_cur) {
|
||||
const char *tl = meow_lyric_line_tl(i);
|
||||
if (tl && tl[0])
|
||||
meow_ui_text(x + lv->l.pad, vsy + rows * lv->l.sub_lh + 2, tl,
|
||||
lv->l.tl_sz, lv_mix(0xFFFFFF, bgcol, 120));
|
||||
}
|
||||
}
|
||||
if (n == 0)
|
||||
meow_ui_text(x + lv->l.pad, vy + H / 2 - 10, lv->empty, 20, col.cur);
|
||||
}
|
||||
|
||||
/* 命中回调:点行 seek(用与绘制一致的布局逐块扫描;块外就近取整)。
|
||||
* 命中某行 → 存 last 行 + 回调 on_seek(ms)(若有),返回 1=消费;否则 0。 */
|
||||
static int lv_hit_cb(meow_widget *w, int gx, int gy, void *ctx)
|
||||
{
|
||||
meow_lyric_view *lv = (meow_lyric_view *)ctx;
|
||||
int rect[4]; meow_widget_rect(w, rect);
|
||||
int rx = rect[0], ry = rect[1], rw = rect[2], rh = rect[3];
|
||||
/* 视图矩形内含判定(含边缘;点必须在控件内)。 */
|
||||
if (gx < rx || gx >= rx + rw || gy < ry || gy >= ry + rh) return 0;
|
||||
int n = meow_lyric_count();
|
||||
if (n <= 0) return 0;
|
||||
int maxw = rw - 2 * lv->l.pad; if (maxw < 1) maxw = 1;
|
||||
int cnt = lv_compute_layout(lv, lv->pos_ms, maxw);
|
||||
if (cnt <= 0) return 0;
|
||||
int line = -1;
|
||||
for (int k = 0; k < cnt; k++) {
|
||||
int yb = lv->vis[k].y;
|
||||
int yh = lv->vis[k].bh * lv->l.sub_lh; /* 块高含当前行翻译,点翻译行归本行 */
|
||||
if (gy >= yb && gy < yb + yh) { line = lv->vis[k].ent; break; }
|
||||
}
|
||||
if (line < 0) /* 落在可见块之外 → 就近取整(顶部/底部可见行) */
|
||||
line = (gy < lv->vis[0].y) ? lv->vis[0].ent : lv->vis[cnt - 1].ent;
|
||||
if (lv->on_seek) {
|
||||
int64_t t = meow_lyric_line_time(line);
|
||||
if (t >= 0) lv->on_seek(t, lv->seek_ctx);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ---- 生命周期 ---- */
|
||||
meow_lyric_view *meow_lyric_view_create(int x, int y, int w, int h,
|
||||
meow_lyric_width_fn width, void *width_ctx)
|
||||
{
|
||||
meow_lyric_view *lv = (meow_lyric_view *)calloc(1, sizeof *lv);
|
||||
if (!lv) return NULL;
|
||||
lv->w = meow_widget_custom_create(x, y, w, h, lv_render, lv_hit_cb, lv);
|
||||
if (!lv->w) { free(lv); return NULL; }
|
||||
lv->width = width;
|
||||
lv->width_ctx = width_ctx;
|
||||
lv->l = k_def_layout;
|
||||
memcpy(lv->empty, k_def_empty, sizeof k_def_empty);
|
||||
return lv;
|
||||
}
|
||||
|
||||
void meow_lyric_view_free(meow_lyric_view *lv)
|
||||
{
|
||||
if (!lv) return;
|
||||
if (lv->w) meow_widget_free(lv->w);
|
||||
free(lv);
|
||||
}
|
||||
|
||||
meow_widget *meow_lyric_view_widget(const meow_lyric_view *lv)
|
||||
{
|
||||
return lv ? lv->w : NULL;
|
||||
}
|
||||
|
||||
void meow_lyric_view_set_pos(meow_lyric_view *lv, int64_t pos_ms)
|
||||
{
|
||||
if (lv) lv->pos_ms = pos_ms;
|
||||
}
|
||||
|
||||
void meow_lyric_view_set_colors(meow_lyric_view *lv, const meow_lyric_view_colors *c)
|
||||
{
|
||||
if (!lv) return;
|
||||
if (!c) { lv->has_colors = 0; return; }
|
||||
lv->colors = *c;
|
||||
lv->has_colors = 1;
|
||||
}
|
||||
|
||||
void meow_lyric_view_set_layout(meow_lyric_view *lv, const meow_lyric_view_layout *ls)
|
||||
{
|
||||
if (!lv) return;
|
||||
lv->l = ls ? *ls : k_def_layout;
|
||||
}
|
||||
|
||||
void meow_lyric_view_set_empty(meow_lyric_view *lv, const char *txt)
|
||||
{
|
||||
if (!lv) return;
|
||||
if (!txt || !txt[0]) { lv->empty[0] = 0; return; }
|
||||
strncpy(lv->empty, txt, sizeof lv->empty - 1);
|
||||
lv->empty[sizeof lv->empty - 1] = 0;
|
||||
}
|
||||
|
||||
void meow_lyric_view_set_on_seek(meow_lyric_view *lv, meow_lyric_seek_fn cb, void *ctx)
|
||||
{
|
||||
if (!lv) return;
|
||||
lv->on_seek = cb;
|
||||
lv->seek_ctx = ctx;
|
||||
}
|
||||
|
||||
/* ---- 查询 ---- */
|
||||
uint32_t meow_lyric_view_cur_color(const meow_lyric_view *lv)
|
||||
{
|
||||
if (!lv) return 0;
|
||||
return lv_line_color(lv, meow_theme_get(), 0);
|
||||
}
|
||||
|
||||
uint32_t meow_lyric_view_line_color(const meow_lyric_view *lv, int dist)
|
||||
{
|
||||
if (!lv) return 0;
|
||||
return lv_line_color(lv, meow_theme_get(), dist);
|
||||
}
|
||||
|
||||
int meow_lyric_view_hit_line(meow_lyric_view *lv, int x, int y)
|
||||
{
|
||||
if (!lv || !lv->w) return -1;
|
||||
int rect[4]; meow_widget_rect(lv->w, rect);
|
||||
int rx = rect[0], ry = rect[1], rw = rect[2], rh = rect[3];
|
||||
if (x < rx || x >= rx + rw || y < ry || y >= ry + rh) return -1;
|
||||
int n = meow_lyric_count();
|
||||
if (n <= 0) return -1;
|
||||
int maxw = rw - 2 * lv->l.pad; if (maxw < 1) maxw = 1;
|
||||
int cnt = lv_compute_layout(lv, lv->pos_ms, maxw);
|
||||
if (cnt <= 0) return -1;
|
||||
for (int k = 0; k < cnt; k++) {
|
||||
int yb = lv->vis[k].y, yh = lv->vis[k].bh * lv->l.sub_lh;
|
||||
if (y >= yb && y < yb + yh) return lv->vis[k].ent;
|
||||
}
|
||||
return (y < lv->vis[0].y) ? lv->vis[0].ent : lv->vis[cnt - 1].ent;
|
||||
}
|
||||
|
||||
int meow_lyric_view_visible(meow_lyric_view *lv, int64_t pos_ms,
|
||||
meow_lyric_view_vis *out, int cap)
|
||||
{
|
||||
if (!lv || !lv->w) return 0;
|
||||
int rect[4]; meow_widget_rect(lv->w, rect);
|
||||
int maxw = rect[2] - 2 * lv->l.pad; if (maxw < 1) maxw = 1;
|
||||
int cnt = lv_compute_layout(lv, pos_ms, maxw);
|
||||
if (out && cnt > 0) {
|
||||
int n = (cnt < cap) ? cnt : cap;
|
||||
for (int i = 0; i < n; i++) out[i] = lv->vis[i];
|
||||
}
|
||||
return cnt;
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* meow_lyric_view.h — MeowOS SDK 通用歌词页渲染控件(可复用,供 launcher Now Playing 与 ncm 共用)。
|
||||
*
|
||||
* 定位:把 ncm_app/app/ui/player.c 已成熟(agent-380 修好 s_lyr_bh)的歌词页布局上收成 SDK 控件。
|
||||
* 能力对齐 ncm 歌词页:活动行纯白(居中偏上锚定)、上下行按行距逐级灰阶、翻译行紧跟原文行不叠行
|
||||
* (块高含翻译行)、超宽折行(中西文兼容)、上方保底 ≥above_min 行已过、总视觉行数 ≤max_rows、
|
||||
* 点行 seek 命中与绘制一致(同一布局函数)。
|
||||
*
|
||||
* 数据来源:**绑定批1 meow_lyric**(解析/查询 API:meow_lyric_count/line_at/line_text/line_tl/line_time,
|
||||
* 全局单例,线程安全)+ 播放位置(meow_lyric_view_set_pos)。宽度测量用回调注入
|
||||
* (`meow_lyric_width_fn`,包 meow_ui_text_width 的包装;宿主测试传自己的测量函数)。
|
||||
*
|
||||
* 控件形态:内部持一个 `meow_widget`(W_CUSTOM 自定义画布),经 meow_lyric_view_widget() 取出供
|
||||
* meow_widget_draw/hit/注册;render_cb/hit_cb 都由本控件自绘/自判。App 也可只用纯查询接口
|
||||
* (meow_lyric_view_visible/hit_line)在自绘流程里复用布局逻辑。
|
||||
*
|
||||
* 主题:用 SDK 全局 meow_theme 为默认(明暗背景 scrim、活动行 onSurface=dark 纯白、灰阶递减),
|
||||
* 为批5「封面取色动态主题」预留 meow_lyric_view_set_colors 覆盖入口(字段 0=仍用主题默认)。
|
||||
*
|
||||
* 依赖:纯 SDK(meow_ui.h + meow_lyric.h),不引任何 ncm 私有符号;可链 meow_ui.a 使用,
|
||||
* 亦可宿主 gcc 单测(同 test_now_playing_widgets 链法)。本头为**独立公共头**(仿 meow_ui_view.h
|
||||
* 模式,不随 meow_ui.h 伞头发布),App 显式 `#include "meow_lyric_view.h"`。
|
||||
*
|
||||
* 命名:API 一律 `meow_lyric_view_*`(对齐 SDK 规范);控件内布局/绘制/命中共享同一
|
||||
* meow_lyric_view_visible 计算,保证「点行 seek 命中与绘制一致」。
|
||||
*/
|
||||
#ifndef MEOW_LYRIC_VIEW_H
|
||||
#define MEOW_LYRIC_VIEW_H
|
||||
|
||||
#include "meow_ui.h" /* meow_widget / meow_theme / meow_ui_text 等 */
|
||||
#include "meow_lyric.h" /* meow_lyric_width_fn + 行查询 API(批1) */
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct meow_lyric_view meow_lyric_view;
|
||||
|
||||
/* 可见层数硬上限(对齐 ncm LYR_MAX_ROWS;数组按此定容,运行时 layout.max_rows 不得越此)。 */
|
||||
#define MEOW_LYRIC_VIEW_MAX_ROWS 9
|
||||
|
||||
/* ---- 可见条目(布局/命中/绘制共用,宿主可测)----
|
||||
* ent=歌词行下标;rw=原文折行段数;bh=块高(子行数)=rw + 当前行翻译占 1(其余条目 bh=rw);
|
||||
* sz=该条目字号;y=块顶 y(绝对屏幕坐标)。 */
|
||||
typedef struct meow_lyric_view_vis {
|
||||
int ent;
|
||||
int rw;
|
||||
int bh;
|
||||
int sz;
|
||||
int y;
|
||||
} meow_lyric_view_vis;
|
||||
|
||||
/* ---- 主题色覆盖(批5 封面取色动态主题预留)----
|
||||
* 全部字段:0(scrim_alpha<0)= 用 meow_theme 内建默认;显式值 = 覆盖。
|
||||
* 默认映射:scrim=th->scrim(黑叠加在封面之上,alpha 176);cur=th->onSurface(dark=0xFFFFFF 纯白);
|
||||
* gray[0]=cur,gray[1..5]=对 cur 逐级缩灰(对纯白即 ncm 的 {BBBBBB,929292,808080,6A6A6A,555555})。
|
||||
* scrim_alpha==0 = 不画 scrim(App 已自绘封面/叠加层时用)。 */
|
||||
typedef struct meow_lyric_view_colors {
|
||||
uint32_t scrim; /* 背景 scrim 色(半透明叠加在封面之上) */
|
||||
uint32_t cur; /* 活动行色(默认 th->onSurface;dark 下=纯白) */
|
||||
uint32_t gray[6]; /* 距活动行 0..5 灰阶;gray[0]=cur。字段 0=自动递减 */
|
||||
int scrim_alpha; /* scrim 透明度 0-255;<0=默认(176);0=不画 scrim */
|
||||
} meow_lyric_view_colors;
|
||||
|
||||
/* ---- 布局参数(对齐 ncm;NULL=内建默认)---- */
|
||||
typedef struct meow_lyric_view_layout {
|
||||
int pad; /* 左边距(默认 24) */
|
||||
int sub_lh; /* 子行高(默认 44) */
|
||||
int cur_sz; /* 活动行字号(默认 28) */
|
||||
int oth_sz; /* 上下行字号(默认 24) */
|
||||
int tl_sz; /* 当前行翻译字号(默认 20) */
|
||||
int max_rows; /* 可见视觉行数上限(默认 9;≤MEOW_LYRIC_VIEW_MAX_ROWS) */
|
||||
int above_min; /* 活动行上方保底已过行数(默认 2) */
|
||||
int above_off; /* 活动行块中心相对视图中线偏上 px(默认 50,需求3 偏上锚定) */
|
||||
} meow_lyric_view_layout;
|
||||
|
||||
/* 点行 seek 回调:meow_lyric_view_set_on_seek 设置;命中某行后回调 line_time(ms)。 */
|
||||
typedef void (*meow_lyric_seek_fn)(int64_t ms, void *ctx);
|
||||
|
||||
/* ---- 生命周期 ---- */
|
||||
meow_lyric_view *meow_lyric_view_create(int x, int y, int w, int h,
|
||||
meow_lyric_width_fn width, void *width_ctx);
|
||||
void meow_lyric_view_free(meow_lyric_view *lv);
|
||||
/* 底层 W_CUSTOM meow_widget:App 用它做 draw/hit/注册(launcher 控件模型)。 */
|
||||
meow_widget *meow_lyric_view_widget(const meow_lyric_view *lv);
|
||||
|
||||
/* ---- 数据绑定 ---- */
|
||||
void meow_lyric_view_set_pos(meow_lyric_view *lv, int64_t pos_ms); /* 播放器位置(ms) */
|
||||
|
||||
/* ---- 主题/布局/空态 ---- */
|
||||
void meow_lyric_view_set_colors(meow_lyric_view *lv, const meow_lyric_view_colors *c); /* NULL=回主题默认 */
|
||||
void meow_lyric_view_set_layout(meow_lyric_view *lv, const meow_lyric_view_layout *ls); /* NULL=回内建默认 */
|
||||
void meow_lyric_view_set_empty(meow_lyric_view *lv, const char *txt); /* 无歌词占位文案;NULL=默认 */
|
||||
/* 点行 seek(widget hit 命中时回调;App 也可只用 meow_lyric_view_hit_line 自行 seek)。 */
|
||||
void meow_lyric_view_set_on_seek(meow_lyric_view *lv, meow_lyric_seek_fn cb, void *ctx);
|
||||
|
||||
/* ---- 查询 ——
|
||||
* cur_color / line_color:取该视图生效(主题默认或覆盖)的活动行色 / 距活动行 dist(0..5) 行色。
|
||||
* visible:按 pos_ms 计算可见条目布局(与绘制/命中同一算法)写入 out[≤cap],返回条目数(≤0=空/无活动行)。
|
||||
* hit_line:点 (x,y) 命中某歌词行下标(与绘制一致的布局;块外就近取整);未命中返回 -1。 */
|
||||
uint32_t meow_lyric_view_cur_color(const meow_lyric_view *lv);
|
||||
uint32_t meow_lyric_view_line_color(const meow_lyric_view *lv, int dist);
|
||||
int meow_lyric_view_hit_line(meow_lyric_view *lv, int x, int y);
|
||||
int meow_lyric_view_visible(meow_lyric_view *lv, int64_t pos_ms,
|
||||
meow_lyric_view_vis *out, int cap);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MEOW_LYRIC_VIEW_H */
|
||||
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* meow_net_ipc.c — MeowOS SDK 网络自愿通道 → netmgrd IPC 桥(期4;追加式协议 0x00EE/0x00EF)。
|
||||
*
|
||||
* 与 meow_notify_ipc.c 同型:只做「一次性 connect + send + read」,不维护连接/订阅循环。
|
||||
* - meow_net_policy_check(host):MSG_NET_SDK op=NET_SDK_CHECK(app_id=MEOW_LOG_APP + host);
|
||||
* 读到 allow=1 → 返回 0(allow),allow=0 → 返回 1(block);任何失败(无 socket/超时/非法应答)
|
||||
* → 返回 0(乐观放行,绝不因监控不可用误封业务——NETMGR_DESIGN §6.3 自愿性质)。
|
||||
* - meow_net_report(rx,tx):MSG_NET_SDK op=NET_SDK_REPORT;send 后读一次应答头即关(不读文本),
|
||||
* best-effort,失败静默(返回值 void)。
|
||||
* 映射层(SDK 入参 ↔ IPC m_net_sdk_t 字段)集中在本文件;app_id 取 env MEOW_LOG_APP(与 meow_ui_log.c
|
||||
* 口径一致),socket 路径取 env MEOW_NET_SOCK(宿主/qemu 覆盖)缺省 /usr/data/meow_net.sock。
|
||||
*
|
||||
* 线程安全:app_id pthread_once 缓存;本文件纯传输层不调 meow_log(与 meow_notify_ipc.c 同约定)。
|
||||
*/
|
||||
#include "meow_net_ipc.h"
|
||||
#include "meow_ipc.h" /* 共享协议:帧头/m_net_sdk_t/m_net_sdk_result_t/MSG_NET_SDK* —— 防漂移 */
|
||||
|
||||
#include <stdio.h> /* snprintf */
|
||||
#include <stdlib.h> /* getenv */
|
||||
#include <string.h>
|
||||
#include <errno.h> /* EINTR */
|
||||
#include <unistd.h>
|
||||
#include <pthread.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <sys/time.h>
|
||||
|
||||
#define MEOW_NET_IPC_TIMEOUT_MS 500 /* 应答读超时:netmgrd 单线程 poll,>500ms 视为降级 */
|
||||
|
||||
static pthread_once_t g_once = PTHREAD_ONCE_INIT;
|
||||
static char g_app_id[MEOW_APP_ID_MAX] = "app";
|
||||
|
||||
static void ipc_init(void)
|
||||
{
|
||||
const char *s = getenv("MEOW_LOG_APP");
|
||||
if (s && s[0]) snprintf(g_app_id, sizeof g_app_id, "%s", s);
|
||||
}
|
||||
|
||||
const char *meow_net_ipc_app_id(void)
|
||||
{
|
||||
pthread_once(&g_once, ipc_init);
|
||||
return g_app_id;
|
||||
}
|
||||
|
||||
/* socket 路径:优先 env MEOW_NET_SOCK,缺省 /usr/data/meow_net.sock。 */
|
||||
static const char *ipc_sock_path(void)
|
||||
{
|
||||
const char *s = getenv("MEOW_NET_SOCK");
|
||||
return (s && s[0]) ? s : "/usr/data/meow_net.sock";
|
||||
}
|
||||
|
||||
/* 连接 socket;失败 -1(调用方降级)。 */
|
||||
static int conn_path(const char *path)
|
||||
{
|
||||
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (fd < 0) return -1;
|
||||
struct sockaddr_un a;
|
||||
memset(&a, 0, sizeof a);
|
||||
a.sun_family = AF_UNIX;
|
||||
strncpy(a.sun_path, path, sizeof a.sun_path - 1);
|
||||
if (connect(fd, (struct sockaddr *)&a, sizeof a) < 0) { close(fd); return -1; }
|
||||
return fd;
|
||||
}
|
||||
static int write_all(int fd, const uint8_t *p, size_t n)
|
||||
{
|
||||
size_t off = 0;
|
||||
while (off < n) {
|
||||
ssize_t w = write(fd, p + off, n - off);
|
||||
if (w < 0) { if (errno == EINTR) continue; return 0; }
|
||||
if (w == 0) return 0;
|
||||
off += (size_t)w;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
static int read_all(int fd, uint8_t *p, size_t n)
|
||||
{
|
||||
size_t off = 0;
|
||||
while (off < n) {
|
||||
ssize_t r = read(fd, p + off, n - off);
|
||||
if (r < 0) { if (errno == EINTR) continue; return 0; }
|
||||
if (r == 0) return 0;
|
||||
off += (size_t)r;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
static int send_frame(int fd, uint16_t type, uint32_t req_id, const void *payload, uint32_t len)
|
||||
{
|
||||
uint8_t hdr[MEOW_IPC_HDR_LEN];
|
||||
uint32_t magic = MEOW_IPC_MAGIC, rl = len;
|
||||
uint16_t ver = MEOW_IPC_VERSION, t = type;
|
||||
memcpy(hdr + 0, &magic, 4); memcpy(hdr + 4, &ver, 2); memcpy(hdr + 6, &t, 2);
|
||||
memcpy(hdr + 8, &req_id, 4); memcpy(hdr + 12, &rl, 4);
|
||||
if (!write_all(fd, hdr, sizeof hdr)) return 0;
|
||||
if (len && payload && !write_all(fd, payload, len)) return 0;
|
||||
return 1;
|
||||
}
|
||||
static void set_timeout(int fd)
|
||||
{
|
||||
struct timeval tv;
|
||||
tv.tv_sec = MEOW_NET_IPC_TIMEOUT_MS / 1000;
|
||||
tv.tv_usec = (MEOW_NET_IPC_TIMEOUT_MS % 1000) * 1000;
|
||||
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
|
||||
}
|
||||
|
||||
int meow_net_policy_check(const char *host)
|
||||
{
|
||||
if (!host || !host[0]) return 0;
|
||||
m_net_sdk_t q;
|
||||
memset(&q, 0, sizeof q);
|
||||
q.op = NET_SDK_CHECK;
|
||||
snprintf(q.app_id, sizeof q.app_id, "%s", meow_net_ipc_app_id());
|
||||
snprintf(q.host, sizeof q.host, "%s", host);
|
||||
|
||||
int fd = conn_path(ipc_sock_path());
|
||||
if (fd < 0) return 0; /* java socket:乐观放行 */
|
||||
set_timeout(fd);
|
||||
int allow = 0; /* 默认放行 */
|
||||
if (send_frame(fd, MSG_NET_SDK, 0x4E455443u /* "NETC" */, &q, (uint32_t)sizeof q)) {
|
||||
uint8_t rhdr[MEOW_IPC_HDR_LEN];
|
||||
if (read_all(fd, rhdr, sizeof rhdr)) {
|
||||
uint16_t rtype; uint32_t rlen;
|
||||
memcpy(&rtype, rhdr + 6, 2); memcpy(&rlen, rhdr + 12, 4);
|
||||
if (rtype == MSG_NET_SDK_RESULT && rlen >= 10) {
|
||||
uint8_t pay[12];
|
||||
if (read_all(fd, pay, 12)) { /* code(4)+allow(4)+text_len(2)+pad(2) */
|
||||
m_net_sdk_result_t res;
|
||||
memset(&res, 0, sizeof res);
|
||||
memcpy(&res.code, pay, 4);
|
||||
memcpy(&res.allow, pay + 4, 4);
|
||||
memcpy(&res.text_len, pay + 8, 2);
|
||||
if (res.text_len < sizeof res.text)
|
||||
read_all(fd, (uint8_t *)res.text, res.text_len);
|
||||
if (res.code == MEOW_OK) allow = (res.allow != 0) ? 1 : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
close(fd);
|
||||
return allow ? 0 : 1; /* allow=1 → 返回 0(放行);allow=0 → 返回 1(block) */
|
||||
}
|
||||
|
||||
void meow_net_report(uint32_t rx, uint32_t tx)
|
||||
{
|
||||
m_net_sdk_t q;
|
||||
memset(&q, 0, sizeof q);
|
||||
q.op = NET_SDK_REPORT;
|
||||
snprintf(q.app_id, sizeof q.app_id, "%s", meow_net_ipc_app_id());
|
||||
q.rx = rx; q.tx = tx;
|
||||
int fd = conn_path(ipc_sock_path());
|
||||
if (fd < 0) return; /* 无 socket:静默 */
|
||||
set_timeout(fd);
|
||||
if (send_frame(fd, MSG_NET_SDK, 0x4E455452u /* "NETR" */, &q, (uint32_t)sizeof q)) {
|
||||
uint8_t rhdr[MEOW_IPC_HDR_LEN];
|
||||
read_all(fd, rhdr, sizeof rhdr); /* best-effort:读一次应答头即关,不解析 */
|
||||
}
|
||||
close(fd);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* meow_net_ipc.h — MeowOS SDK 网络自愿通道 → netmgrd IPC 桥(内部头,不随 meow_ui.h 发布)。
|
||||
*
|
||||
* 定位(NETMGR_DESIGN §6.3/§4):让框架 App 的 meow_net_policy_check()/meow_net_report() 真正经
|
||||
* AF_UNIX IPC 到 netmgrd(/usr/data/meow_net.sock)。协议帧与负载结构复用共享 meow_ipc.h:
|
||||
* - MSG_NET_SDK(0x00EE) ↑:op=NET_SDK_CHECK(app_id+host)/ NET_SDK_REPORT(app_id+rx+tx);
|
||||
* - MSG_NET_SDK_RESULT(0x00EF) ↓:code + (CHECK)allow + 状态文本。
|
||||
* 约定:只做「一次性 connect + send + read」,不维护订阅循环(同 meow_notify_ipc.c 型);
|
||||
* 全部失败路径优雅降级(无 socket/写失败/超时/非法应答 → check 返回 0 放行、report 静默)。
|
||||
*/
|
||||
#ifndef MEOW_NET_IPC_H
|
||||
#define MEOW_NET_IPC_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include "meow_ui.h" /* meow_net_policy_check / meow_net_report */
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* 当前 app 的 app_id(env MEOW_LOG_APP,缺省 "app";pthread_once 缓存,线程安全)。 */
|
||||
const char *meow_net_ipc_app_id(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* MEOW_NET_IPC_H */
|
||||
@@ -0,0 +1,499 @@
|
||||
/*
|
||||
* meow_png_stream.c — M23 真缩解后端(二):PNG 逐行 inflate + BMP 按行跳采样。
|
||||
*
|
||||
* 为什么不用 stb 的 PNG 路径:stb 先整幅 inflate + 反滤波成 raw,再展开成 RGBA 整幅,
|
||||
* 最后才降采样——1600×2400 PNG 实测 +26.2MB。这里改成「按行流式」:
|
||||
* - IDAT 边到边 inflate,一次只持有一行(filter 字节 + rowbytes)与上一行;
|
||||
* - 反滤波后**只把需要的列**写进目标行(最近邻,与 meow_img_downscale 同口径);
|
||||
* - 不需要的行整行丢弃(但仍要反滤波,filter 依赖上一行);
|
||||
* - 峰值 ≈ 两行缓冲 + zlib 窗口(32KB) + 目标 RGBA,与源图分辨率无关。
|
||||
* 支持:gray / gray+A / RGB / RGBA / 调色板,bit depth 1/2/4/8/16(16bit 取高字节降 8bit),
|
||||
* tRNS 透明。不支持 → 返回 0 回退 stb:Adam7 交错、非 0 压缩/滤波方法、异常色型组合。
|
||||
*
|
||||
* BMP:文件流按目标行直接 seek 到源行(不读全图),行内跳列采样。支持 8bpp 调色板 /
|
||||
* 16bpp RGB555 / 24bpp / 32bpp(BI_RGB);RLE、BITFIELDS、1/2/4bpp → 回退 stb。
|
||||
*
|
||||
* 许可证:MeowOS 自写(MIT,随 SDK 发布)。PNG/zlib 语义参考 RFC 2083 / zlib 手册;
|
||||
* BMP 布局参考 stb_image 的解析口径(同为本仓已有依赖,行为对齐)。
|
||||
*/
|
||||
#include "meow_jpeg.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* ---------------- zlib ABI ---------------- */
|
||||
#if defined(__has_include)
|
||||
# if __has_include(<zlib.h>)
|
||||
# include <zlib.h>
|
||||
# define MZP_HAVE_H 1
|
||||
# endif
|
||||
#endif
|
||||
#ifndef MZP_HAVE_H
|
||||
/* 设备 musl sysroot 没有 zlib.h(meow_font.c 同此口径):按 libz.a 的 ABI 声明最小 API。
|
||||
* 指针/unsigned long 交给编译器定尺寸,宿主 64 位与设备 32 位都成立。 */
|
||||
typedef unsigned char mzp_Bytef;
|
||||
typedef unsigned int mzp_uInt;
|
||||
typedef unsigned long mzp_uLong;
|
||||
typedef void *mzp_voidpf;
|
||||
typedef struct mzp_stream_s {
|
||||
mzp_Bytef *next_in; mzp_uInt avail_in; mzp_uLong total_in;
|
||||
mzp_Bytef *next_out; mzp_uInt avail_out; mzp_uLong total_out;
|
||||
char *msg;
|
||||
void *state;
|
||||
void *zalloc;
|
||||
void *zfree;
|
||||
mzp_voidpf opaque;
|
||||
int data_type;
|
||||
mzp_uLong adler;
|
||||
mzp_uLong reserved;
|
||||
} mzp_stream;
|
||||
extern int inflateInit_(mzp_stream *, const char *, int);
|
||||
extern int inflate(mzp_stream *, int);
|
||||
extern int inflateEnd(mzp_stream *);
|
||||
extern const char *zlibVersion(void);
|
||||
# define z_stream mzp_stream
|
||||
# define Z_OK 0
|
||||
# define Z_STREAM_END 1
|
||||
# define Z_NO_FLUSH 0
|
||||
#endif
|
||||
|
||||
/* ---------------- 字节源 ---------------- */
|
||||
typedef struct {
|
||||
FILE *f;
|
||||
const unsigned char *mem;
|
||||
size_t memlen, mempos;
|
||||
} png_src;
|
||||
|
||||
static int ps_read(png_src *s, void *dst, size_t n)
|
||||
{
|
||||
if (s->mem) {
|
||||
if (s->mempos + n > s->memlen) return 0;
|
||||
memcpy(dst, s->mem + s->mempos, n);
|
||||
s->mempos += n;
|
||||
return 1;
|
||||
}
|
||||
return fread(dst, 1, n, s->f) == n;
|
||||
}
|
||||
|
||||
static int ps_skip(png_src *s, size_t n)
|
||||
{
|
||||
if (s->mem) {
|
||||
if (s->mempos + n > s->memlen) return 0;
|
||||
s->mempos += n;
|
||||
return 1;
|
||||
}
|
||||
return fseek(s->f, (long)n, SEEK_CUR) == 0;
|
||||
}
|
||||
|
||||
/* ================================================================== */
|
||||
/* PNG */
|
||||
/* ================================================================== */
|
||||
typedef struct {
|
||||
png_src src;
|
||||
z_stream z;
|
||||
int z_ready, z_end;
|
||||
unsigned w, h;
|
||||
int depth, color;
|
||||
int bits; /* 每像素位数 */
|
||||
size_t rowbytes;
|
||||
unsigned char *line; /* 1 filter 字节 + rowbytes */
|
||||
unsigned char *prev; /* 上一行的像素字节 */
|
||||
unsigned char *cur; /* 本行的像素字节 */
|
||||
size_t need, got;
|
||||
unsigned char pal[256][3];
|
||||
unsigned char trns_a[256]; /* 调色板透明度 */
|
||||
int pal_n;
|
||||
int have_trns;
|
||||
unsigned trns[3]; /* gray / RGB 的 tRNS 值(已降到 8bit) */
|
||||
unsigned char *out;
|
||||
int tw, th;
|
||||
int next_ty;
|
||||
unsigned y;
|
||||
} png_ctx;
|
||||
|
||||
/* 从行里取 px 列的 8bit 通道;返回通道数(写入 o[0..3] 的 RGB(A) 语义由调用方定)。 */
|
||||
static void png_get_px(const png_ctx *c, const unsigned char *row, int x, unsigned char *o)
|
||||
{
|
||||
int d = c->depth;
|
||||
if (d < 8) {
|
||||
int per = 8 / d; /* 每字节像素数 */
|
||||
int byte = x / per;
|
||||
int shift = 8 - d * (x % per + 1);
|
||||
int v = (row[byte] >> shift) & ((1 << d) - 1);
|
||||
if (c->color == 3) { /* 调色板:子字节值即索引 */
|
||||
o[0] = c->pal[v][0]; o[1] = c->pal[v][1]; o[2] = c->pal[v][2];
|
||||
o[3] = c->trns_a[v];
|
||||
} else {
|
||||
unsigned char g = (unsigned char)(v * 255 / ((1 << d) - 1));
|
||||
o[0] = o[1] = o[2] = g;
|
||||
/* tRNS 存的是原始采样值(0..2^d-1),不是展开后的 0..255 */
|
||||
o[3] = (c->color == 0 && c->have_trns && (unsigned)v == c->trns[0]) ? 0 : 255;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (d == 8) {
|
||||
if (c->color == 0) { /* gray */
|
||||
o[0] = o[1] = o[2] = row[x];
|
||||
o[3] = (c->have_trns && row[x] == c->trns[0]) ? 0 : 255;
|
||||
} else if (c->color == 2) { /* RGB */
|
||||
o[0] = row[x * 3]; o[1] = row[x * 3 + 1]; o[2] = row[x * 3 + 2];
|
||||
o[3] = (c->have_trns && row[x * 3] == c->trns[0] &&
|
||||
row[x * 3 + 1] == c->trns[1] && row[x * 3 + 2] == c->trns[2]) ? 0 : 255;
|
||||
} else if (c->color == 3) { /* palette */
|
||||
int i = row[x];
|
||||
o[0] = c->pal[i][0]; o[1] = c->pal[i][1]; o[2] = c->pal[i][2];
|
||||
o[3] = c->trns_a[i];
|
||||
} else if (c->color == 4) { /* gray + A */
|
||||
o[0] = o[1] = o[2] = row[x * 2];
|
||||
o[3] = row[x * 2 + 1];
|
||||
} else { /* RGBA */
|
||||
o[0] = row[x * 4]; o[1] = row[x * 4 + 1];
|
||||
o[2] = row[x * 4 + 2]; o[3] = row[x * 4 + 3];
|
||||
}
|
||||
return;
|
||||
}
|
||||
/* 16bit:取高字节 */
|
||||
if (c->color == 0) {
|
||||
o[0] = o[1] = o[2] = row[x * 2];
|
||||
o[3] = (c->have_trns && row[x * 2] == c->trns[0]) ? 0 : 255;
|
||||
} else if (c->color == 2) {
|
||||
o[0] = row[x * 6]; o[1] = row[x * 6 + 2]; o[2] = row[x * 6 + 4];
|
||||
o[3] = (c->have_trns && row[x * 6] == c->trns[0] &&
|
||||
row[x * 6 + 2] == c->trns[1] && row[x * 6 + 4] == c->trns[2]) ? 0 : 255;
|
||||
} else if (c->color == 4) {
|
||||
o[0] = o[1] = o[2] = row[x * 4];
|
||||
o[3] = row[x * 4 + 2];
|
||||
} else {
|
||||
o[0] = row[x * 8]; o[1] = row[x * 8 + 2];
|
||||
o[2] = row[x * 8 + 4]; o[3] = row[x * 8 + 6];
|
||||
}
|
||||
}
|
||||
|
||||
/* 一行反滤波完成后:按需写目标行。 */
|
||||
static int png_emit_row(png_ctx *c)
|
||||
{
|
||||
unsigned char ft = c->line[0];
|
||||
const unsigned char *src = c->line + 1;
|
||||
unsigned char *dst = c->cur;
|
||||
size_t x, bpp = (size_t)((c->bits + 7) / 8);
|
||||
if (bpp < 1) bpp = 1;
|
||||
if (ft > 4) return 0;
|
||||
for (x = 0; x < c->rowbytes; x++) {
|
||||
unsigned char left = (x >= bpp) ? dst[x - bpp] : 0;
|
||||
unsigned char up = c->prev ? c->prev[x] : 0;
|
||||
unsigned char ul = (c->prev && x >= bpp) ? c->prev[x - bpp] : 0;
|
||||
unsigned char v = src[x];
|
||||
switch (ft) {
|
||||
case 0: dst[x] = v; break;
|
||||
case 1: dst[x] = (unsigned char)(v + left); break;
|
||||
case 2: dst[x] = (unsigned char)(v + up); break;
|
||||
case 3: dst[x] = (unsigned char)(v + (((unsigned)left + (unsigned)up) >> 1)); break;
|
||||
default: {
|
||||
int p = (int)left + (int)up - (int)ul;
|
||||
int pa = p - (int)left; if (pa < 0) pa = -pa;
|
||||
int pb = p - (int)up; if (pb < 0) pb = -pb;
|
||||
int pc = p - (int)ul; if (pc < 0) pc = -pc;
|
||||
unsigned char pred = (pa <= pb && pa <= pc) ? left : (pb <= pc ? up : ul);
|
||||
dst[x] = (unsigned char)(v + pred);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (c->next_ty < c->th) {
|
||||
long sy = (long)c->next_ty * (long)c->h / c->th;
|
||||
if (sy == (long)c->y) {
|
||||
unsigned char *o = c->out + (size_t)c->next_ty * c->tw * 4;
|
||||
int tx;
|
||||
for (tx = 0; tx < c->tw; tx++) {
|
||||
int sx = (int)((long)tx * (long)c->w / c->tw);
|
||||
unsigned char px[4];
|
||||
png_get_px(c, dst, sx, px);
|
||||
o[tx * 4] = px[0]; o[tx * 4 + 1] = px[1];
|
||||
o[tx * 4 + 2] = px[2]; o[tx * 4 + 3] = px[3];
|
||||
}
|
||||
c->next_ty++;
|
||||
}
|
||||
}
|
||||
/* 交换 prev/cur(prev 直接成为反滤波基准) */
|
||||
{
|
||||
unsigned char *t = c->prev;
|
||||
c->prev = c->cur;
|
||||
c->cur = t;
|
||||
}
|
||||
c->y++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
#define PNG_NEED_MORE 2
|
||||
|
||||
static int png_pump(png_ctx *c)
|
||||
{
|
||||
while (c->got < c->need) {
|
||||
int ret;
|
||||
c->z.next_out = c->line + c->got;
|
||||
c->z.avail_out = (unsigned)(c->need - c->got);
|
||||
ret = inflate(&c->z, Z_NO_FLUSH);
|
||||
c->got = c->need - c->z.avail_out;
|
||||
if (ret == Z_STREAM_END) c->z_end = 1;
|
||||
else if (ret != Z_OK) return 0;
|
||||
if (c->got < c->need && (c->z.avail_in == 0 || c->z_end))
|
||||
return c->z_end ? 0 : PNG_NEED_MORE;
|
||||
}
|
||||
if (!png_emit_row(c)) return 0;
|
||||
c->got = 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int png_bits_of(int color, int depth)
|
||||
{
|
||||
switch (color) {
|
||||
case 0: return depth; /* gray */
|
||||
case 2: return depth * 3; /* rgb */
|
||||
case 3: return depth; /* palette */
|
||||
case 4: return depth * 2; /* gray + a */
|
||||
case 6: return depth * 4; /* rgba */
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static int png_depth_ok(int color, int depth)
|
||||
{
|
||||
if (color == 0) return depth == 1 || depth == 2 || depth == 4 || depth == 8 || depth == 16;
|
||||
if (color == 3) return depth == 1 || depth == 2 || depth == 4 || depth == 8;
|
||||
if (color == 2 || color == 4 || color == 6) return depth == 8 || depth == 16;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int meow_png_decode_stream(FILE *f, const unsigned char *mem, size_t len,
|
||||
int maxdim, int *tw, int *th, unsigned char **rgba)
|
||||
{
|
||||
png_ctx c;
|
||||
unsigned char sig[8], hdr[8];
|
||||
unsigned char ibuf[4096];
|
||||
int have_ihdr = 0, i, ok = 0;
|
||||
|
||||
if (maxdim <= 0) return 0;
|
||||
memset(&c, 0, sizeof c);
|
||||
c.src.f = f;
|
||||
c.src.mem = mem;
|
||||
c.src.memlen = len;
|
||||
if (!ps_read(&c.src, sig, 8)) return 0;
|
||||
if (memcmp(sig, "\x89PNG\r\n\x1a\n", 8) != 0) return 0;
|
||||
|
||||
while (ps_read(&c.src, hdr, 8)) {
|
||||
unsigned clen = ((unsigned)hdr[0] << 24) | ((unsigned)hdr[1] << 16) |
|
||||
((unsigned)hdr[2] << 8) | hdr[3];
|
||||
const unsigned char *type = hdr + 4;
|
||||
if (clen > 0x7FFFFFFFu) return 0;
|
||||
|
||||
if (memcmp(type, "IHDR", 4) == 0) {
|
||||
unsigned char d[13];
|
||||
if (clen < 13 || have_ihdr) return 0;
|
||||
if (!ps_read(&c.src, d, 13)) return 0;
|
||||
c.w = ((unsigned)d[0] << 24) | ((unsigned)d[1] << 16) | ((unsigned)d[2] << 8) | d[3];
|
||||
c.h = ((unsigned)d[4] << 24) | ((unsigned)d[5] << 16) | ((unsigned)d[6] << 8) | d[7];
|
||||
c.depth = d[8];
|
||||
c.color = d[9];
|
||||
if (d[10] != 0 || d[11] != 0 || d[12] != 0) return 0; /* 只支持非交错 + 标准压缩/滤波 */
|
||||
if (c.w == 0 || c.h == 0 || c.w > 65535u || c.h > 65535u) return 0;
|
||||
if (!png_depth_ok(c.color, c.depth)) return 0;
|
||||
c.bits = png_bits_of(c.color, c.depth);
|
||||
c.rowbytes = ((size_t)c.w * c.bits + 7) / 8;
|
||||
if (c.rowbytes == 0) return 0;
|
||||
/* 小图不动:与 meow_img_load_file_dim 的「旧入口」判定一致 */
|
||||
if ((long)(c.w > c.h ? c.w : c.h) <= maxdim) return 0;
|
||||
if (clen > 13 && !ps_skip(&c.src, clen - 13)) return 0;
|
||||
have_ihdr = 1;
|
||||
c.need = c.rowbytes + 1;
|
||||
c.line = (unsigned char *)malloc(c.need);
|
||||
c.prev = (unsigned char *)malloc(c.rowbytes);
|
||||
c.cur = (unsigned char *)malloc(c.rowbytes);
|
||||
if (!c.line || !c.prev || !c.cur) goto done;
|
||||
memset(c.prev, 0, c.rowbytes);
|
||||
memset(c.cur, 0, c.rowbytes);
|
||||
for (i = 0; i < 256; i++) c.trns_a[i] = 255;
|
||||
meow_img_target_dims((int)c.w, (int)c.h, maxdim, &c.tw, &c.th);
|
||||
c.out = (unsigned char *)malloc((size_t)c.tw * c.th * 4);
|
||||
if (!c.out) goto done;
|
||||
memset(c.out, 0, (size_t)c.tw * c.th * 4);
|
||||
memset(&c.z, 0, sizeof c.z);
|
||||
if (inflateInit_(&c.z, zlibVersion(), (int)sizeof c.z) != Z_OK) goto done;
|
||||
c.z_ready = 1;
|
||||
} else if (memcmp(type, "PLTE", 4) == 0) {
|
||||
unsigned char d[256 * 3];
|
||||
if (clen > sizeof d || clen % 3) return 0;
|
||||
if (!ps_read(&c.src, d, clen)) return 0;
|
||||
c.pal_n = (int)(clen / 3);
|
||||
memcpy(c.pal, d, clen);
|
||||
} else if (memcmp(type, "tRNS", 4) == 0) {
|
||||
unsigned char d[256];
|
||||
if (clen > sizeof d) return 0;
|
||||
if (!ps_read(&c.src, d, clen)) return 0;
|
||||
c.have_trns = 1;
|
||||
if (c.color == 3) {
|
||||
for (i = 0; i < (int)clen && i < 256; i++) c.trns_a[i] = d[i];
|
||||
} else if (c.color == 0 && clen >= 2) {
|
||||
/* 8bit/低位深:采样值存在低字节;16bit:与像素一样取高字节比较 */
|
||||
c.trns[0] = (c.depth == 16) ? d[0] : d[1];
|
||||
} else if (c.color == 2 && clen >= 6) {
|
||||
if (c.depth == 16) {
|
||||
c.trns[0] = d[0]; c.trns[1] = d[2]; c.trns[2] = d[4];
|
||||
} else {
|
||||
c.trns[0] = d[1]; c.trns[1] = d[3]; c.trns[2] = d[5];
|
||||
}
|
||||
}
|
||||
} else if (memcmp(type, "IDAT", 4) == 0) {
|
||||
unsigned remain = clen;
|
||||
if (!have_ihdr || !c.out) return 0;
|
||||
while (remain) {
|
||||
unsigned n = remain > sizeof ibuf ? (unsigned)sizeof ibuf : remain;
|
||||
if (!ps_read(&c.src, ibuf, n)) goto done;
|
||||
remain -= n;
|
||||
c.z.next_in = ibuf;
|
||||
c.z.avail_in = n;
|
||||
while (c.z.avail_in && c.next_ty < c.th) {
|
||||
int r = png_pump(&c);
|
||||
if (r == 0) goto done;
|
||||
if (r == PNG_NEED_MORE) break;
|
||||
}
|
||||
}
|
||||
} else if (memcmp(type, "IEND", 4) == 0) {
|
||||
break;
|
||||
} else {
|
||||
if (clen && !ps_skip(&c.src, clen)) return 0;
|
||||
}
|
||||
/* 跳过 CRC */
|
||||
{
|
||||
unsigned char crc[4];
|
||||
if (!ps_read(&c.src, crc, 4)) break;
|
||||
}
|
||||
if (c.out && c.next_ty >= c.th) break; /* 目标行都拿到了,不必读完 */
|
||||
}
|
||||
|
||||
if (!have_ihdr || !c.out || c.next_ty < c.th) goto done;
|
||||
if (c.color == 3 && c.pal_n <= 0) goto done;
|
||||
*tw = c.tw;
|
||||
*th = c.th;
|
||||
*rgba = c.out;
|
||||
c.out = NULL;
|
||||
ok = 1;
|
||||
done:
|
||||
if (c.z_ready) inflateEnd(&c.z);
|
||||
free(c.line);
|
||||
free(c.prev);
|
||||
free(c.cur);
|
||||
free(c.out);
|
||||
return ok;
|
||||
}
|
||||
|
||||
/* ================================================================== */
|
||||
/* BMP */
|
||||
/* ================================================================== */
|
||||
static unsigned le16(const unsigned char *p) { return (unsigned)p[0] | ((unsigned)p[1] << 8); }
|
||||
static unsigned le32(const unsigned char *p)
|
||||
{
|
||||
return (unsigned)p[0] | ((unsigned)p[1] << 8) | ((unsigned)p[2] << 16) | ((unsigned)p[3] << 24);
|
||||
}
|
||||
|
||||
int meow_bmp_decode_stream(FILE *f, const unsigned char *mem, size_t len,
|
||||
int maxdim, int *tw, int *th, unsigned char **rgba)
|
||||
{
|
||||
png_src src;
|
||||
unsigned char fh[14], dh[4];
|
||||
unsigned char dib[1024];
|
||||
unsigned dibsize, dataoff, bpp, compression, colors;
|
||||
long w, h, rowsize;
|
||||
int topdown, i;
|
||||
unsigned char pal[256][3];
|
||||
unsigned char *row = NULL, *out = NULL;
|
||||
int tw_ = 0, th_ = 0, ok = 0;
|
||||
|
||||
if (maxdim <= 0) return 0;
|
||||
memset(&src, 0, sizeof src);
|
||||
src.f = f;
|
||||
src.mem = mem;
|
||||
src.memlen = len;
|
||||
if (!ps_read(&src, fh, 14)) return 0;
|
||||
if (fh[0] != 'B' || fh[1] != 'M') return 0;
|
||||
dataoff = le32(fh + 10);
|
||||
if (!ps_read(&src, dh, 4)) return 0;
|
||||
dibsize = le32(dh);
|
||||
if (dibsize < 40 || dibsize > sizeof dib) return 0; /* BITMAPCOREHEADER/OS2 → stb */
|
||||
memcpy(dib, dh, 4);
|
||||
if (!ps_read(&src, dib + 4, dibsize - 4)) return 0;
|
||||
w = (long)(int)le32(dib + 4);
|
||||
h = (long)(int)le32(dib + 8);
|
||||
bpp = le16(dib + 14);
|
||||
compression = le32(dib + 16);
|
||||
colors = (dibsize >= 36) ? le32(dib + 32) : 0;
|
||||
if (compression != 0) return 0; /* RLE/BITFIELDS → stb */
|
||||
if (w <= 0 || h == 0) return 0;
|
||||
if (bpp != 8 && bpp != 16 && bpp != 24 && bpp != 32) return 0;
|
||||
topdown = h < 0;
|
||||
if (h < 0) h = -h;
|
||||
if ((long)(w > h ? w : h) <= maxdim) return 0; /* 小图走旧入口 */
|
||||
|
||||
if (bpp <= 8) {
|
||||
unsigned n = colors ? colors : 256;
|
||||
unsigned char e[4];
|
||||
if (n > 256) n = 256;
|
||||
for (i = 0; (unsigned)i < n; i++) {
|
||||
if (!ps_read(&src, e, 4)) return 0;
|
||||
pal[i][0] = e[2]; pal[i][1] = e[1]; pal[i][2] = e[0];
|
||||
}
|
||||
for (; i < 256; i++) pal[i][0] = pal[i][1] = pal[i][2] = 0;
|
||||
}
|
||||
|
||||
rowsize = (w * bpp + 31) / 32 * 4;
|
||||
meow_img_target_dims((int)w, (int)h, maxdim, &tw_, &th_);
|
||||
row = (unsigned char *)malloc((size_t)rowsize);
|
||||
out = (unsigned char *)malloc((size_t)tw_ * th_ * 4);
|
||||
if (!row || !out) goto done;
|
||||
|
||||
for (int ty = 0; ty < th_; ty++) {
|
||||
int sy = (int)((long)ty * h / th_);
|
||||
long file_row = topdown ? sy : (h - 1 - sy);
|
||||
long off = (long)dataoff + file_row * rowsize;
|
||||
unsigned char *o = out + (size_t)ty * tw_ * 4;
|
||||
if (src.mem) {
|
||||
if (off < 0 || (size_t)off + (size_t)rowsize > src.memlen) goto done;
|
||||
memcpy(row, src.mem + off, (size_t)rowsize);
|
||||
} else {
|
||||
if (fseek(src.f, off, SEEK_SET) != 0) goto done;
|
||||
if (fread(row, 1, (size_t)rowsize, src.f) != (size_t)rowsize) goto done;
|
||||
}
|
||||
for (int tx = 0; tx < tw_; tx++) {
|
||||
int sx = (int)((long)tx * w / tw_);
|
||||
unsigned char *p = o + (size_t)tx * 4;
|
||||
if (bpp == 24) {
|
||||
const unsigned char *q = row + sx * 3;
|
||||
p[0] = q[2]; p[1] = q[1]; p[2] = q[0]; p[3] = 255;
|
||||
} else if (bpp == 32) {
|
||||
const unsigned char *q = row + sx * 4;
|
||||
p[0] = q[2]; p[1] = q[1]; p[2] = q[0]; p[3] = 255; /* BI_RGB:alpha 不作为语义 */
|
||||
} else if (bpp == 16) {
|
||||
unsigned v = le16(row + sx * 2);
|
||||
unsigned r = (v >> 10) & 31, g = (v >> 5) & 31, b = v & 31;
|
||||
p[0] = (unsigned char)((r << 3) | (r >> 2));
|
||||
p[1] = (unsigned char)((g << 3) | (g >> 2));
|
||||
p[2] = (unsigned char)((b << 3) | (b >> 2));
|
||||
p[3] = 255;
|
||||
} else {
|
||||
unsigned idx = row[sx];
|
||||
p[0] = pal[idx & 255][0]; p[1] = pal[idx & 255][1];
|
||||
p[2] = pal[idx & 255][2]; p[3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
*tw = tw_;
|
||||
*th = th_;
|
||||
*rgba = out;
|
||||
out = NULL;
|
||||
ok = 1;
|
||||
done:
|
||||
free(row);
|
||||
free(out);
|
||||
return ok;
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* meow_select.c — §5.8.6 枚举/预设选择器(M7.2 设置页缺口 #4)。
|
||||
*
|
||||
* 封装 = BottomSheet(标题 + 选项列表)+ meow_listview(SINGLE) 单选列表。选项用 meow_listview
|
||||
* 渲染(单选高亮 primaryContainer、可滚动 meow_scroll 惯性);外层模态复用 meow_ui_modal_*
|
||||
* + meow_ui_modal_capture/restore(与 meow_dialog 同源:scrim 只叠一层/关闭无残影)。
|
||||
* 阻塞式(meow_input_pop 轮询);打开前排空残留触摸;点面板外/左缘返回 = 取消;点选项 = 选中 +
|
||||
* 关闭 + on_change 回调。选项数组定长(≤MEOW_SELECT_MAX_ITEMS)。
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "include/meow_select.h"
|
||||
#include "meow_ui_internal.h" /* meow_input_pop/drain + meow_ui_modal_capture/restore */
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define MEOW_SELECT_ROW 48 /* 选项行高(px) */
|
||||
|
||||
struct meow_select {
|
||||
char title[96];
|
||||
char items[MEOW_SELECT_MAX_ITEMS][64];
|
||||
int nitems;
|
||||
int cur; /* 初始/当前选中项 */
|
||||
int result; /* 选中下标 / -1=取消 */
|
||||
int done;
|
||||
int h; /* 面板高度(0=自动) */
|
||||
meow_select_fn cb;
|
||||
void *ctx;
|
||||
};
|
||||
|
||||
/* meow_listview src2 数据源:填 title=options[idx](右尾件无;enabled=1)。 */
|
||||
static int select_src2(int idx, meow_list_item *out, void *ctx)
|
||||
{
|
||||
meow_select *s = (meow_select *)ctx;
|
||||
if (idx < 0 || idx >= s->nitems) return 0;
|
||||
memset(out, 0, sizeof *out);
|
||||
out->title = s->items[idx];
|
||||
out->right = MEOW_LIST_RIGHT_NONE;
|
||||
out->enabled = 1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
meow_select *meow_select_create(const char *title, int cur)
|
||||
{
|
||||
meow_select *s = (meow_select *)calloc(1, sizeof *s);
|
||||
if (!s) return NULL;
|
||||
if (title) { strncpy(s->title, title, sizeof s->title - 1); s->title[sizeof s->title - 1] = '\0'; }
|
||||
s->cur = cur; s->result = -1; s->done = 0;
|
||||
return s;
|
||||
}
|
||||
int meow_select_add_item(meow_select *s, const char *label)
|
||||
{
|
||||
if (!s || s->nitems >= MEOW_SELECT_MAX_ITEMS) return -1;
|
||||
if (label) { strncpy(s->items[s->nitems], label, 63); s->items[s->nitems][63] = '\0'; }
|
||||
int idx = s->nitems;
|
||||
s->nitems++;
|
||||
return idx;
|
||||
}
|
||||
void meow_select_set_on_change(meow_select *s, meow_select_fn cb, void *ctx)
|
||||
{ if (s) { s->cb = cb; s->ctx = ctx; } }
|
||||
|
||||
int meow_select_open(meow_select *s)
|
||||
{
|
||||
if (!s || s->nitems <= 0) return -1;
|
||||
const meow_theme *th = meow_theme_get();
|
||||
meow_input_start();
|
||||
meow_input_drain(); /* 排空残留触摸(goodix 长尾) */
|
||||
|
||||
s->done = 0; s->result = -1;
|
||||
int W = meow_ui_width(), H = meow_ui_height();
|
||||
int sb = meow_ui_statusbar_height(); if (sb < 0) sb = 0;
|
||||
int pad = 24;
|
||||
int title_h = meow_ui_text_height(th->font_title);
|
||||
int rowh = MEOW_SELECT_ROW;
|
||||
int avail = H - sb - 20;
|
||||
int min_h = pad + title_h + 12 + rowh + 20;
|
||||
int want = (s->h > 0) ? s->h : min_h;
|
||||
int est = pad + title_h + 12 + s->nitems * rowh + 20;
|
||||
if (est > want) want = est;
|
||||
if (want > avail) want = avail;
|
||||
if (want < min_h) want = (min_h < avail) ? min_h : avail;
|
||||
int ph = want, py = H - ph; /* 底部面板(顶部圆角) */
|
||||
int list_y = py + pad + title_h + 12;
|
||||
int list_h = ph - (list_y - py) - pad; /* 底部余量 */
|
||||
if (list_h < rowh) list_h = rowh;
|
||||
|
||||
/* 选项列表(meow_listview[SINGLE]:单选高亮 + 可滚动)。open 时创建,close 释放(冷路径)。 */
|
||||
meow_widget *lv = meow_listview_create(0, list_y, W, list_h, s->nitems,
|
||||
select_src2, s, MEOW_LIST_SEL_SINGLE);
|
||||
if (!lv) return -1;
|
||||
meow_listview_set_item_h(lv, rowh);
|
||||
/* 定位初始选中项在视口内。 */
|
||||
int target = s->cur * rowh - (list_h - rowh) / 2;
|
||||
int maxs = meow_listview_max_scroll(lv);
|
||||
if (target < 0) target = 0;
|
||||
if (target > maxs) target = maxs;
|
||||
meow_listview_set_scroll(lv, target);
|
||||
meow_listview_set_sel(lv, s->cur);
|
||||
meow_scroll *sc = meow_scroll_wrap(lv, 1); /* 惯性滚动(fling/snap) */
|
||||
|
||||
/* §6.4 模态作用域 + 背景快照(关闭即还原,无残影/不叠黑)。 */
|
||||
meow_ui_modal *m = meow_ui_modal_begin();
|
||||
meow_ui_modal_capture();
|
||||
/* 首次绘制:遮罩 + 底部面板(顶部圆角)+ 标题 + 选项列表。 */
|
||||
meow_ui_fill_rect_alpha(0, 0, W, H, 0x000000, 150);
|
||||
meow_gfx_fill_round(0, py, W, ph, th->radius_card, th->surfaceContainerHighest);
|
||||
meow_ui_text(pad, py + pad, s->title, th->font_title, th->onSurface);
|
||||
meow_widget_draw(lv);
|
||||
meow_ui_flush();
|
||||
|
||||
int dy_start = 0, down = 0;
|
||||
while (!s->done) {
|
||||
meow_input_ev e;
|
||||
while (meow_input_pop(&e)) {
|
||||
meow_ui_activity_ping(); /* 模态期间交互也重置息屏计时 */
|
||||
if (e.type == MEOW_EV_TOUCH_DOWN) { down = 1; dy_start = e.b; }
|
||||
else if (e.type == MEOW_EV_TOUCH_MOVE) {
|
||||
if (down && sc) {
|
||||
meow_scroll_track(sc, e.b - dy_start, meow_ui_time_ms());
|
||||
dy_start = e.b;
|
||||
}
|
||||
} else if (e.type == MEOW_EV_TAP) {
|
||||
int x = e.a, y = e.b;
|
||||
if (y < py) { s->done = 1; s->result = -1; break; } /* 点面板外 = 取消 */
|
||||
int idx = meow_listview_tap_at(lv, x, y); /* 单选高亮 = 选中 */
|
||||
if (idx >= 0) {
|
||||
s->result = idx; s->cur = idx;
|
||||
if (s->cb) s->cb(idx, s->ctx); /* 关闭即回调 */
|
||||
s->done = 1;
|
||||
}
|
||||
/* 面板内但未命中项:保持打开(等下一次点击)。 */
|
||||
} else if (e.type == MEOW_EV_SWIPE) {
|
||||
if (e.a == MEOW_SWIPE_LEFT) { s->done = 1; s->result = -1; } /* 返回 = 取消 */
|
||||
else if (sc) meow_scroll_fling(sc);
|
||||
}
|
||||
}
|
||||
meow_anim_tick(); /* 推进 fling */
|
||||
if (!s->done) usleep(16000);
|
||||
}
|
||||
meow_ui_modal_end(m);
|
||||
meow_ui_modal_restore();
|
||||
if (sc) meow_scroll_free(sc);
|
||||
meow_widget_free(lv);
|
||||
return s->result;
|
||||
}
|
||||
void meow_select_free(meow_select *s) { if (s) free(s); }
|
||||
|
||||
int meow_select_show(const char *title, const char *const *opts, int n, int cur)
|
||||
{
|
||||
if (!opts || n <= 0) return -1;
|
||||
meow_select *s = meow_select_create(title, cur);
|
||||
if (!s) return -1;
|
||||
for (int i = 0; i < n && i < MEOW_SELECT_MAX_ITEMS; i++) meow_select_add_item(s, opts[i]);
|
||||
int r = meow_select_open(s);
|
||||
meow_select_free(s);
|
||||
return r;
|
||||
}
|
||||
+15
-1
@@ -32,8 +32,14 @@
|
||||
* include/meow_ui_log.h 应用日志(§9)
|
||||
* include/meow_ui_notify.h 应用通知(§9.1)
|
||||
* include/meow_ui_config.h 应用配置读写(§9.2)
|
||||
* include/meow_ui_font.h 字体列表(§5.7)+ 字体分层(§10)
|
||||
* include/meow_ui_font.h 字体列表(§5.7)+ 字体分层(§10)+ 矢量字 meow_ft_*(§10.1)
|
||||
* 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_lazygrid.h 2 维虚拟化宫格(§12.4:meow_lazygrid,M7.4 主界面前置)
|
||||
* include/meow_select.h 枚举/预设选择器(M7.2 设置页缺口 #4:BottomSheet+单选列表)
|
||||
* 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 +69,13 @@
|
||||
#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_lazygrid.h"
|
||||
#include "include/meow_select.h"
|
||||
#include "include/meow_ui_str.h"
|
||||
#include "include/meow_ui_net.h" /* 期4 网络自愿通道:meow_net_policy_check / meow_net_report */
|
||||
#include "include/meow_cachegov.h" /* 磁盘缓存 page-cache 治理(A:DONTNEED L0/L1 + refault 回退;C 归 svc_perf) */
|
||||
|
||||
#endif /* MEOW_UI_H */
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* meow_ui_anim.c — 动画基座 + UI 节拍器(§12.2)。
|
||||
*
|
||||
* 时间驱动 tween:start 记录起始时刻(meow_ui_time_ms,单调),tick 按经过时间求 t,
|
||||
* 代入 ease 得 v = from + (to-from)*ease(t),回调 update;t 到 1 → 回调 done(canceled=0) 并回收。
|
||||
* 取消 → 回调 done(canceled=1) 并回收。静态池(≤MEOW_ANIM_MAX),热路径零 malloc。
|
||||
* 无动画时 meow_anim_tick() 仅查活动计数即返回(零开销)。
|
||||
*
|
||||
* 节拍挂接:由 meow_ui_run 主循环(on_tick 之后)与 meow_ui_feed_tick(阻塞 IME 循环)调用,
|
||||
* 不引入常驻高频轮询。modal/dialog 阻塞循环不喂 tick(动画暂停,符合「模态期不做动画」惯例)。
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "include/meow_ui_anim.h"
|
||||
#include <string.h>
|
||||
|
||||
struct meow_anim {
|
||||
int active;
|
||||
float from, to;
|
||||
uint32_t duration_ms;
|
||||
meow_ease ease;
|
||||
uint64_t start_ms;
|
||||
float value;
|
||||
int region_set;
|
||||
int rx, ry, rw, rh;
|
||||
meow_anim_update_fn update;
|
||||
meow_anim_done_fn done;
|
||||
void *ctx;
|
||||
};
|
||||
|
||||
static struct meow_anim g_pool[MEOW_ANIM_MAX];
|
||||
static unsigned g_active = 0; /* 活动动画计数(0 = 空闲,tick 直退) */
|
||||
|
||||
/* ---- 缓动 ---- */
|
||||
float meow_ease_apply(meow_ease e, float t)
|
||||
{
|
||||
if (e == MEOW_EASE_OUT) {
|
||||
/* ease-out quad */
|
||||
float u = 1.0f - t;
|
||||
return 1.0f - u * u;
|
||||
}
|
||||
if (e == MEOW_EASE_IN_OUT) {
|
||||
/* ease-in-out quad */
|
||||
if (t < 0.5f) return 2.0f * t * t;
|
||||
float u = -2.0f * t + 2.0f;
|
||||
return 1.0f - u * u * 0.5f;
|
||||
}
|
||||
if (e == MEOW_EASE_OVERSHOOT) {
|
||||
/* ease-out-back(越过目标再回稳);t=1 → 恰为 1 */
|
||||
const float c1 = 1.70158f;
|
||||
const float c3 = c1 + 1.0f;
|
||||
float u = t - 1.0f;
|
||||
return 1.0f + c3 * u * u * u + c1 * u * u;
|
||||
}
|
||||
/* MEOW_EASE_LINEAR */
|
||||
return t;
|
||||
}
|
||||
|
||||
/* ---- 生命周期 ---- */
|
||||
meow_anim *meow_anim_start(const meow_anim_desc *d)
|
||||
{
|
||||
if (!d) return NULL;
|
||||
for (int i = 0; i < MEOW_ANIM_MAX; i++) {
|
||||
struct meow_anim *a = &g_pool[i];
|
||||
if (a->active) continue;
|
||||
a->active = 1;
|
||||
a->from = d->from;
|
||||
a->to = d->to;
|
||||
a->duration_ms = d->duration_ms ? d->duration_ms : 1; /* 0 → 瞬发(下一 tick 完成) */
|
||||
a->ease = d->ease;
|
||||
a->start_ms = meow_ui_time_ms();
|
||||
a->update = d->update;
|
||||
a->done = d->done;
|
||||
a->ctx = d->ctx;
|
||||
a->region_set = 0;
|
||||
a->value = d->from;
|
||||
g_active++;
|
||||
if (a->update) a->update(a, a->value, a->ctx); /* 起始帧回调(t=0) */
|
||||
return a;
|
||||
}
|
||||
return NULL; /* 池满 */
|
||||
}
|
||||
|
||||
void meow_anim_cancel(meow_anim *a)
|
||||
{
|
||||
if (!a || !a->active) return;
|
||||
a->active = 0;
|
||||
if (g_active > 0) g_active--;
|
||||
if (a->done) a->done(a, 1, a->ctx);
|
||||
}
|
||||
|
||||
int meow_anim_active(meow_anim *a) { return (a && a->active) ? 1 : 0; }
|
||||
float meow_anim_value(meow_anim *a) { return a ? a->value : 0.0f; }
|
||||
|
||||
void meow_anim_set_region(meow_anim *a, int x, int y, int w, int h)
|
||||
{
|
||||
if (!a) return;
|
||||
if (x < 0) { a->region_set = 0; return; }
|
||||
a->region_set = 1;
|
||||
a->rx = x; a->ry = y; a->rw = w > 0 ? w : 0; a->rh = h > 0 ? h : 0;
|
||||
}
|
||||
|
||||
/* ---- 节拍器 ---- */
|
||||
unsigned meow_anim_count(void) { return (unsigned)g_active; }
|
||||
|
||||
void meow_anim_tick(void)
|
||||
{
|
||||
if (g_active == 0) return; /* 无动画:零开销直接返回 */
|
||||
uint64_t now = meow_ui_time_ms();
|
||||
for (int i = 0; i < MEOW_ANIM_MAX; i++) {
|
||||
struct meow_anim *a = &g_pool[i];
|
||||
if (!a->active) continue;
|
||||
uint64_t elapsed = now - a->start_ms;
|
||||
float t = (float)(elapsed) / (float)a->duration_ms;
|
||||
int finished = (t >= 1.0f);
|
||||
if (t > 1.0f) t = 1.0f;
|
||||
if (t < 0.0f) t = 0.0f;
|
||||
float v = a->from + (a->to - a->from) * meow_ease_apply(a->ease, t);
|
||||
a->value = v;
|
||||
if (a->update) a->update(a, v, a->ctx);
|
||||
if (a->region_set && a->rw > 0 && a->rh > 0)
|
||||
meow_ui_flush_region(a->rx, a->ry, a->rw, a->rh); /* 只刷动画影响区 */
|
||||
if (finished) {
|
||||
a->active = 0;
|
||||
if (g_active > 0) g_active--;
|
||||
if (a->done) a->done(a, 0, a->ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
+40
-5
@@ -75,7 +75,7 @@ static const char *sock_path(void)
|
||||
}
|
||||
|
||||
/* 连接 audiod。**全部有界**:connect 非阻塞 + poll 2s(对端 listen backlog 满/wedge 时
|
||||
* 阻塞 connect 会把 App 播放线程永久压进内核——真机长流卡死的同款路径);
|
||||
* 阻塞 connect 会把 App 播放线程永久压进内核——真机长流卡死时 ncm 卡死的同款路径);
|
||||
* 建连后 SO_SNDTIMEO/RCVTIMEO 2s:audiod 停摆时 write 超时返回错误 → 标记流死,
|
||||
* App 走「重新 open」恢复路径,绝不永久阻塞。 */
|
||||
#define AUD_CONN_TMO_MS 2000
|
||||
@@ -229,7 +229,8 @@ static int fmt_bps(int fmt)
|
||||
case AUDIO_FMT_S16: return 2;
|
||||
case AUDIO_FMT_S24:
|
||||
case AUDIO_FMT_S32:
|
||||
case AUDIO_FMT_FLOAT: return 4;
|
||||
case AUDIO_FMT_FLOAT:
|
||||
case AUDIO_FMT_DOP: return 4; /* DOP=32-bit DoP 容器(16bit DSD+标记+0),同 S32 宽 */
|
||||
default: return 2;
|
||||
}
|
||||
}
|
||||
@@ -262,7 +263,7 @@ static int send_write_chunk(const uint8_t *p, uint32_t len)
|
||||
|
||||
/* ---- 公开 API ---- */
|
||||
|
||||
int meow_audio_open(int rate, int ch, int fmt)
|
||||
int meow_audio_open_ex(int rate, int ch, int fmt, uint32_t flags)
|
||||
{
|
||||
/* env 检查:调试直开 */
|
||||
const char *de = getenv("MEOW_AUDIO_DIRECT");
|
||||
@@ -270,7 +271,10 @@ int meow_audio_open(int rate, int ch, int fmt)
|
||||
|
||||
pthread_mutex_lock(&g_mu);
|
||||
if (g_has_stream) { pthread_mutex_unlock(&g_mu); return MEOW_ERR_BUSY; }
|
||||
if (ch > 2 || ch < 1 || rate <= 0 || fmt < AUDIO_FMT_S16 || fmt > AUDIO_FMT_FLOAT || fmt == AUDIO_FMT_FLOAT) {
|
||||
int dsd = (fmt == AUDIO_FMT_DOP) && (flags & AUDIO_FLAG_DSD);
|
||||
int fmt_ok = (fmt == AUDIO_FMT_S16 || fmt == AUDIO_FMT_S24 ||
|
||||
fmt == AUDIO_FMT_S32 || fmt == AUDIO_FMT_DOP);
|
||||
if (ch > 2 || ch < 1 || rate <= 0 || !fmt_ok || (fmt == AUDIO_FMT_DOP && !dsd)) {
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
return MEOW_ERR_INVAL;
|
||||
}
|
||||
@@ -297,7 +301,7 @@ int meow_audio_open(int rate, int ch, int fmt)
|
||||
m_audio_open_t o;
|
||||
memset(&o, 0, sizeof o);
|
||||
snprintf(o.app_id, sizeof o.app_id, "%s", getenv("MEOW_LOG_APP") ? getenv("MEOW_LOG_APP") : "app");
|
||||
o.rate = (uint32_t)rate; o.ch = (uint16_t)ch; o.fmt = (uint16_t)fmt;
|
||||
o.rate = (uint32_t)rate; o.ch = (uint16_t)ch; o.fmt = (uint16_t)fmt; o.flags = flags;
|
||||
uint32_t req = (g_pending == 0xFFFFFFFF) ? 1 : (g_pending + 1); /* 单调递增(单个挂起) */
|
||||
g_pending = req;
|
||||
g_resp_len = 0;
|
||||
@@ -322,6 +326,35 @@ int meow_audio_open(int rate, int ch, int fmt)
|
||||
return (int)g_sid;
|
||||
}
|
||||
|
||||
int meow_audio_open(int rate, int ch, int fmt)
|
||||
{
|
||||
return meow_audio_open_ex(rate, ch, fmt, 0);
|
||||
}
|
||||
|
||||
int meow_audio_set_volume(int pct)
|
||||
{
|
||||
if (pct < 0) pct = 0;
|
||||
if (pct > 100) pct = 100;
|
||||
pthread_mutex_lock(&g_mu);
|
||||
int fd = conn_locked();
|
||||
if (fd < 0) { pthread_mutex_unlock(&g_mu); return MEOW_ERR_NOENT; }
|
||||
m_audio_volume_t v; v.pct = (int32_t)pct;
|
||||
uint32_t req = (g_pending == 0xFFFFFFFF) ? 1 : (g_pending + 1);
|
||||
g_pending = req;
|
||||
if (send_frame(g_fd, MSG_AUDIO_VOLUME, req, &v, sizeof v) != 0) {
|
||||
mark_dead_locked(); pthread_mutex_unlock(&g_mu); return MEOW_ERR_IO;
|
||||
}
|
||||
/* 等 ACK(3s 超时;audiod 停摆时透明失败标记流死) */
|
||||
struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); ts.tv_sec += 3;
|
||||
int rc = 0;
|
||||
while (g_pending == req) {
|
||||
if (pthread_cond_timedwait(&g_cond, &g_mu, &ts) != 0) { rc = MEOW_ERR_TIMEOUT; break; }
|
||||
}
|
||||
if (g_resp_type != MSG_ACK) rc = (rc == 0) ? MEOW_ERR_IO : rc;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int meow_audio_write(const void *pcm, int frames)
|
||||
{
|
||||
if (!pcm || frames <= 0) return 0;
|
||||
@@ -429,8 +462,10 @@ static int state_text_parse(const char *t, meow_audio_state *o)
|
||||
if (strncmp(p + 4, "S16", 3) == 0) f = AUDIO_FMT_S16;
|
||||
else if (strncmp(p + 4, "S24", 3) == 0) f = AUDIO_FMT_S24;
|
||||
else if (strncmp(p + 4, "S32", 3) == 0) f = AUDIO_FMT_S32;
|
||||
else if (strncmp(p + 4, "DoP", 3) == 0) f = AUDIO_FMT_DOP;
|
||||
o->fmt = (uint16_t)f;
|
||||
}
|
||||
if ((p = strstr(t, "dsd="))) o->dsd = atoi(p + 4);
|
||||
if ((p = strstr(t, "written="))) o->written_frames = (uint64_t)strtoull(p + 8, NULL, 10);
|
||||
if ((p = strstr(t, "played="))) o->played_frames = (uint64_t)strtoull(p + 7, NULL, 10);
|
||||
if ((p = strstr(t, "underrun="))) o->underrun = (uint32_t)atoi(p + 9);
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
/*
|
||||
* meow_ui_box.c — 线性布局引擎(§12.1 meow_box)。
|
||||
*
|
||||
* meow_box 是**纯布局计算器**(非控件):静态池创建、定长子项数组、arrange 单遍求解,
|
||||
* 把每个子控件的 x/y/w/h 写进 meow_widget(meow_widget_set_rect);嵌套 sub-box 递归求解。
|
||||
* 用显式静态栈(深度≤MEOW_BOX_MAX_DEPTH)做嵌套展开,无 C 递归,杜绝栈爆炸。
|
||||
*
|
||||
* 求解模型(主轴向):
|
||||
* - 每个子项占位 = lead(margin) + extent + trail(margin);项间再叠 box->gap。
|
||||
* - FIXED/WRAP 的 extent 先定;WEIGHT 的 extent=0(待均分)。
|
||||
* - leftover = content_main - foot;若权重和>0 → 均分 leftover;否则按主轴对齐分配。
|
||||
* 交叉轴:按 START/CENTER/END/STRETCH 对齐,STRETCH 填满容器交叉向减 margin。
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "include/meow_ui_box.h"
|
||||
#include "meow_ui_internal.h" /* meow_widget_rect(只读几何) */
|
||||
#include <string.h>
|
||||
|
||||
#define MEOW_BOX_POOL 16 /* 静态池大小(并发 box 数) */
|
||||
|
||||
struct meow_box {
|
||||
int dir; /* MEOW_BOX_H / MEOW_BOX_V */
|
||||
int x, y, w, h; /* 容器矩形(绝对坐标) */
|
||||
int pad_l, pad_t, pad_r, pad_b;
|
||||
int gap;
|
||||
int main_align;
|
||||
int cross_align;
|
||||
meow_box_item items[MEOW_BOX_MAX_CHILD];
|
||||
int n;
|
||||
};
|
||||
|
||||
static meow_box g_pool[MEOW_BOX_POOL];
|
||||
static unsigned char g_used[MEOW_BOX_POOL];
|
||||
|
||||
/* ---- 静态池存取 ---- */
|
||||
meow_box *meow_box_new(meow_box_dir dir)
|
||||
{
|
||||
for (int i = 0; i < MEOW_BOX_POOL; i++) {
|
||||
if (!g_used[i]) {
|
||||
g_used[i] = 1;
|
||||
meow_box *b = &g_pool[i];
|
||||
memset(b, 0, sizeof *b);
|
||||
b->dir = (dir == MEOW_BOX_H) ? MEOW_BOX_H : MEOW_BOX_V;
|
||||
return b;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
void meow_box_free(meow_box *b)
|
||||
{
|
||||
if (!b) return;
|
||||
for (int i = 0; i < MEOW_BOX_POOL; i++)
|
||||
if (&g_pool[i] == b) { g_used[i] = 0; return; }
|
||||
}
|
||||
|
||||
void meow_box_set_rect(meow_box *b, int x, int y, int w, int h)
|
||||
{
|
||||
if (!b) return;
|
||||
b->x = x; b->y = y;
|
||||
b->w = w > 0 ? w : 0; b->h = h > 0 ? h : 0;
|
||||
}
|
||||
void meow_box_set_padding(meow_box *b, int l, int t, int r, int btm)
|
||||
{
|
||||
if (!b) return;
|
||||
b->pad_l = l; b->pad_t = t; b->pad_r = r; b->pad_b = btm;
|
||||
}
|
||||
void meow_box_set_gap(meow_box *b, int gap)
|
||||
{
|
||||
if (!b) return;
|
||||
b->gap = gap > 0 ? gap : 0;
|
||||
}
|
||||
void meow_box_set_align(meow_box *b, int main_align, int cross_align)
|
||||
{
|
||||
if (!b) return;
|
||||
b->main_align = main_align;
|
||||
b->cross_align = cross_align;
|
||||
}
|
||||
|
||||
/* ---- 子项 ---- */
|
||||
int meow_box_add(meow_box *b, meow_widget *w, int size, int weight)
|
||||
{
|
||||
return meow_box_add_ex(b, w, size, weight, 0, 0, NULL, NULL);
|
||||
}
|
||||
int meow_box_add_ex(meow_box *b, meow_widget *w, int size, int weight,
|
||||
int min, int preferred, meow_box_measure_fn m, void *mctx)
|
||||
{
|
||||
if (!b || !w || b->n >= MEOW_BOX_MAX_CHILD) return -1;
|
||||
if (size != MEOW_BOX_FIXED && size != MEOW_BOX_WEIGHT && size != MEOW_BOX_WRAP) return -1;
|
||||
if (size == MEOW_BOX_WEIGHT && weight <= 0) return -1;
|
||||
for (int i = 0; i < b->n; i++)
|
||||
if (b->items[i].w == w) return -1; /* 同控件去重 */
|
||||
meow_box_item *it = &b->items[b->n++];
|
||||
memset(it, 0, sizeof *it);
|
||||
it->w = w; it->size = size; it->weight = weight > 0 ? weight : 1;
|
||||
it->min = min > 0 ? min : 0;
|
||||
it->preferred = preferred > 0 ? preferred : 0;
|
||||
it->measure = m; it->mctx = mctx;
|
||||
return 0;
|
||||
}
|
||||
int meow_box_add_sub(meow_box *b, meow_box *sub, int size, int weight)
|
||||
{
|
||||
if (!b || !sub || b->n >= MEOW_BOX_MAX_CHILD) return -1;
|
||||
if (size != MEOW_BOX_FIXED && size != MEOW_BOX_WEIGHT && size != MEOW_BOX_WRAP) return -1;
|
||||
if (size == MEOW_BOX_WEIGHT && weight <= 0) return -1;
|
||||
for (int i = 0; i < b->n; i++)
|
||||
if (b->items[i].sub == sub) return -1;
|
||||
meow_box_item *it = &b->items[b->n++];
|
||||
memset(it, 0, sizeof *it);
|
||||
it->sub = sub; it->size = size; it->weight = weight > 0 ? weight : 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- 查询 ---- */
|
||||
int meow_box_count(const meow_box *b) { return b ? b->n : 0; }
|
||||
meow_widget *meow_box_widget_at(const meow_box *b, int idx)
|
||||
{ return (b && idx >= 0 && idx < b->n) ? b->items[idx].w : NULL; }
|
||||
meow_box *meow_box_sub_at(const meow_box *b, int idx)
|
||||
{ return (b && idx >= 0 && idx < b->n) ? b->items[idx].sub : NULL; }
|
||||
void meow_box_rect(const meow_box *b, int out[4])
|
||||
{
|
||||
if (!out) return;
|
||||
if (!b) { out[0] = out[1] = out[2] = out[3] = 0; return; }
|
||||
out[0] = b->x; out[1] = b->y; out[2] = b->w; out[3] = b->h;
|
||||
}
|
||||
|
||||
/* ---- 内部:取子项 margin(widget opts;sub 无 margin)---- */
|
||||
static void item_margin(const meow_box_item *it, meow_box_dir dir,
|
||||
int *main_before, int *main_after, int *cross)
|
||||
{
|
||||
*main_before = *main_after = *cross = 0;
|
||||
if (!it->w) return;
|
||||
meow_widget_opts o;
|
||||
if (meow_widget_opts_get(it->w, &o) != 0) return;
|
||||
int mx = o.margin_x > 0 ? o.margin_x : 0;
|
||||
int my = o.margin_y > 0 ? o.margin_y : 0;
|
||||
if (dir == MEOW_BOX_H) { *main_before = mx; *main_after = mx; *cross = my; }
|
||||
else { *main_before = my; *main_after = my; *cross = mx; }
|
||||
}
|
||||
|
||||
/* ---- 内部:子项主轴向 extent ----
|
||||
* FIXED → 控件/嵌套盒当前主轴向尺寸
|
||||
* WRAP → measure 回调(若有)否则控件当前尺寸;下限 clamp 到 min
|
||||
* WEIGHT → 0(由均分阶段填充) */
|
||||
static int item_extent(const meow_box_item *it, int main_is_w)
|
||||
{
|
||||
int extent = 0;
|
||||
if (it->w) {
|
||||
int r[4]; meow_widget_rect(it->w, r);
|
||||
extent = main_is_w ? r[2] : r[3];
|
||||
} else if (it->sub) {
|
||||
extent = main_is_w ? it->sub->w : it->sub->h;
|
||||
}
|
||||
if (it->size == MEOW_BOX_WRAP && it->measure) {
|
||||
int mn = 0, pf = 0;
|
||||
it->measure(it->mctx, &mn, &pf);
|
||||
if (pf > 0) extent = pf;
|
||||
else if (mn > 0) extent = mn;
|
||||
if (mn > 0 && extent < mn) extent = mn; /* clamp 到下限 */
|
||||
}
|
||||
if (extent < 0) extent = 0;
|
||||
return extent;
|
||||
}
|
||||
|
||||
/* ---- 单容器求解(一次 pass:算 extent + 位置,写全部子项矩形)----
|
||||
* 子项为嵌套 meow_box 时只写其矩形(父求结果),后续由 meow_box_arrange 的
|
||||
* 显式栈递归求解其内部。 */
|
||||
static void box_layout_one(meow_box *b)
|
||||
{
|
||||
int n = b->n;
|
||||
if (n <= 0) return;
|
||||
int cx = b->x + b->pad_l;
|
||||
int cy = b->y + b->pad_t;
|
||||
int cw = b->w - b->pad_l - b->pad_r; if (cw < 0) cw = 0;
|
||||
int ch = b->h - b->pad_t - b->pad_b; if (ch < 0) ch = 0;
|
||||
int main_is_w = (b->dir == MEOW_BOX_H);
|
||||
int main_size = main_is_w ? cw : ch;
|
||||
int cross_size = main_is_w ? ch : cw;
|
||||
int gap = b->gap;
|
||||
|
||||
int ext[MEOW_BOX_MAX_CHILD], lead[MEOW_BOX_MAX_CHILD],
|
||||
trail[MEOW_BOX_MAX_CHILD], wgt[MEOW_BOX_MAX_CHILD];
|
||||
int nw = 0, wsum = 0;
|
||||
long long foot = 0;
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
const meow_box_item *it = &b->items[i];
|
||||
int mb, ma, cr;
|
||||
item_margin(it, (meow_box_dir)b->dir, &mb, &ma, &cr);
|
||||
lead[i] = mb; trail[i] = ma; (void)cr;
|
||||
wgt[i] = (it->size == MEOW_BOX_WEIGHT) ? it->weight : 0;
|
||||
if (it->size == MEOW_BOX_WEIGHT) {
|
||||
ext[i] = 0; nw++; wsum += it->weight;
|
||||
} else {
|
||||
ext[i] = item_extent(it, main_is_w);
|
||||
}
|
||||
foot += (long long)lead[i] + trail[i] + ext[i];
|
||||
}
|
||||
foot += (long long)gap * (n - 1);
|
||||
|
||||
long long leftover = (long long)main_size - foot;
|
||||
|
||||
if (nw > 0 && leftover > 0) {
|
||||
/* 均分剩余到权重项;余数塞给最后一个权重项铺满精确 */
|
||||
long long consumed = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (wgt[i] > 0) { ext[i] = (int)(leftover * wgt[i] / wsum); consumed += ext[i]; }
|
||||
}
|
||||
for (int i = n - 1; i >= 0; i--) {
|
||||
if (wgt[i] > 0) { ext[i] += (int)(leftover - consumed); break; }
|
||||
}
|
||||
leftover = 0;
|
||||
}
|
||||
|
||||
/* 主轴对齐(仅当无权重、leftover != 0 有意义;有权重时 leftover≈0 → START 等效) */
|
||||
int off0 = 0;
|
||||
int intergap = gap;
|
||||
if (nw == 0) {
|
||||
if (leftover > 0) {
|
||||
switch (b->main_align) {
|
||||
case MEOW_BOX_ALIGN_CENTER: off0 = (int)(leftover / 2); break;
|
||||
case MEOW_BOX_ALIGN_END: off0 = (int)leftover; break;
|
||||
case MEOW_BOX_ALIGN_SPACE_BETWEEN: off0 = 0;
|
||||
intergap = (n > 1) ? gap + (int)(leftover / (n - 1)) : gap; break;
|
||||
case MEOW_BOX_ALIGN_SPACE_AROUND: off0 = (int)(leftover / (2 * n));
|
||||
intergap = gap + (int)(leftover / n); break;
|
||||
case MEOW_BOX_ALIGN_SPACE_EVENLY: off0 = (int)(leftover / (n + 1));
|
||||
intergap = gap + (int)(leftover / (n + 1)); break;
|
||||
default: off0 = 0; break; /* START */
|
||||
}
|
||||
} else {
|
||||
off0 = 0; intergap = gap; /* 溢出:从起点排(可越界,交滚动容器) */
|
||||
}
|
||||
}
|
||||
|
||||
/* 主轴定位 + 交叉轴定位,写子控件矩形(主轴起点 = 内容原点 cx/cy)。
|
||||
* rel = 相对内容原点的主轴游标;content_rel = 本项内容相对起点;content_pos = 绝对主轴坐标。 */
|
||||
int main_base = main_is_w ? cx : cy;
|
||||
int rel = off0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
const meow_box_item *it = &b->items[i];
|
||||
int content_rel = rel + lead[i];
|
||||
int content_pos = main_base + content_rel;
|
||||
if (it->w) {
|
||||
int r[4]; meow_widget_rect(it->w, r);
|
||||
meow_widget_opts o; int has = (meow_widget_opts_get(it->w, &o) == 0);
|
||||
int cmx = (has && o.margin_x > 0) ? o.margin_x : 0;
|
||||
int cmy = (has && o.margin_y > 0) ? o.margin_y : 0;
|
||||
int cm = main_is_w ? cmy : cmx;
|
||||
int base = main_is_w ? r[3] : r[2];
|
||||
int cross = 0, cross_ext = 0;
|
||||
if (b->cross_align == MEOW_BOX_CROSS_STRETCH) {
|
||||
cross_ext = cross_size - 2 * cm; if (cross_ext < 0) cross_ext = 0;
|
||||
cross = cm;
|
||||
} else {
|
||||
cross_ext = base;
|
||||
int pad = cross_size - 2 * cm - cross_ext; if (pad < 0) pad = 0;
|
||||
if (b->cross_align == MEOW_BOX_CROSS_CENTER) cross = cm + pad / 2;
|
||||
else if (b->cross_align == MEOW_BOX_CROSS_END) cross = cm + pad;
|
||||
else cross = cm; /* START */
|
||||
}
|
||||
if (main_is_w)
|
||||
meow_widget_set_rect(it->w, content_pos, cy + cross, ext[i], cross_ext);
|
||||
else
|
||||
meow_widget_set_rect(it->w, cx + cross, content_pos, cross_ext, ext[i]);
|
||||
} else if (it->sub) {
|
||||
meow_box *sb = it->sub;
|
||||
int base = main_is_w ? sb->h : sb->w;
|
||||
int cross = 0, cross_ext = 0;
|
||||
if (b->cross_align == MEOW_BOX_CROSS_STRETCH) {
|
||||
cross_ext = cross_size; if (cross_ext < 0) cross_ext = 0;
|
||||
cross = 0;
|
||||
} else {
|
||||
cross_ext = base;
|
||||
int pad = cross_size - cross_ext; if (pad < 0) pad = 0;
|
||||
if (b->cross_align == MEOW_BOX_CROSS_CENTER) cross = pad / 2;
|
||||
else if (b->cross_align == MEOW_BOX_CROSS_END) cross = pad;
|
||||
else cross = 0;
|
||||
}
|
||||
if (main_is_w) { sb->x = content_pos; sb->y = cy + cross; sb->w = ext[i]; sb->h = cross_ext; }
|
||||
else { sb->x = cx + cross; sb->y = content_pos; sb->w = cross_ext; sb->h = ext[i]; }
|
||||
}
|
||||
if (i < n - 1)
|
||||
rel = content_rel + ext[i] + trail[i] + intergap;
|
||||
else
|
||||
rel = content_rel + ext[i] + trail[i];
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 求解:单遍,显式静态栈展开嵌套(深度≤MEOW_BOX_MAX_DEPTH)---- */
|
||||
static meow_box *g_stack[MEOW_BOX_MAX_DEPTH];
|
||||
|
||||
void meow_box_arrange(meow_box *b)
|
||||
{
|
||||
if (!b) return;
|
||||
int top = 0;
|
||||
g_stack[0] = b;
|
||||
while (top >= 0) {
|
||||
meow_box *cur = g_stack[top--];
|
||||
box_layout_one(cur); /* 写全部直接子项矩形(含 sub->rect) */
|
||||
for (int i = 0; i < cur->n; i++) {
|
||||
meow_box *sb = cur->items[i].sub;
|
||||
if (!sb) continue;
|
||||
if (top + 1 >= MEOW_BOX_MAX_DEPTH) continue; /* 深度上限:跳过(矩形已被父写定) */
|
||||
g_stack[++top] = sb;
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
-12
@@ -27,7 +27,7 @@
|
||||
static unsigned char *g_modal_cap = NULL;
|
||||
static int g_modal_cap_line = 0, g_modal_cap_h = 0, g_modal_cap_bpp = 0;
|
||||
|
||||
static void modal_capture(void)
|
||||
void meow_ui_modal_capture(void)
|
||||
{
|
||||
unsigned char *bb = meow_ui_back_buf();
|
||||
int line = meow_ui_back_buf_pitch();
|
||||
@@ -43,7 +43,7 @@ static void modal_capture(void)
|
||||
if (g_modal_cap) memcpy(g_modal_cap, bb, n);
|
||||
}
|
||||
|
||||
static void modal_restore(void)
|
||||
void meow_ui_modal_restore(void)
|
||||
{
|
||||
unsigned char *bb = meow_ui_back_buf();
|
||||
if (!g_modal_cap || !bb) return;
|
||||
@@ -100,8 +100,8 @@ static void dlg_calc(meow_dialog *d, const meow_theme *th, dlg_geom *g)
|
||||
static void dlg_card(meow_dialog *d, const meow_theme *th, const dlg_geom *g)
|
||||
{
|
||||
int pad = 24;
|
||||
meow_gfx_fill_round(g->dx, g->dy, g->dw, g->dh, th->radius_card, th->surfaceContainerHighest);
|
||||
meow_ui_draw_rect_round(g->dx, g->dy, g->dw, g->dh, th->radius_card, th->outline);
|
||||
meow_gfx_fill_round(g->dx, g->dy, g->dw, g->dh, MEOW_SHAPE_EXTRA_LARGE, th->surfaceContainerHighest);
|
||||
meow_ui_draw_rect_round(g->dx, g->dy, g->dw, g->dh, MEOW_SHAPE_EXTRA_LARGE, th->outline);
|
||||
meow_ui_text(g->dx + pad, g->dy + pad, d->title, th->font_title, th->onSurface);
|
||||
|
||||
if (d->type == MEOW_DLG_SELECT) {
|
||||
@@ -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();
|
||||
meow_ui_modal_capture(); /* M2.71-bug3:拍下背景,关闭即还原(scrim 只叠一层/无残影);
|
||||
* 嵌套模态在各自打开时拍「当前已渲染帧」,故两层弹开不叠黑。 */
|
||||
dlg_paint(d, th);
|
||||
meow_ui_flush();
|
||||
|
||||
@@ -286,7 +290,8 @@ void meow_dialog_show(meow_dialog *d)
|
||||
if (d->done) break;
|
||||
usleep(16000);
|
||||
}
|
||||
modal_restore(); /* M2.71-bug3:关闭还原背景,无残影 */
|
||||
meow_ui_modal_end(m); /* §6.4:关闭模态作用域(下层恢复接收触摸) */
|
||||
meow_ui_modal_restore(); /* M2.71-bug3:关闭还原背景,无残影 */
|
||||
}
|
||||
|
||||
int meow_dialog_result(meow_dialog *d) { return d ? d->result : 0; }
|
||||
@@ -349,10 +354,11 @@ 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:模态作用域(下层收不到触摸) */
|
||||
meow_ui_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);
|
||||
meow_gfx_fill_round(0, py, W, ph, MEOW_SHAPE_EXTRA_LARGE, th->surfaceContainerHighest);
|
||||
meow_ui_text(pad, py + pad, s->title, th->font_title, th->onSurface);
|
||||
for (int i = 0; i < s->nitems; i++) {
|
||||
int ry = py + pad + meow_ui_text_height(th->font_title) + 12 + i * MEOW_MODAL_ROW;
|
||||
@@ -382,7 +388,8 @@ int meow_sheet_open(meow_sheet *s)
|
||||
if (s->done) break;
|
||||
usleep(16000);
|
||||
}
|
||||
modal_restore(); /* M2.71-bug3:关闭还原背景,无残影 */
|
||||
meow_ui_modal_end(m); /* §6.4:关闭模态作用域 */
|
||||
meow_ui_modal_restore(); /* M2.71-bug3:关闭还原背景,无残影 */
|
||||
return s->result;
|
||||
}
|
||||
void meow_sheet_free(meow_sheet *s) { if (s) free(s); }
|
||||
@@ -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 / 区域重画 / 整帧 */
|
||||
/* ============================================================ */
|
||||
|
||||
+216
-39
@@ -12,7 +12,7 @@
|
||||
#include "meow_ui_internal.h"
|
||||
#include "meow_ui_sys.h"
|
||||
#include "sb_icons.h"
|
||||
#include "meow_font.h" /* 公开 SDK 仓内 vendored(include/meow_font.h),不再引主仓 launcher 头 */
|
||||
#include "meow_font.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -20,7 +20,6 @@
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <time.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/mman.h>
|
||||
|
||||
@@ -172,15 +171,9 @@ static void refresh_governor(unsigned long long cost_ms, int period)
|
||||
}
|
||||
|
||||
/* ---- 工具 ---- */
|
||||
static uint64_t now_ms(void)
|
||||
{
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (uint64_t)ts.tv_sec * 1000u + (uint64_t)ts.tv_nsec / 1000000u;
|
||||
}
|
||||
|
||||
uint64_t meow_ui_time_ms(void) { return now_ms(); }
|
||||
|
||||
/* 时钟统一由 meow_ui_time.c 提供(meow_ui_time_ms,声明见 include/meow_ui_draw.h):
|
||||
* mipsel 上 zig musl 的 time64 ABI 自相矛盾,取时必须走内核 o32 ABI 再升位,
|
||||
* 故本 TU 不再保留取时实现(详见 sdk/CLOCK_ABI.md)。 */
|
||||
static uint16_t rgb565(uint32_t c)
|
||||
{
|
||||
return (uint16_t)(((c >> 8) & 0xF800) | ((c >> 5) & 0x07E0) | ((c >> 3) & 0x001F));
|
||||
@@ -254,22 +247,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 +269,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 +305,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)
|
||||
{
|
||||
@@ -811,6 +823,17 @@ int meow_ui_text_height(int size)
|
||||
return 7 * fallback_scale(size);
|
||||
}
|
||||
|
||||
/* ================= 矢量字(FreeType)渲染薄封装(§10.1) =================
|
||||
* meow_ft_text 在 meow_ui_ft.c(独立 .o,App 显式调 meow_ft_* 才拉取整条 FreeType 依赖),
|
||||
* 它经本处两个薄封装写进 back_buf:blend 一个灰度像素(过 clip)、标一块脏区。
|
||||
* 这两函数在 gfx.o 里(任何 App 本就拉取 gfx.o),ft.o 引用它们不引入新的未拉入成员,
|
||||
* 从而保证「不调 meow_ft_* 的 App 零增量」(freetype .o 不会被拉入)。 */
|
||||
void meow_gfx_ft_blend_px(int x, int y, uint32_t color, int alpha)
|
||||
{ put_px_blend(x, y, color, alpha); }
|
||||
|
||||
void meow_gfx_ft_dirty(int x, int y, int w, int h)
|
||||
{ dirty_add(x, y, w, h); }
|
||||
|
||||
/* ================= RGBA blit(图片层用) ================= */
|
||||
void meow_gfx_blit_rgba(int x, int y, int w, int h, const uint8_t *rgba, int alpha)
|
||||
{
|
||||
@@ -833,6 +856,106 @@ void meow_gfx_blit_rgba(int x, int y, int w, int h, const uint8_t *rgba, int alp
|
||||
dirty_add(x1, y1, x2 - x1, y2 - y1);
|
||||
}
|
||||
|
||||
/* ================= RGB565 页图(M32) =================
|
||||
* 阅读器页槽常驻 RGB565(2B/px)而不是 RGBA:真机 480×853 页 1599KB → 800KB。
|
||||
* 量化与 back_buf 用同一个 rgb565()(逐位截断),所以 565 屏上落下的像素与旧的
|
||||
* RGBA blit 路径**逐像素一致**——不引入色偏,也不会比现在多出色带。
|
||||
* 命中页(解码/落框尺寸 == 显示尺寸)走 meow_gfx_blit_rgb565:每行一次 memcpy,
|
||||
* 单次拷贝、没有每帧重采样。alpha 按不透明处理(页图 alpha 恒为 255;带透明像素的
|
||||
* PNG/32bpp BMP 页会与旧 RGBA 路径不同——旧路径混合到底色,这里直接画 RGB)。 */
|
||||
uint16_t meow_gfx_rgb565(uint32_t rgb)
|
||||
{
|
||||
return rgb565(rgb);
|
||||
}
|
||||
|
||||
void meow_gfx_rgba_to_rgb565(const uint8_t *rgba, uint16_t *out, int npx)
|
||||
{
|
||||
if (!rgba || !out || npx <= 0) return;
|
||||
for (int i = 0; i < npx; i++) {
|
||||
const uint8_t *p = rgba + (size_t)i * 4;
|
||||
out[i] = rgb565(((uint32_t)p[0] << 16) | ((uint32_t)p[1] << 8) | (uint32_t)p[2]);
|
||||
}
|
||||
}
|
||||
|
||||
/* 目标矩形与 clip 求交;返回 1=还有可画区域。 */
|
||||
static int blit565_clip(int x, int y, int w, int h, int *x1, int *y1, int *x2, int *y2)
|
||||
{
|
||||
if (!g_init_ok || w <= 0 || h <= 0) return 0;
|
||||
*x1 = x; *y1 = y; *x2 = x + w; *y2 = y + h;
|
||||
if (*x1 < 0) *x1 = 0;
|
||||
if (*y1 < 0) *y1 = 0;
|
||||
if (*x2 > g_xres) *x2 = g_xres;
|
||||
if (*y2 > g_yres) *y2 = g_yres;
|
||||
if (g_clip_n > 0) {
|
||||
int *c = g_clip_stack[g_clip_n - 1];
|
||||
if (*x1 < c[0]) *x1 = c[0];
|
||||
if (*y1 < c[1]) *y1 = c[1];
|
||||
if (*x2 > c[2]) *x2 = c[2];
|
||||
if (*y2 > c[3]) *y2 = c[3];
|
||||
}
|
||||
return *x2 > *x1 && *y2 > *y1;
|
||||
}
|
||||
|
||||
void meow_gfx_blit_rgb565(int x, int y, int w, int h, const uint16_t *px)
|
||||
{
|
||||
if (!px) return;
|
||||
int x1, y1, x2, y2;
|
||||
if (!blit565_clip(x, y, w, h, &x1, &y1, &x2, &y2)) return;
|
||||
int n = x2 - x1;
|
||||
for (int py = y1; py < y2; py++) {
|
||||
const uint16_t *src = px + (size_t)(py - y) * w + (x1 - x);
|
||||
unsigned char *dst = g_fb + (size_t)py * g_line + (size_t)x1 * (size_t)(g_bpp / 8);
|
||||
if (g_bpp == 16) {
|
||||
memcpy(dst, src, (size_t)n * 2); /* 命中页:每行一次拷贝 */
|
||||
} else {
|
||||
uint32_t *d32 = (uint32_t *)dst;
|
||||
for (int i = 0; i < n; i++) {
|
||||
uint16_t c = src[i];
|
||||
d32[i] = ((uint32_t)ex5((c >> 11) & 0x1F) << 24) |
|
||||
((uint32_t)ex6((c >> 5) & 0x3F) << 16) |
|
||||
((uint32_t)ex5(c & 0x1F) << 8) | 0xFFu;
|
||||
}
|
||||
}
|
||||
}
|
||||
dirty_add(x1, y1, n, y2 - y1);
|
||||
}
|
||||
|
||||
/* 最近邻缩放(仅几何不符时的兜底路径;采样点与 meow_img_resample 同式)。
|
||||
* 命中页不会走到这里——那是热路径纪律(M17/M32)。 */
|
||||
void meow_gfx_blit_rgb565_scaled(int x, int y, int w, int h,
|
||||
int sw, int sh, const uint16_t *px)
|
||||
{
|
||||
if (!px || sw <= 0 || sh <= 0) return;
|
||||
int x1, y1, x2, y2;
|
||||
if (!blit565_clip(x, y, w, h, &x1, &y1, &x2, &y2)) return;
|
||||
int n = x2 - x1;
|
||||
for (int py = y1; py < y2; py++) {
|
||||
int sy = (int)((long long)(py - y) * sh / h);
|
||||
if (sy >= sh) sy = sh - 1;
|
||||
const uint16_t *row = px + (size_t)sy * sw;
|
||||
unsigned char *dst = g_fb + (size_t)py * g_line + (size_t)x1 * (size_t)(g_bpp / 8);
|
||||
if (g_bpp == 16) {
|
||||
uint16_t *d16 = (uint16_t *)dst;
|
||||
for (int i = 0; i < n; i++) {
|
||||
int sx = (int)((long long)(x1 + i - x) * sw / w);
|
||||
if (sx >= sw) sx = sw - 1;
|
||||
d16[i] = row[sx];
|
||||
}
|
||||
} else {
|
||||
uint32_t *d32 = (uint32_t *)dst;
|
||||
for (int i = 0; i < n; i++) {
|
||||
int sx = (int)((long long)(x1 + i - x) * sw / w);
|
||||
if (sx >= sw) sx = sw - 1;
|
||||
uint16_t c = row[sx];
|
||||
d32[i] = ((uint32_t)ex5((c >> 11) & 0x1F) << 24) |
|
||||
((uint32_t)ex6((c >> 5) & 0x3F) << 16) |
|
||||
((uint32_t)ex5(c & 0x1F) << 8) | 0xFFu;
|
||||
}
|
||||
}
|
||||
}
|
||||
dirty_add(x1, y1, n, y2 - y1);
|
||||
}
|
||||
|
||||
/* ================= present / 上屏 ================= */
|
||||
static void dump_ppm(void)
|
||||
{
|
||||
@@ -865,7 +988,7 @@ static void statusbar_collapse_step(void)
|
||||
int tgt = g_sbar_collapsed ? MUI_SBAR_H : 0;
|
||||
if (g_sbar_collapse_h == tgt) return;
|
||||
if (g_sbar_collapse_anim_ms <= 0) { g_sbar_collapse_h = tgt; return; }
|
||||
unsigned long long now = now_ms();
|
||||
unsigned long long now = meow_ui_time_ms();
|
||||
unsigned long long el = now - g_sbar_collapse_start_ms;
|
||||
int span = g_sbar_collapse_anim_ms;
|
||||
if ((int)el >= span) { g_sbar_collapse_h = tgt; return; }
|
||||
@@ -902,7 +1025,7 @@ void meow_ui_statusbar_set_collapsed(int on)
|
||||
if (tgt == g_sbar_collapsed) return;
|
||||
g_sbar_collapsed = tgt;
|
||||
g_sbar_collapse_from_h = g_sbar_collapse_h;
|
||||
g_sbar_collapse_start_ms = now_ms();
|
||||
g_sbar_collapse_start_ms = meow_ui_time_ms();
|
||||
dirty_full(); /* 顶栏显隐/上收变化 → 全帧重画 */
|
||||
meow_ui_sbar_write_mode(); /* App 进程上报顶条模式(launcher 感知全屏/收合,agent-146) */
|
||||
}
|
||||
@@ -944,13 +1067,33 @@ static int statusbar_touched(void)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- §10.5 叠加让位自抑(present 层)----
|
||||
* /tmp/meow_notif_active 存在 = 通知中心叠加会话进行中(launcher 正在 fb 上渲染面板)。
|
||||
* App 进程(非系统 UI 渲染者)此时不上屏;200ms 节流缓存存在性(别每帧 stat)。
|
||||
/* ---- §10.5 叠加让位自抑(present / input 共用判定)----
|
||||
* /tmp/meow_notif_active = 前台屏幕归属标记,**内容=当前前台 app_id**(空=菜单/叠加层覆盖/
|
||||
* 无前台单元,即「抑制全部 App」)。App 进程(非系统 UI 渲染者)据此判「我是否前台」:
|
||||
* - 内容==本进程 app_id(env MEOW_LOG_APP)→ 前台 → 照常上屏 / 收输入;
|
||||
* - 内容为空 → 菜单前台或被叠加层覆盖 → 全抑制(含自身);
|
||||
* - 内容为其它 app_id → 后台常驻(批①多 App)/非前台 → 自抑上屏 + 排空输入。
|
||||
* 200ms 节流缓存存在性+内容(别每帧 open/stat)。
|
||||
* 计数 g_overlay_suppressed 供宿主单测断言(meow_gfx_overlay_suppressed_count)。 */
|
||||
static unsigned long long g_overlay_suppressed = 0;
|
||||
/* §后台化保活-残帧:记录「上一次 present 是否处于叠加自抑态」,用于在抑制→不抑制的恢复边沿
|
||||
* 强制整帧重画(盖掉 launcher 在后台化期间画进 buffer0 的桌面/主界面帧)。 */
|
||||
static int g_was_overlay_suppressed = 0;
|
||||
|
||||
static int overlay_present_suppressed(void)
|
||||
/* 本进程 app_id(env MEOW_LOG_APP,launcher 注入)= 判「我是否前台」的身份锚。 */
|
||||
static const char *overlay_own_app(void)
|
||||
{
|
||||
static const char *s = NULL;
|
||||
if (!s) {
|
||||
const char *e = getenv("MEOW_LOG_APP");
|
||||
s = (e && e[0]) ? e : "";
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
/* §批①多 App 后台常驻:present 层与 input 层共用的「是否自抑」判定(单一实现,防两端漂移)。
|
||||
* 系统 UI(launcher 渲染面板/主界面)恒 0。返回 1=本进程应自抑上屏+排空输入。 */
|
||||
int meow_ui_overlay_suppressed_active(void)
|
||||
{
|
||||
if (meow_ui_sys_render_enabled()) return 0; /* launcher(系统 UI)渲染面板,照常上屏 */
|
||||
static char s_path[108];
|
||||
@@ -962,10 +1105,32 @@ static int overlay_present_suppressed(void)
|
||||
snprintf(s_path, sizeof s_path, "%s", (e && e[0]) ? e : MEOW_NOTIF_ACTIVE_DEFAULT);
|
||||
s_path_init = 1;
|
||||
}
|
||||
unsigned long long now = now_ms();
|
||||
unsigned long long now = meow_ui_time_ms();
|
||||
if (s_last_chk && now - s_last_chk < MEOW_OVERLAY_CHK_MS) return s_last_res;
|
||||
s_last_chk = now;
|
||||
s_last_res = (access(s_path, F_OK) == 0) ? 1 : 0;
|
||||
{
|
||||
char owner[48];
|
||||
int have = 0;
|
||||
owner[0] = 0;
|
||||
int fd = open(s_path, O_RDONLY);
|
||||
if (fd >= 0) {
|
||||
ssize_t n = read(fd, owner, sizeof(owner) - 1);
|
||||
close(fd);
|
||||
if (n > 0) owner[n] = 0;
|
||||
have = 1; /* 文件存在(无论内容)→ 处于抑制管控 */
|
||||
} else {
|
||||
have = 0; /* 文件不存在 → 无抑制 */
|
||||
}
|
||||
if (!have) {
|
||||
s_last_res = 0;
|
||||
} else if (owner[0] == 0) {
|
||||
s_last_res = 1; /* 空内容=全抑制(菜单前台/叠加层覆盖) */
|
||||
} else {
|
||||
const char *me = overlay_own_app();
|
||||
/* 内容==本进程 app_id → 前台不抑制;否则(后台常驻/非前台)→ 抑制 */
|
||||
s_last_res = (strcmp(owner, me) == 0) ? 0 : 1;
|
||||
}
|
||||
}
|
||||
return s_last_res;
|
||||
}
|
||||
|
||||
@@ -976,13 +1141,25 @@ static void present(void)
|
||||
if (!g_init_ok || !g_back_buf) return;
|
||||
if (!(g_dirty_full || g_ndirty > 0)) return;
|
||||
/* §10.5 叠加让位自抑(仅 App 进程):App 前台播放中唤起通知中心时 launcher 不 SIGSTOP
|
||||
* (保 PCM 供流,App 播放线程同进程),改由 App 进程在此自抑上屏——绘制照常进 back_buf,
|
||||
* 只是不 present 到 fb,防 App 重绘穿透 launcher 叠加层。系统 UI 进程(launcher 渲染面板)
|
||||
* 不抑制。抑制期脏区保留累积(超上限自然升级整帧),标记消失后下一次 present 一次上屏。 */
|
||||
if (overlay_present_suppressed()) { g_overlay_suppressed++; return; }
|
||||
* (保 PCM 供流,ncm 播放线程同进程),改由 App 进程在此自抑上屏——绘制照常进 back_buf,
|
||||
* 只是不 present 到 fb,防 App 重绘穿透 launcher 叠加层;后台常驻(批①)App 同判据自抑。
|
||||
* 系统 UI 进程(launcher 渲染面板)不抑制。抑制期脏区保留累积(超上限自然升级整帧),
|
||||
* 判定为「回到前台」后下一次 present 一次上屏。 */
|
||||
if (meow_ui_overlay_suppressed_active()) { g_overlay_suppressed++; g_was_overlay_suppressed = 1; return; }
|
||||
/* §后台化保活-残帧:抑制→不抑制的**恢复边沿**强制整帧重画。App 被 launcher 转后台保活
|
||||
* (/tmp/meow_notif_active 存在)期间 present 被自抑,back_buf 仍保留 App 自己的完整帧;
|
||||
* 但 launcher 已把桌面/主界面整帧画进 buffer0 盖在 App 残帧上。恢复前台时若只 present
|
||||
* 抑制期累积的小脏区(时钟/状态栏),launcher 的桌面帧会在 App 未重画区域透出(真机
|
||||
* 「桌面叠在 ncm UI 下面/旧播放界面层叠」。标记由有→无即「回到前台」,强制 dirty_full
|
||||
* 让下一次 present 把 App 完整 back_buf 一次上屏,盖掉桌面帧。仅真实 App 进程(launcher
|
||||
* 系统 UI 恒不抑制,本边沿不触发)。 */
|
||||
if (g_was_overlay_suppressed) {
|
||||
g_was_overlay_suppressed = 0;
|
||||
dirty_full();
|
||||
}
|
||||
/* 帧率门控(A-1/A-2,从 launcher 迁入):present 距上次 ≥ period(1000/eff_rate) 才呈现;
|
||||
* 未满周期则保留脏区(不 dirty_reset),下一轮再试。refresh_always_max=1 时跳过门控(恒满速)。 */
|
||||
unsigned long long now = now_ms();
|
||||
unsigned long long now = meow_ui_time_ms();
|
||||
int period = (g_eff_rate > 0) ? (1000 / g_eff_rate) : 16;
|
||||
if (!g_refresh_always_max) {
|
||||
if (g_last_present_ms && now - g_last_present_ms < (unsigned long long)period) return;
|
||||
@@ -1012,7 +1189,7 @@ static void present(void)
|
||||
}
|
||||
if (g_fb_mem) {
|
||||
ioctl(g_fb_fd, FBIO_WAITFORVSYNC, 0); /* 等垂直同步 */
|
||||
unsigned long long tbg = now_ms();
|
||||
unsigned long long tbg = meow_ui_time_ms();
|
||||
/* 「同一个状态栏」闭环:App 进程不渲染状态栏(meow_ui_sys_render_enabled()==0),也绝不向
|
||||
* 真实 fb 写顶条 inset 区(顶条由 launcher 系统进程叠加绘制)。否则 App 首帧全量拷贝
|
||||
* (meow_ui_init 置 dirty_full)会把 launcher 刚叠画的顶条冲成未初始化数据——真机
|
||||
@@ -1049,7 +1226,7 @@ static void present(void)
|
||||
}
|
||||
}
|
||||
}
|
||||
refresh_governor(now_ms() - tbg, period);
|
||||
refresh_governor(meow_ui_time_ms() - tbg, period);
|
||||
}
|
||||
g_last_present_ms = now;
|
||||
g_frames_presented++;
|
||||
|
||||
+11
-11
@@ -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); }
|
||||
}
|
||||
}
|
||||
|
||||
+180
-7
@@ -1,10 +1,11 @@
|
||||
/*
|
||||
* meow_ui_img.c — 图片层(B1.2)。基于 stb_image(单头文件、静态编入),
|
||||
* 解 PNG/JPEG/BMP → RGBA,统一最近邻缩放/居中裁方。
|
||||
* meow_ui_img.c — 图片层(B1.2)。解码:M23 真缩解后端(meow_jpeg.c / meow_png_stream.c)
|
||||
* 优先,不支持/失败回退 stb_image(单头文件、静态编入);统一输出 RGBA,最近邻缩放/居中裁方。
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
#include "stb_image.h"
|
||||
#include "meow_jpeg.h" /* M23 真缩解后端(内部头,不对外导出) */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -42,6 +43,147 @@ meow_img *meow_img_load_file(const char *path)
|
||||
return img_alloc(w, h, p);
|
||||
}
|
||||
|
||||
/* 追加(M12):预算解码。选整数缩解档位 s ∈ {1,2,4,8}——取满足「长边/s ≥ maxdim」的最大档,
|
||||
* 保证缩解结果不欠采样(不丢细节),再由 meow_img_downscale 精修到 maxdim。长边 < 2*maxdim
|
||||
* 时无档可用(s=1),退回整幅解码后精修——此时本就接近目标尺寸,无需牺牲。 */
|
||||
int meow_img_pick_scale(int longside, int maxdim)
|
||||
{
|
||||
int s = 1;
|
||||
while (s < 8 && (longside / (s * 2)) >= maxdim) s *= 2;
|
||||
return s;
|
||||
}
|
||||
|
||||
/* 追加(M12):缩解结果落地 + 需要时精修。p 所有权转交。 */
|
||||
static meow_img *img_finish_dim(unsigned char *p, int w, int h, int maxdim)
|
||||
{
|
||||
if (!p) return NULL;
|
||||
meow_img *im = img_alloc(w, h, p);
|
||||
if (!im) return NULL;
|
||||
if (im->w <= maxdim && im->h <= maxdim) return im;
|
||||
meow_img *small = meow_img_downscale(im, maxdim);
|
||||
if (!small) return im; /* 精修失败:缩解结果已小于原图,仍可用 */
|
||||
meow_img_free(im);
|
||||
return small;
|
||||
}
|
||||
|
||||
int meow_img_probe_file(const char *path, int *w, int *h, int *channels)
|
||||
{
|
||||
int pw = 0, ph = 0, pc = 0;
|
||||
if (!path || !stbi_info(path, &pw, &ph, &pc)) return 0;
|
||||
if (w) *w = pw;
|
||||
if (h) *h = ph;
|
||||
if (channels) *channels = pc;
|
||||
return 1;
|
||||
}
|
||||
|
||||
meow_img *meow_img_load_file_dim(const char *path, int maxdim)
|
||||
{
|
||||
if (!path) return NULL;
|
||||
if (maxdim <= 0) return meow_img_load_file(path); /* 不限尺寸 = 旧入口 */
|
||||
/* 追加(M23):真缩解后端直接产出目标尺寸(JPEG 按 DCT 档位;PNG 逐行 inflate 丢行丢列;
|
||||
* BMP 按行 seek 跳采样)。峰值只与输出尺寸相关。
|
||||
* 不支持/失败/几乎等尺寸(4 分量 CMYK、算术编码、交错 PNG、RLE BMP、小图…)返回 0 →
|
||||
* 落回下面的 stb 旧路径,对外语义与档位表现不变。 */
|
||||
{
|
||||
unsigned char *bp = NULL;
|
||||
int bw = 0, bh = 0;
|
||||
if (meow_img_backend_file(path, maxdim, &bw, &bh, &bp) && bp)
|
||||
return img_finish_dim(bp, bw, bh, maxdim);
|
||||
}
|
||||
int w = 0, h = 0, c = 0;
|
||||
if (!stbi_info(path, &w, &h, &c)) return NULL;
|
||||
int longside = w > h ? w : h;
|
||||
if (longside <= maxdim) return meow_img_load_file(path); /* 小图不动:旧路径 */
|
||||
unsigned char *p = stbi_load_scaled(path, &w, &h, &c, 4, meow_img_pick_scale(longside, maxdim));
|
||||
return img_finish_dim(p, w, h, maxdim);
|
||||
}
|
||||
|
||||
/* 追加(M22):档位由调用方指定。与 meow_img_load_file_dim 同语义(结果长边 ≤ maxdim,
|
||||
* 超了再精修一次),只是档位不再由 maxdim 反推 —— 内存预算紧时可选更粗的档位。
|
||||
* M27:真缩解后端优先(step 折算成目标长边 ceil(长边/step),后端按该长边选 DCT 阶,
|
||||
* 峰值只与输出尺寸相关);后端不支持时回退 stbi_load_scaled(step),语义与 M22 一致。
|
||||
*
|
||||
* M27 r1(P1-1):allow_stb_fallback=0 时只尝试后端,**绝不回退 stb**——阅读器的门控按
|
||||
* 「后端峰值」放行时必须走这里,否则「头部说支持、解码中途失败」会静默落到 stb,
|
||||
* 实测可放出 18–36MB(截断 12MP JPEG / 头-only BMP / 非法位深 PNG),即 M22 的 SIGKILL 场景。
|
||||
* 后端失败返回 NULL(调用方按坏图占位)。 */
|
||||
meow_img *meow_img_load_file_step_ex(const char *path, int maxdim, int step,
|
||||
int allow_stb_fallback, int *used_backend)
|
||||
{
|
||||
if (used_backend) *used_backend = 0;
|
||||
if (!path) return NULL;
|
||||
if (step <= 1 && maxdim <= 0)
|
||||
return allow_stb_fallback ? meow_img_load_file(path) : NULL;
|
||||
if (step <= 0) step = 1;
|
||||
if (step > 8) step = 8;
|
||||
int w = 0, h = 0, c = 0;
|
||||
if (!stbi_info(path, &w, &h, &c)) return NULL;
|
||||
int longside = w > h ? w : h;
|
||||
if (step == 1 && maxdim <= 0)
|
||||
return allow_stb_fallback ? meow_img_load_file(path) : NULL;
|
||||
/* 追加(M23/M27):真缩解后端优先(JPEG/PNG/BMP 全部格式)。step 是「输出别比这个档位
|
||||
* 更细」的提示:折算成目标长边 ceil(长边/step) 传给后端(后端按该长边自动选 DCT 阶),
|
||||
* 与下面 stbi_load_scaled(step) 的语义一致,峰值只与输出尺寸相关;maxdim<=0 时不精修,
|
||||
* 结果仍是档位尺寸。后端不支持(CMYK/算术编码/交错/RLE…)时回退 stb,行为与 M22 一致。
|
||||
* M27 起阅读器的峰值模型已与后端同口径(page_source 的 reader_estimate_peak),
|
||||
* 所以 PNG/BMP 也一并接后端——这正是「修好了却用不上」的最后一处缝。 */
|
||||
{
|
||||
int eff = maxdim > 0 ? maxdim : longside;
|
||||
int step_cap = (longside + step - 1) / step;
|
||||
unsigned char *bp = NULL;
|
||||
int bw = 0, bh = 0;
|
||||
if (step_cap > 0 && step_cap < eff) eff = step_cap;
|
||||
if (eff > 0 && meow_img_backend_file(path, eff, &bw, &bh, &bp) && bp) {
|
||||
if (used_backend) *used_backend = 1;
|
||||
return img_finish_dim(bp, bw, bh, maxdim > 0 ? maxdim : longside);
|
||||
}
|
||||
}
|
||||
if (!allow_stb_fallback) return NULL; /* P1-1:门控按后端放行的页,失败即坏图 */
|
||||
unsigned char *p = stbi_load_scaled(path, &w, &h, &c, 4, step);
|
||||
/* maxdim<=0:不精修(保留档位输出的原始尺寸)。 */
|
||||
return img_finish_dim(p, w, h, maxdim > 0 ? maxdim : longside);
|
||||
}
|
||||
|
||||
meow_img *meow_img_load_file_step(const char *path, int maxdim, int step)
|
||||
{
|
||||
/* 通用入口保持 M22 语义:后端失败允许回退 stb。 */
|
||||
return meow_img_load_file_step_ex(path, maxdim, step, 1, NULL);
|
||||
}
|
||||
|
||||
/* 追加(M22):精确重采样到目标尺寸(最近邻,双向)。阅读器把缩解档位的结果一次性
|
||||
* 落到显示框,之后命中页就是单次原生 blit(M17 的热路径纪律不被粗档位破坏)。 */
|
||||
static unsigned char *scale_buf(const meow_img *im, int dw, int dh, int cover);
|
||||
|
||||
meow_img *meow_img_resample(meow_img *im, int w, int h)
|
||||
{
|
||||
if (!im || w <= 0 || h <= 0) return NULL;
|
||||
if (im->w == w && im->h == h) return NULL;
|
||||
unsigned char *tmp = scale_buf(im, w, h, 0);
|
||||
if (!tmp) return NULL;
|
||||
return img_alloc(w, h, tmp);
|
||||
}
|
||||
|
||||
meow_img *meow_img_load_mem_dim(const void *buf, int len, int maxdim)
|
||||
{
|
||||
if (!buf || len <= 0) return NULL;
|
||||
if (maxdim <= 0) return meow_img_load_mem(buf, len);
|
||||
/* 追加(M23):同文件入口,后端直接从调用方缓冲解码(不再整幅解 + 重采样)。 */
|
||||
{
|
||||
unsigned char *bp = NULL;
|
||||
int bw = 0, bh = 0;
|
||||
if (meow_img_backend_mem(buf, (size_t)len, maxdim, &bw, &bh, &bp) && bp)
|
||||
return img_finish_dim(bp, bw, bh, maxdim);
|
||||
}
|
||||
int w = 0, h = 0, c = 0;
|
||||
if (!stbi_info_from_memory((const unsigned char *)buf, len, &w, &h, &c)) return NULL;
|
||||
int longside = w > h ? w : h;
|
||||
if (longside <= maxdim) return meow_img_load_mem(buf, len);
|
||||
unsigned char *p = stbi_load_scaled_from_memory((const unsigned char *)buf, len,
|
||||
&w, &h, &c, 4,
|
||||
meow_img_pick_scale(longside, maxdim));
|
||||
return img_finish_dim(p, w, h, maxdim);
|
||||
}
|
||||
|
||||
void meow_img_free(meow_img *im)
|
||||
{
|
||||
if (!im) return;
|
||||
@@ -49,8 +191,12 @@ void meow_img_free(meow_img *im)
|
||||
free(im);
|
||||
}
|
||||
|
||||
int meow_img_w(meow_img *im) { return im ? im->w : 0; }
|
||||
int meow_img_h(meow_img *im) { return im ? im->h : 0; }
|
||||
int meow_img_w(const meow_img *im) { return im ? im->w : 0; }
|
||||
int meow_img_h(const meow_img *im) { return im ? im->h : 0; }
|
||||
|
||||
/* 访问器:取 RGBA 像素指针(meow_img 对外 opaque,本函数为唯一像素读入口;供取色/直读像素)。
|
||||
* NULL 返回 NULL。 */
|
||||
const unsigned char *meow_img_pixels(const meow_img *im) { return im ? im->rgba : NULL; }
|
||||
|
||||
/* 最近邻采样缩放(cover=1 居中裁方),返回 dw*dh*4 临时 RGBA 缓冲 */
|
||||
static unsigned char *scale_buf(const meow_img *im, int dw, int dh, int cover)
|
||||
@@ -70,13 +216,16 @@ static unsigned char *scale_buf(const meow_img *im, int dw, int dh, int cover)
|
||||
s = sx < sy ? sx : sy; /* 源像素/目标像素(铺满 → 取小) */
|
||||
float oxs = ((float)im->w - (float)dw * s) * 0.5f; /* 居中裁剪起点(源像素,≥0) */
|
||||
float oys = ((float)im->h - (float)dh * s) * 0.5f;
|
||||
if (oxs < 0) oxs = 0; if (oys < 0) oys = 0;
|
||||
if (oxs < 0) oxs = 0;
|
||||
if (oys < 0) oys = 0;
|
||||
for (int dy = 0; dy < dh; dy++) {
|
||||
int syi = (int)((float)dy * s + oys);
|
||||
if (syi < 0) syi = 0; if (syi >= im->h) syi = im->h - 1;
|
||||
if (syi < 0) syi = 0;
|
||||
if (syi >= im->h) syi = im->h - 1;
|
||||
for (int dx = 0; dx < dw; dx++) {
|
||||
int sxi = (int)((float)dx * s + oxs);
|
||||
if (sxi < 0) sxi = 0; if (sxi >= im->w) sxi = im->w - 1;
|
||||
if (sxi < 0) sxi = 0;
|
||||
if (sxi >= im->w) sxi = im->w - 1;
|
||||
memcpy(out + ((size_t)dy * dw + dx) * 4,
|
||||
im->rgba + ((size_t)syi * im->w + sxi) * 4, 4);
|
||||
}
|
||||
@@ -96,6 +245,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;
|
||||
|
||||
+167
-20
@@ -26,6 +26,8 @@
|
||||
#include "meow_ui_sys.h"
|
||||
#include "meow_ui_vol.h"
|
||||
#include "meow_ui_media.h"
|
||||
#include "meow_ui_view.h" /* M7.0a 缺口#7:把 meow_view_dispatch_event 接入 meow_ui_run */
|
||||
#include "meow_ui_nav.h" /* 批②:底部上滑回桌面→meow_ui_nav_request_exit(MSG_NAV EXIT→批①保活) */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -56,17 +58,26 @@
|
||||
|
||||
#define SWIPE_THRESH 65 /* px:抬起位移超过视为滑动(GESTURE_BACK: 90→65,用户实测 90 偏多,65 更顺手) */
|
||||
#define EDGE_W 24 /* 左滑返回的「左缘起点」宽度(t_dx < EDGE_W 才认返回手势) */
|
||||
#define HOME_EDGE_H 28 /* 批②:底部起滑区高(自屏底向上;镜像 np_gesture.h NP_HOME_EDGE_H) */
|
||||
#define HOME_SWIPE 65 /* 批②:底部上滑位移阈值(镜像 np_gesture.h NP_HOME_SWIPE;= SWIPE_THRESH) */
|
||||
#define MEOW_LONG_PRESS_MS 500u /* §6.3 通用长按默认阈值(ms;meow_ui_set_long_press_ms 可配) */
|
||||
#define MEOW_LONG_PRESS_SLOP 12 /* §6.3 按下后「仍算按住」的最大位移(px);超此取消长按 */
|
||||
#define QUEUE_N 128
|
||||
#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(顶部热区为系统保留手势)。
|
||||
* 与左缘返回(SWIPE_LEFT)/ meow_ui_swipe_hint 互不影响——后者仍全屏生效,详见下方触摸机。 */
|
||||
#define MEOW_EV_NOTIF_PULL 5
|
||||
/* 批②:底部热区上滑 → 回桌面(后台保活,走批①链路)。系统手势,a=MEOW_SWIPE_UP(供
|
||||
* on_swipe 接管、view 树消费),b=位移(px)。定义在 input.c 内部(同 MEOW_EV_NOTIF_PULL
|
||||
* 未入公共头),宿主单测按同值声明(见 test_meow_ui_home_gesture.c)。 */
|
||||
#define MEOW_EV_HOME 9
|
||||
|
||||
/* 顶部下滑 → 通知中心判定:纵向 DOWN 手势的起点(origin_y)是否落在顶部热区内。
|
||||
* 热区由 meow_ui_statusbar_hotzone() 给出(非全屏=状态栏高、全屏=MEOW_SBAR_HOTZONE_FS、
|
||||
@@ -76,6 +87,20 @@ int meow_ui_input_is_notif_pull(int origin_y, int hotzone)
|
||||
return origin_y >= 0 && origin_y < hotzone;
|
||||
}
|
||||
|
||||
/* 批② 底部热区上滑 → 回桌面(home)判定:镜像 launcher np_gesture.h np_home_gesture 同口径
|
||||
* (热区 28px / 上移 ≥65px / 竖向主导 / 避开左缘返回列),SDK 侧 fb_h=屏幕高(复用
|
||||
* meow_ui_height())。供宿主单测直调(判据与触摸状态机 touch_release 共用)。本判定把
|
||||
* 「底部热区起滑 + 上移超阈值且竖向主导」从普通上滑中剥离——中部上滑/滚动仍归 App
|
||||
* (LazyColumn/LazyGrid 滚动不受影响),底部热区的横向滑仍走原有左缘返回/横向逻辑。 */
|
||||
int meow_ui_input_is_home_swipe(int down_x, int down_y, int dx, int dy, int fb_h)
|
||||
{
|
||||
int adx = dx < 0 ? -dx : dx;
|
||||
if (down_x >= EDGE_W && down_y >= fb_h - HOME_EDGE_H && down_y >= 0 &&
|
||||
dy <= -HOME_SWIPE && adx < -dy)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int g_thread_started = 0;
|
||||
static pthread_t g_thread;
|
||||
static volatile int g_thread_run = 1;
|
||||
@@ -136,28 +161,17 @@ static int meow_input_jack_debounce(void)
|
||||
return g_jack_changed_at && (now - g_jack_changed_at) < 800;
|
||||
}
|
||||
|
||||
/* ---- §10.5 叠加让位:通知中心叠加会话期输入排空(播放中 App 不被 SIGSTOP 的配套)----
|
||||
* 媒体播放中的 App 前台唤起通知中心时 launcher 不冻结 App(保 PCM 供流,App 播放线程同进程),
|
||||
/* ---- §10.5 叠加让位:通知中心叠加会话期 / 批①后台常驻期 输入排空(播放中 App 不被 SIGSTOP 的配套)----
|
||||
* 媒体播放中的 App 前台唤起通知中心时 launcher 不冻结 App(保 PCM 供流,ncm 播放线程同进程),
|
||||
* App 输入线程仍在读同一批 evdev 设备(每 fd 各收一份事件副本,与 launcher 互不剥夺)——若不
|
||||
* 抑制,面板上的一次点击会被 App 再消费一次(如面板媒体卡片「暂停」与 App 同坐标控件双重触发)。
|
||||
* 故标记存在期:排空本进程 fd 队列并丢弃、EVIOCGKEY 位图静默对齐(不产生边沿语义,系统键动作
|
||||
* 叠加期由 launcher/watchdog 单侧承担)。200ms 节流缓存存在性(与 present 层同口径,别每圈 stat)。 */
|
||||
* 抑制,面板上的一次点击会被 App 再消费一次(如面板媒体卡片「暂停」与 App 同坐标控件双重触发);
|
||||
* 批①后台常驻 App 同理:前台 App(如 Hello)时,后台 ncm 的触摸必须被排空(一次交互只响应前台)。
|
||||
* 判据统一走 meow_ui_overlay_suppressed_active()(内容=前台 app_id;非前台/空=抑制)。抑制期:
|
||||
* 排空本进程 fd 队列并丢弃、EVIOCGKEY 位图静默对齐(不产生边沿语义,系统键动作叠加期由
|
||||
* launcher/watchdog 单侧承担)。200ms 节流缓存由共享判定承担(与 present 层同口径)。 */
|
||||
static int overlay_input_suppressed(void)
|
||||
{
|
||||
static char s_path[108];
|
||||
static int s_path_init = 0;
|
||||
static unsigned long long s_last_chk = 0;
|
||||
static int s_last_res = 0;
|
||||
if (!s_path_init) {
|
||||
const char *e = getenv("MEOW_NOTIF_ACTIVE_FILE"); /* 宿主/单测覆写(进程内只解析一次) */
|
||||
snprintf(s_path, sizeof s_path, "%s", (e && e[0]) ? e : "/tmp/meow_notif_active");
|
||||
s_path_init = 1;
|
||||
}
|
||||
unsigned long long now = now_ms();
|
||||
if (s_last_chk && now - s_last_chk < 200) return s_last_res;
|
||||
s_last_chk = now;
|
||||
s_last_res = (access(s_path, F_OK) == 0) ? 1 : 0;
|
||||
return s_last_res;
|
||||
return meow_ui_overlay_suppressed_active();
|
||||
}
|
||||
|
||||
/* ---- 系统按键语义(M2.55:SDK 输入层消化,App 前台也生效)----
|
||||
@@ -197,6 +211,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)
|
||||
@@ -332,7 +347,16 @@ static void touch_release(void)
|
||||
}
|
||||
} else {
|
||||
int dir = mdy > 0 ? MEOW_SWIPE_DOWN : MEOW_SWIPE_UP;
|
||||
push_ev(MEOW_EV_SWIPE, dir, ady > adx ? ady : adx);
|
||||
int dist = ady > adx ? ady : adx;
|
||||
/* 批②:竖向主导且上移(dir==UP)时,判「底部热区上滑 → 回桌面」。仅当起点在底部
|
||||
* 热区(最后 HOME_EDGE_H px)且**不在左缘**(down_x>=EDGE_W,与左缘返回互斥)且
|
||||
* 上移 ≥HOME_SWIPE 且竖向主导(adx<-dy)才命中;否则照常 SWIPE_UP(中部上滑/滚动交
|
||||
* 由 App)。命中改入队 MEOW_EV_HOME(区别于普通上滑,供 run 循环按兜底回桌面)。 */
|
||||
if (dir == MEOW_SWIPE_UP &&
|
||||
meow_ui_input_is_home_swipe(t_dx, t_dy, mdx, mdy, meow_ui_height()))
|
||||
push_ev(MEOW_EV_HOME, MEOW_SWIPE_UP, dist);
|
||||
else
|
||||
push_ev(MEOW_EV_SWIPE, dir, dist);
|
||||
}
|
||||
} else {
|
||||
push_ev(MEOW_EV_TAP, t_cx, t_cy);
|
||||
@@ -404,6 +428,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,11 +727,21 @@ 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;
|
||||
/* M7.0a 缺口#7:meow_view 事件路由(§A.7)——有活动页 view 子树且其 on_event
|
||||
* 命中即消费(不再落 App on_tap);无 view 树/未命中返回 0 → 回落 on_tap 原路径。 */
|
||||
if (meow_view_dispatch_event(MEOW_EV_TAP, e.a, e.b)) continue;
|
||||
if (g_events_set && g_events.on_tap) g_events.on_tap(e.a, e.b);
|
||||
} else if (e.type == MEOW_EV_KEY) {
|
||||
/* M7.0a 缺口#7:先进 view 路由,命中即消费;否则回落 App on_key(语义不变——
|
||||
* 音量/媒体系统键已在输入线程消化,这里只决定 App/页面是否收到语义事件)。 */
|
||||
if (meow_view_dispatch_event(MEOW_EV_KEY, e.a, e.b)) continue;
|
||||
if (g_events_set && g_events.on_key) g_events.on_key(e.a, e.b);
|
||||
} else if (e.type == MEOW_EV_SWIPE) {
|
||||
if (in_startup) continue; /* 启动残留被当左缘滑动:丢弃,防进入即返回 */
|
||||
@@ -633,6 +750,10 @@ 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:抬手(滑动)边沿,释放手势捕获 */
|
||||
/* M7.0a 缺口#7:view 路由先于 App 窗口的 on_swipe 与默认返回手势——页 view
|
||||
* 可消费滑动(页内返回/翻页/横幅);命中即跳过下面对 on_swipe/默认退出的分发。 */
|
||||
if (meow_view_dispatch_event(MEOW_EV_SWIPE, e.a, e.b)) continue;
|
||||
/* A-0 全局左缘返回(方案A):
|
||||
* - APP 注册 on_swipe → APP 接管,自行决定(meow_ui_request_exit / 页内返回);
|
||||
* - APP 未注册 on_swipe 且非全屏(sb_active)→ SDK 默认退出 app(返回=退出);
|
||||
@@ -644,6 +765,22 @@ void meow_ui_run(const meow_ui_events *ev)
|
||||
} else if (e.a == MEOW_SWIPE_LEFT && sb_active) {
|
||||
meow_ui_request_exit();
|
||||
}
|
||||
} else if (e.type == MEOW_EV_HOME) {
|
||||
/* 系统级「底部热区上滑回桌面」(用户 2026-09-04 批②补全,任意 App 内生效):
|
||||
* 屏底边缘热区+竖向主导上滑已由 meow_ui_input_is_home_swipe 过滤(与 App 中部
|
||||
* 滚动、底部横向滑、左缘返回互斥),故作为**系统保留手势**优先于 App on_swipe /
|
||||
* view 树——否则现有 App(ncm/hello/files/reader 均注册 on_swipe)会接管导致
|
||||
* App 内上滑不回桌面。命中调 meow_ui_nav_request_exit()(MSG_NAV EXIT → launcher
|
||||
* 批① meow_bg_add 保活,进程保留转后台、SDK present 自抑);**不**调
|
||||
* meow_ui_request_exit()——那会断送保活(进程 exit(0))。launcher 据 app 声明
|
||||
* (background_keepalive/background_audio+媒体活跃)决定无限期/宽限。 */
|
||||
if (in_startup) continue; /* 启动窗口误触残留:丢弃 */
|
||||
meow_ui_swipe_hint(-1);
|
||||
meow_ui_gesture_end(); /* §6.4:抬手边沿,释放手势捕获 */
|
||||
/* IME 打开时软键盘覆盖底部:键盘拥有底部区,把事件交给 IME 且**绝不**回桌面
|
||||
* (防输入时误退)。 */
|
||||
if (meow_ime_active()) { (void)meow_ime_handle_event(&e); continue; }
|
||||
meow_ui_nav_request_exit();
|
||||
} else if (e.type == MEOW_EV_NOTIF_PULL) {
|
||||
if (in_startup) continue; /* 启动窗口顶区误触残留:丢弃,防进入即开通知中心 */
|
||||
/* 系统级「下拉展开通知中心」:起始于顶部热区的下滑为系统保留手势,不下发
|
||||
@@ -652,6 +789,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 +798,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 +827,9 @@ 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——顺手释放捕获 */
|
||||
/* M7.0a 缺口#7:view 路由先于 §6.3 通用长按回调(页 view 可消费长按)。 */
|
||||
if (meow_view_dispatch_event(MEOW_EV_LONG_PRESS, e.a, e.b)) continue;
|
||||
if (g_long_press_cb) g_long_press_cb(e.a, e.b);
|
||||
}
|
||||
}
|
||||
@@ -697,6 +841,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)也保持时钟走动。
|
||||
|
||||
@@ -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),缓存,随
|
||||
@@ -127,6 +131,13 @@ void meow_gfx_mark_full(void);
|
||||
* 菜单内容盖到 App 内容上)——叠加前用本函数撤销整帧脏,再按需标局部脏(agent-146)。 */
|
||||
void meow_gfx_dirty_reset(void);
|
||||
|
||||
/* ---- 模态背景快照/恢复(meow_ui_dialog.c 提供;dialog/sheet/select 共用)----
|
||||
* open 前调用 capture 把整帧背景拍下(modal_capture),close 后调用 restore 回填 + 整帧上屏
|
||||
* (modal_restore)→ scrim 每次只叠一层、关闭即还原无残影(M2.71-bug3)。共享同一份快照缓冲,
|
||||
* 各模态组件(meow_select 等)无需重复实现。 */
|
||||
void meow_ui_modal_capture(void);
|
||||
void meow_ui_modal_restore(void);
|
||||
|
||||
/* 返回离屏 back_buf 指针(launcher 系统组件与 SDK 共用同一 back_buf 用)。 */
|
||||
unsigned char *meow_ui_back_buf(void);
|
||||
/* back_buf 的每行字节数(pitch)与位深(16/32)。M2.71-bug3:模态/IME 快照恢复用。 */
|
||||
@@ -152,6 +163,12 @@ int meow_gfx_is_device(void);
|
||||
* (仅 App 进程会自抑;宿主单测断言用,设备上无调用方)。 */
|
||||
unsigned long long meow_gfx_overlay_suppressed_count(void);
|
||||
|
||||
/* §批①多 App 后台常驻:本进程是否应自抑上屏+排空输入(present/input 层共用判定)。
|
||||
* /tmp/meow_notif_active 内容=前台 app_id:内容==本进程 app_id → 前台不抑制(返回 0);
|
||||
* 内容为空或其它 app_id → 菜单前台/叠加层覆盖/后台常驻 → 自动抑制(返回 1)。
|
||||
* 系统 UI(launcher)恒 0;200ms 节流缓存。meow_ui_gfx.c 实现,input 层复用。 */
|
||||
int meow_ui_overlay_suppressed_active(void);
|
||||
|
||||
/* 中文/日文字库(mfont)是否已加载(状态栏「蓝牙」CJK 与回退 "BT" 二选一)。 */
|
||||
int meow_gfx_mfont_ready(void);
|
||||
|
||||
@@ -234,6 +251,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 */
|
||||
@@ -262,4 +284,8 @@ int meow_ime_handle_event(const meow_input_ev *e);
|
||||
/* 当前 IME 是否拼音模式(供 meow_ui_input.c 决定是否喂长按轮询)。 */
|
||||
int meow_ime_mode_pinyin(void);
|
||||
|
||||
/* ---- 时钟(meow_ui_time.c)---- */
|
||||
/* 秒/纳秒 → 单调毫秒的唯一换算点(meow_ui_time_ms 与宿主单测共用)。 */
|
||||
uint64_t meow_time_ms_from_parts(int64_t sec, int64_t nsec);
|
||||
|
||||
#endif /* MEOW_UI_INTERNAL_H */
|
||||
+2
-2
@@ -146,9 +146,9 @@ void meow_log_install_exit_handler(void)
|
||||
/* M2.87:SIGUSR1 无害护栏(防线冗余)。watchdog 对「左缘起点+右滑」发 SIGUSR1 当返回事件;
|
||||
* SDK 框架 App(framework=1)自己经 on_swipe 处理左缘返回(meow_ui_input.c),不应被 SIGUSR1
|
||||
* 终止。但看门狗对 framework 判定有误/竞态/测试注入等**任何**路径漏进 SIGUSR1,默认动作都会
|
||||
* 杀死 App(某 App signal=16 崩溃根因之一)。本节安装一个不做任何致命动作的处理器:记日志+忽略
|
||||
* 杀死 App(ncm signal=16 崩溃根因之一)。本节安装一个不做任何致命动作的处理器:记日志+忽略
|
||||
* ——框架 App 的返回/退出语义由 SDK on_swipe 统一决策,SIGUSR1 仅作「绝不杀 App」的保活护栏,
|
||||
* 不重复触发返回(避免与 on_swipe 双重返回)。App 若自装 SIGUSR1 处理器(hello 等框架 App 均未装,
|
||||
* 不重复触发返回(避免与 on_swipe 双重返回)。App 若自装 SIGUSR1 处理器(hello/ncm 均未装,
|
||||
* 裸沉浸 App 自绘处理)会覆盖本处理器;`--sim-swipe` 测试路径也随之被吞。 */
|
||||
static void sigusr1_guard(int sig)
|
||||
{
|
||||
|
||||
+1
-1
@@ -214,7 +214,7 @@ void meow_ui_media_stop(void)
|
||||
|
||||
/* 物理媒体键 keycode → MSG_PLAYCTL cmd(1=播放 2=暂停 3=上一首 4=下一首 5=seek 6=播放/暂停切换)。
|
||||
* 播放/暂停(164)恒发 cmd=6 toggle——SDK 本地不再按「最近上报 state」判 play/pause:
|
||||
* 走自有 IPC 线程的媒体源,不经 meow_ui_media_meta 上报,
|
||||
* 走自有 IPC 线程的媒体源(如 ncm 的 ipc_media.c)不经 meow_ui_media_meta 上报,
|
||||
* 本地 state 恒 0 → 恒发「播放」,播放中按键无效(真机 bug)。toggle 由媒体源按自身
|
||||
* 当前态翻转(权威在源)。上一曲 165/19→3,下一曲 163→4。非媒体键返回 -1。 */
|
||||
static int media_key_cmd(int code)
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
/*
|
||||
* meow_ui_scroll.c — 滚动物理(§12.3)。
|
||||
*
|
||||
* 速度采样:track 时把 (pos, t) 记进定长环形(≤MEOW_SCROLL_SAMPLES),fling 时取
|
||||
* 最近 MEOW_SCROLL_WINDOW ms 窗口,按「间隔速度 × 近者权重」加权得出初速(px/s)。
|
||||
* 位置模型:ease-out quad 的位移曲线(末速恰为 0),初速与输入 v 对齐,时长 = 2×tau。
|
||||
* 边界:free 目标越界 → clamp 到 [0,max];若越界速度大 → 补两段短 IN_OUT 回弹
|
||||
* (overshoot 超过边界方向的内侧再回位),不做弹簧。停止后可选 snap 到 step 倍数。
|
||||
*
|
||||
* 不引 libm(SDK 约定,免 -lm);只用整数 + 纯浮点四则。
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "include/meow_ui_scroll.h"
|
||||
#include "meow_ui_internal.h" /* meow_widget_rect(定位 redraw 视口区) */
|
||||
#include "include/meow_ui_anim.h"
|
||||
#include <string.h>
|
||||
|
||||
#define MEOW_SCROLL_POOL 8
|
||||
#define MEOW_SCROLL_SAMPLES 8 /* 速度采样点数(最近 N 加权) */
|
||||
#define MEOW_SCROLL_WINDOW 110 /* 速度窗口(ms) */
|
||||
#define MEOW_SCROLL_TAU 180 /* 默认摩擦时间常数(ms) */
|
||||
#define MEOW_SCROLL_MIN_SPEED 60 /* 低于此的初速不 fling(px/s) */
|
||||
#define MEOW_SCROLL_MAX_DUR 900 /* fling 时长上限(ms) */
|
||||
#define MEOW_SCROLL_BOUNCE_MS 110 /* 每段回弹时长(ms) */
|
||||
#define MEOW_SCROLL_SNAP_MS 120
|
||||
|
||||
typedef enum {
|
||||
MEOW_SCROLL_IDLE = 0,
|
||||
MEOW_SCROLL_FLING,
|
||||
MEOW_SCROLL_BOUNCE1,
|
||||
MEOW_SCROLL_BOUNCE2,
|
||||
MEOW_SCROLL_SNAP,
|
||||
} meow_scroll_phase;
|
||||
|
||||
struct meow_scroll {
|
||||
meow_scroll_set_fn set;
|
||||
meow_scroll_max_fn max;
|
||||
meow_scroll_get_fn get;
|
||||
meow_scroll_redraw_fn redraw;
|
||||
void *ctx;
|
||||
|
||||
int snap_step; /* >0 = 吸附步长 */
|
||||
int tau_ms; /* 摩擦时间常数 */
|
||||
/* 速度采样(顺序数组,满则前移,≤SAMPLES) */
|
||||
int v_pos[MEOW_SCROLL_SAMPLES];
|
||||
uint64_t v_t[MEOW_SCROLL_SAMPLES];
|
||||
int v_n;
|
||||
|
||||
meow_scroll_phase phase;
|
||||
meow_anim *anim;
|
||||
int maxv; /* fling 开始时缓存 max_scroll */
|
||||
int bounce; /* fling 边界回弹幅度(带符号;0=不回弹) */
|
||||
int bounce_start; /* 回弹回位起点(= clamp 边界) */
|
||||
int snap_to; /* snap 目标 */
|
||||
int wrap_slot; /* >=0:占用的 g_wrap_ctx 槽下标(-1=非 wrap 实例) */
|
||||
};
|
||||
|
||||
static meow_scroll g_pool[MEOW_SCROLL_POOL];
|
||||
static unsigned char g_used[MEOW_SCROLL_POOL];
|
||||
|
||||
/* wrap 上下文槽:meow_scroll_wrap 占用一个,meow_scroll_free 必须归还(否则进程内最多 wrap
|
||||
* MEOW_SCROLL_POOL 次后恒 NULL)。槽由 wrap 实例独占,free 时按 wrap_slot 精确归还。 */
|
||||
typedef struct { meow_widget *w; int lv; } wrap_ctx;
|
||||
static wrap_ctx g_wrap_ctx[MEOW_SCROLL_POOL];
|
||||
static int g_wrap_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;
|
||||
s->wrap_slot = -1;
|
||||
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) {
|
||||
int ws = s->wrap_slot;
|
||||
if (ws >= 0 && ws < MEOW_SCROLL_POOL) { /* wrap 实例:归还上下文槽 */
|
||||
g_wrap_ctx[ws].w = NULL;
|
||||
g_wrap_ctx[ws].lv = 0;
|
||||
g_wrap_used[ws] = 0;
|
||||
}
|
||||
s->wrap_slot = -1; /* 二次 free 时不再触碰任何槽 */
|
||||
g_used[i] = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- wrap:包一个 meow_widget 列表 ---- */
|
||||
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]); /* 只刷视口区 */
|
||||
}
|
||||
|
||||
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];
|
||||
s->wrap_slot = 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; }
|
||||
@@ -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/<pkg>/strings/<locale>.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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
|
||||
#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 覆盖表路径:<MEOW_DATA_DIR 去尾 /data>/strings/<locale>.txt(= MeowData/<pkg>/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 = <pkg>/data;去尾 "/data" 回到 <pkg>,再拼 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 持久化(失败静默) */
|
||||
}
|
||||
@@ -37,6 +37,12 @@ static char g_background[64] = ""; /* display.background(black
|
||||
static meow_theme_listener g_listeners[8];
|
||||
static int g_nlisteners = 0;
|
||||
|
||||
/* §8.1.5 色板注入:一份自定义板覆写当前生效主题(src==NULL 恢复内建)。
|
||||
* 覆写优先于 settings 解析(随 meow_theme_reload() 重读 settings 后仍生效,与 meow_ui_set_style
|
||||
* 的「程序化覆写不清 settings」语义一致)。 */
|
||||
static meow_theme g_inject;
|
||||
static int g_inject_set = 0;
|
||||
|
||||
/* ---- 读 /usr/data/meow/settings.conf 的 key=value(兼容 "key = value" 带空格形式)----
|
||||
* 与 launcher meow_settings_import 同款解析:按 '=' 切分,key/value 各自去首尾空白,
|
||||
* 精确匹配 key(避免 display.theme 误匹配 display.theme_mode)。返回 0=命中,-1=未命中。 */
|
||||
@@ -295,6 +301,7 @@ static void broadcast(void)
|
||||
/* 当前「style×mode」主题:style=0/1(MD3/MIUIX),dark=0(light)/1(dark)。 */
|
||||
static const meow_theme *theme_current(int style, int dark)
|
||||
{
|
||||
if (g_inject_set) return &g_inject; /* §8.1.5 注入板优先 */
|
||||
if (style < MEOW_STYLE_MD3) style = MEOW_STYLE_MD3;
|
||||
if (style > MEOW_STYLE_MIUIX) style = MEOW_STYLE_MIUIX;
|
||||
return &g_theme[style][dark ? 1 : 0];
|
||||
@@ -339,6 +346,26 @@ meow_theme meow_theme_defaults(meow_theme_mode m)
|
||||
return g_theme[MEOW_STYLE_MD3][resolve_mode(m) == MEOW_THEME_DARK ? 1 : 0];
|
||||
}
|
||||
|
||||
/* ---- §8.1.5 色板导出/注入(单一权威桥)----
|
||||
* 导出:把当前「style×mode」解析生效的完整权威主题拷给调用方(不取指针以防调用方误改内部表)。
|
||||
* 注入:用一份自定义板覆写当前生效主题;NULL 恢复内建。二者都触发 load(g_loaded==0 时先读
|
||||
* settings.conf),使宿主无 settings 文件(单测/脱机)也可靠。 */
|
||||
void meow_theme_export(meow_theme *out)
|
||||
{
|
||||
if (!out) return;
|
||||
const meow_theme *t = meow_theme_get(); /* 解析 g_mode(AUTO→明/暗)+ g_style + 背景覆写 */
|
||||
*out = *t;
|
||||
}
|
||||
|
||||
void meow_theme_inject(const meow_theme *src)
|
||||
{
|
||||
if (!g_loaded) { load_settings(); g_loaded = 1; }
|
||||
if (!src) { g_inject_set = 0; broadcast(); return; }
|
||||
g_inject = *src;
|
||||
g_inject_set = 1;
|
||||
broadcast(); /* 通知 App/状态栏/控件下一帧用新注入板重绘 */
|
||||
}
|
||||
|
||||
/* ---- §8.1.2 顶级主题风格族(minimal)----
|
||||
* meow_ui_style() 返回当前 ui.style(读取走 load_settings,随 meow_theme_reload() 重读,
|
||||
* 故 launcher 改 settings.conf 后经 sb_sdk_refresh→theme_reload 即跟手)。
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
/* meow_ui_time.c — SDK 单调时钟(meow_ui_time_ms)+ mipsel o32 时间 ABI 兜底。
|
||||
*
|
||||
* 根因(M11;真机日志 + 反汇编证据链见 sdk/CLOCK_ABI.md):
|
||||
* zig 0.17 的 clang 前端给 32 位 mips 目标预定义了 __ILP32__;zig 自带 musl 的
|
||||
* time64 回退分支统一写作 `#if defined(SYS_x_time64) && !defined(__ILP32__)`
|
||||
* (该 `!__ILP32__` 本意只排除 x32),于是 mipsel 的 libc 把「time64 入口」编译成了
|
||||
* 老 32 位实现:__clock_gettime64 直接做 SYS_clock_gettime(4263),只写 8 字节
|
||||
* `{ int32 tv_sec; int32 tv_nsec; }`。而 zig 头文件又无条件把 time_t 声明成 64 位
|
||||
* (struct timespec 16 字节、tv_sec 8 字节),两边 ABI 不一致:
|
||||
* tv_sec 低字 = 真 sec,高字 = 真 nsec → 64 位结果高 32 位随机、非单调;
|
||||
* tv_nsec(结构偏移 8)根本没被写 → 毫秒位恒 0(真机实测全是 1000 的倍数)。
|
||||
* 容器旧链(gcc)不定义 __ILP32__,time64 回退正确——对照 patch_v2/ncm_app/ncm:
|
||||
* 同一函数里先 SYS_clock_gettime64(4403)、ENOSYS 后走 4263 并做宽度转换。
|
||||
*
|
||||
* 修复:不依赖 libc 头声明的 struct timespec 宽度,直接按**内核 o32 ABI**
|
||||
* (8 字节 time32:int32 sec + int32 nsec)取时,再自行升成 64 位毫秒;
|
||||
* 同时在本 TU 给出 __clock_gettime / __clock_gettime64 的强定义。应用链接顺序是
|
||||
* `... meow_ui.a ... -lc`,归档先参与解析,因此链接期整体替换掉 libc 里那份
|
||||
* ABI 不一致的实现:time()、gettimeofday()、pthread 定时等待的内部取时
|
||||
* (musl 的 time()/__timedwait_cp 都调 __clock_gettime)随之修正。
|
||||
* 上层时间位宽仍是 64 位,只有内核取数按 o32 time32;2038 前有效。
|
||||
*
|
||||
* 同一句 `!__ILP32__` 还打穿了睡眠:__nanosleep_time64/__clock_nanosleep 走 `#else`
|
||||
* 把 16 字节 time64 结构直接交给 time32 系统调用,内核读到的 nsec 恒为 0 →
|
||||
* `usleep(16000)` 立即返回(SDK 里帧节拍/轮询会变忙等空转)。这里一并按 time32
|
||||
* 转换后重发,恢复亚秒睡眠。
|
||||
*
|
||||
* 残余(未修):futex 相对超时(pthread 定时等待的剩余时间按 8 字节读)、timerfd_settime、
|
||||
* clock_settime/adjtime、pselect/ppoll 超时——同为该守卫的受害者,见 CLOCK_ABI.md §6。
|
||||
*/
|
||||
#include <errno.h>
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "meow_ui.h"
|
||||
|
||||
#if defined(__mips__) && !defined(__mips64) && (__SIZEOF_LONG__ == 4)
|
||||
#define MEOW_TIME32_KERNEL_ABI 1
|
||||
#include <sys/syscall.h>
|
||||
#include <unistd.h>
|
||||
#else
|
||||
#define MEOW_TIME32_KERNEL_ABI 0
|
||||
#endif
|
||||
|
||||
uint64_t meow_time_ms_from_parts(int64_t sec, int64_t nsec)
|
||||
{
|
||||
return (uint64_t)sec * 1000u + (uint64_t)nsec / 1000000u;
|
||||
}
|
||||
|
||||
uint64_t meow_ui_time_ms(void)
|
||||
{
|
||||
#if MEOW_TIME32_KERNEL_ABI
|
||||
struct meow_time32 { int32_t tv_sec; int32_t tv_nsec; } t;
|
||||
t.tv_sec = 0;
|
||||
t.tv_nsec = 0;
|
||||
if (syscall(SYS_clock_gettime32, CLOCK_MONOTONIC, &t) != 0) return 0;
|
||||
return meow_time_ms_from_parts(t.tv_sec, t.tv_nsec);
|
||||
#else
|
||||
struct timespec ts;
|
||||
if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) return 0;
|
||||
return meow_time_ms_from_parts((int64_t)ts.tv_sec, (int64_t)ts.tv_nsec);
|
||||
#endif
|
||||
}
|
||||
|
||||
#if MEOW_TIME32_KERNEL_ABI
|
||||
/* 内核 o32 time32 布局(与 libc 头的 time_t 宽度无关)。 */
|
||||
struct meow_time32 {
|
||||
int32_t tv_sec;
|
||||
int32_t tv_nsec;
|
||||
};
|
||||
|
||||
static int32_t time32_clamp(int64_t v)
|
||||
{
|
||||
if (v > INT32_MAX) return INT32_MAX;
|
||||
if (v < 0) return 0;
|
||||
return (int32_t)v;
|
||||
}
|
||||
|
||||
/* ---- libc 取时入口的强定义 ----
|
||||
* 返回约定与 musl 一致:0 成功 / -1 失败(errno)。
|
||||
* 说明:<time.h> 在 _REDIR_TIME64 下把标识符 clock_gettime 的引用改名为
|
||||
* __clock_gettime64,故本 TU 不定义 clock_gettime 这个名字;musl 里它只是
|
||||
* __clock_gettime 的 weak alias,同一份语义已由下面的强定义接管。 */
|
||||
int __clock_gettime(clockid_t clk, struct timespec *ts);
|
||||
int __clock_gettime64(clockid_t clk, struct timespec *ts);
|
||||
|
||||
int __clock_gettime(clockid_t clk, struct timespec *ts)
|
||||
{
|
||||
struct meow_time32 t;
|
||||
if (!ts) { errno = EFAULT; return -1; }
|
||||
t.tv_sec = 0;
|
||||
t.tv_nsec = 0;
|
||||
if (syscall(SYS_clock_gettime32, clk, &t) != 0) return -1;
|
||||
ts->tv_sec = t.tv_sec;
|
||||
ts->tv_nsec = t.tv_nsec;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __clock_gettime64(clockid_t clk, struct timespec *ts)
|
||||
{
|
||||
return __clock_gettime(clk, ts);
|
||||
}
|
||||
|
||||
/* ---- 睡眠入口(同源缺陷:亚秒 usleep 变空转)----
|
||||
* __clock_nanosleep 按 musl 内部约定返回 0 或正 errno;__nanosleep_time64 是
|
||||
* nanosleep/usleep 的重定向目标,按公共约定返回 0/-1(errno)。 */
|
||||
int __clock_nanosleep(clockid_t clk, int flags, const struct timespec *req, struct timespec *rem);
|
||||
int __clock_nanosleep_time64(clockid_t clk, int flags, const struct timespec *req, struct timespec *rem);
|
||||
int __nanosleep_time64(const struct timespec *req, struct timespec *rem);
|
||||
|
||||
int __clock_nanosleep(clockid_t clk, int flags, const struct timespec *req, struct timespec *rem)
|
||||
{
|
||||
struct meow_time32 rq, rm;
|
||||
long r;
|
||||
|
||||
if (clk == CLOCK_THREAD_CPUTIME_ID) return EINVAL;
|
||||
if (!req) return EFAULT;
|
||||
if (req->tv_nsec < 0 || req->tv_nsec >= 1000000000L) return EINVAL;
|
||||
rq.tv_sec = time32_clamp(req->tv_sec);
|
||||
rq.tv_nsec = (int32_t)req->tv_nsec;
|
||||
rm.tv_sec = 0;
|
||||
rm.tv_nsec = 0;
|
||||
|
||||
if (clk == CLOCK_REALTIME && !flags)
|
||||
r = syscall(SYS_nanosleep, &rq, rem ? &rm : 0);
|
||||
else
|
||||
r = syscall(SYS_clock_nanosleep_time32, clk, flags, &rq, rem ? &rm : 0);
|
||||
if (r != 0) return errno;
|
||||
if (rem) {
|
||||
rem->tv_sec = rm.tv_sec;
|
||||
rem->tv_nsec = rm.tv_nsec;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __clock_nanosleep_time64(clockid_t clk, int flags, const struct timespec *req, struct timespec *rem)
|
||||
{
|
||||
return __clock_nanosleep(clk, flags, req, rem);
|
||||
}
|
||||
|
||||
int __nanosleep_time64(const struct timespec *req, struct timespec *rem)
|
||||
{
|
||||
int r = __clock_nanosleep(CLOCK_REALTIME, 0, req, rem);
|
||||
if (r) { errno = r; return -1; }
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
+58
-2
@@ -9,6 +9,7 @@
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
#include "meow_ui_view.h"
|
||||
#include "include/meow_ui_box.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
@@ -17,6 +18,50 @@
|
||||
static meow_view *g_view_by_id[MEOW_VIEW_BY_ID_MAX]; /* id → 顶层页 view(page_push/pop 查找) */
|
||||
static meow_view *g_top_view = NULL; /* 当前进入的顶层页 view(生命周期跟踪) */
|
||||
|
||||
/* ---- 布局提供者注册表(view → meow_box,opt-in) ---- */
|
||||
static struct { meow_view *v; meow_box *box; } g_view_layout[MEOW_VIEW_BY_ID_MAX];
|
||||
static int g_view_layout_n = 0;
|
||||
|
||||
static void layout_reg_set(meow_view *v, meow_box *box)
|
||||
{
|
||||
for (int i = 0; i < g_view_layout_n; i++) {
|
||||
if (g_view_layout[i].v == v) {
|
||||
g_view_layout[i].box = box;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (g_view_layout_n < MEOW_VIEW_BY_ID_MAX) {
|
||||
g_view_layout[g_view_layout_n].v = v;
|
||||
g_view_layout[g_view_layout_n].box = box;
|
||||
g_view_layout_n++;
|
||||
}
|
||||
}
|
||||
static meow_box *layout_reg_get(const meow_view *v)
|
||||
{
|
||||
for (int i = 0; i < g_view_layout_n; i++)
|
||||
if (g_view_layout[i].v == v) return g_view_layout[i].box;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* 重排:把 view 的布局 box 解算一次(box 矩形未设时自动跟 view 内容矩形)。 */
|
||||
void meow_view_relayout(meow_view *v)
|
||||
{
|
||||
if (!v) return;
|
||||
meow_box *b = layout_reg_get(v);
|
||||
if (!b) return;
|
||||
int r[4]; meow_box_rect(b, r);
|
||||
if (r[2] <= 0 || r[3] <= 0) /* w/h 未设 → 跟 view 内容矩形 */
|
||||
meow_box_set_rect(b, v->x, v->y, v->w, v->h);
|
||||
meow_box_arrange(b);
|
||||
}
|
||||
void meow_view_set_layout(meow_view *v, struct meow_box *box)
|
||||
{
|
||||
if (!v) return;
|
||||
if (!box) { layout_reg_set(v, NULL); return; }
|
||||
layout_reg_set(v, box);
|
||||
meow_view_relayout(v); /* 立即按当前尺寸排一次 */
|
||||
}
|
||||
|
||||
static void run_fn(meow_view_fn f, meow_view *v)
|
||||
{
|
||||
if (f) f(v, v->ctx);
|
||||
@@ -111,6 +156,10 @@ int meow_view_add_child(meow_view *parent, meow_view *child)
|
||||
if (parent->nchildren >= MEOW_VIEW_MAX_CHILD) return -1;
|
||||
parent->children[parent->nchildren++] = child;
|
||||
child->parent = parent;
|
||||
/* 递归修正(§A.4「子视图随父进出」):父已进入时挂上的子视图**立即**进入
|
||||
* (on_enter 不遗漏、参与事件分发)。页在 push/pop 时仍由 view_enter/view_leave 递归驱动,
|
||||
* 此处只补「运行期动态挂子」的间隙(hello 目录嵌套实为 create 后 page_push 前 add,不受影响)。 */
|
||||
if (parent->state & MEOW_VIEW_STATE_ENTERED) view_enter(child);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -146,6 +195,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 +206,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,14 +218,19 @@ 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;
|
||||
}
|
||||
|
||||
/* ---- 页内子导航(预留扩展位 a 桩:本批不实现,仅返回占位)---- */
|
||||
/* ---- 页内子导航(预留扩展位 a,**标注废弃**)----
|
||||
* 设计定案 b(SDK_VIEW_FRAMEWORK_DESIGN.md §F.2:只做子视图容器嵌套,顶层页切仍走 activity
|
||||
* 栈,不做页内子导航栈):本函数对为「将来 a」预留的桩,**不实现、不供依赖**——调用方不得
|
||||
* 认为会建立/弹出子导航。页内「子页」请用 meow_view_add_child 挂 CONTAINER 子视图(组合嵌套),
|
||||
* 或以 activity 栈驱动页切换(page_push/pop/replace)。保留符号仅为未来 a 的占位。 */
|
||||
int meow_view_sub_push(meow_view *v, meow_view *sub)
|
||||
{
|
||||
(void)v; (void)sub;
|
||||
return 0; /* 预留:将来在 ACTIVITY view 内维护子栈 */
|
||||
return 0; /* 预留:将来在 ACTIVITY view 内维护子栈(见设计定案 b,不实现) */
|
||||
}
|
||||
meow_view *meow_view_sub_pop(meow_view *v)
|
||||
{
|
||||
|
||||
+17
-4
@@ -82,12 +82,17 @@ void meow_view_page_replace(int id); /* 旧top on_leave + 新页 on_en
|
||||
|
||||
/* 入口:把语义事件注入当前活动页 view 子树路由——先发子视图(按 z 降序),命中即消费,
|
||||
* 再落本 view 的 on_event(其内做该 view 的 widget 命中)。返回 1=已消费(§A.7)。
|
||||
* 未接线于 meow_ui_run(App 可在 on_tap/on_swipe 等回调里改用它实现 SDK 原生路由)。 */
|
||||
* 已接线于 meow_ui_run(M7.0a):在 §6.4 分流之上,TAP/KEY/SWIPE/LONG_PRESS 先经本路由,
|
||||
* 命中即消费(不再落 App 的 on_* 回调);返回 0 时回落 on_* 原路径(无 view 树 / 未命中 →
|
||||
* 完全向后兼容)。App 也可在自持事件循环里直接调用本函数实现 SDK 原生路由。 */
|
||||
int meow_view_dispatch_event(int ev, int a, int b);
|
||||
|
||||
/* ---- 页内子导航(预留扩展位 a;本批为空实现/桩,仅在 ACTIVITY view 内维护子栈)---- */
|
||||
int meow_view_sub_push(meow_view *v, meow_view *sub); /* 预留;返回 0(未实现于本批) */
|
||||
meow_view *meow_view_sub_pop(meow_view *v); /* 预留;返回 NULL(未实现于本批) */
|
||||
/* ---- 页内子导航(预留扩展位 a,**标注废弃**)----
|
||||
* 设计定案 b(§F.2):不做页内子导航栈,只做子视图容器嵌套;顶层页切仍走 activity 栈。
|
||||
* 故本组为**不实现、不供依赖**的桩(返回 0/NULL);页内子页请用 meow_view_add_child 挂
|
||||
* CONTAINER 子视图,或以 page_push/pop/replace 走 activity 栈。保留符号仅作将来 a 的占位。 */
|
||||
int meow_view_sub_push(meow_view *v, meow_view *sub); /* 废弃(不实现);返回 0 */
|
||||
meow_view *meow_view_sub_pop(meow_view *v); /* 废弃(不实现);返回 NULL */
|
||||
|
||||
/* ---- 坐标:view 相对父内容原点的累计(父坐标链求和 → 屏幕绝对坐标)---- */
|
||||
int meow_view_screen_x(const meow_view *v);
|
||||
@@ -97,6 +102,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
|
||||
|
||||
+795
-109
File diff suppressed because it is too large.
Load diff
+557
-6
@@ -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 */
|
||||
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 */
|
||||
+80
-12
@@ -429,6 +429,14 @@ STBIDEF stbi_uc *stbi_load_from_file (FILE *f, int *x, int *y, int *channels_in
|
||||
// for stbi_load_from_file, file pointer is left pointing immediately after image
|
||||
#endif
|
||||
|
||||
// 追加(MeowOS SDK M12):JPEG 解码期降采样。scale>=2 时按 s 抽取输出行/列,
|
||||
// 不物化整幅原尺寸输出缓冲(*x,*y 返回 ceil(原尺寸/s))。scale<=1 与
|
||||
// stbi_load / stbi_load_from_memory 完全等价;非 JPEG 格式忽略 scale。
|
||||
STBIDEF stbi_uc *stbi_load_scaled_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *channels_in_file, int desired_channels, int scale);
|
||||
#ifndef STBI_NO_STDIO
|
||||
STBIDEF stbi_uc *stbi_load_scaled (char const *filename, int *x, int *y, int *channels_in_file, int desired_channels, int scale);
|
||||
#endif
|
||||
|
||||
#ifndef STBI_NO_GIF
|
||||
STBIDEF stbi_uc *stbi_load_gif_from_memory(stbi_uc const *buffer, int len, int **delays, int *x, int *y, int *z, int *comp, int req_comp);
|
||||
#endif
|
||||
@@ -907,7 +915,7 @@ typedef struct
|
||||
|
||||
#ifndef STBI_NO_JPEG
|
||||
static int stbi__jpeg_test(stbi__context *s);
|
||||
static void *stbi__jpeg_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri);
|
||||
static void *stbi__jpeg_load_ex(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri, int scale);
|
||||
static int stbi__jpeg_info(stbi__context *s, int *x, int *y, int *comp);
|
||||
#endif
|
||||
|
||||
@@ -1134,7 +1142,7 @@ STBIDEF void stbi_set_flip_vertically_on_load_thread(int flag_true_if_should_fli
|
||||
: stbi__vertically_flip_on_load_global)
|
||||
#endif // STBI_THREAD_LOCAL
|
||||
|
||||
static void *stbi__load_main(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri, int bpc)
|
||||
static void *stbi__load_main(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri, int bpc, int jpeg_scale)
|
||||
{
|
||||
memset(ri, 0, sizeof(*ri)); // make sure it's initialized if we add new fields
|
||||
ri->bits_per_channel = 8; // default is 8 so most paths don't have to be changed
|
||||
@@ -1165,7 +1173,9 @@ static void *stbi__load_main(stbi__context *s, int *x, int *y, int *comp, int re
|
||||
// bytes matching expectations; these are prone to false positives, so
|
||||
// try them later
|
||||
#ifndef STBI_NO_JPEG
|
||||
if (stbi__jpeg_test(s)) return stbi__jpeg_load(s,x,y,comp,req_comp, ri);
|
||||
if (stbi__jpeg_test(s)) return stbi__jpeg_load_ex(s,x,y,comp,req_comp, ri, jpeg_scale);
|
||||
#else
|
||||
STBI_NOTUSED(jpeg_scale);
|
||||
#endif
|
||||
#ifndef STBI_NO_PNM
|
||||
if (stbi__pnm_test(s)) return stbi__pnm_load(s,x,y,comp,req_comp, ri);
|
||||
@@ -1257,10 +1267,10 @@ static void stbi__vertical_flip_slices(void *image, int w, int h, int z, int byt
|
||||
}
|
||||
#endif
|
||||
|
||||
static unsigned char *stbi__load_and_postprocess_8bit(stbi__context *s, int *x, int *y, int *comp, int req_comp)
|
||||
static unsigned char *stbi__load_and_postprocess_8bit_ex(stbi__context *s, int *x, int *y, int *comp, int req_comp, int jpeg_scale)
|
||||
{
|
||||
stbi__result_info ri;
|
||||
void *result = stbi__load_main(s, x, y, comp, req_comp, &ri, 8);
|
||||
void *result = stbi__load_main(s, x, y, comp, req_comp, &ri, 8, jpeg_scale);
|
||||
|
||||
if (result == NULL)
|
||||
return NULL;
|
||||
@@ -1283,10 +1293,15 @@ static unsigned char *stbi__load_and_postprocess_8bit(stbi__context *s, int *x,
|
||||
return (unsigned char *) result;
|
||||
}
|
||||
|
||||
static unsigned char *stbi__load_and_postprocess_8bit(stbi__context *s, int *x, int *y, int *comp, int req_comp)
|
||||
{
|
||||
return stbi__load_and_postprocess_8bit_ex(s, x, y, comp, req_comp, 1);
|
||||
}
|
||||
|
||||
static stbi__uint16 *stbi__load_and_postprocess_16bit(stbi__context *s, int *x, int *y, int *comp, int req_comp)
|
||||
{
|
||||
stbi__result_info ri;
|
||||
void *result = stbi__load_main(s, x, y, comp, req_comp, &ri, 16);
|
||||
void *result = stbi__load_main(s, x, y, comp, req_comp, &ri, 16, 1);
|
||||
|
||||
if (result == NULL)
|
||||
return NULL;
|
||||
@@ -1409,6 +1424,19 @@ STBIDEF stbi_us *stbi_load_16(char const *filename, int *x, int *y, int *comp, i
|
||||
return result;
|
||||
}
|
||||
|
||||
// 追加(M12):JPEG 解码期降采样(见头文件声明)。非 JPEG 等价于 stbi_load。
|
||||
STBIDEF stbi_uc *stbi_load_scaled(char const *filename, int *x, int *y, int *comp, int req_comp, int scale)
|
||||
{
|
||||
FILE *f = stbi__fopen(filename, "rb");
|
||||
stbi_uc *result;
|
||||
stbi__context s;
|
||||
if (!f) return stbi__errpuc("can't fopen", "Unable to open file");
|
||||
stbi__start_file(&s,f);
|
||||
result = stbi__load_and_postprocess_8bit_ex(&s,x,y,comp,req_comp, scale);
|
||||
if (result) fseek(f, - (int) (s.img_buffer_end - s.img_buffer), SEEK_CUR);
|
||||
fclose(f);
|
||||
return result;
|
||||
}
|
||||
|
||||
#endif //!STBI_NO_STDIO
|
||||
|
||||
@@ -1433,6 +1461,14 @@ STBIDEF stbi_uc *stbi_load_from_memory(stbi_uc const *buffer, int len, int *x, i
|
||||
return stbi__load_and_postprocess_8bit(&s,x,y,comp,req_comp);
|
||||
}
|
||||
|
||||
// 追加(M12):JPEG 解码期降采样(见头文件声明)。非 JPEG 等价于 stbi_load_from_memory。
|
||||
STBIDEF stbi_uc *stbi_load_scaled_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp, int req_comp, int scale)
|
||||
{
|
||||
stbi__context s;
|
||||
stbi__start_mem(&s,buffer,len);
|
||||
return stbi__load_and_postprocess_8bit_ex(&s,x,y,comp,req_comp, scale);
|
||||
}
|
||||
|
||||
STBIDEF stbi_uc *stbi_load_from_callbacks(stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *comp, int req_comp)
|
||||
{
|
||||
stbi__context s;
|
||||
@@ -1982,6 +2018,7 @@ typedef struct
|
||||
int nomore; // flag if we saw a marker so must stop
|
||||
|
||||
int progressive;
|
||||
int scale; // 追加(M12):输出抽取步长,默认 1=原尺寸
|
||||
int spec_start;
|
||||
int spec_end;
|
||||
int succ_high;
|
||||
@@ -3092,6 +3129,12 @@ static void stbi__jpeg_finish(stbi__jpeg *z)
|
||||
z->idct_block_kernel(z->img_comp[n].data+z->img_comp[n].w2*j*8+i*8, z->img_comp[n].w2, data);
|
||||
}
|
||||
}
|
||||
// 追加(M12):该分量系数已全部 IDCT 进像素平面,立即释放,降低进升级 JPEG 的峰值
|
||||
if (z->img_comp[n].raw_coeff) {
|
||||
STBI_FREE(z->img_comp[n].raw_coeff);
|
||||
z->img_comp[n].raw_coeff = 0;
|
||||
z->img_comp[n].coeff = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3893,6 +3936,10 @@ static stbi_uc *load_jpeg_image(stbi__jpeg *z, int *out_x, int *out_y, int *comp
|
||||
unsigned int i,j;
|
||||
stbi_uc *output;
|
||||
stbi_uc *coutput[4] = { NULL, NULL, NULL, NULL };
|
||||
stbi_uc *rowbuf = NULL; // 追加(M12):整行临时缓冲,仅 dec>1 时使用
|
||||
int dec = z->scale > 1 ? z->scale : 1;
|
||||
int out_w = (z->s->img_x + dec - 1) / dec;
|
||||
int out_h = (z->s->img_y + dec - 1) / dec;
|
||||
|
||||
stbi__resample res_comp[4];
|
||||
|
||||
@@ -3918,13 +3965,19 @@ static stbi_uc *load_jpeg_image(stbi__jpeg *z, int *out_x, int *out_y, int *comp
|
||||
else r->resample = stbi__resample_row_generic;
|
||||
}
|
||||
|
||||
// 追加(M12):dec>1 时整行先转成像素再按步长抽取,不物化整幅原尺寸输出缓冲
|
||||
if (dec > 1) {
|
||||
rowbuf = (stbi_uc *) stbi__malloc_mad2(n, z->s->img_x, 0);
|
||||
if (!rowbuf) { stbi__cleanup_jpeg(z); return stbi__errpuc("outofmem", "Out of memory"); }
|
||||
}
|
||||
|
||||
// can't error after this so, this is safe
|
||||
output = (stbi_uc *) stbi__malloc_mad3(n, z->s->img_x, z->s->img_y, 1);
|
||||
if (!output) { stbi__cleanup_jpeg(z); return stbi__errpuc("outofmem", "Out of memory"); }
|
||||
output = (stbi_uc *) stbi__malloc_mad3(n, out_w, out_h, 1);
|
||||
if (!output) { STBI_FREE(rowbuf); stbi__cleanup_jpeg(z); return stbi__errpuc("outofmem", "Out of memory"); }
|
||||
|
||||
// now go ahead and resample
|
||||
for (j=0; j < z->s->img_y; ++j) {
|
||||
stbi_uc *out = output + n * z->s->img_x * j;
|
||||
stbi_uc *out;
|
||||
for (k=0; k < decode_n; ++k) {
|
||||
stbi__resample *r = &res_comp[k];
|
||||
int y_bot = r->ystep >= (r->vs >> 1);
|
||||
@@ -3939,6 +3992,9 @@ static stbi_uc *load_jpeg_image(stbi__jpeg *z, int *out_x, int *out_y, int *comp
|
||||
r->line1 += z->img_comp[k].w2;
|
||||
}
|
||||
}
|
||||
// 追加(M12):resample 状态机须逐行推进(上面已完成);非采样行直接丢弃
|
||||
if (dec > 1 && (j % (unsigned int) dec) != 0) continue;
|
||||
out = (dec > 1) ? rowbuf : output + (size_t) n * out_w * j;
|
||||
if (n >= 3) {
|
||||
stbi_uc *y = coutput[0];
|
||||
if (z->s->img_n == 3) {
|
||||
@@ -4016,16 +4072,27 @@ static stbi_uc *load_jpeg_image(stbi__jpeg *z, int *out_x, int *out_y, int *comp
|
||||
for (i=0; i < z->s->img_x; ++i) { *out++ = y[i]; *out++ = 255; }
|
||||
}
|
||||
}
|
||||
// 追加(M12):rowbuf 已按整行转好像素,按步长抽取写入输出行
|
||||
if (dec > 1) {
|
||||
stbi_uc *dst = output + (size_t) n * out_w * (j / (unsigned int) dec);
|
||||
unsigned int x;
|
||||
for (x=0; x < (unsigned int) out_w; ++x) {
|
||||
int c;
|
||||
for (c=0; c < n; ++c)
|
||||
dst[(size_t) x * n + c] = rowbuf[(size_t) x * dec * n + c];
|
||||
}
|
||||
}
|
||||
}
|
||||
STBI_FREE(rowbuf);
|
||||
stbi__cleanup_jpeg(z);
|
||||
*out_x = z->s->img_x;
|
||||
*out_y = z->s->img_y;
|
||||
*out_x = out_w;
|
||||
*out_y = out_h;
|
||||
if (comp) *comp = z->s->img_n >= 3 ? 3 : 1; // report original components, not output
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
static void *stbi__jpeg_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri)
|
||||
static void *stbi__jpeg_load_ex(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri, int scale)
|
||||
{
|
||||
unsigned char* result;
|
||||
stbi__jpeg* j = (stbi__jpeg*) stbi__malloc(sizeof(stbi__jpeg));
|
||||
@@ -4033,6 +4100,7 @@ static void *stbi__jpeg_load(stbi__context *s, int *x, int *y, int *comp, int re
|
||||
memset(j, 0, sizeof(stbi__jpeg));
|
||||
STBI_NOTUSED(ri);
|
||||
j->s = s;
|
||||
j->scale = scale; // 追加(M12):输出抽取步长
|
||||
stbi__setup_jpeg(j);
|
||||
result = load_jpeg_image(j, x,y,comp,req_comp);
|
||||
STBI_FREE(j);
|
||||
|
||||
+4
-3
@@ -7,9 +7,10 @@
|
||||
* 编译(宿主 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_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 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_media.c \
|
||||
* meow_ui_img.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* (meow_ui_widget.c 新 W_LISTVIEW 引用 meow_ui_img_scaled → 需链 meow_ui_img.c+stb_image.c+-lm)
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
/* test_meow_cachegov.c — 宿主测试:SDK meow_cachegov(A:DONTNEED L0/L1 + refault 回退)。
|
||||
*
|
||||
* 背景(OOM_PAGECACHE_GOVERNANCE §2/§4,③混合):A 的 DONTNEED/read_pos 热路径随 SDK 库;
|
||||
* 本测试用宿主桩:
|
||||
* - 真实 posix_fadvise(文件数据完整性校验:drop 后文件内容不变、drop_finished 可再读);
|
||||
* - meow_log 桩捕获 "[cachegov] L1 drop off=.. len=.." 断言丢弃范围([0, read_pos-keep_hot));
|
||||
* - meow_cachegov_test_set_time_ms() 注入时钟驱动 refault 滚窗回退(放大 keep_hot→降L0→关A);
|
||||
* - MEOW_CACHEGOV_C_FILE 指向临时文件写入 fake C 状态,断言 download_paused/prefetch_paused。
|
||||
*
|
||||
* 编译(patch_v2/sdk 目录下):
|
||||
* gcc -Os -Wall -Wextra -I. -Iinclude -c meow_cachegov.c -o /tmp/cg.o
|
||||
* gcc -Os -Wall -Wextra -I. -Iinclude test_meow_cachegov.c /tmp/cg.o -lpthread -o test_meow_cachegov
|
||||
* 运行:./test_meow_cachegov (exit 0=PASS / 1=FAIL)
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include "meow_cachegov.h"
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
/* ---- meow_log 桩:捕获最近一条 [cachegov] 行,供断言 ---- */
|
||||
static char g_last_log[512];
|
||||
void meow_log(int lvl, const char *fmt, ...)
|
||||
{
|
||||
(void)lvl;
|
||||
va_list ap; va_start(ap, fmt);
|
||||
vsnprintf(g_last_log, sizeof g_last_log, fmt, ap);
|
||||
va_end(ap);
|
||||
printf(" [cachegov.log] %s\n", g_last_log);
|
||||
}
|
||||
static int log_has(const char *needle) { return strstr(g_last_log, needle) != NULL; }
|
||||
|
||||
static int g_pass = 0, g_fail = 0;
|
||||
static void check(const char *name, int cond)
|
||||
{
|
||||
if (cond) { g_pass++; printf("PASS %s\n", name); }
|
||||
else { g_fail++; printf("FAIL %s\n", name); }
|
||||
}
|
||||
|
||||
/* ---- read_pos 桩:由测试驱动当前读指针 ---- */
|
||||
static uint64_t g_pos;
|
||||
static uint64_t fake_read_pos(void *ctx) { (void)ctx; return g_pos; }
|
||||
|
||||
/* 建一个 size 字节的临时文件并填充确定性内容。 */
|
||||
static int make_file(const char *path, size_t size)
|
||||
{
|
||||
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||
if (fd < 0) return -1;
|
||||
for (size_t off = 0; off < size; off += 4096) {
|
||||
char tmp[4096];
|
||||
memset(tmp, (int)(off / 4096) & 0xFF, sizeof tmp);
|
||||
size_t n = (size - off) < sizeof tmp ? (size - off) : sizeof tmp;
|
||||
if (write(fd, tmp, n) != (ssize_t)n) { close(fd); return -1; }
|
||||
}
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 校验文件内容仍为确定性模式(drop 后数据完整)。 */
|
||||
static int verify_file(const char *path, size_t size)
|
||||
{
|
||||
int fd = open(path, O_RDONLY);
|
||||
if (fd < 0) return 0;
|
||||
char tmp[4096];
|
||||
size_t off = 0;
|
||||
int ok = 1;
|
||||
while (off < size) {
|
||||
size_t n = (size - off) < sizeof tmp ? (size - off) : sizeof tmp;
|
||||
ssize_t r = read(fd, tmp, n);
|
||||
if (r != (ssize_t)n) { ok = 0; break; }
|
||||
char expect[4096];
|
||||
memset(expect, (int)(off / 4096) & 0xFF, n);
|
||||
if (memcmp(tmp, expect, n) != 0) { ok = 0; break; }
|
||||
off += (size_t)n;
|
||||
}
|
||||
close(fd);
|
||||
return ok;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
char path[320], cfile[360];
|
||||
snprintf(path, sizeof path, "/tmp/meow_cg_%ld.bin", (long)getpid());
|
||||
snprintf(cfile, sizeof cfile, "/tmp/meow_cg_%ld.state", (long)getpid());
|
||||
setenv("MEOW_CACHEGOV_C_FILE", cfile, 1);
|
||||
unlink(path); unlink(cfile);
|
||||
|
||||
const size_t size = 1024u * 1024u; /* 1MB 测试文件 */
|
||||
check("make_file", make_file(path, size) == 0);
|
||||
|
||||
/* 1) cfg:默认 + set/get 往返 */
|
||||
struct meow_cachegov_cfg c = {0};
|
||||
meow_cachegov_get_cfg(&c);
|
||||
check("cfg default enabled", c.enabled == 1);
|
||||
check("cfg default keep_hot=256KB", c.keep_hot_bytes == 256u * 1024u);
|
||||
struct meow_cachegov_cfg c2 = c;
|
||||
c2.keep_hot_bytes = 128u * 1024u;
|
||||
c2.drop_mode = 1;
|
||||
c2.refault_backoff = 2;
|
||||
check("set_cfg", meow_cachegov_set_cfg(&c2) == 0);
|
||||
meow_cachegov_get_cfg(&c);
|
||||
check("get_cfg roundtrip", c.keep_hot_bytes == 128u * 1024u && c.drop_mode == 1 && c.refault_backoff == 2);
|
||||
|
||||
/* 2) L1 drop:read_pos=512KB、keep_hot=128KB → drop [0, 384KB) off=0 len=384KB */
|
||||
check("register", meow_cachegov_register(path, size, fake_read_pos, NULL) == 0);
|
||||
g_pos = 512u * 1024u;
|
||||
meow_cachegov_tick();
|
||||
check("L1 drop off=0 len=384KB", log_has("L1 drop") && strstr(g_last_log, "off=0 len=393216"));
|
||||
check("L1 range bounded by read_pos-keep_hot (no L2)",
|
||||
!strstr(g_last_log, "off=393216") && strstr(g_last_log, "len=393216"));
|
||||
|
||||
/* 3) L1 推进:read_pos=800KB → drop [384KB, 800-128KB=672KB) off=384KB len=288KB */
|
||||
g_pos = 800u * 1024u;
|
||||
meow_cachegov_tick();
|
||||
check("L1 drop off=384KB len=288KB", log_has("L1 drop") && strstr(g_last_log, "off=393216") && strstr(g_last_log, "len=294912"));
|
||||
|
||||
/* 4) refault 回退档1(放大 keep_hot 翻倍):3 次触发 2 次 → 0→1(每升降一档需间隔 ≥10s */
|
||||
meow_cachegov_test_set_time_ms(0);
|
||||
meow_cachegov_note_refault(); meow_cachegov_note_refault();
|
||||
meow_cachegov_tick();
|
||||
check("backoff -> 1 (refault*2)", meow_cachegov_backoff_level() == 1);
|
||||
check("file intact after backoff1", verify_file(path, size));
|
||||
/* 档1 且 keep_hot=128KB*2=256KB:read_pos=1000KB → drop 上界=744KB(已丢到 672KB) */
|
||||
g_pos = 1000u * 1024u;
|
||||
meow_cachegov_tick();
|
||||
check("backoff1 L1 keep_hot enlarged (no L2)", meow_cachegov_backoff_level() == 1);
|
||||
|
||||
/* 5) 再 2 次(间隔 10s)→ 档2(L1 关,只 L0)→ download_paused=1 */
|
||||
meow_cachegov_test_set_time_ms(11000);
|
||||
meow_cachegov_note_refault(); meow_cachegov_note_refault();
|
||||
meow_cachegov_tick();
|
||||
check("backoff -> 2 (L1 off)", meow_cachegov_backoff_level() == 2);
|
||||
g_pos = 900u * 1024u;
|
||||
meow_cachegov_tick(); /* L1 off:应无新的 L1 drop 日志 */
|
||||
check("level2 no L1 drop (only L0)", !log_has("L1 drop"));
|
||||
check("download_paused @backoff>=2", meow_cachegov_download_paused() == 1);
|
||||
|
||||
/* 6) 再 2 次(间隔 10s)→ 档3(A 全关)→ prefetch_paused=1 + drop_finished 不再丢页 */
|
||||
meow_cachegov_test_set_time_ms(22000);
|
||||
meow_cachegov_note_refault(); meow_cachegov_note_refault();
|
||||
meow_cachegov_tick();
|
||||
check("backoff -> 3 (A off)", meow_cachegov_backoff_level() == 3);
|
||||
check("prefetch_paused @backoff>=3", meow_cachegov_prefetch_paused() == 1);
|
||||
meow_cachegov_test_set_time_ms(23000);
|
||||
meow_cachegov_drop_finished(path);
|
||||
check("drop_finished skip @A-off", log_has("drop_finished skip"));
|
||||
check("file intact after A-off", verify_file(path, size));
|
||||
|
||||
/* 7) C 侧降载:write fake C state dl_pause=1;滚窗干净逐级降回 0 后 download_paused 仍 =1 */
|
||||
{
|
||||
FILE *f = fopen(cfile, "w");
|
||||
check("write C state", f != NULL);
|
||||
if (f) { fputs("cached_kb=35000\ndirty_kb=1000\ndl_pause=1\nprefetch_pause=0\n", f); fclose(f); }
|
||||
}
|
||||
meow_cachegov_test_set_time_ms(33000);
|
||||
meow_cachegov_tick(); /* 3→2 */
|
||||
meow_cachegov_test_set_time_ms(44000);
|
||||
meow_cachegov_tick(); /* 2→1 */
|
||||
meow_cachegov_test_set_time_ms(55000);
|
||||
meow_cachegov_tick(); /* 1→0 */
|
||||
check("backoff de-escalated to 0", meow_cachegov_backoff_level() == 0);
|
||||
check("download_paused via C file", meow_cachegov_download_paused() == 1);
|
||||
|
||||
/* 8) L0 drop_finished(A 正常态):整文件 DONTNEED + 数据完整 + 注销条目 */
|
||||
unlink(cfile);
|
||||
meow_cachegov_register(path, size, fake_read_pos, NULL);
|
||||
meow_cachegov_test_set_time_ms(66000);
|
||||
meow_cachegov_drop_finished(path);
|
||||
check("drop_finished L0 log", log_has("drop_finished L0"));
|
||||
check("file intact after L0", verify_file(path, size));
|
||||
check("unregister removes entry", meow_cachegov_unregister(path) != 0); /* 已注销 → 找不到 */
|
||||
|
||||
/* 9) disable 时 drop/tick 均 no-op:数据仍完整 */
|
||||
c2.enabled = 0; meow_cachegov_set_cfg(&c2);
|
||||
meow_cachegov_register(path, size, fake_read_pos, NULL);
|
||||
g_pos = 512u * 1024u;
|
||||
meow_cachegov_tick();
|
||||
meow_cachegov_drop_finished(path);
|
||||
check("file intact when disabled", verify_file(path, size));
|
||||
|
||||
unlink(path); unlink(cfile);
|
||||
printf("----\n%d passed, %d failed\n", g_pass, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,466 @@
|
||||
/* test_meow_img_budget.c — M12 预算解码(meow_img_load_file_dim / _mem_dim)宿主验证。
|
||||
*
|
||||
* 覆盖矩阵(对齐 M12 任务 3/4):
|
||||
* B1 探针:meow_img_probe_file 只读头取原始尺寸(8.32MP 图),坏图/不存在返回 0
|
||||
* B2 小图不动:64x64 走旧路径,与 meow_img_load_file 逐字节相同;maxdim<=0 等价旧入口
|
||||
* B3 长边约束:8.32MP baseline JPEG 在 maxdim=480/300/128/64 下结果两维都 ≤ maxdim,长宽比保持
|
||||
* B4 峰值对比:同一 8.32MP JPEG「旧入口 vs 新入口」瞬时分配峰值(--wrap malloc 打点),
|
||||
* 断言新入口峰值 < 旧入口 1/2;B4b 极端 maxdim=100;B4c finding 原始场景 1600x2400 页
|
||||
* B5 内容正确:新入口结果与「旧入口整幅解码 + meow_img_downscale」网格采样比对(平均差阈值)
|
||||
* B6 进升级:progressive JPEG 走同一缩解路径(峰值记录 + 长边约束)
|
||||
* B7 内存入口:meow_img_load_mem_dim 与文件入口等价(峰值 < 旧内存入口 1/2)
|
||||
* B8 坏图:坏 JPEG(魔数+垃圾)/ 空内存 / 不存在路径 → NULL(不崩)
|
||||
* B9 PNG/BMP:尺寸约束成立;M23 起 PNG/BMP 也走解码期缩解(逐行 inflate / 按行 seek),
|
||||
* 峰值应显著低于旧入口(本用例只断言 ≤ maxdim + 非空,并打印峰值供报告引用)
|
||||
*
|
||||
* 夹具(生成一次):
|
||||
* python3 test_meow_img_budget_gen.py "$PREFIX/tmp/meow_img_budget"
|
||||
* 编译(本 sdk 目录下;--wrap 用于给 malloc/free 家族打点;-lz 供 PNG 后端 inflate):
|
||||
* gcc -Wall -Wextra -I. -Iinclude -DMEOW_UI_HOST_TEST -o test_meow_img_budget \
|
||||
* test_meow_img_budget.c meow_ui_img.c meow_jpeg.c meow_png_stream.c stb_image.c \
|
||||
* -lm -lz \
|
||||
* -Wl,--wrap=malloc -Wl,--wrap=calloc -Wl,--wrap=realloc -Wl,--wrap=free
|
||||
* 运行:./test_meow_img_budget "$PREFIX/tmp/meow_img_budget" (exit 0=PASS / 1=FAIL)
|
||||
* 夹具目录可省略:默认 $PREFIX/tmp/meow_img_budget;缺失时自动调 gen 脚本生成。
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h" /* meow_gfx_blit_rgba 原型(本测试只给桩) */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 分配打点:--wrap malloc/free 家族,记录「存活字节」与峰值 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
#define AHDR 32 /* 头部长度:保留原始大小,保证 16 字节对齐 */
|
||||
|
||||
static size_t g_live = 0;
|
||||
static size_t g_peak = 0;
|
||||
|
||||
extern void *__real_malloc(size_t);
|
||||
extern void *__real_calloc(size_t, size_t);
|
||||
extern void *__real_realloc(void *, size_t);
|
||||
extern void __real_free(void *);
|
||||
|
||||
static void acc_alloc(size_t n)
|
||||
{
|
||||
g_live += n;
|
||||
if (g_live > g_peak) g_peak = g_live;
|
||||
}
|
||||
|
||||
void *__wrap_malloc(size_t n)
|
||||
{
|
||||
size_t *h = (size_t *)__real_malloc(n + AHDR);
|
||||
if (!h) return NULL;
|
||||
h[0] = n;
|
||||
acc_alloc(n);
|
||||
return (char *)h + AHDR;
|
||||
}
|
||||
|
||||
void *__wrap_calloc(size_t a, size_t b)
|
||||
{
|
||||
size_t n = a * b;
|
||||
void *p = __wrap_malloc(n);
|
||||
if (p) memset(p, 0, n);
|
||||
return p;
|
||||
}
|
||||
|
||||
void __wrap_free(void *p)
|
||||
{
|
||||
size_t *h;
|
||||
if (!p) return;
|
||||
h = (size_t *)((char *)p - AHDR);
|
||||
g_live -= h[0];
|
||||
__real_free(h);
|
||||
}
|
||||
|
||||
void *__wrap_realloc(void *p, size_t n)
|
||||
{
|
||||
size_t old = p ? ((size_t *)((char *)p - AHDR))[0] : 0;
|
||||
void *q = __wrap_malloc(n);
|
||||
if (!q) return NULL;
|
||||
if (p) {
|
||||
memcpy(q, p, old < n ? old : n);
|
||||
__wrap_free(p);
|
||||
}
|
||||
return q;
|
||||
}
|
||||
|
||||
static size_t peak_reset(void) { g_peak = g_live; return g_live; }
|
||||
static size_t peak_since(size_t base) { return g_peak - base; }
|
||||
|
||||
/* meow_ui_img.c 的绘制函数需要的 gfx 符号:本测试只测解码/内存,桩即可。 */
|
||||
void meow_gfx_blit_rgba(int x, int y, int w, int h, const uint8_t *rgba, int alpha)
|
||||
{
|
||||
(void)x; (void)y; (void)w; (void)h; (void)rgba; (void)alpha;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 断言与打印 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
static int g_fail = 0;
|
||||
#define CHECK(cond, name) do { \
|
||||
if (cond) printf(" PASS %s\n", name); \
|
||||
else { printf(" FAIL %s (%s:%d)\n", name, __FILE__, __LINE__); g_fail++; } } while (0)
|
||||
|
||||
static void peak_report(const char *tag, size_t bytes)
|
||||
{
|
||||
printf(" peak %-28s %8.2f MiB (%zu B)\n", tag, (double)bytes / (1024.0 * 1024.0), bytes);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 路径与夹具 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
static const char *g_dir = NULL;
|
||||
|
||||
static char *path_join(const char *name)
|
||||
{
|
||||
size_t n = strlen(g_dir) + strlen(name) + 2;
|
||||
char *p = (char *)malloc(n);
|
||||
if (!p) { printf("OOM\n"); exit(1); }
|
||||
snprintf(p, n, "%s/%s", g_dir, name);
|
||||
return p;
|
||||
}
|
||||
|
||||
static int file_exists(const char *p)
|
||||
{
|
||||
struct stat st;
|
||||
return stat(p, &st) == 0 && st.st_size > 0;
|
||||
}
|
||||
|
||||
static void ensure_fixtures(void)
|
||||
{
|
||||
char *big = path_join("big.jpg");
|
||||
if (file_exists(big)) { free(big); return; }
|
||||
free(big);
|
||||
printf("夹具缺失,尝试 python3 test_meow_img_budget_gen.py %s\n", g_dir);
|
||||
{
|
||||
char cmd[1024];
|
||||
snprintf(cmd, sizeof cmd, "python3 test_meow_img_budget_gen.py '%s'", g_dir);
|
||||
if (system(cmd) != 0)
|
||||
printf(" 生成失败(缺 Pillow?)——请按头注释手动生成\n");
|
||||
}
|
||||
}
|
||||
|
||||
static uint8_t *read_whole(const char *p, size_t *len)
|
||||
{
|
||||
FILE *f = fopen(p, "rb");
|
||||
uint8_t *buf;
|
||||
long n;
|
||||
if (!f) return NULL;
|
||||
fseek(f, 0, SEEK_END);
|
||||
n = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
if (n <= 0) { fclose(f); return NULL; }
|
||||
buf = (uint8_t *)malloc((size_t)n);
|
||||
if (!buf) { fclose(f); return NULL; }
|
||||
if (fread(buf, 1, (size_t)n, f) != (size_t)n) { free(buf); fclose(f); return NULL; }
|
||||
fclose(f);
|
||||
*len = (size_t)n;
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 图像辅助 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
static int nonblank(const meow_img *im)
|
||||
{
|
||||
int w = meow_img_w(im), h = meow_img_h(im);
|
||||
const unsigned char *p = meow_img_pixels(im);
|
||||
long sum = 0;
|
||||
if (!p || w <= 0 || h <= 0) return 0;
|
||||
for (long i = 0; i < (long)w * h; i += 97)
|
||||
sum += p[i * 4] + p[i * 4 + 1] + p[i * 4 + 2];
|
||||
return sum > 0;
|
||||
}
|
||||
|
||||
/* 按比例网格采样比对两图(尺寸可不同):返回各通道平均绝对差。 */
|
||||
static double mean_abs_diff(const meow_img *a, const meow_img *b, int grid)
|
||||
{
|
||||
int wa = meow_img_w(a), ha = meow_img_h(a);
|
||||
int wb = meow_img_w(b), hb = meow_img_h(b);
|
||||
const unsigned char *pa = meow_img_pixels(a), *pb = meow_img_pixels(b);
|
||||
long acc = 0, cnt = 0;
|
||||
if (!pa || !pb || wa <= 0 || hb <= 0) return 1e9;
|
||||
for (int gy = 0; gy < grid; gy++) {
|
||||
int ya = (int)((long)gy * ha / grid);
|
||||
int yb = (int)((long)gy * hb / grid);
|
||||
for (int gx = 0; gx < grid; gx++) {
|
||||
int xa = (int)((long)gx * wa / grid);
|
||||
int xb = (int)((long)gx * wb / grid);
|
||||
const unsigned char *qa = pa + ((size_t)ya * wa + xa) * 4;
|
||||
const unsigned char *qb = pb + ((size_t)yb * wb + xb) * 4;
|
||||
for (int k = 0; k < 3; k++) {
|
||||
int d = (int)qa[k] - (int)qb[k];
|
||||
acc += d < 0 ? -d : d;
|
||||
cnt++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cnt ? (double)acc / (double)cnt : 1e9;
|
||||
}
|
||||
|
||||
/* ================================================================== */
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
char *p_big, *p_prog, *p_small, *p_png, *p_bmp, *p_broken, *p_missing;
|
||||
size_t peak_old, peak_new;
|
||||
|
||||
if (argc > 1) {
|
||||
g_dir = argv[1];
|
||||
} else {
|
||||
/* Termux 的 /tmp 不可写:优先 $TMPDIR,其次 $PREFIX/tmp。 */
|
||||
static char dflt[600];
|
||||
const char *tmp = getenv("TMPDIR");
|
||||
const char *pre = getenv("PREFIX");
|
||||
if (tmp && *tmp) snprintf(dflt, sizeof dflt, "%s/meow_img_budget", tmp);
|
||||
else if (pre && *pre) snprintf(dflt, sizeof dflt, "%s/tmp/meow_img_budget", pre);
|
||||
else snprintf(dflt, sizeof dflt, "/tmp/meow_img_budget");
|
||||
g_dir = dflt;
|
||||
}
|
||||
ensure_fixtures();
|
||||
|
||||
p_big = path_join("big.jpg");
|
||||
p_prog = path_join("bigprog.jpg");
|
||||
p_small = path_join("small.jpg");
|
||||
p_png = path_join("big.png");
|
||||
p_bmp = path_join("big.bmp");
|
||||
p_broken = path_join("broken.jpg");
|
||||
p_missing = path_join("no_such_file.jpg");
|
||||
|
||||
if (!file_exists(p_big)) {
|
||||
printf("FATAL 夹具 %s 不存在(先跑 test_meow_img_budget_gen.py)\n", p_big);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ---- B1 探针 ---- */
|
||||
printf("[B1] 头探测(零像素内存)\n");
|
||||
{
|
||||
int w = 0, h = 0, c = 0;
|
||||
CHECK(meow_img_probe_file(p_big, &w, &h, &c) == 1, "probe 成功");
|
||||
CHECK(w == 3200 && h == 2600, "probe 原始尺寸 3200x2600");
|
||||
CHECK(c == 3, "probe 通道数 3");
|
||||
printf(" probe → %dx%d ch=%d\n", w, h, c);
|
||||
CHECK(meow_img_probe_file(p_broken, NULL, NULL, NULL) == 0, "probe 截断图 → 0");
|
||||
CHECK(meow_img_probe_file(p_missing, NULL, NULL, NULL) == 0, "probe 不存在 → 0");
|
||||
CHECK(meow_img_probe_file(NULL, NULL, NULL, NULL) == 0, "probe NULL → 0");
|
||||
}
|
||||
|
||||
/* ---- B2 小图不动 ---- */
|
||||
printf("[B2] 小图不动 / maxdim<=0 等价旧入口\n");
|
||||
{
|
||||
meow_img *a = meow_img_load_file(p_small);
|
||||
meow_img *b = meow_img_load_file_dim(p_small, 64);
|
||||
meow_img *z = meow_img_load_file_dim(p_small, 0);
|
||||
CHECK(a && b && z, "小图三条路径都成功");
|
||||
CHECK(meow_img_w(b) == meow_img_w(a) && meow_img_h(b) == meow_img_h(a),
|
||||
"maxdim=64 → 尺寸不变");
|
||||
CHECK(b && a && meow_img_w(a) * meow_img_h(a) > 0 &&
|
||||
memcmp(meow_img_pixels(a), meow_img_pixels(b),
|
||||
(size_t)meow_img_w(a) * meow_img_h(a) * 4) == 0,
|
||||
"小图逐字节与旧入口一致");
|
||||
CHECK(z && a && memcmp(meow_img_pixels(a), meow_img_pixels(z),
|
||||
(size_t)meow_img_w(a) * meow_img_h(a) * 4) == 0,
|
||||
"maxdim=0 等价旧入口(逐字节)");
|
||||
meow_img_free(a); meow_img_free(b); meow_img_free(z);
|
||||
}
|
||||
|
||||
/* ---- B3 长边约束 ---- */
|
||||
printf("[B3] 8.32MP baseline JPEG 长边约束\n");
|
||||
{
|
||||
static const int dims[] = { 480, 300, 128, 64 };
|
||||
int wi = 0, hi = 0;
|
||||
meow_img *full = meow_img_load_file(p_big);
|
||||
CHECK(full != NULL, "旧入口解码成功");
|
||||
if (full) { wi = meow_img_w(full); hi = meow_img_h(full); }
|
||||
printf(" 原图 %dx%d (%.2f MP)\n", wi, hi, (double)wi * hi / 1e6);
|
||||
for (size_t i = 0; i < sizeof dims / sizeof dims[0]; i++) {
|
||||
meow_img *s = meow_img_load_file_dim(p_big, dims[i]);
|
||||
char name[96];
|
||||
snprintf(name, sizeof name, "maxdim=%d → 两维 ≤ maxdim 且非空", dims[i]);
|
||||
CHECK(s && meow_img_w(s) <= dims[i] && meow_img_h(s) <= dims[i] && nonblank(s), name);
|
||||
if (s) {
|
||||
double ar0 = (double)wi / hi, ar1 = (double)meow_img_w(s) / meow_img_h(s);
|
||||
CHECK(ar1 > ar0 * 0.98 && ar1 < ar0 * 1.02, "长宽比保持(±2%)");
|
||||
printf(" maxdim=%-4d → %dx%d\n", dims[i], meow_img_w(s), meow_img_h(s));
|
||||
}
|
||||
meow_img_free(s);
|
||||
}
|
||||
meow_img_free(full);
|
||||
}
|
||||
|
||||
/* ---- B4 峰值对比(JPEG) ---- */
|
||||
printf("[B4] 瞬时分配峰值:旧入口 vs 新入口(同一 8.32MP JPEG)\n");
|
||||
{
|
||||
size_t base = peak_reset();
|
||||
meow_img *old = meow_img_load_file(p_big);
|
||||
peak_old = peak_since(base);
|
||||
|
||||
base = peak_reset();
|
||||
meow_img *nw = meow_img_load_file_dim(p_big, 480);
|
||||
peak_new = peak_since(base);
|
||||
|
||||
CHECK(old && nw, "两条路径都成功");
|
||||
peak_report("旧入口 meow_img_load_file", peak_old);
|
||||
peak_report("新入口 _dim(480)", peak_new);
|
||||
printf(" 整幅 RGBA 理论值 = %.2f MiB;新/旧 = %.2f\n",
|
||||
(double)meow_img_w(old) * meow_img_h(old) * 4 / (1024.0 * 1024.0),
|
||||
(double)peak_new / (double)peak_old);
|
||||
CHECK(peak_new * 2 < peak_old, "新入口峰值 < 旧入口 1/2(不物化整幅 RGBA)");
|
||||
CHECK(peak_new < (size_t)20 * 1024 * 1024, "新入口峰值 < 20 MiB(8.32MP 页可加载)");
|
||||
|
||||
/* B5 内容正确性:新 vs 旧整幅 + downscale 精修 */
|
||||
if (old && nw) {
|
||||
meow_img *ref = meow_img_downscale(old, 480);
|
||||
double d = mean_abs_diff(nw, ref ? ref : old, 64);
|
||||
printf(" 与「旧入口+downscale」网格平均差 = %.2f\n", d);
|
||||
CHECK(d < 20.0, "缩解内容结构一致(平均差 < 20)");
|
||||
meow_img_free(ref);
|
||||
}
|
||||
meow_img_free(old);
|
||||
meow_img_free(nw);
|
||||
}
|
||||
|
||||
/* ---- B4b 极端 maxdim ---- */
|
||||
printf("[B4b] maxdim=100(8 倍缩解档位 + 精修)\n");
|
||||
{
|
||||
size_t base = peak_reset();
|
||||
meow_img *s = meow_img_load_file_dim(p_big, 100);
|
||||
size_t peak = peak_since(base);
|
||||
CHECK(s && meow_img_w(s) <= 100 && meow_img_h(s) <= 100 && nonblank(s), "结果 ≤100 且非空");
|
||||
peak_report("_dim(100)", peak);
|
||||
CHECK(peak * 2 < peak_old, "峰值仍 < 旧入口 1/2");
|
||||
meow_img_free(s);
|
||||
}
|
||||
|
||||
/* ---- B4c finding 原始场景 ---- */
|
||||
printf("[B4c] finding 原始场景:1600x2400 漫画页\n");
|
||||
{
|
||||
char *p_page = path_join("page.jpg");
|
||||
if (file_exists(p_page)) {
|
||||
size_t base = peak_reset();
|
||||
meow_img *old = meow_img_load_file(p_page);
|
||||
size_t p_old = peak_since(base);
|
||||
base = peak_reset();
|
||||
meow_img *nw = meow_img_load_file_dim(p_page, 720);
|
||||
size_t p_new = peak_since(base);
|
||||
CHECK(old && nw, "1600x2400 两条路径都成功");
|
||||
if (nw)
|
||||
CHECK(meow_img_w(nw) <= 720 && meow_img_h(nw) <= 720 && nonblank(nw),
|
||||
"结果 ≤720 且非空");
|
||||
if (old)
|
||||
printf(" 整幅 RGBA 理论值 = %.2f MiB\n",
|
||||
(double)meow_img_w(old) * meow_img_h(old) * 4 / (1024.0 * 1024.0));
|
||||
peak_report("旧入口 page.jpg", p_old);
|
||||
peak_report("新入口 _dim(720)", p_new);
|
||||
CHECK(p_new < (size_t)10 * 1024 * 1024, "新入口峰值 < 10 MiB(设备 ~14MB 口径内)");
|
||||
meow_img_free(old);
|
||||
meow_img_free(nw);
|
||||
} else {
|
||||
printf(" SKIP page.jpg(夹具缺失)\n");
|
||||
}
|
||||
free(p_page);
|
||||
}
|
||||
|
||||
/* ---- B6 进升级 JPEG ---- */
|
||||
printf("[B6] progressive JPEG(同一缩解路径)\n");
|
||||
{
|
||||
size_t base = peak_reset();
|
||||
meow_img *old = meow_img_load_file(p_prog);
|
||||
size_t p_old = peak_since(base);
|
||||
base = peak_reset();
|
||||
meow_img *nw = meow_img_load_file_dim(p_prog, 480);
|
||||
size_t p_new = peak_since(base);
|
||||
CHECK(old && nw, "进升级两条路径都成功");
|
||||
CHECK(nw && meow_img_w(nw) <= 480 && meow_img_h(nw) <= 480 && nonblank(nw),
|
||||
"进升级结果 ≤480 且非空");
|
||||
peak_report("旧入口 progressive", p_old);
|
||||
peak_report("新入口 _dim(480) progressive", p_new);
|
||||
CHECK(p_new < p_old, "进升级峰值下降(系数阵列仍在扫描期驻留,见报告)");
|
||||
meow_img_free(old);
|
||||
meow_img_free(nw);
|
||||
}
|
||||
|
||||
/* ---- B7 内存入口 ---- */
|
||||
printf("[B7] 内存入口 meow_img_load_mem_dim(CBZ/档案页场景)\n");
|
||||
{
|
||||
size_t len = 0;
|
||||
uint8_t *buf = read_whole(p_big, &len);
|
||||
CHECK(buf != NULL, "读入 8.32MP JPEG 到内存");
|
||||
if (buf) {
|
||||
size_t base = peak_reset();
|
||||
meow_img *old = meow_img_load_mem(buf, (int)len);
|
||||
size_t p_old = peak_since(base);
|
||||
base = peak_reset();
|
||||
meow_img *nw = meow_img_load_mem_dim(buf, (int)len, 480);
|
||||
size_t p_new = peak_since(base);
|
||||
CHECK(old && nw, "内存入口两条路径都成功");
|
||||
CHECK(nw && meow_img_w(nw) <= 480 && nonblank(nw), "内存入口结果 ≤480 且非空");
|
||||
peak_report("旧 meow_img_load_mem", p_old);
|
||||
peak_report("新 meow_img_load_mem_dim", p_new);
|
||||
CHECK(p_new * 2 < p_old, "内存入口峰值 < 旧入口 1/2");
|
||||
meow_img_free(old);
|
||||
meow_img_free(nw);
|
||||
free(buf);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- B8 坏图 ---- */
|
||||
printf("[B8] 坏图容错\n");
|
||||
{
|
||||
size_t len = 0;
|
||||
uint8_t *buf;
|
||||
CHECK(meow_img_load_file_dim(p_broken, 480) == NULL, "截断 JPEG → NULL");
|
||||
CHECK(meow_img_load_file_dim(p_missing, 480) == NULL, "不存在路径 → NULL");
|
||||
CHECK(meow_img_load_file_dim(NULL, 480) == NULL, "NULL 路径 → NULL");
|
||||
buf = read_whole(p_broken, &len);
|
||||
CHECK(buf && meow_img_load_mem_dim(buf, (int)len, 480) == NULL, "截断 JPEG 内存入口 → NULL");
|
||||
free(buf);
|
||||
CHECK(meow_img_load_mem_dim("x", 3, 480) == NULL, "垃圾内存 → NULL");
|
||||
}
|
||||
|
||||
/* ---- B9 PNG / BMP(M23 起也走解码期缩解;只断言尺寸/非空,峰值打印) ---- */
|
||||
printf("[B9] PNG / BMP:尺寸约束成立,峰值记录(M23 逐行/跳行缩解,应远低于旧入口)\n");
|
||||
{
|
||||
struct { const char *tag; const char *path; } fx[] = {
|
||||
{ "PNG", p_png }, { "BMP", p_bmp },
|
||||
};
|
||||
for (size_t i = 0; i < sizeof fx / sizeof fx[0]; i++) {
|
||||
if (!file_exists(fx[i].path)) {
|
||||
printf(" SKIP %s(夹具缺失)\n", fx[i].tag);
|
||||
continue;
|
||||
}
|
||||
{
|
||||
size_t base = peak_reset();
|
||||
meow_img *old = meow_img_load_file(fx[i].path);
|
||||
size_t p_old = peak_since(base);
|
||||
meow_img *nw;
|
||||
char name[96];
|
||||
base = peak_reset();
|
||||
nw = meow_img_load_file_dim(fx[i].path, 480);
|
||||
{
|
||||
size_t p_new = peak_since(base);
|
||||
snprintf(name, sizeof name, "%s _dim(480) 两维 ≤480 且非空", fx[i].tag);
|
||||
CHECK(nw && meow_img_w(nw) <= 480 && meow_img_h(nw) <= 480 && nonblank(nw), name);
|
||||
peak_report(fx[i].tag, p_old);
|
||||
peak_report(" ↑ 新入口", p_new);
|
||||
snprintf(name, sizeof name, "%s 新入口峰值 ≤ 旧入口(不再整幅物化)", fx[i].tag);
|
||||
CHECK(p_new <= p_old, name);
|
||||
if (old) printf(" %s 原图 %dx%d\n", fx[i].tag, meow_img_w(old), meow_img_h(old));
|
||||
meow_img_free(nw);
|
||||
}
|
||||
meow_img_free(old);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (g_fail) printf("\nRESULT: FAIL (%d)\n", g_fail);
|
||||
else printf("\nRESULT: ALL PASS\n");
|
||||
free(p_big); free(p_prog); free(p_small); free(p_png);
|
||||
free(p_bmp); free(p_broken); free(p_missing);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env python3
|
||||
"""test_meow_img_budget_gen.py — 为宿主测试 test_meow_img_budget.c 生成图片夹具。
|
||||
|
||||
用法:python3 test_meow_img_budget_gen.py <输出目录>
|
||||
依赖:Pillow(PIL)。夹具只含结构化图案(白底/黑线/灰块),体积极小。
|
||||
|
||||
生成:
|
||||
big.jpg 3200x2600 baseline 4:2:0 JPEG(8.32MP,峰值对比主用例)
|
||||
bigprog.jpg 3200x2600 progressive JPEG(验证进升级解码走同一缩解路径)
|
||||
page.jpg 1600x2400 baseline JPEG(finding 里 15.4MB 的漫画页原尺寸)
|
||||
small.jpg 64x64 baseline JPEG(「小图不动」用例)
|
||||
big.png 2400x1600 RGB PNG(stb 无解码期缩放:峰值如实记录,不作改善断言)
|
||||
big.bmp 2400x1600 24bpp BMP(同上)
|
||||
broken.jpg JPEG 魔数 + 垃圾字节(头部解析失败,坏图用例)
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
|
||||
def page_pattern(w, h):
|
||||
"""漫画页近似:白底 + 分镜框线 + 灰阶网点带。内容稀疏 → JPEG 体积小。"""
|
||||
im = Image.new("RGB", (w, h), (250, 249, 245))
|
||||
d = ImageDraw.Draw(im)
|
||||
for k in range(0, min(w, h) - 400, 60):
|
||||
d.rectangle([100 + k, 100 + k, 400 + k, 300 + k], outline=(12, 12, 14), width=5)
|
||||
for band in range(14):
|
||||
y0 = band * h // 14
|
||||
for x in range(0, w, 9):
|
||||
d.line([(x, y0), (x + 4, y0 + 7)], fill=(110, 110, 116))
|
||||
return im
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
print(__doc__)
|
||||
return 2
|
||||
out = sys.argv[1]
|
||||
os.makedirs(out, exist_ok=True)
|
||||
|
||||
big = page_pattern(3200, 2600)
|
||||
big.save(os.path.join(out, "big.jpg"), "JPEG", quality=55, subsampling=2)
|
||||
big.save(os.path.join(out, "bigprog.jpg"), "JPEG", quality=55, subsampling=2,
|
||||
progressive=True)
|
||||
|
||||
small = page_pattern(64, 64)
|
||||
small.save(os.path.join(out, "small.jpg"), "JPEG", quality=80, subsampling=2)
|
||||
|
||||
page = page_pattern(1600, 2400)
|
||||
page.save(os.path.join(out, "page.jpg"), "JPEG", quality=55, subsampling=2)
|
||||
|
||||
mid = page_pattern(2400, 1600)
|
||||
mid.save(os.path.join(out, "big.png"), "PNG")
|
||||
mid.save(os.path.join(out, "big.bmp"), "BMP")
|
||||
|
||||
# 坏图:JPEG 魔数 + 垃圾字节 → 头部解析失败(stb 对「截断但头部完整」很宽容,不能只截断)
|
||||
garbage = bytes((i * 37 + 11) & 0xFF for i in range(256))
|
||||
with open(os.path.join(out, "broken.jpg"), "wb") as f:
|
||||
f.write(b"\xff\xd8\xff\xe0" + garbage)
|
||||
|
||||
for name in ("big.jpg", "bigprog.jpg", "page.jpg", "small.jpg", "big.png",
|
||||
"big.bmp", "broken.jpg"):
|
||||
print(" %-12s %8d B" % (name, os.path.getsize(os.path.join(out, name))))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,227 @@
|
||||
/* test_meow_img_color.c — 批5 封面双色渐变取色宿主验证。
|
||||
*
|
||||
* 覆盖矩阵(对齐任务批5):
|
||||
* C1 纯色 → 主色=该色(桶平均精确);无显著次桶 → 次色=主色缩暗 2/3(渐变互补)
|
||||
* C2 两色图 → 主/次正确分离(各桶平均精确)
|
||||
* C3 近黑/近白/透明跳过:近黑+鲜艳 → 主=鲜艳;全近黑/全近白/全透明 → -1(回退主题)
|
||||
* C4 尺寸缩放/副采样:12×12 与 120×120 同内容 → 主色不变
|
||||
* C5 meow_img_duocolor 桥接(局部 struct meow_img 直读 rgba)
|
||||
* C6 meow_img_color_derive:可读 fg/accent_fg、背景渐变、scrim=主色压暗
|
||||
* C7 set_colors 注入/回退(lyric_view 层):注入 derive.lyr → cur=fg、gray 随 cur;
|
||||
* NULL 回退 meow_theme 默认
|
||||
*
|
||||
* 编译(在 sdk 目录下;链法同 test_meow_lyric_view + 单链 meow_img_color.c;
|
||||
* meow_ui_time.c 提供 meow_ui_time_ms,meow_jpeg.c/meow_png_stream.c 为 M23 解码后端,-lz 供其 inflate):
|
||||
* gcc -Wall -Wextra -o test_meow_img_color test_meow_img_color.c test_host_stub.c \
|
||||
* meow_img_color.c meow_lyric.c meow_lyric_view.c meow_ui_time.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_jpeg.c meow_png_stream.c meow_ui_media.c meow_notify_ipc.c \
|
||||
* stb_image.c meow_ui_nav.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm -lz
|
||||
* 运行:./test_meow_img_color
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h" /* meow_ui_back_buf/pitch/bpp(如需真实像素断言) */
|
||||
#include "meow_lyric_view.h"
|
||||
#include "meow_img_color.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
|
||||
/* 局部补全 meow_img(与 meow_ui_img.c / meow_img_color.c 布局一致):构造假封面做桥接测试。 */
|
||||
struct meow_img { int w, h; unsigned char *rgba; };
|
||||
|
||||
static int g_fail = 0;
|
||||
#define CHECK(cond, name) do { \
|
||||
if (cond) printf(" PASS %s\n", name); \
|
||||
else { printf(" FAIL %s (%s:%d)\n", name, __FILE__, __LINE__); g_fail++; } } while (0)
|
||||
|
||||
/* 构造 RGBA 缓冲:rows 描述每行占 w 空格,n 个 (start,count,r,g,b,a) 段。 */
|
||||
static unsigned char *make_rgba(int w, int h)
|
||||
{
|
||||
unsigned char *buf = (unsigned char *)calloc((size_t)w * h * 4, 1);
|
||||
assert(buf);
|
||||
return buf;
|
||||
}
|
||||
|
||||
static void fill_px(unsigned char *buf, int w, int x, int y, int r, int g, int b, int a)
|
||||
{
|
||||
unsigned char *p = buf + ((size_t)y * w + (size_t)x) * 4;
|
||||
p[0] = (unsigned char)r; p[1] = (unsigned char)g; p[2] = (unsigned char)b; p[3] = (unsigned char)a;
|
||||
}
|
||||
|
||||
/* 填充一块实心色(x0,y0,w0,h0)。 */
|
||||
static void fill_rect_px(unsigned char *buf, int W, int x0, int y0, int w0, int h0,
|
||||
int r, int g, int b, int a)
|
||||
{
|
||||
for (int y = y0; y < y0 + h0; y++)
|
||||
for (int x = x0; x < x0 + w0; x++)
|
||||
fill_px(buf, W, x, y, r, g, b, a);
|
||||
}
|
||||
|
||||
static void c1_solid(void)
|
||||
{
|
||||
printf("C1 纯色 → 主色精确,次色=主色缩暗 2/3\n");
|
||||
int W = 12, H = 12;
|
||||
unsigned char *buf = make_rgba(W, H);
|
||||
fill_rect_px(buf, W, 0, 0, W, H, 0x30, 0x60, 0xA0, 0xFF);
|
||||
uint32_t m = 0, s = 0;
|
||||
int rc = meow_img_duocolor_rgba(buf, W, H, &m, &s);
|
||||
CHECK(rc == 0, "纯色取色成功");
|
||||
CHECK(m == 0x3060A0, "主色==0x3060A0(桶中心精确)");
|
||||
CHECK(s == 0x20406A, "次色==0x20406A(主色缩暗 2/3 渐变互补)");
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void c2_two_colors(void)
|
||||
{
|
||||
printf("C2 两色图 → 主/次分离\n");
|
||||
int W = 16, H = 16;
|
||||
unsigned char *buf = make_rgba(W, H);
|
||||
fill_rect_px(buf, W, 0, 0, W, 8, 0xE0, 0x30, 0x40, 0xFF); /* 上半:红 */
|
||||
fill_rect_px(buf, W, 0, 8, W, 8, 0x20, 0x40, 0xE0, 0xFF); /* 下半:蓝 */
|
||||
uint32_t m = 0, s = 0;
|
||||
int rc = meow_img_duocolor_rgba(buf, W, H, &m, &s);
|
||||
CHECK(rc == 0, "两色取色成功");
|
||||
CHECK(m != s, "主色 != 次色");
|
||||
/* 蓝的饱和权重更高(sat=192 vs 红 176)→ 主色偏蓝,次色偏红 */
|
||||
CHECK(((m >> 16) & 0xFF) < 0x80 && (m & 0xFF) > 0xA0, "主色为蓝系(B 高)");
|
||||
CHECK(((s >> 16) & 0xFF) > 0xA0 && (s & 0xFF) < 0x80, "次色为红系(R 高)");
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void c3_skip_near(void)
|
||||
{
|
||||
printf("C3 近黑/近白/透明跳过\n");
|
||||
/* 近黑 + 一排鲜艳 → 主=鲜艳(近黑被跳过) */
|
||||
int W = 12, H = 12;
|
||||
unsigned char *buf = make_rgba(W, H);
|
||||
fill_rect_px(buf, W, 0, 0, W, 11, 0x08, 0x08, 0x08, 0xFF); /* 近黑(luma<24) */
|
||||
fill_rect_px(buf, W, 0, 11, W, 1, 0xE0, 0x30, 0x40, 0xFF); /* 鲜艳一行 */
|
||||
uint32_t m = 0, s = 0;
|
||||
int rc = meow_img_duocolor_rgba(buf, W, H, &m, &s);
|
||||
CHECK(rc == 0, "近黑+鲜艳 → 成功");
|
||||
CHECK(m == 0xE03040, "主色==鲜艳色(近黑被跳过)");
|
||||
free(buf);
|
||||
|
||||
/* 全近黑 → -1(回退主题) */
|
||||
buf = make_rgba(W, H);
|
||||
fill_rect_px(buf, W, 0, 0, W, H, 0x10, 0x10, 0x10, 0xFF);
|
||||
CHECK(meow_img_duocolor_rgba(buf, W, H, &m, &s) == -1, "全近黑 → -1");
|
||||
free(buf);
|
||||
|
||||
/* 全近白 → -1 */
|
||||
buf = make_rgba(W, H);
|
||||
fill_rect_px(buf, W, 0, 0, W, H, 0xF8, 0xF8, 0xF8, 0xFF);
|
||||
CHECK(meow_img_duocolor_rgba(buf, W, H, &m, &s) == -1, "全近白 → -1");
|
||||
free(buf);
|
||||
|
||||
/* 全透明 → -1 */
|
||||
buf = make_rgba(W, H);
|
||||
/* 默认 calloc → rgba=0 → alpha=0 全部跳过 */
|
||||
CHECK(meow_img_duocolor_rgba(buf, W, H, &m, &s) == -1, "全透明 → -1");
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void c4_size_invariant(void)
|
||||
{
|
||||
printf("C4 尺寸缩放/副采样不变性\n");
|
||||
/* 12×12 vs 120×120:同内容上下半红蓝 → 主色(蓝)一致 */
|
||||
uint32_t m1 = 0, s1 = 0, m2 = 0, s2 = 0;
|
||||
unsigned char *b1 = make_rgba(12, 12);
|
||||
fill_rect_px(b1, 12, 0, 0, 12, 6, 0xE0, 0x30, 0x40, 0xFF);
|
||||
fill_rect_px(b1, 12, 0, 6, 12, 6, 0x20, 0x40, 0xE0, 0xFF);
|
||||
CHECK(meow_img_duocolor_rgba(b1, 12, 12, &m1, &s1) == 0, "小图取色成功");
|
||||
unsigned char *b2 = make_rgba(120, 120);
|
||||
fill_rect_px(b2, 120, 0, 0, 120, 60, 0xE0, 0x30, 0x40, 0xFF);
|
||||
fill_rect_px(b2, 120, 0, 60, 120, 60, 0x20, 0x40, 0xE0, 0xFF);
|
||||
CHECK(meow_img_duocolor_rgba(b2, 120, 120, &m2, &s2) == 0, "大图取色成功");
|
||||
CHECK(m1 == m2, "大/小图主色一致(副采样不变性)");
|
||||
free(b1);
|
||||
free(b2);
|
||||
}
|
||||
|
||||
static void c5_img_bridge(void)
|
||||
{
|
||||
printf("C5 meow_img_duocolor 桥接(局部 struct meow_img)\n");
|
||||
int W = 4, H = 4;
|
||||
struct meow_img im;
|
||||
im.w = W; im.h = H;
|
||||
im.rgba = make_rgba(W, H);
|
||||
fill_rect_px(im.rgba, W, 0, 0, W, 2, 0xE0, 0x30, 0x40, 0xFF);
|
||||
fill_rect_px(im.rgba, W, 0, 2, W, 2, 0x20, 0x40, 0xE0, 0xFF);
|
||||
uint32_t m = 0, s = 0;
|
||||
int rc = meow_img_duocolor((const meow_img *)&im, &m, &s);
|
||||
CHECK(rc == 0, "meow_img 句柄取色成功");
|
||||
CHECK(((m >> 16) & 0xFF) < 0x80 && (m & 0xFF) > 0xA0, "桥接主色为蓝系");
|
||||
/* NULL → -1 */
|
||||
CHECK(meow_img_duocolor(NULL, &m, &s) == -1, "NULL 句柄 → -1");
|
||||
free(im.rgba);
|
||||
}
|
||||
|
||||
static void c6_derive(void)
|
||||
{
|
||||
printf("C6 meow_img_color_derive 派生主题\n");
|
||||
meow_theme th; memset(&th, 0, sizeof th);
|
||||
th.primary = 0x204060; th.secondary = 0xE0B050; /* 深蓝主 + 琥珀次 */
|
||||
meow_img_color_theme ct;
|
||||
meow_img_color_derive(0x204060, 0xE0B050, &th, &ct);
|
||||
/* 可读性:主深次亮 → 平均亮度偏中低 → 文字白 */
|
||||
CHECK(ct.fg == 0xFFFFFF, "fg==白色(背景偏暗可读)");
|
||||
CHECK(ct.accent == 0xE0B050, "accent==次色(琥珀)");
|
||||
CHECK(ct.accent_fg == 0x1C1C1C, "accent_fg==深色(琥珀上可读图标)");
|
||||
CHECK(ct.bg_top != ct.bg_bottom, "背景双色渐变(顶!=底)");
|
||||
/* scrim=主色压暗:比原主色更暗(向黑靠) */
|
||||
CHECK(((ct.lyr.scrim >> 16) & 0xFF) < 0x20, "lyr.scrim 主色压暗(R 递减)");
|
||||
CHECK(ct.lyr.cur == 0xFFFFFF, "lyr.cur==fg(可读)");
|
||||
CHECK(ct.lyr.scrim_alpha == 176, "lyr.scrim_alpha==176");
|
||||
}
|
||||
|
||||
static void c7_set_colors_inject(void)
|
||||
{
|
||||
printf("C7 set_colors 注入/回退(lyric_view 层)\n");
|
||||
meow_theme_set_mode(MEOW_THEME_DARK);
|
||||
meow_lyric_view *lv = meow_lyric_view_create(0, 0, 480, 720, NULL, NULL);
|
||||
assert(lv);
|
||||
uint32_t def_cur = meow_lyric_view_cur_color(lv); /* dark=0xFFFFFF 纯白 */
|
||||
/* 注入封面派生主题 */
|
||||
meow_img_color_theme ct;
|
||||
meow_img_color_derive(0x204060, 0xE0B050, meow_theme_get(), &ct);
|
||||
meow_lyric_view_set_colors(lv, &ct.lyr);
|
||||
CHECK(meow_lyric_view_cur_color(lv) == ct.lyr.cur, "注入后 cur==派生 fg(可读)");
|
||||
/* gray 随覆盖 cur 自动递减(非白 cur 亦然:红色 cur → 灰阶=0xBB0000 级) */
|
||||
meow_lyric_view_colors c; memset(&c, 0, sizeof c);
|
||||
c.cur = 0xFF0000;
|
||||
meow_lyric_view_set_colors(lv, &c);
|
||||
CHECK(meow_lyric_view_cur_color(lv) == 0xFF0000, "覆盖 cur=红生效");
|
||||
CHECK(meow_lyric_view_line_color(lv, 1) == 0xBB0000, "gray(1) 随覆盖 cur 递减(0xBB0000)");
|
||||
/* 回退:NULL → 主题默认 */
|
||||
meow_lyric_view_set_colors(lv, NULL);
|
||||
CHECK(meow_lyric_view_cur_color(lv) == def_cur, "set_colors(NULL) 回退主题默认");
|
||||
meow_lyric_view_free(lv);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
setbuf(stdout, NULL);
|
||||
setenv("MEOW_UI_DUMP", "/tmp/imgcolor.ppm", 1);
|
||||
meow_ui_opts o = { 480, 720 };
|
||||
assert(meow_ui_init(&o) == 0);
|
||||
meow_ui_activity_push(0x57);
|
||||
|
||||
c1_solid();
|
||||
c2_two_colors();
|
||||
c3_skip_near();
|
||||
c4_size_invariant();
|
||||
c5_img_bridge();
|
||||
c6_derive();
|
||||
c7_set_colors_inject();
|
||||
|
||||
meow_ui_activity_pop();
|
||||
meow_ui_shutdown();
|
||||
printf(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS\n", g_fail);
|
||||
return g_fail;
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
* test_meow_lyric.c — meow_lyric_* 宿主单测(2026-09-03,SDK 通用歌词组件批1)。
|
||||
*
|
||||
* 编译(宿主,meow_lyric 为纯 libc + pthread,无需 SDK 其它对象):
|
||||
* gcc -Wall -Wextra -pthread -o test_meow_lyric test_meow_lyric.c meow_lyric.c -I. -Iinclude
|
||||
* ./test_meow_lyric
|
||||
*
|
||||
* 覆盖矩阵:
|
||||
* T1 解析基础 + 乱序自动按时间升序排序(原文行)
|
||||
* T2 meow_lyric_line_at 边界:首戳前 / 首戳 / 区间 / 恰逢戳 / 末戳后 / 负值
|
||||
* T3 翻译合并:按相同 timestamp 匹配合并;无匹配翻译行忽略
|
||||
* T4 服务器字面 \n / \r 当换行(含 \r\n 折叠成单空格)
|
||||
* T5 行上限截断:meow_lyric_set_max_lines 下调后 parse 超限行丢弃
|
||||
* T6 时间戳小数位自适应:1 位(0.1s) / 2 位(厘秒) / 3 位(网易云毫秒) / >3 位截断到 ms
|
||||
* T7 越界访问:line_text/line_tl 越界返回 "";line_time 越界返回 -1
|
||||
* T8 meow_lyric_clear:清空后 count=0 / text="" / line_time=-1;空输入 parse 返回 0
|
||||
* T9 折行 meow_lyric_wrap:中西文混排断行(英文按词、中文按字符)+ max_rows 截断
|
||||
* + 单字符超宽防死循环 + width 回调为 NULL 时按字节近似兜底
|
||||
*/
|
||||
#include "meow_lyric.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static int g_fail = 0;
|
||||
#define CHECK(cond, msg) do { \
|
||||
if (cond) { printf(" PASS: %s\n", msg); } \
|
||||
else { printf(" FAIL: %s (%s:%d)\n", msg, __FILE__, __LINE__); g_fail = 1; } \
|
||||
} while (0)
|
||||
|
||||
/* 折行宽测量:ASCII=1 单位,多字节(CJK 等)=2 单位(用于中西文断行测试)。 */
|
||||
static int uw(const char *u, int size, void *ctx)
|
||||
{
|
||||
(void)size; (void)ctx;
|
||||
int w = 0;
|
||||
for (const char *p = u; *p; ) {
|
||||
unsigned char c = (unsigned char)*p;
|
||||
int ch = (c < 0x80) ? 1 : (c < 0xE0) ? 2 : (c < 0xF0) ? 3 : 4;
|
||||
w += (c < 0x80) ? 1 : 2;
|
||||
p += ch;
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
static void t1_basic(void)
|
||||
{
|
||||
printf("T1 解析基础 + 乱序按时间升序排序\n");
|
||||
/* 输入乱序;期望按时间戳升序存(Line B@1000 < Line C@2000 < Line A@3000) */
|
||||
const char *lrc = "[00:03.00]Line A\n[00:01.00]Line B\n[00:02.00]Line C\n";
|
||||
int n = meow_lyric_parse(lrc, NULL);
|
||||
CHECK(n == 3, "count==3");
|
||||
CHECK(meow_lyric_count() == 3, "count()==3");
|
||||
CHECK(strcmp(meow_lyric_line_text(0), "Line B") == 0, "idx0==Line B (1000ms 升序最前)");
|
||||
CHECK(strcmp(meow_lyric_line_text(1), "Line C") == 0, "idx1==Line C");
|
||||
CHECK(strcmp(meow_lyric_line_text(2), "Line A") == 0, "idx2==Line A (3000ms 最后)");
|
||||
CHECK(meow_lyric_line_time(0) == 1000, "line_time(0)==1000");
|
||||
CHECK(meow_lyric_line_time(2) == 3000, "line_time(2)==3000");
|
||||
}
|
||||
|
||||
static void t2_line_at(void)
|
||||
{
|
||||
printf("T2 meow_lyric_line_at 边界\n");
|
||||
const char *lrc = "[00:01.00]a\n[00:02.00]b\n[00:03.00]c\n";
|
||||
meow_lyric_parse(lrc, NULL);
|
||||
CHECK(meow_lyric_line_at(500) == 0, "首戳前(500ms)→0");
|
||||
CHECK(meow_lyric_line_at(1000) == 0, "恰逢首戳→0");
|
||||
CHECK(meow_lyric_line_at(1500) == 0, "首区间(1500ms)→0");
|
||||
CHECK(meow_lyric_line_at(2500) == 1, "第二区间(2500ms)→1");
|
||||
CHECK(meow_lyric_line_at(3000) == 2, "恰逢末戳→2");
|
||||
CHECK(meow_lyric_line_at(99999) == 2, "末戳后→n-1");
|
||||
CHECK(meow_lyric_line_at(-100) == 0, "负值→0");
|
||||
}
|
||||
|
||||
static void t3_translation(void)
|
||||
{
|
||||
printf("T3 翻译合并(同 timestamp;无匹配翻译行忽略)\n");
|
||||
const char *lrc = "[00:02.00]Hello\n[00:05.00]World\n";
|
||||
const char *tlrc = "[00:05.00]你好\n[00:02.00]你好世界\n[01:00.00]no match line\n";
|
||||
int n = meow_lyric_parse(lrc, tlrc);
|
||||
CHECK(n == 2, "count==2(原文行数)");
|
||||
CHECK(strcmp(meow_lyric_line_text(0), "Hello") == 0, "idx0原文 Hello");
|
||||
CHECK(strcmp(meow_lyric_line_tl(0), "你好世界") == 0, "idx0翻译 你好世界(按 2000ms 合并)");
|
||||
CHECK(strcmp(meow_lyric_line_text(1), "World") == 0, "idx1原文 World");
|
||||
CHECK(strcmp(meow_lyric_line_tl(1), "你好") == 0, "idx1翻译 你好(按 5000ms 合并)");
|
||||
/* 01:00.00 无对应原文行 → 丢弃,不新增行也不污染其它翻译 */
|
||||
CHECK(meow_lyric_count() == 2, "无匹配翻译行被丢弃(仍 2 行)");
|
||||
}
|
||||
|
||||
static void t4_literal_newline(void)
|
||||
{
|
||||
printf("T4 服务器字面 \\n / \\r 当换行\n");
|
||||
/* C 源码里 \\n 是字面反斜杠+n(服务器塞进一行的多行文本) */
|
||||
const char *lrc = "[00:01.00]line1\\nline2\r\n[00:02.00]a\\r\\nb\n";
|
||||
meow_lyric_parse(lrc, NULL);
|
||||
CHECK(strcmp(meow_lyric_line_text(0), "line1 line2") == 0, "字面 \\n → 空格");
|
||||
CHECK(strcmp(meow_lyric_line_text(1), "a b") == 0, "字面 \\r\\n 成对 → 单空格");
|
||||
}
|
||||
|
||||
static void t5_limit(void)
|
||||
{
|
||||
printf("T5 行上限截断(set_max_lines 下调后超限丢弃)\n");
|
||||
meow_lyric_set_max_lines(2);
|
||||
const char *lrc = "[00:01]a\n[00:02]b\n[00:03]c\n[00:04]d\n";
|
||||
int n = meow_lyric_parse(lrc, NULL);
|
||||
CHECK(n == 2, "count==2(上限 2)");
|
||||
CHECK(strcmp(meow_lyric_line_text(0), "a") == 0, "idx0==a");
|
||||
CHECK(strcmp(meow_lyric_line_text(1), "b") == 0, "idx1==b(第三个 c 被截断)");
|
||||
meow_lyric_set_max_lines(MEOW_LYRIC_LINES_MAX); /* 恢复默认 */
|
||||
n = meow_lyric_parse(lrc, NULL);
|
||||
CHECK(n == 4, "恢复上限后 count==4");
|
||||
/* set_max_lines 越界 clamp */
|
||||
meow_lyric_set_max_lines(0);
|
||||
n = meow_lyric_parse(lrc, NULL);
|
||||
CHECK(n == 1, "set_max_lines(0)→clamp=1,count==1");
|
||||
meow_lyric_set_max_lines(MEOW_LYRIC_LINES_MAX);
|
||||
}
|
||||
|
||||
static void t6_timefrac(void)
|
||||
{
|
||||
printf("T6 时间戳小数位自适应\n");
|
||||
const char *lrc =
|
||||
"[00:01.5]a\n" /* 1 位 → 1500ms */
|
||||
"[00:02.25]b\n" /* 2 位 → 2250ms */
|
||||
"[00:03.456]c\n" /* 3 位(网易云毫秒) → 3456ms */
|
||||
"[00:04.1234]d\n"; /* >3 位截断到 ms → 4123ms */
|
||||
meow_lyric_parse(lrc, NULL);
|
||||
CHECK(meow_lyric_line_time(0) == 1500, "1 位小数 → 1500ms");
|
||||
CHECK(meow_lyric_line_time(1) == 2250, "2 位小数 → 2250ms");
|
||||
CHECK(meow_lyric_line_time(2) == 3456, "3 位小数 → 3456ms");
|
||||
CHECK(meow_lyric_line_time(3) == 4123, ">3 位小数截断 → 4123ms");
|
||||
/* 元数据行 [ti:..] 应跳过,不计入行(与歌词须在独立行;ncm 语义:首个非时间标签即令该行失效) */
|
||||
meow_lyric_parse("[ti:Test]\n[ar:Someone]\n[00:01.00]x\n", NULL);
|
||||
CHECK(meow_lyric_count() == 1, "元数据 [ti:..]/[ar:..] 行不被当作歌词");
|
||||
}
|
||||
|
||||
static void t7_oob(void)
|
||||
{
|
||||
printf("T7 越界访问\n");
|
||||
meow_lyric_parse("[00:01.00]x\n[00:02.00]y\n", NULL);
|
||||
CHECK(strcmp(meow_lyric_line_text(-1), "") == 0, "line_text(-1)==\"\"");
|
||||
CHECK(strcmp(meow_lyric_line_text(99), "") == 0, "line_text(99)==\"\"");
|
||||
CHECK(strcmp(meow_lyric_line_tl(-1), "") == 0, "line_tl(-1)==\"\"");
|
||||
CHECK(strcmp(meow_lyric_line_tl(99), "") == 0, "line_tl(99)==\"\"");
|
||||
CHECK(meow_lyric_line_time(-1) == -1, "line_time(-1)==-1");
|
||||
CHECK(meow_lyric_line_time(99) == -1, "line_time(99)==-1");
|
||||
}
|
||||
|
||||
static void t8_clear_and_empty(void)
|
||||
{
|
||||
printf("T8 clear / 空输入\n");
|
||||
meow_lyric_parse("[00:01.00]x\n", NULL);
|
||||
meow_lyric_clear();
|
||||
CHECK(meow_lyric_count() == 0, "clear 后 count==0");
|
||||
CHECK(strcmp(meow_lyric_line_text(0), "") == 0, "clear 后 line_text(0)==\"\"");
|
||||
CHECK(meow_lyric_line_time(0) == -1, "clear 后 line_time(0)==-1");
|
||||
CHECK(meow_lyric_parse(NULL, NULL) == 0, "parse(NULL,NULL)→0");
|
||||
CHECK(meow_lyric_count() == 0, "空解析后 count==0");
|
||||
}
|
||||
|
||||
static void t9_wrap(void)
|
||||
{
|
||||
printf("T9 折行 meow_lyric_wrap\n");
|
||||
char seg[8][MEOW_LYRIC_SEG_SZ];
|
||||
|
||||
/* 中西文:英文按词断行、中文按字符断行 → "Hello" / "世界" 两段 */
|
||||
int r = meow_lyric_wrap("Hello 世界", 28, 6, seg, 8, uw, NULL);
|
||||
CHECK(r == 2, "中西文折为 2 段");
|
||||
CHECK(strcmp(seg[0], "Hello") == 0, "seg0==Hello(词边界断)");
|
||||
CHECK(strcmp(seg[1], "世界") == 0, "seg1==世界(中文按字符)");
|
||||
|
||||
/* max_rows 截断:每单词一 bar,max_rows=3 → 只出 3 段 */
|
||||
r = meow_lyric_wrap("a b c d e f g", 28, 1, seg, 3, uw, NULL);
|
||||
CHECK(r == 3, "max_rows=3 截断为 3 段");
|
||||
|
||||
/* 单字符超宽(maxw=0):能放一个字符且不死循环,返回完整逐字符段 */
|
||||
r = meow_lyric_wrap("abc", 28, 0, seg, 5, uw, NULL);
|
||||
CHECK(r == 3, "maxw=0 逐字符折为 3 段(无死循环)");
|
||||
CHECK(strcmp(seg[0], "a") == 0 && strcmp(seg[2], "c") == 0, "seg0==a / seg2==c");
|
||||
|
||||
/* width 回调 NULL → 字节近似兜底(maxw 作字节预算) */
|
||||
r = meow_lyric_wrap("abcd", 28, 2, seg, 4, NULL, NULL);
|
||||
CHECK(r == 2, "width=NULL 按字节近似折为 2 段");
|
||||
CHECK(strcmp(seg[0], "ab") == 0 && strcmp(seg[1], "cd") == 0, "seg0==ab / seg1==cd");
|
||||
|
||||
/* 空/超短:txt 空 → 1 段空串 */
|
||||
r = meow_lyric_wrap("", 28, 10, seg, 4, uw, NULL);
|
||||
CHECK(r == 1 && seg[0][0] == 0, "空文本 → 1 段空串");
|
||||
CHECK(meow_lyric_wrap(NULL, 28, 10, seg, 4, uw, NULL) == 1, "NULL 文本 → 1 段");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
printf("== meow_lyric 宿主单测 ==\n");
|
||||
t1_basic();
|
||||
t2_line_at();
|
||||
t3_translation();
|
||||
t4_literal_newline();
|
||||
t5_limit();
|
||||
t6_timefrac();
|
||||
t7_oob();
|
||||
t8_clear_and_empty();
|
||||
t9_wrap();
|
||||
printf("%s\n", g_fail ? "== FAIL ==" : "== PASS ==");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
/*
|
||||
* test_meow_lyric_view.c — meow_lyric_view(SDK 通用歌词页控件)宿主单测(2026-09-03,批2)。
|
||||
*
|
||||
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global;
|
||||
* 另链批1 meow_lyric.c 提供歌词数据;meow_lyric_view.c 为被测控件):
|
||||
* gcc -Wall -Wextra -o test_meow_lyric_view test_meow_lyric_view.c test_host_stub.c \
|
||||
* meow_lyric.c meow_lyric_view.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 meow_ui_nav.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 运行:./test_meow_lyric_view
|
||||
*
|
||||
* 覆盖矩阵(对齐任务批2:活动行纯白/灰阶、翻译行占位不叠行、偏上锚定、折行中西文、点行 seek 命中、
|
||||
* 主题换色、scrim 像素渲染):
|
||||
* L1 活动行纯白 + 上下行逐级灰阶单调递减(dark 主题:onSurface=0xFFFFFF 纯白)
|
||||
* L2 翻译行占位不叠行:3 行原文+翻译 → 活动行块高 bh=rw+1,下一块始于翻译行之后(块间无重叠)
|
||||
* L3 偏上锚定:活动行原文块中心 = 视图中线 - above_off(严格偏上)
|
||||
* L4 折行中西文:长行折成多段(rw>1),且块间按子行高排列
|
||||
* L5 点行 seek 命中:命中=布局一致(点块内/块外就近取整;点翻译行归本行不误触下一行)
|
||||
* L6 主题换色:set_colors 覆盖活动行/灰阶/背景;NULL 恢复主题默认
|
||||
* L7 scrim 像素渲染:draw 后背底像素≈scrim 色(fill_rect_alpha 真实落地)
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h" /* meow_ui_back_buf/pitch/bpp:真实像素断言 */
|
||||
#include "meow_lyric_view.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
|
||||
static int g_fail = 0;
|
||||
#define CHECK(cond, name) do { \
|
||||
if (cond) printf(" PASS %s\n", name); \
|
||||
else { printf(" FAIL %s (%s:%d)\n", name, __FILE__, __LINE__); g_fail++; } } while (0)
|
||||
|
||||
/* on_seek 回调收(验证 widget hit 路径回调接线)。 */
|
||||
static int64_t g_seek_ms = -1;
|
||||
static void seek_cb(int64_t ms, void *ctx) { (void)ctx; g_seek_ms = ms; }
|
||||
|
||||
/* ---- 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);
|
||||
}
|
||||
|
||||
/* 折行宽测量(宿主):ASCII=1 单位,CJK 多字节=2 单位(中西文断行用)。 */
|
||||
static int lw(const char *u, int size, void *ctx)
|
||||
{
|
||||
(void)size; (void)ctx;
|
||||
int w = 0;
|
||||
for (const char *p = u; *p; ) {
|
||||
unsigned char c = (unsigned char)*p;
|
||||
int ch = (c < 0x80) ? 1 : (c < 0xE0) ? 2 : (c < 0xF0) ? 3 : 4;
|
||||
w += (c < 0x80) ? 1 : 2;
|
||||
p += ch;
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
/* 亮度(灰度单调性判断用)。 */
|
||||
static int lum(uint32_t c)
|
||||
{
|
||||
return ((c >> 16) & 255) * 3 + ((c >> 8) & 255) * 6 + (c & 255);
|
||||
}
|
||||
|
||||
/*
|
||||
* 测试歌词:7 行原创 + 两行翻译(与原文同戳合并)。下标:
|
||||
* 0 First(1000) / 1 Second(2000,tl=第二句翻译) / 2 Third(3000) / 3 Fourth(4000)
|
||||
* 4 Fifth(5000,tl=第五句翻译长文本折叠测试啊啊啊) / 5 Sixth(6000) / 6 Seventh(7000)
|
||||
*/
|
||||
static void load_lyric(void)
|
||||
{
|
||||
const char *lrc =
|
||||
"[00:01.00]First\n"
|
||||
"[00:02.00]Second\n"
|
||||
"[00:03.00]Third\n"
|
||||
"[00:04.00]Fourth\n"
|
||||
"[00:05.00]Fifth\n"
|
||||
"[00:06.00]Sixth\n"
|
||||
"[00:07.00]Seventh\n";
|
||||
const char *tlrc =
|
||||
"[00:02.00]\u7b2c\u4e8c\u53e5\u7ffb\u8bd1\n"
|
||||
"[00:05.00]\u7b2c\u4e94\u53e5\u7ffb\u8bd1\u957f\u6587\u672c\u6298\u53e0\u6d4b\u8bd5\u554a\u554a\u554a\n";
|
||||
int n = meow_lyric_parse(lrc, tlrc);
|
||||
assert(n == 7);
|
||||
/* 翻译合并校验(批1 语义,作前置保证) */
|
||||
assert(strcmp(meow_lyric_line_tl(1), "\u7b2c\u4e8c\u53e5\u7ffb\u8bd1") == 0);
|
||||
assert(meow_lyric_line_tl(2)[0] == 0);
|
||||
}
|
||||
|
||||
static void l1_white_gray(void)
|
||||
{
|
||||
printf("L1 活动行纯白 + 灰阶单调递减\n");
|
||||
meow_theme_set_mode(MEOW_THEME_DARK);
|
||||
meow_lyric_view *lv = meow_lyric_view_create(0, 0, 480, 720, lw, NULL);
|
||||
assert(lv);
|
||||
const meow_theme *th = meow_theme_get();
|
||||
/* 活动行 = 主题 onSurface(dark=0xFFFFFF 纯白) */
|
||||
uint32_t cur = meow_lyric_view_cur_color(lv);
|
||||
CHECK(cur == th->onSurface, "cur == th->onSurface (dark=0xFFFFFF 纯白)");
|
||||
CHECK((cur & 0xFFFFFF) == 0xFFFFFF, "cur 为纯白 0xFFFFFF");
|
||||
CHECK(meow_lyric_view_line_color(lv, 0) == cur, "line_color(0)==cur");
|
||||
/* 灰阶单调递减(距活动行越远越灰) */
|
||||
int mono = 1;
|
||||
for (int d = 1; d <= 5; d++)
|
||||
if (lum(meow_lyric_view_line_color(lv, d)) >= lum(meow_lyric_view_line_color(lv, d - 1))) mono = 0;
|
||||
CHECK(mono, "gray dist 1..5 亮度单调递减");
|
||||
/* 最远灰阶(对齐 ncm 0x555555) */
|
||||
CHECK(meow_lyric_view_line_color(lv, 5) == 0x555555, "gray(5)==0x555555 (ncm lyr_gray_tbl)");
|
||||
meow_lyric_view_free(lv);
|
||||
}
|
||||
|
||||
static void l2_translation_block(void)
|
||||
{
|
||||
printf("L2 翻译行占位不叠行(3 行原文 + 翻译)\n");
|
||||
meow_theme_set_mode(MEOW_THEME_DARK);
|
||||
meow_lyric_view *lv = meow_lyric_view_create(0, 0, 480, 720, lw, NULL);
|
||||
assert(lv);
|
||||
meow_lyric_view_set_pos(lv, 2000); /* cur = idx1 Second(有翻译) */
|
||||
meow_lyric_view_vis vis[MEOW_LYRIC_VIEW_MAX_ROWS];
|
||||
int cnt = meow_lyric_view_visible(lv, 2000, vis, MEOW_LYRIC_VIEW_MAX_ROWS);
|
||||
CHECK(cnt > 0, "可见条目数 > 0");
|
||||
/* 找出当前行(idx1)所在块与下一块 */
|
||||
int ci = -1, next = -1;
|
||||
for (int k = 0; k < cnt; k++) {
|
||||
if (vis[k].ent == 1) ci = k;
|
||||
else if (ci >= 0 && next < 0) next = k; /* 当前块之后的第一块 */
|
||||
}
|
||||
CHECK(ci >= 0, "当前行(Second)在可见块中");
|
||||
CHECK(vis[ci].bh == vis[ci].rw + 1, "当前行块高 bh=rw+1(原文折行 + 翻译行占 1 子行)");
|
||||
if (next >= 0) {
|
||||
/* 下一块顶 = 当前块顶 + 当前块高×子行高(块间无重叠、翻译行不叠进下一行) */
|
||||
CHECK(vis[next].y == vis[ci].y + vis[ci].bh * 44, "下一块始于翻译行之后(无叠行)");
|
||||
} else {
|
||||
/* 当前行是最后可视块:其块高应把翻译行算进去(翻译行占位,否则行会被截断) */
|
||||
CHECK(vis[ci].y + vis[ci].bh * 44 <= 720, "当前块含翻译行(占用垂直空间)");
|
||||
}
|
||||
meow_lyric_view_free(lv);
|
||||
}
|
||||
|
||||
static void l3_above_anchor(void)
|
||||
{
|
||||
printf("L3 偏上锚定(活动行块中心在视图中线上方)\n");
|
||||
meow_theme_set_mode(MEOW_THEME_DARK);
|
||||
meow_lyric_view *lv = meow_lyric_view_create(0, 100, 480, 500, lw, NULL); /* 视图中线=350 */
|
||||
assert(lv);
|
||||
meow_lyric_view_set_pos(lv, 5000); /* cur = idx4 Fifth(5 行之后,上方充足) */
|
||||
meow_lyric_view_vis vis[MEOW_LYRIC_VIEW_MAX_ROWS];
|
||||
int cnt = meow_lyric_view_visible(lv, 5000, vis, MEOW_LYRIC_VIEW_MAX_ROWS);
|
||||
CHECK(cnt > 0, "可见条目数 > 0");
|
||||
int ci = -1;
|
||||
for (int k = 0; k < cnt; k++) if (vis[k].ent == 4) { ci = k; break; }
|
||||
CHECK(ci >= 0, "当前行(Fifth)在可见块中");
|
||||
/* 活动行原文块中心 = 块顶 + rw×子行高/2 = 视图中线 - above_off(严格偏上 50px) */
|
||||
int center = vis[ci].y + vis[ci].rw * 44 / 2;
|
||||
CHECK(center == 350 - 50, "活动行块中心=350-50=300(偏上锚定 above_off=50)");
|
||||
CHECK(center < 350, "活动行块中心在视图中线(350)偏上");
|
||||
meow_lyric_view_free(lv);
|
||||
}
|
||||
|
||||
static void l4_wrap_cjk_west(void)
|
||||
{
|
||||
printf("L4 折行中西文(长行折多段,块间按子行高排列)\n");
|
||||
meow_theme_set_mode(MEOW_THEME_DARK);
|
||||
/* 窄视图强制折行:w=120 → maxw=120-48=72 单位 */
|
||||
meow_lyric_view *lv = meow_lyric_view_create(0, 0, 120, 720, lw, NULL);
|
||||
assert(lv);
|
||||
/* 用长英文+中文混排行,使当前行折成 >1 段 */
|
||||
meow_lyric_parse(
|
||||
"[00:01.00]The quick brown fox jumps over the lazy dog and runs far away into the woods to find shelter from the cold rain today\n"
|
||||
"[00:05.00]\u4e2d\u6587\u4e2d\u6587\u4e2d\u6587\u4e2d\u6587\u4e2d\u6587\u4e2d\u6587\u4e2d\u6587\u4e2d\u6587\u4e2d\u6587\u8bba\u6587\u6d4b\u8bd5\u554a\u554a\u554a\u554a\n",
|
||||
NULL);
|
||||
meow_lyric_view_set_pos(lv, 1000);
|
||||
meow_lyric_view_vis vis[MEOW_LYRIC_VIEW_MAX_ROWS];
|
||||
int cnt = meow_lyric_view_visible(lv, 1000, vis, MEOW_LYRIC_VIEW_MAX_ROWS);
|
||||
CHECK(cnt >= 2, "该行折成多段(可见条目含上下行)");
|
||||
/* 当前行(idx0,长文本)应从 1 段折成多段 */
|
||||
int ci = -1;
|
||||
for (int k = 0; k < cnt; k++) if (vis[k].ent == 0) { ci = k; break; }
|
||||
CHECK(ci >= 0, "当前行在可见块中");
|
||||
CHECK(vis[ci].rw > 1, "长行折成 rw>1 段(中西文按 词/字符 断行)");
|
||||
/* 块内子行间距为 sub_lh:子行间不重叠(块高=rw 子行) */
|
||||
CHECK(vis[ci].bh == vis[ci].rw, "无翻译当前行 bh=rw(子行数=折行段数)");
|
||||
/* 恢复 7 行歌词(供后续用例) */
|
||||
load_lyric();
|
||||
meow_lyric_view_free(lv);
|
||||
}
|
||||
|
||||
static void l5_hit_line(void)
|
||||
{
|
||||
printf("L5 点行 seek 命中 = 布局一致\n");
|
||||
meow_theme_set_mode(MEOW_THEME_DARK);
|
||||
meow_lyric_view *lv = meow_lyric_view_create(0, 100, 480, 500, lw, NULL); /* 视图中线=350 */
|
||||
assert(lv);
|
||||
meow_lyric_view_set_pos(lv, 2000); /* cur = idx1 Second(有翻译) */
|
||||
meow_lyric_view_vis vis[MEOW_LYRIC_VIEW_MAX_ROWS];
|
||||
int cnt = meow_lyric_view_visible(lv, 2000, vis, MEOW_LYRIC_VIEW_MAX_ROWS);
|
||||
CHECK(cnt > 0, "可见条目数 > 0");
|
||||
/* 命中必须与绘制布局一致:点每个可见块内子行,命中该块 ent */
|
||||
int consistent = 1;
|
||||
for (int k = 0; k < cnt && consistent; k++) {
|
||||
int yb = vis[k].y, yh = vis[k].bh * 44;
|
||||
for (int dy = 0; dy < yh && consistent; dy += 4) { /* 块内等距采样 */
|
||||
int hit = meow_lyric_view_hit_line(lv, 240, yb + dy);
|
||||
if (hit != vis[k].ent) consistent = 0;
|
||||
}
|
||||
}
|
||||
CHECK(consistent, "命中=布局(块内逐点命中 ent 一致)");
|
||||
/* 点翻译行(当前行块内下一子行)归本行,不误触下一行 */
|
||||
int ci = -1;
|
||||
for (int k = 0; k < cnt; k++) if (vis[k].ent == 1) { ci = k; break; }
|
||||
CHECK(ci >= 0, "当前行在可见块中");
|
||||
if (ci >= 0 && vis[ci].bh > vis[ci].rw) { /* 有翻译行 */
|
||||
int ty = vis[ci].y + vis[ci].rw * 44 + 10; /* 翻译子行内 */
|
||||
CHECK(meow_lyric_view_hit_line(lv, 240, ty) == 1, "点翻译行归本行(Second)不误触下一行");
|
||||
}
|
||||
/* 点可见块外(视图内)→ 就近取整(顶部/底部可见行) */
|
||||
CHECK(meow_lyric_view_hit_line(lv, 240, 101) == vis[0].ent, "视图内顶缝→就近取第一可见块");
|
||||
CHECK(meow_lyric_view_hit_line(lv, 240, 599) == vis[cnt - 1].ent, "视图内底缝→就近取最后可见块");
|
||||
/* 视图外 → 未命中 */
|
||||
CHECK(meow_lyric_view_hit_line(lv, 240, 50) == -1, "视图外(y=50)未命中");
|
||||
CHECK(meow_lyric_view_hit_line(lv, 490, 300) == -1, "视图外(x=490)未命中");
|
||||
/* widget 命中路径:命中当前行(有翻译)→ 触发 on_seek 回调 line_time(1)=2000 */
|
||||
g_seek_ms = -1;
|
||||
meow_lyric_view_set_on_seek(lv, seek_cb, NULL);
|
||||
if (ci >= 0) {
|
||||
int cy = vis[ci].y + 2; /* 当前行原文块内 */
|
||||
CHECK(meow_widget_hit(meow_lyric_view_widget(lv), 240, cy) == 1, "widget hit 命中当前行");
|
||||
CHECK(g_seek_ms == 2000, "on_seek 回调 line_time(Second)=2000");
|
||||
} else {
|
||||
CHECK(1, "on_seek 校验跳过(无当前行块)");
|
||||
}
|
||||
meow_lyric_view_free(lv);
|
||||
}
|
||||
|
||||
static void l6_theme_switch(void)
|
||||
{
|
||||
printf("L6 主题换色(set_colors 覆盖 / NULL 恢复默认)\n");
|
||||
meow_theme_set_mode(MEOW_THEME_DARK);
|
||||
meow_lyric_view *lv = meow_lyric_view_create(0, 0, 480, 720, lw, NULL);
|
||||
assert(lv);
|
||||
uint32_t def_cur = meow_lyric_view_cur_color(lv); /* dark=0xFFFFFF */
|
||||
/* 覆盖:活动行红、灰阶自定义、scrim 自定义 */
|
||||
meow_lyric_view_colors c; memset(&c, 0, sizeof c);
|
||||
c.scrim = 0x1E1E22; c.cur = 0xFF0000; c.scrim_alpha = 255;
|
||||
c.gray[0] = 0xFF0000; c.gray[1] = 0xCC0000; c.gray[2] = 0x990000;
|
||||
c.gray[3] = 0x660000; c.gray[4] = 0x440000; c.gray[5] = 0x220000;
|
||||
meow_lyric_view_set_colors(lv, &c);
|
||||
CHECK(meow_lyric_view_cur_color(lv) == 0xFF0000, "set_colors 覆盖活动行为红");
|
||||
CHECK(meow_lyric_view_line_color(lv, 3) == 0x660000, "set_colors 覆盖灰阶(3)=0x660000");
|
||||
CHECK(meow_lyric_view_line_color(lv, 5) == 0x220000, "set_colors 覆盖灰阶(5)=0x220000");
|
||||
/* 恢复默认:NULL → 回主题 onSurface 纯白 */
|
||||
meow_lyric_view_set_colors(lv, NULL);
|
||||
CHECK(meow_lyric_view_cur_color(lv) == def_cur, "set_colors(NULL) 恢复主题默认");
|
||||
meow_lyric_view_free(lv);
|
||||
}
|
||||
|
||||
static void l7_scrim_pixel(void)
|
||||
{
|
||||
printf("L7 scrim 像素渲染(draw 落地)\n");
|
||||
meow_theme_set_mode(MEOW_THEME_DARK);
|
||||
meow_lyric_view *lv = meow_lyric_view_create(0, 0, 480, 720, lw, NULL);
|
||||
assert(lv);
|
||||
/* 不透明 scrim → 背底像素应为 scrim 色(RGB565 舍入容差) */
|
||||
meow_lyric_view_colors c; memset(&c, 0, sizeof c);
|
||||
c.scrim = 0x203040; c.scrim_alpha = 255;
|
||||
meow_lyric_view_set_colors(lv, &c);
|
||||
meow_lyric_view_set_pos(lv, 2000);
|
||||
static const char *lrc =
|
||||
"[00:01.00]First\n[00:02.00]Second\n[00:03.00]Third\n[00:04.00]Fourth\n";
|
||||
meow_lyric_parse(lrc, NULL);
|
||||
meow_ui_clear(0);
|
||||
meow_widget *w = meow_lyric_view_widget(lv);
|
||||
meow_widget_draw(w);
|
||||
CHECK(px_near(240, 350, 0x20, 0x30, 0x40), "视图中线像素≈scrim 0x203040(文本打桩不影响 scrim)");
|
||||
CHECK(px_near(50, 50, 0x20, 0x30, 0x40), "视图左上像素≈scrim 0x203040");
|
||||
meow_lyric_view_free(lv);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
setbuf(stdout, NULL);
|
||||
setenv("MEOW_UI_DUMP", "/tmp/lyricview.ppm", 1);
|
||||
meow_ui_opts o = { 480, 720 };
|
||||
assert(meow_ui_init(&o) == 0);
|
||||
meow_ui_activity_push(0x53);
|
||||
load_lyric();
|
||||
|
||||
l1_white_gray();
|
||||
l2_translation_block();
|
||||
l3_above_anchor();
|
||||
l4_wrap_cjk_west();
|
||||
l5_hit_line();
|
||||
l6_theme_switch();
|
||||
l7_scrim_pixel();
|
||||
|
||||
meow_ui_activity_pop();
|
||||
printf(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS\n", g_fail);
|
||||
return g_fail;
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
/* test_meow_md3.c — 宿主验证 MD3(Material You)对齐修复(agent-434 P0/P1/P3):
|
||||
* P0 listview 容器模式(默认 renderer:行=surfaceContainer 圆角卡 + 行间空隙 + 去分隔线);
|
||||
* P1 按钮胶囊化(圆角=h/2);
|
||||
* P3 Switch off-track/off-thumb、NavigationBar active pill 64×32、Checkbox/Radio 视觉尺寸。
|
||||
* 断言全部「画完读 back_buf 像素」(region_has/px_near),不依赖 /dev/fb0/字库(meow_font 打桩)。
|
||||
*
|
||||
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global):
|
||||
* gcc -Wall -Wextra -o test_meow_md3 test_meow_md3.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 meow_ui_nav.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 运行:./test_meow_md3
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h" /* meow_ui_back_buf/pitch/bpp */
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
|
||||
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 <= 12; }
|
||||
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);
|
||||
}
|
||||
#define UNPK(c, rr, gg, bb) do { (rr) = ((c) >> 16) & 0xFF; (gg) = ((c) >> 8) & 0xFF; (bb) = (c) & 0xFF; } while (0)
|
||||
|
||||
/* Listview src2 数据源:2 行普通富 item(无图标、无右尾件,label-only)。 */
|
||||
typedef struct { const char *title; const char *sub; int right; int enabled; } trow_t;
|
||||
static trow_t g_rows[2] = {
|
||||
{ "Row 0", NULL, MEOW_LIST_RIGHT_NONE, 1 },
|
||||
{ "Row 1", NULL, MEOW_LIST_RIGHT_NONE, 1 },
|
||||
};
|
||||
static int trow_src(int idx, meow_list_item *out, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
if (idx < 0 || idx >= 2) return 0;
|
||||
out->icon_kind = MEOW_LIST_ICON_NONE;
|
||||
out->title = g_rows[idx].title;
|
||||
out->subtitle = g_rows[idx].sub;
|
||||
out->right = g_rows[idx].right;
|
||||
out->on = 0;
|
||||
out->enabled = g_rows[idx].enabled;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
setenv("MEOW_UI_DUMP", "/tmp/md3.ppm", 1);
|
||||
meow_ui_opts o = { 480, 720 };
|
||||
assert(meow_ui_init(&o) == 0);
|
||||
assert(meow_ui_width() == 480 && meow_ui_height() == 720);
|
||||
const meow_theme *th = meow_theme_get();
|
||||
int sf_r, sf_g, sf_b, sc_r, sc_g, sc_b, p_r, p_g, p_b;
|
||||
int pc_r, pc_g, pc_b, ov_r, ov_g, ov_b, sch_r, sch_g, sch_b, scs_r, scs_g, scs_b;
|
||||
UNPK(th->surface, sf_r, sf_g, sf_b);
|
||||
UNPK(th->surfaceContainer, sc_r, sc_g, sc_b);
|
||||
UNPK(th->primary, p_r, p_g, p_b);
|
||||
UNPK(th->primaryContainer, pc_r, pc_g, pc_b);
|
||||
UNPK(th->outline, ov_r, ov_g, ov_b);
|
||||
UNPK(th->surfaceContainerHighest, sch_r, sch_g, sch_b);
|
||||
UNPK(th->secondaryContainer, scs_r, scs_g, scs_b);
|
||||
|
||||
/* ---- P0 listview 容器模式:行=surfaceContainer 圆角卡 + 空隙,去分隔线 ---- */
|
||||
meow_widget *lv = meow_listview_create(20, 240, 320, 112, 2, trow_src, NULL, MEOW_LIST_SEL_SINGLE);
|
||||
assert(lv);
|
||||
assert(meow_listview_set_item_h(lv, 56) == 56);
|
||||
assert(meow_listview_set_container(lv, 1) == 0);
|
||||
meow_ui_clear(th->surface);
|
||||
meow_widget_draw(lv);
|
||||
/* 行 0 卡(x=28,y=244,w=304,h=48):中心=surfaceContainer;行 1 同。 */
|
||||
assert(px_near(180, 268, sc_r, sc_g, sc_b));
|
||||
assert(px_near(180, 324, sc_r, sc_g, sc_b));
|
||||
/* 卡间空隙(y=292..300):surface(行间分隔用空隙)。 */
|
||||
assert(px_near(180, 296, sf_r, sf_g, sf_b));
|
||||
/* 去分隔线:行边界不再画 outlineVariant(改空隙代替)。 */
|
||||
assert(!px_near(180, 292, ov_r, ov_g, ov_b));
|
||||
/* 选中行 0 → primaryContainer(tonal,非满幅实心)。 */
|
||||
meow_listview_set_sel(lv, 0);
|
||||
meow_ui_clear(th->surface);
|
||||
meow_widget_draw(lv);
|
||||
assert(px_near(180, 268, pc_r, pc_g, pc_b));
|
||||
assert(px_near(180, 324, sc_r, sc_g, sc_b)); /* 行 1 未选仍卡 */
|
||||
printf("PASS listview container: rounded card + gap + no-divider + tonal select\n");
|
||||
|
||||
/* ---- P1 按钮胶囊化:圆角=h/2(56/2=28)---- */
|
||||
meow_widget *btn = meow_btn_create_variant(10, 10, 200, 56, "FILLED", MEOW_BTN_FILLED);
|
||||
assert(btn);
|
||||
meow_ui_clear(th->surface);
|
||||
meow_widget_draw(btn);
|
||||
assert(px_near(110, 22, p_r, p_g, p_b)); /* 上部(避开文字)=primary */
|
||||
/* 顶缘中心在圆角胶囊内(仍 primary);角落(圆角外)回 surface。 */
|
||||
assert(px_near(110, 13, p_r, p_g, p_b));
|
||||
assert(px_near(12, 12, sf_r, sf_g, sf_b)); /* 角落被圆角切掉 */
|
||||
/* 同高矮按钮(h=40 → 圆角 20):上部仍 primary,角落 surface。 */
|
||||
meow_widget *btn2 = meow_btn_create_variant(10, 120, 120, 40, "B", MEOW_BTN_FILLED);
|
||||
assert(btn2);
|
||||
meow_ui_clear(th->surface);
|
||||
meow_widget_draw(btn2);
|
||||
assert(px_near(70, 125, p_r, p_g, p_b));
|
||||
assert(px_near(12, 122, sf_r, sf_g, sf_b));
|
||||
printf("PASS button: capsule radius = h/2\n");
|
||||
|
||||
/* ---- P3 Switch off-track/off-thumb ---- */
|
||||
meow_widget *sw = meow_switch_create(20, 300, 0); /* off */
|
||||
assert(sw);
|
||||
meow_ui_clear(th->surface);
|
||||
meow_widget_draw(sw);
|
||||
assert(px_near(60, 316, sch_r, sch_g, sch_b)); /* off-track=surfaceContainerHighest */
|
||||
assert(px_near(36, 316, sf_r, sf_g, sf_b)); /* off-thumb=surface(表面色,非恒白) */
|
||||
meow_switch_set(sw, 1);
|
||||
meow_ui_clear(th->surface);
|
||||
meow_widget_draw(sw);
|
||||
assert(px_near(28, 316, p_r, p_g, p_b)); /* on-track=primary(thumb 左侧) */
|
||||
printf("PASS switch: off-track surfaceContainerHighest / off-thumb surface\n");
|
||||
|
||||
/* ---- P3 NavigationBar active pill 64×32 ---- */
|
||||
meow_widget *nb = meow_navigationbar_create(0, 600, 480, 80);
|
||||
assert(nb);
|
||||
meow_navigationbar_add_item(nb, "A");
|
||||
meow_navigationbar_add_item(nb, "B");
|
||||
meow_navigationbar_add_item(nb, "C");
|
||||
meow_navigationbar_add_item(nb, "D");
|
||||
meow_navigationbar_set_selected(nb, 0);
|
||||
meow_ui_clear(th->surface);
|
||||
meow_widget_draw(nb);
|
||||
/* item0 pill 中心 (ix+60, y+40)=(60,640) — 64×32 居中于栏;pill 内=secondaryContainer。 */
|
||||
assert(px_near(60, 640, scs_r, scs_g, scs_b));
|
||||
/* pill 外(栏上部)= surfaceContainer(navbar 容器底)。 */
|
||||
assert(px_near(60, 610, sc_r, sc_g, sc_b));
|
||||
printf("PASS navbar: active pill 64x32 (was 48x22)\n");
|
||||
|
||||
/* ---- P3 Checkbox 视觉 24dp(原 34 偏大):勾选框视觉居中,角落回 surface ---- */
|
||||
meow_widget *ck = meow_checkbox_create(150, 60);
|
||||
assert(ck);
|
||||
meow_ui_clear(th->surface);
|
||||
meow_widget_draw(ck);
|
||||
/* 未选:24dp 描边框(左缘=150+5=155);框边=outline;内部=surface(空心)。 */
|
||||
assert(px_near(155, 77, ov_r, ov_g, ov_b)); /* 框左缘 stroke */
|
||||
assert(px_near(167, 77, sf_r, sf_g, sf_b)); /* 内部空心 */
|
||||
meow_widget_set_selected(ck, 1);
|
||||
meow_ui_clear(th->surface);
|
||||
meow_widget_draw(ck);
|
||||
/* 选中:24dp 实心 primary 居中(左缘 155..179 顶 65..89);控件矩形角落(视觉 24 外)不填。 */
|
||||
assert(px_near(150, 60, sf_r, sf_g, sf_b)); /* 视觉 24 外 → surface */
|
||||
assert(px_near(167, 77, p_r, p_g, p_b)); /* 视觉中心=primary */
|
||||
printf("PASS checkbox: 24dp visual (was 34)\n");
|
||||
|
||||
meow_widget_free(lv); meow_widget_free(btn); meow_widget_free(btn2);
|
||||
meow_widget_free(sw); meow_widget_free(nb); meow_widget_free(ck);
|
||||
printf("ALL MD3 PASS\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/* test_meow_time.c — 宿主单测:SDK 时钟换算(meow_ui_time.c / meow_time_ms_from_parts)。
|
||||
*
|
||||
* 背景(M11/M21):mipsel 真机上 meow_ui_time_ms() 曾返回「低 32 位=秒×1000、高 32 位随机」,
|
||||
* 根因是 zig musl 头(time_t 64 位)与 libc 实现(o32 time32 取数)ABI 不一致,见
|
||||
* sdk/CLOCK_ABI.md。修复后取数统一走 meow_time_ms_from_parts(sec, nsec),本测试把
|
||||
* 换算边界(毫秒进位/截断/大秒数/负值回绕)与「真时钟单调且前进」固定在宿主上。
|
||||
*
|
||||
* 注意:宿主(非 mips)编译时 meow_ui_time.c 走 MEOW_TIME32_KERNEL_ABI=0 分支,即普通
|
||||
* clock_gettime —— 本测试**不能**证明 mipsel o32 路径正确(那条路径只靠反汇编证据)。
|
||||
*
|
||||
* 编译(在 sdk/ 目录下):
|
||||
* gcc -Os -Wall -Wextra -I. -Iinclude -c meow_ui_time.c -o meow_ui_time_host.o
|
||||
* gcc -Os -Wall -Wextra -I. -Iinclude test_meow_time.c meow_ui_time_host.o -o test_meow_time
|
||||
* 运行:./test_meow_time (exit 0=PASS / 1=FAIL)
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
|
||||
static int g_fail = 0;
|
||||
static unsigned long g_checks = 0;
|
||||
|
||||
static void eq_ms(const char *name, int64_t sec, int64_t nsec, uint64_t want)
|
||||
{
|
||||
uint64_t got = meow_time_ms_from_parts(sec, nsec);
|
||||
g_checks++;
|
||||
if (got != want) {
|
||||
printf("FAIL %s: sec=%lld nsec=%lld got=%llu want=%llu\n",
|
||||
name, (long long)sec, (long long)nsec,
|
||||
(unsigned long long)got, (unsigned long long)want);
|
||||
g_fail = 1;
|
||||
} else {
|
||||
printf("ok %s = %llu\n", name, (unsigned long long)got);
|
||||
}
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
/* ---- 1) 换算边界:毫秒 = sec*1000 + nsec/1e6(整除截断) ---- */
|
||||
eq_ms("零", 0, 0, 0);
|
||||
eq_ms("(sec,0) 整秒", 1, 0, 1000);
|
||||
eq_ms("999999ns 截断", 0, 999999, 0);
|
||||
eq_ms("1ms 进位", 0, 1000000, 1);
|
||||
eq_ms("1ns 截断", 0, 1, 0);
|
||||
eq_ms("开机 1135.008s", 1135, 8000000, 1135008);
|
||||
eq_ms("跨秒 1.999999999s", 1, 999999999, 1999);
|
||||
eq_ms("开机 1204.999999999s", 1204, 999999999, 1204999);
|
||||
eq_ms("接近 2038", 2147483647, 999999999, 2147483647999ULL);
|
||||
|
||||
/* ---- 2) 负值:from_parts 只做无符号升位((uint64_t)sec*1000 按二补回绕),不钳制。
|
||||
* 真正把内核 time32 的负 sec 钳成 0 的是 meow_ui_time.c 的 time32_clamp(),它只在
|
||||
* mipsel 分支编译、宿主不可达;这里把 from_parts 的回绕语义钉住,防止有人悄悄把它
|
||||
* 理解成「负值 = 0」而改掉换算。 ---- */
|
||||
eq_ms("负 sec 回绕(-1s)", -1, 0, 0xFFFFFFFFFFFFFC18ULL);
|
||||
eq_ms("负 sec+1ms 回绕", -1, 1000000, 0xFFFFFFFFFFFFFC19ULL);
|
||||
|
||||
/* ---- 3) 真时钟:非零、单调不减、量级合理(不上溢成「高 32 随机」) ---- */
|
||||
uint64_t prev = meow_ui_time_ms();
|
||||
g_checks++;
|
||||
if (prev == 0) {
|
||||
printf("FAIL 时钟返回 0(clock_gettime 失败?)\n");
|
||||
g_fail = 1;
|
||||
}
|
||||
g_checks++;
|
||||
if (prev >= (1ULL << 40)) {
|
||||
printf("FAIL 时钟量级异常(>=2^40 ms):%llu\n", (unsigned long long)prev);
|
||||
g_fail = 1;
|
||||
}
|
||||
/* 有界采样(避免时钟停走时测试挂死):要求单调不减且确实前进 */
|
||||
int advanced = 0;
|
||||
for (int i = 0; i < 200000 && !advanced; i++) {
|
||||
uint64_t now = meow_ui_time_ms();
|
||||
if (now < prev) {
|
||||
printf("FAIL 单调倒退:%llu -> %llu\n",
|
||||
(unsigned long long)prev, (unsigned long long)now);
|
||||
g_fail = 1;
|
||||
break;
|
||||
}
|
||||
advanced = now > prev;
|
||||
prev = now;
|
||||
}
|
||||
g_checks++;
|
||||
if (!advanced) {
|
||||
printf("FAIL 时钟未前进(200000 次采样内无变化)\n");
|
||||
g_fail = 1;
|
||||
} else {
|
||||
printf("ok 时钟前进且单调\n");
|
||||
}
|
||||
|
||||
/* ---- 4) 动画常用差值:两次取样的差 = 毫秒(不能是 0 恒等/整千) ---- */
|
||||
uint64_t t0 = meow_ui_time_ms();
|
||||
int waited = 0;
|
||||
for (int i = 0; i < 200000 && !waited; i++)
|
||||
waited = meow_ui_time_ms() != t0;
|
||||
uint64_t t1 = meow_ui_time_ms();
|
||||
g_checks++;
|
||||
if (!waited || t1 <= t0 || t1 - t0 > 5000) {
|
||||
printf("FAIL 取样差值异常:%llu -> %llu\n",
|
||||
(unsigned long long)t0, (unsigned long long)t1);
|
||||
g_fail = 1;
|
||||
} else {
|
||||
printf("ok 取样差值 %llu ms\n", (unsigned long long)(t1 - t0));
|
||||
}
|
||||
|
||||
printf(g_fail ? "== FAIL ==\n" : "== PASS ==\n");
|
||||
printf("checks=%lu\n", g_checks);
|
||||
return g_fail;
|
||||
}
|
||||
@@ -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 meow_ui_nav.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 运行:./test_meow_ui_anim
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h" /* meow_ui_back_buf 等(脏区 flush 值域校验) */
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int g_pass = 0, g_fail = 0;
|
||||
#define CHECK(cond, ...) do { \
|
||||
if (cond) { g_pass++; } \
|
||||
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
|
||||
} while (0)
|
||||
|
||||
#define NEAR(a, b) (((a)-(b)) < 0.0001f && ((b)-(a)) < 0.0001f)
|
||||
|
||||
/* ---- 缓动数值 ---- */
|
||||
static void test_ease(void)
|
||||
{
|
||||
CHECK(NEAR(meow_ease_apply(MEOW_EASE_LINEAR, 0.0f), 0.0f), "lin 0");
|
||||
CHECK(NEAR(meow_ease_apply(MEOW_EASE_LINEAR, 0.5f), 0.5f), "lin .5");
|
||||
CHECK(NEAR(meow_ease_apply(MEOW_EASE_LINEAR, 1.0f), 1.0f), "lin 1");
|
||||
CHECK(NEAR(meow_ease_apply(MEOW_EASE_OUT, 0.0f), 0.0f), "out 0");
|
||||
CHECK(NEAR(meow_ease_apply(MEOW_EASE_OUT, 1.0f), 1.0f), "out 1");
|
||||
CHECK(meow_ease_apply(MEOW_EASE_OUT, 0.5f) > 0.5f && meow_ease_apply(MEOW_EASE_OUT, 0.5f) < 1.0f, "out .5 between");
|
||||
CHECK(NEAR(meow_ease_apply(MEOW_EASE_IN_OUT, 0.0f), 0.0f), "inout 0");
|
||||
CHECK(NEAR(meow_ease_apply(MEOW_EASE_IN_OUT, 0.5f), 0.5f), "inout .5");
|
||||
CHECK(NEAR(meow_ease_apply(MEOW_EASE_IN_OUT, 1.0f), 1.0f), "inout 1");
|
||||
CHECK(NEAR(meow_ease_apply(MEOW_EASE_OVERSHOOT, 0.0f), 0.0f), "overshoot 0");
|
||||
CHECK(NEAR(meow_ease_apply(MEOW_EASE_OVERSHOOT, 1.0f), 1.0f), "overshoot 1");
|
||||
CHECK(meow_ease_apply(MEOW_EASE_OVERSHOOT, 0.85f) > 1.0f, "overshoot mid >1");
|
||||
}
|
||||
|
||||
/* ---- update/done 记录 ---- */
|
||||
typedef struct { meow_anim *a; float last; int updates; int done; int canceled; } anim_track;
|
||||
static void on_update(meow_anim *a, float v, void *ctx)
|
||||
{ anim_track *t = ctx; t->last = v; t->updates++; }
|
||||
static void on_done(meow_anim *a, int canceled, void *ctx)
|
||||
{ anim_track *t = ctx; t->a = a; t->done++; t->canceled = canceled; }
|
||||
|
||||
static void test_tween_complete(void)
|
||||
{
|
||||
anim_track t; memset(&t, 0, sizeof t);
|
||||
meow_anim_desc d; memset(&d, 0, sizeof d);
|
||||
d.from = 0; d.to = 100; d.duration_ms = 200; d.ease = MEOW_EASE_LINEAR;
|
||||
d.update = on_update; d.done = on_done; d.ctx = &t;
|
||||
meow_anim *a = meow_anim_start(&d);
|
||||
CHECK(a != NULL, "start");
|
||||
CHECK(meow_anim_active(a) == 1, "active at start");
|
||||
CHECK(NEAR(meow_anim_value(a), 0.0f), "value from");
|
||||
CHECK(t.updates == 1 && NEAR(t.last, 0.0f), "update t0");
|
||||
meow_anim_tick(); /* 与 start 同时刻:仍 ~0 */
|
||||
CHECK(t.done == 0, "not done yet");
|
||||
usleep(100000); meow_anim_tick(); /* 中点 */
|
||||
CHECK(t.last > 10.0f && t.last < 90.0f, "mid value %f", t.last);
|
||||
usleep(150000); meow_anim_tick(); /* 超过 duration */
|
||||
CHECK(t.done == 1 && t.canceled == 0, "done once non-cancel");
|
||||
CHECK(NEAR(t.last, 100.0f), "final value 100 got %f", t.last);
|
||||
CHECK(meow_anim_active(a) == 0, "inactive after done");
|
||||
usleep(10000); meow_anim_tick();
|
||||
CHECK(t.done == 1, "no re-done");
|
||||
}
|
||||
|
||||
static void test_cancel(void)
|
||||
{
|
||||
anim_track t; memset(&t, 0, sizeof t);
|
||||
meow_anim_desc d; memset(&d, 0, sizeof d);
|
||||
d.from = 0; d.to = 100; d.duration_ms = 500; d.ease = MEOW_EASE_LINEAR;
|
||||
d.update = on_update; d.done = on_done; d.ctx = &t;
|
||||
meow_anim *a = meow_anim_start(&d);
|
||||
usleep(50000); meow_anim_tick();
|
||||
int updates_before = t.updates;
|
||||
meow_anim_cancel(a);
|
||||
CHECK(t.done == 1 && t.canceled == 1, "cancel done(canceled=1)");
|
||||
CHECK(meow_anim_active(a) == 0, "inactive after cancel");
|
||||
meow_anim_tick();
|
||||
CHECK(t.updates == updates_before, "no update after cancel");
|
||||
}
|
||||
|
||||
static void test_parallel(void)
|
||||
{
|
||||
anim_track t1, t2; memset(&t1, 0, sizeof t1); memset(&t2, 0, sizeof t2);
|
||||
meow_anim_desc a, b; memset(&a, 0, sizeof a); memset(&b, 0, sizeof b);
|
||||
a.from = 0; a.to = 10; a.duration_ms = 120; a.ease = MEOW_EASE_LINEAR; a.update = on_update; a.done = on_done; a.ctx = &t1;
|
||||
b.from = 100; b.to = 0; b.duration_ms = 120; b.ease = MEOW_EASE_LINEAR; b.update = on_update; b.done = on_done; b.ctx = &t2;
|
||||
meow_anim *x = meow_anim_start(&a);
|
||||
meow_anim *y = meow_anim_start(&b);
|
||||
CHECK(x && y, "parallel start");
|
||||
CHECK(meow_anim_count() == 2, "count 2");
|
||||
usleep(150000); meow_anim_tick();
|
||||
CHECK(t1.done == 1 && t2.done == 1, "both done");
|
||||
CHECK(NEAR(t1.last, 10.0f) && NEAR(t2.last, 0.0f), "endpoints");
|
||||
CHECK(meow_anim_count() == 0, "count 0");
|
||||
}
|
||||
|
||||
static void test_region_flush(void)
|
||||
{
|
||||
anim_track t; memset(&t, 0, sizeof t);
|
||||
meow_anim_desc d; memset(&d, 0, sizeof d);
|
||||
d.from = 0; d.to = 1; d.duration_ms = 80; d.ease = MEOW_EASE_LINEAR;
|
||||
d.update = on_update; d.done = on_done; d.ctx = &t;
|
||||
meow_anim *a = meow_anim_start(&d);
|
||||
meow_anim_set_region(a, 100, 200, 60, 40); /* 每 tick 只 flush 此区 */
|
||||
usleep(100000); meow_anim_tick();
|
||||
CHECK(t.done == 1, "region anim done");
|
||||
CHECK(t.updates >= 1, "region updates");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
meow_ui_opts o = { 480, 720 };
|
||||
if (meow_ui_init(&o) != 0) { printf("init failed\n"); return 1; }
|
||||
test_ease();
|
||||
test_tween_complete();
|
||||
test_cancel();
|
||||
test_parallel();
|
||||
test_region_flush();
|
||||
meow_ui_shutdown();
|
||||
printf("anim: %d pass / %d fail\n", g_pass, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -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 meow_ui_nav.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 运行:./test_meow_ui_box
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h" /* meow_widget_rect:真实几何断言 */
|
||||
#include "meow_ui_view.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static int g_pass = 0, g_fail = 0;
|
||||
#define CHECK(cond, ...) do { \
|
||||
if (cond) { g_pass++; } \
|
||||
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
|
||||
} while (0)
|
||||
|
||||
static void wr(meow_widget *w, int out[4]) { meow_widget_rect(w, out); }
|
||||
static void boxr(meow_box *b, int out[4]) { meow_box_rect(b, out); }
|
||||
|
||||
/* 创建固定尺寸按钮(既有无绝对定位;注册到绘制物注册表即可) */
|
||||
static meow_widget *mk(int w, int h)
|
||||
{
|
||||
meow_widget *b = meow_btn_create(0, 0, w, h, "X");
|
||||
return b;
|
||||
}
|
||||
|
||||
/* ---- 竖直列:固定 + padding + gap + CROSS_START ---- */
|
||||
static void test_col_fixed(void)
|
||||
{
|
||||
meow_box *bx = meow_box_new(MEOW_BOX_V);
|
||||
CHECK(bx != NULL, "box_new V");
|
||||
meow_box_set_rect(bx, 0, 0, 480, 720);
|
||||
meow_box_set_padding(bx, 20, 20, 20, 20);
|
||||
meow_box_set_gap(bx, 10);
|
||||
meow_box_set_align(bx, MEOW_BOX_ALIGN_START, MEOW_BOX_CROSS_START);
|
||||
meow_widget *a = mk(100, 50), *b = mk(120, 50), *c = mk(80, 50);
|
||||
CHECK(meow_box_add(bx, a, MEOW_BOX_FIXED, 0) == 0, "add a");
|
||||
CHECK(meow_box_add(bx, b, MEOW_BOX_FIXED, 0) == 0, "add b");
|
||||
CHECK(meow_box_add(bx, c, MEOW_BOX_FIXED, 0) == 0, "add c");
|
||||
CHECK(meow_box_count(bx) == 3, "count");
|
||||
meow_box_arrange(bx);
|
||||
int r[4];
|
||||
wr(a, r); CHECK(r[0] == 20 && r[1] == 20 && r[2] == 100 && r[3] == 50, "A x=20 y=20 w100 h50 got %d,%d,%d,%d", r[0], r[1], r[2], r[3]);
|
||||
wr(b, r); CHECK(r[0] == 20 && r[1] == 80 && r[2] == 120, "B y=80 got %d,%d,%d,%d", r[0], r[1], r[2], r[3]);
|
||||
wr(c, r); CHECK(r[0] == 20 && r[1] == 140 && r[2] == 80, "C y=140 got %d,%d,%d,%d", r[0], r[1], r[2], r[3]);
|
||||
meow_box_free(bx);
|
||||
}
|
||||
|
||||
/* ---- 横向行:END 对齐 + 交叉轴中心 ---- */
|
||||
static void test_row_end_center(void)
|
||||
{
|
||||
meow_box *bx = meow_box_new(MEOW_BOX_H);
|
||||
meow_box_set_rect(bx, 10, 0, 480, 200);
|
||||
meow_box_set_padding(bx, 10, 10, 10, 10);
|
||||
meow_box_set_gap(bx, 5);
|
||||
meow_box_set_align(bx, MEOW_BOX_ALIGN_END, MEOW_BOX_CROSS_CENTER);
|
||||
meow_widget *a = mk(100, 50), *b = mk(100, 50), *c = mk(100, 50);
|
||||
CHECK(meow_box_add(bx, a, MEOW_BOX_FIXED, 0) == 0, "add");
|
||||
CHECK(meow_box_add(bx, b, MEOW_BOX_FIXED, 0) == 0, "addb");
|
||||
CHECK(meow_box_add(bx, c, MEOW_BOX_FIXED, 0) == 0, "addc");
|
||||
meow_box_arrange(bx);
|
||||
/* 内容区 x∈[20,470] cw=460;foot=100*3+5*2=310;leftover=150;END → off0=150 */
|
||||
int r[4];
|
||||
wr(a, r);
|
||||
CHECK(r[0] == 20 + 150, "END first x=170 got %d", r[0]);
|
||||
/* 交叉轴中心:cy=10(pad_t);cross_size=ch=180,cross_base=50 → pad=130 → cross=65 → y=10+65=75 */
|
||||
CHECK(r[1] == 75 && r[3] == 50, "center y=75 got %d,%d", r[1], r[3]);
|
||||
wr(b, r); CHECK(r[0] == 20 + 150 + 100 + 5, "END second x=%d", r[0]);
|
||||
wr(c, r); CHECK(r[0] == 380, "END third x=380 got %d", r[0]); /* 170+100+5+100+5+? = 380 */
|
||||
meow_box_free(bx);
|
||||
}
|
||||
|
||||
/* ---- weight 均分:竖直,两个 weight 1:3 ---- */
|
||||
static void test_weight(void)
|
||||
{
|
||||
meow_box *bx = meow_box_new(MEOW_BOX_H);
|
||||
meow_box_set_rect(bx, 0, 0, 480, 200);
|
||||
/* 内容区 480 宽(无 pad/gap)。child 固定 80 + 两个权重 1:3 */
|
||||
meow_widget *fixed = mk(80, 200), *w1 = mk(0, 200), *w2 = mk(0, 200);
|
||||
CHECK(meow_box_add(bx, fixed, MEOW_BOX_FIXED, 0) == 0, "fixed");
|
||||
CHECK(meow_box_add(bx, w1, MEOW_BOX_WEIGHT, 1) == 0, "w1");
|
||||
CHECK(meow_box_add(bx, w2, MEOW_BOX_WEIGHT, 3) == 0, "w2");
|
||||
meow_box_arrange(bx);
|
||||
/* leftover = 480 - 80 = 400;w1=100, w2=300 */
|
||||
int r[4];
|
||||
wr(fixed, r); CHECK(r[0] == 0 && r[2] == 80, "fixed x=0 w80 got %d,%d", r[0], r[2]);
|
||||
wr(w1, r); CHECK(r[0] == 80 && r[2] == 100, "w1 x=80 w100 got %d,%d", r[0], r[2]);
|
||||
wr(w2, r); CHECK(r[0] == 180 && r[2] == 300, "w2 x=180 w300 got %d,%d", r[0], r[2]);
|
||||
meow_box_free(bx);
|
||||
}
|
||||
|
||||
/* ---- WRAP 内容测量(measure 回调给 min/preferred) ---- */
|
||||
static void wrap_measure(void *ctx, int *mn, int *pf) { (void)ctx; *mn = 40; *pf = 120; }
|
||||
static void test_wrap(void)
|
||||
{
|
||||
meow_box *bx = meow_box_new(MEOW_BOX_H);
|
||||
meow_box_set_rect(bx, 0, 0, 480, 100);
|
||||
meow_box_set_gap(bx, 10);
|
||||
meow_box_set_align(bx, MEOW_BOX_ALIGN_START, MEOW_BOX_CROSS_START);
|
||||
meow_widget *w = mk(5, 40); /* 尺寸无关,wrap 用 measure */
|
||||
CHECK(meow_box_add_ex(bx, w, MEOW_BOX_WRAP, 0, 40, 120, wrap_measure, NULL) == 0, "wrap-add");
|
||||
meow_box_arrange(bx);
|
||||
int r[4]; wr(w, r);
|
||||
/* wrap 主轴向 preferred=120;cross_start 交叉=40(widget h 落 V=40?这是 H box,交叉=h) */
|
||||
CHECK(r[2] == 120, "wrap pref w=120 got %d", r[2]);
|
||||
meow_box_free(bx);
|
||||
}
|
||||
|
||||
/* ---- 嵌套:Row 内嵌 Column ---- */
|
||||
static void test_nested(void)
|
||||
{
|
||||
meow_box *row = meow_box_new(MEOW_BOX_H);
|
||||
meow_box *col = meow_box_new(MEOW_BOX_V);
|
||||
meow_box_set_rect(row, 0, 0, 480, 200);
|
||||
meow_box_set_rect(col, 0, 0, 200, 200);
|
||||
meow_box_set_gap(row, 10);
|
||||
meow_widget *a = mk(100, 50), *b = mk(100, 50), *c = mk(100, 50);
|
||||
meow_box_add(row, a, MEOW_BOX_FIXED, 0);
|
||||
meow_box_add_sub(row, col, MEOW_BOX_FIXED, 0);
|
||||
CHECK(meow_box_add_sub(row, col, MEOW_BOX_FIXED, 0) < 0, "sub dedupe");
|
||||
meow_box_add(col, b, MEOW_BOX_FIXED, 0);
|
||||
meow_box_add(col, c, MEOW_BOX_FIXED, 0);
|
||||
meow_box_arrange(row);
|
||||
int r[4];
|
||||
wr(a, r); CHECK(r[0] == 0 && r[2] == 100, "row a x0 w100 got %d,%d", r[0], r[2]);
|
||||
int cr[4]; boxr(col, cr); CHECK(cr[0] == 110 && cr[1] == 0 && cr[2] == 200, "sub box x=110 got %d", cr[0]);
|
||||
/* col 内 b,c 纵向:y 0,50(col y=0, gap0, cross_start) */
|
||||
meow_widget_rect(b, r); CHECK(r[0] == 110 && r[1] == 0, "col b y0 got %d", r[1]);
|
||||
meow_widget_rect(c, r); CHECK(r[1] == 50, "col c y50 got %d", r[1]);
|
||||
meow_box_free(row); meow_box_free(col);
|
||||
}
|
||||
|
||||
/* ---- margin(widget opts)+ 交叉轴 STRETCH ---- */
|
||||
static void test_margin_stretch(void)
|
||||
{
|
||||
meow_box *bx = meow_box_new(MEOW_BOX_V);
|
||||
meow_box_set_rect(bx, 0, 0, 480, 300);
|
||||
meow_box_set_align(bx, MEOW_BOX_ALIGN_START, MEOW_BOX_CROSS_STRETCH);
|
||||
meow_widget *a = mk(100, 60);
|
||||
meow_widget_opts o; memset(&o, 0, sizeof o);
|
||||
o.margin_x = 30; o.margin_y = 10;
|
||||
CHECK(meow_widget_set_opts(a, &o) == 0, "set opts");
|
||||
meow_box_add(bx, a, MEOW_BOX_FIXED, 0);
|
||||
meow_box_arrange(bx);
|
||||
int r[4]; wr(a, r);
|
||||
/* V:交叉轴=x,STRETCH → w = cw - 2*margin_x = 480 - 60 = 420;x = margin_x = 30。
|
||||
* 主轴 y:lead=margin_y=10 → y=10;ext=h=60。 */
|
||||
CHECK(r[0] == 30 && r[2] == 420, "stretch w=420 x=30 got %d,%d", r[0], r[2]);
|
||||
CHECK(r[1] == 10 && r[3] == 60, "y=10 h=60 got %d,%d", r[1], r[3]);
|
||||
meow_box_free(bx);
|
||||
}
|
||||
|
||||
/* ---- SPACE_BETWEEN ---- */
|
||||
static void test_space_between(void)
|
||||
{
|
||||
meow_box *bx = meow_box_new(MEOW_BOX_H);
|
||||
meow_box_set_rect(bx, 0, 0, 500, 100);
|
||||
meow_box_set_align(bx, MEOW_BOX_ALIGN_SPACE_BETWEEN, MEOW_BOX_CROSS_START);
|
||||
meow_widget *a = mk(100, 50), *b = mk(100, 50), *c = mk(100, 50);
|
||||
meow_box_add(bx, a, MEOW_BOX_FIXED, 0);
|
||||
meow_box_add(bx, b, MEOW_BOX_FIXED, 0);
|
||||
meow_box_add(bx, c, MEOW_BOX_FIXED, 0);
|
||||
meow_box_arrange(bx);
|
||||
/* cw=500;foot=300;leftover=200;n-1=2 → 每 gap += 100 → A.x=0 B.x=200 C.x=400 */
|
||||
int r[4];
|
||||
wr(a, r); CHECK(r[0] == 0, "sb A x0 got %d", r[0]);
|
||||
wr(b, r); CHECK(r[0] == 200, "sb B x200 got %d", r[0]);
|
||||
wr(c, r); CHECK(r[0] == 400, "sb C x400 got %d", r[0]);
|
||||
meow_box_free(bx);
|
||||
}
|
||||
|
||||
/* ---- view 集成:meow_view_set_layout + relayout ---- */
|
||||
static void test_view_layout(void)
|
||||
{
|
||||
meow_view *v = meow_view_create(900, MEOW_VIEW_ACTIVITY, NULL, NULL);
|
||||
CHECK(v != NULL, "view create");
|
||||
v->x = 0; v->y = 0; v->w = 480; v->h = 720;
|
||||
meow_box *bx = meow_box_new(MEOW_BOX_V);
|
||||
meow_box_set_gap(bx, 8);
|
||||
meow_box_set_align(bx, MEOW_BOX_ALIGN_START, MEOW_BOX_CROSS_START);
|
||||
meow_widget *a = mk(300, 50), *b = mk(300, 50);
|
||||
meow_box_add(bx, a, MEOW_BOX_FIXED, 0);
|
||||
meow_box_add(bx, b, MEOW_BOX_FIXED, 0);
|
||||
meow_view_add_widget(v, a);
|
||||
meow_view_add_widget(v, b);
|
||||
meow_view_set_layout(v, bx); /* relayout 立即排一次 */
|
||||
int r[4];
|
||||
wr(a, r); CHECK(r[0] == 0 && r[1] == 0 && r[2] == 300, "view a x0 y0 w300 got %d,%d,%d", r[0], r[1], r[2]);
|
||||
wr(b, r); CHECK(r[1] == 58, "view b y=58 got %d", r[1]); /* 0 + 50 + gap8 */
|
||||
meow_view_free(v);
|
||||
meow_box_free(bx);
|
||||
}
|
||||
|
||||
/* ---- 越界去重 / 池满 ---- */
|
||||
static void test_edge(void)
|
||||
{
|
||||
meow_box *bx = meow_box_new(MEOW_BOX_H);
|
||||
meow_widget *a = mk(10, 10), *dup = mk(10, 10);
|
||||
CHECK(meow_box_add(bx, dup, MEOW_BOX_FIXED, 0) == 0, "add dup");
|
||||
CHECK(meow_box_add(bx, dup, MEOW_BOX_FIXED, 0) < 0, "dedupe");
|
||||
CHECK(meow_box_add(bx, NULL, MEOW_BOX_FIXED, 0) < 0, "null add rejected");
|
||||
meow_box_widget_at(bx, 0) ? 0 : 0;
|
||||
CHECK(meow_box_widget_at(bx, 0) == dup, "widget_at 0");
|
||||
CHECK(meow_box_sub_at(bx, 0) == NULL, "sub_at null");
|
||||
meow_box_free(bx);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
test_col_fixed();
|
||||
test_row_end_center();
|
||||
test_weight();
|
||||
test_wrap();
|
||||
test_nested();
|
||||
test_margin_stretch();
|
||||
test_space_between();
|
||||
test_view_layout();
|
||||
test_edge();
|
||||
printf("box: %d pass / %d fail\n", g_pass, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
* test_meow_ui_config.c — meow_config_* 宿主单测(2026-08-30)。
|
||||
*
|
||||
* 编译(宿主,config 为纯 libc,无需 SDK 其它对象):
|
||||
* gcc -Wall -Wextra test_meow_ui_config.c meow_ui_config.c -o test_meow_ui_config -lpthread
|
||||
* gcc -Wall -Wextra -I. -Iinclude test_meow_ui_config.c meow_ui_config.c -o test_meow_ui_config -lpthread
|
||||
*
|
||||
* 两种模式(meow_config_dir 经 pthread_once 缓存,目录只在首次调用确定,故分两次跑):
|
||||
* ./test_meow_ui_config env → 设 MEOW_DATA_DIR=<mkdtemp>,验证持久目录读写
|
||||
|
||||
@@ -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 meow_ui_nav.c \
|
||||
* -I. -Iinclude -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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
/* test_meow_ui_home_gesture.c — 批② SDK 全局「App 内底部热区上滑回桌面」宿主验证。
|
||||
*
|
||||
* 覆盖(并同 meow_ui_input_is_home_swipe 判据、touch_release 分类、meow_ui_run 分发三层):
|
||||
* T1 判定矩阵(meow_ui_input_is_home_swipe,镜像 launcher np_gesture.h np_home_gesture):
|
||||
* 底部热区上滑=1;底部左缘上滑=0(左缘返回互斥);中部上滑=0(App 滚动);底部横向=0;
|
||||
* 底部下滑=0;上移不足=0;横向主导=0;屏底边界(down_y=fb_h-HOME_EDGE_H)之外=0。
|
||||
* T2 触摸状态机分类(真实 touch_release → 队列 pop 断言):
|
||||
* 底部热区上滑→MEOW_EV_HOME(a=MEOW_SWIPE_UP);中部上滑→MEOW_EV_SWIPE(UP)(不被剥回桌面);
|
||||
* 底部左缘横滑→MEOW_EV_SWIPE(LEFT)(原左缘返回,不被剥)。
|
||||
* T3 分发(meow_ui_run 主循环,run-loop 复用 test_meow_ui_view_event 的 driver 线程范式):
|
||||
* 3a 未注册 on_swipe:注入 MEOW_EV_HOME → meow_ui_nav_request_exit 被触发(一次)——回桌面;
|
||||
* 3b 未注册 on_swipe:注入 MEOW_EV_SWIPE(UP)(中部上滑)→ nav_request_exit 不触发(App 滚动);
|
||||
* 3c 注册 on_swipe:注入 MEOW_EV_HOME → nav_request_exit 触发(系统级手势优先于 App on_swipe),
|
||||
* on_swipe 不被调用——屏底热区上滑是系统保留手势,任意 App 内生效(用户 2026-09-04 批②)。
|
||||
*
|
||||
* 编译(宿主 gcc,链真实 SDK 源全集 + 打桩 font/global;**不链 meow_ui_nav.c**——本测以测试内
|
||||
* meow_ui_nav_request_exit 桩计数验证触发):
|
||||
* gcc -Wall -Wextra -o test_meow_ui_home_gesture test_meow_ui_home_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_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. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 运行:./test_meow_ui_home_gesture
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h" /* meow_input_touch_set/move/release + meow_input_pop + MEOW_EV_* */
|
||||
#include "meow_ui_nav.h" /* meow_ui_nav_request_exit 原型(本测以桩计数验证) */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <pthread.h>
|
||||
#include <assert.h>
|
||||
|
||||
/* MEOW_EV_HOME 是 meow_ui_input.c 内部常量(未入公共头,同 MEOW_EV_NOTIF_PULL 惯例),按同值声明。 */
|
||||
#ifndef MEOW_EV_HOME
|
||||
#define MEOW_EV_HOME 9
|
||||
#endif
|
||||
|
||||
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)
|
||||
|
||||
/* fb 高度(宿主 480x720) */
|
||||
#define FBW 480
|
||||
#define FBH 720
|
||||
/* 判据常量镜像(np_gesture.h NP_HOME_EDGE_H=28 / NP_HOME_SWIPE=65 / NP_EDGE_W=24)。 */
|
||||
#define H_EDGE 28
|
||||
#define H_SWIPE 65
|
||||
#define EDGE_W 24
|
||||
|
||||
/* ---- nav_request_exit 桩(不链 meow_ui_nav.c;计数验证 SDK 触发) ---- */
|
||||
static int g_nav_exit_calls = 0;
|
||||
void meow_ui_nav_request_exit(void) { g_nav_exit_calls++; }
|
||||
|
||||
/* ---- on_swipe 桩(App 接管验证) ---- */
|
||||
static int g_swipe_calls = 0;
|
||||
static int g_swipe_dir = -1;
|
||||
static int g_swipe_dx = 0;
|
||||
static void app_on_swipe(int dir, int dx, int dy) { (void)dy; g_swipe_calls++; g_swipe_dir = dir; g_swipe_dx = dx; }
|
||||
|
||||
/* 从队列 pop 一个事件并回填 (type,a,b);无事件返回 0。 */
|
||||
static int pop_ev(int *type, int *a, int *b)
|
||||
{
|
||||
meow_input_ev e;
|
||||
if (!meow_input_pop(&e)) return 0;
|
||||
*type = e.type; *a = e.a; *b = e.b;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ============ T1 判定矩阵(meow_ui_input_is_home_swipe) ============ */
|
||||
static void test_predicate(void)
|
||||
{
|
||||
printf("T1 meow_ui_input_is_home_swipe matrix\n");
|
||||
int bz = FBH - H_EDGE; /* 底部热区上边界 y = 720-28 = 692 */
|
||||
|
||||
CHECK(meow_ui_input_is_home_swipe(240, bz + 10, 0, -120, FBH) == 1, "T1 bottom-hotzone up -> 1");
|
||||
CHECK(meow_ui_input_is_home_swipe(100, FBH - 1, 20, -80, FBH) == 1, "T1 screen-bottom up (x>EDGE_W) -> 1");
|
||||
CHECK(meow_ui_input_is_home_swipe(240, bz + 10, 0, -H_SWIPE, FBH) == 1, "T1 exactly threshold up -> 1");
|
||||
CHECK(meow_ui_input_is_home_swipe(240, bz + 10, -30, -70, FBH) == 1, "T1 up w/ small left dx (adx<dy) -> 1");
|
||||
|
||||
/* 左缘互斥(down_x<EDGE_W) */
|
||||
CHECK(meow_ui_input_is_home_swipe(10, bz + 10, 0, -120, FBH) == 0, "T1 left-edge origin -> 0 (edge-back reserved)");
|
||||
/* 中部上滑(App 滚动) */
|
||||
CHECK(meow_ui_input_is_home_swipe(240, 300, 0, -120, FBH) == 0, "T1 mid-screen up -> 0 (App scroll)");
|
||||
/* 底部横向(左缘返回/横向逻辑保留) */
|
||||
CHECK(meow_ui_input_is_home_swipe(240, bz + 10, 120, 0, FBH) == 0, "T1 bottom horizontal -> 0");
|
||||
/* 底部下滑 */
|
||||
CHECK(meow_ui_input_is_home_swipe(240, bz + 10, 0, 120, FBH) == 0, "T1 bottom down -> 0");
|
||||
/* 上移不足(<H_SWIPE) */
|
||||
CHECK(meow_ui_input_is_home_swipe(240, bz + 10, 0, -(H_SWIPE - 1), FBH) == 0, "T1 up below threshold -> 0");
|
||||
/* 竖向未主导(横向位移≥上移量,adx>=dy) */
|
||||
CHECK(meow_ui_input_is_home_swipe(240, bz + 10, -120, -80, FBH) == 0, "T1 up w/ big dx (not vertical-dominant) -> 0");
|
||||
/* 热区边界之外(down_y < bz) */
|
||||
CHECK(meow_ui_input_is_home_swipe(240, bz - 1, 0, -120, FBH) == 0, "T1 just above hotzone -> 0");
|
||||
}
|
||||
|
||||
/* ============ T2 触摸状态机分类(touch_release → 队列) ============ */
|
||||
static void test_touch_classify(void)
|
||||
{
|
||||
printf("T2 touch_release classification\n");
|
||||
meow_input_drain();
|
||||
|
||||
/* 底部热区上滑 → MEOW_EV_HOME */
|
||||
meow_input_touch_set(1, 240, FBH - 5, 0); /* 起点 y=715 ∈ [692,720) */
|
||||
meow_input_touch_move(240, FBH - 5 - 130); /* 上移 130 */
|
||||
meow_input_touch_release();
|
||||
{
|
||||
int t, a, b;
|
||||
CHECK(pop_ev(&t, &a, &b) == 1 && t == MEOW_EV_HOME && a == MEOW_SWIPE_UP,
|
||||
"T2 bottom-hotzone up -> MEOW_EV_HOME(UP) got type=%d a=%d", t, a);
|
||||
}
|
||||
|
||||
/* 中部上滑 → 普通 MEOW_EV_SWIPE(UP)(不被剥回桌面) */
|
||||
meow_input_touch_set(1, 240, 300, 0);
|
||||
meow_input_touch_move(240, 300 - 130);
|
||||
meow_input_touch_release();
|
||||
{
|
||||
int t, a, b;
|
||||
CHECK(pop_ev(&t, &a, &b) == 1 && t == MEOW_EV_SWIPE && a == MEOW_SWIPE_UP,
|
||||
"T2 mid up -> SWIPE(UP) got type=%d a=%d", t, a);
|
||||
}
|
||||
|
||||
/* 底部左缘横滑 → 原有左缘返回(不被剥) */
|
||||
meow_input_touch_set(1, 5, FBH - 5, 0);
|
||||
meow_input_touch_move(5 + 100, FBH - 5);
|
||||
meow_input_touch_release();
|
||||
{
|
||||
int t, a, b;
|
||||
CHECK(pop_ev(&t, &a, &b) == 1 && t == MEOW_EV_SWIPE && a == MEOW_SWIPE_LEFT,
|
||||
"T2 bottom-left horizontal -> SWIPE(LEFT) got type=%d a=%d", t, a);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============ T3 分发(meow_ui_run 主循环) ============ */
|
||||
static int g_inj_type, g_inj_a, g_inj_b;
|
||||
static void *driver(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
usleep(700000); /* 等 startup cooldown(500ms) 过后再注入 */
|
||||
meow_input_inject(g_inj_type, g_inj_a, g_inj_b);
|
||||
usleep(80000);
|
||||
meow_ui_request_exit();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void run_scenario(int inj_type, int inj_a, int inj_b, int register_swipe)
|
||||
{
|
||||
g_inj_type = inj_type; g_inj_a = inj_a; g_inj_b = inj_b;
|
||||
g_nav_exit_calls = 0;
|
||||
g_swipe_calls = 0; g_swipe_dir = -1; g_swipe_dx = 0;
|
||||
meow_input_drain(); /* 防上一次残留事件影响本次分发 */
|
||||
|
||||
meow_ui_events ev; memset(&ev, 0, sizeof ev);
|
||||
if (register_swipe) ev.on_swipe = app_on_swipe;
|
||||
|
||||
pthread_t thd; pthread_create(&thd, NULL, driver, NULL);
|
||||
meow_ui_run(&ev);
|
||||
pthread_join(thd, NULL);
|
||||
}
|
||||
|
||||
static void test_dispatch(void)
|
||||
{
|
||||
printf("T3 meow_ui_run dispatch (home = nav_request_exit fallback)\n");
|
||||
|
||||
/* 3a 未注册 on_swipe:底部热区上滑 → nav_request_exit 触发一次(回桌面) */
|
||||
run_scenario(MEOW_EV_HOME, MEOW_SWIPE_UP, 120, 0);
|
||||
CHECK(g_nav_exit_calls == 1, "T3a no-on_swipe MEOW_EV_HOME -> nav_request_exit fired (got %d)", g_nav_exit_calls);
|
||||
CHECK(g_swipe_calls == 0, "T3a no-on_swipe -> on_swipe NOT called");
|
||||
|
||||
/* 3b 未注册 on_swipe:中部上滑(普通 MEOW_EV_SWIPE UP)→ nav_request_exit 不触发(App 滚动) */
|
||||
run_scenario(MEOW_EV_SWIPE, MEOW_SWIPE_UP, 120, 0);
|
||||
CHECK(g_nav_exit_calls == 0, "T3b mid-swipe-up -> nav_request_exit NOT fired (App scroll), got %d", g_nav_exit_calls);
|
||||
|
||||
/* 3c 注册 on_swipe:底部热区上滑 → 系统优先,nav_request_exit 触发(回桌面),on_swipe 不被调用 */
|
||||
run_scenario(MEOW_EV_HOME, MEOW_SWIPE_UP, 120, 1);
|
||||
CHECK(g_nav_exit_calls == 1, "T3c on_swipe registered -> nav_request_exit STILL fired (system gesture wins), got %d", g_nav_exit_calls);
|
||||
CHECK(g_swipe_calls == 0, "T3c system-priority home -> on_swipe NOT called");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
meow_ui_opts o = { FBW, FBH };
|
||||
if (meow_ui_init(&o) != 0) { printf("init failed\n"); return 1; }
|
||||
|
||||
test_predicate();
|
||||
test_touch_classify();
|
||||
test_dispatch();
|
||||
|
||||
meow_ui_shutdown();
|
||||
printf("home_gesture: %d pass / %d fail\n", g_pass, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/* test_meow_ui_iconbtn.c — M7.0a 缺口#2 宿主验证:IconButton 圆形命中 + 按压态 + 位图图标。
|
||||
*
|
||||
* 覆盖:
|
||||
* I1 圆形命中:中心/边界命中,角部/外缘不命中(区别于矩形按钮);
|
||||
* I2 owner==active + enabled 双闸门:非 active 或禁用不命中;
|
||||
* I3 按压态视觉:设 MEOW_WS_PRESSED 后重绘,圆心像素向 onSurface 变亮(与未按不同);
|
||||
* 禁用时圆心向灰变暗;
|
||||
* I4 实时按压(meow_input_touch 注入):按下命中圆心 → 圆心变亮;释放恢复。
|
||||
*
|
||||
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global):
|
||||
* gcc -Wall -Wextra -o test_meow_ui_iconbtn test_meow_ui_iconbtn.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 meow_ui_nav.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 运行:./test_meow_ui_iconbtn
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
|
||||
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)
|
||||
|
||||
/* ---- back_buf 像素读取(断言真实渲染,不依赖目检) ---- */
|
||||
static void rd_px(int x, int y, unsigned short *p)
|
||||
{
|
||||
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) { *p = 0; return; }
|
||||
if (bpp == 16) *p = *(unsigned short *)(bb + (size_t)y * line + (size_t)x * 2);
|
||||
else *p = (unsigned short)(*(unsigned int *)(bb + (size_t)y * line + (size_t)x * 4) >> 16);
|
||||
}
|
||||
static unsigned short px565_at(int x, int y)
|
||||
{
|
||||
unsigned short p; rd_px(x, y, &p); return p;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
setenv("MEOW_UI_DUMP", "/tmp/iconbtn.ppm", 1);
|
||||
meow_ui_opts o = { 480, 720 };
|
||||
assert(meow_ui_init(&o) == 0);
|
||||
meow_ui_activity_push(7); /* owner 对齐:IconButton 绑定 active=7 */
|
||||
meow_theme_set_mode(MEOW_THEME_DARK);
|
||||
|
||||
/* size=64 → 外接框 (100,100,64,64) → 圆心 (132,132) r=32 */
|
||||
meow_widget *ic = meow_iconbtn_create(100, 100, 64, MEOW_ICON_PLAY_ARROW);
|
||||
assert(ic != NULL);
|
||||
|
||||
/* ============ I1 圆形命中 ============ */
|
||||
printf("I1 circular hit\n");
|
||||
CHECK(meow_widget_hit(ic, 132, 132), "center hits");
|
||||
CHECK(meow_widget_hit(ic, 100, 132), "left edge (boundary) hits");
|
||||
CHECK(meow_widget_hit(ic, 132, 100), "top edge (boundary) hits");
|
||||
CHECK(!meow_widget_hit(ic, 100, 100), "corner (dist>r) MISSES (rect would hit)");
|
||||
CHECK(!meow_widget_hit(ic, 132, 165), "below center (dist 33>32) MISSES");
|
||||
|
||||
/* ============ I2 owner 闸门:切 active 后不命中 ============ */
|
||||
printf("I2 owner gate\n");
|
||||
meow_ui_activity_push(8); /* 换 active:original owner=7 ≠ active=8 */
|
||||
CHECK(!meow_widget_hit(ic, 132, 132), "non-active owner -> MISS (gate)");
|
||||
meow_ui_activity_pop(); /* 回 active=7 */
|
||||
CHECK(meow_widget_hit(ic, 132, 132), "back to active -> hits");
|
||||
/* enabled 闸门 */
|
||||
meow_widget_set_enabled(ic, 0);
|
||||
CHECK(!meow_widget_hit(ic, 132, 132), "disabled -> MISS");
|
||||
meow_widget_set_enabled(ic, 1);
|
||||
CHECK(meow_widget_hit(ic, 132, 132), "enabled -> hits");
|
||||
|
||||
/* ============ I3 按压态视觉(状态机位 + 禁用变暗) ============
|
||||
* 采样点选 (162,132):圆内 (r=30<32) 但超出图标 ink 右缘(约 x≤148)——取的是圆形底色,
|
||||
* 而非图标墨色(圆心为图标覆盖,不适合测底色 tint)。 */
|
||||
printf("I3 pressed visual (state machine / disabled)\n");
|
||||
const meow_theme *th = meow_theme_get();
|
||||
meow_ui_clear(th->bg);
|
||||
unsigned short p_bg = px565_at(100, 100); /* 空屏角部 = 页面底色 */
|
||||
meow_widget_set_state(ic, MEOW_WS_PRESSED, 0); /* 未按 */
|
||||
meow_widget_draw(ic);
|
||||
unsigned short p_idle = px565_at(162, 132);
|
||||
meow_widget_set_state(ic, MEOW_WS_PRESSED, 1); /* 按下 */
|
||||
meow_widget_draw(ic);
|
||||
unsigned short p_press = px565_at(162, 132);
|
||||
CHECK(p_press != p_idle, "pressed circle-bg pixel differs from idle (tint layer)");
|
||||
/* 禁用:向灰变暗,圆 bg 应更接近 onSurface 混合(与 idle 不同且更亮/更灰) */
|
||||
meow_widget_set_state(ic, MEOW_WS_PRESSED, 0);
|
||||
meow_widget_set_enabled(ic, 0);
|
||||
meow_widget_draw(ic);
|
||||
unsigned short p_dis = px565_at(162, 132);
|
||||
CHECK(p_dis != p_idle, "disabled circle-bg pixel differs from idle (dim)");
|
||||
meow_widget_set_enabled(ic, 1);
|
||||
/* 角部(圆外)不应出现圆形底色:等于页面底色(圆形命中/填充不外溢) */
|
||||
meow_widget_draw(ic);
|
||||
CHECK(px565_at(100, 100) == p_bg, "corner (outside circle) = page bg (no stray fill)");
|
||||
|
||||
/* ============ I4 实时按压(meow_input_touch 注入) ============ */
|
||||
printf("I4 live press via input touch\n");
|
||||
meow_widget_set_state(ic, MEOW_WS_PRESSED, 0);
|
||||
meow_ui_clear(th->bg);
|
||||
meow_widget_draw(ic);
|
||||
unsigned short q_idle = px565_at(162, 132);
|
||||
meow_input_touch_set(1, 132, 132, 0); /* 按在圆心 */
|
||||
meow_widget_draw(ic);
|
||||
unsigned short q_live = px565_at(162, 132);
|
||||
CHECK(q_live != q_idle, "live touch press -> circle bg brightens");
|
||||
meow_input_touch_set(0, 0, 0, 0); /* 释放 */
|
||||
meow_widget_draw(ic);
|
||||
CHECK(px565_at(162, 132) == q_idle, "release -> circle bg restores");
|
||||
|
||||
meow_widget_free(ic);
|
||||
meow_ui_activity_pop();
|
||||
meow_ui_shutdown();
|
||||
printf(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS\n", g_fail);
|
||||
return g_fail;
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
/* test_meow_ui_lazygrid.c — 宿主验证 §12.4 2 维虚拟化宫格(meow_lazygrid):
|
||||
* T1 虚拟化正确性:count/cols/max_scroll;首帧绑定可见行 item;scroll 下滚仅补新入视口的 item;
|
||||
* 回滚不重绑;全量扫过 = count(每 index 恰 bind 一次);命中把滚动+网格坐标映射回 idx 正确。
|
||||
* T2 默认 renderer:圆角卡片底 / 标题/副标题 / 选中高亮(primaryContainer)/ 禁用变淡。
|
||||
* T3 完全自绘 set_item_draw:每可见格调一次 + 自绘内容像素。
|
||||
* T4 空态占位(count=0 + empty_text 居中)。
|
||||
* T5 meow_scroll 集成(meow_lazygrid_scroll_attach:track/fling/snap 到行边界)。
|
||||
*
|
||||
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global):
|
||||
* gcc -o test_meow_ui_lazygrid test_meow_ui_lazygrid.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_lazygrid.c meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 运行:./test_meow_ui_lazygrid
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h" /* meow_ui_back_buf/pitch/bpp:真实像素断言 */
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int g_pass = 0, g_fail = 0;
|
||||
#define CHECK(cond, ...) do { \
|
||||
if (cond) { g_pass++; } \
|
||||
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
|
||||
} while (0)
|
||||
|
||||
/* ---- 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)
|
||||
|
||||
/* ---- 网格几何(测试统一) ---- */
|
||||
#define GX 20
|
||||
#define GY 50
|
||||
#define GW 440
|
||||
#define GH 200
|
||||
#define COLS 3
|
||||
#define CW 100
|
||||
#define CH 80
|
||||
#define GAP 10
|
||||
#define ROWH (CH + GAP) /* 90 */
|
||||
#define N 20 /* 默认总项数 */
|
||||
|
||||
static int g_count_override; /* <0 = 用默认 N;>=0 = 覆盖(空态/小列表) */
|
||||
static int g_bind_total; /* bind 回调计数(含 hit 的临时 bind) */
|
||||
static int g_draw_total; /* draw_cb(自绘)计数 */
|
||||
static int g_disabled_at; /* 该 index 返回 enabled=0(-1=无) */
|
||||
static int g_tap_cb_idx; /* tap_at 回调记录的选中项 */
|
||||
|
||||
static void lg_tap_cb(int idx, void *ctx) { (void)ctx; g_tap_cb_idx = idx; }
|
||||
|
||||
static int lg_count(void *ctx) { (void)ctx; return g_count_override >= 0 ? g_count_override : N; }
|
||||
static void lg_bind(int idx, meow_lazygrid_item *it, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
g_bind_total++;
|
||||
memset(it, 0, sizeof *it);
|
||||
static char t[32], s[32];
|
||||
snprintf(t, sizeof t, "cell %d", idx);
|
||||
snprintf(s, sizeof s, "st %d", idx);
|
||||
it->title = t; it->subtitle = s;
|
||||
it->enabled = 1;
|
||||
if (g_disabled_at == idx) it->enabled = 0;
|
||||
}
|
||||
|
||||
/* ---- T3 完全自绘:每格画实心块 + 角标记 ---- */
|
||||
static void mydraw(int idx, int x, int y, int w, int h, int st, void *ctx)
|
||||
{
|
||||
(void)idx; (void)ctx;
|
||||
g_draw_total++;
|
||||
meow_ui_fill_rect(x, y, w, h, 0x334455); /* 自定义格底 */
|
||||
meow_ui_fill_rect(x + 4, y + 4, 8, 8, 0xFF0000); /* 角标记 */
|
||||
if (st & MEOW_LAZYGRID_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 sc_r, sc_g, sc_b, on_r, on_g, on_b, pc_r, pc_g, pc_b, osv_r, osv_g, osv_b, opc_r, opc_g, opc_b;
|
||||
UNPK(th->surfaceContainer, sc_r, sc_g, sc_b);
|
||||
UNPK(th->onSurface, on_r, on_g, on_b);
|
||||
UNPK(th->primaryContainer, pc_r, pc_g, pc_b);
|
||||
UNPK(th->onSurfaceVariant, osv_r, osv_g, osv_b);
|
||||
UNPK(th->onPrimaryContainer, opc_r, opc_g, opc_b);
|
||||
g_count_override = -1; g_bind_total = 0; g_disabled_at = -1;
|
||||
|
||||
/* ============ T1 虚拟化正确性 ============ */
|
||||
{
|
||||
meow_lazygrid *g = meow_lazygrid_create(GX, GY, GW, GH, COLS, CW, CH, GAP);
|
||||
CHECK(g != NULL, "create");
|
||||
CHECK(meow_lazygrid_set_vsource(g, lg_count, lg_bind, NULL) == 0, "vsource");
|
||||
CHECK(meow_lazygrid_max_scroll(g) == 7 * CH + 6 * GAP - GH, "max_scroll %d (expect %d)",
|
||||
meow_lazygrid_max_scroll(g), 7 * CH + 6 * GAP - GH); /* 620-200=420 */
|
||||
CHECK(meow_lazygrid_scroll(g) == 0, "scroll0");
|
||||
meow_ui_clear(th->bg);
|
||||
meow_lazygrid_draw(g, th);
|
||||
|
||||
int B0 = g_bind_total;
|
||||
int vis_rows = GH / ROWH + 1; /* 3 */
|
||||
int expect0 = vis_rows * COLS; /* 9(row0..2 × 3 格) */
|
||||
CHECK(B0 == expect0, "first draw binds %d got %d", expect0, B0);
|
||||
|
||||
/* 下滚 1 行(90px):新入视口 = 末 1 行 3 格 → bind +3(其余同槽不重绑) */
|
||||
meow_lazygrid_set_scroll(g, ROWH);
|
||||
meow_lazygrid_draw(g, th);
|
||||
int B1 = g_bind_total;
|
||||
CHECK(B1 == B0 + COLS, "scroll+1row binds +%d got %d (was %d)", COLS, B1 - B0, B0);
|
||||
|
||||
/* 回滚到顶:槽仍在 → 不重绑(此段不调 hit,避免临时 bind 干扰计数) */
|
||||
meow_lazygrid_set_scroll(g, 0);
|
||||
meow_lazygrid_draw(g, th);
|
||||
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 = 0;
|
||||
while (prev <= N) {
|
||||
int off = prev * ROWH;
|
||||
meow_lazygrid_set_scroll(g, off);
|
||||
meow_lazygrid_draw(g, th);
|
||||
if (off >= meow_lazygrid_max_scroll(g)) break;
|
||||
prev++;
|
||||
}
|
||||
CHECK(g_bind_total == N, "full sweep binds each index once got %d (expect %d)", g_bind_total, N);
|
||||
|
||||
/* 命中:索引 = 滚动 + 网格坐标映射(此后的 hit 走临时 bind,不影响上面的总数断言) */
|
||||
meow_lazygrid_set_scroll(g, 0);
|
||||
CHECK(meow_lazygrid_hit(g, GX + 10, GY + 10) == 0, "hit cell0 got %d", meow_lazygrid_hit(g, GX + 10, GY + 10));
|
||||
meow_lazygrid_set_scroll(g, ROWH);
|
||||
/* scroll=90:GY+10(视口顶)→ 内容 y=100 → row1([90,170])col0 → idx=3 */
|
||||
CHECK(meow_lazygrid_hit(g, GX + 10, GY + 10) == COLS,
|
||||
"hit maps scroll+grid got %d (expect 3)", meow_lazygrid_hit(g, GX + 10, GY + 10));
|
||||
/* gap 区不命中 */
|
||||
CHECK(meow_lazygrid_hit(g, GX + CW + GAP / 2, GY + 10) == -1, "horizontal gap miss got %d",
|
||||
meow_lazygrid_hit(g, GX + CW + GAP / 2, GY + 10));
|
||||
/* 越界列(row0 左侧无格/第 4 列)不命中 */
|
||||
meow_lazygrid_set_scroll(g, 0);
|
||||
CHECK(meow_lazygrid_hit(g, GX + COLS * (CW + GAP) + 10, GY + 10) == -1, "col overflow miss got %d",
|
||||
meow_lazygrid_hit(g, GX + COLS * (CW + GAP) + 10, GY + 10));
|
||||
CHECK(meow_lazygrid_hit(g, GX + 10, GY + ROWH) == COLS, "hit row1 col0 idx=3 got %d",
|
||||
meow_lazygrid_hit(g, GX + 10, GY + ROWH));
|
||||
|
||||
/* tap_at:命中 → 置选中 + 回调 */
|
||||
g_tap_cb_idx = -1;
|
||||
meow_lazygrid_set_on_tap(g, lg_tap_cb, NULL);
|
||||
int tap_idx = meow_lazygrid_tap_at(g, GX + 50, GY + 40);
|
||||
CHECK(tap_idx == 0, "tap_at idx0 got %d", tap_idx);
|
||||
CHECK(meow_lazygrid_sel(g) == 0, "sel set to 0");
|
||||
CHECK(g_tap_cb_idx == 0, "tap cb idx0 got %d", g_tap_cb_idx);
|
||||
/* 点 row1,col1(idx=4) → 该格命中 */
|
||||
int idx4 = meow_lazygrid_hit(g, GX + 1 * (CW + GAP) + 10, GY + ROWH + 10);
|
||||
CHECK(idx4 == 4, "hit row1col1 idx=%d (expect 4)", idx4);
|
||||
|
||||
meow_lazygrid_free(g);
|
||||
}
|
||||
|
||||
/* ============ T2 默认 renderer:卡片底 / 标题 / 选中高亮 / 禁用 ============ */
|
||||
{
|
||||
meow_lazygrid *g = meow_lazygrid_create(GX, GY, GW, GH, COLS, CW, CH, GAP);
|
||||
meow_lazygrid_set_vsource(g, lg_count, lg_bind, NULL);
|
||||
g_count_override = 3; g_disabled_at = 2; /* 3 格:cell2 禁用 */
|
||||
meow_ui_clear(th->bg);
|
||||
meow_lazygrid_set_sel(g, 0);
|
||||
meow_lazygrid_draw(g, th);
|
||||
/* 选中 cell0 → primaryContainer 底([GX,GY,100,80]) */
|
||||
CHECK(region_has(GX + 4, GY + 4, 90, 60, pc_r, pc_g, pc_b) > 40, "selected cell primaryContainer bg");
|
||||
/* 未选中 cell1 → surfaceContainer 底(MD3 容器:格内对称内缩 m=8 的圆角卡) */
|
||||
int x1 = GX + 1 * (CW + GAP);
|
||||
CHECK(region_has(x1 + 4, GY + 4, 90, 60, sc_r, sc_g, sc_b) > 40, "cell1 surfaceContainer bg");
|
||||
/* 标题画在 cell0 卡内底部(选中 → onPrimaryContainer;副标题在下方 onPrimaryContainer 同色带)。
|
||||
* 镜像 lg_draw_cell 的 ty:cy=GY+m、chh=CH-2m、pad=8 → ty = GY+CH-m-pad-thh-(thh+2)。 */
|
||||
int thh = meow_ui_text_height(th->font_body);
|
||||
int title_y = GY + CH - 8 - 8 - thh - (thh + 2);
|
||||
int c0cx = GX + CW / 2;
|
||||
CHECK(region_has(c0cx - 30, title_y, 60, thh, opc_r, opc_g, opc_b) > 2, "cell0 title drawn (onPrimaryContainer)");
|
||||
/* 禁用 cell2 变淡:卡内左上角(图标/文本区之外)不再是纯 surfaceContainer(被 onSurface α 叠暗) */
|
||||
int x2 = GX + 2 * (CW + GAP);
|
||||
CHECK(!px_near(x2 + 8 + 4, GY + 8 + 4, sc_r, sc_g, sc_b), "disabled cell overlaid (bg dimmed)");
|
||||
meow_lazygrid_free(g);
|
||||
}
|
||||
|
||||
/* ============ T3 完全自绘(set_item_draw) ============ */
|
||||
{
|
||||
meow_lazygrid *g = meow_lazygrid_create(GX, GY, GW, GH, COLS, CW, CH, GAP);
|
||||
meow_lazygrid_set_vsource(g, lg_count, lg_bind, NULL);
|
||||
g_count_override = 3; g_disabled_at = -1; g_draw_total = 0;
|
||||
CHECK(meow_lazygrid_set_item_draw(g, mydraw, NULL) == 0, "set_item_draw");
|
||||
meow_ui_clear(th->bg);
|
||||
meow_lazygrid_draw(g, th);
|
||||
CHECK(g_draw_total == 3, "draw_cb per visible cell got %d (expect 3)", g_draw_total);
|
||||
CHECK(region_has(GX + 4, GY + 4, 8, 8, 0xFF, 0x00, 0x00) > 0, "custom cell content drawn");
|
||||
meow_lazygrid_free(g);
|
||||
}
|
||||
|
||||
/* ============ T4 空态 ============ */
|
||||
{
|
||||
meow_lazygrid *g = meow_lazygrid_create(GX, GY, GW, GH, COLS, CW, CH, GAP);
|
||||
meow_lazygrid_set_vsource(g, lg_count, lg_bind, NULL);
|
||||
g_count_override = 0;
|
||||
meow_lazygrid_set_empty_text(g, "No data");
|
||||
const meow_theme *t2 = meow_theme_get();
|
||||
meow_ui_clear(th->bg);
|
||||
meow_lazygrid_draw(g, t2);
|
||||
int thh = meow_ui_text_height(t2->font_body);
|
||||
int ety = GY + (GH - thh) / 2;
|
||||
CHECK(region_has(GX + GW / 2 - 60, ety, 120, thh, osv_r, osv_g, osv_b) > 2, "empty state text drawn");
|
||||
meow_lazygrid_free(g);
|
||||
}
|
||||
|
||||
/* ============ T5 meow_scroll 集成(fling/snap) ============ */
|
||||
{
|
||||
g_count_override = -1; g_disabled_at = -1;
|
||||
meow_lazygrid *g = meow_lazygrid_create(GX, GY, GW, GH, COLS, CW, CH, GAP);
|
||||
meow_lazygrid_set_vsource(g, lg_count, lg_bind, NULL);
|
||||
meow_scroll *s = meow_lazygrid_scroll_attach(g);
|
||||
CHECK(s != NULL, "scroll attach");
|
||||
meow_scroll_set_snap(s, ROWH);
|
||||
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");
|
||||
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 %d vs %d", end, start);
|
||||
/* snap 到行边界;若撞底(max_scroll=420 非 row_h 的整数倍)则停在边界(clamp 优先) */
|
||||
CHECK((end % ROWH) == 0 || end == meow_lazygrid_max_scroll(g),
|
||||
"fling ends snapped-or-boundary got %d (%%%d, max %d)", end, end % ROWH, meow_lazygrid_max_scroll(g));
|
||||
CHECK(end <= meow_lazygrid_max_scroll(g), "fling in range %d (max %d)", end, meow_lazygrid_max_scroll(g));
|
||||
CHECK(meow_lazygrid_scroll(g) == end, "grid scroll mirrors offset");
|
||||
meow_scroll_free(s);
|
||||
meow_lazygrid_free(g);
|
||||
}
|
||||
|
||||
meow_ui_shutdown();
|
||||
printf("lazygrid: %d pass / %d fail\n", g_pass, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
/* 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 meow_ui_nav.c \
|
||||
* -I. -Iinclude -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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int g_pass = 0, g_fail = 0;
|
||||
#define CHECK(cond, ...) do { \
|
||||
if (cond) { g_pass++; } \
|
||||
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
|
||||
} while (0)
|
||||
|
||||
/* ---- 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<count),回收合法重绑——但**有界**,
|
||||
* 每次 draw 至多重绑当前视口窗口(≤ win),绝不整表 N 重绑(可复用回收语义)。 */
|
||||
int before2 = g_bind_total;
|
||||
int win = (lvh / itemh) + 1; if (win > 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);
|
||||
meow_listview_set_container(lv, 0); /* 锁扁平基线:SDK 默认已变 MD3 容器,本测试专测扁平虚拟化渲染路径 */
|
||||
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_container(lv, 0); /* 锁扁平基线:SDK 默认已变 MD3 容器,段头 band 断言按扁平 */
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
/* 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 meow_ui_nav.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 运行:./test_meow_ui_scroll
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int g_pass = 0, g_fail = 0;
|
||||
#define CHECK(cond, ...) do { \
|
||||
if (cond) { g_pass++; } \
|
||||
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
|
||||
} while (0)
|
||||
|
||||
#define LIST_H 400
|
||||
#define ITEM_H 44
|
||||
#define COUNT 50
|
||||
/* max_scroll = COUNT*ITEM_H - LIST_H = 2200 - 400 = 1800 */
|
||||
#define MAXS (COUNT * ITEM_H - LIST_H)
|
||||
|
||||
static int src(int idx, char *label, int len, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
if (idx < 0 || idx >= COUNT) return 0;
|
||||
snprintf(label, (size_t)len, "row %d", idx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static meow_widget *make_list(void)
|
||||
{
|
||||
meow_widget *lv = meow_list_create(0, 0, 480, LIST_H, COUNT, src, NULL);
|
||||
meow_list_set_item_h(lv, ITEM_H);
|
||||
return lv;
|
||||
}
|
||||
|
||||
static void pump_until_idle(meow_scroll *s, int *max_off, int *mono_ok)
|
||||
{
|
||||
int prev = meow_scroll_offset(s);
|
||||
int guard = 0;
|
||||
while (meow_scroll_active(s) && guard++ < 300) {
|
||||
meow_anim_tick();
|
||||
int cur = meow_scroll_offset(s);
|
||||
if (*max_off < cur) *max_off = cur;
|
||||
if (cur < prev) *mono_ok = 0; /* 下划 fling 应单调不减(回弹段除外:此处只测无回弹) */
|
||||
prev = cur;
|
||||
usleep(10000);
|
||||
}
|
||||
meow_anim_tick();
|
||||
}
|
||||
|
||||
static void test_track_follow(void)
|
||||
{
|
||||
meow_widget *lv = make_list();
|
||||
meow_scroll *s = meow_scroll_wrap(lv, 0);
|
||||
CHECK(s != NULL, "wrap");
|
||||
CHECK(meow_scroll_offset(s) == 0, "init offset 0");
|
||||
meow_scroll_track(s, 10, meow_ui_time_ms());
|
||||
CHECK(meow_scroll_offset(s) == 10, "follow +10 got %d", meow_scroll_offset(s));
|
||||
/* 超量拖到底:clamp 到 max */
|
||||
meow_scroll_track(s, 100000, meow_ui_time_ms());
|
||||
CHECK(meow_scroll_offset(s) == MAXS, "clamp to max got %d", meow_scroll_offset(s));
|
||||
meow_scroll_free(s);
|
||||
meow_widget_free(lv);
|
||||
}
|
||||
|
||||
static void test_fling_no_boundary(void)
|
||||
{
|
||||
meow_widget *lv = make_list();
|
||||
meow_scroll *s = meow_scroll_wrap(lv, 0);
|
||||
/* 从 0 起拖 ~300px,建正初速 */
|
||||
uint64_t t = meow_ui_time_ms();
|
||||
for (int i = 0; i < 12; i++) meow_scroll_track(s, 25, t + (uint64_t)i * 16);
|
||||
int start = meow_scroll_offset(s);
|
||||
CHECK(start > 0, "dragged start>0 got %d", start);
|
||||
meow_scroll_fling(s);
|
||||
CHECK(meow_scroll_active(s) == 1, "fling active");
|
||||
int max_off = start, mono = 1, prev = start;
|
||||
int guard = 0;
|
||||
while (meow_scroll_active(s) && guard++ < 300) {
|
||||
meow_anim_tick();
|
||||
int cur = meow_scroll_offset(s);
|
||||
if (max_off < cur) max_off = cur;
|
||||
if (cur < prev) mono = 0;
|
||||
prev = cur;
|
||||
usleep(10000);
|
||||
}
|
||||
meow_anim_tick();
|
||||
int end = meow_scroll_offset(s);
|
||||
CHECK(mono == 1, "monotonic non-decreasing during fling");
|
||||
CHECK(end >= start, "fling advances end=%d start=%d", end, start);
|
||||
CHECK(end <= MAXS, "no overflow end=%d max=%d", end, MAXS);
|
||||
CHECK(meow_scroll_active(s) == 0, "settles idle");
|
||||
meow_scroll_free(s);
|
||||
meow_widget_free(lv);
|
||||
}
|
||||
|
||||
static void test_fling_boundary_bounce(void)
|
||||
{
|
||||
meow_widget *lv = make_list();
|
||||
meow_scroll *s = meow_scroll_wrap(lv, 0);
|
||||
/* 先拖到近底(~1700) */
|
||||
meow_list_set_scroll(lv, 1700);
|
||||
/* 快速下划,冲过底界 */
|
||||
uint64_t t = meow_ui_time_ms();
|
||||
for (int i = 0; i < 8; i++) meow_scroll_track(s, 40, t + (uint64_t)i * 16);
|
||||
meow_scroll_fling(s);
|
||||
int max_off = 0, mono = 1;
|
||||
pump_until_idle(s, &max_off, &mono);
|
||||
int end = meow_scroll_offset(s);
|
||||
CHECK(max_off <= MAXS, "never exceed max, max_off=%d max=%d", max_off, MAXS);
|
||||
CHECK(end == MAXS, "bounce settles at bottom end=%d", end); /* 回弹后回到底 */
|
||||
CHECK(meow_scroll_active(s) == 0, "idle after bounce");
|
||||
meow_scroll_free(s);
|
||||
meow_widget_free(lv);
|
||||
}
|
||||
|
||||
static void test_snap(void)
|
||||
{
|
||||
meow_widget *lv = make_list();
|
||||
meow_scroll *s = meow_scroll_wrap(lv, 0);
|
||||
meow_scroll_set_snap(s, ITEM_H); /* 吸附到行边界 */
|
||||
/* 慢拖到某非边界位置并给 small velocity → fling 结束 snap 到行倍数 */
|
||||
uint64_t t = meow_ui_time_ms();
|
||||
for (int i = 0; i < 6; i++) meow_scroll_track(s, 20, t + (uint64_t)i * 16);
|
||||
meow_scroll_fling(s);
|
||||
int max_off = 0, mono = 1;
|
||||
pump_until_idle(s, &max_off, &mono);
|
||||
int end = meow_scroll_offset(s);
|
||||
CHECK((end % ITEM_H) == 0, "snap to item multiple got %d", end);
|
||||
CHECK(end <= MAXS, "snap in range %d", end);
|
||||
meow_scroll_free(s);
|
||||
meow_widget_free(lv);
|
||||
}
|
||||
|
||||
static void test_stop(void)
|
||||
{
|
||||
meow_widget *lv = make_list();
|
||||
meow_scroll *s = meow_scroll_wrap(lv, 0);
|
||||
uint64_t t = meow_ui_time_ms();
|
||||
for (int i = 0; i < 6; i++) meow_scroll_track(s, 20, t + (uint64_t)i * 16);
|
||||
meow_scroll_fling(s);
|
||||
CHECK(meow_scroll_active(s) == 1, "fling active (stop test)");
|
||||
meow_scroll_stop(s);
|
||||
CHECK(meow_scroll_active(s) == 0, "stopped immediately");
|
||||
meow_scroll_free(s);
|
||||
meow_widget_free(lv);
|
||||
}
|
||||
|
||||
/* ============ 池槽归还回归(M19)============
|
||||
* meow_scroll_wrap 从 MEOW_SCROLL_POOL(=8) 个 wrap 上下文槽里占一个;旧实现 free 只归还
|
||||
* 会话槽、不归还 wrap 槽 → 进程内最多成功 wrap 8 次,之后恒 NULL(图库每次 load_dir 重建,
|
||||
* 进 8 个目录后列表丢惯性)。以下用例覆盖:长循环回收、free 后重建、交错不串槽、
|
||||
* 池满仍返回 NULL、直接 create 不误清槽、free(NULL)/二次 free 安全。 */
|
||||
#define WRAP_POOL 8 /* = meow_ui_scroll.c 的 MEOW_SCROLL_POOL(内部常量,测试按契约固定) */
|
||||
|
||||
/* 连续 64 次 wrap/free:每次都要拿到可用会话,且绑定当前列表 */
|
||||
static void test_wrap_free_recycle(void)
|
||||
{
|
||||
meow_widget *lv = make_list();
|
||||
int ok = 0, first_fail = -1;
|
||||
for (int i = 0; i < 64; i++) {
|
||||
meow_scroll *s = meow_scroll_wrap(lv, 0);
|
||||
if (!s) { if (first_fail < 0) first_fail = i; continue; }
|
||||
meow_list_set_scroll(lv, 0);
|
||||
meow_scroll_track(s, 7, meow_ui_time_ms());
|
||||
if (meow_scroll_offset(s) == 7) ok++;
|
||||
else if (first_fail < 0) first_fail = i; /* 串槽:set/get 不指向本列表 */
|
||||
meow_scroll_free(s);
|
||||
}
|
||||
CHECK(ok == 64, "64 wrap/free cycles all usable, ok=%d first_fail=%d", ok, first_fail);
|
||||
meow_widget_free(lv);
|
||||
}
|
||||
|
||||
/* wrap → free → 同一列表再 wrap:必须重新可用 */
|
||||
static void test_wrap_after_free(void)
|
||||
{
|
||||
meow_widget *lv = make_list();
|
||||
meow_scroll *s = meow_scroll_wrap(lv, 0);
|
||||
CHECK(s != NULL, "wrap#1 same list");
|
||||
meow_scroll_free(s);
|
||||
s = meow_scroll_wrap(lv, 0);
|
||||
CHECK(s != NULL, "wrap#2 same list after free");
|
||||
if (s) {
|
||||
meow_list_set_scroll(lv, 0);
|
||||
meow_scroll_track(s, 12, meow_ui_time_ms());
|
||||
CHECK(meow_scroll_offset(s) == 12, "re-wrapped scroll works got %d", meow_scroll_offset(s));
|
||||
meow_scroll_free(s);
|
||||
}
|
||||
meow_widget_free(lv);
|
||||
}
|
||||
|
||||
/* 8 个同时活跃 → 第 9 个仍返回 NULL(池满不越界);释放其一的槽立刻可再用 */
|
||||
static void test_wrap_pool_full(void)
|
||||
{
|
||||
meow_widget *lv[WRAP_POOL];
|
||||
meow_scroll *s[WRAP_POOL];
|
||||
int ok = 0;
|
||||
for (int i = 0; i < WRAP_POOL; i++) {
|
||||
lv[i] = make_list();
|
||||
s[i] = meow_scroll_wrap(lv[i], 0);
|
||||
if (s[i]) ok++;
|
||||
}
|
||||
CHECK(ok == WRAP_POOL, "all %d wraps active, ok=%d", WRAP_POOL, ok);
|
||||
|
||||
meow_widget *extra_lv = make_list();
|
||||
meow_scroll *extra = meow_scroll_wrap(extra_lv, 0);
|
||||
CHECK(extra == NULL, "9th simultaneous wrap returns NULL (pool full), got %p", (void *)extra);
|
||||
|
||||
/* 释放中间一个 → 槽立即回收;活着的实例仍各自绑定自己的列表 */
|
||||
meow_scroll_free(s[3]);
|
||||
s[3] = NULL;
|
||||
meow_scroll *again = meow_scroll_wrap(extra_lv, 0);
|
||||
CHECK(again != NULL, "wrap succeeds right after freeing one slot");
|
||||
if (again) {
|
||||
meow_list_set_scroll(extra_lv, 0);
|
||||
meow_scroll_track(again, 9, meow_ui_time_ms());
|
||||
CHECK(meow_scroll_offset(again) == 9, "new wrap bound to its own list got %d",
|
||||
meow_scroll_offset(again));
|
||||
meow_list_set_scroll(lv[5], 33);
|
||||
CHECK(meow_scroll_offset(s[5]) == 33, "other live wrap keeps its own list got %d",
|
||||
meow_scroll_offset(s[5]));
|
||||
meow_scroll_free(again);
|
||||
}
|
||||
for (int i = 0; i < WRAP_POOL; i++) {
|
||||
if (s[i]) meow_scroll_free(s[i]);
|
||||
meow_widget_free(lv[i]);
|
||||
}
|
||||
meow_widget_free(extra_lv);
|
||||
}
|
||||
|
||||
/* 交错:两个 wrap 活跃 → free 其一 → 再 wrap,三者不串槽 */
|
||||
static void test_wrap_interleaved(void)
|
||||
{
|
||||
meow_widget *a = make_list(), *b = make_list(), *c = make_list();
|
||||
meow_scroll *sa = meow_scroll_wrap(a, 0);
|
||||
meow_scroll *sb = meow_scroll_wrap(b, 0);
|
||||
CHECK(sa != NULL && sb != NULL, "two wraps (interleaved) a=%p b=%p", (void *)sa, (void *)sb);
|
||||
if (!sa || !sb) {
|
||||
meow_scroll_free(sa); meow_scroll_free(sb);
|
||||
meow_widget_free(a); meow_widget_free(b); meow_widget_free(c);
|
||||
return;
|
||||
}
|
||||
meow_list_set_scroll(a, 11);
|
||||
meow_list_set_scroll(b, 22);
|
||||
CHECK(meow_scroll_offset(sa) == 11 && meow_scroll_offset(sb) == 22,
|
||||
"distinct ctx a=%d b=%d", meow_scroll_offset(sa), meow_scroll_offset(sb));
|
||||
|
||||
meow_scroll_free(sa);
|
||||
CHECK(meow_scroll_offset(sb) == 22, "remaining wrap intact after freeing first got %d",
|
||||
meow_scroll_offset(sb));
|
||||
|
||||
meow_scroll *sc = meow_scroll_wrap(c, 0);
|
||||
CHECK(sc != NULL, "third wrap after freeing first");
|
||||
if (sc) {
|
||||
meow_list_set_scroll(c, 0);
|
||||
meow_scroll_track(sc, 5, meow_ui_time_ms());
|
||||
CHECK(meow_scroll_offset(sc) == 5, "third wrap bound to its own list got %d",
|
||||
meow_scroll_offset(sc));
|
||||
CHECK(meow_scroll_offset(sb) == 22, "second wrap not clobbered by third got %d",
|
||||
meow_scroll_offset(sb));
|
||||
meow_scroll_free(sc);
|
||||
}
|
||||
meow_scroll_free(sb);
|
||||
meow_widget_free(a); meow_widget_free(b); meow_widget_free(c);
|
||||
}
|
||||
|
||||
/* ---- 直接 create(非 wrap)的回调:用于验证 free 不误清别人的 wrap 槽 ---- */
|
||||
static int g_direct_off;
|
||||
static void direct_set(void *c, int off) { (void)c; g_direct_off = off; }
|
||||
static int direct_max(void *c) { (void)c; return 1000; }
|
||||
static int direct_get(void *c) { (void)c; return g_direct_off; }
|
||||
|
||||
static void test_direct_create_free_keeps_wrap_slot(void)
|
||||
{
|
||||
meow_widget *lv = make_list();
|
||||
meow_scroll *w = meow_scroll_wrap(lv, 0);
|
||||
CHECK(w != NULL, "wrap before direct create");
|
||||
meow_scroll *d = meow_scroll_create(direct_set, direct_max, direct_get, NULL, NULL);
|
||||
CHECK(d != NULL, "direct create");
|
||||
if (d) {
|
||||
meow_scroll_track(d, 40, meow_ui_time_ms());
|
||||
CHECK(g_direct_off == 40, "direct scroll tracks got %d", g_direct_off);
|
||||
meow_scroll_free(d); /* 非 wrap 实例:不得归还/清除任何 wrap 槽 */
|
||||
}
|
||||
if (w) {
|
||||
meow_list_set_scroll(lv, 0);
|
||||
meow_scroll_track(w, 10, meow_ui_time_ms());
|
||||
CHECK(meow_scroll_offset(w) == 10, "wrap intact after freeing direct instance got %d",
|
||||
meow_scroll_offset(w));
|
||||
meow_scroll_free(w);
|
||||
}
|
||||
meow_widget_free(lv);
|
||||
}
|
||||
|
||||
/* free(NULL) 安全;同一实例二次 free 不崩、不清活着的 wrap 槽 */
|
||||
static void test_wrap_free_null_and_double(void)
|
||||
{
|
||||
meow_scroll_free(NULL); /* 必须安全 */
|
||||
|
||||
meow_widget *a = make_list(), *b = make_list();
|
||||
meow_scroll *sa = meow_scroll_wrap(a, 0);
|
||||
meow_scroll *sb = meow_scroll_wrap(b, 0);
|
||||
CHECK(sa != NULL && sb != NULL, "two wraps (double free) a=%p b=%p", (void *)sa, (void *)sb);
|
||||
if (sb) meow_list_set_scroll(b, 44);
|
||||
if (sa) meow_scroll_free(sa);
|
||||
if (sa) meow_scroll_free(sa); /* 二次 free:重复清零,不得清掉 sb 的 ctx */
|
||||
if (sb) {
|
||||
CHECK(meow_scroll_offset(sb) == 44, "live wrap intact after double free got %d",
|
||||
meow_scroll_offset(sb));
|
||||
meow_scroll_free(sb);
|
||||
}
|
||||
meow_scroll *sc = meow_scroll_wrap(a, 0);
|
||||
CHECK(sc != NULL, "wrap still available after free(NULL)/double free");
|
||||
if (sc) meow_scroll_free(sc);
|
||||
meow_widget_free(a); meow_widget_free(b);
|
||||
}
|
||||
|
||||
/* 虚拟化列表行源(仅需 count 有效,不绘制) */
|
||||
static int lv_src2(int idx, meow_list_item *out, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
if (idx < 0 || idx >= COUNT) return 0;
|
||||
memset(out, 0, sizeof *out);
|
||||
out->title = "row";
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* 槽复用不继承上一个 wrap 的 kind:list → listview → list 交替必须各自生效
|
||||
* (free 时清 lv 标记,wrap 时按参数重设;否则复用槽上的旧 kind 会让 set/get 打空) */
|
||||
static void test_wrap_slot_kind_reset(void)
|
||||
{
|
||||
meow_widget *plain = make_list();
|
||||
meow_widget *vl = meow_listview_create(0, 0, 480, LIST_H, COUNT, lv_src2, NULL,
|
||||
MEOW_LIST_SEL_SINGLE);
|
||||
meow_listview_set_item_h(vl, ITEM_H);
|
||||
|
||||
meow_scroll *s = meow_scroll_wrap(plain, 0);
|
||||
CHECK(s != NULL, "wrap plain list");
|
||||
if (s) meow_scroll_free(s);
|
||||
|
||||
s = meow_scroll_wrap(vl, 1);
|
||||
CHECK(s != NULL, "wrap listview on recycled slot");
|
||||
if (s) {
|
||||
meow_listview_set_scroll(vl, 0);
|
||||
meow_scroll_track(s, 20, meow_ui_time_ms());
|
||||
CHECK(meow_scroll_offset(s) == 20, "listview wrapped scroll works got %d",
|
||||
meow_scroll_offset(s));
|
||||
meow_scroll_free(s);
|
||||
}
|
||||
|
||||
s = meow_scroll_wrap(plain, 0);
|
||||
CHECK(s != NULL, "wrap plain list again on recycled slot");
|
||||
if (s) {
|
||||
meow_list_set_scroll(plain, 0);
|
||||
meow_scroll_track(s, 15, meow_ui_time_ms());
|
||||
CHECK(meow_scroll_offset(s) == 15, "plain list after listview slot reuse got %d",
|
||||
meow_scroll_offset(s));
|
||||
meow_scroll_free(s);
|
||||
}
|
||||
meow_widget_free(vl);
|
||||
meow_widget_free(plain);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
meow_ui_opts o = { 480, 720 };
|
||||
if (meow_ui_init(&o) != 0) { printf("init failed\n"); return 1; }
|
||||
/* 池槽回归先跑:新建进程里第一个动 pool 的用例,便于对照「修复前 8/64、修复后 64/64」 */
|
||||
test_wrap_free_recycle();
|
||||
test_track_follow();
|
||||
test_fling_no_boundary();
|
||||
test_fling_boundary_bounce();
|
||||
test_snap();
|
||||
test_stop();
|
||||
test_wrap_after_free();
|
||||
test_wrap_pool_full();
|
||||
test_wrap_interleaved();
|
||||
test_direct_create_free_keeps_wrap_slot();
|
||||
test_wrap_slot_kind_reset();
|
||||
test_wrap_free_null_and_double();
|
||||
meow_ui_shutdown();
|
||||
printf("scroll: %d pass / %d fail\n", g_pass, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
/* test_meow_ui_select.c — 宿主验证枚举/预设选择器(meow_select):
|
||||
* T1 打开→点选项→单选高亮(primaryContainer)+ 选中 + 关闭即回调(on_change(idx))+ 返回值。
|
||||
* T2 取消:点面板外(遮罩)→ 返回 -1,不调 on_change。
|
||||
* T3 meow_select_show 一次性:返回选中下标。
|
||||
*
|
||||
* 阻塞式模态用后台线程驱动(同 test_ime_fix 范式),主线程经 meow_input_inject 喂触摸。
|
||||
* 编译(宿主 gcc):
|
||||
* gcc -o test_meow_ui_select test_meow_ui_select.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_select.c meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 运行:./test_meow_ui_select
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h" /* meow_input_inject + back_buf 读 */
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <pthread.h>
|
||||
|
||||
static int g_pass = 0, g_fail = 0;
|
||||
#define CHECK(cond, ...) do { \
|
||||
if (cond) { g_pass++; } \
|
||||
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
|
||||
} while (0)
|
||||
|
||||
static void 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; }
|
||||
#define UNPK(c, rr, gg, bb) do { (rr) = ((c) >> 16) & 0xFF; (gg) = ((c) >> 8) & 0xFF; (bb) = (c) & 0xFF; } while (0)
|
||||
|
||||
/* ---- 驱动:后台线程跑阻塞 meow_select_open ---- */
|
||||
typedef struct {
|
||||
meow_select *s;
|
||||
int ret;
|
||||
} sel_arg;
|
||||
static void *sel_driver(void *p)
|
||||
{
|
||||
sel_arg *a = (sel_arg *)p;
|
||||
a->ret = meow_select_open(a->s);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* ---- on_change 回调记录 ---- */
|
||||
static int g_cb = -1;
|
||||
static void on_change(int idx, void *ctx) { (void)ctx; g_cb = idx; }
|
||||
|
||||
/* 计算面板/选项列表几何(与 meow_select_open 同式;用于注入命中坐标 + 像素采样) */
|
||||
static int sel_list_y(int nitems)
|
||||
{
|
||||
const meow_theme *th = meow_theme_get();
|
||||
int W = meow_ui_width(), H = meow_ui_height();
|
||||
int sb = meow_ui_statusbar_height(); if (sb < 0) sb = 0;
|
||||
int pad = 24, rowh = 48;
|
||||
int title_h = meow_ui_text_height(th->font_title);
|
||||
int avail = H - sb - 20;
|
||||
int min_h = pad + title_h + 12 + rowh + 20;
|
||||
int want = min_h;
|
||||
int est = pad + title_h + 12 + nitems * rowh + 20;
|
||||
if (est > want) want = est;
|
||||
if (want > avail) want = avail;
|
||||
if (want < min_h) want = (min_h < avail) ? min_h : avail;
|
||||
int py = H - want;
|
||||
return py + pad + title_h + 12;
|
||||
}
|
||||
static int sel_sheet_top(int nitems)
|
||||
{
|
||||
const meow_theme *th = meow_theme_get();
|
||||
int H = meow_ui_height();
|
||||
int sb = meow_ui_statusbar_height(); if (sb < 0) sb = 0;
|
||||
int pad = 24, rowh = 48;
|
||||
int title_h = meow_ui_text_height(th->font_title);
|
||||
int avail = H - sb - 20;
|
||||
int min_h = pad + title_h + 12 + rowh + 20;
|
||||
int want = min_h;
|
||||
int est = pad + title_h + 12 + nitems * rowh + 20;
|
||||
if (est > want) want = est;
|
||||
if (want > avail) want = avail;
|
||||
if (want < min_h) want = (min_h < avail) ? min_h : avail;
|
||||
return H - want;
|
||||
}
|
||||
#define SEL_ROW 48
|
||||
|
||||
/* ============ T1 选中 + 回调 + 单选高亮 ============ */
|
||||
static void test_select_highlight(void)
|
||||
{
|
||||
meow_select *s = meow_select_create("Choose", 2);
|
||||
meow_select_add_item(s, "Alpha");
|
||||
meow_select_add_item(s, "Beta");
|
||||
meow_select_add_item(s, "Gamma");
|
||||
meow_select_set_on_change(s, on_change, NULL);
|
||||
int list_y = sel_list_y(3);
|
||||
sel_arg a = { s, -999 };
|
||||
g_cb = -1;
|
||||
pthread_t th; pthread_create(&th, NULL, sel_driver, &a);
|
||||
/* 等 open 的 drain + 首帧绘制完成(高亮已画进 back_buf) */
|
||||
usleep(150000);
|
||||
/* 单选高亮:cur=2(第 3 行)选中背景 = primaryContainer */
|
||||
const meow_theme *thm = meow_theme_get();
|
||||
int pc_r, pc_g, pc_b; UNPK(thm->primaryContainer, pc_r, pc_g, pc_b);
|
||||
int hx = 12, hy = list_y + 2 * SEL_ROW + SEL_ROW / 2; /* 容器卡横向内缩 8px(MEOW_LISTV_CARD_MARGIN),探针取卡内 */
|
||||
int r, g, b; rd_px(hx, hy, &r, &g, &b);
|
||||
CHECK(near2(r, pc_r) && near2(g, pc_g) && near2(b, pc_b),
|
||||
"T1 highlight row2 primaryContainer got %02X%02X%02X", r, g, b);
|
||||
/* 点第 1 行(idx 0)→ 选中 + 关闭 + 回调 */
|
||||
meow_input_inject(MEOW_EV_TAP, 100, list_y + 0 * SEL_ROW + SEL_ROW / 2);
|
||||
pthread_join(th, NULL);
|
||||
CHECK(a.ret == 0, "T1 open return idx0 got %d", a.ret);
|
||||
CHECK(g_cb == 0, "T1 on_change(0) got %d", g_cb);
|
||||
meow_select_free(s);
|
||||
}
|
||||
|
||||
/* ============ T2 取消(点面板外) ============ */
|
||||
static void test_select_cancel(void)
|
||||
{
|
||||
meow_select *s = meow_select_create("Choose", 0);
|
||||
meow_select_add_item(s, "Alpha");
|
||||
meow_select_add_item(s, "Beta");
|
||||
meow_select_set_on_change(s, on_change, NULL);
|
||||
sel_arg a = { s, -999 };
|
||||
g_cb = -1;
|
||||
pthread_t th; pthread_create(&th, NULL, sel_driver, &a);
|
||||
usleep(150000);
|
||||
int top = sel_sheet_top(2);
|
||||
meow_input_inject(MEOW_EV_TAP, 240, (top > 30) ? 10 : 10); /* 面板外(遮罩) */
|
||||
pthread_join(th, NULL);
|
||||
CHECK(a.ret == -1, "T2 cancel returns -1 got %d", a.ret);
|
||||
CHECK(g_cb == -1, "T2 on_change NOT fired on cancel got %d", g_cb);
|
||||
meow_select_free(s);
|
||||
}
|
||||
|
||||
/* ============ T3 meow_select_show 一次性 ============ */
|
||||
static void *show_driver(void *p)
|
||||
{
|
||||
(void)p;
|
||||
static const char *opts[] = { "A", "B", "C", "D" };
|
||||
static int r = -999;
|
||||
r = meow_select_show("Preset", opts, 4, 1);
|
||||
return (void *)(long)r;
|
||||
}
|
||||
static void test_select_show(void)
|
||||
{
|
||||
g_cb = -1;
|
||||
pthread_t th; pthread_create(&th, NULL, show_driver, NULL);
|
||||
usleep(150000);
|
||||
int list_y = sel_list_y(4);
|
||||
meow_input_inject(MEOW_EV_TAP, 100, list_y + 2 * SEL_ROW + SEL_ROW / 2); /* 选第 3 项 idx2 */
|
||||
long r = 0; pthread_join(th, (void **)&r);
|
||||
CHECK(r == 2, "T3 show returns idx2 got %ld", r);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
meow_ui_opts o = { 480, 720 };
|
||||
if (meow_ui_init(&o) != 0) { printf("init failed\n"); return 1; }
|
||||
test_select_highlight();
|
||||
test_select_cancel();
|
||||
test_select_show();
|
||||
meow_ui_shutdown();
|
||||
printf("select: %d pass / %d fail\n", g_pass, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -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. -Iinclude
|
||||
*
|
||||
* 两种模式(覆盖表按 locale 懒加载,分两次跑):
|
||||
* ./test_meow_ui_str tmp → unsetenv MEOW_DATA_DIR,仅验证内嵌双语表(无覆盖表)
|
||||
* ./test_meow_ui_str env → 设 MEOW_DATA_DIR=<mkdtemp>/data,写 <mkdtemp>/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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
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) {
|
||||
/* 建 <mkdtemp>/data + <mkdtemp>/strings/en_us.txt(meow_str 由 MEOW_DATA_DIR 去尾 /data
|
||||
* 得 <mkdtemp>/,再拼 strings/<locale>.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;
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
/* test_meow_ui_swipe.c — 宿主验证列表行左滑删除(§5.5,M7.3 缺口 #9):
|
||||
* T1 滑动手势判定与冲突消解:begin 命中可滑行返回 1;横向小位移 = swipe(move 返回 1 消费);
|
||||
* 纵向大位移 = 滚动手势优先(move 返回 0 并取消本行 swipe);非启用行 begin 返回 0。
|
||||
* T2 左滑超阈值 → 删除动画(meow_anim 滑出),结束后 on_delete 回调恰一次 + swipe 状态复位。
|
||||
* T3 未超阈值 → 回弹,on_delete 不触发,swipe_dx 回到 0。
|
||||
* T4 点按(无位移):begin 后 end(未 started)返回 0(App 走 tap_at),不触发 on_delete。
|
||||
* T5 右露删除区像素:滑动中重绘列表 → 右露区画主题 error 底 + 行内容左移。
|
||||
*
|
||||
* 编译(宿主 gcc):
|
||||
* gcc -o test_meow_ui_swipe test_meow_ui_swipe.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 meow_ui_nav.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 运行:./test_meow_ui_swipe
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int g_pass = 0, g_fail = 0;
|
||||
#define CHECK(cond, ...) do { \
|
||||
if (cond) { g_pass++; } \
|
||||
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
|
||||
} while (0)
|
||||
|
||||
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)
|
||||
|
||||
#define LVX 20
|
||||
#define LVY 50
|
||||
#define LVW 440
|
||||
#define LVH 300
|
||||
#define ITEM 56
|
||||
#define CNT 10
|
||||
|
||||
static int del_count; /* on_delete 回调次数 */
|
||||
static int del_idx; /* 最后一次删除 idx */
|
||||
static void on_delete(int idx, void *ctx) { (void)ctx; del_count++; del_idx = idx; }
|
||||
|
||||
static int scount(void *ctx) { (void)ctx; return CNT; }
|
||||
static void sbind(int idx, meow_lv_item *it, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
memset(it, 0, sizeof *it);
|
||||
static char t[32];
|
||||
snprintf(t, sizeof t, "notif %d", idx);
|
||||
it->title = t; it->icon_kind = MEOW_LIST_ICON_NONE; it->right = MEOW_LIST_RIGHT_NONE;
|
||||
it->enabled = 1;
|
||||
}
|
||||
|
||||
static meow_widget *make_list(void)
|
||||
{
|
||||
meow_widget *lv = meow_listview_create(LVX, LVY, LVW, LVH, 0, NULL, NULL, MEOW_LIST_SEL_NONE);
|
||||
meow_listview_set_item_h(lv, ITEM);
|
||||
meow_listview_set_vsource(lv, scount, sbind, NULL);
|
||||
meow_listview_set_item_renderer(lv, 2, 0, 0); /* 双行、无图标、无尾件 */
|
||||
meow_listview_set_swipe_delete(lv, 1, on_delete, NULL);
|
||||
return lv;
|
||||
}
|
||||
|
||||
/* 推进 meow_anim 直到 swipe 动画结束(模拟 on_tick 节拍)。 */
|
||||
static void pump_swipe(meow_widget *lv)
|
||||
{
|
||||
int guard = 0;
|
||||
while (meow_listview_swipe_active(lv) && guard++ < 80) {
|
||||
meow_anim_tick();
|
||||
usleep(10000);
|
||||
}
|
||||
meow_anim_tick();
|
||||
}
|
||||
|
||||
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 err_r, err_g, err_b; UNPK(th->error, err_r, err_g, err_b);
|
||||
|
||||
int row0y = LVY + ITEM / 2; /* 行 0 中心 y(scroll=0) */
|
||||
|
||||
/* ============ T1 手势判定 / 冲突消解 ============ */
|
||||
{
|
||||
meow_widget *lv = make_list();
|
||||
/* begin 命中可滑行行 → 1 */
|
||||
CHECK(meow_listview_swipe_begin(lv, 100, row0y) == 1, "T1 begin on swipe row = 1");
|
||||
/* 横向小位移 → 裁定 swipe,move 返回 1(消费) */
|
||||
CHECK(meow_listview_swipe_move(lv, 40, row0y) == 1, "T1 move horizontal = 1 (consumed)");
|
||||
int dx = meow_listview_swipe_offset(lv);
|
||||
CHECK(dx == -60, "T1 dx=-60 got %d", dx);
|
||||
/* 又一小位移(不超容差也不改变方向) */
|
||||
CHECK(meow_listview_swipe_move(lv, 30, row0y) == 1, "T1 move horizontal 2 = 1");
|
||||
CHECK(meow_listview_swipe_end(lv, 30, row0y) == 1, "T1 end started swipe = 1 (bounce)");
|
||||
pump_swipe(lv);
|
||||
CHECK(meow_listview_swipe_offset(lv) == 0, "T1 bounce back dx=0 got %d", meow_listview_swipe_offset(lv));
|
||||
CHECK(del_count == 0, "T1 no delete on bounce");
|
||||
meow_widget_free(lv);
|
||||
}
|
||||
{
|
||||
/* 纵向大位移 → 滚动手势优先(move 返回 0 + 取消) */
|
||||
meow_widget *lv = make_list();
|
||||
CHECK(meow_listview_swipe_begin(lv, 100, row0y) == 1, "T1 begin2 = 1");
|
||||
CHECK(meow_listview_swipe_move(lv, 100, row0y + 80) == 0, "T1 vertical move = 0 (yield to scroll)");
|
||||
CHECK(meow_listview_swipe_active(lv) == 0, "T1 vertical cancels swipe");
|
||||
CHECK(meow_listview_swipe_offset(lv) == 0, "T1 vertical dx=0");
|
||||
meow_widget_free(lv);
|
||||
}
|
||||
{
|
||||
/* 非启用行 begin → 0 */
|
||||
meow_widget *lv = make_list();
|
||||
meow_listview_set_swipe_delete(lv, 0, NULL, NULL);
|
||||
CHECK(meow_listview_swipe_begin(lv, 100, row0y) == 0, "T1 disabled begin = 0");
|
||||
meow_widget_free(lv);
|
||||
}
|
||||
|
||||
/* ============ T2 超阈值 → 删除动画 + on_delete ============ */
|
||||
{
|
||||
del_count = 0; del_idx = -1;
|
||||
meow_widget *lv = make_list();
|
||||
CHECK(meow_listview_swipe_begin(lv, 100, row0y) == 1, "T2 begin = 1");
|
||||
/* 左滑 200px(阈值=LVW*2/5=176)→ 超阈值 */
|
||||
CHECK(meow_listview_swipe_move(lv, 100 - 200, row0y) == 1, "T2 move big left = 1");
|
||||
CHECK(meow_listview_swipe_end(lv, 100 - 200, row0y) == 1, "T2 end = 1 (delete anim)");
|
||||
CHECK(meow_listview_swipe_active(lv) == 1, "T2 delete anim active");
|
||||
pump_swipe(lv);
|
||||
CHECK(del_count == 1, "T2 on_delete fired once got %d", del_count);
|
||||
CHECK(del_idx == 0, "T2 on_delete idx0 got %d", del_idx);
|
||||
CHECK(meow_listview_swipe_active(lv) == 0, "T2 state reset inactive");
|
||||
CHECK(meow_listview_swipe_offset(lv) == 0, "T2 dx reset 0 got %d", meow_listview_swipe_offset(lv));
|
||||
meow_widget_free(lv);
|
||||
}
|
||||
|
||||
/* ============ T3 未超阈值 → 回弹,on_delete 不触发 ============ */
|
||||
{
|
||||
del_count = 0;
|
||||
meow_widget *lv = make_list();
|
||||
CHECK(meow_listview_swipe_begin(lv, 100, row0y) == 1, "T3 begin = 1");
|
||||
CHECK(meow_listview_swipe_move(lv, 55, row0y) == 1, "T3 move left 45px = 1");
|
||||
CHECK(meow_listview_swipe_offset(lv) == -45, "T3 dx=-45 got %d", meow_listview_swipe_offset(lv));
|
||||
CHECK(meow_listview_swipe_end(lv, 55, row0y) == 1, "T3 end = 1 (bounce anim)");
|
||||
CHECK(meow_listview_swipe_active(lv) == 1, "T3 bounce active");
|
||||
pump_swipe(lv);
|
||||
CHECK(del_count == 0, "T3 on_delete NOT fired got %d", del_count);
|
||||
CHECK(meow_listview_swipe_offset(lv) == 0, "T3 bounce dx=0 got %d", meow_listview_swipe_offset(lv));
|
||||
CHECK(meow_listview_swipe_active(lv) == 0, "T3 inactive after bounce");
|
||||
meow_widget_free(lv);
|
||||
}
|
||||
|
||||
/* ============ T4 点按(无位移)= 行点击,end 返回 0 ============ */
|
||||
{
|
||||
del_count = 0;
|
||||
meow_widget *lv = make_list();
|
||||
CHECK(meow_listview_swipe_begin(lv, 100, row0y) == 1, "T4 begin = 1");
|
||||
CHECK(meow_listview_swipe_end(lv, 100, row0y) == 0, "T4 tap end = 0 (let App tap_at)");
|
||||
CHECK(del_count == 0, "T4 no delete on tap");
|
||||
CHECK(meow_listview_swipe_active(lv) == 0, "T4 inactive after tap");
|
||||
meow_widget_free(lv);
|
||||
}
|
||||
|
||||
/* ============ T5 右露删除区像素(默认 renderer) ============ */
|
||||
{
|
||||
meow_widget *lv = make_list();
|
||||
CHECK(meow_listview_swipe_begin(lv, 100, row0y) == 1, "T5 begin = 1");
|
||||
CHECK(meow_listview_swipe_move(lv, 100 - 200, row0y) == 1, "T5 move big left = 1");
|
||||
meow_ui_clear(th->bg);
|
||||
meow_widget_draw(lv);
|
||||
/* 右露区 [LVX+LVW+dx, row0, -dx, ITEM] = [260,50,200,56] → error 底 */
|
||||
CHECK(region_has(270, row0y - ITEM / 2 + 4, 180, ITEM - 8, err_r, err_g, err_b) > 50,
|
||||
"T5 delete reveal error bg got %d", region_has(270, row0y - ITEM / 2 + 4, 180, ITEM - 8, err_r, err_g, err_b));
|
||||
meow_listview_swipe_end(lv, 100 - 200, row0y);
|
||||
pump_swipe(lv);
|
||||
meow_widget_free(lv);
|
||||
}
|
||||
|
||||
meow_ui_shutdown();
|
||||
printf("swipe: %d pass / %d fail\n", g_pass, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -9,9 +9,10 @@
|
||||
* 编译(宿主 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_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 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_img.c meow_ui_media.c \
|
||||
* meow_notify_ipc.c stb_image.c meow_ui_nav.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
*
|
||||
* 验证要点:四套主题 primary/radius 各不相同(MD3 蓝 / MIUIx 橙 + 更大圆角),
|
||||
* 同 style 下 light/dark 的 surface 成对切换;图形用 MEOW_UI_DUMP 导出 PPM 目检。
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
/* test_meow_ui_theme_export.c — M7.0a 主题收口前置(缺口#6 SDK 半)宿主验证:
|
||||
* §8.1.5 色板导出/注入——meow_theme_export 拷当前权威主题;meow_theme_inject 覆写当前生效主题。
|
||||
*
|
||||
* 覆盖:
|
||||
* T1 导出一致性:export 后关键字段 == meow_theme_get()(当前 style×mode 权威板);
|
||||
* T2 注入:inject 一份自定义板 → meow_theme_get()/export 都返回注入板(单一权威反映);
|
||||
* 注入恢复内建:inject(NULL) 后回到目标 style×mode 的内建板;
|
||||
* T3 launcher 薄壳映射契约:导出板能映射出 meow_ui_theme_t 的 9 个角色字段(surface/
|
||||
* surface_variant/surface_container/primary/on_surface/on_surface_variant/outline/error/
|
||||
* surface_tint),值与 SDK meow_theme 对应字段一致。
|
||||
*
|
||||
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global):
|
||||
* gcc -Wall -Wextra -o test_meow_ui_theme_export test_meow_ui_theme_export.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 meow_ui_nav.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 运行:./test_meow_ui_theme_export
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
|
||||
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)
|
||||
|
||||
/* launcher meow_ui_theme_t 的 9 字段形状(测试内复刻字段名,不依赖 launcher 内部头)。 */
|
||||
typedef struct {
|
||||
uint32_t surface, surface_variant, surface_container;
|
||||
uint32_t primary, on_surface, on_surface_variant;
|
||||
uint32_t outline, error, surface_tint;
|
||||
} shell_theme;
|
||||
|
||||
static shell_theme export_shell(const meow_theme *t)
|
||||
{
|
||||
shell_theme s;
|
||||
s.surface = t->surface;
|
||||
s.surface_variant = t->surfaceVariant;
|
||||
s.surface_container = t->surfaceContainer;
|
||||
s.primary = t->primary;
|
||||
s.on_surface = t->onSurface;
|
||||
s.on_surface_variant= t->onSurfaceVariant;
|
||||
s.outline = t->outline;
|
||||
s.error = t->error;
|
||||
s.surface_tint = t->surfaceTint;
|
||||
return s;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
setenv("MEOW_UI_DUMP", "/tmp/theme_export.ppm", 1);
|
||||
meow_ui_opts o = { 480, 720 };
|
||||
assert(meow_ui_init(&o) == 0);
|
||||
|
||||
/* ---- T1 导出一致性(MD3 dark) ---- */
|
||||
printf("T1 export consistency\n");
|
||||
meow_theme_set_mode(MEOW_THEME_DARK);
|
||||
meow_ui_set_style(MEOW_STYLE_MD3);
|
||||
const meow_theme *t0 = meow_theme_get();
|
||||
meow_theme e0; meow_theme_export(&e0);
|
||||
CHECK(e0.surface == t0->surface, "export.surface == get.surface");
|
||||
CHECK(e0.primary == t0->primary, "export.primary == get.primary");
|
||||
CHECK(e0.surfaceVariant== t0->surfaceVariant,"export.surfaceVariant == get.surfaceVariant");
|
||||
CHECK(e0.onSurface == t0->onSurface, "export.onSurface == get.onSurface");
|
||||
CHECK(e0.radius == t0->radius, "export.radius == get.radius");
|
||||
CHECK(e0.bg == t0->bg, "export.bg == get.bg (background override applied)");
|
||||
|
||||
/* MIUIx light:确认导出随 style×mode 切换(不是固定板) */
|
||||
meow_ui_set_style(MEOW_STYLE_MIUIX);
|
||||
meow_theme_set_mode(MEOW_THEME_LIGHT);
|
||||
const meow_theme *t1 = meow_theme_get();
|
||||
meow_theme e1; meow_theme_export(&e1);
|
||||
CHECK(e1.primary == 0xF57200 && t1->primary == 0xF57200, "MIUIx light primary exported");
|
||||
CHECK(e1.bg == t1->bg && e1.surface == t1->surface, "MIUIx light bg/surface exported");
|
||||
CHECK(e1.surface == (uint32_t)0xF7F3EE, "MIUIx light surface == warm default");
|
||||
|
||||
/* ---- T2 注入覆写 + 恢复内建 ---- */
|
||||
printf("T2 inject override + restore\n");
|
||||
meow_theme custom; memset(&custom, 0, sizeof custom);
|
||||
custom.surface = 0x112233; custom.primary = 0xABCDEF; custom.onSurface = 0x0F0F0F;
|
||||
custom.radius = 99; custom.radius_card = 100;
|
||||
meow_theme_inject(&custom);
|
||||
const meow_theme *ti = meow_theme_get();
|
||||
meow_theme ei; meow_theme_export(&ei);
|
||||
CHECK(ti->primary == 0xABCDEF && ti->surface == 0x112233 && ti->radius == 99,
|
||||
"inject -> get() reads injected palette");
|
||||
CHECK(ei.primary == 0xABCDEF && ei.surface == 0x112233, "inject -> export reads injected palette");
|
||||
/* meow_theme_export == meow_theme_get even when injected(单一权威) */
|
||||
CHECK(ei.onSurface == ti->onSurface && ei.radius_card == ti->radius_card,
|
||||
"export == get while injected");
|
||||
/* 注入恢复内建 */
|
||||
meow_theme_inject(NULL);
|
||||
const meow_theme *tr = meow_theme_get();
|
||||
CHECK(tr->primary == 0xF57200 && tr->surface == 0xF7F3EE,
|
||||
"inject(NULL) restores builtin (MIUIx light)");
|
||||
|
||||
/* ---- T3 launcher 薄壳映射契约(9 角色字段) ---- */
|
||||
printf("T3 shell mapping contract\n");
|
||||
meow_theme_export(&e1);
|
||||
shell_theme sh = export_shell(&e1);
|
||||
CHECK(sh.surface == e1.surface, "shell.surface == surface");
|
||||
CHECK(sh.surface_variant == e1.surfaceVariant, "shell.surface_variant == surfaceVariant");
|
||||
CHECK(sh.surface_container == e1.surfaceContainer, "shell.surface_container == surfaceContainer");
|
||||
CHECK(sh.primary == e1.primary, "shell.primary == primary");
|
||||
CHECK(sh.on_surface == e1.onSurface, "shell.on_surface == onSurface");
|
||||
CHECK(sh.on_surface_variant == e1.onSurfaceVariant, "shell.on_surface_variant == onSurfaceVariant");
|
||||
CHECK(sh.outline == e1.outline, "shell.outline == outline");
|
||||
CHECK(sh.error == e1.error, "shell.error == error");
|
||||
CHECK(sh.surface_tint == e1.surfaceTint, "shell.surface_tint == surfaceTint");
|
||||
/* 关键角色字段值与 MIUIx light 内建一致(证明读的是 SDK 单一权威,而非 launcher 自持 4 套) */
|
||||
CHECK(sh.surface == 0xF7F3EE && sh.primary == 0xF57200 && sh.error == 0xB3261E,
|
||||
"shell roles match SDK builtin (single authority)");
|
||||
|
||||
meow_ui_shutdown();
|
||||
printf(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS\n", g_fail);
|
||||
return g_fail;
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/* test_meow_ui_view_event.c — M7.0a 缺口#7 宿主验证:meow_view 事件接线 + 递归 enter/leave。
|
||||
*
|
||||
* A 部(无 run 循环,直调 meow_view_*):递归 enter/leave 顺序 + 运行期动态挂子视图立即进入
|
||||
* (view.c meow_view_add_child 修正)+ 子>父 on_event 路由。
|
||||
* B 部(经 meow_ui_run 主循环):把语义事件注入队列 → run 循环先经 meow_view_dispatch_event
|
||||
* 路由,命中的事件不再落 App on_* 回调(on_tap/on_key/on_swipe/long_press 零触发),未命中
|
||||
* 的事件照常回落 App 回调——验证「接线于 meow_ui_run、于 §6.4 分流之上」。
|
||||
*
|
||||
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global):
|
||||
* gcc -Wall -Wextra -o test_meow_ui_view_event test_meow_ui_view_event.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 meow_ui_nav.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 运行:./test_meow_ui_view_event
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h" /* MEOW_EV_* */
|
||||
#include "meow_ui_view.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <pthread.h>
|
||||
#include <assert.h>
|
||||
|
||||
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)
|
||||
|
||||
/* ---------- A 部:递归 enter/leave 顺序 + 运行期动态挂子 ---------- */
|
||||
static int g_trace[16]; static int g_trace_n = 0;
|
||||
static void vtrace_enter(meow_view *v, void *ctx) { (void)ctx; g_trace[g_trace_n++] = v->id; }
|
||||
static void vtrace_leave(meow_view *v, void *ctx) { (void)ctx; g_trace[g_trace_n++] = -v->id; }
|
||||
static int g_child3_hits = 0;
|
||||
static int vev_child3(meow_view *v, int ev, int a, int b, void *ctx)
|
||||
{ (void)v;(void)ctx; if (ev == MEOW_EV_TAP && a == 33 && b == 33) { g_child3_hits++; return 1; } return 0; }
|
||||
|
||||
/* ---------- B 部:run 循环接线 ---------- */
|
||||
static int g_tap_calls = 0, g_key_calls = 0, g_swipe_calls = 0, g_lp_calls = 0;
|
||||
static void app_on_tap(int x, int y) { (void)x;(void)y; g_tap_calls++; }
|
||||
static void app_on_key(int k, int d) { (void)k;(void)d; g_key_calls++; }
|
||||
static void app_on_swipe(int dir, int dx, int dy) { (void)dir;(void)dx;(void)dy; g_swipe_calls++; }
|
||||
static void app_lp(int x, int y) { (void)x;(void)y; g_lp_calls++; }
|
||||
|
||||
/* 父 view:消费 TAP@(10,10) 与 LONG_PRESS(任何)。 */
|
||||
static int vev_parent(meow_view *v, int ev, int a, int b, void *ctx)
|
||||
{ (void)v;(void)ctx;
|
||||
if (ev == MEOW_EV_TAP && a == 10 && b == 10) return 1;
|
||||
if (ev == MEOW_EV_LONG_PRESS) return 1;
|
||||
return 0; }
|
||||
/* 子 view:消费 KEY(任何)与 SWIPE_LEFT。 */
|
||||
static int vev_child(meow_view *v, int ev, int a, int b, void *ctx)
|
||||
{ (void)v;(void)b;(void)ctx;
|
||||
if (ev == MEOW_EV_KEY) return 1;
|
||||
if (ev == MEOW_EV_SWIPE && a == MEOW_SWIPE_LEFT) return 1;
|
||||
return 0; }
|
||||
|
||||
static void *driver(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
usleep(700000); /* 等 startup cooldown(500ms) 过后再发 */
|
||||
meow_input_inject(MEOW_EV_KEY, MEOW_KEY_VOLUMEUP, 1); /* child 消费 → app_on_key 不触发 */
|
||||
usleep(80000);
|
||||
meow_input_inject(MEOW_EV_TAP, 10, 10); /* parent 消费 → app_on_tap 不触发 */
|
||||
usleep(80000);
|
||||
meow_input_inject(MEOW_EV_TAP, 50, 50); /* 无 view 命中 → 回落 app_on_tap */
|
||||
usleep(80000);
|
||||
meow_input_inject(MEOW_EV_SWIPE, MEOW_SWIPE_LEFT, 80); /* child 消费 → app_on_swipe 不触发 */
|
||||
usleep(80000);
|
||||
meow_input_inject(MEOW_EV_SWIPE, MEOW_SWIPE_RIGHT, 80); /* 无 view 命中 → 回落 app_on_swipe */
|
||||
usleep(80000);
|
||||
meow_input_inject(MEOW_EV_LONG_PRESS, 10, 10); /* parent 消费 → long_press 不触发 */
|
||||
usleep(80000);
|
||||
meow_ui_request_exit();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
setenv("MEOW_UI_DUMP", "/tmp/view_event.ppm", 1);
|
||||
meow_ui_opts o = { 480, 720 };
|
||||
assert(meow_ui_init(&o) == 0);
|
||||
|
||||
/* ================= A 部:递归 enter/leave + 动态挂子 ================= */
|
||||
printf("A: recursive enter/leave + runtime child add\n");
|
||||
{
|
||||
static const meow_view_hooks ph = { .on_enter = vtrace_enter, .on_leave = vtrace_leave,
|
||||
.on_event = vev_parent };
|
||||
static const meow_view_hooks ch = { .on_enter = vtrace_enter, .on_leave = vtrace_leave,
|
||||
.on_event = vev_child };
|
||||
static const meow_view_hooks c3 = { .on_enter = vtrace_enter, .on_leave = vtrace_leave,
|
||||
.on_event = vev_child3 };
|
||||
meow_view *pv = meow_view_create(1, MEOW_VIEW_ACTIVITY, &ph, NULL);
|
||||
meow_view *cv = meow_view_create(2, MEOW_VIEW_CONTAINER, &ch, NULL);
|
||||
assert(meow_view_add_child(pv, cv) == 0);
|
||||
g_trace_n = 0;
|
||||
meow_view_page_push(1); /* pv.enter → cv.enter */
|
||||
CHECK(g_trace_n == 2 && g_trace[0] == 1 && g_trace[1] == 2, "push: parent.enter -> child.enter");
|
||||
/* 运行期动态挂第三个子视图(父已 ENTERED)→ 立即进入(view.c 修正) */
|
||||
meow_view *c3v = meow_view_create(3, MEOW_VIEW_CONTAINER, &c3, NULL);
|
||||
g_trace_n = 0;
|
||||
assert(meow_view_add_child(pv, c3v) == 0);
|
||||
CHECK(g_trace_n == 1 && g_trace[0] == 3, "runtime add-child-while-entered -> child.enter fires");
|
||||
/* 新挂子 view 参与事件路由:命中 (33,33) 被子 3 消费 */
|
||||
g_child3_hits = 0;
|
||||
CHECK(meow_view_dispatch_event(MEOW_EV_TAP, 33, 33) == 1 && g_child3_hits == 1,
|
||||
"runtime-added child participates in dispatch");
|
||||
/* 离开:子视图(挂载序 2 → 3)先后离开 → 父(递归离开,子->父) */
|
||||
g_trace_n = 0;
|
||||
meow_view_page_pop();
|
||||
CHECK(g_trace_n == 3 && g_trace[0] == -2 && g_trace[1] == -3 && g_trace[2] == -1,
|
||||
"pop: child2.leave -> child3.leave -> parent.leave");
|
||||
meow_view_free(pv); /* 递归释放 2/3 */
|
||||
printf(" PASS A: recursive order + dynamic child\n");
|
||||
}
|
||||
|
||||
/* ================= B 部:meow_ui_run 接线(§6.4 之上) ================= */
|
||||
printf("B: meow_view_dispatch_event wired into meow_ui_run\n");
|
||||
{
|
||||
static const meow_view_hooks ph = { .on_event = vev_parent };
|
||||
static const meow_view_hooks ch = { .on_event = vev_child };
|
||||
meow_view *pv = meow_view_create(20, MEOW_VIEW_ACTIVITY, &ph, NULL);
|
||||
meow_view *cv = meow_view_create(21, MEOW_VIEW_CONTAINER, &ch, NULL);
|
||||
assert(meow_view_add_child(pv, cv) == 0);
|
||||
meow_view_page_push(20); /* 进入父+子(g_top_view=20) */
|
||||
|
||||
g_tap_calls = g_key_calls = g_swipe_calls = g_lp_calls = 0;
|
||||
meow_ui_set_long_press_cb(app_lp);
|
||||
meow_ui_events ev; memset(&ev, 0, sizeof ev);
|
||||
ev.on_tap = app_on_tap; ev.on_key = app_on_key; ev.on_swipe = app_on_swipe;
|
||||
|
||||
pthread_t thd; pthread_create(&thd, NULL, driver, NULL);
|
||||
meow_ui_run(&ev); /* 阻塞直到 driver 里 request_exit */
|
||||
pthread_join(thd, NULL);
|
||||
|
||||
CHECK(g_key_calls == 0, "KEY consumed by child -> app_on_key NOT called");
|
||||
CHECK(g_tap_calls == 1, "TAP(10,10) consumed by parent; TAP(50,50) fell through -> exactly 1 on_tap");
|
||||
CHECK(g_swipe_calls == 1, "SWIPE_LEFT consumed by child; SWIPE_RIGHT fell through -> exactly 1 on_swipe");
|
||||
CHECK(g_lp_calls == 0, "LONG_PRESS consumed by parent -> long_press cb NOT called");
|
||||
printf(" PASS B: run loop routes through views, fallback preserved\n");
|
||||
|
||||
meow_view_page_pop();
|
||||
meow_view_free(pv);
|
||||
}
|
||||
|
||||
meow_ui_shutdown();
|
||||
printf(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS\n", g_fail);
|
||||
return g_fail;
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
* 当前夹后音量(含 step 语义:a.volume_step 只决定步进,展示原样不/舍入)。
|
||||
*
|
||||
* 编译(宿主 gcc,mipsel 无关):
|
||||
* gcc -o test_meow_ui_vol test_meow_ui_vol.c meow_ui_vol.c -I.
|
||||
* gcc -o test_meow_ui_vol test_meow_ui_vol.c meow_ui_vol.c -I. -Iinclude
|
||||
*/
|
||||
#include "meow_ui_vol.h"
|
||||
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global):
|
||||
* gcc -o test_meow_ui_widgets test_meow_ui_widgets.c test_host_stub.c \
|
||||
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c \
|
||||
* meow_ui_draw.c meow_ui_view.c meow_ui_img.c meow_notify_ipc.c stb_image.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 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_img.c meow_ui_media.c \
|
||||
* meow_notify_ipc.c stb_image.c meow_ui_nav.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 运行:MEOW_UI_DUMP=/tmp/b2.ppm ./test_meow_ui_widgets (可选,dump 屏幕)
|
||||
*
|
||||
* 注:W_LISTVIEW 的 MEOW_LIST_ICON_IMG 分支调用 meow_ui_img_scaled → 宿主链 meow_ui_img.c+stb_image.c+-lm。
|
||||
@@ -201,6 +202,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");
|
||||
@@ -249,6 +319,7 @@ int main(void)
|
||||
* lv 位于 (20,240,320,112)item_h=56 → 行0 y240..296、行1 296..352。
|
||||
* 断言全部「画完读 back_buf 像素」(region_has/px_near),非目检。 */
|
||||
meow_widget *lv = W(meow_listview_create(20, 240, 320, 112, 4, lv_src, NULL, MEOW_LIST_SEL_SINGLE));
|
||||
meow_listview_set_container(lv, 0); /* 锁扁平基线:SDK 默认已变 MD3 容器,此测试专测 set_container(0) 扁平回退路径 */
|
||||
assert(meow_listview_set_item_h(lv, 56) == 56);
|
||||
const meow_theme *lth = meow_theme_get();
|
||||
int lvx = 20, lvy = 240, lvw = 320, lvh = 112, itemh = 56;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/* test_net_ipc.c — MeowOS SDK 网络自愿通道 宿主自测(非生产代码)。
|
||||
*
|
||||
* 通过链接 meow_net_ipc.c 调 meow_net_policy_check()/meow_net_report(),对运行中的 netmgrd
|
||||
* (MEOW_NET_SOCK 指向测试 socket)验证:
|
||||
* - meow_net_policy_check(host):按 MEOW_LOG_APP 实体判定 allow/block(netmgrd 决策权威);
|
||||
* - meow_net_report(rx,tx):best-effort 上报(netmgrd 记入 entity_bytes)。
|
||||
* 用法:cc -Iinclude -I. test_net_ipc.c meow_net_ipc.c -o test_net_ipc
|
||||
* 然后 MEOW_NET_SOCK=<sock> MEOW_LOG_APP=<pkg> ./test_net_ipc
|
||||
* 注:需运行中的 netmgrd;缺 netmgrd 时本测试无断言恒退 0,不能作为通过证据。
|
||||
*/
|
||||
#include "include/meow_ui_net.h"
|
||||
#include <stdio.h>
|
||||
|
||||
int main(void)
|
||||
{
|
||||
/* CHECK:域名放行/拒绝判定(netmgrd 决策)。 */
|
||||
int a = meow_net_policy_check("music.163.com");
|
||||
printf("CHECK_GOOD=%d\n", a); /* 期望 0(allow/放行) */
|
||||
int b = meow_net_policy_check("ads.example.com");
|
||||
printf("CHECK_BLOCK=%d\n", b); /* 期望 1(block/拒绝,若策略 deny 该域) */
|
||||
/* 空 host → 0 放行(防御)。 */
|
||||
int c = meow_net_policy_check(NULL);
|
||||
printf("CHECK_NULL=%d\n", c);
|
||||
/* REPORT:best-effort 上报。 */
|
||||
meow_net_report(12345, 67890);
|
||||
printf("REPORT_DONE\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -16,8 +16,9 @@
|
||||
* 编译(宿主 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 \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_ime.c meow_ui_str.c meow_ui_activity.c meow_ui_media.c \
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_notify_ipc.c meow_ui_nav.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 运行:./test_notif_gesture
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* gcc -Wall -o test_notif_open test_notif_open.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_notify_ipc.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 运行:./test_notif_open
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
*
|
||||
* 编译(宿主 gcc):
|
||||
* gcc -o test_notify_integration test_notify_integration.c \
|
||||
* meow_ui_notify.c meow_notify_ipc.c -I. -I../meow_launcherd -lpthread
|
||||
* meow_ui_notify.c meow_notify_ipc.c -I. -Iinclude -lpthread
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_notify_ipc.h"
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
*
|
||||
* 编译(宿主 gcc,不依赖 meow_ui.a 其余 TU):
|
||||
* gcc -o test_notify_ipc test_notify_ipc.c meow_notify_ipc.c \
|
||||
* -I. -I../meow_launcherd -lpthread
|
||||
* -I. -Iinclude -lpthread
|
||||
* 运行:./test_notify_ipc (全部 PASS 退出码 0,任一 FAIL 退出码 1)
|
||||
*/
|
||||
#include "meow_notify_ipc.h"
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
* 验证:SDK meow_notify_ipc_send() 产出的帧被 meow_notif_post() 真实落库并回分配 nid>0;
|
||||
* 随后 meow_notify_ipc_dismiss() 经 meow_notif_dismiss() 撤下(count 归 0)。
|
||||
*
|
||||
* 编译(宿主 gcc):
|
||||
* 编译(宿主 gcc;**需主仓组件,公开仓跳过**——../meow_launcherd/meow_notif.c 未随本仓发布):
|
||||
* gcc -o test_notify_post test_notify_post.c meow_notify_ipc.c \
|
||||
* ../meow_launcherd/meow_notif.c -I. -I../meow_launcherd -lpthread
|
||||
* ../meow_launcherd/meow_notif.c -I. -Iinclude -I../meow_launcherd -lpthread
|
||||
*/
|
||||
#include "meow_notify_ipc.h"
|
||||
#include "meow_ui.h"
|
||||
|
||||
+53
-24
@@ -1,21 +1,27 @@
|
||||
/* test_overlay_suppress.c — §10.5 叠加让位 present 自抑宿主验证(离屏 480x720,16bpp)。
|
||||
*
|
||||
* 背景:媒体播放中的 App 前台唤起通知中心时 launcher 不 SIGSTOP(保 PCM 供流),改由 SDK
|
||||
* present 层见 /tmp/meow_notif_active(宿主用 MEOW_NOTIF_ACTIVE_FILE 覆写)自抑上屏。
|
||||
* present 层见 /tmp/meow_notif_active(宿主用 MEOW_NOTIF_ACTIVE_FILE 覆写)自抑上屏;
|
||||
* 批①后台常驻后标记**内容化**:内容=当前前台 app_id(空=菜单/叠加=全抑制)。本测试同时验证
|
||||
* 批①多 App 常驻的「前台/后台」判定:内容==本进程 app_id(env MEOW_LOG_APP)→ 前台不抑制;
|
||||
* 内容为其它 app_id / 空 → 后台常驻/被覆盖 → 自抑。
|
||||
*
|
||||
* 覆盖:
|
||||
* T1 无标记:flush 正常上屏(frames_presented 递增,抑制计数 0);
|
||||
* T2 有标记:flush 被抑制(只画进 back_buf 不 present:frames 不变、抑制计数递增);
|
||||
* T2b 抑制期第二次 flush 仍被抑制(200ms 节流缓存内行为一致);
|
||||
* T3 标记消失:下一次 flush 恢复上屏(抑制期累积脏区一次上屏,无需重画);
|
||||
* T4 系统 UI 进程(launcher,system_mode=1)即使有标记也不抑制(面板必须照常上屏)。
|
||||
* T1 无标记:flush 正常上屏(frames_presented 递增,抑制计数 0);
|
||||
* T2 标记=其它 app_id(后台常驻)→ 抑制(只画进 back_buf 不上屏:frames 不变、计数递增);
|
||||
* T2b 抑制期第二次 flush 仍被抑制(200ms 节流缓存内行为一致);
|
||||
* T2c 标记=自身 app_id(前台 App)→ 不抑制(恢复上屏,恢复边沿整帧重画;计数不再增长);
|
||||
* T3 标记=空内容(菜单/叠加全抑制)→ 仍抑制;
|
||||
* T3b 标记消失 → 恢复上屏(抑制期累积脏区一次上屏,无需重画);
|
||||
* T4 系统 UI 进程(launcher,system_mode=1)即使有标记也不抑制(面板必须照常上屏)。
|
||||
*
|
||||
* 编译(宿主 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_draw.c meow_ui_view.c meow_notify_ipc.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread
|
||||
* 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_media.c \
|
||||
* meow_notify_ipc.c meow_ui_nav.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
@@ -26,13 +32,16 @@
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
#define MARKER "/tmp/test_notif_active_sdk"
|
||||
#define MARKER "/tmp/test_notif_active_sdk"
|
||||
#define SELF_ID "testapp"
|
||||
|
||||
static void marker_write(void)
|
||||
static void marker_write(const char *content)
|
||||
{
|
||||
int fd = open(MARKER, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||
assert(fd >= 0);
|
||||
if (write(fd, "1", 1) < 0) assert(0);
|
||||
if (content && content[0]) {
|
||||
if (write(fd, content, strlen(content)) < 0) assert(0);
|
||||
}
|
||||
close(fd);
|
||||
}
|
||||
|
||||
@@ -40,6 +49,7 @@ int main(void)
|
||||
{
|
||||
setenv("MEOW_UI_DUMP", "/tmp/overlay_suppress.ppm", 1);
|
||||
setenv("MEOW_NOTIF_ACTIVE_FILE", MARKER, 1);
|
||||
setenv("MEOW_LOG_APP", SELF_ID, 1); /* 本测试进程身份=前台/后台的判定锚 */
|
||||
unlink(MARKER);
|
||||
meow_ui_opts o = { 480, 720 };
|
||||
assert(meow_ui_init(&o) == 0);
|
||||
@@ -53,15 +63,15 @@ int main(void)
|
||||
assert(meow_gfx_overlay_suppressed_count() == 0);
|
||||
printf("T1 PASS: no marker -> presented=1 suppressed=0\n");
|
||||
|
||||
/* T2:有标记 → 抑制(等 >200ms 越过存在性节流缓存) */
|
||||
marker_write();
|
||||
/* T2:标记=其它 app_id(后台常驻 App)→ 抑制(等 >200ms 越过存在性节流缓存) */
|
||||
marker_write("ncm");
|
||||
usleep(250000);
|
||||
unsigned long long f0 = meow_ui_frames_presented();
|
||||
meow_ui_clear(0x223344);
|
||||
meow_ui_flush();
|
||||
assert(meow_ui_frames_presented() == f0);
|
||||
assert(meow_gfx_overlay_suppressed_count() == 1);
|
||||
printf("T2 PASS: marker present -> suppressed (frames frozen at %llu)\n", f0);
|
||||
printf("T2 PASS: marker=other app -> suppressed (frames frozen at %llu)\n", f0);
|
||||
|
||||
/* T2b:抑制期再次 flush 仍抑制(节流缓存内一致) */
|
||||
meow_ui_clear(0x334455);
|
||||
@@ -70,24 +80,43 @@ int main(void)
|
||||
assert(meow_gfx_overlay_suppressed_count() == 2);
|
||||
printf("T2b PASS: still suppressed within throttle window\n");
|
||||
|
||||
/* T3:标记消失 → 恢复上屏(抑制期累积的整帧脏一次上屏,无需重画) */
|
||||
/* T2c:标记=自身 app_id(前台 App)→ 不抑制(批①前后台判定核心;恢复边沿整帧重画一次上屏) */
|
||||
marker_write(SELF_ID);
|
||||
usleep(250000);
|
||||
meow_ui_clear(0x445566);
|
||||
meow_ui_flush();
|
||||
assert(meow_ui_frames_presented() == f0 + 1);
|
||||
assert(meow_gfx_overlay_suppressed_count() == 2); /* 不再增长(前台不抑制) */
|
||||
printf("T2c PASS: marker=self -> foreground, present resumed (accumulated dirty flushed)\n");
|
||||
|
||||
/* T3:标记=空内容(菜单/叠加层覆盖=全抑制)→ 仍抑制 */
|
||||
marker_write("");
|
||||
usleep(250000);
|
||||
unsigned long long f1 = meow_ui_frames_presented();
|
||||
meow_ui_clear(0x556677);
|
||||
meow_ui_flush();
|
||||
assert(meow_ui_frames_presented() == f1);
|
||||
assert(meow_gfx_overlay_suppressed_count() == 3);
|
||||
printf("T3 PASS: empty marker (menu/overlay) -> suppressed\n");
|
||||
|
||||
/* T3b:标记消失 → 恢复上屏(抑制期累积的整帧脏一次上屏) */
|
||||
unlink(MARKER);
|
||||
usleep(250000);
|
||||
meow_ui_flush();
|
||||
assert(meow_ui_frames_presented() == f0 + 1);
|
||||
assert(meow_gfx_overlay_suppressed_count() == 2);
|
||||
printf("T3 PASS: marker gone -> present resumed (accumulated dirty flushed)\n");
|
||||
assert(meow_ui_frames_presented() == f1 + 1);
|
||||
assert(meow_gfx_overlay_suppressed_count() == 3);
|
||||
printf("T3b PASS: marker gone -> present resumed (accumulated dirty flushed)\n");
|
||||
|
||||
/* T4:系统 UI 进程(launcher)不抑制——通知中心面板本身必须照常上屏 */
|
||||
meow_ui_sys_set_system_mode(1);
|
||||
assert(meow_ui_sys_render_enabled() == 1);
|
||||
marker_write();
|
||||
marker_write("ncm");
|
||||
usleep(250000);
|
||||
unsigned long long f1 = meow_ui_frames_presented();
|
||||
meow_ui_clear(0x445566);
|
||||
unsigned long long f2 = meow_ui_frames_presented();
|
||||
meow_ui_clear(0x667788);
|
||||
meow_ui_flush();
|
||||
assert(meow_ui_frames_presented() == f1 + 1);
|
||||
assert(meow_gfx_overlay_suppressed_count() == 2); /* 不再增长 */
|
||||
assert(meow_ui_frames_presented() == f2 + 1);
|
||||
assert(meow_gfx_overlay_suppressed_count() == 3); /* 不再增长 */
|
||||
unlink(MARKER);
|
||||
printf("T4 PASS: system-UI process (launcher) never suppressed\n");
|
||||
|
||||
|
||||
@@ -15,15 +15,16 @@
|
||||
* P2 list 焦点:命中非 list 区域时释放列表焦点、切页滚动(不再沿用旧焦点继续滚列表)。
|
||||
* P3 list 脏区:W_LIST 绘制已裁剪到视口;滚动(非整行偏移)+选中行不再向标题间隙/底缘下泄漏。
|
||||
*
|
||||
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime/img/config/
|
||||
* notify/ipc + 打桩 font/global(test_host_stub) + stb_image + hello 拆分层):
|
||||
* 编译(需主仓组件,公开仓跳过):本仓不含 ../hello_app(公开仓为 examples/hello),且本文件
|
||||
* 直接 #include "../hello_app/hello_internal.h";以下路径按主仓布局(打桩 font/global +
|
||||
* stb_image + hello 拆分层):
|
||||
* 1) 单独编译 app.c 并把 main 改名 hello_app_main(否则与本文件 main 冲突):
|
||||
* gcc -c -Wall -Wextra -DMEOW_UI_HOST_TEST -Dmain=hello_app_main \
|
||||
* -I. -I../hello_app ../hello_app/app.c -o app.o
|
||||
* -I. -Iinclude -I../hello_app ../hello_app/app.c -o app.o
|
||||
* 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 \
|
||||
@@ -36,7 +37,7 @@
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
#include "../examples/hello/hello_internal.h"
|
||||
#include "../hello_app/hello_internal.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
+3
-3
@@ -9,9 +9,9 @@
|
||||
* 编译(宿主 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_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
|
||||
* 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_img.c meow_ui_media.c \
|
||||
* meow_notify_ipc.c stb_image.c meow_ui_nav.c -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log + 打桩 font/global + meow_notify_ipc):
|
||||
* gcc -o test_sb_collapse test_sb_collapse.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_notify_ipc.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
|
||||
Loaded 100 of 176 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user